All Articles

クソザコプログラマーが kerasを用いた転移学習に挑戦してみる

はじめに

これは半年ほど前,まだブログを始めていなかった頃に個人の備忘録として書いたものです. せっかくなので, 今日という日の勢いに任せて 記事にしたいと思います. 個人の備忘録なので,読み物としては適さないと思いますが,参考になれば幸いです.(なるかぁ…?) 少なくとも,よくわからない何かをやった気にはなれると思います.

実際にやったこと

訓練データ/評価データの準備

はじめに,画像データを用意します. 今回はcifar10のデータセットを利用します. kerasに用意されているdatasetsの中からcifar10を読み込みます. 他のデータセットを使いたい場合はそれに応じたモジュールを使用してください.

(x_train, y_train), (x_test, y_test) = datasets.cifar10.load_data()

cifar10は10のクラスに分類された画像データなので,ラベルとクラス名の対応付をするためにクラス名を定義しておきます.

class_list = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

これで0のラベルはairplaneのように,ラベルとクラス名の対応付ができます.

このままでは読み込んだ画像はメモリに乗りっぱなしになるので,二次記憶に退避させます. はじめに,画像データを保存するフォルダを作ります.

os.makedirs('data/train', exist_ok=True)
os.makedirs('data/test', exist_ok=True)

次に画像を保存します.

# Save images for training
for idx, (x_data, y_data) in enumerate(zip(x_train, y_train)):
    cid = y_data[0]
    os.makedirs(os.path.join('data/train', class_list[cid]), exist_ok=True)
    file_path = os.path.join('data/train', class_list[cid], '{:d}_{}_{:05d}.png'.format(cid,class_list[cid], idx))
    Image.fromarray(x_data).save(file_path)
# Save images for test
for idx, (x_data, y_data) in enumerate(zip(x_test, y_test)):
    cid = y_data[0]
    os.makedirs(os.path.join('data/test', class_list[cid]), exist_ok=True)
    file_path = os.path.join('data/test', class_list[cid], '{:d}_{}_{:05d}.png'.format(cid,class_list[cid], idx))
    Image.fromarray(x_data).save(file_path)

ファイル名はあくまで一例ですが,なぜこのような書き方をしているのでしょうか. 上記のコードの結果,ディレクトリ構成はこのようになります.(画像ファイルは省略)

data 
 ├── train
 │    ├── airplain
 │    ├── automobile
 │    ├── bird
 │    ├── cat
 │    ├── deer
 │    ├── dog
 │    ├── frog
 │    ├── horse
 │    ├── ship
 │    └── truck
 └── test
      ├── airplain
      ├── automobile
      ├── bird
      ├── cat
      ├── deer
      ├── dog
      ├── frog
      ├── horse
      ├── ship
      └── truck

ここで,保存しているのはあくまで「画像のみ」です. x_train, x_testに保存されていた内容です. このままでは正解ラベル(y_train, y_testの中身)は保存できません. 正解ラベルを保存する方法にはどのような方法があるでしょうか? ファイル名に含めるのもいい考えです. ここではクラス名ごとにディレクトリを作成することでわかりやすくしています.

次にモデルを作成します.

inputs = Input(shape=(32, 32, 3))
x = layers.Lambda(preprocess_input, name='preprocess')(inputs)

base_model = VGG16(weights='imagenet', include_top=False, input_tensor=x)

model = Sequential([
    base_model,
    layers.Flatten(),
    layers.Dense(10, activation='softmax'),
])

順を追って説明します. 先程,計算を行うため,画像を数字で表していると言いました.

一行目ではモデルのインプットとしてInputというオブジェクトを作成しています. また,その形は(32, 32, 3)です.

次に,学習させる前に画像の前処理を行います. ここでは,preprocess_inputという関数を実行する関数を層として設定しています. つまり,この層に入力されたデータはpreprocess_inputという関数に渡され,その戻り地が層の出力とされます.

今回はモデルの層の一つとして加えましたが,事前に別で前処理を行ったものを利用しても同じです.

さて,次の行でVVG16のモデルを定義しています. imagenetで学習させた【】層を含めずに定義しています.

次に,今回の学習で使うモデルの定義です. Sequentialとは「連続した」といったような意味で,引数として与えられた層を連続させたモデルを返します. はじめに,先程定義したVVG16の学習済みモデル,(次にすべての層をまとめて【】にする),次に全結合層を通して,出力を10次元にします. こうすることで,10のクラスそれぞれの確率が出力されることになります. モデルのサマリーを見てみましょう.

model.summary()
# Layer (type)                 Output Shape              Param #   
# =================================================================
# vgg16 (Model)                (None, 1, 1, 512)         14714688  
# _________________________________________________________________
# flatten_1 (Flatten)          (None, 512)               0         
# _________________________________________________________________
# dense_1 (Dense)              (None, 10)                5130      
# =================================================================
# Total params: 14,719,818
# Trainable params: 14,719,818
# Non-trainable params: 0
# _________________________________________________________________

次に学習済みの部分のパラメータを凍結し,全結合層のみのパラメータを学習するように設定します.

base_model.trainable = False
model.summary()

これを行うことによってTrainable paramsの値が大幅に減っているのが確認できます. 全結合層のパラメータ数と同じ数になっていますね.

Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 1, 1, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                5130      
=================================================================
Total params: 14,719,818
Trainable params: 5,130
Non-trainable params: 14,714,688
_________________________________________________________________

次に,モデルをコンパイルし設定を確定させます. このときに,【損失関数,評価指標を設定します.】

model.compile(optimizer=optimizers.RMSprop(lr=0.0001), loss='binary_crossentropy', metrics=['acc'])

これで訓練データを回して…と行きたいところですが大事なことが残っています.

10クラス分類をするので,モデルのアウトプットは10次元のデータです.ここで,y_train, y_testの次元数を見てみましょう.

print(y_train.shape, y_test.shape)

# (50000, 1) (10000, 1)

y_train, y_testは一次元のデータとなっています.このままでは,y_train, y_testを指定するとエラーとなります.

history = model.fit(x_train, y_train, epochs=10, batch_size=20)
> ValueError: Error when checking target: expected dense_1 to have shape (10,) but got array with shape (1,)

10次元のアウトプットをするモデルを作成したのに,正解データが一次元のままなので当然ですね. これを回避するには正解データを10次元のデータに変換すれば良さそうです.

from keras.utils import to_categorical
y_train_categ = to_categorical(y_train, 10)
y_train_categ

# [[0. 0. 0. ... 0. 0. 0.]
# [0. 0. 0. ... 0. 0. 1.]
# [0. 0. 0. ... 0. 0. 1.]
# ...
# [0. 0. 0. ... 0. 0. 1.]
# [0. 1. 0. ... 0. 0. 0.]
# [0. 1. 0. ... 0. 0. 0.]]

一番最後のデータで言えば,ラベル2である確率が1.0.それ以外である確率は0.0ということになります. ともあれこれで10次元のデータにすることができたので,いよいよ学習していきます.

history = model.fit(x_train, y_train_categ, epochs=1, batch_size=20)

画像が多すぎてメモリに乗り切らない…なんて場合は,画像が保存されたディレクトリからストリームを作成することもできます.

from keras.preprocessing.image import ImageDataGenerator
import numpy
gen = ImageDataGenerator().flow_from_directory('data/dataset', target_size=(32, 32), class_mode=None)
history = model.fit_generator(gen)

これは指定されたディレクトリに存在する画像をメモリに載せては開放し…を自動でやってくれる「ジェネレーター」を返します. 一度にすべてをメモリに乗せるわけではないので,メモリの少ない環境でも動作できるようになっています.(その分,スワップが発生するので時間はかかります.)

それでは訓練したモデルを使用して予測をしてみましょう.

preds = model.predict(x_test)
predicted_labels = numpy.argmax(preds,axis=-1)
metrics = model.evaluate(x_test, to_categorical(y_test, 10))

最後に

ここまで長々とお付き合いいただきありがとうございます. 自分の文章力の無さを痛感するばかりですが,ここでなんとこんな記事よりも多くの情報量を持ちながらこの記事よりも遥かにわかりやすく簡潔にまとめられているページがあるんです. 知りたいですよね?

こちらです.

結局は公式のドキュメントを読むのが一番早いし確実なんです. 今はGoogle TranslateやDeepLなんて便利なものもあるんので意外と読めますよ.

上記のページはちょっと…という方にはこちらもおすすめです.