keras functional APIの使い方メモ

kerasでちょっとややこしいネットワークを作るときに使うfunctional APIの使い方を調べたのでまとめます。

公式ドキュメントはここ
https://keras.io/ja/getting-started/functional-api-guide/

## どんな事ができるか
kerasでneural networkモデルを作るにはkeras.models.Sequentialを使う方法と、functional APIを使う方法があります。Sequentialは単純な一本道のモデルを手軽に書けるように作られたものですが、複数入出力や分岐を導入するにはfunctional APIが必要です。

Sequentialモデルの使い方はこちら
https://qiita.com/studio_haneya/items/d6802cb3c830201a60c7

環境構築は前に書いたのでそちらを見てください
https://qiita.com/studio_haneya/items/0a9fb00aabbebda6d0c8
https://qiita.com/studio_haneya/items/dbb7a86d3ed4893e5500

## packageのimportとデータの準備
tensorflow2の場合はtensorflow.kerasを、tensorflow1の場合はkerasを使いますが、keras以下は書き方が同じなので以下のようにしておけばどっちでも動くようになります。データはみんな大好きmnistを使います。

```python:python
import tensorflow as tf
if int(tf.__version__.split('.')[0]) >= 2:
    from tensorflow import keras
else:
    import keras

# mnistをダウンロード
mnist = keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 0-255の値が入っているので、0-1に収まるよう正規化します
x_train, x_test = x_train / 255.0, x_test / 255.0

# データを確認
print(x_train.shape, x_test.shape)
```
## Sequentialの場合の書き方
keras.models.Sequential()にlistで与えるか、model.add()で1層ずつ足してくかしてモデルをつくり、最期に学習条件を決めてcompileすれば完成です。

```python:python
# Sequentialモデルを定義します
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10, activation='softmax')
])

# モデルをcompileします
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit(x_train, y_train, validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate(x_test, y_test))
```
## functional APIの書き方
上記のSequentialの場合とまったく同じモデルをfunctional APIで書くと次のようになります。Sequentialだと入力数と出力数がどちらも１つと決まってるのでSequentialでネットワーク構造を定義したら完成でしたが、functional APIだと入力と出力をどちらも複数設定できますので、ネットワーク構造をkeras.layersで定義する部分の２つを書いておいて、入力と出力がいくつあるのかkeras.Model()で定義して完成となります。

```python:python
# モデル構造を定義します
inputs = keras.layers.Input(shape=(28, 28))
x = keras.layers.Flatten()(inputs)
x = keras.layers.Dense(128, activation='relu')(x)
x = keras.layers.Dropout(0.2)(x)
predictions = keras.layers.Dense(10, activation='softmax')(x)

# 入出力を定義します
model = keras.Model(inputs=inputs, outputs=predictions)

# モデルをcompileします
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit(x_train, y_train, validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate(x_test, y_test))
```
学習データとテストデータのようにkerasの外からkerasモデルに渡すデータは必ず最初にkeras.layers.Input()で受け取り、そこから加える層の右にその層への入力を（）付きで与えるように書いて、1層ずつ増やしていくという書き方になります。下の例だとpredictionsに入力から出力までのInput => Flatten => Dense(128, relu) => Dropout => Dense(10, softmax)までのネットワークが全部入ってますので、Sequentialで書いたmodelと同じ内容になります。

## 入力が2つある場合
入力が複数ある場合はinputが複数あるネットワークを書いて、keras.Model()にlistでinputを与えるようにします。下の例はmnistデータを2つに分けてkeras model内で結合してから同じネットワークに通すようにしたものです。

```python:python
# 複数入力のテストの為にxを分割してみます
x_train2_1 = x_train.reshape(60000, 784)[:,:392]
x_train2_2 = x_train.reshape(60000, 784)[:,392:]
x_test2_1 = x_test.reshape(10000, 784)[:,:392]
x_test2_2 = x_test.reshape(10000, 784)[:,392:]

# Functional APIでモデルを定義します
input1 = keras.layers.Input(shape=(392,))
input2 = keras.layers.Input(shape=(392,))
inputs = keras.layers.concatenate([input1, input2])
x = keras.layers.Dense(128, activation='relu')(inputs)
x = keras.layers.Dropout(0.2)(x)
predictions = keras.layers.Dense(10, activation='softmax')(x)

# 入出力を定義します
model = keras.Model(inputs=[input1, input2], outputs=predictions)

# モデルをcompileします
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit([x_train2_1, x_train2_2], y_train, validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate([x_test2_1, x_test2_2], y_test))
```
## 入力と出力が2つある場合
分岐を加えて出力が2つあるmodelに変えてみました。x1とx2の2つの経路に分岐していて、prediction1とprediction2がそれぞれの出力までのネットワーク情報をもっています。出力段が2つになったのでkeras.Model()に与える出力段も

```python:python
# 複数入力のテストの為にxを分割してみます
x_train2_1 = x_train.reshape(60000, 784)[:,:392]
x_train2_2 = x_train.reshape(60000, 784)[:,392:]
x_test2_1 = x_test.reshape(10000, 784)[:,:392]
x_test2_2 = x_test.reshape(10000, 784)[:,392:]

# Functional APIでモデルを定義します
input1 = keras.layers.Input(shape=(392,))
input2 = keras.layers.Input(shape=(392,))

inputs1 = keras.layers.concatenate([input1, input2])
x1 = keras.layers.Dense(128, activation='relu')(inputs1)
x1 = keras.layers.Dropout(0.2)(x1)
prediction1 = keras.layers.Dense(10, activation='softmax')(x1)

inputs2 = keras.layers.concatenate([input1, input2])
x2 = keras.layers.Dense(128, activation='relu')(inputs2)
x2 = keras.layers.Dropout(0.2)(x2)
prediction2 = keras.layers.Dense(10, activation='softmax')(x2)

# 入出力を定義します
model = keras.Model(inputs=[input1, input2], outputs=[prediction1, prediction2])

# モデルをcompileします
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit([x_train2_1, x_train2_2], [y_train, y_train], 
                 validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate([x_test2_1, x_test2_2], [y_test, y_test]))
```
# 出力2つの損失関数を別にする
せっかく出力を分けたので損失関数も別々に入れてみます。modelを作るときにname=''で名付けておいて、compile()するときにlossを辞書型で渡せば出力ごとに異なる損失関数を使うことができます。下の例だと同じ損失関数を使ってますが、ぜんぜん違う損失関数を指定しても構いません。学習はトータルの損失関数を最小化するように進めますがデフォルトでは単純に合計するようです。加算比率をloss_weightsに辞書型で渡すことで指定することもできるので、以下では0.5ずつで加算するようにしています。

```python:python
# 複数入力のテストの為にxを分割してみます
x_train2_1 = x_train.reshape(60000, 784)[:,:392]
x_train2_2 = x_train.reshape(60000, 784)[:,392:]
x_test2_1 = x_test.reshape(10000, 784)[:,:392]
x_test2_2 = x_test.reshape(10000, 784)[:,392:]

# Functional APIでモデルを定義します
input1 = keras.layers.Input(shape=(392,))
input2 = keras.layers.Input(shape=(392,))

inputs1 = keras.layers.concatenate([input1, input2])
x1 = keras.layers.Dense(128, activation='relu')(inputs1)
x1 = keras.layers.Dropout(0.2)(x1)
prediction1 = keras.layers.Dense(10, activation='softmax', name='prediction1')(x1)

inputs2 = keras.layers.concatenate([input1, input2])
x2 = keras.layers.Dense(128, activation='relu')(inputs2)
x2 = keras.layers.Dropout(0.2)(x2)
prediction2 = keras.layers.Dense(10, activation='softmax', name='prediction2')(x2)

# 入出力を定義します
model = keras.Model(inputs=[input1, input2], outputs=[prediction1, prediction2])

# モデルをcompileします
model.compile(optimizer='adam',
              loss={'prediction1': 'sparse_categorical_crossentropy', 
                    'prediction2': 'sparse_categorical_crossentropy'},
              loss_weights={'prediction1': 0.5,
                            'prediction2': 0.5},
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit([x_train2_1, x_train2_2], [y_train, y_train], 
                 validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate([x_test2_1, x_test2_2], [y_test, y_test]))
```


## 学習済みmodelを組み込む
学習済みmodelを部品として組み込むことも出来ます。使い方はkeras.layersの代わりに学習済みmodelを置くだけですし、組み込んだら1つのkeras modelとして使えますのでアンサンブルモデルも簡潔に書けて便利です。

下の例では上半分で学習しで作ったmodelを下半分で部品として組み込んだmodel2を作っています。

```python:python
# Functional APIでモデルを定義します
inputs = keras.layers.Input(shape=(28, 28))
x = keras.layers.Flatten()(inputs)
x = keras.layers.Dense(128, activation='relu')(x)
x = keras.layers.Dropout(0.2)(x)
predictions = keras.layers.Dense(10, activation='softmax')(x)

# 入出力を定義します
model = keras.Model(inputs=inputs, outputs=predictions)

# モデルをcompileします
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model.summary())

# 学習します
hist = model.fit(x_train, y_train, validation_split=0.1, epochs=5)

# テストデータの予測精度を計算します
print(model.evaluate(x_test, y_test))

######################################################

# モデルを再利用するモデルを定義します
inputs = keras.layers.Input(shape=(28, 28))
predictions = model(inputs)

# モデルをcompileします
model2 = keras.Model(inputs=inputs, outputs=predictions)
model2.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
display(model2.summary())

# テストデータの予測精度を計算します
print(model2.evaluate(x_test, y_test))
```

これを使えば論文に出てくるようなネットワークが簡単に作れますね。レッツトライ。

# 更新履歴
20190827: 損失関数を別々にする場合について追記しました