@nahisaho/satori 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ ---
2
+ name: scientific-federated-learning
3
+ description: |
4
+ 連合学習スキル。Flower フレームワークによる FL パイプライン・
5
+ FedAvg/FedProx/FedOpt 集約戦略・差分プライバシー (DP-SGD)・
6
+ 非 IID データ分割・通信効率化。
7
+ ---
8
+
9
+ # Scientific Federated Learning
10
+
11
+ プライバシー保護型分散機械学習を実現する連合学習パイプラインを提供する。
12
+
13
+ ## When to Use
14
+
15
+ - 複数施設・組織のデータを集約せずにモデル学習するとき
16
+ - 医療データ・個人情報を含むデータで ML を行うとき
17
+ - 差分プライバシーを適用した学習が必要なとき
18
+ - 非 IID データ分割下での連合学習を設計するとき
19
+ - 通信効率を考慮した分散学習を構築するとき
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ## 1. Flower 連合学習パイプライン
26
+
27
+ ```python
28
+ import flwr as fl
29
+ import numpy as np
30
+ from typing import Dict, List, Tuple, Optional
31
+
32
+
33
+ def create_fl_client(model, train_loader, val_loader,
34
+ device="cpu"):
35
+ """
36
+ Flower クライアント生成。
37
+
38
+ Parameters:
39
+ model: nn.Module — PyTorch モデル
40
+ train_loader: DataLoader — 訓練データ
41
+ val_loader: DataLoader — 検証データ
42
+ device: str — "cpu" / "cuda"
43
+ """
44
+ import torch
45
+
46
+ class SatoriFlClient(fl.client.NumPyClient):
47
+ def get_parameters(self, config):
48
+ return [val.cpu().numpy()
49
+ for val in model.parameters()]
50
+
51
+ def set_parameters(self, parameters):
52
+ for param, new_val in zip(model.parameters(), parameters):
53
+ param.data = torch.tensor(new_val).to(device)
54
+
55
+ def fit(self, parameters, config):
56
+ self.set_parameters(parameters)
57
+ model.train()
58
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
59
+ criterion = torch.nn.CrossEntropyLoss()
60
+
61
+ epochs = config.get("local_epochs", 1)
62
+ for _ in range(epochs):
63
+ for X, y in train_loader:
64
+ X, y = X.to(device), y.to(device)
65
+ optimizer.zero_grad()
66
+ loss = criterion(model(X), y)
67
+ loss.backward()
68
+ optimizer.step()
69
+
70
+ return self.get_parameters(config), len(train_loader.dataset), {}
71
+
72
+ def evaluate(self, parameters, config):
73
+ self.set_parameters(parameters)
74
+ model.eval()
75
+ criterion = torch.nn.CrossEntropyLoss()
76
+ total_loss, correct, total = 0.0, 0, 0
77
+
78
+ with torch.no_grad():
79
+ for X, y in val_loader:
80
+ X, y = X.to(device), y.to(device)
81
+ preds = model(X)
82
+ total_loss += criterion(preds, y).item() * len(y)
83
+ correct += (preds.argmax(1) == y).sum().item()
84
+ total += len(y)
85
+
86
+ return total_loss / total, total, {"accuracy": correct / total}
87
+
88
+ return SatoriFlClient()
89
+
90
+
91
+ def create_fl_strategy(algorithm="fedavg", min_clients=2,
92
+ fraction_fit=1.0, fraction_evaluate=1.0,
93
+ proximal_mu=0.1):
94
+ """
95
+ 連合学習集約戦略の選択。
96
+
97
+ Parameters:
98
+ algorithm: str — "fedavg" / "fedprox" / "fedopt" / "fedadam"
99
+ min_clients: int — 最小クライアント数
100
+ fraction_fit: float — 学習参加率
101
+ fraction_evaluate: float — 評価参加率
102
+ proximal_mu: float — FedProx 近接項の強度
103
+ """
104
+ common = dict(
105
+ min_fit_clients=min_clients,
106
+ min_evaluate_clients=min_clients,
107
+ min_available_clients=min_clients,
108
+ fraction_fit=fraction_fit,
109
+ fraction_evaluate=fraction_evaluate,
110
+ )
111
+
112
+ strategies = {
113
+ "fedavg": fl.server.strategy.FedAvg(**common),
114
+ "fedprox": fl.server.strategy.FedProx(
115
+ proximal_mu=proximal_mu, **common),
116
+ "fedadam": fl.server.strategy.FedAdam(
117
+ eta=1e-1, eta_l=1e-1, tau=1e-9, **common),
118
+ }
119
+
120
+ strategy = strategies.get(algorithm, strategies["fedavg"])
121
+ print(f"FL Strategy: {algorithm} | min_clients={min_clients}")
122
+ return strategy
123
+ ```
124
+
125
+ ## 2. 差分プライバシー (DP-SGD)
126
+
127
+ ```python
128
+ def apply_differential_privacy(model, train_loader,
129
+ target_epsilon=1.0,
130
+ target_delta=1e-5,
131
+ max_grad_norm=1.0,
132
+ noise_multiplier=1.1,
133
+ epochs=10, lr=1e-3):
134
+ """
135
+ Opacus DP-SGD による差分プライバシー学習。
136
+
137
+ Parameters:
138
+ model: nn.Module — PyTorch モデル
139
+ train_loader: DataLoader — 訓練データ
140
+ target_epsilon: float — プライバシーバジェット ε
141
+ target_delta: float — プライバシーパラメータ δ
142
+ max_grad_norm: float — 勾配クリッピングノルム
143
+ noise_multiplier: float — ノイズ乗数 σ
144
+ epochs: int — 学習エポック数
145
+ lr: float — 学習率
146
+ """
147
+ import torch
148
+ from opacus import PrivacyEngine
149
+
150
+ optimizer = torch.optim.SGD(model.parameters(), lr=lr)
151
+ privacy_engine = PrivacyEngine()
152
+
153
+ model, optimizer, train_loader = privacy_engine.make_private_with_epsilon(
154
+ module=model,
155
+ optimizer=optimizer,
156
+ data_loader=train_loader,
157
+ epochs=epochs,
158
+ target_epsilon=target_epsilon,
159
+ target_delta=target_delta,
160
+ max_grad_norm=max_grad_norm,
161
+ )
162
+
163
+ criterion = torch.nn.CrossEntropyLoss()
164
+ history = []
165
+
166
+ for epoch in range(epochs):
167
+ model.train()
168
+ total_loss = 0
169
+ for X, y in train_loader:
170
+ optimizer.zero_grad()
171
+ loss = criterion(model(X), y)
172
+ loss.backward()
173
+ optimizer.step()
174
+ total_loss += loss.item()
175
+
176
+ epsilon = privacy_engine.get_epsilon(delta=target_delta)
177
+ history.append({"epoch": epoch + 1,
178
+ "loss": total_loss / len(train_loader),
179
+ "epsilon": epsilon})
180
+ print(f"Epoch {epoch+1}: loss={total_loss/len(train_loader):.4f}, "
181
+ f"ε={epsilon:.2f}")
182
+
183
+ import pandas as pd
184
+ return pd.DataFrame(history)
185
+ ```
186
+
187
+ ## 3. 非 IID データ分割
188
+
189
+ ```python
190
+ def create_non_iid_splits(dataset_labels, n_clients=5,
191
+ alpha=0.5, seed=42):
192
+ """
193
+ Dirichlet 分布ベースの非 IID データ分割。
194
+
195
+ Parameters:
196
+ dataset_labels: np.ndarray — 全データのラベル配列
197
+ n_clients: int — クライアント数
198
+ alpha: float — Dirichlet α (小さいほど偏りが大きい)
199
+ seed: int — 乱数シード
200
+ """
201
+ rng = np.random.default_rng(seed)
202
+ n_classes = len(np.unique(dataset_labels))
203
+ client_indices = [[] for _ in range(n_clients)]
204
+
205
+ for c in range(n_classes):
206
+ class_idx = np.where(dataset_labels == c)[0]
207
+ proportions = rng.dirichlet(np.repeat(alpha, n_clients))
208
+ split_points = (np.cumsum(proportions) * len(class_idx)).astype(int)
209
+ splits = np.split(class_idx, split_points[:-1])
210
+ for i, split in enumerate(splits):
211
+ client_indices[i].extend(split.tolist())
212
+
213
+ # 分布サマリー
214
+ for i, indices in enumerate(client_indices):
215
+ labels = dataset_labels[indices]
216
+ unique, counts = np.unique(labels, return_counts=True)
217
+ dist = dict(zip(unique.tolist(), counts.tolist()))
218
+ print(f"Client {i}: {len(indices)} samples, dist={dist}")
219
+
220
+ return client_indices
221
+ ```
222
+
223
+ ---
224
+
225
+ ## パイプライン統合
226
+
227
+ ```
228
+ [プライバシー要件] → federated-learning → model-monitoring
229
+ (連合学習) (モデル監視)
230
+
231
+ deep-learning ← transfer-learning
232
+ (基盤 NN) (転移学習)
233
+ ```
234
+
235
+ ## パイプライン出力
236
+
237
+ | ファイル | 説明 | 次スキル |
238
+ |---------|------|---------|
239
+ | `fl_strategy_config.json` | FL 集約設定 | → サーバー起動 |
240
+ | `dp_training_history.csv` | DP 学習履歴 | → model-monitoring |
241
+ | `client_splits.json` | 非 IID 分割情報 | → FL クライアント |
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: scientific-multi-task-learning
3
+ description: |
4
+ マルチタスク学習スキル。Hard/Soft Parameter Sharing・
5
+ GradNorm 勾配正規化・PCGrad 勾配投影・
6
+ タスクバランシング・補助タスク設計。
7
+ ---
8
+
9
+ # Scientific Multi-Task Learning
10
+
11
+ 複数の関連タスクを同時に学習し、共有表現を活用して
12
+ 各タスクの汎化性能を向上させるパイプラインを提供する。
13
+
14
+ ## When to Use
15
+
16
+ - 複数の関連予測タスクを同時に実行するとき
17
+ - 共有表現を学習してデータ効率を高めたいとき
18
+ - 主タスク + 補助タスクの構成で学習するとき
19
+ - タスク間の勾配干渉を解消するとき
20
+ - マルチ出力回帰・分類を設計するとき
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ## 1. Hard Parameter Sharing MTL
27
+
28
+ ```python
29
+ import torch
30
+ import torch.nn as nn
31
+ from typing import Dict, List, Tuple
32
+
33
+
34
+ class HardSharingMTL(nn.Module):
35
+ """
36
+ Hard Parameter Sharing マルチタスクモデル。
37
+
38
+ 共有エンコーダ + タスク別ヘッドの構成。
39
+ """
40
+
41
+ def __init__(self, input_dim, shared_dims, task_configs):
42
+ """
43
+ Parameters:
44
+ input_dim: int — 入力次元
45
+ shared_dims: list[int] — 共有層のユニット数
46
+ task_configs: dict — {task_name: {"output_dim": int, "head_dims": [int]}}
47
+ """
48
+ super().__init__()
49
+
50
+ # 共有エンコーダ
51
+ layers = []
52
+ in_d = input_dim
53
+ for d in shared_dims:
54
+ layers.extend([nn.Linear(in_d, d), nn.ReLU(),
55
+ nn.BatchNorm1d(d), nn.Dropout(0.2)])
56
+ in_d = d
57
+ self.shared_encoder = nn.Sequential(*layers)
58
+
59
+ # タスク別ヘッド
60
+ self.task_heads = nn.ModuleDict()
61
+ for name, config in task_configs.items():
62
+ head_layers = []
63
+ h_in = in_d
64
+ for h_d in config.get("head_dims", [64]):
65
+ head_layers.extend([nn.Linear(h_in, h_d), nn.ReLU()])
66
+ h_in = h_d
67
+ head_layers.append(nn.Linear(h_in, config["output_dim"]))
68
+ self.task_heads[name] = nn.Sequential(*head_layers)
69
+
70
+ def forward(self, x):
71
+ shared = self.shared_encoder(x)
72
+ return {name: head(shared) for name, head in self.task_heads.items()}
73
+
74
+
75
+ def train_mtl_model(model, train_loader, task_losses,
76
+ task_weights=None, epochs=50,
77
+ lr=1e-3, device="cpu"):
78
+ """
79
+ MTL モデルの学習。
80
+
81
+ Parameters:
82
+ model: HardSharingMTL — MTL モデル
83
+ train_loader: DataLoader — {task_name: (X, y)} バッチ
84
+ task_losses: dict — {task_name: loss_fn}
85
+ task_weights: dict | None — {task_name: float} タスク重み
86
+ epochs: int — 学習エポック数
87
+ lr: float — 学習率
88
+ device: str — デバイス
89
+ """
90
+ import pandas as pd
91
+
92
+ if task_weights is None:
93
+ task_weights = {name: 1.0 for name in task_losses}
94
+
95
+ model.to(device)
96
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr)
97
+ history = []
98
+
99
+ for epoch in range(epochs):
100
+ model.train()
101
+ epoch_losses = {name: 0.0 for name in task_losses}
102
+
103
+ for batch in train_loader:
104
+ X = batch["X"].to(device)
105
+ outputs = model(X)
106
+ optimizer.zero_grad()
107
+
108
+ total_loss = 0
109
+ for name, loss_fn in task_losses.items():
110
+ y = batch[name].to(device)
111
+ task_loss = loss_fn(outputs[name], y)
112
+ total_loss += task_weights[name] * task_loss
113
+ epoch_losses[name] += task_loss.item()
114
+
115
+ total_loss.backward()
116
+ optimizer.step()
117
+
118
+ record = {"epoch": epoch + 1}
119
+ for name in task_losses:
120
+ record[f"loss_{name}"] = epoch_losses[name] / len(train_loader)
121
+ history.append(record)
122
+
123
+ if (epoch + 1) % 10 == 0:
124
+ losses_str = " | ".join(
125
+ f"{n}={epoch_losses[n]/len(train_loader):.4f}"
126
+ for n in task_losses)
127
+ print(f"Epoch {epoch+1}: {losses_str}")
128
+
129
+ return pd.DataFrame(history)
130
+ ```
131
+
132
+ ## 2. GradNorm — 動的タスクバランシング
133
+
134
+ ```python
135
+ def gradnorm_balance(model, task_losses, train_loader,
136
+ alpha=1.5, epochs=50, lr=1e-3, device="cpu"):
137
+ """
138
+ GradNorm による動的タスク重みバランシング。
139
+
140
+ Parameters:
141
+ model: HardSharingMTL — MTL モデル
142
+ task_losses: dict — {task_name: loss_fn}
143
+ train_loader: DataLoader
144
+ alpha: float — GradNorm 非対称度パラメータ
145
+ epochs: int — 学習エポック
146
+ lr: float — 学習率
147
+ device: str — デバイス
148
+ """
149
+ import pandas as pd
150
+
151
+ task_names = list(task_losses.keys())
152
+ n_tasks = len(task_names)
153
+ log_weights = torch.zeros(n_tasks, requires_grad=True, device=device)
154
+
155
+ model.to(device)
156
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr)
157
+ weight_optimizer = torch.optim.Adam([log_weights], lr=0.025)
158
+
159
+ initial_losses = None
160
+ history = []
161
+
162
+ for epoch in range(epochs):
163
+ model.train()
164
+ epoch_losses = {n: 0.0 for n in task_names}
165
+
166
+ for batch in train_loader:
167
+ X = batch["X"].to(device)
168
+ outputs = model(X)
169
+
170
+ weights = torch.softmax(log_weights, dim=0) * n_tasks
171
+ losses = []
172
+ for i, name in enumerate(task_names):
173
+ y = batch[name].to(device)
174
+ task_loss = task_losses[name](outputs[name], y)
175
+ losses.append(task_loss)
176
+ epoch_losses[name] += task_loss.item()
177
+
178
+ if initial_losses is None:
179
+ initial_losses = [l.item() for l in losses]
180
+
181
+ total_loss = sum(w * l for w, l in zip(weights, losses))
182
+
183
+ optimizer.zero_grad()
184
+ weight_optimizer.zero_grad()
185
+ total_loss.backward(retain_graph=True)
186
+
187
+ # GradNorm 更新
188
+ shared_params = list(model.shared_encoder.parameters())
189
+ norms = []
190
+ for l in losses:
191
+ g = torch.autograd.grad(l, shared_params[-1],
192
+ retain_graph=True)[0]
193
+ norms.append(torch.norm(g))
194
+
195
+ avg_norm = torch.stack(norms).mean()
196
+ loss_ratios = torch.tensor(
197
+ [l.item() / il for l, il in
198
+ zip(losses, initial_losses)], device=device)
199
+ relative_inv = loss_ratios / loss_ratios.mean()
200
+ target_norms = avg_norm * (relative_inv ** alpha)
201
+
202
+ gradnorm_loss = sum(
203
+ torch.abs(n - t) for n, t in
204
+ zip(norms, target_norms))
205
+ gradnorm_loss.backward()
206
+
207
+ optimizer.step()
208
+ weight_optimizer.step()
209
+
210
+ record = {"epoch": epoch + 1}
211
+ for i, name in enumerate(task_names):
212
+ record[f"loss_{name}"] = epoch_losses[name] / len(train_loader)
213
+ record[f"weight_{name}"] = (
214
+ torch.softmax(log_weights, 0) * n_tasks)[i].item()
215
+ history.append(record)
216
+
217
+ return pd.DataFrame(history)
218
+ ```
219
+
220
+ ---
221
+
222
+ ## パイプライン統合
223
+
224
+ ```
225
+ [複数タスク定義] → multi-task-learning → feature-importance
226
+ (共有表現学習) (特徴量解釈)
227
+
228
+ deep-learning ← transfer-learning
229
+ (基盤 NN) (転移学習)
230
+ ```
231
+
232
+ ## パイプライン出力
233
+
234
+ | ファイル | 説明 | 次スキル |
235
+ |---------|------|---------|
236
+ | `mtl_model.pt` | MTL モデル | → 推論 |
237
+ | `mtl_history.csv` | タスク別学習履歴 | → 可視化 |
238
+ | `gradnorm_weights.csv` | 動的タスク重み推移 | → バランシング分析 |
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: scientific-neural-architecture-search
3
+ description: |
4
+ ニューラルアーキテクチャ探索 (NAS) スキル。DARTS 微分可能 NAS・
5
+ Optuna NAS 統合・効率的ネットワーク設計・探索空間定義・
6
+ Pareto 最適化 (精度 vs FLOPS)。
7
+ ---
8
+
9
+ # Scientific Neural Architecture Search
10
+
11
+ ニューラルネットワーク構造の自動探索・最適化パイプラインを提供する。
12
+
13
+ ## When to Use
14
+
15
+ - NN アーキテクチャを自動的に最適化したいとき
16
+ - 精度と計算コストの Pareto 最適解を探索するとき
17
+ - 探索空間を定義して効率的な構造探索を行うとき
18
+ - DARTS 系の微分可能 NAS を実装するとき
19
+ - モデル圧縮・効率化を自動化するとき
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ## 1. Optuna NAS — ネットワーク構造探索
26
+
27
+ ```python
28
+ import optuna
29
+ import torch
30
+ import torch.nn as nn
31
+ from typing import Dict, Any
32
+
33
+
34
+ def optuna_nas(train_loader, val_loader, search_space=None,
35
+ n_trials=50, n_epochs=10, device="cpu",
36
+ direction="maximize", metric="accuracy"):
37
+ """
38
+ Optuna によるニューラルアーキテクチャ探索。
39
+
40
+ Parameters:
41
+ train_loader: DataLoader — 訓練データ
42
+ val_loader: DataLoader — 検証データ
43
+ search_space: dict | None — カスタム探索空間
44
+ n_trials: int — 試行回数
45
+ n_epochs: int — 各試行の学習エポック数
46
+ device: str — "cpu" / "cuda"
47
+ direction: str — "maximize" / "minimize"
48
+ metric: str — 最適化指標名
49
+ """
50
+ input_dim = next(iter(train_loader))[0].shape[1]
51
+ n_classes = len(torch.unique(
52
+ torch.cat([y for _, y in train_loader])))
53
+
54
+ def build_model(trial):
55
+ n_layers = trial.suggest_int("n_layers", 1, 5)
56
+ layers = []
57
+ in_features = input_dim
58
+
59
+ for i in range(n_layers):
60
+ out_features = trial.suggest_int(
61
+ f"n_units_l{i}", 16, 512, log=True)
62
+ layers.append(nn.Linear(in_features, out_features))
63
+
64
+ activation = trial.suggest_categorical(
65
+ f"activation_l{i}", ["relu", "gelu", "silu"])
66
+ act_map = {"relu": nn.ReLU(), "gelu": nn.GELU(),
67
+ "silu": nn.SiLU()}
68
+ layers.append(act_map[activation])
69
+
70
+ dropout = trial.suggest_float(
71
+ f"dropout_l{i}", 0.0, 0.5)
72
+ if dropout > 0:
73
+ layers.append(nn.Dropout(dropout))
74
+
75
+ use_bn = trial.suggest_categorical(
76
+ f"batchnorm_l{i}", [True, False])
77
+ if use_bn:
78
+ layers.append(nn.BatchNorm1d(out_features))
79
+
80
+ in_features = out_features
81
+
82
+ layers.append(nn.Linear(in_features, n_classes))
83
+ return nn.Sequential(*layers)
84
+
85
+ def objective(trial):
86
+ model = build_model(trial).to(device)
87
+ lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
88
+ optimizer_name = trial.suggest_categorical(
89
+ "optimizer", ["Adam", "AdamW", "SGD"])
90
+
91
+ opt_cls = getattr(torch.optim, optimizer_name)
92
+ optimizer = opt_cls(model.parameters(), lr=lr)
93
+ criterion = nn.CrossEntropyLoss()
94
+
95
+ for epoch in range(n_epochs):
96
+ model.train()
97
+ for X, y in train_loader:
98
+ X, y = X.to(device), y.to(device)
99
+ optimizer.zero_grad()
100
+ loss = criterion(model(X), y)
101
+ loss.backward()
102
+ optimizer.step()
103
+
104
+ # 枝刈り
105
+ model.eval()
106
+ correct, total = 0, 0
107
+ with torch.no_grad():
108
+ for X, y in val_loader:
109
+ X, y = X.to(device), y.to(device)
110
+ correct += (model(X).argmax(1) == y).sum().item()
111
+ total += len(y)
112
+
113
+ trial.report(correct / total, epoch)
114
+ if trial.should_prune():
115
+ raise optuna.TrialPruned()
116
+
117
+ return correct / total
118
+
119
+ study = optuna.create_study(
120
+ direction=direction,
121
+ pruner=optuna.pruners.MedianPruner(n_warmup_steps=3))
122
+ study.optimize(objective, n_trials=n_trials)
123
+
124
+ print(f"Best {metric}: {study.best_value:.4f}")
125
+ print(f"Best params: {study.best_params}")
126
+ return study
127
+
128
+
129
+ def nas_pareto_search(train_loader, val_loader,
130
+ n_trials=100, device="cpu"):
131
+ """
132
+ 多目的 NAS — 精度 vs モデルサイズの Pareto 最適化。
133
+
134
+ Parameters:
135
+ train_loader: DataLoader — 訓練データ
136
+ val_loader: DataLoader — 検証データ
137
+ n_trials: int — 試行回数
138
+ device: str — デバイス
139
+ """
140
+
141
+ def objective(trial):
142
+ n_layers = trial.suggest_int("n_layers", 1, 4)
143
+ total_params = 0
144
+ in_f = next(iter(train_loader))[0].shape[1]
145
+ n_classes = len(torch.unique(
146
+ torch.cat([y for _, y in val_loader])))
147
+
148
+ layers = []
149
+ for i in range(n_layers):
150
+ out_f = trial.suggest_int(f"units_{i}", 8, 256, log=True)
151
+ layers.append(nn.Linear(in_f, out_f))
152
+ layers.append(nn.ReLU())
153
+ total_params += in_f * out_f + out_f
154
+ in_f = out_f
155
+ layers.append(nn.Linear(in_f, n_classes))
156
+ total_params += in_f * n_classes + n_classes
157
+
158
+ model = nn.Sequential(*layers).to(device)
159
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
160
+ criterion = nn.CrossEntropyLoss()
161
+
162
+ for _ in range(5):
163
+ model.train()
164
+ for X, y in train_loader:
165
+ X, y = X.to(device), y.to(device)
166
+ optimizer.zero_grad()
167
+ criterion(model(X), y).backward()
168
+ optimizer.step()
169
+
170
+ model.eval()
171
+ correct, total = 0, 0
172
+ with torch.no_grad():
173
+ for X, y in val_loader:
174
+ X, y = X.to(device), y.to(device)
175
+ correct += (model(X).argmax(1) == y).sum().item()
176
+ total += len(y)
177
+
178
+ return correct / total, total_params
179
+
180
+ study = optuna.create_study(
181
+ directions=["maximize", "minimize"])
182
+ study.optimize(objective, n_trials=n_trials)
183
+
184
+ print(f"Pareto front: {len(study.best_trials)} solutions")
185
+ return study
186
+ ```
187
+
188
+ ---
189
+
190
+ ## パイプライン統合
191
+
192
+ ```
193
+ [タスク定義] → neural-architecture-search → deep-learning
194
+ (構造探索) (本格学習)
195
+
196
+ automl ← ensemble-methods
197
+ (HPO) (アンサンブル)
198
+ ```
199
+
200
+ ## パイプライン出力
201
+
202
+ | ファイル | 説明 | 次スキル |
203
+ |---------|------|---------|
204
+ | `nas_study.pkl` | Optuna Study | → 最良構造抽出 |
205
+ | `pareto_front.csv` | Pareto 最適解群 | → モデル選択 |
206
+ | `best_architecture.json` | 最良アーキテクチャ | → deep-learning |