人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習、Deep Learning、Keras、PyTorchに関する記事が多いです。



PyTorch (5) Multilayer Perceptron

今回は多層パーセプトロンでMNIST。おなじみ。

180203-multilayer-perceptron.ipynb - Google ドライブ

import torch
import torch.nn as nn
import torchvision
import torchvision.datasets as dsets
import torchvision.transforms as transforms
from torch.autograd import Variable

# Hyperparameters 
input_size = 784
hidden_size = 500
num_classes = 10
num_epochs = 50
batch_size = 100
learning_rate = 0.001

入力層は28 x 28 = 784ユニット、隠れ層が500ユニット、出力層が0-9の10クラスという構成。

# MNIST Dataset (Images and Labels)
train_dataset = dsets.MNIST(root='./data', 
                            train=True, 
                            transform=transforms.ToTensor(),
                            download=True)

test_dataset = dsets.MNIST(root='./data', 
                           train=False, 
                           transform=transforms.ToTensor())

# Dataset Loader (Input Pipline)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 
                                          batch_size=batch_size, 
                                          shuffle=False)

MNISTはPyTorchの標準機能で読み込める。PyTorchでデータを扱うにはDataSetDataLoader の2つのクラスが重要DataSetはデータセットのまとまりを表していて、DataLoaderDataSetをセットすることでミニバッチ単位でロードできるようになる。MNISTDataSetを継承したMNISTデータセットを扱うための組み込みクラス。カスタムデータセットを作成する方法はあとで試そう。

transformsを使うといろいろなデータ前処理ができる。ここでは読み込んだ画像データ(PIL.Image.Image)をテンソルに変換する ToTensor() だけを指定。

print(len(train_dataset))  # 60000
print(len(test_dataset))   # 10000
print(len(train_loader))   # 600
print(len(test_loader))    # 100

DataSetのlenはサンプル数を返し、DataLoaderのlenはミニバッチ数を返すので注意!

# 1データだけ取得
image, label = iter(train_loader).next()
print(type(image))   # <class 'torch.FloatTensor'>
print(type(label))   # <class 'torch.LongTensor'>
print(image.size())  # torch.Size([100, 1, 28, 28])
print(label.size())  # torch.Size([100])

DataLoader から1バッチ分のデータを取り出すには iter() で囲んでから next() を呼び出す。テストしたいときに便利!

可視化のコード。

# 可視化
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

def imshow(img):
    npimg = img.numpy()
    # [c, h, w] => [h, w, c]
    plt.imshow(np.transpose(npimg, (1, 2, 0)))

images, labels = iter(train_loader).next()
images, labels = images[:25], labels[:25]
imshow(torchvision.utils.make_grid(images, nrow=5, padding=1))
plt.axis('off')

画像をブロック上に配置する make_grid() がとても便利!

f:id:aidiary:20180204094901p:plain

class MultiLayerPerceptron(nn.Module):
    
    def __init__(self, input_size, hidden_size, num_classes):
        super(MultiLayerPerceptron, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
#         print(x.size())
        out = self.fc1(x)
#         print(out.size())
        out = self.relu(out)
#         print(out.size())
        out = self.fc2(out)
#         print(out.size())
        return out

多層パーセプトロンを構築。PyTorchはモデル構築の方法もいろいろある。Kerasのように Sequential を使ったり、Relu をFunctionにしたり。今後はいろいろ試していこう。

forward() の処理では途中結果を普通に print できるのでデバッグがとてもやりやすい。下のように1バッチ分をモデルに入力して途中出力のサイズを確認したりできる。

model = MultiLayerPerceptron(input_size, hidden_size, num_classes)

# テスト
image, label = iter(train_loader).next()
print("befire view:", image.size())
image = image.view(-1, 28 * 28)
print("after view:", image.size())
model(Variable(image))

上で view はNumPyの reshape みたいな関数。モデルにデータを渡す場合は Variable 化しないといけない。出力は

befire view: torch.Size([100, 1, 28, 28])
after view: torch.Size([100, 784])
torch.Size([100, 784])
torch.Size([100, 500])
torch.Size([100, 500])
torch.Size([100, 10])
Out[68]:
Variable containing:
 0.0881  0.1128  0.1229  ...   0.0547  0.1134  0.0740
 0.1181  0.0273  0.0997  ...   0.0352  0.0604  0.0261
 0.0704  0.0372  0.0514  ...  -0.1734  0.1685  0.2151
          ...             ⋱             ...          
 0.0005  0.0968 -0.0255  ...   0.0411  0.0099  0.0492
-0.0076  0.0129  0.0341  ...  -0.0539  0.1071  0.0137
 0.1145  0.0278  0.0273  ...   0.0387  0.1142  0.0560
[torch.FloatTensor of size 100x10]

あとはこれまでとほとんど一緒。

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

def train(train_loader):
    model.train()
    running_loss = 0
    for batch_idx, (images, labels) in enumerate(train_loader):
        images = Variable(images.view(-1, 28 * 28))
        labels = Variable(labels)
        
        optimizer.zero_grad()
        outputs = model(images)

        loss = criterion(outputs, labels)
        running_loss += loss.data[0]

        loss.backward()
        optimizer.step()

    # ミニバッチ数で割って平均を求める
    train_loss = running_loss / len(train_loader)
    
    return train_loss


def valid(test_loader):
    model.eval()
    running_loss = 0
    correct = 0
    total = 0
    for batch_idx, (images, labels) in enumerate(test_loader):
        images = Variable(images.view(-1, 28 * 28), volatile=True)
        labels = Variable(labels, volatile=True)

        outputs = model(images)

        loss = criterion(outputs, labels)
        running_loss += loss.data[0]

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

    val_loss = running_loss / len(test_loader)
    val_acc = correct / total
    
    return val_loss, val_acc


loss_list = []
val_loss_list = []
val_acc_list = []
for epoch in range(num_epochs):
    loss = train(train_loader)
    val_loss, val_acc = valid(test_loader)

    print('epoch %d, loss: %.4f val_loss: %.4f val_acc: %.4f'
          % (epoch, loss, val_loss, val_acc))
    
    # logging
    loss_list.append(loss)
    val_loss_list.append(val_loss)
    val_acc_list.append(val_acc)

running_lossの計算はちょっと注意。PyTorchの損失関数のデフォルトは、size_average=Trueになっている。つまり、蓄積したlossをミニバッチのサンプル数で割った平均が返される仕様になっている。そのため、ミニバッチ単位でlossを蓄積していって最後にtrain_lossを計算するときはミニバッチ数 len(train_loader) で割って平均とする。

ここらへんは人によって実装方法が違う。たとえば、Transfer Learning Tutorialの実装だとlossをサンプル数倍したものをrunning_lossに蓄積していって最後にミニバッチ数ではなく、サンプル数 len(train_set)で割っている。

あとrunning_lossに加えるときはVariableのまま加えずに loss.data[0] として数値を加えるようにしたほうがよいようだ。

valid() では勾配を保存する必要がないので volatile=True にしておくと効率がよい。

こんな感じでログが出力される。

epoch 0, loss: 2.2311 val_loss: 2.1544 val_acc: 0.5560
epoch 1, loss: 2.0792 val_loss: 1.9867 val_acc: 0.6899
epoch 2, loss: 1.8992 val_loss: 1.7872 val_acc: 0.7371
epoch 3, loss: 1.6928 val_loss: 1.5692 val_acc: 0.7641
epoch 4, loss: 1.4803 val_loss: 1.3590 val_acc: 0.7839
epoch 5, loss: 1.2863 val_loss: 1.1779 val_acc: 0.8020
epoch 6, loss: 1.1248 val_loss: 1.0329 val_acc: 0.8172
epoch 7, loss: 0.9973 val_loss: 0.9203 val_acc: 0.8262
epoch 8, loss: 0.8981 val_loss: 0.8330 val_acc: 0.8360
epoch 9, loss: 0.8207 val_loss: 0.7645 val_acc: 0.8446
epoch 10, loss: 0.7593 val_loss: 0.7099 val_acc: 0.8502
・・・

グラフ化。

import matplotlib.pyplot as plt
%matplotlib inline

# plot learning curve
plt.figure()
plt.plot(range(num_epochs), loss_list, 'r-', label='train_loss')
plt.plot(range(num_epochs), val_loss_list, 'b-', label='val_loss')
plt.legend()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.grid()

plt.figure()
plt.plot(range(num_epochs), val_acc_list, 'g-', label='val_acc')
plt.legend()
plt.xlabel('epoch')
plt.ylabel('acc')
plt.grid()

f:id:aidiary:20180203151926p:plain f:id:aidiary:20180203151933p:plain

そろそろGPUないときつくなってきたので次回からGPU使おう!

参考