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が自信満々に間違えているものばかりが残った。
