PyTorchでMNIST


さて、PyTorchである。
Keras+TensorFlowに不満は何もないけれど、会社で使わせてもらっているPCはCPUがAVX命令に対応してないせいで、もう```pip install tensorflow```で最新版をインストールしても動作しなくなっちゃってる[^1]。
だったら、この機会にPyTorchの書き方も覚えてみるか、くらいの軽いイキオイで。流行ってるしね。

[^1]: version 1.5.0までは動作するので、```pip install -U tensorflow=1.5.0```する。[1.6.0のリリースノート](https://github.com/tensorflow/tensorflow/releases/tag/v1.6.0)のBreaking Changesに'Prebuilt binaries will use AVX instructions. This may break TF on older CPUs.'と記載されている。


## PyTorchのインストール

CUDAを使えない環境では、PyPIで簡単にインストールできる。

```bash
% pip install -U torch torchvision
```

### Tutorialを参照する

何はなくとも、[チュートリアル](https://pytorch.org/tutorials/)である。[公式のチュートリアルのNeural Networkのところ](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html)を見ると、「Chainerに似ている」と言われるのもなんとなく理解できる。

## MNISTを実行

MNISTを実装してみるにあたって、[公式のCIFAR10のチュートリアル](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html)を参考にする。

### MNISTデータのダウンロード

Chainerでいう```chainer.datasets.mnist.get_mnist(withlabel=True, ndim=3)```とか、Kerasでいう```keras.datasets.mnist.load_data()```に相当するヤツがPyTorchにもある。
[torchvision.datasetsのドキュメント](https://pytorch.org/docs/stable/torchvision/datasets.html#mnist)参照。

ちなみに、いつも気軽に使っているMNISTは、[THE MNIST DATABASE of handwritten digits](http://yann.lecun.com/exdb/mnist/)というサイトが一時配布元である。

- [train-images-idx3-ubyte.gz](http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz): training set images (9912422 bytes) 
- [train-labels-idx1-ubyte.gz](http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz): training set labels (28881 bytes) 
- [t10k-images-idx3-ubyte.gz](http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz): test set images (1648877 bytes) 
- [t10k-labels-idx1-ubyte.gz](http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz): test set labels (4542 bytes)

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

```bash
% 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```を通じて正規化する。

```py
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のチュートリアル](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html)に、そう書いてある。

> 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してみる](https://qiita.com/fukuit/items/b3fa460577a0ea139c88)」と同じmodelを作ることにしようかな。

```python
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```というファイル名で保存しておく。

```python
from net import Net

net = Net()
```

### loss関数と最適化の設定をする

公式ドキュメントがCrossEntroyとSGDを使う設定になっているので、ソレに習う。

```python
import torch.optim as optim

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

[loss functionsのドキュメント](https://pytorch.org/docs/stable/nn.html#loss-functions)によると、MSELoss()とかももちろんある。

[optim.SGD()のドキュメント](https://pytorch.org/docs/stable/_modules/torch/optim/sgd.html)を見ると、```learning rate```以外には、```momentum```とか```weight_decay```とか```dampening```とか```nesterov```とか設定できる。
深い意味はないけれど、ちょっと設定を変えてみるか。

```python
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では、そうはいかないらしい。

```python
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を使う。

```python
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](https://qiita.com/fukuit/items/b3fa460577a0ea139c88)と比べると、まあまあ悪くない。

## 課題

さて、Kerasで書いた時みたいに、```history```みたいなのをどうやって保存できるんだろうか？

### そして疑問

学習の過程で、以下のようにして誤差逆伝播をしているのだけれど、どうしてこれで、```net = Net()```で定義したネットワークに反映されているのかがまだ理解できていない。

```python
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をしてない気がする。そこはちゃんと修正しないといけないかな。

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

----

## 本日のコード

- https://github.com/fukuit/Python_SelfLearning/blob/master/pytorch_lesson/mnist/net.py

```python: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
```

- https://github.com/fukuit/Python_SelfLearning/blob/master/pytorch_lesson/mnist/mnist.py

```python: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難しいぞ。