MENU

水増しした画像データを使って再度CNNでデータを学習

f:id:hanamichi_sukusuku:20210301140333p:plain

f:id:hanamichi_sukusuku:20210301140357p:plain

実行結果

f:id:hanamichi_sukusuku:20210301140529p:plain

f:id:hanamichi_sukusuku:20210301140545p:plain

hanamichi-sukusuku.hatenablog.com

実行結果としては上記で水増しせずに学習を行った場合の正解率は0.816...ほどだったので少し精度をあげることができた。

 

プログラムを見ていく。

 

必要な変数の定義

import cnn_model
import keras
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split
import cv2

# 入力と出力を指定
im_rows = 32 # 画像の縦ピクセルサイズ
im_cols = 32 # 画像の横ピクセルサイズ
im_color = 3 # 画像の色空間
in_shape = (im_rows, im_cols, im_color)
nb_classes = 3

 

im_rows、im_colsでは学習に使用する縦、横のピクセルサイズを指定。

im_colorではRGB色空間を指定。白黒なら1。

in_shapeではCNNのモデルの入力層に渡す入力データ数を定義。

nd_classesは出力数。今回はsushi,salad,tohuの3種類なので3。

 

画像データ読み込み

photos = np.load('image/photos.npz')
x = photos['x']
y = photos['y']

imageディレクトリのphotos.npzはFlickr APIで取得した画像データをnumpy形式に変換したデータが格納されている。

x = photos['x'], y = photos['y']ではnumpy形式でデータを保存するときに配列名をそれぞれx,yと指定したため、['配列名']として取り出すことができる。

 

各データをCNNで学習できるように変換

# 読み込んだデータをの三次元配列に変換
x = x.reshape(-1, im_rows, im_cols, im_color)
x = x.astype('float32') / 255
# ラベルデータをone-hotベクトルに直す
y = keras.utils.to_categorical(y.astype('int32'), nb_classes)

# 学習用とテスト用に分ける
x_train, x_test, y_train, y_test = train_test_split(
x, y, train_size=0.8)

 

画像データ(x)は一つの画像を3つの要素を32列保有した行列を32行で表すことができるのでreshapeで三次元の配列に変換し、astype('float32') でndarrayの配列が保有しているデータ型を変換している。float32で小数点第八位までの値を扱うように指定。小数点第九位の値を四捨五入して第八位の値を決定する。255で割ることで0.0~1.0でデータを表現するようにしている。

 

ラベルデータ(y)ではkeras.utils.to_categorical()でone-hotベクトルに変換する。

train_test_split()で学習用、テスト用にデータを分割。

 

学習データの水増し

f:id:hanamichi_sukusuku:20210301143151p:plain

画像データを回転させ、学習用のデータを水増しする。

for i, xi in enumerate(x_train):

学習データを一つずつ取り出す。

for ang in range(-30, 30, 5):

range()に3つの引数を渡した時は(start, stop, step)の様に指定することでstepの値だけ飛ばした等差数列が生成される。

今回で言えば

(-30, 30, 5)なので[-30,-25, -20, -15. -10, -5, 0, 5, 10, 15, 20, 25]という数列が生成される。この時、start<= i < stopなので気を付ける。

 

cv2.getRotation Matrix2D(回転の中心点, 角度, 拡大比率)でアフィン変換行列を生成。

cv2.warpAffine(元画像データ, アフィン変換行列, ピクセルサイズ)でアフィン変換。

アフィン変換を行ったデータをx_newに追加しデータを作り直していく。

ラベルデータも忘れずに。

 

cv2.flip()はopencvで画像を上下左右に反転させる関数。第一引数に元となるndarrayのデータ、第二引数に反転の方向を指定。

flipcode = 0上下反転、flipcode > 0左右反転、flipcode < 0上下左右反転。0か1か-1を目的に応じて指定すれば良い。今回は1なので左右反転。

反転させた画像もx_newに追加。

 

水増しした画像をnumpy形式に変換して学習用に置き換える

print('水増し前=', len(y_train))
x_train = np.array(x_new)
y_train = np.array(y_new)
print('水増し後=', len(y_train))

 

モデルの読み込みと学習、評価

f:id:hanamichi_sukusuku:20210301150911p:plain

別ファイルに作成したCNNモデルをcnn_modelファイルのget_model()関数を呼び出し、返り値としてCNNモデルを取得。

model.fit()で学習。

modelevaluate()で評価。

 

学習の様子をグラフで表示、モデルの重みデータを保存

# 正解率の推移をプロット
plt.plot(hist.history['accuracy'])
plt.plot(hist.history['val_accuracy'])
plt.title('Accuracy')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# ロスの推移をプロット
plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.title('Loss')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

model.save_weights('./image/photos-model.hdf5')

 

 

 

 

アフィン変換 opencv

アフィン変換とは画像を回転させたり、拡大縮小させたり、平行移動をまとめて3✖️3の行列を使って変換することをアフィン変換という。

opencvのcv2.getRotaionMatrix2D()ではアフィン変換に使用する、画像の回転に必要なアフィン行列を作成している。引数には(画像の中心の座標,回転させたい角度,拡大比率)を指定する。

cv2.getRotationMatrix2D()で作成したアフィン行列をアフィン変換するのが、cv2.warpAffine(元画像, アフィン変換行列, ピクセルサイズ)。

これらを行うことで画像の回転や、拡大縮小などの処理を画像に加えることができる。

画像データの水増し opencv

写真のデータ数が少なく判定精度が悪くなってしまう場合には画像データを回転させたり、反転させることでデータを水増しできる。コンピューターにとっては元の画像と全く異なる画像と認識される。

f:id:hanamichi_sukusuku:20210227202634p:plain

実行結果

f:id:hanamichi_sukusuku:20210227202700j:plain

このプログラムでは一つの画像を回転させて描画している。

 

cv2.getRotationMatrix2D(画像の中心の座標,回転させたい角度,拡大比率)を利用して画像を回転。

cv2.warpAffine()ではアフィン変換をしている。イメージはgetRotationMatrix2Dで回転を表すアフィン変換行列を作成し、cv2.warpAffine(元画像, アフィン変換行列, ピクセルサイズ)でアフィン変換行列を適用する感じ。

 

 

Flickr APiで取得した画像データをCNNを利用して判定

hanamichi-sukusuku.hatenablog.com

今回は上記でnumpy形式に変換したFlickr APIで取得した画像データを使って、CNNで機械学習にかけていく。

 

モデルを別ファイルに定義

 cnn_model.py

f:id:hanamichi_sukusuku:20210226165527p:plain

モデルでは畳み込み、畳み込み、プーリング、ドロップアウト、この流れを2回繰り返し、Flatten()で平滑化、全結合層へと流し込んでいくように定義。

 

get_model()関数ではdef_model()関数を呼び出しモデルを取得。

そして、モデルを構築し呼び出し元に返す。

 

次にnumpy形式に変換したFlickr APIの画像データを作成したモデルを用いて学習させていく

f:id:hanamichi_sukusuku:20210226170505p:plain

f:id:hanamichi_sukusuku:20210226170629p:plain

実行結果

f:id:hanamichi_sukusuku:20210226170721p:plain

f:id:hanamichi_sukusuku:20210226171045p:plain

実行結果としてはそこまでいい正解率ではなかった。画像クリーニングでの精査をしっかりすればもう少し上がる気がする。

 

プログラムを見ていく

入力出力の変数を定義

im_rows = 32 # 画像の縦ピクセルサイズ
im_cols = 32 # 画像の横ピクセルサイズ
im_color = 3 # 画像の色空間
in_shape = (im_rows, im_cols, im_color)
nb_classes = 3

画像サイズは縦、横32ピクセルなので32をそれぞれ定義。

今回扱う画像の色空間はRGBなのでim_colorには3を定義。

in_shapeには入力レイヤーに渡す入力を定義。CNNなので3次元の入力数を定義。

nd_classesには出力数を指定。sushi、salad、tofuの3種類なので3。

 

画像データの読み込み

photos = np.load('image/photos.npz')
x = photos['x']
y = photos['y']

 

作成したnumpy形式のデータはimageディレクトリにphotos.npzというファイル名で保存されているのでそれを読み込む。

photos['x']、photos['y']でx(画像データ)、y(ラベルデータ)を取得しているのは、保存する際のnp.savez()でそれぞれの配列名をx,yとして保存したため、オブジェクトからの取得の仕方が['x']、['y']のようになる。

 

データをCNNで学習できるように変換、テスト用、学習用に分割

x = x.reshape(-1, im_rows, im_cols, im_color)
x = x.astype('float32') / 255
# ラベルデータをone-hotベクトルに直す --- (*4)
y = keras.utils.to_categorical(y.astype('int32'), nb_classes)

# 学習用とテスト用に分ける --- (*5)
x_train, x_test, y_train, y_test = train_test_split(
   x, y, train_size=0.8)

reshapeで三次元の配列に変換、astypeでデータ型の変換と255で割ることでデータを0.0~1.0で表現できるようにしている。

ラベルデータはone-hotベクトルに変換するためkeras.utils.to_categorical()で変換。

train_test_split()でテスト用、学習用に変換。

 

先ほど作成したCNNモデルを定義したファイルからモデルを読み込み、学習させる

model = cnn_model.get_model(in_shape, nb_classes)

# 学習を実行 --- (*7)
hist = model.fit(x_train, y_train,
   batch_size=32,
   epochs=20,
   verbose=1,
   validation_data=(x_test, y_test))

 

cnn_model.get_model(in_shape, nb_classes)では上記で作成したcnn_model.pyからget_model関数を実行している。これによってCNNモデルを取得できる。

 

model.fit()で学習。validation_dataで学習とともにテストデータの評価結果を変数histに格納。

 

モデル評価と結果出力

score = model.evaluate(x_test, y_test, verbose=1)
print('正解率=', score[1], 'loss=', score[0])

model.evaluate()でテストデータの評価。

 

学習の様子をグラフに描画

# 正解率の推移をプロット
plt.plot(hist.history['accuracy'])
plt.plot(hist.history['val_accuracy'])
plt.title('Accuracy')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# ロスの推移をプロット
plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.title('Loss')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

model.save_weights('./image/photos-model-light.hdf5')

 

historyオブジェクトが格納されている変数histから正解率、ロス、それぞれのデータを取得しグラフに描画。

最後にmodel.save_weights()でモデルの重みデータを保存。

 

 

 

 

 

Flickr APIで取得した画像データをNumpy形式に変換してファイルに保存

Flickr APIで取得した画像データから関係のない画像を削除してクリーニングしたデータをnumpy形式のデータに変換してファイルに保存していく。

Flickr APIを使用して画像の取得に関しては下記の記事で取得している。

hanamichi-sukusuku.hatenablog.com

 

f:id:hanamichi_sukusuku:20210225150004p:plain

実行結果

imageディレクトリにphotos.npというファイル名でNumpy形式の画像データが格納される。

 

必要な変数の定義

outfile = "image/photos.npz" # 保存ファイル名
max_photo = 100 # 利用する写真の枚数
photo_size = 32 # 画像サイズ
x = # 画像データ
y = # ラベルデータ

 

main()関数の実行

if __name__ == '__main__':
  main()

 

main()関数、各フォルダの画像データを読み込み、glob_files()関数を呼び出しnumpy形式に変換してファイルに保存

f:id:hanamichi_sukusuku:20210225151606p:plain

glob_files()関数にファイルのパスとラベルデータを渡してx,yデータを生成。

np.savez()で複数のnumpy配列を保存。(ファイルパス, xデータ, yデータ)。

一つのnumpy配列の場合はnp.save()。

 

glob_files()関数、引数で受け取ったファイルパスの画像データをnumy形式に変換してx(画像データ),y(ラベルデータ)を作成

f:id:hanamichi_sukusuku:20210225152008p:plain

glob.glob()で引数で受け取ったパスの.jpgを含むファイル名を全て取得。

random.shuffle()でリストをランダムに並べ替える。

for文の中では100枚のデータを使用するのでif文で100枚を超えた時処理を止めるようにしている。

Pillowは画像処理ライブラリ。ここではPillowライブラリのImage関数を使用して画像の読み込みや色空間の変換を行っているがopencvのような高度な画像処理はできない。ただ、リサイズやトリミングなど簡単な処理はopencvを使用するよりも簡単に実装できるため状況によって使い分けるといい。

 

Image.open(読み込みたい画像のパス)

img.convert(変換したい色空間の指定)

img.resize*1でサイズ指定

np.asarray()で変換したいオブジェクトを指定。np.array()でもnp.asarray()でもnumpy形式への変換はできる。違いとしては引数に指定したオブジェクトが変更された時それを反映されるかされないか。

例(np.asarrayの引数がnp.ndarrayの時でないと下記の違い発生しない)

np.asarray

neko = [0,1,2,3]

neko = np.array(neko)

n = np.asarray(neko)

neko[0] = 100

print(neko)

print(n)

>>[100,1,2,3]

>>[100,1,2,3]

同期的に元のデータが変更すればasarrayで変換したデータも更新される。

np.array

neko = [0,1,2,3]

neko = np.array(neko)

n = np.array(neko)

neko[0] = 100

print(neko)

print(n)

>>[100,1,2,3]

>>[0,1,2,3]

np.arrayで変換したデータと元データは別物として扱われる。

 

*1:width, height

python Flickr APIの使用

前提として、yahoo.comのアカウント作成とFlickr APIのページでKeyとSecretを生成し、FlickrAPIを使用するのに必要なモジュール(flickrapiモジュール)をインストールした状態で始める。あと、imageディレクトリも作成しておく。

f:id:hanamichi_sukusuku:20210224203115p:plain

実行結果

f:id:hanamichi_sukusuku:20210224205124p:plain

imageディレクトリ以下にそれぞれの料理名のディレクトリが作成され、300枚ずつの画像がダウンロードされる。

 

Flickrとは写真共有サイトで、ここで提供する写真を写真検索APIを利用して、様々な種類の写真を集めることができる。

 

APIキーとシークレットの定義

key = "a880b66929d40bdc255e3cb6b28eaa56"
secret = "885f6135fc82a8e5"
wait_time = 1 # 待機秒数(1以上を推奨)

このキーとシークレットがないとAPIを使用できない。

wait_timeは一度に大量の画像をダウンロードしようとすると、Flickrのサーバーに負荷をかけてしまうので、写真を1枚ダウンロードするごとに1秒のウエイトをおくために定義している。

 

main()関数の呼び出し

if __name__ == '__main__':
  main()

 

main()関数、キーワードとディレクトリ名を指定してファイルをダウンロード

def main():
go_download('マグロ 寿司', 'sushi')
go_download('サラダ', 'salad')
go_download('麻婆豆腐', 'tofu')

go_download()関数にキーワードとディレクトリ名を渡す。

今回は3種類だが、他のキーワードを入力すればそのキーワードの写真もダウンロードできる。

 

go_download()関数、Flickr APIで写真を検索しダウンロード

f:id:hanamichi_sukusuku:20210224205008p:plain

この関数によってFlickr APIを使用して写真をダウンロードする。

 

flickr = FlickrAPI(key, secret, format='parsed-json')

この部分でFlickr APIにアクセス。(キー、シークレット、受け取るフォーマットの指定(今回はjson))。

生成されたオブジェクトからphotos.search()でキーワードで写真を検索。

・text = keyword(変数) 検索語

・per_page = 300 取得件数

・media = photos 写真の検索を指定、videosと指定すると動画のみになる。

・sort = 'relevance' 検索語の関連順に並べる、relevanceは最新のものからの取得を指定。

・safe_search = 1 不適切な画像を除外してくれる、1~3があり1が最も安全。

・extras = 'url_q, license' 余分に取得するデータの種類を指定、複数の場合は,で区切る。url_qは150✖️150の画像ファイルURLの取得を指定。licenseはライセンス情報の取得を指定。

このflickr.photos.search()で取得した変数resの中身は

f:id:hanamichi_sukusuku:20210224211956p:plain

このようになっている。寿司、マグロのキーワードで検索したときの中身。

キーワードに関連した写真データが取得できていることがわかる。extrasに指定したurl_q,licenseもちゃんと取得できている。これらのデータはphotosという名前のキーに格納されているため

photos = res['photos']
pprint(photos)

 

res['photos']で変数photosにphotosの値を格納。

pprintモジュールではリストや辞書型のデータを綺麗に整形して出力してくれる。

pprint(photos)で辞書型のデータを整形し出力。

 

ここまでで、Flickr APIにアクセスし、辞書型で300枚の画像に関するデータは取得できた。これ以降で画像データをダウンロードしていく。

f:id:hanamichi_sukusuku:20210224213346p:plain

try-exceptを使用し、tryの後に例外が発生するかもしれないが実行したい処理を、exceptの後に例外が発生した時の処理を記述する。

 

enumerate(photos['photo'])では先ほど、変数resの中身を見てわかる通り、一枚ずつの写真データは文字列photoをキーとして格納されているので、そのデータを取得し、ループ処理をしている。

変数filepathで保存先のパスを作成。

urlretrieve()はネット上からファイルをダウンロードするために使用。

urlretrieve(ダウンロードしたいファイルのURL, 保存先のパス)

timeモジュールでは時刻に関する様々な関数を使用することができる。sleep()関数を実行することで処理を一時停止させる。これは一度に大量の画像をダウンロードしようとすると、Flickrのサーバーに負荷をかけてしまうので、写真を1枚ダウンロードするごとに1秒のウエイトをおくため。

 

exceptの処理では標準ライブラリのtracebackを使用することで発生した例外がどんな原因なのかを出力するようにしている。

 

 

 

 

 

 

 

 

 

 

 

 

 

作成したデータベースのデータを機械学習にかける

モデル構築

f:id:hanamichi_sukusuku:20210223160238p:plain

実行結果

MLPモデルのファイルが保存される。

 

hanamichi-sukusuku.hatenablog.com

扱うデータは上記で作成。

in_sizeで入力数を定義。ラベルデータとして体型の値を使用するので出力数に6を定義。

 

モデルの重みファイルの保存

f:id:hanamichi_sukusuku:20210223161016p:plain

実行結果

学習済みの重みファイルが作成される。

 

データベースから新しい100件のデータを取得し、学習データを作成する

f:id:hanamichi_sukusuku:20210223161752p:plain

 

with sqlite3.connect()でデータベースに接続、cursorオブジェクト生成。

for文で実行しているSQL文は

SELECT * FROM person ORDER BY id DESC LIMIT 100

・SELECT(データベースからデータ取得)

・* FROM person(personテーブルから全て取得、*を使うことで「全て」という意味になる)

・ORDER BY(データをソートして取得したい時に使用)

・id DESC(idを降順にソートしてレコード取得) LIMIT 100(100件取得)

ブロック変数rowにはid,height, weight, typeNoが格納されているのでそれぞれ変数に代入。

height、weightの正規化に関しては大体身長では2m、体重では150kgを最大値として考えてこの値を使用していると思う。

x、yにそれぞれ追加。

 

モデル読み込み、既に重みデータがあればそれも読み込む

model = load_model('hw_model.h5')

# 既に学習データがあれば読み込む --- (*5)
if os.path.exists('hw_weights.h5'):
   model.load_weights('hw_weights.h5')

 

既に重みデータが存在すれば、データベースに新しいデータが追加された時、保存しておいた前の重みデータをさらに追加したデータによって学習させることができる。実際のプログラムで使用するには毎回一から学習させていては時間がかかってしまう。既に学習済みのデータがある場合にmodel.fitをすると前回の学習結果に加えて新しいデータで学習できるので、新たなデータに対応するようにパラメーター修正するような仕組みになっている。

 

ラベルデータをone-hotベクトルに変換

nb_classes = 6 # 体型を6段階に分ける
y = to_categorical(y, nb_classes) # one-hotベクトルに直す

 

学習、重みファイル保存

model.fit(np.array(x), y,
  batch_size=50,
  epochs=100)

# 結果を保存する --- (*7)
model.save_weights('hw_weights.h5')

 

精度を確認していく

f:id:hanamichi_sukusuku:20210223165351p:plain

実行結果

f:id:hanamichi_sukusuku:20210223165409p:plain

身長160cm、体重50kgは「標準体重(普通体重)」であるのが正しいが、低体重(痩せ型)と間違った値を出力している。これは学習したデータが100件と少なすぎたため。

これを改善するために体重データをデータベースに挿入するプログラムとデータを学習するプログラムを交互に繰り返し実行していく。そして、5000件ほどのデータを学習させてみる。面倒なら挿入する値を一気に5000件、学習する値も5000件にして実行すると良い。

実行結果

f:id:hanamichi_sukusuku:20210223170538p:plain

正解率を向上させることができた。

 

まとめ

・データベースから定期的にデータを読み出して機械学習の分類器に学習させることができる。

・学習するデータは、CSV形式でも、RDBMSのデータベースから取り出したものでも、正しいデータであれば十分使える。

・定期的にデータが追加される学習器であれば、日々データが増えることで判定精度も向上していく。