基于Transformers的自然语言处理(NLP)入门(一)

本文为参加Datawhale组队学习时所写,如若需了解细致内容,请去到Datawhale官方开源课程基于transformers的自然语言处理(NLP)入门 (datawhalechina.github.io)

前言

常见的NLP任务

  • 文本分类 对单个,两个或者多段文本进行分类

  • 序列标注 对文本序列中的token,字或者词进行分类

  • 问答任务

    • 抽取式问答 从一段给定的文本之中找到答案
    • 多选式回答 从多个选项中选出一个正确答案
  • 生成任务

    • 语言模型 根据已有的一段文字生成一个字
    • 摘要生成 根据一大段文字生成一小段总结性文字
    • 机器翻译 将源语言翻译成为目标语言

Transformer的兴起

2017年,Transformer模型结构首次被提出并在机器翻译上取得了效果。2018年Bert模型使用Transformer模型结构进行大规模语言预训练,刷新了许多NLP任务榜单的最高分。2019年-2021年,研究人员将Transformer这种模型结构预训练+微调这种方式结合,提出了一系列Transformer模型结构,训练方法的改进。Transformer的模型结构优异。使得其参数量可以非常庞大从而容纳更多的信息,随着预训练不断提升以及近年来计算能力的提升,越来越好的Transformer不断涌现。

Number of parameters of recent Transformers models

Transformer相关原理

图解Attention

基于循环神经网络(RNN)的一类seq2seq模型,在处理长文本时遇到了挑战,对于长文本中不同位置的信息进行attention(注意力机制)有助于提高RNN的模型效果。

seq2seq框架

seq2seq(sequence to sequence)是一中常见的NLP模型结构,“序列到序列”。从一个文本得到一个新的文本。

seq2seq细节

seq2seq模型由编码器和解码器组成。编码器会处理输入序列中的每个元素并获得输入信息,这些信息会被转换成为一个向量(称为context向量)。当我们处理完整个输入序列后,编码器把 context向量 发送给解码器,解码器通过context向量中的信息,逐个元素输出新的序列。

eq2seq模型中的编码器和解码器一般采用的是循环神经网络RNN。

context向量对应上图中间浮点数向量。在下文中,我们会可视化这些数字向量,使用更明亮的色彩来表示更高的值,如上图右边所示

context向量本质上是一组浮点数,context的数组长度是基于编码层的RNN的隐藏层神经元数量的。

RNN如何处理输入序列?

  1. 序列输入时一个句子,setence = {w1, w2, ..., wn}
  2. RNN首先将句子中的每一个词映射成为一个向量得到一个向量序列,X = {x1, x2, ..., xn}
  3. 然后在处理第t∈[1,n]个时间步的序列输入xt时,RNN网络的输入和输出可以表示为:ht= RNN(xt, ht)
    • 输入:RNN在时间步t的输入之一为单词wt经过映射得到的向量xt
    • 输入:RNN另一个输入为上一个时间步t-1得到的hidden state向量ht-1,同样是一个向量。
    • 输出:RNN在时间步t的输出为ht hidden state向量。

上图左边每个单词经过word embedding算法之后得到中间一个对应的4维的向量。

编码器在每一个是时间步得到hidden state,并将最终的state传输给解码器,解码器根据编码器给予的最后一个hidden state信息解码处输出序列,最后一个 hidden state实际上是我们上文提到的context向量。

Attention

基于RNN的seq2seq模型编码器所有信息都编码到了一个context向量中,便是这类模型的瓶颈。一方面单个向量很难包含所有文本序列的信息,另一方面RNN递归地编码文本序列使得模型在处理长文本时面临非常大的挑战。

attention注意力机制,使得seq2seq模型可以有区分度、有重点地关注输入序列。

图:在第 7 个时间步,注意力机制使得解码器在产生英语翻译student英文翻译之前,可以将注意力集中在法语输入序列的:étudiant。这种有区分度得attention到输入序列的重要信息,使得模型有更好的效果。

带有注意力的seq2seq模型

  • 编码器把更多的数据传递给解码器,而不是只传递最后的hidden state
  • 注意力模型的解码器在产生输出之前,做了一个额外的attention处理。如下图所示,具体为:
    • 由于编码器中每个 hidden state(隐藏层状态)都对应到输入句子中一个单词,那么解码器要查看所有接收到的编码器的 hidden state(隐藏层状态)。
    • 给每个 hidden state(隐藏层状态)计算出一个分数(我们先忽略这个分数的计算过程)。
    • 所有hidden state(隐藏层状态)的分数经过softmax进行归一化。
    • 将每个 hidden state(隐藏层状态)乘以所对应的分数,从而能够让高分对应的 hidden state(隐藏层状态)会被放大,而低分对应的 hidden state(隐藏层状态)会被缩小。
    • 将所有hidden state根据对应分数进行加权求和,得到对应时间步的context向量。

seq2seq模型解码器全流程

  1. 注意力模型的解码器 RNN 的输入包括:一个word embedding 向量,和一个初始化好的解码器 hidden state,图中是hinit
  2. RNN 处理上述的 2 个输入,产生一个输出和一个新的 hidden state,图中为h4。
  3. 注意力的步骤:我们使用编码器的所有 hidden state向量和 h4 向量来计算这个时间步的context向量(C4)。
  4. 我们把 h4 和 C4 拼接起来,得到一个橙色向量。
  5. 我们把这个橙色向量输入一个前馈神经网络(这个网络是和整个模型一起训练的)。
  6. 根据前馈神经网络的输出向量得到输出单词:假设输出序列可能的单词有N个,那么这个前馈神经网络的输出向量通常是N维的,每个维度的下标对应一个输出单词,每个维度的数值对应的是该单词的输出概率。
  7. 在下一个时间步重复1-6步骤。

图解Transformer

Transoformer整体结构图,左半部分为编码器(encoder),右半部分为解码器(decoder)。

Transformer宏观结构

Transformer最开始提出来解决机器翻译任务,因此可以看作是seq2seq模型的一种。

编码器和解码器都是多层组成的。层数并不固定,每一层编码器和解码器结构是一样的,不同层编码器和解码器不共享参数。

单层encoder由两部分组成

  • Self-Attention Layer
  • Feed Forward Neural Network (前馈神经网络,FFNN)

编译器的输入文本序列最开始经过embedding转换,得到每一个单词的向量表示,然后经过self-attention层进行变换和信息交互得到新的向量表示,self-attention处理一个词时,不仅会使用这个词本身,还会使用上下文的信息,后通过FFNN得到新的向量进入下一层,这一过程中,单词的向量表示的维度并不会发生改变。

解码器在self-attention和FFNN中间插入了一个Encoder-Decoder Attention层,这一层帮助解码器聚焦于输入序列中最相关的部分,使得解码器在不同的时候注意的对象不同。

Transformer结构细节

输入处理

词向量

和常见的NLP 任务一样,我们首先会使用词嵌入算法(embedding algorithm),将输入文本序列的每个词转换为一个词向量。实际应用中的向量一般是 256 或者 512 维。

假设我们的输入文本是序列包含了3个词,那么每个词可以通过词嵌入算法得到一个4维向量(简化过程),于是整个输入被转化成为一个向量序列。

如果每一个句子的长度不一样,我们会选择一个合适的长度,对于较短的句子进行填充(padding),对于较长的句子进行截断。

位置向量

Transformer模型对每个输入的词向量都加上了一个位置向量。这些向量有助于确定每个单词的位置特征,或者句子中不同单词之间的距离特征,可以为模型提供更多有意义的信息,比如词的位置,词之间的距离等。

上面表达式中的pospos代表词的位置,dmodel代表位置向量的维度,i∈[0,dmodel)代表位置dmodel维位置向量第i维。

这种方法的优点是:可以扩展到未知的序列长度。

编码器encoder

编码部分的输入文本序列经过输入处理之后得到了一个向量序列,这个向量序列将被送入第1层编码器,第1层编码器输出的同样是一个向量序列,再接着送入下一层编码器:第1层编码器的输入是融合位置向量的词向量,更上层编码器的输入则是上一层编码器的输出

Self-attention层

我们要翻译的句子为:

1
The animal didn't cross the street because it was too tired

这个句子中it这一个指代词的具体含义是什么?

RNN 在处理序列中的一个词时,会考虑句子前面的词传过来的hidden state,而hidden state就包含了前面的词的信息;而Self Attention机制值得是,当前词会直接关注到自己句子中前后相关的所有词语。

self-attention细节

假设一句话包含两个单词:Thinking Machines。自注意力的一种理解是:Thinking-Thinking,Thinking-Machines,Machines-Thinking,Machines-Machines,共22种两两attention。

  • 第1步:对输入编码器的词向量进行线性变换得到:Query向量: q1,q2,Key向量: k1,k2,Value向量: v1,v2。这3个向量是词向量分别和3个参数矩阵相乘得到的,而这个矩阵也是是模型要学习的参数。

attention计算的逻辑常常可以描述为:query和key计算相关或者叫attention得分,然后根据attention得分对value进行加权求和。

  • 第2步:计算Attention Score(注意力分数)。假设我们现在计算第一个词Thinking 的Attention Score(注意力分数),需要根据Thinking 对应的词向量,对句子中的其他词向量都计算一个分数。

    这些分数决定了我们在编码Thinking这个词时,需要对句子中其他位置的词向量的权重。Attention score是根据"Thinking" 对应的 Query 向量和其他位置的每个词的 Key 向量进行点积得到的。Thinking的第一个Attention Score就是q1和k1的内积,第二个分数就是q1和k2的点积。这个计算过程在下图中进行了展示,下图里的具体得分数据是为了表达方便而自定义的。

  • 第3步:把每个分数除以\(\sqrt{d_k}\),dk是Key向量的维度。你也可以除以其他数,除以一个数是为了在反向传播时,求梯度时更加稳定。
  • 第4步:接着把这些分数经过一个Softmax函数,Softmax可以将分数归一化,这样使得分数都是正数并且加起来等于1, 如下图所示。 这些分数决定了Thinking词向量,对其他所有位置的词向量分别有多少的注意力。
  • 第5步:得到每个词向量的分数后,将分数分别与对应的Value向量相乘。这种做法背后的直觉理解就是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放到了它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大的。

  • 第6步:把第5步得到的Value向量相加,就得到了Self Attention在当前位置(这里的例子是第1个位置)对应的输出。

  • 最后,在下图展示了 对第一个位置词向量计算Self Attention 的全过程。最终得到的当前位置(这里的例子是第一个位置)词向量会继续输入到前馈神经网络。注意:上面的6个步骤每次只能计算一个位置的输出向量,在实际的代码实现中,Self Attention的计算过程是使用矩阵快速计算的,一次就得到所有位置的输出向量。

self-attention矩阵计算

将self-attention计算6个步骤中的向量放一起,比如\(X=[x_1;x_2]X=[x1;x2]\),便可以进行矩阵计算啦。充分利用GPU的优势,加快速度。

多头注意力机制

  • 它扩展了模型关注不同位置的能力。当我们翻译句子:The animal didn’t cross the street because it was too tired时,我们不仅希望模型关注到"it"本身,还希望模型关注到"The"和“animal”,甚至关注到"tired"。这时,多头注意力机制会有帮助
  • 多头注意力机制赋予attention层多个子空间表示。多头注意力机制会有多组权重矩阵,因此可以将X变换到更多种子空间进行表示。

由于前馈神经网络层接收的是 1 个矩阵(其中每行的向量表示一个词),而不是 8 个矩阵,所以我们直接把8个子矩阵拼接起来得到一个大的矩阵,然后和另一个权重矩阵W^OWO相乘做一次变换,映射到前馈神经网络层所需要的维度。

运用多头注意力机制后

Attention代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class MultiheadAttention(nn.Module):
# 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.FLoaTensor([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 维
bsz = query.shape[0]
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))

# 第三步,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()
# 这里的矩阵转换就是:把多组注意力的结果拼接起来
# 最终结果就是 [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 层和 FFNN)都有一个残差连接和层标准化(layer-normalization),如下图所示。

假设一个 Transformer 是由 2 层编码器和两层解码器组成的,将全部内部细节展示起来如下图所示。

解码器

编码器一般有多层,第一个编码器的输入是一个序列文本,最后一个编码器输出是一组序列向量,这组序列向量会作为解码器的K、V输入,其中K=V=解码器输出的序列向量表示。这些注意力向量将会输入到每个解码器的Encoder-Decoder Attention层,这有助于解码器把注意力集中到输入序列的合适位置

解码(decoding )阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译),解码器当前时间步的输出又重新作为输入Q和编码器的输出K、V共同作为下一个时间步解码器的输入。然后重复这个过程,直到输出一个结束符。

解码器中的 Self Attention 层,和编码器中的 Self Attention 层的区别:

  1. 在解码器里,Self Attention 层只允许关注到输出序列中早于当前位置之前的单词。具体做法是:在 Self Attention 分数经过 Softmax 层之前,屏蔽当前位置之后的那些位置(将attention score设置成-inf)。
  2. 解码器 Attention层是使用前一层的输出来构造Query 矩阵,而Key矩阵和 Value矩阵来自于编码器最终的输出。

线性层和softmax

线性层就是一个普通的全连接神经网络,可以把解码器输出的向量,映射到一个更大的向量,这个向量称为 logits 向量:假设我们的模型有 10000 个英语单词(模型的输出词汇表),此 logits 向量便会有 10000 个数字,每个数表示一个单词的分数。

然后,Softmax 层会把这些分数转换为概率(把所有的分数转换为正数,并且加起来等于 1)。然后选择最高概率的那个数字对应的词,就是这个时间步的输出单词。

损失函数

Transformer训练的时候,需要将解码器的输出和label一同送入损失函数,以获得loss,最终模型根据loss进行方向传播。

只要Transformer解码器预测了组概率,我们就可以把这组概率和正确的输出概率做对比,然后使用反向传播来调整模型的权重,使得输出的概率分布更加接近整数输出。

  • Greedy decoding:由于模型每个时间步只产生一个输出,我们这样看待:模型是从概率分布中选择概率最大的词,并且丢弃其他词。这种方法叫做贪婪解码(greedy decoding)。
  • Beam search:每个时间步保留k个最高概率的输出词,然后在下一个时间步,根据上一个时间步保留的k个词来确定当前应该保留哪k个词。假设k=2,第一个位置概率最高的两个输出的词是”I“和”a“,这两个词都保留,然后根据第一个词计算第2个位置的词的概率分布,再取出第2个位置上2个概率最高的词。对于第3个位置和第4个位置,我们也重复这个过程。这种方法称为集束搜索(beam search)。

基于Transformers的自然语言处理(NLP)入门(一)
https://www.spacezxy.top/2021/09/13/nlp-transformer/nlp-transformer-1/
作者
Xavier ZXY
发布于
2021年9月13日
许可协议