Sequence to Sequence(Seq2Seq)是由encoder(编码器)和decoder(解码器)两个RNN组成的。其中encoder负责对输入句子的理解,转化为context vector,decoder负责对理解后的句子的向量进行处理,解码,获得输出。
那么此时,就有一个问题,在encoder的过程中得到的context vector作为decoder的输入,那么这样一个输入,怎么能够得到多个输出呐?
其实就是当前一步的输出,作为下一单元的输入。然后得到结果
outputs = []
while True:
output = decoder(output)
outputs.append(output)
那么循环什么时候停止呐?
在训练数据集中,可以在输出的最后面添加一个结束符“<\END>”,如果遇到结束符,则可以终止循环。
outputs = []
while output!=""
output = decoderd(output)
outputs.append(output)
总之:Seq2Seq模型中的encoder接收一个长度为M的序列,得到一个context vector,之后decoder把这一个context vector转化为长度为N的序列作为输出,从而构成一个M to N的模型,能够处理很多不定长输入输出的问题。比如:文本翻译、问答、文章摘要、关键字写诗等等。
下面,我们通过一个简单的例子,来看看普通的Seq2Seq模型应该如何实现。
需求:完成一个模型,实现往模型中输入一串数字,输出这串数字+0
例如:输入:123456789,输出:1234567890
由于输入的是数字,为了把这些数字和词典中的真实数字进行对应,可以把这些数字理解为字符串。那么我们需要做的是:
"""
将数字进行序列化
"""
class num_sequence():
PAD_TAG = "PAD"
UNK_TAG = "UNK"
UNK = 0
PAD = 1
EOS_TAG = "EOS"
EOS = 2
def __init__(self):
self.dict = {
self.UNK_TAG:self.UNK,
self.PAD_TAG:self.PAD
}
for i in range(10):
self.dict[str(i)] = len(self.dict)
self.reverse_dict = dict(zip(self.dict.values(),self.dict.keys()))
def transform(self,sentence,max_len=None,add_eos=False):
"""把字符串转换为序列
add_eos: true 输出句子长度为max_len+1
add_eos: false 输出句子长度为max_len
"""
if len(sentence)>max_len:
sentence = sentence[:max_len]
sentence_len = len(sentence)
if add_eos:
sentence = sentence + [self.EOS_TAG]
if sentence_len<max_len:
sentence = sentence+[self.PAD]*(max_len-sentence_len)
result = [self.dict.get(i,self.UNK) for i in sentence]
return result
def reversedTransform(self,sentence):
"""把序列转换会字符串"""
return [self.reverse_dict.get(i,self.UNK_TAG) for i in sentence]
准备dataset
其中需要注意:
class my_dataset(Dataset):
def __init__(self):
# 使用numpy创建一堆数字
self.data = np.random.randint(0,1e6,size=[500000])
def __getitem__(self, index):
input = list(str(self.data[index]))
label = input+["0"]
input_length = len(input)
label_length = len(label)
return input,label,input_length,label_length
def __len__(self):
return len(self.data)
def collate_fn(batch):
# 1.对batch进行按照长度从长到短降序排序
batch = sorted(batch,key=lambda x:x[3],reverse=True)
input,label,input_length,label_length = list(zip(*batch))
# 把input转化为序列
# 2.进行padding的操作
input = torch.LongTensor([config.sequence.transform(i,max_len=config.max_len) for i in input])
label = torch.LongTensor([config.sequence.transform(i, max_len=config.max_len+1) for i in label])
input_length = torch.LongTensor(input_length)
label_length = torch.LongTensor(label_length)
return input,label,input_length,label_length
dataloader = DataLoader(my_dataset(),shuffle=True,batch_size=config.train_batch_size,collate_fn=collate_fn)
编码器(encoder)的目的就是为了对文本进行编码,把编码后的结果交给后续的程序使用。所以在这里我们可以使用Embedding+GRU的结构来使用,使用最后一个time step的输出(hidden state)作为句子的编码结果。
需要注意:
nn.utils.rnn.pack_padded_sequence
对padding后的句子进行打包的操作能够更快获得LSTM或者GRU的结果。nn.utils.rnn.pad_packed_sequence
对打包的内容进行解包的操作。nn.utils.rnn.pack_padded_sequence
使用过程中需要对batch中的内容按照句子的长度降序排序。class Encoder(nn.Module):
def __init__(self):
super(Encoder,self).__init__()
self.embedding = nn.Embedding(
num_embeddings = len(config.sequence),
embedding_dim=config.embedding_dim,# 词的维度
padding_idx=config.num_sequence.PAD
)
self.gru = nn.GRU(
input_size=config.embedding_dim,
hidden_size=config.hidden_size,
num_layers=config.num_layers,
batch_first=True
)
def forward(self, input,length):
"""
:param input:[batch_size,seq_len]
:return:
"""
embeded = self.embedding(input) # embeded :[batch_size,seq_len,embedding_dim]
# 对文本对齐之后的句子进行打包,能够加速在LSTM或GRU中的计算过程
embeded = pack_padded_sequence(embeded,length,batch_first=True)
out,hidden = self.gru(embeded)
# 解包
result,out_length = pad_packed_sequence(out,batch_first=True,padding_value=config.num_sequence.PAD)
return result,hidden
解码器主要负责实现对编码之后结果的处理,得到预测值,为后续计算损失做准备。此时需要思考:
[batch_size,max_len]
,从而我们知道输出的结果需要是一个[batch_size,max_len,vocab_size]
的形状[1,batch_size,hidden_size]
进行操作,得到预测值。解码器也是一个RNN,即也可以使用LSTM或GRU的结构,所以在解码器中:
[batch_size,hidden_size]
(会进行形状的调整为[batch_size,vocab_size]
),把这个输出作为输入再使用解码器进行解码[batch_size,max_len,vocab_size]
"""
实现解码器(decoder.py)
"""
import torch.nn as nn
import torch
from seq2seq import config
import torch.nn.functional as F
class Decoder(nn.Module):
def __init__(self):
super(Decoder,self).__init__()
self.embedding = nn.Embedding(num_embeddings=len(config.sequence),
embedding_dim=config.embedding_dim,
padding_idx=config.sequence.PAD)
self.gru = nn.GRU(hidden_size=config.hidden_size,
input_size=config.embedding_dim,
batch_first=True,
num_layers=config.num_layers)
self.fc = nn.Linear(config.hidden_size,len(config.sequence))
def forward(self, target,encoder_hidden):
# 1.获取encoder的输出,作为decoder第一次的hidden_state
decoder_hidden = encoder_hidden
# 2.准备decoder第一个时间步的输入,[batch_size,1] SOS 作为输入
batch_size = target.size(0)
decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64)*config.sequence.SOS)
# 3.在第一个时间步上进行计算,得到第一个时间步的输出,hidden_state
# 4.把第一个时间步的输出进行计算,得到第一个最后的输出结果
# 5.把前一次的hidden_state作为当前时间步的hidden_state,把前一步的输出作为当前时间步的输入
# 6.循环4-5步
# 保存预测的结果
decoder_outputs = torch.zeros([batch_size,config.max_len+2,len(config.sequence)])
for t in range(config.max_len+2): # +2 是因为dataset中+1,add_eos加了1
decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
decoder_outputs[:,t,:] = decoder_output_t
# topk取前k个值
value,index = torch.topk(decoder_output_t,1)
decoder_input = index
return decoder_outputs,decoder_hidden
def forward_step(self,decoder_input,decoder_hidden):
"""
计算每个时间步上的结果
:param decoder_input:[batch_size,1]
:param decoder_hidden:[1,batch_size,hidden_size]
:return:
"""
decoder_input_embeded = self.embedding(decoder_input)
# out:[batch_size,1,hidden_size]
# decoder_hidden:[1,batch_size,hidden_size]
out,decoder_hidden = self.gru(decoder_input_embeded,decoder_hidden)
out = out.squeeze(1) # [batch_size,hidden_size]
output = F.log_softmax(self.fc(out),dim=-1) # [batch_size,vocab_size]
return output,decoder_hidden
模型的搭建
"""
把encoder和decoder进行合并,得到seq2seq模型
"""
import torch.nn as nn
from seq2seq.encoder import Encoder
from seq2seq.decoder import Decoder
class Seq2seq(nn.Module):
def __init__(self):
super(Seq2seq,self).__init__()
self.encoder = Encoder()
self.decoder = Decoder()
def forward(self,input,input_length,traget,traget_length):
encoder_outputs,encoder_hidden = self.encoder(input,input_length)
decoder_outputs,decoder_hidden = self.decoder(traget,encoder_hidden)
return decoder_outputs,decoder_hidden
模型的训练和保存
# 训练流程:
# 1. 实例化model,optimizer,loss
# 2. 遍历Dataloader
# 3. 调用模型得到output
# 4. 计算损失
# 5. 模型保存和加载
from seq2seq.dataset import dataloader
from seq2seq.seq2seq_model import Seq2seq
from torch.optim import Adam
import torch.nn.functional as F
import torch
from seq2seq import config
seq2seq = Seq2seq()
optimizer = Adam(seq2seq.parameters(),lr = 0.001)
def train(epoch):
for index,(input,label,input_length,label_length) in enumerate(dataloader):
optimizer.zero_grad()
decoder_outputs,_ = seq2seq(input,input_length,label,label_length)
# 将decoder_outputs变为2维,label真实值变成1维
decoder_outputs = decoder_outputs.view(decoder_outputs.size(0)*decoder_outputs.size(1),-1)# [batch_size*seq_len,-1]
label = label.view(-1)# [batch_size*seq_len]
print(decoder_outputs.size())
print(label.size())
loss = F.nll_loss(decoder_outputs,label)
loss.backward()
optimizer.step()
print(epoch,index,loss)
if index%100 == 0:
torch.save(seq2seq.state_dict(),config.model_path)
torch.save(optimizer.state_dict(),config.model_path)
if __name__ == '__main__':
for i in range(10):
train(i)
模型的评估
在decoder.py中需要创建evaluate方法
def evaluate(self,encoder_hidden):
"""模型评估"""
decoder_hidden = encoder_hidden # [1,batch_size,hidden_size]
batch_size = decoder_hidden.size(1)
decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64)*config.sequence.SOS)
indices = []
# while True:
for i in range(config.max_len):
decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
value,index = torch.topk(decoder_output_t,1)# 获取概率最大的词语 # [batch_size,1]
decoder_input = index
# if index.item() == config.sequence.EOS:
# break
indices.append(index.squeeze(-1).cpu().detach().numpy())
return indices
然后再进行评估,创建eval.py
"""
模型的评估
"""
from seq2seq import config
from seq2seq import seq2seq_model
import numpy as np
import torch
# 训练流程
# 1. 准备训练数据
data = [str(i) for i in np.random.randint(0,1e8,size=[100])] # input
data = sorted(data,key=lambda x:len(x),reverse=True)
input_length = torch.LongTensor([len(i) for i in data])
input = torch.LongTensor([config.sequence.transform(i,config.max_len) for i in data])
# 2. 实例化模型,加载model
seq2seq = seq2seq_model()
model = seq2seq.load_state_dict(config.model_path)
# 3. 获取预测值
indices = seq2seq.evaluate(input,input_length)
indices = np.array(indices).transpose()
# 4. 反序列化,观察结果
result = []
for line in indices:
temp_result = config.sequence.reversedTransform(line)
cur_line = ""
for word in temp_result:
if word == config.sequence.EOS_TAG:
break
cur_line+=word
result.append(cur_line)
print(data[:10])
print(result[:10])
一些问题:
在RNN的训练过程中,使用前一个预测的结果作为下一个step的输入,可能会导致一步错,步步错的结果,那么如何提高模型的收敛速度?
Teacher forcing主要过程就是在seq2seq中,把真实值和预测值交替作为某一个时间步的输入,避免一步错步步错的情况。
if random.random()>config.teacher_forcing_ratio: # 使用teacher forcing机制,加速收敛
decoder_input = target[t]
else:
# topk取前k个值
value,index = torch.topk(decoder_output_t,1)
decoder_input = index
本文参照代码:https://github.com/SpringMagnolia/PytorchTutorial/tree/master/seq2seq