Pythonでディープラーニングを実装するための深層学習ライブラリTensorFlowにおいてKerasの各種API(Sequential API, Functional API, Subclassing API)について使用方法の違いについて紹介します。
Contents
TensorFlow, Kerasでモデルを構築する様々な方法
TensorFlowにおいてKerasのAPIは、実装したい問題の複雑さやプログラマのレベルに応じてSequential API、Functional API、Subclassing APIという方法があります。
本記事では、各モデル構築方法の概要について説明します。
Sequantial API, Functional API, Subclassing API
以降では、Sequential API、Funcitional API、Subclassing APIのそれぞれの概要と簡単な実装例を見ることで違いを確認していきたいと思います。
各モデルの概要
TensorFlowのKerasを用いたモデル構築方法としては、以下の3種類があります。
種類 | 概要 | エンジニア レベル | 用途 |
---|---|---|---|
Sequential API | 層を積み重ねるという最も単純で使いやすいモデル構築方法 | 初心者 | 単純なモデル構築 |
Functional API | 関数を積み上げるように構築するAPIで、最も一般的に使われるAPI | 一般~上級エンジニア | 標準的なユースケース~複雑なモデル構築 |
Subclassing API | Modelクラスを継承し、各機能を一から実装、構築する方法 | 研究者、上級エンジニア | 研究などの複雑なモデル構築 |
最も一般的なユースケースで使用されているのはFunctional APIかと思います。研究や複雑なユースケースの場合でより複雑なディープラーニングの構築が必要となる場合には、Subclassing APIで、Modelクラスを継承して細かく実装していく必要があります。
簡単な実装例
手書き文字のデータセットとして有名なMNIST(エムニスト)を用いた手書き文字の分類をする例で、それぞれのAPIの実装例を紹介しつつ、違いを確認していきたいと思います。
MNIST画像は$28\times28$の画像なので、reshapeで784次元ベクトルとし、0~1の数値に正規化してから入力します。また、訓練データの一部を評価データとすることで、訓練データ、評価データ、テストデータという3つの種類を用意します。以降で紹介するプログラム例でデータは以下の通りで同じように読み込んでいます。
from tensorflow.keras.datasets import mnist # ===== MNISTデータを読み込み&正規化 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28 * 28)).astype("float32") / 255 test_imgs = test_imgs.reshape((10000, 28 * 28)).astype("float32") / 255 # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx]
画像なのでモデルとしてCNNを使う方法等ありますが、今回はシンプルに全結合のニューラルネットワークにドロップアウトをつけただけのシンプルなモデル構造を使います。中間層は256ノードの層を2つつなげて、最後の出力層では0~9の識別をするので10ノードの層を作ります。
活性化関数は中間層ではReLU関数、最後の出力層は確率値として出力するためにsoftmax関数を使用します。また、今回使い方を紹介することが目的ですので、実行を確認しやすいように繰り返しのエポック数は5と少なめにしています。エポック数は変えながら試してみてください。
【検証データとテストデータを分ける理由】
ディープラーニングの場合には、検証データとテストデータを明確に分けます。というのも、検証データを使ってパラメータチューニングを行ううちに検証データセットを過学習する結果になることがあるためです。これは、情報の漏れと言われます。そのため、最終的には訓練に全く関わっていないテストデータセットで評価をするべきです。
Sequential APIを用いる場合
実装例
Sequential APIを用いる場合の実装例を紹介します。
import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.datasets import mnist def main(): """メイン関数""" # ===== MNISTデータを読み込み&正規化 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28 * 28)).astype("float32") / 255 test_imgs = test_imgs.reshape((10000, 28 * 28)).astype("float32") / 255 # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx] # ===== モデルを構築する(Sequential APIを使用) model = keras.Sequential( [ keras.Input(shape=(28 * 28,)), layers.Dense(256, activation="relu"), layers.Dense(256, activation="relu"), layers.Dropout(0.5), layers.Dense(10, activation="softmax"), ] ) # ===== optimizer、損失関数、指標を指定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) print(model.summary()) # ===== fitを使ったモデルの訓練 history = model.fit( train_imgs, train_labels, epochs=5, validation_data=(val_imgs, val_labels) ) # ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result) # ===== predictを使って予測結果を表示 preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}") if __name__ == "__main__": main()
【実行結果例】 Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 256) 200960 dense_1 (Dense) (None, 256) 65792 dropout (Dropout) (None, 256) 0 dense_2 (Dense) (None, 10) 2570 ================================================================= Total params: 269,322 Trainable params: 269,322 Non-trainable params: 0 _________________________________________________________________ None Epoch 1/5 1500/1500 [==============================] - 5s 3ms/step - loss: 0.2733 - accuracy: 0.9181 - val_loss: 0.1202 - val_accuracy: 0.9632 Epoch 2/5 1500/1500 [==============================] - 5s 3ms/step - loss: 0.1148 - accuracy: 0.9658 - val_loss: 0.0976 - val_accuracy: 0.9722 Epoch 3/5 1500/1500 [==============================] - 5s 4ms/step - loss: 0.0803 - accuracy: 0.9757 - val_loss: 0.0957 - val_accuracy: 0.9725 Epoch 4/5 1500/1500 [==============================] - 5s 4ms/step - loss: 0.0648 - accuracy: 0.9794 - val_loss: 0.0920 - val_accuracy: 0.9729 Epoch 5/5 1500/1500 [==============================] - 5s 3ms/step - loss: 0.0482 - accuracy: 0.9847 - val_loss: 0.0891 - val_accuracy: 0.9778 313/313 [==============================] - 1s 2ms/step - loss: 0.0803 - accuracy: 0.9783 [0.08031921088695526, 0.9782999753952026] 313/313 [==============================] - 0s 995us/step 予測: 7, 正解: 7
以降で各部分について詳細を解説します。Sequential APIについては以下の流れで使用します。
1.Sequentialをインスタンス化し、各層の情報を入力
# ===== モデルを構築する(Sequential APIを使用) model = keras.Sequential( [ keras.Input(shape=(28 * 28,)), layers.Dense(256, activation="relu"), layers.Dense(256, activation="relu"), layers.Dropout(0.5), layers.Dense(10, activation="softmax"), ] )
各層の情報をSequentialモデルに渡すことでモデルを構築しています。keras.Inputが入力の形状を示すもので、layers.Denseが中間層です。ノードがいくつであるのか、活性化関数(activation)が何かといった情報等を引数に渡すことができます。
また、過学習を抑制する有効な方法であるドロップアウトについても層として定義されており、ノードを削除する割合を指定して設定することができます。この例では0.5なので半分を削除して訓練します。
Sequentialのもう一つの書き方として、以下のようにaddを使って順次追加していっても構いません。
# ===== モデルを構築する(Sequential APIを使用) # addで層を追加する場合 model = keras.Sequential() model.add(keras.Input(shape=(28 * 28,))) model.add(layers.Dense(256, activation="relu")) model.add(layers.Dense(256, activation="relu")) model.add(layers.Dropout(0.5)) model.add(layers.Dense(10, activation="softmax"))
2.オプティマイザ、損失関数、指標の指定
次に作成したモデルをcompileメソッドで構築します。その際には、オプティマイザ、損失関数、指標といった情報を指定します。
- オプティマイザ:損失関数に基づいてネットワークをどのように更新するかを設定する
- 損失関数:訓練中に最小化する関数を指定する
- 指標:正解率など検証に使用する指標値を指定する
# ===== optimizer、損失関数、指標を指定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) print(model.summary())
それぞれの細かな説明は割愛しますが簡単に紹介します。
オプティマイザーとしてはadamを指定しています。Adamはモメンタム法とAdaGradを組み合わせて効率的に勾配方向に進むように設計されたアルゴリズムです。他にもSGD、AdaGrad、RMSProp等色々ありますが、最初はAdamを用いて学習し、精度が高くならない時にAdaGradやRMSProp等を使うのが一つのやり方かなと思います。
損失関数は、’sparse_categorical_crossentropy’を使用しています。クロスエントロピーは分類問題を扱う場合には一般的に使用される方法です。
指標としては、正解率(accuracy)を指定し、予測した全体で正解した割合を指標にしています。
また、summary()は作成したモデルを確認するのに便利です。出力の以下の部分のように、モデルの構造について出力してくれます。
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 256) 200960 dense_1 (Dense) (None, 256) 65792 dropout (Dropout) (None, 256) 0 dense_2 (Dense) (None, 10) 2570 ================================================================= Total params: 269,322 Trainable params: 269,322 Non-trainable params: 0 _________________________________________________________________ None
各層でのパラメータ数も分かります。例えばdense層の部分では、(784+1)*256の重みが存在します。ここで+1となっているのは各ノードのバイアス部分です。モデル全体としては269,322の重みを調整していることになります。
3.fitを使った訓練
ここまで準備できたらついに訓練です。訓練では組み込みのfitメソッドが使用できます。
# ===== fitを使ったモデルの訓練 history = model.fit( train_imgs, train_labels, epochs=5, validation_data=(val_imgs, val_labels) )
fitメソッドには、訓練で使う画像(train_imgs)と正解ラベル(train_labels)を渡します。また、訓練のエポック数(epochs)を指定します。また、検証用データをvalidation_dataとして渡すことで、各エポックで検証データを用いた指標値の計算を行うことができます。
今回historyとして格納した変数は使用していませんが、historyには各エポックでの指標値の変化の情報等を保持しているため、学習の推移を後で確認することができます。
4.evaluateを用いたテストデータの評価
modelの訓練が終わった後には、テストデータ(test_imgs, test_labels)に対してどれだけの性能が出るのかを評価します。評価する際にはevaluateメソッドを使用します。
# ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result)
結果としては、[損失値(loss), 指標値(今回は指定した’accuracy’)]の情報が返ってきます。
5.モデルを使った予測
では、訓練して作成したモデルを使って予測をしてみましょう。予測ではpredictメソッドが使用できます。
# ===== predictを使って予測結果を表示 preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}")
今回はsoftmax関数を使っているので、実行結果のpredsは(784, 10)の配列となり、各列にどの数字であるかの確率値が入ります。そのため、予測としては確率が最も高いものになるので、argmaxを使って最も高い確率値の部分を予測として確認しています。
モデルの重みが計算されるのはbuild()が呼び出されたタイミング
少し補足的な説明をします。モデルを作成する際には、以下のようにInputの形状を指定しなくても問題がありません。同様に各layersの層も前の層のサイズを引数に渡すようなことをしていないことがわかるかと思います。
# ===== モデルを構築する(Sequential APIを使用) model = keras.Sequential( [ layers.Dense(256, activation="relu"), layers.Dense(256, activation="relu"), layers.Dropout(0.5), layers.Dense(10, activation="softmax"), ] )
それぞれの層での具体的な重みが作成されるのは、そのモデルに入力形状が与えられて最初に呼び出されたときです。そのため、この時点では重みが存在しないので上記コードの後に以下を実行するとエラーとなります。(summary()なども同様です)
# ===== この時点ではモデルの重みについては計算されていない print(model.weights)
【実行結果】 ValueError: Weights for model sequential have not yet been created. Weights are created when the Model is first called on inputs or `build()` is called with an `input_shape`.
エラーの記載を見てもわかるように、この時点では重みが計算されておらず、入力値とともに初めてモデルが呼ばれたとき、またはbuild()に入力形状が渡されて呼び出されたときに重みが計算されるということが書かれています。そのため、以下を明示的に実行すると重みの値を見ることができるようになります。
# ===== buildを呼び出すと重みが作成される。 model.build(input_shape=(None, 28 * 28)) print(model.weights)
最初の引数はバッチサイズを表し、Noneはバッチサイズが指定されないことを意味します。上記で見てきたkeras.Inputの引数と少し違うので注意してください。
もう少し細かなことを言っておくと、keras.Model内の__call__()メソッド内でbuild()、call()というメソッドが呼び出されます。__call__()が呼び出された際に重みが計算されていなかったらbuild()を実行した後にcall()が実行されます。このことは、後々のSubclassing APIの構造を把握するためには理解しておくと便利です。
Functional APIを用いる場合
実装例
Functional APIを用いる場合の実装例を紹介します。
import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.datasets import mnist def main(): """メイン関数""" # ===== MNISTデータを読み込み&正規化 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28 * 28)).astype("float32") / 255 test_imgs = test_imgs.reshape((10000, 28 * 28)).astype("float32") / 255 # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx] # ===== モデルを構築する(Functional APIを使用) inputs = keras.Input(shape=(28 * 28,)) x = layers.Dense(256, activation="relu")(inputs) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(10, activation="softmax")(x) model = keras.Model(inputs=inputs, outputs=outputs) print(model.summary()) # ===== optimizer、損失関数、指標を指定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) # ===== fitを使ったモデルの訓練 history = model.fit( train_imgs, train_labels, epochs=5, validation_data=(val_imgs, val_labels) ) # ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result) # ===== predictを使って予測結果を表示 preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}") if __name__ == "__main__": main()
実行結果はSequentialの場合と同じようになるので省略します。見てもらうと分かりますが、compile以降のコードはSequentialの場合と同じです。そのため、Sequentialとは異なるモデル構築方法の部分のみ詳細を説明します。
Functional APIにおけるモデル構築方法
Functional APIを用いたモデル構築をしている部分は以下の部分になります。
# ===== モデルを構築する(Functional APIを使用) inputs = keras.Input(shape=(28 * 28,)) x = layers.Dense(256, activation="relu")(inputs) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(10, activation="softmax")(x) model = keras.Model(inputs=inputs, outputs=outputs) print(model.summary())
層の定義方法としてはSequentialと同じですが、さらにその後ろに()で前の層の出力を渡している点が異なります。
全ての層のつながりを定義した後に、モデルを構築する際にはkeras.Modelのコンストラクタに入力と出力を指定します。
モデルの可視化
Functional APIではモデル構築後に以下のようにkeras.utils.plot_modelを使用することでモデルの可視化ができます。
# ===== モデルを構築する(Functional APIを使用) inputs = keras.Input(shape=(28 * 28,)) x = layers.Dense(256, activation="relu")(inputs) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(10, activation="softmax")(x) model = keras.Model(inputs=inputs, outputs=outputs) print(model.summary()) # モデルを画像で可視化する keras.utils.plot_model(model, "mnist_classifier.png", show_shapes=True)
このように可視化すると、各層の入出力情報も含めて分かりやすいので非常に便利です。
なお、環境の状況により以下のエラーが出る場合があります。その場合は、エラーに記載されている通り「pip install pydot」と、graphvizのインストールを実施してください。
ImportError: You must install pydot (`pip install pydot`) and install graphviz (see instructions at https://graphviz.gitlab.io/download/) for plot_model/model_to_dot to work.
複数の入出力を持つようなモデルの設計
Functional APIがSequentialとどのように違うのかあまり分からないかと思います。Sequential APIでは、一本のつながった層しか定義することができませんでしたが、Functional APIでは複数入力、複数出力のようなモデルにも対応できます。
例えば、以下のようなことができるようになります。
# ===== モデルを構築する(Functional APIを使用) # 複数の入出力を持つようなモデルの設計 inputs1 = keras.Input(shape=(28 * 28,)) inputs2 = keras.Input(shape=(10,)) inputs = layers.Concatenate()([inputs1, inputs2]) x = layers.Dense(256, activation="relu")(inputs) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) outputs1 = layers.Dense(10, activation="softmax")(x) outputs2 = layers.Dense(1, activation="sigmoid")(x) model = keras.Model(inputs=[inputs1, inputs2], outputs=[outputs1, outputs2]) print(model.summary()) # モデルを画像で可視化する keras.utils.plot_model(model, "mnist_classifier_multi.png", show_shapes=True)
上記の例では複数入力をConcatenateでつなげてネットワークに流して、2値分類をするような出力層をさらに一つ追加しているような例です。※こんなこともできるというだけで、何か具体的な実例を想定した例ではありません。
このように、Functional APIを使用すればSequentialでは表現しきれなかったモデル構造を構築することが可能になります。
Subclassing APIを用いる場合
実装例
Subclassing APIを用いる場合の実装例を紹介します。
import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.datasets import mnist class MnistModel(keras.Model): """MNIST 分類モデル""" def __init__(self): """コンストラクタ""" # superのコンストラクタを呼び出す super(MnistModel, self).__init__() # 各層の定義を行う self.layer1 = layers.Dense(256, activation="relu") self.layer2 = layers.Dense(256, activation="relu") self.dropout = layers.Dropout(0.5) self.classifier = layers.Dense(10, activation="softmax") def call(self, x): """フォワード処理 Args: x: 入力データ Returns: フォワード処理の計算結果 """ x = self.layer1(x) x = self.layer2(x) x = self.dropout(x) outputs = self.classifier(x) return outputs def main(): """メイン関数""" # ===== MNISTデータを読み込み&正規化 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28 * 28)).astype("float32") / 255 test_imgs = test_imgs.reshape((10000, 28 * 28)).astype("float32") / 255 # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx] # ===== モデルを構築する(Subclassing APIを使用) model = MnistModel() # ===== optimizer、損失関数、指標を指定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) # ===== fitを使ったモデルの訓練 history = model.fit( train_imgs, train_labels, epochs=5, validation_data=(val_imgs, val_labels) ) # ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result) # ===== predictを使って予測結果を表示 # preds = model(test_imgs)でも同じ preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}") if __name__ == "__main__": main()
実行結果はSequentialやFunctionalの場合と同じようになるので省略します。見てもらうと分かりますが、compile以降のコードはSequentialやFunctionalの場合と同じです。そのため、異なるモデル構築方法の部分のみ詳細を説明します。
Modelクラスの継承
Subclassing APIを用いる場合は、keras.Modelクラスを継承して細部を実装していきます。
class MnistModel(keras.Model): """MNIST 分類モデル""" def __init__(self): """コンストラクタ""" # superのコンストラクタを呼び出す super(MnistModel, self).__init__() # 各層の定義を行う self.layer1 = layers.Dense(256, activation="relu") self.layer2 = layers.Dense(256, activation="relu") self.dropout = layers.Dropout(0.5) self.classifier = layers.Dense(10, activation="softmax") def call(self, x): """フォワード処理 Args: x: 入力データ Returns: フォワード処理の計算結果 """ x = self.layer1(x) x = self.layer2(x) x = self.dropout(x) outputs = self.classifier(x) return outputs
自分で実装することになるため自由度はいくらでもあることが特徴です。最低限実装する必要があるのは、コンストラクタ__init__()とcall()メソッドになります。
コンストラクタ__init__()では、メンバ変数として各層の定義を行います。この時、superのコンストラクタを呼び出すことを忘れないようにしましょう。
次にcallではフォワード処理を実行します。この処理では、各層の接続関係等について定義しますのでFunctional APIでの定義に似ていますね。
また、このcall()は、model(test_imgs)のように記述した時に呼び出される__call__()メソッドに関連しています。__call__()を直接実装すればよいかと思うかもしれません。ただ、__call__()メソッド内では、重みが計算されていない時はbuild()が呼び出されてからcall()が呼び出されます。この際のcallメソッドを実装しているわけです。(最初の頃は私もなぜ__call__()ではなく、call()を実装するのかよく分かりませんでした…)
さて、自分で作成したモデルを使用する場合は、以下のようにインスタンス化して使用します。後の処理(訓練や評価など)については、SequentialやFunctinalの場合と同じです。
# ===== モデルを構築する(Subclassing APIを使用) model = MnistModel()
keras.Modelを継承してサブクラス化する方法は、ディープラーニングのモデルを構築するためには最も柔軟な方法になります。callメソッド内で再帰的な処理を作るなどどのようなことも実現できるのがメリットです。
一方で、実装に関する責任は全て開発者となるため、細かなミスが発生しやすいというデメリットがあります。
Subclassing APIでサポートされない範囲
Functional APIは、しっかりと定義されたデータ構造、処理をブロックのように組み立てていく方法のため、上記で紹介した図での可視化などデータ構造を調べたりすることが容易です。
一方でSubclassing APIを用いた方法では、モデルの実態を把握するのが容易ではありません。例えば、summary()を使っても層の結合に関する情報を表示することはできませんし、plot_model()でモデルの接続状態を可視化することもできません。このようにフォワードの計算処理は完全にブラックボックス化されます。
このように、対応する問題の複雑さ等を考慮して、どのAPIを使用するかを決める必要があります。やはり、最もバランスがよいのはFunctional APIだと思います。
まとめ
TensorFlowにおいてKerasのAPIでモデルを構築する方法としてSequential API、Functional API、Subclassing APIについて紹介しました。MNISTの手書き文字データセットを用いてそれぞれの使い方や違いについてまとめています。
Functional APIが一番バランスよく広いケースの問題に対応できるかと思います。研究目的の方や複雑なモデル構築が必要な方は、Subclassing APIでモデルをサブクラス化して細部を実装する必要があるわけですが、全てエンジニアの責任となり、ミスが発生しやすくなったりデバッグ作業は大変になったりしますので注意が必要です。
どれかのAPIについて決めてしまうというよりも、現在で自分が直面している問題の複雑さや自分のレベルに合わせて適切にAPIを選択できるように、よく考えてみるようにしましょう。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。