ワイドアパーチャ写真から 3D モデルを生成

VR180 という規格がある。端的に言うと、「180度の視野がある 3D (両眼立体視)」と解釈できるためのメタデータ作法。
QooCam と Insta360 EVO という VR180 カメラを手に入れて、身近な出来事いろいろを VR180 写真や動画で撮りまくっている。

f:id:n_shuyo:20190808185306j:plain
QooCam と Insta360 EVO (とオマケの THETA V)

で、使えば使うほど VR180 は過渡期の技術だなあと実感している。
今は VR/AR 向けの手軽な映像記録手段に VR180 と 360度カメラの他にないから、しばらく使い続けるけどね。

最大の問題は、視点が固定されてしまうので 6DoF VR や AR(MR含む) で価値を発揮できないこと。
もう一つの問題は、(少なくとも今ある全ての VR180 カメラでは)瞳孔間距離に相当する2眼のレンズの距離が固定で、近い距離(50cm以内)の被写体をうまく立体視できるように撮影しづらいこと。

ただでさえ立体視が意味のある距離は狭いのに(遠いと視差が発生しない)、近い方もうまく撮れないのはかなり痛い。手の届く範囲こそ立体視したいのに…。
ほとんどの VR180 カメラはおのおののギミックで 360度カメラに変形できるんだけど、撮り比べたらどうせ THETA の圧勝(スティッチ精度とかジャギー解消とか、拡大してみたらほぼ一瞬でわかる)なんで、そっちは360度専門のカメラに任せて、近距離撮影用にレンズ間距離を調整できる機能とかの方がよっぽど欲しい。後処理の都合で固定せざるをえないんだろうけど……。

これらの問題を根本的に解消するには、やはり 3D モデルとして撮影できるようになる必要がある。簡易でもいいので。
具体的には、深度センサを含むマルチカメラで撮影したらその場で glb などの可搬性が高くてテクスチャも包含する 3D モデルデータが生成される手法に VR180 は置き換えられると予想している(ちなみに 360度カメラは、空気遠近法が表現できるほど高解像度になる方向で生き残ると睨んでいる)。
その点 Huawei P30 Pro(3光学+1TOFカメラ)には大いに期待しまくってたんだけど、技術の場外戦で先行き不透明……。

と、ここまでが前置き。

iPhone 8 以降のインカメラや Huawei のここ数年の機種など、いくつかのスマホは深度情報を含んだ写真を撮影できる。
そしたらその深度情報から 3D モデルを生成できるよね! やってみた! という話がこの記事の本題。

f:id:n_shuyo:20190816201209j:plain:w400

対象は Huawei のワイドアパーチャ(撮影後に焦点距離を変更できる)で撮影した写真。
ワイドアパーチャ写真は、一見は最近流行りのポートレート写真(人物や対象を強調するため、それ以外をぼかした写真)に見えるが、先頭以外のフレームにパンフォーカス(手前から奥まで焦点があった)写真と深度情報が格納されていて、編集時には該当する深度以外をぼかすことで焦点距離の変更をエミュレートしている。
既存の Huawei スマホは TOF センサなど積んでおらず、深度の情報はマルチカメラによる僅かな視差(1cmくらい……)と機械学習で推定していると思われる。単色無地の壁とか奥行き均一(z方向に垂直な面)になっちゃうし。

深度情報の取り出しは次のブログやコードを参考に。

Huawei Mate 10 liteで撮った画像から深度情報(Depth of Field・Depth Map)を取得する方法 - Qiita
https://qiita.com/kotauchisunsun/items/d5c2b73ec3d76d159449
jpbarraca/dual-camera-edof
Extract the EDOF from photos obtained from Dual Camera smartphones:https://github.com/jpbarraca/dual-camera-edof

実際のコードがこちら。

撮影したワイドアパーチャ写真データ(Google Photos とか通すと必要なフレームやメタデータが落ちるんで、元のファイルをそのまま持ってこないとダメ)を edof.py に食わせると、model ディレクトリに obj と mtl とテクスチャ画像を出力する。
動作確認したのは手元にある nova lite 2 だけだが、深度情報取り出しロジックは上のブログの記事と同じなので、たぶん Mate シリーズや P20 とかで撮ったワイドアパーチャでも動く、はず。
深度情報は、nova lite 2 の場合、612x816 の unsigned char で格納されており、各値が元の写真と縮尺を合わせた場合の各領域の深度(0~255)を表している。
写真画像は重力の方向が下になるように勝手に回転してくれるが、深度情報はなぜか回転してくれないので、向きにあわせて自前で回転する必要がある。


出力された obj には法線ベクトルを計算するのをサボって付けていないため、Blender などで一度読み込んで法線ベクトルを付加しないと Unity や WebGL などで正常に表示されない。
Blender なら obj をインポートし、オブジェクトを選択(右クリック)、編集モードに移行(TABキー)、タブの「シェーディング/UV」を開いて、法線「再計算」ボタンをクリック、オブジェクトモードに戻って(TABキー)、obj なり好きな形式でエクスポートすれば OK。
裏表が逆になることもちょいちょいあるので、そのときは編集モードで「メッシュ>面>面を反転」して、エクスポートし直し。

奥行きは 0~255 の離散値なので、適当にスケールした程度で z 軸の値に使ったらガタガタのレゴブロックにようになってしまう。
そこで畳込みでぼかしを与えている*1。与え具合は --nconv と --range というオプションで指定できる。--range はもとの値からどれだけの移動を許可するか、--nconv はぼかし処理を何回行うかの指定で、思いっきりなめらかにしたければ --nconv 20 --range 10 くらいを指定しておけばいいだろう。
レゴブロックを見たかったら --nconv 0 で。


f:id:n_shuyo:20190816171617j:plain:w400

nova lite 2 で撮影したワイドアパーチャに対する作例をいくつか示してみる。
近距離の対象+背景(いわゆるポートレート写真)あたりだとそれなりなモデルを生成する。わずかな視差が多少有効かつ、奥行き推定モデルもそういうデータを一番たくさん食べてるんだろう。
f:id:n_shuyo:20190816201128j:plain:w300f:id:n_shuyo:20190816201149j:plain:w300

そこから離れるほど、まあ言葉飾らず言えば、悲惨になっていく。
近距離背景+オブジェクト(静物画みたいなやつ)はまだまし。
f:id:n_shuyo:20190816201110j:plain:w400f:id:n_shuyo:20190816201116j:plain:w300

中距離になるとかなりつらい。
このあたりを結果が書き割りのようになっているのを見ると、基本的には写真を領域に分割し、それぞれの奥行きを個別に推定しているんだろうなあという処理が見える。
壁がおかしいことになっているのは、単色無地で奥行き推定に失敗しているからだろう。
f:id:n_shuyo:20190816201047j:plain:w300f:id:n_shuyo:20190816201058j:plain:w300

奥行きのある廊下とかもダメ。
f:id:n_shuyo:20190816201014j:plain:w300f:id:n_shuyo:20190816201026j:plain:w300

遠距離もダメダメ。
f:id:n_shuyo:20190816200741j:plain:w300f:id:n_shuyo:20190816200759j:plain:w300


最後のはともかく、廊下くらいまでは TOF カメラを積めばもうちょいマシなものがでてくるんじゃないかと期待している。
P30 Pro (ないし Mate 30 Pro) は日本でもちゃんと出るんだろうか……。出初めは高すぎて手出なさそうだけど(苦笑

*1:ガウシアンフィルタによるぼかしも一応実装してみたが、あんまり良い感じにならなかった。試してみたければ --gfilter オプションで。

A-Frame で glb を表示すると暗くなる件

Oculus Go が出てから WebVR ( A-Frame ) でいろいろこっそり作ってみている。
ノウハウもそこそこ溜まってきて、アウトプットしようかなと思いつつもサボりっぱなし(苦笑

いろんな角度から撮った写真から 3D モデルを生成する photogrammetry ツールの 3DF Zephyr が楽しくて、3D モデルにしたらおもしろそうと思うようなものを写真取りまくって試している。
たとえばお寿司。

 


Photogrammetry(3DF Zephyr)でお寿司回してみた

 

こうした 3D モデルはなにがしかのフォーマットで保存するわけだが、とりあえず最初に使った obj+mtl 形式をずっと使ってきた。
後で調べると、まあずいぶんわんさかとある( Template:3Dファイル形式 - Wikipedia )。どれがいいのか比べて検討してみたが、obj+mtl をサポートしていないツールって基本無くて、交換用フォーマットとしてはなんだかんだ一番ポータビリティ高いのかも、と思ってたりする。

glTF は次の標準を狙っているということと OpenGL に最適化しているということで気になったけど、Blender がエクスポートできるのにインポートできない(要別途プラグイン)など、意外とサポートしてないツールがちょいちょいあるのが難点。
そして、A-Frame はちゃんと glTF をサポートしているのだけど、これで gltf/glb を表示するとなぜか暗くなるという現象があって、使うのをやめていた。


同じ 3DF Zepher からエクスポートした obj と glb(glTF のバイナリ形式) を A-Frame で表示すると見た目がこれくらい変わる。
モデルはサイボウズのエントランスにいるキリンのぬいぐるみ(「こきりんとーん」という名前)を 3DF Zephyr で 3D モデル化したもの。
(真ん中に浮いてるキューブは、後の対策で色がどう変わってしまうか見るために入れている)

 
f:id:n_shuyo:20181128161746j:plain

 

ライティングをいろいろ工夫したがなんともならない。directional light をあてても暗いままなので、どうも「暗い」と言うより「黒い」らしい。
3DF Zephyr で出力したものが悪いのかと、obj を Blender や MS 3D Builder で読んで glb にしても症状変わらず。
glb を A-Frame で表示したいなんてすごくよくある話だと思うんだが、ネットで探してもそれらしい情報はなぜか出てこない。手詰まり。

というわけで glb の使用はあきらめていたのだが、obj と glb の Oculus Go でのロード時間を比べると、大きいときは glb が 300ms くらいのときに obj+mtl が 3000ms と10倍から違う。*1

このモデルのロード(正確にはおそらくグラフィックメモリへの転送)中は完全に止まってしまう。初回表示時の 1回だけとはいえ 3秒待ちは体験を大きく損ねる。

そうなると「 glb 暗くなる問題」をなんとか解消して glb を使いたいところ。本腰を入れて探して、 Three.js の issue でようやくそれらしい情報に突き当たった。

GLTFLoader display model is darker · Issue #12554 · mrdoob/three.js · GitHub

長いんでテキトーに要約すると、まさに gltf だと暗くなるよ問題で、同様に glb を作るツールによらず起きるので データの問題ではないと報告。にもかかわらず、これ Three.js のせいじゃないよと close されたあと、問題を再認識して、"So... what's going on here?" で終了。えー困るー。

ただここには renderer.gammaOutput = true; したらいいよ、という情報も書かれていた。Three.js の renderer は、A-Frame では <a-scene> からアクセスできる。具体的には、

document.querySelector("a-scene").renderer.gammaOutput=true;

を </body> の後ろとか、ページの表示完了イベントとかで実行すればいい。

上のキリンで実行すると次のようになり、 glb モデルが期待する明るさになった!

 

f:id:n_shuyo:20181128161752j:plain

ただし見てわかる通り、確かに glb キリンは元の obj キリンと同じ明るさになったが、glb 以外のオブジェクトが明るすぎる……。

まだこの新しい問題は解決できていないが、「どうやっても暗い(黒い)」と違って対処のしようはあるので、一歩前進?

 

*1:同時表示可能オブジェクト数でも glb のほうが若干 obj より有利。といっても本当に「若干」で、巨大なテクスチャのモデルについて、obj だと12個表示したらブラウザが落ちるか VR モードが強制解除されるところ、glb だと13個表示できる「ことがある」くらいの差……。

#銀座VR に WebVR で行けるようにしてみた

5月に発売されたスタンドアローン(ひも無し) VR ヘッドセットの Oculus Go が想像通りの良いものだったので、会社で100人くらいにかぶせて回ったり、Unity や WebVR でなんかちょこちょこ作ってみたりということを最近している。

7/7 に銀座VR という、VR で作ったものを持ち寄るイベントがあるって直前で気づく。
銀座SIXでマーロウのプリンを買って帰ると言ったら簡単に参加許可が下りたので、のこのこ出かけてきた。


一体型VR限定イベント、つまり発売されたばかりの Oculus Go と Mirage Solo がメインターゲットということで時間が足りなかったか、未完成とかコンセプト重視な展示が中心だったかな。
一体型VRの最大のメリットでもある「持ち寄りやすさ」を活かすような展示があって、みんなが自分の Oculus Go を首から下げて歩いている……というの期待しちゃったりして、一応自分の Oculus Go を持ち込んだりもしてたのだが、カバンから出す機会もついぞなかった。無念。
そう思うなら自分で作れということだな。


こういうイベントは見るだけより自分も展示側で参加するほうが絶対に楽しいしんで、次があれば考えたいかも。
今回は間に合うわけもなかったけど、とりあえず Richo THETA で 360度写真を何枚か撮ったので、それをバーチャルツアー的に見せる VR コンテンツを作ってみた。
VR ヘッドセットで見るのが前提(Oculus Go で動作確認、他のヘッドセットでも動くことを期待するコードを書いたつもり)だが、PC や iPad でも見れる。


ブラウザでリンクを開くと、VR ヘッドセットの場合はブラウザ右下のゴーグルをクリックすると VR モードになり、会場の360度写真の中に入る。
自分の下を見ると会場マップがあるので、緑のマーカーをコントローラーで選択すると、その位置に移動する(写真が切り替わる)。

PCの場合は画面をドラッグすると視線の向きを変えられる。同様に下の方にマップがあるので、クリックで移動する。

iPad は、本体を掲げてぐるぐる周りを見ると、その向きの写真が表示される。移動は下向けてマップをタップ。


360度写真を見る&地図をポイントして移動するだけ。
VR コンテンツとしては簡素すぎるが、これでも十分雰囲気を味わえる。
各展示が再生できたり、出展者の Oculus Rooms の入口があったりしたら、ほんとに現地に行かなくても済むようになるかもw


マップを下においたのは思いつきといくつかの事情からの苦肉の策で、正直操作はしにくい。
でもマップと写真の向きがあっていることで、会場がどんな場所かすごく想像しやすくなっている……と思ったのだがどうだろう(身贔屓?)。


こういった VR コンテンツを作るなら、選択肢としては

のどっちか。

Unity は導入と配布のハードルがかなり高いが、ノウハウたんまり、エコシステム充実(アセットストア)、高パフォーマンスと、本格的なものを作るなら正直一択に近い。
でも、もともと Web 屋さんだった身としては、WebVR の可能性をできるだけ追求したいところ。というわけで Mozilla 謹製 WebVR フレームワーク A-Frame をがんばって使ってみている。
しかしかなりの発展途上で、ぶっちゃけ上の VR コンテンツを作るだけでも明文化されていないノウハウをかき集めたり自分で見つけ出したりしないといけない。とはいえ、コンテンツの配布が極めて容易という特大のメリットは捨てがたい。


今回の VRツアーみたいのが HTML+JS 70行程度で書けるのも WebVR の良いところだろう。
Unity だと開発環境を作るだけで何時間かかかるし、変更のたびにかかるコンパイル時間もバカにならない。


というわけで WebVR にも確実に需要はあるので、現時点でいくつかある A-Frame を使うノウハウについては、別途まとめるつもり。

「Chainer による実践深層学習」の気づいたこといくつか


Chainer について書かれた数少ない本。
この9月に v2 対応版が出た。が、v3 リリース秒読みの時期に……というツラミはある*1
深層学習ライブラリは現状「泳ぎ続けなければ死ぬ」(アップデート止まったら、終わったのかな? とか思っちゃう)ので、宿命的にしょうがないのかな。


社内でこの本の読書会とかしており、ちょいちょい間違いを見つけてしまう。
細かいのはもういいかなとは思うんだが(全部書いてたら正直キリがない)、せっかくの Chainer 本、読者が誤解すると事故が起きそうなちょっと大きめの間違いを放置するのはもったいないので、ここにメモしておく。
自分で読んだのは RNN 以降なので、主にその範囲。


以下、章・ページやリスト名などの表記は v1 版だが、v2 版でも残っていれば特定は難しくないと思う。
手元にあるのは v1 版で、v2 版では直ってるかもしれない。が、ちら見した感じでは残ってそうだった……。
ちなみに著者の新納先生のサイトに Erratta も出ているので参考に。見つけた間違いはほとんど載っていないが……。

7.7 Chainer による LSTM の実装(p98-99)


LSTM をあえて基本的な部品だけで実装する、本書の白眉。


lstm0.py のパラメータ宣言部で、式 Wx+Rh+b を実装するのに W 用の L.Linear と R 用の L.Linear を宣言しているが、両方がバイアス項を持つため、ダブってしまっている。Wx+Rh+a+b という状態。
致命的な悪さはおそらくしないだろうが、メリットは何もない*2ので、片方に nobias=True をつけて、バイアス項を一本化するべきだろう。
学習時間も 5% ほど短くなる。

class MyLSTM(chainer.Chain):
    def __init__(self, v, k):
        super(MyLSTM, self).__init__(
            embed = L.EmbedID(v, k),
            Wz = L.Linear(k, k, nobias=True),
            Wi = L.Linear(k, k, nobias=True),
            Wf = L.Linear(k, k, nobias=True),
            Wo = L.Linear(k, k, nobias=True),
            Rz = L.Linear(k, k),
            Ri = L.Linear(k, k),
            Rf = L.Linear(k, k),
            Ro = L.Linear(k, k),
            W = L.Linear(k, v),
        )

7.7 Chainer による LSTM の実装(p100)

また学習の部分は素の RNN のものと同じでよいのですが、計算時間がかなりかかります。RNN の学習には unchain_backward() という関数を使うことで改善されます。これは長い系列を学習する際に、古い情報を捨てて、計算時間を改善するのに使います。ここでは文の長さが 30 を超える場合に、この関数を起動することにします。


と記述されているが、残念ながらここでの使い方では unchain_backward は全く効果がない。
本書の lstm0.py で unchain_backward を入れたり削ったりしたときに性能が変わったように見えたとしても、それはおそらく初期値の乱数の影響である。


unchain_backward は、L.LSTM のような「前時刻の隠れユニットや記憶の状態をメンバーとして保持しておき、各時刻ごとに __call__ を呼ぶ」タイプの実装を前提として設計されている。RNN のグラフの横方向の辺を切って、計算グラフを小さくするために、例えば 30 時刻ごとに unchain_backward を発行する。加えて、公式のサンプルを含め、多くの LSTM 実装では「1系列=全データをつなげたもの」なので、切らないと大変なことになる。


しかし、本書の lstm0.py は「__call__ には系列全体を渡す。隠れユニットや記憶の状態は __call__ の先頭でローカル変数として宣言&初期化する」タイプの実装となっている。したがって、前の __call__ のあとに unchain_backward を発行しようがしまいが、次の __call__ が呼ばれたときは計算グラフは切れている。


ちなみに本書では「1系列=1文」であり、truncate しなくても死なない。が、隠れユニットが 100 次元しか無いので、適切に切ったほうがおそらく性能は高くなるだろう。
しかし lstm0.py で truncate したいと思っても、学習ループ(特に backward 発行)と時刻のループが別れてしまっているので、簡単な改造では難しそう。同等ではないが、 __call__ の中で、30時刻ごとに h.unchain() と c.unchain() を発行すれば、近いものになるのかな?

8.5 Attention の導入(p123)


Encoder の各時刻の隠れベクトルを Decoder に渡すためのリスト gh を作る部分の説明。

リスト gh に Encoder 側の \tilde{h}_i を順番に追加しています。
(中略)
gh に \tilde{h}_i を追加する際に、明示的に np.copy を使っていますが、おそらく必要ありません。そのまま ht.data[0] を渡しても問題ないとは思いますが、念のためコピーしました。


コピーしちゃダメーーー!!!


.data やそのコピーではなく Variable のまま渡さないと、せっかくの計算グラフが切れてしまう。
Decoder に渡す変数(もともとの Attention では隠れユニットを渡すが、最近のモデルでは Encoder の出力を渡すのが流行っている?)の計算グラフを切ってしまうと、「Attention にも有効なように Encoder が符号化してくれる」という Attention の嬉しさの一つが消えてしまう。


というわけで、コピーしないで、Variable のまま渡しましょう。

*1:ちなみに v1 版も、Chainer v2 が出た後の出版、しかも v1.10 準拠だった……

*2:aとbが最適解を持たないというデメリットはある

無限関係モデル(Infinite Relational Model)の紹介資料+実装


サイボウズ・ラボでは社内向けの機械学習勉強会を 2012年から週1ペースで継続している(前身の PRML 読書会も合わせれば 2011年から)。割り振られた担当者が、書籍や論文など読んだり、実装してみた話などを紹介している。


例えば今年の4月以降の勉強会のネタをピックアップするとこんな感じ。明らかに「いわゆる機械学習」周辺にすら含みようがないネタもポロポロあるが、「機械について学習」くらいまで拡大解釈している、ことにしてほしい(苦笑)。

  • プライバシー保護データマイニング
  • 並行実行制御
  • 強化学習
  • 状態空間モデル
  • 秘密計算(暗号化したまま各種演算)
  • 確率論
  • seq2seqで計算
  • Attention
  • End-to-End Memory Networks
  • WebAssembly
  • 複雑ネットワーク
  • "Why does deep and cheap learning work so well?"
  • "Sliding right into disaster"


この勉強会の資料は一部公開されている。西尾さんの強化学習や、光成さんの暗号系などなど。


中谷も当勉強会で機械学習自然言語処理のネタを紹介してきたのだが、資料はほとんど公開してなかった。口頭の説明やその場での書き込み前提とか、著作権的な配慮が足りてないとか、内部データを使っちゃってるとか、セキララな毒舌が炸裂してるとか、要は内部勉強会なことに甘えて作りがユルかった。
まあでもやっぱりもったいないので、先週から資料を人に見せられるレベルまで改訂して公開週間を始めた。第1弾が Memory Networks で、第2弾がこの無限関係モデル(Infinite Relational Model)。



無限関係モデルは2年くらい前(↓このあたりのブログ記事を書いていた頃)にやったので、上の最近の勉強会ネタリストにはない。


古いネタを掘り起こしてきたのは、実装があるものを優先したため。


この実装では、スライドでも説明しているとおり、ベルヌーイ分布をポアソン分布に替えた「0/1 じゃない関係解析」版を実装してみている。
が、ポアソン分布が外れ値に弱いので、ちょっと多い項目があると1要素のクラスタを作ってしまい、残念ながら使い物にならなかった。負の二項分布あたりを使いたいところだが、全情報付き事後分布を閉じた形で書き下せないだろう……。
CRP を Stick Breaking で書き直して、トピック数の上限入れて、Stan あたりで解かせてみるというのも考えたけど、ちょっと大きくなるだけで死ぬよな。まずはモデルとして意味があるか検証する、ってならアリかもしれない。


「古いネタ」なんて言っちゃったが、引き続き「続・わかりやすいパターン認識」はノンパラベイズについてきちんと詳解してくれている希少な和書だろう。続パタ以外となると、今なら佐藤一誠さんの「ノンパラメトリックベイズ」(MLP青シリーズ)があるので、もう1つ選択肢がある? でも続パタとは難易度がかなり違うので、読者層は重ならないかもしれない(と、えらそうに評してみたが、買っただけでまだ読んでない。ごめん)。


ノンパラベイズ、というかトピックモデルは昨今の深層学習ブームに押されまくっているが、内部の構造を想像しながらモデリングする楽しさは(一般的な)深層学習には無いものなので、またそのうち揺り戻しが来るかも?


というわけで次の資料公開週間は、実装があるので GAN あたりかな(トピックモデル推しの舌の根も乾かぬうちに……)。
GAN の記事や資料はすでに溺れるほどあるのであまり意味ないかもしれないが、Conditional GAN をやってる人は少ないっぽいので、そのあたりはちょっとおもしろいかもしれない。

End-to-End Memory Networks の勉強会資料+補足


前記事で End-to-End Memory Networks の実装を公開してたが、さらに社内勉強会の資料も公開する。
モデルもわかるように一応説明したつもり。



以下、前記事で書き忘れたこと+補足。

  • 実装は CPU / GPU 両対応している。が、GPU の方が遅い(苦笑)。たぶんデータの渡し方が悪い&モデルが小さいので、演算時間がオーバーヘッドを上回れないのだろう。データの渡し方を工夫すれば改善するだろうが、Random Noise がどうせ台無しにするので、そこを頑張るのはやめた。
  • 質問と記憶から応答情報を生成するのが基本。その応答情報を新しい質問とみなしてフィードバックすることで RNN 的な多層ネットワークを構成することができる。表現力が上がる……のかな? 実装では層の数をオプションで指定できるようにしているので、1層と3層で傾向がどのように変わるのか確認してみるのも面白いかもしれない。

End-to-End Memory Networks を実装してみた


久しぶりの更新。


学生さんが好きなものを開発するのを支援するサイボウズ・ラボユースという制度が始まってもう7年目。
先日、4年ぶりにラボユース合宿が開催された。


詳しくはリンク先の記事を見てもらいたいが、要は、泊まり込みで朝から晩までみっちりコードを書きまくり、夜も思う存分プログラミング談義な合宿に参加できるということ。朝昼晩のごはんもついてる。温泉もある(※開催地による)。もちろん費用はサイボウズ持ち。


サイボウズ・ラボユースは通年募集、まだまだ応募できる。興味あればぜひ。宣伝終わり。


さて、合宿にはサイボウズ・ラボの社員ものこのこついていく。
いろいろ話したり、指導したりもするのだが、やっぱりコードを書いている時間が一番長い。
しかしせっかくの合宿という場なのに、いつもと同じコードを書くのは芸がない。
そこで Memory Networks を実装してみることにした。


実は Memory Networks が、あまり好きではない。むしろ嫌いかもw。
だからこそ、食わず嫌いに陥らないために、いつもと違う雰囲気の中で実装してみようという志なわけだ。
と偉そうに言ってみたが、論文をろくに精読もしていない状態から3日間で実装するのはさすがに無謀で、合宿後も結構みっちりコード書いたり実験したりする羽目に(苦笑)。


Memory Networks とは、記憶した知識から質問にふさわしい情報を取り出し、回答を生成するモデル。
直接的には質問応答問題だが、汎用人工知能に発展させたいという野望が見え隠れしている。
日本語ならこちらのブログ記事か、MLP シリーズの「深層学習による自然言語処理」か。


前者はかなり詳しいが、さすがにこの記事だけで実装できるわけではなく、元論文を読む必要がある。
代表的な論文はこちらの3本だろう。

  • Weston, Jason, Sumit Chopra, and Antoine Bordes. "Memory networks." arXiv preprint arXiv:1410.3916 (2014).
  • Sukhbaatar, Sainbayar, Jason Weston, and Rob Fergus. "End-to-end memory networks." Advances in neural information processing systems. 2015.
  • Kumar, Ankit, et al. "Ask me anything: Dynamic memory networks for natural language processing." International Conference on Machine Learning. 2016.


素の Memory Networks は、「記憶にあるどの知識を参照するべきか」という情報が質問に付いているという前提のモデルである。
さすがにそれはちょっとなー、という人には、「どの知識を参照するべきか」も一緒に学習する End-to-End Memory Networks がある。
ネットワークの大きさも手頃で、3日間で実装するにはちょうどいいだろう(できなかったが)。


さらに発展した Dynamic Memory Networks では、「人間の推論はいきなり回答が出てくるのではなく、段階を踏んでいる」ことをモデルに組み込んだ。
End-to-End Memory Networks を実装してみて、その苦手なタスクを目の当たりにすれば、なるほど、そっちへ発展させたくなる気持ちがよくわかる。

End-to-End Memory Networks


ここで End-to-End Memory Networks の詳細に踏み込んだら、いつまでたっても実装の話に入れない。
社内勉強会用に End-to-End Memory Networks の資料を作ったので、モデルの概略は後ほどそちらを公開するときに語ることにする。
ここではモデルは既知として、実装によって確認できた知見をメインにしよう。


End-to-End Memory Networks のモデルそのものはシンプルかつ小さいので、モデルを記述するだけなら、どの深層学習ライブラリでも 10行ちょいで書けるだろう。
ただし、それだけでは全く性能が出ない。そこでさまざまな「工夫」を追加で施すことになる。

  • Temporal Encoding
  • Random Noise
  • Position Encoding
  • Linear Start
  • 勾配の切り詰め


素朴な End-to-End Memory Networks では、知識は記憶に追加されるだけであり、質問との関連を推定するときに時刻は考慮しない。
しかしそれでは、"Sandra moved to the garden." と "Sandra journeyed to the bathroom." という2つの知識の記憶があるとき、"Where is Sandra?" と質問されても、どっちの知識が今の Sandra の情報を表しているのかわからない。
そこで「記憶の知識の時刻と、質問時の時刻の差」の情報を組み込むのが Temporal Encoding だ。
これを入れないと、笑っちゃうくらい性能が出ない(タスクによってはランダムと同等まで落ちる)ので、Temporal Encoding は必須である。


ただし、Memory Networks の理想の姿であれば、知識が入ってきたときに「Sandra は garden に行った」という記憶を「 garden に行った後、bathroom に行った」に更新(Generalization)するべきなのだろう。
そこをモデル化していないツケを Temporal Encoding というヒューリスティックで払ってるわけだ。


Random Noise は、記憶の系列に 10% の確率で 0 ベクトルを挿入して時刻をずらすことで、Temporal Encoding が特定の訓練データに過適合するのを防ぐ。
論文ではかなり効果があるようだが、手元の実験ではいくつかのタスクで汎化性能がちょっこり上がった? くらいの印象。


素朴な End-to-End Memory Networks では「単語ベクトルの総和」を文のベクトルとするのだが、それだけでは "Mary handed the football to John." と "John passed the football to Mary." がほとんど区別できない。Position Encoding は単語ベクトルを加算するときに、ベクトルの要素ごとに単語の文中の位置に応じた重みを与えることで、単語の位置の情報を文ベクトルに落とし込む。


m_{ik}=\sum_j \left\{\left(1-\frac jJ\right)-\frac kd\left(1-\frac{2j}J\right)\right\}\left({\bf A}{\bf x}_{ij}\right)_k


ここで x_ij は i 番目の文の j 番目の単語(1-hot vector)、A は単語を分散表現ベクトルに変換する行列、J は文長(単語数)、d は分散表現の次元、k=1,…,d は分散表現ベクトルの要素インデックス。
この式は次のように変形することで、固定長の演算に落とし込むことができる。


m_{ik}=\left(1-\frac kd\right)\left({\bf A}\sum_j{\bf x}_{ij}\right)_k+\left(\frac{2k}d-1\right)\left({\bf A}\sum_j\frac jJ{\bf x}_{ij}\right)_k


データに対しては、単語頻度行列を単純和 \sum_j{\bf x}_{ij} と重み付き和 \sum_j\frac jJ{\bf x}_{ij} の2つをあらかじめ計算しておけばよい。
文を RNN とかでベクトル化すればこんな工夫はしなくていいだろうが、学習がテキメンに重くなる。
この手法ならネットワークの大きさを固定できるので、速度的には大幅に有利だろう。


Linear Start は、質問と各記憶の関連度を確率に落とし込むソフトマックス層がネットワークの中間にあるのだが、これを学習の初期に取り除いてしまうという手法。
学習が早くなり、local minimum に捕まりにくい……と論文は言うのだが、正直効果は実感できなかった。
validation loss が上昇したらソフトマックス層を挿入して本来のモデルに戻すので、Linear Start するしないはせいぜい初期値の影響程度。上述の固定長で実装すればかなり高速に学習できてしまうので、初期値を変えて何回かトライする&ちょっと長めに Epoch 回すくらいで十分 Linear Start を上回れるんじゃないの?


学習については、"No momentum or weight decay was used." と書かれており、生 SGD を使うことが明示されている。
しかし学習率を 1e-5 以下にしても、かなりの頻度であっさり inf に飛ぶ。特に上の Linear Start を組み込んだら、inf に飛ばずに学習できる方がまれになる。そこで、backward 後に、各パラメータごとに勾配のノルムが 40 を超えていたら、スカラー倍して 40 に納める、という強引な変換を入れる。
こんなの初めて見たんだけど、アリなのかな? GAN の学習が不安定なのとか、これでうまくいく割合が増えたり?


ただ、そんな勾配の切り詰めなんかしなくても、Adam でさっくり学習できたりする(苦笑)*1。まあ、Momentum 系特有の乱高下はちょいちょい起きるけど。


あとは学習率を細かく変えるとか、ミニバッチは 32 とか、指定されてるけど、そのへんはネグった。
というわけで実装はこちら。


当初、ベクトルを計算するところで文の長さに応じた処理が必要になると思って Chainer を選んだのだが、固定長で済むので Tensorflow と使うべきだった。スパース行列もサポートしているし。無念。
Chainer から Tensorflow への移植がどんなものかという興味もあるので、気が向いたら Tensorflow で実装し直すかもしれない。たぶん、そんなに大変じゃない。

bAbI データセット
https://research.fb.com/downloads/babi/


データセットは bAbI。Facebook が Memory Networks のために作った?
ごくごく単純な文法と、ごくごく小さな語彙セットで構成され、しかも回答は1単語という、名前の通りとても簡単な質問応答データセット。20 のタスクが用意されているが、否定文を含むのはその中の1つだけだったり。
リンク先から bAbI Tasks Data 1-20 (v1.2) をダウンロードして展開(ファイル名は tasks_1-20_v1-2.tar.gz )、e2emn.py を実行すると Task 1 で学習・推論する。
他のタスクに変えたり、上述の工夫を On/Off したり、Adam で学習したりもできるので、ヘルプ見て。


Task 1〜20 それぞれについて、初期値を変えて5回学習、それぞれ一番良いエラー率を拾ったものがこちら。
論文に合わせて、BoW(Temporal Encodingのみ)と、Position Encoding, Linear Start, Random Noise を順に有効にしていったものについて実験している。
大勢には影響ないだろうと思って validation=test にしてる。手抜き。



細かいところは色々違ってるが、かなり論文に近い結果が再現できた?
違いを産んでいるのは、やっぱりミニバッチを実装していないことかもしれない。
トライしなかったわけではないのだが、ソフトマックス層の幅がデータごとに違うのをうまく実装するのがめんどくさくなって、半日くらいであきらめてしまった。

*1:Adam の本来のアルゴリズムのおかげなのか、Chainer の Adam 実装のおかげなのかはわかってない。