美文网首页
Pytorch_Seq2Seq与Attention

Pytorch_Seq2Seq与Attention

作者: xieyan0811 | 来源:发表于2019-12-18 10:46 被阅读0次

    自然语言处理是典型的序列问题,其底层算法在最近几年迅速发展,比如去年年底发布的BERT在11项自然语言处理任务中表现卓越,今年GPT-2生成文本(写作)的水平也有了显著提高。

    目前这些最先进的技术都基于Transformer模型,该模型从RNN,LSTM,Seq2Seq,Attention,ConvS2S,Transformer一步步进化而来,还涉及自然语言处理的相关知识,包含的知识点太多,无法一次说清。笔者将其分成几篇,从其进化过程逐步引入。之前已经介绍过RNN及LSTM,本篇将介绍Seq2Seq和Attention算法。

    翻译功能

    深度学习中的自然语言处理常用于自动翻译、语言识别、问答系统、提取概要、写作等等领域。

    其中自动翻译是一项非常典型的应用,在翻译过程中,输入和输出的词汇个数可长可短,不能一一对应,不同语言词汇顺序又可能不同,并且还有一词多义,一义多词,词在不同位置含义不同的情况……是相对复杂的自然语言处理问题。

    先来看看人怎么解决翻译问题,面对一种完全不认识的语言,人把句子分解成词,通过查字典的方式将词转换成母语,然后再通过语法组合成句。其中主要涉及词的实际含义、内容的先后关系,两种语言对应关系。机器既不需要了解各个词的含义和语法,也不需要字典,就能通过大量训练实现翻译功能,并且效果还不错。这让神经网络看起来更加难以理解。

    一开始的深度学习神经网络,没有逐词翻译的逻辑,主要实现的是序列生成模型,根据前面的一个词或者几个词去推测后面的词。所以人们认为,机器并没有真正理解语言,以及两种语言之间的对应关系,通过训练生成的知识分散在网络各个节点用权重表示,也不能提炼总结,完全是个黑盒。同时,它也不能代入已有的知识,如果换成与训练数据不同的情境,就无法正常工作了。

    翻译模型发展到今天,已很大程度改善了这一问题,现在的模型可以通过训练学习到什么是“苹果”,也可以生成翻译词典。而且这些规则不需要事先输入,是它自己“学”出来的。通过注意力算法,不仅能实现翻译,还能找到词间的对应关系(双语词典);词向量可以从多个角度描述词的特征,对比“苹果”和“沙果”的相似度(词汇含义);据此,就可以把高频率出现的规则总结成知识。

    Seq2Seq

    1. 引入

    设想最简单的情况,将一句中文X(x1,x2,x3,x4)翻译成英文Y(y1,y2,y3)。

    如果把模型想像成黑盒,则如图下所示:

    由于不同语言的词汇不存在绝对的一一对应关系,人工翻译一般是看完输入的完整句子,才开始翻译并输出,如果有条件,最好还能看一下上下文语境。模型处理数据流也是如此。

    前几篇介绍了循环神经网络RNN,它不断向后传递隐藏层h的内容,使得序列中的信息逐步向后传递,下图是RNN网络在翻译问题中最简单的用法,LSTM和GRU原理与RNN相同。

    在RNN循环网络中,神经网络的每个时间步对应同一组参数,这些参数存储着翻译功能所包含的大量信息;在翻译任务中,两种语言的词汇语法不同,用同一组参数描述它们显然比较粗糙。如果能对两种语言生成两种规则,用不同网络的不同参数描述,则更加合理。于是,将翻译过程拆分为编码Encoder和解码Decoder两个子模型,可把这个过程想像成:先把中文翻译成一种语义编码c,再把语义编码c翻译成英文。

    进一步细化,在Decoder过程中,生成每个词汇时,除了需要依赖上一步的隐藏层输出,还需要参考输出序列的前一个词,使得生成的序列符合语法规则(如介词的位置),设置输出序列的第一个词为<start>,最后一个词为<end>,细化后的逻辑如下图示。

    2. 概念

    Seq2Seq也被称为S2S,是Sequence to Sequence的简称,即序列到序列的转换。它始于谷歌在2014年发表的一篇论文《Sequence to Sequence Learning with Neural Networks》。

    上图中的Encoder-Decoder网络结构就是Seq2Seq,Encoder和Decoder可以使用RNN,LSTM,GRU等基础模型。简言之,就是把翻译中原来的一个循环网络变成了两个。

    除了翻译,Seq2Seq也被用于提取概要,问答,语音识别等场景之中,处理输入和输出规则不同的情况,但是在生成文本的任务中,比如通过前面文字续写后续文字,输入和输出都是同样的序列,则无需Seq2Seq。

    转换词向量

    在自然语言处理中,常将单词作为序列中的元素。

    模型只能接收数值型数据,代入模型前,需要把词汇转换成数值,如果使用One-Hot编码,数据维度将非常大,并且无法描述词与词之间的相似度。更常用的方法是词嵌入Word Embedding,它将每个词表示成向量,比如把“hello”,转换成三维的值[-1.7123, -0.6566, -0.6055],可将该操作理解成:把一个词汇拆分成为多个属性。通过比较各个属性的差异可以计算两个词汇之间的距离。

    在不同层面,不同角度将看到事物的不同属性(特征),比如梨和苹果都是水果,但是颜色差异很大,通过模型计算出来的词属性与训练的目标以及训练数据有关。词汇的特征通过反向传播计算得来,从这个角度看,神经网络对每个词进行了特征提取,也可作为词特征提取工具来使用。

    在Pythorch中使用torch.nn.Embedding可实现该功能,它提供了词的索引号与向量之间的转换表。用法是: torch.nn.Embedding(m, n) 其中m 表示单词的总数目,n 表示词嵌入的维度(一个词转成几个特征,常用的维度是256-512),词嵌入相当于将输入的词序列转换成一个矩阵,矩阵的每一行表示一个单词,列为每个单词的多个特征。Embedding也是一层网络,其参数通过训练求得。而词对应的每一维特征的具体值如-1.7123通过这些参数计算得出。

    下面例程,将词序列“hello world”转换成矩阵。

    from torch import nn
    from torch.autograd import Variable
    
    dic = {'hello':0, 'world':1} # 词汇与索引号转换字典
    embed = nn.Embedding(2, 3) # 共两个词汇,每个词汇转换成三个特征  
    # Embedding的输入是一个LongTensor。
    print(embed(Variable(torch.LongTensor([1])))) # 1为词汇的索引号
    # 输出结果:tensor([[-1.5716, 0.8978, 0.4581]], grad_fn=<EmbeddingBackward>)
    print(embed(Variable(torch.LongTensor([dic['hello'],dic['world']]))))
    # 输出结果:tensor([[-1.7123, -0.6566, -0.6055],
    # [-1.5716, 0.8978, 0.4581]], grad_fn=<EmbeddingBackward>)
    

    Attention

    注意力Attention指的是一类算法,常见的有local attention,global attention,self attention等等。

    注意力方法最初出现在图像处理问题之中,当人眼观察一幅图像时,某一时刻的视觉焦点只集中在一点上,其注意力是不均衡的,视觉注意力焦点可提高效率和准确性。算法借鉴了人类注意力机制,实现方法是给不同的数据分配不同的权重。

    在上述的Seq2Seq模型中,生成目标句子中的单词时,不论生成哪个单词,都根据语义编码C,比如将“I love you” 翻译成“我爱你”时,“I love you”三个词对“我”的贡献度都一样,而我们希望“I”对“我”的贡献度更大,于是使用了Attention算法。

    实现Attention的方式有很多种,这里展示比较常用的一种。在Encoder的过程中保留每一步RNN单元的隐藏状态h1……hn,组成编码的状态矩阵Encoder_outputs;在解码过程中,原本是通过上一步的输出yt-1和前一个隐藏层h作为输入,现又加入了利用Encoder_outputs计算注意力权重attention_weight的步骤。

    用图和文字很难说清楚,看代码更容易,下面分析将Pytorch官方教程Attention模型的核心部分,完整程序见:
    https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html
    建议读者运行该例程,跟踪每一步的输入和输出,可以尝试修改代码实现中文互译功能。

    下面为编码器Encoder的实现部分,编码器包含:词向量转换embedding和循环网络GRU。

    class EncoderRNN(nn.Module):
        # 参数:input_size为输入语言包含的词个数
        def __init__(self, input_size, hidden_size):
            super(EncoderRNN, self).__init__()
            self.hidden_size = hidden_size
            self.embedding = nn.Embedding(input_size, hidden_size) #每词 hidden_size个属性
            self.gru = nn.GRU(hidden_size, hidden_size)
    
        def forward(self,input, hidden):
            embedded = self.embedding(input).view(1,1,-1)
            output = embedded
            output, hidden = self.gru(output, hidden)
             return output, hidden
    
        def initHidden(self):
            return torch.zeros(1,1, self.hidden_size, device=device)
    

    其中forward每次处理序列中的一个元素(一个词)。

    难度较大的是Decoder解码模块,注意力逻辑主要实现在该模块中:

    class AttnDecoderRNN(nn.Module):
        # 参数:output_size为输出语言包含的所有单词数
        def __init__(self,hidden_size,output_size, dropout_p=0.1, max_length = MAX_LENGTH):
            super(AttnDecoderRNN, self).__init__()
            self.hidden_size = hidden_size
            self.output_size = output_size
            self.dropout_p = dropout_p
            self.max_length = max_length
            self.embedding = nn.Embedding(self.output_size, self.hidden_size)
            self.attn = nn.Linear(self.hidden_size*2, self.max_length)
            self.attn_combine = nn.Linear(self.hidden_size*2, self.hidden_size)
            self.dropout = nn.Dropout(self.dropout_p)
            self.gru = nn.GRU(self.hidden_size, self.hidden_size)
            self.out = nn.Linear(self.hidden_size, self.output_size) # 把256个特征转换成输出语言的词汇个数
    
        # 参数:input每步输入,hidden上一步结果,encoder_outputs编码的状态矩阵
        # 计算的值是各词出现的概率
        def forward(self, input, hidden, encoder_outputs):
            embedded = self.embedding(input).view(1,1,-1)
            embedded = self.dropout(embedded)
            attn_weights = F.softmax(
            self.attn(torch.cat([embedded[0],hidden[0]],1)),dim=1)
            attn_applied = torch.bmm(attn_weights.unsqueeze(0), # unsqueeze维度增加
            encoder_outputs.unsqueeze(0))
            output = torch.cat([embedded[0], attn_applied[0]],1) # 注意力与当前输入拼接
            output = self.attn_combine(output).unsqueeze(0)
            output = F.relu(output) # 激活函数
            output, hidden = self.gru(output, hidden)
            output = F.log_softmax(self.out(output[0]),dim=1)
            return output, hidden, attn_weights
    
        def initHidden(self):
            return torch.zeros(1,1, self.hidden_size, device=devic
    

    代码核心是前向传播函数forward,第一个难点是计算attn_weights,先用cat组装输入词向量embedded和隐藏层hidden信息256+256=512,转入全连接层attn,转换后输出10维数据(序列最长10个单词),再用softmax转成和为1的比例值。计算结果是注意力权重attn_weights大小为[1,10],它描述的是输入encoder中各位置元素对当前decoder输出单词的重要性占比,比如“I love you”对“爱”字的重要性分别是[0.2,0.6,0.2]。训练调整attn层参数以实现这一功能。

    然后计算attn_applied,用注意力权重attn_weights[1,10](每个位置的重要性)乘记录encoder每一步状态的矩阵encoder_outputs[10,256](每个位置的状态)。得到一个综合权重attn_applied[1,256],用于描述“划了重点”之后的输入序列对当前预测这个单词的影响。得出attn_applied之后,再与词向量embed值组合、转换维度、经过激活函数处理后,和隐藏层一起传入gru循环网络。

    最后通过全连接层out把256维特征转换成输出语言对应的单词个数,其中每维度的值描述了生成该词的可能性,再用log_softmax转换成输出要求格式,以便与其误差函数配合使用(后面详细介绍)。

    下面是训练部分,每调用一次train训练一个句子。其中传入的encoder和decoder分别是上面定义的EncoderRNN和AttnDecoderRNN,input_tensor和target_tensor是训练的原句和译文。

    def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer,
        decoder_optimizer, criterion, max_length = MAX_LENGTH):
        encoder_hidden = encoder.initHidden()
        encoder_optimizer.zero_grad() # 分别优化encoder和decoder
        decoder_optimizer.zero_grad()
        input_length = input_tensor.size(0)
        target_length = target_tensor.size(0)
        encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
        loss = 0
    
        for ei in range(input_length): # 每次传入序列中一个元素
            encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
            encoder_outputs[ei]=encoder_output[0,0] # seq_len为1,batch_size为1,大小为 hidden_size
    
        decoder_input = torch.tensor([[SOS_token]], device=device) # SOS为标记句首
        decoder_hidden = encoder_hidden # 把编码的最终状态作为解码的初始状态
    
        for di in range(target_length): # 每次预测一个元素
            decoder_output, decoder_hidden, decoder_attention = decoder(
            decoder_input, decoder_hidden, encoder_outputs)
            topv, topi = decoder_output.topk(1) # 将可能性最大的预测值加入译文序列
            decoder_input = topi.squeeze().detach()
            loss+=criterion(decoder_output, target_tensor[di])
            if decoder_input.item()==EOS_token:
            break
    
        loss.backward()
        encoder_optimizer.step()
        decoder_optimizer.step()
        return loss.item() / target_length
    

    其中第一个循环为Encoder,程序对输入序列中每个元素做encoder,并把每一次返回的中间状态hidden存入encoder_outputs,最终生成保存所有位置状态的矩阵encoder_outputs。

    第二个循环为Decoder,程序利用当前的隐藏状态decoder_hidden,解码序列的前一个元素decoder_input,和输入的状态矩阵encoder_outputs做解码,并从解码器的输出中选中最有可能的单词作为后序的输入,直到序列结束。其整体误差是每个元素误差的平均值。

    Attention还有很多变型,比如local attention为了减少计算量,加入了窗口的概念,只对其中一部分位置操作(选一个点,向右左扩展窗口),窗口以外都取0;self attention将在下篇Transformer中详细介绍。

    词向量转换成词

    翻译的第一步是将词的索引号转换成词向量,相对的,最后一步将词向量转换成词的索引号,以确定具体的词。Decoder的最后部分实现了该功能,它使用全连接层out进行维度转换,最后使用log_softmax转换成概率的log值。

    softmax输出的是概率,整体可能性为1。比如输出的语言只有三个词汇[‘a’,’b’,’c’],softmax求出它们的可能性分别是[0.1,0.1,0.9],那么此外最可能是’c’。Log_softmax是对softmax的结果再做log运算,生成对数概率向量。

    log函数曲线如下:

    由于softmax输出的各个值在0-1之间,梯度太小对反向传播不利,于是log_softmax将0-1映射到负无穷到0之间更宽的区域之中,从而放大了差异。同时,它与损失函数NLLLoss配合使用,NLLLoss的输入是一个对数概率向量和一个目标标签,正好对应最后一层是log_softmax的网络。另外,也可以使用交叉熵作为误差函数:CrossEntropyLoss=log_softmax + NLLLoss。

    参考

    Seq2Seq论文《Sequence to Sequence Learningwith Neural Networks》
    https://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks.pdf

    Attention论文《Neural machine translation by jointly learning to align and translate》
    https://arxiv.org/pdf/1409.0473v2.pdf

    相关文章

      网友评论

          本文标题:Pytorch_Seq2Seq与Attention

          本文链接:https://www.haomeiwen.com/subject/skbynctx.html