Rubyで『集合知プログラミング』(1)

『集合知プログラミング』を読了しました。大変勉強になりました。
Pythonによる書籍内サンプルコードをRubyで書いてみたいと思います。
書籍掲載のサンプルコードはこちらの"Examples"からダウンロードできます。

第2章「推薦を行う」

"recommendations.rb"を実装してみます。

まずディクショナリ。

critics = {
  'Lisa Rose'=>{
    'Lady in the Water'=>2.5,
    'Snakes on a Plane'=>3.5,
    'Just My Luck'=>3.0,
    'Superman Returns'=>3.5,
    'You, Me and Dupree'=>2.5,
    'The Night Listener'=>3.0
  },
  'Gene Seymour'=>{
    'Lady in the Water'=>3.0,
    'Snakes on a Plane'=>3.5,
    'Just My Luck'=>1.5,
    'Superman Returns'=>5.0,
    'The Night Listener'=>3.0,
    'You, Me and Dupree'=>3.5
  },
  'Michael Phillips'=>{
    'Lady in the Water'=>2.5,
    'Snakes on a Plane'=>3.0,
    'Superman Returns'=>3.5,
    'The Night Listener'=>4.0
  },
  'Claudia Puig'=>{
    'Snakes on a Plane'=>3.5,
    'Just My Luck'=>3.0,
    'The Night Listener'=>4.5,
    'Superman Returns'=>4.0,
    'You, Me and Dupree'=>2.5
  },
  'Mick LaSalle'=>{
    'Lady in the Water'=>3.0,
    'Snakes on a Plane'=>4.0,
    'Just My Luck'=>2.0,
    'Superman Returns'=>3.0,
    'The Night Listener'=>3.0,
    'You, Me and Dupree'=>2.0
  },
  'Jack Matthews'=>{
    'Lady in the Water'=>3.0,
    'Snakes on a Plane'=>4.0,
    'The Night Listener'=>3.0,
    'Superman Returns'=>5.0,
    'You, Me and Dupree'=>3.5
  },
  'Toby'=>{
    'Snakes on a Plane'=>4.5,
    'You, Me and Dupree'=>1.0,
    'Superman Returns'=>4.0
  }
}

p1とp2の距離を基にした類似性スコアを計算します。

def sim_distance(prefs, p1, p2)
  # 共通のアイテムを取得
  si = prefs[p1].keys & prefs[p2].keys
  # 共通項がなければ類似性は0
  return 0 if si.length == 0
  # 差の自乗和
  sum_of_squares = si.inject(0) do |sum, item|
    sum + (prefs[p1][item] - prefs[p2][item])**2
  end
  1 / (1 + sum_of_squares)
end

p1とp2のピアソン相関係数を計算します。

def sim_pearson(prefs, p1, p2)
  si = prefs[p1].keys & prefs[p2].keys
  n = si.length
  return 0 if n == 0
  # 合計,自乗和,内積を計算
  sum1 = sum2 = sum_sq1 = sum_sq2 = sum_prod = 0
  si.each do |item|
    sum1 += prefs[p1][item]
    sum2 += prefs[p2][item]
    sum_sq1 += prefs[p1][item]**2
    sum_sq2 += prefs[p2][item]**2
    sum_prod += prefs[p1][item] * prefs[p2][item]
  end
  # ピアソン相関係数を計算
  num = sum_prod - (sum1 * sum2 / n)
  den = Math.sqrt((sum_sq1 - sum1**2 / n) * (sum_sq2 - sum2**2 / n))
  den == 0? 0 : num / den
end

ディクショナリprefsから,personにマッチするアイテムをリストアップします。

def top_matches(prefs, person, n=5, similarity=method(:sim_pearson))
  # person以外の項目について係数を計算
  scores = (prefs.keys - [person]).map do |other|
    [similarity.call(prefs, person, other), other]
  end
  # 係数の降順にn個取得
  scores.sort.reverse[0...n]
end

personへの推薦を算出します。

def get_recommendations(prefs, person, similarity=method(:sim_pearson))
  totals = Hash.new(0)
  sim_sums = Hash.new(0)
  (prefs.keys - [person]).each do |other|
    sim = similarity.call(prefs, person, other)
    # 0以下のスコアは無視
    next if sim <= 0

    prefs[other].each_key do |item|
      # 未評価のみ対象
      if !prefs[person].include?(item) or prefs[person][item] == 0 then
        # 評点の重み付き合計
        totals[item] += prefs[other][item] * sim
        sim_sums[item] += sim
      end
    end
  end
  # 重み付き平均による正規化されたランキング
  rankings = totals.map{|item, total| [total/sim_sums[item], item]}
  rankings.sort.reverse
end

ディクショナリを変換(メインのキーを評者から映画に変更)します。

def transform_prefs(prefs)
  result = {}
  prefs.each_key do |person|
    prefs[person].each_key do |item|
      result[item] ||= {}
      result[item][person] = prefs[person][item]
    end
  end
  result
end

解析に必要なメソッドは揃いました。
テストします。

def get_disp(result)
  result.map{|score,item| "%-20s : %f" % [item,score]}
end

puts "\n# Lisa RoseとGene Seymourの類似度"
puts sim_distance(critics, 'Lisa Rose', 'Gene Seymour')
puts sim_pearson(critics, 'Lisa Rose', 'Gene Seymour')

puts "\n# Tobyと嗜好の似た評者のランキング"
puts get_disp(top_matches(critics, 'Toby'))

puts "\n# Tobyに推薦する映画とTobyの予想評点(ピアソン)"
puts get_disp(get_recommendations(critics, 'Toby'))
puts "\n# Tobyに推薦する映画とTobyの予想評点(距離)"
puts get_disp(get_recommendations(critics, 'Toby', method(:sim_distance)))

puts "\n# Superman Returnsに似た映画のランキング"
puts get_disp(top_matches(transform_prefs(critics), 'Superman Returns'))

puts "\n# Just My Luckに高評価を出しそうな評者とその予想評点"
puts get_disp(get_recommendations(transform_prefs(critics), 'Just My Luck'))

結果はこうなりました。

# Lisa RoseとGene Seymourの類似度
0.148148148148148
0.39605901719067

# Tobyと嗜好の似た評者のランキング
Lisa Rose : 0.991241
Mick LaSalle : 0.924473
Claudia Puig : 0.893405
Jack Matthews : 0.662849
Gene Seymour : 0.381246

# Tobyに推薦する映画とTobyの予想評点(ピアソン)
The Night Listener : 3.347790
Lady in the Water : 2.832550
Just My Luck : 2.530981

# Tobyに推薦する映画とTobyの予想評点(距離)
The Night Listener : 3.500248
Lady in the Water : 2.756124
Just My Luck : 2.461988

# Superman Returnsに似た映画のランキング
You, Me and Dupree : 0.657952
Lady in the Water : 0.487950
Snakes on a Plane : 0.111803
The Night Listener : -0.179847
Just My Luck : -0.422890

# Just My Luckに高評価を出しそうな評者とその予想評点
Michael Phillips : 4.000000
Jack Matthews : 3.000000

2.6節以降は割愛(サボり)