为什么选择序列模型?
循环神经网络(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^{
输出数据也一样,还是用 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)
想要表示一个句子里的单词,第一件事是做一张词表,有时也称为词典,意思是列一列表示方法中用到的单词。这个词表中的第一个词是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} ⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡aaaron⋮and⋮harry⋮potter⋮zulu⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤12⋮367⋮4075⋮6830⋮10000
所以在这个例子中用了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^{
如果遇到了一个不在词表中的单词,答案就是创建一个新的标记,也就是一个叫做Unknow Word的伪造单词,用作为标记,来表示不在词表中的单词。
循环神经网络模型
使用标准神经网络,我们有9个输入单词。这9个输入单词,可能是9个one-hot向量,然后将它们输入到一个标准神经网络中,经过一些隐藏层,最终会输出9个值为0或1的项,它表明每个输入单词是否是人名的一部分。
这个方法并不好,主要有两个问题:
一是输入和输出数据在不同例子中可以有不同的长度,不是所有的例子都是 T x = T y T_x=T_y Tx=Ty。即使每个句子都有最大长度,也许能够填充(pad)或零填充(zero pad)使每个输入语句都达到最大长度,也就是说设定一个最大序列长度,对每个输入和输出序列补零并统一到最大长度。但仍然看起来不是一个好的表达方式。
二是它并不共享从文本的不同位置上学到的特征。具体来说,如果神经网络已经学习到了在位置1出现的Harry可能是人名的一部分,那么如果Harry出现在其他位置,比如 x < t > x^{
x < 1 > , ⋯ , x < t > , ⋯ , x < T x > x^{<1>},\cdots,x^{
下面建立循环神经网络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^{
要开始整个流程,在零时刻需要构造一个激活值 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^{
写成矩阵形式: 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^{
通过时间的反向传播
当在编程框架中实现循环神经网络时,编程框架(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}^{
它对应的是序列中一个具体的词,如果它是某个人的名字,那么 y < t > {y}^{
现在来定义整个序列的损失函数: 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}^{
反向传播算法需要在相反的方向上进行计算和传递信息,最终就是把前向传播的箭头都反过来,在这之后就可以计算出所有合适的量,然后就可以通过导数相关的参数,用梯度下降法来更新参数。
在这个反向传播的过程中,最重要的信息传递或者说最重要的递归运算就是这个从右到左的运算,这个算法有一个很别致的名字,叫做“通过(穿越)时间反向传播(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的偏导数。
不同类型的循环神经网络
事实上,对于其他一些应用, 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很好地实现。
比如做一个语音识别系统,某句语音有两种翻译:
应该更像第二种,事实上,这就是一个好的语音识别系统要帮助输出的东西,即使这两句话听起来是如此相似。而让语音识别系统去选择第二个句子的方法就是使用一个语言模型,利用语言模型得到各自语句的概率,选择概率最大的语句作为正确的翻译。
一个语音识别模型可能算出第一句话的概率是 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×10−13而第二句话的概率是 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×10−10比较这两个概率值,显然说的话更像是第二种,因为第二句话的概率比第一句高出1000倍以上,语音识别系统能够在这两句话中作出选择。
语言模型会告诉你某个特定的句子它出现的概率是多少。概率计算表达式为: P ( y < 1 > , y < 2 > , ⋯ , y < T y > ) P(y^{<1>},y^{<2>},\cdots,y^{
为了使用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(
然后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^{
如果用很大的训练集来训练这个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^{
第一步是对模型生成的第一个词进行采样,于是输入 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}^{
也正是这个原因,所以基本的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}^{
在讲很深的神经网络时,也提到了梯度爆炸,在反向传播的时候,随着层数的增多,梯度不仅可能指数型的下降,也可能指数型的上升。事实上梯度消失在训练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^{
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^{
所以这些等式表示了GRU单元的计算,在每个时间步,将用一个候选值 c ~ < t > \tilde{c}^{
在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^{
然后GRU的关键部分就是用 c ~ \tilde{c} c~更新 c c c的等式。然后门决定是否要真的更新它。记忆细胞 c < t > c^{
接下来要给GRU用的式子就是 c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{
公式总结:
GRU单元输入 c < t − 1 > c^{
再用一个不同的参数集,通过sigmoid激活函数算出更新门 Γ u \Gamma_u Γu,公式3。最后所有的值通过另一个运算符(空白框)结合,公式4。它把 c < t − 1 > c^{
问题:若 Γ u = 0 \Gamma_u=0 Γu=0,岂不是 a < t > = a < t − 1 > a^{
这就是一个简化过的GRU单元,它的优点就是通过门决定,当从左到右扫描一个句子的时候,这个时机是要更新某个记忆细胞,还是不更新,不更新直到真的需要使用记忆细胞的时候,这可能在句子之前就决定了。因为Sigmoid的值, Γ u \Gamma_u Γu门很容易取到0值,或者说非常接近0。在这样的情况下,这个更新式子就会变成 c < t > = c < t − 1 > c^{
一个细节: c < t > c^{
当然在实际应用中 Γ 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^{
公式总结:
有很多方法可以来设计这些类型的神经网络,然后为什么有 Γ 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我们有:
c ~ < t > \tilde{c}^{
LSTM是一个比GRU更加强大和通用的版本,论文:Hochreiter & Schmidhuber 1997. Long Short-Term Memory,它在序列模型上有着巨大影响。这篇论文是挺难读懂的,它在深度学习社群有着重大的影响,深入讨论了梯度消失的理论。
上面是LSTM主要的式子,在LSTM中不再有 a < t > = c < t > a^{
下面这个图的灵感来自于Christopher Olah的一篇博客,标题是《理解LSTM网络》,翻译版见此处。这里的这张图跟他博客上的图是很相似的,但关键的不同可能是这张图用了 a < t − 1 > a^{
如下图所示,这是其中一个,再把它们连起来,就是把它们按时间次序连起来,这里输入 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^{
LSTM主要的区别在于一个技术上的细节,比如这有一个100维的向量,有一个100维的隐藏的记忆细胞单元,然后比如 c < t − 1 > c^{
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模型,这个模型可以在序列的某点处不仅可以获取之前的信息,还可以获取未来的信息。
在判断第三个词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}^{
这就是双向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]
看看这个值( 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。
总结一下,这是如何用词嵌入做迁移学习的步骤:
第一步,先从大量的文本集中学习词嵌入。一个非常大的文本集,或者可以下载网上预训练好的词嵌入模型。
第二步,可以用这些词嵌入模型把它迁移到新的只有少量标注训练集的任务中,比如说用这个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] eman−ewoman≈⎣⎢⎢⎡−2000⎦⎥⎥⎤
类似的,假如用 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] eking−equeen≈⎣⎢⎢⎡−2000⎦⎥⎥⎤
这个结果表示,man和woman主要的差异是gender上的差异,而king和queen之间的主要差异,根据向量的表示,也是gender上的差异,这就是为什么 e m a n − e w o m a n e_{man}-e_{woman} eman−ewoman和 e k i n g − e q u e e n e_{king}-e_{queen} eking−equeen结果是相同的。当算法被问及man对woman相当于king对什么时,算法所做的就是计算 e m a n − e w o m a n e_{man}-e_{woman} eman−ewoman,然后找出一个向量也就是找出一个词,使得 e m a n − e w o m a n ≈ e k i n g − e ? e_{man}-e_{woman}\approx e_{king}-e_{?} eman−ewoman≈eking−e?,也就是说,当这个新词是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} eman−ewoman≈eking−ew,即找到单词 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} eking−eman+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,eking−eman+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} eking−eman+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} eman−ewoman≈eking−ew成立的关系,会像上边那样成一个平行四边形,尽管在这个例子最初的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)=∥u∥2∥v∥2uTv. 分子其实就是 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 ∥u−v∥2. 距离越小,相似性越大。
如果向量 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) (1⩽k⩽300). 它等于 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). x→y(orange→juice).
为了表示输入,比如单词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(t∣c)=∑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=1∑10000yilogy^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) |
---|---|---|
orange | juice | 1 |
orange | king | 0 |
orange | book | 0 |
orange | the | 0 |
orange | of | 0 |
总结一下,生成这些数据的方式是选择一个上下文词,再选一个目标词,表的第一行给了一个正样本。然后给定 k k k次,将用相同的上下文词,再从字典中选取随机的词,king、book、the、of等,并标记0,这些就会成为负样本。出现以下情况也没关系,就是如果从字典中随机选到的词,正好出现在了词距内,比如说在上下文词orange正负10个词之内。
接下来构造一个监督学习问题,其中学习算法输入 x x x,输入这对词,要去预测目标的标签,即预测输出 y y y。因此问题就是给定一对词,像orange和juice,这两个词是通过对靠近的两个词采样获得的还是分别在文本和字典中随机选取得到的呢?这个算法就是要分辨这两种不同的采样方式,这就是如何生成训练集的方法。
那么在这个例子中,我们就用 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(t∣c)=∑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=1∣c,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|} ∣v∣1,均匀且随机地抽取负样本,这对于英文单词的分布是非常没有代表性的。
所以论文的作者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)