水増しした画像データを使って再度CNNでデータを学習
実行結果
hanamichi-sukusuku.hatenablog.com
実行結果としては上記で水増しせずに学習を行った場合の正解率は0.816...ほどだったので少し精度をあげることができた。
プログラムを見ていく。
必要な変数の定義
im_rows、im_colsでは学習に使用する縦、横のピクセルサイズを指定。
im_colorではRGB色空間を指定。白黒なら1。
in_shapeではCNNのモデルの入力層に渡す入力データ数を定義。
nd_classesは出力数。今回はsushi,salad,tohuの3種類なので3。
画像データ読み込み
imageディレクトリのphotos.npzはFlickr APIで取得した画像データをnumpy形式に変換したデータが格納されている。
x = photos['x'], y = photos['y']ではnumpy形式でデータを保存するときに配列名をそれぞれx,yと指定したため、['配列名']として取り出すことができる。
各データをCNNで学習できるように変換
画像データ(x)は一つの画像を3つの要素を32列保有した行列を32行で表すことができるのでreshapeで三次元の配列に変換し、astype('float32') でndarrayの配列が保有しているデータ型を変換している。float32で小数点第八位までの値を扱うように指定。小数点第九位の値を四捨五入して第八位の値を決定する。255で割ることで0.0~1.0でデータを表現するようにしている。
ラベルデータ(y)ではkeras.utils.to_categorical()でone-hotベクトルに変換する。
train_test_split()で学習用、テスト用にデータを分割。
学習データの水増し
画像データを回転させ、学習用のデータを水増しする。
学習データを一つずつ取り出す。
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形式に変換して学習用に置き換える
モデルの読み込みと学習、評価
別ファイルに作成したCNNモデルをcnn_modelファイルのget_model()関数を呼び出し、返り値としてCNNモデルを取得。
model.fit()で学習。
modelevaluate()で評価。
学習の様子をグラフで表示、モデルの重みデータを保存
画像データの水増し opencv
写真のデータ数が少なく判定精度が悪くなってしまう場合には画像データを回転させたり、反転させることでデータを水増しできる。コンピューターにとっては元の画像と全く異なる画像と認識される。
実行結果
このプログラムでは一つの画像を回転させて描画している。
cv2.getRotationMatrix2D(画像の中心の座標,回転させたい角度,拡大比率)を利用して画像を回転。
cv2.warpAffine()ではアフィン変換をしている。イメージはgetRotationMatrix2Dで回転を表すアフィン変換行列を作成し、cv2.warpAffine(元画像, アフィン変換行列, ピクセルサイズ)でアフィン変換行列を適用する感じ。
Flickr APiで取得した画像データをCNNを利用して判定
hanamichi-sukusuku.hatenablog.com
今回は上記でnumpy形式に変換したFlickr APIで取得した画像データを使って、CNNで機械学習にかけていく。
モデルを別ファイルに定義
cnn_model.py
モデルでは畳み込み、畳み込み、プーリング、ドロップアウト、この流れを2回繰り返し、Flatten()で平滑化、全結合層へと流し込んでいくように定義。
get_model()関数ではdef_model()関数を呼び出しモデルを取得。
そして、モデルを構築し呼び出し元に返す。
次にnumpy形式に変換したFlickr APIの画像データを作成したモデルを用いて学習させていく
実行結果
実行結果としてはそこまでいい正解率ではなかった。画像クリーニングでの精査をしっかりすればもう少し上がる気がする。
プログラムを見ていく
入力出力の変数を定義
画像サイズは縦、横32ピクセルなので32をそれぞれ定義。
今回扱う画像の色空間はRGBなのでim_colorには3を定義。
in_shapeには入力レイヤーに渡す入力を定義。CNNなので3次元の入力数を定義。
nd_classesには出力数を指定。sushi、salad、tofuの3種類なので3。
画像データの読み込み
作成したnumpy形式のデータはimageディレクトリにphotos.npzというファイル名で保存されているのでそれを読み込む。
photos['x']、photos['y']でx(画像データ)、y(ラベルデータ)を取得しているのは、保存する際のnp.savez()でそれぞれの配列名をx,yとして保存したため、オブジェクトからの取得の仕方が['x']、['y']のようになる。
データをCNNで学習できるように変換、テスト用、学習用に分割
reshapeで三次元の配列に変換、astypeでデータ型の変換と255で割ることでデータを0.0~1.0で表現できるようにしている。
ラベルデータはone-hotベクトルに変換するためkeras.utils.to_categorical()で変換。
train_test_split()でテスト用、学習用に変換。
先ほど作成したCNNモデルを定義したファイルからモデルを読み込み、学習させる
cnn_model.get_model(in_shape, nb_classes)では上記で作成したcnn_model.pyからget_model関数を実行している。これによってCNNモデルを取得できる。
model.fit()で学習。validation_dataで学習とともにテストデータの評価結果を変数histに格納。
モデル評価と結果出力
model.evaluate()でテストデータの評価。
学習の様子をグラフに描画
historyオブジェクトが格納されている変数histから正解率、ロス、それぞれのデータを取得しグラフに描画。
最後にmodel.save_weights()でモデルの重みデータを保存。
Flickr APIで取得した画像データをNumpy形式に変換してファイルに保存
Flickr APIで取得した画像データから関係のない画像を削除してクリーニングしたデータをnumpy形式のデータに変換してファイルに保存していく。
Flickr APIを使用して画像の取得に関しては下記の記事で取得している。
hanamichi-sukusuku.hatenablog.com
実行結果
imageディレクトリにphotos.npというファイル名でNumpy形式の画像データが格納される。
必要な変数の定義
main()関数の実行
main()関数、各フォルダの画像データを読み込み、glob_files()関数を呼び出しnumpy形式に変換してファイルに保存
glob_files()関数にファイルのパスとラベルデータを渡してx,yデータを生成。
np.savez()で複数のnumpy配列を保存。(ファイルパス, xデータ, yデータ)。
一つのnumpy配列の場合はnp.save()。
glob_files()関数、引数で受け取ったファイルパスの画像データをnumy形式に変換してx(画像データ),y(ラベルデータ)を作成
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ディレクトリも作成しておく。
実行結果
imageディレクトリ以下にそれぞれの料理名のディレクトリが作成され、300枚ずつの画像がダウンロードされる。
Flickrとは写真共有サイトで、ここで提供する写真を写真検索APIを利用して、様々な種類の写真を集めることができる。
APIキーとシークレットの定義
このキーとシークレットがないとAPIを使用できない。
wait_timeは一度に大量の画像をダウンロードしようとすると、Flickrのサーバーに負荷をかけてしまうので、写真を1枚ダウンロードするごとに1秒のウエイトをおくために定義している。
main()関数の呼び出し
main()関数、キーワードとディレクトリ名を指定してファイルをダウンロード
go_download()関数にキーワードとディレクトリ名を渡す。
今回は3種類だが、他のキーワードを入力すればそのキーワードの写真もダウンロードできる。
go_download()関数、Flickr APIで写真を検索しダウンロード
この関数によってFlickr APIを使用して写真をダウンロードする。
この部分で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の中身は
このようになっている。寿司、マグロのキーワードで検索したときの中身。
キーワードに関連した写真データが取得できていることがわかる。extrasに指定したurl_q,licenseもちゃんと取得できている。これらのデータはphotosという名前のキーに格納されているため
res['photos']で変数photosにphotosの値を格納。
pprintモジュールではリストや辞書型のデータを綺麗に整形して出力してくれる。
pprint(photos)で辞書型のデータを整形し出力。
ここまでで、Flickr APIにアクセスし、辞書型で300枚の画像に関するデータは取得できた。これ以降で画像データをダウンロードしていく。
try-exceptを使用し、tryの後に例外が発生するかもしれないが実行したい処理を、exceptの後に例外が発生した時の処理を記述する。
enumerate(photos['photo'])では先ほど、変数resの中身を見てわかる通り、一枚ずつの写真データは文字列photoをキーとして格納されているので、そのデータを取得し、ループ処理をしている。
変数filepathで保存先のパスを作成。
urlretrieve()はネット上からファイルをダウンロードするために使用。
urlretrieve(ダウンロードしたいファイルのURL, 保存先のパス)
timeモジュールでは時刻に関する様々な関数を使用することができる。sleep()関数を実行することで処理を一時停止させる。これは一度に大量の画像をダウンロードしようとすると、Flickrのサーバーに負荷をかけてしまうので、写真を1枚ダウンロードするごとに1秒のウエイトをおくため。
exceptの処理では標準ライブラリのtracebackを使用することで発生した例外がどんな原因なのかを出力するようにしている。
作成したデータベースのデータを機械学習にかける
モデル構築
実行結果
MLPモデルのファイルが保存される。
hanamichi-sukusuku.hatenablog.com
扱うデータは上記で作成。
in_sizeで入力数を定義。ラベルデータとして体型の値を使用するので出力数に6を定義。
モデルの重みファイルの保存
実行結果
学習済みの重みファイルが作成される。
データベースから新しい100件のデータを取得し、学習データを作成する
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.fitをすると前回の学習結果に加えて新しいデータで学習できるので、新たなデータに対応するようにパラメーター修正するような仕組みになっている。
ラベルデータをone-hotベクトルに変換
学習、重みファイル保存
精度を確認していく
実行結果
身長160cm、体重50kgは「標準体重(普通体重)」であるのが正しいが、低体重(痩せ型)と間違った値を出力している。これは学習したデータが100件と少なすぎたため。
これを改善するために体重データをデータベースに挿入するプログラムとデータを学習するプログラムを交互に繰り返し実行していく。そして、5000件ほどのデータを学習させてみる。面倒なら挿入する値を一気に5000件、学習する値も5000件にして実行すると良い。
実行結果
正解率を向上させることができた。
まとめ
・データベースから定期的にデータを読み出して機械学習の分類器に学習させることができる。
・学習するデータは、CSV形式でも、RDBMSのデータベースから取り出したものでも、正しいデータであれば十分使える。
・定期的にデータが追加される学習器であれば、日々データが増えることで判定精度も向上していく。