seq2seq模型是自然语言处理任务中一个常见的模型,于2014年由Google团队提出,该模型的创新之处在于将encoder-decoder框架应用到了RNN/LSTM中,文章中输入序列通过多层LSTM组成的encoder被映射成一个固定维度的中间语义编码向量C,另一组多层LSTM组成的decoder将中间语义编码向量C映射成目标序列。
下面是论文中seq2seq的输入输出图形化解释:
说明:seq2seq模型的输入序列是A,B,C,输出序列是W,X,Y,Z,蓝色部分是encoder,红色部分是decoder。
注:seq2seq原文可以在这里下载。
现在利用seq2seq模型实现一个简单的英文反义词翻译实验,模型的输入是一个词,输出是其对应的反义词,为了方便起见,我们的训练数据是很简单的,只有 6 组,并且每个英文单词的长度不会超过 5,训练数据的形式如下:
[['man', 'women'], ['black', 'white'], ['king', 'queen'], ['girl', 'boy'], ['up', 'down'], ['high', 'low']]
说明:原始训练样本是一个二维的矩阵,用 seq_data 表示,每一行表示一组训练样本,第一个元素表示原始序列,第二个元素表示目标序列。
由于英文单词的长度都不会超过 5,因此定义时间步 n_step = 5,隐藏层 n_hidden = 128。考虑到英文单词的字母总共有 26 个,于是字符列表 char_arr 中必须加入 26 个英文字符(默认是小写)。训练数据单词的长度往往不一致,因此需要将其 padding 成相同长度的序列,于是还要给字符列表 char_arr 中加入一个 padding 符 ‘P’,另外,还需要将起始符 ‘S’ 和终止符 ‘E’ 加入字符列表 char_arr。
之后对字符列表进行字典化,然后对训练样本目的是把英文的输入样本转换成序列化的表示形式。代码如下:
n_step = 5 # n_step表示输入单词的最大长度
n_hidden = 128
char_arr = [c for c in "SEPabcdefghijklmnopqrstuvwxyz"]
num_dic = {c: i for i, c in enumerate(char_arr)}
int_dic = {i: c for i, c in enumerate(char_arr)}
seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'],
['girl', 'boy'], ['up', 'down'], ['high', 'low']]
n_class = len(char_arr)
batch_size = len(seq_data)
# 深拷贝
seq_copy = copy.deepcopy(seq_data)
inp, out, tar = make_batch()
对于第一组训练样本 [‘man’, ‘women’] 而言,如何得到 ‘man’ 的序列化输入呢?
首先,由于’man’ 的长度 3 是小于 n_step 的,因此需要在 ‘man’ 的后面填充 n_step - 3 个长度的 ‘P’,然后对于 ‘manPP’ 可以根据字典 num_dic 得到对应的序列表示 [15, 3, 16, 2, 2],之后获得 'manPP’中每个字母的 one-hot 表示,是一个 5 * 29维的稀疏矩阵。于是,'man’就转换成了网络可以识别的形式。‘woman’ 是 ‘man’ 的标签数据,预处理是类似的,不同之处在于,‘woman’ 需要给最前面加上起始符 ‘S’。全部样本的预处理代码如下:
def make_batch():
input_batch = []
output_batch = []
target_batch = []
for seq in seq_copy:
for i in range(2):
seq[i] = seq[i] + 'P' * (n_step - len(seq[i]))
inp = [num_dic[s] for s in seq[0]]
out = [num_dic[s] for s in ('S' + seq[1])]
tar = [num_dic[s] for s in (seq[1] + 'E')]
input_batch.append(np.eye(n_class)[inp]) # 训练样本的输入向量表示
output_batch.append(np.eye(n_class)[out]) # 训练样本的输出向量表示
target_batch.append(tar) # 用于计算损失函数的输出向量表示
return torch.FloatTensor(input_batch), torch.FloatTensor(output_batch), torch.LongTensor(target_batch)
模型训练过程如下:
# 定义损失函数和优化器
model = seq2seq()
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(4000):
# 初始化隐藏层的状态
hidden = torch.zeros(1, batch_size, n_hidden)
output = model(inp, hidden)
output = output.transpose(0, 1) # output:[batch_size, num_step(n_step + 1), n_class]
# 计算损失
optimizer.zero_grad()
loss = 0
for i in range(output.shape[0]):
loss += criterion(output[i], tar[i])
# output[i]:[n_step+1, n_class], tar[i]:[n_step+1]
if epoch % 200 == 0:
print("epoch:{} loss:{:.6f}".format(epoch, loss))
loss.backward()
optimizer.step()
最后给出了预测部分,测试的样本选取的是训练样本,这里仅仅是为了说明问题。
# !-*- coding:utf-8 -*-
# @author:zhangsa
# @file:.py
import numpy as np
import torch.nn as nn
import torch
import copy
def make_batch():
input_batch = []
output_batch = []
target_batch = []
for seq in seq_copy:
for i in range(2):
seq[i] = seq[i] + 'P' * (n_step - len(seq[i]))
inp = [num_dic[s] for s in seq[0]]
out = [num_dic[s] for s in ('S' + seq[1])]
tar = [num_dic[s] for s in (seq[1] + 'E')]
input_batch.append(np.eye(n_class)[inp]) # 训练样本的输入向量表示
output_batch.append(np.eye(n_class)[out]) # 训练样本的输出向量表示
target_batch.append(tar) # 用于计算损失函数的输出向量表示
return torch.FloatTensor(input_batch), torch.FloatTensor(output_batch), torch.LongTensor(target_batch)
class seq2seq(nn.Module):
def __init__(self):
super().__init__()
self.enc_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden)
self.dec_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden)
self.fc = nn.Linear(n_hidden, n_class)
def forward(self, enc_input, hidden):
batch_size = enc_input.shape[0]
enc_input = enc_input.transpose(0, 1) # enc_shape:[num_step(n_step), batch_size, n_class]
# dec_input = dec_input.transpose(0, 1) # dec_shape:[num_step(n_step + 1), batch_size, n_class]
# 编码器的输入时随机初始化
dec_input = torch.randint(n_class, (n_step + 1, batch_size, n_class)).type(dtype=torch.FloatTensor)
_, enc_state = self.enc_cell(enc_input, hidden) # enc_state:中间的语义编码 c
# enc_state:[1, batch_size, n_hidden]
dec_out, _ = self.dec_cell(dec_input, enc_state)
# dec_out:[num_step(n_step + 1), batch_size, n_hidden]
out = self.fc(dec_out) # out:[num_step(n_step + 1), batch_size, n_class]
return out
if __name__ == "__main__":
n_step = 5 # n_step表示输入单词的最大长度
n_hidden = 128
char_arr = [c for c in "SEPabcdefghijklmnopqrstuvwxyz"]
num_dic = {c: i for i, c in enumerate(char_arr)}
int_dic = {i: c for i, c in enumerate(char_arr)}
seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'],
['girl', 'boy'], ['up', 'down'], ['high', 'low']]
n_class = len(char_arr)
batch_size = len(seq_data)
# 深拷贝
seq_copy = copy.deepcopy(seq_data)
inp, out, tar = make_batch()
# 定义损失函数和优化器
model = seq2seq()
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(4000):
# 初始化隐藏层的状态
hidden = torch.zeros(1, batch_size, n_hidden)
output = model(inp, hidden)
output = output.transpose(0, 1) # output:[batch_size, num_step(n_step + 1), n_class]
# 计算损失
optimizer.zero_grad()
loss = 0
for i in range(output.shape[0]):
loss += criterion(output[i], tar[i])
# output[i]:[n_step+1, n_class], tar[i]:[n_step+1]
if epoch % 200 == 0:
print("epoch:{} loss:{:.6f}".format(epoch, loss))
loss.backward()
optimizer.step()
# 模型预测
def translation():
model.eval()
input_batch, output_batch, _ = make_batch()
hidden_state = torch.zeros(1, 6, n_hidden)
predict = model(input_batch, hidden_state) # predict:[num_step, batch_size, n_class]
predict = predict.transpose(0, 1) # predict:[batch_size, num_step, n_class]
predict_words = []
for i in range(predict.shape[0]):
_, pre_seq = torch.max(predict[i], dim=1)
pre_word = [int_dic[p] for p in pre_seq.detach().numpy()]
pre = ""
for p in pre_word:
if p == 'P' or p == 'E':
break
else:
pre += p
predict_words.append(pre)
return predict_words
print("test:")
for i in range(len(seq_data)):
print(seq_data[i][0], "-->", translation()[i])
https://github.com/graykode/nlp-tutorial/blob/master/4-1.Seq2Seq/Seq2Seq.py
seq2seq原文