Keras : Getting started : Keras functional API

Keras : Getting Started : Keras functional API (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/15/2018 (2.2.0)

* 本ページは、Keras 本家サイトの – Getting started with the Keras functional API を
動作確認・翻訳した上で適宜、補足説明したものです:

* Keras 本家サイトには日本語訳も用意されていますが、それとは無関係に翻訳したものです。
* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

本文

Keras functional API は、マルチ出力モデル、有向非巡回グラフ、あるいは共有層を伴うモデルのような複雑なモデルを定義するための方法です。

このガイドは貴方が既に Sequential モデルに精通していることを想定しています。

単純なものから始めましょう。

 

最初のサンプル: 密結合 (= densely-connected) ネットワーク

そのようなネットワークを実装するために Sequential モデルは多分より良い選択ですが、本当に単純なものから始めることは役立つでしょう。

  • 層インスタンスは (tensor 上で) callable (呼び出し可能) で、それは tensor を返します。
  • そして入力 tensor と出力 tensor は Model を定義するために使用できます。
  • そのようなモデルは丁度 Keras Sequential モデルのように訓練できます。
from keras.layers import Input, Dense
from keras.models import Model

# これは tensor を返します。
inputs = Input(shape=(784,))

# 層インスタンスは tensor 上で呼び出し可能で、tensor を返します。
x = Dense(64, activation='relu')(inputs)
x = Dense(64, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)

# これは Input 層と 3 つの Dense 層を含むモデルを作成します。
model = Model(inputs=inputs, outputs=predictions)
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(data, labels)  # starts training

 

層のように、総てのモデルは呼び出し可能です

functional API では、訓練されたモデルを再利用することは容易です : それが層であるかのように、それを tensor 上で呼び出すことにより、任意のモデルを扱うことができます。モデルを呼び出すことにより単にモデルのアーキテクチャを再利用するだけではなく、その重みを再利用していることに注意してください。

x = Input(shape=(784,))
# これは動作して、上で定義した 10 通りの softmax を返します。
y = model(x)

これは例えば、入力のシークエンスを処理できるモデルを素早く作成することを可能にします。画像分類モデルを動画分類モデルに単に一行で変えることもできるでしょう。

from keras.layers import TimeDistributed

# 20 タイムステップのシークエンスのための Input tensor、
# それぞれが 784-次元ベクトルを含みます。
input_sequences = Input(shape=(20, 784))

# これは前のモデルを入力シークエンスの総てのタイムステップに適用します。
# 前のモデルの出力は 10-通りの softmax でしたので、
# 下の層の出力はサイズ 10 の 20 ベクトルのシークエンスになります。
processed_sequences = TimeDistributed(model)(input_sequences)

 

マルチ入力とマルチ出力モデル

ここに functional API のための良いユースケースがあります : マルチ入力と出力を持つモデルです。functional API は巨大な数の絡み合ったデータストリームを操作することを容易にします。

次のモデルを考えましょう。ツイッター上でニュース・ヘッドラインが幾つの「リツイート」と「いいね」を受け取るかを予測することを求めます。モデルへの主入力は単語のシークエンスとしてのヘッドラインそれ自身ですが、ひと味添えるために、私達のモデルはまた補助的な入力を持ち、ヘッドラインが投稿された時刻 etc. のような特別なデータを受け取ります。モデルはまた 2 つの損失関数を通して管理されます (= supervised)。モデルにおいて早期に主要な損失関数を使用することは深層モデルに対して良い正則化メカニズムです。

ここにモデルがどのように見えるかがあります :

 
それを functional API で実装しましょう。

主要な入力はヘッドラインを整数のシークエンス (各整数は単語をエンコード) として受け取ります。整数は 1 と 10,000 (10,000 単語の語彙) の間でシークエンスは 100 単語長です。

from keras.layers import Input, Embedding, LSTM, Dense
from keras.models import Model

# ヘッドライン入力: 1 と 10000 の間の 100 の整数のシークエンスを受け取ることを意味します。
# 任意の層に、"name" 引数を渡すことで命名できることに注意してください。
main_input = Input(shape=(100,), dtype='int32', name='main_input')

# この埋め込み層は入力シークエンスを密な 512-次元ベクトルのシークエンスにエンコードします。
x = Embedding(output_dim=512, input_dim=10000, input_length=100)(main_input)

# LSTM はベクトル・シークエンスをシークエンス全体についての情報を含む単一のベクトルに変換します。
lstm_out = LSTM(32)(x)

ここで補助的な損失を挿入します、(モデルでは主要なロスが遥かにより高くなるにしても) LSTM と埋め込み層がスムースに訓練されることを可能にするためです。

auxiliary_output = Dense(1, activation='sigmoid', name='aux_output')(lstm_out)

ここで、補助的な入力データをそれに LSTM 出力を結合することによりモデルに供給します :

auxiliary_input = Input(shape=(5,), name='aux_input')
x = keras.layers.concatenate([lstm_out, auxiliary_input])

# トップ上に深層密結合ネットワークをスタックします。
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)

# そして最後に主要なロジスティック回帰層を追加します。
main_output = Dense(1, activation='sigmoid', name='main_output')(x)

これは 2 つの入力と 2 つの出力を持つモデルを定義します :

model = Model(inputs=[main_input, auxiliary_input], outputs=[main_output, auxiliary_output])

モデルを compile して補助的な損失に 0.2 の重みを割り当てます。各異なる出力に異なる loss_weights または loss を指定するために、リストか辞書を使用できます。ここでは loss 引数として単一の損失を渡しますので、同じ損失が総ての出力上で使用されます。

model.compile(optimizer='rmsprop', loss='binary_crossentropy',
              loss_weights=[1., 0.2])

入力配列とターゲット配列のリストを渡すことによりモデルを訓練できます :

model.fit([headline_data, additional_data], [labels, labels],
          epochs=50, batch_size=32)

入力と出力は名前付けられていますので (それらに “name” 引数を渡しました)、モデルを次を通してコンパイルすることもできました :

model.compile(optimizer='rmsprop',
              loss={'main_output': 'binary_crossentropy', 'aux_output': 'binary_crossentropy'},
              loss_weights={'main_output': 1., 'aux_output': 0.2})

# そして次を通してそれを訓練しました :
model.fit({'main_input': headline_data, 'aux_input': additional_data},
          {'main_output': labels, 'aux_output': labels},
          epochs=50, batch_size=32)

 

共有層

functional API のためのもう一つの良いユース (ケース) は共有層を使用するモデルです。共有層を見てみましょう。

ツイートのデータセットを考えましょう。2 つのツイートが同じ人からのものか否かを見分けられるモデルを構築することを望みます (これはユーザを例えば、彼らのツイートの類似性により比較することを可能にします)。

これを達成するための一つの方法は 2 つのツイートを 2 つのベクトルにエンコードし、ベクトルを結合してロジスティック回帰に追加するモデルを構築することです ; これは 2 つのツイートが同じ作者を共有している確率を出力します。それからモデルはポジティブなツイート・ペアとネガティブなツイート・ペア上で訓練されます。

問題は対称的なので、最初のツイートをエンコードするメカニズムは 2 番目のツイートをエンコードするために再利用されるべきです (重みと総て)。ここでツイートをエンコードするために共有 LSTM 層を使用します。

これを functional API で構築しましょう。ツイートのための入力として shape (280, 256) の二値行列を取ります、i.e. サイズ 256 の 280 ベクトルのシークエンスで、ここでは 256-次元ベクトルの各次元は (256 個の頻度の高いキャラクタのアルファベットからの) 文字の有無をエンコードします。

import keras
from keras.layers import Input, LSTM, Dense
from keras.models import Model

tweet_a = Input(shape=(280, 256))
tweet_b = Input(shape=(280, 256))

異なる入力に渡り層を共有するためには、単純に層を一度インスタンス化して、それから望むだけの数の入力上でそれを呼び出します :

# この層は入力として行列を取ることができてサイズ 64 のベクトルを返します。
shared_lstm = LSTM(64)

# 同じ層インスタンスを複数回再利用するとき、
# 層の重みもまた再利用されます (それは効率的に *同じ* 層です)。
encoded_a = shared_lstm(tweet_a)
encoded_b = shared_lstm(tweet_b)

# それから 2 つのベクトルを結合できます :
merged_vector = keras.layers.concatenate([encoded_a, encoded_b], axis=-1)

# そしてトップ上にロジスティック回帰を追加します。
predictions = Dense(1, activation='sigmoid')(merged_vector)

# tweet 入力を predictions にリンクして訓練可能なモデルを定義します。
model = Model(inputs=[tweet_a, tweet_b], outputs=predictions)

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.fit([data_a, data_b], labels, epochs=10)

共有層の出力や出力 shape をどのように読むかを見るために一時停止しましょう。

 

層「ノード」の概念

ある入力上で層を呼び出すときはいつでも、貴方は新しい tensor (層の出力) を作成し、そして貴方は「ノード」を層に追加し、入力 tensor を出力 tensor にリンクします。同じ層を複数回呼び出すとき、層は 0, 1, 2… とインデックス付けされた複数のノードを所有します。

Keras の以前のバージョンでは、layer.get_output() で層インスタンスの出力 tensor を、あるいは layer.output_shape によりその出力 shape を得ることが出来ました。依然として可能です (get_output() がプロパティ output に置き換えられたことを除いて)。しかし層が複数の入力に接続されていたらどうでしょう?

層が一つの入力にだけ接続されている限りは、混乱はなく、.output は層の一つの出力を返すでしょう :

a = Input(shape=(280, 256))

lstm = LSTM(32)
encoded_a = lstm(a)

assert lstm.output == encoded_a

層が複数入力を持つ場合はそうではありません :

a = Input(shape=(280, 256))
b = Input(shape=(280, 256))

lstm = LSTM(32)
encoded_a = lstm(a)
encoded_b = lstm(b)

lstm.output
>> AttributeError: Layer lstm_1 has multiple inbound nodes,
hence the notion of "layer output" is ill-defined.
Use `get_output_at(node_index)` instead.

Okay それでは。次は動作します :

assert lstm.get_output_at(0) == encoded_a
assert lstm.get_output_at(1) == encoded_b

十分に単純ですね、違いますか?

同じことがプロパティ input_shape と output_shape にも当てはまります : 層が一つのノードだけを持つ限り、あるいは総てのノードが同じ入力/出力 shape を持つ限りは、「層入力/出力 shape」の概念は上手く定義されて、layer.output_shape/layer.input_shape により一つの shape が返されます。しかし例えば、同じ Conv2D 層を shape (32, 32, 3) の入力に、そして shape (64, 64, 3) の入力に適用する場合、層は複数の入力/出力 shape を持ち、そしてそれらが属するノードのインデックスを指定することによりそれらを取得しなければなりません :

a = Input(shape=(32, 32, 3))
b = Input(shape=(64, 64, 3))

conv = Conv2D(16, (3, 3), padding='same')
conved_a = conv(a)

# ここまで一つの入力だけですから、次は動作します :
assert conv.input_shape == (None, 32, 32, 3)

conved_b = conv(b)
# 今は `.input_shape` プロパティは動作しないでしょう、しかしこれは動作します :
assert conv.get_input_shape_at(0) == (None, 32, 32, 3)
assert conv.get_input_shape_at(1) == (None, 64, 64, 3)

 

更に例題

コード・サンプルは依然として始めるに最善の方法ですので、ここに更に幾つかがあります。

 

Inception モジュール

Inception アーキテクチャについての更なる情報については、Going Deeper with Convolutions を見てください。

from keras.layers import Conv2D, MaxPooling2D, Input

input_img = Input(shape=(256, 256, 3))

tower_1 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_1 = Conv2D(64, (3, 3), padding='same', activation='relu')(tower_1)

tower_2 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_2 = Conv2D(64, (5, 5), padding='same', activation='relu')(tower_2)

tower_3 = MaxPooling2D((3, 3), strides=(1, 1), padding='same')(input_img)
tower_3 = Conv2D(64, (1, 1), padding='same', activation='relu')(tower_3)

output = keras.layers.concatenate([tower_1, tower_2, tower_3], axis=1)

 

畳み込み層上の残差接続

残差ネットワークについての更なる情報については、Deep Residual Learning for Image Recognition を見てください。

from keras.layers import Conv2D, Input

# 3-チャネル 256x256 画像のための入力 tensor
x = Input(shape=(256, 256, 3))
# 3 出力チャネルを持つ 3x3 conv (入力チャネルと同じ)
y = Conv2D(3, (3, 3), padding='same')(x)
# これは x + y を返します。
z = keras.layers.add([x, y])

 

共有ビジョン・モデル

このモデルは 2 つの入力上で同じ画像処理モジュールを再利用します、2 つの MNIST 数字が同じ数字か異なる数字かを分類するために。

from keras.layers import Conv2D, MaxPooling2D, Input, Dense, Flatten
from keras.models import Model

# 最初に、ビジョン・モジュールを定義します。
digit_input = Input(shape=(27, 27, 1))
x = Conv2D(64, (3, 3))(digit_input)
x = Conv2D(64, (3, 3))(x)
x = MaxPooling2D((2, 2))(x)
out = Flatten()(x)

vision_model = Model(digit_input, out)

# それから tell-digits-apart モデルを定義します。
digit_a = Input(shape=(27, 27, 1))
digit_b = Input(shape=(27, 27, 1))

# ビジョン・モデルは共有されます、重みと総て
out_a = vision_model(digit_a)
out_b = vision_model(digit_b)

concatenated = keras.layers.concatenate([out_a, out_b])
out = Dense(1, activation='sigmoid')(concatenated)

classification_model = Model([digit_a, digit_b], out)

 

VQA (Visual question answering) モデル

このモデルは絵・写真 (= picture) についての自然言語の質問を尋ねられたとき正しい 1-単語の答えを選択できます。

それは質問をベクトルにエンコードし、画像をベクトルにエンコードし、2 つを結合し、そして可能性のある答えの幾つかの語彙に渡りトップ上のロジスティック回帰で訓練することにより動作します。

from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.layers import Input, LSTM, Embedding, Dense
from keras.models import Model, Sequential

# 最初に、Sequential モデルを使用してビジョン・モデルを定義しましょう。
# このモデルは画像をベクトルにエンコードします。
vision_model = Sequential()
vision_model.add(Conv2D(64, (3, 3), activation='relu', padding='same', input_shape=(224, 224, 3)))
vision_model.add(Conv2D(64, (3, 3), activation='relu'))
vision_model.add(MaxPooling2D((2, 2)))
vision_model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
vision_model.add(Conv2D(128, (3, 3), activation='relu'))
vision_model.add(MaxPooling2D((2, 2)))
vision_model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
vision_model.add(Conv2D(256, (3, 3), activation='relu'))
vision_model.add(Conv2D(256, (3, 3), activation='relu'))
vision_model.add(MaxPooling2D((2, 2)))
vision_model.add(Flatten())

# 今ビジョン・モデルの出力を持つ tensor を取得しましょう :
image_input = Input(shape=(224, 224, 3))
encoded_image = vision_model(image_input)

# 次に、質問をベクトルにエンコードするための言語モデルを定義しましょう。
# 各質問はせいぜい 100 単語長で、単語を 1 から 9999 までの整数としてインデックス付けします。
question_input = Input(shape=(100,), dtype='int32')
embedded_question = Embedding(input_dim=10000, output_dim=256, input_length=100)(question_input)
encoded_question = LSTM(256)(embedded_question)

# 質問ベクトルと画像ベクトルを結合しましょう :
merged = keras.layers.concatenate([encoded_question, encoded_image])

# そしてトップ上で 1000 単語に渡りロジスティック回帰を訓練しましょう :
output = Dense(1000, activation='softmax')(merged)

# これは最終モデルです :
vqa_model = Model(inputs=[image_input, question_input], outputs=output)

# 次のステージは実際のデータ上でこのモデルを訓練していきます。

 

動画 question answering モデル

画像 QA モデルを訓練した今、それを動画 QA モデルに素早く変更することができます。適切な訓練により、それに短い動画 (e.g. 100-フレーム人物アクション) を見せて動画について自然言語の質問 (e.g. 「少年は何のスポーツをプレイしていますか?」-> “サッカー (= football)”) を尋ねることができます。

from keras.layers import TimeDistributed

video_input = Input(shape=(100, 224, 224, 3))
# これは前に訓練された vision_model (重みは再利用) を通してエンコードされた動画です。
encoded_frame_sequence = TimeDistributed(vision_model)(video_input)  # 出力はベクトルのシークエンス
encoded_video = LSTM(256)(encoded_frame_sequence)  # 出力はベクトル

# これは question エンコーダのモデル単位の表現で、前のように同じ重みを再利用しています :
question_encoder = Model(inputs=question_input, outputs=encoded_question)

# 質問をエンコードするためにそれを使用しましょう :
video_question_input = Input(shape=(100,), dtype='int32')
encoded_video_question = question_encoder(video_question_input)

# そしてこれは動画 question answering モデルです :
merged = keras.layers.concatenate([encoded_video, encoded_video_question])
output = Dense(1000, activation='softmax')(merged)
video_qa_model = Model(inputs=[video_input, video_question_input], outputs=output)
 

以上