月: 2016年6月

tensorflowの学習結果をブラウザから使う

さて前回の続き。

tensorflowで学習が進んだのはわかった、さてある程度うまくいったあたりで、この学習結果で友達たちに見せて遊べるようにするには何があればいいか、ということを考えていた。どうしたらいいだろうか。

まず、tensorflowにはsaverというものがあり、学習結果などをファイルに書き出せ、そこから復帰もできる。これは便利で良いのだけれど、あとで利用するためだけにtensorflowをインストールしたサーバがどこかに必要になるけど、そんなものをメンテしたくない。もっとお気軽にいきたい。

tensorflowの学習結果マトリックスはevalするとnumpyのndarrayに変換できる。numpyにはnpyファイル形式というものがあり、ndarrayはこの形式で保存できる。まずはこれを保存するようにした。少なくともこうしておけばnumpyの入ったPython環境で利用ができるだろう。

ちょっと調べてみたらAppEngineからでもnumpyは使えそうなので(https://cloud.google.com/appengine/docs/python/tools/using-libraries-python-27)、まあ最悪それを使えばメンテコストはそれなりに低いものができるとは思う。あんまり面白くはないけれど。

もう一つ別な方法がある。ブラウザ内の処理環境に移植するという方法だ。


ブラウザ内というのはつまりはJavaScriptなのだけど、さすがにマトリックスの計算などをJavaScriptで書きたくない。性能も出ないだろう。そこでEmscriptenを使った。

EmscriptenはC/C++などのコンパイラツールチェインで、LLVMビットコードをasm.jsにコンパイルできる。asm.jsというのは……JSのサブセットで高速化などの最適化がしやすいもの。さらにwebassembly (wasm)にもコンパイルできるようだったが、そもそもwasmが手元のChromeではまだフラグなしでは使えないので今回は見送った。

さてEmscripten。Emscriptenにはem++というC++コンパイラがあり、バックエンドのclang++と連携してLLVMビットコードの生成やasm.jsの生成を行ってくれる。ようするに普通のc++/g++/clang++のところを置き換えてやれば.jsができるようだ。さらにはconfigureなどとうまく連携するためのものもあるようだが、今回は自分がいちから書くだけなので、適当にMakefileを書き、CXXを切り替えればコンパイルできるというやっつけ仕事にした。それにしても、イチからC++のコードを書くのも、白紙の状態からMakefile書くのも、けっこう久しぶりだな……。

行列計算については、C++ではEigenというライブラリが定番のようなのでこれを使った。tensorflowも裏ではEigenを使っているようだ。実装内容は……たんにtensorflowのBasicLSTMCellの実装を見ながら素直に移植した、とまあそれだけで、ここにはそれほどの苦労はない。

苦労はどちらかというとnpyファイルの扱いのほうがあった。npyファイル形式は公開されているが、C++のパーザなんてものはない。というか、検索したらcnpyというライブラリがあったのだけど、今回の自分の用途には向いていなかったようなので、利用しなかった。けっきょくやっつけパーザを自分で書いてEigenのマトリックスにした。

とりあえず一通りのものが動いたところで、手元の環境でネイティブバイナリを動かして、これが動くか確認する必要がある……これはまあ動いた。

とするとEmscriptenでコンパイルされた.jsも同じく動作するはずだ。はずだが……そのためにはnpyファイルをどうにかして生成したJSのコードに渡さないといけない。元はどうあれコンパイルされたJSファイルはふつうのJSと同じ実行環境でしかない。ただファイルを開けばいいというものではない。

外部ファイルはどうやってEmscriptenのC++に読ませたらいいか。これにはいくつか方法があるが、一番手軽なのはコンパイル時にデータファイルを指定する方式だ。

em++に –preload-file というコマンドラインフラグを渡すと、コンパイラはファイルをまとめたパッケージを作り、また生成されたJSファイルではメイン処理が動く前にまずこのパッケージデータを読み込む処理が走る(参考: http://kripken.github.io/emscripten-site/docs/porting/files/packaging_files.html#packaging-files)。そしてC++側でファイルを読み込むコードによってこのパッケージデータ上のデータが読めるようになる。これに必要なnpyをパッケージしておけば、C++側では単にファイルを読めばいいだけになってかなりお手軽だ。

が、試してみた感じではメモリ消費の問題があるようだった。マトリックスはさほど巨大なものではないはずなのだが、そうはいっても全部合わせればそれなりの規模ではあるし、こうしたコードにすることでファイルシステムのエミュレーションも必要になる。試しにやってみたらメモリ消費が急増してまったく動かなかったので、この方向性は諦めた。

ほかにもいろいろあったのだが、けっきょくお手軽なのは

  • JS側で必要なデータをfetchし、
  • その結果のArrayBufferをC++に渡す

ということのようだ。embindを使うとArrayBufferはC++のstd::stringとして入ってくるので、そこから先はnpyパーザを使えば完成、という次第。

ただしこれも、たかが数十MBのデータでもデフォルトでは大きすぎるということで呼び出しに失敗してしまう。これを何とかするにはリンク時に -s TOTAL_MEMORY= でそれなりの大きさのバッファを作る(今回は256MBとした)ことで解決した。これはまああまり賢くはないアプローチである気はする(たとえば、ほんとうはもう少し巨大なデータでも試した結果があったのだけど、そっちはこの方式だと破綻してしまったので試していない)。常識的に考えれば、fetchのストリーミングAPIでちょっとずつC++にデータを渡してやれば良いのだろうけれど。


にしてもEmscripten / asm.js、思ってたよりずっと高速に動作していてけっこうすごい。動作も当然なんの問題もない。いまどき、ブラウザのなかで行列計算したくなったらEmscriptenで一発なのだなあ。

良い時代になりましたね。web assemblyも楽しみになってきました。

ディープラーニングで英語を読ませる

概要

tensorflowの練習に、英単語(アルファベット文字列)を入れると、その読みとなるカタカナを出力するという機械学習課題をやってみて、まぁいちおう動いているなという程度のものができた。

学習結果をブラウザ内で実行できるよう移植して http://www.jmuk.org/en-ja/ に置いといた。

本文

tensorflowを勉強したのはいいのだけど、なんらかの練習課題をやってみたい、なにかいい課題はないだろうか、と思っていた。個人が趣味でやってみる課題では学習データの準備をどうするかが気になるところだけど、それも簡単にそろうやつがいい。データの手動のタグ付けをしない課題がいい。

などと考えているうちに「英単語を読ませてカタカナにする」という課題を思いついた。たとえば “google” という入力文字列から “グーグル” という文字列を出力させたい。そういう問題になる。

これを学習するには学習データとして大量の「英単語とカタカナ語のペア」が必要になるわけだけど、これにはウィキペディアのデータを使う。ウィキペディアのページはサイドバーに他言語版ウィキペディアの同項目のページへのリンクがありますよね。このデータを取ってくることで、同じ事物に対する英単語と日本語の対を取ってこれる。ウィキメディア財団のサイトはどれも定期的にデータをダンプして誰でも取得できるようになっているので、この関係性が格納されたwikidataのデータダンプをある日ダウンロードして項目の対を作った。

もちろんここには漢字の単語やかなり関係性の薄い単語も含まれているので、日本語はカタカナや中黒などのみを含むものとか、末尾のカッコはdisambiguationだから消すとか、まあいろんな適当なヒューリスティックスを入れて学習データを作る。ちなみに当初は項目同士のペアで学習させていたのだけど、なかなかうまく行かなかったので、このデータをもとに単語同士のペアをつくって学習させた。この辺もけっこういろいろ試行錯誤があるのだけど、つまんないと思うので略す。

さて、学習データはできた、ではどう学習させるか。

この課題というのは文字列を受け取って文字列を出力するというものなのだが、文字列の長さは学習ペアごとにけっこうばらばらなので(また文字数も一対一対応ではないので)、ふつうのフィードフォワードニューラルネットワークではうまく解くことができない。リカレントニューラルネットワークを使って解く、seq2seqというモデルを使う。というか、「英語の読みの推定」というのはseq2seqとしては非常に典型的な(であるがゆえに練習課題としてはけっこういい)タスクだと思う。

リカレントニューラルネットワーク(RNN)というのは、前回の出力結果(の一部)を次回の入力に加えるというタイプのニューラルネットワークの総称で、これによって内部状態のようなものが保持でき、時系列データとか、順番に入力がくるようなもの(文字列とか単語列とか)の処理によいとされている。

seq2seqでは典型的にはこのRNNセルをふたつ用意する。まず入力文字列(今回の場合は英語文字列)をいっぽうのRNNに入力していって状態遷移をさせ、遷移結果の状態をふたつ目のRNNに渡す。ふたつ目のRNNにはまず「ここから学習開始」を意味する特殊なシンボル(GO)を入れ、その出力結果を最初の文字とみなす。それ以降はひとつ前の出力結果の文字を入力とし、次の文字を出力していく。文字列の最後にはEOS (end of sentence) という特殊なシンボルが出力されると仮定して、そうなったら完了。

図としてはこんなかんじ。

ここでのABCというのがもとの入力列で、WXYZというのが出力列。この図ではRNNセルは単独だが、ABC側と<go>WXYZ側でセルを分けるのが良いとされている。

さて、実はtensorflowにはRNNやseq2seqのモデルが標準で提供されていてチュートリアルまである(この図もそこへのリンク)。というかさらにいうとtranslate.pyというコマンドがあって、これを使えば特に何もしなくても学習できちゃうかも(さすがにそれはないなと思ったので使わなかったけど)。ともあれこのチュートリアルとリンクされている論文は素直に良いと思うので、詳しくはそのへんを読めばよろしいことでしょう。

自分でいろいろ組んでみて試してみたものの、結果的にはたいしたことない構成に落ち着いたので、この程度なら既存のコードをそのまま動かすんでもいいんじゃないの、という気がするけれど、まあ自分で書いたからこそ、多層化させたりいろいろパラメータをいじったりいろいろ試してみたりというのが目的の練習課題なので、と自分に言い聞かせているところ。

ところでtensorflowにおけるRNNセルやseq2seqの実装というのはなかなか面白い構成になっている。

たとえばRNNセルを表現したクラス RNNCell やその実装である LSTMCell のようなクラスの実装を見ても、実際のマトリックスのデータ(tf.Variableなど)はインスタンス変数としては保持しない。状態数などの基本的なパラメータだけを持っている。そして実際にグラフ構造を作るときに変数の名前で名寄せしてデータを取り出すようになっている。

つまりRNNセルクラスのインスタンスたちは「どういう計算をする」という構造だけを定義していて、具体的に誰と計算をするかは必要なときに決めるわけだ。tensorflowは変数スコープというものを定義することができ、スコープを一致させれば同じマトリックスに対して計算ができるし、変えれば構造は同じだけど違う対象と計算をするようになる。同じような構成を何段も組むときにループを使ってうまく書ける、などのことが綺麗にかけたりするのだろうと思う。あとインスタンスの作成がgraphのスコープと無関係にできるってのもあるのかな。なんにしても変数スコープってそう使うんだなっていう感じはある。

distributed tensorflowがこの変数スコープとうまく適合しているとよく出来ているなあとさらに感心するところな気もするが、あんまりそうなっていない気がする。この辺はまだそこまで煮詰まっていないのかな。


話がずれた。

さて、学習結果をいろいろ遊んでみると、やっぱり相当変な部分がのこっている。日本人の名前がうまく読めない、とかはもうしょうがないとおもうんだけど、ごく普通の名前、たとえばJamesが読めないってのはどうなんだろうな。学習データの偏りじゃないかなと思っているけれども、救えたほうがよいよなーと思ったりもする。が、そういうのをなんとかするために試行錯誤するのも楽しいけれど時間かかるのでとりあえずもういいやこれで、と思うことにした。あきた、とも言う。

それでもなんというか、当初なかなかうまくいかないなーと思っていたものがあるときいきなりそこそこ読めるようになった時はけっこうびっくりしたし、なんというか、すごいなあと素直に思ったものだし、やっぱりそこそこ読めている結果になっていて面白い。なので、自分としてはまあいちおうこんなもんか、ってところまで行けたかなと思ってる次第。まあほんと大したことないんですが……。

記事も長くなったので、ブラウザ環境への移植の話はまた明日にでも。