Seq2Seq与Attention
Seq2Seq 与 Attention
自然语言处理是典型的序列问题,其底层算法在最近几年迅速发展,比如去年年底发布的 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”转换成矩阵。
1 | from torch import nn |
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。
1 | class EncoderRNN(nn.Module): |
其中 forward 每次处理序列中的一个元素(一个词)。
难度较大的是 Decoder 解码模块,注意力逻辑主要实现在该模块中:
1 | class AttnDecoderRNN(nn.Module): |
代码核心是前向传播函数 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 是训练的原句和译文。
1 | def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, |
其中第一个循环为 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》