- 参考:
- 李宏毅2021/2022春机器学习课程
- 王树森 RNN & Transformer 教程
文章目录
- 0. 背景:序列数据及相关任务
- 1. 早期序列模型
- 1.1 循环神经网络 RNN
- 1.2 长短期记忆网络 LSTM
- 1.3 改善 RNN/LSTM 的三个技巧
- 1.3.1 通过堆叠扩展为深度模型
- 1.3.2 使用双向模型避免遗忘
- 1.3.3 使用预训练模型
- 1.4 传统序列模型任务
- 1.4.1 自动文本生成(Autoregress 结构)
- 1.4.2 机器翻译(Encoder-Decoder 结构)
- 2. 注意力机制
- 2.1 注意力机制 (Attention)
- 2.2 自注意力机制 (Self-Attention)
- 3. Attention + Encoder-Decoder
- 3.1 Transformer
- 3.2 GPT
- 3.3 BERT
0. 背景:序列数据及相关任务
序列数据
是由一组相互关联的样本组成的数据,其中任意样本对应的标记是由其自身和其他样本共同决定的;序列数据任务
是输入或输出为序列数据的机器学习任务,用传统机器学习模型处理他们是困难的,比如 序列模型(1)—— 难处理的序列数据 中第 3 节的例子- 传统方法的局限性在于其问题建模,这些模型不是针对可变长度的输入输出设计的,无法体现序列数据的特点,具体而言
- 传统的 MLP、CNN 这类模型都是
one-to-one
模型,即一个输入一个输出。这种模型会把序列数据作为一个整体来考虑,其输入输出的尺寸必须是固定的,比如输入一张固定尺寸的图像输出其类别,或者输入一段固定长度的句子预测下一个词Note:借助一些手段,可以强行用传统模型处理变长的输入输出问题,如 各种监督学习范式(强监督、半监督、多标记、偏标记、多示例、多示例多标记、标记分布…) 中的多示例多标记问题,但是这些模型本质上还是要将输入输出处理成固定的长度(比如用固定长度的 0/1 向量选出不同数量的样本作为输入输出)。至于样本间的关系,虽然可以通过网络自己学出来,但由于缺乏考虑这种关系的显式结构,所以很难学得好(可以参考 从模型容量的视角看监督学习)
- 一个良好的序列模型应该是
many-to-many
模型,即支持可变长度的输入输出,并且最好能对序列样本间的关系进行显式建模,注意这样的模型也可以直接用来处理one-to-many
,many-to-one
甚至one-to-one
问题。语音识别、本文情感分析、序列预测等等序列任务都能被这种模型更好地处理RNN 就是一种良好的序列模型,下图给出了各类输入输出情况下的 RNN 结构
- 传统的 MLP、CNN 这类模型都是
1. 早期序列模型
- RNN 和 LSTM 等早期序列模型模仿人类处理序列数据的过程,人类阅读文本时每次看一个词,逐渐在大脑中积累文本信息,这些模型也是如此,其内部有一个隐状态代表目前积累的信息,每次读入一个序列样本就将其更新,如下图所示
注意这里的每一列都是不同时刻下的同一个模型,可见,模型的每个隐状态都表示 “当前位置之前的 ‘已见序列片段’ 的特征”,具体应用时- 对于 “文本情感分析” 这类
传统监督学习任务
(many-to-one),常将序列模型作为 “特征提取器”,只使用最后一个隐状态 h t \pmb{h}_t hhht 作为整个序列的特征向量,用它接一个分类头或回归头作为整个模型的输出。在训练时,只要像图像分类等普通监督学习任务一样训练即可 - 对于 “文本生成” 这类
标准语言模型任务
(one-to-many),即不断根据之前序列样本预测下一个样本值的任务,通常会如下图所示做 Autoregress,这时我们会增加一个分类头或回归头将隐状态 h \pmb{h} hhh 变换为输出 x \pmb{x} xxx,推断时不断地将上一步模型输出合并到下一步模型的输入中。在训练时,会构造很多以连续的 n 个样本作为输入,紧接着第 n+1 个样本作为标签的自监督样例,详见下文 1.4.1 节 - 对于 “文本翻译” 这类
Seq2Seq 任务
(many-to-many),通常使用 Encoder-Decoder 结构。这时 Encoder 就是类似 1 中的 many-to-one 序列特征提取器,Decoder 就是类似 2 中的 one-to-many 序列生成器,Encoder 提取的特征作为 Decoder 的初始 seed,二者结合就能做 many-to-many 了。训练时通常用 teacher-forcing 形式,详见下文 1.4.2 节
- 对于 “文本情感分析” 这类
1.1 循环神经网络 RNN
- RNN 是实现第 1 节中 “信息积累” 概念的最简单模型,其结构图如下所示
具体而言,设隐藏层激活函数为 ϕ \phi ϕ, t t t 时刻输入批量大小为 n n n 样本维度为 d d d 的小批量样本 X t ∈ R n × d \pmb{X}_t\in \mathbb{R}^{n\times d} XXXt∈Rn×d,设隐藏变量维度为 h h h,前一个时刻的小批量隐层变量为 H t − 1 ∈ R n × h \pmb{H}_{t-1}\in\mathbb{R}^{n\times h} HHHt−1∈Rn×h。引入模型的权重参数 W x h ∈ R d × h , W h h ∈ R h × h \pmb{W}_{xh}\in \mathbb{R}^{d\times h}, \pmb{W}_{hh}\in \mathbb{R}^{h\times h} WWWxh∈Rd×h,WWWhh∈Rh×h 和偏置参数 b h ∈ R 1 × h \pmb{b}_h\in \mathbb{R}^{1\times h} bbbh∈R1×h,则该步的隐藏变量 H t ∈ R n × h \pmb{H}_t\in\mathbb{R}^{n\times h} HHHt∈Rn×h 可以如下计算
H t = ϕ ( X t W x h + H t − 1 W h h + b h ) = ϕ ( [ X t H t − 1 ] [ W x h W h h ] + b h ) \begin{aligned} \pmb{H}_t &= \phi(\pmb{X}_t\pmb{W}_{xh}+\pmb{H}_{t-1}\pmb{W}_{hh}+\pmb{b}_h) \\ &= \phi\left({\left[ \begin{array}{ccc} \pmb{X}_t & \pmb{H}_{t-1} \end{array} \right ]} {\left[ \begin{array}{ccc} \pmb{W}_{xh}\\ \pmb{W}_{hh} \end{array} \right ]}+\pmb{b}_h\right) \\ \end{aligned} HHHt=ϕ(XXXtWWWxh+HHHt−1WWWhh+bbbh)=ϕ([XXXtHHHt−1][WWWxhWWWhh]+bbbh) 再设输出维度为 m m m,用于输出的 FC 层参数为 W h m ∈ R h × m , b m ∈ R 1 × m \pmb{W}_{hm}\in\mathbb{R}^{h\times m}, \pmb{b}_m\in\mathbb{R}^{1\times m} WWWhm∈Rh×m,bbbm∈R1×m,该步的输出变量 O t ∈ R n × m \pmb{O}_t\in\mathbb{R}^{n\times m} OOOt∈Rn×m 可以如下计算
O t = H t W h m + b m \pmb{O}_t = \pmb{H}_t \pmb{W}_{hm} + \pmb{b}_m OOOt=HHHtWWWhm+bbbm 注意在不同的时间步使用的都是相同的模型参数 W , b \pmb{W},\pmb{b} WWW,bbb,因此 RNN 的参数开销不会随着时间步的增加而增加
1.2 长短期记忆网络 LSTM
-
LSTM 是对 RNN 模型的改进,可以有效缓解 RNN 的梯度消失(见下文 1.3.2 节)问题,其结构如下所示
这个结构看上去很复杂,不过我们可以从先从宏观角度来理解:相比 RNN,LSTM 针对序列数据性质增加了对数据处理过程的限制,从而减少了模型的弹性/容量,使它能在使得相同样本量下更好地提取序列信息,这和 CNN 比 MLP 能更好地处理图像数据是一个道理 -
现在来仔细看一下这个结构,LSTM 的设计灵感来自于计算机的逻辑门,它的输出和 RNN 一样仍然是隐状态 H \mathbf{H} H,只是生成过程更复杂。相比 RNN,LSTM 引入了
记忆单元cell
C \mathbf{C} C,它和隐状态 H \mathbf{H} H 具有相同的形状,用于记录附加的信息并产生输出 H \mathbf{H} H,其他的所有门都是为了控制这个记忆单元服务的。具体而言,设隐藏层维度为 h h h,batch size 为 n n n,样本维度为 d d d,则批量输入为 X t ∈ R n × d \mathbf{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d,前一时刻隐状态为 H t − 1 ∈ R n × h \mathbf{H}_{t-1} \in \mathbb{R}^{n \times h} Ht−1∈Rn×h- 候选记忆 C ~ \pmb{\tilde{C}} C~C~C~ 和 RNN 中的隐状态 H \mathbf{H} H 完全一致,只是指定了使用激活函数 ϕ = tanh \phi=\tanh ϕ=tanh,将值压倒 ( − 1 , 1 ) (-1,1) (−1,1)
- 遗忘门 F t ∈ R n × h \mathbf{F}_t \in \mathbb{R}^{n \times h} Ft∈Rn×h 用来控制要保留记忆单元 C \mathbf{C} C 中的过去信息的程度/比例,用 sigmoid 激活的 FC 层压倒 (0,1)
- 输入门 I t ∈ R n × h \mathbf{I}_t \in \mathbb{R}^{n \times h} It∈Rn×h 用来控制候选记忆 C ~ \mathbf{\tilde{C}} C~ 被合并到记忆单元的程度/比例,用 sigmoid 激活的 FC 层压倒 (0,1)
- 输出门 O t ∈ R n × h \mathbf{O}_t \in \mathbb{R}^{n \times h} Ot∈Rn×h 用来控制记忆单元 C \mathbf{C} C 中作为输出 H \pmb{H} HHH 的程度/比例,用 sigmoid 激活的 FC 层压倒 (0,1)
列一下公式,有
C ~ t = tanh ( X t W x c + H t − 1 W h c + b c ) , I t = σ ( X t W x i + H t − 1 W h i + b i ) , F t = σ ( X t W x f + H t − 1 W h f + b f ) , O t = σ ( X t W x o + H t − 1 W h o + b o ) , \begin{aligned} \tilde{\mathbf{C}}_t &= \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c), \\ \mathbf{I}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xi} + \mathbf{H}_{t-1} \mathbf{W}_{hi} + \mathbf{b}_i),\\ \mathbf{F}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xf} + \mathbf{H}_{t-1} \mathbf{W}_{hf} + \mathbf{b}_f),\\ \mathbf{O}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xo} + \mathbf{H}_{t-1} \mathbf{W}_{ho} + \mathbf{b}_o), \end{aligned} C~tItFtOt=tanh(XtWxc+Ht−1Whc+bc),=σ(XtWxi+Ht−1Whi+bi),=σ(XtWxf+Ht−1Whf+bf),=σ(XtWxo+Ht−1Who+bo), 这里的 W , b \pmb{W},\pmb{b} WWW,bbb 都是要学习的参数,相比 RNN 多了三组。另外,所有信息选取都是通过对应位置乘以 (0,1) 间小数的方式进行的,如图可见有 “遗忘门 F \mathbf{F} F 去除部分老记忆 C t − 1 \mathbf{C}_{t-1} Ct−1”、“输入门 I \mathbf{I} I 合并部分候选记忆 C ~ \mathbf{\tilde{C}} C~” 和 “输出门 O t \mathbf{O}_t Ot 保留部分新记忆 C t \mathbf{C}_t Ct” 三处,公式表示为
C t = F t ⊙ C t − 1 + I t ⊙ C ~ t H t = O t ⊙ tanh ( C t ) . \begin{aligned} \mathbf{C}_t &= \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t\\ \mathbf{H}_t &= \mathbf{O}_t \odot \tanh(\mathbf{C}_t). \end{aligned} CtHt=Ft⊙Ct−1+It⊙C~t=Ot⊙tanh(Ct). -
直观地看这个结构,其实就是每次 batch 输入先产生和 RNN 中 H \mathbf{H} H 完全一样的 C ~ t \mathbf{\tilde{C}}_t C~t 作为 “当前步记忆”,然后用遗忘和输入门控制它和 “历史序列记忆” C t − 1 \mathbf{C}_{t-1} Ct−1 混合得到 “最新序列记忆” C t \mathbf{C}_{t} Ct,最后使用输出门将 C t \mathbf{C}_{t} Ct 衰减后输出为 H t \mathbf{H}_t Ht
- 如果遗忘门始终为 1 且输入门始终为 0, 则过去的记忆元 C t − 1 \mathbf{C}_{t-1} Ct−1 将随时间被保存并传递到当前时间步,缓解梯度消失问题,更好地捕获序列中的长距离依赖关系。这有点类似残差网络通过残差连接避免梯度消失的思想
- 只要输出门接近 1,我们就能够有效地将所有记忆信息传递给预测部分, 而对于输出门接近 0,我们只保留记忆元内的所有信息,而不需要更新隐状态
-
最后再回到宏观来看,LTSM 通过学习四组系数 & 偏置参数,要求模型按照上述逻辑构造记忆并从中提取输出,相比 RNN 直接一个 FC 加激活函数,它通过模型结构引导其做出更符合序列性质的行为,从而缓解了 RNN 的梯度消失和梯度爆炸问题,大幅提升了 RNN 的性能
1.3 改善 RNN/LSTM 的三个技巧
1.3.1 通过堆叠扩展为深度模型
- 如果模型只有一层,其容量是有限的,无法表示太复杂的映射。但是注意到第 1 节这种序列模型对每一个输入都可以有一个输出代表对当前时刻之前的序列内容的聚合,而所有这些输出可以组合成一个新的序列,这样一来我们其实可以再把这个新序列输入参数相同的模型,这样每个输出将会是对当前时刻之前的序列内容的进一步聚合,如此反复堆叠就得到了
Stacked RNN/Stacked LSTM
,其容量更大,能表示的映射关系也更加复杂,如下图所示
需要注意的是,对于更大容量的模型,需要提供更多的训练样本以避免过拟合
1.3.2 使用双向模型避免遗忘
-
无论 RNN 还是 LSTM,模型都只能利用隐藏状态间接地获取之前序列的信息,由于隐藏状态的维度一定远远小于之前的变长序列所有样本的连接维度,这种做法无可避免地会损失一些信息,体现在微观层面上就是两个经典问题
梯度消失
:参数更新梯度被近期样本主导(和 MLP 里的梯度消失不太一样)梯度爆炸
:参数更新梯度梯度趋近 ∞ \infin ∞
在 RNN 中这两个问题尤其严重(可以参考 RNN梯度消失和爆炸的原因),以梯度消失为例,近期的序列样本主导了优化方向,这会导致模型快速忘记相隔时间比较久的早期序列输入,比如下例
这个 RNN 的任务是根据先前序列预测下一个单词,在 x 1 x_1 x1 处输入了 “China”,但由于梯度消失这个信息几乎没法被记住,模型难以在相隔较远的 h t + 1 h_{t+1} ht+1 处给出 “Chinese” 的预测
这里模型大概能学到应该输出一个语言,但是具体是什么语言会被近期序列的倾向所主导LSTM 通过引入 “记忆单元” 缓解了这些问题,但依然无法完全解决。一个简单粗暴的优化方案是直接同时从两个方向训练 RNN 或 LSTM,这样得到的
Bidirectional RNN/Bidirectional LSTM
结构如下
这样一来,一个方向的早期样本就成了另一个方向的近期样本,可以缓解 RNN/LSTM 的遗忘问题。另外这里输出的 y y y 是两个模型输出的向量拼接,它也可以像上面那样进行 stack 从而扩展容量
1.3.3 使用预训练模型
-
处理序列数据时往往要做一步 embedding,使得序列样本变得可以处理。比如常见的文本模型,你没法直接向网络输入一个单词,这时有几个做法
- 直接给每个单词编一个数字,但是这种简单做法存在问题,比如 “car” 编码为 1,“cat” 编码为 2,“red” 编码为 3,那么你会发现 “car” + “cat” = “red”,这是不合理的
- 更合理一点的做法是把单词编码成 one-hot 向量,但是这样一来如果单词数量多的话,输入的维度就会特别大,导致处理困难
- 目前通常的做法是先编码成 one-hot,然后接一个 FC 层把它降维,而且我们希望降维之后的词向量能够体现语义上的远近关系,比如 “汽车” 和 “跑车” 应该在嵌入空间接近;“汽车” 和 “苹果” 应该在嵌入空间远离
-
一个问题是,FC 嵌入层往往很大,有时甚至比模型参数还多不少,如果直接把它接在 RNN 或 LSTM 的输入之前一起训练,很容易导致嵌入层过拟合,影响模型性能。这时我们可以先用一个别的任务专门训练这个 FC 嵌入层,这就是所谓的
预训练pre-train
过程,几个注意点是- 预训练任务必须要有对相同对象做嵌入的嵌入层,而且最好有大数据集
- 预训练任务可以是不同的任务,但是和目标任务越相关越好,因为学到的词向量会受到任务性质影响,越相关的任务,后期的调优过程越好做
- 预训练任务可以使用不同的模型,只要有 FC 嵌入层就行
完成预训练后,直接用预训练模型得到的 FC 嵌入层参数初始化目标模型的 FC 嵌入层,然后在目标任务训练过程中有时将其固定住只训练模型的其他部分,有时也带着这个 FC 层一起训练,这称为
微调fine-turn
过程 -
所有涉及预处理的部分都可以用类似思路进行预训练,可以有效提高模型性能
1.4 传统序列模型任务
- 虽然 RNN 和 LSTM 都能对每个输入给出一个输出,但通常还是会用最后一个样本对应的输出作为整个序列的特征用于具体任务,因此这些传统模型自身是倾向
many-to-one
形式的,但是通过巧妙的应用,其能处理的问题涵盖了one-to-many
,many-to-one
和one-to-one
所有情况,下面简单举两个例子进行说明
1.4.1 自动文本生成(Autoregress 结构)
- 这个任务只需要一大段文本作为材料,就能生成出类似风格的句子
- 训练阶段,使用自监督方式构造样本,每个样本都是以一段固定长度文本作为输入序列,其后的词或字符作为预测标记,直接做多分类监督学习即可。样本的具体构造方式可以是随机截取,也可以是用一个滑动窗口以一定的偏移量扫过输入文本材料。注意到这样的输入输出符合
many-to-one
的形式,另外由于输入长度必须固定,某种程度上也能看做one-to-one
形式换个角度看,此模型也可理解为先用序列模型提取前驱序列的特征(RNN/LSTM 视角下就是隐变量),再用这个特征做多分类来选择生成下一个 token,直接把这两件事放在一起进行 end-to-end 的训练
- 预测阶段,使用 Autoregress 的方式反复生成单词或字符,如下图所示
随便给定一个种子向量代表前驱序列特征,再给一个起始 token,模型就能输出后继 token 和新的隐状态,再将后继 token 作为输入就能继续生成,不断重复下去就能生成无限制长度的序列,这就是所谓的 “Autoregress”。总体上看属于one-to-many
形式
- 训练阶段,使用自监督方式构造样本,每个样本都是以一段固定长度文本作为输入序列,其后的词或字符作为预测标记,直接做多分类监督学习即可。样本的具体构造方式可以是随机截取,也可以是用一个滑动窗口以一定的偏移量扫过输入文本材料。注意到这样的输入输出符合
- 自动文本生成任务的意义在于,通过特定的结构安排,可以让只会做
to-one
任务的模型完成to-many
任务。监督学习模型基本都是 to-one 的,所以借助这个结构,我们甚至可以用 MLP 等八竿子打不着的模型来做文本生成,但由于这些网络缺乏对于序列任务的归纳偏置,提取序列特征的能力差,效果通常很差。可以参考 序列模型(1)—— 难处理的序列数据
1.4.2 机器翻译(Encoder-Decoder 结构)
-
机器翻译的输入和输出都是变长度序列,是典型的
many-to-many
问题,机器学习中也称这种任务为 “Seq2Seq” 任务。为了让 RNN/LSTM 有能力处理这类问题,模型要设计成特殊的 “编码器-解码器架构”。正如其名,这种架构含有两个组件编码器Encoder
:接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态,即从输入序列中提取一个特征向量。原始的 RNN/LSTM 可以完成这种many-to-one
的任务,但特征向量中不可避免地会损失长跨度样本的信息,这也是后面出现注意力机制的重要原因解码器Decoder
:将固定形状的编码状态映射到长度可变的序列。这恰好是上面 1.4.1 节那种one-to-many
任务,所以 Decoder 也可以看做使用 Encoder 编码特征作为初始种子特征向量的自动文本生成任务。为了能自动控制输出长度,通常要增加 “起始” 和 “终止” 两个特殊 token
-
机器翻译模型设计得很巧妙,通过连接
many-to-one
和one-to-many
的两个组件真正实现了many-to-many
任务-
训练阶段,Encoder 和 Decoder 连在一起作为一个 many-to-many 模型训练。以文本翻译任务为例,对于数据库中一个形如 ( 样 本 序 列 , 标 签 序 列 ) (样本序列, 标签序列) (样本序列,标签序列) 的样本,如下操作
- Encoder 输入完整的样本序列,将 “起始” token 输入 Decoder,预测目标序列第1个 token
p
1
\pmb{p}_1
ppp1,它和标签序列第1个真实 token
y
1
\pmb{y}_1
yyy1 计算损失
- Encoder 输入完整的样本序列,将 “起始” token 和标签序列第1个 token 输入 Decoder,预测目标序列第2个 token p 2 \pmb{p}_2 ppp2,它和标签序列第2个真实 token y 2 \pmb{y}_2 yyy2 计算损失
- 重复直到标签序列最后一个 token
每次计算的损失梯度会从 Decoder 一直回传到 Encoder,同时更新两个组件的参数
- Encoder 输入完整的样本序列,将 “起始” token 输入 Decoder,预测目标序列第1个 token
p
1
\pmb{p}_1
ppp1,它和标签序列第1个真实 token
y
1
\pmb{y}_1
yyy1 计算损失
-
测试阶段,Encoder 输入待翻译的句子,Decoder 输入 “起始” token,让它如 1.4.1 节一样自回归地生成序列,直到输出 “结束” token 未知
-
-
本节介绍的 “编码器-解码器结构” 是序列学习领域的一个重要模型,后面的 transformer 其实也是基于这个架构设计的
2. 注意力机制
2.1 注意力机制 (Attention)
-
正如上文 1.3.2 节的讨论,RNN 和 LSTM 这种传统序列模型对长跨度序列样本的特征提取能力有限,很容易出现梯度消失等问题,这个问题即使用双向模型也无法完全解决。Attention 是针对这个遗忘问题设计的,以机器翻译任务为例
可见当句子长度超过 20 词时,传统序列模型翻译的 BLEU 评分会迅速下降,增加 attention 机制后问题解决 -
Attention 机制最早提出于 ICLR 2015 的文章 Neural machine translation by jointly learning to align and translate,这篇文章基于 Encoder-Decoder 结构做机器翻译任务,它的思想很简单,Decoder 输出每个词时不要再只依赖一个隐状态特征向量 s \pmb{s} sss,而是去看完整地看一遍要翻译的原句,从原句的所有样本中提取信息汇聚成上下文向量 c \pmb{c} ccc 作为 Decoder 的附加输入,而所谓 attention,本质就是汇聚上下文向量时的权重
Note: c \pmb{c} ccc 可以作为附加信息和 s \pmb{s} sss 一起作为解码器输入;也可以完全不用 s \pmb{s} sss 只依靠 c \pmb{c} ccc 和输入 token x ′ \pmb{x}' xxx′ 进行解码,因为 c \pmb{c} ccc 中已经包含了从来自原始序列所有样本的特征
将这样的 attention 机制加入 RNN/LSTM Encoder-Decoder 结构,如下
相比原始 RNN/LSTM Encoder-Decoder,差别仅在于多了一个上下文向量 c i \pmb{c}_i ccci 作为解码的附加信息(下图显示了注意力汇聚过程和附加输入结构,忽略了注意力计算过程)
-
下面不严谨地歇写一下上下文向量的计算过程
- 利用
注意力评分函数
a a a,计算输入 Decoder 的隐状态 s j \pmb{s}_j sssj 和 Encoder 的所有样本输出 h 1 , . . . , h m \pmb{h}_1,...,\pmb{h}_m hhh1,...,hhhm 的注意力得分
α 1 , . . . , α m \alpha_1,...,\alpha_m α1,...,αm,这个得分体现的是 s j \pmb{s}_j sssj 和 Encoder 各个输出 h i \pmb{h}_i hhhi 的相关性
α i = a ( h i , s j ) \alpha_i = a(\pmb{h}_i,\pmb{s}_j) αi=a(hhhi,sssj) - 对注意力得分进行 softemax,转为权重形式
α 1 , . . . , α m ← softmax ( α 1 , . . . , α m ) \alpha_1,...,\alpha_m \leftarrow \text{softmax}(\alpha_1,...,\alpha_m) α1,...,αm←softmax(α1,...,αm) - 利用权重对 Encoder 各个输出进行汇聚,得到解码当前 token 所需的
上下文向量
,直接将其作为解码 s \pmb{s} sss 时输入 RNN Decoder 的附加信息即可
c j = ∑ k α k h k \pmb{c}_j = \sum_{k}\alpha_k \pmb{h}_k cccj=k∑αkhhhk
上述过程总结成一句话就是:利用 s \pmb{s} sss 和 h \pmb{h} hhh 的相关性对 h \pmb{h} hhh 加权求和得到 c \pmb{c} ccc。这里有一个问题,直接用原始的 s \pmb{s} sss 和 h \pmb{h} hhh 向量往往不够灵活,比如二者维度可能不同,或者我们想在更高维度计算相关性,又或者想在更高维度汇聚 h \pmb{h} hhh 中的信息,于是我们可以加一步抽象化
- 使用矩阵
Q
\pmb{Q}
QQQ 对
s
\pmb{s}
sss 的维度进行变换,得到
查询向量query
q \pmb{q} qqq - 使用矩阵
K
\pmb{K}
KKK 对
h
\pmb{h}
hhh 的维度进行变换,得到
键向量key
k \pmb{k} kkk - 使用矩阵
V
\pmb{V}
VVV 对
h
\pmb{h}
hhh 的维度进行变换,得到
值向量key
v \pmb{v} vvv
这里的 Q , K , V \pmb{Q},\pmb{K},\pmb{V} QQQ,KKK,VVV 矩阵都是自己学出来的,在汇聚上下文向量 c \pmb{c} ccc 时,通过 q , k \pmb{q},\pmb{k} qqq,kkk 计算相关性,对 v \pmb{v} vvv 加权求和进行汇聚,如下图所示
- 利用
-
注意力评分函数可以有多种设计
- 原始论文中使用的是
加性注意力additive attention
,它允许 q \pmb{q} qqq 和 k \pmb{k} kkk 是不同长度的向量(事实上原始论文中的 q \pmb{q} qqq 和 k \pmb{k} kkk 就是 s \pmb{s} sss 和 h \pmb{h} hhh 本身),给定 q ∈ R q , k ∈ R k \pmb{q}\in\mathbb{R}^q, \pmb{k}\in\mathbb{R}^k qqq∈Rq,kkk∈Rk,注意力得分为
a ( q , k ) = w v ⊤ tanh ( W q q + W k k ) ∈ R , a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}, a(q,k)=wv⊤tanh(Wqq+Wkk)∈R, 其中可学习的参数包括 W q ∈ R h × q , W k ∈ R h × k , w v ∈ R h \mathbf W_q\in\mathbb R^{h\times q}, \mathbf W_k\in\mathbb R^{h\times k}, \mathbf w_v\in\mathbb R^{h} Wq∈Rh×q,Wk∈Rh×k,wv∈Rh - 现在主流用的是 transformer 使用的
缩放点积注意力scaled dot-product attention
,它的计算效率更高,但要求 q \pmb{q} qqq 和 k \pmb{k} kkk 是相同长度的向量。假设查询和键的所有元素都是独立的 d d d 维随机变量, 且都是均值0方差1,那么两个向量的点积的均值为0方差为 d d d,将点积除以 d \sqrt{d} d 使其方差为1,注意力得分为
a ( q , k ) = q ⊤ k d a(\mathbf q, \mathbf k) = \frac{\mathbf{q}^\top \mathbf{k}}{\sqrt{d}} a(q,k)=dq⊤k
- 原始论文中使用的是
-
引入 attention 机制,要求模型显式地考虑输出 token 和输入句子各个 token 间的相关性,可以解决传统 Seq2Seq 模型的遗忘问题。通过可视化训练后的 attention 向量,发现机器确实能够学到输入句子和输出句子各个 token 间合理的相关性
但这种能力是有代价的,设输入序列长 m m m,输出序列长 t t t,对传统模型引入 attention 机制会使计算复杂复从 Q ( m + t ) Q(m+t) Q(m+t) 大幅上升到 Q ( m × t ) Q(m\times t) Q(m×t) -
值得注意的是,这种和传统序列模型结合的 attention 在 transformer 语境中被称为 cross attention,用来强调 attention 计算发生在 Decoder 和 Encoder 之间
2.2 自注意力机制 (Self-Attention)
- Self-Attention 机制提出于 2016 年的论文 Long Short-Term Memory-Networks for Machine Reading,原始的 attention 机制仅能用于 Seq2Seq 模型,而 self-attention 解除了这个限制,可以用于所有的 RNN/LSTM 结构的模型上
- Self-Attention 的思想也很简单,其实和 2.1 节的在计算上没有任何区别,只是原始 attention 机制计算的是输出句子各个 token 和输入句子各个 token 之间的 attention;Self-Attention 只考虑一个句子,它计算自己的各个 token 和该 token 位置之前的其他 token 的 attention,然后基于这个从之前的所有 token 中汇聚上下文
c
\pmb{c}
ccc,直接代替传统 RNN 中的隐状态
s
\pmb{s}
sss,或者作为附加信息和
s
\pmb{s}
sss 拼在一起使用,Self-Attention 和 SimpleRNN 结合的一个简单示例如下
这里 h \pmb{h} hhh 代表隐状态,而且在更新隐状态计算时直接用汇聚向量 c \pmb{c} ccc 代替 h \pmb{h} hhh 使用 - 直观地看,Self-Attention 就是在更新 RNN/LSTM 隐状态时显式地要求模型重新看一遍之前的所有历史序列样本,从而避免遗忘,思想本质和 Attention 机制没有区别,不同之处在于 Self-Attention 的主要目的是用文本中的其它 token 来增强目标 token 的语义表示。从 attention 向量的可视化结果看,Self-Attention 可以避免遗忘,还能帮助传统 RNN/LSTM 更加关注有相关性的 token
- 值得注意的是,这种和传统序列模型结合的 self-attention 在 transformer 语境中被称为 masked self-attention,它其实只关注了句子中该 token 之前的部分
3. Attention + Encoder-Decoder
- 累了,这块待续…
3.1 Transformer
- 完全利用 Self-attention、Masked Self-attention、Cross-attention 和 MLP 搭建的 Encoder-Decoder 结构
3.2 GPT
- Transformer 的 Decoder 部分,Autoregress 预训练,主要用来做文本生成任务,有特征提取能力但是不如 BERT
3.3 BERT
- Transformer 的 Encoder 部分,完形填空+判断上下句预训练,主要用来学特征(动态词嵌入),利用这个特征可以做很多下游任务