背景介绍
最近在开发产品的过程中,想为用户推送他们感兴趣的内容。一开始,我尝试在后端通过标签排序算法来为用户推荐,但效果并不理想。不仅需要构建大量的标签体系,后端的维护成本也很高。
后来,我想到了推荐系统。虽然之前从未接触过推荐系统(因为提及推荐系统,首先想到的就是机器学习),但我还是决定开始研究,因为推荐系统是实现用户个性化体验的最佳实践。最终,我选择了基于物品的协同过滤算法(Item-CF),因为它是推荐系统中最简单、最直观的算法之一。
算法原理
基于物品的协同过滤算法(Item-CF)
Item-CF的核心思想是"喜欢同样物品的用户也会喜欢相似的物品"。例如:如果用户A喜欢物品B,那么系统会将和物品B最相似的物品D推荐给用户A。
相似度计算
在Item-CF中,相似度计算是核心环节。我们使用余弦相似度来计算物品之间的相似程度。
根据余弦定理,对于坐标中的两个向量,其夹角越小,则相似度越大:

余弦相似度计算公式:
cos(θ)=∑i=1nAiBi∑i=1nAi2∑i=1nBi2cos(θ) = \frac{\sum_{i=1}^n A_i B_i}{\sqrt{\sum_{i=1}^n A_i^2} \sqrt{\sum_{i=1}^n B_i^2}}cos(θ)=∑i=1nAi2∑i=1nBi2∑i=1nAiBi
公式说明:
- 分子是向量A和向量B的点积
- 分母是向量A和向量B的模长的乘积
- 结果范围在[-1,1]之间,值越大表示越相似
除了余弦相似度,还有其他计算方法:
- 皮尔逊相关系数
- Jaccard相似系数
- 欧几里得距离
算法实现流程
1. 收集用户对物品的评分数据
将用户对物品的评分数据组织成矩阵 R,其中 R[u][i] 表示用户 u 对物品 i 的评分:
- R[u][i] ∈ [1,5]:表示用户的评分值
- R[u][i] = 0:表示用户未对该物品评分
例如:
| 用户\物品 | 物品1 | 物品2 | 物品3 | 物品4 |
|---|---|---|---|---|
| 用户1 | 5 | 3 | 0 | 4 |
| 用户2 | 4 | 0 | 2 | 5 |
2. 构建物品共现矩阵
共现矩阵 W 记录了物品之间被同一用户同时评分的次数:
- W[i][j] 表示物品i和物品j被同一用户同时评分的次数
- W 是一个对称矩阵,即 W[i][j] = W[j][i]
计算公式:
Wi,j=∑u∈U{1,if Ru,i>0 and Ru,j>00,otherwiseW_{i,j} = \sum_{u \in U} \begin{cases} 1, & \text{if } R_{u,i} > 0 \text{ and } R_{u,j} > 0 \\ 0, & \text{otherwise} \end{cases}Wi,j=u∈U∑{1,0,if Ru,i>0 and Ru,j>0otherwise
其中:
- U 是所有用户的集合
- R_{u,i} 表示用户u对物品i的评分
3. 计算物品间的相似度
使用余弦相似度计算物品之间的相似程度,得到相似度矩阵 S:
Si,j=Wi,j∑kWi,k⋅∑kWj,kS_{i,j} = \frac{W_{i,j}}{\sqrt{\sum_{k}W_{i,k} \cdot \sum_{k}W_{j,k}}}Si,j=∑kWi,k⋅∑kWj,kWi,j
其中:
- S[i][j] 表示物品i和物品j的相似度
- W[i][j] 是共现矩阵中物品i和j的共现次数
- 分母部分是物品i和j各自的共现次数的平方和的乘积的平方根
4. 基于相似度为用户推荐物品
对于用户u,预测其对未评分物品i的可能评分:
Pu,i=∑j∈N(i)Si,j⋅Ru,j∑j∈N(i)∣Si,j∣P_{u,i} = \frac{\sum_{j \in N(i)} S_{i,j} \cdot R_{u,j}}{\sum_{j \in N(i)} |S_{i,j}|}Pu,i=∑j∈N(i)∣Si,j∣∑j∈N(i)Si,j⋅Ru,j
其中:
- P_{u,i} 是预测的用户u对物品i的评分
- N(i) 是与物品i最相似的K个物品的集合
- S_{i,j} 是物品i和j的相似度
- R_{u,j} 是用户u对物品j的实际评分
推荐步骤:
- 对用户未评分的每个物品计算预测评分
- 选择预测评分最高的N个物品作为推荐结果
- 可以设置评分阈值,只推荐预测评分超过阈值的物品
这个预测公式本质上是一个加权平均,权重由物品间的相似度决定。相似度越高的物品,其评分对预测结果的影响越大。
Golang实现
1. 数据准备
首先,我们需要用户-物品评分矩阵作为输入数据:
| 用户\物品 | 物品1 | 物品2 | 物品3 | 物品4 |
|---|---|---|---|---|
| 用户1 | 5 | 3 | 0 | 4 |
| 用户2 | 4 | 0 | 2 | 5 |
| 用户3 | 0 | 4 | 5 | 0 |
| 用户4 | 3 | 5 | 0 | 2 |
| 用户5 | 0 | 2 | 4 | 3 |
说明:
- 评分范围:1-5分
- 0表示用户未对该物品评分
- 矩阵中的每一行代表一个用户对所有物品的评分
- 每一列代表一个物品获得的所有用户评分
2. 构建共现矩阵
共现矩阵表示不同物品被同一用户同时评分的次数:
package main
import "fmt"
func main() {
// 用户-物品评分矩阵
// 行代表用户,列代表物品
// 评分范围1-5,0表示未评分
ratings := [][]int{
{5, 3, 0, 4},
{4, 0, 2, 5},
{0, 4, 5, 0},
{3, 5, 0, 2},
{0, 2, 4, 3},
}
// 初始化物品共现矩阵
numItems := len(ratings[0])
coOccurrence := make([][]int, numItems)
for i := range coOccurrence {
coOccurrence[i] = make([]int, numItems)
}
// 计算共现矩阵
for _, userRatings := range ratings {
for i := 0; i < numItems-1; i++ {
// 只有当用户对物品i有评分时才继续
if userRatings[i] > 0 {
// 只需要检查i之后的物品,避免重复计算
for j := i + 1; j < numItems; j++ {
// 当用户对物品j也有评分时,增加共现次数
if userRatings[j] > 0 {
coOccurrence[i][j]++
coOccurrence[j][i]++ // 矩阵是对称的,同时更新对称位置
}
}
}
}
}
fmt.Println("物品共现矩阵:")
for _, row := range coOccurrence {
fmt.Println(row)
}
}
- 计算物品之间的相似度
func similarity(ratings [][]int) [][]float64 {
numItems := len(ratings[0])
similarity := make([][]float64, numItems)
for i := range similarity {
similarity[i] = make([]float64, numItems)
}
// 首先计算共现矩阵
coOccurrence := make([][]int, numItems)
for i := range coOccurrence {
coOccurrence[i] = make([]int, numItems)
}
// 计算共现矩阵
for _, userRatings := range ratings {
for i := 0; i < numItems-1; i++ {
if userRatings[i] > 0 {
for j := i + 1; j < numItems; j++ {
if userRatings[j] > 0 {
coOccurrence[i][j]++
coOccurrence[j][i]++
}
}
}
}
}
// 基于共现矩阵计算相似度
for i := 0; i < numItems; i++ {
for j := 0; j < numItems; j++ {
if i != j {
// 使用余弦相似度
// cos(i,j) = (i·j) / (||i|| * ||j||)
dotProduct := float64(coOccurrence[i][j]) // 点积就是共现次数
// 计算向量的模
normI := 0.0
normJ := 0.0
for k := 0; k < numItems; k++ {
normI += float64(coOccurrence[i][k] * coOccurrence[i][k])
normJ += float64(coOccurrence[j][k] * coOccurrence[j][k])
}
// 计算余弦相似度
if normI > 0 && normJ > 0 {
similarity[i][j] = dotProduct / (math.Sqrt(normI) * math.Sqrt(normJ))
}
}
}
}
fmt.Println("物品相似度矩阵:")
for _, row := range similarity {
fmt.Println(row)
}
return similarity
}
- 根据相似度为用户推荐物品
// recommendItems 基于用户评分和物品相似度矩阵推荐物品
// 参数:
// - userRatings: 用户对所有物品的评分数组,0表示未评分
// - similarity: 物品之间的相似度矩阵
//
// 返回:
// - []int: 推荐物品的索引数组
func recommendItems(userRatings []int, similarity [][]float64) []int {
// 获取物品总数
numItems := len(userRatings)
// 创建预测评分数组
predictedRatings := make([]float64, numItems)
// 对用户未评分的物品进行评分预测
for i := 0; i < numItems; i++ {
if userRatings[i] == 0 {
predictedRatings[i] = predictRating(userRatings, similarity, i)
}
}
// 打印预测评分
fmt.Println("预测评分:", predictedRatings)
// 找出预测评分最高的物品
maxRating := -1.0
recommendedItems := []int{}
// 遍历所有预测评分,找出评分最高的物品
for i, rating := range predictedRatings {
if rating > maxRating {
// 如果发现更高的评分,更新最高评分和推荐列表
maxRating = rating
recommendedItems = []int{i}
} else if rating == maxRating {
// 如果评分相同,将该物品也添加到推荐列表中
recommendedItems = append(recommendedItems, i)
}
}
return recommendedItems
}
// predictRating 使用基于物品的协同过滤算法预测用户对特定物品的评分
// 参数:
//
// userRatings: 单个用户对所有物品的评分数组
// similarity: 物品之间的相似度矩阵
// itemIndex: 要预测评分的物品索引
func predictRating(userRatings []int, similarity [][]float64, itemIndex int) float64 {
// 初始化分子和分母
numerator, denominator := 0.0, 0.0
// 遍历用户对所有物品的评分
for i, rating := range userRatings {
// 只考虑用户已评分的物品(rating > 0)且不是目标物品
if rating > 0 && i != itemIndex {
// 分子累加:用户评分 * 物品相似度
numerator += float64(rating) * similarity[itemIndex][i]
// 分母累加:相似度的绝对值
denominator += math.Abs(similarity[itemIndex][i])
}
}
// 如果分母为0(即没有相似的物品),返回0
if denominator == 0 {
return 0
}
// 返回加权平均值作为预测评分
return numerator / denominator
}
算法优化建议
-
数据预处理
- 处理异常值和缺失值
- 进行数据归一化
-
性能优化
- 使用稀疏矩阵存储数据
- 对相似度矩阵进行缓存
- 设置相似度阈值,过滤低相似度的物品
-
推荐结果优化
- 引入时间衰减因子
- 考虑物品的热度惩罚
- 增加多样性推荐
总结
基于物品的协同过滤算法实现相对简单,适合作为推荐系统的入门算法。它的优点是:
- 实现简单直观
- 不需要物品的内容特征
- 可以发现用户潜在兴趣
局限性:
- 冷启动问题
- 稀疏矩阵问题
- 无法利用物品的内容特征
参考资料
- 推荐系统实践 - 项亮
- Item-Based Collaborative Filtering Recommendation Algorithms - Sarwar et al.
424

被折叠的 条评论
为什么被折叠?



