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


Scala de 集合知プログラミングの第2章を進めますよ(`・ω・´)

今回は前回までにやってきた推薦手法を実際に提供されている大規模データに適用させてみますYO!

MovieLensのデータセットを使う

ミネソタ大学のGroupLens Projectによって、作られた映画評価のデータセットであるMovieLensのデータを使ってみます。

MovieLens Data Sets

ちなみにGroupLens Projectは協調フィルタリングに関する研究プロジェクトで、その映画版フィルタリングとしてのMovieLensが開発されているみたいです。

Movie Lens

今回は公開されているMovieLens用データセットを使って推薦を行ってみます(`・ω・´)

MovieLens DataSetsはいくつかのデータを持っているのですが、今回は評価値データであるu.dataと映画のデータであるu.itemの2つを利用します。

  • u.data
    • ユーザID, 映画ID, ユーザによる映画絵の評価、タイムスタンプで構成
196     242     3       881250949
186     302     3       891717742
22      377     1       878887116
  • u.item
  • 映画ID、映画名、リリース日…などのデータで構成されています
1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0

MovieLensは(評価数が)100K, 1M, 10Mなどの複数規模のデータが公開されているのですが、今回は一番規模の小さい100K(映画数1682, ユーザ数943)を利用したいと思います。

…ということで、とりあえずファイルをダウンロードして適当な位置に保存してやりますよ

PythonでMovieLens

まずはPythonを使ってMovieLensのデータから、本書で作成した推薦処理に適用できる形のデータセットに変換する処理をサンプル写経として書いていきますよ(`・ω・´)

# recommendation.pyに追記します

# MovieLensのdata Setsを読み込みます
def loadMovieLens(path='data/ml-data'):
  # 映画のタイトルを取得する処理です
  movies = {}
  for line in open(path + '/u.item'):
    # u.item内のデータから映画IDと映画名のペアを作成していきます
    (id, title) = line.split('|')[0:2]
    movies[id] = title
  
  # 実際の評価値データを変換していきます
  prefs = {}
  for line in open(path + '/u.data'):
    #  評価値データからユーザ→アイテム形式の辞書データを生成シマス
    (user, movieid, rating, ts) = line.split('\t')
    prefs.setdefault(user, {})
    prefs[user][movies[movieid]] = float(rating)
  return prefs

作成するデータの構造を見てみると、ユーザベースの協調フィルタリングを実現するみたいですね。

それでは実行してみますよ

# パッケージをインポートします
>>> from recommendation import *
# データセットを変換しますよ(`・ω・´)
>>> prefs = loadMovieLens()
# うん、出来ました
>>> prefs['87']
{'Birdcage, The (1996)': 4.0, 'E.T. the Extra-Terrestrial (1982)': 3.0, 'Bananas (1971)': 5.0, 'Sting, The (1973)': 5.0, 'Bad Boys (1995)': 4.0, 'In the Line of Fire (1993)': 5.0, 'Star Trek: The Wrath of Khan (1982)': 5.0, 'Speechless (1994
<省略>

せっかくなので、このデータを使って実際に推薦を行って見ます(´・ω・`)

# 作成したデータを使って推薦処理を実行します
>>> getRecommendations(prefs, '87')[0:10]
[(5.0, 'They Made Me a Criminal (1939)'), (5.0, 'Star Kid (1997)'), (5.0, 'Santa with Muscles (1996)'), (5.0, 'Saint of Fort Washington, The (1993)'), (5.0, 'Marlene Dietrich: Shadow and Light (1996) '), (5.0, 'Great Day in Harlem, A (1994)'), (5.0, 'Entertaining Angels: The Dorothy Day Story (1996)'), (5.0, 'Boys, Les (1997)'), (4.8988444312892296, 'Legal Deceit (1997)'), (4.815019082242709, 'Letter From Death Row, A (1998)')]

うん、できました(`・ω・´)ついでにアイテムベースの推薦もやってみますよ

# アイテム間の類似度データセットを作成してやりますよ
>>> itemsim = calculateSimilarItems(prefs)
100 / 1664
200 / 1664
<省略>

# アイテムベースの推薦を行います
>>> getRecommendItems(prefs, itemsim, '87')[0:10]
[(5.0, "What's Eating Gilbert Grape (1993)"), (5.0, 'Temptress Moon (Feng Yue) (1996)'), (5.0, 'Street Fighter (1994)'), (5.0, 'Spice World (1997)'), (5.0, 'Sliding Doors (1998)'), (5.0, 'Shooting Fish (1997)'), (5.0, "Roseanna's Grave (For Roseanna) (1997)"), (5.0, 'Rendezvous in Paris (Rendez-vous de Paris, Les) (1995)'), (5.0, 'Reluctant Debutante, The (1958)'), (5.0, 'Police Story 4: Project S (Chao ji ji hua) (1993)')]

データ数が小さいからそこまでの差は出ませんが、アイテムベースのほうが若干計算が速いですね(´・ω・`)

Scala de MovieLens

それでは先のPythonコードをScalaコードに翻訳していきますよ(`・ω・´)

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

// ファイル読み込み用のパッケージをインポートします
import scala.io.Source
// MovieLens Data Setsから推薦用のデータセットを作成します
def loadMovieLens(path:String="data/ml-data"):Map[String, Map[String, Double] = {
  // movieLensのitemデータを読み込みます
  //   ダウンロードしたitemデータがぶっ壊れていたみたいなので
  //   若干編集して使いました…Pythonは問題なく行けたのは何故だ?
  val source_item = Source.fromFile(path + "/u.item.modify")
  val movies = source_item.getLines().map(x =>
       x.split('|').take(2) match { case Array(a, b) => (a -> b)}
    ).toMap
  // ファイル読み込みをクローズします
  source_item.close
    
  // 評価値データを読み込みリストとして展開します
  val source_data = Source.fromFile(path + "/u.data")
  val prefs = source_data.getLines().
    map(x => x.split("\t") match { case Array(a, b, c, d) =>
      (a, movies(b), c.toDouble)}).toList
  // 読み込みをクローズします
  source_data.close
  
  // transformPrefsを参考にして
  // 入れ子構造に再構築する再帰関数を用意します
  def convert(datas:List[(String, String, Double)], 
    items:Map[String, Map[String, Double]]
     = Map.empty[String, Map[String, Double]]):Map[String, Map[String, Double]] = {      
    // 処理用に展開したリストがなくなれば終了
    if(datas.isEmpty){
      items
    }else {
      //// 再帰処理を行います
      // 先頭だけ取り除いて処理続行です
      convert(datas.tail, 
        //// 取り出した先頭要素をリターン用のMapに組み込んでやります
        // リターン用のMapの第1要素にアイテムキーが存在していなければ追加シマス
        if(!items.isDefinedAt(datas.head._1)){
          items + (datas.head._1 -> Map(datas.head._2 -> datas.head._3))
        // リターン用のMapの第1要素にアイテムキーが存在していた場合は
        // 第二要素(内側のMap)に新しい値の組み合わせを追加します
        }else{
          // どの第1要素に追加するかをmapで判定し 
          items.map{case(item, values) => 
            // 追加すべき要素が見つかった場合はMapの要素を結合します
            if(item == datas.head._1){
              item -> (values ++ Map(datas.head._2 -> datas.head._3))
            // 該当しない要素であればそのままリターンします
            }else{ item -> values }}
        })
    }
  }
  // 再帰関数を利用して展開したリストを二重Mapに再編集してやります
  convert(prefs)
}

勢いでイミュータブルな実装にしてみたけども、ごちゃごちゃしているからミュータブル版も書いてみる(´・ω・`)多分こっちのほうが処理ははるかに速いです

def loadMovieLens(path:String="data/ml-data"):Map[String, Map[String, Double]] = {
  val source_item = Source.fromFile(path + "/u.item.modify")
  val movies = source_item.getLines().map(x =>
       x.split('|').take(2) match { case Array(a, b) => (a -> b)}
    ).toMap
  source_item.close
  import scala.collection.mutable
  val prefs = mutable.Map.empty[String, mutable.Map[String, Double]]
  
  val source_data = Source.fromFile(path + "/u.data")
  for(line <- source_data.getLines){
    var Array(a, b, c, d) = line.split("\t")
    if(!prefs.isDefinedAt(a)) prefs(a) = mutable.Map.empty[String, Double]
    prefs(a)(movies(b))  = c.toDouble
  }
  source_data.close
  
  // ファイル読み込みをクローズして
  // イミュータブルをミュータブルに変更して値をリターン
  prefs.map(x => (x._1, x._2.toMap)).toMap
}

ファイル読み込みについてはこちらを参照しました
ゆろよろ日記
遙か彼方の彼方まで


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

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

// データセットを作成してやります
scala> val prefs = loadMovieLens()
prefs: Map[String,Map[String,Double]] = Map((710,Map(Toy Story (1995) -> 4.0, Aladdin (1992) -> 3.0, Groundhog Day (1993) -> 3.0, Fugitive, The (1993) -> 4.0, Citizen Kane (1941) -> 5.0, Titanic (1997) -> 4.0, Cop Land (1997) -> 3.0, Air For
<省略>

// 確認してみますよ
scala> prefs("87")
res0: Map[String,Double] = Map((Truth About Cats & Dogs, The (1996),4.0), (E.T. the Extra-Terrestrial (1982),3.0), (Searching for Bobby Fischer (1993),4.0), (Th
<省略>

// 推薦してみます
scala> getRecommendations(prefs, "87").take(10)
res1: List[(Double, String)] = List((5.0,Star Kid (1997)), (4.89884443128923,Legal Deceit (1997)), (4.815019082242709,Letter From Death Row, A (1998)), (4.7321082983941425,Hearts and Minds (1996)), (4.696244466490867,Pather Panchali (1955)), (4.652397061026758,Lamerica (1994)), (4.538723693474813,Leading Man, The (1996)), (4.535081339106104,Mrs. Dalloway (1997)), (4.532337612572981,Innocents, The (1961)), (4.527998574747078,Casablanca (1942)))

//// ついでにアイテムベースの推薦もやってみますよ
// アイテム間類似度を計算してやります
scala> val itemsim = calculateSimilarItems(prefs)
itemsim: Map[String,List[(Double, String)]] = Map((Mrs. Dalloway (1997),List((1.0,Mediterraneo (1991)), (0.5,Pink Floyd - The Wall (1982)), (0.3333333333333333,

// アイテムを推薦してみます
scala> getRecommendationItems(prefs, itemsim, "87").take(10)
res5: List[(Double, String)] = List((5.000000000000001,When Night Is Falling (1995)), (5.000000000000001,Kull the Conqueror (1997)), (5.000000000000001,Country Life (1994)), (5.000000000000001,Golden Earrings (1947)), (5.000000000000001,Shiloh (1997)), (5.0,Dangerous Beauty (1998)), (5.0,Two or Three Things I Know About Her (1966)), (5.0,Dead Presidents (1995)), (5.0,Reluctant Debutante, The (1958)), (5.0,Mercury Rising (1998)))


おお、なんとかできましたな(´・ω・`)

ユーザベース VS アイテムベース

ユーザベースとアイテムベースはそれぞえ利点と欠点があるので適材適所で使いましょーとのことです。

たとえば次のようになりますね

  • ユーザベースの協調フィルタリング
    • メリット
      • 動作がシンプル
      • 動的に計算できるのでバックグラウンドの処理はいらない
    • デメリット
      • データ数が増えると動作が低速に
      • 疎なデータ(全アイテムに対して各ユーザが評価したアイテム数が少ない)の場合は精度が悪い
  • アイテムベースの協調フィルタリング
    • メリット
      • データ数が増えてもある程度高速に動く
      • 疎なデータでも精度が劣化しづらい
    • デメリット
      • バッチ処理などでアイテム類似度を定期的に計算する必要あり

以上の状況からユーザベースのフィルタリングは頻繁に変更が行われる小規模データセットをベースに推薦するのに向いていて、 逆にアイテムベースは大規模だけれども変更の頻度が少ないデータセットを利用するのに向いていると言えるみたいです。

エクササイズ

本書から読者への挑戦状的なアレです

今回はスルーしますが、いつかやりたいと思うので羅列だけしておきます(´・ω・`)

  • Tanimoto係数について調査、検討みる
  • del.icio.usにおけるタグの類似性を計算してアイテムの推薦を考えてみる
  • ユーザベースフィルタリングを、事前にユーザ間類似度を計算しておくことで効率化してみる
  • del.icio.usのデータを基にアイテムベースの協調フィルタリングをやってみる
  • Audioscrobbler(現last.fm)のAPIを利用して音楽推薦システムを構築する

まとめ

ユーザの嗜好データを基に類似度を駆使してアイテムの推薦を行なってきた第2章はこれで終わりです。

次回は第3章のグループを見つけ出す処理についてやっていきます…ってクラスタリング

ちなみに、ここまでやってきてみてイミュータブルな実装は集計処理系には向かないなぁ…とハゲしく痛感しているところ(´・ω・`)やり方が悪いだけかも知れないですが

まあ、Scala的折衷主義でイミュータブルとミュータブルを使い分ければいいのでしょうが…なんかいい方法はないものかなぁ?