118
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PyTorchでMNIST

Last updated at Posted at 2018-07-27

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

ということで、ダウンロードしておく。こういう時、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=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をしてない気がする。そこはちゃんと修正しないといけないかな。

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


本日のコード

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=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難しいぞ。

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

118
81
1

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
118
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?