【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】

这里是国科大自然语言处理的第四次作业,同样也是从小白的视角解读程序和代码,现在我们开始吧(今天也是花里胡哨的一天呢

目录

  • 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

1.程序与实验说明

实验要求

  1. 任选一个深度学习框架建立一个机器翻译模型,实现在IWSLT14 En-Zh数据集上进行的机器翻译
  2. 序列生成任务需要大量的计算资源,公平起见,本作业会按照提交的代码判定成绩而不是模型的训练效果

事实证明老师的考虑是很周到的,我虽然完成了实验内容,但是模型的准确率实在是上不了台面☹️,这一点也会在后面进行说明

程序说明

代码:https://download.csdn.net/download/qq_39328436/69026304
程序目录:
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第1张图片

  • corpus中是IWSLT14 En-Zh数据集的原始语料以及经过预处理后的文件
  • data中存储的是最终参与训练和测试的数据
  • runs文件夹保存每次训练记录
  • model.py中描述模型代码,比如encoder,decoder等
  • translate-best.th:do_train为训练模块,do_test为测试模块,do_translate是一个小demo,输入英文句子会打印出损失最低的5个翻译结果。
  • translate.py中
  • translation_model.log记录训练过程中的损失

2.知识概述

2.1 序列生成问题Seq2Seq

  • 深度学习中建模序列生成问题方法:构建一个联合的神经网络,以端到端的方式将一个序列化数据映射成另一个序列化数据。简称 Sequence-to-Sequence Generation (Seq2Seq)模型。主流的Seq2Seq模型通常基于Encoder-Decoder框架实现。
  • Seq2Seq模型:
    【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第2张图片
    • Encoder:将输入序列进行编码形成后继处理需要的输入表示形式
    • 生成式模型Decoder:根据编码端形成的输入表示和先前时刻产生成的输出tokens,生成当前输出token ( 编码端和解码端有各自词表,二者可相同或不同 。解码端需处理集外词OOV,一般用UNK 代替)
    • 选择式模型Decoder:根据编码端形成的输入表示和先前时刻产生成的输出tokens,从输入端选择一个token作为输出 token ( 解码端和编码端词表相同
    • 选择-生成式模型Decoder:根据编码端形成的输入表示和先前时刻产生成的输出tokens,生成或从输入端选择当前输出token ( 编码端和解码端有各自词表,二者可相同或不同 。解码端需处理集外词OOV,一般用UNK 代替,该方法可有效的处理输出端的OOV 问题)

Encoder和Decoder具体使用什么模型都是由研究者自己确定。比如:CNN/RNN/BiRNN/GRU/LSTM/transformer等。很明显本次实验机器翻译任务是生成式decoder。

2.2 RNN+Attention 架构生成模型

纯RNN的生成模型会有什么问题?

输入序列(x1,x2,x3)经过模型得到生成序列(y1,y2,y3),当模型翻译任意一个yi时,所用到的中间语义C都是同一个。而事实上,当我们翻译“杰瑞”时,英文单词“Jerry”应该比其他单词有更重要的影响,比如(Tom,0.3)(Chase,0.2)(Jerry,0.5)。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第3张图片
在RNN模型基础上加入Attention机制就能解决上面提到的这个问题了。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第4张图片

2.3 机器翻译

任务描述:
机器翻译是利用计算机把一种语言(源语言, source language) 翻译成另一种语言(目标语言, target language)的技术。神经机器翻译是序列生成问题,主流神经机器翻译模型有基于RNN的,基于CNN的和基于自注意力机制的。

神经机器翻译系统需要考虑的问题:

  1. 词汇表受限问题:考虑到计算的复杂度问题,在神经机器翻译模型中会使用一个受限词表,这样会导致很多单词成了词表外的OOV词。而这种OOV词在翻译时很难处理并且打破了句子结构,增加了语句的歧义性,因此,如何处理罕见词成为NMT领域非常必要的研究问题。
  2. 翻译覆盖率问题:在**“seq2seq+attention”**框架下机器翻译过程中,翻译当前词汇的“注意力”与翻译在此之前的词汇的“注意力”是独立的,当前的操作不能从之前的翻译中获取alignment相关的信息,这样就导致了“过翻译” (Over-Translation:源端某些词被重复翻译若干次)和“欠翻译” (Under-Translation:源端某些词未被翻译)的问题,coverage机制通过在解码的过程中,保持对attention信号持续关注(利用),可以缓解过翻译和欠翻译问题。
  3. 系统鲁棒性问题:神经网络能够对全局上下文进行建模,但对于局部变化过于敏感如,提升系统的容错性,一致性(鲁棒性)对用户体验十分重要。可采用对抗学习等训练方法提升系统的鲁棒性。

2.4 GRU

GRU(Gate Recurrent Unit)是循环神经网络(Recurrent Neural Network, RNN)的一种。和LSTM(Long-Short Term Memory)一样,也是为了解决长期记忆和反向传播中的梯度等问题而提出来的。

GRU与LSTM的区别如下:
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第5张图片

2.5 注意力机制

注意力机制是神经网络中的一个加权求和的组件。输入是Q,K,输出是Att-V。Attention要回答的问题是:对于Q来说K有多重要?,重要性由输出V描述。Attention机制主要分为三个步骤,对应下图中的三个阶段。

  1. 计算F(Q,Ki):F为注意力打分函数,本质上该打分函数描述Q和K之间的关系,它可以是一个小型的神经网络。常见的打分函数有点积模型,缩放点积模型,双线性模型
  2. softmax(f(Q,Ki)):经过softmax之后会形成一个概率分布,也就是得到了权重
  3. 加权求和:Att-V = 1ⅹK1+2ⅹK2+3ⅹK3+4ⅹK4+5ⅹK5
    【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第6张图片

3.数据

数据来源

数据来源于小规模数据集: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中的数据每一行是对齐的.
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第7张图片

数据处理

在数据处理这一部分,需要将中英文语料按行合并到一起,每一行前部分是英文,通过一个制表符连接英文句子对应的中文。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第8张图片
按行合并两个txt文件本不是一件难事,但是简单的合并会导致出现大量下图中的数据:
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第9张图片
这是因为这个数据集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条。

4. 模型

整个模型由Encoder、Attention及Decoder组成,外层用Seq2Seq统一包装。模型结构如下图所示:
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第10张图片
编码器采用双向RNN,解码器采用单向RNN,Attention采用双线性Att。

Encoder

Encoder采用BiGRU结构

  • nn.embedding :将输入的句子映射为词向量。
  • nn.GRU: bidirectional =true表示设置为双向GRU,输入输出需要pack、pad。
  • nn.Dropout:讲输入张量部分元素设置为0,防止模型过拟合
  • nn.Linear:线性Linear层,将GRU最后一个hidden state变换为decoder的初始hidden state输入
  • nn.pad_packed_sequenceh,nn.pack_padded_sequence:实现对文本的填充和相互转化。在RNN网络中,文本的pad操作用于各文本长度的对齐;而pack操作用于实现文本序列数据的压缩。
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

Attention

  • 步骤一:计算F(Q,Ki)
    • 打分函数由nn.Linear双线性模型实现(双线性Attention)
      【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第11张图片
  • 步骤二:softmax
    • F.softmax(atten,dim=2) 得到概率
  • 步骤三:加权求和
    • torch.bmm(atten,context)
  • masked_fill(mask,-1e6):atten做mask,对于source中pad的部分以及target的pad部分用很小的负数代替,以消除后面对softmax的概率影响
  • torch.cat((context,output),dim=2):注意力输出和source state做信息融合concate
  • context:encoder的gru hidden state
  • output:decoder的gru hidden state
  • mask:在decoder中创建
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

decoder的结构为单向GRU。

  • nn.Embedding:Embedding层,将target输入查找词向量
  • nn.GRU:单向GRU层,输入输出需要pack、pad,为了保证和source句子对对齐,我们没法保证按句子长度排序,pack_padded_sequence时需要将enforce_sorted置为False。
  • self.atten:进行Attention操作
  • self.create_mask:在Attention之前需要创建mask,具体而言是创建Padding Mask。
  • log_softmax: 将输出转为vocab_size的softmax概率分布并取对数
 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

Seq2Seq

将模型整合后,整个完整的模型计算图:

  1. src输入Embedding层src_embed
  2. src_embed经过双向GRU层,得到src_hidden,src_last_h
  3. src_last_h经过线性层、tanh激活得到decoder的初始hidden输入tgt_init_h
  4. tgt输入Embedding层tgt_embed
  5. tgt_embed及tgt_init_h经过单向GRU层,得到tgt_hidden
  6. 根据src及tgt句子batch中的长度,创建mask
  7. src_hidden和tgt_hidden做双线性attention得到输出a_tt
  8. a_tt做mask后softmax归一化为概率分布
  9. src_hidden与a_tt加权求和输出att_value
  10. att_value与tgt_hidden信息融合concate后输入线性层、tanh激活输出为tgt_output
  11. tgt_output输入线性层、softmax后取对数,得到最终的target vocab size上的对数概率分布。模型的解码过程使用beam search,最大解码长度默认取100,主要是我们的语料数据较少且语句较短。

5.训练

在最开始尝试以10epoch训练15万条数据,一个epoch跑完已经耗时10个小时,无奈最后将数据量减少到1.5万,epoch减少到5,耗时8个小时完成了训练。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第12张图片
即使如此,看上图中的loss也能知道,训练效果很糟糕。

相关参数:

  • batch_size=16
  • learnning_rate=5e-4
  • dropout=0.2
  • epoch=5
  • optimizer:AdamW

6.测试

测试效果

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跑的话根本跑不完所有的语料。在这里我贴一个用其他语料训练完成的模型,准确率会比我训的这个高很多:【审核中…】

demo展示

运行do_translate模块,允许输入任意一个句子,控制台会打印出最优的五个翻译结果:
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第13张图片
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第14张图片
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第15张图片
上图贴出来的都是正确翻译的结果,对于bleu值只有2.71的模型,不出所料绝大部分都是翻译都是错误的

7.疑问与思考

序列生成模型评价指标

BLEU-精度

  • bilingual evaluation understudy :衡量模型生成序列与参考序列之间的N元词组的重合度,最早用来评价机器翻译模的质量,目前也广泛应用在各种序列生成任务中。
  • 实现方法:统计同时出现在生成序列和参考序列中 的 n 元词的个数,最后把匹配到的n 元词的数目除以生成序列单词数目,得到评测结果( 元组集合的精度)
  • Bleu值只计算精度,不关心召回率(即参考序列中的n元组合是否在生成序列中出现)
    【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第16张图片

ROUGE-召回率

  • recall-oriented understudy for gisting evaluation :最早应用于文本摘要领域,和bleu值相似,但是rouge计算的是召回率

Pytorch中的pack和pad操作

参考:https://blog.csdn.net/guofei_fly/article/details/104053532
在RNN网络中,文本的pad操作用于各文本长度的对齐;而pack操作用于实现文本序列数据的压缩。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第17张图片

【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第18张图片

信息融合

参考:https://blog.csdn.net/weixin_38646522/article/details/116764227
特征融合目前有两种常用的方式,一种是add操作,一种是Concat操作。
区别:

  • 对于Concat操作而言,通道数的合并,也就是说描述图像本身的特征增加了,而每一特征下的信息是没有增加。
  • 对于add层更像是信息之间的叠加。add前后的tensor语义是相似的。

需要将A与B的Tensor进行融合:

  • 如果它们语义不同,则我们可以使用Concat的形式。
  • 如果A 与B是相同语义,如A与B是不同分辨率的特征,其语义是相同的,我们可以使用add来进行融合

❤️mask机制

Padding Mask

在训练中每个样本的原始句子的长度是不一样的,在进行 batch训练之前,要先进行长度的统一,过长的句子可以通过truncating 截断到固定的长度,过短的句子可以通过 padding 增加到固定的长度,但是 padding 对应的字符只是为了统一长度,并没有实际的价值,因此希望在之后的计算中屏蔽它们,这时候就需要 Mask。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第19张图片
对于那些补零的数据,为了让attention机制不把注意力放在这些位置上,把这些位置的值加上一个非常大的负数(负无穷),经过softmax后,这些位置的权重就会接近0。Transformer的padding mask实际上是一个张量,每个值都是一个Boolean,值为false的地方就是要遮挡的地方。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第20张图片

Sequence Mask

将输入组成输入矩阵,乘以一个 mask矩阵,屏蔽当前词到最后的词,使当前词只能看到它前面的词。用在decoder端。
【一起入门NLP】中科院自然语言处理作业四:RNN+Attention实现Seq2Seq中英文机器翻译(Pytorch)【代码+报告】_第21张图片

beam search

参考:https://zhuanlan.zhihu.com/p/36029811?group_id=972420376412762112
在Beam Search中只有一个参数B,叫做beam width(集束宽),用来表示在每一次挑选top B的结果。在集束宽为3时,集束搜索一次只考虑3个可能结果。注意如果集束宽等于1,只考虑1种可能结果,这实际上就变成了贪婪搜索算法,但是如果同时考虑多个,可能的结果比如3个,10个或者其他的个数,集束搜索通常会找到比贪婪搜索更好的输出结果。

好啦,这次的作业也算是勉强顺利完成啦

你可能感兴趣的:(#,自然语言处理,自然语言处理,机器翻译,pytorch,attention)