想象你经营着一家火锅店,菜单上有上百道菜,常客也有几百位。你很想给每位顾客推荐“他爱吃的那一口”,但手里的评分表却空了一大半——顾客很少主动打分。店里两位负责人想了个办法:大厨先把每道菜的“风味特征”列出来(辣度、麻度、鲜度),口味经理再根据这些特征估计每位顾客的口味偏好;反过来,口味经理的口味表一定,大厨又重新调整菜品特征。两人就这样一轮轮“交替”着校准,最后即使空白的评分格也能被推测出来。这个思路,就是推荐系统里鼎鼎大名的 ALS(Alternating Least Squares,交替最小二乘)协同过滤算法。它把复杂的“顾客—菜品评分表”拆成两张小表,再通过轮流优化的方式,让它们越来越接近真实偏好。
掌握 ALS,是成为推荐系统工程师的基础能力。无论做视频、电商还是音乐推荐,你都会用到它来从海量行为数据中挖掘用户兴趣,并为大规模分布式推荐系统打下核心算法功底。
可视化演示
展示Seq2seq编码器将输入序列压缩为上下文向量,解码器基于上下文自回归生成多步预测的端到端过程,可切换编码、解码、完整流程三种模式观察。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Seq2seq多步预测流程 编码器逐步读取输入序列,将信息压缩为固定维度的上下文向量 上下文向量作为解码器初始状态,承载全部输入序列信息 解码器自回归生成多步预测,绿色为预测值,红色为真实值参考 h_t = f(h_{t-1}, x_t), c = h_T, y_t = g(c, y_{t-1}) 完整流程展示编码到解码的端到端过程
矩阵分解原理
火锅店的评分表上,第 u 行第 i 列是顾客 u 对菜品 i 的打分。但大部分格子是空的,因为很少有顾客每道菜都尝过。大厨和口味经理决定换个思路:不直接填满大表,而是把大表“拆”成两张小表。一张是顾客口味表,记录每位顾客对“辣度、麻度、鲜度”等 k 个口味的偏好;另一张是菜品风味表,记录每道菜的 k 个风味标签。只要把两个向量的对应分量相乘再累加,就能估算出顾客对菜品的喜爱程度。
在数学上,这个稀疏的用户—物品评分矩阵记为 \(\mathbf{R}\),大小为 \(m \times n\);拆出来的顾客口味表叫用户因子矩阵 \(\mathbf{P}\),大小为 \(m \times k\);菜品风味表叫物品因子矩阵 \(\mathbf{Q}\),大小为 \(n \times k\)。\(k\) 远小于 \(m\) 和 \(n\),所以这种分解也叫低秩近似。由于隐因子是算法自动从行为中学出来的,不必人解释具体含义,因此它们又被称为“隐因子”。
回到火锅店:这道菜“辣度 4、麻度 2、鲜度 5”,顾客“爱吃辣、怕麻、爱鲜”,两者点积后得分很高,推荐就产生了。矩阵分解正是这样把“大而不全”的评分表,压缩成了两张人人都能算的小名片。
- 预测评分顾客 u 对菜品 i 的预测值 \(\hat{r}_{ui}\) 由用户隐向量和物品隐向量点积得到。
- 隐因子加和将 k 个隐因子上的分量相乘后累加,得到最终预测值。
- 低秩规模k 是隐因子数,远小于用户数和物品数,成为压缩后的数据规模。
ALS交替优化
两张表应该怎么同时调?如果两位经理同时改,你等我、我等你会陷入僵局。ALS 的办法是“轮流来”:大厨先把所有菜品的风味标签定住,口味经理根据这些标签,用最小二乘为每位顾客计算一份最匹配的口味表;然后调过来,口味表固定,大厨再重新调整每道菜的标签。如此交替,每次只解一个简单的最小化问题,避免了一次性求解两个未知矩阵的复杂非凸问题。
用数学语言说,先固定 \(\mathbf{Q}\),则 \(\mathbf{Q}\) 成了已知量,对每个用户 u 的目标变成一个只含 \(\mathbf{p}_u\) 的二次函数;它是最小二乘问题,有唯一闭合解。更新完所有用户的 \(\mathbf{p}_u\) 后,再固定 \(\mathbf{P}\),同样求解每个物品的 \(\mathbf{q}_i\)。一轮轮交替下去,损失逐渐下降并收敛。
两位经理不需要一次性推出完美答案。只要大厨先给出“菜单特征初稿”,口味经理就能算出最优口味;口味一定,大厨又能修正菜单。如此循环几轮,两张表的匹配度越来越高,预测也越来越准。
- 固定物品矩阵把 \(\mathbf{q}_i\) 看作常数,只寻找最优的 \(\mathbf{p}_u\)。
- 平方误差\(r_{ui}\) 是真实评分,\(\mathbf{p}_u \mathbf{q}_i^T\) 是当前预测,差值越小越好。
- 正则项\(\lambda \|\mathbf{p}_u\|^2\) 对过大的向量元素加以惩罚,防止过拟合。
损失函数推导
如何衡量“调得好不好”?老板引入了“差评平方”考核:预测和实际差 1 分,记 1 分惩罚;差 2 分,记 4 分;差得越多扣分越重。把所有有记录格子的平方误差加起来,就是模型的总误差。但光看误差还不够——如果两位经理为了讨好少数顾客,把特征值调得极端夸张,遇到没吃过的顾客就会乱预测。所以还要加一条“员工守则”:向量值不要太大,这就是正则项。
把平方误差和正则加到一起,就得到了 ALS 要优化的整体目标函数。其中第一个累加覆盖所有有观测评分的 \((u,i)\) 对,第二项和第三项分别约束用户向量和物品向量的大小。这个函数越小,说明模型对已知评分的拟合越好,同时泛化能力也有保障。
火锅店的“综合满意度考核”既看预测准不准,也看方案稳不稳。把这两点放进一个函数里,ALS 的一切迭代更新,都为了让这个分数不断下降。
- 观测评分集\(\mathcal{R}\) 表示矩阵中所有已知评分的下标集合,缺失项不参与误差计算。
- 平方误差项真实分与预测分的平方差,惩罚越狠越能逼着模型逼近真实。
- 用户正则对所有用户向量的平方和进行惩罚,避免某些顾客口味值异常大。
- 物品正则同理约束所有菜品向量,使模型面对新组合时更稳定。
隐式反馈处理
不是每道菜顾客都会留下评分。更多时候,他们只是进店转了一圈、在菜单前多停了几秒、或者默默涮完一整盘毛肚。这些行为没有“4分”或“5分”,但同样透露偏好。ALS 的隐式反馈版本,正是为这类数据而生:把“看过的菜”记为 1,“没看过的菜”记为 0,再用行为的强度给每个样本配上置信度权重,比如看了 10 分钟比看了 10 秒更可信。
加权后的目标函数中,每一项都乘上一个 \(c_{ui}\)。\(c_{ui}\) 越大,表示这个行为越强烈的样本越要被认真拟合;而 0 值样本也会以较小权重参与训练,让模型学会“没看不代表绝对讨厌”。这样,原来缺失的信息也能被利用起来。
顾客在菜单前停留越久,老板就越确信他喜欢这道菜。隐式反馈 ALS 正是通过这样的信心理念,把记不住的“路过”也变成推荐依据。
- 隐式偏好值\(p_{ui}\) 表示用户是否对物品产生过行为,如看过记 1,没看过记 0。
- 置信度权重\(c_{ui} = 1 + \alpha t_{ui}\),其中 \(t_{ui}\) 是行为强度,alpha 控制信任度增长速度。
- 加权误差预测行为值与真实行为值的差,按置信度加权后平方,\(c_{ui}\) 越大越重要。
并行化实现
火锅店开了 100 家分店。如果大厨把菜品特征定好,每位顾客的口味估计就只跟顾客自己的数据有关,天然可以交给不同分店同时算。ALS 最迷人的地方就在这:固定 \(\mathbf{Q}\) 后,每个用户向量 \(\mathbf{p}_u\) 只依赖该用户的评分行和 \(\mathbf{Q}\),彼此完全独立,因此可以按用户行把数据切分到不同机器并行更新;固定 \(\mathbf{P}\) 后,同理可以按物品列并行更新。
在 Spark 这样的分布式平台上,用户矩阵按行分区存储,物品矩阵作为广播变量发送到每个工作节点。每轮交替就是两次大规模并行求解:先并行更新所有用户向量,再重新分区、并行更新所有物品向量。用户向量的闭合解公式如下:
各分店算完自己负责的顾客后,把结果汇总,再广播新的菜品特征,进入下一轮。正是这种“分店并行、总部协调”的模式,让 ALS 能扛住上亿用户和上千万商品的数据规模。
- 子矩阵\(\mathbf{Q}_{\mathcal{R}_u}\) 是用户 u 有历史交互的那些物品组成的行子矩阵。
- 正则化系数矩阵\(\lambda \mathbf{I}\) 是 k 阶单位矩阵乘上正则系数,保证矩阵可逆。
- 右侧向量\(\mathbf{r}_u\) 是用户 u 的评分/反馈向量,\(\mathbf{Q}_{\mathcal{R}_u}^T \mathbf{r}_u\) 汇集了历史信息的加权和。
- 矩阵求逆整个表达式即经典岭回归解,得到本轮用户向量的最优值。
部署与评估
因子矩阵训练好后,推荐系统进入落地阶段。对用户 u,把未交互过的物品 i 代入预测公式,得到 \(\hat{r}_{ui}\),按预测分从高到低排出 Top-N 列表,就是最常见的离线推荐产出。上线后还得量化效果:把历史数据切出测试集,将预测分与真实分比较,计算 RMSE 等指标,衡量模型预测误差是否可接受。
RMSE 对“大偏差”尤其敏感,能清晰反映预测是否离谱。实践中还会配合精确率、召回率等排序指标评估推荐清单质量。模型也可以定期用新增行为数据增量更新,让推荐保持新鲜。
火锅店把“私人订制菜单”试运行两周,看回头客是否增加、差评是否减少,再用这些反馈回头调模型。部署与评估就是推荐系统的“试菜环节”,不断校准,直到每桌客人都觉得这菜单像为自己量身定制。
- 测试集\(T\) 是留出的真实评分集合,用于模拟“未来行为”。
- 误差平方真实评分与预测评分的差平方,放大大偏差的惩罚。
- 平均与开方对所有测试样本求平均后开平方根,单位与原始评分一致。
AI 应用场景
视频平台个性化推荐
阶段:离线模型训练 + 在线 TopN 推荐
解决的问题:用户看视频很少打分,传统基于统计的推荐无法挖掘“看完一半”“反复回味”等隐式行为;ALS 隐式反馈版本能根据观看时长、点赞、收藏等行为,给每个视频学习低维向量,并实时产出候选视频列表。
具体案例:某视频 App 每天产生数十亿条播放记录。工程师利用 Spark ALS 将“观看时长”转换成置信度权重,训练用户因子和视频因子;离线生成每个用户 Top 100 候选,再交给线上排序层精排。
电商平台购物篮推荐
阶段:数据预处理 + 矩阵分解建模
解决的问题:电商平台用户购买行为极其稀疏,ALS 能同时融入点击、加购、购买等不同强度的行为,解决“买了 A 的人还想买什么”的关联推荐问题。
具体案例:某电商平台对近 30 天用户行为构造隐式反馈矩阵,用 ALS 训练得到商品向量,在商品详情页展示“看了又看”“搭配购”,点击率提升约 15%。
音乐应用口味迁移
阶段:隐式反馈建模 + 个性化歌单生成
解决的问题:歌单偏好很难用评分表达,但播放次数、收藏、跳过行为能间接表达喜好。ALS 的因子向量还能迁移到相似歌手、相似歌单计算,让新歌也能被推荐。
具体案例:某音乐 App 用 ALS 对用户播放序列生成“口味向量”,再利用向量相似度扩展相似歌曲池,冷门歌手的新歌因此获得了更多曝光。
Python 演示
import numpy as np class ALS: def __init__(self, k=5, _lambda=0.1, iterations=20): self.k = k self._lambda = _lambda self.iterations = iterations self.P = None self.Q = None self._loss = float('inf') def fit(self, R): m, n = R.shape rng = np.random.default_rng(42) self.P = rng.normal(size=(m, self.k)) self.Q = rng.normal(size=(n, self.k)) for it in range(self.iterations): for u in range(m): self.P[u] = self._solve(R[u], self.Q) for i in range(n): self.Q[i] = self._solve(R[:, i], self.P) self._loss = self._compute_loss(R) print(f"iter {it + 1}, loss {self._loss:.4f}") def _solve(self, r, M): idx = np.where(r != 0)[0] if len(idx) == 0: return np.zeros(self.k) A = M[idx].T @ M[idx] + self._lambda * np.eye(self.k) b = M[idx].T @ r[idx] return np.linalg.solve(A, b) def _compute_loss(self, R): mask = R != 0 pred = self.P @ self.Q.T err = (R[mask] - pred[mask]) ** 2 reg = self._lambda * (np.sum(self.P ** 2) + np.sum(self.Q ** 2)) return float(np.sqrt(np.mean(err)) + reg) def predict(self, u, i): return float(self.P[u] @ self.Q[i]) if __name__ == "__main__": R = np.zeros((20, 15)) rng = np.random.default_rng(0) for _ in range(80): u, i = rng.integers(0, 20), rng.integers(0, 15) R[u, i] = rng.integers(1, 6) model = ALS(k=4, _lambda=0.1, iterations=15) model.fit(R) print("predict(0, 3):", model.predict(0, 3))
self.P[u] = self._solve(R[u], self.Q)固定物品矩阵,用最小二乘更新用户 u 的因子向量。self.Q[i] = self._solve(R[:, i], self.P)固定用户矩阵,再更新物品 i 的因子向量。idx = np.where(r != 0)[0]只选有记录的非零位置,跳过缺失项。A = M[idx].T @ M[idx] + self._lambda * np.eye(self.k)构造带正则的线性方程组系数矩阵。b = M[idx].T @ r[idx]构造线性方程组的右侧常数向量。return np.linalg.solve(A, b)直接解出该向量的闭合最优解。pred = self.P @ self.Q.T用两个因子矩阵重建完整的预测评分表。err = (R[mask] - pred[mask]) ** 2只计算真实评分对应位置的平方误差。reg = self._lambda * (np.sum(self.P ** 2) + np.sum(self.Q ** 2))累加用户和物品向量的正则惩罚。model.fit(R); print("predict(0, 3):", model.predict(0, 3))训练模型并对缺失位置做预测。
常见误区
误区我一直以为 ALS 只能处理用户打星的显式评分,像点击、观看时长这种数据根本没法用 ALS 建模。
正解ALS 有专门的隐式反馈版本,把“看过 = 1、没看过 = 0”作为基础标签,再用置信度权重描述行为强度。观看越久、点击越多,权重越大。所以浏览、收藏、播放时长都是 ALS 的天然输入。
为何常见:大多数入门教材都用 MovieLens 评分数据举例,让人误以为 ALS 天生离不开 1-5 星。
误区k 设得越大,模型表达能力越强,推荐肯定越准,所以直接设个几百。
正解k 太大容易把噪声和个别用户习惯都学进去,导致过拟合,在测试集上误差反而上升。实际中需要通过交叉验证在 k 和泛化能力之间取平衡,常见取值在 10~100 之间。
为何常见:“越复杂越厉害”的直觉常让人忽略低秩近似的本质是要抓主要规律,而不是背下所有细节。
误区矩阵分解不就是 SVD 吗?ALS 只是 SVD 的另一种叫法吧?
正解标准 SVD 要求矩阵必须完整、没有缺失值;而真实评分矩阵稀疏到 99% 都是空。ALS 专门针对稀疏矩阵设计,通过交替最小二乘直接拟合潜在因子,不需要把缺失值先填满。
为何常见:两者都做低秩分解,形式上相似,导致不少人把名字混着用。
误区只要迭代次数够多,误差就会降到 0,正则项加不加无所谓。
正解没有正则项时,闭合解中的矩阵可能不可逆,隐向量元素会变得很大,训练集拟合很好但测试集预测离谱。正则项相当于给向量加了个“限幅器”,换来稳定和泛化。
为何常见:初学阶段往往只盯着训练损失下降,忘了模型上线的目标是预测未知数据。
误区ALS 就是固定一个、更新另一个,做一次不就完事了吗?
正解固定 Q 求出来的 P,是在“Q 还不准”的前提下得到的;同理再算出的 Q 也不准。只有在多轮交替中,P 和 Q 才互相促进、逐步逼近最优。一般需要迭代十几到几十轮,损失曲线才会收敛。
为何常见:从更新公式看,每次都有闭合解,容易误以为一步到位,忽略了两个未知量需要协同逼近。
面试题
Q1. ALS 中“交替”更新的核心含义是什么?
- A.交替调节学习率和正则系数
- B.交替固定用户矩阵和物品矩阵更新另一个← 正确答案
- C.交替使用训练集和验证集误差
- D.交替切换损失函数和正则项
答案:B。ALS 的关键是固定一方、求解另一方,轮换交替直到收敛。学习率和正则系数虽然是超参,但不是“交替”对象;训练集/验证集切换也不属于 ALS 的核心流程。
Q2. 矩阵分解中隐因子数 k 代表的是什么?
- A.用户物品打分矩阵的稀疏比例
- B.推荐列表返回给用户的条数
- C.用户向量与物品向量的维度数← 正确答案
- D.模型训练时迭代的总轮次数
答案:C。k 是隐向量维度,表示用多少个隐因子来刻画用户口味和物品风味。它不是稀疏度、不是 TopN 条数,也不是迭代轮数。
Q3. 推荐系统评估中 RMSE 主要衡量什么?
- A.预测评分与真实评分的偏差大小← 正确答案
- B.推荐列表中物品的覆盖范围
- C.用户点击推荐物品的次数
- D.隐因子矩阵的稀疏程度
答案:A。RMSE 把每个测试样本的预测误差平方后取均值再开根,集中刻画预测值偏离真实值的程度。它不关心覆盖率和点击率,也不衡量因子矩阵结构。
Q4. 在 ALS 用户向量更新公式中,正则项主要的用途是什么?
- A.防止隐向量元素过大而过拟合← 正确答案
- B.让矩阵相乘的计算速度更快
- C.把隐式反馈转成显式评分
- D.将用户向量维数自动降低
答案:A。正则项对向量平方和加惩罚,等于给向量加上限,避免训练集爆表但测试集崩盘。它不加速计算、不转换反馈类型,也不会自动降维。
Q5. 下列哪一项最适合作为隐式反馈?
- A.用户给电影打出五星评分
- B.用户完整看完了一部影片← 正确答案
- C.用户填写了满意度问卷
- D.用户在评论区写下长文
答案:B。完整看完影片是用户自然留下的行为痕迹,没有主动打分,是典型隐式反馈。五星评分和问卷属于显式反馈,评论虽然非结构化,但通常不算评分类隐式反馈。
Q6. 隐式反馈 ALS 相比显式反馈 ALS 的差异包括哪些?
- A.使用置信度权重描述行为强度← 正确答案
- B.只用非零反馈样本更新模型
- C.未观测样本也以低权重参与训练← 正确答案
- D.隐向量维度必须设置为更大值
答案:AC。隐式反馈 ALS 对所有样本加权,观测越多权重越高,未观测样本也以较小权重参与,这是其核心差异。A、C 正确,B 与实际相反,D 不是必须要求。
Q7. ALS 中固定物品矩阵后,更新用户向量的闭合解是什么形式?
- A.岭回归形式的线性方程组求解← 正确答案
- B.对矩阵逐元素做梯度下降
- C.对评分矩阵做奇异值截断
- D.用随机采样替代最优求解
答案:A。固定 Q 后,目标函数变成关于 p_u 的二次型,求导等于 0 得到岭回归形式的闭合解 \((Q^TQ+\lambda I)^{-1}Q^Tr\),不需要梯度下降,也不是 SVD。
Q8. 为什么 ALS 能在大规模数据上高效并行化?
- A.固定一方后每个用户向量可独立更新← 正确答案
- B.损失函数天然是严格凸函数
- C.物品矩阵把用户矩阵压缩到一维
- D.评分矩阵可以被整体放入单机内存
答案:A。固定 Q 后,每个用户向量的更新只依赖自己的评分行和 Q,互不干扰,所以可按行分区并行。损失函数非凸,物品矩阵也不切割成用户维度,D 与大规模场景矛盾。
Q9. 隐式反馈 ALS 的置信度权重常设为 \(c_{ui}=1+\alpha t_{ui}\),其中 \(t_{ui}\) 表示什么?
- A.用户对该物品的行为强度← 正确答案
- B.模型当前的迭代轮次数
- C.评分矩阵的稀疏度
- D.正则项的衰减速率
答案:A。\(t_{ui}\) 是行为强度,比如观看时长、购买次数。行为越强,置信度越高,模型对该样本越重视。它不是迭代轮数、矩阵稀疏度或正则衰减系数。
Q10. 关于 Spark 中 ALS 的并行化实现,下列说法正确的是?
- A.用户矩阵按行分区后并行更新← 正确答案
- B.物品矩阵需要广播到各工作节点← 正确答案
- C.一轮交替结束后需要重新分区交换数据← 正确答案
- D.每次迭代必须把全部数据载入内存
答案:ABC。Spark ALS 把用户矩阵按行分区,物品矩阵广播到各分区,更新完用户后重新分区再更新物品,所以 A、B、C 正确。D 太绝对,Spark 通过分区和 RDD 调度避免一次性全部载入内存,大数据场景下必须控制内存占用。