アニメ・特撮もOK！プログラミング超初心者のバイクタイプ識別AIアプリ作成記

# はじめに
このブログはプログラミング**超初心者**がAidemyさんの「AIアプリ開発コース」を受講し、
その成果物としてCNNを使ったAI画像識別アプリを作成する過程を記録したものです。

まずは人間のスペックから。私は大学の専攻は文学部史学科でした。
社会人になってからは、なぜか経理を担当するようになり、規模の大小、官民も業界も問わず様々な職場で経理および給与計算等の仕事をしてきました。
また、ウインドウズが世に放たれる前から社会にでておりましたので、手書き帳簿やMS-DOS及びLotus-123を使ったこともあります。
高校数学には苦手意識はあるけれど数字はむしろ大好きでexcel関数とはお友達、でもVBAは使ったことがない、そんな感じです。
このブログが初心者あるいは同年代の方々の一助になればと思い、筆を執っております。

# 目次
1.実行環境
2.アプリ作成の目的＆目標
3.バイクタイプのカテゴライズ
4.画像収集
5.モデルの定義⇔学習⇔制度の評価
6.動作確認
7.考察

# 1.実行環境
・Python3.10.9
・Google Colaboratory
・Visual Studio Code 

# 2.アプリ作成の目的＆目標
バイク識別アプリを作成しようと思ったのは、単純にバイクのフォルムがとても好きだからです。
バイクレースの最高峰であるMotoGP（昔のWGP）が好きで海外サーキット観戦に行ったこともあります。
ライダーが騎乗しサーキットを疾走する姿には心が踊ります。
まさしく「人馬一体」でとても美しい。。。よし、題材はバイクにしよう！と決めました。

最初は歴代のレーシングマシンを識別するアプリを作ろうかと思いましたが、画像を必要枚数集めるのが困難で断念、
代わりにバイクのタイプを識別するのなら可能ではないか、と思い立ちました。
そしてそのアプリで実車だけでなくアニメや特撮で登場するバイク（っぽい乗り物）も識別しちゃおう！目標は高く！

アプリ完成後に識別するのは「AKIRA」の有名な金田のバイクと私の子供のころの憧れ「仮面ライダーV3」のハリケーン号を選びました。
私の予想では金田のバイクはビックスクーターだと思われるのでスクーター。
ハリケーン号はベース車がスズキのTM250なのでオフロードと識別されるか、あるいはカウルがあるのでスーパースポーツかそのあたりではないでしょうか。
結果をお楽しみに～

さて、どうなりますやら。

# 3.バイクタイプのカテゴライズ
バイクタイプは私の思った以上に細分化されていました。
参考にさせていだいたサイトさんでは10タイプ。多くなりすぎると画像収集が難しくなり、学習に時間もかかるだろうと考え、今回は５タイプをピックアップすることにしました。

参考サイト⇒[バイクの種類にはどんなものがある？](https://hondago-bikerental.jp/bike-lab/19094.html)

### 1.ネイキッド
最もオールラウンダー、日本では最もスタンダードなバイク。教習車としても使われている。
ネイキッド（裸の）と表されているようにエンジンがむき出しになっているタイプ。
![1_naked.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/8df793f5-19f8-6c7b-f964-5c18a123f755.jpeg)


### 2.クルーザー（アメリカン）
車体が長く低く、ステップの位置が着座よりも大きく前方にあり特徴的なライディングポジションを持つ。
ハーレーダビッドソンが有名。
![2_cruiser.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/14a7d2c5-4388-0be1-6d63-ffc13aa32b13.jpeg)


### 3.スーパースポーツ
カウルで車体がおおわれている。ライディングポジションは前傾姿勢になり、スポーツ性の高い車体。
レーサーレプリカもここに入る。
![3_supersport.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/f5cde123-d3d7-5a46-e314-965bbf3ed793.jpeg)


### 4.オフロード
軽量な車体にストロークの長いサスペンションやブロックタイヤを装備しており、未舗装路でも走行できる。
![4_offroad.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/4a9a721d-cb56-3747-7e56-dc89b0a920b4.jpeg)


### 5.スクーター
エンジンが座席の下にあり、前方に足踏台がある。着座姿勢はシートを跨がず腰掛ける形となる。
![5_scooter.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/b3f8256d-f4be-6e9f-c9f9-f63ef21ba8af.jpeg)



# 4.画像収集
チューターさんに教えていただいたクローラーでスクレイピングで画像を収集しました。
```python:Bing_crawler
!pip install icrawler

# Bing用クローラーのモジュールをインポート
from icrawler.builtin import BingImageCrawler

# Bing用クローラーの生成
bing_crawler = BingImageCrawler(
    downloader_threads=4,           # ダウンローダーのスレッド数
    storage={'root_dir': 'naked'}) # ダウンロード先のディレクトリ名

# クロール（キーワード検索による画像収集）の実行
bing_crawler.crawl(
    keyword="ネイキッドバイク",   # 検索キーワード（日本語もOK）
    max_num=300)                    # ダウンロードする画像の最大枚数
```

バイクタイプごとにキーワードを入れ、max300でスクレイピングを行いました。
バイクはタイプ別にわかれているといえ、スーパースポーツっぽいスクーター等も存在しますし、そもそもバイクではない画像等も引っ張ってきているため、最後は目視で選別しました。
また今回は人が乗っている画像も採用しないこととしました。他のキーワードでもスクレイピングを行いそれぞれのタイプごとに150枚の画像を用意しました。
何度も手直ししたため最初にスクレイピングを開始してからここまでで６日かかりました。


# 5.モデルの定義⇔学習⇔精度の評価
### 1.ライブラリーのインポート
まず、必要なライブラリー(keras,VGG16等)をインポートします。
```py:get_module
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
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
```
### 2.画像の読み込み
OpenCVをサイズを（100：100）とし画像データ読み込み、リストを作成します。
画像データをndarray,多次元配列にします。
学習データ(X)と検証データ(y)を8：2に分割し、検証データに正解ら別を代入し、one-hotベクトルにします。

```py:img
#画像ファイルを取得
path_naked = os.listdir('./drive/MyDrive/bike_data/bike_types/naked/')
path_cruiser = os.listdir('./drive/MyDrive/bike_data/bike_types/cruiser/')
path_supersport = os.listdir('./drive/MyDrive/bike_data/bike_types/supersport/')
path_offroad = os.listdir('./drive/MyDrive/bike_data/bike_types/offroad/')
path_scooter = os.listdir('./drive/MyDrive/bike_data/bike_types/scooter/')

#画像のリストを作成
img_naked = []
img_cruiser = []
img_supersport = []
img_offroad =[]
img_scooter =[]

for i in range(len(path_naked)):
  img = cv2.imread('./drive/MyDrive/bike_data/bike_types/naked/' + path_naked[i])
  b,g,r = cv2.split(img)
  img = cv2.merge([r,g,b])
  img = cv2.resize(img, (100,100))
  img_naked.append(img)

for i in range(len(path_cruiser)):
  img = cv2.imread('./drive/MyDrive/bike_data/bike_types/cruiser/' + path_cruiser[i])
  b,g,r = cv2.split(img)
  img = cv2.merge([r,g,b])
  img = cv2.resize(img, (100,100))
  img_cruiser.append(img)

for i in range(len(path_supersport)):
  img = cv2.imread('./drive/MyDrive/bike_data/bike_types/supersport/' + path_supersport[i])
  b,g,r = cv2.split(img)
  img = cv2.merge([r,g,b])
  img = cv2.resize(img, (100,100))
  img_supersport.append(img)

for i in range(len(path_offroad)):
  img = cv2.imread('./drive/MyDrive/bike_data/bike_types/offroad/' + path_offroad[i])
  b,g,r = cv2.split(img)
  img = cv2.merge([r,g,b])
  img = cv2.resize(img, (100,100))
  img_offroad.append(img)  

for i in range(len(path_scooter)):
  img = cv2.imread('./drive/MyDrive/bike_data/bike_types/scooter/' + path_scooter[i])
  b,g,r = cv2.split(img)
  img = cv2.merge([r,g,b])
  img = cv2.resize(img, (100,100))
  img_scooter.append(img)  


#np.arrayでXに学習データ、yに正解ラベルを代入
X = np.array(img_naked + img_cruiser + img_supersport + img_offroad + img_scooter)
y = np.array([0]*len(img_naked) + [1]*len(img_cruiser) + [2]*len(img_supersport) + [3]*len(img_offroad) + [4]*len(img_scooter))

#ラベルをシャッフル
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

#学習データと検証データに分割
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):]

#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
```
### 3.転移学習のモデル
精度をあげるために転移学習のモデルのVGG16を使用します。
（私はVGG16は経験値の高い頼れる先輩というイメージを持っています）
```py:VGG16
#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(100,100,3))
vgg16=VGG16(include_top=False,
          weights='imagenet',
          input_tensor=input_tensor)
```

### 4.モデルの定義
さて。ここから！モデルの定義に入ります。
まずは講座で勉強した形でやってみましょう。
```py:model_256
#モデルの定義
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(rate=0.5))
top_model.add(Dense(5,activation='softmax'))

model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

for layer in model.layers[:19]:
  layer.trainable = False

model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4,momentum=0.9),
              metrics=['accuracy'])

history = model.fit(X_train, y_train, batch_size=100, epochs=10, verbose=1, validation_data=(X_test, y_test))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/ca5e4e84-6d8e-b7da-53e5-ddf0796ee25b.png)

結果は。。。正解率も損失率もよくないですね。。。うまく学習されませんでした。
まずはそもそもの画像データが画角にあってなくて画像データが小さすぎるのではと思い、画像の読み込みを100：150にしてみたり、学習回数を上げてみてはどうだろうとエポック数を100にしてみたり、Dense層を二層にしてみたり（下記）といろいろ試してみましたが一向に精度があがりません。
```py:Dense_2
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(rate=0.5))
top_model.add(Dense(128, activation='relu'))
top_model.add(Dropout(rate=0.5))
top_model.add(Dense(5,activation='softmax'))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/9a1e880c-b0d6-22f7-5e15-f34ffdf6a93c.png)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/c0bca7fe-3b59-f0bb-27bf-90c92893e22c.png)

せめて６割超えないかなあ。。。
弱気になってチューターさんに「３カテゴリーの方がいいですかね」と相談したところ「自分がどうしたいかと精度より考察が大切」との助言をいただき、所信を貫く決意をしました。
そう、「なにができるか」ではなく「なにをしたいか」、ですよね！
その後、何かヒントはないかとあきらめず情報を求めました。

下記のyoutubeがとても参考になりました。
ソニーのNeural Network Console
[Deep Learning入門：層数、ニューロン数を決める指針](https://youtu.be/OwccN7rj4Qg)

お話されていることをちゃんと理解していないかもしれないですが、もしかして、Dense層のユニット数を増やしてみたらいいのではないか！とヒントを頂けました。
この際、バッチサイズも大きくしちゃえばいっぱい学習してくれるのでは？？？と思いつき
（なかばやけくそで）両方をどんと上げてみました。

__2048ユニット__

```py:model_2048_100:150
#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(150,100,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(Dense(2048, activation='relu'))
top_model.add(Dropout(rate=0.5))
top_model.add(Dense(5,activation='softmax'))

#VGG16とtop_modelを連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

#VGG16の重みを19層に固定
for layer in model.layers[:19]:
  layer.trainable = False

#パラメータの設定
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4,momentum=0.9),
              metrics=['accuracy'])

#学習の実行
history = model.fit(X_train, y_train, batch_size=2048, epochs=100, validation_data=(X_test, y_test))  
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/06426d44-c67c-312b-7152-02138acbd81e.png)

やった！正解率が８割超えてきた！！！！結果がでた瞬間、まじでガッツポーズをしました！
ふ～やっとアプリを作ることができる！と思ったんですが、h5ファイルが155MBになってしまい、なんと容量オーバーでアップロードできないということがデプロイ中に発覚し、また学習のし直しに！

ユニット数とバッチサイズを半分にしたところ精度もさほど落ちずに、やっとアップできる容量となりました。ちなみにバッチサイズがユニット数と同数なのは、いくつか試した結果この組み合わせが一番よかったという経験によるものです。
ここまでの試行錯誤でほぼ5日かかりました。
### <最終モデル>
```py:model_1024
#モデルの定義
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(1024, activation='relu'))
top_model.add(Dropout(rate=0.5))
top_model.add(Dense(5,activation='softmax'))

#VGG16とtop_modelを連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

#VGG16の重みを19層に固定
for layer in model.layers[:19]:
  layer.trainable = False

#パラメータの設定
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4,momentum=0.9),
              metrics=['accuracy'])

#学習の実行
history = model.fit(X_train, y_train, batch_size=1024, epochs=100, validation_data=(X_test, y_test)) 
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/d73013ac-9347-e73a-6bc0-c6a03a082c84.png)

# 6.バイクアプリの動作確認

https://biketypefive.onrender.com

アプリのフレームはAidemyさんのフォーマットを使用しました。初心者は欲張ってはいけません。
ローカル環境でちゃんと動いたのでアプリをデプロイ。できた。本当に良かった。ほっとしました。

バイクタイプのカテゴライズで掲載した画像では全問正解できました。めでたい！

さて、設定した目標は達成できるのか？！
早速、試してみましょう！！

### 金田のバイク
![金田のバイク.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/4a85ae29-7a2b-845b-7e82-fb2acbc18764.jpeg)
__< 結果 > スクーター__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/c3801ec5-0281-1c1d-29aa-bb2abc343ab9.png)

### 金田のバイク2（2012年大友克洋GENGA展で展示された実物大のモデル）
![金田のバイク2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/2cbbac22-ff98-df8e-7583-91974709c493.jpeg)
__< 結果 > スクーター__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/3b6b3afb-3ef7-5363-10b6-ee796952c533.png)

### ハリケーン号
__まずはベース車　TM250__
![TM250.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/a2668929-46b4-7aba-2a65-35b1def71c49.jpeg)
__< 結果 > オフロード__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/96baa39e-1bed-438f-d625-6c1a91cad633.png)
__ハリケーン号__
![ハリケーン号.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/f5119bc8-a28b-02ba-2f00-98c2b10129cf.jpeg)
__< 結果 > オフロード__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359689/43225b46-1a0d-1f32-c5ec-bcf3f1861167.png)

__全問予想通りでした！！！__

# 7.考察＆反省
まずはある程度の精度を出せた点は評価したいと思います。
よく機械学習では最初の「データ収集」と「データの前処理」が大切と言われますが、ここが精度に与える影響は本当に大きいと思いました。
精度を上げるにはユニット数を多くする、今回はこれが正解だったように思いますが、ビギナーズラックなような気がしています。もっと理論的に最適解を導き出せるようにしなければ。
そしてアプリを作るには全体のバランスを考えなければならないということも学びました。
資源は有限です。できる範囲で最善を求めることが大切なのだ、と実感しました。

# おわりに
Aidemyさんの講座を始めてから２か月強経ちました。チューターさんたちに助けていただき、なんとかここまで辿り着くことができました。ほんとうにありがとうございました。
今の私はAI、Pythonの世界の扉をたたいたに過ぎないと思っております。まだまだいろいろ足りない。でも重要な一歩を踏み出せたと思います。
独学ではここまでこれなかったでしょう。
だってコマンドプロンプトなんて「なんか変なのでた」ぐらいにしか思ってませんでしたから、まさか自分が使用するとは思わなかった、あれ？今思い出しましたがもしかしてMS-DOSってコマンドプロンプトだったんじゃ…
とにかく、今後も勉強を続け、仕事でつかえるようにしたいし、少なくとも何か毎日の生活に役立てることができるようになれば。
自分の開発したAIアプリに老後を助けてもらえるよう頑張りたいと思います。