ちょっと話題の記事

Tensorflowの転移学習サンプルを機械学習の初心者がギリ分かるところまで噛み砕いてみた

2020.05.14

せーのでございます。

機械学習というワードはかなり一般化され、エンジニアじゃない方なら「ああ、自動運転とかのやつでしょ」くらいに浸透しています。
特にエンジニアの方であれば「教師あり学習」「教師なし学習」「強化学習」がどういうものを指すか、というのはぼんやりイメージできるかと思います。

そんな機械学習、せっかくなので始めてみたい、とざっくり中身を見出した、、、くらいの方が今日のエントリーの読者対象となります。

今日のテーマは「転移学習」です。

転移学習のやり方を知りたい。最短で。

転移学習、というのはざっくり言うと「元々学習されているモデルを使って自分たちの使いたい方向に再学習すること」です。

機械学習をやりだすと必ず当たる壁が「データが足りない」というものです。特にディープラーニングを使って例えば画像の分類をしたい、とした場合、精度を出すには最低でも数百枚、一般的には数千枚〜数十万枚という画像が必要となります。その画像(データセットと言います)を用意するのが大変なのです。

そこで考え出された方法が「転移学習」です。例えば公園に咲いている花の写真を撮って、それがチューリップなのかヒヤシンスなのかを見分けたい、とします。転移学習を使えば、既にある「花を分類する学習モデル」をベースにしてチューリップとヒヤシンスの画像を追加で学習されればそれぞれ数十枚くらいの写真でもモデルが完成します。便利で実用的ですよね。

ですがいざ実際にやってみようとすると、転移学習の「説明」は色々なサイトに出てくるのですが、転移学習のサンプルはなかなか見つかりません。更にようやくサンプルを見つけても数年前のものでライブラリが古くなっていたり、サンプルの解説が難解だったりして、なかなか理解するのに時間がかかります。

そこで今回はTensorFlowが公開している転移学習のチュートリアルサンプルを使って、なるべく引っかかりそうな部分を噛み砕いてみたいと思います。

Transfer learning with a pretrained ConvNet

それでは早速見ていきましょう。

ちなみにこの資料はnotebookとしても使えるようになっており、Google Colabのボタンをクリックすると試しながらすすめることができます。Google ColabとはGCPのサービスの一つで、notebook形式のものをクラウド上でサクッと動かせます。notebookというのは、簡単に言うとmarkdown形式のWikiとソースコードの実行環境が合わさったようなものです。説明部分は読み物として読み、コード部分は実行ボタンを押すことで実際に動かせます。

ちなみにタイトルの「ConvNet」とは日本語で「畳み込みニューラルネットワーク(convolutional neural networks/CNN)」と言われるもので、画像などを学習する時最適なディープラーニングの手法の一つです。

やること

このチュートリアルでは「MobileNet V2」という1000種類の様々な画像を分類する学習モデルを転移学習させて犬と猫の画像を見分ける学習モデルを作ります。

大まかな流れとしては

  1. 犬と猫のデータセットを用意する
  2. MobileNet V2を読み込み、その特徴だけを使って犬と猫のデータセットを学習する
  3. 犬と猫を見分けやすくするためにパラメータを微調整する

となります。

ここからは実際のコードのうち、引っかかりそうな部分だけ取り上げます。全体のコードを参考にしたい方はこちらを参照してください。

犬と猫のデータセットを用意する

まず犬と猫のデータセットを用意します。チュートリアルではTensorFlowが事前に用意してあるデータセットがあるので、それを使います。

(raw_train, raw_validation, raw_test), metadata = tfds.load(
    'cats_vs_dogs',
    split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
    with_info=True,
    as_supervised=True,
)

読み込む時にデータセットを「train」「validation」「test」に分けています。機械学習のモデルを作成する時は用意したデータセットの80%くらいをモデルを学習するために使い、残りの10%のデータをそのモデルの検証用、最後の10%をテスト用として持つことが一般的です。

train, validation, testの3つのデータセットの中身を見てみます。

print(raw_train)
print(raw_validation)
print(raw_test)

# ----------------
# <DatasetV1Adapter shapes: ((None, None, 3), ()), types: (tf.uint8, tf.int64)>
# <DatasetV1Adapter shapes: ((None, None, 3), ()), types: (tf.uint8, tf.int64)>
# <DatasetV1Adapter shapes: ((None, None, 3), ()), types: (tf.uint8, tf.int64)>
# ----------------

shapes, というのは読み込んだデータセットの枠のようなイメージです。画像の学習の時には「縦, 横, チャンネル数」という3次元の行列で表します。
画像のチャンネル数は1か3で表され、1の場合はグレースケール(白黒)、3はRGBカラーを表します。例えば

この色はRGB的には(R252 G157 B184)となるのでこの一番左上の端っこのピクセル(X:0, Y:0)の値は[[0], [0], [252, 157, 184]]という感じで表されます。

縦と横の値がNoneになっているのは、この後動的に指定するように空けているからです。

次にそれぞれの画像データを160x160にリサイズします。

IMG_SIZE = 160 # All images will be resized to 160x160

def format_example(image, label):
  image = tf.cast(image, tf.float32)
  image = (image/127.5) - 1
  image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
  return image, label
  
train = raw_train.map(format_example)
validation = raw_validation.map(format_example)
test = raw_test.map(format_example)

データをシャッフルしてバッチ処理します。

BATCH_SIZE = 32
SHUFFLE_BUFFER_SIZE = 1000

train_batches = train.shuffle(SHUFFLE_BUFFER_SIZE).batch(BATCH_SIZE)
validation_batches = validation.batch(BATCH_SIZE)
test_batches = test.batch(BATCH_SIZE)

for image_batch, label_batch in train_batches.take(1):
   pass

image_batch.shape

# ----------------------
# TensorShape([32, 160, 160, 3])
# ----------------------

バッチ処理とは、大きなデータセットをいっぺんに学習させると効率が悪いので、小さいかたまりに区切って学習させることです。この例では32ごとのバッチに区切ります。
区切った結果train, validation, testそれぞれのバッチの中身は[32, 160, 160, 3]となりました。これはつまり縦160, 横160の画像の一つ一つのR(赤), G(緑), B(青)の値を格納したものが32個あるよ、ということです。

MobileNet V2を読み込む

データセットの準備ができたので、転移元となるMobileNet V2の学習モデルを読み込みます。

IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)

# Create the base model from the pre-trained model MobileNet V2
base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
                                               include_top=False,
                                               weights='imagenet')

読み込む時に引数としてinclude_top=False、と指定しています。これは一番上の層は読み込まない、ということを指しています。機械学習は下から上にフィルタリングされていくのですが、下の方が「こういう色のもの」「エッジがここ」のような汎用的なフィルターで、上に行くほどどんどん細かい内容の分類が行われます。ですので転移学習では元の学習モデルの一番上の層以外の部分を読み込んで、そこに新しい特徴のフィルター、今回で言えば犬と猫を見分けるフィルターを加えて再学習させます。

読み込んだモデルに先程バッチ処理をしたtrainのデータセットを入れてみます。

feature_batch = base_model(image_batch)
print(feature_batch.shape)

# ----------------------
# (32、5、5、1280)
# ----------------------

モデルは先程の(160, 160, 3)のデータを5x5x1280の特徴量のブロックに置き換えます。結果、置き換えたfeature_batchは(32, 5, 5, 1280)となります。

特徴を抽出して再学習させる

次にMobileNet V2の特徴抽出部分のみを使い、上に2つの層を追加してスタック(重ね合わせ)します。

base_model.trainable = False

global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
feature_batch_average = global_average_layer(feature_batch)
print(feature_batch_average.shape)

# --------------
# (32, 1280)
# -------------

prediction_layer = tf.keras.layers.Dense(1)
prediction_batch = prediction_layer(feature_batch_average)
print(prediction_batch.shape)

# --------------
# (32, 1)
# -------------

model = tf.keras.Sequential([
  base_model,
  global_average_layer,
  prediction_layer
])

まず最初にbase_model.trainable = False、と指定して元の学習モデルの重み付けを固めます。こうすることで再学習しても元のモデルの特徴が維持されます。次にglobal_average_layer、prediction_layerという2つの層を追加します。global_average_layerは元の学習モデルの特徴量を平均化する層です。元の学習モデルの特徴量は(5, 5, 1280)でしたので、この5x5分のデータを平均化して1280の特徴にまとめる、ということです。

prediction_layerはDenseという関数を使って最終的に全ての要素を結合させた値を出すものです。
通常Denseを使うときには引数としてActivation、活性化関数というものを引数に入れます。通常は活性化関数(ここで使っているKerasというライブラリではSoftmax関数を使う)を使うことでそれぞれの予測データ、今回で言えば犬はこれくらい、猫はこれくらい、という個別のデータをあわせて、犬が何%、猫が何%、という形に変換しますが、今回は生のデータとして扱うので入っていません。

後はモデルをコンパイルして再学習させます。結果はこのようになっています。

Accuracyというのはどれだけ正確か、を表しています。Cross Entropyというのはloss、損失関数の値を表しています。損失関数、というのは正しい値と現実の値の差のことを指します。今回で言えば、例えば犬が答えで予測した結果が犬であれば損失関数は0となります。つまりこの値が低ければ低いほど正確だ、ということです。Cross Entropyとはその損失関数を出す種類の一つで、日本語では交差エントロピー法と言われます。

微調整する

最後にこの精度を更に上げるために微調整します。微調整方法はベースとなっている元の学習モデル、base_modelのうちいくつかの重み付けを再学習させます。これをファインチューニング、と言います。

base_model.trainable = True

# Let's take a look to see how many layers are in the base model
print("Number of layers in the base model: ", len(base_model.layers))

# --------------------
# Number of layers in the base model:  155
# --------------------
 

# Fine-tune from this layer onwards
fine_tune_at = 100

# Freeze all the layers before the `fine_tune_at` layer
for layer in base_model.layers[:fine_tune_at]:
  layer.trainable =  False
 

まず最初にbase_modelの層の数を数えています。MobileNet V2の層の数は155でした。そのうち100までの重み付けはそのままで、残りの55層の重み付けを再学習することで調整させます。

再コンパイルして学習させると、こうなりました。

だいぶ精度が高まりました。ただ犬と猫のデータセット数が少ないため、あまりこの損失関数の値が低すぎると過学習(オーバーフィッティング)の可能性も出てきます。過学習とは、学習しすぎて「犬と猫の画像を見分ける」というより「データセットにある画像を見分ける」ことに特化しすぎる形になってしまい、準備した画像ではかなり高い精度が出るのに、それ以外の画像ではガクンと精度が落ちる現象を言います。過学習になると改善させるのは大変なので注意が必要です。

あとはこのモデルを推論のAPIに突っ込んで上げればOK、となります。

まとめ

ということで転移学習のサンプルをなるべく細かく解説してみました。機械学習を始めたばかりの方はなんとなくイメージがつかめましたでしょうか。
転移学習はとても実用性が高い学習法だと思いますので、イメージを掴んだらどんどん試してみるのが良いと思います。

参考リンク