@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.
- package/README.md +61 -29
- package/package.json +1 -1
- package/src/.github/skills/scientific-adaptive-experiments/SKILL.md +287 -0
- package/src/.github/skills/scientific-anomaly-detection/SKILL.md +296 -0
- package/src/.github/skills/scientific-causal-ml/SKILL.md +240 -0
- package/src/.github/skills/scientific-data-profiling/SKILL.md +247 -0
- package/src/.github/skills/scientific-federated-learning/SKILL.md +241 -0
- package/src/.github/skills/scientific-geospatial-analysis/SKILL.md +274 -0
- package/src/.github/skills/scientific-model-monitoring/SKILL.md +247 -0
- package/src/.github/skills/scientific-multi-task-learning/SKILL.md +238 -0
- package/src/.github/skills/scientific-network-visualization/SKILL.md +278 -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-reproducible-reporting/SKILL.md +330 -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
- package/src/.github/skills/scientific-time-series-forecasting/SKILL.md +246 -0
|
@@ -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 |
|