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

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

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

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

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

 

MNIST MLP

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

最初は定番の MNIST 画像分類タスクのための MLP モデルから始めます。

準備

まずは最初に 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 データセット

最初にデータセットを作成しますが、Torch Vision と NumPy 経由の 2 通りでデータを取得する方法を示します :

TorchVision

TorchVision を利用する場合には、torchvision.datasets.MNIST で直接データセットを作成できます :

import torchvision

# MNIST Dataset
ds_train = torchvision.datasets.MNIST(root='./data',
                            train=True,
                            transform=torchvision.transforms.ToTensor(),
                            download=True)

ds_test = torchvision.datasets.MNIST(root='./data',
                            train=False,
                            transform=torchvision.transforms.ToTensor())

print("dataset created successfully.")

NumPy

NumPy データを得るために、便宜的に tf.keras.datasets を利用してデータをロードします :

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, 784).astype('float32')
x_test = x_test.reshape(10000, 784).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 でデータローダが作成できます。
データセットを TorchVision から作成した場合でも NumPy 経由で作成した場合でも同じです :

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 を継承して MLP モデルを定義します。
特に難しい話しはないのですが、完全結合層は Dense ではなく Linear になります。
Dropout 層も追加しました :

class MLPModel (nn.Module):

    def __init__(self):
        super(MLPModel, self).__init__()

        self.fc1 = nn.Linear(784, 512)
        self.fc2 = nn.Linear(512, 512)
        self.fc3 = nn.Linear(512, num_classes)

        self.dropout1 = nn.Dropout2d(0.2)
        self.dropout2 = nn.Dropout2d(0.2)


    def forward(self, x):
        x = F.relu(self.fc1(x))
        x= self.dropout1(x)
        x = F.relu(self.fc2(x))
        x = self.dropout2(x)

        return self.fc3(x)
        # return F.log_softmax(self.fc3(x))

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

model = MLPModel().to(device)

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

print(model)

Out:

MLPModel(
  (fc1): Linear(in_features=784, out_features=512, bias=True)
  (dropout1): Dropout2d(p=0.2)
  (fc2): Linear(in_features=512, out_features=512, bias=True)
  (dropout2): Dropout2d(p=0.2)
  (fc3): Linear(in_features=512, 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)
#optimizer = optim.Adam(model.parameters(), lr=0.01)

 

訓練コード (per epoch)

各エポックの訓練コードです。
バッチは最初に device に転送します。また、TorchVision 経由で得たバッチの images の shape は (100, 1, 28, 28) ですので、reshape してやります (NumPy 経由でデータセットを作成した場合にはこの操作は必要ありません)。
100 ステップ毎に損失値を表示してやります :

def train(epoch):
    model.train()
    steps = len(ds_train)//batch_size
    for step, (images, labels) in enumerate(dataloader_train, 1):
        images, labels = images.view(-1, 28*28).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()))

 

評価コード (per epoch)

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

def eval():
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for (images, labels) in dataloader_test:
            images, labels = images.view(-1, 28*28).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))

 

訓練の実行

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

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

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

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

実行時出力です :

Epoch [1/20], Step [100/600], Loss: 2.1855
Epoch [1/20], Step [200/600], Loss: 1.9285
Epoch [1/20], Step [300/600], Loss: 1.1925
Epoch [1/20], Step [400/600], Loss: 0.8519
Epoch [1/20], Step [500/600], Loss: 0.8498
Epoch [1/20], Step [600/600], Loss: 0.5263
Val Acc : 0.8605
Epoch [2/20], Step [100/600], Loss: 0.4407
Epoch [2/20], Step [200/600], Loss: 0.5408
Epoch [2/20], Step [300/600], Loss: 0.5178
Epoch [2/20], Step [400/600], Loss: 0.4496
Epoch [2/20], Step [500/600], Loss: 0.3611
Epoch [2/20], Step [600/600], Loss: 0.4713
Val Acc : 0.9018

...

Epoch [20/20], Step [100/600], Loss: 0.1126
Epoch [20/20], Step [200/600], Loss: 0.1069
Epoch [20/20], Step [300/600], Loss: 0.0975
Epoch [20/20], Step [400/600], Loss: 0.0664
Epoch [20/20], Step [500/600], Loss: 0.0999
Epoch [20/20], Step [600/600], Loss: 0.1520
Val Acc : 0.9699

 

TensorBoard の利用

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

from tensorboardX import SummaryWriter
writer = SummaryWriter()

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

writer.close()

訓練ルーチンでは、便宜上、グローバルステップをカウントする global_step を追加して、SummaryWriter の .add_scalar() メソッドで損失を書き込みます :

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.view(-1, 28*28).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)

 
評価ルーチンはエポック毎ですので、epoch カウントを利用して検証精度を書き込みます :

def eval(epoch, writer):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for (images, labels) in dataloader_test:
            images, labels = images.view(-1, 28*28).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)

 

以上