Edited at

Kerasのバックエンドを使った層を追加する

More than 1 year has passed since last update.

Kerasのバックエンドを使って直接テンソル計算等を行う際に学んだことをまとめます。


Kerasのバックエンド

Kerasでネットワークを構築する際は、Layerに定義された層を使っていくことがほとんどだと思いますが、特殊なLayerは関数を作ってLambda層やMerge層に突っ込むことが必要になります。

例えば以下は入力の絶対値を返す層を持ったモデルを作成しています。


lambda_layer_exp.py

from keras.models import Model

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

x_in = Input(shape=(3, 3))
x = Lambda(lambda x: K.abs(x))(x_in)
model = Model(input=x_in, output=x)


このモデルに値を入れてみます。

>>> import numpy as np

>>> model.predict([np.array([[[-1,2,3],[4,-5,6],[7,8,-9]]])])
array([[[ 1., 2., 3.],
[ 4., 5., 6.],
[ 7., 8., 9.]]], dtype=float32)

入力が2つ以上ある場合はMerge層で対応します。

以下の例では2つの入力の絶対値の和を取る層を追加しています。

Merge層で関数を使う場合はoutput_shapeを明示する必要があります。


merge_layer_exp.py

from keras.models import Model

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

x_in1 = Input(shape=(3,))
x_in2 = Input(shape=(3,))
x = merge([x_in1, x_in2], mode=lambda x: K.abs(x[0]) + K.abs(x[1]), output_shape=(3,))
model = Model(input=[x_in1, x_in2], output=x)


このモデルで計算を実行すると以下のようになります。

>>> import numpy as np

>>> model.predict([np.array([[-1,-2,3]]), np.array([[4,-5,-6]])])
array([[ 5., 7., 9.]], dtype=float32)

バックエンドに存在する関数はnumpyやTensorflow,Theanoなどで使われているものとほぼ同等のものが多いのですが、使い方が分かりにくいものもあったので、それらを中心にまとめてみます。


dot, batch_dot

Kerasのバックエンドを使う時とそうでない時の注意として、バックエンドを使うと、Tensorflowなどと同様にバッチの次元を考慮しなければならないということです。

KerasのLayerにあるものでshapeに関する引数を取るものは、基本的にバッチの次元を抜いて考えます。RGBの画像であればshape=(3, 32, 32)というふうに与えますが、バックエンドの関数ではshape=(None, 3, 32, 32)のようにバッチの次元を考慮して計算を考えなければいけません。

ここで紹介するdot積の関数dot,batch_dotはそれぞれバッチの次元を考慮する、考慮しない関数になっています。

以下に例を示します。

import keras.backend as K

a = K.variable([[1,2],[3,4]])
b = K.variable([[5,6],[7,8]])

print K.eval(K.dot(a, b)) # a行列とb行列の掛け算
print K.eval(K.batch_dot(a, b, 1)) # a[i]とb[i]のdot積の配列
print K.eval(a * b) # 要素ごとの掛け算

これを実行すると以下のような結果になります。

[[ 19.,  22.],

[ 43., 50.]]
[[ 17.],
[ 53.]]
[[ 5., 12.],
[ 21., 32.]]

また、このようなdimensionが変更するような計算をLambdaレイヤーに突っ込む場合はoutput_shapeを明示的に与える必要があります。

import numpy as np

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

x_in = Input(shape=(2, 2))
x = Lambda(lambda x: K.dot(K.variable([0, 1]), x), output_shape=(2,))(x_in)
model = Model(input=x_in, output=x)
print model.predict([np.array([[[1,2],[3,4]]])])
# [[ 3. 4.]]


one_hot

自然言語処理やTensorflowを使っている人は知っているかもしれませんが、ウィキペディアにも説明されている通り、“1つだけHigh(1)であり、他はLow(0)であるようなビット列❞を生成します。

以下に例を示します。

print K.eval(K.one_hot(K.variable([0,2,1,0], dtype=int), 3))

# [[ 1. 0. 0.]
# [ 0. 0. 1.]
# [ 0. 1. 0.]
# [ 1. 0. 0.]]


dimensionの編集

permute_dimensions, expand_dims, squeezeで次元の置換、追加、削除が行えます。

a = K.variable([[[1,2],[3,4]]])

print K.eval(K.shape(a))
# [1, 2, 2]

print K.eval(K.permute_dimensions(a, [1, 2, 0]))
# [[[ 1.],
# [ 2.]],
#
# [[ 3.],
# [ 4.]]]

print K.eval(K.expand_dims(a, 2))
# [[[[ 1., 2.]],
#
# [[ 3., 4.]]]]

print K.eval(K.squeeze(a, 0))
# [[ 1., 2.],
# [ 3., 4.]]


gather

いわゆるスライス処理ですが、最初の次元に対してのみインデックスを指定できるので、任意の軸に対してインデキシングしたいときはpermute_dimensionsとなどと組み合わせる必要があります。

a = K.variable([[1,2],[3,4],[5,6]])

print K.eval(K.gather(a, 0))
# [ 1., 2.]
print K.eval(K.gather(K.permute_dimensions(a, [1, 0]), 0)) # K.eval(K.gather(K.transpose(a), 0))と等価
# [ 1., 3., 5.]


最後に

最後にここまでの知識を使って、chainerで書かれたモデルをkerasで実装しなおしてみました。

ネタはValue Iteration Networks(@peisuke さんのchainer実装)です。

NIPS2016のベストペーパーだったみたいですね。

githubにあげています。

vin-keras