Ahogrammer

Deep Dive Into NLP, ML and Cloud

デバッガを使ってKerasのモデルをデバッグする

3行まとめ

  • Keras で作成したモデルをデバッグしたい。
  • Keras には標準でデバッガが用意されていない。
  • Keras の Session オブジェクトを tfdbg でラップしてデバッグする。

オープンソースニューラルネットワークライブラリである Keras は計算グラフに基づいています。 一般的な Keras のプログラムは以下の段階から構成されています。

  1. Sequentialモデルか Functional API を用いて、モデルを構築する
  2. 計算グラフを実行し、学習や予測を行う

ここで問題となるのが、2番目の計算グラフの実行中にエラーやバグが発生した場合、デバッグが難しいことです。 なぜなら、内部構造や内部状態が外から見えないため、どのような状況で計算に失敗したのかわからないからです。

Kerasには今のところdebuggerは存在しませんが、TensorFlowには tfdbg と呼ばれるdebuggerが用意されています。 tfdbgを使うことで、学習中や予測中の内部構造や内部状態を明らかにすることができます。

本記事では tfdbg をKerasで使ってデバッグを行う方法を紹介します。 tfdbgを使うため、KerasのバックエンドにはTensorFlowを想定しています。 では、短いサンプルコードを通してデバッガの動作を見てみます。 以下のコードは、簡単な評判分析のモデルを Sequentialモデルで定義し、学習します。

import keras.backend as K
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Dense, Embedding, LSTM
from keras.datasets import imdb
from tensorflow.python import debug as tf_debug

sess = K.get_session()
sess = tf_debug.LocalCLIDebugWrapperSession(sess)
K.set_session(sess)

max_features = 20000
maxlen = 80
batch_size = 32

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)

model = Sequential()
model.add(Embedding(max_features, 128))
model.add(LSTM(128, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer='adam', metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=batch_size,
          epochs=15, validation_data=(x_test, y_test))
score, acc = model.evaluate(x_test, y_test,
                            batch_size=batch_size)

この例でわかるように、Keras の Session オブジェクトをデバッグ用のクラス(LocalCLIDebugWrapperSession)でラップしています。 これにより、計算グラフを実行した際に、 tfdbg インターフェースが起動します。この インターフェース内でマウスクリックやコマンドを使うと、その後のコードの実行や、グラフのノードや属性の調査をできます。詳しい使い方については、tfdbgのドキュメントを見てください。

f:id:Hironsan:20170915085618p:plain

まとめ

  • Kerasで作成したモデルをデバッグしたい。
  • Kerasには標準でデバッガが用意されていない。
  • Kerasのsessionオブジェクトをtfdbgでラップしてデバッグする。

参考資料

TensorFlow のデバッガ tfdbg に関する公式ドキュメント
Debugging TensorFlow Programs

Google Developers blog による tfdbg の解説
Debug TensorFlow Models with tfdbg

Keras backends の公式ドキュメント
Keras backends

Kerasでカスタムレイヤーを作成する方法

Kerasでは様々なレイヤーが事前定義されており、それらをレゴブロックのように組み合わせてモデルを作成していきます。 たとえば、EmbeddingやConvolution, LSTMといったレイヤーが事前定義されています。 通常は、これらの事前定義された便利なレイヤーを使ってモデルを作成します。

しかし、事前定義されたレイヤーだけでは必要な機能を実装できない時があります。 たとえば、最新の論文を実装しようとしたときに、その中で使われている機能がKerasに存在しないということはよくあります。

必要なレイヤーがKerasに存在しない場合、自分でカスタムレイヤーを実装する必要があります。 Kerasは自分でカスタムレイヤーを実装するための機能を提供してくれています。 基本的に重みを持たないレイヤーに関してはLambdaで定義し、学習可能な重みを持たせたい場合はカスタムレイヤーの実装を検討することになります。

本記事では、Kerasでのカスタムレイヤーの作成方法を紹介します。

カスタムレイヤーの作り方

カスタムレイヤーを作成するためには、keras.engine.topologyモジュールのLayerクラスを継承したクラスを作る必要があります。 具体的には、Layerで定義されているメソッドをオーバーライドして、必要なレイヤーを作成します。

その際に重要なメソッドは以下の4つです:

  • __init__
  • build
  • call
  • compute_output_shape

以下では各メソッドの役割について説明していきます。

__init__()

初期化メソッドでは、レイヤーを初期化します。 具体的には、必要な機能を実装する際に必要な情報を渡しておきます。 たとえば、重みの初期化方法とか内部のユニット数とかそんな情報を渡します。

build(self, input_shape)

buildでは学習可能な重みを定義します。 具体的には、self.add_weightsメソッドにshapeや初期化方法、正規化方法を渡して重みを定義します。 ここで使用する初期化方法や正規化方法は__init__()メソッドに渡しておいたものを使用します。

また、buildの最後には必ず、self.build = Trueと設定しなければなりません。 self.buildに直接設定してもいいのですが、super([Layer], self).build()を呼び出してもできます。

call(self, x, mask=none)

callメソッドにはForwardの計算で行いたいことを実装します。 callメソッドの中では、__init__で設定した情報、buildで定義した重み、そしてレイヤーへの入力を使って、自分の行いたい計算をします。 カスタムレイヤーのメソッドの中でも最も重要なメソッドの一つです。

compute_output_shape(self, input_shape)

このメソッドは名前の通り出力のshapeを計算します。 作成したレイヤーの内部で入力のshapeを変更する場合には,ここでshape変換のロジックを指定します。 変更しない場合は定義する必要はありません。入力のshapeがそのまま返されます。

カスタムレイヤの保存と読み込み

Kerasではモデルを保存する際に、レイヤーの情報をjsonに変換します。 その際、レイヤーのget_configメソッドが重要な役割を果たします。 get_configメソッド内に、どの情報をjson化するのか記述していきます。 このjsonには、カスタムレイヤーを復元するのに必要な情報を含める必要があります。

カスタムレイヤーの読み込みについては公式のFAQに方法が示されています。
Handling custom layers (or other custom objects) in saved models

簡単に述べておくと、load_model関数の引数であるcustom_objectsに定義したカスタムレイヤーを渡しましょうという話です。 以下のような感じで渡すと、保存したモデルを正常に読み込むことができます。

from keras.models import load_model
# AttentionLayer というカスタムレイヤーを定義したと仮定
model = load_model('my_model.h5', custom_objects={'AttentionLayer': AttentionLayer})

各メソッドの呼ばれる順番

ここでは各メソッドが呼ばれる順番について書いておきます。 呼ばれる順番について知らなくてもレイヤーを定義することはできますが、知っているとデバッグするときに役立つかもしれません。

まず、レイヤーをインスタンス化する際に __init__ が呼ばれます。これは問題ないでしょう。 レイヤーをインスタンス化したら、レイヤーに入力を与えて呼び出します。 一応書いておくと、これは以下のようなイメージです:

x = Input(batch_shape=(None, maxlen), dtype="int32")
embedding = Embedding(vocab_size, dim)(x)

上記のEmbeddingレイヤーのように、レイヤーを呼び出した際に実行されるのが、__call__メソッドです。 __call__ の中では以下の順序でメソッドが呼ばれます:

  • build
  • call
  • compute_mask
  • compute_output_shape

ちなみに、compute_maskではマスキングの計算を行います。

実際のコード

カスタムレイヤーの公式Exampleとして、以下のファイルが挙げられます。
keras/antirectifier.py at master · fchollet/keras · GitHub

上記のExampleで基本的なところを理解したら、Kerasに事前定義されているレイヤーを見るのが一番勉強になるかなと感じています。

ちなみに、私もカスタムレイヤーを定義してみました。Keras用のCRF層です。
keras-crf-layer

参考資料

カスタムレイヤーの作り方に関する公式ドキュメント
Writing your own Keras layers

カスタムレイヤーの公式Example
keras/antirectifier.py at master · fchollet/keras · GitHub

カスタムレイヤーの作成方法のチュートリアル
For beginners; Writing a custom Keras layer

カスタムレイヤーでシリアライズを行う話
kerasでカスタムレイヤーのシリアライズを行う

カスタムレイヤーの読み込み方法
Handling custom layers (or other custom objects) in saved models

KerasのLambda層でreshapeしたとき、保存に失敗する(場合がある)話

TL;DR

  • keras.backendのreshapeを使ってLambda層でreshapeしたい
  • reshapeのshapeにテンソルを指定するとモデルの保存(save)に失敗する
  • saveではなくsave_weightsを使うと保存できる

背景

まず問題が起きる状況について説明しておきたい。 簡単にまとめると以下のような状況だ。

  • ミニバッチごとにshapeの異なるデータを入力する
  • モデルの途中にreshapeが入っている
  • reshapeには入力のshapeを使用する

ミニバッチごとにshapeの異なるデータを入力するというのは、言語処理なんかではよく行われると思う。 たとえば、LSTMでの計算を効率化するために、ミニバッチ内の最大の系列長に合わせてpaddingを行い、系列長を揃える場合が挙げられる。

上記の状況を示すプログラムは以下の通り:

import keras.backend as K
from keras.layers import Input, Lambda

emb_size = 25
x = Input(batch_shape=(None, None, None, emb_size))
s = K.shape(x)
pred = Lambda(K.reshape, 
              arguments={'shape': (-1, s[-2], emb_size)})(x)
model = Model(inputs=[x], outputs=pred)
model.compile('sgd', 'categorical_crossentropy')
model.save('model.h5')

実際にはこんなモデルはありえないが、説明のためのtoy programだと了承していただきたい。

コードについて簡単に説明しておく。 まず、入力のうち3つの次元は動的に決まる。このうち一つはバッチサイズである。 この入力に対して、keras.backendのshapeを使ってshapeを取得している。ちなみに取得したshapeはTensorで表される:

>>> s = K.shape(x)
>>> s
<tf.Tensor 'Shape:0' shape=(4,) dtype=int32>

次に、取得したshapeとbackendのreshapeを使って入力を変換している。 Reshape層を使わないのは、Reshape層のshapeにTensorを指定できないからである。 Tensorを指定すると「Tensorはboolみたいには使用できないよ」という例外が出る:

>>> s = K.shape(x)
>>> Reshape(target_shape=(-1, s[-2], emb_size))(x)
Traceback (most recent call last):
...
    raise TypeError("Using a `tf.Tensor` as a Python `bool` is not allowed. "
TypeError: Using a `tf.Tensor` as a Python `bool` is not allowed. Use `if t is not None:` instead of `if t:` to test if a tensor is defined, and use TensorFlow ops such as tf.cond to execute subgraphs conditioned on the value of a tensor.

それならば、K.int_shapeで得られるTensorではないshapeを指定すればよいではないか?という話になるがそれもできない。 というのも、K.int_shapeで得られたshapeのs[-2]はNoneであり、ReshapeにはNoneを指定できないからである。 そのときに発生する例外は以下の通り:

>>> s = K.int_shape(x)
>>> Reshape(target_shape=(-1, s[-2], emb_size))(x)
Traceback (most recent call last):
...
TypeError: unorderable types: NoneType() < int()

したがって、keras.backendのreshapeを使うことになる。 ただ、Kerasの場合、レイヤーを接続してモデルを構築しないと「keras_historyがないよ」という例外が発生する。 したがって、Lambdaでreshapeをラップしている。

pred = Lambda(K.reshape, 
              arguments={'shape': (-1, s[-2], emb_size)})(x)

このtoy programではcompileはうまくいく。また学習も順調に進む。

model = Model(inputs=[x], outputs=pred)
model.compile('sgd', 'categorical_crossentropy')

問題

しかし以下のようにsaveメソッドを使ってモデルを保存すると問題が起きる。

model.save('model.h5')

確認しているだけでも以下の5つの例外がランダム(に見えるよう)に発生する:

  • TypeError: cannot serialize ‘_io.TextIOWrapper’ object
  • TypeError: object.new(PyCapsule) is not safe, use PyCapsule.new()
  • AttributeError: ‘NoneType’ object has no attribute ‘update’
  • TypeError: cannot deepcopy this pattern object
  • TypeError: can’t pickle module objects

調べていくと、これらの例外はreshape時にテンソルを指定すると、Lambdaでシリアライズができないことに起因するようだ。

解決策

save_weightsでネットワークの重みだけ保存することで解決する。

model.save_weights('model_weights.h5')

まとめ

  • keras.backendのreshapeを使ってLambda層でreshapeしたい
  • reshapeのshapeにテンソルを指定するとモデルの保存(save)に失敗する
  • saveではなくsave_weightsを使うと保存できる