ファインチューニング
本章では、ディープラーニングで多く用いられるファインチューニング (fine tuning) について学んでいきます。ファインチューニングとは、異なるデータセットで学習済みのモデルに関して一部または全部を再利用して、新しいモデルを構築する手法です。モデルの構造とパラメータを活用し、特徴抽出器としての機能を果たします。手持ちのデータセットのサンプル数が少ないがために精度があまり出ない場合でも、ファインチューニングを使用すれば、性能が向上する場合があります。
類似した用語として転移学習 (transfer learning) がありますが、学習済みモデルをそのまま使用するか、一部を使用するかで異なります。
本章の流れは、
- データを準備して理解する
- 自作した分類モデルで学習と評価
- 事前学習済みモデルの重みを読み込んで学習と評価
上記のように、一度自作のモデルを組んで結果を確認した後に、ファインチューニングをすればどの程度精度が向上するかを見ていきます。
本章の構成
- 画像分類
- ファインチューニング
画像分類
前章で扱った手書き文字である MNIST の分類は簡単なネットワークの定義でもある程度の正解率が得られますが、少し難易度をあげた問題設定で試してみましょう。CIFAR10 と呼ばれる以下のような 10 クラスの分類を行います。CIFAR10 も MNIST と同様に、tf.keras.datasets
にデータセットが用意されています。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2
import tensorflow as tf
# GPU が使用可能であることを確認
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())
データセットの準備
MNIST と同じようにデータセットを準備しましょう。
# データセットの読み込み
train, test = tf.keras.datasets.cifar10.load_data()
こちらのデータセットも MNIST と同じデータセット構成をしています。これまでの流れを参考に TensorFlow で扱えるデータ形式まで変換していきましょう。
# 画像の情報
train[0].shape, test[0].shape
# ラベルの情報
train[1].shape, test[1].shape
# 学習用データセットとテスト用データセットに対して正規化
x_train = train[0] / 255
x_test = test[0] / 255
# 目標値の切り分け
t_train = train[1]
t_test = test[1]
# 32bit にキャスト
x_train, x_test = x_train.astype('float32'), x_test.astype('float32')
t_train, t_test = t_train.astype('int32'), t_test.astype('int32')
CNN モデルの定義
前章の MNIST で 97% を出したモデルでもう一度試してみましょう。どれくらい精度を出すことができるでしょうか。CNN モデルの概要を再度掲載しておきます。
import os, random
def reset_seed(seed=0):
os.environ['PYTHONHASHSEED'] = '0'
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
画像サイズが変わっていることに注意してください。MNIST では (28, 28, 1) でしたが、CIFAR10 では (32, 32, 3) になります。
from tensorflow.keras import models,layers
# シードの固定
reset_seed(0)
# モデルの構築
model = models.Sequential([
# 特徴量抽出
layers.Conv2D(filters=3, kernel_size=(3, 3), activation='relu', input_shape=(32, 32, 3)),
layers.MaxPool2D(pool_size=(2, 2)),
# ベクトル化
layers.Flatten(),
# 識別
layers.Dense(100, activation='relu'),
layers.Dense(10, activation='softmax')
])
モデルの定義が完了しました。summary()
メソッドでパラメータを確認します。
model.summary()
目的関数と最適化手法の選択
今回は最適化の手法に Adam を、目的関数は分類の問題設定のため sparse categorical crossentropy を使用します。
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=0.01)
# モデルのコンパイル
model.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
モデルの学習
バッチサイズ、エポック数を定義して、モデルの学習を実行します。
# モデルの学習
batch_size = 4096
epochs = 30
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, t_test))
10 クラスの分類であるため、ベースラインとなる正解率が 10% (=1/10) とすると、正解率を約 48 % 程度までは上昇させることができていますが、それでもまだ正解率は高いとは言えない結果になっています。
ファインチューニング
冒頭で説明した通り、ファインチューニングを実装します。学習済みモデルは世界中で公開されており、それぞれのタスクに合わせて学習済みモデルを使用する必要があります。
今回は、世界最大の画像認識コンペティション ImageNet Large Scale Visual Recognition Competition (ILSVRC) で使用されている 1000 クラスの物体を分類するタスクの学習済みモデルを利用します。この ILSVRC の学習済みモデルはフレームワーク側で用意されていることが多く、簡単に使い始めることができます。まずは、この学習済みモデルで試してみると良いでしょう。また、ネットワークの構造は VGG16 というモデルを使用します。今でも幅広く活用されているモデルです。
学習済みモデルは tf.keras.applications
以下に置かれています。今回使用するモデル以外にも画像分類モデルであれば、
- VGG16, 19
- ResNet
- DenseNet
- Inception v3
- MobileNet v2
- NASNet
などがあります。
最初に、ImageNet で学習された重みを持った VGG16 モデルをインスタンス化します。インスタンス化するときに、include_top=False
とすることで、全結合層レイヤーを除くことができます。全結合層のレイヤーは、識別するためのレイヤーであり、ImageNet では 1000 クラス分類ですが、今回は 10 クラス分類なので最終層に全結合層を自分の問題設定に合うように追加します。学習済みの重みを持ったモデルは、特徴抽出器として役立ちます。また、学習済みの重みを使うためには weights='imagenet'
とします。
- ファインチューニングする際の注意点
include_top=False
とすると事前学習済みの全結合層を除くweights='imagenet'
とすると事前学習済みの重みを引き継ぐ
それでは実装していきます。
from tensorflow.keras.applications import resnet, VGG16
# 学習済みモデルをインスタンス化
base_model = VGG16(input_shape=(224, 224, 3),
include_top=False, weights='imagenet')
base_model.summary()
モデルの概要を確認したとおり、最終層が MaxPooling
で終わっています。こちらは include_top=False
にしたためです。
それではデータの準備から進めていきたいのですが、今回画像サイズを大きくしますので 50,000 枚すべてのデータを使用すると膨大な量の演算が必要になってきます。そのため、今回は学習用データセットを 10,000 枚、テスト用データセットを 5,000 枚にして実装していきます。
今回のようにランダムに整数で値を取得したい場合に便利なのが、np.random.randint()
関数です。np.random
モジュール内のその他の乱数生成方法についてはこちらを確認してください。引数には、
- low : 最小値
- high : 最大値
- size : 配列のサイズ
があります。例えば今回の学習用データセットから 10,000 枚の画像を取り出したい場合には np.random.randint(0, 50000, 10000)
と実行します。
# ランダムにデータを取得する
train_choice = np.random.randint(low=0, high=50000, size=10000)
test_choice = np.random.randint(low=0, high=10000, size=5000)
# データの準備
x_train = train[0][train_choice]
x_test = test[0][test_choice]
t_train = train[1][train_choice].astype('int32')
t_test = test[1][test_choice].astype('int32')
また、VGG16 では画像サイズ (224, 224, 3) の画像で事前学習されているため、データサイズを変更(リサイズ)する必要があります。それぞれの事前学習済みモデルを使用する際にも基本的には学習時と同サイズにする必要があります。
リサイズ方法にはいくつも種類がありますが、今回は OpenCV の resize()
を使用します。引数に画像 (src
) とリサイズ後のサイズ (dsize
) を指定しましょう。
_train, _test = [], []
# 画像サイズを 224 × 224 にリサイズしてリストに格納
for img in x_train:
_train.append(cv2.resize(src=img, dsize=(224, 224)))
for img in x_test:
_test.append(cv2.resize(src=img, dsize=(224, 224)))
# リストから ndarray に変換し、正規化
x_train = np.array(_train, dtype='float32') / 255.0
x_test = np.array(_test, dtype='float32') / 255.0
x_train.shape, x_test.shape
全結合層を追加
テンソルからベクトルに直す操作として、Flatten
を使いますが、学習すべきパラメータ数が膨大に増加してしまいます。その代わりによく使われるレイヤーとして、GlobalAveragePooling2D
があります。頭文字を取って GAPと呼ぶことも覚えておきましょう。
もしも最終層の特徴マップのサイズが (7, 7, 512)
であれば、Flatten
を使用すると、7×7×512 のベクトルになります。GlobalAveragePooling2D
を使えば、特徴マップのチャネルごとの平均値を出力してくれるので、 ベクトルになります。 のパラメータ削減になります。
そして、10 クラス分類なのでノード数 10 の全結合層も最終層に追加します。
reset_seed(0)
# モデルの定義
finetuned_model = models.Sequential([
base_model,
layers.GlobalAveragePooling2D(),
layers.Dense(512, activation='relu'),
layers.Dense(10, activation='softmax')
])
モデルをコンパイルします。最適化手法、目的関数、監視する評価指標を設定しましょう。今回は最適化手法に SGD を使用してみます。
optimizer = tf.keras.optimizers.SGD(lr=0.01)
# モデルのコンパイル
finetuned_model.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
finetuned_model.summary()
モデルの学習
学習させる前に、一度学習前のモデルでテスト用データセットに対して順伝播させてみます。学習後のモデルと比較するためです。順伝播する方法は、evaluate()
で行うことができます。
loss, accuracy = finetuned_model.evaluate(x_test, t_test)
print(f'loss : {loss}, acuracy : {accuracy}')
正解率は 9% 程度です。ここから 10 エポックでどの程度正解率が向上するのか見ていきましょう。
# モデルの学習
history = finetuned_model.fit(x_train, t_train,
epochs=10,
batch_size=32,
validation_data=(x_test, t_test))
予測精度の評価
学習結果を確認します。
results = pd.DataFrame(history.history)
results.tail(3)
# 損失を可視化
results[['loss', 'val_loss']].plot(title='loss')
plt.xlabel('epochs');
# 正解率を可視化
results[['accuracy', 'val_accuracy']].plot(title='accuracy')
plt.xlabel('epochs');
上記の結果の通り、わずか 10 エポック分の学習で正解率を 86% まで高めることができました。さらなる学習やハイパーパラメータの調整などでさらに正解率を高められることが期待できます。ぜひ、挑戦してみてください。