(35)【PyTorchでMNIST #6】不正解画像と判定結果をJPEG画像出力する。

投稿者: | 2025年4月23日

656 views

この記事は最終更新から 378日 が経過しています。

【1】やりたいこと

3 epochsで正解率が 97%ということは、
10,000個のテストデータに対して、不正解が 300個しかなかったということだ。

この賢い AIが、どんな画像を苦手としているのか?
を見てみたい。

【2】やってみる

プログラムを変更する必要がある。
変更方針は以下の通り。
・テスト実行時、テストデータに対する判定結果を記録しておく。→ 画像作成時にこれを再利用する。
・出力画像の中には、画像と判定結果(ラベルごとの出力値)を一緒に表示する。

1) プログラミング

(1) trainer.py

前回のプログラム からの変更点をハイライト表示する。

import torch
import torch.nn as nn
import torch.optim as optim

class Trainer:
    #//////////////////////////////////////////////////////////////////////////
    def __init__(self, model, device, lr=0.001):
        self.model = model.to(device)
        self.device = device
        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = optim.Adam(self.model.parameters(), lr=lr)

    #//////////////////////////////////////////////////////////////////////////
    def train(self, train_loader, epochs=3):
        self.model.train()
        for epoch in range(epochs):
            for data, target in train_loader:
                data, target = data.to(self.device), target.to(self.device)
                self.optimizer.zero_grad()
                output = self.model(data)
                loss = self.criterion(output, target)
                loss.backward()
                self.optimizer.step()
            print(f"[{epoch+1}/{epochs}] Epoch complete")

    #//////////////////////////////////////////////////////////////////////////
    def test(self, test_loader):
        self.model.eval()
        correct = 0
        total = 0
        all_images = []      # 出力用:全画像データ
        all_probs  = []      # 出力用:全テスト結果
        all_labels = []      # 出力用:全画像データの正解ラベル
        #----------------------------------------------------------------------
        with torch.no_grad():
            for data, target in test_loader:
                data, target = data.to(self.device), target.to(self.device)
                outputs = self.model(data)
                _, predicted = torch.max(outputs.data, 1)
                total += target.size(0)
                correct += (predicted == target).sum().item()
                # 出力用に今回バッチのテスト結果を退避しておく。
                probs = torch.softmax(outputs, dim=1)
                all_images.extend(data.cpu())
                all_probs.extend(probs.cpu())
                all_labels.extend(target.cpu())
        #----------------------------------------------------------------------
        acc = 100 * correct / total
        print(f"Test Accuracy: {acc:.2f}%")
        return acc, all_images, all_probs, all_labels

    #//////////////////////////////////////////////////////////////////////////
    def save(self, path):
        torch.save(self.model.state_dict(), path)
        print(f"Model saved to {path}")

    #//////////////////////////////////////////////////////////////////////////
    def load(self, path):
        self.model.load_state_dict(torch.load(path, map_location=self.device))
        print(f"Model loaded from {path}")

(2) control_test.py

コマンドライオプションで -v を指定した場合に画像出力するようにした。
画像出力関数 output_results_image() を新規追加した。
target=”_blank”>前回のプログラム からの変更点をハイライト表示する。

from torch.utils.data import DataLoader
from net_model import Net
from trainer import Trainer
from dataset_MNIST import MyDataset
import torch
import argparse
import matplotlib.pyplot as plt
import numpy as np
import os

#//////////////////////////////////////////////////////////////////////////////
def output_results_image(images, probs_list, labels):
    os.makedirs("results", exist_ok=True)  # 出力先ディレクトリを作成
    #for idx in range(len(images)):
    for idx in range(1000):
        prob = probs_list[idx].numpy()     # prob は長さ10の確率ベクトル(各数字が正解である確率)
        predicted_label = np.argmax(prob)  # 確率ベクトルの中で最も値が大きいインデックス(つまり予測ラベル)を取り出す。
        true_label = labels[idx]           # この画像の正解ラベル(教師データ)を取り出す。
        if predicted_label == true_label:  # 正解画像の場合はスキップする。
            continue
        nClasses = len(prob)

        # images[idx] は形状が (1, 28, 28) のテンソル(1チャネルの画像)なので、
        # .squeeze(0) で先頭の次元(1)を消して (28, 28) にし .numpy() で NumPy 配列に変換する。
        img = images[idx].squeeze(0).numpy()

        # 画像1枚ごとに新しい図を作る(サイズは 4インチ × 4インチ)
        # save実行時の dpi=150 で画像サイズを更に調整可能
        # fig は図全体、ax はその中の描画領域
        fig, ax = plt.subplots(figsize=(3, 3))  # 画像ごとに新しい図を作成
        ax.set_facecolor('white')

        # 上半分に画像
        # 画像をグレースケールで表示する。
        # X軸は 0〜28(matplotlib内部の仮想的な座標系の数値)
        # Y軸は 30〜56 にしてセル内の上側(上半分)に表示されるようにする。
        ax.imshow(img, cmap='gray', extent=[0.4, 28.4, 30, 56])

        # 下半分に棒グラフ(正解は赤、他は灰色)
        bar_colors = ['red' if i == true_label else 'gray' for i in range(nClasses)]
        # 棒グラフを描く。
        # Bar横位置(X座標): 0, 3.2, 6.4, ..., 28.8(ラベル0〜9の位置)
        # Bar高さ: prob(=出力層の値) * 28 → 0〜28の範囲にスケーリング
        # Bar幅: 3
        x_positions = np.arange(nClasses) * 3.2
        ax.bar(x_positions, prob * 28, width=3, bottom=0, color=bar_colors)

        # 軸設定(水平方向グリッド付き)
        # X軸の目盛りを0〜9のラベルに対応させて表示する。
        ax.set_xticks(x_positions)
        ax.set_xticklabels([str(i) for i in range(nClasses)], fontsize=8)
        # Y軸も0.0〜1.0の範囲に目盛りを付けるが、表示は28スケールに合わせた位置になる。
        y_ticks = np.arange(0.0, 1.01, 0.1) * 28
        ax.set_yticks(y_ticks)
        ax.set_yticklabels([f"{y:.1f}" for y in np.arange(0.0, 1.01, 0.1)], fontsize=7)
        # Y軸に点線のグリッド線を引く。線の色は薄いグレー、太さ0.5
        ax.yaxis.grid(True, linestyle='--', linewidth=0.5, color='lightgray')

        # 不要な余白を削除。図全体の余白を自動で調整して、見やすくする。
        plt.tight_layout()

        # JPEGとして保存(例:results/image_00000.jpg)
        filename = f"results/i_{idx:05}.jpg"
        plt.savefig(filename, dpi=150)
        plt.close(fig)  # メモリ節約のため明示的に閉じる

#//////////////////////////////////////////////////////////////////////////////
def execTest( prmfile, visualize ):
    # データ
    dataset = MyDataset()
    test_loader = DataLoader(dataset.get_test_dataset(), batch_size=1000, shuffle=False)
    # 環境
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = Net()
    trainer = Trainer(model, device)
    # 実行
    trainer.load(prmfile)
    acc, images, probs, labels = trainer.test(test_loader)
    # 結果表示
    if visualize:
        output_results_image(images, probs, labels)

#//////////////////////////////////////////////////////////////////////////////
def main():
    parser = argparse.ArgumentParser(description="Load and test trained MNIST model.")
    parser.add_argument('-p', type=str, required=True, help='Filename of trained model to load')
    parser.add_argument('-v', action='store_true', help='Visualize test result samples')
    args = parser.parse_args()
    execTest(prmfile=args.p, visualize=args.v)

#//////////////////////////////////////////////////////////////////////////////
if __name__ == "__main__":
    main()

2) 実行結果

コマンドライオプションで -v を指定してテストプログラムを実行する。

$ python control_test.py -p AAA.pth -v
Model loaded from AAA.pth
Test Accuracy: 96.89%

すると、直下の results ディレクトリに前述の通りの画像が出力される。

個々の画像はこんな仕様で出力される。
・上半分に、テスト画像を表示する。
・下半分に、ラベルごとの判定結果を表示する。(出力層の各ニューロンの出力値をsoftmax関数に通した値)
・赤色のバーが正解ラベルだ。

【3】さらに賢くチューンニングした AIでやってみる

上で使った AIのネットワーク構成は以下の通り。
INPUT[28][28] → Full[128] → ReLU → Full[64] → ReLU → Full[10]

画像認識に対して有効な CNNを使った以下のネットワーク構成で試してみる。
INPUT[28][28] → CNN[16][28][28] → ReLU → MaxPool[16][14][14] → CNN[32][14][14] → ReLU → MaxPool[32][7][7] → Full[256] → ReLU → Full[10]

import torch.nn as nn
import torch

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # bs : batch_size
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, padding=1)  # [bs][1][28][28] → [bs][16][28][28]
        self.pool  = nn.MaxPool2d(kernel_size=2, stride=2)                                #  → [bs][16][14][14]               
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1) #  → [bs][32][14][14] →pool→ [bs][32][7][7]
        self.fc1   = nn.Linear(in_features=32 * 7 * 7, out_features=256)                  #  → [bs][32x7x7] → [bs][256]
        self.fc2   = nn.Linear(in_features=256, out_features=10)                          #  → [bs][256] → [bs][10] 

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))   #   CNN → ReLU → Max.pooling
        x = self.pool(torch.relu(self.conv2(x)))   # → CNN → ReLU → Max.pooling
        x = x.view(-1, 32 * 7 * 7)                 # → [bs][32][7][7] → flatten [bs][32x7x7=1568]
        x = torch.relu(self.fc1(x))                # → [1568] → FC→ [256] → ReLU
        x = self.fc2(x)                            # → [10]
        return x

ついでに epoch数を 10に増やして学習を実行した。
→ 下記の通り、正解率は 99.02% に上昇した。

$ python control_train.py -p BBB.pth
[1/10] Epoch complete
[2/10] Epoch complete
[3/10] Epoch complete
[4/10] Epoch complete
[5/10] Epoch complete
[6/10] Epoch complete
[7/10] Epoch complete
[8/10] Epoch complete
[9/10] Epoch complete
[10/10] Epoch complete
Test Accuracy: 99.02%
Model saved to BBB.pth

続けて、NG画像の結果を JPEG画像ファイル出力させる。

$ python control_test.py -p BBB.pth -v
Model loaded from BBB.pth
Test Accuracy: 99.02%

正解率 99%の AIが苦手とした画像はこんなものだった。

全体的に、「惜しい!」という結果が見られなかった。
人が目視すれば「明らかに違う」と判断できる画像でも、AIが自信満々に間違えているものばかりが残った。


アクセス数(直近7日): ※試験運用中、BOT除外簡易実装済
  • 2026-05-08: 0回
  • 2026-05-07: 0回
  • 2026-05-06: 1回
  • 2026-05-05: 1回
  • 2026-05-04: 0回
  • 2026-05-03: 0回
  • 2026-05-02: 0回
  • コメントを残す

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です