基于物品的协同过滤算法实现

背景介绍

最近在开发产品的过程中,想为用户推送他们感兴趣的内容。一开始,我尝试在后端通过标签排序算法来为用户推荐,但效果并不理想。不仅需要构建大量的标签体系,后端的维护成本也很高。

后来,我想到了推荐系统。虽然之前从未接触过推荐系统(因为提及推荐系统,首先想到的就是机器学习),但我还是决定开始研究,因为推荐系统是实现用户个性化体验的最佳实践。最终,我选择了基于物品的协同过滤算法(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=1nAi2i=1nBi2i=1nAiBi

公式说明:

  1. 分子是向量A和向量B的点积
  2. 分母是向量A和向量B的模长的乘积
  3. 结果范围在[-1,1]之间,值越大表示越相似

除了余弦相似度,还有其他计算方法:

  • 皮尔逊相关系数
  • Jaccard相似系数
  • 欧几里得距离

算法实现流程

1. 收集用户对物品的评分数据

将用户对物品的评分数据组织成矩阵 R,其中 R[u][i] 表示用户 u 对物品 i 的评分:

  • R[u][i] ∈ [1,5]:表示用户的评分值
  • R[u][i] = 0:表示用户未对该物品评分

例如:

用户\物品物品1物品2物品3物品4
用户15304
用户24025

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=uU{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,kkWj,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=jN(i)Si,jjN(i)Si,jRu,j

其中:

  • P_{u,i} 是预测的用户u对物品i的评分
  • N(i) 是与物品i最相似的K个物品的集合
  • S_{i,j} 是物品i和j的相似度
  • R_{u,j} 是用户u对物品j的实际评分

推荐步骤:

  1. 对用户未评分的每个物品计算预测评分
  2. 选择预测评分最高的N个物品作为推荐结果
  3. 可以设置评分阈值,只推荐预测评分超过阈值的物品

这个预测公式本质上是一个加权平均,权重由物品间的相似度决定。相似度越高的物品,其评分对预测结果的影响越大。

Golang实现

1. 数据准备

首先,我们需要用户-物品评分矩阵作为输入数据:

用户\物品物品1物品2物品3物品4
用户15304
用户24025
用户30450
用户43502
用户50243

说明:

  • 评分范围: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)
	}
}

  1. 计算物品之间的相似度
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
}

  1. 根据相似度为用户推荐物品
// 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
}

算法优化建议

  1. 数据预处理

    • 处理异常值和缺失值
    • 进行数据归一化
  2. 性能优化

    • 使用稀疏矩阵存储数据
    • 对相似度矩阵进行缓存
    • 设置相似度阈值,过滤低相似度的物品
  3. 推荐结果优化

    • 引入时间衰减因子
    • 考虑物品的热度惩罚
    • 增加多样性推荐

总结

基于物品的协同过滤算法实现相对简单,适合作为推荐系统的入门算法。它的优点是:

  • 实现简单直观
  • 不需要物品的内容特征
  • 可以发现用户潜在兴趣

局限性:

  • 冷启动问题
  • 稀疏矩阵问题
  • 无法利用物品的内容特征

参考资料

  1. 推荐系统实践 - 项亮
  2. Item-Based Collaborative Filtering Recommendation Algorithms - Sarwar et al.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值