这里是国科大自然语言处理的第四次作业,同样也是从小白的视角解读程序和代码,现在我们开始吧(今天也是花里胡哨的一天呢)
目录
- 1.程序与实验说明
- 实验要求
- 程序说明
- 2.知识概述
- 2.1 序列生成问题Seq2Seq
- 2.2 RNN+Attention 架构生成模型
- 2.3 机器翻译
- 2.4 GRU
- 2.5 注意力机制
- 3.数据
- 数据来源
- 数据处理
- 4. 模型
- Encoder
- Attention
- Decoder
- Seq2Seq
- 5.训练
- 6.测试
- 测试效果
- 为什么效果差
- demo展示
- 7.疑问与思考
- 序列生成模型评价指标
- BLEU-精度
- ROUGE-召回率
- Pytorch中的pack和pad操作
- 信息融合
- ❤️mask机制
- Padding Mask
- Sequence Mask
- beam search
事实证明老师的考虑是很周到的,我虽然完成了实验内容,但是模型的准确率实在是上不了台面☹️,这一点也会在后面进行说明
代码:https://download.csdn.net/download/qq_39328436/69026304
程序目录:
Encoder和Decoder具体使用什么模型都是由研究者自己确定。比如:CNN/RNN/BiRNN/GRU/LSTM/transformer等。很明显本次实验机器翻译任务是生成式decoder。
纯RNN的生成模型会有什么问题?
输入序列(x1,x2,x3)经过模型得到生成序列(y1,y2,y3),当模型翻译任意一个yi时,所用到的中间语义C都是同一个。而事实上,当我们翻译“杰瑞”时,英文单词“Jerry”应该比其他单词有更重要的影响,比如(Tom,0.3)(Chase,0.2)(Jerry,0.5)。
在RNN模型基础上加入Attention机制就能解决上面提到的这个问题了。
任务描述:
机器翻译是利用计算机把一种语言(源语言, source language) 翻译成另一种语言(目标语言, target language)的技术。神经机器翻译是序列生成问题,主流神经机器翻译模型有基于RNN的,基于CNN的和基于自注意力机制的。
神经机器翻译系统需要考虑的问题:
GRU(Gate Recurrent Unit)是循环神经网络(Recurrent Neural Network, RNN)的一种。和LSTM(Long-Short Term Memory)一样,也是为了解决长期记忆和反向传播中的梯度等问题而提出来的。
注意力机制是神经网络中的一个加权求和的组件。输入是Q,K,输出是Att-V。Attention要回答的问题是:对于Q来说K有多重要?,重要性由输出V描述。Attention机制主要分为三个步骤,对应下图中的三个阶段。
数据来源于小规模数据集:IWSLT14 En-Zh,包含了143920个训练样本,19989个验证样例和15992个测试样例。其中,训练集数据在train_zh.txt/train_en.txt中,验证数据在valid_zh.txt/valid_en.txt中,测试集数据在test_zh.txt/test_en.txt中。X_zh.txt与X_en.txt中的数据每一行是对齐的.
在数据处理这一部分,需要将中英文语料按行合并到一起,每一行前部分是英文,通过一个制表符连接英文句子对应的中文。
按行合并两个txt文件本不是一件难事,但是简单的合并会导致出现大量下图中的数据:
这是因为这个数据集IWSLT14 En-Zh是一个人的现场演讲,其中会出现“(众人鼓掌)” 这样的话外音,为保证这些话外音不会影响数据导入需要将他们删除。
with open('train_en.txt', 'r') as fa: # 读取需要拼接的前面那个TXT
with open('train_zh.txt', 'r') as fb: # 读取需要拼接的后面那个TXT
with open('train.txt', 'w') as fc: # 写入新的TXT
for line in fa:
fc.write(line.strip('\r\n')) # 用于移除字符串头尾指定的字符
fc.write('\t')
temp=fb.readline().replace('(鼓掌)', '')
temp=temp.replace('(鼓掌声)', '')
temp=temp.replace('(众人鼓掌)', '')
temp=temp.replace('(热烈鼓掌)', '')
temp=temp.replace('(观众鼓掌)', '')
temp = temp.replace('(观众掌声)', '')
fc.write(temp)
考虑到只能用笔记本cpu来跑代码,最后选取了其中15000条数据来训练。测试数据和验证数据都分别是2000条。
整个模型由Encoder、Attention及Decoder组成,外层用Seq2Seq统一包装。模型结构如下图所示:
编码器采用双向RNN,解码器采用单向RNN,Attention采用双线性Att。
Encoder采用BiGRU结构
class Encoder(nn.Module):
def __init__(self,vocab_size,embed_size,enc_hidden_size,dec_hidden_size,dropout=0.2):
super(Encoder,self).__init__()
self.embed = nn.Embedding(vocab_size,embed_size)
self.rnn = nn.GRU(embed_size,enc_hidden_size,batch_first=True,bidirectional=True)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(enc_hidden_size*2, dec_hidden_size)
def forward(self,x,lengths):
embedded = self.dropout(self.embed(x))
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded,lengths,batch_first=True)
packed_out, hid = self.rnn(packed_embedded)
out,_ = nn.utils.rnn.pad_packed_sequence(packed_out,batch_first=True,total_length=max(lengths))
hid = torch.cat([hid[-2],hid[-1]],dim=1)# 将hid双向叠加 【batch, 2*enc_hidden_size】
hid = torch.tanh(self.fc(hid)).unsqueeze(0) # 转为decoder输入hidden state 【1,batch,dec_hidden_size】
return out,hid
class Attention(nn.Module):
""" """
def __init__(self,enc_hidden_size,dec_hidden_size):
super(Attention,self).__init__()
self.enc_hidden_size = enc_hidden_size
self.dec_hidden_size = dec_hidden_size
self.liner_in = nn.Linear(2*enc_hidden_size,dec_hidden_size)
self.liner_out = nn.Linear(2*enc_hidden_size+dec_hidden_size,dec_hidden_size)
def forward(self,output,context,mask):
batch_size = context.shape[0]
enc_seq = context.shape[1]
dec_seq = output.shape[1]
# score计算公式使用双线性模型 h*w*s
context_in = self.liner_in(context.reshape(batch_size*enc_seq,-1).contiguous())
context_in = context_in.view(batch_size,enc_seq,-1).contiguous()
atten = torch.bmm(output,context_in.transpose(1,2))
atten.data.masked_fill(mask,-1e6) # mask置零
atten = F.softmax(atten,dim=2)
context = torch.bmm(atten,context) # 将atten与source的hidden state输出做加权求和
output = torch.cat((context,output),dim=2) # 将attention + output 堆叠获取融合信息
output = torch.tanh(self.liner_out(output.view(batch_size*dec_seq,-1))).view(batch_size,dec_seq,-1) #Linear转换为target的hidden维度,再经tanh激活
return output,atten
decoder的结构为单向GRU。
def __init__(self,vocab_size,embedded_size,enc_hidden_size,dec_hidden_size,dropout=0.2):
super(Decoder,self).__init__()
self.embed = nn.Embedding(vocab_size,embedded_size)
self.atten = Attention(enc_hidden_size,dec_hidden_size)
self.rnn = nn.GRU(embedded_size,dec_hidden_size,batch_first=True)
self.out = nn.Linear(dec_hidden_size,vocab_size)
self.dropout = nn.Dropout(dropout)
def create_mask(self,x_len,y_len):
# 最长句子的长度
max_x_len = x_len.max()
max_y_len = y_len.max()
# 句子batch
batch_size = len(x_len)
# 将超出自身序列长度的元素设为False
x_mask = (torch.arange(max_x_len.item())[None, :] < x_len[:, None]).float()
y_mask = (torch.arange(max_y_len.item())[None, :] < y_len[:, None]).float()
# 需要mask的地方设置为true
mask = (1 - y_mask[:, :, None] * x_mask[:, None, :]) != 0
return mask
def forward(self,ctx,ctx_lengths,y,y_lengths,hid):
y_embed = self.dropout(self.embed(y))
y_packed = nn.utils.rnn.pack_padded_sequence(y_embed,y_lengths,batch_first=True,enforce_sorted=False)
pack_output, hid = self.rnn(y_packed,hid)
output_seq,_ = nn.utils.rnn.pad_packed_sequence(pack_output,batch_first=True,total_length=max(y_lengths))
mask = self.create_mask(ctx_lengths,y_lengths)
# annention处理
output,atten = self.atten(output_seq,ctx,mask)
output = F.log_softmax(self.out(output),dim=-1)
return output,atten,hid
将模型整合后,整个完整的模型计算图:
在最开始尝试以10epoch训练15万条数据,一个epoch跑完已经耗时10个小时,无奈最后将数据量减少到1.5万,epoch减少到5,耗时8个小时完成了训练。
即使如此,看上图中的loss也能知道,训练效果很糟糕。
相关参数:
2000条测试数据最后计算出来的bleu值为2.71,可以说是非常低了(对bleu值的说明见最后一小节)
我认为训练效果差与数据处理有很大的关系,IWSLT14 En-Zh这个数据集有太多的口语内容,比如说:
Ugh. Mini-Me. 呃。我太小了--
同时也有太多的话外音,比如说:
1.76 times 0.2 over here is 352 meters per second. (众笑+鼓掌) 1.76乘以0.2得到的是每秒352米。
关于话外音的问题在数据预处理阶段已经尽可能删除了,但是仍然存在一部分嵌入在句子内部的话外音无法删除干净。此外,语料中各个句子的长短很不一致,长的句子将近有100个单词,短的句子就一两个单词,这也会影响训练效果。当然最重要的原因是计算资源不够,有服务器的同学会相对好一点,直接用cpu跑的话根本跑不完所有的语料。在这里我贴一个用其他语料训练完成的模型,准确率会比我训的这个高很多:【审核中…】
运行do_translate模块,允许输入任意一个句子,控制台会打印出最优的五个翻译结果:
上图贴出来的都是正确翻译的结果,对于bleu值只有2.71的模型,不出所料绝大部分都是翻译都是错误的
参考:https://blog.csdn.net/guofei_fly/article/details/104053532
在RNN网络中,文本的pad操作用于各文本长度的对齐;而pack操作用于实现文本序列数据的压缩。
参考:https://blog.csdn.net/weixin_38646522/article/details/116764227
特征融合目前有两种常用的方式,一种是add操作,一种是Concat操作。
区别:
需要将A与B的Tensor进行融合:
在训练中每个样本的原始句子的长度是不一样的,在进行 batch训练之前,要先进行长度的统一,过长的句子可以通过truncating 截断到固定的长度,过短的句子可以通过 padding 增加到固定的长度,但是 padding 对应的字符只是为了统一长度,并没有实际的价值,因此希望在之后的计算中屏蔽它们,这时候就需要 Mask。
对于那些补零的数据,为了让attention机制不把注意力放在这些位置上,把这些位置的值加上一个非常大的负数(负无穷),经过softmax后,这些位置的权重就会接近0。Transformer的padding mask实际上是一个张量,每个值都是一个Boolean,值为false的地方就是要遮挡的地方。
将输入组成输入矩阵,乘以一个 mask矩阵,屏蔽当前词到最后的词,使当前词只能看到它前面的词。用在decoder端。
参考:https://zhuanlan.zhihu.com/p/36029811?group_id=972420376412762112
在Beam Search中只有一个参数B,叫做beam width(集束宽),用来表示在每一次挑选top B的结果。在集束宽为3时,集束搜索一次只考虑3个可能结果。注意如果集束宽等于1,只考虑1种可能结果,这实际上就变成了贪婪搜索算法,但是如果同时考虑多个,可能的结果比如3个,10个或者其他的个数,集束搜索通常会找到比贪婪搜索更好的输出结果。
好啦,这次的作业也算是勉强顺利完成啦