【NLP】第十二章:Transformer原理、计算流程以及代码实现-2

15 篇文章 ¥9.90 ¥99.00

说明:由于内容太多,所以进行了拆分。本部分承接前文:【NLP】第十一章:Transformer原理、计算流程以及代码实现-1-CSDN博客

四、pytorch中的Transformer
输入数据整理完毕后,按道理应该继续展示数据在编码器-解码器-输出部分的流动过程,但是这里我添加一个使用pytorch调包实现的环节。因为要确保此后我们手动计算的数据流是正确的,就是至少要和pytorch计算出来的结果一致的,才能证明我们对Transformer的理解是没有偏差的。所以我们得先调用pytorch的Transformer类先来计算一下,然后我们再自己手动计算,最后看看结果是不是一致。所以本部分可以看作是,从pytorch角度再来理解一遍Transformer模型

1、pytorch中用于构建Transformer的各个组件介绍
pytorch中的Transformer的依据是17年的Attention is all you need论文编写的实现方法。但是pytorch中是没有完整的Transformer算法的。因为在NLP世界中,不同的任务会对Transformer架构提出不同的要求,编码器和解码器是设计成可以独立或一起使用的组件。它们可以根据不同的NLP任务需求进行组合,以适应各种场景。现在的NLP任务都比较复杂,现在几乎没有人单独使用一个Transformer去完成一个nlp领域的任务。在nlp实战中,一般都是用多个Transformer组合起来的大型架构,比如Bert,GPT。也所以我们现在都流行用像Huggingface这样的新框架,从中直接调用BERT,GPT等架构。也所以pytorch只是提供了各种任务的基础架构,这种分离使用编码器或解码器的设计使得Transformer架构极为灵活,可以通过调整来适应各种不同的NLP任务需求。而具体的架构选择(编码器、解码器或者二者兼用)取决于我们试图解决的问题类型。而且一个完整的工业级NLP任务,除了这些,还需要进行数据预处理、mask的创建、训练循环、损失函数计算、优化器配置等环节。

pytorch中没有完整的Transformer架构,只有用于构建Transform的各个层。所以Transformer的各个组件都是位于pytorch.nn模块下面的:

(1)nn.Transformer仅仅封装了Transformer中包含编码器(Encoder)和解码器(Decoder)。Transformer架构中的输入部分和输出部分是不包括在这个里面的。
(2)nn.TransformerEncoderLayernn.TransformerDecoderLayer这两个类表示Transformer编码器的单层和解码器的单层。它包含了自注意力机制(self-attention)和前馈网络(feedforward network, FFN),以及必要的的归一化和残差连接。
(3)nn.TransformerEncodernn.TransformerDecoder:这两个类是Transformer编码器的实现和解码器的实现,其中nn.TransformerEncoder包含了多个nn.TransformerEncoderLayer层的堆叠。nn.TransformerDecoder包含了多个nn.TransformerDecoderLayer层的堆叠。
(4)nn.MultiheadAttention:这个模块实现了多头注意力机制,这是Transformer模型的核心组件之一,前面注意力篇章有详细讲解。多头注意力允许模型在不同的位置同时处理来自序列不同部分的信息,可以实现交叉注意力的计算。
(5)nn.LayerNorm:层归一化(Layer Normalization)通常用在Transformer的各个子层的输出上,有助于稳定训练过程,并且提高了训练的速度和效果。
(6)nn.Embedding:将每个标记Token(如单词、字符等)映射到一个高维空间的向量。使模型能够处理文本数据,并可以伴随模型训练进行迭代,为每个唯一的标记赋予语义信息。这个层前面反复讲过多次。
(7)nn.Transformer.generate_square_subsequent_mask:掩码函数。用于生成一个方形矩阵,用作Transformer模型中自注意力机制的上三角遮罩。这个遮罩确保在序列生成任务中,例如语言模型中,任何给定的元素只会考虑到序列中先于它的元素(即它只能看到过去的信息,不能看到未来的信息)。这种掩码通常在解码器部分使用,防止在预测下一个输出时"作弊"。具体来说,该函数创建了一个方阵,其中对角线及其以下的元素为0(表示可以"看到"这些位置的元素),其余元素为负无穷大(在softmax之前应用,表示位置被屏蔽,不应该有注意力权重)。


Transformer的这种模块化构件,使得它可以拆分使用或者魔改自由拼装。通常不同的LNP任务上会使用Transformer架构的不同部分,很多更优的模型也是在Transformer的基础上调整改造的。

2、nn.Transformer()类的参数解释
从上图可见,nn.Transformer是最终的集成类,我们就用这个类来搭建Transformer模型,所以我们先看看这个类的参数:

A:之所以有这个参数是因为文章 https:://arxiv.org/pdf/2002.04745v1.pdf 比较了Post-Layer norm和Pre-Layer norm的区别,证明了Layer norm放在residual block之后输出层附件参数的期望梯度较大。在较大的梯度上使用较大的学习率会使训练不稳定,因此需要热训练学习率。而将LN放在residual block之前,则会让梯度表现的更好,可以取消热身训练,以及更少的超参调整和训练时间。pytorch在定义Transformer的时候也是考虑到这个因素,才设置的norm_first参数来控制Post-LN和Pre-LN。

但是上图右边的(a)是Transformer论文的架构,是Post-LN,所以pytorch的norm_first参数默认是False。

如果你想用Pre-LN,那你就把参数norm_first打开即可。这也是此后Transformer的一个重要的改进点。其实这个知识点我们并不陌生,我在讲残差网络时 【深度视觉】第十章:复现SOTA 模型:ResNet_视觉lstm的sota-CSDN博客 ,就详细讲过前辈们是如何实验BN层前置和后置的,对残差网络熟悉的同学对这个不会陌生。

3、使用torch.nn.Transformer()类创建我们自己的架构

由于我们现在是深入学习Transformer的,我们的目标是拆解Transformer的各个计算细节和流程。所以这里的:
d_model=6,因为我的词向量是6个特征嘛。
nhead就设置2个头,麻雀虽小五脏俱全嘛。
编码器和解码器都各设1个,迷你的架构才容易截图,容易说清楚,容易看清对错嘛。
FFN的就设置为d_model的两倍,12个神经元。
dropout层就设置为0,因为dropout是随机失效一些特征的,如果设置了这个参数,每次运行的结果都不一样,我们就无法复现结果了。
激活函数就用默认的relu。
batch_first设置为True,因为我们的数据就是按batch_first=True设计的。
norm_first使用默认的False即可,因为我们不是实验效果,目的是跑正确一个正向传播,看清所有细节的。

4、Transformer的架构
标题3已经实例化一个最简Transformer模型了,我们就可以通过pytorch查看它的架构了:

说明:torch.nn.Transformer()类实例化的架构,只包含编码器和解码器。所以上图只是编码器和解码器的架构,也不是数据流!
上图的架构中,前面的小括号是那个层的名字。上图我按照层名给大家一一标注出每个层对应在右边的什么位置。但是上面的层也不是整个架构的所有层,比如激活层、残差层、dropout层就没有展示,但是这些层都将会牵扯到数据流的变换,所以这里我先给大家注明了,后面还会针对这些层进行深挖讲解。

5、pytorch中的Transformer的数据流
根据标题4的架构,我们现在就可以进一步细化数据流了:
下图基本就是数据流的最底层细化流程了,这些是我看torch.nn.modules.Transformer.py源码后总结的,所以基本上也就是pytorch对transformer论文《attention is all you need》的理解和实现:

但是,上图中最主要的三个注意力模块我没有展开,一是因为我在前面已经把它非常非常详细得讲过了:【NLP】第五章:注意力机制Attention_nlp 交叉注意力机制 教程-CSDN博客 ,二是还差一个mask掩码没讲过,但是这个点涉及的内容有点多,下面单独讲。其次就是归一化层norm、前馈层FFN、激活层(relu\gelu)、Dropout层再展开讲讲细节就讲完pytorch角度下的Transformer了。

6、Add & Norm 模块
Transformer模型中的每个子层都伴随一个残差连接,然后紧接着一个层归一化操作。具体来说,对于每个子层(例如自注意力或前馈网络),输入首先通过子层自身,然后将子层的输出与输入进行相加Add(残差连接),最后对这个相加的结果进行层归一化Norm。

(1)Add & Norm 中的 Add
Add为residule block(残差模块),就是残差连接(Residual Connection),也叫跳跃连接(shortcut connection)。数据在这里进行加和。
这个操作是残差网络的灵魂、是加深网络的法器,可以有效避免由于网络过深而出现退化的现象。在我的残差网络博文 【深度视觉】第十章:复现SOTA 模型:ResNet_视觉lstm的sota-CSDN博客 里面有对网络退化跳跃连接操作的详细解读。

(2)Add & Norm 中的 Norm
Norm为Normalization模块,也叫标准化模块、归一化模块、规范化模块等称呼。Transformer中的Norm又称Layer Norm,LN,属于规范化一族,和Batch Normalization、Group Normalization、Instance Normalization等规范化技术有着相似的目的,但操作方式略有不同。

一般情况下,Norm层都是用于调整数据流的,因为它将输出限制在一定的范围内,这样就可以减少梯度消失或梯度爆炸的问题了,就提高了模型的稳定性和训练效率。Norm在Transformer论文中也是被一笔带过,但Norm之于Transformer的意义远不止规范数据流,Norm是Transformer中不可或缺的一部分!有人做实验,将网络中的LN层全部删除后,模型就不再收敛了。因此对于Transformer来说,LN的效果已经不是"有多好",而是"不能没有"的操作!但有人竟然据此说LN就如LSTM中的tanh,它是为Transformer提供非线性表达能力、增强模型效果的。我个人认为这样说有点不对。LN在调整数据流的时候提供的仅仅是线性变换不是像relu或者像tanh这样的非线性变换!所以LN的主要功能还是自适应地调整数据流的作用。

Norm看似简单其实里面有不少弯弯绕,实属崩溃,我是整理一次崩溃一次!
对于Norm,我们只需要注意两点:一是它的位置。Norm的位置在小标题2中的A处已经说明白了,这里不再赘述。二是它的计算流程和独特的迭代方式,这里我展开讲一下:
LN是由BN发展而来的,所以这俩经常被放在一起对比着说,下面我也对比展示一下BN和LN的手动计算过程:

但是在实际中,不管是BN还是LN的计算过程不止上图那么简单,下面我们再看看pytorch中BN层的计算过程和迭代方式:

唉,一会儿有偏一会儿又无偏,整理这张图我要吐血了,被一个几乎无关的知识点弄得眼花缭乱的。。。。。大概是不甘心吧,因为曾经梳理过不止两次BN,没想到又忘干净了。我这篇博文 【深度学习】第六章:模型效果评估与优化_模型评估与优化-CSDN博客 中有对BN层的更加详细的解读,可以参考。其实这个计算流程是:

一是,BN层是按batch中的一个个特征来归一化的。由于归一化都可以转化成线性变换的方式,所以pytorch在实现BN层时,用的是一个特殊的线性层(深度分组FNN)来实现的。所以线性层上的神经元对应的参数就是根据反向传播求得的梯度来迭代的。而且这个线性层的初始化不是随机的,是w=1,b=0,这样初始化的。为什么要这样初始化?自然是它的本性喽,它生来就是调整数据流的嘛,所以初始化参数是从1倍缩放、0偏移开始自适应的迭代的。

二是,其实BN层的设计是非常巧妙的。假如我们的样本有100万条,如果你对100万条样本一次归一化,是不是计算量就爆炸了,但是一个一个batch的计算running_mean和running_var,那这100万条数据迭代完毕后,是不是最终的running_mean和running_var就是无限逼近整体数据的均值和方差。所以,BN在每归一化一个batch数据时,都会将running_mean和running_var和这个batch数据的mean和无偏方差进行加权求和。或者说,BN每归一化一个batch数据后,整体的均值和方差都会往这个batch数据的均值和方差的方向移动0.1倍的距离。上图A就是我们迭代running_mean和running_var时使用的比例。所以说BN是一个非常精妙的设计,非常适合在GPU上跑。

三是,BN层是分训练(model.train())和测试(model.eval())两种状态的。当模型处于训练状态下,BN层的参数(阿尔法,贝塔)是要根据反向传播求得梯度进行更新的。因为它本质上就是一个线性层嘛。所以BN是起到一个自适应的调整数据流的作用的。所以在训练状态下,B参数affine=True,表示是要更新参数的。此外,在训练状态下,模型还得要一个个batch搜集整个数据集中的样本的均值和方差的,所以C参数track_running_stats=True。
当模型训练完毕开始测试的时候,BN层的参数B以及C都要停止迭代,所以此时affine=False, track_running_stats=False。此时测试集数据流经BN层时就不用计算批次的均值和方差了(有时就一条测试样本,你也无法计算均值和方差啊),就直接对测试集进行running_mean和running_var归一化,再用训练完毕的BN参数进行缩放和偏移。

四是,再强调一遍:在训练状态,数据流经过BN层时,BN层是先计算这个batch的均值和有偏的标准差,然后进行减均值除有偏标准差的线性变换,计算完毕后再根据参数(阿尔法和贝塔)再对数据进行二次线性变换!如上图的E、F处,E处是减均值除有偏标准差的操作,F处是乘阿尔法加贝塔的操作。这是BN层数据变换的特殊之处!!!

  • 下面看看pytorch中LN层的计算过程和迭代方式


非常了解了BN,LN就是BN的简化版:

一是,LayerNorm不会像BatchNorm那样跟踪统计全局的均值方差,因此train()和eval()对LayerNorm没有影响。
二是,elementwise_affine如果设为False,则LayerNorm层不含有任何可学习参数。这是LN的一个优势,因为不需要批训练嘛,在单条数据内部就能归一化。但是这个操作要慎用啊!因为前文说过LN层是自适应调整Transformer数据流的,你把LN的参数关闭了,不是自己找不痛快嘛。
三是,网上有大量LN层的优点和好处,只能说都是一些无理论支撑的感性结论,比如上面说的非线性表达,存粹就是瞎说,从我们这里的计算流程看,哪里是非线性变换了?!样本内部归一化也是线性变换,LN参数变换也是线性变换,压根就没看到非线性的影子!
四是,LN层的作用就是平滑每个样本内部各个特征之间的差异,防止某些维度的特征过大或者过小,出现梯度爆炸或者梯度消失的情况。
五是,LN层的参数和BN层的参数一样!都是进行线性变换的!而且计算方式都是深度分组网络的计算方式。
六是,总之,不管是BN还是LN,都是规范数据流的,都是自适应的调整数据流的,防止训练中出现梯度问题的。

7、前馈层Feed Forward(FFN) + 激活函数
Transformer中的激活函数只出现在FFN层,因为FFN是个线性层嘛,在线性层后面加激活函数是常识,所以本小节把FFN和激活函数放一起讲。

在Transformer中,从功能上说,FFN层是和注意力层并驾齐驱的一个组件,二者各司其职,相互补充。注意力层是通过计算序列中所有样本之间的注意力权重,实现对样本与样本之间信息的全局捕捉,所以注意力层是不直接对样本特征进行变换的。在Transformer架构中,FFN通常位于注意力层之后,是负责对每个样本的特征进行独立变换,是学习样本特征的一个模块。所以说它和注意力模块是各司其职互相补充的。

(1)在Transformer论文中,FFN的全称是Position-wise Feed-Forward Networks,有的地方叫点对点前馈神经网络,有的叫位置前馈网络,真是五花八门,其实它就仅仅是一个两层的全连接线性层而已!从上图LN的数据流图看,是不是LN才更是一个Position-wise,点对点的呀。。。。这里的FFN就是一个最原始的全连接线性层,全连接线性层是要充分混合了一个词向量中的所有特征,就是把所有特征按网络参数权重相乘再相加,真是混得彻彻底底的,咋就Position-wise,点对点了?!网上还有大量的文章说FFN的优点是可以分布式计算、就是并行计算,,,可以保留位置信息,,,反正我是有点懵,我是没搞懂它咋就保留位置信息了,咋又可以并行计算了。实在无语。。。。不知道这个层为啥取这么一个傻x名字,还有一堆傻x解读,害的我查询了好久,心情难以平静。。虽说天下文章一大抄,那抄的时候也得过过脑子啊。。。一片傻x,乌烟瘴气的,找不到北。。。

Transformers原始论文对此的解释是:FNN可以看作用1x1的卷积核来进行特征的升维和降维。对卷积层非常了解的同学对此就不惑了,卷积层最大的特点就是平移不变性,如果用1x1的卷积核那就是Position-wise,点对点了。但是原始论文对FFN的实现又不是用1x1的卷积核,而是全连接线性层,所以pytorch也是使用全连接线性层:

第一个线性层将特征数量从d_model(论文中是512维)扩充到dim_feedforward(论文中是2048维,是d_model的4倍),第二个线性层再将特征数量从dim_feedforward个映射回d_model个。论文中的公式是:

公式中的x是输入张量,W1、b1、W2、b2是两个线性层上的权重和偏置;max(0, xW1+b1)是对第一个线性层后面施加一个Relu激活函数。
第二个线性层不加激活函数,说明第二层是负责输出的。第一个线性层后面加一个relu激活函数,说明第一个层才是真正进行捕获非线性关系的层!才是为Transformer提供非线性表达能力、增强模型效果的。

所以,FFN变小意味着model capacity也变小,大概率会让整体performance变得很差。两个线性层中间的hidden dimension的expansion ratio一般设置为4,有人在ViT上的实验,把expansion ratio调小会发现怎么都不如大点好。当然太大也不行,因为FFN这里的expansion ratio决定了整个Transformer在推理时的peak memory consumption,有可能造成out-of-memory (OOM) error,所以大部分我们看到的expansion ration也就在4倍,这是一个比较合适的performance-memory trade-off.

这是我到网上看到的关于"第一个线性层的神经元个数设置"的言论,他说的model capacity就是第一个线性层的神经元个数是词嵌入向量特征个数的倍数,说的是一个经验倍数4倍是最优的。虽然不怎么说人话,但他说得没错。

(2)我们知道全连接线性层主要是用来学习特征之间的关系,是对特征进行线性变换的,是对特征进行升维,然后在高维空间捕捉特征之间的潜在关系。而全连接层后面的激活函数则是进行非线性变换的,是捕捉特非线性关系的。所以整个Transformer中,注意力模块是学习样本与样本之间的关系的,残差连接是避免网络退化的,LN是调整数据流的,只有FFN才是学习每个样本中的特征的。或者说词向量中的语义信息其实是存储在FFN中的。就是FFN才是学习语义的。所以FFN是Transformers必备的功能模块,没有FFN的Transformers是学不到什么东西的。也所以FFN是后人魔改的一环,一般都是从两方面入手:一是魔改线性层,但线性层已经极简极底层了,似乎魔改的空间不大,有的人干脆直接换架构,比如改成卷积层。另一方面是从激活函数方面魔改,就是换各种激活函数喽,其中最常见的是改rele为GELU:

GELU(Gaussian Error Linear Units) 是一种基于高斯误差函数的激活函数,相较于ReL等激活函数,GELU更加平滑,有助于提高训练过程的收敛速度和性能。GELU可以看作是一种sigmoid和ReLU的混合体: 
GELU激活函数普遍应用于Transformer、BERT、GPT-2等模型中。但是GELU激活函数的计算相对复杂,涉及到指数、平方根和双曲正切等运算,因此在计算资源有限的情况下可能会带来较大的计算开销,或者说就是计算效率低。而ReLU是一个非常简单的函数,仅仅是输入为负数时返回0,而输入为正数时返回自身,从而仅包含了一次分段线性变换。但是,ReLU函数存在一个问题,就是在输入为负数时,输出恒为0,这个问题可能会导致神经元死亡,从而降低模型的表达能力。GeLU函数则是一个连续的S形曲线,介于Sigmoid和ReLU之间,形状比ReLU更为平滑,可以在一定程度上缓解神经元死亡的问题。GELU函数的输出可以落在一个更广的范围内,有助于加速模型的收敛速度。

8、Transformer中的掩码机制(masking)
mask对于处理复杂任务有着至关重要的作用。但是mask也是一个很多的点。此处处理不好会导致训练中出现很多nan,无法训练的情况。出现nan还是看得见的坑,还有一些看不见的坑,比如死活收敛不了,死活效果提不上来等,多少都和mask操作有关系。这部分也比较难,你得对整个数据流非常清晰,你才知道这部分该怎么操作,这个操作影响了什么,以及是如何影响的,也是你能魔改架构、处理更加复杂任务的底气所在。

但是,本部分内容特别乱,从各种随心所欲的名称叫法就可窥一斑。而且这部分和前面的编码器输入字典中P编码为0的设计标识符,尤其是标识符中的0填充、以及D为啥叫因果注意力模块,它是咋因果了我前面为啥说让Q更纯洁,咋就纯洁了,为啥要纯洁等问题,都有千丝万缕的联系和呼应。我尽量往细了梳理吧。

(1)Transformer中有哪些地方会涉及到mask?
Transformer中有两个地方会涉及到mask。一是3个注意力模块会用到mask;二是在训练模型计算损失loss时,需要指定ignoreindex=pad_index,也是一种mask。

(2)注意力模块中的mask的功能
在Transformer的注意力模块中使用mask,主要是达到下面两种目的:

  • padding_mask(填充掩码)

    padding_mask也叫填充掩码,有的地方还叫attention_mask,非常混乱。
    从功能上看就是把输入注意力模块sequence中的0填充样本都给mask了。为什么?

    因为我们在模型训练过程中,数据会以batch(多个句子向量,也就是多个sequence)的形式输入到模型进行训练,而自然语言中一个batch内的每一句话的长度可能不一致,为了保证一个batch内输入的长度一致,所以需要将所有sequence都补全到最大的长度,也就是将短句子的长度填充到最长句子的长度,一般使用0进行填充。这种用0填充的样本是没有意义的,而且这种样本的数量还可能会很多,因此:

    一是,在正向传播过程中,这些样本不应该成为计算注意力分数扭转V矩阵的重要因素。就是这些样本计算出来的注意力权重不能太大,不能成为生成V矩阵的重要因素,因为这些样本没有意义。

    二是,在反向传播求梯度的模型训练过程中,如果这些0填充样本的特征数值比较大,那么反向传播求梯度时,链式求导过程中的连乘项就是这些样本的特征数值,导致这些样本流经的网络线路上的参数梯度就比较大。这样模型就没有学习有效样本,而是学的都是0填充样本。这是我们不想看到的。而且很容易陷入模型难以收敛、无法训练的窘境。所以我们需要mask来避免0样本对train的影响。

    padding_mask就是在注意力计算过程中,让注意力分数集中在有意义的词上,而非0填充的词上。所以Transformer的三个注意力模块都会用到padding_mask

  • sequence_mask(序列掩码)

    sequence_mask也叫序列掩码,有的地方叫causal_mask(因果掩码。),有的还叫future_mask,有的地方竟然也叫attention mask, 很无语。。。

    sequence_mask是一个上三角矩阵

    在Transformer模型中,sequence_mask只出现在解码器的第一个注意力模块:带掩码的多头自注意力模块。因为这个注意力模块的输入是目标文本,就是英译文数据,一是提取英译文sequence中的样本和样本之间的联系的,二是用答案生成纯粹的Q的。所以,sequence_mask一是用来遮挡目标文本的,防止解码器提前看到待预测的内容。或者说是防止泄露要预测的标签信息,需要用mask来“遮盖”它的。二是生成更加存粹的Q,让一个存粹的Q去和解码器第二个注意力模块中去匹配K(V)。

    所以以后你看到有人给Transformer解码器中的第一个注意力模块叫因果自注意力层,你从sequence_mask角度来解释,就也是一个自圆其说的答案。

anyway, 不管你的mask叫啥,你只要清楚它的功能是干什么的,你就知道怎么操作了。或者说,不同的mask是由你任务的特定需求决定的,如果你的任务就不需要解码器,那自然就不需要sequence mask。有些人需要对Transformer魔改,比如它想让不同的头看到不同的信息,那他可能就需要其他特定的mask了,那就需要他自己去制作特定的mask,就是不同任务会对应不同的mask。

(3)Transformer的三个注意力模块涉及到的mask:
我们知道Transformer架构中有三个注意力模块。编码器中有一个多头自注意力模块、解码器中有两个带掩码的多头自注意力模块多头交叉注意力模块。根据上面说的mask的功能,我们可以推断出这三个模块分别需要的mask是:

  • 编码器的自注意力模块,用到的掩码是padding_mask。因为数据集中序列不固定,而网络却要求固定长度输入。

  • 解码器中的掩码自注意力模块,要同时用到padding_mask和sequence_mask。其中padding_mask是遮挡目标文本中的0填充的。sequence_mask是实现因果的。

  • 解码器中的交叉注意力模块,用到的掩码是padding_mask。而且这个mask和编码器的自注意力模块使用的mask是同一个mask。这个非常好理解,因为就是编码器的输出,进入解码器的交叉注意力模块中的呀,所以它的0填充样本的位置是不变的,所以这两个mask是一样的。

(4)padding_mask的实现原理:
padding_mask的目的是为了让attention的计算结果不受padding的影响,以我们的数据"我喜欢猫"这句话为例:

上图中,A表示只要Q和K其一为padding,那么我们就将其attention权重置为0;
而B表示当K为padding时将对应attention权重为0。

在实际模型训练过程中,我们使用的是B而不是A!!!因为使用B不会出错,因为Q为padding的部分最终计算loss时会被过滤掉,就是我前面提到的指定ignoreindex=pad_index时会被过滤掉,不纳入计算。所以Q是否被mask无影响。而使用A时,由于有些行的所有位置都被mask掉了,这部分后面扭动V时容易出现nan。所以我们一般都是把K中的padding部分mask掉。
也所以在pytorch的实现中,padding_mask掩码的参数是xx_key_padding_mask。这个后面我们还要展示。

(5)sequence_mask的实现原理
sequence_mask的目的一是为了遮蔽答案,二是为了让Q更纯粹。sequence_mask只出现在解码器的第一个注意力模块中
输入解码器第一个注意力模块的数据是目标文本,我们以目标文本中的"I like cat"为例:

从上图就可以直观的看出:

一是,为啥我前面一直说让进入交叉注意力模块中的Q更纯粹。在注意力机制中,我们是拿一个个样本当Q来匹配KV的。从上图可见,用sequence_mask遮盖后的attention score矩阵,这个矩阵在扭转V时,得到的B。B是以Q的身份,进入解码器第二个交叉注意力模块的,此时的Q是不是就是一个更加存粹的Q,它是不含当前样本之后的样本信息的样本,是不是就实现了一是遮挡答案的目的,二是实现了从答案到“不含答案的Q”的华丽转身!

二是,目标文本为啥要标识符S。你可以反向思考一下,如果没有S,用一个对角上三角mask住attention score矩阵,那它扭转V是不是把第一个样本全部置为0了,你再拿一个全是0的样本当作Q进入交叉注意力模块,Q找到的K就是全是0呀,因为Q找K时是和K做点积运算的呀,可不都是0了,后面也可以预见全是0,那我们训练个啥呀。所以这里加一个S标识符是一个非常巧妙的设计、非常智慧和聪明!

三是,从这个计算流程上看,也就是我们模型的训练阶段,模型的预测不是一个个单词蹦出来的,是一次性预测的!没有一个个预测一说!只有当模型处于测试阶段,此时已经没有目标文本可输入了,pytorch底层会用代码写死,将前面已经预测出来的单词,加入到A中,自动生成目标文本,才出现一个个单词蹦着预测出来的效果!

是不是前面很多疑问到这里豁然开朗了一些了?!

(6)使用pytorch调包,如何传入mask?
ptorch中的nn.Transformer()类的参数是分两部分:一部分是init中定义架构的参数(小标2已经详细介绍过了),另一部分是forward的推理参数,这部分才涉及到mask。也就是mask是发生在forward环节的。在这个环节中,Padding Mask和Sequence Mask会有不同的实现方式,具体取决于你使用的框架和库。在实际应用中为了保持计算效率,通常会根据输入数据的形状和需求来动态生成这些mask。

我们模型架构搭建完毕,实例化模型后,就可以输入数据到模型,进行正向传播了。正向传播的底层是要调用Transformer的forward函数的。调用forward函数也是要传入参数的,下面是我自己电脑上的forward参数:

如上图所示,在transformer中和注意力模块有关的数据流是:进入编码器的多头自注意力模块的src、进入解码器的带掩码的多头自注意力模块的tgt、从编码器出来进入解码器中的交叉注意力模块的memory。这三个数据流都有xx_mask、xx_key_padding_mask、xx_is_causal三个参数。其中:

xx_mask参数需要传的值是sequence_mask,也所以从上图右边的解释文档中也可以看到,如果xx_is_causal=True,就使用xx_mask。
xx_key_padding_mask参数需要传入的值是padding_mask。注意:只遮住K部分的O填充样本即可!!!!

pytorch在实现xx_mask的API是attn_mask,实现xx_key_padding_mask的API是key_padding_mask。这些名称的叫法我们可以这样理解:

那具体怎么实现这些mask呢?

说明:pytorch只接收bool矩阵的mask或者浮点数矩阵的mask,所以上面的dec_sequence_mask我们可以把0变成False,-inf变成True,代码就优美一些,但是我现在这样虽不美,但不影响效果。

需要强调的是:在实现mask的过程中,pytorch的源码中是直接置为-float("inf"),但如果我们自己手动计算时,你也设置为-inf就会出现大量的nan,因为softmax计算的时候就溢出了,影响训练,所以在实际中我们一般是设置成一个很小的负数,比如Bert中用的-10000

说明:上图我实现的这些mask都是针对pytorch调包使用的。pytorch底层已经为我们做了大量的工作,所以这里mask的计算流程这里是不可见的,如果我们自己手动计算,这些mask还是有些不一样的,后面讲手动计算数据流时,还会继续展开讲怎么生成我们需要的mask,以及mask在计算过程中是如何使用的

最后再强调一遍细节
疑问1:tgt中还有标识符S,标识符需要mask吗?
答:标识符S是不用mask的,相反它还必须得露出来,我们预测第一个单词时,就是以它为条件的。
疑问2:tgt是分两路进入解码器的,一路进入带掩码的多头自注意力模块,一路可是跳跃连接直达交叉注意力模块的,我们只遮挡了第一个注意力模块的答案,那直达的那一路,不是就泄露答案了吗?
答:这个疑问似乎只有我自己提出来了,我找了好多资料,没人讨论这个问题。。。思来想去,哈哈。。。原来这就不是个问题。此话怎讲?参考上上图的左图atten_mask,那里的上三角mask之所以遮住上三角部位就是为了不想让前面的样本看到后面的样本,而注意力矩阵是干什么的?!注意力矩阵不就是混合样本的嘛!所以上三角遮住后的注意力矩阵和V相乘后的V',V'中的前面样本就不包含后面样本了!而与此同时,残差本身就是各个样本没有融合的矩阵,所以add残差后,也就没有前面样本看到后面样本这一说了!所以也就不存在泄露答案一说了。
疑问3:到底解码器是怎么依次循环预测的?
答:在测试阶段,编码器是依次循环预测的。但是在训练阶段,是一次性就预测出一个sequence了。其实在测试解读也是一次性预测的,只不过我们让它循环了sequence_len次,每次只取对应位置的预测结果罢了。

9、万事俱备,建模-正向传播一次

分步看看结果是否一致:

至此,我们从pytorch的角度,又把Transformer的原理和各个环节的计算流程又梳理了一遍。下面我再手动计算一遍Transformer,看看和pytorch调包的结果是否一样。由于篇幅太长,我再开一个篇章写:【NLP】第九章:Transformer原理、计算流程以及代码实现-3-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宝贝儿好

6元以上者可私信获PDF原文档

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值