跳转至

4 编码器部分实现

学习目标

  • 了解编码器中各个组成部分的作用
  • 掌握编码器中各个组成部分的实现过程

4.1 编码器介绍

由N个编码器层堆叠而成,每个编码器层由两个子层连接结构组成。

第一个子层连接结构包括一个多头自注意力子层和一个残差连接以及规范化层(层归一化)

第二个子层连接结构包括一个前馈全连接子层和一个残差连接以及规范化层(层归一化)

4.2 掩码张量

4.2.1 掩码张量介绍

掩码张量(Mask Tensor)是一种用于控制模型对输入数据的访问或处理方式的工具。它在深度学习中广泛应用,尤其是在处理变长序列(如自然语言处理中的句子)或需要忽略某些数据时。掩码张量通常是一个二进制张量(值为0或1)或下三角矩阵,用于指示哪些位置是有效的,哪些位置是无效的(需要被忽略)。

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

4.2.2 掩码张量类型

Transformer 中常用的掩码张量主要分为以下两种类型:

  • Padding Mask(填充掩码)

    • 作用:在处理变长序列时,输入序列通常会被填充到相同的长度(例如,使用 标记)。Padding Mask 用于屏蔽这些填充位置,确保注意力机制不会将注意力分配到这些无效位置。
    • 场景:适用于编码器(Encoder)和解码器(Decoder)的输入序列。
    • 实现:
      • Padding Mask 是一个二进制张量,形状通常为 (batch_size, seq_len) 或 (batch_size, 1, 1, seq_len)(适配多头注意力机制)。
      • 对于填充位置,掩码值为 False 或负无穷大(-inf);对于有效位置,掩码值为 True 或 1。
      • 在注意力计算中,掩码会与注意力权重(scores)相加,填充位置的权重会被设置为负无穷大,softmax 后这些位置的注意力权重接近 0。
  • Look-Ahead Mask(前向掩码/因果掩码)

    • 作用:在解码器(Decoder)中,生成序列时,模型在生成第 \(t​​\)$ 个词时只能依赖前 ​$\(t-1​\) 个词。前向掩码用于屏蔽未来的词,防止模型“作弊”看到后续信息。
    • 场景:主要用于解码器的自注意力层(Masked Multi-Head Attention)。
    • 实现
      • Look-Ahead Mask 是一个下三角矩阵,形状为 (seq_len, seq_len) 或 (1, 1, seq_len, seq_len)(适配多头注意力)。
      • 对角线及以下位置为 True 或 1(允许关注),对角线以上位置为 False 或负无穷大(屏蔽)。

4.2.3 掩码张量作用

  • 忽略填充部分:在处理变长序列时,通常会用填充(padding)将序列补齐到相同长度。掩码可以告诉模型忽略这些填充部分。
  • 防止未来信息泄露:在解码器中,掩码可以防止模型在生成当前词时访问未来的信息。
  • 选择性处理:在注意力机制中,掩码可以控制哪些位置可以参与计算。

在transformer中, 掩码张量主要应用在attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算时有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此未来的信息可能被提前利用,所以我们会进行遮掩。关于解码器的有关知识将在后面的章节中讲解。

4.2.4 掩码张量在Transformer中的具体使用

Transformer 模型包含编码器(Encoder)和解码器(Decoder),掩码张量在不同部分的用途如下:

  • 编码器(Encoder)
    • 主要使用 Padding Mask:
      • 编码器处理输入序列(如源语言句子),需要屏蔽填充位置。
      • 例如,在机器翻译任务中,输入句子可能有不同的长度,Padding Mask 确保模型只关注有效词。
  • 解码器(Decoder)

    • 自注意力层(Masked Multi-Head Attention):
      • 解码器的第一个自注意力层使用 Look-Ahead MaskPadding Mask 的组合。
      • Look-Ahead Mask 确保生成第 \(t\) 个词时只关注前 \(t-1\) 个词。
      • Padding Mask 屏蔽填充位置(如果目标序列被填充)。
    • 编码器-解码器注意力层:
      • 解码器的第二个注意力层(Encoder-Decoder Attention)使用 Padding Mask,屏蔽编码器输入中的填充位置。
      • 这里不需要 Look-Ahead Mask,因为该层关注的是编码器的输出,而不是解码器的未来信息。
  • 训练 vs 推理

    • 训练阶段:
      • 训练时,输入和输出序列都是已知的,因此 Padding Mask 和 Look-Ahead Mask 都可以提前计算。
      • 解码器使用完整的 Look-Ahead Mask 来模拟自回归生成。
    • 推理阶段:
      • 推理时,解码器是自回归的(逐词生成),Look-Ahead Mask 仍然用于确保只关注之前的词。
      • Padding Mask 可能只在编码器输入或部分解码器输入中使用(如果有填充)。

4.2.5 生成掩码张量代码实现

下三角掩码 (Causal Mask): 用于遮盖未来位置,确保自回归模型的正确性。

Python
import torch
import matplotlib.pyplot as plt


# 下三角:全1三角在下, 0代表掩码
# 下三角矩阵作用: 生成字符时,希望模型不要使用当前字符后面的字符。
# 使用遮掩mask,防止未来的信息可能被提前利用
# 实现方法:下三角矩阵
# 函数 subsequent_mask 实现分析
# 产生上三角矩阵 torch.triu(torch.ones((size, size))).type(torch.uint8)
# 产生下三角矩阵 torch.tril(torch.ones((size, size))).type(torch.uint8)
def subsequent_mask(size):
    # 产生下三角矩阵 产生一个方阵
    subsequent_mask = torch.tril(torch.ones((size, size))).type(torch.uint8)
    return subsequent_mask


if __name__ == '__main__':
    # 产生5*5的下三角矩阵
    size = 5
    mask = subsequent_mask(size)
    print('下三角矩阵--->\n', mask)

    # 掩码张量可视化
    plt.figure(figsize=(5, 5))
    plt.imshow(subsequent_mask(20))
    plt.show()

    # 因果掩码操作
    scores = torch.randn(5, 5)
    scores = scores.masked_fill(mask == 0, float('-inf'))
    print('scores--->', scores)

输出结果:

Python
下三角矩阵--->
 tensor([[1, 0, 0, 0, 0],
        [1, 1, 0, 0, 0],
        [1, 1, 1, 0, 0],
        [1, 1, 1, 1, 0],
        [1, 1, 1, 1, 1]], dtype=torch.uint8)
scores---> tensor([[-1.2274,    -inf,    -inf,    -inf,    -inf],
        [ 1.5238, -0.5331,    -inf,    -inf,    -inf],
        [ 0.1578,  0.0280,  0.4225,    -inf,    -inf],
        [-1.0662, -0.5465,  0.1994,  0.5301,    -inf],
        [-0.5187, -1.4820,  0.3464, -0.4260,  1.3643]])

效果分析:

  • 通过观察可视化方阵, 紫色是0的部分, 这里代表被遮掩, 黄色代表没有被遮掩的信息, 横坐标代表目标词汇的位置, 纵坐标代表可查看的位置。
  • 我们看到,在纵坐标0的位置我们看到第1个为黄色,剩下的都为紫色;纵坐标1的位置我们看到前2个为黄色,剩下的都为紫色,以此类推。

4.2.6 小结

  • 什么是掩码张量:
    • 掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩, 也可以说被替换, 它的表现形式是一个张量。
  • 掩码张量类型:
    • 填充掩码: 屏蔽无效的填充位置,适用于编码器和解码器。
    • 因果掩码: 屏蔽未来位置,仅用于解码器的自注意力层。
  • 掩码张量的作用:
    • 在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
  • 实现了生成向后遮掩的掩码张量函数: subsequent_mask
    • 它的输入是size, 代表掩码张量的大小.
    • 它的输出是一个二维形成1方阵的下三角阵.
    • 最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途.

4.3 注意力机制

Transformer编码器端的核心是自注意力机制 (Self-Attention),也称为缩放点积注意力 (Scaled Dot-Product Attention)。它允许模型在处理输入序列时,关注序列中不同位置的信息,从而捕捉长距离依赖关系。相比于循环神经网络 (RNN),自注意力机制可以并行计算,效率更高,并且更容易学习长距离依赖。

我们这里使用的注意力计算规则:

1737856664337

4.3.1 注意力计算规则的代码实现

Python
import torch
import torch.nn as nn
import math


# 自注意力机制函数attention 实现思路分析
# attention(query, key, value, mask=None, dropout=None)
# 1 求查询张量特征尺寸大小 d_k
# 2 求查询张量q的权重分布socres    q@k^T /math.sqrt(d_k)
# 形状[2,4,512] @ [2,512,4] --->[2,4,4]
# 3 是否对权重分布scores进行 scores.masked_fill(mask == 0, -1e9)
# 4 求查询张量q的权重分布 p_attn F.softmax()
# 5 是否对p_attn进行dropout if dropout is not None:
# 6 求查询张量q的注意力结果表示 [2,4,4]@[2,4,512] --->[2,4,512]
# 7 返回q的注意力结果表示 q的权重分布

def attention(query, key, value, mask=None, dropout=None):
    # query, key, value:代表注意力的三个输入张量
    # mask:代表掩码张量
    # dropout:传入的dropout实例化对象

    # 1 求查询张量特征尺寸大小
    d_k = query.size()[-1]

    # 2 求查询张量q的权重分布socres    q@k^T /math.sqrt(d_k)
    # [2,4,512] @ [2,512,4] --->[2,4,4]
    # 注意: key.transpose(-2, -1), 使用-2和-1, 后续多头注意力中 query和key变成[2,8,4,64]
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

    # 3 是否对权重分布scores 进行 masked_fill
    if mask is not None:
        # 根据mask矩阵0的位置 对scores矩阵对应位置进行掩码
        # mask:一个布尔张量(True或False)
        # value:在自然语言处理中,将填充部分的注意力分数设置为一个极小的值(如-1e9),使其在Softmax 后接近0。
        scores = scores.masked_fill(mask=(mask==0), value=-1e9)
    # print('scores--->', scores)
    # 4 求查询张量q的权重分布 softmax
    p_attn = torch.softmax(scores, dim=-1)

    # 5 是否对p_attn进行dropout
    if dropout is not None:
        p_attn = dropout(p_attn)

    # 返回 查询张量q的注意力结果表示 bmm-matmul运算, 注意力查询张量q的权重分布p_attn
    # [2,4,4]*[2,4,512] --->[2,4,512]
    return torch.matmul(p_attn, value), p_attn

调用注意力函数:

Python
def dm_test_attention():
    vocab = 1000    # 词表大小是1000
    d_model = 512    # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1    # 置0概率为0.1
    max_len = 60    # 句子最大长度

    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embedded_result)

    query = key = value = pe_result  # torch.Size([2, 4, 512])
    print('编码阶段 对注意力权重分布 不做掩码')

    attn1, p_attn1 = attention(query, key, value)
    print('注意力权重 p_attn1--->', p_attn1.shape, '\n', p_attn1)    # torch.Size([2, 4, 4])
    print('注意力表示结果 attn1--->', attn1.shape, '\n', attn1)    # torch.Size([2, 4, 512])

    # print('*' * 50)
    # print('编码阶段 对注意力权重分布 做掩码')
    # # 填充掩码
    # # (x != 0).type(torch.uint8): 值为0的位置为0, 不为0的位置为1
    # # unsqueeze(1) -> (2,1,4), 后续masked_fill操作时在1轴上进行广播变成(2,4,4)和scores对齐
    # mask = (x != 0).type(torch.uint8).unsqueeze(1)
    # attn2, p_attn2 = attention(query, key, value, mask=mask)
    # print("注意力权重 p_attn2--->", p_attn2.shape, '\n', p_attn2)
    # print("注意力表示结果 attn2--->", attn2.shape, '\n', attn2)


if __name__ == '__main__':
    dm_test_attention()

对注意力权重分布不做掩码:

Python
编码阶段 对注意力权重分布 不做掩码
scores---> tensor([[[12298.7109,     652.4590,    -202.9256,    -651.9707],
                 [    652.4590, 11923.3975,    -672.6254,    -483.3305],
                 [ -202.9256,    -672.6254, 11569.7334,     113.4300],
                 [ -651.9707,    -483.3305,     113.4300, 13035.6406]],

                [[12800.8125,     368.1026,     521.0147,    -473.5522],
                 [    368.1026, 14176.6309,    -525.0483,     398.1827],
                 [    521.0147,    -525.0483, 13696.3105,        16.0584],
                 [ -473.5522,     398.1827,        16.0584, 12098.5850]]],
             grad_fn=<DivBackward0>)
注意力权重 p_attn1---> torch.Size([2, 4, 4]) 
 tensor([[[1., 0., 0., 0.],
                 [0., 1., 0., 0.],
                 [0., 0., 1., 0.],
                 [0., 0., 0., 1.]],

                [[1., 0., 0., 0.],
                 [0., 1., 0., 0.],
                 [0., 0., 1., 0.],
                 [0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>)
注意力表示结果 attn1---> torch.Size([2, 4, 512]) 
 tensor([[[ 50.0682,    47.8566,    -8.7233,    ...,    19.0476,     0.0000,     4.8790],
                 [    0.0000,    -7.3437, -29.3865,    ..., -11.7984, -24.2216,     0.0000],
                 [ 10.8591, -23.4229, -21.1743,    ..., -22.7963, -16.1870,    36.2169],
                 [-22.7074,    21.0137,     7.3979,    ..., -21.1029, -58.0060,     2.6377]],

                [[    0.0000,    14.3677,    21.8205,    ...,    -5.0804,    -0.5723,     1.5113],
                 [ 14.4683,     0.0000,    -4.6780,    ...,    28.5337,     5.6747,    33.7763],
                 [-17.6688,    30.0477,    24.0871,    ...,    28.0825,    22.2496, -27.7949],
                 [ -3.6961,    -5.8306, -18.9645,    ..., -23.1688,     5.0303,    12.7125]]],
             grad_fn=<UnsafeViewBackward0>)
**************************************************
编码阶段 对注意力权重分布 做掩码
scores---> tensor([[[ 1.2299e+04, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [ 6.5246e+02,    1.1923e+04, -1.0000e+09, -1.0000e+09],
                 [-2.0293e+02, -6.7263e+02,    1.1570e+04, -1.0000e+09],
                 [-6.5197e+02, -4.8333e+02,    1.1343e+02,    1.3036e+04]],

                [[ 1.2801e+04, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [ 3.6810e+02,    1.4177e+04, -1.0000e+09, -1.0000e+09],
                 [ 5.2101e+02, -5.2505e+02,    1.3696e+04, -1.0000e+09],
                 [-4.7355e+02,    3.9818e+02,    1.6058e+01,    1.2099e+04]]],
             grad_fn=<MaskedFillBackward0>)
注意力权重 p_attn2---> torch.Size([2, 4, 4]) 
 tensor([[[1., 0., 0., 0.],
                 [0., 1., 0., 0.],
                 [0., 0., 1., 0.],
                 [0., 0., 0., 1.]],

                [[1., 0., 0., 0.],
                 [0., 1., 0., 0.],
                 [0., 0., 1., 0.],
                 [0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>)
注意力表示结果 attn2---> torch.Size([2, 4, 512]) 
 tensor([[[ 50.0682,    47.8566,    -8.7233,    ...,    19.0476,     0.0000,     4.8790],
                 [    0.0000,    -7.3437, -29.3865,    ..., -11.7984, -24.2216,     0.0000],
                 [ 10.8591, -23.4229, -21.1743,    ..., -22.7963, -16.1870,    36.2169],
                 [-22.7074,    21.0137,     7.3979,    ..., -21.1029, -58.0060,     2.6377]],

                [[    0.0000,    14.3677,    21.8205,    ...,    -5.0804,    -0.5723,     1.5113],
                 [ 14.4683,     0.0000,    -4.6780,    ...,    28.5337,     5.6747,    33.7763],
                 [-17.6688,    30.0477,    24.0871,    ...,    28.0825,    22.2496, -27.7949],
                 [ -3.6961,    -5.8306, -18.9645,    ..., -23.1688,     5.0303,    12.7125]]],
             grad_fn=<UnsafeViewBackward0>)

4.3.2 带有mask的输入参数

Python
def dm_test_attention():
    vocab = 1000    # 词表大小是1000
    d_model = 512    # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])
    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1    # 置0概率为0.1
    max_len = 60    # 句子最大长度

    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embedded_result)

    query = key = value = pe_result    # torch.Size([2, 4, 512])
    print('编码阶段 对注意力权重分布 做掩码')
    mask = (x != 0).type(torch.uint8).unsqueeze(1)
    attn2, p_attn2 = attention(query, key, value, mask=mask)
    print("注意力权重 p_attn2--->", p_attn2.shape, '\n', p_attn2)
    print("注意力表示结果 attn2--->", attn2.shape, '\n', attn2)


if __name__ == '__main__':
    dm_test_attention()

带有mask的输出效果:

Python
编码阶段 对注意力权重分布 做掩码
scores---> tensor([[[-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09]],

                [[-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
                 [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09]]],
             grad_fn=<MaskedFillBackward0>)
注意力权重 p_attn2---> torch.Size([2, 4, 4]) 
 tensor([[[0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500]],

                [[0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500],
                 [0.2500, 0.2500, 0.2500, 0.2500]]], grad_fn=<SoftmaxBackward0>)
注意力表示结果 attn2---> torch.Size([2, 4, 512]) 
 tensor([[[-19.8931, -22.4384,     4.8441,    ...,     2.8929,    10.8286,    -2.8991],
                 [-19.8931, -22.4384,     4.8441,    ...,     2.8929,    10.8286,    -2.8991],
                 [-19.8931, -22.4384,     4.8441,    ...,     2.8929,    10.8286,    -2.8991],
                 [-19.8931, -22.4384,     4.8441,    ...,     2.8929,    10.8286,    -2.8991]],

                [[ -0.8791,    19.8708,     8.6883,    ...,    -5.6779,    -1.3241,    22.1666],
                 [ -0.8791,    19.8708,     8.6883,    ...,    -5.6779,    -1.3241,    22.1666],
                 [ -0.8791,    19.8708,     8.6883,    ...,    -5.6779,    -1.3241,    22.1666],
                 [ -0.8791,    19.8708,     8.6883,    ...,    -5.6779,    -1.3241,    22.1666]]],
             grad_fn=<UnsafeViewBackward0>)

4.3.3 小结

  • 学习并实现了注意力计算规则的函数: attention
    • 它的输入就是Q,K,V以及mask和dropout, mask用于掩码, dropout用于随机置0。
    • 它的输出有两个, query的注意力表示以及注意力张量。

4.4 多头注意力机制

4.4.1 概念

多头注意力机制(Multi-Head Attention)是Transformer模型的核心组件之一,用于捕捉输入序列中不同位置之间的依赖关系。它通过并行计算多个注意力头(Attention Heads),从不同的子空间中提取信息,从而增强模型的表达能力。

多头注意力机制的关键在于同时使用多个注意力头,每个头通过独立的线性变换(学习不同的Q、K、V矩阵)来学习输入数据在不同子空间中的表示,每个注意力头学习不同的子空间(不同的“视角”),从而可以在不同的子空间中获取输入序列之间的不同关系。在所有注意力头计算完成后,它们的结果会被合并,最后通过一个线性变换映射到输出空间,以此产生更丰富的表示。

为什么多头能学习不同子空间的信息?

  • 独立的参数
    每个头有自己独立的Q、K、V矩阵,这些矩阵是通过训练学习得到的。因此,不同的头可以关注输入数据的不同方面。

  • 不同的注意力分布
    由于每个头的参数不同,它们计算的注意力权重也会不同。例如,一个头可能关注局部的特征,另一个头可能关注全局的特征。

  • 特征多样性

    通过多个头的组合,模型能够捕捉到输入数据在不同子空间中的多样化表示,从而增强模型的表达能力。

在多头自注意力机制中,输入句子中的每个词的词嵌入向量会被分割成多个头 (head)。 假设有h个头,那么每个头获得的向量维度就是 d_model / h。这种分割只发生在词嵌入向量的最后一维 (即d_model维度) 上。词嵌入层张量形状->[batch_size, seq_len, d_model]

多头注意力机制通过并行计算多个注意力头,从不同的子空间中提取信息,然后将结果拼接起来。具体步骤如下:

  • 步骤1:线性变换

    对输入进行线性变换,生成多个头的查询、键和值:

    \(Q_i=QW_i^Q, K_i=KW_i^K, V_i=VW_i^V\)

    其中:

    • \(W_i^Q,W_i^K,W_i^V\) 是第\(i\)个头的可学习权重矩阵。
    • \(Q,K,V​\)是输入序列的查询、键和值。
  • 步骤2:计算注意力

    对每个头分别计算注意力:

    \(head_i=Attention(Q_i,K_i,V_i)​\)

  • 步骤3:拼接多头结果并线性变换

    将所有头的输出拼接起来,然后通过一个线性变换得到最终输出:

    \(MultiHead(Q,K,V)=Concat(head_1,head_2,…,head_h)W^O​​\)

    其中:

    • \(h​\)是注意力头的数量。
    • \(W^O​\)​是输出的可学习权重矩阵。

4.4.2 结构图

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层(Q,K,V分别经过head次线性变换层)。其实并不是,图中只使用了一组线性变化层,即三个变换张量对Q,K,V分别进行1次线性变换,这些变换不会改变原有张量的尺寸。

因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算(句子中的每个词的表示的一部分),也就是只分割了最后一维的词嵌入向量(batch_size, seq_len, d_model)。这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制。

Properties
理论上Q,K,V分别进行8次线性变换:
Q1 = W1Q 
Q2 = W2Q 
...
Q8 = W8Q
Q形状为(4, 512)W形状为(512, 64)Q1~Q8形状为(4, 64)Q1~Q8在特征维度进行stack操作得到Q_NEW(4, 8, 64)

实际操作Q,K,V分别进行1次线性变换:
Q_NEW = WQ
Q形状为(4, 512)W形状为(512, 512)Q_NEW形状为(4, 512)
将Q_NEW通过view操作在特征维度上进行分割得到8个头的表示(4, 8, 64)

4.4.3 作用

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

  • 捕捉多种关系:每个头能够从不同的子空间捕捉信息,允许模型同时关注序列中不同的方面。不同的头可以学习序列中不同的相似性或关联性。
  • 提高模型表达能力:多头注意力机制使得模型能够并行地从多个角度理解数据,从而提高模型的表达能力。
  • 并行化计算:由于每个头的计算是独立的,可以并行处理,提升计算效率。

4.4.4 代码实现

Python
import copy

# 多头注意力机制类 MultiHeadedAttention 实现思路分析
# 1 init函数    (self, head, embedding_dim, dropout_p=0.1)
    # 每个头特征尺寸大小self.d_k    多少个头self.head    线性层列表self.linears
    # self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
    # 注意力权重分布self.attn=None    dropout层self.dropout
# 2 forward(self, query, key, value, mask=None)
    # 2-1 掩码增加一个维度[8,4,4] -->[1,8,4,4] 求多少批次batch_size
    # 2-2 数据经过线性层 切成8个头,view(batch_size, -1, self.head, self.d_k), transpose(1,2)    # 数据形状变化[2,4,512] ---> [2,4,8,64] ---> [2,8,4,64]
    # 2-3 24个头 一起送入到attention函数中求 x, self.attn
    # attention([2,8,4,64],[2,8,4,64],[2,8,4,64],[1,8,4,4]) ==> x[2,8,4,64], self.attn[2,8,4,4]]
    # 2-4 数据形状再变化回来 x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k)
    # 数据形状变化 [2,8,4,64] ---> [2,4,8,64] ---> [2,4,512]
    # 2-5 返回最后线性层结果 return self.linears[-1](x)

# 深度copy模型 输入模型对象和copy的个数 存储到模型列表中
def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

class MultiHeadedAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout_p=0.1):
        super(MultiHeadedAttention, self).__init__()
        # 确认数据特征能否被被整除 eg 特征尺寸512 % 头数8
        assert embedding_dim % head == 0, 'head不能被整除'
        # 计算每个头特征尺寸 特征尺寸512 // 头数8 = 64
        self.d_k = embedding_dim // head
        # 多少头数
        self.head = head
        # 克隆四个线性层
        # Q K V 分别线性计算的三个线性层    3层
        # 多个头的注意力表示拼接后线性计算的线性层    1层
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
        # 注意力权重分布
        self.attn = None
        # dropout层
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, query, key, value, mask=None):
        # 求数据多少行 eg:[2,4,512] 则batch_size=2
        batch_size = query.size()[0]

        # 数据形状变化[2,4,512] ---> [2,4,8,64] ---> [2,8,4,64]
        # 4代表4个单词 8代表8个头 让句子长度4和句子特征64靠在一起 更有利捕捉句子特征
        query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1,2)
                             for model, x in zip(self.linears, (query, key, value))]

        # myoutptlist_data = []
        # for model, x in zip(self.linears, (query, key, value)):
        #     print('x--->', x.shape) # [2,4,512]
        #     myoutput = model(x)
        #     print('myoutput--->',    myoutput.shape)    # [2,4,512]
        #     # [2,4,512] --> [2,4,8,64] --> [2,8,4,64]
        #     tmpmyoutput = myoutput.view(batch_size, -1,    self.head, self.d_k).transpose(1, 2)
        #     myoutptlist_data.append( tmpmyoutput )
        # mylen = len(myoutptlist_data)     # mylen:3
        # query = myoutptlist_data[0]         # [2,8,4,64]
        # key = myoutptlist_data[1]             # [2,8,4,64]
        # value = myoutptlist_data[2]         # [2,8,4,64]

        # attention()->4.3章节注意力机制函数
        # 注意力结果表示x形状 [2,8,4,64] 注意力权重attn形状:[2,8,4,4]
        # attention([2,8,4,64],[2,8,4,64],[2,8,4,64],[1,8,4,4]) ==> x[2,8,4,64], self.attn[2,8,4,4]]
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

        # 拼接多头
        # 数据形状变化 [2,8,4,64] ---> [2,4,8,64] ---> [2,4,512]
        x = x.transpose(1,2).contiguous().view(batch_size, -1, self.head*self.d_k)

        # 返回最后变化后的结果 [2,4,512]---> [2,4,512]
        # self.linears[-1]: 获取最后一个线性层对象
        return self.linears[-1](x)

函数调用:

Python
# 测试多头注意力机制
def dm_test_MultiHeadedAttention():
    vocab = 1000    # 词表大小是1000
    d_model = 512    # 词嵌入维度是512维
    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1    # 置0概率为0.1
    max_len = 60    # 句子最大长度
    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embedded_result)

    head = 8    # 头数head
    query = key = value = pe_result    # torch.Size([2, 4, 512])

    # 输入的掩码张量mask
    # (x != 0).type(torch.uint8): 值为0的位置为0, 不为0的位置为1
    # unsqueeze(1) -> (2,1,4), 后续masked_fill操作时在1轴上进行广播变成(2,4,4)和scores对齐
    # unsqueeze(1) -> (2,1,1,4), 后续masked_fill操作时在1/2轴上进行广播变成(2,8,4,4)和scores对齐
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
    my_mha = MultiHeadedAttention(head, d_model, dropout_p)
    mha_result = my_mha(query, key, value, mask)
    print('多头注意机制后的x', mha_result.shape, '\n', mha_result)
    print('多头注意力机制的注意力权重分布', my_mha.attn.shape)


if __name__ == '__main__':
    dm_test_MultiHeadedAttention()

输出结果:

Python
多头注意机制后的x torch.Size([2, 4, 512]) 
 tensor([[[-1.5029,    2.2905,    3.5975,    ...,    2.2099, -5.0509, -3.9762],
                 [ 0.9588,    0.5078,    0.2165,    ...,    6.0500, -0.1836, -1.5780],
                 [-1.4579,    2.2605,    3.1544,    ...,    5.5600, -4.3246, -3.8505],
                 [-1.0983,    1.0788,    2.8548,    ...,    4.6244, -3.7929, -3.1282]],

                [[-2.9288, -2.2267,    3.6888,    ..., -0.5745,    5.1030, -6.2719],
                 [-2.5670,    2.3732,    2.1863,    ..., -5.6680,    3.0177, -1.9991],
                 [-1.8909, -0.5001,    2.6733,    ..., -2.8760,    4.8483, -5.3741],
                 [-4.7026, -2.5351,    3.0038,    ..., -0.5272,    3.3875, -5.8972]]],
             grad_fn=<ViewBackward0>)
多头注意力机制的注意力权重分布 torch.Size([2, 8, 4, 4])

4.4.5 小结

  • 什么是多头注意力机制:
    • 多头注意力机制(Multi-Head Attention)是 Transformer 模型的核心组件之一,用于捕捉输入序列中不同位置之间的依赖关系。它通过并行计算多个注意力头(Attention Heads),从不同的子空间中提取信息,从而增强模型的表达能力。
  • 多头注意力机制的作用:
    • 捕捉多种关系:每个头能够从不同的子空间捕捉信息,允许模型同时关注序列中不同的方面。不同的头可以学习序列中不同的相似性或关联性。
    • 提高模型表达能力:多头注意力机制使得模型能够并行地从多个角度理解数据,从而提高模型的表达能力。
    • 并行化计算:由于每个头的计算是独立的,可以并行处理,提升计算效率。
  • 实现多头注意力机制的类: MultiHeadedAttention
    • 因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones。
    • clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数。
    • clones函数的输出是装有N个克隆层的Module列表。
    • 接着实现MultiHeadedAttention类, 它的初始化函数输入是h, d_model, dropout_p分别代表头数,词嵌入维度和置零概率。
    • 它的实例化对象输入是Q, K, V以及掩码张量mask。
    • 它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示。

4.5 前馈全连接层

4.5.1 介绍

前馈全连接层(Feed-Forward Neural Network, FFN)Transformer模型中用于进一步处理输入数据的一个核心组件,位于多头注意力机制(Multi-Head Attention)后面。它主要用于对每个位置的表示进行非线性转换,以增强模型的表达能力。

在Transformer模型中,前馈全连接层通常由两个线性变换层(全连接层)和一个非线性激活函数组成,其结构可以用以下公式表示:

\(FFN(x)=ReLU(xW_1+b_1)W_2+b_2\)

其中:

  • \(x​\):输入特征(形状为[batch_size, seq_len, d_model])。
  • \(W_1,b_1\)​​:第一层的权重矩阵和偏置。
  • \(W_2,b_2​\):第二层的权重矩阵和偏置。
  • \(ReLU\):激活函数(也可以是其他非线性函数,如\(GELU\)​)。

参数说明:

  • 第一层将输入从d_model维度映射到d_ff维度(通常d_ff > d_model)。

  • 第二层将d_ff维度映射回d_model维度。

作用:

  • 非线性变换:通过激活函数(如ReLU或GELU)对数据进行非线性转换,使得模型能够表示复杂的函数映射,增强模型的表达能力。
  • 提升维度:前馈层将输入的维度d_model扩展到更大的维度d_ff,再缩回d_model。这种升维-降维的操作,允许模型在更高维的空间中进行更丰富的特征学习。
  • 位置独立性:前馈层是对每个位置的表示独立处理的,意味着它不会考虑序列中元素之间的位置关系,而是通过注意力机制来捕捉这些关系。因此,前馈层更专注于处理每个位置的特征变换。

4.5.2 代码实现

Python
# 前馈全连接层PositionwiseFeedForward实现思路分析
# 1 init函数    (self,    d_model, d_ff, dropout=0.1):
    # 定义线性层self.linear1 self.linear2, self.dropout层
# 2 forward(self, x)
    # 数据经过self.w1(x) -> F.relu() ->self.dropout() ->self.w2 返回

class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout_p=0.1):
        # d_model    第1个线性层输入维度
        # d_ff         第2个线性层输出维度
        super(PositionwiseFeedForward, self).__init__()
        # 定义线性层w1 w2 dropout
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, x):
        # 数据依次经过第1个线性层 relu激活层 dropout层,然后是第2个线性层
        return self.linear2(self.dropout(torch.relu(self.linear1(x))))

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

ReLU函数图像:

函数调用:

Python
def dm_test_PositionwiseFeedForward():
        vocab = 1000    # 词表大小是1000
        d_model = 512    # 词嵌入维度是512维
        # 输入x 形状是2 x 4
        x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

        # 输入部分的Embeddings类
        my_embeddings = Embeddings(vocab, d_model)
        embedded_result = my_embeddings(x)

        dropout_p = 0.1    # 置0概率为0.1
        max_len = 60    # 句子最大长度
        # 输入部分的PositionalEncoding类
        my_pe = PositionalEncoding(d_model, dropout_p, max_len)
        pe_result = my_pe(embedded_result)

        head = 8    # 头数head
        query = key = value = pe_result    # torch.Size([2, 4, 512])

        # 输入的掩码张量mask
        mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
        my_mha = MultiHeadedAttention(head, d_model, dropout_p)
        mha_result = my_mha(query, key, value, mask)

        # 测试前馈全链接层
        my_pff = PositionwiseFeedForward(d_model=512, d_ff=64, dropout_p=0.1)
        ff_result = my_pff(mha_result)
        print('x--->', ff_result.shape, ff_result)


if __name__ == '__main__':
    dm_test_PositionwiseFeedForward()

输出结果:

Python
x---> torch.Size([2, 4, 512]) tensor([[[ 0.2622,    0.4398, -0.4772,    ..., -0.1021,    0.9268,    2.1575],
                 [ 0.4111,    1.1654, -0.5249,    ..., -0.2609,    2.1317,    2.6406],
                 [ 0.9710,    0.0747, -1.6504,    ..., -0.9052,    1.3052,    3.3491],
                 [ 0.5767,    1.7927, -1.0501,    ...,    0.0282,    1.4282,    3.1193]],

                [[ 0.4097, -0.1773, -0.8848,    ..., -0.8248,    2.7958,    0.9619],
                 [ 0.1791, -1.0730, -0.8525,    ..., -1.5877,    2.3985,    1.4295],
                 [ 0.6742,    0.0753, -1.0631,    ..., -2.4176,    2.1048,    0.5939],
                 [-0.2708, -0.4281, -0.1506,    ..., -0.9803,    1.8283,    1.0055]]],
             grad_fn=<ViewBackward0>)

4.5.3 小结

  • 什么是前馈全连接层:
    • 在Transformer模型中,前馈全连接层通常由两个线性变换层(全连接层)和一个非线性激活函数组成。
  • 前馈全连接层的作用:
    • 非线性变换: 非线性激活函数为模型引入了非线性能力,使其可以学习非线性关系。
    • 提升维度:前馈层将输入的维度d_model扩展到更大的维度d_ff,再缩回d_model。这种升维-降维的操作,允许模型在更高维的空间中进行更丰富的特征学习,提取更高级的特征。
    • 位置独立性:每个位置的输入会通过相同的前馈全连接层,但每个位置的表示都经过独立的变换,这有助于模型学习位置相关的特征。
  • 实现前馈全连接层的类: PositionwiseFeedForward
    • 它的实例化参数为d_model, d_ff, dropout, 分别代表词嵌入维度, 线性变换维度, 和置零比率。
    • 它的输入参数x, 表示上层的输出。
    • 它的输出是经过两层线性网络变换的特征表示。

4.6 规范化层(层归一化)

4.6.1 介绍

  • 概念

    层归一化(Layer Normalization) 是一种用于提高深度神经网络训练稳定性和加速收敛的技术,广泛应用于现代深度学习模型中,尤其是在Transformer等序列建模网络中。它通过对每一层的输出进行归一化处理,来缓解梯度消失或爆炸的问题,并有助于模型在训练过程中更加稳定。

  • 核心思想

    层归一化的核心思想是对每个输入样本在每一层内部进行标准化。具体来说,它会将输入的特征按层(即按样本维度的层面)进行归一化,而不是像批归一化(Batch Normalization)那样按批次(即样本的层面)进行归一化。

    层归一化的目标是确保每个神经网络层的输入分布具有一致的均值和方差,这样可以防止激活值过大或过小,导致梯度在反向传播时出现不稳定的情况。

  • 公式

    假设某一层的输入为一个向量\(x=[x_1, x_2, ..., x_d]\),其中​\(d\)是该层的特征维度。层归一化对该层的输入进行标准化的过程如下:

    • 计算均值和方差

      对输入向量\(x​\)中的每个元素,计算其均值和方差:

      • 均值\(u = \frac{1}{d} \sum_{i=1}^{d} x_i\)
      • 方差\(\sigma^2 = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2\)
    • 标准化

      然后,通过将每个元素减去均值并除以标准差来对输入进行标准化,得到标准化后的值\(\hat{x}_i​\)

      \(\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2} + \epsilon}​\)

      其中,\(\epsilon​\)\(是一个小的常数(通常是 ​\)\(10^{-5}​\)\(10^{-6}​\)),用于避免除零错误。

    • 缩放与平移

      在标准化的基础上,层归一化通常会引入两个可学习的参数:缩放因子(gamma)平移因子(beta),用于调整标准化后的输出,以便网络能够学习到合适的表示。

      • 缩放因子\(\gamma \in \mathbb{R}^d​\)
      • 平移因子\(\beta \in \mathbb{R}^d\)
    • 最终输出为

      \(y_i = \gamma \hat{x}_i + \beta\)

      这里,\(y_i​\)就是经过层归一化处理后的输出。

  • 作用

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

    • 稳定训练过程:层归一化可以确保每一层的输入分布更加稳定,避免激活值过大或过小,防止梯度消失或爆炸。
    • 不依赖批次大小:层归一化与 批归一化(Batch Normalization) 不同,它不依赖于批次大小,而是针对单个样本的特征进行归一化。这意味着层归一化在RNN和Transformer等处理变长输入的模型中更加有效,因为这些模型的批次大小可能变化。
    • 适用于时间序列模型:由于它对每个时间步进行归一化处理,而不依赖于整个批次,因此在像LSTM或Transformer等序列模型中,层归一化能够很好地适应时间序列的动态变化。
    • 更快的收敛速度:通过归一化,每一层的激活值保持在相对统一的范围内,这有助于更快速的梯度更新,从而加速收敛。

Tips:层归一化 vs 批归一化

特性 层归一化(Layer Normalization) 批归一化(Batch Normalization)
计算方式 每个样本的特征维度进行归一化 对一个批次的所有样本进行归一化
归一化维度 按特征维度进行归一化(样本内部归一化) 按批次维度进行归一化(批次内归一化)
适用模型 适用于RNN、Transformer等序列模型 适用于CNN、全连接网络等大多数网络
依赖批次大小 不依赖批次大小 依赖批次大小,较小批次会影响统计量的准确性
训练速度 可以加速训练,但不如批归一化显著 通常能加速训练并稳定模型,但受限于批次大小
实现复杂度 实现简单,无需存储全局统计量 需要存储全局均值和方差

4.6.2 代码实现

Python
# 规范化层 LayerNorm 实现思路分析
# 1 __init__(self, features, eps=1e-6):
    # 定义线性层self.a2 self.b2, nn.Parameter(torch.ones(features))
# 2 forward(self, x) 返回标准化后的结果
    # 对数据求均值 保持形状不变 x.mean(-1, keepdims=True)
    # 对数据求标准差 保持形状不变 x.std(-1, keepdims=True)
    # 对数据进行标准化变换 反向传播可学习参数a2 b2 
    # eg self.a2 * (x-mean)/(std + self.eps) + self.b2

class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        # 参数features 待规范化的数据维度
        # 参数 eps=1e-6 防止分母为零
        super(LayerNorm, self).__init__()
        # 定义a2 γ规范化层的系数 y=kx+b中的k
        self.a2 = nn.Parameter(torch.ones(features))
        # 定义b2 β规范化层的系数 y=kx+b中的b
        self.b2 = nn.Parameter(torch.zeros(features))
        # 小常数
        self.eps = eps

    def forward(self, x):
        # 对数据求均值 保持形状不变
        # -1: 根据最后1个维度计算, 词特征维度
        # [2,4,512] -> [2,4,1]
        mean = x.mean(-1, keepdims=True)
        # 对数据求标准差 保持形状不变
        # [2,4,512] -> [2,4,1]
        std = x.std(-1, keepdims=True)
        # 对数据进行标准化变换 反向传播可学习参数a2 b2
        # 注意 * 表示对应位置相乘 不是矩阵运算
        x = self.a2 * (x - mean) / (std + self.eps) + self.b2
        return x

函数调用:

Python
# 规范化层测试
def dm_test_LayerNorm():
    vocab = 1000    # 词表大小是1000
    d_model = 512    # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)    # [2, 4, 512]

    dropout_p = 0.2    # 置0概率为0.2
    max_len = 60    # 句子最大长度
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embedded_result)

    query = key = value = pe_result    # torch.Size([2, 4, 512])
    # 调用验证

    d_ff = 64
    head = 8

    # 多头注意力机制的输出 作为前馈全连接层的输入
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
    my_mha = MultiHeadedAttention(head, d_model, dropout_p)
    mha_result = my_mha(query, key, value, mask)

    my_ff = PositionwiseFeedForward(d_model, d_ff, dropout_p)
    ff_result = my_ff(mha_result)

    features = d_model = 512
    eps = 1e-6
    my_ln = LayerNorm(features, eps)
    ln_result = my_ln(ff_result)
    print('规范化层:', ln_result.shape, ln_result)


if __name__ == '__main__':
    dm_test_LayerNorm()

输出结果:

Python
规范化层: torch.Size([2, 4, 512]) tensor([[[-0.3092, -0.0910, -1.6996,    ..., -0.9303,    0.4432,    0.2160],
                 [ 0.6669,    1.0475,    0.3514,    ...,    1.4554,    0.9730, -1.4658],
                 [ 1.3659,    0.6126, -0.8531,    ..., -0.8469,    1.0127, -1.0504],
                 [ 1.3816,    0.3314, -0.3039,    ...,    0.2701,    0.5732, -1.0152]],

                [[ 0.3848, -0.8395,    0.0173,    ...,    0.6260,    0.3904, -1.2215],
                 [-1.1391, -0.8122,    0.1111,    ...,    0.2632, -0.0076, -1.2140],
                 [ 0.3211, -0.4198,    0.1856,    ..., -0.6273,    0.1961,    0.0278],
                 [-0.2528, -1.5907,    0.3159,    ..., -0.1957,    0.0760, -1.1218]]],
             grad_fn=<AddBackward0>)

4.6.3 小结

  • 什么是规范化层:
    • 一种用于提高深度神经网络训练稳定性和加速收敛的技术,广泛应用于现代深度学习模型中,尤其是在Transformer等序列建模网络中。
    • 对每个输入样本在每一层内部进行标准化,确保每个神经网络层的输入分布具有一致的均值和方差。
  • 规范化层的作用:
    • 稳定训练:减少内部协变量偏移(Internal Covariate Shift),使训练过程更加稳定。
    • 加速收敛:通过归一化输入分布,加快模型的收敛速度。
    • 改善泛化:提高模型的泛化能力。
  • 实现规范化层的类: LayerNorm
    • 它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小的数。
    • 它的输入参数x代表来自上一层的输出。
    • 它的输出就是经过规范化的特征表示。

4.7 子层连接

4.7.1 介绍

  • 概念

    子层连接(Sublayer Connection),也称为残差连接(Residual Connection),是Transformer模型中的一个关键设计,用于将多个子层(如自注意力层和前馈全连接层)组合在一起。它通过残差连接(Residual Connection)和层归一化(Layer Normalization)来增强模型的训练稳定性和性能。

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

  • 结构

    • 残差连接:将子层的输入直接加到子层的输出上。

    • 层归一化:对残差连接的结果进行归一化。

    • 公式:

      \(Output=LayerNorm(x+Sublayer(x))​\)

      • \(x​\):子层的输入
      • \(Sublayer(x)​\):子层的输出(如自注意力层或前馈全连接层)
      • \(LayerNorm​\):层归一化
  • 作用

    • 避免梯度消失或爆炸:在深度神经网络中,梯度可能会在反向传播过程中逐渐消失或爆炸,导致训练不稳定。通过残差连接,输入能够直接传递到输出,从而有效地缓解了梯度消失问题。梯度可以通过残差路径传递,使得深层网络的训练变得更加容易。
    • 加速收敛:由于残差连接使得信息更容易流动,因此它能够加速模型的训练过程。这种加速效果特别显著,在更深层的网络中,残差连接可以帮助网络更快地收敛到最佳解。
    • 有效信息传递:层归一化的应用确保了每一层的输出具有合适的分布,从而避免了过大的激活值引起的数值不稳定问题。这保证了模型的训练过程中,信息能够有效地在不同层之间传递。
    • 防止过拟合:通过残差连接,模型可以更好地捕捉和保留有用的特征,避免信息丢失,有助于减轻过拟合问题,尤其是在深层网络中。

4.7.2 代码实现

Python
# 子层连接结构 子层(前馈全连接层 或者 注意力机制层)+ norm层 + 残差连接
# SublayerConnection实现思路分析
# 1 __init__(self, size, dropout=0.1):
    # 定义self.norm层 self.dropout层, 其中LayerNorm(size)
# 2 forward(self, x, sublayer) 返回+以后的结果
    # 数据self.norm() -> sublayer()->self.dropout() + x
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout_p=0.1):
        super(SublayerConnection, self).__init__()
        # 参数size 词嵌入维度尺寸大小
        # 参数dropout 置零比率
        self.size = size
        self.dropout_p = dropout_p
        # 定义norm层
        self.norm = LayerNorm(self.size)
        # 定义dropout
        self.dropout = nn.Dropout(self.dropout_p)

    def forward(self, x, sublayer):
        # 参数x 代表数据
        # sublayer 函数入口地址 子层函数(前馈全连接层 或者 注意力机制层函数的入口地址)
        # 方式1 数据self.norm() -> sublayer() -> self.dropout() + x    
        # 通常效果最好
        # myres = x + self.dropout(sublayer(self.norm(x)))
        # 方式2 数据sublayer() -> self.norm() -> self.dropout() + x
        # 不推荐,可能导致训练不稳定
        # myres = x + self.dropout(self.norm(sublayer(x)))
        # 方式3 数据sublayer() -> self.dropout() + x -> self.norm()
        # Transformer的标准实现
        myres = self.norm(x + self.dropout(sublayer(x)))
        return myres

函数调用:

Python
def dm_test_SublayerConnection():
    vocab = 1000    # 词表大小是1000
    d_model = 512
    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)    # [2, 4, 512]

    dropout_p = 0.2    # 置0概率为0.2
    max_len = 60    # 句子最大长度 
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embedded_result)

    size = 512
    head = 8
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
    # 多头自注意力子层
    self_attn = MultiHeadedAttention(head, d_model)
    sublayer = lambda x: self_attn(x, x, x, mask)

    # 子层连接结构
    my_sc = SublayerConnection(size, dropout_p)
    sc_result = my_sc(pe_result, sublayer)
    print('sc_result.shape--->', sc_result.shape)
    print('sc_result--->', sc_result)


if __name__ == '__main__':
    dm_test_SublayerConnection()

输出结果:

Python
sc_result.shape---> torch.Size([2, 4, 512])
sc_result---> tensor([[[ 7.0114e+00,    3.5230e+01, -3.9275e+01,    ...,    3.0409e+01,
                     4.3308e+01, -1.4148e+01],
                 [ 6.0625e+01, -1.7394e+01, -1.6531e+01,    ...,    1.7075e+01,
                     1.2843e+01,    1.7425e+01],
                 [-3.2658e+00, -3.8847e+01, -2.7823e+00,    ..., -3.3643e+01,
                     2.1593e+01,    2.6389e+01],
                 [ 3.1625e+01,    4.7571e+00, -3.5086e+01,    ...,    1.1333e-01,
                     4.0569e-02,    4.4964e-01]],

                [[-1.1280e+01,    8.7631e-01, -1.5429e+01,    ..., -5.2952e+01,
                    -2.4344e+01, -8.6224e+00],
                 [ 2.5465e+01, -3.4799e+01,    2.4003e-01,    ...,    4.8790e+01,
                    -2.3115e-01, -4.4688e+01],
                 [-5.0735e-02,    1.1536e+00,    2.1595e-01,    ..., -2.3241e+01,
                     1.8083e+01,    1.7346e+01],
                 [ 2.1725e+00,    9.8909e+00, -1.9748e+01,    ...,    2.9949e+01,
                     2.8737e+01,    2.5619e-02]]], grad_fn=<AddBackward0>)

4.7.3 小结

  • 什么是子层连接结构:
    • 子层连接(Sublayer Connection),也称为残差连接(Residual Connection),是 Transformer 模型中的一个关键设计,用于将多个子层(如自注意力层和前馈全连接层)组合在一起。它通过残差连接(Residual Connection)和层归一化(Layer Normalization)来增强模型的训练稳定性和性能。
  • 学习并实现了子层连接结构的类: SublayerConnection
    • 类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率。
    • 它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示。
    • 它的输出就是通过子层连接结构处理的输出。

4.8 编码器层

4.8.1 介绍

  • 概念

    编码器层(Encoder Layer)是Transformer编码器的基本构建单元,它重复堆叠形成整个编码器,负责逐步提取输入序列的特征。每个编码器层由两个核心子层组成:

    • 多头自注意力机制(Multi-Head Self-Attention):用于捕捉输入序列中每个位置与其他位置的关系。
    • 前馈全连接层(Feed-Forward Neural Network, FFN):用于对每个位置的表示进行非线性变换。

    每个子层后都有残差连接(Residual Connection)和层归一化(Layer Normalization),以增强模型的训练稳定性和性能。

  • 结构/工作流程

    • 输入:

      • 每个编码器层的输入是上一层编码器层的输出,或者对于第一层编码器层来说,是输入嵌入向量加上位置编码向量,形状为 [batch_size, seq_len, d_model]
    • 多头自注意力机制

      • 将输入x传递给多头自注意力层,得到输出\(Attention(x)​\)
      • 多头自注意力层会捕捉输入序列中各个token之间的依赖关系,每个注意力头关注不同的特征,然后将多个头的输出拼接。
      • Q, K, V三个矩阵都来自于相同的输入x,这是“自”注意力的含义。
    • 残差连接与层归一化

      • 将多头自注意力的输入x与输出\(Attention(x)​\) 相加,形成残差连接:

        \(x + Attention(x)​\)

      • 对残差连接的结果进行层归一化,得到第一部分的输出:

        \(LayerNorm(x + Attention(x))​\)

    • 前馈全连接层

      • 将经过残差连接和层归一化的输出传递给前馈全连接网络,得到输出\(FFN(x)​\)
      • 前馈全连接网络会对每个token的表示进行非线性变换,并提取更高级的特征。
    • 残差连接与层归一化

      • 将前馈全连接网络的输入(即上一层的输出)与输出\(FFN(x)\)相加,形成残差连接:

        \(LayerNorm(x + Attention(x)) + FFN(x)​\)

      • 对残差连接的结果进行层归一化,得到第二部分的输出,也是编码器层的最终输出:

        \(LayerNorm(LayerNorm(x + Attention(x)) + FFN(x))\)

    • 输出:

      • 经过处理的特征表示,该层的输出会被传递给下一层的输入(形状为[batch_size, seq_len, d_model])。
  • 作用

    对输入的token表示进行处理,从而提取更高级的特征和上下文信息。每个编码器层都能够处理上一层的输出,并逐步将其转化为更丰富、更抽象的表示。

4.8.2 代码实现

Python
# 编码器层类 EncoderLayer 实现思路分析
# init函数 (self, size, self_attn, feed_forward, dropout):
    # 实例化多头注意力层对象self_attn # 前馈全连接层对象feed_forward    size词嵌入维度512
    # clones两个子层连接结构 self.sublayer = clones(SublayerConnection(size,dropout_p),2)
# forward函数 (self, x, mask)
    # 数据经过子层连接结构1 self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
    # 数据经过子层连接结构2 self.sublayer[1](x, self.feed_forward)
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout_p):
        super(EncoderLayer, self).__init__()
        # 实例化多头注意力层对象
        self.self_attn = self_attn
        # 前馈全连接层对象feed_forward
        self.feed_forward = feed_forward
        # size词嵌入维度512
        self.size = size
        self.dropout_p = dropout_p
        # clones两个子层连接结构 self.sublayer = clones(SublayerConnection(size,dropout_p),2)
        self.sublayer = clones(SublayerConnection(self.size, self.dropout_p), 2)

    def forward(self, x, mask):
        # 数据经过第1个子层连接结构
        # 参数x:传入的数据    参数lambda x... : 子函数入口地址
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

        # 数据经过第2个子层连接结构
        # 参数x:传入的数据    self.feed_forward子函数入口地址
        x = self.sublayer[1](x, self.feed_forward)
        return x

函数调用:

Python
def dm_test_EncoderLayer():
    vocab = 1000    # 词表大小是1000
    d_model = 512

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    my_embeddings = Embeddings(vocab, d_model)
    embeded_result = my_embeddings(x)    # [2, 4, 512]

    dropout_p = 0.2    # 置0概率为0.2
    max_len = 60    # 句子最大长度 
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embeded_result)

    size = 512
    head = 8
    d_ff = 64
    # 实例化多头注意力机制类对象
    self_attn = MultiHeadedAttention(head, d_model)
    # 实例化前馈全连接层对象
    my_ff = PositionwiseFeedForward(d_model, d_ff, dropout_p)
    # mask数据
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)

    # 实例化编码器层对象
    my_encoderlayer = EncoderLayer(size, self_attn, my_ff, dropout_p)

    # 数据通过编码层编码
    el_result = my_encoderlayer(pe_result, mask)
    print('el_result.shape', el_result.shape, el_result)


if __name__ == '__main__':
    dm_test_EncoderLayer()

输出结果:

Python
el_result.shape torch.Size([2, 4, 512]) tensor([[[ 42.3457, -17.2756,     0.1695,    ...,    20.6587, -28.8568,     0.1003],
                 [-13.3407, -18.3088, -21.3491,    ...,    -8.9874, -40.2917,    56.1672],
                 [ 44.9739,    27.2132, -15.0390,    ...,    -7.1177,     5.7607,    13.7697],
                 [    0.1546, -20.3611,    15.9304,    ...,     7.0122,    42.5421,    -1.7627]],

                [[-15.8305,    -5.5882, -27.7982,    ...,    -0.2449,    62.8648, -18.9937],
                 [ -0.2576,    -5.0591,    -0.5264,    ...,    35.8405, -14.7624,    59.5532],
                 [    2.9312,    40.0757,    42.7443,    ...,    42.9479,    62.8699,    37.2467],
                 [ 10.2777, -17.0255,    -8.0018,    ...,    34.2319, -35.8646,    -0.3673]]],
             grad_fn=<AddBackward0>)

4.8.3 小结

  • 什么是编码器层:
    • 编码器层(Encoder Layer)是Transformer编码器的基本构建单元,它重复堆叠形成整个编码器,负责逐步提取输入序列的特征。
  • 编码器层的作用:
    • 对输入的token表示进行处理,从而提取更高级的特征和上下文信息。每个编码器层都能够处理上一层的输出,并逐步将其转化为更丰富、更抽象的表示。
  • 学习并实现了编码器层的类: EncoderLayer
    • 类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率dropout。
    • 实例化对象的输入参数有2个,x代表来自上一层的输出, mask代表掩码张量。
    • 它的输出代表经过整个编码层的特征表示。

4.9 编码器

4.9.1 介绍

  • 概念

    编码器(Encoder)是Transformer架构中的核心组成部分,它负责将输入的序列(通常是一个词汇序列或其他类型的输入数据)映射到一个高维空间中的表示(中间语义张量c),这个高维空间中的表示随后会被解码器用于生成输出序列。在 Transformer中,编码器是由多个相同结构的层叠加而成,每个编码器层都由多头自注意力机制前馈全连接网络两个子层组成,并且每个子层都使用残差连接层归一化来确保训练过程的稳定性和加速收敛。

  • 结构

    编码器由多个相同的编码器层(Encoder Layer)堆叠而成。每个编码器层包含两个核心子层:

    • 多头自注意力机制(Multi-Head Self-Attention)
      • 计算输入序列中每个位置与其他位置的相关性。
      • 通过多个注意力头捕捉不同子空间的特征。
    • 前馈全连接层(Feed-Forward Neural Network, FFN)
      • 对每个位置的表示进行非线性变换。
      • 通常由两个全连接层和激活函数(如 ReLU)组成。

    每个子层后都有:

    • 残差连接(Residual Connection):将输入直接加到子层输出上。
    • 层归一化(Layer Normalization):对输出进行归一化。
  • 工作流程

    • 输入:
      • 每个编码器层的输入是上一层的输出,或者对于第一层来说,是输入嵌入向量加上位置编码向量。
    • 多头自注意力机制:
      • 将输入传递给多头自注意力层,捕捉输入序列中各个 token 之间的依赖关系,并生成一个加权后的表示。
    • 残差连接和层归一化:
      • 将多头自注意力的输入与输出相加,形成残差连接,然后进行层归一化操作。
    • 前馈全连接网络:
      • 将经过残差连接和层归一化的输出传递给前馈全连接网络,进行非线性变换和特征提取。
    • 残差连接和层归一化:
      • 将前馈全连接网络的输入与输出相加,形成残差连接,然后进行层归一化操作。
    • 输出:
      • 每个编码器层的输出都会作为下一层编码器层的输入,最终最后一层的输出就是整个编码器的输出。
  • 作用

    编码器的主要功能是将输入序列映射到一个高维特征空间,具体包括:

    • 特征提取:通过自注意力机制捕捉输入序列中每个位置与其他位置的关系。
    • 特征增强:通过前馈全连接层对特征进行非线性变换。
    • 层次化表示:通过多层堆叠,逐步提取更高层次的特征表示。

4.9.2 代码实现

Python
# 编码器类 Encoder 实现思路分析
# init函数 (self, layer, N)
# 实例化多个编码器层对象self.layers     通过方法clones(layer, N)
# 实例化规范化层 self.norm = LayerNorm(layer.size)
# forward函数 (self, x, mask)
# 数据经过N个层 x = layer(x, mask)
#    返回规范化后的数据 return self.norm(x)
class Encoder(nn.Module):
    def __init__(self, layer, N):
        # 参数layer 1个编码器层
        # 参数 编码器层的个数
        super(Encoder, self).__init__()
        # 实例化多个编码器层对象
        self.layers = clones(layer, N)

    def forward(self, x, mask):
        # 数据经过N个层 x = layer(x, mask)
        for layer in self.layers:
            x = layer(x, mask)

        # 返回编码器语义向量
        return x

函数调用:

Python
def dm_test_Encoder():
    vocab = 1000    # 词表大小是1000
    d_model = 512

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])

    my_embeddings = Embeddings(vocab, d_model)
    embeded_result = my_embeddings(x)    # [2, 4, 512]

    dropout_p = 0.2    # 置0概率为0.2
    max_len = 60    # 句子最大长度    
    my_pe = PositionalEncoding(d_model, dropout_p, max_len)
    pe_result = my_pe(embeded_result)

    size = 512
    head = 8
    d_model = 512
    d_ff = 64

    # 获取位置编码器层 编码以后的结果
    c = copy.deepcopy
    self_attn = MultiHeadedAttention(head, d_model)
    dropout_p = 0.2
    my_ff = PositionwiseFeedForward(d_model, d_ff, dropout_p)
    my_encoderlayer = EncoderLayer(size, c(self_attn), c(my_ff), dropout_p)

    # 编码器中编码器层的个数N
    N = 6
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)

    # 实例化编码器对象
    my_encoder = Encoder(my_encoderlayer, N)
    encoder_result = my_encoder(pe_result, mask)
    print('encoder_result.shape--->', encoder_result.shape)
    print('encoder_result--->', encoder_result)
    return encoder_result

if __name__ == '__main__':
    dm_test_Encoder()

输出结果:

Python
encoder_result.shape---> torch.Size([2, 4, 512])
encoder_result---> tensor([[[-1.0195, -2.3425,    1.2915,    ..., -0.1201, -1.1911,    2.0452],
                 [-1.1250,    0.8957, -1.4046,    ..., -1.5621, -1.0622, -0.0025],
                 [-0.9776, -0.1088,    0.0161,    ...,    1.2835,    0.0135, -0.4939],
                 [ 0.1376, -0.5369,    0.3099,    ..., -0.4200,    2.2780, -1.8525]],

                [[ 2.2200,    1.8239, -1.6174,    ..., -0.3400, -0.3632, -2.1963],
                 [ 1.6247,    0.6643,    0.0277,    ..., -0.0780,    0.0067,    0.8427],
                 [-0.4811,    1.1233,    0.1594,    ...,    1.1535,    0.8534,    0.2948],
                 [-0.4268, -0.4649,    1.7288,    ..., -2.4360, -0.0879, -0.9242]]],
             grad_fn=<AddBackward0>)

4.9.3 小结

  • 什么是编码器:
    • 编码器(Encoder)Transformer架构中的核心组成部分,它负责将输入的序列(通常是一个词汇序列或其他类型的输入数据)映射到一个高维空间中的表示(中间语义张量c),这个高维空间中的表示随后会被解码器用于生成输出序列。
  • 编码器的作用:
    • 将输入序列映射到一个高维特征空间,对输入进行指定的特征提取。
  • 学习并实现了编码器的类: Encoder
    • 类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数。
    • forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量。
    • 编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分。