TensorFlow/Kerasを用いて、訓練済みモデルVGG16を使った転移学習の方法について解説します。データセットとしてCIFAR10を使って分類への適用をしてみようと思います。
転移学習
転移学習とは、学習済みのモデルを使って新しいモデルの学習を行うことを言います。
ディープラーニングでは、大規模なニューラルネットワークを学習させることになるため非常に時間がかかり、データも大量に必要です。このような時に、大量データで既に学習されて公開されているモデルを使うと非常に便利です。
転移学習のイメージは以下のような形です。入力後に畳み込み層やプーリング層などの層があり、最後に全結合層の分類器がついているようなモデルだとします。この時、畳み込み層については訓練済みのものをそのまま使用し、最後の分類器の部分を付け替えます。畳み込み層の重みについては凍結して固定した状態にして、新しい分類器部分の重みを訓練することになります。
本記事ではTensorFlow/Kerasを用いた転移学習の実装例を紹介しますが、モデルとしてはVGG16というものを使用します。
VGG16は、2014年にILSVRCという画像認識コンペティションで2位となったモデルで、決して新しい技術のモデルというわけではありませんが、オンライン講座や書籍等で転移学習の説明によく用いられます。本記事でもVGG16の訓練済みモデルを用いて解説をしていこうと思います。
TensorFlow/Kerasで訓練済みモデルのVGG16を用いた画像分類の実装
以降では、TensorFlow/Kerasの訓練済みモデルVGG16を用いた画像分類の実装例を紹介していきます。
TensorFlow/Kerasでは、tensorflow.keras.applicationsにImageNetという大規模なデータセットで学習した画像分類モデルと重みが使えるようになっています。VGG16の他にもResNet、DenseNet等のより新しいモデルもあります。
また、今回例に使うデータセットはCIFAR10(サイファーテン)というデータセットです。CIFAR10は10種類の画像のデータセットで32×32のサイズで、RGBの3チャンネルを持つデータセットとなっています。各ラベルは以下の対応となっています。
- 0:飛行機
- 1:自動車
- 2:鳥
- 3:猫
- 4:鹿
- 5:犬
- 6:蛙
- 7:馬
- 8:船
- 9:トラック
実装例
以下に、TensorFlow/Kerasを用いた実装例を示します。
import matplotlib.pyplot as plt import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.applications.vgg16 import VGG16 from tensorflow.keras.datasets import cifar10 from tensorflow.keras.optimizers import SGD def main(): """メイン関数""" (train_imgs, train_labels), (test_imgs, test_labels) = cifar10.load_data() # 訓練データの一部(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] # ===== VGG16で事前に学習されたモデルを読み込む vgg16 = VGG16(include_top=False, weights="imagenet", input_shape=(32, 32, 3)) # ===== 新しい出力用分類モデルを作成する # VGG16の出力を平坦化して全結合層へ接続 x = layers.Flatten()(vgg16.output) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) # 分類のために10ノードに接続 outputs = layers.Dense(10, activation="softmax")(x) # モデルの作成 model = keras.Model(inputs=vgg16.input, outputs=outputs) # 0~18までのlayerがVGG16に関連するので重みを固定する for layer in model.layers[:19]: layer.trainable = False # モデル構成の表示&画像保存 print(model.summary()) keras.utils.plot_model( model, "transfer_learning_vgg16_cifar10.png", show_shapes=True ) # ===== オプティマイザ、損失関数、指標を設定してコンパイル # 転移学習の場合、最適化関数はSGDの選択がよいとされている model.compile( optimizer=SGD(learning_rate=1e-4, momentum=0.9), loss="sparse_categorical_crossentropy", metrics=["accuracy"], ) # ===== fitを使ったモデルの訓練 # 設定 num_epochs = 10 callbacks = [ keras.callbacks.ModelCheckpoint("vgg16_cifar10.keras", save_best_only=True) ] # 訓練の実行 history = model.fit( train_imgs, train_labels, epochs=num_epochs, batch_size=32, validation_data=(val_imgs, val_labels), callbacks=callbacks, ) # ===== history情報の可視化 # 損失関数(loss)の履歴 loss = history.history["loss"] val_loss = history.history["val_loss"] # 正解率(accuracy)の履歴 acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] # 損失関数の履歴描画 x_epoch = range(1, num_epochs + 1) plt.plot(x_epoch, loss, "r", label="training loss") plt.plot(x_epoch, val_loss, "b", label="validation loss") plt.xlabel("Epochs") plt.ylabel("Loss") plt.legend() # 正解率の履歴描画 plt.figure() plt.plot(x_epoch, acc, "r", label="training acc") plt.plot(x_epoch, val_acc, "b", label="validation acc") plt.xlabel("Epochs") plt.ylabel("Accuracy") plt.legend() plt.show() # ===== 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: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 32, 32, 3)] 0 block1_conv1 (Conv2D) (None, 32, 32, 64) 1792 block1_conv2 (Conv2D) (None, 32, 32, 64) 36928 block1_pool (MaxPooling2D) (None, 16, 16, 64) 0 block2_conv1 (Conv2D) (None, 16, 16, 128) 73856 block2_conv2 (Conv2D) (None, 16, 16, 128) 147584 block2_pool (MaxPooling2D) (None, 8, 8, 128) 0 block3_conv1 (Conv2D) (None, 8, 8, 256) 295168 block3_conv2 (Conv2D) (None, 8, 8, 256) 590080 block3_conv3 (Conv2D) (None, 8, 8, 256) 590080 block3_pool (MaxPooling2D) (None, 4, 4, 256) 0 block4_conv1 (Conv2D) (None, 4, 4, 512) 1180160 block4_conv2 (Conv2D) (None, 4, 4, 512) 2359808 block4_conv3 (Conv2D) (None, 4, 4, 512) 2359808 block4_pool (MaxPooling2D) (None, 2, 2, 512) 0 block5_conv1 (Conv2D) (None, 2, 2, 512) 2359808 block5_conv2 (Conv2D) (None, 2, 2, 512) 2359808 block5_conv3 (Conv2D) (None, 2, 2, 512) 2359808 block5_pool (MaxPooling2D) (None, 1, 1, 512) 0 flatten (Flatten) (None, 512) 0 dense (Dense) (None, 256) 131328 dropout (Dropout) (None, 256) 0 dense_1 (Dense) (None, 10) 2570 ================================================================= Total params: 14,848,586 Trainable params: 133,898 Non-trainable params: 14,714,688 _________________________________________________________________ None Epoch 1/10 1250/1250 [==============================] - 27s 19ms/step - loss: 4.3056 - accuracy: 0.3212 - val_loss: 1.7398 - val_accuracy: 0.4300 Epoch 2/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.9014 - accuracy: 0.3829 - val_loss: 1.5865 - val_accuracy: 0.4664 Epoch 3/10 1250/1250 [==============================] - 25s 20ms/step - loss: 1.7367 - accuracy: 0.4121 - val_loss: 1.5123 - val_accuracy: 0.4869 Epoch 4/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.6590 - accuracy: 0.4293 - val_loss: 1.4693 - val_accuracy: 0.4943 Epoch 5/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.5964 - accuracy: 0.4473 - val_loss: 1.4328 - val_accuracy: 0.5095 Epoch 6/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.5425 - accuracy: 0.4602 - val_loss: 1.4105 - val_accuracy: 0.5155 Epoch 7/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.5128 - accuracy: 0.4660 - val_loss: 1.3926 - val_accuracy: 0.5183 Epoch 8/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.4788 - accuracy: 0.4768 - val_loss: 1.3661 - val_accuracy: 0.5300 Epoch 9/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.4543 - accuracy: 0.4858 - val_loss: 1.3529 - val_accuracy: 0.5359 Epoch 10/10 1250/1250 [==============================] - 24s 19ms/step - loss: 1.4379 - accuracy: 0.4933 - val_loss: 1.3401 - val_accuracy: 0.5398 313/313 [==============================] - 5s 15ms/step - loss: 1.3503 - accuracy: 0.5351 [1.3502779006958008, 0.535099983215332] 313/313 [==============================] - 4s 14ms/step 予測: 6, 正解: [3]
精度は約54%台と全然高くはありません。また、今回実行してみた限り、最初(0番目)のデータに対する予測は失敗してしまっています。訓練データのかさ増しや繰り返しによりもっと精度が出るようですが、今回は転移学習の実装方法を示すということで深堀りはしません。
実装内容の解説
上記で紹介した実装例の各部分ごとの内容を説明していきます。
必要モジュールのインポート
import matplotlib.pyplot as plt import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.applications.vgg16 import VGG16 from tensorflow.keras.datasets import cifar10 from tensorflow.keras.optimizers import SGD
まずは、必要なモジュール類をインポートします。VGG16はtensorflow.keras.applications.vgg16からインポートします。また、データセットとしてはKeras内のCIFAR10がありますのであわせてインポートしています。
データセットの用意
(train_imgs, train_labels), (test_imgs, test_labels) = cifar10.load_data() # 訓練データの一部(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]
今回使用するCIFAR10データセットを準備します。読み込みはload_data()で実行できます。また、訓練データのうち20%を検証データとして抜き出しています。
【検証データとテストデータを分ける理由】
ディープラーニングの場合には、検証データとテストデータを明確に分けます。というのも、検証データを使ってパラメータチューニングを行ううちに検証データセットを過学習する結果になることがあるためです。これは、情報の漏れと言われます。そのため、最終的には訓練に全く関わっていないテストデータセットで評価します。
VGG16モデルを読み込む
# ===== VGG16で事前に学習されたモデルを読み込む vgg16 = VGG16(include_top=False, weights="imagenet", input_shape=(32, 32, 3))
ここから転移学習に関する部分に入っていきます。インポートしたVGG16をインスタンス化します。
include_topは、全結合分類器を含めるかどうかを指定するものでTrueにするとImageNetの1,000個のクラスへの分類器となりますが、今回新しい分類器を後で追加するのでFalseとします。
weightsは’imagenet’、input_shapeはCIFAR10の入力形状である(32, 32, 3)を指定しています。
新しい分類モデルを作成し、VGG16モデルから接続する
# ===== 新しい出力用分類モデルを作成する # VGG16の出力を平坦化して全結合層へ接続 x = layers.Flatten()(vgg16.output) x = layers.Dense(256, activation="relu")(x) x = layers.Dropout(0.5)(x) # 分類のために10ノードに接続 outputs = layers.Dense(10, activation="softmax")(x)
ここでは、新しい分類モデルを作成し、VGG16モデルの出力を接続しています。
まずはFlattenで平坦化し、その後256ノードの全結合層、ドロップアウト、分類のための10ノードの全結合層といった形で接続をしています。
モデルの作成と重みの凍結
# モデルの作成 model = keras.Model(inputs=vgg16.input, outputs=outputs) # 0~18までのlayerがVGG16に関連するので重みを固定する for layer in model.layers[:19]: layer.trainable = False # モデル構成の表示&画像保存 print(model.summary()) keras.utils.plot_model( model, "transfer_learning_vgg16_cifar10.png", show_shapes=True )
モデルの作成はkeras.Modelにインプットとアウトプットを指定します。inputsはvgg16.inputで、outputsは先ほど作成した分類器の出力を指定します。
model.layersに各層が保存されているわけですが、0~18についてはVGG16に関する層です。この部分の重みは凍結(固定)したいので、for文を使ってtrainableをFalseと設定します。summaryでモデルを表示していますが、以下のようにVGG16の部分の重みがNon-trainable paramsになることが分かります。
Total params: 14,848,586 Trainable params: 133,898 Non-trainable params: 14,714,688
また、keras.utils.plot_modelでモデルの可視化をしています。block5_poolまでがVGG16のモデルで、それ以降が今回個別に追加した部分です。
モデルのコンパイル
# ===== オプティマイザ、損失関数、指標を設定してコンパイル # 転移学習の場合、最適化関数はSGDの選択がよいとされている model.compile( optimizer=SGD(learning_rate=1e-4, momentum=0.9), loss="sparse_categorical_crossentropy", metrics=["accuracy"], )
モデルのコンパイルはcompileを使用します。optimizer、loss、metricsでそれぞれ指定します。
転移学習をする場合、最適化(optimizer)は、確率的勾配降下法(SGD)を選択するのがよいとされています。ただし、必ずしも最適とは限りませんので色々な方法を試してみてください。
モデルの訓練(学習)
# ===== fitを使ったモデルの訓練 # 設定 num_epochs = 10 callbacks = [ keras.callbacks.ModelCheckpoint("vgg16_cifar10.keras", save_best_only=True) ] # 訓練の実行 history = model.fit( train_imgs, train_labels, epochs=num_epochs, batch_size=32, validation_data=(val_imgs, val_labels), callbacks=callbacks, )
モデルの訓練はfitで実行します。今回はエポックは10としましたが、数字は変更して試してみてください。評価用のデータとしてvalidation_dataを指定しています。
また、callbackを使ってチェックポイントとしてモデルを保存しています。save_best_only=Trueとすることで最もよいモデルのみ保存します。保存したモデルを読み込んで使う場合は例えば以下のように使用することができます。
import numpy as np from tensorflow import keras from tensorflow.keras.datasets import cifar10 def main(): _, (test_imgs, test_labels) = cifar10.load_data() model = keras.models.load_model("vgg16_cifar10.keras") # ===== 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()
訓練(学習)状況の可視化
# ===== history情報の可視化 # 損失関数(loss)の履歴 loss = history.history["loss"] val_loss = history.history["val_loss"] # 正解率(accuracy)の履歴 acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] # 損失関数の履歴描画 x_epoch = range(1, num_epochs + 1) plt.plot(x_epoch, loss, "r", label="training loss") plt.plot(x_epoch, val_loss, "b", label="validation loss") plt.xlabel("Epochs") plt.ylabel("Loss") plt.legend() # 正解率の履歴描画 plt.figure() plt.plot(x_epoch, acc, "r", label="training acc") plt.plot(x_epoch, val_acc, "b", label="validation acc") plt.xlabel("Epochs") plt.ylabel("Accuracy") plt.legend() plt.show()
fitは返却値として、損失関数や指標の推移(history)を返却します。上記の部分では、historyの中に保存されている損失関数(loss)の履歴と正解率(accuracy)の履歴を取得してきて、matplotlibのplotで描画しています。なお、val_xxxとなっているものについては、評価データに対する値になります。
テストデータでの評価及び予測
# ===== 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]}")
上記の部分はテストデータを使った評価と予測に関する部分です。テストデータを使った評価はevaluateを用いて実行できます。結果は[損失(loss), 正解率(accuracy)]という形で返ってきます。
また、予測結果はpredictを用いて実行できます。今回のモデルは各文字に対する確率値が計算されます。そのため、予測結果としては確率が最も高い値となるため、argmaxで取得しています。
以上が、TensorFlow/Kerasを用いて訓練済みモデルのVGG16を用いた転移学習の実装例です。
まとめ
VGG16という訓練済みモデルを用いて、転移学習する実装例について紹介しました。データセットとしてはCIFAR10を用いています。
VGG16は最新のモデルというようなものではありませんが、転移学習のイメージをつかむためにはよく説明に使用されるものとなっています。KerasにはVGG16の他にもResNetやDenseNet等の他の訓練済みモデルも用意されています。
是非色々な問題で訓練済みモデルを用いた転移学習を試してみてもらいたいと思います。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。