deeplearningで柴犬の写真からうちの子かどうか判定(2) データ増量・転移学習・ファインチューニング

## はじめに
- こちらは私自身の機械学習やディープラーニングの勉強記録のアウトプットです。
- **前回の[deeplearningで柴犬の写真からうちの子かどうか判定(1)](https://qiita.com/ryoo/items/3a6cba285fdb5ad38fec)**に引き続き、Google Colaboratoryで画像データの2種分類を行います。
- 様々なエラーでつまずいた箇所などもなるべく記述し、なるべく誰でも再現がしやすいように記載します。

## この記事の対象者・参考にした文献
前回と同じです。詳細は**[こちら](https://qiita.com/ryoo/items/3a6cba285fdb5ad38fec)**。

## 私について
- 2019年9月に**JDLA Deep Learning for Engeneer 2019#2**を取得。
- 2020年3月末までは公益法人の事務職。2020年4月からはデータエンジニアにキャリアチェンジ。

## 前回(1)の分析の概要
- 愛犬(柴犬)の写真60枚、(愛犬以外の)柴犬の写真の写真を60枚、計120枚の画像ファイル(jpg)を集め、それらを deep learning で2分類しました。
- モデルを学習させて、テストデータで検証した結果、精度は75～76％程度に留まりました。

## 今回(2)の手順概要
**手順1　分析データ量を増加し(写真を倍の枚数に増やす)、Google Driveにアップロード**
**手順2　Google Drive上に作業用フォルダを作成し、データを解凍、コピーする**
**手順3　モデル構築・学習・結果**
**手順4　ImageNetのモデル(VGG16)で転移学習**
**手順5　ImageNetのモデル(VGG16)でファインチューニング**

### 手順1　分析データ量を増加し(写真を倍の枚数に増やす)、Google Driveにアップロード
#### (1) 写真データの増量
前回の分析で、分類精度が7割台に留まった原因はいろいろあると思いますが、その最たるものは、やはり学習用データが60データと少量であったためであると思います。データを増やすのはなかなか大変ですが、今回の分類のため、写真の枚数を愛犬120枚、愛犬以外の柴120枚、計240枚に増量しました。このデータファイルを基に、再度分類を行います。

- 愛犬のjpgファイル(120枚)　新たに追加した写真例　⇒　mydog2.zipにまとめる
![mydog28.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/c2766a1c-531e-d5ae-4b8c-7cc488373e50.jpeg)![mydog20.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/987763d2-33af-888a-d6b2-0447686173f5.jpeg)![mydog36.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/9866e895-8b17-04ab-4469-6db4698d69db.jpeg)![mydog104.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/7d8ccc49-6c36-dee6-5969-127e5c3b42b9.jpeg)

- 愛犬以外の柴犬のjpgファイル(120枚)　新たに追加した写真例　→　otherdogs2.zipにまとめる
![pixabay041.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/51283c20-1add-e397-4044-b73150e56a6c.jpeg)![pixabay051.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/ac84331b-ce8a-fc63-a999-10cad603d663.jpeg)![pixabay015.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/33593f32-69ed-51af-4af2-1e1251fbbcc0.jpeg)![wp011.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/62c281ff-bc4e-eac7-529a-930f81546625.jpeg)

#### (2) Google drive内のデータ格納用フォルダを整備する
- 前回の分析を実施した際に、データを格納するフォルダを次のとおり作成しました。(図は私のフォルダ構成例)
![drive3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/89a7ad1b-5642-aebd-ae1d-0eb39b4c5ddc.png)

- データファイルが増えた(120データ→240データ)ことにより、今回の分析を行う前に前回使用したデータファイルは一旦丸ごと削除して、新たに入れ替えることにします。そのため、具体的にはGoogle Drive上の操作で、"use_data"フォルダ内の赤字で示した"train","validation","test"の3つのフォルダについては、格納データごとすべて削除します。
![drive4b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/70daff6c-da70-3c30-71b3-064e5b510c5d.png)

#### (3) データファイルのアップロード
- "mydog2.zip" "otherdogs2.zip"の2つのzipファイルをGoogle Drive("original_data"フォルダ)にアップロードします。 
- **"mydog2.zip"** **"otherdogs2.zip"**の2つのzipファイルについては、**[私のgithub](https://github.com/takaosan8/JupyterNotebook)**に掲載しています。

### 手順2　Google Drive上に作業用フォルダを作成し、データを解凍、コピーする
- 今回はtrainデータに各60枚、validationデータに各30枚、testデータに各30枚を割り当てます。
- ここからはGoogle Colaboratoryを起動してColab上で操作していきます。
- 以下の内容は前回の実装と共通する部分が大半のため、ここでのコードについてはjupyternotebook形式で**[私のgithub](https://github.com/takaosan8/JupyterNotebook)**に掲載しています。
- ファイル名：mydog_or_otherdogs2_1(120data_input320px).ipynb

<font color="Tomato">**注）ColaboratoryとGoogle Driveとの間の連携に関するタイムラグについて**　これは注意点というより、メモ書きに近いのですが、ColaboratoryとGoogle Driveとの連携にはタイムラグがあるように思います。安全に処理を実施するには、作業用フォルダ作成、コピー等の各処理は一気通貫に行うのではなく、ワンステップずつ、各処理が完了しているのを確認しながら進めたほうがよさそうだと思います。私の試行だと、ColaboratoryからGoogle Driveに命令を出した後、それが実際に反映するのにちょっと時間を要しています。そのため、タイミングにもよるかもしれませんが、反映前に次の処理を走らせるとエラーになる場合がありました。一気通貫でうまくいかない場合には、処理を何ステップかに分けて進めてみるとよいと思います。</font>

### 手順3　モデル構築・学習・結果
#### (1) データオーギュメンテーションなしでの学習の結果
- 訓練の結果は、次のグラフのとおりとなりました。accuracy, lossともに過学習の兆候が出ていて、目立った改善はありません。
![traial03b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/a4f7a94c-c69f-cb85-d791-81469a7c4a68.png)

#### (2) テストデータへの適用(データオーギュメンテーションなし)
テストデータに適用した検証結果については次のとおりとなりました。サンプルデータ数を増やしたことが精度の向上につながり、accuracyも8割程度に到達しています。
**test loss: 1.7524430536416669**
**test acc: 0.8166666567325592**

#### (3) データオーギュメンテーションありでの学習の結果
続いてデータオーギュメンテーションありで学習させました。訓練の結果は、次のグラフのとおりとなりました。
![traial04-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/877071a3-5d44-bda1-0ee0-5a93f302273b.png)

#### (4) テストデータへの適用(データオーギュメンテーションあり)

次にImageGeneratorの設定を画像データの水増しありにした場合のテストデータの検証結果は次のとおりです(水増し条件は前回の試行の設定と同じで、結果表示のみ)。こちらの方がaccuracyがより高くなっています。
**test loss: 1.382319548305386**
**test acc: 0.8666666634877522**


### 手順4　ImageNetのモデル(VGG16)で転移学習
今度は転移学習を実施します。代表的なモデルであるImageNetの学習済みモデル (VGG16) をkerasのライブラリからインポートして使用します。VGG16モデルからは、学習済み畳み込みベースのみを利用し、汎用性の高い局所的な特徴マップを抽出させます。その出力に「(柴犬用)うちの子－よその子」分類器を接続することで、分類精度の向上を図ります。
#### (1) VGG16について
- VGG16は、畳み込み13層、全結合層3層、計16層からなる多層ニューラルネットワーク。公開されているモデルはImageNetと呼ばれる大規模な画像セットを用いて訓練されたもの。
- Kerasのライブラリとして keras.applications.vgg16 モジュールに実装されている。

#### (2) モデルの読み込み
- 以下は、Google Drive内にデータの格納フォルダ、作業用フォルダが生成されており、分析用画像データが格納されていることを前提に進めて行きます。(これまでの分析環境そのままで実施します)
- 変数conv_baseにVGG16モデルを読み込みます。

```python
# VGG16の読み込み
from keras.applications import VGG16

conv_base = VGG16(weights='imagenet', # 重みの種類を指定(ここではImagenetで学習した重みを指定)
                  include_top=False, # NWの出力側にある全結合分類器を含めるかどうか(ここでは自作の全結合分類器を使用するので含まない設定とする)
                  input_shape=(320, 320, 3)) # NWに供給する画像テンソルの形状でImageNet標準は(224, 224 3)　(今回は320pxl*320pxlのRGB画像を指定)
conv_base.summary()
```
読み込んだVGG16の該当部分について、次のようなモデル構造が表示されます。

```python
Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, 320, 320, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 320, 320, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 320, 320, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 160, 160, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 160, 160, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 160, 160, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 80, 80, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 80, 80, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 80, 80, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 80, 80, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 40, 40, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 40, 40, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 40, 40, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 40, 40, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 20, 20, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 20, 20, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 20, 20, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 20, 20, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 10, 10, 512)       0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
```
#### (3) モデルの構築
conv_baseに今回の2値分類用の全結合層を結合し、モデルを構築します。

```python
from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.summary()
```
次のようなモデル構造が表示されます。

```python
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 10, 10, 512)       14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 51200)             0         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               13107456  
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
=================================================================
Total params: 27,822,401
Trainable params: 27,822,401
Non-trainable params: 0
_________________________________________________________________
```
#### (4) 訓練可能な重みの数を確認します

```python
# conv_baseを凍結する前の状態で訓練可能な重みの数
print('conv_baseを凍結する前の状態で訓練可能な重みの数:' ,len(model.trainable_weights))
```
実行すると「30」という結果が表示されます。

#### (5) conv_baseの重みのみを訓練不可能に設定し、設定結果を確認します
```python
# conv_baseの重みのみ訓練不可能に設定する
conv_base.trainable = False

# 訓練可能な重みの数の確認
print('conv_base凍結後の状態で訓練可能な重みの数:' ,len(model.trainable_weights))
```
実行すると、訓練可能な重みの数は「4」という結果に変更されて表示されます。このセットアップ状態でモデルの学習を実施します。

#### (5) 画像のテンソル化、学習
以下のコードで実施します。

```python
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

# trainデータのジェネレータ設定 水増し：あり
train_datagen = ImageDataGenerator(
    rescale=1./255,
    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')

# validationデータ、testデータ用のジェネレータ設定 水増し：なし\(validationデータとtestデータのジェネレータは共通とする)
test_datagen = ImageDataGenerator(rescale=1./255)

# trainデータのテンソル化
train_generator = train_datagen.flow_from_directory(
        # target directory
        train_dir,
        # size 320x320
        target_size=(320, 320),
        batch_size=20,
        # 損失関数としてbinary_crossentropyを使用するため、二値のラベルが必要
        class_mode='binary')

# validationデータのテンソル化
validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(320, 320),
        batch_size=32,
        class_mode='binary')

# モデルのコンパイル
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=2e-5),
              metrics=['acc'])

# 学習
history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=30,
      validation_data=validation_generator,
      validation_steps=50,
      verbose=2)
```
実行後のモデルは保存します。

```python
model.save('mydog_or_otherdogs_02a.h5')
```
#### (6) 実行結果
- 訓練結果のグラフは次のとおりです。
![traial04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/9b97102e-50d6-eb8f-6e8c-c478a998792b.png)
- グラフの形状に変化が出てきました。accuracyは0.88あたりで始まり、0.93から0.96あたりまで改善しています。lossは0.3あたりから始まり、0.1から0.2の範囲を推移しています。過学習状態から少し改善の兆候が出てきました。

- 続いて、テストデータでこのモデルの分類結果を検証します。(コードは前回記事を参照)
**test loss: 0.274524162985399**
**test acc: 0.9333333373069763**
- 転移学習の結果、分類性能は改善しています。accuracyが初めて9割を超えました。
- この試行では、畳み込みベースにVGG16の学習済みモジュールのみを使用しましたが、ImageNetでのトレーニングにより得られた汎用性の高い局所的な特徴マップの抽出能力が、分類性能に大きく貢献しているであろうことが確認できます。

### 手順5　ImageNetのモデル(VGG16)でファインチューニング

#### (1) ファインチューニングの概要
> ファインチューニングは特徴抽出に使用される凍結された畳み込みベースの出力側の層をいくつか解凍し、モデルの新しく追加された部分(この場合は全結合分類器)と解凍した層の両方で訓練を行う、という仕組みになっています。

#### (2) ファインチューニングの実施手順
> 1. 訓練済みのベースネットワークの最後にカスタムネットワークを追加する  
2. ベースネットワークを凍結する  
3. 追加した部分の学習を行う  
4. ベースネットワークの一部の層を解凍する  
5. 解凍した層と追加した部分の訓練を同時に行う  

※(1)(2)の記載については、『PythonとKerasによるディープラーニング』Francois Chollet著　株式会社クイープ訳　巣籠悠輔監訳　株式会社マイナビ出版刊より、該当部分を引用

#### (3) 訓練可能な重みの設定
- 次のコードでファインチューニングの実施に際して訓練可能な重みを設定します。
- block5_conv1, block5_conv2, block5_conv3の3つの層のみ、訓練可能に設定します。

```python
conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
  if layer.name == 'block5_conv1':
    set_trainable = True
  if set_trainable:
    layer.trainable = True
  else:
    layer.trainable = False
```
#### (4) ファインチューニングの実行
次のコードで学習をさせます。

```python
# オプティマイザにRMSpropを選択し、かなり低い設定の学習率を使用
# 更新値が大きいとファインチューニングの対象層３つの表現を傷つけてしまうため

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

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=100,
      validation_data=validation_generator,
      validation_steps=50)
```
訓練後のモデルは保存します。

```python
model.save('mydog_or_otherdogs_02b.h5')
```
#### (5) 結果グラフ
結果のグラフは次のとおりです。
![traial05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/51e1583f-b523-b1eb-9cb3-8982f6da15d5.png)
次のコードでグラフのスムース化を行います。

```python
# プロットのスムージング
def smooth_curve(points, factor=0.8):
    smoothed_points = []
    for point in points:
        if smoothed_points:
            previous = smoothed_points[-1]
            smoothed_points.append(previous * factor + point * (1 - factor))
        else:
            smoothed_points.append(point)
    return smoothed_points


plt.plot(epochs, smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs, smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs, smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
```
グラフは次のとおりとなります。validationの状況を見ると、エポックの回数が増えるごとに性能が上がっている様子ではないものの、以前に比べて、グラフの変化の幅がより狭い範囲で表示されているため、精度が改善されていることが期待できます。
![traial06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281414/01272ed8-a53e-d120-2247-779e97861d31.png)

#### (6) 学習済みモデルをテストデータに適用して分類精度を確認します
テストデータに適用した検証結果については次のとおりとなりました。

**test loss: 0.5482699687112941**
**test acc: 0.9499999916553498**

ファインチューニングの結果、分類性能をさらに向上させることができました。さらなる精度の向上へ向けて、まだ試していないアプローチもいろいろ実施してみたいところです。今回の検証ではここまでで一区切りとします。