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


集合知プログラミングの第2章の続きをやっていきますよ(`・ω・´)前回は映画に対する評点という嗜好データを定義したので、今回はその類似度の計算をやっていきたいと思います。

似ているユーザを探し出す

嗜好の似ているユーザ同士を探索するためには、全ユーザを比較してその嗜好の類似性スコアを算出するような形で行いますデス。類似計算については幾つかあるのですが、本書では次の2つを利用するみたいです。

ユークリッド距離によるスコア

ユークリッド距離による類似計算は嗜好データをn次元の座標に見立ててやって、その座標間の距離を類似度としてみなすものですね(`・ω・´)まあ、中学校くらいでやった2点間の距離をn次元に拡張したと思えばいいわけですな

例えばある2つのアイテムである商品1, 商品2に対して、2人にユーザA, Bの嗜好値がそれぞれ(a1, a2), (b1, b2)だった場合に、このユークリッド距離は((a1 - b1)^2 + (a2 - b2) ^2)^(1/2)で表現できるわけですネ(^は累乗表現デス)

それでは実際にコードに落としこんで計算してみますが、ある2人のユーザの嗜好値データがそれぞれ(5, 4), (4, 1)だった場合のユークリッド距離は次のような感じで演算できますネ

Pythonの場合は累乗を求めるpow関数と平方根を求めるmathモジュールのsqrt関数を利用することで演算できるみたいですね

>>> from math import sqrt
>>> sqrt(pow(5 - 4, 2) + pow(4 - 1, 2))
3.1622776601683795

Scalaの場合もMathパッケージ内のpow、sqrtの両メソッドを利用することで演算できそうです

scala> import Math.{sqrt, pow}
import Math.{sqrt, pow}

scala> sqrt(pow(5 - 4, 2) + pow(4 - 1, 2)) 
res0: Double = 3.1622776601683795

うん、2次元ユークリッド距離はこんな感じであっさりと計算できるわけですネ、それではこのユークリッド距離を類似度として利用することをかんえてみますデス

ユークリッド距離を利用して類似度を考える場合は、2点間(2つのアイテム)の距離が小さいほど類似度が大きいと考えることが出来るのです(`・ω・´)なのでユークリッド距離の逆数を類似度として扱うのが一般的みたいですネ。

ただし、2つのアイテムの距離が0になってしまった場合0の逆数という無限大演算でエラーになってしまうコトを防ぐために、求めたユークリッド距離に1を加えてやりますデス。そうすることで最大類似度1, 最小類似度が0(全く共通しない)となるのですね。

ちなみにこの0~1の類似度範囲は一般的に観るものですな(´・ω・`)まあ、最大類似度100パーセントとか、みたいに直感的に扱えるからですかね

それでは逆数による類似度計算もコードとして書いてやりますよ

>>> 1 / (1 + sqrt(pow(5 - 4, 2) + pow(4 - 1, 2)))
0.2402530733520421
scala> 1 / (1 + sqrt(pow(5 - 4, 2) + pow(4 - 1, 2)))
res1: Double = 0.240253073352042

それでは、このユークリッド距離による類似度計算を前回定義した映画評点嗜好値データというn次元の嗜好値から計算してやりますよ

  • Pythonで映画評点類似度
# recommendation.pyに追記
from math import sqrt

# person1とperson2のユークリッド距離を基に類似度計算
def sim_distance(prefs, person1, person2):
  # 2人とも評価をしている映画のリストを作成
  si = {}
  for item in prefs[person1]:
    if item in prefs[person2]:
      si[item] = 1
  
  # 2人が共通して評価しているものがなければ類似無しと判定して0とする
  if len(si) == 0:
    return 0
    
  # 軸ごとの全ての座標の差の平方を合計
  sum_of_squares = sum([pow(prefs[person1][item] - prefs[person2][item], 2)
                                      for item in prefs[person1] if item in prefs[person2]])
  
  # 逆数類似度を演算
  return 1 / (1 + sum_of_squares)

とりあえず実行してみますかね

>>> import recommendation
>>> recommendation.sim_distance(recommendation.critics, 'Lisa Rose', 'Gene Seymour')
0.14814814814814814

うん、類似度を計算できました(`・ω・´)ちなみに上記の式はsqrtを使った平方根を計算していないような…(´・ω・`)まあ、使わなくても意味合いは変わらないのでOKなんですが、使うとすればこんな感じですかね

from math import sqrt
def sim_distance(prefs, person1, person2):
  si = {}
  for item in prefs[person1]:
    if item in prefs[person2]:
      si[item] = 1
  if len(si) == 0:
    return 0
  sum_of_squares = sum([pow(prefs[person1][item] - prefs[person2][item], 2)
                                      for item in prefs[person1] if item in prefs[person2]])

  # 合計した値の平方根を使って逆数演算をしますデス
  return 1 / (1 + sqrt(sum_of_squares))


それではScalaで書き下していきましょうかね

  • Scalaで映画評点類似度
// Recommendation.scalaのRecommendationオブジェクトに追記
import Math.{sqrt, pow}
// 類似度距離を求めますよ
def sim_distance(prefs:Map[String, Map[String, Double]], 
  person1:String, person2:String):Double = {

  // 共通する要素がなければ類似度0として終了します
  if(!prefs(person1).exists(p1 => prefs(person2).exists(p2 => p1._1 == p2._1)))
    return 0

  // 共通する要素について差の平方を求めていきます     
  val squares = for{
      p1 <- prefs(person1)
      if(prefs(person2).exists(p2 => p1._1 == p2._1))
    } yield {
      pow(prefs(person1)(p1._1) - prefs(person2)(p1._1), 2)
    }
  // 差の平方の和を取りますです
  val sum_of_squares:Double = squares.sum

  // Pythonのサンプルに合わせて平方根を使わないバージョンで類似を計算します
  1 / (1 + sum_of_squares)
  // ユークリッド距離的に平方根を使う場合はこうですね
  // 1 / (1 + sqrt(sum_of_squares))
}

それではコンパイルして実行してみますよ(`・ω・´)

scala> import org.plasticscafe.collective.recommend.Recommendation
              import org.plasticscafe.collective.recommend.Recommendation

scala> Recommendation.sim_distance(Recommendation.critics,"Lisa Rose", "Gene Seymour")
res0: Double = 0.14814814814814814

おお!できた(゚∀゚)でもPythonコードの翻訳っぽくてアレなんで(個人的に)Scalaっぽい(と思い込んでいる)感じに書き変えてみますよ

import Math.pow

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

    if(!prefs(person1).exists(p1 => prefs(person2).exists(p2 => p1._1 == p2._1)))
      return 0
    
    // 2段階に分けていた処理を高階関数を使ってまとめてしまいます
    val sum_of_squares = prefs(person1).filter(p1 =>
      prefs(person2).exists(p2 => p1._1 == p2._1)).map(p1 =>
       pow(prefs(person1)(p1._1) - prefs(person2)(p1._1), 2)).sum

    1 / (1 + sum_of_squares)
  }
  // 同じオブジェクト内にまとめたんだからショートカット用のメソッドを追加します
  def sim_distance(person1:String, person2:String):Double = {
    sim_distance(critics, person1:String, person2:String)
  }

こちらも実行してみますかね(´・ω・`)

scala> import org.plasticscafe.collective.recommend.Recommendation
import org.plasticscafe.collective.recommend.Recommendation

scala> Recommendation.sim_distance("Lisa Rose", "Gene Seymour")   
res1: Double = 0.14814814814814814

うん、なんとか出来ました(`・ω・´)でも本当にScalaっぽいかどうか不安なのでツッコミ希望デス(´・ω・`)

いじょー

ユークリッド距離でいっぱいいっぱいだったので、ピアソン相関係数は次回に回しますデス(´・ω・`)