1 输入部分介绍
输入部分包含:
- 源文本嵌入层及其位置编码器
- 目标文本嵌入层及其位置编码器
2 文本嵌入层的作用
-
无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 希望在这样的高维空间捕捉词汇间的关系.
-
文本嵌入层的代码分析:
# 导入必备的工具包 import torch # 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层, # 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子. import torch.nn as nn # 数学计算工具包 import math # torch中变量封装函数Variable. from torch.autograd import Variable # 定义Embeddings类来实现文本嵌入层,这里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相乘作为结果返回 # 让 embeddings vector 在增加 之后的 postion encoing 之前相对大一些的操作, # 主要是为了让position encoding 相对的小,这样会让原来的 embedding vector 中的信息在和 position encoding 的信息相加时不至于丢失掉 # 让 embeddings vector 相对大一些 return self.lut(x) * math.sqrt(self.d_model)
- nn.Embedding演示:
>>> embedding = nn.Embedding(10, 3)
>>> input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
>>> embedding(input)
tensor([[[-0.0251, -1.6902, 0.7172],
[-0.6431, 0.0748, 0.6969],
[ 1.4970, 1.3448, -0.9685],
[-0.3677, -2.7265, -0.1685]],
[[ 1.4970, 1.3448, -0.9685],
[ 0.4362, -0.4004, 0.9400],
[-0.6431, 0.0748, 0.6969],
[ 0.9124, -2.3616, 1.1151]]])
>>> embedding = nn.Embedding(10, 3, padding_idx=0)
>>> input = torch.LongTensor([[0,2,0,5]])
>>> embedding(input)
tensor([[[ 0.0000, 0.0000, 0.0000],
[ 0.1535, -2.0309, 0.9315],
[ 0.0000, 0.0000, 0.0000],
[-0.1655, 0.9897, 0.0635]]])
- 实例化参数:
# 词嵌入维度是512维 d_model = 512 # 词表大小是1000 vocab = 1000
- 输入参数:
# 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
- 调用:
emb = Embeddings(d_model, vocab) embr = emb(x) print("embr:", embr)
- 输出效果:
embr: Variable containing: ( 0 ,.,.) = 35.9321 3.2582 -17.7301 ... 3.4109 13.8832 39.0272 8.5410 -3.5790 -12.0460 ... 40.1880 36.6009 34.7141 -17.0650 -1.8705 -20.1807 ... -12.5556 -34.0739 35.6536 20.6105 4.4314 14.9912 ... -0.1342 -9.9270 28.6771 ( 1 ,.,.) = 27.7016 16.7183 46.6900 ... 17.9840 17.2525 -3.9709 3.0645 -5.5105 10.8802 ... -13.0069 30.8834 -38.3209 33.1378 -32.1435 -3.9369 ... 15.6094 -29.7063 40.1361 -31.5056 3.3648 1.4726 ... 2.8047 -9.6514 -23.4909 [torch.FloatTensor of size 2x4x512]
3 位置编码器的作用
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
3.1 位置编码器的代码分析
# 定义位置编码器类, 我们同样把它看做一个层, 因此会继承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)
# 初始化一个绝对位置矩阵, 在我们这里,词汇的绝对位置就是用它的索引去表示.
# 所以我们首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
# 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个max_len x 1 的矩阵,
position = torch.arange(0, max_len).unsqueeze(1)
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
# 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
# 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵,
# 而是有了一个跳跃,只初始化了一半即1xd_model/2 的矩阵。 为什么是一半呢,其实这里并不是真正意义上的初始化了一半的矩阵,
# 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵.
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵,要想和embedding的输出(一个三维张量)相加,
# 就必须拓展一个维度,所以这里使用unsqueeze拓展维度.
pe = pe.unsqueeze(0)
# 最后把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)
# 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
return self.dropout(x)
- nn.Dropout演示:
>>> m = nn.Dropout(p=0.2) >>> 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) 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 containing: ( 0 ,.,.) = 35.9321 3.2582 -17.7301 ... 3.4109 13.8832 39.0272 8.5410 -3.5790 -12.0460 ... 40.1880 36.6009 34.7141 -17.0650 -1.8705 -20.1807 ... -12.5556 -34.0739 35.6536 20.6105 4.4314 14.9912 ... -0.1342 -9.9270 28.6771 ( 1 ,.,.) = 27.7016 16.7183 46.6900 ... 17.9840 17.2525 -3.9709 3.0645 -5.5105 10.8802 ... -13.0069 30.8834 -38.3209 33.1378 -32.1435 -3.9369 ... 15.6094 -29.7063 40.1361 -31.5056 3.3648 1.4726 ... 2.8047 -9.6514 -23.4909 [torch.FloatTensor of size 2x4x512]
- 调用:
pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) print("pe_result:", pe_result)
- 输出效果:
pe_result: Variable containing: ( 0 ,.,.) = -19.7050 0.0000 0.0000 ... -11.7557 -0.0000 23.4553 -1.4668 -62.2510 -2.4012 ... 66.5860 -24.4578 -37.7469 9.8642 -41.6497 -11.4968 ... -21.1293 -42.0945 50.7943 0.0000 34.1785 -33.0712 ... 48.5520 3.2540 54.1348 ( 1 ,.,.) = 7.7598 -21.0359 15.0595 ... -35.6061 -0.0000 4.1772 -38.7230 8.6578 34.2935 ... -43.3556 26.6052 4.3084 24.6962 37.3626 -26.9271 ... 49.8989 0.0000 44.9158 -28.8435 -48.5963 -0.9892 ... -52.5447 -4.1475 -3.0450 [torch.FloatTensor of size 2x4x512]
3.2 绘制词汇向量中特征的分布曲线
import matplotlib.pyplot as plt import numpy as np # 创建一张15 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这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算.
4 解码器介绍
解码器部分:
- 由N个解码器层堆叠而成
- 每个解码器层由三个子层连接结构组成
- 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
- 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
- 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
- 说明:
- 解码器层中的各个部分,如,多头注意力机制,规范化层,前馈全连接网络,子层连接结构都与编码器中的实现相同. 因此这里可以直接拿来构建解码器层.
5 解码器层
5.1 解码器层的作用:
- 作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.
5.2 解码器层的代码实现
# 使用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, 第四个是前馈全连接层对象,最后就是droupout置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,
来自编码器层的语义存储变量mermory, 以及源数据掩码张量和目标数据掩码张量.
"""
# 将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([[[ 1.9604e+00, 3.9288e+01, -5.2422e+01, ..., 2.1041e-01, -5.5063e+01, 1.5233e-01], [ 1.0135e-01, -3.7779e-01, 6.5491e+01, ..., 2.8062e+01, -3.7780e+01, -3.9577e+01], [ 1.9526e+01, -2.5741e+01, 2.6926e-01, ..., -1.5316e+01, 1.4543e+00, 2.7714e+00], [-2.1528e+01, 2.0141e+01, 2.1999e+01, ..., 2.2099e+00, -1.7267e+01, -1.6687e+01]], [[ 6.7259e+00, -2.6918e+01, 1.1807e+01, ..., -3.6453e+01, -2.9231e+01, 1.1288e+01], [ 7.7484e+01, -5.0572e-01, -1.3096e+01, ..., 3.6302e-01, 1.9907e+01, -1.2160e+00], [ 2.6703e+01, 4.4737e+01, -3.1590e+01, ..., 4.1540e-03, 5.2587e+00, 5.2382e+00], [ 4.7435e+01, -3.7599e-01, 5.0898e+01, ..., 5.6361e+00, 3.5891e+01, 1.5697e+01]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
5.3 解码器层总结
-
学习了解码器层的作用:
- 作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.
-
学习并实现了解码器层的类: DecoderLayer
- 类的初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层的尺寸,第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,第三个是src_attn,多头注意力对象,这里Q!=K=V, 第四个是前馈全连接层对象,最后就是droupout置0比率.
- forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储变量mermory, 以及源数据掩码张量和目标数据掩码张量.
- 最终输出了由编码器输入和目标数据一同作用的特征提取结果.
6 解码器
6.1 解码器的作用
- 根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示.
6.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.9898, -0.3216, -1.2439, ..., 0.7427, -0.0717, -0.0814], [-0.7432, 0.6985, 1.5551, ..., 0.5232, -0.5685, 1.3387], [ 0.2149, 0.5274, -1.6414, ..., 0.7476, 0.5082, -3.0132], [ 0.4408, 0.9416, 0.4522, ..., -0.1506, 1.5591, -0.6453]], [[-0.9027, 0.5874, 0.6981, ..., 2.2899, 0.2933, -0.7508], [ 1.2246, -1.0856, -0.2497, ..., -1.2377, 0.0847, -0.0221], [ 3.4012, -0.4181, -2.0968, ..., -1.5427, 0.1090, -0.3882], [-0.1050, -0.5140, -0.6494, ..., -0.4358, -1.2173, 0.4161]]], grad_fn=<AddBackward0>) torch.Size([2, 4, 512])
6.3 解码器总结
-
学习了解码器的作用:
- 根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示.
-
学习并实现了解码器的类: Decoder
- 类的初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N.
- forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,src_mask, tgt_mask代表源数据和目标数据的掩码张量.
- 输出解码过程的最终特征表示.
7 输出部分介绍
- 输出部分包含:
- 线性层
- softmax层
8 线性层的作用
- 通过对上一步的线性变化得到指定维度的输出, 也就是转换维度的作用.
9 softmax层的作用
- 使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1.
3.1 线性层和softmax层的代码分析
# nn.functional工具包装载了网络层中那些只进行计算, 而没有参数的层
import torch.nn.functional as F
# 将线性层和softmax计算层一起实现, 因为二者的共同目标是生成最后的结构
# 因此把类的名字叫做Generator, 生成器类
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([[[-7.8098, -7.5260, -6.9244, ..., -7.6340, -6.9026, -7.5232], [-6.9093, -7.3295, -7.2972, ..., -6.6221, -7.2268, -7.0772], [-7.0263, -7.2229, -7.8533, ..., -6.7307, -6.9294, -7.3042], [-6.5045, -6.0504, -6.6241, ..., -5.9063, -6.5361, -7.1484]], [[-7.1651, -6.0224, -7.4931, ..., -7.9565, -8.0460, -6.6490], [-6.3779, -7.6133, -8.3572, ..., -6.6565, -7.1867, -6.5112], [-6.4914, -6.9289, -6.2634, ..., -6.2471, -7.5348, -6.8541], [-6.8651, -7.0460, -7.6239, ..., -7.1411, -6.5496, -7.3749]]], grad_fn=<LogSoftmaxBackward>) torch.Size([2, 4, 1000])