11-序列模型

循环序列模型

序列模型介绍

输入或输出中包含序列数据的模型叫做序列模型。以循环神经网络RNN为基础建立的序列模型在自然语言处理、语音识别等领域引起巨大变革。

下面是一些序列模型的典型应用:

  • 语音识别:输入输出都是序列
  • 音乐生成:输出为序列
  • 情感分析:输入为序列
  • DNA序列分析:输入为序列
  • 机器翻译:输入输出都是序列
  • 视频行为识别:输入为序列
  • 命名实体识别:输入输出都为序列

数学符号定义

例如命名实体识别问题中,识别一句话中的所有人名:

x: Harry Potter and Hermione Granger invented a new spell.

y: 1 1 0 1 1 0 0 0 0

输入和输出为x和y。

  • 使用x<t>x^{<t>}来表示第t个词,y<t>y^{<t>}表示第t个词对应结果。例如句子中的Harry Potter分别对应x<1>,x<2>x^{<1>},x^{<2>},对应的y是y<1>,y<2>y^{<1>},y^{<2>}
  • 使用Tx,TyT_x,T_y表示输入和输出的长度,上面句子中Tx=9T_x = 9Ty=9T_y = 9,但TxT_xTyT_y不一定相等
  • 使用(i)(i)表示第i个样本,因此x(i)<t>x^{(i)<t>}第i个样本的第t个元素,y(i)<t>y^{(i)<t>}表示第i个样本的第t个输出,Tx(i)T_x^{(i)}表示第i个样本的输入长度,Ty(i)T_y^{(i)}表示第i个样本的输出长度

在自然语言处理中,表示序列中一个单独的词,第一件事是建立一张词表,也称为词典,列出所有表示方法中用到的单词,例如,词表是下面这样:

[aAaronandharryPotterZulu]\begin{bmatrix}a & Aaron & \cdots & and & \cdots & harry & \cdots Potter & \cdots & Zulu \end{bmatrix}

共有10000个单词,其中第一个词是a,第二个词是Aaron,单词and的索引是367,单词Harry索引是4075,Potter索引是6830。

接下来使用one-hot表示法来表示词典里的每个单词。

例如,x<1>x^{<1>}表示Harry单词,那么他就是一个第4075行是1,其余行全为0的向量。

x<1>=[014075th index0]x^{<1>} = \begin{bmatrix}0 & \cdots & \overbrace{1}^{4075_{th}\ index} &\cdots & 0 \end{bmatrix}

同理,

x<2>=[016830th index0]x^{<2>} = \begin{bmatrix}0 & \cdots & \overbrace{1}^{6830_{th}\ index} &\cdots & 0 \end{bmatrix}

……

因此每个单词都用一个10000维的向量来表示,向量中1的位置就是词表中该单词的位置。

x<t>x^{<t>}是一个one-hot向量,只有一个值为1,其余全为0。

最后,如果遇到一个不在词表中的单词,那么就创建一个新的标记,一个叫做Unknown Word的伪造单词,用<UNK>作为标记,后续会详细讨论。

循环神经网络模型

为什么不使用标准网络

image-20221003092838813

使用标准网络如上图所示,有9个输入向量,输入到标准神经网络中,经过隐藏层,输出9个值为0或1的项,表明每个单词是否是人名的一部分。

但结果表明这种方法并不好,主要有两个问题:

  • 输入和输出数据在不同任务中有不同的长度,不是所有的里都有着同样输入长度TxT_x和同样输出长度TyT_y,即使每个句子设定一个最大长度,使用填充(pad)是每个句子达到最大长度,但这仍然不是一个好的表达方式
  • 一个像这样单纯的网络结构,并不共享从文本的不同位置学到的特征,具体来说,如果神经网络已经学习到了位置1的Harry是人名的一部分,那么如果Harry出现在句子其他位置时,这种网络会重新识别一遍,而不是自动地根据之前的学习情况将Harry识别为人名的一部分

同时,上面例子每个单词x<t>x^{<t>}都是一个10000维的one-hot

向量,那么第一层的权重矩阵会有巨量参数。

因此需要研究新的神经网络结构。

循环神经网络

下图为循环神经网络的结构。

image-20221003094744242

以从左到右的顺序读一个句子,第一个单词是x<1>x^{<1>},输入到神经网络层,得到y^<1>\widehat{y}^{<1>}

然后读到第二个单词x<2>x^{<2>}时,不仅使用x<2>x^{<2>}来预测y<2>y^{<2>},还使用来自时间步1,预测y^<1>\widehat{y}^{<1>}时的某些信息,具体来说,时间步1中的激活值会传递时间步2中,然后在下一个时间步,循环神经网络输入单词x<3>x^{<3>},然后根据时间步2的激活值和单词x<3>x^{<3>}预测出y<3>y^{<3>},以此类推。

概括来说,在每一个时间步中,循环神经网络传递一个激活值到下一个时间步用于计算。

在这个例子中,Tx=TyT_x = T_y,如果TxTyT_x \ne T_y,这个结构需要做一些改变。


但是第一个时间步没有上一步给它传递激活值,因此需要在零时刻构造一个激活值a<0>a^{<0>},通常是零向量,有些研究人员也会使用其他方法初始化a<0>a^{<0>},但使用零向量作为零时刻的伪激活值是最常见的选择。


循环神经网络从左向右扫描数据,同时每个时间步的参数也是共享的。

使用WaxW_{ax}表示从x<t>x^{<t>}到隐藏层连接的一系列参数,每个时间步都使用相同参数WaxW_{ax}

激活值,也就是水平连接,是由参数WaaW_{aa}决定的,每一个时间步都使用相同参数WaaW_{aa}

同样,输出结果y<t>y^{<t>}由参数WyaW_{ya}决定,每个时间步使用相同参数WyaW_{ya}

image-20221003100317817

以第一个时间步为例,计算过程为:

a<1>=g1(Waaa<0>+Waxx<1>+ba)y^<1>=g2(Wyaa<1>+by)a^{ <1> } = g_1(W_{aa}a^{ <0> } + W_{ax} x^{ <1> } + b_a) \\ \widehat{y}^{ <1> } = g_2(W_{ya} a^{<1>} + b_y)

首先计算a<1>a^{<1>},然后计算y<1>y^{<1>}

使用这种符号约定来表示矩阵下标,例如WaxW_{ax},第一个下标表示用来计算激活值a类型的变量,第二个下标表示要乘上x类型的变量;同样,WyaW_{ya}表示计算y类型的变量,要乘上a类型的变量。

激活函数g1,g2g_1,g_2不一定相同,常用的激活函数是tanh,有时候也会用ReLU

更一般情况下,在t时刻有:

a<t>=g1(Waaa<t1>+Waxx<t>+ba)y^<t>=g2(Wyaa<t>+by)\boldsymbol{ a^{ <t> } = g_1(W_{aa}a^{ <t-1> } + W_{ax} x^{ <t> } + b_a) } \\ \boldsymbol{ \widehat{y}^{ <t> } = g_2(W_{ya} a^{<t>} + b_y) }

这些式子定义了神经网络的前向传播。

现在将这两个式子简化一下。

把式子中a<t>=g1(Waaa<t1>+Waxx<t>+ba)a^{ <t> } = g_1(\overbrace{ W_{aa}a^{ <t-1> } + W_{ax} x^{ <t> } } + b_a)简化为Wa[a<t1>,x<t>]W_a[a^{<t-1>}, x^{<t>}]

其中:

Wa=[WaaWax][a<t1>,x<t>]=[a<t1>x<t>]W_a = \begin{bmatrix}W_{aa} &|& W_{ax} \end{bmatrix} \\ [a^{<t-1>}, x^{<t>}] = \begin{bmatrix} a^{<t-1>} \\ x^{<t>} \end{bmatrix}

也就是把WaaW_{aa}WaxW_{ax}放在同一个矩阵中,行数相同,WaxW_{ax}放到WaaW_{aa}后面。

[a<t1>,x<t>][a^{<t-1>}, x^{<t>}]将两个向量放在一起形成一个列向量

例如WaaW_{aa}的维度为(100,100)(100,100),而WaxW_{ax}的维度是(100,10000)(100,10000),那么:

Wa=[Waa100Wax10000]W_a = \begin{bmatrix} \underbrace{W_{aa} }_{100} \quad|\quad \underbrace{ W_{ax} }_{10000} \end{bmatrix}

WaW_a的维度是(100,10100)(100,10100)

a<t1>a^{<t-1>}是100维的向量,x<t>x^{<t>}是10000维的向量,因此有:

[a<t1>,x<t>]=[100a<t1>——10000x<t>][a^{<t-1>}, x^{<t>}] = \begin{bmatrix}100 \rightarrow a^{<t-1>} \\ —— \\ 10000 \rightarrow x^{<t>} \end{bmatrix}

因此[a<t1>,x<t>][a^{<t-1>}, x^{<t>}]是一个10100维的向量。

Wa[a<t1>,x<t>]W_a[a^{<t-1>}, x^{<t>}]使用矩阵乘法,这样就可以将原来的两个式子相乘简化成一个矩阵乘法运算。

对于y^<t>=g2(Wyaa<t>+by)\widehat{y}^{ <t> } = g_2(W_{ya} a^{<t>} + b_y)简化为y^<t>=g2(Wya<t>+by)\widehat{y}^{ <t> } = g_2(W_{y} a^{<t>} + b_y),使用一个下标符号,表示计算时输出什么类型的量,所以WyW_y就表示它是计算y类型的权重矩阵,而上面的Wa,baW_a,b_a表示用来计算激活值a类型的权重矩阵。

因此现在前向传播公式为:

a<t>=g1(Wa[a<t1>,x<t>]+ba)y<t>=g2(Wya<t>+by)a^{<t>} = g_1(W_a[a^{ <t-1>}, x^{ <t> } ] + b_a) \\ y^{<t>} = g_2(W_ya^{ <t> } + b_y)

一个RNN单元的前向传播过程为:

image-20221003153119880

在这个神经网络中,预测y<3>y^{<3>}时,不仅使用x<3>x^{<3>},还会使用来自x<1>x^{<1>}x<2>x^{<2>}的信息,因此这种网络的一个缺点是只使用了这个序列之前的信息来做出预测,而没有使用序列之后的信息。

例如给定两个句子:

He said, "Teddy Roosevelt was a great President."

He said, "Teddy was a great President."

要判断Teddy是不是人名的一部分,仅仅知道前两个词是不够的,这两句话的前两个词相同,但是后面单词不一样,所以还需要知道Teddy后面的信息。

所以这种神经网络结构的限制是在某时刻的预测仅使用了该时刻之前序列的输入信息,而没有使用序列后部分的信息。之后的双向循环网络(BRNN)会解决这个问题。

通过时间的反向传播

已经学习了循环神经网络的前向传播,反向传播就是将前项传播反过来。

image-20221003105349345

为了计算反向传播,需要定义损失函数。

首先定义某一时刻t或序列中某一个词的损失函数,使用交叉熵损失函数:

L<t>(y^<t>,y<t>)=y<t>logy^<t>(1y<t>)log(1y^<t>)L^{<t>}(\widehat{y}^{<t>}, y^{<t>}) = -y^{<t>}\log{ \widehat{y}^{<t>} } - (1 - y^{<t>} ) \log({1- \widehat{y}^{<t>} })

它对应序列中一个具体的词,如果是某个人名字,那么y<t>=1y^{<t>} =1,然后神经网络将输出这个词是名字的概率值。将它定义为标准逻辑回归损失函数,也叫作交叉熵损失函数(Cross Entropy Loss)。

接下来定义整个序列的损失函数:

L(y^,y)=t=1TxL<t>(y^<t>,y<t>)L(\widehat{y}, y) = \sum_{t=1}^{T_x}L^{<t>}(\widehat{y}^{<t>}, y^{<t>})

也就是将所有时间步的损失函数值求和。

反向传播中需要更新的参数有Waa,Wax,Wya,by,baW_aa,W_ax,W_ya,b_y,b_a

因此这些参数都是在整个时间序列中共享的,所以这些参数对应的梯度是每一个时刻里的梯度总和。

(未完待续)

不同类型的循环神经网络

上面学到的循环神经网络,输入和输出个数相等。还有其他类型的循环神经网络包括:一对一,一对多,多对一和多对多。

一对一的结构其实就是普通的神经网络。

image-20221003162612507

一对多的结构为:

image-20221003162655028

其中,每个时间步的输出y<t>y^{<t>}都作为下一个时间步的输入。


多对一的结构为:

image-20221003162814913

也就是只在最后一个时间步输出结果。


多对多的结构包括Tx=TyT_x = T_yTxTyT_x \ne T_y

Tx=TyT_x = T_y

image-20221003163916795

这也是之前学到的结构。

TxTyT_x \ne T_y

image-20221003164003716

其中左边的输入结构称为编码器(encoder),右边的输出结构称为解码器(decoder)。

严格来说,还有一种结构:”注意力机制(attention)“,后续会学习到。

循环神经网络的梯度消失

基本的RNN有一个很大的问题,就是梯度消失。

来看一个语言模型的例子:
The cat which already ate..., was full.

The cats which aleady ate...,were full.

上面两句话的cat分别是单数和复数,对应后面的be动词是waswere

也就是最前面的单词对句子后面的单词有影响,但是上面讲到的基本的RNN模型,不擅长捕获这种长期依赖效应。

下面说明为什么。

在之前的学习中,已经知道,训练很深的网络,会出现梯度消失的问题,也就是从左到右进行前向传播后,再进行反向传播,从输出y^\widehat{y}得到的梯度很难传播回去。RNN有同样的问题,后面层的输出误差很难影响前面层的计算,这意味着,让一个神经网络能够意识到它要记住看到的是单数名词还是复数名词,然后在序列后面生成依赖单复数的waswere是很难的。

深度神经网络也存在梯度爆炸的问题,在反向传播时,随着层数增多,梯度可能指数型增长,从而导致网络参数崩溃,因此梯度爆炸很明显,如果看到NaN,或者不是数字的情况,说明出现了数值溢出,解决梯度爆炸的一个方法是梯度修剪:观察梯度向量,如果它大于某个阈值,那么就缩放梯度向量,保证它不会太大。因此梯度爆炸比较容易解决,而梯度消失更难解决。

接下来介绍GRU单元,也就是门控循环单元网络,用来解决梯度消失问题。

GRU单元

GRU单元(Gated Recurrent Unit),改变了RNN的隐藏层,从而更好地捕捉深层连接,并改善梯度消失问题。

首先给出基本的RNN单元结构:

image-20221004094854772

门控循环结构与上图的结构类似。

首先定义一个新的变量cc,表示记忆细胞,它的作用是提供记忆能力,比如一只猫的单数还是复数,当看到后面的句子时,能根据cc来判断。

于是在时间t处,有记忆细胞c<t>c^{<t>},GRU的输出激活值a<t>a^{<t>}c<t>c^{<t>}相等,即a<t>=c<t>a^{<t>} = c^{<t>}

在每个时间步,都用一个候选值c~<t>\tilde{c}^{<t>}来重写记忆细胞,代替c<t>c^{<t>},然后使用tanh激活函数计算,即c~<t>=tanh(Wc[c<t1>,x<t>]+bc)\tilde{c}^{<t>} = tanh(W_c [c^{<t-1>}, x^{<t>}] + b_c),所以c~<t>\tilde{c}^{<t>}的值就是替代值,用来代替c<t>c^{<t>}的值。

在GRU中一个重要的思想是:有一个门,叫做Γu\Gamma_u,u代表更新门,这是一个0到1之间的值,实际上这个值是把式子带入sigmoid函数得到的,Γu=sigmoid(Wc[c<t1>,x<t>]+bu)\Gamma_u = sigmoid(W_c [c^{<t-1>}, x^{<t>}] + b_u)。这个门决定是否要更新记忆细胞值。

接下来的将Γu\Gamma_uc~<t>\tilde{c}^{<t>}结合到一起:

c<t>=Γuc~<t>+(1Γu)c<t1>c^{<t>} = \Gamma_u * \tilde{c}^{<t>} + (1- \Gamma_u)* c^{<t-1>}

如果Γu=1\Gamma_u = 1,那么就使用c~<t>\tilde{c}^{<t>}代替c<t>c^{<t>};如果Γu=0\Gamma_u = 0,意思是不更新c<t>c^{<t>},使用旧值。

因此门\Gamaa_u用来决定是否更新记忆细胞,例如在梯度消失部分给出的两个句子,在cat时间步中,如果是单数,就将c<t>c^{<t>}设为1,如果是复数,就设为0。

然后GRU会一直记住c<t>c^{<t>}的值,一直到was这一步,c<t>c^{<t>}的值还是1,于是就知道了前面的名词主语是单数,使用was,完成后,门Γu\Gamma_u就告诉你什么时候更新这个记忆细胞值,在cat时间步中更新为1,在was时间步中使用完成,不需要再记住它了,因此使用Γu\Gamma_u更新。

GRU的结构为:

image-20221004103244921

这就是一个GRU单元,它的优点就是通过门决定,当从左到右扫描一个句子时,是否更新某个记忆细胞。


一些实现的具体细节包括:c<t>c^{<t>}可以是一个向量,如果有100维的隐藏激活值,那么c<t>c^{<t>}就是100维的,c~<t>\tilde{c}^{<t>}也是100维的,同样Γu\Gamma_u也是100维,这样的话,上面的公式中*表示element-wise,也就是对应元素相乘。

在实际应用中,Γu\Gamma_u不会等于0或者1,它一般是0到1的一个中间值,元素对应乘积就是告诉GRU单元,哪些记忆细胞需要更新,哪些不用更新。


上述就是GRU最重要的内容了,但上面展示的GRU结构是简化过的,下面介绍完整的GRU单元。

要改变的是在第一个式子中给记忆细胞的候选值添加一个新的项,也是一个门,叫做Γr\Gamma_r,可以认为r表示相关性relevance,这个Γr\Gamma_r门决定了c<t>c^{<t>}的候选值c~<t>\tilde{c}^{<t>}c<t1>c^{<t-1>}有多大的相关性,计算这个门Γr\Gamma_r需要参数,公式为:

Γr=σ(Wr[c<t1>,x<t>]+br)\Gamma_r = \sigma(W_r [c^{<t-1>}, x^{<t>}] + b_r)

然后候选值c~<t>\tilde{c}^{<t>}的计算变为:

c~<t>=tanh(Wc[Γrc<t1>,x<t>]+bc)\tilde{c}^{<t>} = tanh(W_c [\Gamma_r * c^{<t-1>}, x^{<t>}] + b_c)

因此完整的GRU单元的计算公式为:

{Γr=σ(Wc[c<t1>,x<t>]+br)c~<t>=tanh(Wc[Γrc<t1>,x<t>]+bc)Γu=σ(Wc[c<t1>,x<t>]+bu)c<t>=Γuc~<t>+(1Γu)c<t1>a<t>=c<t>\begin{cases} \Gamma_r = \sigma(W_c [c^{<t-1>}, x^{<t>}] + b_r) \\ \\ \tilde{c}^{<t>} = tanh(W_c [\Gamma_r * c^{<t-1>}, x^{<t>}] + b_c) \\ \\ \Gamma_u = \sigma(W_c [c^{<t-1>}, x^{<t>}] + b_u) \\ \\ c^{<t>} = \Gamma_u * \tilde{c}^{<t>} + (1- \Gamma_u)* c^{<t-1>} \\ \\ a^{<t>} = c^{<t>} \end{cases}

σ\sigma就是sigmoid函数。

LSTM

LSTM(Long-Short Term Momery),长短期记忆网络。LSTM是一个比GRU更强大和通用的版本。

在GRU中,a<t>=c<t>a^{<t>} = c^{<t>},但是在LSTM中,这两个变量不再相等。

LSTM和GRU相比,去掉了相关度门Γr\Gamma_r,增加了两个新的门:遗忘门Γf\Gamma_f和输出门Γo\Gamma_o

LSTM的公式为:

{c~<t>=tanh(Wc[a<t1>,x<t>]+bc)Γu=σ(Wu[a<t1>,x<t>]+bu)Γf=σ(Wf[a<t1>,x<t>]+bf)Γo=σ(Wo[a<t1>,x<t>]+bo)c<t>=Γuc~<t>+Γfc<t1>a<t>=Γotanh(c<t>)\begin{cases} \tilde{c}^{<t>} = tanh(W_c [a^{<t-1>}, x^{<t>}] + b_c) \\ \\ \Gamma_u = \sigma(W_u [a^{<t-1>}, x^{<t>}] + b_u) \\ \\ \Gamma_f = \sigma(W_f [a^{<t-1>}, x^{<t>}] + b_f) \\ \\ \Gamma_o = \sigma(W_o [a^{<t-1>}, x^{<t>}] + b_o) \\ \\ c^{<t>} = \Gamma_u * \tilde{c}^{<t>} + \Gamma_f* c^{<t-1>} \\ \\ a^{<t>} = \Gamma_o * tanh( c^{<t>} ) \end{cases}

因此LSTM有三个门:更新门Γu\Gamma_u,遗忘门Γf\Gamma_f和输出门Γo\Gamma_o

LSTM的结构为:

image-20221004114026538

选择使用GRU或LSTM没有统一的准则,GRU是在LSTM的基础上做的简化。GRU的优点是更简单,所以更容易创建一个大型的网络,在计算性能上运行的更快,而LSTM更加强大和灵活。

如果必须选择一个使用,大部分人还是会把LSTM作为默认的选择。

双向循环神经网络

之前学到的循环神经网络是单向的,也就是在某个时间步中,只能获取该时间步之前的信息,而无法获取该时间步之后的信息。

因此例如在命名实体识别中,给定两个句子:

He said, "Teddy Roosevelt was a great President."

He said, "Teddy was a great President."

在判断Teddy是否是人名一部分时,仅有Teddy前面的信息是不够的。

因此需要使用双向循环神经网络(Bidirectional RNN)。

单向的循环神经网络结构为:

image-20221004132604339

双向循环神经网络在单向循环神经网络的基础上,增加了从右向左的传输过程。

image-20221004134025397

使用箭头\rightarrow\leftarrow来表示从左向右和从右向左。

因此双向循环神经网络的前向传播过程是:首先从a<0>a^{<0>}开始从左向右传播,到达最后一个RNN单元后,从a<Tx>\overleftarrow{a}^{<T_x>}向左传播,到第一个RNN单元a<1>\overleftarrow{a}^{<1>}结束。

因此对于预测值有:

y^<t>=g(Wy[ a<t>,a<t>]+by)\widehat{y}^{<t>} = g(W_y [\ { \overrightarrow{a} }^{<t>}, {\overleftarrow{a} }^{<t>} ] + b_y)

上述就是双向循环神经网络的基本内容。这些基本单元不仅仅可以用于标准RNN结构,也能用于GRU单元或者LSTM单元。

事实上,对于有大量自然语言处理问题的文本,有LSTM单元的双向RNN模型是用得最多的。


双向RNN网络模型的缺点是需要完整的数据序列,而不能预测任意位置。

例如语言识别系统,使用双向RNN模型必须考虑整个语音表达,因此需要获取整个语音表达后才能处理这段语音,而对于语言识别系统来说,需要根据当前的语音输入,实时处理,所以对于这种应用,通常会有更加复杂的模块。

深层循环神经网络

将RNN的多个层堆叠到一起构建深层的循环神经网络(Deep RNN)。

这是之前的单层循环神经网络。

image-20221004140629040

堆叠多层,构建一个深层循环神经网络:

image-20221004140934597

其中符号[1]表示第一层。每一个单元都有两个输入,一个输出。

来看一个具体的例子,例如第二层的第三个单元a[2]<3>a^{[2]<3>},计算公式为:

a[2]<3>=g(Wa[2] [a[2]<2>,a[1]<3>]+ba[2])a^{[2]<3>} = g(W_a^{[2]}\ [a^{[2]<2>}, a^{[1]<3>}] + b_a^{[2]})

每一层都有自己的参数Wa[i]W_a^{[i]}ba[i]b_a^{[i]}

对于普通的深度神经网络,可能有100层,但是对于RNN来说,有三层就已经不少了。

其中深层循环神经网络的每一个单元可以是标准RNN单元,也可以是GRU单元或者LSTM单元。并且也可以构建深层的双向RNN网络。

自然语言处理和词嵌入

词汇表示

已经学习了RNN、GRU单元和LSTM单元等。接下来学习如何把这些模型应用到自然语言处理中。

自然语言处理中, 一个很重要的概念就是词嵌入(word embeddings),这是语言表示的一种方式,可以让算法自动理解一些类似的词,例如男人对女人,国王对王后,等等。

首先学习词汇表示,目前为止一直使用词表来表示词,每个词对应一个one-hot向量,例如man在词表位置是5391,可以使用O5391O_{5391}代表这个one-hot向量,其他词kingqueenappleorange等都可以这样表示出来。

但这种表示方式的一大缺点是:每个词都是孤立的,这样使得算法对相关词的泛化能力不强。

例如,语言模型已经学习了,看到句子I want a glass of orange __,那么下个词很可能是juice,但是当语言模型看到句子I want a glass of apple __时,因为模型并不知道orangeapple是相近或者类似的东西,所以模型无法推断横线的词。

这是因为任意两个one-hot向量的内积都是0,所以任意两个词都是孤立的,没有联系。

因此换一种表达方式会更好,使用特征化来表示每个词,manwoman等这些单词,学习这些词的特征或者数值。

例如,可以使用特征:性别,年龄,食物等等来表示一个词。

ManWomenKingQueenAppleOrange
Gender-11-0.950.970.000.00
Age0.030.020.70.690.03-0.02
Food0.010.010.020.010.950.97
……
size

对于特征 性别Gender来说,男人的特征值设为-1,女人特征值设为1,因此国王King大部分为男人,所以值设为-0.95,而王后大部分为女人,值可以设为0.97,而苹果(Apple)和橘子(Orange)和性别没有相关度,因此设为0;

对于年龄来说,男人(Man)和女人(Woman)和年龄没什么联系,因此值很小,而国王和王后大部分都是成年人,因此值较大,苹果和橘子同样和年龄没有联系。

还可以有很多特征,来表示一个词。

上述只是为了理解简单,在实际中并不使用这些实际的有具体含义的特征来表示一个词。


总而言之,词嵌入可以将概念相似或有联系的词在数值化上联系的更紧密。

词嵌入算法会学习非常大的无标签的文本集,然后可以迁移学习,在一个有较小训练集的任务中使用。

具体步骤是:

  • 从海量的文本训练集中学习词嵌入
  • 将词嵌入迁移到有较小规模训练集的新任务中
  • 可选:继续使用新数据微调词嵌入模型

所以当任务的训练集相对较小时,词嵌入的作用最明显。

词嵌入的特性

词嵌入还有一个特性是能够帮助类比推理。

例如,提出一个问题,如果man对应woman,那么king应该对应什么,我们人类可以推理出king对应的是queen,词嵌入就可以做这样的类比推理。

image-20221005100921014

如上图,每个词使用四个特征来表示,每个词是一个四维向量,例如man可以用嵌入向量emane_{man}来表示,women用嵌入向量ewomene_{women}表示。

那么将emane_{man}ewomane_{woman}相减可得到:

emanewoman=[10.010.030.09][10.020.020.01][2000]e_{man} - e{woman} = \begin{bmatrix} -1 \\ 0.01 \\ 0.03 \\ 0.09 \\ \end{bmatrix} - \begin{bmatrix} 1 \\ 0.02 \\ 0.02 \\ 0.01 \\ \end{bmatrix} \approx \begin{bmatrix} -2 \\ 0 \\ 0 \\ 0 \\ \end{bmatrix}

类似的,ekinge_{king}equeene_{queen}做减法运算有:

ekingequeen=[0.950.930.700.02][0.970.950.690.01][2000]e_{king} - e{queen} = \begin{bmatrix} -0.95 \\ 0.93 \\ 0.70 \\ 0.02 \\ \end{bmatrix} - \begin{bmatrix} 0.97 \\ 0.95 \\ 0.69 \\ 0.01 \\ \end{bmatrix} \approx \begin{bmatrix} -2 \\ 0 \\ 0 \\ 0 \\ \end{bmatrix}

这两个运算的结果表示:manwoman主要的差异是gender性别的差异,而kingqueen也是性别的差异,所以得出这种类比推理的结果就是,当算法需要推理man对应woman,那么king对应什么的时候,算法要做的是计算emanewomane_{man} - e_{woman},然后找出一个向量也就是找出一个词w,使得emanewomanekingewe_{man} - e_{woman} \approx e_{king} - e_{w}

因此可以定义一个相似度函数Sim,算法需要做的就是找到一个单词w:

argmax{Sim(ew,ekingeman+ewoman)}argmax \{Sim(e_w, e_{king} - e_{man} + e_{woman})\}

也就是将上面等式的ewe_w挪到一遍,剩下项挪到另一边,然后找到使得相似度函数值最大的那个单词w,就是推理的结果。

最常用的相似度函数是余弦相似度,公式定义为:

cos(θ)=sim(u,v)=uTvuv\cos(\theta) = sim(u,v) = \frac{u^Tv}{\parallel u\parallel\parallel v\parallel}

uTvu^Tv表示向量u、v的内积,也就是向量对应元素相乘。下面的\parallel \parallel表示向量的范数或长度。

ABAB=i=1nAi×Bii=1nAi2×i=1nBi2\frac{A \cdot B}{\parallel A\parallel\parallel B\parallel} = \frac{ \sum_{i=1}^{n}A_i \times B_i }{ \sqrt{\sum_{i=1}^{n} {A_i}^2 } \times \sqrt{\sum_{i=1}^{n} {B_i}^2 } }

这个式子其实就是向量A和B的夹角余弦值,当夹角为0,余弦值为1;夹角为90,度,余弦值为0。

因此,两个向量越相似,角度越小,余弦相似度就越接近于1。


因此词嵌入一个显著成果就是,可以学习类比关系的一般性,例如能学习man对应woman相当于boy对应于girlCanada首都是Ottawa,相当于China对应BeiJing等等。

嵌入矩阵

将学习词嵌入这一问题具体化,当应用算法学习词嵌入时,实际上是学习一个嵌入矩阵。

举个例子,假设词表有10000个单词,词嵌入要做的就是学习一个嵌入矩阵EE,他将是一个300×10000300\times 10000的矩阵,这个矩阵的各列代表词汇表中不同单词的嵌入向量,假设orange单词编号是6257,使用符号O6257O_{6257}表示这个10000维的one-hot向量,它在第6257个位置上是1,其余各处都是0。

那么该单词的嵌入向量可以用:e6245=EO6257e_{6245} = EO_{6257}

更一般来说,如果要得到第i个单词的嵌入向量,可以用ei=EOie_{i} = EO_{i}得到。

实际就是取嵌入矩阵EE的第i列。

在学习嵌入矩阵时,使用ei=EOie_{i} = EO_{i}更为方便,但在实际代码中,不会使用相乘的方法来得到一个嵌入向量,因为计算效率太低,而是有一个专门的函数来获取矩阵EE的某列。

在学习这个嵌入矩阵时,首先随机初始化矩阵,然后使用梯度下降法来学习这个矩阵的参数。

Word2Vec

Word2Vec是词嵌入的一种训练方法。将one-hot向量作为word2vec的输入,通过word2vec训练低维词向量(word embedding)。Word2vec主要包括两种训练模型:Skip-gram模型和``CBOW(Continuous Bag-of-Words Model)模型,包括两种加速训练的算法:Hierarchical Softmax`(分层Softmax)和负采样(Negative Sample)。

Word2Vec是从大量的文本语料中以无监督的方式学习语义知识的一种模型,通过学习文本,来用词向量的方式表示词的语义信息,即通过一个嵌入空间,使得语义上相似的单词在此空间内的距离很近,Embedding其实就是一个映射,将单词从原来所属的词表空间映射到新的多维空间,也就是把原来词所在空间嵌入到一个新的空间里去。

举个例子,判断一个词的词性,是动词还是名词,用机器学习的思路,有一系列的样本(x,y),x是词语,y是词性,要通过机器学习算法构建一个f(x)->y的映射,但这里的数学模型只接受数值型的输入,而NLP中词语都是符号形式的,所以需要把这些词转换成数值类型,换句话说,嵌入到一个数学空间中,这种嵌入方法,就叫做词嵌入,Word2Vec就是词嵌入的一种。

f(x)->y

在NLP中,把x看做句子中的一个词语,而y看做这个词语的上下文词语,而f就是语言模型,这个语言模型的目的就是判断(x,y)这个样本是否符合自然语言的法则,更通俗一点,就是词语x和y放一块,是不是一句人话。

Word2Vec正是来源于这个思想,但Word2Vec不是要把f训练的多么完美,而是关心模型训练完后的参数(这里特指神经网络的权重),然后把这些权重作为输入x的向量化表示,这个向量就叫做词嵌入向量。

两种模型的简单形式

上面提到,y是x的上下文,所以如果y只取上下文里一个词语时,语言模型就变为:

用当前词预测它的下一个词y。

这里的x使用one-hot向量表示,y是词表中每个词的输出概率,我们希望输出的y跟真实的yone-hot向量越接近越好。

image-20221007165001581

上面模型结构图就是一个输入对应一个输出。

首先说明:隐层没有激活函数,或者说隐层的激活函数是线性的。

训练完成后得到的神经网络权重,就是我们需要的嵌入矩阵,因为输入是one-hot向量,所以从输入层到隐藏层的权重里,只有1对应的位置被激活,这些权重个数,和隐藏层的节点数是一致的,也就是每个节点都只有一个权重被激活,这些权重写成向量,就是当前输入词的词向量,所以这个向量就可以唯一表示x。

输入层到隐藏层的权重矩阵是WV×NW_{V\times N},其中V是语料库中词的总数量,N是隐藏层的节点数,这是一个超参数。这里权重矩阵维度是(V,N),因此输入x如果是列向量,那么对应的词向量是xTWx^TW,得到的词向量是一个行向量;如果将权重矩阵写成维度是(N,V)的矩阵,那么输入x是列向量,对应的词向量是WxWx,得到的词向量是一个列向量。因此权重矩阵写成这两种维度都可以,转置关系,注意对应x是列向量还是行向量即可。

词向量的维度(也就是隐藏层节点数量)一般要远远小于词语总数V,所以Word2Vec本质上是一种降维操作,将稀疏的one-hot型向量降维到Word2Vec的稠密向量形式。

因此CBOW模型和Skip-gram模型都是训练用稠密向量表示词的方法,并且可以将相似的词聚合起来,在嵌入空间的距离比较近。

在Wrod2Vec中,将文本分成两类:Target和Context,也就是目标词和上下文词。

CBOW模型,在给定上下文词的情况下,预测目标词输出概率,即上下文词作为输入,目标词概率为输出;

Skip-gram模型,是在给定目标词的情况下,预测所有上下文词的输出概率,即目标词为输入,上下文词概率为输出。

两种模型的相似之处

  • CBOW模型和Skip-gram模型都是把文本分别两组:目标词(Target)和上下文词(Context),其中上下文词的数量取决于设定的窗口大小,假设窗口大小为2,那么上下文词会是:目标词左侧的两个词,和目标词右侧的两个词,如果上下文词的数量为:C,窗口大小为:w,有:C2wC \le 2w

  • 输入:两种模型中,词都首先需要转换成ont-hot向量再传入模型中

  • 第一次转换:输入层到隐层的权重矩阵WW,维度为(V,N)(V,N),V是语料库的单词数量,N用来定义词嵌入向量的维度,是一个超参数。输入的one-hot向量乘以权重矩阵WW得到一个N维的嵌入向量

  • 第二次转换:隐层到输出层的权重矩阵WW^{\prime},维度为(N,V)(N,V),隐层输出的N维嵌入向量乘以权重矩阵WW^{\prime},得到一个V维的向量

  • 使用softmax函数输出预测概率,每个值都在0和1之间,具有最高概率的元素最可能是目标词

    softmax(z)=exp(zi)j=1Vexp(zj)softmax(z) = \frac{exp(z_i)}{\sum_{j=1}^{V}exp(z_j) }

    zz是一个V维的向量,ziz_i表示向量zz中第i个元素。

  • 参数更新:通过反向传播来更新参数,权重矩阵WWWW^{\prime}都是随机进行初始化的

两种模型的不同之处

  • CBOW模型根据给定的上下文词预测目标词,而Skip-gram模型根据目标词预测上下文词
  • CBOW模型,因为上下文词可能有多个,所以在输入层到隐层中,上下文词的one-hot向量乘以权重矩阵得到多个N维的向量后,需要取平均值再传入隐层

一个例子

以一个例子来说明CBOWSkip-gram模型的工作流程。

  • 语料库:I drink coffee everyday
  • 目标词:coffee
  • 窗口大小:2
  • 上下文:{I,drink,everyday}

注意,因为这里语料库仅有四个词,所以大小为2的窗口全包括了,在实际中,语料库要大得多。

四个单词的one-hot向量为:

  • I[1000]T\begin{bmatrix}1&0&0&0\end{bmatrix}^T
  • drink[0100]T\begin{bmatrix}0&1&0&0\end{bmatrix}^T
  • coffee[0010]T\begin{bmatrix}0&0&1&0\end{bmatrix}^T
  • everyday[0001]T\begin{bmatrix}0&0&0&1\end{bmatrix}^T

设隐藏层节点个数为3,也就是词嵌入向量维度为3。

那么CBOW的计算流程为:

image-20221008104352336

  1. 首先初始化权重矩阵WW和权重矩阵WW^{\prime}

    W4×3=[111221311021]W3×4=[111022221120]W_{4\times3} = \begin{bmatrix} 1 & 1 & -1 \\ 2 & 2 & 1 \\ 3 & 1 & 1 \\ 0 & 2 & 1 \\ \end{bmatrix} \qquad W^{\prime}_{3\times4} = \begin{bmatrix} 1 & -1 & 1 & 0\\ 2 & 2 & 2 & 2 \\ -1 & -1 & 2& 0 \\ \end{bmatrix}

  2. 使用上下文词的one-hot向量与权重矩阵WW相乘,得到三个N维的向量,然后取平均值,得到一个N维的向量,这里N=3

    [1000]×W=[111][0100]×W=[221][0001]×W=[021]}AVG[1+2+031+2+231+1+13][11.670.33]\left .\begin{array}{} \begin{bmatrix}1 & 0 & 0 & 0\end{bmatrix}\times W = \begin{bmatrix}1 & 1 & -1 \end{bmatrix} \\ \begin{bmatrix}0 & 1 & 0 & 0\end{bmatrix}\times W = \begin{bmatrix}2 & 2 & 1 \end{bmatrix} \\ \begin{bmatrix}0 & 0 & 0 & 1\end{bmatrix}\times W = \begin{bmatrix}0 & 2 & 1 \end{bmatrix} \\ \end{array}{} \\ \right\} AVG \Longrightarrow \begin{bmatrix}\frac{1+2+0}{3} & \frac{1+2+2}{3} & \frac{-1+1+1}{3} \end{bmatrix} \approx \begin{bmatrix}1 & 1.67 & 0.33 \end{bmatrix}

  3. 使用隐藏得到的N维向量与权重矩阵WW^{\prime}相乘

    [11.670.33]×W=[4.012.015.003.34]\begin{bmatrix}1 & 1.67 & 0.33 \end{bmatrix} \times W^{\prime} = \begin{bmatrix}4.01 & 2.01 & 5.00 & 3.34 \end{bmatrix}

  4. 然后经过softmax输出预测概率

    [4.012.015.003.34]softmax[0.230.030.620.12]\begin{bmatrix}4.01 & 2.01 & 5.00 & 3.34 \end{bmatrix} - softmax\rightarrow\begin{bmatrix}0.23 & 0.03 & 0.62 & 0.12 \end{bmatrix}

    其中0.62就是目标词coffee的预测输出概率

Skip-gram模型的计算流程为:

image-20221008110834353

Skip-gram模型的计算流程和CBOW类似,首先使用目标词的one-hot向量与权重矩阵WW相乘,得到一个N维的矩阵,然后再乘以权重矩阵WW^{\prime},得到C个中间向量,每一个上下文词对应一个向量,然后使用softmax得到概率。

注意:因为权重矩阵WW^{\prime}是共享的,这C个中间向量是完全相同的,在后面会说明如何根据不同上下文词更新参数。

CBOW&SG模型的参数更新过程

在训练过程中,每次迭代都包含两步:

  • 前向传播:使用输入词one-hot向量,和随机初始化的权重矩阵WWWW^{\prime}得到最终概率输出
  • 反向传播:评估预测输出和真实值的距离,更新(调整)参数WW^{\prime}WW,更具体来说,更新WW^{\prime}的列,和WW对应输入词的行

CBOW模型为例,说明参数更新过程。

首先定义几个数学符号:

  • WIW_I:输入词
  • WOW_O:输出词
  • uju_j:向量uu的第j个元素,一个标量
  • vwIv_{w_I}:输入词WIW_I的向量化表示,其实就是权重矩阵WW中的一行
  • vwjv^{\prime}_{w_j}:权重矩阵WW^{\prime}的一列
  • yjy_j:输出层的第j个元素

前向传播

  1. 根据输入词计算出向量hh

    h=vwIh = v_{w_I}

    因为wIw_I是一个one-hot向量,所以hhvwjv_{w_j}一样,是权重矩阵WW的对应输入词的一行

  2. 计算向量uu

    u=h×Wu = h \times W^{\prime}

    uu是一个V维的向量

  3. 通过softmax输出预测概率

    yj=P(wOwI)=exp(uj)j=1Vexp(uj)y_j = P(w_O | w_I) = \frac{ exp(u_j) }{\sum_{ j^{\prime}=1 }^{V} exp(u_{ j^{\prime} } ) }

    其中uju_j是一个标量:

    uj=h×vwju_j = h \times v^{\prime}_{w_j}

    hh是一个N维行向量,vwjv^{\prime}_{w_j}是权重矩阵WW^{\prime}的一列,是一个N维列向量,与hh相乘,得到uju_j

    并有:

    j=1Vyi=1\sum_{j=1}^{V} y_i = 1

    也就是所有预测词的概率和为1。

    因此联合前面公式,有:

    yj=P(WjWI)=exp(vwIvwj)j=1Vexp(vwIvwj)(1)y_j = P(W_j | W_I) = \frac{exp(v_{w_I} v^{\prime}_{w_j } ) } {\sum_{ j^{\prime} = 1 }^{V} exp(v_{w_I } v^{\prime}_{w_{ j^{\prime} } } ) } \tag{1}

    其中:vwjv_{w_j}是N维的行向量,vwjv^{\prime}_{w_j}是N维的列向量。

    上面公式要注意vwjv_{w_j}vwjv^{\prime}_{w_j}是行向量还是列向量,乘出来得到的uju_j是一个1×1的矩阵,也可以看做一个标量,或者使用点积运算,这样就不用管是行向量还是列向量。

    CBOW模型的输出是一个V维的列向量,每一个yjy_j都对应这个列向量里的一个元素值。

上面的式子针对的是CBOW只有一个上下文词输入的情况,如果有多个上下文词,那么只需要修改第二步的公式hh的求法:

h=1C(x1T+x1T+x1T++xCT)Wh = \frac{1}{C} (x_1^T +x_1^T + x_1^T +\cdots+ x_C^T)W

即求平均后,再计算,hh是一个N维的行向量。

反向传播

首先定义损失函数。

训练的目标是要最大化公式(1),也就是目标词的预测输出概率,在给定输入词WIW_I的条件下,实际观察词WOW_O(在输出层索引为jj*)的条件概率。

maxp(WOWI)=maxyj=maxlogyj=max(ujlogj=1Vexp(uj))=L\begin{aligned} max p(W_O | W_I) = max y_{j*} & = max \log{y_{j*} } \\ & = max\rightarrow ( u_{j*} - \log{\sum_{j^{\prime}=1 }^{V}exp(u_{j^{\prime}} } ) ) \\ & =-L \end{aligned}

因为要使概率最大,所以加个负号,就是使得L最小,取log是为了方便计算。

所以L=logp(wOwI)L = - \log{p(w_O | w_I)}是损失函数,

L=logj=1Vexp(uj)ujL = \log{\sum_{j^{\prime}=1 }^{V}exp(u_{j^{\prime} } } ) - u_{j*}

则有:

Luj=exp(uj)j=1Vexp(uj)tj=yitj=ej\begin{aligned} \frac{\partial L}{\partial u_j} & = \frac{exp(u_j) }{\sum_{j^{\prime}=1 }^V exp(u_{ j^{\prime} } ) } -t_j \\ & = y_i - t_j \\ &= e_j \end{aligned}

其中当j=jj = j*时,$ t_j=1 ,因为当,因为当j=j*$时, uju_{j*}是变量,导数为1;当jjj \ne j*时,uju_{j*}看做一个常数,导数为0,即tj=0t_j = 0

这里的tjt_j其实就代表目标词的one-hot向量,只有对应索引的值为1,所以需要减1,其他位置都为0。

导数值设为eje_j,这个导数只是输出层的预测误差。

接下来求隐含层对输出层权重的梯度。

对于权重矩阵WW^{\prime}中的每一个值wijw^{\prime}_{ij},有:

Lwij=Lujujwij=ejhi\frac{\partial L}{\partial w^{\prime}_{ij} } = \frac{\partial L}{\partial u_j} \cdot \frac{u_j}{\partial w^{\prime}_{ij} } = e_j \cdot h_i

其中详细说明下ujwij\Large \boldsymbol{\frac{u_j}{\partial w^{\prime}_{ij} } }如何计算。

首先知道:

uj=h×vwju_j = h \times v^{\prime}_{w_j}

其中hh是一个N维行向量,vwjv^{\prime}_{w_j}WW^{\prime}的第j列。

将元素详细展开,例如u1u_1

u1=h1w11+h2w21+h3w31++hNwN1u_1 = h_1 w^{\prime}_{11} +h_2 w^{\prime}_{21} + h_3 w^{\prime}_{31} + \cdots + h_N w^{\prime}_{N1}

因此有:

uj=h1w1j+h2w2j+h3w3j++hNwNju_j = h_1 w^{\prime}_{1j} +h_2 w^{\prime}_{2j} + h_3 w^{\prime}_{3j} + \cdots + h_N w^{\prime}_{Nj}

所以有:

ujwij=hi\frac{\partial u_j}{\partial w^{\prime}_{ij} } = h_i

接下来,使用随机梯度下降,可以得到隐含层到输出层参数的更新公式:

wij(new)=wij(old)αejhi{ w^{\prime}_{ij} }^{(new)} ={ w^{\prime}_{ij} }^{(old)} - \alpha \cdot e_j \cdot h_i

α\alpha是学习率。

或者:

wj(new)=wj(old)αejhTfor j=1,2,,V{ w^{\prime}_{j} }^{(new)} ={ w^{\prime}_{j} }^{ (old) } - \alpha \cdot e_j \cdot h^T \quad for\ j =1,2,\cdots,V

hTh^Twjw^{\prime}_{j}都是N维列向量。

然后求输入层到隐含层的权重更新公式:

首先损失函数LLhih_i求导:

Lhi=j=1VLujujhi=j=1Vejwij=EHi\frac{\partial L}{\partial h_i} = \sum_{j=1}^{V} \frac{\partial L}{\partial u_j} \cdot \frac{\partial u_j}{\partial h_i} = \sum_{j=1}^{V} e_j \cdot w^{\prime}_{ij} = EH_i

其中,hih_i是隐含层的第i个神经元输出,一个数值,EHEH是一个N维的列向量,EHiEH_i是其中一个元素值。

接下来要求LLWW的梯度,首先回想隐含层对输入层数据执行线性计算的过程:

hi=k=1Vxkwkih_i = \sum_{k=1}^{V} x_k\cdot w_{ki}

LLWW的每个元素求梯度:

Lwki=Lhihiwki=EHixk\frac{\partial L}{\partial w_{ki} } = \frac{\partial L}{\partial h_i} \cdot \frac{\partial h_i}{\partial w_{ki} } = EH_i \cdot x_k

所以可以得到:

LW=x×EHT\frac{\partial L}{\partial W} = x \times EH^T

其中xx是V维列向量,EHEH是N维列向量,得到一个V×NV\times N的矩阵。

因为向量xx只有一个元素值为1,其余全为0,所以最后得到的V×NV\times N矩阵LW\frac{\partial L}{\partial W},只有一行不为0,其余全为0,不为0的那一行就是列向量EHEH的转置,所以可以只更新对应输入词的那一行,其余行不需要更新。

因此可以得到WW的更新公式:

vwI(new)=vwI(old)αEHTv^{(new)}_{w_I} = v^{(old)}_{w_I} - \alpha EH^T

α\alpha是学习率。

上面的反向传播计算是针对只有一个上下文词的,如果有多个上下文词,会有一点小小的改动。

隐层到输出层参数的更新公式为:

wj(new)=wj(old)αejhTfor j=1,2,,V{ w^{\prime}_{j} }^{(new)} ={ w^{\prime}_{j} }^{(old)} - \alpha \cdot e_j \cdot h^T \quad for\ j =1,2,\cdots,V

也就是这个公式和之前一样,没有变化。

输入层到隐层的参数更新公式为:

vwI,c(new)=vwI,c(old)1CαEHTfor j=1,2,,Vv^{(new)}_{w_{I,c} } = v^{(old)}_{w_{I,c} } - \frac{1}{C}\alpha EH^T \quad for\ j =1,2,\cdots,V

Skip-gram模型的参数更新过程

Skip-gram模型和CBOW恰好相反,CBOW输入变成现在的输出,输出变成现在的输入。

前向传播

h=vwIh = v_{w_I}

其中,和CBOW一样,hh是权重矩阵WW的一行,也就是维度为N的行向量。

设上下文词共有C个,那么Skip-gram模型需要输出C个概率分布,每一个概率分布对应一个上下文词,并且由于共享权重矩阵WW^{\prime},这C个输出其实完全相同。

有:

p(wc,j=WO,CWI)=yc,j=exp(uc,j)j=1Vexp(uj)p(w_{c,j} = W_{O,C} | W_I) = y_{c,j} = \frac{exp(u_{c,j} ) }{\sum_{j^{\prime} =1 }^V exp(u_{j^{\prime} } ) }

其中yc,jy_{c,j}表示第c个词输出向量中,第j个输出元素值。

因为输出层共享权重参数,所以:

uc,j=uj=hvwjfor c=1,2,,Cu_{c,j} = u_j = h \cdot v^{\prime}_{w_j} \quad for \ c = 1,2,\cdots, C

反向传播

损失函数为:

L=logp(WO,1,WO,2,,WO,CWI)=log(c=1Cp(WO,cWI))=c=1Clog(j=1Vexp(uj))c=1Cuj,c=Clog(j=1Vexp(uj))c=1Cuj,c\begin{aligned} L & = -\log{ p( W_{O,1},W_{O,2},\cdots,W_{O,C} | W_I) } \\ & = -\log(\coprod_{c=1}^C p(W_{O,c}| W_I) ) \\ & = \sum_{c=1}^C \log( \sum_{j'=1}^{V}exp(u_{j'} ) ) - \sum_{c=1}^Cu_{j*,c} \\ & = C\log( \sum_{j'=1}^{V}exp(u_{j'} ) ) - \sum_{c=1}^Cu_{j*,c} \end{aligned}

在求导、求梯度过程中,其实就是在CBOW模型参数更新过程中写过的内容和公式, 只不过Skip-gram需要求多个上下文词的导数、梯度,但是原理是一样的,公式稍稍有些不同。

损失函数LLuc,ju_{c,j}求导:

Luc,j=yc,j tc,j=ec,j\frac{\partial L}{\partial u_{c,j} } = y_{c,j} \ - t_{c,j} = e_{c,j}

简单起见,定义一个V维的向量EI=[EI1EI2EIV]EI=\begin{bmatrix}EI_1 & EI_2 & \cdots & EI_V\end{bmatrix},表示所有上下文单词的预测误差。

EIj=c=1Cec,jEI_j = \sum_{c=1}^{C} e_{c,j}

那么从隐含层到输出层的权重WW'求导:

Lwij=c=1CLuijuc,jwij=EIjhi\frac{L}{w'_{ij} } = \sum_{c=1}^C \frac{L}{u_{ij} } \cdot \frac{u_{c,j} }{w'_{ij} } = EI_j \cdot h_i

则更新公式为:

wij(new)=wij(old)αEIjhiw'^{(new)}_{ij} = w'^{(old)}_{ij} - \alpha\cdot EI_j\cdot h_i

或者:

wj(new)=wj(old)αEIjhw'^{(new)}_{j} = w'^{(old)}_{j} - \alpha\cdot EI_j\cdot h

接着计算输入层到隐层的参数更新公式,同样定义一个N维的EHEH向量,有:

EHi=j=1VEIjwijEH_i=\sum_{j=1}^V EI_j \cdot w'_{ij}

则更新公式为:

vwI(new)=vwI(old)αEHiTv^{(new)}_{w_I} = v^{(old)}_{w_I} - \alpha \cdot EH_i^T

现在来回答之前的问题,也就是由于权重矩阵WW'是共享的,所以每个上下文词的输出向量都是相同的,那么如何更新参数?

这是因为不同的上下文词对应了uu的不同的索引,因此通过反向传播,评估真实上下文词和预测值之间的误差,来更新相应的WW'的列,从而减少误差。

也就是说,虽然每个上下文词的输出是相同的,但是不同的上下文词代表了权重矩阵WW'的不同列,因此每个上下文词的输出会更新WW'的对应列。

参考资料

GloVe

GloVe是词嵌入的另一种训练方法,全称为Global Vectors for Word Representation,它是一个基于全局词频统计的词表示工具。

GloVe的实现分为以下几步:

1. 构建共现矩阵

共现矩阵(Co-ocurrence Matrix),顾名思义,就是共同出现的意思。

根据语料库构建共现矩阵。

矩阵中的每一个元素XijX_{ij},代表上下文单词jj在单词ii的上下文窗口中出现的次数。

例如,语料库为:I love you but you love him I am sad,共有7个单词:

iloveyoubuthimamsad

如果设定上下文窗口为5(也就是左右词各取两个),那么就有以下窗口内容:

窗口标号中心词窗口内容
0ii love you
1lovei love you but
2youi love you but you
3butlove you but you love
4youyou but you love him
5lovebut you love him i
6himyou love him i am
7ilove him i am sad
8amhim i am sad
9sadi am sad

以窗95为例,说明如何构建共现矩阵:

中心词为sad,上下文词为i,am,则:Xsad,i+=1,Xsad,am+=1X_{sad,i} += 1,X_{sad,am} += 1

那么共现矩阵为:

image-20221009151334853

共现矩阵每一个元素XijX_{ij}代表单词ii和上下文单词jj在特定大小的上下文窗口内出现的次数,一般而言这个次数最小为1,但是GloVe并不这样认为,它根据两个单词在上下文窗口的距离d,提出了一个衰减函数:decay=1ddecay = \frac{1}{d},用于计算权重,也就是说距离越远的单词所占总计数的权重越小。

2. 词向量和共现矩阵的近似关系

构建词向量(Word vector)和共现矩阵之间的近似关系:

wiTw~j+bi+b~j=log(Xij)w^T_i \tilde{w}_j + b_i + \tilde{b}_j = \log(X_{ij})

其中wiTw^T_iw~j\tilde{w}_j是最终要求解的两个词向量,bjb_jb~j\tilde{b}_j分别是两个词向量的偏置。

为了说明这个公式怎么来的,先定义一些变量:

  • XijX_{ij}:表示单词jj在单词ii的上下文中出现的次数
  • XiX_i:表示单词ii的上下文中所有单词出现的总次数,Xi=kXikX_i = \sum^{k}X_{ik}
  • Pij=P(ji)=XijXi\large P_{ij} = P(j|i) = \frac{ X_{ij} }{X_i}, 即表示单词jj出现在单词ii上下文中的频率

有了这些定义后,来看一个表格:

image-20221009153532838

可以看到,对于单词icesteam,不同的单词kk有不同的频率。

表格最后一行是最重要的,它是两个频率之比,通过这个比值,可以观察出单词iijj相对于单词kk那个更相关,例如第一列,P(solidice)/P(solidsteam)=8.9P(solid|ice)/P(solid|steam) = 8.9远远大于1,所以solidice相比于solidsteam来说,有更强的相关性;而对于第二列,P(gasice)/P(gassteam)=8.5×102P(gas|ice)/P(gas|steam) = 8.5\times 10^{-2},远远小于1,所以gassteam有更强的相关性。

而对于第三列和第四列,P(waterice)P(water|ice)P(watersteam)P(water|steam)的值都较大,说明icesteam都和water具有较强的相关性,并且两个频率比值接近1;相反,P(fashionice)P(fashion|ice)P(fashionsteam)P(fashion|steam)的值都较小,所以icesteam都和单词fashion不具有什么相关性,但是频率比值也接近1。

因此可以得出结论:

如果两个单词i,ji,j都与单词kk有较强的相关性或者较差相关性,那么两个概率比值接近1,

而如果i,ji,j其中一个单词具有较强相关性,另一个相关性较差,则概率比值会远小于1或远大于1。


综上,可以说明:通过概率的比例而不是概率本身来学习词向量是更恰当的方法。因此下文所以内容都围绕这一点来展开,为了捕捉上面提到的概率比例,可以构造以下函数:

F(wi,wj,w~k)=PikPjkF(w_i,w_j,\tilde{w}_k) = \frac{ P_{ik} }{ P_{jk} }

函数FF的参数和具体形式未定,有三个参数wi,wj,w~kw_i,w_j,\tilde{w}_kwww~\tilde{w}是不同的向量。

因为向量空间是线性结构的(可能是因为向量能够加减?),所以要表达出两个概率比例差,最简单办法是作差,因此得到:

F(wiwj,w~k)=PikPjkF(w_i - w_j, \tilde{w}_k) =\frac{ P_{ik} }{ P_{jk} }

可以发现,上式的右边是一个标量,而左边参数是一个向量表示,为了保证FF是线性结构,把左侧转换成两个向量内积:

F((wiwj)Tw~k)=PikPjkF( (w_i - w_j)^T \tilde{w}_k) =\frac{ P_{ik} }{ P_{jk} }

接下来就到了较难理解的部分。

注意到wiw_iwjw_j的距离与wjw_jwiw_i的距离是相等的,并且共现矩阵是一个对称的矩阵,即XT==XX^T == X,因此如果进行如下交换:www~k\tilde{w}_k做交换,XXXTX^T做交换,公式应该保持不变,但是:

F((wiwj)Tw~k)=PikPjkPkiPkjF( (w_i - w_j)^T \tilde{w}_k) =\frac{ P_{ik} }{ P_{jk} } \ne \frac{ P_{ki} }{ P_{kj} }

因此需要修改。

FF中参数拆开:

F((wiwj)Tw~k)=F(wiw~kwjw~k)F( (w_i - w_j)^T \tilde{w}_k) = F(w_i\tilde{w}_k - w_j\tilde{w}_k )

GloVe论文中是这样做的:

F((wiwj)Tw~k)=F(wiw~kwjw~k)=F(wiTw~k)F(wjTw~k)F( (w_i - w_j)^T \tilde{w}_k) = F(w_i\tilde{w}_k - w_j\tilde{w}_k ) = \frac{F(w_i^T\tilde{w}_k) }{F(w_j^T\tilde{w}_k)}

这时候就可以满足:

F(wiTw~k)F(wjTw~k)=F(wkTw~i)F(wkTw~j)\frac{F(w_i^T\tilde{w}_k) }{F(w_j^T\tilde{w}_k)} = \frac{F(w_k^T\tilde{w}_i) }{F(w_k^T\tilde{w}_j)}

exe^x函数具有这样的特性,又有:

F(wiTw~k)=Pik=XikXiF(w_i^T \tilde{w}_k) = P_{ik} = \frac{ X_{ik} }{ X_i }

因此有:

wiTw~k=log(Pik)=log(Xik)log(Xi)(a)w_i^T \tilde{w}_k = \log(P_{ik}) = \log(X_{ik} ) - \log(X_i ) \tag{a}

所以有:

F(wiw~kwjw~k)=ewiw~kwjw~k=elogPikelogPjk=PikPjkF(w_i\tilde{w}_k - w_j\tilde{w}_k ) = \large e^{ w_i\tilde{w}_k - w_j\tilde{w}_k } = \frac{e^{\log{ P_{ik} } } }{ e^{\log{ P_{jk} } } } = \frac{ P_{ik} }{ P_{jk} }

此时可以发现,公式(a)由于有右侧log(Xi)\log(X_i)的存在,上面式子是不满足对称性的,并且log(Xi)\log(X_i)kk没有关系,只跟kk有关系,于是可以针对wiw_i增加一个偏置bib_i把它替换掉,于是有:

wiTw~k+bi=log(Xik)w_i^T \tilde{w}_k +b_i = \log(X_{ik} )

但是上式还是不满足对称性,因此针对w~k\tilde{w}_k再增加一个偏置项bkb_k,最终得到:

wiTw~k+bi+bk=log(Xik)w_i^T \tilde{w}_k +b_i + b_k = \log(X_{ik} )

3. 构造损失函数

有了上面公式后就可以构造损失函数:

J=i,j=1Vf(Xij)(wiTw~j+bi+b~jlogXij)2J = \sum_{i,j=1}^V f( X_{ij} )(w_i^T \tilde{w}_j +b_i + \tilde{b}_j - \log{X_{ij} })^2

这个损失函数的基本形式就是最简单的平方差损失,只不过在此基础上增加了一个权重函数f(Xij)f(X_{ij})

下面来解释为什么要加入这个权重函数:

在一个语料库中,肯定存在很多单词,它们在一起的次数是很多的,因此希望:

  • 这些单词的权重要大于那些很少在一起出现的单词,所以这个函数需要是非递减函数
  • 同时不希望这个权重过大,因此当达到一定程度之后应该不再增加
  • 如果两个单词没有出现在一起,即Xij=0X_{ij} = 0,那么不应该参与到损失函数计算中,即f(x)f(x)要满足f(0)=0f(0) = 0

满足上面条件的函数有很多,论文作者采用了如下的分段函数:

f(x)={(xxmax)αifx<xmax1ifxxmax\large f(x) = \begin{cases} ( \frac{x}{x_{max} } ) ^{\alpha} \quad& if \quad x \lt x_{max} \\ 1 \quad & if \quad x \ge x_{max} \end{cases}

其中xmaxx_{max}α\alpha都是可调整的超参数,论文作者采用的值为xmax=100,α=0.75x_{max} = 100,\alpha = 0.75

image-20221009162554419

序列模型和注意力机制

Seq2seq模型(sequence to sequence)模型,从机器翻译到语音识别,都发挥着很大的作用。

例如,输入一个法语句子:Jane visite I'Afrique en septembre.,翻译成英语:Jane is visiting Africa in September.

和之前一样,使用x<1>,,x<5>x^{<1>},\cdots,x^{<5>}来表示输入的单词,y<1>,,y<6>y^{<1>},\cdots,y^{<6>}表示输出的单词,那么如何建立一个网络来输入序列xx和序列yy呢。

首先建立一个网络,叫做编码网络(encoder network),它是一个RNN结构,RNN单元可以是GRU也可以是LSTM。每次只向网络中输入一个法语单词,输入序列接收完成后,这个编码网络会输出一个向量表示这个输入序列。

之后建立一个解码网络(decoder network),以编码网络的输出作为输入,之后可以被训练为每次输出一个翻译后的单词,一直到它输出序列的结尾。

image-20221010085410501

在给出足够的法语和英语文本情况下,训练该模型,这个模型使用一个编码网络对输入的法语句子进行编码,然后用一个解码网络来生成对应英语翻译。

还有一个类似的结构用做图像描述,或者叫做 看图说话。

也就是输入一张图片,自动地输出该图片的描述。

在之前已经学习了如何将图片输入到卷积神经网络中,比如一个预训练的AlexNet,去掉AlexNet最后的softmax单元,把这个向量输入到RNN中,RNN生成图像的描述。

image-20221010085507402

选择最可能的句子

在seq2seq模型中,需要输出最准确的句子,例如在机器翻译中,可能有多个候选的翻译句子,模型需要输出最准确的,而不是一个随机选取的翻译。

例如想通过模型将法语翻译成英文,通过输入的法语句子,模型会告诉你各种英文翻译的可能性,xx在这里是法语句子Jane visite l'Afrique en septembre.,模型会输出不同英语翻译所对应的翻译。

可能会得到两种翻译:

Jane is visiting Africa in September.

Jane is going to be visiting Africa in September.

显然第一句话更好,更简洁。

因此当使用机器翻译模型时,并不是从得到的分布中随机采样,而是找到一个英语句子yy,使得条件概率最大化。而解决这种问题,通用的算法是集束搜索(Beam Search)。

集束搜索

在生成文本时,需要解码操作,也就是查找最好的文本序列组合。贪心搜索是比较简单的一种搜索算法。

例如要把句子I love you翻译成我爱你,则贪心搜索的过程是:

image-20221011085018104

贪心搜索在每一时刻选择当前最有可能的单词,例如预测第一个单词时,的概率最大,则第一个单词预测为,预测第二个单词时,的概率最大,则第二个单词为,以此类推,贪心搜索有较高的运行效率,但每一步均考虑局部最优,有时候不能得到全局最优解。

Bean Search集束搜索对贪心搜索进行了改进,扩大了搜索空间,更容易得到全局最优解。

集束搜索有一个参数beam-size,表示每一时刻保留概率最高的beam-size个序列,然后下一时刻用这k个词继续生成。

还以I love you翻译成我爱你为例,beam-size = 2,集束搜索过程为:

image-20221011090033050

在第一个时刻,的预测分数最高,由于bean-size = 2,会保留两个概率最高词,因此Beam Search会保留,以这两个词为基础,预测第二个词,在第二个时刻,我爱你爱两个序列概率最高,因此保留这两个,第三个时刻,得到两个概率最高序列为我爱你你爱我,选择概率最高的,得到我爱你

beam-size = 1,实际就变成了贪心算法。

改进集束搜索

前面讲到的集束搜索,就是最大化下面这个概率式子:

P(y<1>X)P(y<2>X,y<1>)P(y<Ty>X,y<1>,y<2>y<Ty1>)P(y^{<1>}|X)P(y^{<2>}|X,y^{<1>})\cdots P(y^{<T_y>}|X,y^{<1>},y^{<2>} y^{<T_y - 1>})

找到使得这个概率式子值最大的那个yy,可以简化为:

argmaxyt=1TyP(y<t>x,y<1>,,y<t1>)(a)\arg \max_y \prod_{t=1}^{T_y} P(y^{<t>}|x, y^{<1>}, \cdots, y^{<t-1>}) \tag{a}

这个式子就是找到使得概率最大的那个序列yy

但是这些概率都是远小于1的,因此概率的乘积会得到很小很小的数字,会造成数值下溢,数值溢出就是数值太小,导致电脑浮点表示无法精确存储。

因此在实践中,不会最大化这个乘积,而是取log\log值,因为log\log是严格单调递增的函数,所以最大化logP(yx)\log{P(y|x)},就是最大化P(yx)P(y|x)

log\log会将概率乘积变成概率的log\log值相加。

因此得到:

argmaxyy=1TylogP(y<t>x,y<1>,,y<t1>)(b)\arg \max_{y}\sum_{y=1}^{T_y} \log{ P(y^{<t>}|x, y^{<1>}, \cdots, y^{<t-1>}) } \tag{b}

这样可以使数值更稳定,不容易出现舍入误差或数值下溢。


如果看公式(a),会发现,如果有一个很长的句子,那么TyT_y会很大,相当于要乘上很多小于1的概率值,因此会得到一个更小的概率值,所以这个目标函数有一个缺点,就是更偏向短的输出,即倾向于简短的翻译结果,因为短句子是由更少数量的概率乘积得到的。

同样的,公式(b)也有相同问题,概率都在0到1之间所以每个概率的log\log值都是小于0的,句子越长,加起来的负数越多,得到的结果越负。

因此可以进行(Length normalization)长度归一化,使得算法表现更好,即除以翻译结果的单词数量,明显地减少对输出长结果的惩罚,公式为:

1Tyy=1TylogP(y<t>x,y<1>,,y<t1>)(c)\frac{1}{T_y} \sum_{y=1}^{T_y} \log{ P(y^{<t>}|x, y^{<1>}, \cdots, y^{<t-1>}) } \tag{c}

在实践中,相比于直接除TyT_y,有时会用一个更柔和的方法,在TyT_y上加上指数α\alpha

1Tyαy=1TylogP(y<t>x,y<1>,,y<t1>)(d)\frac{1}{T_y^{ \alpha} } \sum_{y=1}^{T_y} \log{ P(y^{<t>}|x, y^{<1>}, \cdots, y^{<t-1>}) } \tag{d}

α\alpha可以取0.7。

如果α=1\alpha = 1,那么就相当于完全用长度归一化;

如果α=0\alpha = 0,就相当于完全没有归一化。

α\alpha就是另一个超参数,需要调整大小来得到最好的结果。


总结一下如何运行集束搜索算法,当运行集束搜索时,例如输出长度为30,束宽(beam size)为3,那么需要记录所有这些可能的句子,长度为1、2、3、4一直到30的三个最可能的选择,然后针对这些句子,使用公式(d)进行打分,得到概率最大的几个句子,然后对这些集束搜索得到的句子,计算目标函数,最后从经过评估的这些句子中,挑选出在归一化的log\log概率目标函数中得分最高的一个,有时这个目标函数也叫作**归一化的对数似然目标函数。**这就是最终输出的预测结果。


还有一些实现的细节,如何选择beam size

beam size越大,考虑的选择越多,最后得到的句子就越准确,但是beam size越大,计算成本越高,因为需要把很多可能结果保存起来。

在实际中,经常会把beam size设为10,但是对于科研来说,可能想压榨出全部性能,得到一个最好结果发论文,因此会把beam-size设的比较大,例如1000。

这也取决于特定的应用和领域。

从束宽1到10,搜索结果会有一个很大的改善,但是从1000增大到3000,效果就没那么明显了。


最后说明一下集束搜索和BFS、DFS搜索算法的关系。

集束搜索运行的更快,但是不能保证一定能够找到最准确的最大值,而BFS和DFS都是精确的搜索算法。

注意力模型

深度学习里的注意力模型模拟的是人脑的注意力模型,例如当观赏一幅画时,虽然我们可以看到整幅画的全貌,但是在深入仔细观察时,眼睛聚焦的只有很小的一块,这个时候人脑对整幅图的关注并不是均衡的,有一定的权重区分的。

这就是注意力模型的核心思想。

在NLP领域,注意力模型首先引入神经网络机器翻译中(NMT),NMT任务是一个典型的seq2seq任务。

在上面已经介绍了seq2seq模型的基本结构。

image-20221011102825834

具体来看:

image-20221011103803895

其中左边叫做encoder,右边是decoder

Encoder部分RNN在序列结束时输出一个语义向量C,该向量可以看成拥有输入序列的全部上下文语义信息,

对于解码器Decoder来说,任务时根据句子的语义C和之前已经生成的历史信息y<1>,y<2>,,y<t1>y^{<1>},y^{<2>},\cdots, y^{<t-1>}来生成t时刻的单词y<t>y^{<t>}

y<t>=g(C,y<1>,y<2>,,y<t1>)y^{<t>} = g(C,y^{<1>},y^{<2>},\cdots,y^{<t-1>})

但是,在Encoder-Decoder框架中,在预测每一个y<t>y^{<t>}时语义编码C都是相同的,这意味着输入句子X的每个单词对输出Y中的每一个单词影响都是相同的。

这显示和我们生活常识不同,当我们翻译一句话某个单词时,跟它相邻词的参考价值,要大于远离该单词的词。

因此为了解决这一问题,引入了注意力模型,通过参数,来控制每一个词在语义向量中的权重。

让生成词不是只能关注语义编码向量C,而是增加了一个“注意力范围”,表示接下来的输出词要重点关注输入序列中的哪些部分,然后根据关注的区域来生成下一个输出。

大致模型结构如下:

image-20221011104402612

对于每一个输出词,都有一个不同的语义向量。

具体来看某一个输出词的预测过程:

image-20221011110140373

下半部分是Encoder结果,采用双向RNN构成,将同一个时刻的两个RNN单元的隐状态做拼接形成最终隐状态输出:

a<t>=[a<t>,a<t>]a^{ <t>} = [\quad \overrightarrow{a}^{ <t> },\overleftarrow{ a }^{ <t> } ]

使用符号SS来表示Decoder每个单元的隐藏值,便于区分。

每一个输出词y<t>y^{<t>},对应一个语义编码C<t>C^{<t>},这个语义编码是由每一个输入单元的激活值和对应权重得到的。

C<t>=j=1Txα<t,j>a<t>C^{<t>} = \sum_{j = 1}^{T_x} \alpha^{<t,j>} a^{<t>}

其中权重的计算方法为:

α<t,j>=exp(e<t,j>)k=1Txexp(e<t,k>)\alpha^{<t,j>} = \frac{exp(e^{<t,j>} ) }{\sum_{k=1}^{T_x} exp(e^{<t,k>} ) }

使用了softmax函数,来确保所有权重加起来为1。

其中e<t,i>e^{<t,i>}的计算方式为:

e<t,j>=f(s<t1>,a<j>)e^{<t,j>} = f(s^{<t-1>}, a^{<j>})

其中s<t1>s^{<t-1>}Decoder上一个时刻的隐状态,a<j>a^{<j>}Encoder是第j个时刻的隐状态输出。

ff是一个网络结构,可以看成一个小神经网络。

image-20221011111900202

但是不知道这个具体函数是什么,所以可以训练一个很小的神经网络,来学习这个函数,相信反向传播、梯度下降算法,可以学到一个正确的函数。


简单来解释就是,α<t,j>\alpha^{<t,j>}权重表示,在输出单词y<t>y^{<t>}的情况下,需要在输入单词x<j>x^{<j>}上花费的注意力数量。

而这个权重α<t,j>\alpha^{<t,j>}是根据Decoder上一个时间步的隐藏值S<t1>S^{<t-1>}Encoder中第j个时间步,也就是第j个单词对应的隐藏值a<j>a^{<j>}


如果有TxT_x个输入词和TyT_y个输出词,那么注意力参数总数为Tx×TyT_x \times T_y。所以这个算法的一个缺点是算法时间负责度是O(n3)O(n^3),所以在机器翻译应用中,输入和输出的句子一般不会太长。


11-序列模型
https://zhaoquaner.github.io/2022/10/11/DeepLearning/CourseNote/11-序列模型/
更新于
2022年10月11日
许可协议