Python
Keras
TensorFlow

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

More than 1 year has passed since last update.

変数名コントロールの違い について追記しました.)

はじめに

TensorFlow Dev Summitなどで情報がリリースされていますが,TensorFlowとKerasの統合が進められています.Keras Blog - Introducing Keras 2 から引用します.

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

上記の通り,統合について,"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分類のサンプルコードは,以下のようになります.

# 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)から変更を加えるやり方でコーディングを行いました.

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%のように見えます.

また,コード後半部(以下)も大きな変更なしでした.

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 になります.)

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変数の使い方が気になるので,名前(変数名)を確認しておきます.

#    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から離れる方が懸命と思われます.

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の前記事)

ところで,上記,"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"指定で,再利用することはでないようです.以下のコードを参照下さい.

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