• トップ
  • ブログ一覧
  • 【第3回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみた【非同期処理編】
  • 【第3回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみた【非同期処理編】

    メディアチームメディアチーム
    2019.11.14

    IT技術

    この記事は、「ResponderとKerasを使って機械学習Webアプリケーションを作ってみる」という連載記事になります。

    連載終了後、「機械学習と相性が良いのでは!?」と思い立ち、「Responder」と「機械学習」を絡めた記事を書くことを決めました。

    簡単なアプリケーションではありますが、Responderのさらに詳しい使い方が分かっていただける記事になるはずです!

    まずは、第1回をお読みください

    featureImg2019.10.29【第1回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみた【大枠作成編】第1回~ResponderとKerasで機械学習アプリケーションを作りたい!~今、人気に火が着きつつある Python...

    さて、今回は本アプリケーションの核を作っていきます。

    「作成したネットワークで機械学習をバックグラウンドで行い、その間ユーザには待機ページを表示させておく」といった部分を作成します。

    簡単そうに見えて、これが意外と厄介なのです。

    ただ、Responderでは、このような非同期処理(バックグラウンドタスク)が簡単に書けるので、使ってみたいと思います。

    前回の記事はこちら

    featureImg2019.11.05【第2回】ResponderとKerasを使って機械学習Webアプリケーションを作ってみた【ネットワーク作成編】第2回~ResponderとKerasで機械学習アプリケーションを作りたい!~この第2回記事は、「Responderと...

    LearnControllerを作る

    それでは、前回、大枠だけ作った学習部のコントローラに加筆していきます。

    前回までは、以下のようになっていました。

    1class LearnController:
    2    async def on_get(self, req, resp, dataset):
    3        api.redirect(resp, '/404')
    4        return
    5
    6    async def on_post(self, req, resp, dataset):
    7        pass  # POST処理

    このPOST処理部分に加筆していきます。

    まずは、ざっくりと「こんな関数があったらいいな」的な発想でコードを書きます

    この考え方は意外と重要です(笑)

    1async def on_post(self, req, resp, dataset):
    2    # 任意のIDを設定する
    3    uid = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M%S')
    4    uid += str(uuid.uuid4())
    5
    6    data = await req.media()
    7
    8    # [あとで実装] 学習データセットを取得
    9    train_data = get_data(dataset)
    10
    11    # [あとで実装] バックグラウンドで学習させる
    12    learn_model(data, train_data, uid)
    13
    14    # 学習終了待機ページへリダイレクト
    15    api.redirect(resp, '/learning/' + uid)

    ここで任意のIDを生成する理由は、同時にこのアプリケーションにアクセスした時に、結果が競合しないようにするための簡単な回避です。

    最初に、以下をインポートする必要がありますので加筆してください。

    1import uuid
    2import datetime

    あったらいいな関数①: get_data()

    では、1つ目の「あったらいいな関数」を実装します。

    この関数では、指定されたデータセットに対して、「訓練データ」「訓練ラベル」「検証データ」「検証ラベル」の4つを、辞書型変数として返してくれることを期待しています。

    今回用いるデータセットは、以下を取得しますが、若干形式が異なるため、この関数内でうまいこと同じような形式に変換します

    Keras・MNIST
    scikit-learn・Iris
    ・Wine

    また、「Iris」と「Wine」については、「訓練データ」と「検証データ」が元から分けられていません。

    そのため、今回は簡単に全データのうち120個を「訓練データ」、それ以外を「検証データ」とすることにします。

    実装

    まず、以下を controller.py の最初に加筆します。

    1from keras.datasets import mnist
    2from sklearn.datasets import load_iris, load_wine
    3from sklearn.preprocessing import scale

    では、関数get_data()を作っていきましょう!

    記述する場所は、 controller.py のどこでも良いですが、無難に一番下に加筆していきます。

    1def get_data(dataset):
    2    train_data = dict()
    3
    4    if dataset == 'mnist':
    5        (x_train, y_train), (x_test, y_test) = mnist.load_data()
    6        x_train = x_train.reshape(60000, 784)  # 2次元配列を1次元に変換
    7        x_test = x_test.reshape(10000, 784)
    8        x_train = x_train.astype('float32')  # int型をfloat32型に変換
    9        x_test = x_test.astype('float32')
    10        x_train /= 255  # [0-255]の値を[0.0-1.0]に変換
    11        x_test /= 255
    12
    13        train_data['x_train'] = x_train
    14        train_data['x_test'] = x_test
    15
    16        # One-hot ベクタに変換
    17        y_train = keras.utils.to_categorical(y_train, 10)
    18        y_test = keras.utils.to_categorical(y_test, 10)
    19        train_data['y_train'] = y_train
    20        train_data['y_test'] = y_test
    21
    22    elif dataset == 'iris':
    23        iris = load_iris()
    24        all_data = scale(iris['data'])  # 平均0 分散1 で標準化
    25        target = iris['target']
    26
    27        # データを対応関係を保ったままシャッフル
    28        data_size = len(all_data)
    29        p = np.random.permutation(data_size)
    30        all_data = all_data[p]
    31        target = target[p]
    32        target = keras.utils.np_utils.to_categorical(target)  # to one-hot
    33
    34        # 訓練データと検証データに分割する
    35        train_data['x_train'] = all_data[:120]
    36        train_data['x_test'] = all_data[120:]
    37        train_data['y_train'] = target[:120]
    38        train_data['y_test'] = target[120:]
    39
    40    else:
    41        wine = load_wine()
    42        all_data = scale(wine['data'])  # 平均0 分散1 で標準化
    43        target = wine['target']
    44
    45        # データを対応関係を保ったままシャッフル
    46        data_size = len(all_data)
    47        p = np.random.permutation(data_size)
    48        all_data = all_data[p]
    49        target = target[p]
    50        target = keras.utils.np_utils.to_categorical(target)  # to one-hot
    51
    52        # 訓練データと検証データに分割する
    53        train_data['x_train'] = all_data[:120]
    54        train_data['x_test'] = all_data[120:]
    55        train_data['y_train'] = target[:120]
    56        train_data['y_test'] = target[120:]
    57
    58    return train_data

    少し長々としたコードですが、実は、大したことはやっていません。

    注意としては、「Iris」と「Wine」ではデータ値にばらつきがあるので、平均と分散を揃える標準化を行なっています。

    これをしないと、学習が困難になってしまうからです。

    機械学習を行う: learn_model()

    次の「あったらいいな関数」を実装しましょう。

    この関数では、「モデルを作成およびモデルを学習させた後、結果を描画する」までを担います。

    簡単に言えば「超重い関数」です。

    もし、普通に実装すると、ユーザは学習が終わるまでずっと何もページが表示されないままの読み込み状態に遭遇してしまいます。

    こんなアプリは誰も使いたがらない上に、下手すればサーバからの応答時間が長すぎてエラーを吐かれてしまう可能性があります(笑)

    非同期処理・バックグラウンドタスク

    そこで普通は、マルチスレッドにして並列処理にしたりしますが、Responderでは、それが簡単に実装可能です。

    公式でも、それを推しているようですので使ってみましょう!

    なんと、Responderでは、バックグラウンドで処理したい関数に@api.background.task というデコレータを付けるだけです。

    では、早速、関数を作っていきましょう!

    必要なモジュールのインポート

    まずは、モデル作成に必要なモジュールをインポートします。

    インポート場所は、controller.py の最初です。

    1import keras
    2from keras.models import Sequential
    3from keras.layers import Dense

    今回は超シンプルなネットワークですので、これだけでOKです。

    関数を実装

    実装例は、以下のとおりです。

    各処理にはコメントがあるので、ひとつひとつ丁寧に追ってみてください。

    1@api.background.task
    2def learn_model(data, all_data, uid):
    3
    4    fc = None
    5    if 'fc[]' in data:  # 中間層がPOSTデータにあるならば
    6        fc = data.get_list('fc[]')
    7
    8    # モデル構築
    9    model = Sequential()
    10
    11    if fc is None:  # 中間層がないならば、シンプルな2層ネットワーク
    12        model.add(Dense(int(data['output']), activation='softmax', input_shape=(int(data['input']),)))
    13
    14    else:  # 中間層がある
    15        is_first = True
    16
    17        for _fc in fc:
    18            if is_first:
    19                model.add(Dense(int(_fc), activation='sigmoid', input_shape=(int(data['input']),)))
    20                is_first = False
    21            else:
    22                model.add(Dense(int(_fc), activation='sigmoid'))
    23
    24        model.add(Dense(int(data['output']), activation='softmax'))
    25
    26    # モデル作成おわり
    27    # コンソールにネットワーク情報を表示させたい場合
    28    # model.summary()
    29
    30    # 学習
    31    model.compile(loss='categorical_crossentropy',
    32                  optimizer=keras.optimizers.SGD(  # 学習率の最適化部分
    33                      lr=float(data['eta']),  # 初期学習率
    34                      decay=float(data['decay']),  # 学習率減衰
    35                      momentum=float(data['momentum']),  # 慣性項
    36                  ),
    37                  metrics=['accuracy'])
    38
    39    # 学習結果
    40    history = model.fit(all_data['x_train'], all_data['y_train'],  # 画像とラベルデータ
    41                        epochs=int(data['epoch']),  # エポック数の指定
    42                        verbose=1,  # ログ出力の指定. 0だとログが出ない
    43                        validation_data=(all_data['x_test'], all_data['y_test']))
    44
    45    acc = history.history['acc']
    46    loss = history.history['loss']
    47    val_acc = history.history['val_acc']
    48    val_loss = history.history['val_loss']
    49
    50    # モデルを破棄
    51    keras.backend.clear_session()
    52
    53    # 結果をグローバル変数として保持
    54    global result
    55    result = [acc, loss, val_acc, val_loss]
    56
    57    # ここからグラフ描画部分
    58    plt.figure(figsize=(10, 4))
    59
    60    plt.subplot(1, 2, 1)
    61    plt.plot(acc, label='acc', color='b')
    62    plt.plot(val_acc, label='val_acc', color='g')
    63    plt.xlabel('epoch')
    64    plt.ylabel('accuracy')
    65    plt.ylim(0, 1)
    66    plt.legend()
    67
    68    plt.subplot(1, 2, 2)
    69    plt.plot(loss, label='loss', color='b', ls='--')
    70    plt.plot(val_loss, label='val_loss', color='g', ls='--')
    71    plt.xlabel('epoch')
    72    plt.ylabel('loss')
    73    plt.legend()
    74
    75    plt.savefig('static/images/' + uid + '_history.svg', dpi=300)
    76    plt.close()

    注意する部分

    注意する部分はいくつかありますが、簡単に説明を入れておきます。

    特に2つ目は、筆者も少し悩まされました…。

    Responder では、POSTで得た配列データはget_list()  でリストとして取得できる

    バックエンドで動いている TensorFlow の性質上、keras.backend.clear_session()  で TensorFlow のデータフローグラフを削除しておかないと、他データセットを使った学習を続けて行えない

    global result  で明示的に大域変数にしておくことで、他クラスからもアクセスが可能 (親クラスでまとめてメンバ変数にしてしまうのもアリ)

    学習中のビューを仮作成

    それでは、リダイレクト先のビューを作って動作確認をしましょう。

    リダイレクト先のURLは、/learning/{uid} としましたので、ルートを追加します。

    1# urls.py
    2api.add_route('/learning/{uid}', LearningController)

    コントローラ

    次に、コントローラを実装します。

    まだ仮ですので、以下のような感じで構いません。

    1class LearningController:
    2    async def on_get(self, req, resp, uid):
    3
    4        title = 'ネットワークを学習中...'
    5        resp.html = api.template('learning.html', title=title, uid=uid)

    ビュー

    ビューは、以下のようにしてみました。

    ザ・シンプルです。

    1{% extends "layout.html" %}
    2
    3{% block content %}
    4<br>
    5<h1>{{ title }}</h1>
    6<hr>
    7{% autoescape false %}
    8{{ 'データセットを選択' | badge }}
    9{{ 'ネットワークを作成' | badge }}
    10{{ '学習' | badge_active }}
    11{{ '結果' | badge }}
    12{% endautoescape %}
    13
    14<br>
    15<br>
    16<p><span class="text-secondary">ID: {{uid}}</span></p>
    17<p>ネットワークを学習中...</p>
    18<p>学習が終了したら自動的にリダイレクトされるのでこのままお待ちください。</p>
    19
    20{% endblock %}

    ちなみに、まだ学習終了のリダイレクト処理は実装していないので、待っても何も起こりません。

    試しに動かしてみる

    それでは、実際に動かしてみるとします!

    データを選択して、ネットワークを作成します。

    次に、「このネットワークで学習を行う」をクリックしてみてください。

    学習中ページにリダイレクトされるかと思います。

    そんな中、コンソール上ではKerasの機械学習処理が実行されています。

    上記図のようになっていたら、バックグラウンド処理がうまく実行できています!

    第4回へつづく!

    今回は、アプリケーションの核となる部分の作成をしました。

    Responderでの、バックグラウンド処理についてもご理解いただけたと思います。

    次回は、「学習終了後の動作」について実装していこうかと思います!

    (これがまた重要です!)

    今のままでは学習終了しても、ユーザには「学習中…」のままなので(笑)

    なんと、早くも次回で最終回!

    お楽しみに!

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    Kerasのオススメ本

    直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
    直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ

     

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background