パターン認識と機械学習(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 の論文多いとかって、要はアルゴリズムとデータセットの数のかけ算だけ論文あるってことですよね。そういうのどーかなー」とか噛みついてみたりしてた。
アルコールも入っていないのに からんですいません。
自分のやったこと
先日から作っているニューラルネットワークの Ruby 実装の高速化を図りつつ、たたみこみネットワークを実装してみた。
LeCun の "Gradient-Based Learning Applied to Document Recognition" を読んで、Le-Net5 を実装してみたくなったので、それの実現に向けての第一歩という感じ。
ニューラルネットワーク(多層パーセプトロン)の Ruby 実装については、小出しに何度も記事を書いているので、スペックを整理。
- DSL 的にネットワークを定義
- いろんなネットワークが組める
- PRML 5.3 まではほぼフル実装
- グラフ描画は未サポート
- 学習結果を 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 の和となる。
つまり、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が付いていればいいらしい。ウソ