PyTorchの基礎 (2) -ニューラルネットワークの作り方-

# PyTorch によるニューラルネットワーク

以下の公式リファレンスを参考にした．
[Neural Networks -- PyTorch Tutorials 1.4.0 documentation](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html)

ニューラルネットワークの訓練の一般的な手順は以下の通り．
**1. データ（訓練データ・テストデータ）を準備する．**
**2. 訓練可能なパラメータを持つニューラルネットワークを定義する．（Define the network）**
**3. ネットワークに訓練データを入力したときの損失関数を計算する．（Loss function）**
**4. ネットワークのパラメータに関する損失関数の勾配を計算する．（backward）**
**5. 損失関数の勾配に基づいてパラメータを更新する．（optimize）**
**6. 3 ~ 6 を何度も繰り返すことで訓練を行う．**

手順通りにニューラルネットワークを構築していく．

## 1. データの準備
ニューラルネットワークの訓練に使うデータは，既にパッケージで用意されているものを使用するか，自分で準備したデータを使う．

既に用意されているものを使用する場合は`torchvision`パッケージを利用すると便利である．
機械学習でよく使用される MNIST や CIFAR10 などのデータセット`torchvision.datasets`が準備されているほか，汎用的な機械学習モデル`torchvision.models`やデータ処理を行うモジュール`torchvision.transforms`が用意されている．
詳しくは公式ドキュメントを参照 --> [torchvision](https://pytorch.org/docs/stable/torchvision/index.html)

訓練を実行するときには`torch.utils.data.DataLoader`というデータの箱を用意する．`DataLoader`は，入力データとそのラベルを合わせたデータセットを，バッチサイズだけひとまとめにしたものである．

準備の手順は以下の通り．
**(1) データの前処理を行う`transforms`を用意する．**
**(2) `transforms`を引数として Dataset クラスをインスタンス化して`Dataset`を用意する．**
**(3) `Dataset`を引数として DataLoader クラスをインスタンス化して`DataLoader`を用意する．**
**(4) 訓練時には`DataLoader`を用いて，訓練データとラベルをバッチサイズのかたまりで取得する．**



## 2. ニューラルネットワークの定義
ニューラルネットワークは`torch.nn`パッケージを用いて構築できる．
`nn`は自動微分`autograd`を利用することでモデルの定義や微分を実行する．

`nn.Module`はニューラルネットワークの各種レイヤーや`forward(input)`メソッドを持っている．
そのため，新しいネットワークを構築する際には`nn.Module`クラスを継承すればよい．


```python
import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 1 input image channel, 6 output channels, 3x3 square convolution
        # kernel
        self.conv1 = nn.Conv2d(1, 6, 3)
        self.conv2 = nn.Conv2d(6, 16, 3)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 6 * 6, 120)  # 6*6 from image dimension
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Max pooling over a (2, 2) window
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # If the size is a square you can only specify a single number
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def num_flat_features(self, x):
        size = x.size()[1:]  # all dimensions except the batch dimension
        num_features = 1
        for s in size:
            num_features *= s
        return num_features

net = Net()
print(net)

# ---Output---
#Net(
#  (conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
#  (conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
#  (fc1): Linear(in_features=576, out_features=120, bias=True)
#  (fc2): Linear(in_features=120, out_features=84, bias=True)
#  (fc3): Linear(in_features=84, out_features=10, bias=True)
#)
```

`__init__()`メソッドにて，ネットワークが保持するレイヤーを定義する．
`Linear`や`Conv2d`などのよく使用するほとんどのレイヤーが`torch.nn`で定義されている．
詳しくは公式ドキュメントを参照 --> [torch.nn](https://pytorch.org/docs/stable/nn.html)

同様に，`relu`や`max_pool2d`などの処理は`torch.nn.functional`で定義されている．
処理が必要な場面で適宜呼び出して使用すればよい．
詳しくは公式ドキュメントを参照 --> [torch.nn.functional](https://pytorch.org/docs/master/nn.functional.html#torch.nn.functional.pixel_shuffle)

`forward()`メソッドにて，ネットワークの順伝播を定義する．
入力`x`が出力されるまでに通るレイヤーや実行される処理を順番に定義していく．

なお，ネットワークの逆伝播である`backward()`は定義する必要はない．
`forward()`を定義し`autograd`を使用することで．自動的に逆伝播が求まるようになっている．

訓練可能なパラメータは`net.parameters()`で取得できる．
重みパラメータとバイアスパラメータで別々に取得されるため，定義したレイヤーの数 $\times$ 2 の長さのパラメータのリストが得られる．

```python
params = list(net.parameters())
print(len(params))
print(params[0].size())   # conv1's weight
print(params[1].size())   # conv1's bias
print(params[0][0,:,:,:]) # conv1's weights on the first dimension

# ---Output---
#10
#torch.Size([6, 1, 3, 3])
#torch.Size([6])
#tensor([[[-0.0146, -0.0219,  0.0491],
#         [-0.3047, -0.0137,  0.0954],
#         [-0.2612, -0.2972, -0.2798]]], grad_fn=<SliceBackward>)
```

このネットワークに対して，$32 \times 32$ の適当なデータを入力してみる．

```python
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

# ---Output---
#tensor([[-0.0703,  0.0575, -0.0679, -0.1168, -0.1093,  0.0815, -0.0085,  0.0408,
#          0.1275,  0.0472]], grad_fn=<AddmmBackward>)
```

入力した乱数が初期パラメータを持つレイヤーを通って出力されている．

`zero_grad()`メソッドで全てのパラメータの勾配をゼロにできる．予期せぬパラメータ更新を避けるために`backward()`を実行する前に`zero_grad()`を実行することが推奨される．

`torch.nn`はミニバッチが入力されることを前提としている．例えば，`nn.Conv2d`は入力として4次元の Tensor ($\rm{nSamples} \times nChannels \times Height \times Width$) を用意する必要がある．


## 3. 損失関数
`MSELoss()`や`CrossEntropyLoss()`などのよく使用する損失関数は`nn`パッケージに用意されている．
以下では，乱数を入力した際の出力値と，同じサイズの乱数列を用いて`MSELoss`を計算している．

```python
input = torch.randn(1, 1, 32, 32)
output = net(input)
target = torch.randn(10)    # a dummy target, for example
target = target.view(1,-1)  # make it the same shape as output
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)

# ---Output---
#tensor(0.5322, grad_fn=<MseLossBackward>)
```


ここまでの順伝播を追ってみると，

```
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d 
      -> view -> linear -> relu -> linear -> relu -> linear 
      -> MSELoss 
      -> loss
```

となっており，`grad_fn`属性を見ることで確認できる．

```python 
print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

# ---Output---
#<MseLossBackward object at 0x7f5008a1c4e0>
#<AddmmBackward object at 0x7f5008a1c5c0>
#<AccumulateGrad object at 0x7f5008a1c4e0>
```

## 4. 勾配計算
パラメータ更新のために誤差逆伝播を行うには，損失関数の勾配が必要である．PyTorch では，損失関数`loss`に対して`loss.backward()`を実行すれば勾配を自動的に計算してくれる．
勾配が蓄積されてしまうのを避けるために，訓練時には iteration ごとに`net.zero_grad()`を実行し，勾配を消去することが推奨される．

```python
net.zero_grad()     # zeroes the gradient buffers of all parameters
print("conv1.bias.grad before backward")
print(net.conv1.bias.grad)

loss.backward()
print("conv1.bias.grad after backward")
print(net.conv1.bias.grad)

# ---Output---
#conv1.bias.grad before backward
#tensor([0., 0., 0., 0., 0., 0.])
#conv1.bias.grad after backward
#tensor([ 0.0072, -0.0051, -0.0008, -0.0017,  0.0043, -0.0030])
```

## 5. パラメータの更新
パラメータ更新（最適化）は`torch.optim`から引用すればよい．
ここでは，以下の式で定義される確率的勾配降下法（SGD）を使用してみる．
詳しくは公式ドキュメントを参照 --> [torch.optim](https://pytorch.org/docs/master/optim.html)

```
weight -> weight - learning_rate * gradient
```

```python
import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad()   # zero the gradient buffers
output = net(input)
loss = criterion(output,target)
loss.backward()
optimizer.step()        # do the update
```

## 6. 訓練 
上記の 3 ~ 6 の手順を繰り返すことでネットワークの訓練を実行する．

## CIFAR10を用いた実装

例として，CIFAR10 を用いた画像分類を行うニューラルネットワークを訓練する．
以下の公式リファレンスを参照した．
[Training a Classifier -- PyTorch Tutorials 1.4.0 documentation](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html)

### データの準備
`torchvision.datasets`に用意されている CIFAR10 のデータを取得し標準化する．
torchvision dataset のデータは [0,1] の範囲の値を持つ PILImage であるため，ここでは [-1,1] の範囲の値を持つ Tensor に標準化している．

```python
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

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

準備したデータを表示してみる．

```python
import matplotlib.pyplot as plt
import numpy as np

def imshow(img):
    img = img/2 + 0.5 # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1,2,0)))
    plt.show()
    
# get some random training imges
dataiter = iter(trainloader)
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))

# print labels
print(''.join('%5s' % classes[labels[j]] for j in range(4)))

```
[Output]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488633/e7b8417a-0841-103d-ac3b-fdaa36f213b6.png)

### ネットワークの構築
続いて，画像の分類を行うネットワークを構築する．

```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(3, 6, 5)
        self.pool = nn.MaxPool2d(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)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()
```

### 損失関数と最適化手法の定義
ネットワークが構築できたら，損失関数と最適化手法を定義する．

```python
import torch.optim as optim

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

### 訓練
ネットワーク・損失関数・最適化手法が定義できたら，訓練データを使って訓練を開始する．

```python
for epoch in range(2): # loop over the dataset multiple times
    
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data
        
        # 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 every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' % (epoch+1, i+1, running_loss/2000))
            running_loss = 0.0
            
print('Finished Training')

# ---Output---
#[1,  2000] loss: 2.149
#[1,  4000] loss: 1.832
#[1,  6000] loss: 1.651
#[1,  8000] loss: 1.573
#[1, 10000] loss: 1.514
#[1, 12000] loss: 1.458
#[2,  2000] loss: 1.420
#[2,  4000] loss: 1.371
#[2,  6000] loss: 1.348
#[2,  8000] loss: 1.333
#[2, 10000] loss: 1.326
#[2, 12000] loss: 1.293
#Finished Training
```

ここでは，12000個の訓練データすべてを使った訓練を2回行っている．訓練に使用するデータが増えるほど，損失関数 loss が小さくなっているため，学習が進行している様子が観察できる．（学習はまだ完了していないと思われるが，今回はここで中断し先に進む．）

### モデルパラメータの保存
訓練済みのモデルのパラメータは`torch.save()`で保存できる．

```python
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
```

### テストデータに適用
テストデータに対して，訓練済みのネットワークを適用してみる．
まずは，テストデータの中身を確認する．

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

[Output]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488633/4715263e-2292-9e04-ab1e-fe61e2a4c918.png)

続いて，保存したネットワークのパラメータを読み込む．
その後，読み込んだモデルにテストデータを入力し，分類結果を表示してみる．

```python
net = Net()
net.load_state_dict(torch.load(PATH))
# ---Output---
# <All keys matched successfully>

outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
# ---Output---
# Predicted:    cat  ship plane plane
```

3枚目の画像を ship ではなく plane と誤判定しているが，それ以外の3つは正しく分類できていることがわかる．

10000枚のテストデータすべてに対する正解率を計算してみる．

```python
correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
print('Accuracy of the network on the 10000 test images: %d %%' % (100*correct/total))
# ---Output---
# Accuracy of the network on the 10000 test images: 52 %
```

正解率は 52% となり，画像分類器としてはあまり精度の良いものではないといえる．

続いて，分類の種類ごとに正解率を取得してみる．

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

# ---Output---
# Accuracy of plane : 61 %
# Accuracy of   car : 61 %
# Accuracy of  bird : 52 %
# Accuracy of   cat : 26 %
# Accuracy of  deer : 34 %
# Accuracy of   dog : 51 %
# Accuracy of  frog : 67 %
# Accuracy of horse : 43 %
# Accuracy of  ship : 76 %
# Accuracy of truck : 50 %
```

ここから，cat の分類は苦手だが ship の分類は得意であることがわかる．

### GPUを用いる場合
GPU上で訓練を行う場合には，`device`で CUDA device を指定する必要がある．
まず，GPU が利用可能かどうか調べる．以下のコードで`cuda:0`と表示されれば GPU が利用可能である．

```python
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Assuming that we are on a CUDA machine, this should print a CUDA device:
print(device)

# ---Output---
# cuda:0
```

`.to(device)`でネットワークやデータを GPU 上に移動できる．
訓練の際には，イテレーションごとにデータを GPU に移動することを忘れないように．

```python
net.to(device)
inputs, labels = data[0].to(device), data[1].to(device)
```

### まとめ
最後に，以上の手順を一つのコードでまとめておく．

```python
# import packages -------------------------------
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms

# prepare data ----------------------------------
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

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

# define a network ------------------------------
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(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)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()

# define loss function and optimizer -------------
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

# start training ---------------------------------
for epoch in range(2): # loop over the dataset multiple times
    
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data
        
        # 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 every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' % (epoch+1, i+1, running_loss/2000))
            running_loss = 0.0
            
print('Finished Training')

# check on test data ----------------------------
correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
print('Accuracy of the network on the 10000 test images: %d %%' % (100*correct/total))
```