PRML Hackathon #1


パターン認識と機械学習(PRML) Hackathon #1 という、集まって各自思い思いに好きなものを作る(ん?)*1という Hackathon があったので、のこのこ行ってきた。


せっせと実装して、 19時くらいから各自の作ったものを軽く発表。
PRML に直接関係ないことをやってはる人の話が面白くて、なんかくやしい。
みなさんがどのようなことをやられていたかは、naoya_t さんがまとめてはるので、そちらで。


id:tsubosaka さんと tb_yasu さんが話してはった LSH はちょっこり気になるところ。iVoca でそろそろレコメンドしたいな、とか思っているので、使えるのかも?


id:Cryolite さんと id:smly さんが iris と adult dataset をパーセプトロンで分類させてみて、やっぱ線形分離じゃあないっぽいね、的な話をされていたので、帰ってからニューラルネットワークで分類させてみた、というのはまた別途。


19時から各自5分ずつ発表、って参加者20人近くいたら2時間だよね、と気づいたのは全員の発表が終わった21時。
分煙? なにそれおいしいの?」なロイホに移動して、tb_yasu さんに「 LSH の論文多いとかって、要はアルゴリズムとデータセットの数のかけ算だけ論文あるってことですよね。そういうのどーかなー」とか噛みついてみたりしてた。
アルコールも入っていないのに からんですいません。


次回は本読みに戻って、10/3 実施

自分のやったこと


先日から作っているニューラルネットワークRuby 実装の高速化を図りつつ、たたみこみネットワークを実装してみた。
LeCun の "Gradient-Based Learning Applied to Document Recognition" を読んで、Le-Net5 を実装してみたくなったので、それの実現に向けての第一歩という感じ。


ニューラルネットワーク(多層パーセプトロン)の Ruby 実装については、小出しに何度も記事を書いているので、スペックを整理。

  • DSL 的にネットワークを定義
    • いろんなネットワークが組める
  • PRML 5.3 まではほぼフル実装
    • 順伝播, 勾配降下, 数値微分, 逆伝播, tanh, sigmoid, softmax
  • グラフ描画は未サポート
    • 学習結果を R の式で出力させて、R のコンソールにコピペ(汗)
  • 高速化のためにコード生成 【→あとで別の記事に】
    • でも満足する速度には至らず……
    • でもコード生成の方が たたみこみの実装は楽だった!(^_^v


ソースはここ。
http://github.com/shuyo/iir/tree/master/neural/


DSL 的にネットワークを定義」というところを説明するために、2層の場合と3層の場合をみてみる。

2層(隠れユニット1層)

# ネットワーク
network = Network.new(:error_func=>ErrorFunction::CrossEntropy)

# ユニットを定義
in_units = [Unit.new("x1"), Unit.new("x2")]           # 入力
hiddenunits = (1..6).map{|i| TanhUnit.new("z1#{i}")}  # 隠れユニット1
out_unit = [SigUnit.new("y1")]                        # 出力

# ユニット間のリンクを定義
network.in  = in_units             # 入力
network.link in_units, hiddenunits # 入力 → 隠れユニット
network.link hiddenunits, out_unit # 隠れユニット → 出力
network.out = out_unit             # 出力

3層(隠れユニット2層)

# ネットワーク
network = Network.new(:error_func=>ErrorFunction::CrossEntropy)

# ユニットを定義
in_units = [Unit.new("x1"), Unit.new("x2")]           # 入力
hiddenunits = (1..6).map{|i| TanhUnit.new("z1#{i}")}  # 隠れユニット1
hiddenunits2 = (1..6).map{|i| TanhUnit.new("z2#{i}")} # 隠れユニット2
out_unit = [SigUnit.new("y1")]                        # 出力

# ユニット間のリンクを定義
network.in  = in_units                 # 入力
network.link in_units, hiddenunits     # 入力 → 隠れユニット1
network.link hiddenunits, hiddenunits2 # 隠れユニット1 → 隠れユニット2
network.link hiddenunits2, out_unit    # 隠れユニット2 → 出力
network.out = out_unit                 # 出力


Network::in に入力ユニットを指定、Network::link でユニット間を結合(バイアス項は自動的に付加)、Network::out に出力ユニットを設定することで、いろいろな形のネットワークを柔軟に構築して試してみることが出来る。
フルリンクの場合は上記のように簡潔に書けるし、個別に記述すればもっと複雑な接続も可能。iris dataset の学習ではそれが有効だったが、それはまた別の記事に。


では本題の「たたみこみ」。


ニューラルネットワークにおける たたみこみの定義は PRML 5.5.6 をみてもらうとして、実装に必要なのは「たたみこみの場合に、逆伝播の式はどのようになるか」(PRML の演習 5.28)。
でもこれは簡単。


非たたみこみでは w は1つの活性にしかひも付かないが、たたみこみでは複数の活性にひも付くため、PRML (5.50) で左辺の偏微分を展開したときに複数の δ_j z_ji の和となる。

\frac{\partial E_n}{\partial w_{i}}=\sum_{j}\frac{\partial E_n}{\partial a_j}\frac{\partial a_j}{\partial w_{i}}=\sum_{j}\delta_j z_{ji}

つまり、w による偏微分(=勾配の要素)が、重みに w をもつ全てのリンクについての (接続元の z_i)*(接続先の δ_j) の和になる。
z_i と δ_j を求めるところは影響を受けない。
簡単。


と、そうはいっても、上に書いたような汎用性を確保しようとおもったら、ちょいとめんどくさい。
そもそも DSL をどのように書かせるか、が問題。そこが一番悩んだ(苦笑)。


こうなった。

in_units = (1..(28*28)).map{|i| Unit.new("x#{i}")}
convolutions = (1..(24*24)).map{|i| IdentityUnit.new("c#{i}")} # たたみこみ層
hiddenunits = (1..100).map{|i| TanhUnit.new("z#{i}")}
out_unit = (1..10).map{|i| SoftMaxUnit.new("y#{i}")}

# network
network = Network.new(:error_func=>ErrorFunction::SoftMax)
network.in = in_units

network.convolutional_link in_units, convolutions, 28, 28, 5 # たたみこみ
network.link convolutions, hiddenunits

network.link hiddenunits,  out_unit
network.in = in_units


Network::convolutional_link で たたみこみリンクを指定する。とりあえず2次元の畳み込みだけ考慮すればよいだろうということで、縦横の大きさと、たたみこみエリアの大きさをパラメータで与える。
たたみこみの活性関数は恒等関数でいいのか、ハイパボリックタンジェントがいいのか(convolutions の定義で IdentityUnit ではなく TanhUnit を使う)とか、そこらへんはまだよくわかっていない。


実装が済んだら当然、次は実験して効果を確かめるはずだが、まだそれらしい結果にたどり着けないでいる。
たたみこみ を行うくらいだから、入力はそれなりに次元が大きく、またデータセットも大きい。


畳み込み層(特徴マップ1個)→隠れユニット層(ユニット数30)→出力(10個、ソフトマックス)という、これ以上簡単にするのが難しいネットワークで MNIST 6万件を100周学習させるのでさえ 100時間(!!!)かかる計算。
そしてニューラルネットワークは初期値依存が非常に高いので、1回試行するだけでは足りず、できれば100回単位で試行して、的中率の最高とか最低とか平均とかを見たい。


Ruby なんかで実装するから遅いんだよ〜、とか言われそうだが、1件の学習は 60 msec。
それが600万件あるだけなのだ。
sampling を実装すれば、次元を落とすことが出来るけど、うーん、それでもまだ多い。


とはいえ Ruby の範囲でできる高速化はほぼ限界なので、やっぱり C++ で実装するかあとか心揺れ中。
naoya_t さんに「そんなのリニアに速くなるだけでしょ」と言われたときは、そうだよな、がんばって10倍速い実装作るより PC が 10倍速くなるのを待つ方がいいよな、とか思いつつ、100時間が 10時間になればそれはそれで嬉しい。うーむ。

*1:PかRかMかLが付いていればいいらしい。ウソ