お勧め映画検索のデータ準備
この辺の続き。
近似値が出ていれば、ある人間 azalea から見てある程度近しい人間を選び、お勧めしてる商品(azalea が持っていないもので、相手がプラス評価しているもの)が推薦できる。
その精度は (Σ人数(類似度 × お勧め度))/人数
でおよそその人が下すであろう点数の期待値が算出できる。
さて、ここまでは特に問題はないのだが、如何せん人と人との類似度一つ計算するのに計算回数がかかり過ぎである。
何せ人数の階上回は計算しなければならない訳だ。少ない人数のコミュニティならまだしも、ECか何かで計算するには手間がかかり過ぎる。
それに対してのアプローチはどうとるべきかという話を #新宿Scala座 第4回 Scala勉強会 on Zusaar この辺で座談会的にやってた。
そこで出たアルゴリズムが、
- 人と人を比較するのではなく、アイテムとアイテムの類似度を見る。その上で似ている物をグループ化し、ユーザがどのグループを好むかで振るいにかける ** アイテムに対する人の関連と見ると、関連が変化する回数が少ない為、計算回数が減る
- 人と人を比較するが、今度は人をグループ化してしまう方法 ** グループ内で比較する分には計算回数が少ないはず
という案が出てきた。
ついでに、その後集合知本を入手。
てか 1-2 回の内容がそのまま過ぎる。blog 漁って調べてたけど、皆似た本読んでたのかもしれない。ちなみに手法は協調フィルタリングというらしい。
- 作者: Toby Segaran,當山仁健,鴨澤眞夫
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/07/25
- メディア: 大型本
- 購入: 91人 クリック: 2,220回
- この商品を含むブログ (277件) を見る
を読んでたら
http://grouplens.org/datasets/movielens/
なんてのが紹介されてた。
調子こいて 1M レコードのファイルをダウンロード。
ファイルの中身が
- users.dat : UserID::Gender::Age::Occupation::Zip-code
- movies.dat : MovieID::Title::Genres
- ratings.dat : UserID::MovieID::Rating::Timestamp
というフォーマットらしい。
ムービー 3952 タイトル、ユーザ 6040 人、評価件数 1000209 件。
素敵過ぎるね。
如何せんこのままでは評価が難しいので、全部 Mongo にでも突っ込もうか。
何か今日中に処理を終わらせられる気がしないw
で、こんなコードを作った。 mongo コネクタ
package net.white_azalea.models import com.mongodb.casbah.Imports._ trait DB { private val host = "127.0.0.1" private val port = 27017 private val databaseName = "movies" private val client = MongoClient(host, port) private val threadLocal = new ThreadLocal[MongoDB] def db(dbName:String):MongoDB = client(dbName) def get() = { Option(threadLocal.get()).getOrElse({ val dbInstance = db(databaseName) threadLocal.set(dbInstance) dbInstance }) } } object DB extends DB
データクラス
package net.white_azalea.models case class User(uid:Long, gender:String, age:Int, occupation:Int, zipCode:String) case class Rating(uid:Long, mid:Long, rating:Int, timestamp:Long) case class Movie(mid:Long, title:String, genre:String)
処理
package net.white_azalea import scala.io.Source import net.white_azalea.models.{Rating, User, DB, Movie} import com.novus.salat._ import com.novus.salat.global._ object Runner extends App { val db = DB.get() def moviesCollection = db("movies") def userCollection = db("users") def ratingCollection = db("rating") def receive(v: AnyRef) = v match { case u:User => userCollection.insert(grater[User].asDBObject(u)) case u:Movie => moviesCollection.insert(grater[Movie].asDBObject(u)) case u:Rating => ratingCollection.insert(grater[Rating].asDBObject(u)) } val startTime = System.currentTimeMillis() println("Insert movies.") var movies = 0 def insertMoviesDat() { val source = Source.fromURL(getClass.getResource("/movies.dat"), "ISO-8859-1") source.getLines().map(str => { movies += 1 val params = str.split("::") params match { case p if p.length == 3 => Some( Movie(p(0).toLong, p(1), p(2))) case _ => None } }).foreach(m => m.foreach(receive)) source.close() } insertMoviesDat() println("Insert users.") var users = 0 def insertUsersDat() { val source = Source.fromURL(getClass.getResource("/users.dat"), "ISO-8859-1") source.getLines().map(str => { users += 1 val params = str.split("::") params match { case p if p.length == 5 => Some( User(p(0).toLong, p(1), p(2).toInt, p(3).toInt, p(4))) case _ => None } }).foreach(m => m.foreach(receive)) source.close() } insertUsersDat() println("Insert ratings.") var ratings = 0 def insertRatingsDat() { val source = Source.fromURL(getClass.getResource("/ratings.dat"), "ISO-8859-1") source.getLines().map(str => { ratings += 1 val params = str.split("::") params match { case p if p.length == 4 => Some( Rating(p(0).toLong, p(1).toLong, p(2).toInt, p(3).toLong)) case _ => None } }).foreach(m => m.foreach(receive)) source.close() } insertRatingsDat() val finishTime = System.currentTimeMillis() println(s"Finish at ${finishTime - startTime} ms") println(s"Insert $movies Movies\nInsert $users Users\nInsert $ratings Ratings.") }
UTF-8 で読み込むと途中でエラーが出る。
で、mongo の中身を消す方法が mongo movies --eval "db.dropDatabase()"
で、実行結果が
Insert movies. Insert users. Insert ratings. Finish at 6584 ms Insert 3883 Movies Insert 6040 Users Insert 1000209 Ratings.
Mongo の insert はえー。
Insert を Akka で並列化しようとしたら、2.2.1 からは構造が変わってて Mailbox 参照とかクリア待ちが出来なくなってた。
内部構造追いかけるのに時間かかったが、正直それくらいなら素で insert した方がはるかにマシだったよ(;´・ω・)