Spark機(jī)器學(xué)習(xí)實(shí)戰(zhàn)(四)電影推薦算法 - 協(xié)同過濾
這篇文章將要介紹推薦算法中最核心的部分闽巩,協(xié)同過濾丰包”郏基于大量用戶對(duì)大量電影的評(píng)分垦巴,將要完成的任務(wù)有兩個(gè):第一,向用戶推薦可能感興趣的電影(即可能會(huì)評(píng)高分的電影)铭段;第二骤宣,找出和某部電影相似的電影出來,即找出的特征相似的電影來序愚。
文章中列出了關(guān)鍵代碼憔披,完整代碼見我的github repository,這篇文章的代碼在
chapter04/src/main/scala/ScalaApp.scala
第1步:協(xié)同過濾簡(jiǎn)介
我們假設(shè)有100個(gè)用戶爸吮,100個(gè)電影以及用戶對(duì)電影的評(píng)分芬膝。然而,沒有一個(gè)網(wǎng)站會(huì)擁有觀看了所有電影并且評(píng)分了的用戶的形娇。在理想狀態(tài)中锰霜,我們希望有10000個(gè)電影評(píng)分,但是這顯然是不可能的埂软。我們可能有1000條評(píng)分锈遥,可能500條纫事。
協(xié)同過濾要做的就是勘畔,基于這僅有的1000條評(píng)分,估計(jì)出剩下的9000條未打出的評(píng)分丽惶。這有什么意義呢炫七?我們這樣就可以預(yù)估出用戶可能喜歡的電影,并向它們推薦钾唬。同時(shí)万哪,我們可以學(xué)習(xí)出用戶的特征侠驯,電影的特征并為它們找出相似的同類來。算法很強(qiáng)大奕巍,目前各大購(gòu)物吟策,視頻網(wǎng)站都在使用它。
我們用矩陣的角度來看一下這個(gè)問題的止,假設(shè)我們有U名用戶檩坚,I個(gè)電影,部分用戶對(duì)部分電影進(jìn)行了評(píng)分诅福,評(píng)分可以表示為一個(gè)U*I
的矩陣匾委,這個(gè)矩陣大部分值是缺失的,因此為一個(gè)稀疏矩陣:
現(xiàn)在我們想把用戶表示為一個(gè)K維的特征氓润,電影也表示成一個(gè)K維的特征赂乐,特征的點(diǎn)積就是對(duì)應(yīng)的評(píng)分。協(xié)同過濾就是利用現(xiàn)有的評(píng)分來學(xué)習(xí)出這些K維特征咖气,從而填補(bǔ)未評(píng)分的項(xiàng)
從數(shù)學(xué)上說就是把U*I
的矩陣挨措,分解為U*K
與K*I
的矩陣積,滿足已有評(píng)分正確的同時(shí)崩溪,填補(bǔ)未評(píng)分項(xiàng)运嗜。為什么要分解呢?因?yàn)镵遠(yuǎn)小于U和I悯舟,存儲(chǔ)方便担租,表示簡(jiǎn)單。
具體求分解矩陣的方式是采用最小二乘法(Alternating Least Squares抵怎,ALS)奋救。
另外補(bǔ)充一句,這種矩陣分解稱為顯示矩陣分解反惕。此外還有隱式矩陣分解尝艘,區(qū)別在于評(píng)分矩陣的項(xiàng)為二進(jìn)制值,另外多了一個(gè)U*I
的信心權(quán)重矩陣表示對(duì)于二進(jìn)制值的信心度姿染。像下面這樣:
第2步:從數(shù)據(jù)集中提取訓(xùn)練特征
我們現(xiàn)在要從MovieLens 100k數(shù)據(jù)集中提取出可以用來訓(xùn)練協(xié)同過濾模型的特征背亥。
u.data中的每一條數(shù)據(jù)長(zhǎng)這樣,分別代表用戶悬赏,電影狡汉,分?jǐn)?shù),時(shí)間戳闽颇。時(shí)間戳在這里并不需要盾戴。
196 242 3 881250949
Rating是MLlib中定義的數(shù)據(jù)格式,其中包括了用戶編號(hào)兵多,Item即電影編號(hào)以及評(píng)分尖啡。
import org.apache.spark.mllib.recommendation.Rating
val sc = new SparkContext("local[2]", "First Spark App")
sc.setLogLevel("ERROR")
val rawData = sc.textFile("data/u.data")
val rawRatings = rawData.map(_.split("\t").take(3))
val ratings = rawRatings.map {case Array(user, movie, rating)
=> Rating(user.toInt, movie.toInt, rating.toDouble)}
第3步:訓(xùn)練模型
ALS是MLlib提供的最小二乘回歸模型橄仆,原理是每次控制一個(gè)矩陣,優(yōu)化另一個(gè)衅斩,反復(fù)迭代最終收斂盆顾。ALS接收的三個(gè)數(shù)值參數(shù)分別為,特征長(zhǎng)度畏梆,即K的大小椎扬,Iteration次數(shù),和lambda參數(shù)(這個(gè)參數(shù)應(yīng)該要由交叉驗(yàn)證得出具温,這里取了0.01)蚕涤。
import org.apache.spark.mllib.recommendation.ALS
val model = ALS.train(ratings, 50, 10, 0.01)
println(model.userFeatures.count)
println(model.userFeatures.take(1))
println(model.predict(196, 242))
訓(xùn)練完畢后就可以看一下我們訓(xùn)練的結(jié)果,以及試著預(yù)測(cè)一下196號(hào)用戶對(duì)242號(hào)影片的評(píng)分了:評(píng)分結(jié)果為2.9820089343215352铣猩。
第4步:向用戶推薦電影
假設(shè)我們要向789號(hào)用戶推薦5部可能感興趣的電影揖铜,推薦模型內(nèi)置函數(shù)幫我們完成了這一點(diǎn),可惜我們得到的是五個(gè)Rating實(shí)例达皿,還不夠具體天吓。
val userId = 789
val K = 5
val topKRecs = model.recommendProducts(userId, K)
println(topKRecs.mkString("\n"))
結(jié)果為
Rating(789,182,5.525450045512849)
Rating(789,573,5.351473049607477)
Rating(789,504,5.1301817702377095)
Rating(789,97,5.125571781347671)
Rating(789,92,5.121346111181028)
我們進(jìn)一步把這位用戶喜歡的電影和推薦給他的電影名字給打印出來。中間用到了u.item數(shù)據(jù)庫(kù)來讀取電影名字
val movies = sc.textFile("data/u.item")
val titles = movies.map(line => line.split("\\|")).map(fields => (fields(0).toInt, fields(1))).collectAsMap()
val moviesForUser = ratings.keyBy(_.user).lookup(789)
println("User " + userId +"'s favorite movies:")
moviesForUser.sortBy(-_.rating).take(5).map(rating => (titles(rating.product), rating.rating)).foreach(println)
println("Movies recommended to user " + userId)
topKRecs.map(rating => (titles(rating.product), rating.rating)).foreach(println)
結(jié)果為:
User 789's favorite movies:
(Godfather, The (1972),5.0)
(Trainspotting (1996),5.0)
(Dead Man Walking (1995),5.0)
(Star Wars (1977),5.0)
(Swingers (1996),5.0)
Movies recommended to user 789
(GoodFellas (1990),5.525450045512849)
(Body Snatchers (1993),5.351473049607477)
(Bonnie and Clyde (1967),5.1301817702377095)
(Dances with Wolves (1990),5.125571781347671)
(True Romance (1993),5.121346111181028)
第5步:尋找相似電影
相似電影的尋找并沒有內(nèi)置的函數(shù)峦椰,我們的思路是找出與目標(biāo)電影的K維特征“夾角”最小的電影龄寞,兩個(gè)向量的夾角定義為向量的點(diǎn)積除以它們的二階范數(shù)。
import org.jblas.DoubleMatrix
def cosineSimilarity(vec1: DoubleMatrix, vec2: DoubleMatrix): Double = {
vec1.dot(vec2) / (vec1.norm2() * vec2.norm2())
}
val itemId = 567
val itemFactor = model.productFeatures.lookup(itemId).head
val itemVector = new DoubleMatrix(itemFactor)
val sims = model.productFeatures.map {case (id, factor) =>
val factorVector = new DoubleMatrix(factor)
val sim = cosineSimilarity(factorVector, itemVector)
(id, sim)
}
val sortedSims = sims.top(K)(Ordering.by[(Int, Double), Double] {case (id, similarity) => similarity})
println(sortedSims.mkString("\n"))
結(jié)果為:
(567,1.0)
(403,0.7299325745744089)
(853,0.7260626510960669)
(563,0.7231278972915091)
(413,0.7225324149301402)
第一個(gè)結(jié)果是它自身汤功,道理很顯然物邑。
下面同樣是把這些電影的名字輸出來:
println("Item number " + itemId + "'s name:")
println(titles(itemId))
println("Names of similar movies:")
println(sortedSims.map {case (id, similarity) => (titles(id), similarity)}.mkString("\n"))
結(jié)果為:
Item number 567's name:
Wes Craven's New Nightmare (1994)
Names of similar movies:
(Wes Craven's New Nightmare (1994),1.0)
(Batman (1989),0.7299325745744089)
(Braindead (1992),0.7260626510960669)
(Stephen King's The Langoliers (1995),0.7231278972915091)
(Tales from the Crypt Presents: Bordello of Blood (1996),0.7225324149301402)
可以看到這些電影都是科幻懸疑類的,說明算法還是很有效的滔金。
第6步:推薦模型效果評(píng)估
算法得出模型后色解,我們要評(píng)估算法就需要規(guī)定一些數(shù)值結(jié)果。最常用的檢驗(yàn)標(biāo)準(zhǔn)為MSE餐茵,即均方誤差科阎。但是這個(gè)檢查方式與推薦無關(guān),它檢驗(yàn)的是對(duì)已有評(píng)分能否完美再現(xiàn)忿族。即比較真實(shí)得分與模型計(jì)算得分之間的差锣笨。計(jì)算方式也不難。其中RMSE為MSE開根號(hào)道批。
val usersProducts = ratings.map {case Rating(user, product, rating) => (user, product)}
val predictions = model.predict(usersProducts).map {case Rating(user, product, rating) => ((user, product), rating)}
val ratingsAndPredictions = ratings.map {case Rating(user, product, rating) =>
((user, product), rating)}.join(predictions)
val MSE = ratingsAndPredictions.map {
case ((user, product), (actual, predicted)) => math.pow((actual - predicted), 2)
}.reduce((x, y) => x + y) / ratingsAndPredictions.count
println("MSE = " + MSE)
println("RMSE = " + math.sqrt(MSE))
我們可以用MLlib的函數(shù)來計(jì)算得出一樣的結(jié)果
import org.apache.spark.mllib.evaluation.RegressionMetrics
val predictedAndTrue = ratingsAndPredictions.map {
case ((user, product), (actual, predicted)) => (actual, predicted)}
val regressionMetrics = new RegressionMetrics(predictedAndTrue)
println("MLlib MSE = " + regressionMetrics.meanSquaredError)
println("MLlib RMSE = " + regressionMetrics.rootMeanSquaredError)
兩者的計(jì)算結(jié)果完全一致:
MSE = 0.08519901884772077
RMSE = 0.29188870969552894
MLlib MSE = 0.08519901884772077
MLlib RMSE = 0.29188870969552894
除了MSE外還有一種評(píng)判方式稱為Mean Average Precision即MAP错英,MAP是用測(cè)試數(shù)據(jù)來得出的,即我們知道最佳的推薦是什么屹徘,來和模型的預(yù)測(cè)結(jié)果相比較走趋。然而我們暫時(shí)沒有這樣的測(cè)試集,所以下面的測(cè)試并不嚴(yán)謹(jǐn)噪伊,得分自然也很低簿煌,我們的做法是把用戶的評(píng)分過的當(dāng)作應(yīng)該推薦的。
val userMovies = ratings.map{ case Rating(user, product, rating) => (user, product) }.groupBy(_._1)
val itemFactors = model.productFeatures.map { case (id, factor) => factor }.collect()
val itemMatrix = new DoubleMatrix(itemFactors)
val imBroadcast = sc.broadcast(itemMatrix)
val allRecs = model.userFeatures.map{ case (userId, array) =>
val userVector = new DoubleMatrix(array)
val scores = imBroadcast.value.mmul(userVector)
val sortedWithId = scores.data.zipWithIndex.sortBy(-_._1)
val recommendedIds = sortedWithId.map(_._2 + 1).toSeq
(userId, recommendedIds)
}
val predictedAndTrueForRanking = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) =>
val actual = actualWithIds.map(_._2)
(predicted.toArray, actual.toArray)
}
val rankingMetrics = new RankingMetrics(predictedAndTrueForRanking)
println("Mean Average Precision = " + rankingMetrics.meanAveragePrecision)
結(jié)果沒有太多參考價(jià)值鉴吹,也不貼出來了姨伟。僅僅是為了介紹一下MAP的計(jì)算方法,利用了MLlib的庫(kù)豆励,很簡(jiǎn)單夺荒。