過渡期にあるTensorFlowとKerasの関係を調べる

（ [変数名コントロールの違い](http://qiita.com/TomokIshii/items/178938b6db1edc16b94e#%E8%BF%BD%E8%A8%98%E5%A4%89%E6%95%B0%E5%90%8D%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E9%81%95%E3%81%84tfcontribkeras-vs-tflayers) について追記しました．）

## はじめに

TensorFlow Dev Summitなどで情報がリリースされていますが，TensorFlowとKerasの統合が進められています．Keras Blog - [Introducing Keras 2](https://blog.keras.io/introducing-keras-2.html) から引用します．

> Keras is best understood as an API specification, not as a specific codebase. In fact, going fowards there will be two separate implementations of the Keras spec: the internal TensorFlow one, available as tf.keras, written in pure TensorFlow and deeply compatible with all TensorFlow functionality, and the external multi-backend one supporting both Theano and TensorFlow (and likely even more backends in the future).

Kerasは，2つのインプリメンテーションに分かれ，一つがTensorFlowの統合に向うもの，もう一つが独立パッケージのままmulti-backend(Theno, TensorFlow, etc.)をサポートするもの，となる予定になっています．私も以前は，Theano/TensorFlowと適宜，切り換えながら使っていましたが，最近は，TensorFlow backend一択となっています．ですので，2つのKerasパッケージの内，私個人は，TensorFlow統合版の方に関心を強く持っています．

先日のDev Summitでは，以下のスケジュールが発表されました．（YouTubeより抜粋）

![Keras-take-aways.png](https://qiita-image-store.s3.amazonaws.com/0/74152/ac1f43f0-00e5-b252-e024-d73bb07b7467.png)


上記の通り，統合について，"tf.contrib.keras"，"tf.keras" とステップを踏む予定のようです．
今回 TensorFlow 1.1 (1.1.0-rc1) がリリースされましたので，早速これをインストールし，内容を調べてみたいと思います．

（プログラミング環境は，Python 3.5.2, TensorFlow 1.1.0-rc1, Keras 2.0.2になります．）

## Keras 2 （multi-backend版）もリリースされてますが
すでに Keras 2 もリリースされており，Qiitaにも紹介記事が出ています．（拝見させていただきました．）私も TensorFlow 1.0 + Keras 2.0 の環境で使ってみました．Keras 1.0の最終版(1.2.2?) と大きくAPIは変わっていませんが，詳細なところでいくつかの関数のkey word（引数，オプション等）に変更がありました．MNIST分類のサンプルコードは，以下のようになります．

```py
# Keras 2.0 + TensorFlow 1.0 backend
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.optimizers import Adagrad
from keras.utils import np_utils

（中略）

model = Sequential()
model.add(Dense(512, input_dim=784))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(10))
model.add(Activation('softmax'))

（後略）
```

かなり省略しましたが，モジュールのインポート・ポイント(from keras.xxx) に着目下さい．


## tf.contrib.keras (in TensorFlow 1.1) を試す
さて，今回リリースされた **tf.contrib.keras** を試してみます．上記コード(TF 1.0 + Keras 2.0)から変更を加えるやり方でコーディングを行いました．

```py
import numpy as np

from tensorflow.contrib.keras.python.keras.datasets import mnist
from tensorflow.contrib.keras.python.keras.models import Sequential
from tensorflow.contrib.keras.python.keras.layers import Dense
from tensorflow.contrib.keras.python.keras.layers import Dropout, Activation
from tensorflow.contrib.keras.python.keras.optimizers import Adagrad
from tensorflow.contrib.keras.python.keras.utils import np_utils
from tensorflow.contrib.keras.python.keras import backend as K

def load_data(nb_classes=10):
    # the data, shuffled and split between tran and test sets
    (X_train, y_train), (X_test, y_test) = mnist.load_data()
    X_train = X_train.reshape(60000, 784)
    X_test = X_test.reshape(10000, 784)
    X_train = X_train.astype('float32')
    X_test = X_test.astype('float32')
    X_train /= 255
    X_test /= 255
    print(X_train.shape[0], 'train samples')
    print(X_test.shape[0], 'test samples')

    # convert class vectors to binary class matrices
    y_train = np_utils.to_categorical(y_train, nb_classes)
    y_test = np_utils.to_categorical(y_test, nb_classes)

    return X_train, y_train, X_test, y_test

def mk_model():
    model = Sequential()
    model.add(Dense(512, input_dim=784))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(10))
    model.add(Activation('softmax'))

    return model
```

コードの前半部になりますが，（あたりまえですが）モジュールのインポート・ポイントが変わります．(ディレクトリ深いです．) model作成部ですが，（関数"mk_model"に入れましたが）内容の変更は全くありません．Keras 2.0 API と（tf.contrib.kerasは）互換性 100％のように見えます．

また，コード後半部（以下）も大きな変更なしでした．

```py
if __name__ == '__main__':
    np.random.seed(1337)        # for reproducibility
    batch_size = 128
    nb_epoch = 20

    X_train, y_train, X_test, y_test = load_data()
    model = mk_model()
    model.summary()             # check model configuration

    model.compile(loss='categorical_crossentropy',
                optimizer=Adagrad(),
                metrics=['accuracy'])
    
    model.fit(X_train, y_train,
          batch_size=batch_size, epochs=nb_epoch,
          verbose=1, 
          validation_data=(X_test, y_test))

    score = model.evaluate(X_test, y_test, verbose=0)
    print('\nTest score   : {:>.4f}'.format(score[0]))
    print('Test accuracy: {:>.4f}'.format(score[1]))

    K.clear_session()
    # This statement is fixed the condition of ...
    # Exception ignored in: <bound method BaseSession.__del__ of 
    # <tensorflow.python.client.session.Session object at 0x7fb79a3fa550>>
    # ...
    # AttributeError: 'NoneType' object has no attribute 'TF_NewStatus'
    #
    # TensorFlow issue: Exception ignored in BaseSession.__del__ #3388

```

機能的に問題なさそうですので，（特にKerasパッケージをディスクから消してはいませんが）今後は，Kerasパッケージをインストールしなくても，TensorFlowをインストールするだけで，Keras APIが使えそうです．（以前，Keras 1.x.x の頃は，KerasとTensorFlowのバージョン同期に気を配る必要がありましたが...） 

但し，プログラムの終了処理時に Error が発生して，少し調べることになりました．計算機リソースの開放に関わる処理で問題が生じることがあるようです．（再現性は不明ですが，私の環境ではほとんどの確率でErrorが発生しました．）

上記の通り， `K.clear_session()` のステートメントを入れることで対策となっています．
（このError, Bugの原因について詳細理解しきれていません．気になる方は関連サイトを参照ください．）

## KerasをLayerクラス・ライブラリとして使う

これだけでは，Keras そのものなので，Kerasの`Model` の枠組みを離れ，Layerクラス・ライブラリとして使ってみます．この使い方は，以前からもサポートされていて特に新しいものではないのですが，ライブラリの最新版(tf.contrib.keras)で試してみます．
（ネタ元は，Keras Blog - [Keras as a simplified interface to TensorFlow: tutorial](https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html) になります．）

```py
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

from tensorflow.contrib.keras.python import keras
from tensorflow.contrib.keras.python.keras import backend as K

def load_data():
    dirn = '../MNIST_data'
    mnist = input_data.read_data_sets(dirn, one_hot=True)

    print(mnist.train.num_examples, 'train samples')
    print(mnist.test.num_examples, 'test samples')
    print(mnist.validation.num_examples, 'validation samples (not used)')

    return mnist

def mlp_model(input):
    # MLP network model
    with tf.variable_scope('mlp_model'):
        x = keras.layers.Dense(units=512, activation='relu')(input)
        x = keras.layers.Dropout(0.2)(x)
        x = keras.layers.Dense(units=512, activation='relu')(x)
        x = keras.layers.Dropout(0.2)(x)
        y_pred = keras.layers.Dense(units=10, activation='softmax')(x)

    return y_pred

if __name__ == '__main__':
    mnist = load_data()
    # tensorflow placeholders
    x = tf.placeholder(tf.float32, [None, 784])
    y_ = tf.placeholder(tf.float32, [None, 10])
    # define TF graph
    y_pred = mlp_model(x)
    loss = tf.losses.softmax_cross_entropy(y_, y_pred)
    train_step = tf.train.AdagradOptimizer(0.05).minimize(loss)
    correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    init = tf.global_variables_initializer()

    with tf.Session() as sess:
        sess.run(init)
        print('Training...')
        for i in range(10001):
            batch_xs, batch_ys = mnist.train.next_batch(100)
            train_fd = {x: batch_xs, y_: batch_ys, K.learning_phase(): 1}
            train_step.run(feed_dict=train_fd)
            if i % 1000 == 0:
                batch_xv, batch_yv = mnist.test.next_batch(200)
                val_accuracy = accuracy.eval(
                    {x: batch_xv, y_: batch_yv, K.learning_phase(): 0})
                print('  step, accurary = %6d: %6.3f' % (i, val_accuracy))
    
        test_fd = {x: mnist.test.images, y_: mnist.test.labels, 
                    K.learning_phase(): 0}
        test_accuracy = accuracy.eval(feed_dict=test_fd)
        print('Test accuracy:', test_accuracy)
```

MNISTのMLP(Multi-layer Perceptron)モデルによる分類コードを「すっきり」と書くことができました．（HighLevel APIを使わない「素」のTensorFlowコードと比較して「すっきり」の意味です．）

KerasをLayerクラス・ライブラリとして使う利点として，ディフォルト値を「適切に」設定してくれることがあります．この点は，TensorFlow Dev Summitでも "an accessible high-level API with good defaults"とプレゼンで強調されていました．上記コード中，フル結合層 Dense() で変数イニシャライザを細かく設定していませんが，Weightに対しては， 'glorot_uniform'(Xavier uniform)のイニシャライザが適用され，Biasに対しては，'zeros'イニシャライザが適用されます．（最近のNeural Networkサンプルコードでは，このパラメータ初期化方法が多く使われているようです．）
（K.learning_phase() のfeed_dict設定は，Dropoutの動作をコントロールしています．詳細は，Keras Blog "Keras as a simplified ..." を参照下さい．）

一応，Tensor変数の使い方が気になるので，名前（変数名）を確認しておきます．

```text
#    vars = tf.global_variables()
#    print('variables:')
#    for v in vars:
#        print(v)

variables:
<tf.Variable 'mlp_model/dense_1/kernel:0' shape=(784, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_1/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_2/kernel:0' shape=(512, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_2/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_3/kernel:0' shape=(512, 10) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_3/bias:0' shape=(10,) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_1/kernel/Adagrad:0' shape=(784, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_1/bias/Adagrad:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_2/kernel/Adagrad:0' shape=(512, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_2/bias/Adagrad:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_3/kernel/Adagrad:0' shape=(512, 10) dtype=float32_ref>
<tf.Variable 'mlp_model/dense_3/bias/Adagrad:0' shape=(10,) dtype=float32_ref>
```

上記，12の tf.Variable の内，最初の6つが，Dense layerのweight(kernel)とbiasで，残り6つがオプティマイザの派生変数になります．ルートの 'mlp_model' は，コード内で自分でつけましたが，'dense_1/kernel', 'dense_1/bias',... の名前は，"tf.contrib.keras" が自動的に名前をつけています．このTensor変数名を自分で決めたい，と思ってドキュメントを調べてみましたが，現状は，ユーザによる名前付けをサポートしていないようです．（できるだけ，細かいところを隠蔽して使いやすくする，というコンセプトからは外れた使い方なのかも知れません．）

## Tensor変数を「名前&変数スコープ」でアクセスしたい場合は
重み共有などを目的に，Tensor変数名にアクセスしたい，変数名を自分で決めたい場合は，Keras APIから離れる方が懸命と思われます．

```py
import numpy as np
import tensorflow as tf
from tensorflow.python.layers import layers
from tensorflow.examples.tutorials.mnist import input_data
from sklearn.metrics import confusion_matrix

# Create n.n. model
def nn_model(images, drop_rate, vs, reuse=False):
    with tf.variable_scope(vs, reuse=reuse):
        net = tf.layers.dense(images, 512, activation=tf.nn.relu, name='dense1')
        net = tf.layers.dropout(net, rate=drop_rate)
        net = tf.layers.dense(net, 512, activation=tf.nn.relu, name='dense2')
        net = tf.layers.dropout(net, rate=drop_rate)
        net = tf.layers.dense(net, 10, activation=None, name='dense3')
   
    return net

x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])
keep_prob = tf.placeholder(tf.float32)

drop_rate = 1 - keep_prob
mlp1_pred = nn_model(x, drop_rate, 'mlp1')
mlp2_pred = nn_model(x, drop_rate, 'mlp1', reuse=True) 

loss = tf.losses.softmax_cross_entropy(y_, mlp1_pred)
train_step = tf.train.AdagradOptimizer(0.05).minimize(loss)
correct_prediction = tf.equal(tf.argmax(mlp1_pred, 1), tf.argmax(y_, 1))
accuracy1 = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

```

上記は，TensorFlow 1.0 以降でサポートされている "tf.layers" を使ったコーディングになります．ここでは，変数名，変数スコープは自分でコントロールできますし，共有変数設定も `reuse` フラグで自由自在です．

（参考，Qiitaの前記事）

- [TensorFlow の名前空間を理解して共有変数を使いこなす](http://qiita.com/TomokIshii/items/ffe999b3e1a506c396c8)
- [TensorFlowの増大するAPIについて少し考えてみた](http://qiita.com/TomokIshii/items/554b0bd5f328b1bd5210)

ところで，上記，"tf.layers" ベースのコードを見て何か気が付かないでしょうか？

そうです，"tf.layers" のAPIと，"tf.contrib.keras" のAPIがとても類似性が高いものとなっています．

- 全結合層の名前が ("fully_connected"などではなく) "dense" である．
- Layerコンストラクタのオプションとして，[kernel_initializer, bias_initializer, kernel_regularizer, bias_regularizer] 等をとる．
- Dropoutのパラメータが，残す方の割合ではなく，無効化する方の割合を指定する．

他にもありそうです．

私の前記事「TensorFlowの増大するAPI...」で，"tf.layers" に対し「APIの整理，整頓が進んでいない」とネガティブな書き方をしてしまいましたが，実際は，Keras APIとの統合が検討されていたのでした．Keras 2.0にでkey word変更されたところも，"tf.layers"を考慮した上での変更と想像されます（自分の思慮のいたらなさに反省です．）

## 最後に
いろいろ調べていくと，今後のTensorFlow, Kerasの統合，特に次のバージョン，TensorFlow 1.2 に期待がかかります．（May-2017あたりでしょうか？）
どのようなAPIになるか，現時点ではあまり情報ありませんが，今のKeras 2の独立パッケージ版と "tf.contrib.keras" との関係を考えると，APIの継続性という点に関しては，大きな問題は発生しないと予想しています．また今しばらくは，TensorFlowの高機能化と "tf.keras" による使い勝手を期待しつつ，現バージョンの理解を深めていきたいと思います．

## （追記）変数名コントロールの違い，"tf.contrib.keras" vs. "tf.layers"
上のtf.conrib.kerasを紹介するところで，「現状は，ユーザによる名前付けをサポートしていないようです．」と書きましたが，これは誤りでした．Layer定義のところで，"name" オプションを指定することができるようです．（ドキュメントには説明ありませんが，github内のテストコードで"name"を使っている箇所がありました．）但し，変数を"reuse"指定で，再利用することはでないようです．以下のコードを参照下さい．

```py
import numpy as np
import tensorflow as tf

from tensorflow.contrib.keras.python import keras
from tensorflow.contrib.keras.python.keras import backend as K
from tensorflow.python.layers import layers

def mlp_model_keras1(input):
    # MLP network model
    with tf.variable_scope('mlp_model'):
        x = keras.layers.Dense(units=512, activation='relu', name='my_dense1')(input)
        x = keras.layers.Dense(units=512, activation='relu', name='my_dense2')(x)
        y_pred = keras.layers.Dense(units=10, activation='softmax', name='my_softmax')(x)

    return y_pred

def mlp_model_keras2(input):
    # MLP network model
    with tf.variable_scope('mlp_model', reuse=True):
        x = keras.layers.Dense(units=512, activation='relu', name='my_dense1')(input)
        x = keras.layers.Dense(units=512, activation='relu', name='my_dense2')(x)
        y_pred = keras.layers.Dense(units=10, activation='softmax', name='my_softmax')(x)

    return y_pred

# Create the model
def mlp_model_by_layers1(input):
    with tf.variable_scope('mlp_by_tflayers'):
        net = tf.layers.dense(input, 512, activation=tf.nn.relu, name='his_dense1')
        net = tf.layers.dense(net, 512, activation=tf.nn.relu, name='his_dense2')
        net = tf.layers.dense(net, 10, activation=None, name='his_dense3')
   
    return net

def mlp_model_by_layers2(input):
    with tf.variable_scope('mlp_by_tflayers', reuse=True):
        net = tf.layers.dense(input, 512, activation=tf.nn.relu, name='his_dense1')
        net = tf.layers.dense(net, 512, activation=tf.nn.relu, name='his_dense2')
        net = tf.layers.dense(net, 10, activation=None, name='his_dense3')
   
    return net

if __name__ == '__main__':
    fake_data = np.ones([10, 784], dtype=np.float32) * 0.5
    x = tf.placeholder(tf.float32, [None, 784])

    # define TF graph
    y_pred1 = mlp_model_keras1(x)
    y_pred2 = mlp_model_keras2(x)
    y_pred3 = mlp_model_by_layers1(x)
    y_pred4 = mlp_model_by_layers2(x)
    
    init = tf.global_variables_initializer()

    with tf.Session() as sess:
        sess.run(init)

    vars = tf.global_variables()
    print('variables:')
    for v in vars:
        print(v)
'''
variables:
# 最初の関数 "mlp_model_keras1" で定義された変数
<tf.Variable 'mlp_model/my_dense1/kernel:0' shape=(784, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/my_dense1/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/my_dense2/kernel:0' shape=(512, 512) dtype=float32_ref>
<tf.Variable 'mlp_model/my_dense2/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model/my_softmax/kernel:0' shape=(512, 10) dtype=float32_ref>
<tf.Variable 'mlp_model/my_softmax/bias:0' shape=(10,) dtype=float32_ref>

# 2番めの関数 "mlp_model_keras2"で定義された変数
<tf.Variable 'mlp_model_1/my_dense1/kernel:0' shape=(784, 512) dtype=float32_ref>
<tf.Variable 'mlp_model_1/my_dense1/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model_1/my_dense2/kernel:0' shape=(512, 512) dtype=float32_ref>
<tf.Variable 'mlp_model_1/my_dense2/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_model_1/my_softmax/kernel:0' shape=(512, 10) dtype=float32_ref>
<tf.Variable 'mlp_model_1/my_softmax/bias:0' shape=(10,) dtype=float32_ref>

# 3番目の関数 "mlp_model_by_layers1" で定義された変数
<tf.Variable 'mlp_by_tflayers/his_dense1/kernel:0' shape=(784, 512) dtype=float32_ref>
<tf.Variable 'mlp_by_tflayers/his_dense1/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_by_tflayers/his_dense2/kernel:0' shape=(512, 512) dtype=float32_ref>
<tf.Variable 'mlp_by_tflayers/his_dense2/bias:0' shape=(512,) dtype=float32_ref>
<tf.Variable 'mlp_by_tflayers/his_dense3/kernel:0' shape=(512, 10) dtype=float32_ref>
<tf.Variable 'mlp_by_tflayers/his_dense3/bias:0' shape=(10,) dtype=float32_ref>

# 4番めの関数 "mlp_model_by_layers2" で定義された変数は，（別なものとしては）見当たらない．
'''
```

4つのモデルの内，最初の2つが tf.contrib.keras を使ったものになります．"keras.layers.Dense"内で"name"を指定したところ，その名前が変数名に反映されています．2つめのモデルで，同じ変数スコープ（同じ変数名）を設定し，tf.variable_scope に `reuse=True` を付けましたがこれは無視され，変数スコープ名を'mlp_model' から 'mlp_model_1' に自動的に変換して変数名がつけられました．

一方，同じことを tf.layers モデルの3番目のもの，4番めのものに行った結果は，スコープ名の自動変更は発生せず，上記コードのコメント部の通りの結果となっています．（3番めの関数で定義したもののみ変数が確保され，4番めの関数については新たに変数は確保されていません．きちんと中身を確認してはいませんが，思惑通り，変数の再利用，共有変数が設定されたと考えられます．

tf.contrib.keras のライブラリと tf.layers を一緒に使う人はいないと思いますが，挙動が異なってきますので，**「混ぜるな危険」**ということになるかと思います．（今後のバージョンで，上でみた仕様が変わる可能性はあるかと思います．）

## 参考文献，web site
- Keras, Documentation
- Integrating Keras & TensorFlow: The Keras workflow, expanded (TensorFlow Dev Summit 2017)  
https://youtu.be/UeheTiBJ0Io
- Introduction Keras 2 - The Keras Blog  
https://blog.keras.io/introducing-keras-2.html
- Keras as a simplified interface to TensorFlow: tutorial - The Keras Blog
https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html
- TensorFlow, github (tf.contrib.kerasのドキュメントは，ほとんど見つかりませんでした．→ 本記事執筆中に TensorFlowドキュメント API 1.1 がリリースされました．)
https://www.tensorflow.org/versions/r1.1/api_docs/python/tf/contrib/keras
- TensorFlow, issue #3388 - Bug: Exception ignored in `BaseSession.__del__`
https://github.com/tensorflow/tensorflow/issues/3388
- Kerasが2.0にアップデートされました。 - Qiita
http://qiita.com/cvusk/items/aa6270301ff2d14fb989
- TensorFlowのHighLevelAPIについての雑感つき解説 - Qiita
http://qiita.com/rindai87/items/72651c702e9265595047`
- TensorFlow の名前空間を理解して共有変数を使いこなす - Qiita
http://qiita.com/TomokIshii/items/ffe999b3e1a506c396c8
- TensorFlowの増大するAPIについて少し考えてみた - Qiita
http://qiita.com/TomokIshii/items/554b0bd5f328b1bd5210