医用画像AIを学ぶとき、最初の壁は「モデル」ではなくデータの扱いです。DICOMという独特のフォーマット、CTのHU値のような物理量、3Dボリューム——一般的な画像認識の常識がそのまま通用しません。

この壁をまとめて面倒見てくれるのが MONAI(Medical Open Network for AI)です。PyTorchベースの医用画像特化フレームワークで、研究から製品開発まで幅広く使われています。

この記事では、MONAI公式チュートリアルでも使われる公開データセット MedNIST を題材に、環境構築→データ準備→学習→評価までを一気に通します。所要時間はGPUなしでも30分程度です。

MONAIとは

  • PyTorchベースの医用画像ディープラーニング用フレームワーク(Project MONAI)
  • NVIDIAやKing’s College Londonなどが中心となって開発するオープンソース
  • 医用画像特有の機能が最初から揃っている
    • DICOM / NIfTI などのローダー
    • 医用画像向けの前処理・Augmentation(強度正規化、3D対応の回転・切り出しなど)
    • UNet・DenseNet・SwinUNETRなどのネットワーク実装
    • Diceなど医用画像で定番の損失・評価指標

「PyTorchは書けるが医用画像は初めて」という人が、車輪の再発明をせずに済むための道具箱です。

環境構築

Python 3.10以降の仮想環境で、次をインストールします。

python -m venv .venv
source .venv/bin/activate

pip install "monai[pillow]" torch matplotlib scikit-learn

GPUがある場合は、環境に合ったPyTorchを pytorch.org の手順で入れてください。今回のデータは小さいのでCPUでも学習できます。

MedNISTデータセット

MedNISTは、医用画像AIの学習用に整備された教材用データセットです。6クラス・約6万枚の2D画像(64×64)で構成されます。

  • AbdomenCT — 腹部CT
  • BreastMRI — 乳房MRI
  • CXR — 胸部X線
  • ChestCT — 胸部CT
  • Hand — 手のX線
  • HeadCT — 頭部CT

つまり課題は「この画像はどのモダリティ(撮像の種類)か」を当てる6クラス分類です。臨床的な意味は薄いですが、医用画像パイプラインの練習台として最適です。

MONAIには専用のダウンローダーが用意されています。

from monai.apps import MedNISTDataset

root_dir = "./data"

train_ds = MedNISTDataset(
    root_dir=root_dir,
    section="training",
    download=True,
    seed=0,
)
print(len(train_ds))          # 学習用サンプル数
print(train_ds[0]["image"].shape, train_ds[0]["label"])

MedNISTDataset は辞書形式({"image": ..., "label": ...})でサンプルを返します。MONAIの前処理は、この辞書ベースのTransform(末尾に d が付くAPI)で組むのが流儀です。

前処理とAugmentation

from monai.transforms import (
    Compose,
    LoadImaged,
    EnsureChannelFirstd,
    ScaleIntensityd,
    RandRotated,
    RandFlipd,
    RandZoomd,
)

train_transforms = Compose([
    LoadImaged(keys="image", image_only=True),
    EnsureChannelFirstd(keys="image"),      # (H, W) -> (C, H, W)
    ScaleIntensityd(keys="image"),          # 強度を [0, 1] に正規化
    RandRotated(keys="image", range_x=0.26, prob=0.5, keep_size=True),
    RandFlipd(keys="image", spatial_axis=0, prob=0.5),
    RandZoomd(keys="image", min_zoom=0.9, max_zoom=1.1, prob=0.5),
])

val_transforms = Compose([
    LoadImaged(keys="image", image_only=True),
    EnsureChannelFirstd(keys="image"),
    ScaleIntensityd(keys="image"),
])

一般的な画像認識と比べたときのポイントは2つです。

  1. ScaleIntensityd(強度の正規化)。医用画像はピクセル値が「明るさ」ではなく物理量(CTならHU値)を表すことがあり、モダリティごとにレンジがまったく違います。ImageNetの平均・分散で正規化する、という常識は通用しません。
  2. Augmentationは「起こり得る変動」だけにする。左右反転は解剖学的に不自然な画像を作ることがあります(心臓が右にある胸部X線など)。今回はモダリティ分類なので雑でも動きますが、臨床タスクではAugmentationの妥当性自体をドメイン知識で検証します。

モデルと学習ループ

モデルはMONAI組み込みの DenseNet121 を使います。2D・1チャネル・6クラスであることを明示するだけです。

import torch
from monai.data import DataLoader
from monai.networks.nets import DenseNet121

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

train_ds = MedNISTDataset(
    root_dir=root_dir, section="training",
    transform=train_transforms, download=True, seed=0,
)
val_ds = MedNISTDataset(
    root_dir=root_dir, section="validation",
    transform=val_transforms, download=False, seed=0,
)

train_loader = DataLoader(train_ds, batch_size=128, shuffle=True, num_workers=4)
val_loader = DataLoader(val_ds, batch_size=256, num_workers=4)

model = DenseNet121(spatial_dims=2, in_channels=1, out_channels=6).to(device)
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

for epoch in range(3):
    model.train()
    for batch in train_loader:
        images = batch["image"].to(device)
        labels = batch["label"].to(device)
        optimizer.zero_grad()
        loss = loss_fn(model(images), labels)
        loss.backward()
        optimizer.step()
    print(f"epoch {epoch + 1} done (last loss: {loss.item():.4f})")

MedNISTは易しいデータセットなので、3エポックも回せば検証精度は99%前後に到達します。

評価 — Accuracyだけ見ない癖をつける

医用画像AIでは、混同行列と感度・特異度を必ず確認する癖をつけましょう。臨床タスクではクラス不均衡が極端(有病率1%など)で、Accuracyが高くても見逃しだらけ、ということが平気で起きるからです。

import numpy as np
from sklearn.metrics import classification_report, confusion_matrix

model.eval()
y_true, y_pred = [], []
with torch.no_grad():
    for batch in val_loader:
        images = batch["image"].to(device)
        logits = model(images)
        y_pred.extend(logits.argmax(dim=1).cpu().numpy())
        y_true.extend(batch["label"].numpy())

class_names = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]
print(classification_report(y_true, y_pred, target_names=class_names, digits=4))
print(confusion_matrix(y_true, y_pred))

今回のような易しいタスクでも、混同行列を見ると「間違えるときは AbdomenCTChestCT の間」といった、解剖学的に納得感のある誤りパターンが観察できて面白いはずです。

ここから先へ — 実戦への3ステップ

MedNISTで「パイプラインを通す」経験ができたら、次はより実戦的な課題へ進みましょう。

  1. セグメンテーション: MONAIの UNet とDice損失で、臓器・病変の領域抽出に挑戦する(Medical Segmentation Decathlonの公開データが定番)
  2. DICOMを直接扱う: pydicom でヘッダを読み、HU値変換・ウィンドウ処理を自分で書いてみる
  3. 3Dへの拡張: CT/MRIをボリュームとして扱い、spatial_dims=3 のネットワークとパッチベース学習を試す

まとめ

  • MONAIは医用画像特有の面倒(フォーマット・前処理・3D)を吸収してくれるPyTorchベースのOSS
  • MedNIST分類なら、CPUでも30分で「医用画像AIのパイプライン一周」を体験できる
  • 医用画像では強度の正規化・Augmentationの妥当性・Accuracy以外の評価指標が重要
  • 次のステップはセグメンテーション、DICOMの生読み、3D化

「なぜ医用画像AIがビジネスとして成立しにくいのか(規制の壁)」に興味が湧いた人は、SaMD入門もあわせてどうぞ。