2015年8月26日水曜日

Rubyでパーセプトロンを実装

パーセプトロンを学んだので、実際に書いてみたいと思います。

やはり手を動かすのが一番なので、今後も機械学習の勉強をしながら、実装できそうだなと思ったところはちょくちょくRubyで書いていくつもりです。



実現したいこと

今回は簡単のために、クラス数は2つ、特徴空間は1次元のものを考えていきます。クラス数が2つだとクラスごと識別関数を用意してその最大値選択をするのではなく、

g(x) = g1(x) - g2(x)

のようにして、ひとつの識別関数の符号によってクラスを判別するようにします。( g(x)が正ならクラス1、負ならクラス2のようになる ) クラス1、クラス2にそれぞれ属する学習パターンを与え、それらからパターンを正しく識別できる重みベクトルを誤り訂正法によって求めます。


ソースコード

class Perceptron

  RHO = 1
  attr_accessor :weight_vector, :log

  def initialize( learning_patterns )
    @learning_patterns = learning_patterns.map do |patterns|
      patterns.map { |pattern| [1, pattern] }
    end
    @weight_vector = [1, 1]
    @log = []
  end

  # 識別関数 g(x)
  def discriminate( pattern )
    @weight_vector.zip( pattern ).inject(0) do |sum, wx|
      sum + wx.inject(:*)
    end
  end

  # 誤識別があった場合重みベクトルを修正する
  def correct_errors
    @learning_patterns.size.times do |i|
      @learning_patterns[i].each do |pattern|
        case i
        # w' = w + ρx
        when ->(c) { c == 0 && discriminate( pattern ) <= 0 }
          @log << @weight_vector
          @weight_vector =
            @weight_vector.zip( pattern.map { |e| RHO * e } )
            .map { |a| a.inject(:+) }

        # w' = w - ρx
        when ->(c) { c == 1 && discriminate( pattern ) >= 0 }
          @log << @weight_vector
          @weight_vector =
            @weight_vector.zip( pattern.map { |e| RHO * e } )
            .map { |a| a.inject(:-) }
        end
      end
    end
  end

  # 重みベクトルが更新されなくなるまで誤り訂正を繰り返す
  def learn
    prev = nil
    while prev != @weight_vector
      prev = @weight_vector
      correct_errors
    end
    @log << @weight_vector
  end
end


アルゴリズム

perceptron.rb の処理の流れについて
  1. 重みベクトルの初期値を適当に決める
  2. 各学習パターンについて、識別に誤りがあった場合重みベクトルを修正する
  3. 2を重みベクトルが修正されなくなるまで繰り返す

ちなみに重みベクトルの修正は
  • クラス1のパターンを2と誤識別したとき w' = w + ρx
  • クラス1のパターンを2と誤識別したとき w' = w - ρx
のように行います(ちなみにρの値はPerceptronクラスの定数RHOの値)。xは超平面g(x)=0と直交しているので、wを各超平面と直交する方向に移動させることができます。→実行結果の図3を参照


実行結果

https://gist.github.com/seinosuke/eea033ac85025ed0a20e
学習結果がどのようになるのかを確認するために、特徴空間と重み空間をプロットする例を上記のリンク先においておきました。gnuplotを使用したので、インストールしていない方は入れておいてください。

学習パターンは図1に示すような点を入力してみます。
クラス1が 12, 5, 2, -2 (右側4つの点)、
クラス2が -4, -6, -15 (左側3つの点)です。
※このとき与える学習パターンが線形分離不可能だと、実行が終わらないので注意してください。

図1 各学習パターン

$ ruby main.rb
実行すると以下のような2つの画像が出力されます。

図2 特徴空間

図3 重み空間

今回の例では特徴空間が1次元(数直線)なので、重み空間は2次元です。

図2では各パターンを学習した結果として、ふたつのクラスの境目を緑の線で示してみました。右4つの点はクラス1、左3つの点はクラス2なので確かに分離できています。この分離点は重みベクトルを (w1, w0) とすると w0 + w1*x = 0 より x = -w0/w1 として求められます。

一方図3は重み空間上で重みベクトルが移動した様子を表していて、初期値の (1, 1) から更新を繰り返し最終的に (1, 3) に到達していることがわかります。今回の例では超平面の w0 + w1*(-2) = 0 と w0 + w1*(-4) = 0 の間が解領域であり、学習によって到達した (1, 3) は確かにその領域内の点です。


おわりに

今回の実装例は完全にクラス数2、特徴空間1次元用なのでそれ以外のデータには対応していません。クラス数が2より大きいものなど、いろいろなものに対応できるものもつくってみたいです。

※追記 2015/09/09
http://syoshinsyakangeisagi.blogspot.com/2015/09/ruby.html
改良版を書いてみました。多分例とかはこっちの方がわかりやすくなってると思います。

参考文献
石井健一郎・前田英作・上田修功・村瀬洋 (1998) 『わかりやすいパターン認識』 オーム社

0 件のコメント:

コメントを投稿