当前位置:   article > 正文

跟我一起学PyTorch-08:循环神经网络RNN_循环神经网络在一对一模型中的应用有分词,词性标记,命名实体识别和

循环神经网络在一对一模型中的应用有分词,词性标记,命名实体识别和

前面提到了CNN和基于CNN的各类网络及其在图像处理上的应用。这类网络有一个特点,就是输入和输出都是固定长度的。比方说在MNIST、CIFAR-10、ImageNet数据集上,这些算法都非常有效,但是只能处理输入和输出都是固定长度的数据集。

在实际中,需要处理很多变长的需求,比方说在机器翻译中,源语言中每个句子长度是不一样的,源语言对应的目标语言的长度也是不一样的。这时使用CNN就不能达到想要的效果。从结构上讲,全连接神经网络和卷积神经网络模型中,网络的结构都是输入层-隐含层(多个)-输出层结构,层与层之间是全连接或者部分连接,但是同一层内是没有连接的,即所有的连接都是朝一个方向。

使计算机模仿人类的行为一直是大家研究的方向,对于图片的识别可以用CNN,那么序列数据用什么呢?本章介绍对于序列数据的处理,以及神经网络家族中另一种新的神经网络——循环神经网络(Recurrent Neural Network,RNN)。RNN是为了处理变长数据而设计的。

本章内容首先提到的是序列数据的处理,然后介绍标准的RNN以及它面临的一些问题,随后介绍RNN的一些扩展LSTM(Long Short-Term Memory)以及RNNs(Recurrent Neural Networks,基于循环神经网络变形的统称)在NLP(Natural Language Process,自然语言处理)上的应用,最后结合一个示例介绍PyTorch中RNNs的实现。

1.序列数据处理

序列数据包括时间序列及串数据,常见的序列有时序数据、文本数据、语音数据等。处理序列数据的模型称为序列模型。序列模型是自然语言处理中的一个核心模型,依赖时间信息。传统机器学习方法中序列模型有隐马尔可夫模型(Hidden Markov Model,HMM)和条件随机场(Conditional Random Field,CRF)都是概率图模型,其中HMM在语音识别和文字识别领域应用广泛,CRF被广泛应用于分词、词性标注和命名实体识别问题。

神经网络处理序列数据,帮助我们从已知的数据中预测未来的模式,在模型识别上取得很好的效果。作为预测未来模式的神经网络有窥视未来的本领,比方说可以根据过去几天的股票价格来预测股票趋势。在前馈神经网络中对于特定的输入都会有相同的输出,所以我们要正确地对输入信息进行编码。对时间序列数据的编码有很多,其中最简单、应用最广泛的编码是基于滑动窗口的方法。下面介绍一下滑动窗口编码的机制。

在时间序列上,滑动窗口把序列分成两个窗口,分别代表过去和未来,这两个窗口的大小都需要人工确定。比如要预测股票价格,过去窗口的大小表示要考虑多久以前的数据进行预测,如果要考虑过去5天的数据来预测未来2天的股票价格,此时的神经网络需要5个输入和2个输出。考虑下面一个简单的时间序列:

1,2,3,4,3,2,1,2,3,4,3,2,1

神经网络可以用3个输入神经元和1个输出神经元,也就是利用过去3个时间的数据预测下1个时间的数据,这时在训练集中序列数据应该表示如下:

[1,2,3] --> [4]
[2,3,4] --> [3]
[3,4,3] --> [2]
[4,3,2] --> [1]

也就是说,从串的起始位置开始,输入窗口大小为3,第4个为输出窗口,是期望的输出值。然后窗口以步长为1向前滑动,落在输入窗口的为输入,落在输出窗口的为输出。这样在窗口向前滑动的过程中,产生一系列的训练数据。其中,输入窗口和输出窗口的大小都是可以变化的,比方说要根据过去3个时间点的数据预测来2个时间点的数据,也就是输出窗口的大小为2,此时得到的训练数据为:

[1,2,3] --> [4,3]
[2,3,4] --> [3,2]
[3,4,3] --> [2,1]
[4,3,2] --> [1,2]

上面的两个例子是在一个时间序列上对数据进行编码,也可以对多个时间序列进行编码。例如,需要通过股票过去的价格和交易量来预测股票趋势,我们有两个时间序列,一个是价格序列,一个是交易量序列:

序列1:1,2,3,4,3,2,1,2,3,4,3,2,1
序列2:10,20,30,40,30,20,10,20,30,40,30,20,10

这时需要把序列2的数据加入到序列1中,同样用输入窗口大小为3,输出窗口大小为1为例,训练集:

[1,10,2,20,3,30] --> [4]
[2,20,3,30,4,40] --> [3]
[3,30,4,40,3,30] --> [2]
[4,40,3,30,2,20] --> [1]

其中序列1用来预测它自己,而序列2作为辅助信息。类似的可以用到多个序列数据的预测上,而且要预测的列可以不在输入信息流中,比方说可以用IBM和苹果的股票价格来预测微软的股票价格,此时微软的股票价格不出现在输入信息中。

滑动窗口机制有点像卷积操作,所以也有人称滑动窗口为1维卷积。在自然语言处理中,滑动窗口等同于Ngram。例如,在词性标注的任务中,输入窗口为上下文的词,输出窗口输出的是输入窗口最右侧一个词的词性,每次向前滑动一个窗口,直到句子结束。对于文本的向量化表示,可以使用one-hot编码,也可以使用词嵌入,相比来说词嵌入是更稠密的表示,训练过程中可以减少神经网络中参数的数量,使得训练更快。

滑动窗口机制虽然可以用来对序列数据进行编码,但是它把序列问题处理成一对一的映射问题,即输入串到输出串的映射,而且两个串的大小都是固定的。很多任务中,我们需要比一对一映射更复杂的表示,例如在情感分析中,我们需要输入一整句话来判断情感极性,而且每个实例中句子长度不确定;或者要使用更复杂的输入——用一张图片来生成一个句子,用来描述这个图片。这样的任务中没有输入到输出的特定映射关系,而是需要神经网络对输入串有记忆功能:在读取输入的过程中,记住输入的关键信息。这时我们需要一种神经网络可以保存记忆,就是有状态的网络。下面我们来介绍有状态的神经网络。

2.循环神经网络

循环神经网络(Recurrent Neural Network,RNN)是从20世纪80年代慢慢发展起来的,与CNN对比,RNN内部有循环结构,这也是名字的由来。需要注意的是,RNN这个简称有时候也会被用来指递归神经网络(Recursive Neural Network),但是这是两种不同的网络结构,递归神经网络是深的树状结构,而循环神经网络是链状结构,要注意区分。提到RNN大多指的是循环神经网络。RNNs即基于循环神经网络变形的总称。

RNN可以解决变长序列问题,通过分析时间序列数据达到“预测未来”的本领,比如要说的下一个词、汽车的运行轨迹、钢琴弹奏的下一个音符等。RNN可以工作在任意长度的序列数据上,使得其在NLP上运用十分广泛:自动翻译、语音识别、情感分析和人机对话等。

循环神经网络里有重复神经网络基本模型的链式形式,在标准的RNN中,神经网络的基本模型仅仅包含了一个简单的网络层,比如一个双极性的Tanh层,如下图所示。

17634123-f247e602ea168772.png

标准RNN的前向传播公式如下:
S t = t a n h ( W [ S t − 1 , X t ] + b ) S_t = tanh(W [S_{t-1}, X_t] + b) St=tanh(W[St1,Xt]+b)

RNN中存在循环结构,指的是神经元的输入是该层神经元的输出。如下图所示。左边是RNN的结构图,右边是RNN结构按时刻展开。时刻是RNN中非常重要的概念,不同时刻记忆在隐藏单元中存储和流动,每一个时刻的隐含层单元有一个输出。在RNN中,各隐含层共享参数W,U,V。

image.png

记忆在隐藏单元中存储和流动,而输入取自于隐藏单元以及网络的最终输出。展开后可以看出网络输出串的整个过程,即图中的输出为 o t − 1 o_{t-1} ot1 o t o_{t} ot o t + 1 o_{t+1} ot+1,展开后的神经网络每一层负责一个输出。 x t x_t xt t t t时刻的输入,可以是当前词的一个one-hot向量, s t s_{t} st t t t时刻的隐藏层状态,是网络的记忆单元, s t s_t st基于前面时刻的隐藏层状态和输入信息计算得出,激活函数可以是Tanh或者ReLU,对于网络 t 0 t_0 t0时刻的神经网络状态可以简单地初始化成0; o t o_t ot t t t时刻神经网络的输出,从神经网络中产生每个时刻的输出信息,例如,在文本处理中,输出可能是词汇的概率向量,通过Softmax得出。

根据输入输出的不同,RNN可以按以下情况分类:

  • 1-N:一个输入,多个输出。例如:图片描述,输入是一个图片,输出是一个句子;音乐生成,输入一个数值,代表一个音符或者一个音乐风格,神经网络自动生成一段旋律;还有句子生成等。
  • N-1:多个输入,一个输出。大多根据输出的串做预测和分类,比如语义分类、情感分析、天气预报、股市预测、商品推荐、DNA序列分类、异常检测等。
  • N-N:多个输入,多个输出,而且输入和输出长度相等。比如命名实体识别,词性标注等输入和输出的长度一样。
  • N-M:一般情况下N≠M,例如机器翻译、文本摘要等,输入输出长度是不一样的。

根据不同的任务,循环神经网络会有不同的结构。如下图所示。

image.png

根据传播方向的不同,还有双向RNN,上面讲到的网络结构都是通过当前时刻和过去时刻产生输出,但是有些任务比如语音识别,需要通过后面的信息判断前面的输出状态。双向循环神经网络就是为了这种需求提出的,它允许 t t t时刻到 t − 1 t-1 t1时刻有链接,从而能够使网络根据未来的状态调整当前的状态。这在实际应用中有很好的例子:语音识别输入的时候,会先输出一个认为不错的序列,但是说完以后会根据后面的输入调整已经出现的输出。双向RNN的结构如下图所示。

image.png

RNN的训练时按时刻展开循环神经网络进行反向传播,反向传播算法的目的是找出在所有网络参数下的损失梯度。因为RNN的参数在所有时刻都是共享的,每一次反向传播不仅依赖当前时刻的计算结果,而且依赖之前的时刻,按时刻对神经网络展开,并执行反向传播,这个过程叫做Back Propagation Through Time(BPTT),是反向传播的扩展。和传统的神经网络一样,在时间序列上展开并前向传播计算出输出,利用所有时刻的输出计算损失 y 0 , y 1 , . . . , y t − 1 , y t y_0,y_1,...,y_{t-1},y_t y0,y1,...,yt1,yt,模型参数通过BPTT算法更新。梯度的反向传递依赖的是损失函数中用到的所有输出,并不是最后时刻的输出。比如损失函数用到了 y 2 , y 3 , y 4 y_2,y_3,y_4 y2,y3,y4,所以梯度传递的时候使用这三个输出,而不使用 y 0 , y 1 y_0,y_1 y0,y1,在所有时刻 W W W b b b都是共享的,所有反向传播才能在所有的时刻上正确计算。

由于存在梯度消失(大多时候)和梯度爆炸(极少,但对优化过程影响极大)的原因,导致RNN的训练很难获取到长时依赖信息。有时句子中对一个词的预测只需要考虑附近的词,而不用考虑很远的开头的地方,比如说在语言模型的任务中,试图根据已有的序列预测相应的单词:要预测“the clouds are in the sky”中最后一个单词“sky”,不需要更多的上下文信息,只要“the clouds are in the”就足够预测出下一个单词就是“sky”了,这种目标词与相关信息很近的情况,RNN是可以通过学习获得的。但是也有一些单词的预测需要更“远”处的上下文信息,比如说“I grew up in France… I speak fluent Frence.”要预测最后一个单词“French”,最近的信息“speak fluent”只能获得一种语言的结果,但是具体是哪一种语言就需要句子其他的上下文了,就是包括“France”的那段,也就是预测目标词依赖的上下文可能会间隔很远。

不幸的是,随着这种间隔的拉长,因为存在梯度消失或爆炸的问题——梯度消失使得我们在优化过程中不知道梯度方向,梯度爆炸会使得学习变得不稳定——RNNs学习这些链接信息会变得很困难。循环网络需要在很长时间序列的各个时刻重复相同的操作来完成深层的计算图,模型中的参数是共享的,导致训练中的误差在网络层上的传递不断累积,最终使得长期依赖的问题变得更加突出,使得深度神经网络丧失了学习先前信息的能力。

上面是标准RNN的概念和分类,针对RNN还有很多更有效的扩展,应用广泛的也是在其基础上发展起来的网络,下面看一下基于RNN的一些扩展。

3.LSTM和GRU

为了解决长期依赖的问题,对RNN进行改进提出了LSTM(Long Short-Term Memory,长的短期记忆网络),从字面意思上看它是短期的记忆,只是比较长的短期记忆,我们需要上下文的依赖信息,但是不希望这些依赖信息过长,所以叫长的短期记忆网络。

LSTM通过设计门限结构解决长期依赖问题,在标准RNN的基础上增加了四个神经网络层,使得LSTM网络包括四个输入:当前时刻的输入信息、遗忘门、输入门、输出门和一个输出(当前时刻网络的输出)。各个门上的激活函数使用Sigmoid函数,其输出在0~1之间,可以定义各个门是否被打开或打开的程度,赋予了它去除或添加信息的能力。

下图是LSTM的结构示意图。从图中可以看出有3个Sigmoid层,从左到右分别是遗忘门(Forget Gate)、输入门(Input Gate)和输出门(Output Gate)。三个Sigmoid层的输入都是当前时刻的输入 x t x_t xt和上一时刻的输出 h t − 1 h_{t-1} ht1,在LSTM前向传播的过程中,针对不同的输入表现不同的角色。下面根据不同的门限和相应的计算公式详细说明一下LSTM的工作原理。

image.png

(1)遗忘门:也称保持门(Keep Gate),这是从对立面说的。遗忘门控制记忆单元里哪些信息舍去(也就是被遗忘),哪些信息被保留。这些状态是神经网络通过数据学习得到的。遗忘门的Sigmoid层输出0~1,这个输出作用于 t − 1 t-1 t1时刻的记忆单元,0表示将过去的记忆完全遗忘,1表示将过去的信息完全保留。遗忘门在整个结构中的位置和前向传播的公式如下所示:

image.png

(2)输入门:也叫更新门(Update Gate)或写入门(Write Gate)。总之,输入门决定更新记忆单元的信息,包括两个部分:一个是Sigmoid层,一个是Tanh层;Tanh层的输入和Sigmoid一样都是当前时刻的输入 x t x_t xt和上一时刻的输出 h t − 1 h_{t-1} ht1,Tanh层从新的输入和网络原有的记忆信息决定要被写入新的神经网络状态中的候选值,而Sigmoid层决定这些候选值有多少被实际写入,要写入的记忆单元信息只有输入门打开才能真正地把值写入,其状态也是神经网络自己学习到的。输入门在整个结构中的位置和前向传播公式如下所示:

image.png

目前为止已经有了遗忘门和输入门,下一步就可以更新神经元状态,也就是神经网络记忆单元的值了。前面两个步骤已经准备好了要更新的值,下面就是怎么更新了。从公式看,当前时刻的神经元状态 C t C_t Ct是两部分的和:一部分是计算通过遗忘门后剩余的信息,即上一时刻的神经元状态 C t − 1 C_{t-1} Ct1 f t f_t ft的乘积;另一部分是从输入中获取的新信息,即 i t i_t it C t ~ \tilde{C_t} Ct~的乘积,得出实际要输出到神经元状态的信息。其中 C t ~ \tilde{C_t} Ct~ t t t时刻新的输入 x t x_t xt和上一时刻神经网络隐含层输出 h t − 1 h_{t-1} ht1总和后的候选值,如下图所示。

image.png

(3)输出门:输出门的功能是读取刚更新过的神经网络状态,也就是记忆单元进行输出,但是具体哪些信息可以输出同样受输出门 o t o_t ot的控制, o t o_t ot通过Sigmoid层实现,产生范围(0,1)之间的值。网络隐含层状态 C t C_t Ct通过一个Tanh层,对记忆单元中的信息产生候选输出,范围是(-1,1),然后与输出门 o t o_t ot相乘得出实际要输出的值 h t h_t ht。输出门在整个结构中的位置和前向传播公式如下图所示。

image.png

LSTM由于有效地解决了标准RNN的长期依赖问题,所以应用很广泛,目前我们所说的RNNs大多都是指的LSTM或者基于LSTM的变体。

从上面看LSTM有复杂的结构和前向传播公式,不过在实际应用中PyTorch有LSTM的封装,程序中使用的时候只需要给定需要的参数就可以了。PyTorch中LSTM的定义:

torch.nn.LSTM(*args, **kwargs)

可接受的参数如下:

  • input_size:输入信息的特征数
  • hidden_size:隐含层状态h的特征数
  • num_layers:循环层数
  • bias:默认为True;如果设置成False,不使用偏置项b_ih和b_hh。
  • batch_first:如果设置成True,输入和输出的Tensor应该为(batch,seq,feature)的顺序。
  • dropout:如果非零,在除输出层外的其他网络层添加Dropout层。
  • bidirectional:如果设置成True,变成双向的LSTM,默认为False。

下面的程序片段是一个简单的:LSTM的例子,定义LSTM的网络结构,输入大小为10,隐含层为20,2个循环层(注意不是时序展开的层),输入的信息是input,隐含层状态为h,记忆单元状态为e,输出是最后一层的输出层特征的Tensor,隐含层状态:

rnn = nn.LSTM(10,20,2)
input = Variable(torch.randn(5,3,10))
h0 = Variable(torch.randn(3,20))
c0 = Variable(torch.randn(3,20))
output,hn = rnn(input,(h0,c0))
  • 1
  • 2
  • 3
  • 4
  • 5

PyTorch中还有一个LSTMCell定义如下,参数含义和LSTM一样:

class torch.nn.LSTMCell(input_size,hidden_size,bias=True)

LSTM的实现内部调用了LSTMCell。LSTMCell是LSTM的内部执行一个时序步骤,从例子可以看出:

rnn = LSTMCell(10,20)
input = Variable(torch.randn(6,3,10))
hx = Variable(torch.randn(3,20))
cx = Variable(torch.randn(3,20))
output = []
for i in range(6):
    hx,cx = rnn(input[i],(hx,cx))
    output.append(hx)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

LSTM的变体有很多,一个很有名的变体是GRU(Gated Recurrent Unit),它在保证LSTM效果的情况下,将遗忘门和输入门整合成一个更新门,同样还将单元状态和隐藏状态合并,并做出一些其他改变。因为GRU比标准的LSTM少了一个门限层,使得其训练速度更快,更方便构建更复杂的网络。GRU的结构图和前向计算公式如下图所示:

image.png

PyTorch中GRU的定义:

class torch.nn.GRU(*args, **kwargs)

GRU的简单示例:

rnn = nn.GRU(10,20,2)
input = Variable(torch.randn(5,3,10))
h0 = Variable(torch.randn(2,3,20))
output,hn = rnn(input,h0)
  • 1
  • 2
  • 3
  • 4

4.LSTM在自然语言处理中的应用

上面介绍了LSTM的由来和各个部分的功能,因为擅长处理序列数据,并能够解决训练中长依赖问题,LSTM在NLP中有着广泛的应用。下面介绍LSTM在NLP中的一些常见应用场景。

1.词性标注

词性标注(Past-of-Speach Tagging,POS Tagging)是自然语言处理中最基本的任务,对给定的句子做每个词的词性标识,是作为其他NLP任务的基础。这里介绍在PyTorch中使用LSTM进行POS Tagging任务。

把输入句子表示成 w 1 , w 2 , . . . , w m w_1,w_2,...,w_m w1,w2,...,wm,其中 w i ∈ V w_i \in V wiV V V V是词汇表, T T T为所有词性标签集合,用 y i y_i yi w i w_i wi表示的词性,我们要预测的是 w i w_i wi的词性 y i ^ \hat{y_i} yi^。模型的输出是 y 1 ^ , y 2 ^ , . . . , y M ^ \hat{y_1},\hat{y_2},...,\hat{y_M} y1^,y2^,...,yM^,其中 y i ^ ∈ T \hat{y_i} \in T yi^T。把句子传入LSTM做预测, i i i时刻的隐含层状态用 h i h_i hi表示。每个词性有唯一的编号,预测 y i ^ \hat{y_i} yi^的前向传播公式:

y i ^ = a r g m a x j ( l o g S o f t m a x ( A h i + b ) ) j \hat{y_i} = argmax_j(log Softmax(Ah_i+b))_j yi^=argmaxj(logSoftmax(Ahi+b))j

在隐含层状态上作用一个仿射函数log Softmax,最终的词性预测结果是输出向量中最大的值,目标空间A的大小为 ∣ T ∣ |T| T

数据的准备过程:

# 输入数据封装成Variable
def prepare_sequence(seq,to_idx):
    idxs = [to_idx[w] for w in seq]
    tensor = torch.LongTensor(idxs)
    return autograd.Variable(tensor)

# 输入数据格式,单个的词和对应的词性
training_data = [("The dog ate the appple".split(),["DET","NN","V","DET","NN"]),
                 ("Everybody read that book".split(),["NN","V","DET","NN"])]

word_to_idx = {}
for sent,tags in training_data:
    for word in sent:
        if word not in word_to_idx:
            word_to_idx[word] = len(word_to_idx)
print(word_to_idx)

# 词性编码
tag_to_idx = {"DET":0,"NN":1,"V":2}

# 一般使用32或者64维,这里为了便于观察程序运行中权重的变化,使用小的维度
EMBEDDING_DIM = 6
HIDDEN_DIM = 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

模型的定义:

class LSTMTagger(nn.Module):
    
    def __init__(self,embedding_dim,hidden_dim,vocab_size,tagset_size):
        super(LSTMTagger,self).__init__()
        self.hidden_dim = hidden_dim
        # 词嵌入,给定词表大小和期望的输出维度
        self.word_embeddings = nn.Embedding(vocab_size,embedding_dim)
        # 使用词嵌入作为输入,输出为隐含层状态,大小为hidden_dim
        self.lstm = nn.LSTM(embedding_dim,hidden_dim)
        # 线性层把隐含层状态空间映射到词性空间
        self.hidden2tag = nn.Linear(hidden_dim,tagset_size)
        self.hidden = self.init_hidden()
    
    # 初始化隐含层状态
    def init_hidden(self):
        return (autograd.Variable(torch.zeros(1,1,self.hidden_size)),
               autograd.Variable(torch.zeros(1,1,self.hidden_size)))
    
    # 前向传播
    def forward(self,sentence):
        embeds = self.word_embeddings(sentence)
        lstm_out,self.hidden = self.lstm(embeds.view(len(sentence),1,-1),self.hidden)
        tag_space = self.hidden2tag(lstm_out.view(len(sentence),-1))
        tag_scores = F.log_softmax(tag_space,dim=1)
        return tag_scores
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

具体的训练过程可以参考PyTorch官网教程,这里特别要指出的是,这里的POS Tagging任务使用的损失函数是负对数似然函数,优化器使用SGD,学习率为0.1:

loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(),lr=0.1)
  • 1
  • 2
2.情感分析

本小节介绍一下LSTM在NLP中另外一个领域的应用:情感分析。Bjarke Felbo在论文中提到了一个情感分析的任务Deepmoji,利用表情符号训练了12亿条推文,用以了解语言是如何表达情感。通过神经网络的学习,模型可以在许多情感相关的文本建模任务中获得最先进的性能。TorchMoji是论文中提出的情感分析的PyTorch实现。模型包含两个双LSTM层,在LSTM后面链接一个Attention层分类器,模型的结构如下图所示。

image.png

Deepmoji可以对输入的句子进行情感方面的分析并生成相应的moji表情,如下图所示。比如输入“What is happening to me ??”和“What a good day !”会输出不同的表情,并给出输出的置信度。具体代码详见GitHub

image.png

image.png

5.序列到序列网络

1.序列到序列原理

序列到序列网络(Seq2seqNetwork),也称为编码解码网络(Encoder Decoder Netword),由两个独立的循环神经网络组成,被称为编码器(Encoder)和解码器(Decoder),通常使用LSTM或者GRU来实现。编码器处理输入数据,其目标是理解输入信息并表示在编码器的最终状态中。解码器从编码器的最终状态开始,逐词生成目标输出的序列,解码器在每个时刻的输入为上一时刻的输出,整体过程如下图所示。

v2-22f16e24ee216751b7a8201e0db7a811_hd.jpg

串到串最常见的场景就是机器翻译,把输入串分词并表示成词向量,每个时刻一个词语输入到编码网络中,并利用EOS(End of Sentence)作为句子末尾的标记。句子输入完成我们得到一个编码器,这时可以用编码器的隐含层状态来初始化解码器,输入到解码器的第一个词是SOS(Start of Sentence),作为目标语言的起始标识,得到的输出是目标语言的第一个词,随后将该时刻的输出作为解码器下一时刻的输入。重复这个过程直到解码器的输出产生一个EOS,目标语言结束的标识,这时就完成了从源语言到目标语言的翻译。后面有具体的例子。

2.注意力机制

从人工翻译句子的经验中可以得到很多启发,从而改善我们提到的串到串模型。人工翻译句子的时候,首先阅读整个句子理解要表达的意思,然后开始写出相应的翻译。但是一个很重要的方面就是在你写新的句子的时候,通常会重新回到源语言的文本,特别注意你目前正在翻译的那部分在源语言中的表达,以确定最好的翻译结果。而我们前面提到的串到串的模型中,编码器一次读入所有的输入并总结到句子的意思保存到编码器的隐含层状态,这个过程像人工翻译的第一部分,而通过解码器得到最终的翻译结果,解码器处理的是翻译的第二个部分。但是“特别注意”的部分在我们的串到串模型中还没有体现,这也是需要完成的部分。

为了在串到串模型中添加注意力机制,解码器在产生 t t t时刻的输出时,让解码器访问所有从编码器的输出,这样解码器可以观察源语言的句子,这个过程是之前没有的。但是在所有时间步都考虑编码器的所有输出,这和人工翻译的过程还是不同的,人工翻译对于不同的部分,需要关注源语言中特定的很小的部分。所以,直接让解码器访问所有编码器的输出是不符合实际的。我们需要对这个过程进行改进,让解码器工作的时候可以动态地注意编码器输出的特定的部分。有研究者提出的解决方案是把输入变成是串联操作,在编码器的输出上使用一个带权重,也就是编码器在 t − 1 t-1 t1时刻的状态,而不是直接使用其输出。具体做法是,首先为编码器的每个输出关联一个分数,这个分数由解码器 t − 1 t-1 t1时刻的网络状态和每个编码器的输出的点乘得到,然后用Softmax层对这些分数进行归一化。最后在加入到串联操作之前,利用归一化后的分数分别度量编码器的输出。这个策略的关键点是,编码器的每个输出计算得到的关联分数,表示了每个编码器的输出对解码器 t t t时刻决策的重要程度。

注意力机制提出后受到了广泛关注,并在语音识别、图像描述等应用上有很好的效果。

6.PyTorch示例:基于GRU和Attention的机器翻译

完整代码详见GitHub

1.公共模块(logger.py)

这里提到的公共模块主要是日志处理模块。在数据处理、模型训练等过程中,需要保留必要的日志信息,这样可以对程序的运行过程、运行结果进行记录和分析。这里记录日志的方式是同时输出到文件和控制台。

import logging as logger
logger.basicConfig(level=logger.DEBUG,
                   format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                   datefmt='%Y-%m-%d %H:%M:%S -',
                   filename='log.txt',
                   filemode='a')  # or 'w', default 'a'
console = logger.StreamHandler()
console.setLevel(logger.INFO)
formatter = logger.Formatter('%(asctime)s %(name)-6s: %(levelname)-6s %(message)s')
console.setFormatter(formatter)
logger.getLogger('').addHandler(console)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
2.数据处理模块(process.py)

数据处理模块主要定义模型训练需要的一些数据处理,包括从文件加载数据,数据解析,和一些辅助函数。

from __future__ import unicode_literals, print_function, division
import math
import re
import time
import jieba
import torch
import unicodedata
from torch.autograd import Variable
from logger import logger

use_cuda = torch.cuda.is_available()
SOS_token = 0
EOS_token = 1
# 中文的时候要设置大一些
MAX_LENGTH = 25


def unicodeToAscii(s):
    '''
    Unicode转换成ASCII,http://stackoverflow.com/a/518232/2809427
    :param s:
    :return:
    '''
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )


def normalizeString(s):
    '''
    转小写,去除非法字符
    :param s:
    :return:
    '''
    s = unicodeToAscii(s.lower().strip())
    s = re.sub(r"([.!?])", r" \1", s)
    # 中文不能进行下面的处理
    # s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s


class Lang:
    def __init__(self, name):
        '''
        添加 need_cut 可根据语种进行不同的分词逻辑处理
        :param name: 语种名称
        '''
        self.name = name
        self.need_cut = self.name == 'cmn'
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}
        self.n_words = 2  # 初始化词数为2:SOS & EOS

    def addSentence(self, sentence):
        '''
        从语料中添加句子到 Lang
        :param sentence: 语料中的每个句子
        '''
        if self.need_cut:
            sentence = cut(sentence)
        for word in sentence.split(' '):
            if len(word) > 0:
                self.addWord(word)

    def addWord(self, word):
        '''
        向 Lang 中添加每个词,并统计词频,如果是新词修改词表大小
        :param word:
        '''
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.word2count[word] = 1
            self.index2word[self.n_words] = word
            self.n_words += 1
        else:
            self.word2count[word] += 1


def cut(sentence, use_jieba=False):
    '''
    对句子分词。
    :param sentence: 要分词的句子
    :param use_jieba: 是否使用 jieba 进行智能分词,默认按单字切分
    :return: 分词结果,空格区分
    '''
    if use_jieba:
        return ' '.join(jieba.cut(sentence))
    else:
        words = [word for word in sentence]
        return ' '.join(words)


import jieba.posseg as pseg


def tag(sentence):
    words = pseg.cut(sentence)
    result = ''
    for w in words:
        result = result + w.word + "/" + w.flag + " "
    return result


def readLangs(lang1, lang2, reverse=False):
    '''

    :param lang1: 源语言
    :param lang2: 目标语言
    :param reverse: 是否逆向翻译
    :return: 源语言实例,目标语言实例,词语对
    '''
    logger.info("Reading lines...")

    # 读取txt文件并分割成行
    lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8'). \
        read().strip().split('\n')

    # 按行处理成 源语言-目标语言对,并做预处理
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]

    # Reverse pairs, make Lang instances
    if reverse:
        pairs = [list(reversed(p)) for p in pairs]
        input_lang = Lang(lang2)
        output_lang = Lang(lang1)
    else:
        input_lang = Lang(lang1)
        output_lang = Lang(lang2)

    return input_lang, output_lang, pairs


eng_prefixes = (
    "i am ", "i m ",
    "he is", "he s ",
    "she is", "she s",
    "you are", "you re ",
    "we are", "we re ",
    "they are", "they re "
)


def filterPair(p):
    '''
    按自定义最大长度过滤
    '''
    return len(p[0].split(' ')) < MAX_LENGTH and \
           len(p[1].split(' ')) < MAX_LENGTH and \
           p[1].startswith(eng_prefixes)


def filterPairs(pairs):
    return [pair for pair in pairs if filterPair(pair)]


def prepareData(lang1, lang2, reverse=False):
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
    logger.info("Read %s sentence pairs" % len(pairs))
    pairs = filterPairs(pairs)
    logger.info("Trimmed to %s sentence pairs" % len(pairs))
    logger.info("Counting words...")
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
    logger.info("Counted words:")
    logger.info('%s, %d' % (input_lang.name, input_lang.n_words))
    logger.info('%s, %d' % (output_lang.name, output_lang.n_words))
    return input_lang, output_lang, pairs


def indexesFromSentence(lang, sentence):
    '''
    :param lang:
    :param sentence:
    :return:
    '''
    return [lang.word2index[word] for word in sentence.split(' ') if len(word) > 0]


def variableFromSentence(lang, sentence):
    if lang.need_cut:
        sentence = cut(sentence)
    # logger.info("cuted sentence: %s" % sentence)
    indexes = indexesFromSentence(lang, sentence)
    indexes.append(EOS_token)
    result = Variable(torch.LongTensor(indexes).view(-1, 1))
    if use_cuda:
        return result.cuda()
    else:
        return result


def variablesFromPair(input_lang, output_lang, pair):
    input_variable = variableFromSentence(input_lang, pair[0])
    target_variable = variableFromSentence(output_lang, pair[1])
    return (input_variable, target_variable)


def asMinutes(s):
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)


def timeSince(since, percent):
    now = time.time()
    s = now - since
    es = s / (percent)
    rs = es - s
    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))


if __name__ == "__main__":
    s = 'Fans of Belgium cheer prior to the 2018 FIFA World Cup Group G match between Belgium and Tunisia in Moscow, Russia, June 23, 2018.'
    s = '结婚的和尚未结婚的和尚'
    s = "买张下周三去南海的飞机票,海航的"
    s = "过几天天天天气不好。"

    a = cut(s, use_jieba=True)
    print(a)
    print(tag(s))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
3.模型定义(model.py)

这部分主要是循环神经网络RNN的定义,包括编码器和解码器两个RNN。

import torch
from torch import nn
from torch.autograd import Variable
from torch.nn import functional as F
from logger import logger
# from process import cut
from process import MAX_LENGTH

use_cuda = torch.cuda.is_available()


class EncoderRNN(nn.Module):
    '''
    编码器的定义
    '''

    def __init__(self, input_size, hidden_size, n_layers=1):
        '''
        初始化过程
        :param input_size: 输入向量长度,这里是词汇表大小
        :param hidden_size: 隐藏层大小
        :param n_layers: 叠加层数
        '''
        super(EncoderRNN, self).__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        '''
        前向计算过程
        :param input: 输入
        :param hidden: 隐藏层状态
        :return: 编码器输出,隐藏层状态
        '''
        try:
            embedded = self.embedding(input).view(1, 1, -1)
            output = embedded
            for i in range(self.n_layers):
                output, hidden = self.gru(output, hidden)
            return output, hidden
        except Exception as err:
            logger.error(err)

    def initHidden(self):
        '''
        隐藏层状态初始化
        :return: 初始化过的隐藏层状态
        '''
        result = Variable(torch.zeros(1, 1, self.hidden_size))
        if use_cuda:
            return result.cuda()
        else:
            return result


class DecoderRNN(nn.Module):
    '''
    解码器定义
    '''

    def __init__(self, hidden_size, output_size, n_layers=1):
        '''
        初始化过程
        :param hidden_size: 隐藏层大小
        :param output_size: 输出大小
        :param n_layers: 叠加层数
        '''
        super(DecoderRNN, self).__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax()

    def forward(self, input, hidden):
        '''
        前向计算过程
        :param input: 输入信息
        :param hidden: 隐藏层状态
        :return: 解码器输出,隐藏层状态
        '''
        try:
            output = self.embedding(input).view(1, 1, -1)
            for i in range(self.n_layers):
                output = F.relu(output)
                output, hidden = self.gru(output, hidden)
            output = self.softmax(self.out(output[0]))
            return output, hidden
        except Exception as err:
            logger.error(err)

    def initHidden(self):
        '''
        隐藏层状态初始化
        :return: 初始化过的隐藏层状态
        '''
        result = Variable(torch.zeros(1, 1, self.hidden_size))
        if use_cuda:
            return result.cuda()
        else:
            return result


class AttnDecoderRNN(nn.Module):
    '''
    带注意力的解码器的定义
    '''

    def __init__(self, hidden_size, output_size, n_layers=1, dropout_p=0.1, max_length=MAX_LENGTH):
        '''
        带注意力的解码器初始化过程
        :param hidden_size: 隐藏层大小
        :param output_size: 输出大小
        :param n_layers: 叠加层数
        :param dropout_p: dropout率定义
        :param max_length: 接受的最大句子长度
        '''
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.n_layers = n_layers
        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)

    def forward(self, input, hidden, encoder_output, encoder_outputs):
        '''
        前向计算过程
        :param input: 输入信息
        :param hidden: 隐藏层状态
        :param encoder_output: 编码器分时刻的输出
        :param encoder_outputs: 编码器全部输出
        :return: 解码器输出,隐藏层状态,注意力权重
        '''
        try:
            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),
                                     encoder_outputs.unsqueeze(0))

            output = torch.cat((embedded[0], attn_applied[0]), 1)
            output = self.attn_combine(output).unsqueeze(0)

            for i in range(self.n_layers):
                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
        except Exception as err:
            logger.error(err)

    def initHidden(self):
        '''
        隐藏层状态初始化
        :return: 初始化过的隐藏层状态
        '''
        result = Variable(torch.zeros(1, 1, self.hidden_size))
        if use_cuda:
            return result.cuda()
        else:
            return result
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
4.训练模块(train.py)

训练模块包括训练过程的定义和评估方法的定义。

import sys
import random
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from torch import nn
from torch import optim
from torch.autograd import Variable
from process import *

use_cuda = torch.cuda.is_available()


def evaluate(input_lang, output_lang, encoder, decoder, sentence, max_length=MAX_LENGTH):
    '''
    单句评估
    :param input_lang: 源语言信息
    :param output_lang: 目标语言信息
    :param encoder: 编码器
    :param decoder: 解码器
    :param sentence: 要评估的句子
    :param max_length: 可接受最大长度
    :return: 翻译过的句子和注意力信息
    '''
    # 输入句子预处理
    input_variable = variableFromSentence(input_lang, sentence)
    input_length = input_variable.size()[0]
    encoder_hidden = encoder.initHidden()

    encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
    encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs

    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(input_variable[ei],
                                                 encoder_hidden)
        encoder_outputs[ei] = encoder_outputs[ei] + encoder_output[0][0]

    decoder_input = Variable(torch.LongTensor([[SOS_token]]))  # 起始标志 SOS
    decoder_input = decoder_input.cuda() if use_cuda else decoder_input

    decoder_hidden = encoder_hidden

    decoded_words = []
    decoder_attentions = torch.zeros(max_length, max_length)
    # 翻译过程
    for di in range(max_length):
        decoder_output, decoder_hidden, decoder_attention = decoder(
            decoder_input, decoder_hidden, encoder_output, encoder_outputs)
        decoder_attentions[di] = decoder_attention.data
        topv, topi = decoder_output.data.topk(1)
        ni = topi[0][0].item()
        # 当前时刻输出为句子结束标志,则结束
        if ni == EOS_token:
            decoded_words.append('<EOS>')
            break
        else:
            decoded_words.append(output_lang.index2word[ni])

        decoder_input = Variable(torch.LongTensor([[ni]]))
        decoder_input = decoder_input.cuda() if use_cuda else decoder_input

    return decoded_words, decoder_attentions[:di + 1]


teacher_forcing_ratio = 0.5


def train(input_variable, target_variable, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion,
          max_length=MAX_LENGTH):
    '''
    单次训练过程,
    :param input_variable: 源语言信息
    :param target_variable: 目标语言信息
    :param encoder: 编码器
    :param decoder: 解码器
    :param encoder_optimizer: 编码器的优化器
    :param decoder_optimizer: 解码器的优化器
    :param criterion: 评价准则,即损失函数的定义
    :param max_length: 接受的单句最大长度
    :return: 本次训练的平均损失
    '''
    encoder_hidden = encoder.initHidden()

    # 清楚优化器状态
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    input_length = input_variable.size()[0]
    target_length = target_variable.size()[0]
    # print(input_length, " -> ", target_length)

    encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
    encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
    # print("encoder_outputs shape ", encoder_outputs.shape)
    loss = 0

    # 编码过程
    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(
            input_variable[ei], encoder_hidden)
        encoder_outputs[ei] = encoder_output[0][0]

    decoder_input = Variable(torch.LongTensor([[SOS_token]]))
    decoder_input = decoder_input.cuda() if use_cuda else decoder_input

    decoder_hidden = encoder_hidden

    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

    if use_teacher_forcing:
        # Teacher forcing: 以目标作为下一个输入
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_output, encoder_outputs)
            loss += criterion(decoder_output, target_variable[di])
            decoder_input = target_variable[di]  # Teacher forcing

    else:
        # Without teacher forcing: 网络自己预测的输出为下一个输入
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_output, encoder_outputs)
            topv, topi = decoder_output.data.topk(1)
            ni = topi[0][0]

            decoder_input = Variable(torch.LongTensor([[ni]]))
            decoder_input = decoder_input.cuda() if use_cuda else decoder_input

            loss += criterion(decoder_output, target_variable[di])
            if ni == EOS_token:
                break

    # 反向传播
    loss.backward()

    # 网络状态更新
    encoder_optimizer.step()
    decoder_optimizer.step()

    return loss / target_length


def showPlot(points):
    '''
    绘制图像
    :param points:
    :return:
    '''
    plt.figure()
    fig, ax = plt.subplots()
    # this locator puts ticks at regular intervals
    loc = ticker.MultipleLocator(base=0.2)
    ax.yaxis.set_major_locator(loc)
    plt.plot(points)


def trainIters(input_lang, output_lang, pairs, encoder, decoder, n_iters, print_every=1000, plot_every=100,
               learning_rate=0.01):
    '''
    训练过程,可以指定迭代次数,每次迭代调用 前面定义的train函数,并在迭代结束调用绘制图像的函数
    :param input_lang: 输入语言实例
    :param output_lang: 输出语言实例
    :param pairs: 语料中的源语言-目标语言对
    :param encoder: 编码器
    :param decoder: 解码器
    :param n_iters: 迭代次数
    :param print_every: 打印loss间隔
    :param plot_every: 绘制图像间隔
    :param learning_rate: 学习率
    :return:
    '''
    start = time.time()
    plot_losses = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total = 0  # Reset every plot_every

    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
    training_pairs = [variablesFromPair(input_lang, output_lang, random.choice(pairs))
                      for i in range(n_iters)]
    # 损失函数定义
    criterion = nn.NLLLoss()

    for iter in range(1, n_iters + 1):
        training_pair = training_pairs[iter - 1]
        input_variable = training_pair[0]
        target_variable = training_pair[1]

        loss = train(input_variable, target_variable, encoder,
                     decoder, encoder_optimizer, decoder_optimizer, criterion)
        print_loss_total += loss
        plot_loss_total += loss

        if iter % print_every == 0:
            print_loss_avg = print_loss_total / print_every
            print_loss_total = 0
            logger.info('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
                                               iter, iter / n_iters * 100, print_loss_avg))

        if iter % plot_every == 0:
            plot_loss_avg = plot_loss_total / plot_every
            plot_losses.append(plot_loss_avg)
            plot_loss_total = 0

    showPlot(plot_losses)


def evaluateRandomly(input_lang, output_lang, pairs, encoder, decoder, n=10):
    '''
    从语料中随机选取句子进行评估
    '''
    for i in range(n):
        pair = random.choice(pairs)
        logger.info('> %s' % pair[0])
        logger.info('= %s' % pair[1])
        output_words, attentions = evaluate(input_lang, output_lang, encoder, decoder, pair[0])
        output_sentence = ' '.join(output_words)
        logger.info('< %s' % output_sentence)
        logger.info('')


def showAttention(input_sentence, output_words, attentions):
    try:
        # 添加绘图中的中文显示
        plt.rcParams['font.sans-serif'] = ['STSong']  # 宋体
        plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
        # 使用 colorbar 初始化绘图
        fig = plt.figure()
        ax = fig.add_subplot(111)
        cax = ax.matshow(attentions.numpy(), cmap='bone')
        fig.colorbar(cax)

        # 设置x,y轴信息
        ax.set_xticklabels([''] + input_sentence.split(' ') +
                           ['<EOS>'], rotation=90)
        ax.set_yticklabels([''] + output_words)

        # 显示标签
        ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
        ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

        plt.show()
    except Exception as err:
        logger.error(err)


def evaluateAndShowAtten(input_lang, ouput_lang, input_sentence, encoder1, attn_decoder1):
    output_words, attentions = evaluate(input_lang, ouput_lang,
                                        encoder1, attn_decoder1, input_sentence)
    logger.info('input = %s' % input_sentence)
    logger.info('output = %s' % ' '.join(output_words))
    # 如果是中文需要分词
    if input_lang.name == 'cmn':
        print(input_lang.name)
        input_sentence = cut(input_sentence)
    showAttention(input_sentence, output_words, attentions)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
5.训练过程(seq2seq.py)

该模块主要是整个训练过程,调用已经定义好的训练方法,完成整个预料上的训练,并把相应模型保存到文件,以方便随时评估和模型调用,这样不用每次都重新执行训练过程(因为从下面给出的训练结果可以看出这个过程很漫长)。

import pickle
import sys
from io import open
from model import AttnDecoderRNN
from model import EncoderRNN
from train import *

use_cuda = torch.cuda.is_available()
logger.info("Use cuda:{}".format(use_cuda))
input = 'eng'
output = 'cmn'
# 从参数接收要翻译的语种名词
if len(sys.argv) > 1:
    output = sys.argv[1]
logger.info('%s -> %s' % (input, output))

# 处理语料库
input_lang, output_lang, pairs = prepareData(input, output, True)
logger.info(random.choice(pairs))

# 查看两种语言的词汇大小情况
logger.info('input_lang.n_words: %d' % input_lang.n_words)
logger.info('output_lang.n_words: %d' % output_lang.n_words)

# 保存处理过的语言信息,评估时加载使用
pickle.dump(input_lang, open('./data/%s_%s_input_lang.pkl' % (input, output), "wb"))
pickle.dump(output_lang, open('./data/%s_%s_output_lang.pkl' % (input, output), "wb"))
pickle.dump(pairs, open('./data/%s_%s_pairs.pkl' % (input, output), "wb"))
logger.info('lang saved.')

# 编码器和解码器的实例化
hidden_size = 256
encoder1 = EncoderRNN(input_lang.n_words, hidden_size)
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words,
                               1, dropout_p=0.1)
if use_cuda:
    encoder1 = encoder1.cuda()
    attn_decoder1 = attn_decoder1.cuda()

logger.info('train start. ')
# 训练过程,指定迭代次数,此处为迭代100000次,每1000次打印中间信息
trainIters(input_lang, output_lang, pairs, encoder1, attn_decoder1, 100000, print_every=1000)
logger.info('train end. ')

# 保存编码器和解码器网络状态
torch.save(encoder1.state_dict(), open('./data/%s_%s_encoder1.stat' % (input, output), 'wb'))
torch.save(attn_decoder1.state_dict(), open('./data/%s_%s_attn_decoder1.stat' % (input, output), 'wb'))
logger.info('stat saved.')

# 保存整个网络
torch.save(encoder1, open('./data/%s_%s_encoder1.model' % (input, output), 'wb'))
torch.save(attn_decoder1, open('./data/%s_%s_attn_decoder1.model' % (input, output), 'wb'))
logger.info('model saved.')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

训练结果如下:

C:\ProgramData\Anaconda3\python.exe E:/workspace/python/chapter7/seq2seq.py
2019-09-01 23:18:50,189 root  : INFO   Use cuda:True
2019-09-01 23:18:50,190 root  : INFO   eng -> cmn
2019-09-01 23:18:50,190 root  : INFO   Reading lines...
2019-09-01 23:18:50,470 root  : INFO   Read 19578 sentence pairs
2019-09-01 23:18:50,487 root  : INFO   Trimmed to 695 sentence pairs
2019-09-01 23:18:50,487 root  : INFO   Counting words...
2019-09-01 23:18:50,492 root  : INFO   Counted words:
2019-09-01 23:18:50,492 root  : INFO   cmn, 994
2019-09-01 23:18:50,492 root  : INFO   eng, 887
2019-09-01 23:18:50,492 root  : INFO   ['他在生你的氣。', 'he is angry with you .']
2019-09-01 23:18:50,492 root  : INFO   input_lang.n_words: 994
2019-09-01 23:18:50,492 root  : INFO   output_lang.n_words: 887
2019-09-01 23:18:50,494 root  : INFO   lang saved.
2019-09-01 23:18:53,528 root  : INFO   train start. 
2019-09-01 23:19:59,536 root  : INFO   1m 6s (- 108m 54s) (1000 1%) 3.4915
2019-09-01 23:20:49,542 root  : INFO   1m 56s (- 94m 44s) (2000 2%) 3.1642
2019-09-01 23:21:40,365 root  : INFO   2m 46s (- 89m 54s) (3000 3%) 2.8599
2019-09-01 23:22:31,133 root  : INFO   3m 37s (- 87m 2s) (4000 4%) 2.5942
2019-09-01 23:23:22,415 root  : INFO   4m 28s (- 85m 8s) (5000 5%) 2.2696
2019-09-01 23:24:13,565 root  : INFO   5m 20s (- 83m 33s) (6000 6%) 1.9124
2019-09-01 23:25:05,176 root  : INFO   6m 11s (- 82m 17s) (7000 7%) 1.5661
2019-09-01 23:25:57,465 root  : INFO   7m 3s (- 81m 15s) (8000 8%) 1.2604
2019-09-01 23:26:49,536 root  : INFO   7m 56s (- 80m 12s) (9000 9%) 0.9532
2019-09-01 23:27:41,903 root  : INFO   8m 48s (- 79m 15s) (10000 10%) 0.7092
……
2019-09-02 00:39:19,369 root  : INFO   80m 25s (- 7m 57s) (91000 91%) 0.0139
2019-09-02 00:40:12,250 root  : INFO   81m 18s (- 7m 4s) (92000 92%) 0.0123
2019-09-02 00:41:04,909 root  : INFO   82m 11s (- 6m 11s) (93000 93%) 0.0126
2019-09-02 00:41:57,523 root  : INFO   83m 3s (- 5m 18s) (94000 94%) 0.0113
2019-09-02 00:42:50,670 root  : INFO   83m 57s (- 4m 25s) (95000 95%) 0.0082
2019-09-02 00:43:43,522 root  : INFO   84m 49s (- 3m 32s) (96000 96%) 0.0123
2019-09-02 00:44:35,892 root  : INFO   85m 42s (- 2m 39s) (97000 97%) 0.0088
2019-09-02 00:45:28,415 root  : INFO   86m 34s (- 1m 46s) (98000 98%) 0.0103
2019-09-02 00:46:20,990 root  : INFO   87m 27s (- 0m 53s) (99000 99%) 0.0105
2019-09-02 00:47:13,401 root  : INFO   88m 19s (- 0m 0s) (100000 100%) 0.0102
2019-09-02 00:47:13,813 root  : INFO   train end. 
2019-09-02 00:47:13,823 root  : INFO   stat saved.
2019-09-02 00:47:13,859 root  : INFO   model saved.

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
6.评估过程(evaluate_eng_cmn.py)

对训练好的神经网络进行评估,可以从语料中随机选取句子进行翻译,也可以指定句子进行翻译,并对翻译过程中的注意力进行可视化。

import pickle
import matplotlib.pyplot as plt
import torch
from logger import logger
from train import evaluate
from train import evaluateAndShowAtten
from train import evaluateRandomly

input = 'eng'
output = 'cmn'
logger.info('%s -> %s' % (input, output))
# 加载处理好的语言信息
input_lang = pickle.load(open('./data/%s_%s_input_lang.pkl' % (input, output), "rb"))
output_lang = pickle.load(open('./data/%s_%s_output_lang.pkl' % (input, output), "rb"))
pairs = pickle.load(open('./data/%s_%s_pairs.pkl' % (input, output), 'rb'))
logger.info('lang loaded.')

# 加载训练好的编码器和解码器
encoder1 = torch.load(open('./data/%s_%s_encoder1.model' % (input, output), 'rb'))
attn_decoder1 = torch.load(open('./data/%s_%s_attn_decoder1.model' % (input, output), 'rb'))
logger.info('model loaded.')


# 对单句进行评估并绘制注意力图像
def evaluateAndShowAttention(sentence):
    evaluateAndShowAtten(input_lang, output_lang, sentence, encoder1, attn_decoder1)


evaluateAndShowAttention("他们肯定会相恋的。")
evaluateAndShowAttention("我现在正在学习。")

# 语料中的数据随机选择评估
evaluateRandomly(input_lang, output_lang, pairs, encoder1, attn_decoder1)

output_words, attentions = evaluate(input_lang, output_lang,
                                    encoder1, attn_decoder1, "我是中国人。")
plt.matshow(attentions.numpy())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

日志如下:

C:\ProgramData\Anaconda3\python.exe E:/workspace/python/chapter7/evaluate_cmn_eng.py
2019-09-02 00:49:48,043 root  : INFO   eng -> cmn
2019-09-02 00:49:48,044 root  : INFO   lang loaded.
2019-09-02 00:49:50,110 root  : INFO   model loaded.
2019-09-02 00:49:51,197 root  : INFO   input = 他们肯定会相恋的。
2019-09-02 00:49:51,197 root  : INFO   output = they are sure to fall in love . <EOS>
cmn
2019-09-02 00:49:51,350 root  : INFO   input = 我现在正在学习。
cmn
2019-09-02 00:49:51,350 root  : INFO   output = i am studying now . <EOS>
2019-09-02 00:49:51,461 root  : INFO   > 他可能很快就到了。
2019-09-02 00:49:51,461 root  : INFO   = he is likely to arrive soon .
2019-09-02 00:49:51,485 root  : INFO   < he is likely to arrive soon . <EOS>
2019-09-02 00:49:51,485 root  : INFO   
2019-09-02 00:49:51,485 root  : INFO   > 我熟悉這個主題。
2019-09-02 00:49:51,485 root  : INFO   = i am familiar with this subject .
2019-09-02 00:49:51,507 root  : INFO   < i am familiar with this subject . <EOS>
2019-09-02 00:49:51,507 root  : INFO   
2019-09-02 00:49:51,507 root  : INFO   > 他的年紀可以開車了。
2019-09-02 00:49:51,507 root  : INFO   = he is old enough to drive a car .
2019-09-02 00:49:51,530 root  : INFO   < he is old enough to drive a car . <EOS>
2019-09-02 00:49:51,531 root  : INFO   
2019-09-02 00:49:51,531 root  : INFO   > 我們要去市中心吃比薩。
2019-09-02 00:49:51,531 root  : INFO   = we are going downtown to eat pizza .
2019-09-02 00:49:51,552 root  : INFO   < we are going downtown to eat pizza . <EOS>
2019-09-02 00:49:51,552 root  : INFO   
2019-09-02 00:49:51,552 root  : INFO   > 她有興趣學習新的想法。
2019-09-02 00:49:51,552 root  : INFO   = she is interested in learning new ideas .
2019-09-02 00:49:51,573 root  : INFO   < she is interested in learning new ideas . <EOS>
2019-09-02 00:49:51,573 root  : INFO   
2019-09-02 00:49:51,573 root  : INFO   > 他是一位有前途的学生。
2019-09-02 00:49:51,573 root  : INFO   = he is a promising student .
2019-09-02 00:49:51,591 root  : INFO   < he is a promising student . <EOS>
2019-09-02 00:49:51,591 root  : INFO   
2019-09-02 00:49:51,591 root  : INFO   > 他今天沒上學。
2019-09-02 00:49:51,591 root  : INFO   = he is absent from school today .
2019-09-02 00:49:51,609 root  : INFO   < he is absent from school today . <EOS>
2019-09-02 00:49:51,609 root  : INFO   
2019-09-02 00:49:51,609 root  : INFO   > 我期待她的來信。
2019-09-02 00:49:51,609 root  : INFO   = i am expecting a letter from her .
2019-09-02 00:49:51,628 root  : INFO   < i am expecting a letter from her . <EOS>
2019-09-02 00:49:51,628 root  : INFO   
2019-09-02 00:49:51,629 root  : INFO   > 他很穷。
2019-09-02 00:49:51,629 root  : INFO   = he is poor .
2019-09-02 00:49:51,640 root  : INFO   < he is poor . <EOS>
2019-09-02 00:49:51,640 root  : INFO   
2019-09-02 00:49:51,640 root  : INFO   > 他擅長應付小孩子。
2019-09-02 00:49:51,640 root  : INFO   = he is good at dealing with children .
2019-09-02 00:49:51,661 root  : INFO   < he is good at dealing with children . <EOS>
2019-09-02 00:49:51,661 root  : INFO   

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

可视化结果如下:

image.png

image.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/373278?site
推荐阅读
相关标签
  

闽ICP备14008679号