さて、PyTorchである。
Keras+TensorFlowに不満は何もないけれど、会社で使わせてもらっているPCはCPUがAVX命令に対応してないせいで、もうpip install tensorflow
で最新版をインストールしても動作しなくなっちゃってる1。
だったら、この機会にPyTorchの書き方も覚えてみるか、くらいの軽いイキオイで。流行ってるしね。
PyTorchのインストール
CUDAを使えない環境では、PyPIで簡単にインストールできる。
% pip install -U torch torchvision
Tutorialを参照する
何はなくとも、チュートリアルである。公式のチュートリアルのNeural Networkのところを見ると、「Chainerに似ている」と言われるのもなんとなく理解できる。
MNISTを実行
MNISTを実装してみるにあたって、公式のCIFAR10のチュートリアルを参考にする。
MNISTデータのダウンロード
Chainerでいうchainer.datasets.mnist.get_mnist(withlabel=True, ndim=3)
とか、Kerasでいうkeras.datasets.mnist.load_data()
に相当するヤツがPyTorchにもある。
torchvision.datasetsのドキュメント参照。
ちなみに、いつも気軽に使っているMNISTは、THE MNIST DATABASE of handwritten digitsというサイトが一時配布元である。
- train-images-idx3-ubyte.gz: training set images (9912422 bytes)
- train-labels-idx1-ubyte.gz: training set labels (28881 bytes)
- t10k-images-idx3-ubyte.gz: test set images (1648877 bytes)
- t10k-labels-idx1-ubyte.gz: test set labels (4542 bytes)
ということで、ダウンロードしておく。こういう時、lynx
とwget
を使う派。
% brew install lynx wget
% `lynx -dump http://yann.lecun.com/exdb/mnist/ | grep "gz$" | sed -e 's/[4-7]\./wget/'`
あ、これ面倒くさいヤツだ。やっぱり、素直にtorchvision.datasets.MNIST()
を使うことにする。
これで、データがダウンロードされ、data
フォルダに保存され、transform
を通じて正規化する。
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, ), (0.5, ))])
trainset = torchvision.datasets.MNIST(root='./data',
train=True,
download=True,
transform=transform)
trainloader = torch.utils.data.DataLoader(trainset,
batch_size=100,
shuffle=True,
num_workers=2)
testset = torchvision.datasets.MNIST(root='./data',
train=False,
download=True,
transform=transform)
testloader = torch.utils.data.DataLoader(testset,
batch_size=100,
shuffle=False,
num_workers=2)
classes = tuple(np.linspace(0, 9, 10, dtype=np.uint8))
このtransforms.Compose(...)
で定義したフィルタを使って、MNISTの各ピクセルの値の数値の範囲を0〜1から、-1〜1の範囲の分布になるように変換する(らしい)。
とりあえず、torchvision.transforms.Normalize()
のドキュメントを見てみると、次のようになっている。
class torchvision.transforms.Normalize(mean, std)
Normalize a tensor image with mean and standard deviation. Given mean:(M1,...,Mn)
and std:(S1,..,Sn)
for n channels, this transform will normalize each channel of the input torch.*Tensor
-1〜1の範囲で、平均0.5、標準偏差0.5になるように正規化してある、と。公式のCIFAR10のチュートリアルに、そう書いてある。
The output of torchvision datasets are PILImage images of range [0, 1]. We transform them to Tensors of normalized range [-1, 1].
NeuralNetの定義
以前に書いた「Keras(+Tensorflow)でMNISTしてみる」と同じmodelを作ることにしようかな。
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(1, 32, 3) # 28x28x32 -> 26x26x32
self.conv2 = nn.Conv2d(32, 64, 3) # 26x26x64 -> 24x24x64
self.pool = nn.MaxPool2d(2, 2) # 24x24x64 -> 12x12x64
self.dropout1 = nn.Dropout2d()
self.fc1 = nn.Linear(12 * 12 * 64, 128)
self.dropout2 = nn.Dropout2d()
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(F.relu(self.conv2(x)))
x = self.dropout1(x)
x = x.view(-1, 12 * 12 * 64)
x = F.relu(self.fc1(x))
x = self.dropout2(x)
x = self.fc2(x)
return x
これは、net.py
というファイル名で保存しておく。
from net import Net
net = Net()
loss関数と最適化の設定をする
公式ドキュメントがCrossEntroyとSGDを使う設定になっているので、ソレに習う。
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
loss functionsのドキュメントによると、MSELoss()とかももちろんある。
optim.SGD()のドキュメントを見ると、learning rate
以外には、momentum
とかweight_decay
とかdampening
とかnesterov
とか設定できる。
深い意味はないけれど、ちょっと設定を変えてみるか。
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(),
lr=0.0005, momentum=0.99, nesterov=True)
学習する
scikit系の関数や、あるいはKeras
とかだったら、fit()
を呼ぶところだけど、pytorchでは、そうはいかないらしい。
for epoch in range(epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(trainloader, 0):
# 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.item()
if i % 100 == 99:
print('[{:d}, {:5d}] loss: {:.3f}'
.format(epoch + 1, i + 1, running_loss / 100))
running_loss = 0.0
print('Finished Training')
こんなの、自分で考えて書けるようになると思えないな。
学習結果の確認
先に定義したtestloaderを使う。
correct = 0
total = 0
with torch.no_grad():
for (images, labels) in testloader:
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy: {:.2f} %%'.format(100 * float(correct/total)))
結果は、次のようになった。
Accuracy: 96.98 %%
Keras+TensorFlowでこの間書いたCNNと比べると、まあまあ悪くない。
課題
さて、Kerasで書いた時みたいに、history
みたいなのをどうやって保存できるんだろうか?
そして疑問
学習の過程で、以下のようにして誤差逆伝播をしているのだけれど、どうしてこれで、net = Net()
で定義したネットワークに反映されているのかがまだ理解できていない。
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
このloss.backward()
とoptimizer.step()
で、どうして次のepochでnet(inputs)
によるoutputs
の値が変化するのだろう。optimizier
がインスタンスになる時には、net.parameters()
が引数として渡されているけれど、それにしてもloss
とは特に何かのfunction()を通してやりとりしていないような気がしてしまうし。
というか、そもそも、このコードだと、学習のときにvalidationをしてない気がする。そこはちゃんと修正しないといけないかな。
ここは、よく解説を読んでみようと思う。
本日のコード
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(1, 32, 3)
self.conv2 = nn.Conv2d(32, 64, 3)
self.pool = nn.MaxPool2d(2, 2)
self.dropout1 = nn.Dropout2d()
self.fc1 = nn.Linear(9216, 128)
self.dropout2 = nn.Dropout2d()
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(F.relu(self.conv2(x)))
x = self.dropout1(x)
x = x.view(-1, 9216)
x = F.relu(self.fc1(x))
x = self.dropout2(x)
x = self.fc2(x)
return x
'''
PyTorch MNIST sample
'''
import argparse
import time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torchvision
import torchvision.transforms as transforms
from torchvision.datasets import MNIST
import torch.optim as optim
from net import Net
def parser():
'''
argument
'''
parser = argparse.ArgumentParser(description='PyTorch MNIST')
parser.add_argument('--epochs', '-e', type=int, default=2,
help='number of epochs to train (default: 2)')
parser.add_argument('--lr', '-l', type=float, default=0.01,
help='learning rate (default: 0.01)')
args = parser.parse_args()
return args
def main():
'''
main
'''
args = parser()
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, ), (0.5, ))])
trainset = MNIST(root='./data',
train=True,
download=True,
transform=transform)
testset = MNIST(root='./data',
train=False,
download=True,
transform=transform)
trainloader = DataLoader(trainset,
batch_size=100,
shuffle=True,
num_workers=2)
testloader = DataLoader(testset,
batch_size=100,
shuffle=False,
num_workers=2)
classes = tuple(np.linspace(0, 9, 10, dtype=np.uint8))
# model
net = Net()
# define loss function and optimier
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(),
lr=args.lr, momentum=0.99, nesterov=True)
# train
for epoch in range(args.epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(trainloader, 0):
# 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.item()
if i % 100 == 99:
print('[{:d}, {:5d}] loss: {:.3f}'
.format(epoch+1, i+1, running_loss/100))
running_loss = 0.0
print('Finished Training')
# test
correct = 0
total = 0
with torch.no_grad():
for (images, labels) in testloader:
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy: {:.2f} %'.format(100 * float(correct/total)))
if __name__ == '__main__':
start_time = time.time()
main()
print('elapsed time: {:.3f} [sec]'.format(time.time() - start_time))
うーん、PyTorch難しいぞ。
-
version 1.5.0までは動作するので、
pip install -U tensorflow=1.5.0
する。1.6.0のリリースノートのBreaking Changesに'Prebuilt binaries will use AVX instructions. This may break TF on older CPUs.'と記載されている。 ↩