0
0

【Aidemy成果物】野菜・果物識別アプリ

Posted at

はじめに

ブログにアクセスいただきありがとうございます。AidemyさんにてAIアプリ作成講座について学ばせていただきました30代主夫です:bow:
「このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています」
学習結果の成果物としてブログを記載いたします。
本講座を受講にあたり、学習のサポートをしていただいたAidemyチューター・サポーターの方々へ厚く感謝申し上げます。

目次

1. 作成したアプリの概要

2. 実装環境
3. データセット
4. 学習モデル
5. 学習結果の推移

6. HTML & CSS

7. Flask
8. アプリの動作確認
9. 感想

1. 作成したアプリの概要

1.1. なにを作ったか??

CNNでの画像分類アプリを作成しました!
アプリの内容:野菜と果物の36種に関する画像分類アプリ
Aidemy成果物:野菜と果物識別アプリ

そさクエtop.png

1.2. 作った背景と経緯

【背景】
我が家にはchildrenが2人います。面白いことに2人は食べものに関する好みが180度異なります。
1人は野菜好き(菜食主義)で もう1人はお肉がだい好き(野菜嫌い)。

【経緯】
アプリで遊ぶことによって、野菜たちを好きになってもらいたい。
作成した画像分類アプリを見せ、野菜に触れることによって興味を創出したい。
野菜嫌いな子は野菜に興味を持つようになってもらい(あわよくば 食べられるように...(笑))。
野菜好きな子にはもっと野菜を好きになってもらいたい。
そんな思いを込めて野菜と果物の画像分類のアプリを作りました!

1.3. アプリ名

そ さ ク エ
蔬菜クエスチョンsosai + question
小さい子でも覚えられるように短い名前で、サイト全体は子ども受けしそうなシンプルかつポップな印象を演出しました!

2. 実装環境

  • Google Colaboratory
  • Visual studio code
  • Pyhton
  • Flask
  • Tensorflow
  • Git
  • Render

3. データセット

Kaggleのデータセット「Fruits and Vegetables Image Recognition Dataset」を使用しました。
データ数 train:100 / test:100 / validation:100
https://www.kaggle.com/datasets/kritikseth/fruit-and-vegetable-image-recognition

4. 学習モデル

◎インポートと画像の準備

Python
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
from tensorflow.keras.layers import Input, Flatten, Dense, Dropout
from tensorflow.keras.models import Sequential, load_model, Model
from tensorflow.keras.utils import load_img, image_dataset_from_directory, to_categorical
from tensorflow.keras import optimizers
from tensorflow.keras.applications.vgg16 import VGG16
from PIL import Image
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# GoogleDriveにデータを保存しパスを設けるためGoogleColaboratoryを紐づけ
from google.colab import drive
drive.mount('/content/drive')

# ベースとなるディレクトリパスの設定
base_dir = '/content/drive/MyDrive/kaggle'

# kaggleのデータセットを受けとるディレクトリの作成(kaggle APIの準備)
# GoogleColaboratoryで使用
!mkdir -p ~/.kaggle
!cp /content/drive/MyDrive/kaggle/kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

# kaggleからデータセットをダウンロードする
# GoogleColaboratoryで使用
import os
os.environ['KAGGLE_CONFIG_DIR'] = "/content/drive/MyDrive/kaggle"
os.chdir(base_dir)
!kaggle datasets download -d kritikseth/fruit-and-vegetable-image-recognition

# zipファイルの解凍
# GoogleColaboratoryで使用
!unzip f '{base_dir}/fruit-and-vegetable-image-recognition.zip'

完全な素人目線から見てベースディレクトリパスの設定は、相当に参考となりました。それまでは長いパスを記載が必要なたび、都度入力する無駄な作業をしておりました...:sweat_smile:
後に出てくるデータセットの各ディレクトリ(train/test/validation)のパスを作成するまで作業効率を上げ、活用できる点は目から鱗な情報でした!


◎各ディレクトリのパス

Python
# train,test,validationディレクトリのパス
train_dir = f"{base_dir}/train"
validation = f"{base_dir}/validation"
test_dir = f"{base_dir}/test"

◎trainディレクトリ内各クラスのサンプル数可視化

Python
# trainディレクトリの画像クラスのリスト
classes = [class_name for class_name in os.listdir(train_dir)]

# trainディレクトリの各クラスに入っているサンプル数のグラフ可視化
count = []
for class_name in classes :
    count.append(len(os.listdir(os.path.join(train_dir, class_name))))

plt.figure(figsize=(20, 6))
ax = sns.barplot(x=classes, y=count, color="lightsalmon")
plt.xticks(rotation=300)
for i in ax.containers:
    ax.bar_label(i, )
plt.title("Number of samples per class", fontsize=25, fontweight="bold")
plt.xlabel("CLASS", fontsize=15)
plt.ylabel("COUNTS", fontsize=15)
plt.yticks(np.arange(0, 105, 10))
plt.show()

Number of samples per class.png

「Fruits and Vegetables Image Recognition Dataset」を活用している高評価者のコードを教えていただき、それを参考としてtrainディレクトリに入っているクラスごとの画像数を抽出しました。そのデータを棒グラフにて可視化して、各クラスのデータ数に関して視認性を向上させました!

参考:Fruits and Vegetables Image - MobileNetV2

Kaggle Fruits and Vegetables Image MobilenetV2


◎各ディレクトリのデータセットを準備

Python
# tf.keras.utils.image_dataset_from_directory関数を使用しtrainデータセットを作成
train = image_dataset_from_directory(
    train_dir,
    label_mode="categorical",
    image_size=(50,50)
    )

# validationデータセットを作成
validation = image_dataset_from_directory(
    validation,
    label_mode="categorical",
    imade_size=(50,50)
    )

# testデータセットを作成
test = image_dataset_from_directory(
    shuffle=False,
    label_mode="categorical",
    image_size=(50, 50)
    )

モデル学習の際に渡す各ディレクトリのデータセットを準備をおこないました。
image_dataset_from_directory関数を教えていただき、それに沿ってデータセットの準備をしました。
見ていただくとtestデータセットの設定のみ他の各ディレクトリのデータセットと異なっています。当初は他のtrain・validationデータと同様にshuffle=Trueでしたが、後に記載するモデルの予測値を混同行列にて算出する際にうまく予測ができず、終盤に修正しました!

参考:Image Data Loading - Keras API reference


◎モデルの定義・学習

Python
# モデルの定義
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights="imagenet", input_tensor=input_tensor)

n_model = Sequential()
n_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
n_model.add(Dense(256, activation="relu"))
n_model.add(Dropout(0.5))
n_model.add(Dense(36, activation="softmax"))
model = Model(inputs=vgg16.input, outputs=n_model(vgg16.output))

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

# コンパイルの設定
model.compile(loss="categorical_crossentropy",
              optimizer=optimizers.Adam(),
              metrics=["accuracy"])

# モデルの学習を実施
history = model.fit(train, verbose=1, epochs=50, validation_data=validation)

# モデルの評価
scores = model.evaluate(test, verbose=1)
print("Test_loss:", scores[0])
print("Test_acc", scores[1])

# resultsディレクトリを作成
result_dir = "results"
if not os.path.exists(result_dir):
    os.mkdir(result_dir)

# 重みを保存
model.save(os.path.join(result_dir, "model.h5"))

VGG16の転移学習モデルとカスタマイズの学習モデルを使用しました。各種設定に関しては設定段階より正解はないとのことで取り合えずこの設定で進めていくこととしたところ、そのままモデルが完成しました!

5. 学習結果の推移

5.1 学習結果

epochs=100 85-100回目編集.png
epochs=100 折れ線グラフ.png

lossはよくわからないですが、正解率で96%を超えているのでいいのではないでしょうか!
epochs数における学習の推移をみてもだいたい50回目以降から学習の頭打ち感がありました。数値をグラフで確認してみても動きが停滞しているような感じがありました。

Python
# 上に記載の折れ線グラフの描画
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# 1つ目のグラフ: Accuracy
ax1.plot(history.history['accuracy'], label='Training Accuracy', marker='o', linestyle='-')
ax1.plot(history.history['val_accuracy'], label='Validation Accuracy', marker='x', linestyle='-')
ax1.set_ylabel('Accuracy')
ax1.set_xlabel('Epoch')
ax1.set_title('Accuracy')
ax1.legend()

# 2つ目のグラフ: Loss
ax2.plot(history.history['loss'], label='Training Loss', marker='o', linestyle='-')
ax2.plot(history.history['val_loss'], label='Validation Loss', marker='x', linestyle='-')
ax2.set_ylabel('Loss')
ax2.set_xlabel('Epoch')
ax2.set_title('Loss')
ax2.legend()

# グラフを表示
plt.show()

5.2 学習結果の描画

☆いろいろな予測結果で遊ぼう 確認しよう☆

◎混同行列の値を算出

Python
# 混同行列の計算
# testデータの予測
y_pred = model.predict(test)
y_pred = np.argmax(y_pred, axis=1)

# testデータのラベルを取得
y_true = np.concatenate([y for x, y in test], axis=0)

# One-Hotエンコーディングを解除
y_true = np.argmax(y_true, axis=1)

# accuracy
accuracy = accuracy_score(y_true, y_pred) # 修正箇所
print("正解率:",accuracy)

# recall
recall = recall_score(y_true, y_pred, average="macro") # 修正箇所
print("再現率:", recall)

# precision
precision = precision_score(y_true, y_pred, average="macro") # 修正箇所
print("適合率:", precision)

# F値
f1 = f1_score(y_true, y_pred, average="macro") # 修正箇所
print("F1:", f1)

epochs=100混同行列編集.png

各数値とも96%を超えているからいいんですッ!


◎各クラスの正解率(リスト&ヒートマップ)

Python
# testデータのラベルを取得
y_true = np.concatenate([y for x, y in test], axis=0)

# One-Hotエンコーディングを解除
y_true = np.argmax(y_true, axis=1)

# 各クラスごとの正解率
num_classes = 36

# testデータでの予測
y_pred = model.predict(test)
predicted_classes = np.argmax(y_pred, axis=1)

class_counts = np.zeros(num_classes)
class_correct = np.zeros(num_classes)

for i in range(len(y_true)):
    true_class = y_true[i]
    predicted_class = predicted_classes[i]
    class_counts[true_class] += 1
    if true_class == predicted_class:
        class_correct[true_class] += 1

class_accuracy = class_correct / class_counts

classes_list =["りんご","バナナ","赤かぶ","ピーマン","キャベツ","パプリカ(トウガラシっぽい)",
                "にんじん","カリフラワー","とうがらし","とうもろこし","きゅうり","なす",
                "にんにく","しょうが","ぶどう","青とうがらし","キウイ","レモン",
                "レタス","マンゴー","玉ねぎ","オレンジ","パプリカ","なし",
                "えんどう豆","パイナップル","ザクロ","じゃがいも","ラディッシュ","大豆",
                "ほうれん草","スイートコーン","さつまいも","とまと","かぶ","すいか"]

print("各クラスの正解率")
for i in range(num_classes):
    print(classes_list[i], ":{:.2f}".format(class_accuracy[i]))

epochs=100各クラスの正解率 編集.png

各クラスの成果率が出るのは面白いですね!正解率が思いのほか高いのもびっくり!

Python
# ヒートマップで可視化
# testデータの予測
y_pred_prob = model.predict(test)  
y_pred = np.argmax(y_pred_prob, axis=1)

# testデータのラベルを取得
y_true = np.concatenate([y for x, y in test], axis=0)

# One-Hotエンコーディングを解除
y_true = np.argmax(y_true, axis=1)

# 混同行列の計算
cm = confusion_matrix(y_true, y_pred)

# クラスラベルを指定
labels = classes

# 混同行列を可視化
plt.figure(figsize=(15, 12))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=labels, yticklabels=labels)
plt.xlabel("Predicted Labels")
plt.ylabel("True Labels")
plt.title("Confusion Matrix")
plt.xticks(rotation=70)
plt.yticks(rotation=0)
plt.show()

epochs=100ヒートマップ.png

ヒートマップでは問題なく学習が進み、正解ができているとの結果となりました。
実は当初から各クラスを見ていて気になっていたことがありました!
クラスの中にcornとsweetcornがあり、似ている形状の野菜だがしっかり識別できるのか?人間でも間違える人がいるというのにAIでも判断ができるのか??
ヒートマップによると、やはり正解値corn ⇒ 予測値sweetcornと判断しているのも見受けられました。ただ、sweetcorn ⇒ cornと予測している結果はない状況でした。AIは優秀ですね!:heart_eyes:
予測と正解が異なっていることもあり、この違いを分析し解明していくことがAIを扱う際の楽しさになるんだろうなぁと一人で納得した今日この頃です:point_up:
※補足
後々確認したところsweetcornのtrainデータの約9割がcornの画像で、testデータに関しはすべてcornでした。そうなるとcorn・sweetcornの区別はどうやっているのか??さらなる疑問が生じましたが今回は突き詰めないで本学習をすすめます。

6. HTML & CSS

6.1 HTMLコード

$\huge{HTMLコードをクリックで参照}$
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aidemy成果物:野菜と果物識別アプリ</title>
    <link rel="stylesheet" href="./static/v_f.css">
</head>
<body>
    <header>
        <h1 class="header-title">
            <span class="star"></span>
            <span class="so"></span>
            <span class="sa"></span>
            <span class="ku"></span>
            <span class="e"></span>
            <span class="star"></span>
        </h1>
    </header>


    <div class="main">
        <h2>AIが画像の識別をしますぜッ!</h2>
        <p>みなさん!<br><span class="emphasis">やさい</span> と <span class="emphasis">くだもの</span> 食べてますか?<br>きになる<span class="emphasis">やさいたち</span>の画像を送信してください</p>
        <P>ただしこのAIはまだまだ勉強中なので以下のやさいたちしか識別できません<br>
            [りんご,バナナ,赤かぶ,ピーマン,キャベツ,にんじん,カリフラワー,とうがらし,
            とうもろこし,<br>きゅうり,なす,にんにく,しょうが,ぶどう,青とうがらし,キウイ,
            レモン,レタス,マンゴー,玉ねぎ,<br>オレンジ,パプリカ,なし,えんどう豆,パイナップル,
            ザクロ,じゃがいも,ラディッシュ,大豆,ほうれん草,<br>スイートコーン,さつまいも,とまと,かぶ,すいか]</P>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="おす" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
        	
            {% with messages = get_flashed_messages() %}
                {% if messages %}
                    <ul>
                        {% for message in messages %}
                            <li>{{ message }}</li>
                        {% endfor %}
                    </ul>
                {% endif %}
            {% endwith %}
    </div>

    <footer>
        <small>&copy; 2024 T.Yamashita.</small>
    </footer>

</body>

</html>

6.2 CSSコード

$\huge{CSSコードをクリックで参照}$
CSS
body {
    font-family: 'Noto Sans JP', sans-serif;
    background-color: #f0f0f0; 
    line-height: 1.6;
    margin: 0;
  }
  
header {
  padding: 0.5rem;
  text-align: center;
  background-image: url("./images/dot.sample.png"); 
  background-repeat: repeat; 
  display: flex; 
  align-items: center; 
  justify-content: center; 
  height: 70px;
}

.header-title {
  font-size: 4rem;
  font-weight: bold;
}

.so, .sa {
  color: #7acca3; 
}

.ku, .e {
  color: #cca37a; 
}

.star { 
  color: #f5e147; 
}

.main {
  max-width: 1300px; 
  height: 600px;
  margin: 1rem auto; 
  padding: 2rem 2rem 2rem 7rem; 
  background-color: white;
  border-radius: 12px; 
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 
  background-image: url("./images/vege.sample.png"); 
  background-size: 44%; 
  background-repeat: no-repeat; 
  background-position: 750px ;
}
 
.main h2 {
  margin-bottom: 1.5rem;
  border-bottom: none; 
  position: relative; 
  font-size: 2rem;
}

.main h2::after {
  content: ""; 
  position: absolute; 
  bottom: -2px; 
  left: 0;
  width: 35%;
  height: 2px;
  background-color: #e7e2e2; 
}

.main p:first-of-type { 
  font-size: 1.5rem;  
  margin-bottom: 2rem; 
  padding-top: 1rem;    
}

.main p .emphasis {
  font-weight: bold;    
  font-family: 'Rounded Mplus 1c', sans-serif; 
  color: #f1874a;      
}

.main p:nth-child(3) { 
  font-size: 0.8rem; 
}

.file_choose,
.btn {
  display: inline-block;
  width: 300px; 
  padding: 1rem;
  margin-bottom: 1.5rem;
  margin-top: 20px; 
  border: none;
  border-radius: 6px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.file_choose {
  background-color: #ecebeb;
  margin-right: 10px;
}

.btn {
  background-color: #4CAF50;
  color: white;
  font-size: 1.2rem;
  font-weight: bold;
  transition: background-color 0.3s ease;
  width: 150px; 
}

.btn:hover {
  background-color: #6bc16e;
}
  
.answer {
  font-size: 1.4rem;
  margin-top: 2.5rem;
  margin-left: 180px; 
  font-weight: bold;
}
  
footer {
  text-align: center;
  padding: 0.3rem;
  margin-top: 0.5rem;
  font-size: 0.9rem;
  color: #777;
  margin: 0 auto;
}

7. Flask

$\huge{Flaskコードをクリックで参照}$
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 = 50

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

app = Flask(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.secret_key = "v_f_app.secrer_key"


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)

            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)

8. アプリの動作確認

実際にRenderでデプロイしたアプリを動かしてみます!
著作権フリーの" にんじん "の画像を診断をさせると...

そさクエ画像選択.png

問題なく" にんじん "と識別されました!:clap:

そさクエ結果.png

ちなみに、空ファイルで"おす"を実行すると...

そさクエ結果 ファイルなし.png

ちゃんと「ファイルがありません」と表示されました。

9. 感想

いままでIT業界やエンジニアとは無関係な生活をしていたなか、今回AidemyさんのAIアプリ開発講座を受講させていただき、個人的には3つの面白さがありました。

  1. いままで知り得ることのできなかったAIに関する知識のインプットに対する面白さ
  2. AIのモデルを作成して動かしてみる面白さ
  3. 学習をすすめるなかで疑問点⇒解決のプロセスに対する面白さ

いままでは製造業の営業職に従事しておりましたが、この度 他業界・他業種・他職種では経験できない『AI』に関する学習をおこなうことができたのはとても有意義でした。今後は学んだことを活かし、他のAIアプリの作成や他のAidemyさんの講座の受講をすすめていき、自身のさらなるスキルアップ・知識のインプットの向上に努めていきたいと思います:ok_hand:


以上
0
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
0
0