Transformer--编码器和解码器(包含掩码张量,注意力机制,多头注意力机制)

pwd`×续缘` 2024-09-01 12:31:01 阅读 64

1.编码器介绍

 2.掩码张量

2.1掩码张量介绍

2.2掩码张量的作用

2.3生成掩码张量的代码分

2.4掩码张量可视化

3.注意力机制

3.1什么是注意力机制 

3.2注意力机制的作用

3.3计算规则以及代码分析

4.多头注意力机制 (了解)

4.1多头注意里机制的概念

 4.2多头注意力机制的结构及作用

5.前馈全连接层

5.1前馈全连接层概念 

5.2前馈全连接层代码分析 

 6.规范化层

6.1规范化层的作用

6.2规范化层的代码实现

7.子层连接结构

7.1子层连接结构图

7.2子层连接结构的作用

7.2.1 残差连接

7.2.3层一归化

7.3子层连接结构的代码实现

8.编码器层 

8.1编码器层的作用

8.2编码器层的代码实现 

9.编码器

9.1代码分析: 

9.2小结


1.编码器介绍

编码器部分: 由N个编码器层堆叠而成,每个编码器层由两个子层连接结构组成,第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接,第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

💡在讲述编码器的结构之前,我们先引入三个概念--掩码张量,注意力机制,多头注意力机制


 2.掩码张量

2.1掩码张量介绍

掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量.

2.2掩码张量的作用

防止未来的信息被利用,我们进行遮掩,具体为什么,我们会在下一小节注意力机制中以及解码器中解释

2.3生成掩码张量的代码分

<code>def subsequent_mask(size):

"""生成向后遮掩的掩码张量, 参数size是掩码张量最后两个维度的大小, 它的最后两维形成一个方阵"""

# 在函数中, 首先定义掩码张量的形状

attn_shape = (1, size, size)

# 然后使用np.ones方法向这个形状中添加1元素,形成上三角阵, 最后为了节约空间,

# 再使其中的数据类型变为无符号8位整形unit8

subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')

# 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作,

# 在这个其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减,

# 如果是0, subsequent_mask中的该位置由0变成1

# 如果是1, subsequent_mask中的该位置由1变成0

return torch.from_numpy(1 - subsequent_mask)

2.4掩码张量可视化

plt.figure(figsize=(5,5))

plt.imshow(subsequent_mask(20)[0])

效果分析:通过观察可视化方阵, 黄色是1的部分, 这里代表被遮掩, 紫色代表没有被遮掩的信息, 横坐标代表目标词汇的位置, 纵坐标代表可查看的位置;我们看到, 在0的位置我们一看望过去都是黄色的, 都被遮住了,1的位置一眼望过去还是黄色, 说明第一次词还没有产生, 从第二个位置看过去, 就能看到位置1的词, 其他位置看不到, 以此类推. 


3.注意力机制

3.1什么是注意力机制 

注意力机制是注意力计算规则能够应用的深度学习网络的载体, 同时包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制.说明: NLP领域中, 当前的注意力机制大多数应用于seq2seq架构, 即编码器和解码器模型.

3.2注意力机制的作用

在解码器端的注意力机制: 能够根据模型目标有效的聚焦编码器的输出结果, 当其作为解码器的输入时提升效果. 改善以往编码器输出是单一定长张量, 无法存储过多信息的情况.在编码器端的注意力机制: 主要解决表征问题, 相当于特征提取过程, 得到输入的注意力表示. 一般使用自注意力(self-attention).

3.3计算规则以及代码分析

<code>import torch.nn.functional as F

def attention(query, key, value, mask=None, dropout=None):

"""注意力机制的实现, 输入分别是query, key, value, mask: 掩码张量,

dropout是nn.Dropout层的实例化对象, 默认为None"""

# 在函数中, 首先取query的最后一维的大小, 一般情况下就等同于我们的词嵌入维度, 命名为d_k

d_k = query.size(-1)

# 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.

# 得到注意力得分张量scores

scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

# 接着判断是否使用掩码张量

if mask is not None:

# 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0

# 则对应的scores张量用-1e9这个值来替换, 如下演示

scores = scores.masked_fill(mask == 0, -1e9)

# 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度.

# 这样获得最终的注意力张量

p_attn = F.softmax(scores, dim = -1)

# 之后判断是否使用dropout进行随机置0

if dropout is not None:

# 将p_attn传入dropout对象中进行'丢弃'处理

p_attn = dropout(p_attn)

# 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力张量

return torch.matmul(p_attn, value), p_attn

学习并实现了注意力计算规则的函数: attention

它的输入就是Q,K,V以及mask和dropout, mask用于掩码, dropout用于随机置0.它的输出有两个, query的注意力表示以及注意力张量.


4.多头注意力机制 (了解)

4.1多头注意里机制的概念

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.

 4.2多头注意力机制的结构及作用

这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.


💡下面进入整体,解码器的主要结构 

5.前馈全连接层

5.1前馈全连接层概念 

在Transformer中前馈全连接层就是具有两层线性层的全连接网络.

前馈全连接层的作用:

考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.

5.2前馈全连接层代码分析 

<code># 通过类PositionwiseFeedForward来实现前馈全连接层

class PositionwiseFeedForward(nn.Module):

def __init__(self, d_model, d_ff, dropout=0.1):

"""初始化函数有三个输入参数分别是d_model, d_ff,和dropout=0.1,第一个是线性层的输入维度也是第二个线性层的输出维度,

因为我们希望输入通过前馈全连接层后输入和输出的维度不变. 第二个参数d_ff就是第二个线性层的输入维度和第一个线性层的输出维度.

最后一个是dropout置0比率."""

super(PositionwiseFeedForward, self).__init__()

# 首先按照我们预期使用nn实例化了两个线性层对象,self.w1和self.w2

# 它们的参数分别是d_model, d_ff和d_ff, d_model

self.w1 = nn.Linear(d_model, d_ff)

self.w2 = nn.Linear(d_ff, d_model)

# 然后使用nn的Dropout实例化了对象self.dropout

self.dropout = nn.Dropout(dropout)

def forward(self, x):

"""输入参数为x,代表来自上一层的输出"""

# 首先经过第一个线性层,然后使用Funtional中relu函数进行激活,

# 之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果.

return self.w2(self.dropout(F.relu(self.w1(x))))

ReLU函数公式: ReLU(x)=max(0, x)


 6.规范化层

6.1规范化层的作用

它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.

6.2规范化层的代码实现

# 通过LayerNorm实现规范化层的类

class LayerNorm(nn.Module):

def __init__(self, features, eps=1e-6):

"""初始化函数有两个参数, 一个是features, 表示词嵌入的维度,

另一个是eps它是一个足够小的数, 在规范化公式的分母中出现,

防止分母为0.默认是1e-6."""

super(LayerNorm, self).__init__()

# 根据features的形状初始化两个参数张量a2,和b2,第一个初始化为1张量,

# 也就是里面的元素都是1,第二个初始化为0张量,也就是里面的元素都是0,这两个张量就是规范化层的参数,

# 因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有参数作为调节因子,

# 使其即能满足规范化要求,又能不改变针对目标的表征.最后使用nn.parameter封装,代表他们是模型的参数。

self.a2 = nn.Parameter(torch.ones(features))

self.b2 = nn.Parameter(torch.zeros(features))

# 把eps传到类中

self.eps = eps

def forward(self, x):

"""输入参数x代表来自上一层的输出"""

# 在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.

# 接着再求最后一个维度的标准差,然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果,

# 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,加上位移参数b2.返回即可.

mean = x.mean(-1, keepdim=True)

std = x.std(-1, keepdim=True)

return self.a2 * (x - mean) / (std + self.eps) + self.b2


7.子层连接结构

7.1子层连接结构图

如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构 

7.2子层连接结构的作用

7.2.1 残差连接

残差连接是Transformer中非常关键的一种连接方式,它允许模型在传递信息时能够“跳过”某些层,直接将输入信息传递给后面的层。这种机制有助于缓解深度神经网络在训练过程中可能出现的梯度消失或梯度爆炸问题,使得模型能够训练得更深、更稳定。在Transformer中,每个编码器层和解码器层都包含多个子层(如自注意力层、前馈全连接层等),这些子层之间通过残差连接相互连接。具体来说,就是每个子层的输出都会与输入相加,然后再经过层归一化处理,作为下一个子层的输入。这样做的好处是,即使网络层数很深,梯度信息也能有效地传递到前面的层,从而提高模型的训练效率和稳定性。

7.2.3层一归化

层归一化是另一种在Transformer中广泛使用的技术,它通过对每一层网络的输出进行归一化处理,使得输出的分布更加稳定,有利于模型的训练。在Transformer中,每个子层的输出都会经过层归一化处理,以确保其分布在一个合理的范围内。

层归一化的作用主要体现在以下几个方面:

加速收敛:通过归一化处理,可以使得模型在训练过程中更快地收敛到最优解。提高稳定性:归一化处理有助于缓解模型在训练过程中的过拟合现象,提高模型的泛化能力。简化调参:归一化处理使得模型对参数的初始值不那么敏感,从而简化了调参过程。

7.3子层连接结构的代码实现

<code># 使用SublayerConnection来实现子层连接结构的类

class SublayerConnection(nn.Module):

def __init__(self, size, dropout=0.1):

"""它输入参数有两个, size以及dropout, size一般是都是词嵌入维度的大小,

dropout本身是对模型结构中的节点数进行随机抑制的比率,

又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随机置0的比率.

"""

super(SublayerConnection, self).__init__()

# 实例化了规范化对象self.norm

self.norm = LayerNorm(size)

# 又使用nn中预定义的droupout实例化一个self.dropout对象.

self.dropout = nn.Dropout(p=dropout)

def forward(self, x, sublayer):

"""前向逻辑函数中, 接收上一个层或者子层的输入作为第一个参数,

将该子层连接中的子层函数作为第二个参数"""

# 我们首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作,

# 随机停止一些网络中神经元的作用,来防止过拟合. 最后还有一个add操作,

# 因为存在跳跃连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出.

return x + self.dropout(sublayer(self.norm(x)))

💡 讲这么多概念和结构,我们最后把他们结合起来


8.编码器层 

8.1编码器层的作用

作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.

8.2编码器层的代码实现 

🏷️🏷️还不熟悉每一层参数可以返回上面对照一下 

# 使用EncoderLayer类实现编码器层

class EncoderLayer(nn.Module):

def __init__(self, size, self_attn, feed_forward, dropout):

"""它的初始化函数参数有四个,分别是size,其实就是我们词嵌入维度的大小,它也将作为我们编码器层的大小,

第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制,

第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象, 最后一个是置0比率dropout."""

super(EncoderLayer, self).__init__()

# 首先将self_attn和feed_forward传入其中.

self.self_attn = self_attn

self.feed_forward = feed_forward

# 如图所示, 编码器层中有两个子层连接结构, 所以使用clones函数进行克隆

self.sublayer = clones(SublayerConnection(size, dropout), 2)

# 把size传入其中

self.size = size

def forward(self, x, mask):

"""forward函数中有两个输入参数,x和mask,分别代表上一层的输出,和掩码张量mask."""

# 里面就是按照结构图左侧的流程. 首先通过第一个子层连接结构,其中包含多头自注意力子层,

# 然后通过第二个子层连接结构,其中包含前馈全连接子层. 最后返回结果.

x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

return self.sublayer[1](x, self.feed_forward)

实例化参数 

size = 512

head = 8

d_model = 512

d_ff = 64

x = pe_result

dropout = 0.2

self_attn = MultiHeadedAttention(head, d_model)

ff = PositionwiseFeedForward(d_model, d_ff, dropout)

mask = Variable(torch.zeros(8, 4, 4))

调用 

el = EncoderLayer(size, self_attn, ff, dropout)

el_result = el(x, mask)

print(el_result)

print(el_result.shape)

输出效果 

tensor([[[ 33.6988, -30.7224, 20.9575, ..., 5.2968, -48.5658, 20.0734],

[-18.1999, 34.2358, 40.3094, ..., 10.1102, 58.3381, 58.4962],

[ 32.1243, 16.7921, -6.8024, ..., 23.0022, -18.1463, -17.1263],

[ -9.3475, -3.3605, -55.3494, ..., 43.6333, -0.1900, 0.1625]],

[[ 32.8937, -46.2808, 8.5047, ..., 29.1837, 22.5962, -14.4349],

[ 21.3379, 20.0657, -31.7256, ..., -13.4079, -44.0706, -9.9504],

[ 19.7478, -1.0848, 11.8884, ..., -9.5794, 0.0675, -4.7123],

[ -6.8023, -16.1176, 20.9476, ..., -6.5469, 34.8391, -14.9798]]],

grad_fn=<AddBackward0>)

torch.Size([2, 4, 512])

🏷️🏷️文章开头我们也介绍了编码器就是N个编码器层堆叠而成


9.编码器

9.1代码分析: 

# 使用Encoder类来实现编码器

class Encoder(nn.Module):

def __init__(self, layer, N):

"""初始化函数的两个参数分别代表编码器层和编码器层的个数"""

super(Encoder, self).__init__()

# 首先使用clones函数克隆N个编码器层放在self.layers中

self.layers = clones(layer, N)

# 再初始化一个规范化层, 它将用在编码器的最后面.

self.norm = LayerNorm(layer.size)

def forward(self, x, mask):

"""forward函数的输入和编码器层相同, x代表上一层的输出, mask代表掩码张量"""

# 首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x,

# 这个循环的过程,就相当于输出的x经过了N个编码器层的处理.

# 最后再通过规范化层的对象self.norm进行处理,最后返回结果.

for layer in self.layers:

x = layer(x, mask)

return self.norm(x)

9.2小结

类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数.forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量.编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分.

 

 

 

 

 

 

 

 

 

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。