(44)【PyTorch】Fashion-MNISTの不正解画像を眺める。

投稿者: | 2025年5月17日

739 views

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

【1】やりたいこと

MNISTと同じ 28×28ピクセルのグレースケール画像のデータセットがあるとのこと。
その名も Fashion-MNIST
衣類画像 10種類を持つ画像分類用のデータセットだ。

https://github.com/zalandoresearch/fashion-mnist

今回は、これを PyTorchを使って学習させ、
どんな画像が苦手なのかを眺めてみる。

使用するプログラムは (35)【PyTorchでMNIST #6】不正解画像と判定結果をJPEG画像出力する。 で作ったものを流用する。
DataLoaderだけを今回用に作り替える。

【2】やってみる

1) プログラム・ソースコード

過去記事 (33)【PyTorchでMNIST #4】プログラムの保守性を向上させる。 に書いたように、機能別に全 5ファイルに分割済みだ。

(1) dataset_FMNIST.py

データセットファイルにアクセスするためのクラス定義だ。
今回は PyTorchがあらかじめ用意してくれている datasets.FashionMNIST クラスを使う。

これを自作したい場合は (38)【PyTorchでCIFAR-10 #1】自動認識してみる。 に書いたようにすればよい。
ブラックボックス化を回避できる。

from torchvision import datasets, transforms

class FMNISTDataset:
    def __init__(self, data_dir='./data'):
        self.transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])
        self.data_dir = data_dir

    def get_train_dataset(self):
        return datasets.FashionMNIST(self.data_dir, train=True, download=True, transform=self.transform)

    def get_test_dataset(self):
        return datasets.FashionMNIST(self.data_dir, train=False, download=True, transform=self.transform)

(2) net_model.py

Neural Networkを定義するクラスだ。
今回のネットワーク構成は以下の通り。

[Input[28][28]]
 → [CNN[16][28][28]] → ReLU
 → [Max.Pool[16][14][14]]
 → [CNN[32][14][14]] → ReLU
 → [Max.Pool[32][7][7]]
 → [Full[256]] → ReLU
 → [Output[10]] → Softmax

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] テンソルの形変換(reshape)32チャンネル・縦7・横7の特徴マップを、32×7×7=1568個の特徴ベクトルに1列に並び替える
        x = torch.relu(self.fc1(x))                # → [1568] → FC→ [256] → ReLU
        x = self.fc2(x)                            # → [10]
        return x

(3) trainer.py

モデルの訓練とテストを実行するクラスだ。
学習済みパラメータの Save / Recallの機能も持つ。

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}")

(4) control_train.py

モデルの訓練を制御するエントリーポイントだ。
先に記した Trainerクラスを使い、学習を制御する。
学習の完了後に、学習済みパラメータファイルを出力する。

from torch.utils.data import DataLoader
from net_model import Net
from trainer import Trainer
from dataset_FMNIST import FMNISTDataset
import torch
import argparse
from datetime import datetime

#//////////////////////////////////////////////////////////////////////////////
def execTrain( save_path ):
    # データ
    dataset = FMNISTDataset()
    train_loader = DataLoader(dataset.get_train_dataset(), batch_size=64, shuffle=True)
    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.train(train_loader, epochs=10)
    trainer.test(test_loader)
    trainer.save(save_path)

#//////////////////////////////////////////////////////////////////////////////
def main():
    parser = argparse.ArgumentParser(description="Train and save model")
    parser.add_argument('-p', type=str, help='Filename to save trained model')
    args = parser.parse_args()
    # ファイル名の決定
    if args.p:
        save_path = args.p
    else:
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        save_path = f"learned_model_{now}.pth"
    execTrain(save_path=save_path)

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

(5) control_test.py

モデルのテストを制御するエントリーポイントだ。
学習済みパラメータファイルの指定が必須だ。
テスト結果 NGのテスト画像についてのレポートファイルを出力するため、処理量が多い。

import torch
from torch.utils.data import DataLoader
from net_model import Net
from trainer import Trainer
from dataset_FMNIST import FMNISTDataset
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(1000):
    for idx in range(len(images)):
        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)

        ax.set_xticks(x_positions)
        # X軸の目盛りラベルは非表示にして、代わりに個別に描画(色分けするため)
        ax.set_xticklabels([''] * nClasses)  # 一旦ラベルを空白にする

        for i in range(nClasses):
            label_color = 'red' if i == true_label else 'black'
            ax.text(x_positions[i], -4, str(i),
                    ha='center', va='top', fontsize=8, color=label_color)



        # 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 = FMNISTDataset()
    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:
        #visualize_results(images, probs, labels)
        output_results_image(images, probs, labels)

#//////////////////////////////////////////////////////////////////////////////
def main():
    parser = argparse.ArgumentParser(description="Load and test trained 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) プログラム実行

訓練実行は control_train.py を動かせばよい。
-p オプションを指定すれば、任意の学習済みパラメータファイル名にできる。

$ python control_train.py -p myParam.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: 91.83%
Model saved to myParam.pth

今回は 10epochs で正解率 91.83% のモデルが出来上がった。
この学習済みパラメータは Python pickle でシリアライズされたデータとして myParam.pth に格納されている。

続けて、出力された学習済みパラメータファイルを指定してテスト実行する。
必ず -p オプションで学習済みパラメータファイルを指定しなければならない。
また、不正解レポート画像を出力させるには -v オプションを指定する。

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

テスト実行が完了すると、本記事のタイトルで書いた通り、不正解と判定された画像がレポート付き JPEG画像ファイルで出力される。
今回の目的はこの画像を眺めることだ。

3) 実行結果

10epochsで正解率 91.83%のモデルが出来上がった。
テスト画像の枚数は 10,000枚もあるので、不正解画像だけで 828枚もある。

すべてを表示できないので、ランダムで本記事にいくつか添付する。
ちなみに、Fashion-MNISTのラベルは左下に記した通り。
赤色文字、赤色バー は正解ラベルだ。

正直な感想は・・・
これは人が目で見て判断するのも難しいなぁ
と思った。

低解像度で、1チャンネルで、似ている物ばかりで…
逆によく 91.8% も正解したなと驚く。

0 – T-shirt/top Tシャツ/トップス
1 – Trouser ズボン
2 – Pullover プルオーバー(長袖)
3 – Dress ドレス(ワンピース)
4 – Coat コート
5 – Sandal サンダル
6 – Shirt シャツ
7 – Sneaker スニーカー
8 – Bag バッグ
9 – Ankle boot アンクルブーツ

アクセス数(直近7日): ※試験運用中、BOT除外簡易実装済
  • 2026-06-22: 7回
  • 2026-06-21: 5回
  • 2026-06-20: 0回
  • 2026-06-19: 3回
  • 2026-06-18: 2回
  • 2026-06-17: 0回
  • 2026-06-16: 1回
  • コメントを残す

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