Transformer 代码详细解析

Transformer 代码详细解析




本文为 【这是B站目前讲的最好的【Transformer实战】教程!带你从零详细解读Transformer模型 一次学到饱!——人工智能、深度学习、神经网络】 教程的文字版,个人学习整理,持续更新中。

该B站视频教程也是根据哈佛大学的一篇文档进行整理:The Annotated Transformer


一、Transformer 背景介绍

1.1 Transformer 的诞生

2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!


论文地址: https://arxiv.org/pdf/1810.04805.pdf


而在BERT中发挥重要作用的结构就是Transformer, 之后又相继出现XLNET,roBERT等模型击败了BERT,但是他们的核心没有变,仍然是:Transformer.

1.2 Transformer 的优势

相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:

  1. Transformer能够利用分布式GPU进行并行训练,提升模型训练效率.
  2. 在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好.
    下面是一张在测评比较图:
    在这里插入图片描述

1.3 Transformer 的市场

在著名的SOTA机器翻译榜单上, 几乎所有排名靠前的模型都使用Transformer,
在这里插入图片描述

其基本上可以看作是工业界的风向标, 市场空间自然不必多说!


二、Transformer架构解析

2.1 认识 Transformer 架构

2.1.1 Transformer模型的作用

  • 学习目标:
    • 了解 Transformer 模型的作用。
    • 了解 Transformer 总体架构图中各个组成部分的名称。

  • Transformer 模型的作用:
    • 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.

  • 声明:
    • 在接下来的架构分析中, 我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作, 因此很多命名方式遵循NLP中的规则. 比如: Embeddding层将称作文本嵌入层, Embedding层产生的张量称为词嵌入张量, 它的最后一维将称作词向量等.

2.1.2 Transformer 总体架构图

在这里插入图片描述

  • Transformer 总体架构可分为四个部分:
    • 输入部分
    • 输出部分
    • 编码器部分
    • 解码器部分

  • 输入部分包含:
    • 源文本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器
      在这里插入图片描述

  • 输出部分:
    • 线性层
    • softmax 层
      在这里插入图片描述

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

  • 解码器部分:
    • 由N个解码器层堆叠而成
    • 每个解码器层由三个子层连接结构组成
    • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
    • 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
    • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
      在这里插入图片描述

2.2 输入部分实现

  • 输入部分包含:
    • 源文本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器
      在这里插入图片描述

  • 文本嵌入层的作用:
    • 无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示,希望在这样的高维空间捕捉词汇间的关系。

  • 文本嵌入层的代码解析:
# 导入必备的工具包
import torch

# 预定义的网络层 torch.nn,工具开发者已经帮助我们开发好的一些常用层,
# 比如卷积层,lstm 层,embedding 层等,不需要我们再重复造轮子
import torch.nn as nn

# 数学计算工具包
import math

# torch 中变量封装函数 Variable。
from torch.autograd import Variable  # 注:tensor不能反向传播,variable可以反向传播。

# 定义 Embedding 类来实现文本嵌入层,这里 s 说明代表两个一模一样的嵌入层,他们共享参数。
# 该类继承 nn.Module,这样就有标准层的一些功能,这里我们也可以理解为一种模式,我们自己实现的所有层
class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """类初始化函数,有两个参数,d_model:指词嵌入的维度,vocab:指词表的大小。"""
        # 接着就是使用 super 的方式知名继承 nn.Module 的初始化函数,我们自己实现的所有层都会这样去
        super(Embeddings, self).__init__()
        # 之后就是调用 nn 中的预定义层 Embedding,获得一个词嵌入对象 self.lut
        self.lut = nn.Embedding(vocab, d_model)
        # 最后就是将 d_model 传入类中
        self.d_model = d_model

    def forward(self, x):
        """
        可以将其理解为该层的前向传播逻辑,所有层中都会有此函数
        当传给该类的实例化对象参数时,自动调用该类函数
        参数 x:因为 Embedding 层是首层,所以代表输入给模型的文本通过词汇映射后的张量。
        """

        # 将 x 传给 self.lut 并与根号下 self.d_model 相乘作为结果返回。
        return self.lut(x) * math.sqrt(self.d_model)  # 此处对结果进行了缩放。思考为啥?

embedding = nn.Embedding(10, 3)  # 10 为 vocab_size,也就是需要编码的目标的总数
input = torch.LongTensor([[1, 2, 4, 5], [4, 3, 2, 9]])
embedding(input)

"""
tensor([[[-0.3748,  0.9854,  2.1664],
         [-0.3865,  0.5358, -0.9346],
         [ 1.1182,  0.9395, -0.0367],
         [-0.6363,  0.5225,  1.1308]],

        [[ 1.1182,  0.9395, -0.0367],
         [-1.6431, -0.7007,  0.3045],
         [-0.3865,  0.5358, -0.9346],
         [-0.4872, -0.5939, -2.2481]]], grad_fn=<EmbeddingBackward0>)
"""

embedding = nn.Embedding(10, 3, padding_idx=0)  # padding_idx=0 代表0全映射为0
input = torch.LongTensor([[0, 2, 0, 5]])
embedding(input)

"""
tensor([[[ 0.0000,  0.0000,  0.0000],
         [ 0.3094, -1.1555, -0.3782],
         [ 0.0000,  0.0000,  0.0000],
         [ 0.7858, -0.8773, -1.7635]]], grad_fn=<EmbeddingBackward0>)
"""

  • 实例化参数:
# 词嵌入维度是512维
d_model = 512

# 词表大小是1000
vocab = 1000

  • 输入参数:
# 输入x是一个使用 Variable 封装的长整型张量,形状是 2x4
# 注:tensor不能反向传播,variable可以反向传播。
x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) 

  • 调用:
emb = Embeddings(d_model, vocab)
embr = emb(x)
print("embr:", embr)

"""
embr: tensor([[[ 18.0835,  18.4307,  25.7455,  ...,  18.6767,  15.9241,   6.8471],
         [ 11.9021,  -8.0713, -26.0825,  ...,  10.6016, -31.4030,  10.6847],
         [  9.1287,  17.2753,  14.2908,  ...,   4.8957,   7.2101, -18.9200],
         [ 14.0852, -38.7575,  46.5348,  ...,  -3.5360,  10.5081, -10.5434]],

        [[-25.3910,  21.7453,  18.1822,  ...,  -4.4432,  -8.6873, -31.0001],
         [ -6.7744,  16.6366,  13.0639,  ..., -27.6570, -25.0405,   6.2767],
         [ 24.7707, -19.4100, -12.0361,  ...,  12.6369,   6.8837,  -0.7358],
         [-26.4784,  -9.7061, -20.1255,  ...,  49.6167, -16.2520,  -1.8203]]],
       grad_fn=<MulBackward0>)
[torch.FloatTensor of size 2x4x512]
"""

  • 位置编码器的作用:
    • 因为在 Transformer 的编码器结构中,并没有针对词汇位置信息的处理,因此需要在 Embedding 层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入向量中,以弥补位置信息的缺失。

  • 位置编码器的代码解析:
# 定义位置编码器类,我们同样把它看做一个层,因此会继承 nn.Module
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        """位置编码器类的初始化函数,共有三个参数,分别是 d_model:词嵌入维度,dropout:置0比率,max_len:每个句子的最大长度"""
        super(PositionalEncoding, self).__init__()

        # 实例化 nn 中预定义的 Dropout 层,并将 dropout 传入其中,获得对象 self.dropout
        self.dropout = nn.Dropout(p=dropout)

        # 初始化一个位置编码矩阵,它是一个 0 矩阵,矩阵的大小是 max_len x d_model。
        pe = torch.zeros(max_len, d_model)  # 5000 * 512

        # 初始化一个绝对位置矩阵,在我们这里,词汇的绝对位置就是用它的索引去表示。
        # 所以我们首先使用 arange 方法获得一个连续自然数向量,然后再使用 unsqueeze 方法拓展向量维度
        # 又因为参数传的是1,代表矩阵拓展的位置,会使变量变成一个 max_len x 1 的矩阵。
        position = torch.arange(0, max_len).unsqueeze(1)  # 5000 * 1

        # 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
        # 最简单思路就是先将 max_len x 1 的绝对位置矩阵,变换成 max_len x d_model 形状,然后覆盖原来的初始化位置编码矩阵即可,
        # 要做这种矩阵变换,就需要一个 1 x d_model 形状的变换矩阵 div_term,我们对这个变换矩阵的要求除了形状外,
        # 还是希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛。这样我们就可以开始初始化这个变换矩阵了,
        # 首先使用 arange 获得一个自然数矩阵,但是细心的同学会发现,我们这里并没有按照预计的一样初始化一个 1 x d_model 的矩阵,
        # 而是有了一个跳跃,只初始化了一半即 1 x d_model / 2 的矩阵,为什么是一半呢,其实这里并不是真正意义上的初始化了一半的矩阵,
        # 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初始化的变换矩阵分布在正弦波上,第二次初始化的矩阵分布在余弦波上,
        # 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵。
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))  # 2 * 1
        pe[:, 0::2] = torch.sin(position * div_term)  # d_model 偶数列插入
        pe[:, 1::2] = torch.cos(position * div_term)  # d_model 奇数列插入

        # 这样我们就得到了位置编码矩阵 pe,pe 现在还只是一个二维矩阵,要想和 embedding 的输出(一个三维张量)相加,
        # 就必须拓展一个维度,所以这里使用 unsqueeze 拓展维度。
        pe = pe.unsqueeze(0)  # 1 * 5000 * 512

        # 最后把 pe 位置编码矩阵注册成模型的 buffer,什么是 buffer 呢,
        # 我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤近似进行更新的增益对象。
        # 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载。
        self.register_buffer('pe', pe)

    def forward(self, x):
        """forward 函数的参数是 x,表示文本序列的词嵌入表示"""
        # 在相加之前我们对 pe 做一些适配工作,将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的 x 的第二维相同即 x.size(1),
        # 因为我们默认 max_len 为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要进行与输入张量的适配。
        # 最后使用 Variable 进行封装,使其与 x 的样式相同,但是它是不需要进行梯度求解的,因此把 requires_grad 设置成 False。
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)  # 1 * x.size(1) * 512
        # 最后使用 self.dropout 对象进行‘丢弃’操作,并返回结果。
        return self.dropout(x)

  • nn.Dropout 演示:
m = nn.Dropout(p=0.2)
input = torch.rand(4, 5)
output = m(input)
output

"""
tensor([[0.4195, 0.6860, 1.0761, 0.0000, 0.0000],
        [0.1527, 1.1528, 0.9567, 0.8003, 0.7162],
        [1.1279, 0.3140, 0.2020, 0.0000, 1.1303],
        [0.2858, 0.0206, 0.0000, 1.0863, 0.4558]])
[torch.FloatTensor of size 4x5]
"""

  • torch.unsqueeze 演示:
x = torch.tensor([1, 2, 3, 4])
torch.unsqueeze(x, 0)

"""
tensor([[1, 2, 3, 4]])
"""

torch.unsqueeze(x, 1)

"""
tensor([[1],
        [2],
        [3],
        [4]])
"""

  • 实例化参数:
# 词嵌入维度是512维
d_model = 512

# 置0比率是0.1
dropout = 0.1

# 句子最大长度
max_len = 60

  • 输入参数:
# 输入 x 是 Embedding 层的输出的张量,形状是 2 x 4 x 512
x = embr

Variable contining:
(0,.,.)=
    18.0835,  18.4307,  25.7455,  ...,  18.6767,  15.9241,   6.8471
    11.9021,  -8.0713, -26.0825,  ...,  10.6016, -31.4030,  10.6847
    9.1287,  17.2753,  14.2908,  ...,   4.8957,   7.2101, -18.9200
    14.0852, -38.7575,  46.5348,  ...,  -3.5360,  10.5081, -10.5434

(1,.,.)=
    -25.3910,  21.7453,  18.1822,  ...,  -4.4432,  -8.6873, -31.0001
    -6.7744,  16.6366,  13.0639,  ..., -27.6570, -25.0405,   6.2767
    24.7707, -19.4100, -12.0361,  ...,  12.6369,   6.8837,  -0.7358
    -26.4784,  -9.7061, -20.1255,  ...,  49.6167, -16.2520,  -1.8203
[torch.FolatTensor of size 2x4x512]

  • 调用:
pe = PositionalEncoding(d_model, dropout, max_len)
pe_result = pe(x)
print("pe_result:", pe_result)

"""
pe_result: Variable containing:
(0,.,.)=
    18.0835,  18.4307,  25.7455,  ...,  18.6767,  15.9241,   6.8471
    11.9021,  -8.0713, -26.0825,  ...,  10.6016, -31.4030,  10.6847
    9.1287,  17.2753,  14.2908,  ...,   4.8957,   7.2101, -18.9200
    14.0852, -38.7575,  46.5348,  ...,  -3.5360,  10.5081, -10.5434

(1,.,.)=
    -25.3910,  21.7453,  18.1822,  ...,  -4.4432,  -8.6873, -31.0001
    -6.7744,  16.6366,  13.0639,  ..., -27.6570, -25.0405,   6.2767
    24.7707, -19.4100, -12.0361,  ...,  12.6369,   6.8837,  -0.7358
    -26.4784,  -9.7061, -20.1255,  ...,  49.6167, -16.2520,  -1.8203
torch.FolatTensor of size 2x4x512
"""
***
* 绘制词汇向量中特征的分布曲线:
import numpy as np
import matplotlib.pyplot as plt

# 创建一张 5 x 5 大小的画布
plt.figure(figsize=(15, 5))

# 实例化 PositionalEncoding 类得到的 pe 对象,输入参数是 20 和 0
pe = PositionalEncoding(20, 0)

# 然后向 pe 传入被 Variable 封装的 tensor,这样 pe 会直接执行 forward 函数,
# 且这个 tensor 里的数值都是0,被处理后相当于位置编码张量
y = pe(Variable(torch.zeros(1, 100, 20)))

# 然后定义画布的横纵坐标,横坐标到100的长度,纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# 因为总共有20维之多,我们这里只查看4,5,6,7维的值。
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())

# 在画布上填写维度信息提示
plt.legend(['dim %d' %p for p in [4, 5, 6, 7]])

在这里插入图片描述


  • 输出效果分析:
    • 每条颜色的曲线代表某一词汇中的特征在不同位置的含义
    • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
    • 正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小,有助于梯度的快速计算。

小节总结:

  • 学习了文本嵌入层的作用:
    • 无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字(先将文本 word2index 成数字)表示转变为向量表示,希望在这样的高维空间捕捉词汇间的关系。
  • 学习并实现了文本嵌入层的类:Embeddings
    • 初始化函数以 d_model,词嵌入维度,和 vocab,词汇总数为参数,内部主要使用了 nn 中的预定层 Embedding 进行词嵌入。
    • 在 forwarding 函数中,将输入 x 传入到 Embedding 的实例化对象中,然后乘以一个根号下 d_model 进行缩放,控制数值的大小。
    • 它的输出是文本嵌入后的结果
  • 学习了位置编码器的作用:
    • 因为在 Transformer 的编码器结构中,并没有针对词汇位置信息的处理,因此需要在 Embedding 层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中,以弥补位置信息的缺失。
  • 学习并实现了位置编码器的类:PositionalEncoding
    • 初始化函数以 d_model, dropout, max_len 为参数,分别代表 d_model:词嵌入维度,dropout:置0比率,max_len:每个句子的最大长度。
    • forward 函数中的输入参数为 x(x + pe),是 Embedding 层的输出。
    • 最终输出一个加入了位置编码信息的词嵌入向量。
  • 实现了绘制词向量中特征的分布曲线:
    • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化。
    • 正弦波和余弦波的值域范围都是1到-1,这又很好的控制了嵌入数值的大小,有助于梯度的快速计算。

2.3 编码器部分实现

  • 学习目标:
    • 了解编码器中各个组成部分的作用。
    • 掌握编码器中各个组成部分的实现过程。

  • 编码器部分:
    • 由 N 个编码器层堆叠而成。
    • 每个编码器层由两个子层连接结构组成。
    • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接。
    • 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接。
      在这里插入图片描述

2.3.1 掩码张量

  • 了解什么是掩码张量以及它的作用。
  • 掌握生成掩码张量的实现过程。

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

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

  • 生成掩码张量的代码分析:
import torch
import numpy as np

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)

  • np.triu 演示:
np.triu([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], k=-1)

"""
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 0,  8,  9],
       [ 0,  0, 12]])
"""

np.triu([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], k=0)

"""
array([[1, 2, 3],
       [0, 5, 6],
       [0, 0, 9],
       [0, 0, 0]])
"""

np.triu([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], k=1)

"""
array([[0, 2, 3],
       [0, 0, 6],
       [0, 0, 0],
       [0, 0, 0]])
"""

  • 输入实例:
# 生成的掩码张量的最后两维的大小
size = 5

  • 调用:
sm = subsequent_mask(size)
print('sm:', sm)

"""最后两维形成一个下三角阵
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)
torch.ByteTensor of size 1x5x5
"""

  • 掩码张量的可视化:
plt.figure(figsize=(5, 5))
plt.imshow(subsequent_mask(20)[0])  # 把第0维取出来(0或者1),从内到外为的索引为:0,1,2

  • 输出效果:
    在这里插入图片描述

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

2.3.1 掩码张量总结

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

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

  • 学习并实现了生成向后遮掩的掩码张量函数:subsequent_mask
    • 它的输入是 size,代表掩码张量的大小。
    • 它的输出是一个最后两维形成1方阵的下三角阵。
    • 最后对生成的掩码张量进行了可视化分析,更深一步理解了它的用途。

2.3.2 注意力机制

  • 学习目标:
    • 了解什么是注意力计算规则和注意力机制。
    • 掌握注意力计算规则的实现过程。

  • 什么是注意力:
    • 我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的),是因为我们的大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果。正是基于这样的理论,就产生了注意力机制。

  • 什么是注意力计算规则:
    • 它需要三个指定的输入Q(query), K(key), V(value),然后通过公式得到注意力的计算结果,这个结果代表 query 在 key 和 value 作用下的表示。而这个具体的计算规则有很多种,我这里只介绍我们用到的这一种。

  • 我们这里使用的注意力的计算规则:
    在这里插入图片描述

  • Q,K,V 的比喻解释:

加入我们有一个问题:给出一段文本,使用一些关键词对它进行描述!
为了方便统一正确回答,这道题可能预先已经给大家写出了一些关键词作为提示,其中这些给出的提示就可以看作是 key,而整个的文本信息就相当于是 query,value 的含义则更抽象,可以比作你看到这段文本信息后,脑子里浮现的答案信息,这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示这些信息,因此 key 与 value 基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来的东西越来越多,并且能够开始对我们 query 也就是这段文本,提取关键信息进行表示,这就是注意力作用的过程,通过这个过程,我们最终脑子里的 value 发生了变化,根据提示 key 生成了 query 的关键词表示方法,也就是另外一种特征表示方法。
刚刚我们说的 key 和 value 一般情况下默认是相同的,与 query 是不同的,这种是我们一般的注意力输入形式,但是有一种特殊情况,就是我们 query 与 key 和 value 相同,这种情况我们称为自注意力机制,就如同我们刚刚的例子,使用一般注意力机制,是使用不同于给定文本的关键词表示它,而自注意力机制,需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它,相当于对文本自身的一次特征提取。


  • 什么是注意力机制:
    • 注意力机制是注意力规则能够应用的深度学习网络的载体,除了注意力计算规则外,还包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体,使用自注意力计算规则的注意力机制称为自注意力机制。

  • 注意力机制在网络中实现的图形表示:
    在这里插入图片描述

  • 注意力计算规则的代码分析:
import torch
import math
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)  # 512
    # 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
    # 得到注意力得分张量 scores
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)  # 4 * 4

    # 接着判断是是否使用掩码张量
    if mask is not None:
        # 使用 tensor 的 masked_fill 方法,将掩码张量和 scores 张量每个位置一一比较,如果掩码张量处为0
        # 则对应的 scores 张量用-1e9这个值来替换,如下演示:
        scores = scores.masked_fill(mask == 0, -1e9)  # 4 * 4

    # 对 scores 的最后一维进行 softmax 操作,使用 F.softmax 方法,第一个参数是 softmax 对象,第二个是目标维度。
    # 这样获得最终的注意力张量
    # 默认是横向 softmax,也就是 dim=1, dim=-1
    p_attn = F.softmax(scores, dim=-1)  # 4 * 4

    # 之后判断是否使用 dropout 进行随机置0
    if dropout is not None:
        # 将 p_attn 传入 dropout 对象中进行‘丢弃’处理。
        p_attn = dropout(p_attn)  # 4 * 4

    # 最后,根据公式将 p_attn 与 value 张量相乘获得最终的 query 注意力表示,同时返回注意力张量。
    return torch.matmul(p_attn, value), p_attn  # 4 * 512,4 * 4

  • tensor.masked_fill 演示:
input = Variable(torch.randn(5, 5))
print(input)

"""
Variable containing:
 2.0344 -0.5450  0.3365 -0.1888 -2.1803
 1.5221 -0.3823  0.8414  0.7836 -0.8481
-0.0345 -0.8643  0.6476 -0.2713  1.5645
 0.8788 -2.2142  0.4022  0.1997  0.1474
 2.9109  0.6006 -0.6745 -1.7262  0.6977
[torch.FloatTensor of size 5x5]
"""

mask = Variable(torch.zeros(5, 5))
print(mask)

"""
Variable containing:
 0  0  0  0  0
 0  0  0  0  0
 0  0  0  0  0
 0  0  0  0  0
 0  0  0  0  0
[torch.FloatTensor of size 5x5]
"""

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

"""
Variable containing:
-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
[torch.FloatTensor of size 5x5]
"""

  • 输入参数:
# 我们令输入的 query,key,value 都相同,位置编码的输出
query = key = value = pe_result

"""
Variable containing:
(0,.,.) = 
	-2.4706,   34.0796,   2.7505,  ...,  -3.4972, -47.7639,  -3.3239
	 19.8931,  21.1933, -22.7047,  ...,  -6.5685, -57.0562,  -8.0796
	 3.4568,    9.4423,  -3.3226,  ...,  -3.1958,  17.6394, -28.5494
	-13.1135,  32.7553,  49.7123,  ...,  17.4657,  47.5091,  12.2267

(1,.,.) = 
	 12.9825,   7.8147,  24.1951,  ...,  22.3257,   9.8316, -15.4033
	 -7.6471,  -8.1521,  -1.1315,  ...,  -0.2127,   3.8205, -19.2361
	-37.0878,  50.6874,  40.1957,  ...,  12.9805, -16.8716, -35.2725
	 5.0624,    0.4235, -17.8792,  ...,  16.9755, -36.1472,   7.2271
[torch.FloatTensor of size 2x4x512]
"""

  • 调用:
attn, p_attn = attention(query, key, value)
print('attn:', attn)
print('p_attn:', p_attn)

# 得到两个结果
# query 的注意力表示:
"""
attn: Variable containing:
(0,.,.) = 
	-2.4706,   34.0796,   2.7505,  ...,  -3.4972, -47.7639,  -3.3239
	 19.8931,  21.1933, -22.7047,  ...,  -6.5685, -57.0562,  -8.0796
	 3.4568,    9.4423,  -3.3226,  ...,  -3.1958,  17.6394, -28.5494
	-13.1135,  32.7553,  49.7123,  ...,  17.4657,  47.5091,  12.2267

(1,.,.) = 
	 12.9825,   7.8147,  24.1951,  ...,  22.3257,   9.8316, -15.4033
	 -7.6471,  -8.1521,  -1.1315,  ...,  -0.2127,   3.8205, -19.2361
	-37.0878,  50.6874,  40.1957,  ...,  12.9805, -16.8716, -35.2725
	 5.0624,    0.4235, -17.8792,  ...,  16.9755, -36.1472,   7.2271
[torch.FloatTensor of size 2x4x512]
"""
# 注意力张量:第一行代表第一个向量与其他向量之间的关系程度,1完全相关,0完全无关
"""
p_attn: Variable containing:
(0 ,.,.) = 
  1  0  0  0
  0  1  0  0
  0  0  1  0
  0  0  0  1

(1 ,.,.) = 
  1  0  0  0
  0  1  0  0
  0  0  1  0
  0  0  0  1
[torch.FloatTensor of size 2x4x4]

"""

  • 带有 mask 的输入参数:
query = key = value = pe_result

# 令 mask 为一个 2x4x4 的0张量
mask = Variable(torch.zeros(2, 4, 4))

  • 调用:
attn, p_attn = attention(query, key, value, mask=mask)
print('attn:', attn)
print('p_attn:', p_attn)

# query 的注意力表示
"""
```python
attn: Variable containing:
( 0 ,.,.) = 
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770

( 1 ,.,.) = 
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
[torch.FloatTensor of size 2x4x512]
"""
# 注意力张量:第一行代表第一个向量与其他向量之间的关系程度,1完全相关,0完全无关
"""
p_attn: Variable containing:
(0 ,.,.) = 
  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

(1 ,.,.) = 
  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
[torch.FloatTensor of size 2x4x4]
"""

2.3.3 多头注意力机制

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

  • 多头注意力机制结构图:
    在这里插入图片描述

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

  • 多头注意力机制的代码实现:
# 用于深度拷贝的 copy 工具包
import copy

# 首先需要定义克隆函数,因为在多头注意力机制的实现中,用到多个结构相同的线性层。
# 我们将使用 clone 函数将它们一同初始化在一个网络层列表对象中,之后的结构中也会用到该函数。
def clones(module, N):
    """用于生成相同网络层的克隆函数,它的参数 module 表示要克隆的目标网络层,N 代表需要克隆的数量"""
    # 在函数中,我们通过 for 循环对 module 进行 N 次深度拷贝,使其每个 module 称为独立的层。
    # 然后将其放在 nn.ModuleList 类型的列表中存放。
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])


# 我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        """在类的初始化时,会传入三个参数,head 代表头数,embedding_dim 代表词嵌入的维度,
        dropout 代表进行 dropout 操作时置0比率,默认是 0.1"""
        super(MultiHeadedAttention, self).__init__()

    	# 在函数中,首先使用了一个测试中常用的 assert 语句,判断 h 是否能被 d_model 整除,
    	# 这是因为我们之后要给每个头分配等量的词特征,也就是 embedding_dim/head 个。
        assert embedding_dim % head == 0

    	# 得到每个头获得的分割词向量维度 d_k
        self.d_k = embedding_dim // head

    	# 传入头数 h
        self.head = head

    	# 然后获得线性层对象,通过 nn 的 Linear 实例化,它的内部变换矩阵是 embedding_dim x embedding_dim,然后使用 clones 函数克隆四个,
		# 为什么是四个呢,这是因为在多头注意力中,Q,K,V 个需要一个,最后拼接的矩阵还需要一个,因此一共是四个。
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)

    	# self.atten 为 None,它代表最后得到的注意力张量,现在还没有结果所以为 None
        self.attn = None

    	# 最后就是一个 dropout 对象,它通过 nn 中的 Dropout 实例化而来,置0比率为传进来的参数 dropout。
        self.dropout = nn.Dropout(dropout)

    def forward(self, query, key, value, mask=None):
        """前向逻辑函数,它的输入参数有四个,前三个就是注意力机制需要的 Q,K,V,
        最后一个是注意力机制中可能需要的 mask 掩码张量,默认是 None"""

        # 如果存在掩码张量 mask
        if mask is not None:
            # 使用 unsqueeze 扩展维度
             mask = mask.unsqueeze(0)

        # 接着,我们获得一个 batch_size 的变量,它是 query 尺寸的第一个数字,代表有多少条样本。
        batch_size = query.size(0)

        # 之后就进入多头处理环节
        # 首先利用 zip 将输入 QKV 与三个线性层组到一起,然后使用 for 循环,将输入 QKV 分贝传到线性层中,
        # 做完线性变换后,开始为每个头分割输入,这里使用 view 方法对线性变换的结果进行维度重塑,多加了一个维度 h,代表头数
        # 这样就意味着每个头可以获得一部分词特征组成的句子,其中的 -1 代表自适应维度
        # 计算机会根据这种变换自动计算这里的值,然后对第二维和第三维进行转置操作,
        # 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
        # 从 attention 函数中可以看到,利用的是原始输入的倒数第一和第二维,这样我们就得到了每个头的输入。
        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))]

        # 得到每个头的输入后,接下来就是将它们传入到 attention 中,
        # 这里直接调用我们之前实现的 attention 函数,同时也将 mask 和 dropout 传入其中。
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

        # 通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,我们需要将其转换为输入的形状以方便后续的计算,
        # 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维转置,然后使用 contiguous 方法,
        # 这个方法的作用就是能够让转置后的张量应用 view 方法,否则将无法直接使用,
        # 所以,下一步就是使用 view 重塑形状,变成和输入形状相同。
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)

        # 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终多头注意力结构的输出。
        return self.linears[-1](x)

  • tensor.masked_fill 演示:
x = torch.randn(4, 4)
x.size()

"""
torch.Size([4, 4])
"""

y = x.view(16)
y.size()

"""
torch.Size([16])
"""

z = x.view(-1, 8)  # the size -1 is inferred from other dimensions
y.size()

"""
torch.Size([2, 8])
"""

a = torch.randn(1, 2, 3, 4)
a.size()

"""
torch.Size([1, 2, 3, 4])
"""

b = a.transpose(1, 2)  # Swaps 2nd and 3rd dimension
b.size()

"""
torch.Size([1, 3, 2, 4])
"""

c = a.view(1, 3, 2, 4)  # Does not change tensor layout in memory
c.size()

"""
torch.Size(1, 3, 2, 4)
"""

torch.equal(b, c)
"""
False
"""

  • torch.transpose 演示:
x = torch.randn(2, 3)
x

"""
tensor([[ 1.0028, -0.9893,  0.5809],
        [-0.1669,  0.7299,  0.4942]])
"""

torch.transpose(x, 0, 1)

"""
tensor([[ 1.0028, -0.1669],
        [-0.9893,  0.7299],
        [ 0.5809,  0.4942]])
"""

  • 实例化参数:
# 头数 head
head = 8

# 词嵌入维度 embedding_dim
embedding_dim = 512

# 置零比率 dropout
dropout = 0.2

  • 输入参数:
# 假设输入的 Q,K,V 仍然相等
query = value = key = pe_result

# 输入的掩码张量
mask = Variable(torch.zeros(8, 4, 4))

  • 调用:
mha = MultiHeadedAttention(head, embedding_dim, dropout)
mha_result = mha(query, key, value, mask)
print(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])
"""

2.3.4 前馈全连接层

  • 学习目标:
    • 了解什么是前馈层及其作用。
    • 掌握前馈全连接层的实现过程

  • 什么是前馈全连接层:
    • 在 Transformer 中前馈全连接层就是具有两层线性层的全连接网络。

  • 前馈全连接层的作用:
    • 考虑注意力机制可能对复杂过程的拟合程度不够,通过增加两层网络来增强模型的能力。

  • 前馈全连接层的代码分析:
# 通过类 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 函数进行激活,
        # 之后再使用 droput 进行随机置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.5 规范化层

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

  • 规范化层的代码实现:
# 通过 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(feature, 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])
"""

2.3.5 规范化层总结:

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

  • 学习并实现了规范化层的类:LayerNorm
    • 它的实例化参数有两个,features 和 eps,分别表示词嵌入特征大小,和一个足够小的数。
    • 它的输入参数 x 代表来自上一层的输出。
    • 它的输出就是经过规范化的特征表示。

2.3.6 子层连接结构

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

  • 子层连接结构图:
    在这里插入图片描述

  • 子层连接结构代码分析:
# 使用 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 中预定义的 dropout 实例化一个 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])
"""

2.3.6 子层连接结构总结:

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

  • 学习并实现了子层连接结构的类:SublayerConnection
    • 类的初始化函数输入参数是 size,dropout,分别代表词嵌入大小和置零比率。
    • 它的实例化对象输入参数是 x,sublayer,分别代表上一层输出以及子层的函数表示。
    • 它的输出就是通过子层连接结构处理的输出。

2.3.7 编码器层

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

  • 编码器层的构成图:
    在这里插入图片描述

  • 编码器层的代码分析:
# 使用 EncoderLayer 类实现编码层
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        """它的初始化函数参数有四个,分别是 size,其实就是我们词嵌入维度的大小,它也将作为我们编码器层的大小,
        第二个 self_attn,之后我们将传入多头子注意力子层实例化对象,并且是自注意力机制,
        第三个是 feed_forward,之后我们将传入前馈全连接层实例化对象,最后一个是置0比率。"""
        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([[[ 1.9921e+01,  3.3425e+01,  6.1629e+00,  ...,  6.2678e+01,
           2.9171e+01,  1.1156e+01],
         [ 2.3325e-01,  8.0859e-02, -3.5207e+01,  ...,  3.7640e+01,
          -1.7566e+01,  1.2176e+01],
         [ 4.4443e+00,  4.8411e+01,  3.3100e+01,  ...,  1.9235e+01,
           1.5410e+01, -1.3317e-02],
         [-5.4771e+00,  2.3913e+01,  3.2659e+01,  ...,  3.3492e+01,
          -1.5238e+01, -1.7955e+01]],

        [[-1.0243e+01, -7.3980e+00,  4.0956e+01,  ...,  1.2323e+01,
           4.2930e+00,  1.0701e+01],
         [ 1.3229e+01, -1.2794e+01,  3.0340e+01,  ...,  7.0137e-01,
          -1.1191e+01, -1.1884e+01],
         [-1.0737e+01,  1.7524e+01,  3.9035e+01,  ..., -1.7176e+01,
          -2.2373e+01, -2.2339e+00],
         [-1.0236e+01,  2.0050e+01, -3.4379e+01,  ...,  3.6887e+01,
           7.5028e+00,  1.4658e+01]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
"""

2.3.7 编码器层总结:

  • 学习了编码器层的作用:
    • 作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程,即编码过程。

  • 学习并实现了编码器层的类:Encoder_layer
    • 类的初始化函数共有4个,分别是 size,其实就是我们词嵌入维度的大小。第二个是 self_attn,之后我们将传入多头自注意力子层实例化对象,并且是自注意力机制。第三个是 feed_forward,之后我们将传入前馈全连接层实例化对象。最后一个是置0比率 dropout。
    • 实例化对象的输入参数有2个,x 代表来自上一层的输出,mask 代表掩码张量。
    • 它的输出代表经过整个编码层的特征表示。

2.3.8 编码器

  • 编码器的作用:
    • 编码器用于对输入进行指定的特征提取过程,也称为编码,由 N 个编码器层堆叠而成。

  • 编码器的结构图:
    在这里插入图片描述

  • 编码器的代码分析:
# 使用 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.9845,  1.3802,  0.0177,  ...,  2.7576,  1.1826,  0.5717],
         [ 0.0362, -0.1015, -1.6067,  ...,  1.4773, -0.8498,  0.6493],
         [ 0.3246,  1.7864,  1.2121,  ...,  0.9372,  0.5335,  0.1605],
         [-0.2135,  0.8947,  1.2349,  ...,  1.3519, -0.9314, -0.5688]],

        [[-0.6183, -0.1443,  1.6978,  ...,  0.5230, -0.0546,  0.7401],
         [ 0.3202, -0.5143,  0.9937,  ..., -0.0110, -0.6322, -0.3755],
         [-0.5319,  0.7588,  1.5268,  ..., -0.8102, -1.0899,  0.0276],
         [-0.5355,  0.9336, -1.6778,  ...,  1.6370,  0.2085,  0.7601]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
"""

2.4 解码器部分实现

  • 解码器部分:
    • 由 N 个解码器层堆叠而成
    • 每个解码器层由三个子层连接结构组成
    • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
    • 第二个子层连接结构包括一个多头注意力层和规范化层以及一个残差连接
    • 第三个子层连接结构包括一个前馈全连接层和规范化层以及一个残差连接
      在这里插入图片描述

  • 说明:
    • 解码器层中的各个部分,如,多注意力机制,规范化层,前馈全连接网络,子层连接结构都与编码其中的实现相同。因此这里可以直接拿过来构建解码器层。

2.4.1 解码器层

  • 解码器层的作用:
    • 作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程。

  • 解码器层的代码实现:
# 使用 DecoderLayer 的类实现解码器层
class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        """初始化函数的参数有5个,分别是 size,代表词嵌入的维度大小,同时也代表解码器层的尺寸,
        第二个是 self_attn,多头自注意力对象,也就是说这个注意力机制需要 Q=K=V,
        第三个是 src_attn,常规的多头注意力对象,这里 Q!=K=V,第四个是前馈全连接层对象,最后就是 dropout 置0比率。"""
        super(DecoderLayer, self).__init__()
        # 在初始化函数中,主要就是将这些输入传到类中
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        # 按照结构图使用 clones 函数克隆三个子层连接对象。
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, source_mask, target_mask):
        """forward 函数中的参数有4个,分别是来自上一层的输入 x,
        来自编码器层的与已存储变量 memory,以及源数据掩码张量和目标数据掩码张量。"""
        # 将 memory 表示成 m 方便之后使用
        m = memory

        # 将 x 传入第一个子层结构,第一个子层结构的输入分别是 x 和 self_attn 函数,因为是自注意力机制,所以 Q,K,V 都是 x,
        # 最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因此此时模型可能还没有生成任何目标数据,
        # 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符一遍计算损失,
        # 但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时,
        # 模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用。
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))

        # 接着进入第二个子层,这个子层中常规的注意力机制,q 是输入 x,k,v 是编码器层输出 memory,
        # 同样也传入 source_mask,但是进行源数据遮掩的原因并非是抑制信息泄露,而是遮蔽掉对结果没有意义的字符而产生的注意力值,
        # 以此提升模型效果和训练速度,这样就完成了第二个子层的处理。
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))

        # 最后一个子层就是前馈全连接子层,经过它的处理后就可以返回结果。这就是我们的解码器层结构。
        return self.sublayer[2](x, self.feed_forward)

  • 实例化参数:
# 类的实例化参数与解码器层类似,相比多出了 src_attn,但是和 self_attn 是同一个类。
head = 8
size = 512
d_model = 512
d_ff = 64
dropout = 0.2
self_attn = src_attn = MultiHeadedAttention(head, d_model, dropout)

# 前馈全连接层也和之前相同
ff = PositionwiseFeedForward(d_model, d_ff, dropout)

  • 输入参数:
# x 是来自目标数据的词嵌入表示,但形式和元数据的词嵌入表示相同,这里使用 per 充当。
x = pe_result

# memory 是来自编码器的输出
memory = en_result

# 实际中 source_mask 和 target_mask 并不相同,这里为了方便计算使它们都为 mask
mask = Variable(torch.zeros(8, 4, 4))
source_mask = target_mask = mask

  • 调用:
dl = DecoderLayer(size, self_attn, src_attn, ff, dropout)
dl_result = dl(x, memory, source_mask, target_mask)
print(dl_result)
print(dl_result.shape)

"""
tensor([[[ 4.4165e+00,  3.9459e+01, -1.8993e+01,  ...,  2.8640e+01,
           4.5007e+01, -2.6191e+00],
         [-3.1433e+01, -3.3698e-02,  9.7387e+00,  ..., -3.1304e+01,
          -2.6359e+01,  2.5803e+01],
         [ 1.0853e-01, -8.5879e+00, -3.1416e+01,  ...,  9.3420e+00,
           9.9350e+00, -4.9279e+01],
         [-2.7589e+00, -1.2277e+01, -1.7585e+01,  ...,  5.6965e+00,
           2.1629e+00, -3.1873e+00]],

        [[ 1.5855e+01, -2.0839e+01,  2.7341e+01,  ..., -2.9785e+01,
           5.4703e+01, -5.4269e+01],
         [-2.5164e+01,  2.5174e+01,  4.4220e+01,  ...,  6.1001e+01,
           1.7924e+01,  1.7182e+01],
         [-1.4131e-01, -9.9527e-02, -1.1019e+01,  ...,  3.1327e+01,
           1.4808e+01, -3.9333e+00],
         [-2.6996e+01, -2.0339e+01,  7.2715e-02,  ..., -1.0746e+01,
           2.1724e+01,  9.0271e+00]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
"""

2.4.1 解码器层总结:

  • 学习了解码器层的作用:
    • 作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程。

  • 学习并实现了解码器层的类:DecoderLayer
    • 类的初始化函数的参数有5个,分别是 size,代表词嵌入的维度大小,同时也代表解码器层的尺寸,第二个是 self_attn,多头自注意力对象,也就是说这个注意力机制需要 Q=K=V,第三个是 src_attn,多头注意力对象,这里 Q!=K=V,第四个是前馈全连接层对象,最后就是 dropout 置0比率。
    • forward 函数的参数有4个,分别是来自上一层的输入 x,来自编码器层的语义存储变量 memory,以及源数据掩码张量和目标数据掩码张量。
    • 最终输出了由编码器输入和目标数据一同作用的特征提取结果。

2.4.2 解码器

  • 解码器的作用:
    • 根据编码器的结果以及上一次预测的结果,对下一次可能出现的“值”进行特征表示。

  • 解码器的代码分析:
# 使用类 Decoder 来实现解码器
class Decoder(nn.Module):
    def __init__(self, layer, N):
        """初始化函数的参数有两个,第一个就是解码器层 layer,第二个是解码器层的个数 N。"""
        super(Decoder, self).__init__()
        # 首先使用 clones 方法克隆了 N 个 layer,然后实例化了一个规范化层。
        # 因为数据走过了所有解码器层最后要做规范化处理。
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, source_mask, target_mask):
        """forward 函数中的参数有4个,x 代表目标数据的嵌入表示,memory 是编码器层的输出,
        source_mask,target_mask 代表元数据和目标数据的掩码张量"""

        # 然后就是对每个层进行循环,当然这个循环就是变量 x 通过每一个层的处理,
        # 得出最后的结果,再进行一次规范化返回即可。
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        return self.norm(x)

  • 实例化参数:
# 分别是解码器层 layer 和解码器层的个数 N
size = 512
d_model = 512
head = 8
d_ff = 64
dropout = 0.2
c = copy.deepcopy
attn = MultiHeadedAttention(head, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
layer = DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout)
N = 8

  • 输入参数:
# 输入参数与解码器层的输入参数相同
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8, 4, 4))
source_mask = target_mask = mask

  • 调用:
de = Decoder(layer, N)
de_result = de(x, memory, source_mask, target_mask)
print(de_result)
print(de_result.shape)

"""
tensor([[[-0.0100,  1.6852, -0.7575,  ...,  1.0972,  1.6985, -0.1922],
         [-1.3588, -0.1017,  0.3757,  ..., -1.3335, -1.5396,  0.9734],
         [-0.2805, -0.2980, -1.4023,  ...,  0.2735,  0.0846, -2.1428],
         [-0.1335, -0.3747, -0.5711,  ...,  0.1190, -0.0894, -0.0129]],

        [[ 0.7397, -0.9673,  1.0315,  ..., -1.4146,  2.1222, -2.3236],
         [-0.9619,  0.8483,  1.7075,  ...,  2.2987,  0.6200,  0.5135],
         [ 0.1447,  0.0146, -0.5066,  ...,  1.0641,  0.3366, -0.3634],
         [-0.8395, -0.8780, -0.1027,  ..., -0.4926,  0.7596,  0.2395]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
"""

2.4.2 解码器总结

  • 学习了解码器的作用:
    • 根据编码器的结果以及上一次预测的结果,对下一次可能出现的“值”进行特征表示。

  • 学习并实现了解码器的类:Decoder
  • 类的初始化函数的参数有两个,第一个就是解码器层 layer,第二个是解码器层的个数 N。
  • forward 函数中的参数有4个,x 代表目标数据的嵌入表示,memory 是编码器层的输出,src_mask,tgt_mask 代表元数据和目标数据的掩码张量。
  • 输出解码过程的最终特征表示。

2.5 输出部分实现

  • 输出部分包括
    • 线性层
    • softmax 层
      在这里插入图片描述

  • 线性层的作用:
    • 通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用。
  • softmax 层的作用:
    • 使最后一维的向量中的数字缩放到0-1的概率值域内,并满足它们的和为1。

  • 线性层和 softmax 层的代码分析:
# nn.functional 工具包装载了网络层中那些只进行计算,而没有参数的层
import torch.nn.functional as F

# 将线性层和 softmax 计算层一起实现,因为二者的共同目标是生成最后的结构
# 因此把类的名字叫做 Gennerator,生成器类
class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        """初始化函数的输入参数有两个,d_model 代表词嵌入维度,vocab_size 代表词表大小。"""
        super(Generator, self).__init__()
        # 首先就是使用 nn 中的预定义线性层进行实例化,得到一个 self.project 等待使用,
        # 这个线性层的参数有两个,就是初始化函数传进来的两个参数:d_model, vocab_size
        self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        """前向逻辑函数中输入是上一层的输出张量 x"""
        # 在函数中,首先使用上一步得到的 self.project 对 x 进行线性变化,
        # 然后使用 F 中已经实现的 log_softmax 进行的 softmax 处理。
        # 在这里之所以使用 log_softmax 是因为和我们这个 pytorch 版本的损失函数实现有关,在其他版本中将修复。
        # log_softmax 就是对 softmax 的结果又取了对数,因为对数函数是单调递增函数,
        # 因此对最终我们取最大的概率值没有影响,最后返回结果即可。
        return F.log_softmax(self.project(x), dim=-1)

  • nn.Linear 演示:
m = nn.Linear(20, 30)
input = torch.randn(128, 20)
output = m(input)
print(output.size())

"""
torch.Size([128, 30])
"""

  • 实例化参数:
# 词嵌入维度是512维
d_model = 512

# 词表大小是1000
vocab_size = 1000

  • 输入参数:
# 输入 x 是上一层网络的输出,我们使用来自解码器层的输出
x = de_result
  • 调用:
gen = Generator(d_model, vocab_size)
gen_result = gen(x)
print(gen_result)
print(gen_result.shape)

"""
tensor([[[-6.2595, -7.7115, -6.7399,  ..., -6.2820, -6.5677, -7.8326],
         [-7.5693, -7.0031, -8.5070,  ..., -7.1465, -6.5088, -7.0358],
         [-6.6488, -6.7498, -8.0701,  ..., -7.7222, -6.7835, -6.9165],
         [-7.1597, -6.9232, -7.2582,  ..., -6.2172, -6.8709, -6.6347]],

        [[-6.9703, -7.5174, -8.0617,  ..., -6.7344, -7.5092, -6.8835],
         [-6.6768, -6.9989, -7.0754,  ..., -7.2584, -6.9826, -7.2480],
         [-7.2625, -6.5267, -7.1707,  ..., -7.4458, -6.8468, -8.1583],
         [-7.2854, -7.1081, -7.2286,  ..., -7.0400, -8.2852, -7.4947]]],
       grad_fn=<LogSoftmaxBackward0>)
torch.Size([2, 4, 1000])
"""

2.5 输出部分实现小结:

  • 学习了输出部分包含:
    • 线性层
  • softmax 层

  • 线性层的作用:
    • 通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用。

  • softmax 层的作用:
    • 使最后一维的向量中的数字缩放到0-1的概率值域内,并满足他们的和为1。

  • 学习并实现了线性层和 softmax 层的类:Generator
    • 初始化函数的输入参数有两个,d_model 代表词嵌入维度,vocab_size 代表词表大小。
    • forward 函数接受上一层的输出。
    • 最终获得经过线性层和 softmax 层处理的结果。

2.6 模型构建

  • 学习目标:
    • 掌握编码器-解码器结构的实现过程。
    • 掌握 Transformer 模型的构建过程。

  • 通过上面的小节,我们已经完成了所有组成部分的实现,接下来就来实现完整的编码器-解码器结构。

  • Transformer 总结架构图:
    在这里插入图片描述

  • 编码器-解码器结构的代码实现:
# 构建编码器-解码器结构类
class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, source_embed, target_embed, generator):
        # encoder:代表编码器对象
        # decoder:代表解码器对象
        # source_embed:代表源数据的嵌入函数
        # target_embed:代表目标数据的嵌入函数
        # generator:代表输出部分类别生成器对象
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = source_embed
        self.tgt_embed = target_embed
        self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
        # source:代表源数据
        # target:代表目标数据
        # source_mask:代表源数据的掩码张量
        # target_mask:代表目标数据的掩码张量
        return self.generator(self.decode(self.encode(source, source_mask), source_mask, target, target_mask))

    def encode(self, source, source_mask):
        return self.encoder(self.src_embed(source), source_mask)

    def decode(self, memory, source_mask, target, target_mask):
        # memory:代表经历编码器编码后的输出张量
        return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)

  • 实例化参数:
vocab_size = 1000
d_model = 512
encoder = en
decoder = de
source_embed = nn.Embedding(vocab_size, d_model)
target_embed = nn.Embedding(vocab_size, d_model)
generator = gen

  • 输入参数:
# 假设源数据与目标数据相同,实际中并不相同
source = target = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))

# 假设 src_mask 与 tgt_mask 相同,实际中并不相同
source_mask = target_mask = Variable(torch.zeros(8, 4, 4))

  • 调用:
ed = EncoderDecoder(encoder, decoder, source_embed, target_embed, generator)
ed_result = ed(source, target, source_mask, target_mask)
print(ed_result)
print(ed_result.shape)

"""
tensor([[[ 0.2102, -0.0826, -0.0550,  ...,  1.5555,  1.3025, -0.6296],
         [ 0.8270, -0.5372, -0.9559,  ...,  0.3665,  0.4338, -0.7505],
         [ 0.4956, -0.5133, -0.9323,  ...,  1.0773,  1.1913, -0.6240],
         [ 0.5770, -0.6258, -0.4833,  ...,  0.1171,  1.0069, -1.9030]],

        [[-0.4355, -1.7115, -1.5685,  ..., -0.6941, -0.1878, -0.1137],
         [-0.8867, -1.2207, -1.4151,  ..., -0.9618,  0.1722, -0.9562],
         [-0.0946, -0.9012, -1.6388,  ..., -0.2604, -0.3357, -0.6436],
         [-1.1204, -1.4481, -1.5888,  ..., -0.8816, -0.6497,  0.0606]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 1000])
"""

  • 接下来将基于以上结构构建用于训练的模型。
  • Trsnsformer 模型构建过程的代码分析:
def make_model(source_vocab, target_vocab, N=6, d_model=512, d_ff=2048, head=8, dropout=0.1):
    """该函数用来构建模型,有7个参数,分别是源数据特征(词汇)总数,目标数据特征(词汇)总数,
    编码器和解码器堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,
    多头注意力结构中的多头数,以及置零比率 dropout。"""

    # 首先得到一个深度拷贝命令,接下来很多结构都需要进行深度拷贝,
    # 来保证他们彼此之间相互独立,不受干扰。
    c = copy.deepcopy

    # 实例化了多头注意力类,得到对象 attn
    attn = MultiHeadedAttention(head, d_model)

    # 然后实例化前馈全连接类,得到对象 ff
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)

    # 实例化位置编码类,得到对象 position
    position = PositionalEncoding(d_model, dropout)

    # 根据结构图,最外层是 EncoderDecoder,在 EncoderDecoder 中,
    # 分别是编码器层,解码器层,源数据 Embedding 层和位置编码组成的有序结构,
    # 目标数据 Embedding 层和位置编码组成的有序结构,以及类别生成器层,
    # 在编码器层中有 attention 子层以及前馈全连接子层,
    # 在解码器层中有两个 attention 子层以及前馈全连接层。
    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, source_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, target_vocab), c(position)),
        Generator(d_model, target_vocab)
    )

    # 模型结构完成后,接下来就是初始化模型中的参数,比如线性层中的变换矩阵
    # 这里一旦判断参数的维度大于1,则会将其初始化成一个服从均匀分布的矩阵,
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform(p)
    return model

  • nn.init.xavier_uniform 演示:
# 这里服从均匀分布 U(-a, a)
w = torch.empty(3, 5)
w = nn.init.xavier_uniform(w, gain=nn.init.calculate_gain('relu'))
print(w)

"""
tensor([[ 0.1434,  1.0356,  1.0044, -0.3106, -0.6682],
        [-0.4416,  0.6925,  1.1832, -0.5946, -1.2158],
        [-0.6423, -0.1506,  0.0246,  0.2574,  0.5394]])
"""

  • 输入参数:
source_vocab = 11
target_vocab = 11
N = 6

# 其他参数都使用默认值

  • 调用:
if __name__ == '__main__':
    res = make_model(source_vocab, target_vocab, N)
    print(res)
  • 输出结果:
# 根据Transformer结构图构建的最终模型结构
EncoderDecoder(
  (encoder): Encoder(
    (layers): ModuleList(
      (0): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (feed_forward): PositionwiseFeedForward(
          (w_1): Linear(in_features=512, out_features=2048)
          (w_2): Linear(in_features=2048, out_features=512)
          (dropout): Dropout(p=0.1)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (1): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
        )
      )
      (1): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (feed_forward): PositionwiseFeedForward(
          (w_1): Linear(in_features=512, out_features=2048)
          (w_2): Linear(in_features=2048, out_features=512)
          (dropout): Dropout(p=0.1)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (1): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
        )
      )
    )
    (norm): LayerNorm(
    )
  )
  (decoder): Decoder(
    (layers): ModuleList(
      (0): DecoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (src_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (feed_forward): PositionwiseFeedForward(
          (w_1): Linear(in_features=512, out_features=2048)
          (w_2): Linear(in_features=2048, out_features=512)
          (dropout): Dropout(p=0.1)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (1): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (2): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
        )
      )
      (1): DecoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (src_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512)
            (1): Linear(in_features=512, out_features=512)
            (2): Linear(in_features=512, out_features=512)
            (3): Linear(in_features=512, out_features=512)
          )
          (dropout): Dropout(p=0.1)
        )
        (feed_forward): PositionwiseFeedForward(
          (w_1): Linear(in_features=512, out_features=2048)
          (w_2): Linear(in_features=2048, out_features=512)
          (dropout): Dropout(p=0.1)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (1): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
          (2): SublayerConnection(
            (norm): LayerNorm(
            )
            (dropout): Dropout(p=0.1)
          )
        )
      )
    )
    (norm): LayerNorm(
    )
  )
  (src_embed): Sequential(
    (0): Embeddings(
      (lut): Embedding(11, 512)
    )
    (1): PositionalEncoding(
      (dropout): Dropout(p=0.1)
    )
  )
  (tgt_embed): Sequential(
    (0): Embeddings(
      (lut): Embedding(11, 512)
    )
    (1): PositionalEncoding(
      (dropout): Dropout(p=0.1)
    )
  )
  (generator): Generator(
    (proj): Linear(in_features=512, out_features=11)
  )
)

猜你喜欢

转载自blog.csdn.net/weixin_51524504/article/details/130366152