文章介绍
本文是对Sequence to Sequence Learning with Neural Networks论文学习笔记,及其中论文代码复现和分析:文章主要提及本文提出了一种通用的端对端的方法进行序列到序列的学习,其中Encoder和Deocder都是多层的LSTM。我们的模型在机器翻译上取得了非常好的效果(这里指在WMT14英语到法语上的实验)。
背景知识
BLEU介绍
对于一个翻译结果一般是通过人工的方法进行评价结果,这样得到的结果肯定十分准确,但是其的速度特别慢,所要消耗的价格也会特别高。肯定无法满足机器翻译的结果过程评价,这样就无法得知翻译结果的好坏,于是便有机器自动评价,通过设置指标对翻译结果自动评价其的优点是较为准确,速度快,免费;缺点是可能和人工评价有一些出入。这里的评价指标为BLEU。
BLEU评价指标:自动对机器翻译的结果进行评价。
例如:
Candidate: the the the the the the the.
Reference 1: the cat is on the mat.
Reference 2: there is a cat on the mat
.𝑪ount(the)=7
𝑪𝒐𝒖𝒏𝒕𝟏𝒄𝒍𝒊𝒑 (the)=min(7,2)=2
𝑪𝒐𝒖𝒏𝒕𝟐𝒄𝒍𝒊𝒑 (the)=min(7,2)=1
𝑪𝒐𝒖𝒏𝒕𝒄𝒍𝒊𝒑(𝒕𝒉𝒆) = 𝒎𝒂𝒙 (𝟐, 𝟏) = 2
若候选的为我们翻译的,参考的为标准答案。统计候选中词语出现的次数(这里只有the),然后在判断各词在参考中出现的次数和候选中的次数取小,再将得到的结果取大。可以得到:
p
1
=
𝑪
𝒐
𝒖
𝒏
𝒕
𝒄
𝒍
𝒊
𝒑
(
𝒕
𝒉
𝒆
)
/
.
𝑪
o
u
n
t
(
t
h
e
)
p1 = 𝑪𝒐𝒖𝒏𝒕𝒄𝒍𝒊𝒑(𝒕𝒉𝒆)/.𝑪ount(the)
p1=Countclip(the)/.Count(the)
注:只使用1-gram的问题:对每个词进行翻译就能得到很高的分,完全没考虑到句子的流利性。
解决方法:使用多-gram融合,BLEU使用1-4gram。
公式为:
𝒑
n
=
σ
n
−
𝒈
𝒓
𝒂
𝒎
∈
𝑪
𝑪
𝒐
𝒖
𝒏
𝒕
𝒄
𝒍
𝒊
𝒑
(
𝒏
−
𝒈
𝒓
𝒂
𝒎
)
/
σ
n
−
𝒈
𝒓
𝒂
𝒎
∈
𝑪
𝑪
𝒐
𝒖
𝒏
𝒕
(
𝒏
−
𝒈
𝒓
𝒂
𝒎
)
𝒑n=σn−𝒈𝒓𝒂𝒎∈𝑪 𝑪𝒐𝒖𝒏𝒕𝒄𝒍𝒊𝒑(𝒏 − 𝒈𝒓𝒂𝒎)/σn−𝒈𝒓𝒂𝒎∈𝑪 𝑪𝒐𝒖𝒏𝒕 (𝒏 − 𝒈𝒓𝒂𝒎)
pn=σn−gram∈CCountclip(n−gram)/σn−gram∈CCount(n−gram)
其中1—gram是选取一个词,2—gram选取两个词其他类似,然后将得到的结果每一个按照1/4加权求和,但由于长度问题,目前的评价指标对断句有利。所以可以对长度加上惩罚进行解决:
这里相当于对结果加一个系数,如果候选的句子比推荐的长就不惩罚系数设置成1,如果比其短对其做上述处理。
Seq2Seq模型简介
目前的Seq2Seq模型一般包括Encoder和Decoder两部分,主要是由输入的一个序列,经过Encoder产生一个向量再由Decoder解码生成一个序列。由于本次利用的是机器翻译这里介绍一下机器翻译Seq2Seq模型。如图:
这里的Encoder和Decoder都是RNN可以是GRU也可以是LSTM,输入的为源语言,经过Encoder变成向量C,然后Decoder由输入(start)和C中的信息翻译第一个词语,然后利用得到的词去翻译下一个词直到遇到EOS(end of sequence)结束(同时你也可以设置一个翻译的最大长度)。
Deep NMT模型
这里每一横行代表一个LSTM共享一个参数,不同的LSTM的参数不同,由输入得到的h不仅要向同一层的LSTM传输还要以输入传向上一层的LSTM,如图中所示,每一层的LSTM经历过Encoder会产生对应的h传入Decoder中的LSTM进行同样的运算获得第一个翻译值,将其作为输入继续进行直到EOS。
代码复现
以上介绍的都是背景知识,这里我将分析对英语向法语翻译代码的分析:
参数设置
import argparse
def ArgumentParser():
parser = argparse.ArgumentParser()
parser.add_argument('--embed_size', type=int, default=10, help="embedding size of word embedding")
parser.add_argument("--epoch",type=int,default=200,help="epoch of training")
parser.add_argument("--cuda",type=bool,default=True,help="whether use gpu")
parser.add_argument("--gpu",type=int,default=1,help="whether use gpu")
parser.add_argument("--learning_rate",type=float,default=0.02,help="learning rate during training")
parser.add_argument("--batch_size",type=int,default=64,help="batch size during training")
parser.add_argument("--seed",type=int,default=1,help="seed of random")
parser.embedding_pretrained = None
return parser.parse_args(args=[])
利用类封装函数,设置本次程序说需的参数:
embed_size ——————》》单词Embedding的嵌入大小
epoch—————————》》训练的轮数
cuda,gpu———————》》是否使用GPU(其中cuda为bool值gpu为int值)
learning_rate——————》》学习率
batch_size———————》》训练的批大小
数据处理
导入相应库
from torch.utils import data
import os
import nltk
import numpy as np
import pickle
from collections import Counter
进行数据读取处理
- 数据集的读取
初始化
class iwslt_Data(data.DataLoader):
def __init__(self,source_data_name="train.tags.de-en.de",
target_data_name="train.tags.de-en.en"
,source_vocab_size = 30000, target_vocab_size = 30000):
self.path = os.path.abspath(".")
if "data" not in self.path:
self.path += "/data"
self.source_data_name = source_data_name
self.target_data_name = target_data_name
self.source_vocab_size = source_vocab_size
self.target_vocab_size = target_vocab_size
self.source_data, self.target_data, self.target_data_input = self.load_data()
我们利用iwslt_Data类对数据集进行处理,这里需要我们对进行初始化,首先判断当前目录下是否含有data文件如果没有加入,然后对源数据名称,目标数据名称和他们的词表大小赋值,再利用导入数据,将源语言数据集和目标语言数据集,以及输入的目标数据导入。
导入数据集函数
def load_data(self):
raw_source_data = open(self.path+"/iwslt14/"+self.source_data_name,encoding="utf-8").readlines()
raw_target_data = open(self.path+"/iwslt14/"+self.target_data_name,encoding="utf-8").readlines()
print (len(raw_target_data))
print (len(raw_source_data))
source_data = []
target_data = []
for i in range(len(raw_source_data)):
if raw_target_data[i]!="" and raw_source_data[i]!="" and raw_source_data[i][0]!="<" and raw_target_data[i][0]!="<":
source_sentence = nltk.word_tokenize(raw_source_data[i],language="german")
target_sentence = nltk.word_tokenize(raw_target_data[i],language="english")
if len(source_sentence)<=100 and len(target_sentence)<=100:
source_data.append(source_sentence)
target_data.append(target_sentence)
if not os.path.exists(self.path + "/iwslt14/source_word2id"):
source_word2id = self.get_word2id(source_data,self.source_vocab_size)
target_word2id = self.get_word2id(target_data,self.target_vocab_size)
self.source_word2id = source_word2id
self.target_word2id = target_word2id
pickle.dump(source_word2id, open(self.path + "/iwslt14/source_word2id", "wb"))
pickle.dump(target_word2id, open(self.path + "/iwslt14/target_word2id", "wb"))
else:
self.source_word2id = pickle.load(open(self.path + "/iwslt14/source_word2id", "rb"))
self.target_word2id = pickle.load(open(self.path + "/iwslt14/target_word2id", "rb"))
source_data = self.get_id_datas(source_data,self.source_word2id)
target_data = self.get_id_datas(target_data,self.target_word2id,is_source=False)
target_data_input = [[2]+sentence[0:-1] for sentence in target_data]
source_data = np.array(source_data)
target_data = np.array(target_data)
target_data_input = np.array(target_data_input)
return source_data,target_data,target_data_input
由上可以知道我们要利用导入数据,将源语言数据集和目标语言数据集,以及输入的目标数据导入这里我们介绍导入函数
-
数据集的读取:
利用文件打开读取源语言和目标语言的数据集,然后输出源语言和目标语言的数据大小 -
对数据进行分词过滤:
建立源语言,和目标语言的的空列表,将源语言和目标语言中不是空格和”《“的数据分别利用nltk库中的language将法语设置为源语言,将英语设置为目标语言进行过滤(其中由于数据集中含有对数据集介绍的文字均为《开头不是我们要的数据所以不进行读取),由于数据的长度如果太长会影响训练和学习,所以我们只保留长度小于100的进行第二次过滤。 -
对源语言和目标语言分别获得Word2id:
首先判断是否已经存在word2id的文件,如果不存在首先添加文件夹,然后利用get_word2id将目标语言和源语言获得对应词向量大小的word2id,并将其赋给原初始化的源语言和目标语言wordid,并将他们打包成二进制文件;如果原文件中已经处理好word2id可以直接将他们打包成二进制文件。 -
然后再利用词向量利用get_id_datas函数分别获取源语言和目标语言的词向量
-
分别获得输入目标,源,目标语言数据集:
-由于输入的目标语言需要开始的标志所以在目标语言加入代表SOS(start of sequence)的2
并将源语言和目标语言,输入的目标语言转换成numpy格式,并且返回
目标语言word2id和源语言数据及目标语言数据的分别转换id函数
由上一个函数我们利用get_word2id和get_id_datas函数这里我们详细介绍其的函数内容:
def get_word2id(self,data,word_num):
words = []
for sentence in data:
for word in sentence:
words.append(word)
word_freq = dict(Counter(words).most_common(word_num-4))
word2id = {"<pad>":0,"<unk>":1,"<start>":2,"<end>":3}
for word in word_freq:
word2id[word] = len(word2id)
return word2id
get_word2id:
- 首先定义一个空列表将所有的词写入列表之中
- 利用Counter对word中的词计数(counter会对词出现的个数自动计数),并由出现的次数排序获取由输入的词向量大小的数量减去4的出现次数靠前的数据(这里减去4是因为其中要包括表示《pad》填充的0,《unk》的1,《start》开始的2,《end》结束的3)
- 然后对每个词分配一个id,并将word2id返回
def get_id_datas(self,datas,word2id,is_source = True):
for i, sentence in enumerate(datas):
for j, word in enumerate(sentence):
datas[i][j] = word2id.get(word,1)
if is_source:
datas[i] = datas[i][0:100] +[0]*(100-len(datas[i]))
datas[i].reverse()
else:
datas[i] = datas[i][0:99]+ [3] + [0] * (99 - len(datas[i]))
return datas
get_id_datas:
- 首先对数据进行遍历并且将其转换成id
- 然后判断句子是否为源语言数据(由于源语言不需要进行添加start和end因为他本身可以识别,但是需要将数据逆序输入以使结果更加的精准所以需要判断是否为源语言)如果是,将数据填充100的序列长度,并将数据逆序
- 如果表示将读入的数据添加start和end并且填充
方便读取的简单函数
为了方便我们数据读取和处理的操作我们定义一些简单函数:
def __getitem__(self, idx):
return self.source_data[idx],self.target_data_input[idx], self.target_data[idx]
def __len__(self):
return len(self.source_data)
这里包括输入词获取标签,和返回数据长度的函数
Deep_NMT模型
导入相应的库
import torch
import torch.nn as nn
import numpy as np
进行模型的初始化
class Deep_NMT(nn.Module):
def __init__(self,source_vocab_size,target_vocab_size,embedding_size,
source_length,target_length,lstm_size):
super(Deep_NMT,self).__init__()
self.source_embedding =nn.Embedding(source_vocab_size,embedding_size)
self.target_embedding = nn.Embedding(target_vocab_size,embedding_size)
self.encoder = nn.LSTM(input_size=embedding_size,hidden_size=lstm_size,num_layers=4,
batch_first=True)
self.decoder = nn.LSTM(input_size=embedding_size,hidden_size=lstm_size,num_layers=4,
batch_first=True)
self.fc = nn.Linear(lstm_size, target_vocab_size)
- 首先(对上述模型进行继承)实现两个语言的embeding(输入源语言和目标语言词表)。
- 对encoder和decoder初始化(其中batch_size必须设置为“Ture”否则输入格式会出错,length就会放在第一维度)。
- 设置全连接层
模型forward函数
def forward(self, source_data,target_data, mode = "train"):
source_data_embedding = self.source_embedding(source_data)
enc_output, enc_hidden = self.encoder(source_data_embedding)
if mode=="train":
target_data_embedding = self.target_embedding(target_data)
dec_output, dec_hidden = self.decoder(target_data_embedding,enc_hidden)
outs = self.fc(dec_output)
else:
target_data_embedding = self.target_embedding(target_data)
dec_prev_hidden = enc_hidden
outs = []
for i in range(100):
dec_output, dec_hidden = self.decoder(target_data_embedding, dec_prev_hidden)
pred = self.fc(dec_output)
pred = torch.argmax(pred,dim=-1)
outs.append(pred.squeeze().cpu().numpy())
dec_prev_hidden = dec_hidden
target_data_embedding = self.target_embedding(pred)
return outs
对forward代码:
-
首先判断“train”模型(由于在训练和验证的时间需要把真实的标签输入,测试时需要一步一步进行,训练时利用真实的标签可以加快速度,所以要对模型进行区别)
-
如果为“train”:
首先,对输入的数据得到他的emedding得到大小为(batch_sizelengthemedding)送入encoder的四层LSMT中,返回相应的encode_size。(只返回顶层的隐藏层输出,包括每层h,c,)将得的隐藏层输入到线性层中得到输出。 -
如果不是先由目标数据获得embedin,在对输入数据中100个词逐个翻译(包括将目标数据embeding大小和上述得到的隐藏层用decoder获得顶层的h和c,将输出的放入fc,将其中概率最大的输出保存)最后的输出为3维向量。
正式训练和测试
相关库的导入
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
from model import Deep_NMT
from data import iwslt_Data
import numpy as np
from tqdm import tqdm
import config as argumentparser
config = argumentparser.ArgumentParser()
将本次实验所需的库全部导入,其中包括对其他函数 封装成函数并对他们引用(包括数据处理,模型,和参数的读取)
if config.cuda and torch.cuda.is_available():
torch.cuda.set_device(config.gpu)
torch.cuda.is_available()
判断是否使用GPU(如果使用将参数放入gpu中),再由后面的函数获得是否为gpu
数据读取
training_set = iwslt_Data()
training_iter = torch.utils.data.DataLoader(dataset=training_set,
batch_size=config.batch_size,
shuffle=True,
num_workers=0)
valid_set = iwslt_Data(source_data_name="IWSLT14.TED.dev2010.de-en.de",target_data_name="IWSLT14.TED.dev2010.de-en.en")
valid_iter = torch.utils.data.DataLoader(dataset=valid_set,
batch_size=config.batch_size,
shuffle=True,
num_workers=0)
test_set = iwslt_Data(source_data_name="IWSLT14.TED.tst2012.de-en.de",target_data_name="IWSLT14.TED.tst2012.de-en.en")
test_iter = torch.utils.data.DataLoader(dataset=test_set,
batch_size=config.batch_size,
shuffle=True,
num_workers=0)
调用数据处理函数获取训练集,验证集,测试集。
模型调用
model = Deep_NMT(source_vocab_size=30000,target_vocab_size=30000,embedding_size=256,
source_length=100,target_length=100,lstm_size=256)
if config.cuda and torch.cuda.is_available():
model.cuda()
制定相应的参数获取模型,判断是否使用gpu如果使用的话将模型放入gpu中
构造损失函数和优化器
criterion = nn.CrossEntropyLoss(reduce=False)
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)
loss = -1
target_id2word = dict([[x[1],x[0]] for x in training_set.target_word2id.items()])
这里损失函数必须将reduce改成False若不改成,其返回的就是向量,可以使训练更快,并且不受pad的影响。
这里target_id2word是将id和word组成新的词典方便以后的使用,就是将原字典的key和value的值互换并组成一个新的词典。
生成验证集``
def get_dev_loss(data_iter):
# 生成验证集loss
model.eval()
process_bar = tqdm(data_iter)
loss = 0
for source_data, target_data_input, target_data in process_bar:
if config.cuda and torch.cuda.is_available():
source_data = source_data.cuda()
target_data_input = target_data_input.cuda()
target_data = target_data.cuda()
else:
source_data = torch.autograd.Variable(source_data).long()
target_data_input = torch.autograd.Variable(target_data_input).long()
target_data = torch.autograd.Variable(target_data).squeeze()
out = model(source_data, target_data_input)
loss_now = criterion(out.view(-1, 30000), autograd.Variable(target_data.view(-1).long()))
weights = target_data.view(-1) != 0
loss_now = torch.sum((loss_now * weights.float())) / torch.sum(weights.float())
loss+=loss_now.data.item()
return loss
先判断数据是否数据是否使用GPU,并将其转换成相应的相应的格式。再判断数值是否为0(也就是pad的值),如果是则将值设为0不是设置成1,然后利用当前的loss和将数值不为0的数值加权求和。
生成测试的BLUE
def get_test_bleu(data_iter):
model.eval()
process_bar = tqdm(data_iter)
refs = []
preds = []
for source_data, target_data_input, target_data in process_bar:
target_input = torch.Tensor(np.zeros([source_data.shape[0], 1])+2)
if config.cuda and torch.cuda.is_available():
source_data = source_data.cuda()
target_input = target_input.cuda().long()
else:
source_data = torch.autograd.Variable(source_data).long()
target_input = torch.autograd.Variable(target_input).long()
target_data = target_data.numpy()
out = model(source_data, target_input,mode="test")
out = np.array(out).T
tmp_preds = []
for i in range(out.shape[0]):
tmp_preds.append([])
for i in range(out.shape[0]):
for j in range(out.shape[1]):
if out[i][j]!=3:
tmp_preds[i].append(out[i][j])
else:
break
preds += tmp_preds
tmp_refs = []
for i in range(target_data.shape[0]):
tmp_refs.append([])
for i in range(target_data.shape[0]):
for j in range(target_data.shape[1]):
if target_data[i][j]!=3 and target_data[i][j]!=0:
tmp_refs[i].append(target_data[i][j])
tmp_refs = [[x] for x in tmp_refs]
refs+=tmp_refs
bleu = corpus_bleu(refs,preds)*100
with open("./data/result.txt","w") as f:
for i in range(len(preds)):
tmp_ref = [target_id2word[id] for id in refs[i][0]]
tmp_pred = [target_id2word[id] for id in preds[i]]
f.write("ref: "+" ".join(tmp_ref)+"\n")
f.write("pred: "+" ".join(tmp_pred)+"\n")
f.write("\n\n")
return bleu
- 首先在测试模式下,对上述三个数据进行遍历,判断数据是否为GPU,如果是就放在GPU中进行如果不是将其转换成long的模式,这里是将目标数据转换成numpy形式(不转的算的值都会为0)
- 这里介绍一下target_input为全为2的向量。也就是全为start
- 将源语言数据集和目标输入的向量输入得到输出并将其转换成numpy模式。并进行转置
- 然后就是进行预测在数值不为3(停止)的数据进行预测,由一开始是建立在out的第一维度大小的空列表如果要是对某个位置直接赋值就会使数值的全部都被赋为那个值,所以这里使用循环的方式对预测数据添加到我们的空列表中。
- 由于根据BLEU对Seq的预测每一个预测的参考可能不止一个所以要在每一个数据后加一个维度,方便函数的使用。然后将实际的预测加入。将其预测值输入corpus_bleu并乘以100便或得目标bleu值。
- 最后将结果写道result文件中(包括预测的词和真实的值)
正试进入训练和测试
for epoch in range(config.epoch):
model.train()
process_bar = tqdm(training_iter)
for source_data, target_data_input, target_data in process_bar:
if config.cuda and torch.cuda.is_available():
source_data = source_data.cuda()
target_data_input = target_data_input.cuda()
target_data = target_data.cuda()
else:
source_data = torch.autograd.Variable(source_data).long()
target_data_input = torch.autograd.Variable(target_data_input).long()
target_data = torch.autograd.Variable(target_data).squeeze()
out = model(source_data,target_data_input)
loss_now = criterion(out.view(-1,30000), autograd.Variable(target_data.view(-1).long()))
weights = target_data.view(-1)!=0
loss_now = torch.sum((loss_now*weights.float()))/torch.sum(weights.float())
if loss == -1:
loss = loss_now.data.item()
else:
loss = 0.95*loss+0.05*loss_now.data.item()
process_bar.set_postfix(loss=loss_now.data.item())
process_bar.update()
optimizer.zero_grad()
loss_now.backward()
optimizer.step()
test_bleu = get_test_bleu(test_iter)
print("test bleu is:", test_bleu)
valid_loss = get_dev_loss(valid_iter)
print ("valid loss is:",valid_loss)
根据config.epoch为100进行100轮的训练,这里首先和前面一样利用迭代将数据遍历,首先通过判断是否使用GPU如果使用将数据改成GPU,不是改为long,其他步骤和上述一样。
总结
- 这里是我对相关代码的总结和笔记
- 这里主要区分训练和测试对数据处理的不同