学习目标
🍀 了解编码器中各个组成部分的作用.
🍀 掌握编码器中各个组成部分的实现过程.
🍔 编码器介绍
编码器部分: * 由N个编码器层堆叠而成 * 每个编码器层由两个子层连接结构组成 * 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接 * 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接。
🍔 前馈全连接层
2.1 前馈全连接层
- 在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
- 前馈全连接层的作用:
- 考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
2.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)
- ReLU函数图像:
- 实例化参数:
d_model = 512 # 线性变化的维度 d_ff = 64 dropout = 0.2
- 输入参数:
# 输入参数x可以是多头注意力机制的输出 x = mha_result
tensor([[[-0.3075, 1.5687, -2.5693, ..., -1.1098, 0.0878, -3.3609], [ 3.8065, -2.4538, -0.3708, ..., -1.5205, -1.1488, -1.3984], [ 2.4190, 0.5376, -2.8475, ..., 1.4218, -0.4488, -0.2984], [ 2.9356, 0.3620, -3.8722, ..., -0.7996, 0.1468, 1.0345]], [[ 1.1423, 0.6038, 0.0954, ..., 2.2679, -5.7749, 1.4132], [ 2.4066, -0.2777, 2.8102, ..., 0.1137, -3.9517, -2.9246], [ 5.8201, 1.1534, -1.9191, ..., 0.1410, -7.6110, 1.0046], [ 3.1209, 1.0008, -0.5317, ..., 2.8619, -6.3204, -1.3435]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
- 调用:
ff = PositionwiseFeedForward(d_model, d_ff, dropout) ff_result = ff(x) print(ff_result)
- 输出效果:
tensor([[[-1.9488e+00, -3.4060e-01, -1.1216e+00, ..., 1.8203e-01, -2.6336e+00, 2.0917e-03], [-2.5875e-02, 1.1523e-01, -9.5437e-01, ..., -2.6257e-01, -5.7620e-01, -1.9225e-01], [-8.7508e-01, 1.0092e+00, -1.6515e+00, ..., 3.4446e-02, -1.5933e+00, -3.1760e-01], [-2.7507e-01, 4.7225e-01, -2.0318e-01, ..., 1.0530e+00, -3.7910e-01, -9.7730e-01]], [[-2.2575e+00, -2.0904e+00, 2.9427e+00, ..., 9.6574e-01, -1.9754e+00, 1.2797e+00], [-1.5114e+00, -4.7963e-01, 1.2881e+00, ..., -2.4882e-02, -1.5896e+00, -1.0350e+00], [ 1.7416e-01, -4.0688e-01, 1.9289e+00, ..., -4.9754e-01, -1.6320e+00, -1.5217e+00], [-1.0874e-01, -3.3842e-01, 2.9379e-01, ..., -5.1276e-01, -1.6150e+00, -1.1295e+00]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
2.3 前馈全连接层总结
- 学习了什么是前馈全连接层:
- 在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
- 学习了前馈全连接层的作用:
- 考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
- 学习并实现了前馈全连接层的类: PositionwiseFeedForward
- 它的实例化参数为d_model, d_ff, dropout, 分别代表词嵌入维度, 线性变换维度, 和置零比率.
- 它的输入参数x, 表示上层的输出.
- 它的输出是经过2层线性网络变换的特征表示.
🍔 规范化层
3.1 规范化层的作用
- 它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
3.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
- 实例化参数:
features = d_model = 512 eps = 1e-6
- 输入参数:
# 输入x来自前馈全连接层的输出 x = ff_result tensor([[[-1.9488e+00, -3.4060e-01, -1.1216e+00, ..., 1.8203e-01, -2.6336e+00, 2.0917e-03], [-2.5875e-02, 1.1523e-01, -9.5437e-01, ..., -2.6257e-01, -5.7620e-01, -1.9225e-01], [-8.7508e-01, 1.0092e+00, -1.6515e+00, ..., 3.4446e-02, -1.5933e+00, -3.1760e-01], [-2.7507e-01, 4.7225e-01, -2.0318e-01, ..., 1.0530e+00, -3.7910e-01, -9.7730e-01]], [[-2.2575e+00, -2.0904e+00, 2.9427e+00, ..., 9.6574e-01, -1.9754e+00, 1.2797e+00], [-1.5114e+00, -4.7963e-01, 1.2881e+00, ..., -2.4882e-02, -1.5896e+00, -1.0350e+00], [ 1.7416e-01, -4.0688e-01, 1.9289e+00, ..., -4.9754e-01, -1.6320e+00, -1.5217e+00], [-1.0874e-01, -3.3842e-01, 2.9379e-01, ..., -5.1276e-01, -1.6150e+00, -1.1295e+00]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
- 调用:
ln = LayerNorm(features, eps) ln_result = ln(x) print(ln_result)
- 输出效果:
tensor([[[ 2.2697, 1.3911, -0.4417, ..., 0.9937, 0.6589, -1.1902], [ 1.5876, 0.5182, 0.6220, ..., 0.9836, 0.0338, -1.3393], [ 1.8261, 2.0161, 0.2272, ..., 0.3004, 0.5660, -0.9044], [ 1.5429, 1.3221, -0.2933, ..., 0.0406, 1.0603, 1.4666]], [[ 0.2378, 0.9952, 1.2621, ..., -0.4334, -1.1644, 1.2082], [-1.0209, 0.6435, 0.4235, ..., -0.3448, -1.0560, 1.2347], [-0.8158, 0.7118, 0.4110, ..., 0.0990, -1.4833, 1.9434], [ 0.9857, 2.3924, 0.3819, ..., 0.0157, -1.6300, 1.2251]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
3.3 规范化层总结
- 学习了规范化层的作用:
- 它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
- 学习并实现了规范化层的类: LayerNorm
- 它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小的数.
- 它的输入参数x代表来自上一层的输出.
- 它的输出就是经过规范化的特征表示.
🍔 子层连接结构
4.1 子层连接结构:
- 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
- 子层连接结构图:
4.2 子层连接结构的代码分析
# 使用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)))
- 实例化参数
size = 512 dropout = 0.2 head = 8 d_model = 512
- 输入参数:
# 令x为位置编码器的输出 x = pe_result mask = Variable(torch.zeros(8, 4, 4)) # 假设子层中装的是多头注意力层, 实例化这个类 self_attn = MultiHeadedAttention(head, d_model) # 使用lambda获得一个函数类型的子层 sublayer = lambda x: self_attn(x, x, x, mask)
- 调用:
sc = SublayerConnection(size, dropout) sc_result = sc(x, sublayer) print(sc_result) print(sc_result.shape)
- 输出效果:
tensor([[[ 14.8830, 22.4106, -31.4739, ..., 21.0882, -10.0338, -0.2588], [-25.1435, 2.9246, -16.1235, ..., 10.5069, -7.1007, -3.7396], [ 0.1374, 32.6438, 12.3680, ..., -12.0251, -40.5829, 2.2297], [-13.3123, 55.4689, 9.5420, ..., -12.6622, 23.4496, 21.1531]], [[ 13.3533, 17.5674, -13.3354, ..., 29.1366, -6.4898, 35.8614], [-35.2286, 18.7378, -31.4337, ..., 11.1726, 20.6372, 29.8689], [-30.7627, 0.0000, -57.0587, ..., 15.0724, -10.7196, -18.6290], [ -2.7757, -19.6408, 0.0000, ..., 12.7660, 21.6843, -35.4784]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
4.3 子层连接结构总结
- 什么是子层连接结构:
- 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构), 在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
- 学习并实现了子层连接结构的类: SublayerConnection
- 类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率.
- 它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示.
- 它的输出就是通过子层连接结构处理的输出.
🍔 编码器层
5.1 编码器层的作用
- 作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.
- 编码器层的构成图:
5.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])
5.3 编码器层总结
- 学习了编码器层的作用:
- 作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.
- 学习并实现了编码器层的类: EncoderLayer
- 类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率dropout.
- 实例化对象的输入参数有2个,x代表来自上一层的输出, mask代表掩码张量.
- 它的输出代表经过整个编码层的特征表示.
🍔 编码器
6.1 编码器的作用
- 编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
- 编码器的结构图:
6.2 编码器的代码分析
# 使用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)
- 实例化参数:
# 第一个实例化参数layer, 它是一个编码器层的实例化对象, 因此需要传入编码器层的参数 # 又因为编码器层中的子层是不共享的, 因此需要使用深度拷贝各个对象. size = 512 head = 8 d_model = 512 d_ff = 64 c = copy.deepcopy attn = MultiHeadedAttention(head, d_model) ff = PositionwiseFeedForward(d_model, d_ff, dropout) dropout = 0.2 layer = EncoderLayer(size, c(attn), c(ff), dropout) # 编码器中编码器层的个数N N = 8 mask = Variable(torch.zeros(8, 4, 4))
- 调用:
en = Encoder(layer, N) en_result = en(x, mask) print(en_result) print(en_result.shape)
- 输出效果:
tensor([[[-0.2081, -0.3586, -0.2353, ..., 2.5646, -0.2851, 0.0238], [ 0.7957, -0.5481, 1.2443, ..., 0.7927, 0.6404, -0.0484], [-0.1212, 0.4320, -0.5644, ..., 1.3287, -0.0935, -0.6861], [-0.3937, -0.6150, 2.2394, ..., -1.5354, 0.7981, 1.7907]], [[-2.3005, 0.3757, 1.0360, ..., 1.4019, 0.6493, -0.1467], [ 0.5653, 0.1569, 0.4075, ..., -0.3205, 1.4774, -0.5856], [-1.0555, 0.0061, -1.8165, ..., -0.4339, -1.8780, 0.2467], [-2.1617, -1.5532, -1.4330, ..., -0.9433, -0.5304, -1.7022]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
6.3 编码器总结
- 学习了编码器的作用:
- 编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
- 学习并实现了编码器的类: Encoder
- 类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数.
- forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量.
- 编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分.