tfdbg を使って Keras の nan や inf を潰す

# はじめに
Keras に限らず、機械学習等の科学計算をおこなっているときに nan や inf が出現することがあります。
nan や inf は主にゼロ除算などのバグに起因して発生しますが、nan や inf それ自体を目的に使うこともあるため、エラーになるわけではありません。エラーになったとしても、「きっとどこかでゼロ除算しているんだろなぁ・・・」と思いを馳せることができるだけです。

そこで、TensorFlow 公式デバッガである tfdbg を使って、(TensorFlow バックエンドの) Keras モデルをデバッグして nan や inf の原因を追ってみます。

今回デモ用に利用したコードは公開していますので、ご興味があればご参照ください：
https://github.com/keisuke-nakata/keras-tfdbg-tutorial


# tfdbg とは

公式の tfdbg ドキュメントのはじめには次のようにあります：

> TensorFlow debugger (tfdbg) is a specialized debugger for TensorFlow. It lets you view the internal structure and states of running TensorFlow graphs during training and inference, which is difficult to debug with general-purpose debuggers such as Python's pdb due to TensorFlow's computation-graph paradigm.

抄訳すると、「tfdbg は TensorFlow 専用のデバッガで、Python の pdb みたいに TensorFlow をデバッグできるよ」という感じです。

TensorFlow をバックエンドとして Keras を利用している場合でも tfdbg することが出来ます。


# 今回対象とするモデル

今回デバッグの対象とするのは、 MNIST を2層のパーセプトロン (入力と出力を入れれば4層) で分類するようなモデルです：

```
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_1 (Dense)              (None, 500)               392500
_________________________________________________________________
dense_2 (Dense)              (None, 10)                5010
=================================================================
Total params: 397,510
Trainable params: 397,510
Non-trainable params: 0
_________________________________________________________________
```

モデルは次のように構築しています (なお、以下では `import` やデータの読み込み等の本質ではない部分は省略していますので、詳細や再現に興味があれば元コードをご覧ください)：

```python
def build_model():
    def reproducable_trunc_norm():
        return TruncatedNormal(mean=0.0, stddev=0.1, seed=RAND_SEED)

    def reproducable_const():
        return Constant(value=0.1)

    def unstable_categorical_crossentropy(y_true, y_pred):
        return - tf.reduce_sum(y_true * tf.log(y_pred), axis=len(y_pred.get_shape()) - 1)

    model = Sequential()
    model.add(Dense(
        HIDDEN_SIZE, input_shape=(FLAT_IMAGE_SIZE, ), activation='relu',
        kernel_initializer=reproducable_trunc_norm(), bias_initializer=reproducable_const()))
    model.add(Dense(
        NUM_LABELS, activation='softmax',
        kernel_initializer=reproducable_trunc_norm(), bias_initializer=reproducable_const()))

    model.compile(loss=unstable_categorical_crossentropy, optimizer=Adam(lr=0.025), metrics=['accuracy'])
    # model.compile(loss='categorical_crossentropy', optimizer=Adam(lr=0.025), metrics=['accuracy'])
    model.summary()
    return model
```

`def reproducable_trunc_norm` と `def reproducable_const` は、今回デバッグするにあたって挙動を固定するための乱数調整です。
また、今回 nan を引き起こす原因となっているのは `def unstable_categorical_crossentropy` を model の loss に指定しているためです。今回はデモなので事前に原因が明らかですが、気づかなかったものとして話を進めます。正しくは `loss='categorical_crossentropy'` ですね (コメントアウトされている行)。

では、モデルを学習させてみましょう。

```python
x_train, y_train, x_test, y_test = get_mnist_dataset()
model = build_model()
model.fit(x_train, y_train, batch_size=100, epochs=10, validation_data=(x_test, y_test))
```

<img width="879" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/6de7f831-2210-4972-dbcb-73744683b8b7.png">

loss が nan ですね。つらいです。
(上の結果は、`python debug_mnist.py` とすると得られます)

# デバッグしてみる

## tfdbg を起動してみる

早速デバッグしてみましょう。
まずは Keras が裏で使用している TensorFlow のセッションをハックしにいって、デバッガを仕込みます。以下の関数を最初に呼べば OK です[^1]。

```python
def set_debugger_session():
    sess = K.get_session()
    sess = tf_debug.LocalCLIDebugWrapperSession(sess)
    sess.add_tensor_filter('has_inf_or_nan', has_inf_or_nan)
    K.set_session(sess)
```

`python debug_mnist.py --debug` を実行すると、`set_debugger_session()` を最初に呼んでから Keras のモデルの学習が始まるようにプログラムを書いているため、次のような画面が立ち上がると思います：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/dd4b5e21-f4f2-7136-6556-39e9b369e924.png">

無事 tfdbg が起動しました！
おもむろに `run` コマンドを二回入力[^2]してから、 `lt` (`list_tensors` の省略形です) を打ってみてください：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/c48d7674-f794-9deb-4bbd-89111d3755fc.png">

TensorFlow のグラフ上に存在するテンソルの一覧が表示されました！
もちろんテンソルの中身を確認することも出来ます。
`pt dense_1/kernel:0` を入力してみてください。(`pt` は `print_tensor` の省略形です)
(なお、`lt` で表示したテンソルの名前をおもむろにクリックしても、そのテンソルを `pt` したのと同じことをしてくれます)

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/820ff20d-85de-cbc9-f9c4-7ee816e5b3f8.png">

いま表示しているのは `dense_1/kernel:0` ですが、これは、先程の model の構造と照らし合わせると、「第1層目の Dense レイヤーの kernel」、すなわち重み行列 $W$ が表示されています。(Keras では学習対象となる主パラメータのことを kernel と呼ぶことが多いです)
`shape: (784, 500)` というのも、MNIST の画像サイズが 28*28=784 で、最初の隠れ層のノー数を 500 に設定していることと辻褄が合っています。

## nan と inf を探す

tfdbg はデバッガなので、「この条件が満たされるまで実行」もできます。
(そのために `session` に登録した関数が `has_inf_or_nan` です。これは自分で作成して自由に名前をつけて登録することが出来ます)。

nan や inf の原因を探すには、実際に nan や inf が出現するまで何回か `run` を実行しないといけません。上述の機能を使って、モデルのどこかに nan または inf が出現するまで `run` を実行してみましょう。

`run -f has_inf_or_nan`

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/fb16b56f-eadd-fd90-2392-bd9dfb300df2.png">

tfdbg のタイトルバーに「run-end: run #2: 3 fetches; 3 feeds」と表示され、実行が停止しました。どうやら nan か inf が出現したようです。
`lt` を実行し、テンソル一覧が表示されることを確認してください。

さらに、`lt -f has_inf_or_nan` を実行することで、「どのテンソルが nan または inf を持っているのか」を表示することが可能です。

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/02c62627-74e6-fdd2-4f4c-dfdc3d522670.png">

問題のテンソルがひとつしかありません。中身を表示してみます `pt Const_4:0`

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/3e278ac4-43f1-ab2f-6e6e-389bfb3a7fee.png">

こいつの中身は `array(inf, dtype=float32)` でした。確かに inf です。
もう1回だけ実行を進めてみます。`run` を実行してから、`lt -f has_inf_or_nan` を実行してください：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/b07efd4c-3301-7c92-f6ff-7a85a00bcb67.png">

今度は `Const_4:0` だけではなく、たくさんテンソルの中身に nan または inf が入ってしまっているようです。

さて、テンソル一覧の上のほうを見ると、`Log:0`, `mul:0`, `Sum:0`, `Neg:0` 等が並んでいますね。
この計算グラフの並びは、もう明らかに自分で定義したロス関数である

```python
    def unstable_categorical_crossentropy(y_true, y_pred):
        return - tf.reduce_sum(y_true * tf.log(y_pred), axis=len(y_pred.get_shape()) - 1)
```

のことです (最初に `tf.log(y_pred)` (つまり `Log:0`) が計算され、次に `*` (つまり `mul:0`) が、次に `reduce_sum` (つまり `Sum:0`) が、次に `-` (つまり `Neg:0`) が計算されていますね)。
では `Log:0` の中身を見てみましょう。`pt Log:0`：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/b222baa6-b042-0472-ae3a-fc7247e29a6f.png">

-inf がいます！見づらければ、おもむろに `/inf` とでも打つとハイライトしてくます。もう Log の時点で -inf が来ている時点でアウトですね。ログの結果が -inf になるのは、$\log(0)$ が評価されたからに違いありません。
上のメニューから、`list_inputs` をクリックしてみてください：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/8a5d2fd6-d284-2b95-7b9e-7c9b6491da35.png">

Log に対する入力が表示されます。最後の活性化関数である Softmax が見えますね。`pt dense_2/Softmax:0` を実行してみてください：

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/62450/c7360c2d-d251-6036-70e5-ef86019ef74d.png">

思った通り、Log で -inf になっていた部分の値が 0 です。
これでは loss の値が nan になるのも当然です。

ここまでくれば、あとは「softmax crossentropy inf」とかでググれば問題はほぼ解決したようなものです。
今回の例なら、きっと「softmax と crossentropy を組み合わせて使うときは数値計算の都合上、特殊なことをしないといけないから、自分でロス定義しないでライブラリの実装を使いなさい」的な情報が見つかることでしょう。

tfdbg を終了するときは、 `quit` コマンドを実行してください。

原因がわかったので、コードのコメントアウトされている部分を復活させて実行すると、無事 nan を回避することができるようになります：

```
$ python debug_mnist.py
Using TensorFlow backend.
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_1 (Dense)              (None, 500)               392500
_________________________________________________________________
dense_2 (Dense)              (None, 10)                5010
=================================================================
Total params: 397,510
Trainable params: 397,510
Non-trainable params: 0
_________________________________________________________________
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
2017-11-26 01:35:25.856563: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
2017-11-26 01:35:25.856604: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
2017-11-26 01:35:25.856621: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up CPU computations.
2017-11-26 01:35:25.856629: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX2 instructions, but these are available on your machine and could speed up CPU computations.
2017-11-26 01:35:25.856638: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use FMA instructions, but these are available on your machine and could speed up CPU computations.
60000/60000 [==============================] - 14s - loss: 1.6388 - acc: 0.8431 - val_loss: 0.1992 - val_acc: 0.9450
Epoch 2/10
60000/60000 [==============================] - 8s - loss: 0.1921 - acc: 0.9482 - val_loss: 0.2155 - val_acc: 0.9468
Epoch 3/10
60000/60000 [==============================] - 10s - loss: 0.1814 - acc: 0.9529 - val_loss: 0.2108 - val_acc: 0.9477
Epoch 4/10
60000/60000 [==============================] - 12s - loss: 0.1734 - acc: 0.9567 - val_loss: 0.2226 - val_acc: 0.9507
Epoch 5/10
60000/60000 [==============================] - 14s - loss: 0.1728 - acc: 0.9584 - val_loss: 0.2229 - val_acc: 0.9516
Epoch 6/10
60000/60000 [==============================] - 24s - loss: 0.1670 - acc: 0.9608 - val_loss: 0.2424 - val_acc: 0.9566
Epoch 7/10
60000/60000 [==============================] - 17s - loss: 0.1707 - acc: 0.9609 - val_loss: 0.2184 - val_acc: 0.9565
Epoch 8/10
60000/60000 [==============================] - 13s - loss: 0.1697 - acc: 0.9626 - val_loss: 0.2641 - val_acc: 0.9514
Epoch 9/10
60000/60000 [==============================] - 7s - loss: 0.1619 - acc: 0.9637 - val_loss: 0.2136 - val_acc: 0.9631
Epoch 10/10
60000/60000 [==============================] - 13s - loss: 0.1603 - acc: 0.9644 - val_loss: 0.2962 - val_acc: 0.9505
Test loss: 0.296182284909
Test accuracy: 0.9505
```

HAPPY

# ちなみに

softmax の値が 0 になるのは、softmax に入力されるベクトルの中身に巨大な数値が含まれていて、softmax の式の分母の基準化が効きすぎるときに起こります。exp を取るので、数値の大きさが超拡大されて情報落ち[^3]が発生するからです。ただし、softmax の出力が 0 になる事自体はおかしなことではありません (そのクラスに所属する確率が 0 であるというのは有効な予測です)。問題なのは、その後に log を取っているので log(0) が評価されてしまうことです。

softmax は入力に定数を足し引きしても結果が変わらないという性質があるので、それを使って入力の最大値でスケーリングをかけることで情報落ちを回避できます。
また、cross_entropy と softmax は超簡単に言えばそれぞれ log と exp を取る演算なので、$\text{crossentropy}(\text{softmax}(x))$ を計算するのではなく、まず softmax と crossentropy の合成関数を作って等価に変形してやると一部の log と exp を打ち消すことができます。
以上の工夫をすると、 softmax の結果がほぼゼロになるときでも -inf にならずにすみます。

よって、TensorFlow や chainer では、「softmax をとってから cross_entropy を計算」するのではなく、「softmax と cross_entropy を同時に計算」する関数が用意されています。
TensorFlow なら [tf.nn.softmax_cross_entropy_with_logits](https://www.tensorflow.org/api_docs/python/tf/nn/softmax_cross_entropy_with_logits) ですし、chainer なら [chainer.functions.softmax_cross_entropy](https://docs.chainer.org/en/stable/reference/generated/chainer.functions.softmax_cross_entropy.html#chainer.functions.softmax_cross_entropy) です。

一方で、Keras では、「softmax をとってから cross_entropy を計算」します。話が違う、と思うかもしれませんが、Keras は log(0) を防ぐために softmax からの出力を $[\epsilon, 1 - \epsilon]$ の領域に寄せてから cross_entropy を計算して inf 問題を回避しています。 (https://github.com/fchollet/keras/blob/master/keras/backend/tensorflow_backend.py#L2888)

# 参考資料
- [デバッガを使ってKerasのモデルをデバッグする - 自然言語処理の深遠](http://hironsan.hatenablog.com/entry/keras-model-debug)
- [Debugging TensorFlow Programs  |  TensorFlow](https://www.tensorflow.org/programmers_guide/debugger)
    - TensorFlow 公式ドキュメント
- [Google Developers Japan: tfdbg による TensorFlow モデルのデバッグ](https://developers-jp.googleblog.com/2017/03/debug-tensorflow-models-with-tfdbg.html)
    - tfdbg を紹介する Google Developers Blog (Japan) の記事
- [tensorflow/debug_mnist.py at master · tensorflow/tensorflow](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/debug/examples/debug_mnist.py)
    - TensorFlow 公式の tfdbg のサンプル
- [keras/mnist_cnn.py at master · fchollet/keras](https://github.com/fchollet/keras/blob/master/examples/mnist_cnn.py)
    - Keras の MNIST サンプル
- [keras/tensorflow_backend.py at master · fchollet/keras](https://github.com/fchollet/keras/blob/master/keras/backend/tensorflow_backend.py#L2888)
    - Keras が catgorical_crossentropy の不安定性をどうやって解消しているかの部分

[^1]: `has_inf_or_nan` は、tfdbg を TensorFlow から直接呼んだときは最初からセットされているようですが、Keras から呼ぶとなぜかセットされていなかったので、自分で設定しています。
[^2]: 二回入力しているのは、テンソルを初期化させるためです。`run` コマンド一回が TensorFlow 側の `session.run()` 1回に対応するのですが、Keras を使っていると隠蔽しまいます。おそらく1回目の `run` ではテンソルの定義のみがおこなわれているようです。
[^3]: exp(1e-3)/exp(1e+3) とか計算したら分母がでかすぎて結果がゼロになってしまう