本記事では、TensorFlow で用意されたモデルを使用し、転移学習を行う方法について解説します。
今回は、VGG16モデルをベースに、メガネの形状を識別出来る様な新しいモデルを構築していきます。
学習済みモデルとは(復習)
以前、我が家の犬の犬種を識別するプログラムを実装しました。
あの時使用したのは TensorFlow にあらかじめ用意されている VGG16 というモデルでした。
このモデルにはすでに犬のデータを用いた学習が行われており、犬のデータを識別可能な状態にあります。
この様に、対象データについての学習を既に終えているモデルのことを ” 学習済みモデル(Pre-Trained Model) ” と呼びます。
画像の識別を行いたいと思った時、既に学習済みのモデルが存在するのであればモデル構築のプロセスを殆どスキップ出来るため、モデル構築に当たっては先ず検討したい方法です。
学習していない画像に対する反応
ちなみに、未学習の画像に対するモデルの反応を見ておきたいと思います。
犬種判定にめっぽう強いVGG16さんに、メガネの画像を渡してみます。
1 2 3 4 5 6 7 8 9 10 |
------ 画像 1 枚目 ------ 予測分類名 : hand_blower 分類確率 : 0.1611287146806717 予測分類名 : whistle 分類確率 : 0.13903774321079254 予測分類名 : pick 分類確率 : 0.07396072149276733 |
送風機、あるいは笛…?
まるっきり、判定できていませんね。彼にとってこれはまるっきり未知の画像ですから、しょうがないです。
転移学習とは
識別を行いたい画像に対して学習を行なっている様なモデルが世に存在しないときは、自分でモデルを構築しなければなりません。
しかしそんな時でも、既に存在するモデルを流用することが出来ます。
その方法が、” 転移学習 ( transfer learning ) ” です。これは既に存在するモデルのネットワークはそのままに、全結合層のみを新規に作成し再学習させるという方法です。
例えば先に例を出した VGG16モデル は、対象画像の特徴量を適切に抽出出来る様なネットワークを保持しています。ただ、取得した特徴量と新規画像との紐付けが出来ないだけです。
なので、VGG16モデルをベースにしつつ、特徴量とカテゴライズの機能だけを刷新してやれば新規の画像に対しても識別可能なモデルが完成する、という考え方です。
今回はこの考え方に則って、メガネの画像に対する画像識別を行なっていきたいと思います。
目的
タイトルにある通り、今回の目的はメガネの形状を識別することです。
具体的な仕様は次の通りです。
[ モデル ]
VGG16モデルをベースに転移学習を行なったもの
[ 機能 ]
与えられたメガネの画像に対して形状識別を行い、以下のいずれかの形状であるかを判定する。
・丸メガネ(round_glasses)
・スクエアメガネ(square_glasses)
・ウェリントン型メガネ(wellington_glasses)
[ 画像 ]
以下の画像を用意する。
・丸メガネ…110枚
・スクエアメガネ…110枚
・ウェリントン型メガネ…110枚
なお、上記画像を以下の様に分割する。
・訓練用データ…各6割
・検証用データ…各3割
・テスト用データ…各1割
実装
ラベリング
実装に入る前に、画像データのラベリングについてです。
今回は keras の ImageDataGenerator クラスのメソッド flow_from_directory を使用しますが、こいつは指定ディレクトリ内の画像をディレクトリ名でラベル付けしてくれるという優れものです。
ですので、各ディレクトリには round_grasses などのラベル名を付けた上で画像を格納しています。
| — train — | — round_glasses
| | — square_glasses
| | — wellington_glasses
| — validate — | — round_glasses
. | — square_glasses
. | — wellington_glasses
ちなみに各ディレクトリの下には、このような画像がたくさん入っています。
画像を集めるのが一番大変でした。
- round_glasses
- square_glasses
- wellington_glasses
スクエアなメガネとウェリントンのメガネの識別が、難しそうですね。
実装
では早速実装に入っていきます。今回はちょっと長いので、分割しながらみていきます。
- モデル構築用の関数を定義します。15層目までの重みを固定(再学習させない)した上で、全結合層を追加します。今回は3クラス分類なので、最終出力は3次元です。
123456789101112131415161718192021222324252627282930313233from tensorflow.python.keras.models import Sequentialfrom tensorflow.python.keras.layers import Dense, Dropout, Flatten# モデルを構築するdef build_transfer_model(vgg16):# VGG16を呼び出し、操作可能にするためにシーケンシャル・モデルに落とし込むmodel = Sequential(vgg16.layers)# 15層目までの再学習をオフにするfor layer in model.layers[:15]:layer.trainable = False# *** 全結合層を新たに追加する ***# Flatten--入力を平滑化するmodel.add(Flatten())# Dense --通常の全結合レイヤー## 出力256次元model.add(Dense(256,activation='relu'))# Dropout--入力にドロップアウトを適用し、過学習を防ぐmodel.add(Dropout(0.5))# Dense## 出力3次元model.add(Dense(3,activation='softmax'))# モデルを返却するreturn model - VGG16モデルを呼び出します。全結合層はimportしません。
123456from tensorflow.python.keras.applications.vgg16 import VGG16# VGG16モデルを呼び出す。全結合層は新たに定義するため、include_topをfalseにするvgg16 = VGG16(include_top = False,input_shape = (224, 224, 3))
- VGG16をベースに、モデルを再構築します。modelが4なのは、3回失敗したからです…。
123456789101112131415161718import osimport jsonimport picklefrom datetime import datetimefrom tensorflow.python.keras.optimizers import SGDfrom tensorflow.python.keras.preprocessing.image import ImageDataGeneratorfrom tensorflow.python.keras.applications.vgg16 import preprocess_inputfrom tensorflow.python.keras.callbacks import ModelCheckpoint, CSVLogger# モデルを呼び出すmodel4 = build_transfer_model(vgg16)# モデルをコンパイルするmodel4.compile(loss='categorical_crossentropy',optimizer=SGD(lr=1e-4, momentum=0.9),metrics=['accuracy'])
- ジェネレータを定義します。今回は訓練用画像用のジェネレータに、データ拡張の設定を施します。
123456789101112131415# ジェネレータを定義する# 訓練用ジェネレータの定義## データ拡張有りtrain_gen = ImageDataGenerator(width_shift_range = 0.2,shear_range = 0.2,zoom_range = 0.2,rescale = 1.0 / 255)# 検証用ジェネレータの定義## データ拡張無しvalidate_gen = ImageDataGenerator(rescale = 1.0 / 255) - ジェネレータを使用して、画像に対するイテレータを作成します。用意しておいた画像を格納しているディレクトリを指定します。この際、イテレータ先の画像を読み込む際のバッチサイズなどを指定します。
12345678910111213141516171819# 指定ディレクトリから画像を読み込み、データ拡張しつつラベリングを行う。# 訓練用train = train_gen.flow_from_directory('pic/glasses/all_glasses/train/',target_size = (224, 224),batch_size = 10,class_mode = 'categorical',shuffle = True)# 検証用validate = validate_gen.flow_from_directory('pic/glasses/all_glasses/validate/',target_size = (224, 224),batch_size = 10,class_mode = 'categorical',shuffle = True) - モデル、ネットワーク、ログの保存設定をします。
12345678910111213141516171819202122232425262728293031323334353637383940# モデル保存用ディレクトリの作成model_dir = os.path.join('saved_models',datetime.now().strftime('%y%m%d_%H%M'))os.makedirs(model_dir, exist_ok=True)print('model_dir:{}'.format(model_dir))dir_weights = os.path.join(model_dir, 'weights')os.makedirs(dir_weights, exist_ok=True)print('weights_dir:{}'.format(dir_weights))# ネットワークの保存model_json = os.path.join(model_dir, 'model_json')with open(model_json, 'w') as f:json.dump(model4.to_json(), f)# 正解ラベルの保存model_classes = os.path.join(model_dir, 'classes_pkl')with open(model_classes,'wb') as f:pickle.dump(train.class_indices, f)# callbacksの設定cp_filepath = os.path.join(dir_weights, 'ep_{epoch:02d}_ls_{loss:.1f}.h5')cp = ModelCheckpoint(cp_filepath,monitor='loss',verbose=0,save_best_only=False,save_weights_only=True,mode='auto',period=5)csv_filepath = os.path.join(model_dir, 'loss.csv')csv = CSVLogger(csv_filepath, append=True) - 学習を実行します。
1 2 3 4 5 6 7 8 |
# モデルの学習 history = model4.fit_generator(train, epochs = 10, validation_data = validate, callbacks = [cp, csv], steps_per_epoch = 10, validation_steps = 10 ) |
出力ログ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Found 213 images belonging to 3 classes. Found 63 images belonging to 3 classes. model_dir:saved_models\190206_1623 weights_dir:saved_models\190206_1623\weights Epoch 1/10 10/10 [==============================] - 64s 6s/step - loss: 1.3052 - acc: 0.3837 - val_loss: 0.8725 - val_acc: 0.6237 Epoch 2/10 10/10 [==============================] - 64s 6s/step - loss: 1.0086 - acc: 0.5300 - val_loss: 0.6480 - val_acc: 0.7742 Epoch 3/10 10/10 [==============================] - 63s 6s/step - loss: 0.7760 - acc: 0.6275 - val_loss: 0.5232 - val_acc: 0.8495 Epoch 4/10 10/10 [==============================] - 65s 6s/step - loss: 0.6697 - acc: 0.7200 - val_loss: 0.5778 - val_acc: 0.8065 Epoch 5/10 10/10 [==============================] - 65s 7s/step - loss: 0.5397 - acc: 0.8300 - val_loss: 0.4731 - val_acc: 0.8172 Epoch 6/10 10/10 [==============================] - 62s 6s/step - loss: 0.5152 - acc: 0.7774 - val_loss: 0.5920 - val_acc: 0.6882 Epoch 7/10 10/10 [==============================] - 66s 7s/step - loss: 0.6405 - acc: 0.7300 - val_loss: 0.4795 - val_acc: 0.8280 Epoch 8/10 10/10 [==============================] - 63s 6s/step - loss: 0.5468 - acc: 0.7483 - val_loss: 0.3831 - val_acc: 0.9032 Epoch 9/10 10/10 [==============================] - 65s 7s/step - loss: 0.4991 - acc: 0.7800 - val_loss: 0.4034 - val_acc: 0.8925 Epoch 10/10 10/10 [==============================] - 65s 7s/step - loss: 0.4416 - acc: 0.7900 - val_loss: 0.4350 - val_acc: 0.8065 |
回数が増えるにつれ、損失が減少し正解率が上昇しているのがわかります。
識別の実行
ここまでで学習は終了したので、実際の画像を渡して画像の識別を行なってみましょう。
テスト用ディレクトリからランダムに画像を15枚抜き出し、それぞれをモデルに判定させ、各画像のタイトルに識別結果を付与するようにしています。
識別率は8割といったところなのでところどころ間違いはありますが、概ね正しく識別できていることが確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import os, random test_dir='pic/glasses/all_glasses/test/' files = os.listdir(test_dir) imgs = random.sample(files, 15) plt.figure(figsize=(15,10)) for i in range(15): img = image.load_img(test_dir + imgs[i], target_size=(224,224)) img = np.array(img) plt.subplot(3,5,i+1) plt.imshow(img) img = img[np.newaxis] pred = model4.predict(img) pred_num = np.argmax(pred) pred_name = '' for x in range(3): if train.class_indices.get('round_glasses') == pred_num: pred_name = 'round_glasses' elif train.class_indices.get('square_glasses') == pred_num: pred_name = 'square_glasses' else : pred_name = 'wellington_glasses' plt.title(pred_name) plt.show() |
やはり、スクエアなメガネとウェリントンメガネの識別が難しい様ですね…。
まとめ
- 未知の画像に対して画像識別可能なモデルを構築したい時には、既存のモデルを流用する 転移学習 という手法を取ることが出来る。
- データ拡張で画像の水増しは可能だが、ある程度の数は必要。(各画像20枚で学習させても上手く行かず、100枚で良い感じに)
- データ拡張はし過ぎてもいけないし、しなさ過ぎてもいけない。(記事外で、試行錯誤の過程がありました)
- 画像を集めて振り分けるのが一番面倒。
以上