クイックPyTorch入門

いままでKerasしか使ったことなかったが、他の人のコードを読んだり、修正したりするのにPyTorch需要が出てきたので、いまさらPyTorchを学びました。
細かいことは後から知っていけばよいので、とにかく超簡単な例で一連の流れを学びます。

今回使うnotebookは[こちら](https://github.com/birdwatcherYT/pytorch_starter/blob/main/pytorch_starter.ipynb)。
notebookだけ見てもわかるようにコメント書いてあります。

# データ準備
手書き数字データのmnistを使っていきます。

```python
import torch
import torchvision
import torchvision.transforms as transforms
# データの変換用
transform = transforms.Compose(
    [
        # numpy.ndarrayをテンソルに変換
        transforms.ToTensor(),
        # 平均0.5、標準偏差0.5へ。チャンネル数のタプルで渡す。
        transforms.Normalize(mean=(0.5, ), std=(0.5, ))
    ]
)
# 学習データ用意
trainset = torchvision.datasets.MNIST(
    root='./dataset', # 保存先
    train=True,
    download=True,
    transform=transform
)
# 学習用ローダー
trainloader = torch.utils.data.DataLoader(
    trainset,
    batch_size=128,
    shuffle=True,
    num_workers=2
)
```
- `transforms`: データセットを前処理するためのツールです
- `torchvision.datasets.MNIST`: pytorchがMNISTデータセットを用意してくれているのでこれを使います
- `DataLoader`: MNISTに限らず、自分で用意したデータでもよく使うもの。指定したデータセットをバッチサイズごと取り出してくれる。


## データの確認
```python
dataiter = iter(trainloader)
images, labels = dataiter.next()
```
![スクリーンショット 2021-03-13 0.58.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/b12e8a04-5107-81f3-7b0f-a324e7b242fa.png)
バッチサイズの分数字ラベルと、画像データが取り出されていることが確認できます。

# モデル
あえて単純なモデルにしてます。

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

# ネットワークの定義
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 28x28次元 -> 256次元
        self.fc1 = nn.Linear(1*28*28, 256)
        # 256次元 -> 10次元（10クラスあるため）
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        # shapeを変形する（バッチサイズ x 次元数）
        x = x.view(-1, 1*28*28)
        # 線形変換後ReLuをかます
        x = F.relu(self.fc1(x))
        # 【注意】Kerasだと最後にsoftmaxをかますが、
        # pytorchだと損失関数を計算する nn.CrossEntropyLoss の中にsoftmaxの計算が含まれている
        # ただし、予測時に確率にしたい場合はアウトプットを nn.functional.softmax() で変換する必要がある
        x = self.fc2(x)
        return x
```
- `nn.Module`を継承してモデルを定義します
- `__init__`内では、モデルで使う層を定義しておきます
    - KerasだとSequentialにaddしていくだけだったが、PyTorchでは事前に層を定義してから`forward`で使う
- `forward`では、データ入力されたときのモデルの出力を書きます
    - この例では、1次元ベクトルにしたあとに、線形変換に活性化関数をかまして、最後に線形変換して返す
    - 【注意】のコメントはぜひ見ておいてください

```python
net = Net()

import torch.optim as optim
# 目的関数
criterion = nn.CrossEntropyLoss()
# 最適化手法
optimizer = optim.Adam(net.parameters(), lr=0.001)
```
- モデルのインスタンス作成
- 目的関数の設定（今回は`CrossEntropyLoss`）
- 最適化手法を設定します（今回は`Adam`）

# 学習
Kerasと違って自分で学習のループを書くことになります。

```python
# Kerasやsklearnでいうfit
epochs=10
varbose=100
for epoch in tqdm(range(epochs)):
    # 損失（表示用）
    running_loss = 0.0
    for i, (inputs, labels) in enumerate(trainloader):
        # 勾配の初期化
        optimizer.zero_grad()
        # Netのforwardに基づいて出力が計算される
        outputs = net(inputs)
        # 損失の計算
        loss = criterion(outputs, labels)
        # ↑ここまでで計算グラフの構築が行われ、勾配に使う情報が設定される
        # 勾配を計算
        loss.backward()
        # 最適化法に基づいてパラメータ更新
        optimizer.step()
        # varbose回分の損失の和。item() で値を取り出している
        running_loss += loss.item()
        if i % varbose == varbose-1:
            print(f"epoch:{epoch + 1}, batch:{i + 1}, loss: {running_loss / varbose}")
            running_loss = 0.0
```

- `optimizer.zero_grad()`: 勾配情報をクリアします
    - 前の勾配情報が残っているため、ゼロクリアする必要がある
- `outputs = net(inputs)`: モデルのforwardに基づいて出力が計算されます
- `loss = criterion(outputs, labels)`: 先程指定した`CrossEntropyLoss`に基づいて損失計算されます
- `loss.backward()`: 逆伝播で勾配を計算します
    - この段階ではまだパラメータは更新されてません
- `optimizer.step()`: パラメータ更新されます

その他の記述は学習の進捗が見えるようにするもので、特に意味はありません。

>epoch:1, batch:100, loss: 0.6820216017961502
epoch:1, batch:200, loss: 0.3693145060539246
epoch:1, batch:300, loss: 0.332705043554306
epoch:1, batch:400, loss: 0.29559941463172434
epoch:2, batch:100, loss: 0.23795114882290364
epoch:2, batch:200, loss: 0.20222241077572106
epoch:2, batch:300, loss: 0.1960952030867338
epoch:2, batch:400, loss: 0.1974168461561203
epoch:3, batch:100, loss: 0.15257078558206558
epoch:3, batch:200, loss: 0.1440433470159769
epoch:3, batch:300, loss: 0.142299246750772
epoch:3, batch:400, loss: 0.13810560397803784
epoch:4, batch:100, loss: 0.11472118373960256
epoch:4, batch:200, loss: 0.10906709022819996
epoch:4, batch:300, loss: 0.11881133031100034
epoch:4, batch:400, loss: 0.10873796993866564
epoch:5, batch:100, loss: 0.09303514676168562
epoch:5, batch:200, loss: 0.09735027667135
epoch:5, batch:300, loss: 0.09419337672181427
epoch:5, batch:400, loss: 0.08241723172366619

実行するとこのように表示されます。

# 予測

```python
correct = 0
total = 0
# 予測時に勾配更新しないため、no_gradを指定
with torch.no_grad():
    for (images, labels) in testloader:
        # forwardした結果
        # これを nn.functional.softmax() すると確率が得られるが、ここでは使わない
        outputs = net(images)
        # torch.max() は (最大値, そのindex)を返す関数。dimは最大値を見る軸
        _, predicted = torch.max(outputs.data, dim=1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
print(f"Accuracy: {100 * float(correct/total)}")
```
>Accuracy: 97.26

- `outputs = net(images)`: モデルの`forward`に基づいて予測されます
    - 【注意】でも書いたとおり、`nn.functional.softmax()`を取らないと確率になってません
- `_, predicted = torch.max(outputs.data, dim=1)`: 予測値が最大となるindexを取ってきます（これが予測した数字）
- `total += labels.size(0)`: 予測したサンプル数の合計を計算してます
- `correct += (predicted == labels).sum().item()`: 正解した個数を記録してます
    - `item()`はTensorから値を取ってくる関数です

# GPUを使うには
コードの書き方だけのメモで、環境構築は別のサイトを参照してください。

```python
# cudaが使えるか確認
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
```
>device(type='cpu')

- `torch.cuda.is_available()`: GCPを使えるかどうか判定します

```python
# .toメソッドでデバイスに移すようにする
# ネットワークに関して
net = Net().to(device)
# データに関して
images, labels = dataiter.next()
images, labels = images.to(device), labels.to(device)
```

- `.to(device)`でネットワークとデータをデバイスに移すようにします