Recurrent Neural Networks
Why sequence models
这些序列模型基本都属于监督式学习,输入x和输出y不一定都是序列模型。如果都是序列模型的话,模型长度不一定完全一致。
Notation
输入x:如“Harry Potter and Herminone Granger invented a new spell.”(以序列作为一个输入),x < t > x^{<t>} x < t > 表示输入x中的第t个符号。
输出y:如“1 1 0 1 1 0 0 0 0”(人名定位),同样,用y < t > y^{<t>} y < t > 表示输出y中的第t个符号。
T x T_x T x 用来表示输入x的长度;
T y T_y T y 用来表示输出y的长度;
x ( i ) < t > x^{(i)<t>} x ( i ) < t > 表示第i个输入样本的第t个符号,其余同理。
利用单词字典编码来表示每一个输入的符号:如one-hot编码等,实现输入x和输出y之间的映射关系。
Recurrent Neural Network Model
传统标准的神经网络 :
对于学习X和Y的映射,我们可以很直接的想到一种方法就是使用传统的标准神经网络。也许我们可以将输入的序列X以某种方式进行字典编码以后,如one-hot编码,输入到一个多层的深度神经网络中,最后得到对应的输出Y。如下图所示:
循环神经网络 :
循环神经网络作为一种新型的网络结构,在处理序列数据问题上则不存在上面的两个缺点。如下图所示:
序列模型从左到右,依次传递,此例中,T x = T y T_x=T_y T x = T y 。x < t > x^{<t>} x < t > 到y ^ < t > hat{y}^{<t>} y ^ < t > 之间是隐藏神经元。a < t > a^{<t>} a < t > 会传入到第t+1个元素中,作为输入。其中,a < 0 > a^{<0>} a < 0 > 一般为零向量。
RNN模型包含三类权重系数,分别是W a x , W a a , W y a W_{ax},W_{aa},W_{ya} W a x , W a a , W y a 。且不同元素之间同一位置共享同一权重系数。
RNN的正向传播(Forward Propagation)过程为: a < t > = g ( W a a ⋅ a < t − 1 > + W a x ⋅ x < t > + b a ) a^{<t>}=g(W_{aa}⋅a^{<t−1>}+W_{ax}⋅x^{<t>}+b_a) a < t > = g ( W a a ⋅ a < t − 1 > + W a x ⋅ x < t > + b a ) y ^ < t > = g ( W y a ⋅ a < t > + b y ) hat{y}^{<t>}=g(W_{ya}⋅a^{<t>}+b_y) y ^ < t > = g ( W y a ⋅ a < t > + b y ) 其中,g(⋅)表示**函数,不同的问题需要使用不同的**函数。 为了简化表达式,可以对a < t > a^{<t>} a < t > 项进行整合: W a a ⋅ a < t − 1 > + W a x ⋅ x < t > = [ W a a W a x ] [ a [ t − 1 ] x < t > ] → W a [ a < t − 1 > , x < t > ] W_{aa}cdot a^{<t-1>}+W_{ax}cdot x^{<t>}=[W_{aa} W_{ax}]begin{bmatrix}a^{[t-1]} \x^{<t>}end{bmatrix}to W_a[a^{<t-1>},x^{<t>}] W a a ⋅ a < t − 1 > + W a x ⋅ x < t > = [ W a a W a x ] [ a [ t − 1 ] x < t > ] → W a [ a < t − 1 > , x < t > ]
则正向传播可表示为:
a < t > = g ( W a [ a < t − 1 , x < t > ] + b a ) a^{<t>}=g(W_a[a^{<t-1},x^{<t>}]+b_a) a < t > = g ( W a [ a < t − 1 , x < t > ] + b a ) y ^ < t > = g ( W y ⋅ a < t > + b y ) hat{y}^{<t>}=g(W_y cdot a^{<t>}+b_y) y ^ < t > = g ( W y ⋅ a < t > + b y )
Backpropagation through time
针对上面识别人名的例子,经过RNN正向传播,单个元素的Loss function为: L < t > ( y ^ < t > , y < t > ) = − y < t > l o g y ^ < t > − ( 1 − y < t > ) l o g ( 1 − y ^ < t > ) L^{<t>}(ŷ^{<t>},y^{<t>})=−y^{<t>}logŷ^{<t>}−(1−y^{<t>})log (1−ŷ^{<t>}) L < t > ( y ^ < t > , y < t > ) = − y < t > l o g y ^ < t > − ( 1 − y < t > ) l o g ( 1 − y ^ < t > ) 该样本所有元素的Loss function为: L ( y ^ , y ) = ∑ t = 1 T y L < t > ( y ^ < t > , y < t > ) L(ŷ ,y)=sum_{t=1}^TyL^{<t>}(ŷ^{<t>},y^{<t>}) L ( y ^ , y ) = ∑ t = 1 T y L < t > ( y ^ < t > , y < t > ) 然后,反向传播(Backpropagation)过程就是从右到左分别计算L(ŷ ,y)对参数W a , W y , b a , b y W_a,W_y,b_a,b_y W a , W y , b a , b y 的偏导数。思路与做法与标准的神经网络是一样的。一般可以通过成熟的深度学习框架自动求导,例如PyTorch、Tensorflow等。这种从右到左的求导过程被称为Backpropagation through time。
Different types of RNNs
上图左下角many to many 模型是T x = T y T_x=T_y T x = T y ,而右下角many to many 模型是T x ≠ T y T_xneq T_y T x ̸ = T y
Language model and sequence generation
在NLP中,构建语言模型是最基础也是最重要的工作之一,我们可以通过RNN来很好的实现。 什么是语言模型呢?举个例子,在语音识别中,某句语音有两种翻译:
The apple and pair salad.
The apple and pear salad.
很明显,第二句话更有可能是正确的翻译。语言模型实际上会计算出这两句话各自的出现概率。比如第一句话概率为1 0 − 13 10^{−13} 1 0 − 1 3 ,第二句话概率为1 0 − 10 10^{−10} 1 0 − 1 0 。也就是说,利用语言模型得到各自语句的概率,选择概率最大的语句作为正确的翻译。概率计算的表达式为: P ( y < 1 > , y < 2 > , . . . . , y < T y > ) P(y^{<1>},y^{<2>},....,y^{<T_y>}) P ( y < 1 > , y < 2 > , . . . . , y < T y > ) 如何使用RNN构建语言模型?首先,我们需要一个足够大的训练集,训练集由大量的单词语句语料库(corpus)构成。然后,对corpus的每句话进行切分词(tokenize)。做法就跟第2节介绍的一样,建立vocabulary,对每个单词进行one-hot编码。例如下面这句话: The Egyptian Mau is a bread of cat. One-hot编码已经介绍过了,不再赘述。还需注意的是,每句话结束末尾,需要加上< EOS >作为语句结束符。另外,若语句中有词汇表中没有的单词,用< UNK >表示。假设单词“Mau”不在词汇表中,则上面这句话可表示为: The Egyptian < UNK > is a bread of cat. < EOS > 准备好训练集并对语料库进行切分词等处理之后,接下来构建相应的RNN模型。
语言模型的RNN结构如上图所示,x < 1 > x^{<1>} x < 1 > 和a < 0 > a^{<0>} a < 0 > 均为零向量。Softmax输出层y ^ < 1 > hat{y}^{<1>} y ^ < 1 > 表示出现该语句第一个单词的概率,softmax输出层y ^ < 2 > hat{y}^{<2>} y ^ < 2 > 表示在第一个单词基础上出现第二个单词的概率,即条件概率,以此类推,最后是出现< EOS >的条件概率。
单个元素的softmax loss function为: L < t > ( y ^ < t > , y < t > ) = − ∑ i y i < t > l o g y ^ i < t > L^{<t>}(hat{y}^{<t>},y^{<t>})=−sum_iy^{<t>}_iloghat{y}^{<t>}_i L < t > ( y ^ < t > , y < t > ) = − ∑ i y i < t > l o g y ^ i < t > 该样本所有元素的Loss function为: L ( y ^ , y ) = ∑ t L < t > ( y ^ < t > , y < t > ) L(hat{y} ,y)=sum_tL^{<t>}(hat{y}^{<t>},y^{<t>}) L ( y ^ , y ) = ∑ t L < t > ( y ^ < t > , y < t > ) 对语料库的每条语句进行RNN模型训练,最终得到的模型可以根据给出语句的前几个单词预测其余部分,将语句补充完整。例如给出“Cats average 15”,RNN模型可能预测完整的语句是“Cats average 15 hours of sleep a day.”。
最后补充一点,整个语句出现的概率等于语句中所有元素出现的条件概率乘积。例如某个语句包含y < 1 > , y < 2 > , y < 3 > y^{<1>},y^{<2>},y^{<3>} y < 1 > , y < 2 > , y < 3 > ,则整个语句出现的概率为: P ( y < 1 > , y < 2 > , y < 3 > ) = P ( y < 1 > ) ⋅ P ( y < 2 > ∣ y < 1 > ) ⋅ P ( y < 3 > ∣ y < 1 > , y < 2 > ) P(y^{<1>},y^{<2>},y^{<3>})=P(y^{<1>})⋅P(y^{<2>}|y^{<1>})⋅P(y^{<3>}|y^{<1>},y^{<2>}) P ( y < 1 > , y < 2 > , y < 3 > ) = P ( y < 1 > ) ⋅ P ( y < 2 > ∣ y < 1 > ) ⋅ P ( y < 3 > ∣ y < 1 > , y < 2 > )
Sampling novel sequences
利用训练好的RNN语言模型,可以进行新的序列采样,从而随机产生新的语句。与上一节介绍的一样,相应的RNN模型如下所示:
首先输入x < 1 > = 0 , a < 0 > = 0 x^{<1>}=0,a^{<0>}=0 x < 1 > = 0 , a < 0 > = 0 ,在第一个时间戳输出的softmax分布中随机选取一个word作为新语句的首单词y ^ < 1 > hat{y}^{<1>} y ^ < 1 > 。
然后继续下一个时间戳,我们以刚刚采样得到的y ^ < 1 > hat{y}^{<1>} y ^ < 1 > 作为下一个时间戳的输入,进而softmax层会预测下一个输出y ^ < 2 > hat{y}^{<2>} y ^ < 2 > ,以此类推。
如果字典中有结束的标志如:“EOS”,那么输出是该符号时则表示结束;若没有这种标志,则我们可以自行设置结束的时间戳。
上面的模型是基于词汇的语言模型,我们还可以构建基于字符的语言模型,其中每个单词和符号则表示一个相应的输入或者输出:
character level RNN的优点是能有效避免遇到词汇表中不存在的单词< UNK >。但是,character level RNN的缺点也很突出。由于是字符表征,每句话的字符数量很大,这种大的跨度不利于寻找语句前部分和后部分之间的依赖性。另外,character level RNN的在训练时的计算量也是庞大的。基于这些缺点,目前character level RNN的应用并不广泛,但是在特定应用下仍然有发展的趋势。
Vanishing gradients with RNNs
语句中可能存在跨度很大的依赖关系,即某个word可能与它距离较远的某个word具有强依赖关系。例如下面这两条语句:
The cat, which already ate fish, was full.
The cats, which already ate fish, were full.
第一句话中,was受cat影响;第二句话中,were受cats影响。它们之间都跨越了很多单词。而一般的RNN模型每个元素受其周围附近的影响较大,难以建立跨度较大的依赖性。上面两句话的这种依赖关系,由于跨度很大,普通的RNN网络容易出现梯度消失,捕捉不到它们之间的依赖,造成语法错误。
另一方面,RNN也可能出现梯度爆炸的问题,即gradient过大。常用的解决办法是设定一个阈值,一旦梯度最大值达到这个阈值,就对整个梯度向量进行尺度缩小。这种做法被称为gradient clipping。
Gated Recurrent Unit(GRU)
RNN的隐藏层单元结构如下图所示:
为了解决梯度消失问题,对上述单元进行修改,添加了记忆单元,构建GRU,如下图所示:
相应的表达式为:
c ~ < t > = t a n h ( W c [ c < t − 1 > , x < t > ] + b c ) tilde{c}^{<t>}=tanh(W_c[c^{<t-1>},x^{<t>}]+b_c) c ~ < t > = t a n h ( W c [ c < t − 1 > , x < t > ] + b c )
Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) Gamma_u=sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u )
c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=Gamma_u * tilde{c}^{<t>}+(1-Gamma_u) *c^{<t-1>} c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 >
其中c < t − 1 > = a < t − 1 > , c < t > = a < t > c^{<t-1>}=a^{<t-1>},c^{<t>}=a^{<t>} c < t − 1 > = a < t − 1 > , c < t > = a < t > .Γ u Gamma_u Γ u 意为gate,记忆单元。当Γ u = 1 Gamma_u=1 Γ u = 1 时,代表更新;当Γ u Gamma_u Γ u =0时,代表记忆,保留之前的模块输出。这一点跟CNN中的ResNet的作用有点类似。因此Γ u Gamma_u Γ u 能够保证RNN模型中跨度很大的依赖关系不受影响,消除梯度消失问题。
上面介绍的是简化版的GRU模型,完整的GRU添加了另外一个gate,即Γ r Gamma_r Γ r 表达式如下: c ~ < t > = t a n h ( Γ r ∗ W c [ c < t − 1 > , x < t > ] + b c ) tilde{c}^{<t>}=tanh(Gamma_r *W_c[c^{<t-1>},x^{<t>}]+b_c) c ~ < t > = t a n h ( Γ r ∗ W c [ c < t − 1 > , x < t > ] + b c ) Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) Gamma_u=sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) Γ r = σ ( W r [ c < t − 1 > , x < t > ] + b r ) Gamma_r=sigma(W_r[c^{<t-1>},x^{<t>}]+b_r) Γ r = σ ( W r [ c < t − 1 > , x < t > ] + b r ) c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=Gamma_u * tilde{c}^{<t>}+(1-Gamma_u) *c^{<t-1>} c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 >
Long Short Term Memory(LSTM)
LSTM是另一种更强大的解决梯度消失问题的方法。它对应的RNN隐藏层单元结构如下图所示:
相应的表达式为:
c ~ < t > = t a n h ( W c [ a < t − 1 > , x < t > ] + b c ) tilde{c}^{<t>}=tanh(W_c[a^{<t-1>},x^{<t>}]+b_c) c ~ < t > = t a n h ( W c [ a < t − 1 > , x < t > ] + b c ) Γ u = σ ( W u [ a < t − 1 > , x < t > ] + b u ) Gamma_u=sigma(W_u[a^{<t-1>},x^{<t>}]+b_u) Γ u = σ ( W u [ a < t − 1 > , x < t > ] + b u ) Γ f = σ ( W f [ a < t − 1 > , x < t > ] + b f ) Gamma_f=sigma(W_f[a^{<t-1>},x^{<t>}]+b_f) Γ f = σ ( W f [ a < t − 1 > , x < t > ] + b f ) Γ o = σ ( W o [ a < t − 1 > , x < t > ] + b o ) Gamma_o=sigma(W_o[a^{<t-1>},x^{<t>}]+b_o) Γ o = σ ( W o [ a < t − 1 > , x < t > ] + b o ) c < t > = Γ u ∗ c ~ < t > + Γ f ∗ c < t − 1 > c^{<t>}=Gamma_u * tilde{c}^{<t>}+Gamma_f *c^{<t-1>} c < t > = Γ u ∗ c ~ < t > + Γ f ∗ c < t − 1 > a < t > = Γ o ∗ c < t > a^{<t>}=Gamma_o *c^{<t>} a < t > = Γ o ∗ c < t >
GRU可以看成是简化的LSTM,两种方法都具有各自的优势。
Bidirectional RNN
BRNN对应的输出y < t > y^{<t>} y < t > 表达式为:
y ^ < t > = g ( W y [ a → < t > , a ← < t > ] + b y ) hat{y}^{<t>}=g(W_y[overrightarrow{a}^{<t>},overleftarrow{a}^{<t>}]+b_y) y ^ < t > = g ( W y [ a < t > , a < t > ] + b y )
BRNN能够同时对序列进行双向处理,性能大大提高。但是计算量较大,且在处理实时语音时,需要等到完整的一句话结束时才能进行分析。
Deep RNNs
Deep RNNs由多层RNN组成,其结构如下图所示:
与DNN一样,用上标[l][l]表示层数。Deep RNNs中a [ l ] < t > a^{[l]<t>} a [ l ] < t > 的表达式为: a [ l ] < t > = g ( W a [ l ] [ a [ l ] < t − 1 > , a [ l − 1 ] < t > ] + b a [ l ] ) a^{[l]<t>}=g(W^{[l]}_a[a^{[l]<t−1>},a^{[l−1]<t>}]+b^{[l]}_a) a [ l ] < t > = g ( W a [ l ] [ a [ l ] < t − 1 > , a [ l − 1 ] < t > ] + b a [ l ] ) 我们知道DNN层数可达100多,而Deep RNNs一般没有那么多层,3层RNNs已经较复杂了。
另外一种Deep RNNs结构是每个输出层上还有一些垂直单元,如下图所示: