plant-raspberrypi3のブログ

ラズベリーパイ3とPythonに挑戦して、植物を愛でたり画像を加工したりします。最近はscikit-imageの勉強してます。

機械学習(SVM)で花とそれ以外の部分を自動分類してみた <その3>

こんにちは。らずべりーです。

前回に引き続き、機械学習による分類です。

その1とその2は以下をご参照のこと。

plant-raspberrypi3.hatenablog.com

plant-raspberrypi3.hatenablog.com

目標

紅色の花の部分画像と、それ以外(葉や枝)の部分画像を自動で区別してくれる分類器を作成する

まだ難しい方法はハードルが高いので、上記のような単純な課題設定にしてみました。 紅色の花の画像として、自分で過去に撮影していたツバキとツツジの写真を使用しました。

実行環境

手順

  1. 一枚の画像から部分画像を作成
  2. 部分画像をフォルダ分け (花の部分かそれ以外か)
  3. 各フォルダからランダムに同数の画像を読み込んでラベルつけ
  4. 個々の画像データから、基本統計量を計算
  5. サポートベクターマシンで学習させ、分類器を作成
  6. 分類器の性能をテスト

今回は4の続きから。

4. 個々の画像データの基本統計量を計算

4-4. 取り込んだ画像の基本統計量計算をバルクで実行

4-1から4-3までのコードを、バルクで処理できるようにまとめてみました。

まず、バルクで画像の読み込み&リストに格納。

import pandas as pd
import numpy as np
import scipy.stats as ss
import os
import cv2
import random

#画像をバルクで読み込むための関数
def img_bulk_read_cv2(path1,format=".png"):
    os.chdir(path1)
    all_files = os.listdir(path1)
    file_names = [i for i in all_files if i.endswith(format)]
    png_files = [cv2.imread(i) for i in file_names]
    return file_names, png_files

#BGRデータをH,S,Vに変換する関数
def bgr2h_s_v(img):
    return cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))

#画像データの基本統計量を計算する関数
def img_stats(img):
    array1 = img.flatten() #2次元画像を1次元画像に変換
    index_list = list(pd.Series(array1).describe().index)
    value_list = list(pd.Series(array1).describe().values)
    mode = float(ss.mode(array1)[0])
    kurtosis = ss.kurtosis(array1)
    skew = ss.skew(array1)
    index_list += ["mode","kurtosis","skew"]
    value_list += [mode, kurtosis, skew]
    return pd.Series(value_list, index=index_list), np.array(value_list)

#画像をバルクで読み込み、リストに格納
path1 = "/usr/local/working/20171105-tsubaki_tsutsuji/flower"
path2 = "/usr/local/working/20171105-tsubaki_tsutsuji/others"
path3 = "/usr/local/working/20171105-tsubaki_tsutsuji/partial"
fn1, flower_raw = img_bulk_read_cv2(path1, format="jpg")
fn2, others_raw = img_bulk_read_cv2(path2, format="jpg")
fn3, partial_raw = img_bulk_read_cv2(path3, format="jpg")

flowerデータの一例 f:id:plant-raspberrypi3:20171105184720j:plain

othersデータの一例 f:id:plant-raspberrypi3:20171105184742j:plain

partialデータの一例 f:id:plant-raspberrypi3:20171105184836j:plain

各グループの画像の数を最小のものに揃えます。

機械学習の時は各グループのサンプル数ができるだけ揃っていた方が良いという噂、、、

#各グループの画像の数を確認
print(len(flower_raw),",",len(others_raw),",",len(partial_raw))
#137 , 799 , 88

min_len = min(len(flower_raw),len(others_raw),len(partial_raw))

flower = random.sample(flower_raw,min_len)
others = random.sample(others_raw, min_len)
partial = random.sample(partial_raw, min_len)

リストの中身をランダムに抽出する方法については以下のサイトを参考にさせていただきました。

www.lifewithpython.com

各画像をグループ別にH, S, Vに分離。

f_h, f_s, f_v = [], [], []
for i in flower:
    h,s,v = bgr2h_s_v(i)
    f_h.append(h)
    f_s.append(s)
    f_v.append(v)

o_h, o_s, o_v = [], [], []
for i in others:
    h,s,v = bgr2h_s_v(i)
    o_h.append(h)
    o_s.append(s)
    o_v.append(v) 
    
p_h, p_s, p_v = [], [], []
for i in partial:
    h,s,v = bgr2h_s_v(i)
    p_h.append(h)
    p_s.append(s)
    p_v.append(v) 

各画像の基本統計量を計算。

img_list = [f_h, f_s, f_v, o_h, o_s, o_v, p_h, p_s, p_v]
f_ha, f_sa, f_va = [],[],[]
o_ha, o_sa, o_va = [],[],[]
p_ha, p_sa, p_va = [],[],[]
array_list = [f_ha, f_sa, f_va, o_ha, o_sa, o_va, p_ha, p_sa, p_va]
for i,j in zip(img_list,array_list):
    for x in i:
        stat_df, stat_array = img_stats(x)
        j.append(stat_array)

forループが二重になっているせいか、ここの処理はちょっと時間がかかりました(1分くらい?)。

各グループごとにH, S, Vの基本統計量を結合し、一つのデータにまとめます。

f_data = np.c_[np.array(f_ha), np.array(f_sa),np.array(f_va)]
o_data = np.c_[np.array(o_ha), np.array(o_sa),np.array(o_va)]
p_data = np.c_[np.array(p_ha), np.array(p_sa),np.array(p_va)]

numpy.arrayの結合の仕方については以下のサイト参照。

qiita.com

np.concatenate()hstack()vstack()といった方法もあるようです。(まだ使いこなせていない。)

python-remrin.hatenadiary.jp

ailaby.com

ここまでで基本統計量データの準備ができました。

5. サポートベクターマシンで学習させ、分類器を作成 〜 6. 分類器の性能をテスト

4までで準備した3グループの基本統計量データを1つにまとめ、ちゃんと結合できているか調べます。

次に、データの正解ラベルを作成します。

今回、花の部分(flower)は2、一部のみ花の部分が含まれているもの(partial)は1、花の部分が含まれていないもの(others)は0としました。

#3グループのデータ
data = np.r_[f_data, o_data, p_data]
print(pd.DataFrame(data).shape)
#(264, 33)

#3グループのラベル
labels = np.r_[np.ones(len(f_ha))*2, np.zeros(len(o_ha)), np.ones(len(p_ha))]

#flowerとothersのみのデータ
data2 = np.r_[f_data, o_data]

#flowerとothersのみのラベル
labels2 = np.r_[np.ones(len(f_ha))*2, np.zeros(len(o_ha))]

いよいよ機械学習です!!!

分類問題では(深層学習以外で)最も評判が良さげなSVMにトライしてみました。

SVMとはなんぞや、ということについては他のサイトに説明を任せることにします(あまりちゃんと理解できていない)。

個人的にこの辺りのサイトの解説がわかりやすいと感じました。

logics-of-blue.com

www.sys.ci.ritsumei.ac.jp

早速、scikit-learnのsvmを使ってやってみます。

公式ドキュメントは以下。

sklearn.svm.SVC — scikit-learn 0.19.1 documentation

ですが、まだちょっと敷居が高かったので、とりあえず他の方々がやっているのを真似っこしてみるところからはじめました。

qiita.com

may46onez.hatenablog.com

qiita.com

公式ドキュメントもおいおい詳しく見ていきたいです。

今回はグリッドサーチなどは行わず、クロスバリデーションと適当に調整したパラメータで学習してみました。

まずはじめに、flowerothersの2種類に分類してみました。

from sklearn import svm
from sklearn.cross_validation import train_test_split
from sklearn.metrics import classification_report, confusion_matrix

# 訓練データとテストデータに分割
X_train2, X_test2, y_train2, y_test2 = train_test_split(data2, labels2, test_size=0.2, random_state=0)

#svmによるモデルの作成
clf2 = svm.SVC(C=1, cache_size=200, 
  decision_function_shape='ovr', degree=3, gamma=0.00001, kernel='rbf')

#モデルを訓練データで学習
clf2.fit(X_train2, y_train2)

テストデータを用いて、学習したモデルの性能を評価します。

y_true2, y_pred2 = y_test2, clf2.predict(X_test2)

print(classification_report(y_true2, y_pred2))

#               precision    recall  f1-score   support
#
#        0.0       1.00      1.00      1.00        18
#        2.0       1.00      1.00      1.00        18
#
# avg / total       1.00      1.00      1.00        36

print(confusion_matrix(y_true2, y_pred2))

#[[18  0]
#[ 0 18]]

各グループ18ずつ、合計36のデータ全てが正しく分類できているようです。

問題が簡単すぎたんでしょうか???

次に、partialのデータも入れて、3種類に分類するのを試しました。

X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.2, random_state=0)

clf = svm.SVC(C=1, cache_size=200, 
  decision_function_shape='ovr', degree=3, gamma=0.00001, kernel='rbf')

clf.fit(X_train, y_train)

y_true, y_pred = y_test, clf.predict(X_test)

print(classification_report(y_true, y_pred))

#               precision    recall  f1-score   support
#
#        0.0       0.77      0.89      0.83        19
#        1.0       0.86      0.67      0.75        18
#        2.0       0.94      1.00      0.97        16
#
# avg / total       0.85      0.85      0.84        53

print(confusion_matrix(y_true, y_pred))

#[[17  2  0]
# [ 5 12  1]
# [ 0  0 16]]

partialデータの分類が難しいようですが、85%程度は予測できているみたいです。

グリッドサーチを行えば、もっと良いパラメータが見つかるかも??

confusion matrixがそのままだと見づらかったので、pandasで見やすく表示しなおしてみます。

cm = confusion_matrix(y_true, y_pred)
pd.DataFrame(cm/cm.sum(axis=1)).round(2)

横が予測データ、縦が正解ラベルです。 2がflower、1がpartial、0がothersを表します。

f:id:plant-raspberrypi3:20171112173700p:plain

どうもotherspartialがうまく見分けられないのが精度が上がらない原因のようです。

flowerについてはほぼ正しく分類できている様子なので、今後このモデルを使って画像認識的なことができないか試していきたいところ。

長い道のりだった、、、:(´ཀ`」 ∠):


2017/11/13 追記 scikit-learnについて、一部コードに仕様変更があるようです。

以下の記事にまとめました。

plant-raspberrypi3.hatenablog.com