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のインストールは超絶簡単で,公式サイトで自分の環境をポチポチしていくと,インストール用のコードを表示してくれます.僕の環境の場合は以下のような感じでした.
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))
おぉ,カエルばっかだ.
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)))
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が入っています.None
かVariable
のみ受け付けるようです.
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()
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. 便利な何か
(2017/10/27修正)net.parameters
で大体どんなモデルかわかります.
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の抽象化の仕方はかなり参考になる.
- 使いやすそう.