Transformer代码实现
Transformer
seq2seq
-
简介:Seq2Seq技术,全称Sequence to Sequence,该技术突破了传统的固定大小输入问题框架,开通了将经典深度神经网络模型(DNNs)运用于在翻译,文本自动摘要和机器人自动问答以及一些回归预测任务上,并被证实在英语-法语翻译、英语-德语翻译以及人机短问快答的应用中有着不俗的表现。
-
核心思想:Seq2Seq解决问题的主要思路是通过深度神经网络模型(常用的是LSTM,长短记忆网络,一种循环神经网络)将一个作为输入的序列映射为一个作为输出的序列,这一过程由编码(Encoder)输入与解码(Decoder)输出两个环节组成, Encoder负责把序列编码成一个固定长度的向量,这个向量作为输入传Decoder,输出可变长度的向量。
Transformer模型
- 简介:Transformer是一个面向sequence to sequence任务的模型,在2017年的论文《Attention Is All You Need》中首次提出。它由编码器(encoder)和解码器(decoder)组成,其中每个都是多头自注意力模块的叠加。Transformer不使用序列对齐的递归神经网络或卷积神经网络,而是完全依赖自注意力(self-attention)来计算输入和输出的表示。
- 架构图:
文本嵌入层Embedding
-
作用:无论是源文本嵌入(Input Embedding)还是目标文本嵌入(Output Embedding),都是为了将文本中词汇的数字表示转变为向量表示,希望在这样的高维空间捕捉词汇间的关系。
-
代码实现:
import torch import torch.nn as nn import torch.nn.functional as F from torch.autograd import Variable import math # import matplotlib.pyplot as plt import numpy as np import copy from pyitcast.transformer_utils import Batch from pyitcast.transformer_utils import get_std_opt from pyitcast.transformer_utils import LabelSmoothing from pyitcast.transformer_utils import SimpleLossCompute from pyitcast.transformer_utils import run_epoch from pyitcast.transformer_utils import greedy_decode class Embedding(nn.Module): def __init__(self, d_model, vocab): # d_model: 词嵌入的维度,即每个词汇被转换为的向量的长度 # vocab: 词表的大小,即词汇的数量 super(Embedding, self).__init__() # 将参数传入类中 self.d_model = d_model # 定义Embedding层 self.lut = nn.Embedding(vocab, d_model) def forward(self, x): # 在forward函数中,将输入传到Embedding的实例化对象中,然后以一个根号下d_model进行缩放,控制数值大小,最后输出是文本嵌入后的结果。 # x: 代表输入进模型的文本通过词汇映射后的数字张量 return self.lut(x) * math.sqrt(self.d_model)
-
测试输出:
x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) print(x) print(x.shape) d_model = 512 vocab = 1000 embedding = Embedding(d_model, vocab) EmbeddingObject = embedding(x) print(EmbeddingObject) print(EmbeddingObject.shape) # 输出 tensor([[100, 2, 421, 508], [491, 998, 1, 221]]) # 将其想象为一个2行4列的矩阵。 torch.Size([2, 4]) tensor([[[ 20.9322, -51.4943, -14.7932, ..., 4.3573, -25.8562, -7.4067], [ 7.8387, -20.0004, -43.9977, ..., 18.5799, -41.7420, -21.9780], [-41.7615, -37.4706, -16.7876, ..., -1.4215, 1.3570, -30.0393], [-15.9551, 37.0407, 10.1134, ..., 36.7671, 20.4190, 10.1303]], [[-10.4846, -12.0343, 32.1195, ..., 4.7028, 5.8652, -10.1948], [-19.8656, 33.3374, -46.2459, ..., 15.8807, -6.1964, -17.2896], [ 29.8908, -28.8843, -12.5927, ..., 2.8657, -37.5183, -1.6444], [ 52.3843, 1.0207, 13.7238, ..., 31.5098, 36.5223, -13.5179]]], grad_fn=<MulBackward0>) # 将其想象为一个包含2个“块”的数组,每个“块”都是一个4x512的矩阵。 torch.Size([2, 4, 512])
位置编码器层PositionalEncoding
-
作用:因为在transformer的编码器结构中,并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器。词汇位置不同可能会产生不同的语义信息。
-
公式:
-
代码实现:
# 构建位置编码器的类 class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout, max_len=5000): # d_model:代表词嵌入的维度 # dropout: 代表Dropout层的置零比率 # max_len:代表每个句子的最大长度 super(PositionalEncoding, self).__init__() # 实例化Dropout层, Dropout是一种正则化技术,它在训练过程中随机关闭网络的一些神经元。 self.dropout = nn.Dropout(p=dropout) # 初始化一个位置编码矩阵,大小是max_len * d_model pe = torch.zeros(max_len, d_model) # 初始化一个绝对位置矩阵,max_len * 1 # torch.arange(0, max_len):这个函数生成一个从0到max_len-1的整数序列。 # .unsqueeze(1):这个函数将张量的维度增加一维。具体来说,如果原始张量的大小是[N],那么unsqueeze(1)之后,张量的大小会变成[N, 1]。 position = torch.arange(0, max_len).unsqueeze(1) # 定义一个变化矩阵div_term,跳跃式的初始化 # torch.arange(0, d_model, 2): 这会生成一个从0开始,以2为步长,直到d_model(不包括d_model)的整数序列。 # 例如,如果d_model是10,那么这个函数会生成[0, 2, 4, 6, 8]。 # torch.exp(): 这会对上述整数序列中的每个元素计算其指数。 div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)) # 将前面定义的变化矩阵进行奇数,偶数的分别赋值 pe[:, 0::2] = torch.sin(position * div_term) # : 表示选取所有的批次。 # 1::2 表示从第一个维度开始,每隔一个元素选取,即选择第1、3、5...个元素。 pe[:, 1::2] = torch.cos(position * div_term) # 将二维张量扩充成三维张量 pe = pe.unsqueeze(0) # 将位置编码矩阵注册成模型的buffer, 这个buffer不是模型中的参数,不跟随优化器进行更新换代 # 注册成buffer后我们就可以在模型保存后重新加载的时候,将这个位置编码器和模型参数一同加载进来 self.register_buffer('pe', pe) def forward(self, x): # x: 代表文本序列的词嵌入表示 # 首先明确pe的编码太长了,将第二个维度,也就是max_len对应的那个维度缩小成x的句子长度同等的长度 x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) return self.dropout(x)
-
测试输出:
x = EmbeddingObject print(x) print(x.shape) dropout = 0.1 max_len = 60 positionalEncoding = PositionalEncoding(d_model, dropout, max_len) positionalEncodingObject = positionalEncoding(x) print(positionalEncoding) print(positionalEncoding.shape) # 输出 tensor([[[ 25.4406, -0.1463, 9.9571, ..., -11.6741, -11.4295, -29.7098], [ 0.2418, -14.6032, 0.2473, ..., 4.4960, -34.0593, -9.1118], [-19.3168, -12.1143, -33.3925, ..., 22.6954, 4.7615, -28.0245], [ 39.4870, 3.0403, 17.7267, ..., 27.0957, 2.4495, 1.0032]], [[ 9.2127, -22.6310, -30.5518, ..., -6.4104, 6.8917, 10.9890], [ 4.9242, 6.4921, -9.2875, ..., -26.0560, 13.1564, -2.1697], [-10.3924, 62.1448, -35.4104, ..., 5.0267, 16.0242, 10.5429], [ 20.7169, 41.5139, -28.7543, ..., 7.8275, 32.8805, -51.7618]]], grad_fn=<MulBackward0>) torch.Size([2, 4, 512]) tensor([[[ 28.2674, 0.9485, 11.0634, ..., -11.8601, -12.6995, -31.8998], [ 1.2036, -15.6255, 1.1880, ..., 0.0000, -37.8436, -9.0132], [-20.4527, -13.9228, -36.0623, ..., 26.3283, 5.2908, -30.0272], [ 44.0313, 2.2781, 19.9687, ..., 31.2174, 2.7220, 2.2258]], [[ 10.2364, -24.0345, -33.9465, ..., -6.0115, 7.6574, 13.3211], [ 6.4063, 7.8138, -9.4063, ..., -27.8400, 0.0000, -1.2997], [-10.5368, 68.5874, -38.3045, ..., 6.6963, 17.8048, 12.8255], [ 0.0000, 45.0265, -31.6769, ..., 9.8083, 36.5342, -56.4020]]], grad_fn=<MulBackward0>) torch.Size([2, 4, 512])
-
nn.Dropout演示:
>>> m = nn.Dropout(p=0.2) # 置零率20% >>> input = torch.randn(4, 5) >>> output = m(input) >>> output Variable containing: 0.0000 -0.5856 -1.4094 0.0000 -1.0290 2.0591 -1.3400 -1.7247 -0.9885 0.1286 0.5099 1.3715 0.0000 2.2079 -0.5497 -0.0000 -0.7839 -1.2434 -0.1222 1.2815 [torch.FloatTensor of size 4x5]
-
torch.unsqueeze演示:
>>> x = torch.tensor([1, 2, 3, 4]) >>> torch.unsqueeze(x, 0) #torch.unsqueeze() 是一个用于增加张量维度的函数。 tensor([[ 1, 2, 3, 4]]) >>> torch.unsqueeze(x, 1) tensor([[ 1], [ 2], [ 3], [ 4]])
掩码张量Masked
-
定义:掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有0和1;代表位置被遮掩或者不被遮掩。
-
作用:在transformer中,掩码主要的作用有两个,一个是屏蔽掉无效的padding区域,一个是屏蔽掉来自“未来”的信息。Encoder中的掩码主要是起到第一个作用,Decoder中的掩码则同时发挥着两种作用。
- 屏蔽掉无效的padding区域:我们训练需要组batch进行,就以机器翻译任务为例,一个batch中不同样本的输入长度很可能是不一样的,此时我们要设置一个最大句子长度,然后对空白区域进行padding填充,而填充的区域无论在Encoder还是Decoder的计算中都是没有意义的,因此需要用mask进行标识,屏蔽掉对应区域的响应。
- 屏蔽掉来自未来的信息:我们已经学习了attention的计算流程,它是会综合所有时间步的计算的,那么在解码的时候,就有可能获取到未来的信息,这是不行的。因此,这种情况也需要我们使用mask进行屏蔽。现在还没介绍到Decoder,如果没完全理解,可以之后再回过头来思考下。
-
代码实现:
# 构建掩码张量的函数 def subsequent_mask(size): # size: 代码掩码张量后的两个维度,形成一个方阵 attn_shape = (1, size, size) # 使用np.ones()先构建一个全1的张量,然后利用np.tric()形成上三角矩阵 sub_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8') # 使得这个三角矩阵反转 return torch.from_numpy(1 - sub_mask)
-
测试输出:
subsequentMask = subsequent_mask(5) print(subsequentMask) print(subsequentMask.shape) # 输出 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.Size([1, 5, 5])
-
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]])
注意力机制Attention
-
注意力:我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果。正是基于这样的理论,就产生了注意力机制。
-
注意力计算规则:它需要三个指定的输入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相同,这种情况我们称为自注意力机制,就如同我们的刚刚的例子, 使用一般注意力机制,是使用不同于给定文本的关键词表示它. 而自注意力机制, 需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本自身的一次特征提取.
-
什么是注意力机制:注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体。使用自注意力计算规则的注意力机制称为自注意力机制。
-
代码实现:
# 实现注意力机制的类 def attention(query, key, value, mask=None, dropout=None): # query, key, value:代表注意力的三个输入张量 # query: 查询向量,通常表示当前位置的上下文信息。 # key: 键向量,通常表示输入序列中不同位置的信息。 # value: 值向量,与键相对应,表示对应位置的实际信息。 # mask:掩码张量 # dropout: 传入的Dropout实例化对象,用于防止过拟合。 # 首先将query的最后一个维度提取出来,代表的是词嵌入的维度 d_k = query.size(-1) # 按照注意力计算公式,将query和key的转置进行矩阵乘法,然后除以缩放稀疏 scores = torch.matmul(query, key.transpose(-2, -1)) // math.sqrt(d_k) # 判断是否使用掩码张量 if mask is not None: # 利用masked_fill方法,将掩码张量和0进行位置的意义比较,如果等于0,替换成一个非常小的数字 scores = scores.masked_fill(mask == 0, -1e9) # 对scores的最后一个维度上进行softmax操作,softmax函数可以将输入的原始分数转换成概率分布。 p_attn = F.softmax(scores, dim=-1) # 判断是否使用dropout if dropout is not None: p_attn = dropout(p_attn) # 最后一步完成p_attn和value张量的乘法,并返回query注意力表示 return torch.matmul(p_attn, value), p_attn
-
测试输出:
query = key = value = positionalEncodingObject attn, p_attn = attention(query, key, value) print(attn) print(attn.shape) print('*****') print(p_attn) print(p_attn.shape) # 输出 tensor([[[ -8.0231, 2.7090, 35.9290, ..., -13.6550, 0.9085, 0.0000], [ 4.9934, -13.7417, -21.7806, ..., 45.8677, 41.5174, -30.6068], [ 22.3839, -48.4813, 3.1680, ..., 56.4129, -11.7720, 44.1821], [ -8.3350, 0.2687, 31.0257, ..., -2.5953, -36.2484, -12.4895]], [[-25.5093, -13.5794, -12.0962, ..., -7.7975, 0.0000, 0.0000], [ 7.9462, 0.0000, 0.0000, ..., 9.3136, 2.5337, -37.3172], [ 73.0789, 0.0000, 28.9866, ..., 21.9179, -29.5257, -10.9643], [-20.8747, -6.6917, 0.0000, ..., 8.6810, 21.4550, 74.4928]]], grad_fn=<UnsafeViewBackward0>) torch.Size([2, 4, 512]) ***** tensor([[[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]], [[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>) torch.Size([2, 4, 4])
-
tensor.masked_fill演示:
>>> input = Variable(torch.randn(5, 5)) >>> 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)) >>> 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]
多头注意力机制MultiHeadAttention
-
定义:每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量。这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成了多头注意力机制。
-
结构图:
-
作用:这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果。
-
代码实现:
# 实现克隆函数,因为在多头注意力机制下,要用到多个结构相同的线性层 # 需要使用clone函数将他们一同初始化到一个网络层列表对象中 def clone(model, N): # model: 代表要克隆的目标网络层 # N: 将model克隆几个 return nn.ModuleList([copy.deepcopy(model) for _ in range(N)]) # 实现多头注意力机制的类 class MultiHeadAttention(nn.Module): def __init__(self, head, embedding_dim, dropout=0.1): # head: 代表几个头的参数 # embedding_dim: 代表词嵌入的维度 # dropout: 进行Dropout操作时,置零的比率 super(MultiHeadAttention, self).__init__() # 要确认一个事实,多头的数量head需要整除词嵌入的维度embedding_dim assert embedding_dim % head == 0 self.head = head self.embedding_dim = embedding_dim # 得到每个头获得的词向量的维度 self.d_k = embedding_dim // head # 获得线性层,要获得4个,分别是Q,K,V以及最终的输出线性层 self.linears = clone(nn.Linear(embedding_dim, embedding_dim), 4) # 初始化注意力张量 self.attn = None # 初始化dropout对象 self.dropout = nn.Dropout(p=dropout) def forward(self, query, key, value, mask=None): # query, key, value是注意力机制的三个张量,mask代表掩码张量 # 首先判断是否使用掩码张量 if mask is not None: # 使用squeeze将掩码张量进行维度扩充,代表多头中的第n个头 mask = mask.unsqueeze(0) # 得到batch_size batch_size = query.size(0) # 首先使用zip将网络层和输入数据连接在一起,模型输出利用view和transpose进行维度和形状的改变 # 首先利用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))] # 将每个头的输出传入到注意力层 x, self.attn = attention(query, key, value, mask, self.dropout) # 得到每个头的计算结果是4维张量,需要进行形状的转换 # 前面已经将1,2两个维度进行过转置,在这里要重新转置回来 # 注意:经历了transpose()方法后,必须要使用contiguous方法,不然无法使用view()方法 x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k) # 最后将x输入线性层列表中的最后一个线性层中进行处理,得到最终的多头注意力结构输出 return self.linears[-1](x)
-
测试输出:
# 实例化若干参数 head = 8 embedding_dim = 512 dropout = 0.2 # 若干输入参数的初始化 query = key = value = positionalEncodingObject mask = Variable(torch.zeros(8, 4, 4)) multiHeadAttention = MultiHeadAttention(head, embedding_dim, dropout) multiHeadAttention_result = multiHeadAttention(query, key, value, mask) print(multiHeadAttention_result) print(multiHeadAttention_result.shape) # 输出 tensor([[[ 8.5721, 10.1739, -2.9751, ..., 0.3821, -4.8186, -0.5123], [ 4.0143, 6.7001, -0.9090, ..., 4.4585, -0.2625, 0.2795], [ 5.3405, 9.1134, 3.7066, ..., 5.6258, 1.2300, 0.4893], [ 8.0025, 8.4887, 0.9138, ..., 6.3994, -3.2864, 0.0521]], [[-3.6769, -2.1025, -1.0527, ..., -3.7192, 6.6875, -5.3920], [-5.1498, 2.2510, -1.5451, ..., -7.5426, 5.3772, -2.8382], [-7.8900, -1.4180, -0.9723, ..., -3.6644, 7.1595, -9.4663], [-3.3870, -1.4552, -0.6836, ..., -5.3271, 6.7151, -8.5503]]], grad_fn=<ViewBackward0>) torch.Size([2, 4, 512])
-
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]])
前馈全连接层PositionalwiseFeedForward
-
定义:在Transformer中前馈全连接层就是具有两层线性层的全连接网络。
-
计算规则:
-
作用:考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力。
-
代码实现:
# 构建前馈全连接网络类 class PositionalwiseFeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout=0.1): # d_model:代表词嵌入的维度,同时也是两个线性层的输入维度和输出维度 # d_ff: 代表第一个线性层的输出维度,和第二个线性层的输入维度 # dropout:经过Dropout层处理时,随机置零的比率 super(PositionalwiseFeedForward, self).__init__() # 定义两层全连接的线性层 self.w1 = nn.Linear(d_model, d_ff) self.w2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(p=dropout) def forward(self, x): # x: 代表来自上一层的输出 # 首先将x送入第一个线性层网络,然后经历relu函数的激活,再经历dropout层的处理 # 最后送入第二个线性层 # ReLU函数公式: ReLU(x)=max(0, x) return self.w2(self.dropout(F.relu(self.w1(x))))
-
测试输出:
x = multiHeadAttention_result d_model = 512 d_ff = 64 dropout = 0.2 ff = PositionalwiseFeedForward(d_model, d_ff, dropout) ff_result = ff(x) print(ff_result) print(ff_result.shape) # 输出 tensor([[[ 2.4549, -1.3077, -1.2298, ..., -2.7669, -0.1127, -0.5245], [ 1.5202, -0.4648, -0.8775, ..., -1.9649, 0.9899, -1.0539], [ 2.5546, -1.4445, -0.5647, ..., -1.7741, 0.5518, -0.7363], [ 2.0396, -1.8694, -0.9712, ..., -2.1345, -0.7282, -2.2315]], [[ 1.6249, -2.1991, -2.1690, ..., -0.3968, -0.3625, -0.5384], [ 0.2948, -1.0522, -1.0227, ..., 0.4925, 0.3458, -0.1617], [ 0.8061, -2.1172, -1.4675, ..., -0.0106, -0.2260, 0.1426], [ 1.6552, -1.6434, -1.4215, ..., 0.1458, -0.7503, -0.9788]]], grad_fn=<ViewBackward0>) torch.Size([2, 4, 512])
规范化层LayerNorm
-
作用:它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内。
-
代码实现:
# 构建规范层的类 class LayerNorm(nn.Module): def __init__(self, features, eps=1e-6): # features: 代表词嵌入的维度 # eps: 一个足够小的正数,用来在规范化计算公式的分母中,防止除零操作 super(LayerNorm, self).__init__() self.eps = eps # 初始化两个参数张量a2, b2, 用于对结果做规范化操作计算 # 将其永nn.Parameter进行封装,代表他们也是模型中的参数 self.a2 = nn.Parameter(torch.ones(features)) self.b2 = nn.Parameter(torch.zeros(features)) def forward(self, x): # x: 代表上一层网络的输出 # 首先对x进行最后一个维度上的求均值操作,同时操持输出维度和输入维度一致 mean = x.mean(-1, keepdim=True) # 接着对x进行字后一个维度上的求标准差的操作,同时保持输出维度和输入维度一致 std = x.std(-1, keepdim=True) # 按照规范化公式进行计算并返回 # 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,加上位移参数b2。返回即可。 return self.a2 * (x - mean) / (std + self.eps) + self.b2
-
测试输出:
features = d_model = 512 eps = 1e-6 x = ff_result ln = LayerNorm(features, eps) ln_result = ln(x) print(ln_result) print(ln_result.shape) # 输出 tensor([[[ 1.7069, -0.9589, -0.0450, ..., 0.0984, 0.3013, 1.7707], [ 1.4454, -0.6464, -1.3580, ..., 0.6166, -1.5769, 1.8748], [ 0.9367, -1.4668, -1.8986, ..., 0.2531, -1.1650, 1.1912], [ 1.3384, -1.2680, -1.8974, ..., 1.0951, 0.3698, 2.7684]], [[-0.0063, 1.4429, -1.0439, ..., 0.5650, 0.4948, -0.9835], [ 0.4989, 1.1399, -1.0816, ..., 0.7902, 0.4342, -0.8387], [ 0.5844, 0.5935, -0.5466, ..., 0.1880, 0.1384, -0.8957], [ 1.5526, 0.4065, 0.5671, ..., 0.2406, -0.1133, -0.9655]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
子层连接结构SubLayerConnection
-
定义:如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构。
-
结构图:
-
代码实现:
# 构建子层连接结构的类 class SubLayerConnection(nn.Module): def __init__(self, size, dropout=0.1): # size: 代表词嵌入的维度 # dropout: 进行Dropout操作的置零比率 super(SubLayerConnection, self).__init__() self.size = size self.dropout = nn.Dropout(p=dropout) # 实例化一个dropout对象 self.norm = LayerNorm(size) def forward(self, x, sublayer): # x: 代表上一层传入的张量 # sublayer: 该子层连接中子层函数 # 首先将x进行规范化,然后送入子层函数中处理,处理结果进入dropout层,最后进行残差连接 # 因为存在跳跃连接(残差连接),所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出. # sublayer = lambda x: self_attn(x, x, x, mask) return x + self.dropout(sublayer(self.norm(x)))
-
测试输出:
size = 512 dropout = 0.2 x = positionalEncodingObject self_attn = MultiHeadAttention(head, d_model) sublayer = lambda x: self_attn(x, x, x, mask) subLayerConnection = SubLayerConnection(size, dropout) subLayerConnection_result = subLayerConnection(x, sublayer) print(subLayerConnection_result) print(subLayerConnection_result.shape) # 输出 tensor([[[ 0.0000, 18.6041, -34.5794, ..., -22.7144, -22.9378, 42.8660], [ 0.4481, 25.0820, 32.5948, ..., 41.5272, 3.9152, 17.3423], [ -0.2508, -0.1172, -14.1209, ..., 17.9321, 24.1920, -29.2257], [-42.5516, -5.1233, -31.1811, ..., 37.4338, -14.2385, -15.8819]], [[ 4.1351, 0.0000, 8.7511, ..., -3.8770, -7.3691, 51.4203], [ 5.2168, -0.0591, -24.7429, ..., -26.8651, 53.1842, 26.0121], [ 30.4731, 22.9993, 1.8861, ..., 53.1777, 12.6383, -15.2948], [ 21.6280, 4.3722, -15.9978, ..., -34.3383, -26.1764, -23.0221]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
编码器层EncoderLayer
-
作用:作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程, 即编码过程。
-
结构图:
-
代码实现:
# 构建编码层的类 class EncoderLayer(nn.Module): def __init__(self, size, self_attn, feed_forward, dropout): # size: 代表词嵌入的维度 # self_attn: 代表传入的多头自注意力子层的实例化对象 # feed_forward: 代表前馈全连接层实例化对象 # dropout: 进行dropout操作时的置零比率 super(EncoderLayer, self).__init__() # 将两个实例化对象和参数传入类中 self.size = size self.self_attn = self_attn self.feed_forward = feed_forward self.dropout = nn.Dropout(p=dropout) # 编码器中有2个子层连接结构,使用clones函数进行操作 self.sublayer = clone(SubLayerConnection(size, dropout), 2) def forward(self, x, mask): # x: 代表上一层的传入张量 # mask: 代表掩码张量 # 首先让x经过第一个子层连接结构,内部包含多头自注意力机制子层 # 再让张量经过第二个子层连接结构,其中包含前馈全连接网络 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 = positionalEncodingObject dropout = 0.2 self_attn = MultiHeadAttention(head, d_model) ff = PositionalwiseFeedForward(d_model, d_ff, dropout) mask = Variable(torch.zeros(8, 4, 4)) encoderLayer = EncoderLayer(size, self_attn, ff, dropout) encoderLayer_result = encoderLayer(x, mask) print(encoderLayer_result) print(encoderLayer_result.shape) # 输出 tensor([[[-51.1375, 12.3637, 1.1911, ..., -44.6784, 2.2108, -19.6772], [ 53.1009, 23.9683, -9.0194, ..., -13.9445, 18.3097, -1.9705], [ 21.5652, -36.5157, 23.2846, ..., -0.4406, -19.1261, 24.9179], [ -7.5961, 9.9738, 7.9152, ..., 0.4272, 0.1992, -56.0868]], [[ 8.9895, -0.4793, 14.1161, ..., 0.1511, -9.1903, 16.8203], [ 12.3082, 7.1729, 18.4264, ..., -11.3750, 31.7641, 21.6821], [-21.9309, 55.3286, -5.8807, ..., 20.1439, 0.6204, 35.1122], [ 0.4908, -4.9080, -30.6053, ..., 17.8941, -29.7550, -32.1215]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
编码器Encoder
-
作用:编码器用于对输入进行指定的特征提取过程,也称为编码,由N个编码器层堆叠而成。
-
代码实现:
# 构建编码器类Encoder class Encoder(nn.Module): def __init__(self, layer, N): # layer: 代表编码器层 # N:代表编码层中有几个layer super(Encoder, self).__init__() # 首先使用clones函数克隆N个编码层放置在self.layer中 self.layers = clone(layer, N) # 初始化一个规范化层,作用在编码层的最后面 self.norm = LayerNorm(layer.size) def forward(self, x, mask): # x: 代表上一层输出的张量 # mask: 代表掩码张量 # 让x依次经历N个编码层的处理,最后再经过规范层就可以输出了 for layer in self.layers: x = layer(x, mask) return self.norm(x)
-
测试输出:
size = 512 x = positionalEncodingObject c = copy.deepcopy attn = MultiHeadAttention(head, d_model) ff = PositionalwiseFeedForward(d_model, d_ff, dropout) dropout = 0.2 layer = EncoderLayer(size, c(attn), c(ff), dropout) N = 8 encoder = Encoder(layer, N) encoder_result = encoder(x, mask) print(encoder_result) print(encoder_result.shape) # 输出 tensor([[[-1.4475, 1.8613, 1.3698, ..., -0.7844, -0.4799, 1.2193], [-0.5347, -0.0231, -1.0717, ..., 0.7434, -0.5930, 1.3094], [-1.1944, 0.8164, 1.6217, ..., 0.4766, -0.5818, -0.2193], [ 0.1660, -0.3936, -0.9785, ..., -1.1505, -1.3357, -1.0917]], [[ 0.2486, -1.8450, -1.7822, ..., -0.7271, 1.2453, 0.7618], [ 0.8398, 0.3417, -0.8577, ..., -0.1886, -0.4140, -1.8356], [-0.0806, 0.4436, 3.0465, ..., 0.8352, 1.2329, -0.5215], [-0.0969, 1.4896, -0.0977, ..., -2.1358, -1.2012, 0.1451]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
解码器层DecoderLayer
-
作用:作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程。
-
结构图:
-
代码实现:
# 构建解码器层类 class DecoderLayer(nn.Module): def __init__(self, size, self_attn, src_attn, feed_forward, dropout=0.1): # size: 代表词嵌入的维度 # self_attn: 代表多头自注意力机制的对象,就是说这个注意力机制需要Q=K=V # src_attn: 代表常规的注意力机制的对象,Q!=K=V # feed_forward: 代表前馈全连接层的对象 # dropout: 代表Dropout的置零比率 super(DecoderLayer, self).__init__() # 将参数传入类中 self.size = size self.self_attn = self_attn self.src_attn = src_attn self.feed_forward = feed_forward self.dropout = nn.Dropout(p=dropout) # 按照解码器层的结构图,使用clones函数克隆3个子层连接对象 self.sublayer = clone(SubLayerConnection(size, dropout), 3) def forward(self, x, memory, source_mask, target_mask): # x: 代表上一层输入的张量 # memory: 代表编码器的语义存储张量 # source_mask: 源数据的掩码张量 # target_mask:目标数据的掩码张量 m = memory # 第一步让x经历第一个子层,多头自注意力机制的子层 # 采用target_mask,为了将解码时未来的信息进行遮掩,比如模型解码第二个字符,只能看见第一个字符信息 # 将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,因为是自注意力机制,所以Q,K,V都是x, # 最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据, # 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失, # 但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时, # 模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用. x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask)) # 第二步让x经历第二个子层,常规的注意力机制的子层,Q!=K=V # 接着进入第二个子层,这个子层中常规的注意力机制,q是输入x; k,v是编码层输出memory, # 同样也传入source_mask,但是进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力值, # 以此提升模型效果和训练速度. 这样就完成了第二个子层的处理. x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask)) # 第三步让x经历第三个子层,前馈全连接层 return self.sublayer[2](x, self.feed_forward)
-
测试输出:
head = 8 size = 512 d_model = 512 d_ff = 64 dropout = 0.2 self_attn = MultiHeadAttention(head, d_model, dropout) src_attn = MultiHeadAttention(head, d_model, dropout) ff = PositionalwiseFeedForward(d_model, d_ff, dropout) x = positionalEncodingObject memory = encoder_result source_mask = target_mask = mask decoderLayer = DecoderLayer(size, self_attn, src_attn, ff, dropout) decoderLayer_result = decoderLayer(x, memory, source_mask, target_mask) print(decoderLayer_result) print(decoderLayer_result.shape) # 输出 tensor([[[ 1.1368e+01, -2.0912e+01, 2.7469e+01, ..., -5.5801e+00, -1.6709e+01, 4.9490e+01], [ 7.4400e-01, 4.2864e-01, -3.0174e+01, ..., -4.8554e-01, 2.8395e+01, -3.8213e+01], [ 1.5695e+01, -5.2318e+01, 4.4639e+00, ..., 1.8662e+01, 1.9034e+01, 3.4696e+00], [ 4.1799e+00, -5.5088e+01, -4.2885e+01, ..., 9.3022e-03, -9.3341e-02, -1.4480e+01]], [[-3.1264e-01, -3.2143e+01, -1.6388e+01, ..., -2.5739e+01, -4.9677e+00, -1.6531e+00], [-5.3797e+01, 3.5809e+01, 1.0892e+01, ..., 1.3330e+00, -2.2780e+00, 3.6050e+01], [ 9.4846e-02, -5.1600e-01, 6.0058e+00, ..., -5.2425e+00, -3.2987e+01, -1.6913e+01], [ 1.3349e+01, -1.2124e+01, -3.4833e+01, ..., 5.6134e+01, -2.4085e+01, 1.5357e+01]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
解码器Decoder
-
作用:根据编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示。
-
代码实现:
# 构建解码器类 class Decoder(nn.Module): def __init__(self, layer, N): # layer:代表解码器层的对象 # N:代表将layer进行几层的拷贝 super(Decoder, self).__init__() self.N = N # 利用clones函数克隆N个layer self.layers = clone(layer, N) # 实例化一个规范化层 self.norm = LayerNorm(layer.size) def forward(self, x, memory, source_mask, target_mask): # x: 代表目标数据的嵌入表示 # memory: 代表编码器的输出张量 # source_mask: 源数据的掩码张量 # target_mask:目标数据的掩码张量 # 要将x依次经历所有的编码器层处理,最后通过规范化层 for layer in self.layers: x = layer(x, memory, source_mask, target_mask) return self.norm(x)
-
测试输出:
size = 512 d_model = 512 head = 8 d_ff = 64 dropout = 0.2 c = copy.deepcopy attn = MultiHeadAttention(head, d_model, dropout) ff = PositionalwiseFeedForward(d_model, d_ff, dropout) layer = DecoderLayer(size, c(attn), c(attn), c(ff), dropout) N = 8 x = positionalEncodingObject memory = positionalEncodingObject mask = Variable(torch.zeros(8, 4, 4)) source_mask = target_mask = mask decoder = Decoder(layer, N) decoder_result = decoder(x, memory, source_mask, target_mask) print(decoder_result) print(decoder_result.shape) # 输出 tensor([[[-0.3512, -1.4253, -0.3600, ..., 0.7636, 0.1939, 0.3197], [ 0.1987, -0.8045, -0.8271, ..., 1.2602, 0.6039, 0.3763], [ 0.3300, -0.2209, -1.1473, ..., -0.2402, 1.5630, 0.5502], [-1.0890, -0.1592, -1.3271, ..., 1.1127, 0.5532, 0.7165]], [[ 0.9529, -0.7351, 2.1126, ..., 0.3900, -1.0532, 1.6397], [ 0.3718, -1.9382, 1.8154, ..., 1.5813, -0.8640, 1.1192], [-0.0669, -1.9519, 0.6897, ..., 0.3990, -0.2995, 1.8925], [ 0.1525, -1.3854, 2.1504, ..., -0.0210, -1.0353, 1.1538]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
输出部分Generator
-
线性层的作用:通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用。
-
softmax层的作用:使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1。
-
结构图:
-
代码实现:
# 将线性层和softmax计算层一起实现, 因为二者的共同目标是生成最后的结构 # 因此把类的名字叫做Generator, 生成器类 class Generator(nn.Module): def __init__(self, d_model, vocab_size): # d_model: 代表词嵌入的维度 # vocab_size:代表词表的总大小 super(Generator, self).__init__() self.vocab_size = vocab_size self.embedding_dim = embedding_dim # 定义一个线性层,作用是完成网络输出维度的变换 self.project = nn.Linear(d_model, vocab_size) def forward(self, x): # x: 代表上一层的输出张量 # 首先将x送入线性层中,让其经历softmax的处理 return F.log_softmax(self.project(x), dim=-1)
-
测试输出:
d_model = 512 vocab_size = 1000 x = decoder_result gen = Generator(d_model, vocab_size) gen_result = gen(x) print(gen_result) print(gen_result.shape) # 输出 tensor([[[-6.9884, -7.0642, -7.2207, ..., -7.2615, -7.9681, -6.4965], [-7.2761, -6.8583, -6.7529, ..., -6.5239, -7.0339, -6.4207], [-7.1865, -7.2196, -6.7001, ..., -7.1264, -7.8712, -6.9600], [-6.4563, -7.3869, -7.1341, ..., -6.8356, -7.7405, -6.7395]], [[-6.6164, -7.5654, -6.3168, ..., -7.6955, -6.9655, -6.8627], [-6.2549, -6.4234, -6.6807, ..., -7.4863, -7.7970, -7.1111], [-6.0643, -6.7691, -6.8876, ..., -7.0476, -6.3615, -8.1939], [-6.0114, -7.2431, -6.5140, ..., -7.0258, -7.3602, -7.9179]]], grad_fn=<LogSoftmaxBackward0>) torch.Size([2, 4, 1000])
-
nn.Linear演示:
>>> linear = nn.Linear(20, 30) >>> input = torch.randn(128, 20) >>> output = linear(input) >>> print(output.size()) torch.Size([128, 30])
编码器-解码器EncoderDecoder
-
代码实现:
# 构建编码器-解码器结构类 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 = encoder decoder = decoder 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([[[-7.2573, -7.0272, -5.9304, ..., -6.6923, -7.7866, -7.4421], [-7.4487, -7.0440, -6.5185, ..., -6.1462, -7.1469, -7.7528], [-7.1498, -6.9138, -5.9281, ..., -6.3195, -7.5294, -7.0373], [-7.0366, -6.9079, -5.8545, ..., -6.9902, -7.4398, -7.2774]], [[-6.9457, -8.1266, -6.5587, ..., -7.5354, -7.9238, -7.7191], [-6.8831, -8.1027, -6.3820, ..., -7.2989, -7.8086, -7.2228], [-7.7211, -7.6510, -6.6136, ..., -7.3051, -7.6189, -6.7890], [-7.2824, -7.9022, -6.5301, ..., -7.5771, -7.4424, -7.6317]]], grad_fn=<LogSoftmaxBackward0>) torch.Size([2, 4, 1000])
模型构建make_model
-
代码实现:
def make_model(source_vocab, target_vocab, N=6, d_model=512, d_ff=2048, head=8, dropout=0.1): # source_vocab:代表源数据的词汇总数 # target_vocab:代表目标数据的词汇总数 # N:代表编码器和解码器堆叠的层数 # d_model:代表词嵌入的维度 # d_ff:代表前馈全连接层中变换矩阵的维度 # head:多头注意力机制中的头数 # dropout:指置零的比率 c = copy.deepcopy # 实例化一个多头注意力的类 attn = MultiHeadAttention(head, d_model, dropout) # 实例化一个前馈全连接层的网络对象 ff = PositionalwiseFeedForward(d_model, d_ff, dropout) # 实例化一个位置编码器 position = PositionalEncoding(d_model, dropout) # 实例化模型model, 利用的是EncoderDecoder类 # 编码器的结构里面有2个子层,attention层和前馈全连接层 # 解码器的结构中有3个子层,两个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(Embedding(d_model, source_vocab), c(position)), nn.Sequential(Embedding(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
-
测试输出:
source_vocab = 11 target_vocab = 11 N = 6 if __name__ == '__main__': res = make_model(source_vocab, target_vocab, N) print(res) # 输出 EncoderDecoder( (encoder): Encoder( (layers): ModuleList( (0-5): 6 x EncoderLayer( (self_attn): MultiHeadAttention( (linears): ModuleList( (0-3): 4 x Linear(in_features=512, out_features=512, bias=True) ) (dropout): Dropout(p=0.1, inplace=False) ) (feed_forward): PositionalwiseFeedForward( (w1): Linear(in_features=512, out_features=2048, bias=True) (w2): Linear(in_features=2048, out_features=512, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (dropout): Dropout(p=0.1, inplace=False) (sublayer): ModuleList( (0-1): 2 x SubLayerConnection( (dropout): Dropout(p=0.1, inplace=False) (norm): LayerNorm() ) ) ) ) (norm): LayerNorm() ) (decoder): Decoder( (layers): ModuleList( (0-5): 6 x DecoderLayer( (self_attn): MultiHeadAttention( (linears): ModuleList( (0-3): 4 x Linear(in_features=512, out_features=512, bias=True) ) (dropout): Dropout(p=0.1, inplace=False) ) (src_attn): MultiHeadAttention( (linears): ModuleList( (0-3): 4 x Linear(in_features=512, out_features=512, bias=True) ) (dropout): Dropout(p=0.1, inplace=False) ) (feed_forward): PositionalwiseFeedForward( (w1): Linear(in_features=512, out_features=2048, bias=True) (w2): Linear(in_features=2048, out_features=512, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (dropout): Dropout(p=0.1, inplace=False) (sublayer): ModuleList( (0-2): 3 x SubLayerConnection( (dropout): Dropout(p=0.1, inplace=False) (norm): LayerNorm() ) ) ) ) (norm): LayerNorm() ) (src_embed): Sequential( (0): Embedding( (lut): Embedding(11, 512) ) (1): PositionalEncoding( (dropout): Dropout(p=0.1, inplace=False) ) ) (tgt_embed): Sequential( (0): Embedding( (lut): Embedding(11, 512) ) (1): PositionalEncoding( (dropout): Dropout(p=0.1, inplace=False) ) ) (generator): Generator( (project): Linear(in_features=512, out_features=11, bias=True) ) )