推薦系統ALS矩陣分解
矩陣分解模型的物理意義
我們希望學習到一個P代表user的特徵,Q代表item的特徵。特徵的每一個維度代表一個隱性因子,比如對電影來說,這些隱性因子可能是導演,演員等。當然,這些隱性因子是機器學習到的,具體是什麼含義我們不確定。
學習到P和Q之後,我們就可以直接P乘以Q就可以預測所有user對item的評分了。
講完矩陣分解推薦模型,下面到als了(全稱Alternatingleast squares)。其實als就是上面損失函式最小化的一個求解方法,當然還有其他方法比如SGD等。
als論文中的損失函式是(跟上面那個稍微有點不同)
每次迭代,
固定M,逐個更新每個user的特徵u(對u求偏導,令偏導為0求解)。
固定U,逐個更新每個item的特徵m(對m求偏導,令偏導為0求解)。
論文中是這樣推導的
這是每次迭代求u的公式。求m的類似。
為了更清晰的理解,這裡結合spark的als程式碼講解。
spark原始碼中實現als有三個版本,一個是LocalALS.scala(沒有用spark),一個是SparkALS.scala(用了spark做並行優化),一個是mllib中的ALS。
本來LocalALS.scala和SparkALS.scala這個兩個實現是官方為了開發者學習使用spark展示的,
mllib中的ALS可以用於實際的推薦。
但是mllib中的ALS做了很多優化,不適合初學者研究來理解als演算法。
因此,下面我拿LocalALS.scala和SparkALS.scala來講解als演算法。
LocalALS.scala
// Iteratively update movies then users for (iter <- 1 to ITERATIONS) { println(s"Iteration $iter:") ms = (0 until M).map(i => updateMovie(i, ms(i), us, R)).toArray //固定使用者,逐個更新所有電影的特徵 us = (0 until U).map(j => updateUser(j, us(j), ms, R)).toArray //固定電影,逐個更新所有使用者的特徵 println("RMSE = " + rmse(R, ms, us)) println() }
//更新第j個user的特徵向量
def updateUser(j: Int, u: RealVector, ms: Array[RealVector], R: RealMatrix) : RealVector = {
var XtX: RealMatrix = new Array2DRowRealMatrix(F, F) //F是隱性因子的數量
var Xty: RealVector = new ArrayRealVector(F)
// For each movie that the user rated 遍歷該user評分過的movie.顯然,這裡預設該使用者評分過所有電影,所以是0-M.實際應用求解,只需要遍歷該使用者評分過的電影.
for (i <- 0 until M) {
val m = ms(i)
// Add m * m^t to XtX 外積後 累加到XtX
XtX = XtX.add(m.outerProduct(m)) //向量與向量的外積:一個當作列向量,一個當作行向量,做矩陣乘法,結果是一個矩陣
// Add m * rating to Xty
Xty = Xty.add(m.mapMultiply(R.getEntry(i, j)))
}
// Add regularization coefficients to diagonal terms
for (d <- 0 until F) {
XtX.addToEntry(d, d, LAMBDA * M)
}
// Solve it with Cholesky 其實是解一個A*x=b的方程
new CholeskyDecomposition(XtX).getSolver.solve(Xty)
}
再結合論文中的公式
其實程式碼中的XtX就是公式中左邊紅圈的部分,Xty就是右邊紅圈的部分。
同理,更新每個電影的特徵m類似,這裡不再重複。
SparkALS.scala
for (iter <- 1 to ITERATIONS) {
println(s"Iteration $iter:")
ms = sc.parallelize(0 until M, slices)
.map(i => update(i, msb.value(i), usb.value, Rc.value))
.collect()
msb = sc.broadcast(ms) // Re-broadcast ms because it was updated
us = sc.parallelize(0 until U, slices)
.map(i => update(i, usb.value(i), msb.value, Rc.value.transpose()))
.collect()
usb = sc.broadcast(us) // Re-broadcast us because it was updated
println("RMSE = " + rmse(R, ms, us))
println()
}
SparkALS版本相對於LocalALS的亮點時,做了並行優化。LocalALS中,每個user的特徵是序列更新的。而SparkALS中,是並行更新的。
參考資料:
《Large-scale Parallel Collaborative Filtering for the Netflix Prize》(als-wr原論文)
《Matrix Factorization Techniques for Recommender Systems》(矩陣分解模型的好材料)