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

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

1.程序与实验说明

实验要求

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

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

程序说明

代码:https://download.csdn.net/download/qq_39328436/69026304
程序目录:
在这里插入图片描述

  • 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模型:
    在这里插入图片描述
    • 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)。
在这里插入图片描述
在RNN模型基础上加入Attention机制就能解决上面提到的这个问题了。
在这里插入图片描述

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的区别如下:
在这里插入图片描述

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
    在这里插入图片描述

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中的数据每一行是对齐的.
在这里插入图片描述

数据处理

在数据处理这一部分,需要将中英文语料按行合并到一起,每一行前部分是英文,通过一个制表符连接英文句子对应的中文。
在这里插入图片描述
按行合并两个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条。

4. 模型

整个模型由Encoder、Attention及Decoder组成,外层用Seq2Seq统一包装。模型结构如下图所示:
在这里插入图片描述
编码器采用双向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)
      在这里插入图片描述
  • 步骤二: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个小时完成了训练。
在这里插入图片描述
即使如此,看上图中的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模块,允许输入任意一个句子,控制台会打印出最优的五个翻译结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上图贴出来的都是正确翻译的结果,对于bleu值只有2.71的模型,不出所料绝大部分都是翻译都是错误的😭

7.疑问与思考

🖤序列生成模型评价指标

BLEU-精度

  • bilingual evaluation understudy :衡量模型生成序列与参考序列之间的N元词组的重合度,最早用来评价机器翻译模的质量,目前也广泛应用在各种序列生成任务中。
  • 实现方法:统计同时出现在生成序列和参考序列中 的 n 元词的个数,最后把匹配到的n 元词的数目除以生成序列单词数目,得到评测结果( 𝒏 元组集合的精度)
  • Bleu值只计算精度,不关心召回率(即参考序列中的n元组合是否在生成序列中出现)
    在这里插入图片描述

ROUGE-召回率

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

💛Pytorch中的pack和pad操作

参考:https://blog.csdn.net/guofei_fly/article/details/104053532
在RNN网络中,文本的pad操作用于各文本长度的对齐;而pack操作用于实现文本序列数据的压缩。
在这里插入图片描述

在这里插入图片描述

💚信息融合

参考: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。
在这里插入图片描述
对于那些补零的数据,为了让attention机制不把注意力放在这些位置上,把这些位置的值加上一个非常大的负数(负无穷),经过softmax后,这些位置的权重就会接近0。Transformer的padding mask实际上是一个张量,每个值都是一个Boolean,值为false的地方就是要遮挡的地方。
在这里插入图片描述

Sequence Mask

将输入组成输入矩阵,乘以一个 mask矩阵,屏蔽当前词到最后的词,使当前词只能看到它前面的词。用在decoder端。
在这里插入图片描述

💜beam search

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

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

  • 12
    点赞
  • 73
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
PyTorch是一种深度学习框架,可以用于实现序列到序列(seq2seq)的机器翻译任务。在seq2seq模型中,编码器将源序列编码为一个固定长度的向量,解码器则将该向量解码为目标序列。为了提高翻译质量,可以使用注意力机制来在解码器中引入上下文信息。 在PyTorch实现seq2seq模型,可以使用nn.Module类来定义模型架构。首先,需要定义编码器和解码器的结构。编码器通常使用循环神经网络(RNN)或卷积神经网络(CNN)进行实现,而解码器则需要使用注意力机制。注意力机制可以使解码器关注输入序列中最相关的部分并根据其进行翻译。 实现注意力机制时,需要计算每个输入序列位置和当前解码器状态之间的相似度。这可以通过计算点积或使用神经网络来实现。然后,可以将相似度作为权重,对输入序列进行加权求和,以计算上下文向量。最后,将上下文向量与当前解码器状态组合在一起,以生成下一个目标序列符号的概率分布。 在训练过程中,可以使用交叉熵损失函数来计算模型输出与正确目标序列之间的差异,并使用反向传播算法更新模型参数。在推理过程中,可以使用贪婪搜索或束搜索来生成翻译结果。 总的来说,PyTorch提供了一种灵活且高效的方式来实现seq2seq模型和注意力机制,可以用于各种自然语言处理任务,包括机器翻译、问答系统和对话生成等。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vector<>

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值