transformer系列:#3 深度解析多头注意力 📅 2026/7/22 11:22:45 编码器中的自注意力输入序列关注自己解码器中的自注意力目标序列关注自己解码器中的编码器-解码器注意力目标序列关注输入序列1 Transformer 中如何使用注意力注意力输入的参数Query、Key 和 Value注意力层接收三个参数形式的输入分别叫做 Query查询、Key键 和 Value值。这三个参数的结构很相似序列里的每个词都由一个向量来表示。编码器自注意力输入序列首先被送入输入嵌入和位置编码该模块为输入序列中的每个词生成一个编码表示该表示捕捉了每个词的含义和位置。然后该编码表示被送入第一个编码器中的自注意力机制该机制接收查询Query、键Key和值Value三个参数。自注意力机制随后也为输入序列中的每个词生成一个编码表示但这次编码表示中加入了每个词的注意力分数。随着编码结果依次经过堆栈中的所有编码器每个自注意力模块都会将自身的注意力分数添加到每个词的编码表示中。2 编码器自注意力解码器自注意力在解码器栈中目标序列首先被送入输出嵌入和位置编码该过程会为目标序列中的每个词生成一个编码表示该表示捕捉了每个词的含义和位置。然后该编码表示被送入第一个解码器中的自注意力机制该机制会接收查询Query、键Key和值Value这三个参数。自注意力机制随后也会为目标序列中的每个词生成一个编码表示该表示还包含了每个词的注意力得分。经过层归一化之后这个表示被传给第一个解码器中「编码器-解码器注意力」的 Query 参数。编码器-解码器注意力与此同时编码器堆栈最后一个编码器的输出被传给编码器-解码器注意力的 Value 和 Key 参数。这样一来编码器-解码器注意力同时拿到了两样东西一个是目标序列的表示来自解码器自注意力另一个是输入序列的表示来自编码器堆栈。所以它生成的表示里既包含了目标序列每个词的注意力分数也融入了输入序列注意力分数带来的影响。当这个表示流过解码器堆栈里的所有解码器时每一个自注意力层和每一个编码器-解码器注意力层也都会把自己的注意力分数继续加进每个词的表示里。多个注意力头在 Transformer 里注意力模块会把自己的计算并行地重复多次。每一次重复就叫做一个「注意力头」。注意力模块会把它的 Query、Key 和 Value 参数按 N 份切开然后把每一份独立地传给一个单独的头。所有这些相似的注意力计算结果最后会被组合在一起生成一个最终的注意力分数。这就是「多头注意力」的由来——它给了 Transformer 更强大的能力去为每个词编码多重关系和细微差别。3 多个注意力头为了准确理解数据在内部是怎么处理的我们还是以训练一个翻译任务为例一步步走过注意力模块的工作流程。我们只用一个训练样本输入序列是英文 “You are welcome”目标序列是西班牙语 “De nada”。注意力的超参数有三个超参数决定了数据的维度嵌入尺寸嵌入向量的宽度我们的例子里用了宽度 6。这个维度会贯穿整个 Transformer 模型所以有时候也会被叫成别的名字比如模型尺寸等等。查询尺寸等于 Key 和 Value 的尺寸三个线性层用来生成 Query、Key 和 Value 矩阵时各自使用的权重的尺寸我们的例子里查询尺寸用了 3。注意力头的数量我们的例子里用了 2 个头。除此之外还有个批量大小它给了我们一个表示样本数量的维度。输入层输入嵌入和位置编码层产出一个形状为 (样本数, 序列长度, 嵌入尺寸) 的矩阵。这个矩阵被喂给堆栈里第一个编码器的 Query、Key 和 Value。4 输入层为了让图示更简单我们会丢掉批量这个维度只关注剩下的维度。4-1 输入层线性层Query、Key 和 Value 各自有一个独立的线性层每个层有自己的权重。输入数据穿过这些线性层就产生了 Q、K 和 V 矩阵。5 线性层把数据拆分到各个注意力头现在数据要被拆分到多个注意力头上这样每个头才能独立处理。但有个关键点需要理解这仅仅是逻辑上的拆分。Query、Key 和 Value 并不是真的被切成了一个个独立的物理矩阵——每个头一个。相反Query、Key 和 Value 各自仍然是一个单独的数据矩阵只不过矩阵里为每个头划出了逻辑上独立的分区。同样也不是每个头都有自己独立的线性层。实际上所有注意力头共享同一个线性层只不过它们在自己的那一块逻辑数据分区上操作。线性层的权重也按头做了逻辑分区这种逻辑拆分是通过把输入数据和线性层的权重都均匀地划分到各个注意力头上实现的。我们可以通过设定下面的查询尺寸来达到这一点查询尺寸 嵌入尺寸 / 头的数量6 线性层的权重也按头做了逻辑分区在我们的例子里查询尺寸 6 / 2 3。尽管线性层的权重和输入数据是一个单独的矩阵但我们可以把它想象成每个头的独立层权重被堆叠在了一起。6.1 线性层的权重也按头做了逻辑分区因此所有头的计算可以通过一次矩阵运算完成而不需要 N 次独立的运算。这让计算更高效也让模型保持简洁因为需要的线性层更少了但同时仍然保留了独立注意力头的强大能力。重塑 Q、K 和 V 矩阵线性层输出的 Q、K 和 V 矩阵会被重塑以显式地包含一个「头」的维度。现在每个切片就对应每个头的矩阵。这个矩阵会被再次重塑——这次是交换「头」和「序列」这两个维度。尽管图示里没有画出批量维度但 Q 现在的维度变成了 (批量, 头, 序列, 查询尺寸)。7 重塑 Q、K 和 V 矩阵下面的图示展示了我们的示例 Q 矩阵从线性层出来后被拆分的完整过程。最后一个步骤只是为了可视化——虽然 Q 矩阵仍然是一个单独的矩阵但我们可以把它想象成每个头有一个逻辑上独立的 Q 矩阵。7.1 重塑 Q、K 和 V 矩阵现在我们准备好计算注意力分数了。为每个头计算注意力分数现在我们有了 Q、K 和 V 这三个矩阵并且它们已经被拆分到了各个头上。下面就用它们来计算注意力分数。我们会只使用最后两个维度序列和查询尺寸来展示单个头的计算同时跳过前两个维度批量和头。本质上我们可以想象这些计算会为每个头、批量里的每个样本重复进行——尽管很明显它们实际上是作为一次矩阵运算发生的而不是一个循环。第一步是做一个 Q 和 K 之间的矩阵乘法。8 计算每个头部的注意力得分然后一个掩码值会被加到结果上。在编码器自注意力里这个掩码用来屏蔽掉填充值让它们不参与注意力分数的计算。在解码器自注意力和解码器的编码器-解码器注意力里会用到不同的掩码我们稍后在流程中会讲到。8.1接下来结果被缩放除以查询尺寸的平方根。然后对它应用一个 Softmax。8.2最后再做一次矩阵乘法这次是 Softmax 的输出 与 V 矩阵相乘。8.3编码器自注意力里完整的注意力分数计算如下所示8.4把每个头的注意力分数合并起来现在我们为每个头都得到了独立的注意力分数需要把它们合并成一个单一的分数。这个合并操作基本上就是拆分操作的逆过程。它的做法很简单重塑结果矩阵去掉头这个维度。具体步骤是通过交换头和序列维度来重塑注意力分数矩阵。换句话说矩阵形状从 (批量, 头, 序列, 查询尺寸) 变成 (批量, 序列, 头, 查询尺寸)。再通过重塑成 (批量, 序列, 头 * 查询尺寸)来坍缩掉「头」维度。这实际上就是把每个头的注意力分数向量拼接成了一个合并后的注意力分数。因为 嵌入尺寸 头数量 * 查询尺寸所以合并后的分数维度是 (批量, 序列, 嵌入尺寸)。下面的图示展示了示例分数矩阵合并的完整过程。