人工知能に関する断創録

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



PyTorch (6) Convolutional Neural Network

今回は畳み込みニューラルネットワーク。MNISTとCIFAR-10で実験してみた。

Jupyter Notebook

MNIST

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

# Hyperparameters 
num_epochs = 10
batch_size = 100
learning_rate = 0.001

use_gpu = torch.cuda.is_available()
if use_gpu:
    print('cuda is available!')

今回からGPU対応した。PyTorchはKerasと違ってGPUモードにするために明示的にコーディングが必要。ここがちょっと面倒><

まずは use_gpu というGPUを使える環境かどうか判定するフラグを用意する。使える場合は自動的にGPUモードに切り替わるようにした。

# 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を読み込むDataSetとDataLoaderを作成。前回(2018/2/4)と同じ。

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

        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=5, padding=2),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(2))

        self.layer2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=5, padding=2),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2))

        self.fc = nn.Linear(7 * 7 * 32, 10)
        
    def forward(self, x):
#         print('1:', x.size())
        out = self.layer1(x)
#         print('2:', out.size())
        out = self.layer2(out)
#         print('3:', out.size())
        out = out.view(out.size(0), -1)
#         print('4:', out.size())
        out = self.fc(out)
#         print('5:', out.size())
        return out
  • CNNはブロック単位で処理した方がよいのでブロック(Conv+BN+ReLU+Pooling)ごとにまとめて Sequential を使うとわかりやすくなる。Kerasっぽく書けるのでいい!
  • Conv2dBatchNorm2d はKerasと違って入力と出力のユニットサイズを省略できない。
  • サイズを自分で計算するのが面倒ならば、モデルの途中結果サイズを print(out.size()) で出力してみるとよい。
# テスト
model = CNN()
images, labels = iter(train_loader).next()
print(images.size())
outputs = model(Variable(images))
torch.Size([100, 1, 28, 28])
1: torch.Size([100, 1, 28, 28])
2: torch.Size([100, 16, 14, 14])
3: torch.Size([100, 32, 7, 7])
4: torch.Size([100, 1568])   => 1568 = 32 x 7 x 7
5: torch.Size([100, 10])

モデルオブジェクトの作成。

model = CNN()
if use_gpu:
    model.cuda()

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

GPUモードで動かすには

  • モデルを cuda() でGPUに転送する
  • テンソルデータを cuda() でGPUに転送する

の2つだけ実装すればよい。Kerasより面倒だけど意外に簡単。

print(model) するといい感じで構造が表示される。

CNN(
  (layer1): Sequential(
    (0): Conv2d (1, 16, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True)
    (2): ReLU()
    (3): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  )
  (layer2): Sequential(
    (0): Conv2d (16, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True)
    (2): ReLU()
    (3): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  )
  (fc): Linear(in_features=1568, out_features=10)
)

次は訓練ループ。これまでとほとんど同じ。

def train(train_loader):
    model.train()
    running_loss = 0
    for batch_idx, (images, labels) in enumerate(train_loader):
        if use_gpu:
            images = Variable(images.cuda())
            labels = Variable(labels.cuda())
        else:
            images = Variable(images)
            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):
        if use_gpu:
            images = Variable(images.cuda(), volatile=True)
            labels = Variable(labels.cuda(), volatile=True)
        else:
            images = Variable(images, 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)

# save the trained model
np.save('loss_list.npy', np.array(loss_list))
np.save('val_loss_list.npy', np.array(val_loss_list))
np.save('val_acc_list.npy', np.array(val_acc_list))
torch.save(model.state_dict(), 'cnn.pkl')

Batch Normalizationなど学習時と推論時で挙動が変わるレイヤを使う場合はモデルのモードに要注意

  • model.train() で訓練モード
  • model.eval() で評価モード

に切り替える必要がある。切り替えなくてもエラーにはならないが性能が出なかったりする。Kerasにはなかったので忘れやすい!

また先に書いたようにGPUモードで動かすときはテンソルを下のように cuda() でGPUに送る必要がある!

        if use_gpu:
            images = Variable(images.cuda())
            labels = Variable(labels.cuda())
        else:
            images = Variable(images)
            labels = Variable(labels)

GPUで動かすと下のようになった。

cuda is available!
epoch 0, loss: 0.1666 val_loss: 0.0549 val_acc: 0.9824
epoch 1, loss: 0.0487 val_loss: 0.0372 val_acc: 0.9875
epoch 2, loss: 0.0369 val_loss: 0.0283 val_acc: 0.9905
epoch 3, loss: 0.0295 val_loss: 0.0359 val_acc: 0.9884
epoch 4, loss: 0.0247 val_loss: 0.0302 val_acc: 0.9904
epoch 5, loss: 0.0194 val_loss: 0.0402 val_acc: 0.9871
epoch 6, loss: 0.0161 val_loss: 0.0298 val_acc: 0.9903
epoch 7, loss: 0.0133 val_loss: 0.0351 val_acc: 0.9883
epoch 8, loss: 0.0123 val_loss: 0.0307 val_acc: 0.9909
epoch 9, loss: 0.0104 val_loss: 0.0242 val_acc: 0.9926

前回、多層パーセプトロンでの精度が85%くらいだったので99.3%出るCNNはすごく性能がよいことがわかる。

CIFAR-10

次は同じことをCIFAR-10でやってみよう。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.autograd import Variable

use_gpu = torch.cuda.is_available()
if use_gpu:
    print('cuda is available!')
num_epochs = 30
batch_size = 128

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # [0, 1] => [-1, 1]
])

train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
                                         download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
                                           shuffle=True, num_workers=4)

test_set = torchvision.datasets.CIFAR10(root='./data', train=False,
                                        download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size,
                                          shuffle=False, num_workers=4)

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
  • CIFAR-10のデータは画像のピクセルが [0, 1] ではなく、[-1, 1] になるように標準化している。
  • num_workers を指定するとファイルの読み込みが並列化される(CPUのコアが複数ある場合はすごく速くなる!)

いくつかサンプルを描画してみよう。

import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

def imshow(img):
    # unnormalize [-1, 1] => [0, 1]
    img = img / 2 + 0.5
    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[:16], labels[:16]
imshow(torchvision.utils.make_grid(images, nrow=4, padding=1))
plt.axis('off')

f:id:aidiary:20180205160944p:plain

いい感じ。

参考のチュートリアルに従ってCNNの簡単なモデルを定義してみた。上のMNISTよりシンプルという・・・

class CNN(nn.Module):

    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, kernel_size=5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

ReLUは nn.ReLU() で層として定義する場合もあるが、パラメータがないので F.relu() のように関数として使うこともできる。層で書いておくとモデルを print したときに表示される(さっきの例)。

model = CNN()
if use_gpu:
    model.cuda()
print(model)
CNN(
  (conv1): Conv2d (3, 6, kernel_size=(5, 5), stride=(1, 1))
  (pool): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (conv2): Conv2d (6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120)
  (fc2): Linear(in_features=120, out_features=84)
  (fc3): Linear(in_features=84, out_features=10)
)

この表示方法はシンプルだがとてもわかりやすい。

あとはこれまでと同じ。

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

def train(train_loader):
    model.train()
    running_loss = 0
    for i, (images, labels) in enumerate(train_loader):
        if use_gpu:
            images = Variable(images.cuda())
            labels = Variable(labels.cuda())
        else:
            images = Variable(images)
            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 i, (images, labels) in enumerate(test_loader):
        if use_gpu:
            images = Variable(images.cuda(), volatile=True)
            labels = Variable(labels.cuda(), volatile=True)
        else:
            images = Variable(images, 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)
        if use_gpu:
            correct += (predicted.cpu() == labels.cpu().data).sum()
        else:
            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)

print('Finished training')

# save the trained model
np.save('loss_list.npy', np.array(loss_list))
np.save('val_loss_list.npy', np.array(val_loss_list))
np.save('val_acc_list.npy', np.array(val_acc_list))
torch.save(model.state_dict(), 'cnn.pkl')

結果をプロットしてみよう。

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

loss_list = np.load('loss_list.npy')
val_loss_list = np.load('val_loss_list.npy')
val_acc_list = np.load('val_acc_list.npy')

# 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:20180205161516p:plain f:id:aidiary:20180205162157p:plain

今回はチュートリアルのシンプルなCNNなので63%くらい。あとでチューニングしてみよう!

参考