TensorFlow

【TensorFlow/Keras】訓練済みモデルVGG16での転移学習

【TensorFlowKeras】訓練済みモデルVGG16での転移学習

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]
VGG16 転移学習 損失値
VGG16 転移学習 正解率

精度は約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%を検証データとして抜き出しています。

Note

【検証データとテストデータを分ける理由】

ディープラーニングの場合には、検証データとテストデータを明確に分けます。というのも、検証データを使ってパラメータチューニングを行ううちに検証データセットを過学習する結果になることがあるためです。これは、情報の漏れと言われます。そのため、最終的には訓練に全く関わっていないテストデータセットで評価します。

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のモデルで、それ以降が今回個別に追加した部分です。

VGG16を用いた転移学習

モデルのコンパイル

    # ===== オプティマイザ、損失関数、指標を設定してコンパイル
    # 転移学習の場合、最適化関数はSGDの選択がよいとされている
    model.compile(
        optimizer=SGD(learning_rate=1e-4, momentum=0.9),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )

モデルのコンパイルはcompileを使用します。optimizer、loss、metricsでそれぞれ指定します。

Note

転移学習をする場合、最適化(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等の他の訓練済みモデルも用意されています。

是非色々な問題で訓練済みモデルを用いた転移学習を試してみてもらいたいと思います。

Pythonによるディープラーニング」はTensorFlow/Kerasの中~上級者向けの本ですが非常におすすめです。CNN, RNN, Transformer, GAN等高度なモデルも扱っており面白い&TensorFlow/Kerasの実装力をつけることができます。