【Python】画像認識で丸い野菜の識別

このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。

# 目次
1. はじめに
1. 実際のアプリケーション
1. 実行環境
1. 画像収集
1. 実行
1. 実行結果
1. まとめ


# 1.はじめに
私は転職活動のためにAidemyのAI アプリ開発講座を受講しました。
プログラミングは学生時代に少しだけかじった事がありましたが、それも10年前の話で全く分からない状態でスタートしました。

今回アプリを開発するにあたりテーマを何にしようかと思ったときに、以前からアニメ「銀の匙」や「もやしもん」を観て農家さんは休みが無く、なりてもどんどん減っていて大変だな楽にならないのかなと思うことがありました。
転職活動の中でAIを使い「規格品」と「そうでないもの」に判別できるようにした農家さんを知って、私も野菜の識別ができるものを作ろうと思いました。
私は農家ではないので規格品ではない野菜の写真を集めるのは困難だと思い、丸い野菜の識別ができるものを作成しました。

コードのそばに何をしているのか、自分でわかるよう細かくコメントを記入していますので他の初心者の方にも今している処理が何なのかわかりやすくなっているのではないかと思うので参考になればと思います。


# 2.実際のアプリケーション

https://yasai-4kxq.onrender.com


# 3.実行環境
* Windows 10
* Visual Studio Code
* Google Colaboratory

# 4.画像収集
scrapingにてWebから画像を収集しました。

```
# インストール、icrawlerはクローリングを行い画像を集めることができる
!pip install icrawler
# 必要な機能の読込
from icrawler.builtin import BingImageCrawler
```
```
# 画像のダウンロード、キーワード、集める最大枚数を指定する
bing_crawler = BingImageCrawler(downloader_threads=4,
                                storage={'root_dir': 'jagaimo-data'})
bing_crawler.crawl(keyword='jagaimo', filters=None, offset=0, max_num=200)
```
枚数の指定は最大枚数のため指定する枚数が集まるわけではありません。
```
# フォルダのzip化、容量を小さくして読み込みやすくする
!zip -r /content/jagaimo.zip /content/jagaimo-data
```
拡張子がすべてjpgであることを確認します。
自動収集を行うと料理画像やイラストなどが混ざるため、手作業にて不要なデータを削除しました。

私は5種類の野菜を識別するため、上記の作業を5セット行いました。

# 5.実行
 Aidemy講座の「CNNを用いた画像認識」などコースを何度も読み返したり、似たアプリを作っている方のコードを真似しながら講師の方に説明を受け進めていきました。
  
  ```
  import os #os機能がpythonで扱えるようにする
import cv2 #画像処理ライブラリ
import numpy as np #拡張何
import matplotlib.pyplot as plt #グラフの可視化
from tensorflow.keras.utils import to_categorical #正解ラベルをone-hotで求める
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 #最適化関数何


#野菜画像の格納　ファイル名を変数に格納
drive_kyabetu ="/content/drive/MyDrive/Colab Notebooks/data/kyabetu/"
drive_jagaimo = "/content/drive/MyDrive/Colab Notebooks/data/jagaimo/"
drive_tomato = "/content/drive/MyDrive/Colab Notebooks/data/tomato/"
drive_tamanegi = "/content/drive/MyDrive/Colab Notebooks/data/tamanegi/"
drive_kabocha = "/content/drive/MyDrive/Colab Notebooks/data/kabocha/"
```
格納するファイルパスの出し方の参考ページを添付します。

https://www.ushiji.online/colab-file-upload

```
#image_sizeを入れる
image_size = 100

#野菜画像のファイルを取得 画像一つ一つを読み込んでいる
path_kyabetu = [filename for filename in os.listdir(drive_kyabetu) if not filename.startswith('.')]
path_jagaimo = [filename for filename in os.listdir(drive_jagaimo) if not filename.startswith('.')]
path_tomato = [filename for filename in os.listdir(drive_tomato) if not filename.startswith('.')]
path_tamanegi = [filename for filename in os.listdir(drive_tamanegi) if not filename.startswith('.')]
path_kabocha = [filename for filename in os.listdir(drive_kabocha) if not filename.startswith('.')]


#野菜画像を格納するリスト作成
img_kyabetu = []
img_jagaimo = []
img_tomato = []
img_tamanegi = []
img_kabocha = []
for i in range(len(path_kyabetu)):#配列の長さを取得し,リスト内のデータの数だけループする
  img = cv2.imread(drive_kyabetu + path_kyabetu[i] )#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする上で指定しているので（80，80）の意味になる
  img_kyabetu.append(img)#画像配列に画像を加える
for i in range(len(path_jagaimo)):
  img = cv2.imread(drive_jagaimo +path_jagaimo[i] )
  img = cv2.resize(img,(image_size,image_size))
  img_jagaimo.append(img)
for i in range(len(path_tomato)):
  img = cv2.imread(drive_tomato +path_tomato[i] )
  img = cv2.resize(img,(image_size,image_size))
  img_tomato.append(img)
for i in range(len(path_tamanegi)):
  img = cv2.imread(drive_tamanegi +path_tamanegi[i] )
  img = cv2.resize(img,(image_size,image_size))
  img_tamanegi.append(img)
for i in range(len(path_kabocha)):
  img = cv2.imread(drive_kabocha +path_kabocha[i] )
  img = cv2.resize(img,(image_size,image_size))
  img_kabocha.append(img)

#np.arrayでXに学習画像を入れる
X = np.array(img_kyabetu +img_jagaimo + img_tomato + img_tamanegi + img_kabocha )

#np.arrayでｙに正解ラベルの作成(教師あり学習）、分類の名前を数字表現に変換しコンピュータに認識可能な形にする。label_unmはセットで入力
y = np.array([0]*len(img_kyabetu)+[1]*len(img_jagaimo)+[2]*len(img_tomato)+[3]*len(img_tamanegi)+[4]*len(img_kabocha))
label_unm = list(set(y))

#配列の要素をランダムに並べ替える。並べ替えないと後ろのかぼちゃだけが検証データになってしまう
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

#学習データと検証データを用意なんの計算してる。入れ替えたデータの前半の8割（0.8）を学習データに後半2割を検証データとして使う
X_train = X[:int(len(X)*0.8)]#8割を学習データとして使う
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]#2割を検証データとして使う
y_test = y[int(len(y)*0.8):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

#テンソルのオプション　画像のサイズを指定している
input_tensor = Input(shape=(image_size,image_size,3))

#転移学習のモデルとしてVGG16を使用
vgg16 = VGG16(include_top=False,weights='imagenet',input_tensor=input_tensor)

#モデルの定義 活性化関数シグモイド 転移学習の自作モデルとして下記のコードを作成
#数値はいったんよくある数値を入れてみて精度を確認、調整。
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))#Flattenは平坦化層。input_shapeは初期設定
top_model.add(Dense(256, activation="sigmoid"))
top_model.add(Dense(128, activation='sigmoid'))
top_model.add(Dense(10, activation='sigmoid'))
top_model.add(Dense(5, activation='softmax'))
```
全結合層とシグモイド関数を使用しました。
シグモイド関数はディープラーニングの活性化関数の一つで主に2値分類問題における出力層の活性化関数に用いられます。
グラフは滑らかな曲線になります。

```
#vgg16と自作のtop_modelを連結
model = Model(vgg16.input,top_model(vgg16.output))

#vgg16による特微抽出部分の重みを固定。
for layer in model.layers[:19]:
  layer.trainable = False

#コンピュータが実行可能な形式のオブジェクトコードに変換する
model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-2, momentum=0.9),
             metrics=['accuracy']
```
重みのmaxは19。19にする場合はコンパイルのlr=が1e-2。15なら1e-4。と2か所調整が必要です。
```

#学習の実行、精度を出力
history = model.fit(X_train, y_train, batch_size=32, epochs=100, verbose=1, validation_data=(X_test, y_test))
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
#{0}[0]はモデルの損失値。{0}[1]はモデルの評価値。
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))

#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()

#モデルを保存。アプリ画面(.pyファイル)を作るのに必要。
model.save("my_model.h5")
```
コードは下記のブログを参考にさせていただきました。

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

# 6.実行結果
```
validation loss:0.3994729518890381
validation accuracy:0.8785046935081482
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3669197/8e442fe3-2612-d3f2-d0b0-7be1265e26fd.png)

このグラフではオレンジと青の線が近いほど精度が良いことになります。
lossの数字は小さい方がよく、accuracyは多い方が精度が良くなります。
8割ほどの精度が出したかったのでモデル定義のところで中間層、ドロップアウトを入れる入れない、VGG16の重みを変更、image_sizeの変更、エポックの変更をして何度も実行してみました。
image_sizeやエポックは細かい分、精度は上がりますが時間がかかります。
私は今回「丸」という特徴を持っている野菜の識別ができるアプリを作りたかったので、精度の方を優先しました。
それでも、「たまねぎ」を「じゃがいも」や「とまと」と返すものもありました。


# 7.まとめ
アプリを作る中でばらばらだった知識が繋がっていくのが面白かったです。
講座やブログを進める中で自分で検索してもいまいちわからず講師の方に質問することが多くありました。講師の方と一緒に検索する事もあり、そうやって検索すればよかったのかと思う事が何度もありました。
これからは自力で検索できる力をもっとつけて、早くコードを記入できるように復習しながら新しい職種へチャレンジしたいと思います。