2026/10/16 7:13:40

注意力机制原理与实现:从QKV到多头自注意力

注意力机制原理与实现:从QKV到多头自注意力 简介这份PPT系统讲解深度学习中的注意力机制面向有一定RNN和序列到序列基础、希望理解注意力原理与主流变体的学习者解决固定长度上下文向量带来的信息瓶颈问题。内容涵盖Bahdanau注意力、Luong注意力、硬注意力、局部注意力与自注意力并结合机器翻译、文本摘要、图像描述等应用场景说明设计动机与适用条件可作为课堂讲义、组会分享或面试复习的速查材料。压缩包共1个文件为PPT格式大小仅2.7MB便于直接下载使用或二次改版。目前已有1183人学习下载讲解脉络从RNN序列问题切入到编码器解码器局限再到注意力机制引出与变体对比并配有示意图说明注意力权重计算与自注意力在长距离依赖上的优势能帮助读者快速建立从理论到应用的整体框架。1. 注意力机制是什么让模型学会忽略的那张PPT很多人第一次翻开讲注意力机制的课件容易被 Q、K、V 和一堆矩阵公式劝退但这套《深度学习中attention注意力机制.ppt》把顺序安排得很实在先讲清楚注意力到底在解决什么问题再给你看权重是怎么算出来的最后落到 NLP 和 CV 里怎么改结构。注意力机制的本质其实特别朴素——它让模型在处理序列时不再把每个词、每个像素一视同仁而是动态地决定当前这一步该重点关注哪一部分输入。这套资源适合两类人看一类是刚学完循环神经网络准备往 Transformer 过渡的学生另一类是已经在跑模型、但一直把注意力当成黑匣子、想搞懂内部张量形状的从业者。它不挑框架图解为主代码层面的细节需要你自己在复现时补全。2. 从 Encoder-Decoder 说起注意力权重到底在算什么2.1 为什么循环模型需要注意力信息瓶颈与长距离依赖所有讲注意力的课件几乎都会从 Encoder-Decoder 结构切入这套 PPT 也不例外。我当初带 A 同学做序列标注实验时他对为什么有了双向 LSTM 还不够这个问题卡了很久。你想想经典编码器把整个源句子压缩成一个固定长度的语义向量 c解码器每一步都从同一个 c 里取信息。句子短还好说句子一长先输入的词经过多步非线性变换后信息早就被洗得差不多了这就是所谓的信息瓶颈。翻译任务里遇到长句效果断崖式下跌根因就在这。2.2 从全局向量到逐位寻址注意力打分函数的三种常见形式注意力机制的第一步是算相关性分数PPT 里通常给出三种打分函数加性注意力、点积注意力、缩放点积注意力。我在这套 PPT 的框架图上看到它把三种形式并列放在一页并且特别标注了点积形式在 GPU 上效率更高——这是很实在的经验因为加性注意力带一层 tanh 全连接虽然理论上拟合能力强但实际训练时显存占用和计算量都更大。假设编码器某个时刻的输出为 h_j解码器当前隐状态为 s_{i-1}那么相关性分数常见做法是score(s, h) s^T W h双线性也叫 generalscore(s, h) v^T tanh(W1 s W2 h)加性score(s, h) s^T h点积点积形式最简单但也最容易被长度放大——当向量维度大了以后点积结果方差变大softmax 容易进入饱和区梯度变得非常小所以 Transformer 里才在点积结果上除以 sqrt(d_k)。这套 PPT 在讲 softmax 归一化时画了那个经典的权重分布图我建议你看的时候注意一件事softmax 的温度参数直接决定注意力是接近 one-hot还是接近均匀这在后面的蒸馏和可视化里都有用。2.3 用十行代码算出一组注意力权重完整可跑的最小实现PPT 里讲的是数学形式真正理解还是要落到代码。我一般会让学员照着下面这个最小实现敲一遍把中间张量形状打出来比盯着公式看半小时管用得多import numpy as np def attention_score(query, keys, values, temperatureNone): # query: [state_dim]当前解码步的隐状态 # keys: [seq_len, state_dim]编码器所有时刻的输出 # values:[seq_len, state_dim]通常是keys本身 if temperature is None: temperature np.sqrt(query.shape[-1]) # 1) 打分点积得到每个位置的相关性分数 scores np.dot(keys, query) / temperature # [seq_len] # 2) 归一化softmax转成概率分布 weights np.exp(scores - np.max(scores)) weights / weights.sum() # [seq_len]和为1 # 3) 加权求和按权重把values聚合为一个向量 context np.dot(weights, values) # [value_dim] return context, weights # 造一组假数据编码器输出4个时刻隐状态维度是8 keys np.random.randn(4, 8) values keys query np.random.randn(8) _, weights attention_score(query, keys, values) print(注意力权重:, np.round(weights, 4)) print(权重和:, weights.sum())这段代码把注意力拆成三步打分、归一化、加权求和。第 1 步里除以 sqrt(d_k) 是关键我在 2-3 亿参数规模的任务上调过去掉这个缩放后训练前期 loss 经常震荡尤其是序列长度超过 128 时第 2 步用减最大值再取指数的写法是数值稳定技巧防止 exp 溢出第 3 步得到的 context 向量就是解码器当前步需要的上下文摘要。你把它理解成从数据库里按相关性取出内容后面看多头注意力和 KV Cache 的优化思路就顺了。2.4 注意力机制的三大共性动态加权、并行友好、可解释不管 PPT 后面结构怎么变注意力有两个共性贯穿始终。第一是动态加权权重是随输入和当前状态实时算出来的不是固定模板第二是容易并行因为打分和加权都可以写成矩阵乘法这跟循环神经网络必须按时间步展开的串行特性形成鲜明对比。我自己的体会是可解释性这第三条要谨慎看待。很多人在论文里贴注意力热力图但实际部署时注意力权重并不总是与人类直觉的重要词一致。某次我做一个客服工单分类模型发现模型把逗号和句号赋予了很高权重原因根本不是模型关注标点而是标点位置稳定容易作为分类的统计线索。所以你在读这套 PPT 的可解释性章节时可以把热力图当成诊断工具别直接当成因果解释。3. 自注意力与多头机制PPT 里的 Q、K、V 到底怎么用3.1 从 Seq2Seq 到 Self-Attention为什么要跟自己做注意力如果注意力只是在编码器和解码器之间做那它本质上还是两个序列之间的对齐。而 Transformer 里最关键的一步改动是自注意力——序列里的每个元素都跟序列里的所有元素包括自己算相关性。第一眼看上去很反直觉一个句子里的词为什么要互相看但实际操作中自注意力让每一层都能直接建模任意两个位置的依赖关系一个词的解释不再只依赖它左边几个词而是整个上下文。PPT 里通常把 self-attention 的数据流画成三条线QQuery 查询、KKey 键、VValue 值。我最早看这个图也懵后来用检索的思路理解就通了你搜索东西时输入的关键词是 Query后台索引里每个文档的标题是 Key文档正文是 Value搜索引擎先拿 Query 去匹配 Key再用匹配程度把对应的 Value 取出来加权融合。在自注意力里Q、K、V 三个矩阵都由同一个输入 X 乘上三个不同的权重矩阵得到。权重矩阵是模型学出来的所以 Q、K、V 并不一定表示词本身的属性而是模型自己决定用什么角度去匹配和提取。3.2 多头不是多个注意力它是对子空间的分工这套 PPT 讲多头注意力时我特别留意它有没有讲清楚多头到底在干嘛。不少人以为多头就是把注意力算几遍再平均其实没那么简单。多头是把 Q、K、V 的维度切成 h 份每份独立做一次注意力然后把 h 个结果拼起来再经过一个输出投影。这样做的好处是每个头可以在不同子空间里关注不同的关系模式。句子那只猫躺在垫子上它很舒服里的它可能需要一个头关注指代关系另一个头关注语法位置多头就是给模型提供了这种分头协作的能力。我在整理这套 PPT 的配套笔记时把多头注意力的张量形状演变画了一下这是理解的关键import torch import torch.nn as nn class MultiHeadSelfAttention(nn.Module): def __init__(self, embed_dim, num_heads, dropout0.1): super().__init__() assert embed_dim % num_heads 0, embed_dim必须能被num_heads整除 self.embed_dim embed_dim self.num_heads num_heads self.head_dim embed_dim // num_heads # 四个投影矩阵Q、K、V、输出 self.q_proj nn.Linear(embed_dim, embed_dim) self.k_proj nn.Linear(embed_dim, embed_dim) self.v_proj nn.Linear(embed_dim, embed_dim) self.out_proj nn.Linear(embed_dim, embed_dim) self.dropout nn.Dropout(dropout) self.scale self.head_dim ** -0.5 # 缩放系数1/sqrt(d_k) def forward(self, x, maskNone): # x: [batch, seq_len, embed_dim] batch_size, seq_len, _ x.shape # 1) 线性投影后拆成多头形状为[batch, heads, seq_len, head_dim] q self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim) q q.transpose(1, 2) k self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim) k k.transpose(1, 2) v self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim) v v.transpose(1, 2) # 2) 点积算相似度并缩放 scores torch.matmul(q, k.transpose(-2, -1)) * self.scale if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn_weights torch.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) # 3) 加权求和再把多头拼回去 context torch.matmul(attn_weights, v) context context.transpose(1, 2).contiguous() context context.view(batch_size, seq_len, self.embed_dim) return self.out_proj(context), attn_weights这段代码是理解 Transformer 内部结构的核心有四个地方要重点看。第一view 加 transpose 的写法把形状从 [batch, seq_len, embed_dim] 拆成 [batch, num_heads, seq_len, head_dim]拆的时候要保证 embed_dim 能被 num_heads 整除否则会报错我在实验里常用 head_dim64num_heads8embed_dim512 这个组合。第二mask 的操作是把 padding 位置设成负无穷这样 softmax 之后概率就是 0做批训练时不同长度句子必须走这一步否则模型会关注到空位置。第三scale 用的是 head_dim 而不是 embed_dim很多初学的人在这里写错一写错注意力分布就会过于尖锐或过于平坦。第四返回值里的 attn_weights 就是可视化要用的权重矩阵我留这个返回值就是因为后期调试需要看每个头分别在关注什么。3.3 位置编码为什么绕不开自注意力打乱了顺序感自注意力对位置不敏感。把句子里任意两个词换一下位置如果不加位置编码注意力计算出来的结果完全一样因为它是纯集合操作不感知词的先后顺序。这对语言模型是致命的——张三打了李四和李四打了张三完全是两个意思。PPT 里常见的位置编码方案有两种正弦余弦绝对位置编码和可学习位置编码。正弦余弦编码的公式看起来复杂但实际使用时直接查表就行PyTorch 里可以用nn.Embedding(seq_len, embed_dim)来学习位置向量也可以预计算固定的三角编码。我在自己项目里更倾向用可学习位置编码因为实现简单而且在大规模语料上效果并不比固定编码差。唯一要注意的是推理时的长度外推问题——训练时最长见过 512 的位置输入 600 长度的序列时位置编码矩阵越界常见做法是截断或者用动态插值。这套 PPT 如果讲的是 Transformer 基础版一般不会展开长度外推但你在实际用的时候一定会撞上。3.4 掩码注意力解码器里不许看未来自注意力还有一个变体是因果掩码也叫自回归掩码。训练解码器时预测第 t 个词不应该能看到第 t1 个词否则就泄漏了答案。实现方式很简单把注意力分数矩阵的上三角部分全部设成负无穷这样 softmax 之后每个位置只能看到自己和之前的位置。这块 PPT 上通常画一个三角形的白色区域和灰色区域我建议你直接改代码试试把上面那段实现里的 mask 参数换成上三角矩阵跑一遍前向传播观察注意力权重矩阵的形状变化。我在调试某公司内部的一个生成式模型时曾经忘记给解码器加因果掩码结果指标异常好——当时我就警觉了因为测试集指标不可能凭空暴涨查了半天发现是数据泄漏。这个坑很多新手都会踩后面专门讲避坑时还会再提。4. 训练与复现的常见坑注意力机制项目避坑实战4.1 现象加了注意力机制效果反而比普通 LSTM 还差我见过不止一个同学按 PPT 里的公式实现注意力后在文本分类任务上打不过普通 LSTM。原因往往不是注意力没用而是学习率没有重新调。注意力机制里的 softmax 对初始化和学习率非常敏感原来适合 LSTM 的默认学习率在加了注意力后可能导致权重分布过早饱和某个位置的权重直接变成 1其他位置全是 0整个模型退化成只看一个词。解决方法是把学习率降下来同时给 Q、K、V 的投影矩阵设置较小的初始化范围。我当时处理某图像描述生成任务时把学习率从 1e-3 降到 3e-4配合 warmup 策略损失曲线明显变平滑。另外可以试试在注意力权重上加 dropout常见范围是 0.1 到 0.2这能防止模型对某些位置过度依赖。4.2 现象训练时 loss 正常推理时结果一塌糊涂这个坑出在 mask 的使用不一致。训练阶段为了防止 padding 位置参与计算输入给注意力模块的是带 mask 的句子但推理时图省事直接一个句子塞进去忘记构造全 1 的 mask。结果 padding 位置在打分矩阵里被计算了即使权重很小也会轻微污染加权求和的结果。在小数据集上这种污染可能不明显但生成任务里每一步的微小偏差都会被累积放大。解决方法是把 mask 的逻辑封装成独立函数训练和推理统一调用。我在代码里习惯用mask (input_ids ! pad_token_id).unsqueeze(1).unsqueeze(2)这种写法构造出来的 mask 形状跟注意力分数矩阵对齐保证两条路径行为完全一致。4.3 现象梯度检查时发现某些头的权重全是均匀分布训练一个 8 头的 Transformer 时打印每个头的注意力权重发现其中两三个头的分布接近均匀看不出任何关注重点。新手会以为模型训坏了但很多情况下这是正常的冗余现象。有研究指出训练完成后部分头会退化成近乎均匀分布去掉它们对整体指标影响不大这就是所谓的头剪枝。我在某跨平台系统的文本匹配模块里试过剪掉退化头模型大小减小了 10%指标只掉了 0.1%因此看到均匀分布的头时先别着急加正则试着做一次剪枝实验。4.4 现象位置编码数组越界这个问题在前面提过训练长度和推理长度不一致时位置编码矩阵索引超出范围直接报 IndexError。我处理过一个实际案例训练时把句子截断到 128但线上某个长文档有 300 个 token模型上线当天就报错。解决方法是训练时就用支持更长序列的编码方式或者推理时对超长部分做滑窗截断把文档拆成多个 128 token 的片段分别处理再做结果融合。4.5 现象显存占用高得离谱序列长度一翻倍就 OOM注意力的空间复杂度是 O(n^2)序列长度从 128 加到 256分数矩阵的内存占用变成原来的四倍。很多人在 512 长度时能跑一换 1024 长度直接 OOM。解决思路有三个方向一是用 Flash Attention 这类 IO 优化的实现它在计算注意力时不把完整分数矩阵写回显存而是分块计算二是检查是否可以在浅层用 Pooling 或 stride 降低序列长度三是减少注意力头数或 head_dim。这套 PPT 如果没讲这些工程优化你把它当成理论基线就好真正训练大模型时这些省显存的技巧是必须掌握的。5. 把注意力权重打印成图直观验证模型在不在认真看PPT 里的注意力热力图看起来非常直观但自己复现时会发现可视化代码写得对不对、选择哪一层的头来展示都影响最终效果。我常用的方法是把某个测试样本的注意力权重矩阵导出来用 matplotlib 直接画热力图。import matplotlib.pyplot as plt import seaborn as sns def plot_attention(weights, tokens, save_pathattn_map.png): # weights: [num_heads, seq_len, seq_len]来自模型forward返回值 # tokens: 输入文本分词后的token列表 head_idx 0 plt.figure(figsize(8, 8)) # 中文显示需要设置字体这里用sns的heatmap直接画矩阵 sns.heatmap( weights[head_idx].detach().cpu().numpy(), xticklabelstokens, yticklabelstokens, cmapBlues, cbarFalse ) plt.xticks(rotation90) plt.yticks(rotation0) plt.title(fhead {head_idx 1} 的注意力分布) plt.tight_layout() plt.savefig(save_path, dpi150)这段可视化代码有三个使用要点。第一token 列表一定要跟模型的输入 tokenizer 完全对应很多画出来的图对不上坐标轴就是因为可视化时用了原始句子字符串而不是分词后的 token 列表。第二选择展示哪一个头值得斟酌我一般先打印所有头的平均熵选熵值最小的头来画因为熵小说明注意力集中可视化效果和信息量都更好。如果想要采样展示可以同时把 8 个头的热力图排成一排输出。第三热力图的颜色深浅只代表权重数值大小不代表因果重要性这一点要在向别人解释时讲清楚不然容易误导。我做过一次很有意思的诊断把一段包含它字的指代消解样本画出来发现第二层的某个头在它这个位置上对句中名词的注意力特别集中而后面靠近输出层的头反而分布更稀疏。这说明底层头倾向于做词法匹配高层头处理更抽象的语义关系。从那以后我每次调试注意力模型时都会强制走一遍先画热力图、再算头熵、最后做剪枝实验的流程很多看起来玄学的训练问题最后都能在注意力分布里找到线索。希望这个流程能帮你在用这套 PPT 做复现时少走弯路。本文还有配套的精品资源点击获取