Seq2Seq的pytorch实现
Seq2Seq基本结构:Encoder -> Decoder
来看torch官方给出的模型图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PI1qci3g-1653649091522)(https://z3.ax1x.com/2021/04/29/gF2xtP.png#shadow)]
首先来看Encoder:
模型上: Encoder是一个循环神经网络,具体可以是LSTM,GRU
Encoder的输入:[batch_size,seq_len],不妨叫它batch_x那么batch_x[0]指的就是第一个句子中单词索引构成的一个向量,batch_x[0][0]的值就是第一个句子的第一个单词在词典中对应的索引
Encoder的构成: Embedding层 ,GRU层.
Embedding层用于将句子中的单词转换为一个向量(Word2Vec词向量也可以)
nn.Embedding(vocab_size,emdedding_dim)构造时需要两个参数,一个是单词的数目,一个是向量大小
GRU层就是编码器核心部分,用于将序列的信息集中到最后的隐藏层状态里
nn.GRU(emdedding_dim,hidden_size,num_layer,batch_first)构造时需要的参数,一个是输入的大小,也就是一个单词映射到向量的大小,然后是隐藏层大小,最后是隐藏层层数
下面来看Encoder的代码部分
class EncoderRNN(nn.Module):
"""不含注意力的RNN编码器"""
def __init__(self, vocab_size, hidden_size):
super(EncoderRNN, self).__init__()
self.hidden_size = hidden_size
# 词嵌入 :vocab_size个单词 嵌入到hidden_size维中 而后可以根据 index找到对应词的词嵌入(也可以换成词向量)
self.embedding = nn.Embedding(vocab_size, embedding_dim=hidden_size)
# 单向GRU作编码器: 默认1层
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
def forward(self, batch_seq, hidden):
"""
:param batch_seq :[batch_size,seq_len] x[0][0] 指第一句话 第一个词的索引
:param hidden :[?,?]上一次的隐藏层状态
"""
# 词嵌入 [batch_size,seq_len,embedding_dim] x[0][0]是这个词的向量表示
embedded = self.embedding(batch_seq)
# GRU编码
# out的形状与embedding一致
# hidden: [num_layer,batch_size,n_hidden]
out, hidden = self.gru(embedded, hidden)
return out, hidden
def init_hidden(self, num_layer, batch_size):
return torch.zeros(num_layer, batch_size, self.hidden_size)
Encoder的前向传播过程
首先是两个输入的形状和含义
batch_seq指输入的句子,形状为[batch_size,seq_len]
hidden指初始隐藏层状态h0,形状为[num_layer,batch_size,n_hidden],batch_size在第二维的原因是torch的RNN输出的hidden的batch都在第二维,比如hidden[0,0]就是第一层隐藏层的第一个batch对应隐藏层内容
然后是词嵌入
将batch_seq传入Embedding后,输出的形状变为[batch_size,seq_len,embedding_dim],根据词的索引将词变成了一个embedding_dim维的向量
最后是GRU
需要注意的是,nn.GRU一次就会处理完一个序列,不会返回中间状态.
GRU的输入形状是[batch_size,seq_len,embedding_dim]
GRU的输出有两个,一个是对输入的预测,一个是最后时刻的隐层状态
output的形状:[batch_size,seq_len,embedding_dim] 与 输入形状一模一样,output[0,0]表示的含义是第一个句子中第一个单词的预测输出(是一个embedding_dim大小的向量 可以对应到单词索引)
hidden的形状:[num_layer,batch_size,n_hidden]是最后时刻的隐藏层状态,它汇集了batch_size个句子的特征信息,而后会作为解码器的初始隐层输入
下面来看Decoder
模型上:Decoder更像一个全连接网络
Decoder的输入:[batch_size,seq_len]与encoder相同
Decoder的组成:Embedding,GRU,classification,softmax
Embedding与Encoder相同,跳过
GRU与Encoder相同,跳过
classification+softmax:一个全连接层,用于将GRU的ouput变为一个概率分布,用于输出预测的单词
Decoder代码
class DecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size):
super(DecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(output_size, hidden_size)
# 单向GRU
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
self.classification = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, x, hidden):
"""
:param x: []
:param hidden:
:return:
"""
# 输出 [batch_size,seq_len,embedding_dim]
embedded = self.embedding(x)
embedded = F.relu(embedded)
# output:[batch_size,seq_len,embedding_dim]
output, hidden = self.gru(embedded, hidden)
# [batch,seq_len,vocab_size] [0][0] 表示第一个句子 第一个词 预测的结果
output = self.classification(output)
# 对预测的结果做一个LogSoftmax概率统计
output = self.softmax(output.squeeze().view(1, -1))
output = output.unsqueeze(0)
return output, hidden
def init_hidden(self, num_layer, batch_size):
return torch.zeros(num_layer, batch_size, self.hidden_size)
Decoder的前向传播过程:
!!!需要注意的是,与Encoder不同,虽然输入形状为[batch_size,seq_len],但是seq_len必须为1,因为我们需要一步一步的获取Decoder的输出作为下一步的输入
!!!在Encoder中我们是将所有时刻的输入一次性给了GRU,而Decoder不能这样,因为Decoder并没有外部输入,我们只构造一个开始时刻的SOS标志给GRU,而后的输入都是上一时刻的输出.
好,我们继续看前向传播,首先是对输入进行编码,而后是ReLU激活
使用当前时间步的输入,和隐藏层状态作为输入传入GRU,GRU实际只做了一次预测,所以输出为
output:[batch_size,1,embedding_dim]
而后使用全连接对output进行降维,将output置为(1,vocab_size)的形状传入LogSoftmax,计算当前时间步预测的结果概率分布
Decoder的前向传播实际只对一个输入进行了预测,在训练时我们应如此做:
decoder_input = torch.tensor([[SOS_TOKEN]] * 1, dtype=torch.long).view(1, 1)
decoder_hidden = ht
for index in range(target_len):
decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
_, top_index = decoder_output.topk(1)
# 取出当前预测概率最大值的索引 作为下一个输入 [batch:1,seq_len:1]
decoder_input = top_index.squeeze().detach().view(1, 1)
# 这里很失败啊 解码器不该处理seq_len的
# decoder_output[0, 0] 取第一个batch的第一个词的预测概率(实际也就一个词)
# target[0,index] 取第一个batch 对应位置的真实输出做对比
loss += criterion(decoder_output[0, 0], target[0, index])
if decoder_input.item() == EOS_TOKEN:
break
初始时间步,我们人为构造一个起始状态,使用SOS_TOKEN作为起始字符索引构造Tensor
而后使用编码器最后时间步隐层状态ht作为初始输入
之后我们让解码器循环训练,根据其返回的概率分布,我们计算出其预测的单词索引,再将改单词索引变为Tensor作为下一时间步的输入,直到达到真实句子长度或者其预测出EOS_TOKEN结束符为止
总结
一个起始状态,使用SOS_TOKEN作为起始字符索引构造Tensor
而后使用编码器最后时间步隐层状态ht作为初始输入
之后我们让解码器循环训练,根据其返回的概率分布,我们计算出其预测的单词索引,再将改单词索引变为Tensor作为下一时间步的输入,直到达到真实句子长度或者其预测出EOS_TOKEN结束符为止