[医療×生成系AI :GP2.3] 第2回第3章: Pythonと深層学習入門(ニューラルネットを構築しよう)@医療AI教室

難易度:★★☆

💡この第3章では、第1・2章で学んだ知識を活かし、実際にニューラルネットワークを構築してMNIST手書き数字データを分類してみましょう。モデルの定義から訓練、精度の評価、そして誤認識の可視化まで、ディープラーニングの基本サイクルを一通り実装します。

実際に手を動かしてみることで、「学習とは何か?」「誤差逆伝播がどう使われているか?」といった概念が具体的に理解できるはずです。生成系AIの本格的なモデル実装に入る前に、シンプルなニューラルネットで学習の原理を確かめておきましょう。

深層学習の一連のプロセスを概念的に理解・おさらいしておきたい方は、過去の回「【医療AI教室:Vol.7】誤差を逆さにたどる!? “深層学習”の学習プロセス」を一読されることをお勧めします!

目次

1. MNISTデータセットとは?

MNISTは、米国商務省配下の研究所が構築した、28×28ピクセルの手書き数字(0~9)画像が7万枚(学習用6万枚 + テスト用1万枚)含まれる有名なベンチマークデータセットです。

Wikipedia
  • モノクロ1チャネル画像
  • ピクセル値を 0~255 → 0~1 や -1~1 の範囲に正規化
  • 「機械学習初心者が最初に試す定番データセット」として広く利用される

PyTorchでは torchvision.datasets.MNIST を使って自動ダウンロード&簡単に扱えます。ニューラルネットの構造を理解する上で非常に取り組みやすい例です。


2. Google Colab で試すための手順

  1. Google Colab にアクセスし、新しいノートブックを作成します。
  2. 上部メニューの「ランタイム」→「ランタイムのタイプを変更」→「ハードウェアアクセラレータ」を GPU に変更(CPUでも動作しますがGPUが速い)。
  3. 以下のコードブロックをすべて同じノートブックに貼り付け、上から順に実行してください。

3. 学習用コード例

ここでは、MNISTを使ったニューラルネットによる多クラス分類を実装します。

  • データの準備
  • サンプル画像の表示
  • モデルの定義
  • 学習ループ(損失推移グラフつき)
  • テスト評価(誤認識表示、混同行列)

各セルのコードを順番にGoogle Colab のノートブックにコピペしていき、実行し、出力を確認してみましょう。

(1) ライブラリのインストール & インポート

# --- ライブラリとは? ---
# プログラミングでよく使う便利な機能(たとえば数学の計算、画像の表示、AIの学習など)を、
# あらかじめまとめて用意してくれている“道具箱”のようなものです。
# 自分でゼロから作らずに済むので、効率よく開発ができます。

# 1) ライブラリのインストール (Google Colab用)
!pip install torch torchvision scikit-learn seaborn
# ↑ AI・画像処理・機械学習・可視化に必要な4つのライブラリをまとめてインストールします。
# - torch:PyTorchというディープラーニングライブラリの本体
# - torchvision:画像データの読み込みや変換を助けるライブラリ
# - scikit-learn:機械学習の評価や分類などに便利な関数群
# - seaborn:グラフや図をきれいに表示するための可視化ライブラリ

# 2) 必要なライブラリをインポート
import torch
# ↑ PyTorchの基本機能(テンソル計算、自動微分など)を使うために読み込みます。

import torch.nn as nn
# ↑ ニューラルネットワークを構成するためのモジュール(レイヤーなど)を提供。

import torch.nn.functional as F
# ↑ 活性化関数(ReLUなど)や損失関数(CrossEntropyなど)が含まれています。

import torch.optim as optim
# ↑ 最適化アルゴリズム(例:SGDやAdam)を使うためのモジュールです。

import torchvision
# ↑ 画像データセット(MNISTやCIFARなど)や、画像処理のユーティリティが含まれています。

import torchvision.transforms as transforms
# ↑ 画像の前処理(リサイズ、正規化など)を簡単に行うためのツール群。

import matplotlib.pyplot as plt
# ↑ 画像やグラフの描画に使うライブラリ。Matplotlibの基本機能です。

import numpy as np
# ↑ 配列や行列などの数値計算を高速で処理できるライブラリです。

import seaborn as sns  # 混同行列可視化に利用
# ↑ データの可視化に特化したライブラリ。特にヒートマップ表示が得意です。

from sklearn.metrics import confusion_matrix
# ↑ モデルの分類結果を評価する「混同行列」を作る関数が入っています。

# PyTorchでGPUが使えるかどうかチェックし、利用可能ならGPUを使用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ↑ CUDAというNVIDIA製GPUが使える場合はGPU、なければCPUを選択する仕組みです。

print("使用デバイス:", device)
# ↑ 実際に使われるデバイス(CPU or GPU)を出力して確認します。


(2) データセットとDataLoaderの準備

# 3) データ前処理のTransform定義
transform = transforms.Compose([
    transforms.ToTensor(),                      # 画像をTensor(PyTorch用の数値配列)に変換(0〜1の範囲に正規化)
    transforms.Normalize((0.5,), (0.5,))        # 平均0.5、標準偏差0.5でさらに正規化(-1〜1の範囲にスケーリング)
])
# ↑ ディープラーニングでは、画像データをそのまま使うのではなく、
#    モデルに合った形式(テンソル)に変換し、学習しやすいように「正規化」します。
#    transforms.Compose は、複数の前処理を順番にまとめて適用できる便利な仕組みです。

# 4) MNIST学習データセットの作成
train_dataset = torchvision.datasets.MNIST(
    root='./data',                # データの保存先フォルダ(なければ自動で作られます)
    train=True,                   # 訓練用データ(True)か、テスト用データ(False)かを指定
    transform=transform,         # 上で定義した画像の前処理を適用
    download=True                # データがなければ自動でインターネットからダウンロード
)
# ↑ torchvisionに含まれるMNIST(手書き数字画像)のデータセットを準備します。
#    画像は28x28ピクセルで、0〜9の数字がラベルとして付いています。

# 5) バッチサイズを設定し、DataLoaderを作成
train_loader = torch.utils.data.DataLoader(
    train_dataset,               # 上で作成したデータセットを使用
    batch_size=64,               # 1回の学習で使用するデータの数(バッチサイズ)
    shuffle=True                 # 毎回データの順番をシャッフルして、学習の偏りを防ぐ
)
# ↑ DataLoaderは、データを小分けにしてモデルに渡すための仕組みです。
#    バッチ学習と呼ばれ、効率的に学習できるようになります。

print("学習用データ数:", len(train_dataset))
# ↑ ダウンロードした学習データの総数(60,000件)を表示して確認します。

(3) サンプル画像の可視化

# 6) DataLoaderから最初のバッチを取り出し、画像を表示

data_iter = iter(train_loader)  # イテレータを取得
# ↑ DataLoaderからデータを1バッチずつ取り出せる「イテレータ」という仕組みを取得します。
#    Pythonの「for文」で使う仕組みの裏側です。

images, labels = next(data_iter)  # 最初のバッチ(64枚)
# ↑ イテレータから最初のバッチを取り出します。
#    images: 画像データ(64枚分のテンソル)/ labels: 各画像に対応する正解の数字ラベル

images_np = images.numpy()
# ↑ PyTorchのTensor形式を、NumPy形式(Pythonで一般的な数値配列)に変換します。
#    画像表示のときはNumPyに変換すると便利です。
# ※GPU上のTensorだと .numpy() はエラーになります。GPU使用時は .cpu().numpy() が必要。

fig = plt.figure(figsize=(8, 8))
# ↑ 描画用の図(Figure)を作成。サイズは8x8インチ。

for i in range(9):
    ax = fig.add_subplot(3, 3, i+1)
    # ↑ 3行×3列のグリッドに、画像を1枚ずつ並べる準備をします。

    ax.imshow(np.squeeze(images_np[i]), cmap='gray_r')
    # ↑ 画像を表示します。
    # - np.squeeze():画像の余計な次元(例:1チャンネル)を取り除きます。
    # - cmap='gray_r':白黒(反転グレースケール)で表示します。

    ax.set_title(f"Label: {labels[i].item()}")
    # ↑ 画像の上に「正解ラベル(数字)」をタイトルとして表示。

    ax.axis('off')
    # ↑ 画像の周りの軸(枠)を非表示にします。

plt.suptitle("MNISTサンプルイメージ", fontsize=16)
# ↑ 図全体にタイトルを付けます。

plt.show()
# ↑ 表示用のウィンドウにグラフ(ここでは画像)を出力します。

(4) ニューラルネット定義

# 7) ニューラルネットワークの定義

# ======================================
# ネットワーク構造(テキストベース図)
# ======================================
#
# 入力画像 (28x28)
#       │
#       ▼
#   [ Flatten ]         ← 画像を1次元(784)に変換
#       │
#       ▼
#   [ fc1: Linear(784 → 128) ]
#       │
#       ▼
#   [ ReLU ]            ← 活性化関数(非線形変換)
#       │
#       ▼
#   [ fc2: Linear(128 → 10) ]
#       │
#       ▼
#   出力(logits)      ← 各クラス(0〜9)のスコア
#
# ======================================

class SimpleNet(nn.Module):
    # ↑ PyTorchのnn.Moduleを継承して、自作のネットワーク「SimpleNet」を定義します。
    #    nn.Moduleは、ニューラルネットワークを作るための“ひな形”のような存在です。

    def __init__(self):
        super(SimpleNet, self).__init__()
        # ↑ 親クラス(nn.Module)の初期化を呼び出します。これがないと正しく動きません。

        self.fc1 = nn.Linear(28*28, 128)  # 入力784 → 出力128
        # ↑ 入力画像(28×28 = 784ピクセル)を128次元のベクトルに変換する「全結合層(全てのノードが接続)」

        self.fc2 = nn.Linear(128, 10)     # 出力10クラス (0~9)
        # ↑ 128次元の特徴量を、0〜9の「10クラス」に分類する出力層です。

    def forward(self, x):
        # ↑ モデルにデータを渡したとき、どのように処理されるかを定義する関数です。
        #    これは「順伝播(forward pass)」と呼ばれ、入力から出力への流れを意味します。

        x = x.view(-1, 28*28)  # 2次元画像を1次元に
        # ↑ 画像は (1, 28, 28) のような形ですが、全結合層に入れるために (784,) に変形します。
        #    -1は「バッチサイズは自動で合わせて」という意味。

        x = F.relu(self.fc1(x))
        # ↑ 第1層(fc1)に入力を通し、活性化関数ReLUで非線形変換します。
        #    ReLU(Rectified Linear Unit)は、0以下を切り捨て、0より大きい値だけを通します。

        x = self.fc2(x)
        # ↑ 第2層(fc2)に通して、最終的なクラスごとのスコア(logits)を出力します。

        return x
        # ↑ 出力(logits)は、CrossEntropyLossなどの損失関数にそのまま渡せます。


model = SimpleNet().to(device)
# ↑ 上で定義したモデルをインスタンス化し、CPUまたはGPUに移動します(.to(device))。
#    こうすることで、モデルとデータの両方が同じデバイス上で計算されるようになります。

print(model)
# ↑ モデルの構造(レイヤー構成など)を出力して確認します。


(5) 損失関数 & Optimizer

# 8) 損失関数 & 最適化手法

criterion = nn.CrossEntropyLoss()
# ↑ 「クロスエントロピー損失関数」を定義します。
#    これは分類問題(0〜9などのカテゴリ分類)でよく使われる損失関数です。
#    モデルの出力(logits)と、正解ラベルとの“ズレ”を数値化してくれます。
#    ズレが大きいほど損失も大きくなり、学習によってこの損失を最小化していきます。

optimizer = optim.Adam(model.parameters(), lr=0.001)
# ↑ 「Adam(アダム)最適化アルゴリズム」を使って学習を行う設定です。
#    model.parameters() は、学習すべきモデル内の全パラメータ(重みやバイアス)を指定します。
#    lr=0.001 は「学習率(learning rate)」と呼ばれ、どのくらいのスピードで学習するかを調整する重要な値です。

# 💡補足
# - 損失関数(criterion)は「どれくらい間違っているか?」を測るもの。
# - 最適化手法(optimizer)は「どうやってパラメータを直すか?」を決めるものです。
💡クロスエントロピー損失とは?

「正解をどれだけ自信を持って予測できたか?」を評価する指標です。

たとえば、正解が「3」の画像に対して、モデルが「3」と予測し、その確率が高ければ損失は小さく、逆に外れていれば損失は大きくなります。

数式でいうと:

損失 = -log(正解クラスの確率)

つまり、正解データについて、正解と予測した確率が高い=損失が小さいという仕組みです。

PyTorchではこの一行でOK:

criterion = nn.CrossEntropyLoss()

学習では、この損失をもとに「間違った分だけ」モデルが修正されるように工夫されています。


(6) 学習ループと損失推移グラフ

# 9) 学習ループの実行

num_epochs = 5
# ↑ 学習全体の繰り返し回数(エポック数)を指定します。
#    エポックとは、「全データを1周して学習させること」を1回と数えます。

epoch_losses = []
# ↑ 各エポックごとの損失(loss)の平均を保存するためのリストを用意します。

for epoch in range(num_epochs):
    running_loss = 0.0   # 1エポック内での累積損失
    total_batch = 0      # バッチ数のカウント

    for images, labels in train_loader:
        # ↑ データローダーから、画像とラベルを1バッチずつ取り出します。

        images = images.to(device)
        labels = labels.to(device)
        # ↑ 画像とラベルをGPUまたはCPUへ転送(モデルと同じデバイスにする必要があります)。

        optimizer.zero_grad()
        # ↑ 前のバッチで計算された勾配(grad)をリセットします。
        #    勾配は蓄積されるため、毎回0に初期化が必要です。

        outputs = model(images)
        # ↑ モデルに画像を入力し、出力(logits)を取得します。

        loss = criterion(outputs, labels)
        # ↑ 出力と正解ラベルの差(誤差)を計算します(損失関数)。

        loss.backward()
        # ↑ 誤差(loss)を使って、各パラメータの勾配を自動で計算します(逆伝播)。

        optimizer.step()
        # ↑ 勾配をもとに、モデルのパラメータを更新します(学習)。

        running_loss += loss.item()
        # ↑ そのバッチでの損失値を足し合わせます(.item() は純粋な数値に変換するためのもの)。

        total_batch += 1
        # ↑ バッチのカウントを1つ進めます。

    avg_loss = running_loss / total_batch
    # ↑ 1エポック分の平均損失を計算します。

    epoch_losses.append(avg_loss)
    # ↑ 平均損失を記録して、後でグラフに使えるようにします。

    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")
    # ↑ 各エポック終了後に、損失を表示して学習の進み具合を確認します。

print("学習が完了しました!")
# ↑ すべてのエポックが終わったことを表示します。

# 10) 損失の推移をプロット

plt.figure(figsize=(6, 4))
# ↑ グラフ用の図を作成(サイズ指定)。

plt.plot(range(1, num_epochs+1), epoch_losses, marker='o')
# ↑ 各エポックにおける損失の推移を線グラフで表示(丸印をつけて見やすくしています)。

plt.title("Training Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.grid(True)
# ↑ グラフの装飾(タイトル、軸ラベル、グリッド線)

plt.show()
# ↑ グラフを画面に表示します。

  • (a) 勾配リセット → (b) 順伝播 → (c) 損失計算 → (d) 逆伝播 → (e) パラメータ更新
  • 学習が進むと Loss が徐々に減少

(7) テストデータ評価 & 誤認識例の可視化

# 11) テストデータのDataLoaderを作成

test_dataset = torchvision.datasets.MNIST(
    root='./data',               # 学習時と同じフォルダに保存
    train=False,                 # テストデータ(False)を指定
    transform=transform,         # 学習時と同じ前処理(ToTensor+Normalize)を適用
    download=True                # 必要に応じてダウンロード
)
# ↑ テスト用のMNISTデータセット(10,000枚)を用意します。

test_loader = torch.utils.data.DataLoader(
    test_dataset,
    batch_size=64,
    shuffle=False                # テストでは順番をシャッフルしない(再現性のため)
)
# ↑ 学習時と同様、DataLoaderでテストデータを扱いやすくします。

print("テスト用データ数:", len(test_dataset))
# ↑ テストデータが10,000件あることを確認します。

# 12) テストデータを使って正解率を計算

model.eval()
# ↑ モデルを「評価モード」に切り替えます。
#    これはDropoutやBatchNormのような層の振る舞いを学習用から変更するために必要です。

correct = 0
total = 0
# ↑ 正解した数と、全体のサンプル数を数えるためのカウンター。

misclassified_images = []
misclassified_true = []
misclassified_pred = []
# ↑ 間違えた画像と、その正解ラベル/予測ラベルを保存するリスト(最大9件)

with torch.no_grad():
    # ↑ 評価中は勾配計算が不要なので、無効化して処理を軽く・速くします。
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)
        # ↑ テスト画像をモデルに通して、出力(各クラスのスコア)を取得。

        _, predicted = torch.max(outputs, 1)
        # ↑ 各画像について、最もスコアが高いクラスを予測結果とします。

        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        # ↑ 全体の枚数に加算し、正解だったものだけカウントを増やします。

        # 誤認識を9枚までピックアップ
        for i in range(len(labels)):
            if predicted[i] != labels[i]:
                if len(misclassified_images) < 9:
                    misclassified_images.append(images[i].cpu().numpy())
                    misclassified_true.append(labels[i].cpu().item())
                    misclassified_pred.append(predicted[i].cpu().item())
        # ↑ 間違えた画像・正解・予測をそれぞれ記録。
        #    GPU上のTensorをCPUに移して、NumPy配列や整数として扱えるように変換しています。

accuracy = correct / total
print(f"テストデータでの正解率: {accuracy*100:.2f}%")
# ↑ 全体に対して、どれくらい正解したか(正解率)をパーセントで表示します。

# 13) 誤認識した画像を可視化

if len(misclassified_images) > 0:
    fig = plt.figure(figsize=(8, 8))
    for i in range(len(misclassified_images)):
        ax = fig.add_subplot(3, 3, i+1)
        ax.imshow(np.squeeze(misclassified_images[i]), cmap='gray_r')
        # ↑ 誤認識した画像を白黒表示。squeezeで余分な次元を除去。

        ax.set_title(f"True: {misclassified_true[i]}, Pred: {misclassified_pred[i]}")
        # ↑ タイトルとして、「本当のラベル」と「予測されたラベル」を表示。

        ax.axis('off')
        # ↑ 枠線や目盛りを非表示にして画像を見やすくします。

    plt.suptitle("Misclassified Samples", fontsize=16)
    plt.show()
else:
    print("誤認識がありませんでした。高精度です!")
    # ↑ もし9枚以内に誤認識が見つからなければ、それも結果として表示。

  • model.eval() : 推論モード
  • torch.no_grad() : 勾配追跡を無効化し計算負荷を削減
  • 誤認識例を数枚確認することで、どのように間違えているか理解しやすい

(8) (オプション)混同行列の描画

# 14) 混同行列の可視化

y_true = []
y_pred = []
# ↑ 実際のラベル(正解)と、モデルの予測ラベルを保存するリストを用意します。

model.eval()
# ↑ 評価モードに切り替え(BatchNormやDropoutなどの挙動を固定)

with torch.no_grad():
    # ↑ 評価中は勾配計算が不要なので、処理を高速・軽量化します。
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)
        # ↑ モデルにテスト画像を入力して、クラスごとのスコアを取得

        _, predicted = torch.max(outputs, 1)
        # ↑ スコアが最も高いクラスを予測結果として取得します。

        y_true.extend(labels.cpu().numpy())
        y_pred.extend(predicted.cpu().numpy())
        # ↑ 正解ラベル(labels)と予測ラベル(predicted)をそれぞれCPUに移し、
        #    NumPy配列としてリストに追加します(extendで複数を一気に追加)。

cm = confusion_matrix(y_true, y_pred)
# ↑ sklearnの関数を使って「混同行列(confusion matrix)」を作成します。
#    行が実際のクラス、列が予測されたクラスを表し、どこで間違えたかがわかります。

plt.figure(figsize=(8,6))
# ↑ 描画サイズを指定して新しい図を作成

sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
# ↑ Seabornライブラリを使って、混同行列をヒートマップ形式で可視化します。
#    - annot=True:各マスに数値を表示
#    - fmt='d':整数形式で表示
#    - cmap='Blues':青系のカラーマップを使用

plt.title("Confusion Matrix (MNIST)")
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.show()
# ↑ グラフを表示します。
  • クラス0~9の誤認識分布が一目でわかる
  • 対角成分が多いほど正しく識別できている

4. 実行結果

セルをすべて実行すると、以下のようなログやグラフ・画像が出力されます。

  1. 学習前のサンプル画像
    • 手書き数字がどんな形でデータ化されているか分かる
  2. 損失表示 & 推移グラフ
    • 各エポックごとに損失が減少していく様子
  3. テストデータでの正解率
    • シンプルな全結合ネットでも5エポックで90~95%超が出やすい
  4. 誤認識例
    • 「True: 9, Pred: 4」など、間違えたケースを可視化
  5. 混同行列(オプション)
    • クラス間の誤分類を俯瞰できる

5. まとめと次回予告

MNISTは手書き数字0~9のモノクロ画像(28×28)。
PyTorchを使い、以下の流れで学習フローを体験しました。

  1. DataLoader でバッチ分割
  2. モデルに入力順伝播
  3. 損失計算
  4. 逆伝播
  5. パラメータ更新

医療画像や他分野のデータでも、基本構造は同じです。

医療AIへの応用イメージ

  • レントゲンやCT、病理画像などは、さらに大きなモデル(CNN/Transformer)転移学習が有効。
  • データ量やチャネル数は異なっても、学習ループの仕組みは同じ
  • 実運用では、患者情報の保護倫理審査などの面が非常に重要。

次回予告: 「データ準備と前処理の基礎(医療データの扱い方)」

  • テキスト・画像・音声のクリーニング、フォーマット変換
  • 患者データの匿名化、倫理審査、個人情報保護法やHIPAAなどの法令遵守
  • データ分割と過学習対策(Train/Validation/Test)

医療AIプロジェクトでは「データの質」が成果を大きく左右します。どうぞお楽しみに!

次章予告: もっとPythonと深層学習中級編

前章で触れた「計算グラフ」や「損失関数」、「誤差逆伝播」など、Pythonと深層学習中級編を、第2回の4章以降で、技術的な内容に触れていければと思います。


参考リンク


注意

  • 本記事の内容は執筆時点のものです。環境やバージョンによって変更の可能性があります。
  • 実際に患者情報を扱う際は、プライバシー保護と法令遵守を徹底してください。
  • 特に患者画像やカルテ情報は、組織ガイドラインや倫理審査(IRB)手続きを踏んだうえで適切に扱いましょう。

これで「MNISTを用いた簡単なニューラルネットの構築」の解説は終了です。
ぜひColab等で動かし、学習過程や誤認識を可視化しながら、ニューラルネットの理解を深めてください。

  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次