1 1 1 1 1 1 1 1 1 1 Rating 0.00 (0 Votes)
 循环神经网络RNN结构被广泛应用于机器翻译,语音识别,文字识别OCR等方向。本文主要介绍经典的RNN结构,以及RNN的变种(包括Seq2Seq结构和Attention机制)。希望这篇文章能够帮助初学者更好地入门。

经典的RNN结构

图1

这就是最经典的RNN结构,它的输入是:

text{inputs: }{x_1,x_2,...,x_t,...,x_T}

输出为:

text{outputs: }{y_1,y_2,...,y_t,...,y_T}

也就是说,输入和输出序列必有相同的时间长度!

图2

假设输入 x_t ( tin {1,2,...,T} ) 是一个长度为 n_i ( n_{input} ) 的列向量:

x_t = [x_{t,1}, x_{t,2},x_{t,3},...,x_{t,n_i}]^T

隐藏层 h_t 是一个长度为 n_h ( n_{hidden} ) 的列向量:

h_t= [h_{t,1}, h_{t,2},h_{t,3},...,h_{t,n_h}]^T

输出 y_t 是一个长度为 n_o ( n_{output} ) 的列向量:

y_t= [y_{t,1}, y_{t,2},y_{t,3},...,y_{t,n_o}]^T

其中 n_i , n_h , n_o 都是由人工设定的。

图3
  • t 时刻输入层-->t 时刻隐藏层:

h_t^{ih}=W_{ih} cdot x_t+ b_{ih}

  • (t-1) 时刻隐藏层-->t 时刻隐藏层:

h_t^{hh}=W_{hh}cdot h_{t-1}+b_{hh}

  • t 时刻输入层 and(t-1) 时刻隐藏层-->t 时刻隐藏层:

begin{align} h_t&=tanh(h_t^{ih} + h_t^{hh})&=tanh((W_{ih}cdot x_t+ b_{ih})+(W_{hh}cdot h_{t-1}+b_{hh})) end{align}

  • t 时刻隐藏层-->t 时刻输出层:

y_t=W_{ho}cdot h_t+b_{ho}

需要注意的是,对于任意时刻 tin {1,2,...,T} ,所有的权值(包括 W_{ih} , b_{ih} , W_{hh} , b_{hh} , W_{ho} , b_{ho} )都相等,这也就是RNN中的“权值共享”,极大的减少参数量。

其实RNN可以简单的表示为:

y_t=texttt{RNN}(x_t,h_{t-1})=texttt{RNN}(x_t,x_{t-1},...,x_2,x_1)

图4

还有一个小细节:在 t=1 时刻,如果没有特别指定初始状态,一般都会使用全0的 h_0 作为初始状态输入到 h_1 中

text{zero state:   }h_0=[0,0,...,0]

Sequence to Sequence模型

图5

在Seq2Seq结构中,编码器encoder把所有的输入序列都编码成一个统一的语义向量context,然后再由解码器decoder解码。在解码器decoder解码的过程中,不断地将前一个时刻 t-1 的输出作为后一个时刻 t 的输入,循环解码,直到输出停止符为止。

图6

接下来以机器翻译为例,看看如何通过Seq2Seq结构把中文“早上好”翻译成英文“Good morning”:

  1. 将“早上好”通过encoder编码,并将最后 t=3 时刻的隐藏层状态 h_3 作为语义向量。
  2. 以语义向量为decoder的 h_0 状态,同时在 t=1 时刻输入<start>特殊标识符,开始解码。之后不断的将前一时刻输出作为下一时刻输入进行解码,直接输出<stop>特殊标识符结束。

当然,上述过程只是Seq2Seq结构的一种经典实现方式。Seq2Seq结构不再要求输入和输出序列有相同的时间长度!

图7

进一步来看上面机器翻译例子decoder端的 t 时刻数据流,如图7:

  • 首先对RNN输入大小为 [n_i,1] 的向量 x_t (红点);
  • 然后经过RNN输出大小为 [n_o,1] 的向量 y_t (蓝点);
  • 接着使用全连接fc将 y_t 变为大小为 [n_c,1] 的向量 y_t' ,其中 n_c 代表类别数量;
  • 再 y_t' 经过softmax和argmax获取类别index,再经过int2str获取输出字符;
  • 最后将类别index输入到下一状态,直到接收到<stop>标志符停止。

还有一点细节,就是如何将前一时刻输出类别index(数值)送入下一时刻输入(向量)进行解码。假设每个标签对应的类别index如下:

'<start>' : 0,
'<stop>' : 1,
'good' : 2,
'morning' : 3,
...

已知<start>标志符index为0,如果需要将<start>标志符输入到input层,就需要把类别index=0转变为一个 [n_i,1] 长度的特定对应向量。这时就需要应用嵌入 (embedding) 方法。

  • 首先生成一个大小为 [n_c,n_i] embedding随机矩阵:

texttt{embedding}_{[n_c,n_i]}=text{random}(n_c,n_i)

  • 然后通过start标志index=0获取embedding矩阵的第0行,作为start标志对应的输入向量,即通过嵌入恢复维度:

x_1= texttt{embedding}[0,:]

  • 把抽出形状 [n_i,1] 的x_1 作为 t=1 时刻输入送入到RNN,获得 t=1 时刻形状为 [n_o,1] 输出 y_1 :

y_1=texttt{RNN}(x_1,h_0=text{context})

  • 然后再通过全连接将形状为 [n_o,1] 的 y_1 变为形状为 [n_c,1] 的  y_1' (其中 n_c 为 n_{classes} ,代表类别数量),之后通过softmax和argmax获取输出good标志对应的 text{index}=2

y_1'=W_ccdot y_1

text{index}=text{argmax}(text{softmax}(y_1'))

  • 再抽取embedding的第 text{index}=2 行获取 x_2 ,之后不停循环解码:

x_2= texttt{embedding}[2,:]

y_2=texttt{RNN}(x_2,h_1)

可以看到,其实Seq2Seq引入嵌入机制解决从label index数值到输入向量的维度恢复问题。在Tensorflow中上述过程通过以下函数实现:

tf.nn.embedding_lookup

而在pytorch中通过以下接口实现:

torch.nn.Embedding

需要注意的是:train和test阶段必须使用一样的embedding矩阵!否则输出肯定是乱码。

Attention注意力机制

图8

在Seq2Seq结构中,encoder把所有的输入序列都编码成一个统一的语义向量context,然后再由decoder解码。由于context包含原始序列中的所有信息,它的长度就成了限制模型性能的瓶颈。如机器翻译问题,当要翻译的句子较长时,一个context可能存不下那么多信息,就会造成精度的下降。除此之外,如果按照上述方式实现,只用到了编码器的最后一个隐藏层状态,信息利用率低下。

所以如果要改进Seq2Seq结构,最好的切入角度就是:利用encoder所有隐藏层状态 h_t 解决context长度限制问题。

图9

接下来了解一下attention注意力机制基本思路(Luong Attention)

考虑这样一个问题:由于encoder的隐藏层状态 h_t 代表对不同时刻输入 x_t 的编码结果:

texttt{RNN:}  x_tRightarrow h_t   (tin {1, 2, ..., T}) 
即encoder状态 h_1 , h_2 , h_3 对应编码器对“早”,“上”,“好”三个中文字符的编码结果。那么在decoder时刻  t=1 通过3个权重 w_{11} , w_{12} , w_{13} 计算出一个向量 c_1 :

c_1=h_1cdot w_{11}+h_2cdot w_{12}+h_3cdot w_{13}

然后将这个向量与前一个状态拼接在一起形成一个新的向量输入到隐藏层计算结果:

bar{h}_0leftarrow text{concat}(bar{h}_0,c_1)=text{concat}(h_3,c_1)

decoder时刻  t=1 :

c_2=h_1cdot w_{21}+h_2cdot w_{22}+h_3cdot w_{23}

bar{h}_1leftarrowtext{concat}(bar{h}_1,c_2)

decoder时刻 t=2 和 t=3 同理,就可以解决context长度限制问题。由于 w_{11} , w_{12} , w_{13} 不同,就形成了一种对编码器不同输入 x_t 对应 h_t 的“注意力”机制(权重越大注意力越强)。

图10

那么到底什么是attention注意力机制?

为了说明注意力机制结构,重新定义符号:  bar{h}_s 代表encoder状态, h_t 代表decoder状态, tilde{h}_t代表attention layer输出的最终decoder状态,如图10。需要说明,  bar{h}_s 和 h_t 是 [n_h,1] 大小的向量。接下来一起看看注意力机制具体实现方式。

  • 首先,计算decoder的 t 时刻隐藏层状态 h_t 对encoder每一个隐藏层状态  bar{h}_s 权重 a_t(s) 数值:

a_t(s)=frac{exp(text{score}(h_t,bar{h}_s))}{sum_{s'}^{}{}exp(text{score}(h_t,bar{h}_{s'}))}

这里的score可以通过以下两种方式计算:

text{score}(h_t,bar{h}_s) =  begin{cases}     h_t^T bar{h}_s & text{dot }     h_t^T W_abar{h}_s          & text{general} end{cases}

所谓dot就是向量内积,而general通过乘以 W_a 权重矩阵进行计算( W_a 是 [n_h,n_h] 大小的矩阵)。一般来说general方法好于dot方法。

  • 其次,利用权重 a_t(s) 计算所有隐藏层状态  bar{h}_s 加权之和 c_t ,即生成新的大小为 [n_h,1] 的context状态向量:

c_t=sum_{s}^{}{a_t(s)cdotbar{h}_{s}}

  • 接下来,将通过权重 a_t(s) 生成的 c_t 与原始decoder隐藏层 t 时刻状态 h_t 拼接在一起:

tilde{h}_t=text{tanh}(W_ccdot text{concat}(c_t, h_t))=text{tanh}(W_ccdot [c_t;h_t])

这里 c_t 和 h_t 大小都是[n_h,1] ,拼接后会变大。由于需要恢复为原来形状,所以乘以全连接 W_c矩阵。

  • 最后,对加入“注意力”的decoder状态 tilde{h}_t 乘以 W_{ho} 矩阵即可获得输出:

y_t=W_{ho}tilde{h}_t+b_{ho}

也可以根据需要,把新生成的状态 tilde{h}_t 继续送入RNN继续进行学习。其中 W_a 和 W_c 参数需要通过学习获得。

需要说明,上述说明只是注意力机制的一种具体实现方式,并不意味着注意力机制仅此一种。

图11

可以看到,整个Attention注意力机制相当于在Seq2Seq结构上加了一层“包装”,内部通过 W_a权重引入注意力机制。无论在机器翻译,语音识别,自然语言处理(NLP),文字识别(OCR),Attention机制对Seq2Seq结构都有很大的提升。

拓展:如何向RNN加入额外信息?

其实对于RNN(包括Seq2Seq结构)的改进本质上都是给网络添加额外信息,从而使得网络有更完整的信息流,如Attention机制就是将之前的隐藏层状态加权后作为额外信息加入。

图12 RNN添加额外信息的3中方式

所以,假设有额外信息 z_t ,给RNN网络添加额外信息主要有以下3种方式:

  • ADD:直接将 z_t 叠加在输出 y_t 上。

y_tleftarrow y_t+z_t

  • CONCAT:将 z_t 拼接在隐藏层 h_t 后全连接恢复维度(不恢复维度也可以,但是会造成参数量加倍)。上文Attention机制就使用此种方法。

h_tleftarrow W_ccdot text{concat}(h_t,z_t)

  • MLP:新添加一个对 z 的感知单元 h_t^{zh}=W_{zh}cdot z_t+b_{zh} 。

begin{align} h_t&leftarrowtanh(h_t^{ih} + h_t^{hh}+h_t^{zt})&=tanh((W_{ih}cdot x_t+ b_{ih})+(W_{hh}cdot h_{t-1}+b_{hh})+(W_{zh}cdot z_t+b_{zh})) end{align}

读到这里,你也隐约Get到了Attention机制的10086种姿势。

参考文献

[1508.04025] Effective Approaches to Attention-based Neural Machine Translation​arxiv.org
 

参考代码

Pytorch: Translation with a Sequence to Sequence Network and Attention​pytorch.org图标

 

编辑于 2019-01-20

Add comment


Security code
Refresh