【Python】PyTorchでCNNからのチューニングからの多値分類(CIFAR-10)【acc: 0.89】

# はじめに
前回はPyTorchを使ってCNNを構築し、CIFAR-10の分類を行いましたが、がっつり過学習してしまいました。今回は対策として、ドロップアウト、Batch Normalization、データ拡張を実装します。

# できたこと
- データセット: CIFAR-10
- 機械学習モデル: CNN
- 活性化関数: ReLU
- 損失関数: 交差エントロピー関数
- 最適化手法: Adam
- 過学習対策
    - ドロップアウト
    - Batch Normalization
    - Data Augumentation

![結果 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/e54fd679-f21a-fe0b-2728-3f346b82bb20.png)

![学習曲線_損失 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/42cdba41-8d93-87fb-76d4-0b10517da561.png)

![学習曲線_精度 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/0b549896-c6e0-74af-26bc-3c054394c367.png)

https://github.com/grv2688/qt_pytorch/tree/main/241102_1_PyTorch%E3%81%A7CNN%E3%81%8B%E3%82%89%E3%81%AE%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%8B%E3%82%89%E3%81%AE%E5%A4%9A%E5%80%A4%E5%88%86%E9%A1%9E%EF%BC%88CIFAR-10%EF%BC%89

精度は前回の65%から89％まで向上し、過学習も防止できました。

# ライブラリ
```Python
# 必要ライブラリのインポート
%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を使用。

```Python
# GPUチェック
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
```

分類先のクラスを設定。
```Python
# 分類先クラスの名称リスト
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 分類先クラス数　今回は10になる
n_output = len(list(set(classes)))
# 結果確認
print(n_output)
```

# データ準備

Transformsを設定。データ拡張として、反転とRandomErasingを実施。テスト用データにはデータ拡張は施さない。

```Python
# 訓練データ用: 正規化に追加で反転と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をダウンロードする。
```Python
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, 
    download = True, transform = transform)
```

ミニバッチのサイズを指定し、データローダーを2種類（訓練用とテスト用）を用意する。テスト用のデータローダーはシャッフル不要。
```Python
# ミニバッチのサイズ指定
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) 
```

乱数を固定。
```Python
# 乱数初期化
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個を表示してみる。
```Python
# イメージとラベル表示
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)
```
![最初の50個.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/829e5042-1073-ce54-eaa7-f1c54f6eb434.png)

RandomErasingもきちんと施されてる。

# モデル構築
CNN を構築します。
- 畳み込み層: 6層
- プーリング層: 3層
```Python
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))
```

# 学習
```Python
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)
```
![TQDM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/e038ae49-68b4-71d3-6f01-cba138105266.png)

最終的に精度は89％くらいになった。学習曲線を表示する。
```Python
# 学習ログ解析
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)
```
```Python
初期状態: 損失: 1.27110 精度: 0.53560
最終状態: 損失: 0.35388 精度: 0.89090
```
![学習曲線_損失.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/d314020b-3a84-4a62-1de4-57adc40af185.png)
![学習曲線_精度.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/dac4b264-2565-e1d3-c2bd-16d8cb3d02b9.png)

前回は途中からテスト用データの損失が上がってしまったが、今回はそのようなことにならなかった。最後に画像イメージと予測結果を表示する。
```Python
show_images_labels(test_loader, classes, net, device)
```
![結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/960a5f44-7e01-8350-b85d-ef509ba45821.png)

38番目のデータは正解がcarに対して、予測結果はtruck。ただ、画像イメージを見てみると、クラシックなフォルムで、トラックに見えなくもない。

```Python
# 間違えた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()
```
![間違えた38番目のデータ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/2b811450-043b-0717-ab77-f4f9268d6ba7.png)

予測値を見てみると、carの確率も31%出ており、AIも悩んだ（？）らしい。
```Python
# 予測値を取得
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)
```
```Python
[['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 &深層学習プログラミング

https://www.amazon.co.jp/%E6%9C%80%E7%9F%AD%E3%82%B3%E3%83%BC%E3%82%B9%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B-PyTorch-%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E8%B5%A4%E7%9F%B3-%E9%9B%85%E5%85%B8/dp/4296110322