Lesson 5 Sequence Models
这篇文章其实是 Coursera 上吴恩达老师的深度学习专业课程的第五门课程的课程笔记。
参考了其他人的笔记继续归纳的。
符号定义
假如我们想要建立一个能够自动识别句中人名地名等位置的序列模型,也就是一个命名实体识别问题,这常用于搜索引擎。命名实体识别系统可以用来查找不同类型的文本中的人名、公司名、时间、地点、国家名和货币名等等。
我们输入语句 "Harry Potter and Herminoe Granger invented a new spell." 作为输入数据 \(x\),我们想要这个序列模型输出 \(y\),使得输入的每个单词都对应一个输出值,同时这个 \(y\) 能够表明输入的单词是否是人名的一部分。技术上来说,还有更加复杂的输出形式,它不仅能够表明输入词是否是人名的一部分,它还能够告诉你这个人名在这个句子里从哪里开始到哪里结束。
以简单的输出形式为例。这个输入数据是 9 个单词组成的序列,所以最终我们会有 9 个特征集合来表示这 9 个单词,并按序列中的位置进行索引,\(x^{<1>},x^{<2>}\) 直到 \(x^{<9>}\) 来索引不同的位置。
输出数据也是一样,用 \(y^{<1>},y^{<2>}\) 到 \(y^{<9>}\) 来表示输出数据。同时使用 \(T_x\) 来表示输入序列的长度,\(T_y\) 表示输出序列的长度。在这里例子里,\(T_x=9\),且 \(T_x=T_y\)。
想要表示一个句子里的单词,首先需要做一张词表(或者说词典),也就是列一列我们的表示方法中用到的单词。以下图这个词表为例,它是一个 10,000 个单词大小的词表。这对现代自然语言处理应用来说太小了,对于一般规模的商业应用来说 30,000 到 50,000 词大小的词表比较常见,有些大型互联网公司会有百万词等。
我们以这个 10,000 词的词表为例。我们用 one-hot 表示法来表示词典里的每个单词,也就是说 \(x^{<1>}\) 表示 Harry 这个单词,而 Harry 在词表中的第 4075 行,所以 \(x^{<1>}\) 最终表示为一个长度为 10,000,在 4075 行为 1,其余行为 0 的向量。同理,其他的词也这样进行编码。
循环神经网络模型 (Recurrent Neural Network Model)
如果直接把 9 个 one-hot 向量输入到一个标准神经网络中,经过一些隐藏层,最终会输出 9 个值为 0 或者 1 的项来表明每个输入单词是否是人名的一部分。
但是结果发现这种方法并不好,主要有两个问题。
- 输入和输出数据在不同例子中可以有不同的长度,不是所有的例子都有相同的 \(T_x\) 或 \(T_y\)。而且即使每个句子都有最大长度,我们可以填充使每个输入语句都达到最大长度,但这仍然不是一个很好的方式。
- 这样一个神经网络结构,它并不共享从文本的不同位置上学到的特征。也就是说,如果神经网络已经学习到了在位置 1 出现的 Harry 可能是人名的一部分,那么如果 Harry 出现在其他位置,它也能自动识别其为人名的一部分的话就好了。这其实类似于卷积神经网络中,我们希望将图片的局部学到的内容快速推广到图片的其他部分。所以用一个更好的表达方式,能够让我们减少模型中参数的数量。
循环神经网络如下图所示。将第一个词输入一个神经网络层,让神经网络尝试预测输出,判断这是否是人名的一部分。而接下来第二个词,它不仅用 \(x^{<2>}\) 来预测 \(y^{<2>}\),它也会输入来自上一层神经网络的激活值,接下来的词也以此类推。所以在每一个时间步中,循环神经网络传递一个激活值到下一个时间步中用于计算。如果 \(T_x\) 和 \(T_y\) 不相等,这个结果会需要作出一些改变。
要开始整个流程,在零时刻需要构造一个激活值 \(a^{<0>}\),这通常是零向量。当然也有其他初始化 \(a^{<0>}\) 的方法,不过使用零向量的伪激活值是最常见的选择。
循环神经网络是从左向右扫描数据,同时每个时间步的参数也是共享的。我们用 \(W_{ax}\) 来表示管理着从 \(x_{<1>}\) 到隐藏层的连接的一系列参数,而激活值也就是水平联系是由参数 \(W_aa\) 决定的,同理,输出结果由 \(W_ya\) 决定。这些参数在每个时间步都是相同的。
这个循环神经网络的一个缺点就是它只使用了这个序列中之前的信息来做出预测,如预测 \(\hat{y}^{<3>}\) 时,它没有用到 \(x^{<4>},x^{<5>}\) 等的信息。所以对于这两个句子
Teddy Roosevelt was a great President.
Teddy bears are on sale!
为了判断 Teddy 是否是人名的一部分,仅仅知道句中前两个词是完全不够的。所以后续我们需要使用双向循环神经网络 (BRNN) 来解决这个问题。
我们仍以单向神经网络为例了解其计算过程。
一般开始先输入 \(a^{<0>}\),接着就是前向传播过程。
\[ a^{<1>} = g_{1}(W_{
{aa}}a^{< 0 >} + W_{
{ax}}x^{< 1 >} + b_{a})\\ \hat y^{< 1 >} = g_{2}(W_{
{ya}}a^{< 1 >} + b_{y})\\ \cdots \cdots \]
循环神经网络用的激活函数经常是 tanh,偶尔也会用 ReLU。
前向传播公式的泛化公式如下,在 t 时刻
\[ a^{< t >} = g_{1}(W_{aa}a^{< t - 1 >} + W_{ax}x^{< t >} + b_{a})\\ \hat y^{< t >} = g_{2}(W_{
{ya}}a^{< t >} + b_{y}) \]
我们的符号约定,以 \(W_{ax}\) 为例,第二个下标意味着它要乘以某个 \(x\) 类型的量,然后第一个下标 \(a\) 表示它是用来计算某个 \(a\) 类型的变量。其他几个矩阵符号也是同理。
为了简化这些符号,我们可以简化一下,第一个计算 \(a^{<t>}\) 的公式可以写作
\[ a^{<t>} =g(W_{a}\left\lbrack a^{< t-1 >},x^{} \right\rbrack +b_{a}) \]
然后我们定义 \(W_a\) 为矩阵 \(W_{aa}\) 和 \(W_{ax}\) 水平并列放置,即 \([ {
{W}_{aa}}\vdots {
{W}_{ax}}]=W_{a}\)。而 \(\left\lbrack a^{< t - 1 >},x^{< t >}\right\rbrack\) 表示的是将这两个向量堆在一起,即 \(\begin{bmatrix}a^{< t-1 >} \\ x^{< t >} \\\end{bmatrix}\)。这样,我们就把两个参数矩阵压缩成了一个参数矩阵,当我们建立更复杂模型时,这能简化我们要用到的符号。
同理,对于 \(\hat y^{< t >}\) 的计算,也可以写作
\[ \hat y^{< t >} = g(W_{y}a^{< t >} +b_{y}) \]
RNN 前向传播示意图如下。
穿越时间的反向传播
为了计算反向传播,我们先定义一个元素损失函数。
\[ L^{}( \hat y^{},y^{}) = - y^{}\log\hat y^{}-( 1- y^{})log(1-\hat y^{}) \]
它对应的是序列中一个具体的词,如果它是某个人的名字,那么 \(y^{<t>}\) 的值为 1,然后神经网络将输出这个词是名字的概率值。它被定义为标准逻辑回归损失函数,也叫交叉熵损失函数 (cross entropy loss)。
整个序列的损失函数为
\[ L(\hat y,y) = \ \sum_{t = 1}^{T_{x}}{L^{< t >}(\hat y^{< t >},y^{< t >})} \]
也就是把每个单独时间步的损失函数都加起来。
在这个反向传播过程中,最重要的信息传递或者说最重要的递归运算就是这个从右到左的运算,所以它被叫做穿越时间反向传播 (backpropagation through time)。
RNN 反向传播示意图如下。
不同类型的循环神经网络
并不是所有的情况都满足 \(T_x=T_y\)。比如电影情感分类,输出 \(y\) 可以是 1 到 5 的整数,而输入是一个序列。
之前的命名实体识别问题,属于多对多 (many-to-many) 的结构。因为输入序列有很多的输入,而输出序列也有很多的输出。还有一种多对多结构,和命名实体识别问题不同,它的输入和输出的序列可能是不同长度的。例如,机器翻译,不同语言对于同一句话可能会有不同的长度的语句。而情感分类问题,属于多对一 (many-to-one) 的结构。因为它有很多输入,然后输出一个数字。当然也有一对一 (one-to-one) 结构,也就是标准的神经网络。
其实还有一对多 (one-to-many) 的结构。例子是音乐生成,我们可以使用神经网络通过我们输入的一个整数(用来表示音乐类型或者第一个音符等信息)来生成一段音乐。
语言模型和序列生成
假如我们在做一个语音识别系统,听到一个句子
The apple and pear (pair) salad was delicious.
语音识别系统就要判断,在这个句子中是 pear 还是 pair。这里,就要使用一个语言模型,它能计算出这两句话各自的可能性。
这个概率指的是,假设我们随机拿起一张报纸,打开任意邮件,或者任意网页或者听某人说一句话,这个即将从世界上的某个地方得到的句子会是某个特定句子的概率是多少。
使用 RNN 建立出这样的模型,首先需要一个训练集,包含一个很大的英文文本语料库 (corpus) 或者其他的语言(这取决于我们的目的)。语料库是自然语言处理的一个专有名词,意思就是很长的或者说数量众多的句子组成的文本。
如果训练集中有这么一句话
Cats average 15 hours of sleep a day.
那么首先将这个句子标记化,就是像之前那样,建立一个词典,然后将每个单词都转换为对应的 one-hot 向量。然后我们要定义句子的结尾,一般的做法就是增加一个额外的标记,叫做 EOS,用来表示句子的结尾。这样能帮助我们明白一个句子什么时候结束。
在标记化的过程中,我们可以自行决定要不要把标点符号看成标记。如果要把标点符号看作标记的话,那么我们建立的词典也应该加入这些标点符号。
如果训练集有一些词不在建立的词典里,如下面这个句子
The Egyptian Mau is a bread of cat.
Mau 这个词可能比较少见,并不在我们建立的词典里。这种情况下,我们可以把 Mau 替换成一个叫做 UNK 的代表未知词的标志,我们只针对 UNK 建立概率模型,而不是针对这个具体的词 Mau。
完成标记化后,意味着输入的句子都映射到了各个标志上。下一步就是构建 RNN。
仍然以 "Cats average 15 hours of sleep a day。“ 作为输入为例。在第 0 个时间步,计算激活项 \(a^{<1>}\),它是以 \(x^{<1>}\) 作为输入的函数,而\(x^{<1>},a^{<1>}\) 都会被设为全为 0 的向量。于是 \(a^{<1>}\) 要做的就是它会通过 softmax 进行一些预测来计算出第一个词可能会是什么,结果为 \(\hat{y}^{<1>}\)。这一步其实就是通过一个 softmax 层来预测词典中任意单词会是第一个词的概率。
在下一时间步中,使用激活项 \(a^{<1>}\),然后输入 \(x^{<2>}\) 告诉模型,第一个词是 Cats,以此来计算第二个词会是什么。同理,输出结果同意经过 softmax 层进行预测,预测这些词的概率。以此类推。
为了训练这个网络,我们需要定义代价函数。在某个时间步 \(t\),如果真正的词是 \(y^{<t>}\),而神经网络的