【PyTorchチュートリアル⑦】Visualizing Models, Data, And Training With Tensorboard

# はじめに
[前回](https://qiita.com/sudominoru/items/db8529ef7f0fd7127faa)に引き続き、PyTorch [公式チュートリアル](https://pytorch.org/tutorials/) の第7弾です。
今回は [Visualizing Models, Data, and Training with TensorBoard](https://pytorch.org/tutorials/intermediate/tensorboard_tutorial.html) を進めます。

# Visualizing Models, Data, And Training With Tensorboard

[60 Minute Blitz](https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html) では、基本的なニューラルネットワークを構築し、学習データを利用してトレーニングする方法を見てきました。
今回はトレーニングの状況を可視化し、トレーニングが進行しているかどうかを確認する方法を見ていきましょう。
可視化は TensorBoard を利用します。PyTorchは、ニューラルネットワークのトレーニングと結果を視覚化するためのツール「TensorBoard」を利用することができます。

このチュートリアルでは、torchvision.datasets の Fashion-MNIST データを使用して、その機能の一部を説明します。
次の方法を学びます。

1. データを読み込み、適切な変換を行います。（前のチュートリアルとほぼ同じです）
1. TensorBoardをセットアップします。
1. TensorBoardに書き込みます。
1. TensorBoardを使用してモデルアーキテクチャを可視化します。
1. TensorBoardを使用してトレーニング途中の予測や精度を可視化します。

具体的には、上記の5点で次のことがわかります。

* トレーニングデータを検査する方法
* モデルのトレーニング中にモデルのパフォーマンスを追跡する方法
* トレーニング後のモデルのパフォーマンスを評価する方法。

[CIFAR-10](https://qiita.com/sudominoru/items/61f57946799e67cedd47) チュートリアルと同様のコードから始めます。

```python
%matplotlib inline
```

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

import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# transform の定義
transform = transforms.Compose(
    [transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))])

# データセット
trainset = torchvision.datasets.FashionMNIST('./data',
    download=True,
    train=True,
    transform=transform)
testset = torchvision.datasets.FashionMNIST('./data',
    download=True,
    train=False,
    transform=transform)

# データローダ
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                        shuffle=True, num_workers=2)


testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                        shuffle=False, num_workers=2)

# クラス分類の定数
classes = ('T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
        'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle Boot')

# 画像表示のヘルパー関数
# (以下の `plot_classes_preds` 関数で使用されます)
def matplotlib_imshow(img, one_channel=False):
    if one_channel:
        img = img.mean(dim=0)
    img = img / 2 + 0.5     # 非正規化
    npimg = img.numpy()
    if one_channel:
        plt.imshow(npimg, cmap="Greys")
    else:
        plt.imshow(np.transpose(npimg, (1, 2, 0)))
```

[CIFAR-10](https://qiita.com/sudominoru/items/61f57946799e67cedd47) チュートリアルと同様のモデルを定義しますが、画像が3つではなく1つのチャネルになり、32x32ではなく28x28になるようにするため、少し変更します。

```python
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 4 * 4, 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 * 4 * 4)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()
```

以前と同じオプティマイザと損失関数を定義します。

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

# 1.TensorBoard setup

次に、TensorBoardをセットアップします。
torch.utilsから TensorBoard をインポートし、TensorBoard に書き込むためのオブジェクトである SummaryWriter を定義します。

```python
from torch.utils.tensorboard import SummaryWriter

# デフォルトのログディレクトリは "runs" ですが、こので指定できます。
writer = SummaryWriter('runs/fashion_mnist_experiment_1')
```

この行を実行するだけで、「runs/fashion_mnist_experiment_1」ディレクトリが作成されます。

# 2.Writing to TensorBoard

次に、make_grid を使用して TensorBoard に画像を書き込みます。

```python
# ランダムなトレーニング画像を取得します
dataiter = iter(trainloader)
images, labels = dataiter.next()

# 画像のグリッドを作成します
img_grid = torchvision.utils.make_grid(images)

# 画像を表示します
matplotlib_imshow(img_grid, one_channel=True)

# テンソルボードに書き込みます
writer.add_image('four_fashion_mnist_images', img_grid)
```

（このチュートリアルには記載がありませんが、Google Colaboratory で Tensorboard を利用する場合、TensorBoardノートブック拡張機能を読み込みます。）

```python
%load_ext tensorboard
```

```python
#Google Colaboratory で Tensorboard を利用する場合、マジックコマンドで Tensorboard を実行します
#tensorboard --logdir=runs
%tensorboard --logdir=runs
```

ローカル環境で実行した場合、
https：//localhost:6006
で以下の tensorboard の画面が閲覧できます。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/4af935ae-e822-eac3-4af1-9c05794ab32e.png)
TensorBoard を実行することができました。
以降で TensorBoard の機能を見ていきます。

# 3.Inspect the model using TensorBoard

TensorBoard の長所の1つは、複雑なモデル構造を視覚化できることです。作成したモデルを視覚化してみましょう。

```python
writer.add_graph(net, images)
writer.close()
```

TensorBoardを更新すると、次のような「GRAPHS」タブが表示されます。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/f0d09630-d428-8929-4217-7d32accbea44.png)
「Net」をダブルクリックして展開し、モデルを構成する個々を確認できます。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/2743c757-d75e-1439-3832-e1d84e952f3d.png)
TensorBoard には、画像データなどの高次元データを低次元空間で視覚化するための非常に便利な機能があります。以降で説明します。

# 4.Adding a “Projector” to TensorBoard

add_embeddingメソッドを介して、高次元データの低次元表現を視覚化できます。

```python
import tensorflow as tf
import tensorboard as tb
tf.io.gfile = tb.compat.tensorflow_stub.io.gfile
```

```python
# ヘルパー関数
def select_n_random(data, labels, n=100):
    '''
    データセットからn個のランダムなデータポイントとそれに対応するラベルを選択します
    '''
    assert len(data) == len(labels)

    perm = torch.randperm(len(data))
    return data[perm][:n], labels[perm][:n]

# ランダムな画像とそのターゲットインデックスを選択します
images, labels = select_n_random(trainset.data, trainset.targets)

# 各画像のクラスラベルを取得します
class_labels = [classes[lab] for lab in labels]

# ログの埋め込み
features = images.view(-1, 28 * 28)
writer.add_embedding(features,
                    metadata=class_labels,
                    label_img=images.unsqueeze(1))
writer.close()
```
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/8704fb6d-4485-31d9-bcc8-7ae56b18cc6f.png)
TensorBoard の [PROJECTOR] タブに、これらの100枚の画像が表示されます。
それぞれの画像は784次元ですが、3次元空間に投影されています。
ドラッグすると、3次元の投影を回転できます。
左上にある「色：ラベル」を選択し、「夜間モード」を有効にすると、背景が黒くなり画像が見やすくなります。

ここまでで、TensorBoard を利用してデータを可視化する方法が分かりました。
次に、TensorBoard でトレーニングと評価を可視化する方法を見てみましょう。

# 5.Tracking model training with TensorBoard

[前回のチュートリアル](https://qiita.com/sudominoru/items/61f57946799e67cedd47)では、2000回の反復ごとにモデルの損失値を単に出力しました。このチュートリアルでは損失値を TensorBoard に記録し、plot_classes_preds 関数で予測値を表示します。


```python
# ヘルパー関数
def images_to_probs(net, images):
    '''
    学習したモデルと画像を引数に、予測値とその確率を返却します
    '''
    output = net(images)
    # 出力された確率を予測クラスに変換します
    _, preds_tensor = torch.max(output, 1)
    preds = np.squeeze(preds_tensor.numpy())
    return preds, [F.softmax(el, dim=0)[i].item() for i, el in zip(preds, output)]


def plot_classes_preds(net, images, labels):
    '''
    学習したモデルと画像、教師データを引数に、matplotlib の図を生成します。
    これは、モデルが予測した最も確率の高いラベルを表示し、予測が正しいかどうかを
    色付けします。
    「images_to_probs」関数を使用します。
    '''
    preds, probs = images_to_probs(net, images)
    # 予測されたラベルと実際のラベルとともに、画像をバッチでプロットします。    
    fig = plt.figure(figsize=(12, 48))
    for idx in np.arange(4):
        ax = fig.add_subplot(1, 4, idx+1, xticks=[], yticks=[])
        matplotlib_imshow(images[idx], one_channel=True)
        ax.set_title("{0}, {1:.1f}%\n(label: {2})".format(
            classes[preds[idx]],
            probs[idx] * 100.0,
            classes[labels[idx]]),
                    color=("green" if preds[idx]==labels[idx].item() else "red"))
    return fig
```

[前回のチュートリアル](https://qiita.com/sudominoru/items/61f57946799e67cedd47)と同じモデル使用して学習しますが、コンソールに出力するのではなく、1000バッチごとに TensorBoard に書き込みます。（add_scalar関数）
さらに、トレーニング中の予測値と、予測した画像を出力します。（add_figure関数）

```python
running_loss = 0.0
for epoch in range(1):  # データセットを複数回ループします

    for i, data in enumerate(trainloader, 0):

        # 入力を取得します。データは[inputs, labels]のリストです
        inputs, labels = data

        # 勾配を初期化します
        optimizer.zero_grad()

        # 順伝播 + 逆伝播 + 最適化
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if i % 1000 == 999:    # 1000バッチごと

            # ...1000バッチごとに loss 値を記録する
            writer.add_scalar('training loss',
                            running_loss / 1000,
                            epoch * len(trainloader) + i)

            # ...ランダムなミニバッチで、モデルの予測をMatplotlib図をログに記録します
            writer.add_figure('predictions vs. actuals',
                            plot_classes_preds(net, inputs, labels),
                            global_step=epoch * len(trainloader) + i)
            running_loss = 0.0
print('Finished Training')
```

```python:out
Finished Training
```

「SCALARS」タブで、トレーニング中の loss 値を確認できます。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/dd986984-7bd5-7eea-86d2-98674225e171.png)
さらに、1000バッチごとに行ったモデルの予測を確認できます。「IMAGES」タブを表示し、「predictions vs. actuals」ビジュアライゼーションの下にスクロールしてください。
見てみると、トレーニングを3,000回繰り返しただけで、モデルはすでにシャツ、スニーカー、コートなどを分類できています。ただし、トレーニングの後半ほどは確率は高くありません。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/98da1ff5-f63f-fcdb-ec20-7dd732f39b43.png)
[前回のチュートリアル](https://qiita.com/sudominoru/items/61f57946799e67cedd47)では、トレーニング後にラベルごとの正解率を確認しました。ここでは、TensorBoardを使用して、各クラスのPR曲線をプロットします。

# 6.Assessing trained models with TensorBoard

```python
# 1. test_size x num_classes の Tensor で確率予測を取得します
# 2. test_size の Tensor で preds を取得します
# 実行に最大10秒かかります
class_probs = []
class_preds = []
with torch.no_grad():
    for data in testloader:
        images, labels = data
        output = net(images)
        class_probs_batch = [F.softmax(el, dim=0) for el in output]
        _, class_preds_batch = torch.max(output, 1)

        class_probs.append(class_probs_batch)
        class_preds.append(class_preds_batch)

test_probs = torch.cat([torch.stack(batch) for batch in class_probs])
test_preds = torch.cat(class_preds)

# ヘルパー関数
def add_pr_curve_tensorboard(class_index, test_probs, test_preds, global_step=0):
    '''
    0 から 9 までの「class_index」を取り込み、対応するPR曲線をプロットします
    '''
    tensorboard_preds = test_preds == class_index
    tensorboard_probs = test_probs[:, class_index]

    writer.add_pr_curve(classes[class_index],
                        tensorboard_preds,
                        tensorboard_probs,
                        global_step=global_step)
    writer.close()

# PR曲線をプロットします
for i in range(len(classes)):
    add_pr_curve_tensorboard(i, test_probs, test_preds)
```

「PR CURVES」タブが表示されます。各ラベルのPR曲線を開いて確認してみましょう。一部のラベルでは「曲線の下の領域」がほぼ100％であるのに対し、いくつかのラベルではこの領域が少ないことがわかります。
![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/ac1e744c-31da-d4ba-2b65-9dd822a64b79.png)
このチュートリアルでは、TensorBoard と PyTorch との統合の紹介しました。もちろん、Jupyter Notebook だけでも TensorBoard と同様なことが実現できますが、TensorBoardを使用すると、視覚的に確認することができます。

# 7. 最後に
以上が「Visualizing Models, Data, And Training With Tensorboard」です。
PyTorch で Tensorboard の使い方が分かりました。
[次回](https://qiita.com/sudominoru/items/bf3bd96c6921d9106742)は「TorchVision Object Detection Finetuning Tutorial」を進めたいと思います。

# 履歴
2020/10/15 初版公開
2020/11/15 次回のリンク追加