Rubyでバックプロパゲーションによる学習に入門してみる

Rubyでニューラルネットワークを使った学習をするバックプロパゲーションのプログラムを試してみたいと思い立ったのでメモしておきます。

今回の記事では、その準備ということでまずバックプロパゲーションが何なのかということから振り返ってみたいと思います。

機械学習に関しては勉強を始めたばかりなので、理解が行き届いてない箇所や勘違いしている部分などたくさんあると思いますので教えてくださいますと幸いですm(_ _)m

## バックプロパゲーションについて

### バックプロパゲーションとはなんなのか

まず、バックプロパゲーションがなんなのか、というところから振り返ってみたい。

そのためには、ニューラルネットワークについてまず知らなければいけない。

ニューラルネットワークとは、簡潔に説明すると「ニューロセルを複数結合した情報処理機構」である。

では「ニューロセル」とはなんなのか。

### ニューロセル

ニューロセルはニューラルネットワークにおける**ノード**に相当する。

ニューロセル自体は、複数の入力に対し、それぞれに重みをかけたものを足しあわせ、閾値を引いたものを出力とする機構であり、これは神経細胞をモデルにしているらしい。

![Kobito.ckvvbY.png](https://qiita-image-store.s3.amazonaws.com/0/37676/3f7c8c1f-9e55-dc8a-efa3-65cc4ab5c298.png "Kobito.ckvvbY.png")
引用: http://sacraya.610t.org/Press/No18/neuro/





xを入力、wを重み、vを閾値、zを出力とすると、とすると以下のような式で表せる。

```
u = Σ (x_i * w_i) - v
```

このuを出力関数に突っ込んだ値が出力となる。人間が判断するならここまででもいいような気がするけど、これをコンピュータに理解させやすく変換したい、ということだろうと思う。

```
z = f(u)
```

この出力関数にはいろいろ種類があり、中でも**ステップ関数**と**シグモイド関数**がよく用いられる。

- ステップ関数 ... uが正なら1, 負なら0
- シグモイド関数 ... f(u) = 1/(1+e^-u)で表される

ニューロセルは単体でも論理演算をする機構として機能させれる！なるほど！

### ニューラルネットワークとは

そしてこのニューロセルを複数組み合わせたものがニューラルネットワークと呼ばれるもの。

一番簡単なもので言えば、ニューロセル２つの出力を受け取ったニューロセルが1つの出力を出すようなものとか。そんな風に入力された値がネットワークの中を順番に伝播していくニューラルネットは、**フィードフォワードネットワーク(feed forward network)**または階層型ネットワークと呼ばれる。


### パーセプトロン

そしてバックプロパゲーションを理解するには、まずパーセプトロンについて理解しなければいけないのでそこも復習しておこう。

パーセプトロンとは、フィードフォワード型ネットワークのうちの、ある特定の形式をもったニューラルネットをそう呼んでるだけ。

パーセプトロンは、３層の層構造を持ったニューラルネットであり、中間層から出力層への結合荷重や閾値を学習により変更するというもの。入力層から中間層への結合荷重はランダムな定数とする。


![Kobito.0eDQL6.png](https://qiita-image-store.s3.amazonaws.com/0/37676/fff430b5-32af-64bb-734d-9b86ba0571e2.png "Kobito.0eDQL6.png")
引用: http://d.hatena.ne.jp/ura_ra/20111026/1319642014


パーセプトロンでは、入力層から中間層への結合荷重を変更しなくても、中間層から出力層への結合荷重を適切に選ぶことで、論理積や論理和などの動作を行うことができる。こんな風にパーセプトロンはいろんな出力を作り出すことができる！

ただし、いくら結合荷重を調節しても排他的論理和（XOR）だけは作り出せないことがわかっている。（入力層から中間層への結合荷重の値によっては実現することもできる）。

このように、一般にパーセプトロンでは出力層の結合荷重を調節するだけでは表現できない関数が必ず存在し、この問題を、**パーセプトロンの線形分離不可能問題**と呼ぶ。

結合荷重と閾値の学習には、ヘブの学習則なんていう、生物の神経回路網で頻繁に信号を伝達するシナプスの結合がより強化される、ということに着目した方法がある。


### バックプロパゲーション

前述のパーセプトロンの線形分離不可能問題を回避して学習するために、バックプロパゲーションという手法を用いる。

バックプロパゲーションは、出力層の結合荷重に加えて、中間層の結合荷重を調節する方法。　


ということで、次回実際に何かプログラム動かしてみます！

## Rubyのライブラリを動かしてみる

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

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
```


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

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



### プログラムの内容

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

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


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

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

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

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



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

```ruby
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クラスのインスタンスを生成。引数により入力層、中間層、出力層の数を指定している。

```ruby
# 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回識別を試みていて、最初に学習なしのテストを行い、次に学習を行った後、もう一度識別を試みている。

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

```ruby
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)
```

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

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

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


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

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

次に、手続きだけで表すと学習・学習後の識別の部分は大まかに以下のような流れになっている。

```ruby
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](https://qiita-image-store.s3.amazonaws.com/0/37676/45f95aea-a634-821e-6f25-83fad7c5ab3b.png "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`というファイルを作成した。`../neural_net`の使い方に合わせて準備する手続きを書いただけのスクリプトです。

```ruby
#!/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) do
  predicted = (0..1).max_by {|i| actual[i] }
  ideal[predicted] == 1 
end

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

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

run_test = -> (nn, inputs, expected_outputs) do
  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]
end

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_image.rb                                       

Training the network...


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



### 未知のデータの分類

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

```ruby
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と識別されたことになる。



## まとめ

機械学習系の勉強をするときにRubyでやることってほとんどなかったので楽しかった。

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


## 参考資料

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

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

## 参考記事

- gbuesing/neural-net-ruby

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


- バックプロパゲーション（誤差逆伝播法）の実行時間をRubyとPythonで比較してみました。

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


- ニューラルネットワークで数字を認識するWebアプリを作る(python)

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


- 多層パーセプトロンによる関数近似

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


- Deep Learning (Python, C/C++, Java, Scala, Go) 

https://github.com/yusugomori/DeepLearning