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% も正解したなと驚く。
1 – Trouser ズボン
2 – Pullover プルオーバー(長袖)
3 – Dress ドレス(ワンピース)
4 – Coat コート
5 – Sandal サンダル
6 – Shirt シャツ
7 – Sneaker スニーカー
8 – Bag バッグ
9 – Ankle boot アンクルブーツ