《Sequence Models》课堂笔记
# 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>}\),接着就是前向传播过程。
循环神经网络用的激活函数经常是 tanh,偶尔也会用 ReLU。
前向传播公式的泛化公式如下,在 t 时刻
我们的符号约定,以 \(W_{ax}\) 为例,第二个下标意味着它要乘以某个 \(x\) 类型的量,然后第一个下标 \(a\) 表示它是用来计算某个 \(a\) 类型的变量。其他几个矩阵符号也是同理。
为了简化这些符号,我们可以简化一下,第一个计算 \(a^{<t>}\) 的公式可以写作
然后我们定义 \(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 >}\) 的计算,也可以写作
RNN 前向传播示意图如下。
穿越时间的反向传播
为了计算反向传播,我们先定义一个元素损失函数。
它对应的是序列中一个具体的词,如果它是某个人的名字,那么 \(y^{<t>}\) 的值为 1,然后神经网络将输出这个词是名字的概率值。它被定义为标准逻辑回归损失函数,也叫交叉熵损失函数 (cross entropy loss)。
整个序列的损失函数为
也就是把每个单独时间步的损失函数都加起来。
在这个反向传播过程中,最重要的信息传递或者说最重要的递归运算就是这个从右到左的运算,所以它被叫做穿越时间反向传播 (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>}\),而神经网络的 softmax 层预测结果值为 \(\hat{y}^{<t>}\)。那么 softmax 损失函数为
而总体损失函数为
也就是把所有单个预测的损失函数相加。
如果我们用很大的训练集来训练这个 RNN,那么我们可以通过开头一系列单词来预测之后单词的概率。假设一个新句子只有三个单词,那么这个句子的概率计算如下
对新序列采样
在训练一个序列模型之后,要想了解这个模型学到了什么,一种非正式的方法就是进行一次新序列采样。
我们要做的就是对这些概率分布进行采样来生成一个新的单词序列。
第一步要做的就是对我们想要模型生成的第一个词进行采样。输入 \(x^{<1>},a^{<1>}\) 为 0 向量,然后得到一个 softmax 结果,根据这个 softmax 的分布进行随机采样。也就是对这个结果使用 numpy 命令 (np.random.choice
)。
然后根据模型结构,以此类推。直到得到 EOS 标识或者达到所设定的时间步。如果不想采样到未知标识 UNK,可以拒绝采样到的未知标识,继续在剩下的词中进行重采样。
根据实际应用,也可以构建一个基于字符的 RNN 结构,这样字典仅包含从 a 到 z 的字母,也可以再包含一些标点符号,特殊字符,数字等。这样序列 \(y^{<1>},y^{<2>},\cdots\) 将会是单独的字符而不是单词。
这种结构优点是,我们不必担心会出现未知的标识。而一个主要缺点就是,最后会得到太多太长的序列,计算成本比较高昂。
门控循环单元 (Gated recurrent unit, GRU)
循环神经网络的梯度消失
对于下面两个句子。
The cat, which already ate ......, was full.
The cats, which ate ......, were full.
前面的名词和动词应该保持一致的单复数形式,但是基本的 RNN 模型不擅长捕获这种长期依赖效应。因为梯度消失问题,后面层的输出误差很难影响前面层的计算。
尽管梯度爆炸也是会出现,但是梯度爆炸很明显。因为指数级大的梯度会让参数变得极其大,以至于网络参数崩溃,我们会看到很多 NaN,这意味着网络计算出现了数值溢出。如果发现了梯度爆炸问题,一个解决办法就是用梯度修剪。梯度修剪的意思就是观察梯度向量,如果它大于某个阈值,缩放梯度向量,保证它不会太大。
GRU
标准的 RNN 单元如下图所示。
使用 GRU 可以使 RNN 更好地捕捉深层连接,并改善梯度消失问题。
仍然使用上面提到的单复数例子。GRU 会有个新的变量称为 \(c\),代表细胞 (cell),即记忆细胞。记忆细胞的作用是提供了记忆的能力,比如猫是单数还是复数,当它看到之后的句子的时候,它仍能够判断句子的主语是单数还是复数。于是在时间 \(t\) 处,有记忆细胞 \(c^{<t>}\),然后 GRU 实际输出了激活值 \(a^{<t>}\),且 \(c^{<t>}=a^{<t>}\)。
在每个时间步,我们将用一个候选值重写记忆细胞,即 \({\tilde{c}}^{<t>}\)。然后我们用 tanh 函数来计算
GRU 中真正重要的思想是我们有一个门,记为 \(\Gamma_{u}\),其中下标 \(u\) 代表更新 (update) 。它是一个 0 到 1 之间的值。它的计算方式如下
对于大多数可能的输入,sigmoid 函数的输出总是非常接近 0 或者 1,所以这个值大多数情况下也是非常接近 0 或 1 的。
所以 GRU 的关键部分就是使用 \(\tilde{c}\) 来更新 \(c\),然后使用门来决定是否真的要更新。即
GRU 的一个简化示意图如下。
因为 \(\Gamma_u\) 很接近 0,那么更新式子就会变成 \(c^{<t>}=c^{<t-1>}\)。也就是说,即使经过很多很多的时间步,\(c^{<t>}\) 的值也很好地被维持了,这就是缓解梯度消失问题的关键。
而对于一个完整的 GRU,我们需要在计算第一个式子中给记忆细胞的新候选值加上一个新的项。我们要添加一个新的门 \(\Gamma_r\),其中下标 \(r\) 可以代表相关性 (relevance)。这个门的作用是告诉我们,计算出的下一个 \(c^{<t>}\) 的候选值 \(\tilde{c}^{<t-1>}\) 与 \(c^{<t-1>}\) 有多大的相关性。它的计算方式如下
那么完整的 GRU 计算公式则为
长短期记忆单元 (long short term memory unit, LSTM unit)
LSTM 是一个比 GRU 更加强大和通用的版本。
LSTM 的主要公式如下
在 LSTM 中,我们不再有 \(a^{<t>}=c^{<t>}\),我们专门使用 \(a^{<t>}\) 或者 \(a^{<t-1>}\),而不是用 \(c^{<t-1>}\),也不再用相关门 \(\Gamma_r\)。LSTM 保留了更新门,但不仅仅由更新门来控制,加入了遗忘门 (the forget gate) \(\Gamma_f\) 和输出门 (the output gate) \(\Gamma_o\)。
所以给了记忆细胞选择权去维持旧的值 \(c^{<t-1>}\) 或者加上新的值 \(\tilde{c}^{<t>}\)。
LSTM 示意图如下。
可以发现在上图中的序列中,上面有条线显示了只要正确地设置了遗忘门和更新门,LSTM 是很容易把 \(c^{<0>}\) 的值一直往下传递的。当然,这个图示和一般使用的版本有些许不同。最常用的版本的门值不仅取决于 \(a^{<t-1>}\) 和 \(x^{<t>}\),偶尔也可以偷窥一下 \(c^{<t-1>}\) 的值(上图中编号 13),这叫做窥视孔连接 (peephole connection)。
LSTM 前向传播图:
LSTM 反向传播计算:
门求偏导
参数求偏导
为了计算 \(db_f, db_u, db_c, db_o\),需要各自对 \(d\Gamma_f^{\langle t \rangle}, d\Gamma_u^{\langle t \rangle}, d\tilde c^{\langle t \rangle}, d\Gamma_o^{\langle t \rangle}\) 求和。
最后,计算隐藏状态、记忆状态和输入的偏导数。
什么时候用 GRU,什么时候用 LSTM,其实没有统一的标准。
GRU 的优点是,它是个更加简单的模型,所以容易创建一个更大的网络,而且它只有两个门,在计算性上也运行得更快,然后它可以扩大模型的规模。
但是 LSTM 更加强大和灵活。现在大部分的人还是会把 LSTM 作为默认的选择来尝试。
双向循环神经网络
我们以一个只有 4 个单词的句子为例。那么这个网络会有一个前向的循环单元为 \({\overrightarrow{a}}^{<1>},{\overrightarrow{a}}^{<2>},{\overrightarrow{a}}^{<3>},{\overrightarrow{a}}^{<4>}\),这四个循环单元输入,都会得到对应的输出 \(\hat{y}^{<1>},\hat{y}^{<2>},\hat{y}^{<3>},\hat{y}^{<4>}\)。
接下来,我们增加一个反向循环层,\({\overleftarrow{a}}^{<1>},{\overleftarrow{a}}^{<2>},{\overleftarrow{a}}^{<3>},{\overleftarrow{a}}^{<4>}\),同样这一层也向上连接。这样,这个网络如下所示。先前向计算,然后再反向计算,把所有激活值都计算完了就可以计算预测结果了。
这些单元可以是标准 RNN 单元,也可以是 GRU 或者 LSTM 单元。而且实践中,很多 NLP 问题,有 LSTM 单元的双向 RNN 模型是用得最多的。
BRNN 的缺点就是需要完整的数据序列,才能预测任意位置。
深层循环神经网络 (Deep RNNs)
一个标准的神经网络,首先是输入 \(x\),然后堆叠上隐含层。深层 RNN 类似,堆叠隐含层,然后每层按时间展开就是了,如下图所示。
对于标准的神经网络,可能有很深的网络,但是对于 RNN 来说,有三层就已经不少了。由于时间的维度,RNN 网络会变得相当大。
词嵌入 (Word embedding)
词嵌入是语言表示的一种方式,可以让算法自动的理解一些类似的词。比如男人对女人,国王对王后等等。
之前我们是用词典的 one-hot 向量来表示词,比如说 man 在词典中第 5391 个位置,那么它的 one-hot 向量标记为 \(O_{5391}\)。这种表示方法的一大缺点就是它把每个词都孤立起来了,使得算法对相关词的泛化能力不强。
举个例子,我们的语言模型已经学习到了 "I want a glass of orange juice",但是当它看到 "I want a glass of apple ____" 时,算法可能无法填出 juice 这个单词。算法不知道 apple 和 orange 的关系很接近,因为任何两个 one-hot 向量的内积都是 0。
但是如果我们用特征化来表示每个词,假如说这些特征维度 Gender, Royal, Age 等等,这样对于不同的单词,算法会泛化得更好。
当然,我们最终学习的特征可能不会像 Gender, Royal 等这些比较好理解,甚至不太好用实际意义去解释。
接下来,我们可以把词嵌入应用到命名实体识别任务当中,尽管我们可能只有一个很小的训练集,100,000 个单词,甚至更小。我们可以使用迁移学习,把互联网上免费获得的大量的无标签文本中学习到的知识迁移到一个任务中。
所以,如何用词嵌入做迁移学习的步骤如下:
- 先从大量的文本集中学习词嵌入。一个非常大的文本集,或者可以下载网上预训练好的词嵌入模型,网上可以知道不少,而且词嵌入模型一般都有许可。
- 用这些词嵌入模型迁移到我们的新的只有少量标注训练集的任务中,比如说用一个 300 维的词嵌入来表示单词。
- 在新的任务上训练模型,可以选择要不要继续微调,用新的数据调整词嵌入。当然,一般来说,只有新数据有比较大的数据量时,才会进行微调。
假如说我们以这四个维度的特征来表征词。词的特征向量都以符号 \(e\) 表示。
那么
可以发现这两组向量相减得到的向量基本一致。也就表明,这两对词都只在 gender 这个特征维度有显著差异。
我们可以使用余弦相似度来表征这些向量的相似度。
这样,我们就能通过计算相似度来找到相近的词。
当我们应用算法来学习词嵌入时,其实是在学习一个嵌入矩阵 (embedding matrix)。
假设我们的词典有 10,000 个单词,我们要做的就是学习一个嵌入矩阵 \(E\),它将是一个 \(300\times10,000\) 的矩阵。这个矩阵的各列代表的是词典中 10,000 个单词所代表的特征向量。
学习词嵌入
建立一个语言模型是学习词嵌入的好方法。
如何建立神经网络来预测序列中的下一个单词呢?首先,以下图中的句子为例。先使用 one-hot 向量表示这些单词,然后生成一个参数矩阵 \(E\),用 \(E\) 乘以 one-hot 向量 \(o\),这样得到嵌入向量 \(e\)。于是我们有了很多 300 维的嵌入向量,把它们放进神经网络中,然后再通过一个 softmax 层,然后 softmax 分类器会在 10,000 个可能的输出中预测结尾这个单词。
实际上,更常见的是有一个固定的历史窗口。举个例子,我们总是想预测给定四个单词(也可以是其他的个数)后的下一个单词,这样就可以适应很长或者很短的句子。用一个固定的历史窗口意味着可以处理任意长度的句子,因为输入的维度总是固定的。所以,这个模型的参数就是矩阵 \(E\),对所有的单词用的都是同一个矩阵 \(E\)。
当然除了选前四个单词,还有其他的上下文构建方式。但是建立语言模型,用目标词的前几个单词作为上下文是常见做法。
Word2Vec
假设在训练集中给定了一个这样的句子 "I want a glass of orange juice to go along with my cereal.",在 skip-gram 模型中,我们要做的是抽取上下文和目标词配对,来构造一个监督学习问题。上下文不一定总是目标单词之间离得最近的四个单词或 n 个单词。
我们要做的是随机选一个词作为上下文词,然后随机在一定词距内选另一个词作为目标词。于是我们将构造一个监督学习问题,它给定上下文词,要求预测在这个词一定词距内随机选择的某个目标词。显然,这不是个非常简单的学习问题。但是,构造这个监督学习问题的目标并不是想要解决这个监督学习问题本身,而是想要使用这个学习问题来学到一个好的词嵌入模型。
我们要解决的基本的监督学习问题是学习一种映射关系,从上下文 \(c\) 到某个目标词 \(t\)。从 one-hot 向量 \(O_c\) 开始,然后乘以嵌入矩阵 \(E\) 得到上下文词的嵌入向量,\(e_c=EO_c\)。接着,把向量 \(e_c\) 喂入 softmax 单元,输出 \(\hat{y}\),预测不同目标词的概率:
其中 \(\theta_t\) 是一个与输出 \(t\) 有关的参数,即某个词 \(t\) 和标签相符的概率是多少,这里省略了 softmax 中的偏差项,想要加上的话也是可以加上的。
于是 softmax 的损失函数为
矩阵 \(E\) 将会有很多参数,优化这个关于所有这些参数的损失函数,就能得到一个较好的嵌入向量集。这个就叫做 skip-gram 模型。
这个算法首要的问题就是计算速度,尤其是在 softmax 模型中,每次要计算这个概率,就要对词典中所有词做求和计算,这个求和操作是相当慢的。
这里有一些解决方案,如分级 (hierarchical) 的 softmax 分类器和负采样 (Negative Sampling)。
分级 softmax 分类器
这个分类器的意思是,通过一层一层的节点来分类词。这样计算成本与词典大小的对数成正比,而不是词典大小的线性函数。在实践中,不会使用一棵完美平衡的分类树或者说一棵左边和右边分支的词数相同的对称树,而是会被构造成常用词在顶部,不常用的词在树的更深处。这是一种加速 softmax 分类器的方法。
负采样
这个算法要做的是构造一个新的监督学习算法。给定一对单词来预测者是否是一对上下文词-目标词 (context-target)。
比如,orange 和 juice 为一对正样本,orange 和 king 为一对负样本。我们要做的就是采样得到一个上下文词和一个目标词。正样本的生成方式与 word2vec 类似,先抽取一个上下文词,在一定词距内选一个目标词,标记为 1。然后为了生成一个负样本,我们将用相同的上下文词,再在字典中随机选一个词,标记为 0。如果我们挑选负样本的时候,从字典中随机选到的词,正好出现在了词距内,但是我们标记为负样本也没关系。
然后,我们将构造一个监督学习问题。我们的算法输入词对,预测其标签。
K 值的选取。论文作者推荐小数据集的话,K 从 5 到 20 比较好;如果数据集很大,K 就选的小一点,如 K 等于 2 到 5。这个例子中,我们使 \(K=4\)。
我们定义一个逻辑回归模型,给定输入的 \(c,t\) 对(上下文词 \(c\) 和目标词 \(t\))的条件下输出 \(y=1\) 的概率,即
把它画成一个神经网络,如果输入词是 orange,即第 6257 个词,那么输入它的 one-hot 向量,乘以嵌入矩阵 \(E\),获得嵌入向量 \(e_{6257}\)。这样,我们得到了 10,000 个可能的逻辑回归分类问题。其中一个是用来判断目标词是否是 juice 的分类器。但不是每次迭代都训练全部 10,000 个,\(K=5\) 时,我们只训练其中的 5 个。训练对应真正目标词那一个分类器,再训练 4 个随机选取的负样本,所以不使用一个巨大的 softmax,而是把它转变为多个二分类问题。二分类问题每个都很容易计算,而且每次迭代只要训练它们其中的几个。
其中一个重要的细节就是如何选取负样本。一个方法是根据语言中的经验频率对这些词进行采样,但是 like, the, of, and 这种词有很高的频率。另一个就是用 1 除以词典总词数,即 \(\frac{1}{|v|}\),均匀且随机地抽取负样本,但是这对于英文单词的分布是非常没有代表性的。作为一个折中,论文作者根据经验,采用以下方式进行采样,也就是实际观察到的英文文本的分布:
也就是 \(f(w_i)\) 是观测到的在语料库中的某个单词的词频,通过 \(\frac{3}{4}\) 次方的计算,使其处于完全独立的分布和训练集的观测分布两个极端之间。
GloVe 词向量
GloVe 算法不如 Word2Vec 或是 Skip-Gram 模型用的多,但是也有研究者热衷于它,可能是因为其简便性。
GloVe 代表用词表示的全局变量。还是挑选语料库中位置相近的两个词,即上下文-目标词。GloVe 算法做的就是使其关系开始明确化。假设 \(X_{ij}\) 是单词 \(i\) 在单词 \(j\) 上下文中出现的次数,那么这里 \(i\) 和 \(j\) 就和 \(t\) 和 \(c\) 的功能一样。
如果对于上下文的定义是目标词一定范围词距的单词,那么 \(X_{ij}=X_{ji}\);而如果对于上下文的定义为目标词的前一个单词,那么 \(X_{ij}\) 和 \(X_{ji}\) 就不会相同。
不过对于 GloVe 算法,我们可以定义上下文和目标词为任意两个位置相近的单词,假设是左右各 10 词的距离,那么 \(X_{{ij}}\) 就是一个能够获取单词 \(i\) 和单词 \(j\) 出现位置相近时或是彼此接近的频率的计数器。GloVe 模型做的就是进行优化,将它们之间的差距进行最小化处理。
而如果 \(X_{ij}=0\) 的话,\(log0\) 为未定义,为负无穷大。所以公式中加上了一个额外的加权项 \(f(X_{ij})\),这样 \(X_{ij}=0\) 时,我们有 \(0log0=0\)。这个加权项还有一个作用是,有些词在英语中出现十分频繁如 this, is, of, a 等,它们被叫作停止词,加权项可以给予大量有意义的运算给不常用词,同样给停止词更大但不至于过分的权重。因此,有一些对加权函数 \(f\) 的选择有着启发性的原则。
情感分类 (Sentiment Classification)
情感分类任务就是看一段文本,然后分辨这个人是否喜欢他们在讨论的这个东西,这是自然语言处理中最重要的模块之一,经常用在许多应用中。情感分类一个最大的挑战就是可能标记的训练集没有那么多,但是有了词嵌入,即使只有中等大小的标记的训练集,也能构建一个不错的情感分类器。
下图是一个简单的情感分类模型。假设输入为 "The dessert is excellent",我们从词典中取出这些词,然后形成 one-hot 向量,乘以嵌入矩阵 \(E\) 来获取嵌入向量。其中嵌入矩阵可以从很大的训练集上训练获得。接着,对这些嵌入向量进行求和或者平均,就会得到一个特征向量,把它输入 softmax 分类器,输出 \(\hat{y}\) 也就是一星到五星的概率值。
这个算法运用的平均值运算单元适用于任何长短的评论,它实际上会把所有单词的意思给平均起来。
这个算法有一个问题就是没有考虑词序,尤其是这样一个负面的评价。
"Completely lacking in good taste, good service, and good ambiance."
这个句子中出现了很多 good,分类器很可能会认为这是一个好的评价。
这样,我们有一个更加复杂的模型来处理,使用 RNN 来做情感分类。如下图所示。
词嵌入纠偏
一个已经完成学习的词嵌入可能会输出Man:Computer Programmer,同时输出Woman:Homemaker,那个结果看起来是错的,并且它执行了一个十分不良的性别歧视。因此根据训练模型所使用的文本,词嵌入能够反映出性别、种族、年龄、性取向等其他方面的偏见,一件我尤其热衷的事是,这些偏见都和社会经济状态相关,我认为每个人不论你出身富裕还是贫穷,亦或是二者之间,我认为每个人都应当拥有好的机会,同时因为机器学习算法正用来制定十分重要的决策,它也影响着世间万物,从大学录取到人们找工作的途径,到贷款申请,不论你的的贷款申请是否会被批准,再到刑事司法系统,甚至是判决标准,学习算法都在作出非常重要的决策,所以我认为我们尽量修改学习算法来尽可能减少或是理想化消除这些非预期类型的偏见是十分重要的。
假设说我们已经完成一个词嵌入的学习,先我们要做的事就是辨别出我们想要减少或想要消除的特定偏见的趋势。
以性别偏见为例。主要有以下三个步骤。
-
对于性别偏见来说。我们将一些性别相关的词对进行嵌入向量相减,如 \(e_{he}-e_{she},e_{male}-e_{female}\),然后将这些值取平均。这个趋势,看起来就是性别趋势,但是与我们想要处理的特定偏见无关,所以这就是个无偏的性别趋势。实际上,它会用一个更加复杂的算法——奇异值分解(SVU),和主成分分析很类似。
-
中和步骤。对于那些定义不确切的词可以将其处理一下。如 grandmother, grandfather 这些词定义中本来就含有性别意义,而 doctor, babysitter 这些词我们希望它是中立的。所以对于中立词,我们想要减少他们在水平方向上的距离。
- 均衡步。意思是说你可能会有这样的词对,grandmother和grandfather,或者是girl和boy,对于这些词嵌入,你只希望性别是其区别。那为什么要那样呢?在这个例子中,babysitter和grandmother之间的距离或者说是相似度实际上是小于babysitter和grandfather之间的(上图编号1所示),因此这可能会加重不良状态,或者可能是非预期的偏见,也就是说grandmothers相比于grandfathers最终更有可能输出babysitting。所以在最后的均衡步中,我们想要确保的是像grandmother和grandfather这样的词都能够有一致的相似度,或者说是相等的距离,和babysitter或是doctor这样性别中立的词一样。这其中会有一些线性代数的步骤,但它主要做的就是将grandmother和grandfather移至与中间轴线等距的一对点上(上图编号2所示),现在性别歧视的影响也就是这两个词与babysitter的距离就完全相同了(上图编号3所示)。所以总体来说,会有许多对像grandmother-grandfather,boy-girl,sorority-fraternity,girlhood-boyhood,sister-brother,niece-nephew,daughter-son这样的词对,我们可能想要通过均衡步来解决它们。
均衡背后的关键思想是确保一对特定的单词与49维\(g_\perp\)距离相等 。均衡步骤还可以确保两个均衡步骤现在与\(e_{receptionist}^{debiased}\) 距离相同,或者用其他方法进行均衡。下图演示了均衡算法的工作原理:
主要步骤如下:
序列模型
基础模型
如何构建一个网络来实现机器翻译呢?比如实现输出法语句子 "Jane visite I'Afrique en septembre.",输出英语句子 "Jane is visiting Africa in September."。
首先,建立一个网络,这个网络叫编码网络 (encoder network),如下图编号 1 所示。它是一个 RNN 的结构,RNN 的单元可以是 GRU 也可以是 LSTM。每次只向该网络中输入一个法语单词,将输入序列接收完毕后,这个 RNN 网络会输出一个向量来代表这个输入序列。在这个网络后面,我们建立一个解码网络 (decoder network),如下图编号2所示。它以编码网络的输出作为输入,被训练为每次输出一个翻译后的单词,一直到它输出序列的结尾或者句子结尾标记。
这个模型简单地用一个编码网络来对输入的法语句子进行编码,然后用一个解码网络来生成对应的英语翻译。
与此类似的结构也被用来做图像描述,给出一张图片,如下图中的猫的图片,它能自动地输出该图片的描述:一只猫坐在椅子上。
我们之前已经知道如何将图片输入到卷积神经网络中,比如一个预训练的 AlexNet 结构(上图编号 2)。然后让其学习图片的编码或者学习图片的一系列特征,也就是去掉 softmax 单元(上图编号 3)后的部分会输出一个 4096 维的特征向量,也就是一个图像的编码网络。然后把这个向量输入到 RNN 中(上图编号 4),使用 RNN 来生成图像的描述。
选择最可能的句子
我们可以把机器翻译看成是建立一个条件语言模型,在语言模型中上方是一个我们之前建立的模型,这个模型可以估计句子的可能性,也就是语言模型所做的事情。而机器翻译分为两部分:编码网络(下图绿色)和解码网络(下图紫色),而我们发现解码网络其实和语言模型几乎一模一样。不同在于语言模型总是以零向量(下图编号 4)开始,而机器翻译的编码网络会计算出一系列向量(下图编号 2)来表示输入的句子,解码网络则以这个句子的特征开始,而不是零向量。所以吴恩达老师称之为条件语言模型 (conditional language model)。
我们想实现真正地通过模型将法语翻译成英文,通过输入的法语句子得到各种英文翻译所对应的可能性。\(x\) 在这里是法语句子 "Jane visite I'Afrique en septembre"。我们不想让模型随机地输出,即从得到的分布中进行随机取样,而是找到一个英语句子 \(y\),使得条件概率最大化。
解决这种问题,最通用的算法就是集束搜索 (Beam Search),而不用贪心搜索 (Greedy Search)。
贪心搜索指的是一种来自计算机科学的算法。生成第一个词的分布以后,它将会根据条件语言模型挑选出最有可能的第一个词进入机器翻译模型中,然后继续挑选最有可能的第二个词,接着一直往后挑选最有可能的词。
但是我们真正需要的是一次性挑选出整个单词序列,从 \(y^{<1>},y^{<2>}\) 到 \(y^{<T_y>}\) 来使得整体的概率最大化。所以贪心算法并不管用。
上图中编号 1 的翻译明显比编号 2 的好,所以我们希望机器翻译模型会输出第一个句子的 \(P(y|x)\) 比第二个句子要高。但如果使用贪心算法来挑选出了 "Jane is" 作为前两个词,因为在英语中 going 更加常见,所以模型会选择 "Jane is going" 而不是 "Jane is visiting" 作为翻译,最终得到一个欠佳的句子。
集束搜索 (Beam Search)
集束搜索算法首先做的就是挑选要输出的英语翻译中的第一个单词,为了简化问题,我们忽略大小写,列出了 10,000 个词的词汇表。集束搜索的第一步是用这个网络(绿色是编码网络;紫色是解码网络),来评估第一个单词的概率值。给定输入序列 \(x\),即法语句子,输出 \(y\) 的概率值是多少。
贪婪算法只会挑选最可能的一个单词,然后继续,而集束搜索则会考虑多个选择。集束搜索算法会有一个参数 \(B\),称为集束宽 (beam width)。本例中我们设为 3,意味着集束搜索一次会考虑 3 个词,然后把结果存在计算机内存里以便后面尝试使用这三个词。
假设我们选出了第一个单词三个最有可能的选择为 in, jane, september,集束搜索的第二步会针对每个第一个单词考虑第二个单词是什么,如下图编号 1。为了评估第二个词的概率值,我们用神经网络,绿色是编码部分(下图编号 2)。对于解码部分,当决定单词 in 后面是什么时,解码器的第一个输出 \(y^{<1>}\) 为 in (下图编号 3),然后把它喂回下一个网络单元(下图编号 4)。这里的目的是找出第一个单词是 in 的情况下,第二个单词是什么,即 \(y^{<2>}\) (下图编号 5)。
在第二步中,我们更关心的是要找到最可能的单词对(下图编号 7),而不仅仅是最大概率的第二个单词。按照条件概率的准则,单词对的概率可以表示为第一个单词的概率(下图编号 8)乘以以第一个单词为条件的第二个单词的概率(下图编号 9),而后者可以从编号 10 的网络中得到。
同理,对于第一个单词的第二个备选 "jane" ,第三个备选 "september" 也是同样的步骤。由于我们一直用的集束宽为 3,并且词汇表里有 10,000 个单词,那么最终会有 \(3\times10,000\) 也就是 30,000 个可能的结果。然后依旧按照单词对的概率选出前三个,减少到集束宽的大小。集束搜索算法会保存这些结果,然后用于下一次集束搜索。
接下来的步骤,继续选择与第二步类似。值得注意的是,如果集束宽等于 1,只考虑一种可能结果,这实际上就变成了贪婪搜索算法。
改进集束搜索
有一些小技巧可以帮助集束搜索算法运行的更好。
长度归一化 (length normalization) 就是对集束搜索算法稍作调整的一种方式。集束搜索其实就是最大化
而连乘的乘积其实就是 \(P(y^{<1>},\dots,y^{<{T_y}>}|x)\)。如果计算它,其实相乘的这些概率值都是小于 1 的,通常远小于 1。而很多小于 1 的数相乘,会得到很小很小的数字,会造成数值下溢 (numerical underflow)。指的是数值太小了,导致电脑的浮点表示不能精确地存储。因此在实践中,我们取 log 值,从而得到一个数值上更稳定的算法。即
对于目标函数,还可以做一些改变,可以使得机器翻译表现得更好。如果使用上面的目标函数,那么对于一个很长的句子,这个句子的概率会很低,因为乘了很多项小于 1 的数字。所以这个目标函数有一个缺点是,它可能不自然地倾向于简短的翻译结果。我们可以不再最大化这个目标函数,而是对其进行归一化,通过除以翻译结果的单词数 \(T_y\)。这样就是取每个单词的概率对数值的平均了,这样很明显地减少了对输出长的结果的惩罚。即
上式中的参数 \(\alpha\),可以使得归一化更加柔和,\(\alpha\) 可以等于 0.7。如果 \(\alpha\) 等于 1,就相当于完全用句子长度来归一化,如果 \(\alpha\) 等于 0,就相当于完全没有归一化。它就是算法另一个超参数,需要调整大小来得到最好的结果。
对于如何选择集束宽参数 \(B\)。\(B\) 越大,考虑的选择越多,找到的句子可能越好;但是算法的计算代价也会越大,算法会运行得慢一些,内存占用也会增大。在实践中,其实使用 \(B=3\) 有点偏小。在生产中,经常可以看到把集束宽设为 10,集束宽为 100 对于生产系统来说有点过大;但对于科研来说,人们想获得最好的结果用来发表论文,所以经常可以看到集束宽为 1,000 甚至 3,000。对很多应用来说,从集束宽为 1,到 3,到 10,可能可以看到一个很大的提升;但是当集束宽从 1,000 增加到 3,000 时,效果可能就没那么明显了。
集束搜索的误差分析
以下面的例子来说明。
仍然需要翻译法语句子 "Jane visite I'Afrique en septembre"。假设机器翻译的 dev 集中,也就是开发集 (development set),人工是这样翻译的 "Jane visits Africa in September",记为 \(y^*\)。当已经完成学习 RNN 模型,也就是已完成学习的翻译模型中运行集束搜索算法时,它输出的翻译为 "Jane visited Africa last September",记为 \(\hat{y}\)。
我们的模型有两个主要部分:RNN 模型和集束搜索算法。现在,我们想要找出造成输出 \(\hat{y}\) 这个不太好的翻译的原因。
RNN 实际上是个编码器和解码器,它会计算 \(P(y|x)\)。我们可以使用这个模型来计算 \(P(y^*|x)\) 和 \(P(\hat{y}|x)\),然后比较一下这两个值哪个更大。
第一种情况: \(P(y^*|x)>P(\hat{y}|x)\)
这种情况下,意味着集束搜索选择了 \(\hat{y}\),也就是集束搜索算法此时不能够输出一个使 \(P(y|x)\) 最大化的 \(y\) 值,因为集束搜索算法的目的就是寻找一个 \(y\) 值来使它更大。
因此这种情况下,我们能够得出是集束搜索算法出错了。
第二种情况: \(P(y^*|x)\le P(\hat{y}|x)\)
这种情况下,意味着相比与 \(\hat{y}\),\(y^*\) 成为输出的可能性更小,但是后者其实上是比前者更好的翻译结果。也就是说,这种情况下,是 RNN 模型出了问题。
所以误差分析的过程其实就如下图这样。先遍历开发集,然后在其中找出算法产生的错误。通过这个过程,我们就能够执行误差分析,得出集束搜索算法和 RNN 模型出错的比例,来指导模型的优化。
Bleu 得分
Bleu 代表的是 bilingual evaluation understudy (双语评估替补),这是一种常见的衡量机器翻译的准确性的方法。
假如我们有一个法语句子 "Le chat est sur le tapis",然后其对应的一个人工翻译参考为 "The cat is on the mat"。不过有多种相当不错的翻译。所以其他的人,也许会翻译为 "There is a cat on the mat"。实际上,这两个都是很好的翻译。Bleu 得分做的就是,给定一个机器生成的翻译,它能够自动地计算一个分数来衡量机器翻译的好坏。直觉告诉我们,只要这个机器生成的翻译与任何一个人工翻译的结果足够接近,那么它就会得到一个高的 Bleu 分数。
我们以一个极端的例子为例。假设机器翻译 (MT) 的输出是 "the the the the the the the"。这显然是一个十分糟糕的翻译。衡量机器翻译输出质量的方法之一,是观察输出结果的每一个词看其是否出现在参考中,这杯称作是机器翻译的精确度。这种情况下,机器翻译输出了七个单词并且这七个词中的每一个都出现在了参考 1 或是参考 2。单词 the 在两个参考中都出现了,所以看上去每个词都是很合理的,即这个精确度就是 \(\frac{7}{7}\),看起来是一个极好的精确度。
所以这种方法并不是很有用,将其进行改良,我们把每一个单词的计分上限定为它在参考句子中出现的最多次数。在参考 1 中,单词 the 出现了两次;参考 2 中,单词 the 出现了一次。所以单词 the 的得分上限为 2。那么这个改良后的精确度为 \(\frac{2}{7}\)。分母为 7 个词中单词 the 总共出现的次数,分子为单词 the 在参考中的出现的计数。
到目前为止,我们都只是关注单独的单词。如果我们想考虑成对的单词,定义一下二元词组 (bigrams) 的 Bleu 得分。当然这仅仅只是最终的 Bleu 得分的一部分,可能会考虑单个单词以及二元或多元词组。在下面的例子中,我们分别统计 MT 输出的二元词组在 MT 输出和参考中的计数。因此 \(\frac{4}{6}=\frac{2}{3}\) 为二元词组的改良后的精确度。
现在我们将其泛化为 n 元词组,其精确度定义为
最终的 Bleu 得分被定义为(以综合 \(P_1,P_2,P_3,P_4\) 为例)
实际上还会用到额外的一个叫做 \(BP\) 的惩罚因子来调整,其意思为简短惩罚 (brevity penalty)。那么定义则为
注意力模型 (Attention Model)
像下图这样一个很长的法语句子,我们的神经网络中,绿色部分的编码器要做的就是读整个句子,然后记忆整个句子,再在感知机中传递。而对于紫色部分的解码器,它将生成英文翻译。
但是人工翻译并不会通过读整个法语句子,再记忆里面的东西,然后从零开始翻译成英语句子。人工翻译会一部分一部分地翻译,因为记忆整个句子是非常困难的。对于机器翻译来说也是如此,对于短句子效果可能非常好,有相对高的 Bleu 分数,但是对于长句子,它的表现就会变差。
注意力模型源于机器翻译,但也推广到了其他应用领域。
仍然以法语句子 "Jane visite I'Afrique en Septerbre" 为例。假定我们使用一个双向的 RNN,为了计算每个输入单词的特征集。它可以使用 GRU 或者 LSTM 作为基本单元,实践中, LSTM 使用得更为经常一些。然后,使用另一个 RNN 生成对应的英文翻译,我们使用记号 \(S\) 表示这个 RNN 的隐藏状态而不用 \(A\)。
当我们尝试生成英文翻译的第一个词时,我们应该看对应法语句子的第一个单词及它附近的词。所以注意力模型就会计算注意力权重,我们使用 \(\alpha^{<1,1>}\) 来表示当生成第一个词时,注意力放在第一块信息处的权重。对应的有 \(\alpha^{<1,2>},\alpha^{<1,3>}\)。把他们综合起来作为翻译第一个词的上下文语境,记为 \(C\),这就是这个 RNN 的一个单元。其他单词以此类推,直到最终生成 <EOS>。
再次说明,注意力权重 \(\alpha^{<t,t>}\) 表示的是,生成第 t 个英文词时,需要花多少注意力在第 t 个法语词上面。
我们仍然使用 \(t\) 来表示时间步,\(a^{<t>}\) 就是时间步 \(t\) 上的特征向量。使用 \(t'\) 来索引法语句子里面的词。那么 \(t=1\) 时的上下文语境,就是通过计算注意力权重(上图编号 1)和其对应的特征向量(上图编号 2)的乘积和。即
注意,在一个时间步中,所有的注意力权重均为非负,且它们的和为 1,即
\(\alpha^{<t,t'>}\) 是花费在 \(a^{<t'>}\) 上的注意力权重。它的公式如上图所示。计算它之前,我们需要先计算 \(e^{<t,t'>}\),关键要用 softmax 以确保这些权重加起来等于 1。
计算 \(e\) 值可以训练一个上图所示的小型的神经网络。我们不知道具体的函数去计算它,但是可以使用梯度下降算法计算一个正确的函数。
这个算法的一个缺点就是它要花费三次方的时间,也就是说这个算法的复杂度是 \(O(n^3)\)。但是在机器翻译的应用上,输入和输出的句子一般不会太长,可能三次方的消耗也是可以接受的。
语音识别 (Speech recognition)
语音识别问题指的是,输出音频片段 \(x\) 自动地生成文本 \(y\)。
我们使用注意力模型来构建语音识别系统。就是在横轴上,也就是输入音频的不同时间帧上,用注意力模型来输出文本描述。
也可以使用 CTC 损失函数来做语言识别,其中 CTC 指的是 Connectionist Temporal Classification。
其算法思想如下:
假设语言片段内容为 "the quick brown fox",这时我们使用一个新的网络,结构如上图所示。输入的 \(x\) 与输出 \(y\) 的长度是一样的,示例的只是一个简单的单向 RNN 结构。在实践中,它可以是双向的 LSTM 或 GRU,并且通常是很深的模型。注意,这里时间步的数量非常大。在语音识别中,通常输入的时间步数量要比输出的时间步数量多出很多。这种情况下,CTC 损失函数允许 RNN 生成类似这样的输出 "ttt",然后一个空白符,我们以下划线表示,然后 "h_eee___" 等。这样的输出(如上图所示)对应的就是 "the q"。这样,需要输出的内容其实只有 19 个字符,但是神经网络允许有很多这种重复的字符和很多插入在其中的空白符,使得它能强制输出 1000 个字符。
触发词检测 (Trigger Word Detection)
现在有很多智能系统有其对应的触发词模块,如下图所示。
对于触发词检测,最好的算法是什么,目前还没有一个广泛的定论。
我们以一个算法为例。现在有一个 RNN 结构,我们需要把一个音频片段计算出它的声谱图特征 (spectrogram features) 得到特征向量 \(x^{<1>},x^{<2>},\dots\)。然后,把它放到另一个 RNN 中,再定义目标标签 \(y\)。假如音频片段中的某一点为刚刚说完一个触发词,那么之前的目标标签都设为 0,这点之后对应触发词的音频特征设为 1。这样的标签方案对于 RNN 来说是可行的,并且确实运行得不错。不过该算法一个明显的缺点就是它构建了一个很不平衡的训练集,0 的数量比 1 多太多了。
这里有一个解决方法,虽然听起来有点简单粗暴,但确实能使其变得更容易训练。比起只在一个时间步上去输出 1,其实你可以在输出变回 0 之前,多次输出 1,或说在固定的一段时间内输出多个 1。这样的话,就稍微提高了 1 与 0 的比例。
References