PyTorch 0.4.1 examples (コード解説) : 画像分類 – MNIST (CNN)

PyTorch 0.4.1 examples (コード解説) : 画像分類 – MNIST (CNN)

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

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

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

 

MNIST CNN

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

先に MNIST 画像分類タスクのための MLP モデルを実装しましたので、
同じタスクに対して畳み込みネットワーク (CNN, ConvNet) でモデルを実装してみます。

準備

まずは最初に 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 = 20

 

MNIST データセット

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

tf.keras.datasets を利用して NumPy でデータをロードしますが、畳み込みネットを使用しますので、shape (1, 28, 28) に reshape している点に注意してください :

import tensorflow as tf

# the data, split between train and test sets
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

x_train = x_train.reshape(60000, 1, 28, 28).astype('float32')
x_test = x_test.reshape(10000, 1, 28, 28).astype('float32')

# 正規化
x_train /= 255
x_test /= 255

y_train = y_train.astype('long')
y_test = y_test.astype('long')

そして .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 モデルを定義します。Dropout 層も追加しました :

class CNNModel (nn.Module):
    def __init__(self):
        super(CNNModel, self).__init__()

        self.conv1 = nn.Conv2d(1, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)

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

        self.fc1 = nn.Linear(64*14*14, 128)
        self.fc2 = nn.Linear(128, num_classes)


    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, (2, 2))
        x = self.dropout1(x)

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

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

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

model = CNNModel().to(device)

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

print(model)

Out:

CNNModel(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (dropout1): Dropout2d(p=0.25)
  (dropout2): Dropout2d(p=0.5)
  (fc1): Linear(in_features=12544, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)

 

損失関数と optimizer

損失関数としては nn.CrossEntropyLoss() を使用します。
nn.NLLLoss() を使用する場合には、モデル定義の forward 関数で F.log_softmax(self.fc3(x)) を返す必要があります :

criterion = nn.CrossEntropyLoss()
#criterion = nn.NLLLoss()

optimizer は引続き SGD を使用します :

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

 

訓練コード (per epoch)

各エポックの訓練コードです。
バッチは最初に device に転送します。100 ステップ毎に損失値を表示してやります :

global_step = 0

def train(epoch, writer):
    model.train()
    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.pkl')

実行時出力です :

Epoch [1/20], Step [100/600], Loss: 1.4448
Epoch [1/20], Step [200/600], Loss: 1.0823
Epoch [1/20], Step [300/600], Loss: 0.6340
Epoch [1/20], Step [400/600], Loss: 0.9224
Epoch [1/20], Step [500/600], Loss: 0.4842
Epoch [1/20], Step [600/600], Loss: 0.3546
Val Acc : 0.9561
Epoch [2/20], Step [100/600], Loss: 0.4396
Epoch [2/20], Step [200/600], Loss: 0.6849
Epoch [2/20], Step [300/600], Loss: 0.3359
Epoch [2/20], Step [400/600], Loss: 0.2871
Epoch [2/20], Step [500/600], Loss: 0.5054
Epoch [2/20], Step [600/600], Loss: 0.4565
Val Acc : 0.9695


...

Epoch [20/20], Step [100/600], Loss: 0.2833
Epoch [20/20], Step [200/600], Loss: 0.1941
Epoch [20/20], Step [300/600], Loss: 0.1546
Epoch [20/20], Step [400/600], Loss: 0.0919
Epoch [20/20], Step [500/600], Loss: 0.2461
Epoch [20/20], Step [600/600], Loss: 0.0397
Val Acc : 0.9813

精度 98.13 % が得られました。先の MLP モデルでは精度 96.99 % でしたので、1 ポイント以上改善しています。

グラフの推移を見ると、もう訓練エポックをもう少し増やせば更に精度が上がるかもしれません。

 

補記: 明示的な初期化

◆ 補足として、次のようなコードで Xavier (Glorot) の一様分布でモデルの畳み込み層の重みを明示的に初期化してみましたところ :

import torch.nn.init as init

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                init.xavier_uniform_(m.weight.data, gain=init.calculate_gain('relu'))

精度 99.24 % まで改善されました :

 
◆ また、(重みテンソルが直交行列になるように初期化される) init.orthogonal_ では精度 99.27 % が得られました :

 
◆ 更に、init.kaiming_uniform_ では精度 99.35 % が得られました :

 

以上