のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

WebDNNのサンプルとコードの解説に挑戦

Sponsored Links

皆さんこんにちは。
お元気ですか。師走の12月らしく締め切りに追われています。

この記事は「Deep Learning フレームワークざっくり紹介 Advent Calendar 2017」第7日目です。

qiita.com

DeepLearningフレームワークの1つ、WebDNNのご紹介をします。
このフレームワークを調べたきっかけとして、「MakeGirls.moe」があります。
MakeGirls.moeは萌画像生成のWebサービスで、WebDNNを使用して提供しているようです。

make.girls.moe

WebDNN

WebDNNは東京大学 原田・牛久研究室が作成した深層学習モデル(DNN)を
ウェブブラウザ上で高速実行するためのオープンソースフレームワークです。

次のサイトのアワードに掲載されるほど、良いソフトウェアでもあります。

Open source software Award 2017 for WebDNN at ACM Multimedia 2017

DNNは顕著な成果を様々なタスクであげていますが、度々、計算コストが問題になります。
サーバ上でDNNの全ての計算を行う場合、様々なユーザが送信するリクエストは多数あります。
これを処理しきるにはそれなりのスペックのマシン及び台数が求められます。(多分、個人では無理)

WebDNNはウェブブラウザ上での実行を前提とし、様々な計算の最適化がされているようです。

github.com

WebDNNでWebサイトを構築するまでの手順

WebDNNはいくつかの有名なフレームワークに対応しています。
公式で確認ができているのはChainer, Kerasでしょうか。

KerasからWebDNN用のオブジェクトに変換してそれを利用し、Webサイトで動かすまでやってみます。

KerasからWebDNNへ変換する

まずは、Pretrainingで有名なResNet50を取得します。
KerasからResNet50を取得し、まずはディスクに保存します。
下記の実行は「example/resnet/」配下で行ってください。
(短いのでインタプリタでの実行で良いと思います)

from keras.applications import resnet50
model = resnet50.ResNet50(include_top=True, weights='imagenet')
model.save("resnet50.h5")

次にWebDNNで実行するモデル形式へ変換します。実行コマンドは次の通りです。

python ../../bin/convert_keras.py resnet50.h5 --input_shape '(1,224,224,3)' --out output_keras

Kerasを使い、Web上で実行する。

さて、ここからモデルを画面上で使います。
サンプルのサーバを立ち上げてみます。
サーバを起動します。setup.pyと同じ箇所で次のコマンドを実行します。

python -m http.server

実行後に次のURLにアクセスします。
(README.mdと異なることに注意、パスはexample配下をチェックして適切なパスを使いましょう)

http://localhost:8000/example/resnet/

アクセス後に次の画面が出てきます。
この画面でRunボタンを押すことで、WebDNNで変換されたモデルの実行が可能です。

f:id:tereka:20171203002625p:plain

さて、ここから先はWebDNNのjavascriptを見てみます。

ソースコード

Python(bin/convert_keras.py)

Kerasによる変換の解説になります。
まずは、KerasConverterを使います。このKerasConverterにより、
入力したモデルをWebDNN IR Graphへと変換します。

そして、この変換したWebDNN IR Graphとバックエンド(webgpu,webgl,webassembly,fallback)
とencodingを入力して与えると変換されたデータの出力を行います。
WebDNNのREADME曰く「--encoding eightbit」と引数に与えるとモデルが1/5サイズになるとのこと。

    backends = args.backend.split(",")
    for i, backend in enumerate(backends):
        console.stderr(f"[{path.basename(__file__)}] BackendName: {console.colorize(backend, console.Color.Cyan)}")
        try:
            graph_exec_data = generate_descriptor(backend, graph, constant_encoder_name=args.encoding)
            graph_exec_data.save(output_dir)
        except Exception as ex:
            if flags.DEBUG:
                raise ex

Javascript

ヘッダー

まずは、index.htmlを確認します。
ヘッダーとしてインポートされているスクリプトで必要なのは、
「../../dist/webdnn.js」と「script.js」です。
独自で開発する場合には「../../dist/webdnn.js」をインポートする必要があります。

<head>
    <title>ResNet-50 conversion WebDNN example</title>
    <meta charset="utf-8">
    <script src="../../lib/inflate.min.js"></script>
    <script src="../../dist/webdnn.js"></script>
    <script src="../data/imagenet_labels.js"></script>
    <script src="script.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
</head>
scrpit.js

まず、実行時には次のコードが実行されます。
ここではモデルと画像を読み込み計算結果を返します。

prepare_runでモデルをファイルから読み込むような挙動をしています。
次にrunner#getInputViewsで計算する画像をセットします。

最後にrunner.run();で予測をする計算を行い、getOutputViewsで計算結果を取り出しています。
Chainerとその他でOrderを変化させているのは、
フレームワークによってCHW(Channel, Height,Width)とHWCの順番が異なることが理由です。

async function run() {
    let runner = await prepare_run();

    runner.getInputViews()[0].set(await WebDNN.Image.getImageArray(document.getElementById('input_image'), {
        order: getFrameworkName() === 'chainer' ? WebDNN.Image.Order.CHW : WebDNN.Image.Order.HWC,
        color: WebDNN.Image.Color.BGR,
        bias: [123.68, 116.779, 103.939]
    }));

    let start = performance.now();
    await runner.run();
    let elapsed_time = performance.now() - start;

    let out_vec = runner.getOutputViews()[0].toActual();
    let top_labels = WebDNN.Math.argmax(out_vec, 5);

    let predicted_str = 'Predicted:';
    for (let j = 0; j < top_labels.length; j++) {
        predicted_str += ` ${top_labels[j]}(${imagenet_labels[top_labels[j]]})`;
    }
    log(predicted_str);

    console.log('output vector: ', out_vec);
    log(`Total Elapsed Time[ms/image]: ${elapsed_time.toFixed(2)}`);
}

prepare_runでは、モデルをロードします。
backendとframeworkの名前で識別できるので、その名前を指定して読み込みます。
下記のコードでは非同期にモデルを読み込み、
既にモデルが存在する場合にはその読み込まれているモデルを返します。

async function prepare_run() {
    let backend_name = document.querySelector('input[name=backend]:checked').value;
    let framework_name = getFrameworkName();
    let backend_key = backend_name + framework_name;
    if (!(backend_key in runners)) {
        log('Initializing and loading model');
        let runner = await WebDNN.load(`./output_${framework_name}`, {backendOrder: backend_name});
        log(`Loaded backend: ${runner.backendName}, model converted from ${framework_name}`);

        runners[backend_key] = runner;
    } else {
        log('Model is already loaded');
    }
    return runners[backend_key];
}

最後にloadImageを紹介します。getImageArrayを使うことで、画像をurl(path)から読み込み、
画像データとして保持します。これによりCanvasに画像を埋め込みます。

async function loadImage() {
    let imageData = await WebDNN.Image.getImageArray(document.getElementById("image_url").value, {dstW: 224, dstH: 224});
    WebDNN.Image.setImageArrayToCanvas(imageData, 224, 224, document.getElementById('input_image'));

    document.getElementById('run_button').disabled = false;
    log('Image loaded to canvas');
}

最後に

これを使うとWebページでjavascriptを使い、モデルの実行ができます。
サーバ上で動作させるよりもユーザに提供しやすいので非常におすすめです。
これを使えばユーザにモデルの提供がしやすくなり、遊びで作ったツールを試してもらうことができます。

いつか、提供しよう。そうしよう