バックプロパゲーションとは何なのか-Rubyでニューラルネットワークで学習やってみる#2

ということで前回の続き、バックプロパゲーションを用いたプログラムを実際に動かしてみます。

動作確認

今回はこちらのプログラムを動かしてみることにした。

https://github.com/gbuesing/neural-net-ruby

まず、iris data setのデータを読み込んで学習するサンプルを動かしてみる。

ここで使うiris data setであるiris.dataは以下のようなフォーマットになっていて、右からSepal length(がくの長さ), Sepal width(がくの幅), Petal length(弁の長さ), Petal width(弁の幅), Species(種)となっている。サンプル数は150となっている。

5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
.
.
.
5.7,2.8,4.5,1.3,Iris-versicolor
6.3,3.3,4.7,1.6,Iris-versicolor
4.9,2.4,3.3,1.0,Iris-versicolor
.
.
.
6.5,3.0,5.5,1.8,Iris-virginica
7.7,3.8,6.7,2.2,Iris-virginica
7.7,2.6,6.9,2.3,Iris-virginica

このプログラムは、がくと弁の大きさによって、アヤメの3種のうちどのクラスかということを出力として期待する。

This neural network will predict the species of an iris based on sepal and petal size

プログラムの内容

まず、プログラムの処理内容を確認していきたい。

pryでiris.rb逐次処理しながらプログラム内容を確認した。

18行目x_dataでは各サンプルの4つのパラメータが入っている。

x_data = rows.map {|row| row[0,4].map(&:to_f) }

次のy_dataでは、3つの要素を持つ配列のうち1を立てることでどの種か判別するように格納している

y_data = rows.map {|row| label_encodings[row[4]] }

またここではiris data setのうち0個目から100個のデータを訓練データにし、101個目から50個をテストデータとして用いている。

x_train = x_data.slice(0, 100)
y_train = y_data.slice(0, 100)

x_test = x_data.slice(100, 50)
y_test = y_data.slice(100, 50)

そしてNeuralNetクラスのインスタンスを生成。引数により入力層、中間層、出力層の数を指定している。

# Build a 3 layer network: 4 input neurons, 4 hidden neurons, 3 output neurons
# Bias neurons are automatically added to input + hidden layers; no need to specify these
nn = NeuralNet.new [4,4,3]

このプログラムでは2回識別を試みていて、最初に学習なしのテストを行い、次に学習を行った後、もう一度識別を試みている。

まずは学習なしのテストの部分で、以下のように実行されている。

run_test = -> (nn, inputs, expected_outputs) {
  success, failure, errsum = 0,0,0
  inputs.each.with_index do |input, i|
    output = nn.run input
    prediction_success.(output, expected_outputs[i]) ? success += 1 : failure += 1
    errsum += mse.(output, expected_outputs[i])
  end
  [success, failure, errsum / inputs.length.to_f]
}

puts "Testing the untrained network..."

success, failure, avg_mse = run_test.(nn, x_test, y_test)

->Ruby1.9からの新しいlambda記法である。

->(a,b){ p [a,b] } Ruby1.9 で導入された lambda の新しい記法。以下と同じ。

lambda{|a, b| p [a, b] } http://docs.ruby-lang.org/ja/2.0.0/doc/symref.html

ここでNeuralNet#runを呼び、識別処理を行ったものをoutputに格納される。

NeuralNet本体のソースコードは割愛する。以下を参照していただきたい。

https://github.com/totzYuta/neural-net-ruby/blob/master/neural_net.rb

成功判定はprediction_successが以下のようにあらかじめ作成しておいたy_testのクラス分けの配列を用いて行う。

prediction_success = -> (actual, ideal) {
  predicted = (0..1).max_by {|i| actual[i] }
  ideal[predicted] == 1
}

次に、学習・学習後の識別の部分は以下のようなプログラムとなっている。

puts "\nTraining the network...\n\n"

t1 = Time.now
result = nn.train(x_train, y_train, error_threshold: 0.01, 
                                    max_iterations: 1_000,
                                    log_every: 100
                                    )

# puts result
puts "\nDone training the network: #{result[:iterations]} iterations, #{(result[:error] * 100).round(2)}% mse, #{(Time.now - t1).round(1)}s"


puts "\nTesting the trained network..."

success, failure, avg_mse = run_test.(nn, x_test, y_test)

puts "Trained classification success: #{success}, failure: #{failure} (classification error: #{error_rate.(failure, x_test.length)}%, mse: #{(avg_mse * 100).round(2)}%)"

動作確認

出力結果は以下のようになった。

$ ruby examples/iris.rb                                                                                                              
Testing the untrained network...
Untrained classification success: 16, failure: 34 (classification error: 68%, mse: 28.17%)

Training the network...

[100] 1.72% mse
[200] 1.04% mse
[300] 1.03% mse
[400] 1.03% mse
[500] 1.03% mse
[600] 1.03% mse
[700] 1.03% mse
[800] 1.03% mse
[900] 1.03% mse
[1000] 1.03% mse

Done training the network: 1000 iterations, 1.03% mse, 5.3s

Testing the trained network...
Trained classification success: 48, failure: 2 (classification error: 4%, mse: 1.57%)

学習していない状態では32%の精度だったのに対し、学習後は96%で識別できていることが確認できる。

4x4の学習データを学習させる

次に、用意した4つのサンプルデータを学習させてみる。

学習データは以下の4種類。

Screenshot 2015-07-15 18.59.07.png

number_image.dataというデータファイルを以下のように定義した。一列がひとつのデータにあたり、カンマで区切られた最初の16個の数字が実際の学習データで、17個目の数字はその学習データが文字'1'を表しているのか文字'0'を表しているのかを表している。

5行目はテスト用の未知データとなっている。

0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,1
0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1
0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0
1,1,1,1,1,0,0,1,1,0,0,1,1,1,1,1,0
0,0,1,0,0,1,0,1,0,1,0,1,0,1,1,0,0

新たにexamples/number_image.rbというファイルを作成した。プログラムは以下のようになった。

#!/usr/bin/env ruby

require_relative '../neural_net'

# This neural network will predict the character '0' or '1'

rows = File.readlines("examples/number_image.data").map {|l| l.chomp.split(',') }

class_flags = {
  0 => [1, 0],
  1 => [0, 1]
}

x_data = rows.map {|row| row[0,16].map(&:to_i) }
y_data = rows.map {|row| class_flags[row[17].to_i] }

# Training Data
x_train = x_data.slice(0, 4)
y_train = y_data.slice(0, 4)

# Testing Data
x_test = x_data.slice(4, 1)
y_test = y_data.slice(4, 1)

# Build a 3 layer network: 16 input neurons, 8 hidden neurons, 2 output neurons
# Bias neurons are automatically added to input + hidden layers; no need to specify these
nn = NeuralNet.new [16,8,2]

prediction_success = -> (actual, ideal) {
  predicted = (0..1).max_by {|i| actual[i] }
  ideal[predicted] == 1 
}

mse = -> (actual, ideal) {
  errors = actual.zip(ideal).map {|a, i| a - i }
  (errors.inject(0) {|sum, err| sum += err**2}) / errors.length.to_f
}

error_rate = -> (errors, total) { ((errors / total.to_f) * 100).round }

run_test = -> (nn, inputs, expected_outputs) {
  success, failure, errsum = 0,0,0
  inputs.each.with_index do |input, i|
    output = nn.run input
    prediction_success.(output, expected_outputs[i]) ? success += 1 : failure += 1
    errsum += mse.(output, expected_outputs[i])
  end
  [success, failure, errsum / inputs.length.to_f]
}

puts "\nTraining the network...\n\n"

t1 = Time.now
result = nn.train(x_train, y_train, error_threshold: 0.01, 
                                    max_iterations: 100,
                                    log_every: 10
                                    )

# puts result
puts "\nDone training the network: #{result[:iterations]} iterations, #{(result[:error] * 100).round(2)}% mse, #{(Time.now - t1).round(1)}s"

これを実行したところ、以下のように出力され、学習が適切に行われていることを確認できた。

$ ruby examples/number_image2.rb                                       

Training the network...


Done training the network: 4 iterations, 0.4% mse, 0.0s

未知のデータの分類

次に、number_image.rbに以下を加え、学習させたネットワークでテストデータの認識を行わせてみる。

puts "\nTesting the trained network..."

success, failure, avg_mse = run_test.(nn, x_test, y_test)

puts "Trained classification success: #{success}, failure: #{failure} (classification error: #{error_rate.(failure, x_test.length)}%, mse: #{(avg_mse * 100).round(2)}%)"

出力結果は以下のようになった。

$ ruby examples/number_image.rb                                         

Training the network...

Done training the network: 5 iterations, 0.5% mse, 0.0s

Testing the trained network...
Trained classification success: 1, failure: 0 (classification error: 0%, mse: 0.03%)

もともとのテストデータでは未知データの期待値を0として入力していたので、この未知データx1は0と識別されたことになる。

まとめ

バックプロパゲーションで学習させることが高い認識精度につながることがわかった。

次はもっと大きな学習データで中間層の数を工夫させながら学習して識別させたり、NN法などとの精度の違いなどについても検討したい。

参考資料

[1] 石井健一郎、上田修功、前田英作、村瀬洋 (1998) 『パターン認識オーム社

[2] 小高知宏 (2011) 『はじめての機械学習オーム社

参考記事

  • gbuesing/neural-net-ruby

https://github.com/gbuesing/neural-net-ruby

http://blog.yusugomori.com/post/21858253979/ruby-python

http://qiita.com/ginrou@github/items/07b52a8520efcaebce37

http://aidiary.hatenablog.com/entry/20140122/1390395760

https://github.com/yusugomori/DeepLearning