想象你有一本“干饭日记”:里面按时间顺序记着过去两个月的每一顿饭——加班深夜的麻辣火锅、周末早晨的牛油果沙拉、和朋友聚餐的东北烧烤。当你想预测“下一顿吃什么”时,并不会只盯着最后一顿饭,而是在心里把所有日记条目都过一遍:深夜吃火锅往往因为白天太累,沙拉之后总跟着一杯奶茶……每一个历史行为,都因为“和谁挨着、在什么处境下”而有了新的含义。 推荐系统面临的正是同样的问题:用户点击、购买、观看的序列,就像一本行为日记。自注意力机制(Self-Attention)就是帮系统“复盘日记”的思维过程——让序列中的每个物品与所有其他物品对话,动态计算彼此的关联度;多头注意力(Multi-Head Attention)则是请来多位“美食顾问”,分别从时间规律、口味搭配、消费场景等不同角度审读这本日记。本文就从这本“干饭日记”出发,带你拆解自注意力与多头注意力的原理、位置编码、掩码机制,以及它们在 SASRec 等主流序列推荐模型和多任务推荐系统中的落地之道。
学习自注意力与多头注意力,是解锁 SASRec、BERT4Rec 等主流序列推荐模型的钥匙,也是推荐算法工程师笔试面试绕不开的考点。
可视化演示
展示自注意力机制中Query-Key权重矩阵的softmax归一化过程,以及多头注意力并行捕获不同维度特征的模式,揭示推荐系统中序列建模的核心原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
自注意力权重矩阵 Query-Key点积经softmax归一化为注意力权重 对角线高权重表示自相关性强 Attn(Q,K)=softmax(QK^T/sqrt(d)) 温度越低分布越尖锐
序列推荐背景
小明开始记干饭日记时,只写“今天吃了火锅”。后来他发现,单看这一顿根本解释不了为什么凌晨三点会点麻辣烫。真正有用的信息藏在顺序里:加班 → 想吃重口 → 深夜火锅;沙拉 → 心里委屈 → 奶茶安慰。每一顿饭的含义,都要放在它前面的故事里读。
推荐系统也是一样。传统推荐模型(协同过滤、矩阵分解)把用户抽象成一袋静态偏好——“喜欢辣、偶尔吃草、甜口七分”,却看不到“上周连喝三天奶茶,今天该换清爽的”这种趋势。于是研究者把用户交互记录当作“行为日记”来建模,这就是序列推荐(Sequential Recommendation)。
在数学上,序列推荐把用户最近的交互序列记为 $s_1, s_2, \ldots, s_t$,每个物品先经过嵌入(Embedding)变成一个稠密向量,再按时间顺序堆叠成矩阵,作为后续自注意力层的输入:
有了这本“数字日记”,模型就有了复盘的基础。但更关键的问题是:怎么让每一顿饭在“所有其他饭”的背景下被重新理解?这正是自注意力要解决的事。
- 物品嵌入把离散的物品 ID 映射成稠密的 d 维向量,相当于给每道菜写一份“口味档案”。
- 按序堆叠t 个向量按时间先后从上到下排成矩阵,行号就是日记的页码。
- 形状含义t 是序列长度(回看多少顿饭),d 是嵌入维度(每顿饭描述得多细致)。
自注意力原理
小明开始真正地复盘日记。想预测下一顿,他不会傻盯最后一顿,而是先问自己:现在的处境是什么?加班到崩溃、周末放松、还是刚运动完?这个“当前状态发出的问题”,就是自注意力里的查询(Query)。
序列中的每个物品,也都随身带着“键”(Key)和“值”(Value):键是日记的关键词——菜系、时间、场景;值才是日记正文。模型把问题 Q 与每篇日记的键 K 做匹配打分,分数高说明这篇日记对预测下一顿很有参考价值,然后按分数高低决定“重点读哪几篇”。
打分用的是点积 $\mathbf{Q}\mathbf{K}^\top$,再除以 $\sqrt{d_k}$ 防止数值过大导致 softmax 梯度消失,之后用 softmax 归一化成权重,最后对值 V 做加权求和。每一步都可微,可以端到端训练:
自注意力让每个行为都不再是孤立记录,而是在全部历史中被重新解读。但这只是“一位顾问”的复盘——如果口味顾问和场景顾问看法冲突怎么办?这就引出了多头注意力。
- 点积打分Q 与每个 K 做内积,衡量“当前问题”和“这篇日记关键词”的匹配度。
- 缩放稳定除以 $\sqrt{d_k}$,把分数拉回合理区间,避免 softmax 进入饱和区梯度消失。
- softmax 归一化把分数变成非负且和为 1 的注意力权重,理解为“该参考多少成”。
- 加权聚合用权重对 V 加权求和,得到携带全局上下文的输出向量。
多头注意力机制
一位美食顾问容易有盲区,小明于是请了三位:时间顾问盯着“加班 → 深夜 → 重口”的节奏,口味顾问盯着“辣 → 甜 → 辣”的搭配,场景顾问盯着“工作日便当、周末大餐”的切换。三位各看各的,互不干扰,最后汇总成一份综合判断。
多头注意力就是这个思路:把 Q、K、V 分别通过不同的线性投影矩阵($\mathbf{W}_i^Q, \mathbf{W}_i^K, \mathbf{W}_i^V$)映射到不同的子空间,在每个子空间里独立做一次自注意力,得到第 i 个头 $\mathbf{head}_i$。不同头捕捉不同类型的关系模式。
所有头的结果拼接起来,再乘一个输出投影矩阵 $\mathbf{W}^O$ 完成融合。注意:多头并不增加网络层数,它是在同一层内并行展开多个“视角”:
多头就像让三位顾问分别写下观察,最后由小明汇总判断。工程上它还带来一个意外好处:所有头可以并行计算,速度远快于串行展开的 RNN。
- 线性投影每个头用独立的矩阵把输入压到低维子空间,相当于给顾问分配专属“滤镜”。
- 并行注意力每个头在子空间内独立做一次自注意力,头与头之间互不干扰。
- 结果拼接把 h 个头的输出按列拼接,保留所有视角的信息。
- 融合投影乘 $\mathbf{W}^O$ 把多视角信息压缩回原始维度 d。
位置编码设计
日记如果丢了日期,顺序就全乱了。自注意力有一个数学上的“盲点”——它把序列当集合处理,打乱顺序后算出的注意力一模一样,这叫做置换不变性(Permutation Invariance)。“先火锅后沙拉”和“先沙拉后火锅”,在它眼里没有区别。
为了解决顺序问题,Transformer 系模型会给每个位置加上“位置编码”(Positional Encoding):要么用正弦余弦函数生成固定编码,要么把位置向量作为可学习参数随模型一起训练。编码加到物品向量上,模型就知道“这是第几个行为”。
正弦位置编码的巧妙之处在于:不同维度拥有不同频率,整体类似二进制编码的“低位/高位”,且可以外推到训练时没见过的更长序列:
有了日期戳,自注意力才分得清“先火锅后沙拉”和“先沙拉后火锅”是两段完全不同的故事。可学习位置编码在数据充足时效果更好,相对位置编码则在长序列上更稳健,工业界按场景取舍。
- 位置序号pos 表示物品在序列中的位置,从 0 开始编号。
- 维度分工第 $2i$ 维用正弦、第 $2i+1$ 维用余弦,奇偶配对。
- 频率衰减分母 $10000^{2i/d}$ 随维度增大而增大,频率越来越低,类似尺子上的刻度由密到疏。
- 逐元素相加位置编码与物品嵌入相加后送入注意力层,顺序信息融入每个向量。
掩码因果约束
预测未来有个前提——不能偷看未来。小明预测周五晚饭时,绝不会参考周五之后才发生的日记。但自注意力默认能看到序列的全部位置,如果训练时让它“偷看”,预测时就露馅了,这就是信息泄露(Label Leakage)。
因果掩码(Causal Mask)的做法很直接:在注意力得分矩阵上,把所有“未来位置”对应的分数设为负无穷大。经过 softmax 后,这些位置的权重会变成 0,第 i 个位置就只能看到前 i 个位置的信息。
掩码矩阵 M 是一个上三角为负无穷的矩阵。加上掩码后,模型变成自回归式:每一步输出只依赖过去。这也是 GPT 和 SASRec 的标准做法;而 BERT4Rec 这类模型预测被 mask 的物品时故意用双向注意力,两类模型的本质区别就在这里:
掩码保证了推荐系统不“作弊”:训练时怎么预测,线上就怎么预测。它和位置编码一起,把“无序的集合”重新变成“有序且守规矩的日记本”。
- 构造掩码矩阵第 i 行第 j 列,当 $j > i$(未来)时为负无穷,否则为 0。
- 加到得分上注意力分数加上掩码,未来位置的分数被压到极小。
- softmax 生效负无穷处指数趋近 0,对应权重变为 0。
- 自回归解码t 时刻的输出只能看到 $1$ 到 $t-1$ 的历史,与在线预测一致。
推荐模型实战
把前面所有零件组装起来,就是一台能读日记的机器。经典代表是 SASRec(Self-Attentive Sequential Recommendation):先把用户行为序列的嵌入加上位置编码,送入若干层“带掩码的多头自注意力”,再用最后一个位置的输出预测下一项交互。
SASRec 的每一层都包含多头自注意力、残差连接(Residual Connection)和层归一化(Layer Normalization),结构类似 Transformer 的编码器块。层数越多,模型越能提炼“偏好语义”——从单顿饭的偏好,到“深夜+重口+独食”这种高层次模式。
训练时用交叉熵损失:序列中每个位置都预测“下一个是谁”,共享同一套参数。预测时取最后时间步的输出 $\mathbf{F}_t$,与全部物品嵌入做点积,再 softmax 得到各物品的概率:
SASRec 本质上就是“反复复盘日记 + 严格不看未来 + 预测下一页”。它用自注意力替代 RNN,既支持并行又擅长捕捉长期依赖,是目前序列推荐的事实标准之一。
- 序列表示$\mathbf{F}_t$ 是最后一个行为经过多层多头注意力后得到的向量,浓缩了整本日记。
- 全物品打分$\mathbf{F}_t \mathbf{W}_b^\top + \mathbf{b}$ 对候选物品库里的每个物品打分。
- 概率化softmax 把分数转成概率分布。
- 交叉熵平均对所有训练序列的“下一项预测”求平均负对数似然。
多任务多目标
现实世界没有单一标准。小明在觅食 app 里考虑“下一顿吃什么”时,同时要权衡三个目标:健康吗?买得起吗?做起来快吗?推荐系统同样要同时优化点击率、加购率、转化率等多个目标。
多任务学习(Multi-Task Learning)让一个模型主干同时服务多个目标。最朴素的做法是共享底层编码参数,顶部接多个任务输出头;更进一步用门控(Gating)机制,让不同任务动态地“借用”不同比例的专家子网络,这就是 MMoE(Mixture-of-Experts)的思路。
训练时,总损失是各任务损失的加权和,权重 $\lambda_k$ 可以人工设定,也可以用不确定性加权自动学习:
多任务就像一位主厨同时管理“好吃、快、便宜”三条标准:共享同一套食材(底座参数),但不同目标按需调配不同厨师(专家)。而自注意力与多头注意力,正是这套复杂决策中读懂用户历史的“眼睛”。
- 门控得分$\mathbf{w}_k^\top \mathbf{x}$ 衡量输入 $\mathbf{x}$ 与第 k 个专家的契合程度。
- softmax 归一所有专家的门控权重相加为 1,形成软分配。
- 专家加权融合每个任务按门控比例融合多个专家子网络的输出。
- 损失加权各任务损失按 $\lambda_k$ 加权求和,平衡不同目标的量级与重要度。
AI 应用场景
电商“下一件商品”预测
阶段:召回(Recall)
解决的问题:传统协同过滤只看“买了 A 的用户也买 B”的静态相似,看不到“买了手机之后通常接着买钢化膜”这种顺序规律。自注意力能动态捕捉序列中每次交互的上下文,把“上一个买了什么”变成强信号。
具体案例:淘宝、亚马逊的“经常一起买”和“猜你喜欢”中,模型读入用户最近的浏览/购买序列,用因果掩码的多头注意力预测下一件商品,再进入精排。深夜下单手机壳的用户,下一轮召回里会出现数据线、贴膜,而不是三个月前看过的沙发。
短视频与信息流 Session 推荐
阶段:粗排 / 个性排序
解决的问题:用户连续滑动视频是一条高噪声的会话序列,传统 CTR 模型把每次曝光当独立样本,白白浪费了“连续看了三个搞笑视频”这种节奏信息。序列模型把会话当作干饭日记,自注意力负责找出“哪个视频才是这次刷到底的原因”。
具体案例:抖音、快手的信息流中,模型把用户最近 50 次滑动的视频 ID 作为序列输入,叠加视频类别、作者、时长等特征,多头注意力分别从“内容品类迁移”和“完播节奏”两个视角建模,显著提升人均时长与留存。
广告与电商的多目标排序
阶段:精排(Ranking)
解决的问题:一个推荐结果既要让用户愿意点、愿意加购、还愿意付款,三个目标天然矛盾。共享底座 + 门控的多任务框架,让点击率子任务和转化率子任务共享用户历史行为的注意力表示,又各自保留专属专家,避免“只顾点击不顾转化”。
具体案例:电商大促场景下,MMoE 结构把“点击预估”“加购预估”“支付预估”三个任务挂在同一个多头上,门控网络根据用户实时行为动态分配专家权重,大盘 GMV 与点击率同步提升,同时服务端推理耗时几乎不变。
Python 演示
import numpy as np np.random.seed(42) def softmax(x, axis=-1): e = np.exp(x - x.max(axis=axis, keepdims=True)) return e / e.sum(axis=axis, keepdims=True) class MiniAttention: """迷你多头自注意力,演示前向计算。""" def __init__(self, d_model=16, num_heads=2): self.d_model = d_model self.num_heads = num_heads self.d_head = d_model // num_heads self.WQ = np.random.randn(d_model, d_model) * 0.05 self.WK = np.random.randn(d_model, d_model) * 0.05 self.WV = np.random.randn(d_model, d_model) * 0.05 self.WO = np.random.randn(d_model, d_model) * 0.05 def split_heads(self, x): B, T, D = x.shape x = x.reshape(B, T, self.num_heads, self.d_head) return x.transpose(0, 2, 1, 3) def forward(self, x, mask=None): q = x @ self.WQ k = x @ self.WK v = x @ self.WV qh = self.split_heads(q) kh = self.split_heads(k) vh = self.split_heads(v) scores = qh @ kh.transpose(0, 1, 3, 2) / (self.d_head ** 0.5) if mask is not None: scores = scores + mask weights = softmax(scores) out = weights @ vh B, H, T, Dh = out.shape out = out.transpose(0, 2, 1, 3).reshape(B, T, self.d_model) return out @ self.WO, weights class MiniSASRec: def __init__(self, num_items=20, d_model=16, num_heads=2): self.num_items = num_items self.d_model = d_model self.item_emb = np.random.randn(num_items, d_model) * 0.1 self.pos_emb = np.random.randn(20, d_model) * 0.1 self.attn_layer = MiniAttention(d_model, num_heads) def predict(self, seq): e = self.item_emb[seq] + self.pos_emb[:len(seq)] mask = causal_mask(len(seq)) out, _ = self.attn_layer.forward(e[np.newaxis, :, :], mask=mask) logits = out[0, -1] @ self.item_emb.T return softmax(logits) def causal_mask(length): m = np.zeros((1, 1, length, length)) for i in range(length): m[0, 0, i, i + 1:] = -1e9 return m if __name__ == "__main__": model = MiniSASRec(num_items=20) seq = [3, 7, 12, 18, 5] probs = model.predict(seq) top5 = np.argsort(probs)[::-1][:5] print("历史行为序列:", seq) print("预测下个交互 Top-5 物品:", top5.tolist()) print("对应概率:", [round(float(probs[i]), 4) for i in top5])
qh = self.split_heads(q)把 Q 矩阵切分成 num_heads 个子空间,对应多位“美食顾问”。scores = qh @ kh.transpose(0, 1, 3, 2) / (self.d_head ** 0.5)缩放点积打分,衡量每个历史行为与当前问题的相关度。weights = softmax(scores)把原始分数归一化成非负且和为 1 的注意力权重。out = weights @ vh按权重对值向量加权求和,完成“按相关度记忆”。m[0, 0, i, i + 1:] = -1e9构造因果掩码,把未来位置全部屏蔽。e = self.item_emb[seq] + self.pos_emb[:len(seq)]物品嵌入加位置编码,恢复序列顺序信息。logits = out[0, -1] @ self.item_emb.T取最后位置输出与全物品库做点积,生成候选打分。top5 = np.argsort(probs)[::-1][:5]按概率降序取 Top-5,模拟线上召回结果。
常见误区
误区我以为自注意力就是算出每个历史物品“有多重要”,然后挑最重要的几个,其他没用的丢掉。
正解自注意力不是静态排序,而是“动态加权融合”。权重是针对当前查询(Query)算出来的:同一个历史行为,在“加班后”和“周末早晨”两种情境下权重完全不同;最终输出是所有历史行为的加权求和,而不是选 Top-K 丢弃。就像复盘日记时,没有哪顿饭绝对没用,只是参考程度随处境变化。
为何常见:因为注意力权重天然是“看起来像重要性”的数值,初学者容易把加权求和的机制误解成筛选机制。
误区我以为 head 越多 = 层数越多 = 模型越深,所以多头就是加了几个注意力层。
正解多头是在“同一层内”并行做多次独立的线性投影和注意力计算,再拼接融合,并不增加网络深度。它增加的是“表达宽度”:每个头关注不同子空间的关系模式。好比同一道菜请三位顾问同时尝,而不是让三位顾问排队尝三遍。
为何常见:因为直觉上“多个”很容易联想到“多层叠加”,忽略了并行与串行的结构差异。
误区我觉得自注意力已经看了序列里所有物品,顺序自然就记住了,不用专门管位置。
正解恰恰相反,自注意力对位置“天然失忆”——它把输入当集合,打乱顺序后输出不变,这叫置换不变性。没有位置编码,模型根本分不清“先火锅后沙拉”和“先沙拉后火锅”。位置信息必须显式注入(正弦编码或可学习编码),否则序列推荐毫无意义。
为何常见:因为自注意力确实“看到了所有物品”,让人误以为它连顺序也看到了,忽略了它的数学本质是集合操作。
误区反正训练时数据都在手里,让每个位置都能看到完整序列,信息更多,效果应该更好才对。
正解这是标准的信息泄露陷阱。如果训练时允许看未来,模型会学到“参考答案”;一旦线上预测时必须按时间顺序输出,它就彻底失灵。因果掩码把未来位置设为负无穷,强制训练与推测一致。就像小明预测周五晚饭时,绝不参考周五之后才写的日记。
为何常见:因为离线实验里“偷看”往往能让损失降得更低,让人误以为模型变强了,实则是在自欺欺人。
误区多任务推荐嘛,把点击率损失和转化率损失加在一起反向传播就行了。
正解直接相加有两个问题:一是各任务损失量级不同,大的会主导训练;二是任务间可能负迁移,共享参数互相打架。所以工业界普遍用门控分配专家(如 MMoE)、不确定性加权、动态调节 $\lambda_k$ 等手段,让任务既能共享信息又保持独立。就像主厨分配三名厨师时,要按当天菜量动态调配,而不是每人平均干三分之一。
为何常见:因为“多任务 = 多损失”看起来顺理成章,忽略了损失量级和任务冲突这两个工程关键点。
面试题
Q1. 自注意力中为什么要除以 $\sqrt{d_k}$?
- A.防止点积过大导致梯度消失← 正确答案
- B.让所有分数都小于一
- C.加快矩阵乘法的速度
- D.让权重之和恰好等于一
答案:A。点积结果随维度 $d_k$ 增大而增大,过大数值会让 softmax 进入饱和区,梯度趋近于 0。除以 $\sqrt{d_k}$ 把分数拉回合理范围,稳定训练。B、D 描述的是 softmax 的性质,C 与缩放无关。
Q2. 多头注意力中“头”的本质是什么?
- A.多个并行的线性投影子空间← 正确答案
- B.多个堆叠的注意力网络层
- C.多份参数做简单投票表决
- D.多个损失函数加权求和
答案:A。每个头用独立的 $\mathbf{W}_i^Q, \mathbf{W}_i^K, \mathbf{W}_i^V$ 把输入投影到不同子空间并独立做注意力,最后拼接融合。它增加的是宽度而非深度,B 混淆了层数与头数,C、D 与多头机制无关。
Q3. 自注意力具有置换不变性,序列推荐中如何恢复顺序信息?
- A.加入位置编码保留先后次序← 正确答案
- B.对序列做随机打乱重排
- C.全部换成循环神经网络模型
- D.对物品向量直接取平均处理
答案:A。自注意力把序列当集合,打乱顺序输出不变,必须显式加入位置编码(正弦或可学习)让模型感知先后。B 会彻底丢失顺序,C 是绕开问题而非解决,D 连物品本身的信息都会丢失。
Q4. 训练序列推荐模型时使用因果掩码的目的有哪些?
- A.防止模型偷看未来物品← 正确答案
- B.保证预测过程是自回归的← 正确答案
- C.增加参数量以拟合数据
- D.模拟真实在线预测的场景← 正确答案
答案:ABD。因果掩码把未来位置设为负无穷,经过 softmax 后权重为 0,从而防止信息泄露,并让每一步输出只依赖过去,与线上自回归预测一致。它与参数量无关,C 错误。
Q5. softmax 在注意力机制中的作用是什么?
- A.将注意力权重归一化且和为 1← 正确答案
- B.把向量投影到更高维空间
- C.让最大权重变成一其余为零
- D.对梯度做裁剪防止训练爆炸
答案:A。softmax 把原始分数转成非负且总和为 1 的概率权重,决定每篇“日记”被参考多少成。B 是线性投影的作用,C 是 argmax 或 one-hot 的效果,D 是梯度裁剪,都与 softmax 无关。
Q6. 正弦位置编码的核心优势是什么?
- A.可以外推到更长的序列← 正确答案
- B.完全不需要任何额外计算量
- C.只能编码有限的整数位置
- D.与物品嵌入完全相互等价
答案:A。正弦编码由固定函数生成,任意位置都能算出编码,因此可外推到训练未见过的长度。可学习编码才有位置数量上限,B 说“零计算量”过于绝对,C、D 描述有误。
Q7. 工业界多任务推荐系统中常用的手段有哪些?
- A.共享底层编码器参数← 正确答案
- B.门控网络动态分配专家← 正确答案
- C.对各任务损失加权平衡← 正确答案
- D.只保留点击率单一目标
答案:ABC。共享底座让多任务互相补充数据,门控(如 MMoE)让任务按需借用专家,损失加权解决量级不平衡。D 放弃多目标,与多任务学习的初衷背道而驰。
Q8. 多头注意力拼接完成后还需要做什么?
- A.乘以输出投影矩阵完成融合← 正确答案
- B.直接输出不加任何处理
- C.再做一次 softmax 归一化
- D.按元素取平均并四舍五入
答案:A。拼接后的维度是 $h \cdot d_v$,需要乘以 $\mathbf{W}^O$ 压缩回原始维度 $d$,同时也完成多视角信息的交叉融合。B 会导致维度不匹配,C、D 都不是标准做法。
Q9. 相比 RNN,自注意力在序列建模上的改进包括哪些?
- A.支持并行并缩短依赖路径← 正确答案
- B.参数数量必然大幅减少
- C.任意位置信息直达无损耗← 正确答案
- D.完全不需要再引入位置信息
答案:AC。RNN 必须按时间步串行,且长距离依赖要经过多步传递;自注意力一步直达所有位置且可并行,这是它取代 RNN 的核心原因。B 不成立,自注意力参数量通常不小;D 错误,自注意力仍需位置编码。
Q10. 序列推荐中“预测下一项”通常使用什么损失函数?
- A.交叉熵损失函数来优化← 正确答案
- B.均方误差损失函数优化
- C.合页损失函数来度量错误
- D.感知损失函数来惩罚噪声
答案:A。下一项预测本质是「从全物品库中选一个正确项」的多分类问题,标准做法是对 softmax 概率取负对数似然,即交叉熵。均方误差更适合回归,合页损失多用于 SVM 类模型,感知损失用于图像生成,都与推荐预测目标不匹配。