トマト・茄子の判定モデルを構築してみた。

## はじめに
こんにちは。
以前からデータ分析や自動化に興味があったのですが、AIや機械学習が日常生活やビジネスのあらゆる場面で活躍しているのを目にして、もっと深く学びたいと思うようになり。特に画像分類では、野菜の品質管理や農業の分野でも応用できそうだと感じ、好きな野菜であるナスとトマトを題材にしてチャレンジしてみました。きっと私が選んだ画像を学習すれば、判別はできるはず、、、いや。みなさんできるか。

皆さんもぜひ一緒にお楽しみください、笑。


## プロジェクト概要
人を良くすると書いて「食」と書く。当たり前ですが人は「食」でできている部分が非常に多くこの当たり前の部分を疎かにしている現代人が多いと個人的な意見で思っています。郊外を除き都市部では外を歩けば安心安全な食べ物を販売している”便利なストア”がいたる所にありほんの数秒単位で摂取ができる。しかし安全安心な食べ物は人を良くするのか。そんなことを妄想していると農業分野に興味を持ち霞が関のさまざまな官庁のホームページに行って農業に関するDX化など調べていました。そこで感じたことが大きく二つありました。

１つめは労働力の高齢化。全産業に言えることではありますがその中でも農業の高齢化＋αで新規就農者不足。
農業＝きつい、収入が不安定　⇒　人気がない　⇒　衰退　食料自給率低下

![スクリーンショット 2024-10-14 231406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/c2544481-4f01-5aae-e10c-fbc8e983ee40.png)
＊農林水産省HP記載　世界の食料自給率グラフを引用
https://www.maff.go.jp/j/zyukyu/zikyu_ritu/013.html

２つめはAIや機械学習を活用したソリューションが多くの分野で注目されており農業分野においても、画像認識技術を活用した作物の品質判定や分類が可能となっており、これを効率的に行うための技術は非常に重要との事。

![スクリーンショット 2024-10-14 231729.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/fdb19484-edb5-9296-6a37-50246efa300d.png)
＊農林水産省HP記載　農業DXをめぐる現状と課題を引用　
https://www.maff.go.jp/j/kanbo/dx/attach/pdf/nougyou_dxkousou-16.pdf

今回はその一例として、2種類の野菜（tomato,eggplant）の画像を対象に、VGG16を活用した転移学習モデルを構築してみました。

## 使用技術
TensorFlow/Keras: ディープラーニングフレームワーク
VGG16: 事前学習済みの畳み込みニューラルネットワークモデル
Python: コード全体の記述言語

## VGG16の概要
VGG16は、画像分類タスクで優れた性能を発揮するモデルで、16層の畳み込み層と全結合層で構成されています。今回は、このVGG16をベースに、2種類の野菜を分類するモデルを構築しました。

## データ収集
まず、学習するための各野菜の画像を集めました。
ググったり、各フリーサイトの画像やスーパーで許可を得て野菜を撮影したりどうにかして色々な方法で画像を集めます。日本は様々な機械学習で用いるデータセットの乏しさがIT発展の遅れに悪影響しているのか？？
など考えていると機械学習の壁は厚い。。。泣泣
あとは著作権の兼ね合いもあるのでできれば各地を歩き回り自前のカメラ・スマホで撮影するのが無難かと。機械学習で使用する場合著作権侵害にはならないなどの法律にしてしまうと１００％悪用されると思うのでこれまた法整備も難しい、、、

苦労に苦労を重ねて画像を各、数十枚から数百枚用意しました。
それをVSCodeに読み込みます。

## 学習データのセットアップ（画像の読み込み）
今回はローカルフォルダから予めVSCodeのディレクトリにアップロードした[ナス・トマト]のデータを読み込むためにKerasのImageDataGeneratorというクラスを使用します。

#### ImageDataGeneratorとは
ImageDataGeneratorは、特定のフォルダに格納されている画像データを、前処理しながら、予測モデルに流し込む機能を提供します。まずはImageDataGeneratorをインポートします。以下のように実行します。

```diff_python
from tensorflow.keras.preprocessing.image import ImageDataGenerator
#TensorFlow/Kerasの画像処理ツールである ImageDataGenerator クラスをインポートしています
```

#### 続いて画像データの前処理
ImageDataGeneratorのインスタンスを作成します。パラメータを指定することで、画像データの加工や複製方法の設定が可能です。今回は、以下のパラメータを指定します。
rescaleというパラメーター（画像データが持つピクセル値をスケーリングできます。今回は、1/255を指定します。）
```diff_python
datagen = ImageDataGenerator(rescale=1/255, validation_split=0.2)
#具体的には、以下の2つの処理を行っています。
#rescale=1/255:
#1.学習の効率を上げ数値の大きさの違いによる問題を防ぐためにすべてのピクセル値が０から１の範囲にスケーリング
#2.データセットの25%を検証用データに割り当てる（training_split80%/validation_split20%）
```
#### 画像データの予測モデルへ流し込みを設定
flow_from_directoryメソッドで、ジェネレータを作成できます。第1引数に、流し込みたい画像の格納先フォルダを指定します。
まずは、学習データのジェネレータを作成します。それでは、以下のように実行します。
```diff_python
train_generator = datagen.flow_from_directory(
        r"C:\Users\user\Desktop\programs\data_train",
        subset='training',
        target_size=(256, 256),
        batch_size=8)
```         
![スクリーンショット 2024-10-15 173512.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/289edf2b-4ac8-f412-2795-6c7a84c83acd.png)


実行結果として「Found 80 images（80ファイル見つかった）」と表示されました。これは、data_trainフォルダに含まれる画像100枚（ナス50枚＋トマト50枚）のうち80%（20%はバリデーションデータ）である80枚が検知されたことを意味します。
またbelong to 2class（2クラスに属する）と表示されています。
保存するフォルダ構成によって、ImageDataGeneratorが自動的にどの画像が茄子で、どの画像がトマトなのかを認識するようになります。


なお、転移学習において、ナスとトマトのカテゴリは、数値ラベルとして表現されます。どちらのカテゴリが、どの数値ラベルであるかは、class_indices属性で確認できます。以下のように実行します。
```diff_python
train_generator.class_indices
```
![スクリーンショット 2024-10-15 195035.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/1b89870f-eef3-9eb0-798c-a9983c1a7081.png)
この結果から、ナスは数値ラベル0、トマトは数値ラベル1で
表現されることを確認できました。



学習データのジェネレータと同様に、バリデーションデータのジェネレータも作成しましょう。以下のように実行します。
```diff_python
val_generator = datagen.flow_from_directory(
        r"C:\Users\user\Desktop\programs\data_train",
        subset='validation',
        target_size=(256, 256),
        batch_size=16)
```
![スクリーンショット 2024-10-15 173945.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/646da964-b4a0-a204-a431-f0a77a32a3d0.png)

続いて、テストデータのジェネレータも作成しましょう。なお、テストデータは、「training」と「validation」の区分がないため、subsetパラメータの指定は不要です。以下のように実行します。
```diff_python
test_generator = datagen.flow_from_directory(
      r"C:\Users\user\Desktop\programs\data_test",
      target_size=(256, 256),
      batch_size=16)
```
![スクリーンショット 2024-10-15 174117.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/61428a3c-b524-9be1-c1e4-6ae8961dab3c.png)

以上で学習データのセットアップは完了となります。



## 転移学習とは？
転移学習は、すでに学習済みのモデル（例えばVGG16など）を新しいデータセットに適応させる技術です。ゼロから学習を行うのに比べて、事前学習済みの知識を活用するため、モデルの精度向上が速く、計算コストも低く抑えられるというメリットがあります。

## VGG16に全結合層以外の取得
VGG16の全結合層を取り除くため、include_top=Falseオプションを指定してVGG16を取得します。また、input_shape=(256, 256, 3)も指定します。これによって、VGG16へインプットできる画像のサイズを、ジェネレータ作成の際に設定した画像サイズ「256ピクセル×256ピクセル」に揃えることが可能です。
以下のように実行します。

```diff_python
from tensorflow.keras.applications.vgg16 import VGG16
vgg16_without_fc = VGG16(weights='imagenet', include_top=False, input_shape=(256, 256, 3))

```
![スクリーンショット 2024-10-12 150058.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/fd5f12cf-9ec1-cf9c-8e5c-7e806d411aa3.png)
以上で、VGG16の全結合層以外の取得は完了です。

## 予測モデルの可視化
取得したVGG16の構造を確認するために、summary()メソッドを使って可視化します。
以下実行します。

```diff_python
vgg16_without_fc.summary()
```
![3be7685a-8537-463f-a68d-560a0431ac06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/8cb937e1-b149-4766-1799-43a4db767248.png)

## VGG16のSequentalモデルへの変換
次に、VGG16をSequentialモデルに変換します。VGG16をSequentialモデルに変換する理由は、独自の層を簡潔に追加できるようにするためです。Sequentialモデルとは、Sequentialクラスを用いて予測モデルを定義する形式です。
```diff_python
from tensorflow.keras.models import Sequential
model = Sequential()
for layer in vgg16_without_fc.layers:
    model.add(layer)
```
※この処理によって、予測モデルを定義し、変数に代入しているだけですので、実行結果の表示はありません。

問題なく変換できたか下記コードで確認します。
```diff_python
model.summary()
```
![スクリーンショット 2024-10-12 153127.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/229a36a6-9665-35e5-b844-d3afef38a2d3.png)

## 誤差関数、最適化方法、評価指標の設定
続いて、誤差関数、最適化方法、評価指標の設定を行います。誤差関数として二値交差エントロピー誤差（Binary Cross-Entropy Error）を使用します。二値交差エントロピー誤差は、2つのカテゴリ（今回は犬と猫）を分類する際に一般的に利用される誤差関数です。compileメソッドの引数で、loss='binary_crossentropy'と指定すれば適用されます。

```diff_python
model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
```
以上で誤差関数、最適化方法、評価指標の設定は完了です。

## 学習
学習データとバリデーションデータを使って予測モデルを学習させます。この際に、以下の引数を指定します。

train_generator：学習データを供給するデータジェネレータを指定します。これにより、モデルに対して学習データの流し込みが行われます。
validation_data=val_generator：バリデーションデータとして、val_generatorを指定します。これによって、学習中にバリデーションデータによる損失関数と正解率の推移をモニタリングできます。
epochs：学習をするエポック数を指定します。今回は10エポックで学習します。
下記コードを実行します。
```diff_python
history = model.fit(
      train_generator,
      validation_data=test_generator,
      epochs=10)
```
実行結果
![スクリーンショット 2024-10-13 142923.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/0f943dd8-7c95-0717-78f0-97705b27ef7c.png)

＜ポイント＞
数分かけて各エポックごとに学習が自動で進みますが
下に行く毎にaccuracy：数値（正確さ）が[1]近くなるにつれ機械学習として精度が増していることになるので
ここの数字が変わらないもしくは上下する際は読み込んだ画像の見直しなどをされると改善が見られます。
（このエポックごとに数値が増していく感覚が個人的に一番好きなポイントになります。）
今回の結果ではスムーズにaccuracy数値が上昇したので一旦良しとしこのまま次のステップに行きます。

## 学習履歴の可視化
次に、学習過程での正解率と損失の変化を可視化し、過学習や学習不足を確認しましょう。fit関数の戻り値であるHistoryオブジェクトのhistory属性に、学習履歴の推移が格納されています。DataFrameに変換し、seabornのlineplotメソッドを利用して、損失関数（loss, val_loss）の推移を可視化します。以下のように実行します。

```diff_python
import pandas as pd
import seaborn as sns
df = pd.DataFrame(history.history)
sns.lineplot(data=df[['loss', 'val_loss']])
```
![スクリーンショット 2024-10-14 225534.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/aa5d54fd-67b9-3a1e-8193-44f94eb9aece.png)



同様に、正解率（accuracy, val_accuracy）の推移も可視化します。以下のように実行します。

```diff_python
sns.lineplot(data=df[['accuracy', 'val_accuracy']])
```
![スクリーンショット 2024-10-14 225545.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/bc8da5b6-3b0f-511e-e2f0-6d70df189f6c.png)



損失関数と正解率の推移の曲線から、学習データは良好な数値で安定しており、十分に学習できていると判断できます。横軸はエポック数になり学習履歴の推移と正解率を表す上下のグラフともおおよそエポック数が４～６のあたりで安定しているので過学習を防ぐためにもエポック数は必要最低限でも良いです。
本来であればこの　['loss, 'val_loss]　[accuracy, val_accuracy]　のグラフを見て
モデルの学習は２回ほど行うのが最適です。
１回目は８～１０回ほどの（より複雑な学習の場合エポック数は増えると思います。）エポック数で行い、今回のように４～６くらいで安定しているとわかればエポック数を６などに設定し再度学習を行う方がより精度の高い予測が可能となります。今回は説明簡素化のためにも１回の学習で進めます。

## 評価
次に、学習済み予測モデルの評価を行います。Sequentialクラスのevaluateメソッドを使って評価を実行できます。この際に引数にはテストデータのジェネレータを指定します。以下のように実行します。
```diff_python
model.evaluate(test_generator)
```
![スクリーンショット 2024-10-13 144138.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/a5b7010d-cebd-6b3f-058b-26de64aa4fd7.png)

テストデータによる評価結果は、損失関数（loss）が約0.2498....であり、正解率（accuracy）は約0.8999....です。この評価結果が十分であれば、次のステップに進みます。そうでなければ、予測モデルの構造や損失関数、最適化手法、ハイパーパラメータなどを見直して、再度学習をする必要があります。

## 予測画像の読み込み
いよいよ構築したモデルがうまく判別できるか実行します。
予め用意した予測用の画像を入力します。

```diff_python
from tensorflow.keras.preprocessing.image import load_img
unknown_img = load_img('\用意\した\画像\（茄子）\jpeg', target_size=(256, 256))
unknown_img
```
![スクリーンショット 2024-10-14 232511.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/e2831630-b8d5-b449-8822-94f51eee6cc5.png)

実行したコードに上記のように画像が表示されれば問題ありません。
次に正確に予測ができるように画像の前処理を行います。

## 画像ファイルの前処理
画像データをarray形式に変換し、予測モデルのインプットデータの構造である「任意のサンプル数×縦256ピクセル×横256ピクセル×3チャネル」の形状に揃えましょう。その次に、ImageDataGeneratorのインスタンス作成時の設定と同様に、256で割ってスケーリングを行います。以下のように実行します。
```diff_python
from tensorflow.keras.preprocessing.image import img_to_array

# array形式に変換
unknown_array = img_to_array(unknown_img)
# データ構造の変換
unknown_array = unknown_array.reshape((1, 256, 256, 3))
# スケーリング
unknown_array = unknown_array/255
```

## 予測
最後に、Sequentialクラスのpredictメソッドを利用して、取り込んだ画像データが、茄子・トマトのどちらのカテゴリであるかの予測をします。以下のように実行します。

```diff_python
model.predict(unknown_array)
```
![スクリーンショット 2024-10-14 233128.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/7fbe5775-9960-0d64-fc30-0cedec4f3e7a.png)
※GPUを使用している場合、数値が表示のものと、僅かに異なる場合があります。

ジェネレータ作成時に確認した通り、茄子の数値ラベル0、トマトは数値ラベル1で表現されます。したがって、出力結果から、茄子である確率が99%、トマトである確率が0.004%であると予測できます。

今回は判別がしやすい茄子とトマトを選定したためかなり制度の高い学習と予測ができました。
十分な学習量（データセット）と学習済みモデルを使って転移学習を構築すればかなり精度の高い予測結果を出すことができました。
今後このモデルを応用し様々な画像判別ができればと思います。

そこで忘れてはいけないのがこのモデルの保存です。モデルの保存をしておけば次に別の画像判別を行う際かなりの時間短縮となります。以下ご参考までに

## モデルの保存

学習で実行したコード
```diff_python
history = model.fit(
      train_generator,
      validation_data=test_generator,
      epochs=10)
```
このコードに追記します。
```diff_python
history = model.fit(
      train_generator,
      validation_data=test_generator,
      epochs=10)

# # HDF5形式で保存
model.save('trainmodel.h5')
```

実行すればこのモデルが保存され別の画像判別で使用することができます。

## 補足
予測結果を見たときに一目で茄子であるトマトであるが分かり辛い時があります。
作者はジェネレータ作成時に確認するので認識しやすいですが
これを誰から見ても答えがわかる様に表します。

```diff_python
predicted = model.predict(unknown_array)
import numpy as np
np.argmax(predicted)
```
このコードは、機械学習モデルの予測結果を取得し、最も高い確率を持つクラスのインデックスを返すコードとなります。予測結果は通常、各クラスに対する確率が予測結果として出力されます。たとえば、画像分類であれば、異なるクラスに属する確率が返されます。（ナス=0、トマト=1とラベル分けをしました。）
np.argmax()は、配列の中で最も大きな値を持つ要素のインデックス（位置）を返す関数で
ここでは、predicted配列の中で最も高い確率を持つクラスを返します。たとえば、画像分類において、モデルが「ナス」「トマト」の２クラスのいずれかを予測するとき、この関数は最も高い確率のクラスを選び、そのクラスのインデックスを返します。
![スクリーンショット 2024-10-15 235708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/f791675d-1ecf-7bc5-5531-5c8467ff0b38.png)
出力結果は[0]　（0はナス、１はトマトでしたね。）


さらに分かりやすく視覚化します。
```diff_python
[k for k, v in val_generator.class_indices.items() if v == np.argmax(predicted)]
```
このコードは、val_generator に定義されたクラスラベルのインデックスと予測結果を照らし合わせ、予測されたクラス名を取得する処理を行っています。
np.argmax(predicted)これは、前述したpredicted配列から最も高い確率を持つクラスのインデックスを取得する内容です。

リスト内包表記: [k for k, v in val_generator.class_indices.items() if v == np.argmax(predicted)]:ここでは、val_generator.class_indices.items()を使って辞書のキー（クラス名）と
値（インデックス）を反復処理しています。if v == np.argmax(predicted)の部分では、
class_indicesの値（インデックス）がnp.argmax(predicted)の結果と一致するかどうかを
確認しています。
一致した場合、そのクラス名（キー）kをリストに追加します。

![スクリーンショット 2024-10-15 235841.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904069/c7e7a53c-efd2-8360-5f11-6ccf2c29ac60.png)
その結果今回の出力結果は0なのでegg_plantのクラス名が出力されました。

## 完了
予測結果の通り　画像判定は無事「ナス」（egg_plant）と表示されました。
今回はかなり分かりやすい画像判定でした。
人間の目で判別しやすい物はプログラムでも精度の高い判定は行いやすく
予測させたい画像の知識をある程度、操作する側が判別できていないと
そもそも機械に判別させるのは未だ難しい状態みたいです。
あくまで学習をさせないといけないので教える側がわかっていないと
学習者も学びが少ないということになります。
この辺りは人も同じくですね。


以上が今回チャレンジしたナス・トマトを学習済みモデルを転移学習でモデル構築し
実装されたプログラミングとなります。
非常に初歩的なレベルではあると思いますが
初心者の私にとっては土を耕し種を植えようやく芽が出た思いであります。
その芽が「身」になるようにこのモデルを応用し様々な画像認識を実装しようと思います。

最後までお目通しいただき誠に有難うございました。

## まとめ
今回の投稿では下記内容を記載しました。

・ImageDataGeneratorは、特定のフォルダに格納されている画像データを、前処理しながら、予測モデルに流し込む機能を提供するクラスであること
・VGG16を活用した転移学習方法
・VGG16の全結合層以外を取得する方法
・VGG16をSequentialモデルに変換する方法
・パラメータを凍結する方法
・独自の全結合層を追加する方法
・予測モデルの保存
・予測結果の表現方法