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


Scala de 集合知プログラミングの第2章の続きをやっていきますよ(`・ω・´)

前回は嗜好データからのユーザ類似度を利用したアイテムの推薦という一つの区切り的な処理を行いましたが、今回はコレを応用して似たアイテムの探索を行ってみたいと思います。

似ている製品

前回まではユーザ間の類似度をベースに推薦方法を考えてきましたが、アイテム間の類似度を求める方法について考えてみますデス

ユーザ間の類似度については人を起点としたディクショナリを利用して計算してきたのですが、これをアイテムを起点にしたディクショナリに変換してやることでアイテム間の類似の計算が出来るようになるみたいです

Pythonでアイテム類似計算

まずはPythonサンプルの写経でディクショナリの変換を行ってみますです

変換の対象となるデータ構造はこんな感じで、人に対して各アイテムの嗜好(評価)値がぶら下がっております

{'Lisa Rose': {'Lady in the Water': 2.5, 'Snake on a Plane':3.5},
'Gene Saymour':{'Lady in the Water': 3.0, 'Snake on a Plane':3.5},

これを次のようにアイテムを基準にして、各人の嗜好値データがぶら下がっている形に変換してやります

{'Lady in the Water':{'Lisa Rose':2.5, 'Gene Seymour':3.0},
'Snake on a Plane':{'Lisa Rose':3.5, 'Gene Seymour':3.5}, ...

以前定義したcriticsという嗜好データを書きなおしてやればいいのですが、せっかくコンピュータを使っているので機械的に変換してやります(`・ω・´)

変換コードは次のようになりますネ

# recommendation.pyに追記します

# 嗜好値データの変換処理を行います
def transformPrefs(prefs):
  result={}
  # 嗜好値データごとにループで処理します
  for person in prefs:
    for item in prefs[person]:
      # まだ存在しないキーに付いて初期化
      result.setdefault(item, {})
      # item と personを入れ替えたディクショナリを作成します
      result[item][person] = prefs[person][item]
  # 結果を返します
  return result

とりあえず実行してみますかね(´・ω・`)

>>> import recommendation
# ユーザ→アイテムの嗜好値データをアイテム→ユーザの形式に変換します
>>> recommendation.transformPrefs(recommendation.critics)
{'Lady in the Water': {'Lisa Rose': 2.5, 'Jack Matthews': 3.0, 'Michael Phillips': 2.5, 'Gene Seymour': 3.0, 'Mick LaSalle': 3.0}, 'Snakes on a Plane': {'Jack Matthews': 4.0, 'Mick LaSalle': 4.0, 'Claudia Puig': 3.5, 'Lisa Rose': 3.5, 'Toby': 4.5, 'Gene Seymour': 3.5, 'Michael Phillips': 3.0}, 'Just My Luck': {'Claudia Puig': 3.0, 'Lisa Rose': 3.0, 'Gene Seymour': 1.5, 'Mick LaSalle': 2.0}, 'Superman Returns': {'Jack Matthews': 5.0, 'Mick LaSalle': 3.0, 'Claudia Puig': 4.0, 'Lisa Rose': 3.5, 'Toby': 4.0, 'Gene Seymour': 5.0, 'Michael Phillips': 3.5}, 'The Night Listener': {'Jack Matthews': 3.0, 'Mick LaSalle': 3.0, 'Claudia Puig': 4.5, 'Lisa Rose': 3.0, 'Gene Seymour': 3.0, 'Michael Phillips': 4.0}, 'You Me and Dupree': {'Jack Matthews': 3.5, 'Mick LaSalle': 2.0, 'Claudia Puig': 2.5, 'Lisa Rose': 2.5, 'Toby': 1.0, 'Gene Seymour': 3.5}}

このように変換した辞書を、前回までに作成したtopMatchesやgetRecommendationsに適用することで、似ている製品リストを計算したりオススメのユーザ(評価者)を求めることが出来るようになるわけですね(`・ω・´)

  • 変換した辞書を類似度計算メソッドであるtopMatchesに適用して、指定したアイテムと似ている他のアイテムを探索
    • 似ている映画は何かな?検索なんかに応用できるデス(`・ω・´)
>>> movies = recommendation.transformPrefs(recommendation.critics)
# 指定アイテムとの類似度ランキングを求めます
>>> recommendation.topMatches(movies, 'Superman Returns')
[(0.65795169495976946, 'You Me and Dupree'), (0.48795003647426888, 'Lady in the Water'), (0.11180339887498941, 'Snakes on a Plane'), (-0.17984719479905439, 'The Night Listener'), (-0.42289003161103106, 'Just My Luck')]

なお、マイナスの相関については上の例で考えてみるとSuperman Returnsを好きな人はJust My Luckを嫌う傾向にある…ということを示しているそうです

  • 変換した辞書を推薦メソッドであるgetRecommendationsに適用することで、指定したアイテムに適した評価者を一覧
    • 一緒に映画を見に行く相手を推薦したりできる訳ですね(`・ω・´)
>>> movies = recommendation.transformPrefs(recommendation.critics)
# Just My Luckを一緒に観に行くとしたら誰と?っていう推薦が出来ますね
>>> recommendation.getRecommendations(movies, 'Just My Luck')
[(4.0, 'Michael Phillips'), (3.0, 'Jack Matthews')]
Scalaでアイテム類似計算

それでは本題のScalaコードでの嗜好データ変換&アイテム類似の計算をやってみますよ(`・ω・´)

まずは嗜好値データの変換目標をば…変換前のMapはこんな感じですね

Map("Lisa Rose" -> Map("Lady in the Water" -> 2.5, "Snake on a Plane" -> 3.5),
"Gene Saymour" -> Map("Lady in the Water" -> 3.0, "Snake on a Plane" -> 3.5), ...

変換結果はこんな感じですね…まあ、表記が異なるだけで実質の意味はPythonのときと同じですね

Map("Lady in the Water" -> Map("Lisa Rose" -> 2.5, "Gene Seymour" -> 3.0),
Snake on a Plane" -> Map("Lisa Rose" -> 3.5, "Gene Seymour" -> 3.5) ...

それでは変換用のコードをScalaに翻訳してみますよ

# Recommendation.scalaのRecommendationオブジェクト内に記載します

// 辞書の内容を変換します 
def transformPrefs(prefs:Map[String, Map[String, Double]]):Map[String, Map[String, Double]] = {
    // 変換結果を格納するミュータブルマップを用意します
    import scala.collection.mutable
    val result = mutable.Map.empty[String, mutable.Map[String, Double]]
    // とりあえず展開して
    for(person <- prefs){
      for(item <- person._2){
        // Mapのキーが無い場合は初期化します
        if(!result.isDefinedAt(item._1)) result(item._1) = mutable.Map.empty[String, Double]
        result(item._1)(person._1) = item._2
      }
    }
    // ミュータブルマップをイミュータブルに変換しました
    // そうしないと戻りがmutableになってしまうので(´・ω・`)
    result.map(x => (x._1, x._2.toMap)).toMap
  }
}

mutableなマップをimmutableなMapにするにはtoMapし直せばいいみたい…ですが、どうも2.8からの機能みたいです。

2.7でも同じようなことをやろうとすると次のような感じで変換するのがヨサゲですね
http://stackoverflow.com/questions/2817055/converting-mutable-to-immutable-map

なんとかScalaに翻訳してみたので、とりあえず変換してみますYO!

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

scala> Recommendation.transformPrefs(Recommendation.critics)
res0: Map[String,Map[String,Double]] = Map((Superman Returns,Map(Lisa Rose -> 3.5, Claudia Puig -> 4.0, Michael Phillips -> 3.5, Gene Seymour -> 5.0, Jack Matthews -> 5.0, Toby -> 4.0, Mick LaSalle -> 3.0)), (Lady in the Water,Map(Lisa Rose -> 2.5, Michael Phillips -> 2.5, Gene Seymour -> 3.0, Jack Matthews -> 3.0, Mick LaSalle -> 3.0)), (You Me and Dupree,Map(Lisa Rose -> 2.5, Claudia Puig -> 2.5, Gene Seymour -> 3.5, Jack Matthews -> 3.5, Toby -> 1.0, Mick LaSalle -> 2.0)), (Just My Luck,Map(Mick LaSalle -> 2.0, Claudia Puig -> 3.0, Gene Seymour -> 1.5, Lisa Rose -> 3.0)), (The Night Listener,Map(Lisa Rose -> 3.0, Claudia Puig -> 4.5, Michael Phillips -> 4.0, Gene Seymour -> 3.0, Jack Matthews -> 3.0, Mick LaSalle -> 3.0)), (Snakes on a Plane,Map(Lisa Rose -> 3.5, Claudia Puig -> 3.5,...

それではPythonの時と同様にアイテムベースの推薦処理をやってみます(`・ω・´)

まずは指定したアイテムとの類似度ランキングを計算してみますよ

// 嗜好値データを変換します
scala> val movies = Recommendation.transformPrefs(Recommendation.critics)

// しっかり計算できておりますね(`・ω・´)
scala> Recommendation.topMatches(movies, "Superman Returns")
res1: List[(Double, String)] = List((0.6579516949597696,You Me and Dupree), (0.4879500364742689,Lady in the Water), (0.11180339887498941,Snakes on a Plane), (-0.1798471947990544,The Night Listener), (-0.42289003161103106,Just My Luck))

次に特定のアイテムにたいするオススメユーザを推薦してやります

scala> val movies = Recommendation.transformPrefs(Recommendation.critics)

// こちらもOKデスネ
scala> Recommendation.getRecommendations(movies, "Just My Luck")
res3: List[(Double, String)] = List((4.0,Michael Phillips), (3.0,Jack Matthews))
Scalaっぽいコードを求め続ける自己満足の道

いつもどおりイミュータブルを使わないというコードを考えてやりますです(´・ω・`)That's 自己満足

それではやってみますよ

def transformPrefs(prefs:Map[String, Map[String, Double]]):Map[String, Map[String, Double]] = {
  // 再帰処理を使って展開した形式を
  // 新しく(アイテム→ユーザ)の2重入れ子形式に編成してやります
  def convert(persons:List[(String, String, Double)], 
    items:Map[String, Map[String, Double]]
     = Map.empty[String, Map[String, Double]]):Map[String, Map[String, Double]] = {      
    // 処理用に展開したリストがなくなれば終了
    if(persons.isEmpty){
      items
    }else {
      //// 再帰処理を行います
      // 先頭だけ取り除いて処理続行です
      convert(persons.tail, 
        //// 取り出した先頭要素をリターン用のMapに組み込んでやります
        // リターン用のMapの第1要素にアイテムキーが存在していなければ追加シマス
        if(!items.isDefinedAt(persons.head._1)){
          items + (persons.head._1 -> Map(persons.head._2 -> persons.head._3))
        // リターン用のMapの第1要素にアイテムキーが存在していた場合は
        // 第二要素(内側のMap)に新しい値の組み合わせを追加します
        }else{
          // どの第1要素に追加するかをmapで判定し 
          items.map{case(item, values) => 
            // 追加すべき要素が見つかった場合はMapの要素を結合します
            if(item == persons.head._1){
              item -> (values ++ Map(persons.head._2 -> persons.head._3))
            // 該当しない要素であればそのままリターンします
            }else{ item -> values }}
        })
    }
  }
  // Mapが入れ子になっていると処理しづらいので
  // 入れ子のMap構造を(アイテム名、ユーザ名、評価値)の形式に展開してしまいます 
  val flatten = prefs.flatMap(person => person._2.map(item => (item._1, person._1, item._2)))
  // 展開した内容をリストに置き換えて再帰処理で変換します
  convert(flatten.toList)  
}

ふうー、我ながら計算効率の悪いコードだぜ(´・ω・`)絶対にもっとスマートに書けるはずなので、良い書き方があったら教えてください…今回はコレが精一杯でした...orz

参考にしたのはゆろよろさんとこデス
http://d.hatena.ne.jp/yuroyoro/20100317/1268819400

...結果はともあれ、入れ子のMap要素の結合は次のようにすると良いというのがわかったのが収穫デス(`・ω・´)

// キー -> (要素の結合を ++ 演算子で記述)の形式で書けるみたいです
item -> (values ++ Map(persons.head._2 -> persons.head._3))

まとめ

とりあえず、なんとかかんとかアイテム類似度計算まで出来ました。日に日にvarを使わない課題がきつくなったきておりますが…まあ、修行だと思って出来だけ頑張ってみよう…かな...と(´・ω・`)あくまでも、できるとこまでで

とりあえず次回は実際のWeb上のデータを利用して集合知ってことでdel.icio.usAPIを叩いて遊んでみたいと思いマス