第三章:矩阵分解技术——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 调参实战:那些年我踩过的坑
最后分享几个调参经验:
- 学习率η:我一般从0.01开始,每轮乘以0.9。如果loss震荡,就调小一点。
- 正则化系数λ:0.02是个不错的起点。太大导致欠拟合,太小容易过拟合。
- 隐因子数量k:用验证集来选。我见过一个极端案例——k从10调到200,RMSE只降了1%,但训练时间涨了20倍。这时候就要权衡了。
- 初始化:用均值为0、标准差0.1的正态分布。别用0初始化,否则所有梯度一样,学不出来。
避坑指南:我曾经犯过一个低级错误——忘记对评分做归一化。结果用户A平均打3分,用户B平均打5分,模型学出来的偏置项b_u直接等于2,完全没学到个性化。后来我先把评分减去全局均值,再训练,效果立刻提升。
好了,矩阵分解这块就讲到这里。下一章我们会聊更进阶的FM和FFM模型。记住一句话:矩阵分解是推荐系统的基石,搞懂它,后面的路就好走了。