⭐ 引言

本文主要参考李宏毅老师对于自注意力机制的讲解内容,但在此基础之上进行了一定的补充和删减,文中大部分插图来源于李宏毅老师的课件。本文的主要目的是梳理清楚自注意力机制的基本原理,理解什么是自注意力机制,不关注代码实现和具体的数学运算。本文尽可能把内容只控制在自注意力机制的基本框架上,不进行过多的相关概念的扩展,以免被其他相关内容转移注意力。

說 1. 从“单向量输入”到“多向量输入”

在之前的机器学习方法中,模型的输入变量一般是【单向量】。

例如,对于机器学习入门经典项目——泰坦尼克号生存预测,该项目需要根据泰坦尼克号上的乘客的个人信息(包括性别、年龄、船舱等级、登船港口、船舱号等)来预测乘客的生存情况。在这个问题中,性别、年龄、船舱等级、登船港口、船舱号等特征共同构成了一个“单向量输入”,输出变量是一个标量(生存则为1,死亡则为0)。

在NLP领域,文本内容作为模型的输入,模型的输入变量一般是【多向量】。

【多向量】是指由一系列向量构成的集合(Vector Set),也被叫做序列(Sequence)。

文本内容之所以作为多向量输入,这主要与文本内容的表示方法有关。文本内容作为符号不能直接用于模型计算,所以需要通过一些方法将文本内容转化为向量,然后再作为模型的输入。 目前将文本内容转化为向量的常用方法是 Embedding,通过这种方法可以将一个 token (可以理解为一个单词)转化为一个向量,这样一个句子就可以由一系列向量来表示。至于这里 Embedding 是如何实现将 token 转化为向量的不过多阐述。

泌 2. “多向量输入”对应的输出

对于多变量输入,模型的输出主要包括以下三种可能性:

(1)Sequence Labeling: 每个输入向量对应一个输出(Each vector has a label, N input vectors to N labels) 模型的输出标签(label)数量等于模型的输入向量(vector)数量。 这种情况下,模型输入多少个向量,就会对应输出多少个标签。 例如,对于词性标注任务,模型需要判断每个词的词性(动词、名词、形容词等)。所以对于每一个输入向量(一个单词)都需要对应输入一个标签(单词的词性)。

(2)Classification or Regression: 所有输入向量对应一个输出(The whole sequence has a label, N input vectors to one label) 模型的输出标签(label)数量只有一个。 这种情况下,无论有几个输入向量,模型最终只会输出一个标签。 例如,对于情感分析任务,模型需要判断输入句子的情感倾向(正向情感或负向情感)。所以对于任意多个输入向量(一个句子)都只需要输出一个标签(情感得分)。 (3)Seq2seq: 输出数量不确定(Model decides the number of labels itself, N input vectors to M labels) 模型的输出标签(label)数量不确定。 这种情况下,模型输出的标签数量实现无法确定,需要由模型根据前序输出内容自动判断是否结束输出。 例如,对于翻译任务,模型需要将某种语言的原始句子转换为目标语言的句子。由于各种语言的语法结构和表达方式不同,一般来说,目标语言的句子长度(向量数量)和原始语言的句子长度是不一致的,需要由模型自行决定何时结束输出。

 3. 序列标注问题(Sequence Labeling)

首先,我们只考虑上文提到的第一种情况,即 “每个输入向量对应一个输出” 的情况,下面讨论对于这种问题如何构建模型:

(1)单独建模 由于每个输入向量都会对应一个输入,一种最简单的想法是将每一个向量作为输入,每一个对应的标签作为输出,这样就将一个“多向量输入”的问题转化为了“单向量输入的问题”。 这种方法尽管非常简单,但它最大的问题是无法考虑上下文之间的关系。 例如对于 “I saw a saw” 这句话,翻译为“我看到了一把锯子”,其中第一个 “saw” 的词性应该是动词,第二个 “saw” 的词性应该是名词。但对于上面这种建模方法,模型无法考虑同一个词在句子中不同位置所引起的词性不同,对于同一个词,一定会输出相同的词性。

(2)滑动窗口 单独建模的问题是无法考虑上下文信息,一种改进方法是让模型同时考虑单词左右两边 n 个单词的信息,形成一个滚动窗口。通过这种方式,在对一个单词进行词性标注时,模型可以考虑这个单词周围几个单词的信息,相比于上面单独建模的方法有所提升。 尽管滑动窗口考虑了单词的上下文信息,但这种能力会受限于窗口宽度的大小,无法考虑整个序列的全部信息。 如果直接将窗口长度设定为整个语料库中最长的句子的长度,这样可以保证窗口能够考虑整个序列的信息。但是如果语料库中最长的句子很长,这同样会带来两个问题:① 计算效率不高;② 容易出现过拟合。

 4. 自注意力机制(Self-attention)

在第 3 部分中,我们讨论了使用 单独建模 和 滑动窗口 两种方式来解决序列标注的问题,但两种方法都存在明显的局限性,下面将重点探讨自注意力机制(Self-attention)是如何解决以上问题的,即在保证效率的同时考虑全部上下文信息进行建模,同时模型还可以自动决定更加重视哪部分的上下文。

首先,从宏观来看,Self-attention 是考虑了全部上下文信息的机制。 每输出一个标签(label),Self-attention 都会考虑整个序列的全部信息。单纯从这点来看,这与一个超长的滑动窗口没有区别,重点在于 Self-attention 内部是如何考虑全部上下文信息的。

在下面这张图中,带有黑色框线的矩形表示考虑了全部上下文信息的向量,后文相同。

具体来看,对于每一个输出变量

b

b

b,self-attention 都会考虑全部输入变量

{

a

i

}

\{a^{i}\}

{ai}的信息。

接下来我们探究,对于一个具体的输入向量

a

1

a^1

a1,self-attention 是如何考虑全部上下文信息得出

b

1

b^1

b1 的。

首先,需要判断

{

a

i

}

\{a^i\}

{ai} 中有哪些向量是和

a

1

a^1

a1 相关的,这种相关性被称为 注意力分数,记作

α

\alpha

α。

注意力分数的计算

(1)点积注意力分数(Dot-product) 向量的点积运算可以表示相似度,因此可以通过计算输入向量

a

1

\mathbf{a^1}

a1 和 输入向量

a

2

\mathbf{a^2}

a2 的点积来得到

a

1

\mathbf{a^1}

a1 和

a

2

\mathbf{a^2}

a2 之间的相关性,即注意力分数

α

1

,

2

\alpha_{1, 2}

α1,2​。但是,这样直接计算无法引入模型可学习的参数,因此,我们需要先对输入向量

a

1

\mathbf{a^1}

a1 和 输入向量

a

2

\mathbf{a^2}

a2 进行一定的线性变换,然后再通过点积运算得到注意力分数

α

1

,

2

\alpha_{1, 2}

α1,2​。我们可以通过引入两个矩阵

W

q

\mathbf{W^q}

Wq 和

W

k

\mathbf{W^k}

Wk 来分别对 向量

a

1

\mathbf{a^1}

a1 和

a

2

\mathbf{a^2}

a2 进行线性变换,

a

1

\mathbf{a^1}

a1 通过矩阵

W

q

\mathbf{W^q}

Wq 变换后得到向量

q

\mathbf{q}

q (query),

a

2

\mathbf{a^2}

a2 通过矩阵

W

q

\mathbf{W^q}

Wq 变换后得到向量

k

\mathbf{k}

k (key) 。 注意:这里矩阵

W

q

\mathbf{W^q}

Wq 和

W

k

\mathbf{W^k}

Wk 最开始是随机初始化的,也是后续模型在迭代过程中可以学习的参数,模型可以通过迭代得到最优化的矩阵

W

q

\mathbf{W^q}

Wq 和

W

k

\mathbf{W^k}

Wk。 将

q

\mathbf{q}

q (query)和

k

\mathbf{k}

k (key)进行点积运算即可得到注意力分数。但在实际应用中,一般还会对该结果进行缩放(scaling),即将点积计算结果除以向量维度的平方根。这是因为点积运算的结果会随着向量维度

d

d

d 的增长而增长。点积计算结果除以

d

\sqrt{d}

d

​ 有助于使点积后的结果更加稳定,维持在一定的合理范围内。否则,如果点积结果过大或过小,在后续的 softmax 过程中,由于 softmax 函数对于输入的尺度非常敏感,如果输入的数值非常大或非常小,softmax函数的输出会接近0或1,这会导致梯度非常小,从而在反向传播中导致梯度消失问题。 综上所述,点积法计算注意力分数的公式如下: 设

q

1

=

a

1

W

q

,

k

2

=

a

2

W

k

q^1 = a^1 W_q, k^2 = a^2 W_k

q1=a1Wq​,k2=a2Wk​,向量

a

1

a^1

a1 和

a

2

a^2

a2 的维度为

d

d

d,其中

q

1

q^1

q1 是输入向量

a

1

a^1

a1 的查询表示,

k

2

k^2

k2 是输入向量

a

2

a^2

a2 的键表示,那么

a

1

a^1

a1 和

a

2

a^2

a2 之间的点积缩放法注意力分数为:

Attention

(

a

1

,

a

2

)

=

q

1

k

2

d

k

\text{Attention}(\mathbf{a}_1, \mathbf{a}_2) = \frac{\mathbf{q}_1 \cdot \mathbf{k}_2}{\sqrt{d_k}}

Attention(a1​,a2​)=dk​

​q1​⋅k2​​ (2)加性注意力分数(Additive) 加性注意力分数的计算方式与点积缩放法类似,它们都需要通过矩阵

W

q

\mathbf{W^q}

Wq 和

W

k

\mathbf{W^k}

Wk 对原始输入变量进行线性变换,但是加性方法不对

q

\mathbf{q}

q (query)和

k

\mathbf{k}

k (key)进行点积运算,而是通过加法进行联合表示(concatenation),然后将联合表示的结果送入一个前馈神经网络(通常是一个带有非线性激活函数的单层或多层网络)。这个网络的目的是对输入向量进行变换并捕捉查询和键之间的相互作用。 加性注意力分数的数学计算公式如下: 设

q

1

=

a

1

W

q

,

k

2

=

a

2

W

k

q^1 = a^1 W_q, k^2 = a^2 W_k

q1=a1Wq​,k2=a2Wk​,那么

a

1

a^1

a1 和

a

2

a^2

a2 之间的加性法注意力分数为:

Attention

(

q

1

,

k

2

)

=

v

tanh

(

W

[

q

1

;

k

2

]

)

\text{Attention}(\mathbf{q^1}, \mathbf{k^2}) = \mathbf{v}^\top \text{tanh}(\mathbf{W}[\mathbf{q^1}; \mathbf{k^2}])

Attention(q1,k2)=v⊤tanh(W[q1;k2])

在计算得到注意力分数

α

\alpha

α 之后,一般还需要进行一次 softmax 操作得到

α

\alpha'

α′,完整流程如下图所示。 (注意,向量

a

1

a^1

a1 也需要与自身计算注意力分数得到

α

1

,

1

\alpha_{1, 1}'

α1,1′​。)

在得到归一化的注意力分数之后,向量

a

1

a^1

a1 就可以根据它和其他向量之间的注意力分数来决定考虑哪些向量,也就是根据注意力分数对其他向量进行加权平均,权重(注意力分数)越大的向量,对于向量

a

1

a^1

a1 也就越重要。

但是,其他向量在被加权平均之前,也需要先进行一次线性变换,类似于我们得到

q

\mathbf{q}

q (query)和

k

\mathbf{k}

k (key),向量

a

i

\mathbf{a^i}

ai 通过矩阵

W

q

\mathbf{W^q}

Wq 变换后得到向量

v

i

\mathbf{v^i}

vi (value),然后再根据注意力分数对向量

v

i

\mathbf{v^i}

vi 进行加权平均。

综上所述,self-attention 的输出结果

b

1

b^1

b1 的计算公式如下:

b

1

=

i

α

1

,

i

v

i

\mathbf{b^1} = \sum_{i} \alpha_{1, i} \mathbf{v^i}

b1=i∑​α1,i​vi

同理,我们也可以通过一样的过程,分别计算出向量

a

2

,

a

3

,

a

4

\mathbf{a^2}, \mathbf{a^3}, \mathbf{a^4}

a2,a3,a4 所对应的 self-attention 的输出结果

b

2

,

b

3

,

b

4

\mathbf{b^2}, \mathbf{b^3}, \mathbf{b^4}

b2,b3,b4。

这里值得注意的是,self-attention 的输出结果

b

1

,

b

2

,

b

3

,

b

4

\mathbf{b^1}, \mathbf{b^2}, \mathbf{b^3}, \mathbf{b^4}

b1,b2,b3,b4 是可以同时进行计算的,而不需要像 RNN 那样,必须要知道序列中前一个状态的隐藏变量才能计算下一个状态的结果,因此 self-attention 的效率更高。

數 5. 自注意力机制(Self-attention)的矩阵表示

以上内容介绍了如何计算一个输入向量

a

i

\mathbf{a^i}

ai 对应的 self-attention 的结果,即向量

b

i

\mathbf{b^i}

bi 。为了便于理解,以上内容都是针对单个向量

a

i

\mathbf{a^i}

ai 展示的,但实际上,将一个序列(句子)中包括的所有向量(token)放在一起形成矩阵

I

\mathbf{I}

I,将会更加直观的展现这个过程。

如下图所示,将所有的向量

{

a

i

}

\{\mathbf{a^i}\}

{ai} 组合在一起形成矩阵

I

\mathbf{I}

I,矩阵

I

\mathbf{I}

I分别与

W

q

,

W

k

,

W

v

\mathbf{W^q}, \mathbf{W^k}, \mathbf{W^v}

Wq,Wk,Wv 相乘即可得到所有向量

{

a

i

}

\{\mathbf{a^i}\}

{ai} 对应的 query key 和value,分别记作矩阵

Q

,

K

,

V

\mathbf{Q}, \mathbf{K}, \mathbf{V}

Q,K,V。

得到矩阵

Q

,

K

,

V

\mathbf{Q}, \mathbf{K}, \mathbf{V}

Q,K,V 之后,上面计算向量之间注意力分数的过程也可以通过矩阵来表示,我们需要通过矩阵

Q

\mathbf{Q}

Q 和

K

\mathbf{K}

K来计算得到注意力分数矩阵

A

\mathbf{A}

A,然后通过 softmax 得到矩阵

A

\mathbf{A'}

A′。具体原理如下图所示:

有了注意力分数矩阵之后,我们就可以通过将注意力分数矩阵和值矩阵相乘得到最终的 self-attention 输出。具体过程如下图所示:

重新梳理一下上面的 self-attention 过程,尽管直觉上觉得流程很复杂,但self-attention 的本质是对输入矩阵进行一系列的矩阵乘法,然后得到输出矩阵。

首先,输入矩阵

I

\mathbf{I}

I 分别与参数矩阵

W

q

,

W

k

,

W

v

\mathbf{W^q}, \mathbf{W^k}, \mathbf{W^v}

Wq,Wk,Wv 相乘,分别得到 query, key, value 矩阵

Q

,

K

,

V

\mathbf{Q}, \mathbf{K}, \mathbf{V}

Q,K,V ;然后矩阵

Q

\mathbf{Q}

Q 和 矩阵

K

\mathbf{K}

K 进行内积运算得到注意力得分矩阵

A

\mathbf{A}

A;再通过 softmax 得到 Attention Matrix

A

\mathbf{A'}

A′;最后,通过 Attention Matrix

A

\mathbf{A'}

A′ 对矩阵

V

\mathbf{V}

V 进行加权,得到输出矩阵

O

\mathbf{O}

O。

self-attention 流程如下图所示:

寧 6. 多头注意力机制(Multi-head Self-attention)

到目前为止,我们已经了解了注意力机制及其矩阵表达形式。回顾第 5 部分的内容,可以发现其实整个 self-attention 的过程只有

W

q

,

W

k

,

W

v

\mathbf{W^q}, \mathbf{W^k}, \mathbf{W^v}

Wq,Wk,Wv 三个矩阵是需要模型通过训练数据得到的。对于一些复杂任务,这些参数量可能无法拟合自然语言中复杂的逻辑关系,因此我们可以通过多头注意力(Multi-head Self-attetion 来引入更多的可学习的参数,从而使得模型更加复杂。

具体来说,对于 2-head self-attention,我们需要两个参数矩阵

W

q

,

1

,

W

k

,

1

,

W

v

,

1

\mathbf{W^{q,1}}, \mathbf{W^{k,1}}, \mathbf{W^{v,1}}

Wq,1,Wk,1,Wv,1 和

W

q

,

2

,

W

k

,

2

,

W

v

,

2

\mathbf{W^{q,2}}, \mathbf{W^{k,2}}, \mathbf{W^{v,2}}

Wq,2,Wk,2,Wv,2 ,对于一个输入向量

a

i

\mathbf{a^i}

ai,我们可以分别得到两个输出

b

i

,

1

\mathbf{b^{i, 1}}

bi,1 和

b

i

,

2

\mathbf{b^{i, 2}}

bi,2 ,然后再将

b

i

,

1

\mathbf{b^{i, 1}}

bi,1 和

b

i

,

2

\mathbf{b^{i, 2}}

bi,2 进行线性组合得到最终的输出向量

b

i

\mathbf{b^{i}}

bi。

从直觉上理解,可以认为不同的 头(head) 将会学习文本中不同维度的特征。例如,第一个头可能更加专注词汇的意义和上下文词语的关联性;第二个头可能更加关注句子的语法结构;第三个头可能更加关注捕捉文本中的感情色彩。 (个人觉得这与 CNN 中的多核卷积类似,不同的卷积层学习图像不同方面的特征。)

 7. 位置编码(Positional Encoding)

在上面的 self-attention 过程中,我们忽略了一个很重要的信息,那就是 向量在序列中的位置关系。也就是说,对于上面提出的模型来说,

a

1

\mathbf{a^1}

a1 和

a

2

\mathbf{a^2}

a2 这两个模型来说在位置上来说是没有区别的。但实际的 NLP 任务中,位置信息有可能是很有用的信息。例如,对于词性标注的任务,出现在序列中第一位的单词是动词的可能性就更低。

因此,我们需要通过一个 位置编码(positional encoding) 来将向量的位置信息传递给模型。具体来说,需要在输入向量

a

i

\mathbf{a^i}

ai 的基础之上加上一个位置向量

e

i

\mathbf{e^i}

ei,从而使得模型能够感知到当前向量在整个序列中的位置关系(位置编码向量

e

i

\mathbf{e^i}

ei 如何得到在此不赘述)。

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: