MENU

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のデータベースから取り出したものでも、正しいデータであれば十分使える。

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

 

python データベース(RDBMS)を作成しデータを格納。

f:id:hanamichi_sukusuku:20210222185408p:plain

実行結果

hw.sqlite3というデータベースファイルが生成される。
実際の機械学習のプログラムにおいてはデータベースでのデータを使用することがほとんどなのでデータベースを連携させ、機械学習を行えるようにデータベースの生成から学んでいく。

 

上記コードでデータベースファイルの作成。

今回では簡易に利用できるデータベース(RDBMS)として、SQLiteを利用。SQLiteであれば、pythonの標準ライブラリーなので手軽にSQLクエリーを利用してデータベースを操作できる。

今回作成しているデータベースは、身長と体重、体型の3つのフィールドを持つデータベースとする。

列  フィールド説明    DBフィールド名

0  顧客ID(自動で追加)     id

1  身長(cm)        height

2  体重(kg)        weight

3  体型(0~5の値)     typeNo

 

体型は6段階の値で表現

値  体型

0   低体重(痩せ型)

1  普通体重

2  肥満1

3  肥満2

4  肥満3

5  肥満4

 

データベースに接続して、SQL文の実行

with sqlite3.connect(dbpath) as conn:
 conn.execute(sql)

 

sqlite3.connect()でデータベースに接続し、オブジェクト生成。SQL文を実行するため、execute()メソッドを使用。

 

SQL文ではデータベースを作成するために三連引用符を使用。

CREATE TABLE IF NOT EXISTS person(カラム名 データ型, カラム名 データ型...)

CREATE TABLE(テーブル作成) IF NOT EXISTS(オプション、データベースに同じ名前のテーブルがないときだけテーブルを作成する制約) person(テーブル名)。

続く()内ではカラム名とデータ型を指定している。

id(カラム名)  INTEGER(データ型) PRIMARY KEY(制約、重複した値の保存を禁止する)

height(カラム名) NUMBER

weight(カラム名) NUMBER

typeNo(カラム名) INTEGER

INTEGERとNUMBERはどちらも整数型を表すそうだが明確な違いはわからなかった。

 

次に新規に身長と体重、体型を100件追加するプログラムを実行してデータベースにデータを格納していく。

f:id:hanamichi_sukusuku:20210222194619p:plain

実行結果

f:id:hanamichi_sukusuku:20210222194641p:plain

100件の身長、体重、体型のデータがデータベースに格納される。

 

データベースに接続し、関数呼び出し

with sqlite3.connect(dbpath) as conn:
# データを100件挿入 --- (*4)
 for i in range(100):
  insert_db(conn)
# トータルで挿入した行数を調べる --- (*5)
 c = conn.execute('SELECT count(*) FROM person')
 cnt = c.fetchone()
 print(cnt[0])

sqlite3.connect()で引数にデータベースファイルのパスを指定しデータベースに接続。

for文で100回insert_db()関数を実行。insert_db()関数では身長、体重、体型のデータをデータベースに格納し、内容を出力する。

c = conn.execute('SELECT cout(*) FROM preson')ではpersonテーブルのレコードの総数を取得している。SELECT文はデータベースからデータを取り出す時に使う。count(*)で全てのレコードの総数を取得。FROM句でテーブル指定。返り値はsqlite3.Cursorオブジェクト。

c.fetchone()でcorsorオブジェクトの1行目をタプルで取得する。

 

insert_db()関数、データベースにデータを格納

f:id:hanamichi_sukusuku:20210222200915p:plain

random.randint(130, 180)で130~180までの整数をランダムに変数heightに格納。

weightも同様。

体型はbmiによって値を変更。BMIは体重(kg) / (身長(cm)/100)の2乗 。

bmi = weight / (height / 100) ** 2

**を使用することで身長/100を2乗している。

if文でBMIの値によってtype_no(体型)に格納する値を変更。

 

sql = '''
INSERT INTO person (height, weight, typeNo)
VALUES (?,?,?)
'''
values = (height,weight, type_no)
print(values)
conn.executemany(sql,[values])

conn.executemany()で一度に複数のデータを保存する。タプルをリストのを扱うので第二引数で[]で囲っている。for文を回すように一つの指令(第一引数のSQL文)を複数のデータに実行する(第二引数)。

第一引数のSQL文のINSERTではデータベースにデータを保存したい時にはINSERT文を使う。

書式

INSERT INTO テーブル名(カラム名, カラム名..)

VALUES(値, 値,..)

 

VALUES(?,?)のようにすることでexecutemany()の第二引数で指定したリストが順番に?の部分に格納され、それぞれのカラムに保存される。

 

 

 

 

 

 

 

 

webサービスにおける学習データについて

webサービスにおける学習データについて

現在では多くのサービスで機械学習システムを導入しているが、そのシステムが活躍する上で一番難しいところが「一度作って終わりではない」という点にある。日々、新しい投稿が行われ、分類精度が低下してしまうからである。流行語や新製品など、正解データは常に変化していく。そのため定期的に教師データを新しくして、学習をやり直す必要がある。webサービスでは投稿された内容をデータベースに保存する。そのデータベースに蓄積されたデータを用いて、機械学習システムを構築する。それにより、機械学習システムがwebサービスでユーザーの投稿を支援する。そして、それにより、ユーザーがさらに多くの投稿をし、それらがデータベースに蓄積されていく。そうした、蓄積されたデータを機械学習に応用すると高い精度で投稿を支援できる。

 

多くのwebサービスでは、「webサービス」→「データベース」→「機械学習システム」→「webサービス」→「データベース」....このようなフローが自動的に行われるような構築するのが効果的。何かしらのきっかけで急に判定精度が悪くなる可能性もあるので、精度が落ちたら通知が来るようにするなど、自動化の落とし穴を避ける仕組みも組み込むと良い。

 

まとめ

・学習済みデータを保存しておいて、業務システムから読み込んで使うと効率がいい

・Flaskを利用して、Pythonを利用した機械学習システムを持つWebサーバーを作成できる

機械学習システムの入出力をWeb APIとして利用できるようにしておくと便利

APIを呼び出すwebアプリの作成

f:id:hanamichi_sukusuku:20210220171930p:plain

 

index.html

f:id:hanamichi_sukusuku:20210220173647p:plain

実行結果

f:id:hanamichi_sukusuku:20210220172030p:plain

今回のプログラムでは

hanamichi-sukusuku.hatenablog.com

 

上記で作成した機械学習の機能を持つwebサーバーを使用して、APIを呼び出すwebアプリを作成していく。

webサーバーでは既にURLで渡されたパラメーターを機械学習にかけるようになっている。

今回必要なのはhtml/css, javascriptを用いたindex.htmlファイルを作成する。

 

入力欄とボタンの作成

<DOCTYPE html>
<html><meta charset="utf-8"><body>
<h1>テキストのジャンル判定</h1>
<div>
<textarea id="q" rows="10" cols="60"></textarea>
<br><button id="qButton">判定</button>
<div id="result"></div>
</div>

 

判定ボタンがクリックされたときの処理(JS)

<script>
const qs = (q) => document.querySelector(q)
window.onload = () => {
const q = qs('#q')
const qButton = qs('#qButton')
const result = qs('#result')
// 判定ボタンを押した時 --- (*1)
qButton.onclick = () => {
result.innerHTML = "..."
// APIサーバに送信するURLを構築 --- (*2)
const api = "/api?q=" +
encodeURIComponent(q.value)
// APIにアクセス --- (*3)
fetch(api).then*1で受け取りjson形式で次のthen()の処理に返り値を渡している。then()メソッドでは複数の処理を繋ぎ、順番に実行していく。

then((data)=>...ではhtmlで結果を出力するdiv要素にラベルデータと確率のデータを文字列で追加している。

 

 

 

*1:res) => {

return res.json() // JSONで返す
}).then((data) => {
// 結果を画面に表示 --- (*4)
result.innerHTML =
data["label"] +
"<span style='font-size:0.5em'>(" +
data["per"] + ")</span>"
})
}
}
</script>

const qsで無名関数を定義。引数に指定したidの要素を取得する。

qButton.onclickで判定ボタンが押されたときの処理を記述。

result.innerHTMLでAPIサーバーから出力が返ってくるまで"..."と表示する。

const apiで変数apiAPIサーバーに送信するURLを構築。encodeURIComponent()では引数に指定した文字列をURLで使用できる形式に変換(エンコード)するためのもの。URLでは日本語は使用できないのでパラメーターとして送る文字列をこれによって変換している。

fetch()メソッドでは非同期で引数に指定したURLにHTTPリクエストを送信している(デフォルトはGET)。responseオブジェクトをfetch(api)からthen((response