機械学習を用いて麺類分類器をつくってみた

# [目次]
[1.はじめに](#1-はじめに)
[2.実行環境](#2-実行環境)
[3.準備](#3-準備)
[4.画像収集から学習モデル](#4-画像収集から学習モデル)
[5.分析](#5-分析)
[6.結果](#6-結果)
[7.考察](#7-考察)


# 1. はじめに
はじめまして！
プログラミング未経験な私、思いつきてAidemyさんのAIアプリ開発講座、6ヶ月コースを受講してみました。飽きっぽくて期限内に終了できるか不安でしたがなんとか終われそうです。
[Aidemy Premium Plan](https://aidemy.net/grit/premium/?utm_source=google&utm_medium=cpc&utm_campaign=brand_brand_PPLP&gclid=Cj0KCQiAic6eBhCoARIsANlox876mQXJNdoEgRJ5hoddAY0pbQ0brbQEyI0HB4VUsA0VrMOuUo8fHf4aAh8aEALw_wcB)

はじめて作成した機械学習の成果を供覧します。

`テーマ：『麺類を分類してみた』`:ramen::spaghetti:
＊誰もが好きな「麺類」をテーマに、画像から特徴を抽出し、分類することで麺類の魅力を再確認しようと思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/14e7f626-76ef-66a6-0d4d-c4d257c0df6f.png)

# 2.　実行環境:computer:
＊Google Colaboratory
＊Visual Studio Code


# 3.　準備
まず初めに、
Google Colaboratory(Colab)を使ってGoogleドライブをマウント(連結)します。

```python
from google.colab import drive
drive.mount('/content/drive')
```
次にicrawlerをインストールします。
```python
!pip install icrawler
```

Bing用クローラーのモジュールをインポートします。
```python
from icrawler.builtin import BingImageCrawler
```

# 4.　画像収集から学習モデル
早速画像を集めました。
・うどん
・そば
・パスタ
・ラーメン
検索キーワードは日本語で、それぞれ最大300枚として収集しました。

<details><summary>画像収集</summary>

```python
bing_crawler = BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir':'/content/drive/MyDrive/課題/うどん'})
bing_crawler.crawl(keyword="うどん", max_num=300)

bing_crawler = BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir':'/content/drive/MyDrive/課題/ラーメン'})
bing_crawler.crawl(keyword="ラーメン", max_num=300)

bing_crawler = BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir':'/content/drive/MyDrive/課題/そば'})
bing_crawler.crawl(keyword="そば", max_num=300)

bing_crawler = BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir':'/content/drive/MyDrive/課題/パスタ'})
bing_crawler.crawl(keyword="パスタ", max_num=300)
```
</details>



必要なモジュールをインポートします。
openCV、numpy、matplotlib、tensorflowなどのライブラリーを使用しました。

```python
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
```

それぞれの麺類フォルダの中からファイル一覧を取得し、画像データ格納用リストを作成しました。
画像の前処理として　RGBの変換、画像のリサイズ(300ｘ300→アプリは100x100)、画像データ格納用リストに読み込んだ画像を追加していく等の処理を行いました。

<details><summary>画像の前処理</summary>

```python
path_udon = os.listdir("/content/drive/MyDrive/課題/うどん")
path_soba = os.listdir('/content/drive/MyDrive/課題/そば')
path_ramen = os.listdir('/content/drive/MyDrive/課題/ラーメン')
path_pasta = os.listdir('/content/drive/MyDrive/課題/パスタ')

img_udon = []
img_soba = []
img_ramen = []
img_pasta = []

for i in range(len(path_udon)):
    img = cv2.imread("/content/drive/MyDrive/課題/うどん/" + path_udon[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b]) 
    img = cv2.resize(img, (300,300)) 
    img_udon.append(img) 

for i in range(len(path_soba)):
    img = cv2.imread('/content/drive/MyDrive/課題/そば/' + path_soba [i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (300,300))
    img_soba.append(img)

for i in range(len(path_ramen)):
    img = cv2.imread('/content/drive/MyDrive/課題/ラーメン/' + path_ramen[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (300,300))
    img_ramen.append(img)

for i in range(len(path_pasta)):
    img = cv2.imread('/content/drive/MyDrive/課題/パスタ/' + path_pasta[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (300,300))
    img_pasta.append(img)
```
</details>

収集した画像から合わない物は省いた結果、それぞれの枚数が252/265/281/234枚でした。
精度を上げるためには画像枚数を増やしたい。そのため水増しを試みました。

![キャプチャ　水増し.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/8829e486-c032-9595-7c41-796c06aa47aa.jpeg)


Xに学習画像、yに正解ラベル（0：うどん、1：そば、2：ラーメン、3：パスタ）としました。
訓練用データ8割、テストデータ2割にわけ、訓練データのみ4倍に水増ししました。閾値処理と左右反転した画像を増やして、元データを4倍にしてみました。

<details><summary>訓練データのみ水増し</summary>

```python
X = np.array(img_udon+img_soba+img_ramen+img_pasta)
y = np.array([0]*len(img_udon) + [1]*len(img_soba) + [2]*len(img_ramen) + [3]*len(img_pasta))
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):]


def scratch_image(img, flip=True, thr=True, filt=True, resize=True, erode=True):
    methods = [flip, thr, filt, resize, erode]   
    img_size = img.shape
    filter1 = np.ones((3, 3))
    images = [img]
    scratch = np.array([
        lambda x: cv2.flip(x, 1), 
        lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1],
        lambda x: cv2.GaussianBlur(x, (5,5), 0),
        lambda x: cv2.resize(cv2.resize(x, (x.shape[1]//5, x.shape[0]//5)),(x.shape[1], x.shape[0])),
        lambda x: cv2.erode(x, filter1)
    ])
    

    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])
    for func in scratch[methods]:
        images = doubling_images(func, images)
    return images

scratch_train_images = []
scratch_train_labels = []
for im,label in zip(X_train,y_train):
    tmp = scratch_image(im, flip=True, thr=True, filt=False, resize=False, erode=False)
    scratch_train_images += tmp
    scratch_train_labels += [label]*4


X_train=np.array(scratch_train_images)
# 正解ラベルをone-hot, to_categoricalを使う
y_train = to_categorical(np.array(scratch_train_labels))
y_test=to_categorical(y_test)
```
</details>


# 5.  分析

ココで準備が整いました。

転移学習のモデルとしてVGG16を使用しました。
VGG16は2014年のILSVRで好成績を収めたCNNモデルで、現在でも様々な研究に使用されています。
![VGG16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/f97b74fe-093d-e1c2-2c72-fe16cd6db17d.png)

引用:Karen Simonyan and Andrew Zisserman（2014）：Very Deep Convolutional Networks for Large-Scale Image Recognition. arXiv:1409.1556［cs］(September 2014).

<details><summary>解析</summary>

```python
input_tensor = Input(shape=(300, 300, 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(256, activation='relu'))
top_model.add(Dense(4, activation='softmax'))

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

model.summary()

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'])

print(X_train.shape)
print(y_train)
history=model.fit(X_train, y_train, batch_size=32, epochs=30, validation_data=(X_test, y_test))
```
</details>


何回かトライしましたが最終的にbatch_size=32, epochs=30でやってみました。


# 6.　結果
Epoch 30/30
104/104 [==============================] - 31s 300ms/step - loss: 0.0426 - accuracy: 0.9852 - val_loss: 1.2223 - val_accuracy: 0.7729

正解率、損失率を示します。
4回以降は正解率、損失率ともに変化なくイマイチです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/11c6172f-eab7-814c-ca0d-1276c3a6c882.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/3a032e3e-d98a-de4d-f1c0-35f471255958.png)


因みに、、アプリ作成のためのGitに用量制限があり、300x300を100x100に落として処理しました。精度が8割から7割に低下していますが全体の傾向は同じでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/7223d223-e4ba-0274-63b2-e3c94aba4edf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/7ea7403b-1950-4f18-c164-2b80b34597bd.png)




また、個別に結果を確認したところ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/695d3159-3ac2-c7f6-f9b4-9717c5259ed4.png)
ラーメン


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/b0484033-d306-2e5f-aced-82210bf86fe9.png)
うどん

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/fe0cddf9-b8ca-fe94-1ea3-318bcb7e66a5.png)
そば

大正解でした！！

これらを判別するアプリも作成しました。　https://app-xl42.onrender.com/
一度お試しください:ramen::spaghetti:
　　![キャプチャアプリ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/be9a4eda-8da0-1506-1c32-e758b474c877.jpeg)




# 7.　考察
①考察
精度を上げるため、batch_sizeやepochs数、画像サイズを変更してみたがあまり変化がなかったです。

![キャプチャ4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092432/d57aa75d-a613-9a95-d9d4-c276d3f1ac9b.jpeg)

無作為に取り出した代表画像をみてみると、私の目では、
    うどん→汁＋生or半熟卵
　　そば→ざる（器が四角）
　　パスタ→トマト系の赤色
　　ラーメン→汁＋煮orゆで卵
の傾向があるように思いました。VGG16さんがどこに注目しているのか気になりました。

②今後の展望
4回以降は正解率に変化なく損失率は微増しており過学習状態でした。この原因としては
　①　画像水増しはしてみましたが、収集した元画像枚数が252/265/281/234枚と少なく
　②　キーワードから収集した画像なので典型的でないものや他のもの（ビールとか）が映り込んでいるものも削除せず使用した事などが原因でしょうか。
麺類という類似した画像が多いため、最初に典型的で有効な画像をきちんと選ぶ必要があったと反省しました。諸先輩方が最初のデータ収集が大切だと書かれていたことを実感しました。

今回はVGG16を使用しましたが、他のさらに上位モデル（GoogLe NetやDense Net）の使用を検討してみても良かったのかもしれないです。

麺類の写真を眺めているとお腹がすいてきます。
今後は麺類以外の他の画像でも画像分類にトライしてみたい。また、画像分類以外のAIの活用方法についても学びたいです。が、講座が終了してしまうと独学では難しそうです。残念です。

飽きっぽくて途中、中断してしまっていましたがチューターの方々から優しくご指導いただき、期限内になんとか終われそうです。

Pythonを学び、簡単ですがアプリを作ることができたので、AidemyさんのAIアプリ開発講座を受講して良かったと感じています。

以下、参考にさせてもらった記事です。
https://qiita.com/shizen-shin/items/a997bc228fa2850c9fce

https://qiita.com/a_tonbo/items/0696fd3fcbdeed4e64cf

https://qiita.com/risaQiita55/items/459a794a5f988f3ddf2e