「パターン認識と機械学習(PRML)」5章の勉強して、ニューラルネットワーク実装してみたくならんかったらヤバいよね、ってくらい実装してみろオーラが出てたので、書いた。
少し汎用的に作ってみたかったので、R ではなく Ruby。
http://github.com/shuyo/iir/tree/05e118c62a96cd6f7ab86c55eddebdb8dd148bc5/neural
- ニューラルネットワーク(多層パーセプトロン)の実装
- 逐次的勾配降下法で重みパラメータを学習する
- 順伝播&中心差分による数値微分で勾配を求める(誤差逆伝播も後で)
- error function は二乗和誤差関数(交差エントロピー誤差関数も後で)
- 正規化項は未サポート(over fitting するところまでまだ行かない)
ユニットを組み合わせて自由にネットワークを組める。
TanhUnit や SigUnit や IdentityUnit でユニットを生成し、Network::link でユニットをつなぐことができる。
隠れユニットを2層でも3層でも重ねられるし、同じエンジンで回帰も分類も出来る(はず)。
たとえば PRML の図 5.2 のような、層を飛び越えた結合を持ってたり、単純な層構造ではないちょっとややこしいネットワーク*1なら、以下のように書く。
require "neural.rb" # units x1 = Unit.new("x1") x2 = Unit.new("x2") z1 = TanhUnit.new("z1") z2 = TanhUnit.new("z2") z3 = TanhUnit.new("z3") y1 = IdentityUnit.new("y1") y2 = IdentityUnit.new("y2") # network network = Network.new network.in = [x1, x2] network.link [x1, x2], [z1] network.link [x1], [z3] network.link [x2], [z2] network.link [z1], [z2, z3] network.link [z2, z3], [y2] network.link [x1, z3], [y1] network.out = [y1, y2] network.weights.dump p network.apply(1, 2)
network.weights.dump するとネットワークを数式化して、 R で扱える形式で出力する(ただし出力順は最適化されてないので並び替える必要があるかも)。
network.apply() は入力に対してネットワークを評価して、出力を返す。
z1 <- tanh( -1.09452152850896 * x1 + -0.164124254873188 * x2 ); z2 <- tanh( 0.174092041861453 * x2 + -0.163732466505426 * z1 ); z3 <- tanh( -0.823260928657359 * x1 + -0.743349301302604 * z1 ); y1 <- ( 0.741514177328782 * x1 + 1.30098908005562 * z3 ); y2 <- ( -0.560284110426476 * z2 + -0.360757120405139 * z3 ); [0.533149306592727, -0.198458406447339]
出力。
重みの初期値はランダム(後述)。
というわけで、PRML のサンプルデータを使って回帰分析をニューラルネットワークしてみた( curve_fitting.rb )。
入力ユニット1つ、出力ユニット1つ、隠れユニットは4つ(1層)。
require "neural.rb" # training data ( y = sin(2 PI x) + N(0, 0.3) ) D = [ [0.000000, 0.349486], [0.111111, 0.830839], [0.222222, 1.007332], [0.333333, 0.971507], [0.444444, 0.133066], [0.555556, 0.166823], [0.666667, -0.848307], [0.777778, -0.445686], [0.888889, -0.563567], [1.000000, 0.261502], ] # units in_unit = Unit.new("x1") bias = BiasUnit.new("1") hiddenunits = [TanhUnit.new("z1"), TanhUnit.new("z2"), TanhUnit.new("z3"), TanhUnit.new("z4")] out_unit = IdentityUnit.new("y1") # network network = Network.new network.in = [in_unit] network.link [in_unit, bias], hiddenunits network.link hiddenunits + [bias], [out_unit] network.out = [out_unit] eta = 0.1 1000.times do |tau| error1 = error2 = 0 D.sort{rand}.each do |data| error1 += network.sum_of_squares_error([data[0]], [data[1]]) grad = network.gradient_E([data[0]], [data[1]]) network.descent_weights eta, grad error2 += network.sum_of_squares_error([data[0]], [data[1]]) end puts "error func: #{error1} => #{error2}" end network.weights.dump
分析結果をグラフで見るために dump の出力を R に貼り付け。
f <- function(x1) { #### dump の結果(ここに挿入) z1 <- tanh( -5.31010443290274 * x1 + 2.545193412289 * 1 ); z2 <- tanh( -3.13481519530539 * x1 + 3.47733946208869 * 1 ); z3 <- tanh( 2.84592543698541 * x1 + 0.401839456737652 * 1 ); z4 <- tanh( 1.03309548242672 * x1 + -1.00660409988017 * 1 ); y1 <- ( 1.54193033910729 * z1 + -2.11515890793226 * z2 + 2.25018511144192 * z3 + 0.119938514493498 * z4 + 0.107151296381274 * 1 ); #### dump の結果(ここまで) y1; } plot(f, xlim=c(0,1), ylim=c(-1,1), xlab="", ylab=""); par(new=T) plot(data, xlim=c(0,1), ylim=c(-1,1), xlab="", ylab="");
なかなかきれいにフィッティングしたが、疑問がいくつも。
- 重みの初期値は乱数で決定する必要があるが、「どういう乱数」にすればよいのか。とりあえず N(0, 1) にしといたけど……初期値への依存度が極めて高い(それは PRML 図5.10 を見てもわかる)から、分散はもうちょっと大きくしたいけど、大きくしすぎたくない。うーん。
- 学習率 ηは? 色々試した範囲では η=0.1 が具合が良かったが、一般にはどのように決めればいい? やっぱり交差確認?
- 繰り返し回数は? 100回くらいではどうしてもいい結果が出なかったので、1000回になっているが、ちょっと多すぎる? もっと減らしたい気がしてしまうが、こんなもん? 直感的には、逐次的勾配降下法だとどうしても繰り返し回数がふくらんでしまうんでは、という雰囲気を感じているのだが。
- 早期終了(§5.5.2)のための学習停止は? 誤差関数が増加に転じたら停めちゃえばいい?
- 正規化項の係数λは? やっぱり交差確認?
明日の PRML読書会 #6 でここらへんの話ができたらいいな。
*1:こういう形のネットワークが適したモデルとかあったりするんだろうか?