PyTorchによる画像分類チュートリアル

## はじめに
本記事では[PyTorch公式](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py)を参考に、機械学習ライブラリPyTorchによる画像分類のチュートリアルを行います。

##前提条件
・PyTorchインストール済み
・Jupyterで行うことを想定しNotebook形式で進める（script形式でも問題はない）

##流れ
1.torchvisionを用いてCIFAR10のデータセットを読み込む
2.CNN（畳み込みニューラルネットワーク）の定義
3.損失関数の定義
4.訓練データを使って学習
5.テストデータを使って予測

## 1.CIFAR10のデータセットを読み込む
PyTorchには**torchvision**と呼ばれるライブラリが含まれており、機械学習の画像データセットとしてよく使われているImagenet, CIFAR10, MNISTなどが利用できます。
今回のチュートリアルでは**CIFAR10**のデータセットを利用します。
はじめに以下をインポートします。

```python
import torch
import torchvison
import torchvision.transforms as transform
```




torchvisionから得られるデータセットは[0, 1]の範囲で表されるPIL画像で出力されます。
今回はこれらを学習のために[-1, 1]の範囲に正規化した行列に変換します。

```python
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])  # Normalize(平均, 偏差)

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')
```

データセットのダウンロードに多少時間がかかりますが、終了すると以下のように表示が出ます。

```
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz
                            
Files already downloaded and verified
```
読み込んだ画像とラベルのデータを試しに表示させてみます。

```python
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline # Notebook形式の場合画像を表示させるのに必要

def imshow(img):
    img = img / 2 + 0.5     
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# 訓練データをランダムに取得
dataiter = iter(trainloader)
images, labels = dataiter.next()

# 画像の表示
imshow(torchvision.utils.make_grid(images))
# ラベルの表示
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
```

これを実行すると下記のように表示されます。
上が読み込んだ画像のサンプル、下が画像に対応するラベルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287354/4fd27f35-45ed-a6d0-c17a-a76f88bc0f41.png)

```
dog  ship   cat  deer
```
## 2.CNN（畳み込みニューラルネットワーク）の定義

データの用意ができたので次に画像認識において有効なニューラルネットワークであるCNN(Convolutional Neural Network)のをpytorch内のライブラリを用いて定義します。

```python
# ニューラルネットワーク（NN）を構成する際に使用するライブラリ
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    # NNの各構成要素を定義
    def __init__(self):
        super(Net, self).__init__()

        # 畳み込み層とプーリング層の要素定義
        self.conv1 = nn.Conv2d(3, 6, 5)  # (入力, 出力, 畳み込みカーネル（5*5）)
        self.pool = nn.MaxPool2d(2, 2)  # (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)  # クラス数が１０なので最終出力数は10
    
    # この順番でNNを構成
    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))  # conv1->relu->pool
        x = self.pool(F.relu(self.conv2(x)))  # conv2->relu->pool
        x = x.view(-1, 16 * 5 * 5)  # データサイズの変更
        x = F.relu(self.fc1(x))  # fc1->relu
        x = F.relu(self.fc2(x))  # fc2->relu
        x = self.fc3(x)
        return x


net = Net()
```

今回定義したCNNは、
入力層 → 畳み込み層 → プーリング層 → 畳み込み層 → プーリング層 → 
全結合層 → 全結合層 → 全結合層 → 出力層
という構成になっています。

forward関数内にあるview関数は１つ目の引数に-1を入れることで、２つ目の引数で指定した値にサイズ数を自動的に調整してくれます。

## 3.損失関数とオプティマイザの定義
損失関数とオプティマイザは以下の関数で簡単に定義できます。
今回は損失関数に交差エントロピーを、オプティマイザにAdamを使用します。学習率は0.001です。

```python

import torch.optim as optim  #オプティマイザ用のライブラリ

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
```

##ニューラルネットワークの訓練
```python
for epoch in range(2): 

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):

        # 訓練データから入力画像の行列とラベルを取り出す
        inputs, labels = data

        # 勾配パラメータを０にする
        optimizer.zero_grad()

        # 順伝播 → 逆伝播 → 勾配パラメータの最適化
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 損失関数の変化を2000ミニバッチごとに表示
        running_loss += loss.item()
        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')
```

```
[1,  2000] loss: 1.334
[1,  4000] loss: 1.326
[1,  6000] loss: 1.344
[1,  8000] loss: 1.308
[1, 10000] loss: 1.296
[1, 12000] loss: 1.285
[2,  2000] loss: 1.194
[2,  4000] loss: 1.195
[2,  6000] loss: 1.192
[2,  8000] loss: 1.204
[2, 10000] loss: 1.191
[2, 12000] loss: 1.189
Finished Training
```
損失関数が徐々に下がっていってるのがわかります。


## 5.テストデータによるネットワークのテスト

ランダムに取り出したテストデータの画像とラベルを試しに表示させてみます。

```python
# 訓練データをランダムに取得
dataiter = iter(testloader)
images, labels = dataiter.next()

# 画像の表示
imshow(torchvision.utils.make_grid(images))
# ラベルの表示
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287354/7b8a8076-a902-109a-7ea6-76a9a0e1069c.png)

```
GroundTruth:    cat  ship  ship  plane
```
訓練したNNを用いて予測を行います。

```python
outputs = net(images) # 訓練後のNNに画像を入力
_, predicted = torch.max(outputs, 1) #入力した画像の行列の最大値（もっとも確率の高いもの）を返す

print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
                              for j in range(4)))
```

```
Predicted:    cat  ship  ship  plane
```

上記の結果から4つの画像全てにおいて正しく予測できていることがわかります。

今回ランダムに取り出した4つの画像は全て正解を予測できていましたが、他の画像ではどうなのかを確かめる必要があります。
そこで10000枚あるテストデータがどの程度正しく予測できているかを確かめてみます。

```python

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (
    100 * correct / total))
```

```
Accuracy of the network on the 10000 test images: 58 %
```
上記より５８％の精度で分類が正しく行われていることがわかります。
つまり今回取り出した画像は偶然4つとも正解を予測できていましたが、全体で見ると６割程度の予測精度のようです。

最後に各ラベルごとの予測制度も出してみたいと思います。

```python
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            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 : 60 %
Accuracy of   car : 71 %
Accuracy of  bird : 56 %
Accuracy of   cat : 45 %
Accuracy of  deer : 44 %
Accuracy of   dog : 51 %
Accuracy of  frog : 61 %
Accuracy of horse : 58 %
Accuracy of  ship : 68 %
Accuracy of truck : 67 %
```

各ラベルの予測精度は上記のようになりました。

##おわりに
今回はPyTorchを勉強する目的で画像分類のチュートリアルを整理しつつ実装してみました。
以前はKerasを使用していましたが、KaggleでPyTorchを使用したのを機に使い始めました。機械学習分野はまだまだ学ぶことが多いので引き続き勉強していきたいと思います。

## 参考
・[PyTorch公式](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py)