前言
經過2節對MovieLens資料集的學習,想必讀者對MovieLens資料集認識的不錯了;同時也順帶回顧了些Spark程式設計技巧,Python資料分析技巧。
本節將是讓人興奮的一節,它將實現一個基於Spark的推薦系統引擎。
PS1:關於推薦演算法的理論知識,請讀者先自行學習,本文僅介紹基於ALS矩陣分解演算法的Spark推薦引擎實現。
PS2:全文示例將採用Scala語言。
第一步:提取有效特徵
1. 首先,啟動spark-shell並分配足夠記憶體:
2. 載入使用者對影片的評級資料:
1 // 載入評級資料 2 val rawData = sc.textFile("/home/kylin/ml-100k/u.data") 3 // 展示一條記錄 4 rawData.first()
結果為:
3. 切分記錄並返回新的RDD:
1 // 格式化資料集 2 val rawRatings = rawData.map(_.split("\t").take(3)) 3 // 展示一條記錄 4 rawRatings.first()
4. 接下來需要將評分矩陣RDD轉化為Rating格式的RDD:
1 // 匯入rating類 2 import org.apache.spark.mllib.recommendation.Rating 3 // 將評分矩陣RDD中每行記錄轉換為Rating型別 4 val ratings = rawRatings.map { case Array(user, movie, rating) => Rating(user.toInt, movie.toInt, rating.toDouble) }
這是因為MLlib的ALS推薦系統演算法包只支援Rating格式的資料集。
第二步:訓練推薦模型
接下來可以進行ALS推薦系統模型訓練了。MLlib中的ALS演算法接收三個引數:
- rank:對應的是隱因子的個數,這個值設定越高越準,但是也會產生更多的計算量。一般將這個值設定為10-200;
- iterations:對應迭代次數,一般設定個10就夠了;
- lambda:該引數控制正則化過程,其值越高,正則化程度就越深。一般設定為0.01。
1. 首先,執行以下程式碼,啟動ALS訓練:
1 // 匯入ALS推薦系統演算法包 2 import org.apache.spark.mllib.recommendation.ALS 3 // 啟動ALS矩陣分解 4 val model = ALS.train(ratings, 50, 10, 0.01)
這步將會使用ALS矩陣分解演算法,對評分矩陣進行分解,且隱特徵個數設定為50,迭代10次,正則化引數設為了0.01。
相對其他步驟,訓練耗費的時間最多。執行結果如下:
2. 返回型別為MatrixFactorizationModel物件,它將結果分別儲存到兩個(id,factor)RDD裡面,分別名為userFeatures和productFeatures。
也就是評分矩陣分解後的兩個子矩陣:
上面展示了id為4的使用者的“隱因子向量”。請注意ALS實現的操作都是延遲性的轉換操作。
第三步:使用ALS推薦模型
1. 預測使用者789對物品123的評分:
2. 為使用者789推薦前10個物品:
1 val userId = 789 2 val K = 10 3 4 // 獲取推薦列表 5 val topKRecs = model.recommendProducts(userId, K) 6 // 列印推薦列表 7 println(topKRecs.mkString("\n"))
結果為:
3. 初步檢驗推薦效果
獲取到各個使用者的推薦列表後,想必大家都想先看看使用者評分最高的電影,和給他推薦的電影是不是有相似。
3.1 建立電影id - 電影名字典:
1 // 匯入電影資料集 2 val movies = sc.textFile("/home/kylin/ml-100k/u.item") 3 // 建立電影id - 電影名字典 4 val titles = movies.map(line => line.split("\\|").take(2)).map(array => (array(0).toInt, array(1))).collectAsMap() 5 // 檢視id為123的電影名 6 titles(123)
結果為:
這樣後面就可以根據電影的id找到電影名了。
3.2 獲取某使用者的所有觀影記錄並列印:
1 // 建立使用者名稱-其他RDD,並僅獲取使用者789的記錄 2 val moviesForUser = ratings.keyBy(_.user).lookup(789) 3 // 獲取使用者評分最高的10部電影,並列印電影名和評分值 4 moviesForUser.sortBy(-_.rating).take(10).map(rating => (titles(rating.product), rating.rating)).foreach(println)
結果為:
3.3 獲取某使用者推薦列表並列印:
讀者可以自行對比這兩組列表是否有相似性。
第四步:物品推薦
很多時候還有另一種需求:就是給定一個物品,找到它的所有相似物品。
遺憾的是MLlib裡面竟然沒有包含內建的函式,需要自己用jblas庫來實現 = =#。
1. 匯入jblas庫矩陣類,並建立一個餘弦相似度計量函式:
1 // 匯入jblas庫中的矩陣類 2 import org.jblas.DoubleMatrix 3 // 定義相似度函式 4 def cosineSimilarity(vec1: DoubleMatrix, vec2: DoubleMatrix): Double = { 5 vec1.dot(vec2) / (vec1.norm2() * vec2.norm2()) 6 }
2. 接下來獲取物品(本例以物品567為例)的因子特徵向量,並將它轉換為jblas的矩陣格式:
1 // 選定id為567的電影 2 val itemId = 567 3 // 獲取該物品的隱因子向量 4 val itemFactor = model.productFeatures.lookup(itemId).head 5 // 將該向量轉換為jblas矩陣型別 6 val itemVector = new DoubleMatrix(itemFactor)
3. 計算物品567和所有其他物品的相似度:
1 // 計算電影567與其他電影的相似度 2 val sims = model.productFeatures.map{ case (id, factor) => 3 val factorVector = new DoubleMatrix(factor) 4 val sim = cosineSimilarity(factorVector, itemVector) 5 (id, sim) 6 } 7 // 獲取與電影567最相似的10部電影 8 val sortedSims = sims.top(K)(Ordering.by[(Int, Double), Double] { case (id, similarity) => similarity }) 9 // 列印結果 10 println(sortedSims.mkString("\n"))
結果為:
其中0.999999當然就是自己跟自己的相似度了。
4. 檢視推薦結果:
1 // 列印電影567的影片名 2 println(titles(567)) 3 // 獲取和電影567最相似的11部電影(含567自己) 4 val sortedSims2 = sims.top(K + 1)(Ordering.by[(Int, Double), Double] { case (id, similarity) => similarity }) 5 // 再列印和電影567最相似的10部電影 6 sortedSims2.slice(1, 11).map{ case (id, sim) => (titles(id), sim) }.mkString("\n")
結果為:
看看,這些電影是不是和567相似?
第五步:推薦效果評估
在Spark的ALS推薦系統中,最常用到的兩個推薦指標分別為MSE和MAPK。其中MSE就是均方誤差,是基於評分矩陣的推薦系統的必用指標。那麼MAPK又是什麼呢?
它稱為K值平均準確率,最多用於TopN推薦中,它表示資料集範圍內K個推薦物品與實際使用者購買物品的吻合度。具體公式請讀者自行參考有關文件。
本文推薦系統就是一個[基於使用者-物品評分矩陣的TopN推薦系統],下面步驟分別用來獲取本文推薦系統中的這兩個指標。
PS:記得先要匯入jblas庫。
1. 首先計算MSE和RMSE:
1 // 建立使用者id-影片id RDD 2 val usersProducts = ratings.map{ case Rating(user, product, rating) => (user, product)} 3 // 建立(使用者id,影片id) - 預測評分RDD 4 val predictions = model.predict(usersProducts).map{ 5 case Rating(user, product, rating) => ((user, product), rating) 6 } 7 // 建立使用者-影片實際評分RDD,並將其與上面建立的預測評分RDD join起來 8 val ratingsAndPredictions = ratings.map{ 9 case Rating(user, product, rating) => ((user, product), rating) 10 }.join(predictions) 11 12 // 匯入RegressionMetrics類 13 import org.apache.spark.mllib.evaluation.RegressionMetrics 14 // 建立預測評分-實際評分RDD 15 val predictedAndTrue = ratingsAndPredictions.map { case ((user, product), (actual, predicted)) => (actual, predicted) } 16 // 建立RegressionMetrics物件 17 val regressionMetrics = new RegressionMetrics(predictedAndTrue) 18 19 // 列印MSE和RMSE 20 println("Mean Squared Error = " + regressionMetrics.meanSquaredError) 21 println("Root Mean Squared Error = " + regressionMetrics.rootMeanSquaredError)
基本原理是將實際評分-預測評分扔到RegressionMetrics類裡,該類提供了mse和rmse成員,可直接輸出獲取。
結果為:
2. 計算MAPK:
// 建立電影隱因子RDD,並將它廣播出去 val itemFactors = model.productFeatures.map { case (id, factor) => factor }.collect() val itemMatrix = new DoubleMatrix(itemFactors) val imBroadcast = sc.broadcast(itemMatrix) // 建立使用者id - 推薦列表RDD 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) } // 建立使用者 - 電影評分ID集RDD val userMovies = ratings.map{ case Rating(user, product, rating) => (user, product) }.groupBy(_._1) // 匯入RankingMetrics類 import org.apache.spark.mllib.evaluation.RankingMetrics // 建立實際評分ID集-預測評分ID集 RDD val predictedAndTrueForRanking = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) => val actual = actualWithIds.map(_._2) (predicted.toArray, actual.toArray) } // 建立RankingMetrics物件 val rankingMetrics = new RankingMetrics(predictedAndTrueForRanking) // 列印MAPK println("Mean Average Precision = " + rankingMetrics.meanAveragePrecision)
結果為:
比較坑的是不能設定K,也就是說,計算的實際是MAP...... 正如屬性名:meanAveragePrecision。
小結
感覺MLlib的推薦系統真的很一般,一方面支援的型別少 - 只支援ALS;另一方面支援的推薦系統運算元也少,連輸出個RMSE指標都要寫好幾行程式碼,太不方便了。
唯一的好處是因為接近底層,所以可以讓使用者看到些實現的細節,對原理更加清晰。