第三章:矩阵分解技术——SVD分解、FunkSVD、BiasSVD、SVD++算法原理与实现

好,咱们进入推荐系统里最经典的一章——矩阵分解。说实话,这玩意儿我入行时啃了好久才吃透。但一旦搞明白,你会发现很多推荐场景都能用它来解。今天我就带你一步步拆解SVD家族的四位成员:从最原始的SVD分解,到FunkSVD、BiasSVD,再到SVD++。每个算法我都会结合项目里的坑来讲。

3.1 从协同过滤到矩阵分解:为什么要降维?

先问个问题:你用过协同过滤吧?用户-物品评分矩阵,动辄几百万行几百万列。但里面90%都是空的——用户不可能给所有物品打分。这就是稀疏性问题。

我早期做过一个视频推荐项目,用户-视频矩阵稀疏度高达99.8%。用协同过滤算相似度,结果全是0。后来换成矩阵分解,效果立竿见影。

矩阵分解的核心思想很简单:把高维稀疏矩阵,拆成两个低维稠密矩阵的乘积。说白了,就是给每个用户和每个物品,学一个隐向量(latent factor)。用户喜欢什么风格,物品有什么属性,都浓缩在这几十维的向量里。

核心公式:R ≈ P × Q^T

其中R是m×n的评分矩阵,P是m×k的用户隐因子矩阵,Q是n×k的物品隐因子矩阵。k远小于m和n。

3.2 SVD分解:数学上的完美,工程上的噩梦

传统的SVD(奇异值分解)是线性代数里的标准操作。它能把任意矩阵R分解成:R = U × Σ × V^T。然后取前k个奇异值,就能得到近似矩阵。

但问题来了——SVD要求矩阵不能有缺失值。你想想看,评分矩阵里全是空,怎么分解?

我见过不少新手直接拿原始评分矩阵跑SVD,结果报错。嗯,这里要注意:必须先把缺失值填充成0或平均值。但填0会引入大量噪声,填平均值又会模糊用户偏好。

避坑指南:我曾经在一个电商项目里用均值填充+SVD,结果推荐出来的全是热门商品,完全没有个性化。后来才意识到,填充操作把稀疏矩阵变成了稠密矩阵,计算量暴增,而且填充值严重扭曲了原始分布。

所以,传统SVD在推荐系统里基本被淘汰了。除非你的矩阵非常稠密(比如评分率>50%),否则别用。

3.3 FunkSVD:真正的推荐利器

2006年Netflix Prize大赛上,Simon Funk提出了一个天才想法:只分解有评分的那些位置。不去管缺失值,只优化已知评分上的误差。

这就是FunkSVD,也叫隐语义模型(Latent Factor Model)。它的目标函数是:

minimize ∑(r_ui - p_u · q_i)^2 + λ(||p_u||^2 + ||q_i||^2)

其中r_ui是真实评分,p_u·q_i是预测评分,λ是正则化系数。

优化方法用随机梯度下降(SGD)。每次随机选一个已知评分,计算误差,然后更新对应的p_u和q_i:

e_ui = r_ui - p_u · q_i
p_u = p_u + η * (e_ui * q_i - λ * p_u)
q_i = q_i + η * (e_ui * p_u - λ * q_i)

η是学习率。我习惯设0.01左右,然后每轮衰减。

个人经验:隐因子数量k一般取10~200。我做过一个音乐推荐项目,k=50时效果最好。太小欠拟合,太大过拟合。你可以用交叉验证来调。

FunkSVD的代码实现其实很简洁。下面是我常用的训练函数:

def funk_svd(train_data, k=20, lr=0.01, reg=0.02, epochs=20):
    # 初始化用户和物品隐因子矩阵
    P = np.random.normal(0, 0.1, (n_users, k))
    Q = np.random.normal(0, 0.1, (n_items, k))
    
    for epoch in range(epochs):
        for u, i, r in train_data:
            pred = np.dot(P[u], Q[i])
            err = r - pred
            # 更新
            P[u] += lr * (err * Q[i] - reg * P[u])
            Q[i] += lr * (err * P[u] - reg * Q[i])
        # 学习率衰减
        lr *= 0.9
    return P, Q

你看,核心代码就十几行。但效果比协同过滤好太多了。

3.4 BiasSVD:加入偏置项,效果再提升

FunkSVD虽然好,但它忽略了一个事实:不同用户打分的尺度不一样。有的人习惯打高分,有的人手紧。同样,热门物品评分普遍偏高,冷门物品评分偏低。

所以BiasSVD在FunkSVD基础上加了三个偏置项:

  • 全局平均分μ:所有评分的均值
  • 用户偏置b_u:用户u的打分习惯与平均分的偏差
  • 物品偏置b_i:物品i的受欢迎程度与平均分的偏差

预测公式变成:

r̂_ui = μ + b_u + b_i + p_u · q_i

目标函数也相应调整:

minimize ∑(r_ui - μ - b_u - b_i - p_u·q_i)^2 + λ(b_u^2 + b_i^2 + ||p_u||^2 + ||q_i||^2)

更新时,b_u和b_i也要一起更新:

e_ui = r_ui - (μ + b_u + b_i + p_u·q_i)
b_u += lr * (e_ui - reg * b_u)
b_i += lr * (e_ui - reg * b_i)
p_u += lr * (e_ui * q_i - reg * p_u)
q_i += lr * (e_ui * p_u - reg * q_i)

我建议:BiasSVD是工业界最常用的基线模型。我在做新闻推荐时,BiasSVD比FunkSVD的RMSE降低了约8%。别小看这几个偏置项,它们能捕捉到很多显式的模式。

3.5 SVD++:融入隐式反馈

前面几个模型都只用了显式反馈(评分)。但用户的行为远不止评分——点击、浏览、收藏、购买,这些都是隐式反馈。

SVD++的核心改进是:把用户的历史行为也编码进隐向量。具体来说,每个用户除了有p_u,还有一个从历史物品中聚合出来的隐向量:

p_u + |N(u)|^(-1/2) * ∑ y_j

其中N(u)是用户u有过行为的物品集合,y_j是物品j的隐式反馈向量。

预测公式变成:

r̂_ui = μ + b_u + b_i + (p_u + |N(u)|^(-1/2) * ∑ y_j) · q_i

更新时,除了原来的参数,还要更新y_j:

y_j += lr * (e_ui * |N(u)|^(-1/2) * q_i - reg * y_j)

注意:SVD++的计算量比BiasSVD大不少。因为每个用户都要聚合所有历史物品的y_j。我曾经在一个千万级用户的项目里跑SVD++,单机跑了三天三夜。后来改用Spark分布式实现才搞定。

但效果确实好。在Netflix Prize数据集上,SVD++比BiasSVD的RMSE降低了约6%。如果你的场景里有丰富的隐式反馈(比如点击日志),强烈推荐试试。

3.6 四种算法对比与选型建议

我把这四种算法的特点整理成了一张表,方便你选型:

算法 核心思想 优点 缺点 适用场景
SVD分解 完整矩阵分解 数学严谨 需填充缺失值,计算量大 稠密矩阵,学术研究
FunkSVD 只分解已知评分 简单高效,稀疏友好 忽略偏置和隐式反馈 纯评分数据,快速基线
BiasSVD 加入用户/物品偏置 精度更高,解释性强 参数略多 工业推荐系统首选
SVD++ 融入隐式反馈 利用更多行为信号 计算量大,实现复杂 有丰富隐式反馈的场景

我个人习惯:新项目先用BiasSVD打底,效果稳定且容易调参。如果数据里有大量点击、浏览等隐式反馈,再升级到SVD++。至于传统SVD,除非你写论文需要对比,否则别碰。

3.7 调参实战:那些年我踩过的坑

最后分享几个调参经验:

  1. 学习率η:我一般从0.01开始,每轮乘以0.9。如果loss震荡,就调小一点。
  2. 正则化系数λ:0.02是个不错的起点。太大导致欠拟合,太小容易过拟合。
  3. 隐因子数量k:用验证集来选。我见过一个极端案例——k从10调到200,RMSE只降了1%,但训练时间涨了20倍。这时候就要权衡了。
  4. 初始化:用均值为0、标准差0.1的正态分布。别用0初始化,否则所有梯度一样,学不出来。

避坑指南:我曾经犯过一个低级错误——忘记对评分做归一化。结果用户A平均打3分,用户B平均打5分,模型学出来的偏置项b_u直接等于2,完全没学到个性化。后来我先把评分减去全局均值,再训练,效果立刻提升。

好了,矩阵分解这块就讲到这里。下一章我们会聊更进阶的FM和FFM模型。记住一句话:矩阵分解是推荐系统的基石,搞懂它,后面的路就好走了