第三篇:一個Spark推薦系統引擎的實現

穆晨發表於2017-05-20

前言

       經過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推薦系統中,最常用到的兩個推薦指標分別為MSEMAPK。其中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指標都要寫好幾行程式碼,太不方便了。

       唯一的好處是因為接近底層,所以可以讓使用者看到些實現的細節,對原理更加清晰。

相關文章