Scalaで集合知プログラミング その4


Scala de 集合知プログラミングの第2章を続けていきますよ(`・ω・´)前回はユークリッド距離を利用して嗜好値データの類似度計算を行ったのですが、今回はピアソン相関係数を利用した類似度計算をやってみたいと思います

似ているユーザを探し出す…の続き

類似性の計算でユークリッド距離よりもヨサゲな値が出せるという噂のピアソン相関係数をやっていきたいと思います…なにやら複雑そうですが、頑張っていきますかね(´・ω・`)

ピアソン相関によるスコア

正規化されていないデータを扱う場合などはユークリッド距離よりも良い結果が出せるピアソン相関を扱っていきたいと思います。ここでいう「正規化」は統計的な意味合いで、正規化出来ていない→数値が極端にばらついている状態っていう解釈でいいですかね?

ちなみに本書内では正規化できていない→「たとえば映画の評価が平均より厳しい場合など」という例がだされておりますね。

なおピアソン相関係数をざっくりと説明すると、2つの変数(2人の評者の各映画の評点)にどの程度の直線的な関係があるかどうかを数値化する手法みたいデス…まあ、詳しくはwikipedia:相関係数ってことで(もしくは詳しい人の解説を待つのです)

追記:コメント欄でid:hrsttがここらへんの説明をしてくれております。近いうちに素敵な統計入門記事を作ってくれるに違いないデス(゚∀゚)

とりあえず映画嗜好データのピアソン相関をもとめるアルゴリズムを書きだしてみマスです(`・ω・´)

  • 両方の評者が得点を付けているアイテムを探索
  • それぞれの評者の評点の合計と評点の平方の合計を算出
  • 各評価をかけ合わせた値の合計を算出
  • ピアソンの相関係数を求める式をつかって類似度を計算

ちなみに計算式はこんな感じになるみたいです
\frac{ \displaystyle \sum_{i=1}^{n} (x_{i}-\bar{x})(y_{i}-\bar{y}) }{ \displaystyle \sqrt{\sum_{i=1}^n(x_{i}-\bar{x})^2} \sqrt{\sum_{i=1}^n(y_{i}-\bar{y})^2}}

bar{x}, bar{y}は相加平均っすね(´・ω・`)

まずはサンプルにあるPythonのコードを写経していきますよ

# recommendation.pyに追記

# p1とp2のピアソン相関係数を計算する処理
def sim_pearson(prefs, p1, p2):
  # 両者が共通して評価しているアイテムのリストを取得
  si = {}
  for item in prefs[p1]:
    if item in prefs[p2]:
      si[item] = 1
  
  # 共通して評価しているアイテムがなければ0を返して終了
  n = len(si)  
  if n == 0:
    return 0
    
  # 全てのアイテムの評価を合計
  sum1 = sum([prefs[p1][it] for it in si])
  sum2 = sum([prefs[p2][it] for it in si])
  
  # 全てのアイテムの評価の平方を合計
  sum1Sq = sum([pow(prefs[p1][it], 2) for it in si])
  sum2Sq = sum([pow(prefs[p2][it], 2) for it in si])
  
  # 積を合計
  pSum = sum([prefs[p1][it] * prefs[p2][it] for it in si])
  
  ### ピアソンによるスコアを計算
  # 分子部分を求めます
  num = pSum - (sum1 * sum2 / n)
  # 分母部分を求めます
  den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n))
  # 分母部分が0になった場合は計算不能で0を返します
  if den == 0:
    return 0
  # ピアソンスコアを求めます(`・ω・´)
  r = num / den
  # 値を返します
  return r

それでは実行してみますよ(`・ω・´)

# モジュールを読み込んで
>>> import recommendation
# ピアソン相関係数を計算してやります
>>> print recommendation.sim_pearson(recommendation.critics, 'Lisa Rose', 'Gene Seymour')
0.396059017191

うん、無事に計算できました(`・ω・´)それでは本題のScala版コードを書いていきたいと思います

それでは上記PythonコードをScalaに書きなおしてみますかね

// Recommendation.scalaのRecommendationオブジェクトに追記

// ピアソン相関係数を求める式を定義します
def sim_pearson(prefs:Map[String, Map[String, Double]],
  p1:String, p2:String):Double = {

  // 共通するアイテム一覧を探索
  //// 後々使い回すのでキャッシュ的な意味で計算します
  val si = prefs(p1).filter(x => prefs(p2).contains(x._1)).map(_._1)

  // 共通するアイテムがなければ類似度0として終了します
  ////  アイテム数もあとで使うので変数に確保シマス(´・ω・`)
  val n = si.size
  if(n == 0) return 0

  // 全ての共通嗜好値の合計を計算します
  val sum1 = si.map(prefs(p1)(_)).sum
  val sum2 = si.map(prefs(p2)(_)).sum

  // 平方の合計を計算してやります
  val sum1Sq = si.map(x => pow(prefs(p1)(x), 2)).sum
  val sum2Sq = si.map(x => pow(prefs(p2)(x), 2)).sum
    
  // 積の合計を計算します
  val pSum =  si.map(x =>  prefs(p1)(x) * prefs(p2)(x)).sum
  
  // ピアソンスコアを計算します
  val num = pSum - (sum1 * sum2 / n)
  val den = sqrt(sum1Sq - pow(sum1, 2) / n ) * sqrt(sum2Sq - pow(sum2, 2) / n )
  // 分母が0になったら誤りということで0を返します
  if(den == 0)  return 0
  // 値を返します
  num / den
}

なんとか書き下せたみたいなので実行してみますよ(`・ω・´)

// モジュールをインポートします
scala> import org.plasticscafe.collective.recommend._                           
import org.plasticscafe.collective.recommend._
// 実行してみますよ(`・ω・´)
scala> Recommendation.sim_pearson(Recommendation.critics, "Lisa Rose", "Gene Seymour")
res2: Double = 0.39605901719066977

うん、出来ましたね(`・ω・´)ちなみに類似の値は-1 ~ 1を返すみたいで「完全に一致」の場合には1になるみたいです

なお、書き始めで下のような冗長コードを書いていたのでチョット反省しております。シンプルコードを心がけなくちゃ(´・ω・`)

// 共通アイテムを取得する処理をexistsを使って書いていた(´・ω・`)
val si = prefs(p1).filter(x => prefs(p2).exists(y => y._1 == x._1)).map(_._1)
//// ループの数も(多分)少なくなるしこっちのほうがスッキリ書けるのデス
// val si = prefs(p1).filter(x => prefs(p2).contains(x._1)).map(_._1)

// 合計値の計算処理もなんかごちゃごちゃやってます
val sum2 = prefs(p2).filter(x => si.contains(x._1)).map(_._2).sum
//// 圧倒的にシンプルになりまった(´・ω・`)計算コストも小さくなったはず
// val sum2 = si.map(prefs(p2)(_)).sum

…と、ココマデやってみて前回書いたユークリッド距離のコードもちょっぴりリファクタリングしてみたのです

import Math.pow

  def sim_distance(prefs:Map[String, Map[String, Double]],
    person1:String, person2:String):Double = {

    // 共通するアイテム一覧を探索
    val si = prefs(person1).filter(x => prefs(person2).contains(x._1)).map(_._1)
    // 共通するアイテムがなければ類似度0として終了します
    if(si.size == 0) return 0

    // 高階関数もキャッシュ変数を使ってシンプルに
    val sum_of_squares = si.map(x => 
      pow(prefs(person1)(x) - prefs(person2)(x), 2)).sum
    
    1 / (1 + sum_of_squares)
  }

うん、だいぶスッキリしましたデス(`・ω・´)

どちらの類似性尺度を利用すべきなのか?

本書ではユークリッドとピアソン相関係数の2つの類似性尺度を取り上げていますが、類似性尺度はコレ以外にもJaccard係数やマンハッタン距離等々、たくさんある上にどれを使うのがいいの?ってことはケースバイケースみたいです。

なのでイロイロ試してみてベストなものを探すのが良いよ!とのことです(´・ω・`)

評者をランキングする

ここまでは特定の2人を比較して類似度を計算する方法を観てきたのですが、こいつを利用してとある評者との類似度ランキングを計算できるようにシマスです。

具体的には次のような手順で行うことになります

  • 指定された評者と他の全評者の嗜好値を比較して類似度を計算
  • 計算した類似度をリスト化
  • 作成したリストを類似度の高い順にソート

とりあえず好みの似ている(類似度の高い)他の評者を取得する関数をPythonで記述してみますデス

  • Pythonで類似度ランキング
# recommendation.pyに追記

# 指定した評者と類似度の高い評者ランキングを作成します
### 取得ランキング結果、類似性評価関数を指定できるようにしていますデス
def topMatches(prefs, person, n=5, similarity=sim_pearson):
  # 類似性スコアをリスト化
  scores = [(similarity(prefs, person, other) , other)
    for other in prefs if other != person]
  # 高スコアなものが上位に来るように並び替え
  ## 昇順ソートを逆転してやります(`・ω・´)
  scores.sort()
  scores.reverse()
  # 指定された分の値を返します
  return scores[0:n]

それでは実行しますね(´・ω・`)

# モジュールをリロードしてやります
>>> reload(recommendation)<module 'recommendation' from 'recommendation.pyc'>
# ランキングを計算しますよ
>>> recommendation.topMatches(recommendation.critics, 'Toby', n=3)
# 計算できました
[(0.99124070716192991, 'Lisa Rose'), (0.92447345164190486, 'Mick LaSalle'), (0.89340514744156474, 'Claudia Puig')]

うん、無事計算できたので今度はScala版のコードを書いてみますデス

  • Scalaで類似度ランキング
// ランキング結果を取得するメソッドです
def topMatches(prefs:Map[String, Map[String, Double]], person:String, n:Int = 5,
  similarity:(Map[String, Map[String, Double]], String, String) => Double =
  Recommendation.sim_pearson):List[(Double,String)]  = {
  // 高階関数を使って類似度計算、ソート、抽出を一括処理します
  prefs.filter(other => other._1 != person).
    map(other => (similarity(prefs, person, other._1),other._1)).
    toList.sort(_._1 > _._1).take(n)
}

それでは実行してみますね(´・ω・`)

// モジュールを読み込みます
scala> import org.plasticscafe.collective.recommend._                
import org.plasticscafe.collective.recommend._
// ランキングを取得しますよ
scala> Recommendation.topMatches(Recommendation.critics, "Toby", n=3)
// 無事ランキング結果を取得できました(`・ω・´)
res0: List[(Double, String)] = List((0.9912407071619297,Lisa Rose), (0.9244734516419048,Mick LaSalle), (0.8934051474415644,Claudia Puig))

ちなみにこいつを次のようにカリー化してしまって類似度計算方法だけを切り替えて遊ぶってのもありかも知れないなぁ…とふと思ったのでやってみます

// カリー化してみますよ
def topMatchesCurry(prefs:Map[String, Map[String, Double]], person:String, n:Int = 5)
  (similarity:(Map[String, Map[String, Double]], String, String) => Double):List[(Double,String)]  = {
  prefs.filter(other => other._1 != person).
    map(other => (similarity(prefs, person, other._1),other._1)).
    toList.sort(_._1 > _._1).take(n)
}

んじゃ、ちょっくら使ってみます

scala> import org.plasticscafe.collective.recommend._                                    
import org.plasticscafe.collective.recommend._
// 類似度演算の箇所以外を埋めた部分適用関数を用意します(`・ω・´)。
scala> val toby_ranking = Recommendation.topMatchesCurry(Recommendation.critics, "Toby")_
toby_ranking: ((Map[String,Map[String,Double]], String, String) => Double) => List[(Double, String)] = <function1>

// まずはピアソン相関係数で計算しますよ
scala> toby_ranking(Recommendation.sim_pearson)                                          
res0: List[(Double, String)] = List((0.9912407071619297,Lisa Rose), (0.9244734516419048,Mick LaSalle), (0.8934051474415644,Claudia Puig), (0.6628489803598698,Jack Matthews), (0.38124642583151164,Gene Seymour))

// 今度はユークリッド距離で計算してやります
scala> toby_ranking(Recommendation.sim_distance)                                         
res1: List[(Double, String)] = List((0.3076923076923077,Mick LaSalle), (0.2857142857142857,Michael Phillips), (0.23529411764705882,Claudia Puig), (0.2222222222222222,Lisa Rose), (0.11764705882352941,Jack Matthews))

うん、それぞれの類似度計算の違いがはっきりわかって面白いっすな(´・ω・`)

いじょー

とりあえず類似度計算→ランキングまで終わりです。次回はこれらを利用したアイテムの推薦に入っていきますよ(`・ω・´)