NaiveBayesをRubyで書いてみた

前回Perlで書いたMultivariate Berounoulli Naive BayesをRubyで書いた.多重ハッシュがうまく動かず断念.寝て起きて手直ししたらすぐに動いた.謎.さすがRuby,見た目が綺麗(コードは汚いが).Rubyできちんと?プログラミングするの初めてなので,思ったこと

  • 多重ハッシュを使う際にハッシュの初期化が面倒(テクを知らないだけ?)
  • オブジェクト指向だとどうしてもクラスキャストをしたくなる,入門書には書いてないぞ
  • to_fしないと勝手に切り捨て.Perlに慣れていたのではまった
  • エラーがわかりづらい
  • コンストラクタinitializeはどう考えてもtypo量産だろう(事実すでに2回これではまった)

はい,どれも自分の修行不足によるものです.
Hash.new(0)のようにハッシュの初期値を決めておけるのはかなり便利だけれど,どのクラスのインスタンスなのかを意識しなければいけない弊害.その点Perlスカラー,配列,ハッシュとそれらのインスタンスしかないから,ある意味楽.

昔は生理的に毛嫌いしていたけれど,いつの間にか行末にセミコロンを書かなくてもすんなり書けるようになっていた.純オブジェクト指向なので,色々と弊害がありそう.まだ中間コンパイルしない純インタプリタなので,実行速度に課題がありそう.
かなり綺麗に書けるので,確かにプログラミングが楽しくなるかもしれない.なんだかプラモデルを作っているような気分.まだ不慣れなのでこれでがっつり書く気にはなれないが.Lisp脳になったら書き方ががらっと変わるんだろうなぁ.

そーすこーど

class NaiveBayesSimple

  def initialize
    @frequency_table = Hash.new()       # Frequency table for each class
    @word_table = Hash.new()            # Word feature table
    @instance_count_of = Hash.new(0)    # Instance count of each class
    @total_count = 0                    # Total instance count
  end

  def add_instance(document)
    # If frequency table does NOT have the label hash, add it
    unless @frequency_table.has_key?(document.label) then
      @frequency_table[document.label] = Hash.new(0)
    end

    # Add instance attributes into selected frequency table
    document.attributes.each{|word, frequency|
      @frequency_table[document.label][word] += 1   # Multivariate Berounoulli
      # @frequency_table[document.label][word] += frequency  # Multinomial

      @word_table[word] = 1   # add to word table
    }

    # Add instance count
    @instance_count_of[document.label] += 1
    @total_count += 1
  end

  def classify(attributes)
    # Local variables
    class_prior_of = Hash.new(1)
    likelihood_of = Hash.new(1)
    class_posterior_of = Hash.new(1)
    evidence = 0

    # Calculate class prior for each label
    @instance_count_of.each{|label,freq|
      class_prior_of[label] = freq.to_f / @total_count.to_f
    }

    # Calculate likelihood for each label
    @frequency_table.each_key{|label|

      # Initialize likelihood
      likelihood_of[label] = 1

      @word_table.each_key{|word|

        # Calculate word likelihood by laplace correction
        laplace_word_likelihood = (@frequency_table[label][word] + 1).to_f /
                 (@instance_count_of[label] + @word_table.size()).to_f

        if attributes.has_key?(word) then
          likelihood_of[label] *= laplace_word_likelihood
        else
          likelihood_of[label] *= (1 - laplace_word_likelihood)
        end
      }

      # Calculate likelihood and add to evidence
      class_posterior_of[label] = class_prior_of[label] * likelihood_of[label]
      evidence += class_posterior_of[label]
    }

    # Regularize probability
    class_posterior_of.each{|label, posterior|
      class_posterior_of[label] = posterior / evidence
    }

    # Print result
    p attributes
    class_posterior_of.each{|label, posterior|
      print "#{label}:#{posterior}\n"
    }
    puts "---"
  end
end

class Document
  attr_reader :label
  attr_reader :attributes

  def initialize(label, attributes)
    @label = label           # String
    @attributes = attributes # Hash
  end
end


# Main
classifier = NaiveBayesSimple.new();

# Train
classifier.add_instance(Document.new("positive", {"hoge" => 2, "piyo" => 1}))
classifier.add_instance(Document.new("negative", {"foo" => 2, "bar" => 2}))

# Test
classifier.classify({"hoge" => 1, "piyo" => 1})
classifier.classify({"foo" => 1, "bar" => 1})