PyTorch 0.4.1 examples (コード解説) : 画像分類 – CIFAR-10 & CIFAR-100 (CNN)

PyTorch 0.4.1 examples (コード解説) : 画像分類 – CIFAR-10 & CIFAR-100 (CNN)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/27/2018 (0.4.0 & 0.4.1)

* 本ページは、github 上の以下の pytorch/examples と keras/examples レポジトリのサンプル・コードを参考にしています:

* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

素朴な CNN モデル for CIFAR-10

PyTorch 0.4.1 の自作のサンプルをコードの簡単な解説とともに提供しています。
初級チュートリアル程度の知識は仮定しています。

先に MNIST 画像分類タスクのための MLP/CNN/Network in Network モデルを実装しましたので、
次に CIFAR-10 の分類のためのモデルを素朴な CNN で実装してみます。

ついでに同じモデルで CIFAR-100 でも試します。

準備

まずは最初に torch モジュールをインポートします :

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data as data

0.4.x の作法で device を設定します :

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("device", device)

ついでにシードも設定しておきましょう :

torch.manual_seed(1)

ハイパーパラメータ

そして必要最小限のハイパーパラメータを設定します :

batch_size = 100
num_classes = 10
epochs = 100

 

CIFAR-10 データセット

最初にデータセットを作成しますが、NumPy 経由でデータを取得します (もちろん TorchVision 経由でも同様です) 。

tf.keras.datasets を利用して NumPy でデータをロードしますが、channels_first にするために np.moveaxis で軸を移動します (np.moveaxis は np.rollaxis の後継です) :

import numpy as np
import tensorflow as tf

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()

x_train = np.moveaxis(x_train, [3, 1, 2], [1, 2, 3]).astype('float32')
x_test = np.moveaxis(x_test, [3, 1, 2], [1, 2, 3]).astype('float32')

x_train /= 255
x_test /= 255

y_train = y_train.reshape(-1).astype('long')
y_test = y_test.reshape(-1).astype('long')

※ tf.keras.datasets.cifar10.load_data() によって得られるラベルの shape は (50000, 1) ですので、便宜上、(50000,) に reshape しておきます。

そして .from_numpy でデータセットを作成します :

ds_train = data.TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train))
ds_test  = data.TensorDataset(torch.from_numpy(x_test), torch.from_numpy(y_test))

 

データローダ

データセットから data.DataLoader でデータローダが作成できます :

dataloader_train = data.DataLoader(dataset=ds_train, batch_size=batch_size, shuffle=True)

dataloader_test = data.DataLoader(dataset=ds_test, batch_size=batch_size, shuffle=False)

 

モデル定義

nn.Module を継承して素朴な CNN モデルを定義します。
(畳み込み層 + 畳み込み層 + マックスプーリング層) のブロックを 2 つスタックします :

class Cifar10Model(nn.Module):
    def __init__(self):
        super(Cifar10Model, self).__init__()
        self.conv11 = nn.Conv2d(3, 32, 3, padding=1)
        self.conv12 = nn.Conv2d(32, 32, 3, padding=1)

        self.conv21 = nn.Conv2d(32, 64, 3, padding=1)
        self.conv22 = nn.Conv2d(64, 64, 3, padding=1)

        self.fc1 = nn.Linear(64 * 8 * 8, 512)
        self.fc2 = nn.Linear(512, num_classes)

        self.dropout1 = nn.Dropout2d(0.25)
        self.dropout2 = nn.Dropout2d(0.25)
        self.dropout3 = nn.Dropout2d(0.5)


    def forward(self, x):
        x = F.relu(self.conv11(x))
        x = F.relu(self.conv12(x))
        x = F.max_pool2d(x, (2, 2))
        x = self.dropout1(x)

        x = F.relu(self.conv21(x))
        x = F.relu(self.conv22(x))
        x = F.max_pool2d(x, (2, 2))
        x = self.dropout2(x)

        x = x.view(-1, 64 * 8 * 8)

        x = F.relu(self.fc1(x))
        x = self.dropout3(x)
        return self.fc2(x)

モデルをインスタンス化して device に転送します :

model = Cifar10Model().to(device)

インスタンスを直接プリントすると、含まれる層の情報が得られます :

print(model)

Out:

Cifar10Model(
  (conv11): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv12): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv21): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv22): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (fc1): Linear(in_features=4096, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=10, bias=True)
  (dropout1): Dropout2d(p=0.25)
  (dropout2): Dropout2d(p=0.25)
  (dropout3): Dropout2d(p=0.5)
)

 

損失関数と optimizer

損失関数としては nn.CrossEntropyLoss() を使用します :

criterion = nn.CrossEntropyLoss()

optimizer は先に SGD を使用しましたので、ここでは Adam を使用してみましょう :

optimizer = optim.Adam(model.parameters(), lr=0.001)

ついでに学習率のスケジューラも試してみます (設定数は直感で決めています) :

scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.9)

 

訓練コード (per epoch)

各エポックの訓練コードです。100 ステップ毎に損失値を表示してやります。
学習率スケジューラを使用していますので scheduler.step() を忘れずにエポック毎に呼び出します :

global_step = 0

def train(epoch, writer):
    model.train()
    scheduler.step()

    print("\n--- Epoch : %2d ---" % epoch)
    print("lr : %f" % optimizer.param_groups[0]['lr'])

    steps = len(ds_train)//batch_size
    for step, (images, labels) in enumerate(dataloader_train, 1):
        global global_step
        global_step += 1

        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        if step % 100 == 0:
            print ('Epoch [%d/%d], Step [%d/%d], Loss: %.4f' % (epoch, epochs, step, steps, loss.item()))
            writer.add_scalar('train/train_loss', loss.item() , global_step)

TensorBoard を利用するためには、グローバルステップをカウントする global_step を追加して、SummaryWriter の .add_scalar() メソッドで損失を書き込みます。

 

評価コード (per epoch)

各エポックの最後にテスト・データセットで評価します。
単純に正解数をカウントしています :

def eval(epoch, writer):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for (images, labels) in dataloader_test:
            images, labels = images.to(device), labels.to(device)

            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            correct += (predicted == labels).sum().item()
            total += labels.size(0)

    print("Val Acc : %.4f" % (correct/total))
    writer.add_scalar('eval/val_acc', correct*100/total, epoch)

TensorBoard を利用するために、(評価ルーチンはエポック毎ですので) epoch カウントを利用して検証精度を書き込みます。

 

訓練の実行

次のループで訓練が実行できます :

from tensorboardX import SummaryWriter
writer = SummaryWriter()

for epoch in range(1, epochs+1):
    train(epoch, writer)
    eval(epoch, writer)

writer.close()

tensorboardX モジュールを使用すれば TensorBoard が利用できます。
SummaryWriter をインスタンス化して訓練/評価ルーチンに渡してやります。

モデルは pickle でセーブできます :

torch.save(model.state_dict(), 'model_cifar10.pkl')

実行時出力です :

--- Epoch :  1 ---
lr : 0.001000
Epoch [1/100], Step [100/500], Loss: 1.9326
Epoch [1/100], Step [200/500], Loss: 1.7507
Epoch [1/100], Step [300/500], Loss: 1.7669
Epoch [1/100], Step [400/500], Loss: 1.4873
Epoch [1/100], Step [500/500], Loss: 1.3264
Val Acc : 0.4993

...

--- Epoch : 70 ---
lr : 0.000590
Epoch [70/100], Step [100/500], Loss: 0.2639
Epoch [70/100], Step [200/500], Loss: 0.1678
Epoch [70/100], Step [300/500], Loss: 0.2088
Epoch [70/100], Step [400/500], Loss: 0.1274
Epoch [70/100], Step [500/500], Loss: 0.2136
Val Acc : 0.7819

...

--- Epoch : 100 ---
lr : 0.000063
Epoch [100/100], Step [100/500], Loss: 0.1572
Epoch [100/100], Step [200/500], Loss: 0.3368
Epoch [100/100], Step [300/500], Loss: 0.1692
Epoch [100/100], Step [400/500], Loss: 0.2832
Epoch [100/100], Step [500/500], Loss: 0.2284
Val Acc : 0.7803

検証精度 78.19 % がベストです。平凡な結果で、素朴な CNN で 80 % 前後しか出ません :

 

明示的な初期化 (CIFAR-10)

次に畳み込み層の重みを明示的に初期化した上で訓練してみます。
モデルのアーキテクチャの変更によるインパクトの方が遥かに大きいですが、感触を掴むために一応試しておきます。

◆ 最初に Xavier 一様分布で明示的に初期化したところ、CIFAR-10 について精度が 78.19 % から 78.37% に改善されました :

 
◆ 次に訓練の条件を揃えて幾つかの初期化アルゴリズムを比較してみました :

それぞれの到達精度をまとめると以下のようなものです :

  • 初期化なし : 77.06 %
  • xavier_uniform_ : 77.55 %
  • kaiming_uniform_ : 78.00 %
  • orthogonal_ : 78.31 %

概ね、次のように推移しています :
orthogonal_ > kaiming_uniform_ > xavier_uniform_ > 初期化なし

 

CIFAR-100 データセット

ついでに同じモデルで CIFAR-100 でも試しておきます。
上述のコードがほぼそのまま動作しますが、違う箇所だけ指摘しておきます。

まず、クラス数は 100 です。エポック数も試しに多めに取ります :

batch_size = 100
num_classes = 100
epochs = 150

データのロードは tf.keras.datasets.cifar100 モジュールを使用します :

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar100.load_data()

モデルはそのまま流用しても良かったのですが、クラス数が増えたので、分類層の完全結合層を一つ増やしました :

class Cifar100Model(nn.Module):
    def __init__(self):
        super(Cifar10Model, self).__init__()
        self.conv11 = nn.Conv2d(3, 32, 3, padding=1)
        self.conv12 = nn.Conv2d(32, 32, 3, padding=1)

        self.conv21 = nn.Conv2d(32, 64, 3, padding=1)
        self.conv22 = nn.Conv2d(64, 64, 3, padding=1)

        self.fc1 = nn.Linear(64 * 8 * 8, 1024)
        self.fc2 = nn.Linear(1024, 512)
        self.fc3 = nn.Linear(512, num_classes)

        self.dropout1 = nn.Dropout2d(0.25)
        self.dropout2 = nn.Dropout2d(0.25)
        self.dropout3 = nn.Dropout2d(0.5)
        self.dropout4 = nn.Dropout2d(0.5)


    def forward(self, x):
        x = F.relu(self.conv11(x))
        x = F.relu(self.conv12(x))
        x = F.max_pool2d(x, (2, 2))
        x = self.dropout1(x)

        x = F.relu(self.conv21(x))
        x = F.relu(self.conv22(x))
        x = F.max_pool2d(x, (2, 2))
        x = self.dropout2(x)

        x = x.view(-1, 64 * 8 * 8)
        x = F.relu(self.fc1(x))
        x = self.dropout3(x)
        x = self.fc2(x)
        x = self.dropout4(x)
        return self.fc3(x)

実行時出力です :

--- Epoch :  1 ---
lr : 0.001000
Epoch [1/150], Step [100/500], Loss: 4.4598
Epoch [1/150], Step [200/500], Loss: 4.1714
Epoch [1/150], Step [300/500], Loss: 4.1487
Epoch [1/150], Step [400/500], Loss: 3.8165
Epoch [1/150], Step [500/500], Loss: 3.9835
Val Acc : 0.1219

...

--- Epoch : 33 ---
lr : 0.000810
Epoch [33/150], Step [100/500], Loss: 1.5739
Epoch [33/150], Step [200/500], Loss: 2.0363
Epoch [33/150], Step [300/500], Loss: 1.5778
Epoch [33/150], Step [400/500], Loss: 1.7401
Epoch [33/150], Step [500/500], Loss: 2.1234
Val Acc : 0.3785

...

--- Epoch : 150 ---
lr : 0.000254
Epoch [150/150], Step [100/500], Loss: 0.5650
Epoch [150/150], Step [200/500], Loss: 0.7036
Epoch [150/150], Step [300/500], Loss: 0.5749
Epoch [150/150], Step [400/500], Loss: 0.5420
Epoch [150/150], Step [500/500], Loss: 0.7754
Val Acc : 0.3666

せいぜい 38% です。CIFAR-100 は必ずしも容易なタスクではありませんが、(少なくとも記録上 75 % は出るはずなので) 50 % 程度の平凡な精度は達成したいところですが、素朴な ConvNet では厳しいようです。

また、TensorBoard のグラフで精度を見ると overfitting の傾向が見られます :

 

明示的な初期化 (CIFAR-100)

CIFAR-100 の訓練についても明示的な初期化を試しておきます。

◆ Xavier 一様分布でモデルの畳み込み層の重みを明示的に初期化したところ、
CIFAR-100 については 38.23 % が 40.33 % に改善されました :

◆ そして条件を揃えて各種初期化アルゴリズムを比較してみました。

到達精度をまとめると :

  • 初期化なし – 43.53 %
  • xavier_uniform_ – 44.44 %
  • kaiming_uniform_ – 44.80 %
  • orthogonal_ – 44.69 %
 

以上