このチュートリアルを一通り行うことで、
というレベルから
自分でモデル作れます!転移学習、ファインチューニングできます!
モデルの中身を可視化できます!
という状態になってもらえたらと思っています。
- はじめに
- 自作モデルで車種当て問題を解いてみよう!
- 1-1. データのダウンロード
- 1-2. ライブラリのインポートと、定数の定義
- 1-3. 4層のCNNモデルの作成
- 1-4. モデルのコンパイル
- 1-5. ImageDataGeneratorを使ってディレクトリから画像を読み込む¶
- 1-6. バッチジェネレータを使ってモデルを適応¶
- 1-7. 学習結果をグラフ化
- 1-8. ImageDataGeneratorを使ってデータ拡張を設定する¶
- 1-9. ドロップアウトを追加した新しい7層のCNN
- 1-10. 拡張ジェネレータを用いてCNNを訓練
- 1-11. 結果をグラフ化
- 1-12. 自作モデルでベストな構造は?
- 自作モデルがこの構造でうまくいった理由に対する考察
はじめに
本チュートリアルは、具体的な理解よりも、直感的な理解をして頂くことを目的としています。そのため、厳密性よりも、わかりやすさを優先した表現をすることをご了承願います。
本チュートリアル制作のきっかけ
大学1年のC言語の授業でifとforを習って以来、約二年間全くプログラミングには触らずに過ごしてきた友人と、学生実験で2人で画像解析に関する研究を一ヶ月半かけて行うことになったことがきっかけです。
せっかくまとまった期間があるので、個人プレーではなく協力をしながら実験に取り組めればと思い作成をしました。結果として一ヶ月半でかなり有意義な研究をすることができました。
このチュートリアルが少しでも多くの方に役立てば幸いです。
本チュートリアルの対象者
- if, for, 関数, 変数など基本的なプログラミングに関する知識は記憶の彼方にわずかにあるが、pythonおよびディープラーニングに関する知識は全く無い。
- だけれども、ディープラーニングに興味がある!
- 自力でモデルを作って評価して、またモデルを作ってという一連の流れを身につけたい!!
みたいな方にオススメです。
参考図書
チュートリアルを作成するに当たって参考にさせて頂いた本です。
この本は、個人的にかなりおすすめの本です。
理論的な部分と、実践的な部分が半々です。
しかも、コンピュータビジョン、自然言語、画像生成と幅広く対応しているので、いろんな分野を触って勉強してみたい!という方に向いていると思います。
本チュートリアルで扱う問題
「車の画像から、その画像に写っている車種を当てる」という問題を解くためにモデルを作成します。
本チュートリアルの構成
- 自作モデルで車種当て問題をといてみよう!
- 転移学習で車種当て問題をといてみよう!
- ファインチューニングで車種当て問題をといてみよう!
- 完成したモデルをテストデータで評価してみよう!
- 精度の高いモデルと低いモデルの違いを覗いてみよう!
この4つの章立てで行います。
全てのコードは僕のgithubに上がっているのでcloneをしていただければ全て実行することが可能です。
今回のコードはこちらです。
それでは、チュートリアルを始めます!
自作モデルで車種当て問題を解いてみよう!
1-1. データのダウンロード
git clone https://github.com/ibkuroyagi/digital-image-experiment-cnn-by-car.git
上記をターミナルで実行をしてください。
ダウンロードをしてもらっているうちに、データの入っているディレクトリの説明をします。学習で使うデータは、mini_picturesディレクトリの中に
train :valid : test = 1912 : 657 : 663(枚)
と用途別に保存されています。
それぞれの用途は、
モデルの学習をするため
作成したモデルのハイパーパラメータの調整のため
モデルに汎用性があるかどうか最終判断するため



さらに、それぞれのディレクトリの中で、車種名ごとにディレクトリを作成してあります。階層としては、下記のようになっています。



画像idデータから各車種ごとへのラベリング、ディレクトリ分けなどの前処理に興味のある方はソースコードの1-metadata_search.ipynb、2-pictures_sort.ipynbを見てください!
下記で解説をするのは3-Simple_CNN_model.ipynbのコードです!
1-2. ライブラリのインポートと、定数の定義
#画像を読み込む際基準となるディレクトリ
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")
# 当てる車種が記述されたCSVファイルを読み込む
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"))
print(classes)
print(classes_num)
# 画像サイズ
IMAGE_SIZE = 256
# バッチサイズ
BATCH_SIZE = 32
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])))
['Audi-a3', 'Audi-a5', 'Audi-q5', 'BMW-1-series', 'BMW-4-series', 'BMW-x3', 'Honda-pilot', 'Jeep-wrangler', 'MINI-clubman', 'MINI-countryman', 'Mazda-mazda5', 'Mercedes-Benz-gla', 'Mercedes-Benz-glk', 'Mitsubishi-outlander', 'Nissan-370z', 'Nissan-quest', 'Nissan-rogue-select', 'Subaru-outback', 'Toyota-tacoma', 'Volkswagen-cc']
20
1-3. 4層のCNNモデルの作成
今回は入力として、(256, 256, 3)の大きさを持ったカラー画像を想定したモデルを作成します。
実際の画像の大きさが、縦256、横256、でなくても、ジェネレータの部分でサイズを変更することができるので基本的には問題ありません。
モデルを作る際に注意をしなければならない点は、入力の画像のサイズinput_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)と、出力のクラス数classes_numです。
これがあっていれば、何かしら動くものはできます。
ちなみに、activation='relu'
は活性関数の指定です。最後だけ activation='softmax'
となっているのは、確率として出力をするためです。
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)))
model.add(layers.Conv2D(64, (3, 3),activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(16, (3, 3),activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(classes_num, activation='softmax'))
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 254, 254, 32) 896
_________________________________________________________________
conv2d_2 (Conv2D) (None, 252, 252, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 126, 126, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 124, 124, 16) 9232
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 62, 62, 16) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 61504) 0
_________________________________________________________________
dense_1 (Dense) (None, 20) 1230100
=================================================================
Total params: 1,258,724
Trainable params: 1,258,724
Non-trainable params: 0
_________________________________________________________________
1-4. モデルのコンパイル
from keras import optimizers
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
loss='categorical_crossentropy'
の部分で損失関数を交差エントロピーに指定します。
損失関数を小さくするように更新する部分にoptimizer=optimizers.RMSprop(lr=1e-4)
を用いています。
ここは、Adam、SGDなどいろいろ実験をして決める場所です。lr ( learning rate ) も重要なハイパーパラメータです。実験をする中で決めることはたくさんありますが、今回は決め打ちで行きます。
1-5. ImageDataGeneratorを使ってディレクトリから画像を読み込む¶
from keras.preprocessing.image import ImageDataGenerator
#すべての画像を1/255スケーリング
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
#flow_from_directoryではcategoricalのラベリングはclassesを参照して、ディレクトリ名から自動的にone-hotエンコードされる
train_generator = train_datagen.flow_from_directory(
train_dir, #ターゲットディレクトリ
target_size=(IMAGE_SIZE, IMAGE_SIZE), #すべての画像サイズを256*256に変換
batch_size=BATCH_SIZE, #バッチサイズ
class_mode='categorical',
classes=classes) #categorical_crossentropyを使用するため多クラスラベルが必要
validation_generator = test_datagen.flow_from_directory(
valid_dir, #ヴァリデーションディレクトリ
target_size=(IMAGE_SIZE, IMAGE_SIZE), #すべての画像サイズを256*256に変換
batch_size=BATCH_SIZE, #バッチサイズ
class_mode='categorical',
classes=classes) #categorical_crossentropyを使用するため多クラスラベルが必要
Found 1912 images belonging to 20 classes.
Found 657 images belonging to 20 classes.
1-6. バッチジェネレータを使ってモデルを適応¶
history = model.fit_generator(train_generator,
steps_per_epoch=NUM_TRAINING//BATCH_SIZE,
epochs=30,
validation_data=validation_generator,
validation_steps=NUM_VALIDATION//BATCH_SIZE)
model.save("simplest_cnn_model_mini_1.h5")



1-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(1,len(acc)+1)
#正解率をプロット
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
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()



正解率 | 損失関数 | |
訓練データ | 0.9909 | 0.0300 |
検証データ | 0.8123 | 0.5454 |
- trainデータでは正解率が99%までいっているがvalidデータでは81%しかない
- つまり、過学習が発生している
- 過学習は5epochあたりから発生している
なので、次は過学習に対して効果的とされる工夫をいくつか加えてみます!
1-8. ImageDataGeneratorを使ってデータ拡張を設定する¶
前半でも、何気なく使っていたImageDataGeneratorですが、実はこの人かなり優秀です!
過学習が起きてしまう大きな原因として、使用できる画像枚数が少なくtrainデータに過剰に適合してしまうことがあります。
その問題を、なんとか解消するためにあるのがこのジェネレータです。ジェネレータを通すことによって1枚の画像から、その画像を少し変形した疑似画像を作り出してくれます。
つまり、画像の水増しです!
具体的にできることは、
- rotation_range…画像をランダムに回転させる(回転範囲0~180).
- width_shift_range, height_shift_range…画像を水平または垂直にランダムに平行移動させる範囲(幅全体または高さ全体の割合)
- shear_range…等積変形をランダムに適応
- zoom_range…図形の内側をランダムにズーム
- horizontal_flip=True…画像の半分をランダムに反転(実際の写真のように水平方向の非対称性いついての前提がない場合に重要)
- fill_mode=’nearest’…新たに作成されたピクセルを埋めるための戦略(これらのピクセルが見えるようになるのは回転もしくは平行移動の後)
こんな感じです。
実際に使ってみます。
datagen = ImageDataGenerator(rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
from keras.preprocessing import image
import matplotlib.image as mpimg
#ランダムに水増しされた訓練画像の表示
Audi_a3_dir = os.path.join(train_dir,"Audi-a3")
fnames = [os.path.join(Audi_a3_dir,fname) for fname in os.listdir(Audi_a3_dir)]
# 水増しする画像を選択
image_path = fnames
print(image_path)
# 元の画像を表示
img = mpimg.imread(image_path)
plt.figure()
imgplot = plt.imshow(img)
plt.show()
# 画像読み込み、サイズを変更
img = image.load_img(img_path, target_size=(IMAGE_SIZE, IMAGE_SIZE))
# 形状が(1, 256, 256, 3)のNumpy配列に変形
x = image.img_to_array(img)
# (1, 256, 256, 3)に変形
x = x.reshape((1,) + x.shape)
# ランダムに変換した画像のバッチを生成する
# 無限ループとなるため、何らかのタイミングでbreakする必要あり
i = 0
for batch in datagen.flow(x, batch_size=1):
plt.figure(i)



1枚目が元の画像で、その下がジェネレータによって水増しをした画像です。
左右反転していたり、傾いていることが分かります。
こんな感じで、画像を水増ししていきます。
1-9. ドロップアウトを追加した新しい7層のCNN
前回から、たたみ込み層、dropout、全結合層を追加しました。
今回は含めていませんが、Conv2Dの引数にstrides=(2, 2)
を追加することも一般的には過学習に対して効果的であるといわれています。
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3),activation='relu',input_shape=(IMAGE_SIZE, IMAGE_SIZE,3)) )
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(256, (3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.3))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(classes_num, activation='softmax'))
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
model.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_11 (Conv2D) (None, 254, 254, 32) 896
_________________________________________________________________
max_pooling2d_11 (MaxPooling (None, 127, 127, 32) 0
_________________________________________________________________
conv2d_12 (Conv2D) (None, 125, 125, 64) 18496
_________________________________________________________________
max_pooling2d_12 (MaxPooling (None, 62, 62, 64) 0
_________________________________________________________________
conv2d_13 (Conv2D) (None, 60, 60, 128) 73856
_________________________________________________________________
max_pooling2d_13 (MaxPooling (None, 30, 30, 128) 0
_________________________________________________________________
conv2d_14 (Conv2D) (None, 28, 28, 256) 295168
_________________________________________________________________
max_pooling2d_14 (MaxPooling (None, 14, 14, 256) 0
_________________________________________________________________
conv2d_15 (Conv2D) (None, 12, 12, 128) 295040
_________________________________________________________________
max_pooling2d_15 (MaxPooling (None, 6, 6, 128) 0
_________________________________________________________________
flatten_3 (Flatten) (None, 4608) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 4608) 0
_________________________________________________________________
dense_5 (Dense) (None, 512) 2359808
_________________________________________________________________
dense_6 (Dense) (None, 20) 10260
=================================================================
Total params: 3,053,524
Trainable params: 3,053,524
Non-trainable params: 0
_________________________________________________________________
1-10. 拡張ジェネレータを用いてCNNを訓練
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
fill_mode='nearest')
# 検証データは水増しするべきで無いことに注意
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_dir, #ターゲットディレクトリ
target_size=(IMAGE_SIZE, IMAGE_SIZE), #全ての画像を256*256に変換
batch_size=BATCH_SIZE, #バッチサイズ
class_mode='categorical',#損失関数としてcategorical_crossentropyを使用するため,
classes=classes #他クラスラベルが必要
)
validation_generator = test_datagen.flow_from_directory(
valid_dir, #ターゲットディレクトリ
target_size=(IMAGE_SIZE, IMAGE_SIZE),#全ての画像を256*256に変換
batch_size=BATCH_SIZE, #バッチサイズ
class_mode='categorical',#損失関数としてcategorical_crossentropyを使用するため,
classes=classes #他クラスラベルが必要
)
history = model.fit_generator(
train_generator,
steps_per_epoch=NUM_TRAINING//BATCH_SIZE,
epochs=30,
validation_data=validation_generator,
validation_steps=NUM_VALIDATION//BATCH_SIZE
)
model.save('simple_cnn_model_mini_2.h5')



1-11. 結果をグラフ化
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1,len(acc)+1)
#正解率をプロット
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
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()



正解率 | 損失関数 | |
訓練データ | 0.5452 | 1.4263 |
検証データ | 0.4732 | 1.5953 |
- 目的であった過学習を押えていることを確認
- 30epochを回してもまだ学習の途中
- 正解率はtrainデータでさえ、54%とかなり低くなった
- 1epochあたりの学習時間が倍近くかかるようになった
1-12. 自作モデルでベストな構造は?
CPUで計算できる範囲内でいろいろ実験をしてみた結果、現時点でのベストモデルが完成しました。
結論として、この問題ではシンプルな構造がよりよい結果になることが分かりました。
model_13= models.Sequential()
model_13.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(256, 256, 3)))
model_13.add(layers.Conv2D(64, (3, 3),activation='relu'))
model_13.add(layers.Conv2D(32, (3, 3),activation='relu'))
model_13.add(layers.Flatten())
model_13.add(layers.Dense(classes_num, activation='softmax'))
model_13.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 254, 254, 32) 896
_________________________________________________________________
conv2d_2 (Conv2D) (None, 252, 252, 64) 18496
_________________________________________________________________
conv2d_3 (Conv2D) (None, 250, 250, 32) 18464
_________________________________________________________________
flatten_1 (Flatten) (None, 2000000) 0
_________________________________________________________________
dense_1 (Dense) (None, 20) 40000020
=================================================================
Total params: 40,037,876
Trainable params: 40,037,876
Non-trainable params: 0
_________________________________________________________________
オプティマイザをRMSpropからAdamに変更しました。
model_13.compile(loss='categorical_crossentropy',
optimizer=optimizers.Adam(lr=1e-4),
metrics=['acc'])
ジェネレータを使うと、結果がさちるまでに必要なepoch数が増え計算時間がかなりかかってしまうので今回はリサイズのみにします。
from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
#flow_from_directoryでは、categoricalのラベリングにclassesを参照して、ディレクトリ名から自動的にone-hotエンコードされる
train_generator = train_datagen.flow_from_directory(
train_dir, #ターゲットディレクトリ
target_size=(256, 256), #すべての画像サイズを256*256に変換
batch_size=32, #バッチサイズ
class_mode='categorical',
classes=classes)#categorical_crossentropyを使用するため多クラスラベルが必要
validation_generator = test_datagen.flow_from_directory(
valid_dir, #ヴァリデーションディレクトリ
target_size=(256, 256), #すべての画像サイズを256*256に変換
batch_size=32, #バッチサイズ
class_mode='categorical',
classes=classes)#categorical_crossentropyを使用するため多クラスラベルが必要
history = model_13.fit_generator(train_generator,
steps_per_epoch=1917//32,
epochs=30,
validation_data=validation_generator,
validation_steps=636//32)
model_13.save("4_dense_mini.h5")



acc = history['acc']
val_acc = history['val_acc']
loss = history['loss']
val_loss = history['val_loss']
epochs = range(1,len(acc)+1)
#正解率をプロット
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
#損失値をプロット
plt.figure()# 2枚目の図の下地
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 | 2.7420e-05 |
検証データ | 0.9453 | 0.2303 |
MaxPoolingなどをすることなく畳み込みで特徴量の抽出に特化をした方がより早く、正確に問題を解けるようになることが分かりました。
これは、車という識別対象の特徴が製品ごとに違いがないという点によると考えれられます。
そのため、主に輪郭の抽出を行う畳み込みが効果的に働いたのだと考えます。
ですが、若干、過学習をしていることもわかります。
おそらくの結果が輪郭の情報だけから車種あてをする限界に近いと考えます。
これ以上の精度を求めるのであればより抽象的な表現を反映できるモデルにしなければならないです。 ですが、そのためにはより画像が必要になるのでその対策を考える必要があります。
自作モデルがこの構造でうまくいった理由に対する考察
一般的な手法がうまくいかないのはなぜ?
まず一般的に精度が高くなるとされるMaxpooling, strides, dropout, weigth decay, といった手段を用いるとことごとく精度が下がりました。
今回扱うデータは背景がすべて同一で、取られるまでの距離もほとんど同じです。
違うのは年式と、構図、色くらいなので、位置に関してロバストにするMaxpoolingが効果を発揮できなかったからと考えます。
strides, dropoutに関しては過学習に対して効果的であることは間違いないと思います。
ですが、今回は過学習による影響よりも過学習を抑えるための操作によって、完璧な画像データから一部の情報を失っていくことになるので問題が難しくなってしまったのではないかと考えています。
モデルをさらに改善するために
- 過学習の対策としてジェネレータを使うこと
- あらかじめ大量に画像を学習したモデルと内部状態構造を比較することでこの問題を解くために必要となる要素を理解すること
があげられます。
1はGPUの力技なので飛ばして、2について行っていきます。
そのために、第2章では転移学習を実装します。
最後まで読んでいただきありがとうございました。
続きもぜひ、よろしくお願いします!












リクエストやコメントなどをいただけると嬉しいです!
Twitterから更新報告をしております!
いいね・フォローしていただけると泣いて喜びます。(´;ω;`)
タイピングもままならない完全にプログラミング初心者から
たった二ヶ月で
応用も簡単にできる…!!
という状態になるまで、一気に成長させてくれたオススメのプログラミングスクールをご紹介します!


