236
177

More than 5 years have passed since last update.

【詳細(?)】pytorch入門 〜CIFAR10をCNNする〜

Last updated at Posted at 2017-09-12

1. はじめに

pytorch初心者によるpytorch入門です.

こういう新しいフレームワークを使う時はexampleを見て,そこで使われている関数などをひたすらググりまくる or ドキュメントを読む or いじるのが一番の近道と考えているので,そのメモです.

似たようなやり方でpytorch入門しようとしている人にとってはこの記事で時間の節約になると思います.(なってくれると嬉しい.)

というわけで,CIFAR10でCNNをやるcifar10-tutorialのコードの解読というかググり作業を行います.

2. 環境の準備

2.1. 環境

  • Python 3.6.1 :: Anaconda 4.4.0 (64-bit)

2.2. インストール

pytorchのインストールは超絶簡単で,公式サイトで自分の環境をポチポチしていくと,インストール用のコードを表示してくれます.僕の環境の場合は以下のような感じでした.

http://pytorch.org/
Screen Shot 2017-09-06 at 22.59.46.png

pip install http://download.pytorch.org/whl/cu80/torch-0.2.0.post3-cp36-cp36m-manylinux1_x86_64.whl 
pip install torchvision

3. データのロード,下処理

まずはデータのロードと下処理を行なっているこのコードを見ていきます.

import torch
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

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

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

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

3.1. torchvisionとは

torchvisionとは,pytorchのコンピュータービジョン用のパッケージで,データのロードや下処理用の関数やらが入っているらしいです.

3.2. データの下処理

transforms.Composeで,データをロードした後に行う下処理の関数を構成します.

ToTensor()は名前の通りデータをpytorchの定義するtorch.Tensorというテンソルに型を変更します.

transforms.Normalizeの引数がtorch.Tensorであることから,リストの初めから順に関数を実行してくのでしょう.

transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)は,引数の一つ目のタプルが RGBの各チャンネルの平均を表し,二つ目のタプルが標準偏差を表します.これらの平均と標準偏差にあわせて正規化します.

つまり

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

これで,データをpytorch用のテンソル型に変えて,正規化を行う関数を準備したことになります.

3.3. torchvision.datasets.CIFAR10とは

torchvision.datasets.CIFAR10 は名前の通りCIFAR10のデータをロードするためのクラスです.

download=Trueだったら,rootの位置にデータを保存します.

MNISTやCIFAR10などは,データセットがすでに訓練用とテスト用に分かれています.(混ぜて自分で分割してもいいと思いますが)

CIFAR10では,60000のイメージがあり,そのうち50000が訓練用で,10000がテスト用とされています.train=Trueにすると,あらかじめ訓練用とされている50000の方をロードし,train=Falseで10000のテスト用のデータをロードします.

引数transformで先ほどtransforms.Composeで作った下処理の一連の流れを渡すと,ロードした後に渡した下処理を実行してくれます.

3.4. DataLoaderとは

DataLoaderは,ロードしたデータセットにsamplerクラスというデータをサンプルする用のオブジェクトをくっつけたものです.
確認すると確かにsamplerがくっついています.

trainloader.sampler
# <torch.utils.data.sampler.RandomSampler at 0x7f9099e13ef0>

samplerには見たところ,ランダムサンプリング,順番にサンプリング,重みをつけてサンプリングなどがありました.

DataLoaderの引数にsamplerというのがあるので,ここで自分で定義したsamplerを渡してあげれば良い予感がするので試してみましょう.

ここでは,結果がわかりやすいように,ある1つのデータのみをサンプリングするように重み付けして,サンプリングしてみます.
データ数が少なくて楽なのでテストデータで試します.

import numpy as np

# 一つの画像だけ1の重みベクトルを作る.
weights = np.zeros(10000) # テストデータの数は10000
weights[300] = 1. # 300は適当
num_samples = 4 # サンプリングする回数

# WeightedRandomSamplerを試す.
my_sampler = torch.utils.data.sampler.WeightedRandomSampler(weights, num_samples, replacement=True)
my_testloader = torch.utils.data.DataLoader(testset, batch_size=4,shuffle=False, num_workers=2, sampler=my_sampler)

my_testiter = iter(my_testloader)
images, labels = my_testiter.next()

# imshow関数は次に説明するけどちょっと先走って使う.
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))

imshow(torchvision.utils.make_grid(images))

fig2.png

おぉ,カエルばっかだ.

3.5. 画像のプロット

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

# functions to show an image
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))

# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

fig1.png

3.6. unnormalizeとは

img = img / 2 + 0.5の部分にコメントでunnormalizeとありますが,これをunnormalizeというのは若干ミスリードな気がします.normalizeされているといえばされていますし.

plt.imshowの入力が[0,1]なので,それに合わせてずらしています.

3.7. img.numpy()とは

type(img) # torch.FloatTensor

torch.Tensorは,pytorchで扱うテンソルの型で,「要素のデータ型+Tensor」という名前になっています.この場合FloatTensorなので,float,つまり32bitの浮動小数点です.

ドキュメントに返されるndarrayと元のテンソルは同じ領域をシェアしているからどっちか変更するともう片方も変更されるとあります.

チェックしてみましょう.

a = torch.FloatTensor([1])
b = a.numpy()

# ndarrayの方を変更する
b[0] = 2

# 元のテンソルも2になる.
print("original tensor: ", a) # original tensor: 2
print("ndarray : ", b) # ndarray :  [ 2.]

# 別のメモリ領域を参照している.
print(id(a)) # 140024484781832
print(id(b)) # 140024044621056

おぉ,ndarrayの方の変更が元のテンソルにも反映されていますね.
確保しているメモリ領域は異なるので,値を同じに保つようにして仮想的に同じ領域をシェアしているように動作させているのでしょう.

3.8. plt.imshow(np.transpose(npimg, (1, 2, 0)))とは

npimg = img.numpy()
npimg2 = np.transpose(npimg, (1, 2, 0))

print(npimg.shape) # (3, 36, 138)
print(npimg2.shape) # (36, 138, 3)

ドキュメントを見ると,plt.imshowの引数は,(n, m, RGB)というように並んでいなくてはなりません.
npimgはもともと(RGB, 縦,横)と並んでいるので,np.transposeの第二引数の順に並び替えています.

3.9. dataiter = iter(trainloader)とは

dataiter = iter(trainloader)

print(type(trainloader))
# <class 'torch.utils.data.dataloader.DataLoader'>

print(type(dataiter))
# <class 'torch.utils.data.dataloader.DataLoaderIter'>

iter()でDataLoaderで定義された__iter__が呼ばれ,DataLoaderIterを返すようになっています.

通常のイテレーターと異なり,batch_sizeずつデータを渡さないといけないので,専用のイテレーターを定義してくれているようです.(コード

これによって,dataiter.next()を呼ぶごとにnバッチ目,n+1バッチ目と繰り返しデータを取得してくれます.

3.10. make_gridとは

img = torchvision.utils.make_grid(images)
print(type(images)) # <class 'torch.FloatTensor'>
print(images.size) # torch.Size([4, 3, 32, 32])
print(type(img)) # <class 'torch.FloatTensor'>
print(img.size) # torch.Size([3, 36, 138])

ドキュメントはtorchvision.utils.make_gridです.

make_grid関数では,複数の画像を横に並べてくれます.
make_gridの引数は4次元のテンソルなのに対し,返り値は3次元のテンソルになっています.引数のテンソルが[画像の枚数,RGB, 縦,横]の4次元テンソルだったのが,画像の枚数の次元がなくなっています.

そして,ドキュメントにある通り,デフォルトではpadding=2なので上と下に2ずつ追加されて32が36に,各画像の間と両端に2ずつ追加されて,32*n+2*(n+1)=138 (n=4)つまり横に関しては32が138になっています.

4. モデルの定義

このコードを見ていきます.

from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 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


net = Net()

4.1. Variableとは

Variableは,torch.Tensorをラップして,勾配のデータなどを保持できるようにするものです.

Variableは,テンソルを包んでいるので,Variableに入っているデータをみたい時にはワンステップ必要です.

a = torch.FloatTensor([1.])
a2 = Variable(a)

print(type(a)) # <class 'torch.FloatTensor'>
print(type(a2)) # <class 'torch.autograd.variable.Variable'>
print(type(a2.data)) # <class 'torch.FloatTensor'>

print(a.numpy()) # [ 1.]
print(a2.data.numpy()) # [ 1.]

上のように,.dataにテンソルが格納されています.(参考)

ちなみに,勾配の情報は

print(a2.grad) # None

ここに保存されています.
今は何もないのでNoneが入っています.NoneVariableのみ受け付けるようです.

a2.grad = Variable(torch.FloatTensor([100]))
print(a2.grad) # Variable containing: 100

裏側でこんな感じで代入していい感じにやっているのでしょう.
そしてVariableしか受け付けなくなっているということは,そこにも勾配の情報を保存する予定というわけで,

a2.grad.grad =  Variable(torch.FloatTensor([200]))
print(a2.grad.grad) # Variable containing: 200

高階導関数はこのようにしていじくるのでしょうか?面白いですね.

4.2. nn.Moduleとは

class Net(nn.Module):

nn.Moduleクラスはbaseクラスで,これにforwardなど定義されており,何かモデルを作るときはこれを継承します.
(実際にはforwardに具体的な何かが定義されているわけではなく,継承した後に代入されることを想定しているようです.)

4.3. nn.Conv2dとは

nn.Conv2dの引数は,
左から,インプットのチャンネルの数,アウトプットのチャンネルの数,カーネルサイズです.

このモデル定義の時点で畳み込み層のパラメータであるフィルターに0付近のランダムな値を入れて準備してくれています.
それは以下のように確認できます.

conv1 = nn.Conv2d(3, 6, 5)
print(conv1.weight)

Parameter containing:
(0 ,0 ,.,.) = 
 -0.0011 -0.1120  0.0351 -0.0488  0.0323
 -0.0529 -0.0126  0.1139 -0.0234 -0.0729
  0.0384 -0.0263 -0.0903  0.1065  0.0702
  0.0087 -0.0492  0.0519  0.0254 -0.0941
  0.0351 -0.0556 -0.0279 -0.0641 -0.0790

(0 ,1 ,.,.) = 
 -0.0738  0.0853  0.0817 -0.1121  0.0463
 -0.0266  0.0360  0.0215 -0.0997 -0.0559
  0.0441 -0.0151  0.0309 -0.0026  0.0167
 -0.0534  0.0699 -0.0295 -0.1043 -0.0614
 -0.0820 -0.0549 -0.0654 -0.1144  0.0049
 ...
 [torch.FloatTensor of size 6x3x5x5]

カーネルサイズを5にしたので,(アウトプットのチャンネル数,RGB,カーネルサイズ,カーネルサイズ)のテンソルが準備されています.ちなみに,

conv1 = nn.Conv2d(3, 6, (5,1))
print(conv1.weight)

Parameter containing:
(0 ,0 ,.,.) = 
  0.2339
 -0.0756
  0.0604
 -0.0185
 -0.0975
 ...

というように正方形以外もできるようです.

また,このパラメーターは

type(conv1.weight)
# torch.nn.parameter.Parameter

このようにParameterオブジェクトで定義しており,これはVariableのサブクラスで,パッと見た感じ,表示用の関数が定義されていて,VariableだとVariable containingと出ますが,ParameterではParameter containing:と出るぐらいしか違いがないのではないでしょうか.

4.4. nn.MaxPool2dとは

nn.MaxPool2dがMAXプーリング層です.主に使う引数はkernel_size, stride, paddingぐらいでしょうか.

というわけで,適応な画像に畳み込みとMAXプーリングをしてどのように変換されるか確認して見ましょう.

images, labels = dataiter.next()
print(images.size())
print(type(images))
image_plot = images[0][1].numpy()
plt.imshow(image_plot, cmap='Greys', interpolation='nearest')
plt.show()

# モデルの定義
img_input = Variable(images)
conv = nn.Conv2d(3, 1, 3, padding=1)
pool = nn.MaxPool2d(3, padding=1, stride=1)

# フォワード
conv_output = conv(img_input)
pool_output = pool(conv_output)
print(pool_output.size())

# プロット
conv_plot = conv_output[0][0].data.numpy()
conv_plot 
plt.imshow(conv_plot, cmap='Greys', interpolation='nearest')
plt.show()

pool_plot = pool_output[0][0].data.numpy()
plt.imshow(pool_plot, cmap='Greys', interpolation='nearest')
plt.show()
  • オリジナルの画像
    fig3.png
    馬ですね.

  • 畳み込み後の画像
    fig4.png
    ここで,畳み込みはフィルターのパラメーターをまだ学習していないので,ランダムな0付近のパラメータになっていて,どれも同じぐらいの値なのであまり違いはないようです.

  • 畳み込み&MAXプーリング後の画像
    fig5.png
    MAXプーリングは3*3のMAXをとってぼやける感じになっているのがわかりますね.

4.5. 目的関数の定義,最適化手法の定義

次にこのコードを見ていきます.

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

4.6. nn.CrossEntropyLoss()とは

nn.CrossEntropyLoss()では,目的関数となるオブジェクトを定義しています.

4.7.optim.SGDとは

torch.optimは最適化の色々なアルゴリズムを定義しています.SGDやAdamなど.

torch.optimに定義されている最適化アルゴリズムの中で,ここではoptim.SGDを使用します.

optim.SGDの引数としてnet.parameters()を渡しています.
net.parameters()は,モデルに定義されたパラメーター(torch.nn.parameter.Parameter)をジェネレーターにして返してくれるようです.

type(net.parameters())
# generator

type(net.parameters().__next__())
# torch.nn.parameter.Parameter

print(net.parameters().__next__())

Parameter containing:
(0 ,0 ,.,.) = 
 -0.0998  0.0035 -0.0438 -0.1150 -0.0435
  0.0310 -0.0750 -0.0405 -0.0745 -0.1095
 -0.0355  0.0065 -0.0225  0.0729 -0.1114
  0.0708 -0.0170 -0.0253  0.1060  0.0557
  0.1057  0.0873  0.0793 -0.0309 -0.0861
  ...

optimizerが保持しているオブジェクトを確認すると,

optimizer.__dict__

{'param_groups': [{'dampening': 0,
   'lr': 0.001,
   'momentum': 0.9,
   'nesterov': False,
   'params': [Parameter containing:
    (0 ,0 ,.,.) = 
      0.0380 -0.1152  0.0761  0.0964 -0.0555
     -0.0325 -0.0455 -0.0755  0.0413 -0.0589
      0.0116  0.1136 -0.0992 -0.1149 -0.0414
     -0.0611  0.0827 -0.0906  0.0631  0.0170
      0.0903 -0.0816 -0.0690  0.0470 -0.0578
  ...

net.parameters()で私たパラメーターも含めて,モデルに含まれるパラメーターを全部保持してくれています.

5. モデルの訓練

訓練するためのコードを見ていきます.

for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # 第二引数は,スタート位置で,0なのでenumerate(trainloader)と同じ
        # https://docs.python.org/3/library/functions.html#enumerate

        # get the inputs
        inputs, labels = data

        # wrap them in Variable
        inputs, labels = Variable(inputs), Variable(labels)

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.data[0]
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')

5.1. optimizer.zero_grad()とは

先ほど確認したように,optimizerはすべてのパラメーターを保持しています.

optimizer.zero_grad()では,これらの保持しているVariableのgradを初期化してくれるらしいです.
多分全部Noneにしているんだと思います.

5.2. outputs = net(inputs)とは

outputs = net(inputs)

print(type(outputs))
# <class 'torch.autograd.variable.Variable'>

print(outputs.size())
# torch.Size([4, 10])

outputs
Variable containing:
-2.4825 -4.4286  2.2041  3.4353  2.0734  2.8198  1.9374  0.7751 -2.6798 -3.1932
-1.7512 -4.6657  2.7911  3.9570  0.7931  5.9005 -0.8023  2.9664 -4.3328 -3.2921
 2.4015  2.8962  0.9330 -1.2107 -0.0525 -2.2119 -1.2474 -2.6026 -0.1120  0.4869
-1.3042 -2.7538  1.0985 -0.2462  3.7435  1.1724 -1.4233  6.6892 -3.8201 -2.3132
[torch.FloatTensor of size 4x10]

netに渡すことによって,目的関数まで通した最終的な出力が帰ってきていることがわかります.

5.3. loss = criterion(outputs, labels)とは

コメントでは,forward + backward + optimizeと書いてありますが,forwardのメソッドが見えません.

これは実は,CrossEntropyLossはcallでforwardを呼ぶようになっており,つまり,

loss = criterion(outputs, labels)
loss = criterion.forward(outputs, labels)

この二つは同じことをしています.
なのでloss = criterion(outputs, labels)がforwardになっています.

5.4. loss.backward()とは

lossはVariableオブジェクトです.

type(loss)
# torch.autograd.variable.Variable

Variable.backward()は,中でこのtorch.autograd.backward()を呼びます.

それではこの.backward()が何をしているかというと,目的関数に対して,それに含まれるパラメーターの微分係数を求めています.
簡単な例で試してみましょう.

x = torch.autograd.Variable(torch.Tensor([3,4]), requires_grad=True)
# requires_grad=Trueで,このVariableは微分するぞと伝える

print("x.grad : ", x.grad)
# None
# この時点ではまだ何も入っていない.

# 適当に目的関数を作る.
y = x[0]**2 + 5*x[1]  + x[0]*x[1]
# x[0]の導関数 : 2*x[0] + x[1]
# x[0]の微分係数 : 2*3 + 4 = 10
# x[1]の導関数 : 5 + x[0]
# x[1]の微分係数 : 5 + 3 = 8

y.backward()
# torch.autograd.backward(y) でも良い.

print("x.grad : ", x.grad)
# 10
# 8

# .zero_grad()の代わり
x.grad = None

目的関数yの,入力データ点での微分係数になっていますね.
ここで注意すべきなのは,backwardは損失関数についてなので,backwardするyはスカラーでなくてはなりません.

例えば,

y = x
y.backward()

# RuntimeError: grad can be implicitly created only for scalar outputs

とすると,スカラーにしろと怒られます.

5.5. optimizer.step()とは

.step()は,.backward()で計算した勾配を元に,パラメーターを更新してくれます.
チェックして見ましょう.

optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()

print(net.parameters().__next__())
Parameter containing:
(0 ,0 ,.,.) = 
 -0.0839  0.1434 -0.0371 -0.1394 -0.0277

何回か実行すると(同じデータで最適化してますが)

print(net.parameters().__next__())
Parameter containing:
(0 ,0 ,.,.) = 
 -0.0834  0.1436 -0.0371 -0.1389 -0.0276

というように,少しずつパラメーターが更新されています.

6. Validationする

テストデータに対してモデルの予測を行います.このコードです.

correct = 0
total = 0
for data in testloader:
    images, labels = data
    outputs = net(Variable(images))
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum()

print('Accuracy of the network on the 10000 test images: %d %%' % (
    100 * correct / total))
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
for data in testloader:
    images, labels = data
    #print("images type : ", type(images))
    #print("images.shape : ", images.shape)
    outputs = net(Variable(images))
    _, predicted = torch.max(outputs.data, 1)
    c = (predicted == labels).squeeze()
    for i in range(4):
        label = labels[i]
        class_correct[label] += c[i]
        class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

# Accuracy of plane : 51 %
# Accuracy of   car : 54 %
# Accuracy of  bird : 53 %
# Accuracy of   cat : 33 %
# Accuracy of  deer : 41 %
# Accuracy of   dog : 50 %
# Accuracy of  frog : 54 %
# Accuracy of horse : 65 %
# Accuracy of  ship : 70 %
# Accuracy of truck : 67 %

6.1. .squeeze()とは

見慣れないやつはこれくらいでしょうか.
torch.squeeze
テンソルの次元の中で,1のものを消します.
squeezeは絞るという意味だそうです.

7. 終わりに

7.1. 便利な何か

net.parametersで大体どんなモデルかわかります.(2017/10/27修正)

nn.Moduleの__repr__を,モデルを見やすく表示してくれるように定義してくれているので,以下のようにして大体どんなモデルかわかります.

In [22]: net
Out[22]:
Net (
  (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  (pool): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear (400 -> 120)
  (fc2): Linear (120 -> 84)
  (fc3): Linear (84 -> 10)
)

7.2. 感想

  • pytorchの抽象化の仕方はかなり参考になる.
  • 使いやすそう.
236
177
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
236
177