レコメンドなどで使われる類似度計算をRubyで自動化


レコメンドやデータ分析に使われることも多い類似度計算を簡単にできるmoduleを作りました。

対応している類似度は以下の3つ。

1. コサイン類似度
2. JacCard係数
3. Dice係数

多少汎用的に算出ができるように、配列とハッシュに対応。

① 二つの配列を渡すと類似度が返ってくる
② 二つのハッシュを渡すと類似度が返ってくる
③ モデルとカラムを渡せば類似度を全てのレコードの組み合わせの類似度を算出してくれる(未完成)

※③は、例えばUserテーブルを持っていたとしたら、Userというクラス名と、Userモデルから取得できるデータやカラム名と重みを渡すだけで、user-user全ての類似度を算出してくれたら便利だなと思って作ってみました。


### ①、②については以下のsimilarity_generator.rbで算出できる

```similarity_generator.rb

module SimilarityGenerator

  #data1,data2に配列かハッシュを渡すと類似度が返る
  def calculate_similarity(data1,data2,type="cosine")
    if data1.class==Array
	  calculate_similarity_with_array(data1,data2,type)
    elsif data1.class==Hash
	  calculate_similarity_with_hash(data1,data2,type)
    end
  end

  #vector1とvector2に同じ長さの数列(要素が数字の配列)を渡すと類似度が返る
  #第三引数のtypeに"cosine","jaccard","dice"のいずれかを指定する(default="cosine")
  def calculate_similarity_with_array(vector1,vector2,type="cosine")
    if type=="cosine"	
      #コサイン類似度を計算
      similarity = cosine_similarity(vector1,vector2)
    elsif type=="jaccard"
      #Jaccard係数を計算
      similarity = jaccard_similarity(vector1,vector2)
    elsif type=="dice"
      #Dice係数を計算
      similarity = dice_similarity(vector1,vector2)
    end
    return similarity #類似度を返す
  end

  #hash1とhash2に比較したいhashを渡すと類似度が返る
  #二つのhashは異なる長さ、keyを持っていてもkeyを統合して長さが調整される
  def calculate_similarity_with_hash(hash1,hash2,type="cosine")
    hash3 = hash1.merge(hash2)
    hash3.each do |key,value|
      hash1[key] = 0 if hash1[key].blank?
      hash2[key] = 0 if hash2[key].blank?
    end
    vector1 = hash1.sort.map{|key,val|val}
    vector2 = hash2.sort.map{|key,val|val}
    if type=="cosine"	
      #コサイン類似度を計算
      similarity = cosine_similarity(vector1,vector2)
    elsif type=="jaccard"
      #Jaccard係数を計算
      similarity = jaccard_similarity(vector1,vector2)
    elsif type=="dice"
      #Dice係数を計算
      similarity = dice_similarity(vector1,vector2)
    end
    return similarity #類似度を返す
  end

  #コサイン類似度[START]
  def cosine_similarity(vector1, vector2)
    dp = dot_product(vector1, vector2)
    nm = normalize(vector1) * normalize(vector2)
    dp / nm
  end

  def dot_product(vector1, vector2)
    sum = 0.0
    vector1.each_with_index{ |val, i| sum += val*vector2[i] }
    sum
  end

  def normalize(vector)
    Math.sqrt(vector.inject(0.0){ |m,o| m += o**2 })
  end
  #コサイン類似度[END]

  #Jaccard係数[START]
  def jaccard_similarity(vector1,vector2)
    numerator = 0
    denominator = 0

    vector1.each_with_index do |val1,index|
      val2 = vector2[index]
      numerator += [val1,val2].min
      denominator += [val1,val2].max
    end
    return denominator != 0 ? numerator.to_f / denominator : 0
  end
  #Jaccard係数[END]

  #Dice係数[START]
  def dice_similarity(vector1,vector2)
    numerator = 0
    denominator = 0
    vector1.each_with_index do |val1,index|
      val2 = vector2[index]
      numerator += [val1,val2].min
      denominator += val1+val2
    end
    return denominator != 0 ? 2 * numerator.to_f / denominator : 0
  end
  #Dice係数[END]
end

```

### ③についてはsimilarity_generator.rbに以下を追加する
※未完成なので、うまく使わないと実用性はありません。

```similarity_generator.rb

module SimilarityGenerator

  #(以下を追加)

  def calculate_similarity(class_name, columns={}, type="cosine")
    
    similarity_matrix = []
    
    class_name.find_each do |obj1|

      similarity_matrix_child = []
      obj1_ary = []

      columns.each do |column|
        begin
          obj1_ary << eval("obj1.#{column[0].to_s}").to_i * column[1].to_i
        rescue
          obj1_ary << 0
        end
      end

      class_name.find_each do |obj2|
        obj2_ary = []
        columns.each do |column|
          begin
            obj2_ary << eval("obj2.#{column[0].to_s}").to_i * column[1].to_i
          rescue
            obj2_ary << 0
          end
        end

        if type=="cosine"
          #obj1とobj2のコサイン類似度を配列に入れる
          similarity_matrix_child << cosine_similarity(obj1_ary,obj2_ary)
        elsif type=="jaccard"
          #obj1とobj2のJaccard係数を配列に入れる
          similarity_matrix_child << jaccard_similarity(obj1_ary,obj2_ary)

        elsif type=="dice"
          #obj1とobj2のDice係数を配列に入れる
          similarity_matrix_child << dice_similarity(obj1_ary,obj2_ary)
        end
      end
      similarity_matrix << similarity_matrix_child
    end
    return similarity_matrix #最終的に行列の形で類似度を返す
  end
end

```