Python初心者による野菜画像分析

はじめに


このnoteでは、Python初心者の筆者が、野菜の分類の機械学習モデルを通じて学んだ成果を記録として残すものです。Python初学者の方の参考になれば幸いです。


内容

1.画像収集
2.画像の処理
3.実行環境(インポート)
4.画像の学習
5.画像の可視化
6.実装結果
7.まとめ


実行環境

Google Colaboratery
Visual Studio Code


作成課題

野菜の識別アプリ



Step1.画像の収集

まずは必要な画像の収集を行います。今回は野菜の中でも、「ジャガイモ」、「かぼちゃ」、「きゅうり」、「にんじん」、「ピーマン」を抜粋して画像識別ができるようにしました。また、方法としてWeb上から画像を収集する「スクレイピング」により画像データを集めました。

参考文献

  • >画像データをキーワード検索で効率的に収集する方法(Python「icrawler」のBing検索)
      


>>【だれでもできる】プログラミングが未経験でも大丈夫。Webから大量画像を収集する方法をわかりやすく解説します
  

!pip install icrawler
from icrawler.builtin import BingImageCrawler
import glob

#検索リストの生成
search_words = ["ジャガイモ","かぼちゃ","きゅうり", "にんじん", "ピーマン"]
dir_names = ["jaga","kabocha", "kyuuri", "ninzin", "piman"]

for search_word,dir_name in zip(search_words,dir_names):
  # Bing用クローラーの生成
  bing_crawler = BingImageCrawler(
      downloader_threads=4,           # ダウンローダーのスレッド数
      storage={'root_dir': "/content/drive/MyDrive/"+dir_name}) # ダウンロード先のディレクトリ名

  # クロール(キーワード検索による画像収集)の実行
  bing_crawler.crawl(
      keyword=search_word,   # 検索キーワード(日本語もOK)
      max_num=100)                    # ダウンロードする画像の最大枚数

上記、
・画像検索の決まりコードとして認識する。
・keywordは上記に記した野菜5種を日本語入力。
max_numは、最大1000枚まで指定可能とのこと。
storage={'root_dir': search_word} 辞書型リストで、収集した画像を納めるフォルダ名を作成。
(決まりコードとして認識するのが早い。重要なのは 「search_word」にあたるフォルダ名)


Step2.画像の前処理


Webで画像収集を行うと、自力収集と違い不要なデータが混ざります。
今回は手作業で不要データの間引きました。
精度を高めるために階層を増やしたり、パラメーターの見直しを行うことで精度を高めていきました。


Step3.インポート

pythonで画像認識を行うには、専用のファイルをインポートする必要があります。Aidemy(プログラミングスクール)で学習した内容と講師の方のアドバイスによりファイルをインポートするコードを作成します。

import os#osモジュール(os機能がpythonで扱えるようにする)
import cv2#画像や動画を処理するオープンライブラリ
import numpy as np#python拡張モジュール
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_jagaimo = "/content/drive/MyDrive/jaga/"
drive_kabocha = "/content/drive/MyDrive/kabocha/"
drive_kyuuri = "/content/drive/MyDrive/kyuuri/"
drive_ninzin = "/content/drive/MyDrive/ninzin/"
drive_piman = "/content/drive/MyDrive/piman/"
image_size = 200 #200x200のサイズに指定

#os.listdir() で指定したファイルを取得
path_jagaimo = [filename for filename in os.listdir(drive_jagaimo) if not filename.startswith('.')]
path_kabocha = [filename for filename in os.listdir(drive_kabocha) if not filename.startswith('.')]
path_kyuuri = [filename for filename in os.listdir(drive_kyuuri) if not filename.startswith('.')]
path_ninzin = [filename for filename in os.listdir(drive_ninzin) if not filename.startswith('.')]
path_piman = [filename for filename in os.listdir(drive_piman ) if not filename.startswith('.')]

#野菜画像を格納するリスト作成
img_jagaimo = []
img_kabocha = []
img_kyuuri = []
img_ninzin = []
img_piman = []
for i in range(len(path_jagaimo)):
   #print(drive_jagaimo+ path_jagaimo[i])   
   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_kabocha)):
   img = cv2.imread(drive_kabocha+ path_kabocha[i])
   img = cv2.resize(img,(image_size,image_size))
   img_kabocha.append(img)
for i in range(len(path_kyuuri)):
   img = cv2.imread(drive_kyuuri+ path_kyuuri[i])
   img = cv2.resize(img,(image_size,image_size))
   img_kyuuri.append(img)
for i in range(len(path_ninzin)):
   img = cv2.imread(drive_ninzin+ path_ninzin[i])
   img = cv2.resize(img,(image_size,image_size))
   img_ninzin.append(img)
for i in range(len(path_piman)):
   img = cv2.imread(drive_piman+ path_piman[i])
   img = cv2.resize(img,(image_size,image_size))
   img_piman.append(img)

#np.arrayでXに学習画像、yに正解ラベルを代入
X = np.array(img_jagaimo + img_kabocha + img_kyuuri + img_ninzin + img_piman)
#正解ラベルの作成
y =  np.array([0]*len(img_jagaimo) + [1]*len(img_kabocha) + [2]*len(img_kyuuri) + [3]*len(img_ninzin) + [4]*len(img_piman))
label_num = list(set(y))
#配列のラベルをシャッフルする
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):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

#正解ラベルをone-hotベクトルで求める(三次元での分析→ex.(0, 1, 0))
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:]))
top_model.add(Dense(512, activation="relu"))
#top_model.add(Dropout(0.5))
top_model.add(Dense(256, activation='relu'))
#top_model.add(Dropout(0.5))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(5, activation='softmax'))

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

#vgg16による特徴抽出部分の重みを15層までに固定(以降に新しい層(top_model)が追加)
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=32, epochs=50, verbose=1, validation_data=(X_test, y_test))
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
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()
#モデルを保存
model.save("my_model.h5")


以降、上記コードを自分理解で解説させて頂きます。
自分理解なので細かく説明していくことになりますが、ご容赦ください。
では区切ってご説明します。

コード①

#野菜画像の格納
drive_jagaimo = "/content/drive/MyDrive/jaga/"
drive_kabocha = "/content/drive/MyDrive/kabocha/"
drive_kyuuri = "/content/drive/MyDrive/kyuuri/"
drive_ninzin = "/content/drive/MyDrive/ninzin/"
drive_piman = "/content/drive/MyDrive/piman/"
image_size = 200#200x200のサイズに指定

野菜の画像を収集するためにグーグルドライブに収集した画像を保存します。この時、Google ColaborateryとGoogle Driveをつなげるためにマウントを行い、両者を接続することでコード上で画像収集が可能となります。

コード②

#os.listdir() で指定したファイルを取得
path_jagaimo = [filename for filename in os.listdir(drive_jagaimo) if not filename.startswith('.')]
path_kabocha = [filename for filename in os.listdir(drive_kabocha) if not filename.startswith('.')]
path_kyuuri = [filename for filename in os.listdir(drive_kyuuri) if not filename.startswith('.')]
path_ninzin = [filename for filename in os.listdir(drive_ninzin) if not filename.startswith('.')]
path_piman = [filename for filename in os.listdir(drive_piman ) if not filename.startswith('.')]

os.listdir()について説明します。os.listdir()とはlistdirの引数に、ディレクトリとファイル一覧を取得したいディレクトリのパスを渡すことで、そのディレクトリ内にあるディレクトリとファイルの一覧を取り出す事が出来ます。

参考文献


【Python入門】listdir関数でデータを取得する方法

コード③

#野菜画像を格納するリスト作成
img_jagaimo = []
img_kabocha = []
img_kyuuri = []
img_ninzin = []
img_piman = []

上記、
・空リストを事前に作成する理由: 収集する画像の増減に対応。

コード④

for i in range(len(path_jagaimo)):
   #print(drive_jagaimo+ path_jagaimo[i])   
   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_kabocha)):
   img = cv2.imread(drive_kabocha+ path_kabocha[i])
   img = cv2.resize(img,(image_size,image_size))
   img_kabocha.append(img)
for i in range(len(path_kyuuri)):
   img = cv2.imread(drive_kyuuri+ path_kyuuri[i])
   img = cv2.resize(img,(image_size,image_size))
   img_kyuuri.append(img)
for i in range(len(path_ninzin)):
   img = cv2.imread(drive_ninzin+ path_ninzin[i])
   img = cv2.resize(img,(image_size,image_size))
   img_ninzin.append(img)
for i in range(len(path_piman)):
   img = cv2.imread(drive_piman+ path_piman[i])
   img = cv2.resize(img,(image_size,image_size))
   img_piman.append(img)

上記、
・for文 range() で len()を使用し配列( )の長さを取得。
・リスト内に含まれるデータの数だけループを繰り返す。
・cv2.imreadで画像の読み込み、cv2.resizeで大きさを設定。
・.appendで写真の枚数文追加していく。

参考文献


Python, OpenCVで画像ファイルの読み込み、保存(imread, imwrite)

OpenCVで画像をリサイズする【cv2.resize】

コード⑤

#np.arrayでXに学習画像、yに正解ラベルを代入
X = np.array(img_jagaimo + img_kabocha + img_kyuuri + img_ninzin + img_piman)
#正解ラベルの作成
y =  np.array([0]*len(img_jagaimo) + [1]*len(img_kabocha) + [2]*len(img_kyuuri) + [3]*len(img_ninzin) + [4]*len(img_piman))
label_num = list(set(y))

上記、
・配列内の [0]、[1]、[2]、[3]、[4] はインデックス番号ではない。
・コンピュータは、全てを数値化して処理していく。
 野菜画像を野菜の「名前」では理解できないため、「ほうれん草=0」「ジャガイモ=0」という具合に仮名名の代わりに数字名でそれぞれの野菜を認識してもらう。

コード⑥

#配列のラベルをシャッフルする
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

上記、
random.permutationで元の配列をコピーして新しくランダムな配列を作成することで、学習効力をあげる。

参考文献


numpy.random.permutation – 配列の要素をランダムに並べ替えた新しい配列を生成

コード⑦

#学習データと検証データを用意
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):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

上記
データの80%を訓練データとして、検証データとの分割を図る。

参考文献


機械学習:訓練データとテストデータの分割方法

NumPy配列ndarrayの次元数、形状、サイズ(全要素数)を取得


Step4.画像の学習

Kerasモジュールのmodel.add()を使って転移学習をする

「keras」とは・・
・Pythonで書かれたニューラルネットワークライブラリ、シンプルな記述が特徴。
・機会学習をより簡単に使うためのライブラリ。
・プログラミング経験が無くてもコードの作成が可能。
model.add()=バッチ正規化(標準化)で学習に直接関係のないものを取り除き、学習の効率をあげる

コード⑧

#転移学習のモデルとして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:]))
top_model.add(Dense(512, activation="relu"))
#top_model.add(Dropout(0.5))
top_model.add(Dense(256, activation='relu'))
#top_model.add(Dropout(0.5))
#top_model.add(Dense(32, activation='relu'))
#top_model.add(Dense(32, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(5, activation='softmax'))

vggと自作のtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

上記、
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
・この中の「input_shape」は入力シェイプといい、以降に続く学習を行うための初期設定のようなもの。
・最初の1行目で設定すれば、以降2行目以降は必要ない。(ここはコード構成をまるっと覚える)
・活性化関数であるrelu関数(上限f(x)=x,下限f(x)=0)を使用して、全結合層で学習。
・Dropout5割で「過剰適合」などの過学習を抑制。

参考文献

Keras: Pythonの深層学習ライブラリ
Home - Keras Documentation

Sequentialモデルのガイド

コード⑨

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

#vgg16による特徴抽出部分の重みを15層までに固定(以降に新しい層(top_model)が追加)
for layer in model.layers[:15]:
   layer.trainable = False

上記、
・既存のvgg16モデルと自作モデルを連結させる。
vgg16は15層までとする。(vgg16全てを学習内容に取入れるとデータ量が多く、時間もかかる。)

コード⑩

#訓令課程の設定
 model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])

上記、
・「損失関数(loss function)」と「最適化(Optimization)」で、学習内容を構築。
・lossの値はつまり失敗値なので「0」に近いほどよい。
categorical_crossentrは分類問題を解くために用いる損失関数
to_categoricalはOne-hot-vectorを求めるための関数。

 コード内では
       y_train = to_categorical(y_train)
       y_test = to_categorical(y_test)
 がそれを示す。
・optimizerに指定した「SGD」は「確率的勾配降下法(Stochastic gradient descent)」
 ランダム抽出した1つのデータを使って、誤差の最小値を探索する。
「compile」はSequentialが持つcompileメソッド。

参考文献

One-hotベクトル(ワンホット表現)の意味とメリット・デメリット

勾配降下法とは?分かりやすく図解で解説 | 機械学習ナビ (nisshingeppo.com

categorical_crossentropyとsparse_categorical_crossentropyの違い【Keras】 | 研究所で働くエンジニアのブログ (engineeeer.com)

Modelクラス (functional API) - Keras Documentation

損失関数とは?ニューラルネットワークの学習理論【機械学習】

損失関数の利用方法

オプティマイザ(最適化アルゴリズム)の利用方法

【前編】Pytorchの様々な最適化手法(torch.optim.Optimizer)の更新過程や性能を比較検証してみた!

» KerasAPIリファレンス/ ユーティリティ/Python&NumPyユーティリティ

Keras metrics=[‘accuracy’]とは厳密にはどんな評価関数なのか

TensorFlow, Kerasの基本的な使い方(モデル構築・訓練・評価・予測)

コード⑪

# 学習の実行
#グラフ(可視化)用コード
history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test))

上記、
model.fit()では、compile化した内容を固定のepochs数で訓練。
historyは、.fit()メソッドの戻り値として取得する学習履歴のオブジェクト。
batch_sizeは、学習内容の分別個数。過学習予防。
epochsは、学習する回数。
validation_dataは、検証データ。
verboseは、学習過程の表示の仕方。(「0」表示しない、「1」プログレスバー表示、「2」結果のみ表示)

「トレーニングデータを32個に分別。50回の反復学習。訓練データと共に検証データも記録。」

参考文献

>>コールバックの使い方

>>Keras で MNIST データの学習を試してみよう

 コード⑫

score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)

上記、
model.evaluate()は評価関数。
scoreは「損失値と評価値」という2つの値をもつタプル。(タプルは()、リストは[]で表現)→テストデータを指定するとcompile()で指定した損失関数loss、評価関数metricsの結果が返される。
batch_sizeは、学習内容の分別個数。過学習予防。
verboseは、学習過程の表示の仕方。(「0」表示しない、「1」プログレスバー表示、「2」結果のみ表示)

参考文献


TensorFlow, Kerasの基本的な使い方(モデル構築・訓練・評価・予測)

コード⑬

print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))

上記、
・{0}はformatメソッドの最初の引数、score。
・{0}[0]はscoreの1つ目の要素、モデルの損失値。
・{0}[1]はscoreの2つ目の要素、モデルの評価値。

参考文献
Validation Loss <Train Loss問題への解答



Step5. 画像の可視化

コード⑭

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

上記、
Numpy.matplotlib.pyplotで訓練データと検証データのグラフを可視化

参考文献


Keras で MNIST データの学習を試してみよう (oscasierra.net)


Step6. 実装結果

実装した結果、90%近くの正解率まで引き上げることができました。
要因としては以下の調整が考えられます。
・画素数の増加(img-size=200)
・パラメーターの調整(Denseを増やす)
epoch数を増やすことも検討しましたが、データを見る限りepochs数の増減が評価値の上昇にはあまり相関しないと考えたので、以上の二つを調整しました。






Step7.最後に

まだまだ改善の余地はあるものの、未経験からのスタートで形になったのは非常にうれしく思います。ここまで支えてくださったチューターさんに感謝の意を表したいと思います。
また、WEBアプリケーションとデータ分析、機械学習を引き続き学んでいくことでさらにスキルアップを目指していきたいと思います。

いいなと思ったら応援しよう!