ニューラルネットワークで分類

ニューラルネットワーク作ってみたXOR 学習させてみた の続き。


まず、ニューラルネットワークのエンジンをバージョンアップ。

  • 誤差関数を選べるようにした。
  • 逆伝播を実装。数値微分と逆伝播のどちらを使うか選べるように。
#(前略)

# network
network = Network.new(
  # 誤差関数の指定(デフォルトは SquaresSum)
  :error_func=>ErrorFunction::CrossEntropy,
  # 勾配を算出する方法の指定(デフォルトは BackPropagate)
  :gradient => Gradient::NumericalDiff      
)

#(中略)

# なにがしかのトレーニングデータ
x = [...]
t = [...]

# :error_func で指定した誤差関数で誤差を計算(クロスエントロピー)
puts network.error_function(x, t) 

# 二乗和誤差関数で誤差を計算
puts ErrorFunction::SquaresSum(network, x, t)

# :gradient で指定した方式で勾配を計算(数値微分)
p network.gradient_E(x, t)

# back propagation で勾配を計算
p Gradient::BackPropagate(network, x, t)

誤差関数を変えて効果を検証したり、数値微分が逆伝播の近似になっていることとかも簡単に確かめられるようになった。


ちなみに XOR の学習では、二乗和誤差よりクロスエントロピーの方が遙かによい学習性能だった。
PRML に書いてあるとおり!


さて本題。


ちゃんとしたクラス分類問題にチャレンジする。
PRML のサンプルデータから、二次元空間上に分布する2クラスの分類境界を求める。つまり PRML 図5.22 を再現しよう、ということ。

図のキャプションに「8個の隠れユニット」とあるから、その通りにしてみたのだが、何度やってもだいたい2つか3つの隠れユニットの係数がほぼゼロ(<10^-8)になってしまう。
多すぎるんだろうなあ〜ということで、隠れユニットは6つに。


XOR の学習のときとほとんど同じなのだが、一応スクリプト
外部ファイルからのデータの読み込みがあるのと、隠れユニットの個数を変えやすくなっているところが違う。

require "neural.rb"

# load training data
data = []
open("classification.txt") do |f|
  while line = f.gets
    x1, x2, t = line.split
    data << [[x1.to_f, x2.to_f], [t.to_f]]
  end
end

# units
in_units = [Unit.new("x1"), Unit.new("x2")]
bias = [BiasUnit.new("1")]
hiddenunits = (1..6).map{|i| TanhUnit.new("z#{i}")}
out_unit = [SigUnit.new("y1")]

# network
network = Network.new(:error_func=>ErrorFunction::CrossEntropy)
network.in  = in_units
network.link in_units + bias, hiddenunits
network.link hiddenunits + bias, out_unit
network.out = out_unit

eta = 0.1
sum_e = 999999
1000.times do |tau|
  s = 0
  data.each do |d|
    s += network.error_function(d[0], d[1])
  end
  puts "sum of errors: #{tau} => #{s}"
  break if s > sum_e
  sum_e = s

  data.sort{rand}.each do |d|
    grad = network.gradient_E(d[0], d[1])
    network.weights.descent eta, grad
  end
end
network.weights.dump

全ソースは github に。
http://github.com/shuyo/iir/tree/1c60ee0d8a797093bdb40cebf921b948efd75b2c/neural


学習データは PRML のサポートサイト から classification.txt をダウンロードしておいて実行すると、例によって R 用の数式を吐き出すので、それを使ってグラフを描かせる。

f <- function(x1, x2) {
### classification.rb の出力を貼り付け
z1 <- tanh( 0.724245062481378 * x1 - 6.34195321366142 * x2 + 5.90243475278653 );
z2 <- tanh( -4.12697707890697 * x1 + 6.12505071167141 * x2 + 7.93655504094266 );
z3 <- tanh( 5.79455951471411 * x1 - 0.871914507409759 * x2 - 4.98138971912141 );
z4 <- tanh( 12.7413647397556 * x1 - 0.996851070945907 * x2 - 0.996074334570183 );
z5 <- tanh( -2.54858945755993 * x1 + 6.35851931039518 * x2 + 14.4563325477077 );
z6 <- tanh( -8.22703520207703 * x1 + 1.67912728957595 * x2 - 3.42901207219138 );
y1 <- sig( 1.43603044078087 * z1 + 0.646239444274332 * z2 - 0.614425154924937 * z3 
    - 1.74106890638641 * z4 - 1.07129591746046 * z5 + 3.75103075287345 * z6 + 4.34951131040511 );
### classification.rb の出力を貼り付け (ここまで)
y1;
}

x1 <- seq(-2,3,length=400);
y1 <- seq(-2,3,length=400);
z <- outer(x1, y1, f);
cols <- heat.colors(200);
cols[100] <- "#0000FF";  # 中央を青に
image(x1, y1, z, xlim=c(-2,3), ylim=c(-2,3), zlim=c(-0.5,1.5),
    xlab="", ylab="", col=cols, axes=T);

# トレーニングデータをプロット
DF <- read.table("classification.txt")
par(new=T)
plot(subset(DF, V3==0, c(V1, V2)), xlim=c(-2,3), ylim=c(-2,3),
    xlab="", ylab="", xaxs="i", yaxs="i");
par(new=T)
plot(subset(DF, V3==1, c(V1, V2)), xlim=c(-2,3), ylim=c(-2,3),
    xlab="", ylab="", xaxs="i", yaxs="i", pch=24);

青い線は y = 0.5 。
#このグラフを書くのに一番時間がかかったとかw


なかなかいい感じの学習が出来ていることがわかるが、このいい感じの結果を出すために何度も何度も学習を試す必要があった。
初期値の乱数への依存が高すぎ。


本当は PRML の図5.22 の overfitting の線を描くのが目標だったのだけど、今ひとつ。
っていうか、図5.22 と点の密度が違う……? データが違うのか?


次は、MNIST の手書き文字データ(6万件)の学習にチャレンジしてみたいところ。
今のままじゃあ無理だろうなあ、と思いつつ、試しに突っ込んでみたのだけど……
まずは LeCun の paper にもある「2層&隠れユニット 300個&たたみこみ無し」という単純なネットワークで試してみたのだけど、1周(6万件)で2日って、処理時間がかかりすぎ! 無理無理。
Ruby を 1.9 にしてみてもせいぜい倍速くらいにしかならず。


というわけで、現在高速化を図っている。
PRML Hackathon では、これに たたみ込みを実装することになりそうやなー。