《吴恩达深度学习笔记》之RNN
单独给RNN放在一篇,是工作有点相关加上上一篇太长编辑开始卡顿:)
https://zhuanlan.zhihu.com/p/540389017 笔记依旧参考了。
循环神经网络
1. 循环神经网络基础
上一门课中,我们学习了如何用CNN处理网络状的数据。由于最常见的网格数据是图像,我们主要学习了如何用CNN完成和图像相关的任务。而在这一门课中,我们学习如何用循环神经网络(RNN)等序列模型处理序列数据。序列数据的种类就比较丰富多彩了:

如上图所示,对于语音识别,音乐生成,情绪分类,DNA序列分析,机器翻译,视频动作识别,命名实体识别这些任务,他们的输入和输出至少有一个是某类序列数据,它们都可以用序列模型来建模。
计算机科学中自然语言处理(NLP)任务常常需要使用序列模型。我们在学这门课时,主要围绕NLP问题进行讨论。
1.1 符号标记
序列数据需要用到一些新的符号标记,在开始正式学习之前,我们先以命名实体识别任务为例,认识一下这些新的符号标记。
序列数据需要用到一些新的符号标记,在开始正式学习之前,我们先以命名实体识别任务为例,认识一下这些新的符号标记。
命名实体识别任务要求找出句子中有意义的人名、地名等特殊名词。以这个任务的一个训练样本为例,我们来人一看序列数据的符号标记。
设输入是 Harry Potter and Hermione Granger invented a new spell。
命名实体识别任务的输出y也是一个序列。序列的每一个元素是1或0,表示输入中对应的单词是否为命名实体。刚刚那个输入对应的输出应该是1 1 0 1 1 0 0 0 0。
输入输出都有单词或数字组成的序列。设输入为,序列中的第i个单词记做
,例如
=Potter,整个序列的长度
,同理,输出序列中的第i 个元素记做
,整个序列的长度
。

在表示这些数据时,怎么表示一个单词?计算机不认识一大堆字母。为了让只懂数字的计算机能分辨不同的单词,我们要先准备一个词汇表,并用单词在词汇表例的one-hot编码作为单词的表示。比如Harry是长度为10000的词汇表里第4075个单词,则Harry的表示是,这个向量的长度是10000,只有第4075个元素是1,其他地方都是0。

有了这些符号标记,和序列数据有关的任务就可以完全用数学语言表示了。比如对于命名实体识别任务,每一条样本输入是一个向量序列,输出是一个01的数字序列。
1.2 循环神经网络模型
在序列问题上使用标准神经网络(全连接网络)会有几个问题:
- 每个样本的长度可能不一致。尽管我们可以找到一个最大的长度,并对长度不足的样本进行零填充,但这种做法看上去就不太好。
- 同一个单词在不同的位置时应该算出类似的特征。而标准神经网络会把每个位置的输入区别对待,无法共享各个位置的知识。
- 和图像数据类似,序列数据的输入长度也很大。假如一个句子有10个单词,词汇表的大小时10000,则输入向量大小就是100000,这样,神经网络第一层参数会很大,网络会过拟合。

因此,我们要用一些其他的架构来处理序列数据以规避这些问题。其中一种可行的架构就是循环神经网络(RNN)。

如图所示,在RNN中,对于一个样本,我们每次只输入一个单词,得到一个输出
。除了输出
外,神经网络还会把中间激活输出
传递给下一轮计算,这个
记录了之前单词的某些信息。所有的输出按照这个方法依次计算。当然,第一轮计算时也会用到激活函数
,简单令
为0张量即可。注意,所有的计算都是用同一个权重一样的神经网络。有些文章会把一行RNN计算折叠成一个带循环箭头的神经网络,上图右侧只是另一种画图的方法。

如何用数学语言表示RNN?在第t轮计算中:
其中,表示用于计算i的,乘在j上的矩阵。g是激活函数。中间层激活函数多数情况下用tanh,也可以用ReLU。输出的激活函数视任务而定,在二分类的命名实体识别中,输出激活函数是sigmoid。
为了简化表示,可以把,
拼一下。

这样原来的式子就可以化简了:
RNN也有一些问题。首先,显而易见,每个RNN的输出只能看到它之前的单词。这样是不太好的。比如一句话第一个单词是Teddy,你不知道这是一个人名,还是Teddy bear这样一个普通的物体。稍后我们会学习双向的RNN以解决此问题。
另外,RNN也会面临标准神经网络的梯度爆炸问题。后几节会介绍一些更高级的RNN架构。
1.3 穿越时空之反向传播
看完了正向传播,我们稍微看一下RNN反向传播的过程。
反向传播前,先要定义一个误差函数。对于命名实体识别这种结果是01的问题,可以继续采用交叉熵误差,即对于序列中每一个元素:
对于一个样本:
接下来是反向传播的过程。RNN的计算虽然复杂了一些,但是本质上还是一个计算图,如下面的红色箭头:


由于序列数据有先后的概念,而RNN反向传播又是从后面的数据向前面的数据进行,因此这样的反向传播有“穿越时空之反向传播”的称呼。
1.4 不同输入输出格式的RNN
刚刚学习的那种RNN只能描述输入输出长度一致的任务。在那种架构的基础上稍作修改,我们就能得到描述各种输入输出格式的任务。


- 一对一:其实一对一问题就是标准神经网络,可以不要那个激活输入。
- 一对多:只把输入放入第一轮计算中,后续计算的输入是上一轮的输出。
- 多对一:只输出最后一轮计算的结果。
- 等长多对多:输入一个元素就输出一个元素。
- 不等长多对多:先做几轮不产生输出的计算(编码器),再做几轮只产生输出的计算(解码器)。解码器的输入也可以和一对多一样,来自于上一轮的输出(图中没有画出)。
1.5 RNN 应用:语言模型
为了加深对RNN的理解,我们来看一个基于RNN的应用——语言模型。
语言模型是NLP中的一个基础任务。一个语言模型能够输出某种语言某句话的出现概率。通过比较不同句子的出现概率,我们能够开发出很多应用。比如在英语里,同音的"apple and pear"比"apple and pair"的出现概率高(更可能是一个合理的句子)。当一个语音识别软件听到这句话时,可以分别写下这两句发音相近的句子,再根据语言模型断定这句话应该写成前者。

所以语言模型的功能在于给定任意句子,他都会给出这个特定句子的概率。句子概率的意思是,如果随意拿起一份报纸或者打开一份电邮或打开一份网页或者听到别人随意的话语,在整个情境下你听到下一句特定的话的概率,就像“apple and pear salad”.这是语音识别和机器翻译的基础构成,因为机器翻译系统需要过滤出合适的翻译。所以语言模型的工作是,输入一个句子,(
),语言模型将被作用于输出值y使之表达成一个句子,所做的就是估计这个特定单词序列的概率,而不是作用于x。
也就是说对于一句话,语言模型的输出是
这个式子可以写成
即一句话出现的概率,等于第一个单词出现在句首的概率,乘上第二个单词在第一个单词之后的概率,乘上第三个单词再第一、二个单词之后的概率,这样一直乘下去。
在训练语言模型时,我们一般要用到语料库(corpus)。语料库包含了某种语言大量的通顺的句子。我们希望一个模型能够学习到这种句子的规律,知道一句话在这种语言中出现概率是多少。
语言模型可以用RNN巧妙地实现。整个实现分两步:数据预处理和模型训练。
由于语料库中包含的是自然语言,而RNN的输入是one-hot编码,所以这中间要经过一个预处理的步骤。在NLP中,这一步骤叫做符号化(tokenize)。如我们在「符号标记」一节所学的,我们可以找来一个大小为10000的词汇表,根据每个单词在词汇表中的位置,生成一个one-hot编码。除了普通的词汇外,NLP中还有一些特殊的符号,比如表示句尾的<EOS> (End Of Sentence),表示词汇表里没有的词的<UNK> (Unknown)。
经过预处理后,语料库里的每一句自然语言变成了训练样本 我们可以把每一句话输入RNN,巧妙地训练一个语言模型:

这个计算过程初次接触时有些令人费解,我们慢慢来看懂它。先竖着看一轮计算是怎么完成的。对于每一轮计算,都会给定一个单词编码,输出一个softmax后的概率分布
,它要对齐的训练标签是训练集某一句话的某个单词
。
表示接收之前所有的输入单词后,此时刻应该输出某单词的概率分布,这个输出的含义和多分类中的类似

算出这样的有什么用呢?别急,再横向看一遍,语言模型要求的概率可以写成
RNN每一轮的输出,其实就是要拟合
每一个条件概率的条件,就是每一轮RNN的输入;每一个条件概率的待求事件,就是每一轮RNN的训练标签。比如:
这个条件概率,它的条件是
,待求事件是
,所以第三轮的标签是
,输入是
,(别忘了,在RNN中,前几轮的输入其实也影响了后续的计算)。当然,第一个概率
没有条件,所以第一轮输入
,对softmax的结果依然使用的是交叉熵误差,一个序列的误差等于所有元素的误差之和。
刚刚介绍的是训练过程。在用这个模型计算某句子的概率时,只要把一个句子输入进这个RNN,再去softmax的概率分布里取出需要的概率,一乘,就能算出语言模型要求的整句话的概率了。比如Cats average 15 hours of sleep a day. < EOS >这个句子,我们要令,
,
,……。然后,从第一个输出概率分布
里找出cats对应的概率,去
里找到average对应的概率,去
里找到15对应的概率,以此类推。最后把所有的概率乘起来。
通过这一节,我们学到了RNN的一种应用。是否真正理解语言模型这一任务,并不重要。重要的是,我们学到了RNN是怎么巧妙去完成一项任务的。在完成和序列数据有关的任务时,我们要精心定义RNN的输入序列和输出序列。一旦这两个序列定义好了,训练模型并解决任务就是很轻松的事情。
1.6 用语言模型采样出全新的序列
给定一个别人训练好的RNN语言模型,我们可以弄出一个很好玩的应用:生成一个训练集里没有的句子。
我们刚刚学过,在计算一句话的概率时,RNN会把句子里的每一个单词输入,输出单词出现在前几个单词之后的概率分布。反过来想,我们可以根据RNN输出的概率分布,随机采样出某一个单词的下一个单词出来。具体来说,我们先随机生成句子里的第一个单词,把它输入RNN。再用RNN生成概率分布,对概率分布采样出下一个单词,采样出一个单词就输入一个单词,直到采样出< EOS >。这个过程就好像是在让AI生成句子一样。
对概率分布采样,其实就是以某种概率随机挑选。比如我有两个骰子,我要计算两个骰子点数之和。这个点数之和就是一个概率分布,掷一轮骰子就是去分布里采样。我们可以快速地算出,点数之和为2的概率是, 点数之和为3的概率是。也就是说,我们在采样时,有的概率取到2,的概率取到3。
如果把语料库的最小单元从单词换成字母,句子生成就变成了单词生成,我们可以让AI生成出从没出现过却看上去很合理的单词。
1.7 RNN 梯度问题
在前几门课中,我们曾学过,过深的神经网络会有梯度过大/过小的问题。这些问题在RNN中也存在,毕竟RNN一般都是用来处理很长的序列数据的。
梯度过大的问题倒是有办法解决:设置一个梯度最大值,让所有梯度都不能超过这个值。
梯度过小的问题比较麻烦。想象一个很长的句子:The cat, which ate apples, pears, ...., was full.这个cat和was存在着依赖关系。一旦梯度过小,一个句子前后的依赖关系就不是那么好传递了。
下面几节我们会学一些解决梯度问题的架构。
1.8 GRU (Gated Recurrent Unit)
我们可以用GRU (Gated Recurrent Unit)来替代标准RNN中的计算单元,以解决梯度问题。
为了明确标准RNN的哪些模块被替换了,我们先回顾一下RNN原来的计算单元

在标准的RNN单元中, 和
一起决定了
,
又决定了
先在脑中对这张图有个印象,稍后我们会看到GRU是怎样改进这个计算单元的。
上一节里,我们分析过,梯度消失会导致一句话后面的单词忘掉了前面的单词。那么,可不可以让网络的“记性”更好一点呢?我们可以参考一下人类的记忆行为。比如,当我们在接到了验证码短信后,要把验证码在脑子里记一段时间:读完了短信,要记住验证码;关闭短信应用,要记住验证码;打开需要验证码的应用,要记住验证码;输入验证吗时,要记住验证码;输完了验证码,总算可以忘记验证码了。这个过程中,我们一直在维护“验证码”这个信息,决定是记住它还是忘记它。我们可以让神经网络模型也按照这种思路记忆之前的信息。
在每轮计算更新中间变量时,GRU还要使用到一个新的变量,表示中间变量该不该忘记。这个变量越靠近0,就说明越应该保持之前的中间变量;越靠近1,就越靠近新的。GRU的这个变量起到了电路中逻辑门的效果(GRU的第一个单词是gated)。
具体来说,一个简化版GRU的计算过程用数学符号表示如下:
我们把中间变量临时a更名为c,表示记忆单元memory cell。和之前的a一样,我们每轮要算一个新的:
而同时,我们还要算一个决定是不是要用去更新过去的c的逻辑门
注意,和逻辑门不同,不是真的只能取0或1,而是取0~1中一个中间的值,表示更新的程度。之后,每一轮的
是这样更新的:
它的图示如下,其中紫色部分是更新操作。


唯一的区别是多过了一道
。这种设计的好处很难从理论上解释。当时的研究者试了很多类似的GRU架构,最后发现这样的GRU是效果最好的。
1.9 LSTM(long short term memory)单元
STM单元是另一种改进版的RNN单元。LSTM的核心思想和GRU一模一样,也是使用门来控制记忆变量的更新幅度,只是公式更复杂了一点。在LSTM中,要传递的中间变量有两个:c和a,使用的输出门也从2个增加到了3个。



我们不需要刻意去记LSTM的结构,也不要纠结为什么要在哪个地方用哪个门,只需要知道LSTM和GRU的区别,会用它们就行了。
虽然LSTM比GRU更复杂,但实际上LSTM很早(1997年)就有了,GRU是近几年才有的。二者的效果并没有显著的差别,一般认为LSTM功能更强大,GRU计算速度更快。碰到新任务无脑用LSTM即可,而如果要构建较大的网络则可以考虑使用性能更好的GRU。
1.10 双向RNN
之前提过,标准的RNN有一个问题:先出现的单词无法获取后续单词的信息。比如句首单词是Teddy,你不知道这是“泰迪熊”还是“泰迪”这个人名。为了解决这个问题,我们可以升级一下RNN的基础架构,使用双向RNN(BRNN)。在这个新架构下,GRU和LSTM的单元可以照用,不受影响。
BRNN的示意图如下:

假设一句话有4个单词。在BRNN中,除了会先从1-4正着输入一遍序列外,还会从4-1倒着输入一遍序列。正着传的中间变量叫,倒着传的中间变量叫做
。每一轮输出满足
。
BRNN+LSTM通常是一个新序列任务的标配。当然,BRNN也有一个缺点:必须等一个序列输入完了才能返回结果,而不能实时返回结果。在语音识别等实时性较强的任务里,可能普通RNN更合适一点。

1.11 深层RNN
到目前为止,我们学的RNN都是由几个简单的矩阵运算构成的,似乎和这套课的标题“深度学习”不沾边。实际上,也可以给基础的RNN多加一些参数,变成一个深层的RNN。
正如堆叠标准神经网络的隐藏层一样,我们可以堆叠RNN的基础模块,并传递多个中间变量。由于时序计算的计算成本很高,堆3层的计算量就已经很大了。

如果想进一步提升网络的拟合能力,可以修改计算输出的结构,堆叠一些非时序的神经网络隐藏层。
时序模块之所以计算缓慢,一大原因是无法并行。靠后的变量必须等之前的变量算好了才能计算。而在输出的路径中添加一些隐藏层的运算代价没有那么大,因为这些运算是可以并行的。
同样,使用深层RNN时,双向RNN,还有LSTM, GRU都是可以用的。

总结
在这一课里,我们初次认识了序列数据,并学习了处理序列数据的RNN。利用RNN,我们可以开发出许多和序列数据相关的应用。基础的RNN存在不少问题,所以RNN存在着许多改进方法。让我们看一看这一课的具体知识点:
- 序列数据及相关任务的示例
- 语音识别:输入语音,输出文字
- 音乐生成:无输入,输出语音
- 机器翻译:输入某种文字,输出另一种文字
- 情绪分类:输入文字,输出1-5的分数
- 命名实体识别:输入文字,输出每个单词是否是命名实体
- 单词的表示
- 先准备好一个词汇表。比如大小为10000的词汇表(要包括
<UNK>, <EOS>)。 - 每一个单词是一个长度10000的one-hot向量。单词在词汇表中的序号,就是one-hot向量中值为1的下标。
- 先准备好一个词汇表。比如大小为10000的词汇表(要包括
- 循环神经网络(RNN)
- 基本思想:用表示上文信息
- 计算流程:循环输入序列元素,维护
- 计算公式
- 反向传播的大概流程
- 防止RNN梯度消失:GRU, LSTM
- 基本思想:选择性更新
- 大概了解GRU, LSTM的公式
- GRU, LSTM的使用场景
- 获取下文信息:BRNN
- 基本思想与结构
- 使用场景
- 增强表达能力:深层RNN
- 怎么添加更多层RNN
- 怎么更好地拟合输出
- RNN应用:语言模型
- 语言模型的定义
- 语言模型的训练与推理
- 对语言模型采样
2. 词嵌入(Word2Vec, GloVe)
2.1 词嵌入简介
2.1.1 基础概念
之前我们是用one-hot编码来表示单词的:假设一个单词在词汇表里的序号是t,词汇表大小是T,则这个单词的编码是一个长度为T的向量,向量只有第t维是1,其他维是0。我们用来表示这个单词的one-hot编码。如下图:

这种表示法能区分每个词,但是,它有一个缺陷:one-hot向量两两之间的乘积为0,不能通过向量的相似度推理出单词的相似度。因此,在NLP中,一个重要的任务就是找到一个合理的词表示方法,使得我们能够利用向量的某些性质来表示单词之间的某些特性。
我们来看一种新的词表示方法。假设有Man Woman King Queen Apple Orange这6个单词,我们从性别(Gender),皇家的(Royal),年龄(Age),是否是食物(Food)这几个角度来描述这几个单词,可以填写出这样一份表格:

在这份表格里,每一列的数字可以看成属于某一单词的向量,这个向量就是一种词表示方法。我们用表示词汇表里序号t的这种有意义的向量。比如
就是Man的向量。观察每列的向量,我们能发现Woman和Man很相似,King Queen很相似,Apple和Orange很相似。这样,使用这种词表示,单词的相似度由向量的相似度表示了出来,符合我们对词表示的期望。
当然,使用算法生成的词表示中,向量的每一维不可能像这样可解释性这么强。
这种用向量描述单词的方法称为词嵌入(word embedding)。使用高维向量描述单词,就好像是把一个抽象的概念嵌进了一个向量一样。
2.1.2 词嵌入使用示例
看完了词嵌入的基础概念,我们来看看使用词嵌入有什么好处。
还是以命名实体识别任务为例。假设有这么一句话"Sally Johnson is an orange farmer",我们能够推断出"Sally Johnson"是一个人名,这是因为我们看到了后面的"orange farmer"。橙子农民很可能与人名对应。
假设从刚刚那句话中,模型已经学会了橙子农民与人名之间的关系。现在,又有了一条新的训练样本"Robert Lin is an apple farmer"。使用了词嵌入的话,模型虽然不知道“苹果农民”是什么,但它知道"apple"和"orange"是很相似的东西,能够很快学会这句话的"Robert Lin"也是一个人名。
也就是说,通过使用词嵌入,模型能够利用单词之间的关系更快地完成学习。实际上,不仅是训练,使用了词嵌入后,哪怕出现了训练集中没出现过的单词,模型也能根据单词间的关系做出正确的推理。我们再来看一个例子。
假设又有一条测试样本"Robert Lin is a durian cultivator"。模型可能从来在训练集里没有见过"durian"和"cultivator"这两个单词。但是,通过词嵌入,模型知道"durian(榴莲)"是一种和"apple"相近的东西,"cultivator(培育者)"是一种和"farmer"类似的东西。通过这层关系,模型还是能够推理出"Robert Lin"是一个人名。这就是词嵌入之所以那么重要的原因。词嵌入本质上是一种迁移学习,它隐含了在其他数据中学习到的单词之间的关系。利用这些知识,另一种任务能够在词嵌入的帮助下更快地完成学习。

一般使用词嵌入的步骤如下:
- 用大量数据(千亿级)训练出词嵌入。
- 把词嵌入迁移到新任务上,使用一个较小的数据集(比如,万级)。
- 可选:继续finetune词嵌入(仅当新任务的数据量足够多时)。
除了加速训练外,词嵌入还有一个好处:词嵌入的向量长度往往较少。比如一个长度为10000的字典训练出来的词嵌入可能长度只有300。
结束这节前,顺便提一下词嵌入与上门课讲到的人脸识别中的编码(encoding)之间的关系。不管是嵌入还是编码,其实都是指向量,两种描述不少时候可以通用。但是,人脸识别中的编码主要指对任何一张数据算出一个编码,而嵌入指把一个已知的单词集合的每一个单词嵌进一个向量空间里。二者的主要区别在于输入集合是否固定。

2.1.3 词嵌入的性质
掌握了词嵌入的基本应用,让我们在另一种任务里进一步认识词嵌入的性质。
NLP中有一种任务叫做类比推理,比如,Man和Woman的关系,相当于King和谁的关系?有了词嵌入,这一问题就很好回答了。
还是以刚刚那张人工构造出来的词嵌入表为例。这里我们用来表示Man的词嵌入,以此类推。

单词间的关系不好描述,而向量间的关系却很容易算,我们可以用向量的差来表示向量直接的关系。计算一下 通过猜测,我们发现King和Queen的关系与Man和Woman的关系类似,进而可以得出,进而可以得出,Man之于Woman,相当于King之于Queen。
准确来说,刚刚这个问题相当于求解一个单词, 其嵌入
满足
计算方法就是
其中sim表示某种相似度,比如cosine相似度,我们只需要计算出右边的 再用某种算法就可以求出最合适的
通过这些观察,我们可以发现,词嵌入蕴含了语义信息。词嵌入间的差别很有可能就是语义上的差别。


2.1.4 词嵌入矩阵
假设词嵌入向量的长度是300,有10000个单词。词嵌入的过程,其实就算把一个长度为10000的向量映射成长度为300的向量的过程。这个过程可以用一个300* 10000的矩阵E表示,就是词嵌入向量的数组。每一个词嵌入列向量 可以由one-hot编码
和E计算得到
2.2 词嵌入的学习
认识了词嵌入的基本概念后,我们来看看如何用学习算法得到一个词嵌入矩阵。
学习词嵌入和学习神经网络的参数是一样的。只要我们是在根据词嵌入算一个损失函数,就可以使用梯度下降法优化词嵌入。因此,问题的关键在于如何建模一个使用到词嵌入的优化任务。
2.2.1 语言模型
早期的词嵌入是通过语言模型任务学习到的。回想上周课的内容,语言模型就是预测一句话在这种语言中出现的概率。比如 "I want a glass of orange ___ .",我们会自然地觉得空格里的单词是"juice",这是因为填juice后整句话的出现概率比填其他单词更高一点。
暂时抛开上周讲的RNN,我们可以用一个使用词嵌入的神经网络来学习语言模型,如下图所示:

这个任务的输入是一句话中连续的6个单词,输出是第7个单词的预测。在用神经网络建模时,要先根据各个单词的one-hot编码从词嵌入矩阵E中选出其嵌入
,再把各个单词的嵌入堆叠成一个向量,输入进标准神经网络里,最后用一个softmax预测下一个单词。
在这个模型中,可见的单词数是固定的。假设词嵌入的维度是300,那么根据6个单词进行预测的神经网络的输入向量长度就必须是1800。为了遍历整句话,可以拿一个长度为7(算上预测词)的滑动窗口在整句话上滑一遍。
可见前6个单词,其实算是网络的一个超参数。除了选取前6个单词外,还有其他的选取上下文的方式。常见的上下文选取方式如下:
- 前4个单词
- 前4个单词和后4个单词
- 前1个单词
- 往前数的第2个单词

2.2.2 Word2Vec
Word2Vec是一种比语言模型更高效的词嵌入学习算法。与语言模型任务的思想类似,Word2Vec也要完成一个单词预测任务:给定一个上下文(context)单词,要求模型预测一个目标(target)单词。但是,这个目标单词不只是上下文单词的后一个单词,而是上下文单词前后10个单词中任意一个单词。比如在句子"I want a glass of orange juice to go along with my cereal"中,对于上下文单词glass,目标单词可以是juice, glass, my。
具体来说,每一条训练样本是一个上下文单词和目标单词的词对。比如(orange, juice), (orange, glass)。为了生成这些训练数据,我们要从语料库里每一个句子里采样出训练词对。在采样时,要先对上下文单词采样,再对目标单词采样。
假设有了上下文单词,对目标单词采样很简单,只需要从上下文单词的前后10个单词中均匀采样单词即可。而采样上下文单词就需要一些设计了。在英文中,大部分单词都是a, the, of这些没什么含义的词,如果在句子里均匀采样的话,大部分时候得到的都是这些词。因此,在Word2Vec论文中,有一些对上下文单词采样的设计,各单词的出现概率会更平均一点。
看完了训练数据的采样,再看一看Word2Vec的模型。Word2Vec模型非常简单,它是只有一个softmax层的神经网络,我们可以直接写出这个模型的公式:
即目标单词t在上下文单词c前后出现的概率。
是c的嵌入。
是softmax层的线性计算的参数,这里我们省略掉了bias。求和里的10000是整个词汇表的大小,也就是输出向量的大小。
和其他多分类任务一样,这个任务的损失函数也是交叉熵函数。
Word2Vec的模型结构十分简单,因此,整个模型的计算量全部落在了softmax的分母求和上。假设词汇表有n个单词,整个模型的时间复杂度就是。在词汇表很大时,求和的开销也是很大的。
为了优化这个求和,Word2Vec使用了H-Softmax(Hierachical Softmax)这种优化方式。一个多分类任务,其实可以拆成多个二分类任务。比如有“猫、狗、树、草”这四种类别,我们可以先做是动物还是植物的二分类,再做一次更具体的二分类,最后把两次次二分类的概率乘起来。H-Softmax就是用这种思想优化了softmax的求和。
使用H-Softmax前,要先对所有单词建立一颗二叉树,比如对A, B, C, D四个单词,可以这样建树:"ABCD-(AB,CD)", "AB-(A,B)", "CD-(C,D)"。这样,把一个多分类问题拆成多个二分类问题,就等价于从树的根部开始,经过多个节点,达到单词所在的叶节点。使用H-Softmax时,只要把访问该单词的路径上所有节点的概率乘起来就行了。比如要求单词是的概率,可以先算属于的概率,再算已知属于时是C的概率,二者一乘就是我们要的。
二分类的复杂度是,要做次二分类。因此,经优化后,H-Softmax的复杂度。实际上,这个算法还有一些优化空间。词汇表里的词汇是固定的,我们可以巧妙地修改建立二叉树的方法,进一步减少运算量。“给定各元素的访问概率(在这个问题里是单词在语言里的出现概率),对所有元素建立一颗二叉树,以最小化访问叶节点的路径长度的期望”是一个经典的问题,这个问题的解法叫做哈夫曼编码。这是离散数学的知识,和本课的关系就不大了。
H-Softmax的核心思想是把多分类拆成二分类,搞懂这个就行了。至于使用二叉树,怎么建立更好的二叉树,这是一个独立的子问题,理解它和理解H-Softmax无关。在学NLP时,可以先把这个子问题放一放,理解H-Softmax的用意就行。
Word2Vec的目标任务还有其他的形式。除了找上下文单词前后10个单词中的某个目标单词外,还可以用前后的1个单词预测中间的目标单词,这种方法叫做CBow。两种方法各有千秋。Word2Vec的主要思想是那个单层softmax模型,具体的任务倒不是最重要的。

2.2.3 负采样
通过前面几个小节的学习,我们能够总结词嵌入学习的一些经验:词嵌入的根本目的是学习词嵌入矩阵,使用词嵌入的任务倒没有那么重要。因此,我们可以放心大胆地去简化每轮任务的计算量,加快词嵌入的学习效率。
基于这种思想,我们可以进一步去优化Word2Vec里的多分类任务。实际上,一个N分类任务,可以“复杂化”成N个二分类任务——逐个判断输入是否是N个类别中的一种。顺着这个思路,我们不用去求给定上下文单词时目标单词的概率分布,只需要判断给定上下文单词和目标单词,判断二者是否相关即可。
这样,在每一轮任务中,我们不用去计算多分类的softmax,只要计算一个二分类的sigmoid就行了。这样一种算法叫做负采样(Negative Sampling)。
负采样使用的模型和Word2Vec一样简单:输入一个上下文单词的嵌入,经过一个sigmoid层,输出那个上下文单词和某个目标单词是否相关。
负采样算法中真正的难点是训练数据的生成。在看数据生成算法之前,我们先看一下训练样本的格式。负采样的每一条样本是一个三元组(context, word, target),分别表示上下文单词、目标单词、用01表示的两个单词是否相关。比如,我们可能会得到这样的正负样本:
| context | word | target |
|---|---|---|
| orange | juice | 1 |
| orange | king | 0 |

接下来,我们来看如何生成这些样本。使用Word2Vec的采样方法,我们会对语料库里的每一句话采样出一些词对。这样,每一个词对能构成一个正样本,它的target值为1。
正样本很好生成,可负样本就不是很好采样了。负采样算法使用了一种巧妙的采样方法(这也是其名称的由来):在生成一个正样本的同时,算法还会对同一个上下文单词context生成个target为0的负样本。这些样本里的目标单词word是随机挑选的。
举个例子,设,在"I want a glass of orange juice to go along with my cereal"这句话中,假如我们采样到了(orange, juice)这个词对,我们可能会随机选4个单词,得到下面这些训练样本:
| context | word | target |
|---|---|---|
| orange | juice | 1 |
| orange | king | 0 |
| orange | book | 0 |
| orange | the | 0 |
| orange | of | 0 |
注意,每个负样本是针对一条正样本而言的。尽管orange, of都出现在了这句话里,但我们在考虑(orange, juice)这个正样本词对时,会把其他所有词对都当做负样本。
刚刚讲到,负样本里的word是随机挑选的。其实,这种“随机”有一些讲究。如果对所有单词均匀采样,那么不常用的词会被过度学习;如果按照单词的出现频率采样,of, the这些助词又会被过度学习。因此,在采样负样本时,这个负采样算法的论文使用了这样一种折中的方法:
2.2.4 Glove

GloVe(global vectors for word representation)是一种更加简单的求词嵌入的算法。刚才学习的几种方法都需要进行复杂的采样,而GloVe使用了一种更简洁的学习目标,以代替多分类任务或者二分类任务。
为给定上下文单词i时单词j出现的次数。和前面一样,这里的“上下文”可以有多种定义。比如,如果上下文的定义是“前后5个单词”,那么这就是一个对称的上下文定义,
;如果上下文的定义是“后1个单词”,则
。我们可以简单地把
理解成j出现在i附近的次数。
比如我们把上下文定义为前后2个单词。在句子"a b c b d e"中,。
有了 ,我们就能直接知道给定上下文i时各个单词j的出现频率,而不需要再构建一个分类任务去学习单词出现的条件概率。这样一个新的误差函数是:
和之前几个任务一样,是上下文单词的词嵌入,
是线性计算的参数。
其实再就是拟合某单词j和上下文单词i的相关程度。而
恰好能反映某单词j和上下文单词i的相关程度。
刚刚那个误差函数有几个需要改进的地方:
- log 里面可能出现0。对于
的地方,我们要想办法让
。
- 不同单词的出现频率不同。对于出现频率较少的单词,我们可以限制它对优化目标的影响。
- 可以像普通的线性层一样,加入偏差项bias。
因此,最终的损失函数为(假设词汇表大小10000):
其中,f是权重项,既用于防止(f(0)=0),也用于调节低频率单词的影响。
分别是上下文单词和目标单词的偏差项。
有趣的是,当是对称矩阵的时候,
也是对称的,它们在式子里的作用是等价的。我们可以让最终的词嵌入为
的平均值。
在结束词嵌入的学习前,我们还要补充学习一下词嵌入的一些性质。

上图是我们在这节课的开头学习到的“人造词嵌入”。在这个词嵌入中,向量的每一个维度都有一个意义。而在实际情况中,算法学习出来的词嵌入不能保证每个维度都只有一个意义。根据线性代数的知识,要表示同一个空间,有无数组选择坐标轴的方法。很可能0.3x+0.7y这个方向表示一个意思,0.4y+0.6z这个方向又表示一个意思,而不是每个坐标轴的方向恰好表示一个意思。当然,不管怎么选取坐标轴,两个向量的相对关系不会变,对词嵌入做减法以判断两个单词的关系的做法依然适用
2.3 词嵌入的应用
2.3.1 情感分析
词嵌入可以应用于情感分析(Sentiment Classification)任务。在情感分析任务中,算法的输入是一段文字(比如影评),输出是用户表达出来的喜恶程度(比如1-5星)。

有了词嵌入,我们可以轻松地构筑一个简单的模型。

如上图所示,只要简单地对所有输入单词的词嵌入取平均值,放入softmax即可。
这种算法确实能够生效。但是,它只考虑了每个单词的含义,而忽略了整体的意思。如果句子里有"not"这种否定词,这个模型就不太有效了。为此,我们可以构建更精巧的RNN模型。
如第一周所学,RNN是一个“多对一”任务。我们可以让RNN最后一轮输出一个分类结果。只不过,这次输入RNN的不是one-hot向量,而是更有意义的词嵌入。

2.3.2 消除歧视
词嵌入会自动从大量的本文中学习知识。但是,数据中的知识可能本身带有偏见。比如,在自动学到的词嵌入看来,男人之于程序员,就像女人之于家庭主妇;父亲之于医生,就像母亲之于护士。类似的歧视不仅存在于性别这一维度,还存在于种族、年龄、贫富等维度。我们希望消除词嵌入里面的这些歧视。

词嵌入本身是向量,歧视其实就是某些本应该对称的向量不太对称了。我们的目的就是在带有偏见的维度上令向量对称。
第一步,我们要找到带有偏见的维度。比如,对于性别维度,我们可以算,对这些表示同一意义的方向取一个平均向量,得到偏见的方向。得到方向后,我们可以用一个平面图来可视化和偏见相关的向量。假设词嵌入的长度是300,那么x轴表示带有偏见的那个维度,y轴表示剩余的299个维度。

所有的单词可以分成两类:和性别相关的明确(definitional)单词和剩余不明确的单词(明确单词需要手动找出来)。第二步,我们要让所有不明确单词都恰好回到y轴上。这样,任何其他单词都不会偏向某一性别了。
最后,有些和性别相关还不够对称。我们要想办法让每对和性别相关的词恰好按y轴对称。

总结
在这堂课中,我们系统地学习了词嵌入这个概念,并大致了解了如何在NLP任务中使用词嵌入。相关的知识有:
- 词嵌入简介
- 从one-hot到词嵌入
- 词嵌入向量的意义
- 词嵌入学习算法
- 语音模型
- Word2Vec
- 负采样
- GloVe
- 如何应用词嵌入
词嵌入是专属于NLP的概念,且是NLP任务的基石。如果要开展NLP相关研究,词嵌入是一个绕不过去的知识;反过来说,如果不搞NLP,只是想广泛地学习深度学习,那么词嵌入本身可能不是那么重要,对词嵌入问题的建模方法会更重要一点。
只从实用的角度来看的话,这堂课介绍的知识并没有那么重要,网上能够轻松找到别人预训练好的词嵌入权重。真正重要的是词嵌入在框架中的使用方法,以及如何在一般任务中使用词嵌入。
3. 序列模型与注意力机制
3.1 基础模型
有一些问题,它们的输入和输出都是一个序列。比如说机器翻译,输入是某种语言的一句话,输出是另一种语言的一句话。这种序列到序列的问题可以简单地套用RNN解决。
如我们第一周所学,不等长的序列到序列问题可以用如下的RNN模型解决。前面只有输入的部分叫做编码器(encoder),后面输出的部分叫做解码器(decoder)。

再举另一个任务——看图说话的例子。看图说话,就是输入一幅图片,输出该图片的文字描述。比如给定下图的图片,我们可以说“小猫在椅子上”。这个任务的输入看上去并不是一个序列,但我们可以用某种CNN架构把图片转换成一个向量。这个向量就可以看成图片的编码,和刚刚那个RNN编码器的输出一样。利用这个编码,我们可以用RNN解码器生成一句话。

3.2.1 挑选最好的句子
但是,这种基础的架构类似于我们第一周学的语言模型。它可能生成多个新句子,而不能保证生成最好的句子。为了按要求生成最好的句子,我们要使用一些其他的方法。
比如把“Jane九月要访问非洲”翻译成英文,可以翻译成"Jane is visiting Africa in September.",也可以翻译成"Jane is going to be visiting Africa in September."。上节提到的那个类似于语言模型的RNN架构可以生成出这两个句子中的任何一个。
从语言的角度,"Jane is visiting Africa in September."这个翻译比"Jane is going to be visiting Africa in September."更好。或者说,前面那个句子的概率更高一点。用数学公式来表达,给定被翻译的句子x, 我们希望求一个使最大的序列y。
求解这个最优化问题时,我们可能会想到贪心算法。由于RNN解码器每步的softmax可以输出下一个词的概率(还是和语言模型一样的道理),我们可以求出,
, ...,即用贪心算法每次求出一个概率最大的单词。


然而,每次选一个概率最大的单词,不能保证整句话概率最大。比如,模型可能有"Jane is visiting Africa in September."和"Jane is going to be visiting Africa in September."这两个潜在的候选翻译结果。选第三个单词时,"going"的概率可能比"visiting"要高,按贪心算法,我们最后会生成出第二个句子。可是,从翻译质量来看,第一个句子显然更好,它的概率更高。
为了求出概率最大的输出序列,我们要使用一种较好的搜索算法。

3.2 Beam Search
贪心算法每次只求出能令当前句子概率最大的下一个单词。这种算法太容易遗漏更优的输出了。而如果真的想求出最优的句子,即求出,需要遍历所有可能的y。假如每个单词有N种选择,句子长度
,则搜索算法的复杂度是
。这个指数增长的复杂度是不能接受的。
Beam Search是这样一种折中的启发式搜算算法。它不能保证求出最优解,却能比贪心算法找出更多更优的解。
Beam Search的核心思想可以用一句话概括:相比于只维护一个概率最优句子的贪心算法,Beam Search每次维护B个概率最优的句子。
还是拿开始那句话的翻译为例,并假设B=3,词汇表大小为10000。生成第一个单词时,概率最高的三个单词可能是in, Jane, September。生成第二个单词时,我们要遍历第一个单词是in, Jane, September时的所有30000种两个单词组合的可能。最终,我们可能发现in September, Jane is, Jane visits这三个句子的概率最高。依次类推,我们继续遍历下去,直到生成句子里的所有单词。



这个算法用伪代码表示如下:
Input x, B
# encoder
a = 0
for i in range(tx):
a, _ = RNN(embed(x[i]), a)
# decoder
# step 1
a, p = RNN(0, a)
a_arr = [a] * B
words, prob = get_max_words_from_softmax(p, B)
sentences = copy(words)
# step 2 - ty
for i in range(ty - 1):
all_words = []
all_prob = []
all_a = []
for j in range(B):
new_a, p = RNN(embed(words[j]), a_arr[j])
tmp_words, tmp_prob = get_max_words_from_softmax(p, B)
tmp_a = [new_a] * B
# Accmulative multiply the probablity
for k in range(B):
tmp_prob[k] *= prob[j]
all_words += tmp_words
all_parob += tmp_prob
all_a += tmp_a
words, prob, a_arr = get_max_B(all_words, all_prob, all_a)
# Cancatenate output
for j in range(B):
sentences[j].append(words[B])
y = get_max_sentences(sentences[j], prob)
Output y




这里的是一个超参数,它用于让长度惩罚更平滑一点,即模型更容易生成长句子。一般
。
为了生成不同长度的句子,在让decoder输出句子时,我们可以记录下时概率最大的句子。得到了这些不同长度的候选句子后,把它们的概率乘上归一化项,找出整体概率最大的句子。
Beam Search是一种启发式算法,它的效果取决于超参数B。一般情况下,为了保证速度,B取10就挺不错了。只有在某些不考虑速度的应用或研究中才会令B=100或更高。
加入Beam Search会让我们调试机器翻译算法时更加困难。还是对于开始那个翻译示例,假如人类给出了翻译:Jane visits Africa in September,算法给出了翻译
:Jane visited Africa last September。这个算法的翻译不够好,我们想利用第三门课学的错误分析方法来分析错误的来源。这究竟是Beam Search出了问题,还是RNN神经网络出了问题呢?
对此,我们可以把训练好的RNN当成一个语言模型,输入和
,求出这两个句子的概率。如果
,那说明RNN的判断是准确的,是Beam Search漏搜了;如果
,那说明RNN判断得不准,是RNN出了问题。




3.3 Bleu Score
在图像分类中,我们可以用识别准确率来轻松地评价一个模型。但是,在机器翻译任务中,最优的翻译可能不只一个。比如把“小猫在垫子上”翻译成英文,既可以说"The cat is on the mat",也可以说"There is a cat on the mat"。我们不太好去评价每句话的翻译质量。Bleu Score就是一种衡量翻译质量的指标。
为了评价一句话的翻译质量,我们还是需要专业翻译者给出的参考翻译。比如把刚刚那两句英文翻译作为参考译句:
- The cat is on the mat
- There is a cat on the mat
我们可以把机器的翻译结果和这两句参考结果做对比。对比的第一想法是看看机器翻译的句子里的单词有多少个在参考句子里出现过。但是,这种比较方法有问题。假如机器输出了"the the the the the the the",the在参考句子里出现过,输出的7个单词全部都出现过。因此,翻译准确率是。这显然不是一个好的评价指标。
一种更公平的比较方法是,每个单词重复计分的次数是原句中该单词出现的最大次数。比如,the在第一个参考句子里出现2次,在第二个参考句子里出现1次。因此,我们认为the最多计分两次。这样,这句话的翻译准确率就是。这个评价结果好多了。
我们不仅可以对单个单词计分,还可以对相邻两个单词构成的单词对计分。比如机器翻译输出了句子The cat the cat on the cat。我们统计每一个词对在输出里出现的次数和计分的次数。
| count | score | |
|---|---|---|
| the cat | 2 | 1 |
| cat the | 1 | 0 |
| cat on | 1 | 1 |
| on the | 1 | 1 |
| the mat | 1 | 1 |
这样,这个句子的准确率是。
这种打分方式就叫做bleu score。刚刚我们只讨论了考虑一个单词、两个单词时的打分结果。实际上,我们可以用表示连续考虑n个单词的bleu score。最终评价一个翻译出来的句子时,我们会考虑到n取不同值的情况,比如考虑n=1,2,3,4的情况。最终使用的这种指标叫做组合bleu score,它的计算公式为:

bleu score是一个能十分合理地评价机器翻译句子的指标。在评估机器翻译模型时,我们只要使用这一种指标就行了,这符合我们在第三门课中学习的单一优化目标原则。bleu score最早是在机器翻译任务中提出的,后续很多和句子生成相关的任务都使用了此评估指标。
3.4 注意力模型
我们刚刚学习的这种“编码器-解码器”架构的RNN确实能在机器翻译上取得不错的效果。但是,这种架构存在一定的限制:模型的编码(输入)和解码(输出)这两步都是一步完成的,模型一次性输入所有的句子,一次性输出所有的句子。这种做法在句子较短的时候还比较可行,但输入句子较长时,模型就“记不住”之前的信息了。而我们这一节学习的注意力模型能够很好地处理任意长度的句子。

让我们看看人类在翻译长句的时候是怎么做的。比如把“简访问非洲”翻译成"Jane visits Africa"时,我们一般不会把整句话一次性翻译,而是会对单词(或文字)逐个翻译。我们会把“简”翻译成"Jane",“访问”翻译成"visits",“非洲”翻译成"Africa"。在输出每一个单词时,我们往往只需要关心输入里的某几个单词就行了,而不需要关注所有单词。
注意力模型就使用了类似的原理。在注意力模型中,我们先把输入喂给一个BRNN(双向RNN)。这个BRNN不用来输出句子,而是用于提取每一个输入单词的特征。我们会用另一个单向RNN来输出句子。每一个输出单词的RNN会去查看输入特征,看看它需要“关注”哪些输入。比如,"Jane"的RNN会关注“简”的特征,"visits"的RNN会关注“访”和“问”的特征。这一过程如下图所示(线条表示输出对输入的关注,线条越粗关注度越高)。


这样,不管输入的序列有多长,每一个输出都能找到它需要关注的部分单词,仅根据这些输入来完成翻译,就和我们人类的做法一样。这就是注意力模型的思想。
让我们看一下具体的计算过程。为了区分上下两个RNN,我们用a表示编码RNN的状态,表示输入序号;s表示解码RNN的状态,t表示输出序号。刚才提到的那种关注每个输入单词的注意力机制会给每个输出一个上下文向量
。这个向量和上一轮输出
拼在一起作为这轮解码RNN的输入。

注意,从逻辑上来讲,解码RNN有两个输入。第一个输入和我们之前见过的解码RNN一样,是上一轮的输出
。第二个输入是注意力上下文
。这两个输入通过拼接(concatenate)的方式一起输入解码RNN。我在学到这里的时候一直很疑惑,两个输入该怎么输入进RNN。原视频并没有强调两个输入是拼接在一起的。
如果输出的单词不是很依赖于上一个单词,解码RNN也可以不输入上一个单词,只输入注意力上下文。这门课的编程作业就采用了这种只有一个输入的更为简单的结构。
接下来,我们来详细看看注意力机制是怎么工作的。如前文所述,注意力机制就是要算一个对每个输入的关注度,根据这个关注度以不同的权重去输入里取值。这个过程的表示如下。
设输入状态(把BRNN前后的状态拼接到一起)。假如我们得到了权重
,它表示第t个输出对第t'个输入的关注度,则注意力上下文
的计算方法为
对于每一个t, 所有的t'的和为1。也就是说,上式其实就是一个加权平均数,其中
是权重。“注意力”这个名词看上去很高端,其实就是一个中学生都会的概念而已。
现在,我们还不知道关注度是怎么算的。让我们思考一下,第t个输出单词和第t'个输入单词的关注度取决于谁的信息呢?答案很简单,取决于第t个输出单词的信息和第t'个输入单词的信息。第t个输出单词的信息,可以用其上一层的状态
表达;第t'个输入单词的信息,可以用
表达。怎么用它们算一个关注度出来呢?谁也给不出一个具体的公式,干脆就用一个神经网络来拟合就好了。

我们用一个小全连接网络算出一个输出。由于
最后的和要为1,我们用对
做一个softmax,得到归一化的
。

整理一下,注意力模型的计算步骤如下:
- 用一个编码RNN(比如Bi-LSTM)算出输入的特征
。
- 用一个解码RNN(比如LSTM)的状态
和所有
算一个对每个输入的关注度
。
- 以关注度为权重,
以为值,算一个加权平均数
作为第t个输出的注意力上下文。
- 以上一轮输出
和
为输入,以
为上一轮状态,计算解码RNN这一轮的输出。
注意力模型的效果不错,但它的计算复杂度是平方级别的(输入长度乘输出长度)。不过,机器翻译任务的输入输出都不会太大,这一性能弊端没有那么明显。
3.5 语音任务

到目前为止,我们主要用序列模型完成NLP任务。其实序列模型也很适合用在语音数据上。让我们来快速认识一下语音识别任务的解决方法。
语音数据是一维数据,表示每一时刻的声音强度。而我们人脑在在接受声音时,会自动对声音处理,感知到声音的音调(频率)和响度。
语音识别任务的输入是语音数据,输出是一个句子。我们可以直接用注意力模型解决这个问题(令输出元素为字母而不是单词),也可以用一种叫做CTC(connectionist temporal classification)的算法解决。
CTC算法用于把语音识别的输入输出长度对齐。这样,我们用一个简单的等长多对多RNN就可以了。在语音识别中,输入的长度远大于输出的长度,我们可以想办法扩大输出长度。这种扩充方式如下:
比如,对于句子"the quick brown fox",我们可以把"the q"它扩充成ttt_h_eee____< space >____qqq__。这个和输入等长的序列表示每一个时刻发音者正在说哪个字母。序列中有一些特殊标记,下划线表示没有识别出任何东西,空格< space >表示英语里的空格。
通过这种方法,我们可以把所有训练数据的标签预处理成和输入等长的序列,进而用普通RNN解决语音识别问题。

3.5.1触发词检测
触发词检测也是一类常见的语音问题,我们可以用序列模型轻松解决它。
触发词检测在生活中比较常见,比如苹果设备的"Hey Siri"就可以唤醒苹果语音助手。我们的任务,就是给定一段语音序列,输出何时有人说出了某个触发词。
我们可以用一张图快速地学会如何解决这个问题。如下图所示,对于每一个输入,我们可以构造一个等长的输出,表示每一时刻是否说完了触发词。每当一个触发词说完,我们就往它后面几个时刻标上1。用一个普通RNN就可以解决这个问题了。

总结
这堂课以机器翻译任务为例,介绍了序列到序列问题的一些解决方法。特别地,这堂课介绍了注意力模型。注意力模型在序列到序列问题上有着极佳的表现,并催生了后续纯粹由注意力机制构成的更加强大的Transformer模型。利用这些学到的知识,我们可以轻松地解决大多数序列到序列问题,比如和语音相关的语音识别与触发词检测问题。
这堂课的知识点有:
- Beam Search
- 序列到序列问题的建模方法:解码器与生成器,生成一个概率尽可能大的输出序列
- 为什么需要搜索算法,为什么贪心算法不好
- Beam Search 的过程
- Bleu Score 的计算思想:公平地根据参考序列评价生成序列的质量
- 注意力模型
- 新的编码器-解码器架构
- 注意力机制的动机
- 解码器怎么利用注意力权重
- 注意力权重怎么生成
- 语音问题
- 建模思想:对齐输入输出序列,用简单的RNN解决问题
- 语音识别CTC算法:输出每一时刻正在发音的字符
- 触发词检测:用01表示是否有触发词
4. Transformer
在最后一课中,我们要学习Transformer模型。Transformer是深度学习发展史上的一次重大突破,它在多个领域中取得了傲人的成绩。Transformer最早用于解决NLP任务,它在CV任务上的潜力也在近几年里被挖掘出来。
RNN有一个致命的缺陷:计算必须按照时序执行,无法并行。为了改进这一点,Transformer借用了CNN的想法,并行地用注意力机制处理所有输入,抛弃了经典RNN的组件。
为了理解Transformer,我们将主要学习两个概念:自注意力和多头注意力。
- 自注意力:Transformer会为序列里的每一个元素用注意力生成一个新的表示,就和CNN里卷积层能为每个像素生成高维特征向量一样。这个表示和词嵌入不同,词嵌入只能表示一个单词本身的意义,而「自注意力」生成的表示是和句子里其他单词相关的。
- 多头注意力:多头注意力表示多次利用自注意力机制,生成多个表示,就和CNN里N个卷积核能生成N个特征一样。
4.1 自注意力
词嵌入只能反映一个词本身的意思,而不能反映一个词在句子中的意思。比如我们要把”简访问非洲“翻译成英文,其中第三个字“问”有很多意思,比如询问、慰问等。自注意力的目的就是为每个词生成一个新的表示,反映它在句子中的意思,比如我们为“问”字生成的表示是“访问”这个具体的意思。
自注意力,顾名思义,就是对句子自己使用注意力机制。我们先回忆一下最初的注意力机制。

在翻译句子时,我们要算每个输出单词对每个输入单词的注意力,以这个注意力为权重,我们可以算所有输入状态
的加权平均数c(公式1)。这个c用于输出每一个单词。
注意力是权重的归一化结果(如公式2所示,因为是求平均数,得保证权重
的和为1),这里的
表示第t'个输入和第t个输出的相关量。由于
是用于计算
的,
还获取不了,我们只能用
来表示第个输出。另外,我们用
表示第t'个输入。这样,
就应该由
和
决定。
Transformer用一种通用的公式表示了这种注意力的计算。

我们先不管这里的q,k,v是什么意思,暂且把它们当成标记。用它来表示最初的注意力机制的话,q就相当于输出状态s,k就相当于输入状态,二者的相关关系e就是两个向量的内积qk。也是输入状态a。
现在,我们来看看如何用这个公式计算自注意力。我们将以第三个输入的注意力表示
为例。
假设我们为每个输入单词都已经维护好了3个变量。q,k,v是英文query,key, value的缩写,这一概念来自于数据库。假如数据库里存了学生的年龄,第一条记录的key-value是
("张三", 18),第二条记录的key-value是("李四", 19)。现在,有一条query,询问"张三"的年龄。我们把这一条query和所有key比对,发现第一条记录是我们需要的。因此,我们取出第一个value,即18。
每个单词的q,k,v 也可以有类似的解释。比如第三个字“问”的query 是:哪个字和“问”字组词了?当然,在这个句子里,我们人类可以很轻松地知道答案,“问”和“访”组成了“访问”这个词。每个字的key可以认为是字的固有属性,比如是名词还是动词。每个字的value就可以认为是这个字的词嵌入。


让计算机去查询的话,我们要用这个query去句子的其他字里查出我们要的答案。和数据库的查询一样,我们也要把这个query和所有字的key进行比对。根据注意力的公式,我们用
和所有k相乘再做softmax,得到注意力权重。再用这个权重乘上每个v,加起来,得到所有的加权平均数。既然是查询“问”字和谁组词了,那么这个要找的字肯定是一个动词。因此,计算机会发现
,
比较相关,即这两个向量的内积比较大。一切顺利的话,这个
应该和
很接近,即问题“哪个字和‘问’字组词了?”的答案是第二个字“访”。

这是计算过程。准确来说
.类似地,
都是用这个公式来计算。把所有的A计算合起来,得到下面的公式。
其中,是一个常量,
这一项是用来防止点乘结果的数值过大的。它属于实现细节,对整个式子的意义不影响。抛去这一项的话,上式不过是之前那个公式的矩阵版本。这个公式在原论文里叫做Scaled Dot-Product Attention。
现在,我们已经理解了注意力的公式。自注意力,其实就是令Q=K=V=E,对这个向量自己做注意力。但是,我们还有一个重要的问题没有解决:Q,K,V 在Transformer里是怎么获得的?别急,后几节我们会把所有的概念串起来,学习Transformer的结构。现在,让我们再看一看Transformer里的另一个概念——多头注意力。
4.2 多头注意力
在CNN中,我们会在一个卷积层里使用多个卷积核,以提取出不同的特征。比如第一个卷积核提取出图像水平方向的边缘,第二个卷积核提取出图像垂直方向的边缘。类似地,一次自注意力也只能得到部分的信息,我们可以多次使用自注意力得到多个信息。
上一节的是问题“哪个字和‘问’字组词了?”的答案。我们可以多问几个问题,得到有关“问”字的更多信息,比如“‘问’的主语是谁?”、“‘问’的宾语是谁?”。为了描述这些不同的问题,我们要准备一些可学习的矩阵
。Attenton(
)就是第一组注意力结果,Attenton(
)就是第二组注意力结果,以此类推。这种机制叫做多头注意力。

每次Attention的结果是一个向量,所有Attention的结果concat起来就是多头注意力的结果。
4.3 网络结构
搞懂了自注意力、多头注意力这两个核心机制后,我们就能够看懂Transformer的整体结构了。
让我们看看Transformer是怎么翻译一句话的。给定一个单词序列(为了方便,我们认为输入句子已经被token化,且输入的是每个token的嵌入),我们要先用encoder提取特征,再用decoder逐个输出翻译出来的token。
在encoder中,输入会经过一个多头注意力层。这一层的Q, K, V都是输入的词嵌入。多头注意力层会接到一个前馈神经网络上,这个网络就是一个简单的全连接网络。
别忘了,多头注意力层的可学习参数是,Q, K, V可以是预训练好的词嵌入。
再看decoder。先看一下decoder的输出流程。第一轮,decoder的输入是<SOS>,输出Jane;第二轮,decoder的输入是<SOS> Jane,输出是visits。也就是说,decoder的输入是现有的翻译序列,输出是下一个翻译出来的单词。
在decoder中,输入同样要经过一个多头注意力层。这一层的Q, K, V全是输入的词嵌入。之后,数据会经过另一个多头注意力层,它的Q是上一层的输出,K, V来自于Encoder。为什么这一层有这样的设计呢?大家不妨翻回到前几节,回顾一下经典注意力模型中注意力是怎么计算的。其实,经典注意力模型就是以decoder的隐状态为Q,以encoder的隐状态为K, V的注意力。Transformer不过是用一套全新的公式把之前的机制搬过来了而已。做完这次多头注意力后,数据也是会经过一个全连接网络,输出结果。
encoder和decoder的大模块一共重复了N次,原论文中N=6。

总结一下注意力在Transformer里的使用。多头注意力其实有两个版本:第一个版本是自注意力,用于进一步提取输入的特征;第二个版本和经典注意力模型一样,把输出序列和输入序列关联了起来。
刚才我们学习的是Transformer的主要结构。实际上,Transformer的结构里还有很多细节。
- Positional Encoding (位置编码): 和RNN不同,Transformer的多头注意力无法区分每一个输入的顺序。为了告诉模型每一个token的先后顺序,词嵌入在输入模型之前还要加上一个Positional Encoding。这个编码会给每一个词嵌入的每一维加上一个三角函数值。三角函数之间的差具有周期性,模型能够从这个值里认识到输入序列的顺序信息。
- Add & Norm 层:参考深度CNN的结构,Transformer给每个模块的输出都做了一次归一化,并且使用了残差连接。
- 输出前的线性层和Softmax:为了输出一个单词,我们肯定要做一个softmax。这些层是输出单词的常规操作,和RNN机器翻译模型里的相同。



最后,在训练Tranformer时,还有一个重要的模块:Masked Multi-head Attention。刚刚我们学到,在输出翻译的句子时,我们要输出一个单词,更新一次decoder的输入;输出一个单词,更新一次decoder的输入。然而,在训练时,我们实际上已经有了正确的输出。因此,我们可以同时给decoder看前1个单词,看前2个单词……,并行地训练所有的这些情况。为了只让decoder看到前几个单词,我们可以给输入加一个mask,把后面那些不应该看到的单词“蒙上”。
Masked Multi-head Attention 其实只是一个实现上的小技巧,不能算作一个新模块。具体的实现方式其实有很多,我们只要重点理解这一设计的原因即可。总之,这种设计让Transformer能够并行地训练翻译进度不同的句子。因此,Transfomer的训练效率会比RNN快很多。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)