@nahisaho/satori 0.24.0 → 0.25.0
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.
- package/README.md +38 -22
- package/package.json +1 -1
- package/src/.github/skills/scientific-adaptive-experiments/SKILL.md +287 -0
- package/src/.github/skills/scientific-federated-learning/SKILL.md +241 -0
- package/src/.github/skills/scientific-multi-task-learning/SKILL.md +238 -0
- package/src/.github/skills/scientific-neural-architecture-search/SKILL.md +206 -0
- package/src/.github/skills/scientific-radiology-ai/SKILL.md +285 -0
- package/src/.github/skills/scientific-semi-supervised-learning/SKILL.md +210 -0
- package/src/.github/skills/scientific-statistical-simulation/SKILL.md +227 -0
- package/src/.github/skills/scientific-streaming-analytics/SKILL.md +221 -0
|
@@ -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 |
|