找回密码
 立即注册
首页 业界区 安全 协同过滤算法深入:BPR 与矩阵分解的工程实现 ...

协同过滤算法深入:BPR 与矩阵分解的工程实现

鄂缮输 前天 14:20
协同过滤是推荐系统的核心算法。本文将用工程师的视角,深入解析 BPR 算法,避免复杂数学,重点理解"为什么这么设计"。
目录


  • 协同过滤的直观理解
  • 矩阵分解:降维的艺术
  • BPR算法:成对学习的智慧
  • 源码剖析:Gorse 的 BPR 实现
  • 训练优化技巧
  • 实战:手写简化版 BPR
  • 性能调优指南
协同过滤的直观理解

什么是协同过滤?

场景:你在选电影
  1. 传统方式(基于内容):
  2. 你喜欢科幻片 → 推荐科幻片
  3. 协同过滤:
  4. 和你口味相似的人喜欢X → 推荐X给你
复制代码
"协同"的含义:利用群体智慧
  1. alice 喜欢:A, B, C
  2. bob 喜欢:  A, B, D
  3. charlie 喜欢:A, C, E
  4. 观察:
  5. - alice 和 bob 都喜欢 A, B → 口味相似
  6. - 推荐:D 给 alice(bob 喜欢但 alice 没看过)
  7. - 推荐:C 给 bob(alice 喜欢但 bob 没看过)
复制代码
两种协同过滤

User-Based(基于用户)
  1. 1. 找到和你相似的用户
  2. 2. 看他们喜欢什么
  3. 3. 推荐给你
  4. 问题:
  5. - 用户数量大时计算慢(100万用户 → 100万²次比较)
  6. - 用户兴趣变化快
复制代码
Item-Based(基于物品)
  1. 1. 找到你喜欢物品的相似物品
  2. 2. 推荐相似物品
  3. 优点:
  4. - 物品数量相对稳定
  5. - 可以预计算
复制代码
Matrix Factorization(矩阵分解)
  1. 最现代的方法!
  2. - 用向量表示用户和物品
  3. - 通过机器学习找到最佳向量
  4. - 这就是我们要讲的重点
复制代码
矩阵分解:降维的艺术

问题引入

用户-物品交互矩阵:
  1.          movie1  movie2  movie3  movie4  movie5
  2. alice      1       0       1       0       0
  3. bob        0       1       0       1       0
  4. charlie    1       0       0       0       1
  5. 1 = 喜欢,0 = 未交互
  6. 问题:
  7. 1. 矩阵很稀疏(99% 是 0)
  8. 2. 无法预测未交互的(?)
复制代码
矩阵分解的想法

核心思想:用低维向量表示用户和物品
  1. 原始矩阵:100万用户 × 100万物品 = 1万亿个数
  2. 分解后:
  3. - 用户向量:100万 × 50维 = 5000万个数
  4. - 物品向量:100万 × 50维 = 5000万个数
  5. - 总计:1亿个数
  6. 压缩率:1万亿 / 1亿 = 10000 倍!
复制代码
数学表示(简化版)
  1. 评分预测:
  2. r̂ = 用户向量 · 物品向量
  3. 具体例子:
  4. alice 的向量:[0.8, 0.2, 0.5]  (3维)
  5. movie1 的向量:[0.9, 0.1, 0.6]
  6. 预测 alice 对 movie1 的评分:
  7. r̂ = 0.8×0.9 + 0.2×0.1 + 0.5×0.6
  8.   = 0.72 + 0.02 + 0.3
  9.   = 1.04  (归一化后接近 1,表示喜欢)
复制代码
向量的含义(可解释性)

假设用 3 维向量表示:
  1. 维度1: 科幻程度
  2. 维度2: 动作程度  
  3. 维度3: 文艺程度
  4. alice 的向量:[0.9, 0.2, 0.1]
  5. → 非常喜欢科幻,不太喜欢动作,不喜欢文艺
  6. 电影《星际穿越》的向量:[0.95, 0.1, 0.3]
  7. → 科幻片,少量动作,有点文艺
  8. 预测 alice 对《星际穿越》的评分:
  9. 0.9×0.95 + 0.2×0.1 + 0.1×0.3 = 0.905(很高!)
复制代码
注意:实际中向量维度更高(50-200维),含义不一定可解释。
BPR算法:成对学习的智慧

为什么需要 BPR?

传统方法的问题
  1. 问题:预测评分(1-5星)
  2. 数据:alice 给 movie1 打了 5 星
  3. 传统方法:
  4. 目标 = 让预测值接近 5
  5. 问题:
  6. - 推荐系统中大部分是隐式反馈(点击、浏览)
  7. - 没有明确的评分
  8. - 只知道用户喜欢什么,不知道具体多喜欢
复制代码
BPR 的创新
  1. 不预测绝对评分,而是预测相对偏好
  2. 数据:
  3. - alice 看了 movie1(正样本)
  4. - alice 没看 movie2(负样本)
  5. 目标:
  6. 让 score(alice, movie1) > score(alice, movie2)
  7. 这就是"成对学习"(Pairwise Learning)
复制代码
BPR 的核心思想
  1. # 伪代码
  2. for each user:
  3.     positive_item = 用户交互过的物品
  4.     negative_item = 用户没交互过的物品(随机采样)
  5.    
  6.     score_pos = predict(user, positive_item)
  7.     score_neg = predict(user, negative_item)
  8.    
  9.     # 目标:正样本分数 > 负样本分数
  10.     loss = -log(sigmoid(score_pos - score_neg))
  11.    
  12.     # 梯度下降更新参数
  13.     update_parameters()
复制代码
为什么用 sigmoid?
  1. score_pos - score_neg 的范围:(-∞, +∞)
  2. sigmoid(x) = 1 / (1 + e^(-x))
  3. - x > 0 时,sigmoid(x) → 1(正样本分数高,好!)
  4. - x < 0 时,sigmoid(x) → 0(负样本分数高,不好)
  5. - x = 0 时,sigmoid(x) = 0.5(分不清)
  6. -log(sigmoid(x)):
  7. - x >> 0 时,loss → 0(已经很好了)
  8. - x << 0 时,loss → ∞(很差,需要优化)
  9. - x = 0 时,loss = 0.69(中等)
复制代码
学习率(lr)
  1. func sampleNegative(user User, items []Item, interacted Set) Item {
  2.     for {
  3.         idx := rand.Int() % len(items)
  4.         item := items[idx]
  5.         if !interacted.Contains(item.ID) {
  6.             return item  // 找到一个未交互的
  7.         }
  8.     }
  9. }
复制代码
正则化(reg)
  1. // 按热度的平方根采样(降低热门物品权重)
  2. func sampleNegativeByPopularity(items []Item, popularity []int) Item {
  3.     weights := make([]float64, len(items))
  4.     for i, pop := range popularity {
  5.         weights[i] = math.Sqrt(float64(pop))  // 平方根
  6.     }
  7.     return weightedRandomSample(items, weights)
  8. }
复制代码
Gorse 的 AutoML

Gorse 使用 TPE(Tree-structured Parzen Estimator)自动搜索最佳超参数:
  1. // model/cf/model.go
  2. type BPR struct {
  3.     BaseMatrixFactorization
  4.    
  5.     // 超参数
  6.     nFactors int       // 向量维度(默认50)
  7.     nEpochs  int       // 训练轮数(默认100)
  8.     lr       float32   // 学习率(默认0.05)
  9.     reg      float32   // 正则化系数(默认0.01)
  10.    
  11.     // 模型参数
  12.     UserFactor [][]float32  // 用户向量 [n_users × n_factors]
  13.     ItemFactor [][]float32  // 物品向量 [n_items × n_factors]
  14. }
复制代码
效果
  1. func NewBPR(params Params) *BPR {
  2.     bpr := &BPR{
  3.         nFactors: params.GetInt("n_factors", 50),
  4.         nEpochs:  params.GetInt("n_epochs", 100),
  5.         lr:       params.GetFloat("lr", 0.05),
  6.         reg:      params.GetFloat("reg", 0.01),
  7.     }
  8.     return bpr
  9. }
  10. // 初始化向量(小随机数)
  11. func (bpr *BPR) Init(trainSet dataset.CFSplit) {
  12.     nUsers := trainSet.CountUsers()
  13.     nItems := trainSet.CountItems()
  14.    
  15.     // 用户向量
  16.     bpr.UserFactor = make([][]float32, nUsers)
  17.     for i := range bpr.UserFactor {
  18.         bpr.UserFactor[i] = make([]float32, bpr.nFactors)
  19.         for j := range bpr.UserFactor[i] {
  20.             // 小随机数初始化(-0.01 到 0.01)
  21.             bpr.UserFactor[i][j] = (rand.Float32() - 0.5) * 0.02
  22.         }
  23.     }
  24.    
  25.     // 物品向量(同样方式)
  26.     // ...
  27. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册