吴恩达深度学习笔记(五)

自然语言处理NLP:序列模型

循环神经网络RNN

为什么选择序列模型?
循环神经网络(RNN)之类的模型在语音识别、自然语言处理和其他领域中引起变革。序列模型能够应用在许多领域,例如:语音识别、音乐生成器、情感分类、DNA序列分析、机器翻译、视频动作识别、命名实体识别等。

在进行语音识别时,给定了一个输入音频片段 x x x,并要求输出对应的文字记录 y y y. 这个例子里输入和输出数据都是序列模型,因为 x x x是一个按时播放的音频片段,输出 y y y是一系列单词。

音乐生成问题是使用序列数据的另一个例子,在这个例子中,只有输出数据 y y y是序列,而输入数据可以是空集,也可以是个单一的整数,这个数可能指代想要生成的音乐风格,也可能是想要生成的那首曲子的头几个音符。

在处理情感分类时,输入数据 x x x是序列,会得到类似这样的输入:“There is nothing to like in this movie.”,这句评论对应几星?

系列模型在DNA序列分析中也十分有用,DNA可以用A、C、G、T四个字母来表示。所以给定一段DNA序列,标记出哪部分是匹配某种蛋白质。

在机器翻译过程中,得到这样的输入句:“Voulez-vou chante avecmoi?”,然后输出另一种语言的翻译结果。

在进行视频行为识别时,可能会得到一系列视频帧,然后识别其中的行为。

在进行命名实体识别时,可能会给定一个句子要识别出句中的人名。

所以这些问题都可以被称作使用标签数据 ( x , y ) (x,y) (x,y)作为训练集的监督学习。但从这一系列例子中可以看出序列问题有很多不同类型。有些问题里,输入数据 x x x和输出数据 y y y都是序列, x x x y y y有时也会不一样长。在另一些问题里,只有 x x x或者只有 y y y是序列。

数学符号
下面以命名实体识别为例,介绍序列模型的命名规则。建立一个能够自动识别句中人名位置的序列模型,它的输入语句是这样的:“Harry Potter and Herminoe Granger invented a new spell.”,这就是一个命名实体识别问题,这常用于搜索引擎,比如说索引过去24小时内所有新闻报道提及的人名。命名实体识别系统可以用来查找不同类型的文本中的人名、公司名、时间、地点、国家名和货币名等等。

现在给定这样的输入数据 x x x,假如想要一个序列模型输出 y y y,使得输入的每个单词都对应一个输出值,同时这个能够表明输入的单词是否是人名的一部分。 y : [ 1 1 0 1 1 0 0 0 0 ] \begin{matrix} y:[1& 1& 0& 1& 1& 0& 0& 0& 0]\\ \end{matrix} y[110110000]

技术上来说这也许不是最好的输出形式,还有更加复杂的输出形式,它不仅能够表明输入词是否是人名的一部分,还能够知道这个人名在这个句子里从哪里开始到哪里结束。

这个输入数据是9个单词组成的序列,所以最终会有9个特征集合来表示这9个单词,并按序列中的位置进行索引, x < 1 > , x < 2 > , ⋯   , x < 9 > x^{<1>},x^{<2>},\cdots,x^{<9>} x<1>,x<2>,,x<9>来索引不同的位置,用 x < t > x^{<t>} x<t>来索引这个序列的中间位置。 t t t意味着它们是时序序列。 x : [ x < 1 > x < 2 > x < 3 > x < 4 > x < 5 > x < 6 > x < 7 > x < 8 > x < 9 > ] \begin{matrix} x:[x^{<1>}& x^{<2>}& x^{<3>}& x^{<4>}& x^{<5>}& x^{<6>}& x^{<7>}& x^{<8>}& x^{<9>}]\\ \end{matrix} x[x<1>x<2>x<3>x<4>x<5>x<6>x<7>x<8>x<9>]

输出数据也一样,还是用 y < 1 > , y < 2 > , ⋯   , y < 9 > y^{<1>},y^{<2>},\cdots,y^{<9>} y<1>,y<2>,,y<9>来表示输出数据。同时用 T x T_x Tx来表示输入序列的长度,这个例子中输入是9个单词,所以 T x = 9 T_x=9 Tx=9。用 T y T_y Ty来表示输出序列的长度。这个例子中, T x = T y T_x=T_y Tx=Ty.

i i i个训练样本的序列中第 t t t个元素用 x ( i ) < t > x^{(i)<t>} x(i)<t>来表示。如果 T x T_x Tx是序列长度,那么训练集里不同的训练样本就会有不同的长度,所以 T x ( i ) T_x^{(i)} Tx(i)就代表第 i i i个训练样本的输入序列长度。

想要表示一个句子里的单词,第一件事是做一张词表,有时也称为词典,意思是列一列表示方法中用到的单词。这个词表中的第一个词是a,第二个单词是Aaron,然后更下面一些是单词and,再后面会找到Harry,然后找到Potter,这样一直到最后,词典里最后一个单词可能是Zulu。 [ a a a r o n ⋮ a n d ⋮ h a r r y ⋮ p o t t e r ⋮ z u l u ] 1 2 ⋮ 367 ⋮ 4075 ⋮ 6830 ⋮ 10000 \left[ \begin{array}{c} a\\ aaron\\ \vdots\\ and\\ \vdots\\ harry\\ \vdots\\ potter\\ \vdots\\ zulu\\ \end{array} \right] \begin{array}{c} 1\\ 2\\ \vdots\\ 367\\ \vdots\\ 4075\\ \vdots\\ 6830\\ \vdots\\ 10000\\ \end{array} aaaronandharrypotterzulu123674075683010000

所以在这个例子中用了10000个单词大小的词典,这对现代自然语言处理应用来说太小了。对于一般规模的商业应用来说30000到50000词大小的词典比较常见,而且有些大型互联网公司会用百万词,甚至更大的词典。

如果选定了10000词的词典,构建这个词典的一个方法是遍历训练集,并且找到前10000个常用词,也可以去浏览一些网络词典,获得英语里最常用的10000个单词,接下来可以用one-hot表示法来表示词典里的每个单词。

在这里 x < 1 > x^{<1>} x<1>表示Harry这个单词,它就是一个第4075行是1,其余值都是0的列向量。同样 x < 2 > x^{<2>} x<2>是个第6830行是1,其余位置都是0的向量。and在词典里排第367,所以 x < 3 > x^{<3>} x<3>就是第367行是1,其余值都是0的向量。如果词典大小是10000,那么这里的每个向量都是10000维的。

所以这种表示方法中, x < t > x^{<t>} x<t>指代句子里的任意词,它就是个one-hot向量,因为它只有一个值是1,其余值都是0,所以会有9个one-hot向量来表示这个句中的9个单词,目的是用这样的表示方式表示 x x x,用序列模型在 x x x和目标输出 y y y之间学习建立一个映射。

如果遇到了一个不在词表中的单词,答案就是创建一个新的标记,也就是一个叫做Unknow Word的伪造单词,用作为标记,来表示不在词表中的单词。

循环神经网络模型
使用标准神经网络,我们有9个输入单词。这9个输入单词,可能是9个one-hot向量,然后将它们输入到一个标准神经网络中,经过一些隐藏层,最终会输出9个值为0或1的项,它表明每个输入单词是否是人名的一部分。

这个方法并不好,主要有两个问题:

  • Inputs, outputs can be different lengths in different examples.
  • Doesn’t share features learned across different positions of text.

一是输入和输出数据在不同例子中可以有不同的长度,不是所有的例子都是 T x = T y T_x=T_y Tx=Ty。即使每个句子都有最大长度,也许能够填充(pad)或零填充(zero pad)使每个输入语句都达到最大长度,也就是说设定一个最大序列长度,对每个输入和输出序列补零并统一到最大长度。但仍然看起来不是一个好的表达方式。

二是它并不共享从文本的不同位置上学到的特征。具体来说,如果神经网络已经学习到了在位置1出现的Harry可能是人名的一部分,那么如果Harry出现在其他位置,比如 x < t > x^{<t>} x<t>时,它也能够自动识别其为人名的一部分的话,这就很棒了。这可能类似于在卷积神经网络中,希望将部分图片里学到的内容快速推广到图片的其他部分,我们希望对序列数据也有相似的效果,用一个更好的表达方式能够减少模型中参数的数量。

x < 1 > , ⋯   , x < t > , ⋯   , x < T x > x^{<1>},\cdots,x^{<t>},\cdots,x^{<T_x>} x<1>,,x<t>,,x<Tx>这些都是10000维的one-hot向量,因此这会是十分庞大的输入层。如果总的输入大小是最大单词数(最大序列长度)乘以10000,那么第一层的权重矩阵就会有着巨量的参数。

下面建立循环神经网络RNN。如果以从左到右的顺序读一个句子,第一个单词就是 x < 1 > x^{<1>} x<1>,将第一个词输入一个神经网络层,第一个神经网络的隐藏层,我们可以让神经网络尝试预测输出,判断这是否是人名的一部分。当它读到句中的第二个单词时,假设是 x < 2 > x^{<2>} x<2>,它不是仅用 x < 2 > x^{<2>} x<2>就预测出 y ^ < 2 > \hat{y}^{<2>} y^<2>,它也会输入一些来自时间步1的信息。具体而言,时间步1的激活值就会传递到时间步2。然后,在下一个时间步,循环神经网络输入了单词 x < 3 > x^{<3>} x<3>,然后它尝试预测输出了预测结果 y ^ < 3 > \hat{y}^{<3>} y^<3>,一直到最后一个时间步,输入了 x < T x > x^{<T_x>} x<Tx>,然后输出了 y ^ < T y > \hat{y}^{<T_y>} y^<Ty>。在这个例子中 T x = T y T_x=T_y Tx=Ty,如果 T x ≠ T y T_x\ne T_y Tx=Ty,这个结构会需要作出一些改变。所以在每一个时间步中,循环神经网络传递一个激活值到下一个时间步中用于计算。

要开始整个流程,在零时刻需要构造一个激活值 a < 0 > a^{<0>} a<0>,这通常是零向量。有些研究人员会随机用其他方法初始化 a < 0 > a^{<0>} a<0>,不过使用零向量作为零时刻的伪激活值是最常见的选择,因此把它输入神经网络。

循环神经网络是从左向右扫描数据,同时每个时间步的参数也是共享的,我们用 W a x W_{ax} Wax来表示管理着从 x < 1 > x^{<1>} x<1>到隐藏层的连接的一系列参数,每个时间步使用的都是相同的参数 W a x W_{ax} Wax。而激活值也就是水平联系(horizontal connections),是由参数 W a a W_{aa} Waa决定的,同时每一个时间步都使用相同的参数 W a a W_{aa} Waa,同样输出结果由参数 W y a W_{ya} Wya决定。

在这个循环神经网络中,在预测 y ^ < 3 > \hat{y}^{<3>} y^<3>时,不仅要使用 x < 3 > x^{<3>} x<3>的信息,还要使用来自 x < 1 > x^{<1>} x<1> x < 2 > x^{<2>} x<2>的信息。这个循环神经网络的一个缺点就是它只使用了这个序列中之前的信息来做出预测,尤其当预测 y ^ < 3 > \hat{y}^{<3>} y^<3>时,它没有用到 x < 4 > x^{<4>} x<4> x < 5 > x^{<5>} x<5> x < 6 > x^{<6>} x<6>等等的信息。如果给定了这个句子,He said, “Teddy Roosevelt was a great President.”,为了判断Teddy是否是人名的一部分,仅仅知道句中前两个词是完全不够的,还需要知道句中后部分的信息,这也是十分有用的,因为句子也可能是这样的,He said, “Teddy bears are on sale!”。因此如果只给定前三个单词,是不可能确切地知道Teddy是否是人名的一部分。

所以这样特定的神经网络结构的一个限制是它在某一时刻的预测仅使用了从序列之前的输入信息并没有使用序列中后部分的信息,双向循环神经网络(BRNN)可以处理这个问题。

一般开始先输入 a < 0 > a^{<0>} a<0>,它是一个零向量。接着就是前向传播过程,先计算激活值 a < 1 > a^{<1>} a<1>,然后再计算 y < 1 > y^{<1>} y<1>
a < 1 > = g 1 ( W a a a < 0 > + W a x x < 1 > + b a ) y ^ < 1 > = g 2 ( W y a a < 1 > + b y ) \begin{aligned} 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) \end{aligned} a<1>y^<1>=g1(Waaa<0>+Waxx<1>+ba)=g2(Wyaa<1>+by)

对于 W a x W_{ax} Wax,第二个下标 x x x意味着 W a x W_{ax} Wax要乘以某个 x x x类型的量,然后第一个下标 a a a表示它是用来计算某个 a a a类型的变量。同样的, W y a W_{ya} Wya乘上了某个 a a a类型的量,用来计算出某个 y ^ \hat{y} y^类型的量。

循环神经网络用的激活函数经常是tanh,不过有时候也会用ReLU,但是tanh是更通常的选择,我们有其他方法来避免梯度消失问题。选用哪个激活函数是取决于输出 y y y,如果它是一个二分类问题,那么会用Sigmoid函数作为激活函数,如果是 k k k类别分类问题的话,那么可以选用Softmax作为激活函数。

更一般的情况下,在 t t t时刻: a < t > = g 1 ( W a a a < t − 1 > + W a x x < t > + b a ) y ^ < t > = g 2 ( W y a a < t > + b y ) \begin{aligned} 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) \end{aligned} a<t>y^<t>=g1(Waaa<t1>+Waxx<t>+ba)=g2(Wyaa<t>+by)所以这些等式定义了神经网络的前向传播,可以从零向量 a < 0 > a^{<0>} a<0>开始,然后用 a < 0 > a^{<0>} a<0> x < 1 > x^{<1>} x<1>来计算出 a < 1 > a^{<1>} a<1> y ^ < 1 > \hat{y}^{<1>} y^<1>,然后用 x < 2 > x^{<2>} x<2> a < 1 > a^{<1>} a<1>一起算出 a < 2 > a^{<2>} a<2> y ^ < 2 > \hat{y}^{<2>} y^<2>等等,从左到右完成前向传播。

写成矩阵形式: a < t > = g 1 ( W a [ a < t − 1 > x < t > ] + b a ) y ^ < t > = g 2 ( W y a < t > + b y ) \begin{aligned} a^{<t>} &= g_1(W_{a}\left[ \begin{array}{c} a^{<t-1>}\\ x^{<t>}\\ \end{array} \right]+b_a) \\ \hat{y}^{<t>} &= g_2(W_{y}a^{<t>}+b_y) \end{aligned} a<t>y^<t>=g1(Wa[a<t1>x<t>]+ba)=g2(Wya<t>+by)其中 W a = [ W a a W a x ] W_a=\left[ \begin{matrix} W_{aa}& W_{ax}\\ \end{matrix} \right] Wa=[WaaWax],视频中 [ a < t − 1 > x < t > ] \left[ \begin{array}{c} a^{<t-1>}\\ x^{<t>}\\ \end{array} \right] [a<t1>x<t>]写为 [ a < t − 1 > , x < t > ] \left[ \begin{matrix} a^{<t-1>},x^{<t>}\\ \end{matrix} \right] [a<t1>,x<t>].

通过时间的反向传播
当在编程框架中实现循环神经网络时,编程框架(PyTorch、TensorFlow等)通常会自动处理反向传播。在循环神经网络中,对反向传播的运行有一个粗略的认识还是非常有用的。反向传播的计算方向与前向传播基本上是相反的。回顾前向传播:

为了计算反向传播,先定义一个元素损失函数: L < t > ( y ^ < t > , y < t > ) = − y < t > l o g y ^ < t > − ( 1 − y < t > ) l o g ( 1 − y ^ < t > ) \mathcal{L}^{<t>}(\hat{y}^{<t>},y^{<t>})=-y^{<t>}log\hat{y}^{<t>}-(1-y^{<t>})log(1-\hat{y}^{<t>}) L<t>(y^<t>,y<t>)=y<t>logy^<t>(1y<t>)log(1y^<t>)

它对应的是序列中一个具体的词,如果它是某个人的名字,那么 y < t > {y}^{<t>} y<t>的值就是1,然后神经网络将输出这个词是名字的概率值,比如0.1。将它定义为标准逻辑回归损失函数,也叫交叉熵损失函数(Cross Entropy Loss)。所以这是关于单个位置上或者说某个时间步 t t t上某个单词的预测值的损失函数。

现在来定义整个序列的损失函数: L ( y ^ , y ) = ∑ t = 1 T x L < t > ( y ^ < t > , y < t > ) \mathcal{L}(\hat{y},y)=\sum_{t=1}^{T_x}{\mathcal{L}^{<t>}(\hat{y}^{<t>},y^{<t>})} L(y^,y)=t=1TxL<t>(y^<t>,y<t>)

反向传播算法需要在相反的方向上进行计算和传递信息,最终就是把前向传播的箭头都反过来,在这之后就可以计算出所有合适的量,然后就可以通过导数相关的参数,用梯度下降法来更新参数。

在这个反向传播的过程中,最重要的信息传递或者说最重要的递归运算就是这个从右到左的运算,这个算法有一个很别致的名字,叫做“通过(穿越)时间反向传播(backpropagation through time)”。取这个名字的原因是对于前向传播,需要从左到右进行计算,在这个过程中,时刻 t t t不断增加。而对于反向传播,需要从右到左进行计算,就像时间倒流。

反向传播过程就是从右到左分别计算 L ( y ^ , y ) \mathcal{L}(\hat{y},y) L(y^,y)对参数 W a , W y , b a , b y W_a,W_y,b_a,b_y Wa,Wy,ba,by的偏导数。

RNN反向传播示意图(有时间手推一波)

不同类型的循环神经网络
事实上,对于其他一些应用, T x T_x Tx T y T_y Ty并不一定相等。比如音乐生成这个例子, T x T_x Tx可以是长度为1甚至为空集。再比如电影情感分类,输出 y y y可以是1到5的整数,而输入是一个序列。还有一些情况,输入长度和输出长度不同,他们都是序列但长度不同,比如机器翻译,一个法语句子和一个英语句子不同数量的单词却能表达同一个意思。

所以应该修改基本的RNN结构来处理这些问题,参考Andrej Karpathy的博客,一篇叫做“The Unreasonable Effectiveness of Recurrent Neural Networks”的文章。

这个就叫做“多对多”(many-to-many)的结构,因为输入序列有很多的输入,而输出序列也有很多输出。

处理情感分类问题,这里 x x x可能是一段文本,比如一个电影的评论,“These is nothing to like in this movie.”,所以 x x x就是一个序列,而 y y y可能是从1到5的一个数字,或者是0或1,这代表正面评价和负面评价,而数字1到5代表电影是1星,2星,3星,4星还是5星。我们可以简化神经网络的结构,输入 x < 1 > x^{<1>} x<1> x < 2 > x^{<2>} x<2>,一次输入一个单词,我们不再在每个时间上都有输出了,而是让这个RNN网络读入整个句子,然后在最后一个时间上得到输出。所以这个神经网络叫做“多对一”(many-to-one)结构,因为它有很多输入,很多的单词,然后输出一个数字。

为了完整性,还要补充一个“一对一”(one-to-one)的结构,这个可能没有那么重要,这就是一个小型的标准的神经网络,输入 x x x然后得到输出 y y y

除了“多对一”的结构,也可以有“一对多”(one-to-many)的结构。音乐生成,目标是使用一个神经网络输出一些音符。对应于一段音乐,输入 x x x可以是一个整数,表示想要的音乐类型或者是想要的音乐的第一个音符,并且如果什么都不想输入, x x x可以是空的输入,可设为0向量。

这样这个神经网络的结构,首先是输入 x x x,然后得到RNN的输出,第一个值,然后就没有输入了,再得到第二个输出,接着输出第三个值等等,一直到合成这个音乐作品的最后一个音符,这里也可以写上输入 a < 0 > a^{<0>} a<0>。当生成序列时通常会把第一个合成的输出也喂给下一层,所以实际的网络结构最终就像上图这个样子。

对于“多对多”的结构还有就是输入和输出长度不同的情况。而对于像机器翻译这样的应用,输入句子的单词的数量,比如说一个法语的句子,和输出句子的单词数量,比如翻译成英语,这两个句子的长度可能不同,所以还需要一个新的神经网络结构如下图所示。这个网络的结构有两个不同的部分,这是一个编码器,获取输入,比如法语句子,这是解码器,它会读取整个句子,然后输出翻译成其他语言的结果。

这就是一个“多对多”结构的例子,严格来说,还有一种结构,就是“注意力”(attention based)结构,但是根据现在画的这些图不好理解这个模型。

总结一下这些各种各样的RNN结构,第一个是“一对一"的结构,当去掉 a < 0 > a^{<0>} a<0>时它就是一种标准类型的神经网络。还有一种“一对多”的结构(第二个),比如音乐生成或者序列生成。还有“多对一”,这是情感分类的例子。还有“多对多”的结构,命名实体识别就是“多对多”的例子,其中 T x = T y T_x=T_y Tx=Ty。最后还有一种“多对多”结构的其他版本,如机器翻译, T x ≠ T y T_x\ne T_y Tx=Ty.

现在了解了大部分基本的模块,这些差不多就是所有的神经网络了,除了序列生成。用这些RNN的基本模块,把它们组合在一起就可以构建各种各样的模型。

语言模型和序列生成
在自然语言处理中,构建语言模型是最基础的也是最重要的工作之一,并且能用RNN很好地实现。

比如做一个语音识别系统,某句语音有两种翻译:

  • The apple and pair salad was delicious.
  • The apple and pear salad was delicious.

应该更像第二种,事实上,这就是一个好的语音识别系统要帮助输出的东西,即使这两句话听起来是如此相似。而让语音识别系统去选择第二个句子的方法就是使用一个语言模型,利用语言模型得到各自语句的概率,选择概率最大的语句作为正确的翻译。

一个语音识别模型可能算出第一句话的概率是 P ( T h e   a p p l e   a n d   p a i r   s a l a d ) = 3.2 × 1 0 − 13 P(The\,apple\,and\,pair\, salad)=3.2\times10^{-13} P(Theappleandpairsalad)=3.2×1013而第二句话的概率是 P ( T h e   a p p l e   a n d   p e a r   s a l a d ) = 5.7 × 1 0 − 10 P(The\,apple\,and\,pear\,salad)=5.7\times10^{-10} P(Theappleandpearsalad)=5.7×1010比较这两个概率值,显然说的话更像是第二种,因为第二句话的概率比第一句高出1000倍以上,语音识别系统能够在这两句话中作出选择。

语言模型会告诉你某个特定的句子它出现的概率是多少。概率计算表达式为: P ( y < 1 > , y < 2 > , ⋯   , y < T y > ) P(y^{<1>},y^{<2>},\cdots,y^{<T_y>}) P(y<1>,y<2>,,y<Ty>)语言模型是两种系统的基本组成部分,一个是语音识别系统,还有机器翻译系统,它要能正确输出最接近的句子。而语言模型做的最基本工作就是输入一个句子,准确地说是一个文本序列, y < 1 > y^{<1>} y<1> y < 2 > y^{<2>} y<2>一直到 y < T y > y^{<T_y>} y<Ty>。对于语言模型来说,用 y y y来表示这些序列比用 x x x来表示要更好,然后语言模型会估计某个句子序列中各个单词出现的可能性。

为了使用RNN建立出这样的模型,首先需要一个训练集,包含一个很大的英文文本语料库(corpus)或者其它的语言,想用于构建模型的语言的语料库。语料库是自然语言处理的一个专有名词,意思就是很长的或者说数量众多的英文句子组成的文本。然后,对corpus的每句话进行切分词(tokenize)。建立Vocabulary,对每个单词进行one-hot编码。

假如在训练集中得到这么一句话,“Cats average 15 hours of sleep a day.”,要做的第一件事就是将这个句子标记化,建立一个字典,然后将每个单词都转换成对应的one-hot向量,也就是字典中的索引。可能还有一件事就是要定义句子的结尾,一般的做法就是增加一个额外的标记< EOS >,它表示句子的结尾,如果想要模型能够准确识别句子结尾的话,< EOS >标记可以被附加到训练集中每一个句子的结尾。于是在本例中如果加了< EOS >标记,这句话就会有9个输入, y < 1 > , y < 2 > , ⋯   , y < 9 > y^{<1>},y^{<2>},\cdots,y^{<9>} y<1>,y<2>,,y<9>. 在标记化的过程中,可以自行决定要不要把标点符号看成标记,在本例中,忽略了标点符号,所以只把day看成标志,不包括后面的句号,如果想把句号或者其他符号也当作标志,那么可以将句号也加入字典中。

现在还有一个问题如果训练集中有一些词并不在字典里,比如说字典有10000个最常用的英语单词。现在这个句子“The Egyptian Mau is a bread of cat.”其中有一个词Mau,它可能并不是预先的那10000个最常用的单词,在这种情况下,可以把Mau替换成一个叫做< UNK >的代表未知词的标志,只针对< UNK >建立概率模型。

The Egyptian < UNK > is a bread of cat. < EOS >

完成标识化的过程后,这意味着输入的句子都映射到了各个标志上,或者说字典中的各个词上。下一步要使用一个RNN来构建这些序列的概率模型。

例子:Cats average 15 hours of sleep a day. 在第0个时间步,要计算激活项 a < 1 > a^{<1>} a<1>,它是以 x < 1 > x^{<1>} x<1>作为输入的函数,而 x < 1 > x^{<1>} x<1>会被设为全为0的集合,也就是0向量。 a < 0 > a^{<0>} a<0>也设为0向量,于是 a < 1 > a^{<1>} a<1>会通过Softmax进行一些预测来计算出第一个词可能会是什么,其结果就是 y ^ < 1 > \hat{y}^{<1>} y^<1>,这一步其实是通过一个Softmax层来预测字典中的任意单词会是第一个词的概率,比如说第一个词是 a a a的概率有多少 P ( a ) P(a) P(a),第一个词是Aaron的概率有多少 P ( a a r o n ) P(aaron) P(aaron),第一个词是cats的概率又有多少 P ( c a t s ) P(cats) P(cats),就这样一直到Zulu是第一个词的概率是多少 P ( z u l u ) P(zulu) P(zulu),还有第一个词是UNK的概率有多少 P ( < U N K > ) P(<UNK>) P(<UNK>),还有第一个词是句子结尾标志的概率有多少 P ( < E O S > ) P(<EOS>) P(<EOS>),表示不必阅读。所以 y ^ < 1 > \hat{y}^{<1>} y^<1>的输出是Softmax的计算结果,它只是预测第一个词的概率,而不去管结果是什么。在这个例子中,最终会得到单词Cats。所以Softmax层输出会有10002个结果,因为可能加上了未知词,还有句子结尾这两个额外的标志。

然后RNN进入下个时间步,在下一时间步中,仍然使用激活项 a < 1 > a^{<1>} a<1>,在这步要做的是计算出第二个词会是什么。现在依然传给它正确的第一个词,告诉它第一个词就是Cats,也就是 y ^ < 1 > \hat{y}^{<1>} y^<1>,这里 y < 1 > = x < 2 > y^{<1>}=x^{<2>} y<1>=x<2>。然后在第二个时间步中,输出结果同样经过Softmax层进行预测,RNN的职责就是预测这些词的概率,而不会去管结果是什么,它只会考虑之前得到的词。所以在这种情况下会是average.

然后再进行RNN的下个时间步,现在要计算 a < 3 > a^{<3>} a<3>。为了预测第三个词,也就是15,现在给它之前两个词,告诉它Cats average是句子的前两个词,所以这是下一个输入, x < 3 > = y < 2 > x^{<3>}=y^{<2>} x<3>=y<2>,输入average以后,现在要计算出序列中下一个词是什么,或者说计算出字典中每一个词的概率,通过之前得到的Cats和average,在这种情况下,正确结果会是15,以此类推。

一直到最后,会停在第9个时间步,然后把 x < 9 > x^{<9>} x<9>也就是 y < 8 > y^{<8>} y<8>传给它,也就是单词day,这里是 a < 9 > a^{<9>} a<9>,它会输出 y ^ < 9 > \hat{y}^{<9>} y^<9>,最后的得到结果会是< EOS >标志,在这一步中,通过前面这些得到的单词,不管它们是什么,我们希望能预测出< EOS >句子结尾标志的概率会很高。

所以RNN中的每一步都会考虑前面得到的单词,比如给它前3个单词,让它给出下个词的分布,这就是RNN如何学习从左往右地每次预测一个词。

接下来为了训练这个网络,要定义代价函数。于是,在某个时间步 t t t,如果真正的词是 y < t > y^{<t>} y<t>,而神经网络的Softmax层预测结果值是 y ^ < t > \hat{y}^{<t>} y^<t>,那么这就是Softmax损失函数: L < t > ( y ^ < t > , y < t > ) = − ∑ i y i < t > l o g y ^ i < t > \mathcal{L}^{<t>}(\hat{y}^{<t>},y^{<t>})=-\sum_i{y_i^{<t>}log\hat{y}_i^{<t>}} L<t>(y^<t>,y<t>)=iyi<t>logy^i<t>而总体损失函数: L = ∑ t L < t > ( y ^ < t > , y < t > ) \mathcal{L}=\sum_t{\mathcal{L}^{<t>}(\hat{y}^{<t>},y^{<t>})} L=tL<t>(y^<t>,y<t>)也就是把所有单个预测的损失函数都相加起来。

如果用很大的训练集来训练这个RNN,就可以通过开头一系列单词来预测之后单词的概率。即根据给出语句的前几个单词预测其余部分,将语句补充完整,例如给出“Cats average 15”,RNN模型可能预测完整的语句是“Cats average 15 hours of sleep a day.”。

整个语句出现的概率等于语句中所有元素出现的条件概率乘积。现在有一个新句子,它是 y < 1 > y^{<1>} y<1> y < 2 > y^{<2>} y<2> y < 3 > y^{<3>} y<3>,它只包含3个词,现在要计算出整个句子中各个单词的概率,方法就是第一个Softmax层会告诉你 y < 1 > y^{<1>} y<1>的概率,然后第二个Softmax层会告诉你在考虑 y < 1 > y^{<1>} y<1>的情况下 y < 2 > y^{<2>} y<2>的概率,然后第三个Softmax层告诉你在考虑 y < 1 > y^{<1>} y<1> y < 2 > y^{<2>} y<2>的情况下 y < 3 > y^{<3>} y<3>的概率,把这三个概率相乘,最后得到这个含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>)

对新序列采样
在训练一个序列模型之后,要想了解到这个模型学到了什么,一种非正式的方法就是利用训练好的RNN语言模型,进行新序列采样,从而随机产生新的语句。

一个序列模型模拟了任意特定单词序列的概率 P ( y < 1 > , ⋯   , y < T x > ) P(y^{<1>},\cdots,y^{<T_x>}) P(y<1>,,y<Tx>),我们要做的就是对这些概率分布进行采样来生成一个新的单词序列。

第一步是对模型生成的第一个词进行采样,于是输入 x < 1 > = 0 x^{<1>}=0 x<1>=0 a < 0 > = 0 a^{<0>}=0 a<0>=0,现在第一个时间步得到的 y ^ < 1 > \hat{y}^{<1>} y^<1>是所有可能的输出,是经过Softmax层后得到的概率,然后根据这个Softmax的分布进行随机采样。Softmax分布给的信息就是第一个词是a的概率是多少,是aaron的概率是多少,是zulu的概率是多少,是UNK的概率是多少,然后对这个向量使用例如numpy命令,np.random.choice,来根据向量中这些概率的分布进行采样,这样就能对第一个词进行采样了。

然后继续下一个时间步,现在要做的是把刚刚采样得到的 y ^ < 1 > \hat{y}^{<1>} y^<1>放到 a < 2 > a^{<2>} a<2>,作为下一个时间步的输入,所以不管在第一个时间步得到的是什么词,都要把它传递到下一个位置作为输入,然后Softmax层就会预测 y ^ < 2 > \hat{y}^{<2>} y^<2>是什么。举个例子,假如说对第一个词进行抽样后,得到的是The,The作为第一个词的情况很常见,然后把The当成 x < 2 > x^{<2>} x<2>,现在 x < 2 > = y ^ < 1 > x^{<2>}=\hat{y}^{<1>} x<2>=y^<1>,现在要计算出在第一词是The的情况下,第二个词应该是什么 P ( _ ∣ t h e ) P(\_|the) P(_the),然后得到的结果就是 y ^ < 2 > \hat{y}^{<2>} y^<2>,然后再次用这个采样函数来对 y ^ < 2 > \hat{y}^{<2>} y^<2>进行采样。

然后再到下一个时间步,无论得到什么样的用one-hot码表示的选择结果,都把它传递到下一个时间步,然后对第三个词进行采样。不管得到什么都把它传递下去,一直这样直到最后一个时间步。

如果代表句子结尾的标识在字典中,可以一直进行采样直到得到< EOS >标识,这代表着已经抵达结尾,可以停止采样了。另一种情况是,如果字典中没有这个词,可以决定从20个或100个或其他个单词进行采样,然后一直将采样进行下去直到达到所设定的时间步。不过这种过程有时候会产生一些未知标识,如果要确保算法不会输出这种标识,可以拒绝采样过程中产生任何未知的标识,一旦出现就继续在剩下的词中进行重采样,直到得到一个不是未知标识的词。

这就是如何从RNN语言模型中生成一个随机选择的句子。直到现在我们所建立的是基于词汇的RNN模型,意思就是字典中的词都是英语单词。

根据实际的应用,还可以构建一个基于字符的RNN结构(character level RNN),在这种情况下,字典包含从a到z的字母,可能还会有空格符,还可以有数字0到9,也可以再加上大写的字母,还可以实际地看一看训练集中可能会出现的字符,然后用这些字符组成字典。 V o c a b u l a r y = [ a , b , c , ⋯ , z , . , ; ,   , 0 , 1 , ⋯ , 9 , A , B , ⋯ , Z ] Vocabulary=[a,b,c,⋯,z,.,;,\,,0,1,⋯,9,A,B,⋯,Z] Vocabulary=[a,b,c,,z,.,;,,0,1,,9,A,B,,Z]

如果建立一个基于字符的语言模型,序列 y ^ < 1 > \hat{y}^{<1>} y^<1> y ^ < 2 > \hat{y}^{<2>} y^<2> y ^ < 3 > \hat{y}^{<3>} y^<3>在训练数据中将会是单独的字符。“Cat average 15 hours of sleep a day.”,在该例中C就是 y ^ < 1 > \hat{y}^{<1>} y^<1>,a就是 y ^ < 2 > \hat{y}^{<2>} y^<2>,t就是 y ^ < 3 > \hat{y}^{<3>} y^<3>,空格符就是 y ^ < 4 > \hat{y}^{<4>} y^<4>等等。

使用基于字符的语言模型有优点也有缺点,优点就是不必担心会出现未知的标识。不过基于字符的语言模型一个主要缺点就是最后会得到太多太长的序列,大多数英语句子只有10到20个的单词,但可能包含很多字符。所以基于字符的语言模型在捕捉句子中的依赖关系也就是句子较前部分如何影响较后部分不如基于词汇的语言模型那样可以捕捉长范围的关系,并且基于字符的语言模型训练起来计算成本比较高昂。

自然语言处理的趋势就是,绝大多数都是使用基于词汇的语言模型,但随着计算机性能越来越高,会有更多的应用。在一些特殊情况下,会开始使用基于字符的模型。但是这确实需要更昂贵的计算力来训练,所以现在并没有得到广泛地使用,除了一些比较专门需要处理大量未知的文本或者未知词汇的应用,还有一些要面对很多专有词汇的应用。

在现有的方法下,现在可以构建一个RNN结构,看一看英文文本的语料库,然后建立一个基于词汇的或者基于字符的语言模型,然后从训练的语言模型中进行采样。

这里有一些样本,它们是从一个语言模型中采样得到的,准确来说是基于字符的语言模型。如果模型是用新闻文章训练的,它就会生成左边这样的文本,这有点像一篇不太合乎语法的新闻文本,这句“Concussion epidemic”,to be examined,确实有点像新闻报道。用莎士比亚的文章训练后生成了右边这篇东西。

循环神经网络的梯度消失
基本的RNN算法还有一个很大的问题,就是梯度消失的问题。

现在举个语言模型的例子,假如看到这个句子,“The cat, which already ate ……, was full.”,前后应该保持一致,因为cat是单数,所以应该用was。“The cats, which ate ……, were full.”,cats是复数,所以用were。这个例子中的句子有长期的依赖,最前面的单词对句子后面的单词有影响。目前见到的基本的RNN模型,不擅长捕获这种长期依赖效应。

对于训练很深的网络,讨论过梯度消失的问题。比如说一个很深很深的网络,对这个网络从左到右做前向传播然后再反向传播。如果这是个很深的神经网络,从输出 y ^ \hat{y} y^得到的梯度很难传播回去,很难影响靠前层的权重和前面层的计算。

对于有同样问题的RNN,首先从左到右前向传播,然后反向传播。但是反向传播会很困难,因为同样的梯度消失的问题,后面层的输出误差( y ^ < T y > \hat{y}^{<T_y>} y^<Ty>)很难影响前面层(时间步1和2)的计算。这就意味着,实际上很难让一个神经网络能够意识到它要记住看到的是单数名词还是复数名词,然后在序列后面生成依赖单复数形式的was或者were。而且在英语句子里面,中间的内容(上面句子省略号处)可以任意长。所以需要长时间记住单词是单数还是复数,这样后面的句子才能用到这些信息。

也正是这个原因,所以基本的RNN模型会有很多局部影响,意味着这个输出 y ^ < 3 > \hat{y}^{<3>} y^<3>主要受 y ^ < 3 > \hat{y}^{<3>} y^<3>附近的值( x < 1 > , x < 2 > , x < 3 > x^{<1>},x^{<2>},x^{<3>} x<1>,x<2>,x<3>)的影响, y ^ < T y > \hat{y}^{<T_y>} y^<Ty>基本上很难受到序列靠前的输入( x < 1 > , x < 2 > x^{<1>},x^{<2>} x<1>,x<2>)的影响,这是因为不管输出是什么,不管是对的还是错的,这个区域都很难反向传播到序列的前面部分,也因此网络很难调整序列前面的计算。这是基本的RNN算法的一个缺点。

在讲很深的神经网络时,也提到了梯度爆炸,在反向传播的时候,随着层数的增多,梯度不仅可能指数型的下降,也可能指数型的上升。事实上梯度消失在训练RNN时是首要的问题,尽管梯度爆炸也是会出现,但是梯度爆炸很明显,因为指数级大的梯度会让参数变得极其大,以至于网络参数崩溃。所以梯度爆炸很容易发现,因为参数会大到崩溃,会看到很多NaN,或者不是数字的情况,这意味着网络计算出现了数值溢出。如果发现了梯度爆炸的问题,一个解决方法就是用梯度修剪(Gradient Clipping)。梯度修剪就是观察梯度向量,如果它大于某个阈值,缩放梯度向量(尺度缩小),保证它不会太大,这就是通过一些最大值来修剪的方法。所以如果遇到了梯度爆炸,如果导数值很大,或者出现了NaN,就用梯度修剪,这是相对比较鲁棒的梯度爆炸的解决方法。

训练很深的神经网络时,随着层数的增加,导数有可能指数型的下降或者指数型的增加,可能会遇到梯度消失或者梯度爆炸的问题。加入一个RNN处理1000个时间序列的数据集或者10000个时间序列的数据集,这就是一个1000层或者10000层的神经网络,这样的网络就会遇到上述类型的问题。梯度爆炸基本上用梯度修剪就可以应对,但梯度消失比较棘手。

GRU单元
这节难度较大,有时间看文末的参考资料好好理解!
门控循环单元,它改变了RNN的隐藏层,使其可以更好地捕捉深层连接,并改善了梯度消失问题。

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(Wa[a<t1>,x<t>]+ba),在RNN的时间 t t t处,计算激活值。输入 a < t − 1 > a^{<t-1>} a<t1>,即上一个时间步的激活值,再输入 x < t > x^{<t>} x<t>,再把这两个并起来,然后乘上权重项,在这个线性计算之后,如果 g g g是一个tanh激活函数,再经过tanh计算之后,它会计算出激活值 a < t > a^{<t>} a<t>。然后激活值将会传Softmax单元,或者其他用于产生输出 y ^ < t > \hat{y}^{<t>} y^<t>的东西。

GRU参考论文

The cat, which already ate……, was full.”,需要记得猫是单数的,“The cat was full.”或者是“The cats were full”。当我们从左到右读这个句子,GRU单元将会有个新的变量称为 c c c,代表记忆细胞(memory cell),记忆细胞的作用是提供了记忆的能力,比如说一只猫是单数还是复数,所以当它看到之后的句子的时候,它仍能够判断句子的主语是单数还是复数。于是在时间 t t t处,有记忆细胞 c < t > c^{<t>} c<t>,GRU实际上输出了激活值 a < t > a^{<t>} a<t> c < t > = a < t > c^{<t>}=a^{<t>} c<t>=a<t>。于是想要使用不同的符号 c c c a a a来表示记忆细胞的值和输出的激活值,即使它们是一样的。

所以这些等式表示了GRU单元的计算,在每个时间步,将用一个候选值 c ~ < t > \tilde{c}^{<t>} c~<t>重写记忆细胞,替代了 c < t > c^{<t>} c<t>的值。然后用tanh激活函数来计算, 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>=tanh(Wc[c<t1>,x<t>]+bc).

在GRU中真正重要的思想是有一个门叫做 Γ u \Gamma_u Γu u u u代表更新(update)门,这是一个0到1之间的值。实际上这个值是把这个式子带入Sigmoid函数得到的, Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) \Gamma_u=\sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γu=σ(Wu[c<t1>,x<t>]+bu). Sigmoid函数的输出值总是在0到1之间,对于大多数可能的输入,Sigmoid函数的输出总是非常接近0或者非常接近1。当 Γ u = 1 \Gamma_u=1 Γu=1时,代表更新;当 Γ u = 0 \Gamma_u=0 Γu=0时,代表记忆,保留之前的模块输出。这一点跟CNN中的ResNets的作用有点类似。

然后GRU的关键部分就是用 c ~ \tilde{c} c~更新 c c c的等式。然后门决定是否要真的更新它。记忆细胞 c < t > c^{<t>} c<t>将被设定为0或者1,这取决于考虑的单词在句子中是单数还是复数,因为这里是单数情况,所以先假定它被设为了1,如果是复数的情况就把它设为0。然后GRU单元将会一直记住 c < t > c^{<t>} c<t>的值,直到(was)的位置, c < t > c^{<t>} c<t>的值还是1,这就告诉它,这是单数,所以用was。于是 Γ u \Gamma_u Γu的作用就是决定什么时候会更新这个值( c c c),特别是当看到词组the cat,这就是一个好时机去更新这个值。然后当使用完它的时候,“The cat, which already ate……, was full.”,然后就知道,不需要记住它了,可以忘记它了。

接下来要给GRU用的式子就是 c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast\tilde{c}^{<t>}+(1-\Gamma_u)\ast c^{<t-1>} c<t>=Γuc~<t>+(1Γu)c<t1>. 如果 Γ u = 1 \Gamma_u=1 Γu=1,则 c < t > = c ~ < t > c^{<t>}=\tilde{c}^{<t>} c<t>=c~<t>. 对于所有在这中间的值,应该把门的值设为0,即 Γ u = 0 \Gamma_u=0 Γu=0,不更新它,就用旧的值, c < t > = c < t − 1 > c^{<t>}=c^{<t-1>} c<t>=c<t1>. 从左到右扫描这个句子,当门值为0的时候(上图中间 Γ u \Gamma_u Γu一直为0,表示一直不更新),就是说不更新它的时候,不要更新它,就用旧的值,也不要忘记这个值是什么,这样即使一直处理句子到(was的位置), c < t > c^{<t>} c<t>一直等于 c < t − 1 > c^{<t-1>} c<t1>,于是它仍然记得猫是单数的。

公式总结:

  1. c < t − 1 > = a < t − 1 > c^{<t-1>}=a^{<t-1>} c<t1>=a<t1>
  2. 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>=tanh(Wc[c<t1>,x<t>]+bc)
  3. Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) \Gamma_u=\sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γu=σ(Wu[c<t1>,x<t>]+bu)
  4. c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast\tilde{c}^{<t>}+(1-\Gamma_u)\ast c^{<t-1>} c<t>=Γuc~<t>+(1Γu)c<t1>

GRU单元输入 c < t − 1 > c^{<t-1>} c<t1>,对于上一个时间步,先假设它正好等于 a < t − 1 > a^{<t-1>} a<t1>,所以把这个作为输入。然后 x < t > x^{<t>} x<t>也作为输入,然后把这两个用合适权重结合在一起,再用tanh计算,算出 c ~ < t > \tilde{c}^{<t>} c~<t>,公式2,即 c < t > c^{<t>} c<t>的替代值。

再用一个不同的参数集,通过sigmoid激活函数算出更新门 Γ u \Gamma_u Γu,公式3。最后所有的值通过另一个运算符(空白框)结合,公式4。它把 c < t − 1 > c^{<t-1>} c<t1> c ~ < t > \tilde{c}^{<t>} c~<t> Γ u \Gamma_u Γu作为输入一起产生记忆细胞的新值 c < t > c^{<t>} c<t>,所以 c < t > = a < t > c^{<t>}=a^{<t>} c<t>=a<t>。也可以把这个代入Softmax或者其他预测的东西。

问题:若 Γ u = 0 \Gamma_u=0 Γu=0,岂不是 a < t > = a < t − 1 > a^{<t>}=a^{<t-1>} a<t>=a<t1>

这就是一个简化过的GRU单元,它的优点就是通过门决定,当从左到右扫描一个句子的时候,这个时机是要更新某个记忆细胞,还是不更新,不更新直到真的需要使用记忆细胞的时候,这可能在句子之前就决定了。因为Sigmoid的值, Γ u \Gamma_u Γu门很容易取到0值,或者说非常接近0。在这样的情况下,这个更新式子就会变成 c < t > = c < t − 1 > c^{<t>}=c^{<t-1>} c<t>=c<t1>,这非常有利于维持细胞( c < t > c^{<t>} c<t>)的值,即使经过很多的时间步。这就是缓解梯度消失问题的关键,因此允许神经网络运行在非常庞大的依赖词上,比如说cat和was单词即使被中间的很多单词分割开。

一个细节 c < t > c^{<t>} c<t>可以是一个向量,如果有100维的隐藏的激活值,那么 c < t > c^{<t>} c<t>也是100维的, c ~ < t > \tilde{c}^{<t>} c~<t>也是相同的维度, Γ u \Gamma_u Γu也是相同的维度。 ∗ \ast 实际上就是元素对应的乘积(element-wise),所以如果门 Γ u \Gamma_u Γu是一个100维的向量,里面的值几乎都是0或者1,就是说这100维的记忆细胞 c < t > c^{<t>} c<t>就是要更新的比特

当然在实际应用中 Γ u \Gamma_u Γu不会真的等于0或者1,有时候它是0到1的一个中间值,但是这对于直观思考是很方便的,就把它当成完全确切的0或者就是确切的1。元素对应的乘积做的就是告诉GRU单元哪个记忆细胞的向量维度在每个时间步要做更新,所以可以选择保存一些比特不变,而去更新其他的比特。比如说可能需要一个比特来记忆猫是单数还是复数,其他比特来理解正在谈论食物,因为在谈论吃饭或者食物,然后稍后可能就会谈论“The cat was full.”,可以每个时间点只改变一些比特。(不理解)

对于完整的GRU单元要做的一个改变就是给记忆细胞的新候选值加上一个新的项,要添加一个门 Γ r \Gamma_r Γr,可以认为 r r r代表相关性(relevance)。这个 Γ u \Gamma_u Γu门告诉你计算出的 c < t > c^{<t>} c<t>的候选值 c ~ < t > \tilde{c}^{<t>} c~<t> c < t − 1 > c^{<t-1>} c<t1>有多大的相关性。计算这个门 Γ r \Gamma_r Γr需要参数,一个新的参数矩阵 W r W_r Wr Γ r = σ ( W r [ c < t − 1 > , x < t > ] + b r ) \Gamma_r=\sigma(W_r[c^{<t-1>},x^{<t>}]+b_r) Γr=σ(Wr[c<t1>,x<t>]+br).

公式总结:

  1. c < t − 1 > = a < t − 1 > c^{<t-1>}=a^{<t-1>} c<t1>=a<t1>
  2. c ~ < t > = t a n h ( W c [ Γ r ∗ c < t − 1 > , x < t > ] + b c ) \tilde{c}^{<t>}=tanh(W_c[\Gamma_r\ast c^{<t-1>},x^{<t>}]+b_c) c~<t>=tanh(Wc[Γrc<t1>,x<t>]+bc)
  3. Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) \Gamma_u=\sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γu=σ(Wu[c<t1>,x<t>]+bu)
  4. Γ r = σ ( W r [ c < t − 1 > , x < t > ] + b r ) \Gamma_r=\sigma(W_r[c^{<t-1>},x^{<t>}]+b_r) Γr=σ(Wr[c<t1>,x<t>]+br)
  5. c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast\tilde{c}^{<t>}+(1-\Gamma_u)\ast c^{<t-1>} c<t>=Γuc~<t>+(1Γu)c<t1>

有很多方法可以来设计这些类型的神经网络,然后为什么有 Γ r \Gamma_r Γr?为什么不用简单的版本?这是因为多年来研究者们试验过很多很多不同可能的方法来设计这些单元,去尝试让神经网络有更深层的连接,去尝试产生更大范围的影响,还有解决梯度消失的问题,GRU就是其中一个研究者们最常使用的版本,也被发现在很多不同的问题上也是非常健壮和实用的。但是GRU是一个标准版本,也就是最常使用的。然后另一个常用的版本被称为LSTM,表示长短时记忆网络,GRU和LSTM是在神经网络结构中最常用的两个具体实例。

如果看学术文章的话,有的时候会看到有些人使用另一种符号 h ~ \tilde{h} h~ u u u r r r h h h表示这些量( c ~ \tilde{c} c~ Γ u \Gamma_u Γu Γ r \Gamma_r Γr c c c)。GRU,即门控循环单元,这是RNN的其中之一。这个结构可以更好捕捉非常长范围的依赖,让RNN更加有效。

参考资料:
(1)GRU神经网络
(2)人人都能看懂的GRU

长短期记忆LSTM
这节难度很大,有时间看文末的参考资料好好理解!!
对于GRU我们有:

  1. c ~ < t > = t a n h ( W c [ Γ r ∗ c < t − 1 > , x < t > ] + b c ) \tilde{c}^{<t>}=tanh(W_c[\Gamma_r\ast c^{<t-1>},x^{<t>}]+b_c) c~<t>=tanh(Wc[Γrc<t1>,x<t>]+bc)
  2. Γ u = σ ( W u [ c < t − 1 > , x < t > ] + b u ) \Gamma_u=\sigma(W_u[c^{<t-1>},x^{<t>}]+b_u) Γu=σ(Wu[c<t1>,x<t>]+bu)(更新门)
  3. Γ r = σ ( W r [ c < t − 1 > , x < t > ] + b r ) \Gamma_r=\sigma(W_r[c^{<t-1>},x^{<t>}]+b_r) Γr=σ(Wr[c<t1>,x<t>]+br)(相关门)
  4. c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast\tilde{c}^{<t>}+(1-\Gamma_u)\ast c^{<t-1>} c<t>=Γuc~<t>+(1Γu)c<t1>
  5. a < t > = c < t > a^{<t>}=c^{<t>} a<t>=c<t>

c ~ < t > \tilde{c}^{<t>} c~<t>是代替记忆细胞的候选值,然后使用更新门 Γ u \Gamma_u Γu来决定是否要用 c ~ < t > \tilde{c}^{<t>} c~<t>更新 c < t > c^{<t>} c<t>.

LSTM是一个比GRU更加强大和通用的版本,论文Hochreiter & Schmidhuber 1997. Long Short-Term Memory,它在序列模型上有着巨大影响。这篇论文是挺难读懂的,它在深度学习社群有着重大的影响,深入讨论了梯度消失的理论。

  1. 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>=tanh(Wc[a<t1>,x<t>]+bc)
  2. Γ u = σ ( W u [ a < t − 1 > , x < t > ] + b u ) \Gamma_u=\sigma(W_u[a^{<t-1>},x^{<t>}]+b_u) Γu=σ(Wu[a<t1>,x<t>]+bu)
  3. Γ f = σ ( W f [ a < t − 1 > , x < t > ] + b f ) \Gamma_f=\sigma(W_f[a^{<t-1>},x^{<t>}]+b_f) Γf=σ(Wf[a<t1>,x<t>]+bf)
  4. Γ o = σ ( W o [ a < t − 1 > , x < t > ] + b o ) \Gamma_o=\sigma(W_o[a^{<t-1>},x^{<t>}]+b_o) Γo=σ(Wo[a<t1>,x<t>]+bo)
  5. c < t > = Γ u ∗ c ~ < t > + Γ f ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast \tilde{c}^{<t>}+\Gamma_f\ast c^{<t-1>} c<t>=Γuc~<t>+Γfc<t1>
  6. a < t > = Γ o ∗ t a n h ( c < t > ) a^{<t>}=\Gamma_o\ast tanh(c^{<t>}) a<t>=Γotanh(c<t>)

上面是LSTM主要的式子,在LSTM中不再有 a < t > = c < t > a^{<t>}=c^{<t>} a<t>=c<t>的情况,现在专门使用 a < t > a^{<t>} a<t>或者 a < t − 1 > a^{<t-1>} a<t1>,而不是用 c < t − 1 > c^{<t-1>} c<t1>,也不用相关门 Γ r \Gamma_r Γr. 像以前那样有一个更新门 Γ u \Gamma_u Γu和表示更新的参数 W u W_u Wu,LSTM的一个新特性是不只有一个更新门控制,要用遗忘门 Γ f \Gamma_f Γf来取代 1 − Γ u 1-\Gamma_u 1Γu. 然后有一个新的输出门 Γ o \Gamma_o Γo. 于是记忆细胞的更新值为上面公式5。所以这给了记忆细胞选择权去维持旧的值 c < t − 1 > c^{<t-1>} c<t1>或者就加上新的值 c ~ < t > \tilde{c}^{<t>} c~<t>,这里用了单独的更新门 Γ u \Gamma_u Γu和遗忘门 Γ f \Gamma_f Γf. 最后 a < t > = c < t > a^{<t>}=c^{<t>} a<t>=c<t>的式子会变成上面公式6(视频中少了tanh)。

下面这个图的灵感来自于Christopher Olah的一篇博客,标题是《理解LSTM网络》,翻译版见此处。这里的这张图跟他博客上的图是很相似的,但关键的不同可能是这张图用了 a < t − 1 > a^{<t-1>} a<t1> x < t > x^{<t>} x<t>来计算所有门值,即遗忘门 Γ f \Gamma_f Γf,更新门 Γ u \Gamma_u Γu以及输出门 Γ o \Gamma_o Γo. 然后也经过tanh函数来计算 c ~ < t > \tilde{c}^{<t>} c~<t>,这些值被用复杂的方式组合在一起,比如说元素对应的乘积或者其他的方式来从之前的 c < t − 1 > c^{<t-1>} c<t1>中获得 c < t > c^{<t>} c<t>.

如下图所示,这是其中一个,再把它们连起来,就是把它们按时间次序连起来,这里输入 x < 1 > x^{<1>} x<1>,然后 x < 2 > x^{<2>} x<2> x < 3 > x^{<3>} x<3>,然后可以把这些单元依次连起来,这里输出了上一个时间的 a a a a a a会作为下一个时间步的输入。上面这里有条线,这条线显示了只要正确地设置了遗忘门和更新门,LSTM是相当容易把 c < 0 > c^{<0>} c<0>的值一直往下传递到右边,比如 c < 3 > = c < 0 > c^{<3>}=c^{<0>} c<3>=c<0>。LSTM和GRU非常擅长于长时间记忆某个值,对于存在记忆细胞中的某个值,即使经过很长很长的时间步。

最常用的版本可能是门值不仅取决于 a < t − 1 > a^{<t-1>} a<t1> x < t > x^{<t>} x<t>,有时候也可以偷窥一下 c < t − 1 > c^{<t-1>} c<t1>的值,这叫做偷窥孔连接(peephole connection)。“偷窥孔连接”意思就是门值不仅取决于 a < t − 1 > a^{<t-1>} a<t1> x < t > x^{<t>} x<t>,也取决于上一个记忆细胞 c < t − 1 > c^{<t-1>} c<t1>的值,然后“偷窥孔连接”就可以结合这三个门( Γ u \Gamma_u Γu Γ f \Gamma_f Γf Γ o \Gamma_o Γo)来计算了。

  1. 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>=tanh(Wc[a<t1>,x<t>]+bc)
  2. Γ u = σ ( W u [ a < t − 1 > , x < t > , c < t − 1 > ] + b u ) \Gamma_u=\sigma(W_u[a^{<t-1>},x^{<t>},c^{<t-1>}]+b_u) Γu=σ(Wu[a<t1>,x<t>,c<t1>]+bu)
  3. Γ f = σ ( W f [ a < t − 1 > , x < t > , c < t − 1 > ] + b f ) \Gamma_f=\sigma(W_f[a^{<t-1>},x^{<t>},c^{<t-1>}]+b_f) Γf=σ(Wf[a<t1>,x<t>,c<t1>]+bf)
  4. Γ o = σ ( W o [ a < t − 1 > , x < t > , c < t − 1 > ] + b o ) \Gamma_o=\sigma(W_o[a^{<t-1>},x^{<t>},c^{<t-1>}]+b_o) Γo=σ(Wo[a<t1>,x<t>,c<t1>]+bo)
  5. c < t > = Γ u ∗ c ~ < t > + Γ f ∗ c < t − 1 > c^{<t>}=\Gamma_u\ast \tilde{c}^{<t>}+\Gamma_f\ast c^{<t-1>} c<t>=Γuc~<t>+Γfc<t1>
  6. a < t > = Γ o ∗ t a n h ( c < t > ) a^{<t>}=\Gamma_o\ast tanh(c^{<t>}) a<t>=Γotanh(c<t>)

LSTM主要的区别在于一个技术上的细节,比如这有一个100维的向量,有一个100维的隐藏的记忆细胞单元,然后比如 c < t − 1 > c^{<t-1>} c<t1>的第50个元素只会影响对应的那个门的第50个元素,所以关系是一对一的,于是并不是任意这100维的 c < t − 1 > c^{<t-1>} c<t1>可以影响所有的门元素。相反的, c < t − 1 > c^{<t-1>} c<t1>的第一个元素只能影响门的第一个元素,第二个元素影响对应的第二个元素,如此类推。

LSTM反向传播计算:(有空推导一遍!)

来源:http://www.ai-start.com/dl2017/html/lesson5-week1.html

在深度学习的历史上,LSTM是更早出现的,而GRU是最近才发明出来的,它可能源于Pavia在更加复杂的LSTM模型中做出的简化。研究者们在很多不同问题上尝试了这两种模型,看看在不同的问题不同的算法中哪个模型更好。

GRU的优点是这是个更加简单的模型,所以更容易创建一个更大的网络,而且它只有两个门,在计算性上也运行得更快,然后它可以扩大模型的规模。

但是LSTM更加强大和灵活,因为它有三个门。如果想选一个使用,LSTM在历史进程上是个更优先的选择,所以如果必须选一个,今天大部分的人还是会把LSTM作为默认的选择来尝试。虽然最近几年GRU获得了很多支持,而且感觉越来越多的团队也正在使用GRU,因为它更加简单,而且还效果还不错,它更容易适应规模更加大的问题。无论是GRU还是LSTM,都可以用它们来构建捕获更加深层连接的神经网络。

参考资料:
(1)人人都能看懂的LSTM
(2)理解 LSTM 网络
(3)如何从RNN起步,一步一步通俗理解LSTM
(4)LSTM的公式推导详解
(5)LSTM长短期记忆神经网络的学习与实现
(6)零基础入门深度学习(6) - 长短时记忆网络(LSTM)

双向循环神经网络
双向RNN模型,这个模型可以在序列的某点处不仅可以获取之前的信息,还可以获取未来的信息。

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

在判断第三个词Teddy是不是人名的一部分时,光看句子前面部分是不够的,为了判断 y ^ < 3 > \hat{y}^{<3>} y^<3>是0还是1,除了前3个单词,还需要更多的信息,因为根据前3个单词无法判断他们说的是Teddy bears,还是前美国总统Teddy Roosevelt,所以这是一个非双向的或者说只有前向的RNN。

为了简单,先用3个输入或者说一个只有3个单词的句子, x < 1 > x^{<1>} x<1> x < 3 > x^{<3>} x<3>。从这里开始的这个网络会有一个前向的循环单元叫做 a → < 1 > \overrightarrow{a}^{<1>} a <1> a → < 2 > \overrightarrow{a}^{<2>} a <2> a → < 3 > \overrightarrow{a}^{<3>} a <3>,向右的箭头来表示前向的循环单元,并且它们这样连接。这3个循环单元都有一个当前输入 x x x输入进去,得到预测的 y ^ < 1 > \hat{y}^{<1>} y^<1> y ^ < 2 > \hat{y}^{<2>} y^<2> y ^ < 3 > \hat{y}^{<3>} y^<3>.

增加一个反向循环层,这里有 a ← < 1 > \overleftarrow{a}^{<1>} a <1> a ← < 2 > \overleftarrow{a}^{<2>} a <2> a ← < 3 > \overleftarrow{a}^{<3>} a <3>左箭头代表反向连接。

同样,把网络这样向上连接,这个 a a a反向连接就依次反向向前连接。这个网络就构成了一个无环图。给定一个输入序列 x < 1 > x^{<1>} x<1> x < 3 > x^{<3>} x<3>,这个序列首先计算前向的 a → < 1 > \overrightarrow{a}^{<1>} a <1>,然后计算前向的 a → < 2 > \overrightarrow{a}^{<2>} a <2>,接着 a → < 3 > \overrightarrow{a}^{<3>} a <3>。而反向序列从计算 a ← < 3 > \overleftarrow{a}^{<3>} a <3>开始,反向进行,计算反向的 a ← < 2 > \overleftarrow{a}^{<2>} a <2>. 计算的是网络激活值,这不是反向而是前向的传播,而图中这个前向传播一部分计算是从左到右,一部分计算是从右到左。计算完了反向的 a ← < 2 > \overleftarrow{a}^{<2>} a <2>,可以用这些激活值计算反向的 a ← < 1 > \overleftarrow{a}^{<1>} a <1>,把所有这些激活值都计算完了就可以计算预测结果了。

举个例子,为了预测结果, 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(Wy[a <t>,a <t>]+by). 比如要观察时间3这里的预测结果,信息从 x < 1 > x^{<1>} x<1>过来,流经前向的 a → < 1 > \overrightarrow{a}^{<1>} a <1>到前向的 a → < 2 > \overrightarrow{a}^{<2>} a <2>,到前向的 a → < 3 > \overrightarrow{a}^{<3>} a <3>再到 y ^ < 3 > \hat{y}^{<3>} y^<3>,所以从 x < 1 > x^{<1>} x<1> x < 2 > x^{<2>} x<2> x < 3 > x^{<3>} x<3>来的信息都会考虑在内,而从 x < 4 > x^{<4>} x<4>来的信息会流过反向的 a ← < 4 > \overleftarrow{a}^{<4>} a <4>,到反向的 a ← < 3 > \overleftarrow{a}^{<3>} a <3>再到 y ^ < 3 > \hat{y}^{<3>} y^<3>. 这样使得时间3的预测结果不仅输入了过去和现在的信息,还有未来的信息,这一步涉及了前向和反向的传播信息以及未来的信息。

这就是双向RNN,并且这些基本单元不仅仅是标准RNN单元,也可以是GRU单元或者LSTM单元。事实上,很多的NLP问题,对于大量有自然语言处理问题的文本,有LSTM单元的双向RNN模型是用的最多的。所以如果有NLP问题,并且文本句子都是完整的,首先需要标定这些句子,一个有LSTM单元的双向RNN模型。

通过这些改变,就可以用一个用RNN或GRU或LSTM构建的模型,并且能够预测任意位置,即使在句子的中间,因为模型能够考虑整个句子的信息。这个双向RNN网络模型的缺点就是需要完整的数据的序列,才能预测任意位置。比如说要构建一个语音识别系统,那么双向RNN模型需要考虑整个语音表达,但是如果直接用这个去实现的话,需要等待这个人说完,然后获取整个语音表达才能处理这段语音,并进一步做语音识别。对于实际的语音识别的应用通常会有更加复杂的模块,而不是仅仅用我们见过的标准的双向RNN模型。但是对于很多自然语言处理的应用,如果总是可以获取整个句子,这个标准的双向RNN算法实际上很高效

BRNN能够同时对序列进行双向处理,性能大大提高。但是计算量较大,且在处理实时语音时,需要等到完整的一句话结束时才能进行分析。

深层循环神经网络
要学习非常复杂的函数,通常会把RNN的多个层堆叠在一起构建更深的模型。

一个标准的神经网络,首先是输入 x x x,然后堆叠上隐含层,所以这里应该有激活值,比如说第一层是 a [ 1 ] a^{[1]} a[1],接着堆叠上下一层,激活值 a [ 2 ] a^{[2]} a[2],可以再加一层 a [ 3 ] a^{[3]} a[3],然后得到预测值 y ^ \hat{y} y^

我们不再用原来的 a < 0 > a^{<0>} a<0>表示0时刻的激活值,而是用 a [ 1 ] < 0 > a^{[1]<0>} a[1]<0>来表示第一层,用 a [ l ] < t > a^{[l]<t>} a[l]<t>来表示第 l l l层第 t t t个时间点的激活值。然后我们把这些堆叠在上面,这就是一个有三个隐层的新的网络。

看看这个值( a [ 2 ] < 3 > a^{[2]<3>} a[2]<3>)是怎么算的。激活值 a [ 2 ] < 3 > a^{[2]<3>} a[2]<3>有两个输入,一个是从下面过来的输入,还有一个是从左边过来的输入: a [ 2 ] < 3 > = g ( W a [ 2 ] [ a [ 2 ] < 2 > , a [ 1 ] < 3 > ] + b a [ 2 ] ) a^{[2]<3>}=g(W_a^{[2]}[a^{[2]<2>},a^{[1]<3>}]+b_a^{[2]}) a[2]<3>=g(Wa[2][a[2]<2>,a[1]<3>]+ba[2])这就是这个激活值的计算方法。参数 W a [ 2 ] W_a^{[2]} Wa[2] b a [ 2 ] b_a^{[2]} ba[2]在这一层的计算里都一样,相对应地第一层也有自己的参数 W a [ 1 ] W_a^{[1]} Wa[1] b a [ 1 ] b_a^{[1]} ba[1].

由于时间的维度,RNN网络会变得相当大,很少会看到这种网络堆叠到100层。另外一种Deep RNNs结构是每个输出层上还有一些垂直单元,就是把这里的输出去掉,然后换成一些深的层,这些层并不水平连接,只是一个深层的网络,然后用来预测 y ^ < 1 > \hat{y}^{<1>} y^<1>. 同样这里也加上一个深层网络,然后预测 y ^ < 2 > \hat{y}^{<2>} y^<2>。这种类型的网络结构用的会稍微多一点,这种结构有三个循环单元,在时间上连接,接着一个网络在后面接一个网络,当然 y ^ < 3 > \hat{y}^{<3>} y^<3> y ^ < 4 > \hat{y}^{<4>} y^<4>也一样。

最简单的RNN模型,也可以是GRU单元或者LSTM单元,并且也可以构建深层的双向RNN网络。由于深层的RNN训练需要很多计算资源,需要很长的时间,尽管看起来没有多少循环层,这个在时间上连接了三个深层的循环层。

自然语言处理与词嵌入

词汇表征
词嵌入(word embeddings)是语言表示的一种方式,可以让算法自动的理解一些类似的词,比如男人对女人,比如国王对王后,还有其他很多的例子。通过词嵌入的概念就可以构建NLP应用了,即使模型标记的训练集相对较小。

目前为止一直都是用词汇表来表示词,可能是10000个单词,我们一直用one-hot向量来表示词。比如如果man在词典里是第5391个,那么就可以表示成一个向量,只在第5391处为1,我们用 O 5391 O_{5391} O5391代表这个量,这里 O O O的代表one-hot. 接下来,如果woman是编号9853,那么就可以用 O 9853 O_{9853} O9853来表示,这个向量只在9853处为1,其他为0,其他的词king、queen、apple、orange都可以这样表示出来,这种表示方法的一大缺点就是它把每个词孤立起来,这样使得算法对相关词的泛化能力不强。

假如已经学习到了一个语言模型,当看到“I want a glass of orange ____",那么下一个词很可能是juice. 即使学习算法已经学到了“I want a glass of orange juice”这样一个句子,但如果看到“I want a glass of apple ____”,因为算法不知道apple和orange的关系很接近,就像man和woman,king和queen一样。所以算法很难从已经知道的orange juice是一个常见的东西,而明白apple juice也是很常见的东西或者说常见的句子。这是因为任何两个one-hot向量的内积都是0,很难区分它们之间的差别,所以无法知道apple和orange要比king和orange,或者queen和orange相似的多。在NLP中,我们更希望能掌握不同单词之间的相似程度。

(特征表征)用特征化的表示来表示每个词,man,woman,king,queen,apple,orange或者词典里的任何一个单词,学习这些词的特征或者数值。使用一个特征向量表征单词,特征向量的每个元素都是对该单词某一特征的量化描述,量化范围可以是 [ − 1 , 1 ] [-1,1] [1,1]之间。

举个例子,比如想知道这些词与Gender的关系。假定男性的性别为-1,女性的性别为+1,那么man的性别值就是-1,而woman就是-1。最终根据经验king就是-0.95,queen是+0.97,apple和orange没有性别可言。

另一个特征可以是这些词有多Royal(高贵),所以这些词,man,woman和高贵没太关系,所以它们的特征值接近0。而king和queen很高贵,apple和orange跟高贵也没太大关系。

那么Age呢?man和woman一般没有年龄的意思,也许man和woman隐含着成年人的意思,但也可能是介于young和old之间,所以它们的值也接近0。而通常king和queen都是成年人,apple和orange跟年龄更没什么关系了。

还有一个特征,这个词是否是Food,man不是食物,woman不是食物,king和queen也不是,但apple和orange是食物。当然还可以有很多的其他特征,从Size,Cost,这个东西是不是Alive,是不是一个Action,或者是不是Noun或者是不是Verb,等等。

假设有300个不同的特征,这样的话就有了这一列数字(300个),组成了一个300维的向量来表示man这个词,用 e 5391 e_{5391} e5391这个符号来表示。同样用 e 9853 e_{9853} e9853代表这个300维的向量,用来表示woman这个词,其他的例子也一样。现在,如果用这种表示方法来表示apple和orange这些词,那么apple和orange的这种表示肯定会非常相似,可能有些特征不太一样,比如orange的颜色和apple的不太一样,但总的来说apple和orange的大部分特征实际上都一样,或者说都有相似的值。这样对于已经知道orange juice的算法很大几率上也会明白apple juice这个东西,这样对于不同的单词算法会泛化得更好。这种特征化单词的操作被称为Word Embeddings,即单词嵌入。

这种高维特征的表示能够比one-hot更好的表示不同的单词。而最终学习的特征不会像这里一样这么好理解,新的特征表示的东西肯定会更难搞清楚。尽管如此,接下来要学的特征表示方法却能使算法高效地发现apple和orange会比king和orange,queen和orange更加相似。

如果能够学习到一个300维的特征向量,或者说300维的词嵌入,通常把这300维的数据嵌入到一个二维空间里(降维),这样就可以可视化了。常用的可视化算法是t-SNE算法,来自于论文:van der Maaten and Hinton., 2008. Visualizing data using t-SNE. 如果观察这种词嵌入的表示方法,会发现man和woman这些词聚集在一块,king和queen聚集在一块,这些都是人,也都聚集在一起。动物都聚集在一起,水果也都聚集在一起,像1、2、3、4这些数字也聚集在一起。如果把这些生物看成一个整体,他们也聚集在一起。

这种词嵌入算法对于相近的概念,学到的特征也比较类似,在对这些概念可视化的时候,这些概念就比较相似,最终把它们映射为相似的特征向量。这种表示方式用的是在300维空间里的特征表示,这叫做嵌入(embeddings)。想象一个300维的空间,现在取每一个单词比如orange,它对应一个300维的特征向量,所以这个词就被嵌在这个300维空间里的一个点上了,apple这个词就被嵌在这个300维空间的另一个点上了。为了可视化,t-SNE算法把这个空间映射到低维空间,可以画出一个2维图像然后观察,这就是这个术语嵌入的来源。

使用词嵌入
例子:命名实体识别,假如有一个句子:“Sally Johnson is an orange farmer.”,会发现Sally Johnson就是一个人名,所以这里的输出为1。之所以能确定Sally Johnson是一个人名,是因为知道种橙子的农民一定是一个人,前面已经讨论过用one-hot来表示这些单词, x < 1 > x^{<1>} x<1> x < 2 > x^{<2>} x<2>等等。

但是如果用特征化表示方法,那么用词嵌入作为输入训练好的模型,如果看到一个新的输入:“Robert Lin is an apple farmer.”,因为知道orange和apple很相近,那么算法很容易就知道Robert Lin也是一个人的名字。要是测试集里是不太常见的词比如“Robert Lin is a durian cultivator.” 如果对于一个命名实体识别任务,只有一个很小的标记的训练集,训练集里甚至可能没有durian或者cultivator这两个词。但是如果有一个已经学好的词嵌入,它会告诉你durian是水果,就像orange一样,并且cultivator,做培育工作的人其实跟farmer差不多,那么就有可能从训练集里的“an orange farmer”归纳出“a durian cultivator”也是一个人。

词嵌入能够达到这种效果,其中一个原因就是学习词嵌入的算法会考察非常大的文本集,也许是从网上找到的,这样可以考察很大的数据集可以是1亿个单词,甚至达到100亿也都是合理的,大量的无标签的文本的训练集。通过考察大量的无标签文本,可以发现orange和durian相近,farmer和cultivator相近。因此学习这种嵌入表达,把它们都聚集在一块,通过读取大量的互联网文本发现了orange和durian都是水果。

接下来可以把这个词嵌入应用到命名实体识别任务当中,尽管只有一个很小的训练集,也许训练集里有100000个单词,甚至更小,这就可以使用迁移学习,把从互联网上免费获得的大量的无标签文本中学习到的知识,能够分辨orange、apple和durian都是水果的知识,然后把这些知识迁移到一个只有少量标记的训练数据集的命名实体识别任务中。事实上应该用一个双向的RNN。

总结一下,这是如何用词嵌入做迁移学习的步骤:

  • Learn word embeddings from large text corpus. (1-100B words) (Or download pre-trained embedding online.)
  • Transfer embedding to new task with smaller training set. (say, 100k words)
  • Optional: Continue to fine tune the word embeddings with new data.

第一步,先从大量的文本集中学习词嵌入。一个非常大的文本集,或者可以下载网上预训练好的词嵌入模型。

第二步,可以用这些词嵌入模型把它迁移到新的只有少量标注训练集的任务中,比如说用这个300维的词嵌入来表示单词。这样做的一个好处就是可以用更低维度的特征向量代替原来的10000维的one-hot向量,现在可以用一个300维更加紧凑的向量。

第三步,当在新的任务上训练模型时,在命名实体识别任务上,只有少量的标记数据集上,可以自己选择要不要继续微调,用新的数据调整词嵌入。实际中,只有这个第二步中有很大的数据集才会这样做。

当任务的训练集相对较小时,词嵌入的作用最明显,所以它广泛用于NLP领域(命名实体识别,文本摘要,文本解析,指代消解等)。

词嵌入在语言模型、机器翻译领域用的少一些,这些任务有大量的数据。在其他的迁移学习情形中也一样,如果从某一任务A迁移到某个任务B,只有A中有大量数据,而B中数据少时,迁移的过程才有用。

最后,词嵌入和人脸编码之间有奇妙的关系,对于人脸识别,我们训练了一个Siamese网络结构,这个网络会学习不同人脸的一个128维表示,然后通过比较编码结果来判断两个图片是否是同一个人脸,这个词嵌入的意思和这个差不多。在人脸识别领域用编码这个词来指代这些向量 f ( x ( i ) ) f(x^{(i)}) f(x(i)) f ( x ( j ) ) f(x^{(j)}) f(x(j)),人脸识别领域和这里的词嵌入有一个不同就是,在人脸识别中我们训练一个网络,任给一个人脸照片,甚至是没有见过的照片,神经网络都会计算出相应的一个编码结果。我们学习词嵌入则是有一个固定的词汇表,比如10000个单词,我们学习向量 e 1 e_1 e1 e 10000 e_{10000} e10000,学习一个固定的编码,每一个词汇表的单词的固定嵌入。这里的术语编码(encoding)和嵌入(embedding)可以互换,人脸识别中的算法未来可能涉及到海量的人脸照片,而自然语言处理有一个固定的词汇表,而像一些没有出现过的单词就记为未知单词(< UNK >)。

用词嵌入来实现这种类型的迁移学习,并且通过替换原来的one-hot表示,而是用之前的嵌入的向量,算法会泛化的更好,也可以从较少的标记数据中进行学习。

词嵌入的特性
词嵌入还有一个迷人的特性就是它还能帮助实现类比推理,尽管类比推理可能不是自然语言处理应用中最重要的,不过它能帮助人们理解词嵌入做了什么,以及词嵌入能够做什么。

man如果对应woman,那么king应该对应什么?应该都能猜到king对应queen,有一种算法能自动推导出这种关系。

用一个四维向量 e 5391 e_{5391} e5391来表示man,把它称为 e m a n e_{man} eman,而旁边这个表示woman的嵌入向量,称它为 e w o m a n e_{woman} ewoman,对king和queen也是用一样的表示方法。在该例中假设用的是四维的嵌入向量。这些向量有一个有趣的特性,就是对向量 e m a n e_{man} eman e w o m a n e_{woman} ewoman进行减法运算,即 e m a n − e w o m a n ≈ [ − 2 0 0 0 ] e_{man}-e_{woman}\approx\left[ \begin{array}{c} -2\\ 0\\ 0\\ 0\\ \end{array} \right] emanewoman2000

类似的,假如用 e k i n g e_{king} eking减去 e q u e e n e_{queen} equeen,最后也会得到一样的结果,即 e k i n g − e q u e e n ≈ [ − 2 0 0 0 ] e_{king}-e_{queen}\approx\left[ \begin{array}{c} -2\\ 0\\ 0\\ 0\\ \end{array} \right] ekingequeen2000

这个结果表示,man和woman主要的差异是gender上的差异,而king和queen之间的主要差异,根据向量的表示,也是gender上的差异,这就是为什么 e m a n − e w o m a n e_{man}-e_{woman} emanewoman e k i n g − e q u e e n e_{king}-e_{queen} ekingequeen结果是相同的。当算法被问及man对woman相当于king对什么时,算法所做的就是计算 e m a n − e w o m a n e_{man}-e_{woman} emanewoman,然后找出一个向量也就是找出一个词,使得 e m a n − e w o m a n ≈ e k i n g − e ? e_{man}-e_{woman}\approx e_{king}-e_{?} emanewomanekinge?,也就是说,当这个新词是queen时,式子的左边会近似地等于右边。

论文Mikolov et. al., 2013, Linguistic regularities in continuous space word representations,这是词嵌入领域影响力最为惊人和显著的成果之一,这种思想帮助了研究者们对词嵌入领域建立了更深刻的理解。

在下图中,词嵌入向量在一个可能有300维的空间里,于是单词man、woman、king、queen分别代表空间中的一个点,事实上,向量man和woman的差值非常接近于向量king和queen之间的差值。为了得出这样的类比推理,计算当man对于woman,那么king对于什么,找到单词 w w w来使得, e m a n − e w o m a n ≈ e k i n g − e w e_{man}-e_{woman}\approx e_{king}-e_{w} emanewomanekingew,即找到单词 w w w来最大化 e w e_w ew e k i n g − e m a n + e w o m a n e_{king}-e_{man}+e_{woman} ekingeman+ewoman的相似度: F i n d    w o r d    w : a r g m a x w    S i m ( e w , e k i n g − e m a n + e w o m a n ) Find\,\,word\,\,w:arg\underset{w}{max}\,\,Sim(e_w,e_{king}-e_{man}+e_{woman}) Findwordw:argwmaxSim(ew,ekingeman+ewoman)

我们有一些用于测算 e w e_w ew e k i n g − e m a n + e w o m a n e_{king}-e_{man}+e_{woman} ekingeman+ewoman之间的相似度的函数,然后通过方程找到一个使得相似度最大的单词,如果结果理想的话会得到单词queen. 如果查看一些研究论文就不难发现,通过这种方法来做类比推理准确率大概只有30%~75%,只要算法猜中了单词,就把该次计算视为正确,从而计算出准确率,在该例子中,算法选出了单词queen.

t-SNE算法所做的就是把这些300维的数据用一种非线性的方式映射到2维平面上,可以得知t-SNE中这种映射很复杂。在进行t-SNE映射之后,不能总是期望使等式 e m a n − e w o m a n ≈ e k i n g − e w e_{man}-e_{woman}\approx e_{king}-e_{w} emanewomanekingew成立的关系,会像上边那样成一个平行四边形,尽管在这个例子最初的300维的空间内可以依赖这种平行四边形的关系来找到使等式成立的一对类比,通过t-SNE算法映射出的图像可能是正确的。但在大多数情况下,由于t-SNE的非线性映射,就没法再指望这种平行四边形了,很多这种平行四边形的类比关系在t-SNE映射中都会失去原貌

一个最常用的相似度函数叫做余弦相似度,假如在向量 u u u v v v之间定义相似度: s i m ( u , v ) = u T v ∥ u ∥ 2 ∥ v ∥ 2 sim(u,v)=\frac{u^Tv}{\lVert u \rVert_2\lVert v \rVert_2} sim(u,v)=u2v2uTv. 分子其实就是 u u u v v v的内积,如果 u u u v v v非常相似,那么它们的内积将会很大。这个公式实际就是计算两向量夹角 ϕ \phi ϕ的余弦值。夹角为0度时,余弦相似度就是1,当夹角是90度角时余弦相似度就是0,当夹角是180度时,相似度等于-1。或者用距离相似度,用平方距离或者欧氏距离来表示: ∥ u − v ∥ 2 \lVert u-v \rVert^2 uv2. 距离越小,相似性越大。

如果向量 u u u v v v非常相似,它们的余弦相似度将接近1;如果它们不相似,则余弦相似度将取较小的值。 两个向量之间角度的余弦是衡量它们有多相似的指标,角度越小,两个向量越相似。

词嵌入的一个显著成果就是,可学习的类比关系的一般性。举个例子,它能学会man对于woman相当于boy对于girl,还能学习Ottawa对于Canada相当于Nairobi对于Kenya,这些都是国家中首都城市名字。它还能学习big对于bigger相当于tall对于taller,还能学习Yen对于Janpan,円(Yen)是日本的货币单位,相当于Ruble对于Russia。这些东西都能够学习,只要在大型的文本语料库上实现一个词嵌入学习算法,只要从足够大的语料库中进行学习,它就能自主地发现这些模式。

嵌入矩阵
假设词汇表含有10000个单词,词汇表里有a,aaron,orange,zulu,可能还有一个未知词标记< UNK >。我们要做的就是学习一个嵌入矩阵 E E E,它将是一个300×10000的矩阵,如果词汇表里有10000个,或者加上未知词就是10001维。假设orange的单词编号是6257,代表词汇表中第6257个单词,用符号 O 6527 O_{6527} O6527来表示这个one-hot向量,这个向量除了第6527个位置上是1,其余各处都为0,显然它是一个10000维的列向量。

假设这个嵌入矩阵叫做矩阵 E E E,如果用 E E E去乘以右边的one-hot向量 O 6527 O_{6527} O6527,那么就会得到一个300维的向量。最后得到的这个向量的第 k k k个元素就是orange这一列下的第 k k k个数字 ( 1 ⩽ k ⩽ 300 ) (1\leqslant k\leqslant 300) (1k300). 它等于 e 6527 e_{6527} e6527,这个符号是用来表示这个300×1的嵌入向量,它表示的单词是orange.

更广泛来说,假如说有某个单词 w w w,那么 e w e_w ew就代表单词 w w w的嵌入向量。同样, E O j EO_j EOj O j O_j Oj就是只有第 j j j个位置是1的one-hot向量,得到的结果就是 e j e_j ej,它表示的是字典中单词 j j j的嵌入向量。 E O j = e j EO_j=e_j EOj=ej

我们的目标是学习一个嵌入矩阵 E E E。在下节视频中将会随机地初始化矩阵 E E E,然后使用梯度下降法来学习这个300×10000的矩阵中的各个参数, E E E乘以这个one-hot向量会得到嵌入向量。

当动手实现时,用大量的矩阵和向量相乘来计算它,效率是很低下的,因为one-hot向量是一个维度非常高的向量,并且几乎所有元素都是0,所以矩阵向量相乘效率太低,因为我们要乘以一大堆的0。所以在实践中会使用一个专门的函数来单独查找矩阵 E E E的某列,例如在Keras中就有一个嵌入层(Embedding layer),用这个嵌入层更有效地从嵌入矩阵中提取出需要的列,而不是对矩阵进行很慢很复杂的乘法运算。

学习词嵌入
在深度学习应用于学习词嵌入的历史上,人们一开始使用的算法比较复杂,但随着时间推移,研究者们不断发现他们能用更加简单的算法来达到一样好的效果,特别是在数据集很大的情况下。

构建一个语言模型,并且用神经网络来实现这个模型。于是在训练过程中,可能想要神经网络能够做到输入:“I want a glass of orange ___.”,然后预测这句话的下一个词。实践证明,建立一个语言模型是学习词嵌入的好方法,参考论文Bengio et. al., 2003, A neural probabilistic language model.

从第一个词I开始,建立一个one-hot向量表示这个单词,在第4343个位置是1,它是一个10000维的向量。然后要做的就是生成一个参数矩阵 E E E,用 E E E乘以 o 4343 o_{4343} o4343,得到嵌入向量 e 4343 e_{4343} e4343. 然后对其他的词也做相同的操作。

于是现在有许多300维的嵌入向量。把它们全部放进神经网络中,经过神经网络以后再通过Softmax层,然后这个Softmax分类器(Softmax层有10000个概率输出)会在10000个可能的输出中预测结尾这个单词。假如说在训练集中有juice这个词,训练过程中Softmax的目标就是预测出单词juice,就是结尾的这个单词。这个隐藏层有自己的参数 W [ 1 ] W^{[1]} W[1] b [ 1 ] b^{[1]} b[1],这个Softmax层也有自己的参数 W [ 2 ] W^{[2]} W[2] b [ 2 ] b^{[2]} b[2]。如果它们用的是300维大小的嵌入向量,而这里有6个词,所以用6×300,所以这个输入会是一个1800维的向量,这是通过将这6个嵌入向量堆在一起得到的。

实际上更常见的是有一个固定的历史窗口,举个例子,想预测给定4个单词后的下一个单词,注意这里的4是算法的超参数。这就是如何适应很长或者很短的句子,方法就是总是只看前4个单词。如果一直使用一个4个词的历史窗口,则神经网络会输入一个1200维的特征变量到这个层中,然后再通过Softmax来预测输出。选择有很多种,用一个固定的历史窗口就意味着可以处理任意长度的句子,因为输入的维度总是固定的。

所以这个模型的参数就是矩阵 E E E,对所有的单词用的都是同一个矩阵 E E E。然后这些权重也都是算法的参数,可以用反向传播来进行梯度下降来最大化训练集似然(likelihood),通过序列中给定的4个单词去重复地预测出语料库中下一个单词什么。对足够的训练例句样本,运用梯度下降算法,迭代优化,最终求出嵌入矩阵 E E E

在这个算法的激励下,apple和orange会学到很相似的嵌入,这样做能够让算法更好地拟合训练集,因为它有时看到的是orange juice,有时看到的是apple juice。如果只用一个300维的特征向量来表示所有这些词,算法会发现要想最好地拟合训练集,就要使apple、orange、grape和pear等等,还有像durian这种很稀有的水果都拥有相似的特征向量。

这就是早期最成功的学习词嵌入,学习这个矩阵 E E E的算法之一。假设在训练集中有这样一个更长的句子:“I want a glass of orange juice to go along with my cereal.”。一般地,我们把输入叫做context,输出叫做target。算法预测出了某个单词juice,把它叫做目标词,它是通过一些上下文,这前4个词(Last 4 words)推导出来的。如果目标是学习一个嵌入向量,研究人员已经尝试过很多不同类型的上下文。如果要建立一个语言模型,那么一般选取目标词之前的几个词作为上下文。

比如说,可以提出这样一个学习问题,它的上下文是左边和右边的四个词(4 words on left & right),可以把目标词左右各4个词作为上下文。算法获得左边4个词,也就是a glass of orange,还有右边四个词to go along with,然后要求预测出中间这个词。这个问题需要将左边的还有右边这4个词的嵌入向量提供给神经网络,来预测中间的单词是什么,这也可以用来学习词嵌入。

或者想用一个更简单的上下文,也许只提供目标词的前一个词(Last 1 word),比如只给出orange这个词来预测orange后面是什么,这将会是不同的学习问题。可以构建一个神经网络,只把目标词的前一个词或者说前一个词的嵌入向量输入神经网络来预测该词的下一个词。

还有一个效果非常好的做法就是上下文是附近一个单词(Nearby 1 word),它可能会告诉你单词glass是一个邻近的单词。或者说看见了单词glass,然后附近有一个词和glass位置相近,那么这个词会是什么?这用的是一种Skip-Gram模型的思想。这是一个简单算法的例子,因为上下文相当的简单,比起之前4个词,现在只有1个,但是这种算法依然能工作得很好。事实证明,不同的context选择方法都能计算出较准确的 E E E.

如果真想建立一个语言模型,用目标词的前几个单词作为上下文是常见做法。但如果目标是学习词嵌入,那么就可以用这些其他类型的上下文,它们也能得到很好的词嵌入。

Word2Vec
这节有点难懂!
假设在训练集中给定了一个句子:“I want a glass of orange juice to go along with my cereal.”,在Skip-Gram模型中,抽取上下文和目标词配对,来构造一个监督学习问题。上下文不一定总是目标单词之前离得最近的四个单词,或最近的 n n n个单词。随机选一个词作为上下文词,比如选orange这个词,然后随机在一定词距内选另一个词,比如在上下文词前后5个词内或者前后10个词内,在这个范围内选择目标词。可能正好选到了juice或glass或my作为目标词。

参考论文Mikolov et. al., 2013. Efficient Estimation of Word Representations in Vector Space.

于是我们将构造一个监督学习问题,它给定上下文词,要求预测在这个词正负10个词距或者正负5个词距内随机选择的某个目标词。显然,这不是个非常简单的学习问题,因为在单词orange的正负10个词距之间,可能会有很多不同的单词。但是构造这个监督学习问题的目标并不是想要解决这个监督学习问题本身,而是想要使用这个学习问题来学到一个好的词嵌入模型

继续假设使用一个10000词的词汇表,有时训练使用的词汇表会超过一百万词。但我们要解决的基本的监督学习问题是学习一种映射关系,从上下文,记为c,比如单词orange,到某个目标词,记为t,可能是单词juice或者glass或者my。在我们的词汇表中,orange是第6257个单词,juice是第4834个,映射: x → y ( o r a n g e → j u i c e ) . x\rightarrow y(orange\rightarrow juice). xy(orangejuice).

为了表示输入,比如单词orange,可以先从one-hot向量开始,将其写作 o c o_c oc,这就是上下文词的one-hot向量。然后可以拿嵌入矩阵 E E E乘以向量 o c o_c oc,然后得到了输入的上下文词的嵌入向量,于是这里 e c = E o c e_c=Eo_c ec=Eoc. 在这个神经网络中,我们将把向量 e c e_c ec喂入一个Softmax单元。Softmax单元要做的就是输出 y ^ \hat{y} y^. 这是Softmax模型,预测不同目标词的概率: S o f t m a x : p ( t ∣ c ) = e θ t T e c ∑ j = 1 10000 e θ j T e c Softmax:p(t|c)=\frac{e^{\theta_t^Te_c}}{\sum_{j=1}^{10000}e^{\theta_j^Te_c}} Softmax:p(tc)=j=110000eθjTeceθtTec

这里 θ t \theta_t θt是一个与输出 t t t有关的参数, p p p即某个词 t t t和标签相符的概率是多少。这里省略了Softmax中的偏差项,想要加上的话也可以加上。

最终Softmax的损失函数如下,我们用 y y y表示目标词,这里用的 y y y y ^ \hat{y} y^都是用one-hot表示的, y ^ \hat{y} y^是一个从Softmax单元输出的10000维的向量,这个向量是所有可能目标词的概率: L ( y ^ , y ) = − ∑ i = 1 10000 y i l o g y ^ i \mathscr{L}(\hat{y},y)=-\sum_{i=1}^{10000}y_ilog\hat{y}_i L(y^,y)=i=110000yilogy^i

总结一下,这大体上就是一个可以找到词嵌入的简化模型和神经网络,其实就是个Softmax单元。矩阵 E E E将会有很多参数,有对应所有嵌入向量 e c e_c ec的参数,Softmax单元也有 θ t \theta_t θt的参数。如果优化这个关于所有这些参数的损失函数,就会得到一个较好的嵌入向量集,这个就叫做Skip-Gram模型。它把一个像orange这样的词作为输入,并预测上下文词的前面一些或者后面一些是什么词。

实际上使用这个算法会遇到一些问题,首要的问题就是计算速度。尤其是在Softmax模型中,每次想要计算这个概率,需要对你词汇表中的所有10000个词做求和计算,如果用了一个大小为100000或1000000的词汇表,那么这个分母的求和操作是相当慢的,实际上10000已经是相当慢的了,所以扩大词汇表就更加困难了。

这里有一些解决方案,如分级(hierarchical)的Softmax分类器和负采样。

使用一个分级的Softmax分类器,意思就是说不是一下子就确定到底是属于10000类中的哪一类。如果有一个分类器,它告诉你目标词是在词汇表的前5000个中还是在词汇表的后5000个词中,假如这个二分类器告诉你这个词在前5000个词中,然后第二个分类器会告诉你这个词在词汇表的前2500个词中,或者在词汇表的第二组2500个词中,诸如此类,直到最终找到一个词准确所在的分类器,那么就是这棵树的一个叶子节点。这好比是猜数字游戏,数字范围0~100。我们可以先猜50,如果分类器给出目标数字比50大,则继续猜75,以此类推,每次从数据区间中部开始。像这样一个树形的分类器,意味着树上内部的每一个节点都可以是一个二分类器,比如逻辑回归分类器,所以不需要对词汇表中所有的10000个词求和了。实际上用这样的分类树,计算成本与词汇表大小的对数成正比(log|v|),这个就叫做分级Softmax分类器(类似哈夫曼树)。

在实践中分级Softmax分类器不会使用一棵完美平衡的分类树或者说一棵左边和右边分支的词数相同的对称树。实际上,分级的Softmax分类器会被构造成常用词在顶部,然而不常用的词像durian会在树的更深处,因为更常见的词会更频繁,所以可能只需要少量检索就可以获得常用单词像the和of。然而更少见到的词比如durian就更合适在树的较深处,因为一般不需要到那样的深处,所以有不同的经验法则可以帮助构造分类树形成分级Softmax分类器。

一旦对上下文c进行采样,那么目标词t就会在上下文c的正负10个词距内进行采样。但是要如何选择上下文c?一种选择是可以就对语料库均匀且随机地采样,会发现有一些词,像the、of、a、and、to诸如此类是出现得相当频繁的,上下文到目标词的映射会相当频繁地得到这些种类的词,但是其他词,像orange、apple或durian就不会那么频繁地出现了。可能不会想要训练集都是这些出现得很频繁的词,因为这会导致花大部分的力气来更新这些频繁出现的单词的 e c e_c ec,但想要的是花时间来更新像durian这些更少出现的词的嵌入,即 e d u r i a n e_{durian} edurian。实际上词 p ( c ) p(c) p(c)的分布并不是单纯的在训练集语料库上均匀且随机的采样得到的,而是采用了不同的分级来平衡更常见的词和不那么常见的词。

这就是Word2Vec的Skip-Gram模型,论文实际上有两个不同版本的Word2Vec模型,Skip-Gram只是其中的一个,另一个叫做CBOW,即连续词袋模型(Continuous Bag-Of-Words Model),它获得中间词两边的上下文,然后用周围的词去预测中间的词,这个模型也很有效,也有一些优点和缺点。

总结下:CBOW是从原始语句推测目标字词;而Skip-Gram正好相反,是从目标字词推测出原始语句。CBOW对小型数据库比较合适,而Skip-Gram在大型语料中表现更好。 (下图左边为CBOW,右边为Skip-Gram)

来源:http://www.ai-start.com/dl2017/html/lesson5-week2.html#header-n138

负采样
这节比较难理解!
负采样,它能做到与Skip-Gram模型相似的事情,但是用了一个更加有效的学习算法。

论文:Mikolov et. al.,2013. Distributed Representations of Words and Phrases and their Compositionality.

例句:I want a glass of orange juice to go along with my cereal. 构造一个新的监督学习问题,给定一对单词,比如orange和juice,要去预测这是否是一对上下文词-目标词(context-target)。

先抽取一个上下文词,在一定词距内比如说正负10个词距内选一个目标词,在这个例子中orange和juice就是个正样本,用1作为标记。然后为了生成一个负样本,用相同的上下文词,再在字典中随机选一个词,在这里随机选了单词king,标记为0. 然后再拿orange,再随机从词汇表中选一个词,于是orange–book–0. 同样orange–the–0. 还有orange–of–0,注意of被标记为0,即使of的确出现在orange词的前面。

context(x)word(x)target?(y)
orangejuice1
orangeking0
orangebook0
orangethe0
orangeof0

总结一下,生成这些数据的方式是选择一个上下文词,再选一个目标词,表的第一行给了一个正样本。然后给定 k k k次,将用相同的上下文词,再从字典中选取随机的词,king、book、the、of等,并标记0,这些就会成为负样本。出现以下情况也没关系,就是如果从字典中随机选到的词,正好出现在了词距内,比如说在上下文词orange正负10个词之内。

接下来构造一个监督学习问题,其中学习算法输入 x x x,输入这对词,要去预测目标的标签,即预测输出 y y y。因此问题就是给定一对词,像orange和juice,这两个词是通过对靠近的两个词采样获得的还是分别在文本和字典中随机选取得到的呢?这个算法就是要分辨这两种不同的采样方式,这就是如何生成训练集的方法。

  • 小数据集, k k k从5到20比较好;
  • 大数据集, k k k从2到5比较好。

那么在这个例子中,我们就用 k = 4 k=4 k=4.

学习从 x x x映射到 y y y的监督学习模型,这是之前讨论的Softmax模型 p ( t ∣ c ) = e θ t T e c ∑ j = 1 10000 e θ j T e c p(t|c)=\frac{e^{\theta_t^Te_c}}{\sum_{j=1}^{10000}e^{\theta_j^Te_c}} p(tc)=j=110000eθjTeceθtTec. 为了定义模型,使用记号 c c c表示上下文词,记号 t t t表示可能的目标词,再用 y y y表示0和1,表示是否是一对上下文-目标词。定义一个逻辑回归模型,给定输入的 c , t c,t c,t对的条件下, y = 1 y=1 y=1的概率,即: P ( y = 1 ∣ c , t ) = σ ( θ t T e c ) P(y=1|c,t)=\sigma(\theta_t^Te_c) P(y=1c,t)=σ(θtTec)

将一个Sigmoid函数作用于 θ t T e c \theta_t^Te_c θtTec,对每一个可能的目标词有一个参数向量 θ t \theta_t θt和另一个向量 e c e_c ec,即每一个上下文词的的嵌入向量,用这个公式估计 y = 1 y=1 y=1的概率。如果有 k k k个样本,每一个正样本都有 k k k个对应的负样本来训练一个类似逻辑回归的模型。

如果输入词是orange,要做的就是输入one-hot向量,再传递给 E E E,通过两者相乘获得嵌入向量 e 6257 e_{6257} e6257,就得到了10000个可能的逻辑回归分类问题,其中一个将会是用来判断目标词是否是juice的分类器,还有其他的词,比如说可能下面的某个分类器是用来预测king是否是目标词,诸如此类,预测词汇表中这些可能的单词。

把这些看作10000个二分类逻辑回归分类器,但并不是每次迭代都训练全部10000个,只训练其中的5个,要训练对应真正目标词那一个分类器,再训练4个随机选取的负样本,这就是 k = 4 k=4 k=4的情况。所以不使用一个巨大的10000维度的Softmax,因为计算成本很高,而是把它转变为10000个二分类问题,每个都很容易计算,每次迭代只是训练它们其中的5个,一般而言就是 k + 1 k+1 k+1个,其中 k k k个负样本和1个正样本。这个算法计算成本更低,因为只需更新 k + 1 k+1 k+1个逻辑单元, k + 1 k+1 k+1个二分类问题,相对而言每次迭代的成本比更新10000维的Softmax分类器成本低。

这个技巧就叫负采样。有一个正样本词orange和juice,然后会特意生成一系列负样本,所以叫负采样,即用这4个负样本训练,4个额外的二分类器,在每次迭代中选择4个不同的随机的负样本词去训练算法。

在选取了上下文词orange之后,如何对这些词进行采样生成负样本?一个办法是对中间的这些词进行采样,即候选的目标词,可以根据其在语料中的经验频率进行采样,就是通过词出现的频率对其进行采样。但问题是这会导致在like、the、of、and诸如此类的词上有很高的频率。另一个极端就是用1除以词汇表总词数,即 1 ∣ v ∣ \frac{1}{|v|} v1,均匀且随机地抽取负样本,这对于英文单词的分布是非常没有代表性的。

所以论文的作者Mikolov等人根据经验,他们发现这个经验值的效果最好,它位于这两个极端的采样方法之间,既不用经验频率,也就是实际观察到的英文文本的分布,也不用均匀分布,他们采用以下方式: P ( w i ) = f ( w i ) 3 4 ∑ j = 1 10000 f ( w j ) 3 4 P(w_i)=\frac{f(w_i)^{\frac{3}{4}}}{\sum_{j=1}^{10000}{f(w_j)^{\frac{3}{4}}}} P(wi)=j=110000f(wj)43f(wi)43

进行采样,所以如果 f ( w i ) f(w_i) f(wi)是观测到的在语料库中的某个英文词的词频,通过 3 4 \frac{3}{4} 43次方的计算,使其处于完全独立的分布和训练集的观测分布两个极端之间(这里不懂)。很多研究者现在使用这个方法,似乎也效果不错。

在Softmax分类器中学到词向量,但是计算成本很高。通过将其转化为一系列二分类问题,因此可以非常有效地学习词向量。如果使用这个算法,将可以学到相当好的词向量。当然和深度学习的其他领域一样,有很多开源的实现,当然也有预训练过的词向量,就是其他人训练过的然后授权许可发布在网上的,所以如果想要在NLP问题上取得进展,去下载其他人的词向量是很好的方法,在此基础上改进。

GloVe词向量
这节十分难!!建议参考文末资料!
一个在NLP社区有着一定势头的算法是GloVe算法,这个算法并不如Word2Vec或是Skip-Gram模型用的多,但是也有人热衷于它,可能是因为它简便。

GloVe代表用词表示的全局变量(global vectors for word representation). 之前我们曾通过挑选语料库中位置相近的两个词,列举出词对,即上下文和目标词,GloVe算法做的就是使其关系开始明确化。假定 X i j X_{ij} Xij是单词 i i i在单词 j j j上下文中出现的次数(即 i i i j j j同时出现的次数),那么这里 i i i j j j就和 t t t c c c的功能一样,所以可以认为 X i j X_{ij} Xij等同于 X t c X_{tc} Xtc.

根据上下文和目标词的定义,大概会得出 X i j = X j i X_{ij}=X_{ji} Xij=Xji这个结论。事实上,如果将上下文和目标词的范围定义为出现于左右各10词以内的话,那么就会有一种对称关系。如果对上下文的选择是,上下文总是目标词前一个单词的话,那么 X i j X_{ij} Xij X j i X_{ji} Xji就不会像这样对称了。对于GloVe算法,可以定义上下文和目标词为任意两个位置相近的单词,假设是左右各10词的距离,那么 X i j X_{ij} Xij就是一个能够获取单词 i i i和单词 j j j出现位置相近时或是彼此接近的频率的计数器

GloVe模型做的就是进行优化,将它们之间的差距进行最小化处理: m i n i m i z e ∑ i = 1 10000 ∑ j = 1 10000 f ( X i j ) ( θ i T e j + b i + b j ′ − l o g X i j ) 2 minimize\sum_{i=1}^{10000}\sum_{j=1}^{10000}{f(X_{ij})(\theta_i^Te_j+b_i+b_j'-logX_{ij})^2} minimizei=110000j=110000f(Xij)(θiTej+bi+bjlogXij)2

从上式可以看出,若两个词的Embedding Vector越相近,同时出现的次数越多,则对应的Loss(minimize右边式子)越小。其中 θ i T e j \theta_i^Te_j θiTej,想一下 i i i j j j t t t c c c的功能一样,因此这就和 θ t T e c \theta_t^Te_c θtTec有些类似了,同时对于这个 θ t T e c \theta_t^Te_c θtTec来说,想要知道这两个单词之间有多少联系, t t t c c c之间有多紧密, i i i j j j之间联系程度如何,换句话说就是它们同时出现的频率是多少,这是由 X i j X_{ij} Xij影响的。然后解决参数 θ \theta θ e e e的问题,准备用梯度下降来最小化上面的公式,只想要学习一些向量,这样它们的输出能够对这两个单词同时出现的频率进行良好的预测。引入偏移量 b i , b j ′ b_i,b_j' bi,bj.

如果 X i j = 0 X_{ij}=0 Xij=0,那么 l o g 0 log0 log0就是未定义的,是负无穷大的,所以想要对 X i j = 0 X_{ij}=0 Xij=0时进行求和,就要添加一个额外的加权项 f ( X i j ) f(X_{ij}) f(Xij). 如果 X i j = 0 X_{ij}=0 Xij=0 f ( X i j ) = 0 f(X_{ij})=0 f(Xij)=0,同时会用一个约定,即 0 l o g 0 = 0 0log0=0 0log0=0,意思是如果 X i j = 0 X_{ij}=0 Xij=0,不要进行求和,所以这个 l o g 0 log0 log0项就是不相关项。上面的求和公式表明,这个和仅是一个上下文和目标词关系里连续出现至少一次( X i j > 0 X_{ij}>0 Xij>0)的词对的和。

f ( X i j ) f(X_{ij}) f(Xij)的另一个作用是,有些词在英语里出现十分频繁,比如说this,is,of,a等等,这叫做停止词,但是在频繁词和不常用词之间也会有一个连续统(continuum)。不过也有一些不常用的词,比如durion,还是想将其考虑在内。因此,这个加权因子 f ( X i j ) f(X_{ij}) f(Xij)就可以是一个函数,即使是像durion这样不常用的词,它也能给予大量有意义的运算,同时也能够给像this,is,of,a这些在英语里出现更频繁的词更大但不至于过分的权重。因此对加权函数 f f f的选择有着启发性的原则,就是既不给这些频繁词过分的权重,也不给这些不常用词太小的权值。具体可参考资料1。

最后, θ \theta θ e e e现在是完全对称的,所以 θ i \theta_i θi e j e_j ej就是对称的(why?)。如果只看数学式的话, θ i \theta_i θi e j e_j ej的功能其实很相近,可以将它们颠倒或者将它们进行排序,实际上它们都输出了最佳结果。因此一种训练算法的方法是一致地初始化 θ \theta θ e e e,然后使用梯度下降来最小化输出,当每个词都处理完之后取平均值,所以,给定一个词 w w w,使用优化算法得到所有参数之后,就会有 e w ( f i n a l ) = e w + θ w 2 e_w^{(final)}=\frac{e_w+\theta_w}{2} ew(final)=2ew+θw. 因为 θ \theta θ e e e在这个特定的公式里是对称的,而不像之前我们了解的模型, θ \theta θ e e e和功能不一样,因此也不能取平均。(不懂)

仅仅是最小化,像这样的一个二次代价函数是怎么能够学习有意义的词嵌入的呢?但是结果证明它确实有效,发明者们发明这个算法的过程中,以历史上更为复杂的算法,像是Newer Language模型,以及之后的Word2VecSkip-Gram模型等等为基础,同时希望能够简化所有之前的算法。

以这个表格作为例子来开始学习词向量,第一行的嵌入向量是来表示Gender的,第二行是来表示Royal的,然后是是Age,之后是Food等等。但是当使用GloVe算法来学习一个词嵌入时,不能保证嵌入向量的独立组成部分是能够理解的,为什么呢?

(不懂)假设说有个空间,里面的第一个轴是Gender,第二个轴是Royal,能够保证的是第一个嵌入向量对应的轴是和这个轴有联系的。具体而言,这个学习算法会选择这个作为第一维( e w , 1 e_{w,1} ew,1)的轴,所以给定一些上下文词,第一维可能是这个轴,第二维( e w , 2 e_{w,2} ew,2)也许是这个,它也可能是第二个非正交轴,它可以是学习到的词嵌入中的第二部分。当我们看到这个的时候,如果有某个可逆矩阵,那么这项就可以简单地替换成 ( A θ i ) T ( A − T e j ) (A\theta_i)^T(A^{-T}e_j) (Aθi)T(ATej),因为将其展开: ( A θ i ) T ( A − T e j ) = θ i T A T A − T e j = θ i T e j (A\theta_i)^T(A^{-T}e_j)=\theta_i^TA^TA^{-T}e_j=\theta_i^Te_j (Aθi)T(ATej)=θiTATATej=θiTej

不能保证这些用来表示特征的轴能够等同于人类可能简单理解的轴,具体而言,第一个特征可能是个Gender、Roya、Age、Food Cost和Size的组合,它也许是名词或是一个行为动词和其他所有特征的组合,所以很难看出独立组成部分,即这个嵌入矩阵的单行部分,然后解释出它的意思。尽管有这种类型的线性变换,这个平行四边形映射(?)也说明了我们解决了这个问题,当在类比其他问题时,这个方法也是行得通的。因此尽管存在特征量潜在的任意线性变换,最终还是能学习出解决类似问题的平行四边形映射。无论使用Skip-Gram模型还是GloVe模型等等,计算得到的Embedding Matrix E的每一个特征值不一定对应有实际物理意义的特征值。

参考资料:
(1)原论文:https://www.aclweb.org/anthology/D14-1162.pdf
(2)原项目(网页很难打开):GloVe: Global Vectors for Word Representation
(3)理解GloVe模型(+总结)
(4)高级词向量表达(一)——GloVe(理论、相关测评结果、R&python实现、相关应用)
(5)斯坦福CS224n视频动手学深度学习视频中科院NLP视频(2010)

情绪分类
情感分类任务就是看一段文本,然后分辨这个人是否喜欢他们在讨论的这个东西,这是NLP中最重要的模块之一,经常用在许多应用中。情感分类一个最大的挑战就是可能标记的训练集没有那么多,但是有了词嵌入,即使只有中等大小的标记的训练集,也能构建一个不错的情感分类器。

这是一个情感分类问题的一个例子,输入 x x x是一段文本,而输出 y y y是要预测的相应情感。比如说下图是一个餐馆评价的星级:

如果能训练一个从 x x x y y y的映射,基于这样的标记的数据集,那么就可以用来搜集大家对运营的餐馆的评价。如果有一个情感分类器,那么它就可以看一段文本然后分析出这个人对餐馆的评论的情感是正面的还是负面的,这样就可以一直记录是否存在一些什么问题,或者餐馆是在蒸蒸日上还是每况愈下。

情感分类一个最大的挑战就是可能标记的训练集没有那么多。对于情感分类任务来说,训练集大小从10000到100000个单词都很常见,甚至有时会小于10000个单词,采用了词嵌入能够带来更好的效果,尤其是只有很小的训练集时。

这是一个简单的情感分类的模型,假设有一个句子"dessert is excellent",然后在词典里找这些词,我们通常用10000个词的词汇表。我们要构建一个分类器能够把它映射成输出四个星,给定这四个词,我们取这些词,找到相应的one-hot向量,所以这里就是 o 8928 o_{8928} o8928,乘以嵌入矩阵 E E E E E E可以从一个很大的文本集里学习到,比如它可以从一亿个词或者一百亿个词里学习嵌入,然后用来提取单词the的嵌入向量 e 8928 e_{8928} e8928,对dessert、is、excellent做同样的步骤。

如果在很大的训练集上训练 E E E,比如一百亿的单词,这样就会获得很多知识,甚至从有些不常用的词中获取,然后应用到问题上,即使标记数据集里没有这些词。可以这样构建一个分类器,取这些向量,比如是300维度的向量。然后把它们求和或者求平均。这个单元会得到一个300维的特征向量,把这个特征向量送进Softmax分类器,然后输出 y ^ \hat{y} y^. 这个Softmax能够输出5个可能结果的概率值,从一星到五星,这个就是5个可能输出的Softmax结果用来预测 y y y的值。

这里用的平均值运算单元,这个算法适用于任何长短的评论,因为即使评论是100个词长,也可以对这一百个词的特征向量求和或者平均它们,然后得到一个表示一个300维的特征向量表示,然后把它送进Softmax分类器,所以这个平均值运算效果不错。它实际上会把所有单词的意思给平均起来,或者把例子中所有单词的意思加起来就可以用了。

这个算法有一个问题就是没考虑词序,尤其是这样一个负面的评价,“Completely lacking in good taste, good service, and good ambience.”,但是good这个词出现了很多次,有3个good,如果用的算法跟这个一样,忽略词序,仅仅把所有单词的词嵌入加起来或者平均下来,最后的特征向量会有很多good的表示,分类器很可能认为这是一个好的评论,尽管事实上这是一个差评,只有一星的评价。

我们有一个更加复杂的模型,不是简单地把所有的词嵌入都加起来,我们用一个RNN来做情感分类。首先取这条评论,“Completely lacking in good taste, good service, and good ambiance.”,找出每一个one-hot向量,用每一个one-hot向量乘以词嵌入矩阵 E E E,得到词嵌入表达 e e e,然后把它们送进RNN里。RNN的工作就是在最后一步计算一个特征表示,用来预测 y ^ \hat{y} y^,这是一个多对一的网络结构的例子。有了这样的算法,考虑词的顺序效果就更好了,它就能意识到"things are lacking in good taste",这是个负面的评价,“not good”也是一个负面的评价。而不像原来的算法一样,只是把所有的加在一起得到一个大的向量,根本意识不到“not good”和 “good”不是一个意思,"lacking in good taste"也是如此,等等。

如果训练一个这样的算法,最后会得到一个很合适的情感分类的算法。由于词嵌入(Word Embedding)是在一个更大的数据集里训练的,这样效果会更好,更好地泛化一些没有见过的新的单词。比如其他人可能会说,“Completely absent of good taste, good service, and good ambiance.”,即使absent这个词不在标记的训练集里,如果是在一亿或者一百亿单词集里训练词嵌入,它仍然可以正确判断,并且泛化的很好,甚至这些词是在训练集中用于训练词嵌入的,但是可以不在专门用来做情感分类问题的标记的训练集中。

一旦学习到或者从网上下载词嵌入,就可以很快构建一个很有效的NLP系统。

词嵌入除偏
仍然很难!
现在机器学习和人工智能算法正渐渐地被信任用以辅助或是制定极其重要的决策,因此我们想尽可能地确保它们不受非预期形式偏见影响,比如说性别歧视、种族歧视等等。

当使用术语bias时,是指性别、种族、性取向方面的偏见,那是不同的偏见,同时这也通常用于机器学习的学术讨论中。不过我们讨论的大部分内容是词嵌入是怎样学习类比像Man: Woman,就像King: Queen,这篇论文:Bolukbasi et. al.,2016. Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings. 发现了一个十分可怕的结果,一个已经完成学习的词嵌入可能会输出Man: Computer Programmer,同时输出Woman: Homemaker,结果看起来是错的,并且它执行了一个十分不良的性别歧视。如果算法输出的是Man: Computer Programmer,同时Woman: Computer Programmer这样子会更合理。同时他们也发现如果Father: Doctor,有些完成学习的词嵌入会输出Mother: Nurse(因为Mother也可以是Doctor)。

因此根据训练模型所使用的文本,词嵌入能够反映出性别、种族、年龄、性取向等其他方面的偏见,这些偏见都和社会经济状态相关,每个人不论出身富裕还是贫穷,亦或是二者之间,都应当拥有好的机会,同时因为机器学习算法正用来制定十分重要的决策,它也影响着世间万物,从大学录取到人们找工作的途径,到贷款申请,不论贷款申请是否会被批准,再到刑事司法系统,甚至是判决标准,学习算法都在作出非常重要的决策,所以我们尽量修改学习算法来尽可能减少或是理想化消除这些非预期类型的偏见是十分重要的。

至于词嵌入,它们能够轻易学会用来训练模型的文本中的偏见内容,所以算法获取到的偏见内容就可以反映出人们写作中的偏见。

假设我们已经完成一个词嵌入的学习,那么babysitter,doctor,grandmother,grandfather,girl,boy,she,he的位置如下图所示(图中grandmather应为grandmother),所以首先要辨别出想要减少或想要消除的特定偏见的趋势(direction)。为了便于说明,集中讨论性别歧视,不过这些想法对于其他类型的偏见都是通用的。辨别出与这个偏见相似的趋势,主要有以下三个步骤:

一、确定偏见bias的方向(趋势)。方法是对所有性别对立的单词求差值,再平均。对于性别歧视这种情况来说,我们能做的是 e h e − e s h e e_{he}-e_{she} eheeshe,因为它们的性别不同,然后将 e m a l e − e f e m a l e e_{male}-e_{female} emaleefemale,这些值取平均,将这些差简单地求平均。这个(横轴)趋势看起来就是性别趋势或说是偏见趋势,然后这个(竖轴)趋势与我们想要尝试处理的特定偏见并不相关,因此这就是个无偏见趋势。在这种情况下,偏见趋势可以将它看做1D子空间,所以这个无偏见趋势就会是299D的子空间。原文章中的描述这个偏见趋势可以比1维更高,同时相比于取平均值,实际上它会用一个更加复杂的算法叫做SVD,也就是奇异值分解,如果对主成分分析很熟悉的话,奇异值分解这个算法的一些方法和主成分分析(PCA)其实很类似。 b i a s   d i r e c t i o n = 1 N ( ( e h e − e s h e ) + ( e m a l e − e f e m a l e ) + ⋯   ) bias\,direction=\frac{1}{N}((e_{he}-e_{she})+(e_{male}-e_{female})+\cdots) biasdirection=N1((eheeshe)+(emaleefemale)+)

二、单词中立(中和)化,将需要消除性别偏见的单词投影到non-bias direction上去,消除bias维度,例如babysitter,doctor等。有些词本质上就和性别有关,像grandmother、grandfather、girl、boy、she、he,它们的定义中本就含有性别的内容,不过也有一些词像doctor和babysitter我们想使之在性别方面是中立的。同时在更通常的情况下,可能会希望像doctor或babysitter这些词成为种族中立的,或是性取向中立的等等,不过这里仍然只用性别来举例说明。所以对于像doctor和babysitter这种单词我们就可以将它们在这个轴(竖轴)上进行处理,来减少或是消除它们的性别歧视趋势的成分,也就是说减少它们在这个水平方向上的距离。

三、均衡对,让性别对立单词与上面的中立词距离相等,具有同样的相似度。例如让grandmother和grandfather与babysitter的距离同一化。意思是说可能会有这样的词对,grandmother和grandfather,或者是girl和boy,对于这些词嵌入,只希望性别是其区别。babysitter和grandmother之间的距离或者说是相似度实际上是小于babysitter和grandfather之间的,因此这可能会加重不良状态,或者可能是非预期的偏见,也就是说grandmother相比于grandfather最终更有可能输出babysitter. 所以在最后的均衡步中,我们想要确保的是像grandmother和grandfather这样的词都能够有一致的相似度,或者说是相等的距离,和babysitter或是doctor这样性别中立的词一样。它主要做的就是将grandmother和grandfather移至与中间轴线等距的一对点上,现在性别歧视的影响也就是这两个词与babysitter的距离就完全相同了。所以总体来说,会有许多对像grandmother-grandfather,boy-girl,sorority-fraternity,girlhood-boyhood,sister-brother,niece-nephew,daughter-son这样的词对,可能想要通过均衡步来解决它们。

最后一个细节是怎样才能够决定哪个词是中立的呢?对于这个例子来说doctor看起来像是一个应该对其中立的单词来使之性别不确定或是种族不确定。相反地,grandmother和grandfather就不应是性别不确定的词。也会有一些像是beard词,一个统计学上的事实是男性相比于比女性更有可能拥有胡子,因此也许beard应该比female更靠近male一些。一般来说,大部分英文单词,例如职业、身份等都需要中立化,消除Embedding Vector中性别这一维度的影响。

因此论文作者做的就是训练一个分类器来尝试解决哪些词是有明确定义的,哪些词是性别确定的,哪些词不是。结果表明英语里大部分词在性别方面上是没有明确定义的,只有一小部分词像是grandmother-grandfather,girl-boy,sorority-fraternity等等,不是性别中立的。因此一个线性分类器能够告诉你哪些词能够通过中和步来预测这个偏见趋势,或将其与这个本质是299D的子空间进行处理。

最后,需要平衡的词对的数实际上是很小的,至少对于性别歧视这个例子来说,用手都能够数出来需要平衡的大部分词对。完整的算法会比在这里展示的更复杂一些,可以去看一下原论文了解详细内容。总结一下,减少或者是消除学习算法中的偏见问题是个十分重要的问题,因为这些算法会用来辅助制定越来越多的社会中的重要决策。

参考资料:针对性别特定词汇的均衡算法
如何对两个单词除偏,比如:“actress"和"actor”. 均衡算法适用于您可能希望仅通过性别属性不同的单词对。 举一个具体的例子,假设"actress“比"actor"更接近"保姆"。通过将中和应用于"babysit",我们可以减少与保姆相关的性别刻板印象。但是这仍然不能保证"actress"和"actor"与"babysit"等距。均衡算法可以解决这个问题。
均衡背后的关键思想是确保一对特定的单词与49维 g ⊥ g_{\bot} g距离相等 。均衡步骤还可以确保两个均衡步骤现在与 e r e c e p t i o n i s t d e b i a s e d e^{debiased}_{receptionist} ereceptionistdebiased距离相同,或者用其他方法进行均衡。下图演示了均衡算法的工作原理:

主要步骤如下:(公式的推导有点复杂(参考原论文))

出处:http://www.ai-start.com/dl2017/html/lesson5-week2.html#header-n138

序列模型和注意力机制

基础模型
想通过输入一个法语句子,比如这句 “Jane visite I’Afrique en septembre.”,将它翻译成一个英语句子,“Jane is visiting Africa in September.”。我们用 x < 1 > x^{<1>} x<1>一直到 x < 5 > x^{<5>} x<5>来表示输入的句子的单词,然后用 y < 1 > y^{<1>} y<1> y < 6 > y^{<6>} y<6>来表示输出的句子的单词,那么,如何训练出一个新的网络来输入序列 x x x和输出序列 y y y呢?

论文:(1)Sutskever et al., 2014. Sequence to sequence learning with neural networks.
(2)Cho et al., 2014. Learning phrase representations using RNN encoder-decoder for statistical machine translation.

首先,我们先建立一个网络,这个网络叫做编码网络(encoder network),它是一个RNN的结构,RNN的单元可以是GRU或LSTM。每次只向该网络中输入一个法语单词,将输入序列接收完毕后,这个RNN网络会输出一个向量来代表这个输入序列。之后可以建立一个解码网络(decoder network),它以编码网络的输出作为输入,之后它可以被训练为每次输出一个翻译后的单词,一直到它输出序列的结尾或者句子结尾标记,这个解码网络的工作就结束了。和往常一样我们把每次生成的标记都传递到下一个单元中来进行预测,就像之前用语言模型合成文本时一样。

深度学习在近期最卓越的成果之一就是这个模型确实有效,在给出足够的法语和英语文本的情况下,如果训练这个模型,通过输入一个法语句子来输出对应的英语翻译,这个模型将会非常有效。这个模型简单地用一个编码网络来对输入的法语句子进行编码,然后用一个解码网络来生成对应的英语翻译。

图像描述(图像捕捉),给出一张图片,比如这张猫的图片,它能自动地输出该图片的描述,一只猫坐在椅子上。将图片输入到卷积神经网络中,比如一个预训练的AlexNet结构,然后让其学习图片的编码,或者学习图片的一系列特征。我们去掉最后的Softmax单元,这个预训练的AlexNet结构会得到一个4096维的特征向量(编码向量),向量表示的就是这只猫的图片,所以这个预训练网络可以是图像的编码网络。接着可以把这个4096维向量输入到RNN(decoder network)中,RNN要做的就是生成图像的描述,每次生成一个单词。现在输入一个描述猫的特征向量,然后让网络生成一个输出序列,或者说一个一个地输出单词序列。

事实证明在图像描述领域,这种方法相当有效,特别是当想生成的描述不是特别长时。

论文:(1)Mao et. al., 2014. Deep captioning with multimodal recurrent neural networks.
(2)Vinyals et. al., 2014. Show and tell: Neural image caption generator.
(3)Karpathy and Fei Fei, 2015. Deep visual-semantic alignments for generating image descriptions.

以上就是seq2seq模型以及image2seq模型或者说图像描述模型,这两个模型运作方式有一些不同,主要体现在如何用语言模型合成新的文本,并生成对应序列的方面。一个主要的区别就是不会想得到一个随机选取的翻译,想要的是最准确的翻译,或者说可能不想要一个随机选取的描述,想要的是最好的最贴切的描述。

选择最可能的句子
语言模型是自动生成一条完整语句,语句是随机的。机器翻译模型,用绿色表示encoder网络,用紫色表示decoder网络。会发现decoder网络看起来和之前讲的语言模型(1.6节)几乎一模一样,不同在于语言模型总是以零向量( a < 0 > a^{<0>} a<0>)开始,而encoder网络会计算出一系列向量来表示输入的句子。decoder网络就可以从这个句子开始,所以把它叫做条件语言模型(conditional language model)。相比语言模型,输出任意句子的概率,翻译模型会输出句子的英文翻译,将估计一个英文翻译的概率,“Jane is visiting Africa in September.”,这句翻译是取决于法语句子,“Jane visite I’Afrique en septembre.”,这就是英语句子相对于输入的法语句子的可能性,所以它是一个条件语言模型。 P ( y < 1 > , ⋯   , y < T y > ∣ x < 1 > , ⋯   , x < T x > ) P(y^{<1>},\cdots,y^{<T_y>}|x^{<1>},\cdots,x^{<T_x>}) P(y<1>,,y<Ty>x<1>,,x<Tx>)

现在,假如想真正地通过模型将法语翻译成英文,通过输入的法语句子模型将会告诉你各种英文翻译所对应的可能性。 x x x在这里是法语句子。显然不想让它随机地进行输出,如果从这个分布中进行取样得到 P ( y ∣ x ) P(y|x) P(yx),可能取样一次就能得到很好的翻译,“Jane is visiting Africa in September.”。但是可能也会得到一个截然不同的翻译,“Jane is going to be visiting Africa in September.”,这句话听起来有些笨拙,但它不是一个糟糕的翻译,只是不是最好的而已。有时也会得到这样的翻译,“In September, Jane will visit Africa.”,或者会得到一个很糟糕的翻译,“Her African friend welcomed Jane in September.”。所以当使用这个模型来进行机器翻译时,并不是从得到的分布中进行随机取样,而是要找到一个英语句子 y y y,使得条件概率最大化。所以在开发机器翻译系统时,需要做的一件事就是想出一个算法,用来找出合适的 y y y值,使得条件概率最大化,而解决这种问题最通用的算法就是束搜索(Beam Search)。 a r g m a x y P ( y ^ < 1 > , ⋯   , y ^ < T y > ∣ x ) arg\underset{y}{max}P(\hat{y}^{<1>},\cdots,\hat{y}^{<T_y>}|x) argymaxP(y^<1>,,y^<Ty>x)

为什么不用贪心搜索(Greedy Search)呢?贪心搜索是一种来自计算机科学的算法,生成第一个词的分布以后,它将会根据条件语言模型挑选出最有可能的第一个词进入机器翻译模型中,在挑选出第一个词之后它将会继续挑选出最有可能的第二个词,然后继续挑选第三个最有可能的词,这种算法就叫做贪心搜索,但是真正需要的是一次性挑选出整个单词序列,从 y < 1 > y^{<1>} y<1> y < 2 > y^{<2>} y<2> y < T y > y^{<T_y>} y<Ty>来使得整体的概率最大化。所以这种贪心算法先挑出最好的第一个词,在这之后再挑最好的第二词,然后再挑第三个,这种方法其实并不管用,为了证明这个观点,我们来考虑下面两种翻译。

  1. Jane is visiting Africa in September.
  2. Jane is going to be visiting Africa in September.

第一串翻译明显比第二个好,所以我们希望机器翻译模型会说第一个句子的 P ( y ∣ x ) P(y|x) P(yx)比第二个句子要高,第一个句子对于法语原文来说更好更简洁,虽然第二个也不错,但是有些啰嗦,里面有很多不重要的词。但如果贪心算法挑选出了"Jane is"作为前两个词,因为在英语中going更加常见,于是对于法语句子来说"Jane is going"相比"Jane is visiting"会有更高的概率作为法语的翻译(P(Jane is going|x)>P(Jane is visiting|x)),所以很有可能如果仅仅根据前两个词来估计第三个词的可能性,得到的就是going,最终会得到一个欠佳的句子,在 P ( y ∣ x ) P(y|x) P(yx)模型中这不是一个最好的选择。Greedy Search大大增加了运算成本,降低运算速度。

当想得到单词序列 y < 1 > y^{<1>} y<1> y < 2 > y^{<2>} y<2>一直到最后一个词总体的概率时,一次仅仅挑选一个词并不是最佳的选择。当然,在英语中各种词汇的组合数量还有很多很多,如果字典中有10000个单词,并且翻译可能有10个词那么长,那么可能的组合就有 1000 0 10 10000^{10} 1000010这么多,所以可能的句子数量非常巨大,不可能去计算每一种组合的可能性。所以这时最常用的办法就是用一个近似的搜索算法(近似最优),它会尽力地,尽管不一定总会成功,但它将挑选出句子 y y y使得条件概率最大化,尽管它不能保证找到的 y y y值一定可以使概率最大化,但这已经足够了。

条件语言模型和之前的语言模型一个主要的区别就是,相比之前的模型随机地生成句子,在该模型中要找到最有可能的英语句子,最可能的英语翻译,但是可能的句子组合数量过于巨大,无法一一列举,所以需要一种合适的搜索算法:集束搜索

集束搜索
对于机器翻译来说,给定输入,比如法语句子,不会想要输出一个随机的英语翻译结果,想要一个最好的,最可能的英语翻译结果。对于语音识别也一样,给定一个输入的语音片段,不会想要一个随机的文本翻译结果,想要最好的,最接近原意的翻译结果,集束搜索就是解决这个最常用的算法。

“Jane visite l’Afrique en Septembre.”,我们希望翻译成英语,“Jane is visiting Africa in September”.,集束搜索算法首先做的就是挑选要输出的英语翻译中的第一个单词。这里列出了10000个词的词汇表,所有的单词都以小写列出来。在集束搜索的第一步中用这个网络,绿色是编码部分,紫色是解码部分,来评估第一个单词的概率值,给定输入序列 x x x,即法语作为输入,第一个输出 y y y的概率值是 P ( y ^ < 1 > ∣ x ) P(\hat{y}^{<1>}|x) P(y^<1>x).

贪婪算法只会挑出最可能的那一个单词,然后继续。而集束搜索则会考虑多个选择,集束搜索算法会有一个参数B,叫做集束宽(beam width)。在这个例子中把这个集束宽设成3,这样集束搜索一次考虑3个可能结果,比如对第一个单词有不同选择的可能性,最后找到in、jane、september,是英语输出的第一个单词的最可能的三个选项,然后集束搜索算法会把结果存到计算机内存里以便后面尝试用这三个词。如果集束宽是10,那么跟踪的不仅仅3个,而是10个第一个单词的最可能的选择。所以为了执行集束搜索的第一步,需要输入法语句子到编码网络,然后会解码这个网络,这个Softmax层会输出10000个概率值,得到这10000个输出的概率值,取前三个存起来。

集束搜索算法的第二步,已经选出了in、jane、september作为第一个单词三个最可能的选择,集束算法接下来会针对每个第一个单词考虑第二个单词是什么,单词in后面的第二个单词可能是a或者是aaron,从词汇表里把这些词列了出来,或者是september,可能是列表里的visit,一直到最后一个单词zulu.

为了评估第二个词的概率值,用这个神经网络,对于解码部分,当决定单词in后面是什么,别忘了解码器的第一个输出 y ^ < 1 > \hat{y}^{<1>} y^<1>,把 y ^ < 1 > \hat{y}^{<1>} y^<1>设为单词in,然后把它喂回来,因为它的目的是努力找出第一个单词是in的情况下,第二个单词是什么。这个输出就是 y ^ < 2 > \hat{y}^{<2>} y^<2>,有了这个连接,就是这里的第一个单词in作为输入,这样这个网络就可以在给定法语句子和翻译结果的第一个单词in的情况下评估第二个单词的概率: P ( y ^ < 2 > ∣ x , “ i n ” ) P(\hat{y}^{<2>}|x,“in”) P(y^<2>x,in).

注意,在第二步里我们更关心的是要找到最可能的第一个和第二个单词对,所以不仅仅是第二个单词有最大的概率,而是第一个、第二个单词对有最大的概率。按照条件概率的准则,这个可以表示成第一个单词的概率乘以第二个单词的概率(利用条件概率、事件交换和乘法公式): P ( y ^ < 1 > , y ^ < 2 > ∣ x ) = P ( y ^ < 1 > ∣ x ) P ( y ^ < 2 > ∣ x , y ^ < 1 > ) P(\hat{y}^{<1>},\hat{y}^{<2>}|x)=P(\hat{y}^{<1>}|x)P(\hat{y}^{<2>}|x,\hat{y}^{<1>}) P(y^<1>,y^<2>x)=P(y^<1>x)P(y^<2>x,y^<1>)

这个 P ( y ^ < 2 > ∣ x , y ^ < 1 > ) P(\hat{y}^{<2>}|x,\hat{y}^{<1>}) P(y^<2>x,y^<1>)可以从网络部分里得到,对于已经选择的in、jane、september这三个单词,可以先保存这个 P ( y ^ < 1 > ∣ x ) P(\hat{y}^{<1>}|x) P(y^<1>x)概率值,然后再乘以第二个概率值就得到了第一个和第二个单词对的概率。

现在第一个单词是jane,句子可能是"jane a"、“jane aaron”,到"jane is"、"jane visits"等等。这个网络部分就可以告诉你给定输入 x x x和第一个词是jane的情况下,第二个单词的概率了,和上面一样,可以乘以 P ( y ^ < 1 > ∣ x ) P(\hat{y}^{<1>}|x) P(y^<1>x)得到 P ( y ^ < 1 > , y ^ < 2 > ∣ x ) P(\hat{y}^{<1>},\hat{y}^{<2>}|x) P(y^<1>,y^<2>x).

针对第二个单词所有10000个不同的选择,最后对于单词september也一样,从单词a到单词zulu,用这个网络部分,来看看如果第一个单词是september,第二个单词最可能是什么。所以对于集束搜索的第二步,由于我们一直用的集束宽为3,并且词汇表里有10000个单词,那么最终会有30000个可能的结果,就是集束宽乘以词汇表大小,要做的就是评估这30000个选择。按照第一个词和第二个词的概率,然后选出前三个,这样又减少了这30000个可能性,又变成了3个,减少到集束宽的大小。假如这30000个选择里最可能的是“in september”和“jane is”,以及“jane visits”,这就是这30000个选择里最可能的三个结果,集束搜索算法会保存这些结果,然后用于下一次集束搜索。

因为集束宽等于3,每一步我们都复制3个同样的这种网络来评估部分句子和最后的结果,我们有三个网络副本,每个网络的第一个单词不同,而这三个网络可以高效地评估第二个单词所有的30000个选择。所以不需要初始化30000个网络副本,只需要使用3个网络的副本就可以快速地评估Softmax的输出,即 y ^ < 2 > \hat{y}^{<2>} y^<2>的10000个结果。

集束搜索的第三步,前两个单词最可能的选择是“in September”和“jane is”以及“jane visits”,对于每一对单词我们应该保存起来,给定输入 x x x,即法语句子作为 x x x的情况下, y ^ < 1 > \hat{y}^{<1>} y^<1> y ^ < 2 > \hat{y}^{<2>} y^<2>的概率值和前面一样,现在考虑第三个单词是什么,可以是“in september a”,“in september aaron”,一直到“in september zulu”。为了评估第三个单词可能的选择,我们用这个网络部分,第一单词是in,第二个单词是september,所以这个网络部分可以用来评估在给定输入的法语句子 x x x和给定的英语输出的前两个单词“in September”情况下第三个单词的概率 P ( y ^ < 3 > ∣ x , y ^ < 1 > , y ^ < 2 > ) P(\hat{y}^{<3>}|x,\hat{y}^{<1>},\hat{y}^{<2>}) P(y^<3>x,y^<1>,y^<2>). 此时得到的前三个单词的3种情况的概率为: P ( y ^ < 1 > , y ^ < 2 > , y ^ < 3 > ∣ x ) = P ( y ^ < 1 > ∣ x ) P ( y ^ < 2 > ∣ x , y ^ < 1 > ) P ( y ^ < 3 > ∣ x , y ^ < 1 > , y ^ < 2 > ) P(\hat{y}^{<1>},\hat{y}^{<2>},\hat{y}^{<3>}|x)=P(\hat{y}^{<1>}|x)P(\hat{y}^{<2>}|x,\hat{y}^{<1>})P(\hat{y}^{<3>}|x,\hat{y}^{<1>},\hat{y}^{<2>}) P(y^<1>,y^<2>,y^<3>x)=P(y^<1>x)P(y^<2>x,y^<1>)P(y^<3>x,y^<1>,y^<2>)

对于“jane is”和“jane visits”也一样,然后集束搜索还是会挑选出针对前三个词的三个最可能的选择,可能是“in september jane”,“jane is visiting”,“jane visits Africa”.

然后继续,接着进行集束搜索的第四步,再加一个单词继续,最终这个过程的输出一次增加一个单词,集束搜索最终会找到“jane visits africa in september”这个句子,终止在句尾符号< EOS >,用这种符号的系统非常常见,它们会发现这是最有可能输出的一个英语句子。在集束宽为3时,集束搜索一次只考虑3个可能结果。注意如果集束宽等于1,只考虑1种可能结果,这实际上就变成了贪婪搜索算法。但是如果同时考虑多个,可能的结果比如3个,10个或者其他的个数,集束搜索通常会找到比贪婪搜索更好的输出结果。

改进集束搜索
长度归一化(Length normalization)就是对束搜索算法稍作调整的一种方式,帮助得到更好的结果。束搜索就是最大化这个概率: a r g m a x y ∏ t = 1 T y P ( y < t > ∣ x , y < 1 > , ⋯   , y < t − 1 > ) = a r g m a x y P ( y < 1 > , ⋯   , y < T y > ∣ x ) arg\underset{y}{max}\prod_{t=1}^{T_y}{P(y^{<t>}|x,y^{<1>},\cdots,y^{<t-1>})}\\=arg\underset{y}{max}P(y^{<1>},\cdots,y^{<T_y>}|x) argymaxt=1TyP(y<t>x,y<1>,,y<t1>)=argymaxP(y<1>,,y<Ty>x)

这些概率值都是小于1的,通常远小于1。很多小于1的数乘起来,会得到很小很小的数字,会造成数值下溢。数值太小了,导致电脑的浮点表示不能精确地储存。因此在实践中,我们不会最大化这个乘积,而是取log值: a r g m a x y ∑ t = 1 T y l o g P ( y < t > ∣ x , y < 1 > , ⋯   , y < t − 1 > ) arg\underset{y}{max}\sum_{t=1}^{T_y}{logP(y^{<t>}|x,y^{<1>},\cdots,y^{<t-1>})} argymaxt=1TylogP(y<t>x,y<1>,,y<t1>)

在这加上一个log,最大化这个log求和的概率值,在选择最可能的句子 y y y时,会得到同样的结果。所以通过取log,会得到一个数值上更稳定的算法,不容易出现四舍五入的误差,数值的舍入误差或者说数值下溢。因为log函数是严格单调递增的函数,所以最大化 l o g P ( y ∣ x ) logP(y|x) logP(yx)和最大化 P ( y ∣ x ) P(y|x) P(yx)结果一样。

如果参照原来的目标函数,如果有一个很长的句子,那么这个句子的概率会很低,因为乘了很多项小于1的数字来估计句子的概率。所以这个目标函数有一个缺点,它可能不自然地倾向于简短的翻译结果,它更偏向短的输出,因为短句子的概率是由更少数量的小于1的数字乘积得到的,所以这个乘积不会那么小。这里也有同样的问题,概率值通常小于等于1,实际上在log的这个范围内,加起来的项越多,得到的结果越负。所以对这个算法另一个改变就是我们不再最大化这个目标函数了,可以把它归一化,通过除以翻译结果的单词数量。这样就是取每个单词的概率对数值的平均了,这样很明显地减少了对输出长的结果的惩罚。 1 T y α ∑ t = 1 T y l o g P ( y < t > ∣ x , y < 1 > , ⋯   , y < t − 1 > ) \frac{1}{T_y^{\alpha}}\sum_{t=1}^{T_y}{logP(y^{<t>}|x,y^{<1>},\cdots,y^{<t-1>})} Tyα1t=1TylogP(y<t>x,y<1>,,y<t1>)

在实践中,有个探索性的方法,相比于直接除以 T y T_y Ty,有时会用一个更柔和的方法,在 T y T_y Ty上加上指数 α \alpha α α \alpha α可以等于0.7。如果 α = 1 \alpha=1 α=1,就相当于完全用长度来归一化,如果 α \alpha α等于0, T y T_y Ty的0次幂就是1,就相当于完全没有归一化。 α \alpha α就是算法另一个超参数,需要调整大小来得到最好的结果。不得不承认,这样用 α \alpha α实际上是试探性的,它并没有理论验证。但是大家都发现实践中效果不错,所以很多人都会这么做。

当运行束搜索时,会看到很多长度等于1、2、3的句子。可能运行束搜索30步,考虑输出的句子可能达到长度30。因为束宽为3,会记录所有这些可能的句子长度,长度为1、2、 3、 4 ……30的三个最可能的选择。然后针对这些所有的可能的输出句子,用 1 T y α ∑ t = 1 T y l o g P ( y < t > ∣ x , y < 1 > , ⋯   , y < t − 1 > ) \frac{1}{T_y^{\alpha}}\sum_{t=1}^{T_y}{logP(y^{<t>}|x,y^{<1>},\cdots,y^{<t-1>})} Tyα1t=1TylogP(y<t>x,y<1>,,y<t1>)给它们打分,取概率最大的几个句子,然后对这些束搜索得到的句子,计算这个目标函数(上式)。最后从经过评估的这些句子中,挑选出在归一化的 l o g log log概率目标函数上得分最高的一个,有时这个也叫作归一化的对数似然目标函数

如果束宽很大,会考虑很多的可能,会得到一个更好的结果,但是算法会运行的慢一些,内存占用也会增大,计算起来会慢一点。而如果用小的束宽,结果会没那么好,因为在算法运行中,保存的选择更少,但是算法运行的更快,内存占用也小。

我们例子中用了束宽(B)为3,所以会保存3个可能选择,在实践中这个值有点偏小。在产品中,经常可以看到把束宽设到10,束宽为100对于产品系统来说有点大了,这也取决于不同应用。但是对科研而言,人们想压榨出全部性能,这样有个最好的结果用来发论文,也经常看到大家用束宽为1000或者3000,这也是取决于特定的应用和特定的领域。当B很大的时候,性能提高会越来越少。对于很多应用来说,从束宽1,也就是贪心算法,到束宽为3、10,会看到一个很大的改善,但是当束宽从1000增加到3000时,效果就没那么明显了。

计算机科学里的搜索算法,比如广度优先搜索(BFS),或者深度优先搜索(DFS),束搜索不像其他在计算机科学算法课程中学到的算法一样。广度优先搜索和深度优先搜索,这些都是精确的搜索算法,不同于这些算法,束搜索运行的更快,但是不能保证一定能找到argmax的准确的最大值,并不保证一定能找到正确的翻译语句。

集束搜索的误差分析
误差分析能够帮助你集中时间做项目中最有用的工作,束搜索算法是一种近似搜索算法,也被称作启发式搜索算法(a heuristic search algorithm),它不总是输出可能性最大的句子,它仅记录着B为3或10或100种可能。

用这个例子说明: “Jane visite l’Afrique en septembre”。假如说,在机器翻译的dev集中,人工是这样翻译的:Jane visits Africa in September,将这个标记为 y ∗ y^{*} y. 这是一个十分不错的人工翻译结果。当你在已经完成学习的RNN模型,也就是已完成学习的翻译模型中运行束搜索算法时,它输出了这个翻译结果:Jane visited Africa last September,将它标记为 y ^ \hat{y} y^. 这是一个十分糟糕的翻译,它实际上改变了句子的原意。

模型有两个主要部分,一个是神经网络模型,或者说是seq2seq模型,将这个称作是RNN模型,它实际上是个编码器和解码器。另一部分是束搜索算法,以某个集束宽度B运行。我们希望找出造成这个错误,这个不太好的翻译的原因,是两个部分中的哪一个。大家很容易想到去收集更多的训练数据,这总归没什么坏处。所以同样的,大家也会觉得不行就增大束宽,也是不会错的,或者说是很大可能是没有危害的。但是单纯增大束宽也可能得不到想要的结果。

RNN实际上是个编码器和解码器,它会计算 P ( y ∣ x ) P(y|x) P(yx). 举个例子,对于这个句子:Jane visits Africa in September,将Jane visits Africa填入这里,然后这个就会计算 P ( y ∣ x ) P(y|x) P(yx). 结果表明,此时能做的最有效的事就是用这个模型来计算 P ( y ∗ ∣ x ) P(y^{*}|x) P(yx),同时也用RNN模型来计算 P ( y ^ ∣ x ) P(\hat{y}|x) P(y^x),然后比较一下这两个值哪个更大。取决于实际是哪种情况,就能够更清楚地将这个特定的错误归咎于RNN或是束搜索算法,或说是哪个负有更大的责任。

第一种情况,RNN模型的输出结果 P ( y ∗ ∣ x ) > P ( y ^ ∣ x ) P(y^{*}|x)>P(\hat{y}|x) P(yx)>P(y^x),束搜索算法选择了 y ^ \hat{y} y^,得到 y ^ \hat{y} y^的方式是,用一个RNN模型来计算 P ( y ∣ x ) P(y|x) P(yx),然后束搜索算法做的就是尝试寻找使 P ( y ∣ x ) P(y|x) P(yx)最大的 y y y,不过在这种情况下, y ∗ y^{*} y P ( y ∣ x ) P(y|x) P(yx)值更大,因此束搜索算法实际上不能够给出一个能使 P ( y ∣ x ) P(y|x) P(yx)最大化的 y y y值。因此这种情况下束搜索算法出错了。

第二种情况是 P ( y ∗ ∣ x ) ⩽ P ( y ^ ∣ x ) P(y^{*}|x)\leqslant P(\hat{y}|x) P(yx)P(y^x) y ∗ y^{*} y是比 y ^ \hat{y} y^更好的翻译结果,不过根据RNN模型的结果, P ( y ∗ ) P(y^{*}) P(y)是小于 P ( y ^ ) P(\hat{y}) P(y^)的,也就是说,相比于 y ^ \hat{y} y^ y ∗ y^{*} y成为输出的可能更小。因此在这种情况下,看来是RNN模型出了问题,可能值得在RNN模型上花更多时间。这里略过了有关长度归一化的细节,如果用了某种长度归一化,那么要比较长度归一化后的最优化目标函数值。

先遍历开发集,然后在其中找出算法产生的错误(即算法输出了比人工翻译更差的结果的情况),第一个例子中,假如说 P ( y ∗ ∣ x ) P(y^{*}|x) P(yx)的值为 2 × 1 0 − 10 2\times10^{-10} 2×1010,而 P ( y ^ ∣ x ) P(\hat{y}|x) P(y^x)的值为 1 × 1 0 − 10 1\times10^{-10} 1×1010,这种情况下束搜索算法出错了。接着继续遍历第二个错误,也许是生成目标函数(束搜索算法使之最大化)的RNN模型出现了问题。再接着遍历了更多的例子。通过这个过程,就能够执行误差分析,得出束搜索算法和RNN模型出错的比例是多少。

当发现是束搜索算法造成了大部分错误时,才值得花费努力增大集束宽度。相反地,如果发现是RNN模型出了更多错,那么可以进行更深层次的分析,来决定是需要增加正则化还是获取更多的训练数据,抑或是尝试一个不同的网络结构,或是其他方案。在第三门课(结构化机器学习项目)中,了解到各种技巧都能够应用在这里。

这就是束搜索算法中的误差分析,这个特定的误差分析过程是十分有用的,它可以用于分析近似最佳算法(如束搜索算法),这些算法被用来优化学习算法(例如seq2seq模型/RNN)输出的目标函数。

Bleu得分
机器翻译的一大难题是一个法语句子可以有多种英文翻译而且都同样好,所以当有多个同样好的答案时,怎样评估一个机器翻译系统呢?常见的解决办法是,通过一个叫做BLEU得分的东西来解决。

一个法语句子:Le chat est sur le tapis,然后给你一个这个句子的人工翻译作参考:The cat is on the mat。不过有多种相当不错的翻译,也许会将其翻译为:There is a cat on the mat,实际上这两个都是很好的,都准确地翻译了这个法语句子。BLEU得分做的就是,给定一个机器生成的翻译,它能够自动地计算一个分数来衡量机器翻译的好坏。直觉告诉我们,只要这个机器生成的翻译与任何一个人工翻译的结果足够接近,那么它就会得到一个高的BLEU分数。

BLEU代表bilingual evaluation understudy(双语评估替补)。在戏剧界,侯补演员(understudy)学习资深的演员的角色,这样在必要的时候,他们就能够接替这些资深演员。而BLEU的初衷是相对于请评估员,人工评估机器翻译系统,BLEU得分就相当于一个侯补者,它可以代替人类来评估机器翻译的每一个输出结果。

参考论文Papineni et. al., 2002. BLEU: A Method for Automatic Evaluation of Machine Translation. 这篇论文十分有影响力并且实际上也是一篇很好读的文章。BLEU得分背后的理念是观察机器生成的翻译,然后看生成的词是否出现在至少一个人工翻译参考之中。因此这些人工翻译的参考会包含在开发集或是测试集中。

  • 参考1:The cat is on the mat.
  • 参考2:There is a cat on the mat.
  • MT输出:the the the the the the the.

假设机器翻译系统缩写为MT。机器翻译(MT)的输出是:the the the the the the the. 这显然是一个十分糟糕的翻译。衡量机器翻译输出质量的方法之一是观察输出结果的每一个词看其是否出现在参考中,这叫做机器翻译的精确度(precision)。这个情况下,机器翻译输出了七个单词并且这七个词中的每一个都出现在了参考1或是参考2。单词the在两个参考中都出现了,所以看上去每个词都是很合理的。因此这个输出的精确度就是 7 7 \frac{7}{7} 77,看起来是一个极好的精确度。把出现在参考中的词在MT输出的所有词中所占的比例作为精确度评估标准并不是很有用,因为它似乎意味着,例子中MT输出的翻译有很高的精确度。

我们要用的这个改良后的精确度评估方法,把每一个单词的记分上限定为它在参考句子中出现的最多次数。在参考1中,单词the出现了2次,在参考2中,单词the只出现了1次。而2比1大,所以单词the的得分上限为2。有了这个改良后的精确度,这个输出句子的得分为 2 7 \frac{2}{7} 72,因为在7个词中,我们最多只能给它2分。所以这里分母就是7个词中单词the总共出现的次数,而分子就是单词the出现的计数。我们在达到上限时截断计数,这就是改良(modified)后的精确度评估。

  • 参考1:The cat is on the mat.
  • 参考2:There is a cat on the mat.
  • MT输出:The cat the cat on the mat.

在BLEU得分中,也许想考虑成对的单词,我们定义一下二元词组(bigrams)的BLEU得分。bigram的意思就是相邻的两个单词。这仅仅只是最终的BLEU得分的一部分,我们会考虑一元词组(unigrams)以及二元词组,同时也许会有更长的单词序列,比如说三元词组(trigrams)。现在我们假定机器翻译输出了稍微好一点的翻译:The cat the cat on the mat。这里可能的二元词组有the cat ,cat the,cat on,on the,the mat,这些就是机器翻译中的二元词组。the cat出现了两次 ,cat the出现了一次,剩下的都只出现了一次。最后定义一下截取计数(the clipped count),也就是Countclip. 给算法设置得分上限,上限值为二元词组出现在参考1或2中的最大次数。the cat在两个参考中最多出现一次,将截取它的计数为1。cat the它并没有出现在参考1和参考2中,将它截取为0。cat on出现了一次,我们就记1分。on the出现一次就记1分,the mat出现了一次,所以这些就是截取完的计数。最后,修改后的二元词组的精确度就是Count_clip之和,就是4除以二元词组的总个数6,因此 4 6 = 2 3 \frac{4}{6}=\frac{2}{3} 64=32为二元词组改良后的精确度。

CountCountclip
the cat21
cat the10
cat on11
on the11
the mat11

现在将它公式化。将改良后的一元词组精确度定义为 p 1 p_1 p1 p p p代表的是精确度。不过它定义为一元词组之和,也就是对机器翻译结果中所有单词求和,MT输出就是 y ^ \hat{y} y^ p 1 = ∑ u n i g r a m ∈ y ^ C o u n t c l i p ( u n i g r a m ) ∑ u n i g r a m ∈ y ^ C o u n t ( u n i g r a m ) p_1=\frac{\sum_{unigram\in\hat{y}}Count_{clip}(unigram)}{\sum_{unigram\in\hat{y}}Count(unigram)} p1=unigramy^Count(unigram)unigramy^Countclip(unigram)

也可以定义 p n p_n pn n n n元词组精确度,用n-gram表示 n n n元词组。这个方法都能够衡量机器翻译输出中与参考相似重复的程度。另外,如果机器翻译输出与参考1或是参考2完全一致的话,那么所有的这些 p 1 p_1 p1 p 2 p_2 p2等等的值,都会等于1.0。为了得到改良后的1.0的精确度,只要输出与参考之一完全相同就能满足,不过有时即使输出结果并不完全与参考相同,这也是有可能实现的。 p n = ∑ n − g r a m ∈ y ^ C o u n t c l i p ( n − g r a m ) ∑ n − g r a m ∈ y ^ C o u n t ( n − g r a m ) p_n=\frac{\sum_{n-gram\in\hat{y}}Count_{clip}(n-gram)}{\sum_{n-gram\in\hat{y}}Count(n-gram)} pn=ngramy^Count(ngram)ngramy^Countclip(ngram)

最后,我们将这些组合一下来构成最终的BLEU得分。按照惯例BLEU得分被定义为 p = e x p ( 1 n ∑ i = 1 n p i ) p=exp(\frac{1}{n}\sum_{i=1}^n{p_i}) p=exp(n1i=1npi),我们实际上会用额外的一个叫做BP的惩罚因子来调整这项,BP的意思是“简短惩罚”(brevity penalty) p = B P ⋅ e x p ( 1 n ∑ i = 1 n p i ) p=BP\cdot exp(\frac{1}{n}\sum_{i=1}^n{p_i}) p=BPexp(n1i=1npi)

事实表明,如果输出了一个非常短的翻译,那么它会更容易得到一个高精确度。因为输出的大部分词可能都出现在参考之中,不过我们并不想要特别短的翻译结果。因此简短惩罚(BP)就是一个调整因子,它能够惩罚输出了太短翻译结果的翻译系统。BP的公式如下:

在之前了解了拥有单一实数评估指标的重要性,因为它能够让你尝试两种想法,然后看一下哪个得分更高,尽量选择得分更高的那个。BLEU得分对于机器翻译来说,具有革命性的原因是因为它有一个相当不错的虽然不是完美的单一实数评估指标,因此它加快了整个机器翻译领域的进程。

实践中,很少人会从零实现一个BLEU得分,有很多开源的实现结果,可以下载下来然后直接用来评估系统。BLEU得分被用来评估许多生成文本的系统,比如说机器翻译系统,也有图像描述系统,也就是说会用神经网络来生成图像描述,然后使用BLEU得分来看一下,结果在多大程度上与多个人工完成的参考描述内容相符。

BLEU得分是一个有用的单一实数评估指标,用于评估生成文本的算法,判断输出的结果是否与人工写出的参考文本的含义相似。不过它并没有用于语音识别。因为在语音识别当中,通常只有一个答案,可以用其他的评估方法,来看一下语音识别结果,是否十分相近或是字字正确。

注意力模型直观理解
这节不容易!
当使用RNN读一个句子,于是另一个会输出一个句子。我们要对其做一些改变,称为注意力模型,并且这会使它工作得更好。注意力模型或者说注意力这种思想已经是深度学习中最重要的思想之一。

像这样给定一个很长的法语句子,在神经网络中,这个绿色的编码器要做的就是读整个句子,然后记忆整个句子,再在感知机中传递。而对于这个紫色的神经网络,即解码网络将生成英文翻译。而人工翻译,首先会做的可能是先翻译出句子的部分,再看下一部分,并翻译这一部分。看一部分,翻译一部分,一直这样下去。因为记忆整个长句子是非常困难的。在这个编码解码结构中,会看到它对于短句子效果非常好,于是它会有一个相对高的Bleu分,但是对于长句子而言,它的表现就会变差。有了注意力模型,机器翻译系统的表现会像这个一样(平滑直线),因为翻译只会翻译句子的一部分,不会看到这个有一个巨大的下倾(huge dip),这个下倾实际上衡量了神经网络记忆一个长句子的能力。对待长语句,正确的翻译方法是将长语句分段,每次只对长语句的一部分进行翻译。

论文Bahdanau et. al., 2014. Neural Machine Translation by Jointly Learning to Align and Translate. 虽然这个模型源于机器翻译,但它也推广到了其他应用领域。在深度学习领域,这个是非常有影响力的,非常具有开创性的论文。

我们有一个法语句子:Jane visite l’Afrique en Septembre. 假定我们将使用一个双向的RNN,为了计算每个输入单词的的特征集,必须要理解输出 y ^ < 1 > \hat{y}^{<1>} y^<1> y ^ < 2 > \hat{y}^{<2>} y^<2>一直到 y ^ < 5 > \hat{y}^{<5>} y^<5>的双向RNN。但是我们并不是只翻译一个单词,先去掉上面的 y ^ \hat{y} y^. 使用双向RNN,对于句子里的五个单词,计算一个句子中单词的特征集,也有可能是周围的词。我们将使用另一个RNN生成英文翻译,用 S S S来表示RNN的隐藏状态。我们希望在这个模型里第一个生成的单词将会是Jane,最后生成Jane visits Africa in September. 于是问题就是,当尝试生成第一个词,即输出Jane,那么应该看输入的法语句子的哪个部分?似乎应该先看第一个单词,或者它附近的词。但是别看太远了,比如说看到句尾去了。

所以注意力模型就会计算注意力权重,用 α < 1 , 1 > \alpha^{<1,1>} α<1,1>来表示当生成第一个词时应该放多少注意力在这个第一块信息处。然后我们算第二个, α < 1 , 2 > \alpha^{<1,2>} α<1,2>告诉我们当尝试去计算第一个词Jane时,我们应该花多少注意力在输入的第二个词上面。同理这里是 α < 1 , 3 > \alpha^{<1,3>} α<1,3>,等等。这些将会告诉我们,我们应该花多少注意力在记号为 c c c的内容上。这就是RNN的一个单元(尝试生成第一个词)。

对于RNN的第二步,我们将有一个新的隐藏状态 S < 2 > S^{<2>} S<2>,用一个新的注意力权值集,用 α < 2 , 1 > \alpha^{<2,1>} α<2,1>来告诉我们当生成第二个词visits,我们应该花多少注意力在输入的第一个法语词上。然后同理 α < 2 , 2 > \alpha^{<2,2>} α<2,2>,我们应该花多少注意力在visite词上, α < 2 , 3 > \alpha^{<2,3>} α<2,3>我们应该花多少注意在词l’Afique上面。当然我们第一个生成的词Jane也会输入到这里(见上图),于是我们就有了需要花注意力的上下文 c c c,这也是个输入,然后会一起生成第二个词。

接着来到第三步 S < 3 > S^{<3>} S<3>,这(visits)是输入,我们再有上下文 c c c,它取决于在不同的时间集上面的 α < 3 , t ′ > \alpha^{<3,t'>} α<3,t>. 当尝试去生成第三个词,应该是Africa,这个RNN步骤应该要花注意力 α < 3 , t ′ > \alpha^{<3,t'>} α<3,t> t ′ t' t时的法语词上,而 α < 3 , t ′ > \alpha^{<3,t'>} α<3,t>取决于在 t ′ t' t时的双向RNN的激活值 a < t ′ > = ( a → < t ′ > , a ← < t ′ > ) a^{<t'>}=(\overrightarrow{a}^{<t'>},\overleftarrow{a}^{<t'>}) a<t>=(a <t>,a <t>)和上一步的状态 S < 2 > S^{<2>} S<2>,然后这些一起影响应该花多少注意在输入的法语句子的某个词上面。注意力权重,即 α < t , t ′ > \alpha^{<t,t'>} α<t,t>告诉你,当尝试生成第 t t t个英文词,它应该花多少注意力在第 t ′ t' t个法语词上面。当生成一个特定的英文词时,这允许它在每个时间步去看周围词距内的法语词要花多少注意力。更多细节见下一节。

注意力模型
这节有点难!
注意力模型让一个神经网络只注意到一部分的输入句子。当它在生成句子的时候,更像人类翻译。

我们先假定有一个输入句子,并使用双向的RNN,或双向的GRU或LSTM,去计算每个词的特征。对于前向传播,有第一个时间步的前向传播的激活值,第一个时间步后向传播的激活值,以此类推。它们一共向前了五个时间步,也向后了五个时间步,技术上我们把 a → < 0 > \overrightarrow{a}^{<0>} a <0>设置为0。也可以后向传播6次,设一个( a ← < 6 > \overleftarrow{a}^{<6>} a <6>)都是0的因子。

a < t ′ > = ( a → < t ′ > , a ← < t ′ > ) a^{<t'>}=(\overrightarrow{a}^{<t'>},\overleftarrow{a}^{<t'>}) a<t>=(a <t>,a <t>)来表示时间步 t ′ t' t上的特征向量,这里用 t ′ t' t来索引法语句子里面的词。接下来只进行前向计算,这是个单向的RNN,用状态 S S S表示生成翻译。所以第一个时间步应该生成 y ^ < 1 > \hat{y}^{<1>} y^<1>,当输入上下文 c c c的时候就会这样,如果想用时间来索引它,可以写 c < 1 > c^{<1>} c<1>,这个取决于注意力参数,即 α < 1 , 1 > \alpha^{<1,1>} α<1,1> α < 1 , 2 > ⋯ \alpha^{<1,2>}\cdots α<1,2>,告诉我们应该花多少注意力在从不同时间步中得到的特征或者激活值上。注意力权重将会满足非负的条件,它们加起来等于1: ∑ t ′ α < 1 , t ′ > = 1 c < 1 > = ∑ t ′ α < 1 , t ′ > a < t ′ > \sum_{t'}\alpha^{<1,t'>}=1\\c^{<1>}=\sum_{t'}\alpha^{<1,t'>}a^{<t'>} tα<1,t>=1c<1>=tα<1,t>a<t>

t = 1 t=1 t=1时的上下文 c < 1 > c^{<1>} c<1>,这就变成对 t ′ t' t的求和。于是 α < t , t ′ > \alpha^{<t,t'>} α<t,t>就是 y ^ < t > \hat{y}^{<t>} y^<t>应该在 t ′ t' t时花在 a < t ′ > a^{<t'>} a<t>上注意力的数量。换句话来说,当在 t t t处生成输出词,应该花多少注意力在第 t ′ t' t个输入词上面,这是生成输出的其中一步。

然后下一个时间步,会生成第二个输出。类似的,现在有了一个新的注意力权重集,再用上面的方式将它们相加,这就产生了一个新的上下文 c < 2 > c^{<2>} c<2>,这个也是输入,且允许生成第二个词。这里的神经网络看起来很像相当标准的RNN序列,这里有着上下文向量作为输入,我们可以一次一个词地生成翻译。

α < t , t ′ > \alpha^{<t,t'>} α<t,t>是应该花费在 a < t ′ > a^{<t'>} a<t>上的注意力的数量,当尝试去生成第 t t t个输出的翻译词,下面这个式子可以用来计算 α < t , t ′ > \alpha^{<t,t'>} α<t,t>,在此之前我们要先计算 e < t , t ′ > e^{<t,t'>} e<t,t>,关键要用Softmax,来确保这些权重加起来等于1。如果对 t ′ t' t求和,固定 t t t,这些加起来等于1. α < t , t ′ > = e x p ( e < t , t ′ > ) ∑ t ′ = 1 T x e x p ( e < t , t ′ > ) \alpha^{<t,t'>}=\frac{exp(e^{<t,t'>})}{\sum_{t'=1}^{T_x}exp(e^{<t,t'>})} α<t,t>=t=1Txexp(e<t,t>)exp(e<t,t>)

一种可以用来计算 e < t , t ′ > e^{<t,t'>} e<t,t>的方式是用下面这样的小的神经网络,于是 S < t − 1 > S^{<t-1>} S<t1>就是神经网络在上一个时间步的隐藏状态。 S < t − 1 > S^{<t-1>} S<t1>是给小神经网络的其中一个输入,这是神经网络中的一个隐藏层,因为需要经常计算它们,然后 a < t ′ > a^{<t'>} a<t>,即 t ′ t' t时间步的的特征是另一个输入。直观来想就是,如果想要决定要花多少注意力在 t ′ t' t的激活值上,似乎它会很大程度上取决于上一个时间步的的隐藏状态的激活值。

还没有当前状态的激活值 S < t > S^{<t>} S<t>,因为上下文 c c c会输入到这里,所以还没计算出来,但是看看生成上一个翻译的RNN的隐藏状态,然后对于每一个位置,每一个词都看向它们的特征值 a a a. 但是不知道具体函数是什么,所以可以训练一个很小的神经网络,利用反向传播算法和梯度下降算法学到一个正确的函数。这个小型的神经网络告诉你 y ^ < t > \hat{y}^{<t>} y^<t>应该花多少注意力在 a < t ′ > a^{<t'>} a<t>上面,然后 α < t , t ′ > \alpha^{<t,t'>} α<t,t>这个式子确保注意力权重加起来等于1,于是当持续地一次生成一个词,下面这个神经网络实际上会花注意力在输入句子的右边(right parts)上,它会完全自动地通过梯度下降来学习。

这个算法的一个缺点就是它要花费三次方的时间,就是说这个算法的复杂度是 O ( n 3 ) O(n^3) O(n3)的,如果有 T x T_x Tx个输入单词和 T y T_y Ty个输出单词,注意力参数的总数就会是 T x × T y T_x\times T_y Tx×Ty,所以这个算法有着三次方的消耗。但是在机器翻译的应用上,输入和输出的句子一般不会太长,三次方的消耗是可以接受,但也有很多研究工作,尝试去减少这样的消耗。这个想法也被应用到了其他的很多问题中去,比如图片加标题,就是看一张图,写下这张图的标题。论文Xu et. al., 2015. Show, attend and tell: Neural image caption generation with visual attention.,他们也显示了可以有一个很相似的结构看图片,然后当在写图片标题的时候,一次只花注意力在一部分的图片上面。

因为机器翻译是一个非常复杂的问题。在编程练习中应用了注意力,在日期标准化的问题上面。问题输入了一个日期July 20th 1969,把它转化为标准的形式:1969-07-20,或者这样的日期:23 April, 1564,用一个序列的神经网络,即序列模型去标准化到这样的形式:1564-04-23. 可以训练一个神经网络,输入任何形式的日期,生成标准化的日期形式。

下图将注意力权重 α < t , t ′ > \alpha^{<t,t'>} α<t,t>可视化:这是一个机器翻译的例子,这里被画上了不同的颜色,颜色越白表示注意力权重越大,颜色越深表示权重越小。可以发现对应的输入输出词,注意力权重会变高,因此这显示了当它生成特定的输出词时通常会花注意力在输入的正确的词上面。

语音识别
seq2seq模型在语音识别方面准确性有了很大的提升。现在有一个音频片段 x x x,任务是自动地生成文本 y y y。音频片段图的横轴是时间,一个麦克风的作用是测量出微小的气压变化,像图上这样的音频片段,气压随着时间而变化。假如这个的音频片段的内容是:“the quick brown fox”,这时我们希望一个语音识别算法,通过输入这段音频,然后输出音频的文本内容。考虑到人的耳朵并不会处理声音的原始波形,而是通过一种特殊的物理结构来测量这些,不同频率和强度的声波。音频数据的常见预处理步骤,就是运行这个原始的音频片段,可以把信号转化为频域信号,然后生成一个声谱图(a spectrogram)。横轴是时间,纵轴是声音的频率,而图中不同的颜色,显示了声波能量的大小(the amount of energy),也就是在不同的时间和频率上这些声音有多大。通过这样的声谱图,伪空白输出(the false blank outputs)也经常应用于预处理步骤,也就是在音频被输入到学习算法之前,而人耳所做的计算和这个预处理过程非常相似。

曾经有一段时间,语音识别系统是用音位(phonemes)来构建的,也就是人工设计的基本单元。如果用音位来表示"the quick brown fox",“the"含有"th"和"e"的音,而"quick"有"k” “w” “i” "k"的音,语音学家过去把这些音作为声音的基本单元写下来,把这些语音分解成这些基本的声音单元,而"brown"不是一个很正式的音位,因为它的音写起来比较复杂,不过语音学家们认为用这些基本的音位单元来表示音频,是做语音识别最好的办法。不过在end-to-end模型中,我们发现这种音位表示法已经不再必要了,而是可以构建一个系统,通过向系统中输入音频片段,然后直接输出音频的文本,而不需要使用这种人工设计的表示方法。

使这种方法成为可能的一件事就是用一个很大的数据集,所以语音识别的研究数据集可能长达300个小时,在学术界,甚至3000小时的文本音频数据集,都被认为是合理的大小。大量的研究,大量的论文所使用的数据集中,有几千种不同的声音,而且,最好的商业系统现在已经训练了超过1万个小时的数据,甚至10万个小时,并且它还会继续变得更大。在文本音频数据集中同时包含 x x x y y y,通过深度学习算法大大推进了语音识别的进程。

在横轴上,也就是在输入音频的不同时间帧上,可以用一个注意力模型,来输出文本描述,如"the quick brown fox",或者其他语音内容。

还有一种效果也不错的方法,就是用CTC损失函数来做语音识别。CTC就是Connectionist Temporal Classification,论文:Graves et al., 2006. Connectionist Temporal Classification: Labelling unsegmented sequence data with recurrent neural networks.

算法思想如下:假设语音片段内容是某人说:“the quick brown fox”,这时我们使用一个新的网络,这里输入 x x x和输出 y y y的数量都是一样的,在实际中,它有可能是双向的LSTM结构,或者双向的GRU结构,并且通常是很深的模型。但注意一下这里时间步的数量,它非常地。在语音识别中,通常输入的时间步数量要比输出的时间步的数量多出很多。举个例子,比如有一段10秒的音频,并且特征是100赫兹的,即每秒有100个样本,于是这段10秒的音频片段就会有1000个输入,就是简单地用100赫兹乘上10秒。所以有1000个输入,但可能输出就没有1000个字母了,或者说没有1000个字符。 t t t _ h _ e e e _ _ _ ⊔ _ _ _ q q q _ _ ttt\_h\_eee\_\_\_\sqcup \_\_\_qqq\_\_ ttt_h_eee______qqq__

CTC损失函数允许RNN生成上面的输出,这里的下划线是一个特殊的字符,叫做空白符,然后这里可能有个空格,我们用 ⊔ \sqcup 来表示空格,这样的输出也被看做是正确的输出,这段输出对应的是"the q"。CTC损失函数的一个基本规则是将空白符之间的重复的字符折叠起来。the和quick之间有一个空格符,所以要输出一个空格,通过把用空白符所分割的重复的字符折叠起来,然后就可以把这段序列折叠成"the q"。最后我们得到的文本会短很多。于是这句"the quick brown fox"包括空格一共有19个字符,在这样的情况下,通过允许神经网络有重复的字符和插入空白符使得它能强制输出1000个字符

触发字检测
随着语音识别的发展,越来越多的设备可以通过声音来唤醒,这有时被叫做触发字检测(detection)系统。

触发字系统的例子包括Amazon echo,它通过单词Alexa唤醒;还有百度DuerOS设备,通过"小度你好"来唤醒;苹果的Siri用Hey Siri来唤醒;Google Home使用Okay Google来唤醒,这就是触发字检测系统。如果能建立一个触发字检测系统,也许就能让电脑通过声音来执行某些事。

关于触发字检测系统的文献,还处于发展阶段。对于触发字检测,最好的算法是什么,目前还没有一个广泛的定论。这里就简单介绍一个能够使用的算法。现在有一个这样的RNN结构,我们要做的就是把一个音频片段计算出它的声谱图特征得到特征向量 x < 1 > x^{<1>} x<1> x < 1 > x^{<1>} x<1> x < 3 > ⋯ x^{<3>}\cdots x<3>,然后把它放到RNN中,最后要做的就是定义目标标签 y y y。假如音频片段中的这一点是某人刚刚说完一个触发字,比如"Alexa",那么在这一点之前,就可以在训练集中把目标标签都设为0,然后在这个点之后把目标标签设为1。假如在一段时间之后,触发字又被说了一次,比如是在这个点说的,那么就可以再次在这个点之后把目标标签设为1。这样的标签方案对于RNN来说是可行的,并且确实运行得非常不错。不过该算法一个明显的缺点就是它构建了一个很不平衡的训练集,0的数量比1多太多了,即正负样本分布不均。

一种解决办法是在出现一个触发字时,将其附近的RNN都输出1。这样就简单粗暴地增加了正样本。比起只在一个时间步上去输出1,其实可以在输出变回0之前,多次输出1,或说在固定的一段时间内输出多个1。这样的话,就稍微提高了1与0的比例,这确实有些简单粗暴。在音频片段中,触发字刚被说完之后,就把多个目标标签设为1,这里触发字又被说了一次。说完以后,又让RNN去输出1。

结论和致谢
深度学习是一种超能力,通过深度学习算法,可以让计算机拥有"视觉",可以让计算机自己合成小说,或者合成音乐,可以让计算机将一种语言翻译成另一种,或者对放射影像进行定位然后进行医疗诊断,或者构建自动驾驶系统。

序列模型编程作业

  • 搭建循环神经网络及其应用
  • 词向量的运算与Emoji生成器
  • 机器翻译与触发词检测
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值