はじめに
前回は,多層パーセプトロン (MLP) のモデルをPyTorchで実装・学習し,MNIST手書き数字の認識を行った.
今回は,画像認識モデルで一般に使われている畳み込みニューラルネットワークをネットワークモデルそのものの理解から,PyTorchでの実装までを1から学習していく.その時のメモ書きです.
実行環境
Google Colaboratory
目次
1畳み込みニューラルネットワークの仕組み
2畳み込みニューラルネットワークをPyTorchで実装
1畳み込みニューラルネットワークの仕組み
2畳み込みニューラルネットワークをPyTorchで実装
2.1 畳み込みニューラルネットワークモデルの設計
まず初めに,学習に用いるモデルの設計を行う.
2.1.1 モデル設計に必要なライブラリのインポート
モデル設計に必要なライブラリのインポートを行う.
インポートした各パッケージの詳細は以下のようになっている.
パッケージ | 名前 |
---|---|
torch | PyTrochのリストを扱うもの |
torch.nn | ネットワークの構築 |
torch.nn.funcctional | 様々な関数の使用 |
torch.optim | 最適化アルゴリズムの使用 |
torchvision | 画像処理に関係する処理の使用 |
torchvision.transform | 画像変換機能の使用 |
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
2.1.2 乱数シードの固定
再現性を確保するため,乱数シードを設定する.
乱数シードを固定することにより,毎実行ごとで同じ疑似乱数を得られるようになる.
torch.manual_seed()
:torchの乱数シードの固定,ネットワークの重みの初期値の固定
torch.cuda.manual_seed()
:cuda専用の乱数シードの固定
torch.backends.cudnn.deterministic = True
:True の場合,cuDNN が決定論的畳み込みアルゴリズムのみを使用する
※決定的アルゴリズム (deterministic algorithm) は,計算機科学におけるアルゴリズムの種類であり,その動作が予測可能なものをいう.入力を与えられたとき,決定的アルゴリズムは常に同じ経路で計算を行い,常に同じ結果を返す.
※cuda専用の乱数シード固定の torch.cuda.manual_seed()
は torch.manual_seed()
を実行するだけで,cuda側の乱数シードも固定できるという記事を発見している.(真偽不明)
def setup_torch_seed(seed=1):
# pytorchに関連する乱数シードの固定を行う.
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
# 乱数シードを固定
setup_torch_seed()
2.1.3 ネットワークモデルの定義
畳み込みニューラルネットワーク (CNN) のモデルを作成する.
今回は,①畳み込み層→②プーリング層→③畳み込み層→④プーリング層→⑤全結合層 (中間層2層) で構成する.
①畳み込み層,②プーリング層
今回作成するCNNでは,①畳み込み層で kernel_size(33) のフィルタを用いて,16枚もの特徴マップ (Feature map) を得る.したがってフィルタの数も16個生成される.②プーリング層では,①で生成した特徴マップを局所領域 (22)の範囲から最大値を出力することで,特徴マップを半分の大きさにする.
③畳み込み層,④プーリング層
今回作成するCNNでは,③畳み込み層で kernel_size(33) のフィルタを用いて,①で生成した16枚もの特徴マップからさらに32枚もの特徴マップ (Feature map) を得る.④プーリング層では,③で生成した特徴マップを局所領域 (22)の範囲から最大値を出力することで,特徴マップを半分の大きさにする.
今回は,パディングをしてフィルタによって画像の大きさに変化がないように設計しているため,MNISTデータセットの大きさ (28 * 28) が,①畳み込み層で得られる特徴マップでは画像サイズに変化はない.②プーリング層によって特徴マップが半分の大きさになり, (14 * 14) となる.③畳み込み層では,①同様に画像サイズに変化はなく,④プーリング層によって特徴マップがさらに半分になり (7 * 7) となる.
⑤全結合層 (Fully Connectedlayer)
今回作成するCNNでは,④で生成された特徴マップ (7 * 7) が32枚あるため,全結合層部分の入力層部分の大きさは,(7 * 7) *32 = 1568 となる.中間層の大きさは,1024,2048として設計し,0から9の10個の手書き数字の認識のため,出力層は10として設計した.
class MNIST_CNN(nn.Module):
def __init__(self):
super().__init__()
# 畳み込み層の定義
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
# 全結合層の定義
self.fc1 = nn.Linear(7*7*32, 1024)
self.fc2 = nn.Linear(1024, 2048)
self.fc3 = nn.Linear(2048, 10)
# プーリング層の定義
self.pool = nn.MaxPool2d(2,2)
def forward(self, x):
y = self.pool(F.relu(self.conv1(x)))
y = self.pool(F.relu(self.conv2(y)))
y = y.view(y.size()[0], -1)
y = F.relu(self.fc1(y))
y = F.relu(self.fc2(y))
y = self.fc3(y)
return y
2.2 学習
1 畳み込みニューラルネットワークモデルの設計で行った畳み込みニューラルネットワークの用いてMNISTデータセットの学習を行う.
2.2.1 学習の準備
学習を行う関数と,評価する関数をユーティリティな関数として定義する.
2.2.1.1 モデルを学習する関数の定義
モデルを学習する関数の定義を行う.
引数:ネットワークモデル,学習データ,損失関数,最適化関数,device
返却値:損失 (平均)
def train(model, train_loader, criterion, optimizer, device):
# ネットワークモデルを学習モードに設定
model.train()
sum_loss = 0.0
count = 0
for data, label in train_loader:
count += len(label)
data, label = data.to(device), label.to(device)
optimizer.zero_grad()
outputs = model(data)
loss = criterion(outputs, label)
loss.backward()
optimizer.step()
sum_loss += loss.item()
return sum_loss/count
2.2.1.2 モデルを評価する関数の定義
モデルを評価する関数の定義を行う.
引数:ネットワークモデル,検証データ,損失関数,device
返却値:損失 (平均),正解率
def test(model, test_loader, criterion, device):
# ネットワークモデルを評価モードに設定
model.eval()
sum_loss = 0.0
count = 0
correct = 0
with torch.no_grad():
for data, label in test_loader:
count += len(label)
data, label = data.to(device), label.to(device)
outputs = model(data)
loss = criterion(outputs, label)
sum_loss += loss.item()
pred = torch.argmax(outputs, dim=1)
correct += torch.sum(pred == label)
accuracy_rate = (correct / count).cpu().detach()
return sum_loss/count, accuracy_rate
2.2.2 データセットの読み込み
MNISTデータセット (学習データ) を読み込む.
#訓練データ
train_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=transforms.ToTensor(),
download = True)
#検証データ
test_dataset = torchvision.datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor(),
download = True)
2.2.3 ミニバッチ処理
今回は,ミニバッチ学習にて学習を行う.
したがって,torch.utils.data.DataLoader()
を用いてミニバッチ処理を行う.
batch_size = 256
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=True)
next(iter(train_loader))[0].shape
実行結果
torch.Size([256, 1, 28, 28])
2.2.4 GPUの使用確認
torch.cuda.is_available()
関数の戻り値に応じて,CPUかGPUへの割り当てを表す torch.device
オブジェクトを作成.toメソッドに指定することで,GPUへ必要なものを転送することができるようになった.
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print("device : ", device)
実行結果
device : cuda:0
以上より,GPUが使用可能であることが確認できる.
2.2.5 学習モデルのインスタンス化・損失関数と最適化関数の指定
1.3 ネットワークモデルの定義
で定義した MNIST_CNN()
学習モデルをインスタンス化,損失関数と最適化関数の指定を行う.
今回は,損失関数には,交差エントロピー誤差関数を指定し,最適化関数には,確率的勾配降下法を指定した.
またそれを,GPUが使用可能な場合はGPUに転送する.
model = MNIST_CNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.005)
model.to(device)
criterion.to(device)
print("model : ", model)
print("criterion : ", criterion)
print("optimizer : ", optimizer)
実行結果
model : MNIST_CNN(
(conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=1568, out_features=1024, bias=True)
(fc2): Linear(in_features=1024, out_features=2048, bias=True)
(fc3): Linear(in_features=2048, out_features=10, bias=True)
(pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
criterion : CrossEntropyLoss()
optimizer : SGD (
Parameter Group 0
dampening: 0
differentiable: False
foreach: None
lr: 0.005
maximize: False
momentum: 0
nesterov: False
weight_decay: 0
)
2.2.6 学習
epoch数を指定し,epochの分学習,テストを繰り返し,結果をそれぞれリストに追加していく.
num_epoch = 20
train_loss_list = []
test_loss_list = []
accuracy_rate_list = []
for epoch in range(1, num_epoch+1, 1):
train_loss = train(model, train_loader, criterion, optimizer, device)
test_loss, accuracy_rate = test(model, test_loader, criterion, device)
train_loss_list.append(train_loss)
test_loss_list.append(test_loss)
accuracy_rate_list.append(accuracy_rate)
print("epoch : {}, train_loss : {}, test_loss : {}, accuracy_rate : {}".format(epoch, train_loss, test_loss, accuracy_rate))
実行結果
epoch : 1, train_loss : 0.009005734419822694, test_loss : 0.009182104802131653, accuracy_rate : 0.1021999940276146
epoch : 2, train_loss : 0.008971755707263947, test_loss : 0.009141012477874756, accuracy_rate : 0.2298000007867813
epoch : 3, train_loss : 0.008920254898071288, test_loss : 0.009067785000801087, accuracy_rate : 0.45669999718666077
epoch : 4, train_loss : 0.008812127292156219, test_loss : 0.008897637486457825, accuracy_rate : 0.6326999664306641
epoch : 5, train_loss : 0.008479110908508301, test_loss : 0.008232475662231445, accuracy_rate : 0.7044000029563904
epoch : 6, train_loss : 0.006639669003089269, test_loss : 0.00453096650838852, accuracy_rate : 0.7701999545097351
epoch : 7, train_loss : 0.0031515990376472475, test_loss : 0.00238577398955822, accuracy_rate : 0.8409000039100647
epoch : 8, train_loss : 0.002059006458520889, test_loss : 0.0018029712349176407, accuracy_rate : 0.8759999871253967
epoch : 9, train_loss : 0.0016462658201654753, test_loss : 0.0014823764503002167, accuracy_rate : 0.8973000049591064
epoch : 10, train_loss : 0.0014390044969817002, test_loss : 0.0013546662911772729, accuracy_rate : 0.898099958896637
epoch : 11, train_loss : 0.001315474500010411, test_loss : 0.0013093833580613136, accuracy_rate : 0.9049999713897705
epoch : 12, train_loss : 0.0012224644114573796, test_loss : 0.0011557256728410722, accuracy_rate : 0.9121999740600586
epoch : 13, train_loss : 0.001146275793015957, test_loss : 0.0010716710284352303, accuracy_rate : 0.9203999638557434
epoch : 14, train_loss : 0.0010802611974378427, test_loss : 0.0010353849358856679, accuracy_rate : 0.9214999675750732
epoch : 15, train_loss : 0.0010206743674973646, test_loss : 0.0009917883709073068, accuracy_rate : 0.9246999621391296
epoch : 16, train_loss : 0.0009676709694166979, test_loss : 0.0008946484565734863, accuracy_rate : 0.9309999942779541
epoch : 17, train_loss : 0.0009156493840118249, test_loss : 0.0008705135397613048, accuracy_rate : 0.9337999820709229
epoch : 18, train_loss : 0.0008658854047457377, test_loss : 0.0008229943409562111, accuracy_rate : 0.9375999569892883
epoch : 19, train_loss : 0.0008227167119582494, test_loss : 0.0008264349706470967, accuracy_rate : 0.9412999749183655
epoch : 20, train_loss : 0.0007790606152266264, test_loss : 0.0007312820412218571, accuracy_rate : 0.9455999732017517
2.2.6 学習済みのパラメータの保存
学習可能なモデルの各パラメータを保存する.
デバイスの情報と学習可能なパラメータの値を取得することにより,学習済みのモデルを管理する.
今回は,学習はGPUを用いてを用いて行い,CPUで保存する.
モデルはtorch.save()
で直接保存することもできるが,state_dict()
で保存した方が無駄な情報を削れてファイルサイズを小さくできる.公式ページにもstate_dict()
で保存することが推奨されている.
torch.save(model.to('cpu').state_dict(),'model.pth')
2.3 学習の確認
2.3.1 学習推移の可視化
学習の際に記録した,学習時の損失と評価時の損失の推移をグラフに示す.
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(range(1, len(train_loss_list)+1, 1), train_loss_list, c='b', label='train loss')
plt.plot(range(1, len(test_loss_list)+1, 1), test_loss_list, c='r', label='test loss')
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.grid()
plt.show()
2.3.2 認識率の可視化
評価の際に記録した,評価時の認識率 (正解率) の可視化を行う.
import matplotlib.pyplot as plt
plt.plot(range(1, len(accuracy_rate_list)+1, 1), accuracy_rate_list, c='b', label='accuracy_rate')
plt.xlabel("epoch")
plt.ylabel("accuracy_rate")
plt.legend()
plt.grid()
plt.show()
2.3.3 クラス分類毎の正解率の算出
各数字毎の正解率の算出を行う.
model.to(device)
model.eval()
class_count_list = [0,0,0,0,0,0,0,0,0,0]
class_accuracy_rate_list = [0,0,0,0,0,0,0,0,0,0]
for i in range(len(test_dataset)):
image, label = test_dataset[i]
image = image.view(-1, 1, 28, 28).to(device)
class_count_list[label] = class_count_list[label] + 1
# 推論
prediction_label = torch.argmax(model(image))
if label == prediction_label:
class_accuracy_rate_list[label] = class_accuracy_rate_list[label] + 1
for i in range(10):
class_accuracy = class_accuracy_rate_list[i] / class_count_list[i]
sum_accuracy = sum(class_accuracy_rate_list) / sum(class_count_list)
print("class{} : {:.5f} ( {} / {})".format(i, class_accuracy, class_accuracy_rate_list[i], class_count_list[i]))
print("sum_accuracy : {} ( {} / {})".format(sum_accuracy, sum(class_accuracy_rate_list), sum(class_count_list)))
実行結果
class0 : 0.99184 ( 972 / 980)
class1 : 0.99295 ( 1127 / 1135)
class2 : 0.94864 ( 979 / 1032)
class3 : 0.93861 ( 948 / 1010)
class4 : 0.99287 ( 975 / 982)
class5 : 0.99103 ( 884 / 892)
class6 : 0.96451 ( 924 / 958)
class7 : 0.99027 ( 1018 / 1028)
class8 : 0.92813 ( 904 / 974)
class9 : 0.94648 ( 955 / 1009)
sum_accuracy : 0.9686 ( 9686 / 10000)
2.3.4 学習済みモデルを用いた推論
前章で学習したモデルを用いて,datasetの画像データを1枚づつ入力して,推論結果を受け取りその結果を示す.Dataloaderで生成されるデータとdatasetに格納されているデータの形が異なるので,データの形を整える処理が必要となる.
試しに,50枚の推論結果を表示させてみることとする.
model.eval()
plt.figure(figsize=(20, 15))
for i in range(50):
image, label = test_dataset[i]
image = image.view(-1, 1, 28, 28).to(device) #☆
# 推論
prediction_label = torch.argmax(model(image))
ax = plt.subplot(5, 10, i+1)
ax.imshow(image.detach().to('cpu').numpy().reshape(28, 28), cmap='gray')
ax.axis('off')
ax.set_title('label : {}\n Prediction : {}'.format(label, prediction_label), fontsize=15)
plt.show()
2.4 CNNモデルの判断根拠の可視化
以上で作成した畳み込みニューラルネットワーク (CNN) モデルの判断根拠を Grad-CAM を用いて可視化する.
2.4.1 必要ライブラリのインストール
ライブラリgrad-cam
を以下のコマンドを実行してインストールする.
!pip install grad-cam -q
2.4.2 ライブラリのインポート
前節でインストールしたライブラリをインポートし,モデルの中身を確認する.
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.image import show_cam_on_image
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
print(model)
実行結果
MNIST_CNN(
(conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=1568, out_features=1024, bias=True)
(fc2): Linear(in_features=1024, out_features=2048, bias=True)
(fc3): Linear(in_features=2048, out_features=10, bias=True)
(pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
2.4.3 判断根拠の可視化
まず初めに,1枚のみの画像に対して判断根拠の可視化を行う.
target_layers = [model.conv2]
cam = GradCAM(model = model, target_layers = target_layers, use_cuda = device)
image, label_ = test_dataset[0]
input_image = image.view(-1, 1, 28, 28).to(device)
label = [ClassifierOutputTarget(label_)]
image_d = image.cpu().detach().numpy().copy().reshape(28,28,1)
input_tensor = image #(batch_size, channel, height, width)
grayscale_cam = cam(input_tensor = input_image, targets = label)
grayscale_cam = grayscale_cam[0, :]
visualization = show_cam_on_image(image_d, grayscale_cam, use_rgb = True)
prediction_label = torch.argmax(model(input_image))
plt.imshow(visualization)
plt.title('label : {}\n Prediction : {}'.format(label_, prediction_label), fontsize=15)
plt.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False, bottom=False, left=False, right=False, top=False)
plt.show()
つぎに,学習済みモデルを用いた推論で可視化した手書き数字画像に対して判断根拠の可視化を行う.
target_layers = [model.conv2]
cam = GradCAM(model = model, target_layers = target_layers, use_cuda = device)
plt.figure(figsize=(20, 15))
for i in range(50):
image, label_ = test_dataset[i]
input_image = image.view(-1, 1, 28, 28).to(device)
label = [ClassifierOutputTarget(label_)]
image_d = image.cpu().detach().numpy().copy().reshape(28,28,1)
input_tensor = image #(batch_size, channel, height, width)
grayscale_cam = cam(input_tensor = input_image, targets = label)
grayscale_cam = grayscale_cam[0, :]
visualization = show_cam_on_image(image_d, grayscale_cam, use_rgb = True)
prediction_label = torch.argmax(model(input_image))
ax = plt.subplot(5, 10, i+1)
ax.imshow(visualization)
ax.set_title('label : {}\n Prediction : {}'.format(label_, prediction_label), fontsize=15)
ax.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False, bottom=False, left=False, right=False, top=False)
plt.show()
2.5 畳み込み層におけるデータの流れ
畳み込み層で生成される特徴マップを可視化し,データの流れを理解する.
2.5.1 特徴マップの可視化
1回目の畳み込み層によって生成される特徴マップについて可視化する.
import cv2
plt.figure(figsize=(15, 15))
for i in range(16):
image, label = test_dataset[0]
image = image.cpu().detach().numpy().copy().reshape(28,28)
conv_kernel = (model.state_dict()['conv1.weight'][i]).cpu().detach().numpy().copy().reshape(3,3)
future_map = cv2.filter2D(src=image, ddepth=-1, kernel=conv_kernel)
ax = plt.subplot(4, 4, i+1)
ax.imshow(future_map,cmap='gray')
ax.set_title(f"conv1_feature_map[{i}]")
ax.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False, bottom=False, left=False, right=False, top=False)
plt.show()
2.5.2 フィルタの可視化
1回目の畳み込み層における各フィルタの可視化を行う.
plt.figure(figsize=(15, 15))
for i in range(16):
conv1_1 = (model.state_dict()['conv1.weight'][i]).cpu().detach().numpy().copy()
ax = plt.subplot(4, 4, i+1)
ax.imshow(conv1_1.reshape(3,3), cmap='gray')
ax.set_title(f"conv1.weight[{i}]")
ax.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False, bottom=False, left=False, right=False, top=False)
plt.show()
2.6 学習済みパラメータによる推論
2.6.1 学習パラメータの読み込み
modelの学習によって得られた学習パラメータを,新しいモデルmodel2に読み込む.
device = 'cpu'
model2 = MNIST_CNN()
model2.to(device)
model2.load_state_dict(torch.load('model.pth', map_location=torch.device('cpu')))
2.6.2 ネットワークによる推論
読み込んだ各パラメータをを用いて学習をせずにネットワークを持ちいた推論を行う.
#訓練データ
train_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=transforms.ToTensor(),
download = True)
#検証データ
test_dataset = torchvision.datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor(),
download = True)
batch_size = 256
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=True)
model2.eval()
plt.figure(figsize=(20, 15))
for i in range(50):
image, label = test_dataset[i]
image = image.view(-1, 1, 28, 28).to(device) #☆
# 推論
prediction_label = torch.argmax(model2(image))
ax = plt.subplot(5, 10, i+1)
ax.imshow(image.detach().to('cpu').numpy().reshape(28, 28), cmap='gray')
ax.axis('off')
ax.set_title('label : {}\n Prediction : {}'.format(label, prediction_label), fontsize=15)
plt.show()