NLP课程笔记-基于transformers的自然语言处理入门
- 项目地址
- 2.1 图解attention
- 2.1.1 Seq2seq框架
- 2.1.2 Seq2seq细节
- 2.1.3 Attention
- 1) 基于RNN的seq2seq模型的问题
- 2)基于Attention seq2seq方案
- 2.2 图解transformer
- 2.2.1 transformer宏观结构
- 单层编码器
- 单层解码器
- 2.2.2 transformer结构细节
- 1.输入处理
- 词向量
- 位置向量
- 2.编码器
- self-attention层
- self-attention细节
- Self-Attention矩阵计算(一次得到所有位置经过注意力信息聚合之后,即Self-Attention之后,的所有输出向量)
- 多头注意力机制
- Attention代码实例
- 残差链接
- 3.解码器
- 4.线性层和softmax
- 5损失函数
- 2.3 图解bert
- bert模型通过预训练和微调的方式应用于下游任务
- 模型结构
- 模型输入
- 模型输出
- Bert的预训练任务:Masked Language Model
- bert预训练任务2:相邻句子判断
- BERT的应用
- Bert用于特征提取
- 扩展阅读
项目地址
https://github.com/datawhalechina/learn-nlp-with-transformers/
- 2017年,Attention Is All You Need论文(Google Brain)首次提出了Transformer模型结构并在机器翻译任务上取得了The State of the Art(SOTA, 最好)的效果。
- 2018年,BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (Google AI Language lab)使用Transformer模型结构进行大规模语言模型(language model)预训练(Pre-train),再在多个NLP下游(downstream)任务中进行微调(Finetune),一举刷新了各大NLP任务的榜单最高分,轰动一时。
- 2019年-2021年,研究人员将Transformer这种模型结构和预训练+微调这种训练方式相结合,提出了一系列Transformer模型结构、训练方式的改进(比如transformer-xl,XLnet,Roberta等等)。如下图所示,各类Transformer的改进不断涌现。
现在较为流行的基于transformer模型,基本你都可以在hugging/transformer库中找到并直接使用。
2.1 图解attention
问题:Attention出现的原因是什么? 潜在的答案:基于循环神经网络(RNN)一类的seq2seq模型,在处理长文本时遇到了挑战,而对长文本中不同位置的信息进行attention有助于提升RNN的模型效果。
于是学习的问题就拆解为:1. 什么是seq2seq模型?2. 基于RNN的seq2seq模型如何处理文本/长文本序列?3. seq2seq模型处理长文本序列时遇到了什么问题?4.基于RNN的seq2seq模型如何结合attention来改善模型效果?
2.1.1 Seq2seq框架
seq2seq是一种常见的NLP模型结构,全称是:sequence to sequence,翻译为“序列到序列”。顾名思义:从一个文本序列得到一个新的文本序列。
典型的任务有:机器翻译任务,文本摘要任务。谷歌翻译在2016年末开始使用seq2seq模型,并发表了2篇开创性的论文。
首先看seq2seq干了什么事情?seq2seq模型的输入可以是一个(单词、字母或者图像特征)序列,输出是另外一个(单词、字母或者图像特征)序列。一个训练好的seq2seq模型如下图所示。
2.1.2 Seq2seq细节
由于seq2seq模型可以用来解决机器翻译任务,因此机器翻译被任务seq2seq模型解决过程如下图所示,当作seq2seq模型的一个具体例子来学习。
seq2seq模型中的编码器和解码器一般采用的是循环神经网络RNN(Transformer模型还没出现的过去时代)。编码器将输入的法语单词序列编码成context向量(在绿色encoder和紫色decoder中间出现),然后解码器根据context向量解码出英语单词序列。
深入学习机器翻译任务中的seq2seq模型,如下图所示。**seq2seq模型中的编码器和解码器一般采用的是循环神经网络RNN(Transformer模型还没出现的过去时代)。**编码器将输入的法语单词序列编码成context向量(在绿色encoder和紫色decoder中间出现),然后解码器根据context向量解码出英语单词序列。
图:context向量对应上图中间浮点数向量。在下文中,我们会可视化这些数字向量,使用更明亮的色彩来表示更高的值,如上图右边所示
如上图所示,我们来看一下黄色的context向量是什么?本质上是一组浮点数。而这个context的数组长度是基于编码器RNN的隐藏层神经元数量的。上图展示了长度为4的context向量,但在实际应用中,context向量的长度是自定义的,比如可能是256,512或者1024。
那么RNN是如何具体地处理输入序列的呢?
假设序列输入是一个句子,这个句子可以由
n
n
n个词表示:
s
e
n
t
e
n
c
e
=
{
w
1
,
w
2
,
.
.
.
,
w
n
}
sentence = \{ w_1, w_2,...,w_n\}
sentence={w1,w2,...,wn}。
RNN首先将句子中的每一个词映射成为一个向量得到一个向量序列: X = { x 1 , x 2 , . . . , x n } X = \{x_1, x_2,...,x_n\} X={x1,x2,...,xn},每个单词映射得到的向量通常又叫做:word embedding。
然后在处理第 t ∈ [ 1 , n ] t \in [1,n] t∈[1,n]个时间步的序列输入 x t x_t xt时,RNN网络的输入和输出可以表示为: h t = R N N ( x t , h t − 1 ) h_{t} = RNN(x_t, h_{t-1}) ht=RNN(xt,ht−1)
输入:RNN在时间步
t
t
t的输入之一为单词
w
t
w_t
wt经过映射得到的向量
x
t
x_t
xt。
输入:RNN另一个输入为上一个时间步
t
−
1
t-1
t−1得到的hidden state向量
h
t
−
1
h_{t-1}
ht−1,同样是一个向量。
输出:RNN在时间步
t
t
t的输出为
h
t
h_t
ht hidden state向量。
让我们详细观察一下编码器如何在每个时间步得到hidden sate,并将最终的hidden state传输给解码器,解码器根据编码器所给予的最后一个hidden state信息解码处输出序列。注意,最后一个 hidden state实际上是我们上文提到的context向量。
编码器逐步得到hidden state并传输最后一个hidden state给解码器。
编码器首先按照时间步依次编码每个法语单词,最终将最后一个hidden state也就是context向量传递给解码器,解码器根据context向量逐步解码得到英文输出。
目前为止,希望你已经明白了本文开头提出的前两个问题:1. 什么是seq2seq模型?2. seq2seq模型如何处理文本/长文本序列?
1、是一种从序列输入得到序列输出的模型架构。
2、通过编码器和解码器构成长文本处理的整个过程。编码器和解码器本身一般采用的是循环神经网络来实现(Transformers模型还没出现时)。
那么请思考第3、4个问题:
3. seq2seq模型处理文本序列(特别是长文本序列)时会遇到什么问题?
4.基于RNN的seq2seq模型如何结合attention来解决问题3并提升模型效果?
2.1.3 Attention
1) 基于RNN的seq2seq模型的问题
基于RNN的seq2seq模型编码器所有信息都编码到了一个context向量中,便是这类模型的瓶颈。一方面单个向量很难包含所有文本序列的信息,另一方面RNN递归地编码文本序列使得模型在处理长文本时面临非常大的挑战(比如RNN处理到第500个单词的时候,很难再包含1-499个单词中的所有信息了)。
2)基于Attention seq2seq方案
面对以上问题,Bahdanau等2014发布的Neural Machine Translation by Jointly Learning to Align and Translate 和 Luong等2015年发布的Effective Approaches to Attention-based Neural Machine Translation 两篇论文中,提出了一种叫做注意力attetion的技术。通过attention技术,seq2seq模型极大地提高了机器翻译的质量。归其原因是:attention注意力机制,使得seq2seq模型可以有区分度、有重点地关注输入序列。
下图依旧是机器翻译的例子:
在第7个时间步,注意力机制使得解码器在产生英语翻译之前,可以将注意力集中在 “student” 这个词(在法语里,是 “student” 的意思)。这种从输入序列放大相关信号的能力,使得注意力模型,比没有注意力的模型,产生更好的结果。
让我们继续来理解带有注意力的seq2seq模型:一个注意力模型与经典的seq2seq模型主要有2点不同:
A. 首先,编码器会把更多的数据传递给解码器。编码器把所有时间步的 hidden state(隐藏层状态)传递给解码器,而不是只传递最后一个 hidden state(隐藏层状态),如下面的动态图所示:
B. 注意力模型的解码器在产生输出之前,做了一个额外的attention处理。如下图所示,具体为:
由于编码器中每个 hidden state(隐藏层状态)都对应到输入句子中一个单词,那么解码器要查看所有接收到的编码器的 hidden state(隐藏层状态)。
给每个 hidden state(隐藏层状态)计算出一个分数(我们先忽略这个分数的计算过程)。
所有hidden state(隐藏层状态)的分数经过softmax进行归一化。
将每个 hidden state(隐藏层状态)乘以所对应的分数,从而能够让高分对应的 hidden state(隐藏层状态)会被放大,而低分对应的 hidden state(隐藏层状态)会被缩小。
将所有hidden state根据对应分数进行加权求和,得到对应时间步的context向量。
在第4个时间步,编码器结合attention得到context向量的5个步骤。
所以,attention可以简单理解为:一种有效的加权求和技术,其艺术在于如何获得权重。
现在,让我们把所有内容都融合到下面的图中,来看看结合注意力的seq2seq模型解码器全流程,动态图展示的是第4个时间步:
注意力模型的解码器 RNN 的输入包括:一个word embedding 向量,和一个初始化好的解码器 hidden state,图中是
h
i
n
i
t
h_{init}
hinit。
RNN 处理上述的 2 个输入,产生一个输出和一个新的 hidden state,图中为h4。
注意力的步骤:我们使用编码器的所有 hidden state向量和 h4 向量来计算这个时间步的context向量(C4)。
我们把 h4 和 C4 拼接起来,得到一个橙色向量。
我们把这个橙色向量输入一个前馈神经网络(这个网络是和整个模型一起训练的)。
根据前馈神经网络的输出向量得到输出单词:假设输出序列可能的单词有N个,那么这个前馈神经网络的输出向量通常是N维的,每个维度的下标对应一个输出单词,每个维度的数值对应的是该单词的输出概率。
在下一个时间步重复1-6步骤。
注意:在注意力模型的解码器中,初始状态下输入的绿色word embedding向量代表的是序列结束符(End of Sequence token,通常记作或)。在神经机器翻译和其他序列生成任务中,序列结束符标记输入序列的终止。在解码开始阶段,将<END>作为输入意味着我们在解码器的初始时刻还没有任何生成的单词,因此使<END>来
到目前为止,希望你已经知道本文开头提出的3、4问题的答案啦:
3、seq2seq处理长文本序列的挑战是什么?
答:基于RNN的seq2seq模型编码器所有信息都编码到了一个context向量中,便是这类模型的瓶颈。一方面单个向量很难包含所有文本序列的信息,另一方面RNN递归地编码文本序列使得模型在处理长文本时面临非常大的挑战(比如RNN处理到第500个单词的时候,很难再包含1-499个单词中的所有信息了)。
4、seq2seq是如何结合attention来解决问题3中的挑战的?
答:通过关注编码器输出的所有隐藏层特征,从而解决了单个向量context代表整个数据的信息不足问题。
防止过长的文本在递归编码过程中信息衰减,通过注意力可以有区分度有重点地关注输入文本,从而改善这个问题。
最后,我们可视化一下注意力机制,看看在解码器在每个时间步关注了输入序列的哪些部分:
动态图:解码步骤时候attention关注的词
需要注意的是:注意力模型不是无意识地把输出的第一个单词对应到输入的第一个单词,它是在训练阶段学习到如何对两种语言的单词进行对应(在我们的例子中,是法语和英语)。
下图还展示了注意力机制的准确程度(图片来自于上面提到的论文):
“European Economic Area” 时,注意力分布情况。在法语中,这些单词的顺序,相对于英语,是颠倒的(“européenne économique zone”)。而其他词的顺序是类似的。
2.2 图解transformer
有没有一种神经网络结构直接基于attention构造,并且不再依赖RNN、LSTM或者CNN网络结构了呢?答案便是:Transformer。因此,我们将在本小节对Transformer所涉及的细节进行深入探讨。
Transformer模型在2017年被google提出,直接基于Self-Attention结构,取代了之前NLP任务中常用的RNN神经网络结构,并在WMT2014 Englishto-German和WMT2014 English-to-French两个机器翻译任务上都取得了当时的SOTA。
与RNN这类神经网络结构相比,Transformer一个巨大的优点是:模型在处理序列输入时,可以对整个序列输入进行并行计算,不需要按照时间步循环递归处理输入序列。
2.1章节详细介绍了RNN神经网络如何循环递归处理输入序列,欢迎读者复习查阅。
下图先便是Transformer整体结构图,与篇章2.1中介绍的seq2seq模型类似,Transformer模型结构中的左半部分为编码器(encoder),右半部分为解码器(decoder),下面我们来一步步拆解 Transformer。
图:transformer模型结构
注释和引用说明:本文将通过总-分的方式对Transformer进行拆解和讲解,希望有助于帮助初学者理解Transformer模型结构。
2.2.1 transformer宏观结构
以机器翻译任务为例,先将Transformer这种特殊的seqseq模型看作一个黑盒,黑盒的输入是法语文本序列,输出是英语文本序列(对比2.1章节的seq2seq框架知识我们可以发现,Transformer宏观结构属于seq2seq范畴,只是将之前seq2seq中的编码器和解码器,从RNN模型替换成了Transformer模型)。
图:Transformer黑盒输入和输出
将上图中的中间部分“THE TRANSFORMER”拆开成seq2seq标准结构,得到下图:左边是编码部分encoders,右边是解码器部分decoders。
图:encoder-decoder
下面,再将上图中的编码器和解码器细节绘出,得到下图。我们可以看到,编码部分(encoders)由多层编码器(Encoder)组成(Transformer论文中使用的是6层编码器,这里的层数6并不是固定的,你也可以根据实验效果来修改层数)。同理,解码部分(decoders)也是由多层的解码器(Decoder)组成(论文里也使用了6层解码器)。每层编码器网络结构是一样的,每层解码器网络结构也是一样的。不同层编码器和解码器网络结构不共享参数。
图:6层编码和6层解码器
单层编码器
接下来,我们看一下单层encoder,单层encoder主要由以下两部分组成,如下图所示
- Self-Attention Layer
- Feed Forward Neural Network(前馈神经网络,缩写为 FFNN)
编码器的输入文本序列
w
1
,
w
2
,
.
.
.
,
w
n
w_1, w_2,...,w_n
w1,w2,...,wn最开始需要经过embedding转换,得到每个单词的向量表示
x
1
,
x
2
,
.
.
.
,
x
n
x_1, x_2,...,x_n
x1,x2,...,xn,其中
x
i
∈
R
d
x_i \in \mathbb{R}^{d}
xi∈Rd是维度为
d
d
d的向量,然后所有向量经过一个Self-Attention神经网络层进行变换和信息交互得到
h
1
,
h
2
,
.
.
.
h
n
h_1, h_2,...h_n
h1,h2,...hn,其中
h
i
∈
R
d
h_i \in \mathbb{R}^{d}
hi∈Rd是维度为
d
d
d的向量。
self-attention层处理一个词向量的时候,不仅会使用这个词本身的信息,也会使用句子中其他词的信息(你可以类比为:当我们翻译一个词的时候,不仅会只关注当前的词,也会关注这个词的上下文的其他词的信息)。
Self-Attention层的输出会经过前馈神经网络得到新的
x
1
,
x
2
,
.
.
,
x
n
x_1, x_2,..,x_n
x1,x2,..,xn,依旧是
n
n
n个维度为
d
d
d的向量。这些向量将被送入下一层encoder,继续相同的操作。
图:单层encoder
单层解码器
与编码器对应,如下图,解码器在编码器的self-attention和FFNN中间插入了一个Encoder-Decoder Attention层,这个层帮助解码器聚焦于输入序列最相关的部分(类似于seq2seq模型中的 Attention)。
图:单层decoder
总结一下,我们基本了解了Transformer由编码部分和解码部分组成,而编码部分和解码部分又由多个网络结构相同的编码层和解码层组成。
每个编码层由self-attention和FFNN组成,每个解码层由self-attention、FFNN和encoder-decoder attention组成。
以上便是Transformer的宏观结构啦,下面我们开始看宏观结构中的模型细节。
2.2.2 transformer结构细节
了解了Transformer的宏观结构之后。下面,让我们来看看Transformer如何将输入文本序列转换为向量表示,又如何逐层处理这些向量表示得到最终的输出。
因此本节主要内容依次包含:
- 输入处理 input processing
- 词向量(word embedding)
- 位置向量(position embedding)
- 编码器(encoder)
- 解码器(decoder)
1.输入处理
词向量
和常见的NLP 任务一样,我们首先会使用词嵌入算法(embedding algorithm),将输入文本序列的每个词转换为一个词向量。实际应用中的向量一般是 256 或者 512 维。但为了简化起见,我们这里使用4维的词向量来进行讲解。
如下图所示,假设我们的输入文本是序列包含了3个词,那么每个词可以通过词嵌入算法得到一个4维向量,于是整个输入被转化成为一个向量序列。在实际应用中,我们通常会同时给模型输入多个句子,如果每个句子的长度不一样,我们会选择一个合适的长度,作为输入文本序列的最大长度:如果一个句子达不到这个长度,那么就填充先填充一个特殊的“padding”词;如果句子超出这个长度,则做截断。最大序列长度是一个超参数,通常希望越大越好,但是更长的序列往往会占用更大的训练显存/内存,因此需要在模型训练时候视情况进行决定。
图:3个词和对应的词向量
输入序列每个单词被转换成词向量表示还将加上位置向量来得到该词的最终向量表示。
位置向量
如下图所示,Transformer模型对每个输入的词向量都加上了一个位置向量。这些向量有助于确定每个单词的位置特征,或者句子中不同单词之间的距离特征。词向量加上位置向量背后的直觉是:将这些表示位置的向量添加到词向量中,得到的新向量,可以为模型提供更多有意义的信息,比如词的位置,词之间的距离等。
图:位置编码向量
依旧假设词向量和位置向量的维度是4,我们在下图中展示一种可能的位置向量+词向量:
那么带有位置编码信息的向量到底遵循什么模式?原始论文中给出的设计表达式为:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
/
1000
0
2
i
/
d
model
)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
/
1000
0
2
i
/
d
model
)
PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \\ PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})
PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
(我的思考,位置编码,应当能够被用来衡量两个位置是否是相近;能够同时捕捉段距离和长距离的位置信息;同时能够适应不同长度的文本,即不同规模的位置,都能用来生成位置编码;)
上面表达式中的
p
o
s
pos
pos代表词的位置,
d
m
o
d
e
l
d_{model}
dmodel代表位置向量的维度,
i
∈
[
0
,
d
m
o
d
e
l
)
i \in [0, d_{model})
i∈[0,dmodel)代表位置
p
o
s
pos
pos的
d
m
o
d
e
l
d_{model}
dmodel维位置向量的第
i
i
i维。于是根据上述公式,我们可以得到第
p
o
s
pos
pos位置的
d
m
o
d
e
l
d_{model}
dmodel维位置向量。
在下图中,我们画出了一种位置向量在第4、5、6、7维度、不同位置的的数值大小。横坐标表示位置下标,纵坐标表示数值大小。
图:位置编码在0-100位置,在4、5、6、7维的数值图示
当然,上述公式不是唯一生成位置编码向量的方法。但这种方法的优点是:可以扩展到未知的序列长度。例如:当我们的模型需要翻译一个句子,而这个句子的长度大于训练集中所有句子的长度,这时,这种位置编码的方法也可以生成一样长的位置编码向量。
我的思考和疑问
一、为什么transformer原文中使用的位置向量的公式为什么能够表示单词在句子中的位置,并且衡量单词之间的距离呢?
答:位置编码在Transformer模型中的作用是为序列中的每个单词提供位置信息,因为Transformer本身不具备顺序感知能力。位置编码通过加入位置信息,使模型能够利用序列中单词的位置关系。
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
/
1000
0
2
i
/
d
model
)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
/
1000
0
2
i
/
d
model
)
PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \\ PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})
PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
公式能够有效地表示单词位置并衡量单词之间的距离,原因如下:
- 周期性特性:
- 正弦和余弦函数是周期函数,这意味着它们会重复。这种周期性特性允许模型在不同的尺度上捕捉位置信息。(无论句子有多长,有多少个位置,都可以无限延伸)
- 不同维度的频率不同【问题三、什么是频率,函数的频率是什么,可以先行去理解一下】,使得每个位置(同一位置)的编码在每个维度上都有独特的表示,从而确保不同位置有不同的编码。
- 连续性和平滑性:
- 这些函数是连续的,并且变化平滑,这有助于模型在序列中学习到局部位置之间的关系。例如,位置pos和pos+1,在任何一个维度上(一个维度上)的编码会非常接近(因为相邻),但又有所不同,从而反映它们在序列中的紧密关系。
- 尺寸不变性
- 通过使用不同的频率(由 1000 0 2 i / d model 10000^{2i/d_{\text{model}}} 100002i/dmodel确定),位置编码可以同时捕捉到短距离和长距离的位置信息。较低的维度(频率较高)捕捉到局部的位置信息,而较高的维度(频率较低)捕捉到全局的位置信息。 【此处引出了疑问二、为什么较低的维度(频率较高)捕捉到局部的位置信息,而较高的维度(频率较低)捕捉到全局的位置信息。 可以先跳到二的回答当中进行理解】
- 相对位置信息:
【四、为什么两个位置编码之间的点积会反映它们的相对距离?】
总的来说,位置编码公式通过利用正弦和余弦函数的性质,提供了一个能够表示位置并反映单词之间距离的编码方式,使得Transformer模型能够在不显式编码顺序信息的情况下学习到序列中元素之间的顺序关系和距离。
二、为什么较低的维度(频率较高)捕捉到局部的位置信息,而较高的维度(频率较低)捕捉到全局的位置信息?
总结来说,位置编码公式通过使用不同的频率在不同的维度上编码位置信息,使得Transformer模型能够同时捕捉到序列中的局部和全局位置信息,从而更好地理解和处理自然语言序列。
三、函数的频率是什么?
在数学和信号处理领域,频率是指某个周期性现象在单位时间内重复的次数。对于正弦函数和余弦函数,频率描述了函数的波动速度,即在给定时间内完成一个完整波形周期的次数。
当周期函数当中存在缩放因子k的时候,周期的推算和频率的推算过程如下:
四、为什么两个位置编码之间的点积会反映它们的相对距离?
2.编码器
下图展示了向量序列在单层encoder中的流动:融合位置信息的词向量进入self-attention层,self-attention的输出每个位置的向量再输入FFN神经网络得到每个位置的新向量。
图:单层encoder的序列向量流动
self-attention层
下面来分析一下上图中Self-Attention层的具体机制。
假设我们想要翻译的句子是:
The animal didn't cross the street because it was too tired
这个句子中的 it 是一个指代词,那么 it 指的是什么呢?它是指 animal 还是street?这个问题对人来说,是很简单的,但是对模型来说并不是那么容易。但是,如果模型引入了Self Attention机制之后,便能够让模型把it和animal关联起来了。同样的,当模型处理句子中其他词时,Self Attention机制也可以使得模型不仅仅关注当前位置的词,还会关注句子中其他位置的相关的词,进而可以更好地理解当前位置的词。
与2.1章节中提到的RNN对比一下:RNN 在处理序列中的一个词时,会考虑句子前面的词传过来的hidden state,而hidden state就包含了前面的词的信息;而Self Attention机制值得是,当前词会直接关注到自己句子中前后相关的所有词语,如下图 it的例子:
图:一个词和其他词的attention
上图所示的it是一个真实的例子,是当Transformer在第5层编码器编码“it”时的状态,可视化之后显示it有一部分注意力集中在了“The animal”上,并且把这两个词的信息融合到了"it"中。
self-attention细节
先通过一个简单的例子来理解一下:什么是“self-attention自注意力机制”?假设一句话包含两个单词:Thinking Machines。自注意力的一种理解是:Thinking-Thinking,Thinking-Machines,Machines-Thinking,Machines-Machines,共
2
2
2^2
22种两两attention。那么具体如何计算呢?假设Thinking、Machines这两个单词经过词向量算法得到向量是
X
1
,
X
2
X_1, X_2
X1,X2:
1
:
q
1
=
X
1
W
Q
,
q
2
=
X
2
W
Q
;
k
1
=
X
1
W
K
,
k
2
=
X
2
W
K
;
v
1
=
X
1
W
V
,
v
2
=
X
2
W
V
,
W
Q
,
W
K
,
W
K
∈
R
d
x
×
d
k
2
−
3
:
s
c
o
r
e
11
=
q
1
⋅
q
1
d
k
,
s
c
o
r
e
12
=
q
1
⋅
q
2
d
k
;
s
c
o
r
e
21
=
q
2
⋅
q
1
d
k
,
s
c
o
r
e
22
=
q
2
⋅
q
2
d
k
;
4
:
s
c
o
r
e
11
=
e
s
c
o
r
e
11
e
s
c
o
r
e
11
+
e
s
c
o
r
e
12
,
s
c
o
r
e
12
=
e
s
c
o
r
e
12
e
s
c
o
r
e
11
+
e
s
c
o
r
e
12
;
s
c
o
r
e
21
=
e
s
c
o
r
e
21
e
s
c
o
r
e
21
+
e
s
c
o
r
e
22
,
s
c
o
r
e
22
=
e
s
c
o
r
e
22
e
s
c
o
r
e
21
+
e
s
c
o
r
e
22
5
−
6
:
z
1
=
v
1
×
s
c
o
r
e
11
+
v
2
×
s
c
o
r
e
12
;
z
2
=
v
1
×
s
c
o
r
e
21
+
v
2
×
s
c
o
r
e
22
1: q_1 = X_1 W^Q, q_2 = X_2 W^Q; \\ k_1 = X_1 W^K, k_2 = X_2 W^K;\\ v_1 = X_1 W^V, v_2 = X_2 W^V, \\ W^Q, W^K, W^K \in \mathbb{R}^{d_x \times d_k}\\ 2-3: score_{11} = \frac{q_1 \cdot q_1}{\sqrt{d_k}} , score_{12} = \frac{q_1 \cdot q_2}{\sqrt{d_k}} ; score_{21} = \frac{q_2 \cdot q_1}{\sqrt{d_k}}, score_{22} = \frac{q_2 \cdot q_2}{\sqrt{d_k}}; \\ 4: score_{11} = \frac{e^{score_{11}}}{e^{score_{11}} + e^{score_{12}}},score_{12} = \frac{e^{score_{12}}}{e^{score_{11}} + e^{score_{12}}}; score_{21} = \frac{e^{score_{21}}}{e^{score_{21}} + e^{score_{22}}},score_{22} = \frac{e^{score_{22}}}{e^{score_{21}} + e^{score_{22}}} \\ 5-6: z_1 = v_1 \times score_{11} + v_2 \times score_{12}; z_2 = v_1 \times score_{21} + v_2 \times score_{22}
1:q1=X1WQ,q2=X2WQ;k1=X1WK,k2=X2WK;v1=X1WV,v2=X2WV,WQ,WK,WK∈Rdx×dk2−3:score11=dkq1⋅q1,score12=dkq1⋅q2;score21=dkq2⋅q1,score22=dkq2⋅q2;4:score11=escore11+escore12escore11,score12=escore11+escore12escore12;score21=escore21+escore22escore21,score22=escore21+escore22escore225−6:z1=v1×score11+v2×score12;z2=v1×score21+v2×score22 下面,我们将上诉self-attention计算的6个步骤进行可视化。
第1步:对输入编码器的词向量进行线性变换得到:Query向量: ,Key向量: ,Value向量:
。这3个向量是词向量分别和3个参数矩阵相乘得到的,而这个矩阵也是是模型要学习的参数。
Q,K,V图:计算Query向量:
q
1
,
q
2
q_1, q_2
q1,q2,Key向量:
k
1
,
k
2
k_1,k_2
k1,k2
,Value向量:
v
1
,
v
2
v_1,v_2
v1,v2
其实它们就是 3 个向量,给它们加上一个名称,可以让我们更好地理解 Self-Attention 的计算过程和逻辑。attention计算的逻辑常常可以描述为:query和key计算相关或者叫attention得分,然后根据attention得分对value进行加权求和。
第2步:计算Attention Score(注意力分数)。假设我们现在计算第一个词Thinking 的Attention Score(注意力分数),需要根据Thinking 对应的词向量,对句子中的其他词向量都计算一个分数。这些分数决定了我们在编码Thinking这个词时,需要对句子中其他位置的词向量的权重。
Attention score是根据"Thinking" 对应的 Query 向量和其他位置的每个词的 Key 向量进行点积得到的。Thinking的第一个Attention Score就是
q
1
q_1
q1和
k
1
k_1
k1的内积,第二个分数就是
q
1
q_1
q1和
k
2
k_2
k2的点积。这个计算过程在下图中进行了展示,下图里的具体得分数据是为了表达方便而自定义的。
图:Thinking的Attention Score计算
- 补充:向量的内积
向量的内积(或点积)实际上并不直接对应于任何一种特定的“范数”。内积是一个数学操作,用于计算两个向量之间的相似度,或者更精确地说,它衡量一个向量在另一个向量方向上的投影的大小。内积的定义为:
a ⋅ b = ∑ i = 1 n a i 2 a\cdot b = \sum_{i=1}^{n}{a_i^2} a⋅b=∑i=1nai2
其中 a a a和 b b b是向量, a i 和 b i a_i和b_i ai和bi是他们各自的分量。
然而,内积与 L2 范数(欧几里得范数)有密切的关系。特别是,一个向量的 L2 范数的平方可以通过向量与其自身的内积来计算: ∥ a ∥ 2 2 = a ⋅ a = ∑ i = 1 n a i 2 \Vert{a}\Vert_{2}^{2} = a\cdot a=\sum_{i=1}^{n}a_{i}^2 ∥a∥22=a⋅a=∑i=1nai2
第3步:把每个分数除以
,
d
k
d_{k}
dk是Key向量的维度。你也可以除以其他数,除以一个数是为了在反向传播时,求梯度时更加稳定。->减少梯度消失和梯度爆炸的问题。
思考:为什么会导致出现梯度不稳定的情况呢?
规范化影响:当我们计算点积
s
c
o
r
e
=
q
⋅
k
score=q\cdot k
score=q⋅k 时,如果向量的维度
d
k
d_k
dk很大,点积结果可能会变得非常大。这个大的数值在经过 softmax 函数时会导致梯度消失或爆炸的问题。因为 softmax 函数
s
o
f
t
m
a
x
(
x
i
)
=
e
x
p
(
x
i
)
∑
j
e
x
p
(
x
j
)
softmax(x_i) = \frac{exp(x_i)}{\sum_{j}exp(x_j)}
softmax(xi)=∑jexp(xj)exp(xi)对于非常大的输入值非常敏感,输出的梯度可能非常小(梯度消失)或非常大(梯度爆炸)。
第4步:接着把这些分数经过一个Softmax函数,Softmax可以将分数归一化为一个概率分布,这样使得分数都是正数并且加起来等于1, 如下图所示。 这些分数决定了Thinking词向量,对其他所有位置的词向量分别有多少的注意力。
图:Thinking的Attention Score计算
第5步:得到每个词向量的分数后,将分数分别与对应的Value向量相乘。这种做法背后的直觉理解就是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放到了它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大的。
第6步:把第5步得到的Value向量相加,就得到了Self Attention在当前位置(这里的例子是第1个位置)对应的输出。
最后,在下图展示了 对第一个位置词向量计算Self Attention 的全过程。最终得到的当前位置(这里的例子是第一个位置)词向量会继续输入到前馈神经网络。
图:Thinking经过attention之后的向量表示
z
1
z_1
z1
Self-Attention矩阵计算(一次得到所有位置经过注意力信息聚合之后,即Self-Attention之后,的所有输出向量)
将self-attention计算6个步骤中的向量放一起,比如
X
=
[
x
1
;
x
2
]
X=[x_1;x_2]
X=[x1;x2],便可以进行矩阵计算啦。
下面,依旧按步骤展示self-attention的矩阵计算方法。
X
=
[
X
1
;
X
2
]
Q
=
X
W
Q
,
K
=
X
W
K
,
V
=
X
W
V
W
Q
,
W
K
,
W
K
∈
R
d
x
×
d
k
Z
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
X = [X_1;X_2] \\ Q = X W^Q, K = X W^K, V=X W^V \\ W^Q, W^K, W^K \in \mathbb{R}^{d_x \times d_k}\\ Z = softmax(\frac{QK^T}{\sqrt{d_k}}) V
X=[X1;X2]Q=XWQ,K=XWK,V=XWVWQ,WK,WK∈Rdx×dkZ=softmax(dkQKT)V
第1步:计算 Query,Key,Value 的矩阵。首先,我们把所有词向量放到一个矩阵X中,然后分别和3个权重矩阵
W
Q
,
W
K
W
V
W^Q, W^K W^V
WQ,WKWV 相乘,得到 Q,K,V 矩阵。矩阵X中的每一行,表示句子中的每一个词的词向量。Q,K,V 矩阵中的每一行表示 Query向量,Key向量,Value 向量,向量维度是
d
k
d_k
dk。
第2步:由于我们使用了矩阵来计算,我们可以把上面的第 2 步到第 6 步压缩为一步,直接得到 Self Attention 的输出。
多头注意力机制
Transformer 的论文通过增加多头注意力机制(一组注意力称为一个 attention head),进一步完善了Self-Attention。这种机制从如下两个方面增强了attention层的能力
- 它扩展了模型关注不同位置的能力。在上面的例子中,第一个位置的输出 z 1 z_1 z1包含了句子中其他每个位置的很小一部分信息,但 z 1 z_1 z1仅仅是单个向量,所以可能仅由第1个位置的信息主导了。而当我们翻译句子:The animal didn’t cross the street because it was too tired时,我们不仅希望模型关注到"it"本身,还希望模型关注到"The"和“animal”,甚至关注到"tired"。这时,多头注意力机制会有帮助。
- 多头注意力机制赋予attention层多个“子表示空间”。下面我们会看到,多头注意力机制会有多组 W Q , W K W V W^Q, W^K W^V WQ,WKWV 的权重矩阵(在 Transformer 的论文中,使用了 8 组注意力),,因此可以将 X X X变换到更多种子空间进行表示。接下来我们也使用8组注意力头(attention heads))。每一组注意力的权重矩阵都是随机初始化的,但经过训练之后,每一组注意力的权重 W Q , W K W V W^Q, W^K W^V WQ,WKWV 可以把输入的向量映射到一个对应的”子表示空间“。
图:多头注意力机制
在多头注意力机制中,我们为每组注意力设定单独的 WQ, WK, WV 参数矩阵。将输入X和每组注意力的WQ, WK, WV 相乘,得到8组 Q, K, V 矩阵。
接着,我们把每组 K, Q, V 计算得到每组的 Z 矩阵,就得到8个Z矩阵。
由于前馈神经网络层接收的是 1 个矩阵(其中每行的向量表示一个词),而不是 8 个矩阵,所以我们直接把8个子矩阵拼接起来得到一个大的矩阵,然后和另一个权重矩阵
W
O
W^O
WO相乘做一次变换,映射到前馈神经网络层所需要的维度。
图:拼接8个子矩阵并进行映射变换
总结一下就是:
把8个矩阵 {Z0,Z1…,Z7} 拼接起来
把拼接后的矩阵和WO权重矩阵相乘
得到最终的矩阵Z,这个矩阵包含了所有 attention heads(注意力头) 的信息。这个矩阵会输入到FFNN (Feed Forward Neural Network)层。
以上就是多头注意力的全部内容。最后将所有内容放到一张图中:
学习了多头注意力机制,让我们再来看下当我们前面提到的it例子,不同的attention heads (注意力头)对应的“it”attention了哪些内容。下图中的绿色和橙色线条分别表示2组不同的attentin heads:
图:it的attention
当我们编码单词"it"时,其中一个 attention head (橙色注意力头)最关注的是"the animal",另外一个绿色 attention head 关注的是"tired"。因此在某种意义上,"it"在模型中的表示,融合了"animal"和"tire"的部分表达。
Attention代码实例
下面的代码实现中,张量的第1维是 batch size,第 2 维是句子长度。代码中进行了详细注释和说明。
import torch
import torch.nn as nn
class MultiheadAttention(nn.Module): #定义一个继承nn.Module的类并实现forward方法,任何对该模块实例的直接调用(如output = attention(query, key, value))都会自动调用它的forward方法。
# n_heads:多头注意力的数量
# hid_dim:每个词输出的向量维度
def __init__(self, hid_dim, n_heads, dropout):
super(MultiheadAttention, self).__init__()
self.hid_dim = hid_dim
self.n_heads = n_heads
# 强制 hid_dim 必须整除 h
assert hid_dim % n_heads == 0 #如果不满足就会报错
# 定义 W_q 矩阵
self.w_q = nn.Linear(hid_dim, hid_dim)
# 定义 W_k 矩阵
self.w_k = nn.Linear(hid_dim, hid_dim)
# 定义 W_v 矩阵
self.w_v = nn.Linear(hid_dim, hid_dim)
self.fc = nn.Linear(hid_dim, hid_dim)
self.do = nn.Dropout(dropout)
# 缩放
self.scale = torch.sqrt(torch.FloatTensor([hid_dim // n_heads])) #整数除法
def forward(self, query, key, value, mask=None):
# 注意 Q,K,V的在句子长度这一个维度的数值可以一样,可以不一样。
# K: [64,10,300], 假设batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
# V: [64,10,300], 假设batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
# Q: [64,12,300], 假设batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
bsz = query.shape[0] #记录batch数目,这里指代句子个数
Q = self.w_q(query)
K = self.w_k(key)
V = self.w_v(value)
# 这里把 K Q V 矩阵拆分为多组注意力
# 最后一维就是是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度, 每个 head 的向量长度是:300/6=50
# 64 表示 batch size,6 表示有 6组注意力,10 表示有 10 词,50 表示每组注意力的词的向量长度
# K: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
# V: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
# Q: [64,12,300] 拆分多组注意力 -> [64,12,6,50] 转置得到 -> [64,6,12,50]
# 转置是为了把注意力的数量 6 放到前面,把 10 和 50 放到后面,方便下面计算
Q = Q.view(bsz, -1, self.n_heads, self.hid_dim //
self.n_heads).permute(0, 2, 1, 3)
K = K.view(bsz, -1, self.n_heads, self.hid_dim //
self.n_heads).permute(0, 2, 1, 3)
V = V.view(bsz, -1, self.n_heads, self.hid_dim //
self.n_heads).permute(0, 2, 1, 3)
# 第 1 步:Q 乘以 K的转置,除以scale
# [64,6,12,50] * [64,6,50,10] = [64,6,12,10]
# attention:[64,6,12,10]
attention = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale
# 如果 mask 不为空,那么就把 mask 为 0 的位置的 attention 分数设置为 -1e10,这里用“0”来指示哪些位置的词向量不能被attention到,比如padding位置,当然也可以用“1”或者其他数字来指示,主要设计下面2行代码的改动。
if mask is not None:
attention = attention.masked_fill(mask == 0, -1e10)
# 第 2 步:计算上一步结果的 softmax,再经过 dropout,得到 attention。
# 注意,这里是对最后一维做 softmax,也就是在输入序列的维度做 softmax
# attention: [64,6,12,10]
attention = self.do(torch.softmax(attention, dim=-1)) #在注意力机制中,softmax 用于确定每个值(value)的重要性。通过在此之后应用 dropout,你可以随机地忽略一部分注意力权重,这强制模型不过分依赖于任何特定的输入部分,并可能帮助模型在看到新的、未知的数据时更加鲁棒。总结来说,这段代码通过在注意力权重上实施 dropout 正则化,增加了模型处理不确定性的能力,有助于提高其泛化性。这是一种在训练复杂神经网络时常用的策略,尤其在处理大规模数据或复杂任务时效果显著。
# 第三步,attention结果与V相乘,得到多头注意力的结果
# [64,6,12,10] * [64,6,10,50] = [64,6,12,50]
# x: [64,6,12,50]
x = torch.matmul(attention, V)
# 因为 query 有 12 个词,所以把 12 放到前面,把 50 和 6 放到后面,方便下面拼接多组的结果
# x: [64,6,12,50] 转置-> [64,12,6,50]
x = x.permute(0, 2, 1, 3).contiguous() #contiguous()方法的调用很重要,尤其是在对张量进行一系列操作后,比如permute或transpose,这些操作会改变张量的内存布局(存储顺序)。contiguous()的作用是确保张量在内存中是连续存储的,这通常是执行某些操作前的必要条件。某些操作,如视图操作view(),要求张量必须是连续的。如果尝试在非连续张量上使用这些操作,会导致运行时错误。
# 这里的矩阵转换就是:把多组注意力的结果拼接起来
# 最终结果就是 [64,12,300]
# x: [64,12,6,50] -> [64,12,300]
x = x.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))
x = self.fc(x)
return x
# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(64, 12, 300)
# batch_size 为 64,有 12 个词,每个词的 Key 向量是 300 维
key = torch.rand(64, 10, 300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value = torch.rand(64, 10, 300)
attention = MultiheadAttention(hid_dim=300, n_heads=6, dropout=0.1)
output = attention(query, key, value)
## output: torch.Size([64, 12, 300])
print(output.shape)
残差链接
到目前为止,我们计算得到了self-attention的输出向量。而单层encoder里后续还有两个重要的操作:残差链接、标准化。
编码器的每个子层(Self Attention 层和 FFNN)都有一个残差连接和层标准化(layer-normalization),如下图所示。
将 Self-Attention 层的层标准化(layer-normalization)和涉及的向量计算细节都进行可视化,如下所示:
图:标准化细节
编码器和和解码器的子层里面都有层标准化(layer-normalization)。假设一个 Transformer 是由 2 层编码器和两层解码器组成的,将全部内部细节展示起来如下图所示。
图:2层Transformer示意图
编码器和和解码器的子层里面都有层标准化(layer-normalization)。假设一个 Transformer 是由 2 层编码器和两层解码器组成的,将全部内部细节展示起来如下图所示。
- 解释layer-normalization:层归一化(Layer Normalization)是一种在深度神经网络中常用的归一化技术,主要用于减少训练过程中不同层之间的内部协变量偏移(Internal Covariate Shift)。与批归一化(Batch Normalization)不同,层归一化是对单个样本的所有特征进行归一化,而不是对一个批次中的同一特征进行归一化。这使得层归一化非常适用于处理批大小为1或变化的情况,如在循环神经网络(RNN)中。
层归一化特别适合处理序列数据,例如在自然语言处理中,因为它对于每个样本独立操作,不受其他样本影响。这使得它在处理大小可变的批次或在线学习环境中尤为有用。
3.解码器
现在我们已经介绍了编码器中的大部分概念,我们也基本知道了编码器的原理。现在让我们来看下, 编码器和解码器是如何协同工作的。
编码器一般有多层,第一个编码器的输入是一个序列文本,最后一个编码器输出是一组序列向量,这组序列向量会作为解码器的K、V输入,其中K=V=编码器输出的序列向量表示。这些注意力向量将会输入到每个解码器的Encoder-Decoder Attention层,这有助于解码器把注意力集中到输入序列的合适位置,如下图所示。
解码(decoding )阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译),解码器当前时间步的输出又重新作为输入Q和编码器的输出K、V共同作为下一个时间步解码器的输入。然后重复这个过程,直到输出一个结束符。如下图所示:
解码器中的 Self Attention 层,和编码器中的 Self Attention 层的区别:
- 在解码器里,Self Attention 层只允许关注到输出序列中早于当前位置之前的单词。具体做法是:在 Self Attention 分数经过 Softmax 层之前,屏蔽当前位置之后的那些位置(将attention score设置成-inf)。
- 解码器 Attention层是使用前一层的输出来构造Query 矩阵,而Key矩阵和 Value矩阵来自于编码器最终的输出。
4.线性层和softmax
线性层就是一个普通的全连接神经网络,可以把解码器输出的向量,映射到一个更大的向量,这个向量称为 logits 向量:假设我们的模型有 10000 个英语单词(模型的输出词汇表),此 logits 向量便会有 10000 个数字,每个数表示一个单词的分数。
然后,Softmax 层会把这些分数转换为概率(把所有的分数转换为正数,并且加起来等于 1)。然后选择最高概率的那个数字对应的词,就是这个时间步的输出单词。
图:线性层
5损失函数
Transformer训练的时候,需要将解码器的输出和label一同送入损失函数,以获得loss,最终模型根据loss进行方向传播。这一小节,我们用一个简单的例子来说明训练过程的loss计算:把“merci”翻译为“thanks”。
我们希望模型解码器最终输出的概率分布,会指向单词 ”thanks“(在“thanks”这个词的概率最高)。但是,一开始模型还没训练好,它输出的概率分布可能和我们希望的概率分布相差甚远,如下图所示,正确的概率分布应该是“thanks”单词的概率最大。但是,由于模型的参数都是随机初始化的,所示一开始模型预测所有词的概率几乎都是随机的。
图:概率分布
只要Transformer解码器预测了组概率,我们就可以把这组概率和正确的输出概率做对比,然后使用反向传播来调整模型的权重,使得输出的概率分布更加接近整数输出。
那我们要怎么比较两个概率分布呢?:我们可以简单的用两组概率向量的的空间距离作为loss(向量相减,然后求平方和,再开方),当然也可以使用交叉熵(cross-entropy)]和KL 散度(Kullback–Leibler divergence)。读者可以进一步检索阅读相关知识,损失函数的知识不在本小节展开。
由于上面仅有一个单词的例子太简单了,我们可以再看一个复杂一点的句子。句子输入是:“je suis étudiant” ,输出是:“i am a student”。这意味着,我们的transformer模型解码器要多次输出概率分布向量:
每次输出的概率分布都是一个向量,长度是 vocab_size(前面约定最大vocab size,也就是向量长度是 6,但实际中的vocab size更可能是 30000 或者 50000)
第1次输出的概率分布中,最高概率对应的单词是 “i”
第2次输出的概率分布中,最高概率对应的单词是 “am”
以此类推,直到第 5 个概率分布中,最高概率对应的单词是 “”,表示没有下一个单词了
(多个词,计算损失后求和,通常不需要对每个词的损失取平均;如果只有一个句子,这一步可以是可选的,取决于是否要归一化损失。但在实际应用中,通常处理的是一个批次(batch)的数据,每个批次包含多个句子。
在一个批次的所有句子的损失求和之后,通常还需要对批次中的句子数量取平均。这样做的原因是要确保损失值不会随着批次中句子数量的变化而变化,使得模型训练更稳定。)
图:目标概率分布
我们用例子中的句子训练模型,希望产生图中所示的概率分布 我们的模型在一个足够大的数据集上,经过足够长时间的训练后,希望输出的概率分布如下图所示:
图:模型训练后输出的多个概率分布
我们希望模型经过训练之后可以输出的概率分布也就对应了正确的翻译。当然,如果你要翻译的句子是训练集中的一部分,那输出的结果并不能说明什么。我们希望模型在没见过的句子上也能够准确翻译。
额外提一下greedy decoding和beam search的概念:
Greedy decoding:由于模型每个时间步只产生一个输出,我们这样看待:模型是从概率分布中选择概率最大的词,并且丢弃其他词。这种方法叫做贪婪解码(greedy decoding)。
Beam search:每个时间步保留k个最高概率的输出词,然后在下一个时间步,根据上一个时间步保留的k个词来确定当前应该保留哪k个词。假设k=2,第一个位置概率最高的两个输出的词是”I“和”a“,这两个词都保留,然后根据第一个词计算第2个位置的词的概率分布,再取出第2个位置上2个概率最高的词。对于第3个位置和第4个位置,我们也重复这个过程。这种方法称为集束搜索(beam search)。
2.3 图解bert
bert模型通过预训练和微调的方式应用于下游任务
站在2021年来看,2018年是自然语言处理技术的一个转折点,运用深度学习技术处理文本的能力通过预训练模型被极大的发挥了出来。同时,伴随着NLP开源社区的贡献,很多强大的模型被封装成组件,让NLP初学者也有机会在各种NLP任务上取得非常好的效果。在众多NLP预训练模型里,最经典的基本就是BERT和GPT了,因此本文将开始对BERT(单篇文章的citation已经接近2万)的学习。
BERT在2018年被提出,BERT模型一出现就打破了多个自然语言处理任务的最好记录。BERT的论文发布不久后,BERT团队公开了模型的代码,并提供了基于大规模新书数据集预训练完成的模型下载。BERT的模型代码和模型参数的开源,使得任何一个NLP从业者,都可以基于这个强大的模型组件搭建自己的NLP系统,也节省了从零开始训练语言处理模型所需要的时间、精力、知识和资源。
模型结构
了解了如何使用BERT,接下来让我们更深入地了解一下它的工作原理。BERT原始论文提出了BERT-base和BERT—large两个模型,base的参数量比large少一些,可以形象的表示为下图的样子。
图:BERT base和large
回顾一下篇章2.2的Transformer,BERT模型结构基本上就是Transformer的encoder部分,BERT-base对应的是12层encoder,BERT-large对应的是24层encoder。
图:BERT-base为12层的encoder
模型输入
接着看一下模型的输入和输出:**BERT模型输入有一点特殊的地方是在一句话最开始拼接了一个[CLS] token,如下图所示。这个特殊的[CLS] token经过BERT得到的向量表示通常被用作当前的句子表示。**除了这个特殊的[CLS] token,其余输入的单词类似篇章2.2的Transformer。BERT将一串单词作为输入,这些单词在多层encoder中不断向上流动,每一层都会经过 Self-Attention和前馈神经网络。
模型输出
BERT输入的所有token经过BERT编码后,会在每个位置输出一个大小为 hidden_size(在 BERT-base中是 768)的向量。
图:BERT output
对于上面提到的句子分类的例子,我们直接使用第1个位置的向量输出(对应的是[CLS])传入classifier网络,然后进行分类任务,如下图所示。
图:BERT 接分类器
Bert的预训练任务:Masked Language Model
知道了模型输入、输出、Transformer结构,那么BERT是如何无监督进行训练的呢?如何得到有效的词、句子表示信息呢?以往的NLP预训练通常是基于语言模型进行的,比如给定语言模型的前3个词,让模型预测第4个词。但是,BERT是基于Masked language model进行预训练的:将输入文本序列的部分(15%)单词随机Mask掉,让BERT来预测这些被Mask的词语。如下图所示:
图: BERT mask
这种训练方式最早可以追溯到Word2Vec时代,典型的Word2Vec算法便是:基于词C两边的A、B和D、E词来预测出词C。
bert预训练任务2:相邻句子判断
除了masked language model,BERT在预训练时,还引入了一个新的任务:判断两个句子是否是相邻句子。如下图所示:输入是sentence A和sentence B,经过BERT编码之后,使用[CLS] token的向量表示来预测两个句子是否是相邻句子。
除了masked language model,BERT在预训练时,还引入了一个新的任务:判断两个句子是否是相邻句子。如下图所示:输入是sentence A和sentence B,经过BERT编码之后,使用[CLS] token的向量表示来预测两个句子是否是相邻句子。
图: 2个句子任务
注意事项:为了本文的描述方便,在前面的叙述中,均省略了BERT tokenize的过程,但读者朋友需要注意BERT实际上使用的是WordPieces作为最小的处理单元(采用的是wordpiece算法分词):token,而不是使用单词本身。在 WordPiece中,有些词会被拆分成更小的部分。关于WordPiece分词,本文不过多展开,感兴趣的读者可以阅读和学习subword tokenizer。另外,判断两个句子是否相邻这个任务在后来的研究中逐渐被淡化了,比如roberta模型在被提出的时候就不再使用该任务进行预训练了。
注意事项:为了本文的描述方便,在前面的叙述中,均省略了BERT tokenize的过程,但读者朋友需要注意BERT实际上使用的是WordPieces作为最小的处理单元(采用的是wordpiece算法分词):token,而不是使用单词本身。在 WordPiece中,有些词会被拆分成更小的部分。关于WordPiece分词,本文不过多展开,感兴趣的读者可以阅读和学习subword tokenizer。另外,判断两个句子是否相邻这个任务在后来的研究中逐渐被淡化了,比如roberta模型在被提出的时候就不再使用该任务进行预训练了。
BERT的应用
BERT论文展示了BERT在多种任务上的应用,如下图所示。可以用来判断两个句子是否相似,判断单个句子的情感,用来做抽取式问答,用来做序列标注。
Bert用于特征提取
由于BERT模型可以得到输入序列所对应的所有token的向量表示,因此不仅可以使用最后一层BERT的输出连接上任务网络进行微调,还可以直接使用这些token的向量当作特征。比如,可以直接提取每一层encoder的token表示当作特征,输入现有的特定任务神经网络中进行训练。
图: BERT特征提取
那么我们是使用最后一层的向量表示,还是前几层的,还是都使用呢?下图给出了一种试验结果:
图: BERT特征选择
扩展阅读
- 为什么基于RNN的网络在处理长文本时遇到了挑战?
基于RNN(Recurrent Neural Networks)的网络在处理长文本时面临多种挑战,主要源于RNN的结构和工作机制。以下是一些关键的挑战和原因:
1. 梯度消失和梯度爆炸
原因:RNN在训练过程中依赖于时间步的反向传播(通过时间的反向传播,BPTT)。当序列很长时,长期依赖导致反向传播过程中梯度变得非常小(梯度消失)或非常大(梯度爆炸),这使得网络难以学习。
影响:梯度消失会导致网络无法有效地更新其权重,尤其是影响序列开始部分的权重,因此学习远程依赖变得非常困难。梯度爆炸则可能导致训练过程不稳定,网络权重的更新过度,使得学习过程发散。
2. 长期依赖问题
原因:尽管RNN理论上可以处理任何长度的序列数据,但在实践中,RNN难以学习到序列中的长期依赖关系(即序列早期的信息对序列后期的影响)。
影响:这意味着对于长文本,RNN可能无法记住并利用文本开始部分的信息,特别是当这些信息对理解或预测序列的后续部分至关重要时。
3. 计算效率
原因:RNN必须按序列顺序处理数据,因为当前的输出依赖于前一时间步的隐藏状态。这种顺序依赖性导致无法并行处理序列中的数据。
影响:对于长文本,这种顺序处理方式导致训练和推理速度慢,尤其是在处理大量数据或需要实时响应的应用场景中,效率成为一个显著的问题。
4. 存储限制
原因:在处理长序列时,RNN需要在每个时间步保存信息,这可能导致随着序列长度的增加,存储需求显著增加。
影响:这不仅增加了计算资源的负担,还可能限制了模型处理极长序列的能力。
解决方案
为了克服这些挑战,研究者们开发了几种改进的RNN架构,如长短时记忆网络(LSTM)和门控循环单元(GRU)。这些架构通过引入门控机制来调节信息流,以解决梯度消失问题并更好地捕捉长期依赖。此外,Transformer模型通过使用自注意力机制,使得模型能够同时处理序列中的所有元素,从而提高了处理长文本时的性能和效率。
- 为什么Transformer 可以比 LSTM 更好地处理长期依赖?
Transformer 模型在处理长期依赖问题上表现优于 LSTM 主要得益于其独特的结构和工作机制,特别是利用自注意力(Self-Attention)机制。以下是 Transformer 比 LSTM 在处理长期依赖时更加有效的几个关键原因:
1. 全局依赖建模能力
自注意力机制:Transformer 通过自注意力机制可以直接计算序列中任何两个位置之间的关系,而不需要像 RNN 或 LSTM 那样逐步通过隐藏状态传递信息。这允许 Transformer 在单个操作中捕捉长距离依赖,而无需担心随时间推移梯度消失或爆炸的问题。
直接距离联系:在 LSTM 中,序列早期的信息需要经过多个时间步传递到序列后面,这导致信息可能会在传递过程中逐渐衰减或丢失。Transformer 的自注意力机制允许模型直接“看到”序列中任何位置的信息,即使这些位置相隔非常远。
2. 并行处理能力
非顺序处理:LSTM 必须按时间顺序处理数据,因为每个步骤的输出依赖于前一个时间步的隐藏状态。相比之下,Transformer 可以同时处理整个序列的所有元素,因为自注意力层不依赖于序列中的时间步顺序。这种并行性使得 Transformer 在训练时比基于 RNN 的模型效率更高,尤其是在处理长序列时。
计算效率:由于 Transformer 可以并行处理数据,这减少了训练时间,使模型能够快速调整并优化处理长距离依赖的能力。
3. 灵活的上下文捕捉
可配置的注意力权重:Transformer 的每个头在自注意力层可以学习到不同的上下文关注点,这使得模型能够同时从多个抽象层次捕捉信息,从而更全面地理解和整合长序列中的信息。
动态上下文调整:在 Transformer 中,注意力机制的权重是基于序列的实际内容动态计算的,而非由固定的递归模式决定。这使得模型在处理具有复杂内部结构的长序列时更为灵活和有效。
4. 梯度流的稳定性
避免梯度问题:由于 Transformer 不依赖于时间递归,它不受传统 RNN 模型中常见的梯度消失和梯度爆炸问题的困扰。这种稳定性对于学习深层网络和长序列中的复杂模式至关重要。
总结来说,Transformer 通过其自注意力机制提供了一种有效的方法来直接和并行地处理序列中的长期依赖,相比于 LSTM,这使其在处理长文本或具有复杂依赖结构的序列数据时更为有效和高效。这些优点使 Transformer 成为了许多自然语言处理任务和其他需要处理长序列数据的应用的首选模型。