1. fukuit

    No comment

    fukuit
Changes in body
Source | HTML | Preview

さて、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 DATABASEof handwritten digitsというサイトが一時配布元である。

ということで、ダウンロードしておく。こういう時、lynxwgetを使う派。

% 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=4,
                                            shuffle=True,
                                            num_workers=2)

testset = torchvision.datasets.MNISTs(root='./data', 
                                        train=False, 
                                        download=True, 
                                        transform=transform)
testloader = torch.utils.data.DataLoader(testset, 
                                            batch_size=4,
                                            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(24 * 24 * 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, 24 * 24 * 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 % 2000 == 1999:
            print('[{:d}, {:5d}] loss: {.3f}'
                    .format(epoch + 1, i + 1, running_loss / 2000))
            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でoptimizer.step()net(inputs)で、どうして次のepochでによるnet(inputs)outputsによるの値が変化するのだろう。outputsoptimizierの値が変化するのだろう。がインスタンスになる時には、optimiziernet.parameters()がインスタンスになる時には、が引数として渡されているけれど、それにしてもnet.parameters()loss```とは特に何かのfunction()を通してやりとりしていないような気がしてしまうし。が引数として渡されているけれど、それにしてもlossとは特に何かのfunction()を通してやりとりしていないような気がしてしまうし。

というか、そもそも、このコードだと、学習のときにvalidationをしてない気がする。そこはちゃんと修正しないといけないかな。

ここは、よく解説を読んでみようと思う。


本日のコード

net.py
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
mnist.py
'''
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=4,
                             shuffle=True,
                             num_workers=2)
    testloader = DataLoader(testset,
                            batch_size=4,
                            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 % 2000 == 1999:
                print('[{:d}, {:5d}] loss: {:.3f}'
                      .format(epoch+1, i+1, running_loss/2000))
                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難しいぞ。


  1. 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.'と記載されている。