LoginSignup
3
0

More than 1 year has passed since last update.

文系大卒でITと全く関わりない会社員が野菜識別アプリ作ってみた(初心者向け)

Last updated at Posted at 2021-07-04

0.本記事の概要

この記事はAidemyさんのAIアプリ開発コースを通して、
プログラミングを0から学び、野菜を識別するアプリを作成した過程を記録したモノです。
私のように、全くプログラミングの知識がない方にぜひ読んで欲しいです。

1.はじめに

私は文系大学卒かつITと全く関係ない業種(サービス業)の会社員で営業職をやっています。
受講前は「機械学習」や「ディープラーニング」の言葉はもちろん、「Python」や「HTML」といったプログラミング言語も知りませんでした。
しかし、好きなゲームの勝敗予測でAIが取り入れられていたことやAI棋士がプロ棋士に勝ったニュースを見て、今後プログラミングやAI技術の需要がさらに飛躍すると思ったことで受講に至りました。
また、数学が好きなことや将来に向けて転職を考えていたこともプログラミング学習の後押しになっています。

2.アイデミーでの学習振り返り

今回、私は下記のような順序で学習していきました(番号はAIアプリ開発コースのシラバスをご参照下さい)。
https://aidemy.net/grit/premium/curriculums/ai-app/courses/
(1)①Python入門 ~ ⑪男女識別 を受講して全体像を掴む(2か月程度)。
(2)①Python入門 ~ ⑪男女識別 の添削課題を順番に提出する(1か月程度)。
(3)⑫Flask入門のためのHTML&CSS ~ ⑭MNISTを用いた手書きアプリ作成 を受講する(2週間)
(4)⑮アプリ製作 ~ ⑱Herokuへのデプロイ方法 を受講(3週間)
(5)⑲ブログ作成(1週間)
※途中、全く学習をしない期間が1か月ほどありました。

一通り全体の講座を受けてから、①Python入門に戻って添削課題を提出していく流れで学習をしました。
これから学習を始める初心者の方には、できれば各講座と添削課題は都度提出していくことをおすすめします。
私は各講座を受けてから添削課題を進めようと思いましたが、⑩CNNを用いた画像認識 辺りで全く理解できずに自信を無くしてました。
各講座毎に添削課題を一つ一つクリアしていくことで、自信を無くしてしまう頻度も減らすことができると思います。

また、私のような初心者は学習している中で下記の心構えが必要だと感じました。

・独学では無理だと割り切って、詳しい人に聞く。
(私は知人に聞いていました。独学だと自分の「解釈」で終わってしまうこともあるので、人に聞きましょう。)
・完璧に理解することはできないので、6~8割の理解で次に進む。
(「これはこういうモノなんだ」と割り切って学習を進めていくと、以前理解できなかった内容が書いてあることがあります。)
・理解できなければ、1度諦めて捨ててみる。
(コードは全部覚えようとしないで、次に進んでください。アプリ開発だけが目的なら、使わないコードの可能性も多いにあります)
・無理に学習せず、一度仕組みや構造を理解してみる。
(CNNや男女識別の学習時、私は画像の構造について理解できておらず、かなり時間を消費しました。画像の構造を学ぶことで理解することができました。)

下記からは、私が作成した野菜識別アプリの制作過程について記載します。

3.野菜識別アプリの制作

1人暮らしのとき「余った野菜で作れる料理を提案してくれるアプリがあったら便利なのに」と思った経験から、野菜識別アプリの制作に至りました。
理想は「冷蔵庫の中を写真撮ると、写った野菜で作れる料理を栄養素を気にしながら提案してくれるアプリ」ですが、
現状の技術力だとできないので、今回は「1つの野菜画像を読み込むと野菜を判別してくれるアプリ」の制作を目指します。

3-1.ゴールの決定

13種類の野菜を識別できるアプリで、精度90%以上を目指します。

≪13種類の野菜≫※万人がよく使う野菜を選びました。
・キャベツ ・きゅうり ・じゃがいも ・大根 ・玉ねぎ ・トマト ・なす
・にんじん ・ねぎ ・白菜 ・ピーマン ・ほうれんそう ・レタス

3-2.データの収集

某サイトで下記コードのプログラミングをし、スクレイピングを用いてデータを収集しました。
※下記コード内「~~~」に関しては、サイト情報ですので、念のため伏せています。

#データの収集・スクレイピング
res = requests.get(url)
soup = BeautifulSoup(res.text, "html.parser")
img_tags = soup.find_all("img", class_ ="~~~")

n=0
for j in range(2,12):
    url ='https://wwwhttps://www.~~~~' + "~~~page={}~~~.format(j)
    print(f'{j}pageから画像取得中....')
    for i, img_tag in enumerate(img_tags):
        n= n + 1
        res = requests.get(url)
        soup = BeautifulSoup(res.text, "html.parser")
        img_tags = soup.find_all("img", class_ ="~~~")

        img_url = img_tag["src"]
        img = Image.open(io.BytesIO(requests.get(img_url).content))
        img.save(f"Cabbage/Cabbage{n}.jpg")
        print(f'{j}pageの{i+1}枚目の画像をダウンロード完了!(全体としては{n}枚目)')

3-3.データの整形と加工

各野菜600枚収集しましたが、「料理の画像」「対象野菜が小さいモノ」「野菜が複数映ってしまっているモノ」などが多数あったため、
「対象が大きく映っている」「関係ない物体が映り込んでいない」「色が鮮明」なものを75~83枚選別しました。
また、今回はみじん切り、千切りや野菜のドアップ画像等は含んでいないため、
基本的には「野菜まるごと」「野菜の断面が載っているもの」でないと判定制度がかなり低くなります。
下記は、キャベツの画像例です。
kyabetu.PNG
各野菜毎に75枚~83枚に選別した後、画像の水増しを下記コードにて行いました。
元画像から、
「上下反転 ⇒ 画像追加 ⇒ 左右反転 ⇒ 画像追加 ⇒ 上下反転 ⇒ 画像追加」 
とすることで元画像に対して3枚の画像を水増ししました。

各野菜75枚~83枚に対して4倍の水増しを行い、野菜13種類合計で4060枚の画像としました。

# データの加工・整形
img_Cabbage = []
img_Cucumber = []
img_Potato = []
img_Radish = []
img_Onion = []
img_Tomato = []
img_Eggplant = []
img_Carrot = []
img_Leek = []
img_Chinesecabbage = []
img_Greenpepper = []
img_Spinach = []
img_Lettuce = []
img_list = [img_Cabbage, img_Cucumber, img_Potato, img_Radish, img_Onion, img_Tomato , img_Eggplant, img_Carrot, img_Leek, img_Chinesecabbage, img_Greenpepper, img_Spinach, img_Lettuce]

vegetables_list = ["Cabbage", "Cucumber", "Potato", "Radish", "Onion", "Tomato", "Eggplant", "Carrot", "Leek", "Chinesecabbage", "Greenpepper", "Spinach", "Lettuce"]
for index, vegetable in enumerate(vegetables_list):
    dir_name = '/content/drive/MyDrive/Aidemy/Data/' + vegetable + '/'
    path = os.listdir(dir_name)
    for i in range(len(path)):
        img = cv2.imread(dir_name + path[i])
        img = cv2.resize(img, (224,224))
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img_list[index].append(img)
        img = cv2.flip(img, 0) #上下反転
        img_list[index].append(img)
        img = cv2.flip(img, 1) #左右反転
        img_list[index].append(img)
        img = cv2.flip(img, 0) #上下反転
        img_list[index].append(img)

3-4.データを学習

VGG16を用いて、転移学習を行いました。

# データを学習
X = np.array(img_Cabbage + img_Cucumber + img_Potato + img_Radish + img_Onion + img_Tomato + img_Eggplant + img_Carrot + img_Leek + img_Chinesecabbage + img_Greenpepper + img_Spinach + img_Lettuce)

y = np.array([0]*len(img_Cabbage) + [1]*len(img_Cucumber) + [2]*len(img_Potato) + [3]*len(img_Radish) + [4]*len(img_Onion) + [5]*len(img_Tomato) + [6]*len(img_Eggplant) \
             + [7]*len(img_Carrot) + [8]*len(img_Leek) + [9]*len(img_Chinesecabbage) + [10]*len(img_Greenpepper) + [11]*len(img_Spinach) + [12]*len(img_Lettuce))

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)

input_tensor = Input(shape=(224, 224, 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(512, activation='relu'))
top_model.add(Dropout(rate=0.2))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(rate=0.2))
top_model.add(Dense(64, activation='relu'))
top_model.add(Dropout(rate=0.2))
top_model.add(Dense(13, 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.SGD(lr=1e-4, momentum = 0.9), 
              metrics = ['accuracy'])

history = model.fit(X_train, y_train, batch_size = 64 , epochs = 100, validation_data=(X_test,y_test))

model.save("/content/drive/MyDrive/Aidemy/model/my_model_75.h5")

3-5.モデルを評価

結果は、下記のようになりました。
前述したとおり、今回は基本的に「野菜まるごと」「野菜の断面が載っているもの」の画像を使用したため、正解率90%を超えることができました。
しかしながら、キャベツと白菜のF値が極端に悪いです。形がよく似ている栽培中のキャベツや白菜の画像も使用したことが原因のようです。
実は、「野菜まるごと」「野菜の断面が載っているもの」を厳選する前に、みじん切りや千切り、短冊切りなど画像を使って学習も行いましたが、そこまでの精度が出なかったです。
元画像が少なかったこと、みじん切りや千切りなどにすると野菜の特徴が失われやすいことが原因だと思います。

# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()

num_list = ['0','1','2','3','4','5','6','7','8','9', '10','11','12']
vegetables_list = ["Cabbage", "Cucumber", "Potato", "Radish", "Onion", "Tomato", "Eggplant", "Carrot", "Leek", "Chinesecabbage", "Greenpepper", "Spinach", "Lettuce"]
for i,num in enumerate(num_list):
  i = vegetables_list[i]
  print(i, rep_dict[num]['f1-score'])

Test loss: 0.3931514322757721
Test accuracy: 0.9220032691955566
index.png
F値
Cabbage 0.5962732919254657
Cucumber 0.8442211055276382
Potato 0.8695652173913044
Radish 0.7920792079207921
Onion 0.9142857142857143
Tomato 0.9534883720930233
Eggplant 0.9060773480662985
Carrot 0.8111888111888113
Leek 0.8690476190476191
Chinesecabbage 0.5034965034965035
Greenpepper 0.9029126213592233
Spinach 0.7272727272727272
Lettuce 0.7699115044247786

3-6.HTML、CSSのコード作成

Aidemyさんの講座「Flask入門のためのHTML&CSS」のコードを引用して作成しました。
バック画像だけ変更し、その他は従来のモノを引用しています。

HTMLコード

<!DOCTYPE html>
<html lang='ja'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content="device-width, initial-scale=1.0">
    <meta http-equiv='X-UA-Compatible' content="ie=edge">
    <title>Number Classifier</title>
    <link rel='stylesheet' href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <img class='header_img' src="https://illust8.com/wp-content/uploads/2018/12/vegetable_moriawase_illust_2643.png" alt="Vegetable"">
        <a class='header-logo' href="#">Vegetable Classifier</a>
    </header>

    <div class='main'>    
        <h2> AIが送信された画像の野菜を識別します</h2>
        <p>画像を送信してください</p>
        <form method='POST' enctype="multipart/form-data">
            <input class='file_choose' type="file" name="file">
            <input class='btn' value="submit!" type="submit">
        </form>
        <div class='answer'>{{answer}}</div>
    </div>

    <footer>
        <img class='footer_img' src="https://illust8.com/wp-content/uploads/2018/12/vegetable_moriawase_illust_2643.png" alt="Vegetable">
        <small>&copy; 2021 Aidemy, inc.</small>   
    </footer>
</body>
</html>

CSSコード

header {
    background-color: #76B55B;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}

.header-logo {
    color: #fff;
    font-size: 25px;
    margin: 15px 25px;
}

.header_img {
    height: 25px;
    margin: 15px 25px;
}

.main {
    height: 370px;
}

h2 {
    color: #444444;
    margin: 90px 0px;
    text-align: center;
}

p {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

.answer {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #F7F7F7;
    height: 110px;
    margin: -8px;
    position: relative;
}

.footer_img {
    height: 25px;
    margin: 15px 25px;
}

small {
    margin: 15px 25px;
    position: absolute;
    left: 0;
    bottom: 0;
}

3-7.FLASKのコード作成

Aidemyさんの講座「Flask入門」のコードを引用して作成しました。

import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image

import numpy as np


classes = ["キャベツ","きゅうり","じゃがいも","大根","玉ねぎ","トマト","なす","人参","ねぎ","白菜","ピーマン","ほうれん草","レタス"]
image_size = 224

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

model = load_model('./model.h5')#学習済みモデルをロード


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"

            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")


if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)

3-8.アプリのデプロイ、動作確認

インターネット上でもしっかりと動作確認できました♪
http://vegetablenisshi.herokuapp.com/
スクリーンショット (8).pngスクリーンショット (9).png

野菜単体画像 ⇒ 正解率 10/10

fd400015.jpgfd400083.jpgfd400119.jpgfd400134.jpgfd400154.jpg
fd400161.jpgfd400215.jpgfd400255.jpgfd400314.jpgfd400603.jpg
良かった!!さすがにこれはね・・・!

野菜複数画像 ⇒ 正解率 8/10

fd400139.jpgfd400155.jpgfd400169.jpgfd400306.jpgfd400336.jpg
fd400629.jpgfood26_tph00.jpgimages.jpgimages7.jpgimages8.jpg
結果を見たら、5枚目の人参が「大根」の判定、9枚目の人参が「なす」の判定でした。
これを外してしまうようでは、、、(´Д`)ハァ…

野菜断面画像 ⇒ 正解率 10/10

12431615_PW36.jpg18807497-氷のレタスの断面図.jpgf.jpgg.jpgh.jpg
k.jpgq.jpgx.jpgあ.jpgimages.jpg
断面図も無事クリア!!

野菜みじん切り(おまけ) ⇒ 正解率 0/10

d.jpgf040b809.JPGimages.jpgindex.jpgk.jpg
m.jpgmaxresdefault.jpgp.jpgP1010694.jpgあ.jpg
今回はみじん切りの画像を元画像に採用していないので当たり前ですが、正解率は0でした・・・。
色味とかで1つくらい識別できるかと期待したんですが、ダメみたいです。
みじん切りにすると野菜そのものの特徴が失われるので、みじん切りも識別するとなると、かなり大変そうです。

4.考察、感想

今回はアプリ製作のために画像スクレイピングから始め、オリジナル画像枚数が少なかったことで「野菜まるごと」「野菜の断面が載っているもの」を選別したのでイレギュラーに対応できないアプリになっています。
データの少なさが根本的な原因ではありますが、人参を「大根」「なす」などと識別したため、色味による識別精度をもっと上げられないモノかと考えました。

今回は時間が無かったことでここまでになっていますが、
突き詰めたらキリがないので、やれることはいくらでもあることがプログラミングの楽しいところですね。
そのようなことも有り、今後は下記のようなことをしてみたいです。

・オリジナル画像を増やし、みじん切りや銀杏切り、乱切り、千切り、輪切り、短冊切りなどにも対応できるようにする。
・13種類だけではなく、野菜の数をもっと増やして数多くの野菜識別ができるようにする。
・野菜識別後、主要栄養素を表示してくれるようにする。
・複数野菜が写っている画像に関しては、複数野菜を回答してくれるようにする。
・複数野菜が写っている画像に関しては、映っている野菜で作れる料理を提案してくれるようにする。
・モデルを改善し、最適なモデルの作成
・HTML&CSSを学んで、もっと可愛いデザインでアプリを制作する。

最初は理解が追い付かずに辛かったですが、何とか学習に食らいつくことでプログラミングの大枠は掴めたと思っています。
アプリ制作もできましたし、受講してとても良かったと思っています!
ご精読、有難うございました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0