当前位置:   article > 正文

Transformer架构解析_transformers架构图

transformers架构图

1. Transformer总体架构图

  • Transformer总体架构可分为四个部分:

    • 输入部分

    • 输出部分

    • 编码器部分

    • 解码器部分


  • 输入部分包含:

    • 源文本嵌入层及其位置编码器

    • 目标文本嵌入层及其位置编码器

avatar

 

  • 输出部分包含:

    • 线性层

    • softmax层

avatar


  • 编码器部分:

    • 由N个编码器层堆叠而成

    • 每个编码器层由两个子层连接结构组成

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

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

avatar


  • 解码器部分:

    • 由N个解码器层堆叠而成

    • 每个解码器层由三个子层连接结构组成

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

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

    • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

avatar


 

2  输入部分实现

 

  • 输入部分包含:

    • 源文本嵌入层及其位置编码器

    • 目标文本嵌入层及其位置编码器

avatar

 


文本嵌入层的作用

  • 无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示(最简单的向量表示就是one-hot), 希望在这样的高维空间捕捉词汇间的关系,进行更多的语意表示(因为通过word2index,文本中词汇中每个词只能用一个数字表示,比如在前边案例中word2index= {'hello': 2, 'I': 3, 'am': 4, 'Jay': 5},但是这样的数字仅仅是一维的数字,不能直接输入到模型中,而必须将它转化为向量,这样可以映射成更高维度)


  • 文本嵌入层的代码分析: 
  1. # 导入必备的工具包
  2. import torch
  3. import numpy as np
  4. # 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层,
  5. # 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子.
  6. import torch.nn as nn
  7. import torch.nn.functional as F
  8. # 数学计算工具包
  9. import math
  10. # torch中变量封装函数Variable.
  11. from torch.autograd import Variable
  12. # 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
  13. # 该类继承nn.Module, 这样就有标准层的一些功能, 这里我们也可以理解为一种模式, 我们自己实现的所有层都会这样去写.
  14. class Embeddings(nn.Module):
  15. def __init__(self, d_model, vocab):
  16. """类的初始化函数, 有两个参数, d_model: 指词嵌入的维度, vocab: 指词表的大小."""
  17. # 接着就是使用super的方式指明继承nn.Module的初始化函数, 我们自己实现的所有层都会这样去写.
  18. super(Embeddings, self).__init__()
  19. # 之后就是调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lut
  20. self.lut = nn.Embedding(vocab, d_model)
  21. # 最后就是将d_model传入类中
  22. self.d_model = d_model
  23. def forward(self, x):
  24. """可以将其理解为该层的前向传播逻辑,所有层中都会有此函数
  25. 当传给该类的实例化对象参数时, 自动调用该类函数
  26. 参数x: 因为Embedding层是首层, 所以代表输入给模型的文本通过词汇映射后的张量"""
  27. # 将x传给self.lut并与根号下self.d_model(缩放系数)相乘作为结果返回,乘以缩放系数的目的是为了更好更快的去收敛
  28. return self.lut(x) * math.sqrt(self.d_model)
  • 实例化参数和对象:
  1. # 词嵌入维度是512维
  2. d_model = 512
  3. # 词表大小是1000
  4. vocab = 1000
  5. # 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4
  6. x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
  7. #调用
  8. emb = Embeddings(d_model, vocab)
  9. embr = emb(x)
  10. print(embr)
  11. #由结果可知, embr的形状是 2x4x512,它的含义是:样本个数x序列长度x 词嵌入维度
  12. print(embr.shape)

  • 输出效果:

  1. tensor([[[ 6.2476e+00, 6.7233e-01, 2.0227e+01, ..., 3.2159e+01,
  2. -2.8141e+01, -2.7829e+00],
  3. [ 2.6374e+00, 9.5352e+00, 2.5945e+01, ..., -2.5476e+01,
  4. -3.5082e+00, 1.9178e+01],
  5. [-4.0741e+01, -2.1037e+01, -1.6261e+01, ..., -1.6272e+01,
  6. -1.6390e+01, -3.3882e+01],
  7. [ 3.3734e+01, 3.4147e+01, -3.5206e+01, ..., -1.1523e+01,
  8. -1.5387e+00, 3.5074e+01]],
  9. [[-2.5351e+01, -2.3712e+01, -1.9933e+01, ..., 4.0224e+01,
  10. 2.6974e+01, 2.1199e+01],
  11. [-1.2792e+01, 1.2518e+01, -3.9328e+00, ..., -3.0102e+01,
  12. -1.0851e+01, 1.4496e+01],
  13. [-5.0859e+00, 3.0275e-02, -3.4949e+00, ..., 2.9030e+01,
  14. 8.9397e+00, 2.4887e+01],
  15. [-2.1277e+01, -1.4031e+01, -2.4480e+01, ..., -8.6075e+00,
  16. -4.6328e+01, 1.4581e+01]]], grad_fn=<MulBackward0>)
  17. torch.Size([2, 4, 512])

位置编码器的作用

  • 因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息(也就是说同一个词在文本的不同位置代表的含义也有可能不同)加入到词嵌入张量中, 以弥补位置信息的缺失.


  • 位置编码器的代码分析:

  1. # 定义位置编码器类, 我们同样把它看做一个层, 因此会继承nn.Module
  2. class PositionalEncoding(nn.Module):
  3. def __init__(self, d_model, dropout, max_len=5000):
  4. """位置编码器类的初始化函数, 共有三个参数, 分别是d_model: 词嵌入维度,
  5. dropout: 置0比率, 让部分神经元失效,max_len: 每个句子的最大长度"""
  6. super(PositionalEncoding, self).__init__()
  7. # 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout
  8. self.dropout = nn.Dropout(p=dropout)
  9. # 初始化一个位置编码矩阵, 它是一个0阵,矩阵的大小是max_len x d_model.
  10. pe = torch.zeros(max_len, d_model)
  11. # 初始化一个绝对位置矩阵, 在我们这里,词汇的绝对位置就是用它的索引去表示.
  12. # 所以我们首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
  13. # 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个max_len x 1 的矩阵,
  14. position = torch.arange(0, max_len).unsqueeze(1)
  15. # 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
  16. # 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
  17. # 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
  18. # 还希望它能够将自然数的绝对位置编码缩放成足够小的数字(也就是说初始化的绝对位置矩阵中原本的数值是索引值,现在将该数值缩放到足够小的数值),有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
  19. # 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵,
  20. # 而是有了一个跳跃,只初始化了一半即1xd_model/2 的矩阵。 为什么是一半呢,其实这里并不是真正意义上的初始化了一半的矩阵,-(math.log(10000.0) / d_model)是缩放因子,具体的公式见下图:
  21. # 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上,
  22. # 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵.
  23. div_term = torch.exp(torch.arange(0, d_model, 2) *
  24. -(math.log(10000.0) / d_model))
  25. #[:, 0::2]表示在所有行中,选取偶数列,其中0::2表示从0开始到最后一列,以步长为2进行切片;同理,[:, 1::2] 表示在所有行中,选取奇数列,其中1::2表示从1开始到最后一列,以步长为2进行切片
  26. # print('sin部分==', torch.sin(positon*div_term).shape) # sin部分== torch.Size([60, 256])
  27. # print('cos部分==', torch.cos(positon*div_term).shape)# cos部分== torch.Size([60, 256])
  28. pe[:, 0::2] = torch.sin(position * div_term)
  29. pe[:, 1::2] = torch.cos(position * div_term)
  30. # 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵,要想和embedding的输出(一个三维张量)相加,
  31. # 就必须拓展一个维度,所以这里使用unsqueeze拓展维度.
  32. pe = pe.unsqueeze(0)
  33. # 最后把pe位置编码矩阵注册成模型的buffer,什么是buffer呢,
  34. # 我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象.
  35. # 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
  36. self.register_buffer('pe', pe)
  37. def forward(self, x):
  38. """forward函数的参数是x,它是词嵌入层的输出结果"""
  39. # 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维切片到与输入的x的第二维相同即x.size(1),其中x的形状是(样本个数,句子长度,词嵌入维度),x.size(1)=句子长度,因为pe原本的形状是(1,max_len,d_model),所以pe.size(1)=max_len,而现在我们要使pe.size(1)=x.size(1),具体实现方法是pe[:, :x.size(1)]
  40. # 因为我们默认max_len为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要将max_len替换成现有句子的长度,进行与输入张量的适配.
  41. # 最后使用Variable对位置编码pe进行封装,使其与x的样式相同,但是它是不需要进行梯度求解的,因此把requires_grad设置成false.
  42. #最后的到的x既有embedding层的信息,又有位置编码层的信息
  43. x = x + Variable(self.pe[:, :x.size(1)],
  44. requires_grad=False)
  45. # 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
  46. return self.dropout(x)
  1. 位置编码器公式:

  • 实例化参数和对象:
  1. # 词嵌入维度是512维
  2. d_model = 512
  3. # 置0比率为0.1
  4. dropout = 0.1
  5. # 句子最大长度
  6. max_len=60
  7. # 输入x是Embedding层的输出的张量, 形状是2 x 4 x 512
  8. x = embr
  9. pe = PositionalEncoding(d_model, dropout, max_len)
  10. pe_result = pe(x)
  11. print(pe_result)
  12. print(pe_result.shape)

  • 输出效果:

  1. tensor([[[ -9.3579, -3.4049, 27.3322, ..., 17.4554, 16.5367, 34.5937],
  2. [ 30.4790, 9.4325, -9.0123, ..., -12.0060, -30.7220, -26.6563],
  3. [-38.9807, 25.8522, 1.8133, ..., 20.7788, 0.0000, -15.4891],
  4. [ -0.0000, -9.0428, -17.6793, ..., 0.0000, 18.7494, 38.8911]],
  5. [[-23.7486, -26.6181, 8.3407, ..., 48.5646, -2.7579, -21.9531],
  6. [ -1.8538, -9.2488, -18.8198, ..., 3.9829, -3.2544, 2.4036],
  7. [ 2.2624, 13.4857, 8.3457, ..., 55.8951, 0.0000, -20.9900],
  8. [ 30.1604, -28.6133, -8.0702, ..., -12.4632, -23.9271, -31.4371]]],
  9. grad_fn=<MulBackward0>)
  10. torch.Size([2, 4, 512])
  • 绘制词汇向量中特征的分布曲线:

  1. import matplotlib.pyplot as plt
  2. # 创建一张15 x 5大小的画布
  3. plt.figure(figsize=(15, 5))
  4. # 实例化PositionalEncoding类得到pe对象, 输入参数是20和0
  5. pe = PositionalEncoding(20, 0)
  6. # 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数,
  7. # 且这个tensor里的数值都是0, 被处理后相当于位置编码张量
  8. y = pe(Variable(torch.zeros(1, 100, 20)))
  9. # 然后定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
  10. # 因为总共有20维之多, 我们这里只查看4,5,6,7维的值.
  11. plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
  12. # 在画布上填写维度提示信息
  13. plt.legend(["dim %d"%p for p in [4,5,6,7]])
  • 输出效果

avatar

 

  • 效果分析

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

3 编码器部分实现

  • 编码器部分:

    • 由N个编码器层堆叠而成

    • 每个编码器层由两个子层连接结构组成

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

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

avatar

 

3.1 掩码张量

  • 什么是掩码张量:

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


  • 掩码张量的作用:

    • 在transformer的编码器中, 掩码张量的主要作用是遮蔽掉源语言中对结果没有意义的字符而产生的注意力值(也就是说遮蔽掉对结果没有意义的字符而将注意力集中在有意义的词上面),比如说,对于一句话:“我爱美丽的中国”,我们要提取句子的主要部分,即"我爱中国",而屏蔽掉对结果没有意义的字符,比如"美丽"这个修饰词,以此提升模型效果和训练速度

    • 在transformer的解码器中, 掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时,模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用.


  • 生成掩码张量的代码分析:

  1. def subsequent_mask(size):
  2. """生成向后遮掩的掩码张量, 参数size是掩码张量最后两个维度的大小, 它的最后两维形成一个方阵"""
  3. # 在函数中, 首先定义掩码张量的形状,这里第一个维度1的目的是进行扩维
  4. attn_shape = (1, size, size)
  5. # 然后使用np.ones方法向这个形状中添加1元素,通过np.triu()方法形成上三角阵, 最后为了节约空间, 再使其中的数据类型变为无符号8位整形unit8
  6. subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
  7. # 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作, 将上三角矩阵变成下三角矩阵
  8. # 在这里其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减,
  9. # 如果是0, subsequent_mask中的该位置由0变成1
  10. # 如果是1, subsequent_mask中的该位置由1变成0
  11. return torch.from_numpy(1 - subsequent_mask)
  •  实例化参数和对象:
  1. # 生成的掩码张量的最后两维的大小size = 5
  2. size = 5
  3. sm = subsequent_mask(size)
  4. print(sm)
  5. print(sm.shape)

  • 输出效果:

  1. tensor([[[1, 0, 0, 0, 0],
  2. [1, 1, 0, 0, 0],
  3. [1, 1, 1, 0, 0],
  4. [1, 1, 1, 1, 0],
  5. [1, 1, 1, 1, 1]]], dtype=torch.uint8)
  6. torch.Size([1, 5, 5])
  • 掩码张量的可视化:
  1. plt.figure(figsize=(5,5))
  2. #subsequent_mask的形状是(1,5,5),#subsequent_mask[0]的形状变成了(5,5)
  3. plt.imshow(subsequent_mask(20)[0])

  • 输出效果:

效果分析:

  • 通过观察可视化方阵, 黄色是1的部分, 这里代表被遮掩, 紫色代表没有被遮掩的信息, 横坐标代表目标词汇的位置, 纵坐标代表目标词汇可查看的位置;

  • 我们看到, 在0的位置我们一看望过去都是黄色的, 都被遮住了,说明第一次词还没有产生,模型只能完全靠自己去预测第一个目标词汇,1的位置基本都是黄色,只有一个位置是紫色,说明 从第二个位置看过去, 就能看到位置1的词, 其他位置看不到,此时模型只能利用可以看到的词汇进行预测, 以此类推.

3.2 注意力机制

  • 什么是注意力:

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


  • 什么是注意力计算规则:

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


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

avatar


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

    • Q是一段准备被概括的文本;

    • K是给出的提示;

    • V是大脑中的对提示K的延伸.

    • 当Q=K=V时, 称作自注意力机制.

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

  • 什么是注意力机制:

    • 注意力机制是注意力计算规则能够应用的深度学习网络的载体(也就是说注意力机制就是把注意力规则应用到深度学习神经网络的过程), 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制.


  • 注意力机制在网络中实现的图形表示:

avatar


  • 注意力计算规则的代码分析:

  1. def attention(query, key, value, mask=None, dropout=None):
  2. """注意力机制的实现, 输入分别是query, key, value, mask: 掩码张量,
  3. dropout是nn.Dropout层的实例化对象, 默认为None"""
  4. # 在函数中, 首先取query的最后一维的大小, 一般情况下就等同于我们的词嵌入维度, 命名为d_k
  5. # print('query===', query)
  6. # print('queryshape===', query.shape) #queryshape=== torch.Size([2, 4, 512]) 2表示有2个样本, 4表示每个样本中四个词,512表示把每个词映射到512维度上(可以理解为512个特征)
  7. # print('querysize(-1)===', query.size(-1)) #querysize(-1)=== 512
  8. d_k = query.size(-1)
  9. # 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置(因为query与key的最后一个维度是词嵌入维度,所以如果它们两个相乘,必须将key进行进行转置,这样才能满足矩阵乘法), 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
  10. # 得到注意力得分张量scores
  11. scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
  12. # print('keyshape====', key.shape) # keyshape==== torch.Size([2, 4, 512])
  13. # print('keytranspose====', key.transpose(-2, -1).shape) # keytranspose==== torch.Size([2, 512, 4])
  14. # print(scores.shape) #torch.Size([2, 4, 4])
  15. # 接着判断是否使用掩码张量
  16. if mask is not None:
  17. # 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0
  18. # 则对应的scores张量用-1e9这个值来替换, 因为这里用非常小的数值进行替换,所以该位置永远不会被选中,如下演示
  19. scores = scores.masked_fill(mask == 0, -1e9)
  20. # print(scores.shape) #torch.Size([2, 4, 4])
  21. # 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度.
  22. # 这样获得最终的注意力权重张量
  23. p_attn = F.softmax(scores, dim = -1)
  24. # print('p_attnsoftmax===', p_attn) # p_attnsoftmax== [2, 4, 4]
  25. # 之后判断是否使用dropout进行随机置0
  26. if dropout is not None:
  27. # 将p_attn传入dropout对象中进行'丢弃'处理
  28. p_attn = dropout(p_attn)
  29. # print('p_attndropout===', p_attn) # p_attndropout== [2, 4, 4]
  30. # 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力权重张量
  31. #print(torch.matmul(p_attn, value).shape) #[2, 4, 512]
  32. #因为 [2, 4, 4] X [2, 4, 512]=[2, 4, 512]
  33. return torch.matmul(p_attn, value), p_attn
  •   实例化参数和对象:
  1. # 我们令输入的query, key, value都相同, 都等于位置编码的输出,这里的pe_result的形状是(2,4,512)
  2. query = key = value = pe_result
  3. # 令mask为一个2x4x4的零张量
  4. mask = Variable(torch.zeros(2, 4, 4))
  5. attn, p_attn = attention(query, key, value, mask=mask)
  6. print('attn:',attn.shape)
  7. print('p_attn:',p_attn.shape)

  • 输出效果:

  1. attn: torch.Size([2, 4, 512])
  2. p_attn: torch.Size([2, 4, 4])

3.3 多头注意力机制


  • 学习目标:

    • 了解多头注意力机制的作用.

    • 掌握多头注意力机制的实现过程.


  • 什么是多头注意力机制:

    • 从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵(因为这里的线性层的输入维度和输出维度相同,所以线性层中的变换是方阵,而方阵经过矩阵乘法后它的行列才不会发生变化,所以说输入到的线性层的张量形状和线性层的输出的张量的形状相同),得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量(即:多个头将词向量进行平分,每个头获取词向量的一部分,假如词嵌入维度是300,有三个头,那么每个头获取其中100个词嵌入维度). 这就是所谓的多头,将每个头获得的输入送到注意力机制中, 就形成多头注意力机制.


  • 多头注意力机制结构图:

  • 多头注意力机制的作用:

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


  • 多头注意力机制的代码实现:

  1. # 用于深度拷贝的copy工具包
  2. import copy
  3. # 首先需要定义克隆函数, 因为在多头注意力机制的实现中, 用到多个结构相同的线性层.
  4. # 我们将使用clone函数将他们一同初始化在一个网络层列表对象中. 之后的结构中也会用到该函数.
  5. def clones(module, N):
  6. """用于生成相同网络层的克隆函数, 它的参数module表示要克隆的目标网络层, N代表需要克隆的数量"""
  7. # 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层(也就是说它们的内存地址是不同的),
  8. # 然后将其放在nn.ModuleList类型的列表中存放.
  9. return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
  10. # 我们使用一个类来实现多头注意力机制的处理
  11. class MultiHeadedAttention(nn.Module):
  12. def __init__(self, head, embedding_dim, dropout=0.1):
  13. """在类的初始化时, 会传入三个参数,head代表头数,embedding_dim代表词嵌入的维度,
  14. dropout代表进行dropout操作时置0比率,默认是0.1."""
  15. super(MultiHeadedAttention, self).__init__()
  16. # 在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,如果不能整除,编译器就会报错
  17. # 这是因为我们之后要给每个头分配等量的词特征(即词嵌入维度).也就是embedding_dim/head个.
  18. assert embedding_dim % head == 0
  19. # 得到每个头获得的分割词向量维度d_k
  20. self.d_k = embedding_dim // head
  21. # 传入头数h
  22. self.head = head
  23. # 然后获得线性层对象,通过nn的Linear实例化,它的内部变换矩阵是embedding_dim x embedding_dim,即多头注意力变换矩阵一定是方阵,然后使用clones函数克隆四个线性层。注意:这里设置的线性层的输入维度和输出维度是相同的,所以它不会改变输入到线性层中的张量的形状,因为此时的线性层的作用不是为了改变张量的形状,而是为了增加拟合能力
  24. # 为什么是四个呢,这是因为在多头注意力中,Q,K,V各需要一个,最后拼接的矩阵还需要一个,因此一共是四个.
  25. self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
  26. # self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None.
  27. self.attn = None
  28. # 最后就是一个self.dropout对象,它通过nn中的Dropout实例化而来,置0比率为传进来的参数dropout.
  29. self.dropout = nn.Dropout(p=dropout)
  30. def forward(self, query, key, value, mask=None):
  31. """前向逻辑函数, 它的输入参数有四个,前三个就是注意力机制需要的Q, K, V,
  32. 最后一个是注意力机制中可能需要的mask掩码张量,默认是None. """
  33. # 如果存在掩码张量mask
  34. if mask is not None:
  35. # 使用unsqueeze拓展维度,由原来的三维扩展成四维,这样便于后续的掩码操作,因为掩码张量mask的维度需要和被掩码的张量的维度保持一致,并且的除了第一维的数目不同之外,其它的维度的个数必须相同,比如,形状为(1,8,4,4)的mask可以对形状为(1,8,4,4)的张量或者形状为(2,8,4,4)的张量进行掩码,但是不可以对形状为(8,4,4)的张量进行掩码
  36. mask = mask.unsqueeze(0)
  37. # 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
  38. batch_size = query.size(0)
  39. # 之后就进入多头处理环节
  40. # 首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到线性层中,注意;在 zip(self.linears, (query, key, value))]中,self.linears中包含四个线性层(因为拷贝了4次),但是(query, key, value)是三个元素,所以在作zip运算时,只会取self.linears的前三个线性层
  41. # 做完线性变换后,开始为每个头分割输入,这里使用model(x).view(batch_size, -1, self.head, self.d_k)方法对线性变换的结果model(x)变成四个维度,原本的线性变换的结果的最后一个维度是词嵌入维度,将原先最后一个维度拆分成 self.head x self.d_k,也就是多加了一个维度h,代表头数,
  42. # 这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
  43. # 计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,其目的是为了让代表句子长度维度(也就是自适应维度,即-1所代表的维度)和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
  44. # 从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.这样我们就得到了每个头的输入.
  45. #注意:经过运算后得到的 query, key, value都是四维张量
  46. query, key, value = \
  47. [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
  48. for model, x in zip(self.linears, (query, key, value))]
  49. # 得到每个头的输入后,接下来就是将他们传入到attention中,
  50. # 这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
  51. x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
  52. # 通过多头注意力计算后,我们就得到的每个头计算的计算结果是4维张量,我们需要将其转换为输入的形状以方便后续的计算,也就是说我们需要将形状(batch_size, self.head, -1, self.d_k)转化为(batch_size, -1, self.head*self.d_k),在(batch_size, -1, self.head*self.d_k)相当于(批次样本数,句子长度,词嵌入维度)
  53. #因为前面已经将1,2两个维度进行了转置,这里我们要重新转置回来,即x.transpose(1, 2),恢复到原先的形状然后使用contiguous方法,
  54. # 这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用,也就是说我可以先使用view方法再使用transpose,即view().transpose(),但是反过来就不能直接使用,即不能使用transpose().view(),如果想先使用transpose方法再使用view方法,必须用这种方式:transpose().contiguous().view()
  55. # 所以,下一步就是使用view重塑形状,变成和输入形状相同.也就是将最后一个维度变成词嵌入维度
  56. #contiguous相当于示意图中的concat过程,contiguous的解释链接:https://zhuanlan.zhihu.com/p/64551412
  57. x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
  58. # 最后使用线性层列表中的最后一个线性层即linears[-1]对输入进行线性变换得到最终的多头注意力结构的输出.
  59. return self.linears[-1](x)
  •  实例化参数和对象:
  1. # 头数head
  2. head = 8
  3. # 词嵌入维度embedding_dim
  4. embedding_dim = 512
  5. # 置零比率dropout
  6. dropout = 0.2
  7. # 假设输入的Q,K,V仍然相等
  8. #这里的 pe_result的形状是(2,4,512),其中2表示batchsize,4表示样本的句子长度,512表词嵌入维度
  9. query = value = key = pe_result
  10. # 输入的掩码张量mask
  11. #mask的形状中的8代表头数,4代表每个样本的长度
  12. mask = Variable(torch.zeros(8, 4, 4))
  13. #调用
  14. mha = MultiHeadedAttention(head, embedding_dim, dropout)
  15. mha_result = mha(query, key, value, mask)
  16. print(mha_result)
  17. print(mha_result.shape)

  • 输出效果:

  1. tensor([[[-0.3075, 1.5687, -2.5693, ..., -1.1098, 0.0878, -3.3609],
  2. [ 3.8065, -2.4538, -0.3708, ..., -1.5205, -1.1488, -1.3984],
  3. [ 2.4190, 0.5376, -2.8475, ..., 1.4218, -0.4488, -0.2984],
  4. [ 2.9356, 0.3620, -3.8722, ..., -0.7996, 0.1468, 1.0345]],
  5. [[ 1.1423, 0.6038, 0.0954, ..., 2.2679, -5.7749, 1.4132],
  6. [ 2.4066, -0.2777, 2.8102, ..., 0.1137, -3.9517, -2.9246],
  7. [ 5.8201, 1.1534, -1.9191, ..., 0.1410, -7.6110, 1.0046],
  8. [ 3.1209, 1.0008, -0.5317, ..., 2.8619, -6.3204, -1.3435]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

3.4 前馈全连接层


  • 什么是前馈全连接层:

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


  • 前馈全连接层的作用:

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


  • 前馈全连接层的代码分析:

 

  1. # 通过类PositionwiseFeedForward来实现前馈全连接层
  2. class PositionwiseFeedForward(nn.Module):
  3. def __init__(self, d_model, d_ff, dropout=0.1):
  4. """初始化函数有三个输入参数分别是d_model, d_ff,和dropout=0.1,第一个参数d_model是词嵌入维度,同时也分别是两个线性层的输入维度和输出维度,即第一个是线性层的输入维度也是第二个线性层的输出维度,
  5. 因为我们希望输入通过前馈全连接层后输入和输出的维度不变. 第二个参数d_ff就是第二个线性层的输入维度和第一个线性层的输出维度.
  6. 最后一个是dropout置0比率."""
  7. super(PositionwiseFeedForward, self).__init__()
  8. # 首先按照我们预期使用nn实例化了两个线性层对象,self.w1和self.w2
  9. # 它们的参数分别是d_model, d_ff和d_ff, d_model
  10. self.w1 = nn.Linear(d_model, d_ff)
  11. self.w2 = nn.Linear(d_ff, d_model)
  12. # 然后使用nn的Dropout实例化了对象self.dropout
  13. self.dropout = nn.Dropout(dropout)
  14. def forward(self, x):
  15. """输入参数为x,代表来自上一层的输出"""
  16. # 首先经过第一个线性层,然后使用Funtional中relu函数进行激活,
  17. # 之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果.
  18. return self.w2(self.dropout(F.relu(self.w1(x))))
  •  实例化参数和对象:
  1. d_model = 512
  2. # 线性变化的维度
  3. d_ff = 64
  4. dropout = 0.2
  5. # 输入参数x可以是多头注意力机制的输出
  6. x = mha_result
  7. #调用
  8. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  9. ff_result = ff(x)
  10. print(ff_result)
  11. print(ff_result.shape)

  • 输出效果:

  1. tensor([[[-1.9488e+00, -3.4060e-01, -1.1216e+00, ..., 1.8203e-01,
  2. -2.6336e+00, 2.0917e-03],
  3. [-2.5875e-02, 1.1523e-01, -9.5437e-01, ..., -2.6257e-01,
  4. -5.7620e-01, -1.9225e-01],
  5. [-8.7508e-01, 1.0092e+00, -1.6515e+00, ..., 3.4446e-02,
  6. -1.5933e+00, -3.1760e-01],
  7. [-2.7507e-01, 4.7225e-01, -2.0318e-01, ..., 1.0530e+00,
  8. -3.7910e-01, -9.7730e-01]],
  9. [[-2.2575e+00, -2.0904e+00, 2.9427e+00, ..., 9.6574e-01,
  10. -1.9754e+00, 1.2797e+00],
  11. [-1.5114e+00, -4.7963e-01, 1.2881e+00, ..., -2.4882e-02,
  12. -1.5896e+00, -1.0350e+00],
  13. [ 1.7416e-01, -4.0688e-01, 1.9289e+00, ..., -4.9754e-01,
  14. -1.6320e+00, -1.5217e+00],
  15. [-1.0874e-01, -3.3842e-01, 2.9379e-01, ..., -5.1276e-01,
  16. -1.6150e+00, -1.1295e+00]]], grad_fn=<AddBackward0>)
  17. torch.Size([2, 4, 512])

 

输入到前馈全连接的张量x形状是torch.Size([2, 4, 512]),输出的结果的形状依然是torch.Size([2, 4, 512]),所以前馈全连接并没有改变输入张量的形状

3.5 规范化层


  • 规范化层的作用:

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


  • 规范化层的代码实现:

  1. # 通过LayerNorm实现规范化层的类
  2. class LayerNorm(nn.Module):
  3. def __init__(self, features, eps=1e-6):
  4. """初始化函数有两个参数, 一个是features, 表示词嵌入的维度,
  5. 另一个是eps它是一个足够小的数, 在规范化公式的分母中出现,
  6. 防止分母为0.默认是1e-6."""
  7. super(LayerNorm, self).__init__()
  8. # 根据features的形状初始化两个参数张量a2,和b2,第一个初始化为1张量,
  9. # 也就是里面的元素都是1,第二个初始化为0张量,也就是里面的元素都是0,这两个张量就是规范化层的参数,
  10. # 因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有参数作为调节因子,
  11. # 使其即能满足规范化要求,又能不改变针对目标的表征.最后使用nn.parameter封装,代表他们是模型的参数。
  12. #这里的a2和b2的初始值分别是1和0,后期随着模型的训练它们的值也在不断的变化
  13. self.a2 = nn.Parameter(torch.ones(features))
  14. self.b2 = nn.Parameter(torch.zeros(features))
  15. # 把eps传到类中
  16. self.eps = eps
  17. def forward(self, x):
  18. """输入参数x代表来自上一层的输出"""
  19. # 在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.即:mean = x.mean(-1, keepdim=True),其中-1表示最后一个维度,keepdim=True表示保持输出维度与输入维度一致,它的具体含有见下例的演示
  20. # 接着再求最后一个维度的标准差,并保持输出维度与输入维度一致.然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果,
  21. # 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,比如一个2x2的张量和另一个2x2的张量进行点乘,就是对应位置元素作乘法,加上位移参数b2.返回即可.
  22. mean = x.mean(-1, keepdim=True)
  23. std = x.std(-1, keepdim=True)
  24. return self.a2 * (x - mean) / (std + self.eps) + self.b2
  •   实例化参数和对象:
  1. features = d_model = 512
  2. eps = 1e-6
  3. # 输入x来自前馈全连接层的输出
  4. x = ff_result
  5. #调用
  6. ln = LayerNorm(feature, eps)
  7. ln_result = ln(x)
  8. print(ln_result)
  9. print(ln_result.shape)

  • 输出效果:

  1. tensor([[[ 2.2697, 1.3911, -0.4417, ..., 0.9937, 0.6589, -1.1902],
  2. [ 1.5876, 0.5182, 0.6220, ..., 0.9836, 0.0338, -1.3393],
  3. [ 1.8261, 2.0161, 0.2272, ..., 0.3004, 0.5660, -0.9044],
  4. [ 1.5429, 1.3221, -0.2933, ..., 0.0406, 1.0603, 1.4666]],
  5. [[ 0.2378, 0.9952, 1.2621, ..., -0.4334, -1.1644, 1.2082],
  6. [-1.0209, 0.6435, 0.4235, ..., -0.3448, -1.0560, 1.2347],
  7. [-0.8158, 0.7118, 0.4110, ..., 0.0990, -1.4833, 1.9434],
  8. [ 0.9857, 2.3924, 0.3819, ..., 0.0157, -1.6300, 1.2251]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

 

3.6 子层连接结构


  • 什么是子层连接结构:

    • 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(也就是跳跃连接,比如说按照正常逻辑,第一层与第二层连接,第二层与第三层连接,即1-->2-->3,但是如果第一层跳过第二层直接与第三层连接,那么称为残差连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.


  • 子层连接结构的形式:

    • 多头注意力层+规范化层+ 残差连接

    • 前馈全连接层+ 规范化层+残差连接

     

  • 子层连接结构图:

 

  • 子层连接结构的代码分析:

  1. # 使用SublayerConnection来实现子层连接结构的类
  2. class SublayerConnection(nn.Module):
  3. def __init__(self, size, dropout=0.1):
  4. """它输入参数有两个, size以及dropout, size一般是都是词嵌入维度的大小,
  5. dropout本身是对模型结构中的节点数进行随机抑制的比率,
  6. 又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随机置0的比率.
  7. """
  8. super(SublayerConnection, self).__init__()
  9. # 实例化了规范化对象self.norm
  10. self.norm = LayerNorm(size)
  11. # 又使用nn中预定义的droupout实例化一个self.dropout对象.
  12. self.dropout = nn.Dropout(p=dropout)
  13. def forward(self, x, sublayer):
  14. """前向逻辑函数中, 接收上一个层或者子层的输出作为第一个参数,
  15. 将该子层连接中的子层函数作为第二个参数,子层函数可以是前馈层或者多头注意力层等不同的层"""
  16. # 我们首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作,dropou的作用是随机停止一些网络中神经元的作用,来防止过拟合.
  17. # 最后add,即残差连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出
  18. #这里需要注意的是,在上面的示意图中,首先将输出传给子层处理,之后再对子层进行dropout操作,最后进行规范化和add,这跟我们的代码流程不太相同,原因是上面的示意图仅仅是论文中的示意图,而这里的代码实现流程对论文中的过程进行优化,
  19. return x + self.dropout(sublayer(self.norm(x)))
  •    实例化参数和对象:
  1. size = 512
  2. dropout = 0.2
  3. head = 8
  4. d_model = 512
  5. # 令x为位置编码器的输出
  6. x = pe_result
  7. mask = Variable(torch.zeros(8, 4, 4))
  8. # 假设子层中装的是多头注意力层, 实例化这个类
  9. self_attn = MultiHeadedAttention(head, d_model)
  10. # 使用lambda获得一个函数类型的子层,self_attn(x, x, x, mask)中前三个参数分别代表 query, key, value,因为它们的值都为x,所以此时相当于自注意力机制
  11. sublayer = lambda x: self_attn(x, x, x, mask)、
  12. #调用
  13. sc = SublayerConnection(size, dropout)
  14. sc_result = sc(x, sublayer)
  15. print(sc_result)
  16. print(sc_result.shape)

  • 输出效果:

  1. tensor([[[ 14.8830, 22.4106, -31.4739, ..., 21.0882, -10.0338, -0.2588],
  2. [-25.1435, 2.9246, -16.1235, ..., 10.5069, -7.1007, -3.7396],
  3. [ 0.1374, 32.6438, 12.3680, ..., -12.0251, -40.5829, 2.2297],
  4. [-13.3123, 55.4689, 9.5420, ..., -12.6622, 23.4496, 21.1531]],
  5. [[ 13.3533, 17.5674, -13.3354, ..., 29.1366, -6.4898, 35.8614],
  6. [-35.2286, 18.7378, -31.4337, ..., 11.1726, 20.6372, 29.8689],
  7. [-30.7627, 0.0000, -57.0587, ..., 15.0724, -10.7196, -18.6290],
  8. [ -2.7757, -19.6408, 0.0000, ..., 12.7660, 21.6843, -35.4784]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

3.7 编码器层


  • 学习目标:

    • 了解编码器层的作用.

    • 掌握编码器层的实现过程.


  • 编码器层的作用:

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


  • 编码器层的构成图:

avatar


  • 编码器层的代码分析:

  1. # 使用EncoderLayer类实现编码器层
  2. class EncoderLayer(nn.Module):
  3. def __init__(self, size, self_attn, feed_forward, dropout):
  4. """它的初始化函数参数有四个,分别是size,其实就是我们词嵌入维度的大小,它也将作为我们编码器层的大小,
  5. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制,
  6. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象, 最后一个是置0比率dropout."""
  7. super(EncoderLayer, self).__init__()
  8. # 首先将self_attn和feed_forward传入其中.
  9. self.self_attn = self_attn
  10. self.feed_forward = feed_forward
  11. # 如图所示, 编码器层中有两个子层连接结构, 所以使用clones函数进行克隆
  12. self.sublayer = clones(SublayerConnection(size, dropout), 2)
  13. # 把size传入其中
  14. self.size = size
  15. def forward(self, x, mask):
  16. """forward函数中有两个输入参数,x和mask,分别代表上一层的输出张量,和掩码张量mask."""
  17. # 里面就是按照结构图左侧的流程. 首先通过第一个子层连接结构即self.sublayer[0],其中包含多头自注意力子层,
  18. # 然后通过第二个子层连接结构即self.sublayer[1],其中包含前馈全连接子层. 最后返回结果.
  19. x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
  20. return self.sublayer[1](x, self.feed_forward)
  •     实例化参数和对象:
  1. size = 512
  2. head = 8
  3. d_model = 512
  4. d_ff = 64
  5. x = pe_result
  6. dropout = 0.2
  7. self_attn = MultiHeadedAttention(head, d_model)
  8. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  9. mask = Variable(torch.zeros(8, 4, 4))
  10. #调用
  11. el = EncoderLayer(size, self_attn, ff, dropout)
  12. el_result = el(x, mask)
  13. print(el_result)
  14. print(el_result.shape)

  • 输出效果:

  1. tensor([[[ 33.6988, -30.7224, 20.9575, ..., 5.2968, -48.5658, 20.0734],
  2. [-18.1999, 34.2358, 40.3094, ..., 10.1102, 58.3381, 58.4962],
  3. [ 32.1243, 16.7921, -6.8024, ..., 23.0022, -18.1463, -17.1263],
  4. [ -9.3475, -3.3605, -55.3494, ..., 43.6333, -0.1900, 0.1625]],
  5. [[ 32.8937, -46.2808, 8.5047, ..., 29.1837, 22.5962, -14.4349],
  6. [ 21.3379, 20.0657, -31.7256, ..., -13.4079, -44.0706, -9.9504],
  7. [ 19.7478, -1.0848, 11.8884, ..., -9.5794, 0.0675, -4.7123],
  8. [ -6.8023, -16.1176, 20.9476, ..., -6.5469, 34.8391, -14.9798]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

3.8 编码器


  • 编码器的作用:

    • 编码器用于对输入进行指定的特征提取过程, 也称为编码, 它由N个编码器层堆叠而成,编码器的输出成为解码器的输入的一部分.


  • 编码器的结构图:它是由N个编码器层堆叠而成,如下图所示,这里的N=4

 


  • 编码器的代码分析:

  1. # 使用Encoder类来实现编码器
  2. class Encoder(nn.Module):
  3. def __init__(self, layer, N):
  4. """初始化函数的两个参数分别代表编码器层和编码器层的个数"""
  5. super(Encoder, self).__init__()
  6. # 首先使用clones函数克隆N个编码器层放在self.layers中
  7. self.layers = clones(layer, N)
  8. # 再初始化一个规范化层, 它将用在编码器的最后面.layer.size是编码器层的一个实例属性,代表是词嵌入维度
  9. self.norm = LayerNorm(layer.size)
  10. def forward(self, x, mask):
  11. """forward函数的输入和编码器层相同, x代表上一层的输出, mask代表掩码张量"""
  12. # 首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x,
  13. # 这个循环的过程,就相当于输出的x经过了N个编码器层的处理.
  14. # 最后再通过规范化层的对象self.norm进行处理,返回编码器的最终结果.
  15. for layer in self.layers:
  16. x = layer(x, mask)
  17. return self.norm(x)
  •      实例化参数和对象:
  1. # 第一个实例化参数layer, 它是一个编码器层的实例化对象, 因此需要传入编码器层的参数
  2. # 又因为编码器层中的子层是不共享的, 因此需要使用深度拷贝各个对象.
  3. size = 512
  4. head = 8
  5. d_model = 512
  6. d_ff = 64
  7. c = copy.deepcopy
  8. attn = MultiHeadedAttention(head, d_model)
  9. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  10. dropout = 0.2
  11. layer = EncoderLayer(size, c(attn), c(ff), dropout)
  12. # 编码器中编码器层的个数N
  13. N = 8
  14. mask = Variable(torch.zeros(8, 4, 4))
  15. #调用
  16. en = Encoder(layer, N)
  17. en_result = en(x, mask)
  18. print(en_result)
  19. print(en_result.shape)

  • 输出效果:

  1. tensor([[[-0.2081, -0.3586, -0.2353, ..., 2.5646, -0.2851, 0.0238],
  2. [ 0.7957, -0.5481, 1.2443, ..., 0.7927, 0.6404, -0.0484],
  3. [-0.1212, 0.4320, -0.5644, ..., 1.3287, -0.0935, -0.6861],
  4. [-0.3937, -0.6150, 2.2394, ..., -1.5354, 0.7981, 1.7907]],
  5. [[-2.3005, 0.3757, 1.0360, ..., 1.4019, 0.6493, -0.1467],
  6. [ 0.5653, 0.1569, 0.4075, ..., -0.3205, 1.4774, -0.5856],
  7. [-1.0555, 0.0061, -1.8165, ..., -0.4339, -1.8780, 0.2467],
  8. [-2.1617, -1.5532, -1.4330, ..., -0.9433, -0.5304, -1.7022]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

4 解码器部分实现


  • 解码器部分:

    • 由N个解码器层堆叠而成

    • 每个解码器层由三个子层连接结构组成

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

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

    • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

说明:

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

4.1 解码器层


  • 解码器层的作用:

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


  • 解码器层的代码实现:

  1. # 使用DecoderLayer的类实现解码器层
  2. class DecoderLayer(nn.Module):
  3. def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
  4. """初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层的尺寸,
  5. 第二个是self_attn,多头自注意力机制对象,也就是说这个注意力机制需要Q=K=V,
  6. 第三个是src_attn,常规的多头注意力机制对象,这里Q!=K=V, 第四个是前馈全连接层对象,最后就是droupout置0比率.
  7. """
  8. super(DecoderLayer, self).__init__()
  9. # 在初始化函数中, 主要就是将这些输入传到类中
  10. self.size = size
  11. self.self_attn = self_attn
  12. self.src_attn = src_attn
  13. self.feed_forward = feed_forward
  14. # 按照结构图使用clones函数克隆三个子层连接对象.
  15. self.sublayer = clones(SublayerConnection(size, dropout), 3)
  16. def forward(self, x, memory, source_mask, target_mask):
  17. """forward函数中的参数有4个,分别是来自上一层的输入x,
  18. 来自编码器层的语义存储变量mermory(也就是编码器的输出), 以及源数据掩码张量和目标数据掩码张量.
  19. """
  20. # 将memory表示成m方便之后使用
  21. m = memory
  22. # 将x传入第一个子层结构(即多头自注意力机制子层结构),第一个子层结构的输入分别是x和self-attn函数,因为是自注意力机制,所以Q,K,V都是x,
  23. # 最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据,
  24. # 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,
  25. # 但是我们不希望在生成第一个字符时模型能利用这个信息(也就是说我们不希望在模型开始生成第一个字符前,模型已将提前知道了这个字符),因此我们会将其遮掩,同样生成第二个字符或词汇时,
  26. # 模型只能看见和使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型看到和使用.
  27. x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))
  28. # 接着进入第二个子层,这个子层中采用常规的注意力机制(也就是说此时Q,K,V 不一定全都相等),q是输入x; k,v是编码层输出memory,
  29. # 同样也传入source_mask,但是进行源数据遮掩的原因跟对目标数据的遮掩的原因是不同的,它并非是抑制未来的信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力值(也就是说遮蔽掉对结果没有意义的字符而将注意力集中在有意义的词上面),比如说,对于一句话:“我爱美丽的中国”,我们要提取句子的主要部分,即"我爱中国",而屏蔽掉对结果没有意义的字符,比如"美丽"这个修饰词
  30. # 以此提升模型效果和训练速度. 这样就完成了第二个子层的处理.
  31. x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))
  32. # 最后一个子层就是前馈全连接子层,经过它的处理后就可以返回结果.这就是我们的解码器层结构.
  33. return self.sublayer[2](x, self.feed_forward)
  •      实例化参数和对象:
  1. # 类的实例化参数与解码器层类似, 相比多出了src_attn, 但是和self_attn是同一个类.
  2. head = 8
  3. size = 512
  4. d_model = 512
  5. d_ff = 64
  6. dropout = 0.2
  7. #为了便于演示,我们将自注意力机制和常规注意力机制都采用多头注意力机制
  8. self_attn = src_attn = MultiHeadedAttention(head, d_model, dropout)
  9. # 前馈全连接层也和之前相同
  10. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  11. # x是来自目标数据的词嵌入表示, 但形式和源数据的词嵌入表示相同, 这里使用per充当.
  12. x = pe_result
  13. # memory是来自编码器的输出
  14. memory = en_result
  15. # 实际中source_mask和target_mask并不相同, 这里为了方便计算使他们都为mask
  16. mask = Variable(torch.zeros(8, 4, 4))
  17. source_mask = target_mask = mask
  18. #调用
  19. dl = DecoderLayer(size, self_attn, src_attn, ff, dropout)
  20. dl_result = dl(x, memory, source_mask, target_mask)
  21. print(dl_result)
  22. print(dl_result.shape)

  • 输出效果:

  1. tensor([[[ 1.9604e+00, 3.9288e+01, -5.2422e+01, ..., 2.1041e-01,
  2. -5.5063e+01, 1.5233e-01],
  3. [ 1.0135e-01, -3.7779e-01, 6.5491e+01, ..., 2.8062e+01,
  4. -3.7780e+01, -3.9577e+01],
  5. [ 1.9526e+01, -2.5741e+01, 2.6926e-01, ..., -1.5316e+01,
  6. 1.4543e+00, 2.7714e+00],
  7. [-2.1528e+01, 2.0141e+01, 2.1999e+01, ..., 2.2099e+00,
  8. -1.7267e+01, -1.6687e+01]],
  9. [[ 6.7259e+00, -2.6918e+01, 1.1807e+01, ..., -3.6453e+01,
  10. -2.9231e+01, 1.1288e+01],
  11. [ 7.7484e+01, -5.0572e-01, -1.3096e+01, ..., 3.6302e-01,
  12. 1.9907e+01, -1.2160e+00],
  13. [ 2.6703e+01, 4.4737e+01, -3.1590e+01, ..., 4.1540e-03,
  14. 5.2587e+00, 5.2382e+00],
  15. [ 4.7435e+01, -3.7599e-01, 5.0898e+01, ..., 5.6361e+00,
  16. 3.5891e+01, 1.5697e+01]]], grad_fn=<AddBackward0>)
  17. torch.Size([2, 4, 512])

4.2 解码器


  • 学习目标:

    • 了解解码器的作用.

    • 掌握解码器的实现过程.


  • 解码器的作用:

    • 根据编码器的结果以及上一次预测的结果(解码器是逐个字符进行解码), 对下一次可能出现的'值'进行特征表示.

  • 解码器的结构图:它是由N个解码器层堆叠而成,如下图所示,这里的N=4


  • 解码器的代码分析:

  1. # 使用类Decoder来实现解码器
  2. class Decoder(nn.Module):
  3. def __init__(self, layer, N):
  4. """初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N."""
  5. super(Decoder, self).__init__()
  6. # 首先使用clones方法克隆了N个layer,然后实例化了一个规范化层.
  7. # 因为数据走过了所有的解码器层后最后要做规范化处理.
  8. self.layers = clones(layer, N)
  9. self.norm = LayerNorm(layer.size)
  10. def forward(self, x, memory, source_mask, target_mask):
  11. """forward函数中的参数有4个,x代表目标数据的嵌入表示(即上一层的输出结果),memory是编码器层的输出,
  12. source_mask, target_mask代表源数据和目标数据的掩码张量"""
  13. # 然后就是对每个层进行循环,当然这个循环就是变量x通过每一个层的处理,
  14. # 得出最后的结果,再进行一次规范化返回即可.
  15. for layer in self.layers:
  16. x = layer(x, memory, source_mask, target_mask)
  17. return self.norm(x)
  •     实例化参数和对象:
  1. # 分别是解码器层layer和解码器层的个数N
  2. size = 512
  3. d_model = 512
  4. head = 8
  5. d_ff = 64
  6. dropout = 0.2
  7. c = copy.deepcopy
  8. attn = MultiHeadedAttention(head, d_model)
  9. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  10. #为了便于演示,我们将自注意力机制和常规注意力机制都拷贝多头自注意力机制
  11. layer = DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout)
  12. N = 8
  13. # 输入参数与解码器层的输入参数相同
  14. x = pe_result
  15. #memory是编码器层的输出
  16. memory = en_result
  17. mask = Variable(torch.zeros(8, 4, 4))
  18. source_mask = target_mask = mask
  19. #调用
  20. de = Decoder(layer, N)
  21. de_result = de(x, memory, source_mask, target_mask)
  22. print(de_result)
  23. print(de_result.shape)

  • 输出效果:

  1. tensor([[[ 0.9898, -0.3216, -1.2439, ..., 0.7427, -0.0717, -0.0814],
  2. [-0.7432, 0.6985, 1.5551, ..., 0.5232, -0.5685, 1.3387],
  3. [ 0.2149, 0.5274, -1.6414, ..., 0.7476, 0.5082, -3.0132],
  4. [ 0.4408, 0.9416, 0.4522, ..., -0.1506, 1.5591, -0.6453]],
  5. [[-0.9027, 0.5874, 0.6981, ..., 2.2899, 0.2933, -0.7508],
  6. [ 1.2246, -1.0856, -0.2497, ..., -1.2377, 0.0847, -0.0221],
  7. [ 3.4012, -0.4181, -2.0968, ..., -1.5427, 0.1090, -0.3882],
  8. [-0.1050, -0.5140, -0.6494, ..., -0.4358, -1.2173, 0.4161]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 512])

 

5 输出部分实现


 

  • 输出部分包含:

    • 线性层

    • softmax层

avatar


线性层的作用

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


softmax层的作用

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


  • 线性层和softmax层的代码分析:

  1. # nn.functional工具包装载了网络层中那些只进行计算, 而没有参数的层
  2. import torch.nn.functional as F
  3. # 将线性层和softmax计算层一起实现, 因为二者的共同目标是生成最后的结构
  4. # 因此把类的名字叫做Generator, 生成器类
  5. class Generator(nn.Module):
  6. def __init__(self, d_model, vocab_size):
  7. """初始化函数的输入参数有两个, d_model代表词嵌入维度, vocab_size代表词表大小."""
  8. super(Generator, self).__init__()
  9. # 首先就是使用nn中的预定义线性层进行实例化, 得到一个对象self.project等待使用,
  10. # 这个线性层的参数有两个, 就是初始化函数传进来的两个参数: d_model(词嵌入维度), vocab_size(词表大小)
  11. self.project = nn.Linear(d_model, vocab_size)
  12. def forward(self, x):
  13. """前向逻辑函数中输入是上一层的输出张量x"""
  14. # 在函数中, 首先使用上一步得到的self.project对x进行线性变化,
  15. # 然后使用F中已经实现的log_softmax对最后一个维度进行softmax处理这里的最后一个维度是 vocab_size(词表大小),对它进行softmax得到的是当讲预测的词输入词表中各个词的概率值,也就是说,经过softmax后,最后一个维度是vocab_size个概率值,我们选择概率最大的值所对应的词汇作为预测值
  16. # 在这里之所以使用log_softmax是因为和我们这个pytorch版本的损失函数实现有关, 在其他版本中将修复.
  17. # log_softmax就是对softmax的结果又取了对数, 因为对数函数是单调递增函数,
  18. # 因此对最终我们取最大的概率值没有影响. 最后返回结果即可.
  19. return F.log_softmax(self.project(x), dim=-1)
  •     实例化参数和对象:
  1. # 词嵌入维度是512维
  2. d_model = 512
  3. # 词表大小是1000
  4. vocab_size = 1000
  5. # 输入x是上一层网络的输出, 我们使用来自解码器层的输出
  6. x = de_result
  7. #调用
  8. gen = Generator(d_model, vocab_size)
  9. gen_result = gen(x)
  10. print(gen_result)
  11. print(gen_result.shape)#它的输出结果是torch.Size([2, 4, 1000]),其中2表示有2个样本,4表示每个样本的长度,1000是词表大小,也是1000个概率值,我们取其中概率最大的值作为预测结果

  • 输出效果:

  1. tensor([[[-7.8098, -7.5260, -6.9244, ..., -7.6340, -6.9026, -7.5232],
  2. [-6.9093, -7.3295, -7.2972, ..., -6.6221, -7.2268, -7.0772],
  3. [-7.0263, -7.2229, -7.8533, ..., -6.7307, -6.9294, -7.3042],
  4. [-6.5045, -6.0504, -6.6241, ..., -5.9063, -6.5361, -7.1484]],
  5. [[-7.1651, -6.0224, -7.4931, ..., -7.9565, -8.0460, -6.6490],
  6. [-6.3779, -7.6133, -8.3572, ..., -6.6565, -7.1867, -6.5112],
  7. [-6.4914, -6.9289, -6.2634, ..., -6.2471, -7.5348, -6.8541],
  8. [-6.8651, -7.0460, -7.6239, ..., -7.1411, -6.5496, -7.3749]]],
  9. grad_fn=<LogSoftmaxBackward>)
  10. torch.Size([2, 4, 1000])

 

6 模型构建

 


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


  • Transformer总体架构图:

avatar

编码器-解码器结构的代码实现

  1. # 使用EncoderDecoder类来实现编码器-解码器结构
  2. class EncoderDecoder(nn.Module):
  3. def __init__(self, encoder, decoder, source_embed, target_embed, generator):
  4. """初始化函数中有5个参数, 分别是编码器对象, 解码器对象,
  5. 源数据嵌入函数, 目标数据嵌入函数, 以及输出部分的类别生成器对象
  6. """
  7. super(EncoderDecoder, self).__init__()
  8. # 将参数传入到类中
  9. self.encoder = encoder
  10. self.decoder = decoder
  11. #定义源数据嵌入函数,它的作用是对源数据进行词嵌入
  12. self.src_embed = source_embed
  13. #定义目标数据嵌入函数,它的作用是对目标数据进行词嵌入
  14. self.tgt_embed = target_embed
  15. self.generator = generator
  16. def forward(self, source, target, source_mask, target_mask):
  17. """在forward函数中,有四个参数, source代表源数据, target代表目标数据,
  18. source_mask代表源数据的掩码张量,target_mask代表目标数据的掩码张量"""
  19. # 在函数中, 将source, source_mask传入编码函数(就是下面定义的函数encode), 得到结果后,
  20. # 与source_mask,target,和target_mask一同传给解码函数(就是下面定义的函数decode).
  21. return self.generator(self.decode(self.encode(source, source_mask), source_mask,
  22. target, target_mask))
  23. def encode(self, source, source_mask):
  24. """编码函数, 以source和source_mask为参数"""
  25. # 使用源数据嵌入函数src_embed对source做处理, 然后和source_mask一起传给编码器对象self.encoder
  26. return self.encoder(self.src_embed(source), source_mask)
  27. def decode(self, memory, source_mask, target, target_mask):
  28. """解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参数"""
  29. # 使用tgt_embed对target做处理, 然后和source_mask, target_mask, memory一起传给self.decoder
  30. return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)
  •     实例化参数和对象:
  1. vocab_size = 1000
  2. d_model = 512
  3. #将编码器实例化对象en赋值给encoder
  4. encoder = en
  5. #将解码器实例化对象de赋值给decoder
  6. decoder = de
  7. source_embed = nn.Embedding(vocab_size, d_model)
  8. target_embed = nn.Embedding(vocab_size, d_model)
  9. generator = gen
  10. # 假设源数据与目标数据相同, 实际中并不相同
  11. source = target = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))
  12. # 假设src_mask与tgt_mask相同,实际中并不相同
  13. source_mask = target_mask = Variable(torch.zeros(8, 4, 4))
  14. #调用
  15. ed = EncoderDecoder(encoder, decoder, source_embed, target_embed, generator)
  16. ed_result = ed(source, target, source_mask, target_mask)
  17. print(ed_result)
  18. print(ed_result.shape)

  • 输出效果:

  1. tensor([[[ 0.2102, -0.0826, -0.0550, ..., 1.5555, 1.3025, -0.6296],
  2. [ 0.8270, -0.5372, -0.9559, ..., 0.3665, 0.4338, -0.7505],
  3. [ 0.4956, -0.5133, -0.9323, ..., 1.0773, 1.1913, -0.6240],
  4. [ 0.5770, -0.6258, -0.4833, ..., 0.1171, 1.0069, -1.9030]],
  5. [[-0.4355, -1.7115, -1.5685, ..., -0.6941, -0.1878, -0.1137],
  6. [-0.8867, -1.2207, -1.4151, ..., -0.9618, 0.1722, -0.9562],
  7. [-0.0946, -0.9012, -1.6388, ..., -0.2604, -0.3357, -0.6436],
  8. [-1.1204, -1.4481, -1.5888, ..., -0.8816, -0.6497, 0.0606]]],
  9. grad_fn=<AddBackward0>)
  10. torch.Size([2, 4, 1000])
  • 接着将基于以上结构构建用于训练的模型.


Tansformer模型构建过程的代码分析

  1. def make_model(source_vocab, target_vocab, N=6,
  2. d_model=512, d_ff=2048, head=8, dropout=0.1):
  3. """该函数用来构建模型, 有7个参数,分别是源数据特征(词汇)总数,目标数据特征(词汇)总数,
  4. 编码器和解码器堆叠数,词嵌入维度,前馈全连接网络中变换矩阵的维度,
  5. 多头注意力结构中的多头数,以及置零比率dropout."""
  6. # 首先得到一个深度拷贝命令,接下来很多结构都需要进行深度拷贝,
  7. # 来保证他们彼此之间相互独立,不受干扰.
  8. c = copy.deepcopy
  9. # 实例化了多头注意力类,得到对象attn
  10. attn = MultiHeadedAttention(head, d_model)
  11. # 然后实例化前馈全连接类,得到对象ff
  12. ff = PositionwiseFeedForward(d_model, d_ff, dropout)
  13. # 实例化位置编码类,得到对象position
  14. position = PositionalEncoding(d_model, dropout)
  15. # 根据结构图, 最外层是EncoderDecoder类,在EncoderDecoder中,
  16. # 分别是编码器层,解码器层,源数据Embedding层和位置编码组成的有序结构,
  17. # 目标数据Embedding层和位置编码组成的有序结构,以及类别生成器层.
  18. # 在编码器层中有attention子层以及前馈全连接子层,
  19. # 在解码器层中有两个attention子层以及前馈全连接层.
  20. model = EncoderDecoder(
  21. Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
  22. Decoder(DecoderLayer(d_model, c(attn), c(attn),
  23. c(ff), dropout), N),
  24. #这里的 nn.Sequential()中存放的是两个实例化对象,在程序执行过程中,它会按照先后顺序依次执行,本例中数据先通过词嵌入层,再通过位置编码层
  25. nn.Sequential(Embeddings(d_model, source_vocab), c(position)),
  26. nn.Sequential(Embeddings(d_model, target_vocab), c(position)),
  27. Generator(d_model, target_vocab))
  28. # 模型结构完成后,接下来就是初始化模型中的参数,比如线性层中的变换矩阵
  29. # 这里一但判断参数的维度大于1,则会将其初始化成一个服从均匀分布的矩阵,如果是单一的维度,就直接初始化为0
  30. for p in model.parameters():
  31. if p.dim() > 1:
  32. nn.init.xavier_uniform_(p)
  33. return model
  •      实例化参数和对象:

 

  1. source_vocab = 11
  2. target_vocab = 11
  3. N = 6
  4. # 其他参数都使用默认值
  5. #调用
  6. res = make_model(source_vocab, target_vocab, N)
  7. print(res)

  • 输出效果:

  1. # 根据Transformer结构图构建的最终模型结构
  2. EncoderDecoder(
  3. (encoder): Encoder(
  4. (layers): ModuleList(
  5. (0): EncoderLayer(
  6. (self_attn): MultiHeadedAttention(
  7. (linears): ModuleList(
  8. (0): Linear(in_features=512, out_features=512)
  9. (1): Linear(in_features=512, out_features=512)
  10. (2): Linear(in_features=512, out_features=512)
  11. (3): Linear(in_features=512, out_features=512)
  12. )
  13. (dropout): Dropout(p=0.1)
  14. )
  15. (feed_forward): PositionwiseFeedForward(
  16. (w_1): Linear(in_features=512, out_features=2048)
  17. (w_2): Linear(in_features=2048, out_features=512)
  18. (dropout): Dropout(p=0.1)
  19. )
  20. (sublayer): ModuleList(
  21. (0): SublayerConnection(
  22. (norm): LayerNorm(
  23. )
  24. (dropout): Dropout(p=0.1)
  25. )
  26. (1): SublayerConnection(
  27. (norm): LayerNorm(
  28. )
  29. (dropout): Dropout(p=0.1)
  30. )
  31. )
  32. )
  33. (1): EncoderLayer(
  34. (self_attn): MultiHeadedAttention(
  35. (linears): ModuleList(
  36. (0): Linear(in_features=512, out_features=512)
  37. (1): Linear(in_features=512, out_features=512)
  38. (2): Linear(in_features=512, out_features=512)
  39. (3): Linear(in_features=512, out_features=512)
  40. )
  41. (dropout): Dropout(p=0.1)
  42. )
  43. (feed_forward): PositionwiseFeedForward(
  44. (w_1): Linear(in_features=512, out_features=2048)
  45. (w_2): Linear(in_features=2048, out_features=512)
  46. (dropout): Dropout(p=0.1)
  47. )
  48. (sublayer): ModuleList(
  49. (0): SublayerConnection(
  50. (norm): LayerNorm(
  51. )
  52. (dropout): Dropout(p=0.1)
  53. )
  54. (1): SublayerConnection(
  55. (norm): LayerNorm(
  56. )
  57. (dropout): Dropout(p=0.1)
  58. )
  59. )
  60. )
  61. )
  62. (norm): LayerNorm(
  63. )
  64. )
  65. (decoder): Decoder(
  66. (layers): ModuleList(
  67. (0): DecoderLayer(
  68. (self_attn): MultiHeadedAttention(
  69. (linears): ModuleList(
  70. (0): Linear(in_features=512, out_features=512)
  71. (1): Linear(in_features=512, out_features=512)
  72. (2): Linear(in_features=512, out_features=512)
  73. (3): Linear(in_features=512, out_features=512)
  74. )
  75. (dropout): Dropout(p=0.1)
  76. )
  77. (src_attn): MultiHeadedAttention(
  78. (linears): ModuleList(
  79. (0): Linear(in_features=512, out_features=512)
  80. (1): Linear(in_features=512, out_features=512)
  81. (2): Linear(in_features=512, out_features=512)
  82. (3): Linear(in_features=512, out_features=512)
  83. )
  84. (dropout): Dropout(p=0.1)
  85. )
  86. (feed_forward): PositionwiseFeedForward(
  87. (w_1): Linear(in_features=512, out_features=2048)
  88. (w_2): Linear(in_features=2048, out_features=512)
  89. (dropout): Dropout(p=0.1)
  90. )
  91. (sublayer): ModuleList(
  92. (0): SublayerConnection(
  93. (norm): LayerNorm(
  94. )
  95. (dropout): Dropout(p=0.1)
  96. )
  97. (1): SublayerConnection(
  98. (norm): LayerNorm(
  99. )
  100. (dropout): Dropout(p=0.1)
  101. )
  102. (2): SublayerConnection(
  103. (norm): LayerNorm(
  104. )
  105. (dropout): Dropout(p=0.1)
  106. )
  107. )
  108. )
  109. (1): DecoderLayer(
  110. (self_attn): MultiHeadedAttention(
  111. (linears): ModuleList(
  112. (0): Linear(in_features=512, out_features=512)
  113. (1): Linear(in_features=512, out_features=512)
  114. (2): Linear(in_features=512, out_features=512)
  115. (3): Linear(in_features=512, out_features=512)
  116. )
  117. (dropout): Dropout(p=0.1)
  118. )
  119. (src_attn): MultiHeadedAttention(
  120. (linears): ModuleList(
  121. (0): Linear(in_features=512, out_features=512)
  122. (1): Linear(in_features=512, out_features=512)
  123. (2): Linear(in_features=512, out_features=512)
  124. (3): Linear(in_features=512, out_features=512)
  125. )
  126. (dropout): Dropout(p=0.1)
  127. )
  128. (feed_forward): PositionwiseFeedForward(
  129. (w_1): Linear(in_features=512, out_features=2048)
  130. (w_2): Linear(in_features=2048, out_features=512)
  131. (dropout): Dropout(p=0.1)
  132. )
  133. (sublayer): ModuleList(
  134. (0): SublayerConnection(
  135. (norm): LayerNorm(
  136. )
  137. (dropout): Dropout(p=0.1)
  138. )
  139. (1): SublayerConnection(
  140. (norm): LayerNorm(
  141. )
  142. (dropout): Dropout(p=0.1)
  143. )
  144. (2): SublayerConnection(
  145. (norm): LayerNorm(
  146. )
  147. (dropout): Dropout(p=0.1)
  148. )
  149. )
  150. )
  151. )
  152. (norm): LayerNorm(
  153. )
  154. )
  155. (src_embed): Sequential(
  156. (0): Embeddings(
  157. (lut): Embedding(11, 512)
  158. )
  159. (1): PositionalEncoding(
  160. (dropout): Dropout(p=0.1)
  161. )
  162. )
  163. (tgt_embed): Sequential(
  164. (0): Embeddings(
  165. (lut): Embedding(11, 512)
  166. )
  167. (1): PositionalEncoding(
  168. (dropout): Dropout(p=0.1)
  169. )
  170. )
  171. (generator): Generator(
  172. (proj): Linear(in_features=512, out_features=11)
  173. )
  174. )

 

小节总结

  • 学习并实现了编码器-解码器结构的类: EncoderDecoder

    • 类的初始化函数传入5个参数, 分别是编码器对象, 解码器对象, 源数据嵌入函数, 目标数据嵌入函数, 以及输出部分的类别生成器对象.

    • 类中共实现三个函数, forward, encode, decode

    • forward是主要逻辑函数, 有四个参数, source代表源数据, target代表目标数据, source_mask和target_mask代表对应的掩码张量.

    • encode是编码函数, 以source和source_mask为参数.

    • decode是解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参数


  • 学习并实现了模型构建函数: make_model

    • 有7个参数,分别是源数据特征(词汇)总数,目标数据特征(词汇)总数,编码器和解码器堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,多头注意力结构中的多头数,以及置零比率dropout.

    • 该函数最后返回一个构建好的模型对象.

 

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号