【Aidemy】転移学習を用いた車のボディタイプ識別

# はじめに
Aidemy Premiumプランにてデータ分析コースを3ヶ月受講しました。
本記事はその最終課題になります。
今回はvgg16を用いて転移学習を行い、最終的に車のボディタイプの識別を行いました。
分類したボディタイプ：ミニバン、コンパクトカー、セダン、SUV
上記の分類になった理由は後述します。


# 実行環境
Google Colabにて実装しました。

https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja

# ボディタイプ識別に至った経緯
　当初は10種類の車種の識別を行おうと考えていました。しかし、データを集めて学習を行った結果、正解率が20％程度にしかなりませんでした。
おかしいと思いAidemyの講師の方に相談した所、識別する種類が多いほどデータ数もやはり必要になるので、今構築しているモデルでは限界があり、データ数を増やすか、識別する種類を減らすという対応策を提示していただきました。
そこで、識別する種類を減らすためにボディタイプの識別にしました。
**なお、上述の方針転換の関係で、元々集めていた画像を用いてボディタイプの識別を行っています。**
※そのため、画像の収集の検索名には具体的な車の名前が登場します。

# 目次
1. 画像の収集
2. 画像の水増し
3. 画像の読み込み・データの分割
3. 転移学習(vgg16）
3. モデルの評価
3. おわりに 


# 1.画像の収集
　画像の収集はicrawlerというPythonのライブラリを用い、スクレイピングを行いました。
3行程度書くだけで、お手軽に画像を集めることが出来ます。
スクレイピングのブラウザ指定はGoogleでも出来る様ですが、上手く動かないという記事を散見したので、Bingにて実行しました。

```:画像収集
# スクレイピング用のライブラリをインストール
!pip install icrawler
import os
# Bingでスクレイピングを行うためのモジュール
from icrawler.builtin import BingImageCrawler
# 検索名をリストに
SearchName=["ヤリス","カローラ","アルファード","日産ノート","トヨタライズ","ハリアー","トヨタアクア","ヴォクシー","フリード","スカイライン","ヴェゼル","プリウス"]
# 画像の取得枚数上限
Imgnum=200
# スクレイピング用のフォルダ作成
os.makedirs("./Original",exist_ok=True)
# スクレイピング開始
for name in SearchName:
  crawler=BingImageCrawler(storage={"root_dir":"./Original/"+name})
  crawler.crawl(keyword=name, max_num=200)
```
200枚スクレイピングできているかと思いきや、Bingを利用しても完璧にスクレイピングできず、各フォルダで100枚程度しか
集まりませんでした。


そこで次に画像の水増しを行います。

# 2.画像の水増し
 画像の水増しは、KerasのImageDataGeneratorを用いて、1枚→10枚に画像を増やしました。
回転・幅高さのシフト、反転などを以下のコードにて行います。
```:画像収集
import os
import glob
from keras.preprocessing.image import load_img, img_to_array
from keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import numpy as np
#画像の水増し
# 1枚当たりから水増しする枚数
N_img=10
# 元画像の置いてあるフォルダを指定
Original_path="/content/drive/MyDrive/Original"
dirlist=os.listdir(Original_path)
# .ipynb_checkpointsというファイルを取得しないように各車種の名前がついたフォルダを指定する
dirlist=[filename for filename in dirlist if not filename.startswith(".")]
# 水増しの処理　水増し画像の出力先フォルダは元画像と同じにした
for name in dirlist:
  input_path=Original_path+'/'+name
  files=glob.glob(input_path+'/*.jpg')
  output_path=Original_path+'/'+name
  for i,file in enumerate(files):
    img=load_img(file)
    x=img_to_array(img)
    x = np.expand_dims(x, axis=0)
    # ImageDataGeneratorの生成
    datagen = ImageDataGenerator(
    rotation_range=30, # ランダムに回転させる範囲
    width_shift_range=0.3, # ランダムに幅をシフトさせる範囲
    height_shift_range=0.3, # ランダムに高さをシフトさせる範囲
    zoom_range=0.3,        # ランダムにズームさせる範囲
    horizontal_flip=True, # ランダムに水平方向に反転させる
    vertical_flip=True, # ランダムに垂直方向に反転させる
    )
    # N_img枚数分画像を水増し
    dg= datagen.flow(x, batch_size=1, save_to_dir=output_path, save_prefix='img', save_format='jpg')
    for i in range(N_img):
        batch = dg.next()
```

**水増しが終了したら、/content/drive/MyDrive/Original直下に、セダン、コンパクトカー、SUV、ミニバンフォルダを作り、以下の様に車種を分類してフォルダを置いていきます（手作業)** 
セダン：スカイライン、カローラ
ミニバン：ヴォクシー、アルファード、フリード
コンパクトカー：ヤリス、アクア、ノート
SUV：ハリアー、ライズ、ヴェゼル

# 3.画像の読み込み・データの分割
　続いて、収集・水増しした画像データを読み込んでいきます。読み込む部分の処理などはAidemyの「男女識別」の講座を参考にしました。
画像数が多いからか、読み込みにforを多用しているからかはわかりませんがGPUを使っても20分かかる時もありました。
データを大量に使う場合は速度も意識してコードを書く必要があるなと思いました。
画像の読み込みとデータの分割は以下のコードにて行います。
```:画像の読み込み・データの分割
from collections import  defaultdict
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.utils import to_categorical
import cv2
import os
import random
from google.colab.patches import cv2_imshow
import glob
dirlist=os.listdir("/content/drive/MyDrive/Original")
dirlist=[filename for filename in dirlist if not filename.startswith(".")]
print(dirlist)
car_path_dict=defaultdict(list)
car_img_dict=defaultdict(list)
# ボディタイプがkey,valueが画像のパスを格納したリストである辞書を作成
for key in dirlist:
  car_path_dict[key]=glob.glob("/content/drive/MyDrive/Original/"+key+"/*/*")

for key,values in car_path_dict.items():
  print(key,len(values))
# パスを格納した辞書から画像を読み込んだ辞書を作成する
# 時間がかかります
for key,values in car_path_dict.items():
  print(key)
  for value in values:
    img=cv2.imread(value)
    img=cv2.resize(img,(50,50))
    car_img_dict[key].append(img)

Xtmp=[]
ytmp=[]
for values in car_img_dict.values():
  for value in values:
    Xtmp.append(value)
for k,v in enumerate(car_img_dict):
  print(k,v)
  ytmp=ytmp+[k]*len(car_img_dict[v])
X=np.array(Xtmp)
y=np.array(ytmp)

# 画像の順番をシャッフルする
index=np.random.permutation(np.arange(len(X)))
X=X[index]
y=y[index]

# 画像を分割する　訓練8割、テスト2割
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]

# ラベルづけを行う
y_train=to_categorical(y_train)
y_test=to_categorical(y_test)
```
# 4.転移学習(vgg16)
いよいよ学習を行います。今回はvgg16による、転移学習を行いました。
```:転移学習
from tensorflow.keras.layers import Activation,BatchNormalization, Conv2D, MaxPooling2D
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers

input_tensor=Input(shape=(50,50,3))
vgg16=VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(BatchNormalization())
top_model.add(Dense(512, activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dense(64, activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(5, activation='softmax'))
model=Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
  layer.trainable = False
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.Adam(lr=1e-4, beta_1=0.9,beta_2=0.999,decay=0.0),
              metrics=['accuracy'])

history = model.fit(X_train, y_train, batch_size=64, epochs=50, verbose=1, validation_data=(X_test, y_test))
scores = model.evaluate(X_test, y_test)
print("Test loss",scores[0])
print("Test accuracy",scores[1])
#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
```

# 5.モデルの評価
訓練データの方では順調に学習出来ていますが、テストデータでの正解率が低く、過学習になってしまっていることがわかります。
Dropoutを導入しても、過学習を起こしています。
また、テストデータの方はすぐに頭打ちが来てしまっていることもわかります。
![image (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586366/d9f53ff8-7df2-3707-91af-7cd1caefb81a.png)



# 6.考察
上記の様な結果となってしまった理由を考察していきたいと思います。
・画像のサイズが50×50と小さいものである事
・車単体が大きく映っているものだけでは無く、背景が有る画像もあるため背景が邪魔になってしまっている事。
・そもそもモデルの構築が今回の事象に合っていない可能性
・データ数が少ない?→各車種約3000枚ずつある（ただしかなり水増ししている)
・テストデータの数が少ない？
・最初からボディタイプの識別をしようとしていたわけでは無いので、分類に適した画像が集められていない可能性。
　→"セダン"、"コンパクトカー"、"ミニバン"、"SUVで検索をして、画像を収集すればより今回のテーマに適した画像が集められると思います。

精度が出なかったので、学習に利用する画像のデータのサイズを100×100にして再度学習を行った結果以下のようになりました。
(epoch数が見切れていますが、30です）
多少のばらつきはあるものの、画像サイズが小さいモデルに比べ精度を向上させることができました。
たまにがくっと値が下がっているものがあるのは、背景の影響だったり、水増しをした影響でノイズのような画像が入ってしまっているからかと考えています。
精度は上がったものの、学習コストが上がってしまい時間がよりかかったので、バランスを考える必要があると思いました。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586366/73472558-58ff-608a-e6e1-728a10ca8c1e.png)


# 7.感想
課題の提出がぎりぎりになってしまい、アプリケーションとして利用出来る様に出来なかったのは心残りです。時間に余裕を持つようにしないとなと改めて学べました。
また、良いデータからしか良い分析結果が出ないという事を身をもって学べました。
　機械学習に興味はあったものの、実際どのようにコードを書くのか、仕組みはどうなっているのかという部分はあまり知らなかったので、自分自身である程度の精度を出せる学習モデルを構築できるようになりうれしく思います。
まだまだ自然言語処理や他のデータ分析手法など、興味は尽きないのでこれからも引き続き学んでいこうと思います。