pit-rayの備忘録

知識のあうとぷっと

WebDNNでブラウザで動くGANを実装した話

f:id:pit-ray:20190903182402p:plain

本日はQitaにありそうなタイトルを付けましたが、近年研究が盛んなGAN(Generative Adversarial Networks)をWebアプリにしてみようというだけです。今回は、完全に巨人の肩に乗っておりまして、先人たちの知恵を大いにお借りしています。こりゃ楽チン。

では、目次。



GANとは

Generative Adversarial Networksは、GANやGANsと略され、日本語では敵対的生成ネットワークといいます。このアルゴリズムは、Goodfellow氏の論文[1]にて発表されたものです。名前の通り、敵対するネットワークを用いて学習を進めます。具体的に言うと、生成ネットワーク(Generator)と識別ネットワーク(Discriminator)です。

Goodfellow氏は、これらのネットワークを偽札における偽造者と警察の攻防になぞらえて次のように説明しています。(GANに関係する論文を読んだ人ならば、腐るほど聞いているハズです)
偽造者(生成ネットワーク)は、本物にそっくりな偽札を作り出そうとします。それに対し、警察(識別ネットワーク)はある紙幣を調べ、本物か偽物か判別しようとします。

これが意味するのは、学習後に得られるのは、データセットに似たデータを生成できるネットワークと、データセットを分類できるネットワークということになります。今回は、MNISTデータセットを生成してくれるようなアプリケーションを想定していますので、前者が得られることが目標となります。

それでは、それぞれのネットワークの概略図を見ていきましょう。

f:id:pit-ray:20190904224739j:plain
GANのネットワーク構成

Generatorはガウス分布から生成した乱数を種に、偽の画像を生成します。Discriminatorは従来の分類器のような構造で、読んだ情報を本物か偽物か二値分類します。

近年は、GeneratorやDiscriminatorの中身や構成を変化させ、様々な派生GANが登場していますが、基本的なアルゴリズムは変わりません。今回は、CNNを用いたDCGAN(Deep Convolutional GAN [2])を用います。また、指定できたほうがおもしろいので、Conditional GAN [3]で組みました。
当初はWGAN-gpで組んでいましたが(特に、記事を書いているときに学習をさせていた)、集束が非常に遅く、生成結果もクオリティが低いことから、記事を大きく変更するハメになりました... orz

今回のソースはGithubにて確認できます。Chainer 5.3.0で実装しました。

github.com

WebDNNとは

日本の研究の最先端ともいえる、東京大学大学院 原田研究室にて開発されたフレームワークです。このフレームワークを用いることで、Deep Neural Network(DNN)をWebで高速に実行できるような形に変換できます。

従来、DNNを用いたWebサービスを提供するには、大規模な計算機を用意する必要があり、現実的ではありませんでした。また、クライアント側でその処理を行うアイデアもありましたが、処理速度が非常に遅いという欠点がありました。

WebDNNは次の二点を行うことで、クライアント側で高速に計算を行います。

計算量の削減 2×3を6と置き換えるような変換ルールを多数用いる
Web規格の利用 WebGPU、WebAssemblyなどを用いる


実行速度や対応ブラウザなどは、以下のページで確認できます。
mil-tokyo.github.io

今回は、WebAssemblyをメインに実装します。

WebDNNでConditional DCGANを実装

WebDNNは、DNNモデルをPythonで組み、表面的な実装はJavaScriptで行います。必要なものは、学習済みのパラメータファイル(Chainerではsnapshotに相当)とそれに対応するネットワークです。ここで、パラメータファイルとの整合性をとるのが非常に面倒であるため、学習時のネットワーク構成を変更しないようにします。

WebDNNのセットアップは、MIL WebDNN — MIL WebDNN 1.2.6 documentationを見ればスグ分かると思います。

Python側の実装

webdnn-mnist-dcgan/2pack.py at master · pit-ray/webdnn-mnist-dcgan · GitHub
2pack.py

#coding: utf-8
import numpy as np
import chainer

from webdnn.frontend.chainer import ChainerConverter
from webdnn.backend import generate_descriptor

from networks import Generator

def main():
    z_dim = 100
    device = -1 #CPU
    batch_size = 1
    model = Generator(z_dim)

    model.to_gpu()
    chainer.serializers.load_npz('result-dcgan/gen_snapshot_epoch-200.npz', model)
    model.to_cpu()

    x, _ = model.generate_noise(device, batch_size)
    y = model(x)

    graph = ChainerConverter().convert([x], [y])
    exec_info = generate_descriptor("webassembly", graph)
    exec_info.save("./model")

if __name__ == '__main__':
    main()

WebDNNでは、計算グラフを用いて最適化を行います。Chainerにおいては一度実行しないと計算グラフが生成されないため、仮の入力データを用意しています。

ここで注意したいのは、cupyをnumpyに統一する必要があるということです。WebDNNは内部でnumpyを想定してコーディングされています。したがって、学習時にGPUを使っている場合は、to_gpuメソッドでcupyに統一したうえで読み込み、to_cpuでパラメータ類もnumpyに統一します。

また、Generatorのネットワークが非常に大きい場合、WebDNNで生成される最適化済みファイルの容量が膨れ上がります。この場合、レンタルサーバーなどで一度にアップロードできず、一筋縄ではいきません。対策としては、ある程度の箇所でネットワークを切断し、分割するか、パラメータを少なくするような実装が考えられます。

JavaScript側の実装

webdnn-mnist-dcgan/app.js at master · pit-ray/webdnn-mnist-dcgan · GitHub
app.js

var webdnn = require('webdnn');
var nj = require('numjs');
var runner = null;

async function predict(num) {
    runner = await webdnn.load('model');

    let x = runner.inputs[0];
    let y = runner.outputs[0];

    //generate random normal distribution (z-noise)
    let array = nj.random(100);

    //conditional
    let label = nj.zeros(10);
    label.set(num, 1);
    init_x = nj.concatenate([array, label]);

    x.set(init_x.tolist());
    await runner.run();

    //draw
    var canvas = document.getElementById('output');
    webdnn.Image.setImageArrayToCanvas(y.toActual(), 28, 28, document.getElementById('output'), {
            dstW: canvas.getAttribute('width'),
            dstH: canvas.getAttribute('height'),
            scale: [255, 255, 255],
            bias: [0, 0, 0],
            color: webdnn.Image.Color.GREY,
            order: webdnn.Image.Order.CHW
        });

}

window.onload = function() {
    document.getElementById('button').onclick = function() {
        let num = document.getElementById('number').value;
        //console.log(num);
        predict(num);
    }
}

JavaScript側の実装では、npmなどでパッケージとしてWebDNNを読み込みます。今回は、WebPackで実装しました。

コードの解説は必要ないと思いますが、簡単に説明いたします。

まず、GANでは入力がノイズであるため、ガウス分布の乱数を生成できる数学ライブラリnumjsを利用しました。そこに、conditional GANとしてのラベルを与えています。

WebDNNで結果として画像を表示する際には、HTML側でcanvasを用意して、そのidに書き込むように指定します。その際のオプションとして、チャンネル、高さ、幅の順番や、RGBAかRGBかGray Scaleかの選択などが可能です。(ImageArrayOption | webdnn

また、入力データのセットには、setメソッドを用います。

実装結果

pit-ray.github.io

処理は、クライアント側で行っています。
スタイルシートすら作っていない手抜きです(;´д`)

まとめ

今回は、GANをWebDNNにてWebアプリに応用しました。WebDNNを使うことでソロの方も積極的にビジネスへと応用できるハズです。

ご質問等ありましたら、コメントいただけると幸いです。

参考文献

[1] Ian J. Goodfellow, Jean Pouget-Abadie, Mehdi Mirza, Bing Xu, David Warde-Farley, Sherjil Ozair, Aaron Courville, Yoshua Bengio. Generative Adversarial Networks. arXiv preprint arXiv:1406.2661, 2014

[2] Alec Radford, Luke Metz, Soumith Chintala. Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks. arXiv preprint arXiv:1511.06434, 2016

[3] 今さら聞けないGAN(6) Conditional GANの実装 - Qiita

DropConnectを理解したかった


はじめに

ディープラーニングを行う上で、過学習(overfitting)対策は欠かせません。実際にディープラーニングを行う際、データセットを訓練データ、検証データ、テストデータ等に分割するハズです。しかし、ある場合においてはモデルが訓練データに大きく依存したものになる可能性があります。その結果として訓練データの精度が非常に高くなり、検証データ・テストデータの精度が停滞してしまいます。以下、文献[1]からの引用です。

過学習が起きる原因として、主に次の2つが挙げられます 。

・パラメータを大量に持ち、表現力が高いモデルであること
・訓練データが少ないこと

出典:文献[1]

表現力はレイヤを多層化すればするほど高くなります。より性能が良いモデルを得ようとして多層化したはいいものの、学習が停滞してしまうのでは元も子もありません。過学習を抑制するための手法としては、Weight decayやDropoutなどがあります。また、Dropoutベースの正規化は、DropConnectや変分Dropoutなどがあります。
因みに、Weight decayとは、ネットワークにl2ペナルティを課すという単純な手法ですが、大抵よりよい結果が得られます。しかし、巨大なネットワークには向きません。


全結合ネットワーク(No-Drop)

f:id:pit-ray:20190629144906j:plain
Standard Neural Network

それぞれのノードは、簡単化してAffneレイヤ(Linearレイヤ)のように振舞い、何かしらの活性化関数を通るとします。(これが他であっても(例えばLSTMレイヤなど)入力と出力を考えれば同様に考えられます。ただし、全結合に限ります。)

前提として、今後扱っていく変数の概要を次に示します。

変数名 概要 形状
x 入力 N×1
W 重み N×D
u=Wx Affine(Linear)の結果(行列積) D×1
r=a(u) 活性化関数 a の出力 D×1

上に示す基本的な全結合レイヤは次のように表せます。

        r=a(u)=a(Wx)


それでは、この全結合のニューラルネットワークに対し、それぞれの手法を適用した場合を見ていきたいと思います。

Dropout

Dropoutは、G. Hinton先生らによって2012年に考案された正規化手法です。
意味としては、出力に対してDrop処理を行います。ここでいうDrop処理とは、データを確率的に削減することを指します。
グラフとして表すと下のようになります。

f:id:pit-ray:20190630005101j:plain
Dropout

ここで、削減されるデータの確率(割合)を p とすると、出力として残す確率は ( 1-p ) となります。
この確率にしたがって残す部分を1、無視する部分を0としたバイナリマスクを m とすると、Dropoutを導入した全結合層は次のように表されます。

        r=m*a(Wx)

ただし、* はアダマール積(Hadamard product)を表しています。アダマール積はシューア積(Schur product)や要素ごとの積(element-wise product)とも言われます。

上で述べたように、残したいものだけ残るような形になります。

シンプルなしくみでありつつ、これにて過学習に対して絶大なる効果を発揮します。

実装手法は様々なものが考案されていますので、ChainerやTensorFlowなどのソースコードを参考にするとよいでしょう。分かりやすい実装例を次に示します。(文献[1]p196参照)

import numpy as np

def dropout( y, dropout_ratio = 0.5 ):
    mask = np.random.rand( *y.shape ) > dropout_ratio
    r = mask * y
    return r

numpyのrandom.randは、0.0から1.0の一様なランダム行列を返します。
一様という点から、dropout_ratioと比較した結果はバイナリマスクを作ることと同等になります。
また、*y.shapeは、タプル自体を渡しています。仮にy.shapeのまま渡してしまうと、y.shapeを一つの要素として、( ( x, y, z ), )のように渡されてしまいます。



DropConnect

DropConnectは、Li Wan先生らが2013年に発表した手法であり、Dropoutの一般化したものであるとされています(文献[2]Abstract参照)。

Dropoutと同じようにバイナリマスクを用います。
DropConnectは、上で示した(下に再掲)、Dropoutを適用した全結合レイヤを変形することで求められます。

        r=m*a(Wx)

活性化関数  a によく用いられるものとしては、tanh、Relu、sigmoid等が挙げられます。
ここで、a(0)=0 を満たすものであれば、上の式を変形することができます。

        r=a(m*Wx)

次にバイナリマスクとして異なる形状のものを新たに用意します。

バイナリマスク 形状
m D×1
M N×D

すると次のように変形できます。

        r=a((M*W)x)

この式は、出力をDropするのではなく、重みをDropすることを表しています。
重みをDropするということは、なんぞ?となると思いますが、図に表すとスグ理解できるハズです。

f:id:pit-ray:20190630010842j:plain
DropConnect①

Dropしたものを赤で示しています。赤を取り除くと次のようになります。

f:id:pit-ray:20190630010858j:plain
DropConnect②

このように、出力よりもむしろ、接続を断つというのがDropConnectの手法なのです。
outが出力で、Connectが接続という観点からみても分かるでしょう。

簡単な実装では、重みと同じ形状のバイナリマスクを作り、アダマール積を行えばよいです。実装上、重みやバイアスを含むことから、文献[2]の2.2ではDropConnectをa sparsely connected layerと呼んでいます。レイヤとして一体化させたほうが扱いやすいかもしれません。
オリジナルの実装は、活性化関数を渡す前にガウス分布によるサンプリングを行います。
簡単な実装は、上で示したDropoutの例を応用すればよいです。

DropoutとDropConnectの比較

詳細な結果としての比較は、文献[2]のsection. 6をご覧ください。ここでは、簡単な比較と特徴を述べます。

ここで考えられる最も大きな違いは、バイナリマスクの表現力です。バイナリマスクを二値画像で表すと下のようになります。

f:id:pit-ray:20190630014301j:plain
binary mask

集合体恐怖症の方は大変申し訳ありません。雑いですが、元の論文を参考に作りました。
これをみると分かるように、DropConnectはより複雑な正規化を行うことができます。
ニューラルネットワークのデザインにも寄りますが、大抵の場合、Dropoutよりも良い結果になります。その反面、Dropoutよりも僅かに遅いという欠点があります。また、その実装のコストが高いこともデメリットとして挙げられます。仮にChainerにてLinearレイヤ(線形全結合レイヤ)にDropConnectを導入するとします。その場合、重みに対してDrop機構を備えた線形なレイヤに入れ替えるだけで済みます。Chainerでは SimplifiedDropconnectとして用意されています。これに対して、LSTMレイヤなどに適用するとなると自前でDropConnect機構を備えたLSTMレイヤを作らなくてはいけません。逆伝播(back propagation, backward)はDropのさせ方によって簡単化することも可能なので、実装次第ですが。このように元のレイヤやコストを考慮しなくてはいけません。



以上です。
どのような手段でも構いませんので、気づいた点やご質問等ありましたら、お気軽にお寄せください。



参考文献



[1] 斎藤 康毅 (2018) 『ゼロから作るDeep Learning -Pythonで学ぶディープラーニングの理論と実装』 株式会社オライリー・ジャパン.

[2] Li Wan, Matthew Zeiler, Sixin Zhang, Yann Le Cun, Rob Fergus "Regularization of Neural Networks using DropConnect" Proceedings of the 30th International Conference on Machine Learning, PMLR 28(3):1058-1066, 2013.