@nahisaho/satori 0.23.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.
@@ -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 |
@@ -0,0 +1,285 @@
1
+ ---
2
+ name: scientific-radiology-ai
3
+ description: |
4
+ 放射線診断支援 AI スキル。CADe/CADx パイプライン・
5
+ CT/MRI 分類・セグメンテーション・Grad-CAM 説明可能性・
6
+ 構造化レポート・AI-RADS グレーディング。
7
+ ※ scientific-medical-imaging (DICOM/WSI/Radiomics) の
8
+ 放射線診断 AI 特化拡張。
9
+ ---
10
+
11
+ # Scientific Radiology AI
12
+
13
+ 放射線画像(CT/MRI/X 線)に対する AI 診断支援
14
+ パイプラインを提供する。MONAI ベースの学習・推論・
15
+ 説明可能性・構造化レポート生成を含む。
16
+
17
+ ## When to Use
18
+
19
+ - CT/MRI/X 線画像の AI 分類・セグメンテーションを行うとき
20
+ - CADe (検出) / CADx (診断) パイプラインを構築するとき
21
+ - Grad-CAM で AI 判断の説明可能性を付与するとき
22
+ - 構造化放射線レポートを自動生成するとき
23
+ - AI-RADS スコアリングを実装するとき
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ## 1. MONAI 放射線 AI 分類パイプライン
30
+
31
+ ```python
32
+ import numpy as np
33
+ import torch
34
+ import torch.nn as nn
35
+
36
+
37
+ def build_radiology_classifier(in_channels=1, num_classes=2,
38
+ spatial_dims=3,
39
+ architecture="densenet121"):
40
+ """
41
+ MONAI ベース放射線画像分類モデル。
42
+
43
+ Parameters:
44
+ in_channels: int — 入力チャネル数 (CT=1, MRI multimodal=4)
45
+ num_classes: int — クラス数
46
+ spatial_dims: int — 2 (2D スライス) or 3 (3D ボリューム)
47
+ architecture: str — "densenet121" / "resnet50" / "efficientnet"
48
+ """
49
+ import monai.networks.nets as nets
50
+
51
+ models = {
52
+ "densenet121": nets.DenseNet121(
53
+ spatial_dims=spatial_dims,
54
+ in_channels=in_channels,
55
+ out_channels=num_classes),
56
+ "resnet50": nets.ResNet(
57
+ block="bottleneck", layers=[3, 4, 6, 3],
58
+ block_inplanes=[64, 128, 256, 512],
59
+ spatial_dims=spatial_dims,
60
+ n_input_channels=in_channels,
61
+ num_classes=num_classes),
62
+ "efficientnet": nets.EfficientNetBN(
63
+ "efficientnet-b0",
64
+ spatial_dims=spatial_dims,
65
+ in_channels=in_channels,
66
+ num_classes=num_classes),
67
+ }
68
+ model = models.get(architecture, models["densenet121"])
69
+ total_params = sum(p.numel() for p in model.parameters())
70
+ print(f"Radiology classifier: {architecture} | "
71
+ f"{total_params:,} params | {spatial_dims}D")
72
+ return model
73
+
74
+
75
+ def train_radiology_model(model, train_loader, val_loader,
76
+ epochs=50, lr=1e-4, device="cuda"):
77
+ """
78
+ 放射線 AI モデル学習。
79
+
80
+ Parameters:
81
+ model: nn.Module — 分類モデル
82
+ train_loader: DataLoader — 訓練データ
83
+ val_loader: DataLoader — 検証データ
84
+ epochs: int — 学習エポック数
85
+ lr: float — 学習率
86
+ device: str — デバイス
87
+ """
88
+ import pandas as pd
89
+ from monai.utils import set_determinism
90
+ set_determinism(seed=42)
91
+
92
+ model.to(device)
93
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr,
94
+ weight_decay=1e-4)
95
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
96
+ optimizer, T_max=epochs)
97
+ criterion = nn.CrossEntropyLoss()
98
+ history = []
99
+
100
+ best_val_acc = 0
101
+ for epoch in range(epochs):
102
+ model.train()
103
+ train_loss, correct, total = 0, 0, 0
104
+ for batch in train_loader:
105
+ images = batch["image"].to(device)
106
+ labels = batch["label"].to(device)
107
+ optimizer.zero_grad()
108
+ outputs = model(images)
109
+ loss = criterion(outputs, labels)
110
+ loss.backward()
111
+ optimizer.step()
112
+ train_loss += loss.item()
113
+ correct += (outputs.argmax(1) == labels).sum().item()
114
+ total += len(labels)
115
+
116
+ scheduler.step()
117
+
118
+ # Validation
119
+ model.eval()
120
+ val_loss, val_correct, val_total = 0, 0, 0
121
+ with torch.no_grad():
122
+ for batch in val_loader:
123
+ images = batch["image"].to(device)
124
+ labels = batch["label"].to(device)
125
+ outputs = model(images)
126
+ val_loss += criterion(outputs, labels).item()
127
+ val_correct += (outputs.argmax(1) == labels).sum().item()
128
+ val_total += len(labels)
129
+
130
+ val_acc = val_correct / val_total
131
+ if val_acc > best_val_acc:
132
+ best_val_acc = val_acc
133
+ torch.save(model.state_dict(), "best_radiology_model.pt")
134
+
135
+ history.append({
136
+ "epoch": epoch + 1,
137
+ "train_loss": train_loss / len(train_loader),
138
+ "train_acc": correct / total,
139
+ "val_loss": val_loss / len(val_loader),
140
+ "val_acc": val_acc,
141
+ })
142
+
143
+ if (epoch + 1) % 10 == 0:
144
+ print(f"Epoch {epoch+1}: train_acc={correct/total:.3f}, "
145
+ f"val_acc={val_acc:.3f}")
146
+
147
+ print(f"Best val_acc: {best_val_acc:.4f}")
148
+ return pd.DataFrame(history)
149
+ ```
150
+
151
+ ## 2. Grad-CAM 説明可能性
152
+
153
+ ```python
154
+ def radiology_gradcam(model, image_tensor, target_layer=None,
155
+ target_class=None, device="cuda"):
156
+ """
157
+ 放射線画像に対する Grad-CAM 可視化。
158
+
159
+ Parameters:
160
+ model: nn.Module — 学習済み分類モデル
161
+ image_tensor: torch.Tensor — 入力画像 [1, C, H, W] or [1, C, D, H, W]
162
+ target_layer: nn.Module | None — CAM 対象層
163
+ target_class: int | None — 対象クラス (None=予測クラス)
164
+ device: str — デバイス
165
+ """
166
+ import matplotlib.pyplot as plt
167
+ from monai.visualize import GradCAM
168
+
169
+ model.to(device).eval()
170
+ image_tensor = image_tensor.to(device)
171
+
172
+ if target_layer is None:
173
+ # DenseNet の最終 features 層を使用
174
+ for name, module in model.named_modules():
175
+ if "features" in name or "layer4" in name:
176
+ target_layer = name
177
+ if target_layer is None:
178
+ target_layer = list(model.named_modules())[-2][0]
179
+
180
+ cam = GradCAM(nn_module=model, target_layers=target_layer)
181
+
182
+ if target_class is None:
183
+ with torch.no_grad():
184
+ target_class = model(image_tensor).argmax(1).item()
185
+
186
+ result = cam(x=image_tensor, class_idx=target_class)
187
+ cam_map = result.squeeze().cpu().numpy()
188
+
189
+ # 2D スライス可視化
190
+ if cam_map.ndim == 3:
191
+ mid_slice = cam_map.shape[0] // 2
192
+ cam_map_2d = cam_map[mid_slice]
193
+ img_2d = image_tensor.squeeze().cpu().numpy()[mid_slice]
194
+ else:
195
+ cam_map_2d = cam_map
196
+ img_2d = image_tensor.squeeze().cpu().numpy()
197
+
198
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
199
+ axes[0].imshow(img_2d, cmap="gray")
200
+ axes[0].set_title("Original")
201
+ axes[1].imshow(cam_map_2d, cmap="jet")
202
+ axes[1].set_title(f"Grad-CAM (class={target_class})")
203
+ axes[2].imshow(img_2d, cmap="gray")
204
+ axes[2].imshow(cam_map_2d, cmap="jet", alpha=0.4)
205
+ axes[2].set_title("Overlay")
206
+ for ax in axes:
207
+ ax.axis("off")
208
+ plt.tight_layout()
209
+ plt.savefig("gradcam_radiology.png", dpi=150, bbox_inches="tight")
210
+ print(f"Grad-CAM saved → gradcam_radiology.png (class={target_class})")
211
+ return cam_map
212
+ ```
213
+
214
+ ## 3. 構造化放射線レポート
215
+
216
+ ```python
217
+ def generate_structured_report(predictions, patient_info=None,
218
+ modality="CT", body_part="Chest"):
219
+ """
220
+ AI 支援構造化放射線レポート生成。
221
+
222
+ Parameters:
223
+ predictions: dict — {"finding": str, "probability": float, ...}
224
+ patient_info: dict | None — 患者情報
225
+ modality: str — "CT" / "MRI" / "XR"
226
+ body_part: str — 検査部位
227
+ """
228
+ if patient_info is None:
229
+ patient_info = {"id": "ANON", "age": "N/A", "sex": "N/A"}
230
+
231
+ findings = []
232
+ for finding, prob in predictions.items():
233
+ if prob >= 0.5:
234
+ confidence = "High" if prob >= 0.8 else "Moderate"
235
+ findings.append(f"- {finding}: {prob:.1%} ({confidence} confidence)")
236
+
237
+ report = f"""## Structured Radiology Report (AI-Assisted)
238
+
239
+ **Patient**: {patient_info.get('id', 'N/A')} | \
240
+ Age: {patient_info.get('age', 'N/A')} | Sex: {patient_info.get('sex', 'N/A')}
241
+ **Modality**: {modality} | **Body Part**: {body_part}
242
+
243
+ ### AI Findings
244
+
245
+ {chr(10).join(findings) if findings else '- No significant findings detected'}
246
+
247
+ ### AI Confidence Summary
248
+
249
+ | Finding | Probability | AI-RADS |
250
+ |---------|:-----------:|:-------:|
251
+ """
252
+ for finding, prob in sorted(predictions.items(),
253
+ key=lambda x: x[1], reverse=True):
254
+ rads = 5 if prob >= 0.9 else 4 if prob >= 0.7 else \
255
+ 3 if prob >= 0.5 else 2 if prob >= 0.3 else 1
256
+ report += f"| {finding} | {prob:.1%} | {rads} |\n"
257
+
258
+ report += """
259
+ ### Disclaimer
260
+ > This report was generated with AI assistance and requires
261
+ > review by a qualified radiologist before clinical use.
262
+ """
263
+ print(report)
264
+ return report
265
+ ```
266
+
267
+ ---
268
+
269
+ ## パイプライン統合
270
+
271
+ ```
272
+ [DICOM 取得] → medical-imaging → radiology-ai → clinical-report
273
+ (前処理/Radiomics) (AI 診断) (臨床レポート)
274
+
275
+ explainable-ai ← deep-learning
276
+ (説明可能性) (基盤学習)
277
+ ```
278
+
279
+ ## パイプライン出力
280
+
281
+ | ファイル | 説明 | 次スキル |
282
+ |---------|------|---------|
283
+ | `best_radiology_model.pt` | 学習済み分類モデル | → 推論 |
284
+ | `gradcam_radiology.png` | Grad-CAM 可視化 | → レポート |
285
+ | `structured_report.md` | 構造化レポート | → clinical-report |