自注意力机制核心原理与PyTorch实现:从QKV到缩放点积公式
把“自注意力”这件事讲透比背下Attention(Q,K,V)softmax(QK^T/√d_k)V这个公式重要得多。很多初学者在学 Transformer 时第一关就卡在这里公式能看懂代码一行行能读但真让自己从零实现一个自注意力模块却不知道矩阵该怎么乘、维度为什么对不上、QKV 到底从哪来。我见过不少同学在跑通 BERT、GPT 之后回头问他“自注意力的参数在哪里”他只能说出 softmax 和矩阵乘法却答不上来W_q、W_k、W_v这三个权重矩阵才是真正被梯度更新的对象。这其实就是基础没打牢。这篇文章不打算把 Transformer 全部展开而是聚焦在 7.1.1 这个最基础、也最关键的节点自注意力机制。我会从“为什么需要注意力”讲起再拆解 QKV 的含义和缩放点积公式的每个细节最后用 PyTorch 一步步实现一个可运行的自注意力模块并给出常见误区和工程建议。读完这篇文章你应该能达成三个目标能用通俗语言解释自注意力到底在做什么Q、K、V 分别代表什么。能独立写出一个正确的、带 batch 维度的自注意力 PyTorch 模块。知道为什么公式里要除以√d_k以及自注意力有哪些必须注意的坑。1. 这篇文章真正要解决的问题先给一个明确判断自注意力是 Transformer 架构里最核心、也最容易被误解的组件。它真正革命性的地方不在于它能“看到全局信息”——RNN 把序列全部读一遍之后理论上也能拿到全局信息。自注意力真正的突破是它把序列建模从“顺序依赖”变成了“并行依赖”。在 RNN 里第t个时间步的输出必须等前t-1个时间步计算完才能得到这是顺序依赖。序列一长训练速度上不去而且距离远的两个词之间的信息传递要经过很多步容易丢失。自注意力不一样它一次性把整个序列的所有词袋到一起计算任意两个位置之间的依赖只需要一步矩阵运算就能建立。这一步变化让 GPU 的大规模并行计算能力终于被充分用起来了。但这也带来一个很现实的痛点自注意力的公式太简洁简洁到让人误以为自己懂了。很多教程直接扔出公式然后说“Q 是查询K 是被查的键V 是值”听起来很形象但落到代码里很多人会问x是(batch, seq_len, d_model)QKV 应该是什么维度为什么Q和K要相乘而不是Q和V相乘为什么要除以√d_k不除行不行自注意力模块里有可学习参数吗参数在哪儿这篇文章要解决的就是这些问题。我会先从序列建模的痛点切入解释注意力的来龙去脉再把自注意力的每个细节拆开最后用代码验证。2. 从 RNN 到注意力机制序列建模的痛点要理解自注意力先要知道它解决的是什么问题。深度学习处理序列数据文本、语音、时间序列最早的主力是 RNN包括 LSTM 和 GRU。RNN 的思路是“逐步读取”每个时刻维护一个隐藏状态h_t这个状态包含了从序列开始到当前时刻的信息然后传给下一个时刻。这种设计有两个绕不开的问题。第一个问题是顺序计算导致训练慢。h_t依赖h_{t-1}h_{t-1}依赖h_{t-2}整个序列必须从头到尾依次计算GPU 的并行能力派不上用场。序列长度是 1000就要串行算 1000 步。第二个问题是长距离依赖很难建模。假设一个句子是“小明从北京出发坐了三小时高铁又转了两次地铁终于到了___”。模型要预测的空缺是“上海”还是“杭州”需要重点参考“从北京出发”这个信息。但在 RNN 里这个信息经过很多时间步的传递会不断被“稀释”梯度也容易消失或爆炸。LSTM 用门控机制缓解了这个问题但没有从根上解决。注意力机制最早就是为解决这个问题提出的。2014 年 Bahdanau 等人把它用在机器翻译里解码器在生成每个词的时候不再只依赖最后一个隐藏状态而是回头看编码器的所有隐藏状态给它们分配不同的权重。翻译某个词时模型会自动“注意”源句中相关的词。这是加性注意力。2015 年 Luong 等人提出了乘法注意力dot-product attention计算更高效。再到 2017 年Google 提出 Transformer论文标题直接点明主旨“Attention Is All You Need”。Transformer 中的一个关键变化是注意力不再只用于连接编码器和解码器而是让序列自己对自己做注意力这就是自注意力Self-Attention。自注意力和 seq2seq 里注意力的区别在哪在机器翻译场景里Q 来自解码器当前要翻译的词K 和 V 来自编码器源句的所有词。自注意力则是Q、K、V 全部来自同一个输入序列。也就是说序列里的每个元素都要和序列里的所有元素包括自己计算相关性然后根据相关性加权聚合信息。下面这张对比表可以帮助理解对比维度RNN自注意力计算方式逐步顺序计算所有位置并行计算长距离依赖需要多步传递易丢失一步建立任意位置依赖计算复杂度序列长度 nO(n) 步每步固定开销O(n²) 总开销但可并行是否依赖顺序天然有顺序概念本身无序需要位置编码建模能力依赖隐藏状态压缩信息直接聚合全局信息从这个表能看出自注意力不是没有代价的它的计算复杂度是 O(n²)。这也是后来很多优化工作如 Linformer、Longformer努力解决的问题。但从建模能力角度看自注意力让每个位置都能直接访问全序列信息这种“全局视野”是 RNN 很难做到的。3. 自注意力核心原理Query、Key、Value 与缩放点积公式自注意力机制的核心可以概括成一句话根据查询Query和键Key的相似度对值Value做加权求和。在解释 QKV 之前先看一个贴近生活的类比图书馆检索系统。假设你是一位研究者想找关于“深度学习模型压缩”的文献。你手里的检索需求就是 Query。图书馆里的每本书都有两个属性一个是它的标签Key比如“计算机视觉”“模型压缩”“传统机器学习”另一个是它的内容Value也就是书本身承载的知识。图书管理员拿到你的 Query 后会把它和每本书的 Key 做相似度匹配相似度高的书内容要重点参考相似度低的书内容可以忽略。最后把参考到的内容按相似度加权汇总形成一份你需要的资料综述。在这个过程中Query查询你想找什么。在自注意力里它是当前词发出的“检索意图”。Key键每个词身上贴的标签用来和 Query 匹配。Value值每个词真正携带的信息。匹配到之后要提取的是 Value。现在回到自注意力的计算过程。假设输入序列是X (x_1, x_2, ..., x_n)每个x_i是一个d_model维的向量。比如一个句子有 4 个词每个词用 6 维向量表示X的形状就是(4, 6)。自注意力的第一步是把同一份X通过三个不同的可学习权重矩阵变换成三份不同的向量Q X W_q # Query 矩阵 K X W_k # Key 矩阵 V X W_v # Value 矩阵这里W_q、W_k、W_v是形状为(d_model, d_k)或(d_model, d_v)的可学习参数矩阵。注意这三个权重矩阵才是自注意力模块里真正要被梯度更新的参数。为什么强调这个因为很多初学者误以为自注意力没有参数只是几个矩阵乘法加 softmax。实际上如果 QKV 不加变换、直接用X本身模型就没有任何可学习的能力只能做固定的加权平均那表达能力会非常有限。加入线性变换之后模型才能在训练中学会“从什么角度”去匹配词与词之间的相关性。第二步计算注意力分数。用Q和K的转置做点积scores Q K^TQ的形状是(n, d_k)K^T的形状是(d_k, n)相乘后得到(n, n)的矩阵。其中第i行第j列的值表示第i个词的 Query 和第j个词的 Key 之间的相似度。相似度越高说明第i个词越应该关注第j个词。第三步缩放。把scores除以√d_kscores scores / sqrt(d_k)第四步对每一行做 softmax让注意力权重变成概率分布每一行之和为 1attn_weights softmax(scores, dim-1)第五步用注意力权重对V加权求和得到输出output attn_weights V综合起来就是论文里的公式Attention(Q, K, V) softmax(QK^T / √d_k) V到这里自注意力的主流程就清楚了。但你可能有两个疑问为什么要除以√d_k以及Q、K、V 全来自同一个 X那它们不是一样吗这两个问题下面分别展开。4. 为什么一定要除以 √d_k数值稳定与梯度问题公式里的√d_k是最容易被忽略但非常重要的一个细节。很多初学者会想不就是一个缩放因子吗除以 10 除以 100 有什么区别倒不如说为什么偏偏是√d_k而不是d_k或者1这要从点积的数值特性说起。假设q和k都是d_k维的向量且每个分量独立地从标准正态分布均值 0、方差 1中采样。那么q和k的点积q·k Σ q_i * k_i其均值是 0方差是d_k。换句话说向量维度越高点积的方差就越大点积的值也就越分散到较大的绝对值上。为什么这会造成问题因为在 softmax 里如果输入的值很大softmax 的输出会非常接近 one-hot 分布——最大的那个值对应的概率趋近于 1其它值趋近于 0。梯度方面softmax 在输入极大或极小时梯度会变得非常小模型几乎学不动。举个例子。假设d_k 100某些 q 和 k 的点积可能达到 20 甚至 30。再经过 softmax注意力权重就变成几乎“非 0 即 1”的硬分配。你希望模型能学到“这个位置稍微关注一点那个位置稍微关注一点”这样的平滑权重但数值上 softmax 已经被推到饱和区梯度消失。解决办法就是把点积除以√d_k。因为点积的方差是d_k除以√d_k之后方差就变回 1让点积的数值规模保持稳定不再随着维度增长而膨胀。这样 softmax 的输入不会过大梯度能保持平稳训练也更稳定。可以做一个简单验证。当d_k 4时不缩放和缩放的对比可能不明显但当你把d_k加到 64、128不缩放的注意力矩阵往往会出现某些行 softmax 之后几乎只有 1 个位置是 1其余全是 0这就是“注意力坍塌”。这种注意力权重非常极端不利于模型学习丰富的依赖关系。那为什么不除以d_k呢除以d_k会把方差压到1/d_k数值太小softmax 的输出会过于平滑接近均匀分布注意力就失去选择性了。所以√d_k是刚好把方差标准化到 1 的选择。这也是为什么论文里特别命名为“缩放点积注意力”Scaled Dot-Product Attention。“缩放”这两个字指的就是除√d_k。5. 自注意力完整代码实现从最小示例到批量模块理论讲完接下来进入实操。我从一个最小实现开始逐步写出可以放进项目里的自注意力模块。这里用 PyTorch假设你对基本 Tensor 操作有概念。版本以实际环境为准核心 API 在 PyTorch 1.x 和 2.x 中都兼容。5.1 最小自注意力实现理解矩阵维度先写一个不带 batch 维度、不做任何封装的最小示例目标是看清楚每一步的矩阵形状变化。import torch import torch.nn.functional as F # 构造输入4 个 token每个 token 用 6 维向量表示 # 对应一个长度为 4 的句子d_model 6 x torch.randn(4, 6) # 设置 QKV 的投影维度 d_k 3 d_v 3 # 可学习的权重矩阵 # x 是 (4, 6)W_q 是 (6, 3)q 就是 (4, 3) W_q torch.randn(6, d_k, requires_gradTrue) W_k torch.randn(6, d_k, requires_gradTrue) W_v torch.randn(6, d_v, requires_gradTrue) # 线性投影 q x W_q # (4, 3) k x W_k # (4, 3) v x W_v # (4, 3) # 计算注意力分数q k^T结果 (4, 4) scores q k.T # 缩放 scores scores / (d_k ** 0.5) # softmax得到注意力权重每行和为 1 attn_weights F.softmax(scores, dim-1) # 加权聚合 Value output attn_weights v # (4, 3) print(Q shape:, q.shape) print(K shape:, k.shape) print(V shape:, v.shape) print(scores shape:, scores.shape) print(attn_weights shape:, attn_weights.shape) print(output shape:, output.shape)运行这段代码输出应该类似Q shape: torch.Size([4, 3]) K shape: torch.Size([4, 3]) V shape: torch.Size([4, 3]) scores shape: torch.Size([4, 4]) attn_weights shape: torch.Size([4, 4]) output shape: torch.Size([4, 3])关键点scores是(4, 4)第i行第j列表示第i个 token 对第j个 token 的注意力分数。attn_weights的每一行经过 softmax 后和为 1。output的每个位置都是V的所有行按对应权重加权求和的结果所以每个位置的输出都融合了整个序列的信息。5.2 带 batch 维度的自注意力模块实际项目中输入通常是(batch_size, seq_len, d_model)的三维张量而且很多时候需要传入 mask比如 padding mask 或因果 mask。下面写一个可直接复用的SelfAttention模块。import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, d_model, d_k, d_v): Args: d_model: 输入向量的维度 d_k: Query 和 Key 投影后的维度 d_v: Value 投影后的维度 super().__init__() self.d_model d_model self.d_k d_k self.d_v d_v # 三个线性变换不带 bias 是常见做法 self.w_q nn.Linear(d_model, d_k, biasFalse) self.w_k nn.Linear(d_model, d_k, biasFalse) self.w_v nn.Linear(d_model, d_v, biasFalse) def forward(self, x, maskNone): Args: x: (batch_size, seq_len, d_model) mask: (batch_size, seq_len, seq_len) 或 (seq_len, seq_len) 需要屏蔽的位置为 0 或 True Returns: output: (batch_size, seq_len, d_v) attn_weights: (batch_size, seq_len, seq_len) batch_size, seq_len, _ x.shape q self.w_q(x) # (batch, seq_len, d_k) k self.w_k(x) # (batch, seq_len, d_k) v self.w_v(x) # (batch, seq_len, d_v) # scores: (batch, seq_len, seq_len) scores torch.matmul(q, k.transpose(-2, -1)) / (self.d_k ** 0.5) if mask is not None: # 将 mask 中为 0 的位置填充为负无穷softmax 后权重为 0 scores scores.masked_fill(mask 0, float(-inf)) attn_weights F.softmax(scores, dim-1) output torch.matmul(attn_weights, v) # (batch, seq_len, d_v) return output, attn_weights这段代码相比最小实现做了三件事用nn.Linear替代手动矩阵乘法参数会由 PyTorch 自动管理。加入 batch 维度torch.matmul会自动处理 batch 内的矩阵乘法。支持 mask这是 Transformer 里非常常用的功能。测试一下batch_size 2 seq_len 4 d_model 8 d_k 4 d_v 4 model SelfAttention(d_model, d_k, d_v) x torch.randn(batch_size, seq_len, d_model) output, attn_weights model(x) print(output shape:, output.shape) print(attn_weights shape:, attn_weights.shape) print(attn_weights row sum:, attn_weights.sum(dim-1))预期输出output shape: torch.Size([2, 4, 4]) attn_weights shape: torch.Size([2, 4, 4]) attn_weights row sum: tensor([[1.0000, 1.0000, 1.0000, 1.0000], [1.0000, 1.0000, 1.0000, 1.0000]])注意attn_weights每一行之和为 1说明 softmax 正常工作。如果 mask 引入了负无穷被 mask 的位置权重也会是 0但有效位置的行和仍然是 1。5.3 因果掩码Causal Mask示例在 GPT 这类自回归模型里预测第t个词时不能看到第t个词之后的词否则就是“作弊”。做法是构造一个上三角 mask把未来位置的注意力分数设为负无穷。import torch seq_len 4 # 构造因果 mask下三角为 1上三角为 0 causal_mask torch.tril(torch.ones(seq_len, seq_len)).bool() print(causal_mask:\n, causal_mask) # 模拟未缩放的注意力分数 scores torch.randn(seq_len, seq_len) * 0.5 masked_scores scores.masked_fill(causal_mask 0, float(-inf)) attn_weights torch.softmax(masked_scores, dim-1) print(masked attn_weights:\n, attn_weights)这段代码的意义在于在训练阶段就可以用 mask 模拟“每个位置只能看到自己和之前位置”的自回归属性从而让整个序列并行计算。这也说明自注意力本身是无序的mask 和位置编码是它表达顺序信息的关键手段。5.4 与 PyTorch 官方 MultiheadAttention 对比PyTorch 提供了nn.MultiheadAttention内部就是多头注意力的实现。了解它能帮你验证自己的理解是否和官方实现一致。import torch import torch.nn as nn batch_size 2 seq_len 4 d_model 8 num_heads 2 mha nn.MultiheadAttention(embed_dimd_model, num_headsnum_heads, batch_firstTrue) x torch.randn(batch_size, seq_len, d_model) attn_output, attn_weights mha(x, x, x) print(attn_output shape:, attn_output.shape) print(attn_weights shape:, attn_weights.shape)batch_firstTrue表示输入输出形状为(batch, seq_len, embed_dim)。attn_weights的形状在官方实现里是(batch, num_heads, seq_len, seq_len)因为官方返回的是每个 head 的注意力权重。对照nn.MultiheadAttention能帮你确认自注意力模块里的基本计算流程就是这个公式多头只是把同样的流程复制到多个子空间并行执行。从单头自注意力到多头区别不在“机制”而在“并行和拼接”。多头机制会在第 8 节进一步说明。6. 运行结果与效果验证代码写完之后怎么确认自己实现的是对的除了看输出形状还有几个简单的验证手段。6.1 验证注意力矩阵的行和为 1这是最直观的验证。softmax 的性质决定了每行之和为 1。如果某一行不是 1说明代码里有 bug比如 softmax 的dim写错了或者 mask 处理出了问题。assert torch.allclose(attn_weights.sum(dim-1), torch.ones_like(attn_weights.sum(dim-1))), attention rows must sum to 16.2 验证 mask 是否生效传入 mask 后被 mask 位置的注意力权重应该为 0。mask torch.ones(2, 4, 4) mask[:, 0, 1:] 0 # 第 0 个 token 不能看其他 token model SelfAttention(d_model8, d_k4, d_v4) x torch.randn(2, 4, 8) output, attn_weights model(x, maskmask) # 检查第 0 个样本第 0 行的注意力权重除了第 0 列其余应为 0 print(attn_weights[0, 0])如果 mask 生效输出应该是类似[1.0, 0.0, 0.0, 0.0]的结果。注意这里mask 0位置被填充为负无穷softmax 后权重为 0。6.3 验证输出是否为 V 的加权平均自注意力的最后一步是output attn_weights v。你可以手动把v的第一行拿出来乘以attn_weights[0, 0]的对应权重再求和看是否和output[0, 0]相等。manual_output attn_weights[0, 0] v[0] # 这里的 v 是 forward 内部计算出来的需要先拿到 v不过在实际模块里v是内部变量。一个更简单的做法是固定住权重用同一输入跑两次确认输出一致排除随机初始化带来的干扰。这虽然不是一个严谨的数学验证但能帮你确认模型的可重复性。6.4 运行失败的排查顺序如果代码报错按下面的顺序排查先看维度不匹配的报错信息。自注意力最常见的错误是matmul维度对不上比如q和k.T没有正确转置。检查是否忘了除以√d_k。这个不影响维度但会影响训练稳定性和注意力分布。检查 softmax 的dim。如果你的 scores 形状是(batch, seq_len, seq_len)softmax 必须在最后一维也就是dim-1在 seq_len 方向上归一化而不是在 batch 方向上。检查 mask 的数据类型。masked_fill要求 mask 是布尔张量或者可以比较的位置。如果你传的是浮点 0/1 张量mask 0也能用但如果 mask 里有-inf之类的值要注意判断逻辑。如果训练时 loss 不下降或梯度为 NaN优先检查数值稳定性包括是否用了过大学习率、QK 点积是否未缩放、是否有 mask 把整行都填成负无穷。7. 自注意力常见误区与排查思路结合我看到的初学者提问以下几个误区出现频率最高。误区一认为 Q、K、V 就是输入 X 本身。实际上Q、K、V 是通过三个不同的线性变换得到的。如果不做变换、直接用 X 作为 QKV模型没有任何可学习参数表达力会大打折扣。Transformer 之所以强大很重要的一点就是它用可学习的投影矩阵让模型自行决定“从什么角度”去看序列。误区二认为自注意力完全不包含顺序信息。自注意力对输入序列是“置换等变的”——你把序列的顺序打乱计算过程仍然能跑只是结果也跟着乱。为了让模型理解词序Transformer 会加上位置编码Positional Encoding。这也是为什么不能把自注意力直接用到文本上必须配合位置信息。看到很多初学者问“为什么 Transformer 没有 RNN 也能理解顺序”答案就在这里。误区三忽略缩放因子。很多人实现自注意力时把√d_k省掉了发现训练不稳定或者注意力分布特别尖锐。这就是第 4 节讲的问题。缩放因子不是可有可无的工程细节而是保证数值稳定和梯度健康的关键设计。误区四scores 和 attn_weights 分不清。scores 是 softmax 之前的原始相似度分数可以很大也可以为负attn_weights 是 softmax 之后的行归一化权重每一项在 0 到 1 之间且行和为 1。调试时如果直接打印 scores看到负数或者大于 1 的数不用惊讶那是正常现象。下面这个表格整理了几个典型问题。问题现象可能原因排查方式解决方案输出形状不对QKV 投影维度设置错误打印每一步 tensor shape检查d_model、d_k、d_v是否匹配注意力矩阵每行和不为 1softmax 的 dim 用错了打印attn_weights.sum(dim-1)将 softmax 的dim改为-1训练时注意力分布太尖锐忘记除以√d_k打印缩放前后的 scores 数值范围在QK^T后除以√d_kmask 后注意力权重不为 0mask 条件写反查看 mask 的值和masked_fill逻辑用mask 0定位需要屏蔽的位置有 mask 时全行都是 -inf某一行所有位置都被屏蔽检查该行 mask至少保留一个有效位置否则 softmax 无效模型训练不收敛学习率过大或未做缩放查看 loss 曲线和梯度降低学习率检查数值稳定性特别提醒如果你在实现 Transformer 时遇到“为什么训练一段时间后 loss 变成 NaN”一个常见原因就是scores在未缩放的情况下数值过大softmax 之后出现梯度爆炸。先检查缩放因子再检查 mask。8. 从自注意力到 Transformer工程实践与进阶建议理解了单头自注意力之后下一步就是明白它如何组成完整的 Transformer。8.1 多头注意力为什么要“多头”多头注意力就是把自注意力复制到num_heads个不同的子空间里并行执行然后把结果拼接起来。具体来说d_model的输入会先被投影到num_heads组 QKV。每组 QKV 的维度是d_k d_model / num_heads。每个 head 独立计算注意力得到(seq_len, d_k)的输出最后把所有 head 的输出拼接成(seq_len, d_model)再经过一个输出投影。为什么要这样做一个 head 只能学一种“关注模式”多个 head 可以学到多种不同的关注模式。比如在机器翻译里一个 head 可能关注语法依赖另一个 head 可能关注语义相关词。这就是多头注意力的核心价值让模型在不同表示子空间里捕获更丰富的依赖关系。多头注意力的参数和计算量其实和单头差不多因为每个 head 的维度都缩小了总参数量基本持平。但表达能力更强也更稳定。8.2 工程实现上的三个建议第一个建议合理使用 mask。Transformer 里通常有两类 mask。padding mask 用于屏蔽补齐位置避免无效的 pad token 参与注意力计算causal mask 用于自回归生成保证当前位置只能看到历史信息。两者可以叠加使用做法都是把需要屏蔽的位置填充为负无穷。第二个建议关注计算复杂度。自注意力的复杂度是 O(n²)序列一长显存和耗时增长非常快。如果你处理的是长文本可以考虑稀疏注意力、分块注意力或近似注意力等方案。短文本场景则不必过度优化。第三个建议数值稳定性优先。在生产环境中混合精度训练fp16很常见。在 fp16 下softmax对数值范围更敏感scores未缩放时容易溢出。要确保先除以√d_k再进行 softmax。另外masked_fill时填充float(-inf)在 fp16 下也有溢出风险可以改成用一个很小的负数如-1e9代替这在某些实现中更稳妥。8.3 学习路线上的建议如果你想完整掌握 Transformer自注意力只是第一块拼图。接下来应该按这个顺序学习位置编码理解 Transformer 如何注入顺序信息。残差连接与层归一化LayerNorm理解为什么 Transformer 可以堆很多层。前馈神经网络FFN每个 token 经过注意力后还会过一个两层的 MLP。完整的 Encoder 和 Decoder 结构把上述组件组装起来。训练技巧学习率预热warmup、Adam、梯度裁剪等。推荐用 PyTorch 从零搭建一个迷你 Transformer输入输出都用随机张量跑通再替换成真实文本数据做一个小任务比如字符级语言模型。这种方式比直接使用nn.Transformer更容易加深理解。9. 总结与后续学习方向自注意力是 Transformer 的基石。这篇文章的核心结论可以归纳为几点自注意力的本质是“通过 Query 和 Key 的相似度对 Value 做加权求和”其公式是Attention(Q,K,V)softmax(QK^T/√d_k)V。Q、K、V 来自输入序列的三个不同线性变换W_q、W_k、W_v是模块里真正可学习的参数。√d_k不是没用的缩放因子它把点积的方差拉回 1避免 softmax 进入饱和区保证梯度稳定。自注意力天然无序必须配合位置编码或 mask 来利用序列顺序信息。多头注意力通过多个子空间并行让模型能捕获更多样的依赖关系。如果你正在学习 Transformer建议先亲手跑一遍上面的代码把输入从(4, 6)改成(8, 32)观察维度和注意力分布的变化。然后尝试给SelfAttention加上多头逻辑实现一个最简单的MultiHeadSelfAttention再对照 PyTorch 官方nn.MultiheadAttention验证结果。自注意力这个概念听起来抽象但拆开之后就是“线性变换 矩阵乘法 softmax 加权求和”。把它写熟、跑通、讲清楚Transformer 后面的一切都会顺畅很多。建议把代码保存下来后面学多头注意力、位置编码和完整 Transformer 实现时可以直接复用。如果这篇文章对你有帮助欢迎收藏备用后面我会继续更新 Transformer 系列的其他基础模块。