思想类似线性回归做预测,大致如下
定义一个预测模型(数学公式),
然后确定一个损失函数,
将已有数据作为训练集,
不断迭代来最小化损失函数的值,
最终确定参数,把参数套到预测模型中做预测。
矩阵分解的预测模型是:
损失函数是:
我们希望学习到一个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》(矩阵分解模型的好材料)