python

keras初心者チュートリアル【No.2 転移学習】~CNNで車種判別モデルを作成~

このチュートリアルを一通り行うことで、

超初心者
超初心者
pythonってなに?ディープラーニングって美味しいの?

 

というレベルから

自分でモデル作れます!転移学習、ファインチューニングできます!

モデルの中身を可視化できます!

 

という状態になってもらえたらと思っています。

はじめに

本チュートリアルは、具体的な理解よりも、直感的な理解をして頂くことを目的としています。そのため、厳密性よりも、わかりやすさを優先した表現をすることをご了承願います。

本チュートリアルの対象者

友人
友人
  • if, for, 関数, 変数など基本的なプログラミングに関する知識は記憶の彼方にわずかにあるが、pythonおよびディープラーニングに関する知識は全く無い。
  • だけれども、ディープラーニングに興味がある!
  • 自力でモデルを作って評価して、またモデルを作ってという一連の流れを身につけたい!!

 

みたいな方にオススメです。

参考図書

チュートリアルを作成するに当たって参考にさせて頂いた本です。

この本は、個人的にかなりおすすめの本です。

理論的な部分と、実践的な部分が半々です。

しかも、コンピュータビジョン、自然言語、画像生成と幅広く対応しているので、いろんな分野を触って勉強してみたい!という方に向いていると思います。

本チュートリアルで扱う問題

「車の画像から、その画像に写っている車種を当てる」という問題を解くためにモデルを作成します。

本チュートリアルの構成

  1. 自作モデルで車種当て問題をといてみよう!
  2. 転移学習で車種当て問題をといてみよう!
  3. ファインチューニングで車種当て問題をといてみよう!
  4. 完成したモデルをテストデータで評価してみよう!
  5. 精度の高いモデルと低いモデルの違いを覗いてみよう!

 

本記事は2について解説をします。

全てのコードは僕のgithubに上がっているのでcloneをしていただければ実行することが可能です。

今回のコードはこちらです。

 

それでは、チュートリアルを始めます!

2. 転移学習で車種当て問題を解いてみよう!

2-1. 転移学習とは?

転移学習とは、学習済みモデルをベースに、最終の出力層を付け替えて学習させる手法です。

広くデータが取得できる領域で学習したモデルを、データの収集が困難な別の領域に適応させる、などの使い方が可能です。

イメージとしては、

めっちゃ勉強した人なら、初見の問題でも雰囲気が似てれば解けるでしょ!

というやつです。(雑すぎて怒られそう)

図にするとこんな感じです。

 

一般的に、画像から特徴を取り出す際、たたみ込み層の前半で輪郭や濃淡など外観の情報を、たたみ込み層の後半でこれはAudiぽい部分、TOYOTAぽい部分という抽象的な情報を抽出します。

大量の画像であらかじめ学習をしているモデルでは、外観の情報を抽出するためにフィルタの構造が上手にできていると考えることができます。

なので、転移学習では、すでにできあがっているたたみ込み層をそのまま用いて、

抽出された特徴から「車種当て問題専用の分類器」だけを新規に作成します。

 

2-2. ベースとなるデータセットは?

今回、モデルが学習をしたデータセットはImageNetという巨大なデータセットです。

ImageNetの特徴

  • 1,400万枚以上の画像
  • クラスは2万種類以上
  • 画像に写っている物体(クラス)はラベリング済み

このデータセットの中には、食べ物、動物、建築物もありますが、それだけではなく、スポーツカー、ワゴン車、戦車、航空機なども含まれているので、今回の問題に対して効果的であると考えます。

 

2-3. kerasで利用可能な学習済みモデル

https://keras.io/ja/applications/

これらを使用することができます。

この中から、転移学習に使用するモデルを決定します。

2-4. なぜVGG16を選択するの?

スライドにも記載しましたが、今回VGG16を選択した最大の要因は構造が自作モデルともっも似ていたいからです。

他のモデルに、より計算が速く、正解率の高いモデルも存在していますが、今回の目的は自作モデルとの比較なのでそちらを優先しました。

それでは、実装をしていきます。

2-5. VGG16モデルのたたみ込みベースのインスタンス化

from keras.applications import VGG16
IMAGE_SIZE = 256

conv_base = VGG16(weights='imagenet',
                 include_top=False,
                 input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3))

conv_base.summary()

Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 256, 256, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 256, 256, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 256, 256, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 128, 128, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 128, 128, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 128, 128, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 64, 64, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 64, 64, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 32, 32, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 32, 32, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 16, 16, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 8, 8, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________

2-6. データ拡張を行わない高速な特徴抽出

学習済みたたみ込みベースを使って特徴量を抽出します。

この方法ではモデルの学習はすぐに終わりますが、モデルの保存をしても入力として画像を受け取ることはできません。

今から作成するのは、分類器の部分のみです。

この方法はすぐに学習が終わりますが、ジェネレータを使用できないという欠点があるので注意が必要です。

結果をすぐに見たい場合には、今から説明する方法がオススメです。

その欠点を解消し、なおかつ、たたみ込み層と分類器が合体しているモデルの作り方は下で紹介します。

import os
import numpy as np
import pandas as pd
from keras.preprocessing.image import ImageDataGenerator


mini_metadata = pd.read_csv('mini_metadata.csv',index_col=0)
classes = list(mini_metadata["make_model"].value_counts().index)
classes = sorted(classes)
classes_num = len(mini_metadata.groupby("make_model"))

base_dir = 'mini_pictures'

train_dir = os.path.join(base_dir,'train')
valid_dir = os.path.join(base_dir,'valid')
test_dir = os.path.join(base_dir,'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 32

def extract_features(directory, sample_count):
    features = np.zeros(shape=(sample_count, 8, 8, 512)) #このshapeになる理由->block5_pool (MaxPooling2D)   (None, 8, 8, 512)
    labels = np.zeros(shape=(sample_count,classes_num)) #labelsのshapeについて確認
    generator = datagen.flow_from_directory(directory,
                                           target_size=(IMAGE_SIZE,IMAGE_SIZE),
                                           batch_size=batch_size,
                                           class_mode='categorical',
                                           classes=classes)
    i = 0
    for inputs_batch, labels_batch in generator:
        features_batch = conv_base.predict(inputs_batch)
        features[i*batch_size:(i+1)*batch_size] = features_batch
        labels[i*batch_size:(i+1)*batch_size] = labels_batch
        i += 1
        if i*batch_size >= sample_count:# ジェネレータはデータを無限ループで生成するため画像を一通り処理したらbreak
            break
    return features,labels
train_cnt = 0
valid_cnt = 0
test_cnt = 0
for _class in classes:
    train_label_dir = os.path.join(train_dir, _class)
    train_cnt += len([f for f in os.listdir(train_label_dir) if os.path.isfile(os.path.join(train_label_dir, f))])
    valid_label_dir = os.path.join(valid_dir, _class)
    valid_cnt += len([f for f in os.listdir(valid_label_dir) if os.path.isfile(os.path.join(valid_label_dir, f))])
    test_label_dir = os.path.join(test_dir, _class)
    test_cnt += len([f for f in os.listdir(test_label_dir) if os.path.isfile(os.path.join(test_label_dir, f))])
print(train_cnt,valid_cnt,test_cnt)

train_features, train_labels = extract_features(train_dir,train_cnt)
valid_features, valid_labels = extract_features(valid_dir,valid_cnt)
test_features, test_labels = extract_features(test_dir,test_cnt)

1912 657 663
Found 1912 images belonging to 20 classes.
Found 657 images belonging to 20 classes.
Found 663 images belonging to 20 classes.

 

これより、たたみ込み層から抽出した特徴を分類するモデルを作成します。

from keras import models 
from keras import layers 
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=8*8*512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(classes_num, activation='softmax'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
             loss='categorical_crossentropy',
             metrics=['acc'])

history = model.fit(train_features, train_labels,
                   epochs=100,
                   batch_size=32,
                   validation_data=(valid_features, valid_labels))

model.sumary()
model.save("models/VGG16_model_mini_1.h5")

Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_3 (Dense)              (None, 256)               8388864   
_________________________________________________________________
dropout_2 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 20)                5140      
=================================================================
Total params: 8,394,004
Trainable params: 8,394,004
Non-trainable params: 0
_________________________________________________________________


2-7. 結果をグラフで評価する

import matplotlib.pyplot as plt
%matplotlib inline

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

#正解率をプロット
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

#損失値をプロット
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validatin loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()


正解率 損失関数
訓練データ 1.000 0.0086
検証データ 0.9878 0.9878

検証データで正解率98%まで出ているので、現時点でかなり良いモデルになっていることがわかります。

過学習に関してもほとんどしていないと言って良いでしょう。

ですが、もしかしたらtestデータで3σまでの精度を出せという要求が出るかもしれませんので、さらに精度を高める手法について勉強をしていきましょう。

その準備として、入力を画像で受け取ることができる転移学習モデルを作成します。

2-8. 入力を画像で受け取ることができる転移学習モデル

こちらを先に作成しなかった理由は、モデルの学習に少し時間がかかってしまうからです。

行っていること自体は同じです。

3章で使用するので、是非実行をしてみてください!

訓練済みベースネットワークの最後にカスタムネットワークを追加する

from keras.layers import Dense, GlobalAveragePooling2D,Input
import os.path,sys
from keras.models import Model
from keras.applications.vgg16 import VGG16
from keras.preprocessing.image import ImageDataGenerator
import keras.callbacks
from keras.applications import VGG16
from keras.optimizers import Adam

N_CATEGORIES  = 20
IMAGE_SIZE = 256
BATCH_SIZE = 16


pictures_files = os.listdir(train_dir)
NUM_TRAINING = 0
NUM_VALIDATION = 0
for i in range(classes_num):
    NUM_TRAINING += len(os.listdir(os.path.join(train_dir, pictures_files[i])))
    NUM_VALIDATION += len(os.listdir(os.path.join(valid_dir, pictures_files[i])))
print(NUM_TRAINING,NUM_VALIDATION)

conv_base = VGG16(weights='imagenet',
                 include_top=False,
                 input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
x = conv_base.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation='relu')(x)
predictions = Dense(N_CATEGORIES, activation='softmax')(x)
model = Model(inputs=conv_base.input, outputs=predictions)

model.summary()

1912 657
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 256, 256, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 256, 256, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 256, 256, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 128, 128, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 128, 128, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 128, 128, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 64, 64, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 64, 64, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 32, 32, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 32, 32, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 16, 16, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 8, 8, 512)         0         
_________________________________________________________________
global_average_pooling2d_1 ( (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              525312    
_________________________________________________________________
dense_2 (Dense)              (None, 20)                20500     
=================================================================
Total params: 15,260,500
Trainable params: 15,260,500
Non-trainable params: 0
_________________________________________________________________

ここで注目するべき点は、最後の「Non-trainable params: 0」となっている点です。

このままでは、全てのパラメータが学習されてしまうので、VGG16の最終部分であるblock5_conv3 (Conv2D)より上は、全て学習をしないように指定します。

ベースネットワークを凍結する

for layer in conv_base.layers:
    layer.trainable = False
model.compile(optimizer=Adam(lr=5e-4), loss='categorical_crossentropy',metrics=['accuracy'])

model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 256, 256, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 256, 256, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 256, 256, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 128, 128, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 128, 128, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 128, 128, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 64, 64, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 64, 64, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 32, 32, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 32, 32, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 16, 16, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 8, 8, 512)         0         
_________________________________________________________________
global_average_pooling2d_1 ( (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              525312    
_________________________________________________________________
dense_2 (Dense)              (None, 20)                20500     
=================================================================
Total params: 15,260,500
Trainable params: 545,812
Non-trainable params: 14,714,688
_________________________________________________________________

これによって、パラメータの更新が行われるのが、dense_1 (Dense) 、dense_2 (Dense) のみになりました。

後はいつもと同じように学習をします。

この方法ならジェネレータを使うことができます。

訓練開始

train_datagen = ImageDataGenerator(
   rescale=1.0 / 255,)

test_datagen = ImageDataGenerator(
   rescale=1.0 / 255,
)

train_generator = train_datagen.flow_from_directory(
   train_dir,
   target_size=(IMAGE_SIZE, IMAGE_SIZE),
   batch_size=BATCH_SIZE,
   class_mode='categorical',
   shuffle=True
)

validation_generator = test_datagen.flow_from_directory(
   valid_dir,
   target_size=(IMAGE_SIZE, IMAGE_SIZE),
   batch_size=BATCH_SIZE,
   class_mode='categorical',
   shuffle=True
)

history = model.fit_generator(train_generator,
   steps_per_epoch=NUM_TRAINING//BATCH_SIZE,
   epochs=100,
   verbose=1,
   validation_data=validation_generator,
   validation_steps=NUM_VALIDATION//BATCH_SIZE,
   )


さて、長かった学習が終わりました!

 

ここの部分だけ、historyの保存で少し手こずったので、記載します。

基本的にはmodelもhistoryも保存をするようにコードを作りましょう!

でないと、せっかくの計算が無駄になってしまいます…

僕はこれで、10時間くらいを無駄にしてしまったので本当に気をつけてあげてください…!

model.save('models/VGG16_mini_3.h5')
import codecs, json
b = {}
for k,v in history.history.items():
    b.update({k: [float(v_) for v_ in v]})

with open('histories/history_VGG16_mini_3.json', 'w') as f:
    json.dump(b, f)

2-9. 結果をグラフから評価

f = open('histories/history_VGG16_mini_3.json', 'r')
history = json.load(f)
f.close()

acc = history['accuracy']
val_acc = history['val_accuracy']
loss = history['loss']
val_loss = history['val_loss']

epochs = range(len(acc))

#正解率をプロット
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

#損失値をプロット
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validatin loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()


正解率 損失関数
訓練データ 1.000 0.0022
検証データ 0.9782 0.2483

 

上の結果とほとんど同じになりました。(同じことをしてるので、同じになってくれなければ困りますが。)

 

下で考察をしていきます。

転移学習モデルに対する考察

転移学習をした結果からわかること

  1. 訓練データで正解率1.000という、問題を解くために十分な表現力を獲得した
  2. 1章の自作モデルと比較して損失関数の方が10-2倍小さくできている
  3. 検証データにおいてわずかに過学習をしている
1は、いいことなのでいいです。(笑)
2は、畳み込み層がImageNetに対して最適化されているので、今回の問題にたいしては最適化されていないことが原因と考えます。

もちろん、最適化をしていないにもかかわらずここまでの精度が出せることが凄いんですけどね。

3は、2と同じ原因と考えます。

モデルをさらに改善するために

以上の結果から、

表現力(現在の重みをある程度)を保ったまま過学習を抑えたい!

これが次の目標になりました。

これを達成できたとき理想のモデルとなるので、その内部構造を観察して自作モデルに反映することができるようになります。

そのための手段として、ファインチューニングがあります!

次の章では、ファインチューニングを実装します。

最後まで読んでいただきありがとうございました。

続きもぜひ、よろしくお願いします!

 

keras初心者チュートリアル【No.1 自作モデルの作成】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.3 ファインチューニング】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.4 モデルの評価と可視化】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.5 モデルの内部を可視化】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...

リクエストやコメントなどをいただけると嬉しいです!

Twitterから更新報告をしております!

いいね・フォローしていただけると泣いて喜びます。(´;ω;`)

 

オススメのプログラミングスクールをご紹介

タイピングもままならない完全にプログラミング初心者から

アホいぶきんぐ
アホいぶきんぐ
プログラミングってどこの国の言語なの~?

たった二ヶ月で

いぶきんぐ
いぶきんぐ
え!?人工知能めっちゃ簡単にできるじゃん!

応用も簡単にできる…!!

という状態になるまで、一気に成長させてくれたオススメのプログラミングスクールをご紹介します!

テックアカデミーのPython+AIコースを受講した僕が本音のレビュー・割引あり! というプログラミング完全初心者だった僕が Tech Academy(テックアカデミー)のPython×AIコース を二ヶ月間...

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です