Transformer中的编码器和解码器:掩码张量、注意力机制与多头注意力机制详解
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掩码张量介绍
2.2掩码张量的作用
2.3生成掩码张量的代码分
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什么是注意力机制
3.2注意力机制的作用
3.3计算规则以及代码分析
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多头注意里机制的概念
4.2多头注意力机制的结构及作用
💡下面进入整体,解码器的主要结构
5.前馈全连接层
5.1前馈全连接层概念
在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
前馈全连接层的作用:
5.2前馈全连接层代码分析
# 通过类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 残差连接
7.2.3层一归化
层归一化是另一种在Transformer中广泛使用的技术,它通过对每一层网络的输出进行归一化处理,使得输出的分布更加稳定,有利于模型的训练。在Transformer中,每个子层的输出都会经过层归一化处理,以确保其分布在一个合理的范围内。
层归一化的作用主要体现在以下几个方面:
7.3子层连接结构的代码实现
# 使用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小结
作者:pwd`×续缘`