1. 程式人生 > >推薦系統ALS矩陣分解

推薦系統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》(矩陣分解模型的好材料)