TensorFlow2.0の傾向と対策 〜 keras三銃士

# はじめに
TF2.0のベータ版がついに公開されました（[Announcing TensorFlow 2.0 Beta](https://medium.com/tensorflow/announcing-tensorflow-2-0-beta-abb24bbfbe3d)）。
すでにalpha版やそれ以前からTF2.0を試したりしている人もいると思います。ベータ版の公開を機に、同じコードで挙動が変わる例を紹介しようと思います。

# 問題のコード
問題のコードは次です。このコードは、**TF1系のtf.keras、TF2.0のtf.keras、生kerasの3つで挙動が異なります**。

```python
import numpy as np
import tensorflow as tf
from tensorflow import keras  # kerasと比較するときは単にimport keras


class M(keras.Model):
    def __init__(self):
        super().__init__()
        self.d = [keras.layers.Dense(1)]
        self.r = tf.random.uniform([])

    def call(self, x):
        return self.d[0](x + self.r)


def main():
    m = M()
    m.compile(keras.optimizers.SGD(), loss="mse")
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))


if __name__ == '__main__':
    main()
```

全結合層が1つと乱数が1つあるモデルです。全結合層がリストに入っていますが、これは、本来は好きな個数を入れて使いたいのを説明のため1つにしています。

3つのkerasで次の違いが出ます。

||変数の扱い|乱数の扱い|
|---|---|---|
|TF1系のkeras|訓練される|実行毎に異なる|
|TF2.0のkeras|訓練される|**固定**|
|生keras|**訓練されない**|実行毎に異なる|

端的に何が起こっているかと言うと、

* eagerモードのTF2.0では乱数を取得した時点で固定になる
* 生kerasではリストなどで層を持つと訓練対象にならない

ことが原因です。

以下でその違いを詳しく説明します。

# TF1系のkeras
乱数のシードを固定してsummaryやらpredictの値を見てみます。この場合は、あまり驚かない結果だと思います。

結果は、

* paramsが2
* 重みの値が訓練で変化
* predictの結果が実行毎に異なる

という感じになります。

## コード

``` python
import numpy as np
import tensorflow as tf
from tensorflow import keras

print(tf.__version__)
tf.random.set_random_seed(0)


class M(keras.Model):
    def __init__(self):
        super().__init__()
        self.d = [keras.layers.Dense(1)]
        self.r = tf.random.uniform([])

    def call(self, x):
        return self.d[0](x + self.r)


def main():
    m = M()
    m.compile(keras.optimizers.SGD(), loss="mse")
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))

    # trainableな変数について
    m.summary()
    s = keras.backend.get_session()
    print(s.run(m.variables))
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))
    print(s.run(m.variables))

    # 乱数について
    print(m.predict(np.zeros([1, 1])))
    print(m.predict(np.zeros([1, 1])))


if __name__ == '__main__':
    main()
```

## 結果

```
1.13.1
<<TFのwarningは省略>>
5/5 [==============================] - 1s 193ms/sample - loss: 0.6114
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense (Dense)                multiple                  2
=================================================================
Total params: 2
Trainable params: 2
Non-trainable params: 0
_________________________________________________________________
[array([[0.6851259]], dtype=float32), array([0.01563858], dtype=float32)]
5/5 [==============================] - 0s 333us/sample - loss: 0.9524
[array([[0.68536645]], dtype=float32), array([0.03515691], dtype=float32)]
[[0.3513423]]
[[0.38965154]]

```

# TF2.0のkeras
同じことをTF2.0でします。乱数の固定方法が少し変わったり、直接変数の値が見れる違いがありますが、やっていることは同じです。

結果は、

* paramsが2
* 重みの値が訓練で変化
* predictの結果が実行毎に変化しない

という感じになります。実行毎に変化させるには`call`メソッド内で乱数を作る必要があります。

## コード  
```python

import numpy as np
import tensorflow as tf
from tensorflow import keras

print(tf.__version__)
tf.random.set_seed(0)


class M(keras.Model):
    def __init__(self):
        super().__init__()
        self.d = [keras.layers.Dense(1)]
        self.r = tf.random.uniform([])

    def call(self, x):
        return self.d[0](x + self.r)


def main():
    m = M()
    m.compile(keras.optimizers.SGD(), loss="mse")
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))

    # trainableな変数について
    m.summary()
    print(m.variables)
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))
    print(m.variables)

    # 乱数について
    print(m.predict(np.zeros([1, 1])))
    print(m.predict(np.zeros([1, 1])))


if __name__ == '__main__':
    main()
```

## 結果
```
2.0.0-beta0
<<TFのwarningは省略>>
5/5 [==============================] - 1s 137ms/sample - loss: 0.8910
Model: "m"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense (Dense)                multiple                  2
=================================================================
Total params: 2
Trainable params: 2
Non-trainable params: 0
_________________________________________________________________
[<tf.Variable 'm/dense/kernel:0' shape=(1, 1) dtype=float32, numpy=array([[0.19747218]], dtype=float32)>, <tf.Variable 'm/dense/bias:0' shape=(1,) dtype=float32, numpy=array([0.01887905], dtype=float32)>]
Train on 5 samples
5/5 [==============================] - 0s 367us/sample - loss: 0.8528
[<tf.Variable 'm/dense/kernel:0' shape=(1, 1) dtype=float32, numpy=array([[0.20286475]], dtype=float32)>, <tf.Variable 'm/dense/bias:0' shape=(1,) dtype=float32, numpy=array([0.03734833], dtype=float32)>]
[[0.09657979]]
[[0.09657979]]
```

# 生keras
生kerasでも同じことをします。backendはTF1系なので、同じように使えるんだろうと思っていると痛い目を見ます。

結果は、

* **paramsが0**
* 重みの値が訓練で変化しない
* predictの結果が実行毎に変化

という感じになります。雑な使い方をすると何も訓練されないという落とし穴があります。

## コード
```python
import numpy as np
import tensorflow as tf
import keras

print(tf.__version__)
print(keras.__version__)
tf.random.set_random_seed(0)


class M(keras.Model):
    def __init__(self):
        super().__init__()
        self.d = [keras.layers.Dense(1)]
        self.r = tf.random.uniform([])

    def call(self, x):
        return self.d[0](x + self.r)


def main():
    m = M()
    m.compile(keras.optimizers.SGD(), loss="mse")
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))

    # trainableな変数について
    m.summary()
    s = keras.backend.get_session()
    print(s.run(m.d[0].weights))
    m.fit(np.zeros([5, 1]), np.ones([5, 1]))
    print(s.run(m.d[0].weights))

    # 乱数について
    print(m.predict(np.zeros([1, 1])))
    print(m.predict(np.zeros([1, 1])))


if __name__ == '__main__':
    main()
```

## 結果
```
Using TensorFlow backend.
1.13.1
2.2.4
<<TFのwarningは省略>>
5/5 [==============================] - 1s 152ms/step - loss: 0.4004
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
Total params: 0
Trainable params: 0
Non-trainable params: 0
_________________________________________________________________
[array([[1.1453341]], dtype=float32), array([0.], dtype=float32)]
Epoch 1/1
5/5 [==============================] - 0s 189us/step - loss: 0.9720
[array([[1.1453341]], dtype=float32), array([0.], dtype=float32)]
[[0.5283858]]
[[0.59240544]]
```

# いかがでしたでしょうか
今回、どのkerasを使うかによって挙動が変わってくることが分かりました。そもそも層をリストで持つこととeagerの相性まで調べてないので、TF2.0ではこんなコードを書かないってなる可能性もあります。

とはいえ、仕様が少しずつ違うkerasの亜種が増えて心配です。生kerasがTF2.0のkerasに合わせていくという流れになるのか、別物として開発するのか、それとも別のライブラリが台頭するのでしょうか。