
【第3回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみた【非同期処理編】
2021.12.20
第3回~ResponderとKerasで機械学習アプリケーションを作りたい!~
この記事は、「ResponderとKerasを使って機械学習Webアプリケーションを作ってみる」という連載記事になります。
連載終了後、「機械学習と相性が良いのでは!?」と思い立ち、「Responder」と「機械学習」を絡めた記事を書くことを決めました。
簡単なアプリケーションではありますが、Responderのさらに詳しい使い方が分かっていただける記事になるはずです!
まずは、第1回をお読みください
第3回: バックグラウンドで機械学習を行う
さて、今回は本アプリケーションの核を作っていきます。
「作成したネットワークで機械学習をバックグラウンドで行い、その間ユーザには待機ページを表示させておく」といった部分を作成します。
簡単そうに見えて、これが意外と厄介なのです。
ただ、Responderでは、このような非同期処理(バックグラウンドタスク)が簡単に書けるので、使ってみたいと思います。
前回の記事はこちら
LearnControllerを作る
それでは、前回、大枠だけ作った学習部のコントローラに加筆していきます。
前回までは、以下のようになっていました。
1 2 3 4 5 6 7 | class LearnController: async def on_get(self, req, resp, dataset): api.redirect(resp, '/404') return async def on_post(self, req, resp, dataset): pass # POST処理 |
このPOST処理部分に加筆していきます。
まずは、ざっくりと「こんな関数があったらいいな」的な発想でコードを書きます。
この考え方は意外と重要です(笑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | async def on_post(self, req, resp, dataset): # 任意のIDを設定する uid = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M%S') uid += str(uuid.uuid4()) data = await req.media() # [あとで実装] 学習データセットを取得 train_data = get_data(dataset) # [あとで実装] バックグラウンドで学習させる learn_model(data, train_data, uid) # 学習終了待機ページへリダイレクト api.redirect(resp, '/learning/' + uid) |
ここで任意のIDを生成する理由は、同時にこのアプリケーションにアクセスした時に、結果が競合しないようにするための簡単な回避です。
最初に、以下をインポートする必要がありますので加筆してください。
1 2 | import uuid import datetime |
あったらいいな関数①: get_data()
では、1つ目の「あったらいいな関数」を実装します。
この関数では、指定されたデータセットに対して、「訓練データ」「訓練ラベル」「検証データ」「検証ラベル」の4つを、辞書型変数として返してくれることを期待しています。
今回用いるデータセットは、以下を取得しますが、若干形式が異なるため、この関数内でうまいこと同じような形式に変換します。
Keras | ・MNIST |
scikit-learn | ・Iris ・Wine |
また、「Iris」と「Wine」については、「訓練データ」と「検証データ」が元から分けられていません。
そのため、今回は簡単に全データのうち120個を「訓練データ」、それ以外を「検証データ」とすることにします。
実装
まず、以下を controller.py の最初に加筆します。
1 2 3 | from keras.datasets import mnist from sklearn.datasets import load_iris, load_wine from sklearn.preprocessing import scale |
では、関数 get_data()を作っていきましょう!
記述する場所は、 controller.py のどこでも良いですが、無難に一番下に加筆していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | def get_data(dataset): train_data = dict() if dataset == 'mnist': (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.reshape(60000, 784) # 2次元配列を1次元に変換 x_test = x_test.reshape(10000, 784) x_train = x_train.astype('float32') # int型をfloat32型に変換 x_test = x_test.astype('float32') x_train /= 255 # [0-255]の値を[0.0-1.0]に変換 x_test /= 255 train_data['x_train'] = x_train train_data['x_test'] = x_test # One-hot ベクタに変換 y_train = keras.utils.to_categorical(y_train, 10) y_test = keras.utils.to_categorical(y_test, 10) train_data['y_train'] = y_train train_data['y_test'] = y_test elif dataset == 'iris': iris = load_iris() all_data = scale(iris['data']) # 平均0 分散1 で標準化 target = iris['target'] # データを対応関係を保ったままシャッフル data_size = len(all_data) p = np.random.permutation(data_size) all_data = all_data[p] target = target[p] target = keras.utils.np_utils.to_categorical(target) # to one-hot # 訓練データと検証データに分割する train_data['x_train'] = all_data[:120] train_data['x_test'] = all_data[120:] train_data['y_train'] = target[:120] train_data['y_test'] = target[120:] else: wine = load_wine() all_data = scale(wine['data']) # 平均0 分散1 で標準化 target = wine['target'] # データを対応関係を保ったままシャッフル data_size = len(all_data) p = np.random.permutation(data_size) all_data = all_data[p] target = target[p] target = keras.utils.np_utils.to_categorical(target) # to one-hot # 訓練データと検証データに分割する train_data['x_train'] = all_data[:120] train_data['x_test'] = all_data[120:] train_data['y_train'] = target[:120] train_data['y_test'] = target[120:] return train_data |
少し長々としたコードですが、実は、大したことはやっていません。
注意としては、「Iris」と「Wine」ではデータ値にばらつきがあるので、平均と分散を揃える標準化を行なっています。
これをしないと、学習が困難になってしまうからです。
機械学習を行う: learn_model()
次の「あったらいいな関数」を実装しましょう。
この関数では、「モデルを作成およびモデルを学習させた後、結果を描画する」までを担います。
簡単に言えば「超重い関数」です。
もし、普通に実装すると、ユーザは学習が終わるまでずっと何もページが表示されないままの読み込み状態に遭遇してしまいます。
こんなアプリは誰も使いたがらない上に、下手すればサーバからの応答時間が長すぎてエラーを吐かれてしまう可能性があります(笑)
非同期処理・バックグラウンドタスク
そこで普通は、マルチスレッドにして並列処理にしたりしますが、Responderでは、それが簡単に実装可能です。
公式でも、それを推しているようですので使ってみましょう!
なんと、Responderでは、バックグラウンドで処理したい関数に @api.background.task というデコレータを付けるだけです。
では、早速、関数を作っていきましょう!
必要なモジュールのインポート
まずは、モデル作成に必要なモジュールをインポートします。
インポート場所は、controller.py の最初です。
1 2 3 | import keras from keras.models import Sequential from keras.layers import Dense |
今回は超シンプルなネットワークですので、これだけでOKです。
関数を実装
実装例は、以下のとおりです。
各処理にはコメントがあるので、ひとつひとつ丁寧に追ってみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | @api.background.task def learn_model(data, all_data, uid): fc = None if 'fc[]' in data: # 中間層がPOSTデータにあるならば fc = data.get_list('fc[]') # モデル構築 model = Sequential() if fc is None: # 中間層がないならば、シンプルな2層ネットワーク model.add(Dense(int(data['output']), activation='softmax', input_shape=(int(data['input']),))) else: # 中間層がある is_first = True for _fc in fc: if is_first: model.add(Dense(int(_fc), activation='sigmoid', input_shape=(int(data['input']),))) is_first = False else: model.add(Dense(int(_fc), activation='sigmoid')) model.add(Dense(int(data['output']), activation='softmax')) # モデル作成おわり # コンソールにネットワーク情報を表示させたい場合 # model.summary() # 学習 model.compile(loss='categorical_crossentropy', optimizer=keras.optimizers.SGD( # 学習率の最適化部分 lr=float(data['eta']), # 初期学習率 decay=float(data['decay']), # 学習率減衰 momentum=float(data['momentum']), # 慣性項 ), metrics=['accuracy']) # 学習結果 history = model.fit(all_data['x_train'], all_data['y_train'], # 画像とラベルデータ epochs=int(data['epoch']), # エポック数の指定 verbose=1, # ログ出力の指定. 0だとログが出ない validation_data=(all_data['x_test'], all_data['y_test'])) acc = history.history['acc'] loss = history.history['loss'] val_acc = history.history['val_acc'] val_loss = history.history['val_loss'] # モデルを破棄 keras.backend.clear_session() # 結果をグローバル変数として保持 global result result = [acc, loss, val_acc, val_loss] # ここからグラフ描画部分 plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.plot(acc, label='acc', color='b') plt.plot(val_acc, label='val_acc', color='g') plt.xlabel('epoch') plt.ylabel('accuracy') plt.ylim(0, 1) plt.legend() plt.subplot(1, 2, 2) plt.plot(loss, label='loss', color='b', ls='--') plt.plot(val_loss, label='val_loss', color='g', ls='--') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.savefig('static/images/' + uid + '_history.svg', dpi=300) plt.close() |
注意する部分
注意する部分はいくつかありますが、簡単に説明を入れておきます。
特に2つ目は、筆者も少し悩まされました…。
Responder では、POSTで得た配列データは get_list() でリストとして取得できる
バックエンドで動いている TensorFlow の性質上、 keras.backend.clear_session() で TensorFlow のデータフローグラフを削除しておかないと、他データセットを使った学習を続けて行えない
global result で明示的に大域変数にしておくことで、他クラスからもアクセスが可能 (親クラスでまとめてメンバ変数にしてしまうのもアリ)
学習中のビューを仮作成
それでは、リダイレクト先のビューを作って動作確認をしましょう。
リダイレクト先のURLは、 /learning/{uid} としましたので、ルートを追加します。
1 2 | # urls.py api.add_route('/learning/{uid}', LearningController) |
コントローラ
次に、コントローラを実装します。
まだ仮ですので、以下のような感じで構いません。
1 2 3 4 5 | class LearningController: async def on_get(self, req, resp, uid): title = 'ネットワークを学習中...' resp.html = api.template('learning.html', title=title, uid=uid) |
ビュー
ビューは、以下のようにしてみました。
ザ・シンプルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {% extends "layout.html" %} {% block content %} <br> <h1>{{ title }}</h1> <hr> {% autoescape false %} {{ 'データセットを選択' | badge }} {{ 'ネットワークを作成' | badge }} {{ '学習' | badge_active }} {{ '結果' | badge }} {% endautoescape %} <br> <br> <p><span class="text-secondary">ID: {{uid}}</span></p> <p>ネットワークを学習中...</p> <p>学習が終了したら自動的にリダイレクトされるのでこのままお待ちください。</p> {% endblock %} |

ちなみに、まだ学習終了のリダイレクト処理は実装していないので、待っても何も起こりません。
試しに動かしてみる
それでは、実際に動かしてみるとします!

データを選択して、ネットワークを作成します。
次に、「このネットワークで学習を行う」をクリックしてみてください。

学習中ページにリダイレクトされるかと思います。
そんな中、コンソール上ではKerasの機械学習処理が実行されています。

上記図のようになっていたら、バックグラウンド処理がうまく実行できています!
第4回へつづく!
今回は、アプリケーションの核となる部分の作成をしました。
Responderでの、バックグラウンド処理についてもご理解いただけたと思います。
次回は、「学習終了後の動作」について実装していこうかと思います!
(これがまた重要です!)
今のままでは学習終了しても、ユーザには「学習中…」のままなので(笑)
なんと、早くも次回で最終回!
お楽しみに!
こちらの記事もオススメ!
Kerasのオススメ本
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来