AIによる野菜識別器アプリの作成

# はじめに
初めまして、Yukimuraと申します。
ネットワークエンジニア(非AI)をしていますが、AIエンジニアをやりたいなと思っている今日この頃です。

E資格やDS検定(リテラシーレベル)は取得済みのため、AIに関する基本的な知識はありますが、実践力を身に着けたく、AidemyさんのAIアプリ開発講座を受講しました。

# 本記事の概要
本記事は、AIアプリ開発講座の集大成として、オリジナルアプリ(画像認識)を作成したことについてです。
オリジナルアプリのネタは色々考えたところ、実家が農家のため、野菜識別器とかいいんじゃないかなと思い、それに決めました。
作成した野菜識別器は、以下になります。

[![野菜識別器_スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019410/db5ca237-a3e8-a879-d0b0-1a6d5ab41f10.png)](https://vegetable-classifier-jf4w.onrender.com/)

実は、私よりも以前に同じ講座を受講されていた方が、野菜識別のアプリを作られていました。
なので、以下の記事はとても参考になりました。
(特にソースコードの解説が秀逸です。)
偉大な先輩、ありがとうございました。

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

では、どんなことをしたのかを、具体的に書いていきます。

# 作業環境
・Windows 11 PC
・Google Colaboratory

# ターゲット選定
すぐに作業に取り掛かりたい気持ちを抑えつつ、まずはターゲットを決めなければいけません。
画像から野菜を識別するのは決まっていますが、じゃあ一体何の野菜を採用するのか。
最初は「野菜 一覧」とかでググりながら、「野菜って色々あるなぁ、これなら50種類くらいいけるかな」などと命知らずなことを考えていました。

そんな時間はないだろう、とふと冷静になり、結局30種類を選び、更に画像の集まり具合から最終的に20種類に絞りました。

### 最終的に採用した野菜20種
アスパラガス、枝豆、かぼちゃ、キャベツ、きゅうり、小松菜、さつまいも、ジャガイモ、春菊、セロリ、玉ネギ、大根、トマト、なす、にんじん、にんにく、長ネギ、ピーマン、ほうれん草、レタス

### 今回は採用を見送った野菜10種
オクラ、ごぼう、ショウガ、とうもろこし、ニラ、白菜、パセリ、ブロッコリー、もやし、れんこん

# 画像収集
「icrawler」というwebから画像収集をしてくれる便利なツールがあるので、それを使います。
まずはコマンドプロンプトにて、以下のコマンドでインストール

```
pip install icrawler
```

そして以下のような、簡単なスクリプトを書いて実行すると、サクサクと収集してきてくれます。
```python:
from icrawler.builtin import BingImageCrawler
crawler = BingImageCrawler(storage={"root_dir": './daikon_bing'})
crawler.crawl(keyword='大根', max_num=300)
```

「BingImageCrawler」の部分を「GoogleImageCrawler」に変えれば、グーグルクローラーで収集ができます。
ただ、画像収集に関しては、BingImageCrawlerの方がいいと思いました。
試しに、"大根"について、BingImageCrawlerとGoogleImageCrawlerの両者で実行してみたのですが、前者は213枚、後者は73枚と3倍くらいの差があったからです。
なので、今回の画像収集は、全てBingImageCrawlerで行いました。

尚、野菜1種につき100枚は必要らしいので、max_numを300に設定して、200枚前後を集めました。
それらを、精査によって100枚程度まで選別します。
なので、200枚 × 30種類 = 約6000枚の画像を収集しました。
ツールを使うだけなので、これは大変ではありません。
大変なのは、この後です…。

#### 参考文献
https://atmarkit.itmedia.co.jp/ait/articles/2010/28/news018.html

https://chusotsu-program.com/python-icrawler/

# 画像の前処理
先述の画像約6000枚の精査です。
これがかなり大変でした。
お昼くらいから始めて、暗くなり始めたあたりで、ようやく終わりました…。
多分、6時間くらいはかかったと思います…。
「AI関連では、データ前処理が8割」なんて言葉を聞いたことがありますが、それを実感しました。
50種類でやらなくて、本当に良かったです！

精査方法は、目視で学習に使えそうなものを残しました。
料理の画像や、野菜の花の画像などを除外していくと、不思議と野菜1種につき200⇒100枚くらいに落ち着きました。
ここで、100枚を大幅に下回った10種類を外しました。
よって、最終的には、100枚 × 20種類 = 2000枚になります。
OpenCVによる画像の水増しは、時間の関係で今回はやりませんでした。

ここの精査がやや甘かったのと、有効な画像の水増しをしなかったのが、後々響いてきます。

# 画像のアップロード
今回、モデル生成はGoogle Colaboratoryで行うので、先述の2000枚の画像をアップロードします。
Googleドライブに画像をアップロードし、Google Colaboratoryでcontentフォルダ直下でマウントすればOKです。

※Google Colaboratoryへのファイルのアップロードって、意外とハマりポイントだったりします…。

#### 参考文献
https://blog.kikagaku.co.jp/google-colab-drive-mount

# モデル生成
モデル生成のソースコードは、以下になります。
<details><summary>ソースコード詳細はこちらをクリック</summary><div>

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

#モデルの保存
import os
from google.colab import files


# ファイルパス取得
path_asupara = os.listdir('./drive//MyDrive/vegetable_images/asupara/')
path_edamame = os.listdir('./drive//MyDrive/vegetable_images/edamame/')
path_kabocha = os.listdir('./drive//MyDrive/vegetable_images/kabocha/')
path_kyabetsu = os.listdir('./drive//MyDrive/vegetable_images/kyabetsu/')
path_kyuuri = os.listdir('./drive//MyDrive/vegetable_images/kyuuri/')
path_komatsuna = os.listdir('./drive//MyDrive/vegetable_images/komatsuna/')
path_satsumaimo = os.listdir('./drive//MyDrive/vegetable_images/satsumaimo/')
path_jagaimo = os.listdir('./drive//MyDrive/vegetable_images/jagaimo/')
path_shungiku = os.listdir('./drive//MyDrive/vegetable_images/shungiku/')
path_serori = os.listdir('./drive//MyDrive/vegetable_images/serori/')
path_tamanegi = os.listdir('./drive//MyDrive/vegetable_images/tamanegi/')
path_daikon = os.listdir('./drive//MyDrive/vegetable_images/daikon/')
path_tomato = os.listdir('./drive//MyDrive/vegetable_images/tomato/')
path_nasu = os.listdir('./drive//MyDrive/vegetable_images/nasu/')
path_ninjin = os.listdir('./drive//MyDrive/vegetable_images/ninjin/')
path_ninniku = os.listdir('./drive//MyDrive/vegetable_images/ninniku/')
path_naganegi = os.listdir('./drive//MyDrive/vegetable_images/naganegi/')
path_piman = os.listdir('./drive//MyDrive/vegetable_images/piman/')
path_hourensou = os.listdir('./drive//MyDrive/vegetable_images/hourensou/')
path_retasu = os.listdir('./drive//MyDrive/vegetable_images/retasu/')

# 野菜画像格納用変数宣言
img_asupara = []
img_edamame = []
img_kabocha = []
img_kyabetsu = []
img_kyuuri = []
img_komatsuna = []
img_satsumaimo = []
img_jagaimo = []
img_shungiku = []
img_serori = []
img_tamanegi = []
img_daikon = []
img_tomato = []
img_nasu = []
img_ninjin = []
img_ninniku = []
img_naganegi = []
img_piman = []
img_hourensou = []
img_retasu = []


# 野菜画像格納
for i in range(len(path_asupara)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/asupara/' + path_asupara[i])
    img = cv2.resize(img, (50,50))
    img_asupara.append(img)

for i in range(len(path_edamame)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/edamame/' + path_edamame[i])
    img = cv2.resize(img, (50,50))
    img_edamame.append(img)

for i in range(len(path_kabocha)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/kabocha/' + path_kabocha[i])
    img = cv2.resize(img, (50,50))
    img_kabocha.append(img)

for i in range(len(path_kyabetsu)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/kyabetsu/' + path_kyabetsu[i])
    img = cv2.resize(img, (50,50))
    img_kyabetsu.append(img)

for i in range(len(path_kyuuri)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/kyuuri/' + path_kyuuri[i])
    img = cv2.resize(img, (50,50))
    img_kyuuri.append(img)

for i in range(len(path_komatsuna)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/komatsuna/' + path_komatsuna[i])
    img = cv2.resize(img, (50,50))
    img_komatsuna.append(img)

for i in range(len(path_satsumaimo)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/satsumaimo/' + path_satsumaimo[i])
    img = cv2.resize(img, (50,50))
    img_satsumaimo.append(img)

for i in range(len(path_jagaimo)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/jagaimo/' + path_jagaimo[i])
    img = cv2.resize(img, (50,50))
    img_jagaimo.append(img)

for i in range(len(path_shungiku)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/shungiku/' + path_shungiku[i])
    img = cv2.resize(img, (50,50))
    img_shungiku.append(img)

for i in range(len(path_serori)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/serori/' + path_serori[i])
    img = cv2.resize(img, (50,50))
    img_serori.append(img)

for i in range(len(path_tamanegi)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/tamanegi/' + path_tamanegi[i])
    img = cv2.resize(img, (50,50))
    img_tamanegi.append(img)

for i in range(len(path_daikon)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/daikon/' + path_daikon[i])
    img = cv2.resize(img, (50,50))
    img_daikon.append(img)

for i in range(len(path_tomato)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/tomato/' + path_tomato[i])
    img = cv2.resize(img, (50,50))
    img_tomato.append(img)

for i in range(len(path_nasu)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/nasu/' + path_nasu[i])
    img = cv2.resize(img, (50,50))
    img_nasu.append(img)

for i in range(len(path_ninjin)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/ninjin/' + path_ninjin[i])
    img = cv2.resize(img, (50,50))
    img_ninjin.append(img)

for i in range(len(path_ninniku)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/ninniku/' + path_ninniku[i])
    img = cv2.resize(img, (50,50))
    img_ninniku.append(img)

for i in range(len(path_naganegi)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/naganegi/' + path_naganegi[i])
    img = cv2.resize(img, (50,50))
    img_naganegi.append(img)

for i in range(len(path_piman)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/piman/' + path_piman[i])
    img = cv2.resize(img, (50,50))
    img_piman.append(img)

for i in range(len(path_hourensou)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/hourensou/' + path_hourensou[i])
    img = cv2.resize(img, (50,50))
    img_hourensou.append(img)

for i in range(len(path_retasu)):
    img = cv2.imread('./drive//MyDrive/vegetable_images/retasu/' + path_retasu[i])
    img = cv2.resize(img, (50,50))
    img_retasu.append(img)

# Xに画像を入れる
X = np.array(img_asupara 
             + img_edamame 
             + img_kabocha 
             + img_kyabetsu 
             + img_kyuuri 
             + img_komatsuna 
             + img_satsumaimo 
             + img_jagaimo 
             + img_shungiku 
             + img_serori 
             + img_tamanegi 
             + img_daikon 
             + img_tomato 
             + img_nasu 
             + img_ninjin 
             + img_ninniku 
             + img_naganegi 
             + img_piman 
             + img_hourensou 
             + img_retasu)
# yに正解ラベルを入れる
y =  np.array([0]*len(img_asupara) 
              + [1]*len(img_edamame) 
              + [2]*len(img_kabocha) 
              + [3]*len(img_kyabetsu) 
              + [4]*len(img_kyuuri) 
              + [5]*len(img_komatsuna) 
              + [6]*len(img_satsumaimo) 
              + [7]*len(img_jagaimo) 
              + [8]*len(img_shungiku) 
              + [9]*len(img_serori) 
              + [10]*len(img_tamanegi) 
              + [11]*len(img_daikon) 
              + [12]*len(img_tomato) 
              + [13]*len(img_nasu) 
              + [14]*len(img_ninjin) 
              + [15]*len(img_ninniku) 
              + [16]*len(img_naganegi) 
              + [17]*len(img_piman) 
              + [18]*len(img_hourensou) 
              + [19]*len(img_retasu))

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)

# モデルにVGG16を使用
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(Dense(1024, activation='relu'))
top_model.add(Dropout(0.7))
top_model.add(Dense(20, activation='softmax'))

# vggと、top_modelの連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# vggの層の重みを変更不能にする
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=64, epochs=70, validation_data=(X_test, y_test))

# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
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()

#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save(os.path.join(result_dir, 'model.h5'))

files.download( '/content/results/model.h5' ) 
```
</div></details>

**CNN (Convolutional Neural Network)** による画像認識のコードですが、正直なところ、データの読み込みあたりの書き方が、力技ごり押しのようで、イケてないですね。
リストとFor文を使えば、もっとスマートに書けそうな気がしましたが、動くものを作るのを最優先にしましたので、ああいった感じになってしまいました。
時間があるときに、リファクタリングをしたいと思っています。

処理の大まかな流れとしましては、以下の通りです。

データ読み込み
↓
データ分割
↓
モデル定義
↓
VGG16との連結
↓
コンパイル
↓
学習
↓
精度の評価
↓
評価のプロット
↓
モデルの保存

この中で、モデル定義(Denseのunits)と学習(batch_sizeとepochs)におけるパラメータを調整して、より良いモデル生成をします。

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

#モデルの保存
import os
from google.colab import files
```

importしたライブラリは、上記になります。


```python:
# 特徴量抽出部分のモデル定義
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(1024, activation='relu'))
top_model.add(Dropout(0.7))
top_model.add(Dense(20, activation='softmax'))
```

モデル定義につきましては、今回の場合、これ以上中間層を増やすと、accuracyが著しく落ちるため、これで落ち着きました。
活性化関数は、sigmoid関数よりもrelu関数の方が明らかにaccuracyが良かったため、relu関数を指定しました。

```python:
# 学習
history = model.fit(X_train, y_train, batch_size=64, epochs=70, validation_data=(X_test, y_test))
```

学習につきまして、batch_sizeは、64と128ではそれほど変わらず。
epochsは、50だとちょっとaccuracyが上がり切らず、100だとちょっと多すぎたので、真ん中の70くらいにしました。



様々なパラメータのパターンを何十回か測定したところ、最高でval_accuracyが55%ほどでした。
野菜識別器なので、70%くらいは欲しかったのですが、残念です。

<details><summary>学習結果詳細はこちらをクリック</summary>
Epoch 1/70
/usr/local/lib/python3.7/dist-packages/keras/optimizer_v2/gradient_descent.py:102: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
  super(SGD, self).__init__(name, **kwargs)
25/25 [==============================] - 2s 40ms/step - loss: 35.8766 - accuracy: 0.0748 - val_loss: 11.8645 - val_accuracy: 0.1633
Epoch 2/70
25/25 [==============================] - 1s 29ms/step - loss: 23.2324 - accuracy: 0.1706 - val_loss: 8.6124 - val_accuracy: 0.3087
Epoch 3/70
25/25 [==============================] - 1s 29ms/step - loss: 16.8910 - accuracy: 0.2383 - val_loss: 7.4802 - val_accuracy: 0.3673
Epoch 4/70
25/25 [==============================] - 1s 29ms/step - loss: 13.7042 - accuracy: 0.3086 - val_loss: 6.6567 - val_accuracy: 0.4005
Epoch 5/70
25/25 [==============================] - 1s 29ms/step - loss: 11.3202 - accuracy: 0.3380 - val_loss: 6.0878 - val_accuracy: 0.4107
Epoch 6/70
25/25 [==============================] - 1s 29ms/step - loss: 9.8168 - accuracy: 0.3649 - val_loss: 5.3787 - val_accuracy: 0.4643
Epoch 7/70
25/25 [==============================] - 1s 29ms/step - loss: 8.6416 - accuracy: 0.3879 - val_loss: 4.9492 - val_accuracy: 0.4745
Epoch 8/70
25/25 [==============================] - 1s 29ms/step - loss: 7.4661 - accuracy: 0.4147 - val_loss: 4.7056 - val_accuracy: 0.4847
Epoch 9/70
25/25 [==============================] - 1s 29ms/step - loss: 6.7184 - accuracy: 0.4294 - val_loss: 4.5220 - val_accuracy: 0.4821
Epoch 10/70
25/25 [==============================] - 1s 29ms/step - loss: 6.0693 - accuracy: 0.4383 - val_loss: 4.3961 - val_accuracy: 0.4745
Epoch 11/70
25/25 [==============================] - 1s 29ms/step - loss: 5.3708 - accuracy: 0.4601 - val_loss: 4.2232 - val_accuracy: 0.4796
Epoch 12/70
25/25 [==============================] - 1s 28ms/step - loss: 4.7698 - accuracy: 0.4805 - val_loss: 4.0237 - val_accuracy: 0.5000
Epoch 13/70
25/25 [==============================] - 1s 29ms/step - loss: 4.3647 - accuracy: 0.4978 - val_loss: 3.9316 - val_accuracy: 0.4974
Epoch 14/70
25/25 [==============================] - 1s 30ms/step - loss: 4.3106 - accuracy: 0.5137 - val_loss: 3.8294 - val_accuracy: 0.5026
Epoch 15/70
25/25 [==============================] - 1s 29ms/step - loss: 4.0096 - accuracy: 0.5304 - val_loss: 3.6782 - val_accuracy: 0.5204
Epoch 16/70
25/25 [==============================] - 1s 29ms/step - loss: 3.5132 - accuracy: 0.5438 - val_loss: 3.5664 - val_accuracy: 0.5332
Epoch 17/70
25/25 [==============================] - 1s 29ms/step - loss: 3.2925 - accuracy: 0.5412 - val_loss: 3.5016 - val_accuracy: 0.5204
Epoch 18/70
25/25 [==============================] - 1s 29ms/step - loss: 3.1134 - accuracy: 0.5482 - val_loss: 3.4349 - val_accuracy: 0.5204
Epoch 19/70
25/25 [==============================] - 1s 29ms/step - loss: 2.9102 - accuracy: 0.5636 - val_loss: 3.3871 - val_accuracy: 0.5332
Epoch 20/70
25/25 [==============================] - 1s 29ms/step - loss: 2.6021 - accuracy: 0.5930 - val_loss: 3.3226 - val_accuracy: 0.5230
Epoch 21/70
25/25 [==============================] - 1s 29ms/step - loss: 2.6292 - accuracy: 0.5968 - val_loss: 3.2329 - val_accuracy: 0.5255
Epoch 22/70
25/25 [==============================] - 1s 29ms/step - loss: 2.3134 - accuracy: 0.6064 - val_loss: 3.1330 - val_accuracy: 0.5408
Epoch 23/70
25/25 [==============================] - 1s 29ms/step - loss: 2.4324 - accuracy: 0.5968 - val_loss: 3.0966 - val_accuracy: 0.5383
Epoch 24/70
25/25 [==============================] - 1s 29ms/step - loss: 2.1360 - accuracy: 0.6102 - val_loss: 3.0476 - val_accuracy: 0.5485
Epoch 25/70
25/25 [==============================] - 1s 29ms/step - loss: 2.1603 - accuracy: 0.6115 - val_loss: 3.0394 - val_accuracy: 0.5434
Epoch 26/70
25/25 [==============================] - 1s 30ms/step - loss: 1.9442 - accuracy: 0.6345 - val_loss: 2.9954 - val_accuracy: 0.5459
Epoch 27/70
25/25 [==============================] - 1s 29ms/step - loss: 1.8971 - accuracy: 0.6313 - val_loss: 2.9952 - val_accuracy: 0.5357
Epoch 28/70
25/25 [==============================] - 1s 29ms/step - loss: 1.7660 - accuracy: 0.6530 - val_loss: 3.0271 - val_accuracy: 0.5383
Epoch 29/70
25/25 [==============================] - 1s 29ms/step - loss: 1.7536 - accuracy: 0.6486 - val_loss: 2.9993 - val_accuracy: 0.5255
Epoch 30/70
25/25 [==============================] - 1s 30ms/step - loss: 1.5592 - accuracy: 0.6792 - val_loss: 2.9644 - val_accuracy: 0.5230
Epoch 31/70
25/25 [==============================] - 1s 29ms/step - loss: 1.5253 - accuracy: 0.6850 - val_loss: 2.9286 - val_accuracy: 0.5281
Epoch 32/70
25/25 [==============================] - 1s 29ms/step - loss: 1.4880 - accuracy: 0.6843 - val_loss: 2.8948 - val_accuracy: 0.5204
Epoch 33/70
25/25 [==============================] - 1s 29ms/step - loss: 1.5461 - accuracy: 0.6607 - val_loss: 2.8717 - val_accuracy: 0.5281
Epoch 34/70
25/25 [==============================] - 1s 30ms/step - loss: 1.4480 - accuracy: 0.6901 - val_loss: 2.8396 - val_accuracy: 0.5383
Epoch 35/70
25/25 [==============================] - 1s 29ms/step - loss: 1.4154 - accuracy: 0.6920 - val_loss: 2.8299 - val_accuracy: 0.5357
Epoch 36/70
25/25 [==============================] - 1s 30ms/step - loss: 1.2613 - accuracy: 0.6882 - val_loss: 2.8418 - val_accuracy: 0.5179
Epoch 37/70
25/25 [==============================] - 1s 29ms/step - loss: 1.3110 - accuracy: 0.6978 - val_loss: 2.8377 - val_accuracy: 0.5332
Epoch 38/70
25/25 [==============================] - 1s 30ms/step - loss: 1.1576 - accuracy: 0.7157 - val_loss: 2.8055 - val_accuracy: 0.5383
Epoch 39/70
25/25 [==============================] - 1s 29ms/step - loss: 1.2242 - accuracy: 0.7227 - val_loss: 2.7894 - val_accuracy: 0.5485
Epoch 40/70
25/25 [==============================] - 1s 29ms/step - loss: 1.1093 - accuracy: 0.7323 - val_loss: 2.7611 - val_accuracy: 0.5408
Epoch 41/70
25/25 [==============================] - 1s 29ms/step - loss: 1.2431 - accuracy: 0.7450 - val_loss: 2.7135 - val_accuracy: 0.5357
Epoch 42/70
25/25 [==============================] - 1s 29ms/step - loss: 1.1525 - accuracy: 0.7348 - val_loss: 2.7137 - val_accuracy: 0.5383
Epoch 43/70
25/25 [==============================] - 1s 30ms/step - loss: 1.0988 - accuracy: 0.7361 - val_loss: 2.7160 - val_accuracy: 0.5434
Epoch 44/70
25/25 [==============================] - 1s 29ms/step - loss: 1.0774 - accuracy: 0.7470 - val_loss: 2.7199 - val_accuracy: 0.5536
Epoch 45/70
25/25 [==============================] - 1s 30ms/step - loss: 1.0711 - accuracy: 0.7342 - val_loss: 2.7277 - val_accuracy: 0.5459
Epoch 46/70
25/25 [==============================] - 1s 29ms/step - loss: 0.9480 - accuracy: 0.7597 - val_loss: 2.7460 - val_accuracy: 0.5408
Epoch 47/70
25/25 [==============================] - 1s 29ms/step - loss: 1.0666 - accuracy: 0.7514 - val_loss: 2.7426 - val_accuracy: 0.5357
Epoch 48/70
25/25 [==============================] - 1s 30ms/step - loss: 1.0483 - accuracy: 0.7406 - val_loss: 2.7388 - val_accuracy: 0.5357
Epoch 49/70
25/25 [==============================] - 1s 30ms/step - loss: 0.9762 - accuracy: 0.7489 - val_loss: 2.7414 - val_accuracy: 0.5306
Epoch 50/70
25/25 [==============================] - 1s 30ms/step - loss: 0.8907 - accuracy: 0.7655 - val_loss: 2.7499 - val_accuracy: 0.5383
Epoch 51/70
25/25 [==============================] - 1s 29ms/step - loss: 0.8555 - accuracy: 0.7764 - val_loss: 2.7408 - val_accuracy: 0.5408
Epoch 52/70
25/25 [==============================] - 1s 30ms/step - loss: 0.8303 - accuracy: 0.7885 - val_loss: 2.7335 - val_accuracy: 0.5510
Epoch 53/70
25/25 [==============================] - 1s 30ms/step - loss: 0.9116 - accuracy: 0.7585 - val_loss: 2.7355 - val_accuracy: 0.5357
Epoch 54/70
25/25 [==============================] - 1s 30ms/step - loss: 0.9020 - accuracy: 0.7655 - val_loss: 2.7264 - val_accuracy: 0.5306
Epoch 55/70
25/25 [==============================] - 1s 29ms/step - loss: 0.8301 - accuracy: 0.7783 - val_loss: 2.7055 - val_accuracy: 0.5408
Epoch 56/70
25/25 [==============================] - 1s 30ms/step - loss: 0.7630 - accuracy: 0.7968 - val_loss: 2.7262 - val_accuracy: 0.5383
Epoch 57/70
25/25 [==============================] - 1s 29ms/step - loss: 0.7734 - accuracy: 0.7879 - val_loss: 2.7178 - val_accuracy: 0.5357
Epoch 58/70
25/25 [==============================] - 1s 30ms/step - loss: 0.7316 - accuracy: 0.7949 - val_loss: 2.6867 - val_accuracy: 0.5459
Epoch 59/70
25/25 [==============================] - 1s 30ms/step - loss: 0.8420 - accuracy: 0.7955 - val_loss: 2.6716 - val_accuracy: 0.5434
Epoch 60/70
25/25 [==============================] - 1s 29ms/step - loss: 0.6739 - accuracy: 0.8026 - val_loss: 2.6827 - val_accuracy: 0.5459
Epoch 61/70
25/25 [==============================] - 1s 30ms/step - loss: 0.6887 - accuracy: 0.8102 - val_loss: 2.6769 - val_accuracy: 0.5434
Epoch 62/70
25/25 [==============================] - 1s 30ms/step - loss: 0.7280 - accuracy: 0.8032 - val_loss: 2.6510 - val_accuracy: 0.5485
Epoch 63/70
25/25 [==============================] - 1s 30ms/step - loss: 0.7558 - accuracy: 0.7923 - val_loss: 2.6609 - val_accuracy: 0.5408
Epoch 64/70
25/25 [==============================] - 1s 29ms/step - loss: 0.6398 - accuracy: 0.8224 - val_loss: 2.6271 - val_accuracy: 0.5510
Epoch 65/70
25/25 [==============================] - 1s 30ms/step - loss: 0.6769 - accuracy: 0.8051 - val_loss: 2.6236 - val_accuracy: 0.5408
Epoch 66/70
25/25 [==============================] - 1s 30ms/step - loss: 0.6145 - accuracy: 0.8217 - val_loss: 2.6321 - val_accuracy: 0.5332
Epoch 67/70
25/25 [==============================] - 1s 29ms/step - loss: 0.6556 - accuracy: 0.8141 - val_loss: 2.6275 - val_accuracy: 0.5434
Epoch 68/70
25/25 [==============================] - 1s 30ms/step - loss: 0.6336 - accuracy: 0.8198 - val_loss: 2.6252 - val_accuracy: 0.5485
Epoch 69/70
25/25 [==============================] - 1s 30ms/step - loss: 0.6218 - accuracy: 0.8288 - val_loss: 2.6194 - val_accuracy: 0.5510
Epoch 70/70
25/25 [==============================] - 1s 30ms/step - loss: 0.5855 - accuracy: 0.8371 - val_loss: 2.6253 - val_accuracy: 0.5561
13/13 [==============================] - 0s 19ms/step - loss: 2.6253 - accuracy: 0.5561
Test loss: 2.625286102294922
Test accuracy: 0.5561224222183228

Downloading "model.h5": 
</details>

![モデル精度.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019410/7d81387e-6719-2638-1919-c29f8ffb3baf.png)

おそらく、今の画像データでは、これ以上は出ないでしょう。
val_accuracyを上げるには、画像データの更に厳格な精査と、有効な画像の水増しが必要だと思います。


# まとめ
企画からモデル生成、最終的にアプリ公開まで一通り実践できたので、とても良い経験になりました。

AIアプリ開発講座の3ヶ月コースでしたが、実質最後の3週間くらいで一気に詰め込んだ感じなので、最後は時間が足りなかったですね。
(夏休みの宿題を最終日に頑張る感じを思い出しました。)

最後に、今後の改善点をまとめました。
１．画像の精査をより厳しくする
２．有効な画像の水増しをする
３．データの読み込みのソースコードをスマートに改良する

これらを実施することで、真の野菜識別器を名乗ることができるかと思います。