KerasでTensorboardを使用する際のTips

# 対象読者

- Tensorboardを使っている人

## おまけ
Kerasに関する書籍を翻訳しました。画像識別、画像生成、自然言語処理、時系列予測、強化学習まで幅広くカバーしています。
[直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ](https://www.amazon.co.jp/Deep-Learning-%E2%80%95Python%C3%97Keras%E3%81%A7%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2%E3%82%92%E5%BD%A2%E3%81%AB%E3%81%99%E3%82%8B%E3%83%AC%E3%82%B7%E3%83%94-Antonio-Gulli/dp/4873118263/ref=sr_1_1?s=books&ie=UTF8&qid=1530227887&sr=1-1&keywords=%E7%9B%B4%E6%84%9F+Deep+Learning)

# Tensorboardとは

モデルの値を確認する可視化ツールであり、日々確認できる項目が増えています。
これを使いこなせると機械学習エンジニアの仕事がスムーズになると思います。

https://www.tensorflow.org/get_started/summaries_and_tensorboard

# この記事で紹介するTensorboardの機能

- SCALARS： ロスや精度などの学習中の挙動を確認するために使用
- IMAGES：各層の重みやバイアスの遷移が確認できる
- GRAPHS：モデルの構成を確認するために使用
- DISTRIBUTIONS：各層の値の分布
- HISTOGRAMS：各層の重みのヒストグラム
- PROJECTOR：識別などで潜在空間が適切に識別できる空間になっているかを可視化して確認

## 場合別の使い分け

ざっくりと傾向を把握したい時に使用

- SCALARS

細かい値を見て検証したい

- IMAGES：各層の重みやバイアスの遷移が確認できる
- DISTRIBUTIONS：各層の値の分布
- HISTOGRAMS：各層の重みのヒストグラム
- PROJECTOR：識別などで潜在空間が適切に識別できる空間になっているかを可視化して確認

モデルの構造を確認したい

- GRAPH

下記ですべての機能が使用可能です。ただしGRAPHとPROJECTORに関係する`embeddings_layer_names`と`embeddings_metadata`は注意が必要になります。その点については後述します。

```py3

TensorBoard(log_dir=log_dir,
            histogram_freq=1,
            write_grads=True,
            write_images=1,
            embeddings_freq=1,
            embeddings_layer_names=layer_name,
            embeddings_metadata=metadata_file
            )
```

# SCALARS

下記のように学習データとバリデーションデータのロスと精度の遷移を確認できます。

![Screenshot from 2017-12-04 07:54:03.png](https://qiita-image-store.s3.amazonaws.com/0/10496/2197c752-32a7-e093-4dd1-30f35199788c.png)

ここでのTipsはこのグラフはcsvやjsonでダウンロード可能です。'Show data download links'をクリックするとcsvとjson用のダウンロードリンクを取得できます

![Screenshot from 2017-12-04 07:56:13.png](https://qiita-image-store.s3.amazonaws.com/0/10496/d6c7f267-30ae-385e-e9b0-1cafcb5bf548.png)

# IMAGES

下記で各層の値の遷移が確認できます。

![Screenshot from 2017-12-04 08:00:44.png](https://qiita-image-store.s3.amazonaws.com/0/10496/cefe308f-e684-c6a3-815f-3f3a99eec52b.png)


# GRAPHS

下記のようにモデルの構造を確認できます。

![Screenshot from 2017-12-04 08:05:00.png](https://qiita-image-store.s3.amazonaws.com/0/10496/a2ec94de-6b6e-f376-4759-8086764a0413.png)

下記のようにスコープを決めてモデルを設定しないと上記のようにまとめて表示してくれないので注意が必要です。

```py
with tf.name_scope('Model') as scope:
    model = Sequential()
    with tf.name_scope('Dense') as scope:
        model.add(Dense(N_HIDDEN, input_shape=(RESHAPED,)))

    with tf.name_scope('Relu') as scope:
        model.add(Activation('relu'))

    with tf.name_scope('Dropuout') as scope:
        model.add(Dropout(DROPOUT))

    with tf.name_scope('Dense2') as scope:
        model.add(Dense(N_HIDDEN))

    with tf.name_scope('Relu2') as scope:
        model.add(Activation('relu'))

    with tf.name_scope('Dropuout2') as scope:
        model.add(Dropout(DROPOUT))

    with tf.name_scope('Dense3') as scope:
        model.add(Dense(NB_CLASSES, activation='softmax'))
```

# DISTRIBUTIONS

各値の分布です。
横軸がエポック数で縦軸が値です。

![Screenshot from 2017-12-04 08:10:11.png](https://qiita-image-store.s3.amazonaws.com/0/10496/9dbfe987-653e-e8f9-2417-06bcffa50f87.png)



# HISTOGRAMS

各層の値のヒストグラムです。奥に行けば行くほど最初のエポックの値になります。
横軸が値の範囲で縦軸が値の頻度になります。

- 緑が学習率を極端にして学習して精度が10%程度のMNIST
- ピンクが通常の精度が95%程度のMNIST

場合です。値の範囲がピンクに比べて大きくまた桁が3桁ほど違います。
このように極端に値の範囲が広い、大きな値に集中しているなどの場合に学習が上手く行っていないことが確認できます。

![Screenshot from 2017-12-04 08:26:19.png](https://qiita-image-store.s3.amazonaws.com/0/10496/e8dec2c8-942e-f507-29c8-51ba1f67ec7d.png)


# PROJECTOR

潜在空間をマッピングして可視化する手法です。

![Screenshot from 2017-12-04 08:31:40.png](https://qiita-image-store.s3.amazonaws.com/0/10496/783dcc12-e63f-699b-7e0d-adb18f04f1b4.png)

通常のKerasの機能を使用する場合にMetafileを用意する必要がありますが潜在空間がどのラベルとマッチしているか分からないと作成できません。
そこで下記のTensorboardを継承したクラスを作成して使用します。

```py3
class TensorResponseBoard(TensorBoard):
    def __init__(self, val_size, img_path, img_size, **kwargs):
        super(TensorResponseBoard, self).__init__(**kwargs)
        self.val_size = val_size
        self.img_path = img_path
        self.img_size = img_size

    def set_model(self, model):
        super(TensorResponseBoard, self).set_model(model)

        if self.embeddings_freq and self.embeddings_layer_names:
            embeddings = {}
            for layer_name in self.embeddings_layer_names:
                # initialize tensors which will later be used in `on_epoch_end()` to
                # store the response values by feeding the val data through the model
                layer = self.model.get_layer(layer_name)
                output_dim = layer.output.shape[-1]
                response_tensor = tf.Variable(tf.zeros([self.val_size, output_dim]),
                                              name=layer_name + '_response')
                embeddings[layer_name] = response_tensor

            self.embeddings = embeddings
            self.saver = tf.train.Saver(list(self.embeddings.values()))

            response_outputs = [self.model.get_layer(layer_name).output
                                for layer_name in self.embeddings_layer_names]
            self.response_model = Model(self.model.inputs, response_outputs)

            config = projector.ProjectorConfig()
            embeddings_metadata = {layer_name: self.embeddings_metadata
                                   for layer_name in embeddings.keys()}

            for layer_name, response_tensor in self.embeddings.items():
                embedding = config.embeddings.add()
                embedding.tensor_name = response_tensor.name

                # for coloring points by labels
                embedding.metadata_path = embeddings_metadata[layer_name]

                # for attaching images to the points
                embedding.sprite.image_path = self.img_path
                embedding.sprite.single_image_dim.extend(self.img_size)

            projector.visualize_embeddings(self.writer, config)

        def on_epoch_end(self, epoch, logs=None):
            super(TensorResponseBoard, self).on_epoch_end(epoch, logs)

            if self.embeddings_freq and self.embeddings_ckpt_path:
                if epoch % self.embeddings_freq == 0:
                    # feeding the validation data through the model
                    val_data = self.validation_data[0]
                    response_values = self.response_model.predict(val_data)
                    if len(self.embeddings_layer_names) == 1:
                        response_values = [response_values]

                    # record the response at each layers we're monitoring
                    response_tensors = []
                    for layer_name in self.embeddings_layer_names:
                        response_tensors.append(self.embeddings[layer_name])
                    K.batch_set_value(
                        list(zip(response_tensors, response_values)))

                    # finally, save all tensors holding the layer responses
                    self.saver.save(self.sess, self.embeddings_ckpt_path, epoch)
```

下記の部分でヴァリデーションデータごとにプロジェクションに必要なEmbeddingのTensorを導出しています。

```py
            if self.embeddings_freq and self.embeddings_ckpt_path:
                if epoch % self.embeddings_freq == 0:
                    # feeding the validation data through the model
                    val_data = self.validation_data[0]
                    response_values = self.response_model.predict(val_data)
                    if len(self.embeddings_layer_names) == 1:
                        response_values = [response_values]

                    # record the response at each layers we're monitoring
                    response_tensors = []
                    for layer_name in self.embeddings_layer_names:
                        response_tensors.append(self.embeddings[layer_name])
                    K.batch_set_value(
                        list(zip(response_tensors, response_values)))

                    # finally, save all tensors holding the layer responses
                    self.saver.save(self.sess, self.embeddings_ckpt_path, epoch)
```

バリデーションデータ分だけ潜在空間を用意できたので画像データの作成とmetaファイルをヴァリデーションサイズ分作成すればマッピングが可能です。


```py
img_array = X_test.reshape(100, 100, 28, 28)
img_array_flat = np.concatenate([np.concatenate([x for x in row], axis=1) for row in img_array])
img = Image.fromarray(np.uint8(255 * (1. - img_array_flat)))
img.save(os.path.join(log_dir, 'images.jpg'))
np.savetxt(os.path.join(log_dir, 'metadata.tsv'), np.where(Y_test)[1], fmt='%d')
```

クラスの設定とmerada.tsvの作成ができたら下記のようにcallbackモジュールを作成して使用します。

```py
# MNIST
tb = TensorResponseBoard(log_dir=log_dir, histogram_freq=1, batch_size=10,
                         write_graph=True, write_grads=True, write_images=True,
                         embeddings_freq=1,
                         embeddings_layer_names=['dense_1'],
                         embeddings_metadata='metadata.tsv',
                         val_size=len(X_test), img_path='images.jpg', img_size=[28, 28])
with tf.name_scope('TrainingModel') as scope:
    model.fit(X_train, Y_train,
              batch_size=BATCH_SIZE, epochs=NB_EPOCH,
              callbacks=[tb],
              verbose=VERBOSE, validation_split=VALIDATION_SPLIT)
```

# おまけ

保存する`log_dir`の名前に使用したOptimizerのパラメータや実験条件を加えておくと確認が早くなります。モデルのパラメータはグラフに保存されますがその他の情報は保存されないのでここを使用すると良いと思います。


# 参考

https://stackoverflow.com/questions/42315202/understanding-tensorboard-weight-histograms

https://stackoverflow.com/questions/45265436/keras-save-image-embedding-of-the-mnist-data-set