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の類似度2.6節以降は割愛(サボり)
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