注意力


心理学描述

感谢您关注本博客,因为本博客成功吸引了您的注意力。

注意力机制(Attention Machanism)是一种允许模型在处理信息时专注于关键部分,忽略不相关信息,从而提高处理效率和准确性的机制。它模仿了人类视觉处理信息时选择性关注的特点。

当人类的视觉机制识别一个场景时,通常不会全面扫描整个场景,而是根据兴趣或需求集中关注特定的部分。注意力是稀缺的,而环境中的干扰注意力的信息却并不少。

美国心理学之父 William James 在19世纪90年代指出,受试者基于非自主性提示自主性提示有选择地引导注意力的焦点。非自主性提示是基于环境中物体的突出性和易见性;自主性提示受主观认知和意识的控制。

下侧右图的红色区域表示了人类在观察左图时最先注意到的部分,从而识别出这是一只狼。

机器学习的应用

在传统的序列处理模型中,如循环神经网络(RNN)和长短时记忆网络(LSTM),捕捉长距离依赖关系是一个难题。因为随着序列长度的增加,模型很容易丢失早期输入的信息。

自主性的与非自主性的注意力提示解释了人类的注意力的方式,下面来看看如何通过这两种注意力提示,用神经网络来设计注意力机制的框架。现在考虑一个相对简单的状况,即只使用非自主性提示。由于非自主性提示不依赖于主观意志,因此可以简单使用参数化的全连接层或非参数化的池化层。

现在考虑如何引入自主性提示机制。在注意力机制的背景下,自主性提示被称为查询(Query)。给定任何查询,注意力机制通过注意力汇聚(Attention Pooling)将选择引导至感官输入(Sensory Inputs)。这些感官输入也被称为(Value)。更通俗的解释是,每个值都与一个(Key)配对,这可以想象为感官输入的非自主提示。

下图的架构通过设计注意力汇聚的方式,将给定的查询(自主性提示)与键(非自主性提示)进行匹配,这将引导得出最匹配的值(感官输入)。请读者牢记这一框架设计。

注意力框架

注意力汇聚

上节介绍了框架下的注意力机制的主要成分:查询(自主提示)和键(非自主提示)之间的交互形成了注意力汇聚; 注意力汇聚有选择地聚合了值(感官输入)以生成最终的输出。

为了更好的理解注意力这一概念,我们考虑一个简单的回归问题。对于给定的成对“输入-输出”数据集

{(x1,y1),,(xn,yn)}\{(x_1,y_1),\cdots,(x_n,y_n)\}

我们的目标是让机器学习函数 ff,来预测任意新输入 xx 的输出 y^=f(x)\hat{y}=f(x)

先考虑一种很笨的方法,我们采用平均汇聚估计输出,即有函数

f(x)=1ni=1nyif(x)=\frac{1}{n}\sum_{i=1}^ny_i

这样做显然会带来巨大的误差,因为其忽略了数据集中的输入 xix_i。据此,1964年 NadarayaWatson 提出了一个更好的想法:根据输入的位置对输出进行加权。他们提出了改良版的汇聚函数

f(x)=i=1n(K(xxi)j=1nK(xxj)yi)f(x)=\sum_{i=1}^n\left(\frac{K(x-x_i)}{\\\displaystyle\sum_{j=1}^n K(x-x_j)}y_i\right)

其中 K(x)K(x) 表示核函数,上式描述的回归模型称作Nadaraya-Watson核回归(Nadaraya-Watson Kernel Regression),而我们不做进一步讨论。然而,同常规的非线性回归模型不同,该模型不依赖于任何参数,且其有效结合了输入 xix_i 得以拟合更复杂的非线性关系。受此启发,我们可以从注意力框架的角度给出一个更通用的汇聚函数

f(x)=i=1nα(x,xi)yif(x)=\sum_{i=1}^n \alpha(x,x_i)y_i

上式被称作注意力汇聚公式(Attention Pooling)。根据注意力框架,xx 表示查询,(xi,yi)(x_i,y_i) 表示键值对,而注意力汇聚公式本质上是对 yiy_i 求加权平均。我们将查询 xx 和键 xix_i 的关系建模为注意力权重(Attention Weight),即 α(x,xi)\alpha(x,x_i),这些权重被分配给对应值 yiy_i,满足非负性归一性

我们举一个例子,假设注意力汇聚公式为Nadaraya-Watson核回归形式,且定义高斯核函数

K(x)=12πex22K(x)=\frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}

代入上式不难得到

f(x)=i=1nsoftmax(12(xxi)2)yif(x)=\sum_{i=1}^n\mathrm{softmax}\left(-\frac{1}{2}(x-x_i)^2\right)y_i

这是一个非参数回归模型,我们也可以在 xxix-x_i 距离上乘以可学习参数 ww,得到一个参数回归模型。

注意力评分函数


评分函数

假设有查询 qRq\boldsymbol{q}\in\mathbb{R}^qmm 个键值对 (ki,vi)(\boldsymbol{k}_i,\boldsymbol{v}_i),其中 kiRk,viRv\boldsymbol{k}_i\in\mathbb{R}^k,\boldsymbol{v}_i\in\mathbb{R}^v。注意力汇聚函数可表示为

f(q;(k1,v1),,(km,vm))=i=1mα(q,ki)viRvf\big(\boldsymbol{q};(\boldsymbol{k}_1,\boldsymbol{v}_1),\cdots,(\boldsymbol{k}_m,\boldsymbol{v}_m)\big)=\sum_{i=1}^m\alpha(\boldsymbol{q},\boldsymbol{k}_i)\boldsymbol{v}_i\in\mathbb{R}^v

注意力权重 α(q,ki)\alpha(\boldsymbol{q},\boldsymbol{k}_i) 可以看做一种概率表示,我们不难联想到 Softmax\mathrm{Softmax} 操作。定义注意力评分函数(Attention Scoring Function),简称评分函数并记作 aa。因此注意力权重是通过注意力评分函数 aa 将向量 q,ki\boldsymbol{q},\boldsymbol{k}_i 映射为标量,再经过 Softmax\mathrm{Softmax} 操作得到的。

α(q,ki)=softmax((q,ki))=exp(a(q,ki))j=1mexp(a(q,kj))R\alpha(\boldsymbol{q},\boldsymbol{k}_i)=\mathrm{softmax}\big((\boldsymbol{q},\boldsymbol{k}_i)\big)=\frac{\exp\big(a(\boldsymbol{q},\boldsymbol{k}_i)\big)}{\displaystyle\sum_{j=1}^m\exp\big(a(\boldsymbol{q},\boldsymbol{k}_j)\big)}\in\mathbb{R}

下图展示了注意力汇聚函数的运算流程
注意力评分

因此,选择不同的注意力评分函数 aa 会导致不同的注意力汇聚操作。本节将介绍几个常用的评分函数,稍后将用他们来实现更复杂的注意力机制以及大名鼎鼎的Transformer模型

1
2
3
4
import math
import torch
from torch import nn
from d2l import torch as d2l

掩蔽softmax操作

在介绍常用的评分函数前,先介绍遮蔽softmax操作

正如上面提到的,Softmax\mathrm{Softmax} 操作用于输出一个概率分布作为注意力权重。但在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。例如,为了在机器翻译任务中高效处理小批量数据集,某些文本序列被填充了没有意义的特殊词元 <pad>。为了仅将有意义的词元作为值来获取注意力汇聚,可以指定一个有效序列长度(即词元的个数),以便在计算 Softmax\mathrm{Softmax} 时过滤掉超出指定范围的位置。

定义 masked_softmax 函数,使得超过有效长度位置的编码被设置遮蔽为 00

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def masked_softmax(X, valid_lens):
"""通过在最后一个轴上掩蔽元素来执行softmax操作"""
# X:3D张量,valid_lens:1D或2D张量
if valid_lens is None:
return nn.functional.softmax(X, dim=-1)
else:
shape = X.shape
if valid_lens.dim() == 1:
valid_lens = torch.repeat_interleave(valid_lens, shape[1])
else:
valid_lens = valid_lens.reshape(-1)
# 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
value=-1e6)
return nn.functional.softmax(X.reshape(shape), dim=-1)

加性注意力

当查询和键是不同长度的矢量时,可以使用加性注意力(Additive Attention)作为评分函数,其形式为

a(q,ki)=wvTtanh(Wqq+Wkki)a(\boldsymbol{q},\boldsymbol{k}_i)=\boldsymbol{w}^T_v\tanh(\boldsymbol{W}_q\boldsymbol{q}+\boldsymbol{W}_k\boldsymbol{k}_i)

其中 WqRh×q,WkRh×k,wvRh\boldsymbol{W}_q\in\mathbb{R}^{h\times q},\boldsymbol{W}_k\in\mathbb{R}^{h\times k},\boldsymbol{w}_v\in\mathbb{R}^h 是可学习参数。加性注意力事实上可以这样理解:把查询 q\boldsymbol{q} 和键 ki\boldsymbol{k}_i 连结起来输入到一个单隐藏层MLP中,其隐藏单元数为 hh,并使用 tanh\tanh 作为激活函数输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
self.w_v = nn.Linear(num_hiddens, 1, bias=False)
self.dropout = nn.Dropout(dropout)

def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数,1,num_hidden)
# key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
# 使用广播方式进行求和
features = queries.unsqueeze(2) + keys.unsqueeze(1)
features = torch.tanh(features)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,“键-值”对的个数)
scores = self.w_v(features).squeeze(-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)

缩放点积注意力

当查询和键具有相同的长度 dd 时,使用点积计算是一种效率更高的方法。假设查询 q\boldsymbol{q} 和键 ki\boldsymbol{k}_i 的所有元素都是独立的随机变量,并且都满足零均值和单位方差。根据概率论知识可知

E(qTki)=0,Var(qTki)=d\mathbb{E}(\boldsymbol{q}^T\boldsymbol{k}_i)=0,\quad \mathrm{Var}(\boldsymbol{q}^T\boldsymbol{k}_i)=d

为确保无论向量长度如何,点积的方差在不考虑向量长度的情况下仍然是 11,我们再将点积除以 d\sqrt{d},则缩放点积注意力(Scaled Dot-Product Attention)评分函数可以表示为下式

a(q,ki)=qTkida(\boldsymbol{q},\boldsymbol{k}_i)=\frac{\boldsymbol{q}^T\boldsymbol{k}_i}{\sqrt{d}}

考虑一个小批量情况,对于 nn 个查询和 mm 个键值对,其中查询和键的长度为 dd,值的长度为 vv。用矩阵形式表示查询 QRn×d\boldsymbol{Q}\in\mathbb{R}^{n\times d}、键 KRm×d\boldsymbol{K}\in\mathbb{R}^{m\times d} 和值 VRm×v\boldsymbol{V}\in\mathbb{R}^{m\times v},其缩放点积注意力可以表示为

softmax(QKTd)V\mathrm{softmax}\left(\frac{\boldsymbol{QK}^T}{\sqrt{d}}\right)\boldsymbol{V}

下面的代码实现使用了Dropout进行模型正则化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)

# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,“键-值”对的个数,d)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)

一般情况下,当查询和键是不同长度的矢量时,可以使用可加性注意力评分函数。当它们的长度相同时,使用缩放点积注意力评分函数的计算效率更高。后文介绍的注意力机制模型都依赖于此性质。

Bahdanau注意力


seq2seq的不足

深度学习(九)中,我们介绍了seq2seq模型处理序列转换问题。具体来说,循环神经网络编码器将长度可变的序列转换为固定形状的上下文变量 c\boldsymbol{c},然后循环神经网络解码器根据生成的词元和上下文变量 按词元生成输出目标序列词元。对于解码器有

st=g(yt1,c,st1)\boldsymbol{s}_{t'}=g(\boldsymbol{y}_{t'-1},\boldsymbol{c},\boldsymbol{s}_{t'-1})

我们考虑一个中译英的例子。把 你好世界! 翻译为 Hello World!,在翻译过程中我们想要把 你好 对应为 Hello世界 对应为 World 等等。然而在seq2seq中,所有的输出只依赖于编码器给出的上下文变量 c\boldsymbol{c},这在一定程度上不符合翻译工作的词语对照原则。

与此同时,解码器在每个解码步骤中都会使用不变的上下文变量 c\boldsymbol{c}。并非所有的输入词元都对解码的某个词元有效,因此我们希望找到一种方法能改变上下文变量。受学习对齐想法的启发,Bahdanau等人提出了一个没有严格单向对齐限制的可微注意力模型

Bahdanau注意力

论文原文:NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE

Bahdanau等人的这篇论文对seq2seq的发展影响重大,其主要目的是为了帮助提升seq2seq的神经网络机器翻译质量。在预测词元时,如果不是所有输入词元都相关,模型将仅对齐输入序列中与当前预测相关的部分。这是通过将上下文变量 c\boldsymbol{c} 视为注意力集中的输出来实现的。

借助seq2seq相同的符号标记,假设上下文变量 c\boldsymbol{c} 在任何解码时间步 tt' 会被新的 ct\boldsymbol{c}_{t'} 替换。假设输入序列有 TT 个词元,这个新的上下文变量定义为

ct=t=1Tα(st1,ht)ht\boldsymbol{c}_{t'}=\sum_{t=1}^T\alpha(\boldsymbol{s}_{t'-1},\boldsymbol{h}_t)\boldsymbol{h}_t

对照注意力机制有

  • 查询:解码器上个时间步的隐状态 st1\boldsymbol{s}_{t'-1}
  • 键:编码器隐状态 ht\boldsymbol{h}_t
  • 值:编码器隐状态 ht\boldsymbol{h}_t
  • 注意力汇聚函数:采用 Softmax\mathrm{Softmax} 概率归一化
  • 注意力评分函数:采用加性注意力计算,即有

a(st1,ht)=wvTtanh(Wqst1+Wkht)a(\boldsymbol{s}_{t'-1},\boldsymbol{h}_t)=\boldsymbol{w}^T_v\tanh(\boldsymbol{W}_q\boldsymbol{s}_{t'-1}+\boldsymbol{W}_k\boldsymbol{h}_t)

一个带有Bahdanau注意力的循环神经网络编码器-解码器模型表示如下

Bahdanau注意力

论文数据指出,带有Bahdanau注意力的seq2seq模型得到了更高的BLEU分数。

事实上,原文作者在编码器中使用了双向循环神经网络模型(BiRNN),保证某个词元的翻译兼顾上下文内容。也就是说,编码器某个时间步的隐状态 ht\boldsymbol{h}_t 由前向隐状态 ht\overrightarrow{\boldsymbol{h}_t} 和反向隐状态 ht\overleftarrow{\boldsymbol{h}_t} 拼接而成。

d2l 教程中则简化了这一点,采用了GRU的实现方法。有关代码实现等见下文。

代码实现

忽略原文对解码器的修改,我们只需要在seq2seq模型的基础上修改解码器架构即可。

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
import torch
from torch import nn
from d2l import torch as d2l

# 不使用 d2l.Decoder 基类
class AttentionDecoder(Decoder):
"""带有注意力机制解码器的基本接口"""
def __init__(self, **kwargs):
super(AttentionDecoder, self).__init__(**kwargs)

@property
def attention_weights(self):
raise NotImplementedError

class Seq2SeqAttentionDecoder(AttentionDecoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
# 此处使用自定义的 AdditiveAttention 类,前文已给出
self.attention = AdditiveAttention(
num_hiddens, num_hiddens, num_hiddens, dropout)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(
embed_size + num_hiddens, num_hiddens, num_layers,
dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)

def init_state(self, enc_outputs, enc_valid_lens, *args):
# outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,num_hiddens)
outputs, hidden_state = enc_outputs
return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)

def forward(self, X, state):
# enc_outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,
# num_hiddens)
enc_outputs, hidden_state, enc_valid_lens = state
# 输出X的形状为(num_steps,batch_size,embed_size)
X = self.embedding(X).permute(1, 0, 2)
outputs, self._attention_weights = [], []
for x in X:
# query的形状为(batch_size,1,num_hiddens)
query = torch.unsqueeze(hidden_state[-1], dim=1)
# context的形状为(batch_size,1,num_hiddens)
context = self.attention(
query, enc_outputs, enc_outputs, enc_valid_lens)
# 在特征维度上连结
x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
# 将x变形为(1,batch_size,embed_size+num_hiddens)
out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
outputs.append(out)
self._attention_weights.append(self.attention.attention_weights)
# 全连接层变换后,outputs的形状为
# (num_steps,batch_size,vocab_size)
outputs = self.dense(torch.cat(outputs, dim=0))
return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,
enc_valid_lens]

@property
def attention_weights(self):
return self._attention_weights

其中,我们在解码器中指定了以下输入内容

  1. 编码器在所有时间步的最终层隐状态,将作为注意力的键和值
  2. 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态
  3. 编码器有效长度(排除在注意力池中填充词元)

使用一个包含7个时间步的4个序列输入的小批量测试Bahdanau注意力解码器。

1
2
3
4
5
6
7
8
encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
encoder.eval()
decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
decoder.eval()
X = torch.zeros((4, 7), dtype=torch.long) # (batch_size,num_steps)
state = decoder.init_state(encoder(X), None)

我们在这里指定超参数,实例化一个带有Bahdanau注意力的编码器和解码器,并对这个模型进行机器翻译训练。由于新增的注意力机制,训练要比没有注意力机制的情况慢得多。

1
2
3
4
5
6
7
8
9
10
11
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 250, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = d2l.Seq2SeqEncoder(
len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqAttentionDecoder(
len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
net = EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

训练完成后,可参考seq2seq的预测样例执行翻译工作并得到BLEU分数。

多头注意力


当给定相同的查询、键和值的集合时,我们希望模型可以基于相同的注意力机制学习到不同的行为,然后将不同的行为作为知识组合起来, 捕获序列内各种范围的依赖关系。

为此,与其只使用单独一个注意力汇聚,我们可以用独立学习得到的 hh 组不同的线性投影来变换查询、键和值。然后,这 hh 组变换后的查询、键和值将并行地送到注意力汇聚中。最后,将这 hh 个注意力汇聚的输出拼接在一起,并且通过另一个可以学习的线性投影进行变换,以产生最终输出。这种设计被称为多头注意力(Multihead Attention)。

对于 hh 个注意力汇聚输出,每一个注意力汇聚都被称作一个(Head),结构如下图所示。

多头注意力

给定查询 qRdq\boldsymbol{q}\in\mathbb{R}^{d_q}、键 kRdk\boldsymbol{k}\in\mathbb{R}^{d_k} 和值 vRdv\boldsymbol{v}\in\mathbb{R}^{d_v}。每个注意力头 hi(i=1,,h)\boldsymbol{h}_i(i=1,\cdots,h) 表示为

hi=f(Wi(q)q,Wi(k)k,Wi(v)v)Rpv\boldsymbol{h}_i=f\big(\boldsymbol{W}_i^{(q)}\boldsymbol{q},\boldsymbol{W}_i^{(k)}\boldsymbol{k},\boldsymbol{W}_i^{(v)}\boldsymbol{v}\big)\in\mathbb{R}^{p_v}

其中可学习参数包括 Wi(q)Rpq×dq,Wi(k)Rpk×dk,Wi(v)Rpv×dv\boldsymbol{W}_i^{(q)}\in\mathbb{R}^{p_q\times d_q},\boldsymbol{W}_i^{(k)}\in\mathbb{R}^{p_k\times d_k},\boldsymbol{W}_i^{(v)}\in\mathbb{R}^{p_v\times d_v},注意力汇聚函数 ff 可以使用前小节介绍的加性注意力或缩放点积注意力。多头注意力的输出需要经过另一个线性变换,这表示 hh 个头连结起来,因此有

O=Wo[h1hh]Rpo×hpv\boldsymbol{O}=\boldsymbol{W}_o\begin{bmatrix}\boldsymbol{h}_1\\\vdots\\\boldsymbol{h}_h\end{bmatrix}\in\mathbb{R}^{p_o\times hp_v}

为避免计算代价和参数代价大幅增长,一般设定 pq=pk=pv=po/hp_q=p_k=p_v=p_o/h,此时可并行计算。

自注意力和位置编码


自注意力

在深度学习中,经常使用卷积神经网络或循环神经网络对序列进行编码。有了注意力机制之后,我们将词元序列输入注意力池中,以便同一组词元同时充当查询、键和值。具体来说,每个查询都会关注所有的键值对并生成一个注意力输出。由于查询、键和值来自同一组输入,因此被称为自注意力(Self-Attention),也被称为内部注意力(Intra-Attention)。

给定一个由词元组成的输入序列 x1,,xn\boldsymbol{x}_1,\cdots,\boldsymbol{x}_n,其中 xiRd(1in)\boldsymbol{x}_i\in\mathbb{R}^d(1\leqslant i\leqslant n)。该序列的自注意力输出为一个长度相同的序列 y1,,yn\boldsymbol{y}_1,\cdots,\boldsymbol{y}_n,并且可以表示为

yi=f(xi;(x1,x1),,(xn,xn))Rd\boldsymbol{y}_i=f\big(\boldsymbol{x}_i;(\boldsymbol{x}_1,\boldsymbol{x}_1),\cdots,(\boldsymbol{x}_n,\boldsymbol{x}_n)\big)\in\mathbb{R}^d

上式在注意力机制里可以这样理解

  • 查询:输入词元 xi\boldsymbol{x}_i
  • 键值对:输入词元组成的有序对 (xj,xj)(\boldsymbol{x}_j,\boldsymbol{x}_j),其中 j=1,,nj=1,\cdots,n
  • 注意力汇聚函数:ff 对所有键值对加权求和

因此,自注意力可以看做查询与键来自同一集合且互为注意力的注意力机制。自注意力不在意词元之间的先后关系,因此其采用并行计算,极大提高了训练和运行效率。并且理论上,它可以捕获句子中更长距离的依赖。

下图展示了CNN、RNN和自注意力机制架构,他们用不同方式将一个序列映射为另一个长度相等的序列。
不同架构

位置编码

常规的循环神经网络逐个重复地处理词元,而自注意力机制由于并行计算原则舍弃了顺序计算。我们为了运用序列的顺序信息,需要对输入表示中添加位置编码(Positional Encoding)来注入器绝对位置信息或相对位置信息。

假设小批量输入 XRn×d\boldsymbol{X}\in\mathbb{R}^{n\times d} 包含一个序列中 nn 个词元经过Embedding层处理得到的特征向量,我们要做的是使用位置编码算法给出相同形状的矩阵 PRn×d\boldsymbol{P}\in\mathbb{R}^{n\times d},然后输出 X+P\boldsymbol{X}+\boldsymbol{P}

一个简单的想法是用正整数给词元依次标号,这主要反映出两个问题

  • 模型可能遇到比训练时所用序列更长的序列,泛化能力低
  • 模型的位置表示是无界的,随着序列长度增加,位置值会越来越大

理想地说,位置编码要满足以下这些标准:

  1. 它应该对每个时间步输出一个唯一的编码
  2. 我们的模型应该在不付出任何努力的条件下泛化到更长的句子
  3. 它的编码值应该有界
  4. 它必须是确定性的

在Transformer架构中,几位作者提出了一个简单而实用的位置编码算法,我们本小节只关注这种位置编码算法,即三角编码。假设某词元对应的时间步为 tt,我们考虑构造位置编码向量 ptRd\boldsymbol{p}_t\in\mathbb{R}^d,其中 dd 是Embedding层嵌入特征向量的维度,并且是个偶数。对于该位置编码向量的第 ii 行元素,其定义为

pt(i)={sin(t100002k/d),if i=2kcos(t100002k/d),if i=2k+1(i=0,1,,d1)\boldsymbol{p}_{t}^{(i)}=\begin{cases}\sin\left(\dfrac{t}{10000^{2k/d}}\right),&\text{if }i=2k\\\cos\left(\dfrac{t}{10000^{2k/d}}\right),&\text{if }i=2k+1\end{cases}\quad (i=0,1,\cdots,d-1)

定义基本频率 ωk=1100002k/d\omega_k=\dfrac{1}{10000^{2k/d}},根据上式可以把位置编码向量表示为

pt=[sin(ω0t)cos(ω0t)sin(ωd21t)cos(ωd21t)]d×1\boldsymbol{p}_t=\begin{bmatrix}\sin(\omega_0\cdot t)\\\cos(\omega_0\cdot t)\\\vdots\\\sin(\omega_{\frac{d}{2}-1}\cdot t)\\\cos(\omega_{\frac{d}{2}-1}\cdot t)\end{bmatrix}_{d\times 1}

为了解释上述编码的正确性,考虑十进制数字的二进制表示。下图给出了 0150\sim 15 的二进制表示,可以发现不同比特位上的数字都按照特定频率交错排列,并且越高次位的交错频率越低。

考虑三角编码,频率 ωk\omega_k 会随着向量维度 kk 的增大而减小,并且周期越来越长。这与二进制编码具有相似性,但由于三角函数值占用连续的浮点数空间,相比于整二进制更节省空间。

与此同时,上述的位置编码还允许模型学习得到输入序列中相对位置信息。对于任何确定的位置偏移 δ\delta,时间步为 t+δt+\delta 处的位置编码可以通过线性投影位置 tt 处的位置编码来实现。根据下述线性变换有

[cos(δωk)sin(δωk)sin(δωk)cos(δωk)][pt(2k)pt(2k+1)]=[sin[(t+δ)ωk]cos[(t+δ)ωk]]=[pt+δ(2k)pt+δ(2k+1)]\begin{bmatrix}\cos(\delta \omega_k)&\sin(\delta\omega_k)\\-\sin(\delta\omega_k)&\cos(\delta\omega_k)\end{bmatrix}\begin{bmatrix}\boldsymbol{p}_t^{(2k)}\\\boldsymbol{p}_t^{(2k+1)}\end{bmatrix}=\begin{bmatrix}\sin[(t+\delta)\omega_k]\\\cos[(t+\delta)\omega_k]\end{bmatrix}=\begin{bmatrix}\boldsymbol{p}_{t+\delta}^{(2k)}\\\boldsymbol{p}_{t+\delta}^{(2k+1)}\end{bmatrix}

上式最左侧的投影矩阵不依赖于任何位置的索引,这个性质使得模型可以很容易地学习把握相对位置。

代码实现

下面的代码实现了自注意力机制,本节更详细的代码讲解见下小节。

1
2
3
4
5
6
7
8
9
10
11
12
import math
import torch
from torch import nn
from d2l import torch as d2l

num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
num_hiddens, num_heads, 0.5)
attention.eval()

batch_size, num_queries, valid_lens = 2, 4, torch.tensor([3, 2])
X = torch.ones((batch_size, num_queries, num_hiddens))

同样的,下面给出三角编码的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PositionalEncoding(nn.Module):
"""位置编码"""
def __init__(self, num_hiddens, dropout, max_len=1000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(dropout)
# 创建一个足够长的P
self.P = torch.zeros((1, max_len, num_hiddens))
X = torch.arange(max_len, dtype=torch.float32).reshape(
-1, 1) / torch.pow(10000, torch.arange(
0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
self.P[:, :, 0::2] = torch.sin(X)
self.P[:, :, 1::2] = torch.cos(X)

def forward(self, X):
X = X + self.P[:, :X.shape[1], :].to(X.device)
return self.dropout(X)

Transformers


背景

Transformer是一种用于自然语言处理(NLP)和其他序列到序列(Sequence-To-Sequence)任务的深度学习模型架构,它在2017年由Vaswani等人首次提出。Transformer架构旨在解决循环神经网络在处理长序列数据时存在的计算效率问题和难以并行化等问题。该架构的核心创新是使用自注意力机制替代传统的循环结构,以实现序列的并行处理,并取得了更好的表现。

该架构由谷歌大脑(Google Brain)团队的八位研究人员在2017年提出。主要贡献者 Jakob Uszkoreit 酝酿了自注意力机制的想法,并与同事共同起草了名为Transformers,中文直译为“变形金刚”。在 Noam Shazeer 加入后,项目取得关键突破。团队于2017年提交了论文《Attention Is All You Need》,创造了继MLP、CNN、RNN之后的第四大类深度学习模型,从此开启了深度学习领域的新时代。

论文解读

论文原文:Attention Is All You Need

为方便介绍Transformer模型,我们本节对论文进行解读。

Abstract

The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English to-German translation task, improving over the existing best results, including ensembles, by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data.

Abstract部分简要介绍了当时的序列学习模型发展,主要以CNN和RNN或者带有注意力机制的编码器-解码器架构为主。然后,作者提出了一种完全依靠注意力机制的序列模型,即Transformer架构,并且能够高效的并行训练,在机器翻译任务上取得了历史新高BLEU分数。

背景介绍部分在此省略。为了进一步了解Transformer架构,可以参考下面的论文原图。

上图的左、右板块分别表示Transformer架构的编码器与解码器

  • 编码器:由若干个(N=6N=6)相同的层组成,每个层都可以表示为两个子层
    • 第一个子层表示为多头自注意力汇聚(Multi-Head Self-Attention),其中查询、键和值都来自前一个编码器层的输出,以实现自注意力机制
    • 第二个子层表示为基于位置的前馈网络(Positionwise Feed Forward Network)
    • 受到ResNet启发,每个子层的输出按照残差连接的形式,并且应用层归一化,规定子层和Embedding层的输出维度为 dmodel=512d_{\mathrm{model}}=512 以保证可加性。
  • 解码器:由若干个(N=6N=6)相同的层组成,每个层都可以表示为三个子层
    • 后两个子层和编码器层组成一致。在解码器自注意力中,查询、键和值都来自前一个解码器层的输出
    • 第一个子层被称为编码器-解码器注意力层(Encoder-Decoder Attention),其查询来自前一个解码器层的输出,键和值则来自整个编码器的输出
    • 解码器中的每个位置只能考虑该位置之前的所有位置(不符合的位置编码设为 -\infty),又称掩蔽注意力(Masked Attention),即注意力保留了自回归属性,确保预测仅依赖于已生成的输出词元

从上述总体架构中看出,Transformers使用了位置编码和多种注意力机制

  • 位置编码:通过三角编码,获取和Embedding操作后的输入 X\boldsymbol{X} 一样维度的位置编码矩阵 PRn×dmodel\boldsymbol{P}\in\mathbb{R}^{n\times d_{\mathrm{model}}},得到最终的输入 X+PRn×dmodel\boldsymbol{X+P}\in\mathbb{R}^{n\times d_{\mathrm{model}}}
  • 缩放点积注意力:本文首次提出了缩放点积自注意力方法。对于查询 QRn×dk\boldsymbol{Q}\in\mathbb{R}^{n\times d_k}、键 KRn×dk\boldsymbol{K}\in\mathbb{R}^{n\times d_k} 和值 VRn×dv\boldsymbol{V}\in\mathbb{R}^{n\times d_v},其缩放点积注意力可以表示为

Attention(Q,K,V)=softmax(QKTd)VRn×dv\mathrm{Attention}(\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V})=\mathrm{softmax}\left(\frac{\boldsymbol{QK}^T}{\sqrt{d}}\right)\boldsymbol{V}\in\mathbb{R}^{n\times d_v}

  • 多头注意力:使用 hh 个注意力汇聚机制获取不同层次的信息,在拼接操作 Concat\mathrm{Concat} 后通过线性映射实现多头注意力。定义第 ii 个注意力头的权重矩阵 WiQRdmodel×dk,WiKRdmodel×dk,WiVRdmodel×dv\boldsymbol{W}_i^Q\in\mathbb{R}^{d_{\mathrm{model}}\times d_k},\boldsymbol{W}_i^K\in\mathbb{R}^{d_{\mathrm{model}}\times d_k},\boldsymbol{W}_i^V\in\mathbb{R}^{d_{\mathrm{model}}\times d_v}WORhdv×dmodel\boldsymbol{W}^O\in\mathbb{R}^{hd_v\times d_{\mathrm{model}}},多头注意力表示为

MultiHead(Q,K,V)=Concat(head1,,headh)WORn×dmodelwhere headi=Attention(QWiQ,KWiK,VWiV)\begin{aligned}\mathrm{MultiHead}(Q,K,V)&=\mathrm{Concat}(\mathrm{head}_1,\cdots,\mathrm{head}_h)\boldsymbol{W}^O\in\mathbb{R}^{n\times d_{\mathrm{model}}}\\ \text{where }\mathrm{head}_i&=\mathrm{Attention}(\boldsymbol{QW}_i^Q,\boldsymbol{KW}_i^K,\boldsymbol{VW}_i^V)\end{aligned}

  • Add&Norm\mathrm{Add\&Norm} 层由残差连接和紧随其后的层规范化组成。两者都是构建有效的深度架构的关键,前者通过残差映射学习保证训练效果,后者基于特征维度进行数据归一化,加快训练收敛。

论文原文指定了 h=8h=8,层数 dk=dv=dmodel/h=64d_k=d_v=d_{\mathrm{model}}/h=64

为了更好地处理子层之间的注意力关系,Transformer架构在编码器和解码器的多头注意力汇聚层后加入了全连接前馈网络(FNN)。这个FNN是逐个位置操作的,对于输入的每个 dmodel×1d_{\mathrm{model}}\times 1 向量采用相同参数的MLP操作,具体来说是进行两次线性变换

FFN(x)=W2ReLU(W1x+b1)+b2where matrix W1Rdff×dmodel,W2Rdmodel×dff\begin{aligned}\mathrm{FFN}(\boldsymbol{x})=&\boldsymbol{W}_2\cdot\mathrm{ReLU}(\boldsymbol{W}_1\boldsymbol{x}+\boldsymbol{b}_1)+\boldsymbol{b_2}\\\text{where matrix }&\boldsymbol{W}_1\in\mathbb{R}^{d_{ff}\times d_{\mathrm{model}}},\boldsymbol{W}_2\in\mathbb{R}^{d_{\mathrm{model}}\times d_{ff}}\end{aligned}

论文原文指定了 dff=2048d_{ff}=2048

优势

为了展示Transformer架构的自注意力机制的优势,我们将其与CNN、RNN模型进行对比。先定义如下参数

  • nn:序列长度
  • dd:模型维度
  • kk:卷积核大小
  • rr:受限自注意力的邻域大小
类型 每层复杂度 顺序操作数 最大路径长度
Self-Attention O(n2d)O(n^2 d) O(1)O(1) O(1)O(1)
Recurrent O(nd2)O(n d^2) Θ(n)\Theta(n) Θ(n)\Theta(n)
Convolutional O(knd2)O(k n d^2) O(1)O(1) O(logkn)O(\log_kn)
Self-Attention (restricted) O(rnd)O(r n d) O(1)O(1) O(n/r)O(n/r)
  • 对于卷积核大小为 kk 的CNN模型,由于序列长度为 nn,输入和输出的通道数均为 dd,因此卷积层的计算复杂度为 O(nkd2)O(nkd^2);由于卷积神经网络是分层结构,因此有 O(1)O(1) 个顺粗操作,且最大路径长度为 O(n/k)O(n/k)O(logkn)O(\log_kn)
  • 对于RNN模型,每一次更新隐状态都需要计算 d×dd\times dd×1d\times 1 维度矩阵的乘法,复杂度为 O(d2)O(d^2),因此整个序列的计算复杂度为 O(kd2)O(kd^2);因为RNN逐个计算,顺序操作数和最大路径长度为 Θ(n)\Theta(n)
  • 对于自注意力机制,由于查询、键和值都可以视为 n×dn\times d 矩阵,考虑缩放点积注意力公式,其计算复杂度为 O(n2d)O(n^2d);由于自注意力机制可以并行运算,因此顺序操作数和最大路径长度都为 O(1)O(1)

总而言之,卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。

训练和表现

Transformer 架构主要采用了带标签的标准监督学习进行训练,其核心优化方法结合了 Adam 优化器 和学习率预热策略。在机器翻译等任务中,模型使用标准的交叉熵损失函数来最大化正确翻译词元的概率。为了解决深度神经网络训练不稳定的问题,Transformer 引入了残差连接、层归一化、Dropout和标签平滑技术(Label Smoothing)等来防止模型过拟合,从而提高模型在未见数据上的泛化能力。

模型使用了 β1=0.9,β2=0.98\beta_1=0.9,\beta_2=0.98ε=109\varepsilon=10^{-9} 的 Adam 优化器。预热步数为 40004000

在训练表现上,Transformer 展现出了卓越的并行计算效率和可扩展性。得益于其将序列顺序操作降至最低的设计,模型能够在现代 GPU 上进行极大规模的高效训练。实验表明,Transformer 在机器翻译任务上不仅训练速度显著优于 CNN 和 RNN 架构,更在 BLEU 评分等指标上取得了当时的最佳成,奠定了其作为后续 BERT、GPT 等大模型基础架构的地位。

代码实现

Transformer架构的模型本质上由多个组件组成,因此实现代码是不难的。点此查看官方代码

结语


总之,感谢您花费珍贵的注意力资源以关注本博客。

深度学习部分一共10篇日志,以李沐的*动手学深度学习(D2L)*为蓝本,较为细致的总结了深层网络机器学习模型的发展历程、数学机制与代码实现。自从写完这篇稿子的2026年,深度学习领域的发展速度已经达到一个前所未有的高度,因此模型框架等呈现出不断变化的趋势。无论如何,希望本系列日志能够对您的深度学习入门有所帮助,敬礼∠(°ゝ°)

再次感谢您的关注,那么我们下次再见啦 (๑′ᴗ‵๑)❤