PyTorchでニューラルネットワーク

# はじめに
PyTorchはTensorFlowと並び称される機械学習フレームワークです。
以前、TensorFlowを使ってBERTの実装をしたことがありますが、カスタマイズに苦労しました。
PyTorchの特徴として、カスタマイズのしやすさがあるということなので、今度はPyTorchを使ってBERTの実装に挑戦しようと思います。

今回は、PyTorchに親しむために[公式のチュートリアル](https://pytorch.org/tutorials/beginner/basics/intro.html)にそってニューラルネットワークを作成し、PyTorchの基礎についてまとめます。

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

## 環境作成
ここではMac OSでpipを使った場合の環境作成方法を説明します（使用したOSはMac OS 12.2.1）。
その他の場合は、[こちら](https://pytorch.org/get-started/locally/)を参考に環境を構築してください。

(1) Homebrewでpython3をインストール
```sh
$ brew install python3
```

(2) pipを使ってPyTorchをインストール
```sh
$ pip3 install torch torchvision
```

なお、[Google Colaboratory](https://colab.research.google.com/?hl=ja)などのクラウドサービスを使えば、GPUを簡単に利用することができます。

## Tensor
PyTorchではTensorというデータ構造で、モデルの入力、出力、そしてパラメーターを表現します。
TensorはNumPyの多次元配列データ構造ndarrayに似ています。しかしndarrayとは異なり、GPU上で実行が可能です。

## データセットの準備
PyTorchでは、`torch.utils.data.Dataset`のサブクラスとして多くのデータセットが提供されています。

今回は、そのうちの一つであるFashion-MNISTデータセットを使用します。このデータセットには6万の学習用データと1万の検証用データが含まれています。

各データは、28×28ピクセルのグレースケール画像と、その画像が何であるかを示すラベル（0: T-Shirt, 1: Trouser, 2: Pullover, 3: Dress, 4: Coat, 5: Sandal, 6: Shirt, 7: Sneaker, 8: Bag, 9: Ankle Boot）で構成されています。

![fashion_mnist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146659/f70957e8-af4c-31b8-b204-283d5340a18e.png)

今回扱うFashion-MNISTデータセットに対しては、`FashionMNIST`クラスを使います。

`FashionMNIST`のパラーメーターには、以下のものを指定します。
- root: データが格納されているディレクトリ
- train: 学習用データの場合はTrue、検証用データの場合はFalseを指定します
- download: Trueの場合は、rootで指定したディレクトリにデータがない場合に、インターネットからダウンロードします
- transform: 画像の変換方法　`ToTensor()`は画像をFloatTensor型に変換します。

```python
from torchvision import datasets

#訓練用データ
training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

#検証用データ
test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)
```

独自のデータをデータセットとして使いたい場合は、`torch.utils.data.Dataset`を継承して独自の`Dataset`クラスを作成します。
詳細は[こちら](
https://pytorch.org/tutorials/beginner/basics/data_tutorial.html#creating-a-custom-dataset-for-your-files)をご覧ください。

## データセットの読み込み
データセットの読み込みには、PyTorchで提供されている`torch.utils.data.DataLoader`を使います。
`DataLoader`は`Dataset`をIterableとしてラップしたものです。

学習、検証の際には、何枚かの画像を1セット（ミニバッチ）として処理をしますが、このミニバッチのサイズを`DataLoader`の引数として渡します。

また、入力画像がランダムな順に読み込まれるように、`shuffle=True`を指定します。

```python
from torch.utils.data import DataLoader

#ミニバッチのサイズ
batch_size = 64

#訓練用データの読み込み
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
#検証用データの読み込み
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)
```


## ニューラルネットワークの作成
PyTorchでは、`nn.Module`のサブクラスとしてニューラルネットワークを定義します。

ここでは、PyTorchで提供されている`nn.Modle`のサブクラスである`nn.Flatten`、`nn.Linear`、`nn.ReLU`、`nn.Sequential`を組み合わせて、下図のようなニューラルネットワークを構築します。

![neuralnet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146659/63d1e9e8-0311-e6c5-f1b2-7f04c4026397.png)


**nn.Flatten**
画像に対応するの2次元Tensor（size=28×28）を1次元のTensor（size=784）に変換します。

**nn.Linear**
入力の重み付き総和とバイアスとの和を計算します。


**nn.ReLU**
活性化関数の１つ。負の値を0に変換します。
```
[2.8, -1.2, 0.3] → [2.8, 0, 0.3]
```

**nn.Sequential**
モジュールをつなげて、入力に対して連続的に処理を行なっていきます。


以下が、作成したニューラルネットワークです。
__init__メソッドでネットワーク構造を定義し、forwardメソッドで入力データに対する処理を実装します。
forwardメソッドの戻り値がネットワークの出力となります。

```python
from torch import nn

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits
```

作成したニューラルネットワークを、以下のようにしてデバイス上に配置します。
```python
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"
model = NeuralNetwork().to(device)
```

## 学習と検証

### 損失関数
ニューラルネットワークの学習では、出力と正解との誤差（損失関数）を計算し、損失関数の値が小さくなるように学習をします。
今回は、多クラス分類の学習であるため交差エントロピー誤差（cross entropy error）を損失関数として使います。PyTorchでは`nn.CrossEntropyLoss`として提供されています。
```python
loss_fn = nn.CrossEntropyLoss()
```

### 最適化
最適化とは、先ほど説明した損失関数の値が小さくなるように、ニューラルネットワークのパラメーター（重み、バイアス）を調整することです。パラメーターの調整量のことを勾配（gradient）と呼びます。
今回は最適化アルゴリズムの１つである確率的勾配降下法（stochastic gradient descent, SDG）を使用します。PyTorchでは`torch.optim.SGD`として提供されています。

`torch.optim.SGD`には、モデルのパラメーターと学習係数を指定します。学習係数によりパラメーターの更新量を調整することができます。

```python
#学習係数
learning_rate = 1e-3
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
```

### 学習
ミニバッチ単位で以下の手順を実行し、学習を行います。

(1) ニューラルネットワークに学習用データを入力し、出力を得る。

```python
#X: 学習用データ、model: ニューラルネットワーク、pred: 出力
pred = model(X)
```

(2) 出力と正解から損失関数を計算する。

```python
#pred: 出力、y: 正解、loss_fn: 損失関数
loss = loss_fn(pred, y)
```

(3) 勾配の値をリセットする（0にする）。

```python
optimizer.zero_grad()
```

(4) 損失関数から誤差逆伝播法（back propagation）により、ニューラルネットワーク内の全パラメーターの勾配を計算する。勾配計算はPyTorch組み込みの微分エンジン`torch.autograd`により行われています。詳細を知りたい方は[こちら](https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html)をご覧ください。

```python
loss.backward()
```

(5) 計算した勾配を用いて、全パラメーターの値を更新する

```python
optimizer.step()
```

以下は、上記の手順をまとめたメソッドです。

```python
def train_loop(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        #ニューラルネットワークの出力
        pred = model(X)
        #損失関数
        loss = loss_fn(pred, y)

        #誤差逆伝播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
```

### 検証
検証では、検証データをニューラルネットワークに入力し、得られた出力と正解との誤差を計算します。
検証では学習が不要なため、`torch.no_grad()`によって勾配計算に必要な処理を無効にします（処理性能向上のため）。

```python
def test_loop(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
```

## プログラムの実行
最後に、下記のプログラムで10エポックの学習＋検証を繰り返します。

```python
#エポック数
epochs = 10

for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer)
    test_loop(test_dataloader, model, loss_fn)
print("Done!")
```

その際の出力結果がこちらです。学習が進むにつれ正解率（Accuracy）が上昇し、誤差（loss）が小さくなっていることが確認できます。
```
Epoch 1
-------------------------------
loss: 2.308301  [    0/60000]
loss: 2.282357  [ 6400/60000]
loss: 2.281907  [12800/60000]
loss: 2.266773  [19200/60000]
loss: 2.249853  [25600/60000]
loss: 2.241469  [32000/60000]
loss: 2.219312  [38400/60000]
loss: 2.196299  [44800/60000]
loss: 2.181462  [51200/60000]
loss: 2.192415  [57600/60000]
Test Error: 
 Accuracy: 47.9%, Avg loss: 2.150180 

Epoch 2
-------------------------------
loss: 2.124759  [    0/60000]
loss: 2.131443  [ 6400/60000]
loss: 2.104781  [12800/60000]
loss: 2.071867  [19200/60000]
loss: 2.038198  [25600/60000]
loss: 2.003879  [32000/60000]
loss: 1.980923  [38400/60000]
loss: 1.987780  [44800/60000]
loss: 1.875359  [51200/60000]
loss: 1.905337  [57600/60000]
Test Error: 
 Accuracy: 55.8%, Avg loss: 1.872483 

Epoch 3
-------------------------------
loss: 1.921341  [    0/60000]
loss: 1.855085  [ 6400/60000]
loss: 1.831424  [12800/60000]
loss: 1.744574  [19200/60000]
loss: 1.704655  [25600/60000]
loss: 1.708343  [32000/60000]
loss: 1.642833  [38400/60000]
loss: 1.588720  [44800/60000]
loss: 1.547683  [51200/60000]
loss: 1.498120  [57600/60000]
Test Error: 
 Accuracy: 60.9%, Avg loss: 1.512527 

Epoch 4
-------------------------------
loss: 1.448732  [    0/60000]
loss: 1.442486  [ 6400/60000]
loss: 1.488690  [12800/60000]
loss: 1.276109  [19200/60000]
loss: 1.383281  [25600/60000]
loss: 1.397246  [32000/60000]
loss: 1.326674  [38400/60000]
loss: 1.395975  [44800/60000]
loss: 1.274610  [51200/60000]
loss: 1.193545  [57600/60000]
Test Error: 
 Accuracy: 61.5%, Avg loss: 1.256534 

Epoch 5
-------------------------------
loss: 1.235790  [    0/60000]
loss: 1.250143  [ 6400/60000]
loss: 1.187406  [12800/60000]
loss: 1.277617  [19200/60000]
loss: 1.204994  [25600/60000]
loss: 1.118148  [32000/60000]
loss: 1.168185  [38400/60000]
loss: 1.148146  [44800/60000]
loss: 1.017568  [51200/60000]
loss: 1.056769  [57600/60000]
Test Error: 
 Accuracy: 63.2%, Avg loss: 1.097873 

Epoch 6
-------------------------------
loss: 0.963901  [    0/60000]
loss: 1.041870  [ 6400/60000]
loss: 1.224379  [12800/60000]
loss: 1.055848  [19200/60000]
loss: 1.106856  [25600/60000]
loss: 1.003040  [32000/60000]
loss: 0.870065  [38400/60000]
loss: 0.893893  [44800/60000]
loss: 1.080920  [51200/60000]
loss: 1.000239  [57600/60000]
Test Error: 
 Accuracy: 65.3%, Avg loss: 0.995736 

Epoch 7
-------------------------------
loss: 0.905157  [    0/60000]
loss: 1.014492  [ 6400/60000]
loss: 0.934206  [12800/60000]
loss: 0.886744  [19200/60000]
loss: 0.868839  [25600/60000]
loss: 0.939224  [32000/60000]
loss: 0.985162  [38400/60000]
loss: 0.897734  [44800/60000]
loss: 1.097796  [51200/60000]
loss: 0.958092  [57600/60000]
Test Error: 
 Accuracy: 66.3%, Avg loss: 0.924568 

Epoch 8
-------------------------------
loss: 0.833020  [    0/60000]
loss: 1.027762  [ 6400/60000]
loss: 0.796101  [12800/60000]
loss: 0.934080  [19200/60000]
loss: 0.815363  [25600/60000]
loss: 0.921190  [32000/60000]
loss: 1.076561  [38400/60000]
loss: 0.729981  [44800/60000]
loss: 0.787333  [51200/60000]
loss: 0.905401  [57600/60000]
Test Error: 
 Accuracy: 67.7%, Avg loss: 0.871649 

Epoch 9
-------------------------------
loss: 0.918844  [    0/60000]
loss: 0.978086  [ 6400/60000]
loss: 0.821040  [12800/60000]
loss: 0.663510  [19200/60000]
loss: 0.760209  [25600/60000]
loss: 0.905184  [32000/60000]
loss: 0.881537  [38400/60000]
loss: 0.776515  [44800/60000]
loss: 0.945093  [51200/60000]
loss: 0.792388  [57600/60000]
Test Error: 
 Accuracy: 68.4%, Avg loss: 0.832155 

Epoch 10
-------------------------------
loss: 0.783152  [    0/60000]
loss: 0.680521  [ 6400/60000]
loss: 0.683104  [12800/60000]
loss: 0.824782  [19200/60000]
loss: 0.773960  [25600/60000]
loss: 0.897314  [32000/60000]
loss: 0.935476  [38400/60000]
loss: 0.674080  [44800/60000]
loss: 0.671864  [51200/60000]
loss: 0.707593  [57600/60000]
Test Error: 
 Accuracy: 70.4%, Avg loss: 0.798783 

Done!
```

# まとめ
PyTorchで提供されているモジュールを使って、簡単なニューラルネットワークの作成と、その学習を実装しました。ニューラルネットワークの実装は非常に簡単で直感的でしたが、学習部分はややわかりにくいという印象でした。

次回からは、今回学習した基礎をもとに、PyTorchを使ってTransformerの実装をしてみようと思います。

# 参考文献
- [Introduction to PyTorch](https://pytorch.org/tutorials/beginner/basics/intro.html)