协同过滤推荐算法,是最经典、最常用的推荐算法。通过分析用户兴趣,在用户群中找到指定用户的相似用户,综合这些相似用户对某一信息的评价,形成系统关于该指定用户对此信息的喜好程度预测。
要实现协同过滤,需要以下几个步骤:
1)收集用户偏好;
2)找到相似的用户或物品;
3)计算推荐。
用户评分
从用户的行为和偏好中发现规律,并基于此进行推荐,所以收集用户的偏好信息成为系统推荐效果最基础的决定因素。用户有很多种方式向系统提供自己的偏好信息,比如:评分、投票、转发、保存书签、购买、点击流、页面停留时间等。
1. 将不同的行为分组
一般可以分为查看和购买,然后基于不同的用户行为,计算不同用户或者物品的相似度。
2. 对不同行为进行加权
对不同行为产生的用户喜好进行加权,然后求出用户对物品的总体喜好。当我们收集好用户的行为数据后,还要对 数据进行预处理,最核心的工作就是减噪和归一化。
对用户的行为分析得到用户的偏好后,可以根据用户的偏好计算相似用户和物品,然后可以基于相似用户或物品进行推荐。这就是协同过滤中的两个分支了,即基于用户的协同过滤和基于物品的协同过滤。
3. 皮尔逊相关系数(Pearson Correlation Coefficient)
4. Cosine 相似度(Cosine Similarity)
5. Tanimoto 系数(Tanimoto Coefficient)
推荐计算
- 基于用户的CF(User CF)
基于用户的 CF 的基本思想相当简单:基于用户对物品的偏好找到相邻的邻居用户,然后将邻居用户喜欢的推荐给当前用户。在计算上,就是将一个用户对所有物品的偏好作为一个向量来计算用户之间的相似度,找到K 邻居后,根据邻居的相似度权重及其对物品的偏好,预测当前用户没有偏好的未涉及物品,计算得到一个排序的物品列表作为推荐。图14-1 给出了一个例子,对于用户A,根据用户的历史偏好,这里只计算得到一个邻居-用户C,然后将用户C 喜欢的物品D 推荐给用户A。
- 基于物品的CF(Item CF)
基于物品的CF 的原理和基于用户的CF 类似,只是在计算邻居时采用物品本身,而不是从用户的角度。即基于用户对物品的偏好找到相似的物品,然后根据用户的历史偏好,推荐相似的物品给他。从计算的角度看,就是将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度,得到物品的相似物品后,根据用户历史的偏好预测当前用户还没有表示偏好的物品,计算得到一个排序的物品列表作为推荐。
根据用户评分矩阵采用同现相似度计算物品相似度矩阵。
其相似度计算实现了分布式计算,实现过程如下:
对于欧氏相似度的计算,采用离散计算公式d(x, y) = sqrt(Σ((x(i)-y(i)) * (x(i)- y(i))))。其中,i 只取x、y 同现的点,未同现的点不参与相似度计算;sim(x, y) = m / (1 + d(x, y)),m 为x、y 重叠数,同现次数
根据物品相似度矩阵和用户评分计算用户推荐列表,计算公式是R=W*A,取推荐计算中用户未评分过的物品,并且按照计算结果倒序推荐给用户。
其推荐计算实现了分布式计算。
源码分析
ALS算法:
def train(
ratings: RDD[Rating],
rank: Int,
iterations: Int,
lambda: Double,
blocks: Int,
seed: Long
): MatrixFactorizationModel = {
new ALS(blocks, blocks, rank, iterations, lambda, false, 1.0, seed).run(ratings)
}
def run(ratings: RDD[Rating]): MatrixFactorizationModel = {
require(!ratings.isEmpty(), s"No ratings available from $ratings")
val sc = ratings.context
val numUserBlocks = if (this.numUserBlocks == -1) {
math.max(sc.defaultParallelism, ratings.partitions.length / 2)
} else {
this.numUserBlocks
}
val numProductBlocks = if (this.numProductBlocks == -1) {
math.max(sc.defaultParallelism, ratings.partitions.length / 2)
} else {
this.numProductBlocks
}
val (floatUserFactors, floatProdFactors) = NewALS.train[Int](
ratings = ratings.map(r => NewALS.Rating(r.user, r.product, r.rating.toFloat)),
rank = rank,
numUserBlocks = numUserBlocks,
numItemBlocks = numProductBlocks,
maxIter = iterations,
regParam = lambda,
implicitPrefs = implicitPrefs,
alpha = alpha,
nonnegative = nonnegative,
intermediateRDDStorageLevel = intermediateRDDStorageLevel,
finalRDDStorageLevel = StorageLevel.NONE,
checkpointInterval = checkpointInterval,
seed = seed)
val userFactors = floatUserFactors
.mapValues(_.map(_.toDouble))
.setName("users")
.persist(finalRDDStorageLevel)
val prodFactors = floatProdFactors
.mapValues(_.map(_.toDouble))
.setName("products")
.persist(finalRDDStorageLevel)
if (finalRDDStorageLevel != StorageLevel.NONE) {
userFactors.count()
prodFactors.count()
}
new MatrixFactorizationModel(rank, userFactors, prodFactors)
}
private def recommend(
recommendToFeatures: Array[Double],
recommendableFeatures: RDD[(Int, Array[Double])],
num: Int): Array[(Int, Double)] = {
val scored = recommendableFeatures.map { case (id, features) =>
(id, blas.ddot(features.length, recommendToFeatures, 1, features, 1))
}
scored.top(num)(Ordering.by(_._2))
}
实例:
import org.apache.spark.mllib.recommendation.ALS
import org.apache.spark.mllib.recommendation.MatrixFactorizationModel
import org.apache.spark.mllib.recommendation.Rating
// Load and parse the data
val data = sc.textFile("data/mllib/als/test.data")
val ratings = data.map(_.split(',') match { case Array(user, item, rate) =>
Rating(user.toInt, item.toInt, rate.toDouble)
})
// Build the recommendation model using ALS
val rank = 10
val numIterations = 10
val model = ALS.train(ratings, rank, numIterations, 0.01)
// Evaluate the model on rating data
val usersProducts = ratings.map { case Rating(user, product, rate) =>
(user, product)
}
val predictions =
model.predict(usersProducts).map { case Rating(user, product, rate) =>
((user, product), rate)
}
val ratesAndPreds = ratings.map { case Rating(user, product, rate) =>
((user, product), rate)
}.join(predictions)
val MSE = ratesAndPreds.map { case ((user, product), (r1, r2)) =>
val err = (r1 - r2)
err * err
}.mean()
println("Mean Squared Error = " + MSE)
// Save and load model
model.save(sc, "target/tmp/myCollaborativeFilter")
val sameModel = MatrixFactorizationModel.load(sc, "target/tmp/myCollaborativeFilter")