Python
Keras
TensorFlow
functionalAPI
KerasDay 2

kerasで頭に描いたネットワーク構造を実現するためのTips ~ functional API 編 ~

kerasを使って色々なNNを書いたり色々なデータを学習させたりしていくと、だんだんチュートリアルから逸脱した領域に手を出したくなってきます。そんな時意外とkerasのドキュメントはそこまで親切に面倒をみてくれないので、引っかかったところを中心にこんなネットワークを作りたいならこんな風に書けばよかった、という逆引きリファレンス的な記事です。
※ バックエンドをTensorflowとする事を前提に書いているので別のバックエンドを使っている方は所々違うかもです。
※ こんなこと言っといてあれですが、kerasのドキュメントは実際には充実していて、探しにくいだけで実は書いてあったりする。なので知ってる人は見つけられる内容だったりします。

Functional API

これはまだチュートリアルにも書いてある内容です。

が、kerasって簡単そう!という純朴な考えで飛び込んできた方には実はあまり目に触れない場所にあったり、いろんなサンプルでもSequentialで実装できてしまったりするので実はよく分かってない人が多いんじゃないでしょうか。
こんなやつです。

functionalAPI.py
from keras.models import Model
from keras.layers import Input, Dense, Activation

model_input = Input(shape=(5,))
mid = Dense(10)(model_input)
output = Activation('softmax')(mid)
model = Model(inputs=model_input, outputs=output)

基礎の基礎

僕が知ってる Dense じゃない!って思う人もいると思います。もっと言うと、なんだこれPythonっぽくない!って思う人もいるかもしれません。
実は Dense(10)Activation('softmax') はそれぞれのクラスのインスタンスを生成しており、生成されたものが1つ前のレイヤーを引数とする関数のように扱えるのです。その返り値として Tensor というオブジェクトが返ってきます。

※ 「インスタンス」や「関数」という言葉は概念的な使い方をしているので、kerasのソースコードばりばり読んで書いたりしちゃったりしてる人からすると厳密には違う事言っている可能性はあります。あと全然関係ないけど inputin も予約語だから汎用的なInputレイヤーの名前でいつも困るんだけどいい名前知ってる人いたら教えて。。

なので

functionalAPI2.py
my_dense = Dense(10)
my_activation = Activation('softmax')

model_input = Input(shape=(5,))
mid = my_dense(model_input)
output = my_activation(mid)
model = Model(inputs=model_input, outputs=output)

こうやって書くとなんとなく慣れ親しんだ形に収まるんじゃないでしょうか。特にJavascriptでこんな感じの関数の使い回しするイメージ。

ちなみにもうお分かりかと思いますがこんなモデルですね。
スクリーンショット 2017-12-02 12.43.45.png

ちょっと発展

ここまで書くと勘の良い方は

  • いつも使ってるSequentialで良いじゃん!
  • あ、この my_dense っていうレイヤーをもう一回使ったらどうなるんだろう…?重みが共有されるのかな?

のどちらかを思ったかもしれません。
できれば後者の考えに至るように頑張りましょう。

実は一度生成したレイヤーのインスタンスは複数の場所で使うことができます。しかもそれは同一のレイヤーとして扱われます。

functionalAPI3.py
my_dense = Dense(5)

model_input = Input(shape=(5,))
mid = my_dense(model_input)
mid2 = my_dense(mid)
output = Activation('softmax')(mid2)
model = Model(inputs=model_input, outputs=output)

どのように各層が接続されているか分かりますか?慣れてくると結構functional APIの方が直感的に思えてくると思います。またSequentialだとだんだん実現できないネットワーク構造になってきたのが分かると思います。

こんな感じですね。
スクリーンショット 2017-12-02 13.20.25.png

ちなみにさっきのコードでシレッと変更していますが、 my_dense に対する入力の次元は複数の箇所で使う場合揃えてあげないといけません。当たり前と言えば当たり前ですが、気をつけましょう。

未だにSequential脳の方は Input -> Dense -> Dense -> Activation と何が違うのか分からないかもしれません。形は確かに同じですが、重要なのは途中の2つの Dense の層がそれぞれ重みを持っているのではなく、同じ重みを共有している事です。

適当に学習させて重みを見てみましょう。

functionalAPI4.py
import numpy as np
from keras import backend as K

# 適当な学習
model.compile(loss='mean_squared_error', optimizer='adam')
X = np.random.rand(10,5)
y = np.random.rand(10,5)
model.fit(X, y, epochs=5)

# 出力
print([K.eval(w) for w in model.weights])

# [array([[ 0.16185357, -0.15696804, -0.36604205, -0.02380839, -0.35026637],
#         [-0.13462609,  0.09170605,  0.13241905, -0.628663  , -0.67678481],
#         [ 0.15944755, -0.67250979,  0.35241356,  0.24243712,  0.10116834],
#         [ 0.61587954, -0.04093929,  0.36012208, -0.30871043, -0.31387496],
#         [ 0.66632921, -0.31295586,  0.59317648,  0.44108745,  0.09751857]], dtype=float32),
#  array([ 0.00472282,  0.00472244,  0.00472797,  0.00468933, -0.00472205], dtype=float32)]

30個の重みが出力されると思います。5x5の方はDense層への5つの入力とDense内部の5つのノードそれぞれにつながる重みで、残りの5つは各ノードに対するバイアスになります。モデル内のすべての重みを出力しているので、このモデルは途中のDenseを2回通っているにもかかわらず同じ重みを共有している事がわかります。

y_i = \sum_{k=1}^{5} (w_{ki}*x_k) + b_i \quad (1 \leq i \leq 5)

$x$ が入力で $y$ が出力だとすると $w$ は 5x5 、 $b$ は 5 個の変数ですね。

分岐と結合

さらに、functional APIではまだ使いまわしてほしそうにウズウズしている変数達がいますね。上の例では次の層の入力としてしか使われていないTensor達です。これを思う存分使いまわしてみましょう。

functionalAPI5.py
from keras.models import Model
from keras.layers import Input, Dense, Activation, Multiply

model_input1 = Input(shape=(5,))
model_input2 = Input(shape=(5,))
mid = Dense(10)(model_input1)
mid2 = Dense(10)(model_input2)
multiplied = Multiply()([mid, mid2])
additional_dense = Dense(5)(multiplied)
additional_dense2 = Dense(5)(additional_dense)
output1 = Activation('softmax')(additional_dense)
output2 = Activation('softmax')(additional_dense2)
model = Model(inputs=[model_input1, model_input2], outputs=[output1, output2])

新たに出てきた Multiply は2つのTensorをかけ合わせたものを出力するレイヤーです。そのため2つのレイヤーを引数に取っています。(2つのレイヤーの次元は同じである必要があります)それ以外は先程とそこまで大差ない書き方になっていますね。
違う所といえば、インプット・アウトプットが複数になっている点や additional_dense という変数が2箇所で使われている点です。実際にどのようなネットワークになっているか見てみましょう。

スクリーンショット 2017-12-02 13.57.13.png

Sequentialでは到達できない領域になってきました。functionalAPIを初めて見る人は自由度の高さに驚くかもしれません。実際私もそうでした。

チュートリアルにはもっといろんな事ができる例があるよ

ここまで順を追って見ていただいた方はfunctionalAPIの直感性と自由度の高さを分かっていただけるかと思うのですが、なんとなくkerasのfunctionalAPIに関するドキュメントは初見だととっつきにくい印象があります。
「なんでカッコが2つ連続で続いてんだよ、訳分かんね」
で諦めちゃった人もいるんじゃないでしょうか。私も昔そうでしたがそれではかなりもったいないので、この記事を読んでいただけた方で、まだあまり使ったことの無い方はもう一度kerasの fuctionalAPIガイド を見てみてください。恐らくすんなりと雰囲気はつかめると思います。

「Sequentialで十分だから…」

と思う方は想像してみてください。作ったモデルが意外とうまく動いて、どんどん精度を上げるためにいろんなレイヤーを追加して最後の1歩!って所でレイヤーを分岐したいとなった時。Sequentialで作り続けているとすべてfunctionalAPIで書き直さないといけません。
モデルは概してちょっとずつ複雑化させていきたくなるものです。(本当はシンプルで汎用的なモデルが理想的だし、過学習しやすくなってしまうのでいたずらに複雑化させちゃだめですが)自由度があるという事はそれだけでアドバンテージではないでしょうか。

私が引っかかったネットワーク構造のTips

は、次回別の記事で書きます。ネタをいくつかメモしててそれを書こう!と意気込んだのに、関係ないfunctionalAPIの内容書いたらこんなに長くなってしまった…

【追記】続編書きました → kerasで頭に描いたネットワーク構造を実現するためのTips ~ Lambda 編 ~