c5w1_循环序列模型

发布时间 2023-11-18 13:34:56作者: newbe3three

循环序列模型

自然语言和音频都是前后相关联的数据,对于这些前后相关联的序列数据通过循环神经网络(Recurrent Neural Network, RNN)来进行处理。

使用RNN收i先的应用有下图所示的例子:

image

上图中所有的这些问题都可以通过有监督学习,通过输入给定的标签数据\((X,Y)\)作为训练集进行学习。序列问题有很多不同类型,某些情况下X和Y可以都是序列数据,尽管都是序列数据但是X和Y有可能长度不等;在某些情况下中有X或者只有Y是序列数据。

数学符号

对于一个输入序列数据\(x\),用符号\(x^{<t>}\)来表示这个数据中的第t个元素,对应的用\(y^{<t>}\)来表示输出的序列数据中第t个元素。对于一句话,一个元素可以代表其中一个单词;对于一段音频,一个元素可能代表其中几个帧。

\(T_x\)\(T_y\)表示序列数据的长度。

训练样本序列数的表达和之前一样,所以对于第i个训练样本的第t个元素其数学符号表示为\(x^{(i)<t>}\),对应的输出为\(y^{(i)⟨t⟩}\)。对应即有 \(T^{(i)}_x\)\(T^{(i)}_y\)表示为第i个训练样本序列长度。

以句子中姓名识别为例子,也就是输入一句话识别其中的人名。对于这个训练样本有\(T_x=9\)\(T_y=9\)。(这个训练样本的输入输出长度一致,别忘记也有不一致的情况)

image

那么,对于上述这样的一个NLP领域内的例子,应该如何表示一个单词(也就是一个元素)?

具体来说,首先应该先建立一个词汇表(Vocabulary),或者叫字典(Dictionary),字典用一个向量来表示。其次对于词汇表的大小,由应用的决定,可以是数十万也甚至有百万级的。在本例中,假设词汇表的大小是10000。

建立了词汇表之后,就可以把任意的单词根据在词汇表中的位置,用one-hot向量(独热编码)来表示它。所谓的one-hot向量,是一个和词汇表维度一样的向量 \(R^{|V| \times 1}\)\(|V|\)是词汇表中单词的数量。

用于表示一个单词的one-hot向量中,只有一个元素是1,其余都是0,这个为1的元素的位置对应于单词在词汇表中的位置。

例如,"a"排列在词汇表第一位,所以表示它的向量为:

\[w^a = [1,0,0,...,0]^T \]

此外,为了避免出现不在词汇表中的单词,词汇表里会补充一个用以表示所有未知单词的符号,比如记为

循环神经网络模型

建立一个标准的神经网络模型,学习序列数据\(X\)\(Y\)的映射,可能存在以下问题:

  • 对于不同的训练样本之间,输入和输出的长度可能是不同的。比如有个句子长为9,有个句子长为3。
  • 从输入文本的不同位置学到的特征无法共享。比如网络学习到Harry在第一个位置可能是人名,但在其他位置却无法识别了。
  • 模型中的参数太多了,计算量巨大,每个单词的one-hot向量可都是与词汇表长度匹配的长度。

为了解决这些问题,循环神经网络(Recurrent Neural Network,RNN),下面是一种循环模型的结构图

image

与标准神经网络模型不同,这种循环神经网络模型,会应用到上层的预测的信息。

当元素\(x^{<t>}\)输入到当前时间步(time step)对应的隐藏层时,该隐藏层也会接收上一时间步的隐藏层的激活值\(a^{<t>}\)。其中,对于第一步的激活值\(a^{<0>}\)通常可以直接初始化为0或者随机初始化。

循环神经网络(RNN)从左到右扫描数据, 每个时间步都会共享参数。输入、输出、激活值对应的参数为\(w_{ax}\)\(w_{ay}\)\(w_{aa}\)

此外,RNN只共享了上层的预测信息,但是只知道上一个词的预测信息不一定可以完美的预测这一层的信息,如下面的例子。最好还是可以拿到下层的预测信息,就是双向循环神经网络(BidirectionalRecurrent Neural Network, BRNN)。

He said, “Teddy Roosevelt was a great President.”
He said, “Teddy bears are on sale!”

下图为一个RNN单元的机构

image

正向传播

image

前向传播公式如下:

\[a^{\langle 0 \rangle} = 0\\ a^{\langle t \rangle} = g_1(W_{aa} a^{\langle t-1 \rangle} + W_{ax} x^{\langle t \rangle} + b_a) \\ \hat y^{\langle t \rangle} = g_2 ( W_{ya} a^{\langle t \rangle} + b_y ) \]

激活函数\(g_1\)通常选择tanh,有时也会用ReLU;\(g_2\)根据要输出的类型可以选用sigmoid或softmax函数。

image

为了近一步方便运算和简化参数,将 \(W_{aa}\)\(W_{ax}\)水平并列成为一个矩阵,\(W_a\),同时 \(a^{⟨t-1⟩}\)和 $ x^{⟨t⟩}$ 垂直堆叠。则有下面的公式,这样就只需要两个W参数。

\[W_a=[W_{aa},W_{ax}]\\ a^{\langle t \rangle} = g_1(W_a[a^{\langle t-1\rangle};x^{\langle t\rangle}] + b)\\ \hat y^{\langle t \rangle}=g_2(W_y a^{\langle t \rangle} +b_y) \]

反向传播

和之前一样,当使用编程框架实现循环神经网络时,编程框架通常会自动处理反向传播。但是对循环神经网路的反向传播有一个粗略的认识还是对理解RNN很有帮助的。

为了计算反向传播的过程,需要先定义损失函数。对于输入的一个序列数据中,单个位置上某个单词的的预测值得损失函数采用交叉熵损失函数,如下所示:

\[L^{\langle t \rangle}(\hat y^{\langle t \rangle}, y^{\langle t \rangle}) = -y^{\langle t \rangle}\log\hat y^{\langle t \rangle} - (1 - y^{\langle t \rangle})\log(1-\hat y^{\langle t \rangle}) \]

将所有时间步上的损失函数相加,得到整个序列的成本函数:

\[L(\hat y, y) = \sum^{T_x}_{t=1} L^{\langle t \rangle}(\hat y^{\langle t \rangle}, y^{\langle t \rangle}) \]

image

循环神经网络的反向传播被称作穿越时间的反向传播(Backpropagation through time),这确实是一个很酷炫的名称。同时也形象的描述了从右到左时间步上的反向运算。

下图时更详细的计算公式,要不断地优化W_{x}

image

不同结构的RNN

对于不同的应用,序列模型的输入的长度\(T_x\)和输出的长度\(T_y\)可能是不同的。循环神经网络总得来说可以分为“一对一”、“一对多”、“多对多”等结构。

image

  • 一对一的结构,就是标准的神经网路
  • 一对多,在第一个时间步输入数据,后面每一个时间步的输入都来自于上一层的输出。比如音乐生成
  • 多对一,只在最后一个时间步输出。比如情感评价
  • 多对对的情况下,当\(T_x≠ T_y\)这种情况下,将输入的时间步和输出的时间步拆开对待。前半部分有输如的称为编码器(encoder),后半部分有输出的称为解码器(decoder)。比如机器翻译

语言模型

语言模型(Language Model)是NLP中非常基础且重要的问题。语言模型是定义在单词序列上的概率模型。通过将一段文字或一个句子视为单词序列,来实现预测单词序列中某个位置上某元素出现的概率。

例如,在一个语音识别系统中,语言模型能够通过计算两个读音相似的单词在句子中某位置出现的概率,来判断哪一个读音是正确结果。

image

建立语言模型所采用的训练集是一个大型的语料库(Corpus),由指数量级的众多句子组成的文本。

建立模型的第一步是符号化(Tokenize),即建立字典,然后将与语料库句子中的每一个单词表示为对应的one-hot向量。另外需要增加额外标记EOS和UNK,分别来表示句子结束和不在字典中的单词。如果考虑标点符号,可以在字典中添加对应的符号。

对于这样的预测序列位置中元素出现的可能性,通过RNN建立如下模型:

Cats average 15 hours of sleep a day.

image

  • 在第一个时间步,输入的\(a^{\langle 0 \rangle}\)\(x^{\langle 0 \rangle}\)都是零向量。
  • 每一个时间步的输出\(\hat y ^{\langle t \rangle}\)是通过softmax函数预测出的字典中每一个词出现的概率,所以如果字典是10002个,那么输出\(\hat y ^{\langle t \rangle}\)就是10002个类别的概率。
  • 每一个时间步会把上一个时间步的输出作为输入来预测该时间步上每个单词出现的概率。意思就是,每一个时间步会在上一个时间步预测的概率下预测本时间步元素出现的概率。P(average |cats)。 希望我说清楚了。
  • 以此类推,在最后一个时间步时,预测EOS出现的概率,就是句子在此结尾的可能性。

总结一下,上面的RNN学习从左到右预测,在已知前面所有单词的条件下,下一个单词的概率分布

定义该模型的代价函数:(每一时间步Softmax损失函数求和)

\[L(\hat y^{\langle t \rangle} ,y^{\langle t \rangle})=-\sum_t y^{\langle t \rangle}_ilog\hat y^{\langle t \rangle}_i \]

成本函数是:

\[L=\sum_t L^{(\langle t \rangle)}(\hat y^{\langle t \rangle},y^{\langle t \rangle}) \]

采样

在训练好一个RNN语言模型后,可以通过采样(Sample)新的序列来了解在这个模型中都学到了一些什么。

image

把为零向量的\(a^{(0)}\)\(x^{\langle 1 \rangle}\)作为一个时间步的输入,得到预测出字典中每个词作为第一个词的概率,根据softmax随机分布进行随机采样(np.random.choice),将选出来的结果作为第二个时间步的输入\(x^{\langle 2 \rangle}\)

以此类推,如果你的字典中有EOS,那么采样到EOS,就认为句子结束了。如果没有EOS,那就是你去决定在第几个时间步结束。

当你的字典中有UNk这个符号时,采样也可能会取到UNK。如果你不希望句子中出现UNK,那可以在采样到UNK,抛弃这个输出结果在剩下的解结果中重新采样,直到不是UNK。当然你不在乎UNK也可以。

模型生成的句子,就是它从语料库里学到的知识,尽管可能生成的内容狗屁不通。

目前构建的RNN都是单词级别的(wold-level)。此外,还有字符级别的RNN,即词汇表里不是单词而是由字母、数字符号等字符组成,这样生成的句子中就不会出现UNK。要注意的时,字符级的 RNN序列长度更长,计算的成本也就更高。

RNN的梯度消失

和其他类型的神经网络一样,当层数太深的时候,RNN也会面临梯度消失(vanishing gradients)和梯度爆炸(exploding gradients)的问题。

例如对于下面的两句话

The cat, which already ate …., was full.

The cats, which already ate …., were full.

后面的动词的单复数形式由前面名词的单复数形式决定。但是对于基本的RNN并不擅长捕获这种长期的依赖关系。由于梯度消失,在深层神经网络中,输出的误差很难通过反向传播影响到浅层,随着梯度越来越小,网络很难调整浅层的参数。

在反向传播时,随着层数的增多,梯度不仅可能指数型下降,也有可能指数型上升,即梯度爆炸。梯度爆炸相对容易处理和发现。当发生梯度爆炸,参数会剧烈膨胀到数值溢出(可能结果中出现NaN)。这时可以采用梯度修剪(Gradient Clipping)来解决问题。

观察梯度向量,如果超过了么某个阈值,则缩小梯度向量保证其不会过大。

image

GUR门控循环单元

门控循环单元(Gated Recurrent Unit, GUR),在基本RNN的单元上改进,帮助RNN可以更好的捕捉长距离依赖和梯度消失问题。

下图是基本的RNN单元,利用上一层的\(a^{\langle t-1\rangle}\) 来帮助预测\(a^{\langle t-1\rangle}\),但是当网络比较深的情况,浅层的很难影响到深层。

image

GRU引入记忆单元(Memory Cell)\(c^{\langle t\rangle}\)来解决这个问题,其作用是提供记忆能力,存储了浅层中的特征比如主语的单复数信息。在GRU中\(c^{\langle t\rangle} = a^{\langle t\rangle}\)

因为并不是每一层都需要用到存储在\(c^{\langle t\rangle}\)中的浅层的特征信息,所以它平时就被关在门里,门用符号\(\Gamma_u\),来表示,小标\(u\)用来表示更新操作。不需要的时候\(\Gamma_u=0\);当需要用到浅层的特征信息的时候,这个门就会被打开,\(\Gamma_u=1\),然后结合存储的这个特征去预测这一时间步的输出\(\tilde c^{\langle t \rangle}\)

所以GRU结构的具体公式为:

\[\tilde c^{\langle t \rangle} = tanh(W_c[c^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_c)\\ \Gamma_u = \sigma(W_u[c^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_u) \\ c^{\langle t \rangle} = \Gamma_u \times \tilde c^{\langle t \rangle} + (1-\Gamma_u) \times c^{\langle t-1 \rangle} \\ a^{\langle t \rangle} = c^{\langle t \rangle} \]

image

事实上,\(\Gamma_u\)是应用sigmoid的输出,其值不总为0或1,这里只是方便理解。所以在实践中,\(\tilde c^{\langle t \rangle}\)是会被更新的,通过门控制了权重,当发生梯度消失的时候,\(\Gamma_u\)非常小,那么也就停止了对\(\tilde c^{\langle t \rangle}\)的更新,相当于是恒等式。

以上实际上式简化了的GRU单元,但是蕴含了GUR的重要思想。完整的GUR还添加了一个相关门(Relevance Gate)\(\Gamma_r\),表示\(\tilde c^{\langle t \rangle}\)\(c^{\langle t \rangle}\)的相关性,因此GUR结构的完整表达式如下:

\[\tilde c^{\langle t \rangle} = tanh(W_c[ \Gamma\_r \times c^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_c)\\ \Gamma_u = \sigma(W_u[c^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_u) \\ \Gamma_r = \sigma(W_r[c^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_r) \\ c^{\langle t \rangle} = \Gamma_u \times \tilde c^{\langle t \rangle} + (1-\Gamma_u) \times c^{\langle t-1 \rangle}\\ a^{\langle t \rangle} = c^{\langle t \rangle} \]

LSTM长短期记忆

LSTM(Long Short Term Memory,长短期记忆)网络比 GRU 更加灵活和强大,它额外引入了遗忘门(Forget Gate) \(\Gamma_f\)输出门(Output Gate) \(\Gamma_o\)。其结构图和公式如下:

image

将多个 LSTM 单元按时间次序连接起来,就得到一个 LSTM 网络。图上贯穿\(c^{<t>}\)一条直线,就是实现长距离依赖的关键。

image

以上是简化版的 LSTM。在更为常用的版本中,几个门值不仅取决于$ x^{\langle t \rangle}\(和\) a^{\langle t-1 \rangle}\(,有时也可以偷窥上一个记忆细胞输入的值\) c^{\langle t-1 \rangle}\(,这被称为**窥视孔连接(Peephole Connection)**。*这时,和 GRU 不同,\)c^{⟨t−1⟩}$和门值是一对一的。*

关于GRU和LSTM的选择: 虽然GRU比LSTM更简单,但其实GRU出现的比LSTM要晚一些,它可以看做对LSTM的简化。GRU相对LSTM的优势之一就是它更简单,适合构建更大型的网络;但LSTM更强大些。目前看来,通常会将LSTM作为默认模型测试。

双向循环神经网络 (BRNN)

单向的循环神经网络在某一时刻的预测结果只能使用之前输入的序列信息。双向循环神经网络(Bidirectional RNN,BRNN)可以在序列的任意位置使用之前和之后的数据。其工作原理是增加一个反向循环层,结构如下图所示:

image

因此,有:

\[y^{\langle t \rangle} = g(W_y[\ \overrightarrow a ^{<t>}, \overleftarrow a^{<t>}] + b_y) \]

这个改进的方法不仅能用于基本的 RNN,也可以用于 GRU 或 LSTM。缺点是需要完整的序列数据,才能预测任意位置的结果。例如构建语音识别系统,需要等待用户说完并获取整个语音表达,才能处理这段语音并进一步做语音识别。因此,实际应用会有更加复杂的模块。

深度循环神经网络

为了解决更复杂的问题,我们会将多个RNN叠在一起形成深度循环神经网络(Deep RNN)。结构如下图所示:

image

\(a^{[2]<3>}\)为例,其输入同时来自于上一层的激活函数\(a^{[1]<3>}\)和本层上一个时间步的激活函数\(a^{[2]<2>}\),其计算公式是:

\[a^{[2]<3>} = g(W^{[2]}_a [a^{[1]<3>}, a^{[2]<2>}] + b^{[2]}_a) \]

对于标准CNN来说,可以有很多层,比如100个隐藏层,但对RNN来说3层就已经很深了,因为RNN还有很长的时间维度(temporal dimension),即便3层RNN的训练难度也很大。

当然我们也可以用BRNN创建深度BRNN。