更新于 2026年7月12日

第9节 Decoder 不需要 Padding Mask#

前两天,有一个读者留言说 Transformer 中 Decoder 里的 tgt key padding mask 操作其实是不需要的。在看了以后回复到是需要的,你再好好想一想如果还是没想明白明天再来找我(主要是想让他自己思考一下,有利于自己理解),我告诉你。

第二天,这位读者如约而至,给我发了他画的图示等等。我又想了一下,觉得依旧是必要的。看在对方如此认真地询问这个问题,就打算写篇文章顺便录个视频讲解一下这里的门道。

结果这下到好,当我开始写作这篇文章时,顺着计算逻辑一步一步发现,他说的好像是对的。于是,又开始从头回顾整个 Transformer 的前向传播和损失计算过着,通过图示来进行梳理,同时也通过代码来进行了验证。

9.1 Transformer 计算过程回顾#

首先,我们先来通过一张图回顾一下 Transformer 中3个主要多头注意力部分的计算过程。

图9-1 Transforer 训练时计算过程图
图9-1 Transforer 训练时计算过程图

如图9-1所示便是整个 Transformer 在训练时的计算过程图,从左到右依次是 Encoder 中的多头注意力计算过程、Decoder 中的掩码多头注意力计算过程和 Decoder 中与 Encoder 交互的交叉多头注意力计算过程。

整个过程大家对着图示自行温习,这里就不在赘述了。

我们都知道,整个 Transformer 结构中涉及到的 “掩码” 类型一共有两种:① 用于区分同一个 batch 中不同长度序列是否被填充的 key padding mask;② 在训练时,Decoder 中用于模仿推理过程中在编码当前时禁止看到未来信息的 attention mask(也叫做 casual mask 或 future mask)。

这里需要提醒一点的是,不同的代码实现或框架对这两种“掩码”的称呼不一样,有的地方把填充掩码也成为 attention mask,而把我们这里指代的attention mask 称为 casual mask,大家在阅读相关材料时需要根据上下文理解。

对于 key padding mask ,它在 Transformer 中有3个地方被用到,也就是图9-1中横着看的第二大块;而 attention mask 则只会被用在解码器中的 Masked-multi Head Attention 中。

9.2 padding mask原理解释#

那现在问题来,Decoder 中的 tgt key padding mask 是否真是可以不用呢?真的可以不用。

这个结论听起来好像真的有点难以相信,毕竟好像大家都用了。

首先,对于自回归模型来说,我们在训练阶段为了加速模型的训练过程,都是直接使用小批量样本来进行训练,而这就导致了不同样本长度不能进行计算的问题,因而要使用填充操作来使同一个 batch 中的序列长度保持一致。同时,在自注意力机制中,为了防止注意力被分配到 padding 位置上,所以我们需要通过 padding mask 来忽略掉这部分注意力值。

本来一切的合情合理,但这个时候 Decoder 中偏偏又出现了 Attention mask,如图9-2所示。

图 9-2. Attention mask
图 9-2. Attention mask

在图9-2中,由于 Attention mask 的作用,在得到 Query 中 “who am I P P P” 这个序列的编码结果时:对于 “who” 来说模型只会讲注意力分配到 V 中的第一行上;对于 “am” 来说模型只会将注意力分配到前面两行上;对于 “I” 来说模型只会将注意力分配到前面三行上。 下面关键点来了,敲黑板!!!!

此时我们可以发现,在对前面三个 token 依次进行编码时,由于有 Attention Mask 的存在,所以不需要tgt key padding mask 同样能够保证注意力部分配分到 padding 位置上。

进一步,对于 Query 中后三个位置的编码来说:

① 如果没有 tgt key padding mask 的存在,那么模型在对这三个位置的 token 进行编码时,会分别将注意力分配到 V 中的前四行、前五行和前六行上,换句话说注意力会被分配到 padding 位置上。

② 如果有tgt key padding mask 的存在,那么模型在对这三个位置的 token 进行编码时,都只会将注意力分配 V 中的前三行上,也就是注意力不会被分配到 padding 位置上。

到此时,我们依旧能够得出加与不加 tgt key padding mask 得到的编码结果 Query 的确是不一样的,但是:

你 Query 中的后三行本来就是被标记为填充位置的,你被编码成什么样子、是对的还是错的我们关心吗?

我们不 Care 呀!我们又不会用到你,我管你被编码成什么样了。

不 Care,真的不 Care !

那从哪里可以体现出它的的确确没有被使用到呢?

答:在最后计算损失的时候。

图 9-3. 损失计算过程
图 9-3. 损失计算过程

如图9-3所示,模型根据 Query 和 Encoder 的输出 Memory 交互后得到了输出 Output,再经过一个分类层得到了 y_pred。我们可以看到,如果根据 logits 我们将会得到 “no am who you are .” 这样一个结果(而真实的标签为 “who am I P P P”,即 “4 2 5 0 0 0”)。

由于真实标签的最后三个 token 是填充的,所以我们在计算损失的使用会通过指定 ignore_index 来指定忽略的位置。因此,对于上述预测结果来说在计算损失时只会考虑 “no am who” 对应的损失,所以后三行被编码成什么结果(是否考虑到注意力分配到了 padding 位置上),都不会影响模型的训练过程。

$$ \text{loss}=-(\log 0.12+\log 0.22+\log 0.31)=4.806 $$

到此,我们总算说清楚了在训练阶段原来真的可以不用 tgt key padding mask.

那推理阶段呢?当然更不需要啦,都没有 padding 这回事

9.3 编码验证#

上面我们从原理上分析了训练阶段可以不用 tgt key padding mask 这一结论,下面我们再来通过实际代码计算一下,看看两种情况下的损失是否相等。

 1 import torch
 2 import torch.nn as nn
 3 
 4 torch.random.manual_seed(42)
 5 
 6 d_model = 7
 7 tgt_len = 6
 8 src_len = 5
 9 
10 #===================Encoder=================== 
11 src_em_input = torch.randn(src_len, d_model)
12 q_proj0 = nn.Linear(d_model, d_model)
13 k_proj0 = nn.Linear(d_model, d_model)
14 v_proj0 = nn.Linear(d_model, d_model)
15 q, k, v = q_proj0(src_em_input), k_proj0(src_em_input), v_proj0(src_em_input)
16 src_key_padding_mask = torch.tensor([[False, False, False, True, True]])
17 weight = q @ k.transpose(0, 1)  # [src_len,src_len]
18 print("Encoder attention weight before key padding:")
19 print(weight)
20 weight = weight.masked_fill(src_key_padding_mask, float('-inf'))
21 print("Encoder attention weight after key padding:")
22 print(weight)
23 weight = torch.softmax(weight, dim = -1)  # [tgt_len,tgt_len]
24 memory = weight @ v  # [src_len,d_model]
25 
26 #===================Decoder=================== 
27 ## ------------------ Masked Attention
28 tgt_em_input = torch.randn(tgt_len, d_model)
29 tgt_key_padding_mask = torch.tensor([[False, False, False, True, True, True]])
30 attn_mask = (torch.triu(torch.ones((tgt_len, tgt_len)))==1).transpose(0, 1)
31 attn_mask = attn_mask.float().masked_fill(attn_mask==0, float('-inf')).masked_fill(attn_mask==1, float(0.))
32 q_proj1 = nn.Linear(d_model, d_model)
33 k_proj1 = nn.Linear(d_model, d_model)
34 v_proj1 = nn.Linear(d_model, d_model)
35 q, k, v = q_proj1(tgt_em_input), k_proj1(tgt_em_input), v_proj1(tgt_em_input)
36 # [tgt_len, d_model]
37 weight = q @ k.transpose(0, 1)  # [tgt_len,tgt_len]
38 weight + =attn_mask
39 print("Decoder masked attention weight before key padding:")
40 print(weight)
41 # weight = weight.masked_fill(tgt_key_padding_mask, float('-inf'))  #
42 # print("Decoder masked attention weight after key padding:")
43 print(weight)
44 weight = torch.softmax(weight, dim = -1)  # [tgt_len,tgt_len]
45 output = weight @ v  # [tgt_len,tgt_len] @ [tgt_len, d_model]
46 
47 ## ------------------ Cross Attention
48 q_proj2 = nn.Linear(d_model, d_model)
49 k_proj2 = nn.Linear(d_model, d_model)
50 v_proj2 = nn.Linear(d_model, d_model)
51 q, k, v = q_proj2(output), k_proj2(memory), v_proj2(memory)
52 # q: [tgt_len, d_model],  k,v: [src_len, d_model]
53 weight = q @ k.transpose(0, 1)
54 # [tgt_len, d_model] @ [d_model,src_len] = [tgt_len,src_len]
55 weight = weight.masked_fill(src_key_padding_mask, float('-inf'))
56 weight = torch.softmax(weight, dim = -1)
57 output = weight @ v  # [tgt_len,src_len]  @ [src_len,d_model] = [tgt_len,d_model]
58 
59 #===================Loss=================== 
60 
61 tgt = torch.tensor([1, 2, 3, 0, 0, 0], dtype = torch.long)
62 loss_fcn = torch.nn.CrossEntropyLoss(ignore_index = 0)
63 loss = loss_fcn(output, tgt)
64 print(loss)

打开是否需要tgt_key_padding_mask的开关在第41,42行,其对应的 weight

输出结果分别为:

1 tensor([[-1.2846,    -inf,    -inf,    -inf,    -inf,    -inf],
2         [-0.0381, -0.1050,    -inf,    -inf,    -inf,    -inf],
3         [ 0.1754, -0.1007,  0.4034,    -inf,    -inf,    -inf],
4         [ 0.1751,  0.0842,  0.0198, -0.4393,    -inf,    -inf],
5         [-1.5436, -0.4939, -1.5253, -0.9625, -0.4269,    -inf],
6         [-0.3050, -0.0124, -0.0216, -0.5008, -1.5813, -0.0728]],

1 tensor([[-1.2846,    -inf,    -inf,    -inf,    -inf,    -inf],
2         [-0.0381, -0.1050,    -inf,    -inf,    -inf,    -inf],
3         [ 0.1754, -0.1007,  0.4034,    -inf,    -inf,    -inf],
4         [ 0.1751,  0.0842,  0.0198,    -inf,    -inf,    -inf],
5         [-1.5436, -0.4939, -1.5253,    -inf,    -inf,    -inf],
6         [-0.3050, -0.0124, -0.0216,    -inf,    -inf,    -inf]],
7        grad_fn=<MaskedFillBackward0>)

在这两种情况下,其对应的损失值均为 tensor(1.8582, grad_fn=),具体分析各位读者自行运行代码。

9.4. 总结#

在本篇文章中,首先详细地介绍了Transformer论文的动机以及自注意力机制的原理与多头注意力机制的作用;然后介绍了Transformer中的位置编码以及整个预测和训练过程中模型的编码解码过程;接着进一步介绍了Transformer的网络结构、自注意力机制的原理实现以及Transformer网络的实现;最后,还通过3个实例(包括论文中的翻译模型、文本分类模型,以及对联生成模型)从代码的角度来介绍了整个Transformer网络的原理与使用示例。

在下一篇文章《This Post Is All You Need(下卷)——步步走进BERT》中,将会逐一详细介绍BERT模型的原理细节以及多个下游任务的微调场景等。

引用#

[1] Vaswani A, Shazeer N, Parmar N, et al. Attention is all you need [2] The Illustrated Transformer http://jalammar.github.io/illustrated-transformer/ [3] https://towardsdatascience.com/illustrated-guide-to-transformers-step-by-step-explanation-f74876522bc0 [4] https://pytorch.org/docs/stable/generated/torch.bmm.html?highlight=bmm#torch.bmm [5] LANGUAGE TRANSLATION WITH TRANSFORMER https://pytorch.org/tutorials/beginner/translation_transformer.html [6] The Annotated Transformer http://nlp.seas.harvard.edu/2018/04/03/attention.html [7] SEQUENCE-TO-SEQUENCE MODELING WITH NN.TRANSFORMER AND TORCHTEXT https://pytorch.org/tutorials/beginner/transformer_tutorial.html [8] Transformer model for language understanding https://tensorflow.google.cn/text/tutorials/transformer?hl=en#multi-head_attention [9] https://github.com/multi30k/dataset [11] https://github.com/moon-hotel/TransformerTranslation [12] https://github.com/moon-hotel/TransformerClassification [13] https://pytorch.org/tutorials/beginner/text_sentiment_ngrams_tutorial.html [14] https://github.com/moon-hotel/TransformerCouplet

阅读 --

第8节 Transformer中的全连接层

本文是 Transformer 精读系列第8讲,从两个被普遍忽略的角度复盘 Transformer 结构。其一是 Feed-Forward Network FFN 中的逐元素非线性:从表达能力、层间信息动态处理、与传统 MLP/LSTM 中 …

第7节 基于Transformer的对联生成模型

本文是 Transformer 精读系列第7讲,使用 GitHub 上 70 万条公开对联数据集(couplet-dataset)训练一个中文对联生成模型。系统讲解对联生成与机器翻译在模型结构上的异同(共用中文词表、上下联对应 …

第6节 基于Transformer的分类模型

本文是 Transformer 精读系列第6讲,从零用 Transformer Encoder 搭建 AG_News 新闻文本分类模型。系统讲清分类任务只需 Encoder 不需要 Decoder 的原因、Encoder 输出张量的池化策略 …

第5节 基于Transformer 的翻译模型

本文是 Transformer 精读系列第5讲,从零开始搭建一个英语到德语的机器翻译模型。以 Multi30K 平行语料为对象,详细讲清 6 步数据预处理流程(tokenize、词表、序列化、padding、`/` 与 …

第4节 Transformer 的实现过程

本文是 Transformer 精读系列第4讲,全程使用 PyTorch 一步步实现完整的 Transformer 模型,覆盖 Token Embedding、正弦 Positional …

第1节 多头注意力机制原理

本文是《Attention Is All You Need》精读系列第1讲,从 RNN 串行瓶颈出发讲清 Transformer 为什么必须引入自注意力,再用图解与公式拆解 Scaled Dot-Product …

第2节 位置编码与编码解码过程

本文是 Transformer 精读系列第2讲,用图解与对比示例讲清 Transformer 为什么必须加入位置编码,并拆解正弦 Positional Encoding 公式与高低频维度直觉。随后通过可视化方式演示 …

第3节 网络结构与自注意力实现

本文是 Transformer 精读系列第3讲,详细讲解如何用 PyTorch 一步步实现 MultiHeadAttention 模块。先回顾多层 Transformer 与单层 Encoder-Decoder 结构,再系统拆解自注意力实现 …