Pytorch_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”转换成矩阵。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容