PRML§5から多層パーセプトロンの実装


パターン認識機械学習(PRML)」5章の勉強して、ニューラルネットワーク実装してみたくならんかったらヤバいよね、ってくらい実装してみろオーラが出てたので、書いた。
少し汎用的に作ってみたかったので、R ではなく Ruby

http://github.com/shuyo/iir/tree/05e118c62a96cd6f7ab86c55eddebdb8dd148bc5/neural

ユニットを組み合わせて自由にネットワークを組める。
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:こういう形のネットワークが適したモデルとかあったりするんだろうか?