1畳み込みニューラルネットワーク(CNN)とは
CNNは、小さな「フィルタ(カーネル)」を画像の上でスライドさせながら、各位置で 要素ごとの掛け算 → 総和を計算していくネットワークです。1枚の画像全体に対して同じフィルタを 使い回す(重み共有)ため、全結合層に比べてパラメータ数が大幅に削減され、かつ「エッジ」「質感」のような 局所的なパターンを画像内のどこにあっても検出できるようになります。
MediSeg-AIでは、1回の畳み込みブロック(DoubleConv)の中で
Conv2d(3×3, padding=1) → BatchNorm → ReLU を2回連続で適用しています。
padding=1 なので出力の画像サイズは入力と変わりません(“same” 畳み込み)。
BatchNormは各バッチ内でチャンネルごとに正規化して学習を安定させ、ReLU
(max(0, x))は負の値を0にすることでネットワークに非線形性を持たせます — もし全部が
線形変換(掛け算・足し算)だけなら、何層重ねても1層の線形変換と等価になってしまい、複雑なパターンを
学習できません。
| 実装 | backend/mediseg_ai/models/unet.py:11-28(DoubleConvクラス) |
| カーネルサイズ | 3×3、padding=1(出力サイズ = 入力サイズ) |
| 正規化 / 活性化 | BatchNorm2d → ReLU(各畳み込みの直後) |
2バッチ正規化(Batch Normalization)
前の節で触れたBatchNormを補足します。 学習が進むにつれて各層への入力の分布(平均・分散)がバッチごとに揺れ動くと、後ろの層は 毎回変化する入力分布に合わせて学習をやり直すことになり、学習が不安定になったり遅くなったりします (internal covariate shift と呼ばれる現象)。BatchNormは、その揺れをバッチ単位で打ち消す 仕組みです。
- μB, σB2: そのバッチ・そのチャンネルにおける平均と分散(学習時に動的に計算。推論時は学習中に蓄積した移動平均を使う)。
- γ, β: 学習可能なパラメータ。「平均0・分散1」に固定してしまうとネットワークの表現力が落ちる場合があるため、 必要なら元の分布に近い形へ戻せるようにしている。
- 効果: 各層への入力分布が安定するため、より大きな学習率を使っても発散しにくくなり、 重みの初期値への依存も減る。副次的に、バッチごとの統計量に含まれるノイズが軽い正則化効果も生む。
DoubleConv
(backend/mediseg_ai/models/unet.py:18-25)の中で、2回の畳み込みそれぞれの直後に
nn.BatchNorm2dを適用しています。U-Net全体では DoubleConv が
INC・DOWN1–4・UP1–4 の 9ブロックあり、1ブロックにつき2箇所なので、
合計 18箇所のBatchNorm2dがネットワーク全体に存在します。畳み込み層(Conv2d)
自体は bias=False で作られていますが、これはBatchNormの β がバイアス項の役割を
兼ねるため、素の畳み込みバイアスは不要だからです。
3活性化関数(シグモイド・ソフトマックス)
活性化関数(Activation Function)は、線形変換(入力×重み+バイアス)の出力を
非線形な値に変換して次の層へ渡す関数です。前のセクションで見たReLU
(max(0, x))もその1つで、MediSeg-AIでは9つのDoubleConvブロックすべての中間層で使われています。
線形変換だけをいくら重ねても、結局は1つの大きな線形変換に集約されてしまい複雑な境界線を学習できないため、
この非線形性がネットワークの表現力の源になっています。ここでは中間層のReLUとは役割の異なる、
出力層で用いる2つの活性化関数 — シグモイド関数とソフトマックス関数 — を整理します。
シグモイド関数MediSeg-AIで使用
シグモイド関数(Sigmoid)は、任意の実数を 0〜1の範囲に滑らかに圧縮する関数です。
- 出力を確率として解釈できる: 出力が(0, 1)に収まるため、「病変である確率」のような 二値の見込み値として扱いやすい。
- 勾配消失問題: 入力の絶対値が大きい領域では傾き(微分値)がほぼ0になり、逆伝播で伝わる勾配も ほぼ0になってしまう。これが、深い中間層でシグモイドではなくReLUが使われる理由です (セクション1参照)。
OutC)自体には活性化関数を付けず、
生のロジット(実数)をそのまま出力しています。その代わり、ロジットを「病変らしさ0〜1」の
確率ヒートマップに変換する箇所すべてでシグモイドが使われています —
推論結果を表示する backend/mediseg_ai/inference/pipeline.py:48
(torch.sigmoid(output)、これが可視化ページの「確率ヒートマップ」の正体です)、
評価時の backend/mediseg_ai/evaluation/evaluate.py:124、
Diceスコア計算の backend/mediseg_ai/metrics/torch_adapter.py:12、
そして学習時の DiceLoss(backend/mediseg_ai/training/losses.py:16)。
なお CombinedLoss のもう半分である BCEWithLogitsLoss は、
数値的に安定な実装のためシグモイドと二値交差エントロピーを1つの関数の中で内部的に計算しており、
外からは明示的な sigmoid() 呼び出しは見えません。
ソフトマックス関数MediSeg-AIでは未使用
ソフトマックス関数(Softmax)は、複数の出力(ベクトル)を、合計が1(100%)になる確率分布に変換する関数です。 K個のクラスがある場合、i番目の出力 yi は次のようになります。
- 多クラス分類の出力層: 「犬・猫・鳥」のように3つ以上の選択肢から1つを選ぶ分類問題の出力層で使われる。
- シグモイド関数との関係: クラス数が2つの場合、ソフトマックスは数学的にシグモイド関数と等価になります — つまりシグモイドの多クラス拡張版がソフトマックスです。
UNet は
n_classes=1(backend/mediseg_ai/models/unet.py:93)で構築されており、
各ピクセルについて「病変か背景か」の二値だけを判定するタスクのため、多クラスに正規化する
ソフトマックスではなく、単一の値を0〜1に収めるシグモイドで十分だからです。もしMediSeg-AIを
拡張して「腫瘍・浮腫・正常組織」のように3種類以上をピクセル単位で塗り分けるモデルにする場合は、
出力層をn_classes個に増やし、損失関数もソフトマックス+多クラス交差エントロピー
(CrossEntropyLoss)に置き換える必要があります。
3つの関係性のまとめ
| 活性化関数 | ネットワークに非線形性を与える関数の総称。モデル全体の表現力を上げるため用いる。 |
| ReLU | 負を0にする活性化関数。MediSeg-AIの全9ブロック(DoubleConv)の中間層で使用(出力層OUTCには活性化関数なし)。 |
| シグモイド関数 | 入力を0〜1に圧縮する活性化関数。二値分類の出力層向け。MediSeg-AIの推論・評価・損失計算で使用。 |
| ソフトマックス関数 | 複数の出力を合計1の確率分布に変換する関数。多クラス分類の出力層向け。MediSeg-AIでは未使用(二値タスクのため)。 |
4MediSeg-AIのU-Netアーキテクチャ全体像
U-Netは、画像をだんだん縮小させながら特徴を抽出するエンコーダ(収縮路)と、 その特徴を元の解像度まで復元しながら領域を塗り分けるデコーダ(拡張路)を、 “U” の字型に組み合わせたアーキテクチャです。名前の由来にもなっている スキップ接続(エンコーダの特徴を対応する解像度のデコーダへ直接コピーする経路)によって、 ダウンサンプリングで失われがちな輪郭などの位置情報を、デコーダ側で復元しやすくしています。
MaxPool2dで解像度を半分にしながらチャンネル数を倍にし、デコーダは
ConvTranspose2dで解像度を倍に戻しながらチャンネル数を半分にする。破線の弧がスキップ接続。
各ステージの役割
| INC | 256×256×64ch — 最初のエッジ・色の変化などシンプルなパターンを検出 |
| DOWN1–DOWN3 | 128²→64²→32²、128→256→512ch — 解像度を落としながら段々複雑な構造を認識 |
| BOTTLENECK | 16×16×1024ch — 画像を最も圧縮した中心部。「全体として何が写っているか」の要約 |
| UP1–UP4 | 32²→64²→128²→256²、512→256→128→64ch — スキップ接続と合成しながら解像度を復元 |
| OUTC | 256×256×1ch — Conv2d(1×1)で各ピクセルの「病変らしさ」を1つの値に集約 |
backend/mediseg_ai/models/unet.py:93-122(UNetクラス)。
エンコーダ最深部からの復元には ConvTranspose2d(学習可能な逆畳み込み)を使う設定と、
単純な bilinear 補間を使う設定の2通りをサポートしていますが、公開されている3チェックポイント
(LGG-only / MSD / Combined)はいずれも ConvTranspose2d 版で学習されています。
合計パラメータ数は約3,100万。入力256×256のRGB画像1枚につき、全10ブロックの畳み込みが
この“U”の形で一度だけ実行されます。動かして回転できる3D版は
Architecture Visualizerを、
実際に学習された重みの強弱はステージごとの色分けで
Weight Visualizerで確認できます。
使用しているMRIモダリティ(FLAIR・T1gd・T2w)
MSDデータセット(公開事例「小さな病変を検出できた例」の元データ)は、1つの断面につき
4種類のMRI撮像法(モダリティ)を持っています。ところが UNet(n_channels=3) は
通常のRGB画像と同じ3チャンネル入力を前提にしたアーキテクチャなので、4つのうち3つだけを選んで
R・G・Bの3チャンネルに割り当てています。
- FLAIR: 脳脊髄液の信号を抑制した撮像法。浮腫や病変全体の広がりを最も見やすく捉える。
- T1gd(造影T1強調): 造影剤(ガドリニウム)を使うことで、活発に増殖している腫瘍コアが 明るく強調される。
- T2w: FLAIRを補完する形で浮腫や非造影腫瘍の広がりを捉える。
- 非造影T1w(使われていない): 主に解剖学的な構造(脳の形やコントラスト)を映すための 撮像法で、腫瘍と正常組織のコントラストが他の3つほど際立ちません。4つのうち1つを削るなら 最も腫瘍の情報量が少ないT1wを外す、という判断は臨床的に見て妥当な選択と言えます。
notebooks/07_msd_finetune_from_lgg_checkpoint.ipynb)
と showcase 生成 (notebooks/08_msd_showcase_export.ipynb) の両方に同一の
slice_to_rgb() 関数があり、
chans = [volume[:, :, z, 0], volume[:, :, z, 2], volume[:, :, z, 3]]
(MSDのモダリティ順序 0=FLAIR, 1=T1w, 2=T1gd, 3=T2w のうち index 1 を除く3つ)としてRGB化しています。
ただし、この3つを選んだ理由を明示するコメントはコード中には残っていません — 上記の臨床的な妥当性は
後付けの説明であり、実際に意図された設計判断だったのか、単に3チャンネルの枠に収める必要があった
結果なのかは、コードからは判別できない点に注意してください。
コンペティションレベルとの比較
脳腫瘍セグメンテーションの国際コンペ BraTS(MSD Task01 BrainTumourの元データ)は、 症例ごとに3D立体を復元して Dice を計算し、症例間で平均する公式プロトコルで採点します。 評価領域は入れ子の3つ — Whole Tumor(WT)・Tumor Core(TC)・ Enhancing Tumor(ET) — で、競技の本採点はこれらの平均です。 MediSeg-AIの3D学習(Phase 5)は、このプロトコルに揃えた 症例単位3D Diceで測定しています(学習・評価の症例分離を確保・全73症例)。
- MediSeg-AI 3Dモデル(UNet3D・領域出力): WT 0.8756 / TC 0.7870 / ET 0.7163 / 3領域平均 0.7930(2026-07時点。単一モデルへの単純な反転平均では改善せず)。
- BraTS 2020優勝(nnU-Net): WT 0.8895 / TC 0.8506 / ET 0.8203 / 3領域平均約 0.853(テストセット。検証WTは0.9124)。
- 差: WTは約 −0.014。競技採点に相当する3領域平均では約 −0.06 (主にTC/ET、とくにET)。
docs/COMPETITION_ROADMAP.md を参照してください。
2Dモデルの提供に加え、3D U-Netの自社学習を実施中です。WTは国際コンペ上位手法と近い水準にあり、 残課題はTC/ETを含む3領域平均の約0.06差です。 同様の手法を医用画像に限らず、製造業の外観検査など、対象が小さく背景に対してまばらな領域へ 展開する検討も進めています。
4b3D学習 — UNet3D・パッチ学習・領域出力
現行の2D U-Netは、MRIボリュームを断面(スライス)単位に分解して学習します。
この方式では、前後のスライスにわたる病変のつながり(奥行き方向の文脈)を直接扱えません。
そこで自社では UNet3D
(backend/mediseg_ai/models/unet3d.py)を別実装し、
ボリュームを3次元畳み込みで処理する学習を進めています。2Dモデル・可視化・既存の学習済み重みは
そのまま併用できます。
3D化で変わる点
- 入力形式: 2Dは
(B, C, H, W)。3Dは(B, C, D, H, W)(Dはスライス方向)。演算はConv3d/MaxPool3d/ConvTranspose3d。 - ネットワーク構成: 2D U-Netと同様に DoubleConv / Down×4 / Up×4 / OutConv。
メモリ制約から
base_channels=32(2Dの64ではない)。4チャンネル入力で約2,258万パラメータ。 - 正規化: スライスごとではなく、ボリューム単位・チャンネルごとに強度を揃える (奥行き方向の一貫性を保つため)。
| モデル | UNet3D — backend/mediseg_ai/models/unet3d.py |
| データ | MSDVolumeDataset — backend/mediseg_ai/datasets/msd_volume.py |
| 学習手順 | notebooks/17_msd_3d_training.ipynb(WT二値) /
18_msd_3d_region_training.ipynb(WT/TC/ET) |
| 評価 | brats_eval_3d.py — スライディングウィンドウ + 症例単位3D Dice |
| 計算資源 | 128³パッチ / base=32 でGPUメモリ約16GBが目安 |
パッチ学習(128³)と前景サンプリング
1症例は数百スライスに及ぶため、学習時はボリューム全体ではなく
sample_patch() で固定サイズ(例: 128×128×128)の領域を切り出します。
腫瘍は体積のごく一部であるため、一定確率で腫瘍ボクセルを中心にしたパッチを抽出し
(foreground_prob)、小病変が学習から欠落しないようにしています。
推論: スライディングウィンドウ
学習はパッチ単位でも、評価は症例全体でDiceを算出します。
predict_volume_with_3d_model は重なりを持たせた窓をずらしながら推論し、
重なり部分を加重平均して1つの3Dマスクに復元します。これにより、断面単位の評価とは異なる
「症例単位3D Dice」で性能を確認できます。
領域出力: WT / TC / ET
BraTSの評価は入れ子の3領域です。n_classes=3 と RegionLoss
(チャンネル毎のsigmoid BCE+Dice)により、相互排他ではなく
重なりを許す3つの二値出力を学習します。現時点の測定値は
WT 0.8756 / TC 0.7870 / ET 0.7163 / 平均 0.7930(評価症例73件)。
SegmentationTrainer は次元に依存しないため、
追加の学習器なしで5次元テンソルのまま学習できます(test_train3d_smoke.py)。
Deep supervision(UNet3D(deep_supervision=True))は学習時のみ低解像度の補助出力を追加し、
推論時(eval())は従来どおり単一テンソルを返します。
単一モデルへの単純な反転平均は平均 −0.0034で採用していません。
次の施策は補助損失の強化、学習の収束改善、複数モデルの統合です。
可視化・現行推論で用いるのは主に2D U-Netです。 本節の3D学習は、国際的な評価指標に合わせた性能向上のための自社開発です。 掲載する評価値は セクション4のコンペ比較および
docs/COMPETITION_ROADMAP.md にあります。
5誤差逆伝播法(バックプロパゲーション)
ネットワークは大きく2方向の計算からできています。順伝播(forward)は画像を入力してから予測を 出すまでの通常の計算方向。逆伝播(backward)は、その予測がどれだけ間違っていたか(損失)から 出発して、「この重みをほんの少し変えたら、損失はどれくらい変化するか」という勾配を、 出力側から入力側へ向かって連鎖律(chain rule)で層ごとに計算し直していく方向です。
この連鎖律の計算は、PyTorchでは自動微分(autograd)が全て肩代わりします。 MediSeg-AIの学習コードでは、実際には次の1行を呼ぶだけで、約3,100万パラメータすべての勾配が 計算されます:
# backend/mediseg_ai/training/trainer.py:185
self.scaler.scale(loss).backward() # ← ここで連鎖律が10ブロック分すべてに適用される
6バッチ処理とミニバッチ勾配降下法
逆伝播で勾配を計算する対象は「1枚の画像」ではなく「バッチ(複数枚まとめた集合)」です。 この単位の選び方には3つの方式があります。
- フルバッチ(Batch GD): 全データの平均勾配で1回だけ更新。勾配は正確だが、 データセット全体をメモリに載せる必要があり、1エポックに1回しか重みが動かないため学習が遅い。
- 確率的勾配降下法(SGD, バッチサイズ1): 1枚ごとに更新。反応は速いが、1枚だけから 推定した勾配はノイズが大きく、損失が谷底付近でも大きくジグザグに振れ続けやすい。
- ミニバッチ勾配降下法: 数枚〜数十枚をまとめて平均勾配を取る、両者の折衷案。 GPUは行列演算をバッチ方向にまとめて並列処理できるため、単純に「サンプル数分ループを回す」 SGDよりも同じ時間で多くのデータを処理でき、勾配のノイズも平均化である程度抑えられる。 現代のディープラーニングではほぼ標準の方式です。
DataConfig.batch_size
(backend/mediseg_ai/config_schemas.py:13)の既定値は8ですが、
実際に使われるベース設定 default_base_config()
(backend/mediseg_ai/config_schemas.py:96-102)では16に上書きされています。
つまり256×256のRGB画像を16枚まとめて1回の順伝播・逆伝播・重み更新を行っており、
データセット全体(例: LGGの陽性スライス約1,100枚)に対しては1エポックあたり約69回の重み更新が
発生する計算です。バッチの読み込み自体は torch.utils.data.DataLoader が
num_workers=4(既定)で並列に行い、GPU側の計算を待たせないようにしています。
7勾配降下法とAdamオプティマイザ
逆伝播で「各重みをどちらに動かせば損失が減るか」(勾配)が分かったら、実際に重みを更新するのが 勾配降下法です。最もシンプルな形は次の更新式です:
w ← w − lr × ∂L/∂w
lr(学習率)が大きすぎると損失の谷を飛び越えて発散し、小さすぎると学習が遅すぎて
実用的な時間で収束しません。
MediSeg-AIが実際に使っている設定
MediSeg-AIのデフォルトオプティマイザはAdamです。Adamは全パラメータに同じ学習率を使う代わりに、 各パラメータごとに「勾配の移動平均(momentumに相当)」と「勾配の2乗の移動平均(更新の大きさを 自動調整する分母)」を保持し、パラメータごとに実効的な学習率を自動調整します。これにより、 素の勾配降下法より学習率のチューニングに敏感になりにくく、収束が速くなります。
| オプティマイザ | Adam(既定)。SGD + momentum=0.9も選択可 |
| 学習率(lr) | 0.001(既定) |
| weight_decay | 1e-4(既定) — 重みの大きさ自体にもペナルティをかけ、過学習を抑制 |
| 学習率スケジューラ | CosineAnnealingLR(既定)/ StepLR / ReduceLROnPlateau から選択 |
| 実装 | backend/mediseg_ai/training/trainer.py:85-123(_setup_optimizer / _setup_scheduler) |
8混合精度学習(Mixed Precision / AMP)
GPUの数値計算は通常32ビット浮動小数点(FP32)で行われますが、近年のGPU(NVIDIA Tensor Core搭載)は 16ビット(FP16)の行列演算を専用回路でさらに高速に処理できます。混合精度学習は、 畳み込みなど計算負荷の重い演算をFP16で行いつつ、勾配の累積など数値的に不安定になりやすい箇所だけ FP32を維持することで、精度をほぼ落とさずに学習を高速化・省メモリ化する手法です。
GradScalerが損失を一時的に拡大してから逆伝播し、オプティマイザに渡す前に元の大きさへ戻す。
backend/mediseg_ai/training/trainer.py:54 で
self.use_amp = device.type == "cuda" — CUDA GPUがある場合のみ有効になり、
CPU学習時は自動的にFP32のみにフォールバックします。順伝播と損失計算を
autocast(...)(trainer.py:180)で囲み、
逆伝播は self.scaler.scale(loss).backward()(trainer.py:185)、
重み更新は self.scaler.step(self.optimizer) と self.scaler.update()
(trainer.py:186-187)という3段階で行っています。CPU実行時は
GradScaler(..., enabled=False) が素通しになるため、学習コード自体は
GPU/CPUで同じまま動作します。
9Dice係数とIoU(評価指標)
脳MRIの病変領域は、画像全体からするとごく一部の面積しかありません。そのため 「全ピクセルのうち何%正解したか」というピクセル精度(accuracy)は評価指標として不適切です — 極端な話、モデルが「どこにも病変はない」と常に予測しても、背景ピクセルが大半を占めるため accuracyは高く出てしまいます。そこで医用画像セグメンテーションでは、 予測領域と正解領域がどれだけ重なっているかを測る指標を使います。
- Diceが高評価に慣れている理由: 「重なった部分を2倍数える」ことで、小さな病変領域でも 指標が極端に低く出にくいよう設計されている。医用画像分野で最もよく使われる評価指標。
- IoU(Jaccard係数): 物体検出でもおなじみの指標で、Diceより厳しめの値になる (Dice = 2×IoU / (1+IoU) という数学的な関係がある)。
- accuracyとの違い: 例えば正解領域が全体の2%しかない画像で「病変なし」と予測すると、 accuracyは98%になるがDice・IoUは0になる — こちらの方が実態を正しく反映している。
dice_coefficient()、:24-29 の intersection_over_union()
が上の式をそのまま実装しています(ゼロ除算を避けるための微小値 smooth=1e-6 付き)。
可視化ページや各データセット解説ページに出てくる「Dice: 0.6961」のような数値は、
すべてこの実装で計算された学習中の検証(validation)スコアです。
セクション10で説明する DiceLoss は、この関数を直接呼ぶのではなく
同じ 2|A∩B| / (|A|+|B|) の式を別実装しています — 違いは二値化の有無です。
評価指標である dice_coefficient() は 0.5 で閾値化した二値マスクどうしの重なりを計算する
(最終的な出力の良し悪しをそのまま反映)のに対し、DiceLoss は逆伝播できるよう
シグモイド後の連続値のまま(閾値化せず)計算します。
10学習ループ全体のまとめ
ここまでの3つの技術(CNNの順伝播、逆伝播、勾配降下法)は、実際の学習コードの中では 次のようにひとつのループとして組み合わさっています:
# backend/mediseg_ai/training/trainer.py:179-186 (要約)
self.optimizer.zero_grad(set_to_none=True) # 前のバッチの勾配をクリア
outputs = self.model(images) # ① 順伝播: 画像 → U-Net → 予測ロジット
loss = self.criterion(outputs, masks.float()) # ② 損失計算: 予測 vs 正解マスク
self.scaler.scale(loss).backward() # ③ 逆伝播: 全パラメータの勾配を計算
self.scaler.step(self.optimizer) # ④ 勾配降下法: Adamが全重みを更新
②の損失関数(criterion)には CombinedLoss を使っています —
BCE(二値交差エントロピー)0.5 と Dice損失 0.5 を足し合わせたものです。
- BCE: 各ピクセルを独立に「病変か背景か」の2クラス分類として評価する、最も基本的な損失。
- Dice損失: セクション9で説明したDice係数を
1 − Diceとして損失にしたもの。脳MRIの病変は画像全体に対してごく一部の 面積(極端なクラス不均衡)しかないため、BCE単体ではモデルが「すべて背景と予測する極小解(自明な解)」に収束しやすくなります。 Dice損失を併用することで、前景領域の重なり度合いを直接評価し、クラス不均衡による学習の偏りを防止しています。
backend/mediseg_ai/training/losses.py:39-52(CombinedLoss)。
この1バッチぶんのループ(① → ④)を、データセット全体・複数エポック分繰り返すことで、
ランダムに初期化されたU-Netの重みが少しずつ、脳MRIの病変を検出できる値へと調整されていきます。