はじめに
前回はPyTorchを使ってCNNを構築し、CIFAR-10の分類を行いましたが、がっつり過学習してしまいました。今回は対策として、ドロップアウト、Batch Normalization、データ拡張を実装します。
できたこと
- データセット: CIFAR-10
- 機械学習モデル: CNN
- 活性化関数: ReLU
- 損失関数: 交差エントロピー関数
- 最適化手法: Adam
- 過学習対策
- ドロップアウト
- Batch Normalization
- Data Augumentation
精度は前回の65%から89%まで向上し、過学習も防止できました。
ライブラリ
# 必要ライブラリのインポート
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display
from tqdm.notebook import tqdm
# PyTorch関連ライブラリのインポート
import torch
import torch.nn as nn
import torch.optim as optim
from torchinfo import summary
from torchviz import make_dot
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
import torchvision.datasets as datasets
# warning表示off
import warnings
warnings.simplefilter('ignore')
# デフォルトフォントサイズ変更
plt.rcParams['font.size'] = 14
# デフォルトグラフサイズ変更
plt.rcParams['figure.figsize'] = (6,6)
# デフォルトで方眼表示ON
plt.rcParams['axes.grid'] = True
# numpyの表示桁数設定
np.set_printoptions(suppress=True, precision=5)
GPUを使用。
# GPUチェック
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
分類先のクラスを設定。
# 分類先クラスの名称リスト
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 分類先クラス数 今回は10になる
n_output = len(list(set(classes)))
# 結果確認
print(n_output)
データ準備
Transformsを設定。データ拡張として、反転とRandomErasingを実施。テスト用データにはデータ拡張は施さない。
# 訓練データ用: 正規化に追加で反転とRandomErasingを実施
transform_train = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(0.5, 0.5),
transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)
])
# テストデータ用:
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(0.5, 0.5)
])
CIFAR-10をダウンロードする。
data_root = './ignore_dir/data'
# 訓練データの取得
train_set = datasets.CIFAR10(
root = data_root, train = True,
download = True, transform = transform_train)
# テストデータの取得
test_set = datasets.CIFAR10(
root = data_root, train = False,
ミニバッチのサイズを指定し、データローダーを2種類(訓練用とテスト用)を用意する。テスト用のデータローダーはシャッフル不要。
# ミニバッチのサイズ指定
batch_size = 100
# 訓練用データローダー
# 訓練用なので、シャッフルをかける
train_loader = DataLoader(train_set,
batch_size = batch_size, shuffle = True)
# テスト用データローダー
# テスト時にシャッフルは不要
test_loader = DataLoader(test_set,
batch_size = batch_size, shuffle = False)
乱数を固定。
# 乱数初期化
def torch_seed(seed=123):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms = True
torch_seed()
訓練用データの先頭50個を表示してみる。
# イメージとラベル表示
def show_images_labels(loader, classes, net, device):
# DataLoaderから最初の1セットを取得する
for images, labels in loader:
break
# 表示数は50個とバッチサイズのうち小さい方
n_size = min(len(images), 50)
if net is not None:
# デバイスの割り当て
inputs = images.to(device)
labels = labels.to(device)
# 予測計算
outputs = net(inputs)
predicted = torch.max(outputs,1)[1]
#images = images.to('cpu')
# 最初のn_size個の表示
plt.figure(figsize=(20, 15))
for i in range(n_size):
ax = plt.subplot(5, 10, i + 1)
label_name = classes[labels[i]]
# netがNoneでない場合は、予測結果もタイトルに表示する
if net is not None:
predicted_name = classes[predicted[i]]
# 正解かどうかで色分けをする
if label_name == predicted_name:
c = 'k'
else:
c = 'b'
ax.set_title(label_name + ':' + predicted_name, c=c, fontsize=20)
# netがNoneの場合は、正解ラベルのみ表示
else:
ax.set_title(label_name, fontsize=20)
# TensorをNumPyに変換
image_np = images[i].numpy().copy()
# 軸の順番変更 (channel, row, column) -> (row, column, channel)
img = np.transpose(image_np, (1, 2, 0))
# 値の範囲を[-1, 1] -> [0, 1]に戻す
img = (img + 1)/2
# 結果表示
plt.imshow(img)
ax.set_axis_off()
plt.show()
show_images_labels(train_loader, classes, None, None)
RandomErasingもきちんと施されてる。
モデル構築
CNN を構築します。
- 畳み込み層: 6層
- プーリング層: 3層
class CNN_v4(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=(1,1))
self.conv2 = nn.Conv2d(32, 32, 3, padding=(1,1))
self.conv3 = nn.Conv2d(32, 64, 3, padding=(1,1))
self.conv4 = nn.Conv2d(64, 64, 3, padding=(1,1))
self.conv5 = nn.Conv2d(64, 128, 3, padding=(1,1))
self.conv6 = nn.Conv2d(128, 128, 3, padding=(1,1))
self.relu = nn.ReLU(inplace=True)
self.flatten = nn.Flatten()
self.maxpool = nn.MaxPool2d((2,2))
self.l1 = nn.Linear(4*4*128, 128)
self.l2 = nn.Linear(128, num_classes)
self.dropout1 = nn.Dropout(0.2)
self.dropout2 = nn.Dropout(0.3)
self.dropout3 = nn.Dropout(0.4)
self.bn1 = nn.BatchNorm2d(32)
self.bn2 = nn.BatchNorm2d(32)
self.bn3 = nn.BatchNorm2d(64)
self.bn4 = nn.BatchNorm2d(64)
self.bn5 = nn.BatchNorm2d(128)
self.bn6 = nn.BatchNorm2d(128)
self.features = nn.Sequential(
self.conv1, # 畳み込み
self.bn1, # バッチノーマライゼーション
self.relu, # 活性化関数
self.conv2, # 畳み込み
self.bn2, # バッチノーマライゼーション
self.relu, # 活性化関数
self.maxpool, # プーリング
self.dropout1, # ドロップアウト
self.conv3, # 畳み込み
self.bn3, # バッチノーマライゼーション
self.relu, # 活性化関数
self.conv4, # 畳み込み
self.bn4, # バッチノーマライゼーション
self.relu, # 活性化関数
self.maxpool, # プーリング
self.dropout2, # ドロップアウト
self.conv5, # 畳み込み
self.bn5, # バッチノーマライゼーション
self.relu, # 活性化関数
self.conv6, # 畳み込み
self.bn6, # バッチノーマライゼーション
self.relu, # 活性化関数
self.maxpool, # プーリング
self.dropout3, # ドロップアウト
)
self.classifier = nn.Sequential(
self.l1,
self.relu,
self.dropout3,
self.l2
)
def forward(self, x):
x1 = self.features(x)
x2 = self.flatten(x1)
x3 = self.classifier(x2)
return x3
# モデルインスタンス生成
net = CNN_v4(n_output).to(device)
# 交差エントロピー誤差関数
criterion = nn.CrossEntropyLoss()
# Adam最適化
optimizer = optim.Adam(net.parameters())
# 履歴保存用
history = np.zeros((0, 5))
学習
num_epochs = 100
# 学習用関数
def fit(net, optimizer, criterion, num_epochs, train_loader, test_loader, device, history):
base_epochs = len(history)
for epoch in range(base_epochs, num_epochs+base_epochs):
# 1エポックあたりの正解数(精度計算用)
n_train_acc, n_val_acc = 0, 0
# 1エポックあたりの累積損失(平均化前)
train_loss, val_loss = 0, 0
# 1エポックあたりのデータ累積件数
n_train, n_test = 0, 0
#訓練フェーズ
net.train()
for inputs, labels in tqdm(train_loader):
# 1バッチあたりのデータ件数
train_batch_size = len(labels)
# 1エポックあたりのデータ累積件数
n_train += train_batch_size
# GPUヘ転送
inputs = inputs.to(device)
labels = labels.to(device)
# 勾配の初期化
optimizer.zero_grad()
# 予測計算
outputs = net(inputs)
# 損失計算
loss = criterion(outputs, labels)
# 勾配計算
loss.backward()
# パラメータ修正
optimizer.step()
# 予測ラベル導出
predicted = torch.max(outputs, 1)[1]
# 平均前の損失と正解数の計算
# lossは平均計算が行われているので平均前の損失に戻して加算
train_loss += loss.item() * train_batch_size
n_train_acc += (predicted == labels).sum().item()
#予測フェーズ
net.eval()
for inputs_test, labels_test in test_loader:
# 1バッチあたりのデータ件数
test_batch_size = len(labels_test)
# 1エポックあたりのデータ累積件数
n_test += test_batch_size
# GPUヘ転送
inputs_test = inputs_test.to(device)
labels_test = labels_test.to(device)
# 予測計算
outputs_test = net(inputs_test)
# 損失計算
loss_test = criterion(outputs_test, labels_test)
# 予測ラベル導出
predicted_test = torch.max(outputs_test, 1)[1]
# 平均前の損失と正解数の計算
# lossは平均計算が行われているので平均前の損失に戻して加算
val_loss += loss_test.item() * test_batch_size
n_val_acc += (predicted_test == labels_test).sum().item()
# 精度計算
train_acc = n_train_acc / n_train
val_acc = n_val_acc / n_test
# 損失計算
avg_train_loss = train_loss / n_train
avg_val_loss = val_loss / n_test
# 結果表示
print (f'Epoch [{(epoch+1)}/{num_epochs+base_epochs}], loss: {avg_train_loss:.5f} acc: {train_acc:.5f} val_loss: {avg_val_loss:.5f}, val_acc: {val_acc:.5f}')
# 記録
item = np.array([epoch+1, avg_train_loss, train_acc, avg_val_loss, val_acc])
history = np.vstack((history, item))
return history
history = fit(net, optimizer, criterion, num_epochs,
train_loader, test_loader, device, history)
最終的に精度は89%くらいになった。学習曲線を表示する。
# 学習ログ解析
def evaluate_history(history):
#損失と精度の確認
print(f'初期状態: 損失: {history[0,3]:.5f} 精度: {history[0,4]:.5f}')
print(f'最終状態: 損失: {history[-1,3]:.5f} 精度: {history[-1,4]:.5f}' )
num_epochs = len(history)
if num_epochs < 10:
unit = 1
else:
unit = num_epochs / 10
# 学習曲線の表示 (損失)
plt.figure(figsize=(9,8))
plt.plot(history[:,0], history[:,1], 'b', label='訓練')
plt.plot(history[:,0], history[:,3], 'k', label='検証')
plt.xticks(np.arange(0,num_epochs+1, unit))
plt.xlabel('繰り返し回数')
plt.ylabel('損失')
plt.title('学習曲線(損失)')
plt.legend()
plt.show()
# 学習曲線の表示 (精度)
plt.figure(figsize=(9,8))
plt.plot(history[:,0], history[:,2], 'b', label='訓練')
plt.plot(history[:,0], history[:,4], 'k', label='検証')
plt.xticks(np.arange(0,num_epochs+1,unit))
plt.xlabel('繰り返し回数')
plt.ylabel('精度')
plt.title('学習曲線(精度)')
plt.legend()
plt.show()
evaluate_history(history)
初期状態: 損失: 1.27110 精度: 0.53560
最終状態: 損失: 0.35388 精度: 0.89090
前回は途中からテスト用データの損失が上がってしまったが、今回はそのようなことにならなかった。最後に画像イメージと予測結果を表示する。
show_images_labels(test_loader, classes, net, device)
38番目のデータは正解がcarに対して、予測結果はtruck。ただ、画像イメージを見てみると、クラシックなフォルムで、トラックに見えなくもない。
# 間違えた38番目のデータを抽出
for images, labels in test_loader:
break
image = images[37]
label = labels[37]
# イメージを表示して確認
plt.figure(figsize=(3,3))
w = image.numpy().copy()
w2 = np.transpose(w, (1, 2, 0))
w3 = (w2 + 1)/2
plt.title(classes[label])
plt.imshow(w3)
plt.show()
予測値を見てみると、carの確率も31%出ており、AIも悩んだ(?)らしい。
# 予測値を取得
image = image.view(1, 3, 32, 32)
image = image.to(device)
output = net(image)
# ラベル別の確率値を表示
probs = torch.softmax(output, dim=1)
probs_np = probs.data.to('cpu').numpy()[0]
values = np.frompyfunc(lambda x: f'{x:.04f}', 1, 1)(probs_np)
names = np.array(classes)
tbl = np.array([names, values]).T
print(tbl)
[['plane' '0.0000']
['car' '0.3144']
['bird' '0.0000']
['cat' '0.0000']
['deer' '0.0000']
['dog' '0.0000']
['frog' '0.0000']
['horse' '0.0000']
['ship' '0.0000']
['truck' '0.6855']]
おわりに
CNNにDropout, Batch Normalization, Data Augmantationといった過学習対策を導入した。精度は89%まで向上。
出典
最短コースでわかる PyTorch &深層学習プログラミング