目录
1. 词向量
词向量是自然语言处理中的一种核心技术,用于将词语或文本表示为向量。通过词向量,计算机可以以数值形式理解和处理语言。它们通过将词语映射到向量空间,将相似语义的词表示为距离接近的向量,从而实现更丰富的语言语义分析。
词向量表示词语的好处:1. 语义相似的词在向量空间中靠近,比如“猫”和“狗”的向量接近,而与“车”较远。2. 词向量可以通过线性关系表达某些语义规则,如King - Man + Woman ≈ Queen。
一段文本首先要经过分词(文本编码)得到词索引,再经过词嵌入得到词向量。这里暂时不介绍分词和词嵌入的方法。
2. 掩码softmax
2.1 填充掩码softmax
在词向量技术还未兴起之前,填充掩码Softmax用于处理输入序列中由于长度不一致而造成的填充部分。在自然语言处理任务中,句子通常具有不同的长度,因此在将它们批量处理时,较短的句子会被填充为与最长句子相同的长度。这种填充通常使用0表示。在计算Softmax时,我们希望忽略这些填充的部分,以避免影响模型的学习和输出。
2.2 因果掩码softmax
因果掩码主要用于自回归任务,如序列生成和机器翻译中的解码器部分。它的作用是确保模型在当前时间步只能关注到先前的时间步,避免看到未来的信息,从而符合序列生成的因果关系。
3. 注意力
与RNN相比,注意力的计算复杂度更低,并且可以通过并行计算缩短计算时间。此外,注意力还可以动态地决定哪些输入部分对当前输出最重要,从而对重要信息给予更多的关注,提高了信息的利用效率。
本节将主要介绍Transformer模型中使用的注意力,为理解Transformer模型打好基础。
3.1 注意力机制的 Q Q Q, K K K, V V V
3.1.1 非自主性提示和自主性提示
注意力过程中有两个重要的组件:自主性提示和非自主性提示。
非自主性提示是基于环境的易见性和突出性。假如我们面前有五个物品: 一份报纸、一篇研究论文、一杯咖啡、一本笔记本和一本书。 所有纸制品都是黑白印刷的,但咖啡杯是红色的。 这个咖啡杯在这种视觉环境中是突出和显眼的, 不由自主地引起人们的注意。 所以我们会把视力最敏锐的地方放到咖啡上, 如下图所示。
喝咖啡后,我们会变得兴奋并想读书, 所以转过头,重新聚焦眼睛,然后看看书, 就像下图中描述那样。 与上图中由于突出性导致的选择不同, 此时选择书是受到了认知和意识的控制, 因此注意力在基于自主性提示去辅助选择时将更为谨慎。人的主观意愿推动,选择的力量也就更强大。
3.1.2 如何用自主性提示和非自主性提示理解 Q Q Q, K K K, V V V
上面提到的非自主性提示就像注意力机制中的
K
K
K,而自主性提示就像注意力机制中的
Q
Q
Q,人接受到的感官输入(这里是视觉)就像注意力机制中的
V
V
V。
不妨想象一下这样的一个场景:你和朋友约定在一个地方见面,然后一起去进行今日的计划。朋友可能告诉了你今日他穿的衣服和等待的位置,这些是自主性提示(
Q
Q
Q),你会集中注意力搜索符合这些特征的区域和对象上。此外,朋友看到了你,可能进行挥手或者大声喊你,这些动作和场景内其他引人注目的事物是非自主性提示(
K
K
K),你的注意力被吸引,尽管可能不是你的朋友,但是这种自然而然的反应可以帮你缩小寻找的范围。自主性提示(
Q
Q
Q)和非自主性提示(
K
K
K)结合,并对感官输入(
V
V
V)作出反应,直至找到你的朋友。
3.2 注意力机制的计算过程
下图中给出了一个查询在注意力机制中根据键值对计算输出的过程。查询
q
i
q_i
qi是一个形状为
q
u
e
r
y
_
s
i
z
e
×
1
query\_size\times1
query_size×1的向量,键
k
j
k_j
kj是一个形状为
k
e
y
_
s
i
z
e
×
1
key\_size\times1
key_size×1的向量,值
v
j
v_j
vj是一个形状为
v
a
l
u
e
_
s
i
z
e
×
1
value\_size\times1
value_size×1的向量。在注意力机制中键、值的数量必须一样。
α
\alpha
α是注意力评分函数,查询
q
i
q_i
qi和键
k
j
k_j
kj的注意力评分函数值为
a
(
q
i
,
k
j
)
a(q_i,k_j)
a(qi,kj),查询
q
i
q_i
qi和键
k
j
k_j
kj的注意力权重为
w
a
(
q
i
,
k
j
)
=
α
(
q
i
,
k
j
)
∑
j
α
(
q
i
,
k
j
)
w_a(q_i,k_j)=\displaystyle \frac{\alpha(q_i,k_j)}{\displaystyle\sum_{j}\alpha(q_i,k_j)}
wa(qi,kj)=j∑α(qi,kj)α(qi,kj),最终的输出为
o
=
∑
j
w
a
(
q
i
,
k
j
)
v
j
o=\displaystyle\sum_{j}w_a(q_i,k_j)v_j
o=j∑wa(qi,kj)vj。
3.3 加性注意力
加性注意力的评分函数为
e
i
j
=
w
T
t
a
n
h
(
W
q
q
i
+
W
k
k
j
)
e_{ij}=w^Ttanh(W_qq_i+W_kk_j)
eij=wTtanh(Wqqi+Wkkj),其中查询
q
i
q_i
qi为
q
u
e
r
y
_
s
i
z
e
×
1
query\_size\times1
query_size×1的向量,
W
q
W_q
Wq的形状为
h
×
q
u
e
r
y
_
s
i
z
e
h\times query\_size
h×query_size,键
k
j
k_j
kj为
k
e
y
_
s
i
z
e
×
1
key\_size\times1
key_size×1的向量,
W
k
W_k
Wk的形状为
h
×
k
e
y
_
s
i
z
e
h\times key\_size
h×key_size,
w
w
w为
h
×
1
h\times1
h×1的向量。
加性注意力层的PyTorch实现如下。
import torch
import torch.nn as nn
class AdditiveAttention(nn.Module):
def __init__(self, query_size, key_size, num_hidden, dropout):
super().__init__()
self.wq = nn.Linear(query_size, num_hidden, bias=False)
self.wk = nn.Linear(key_size, num_hidden, bias=False)
self.wv = nn.Linear(num_hidden, 1, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, mask=None):
assert keys.shape[1] == values.shape[1], "The sequence length of keys and values must be equal"
queries = self.wq(queries)
keys = self.wk(keys)
scores = self.wv(nn.Tanh()(queries.unsqueeze(2) + keys.unsqueeze(1))).squeeze(-1)
if mask is not None:
scores.masked_fill_(mask, -float("inf"))
scores = nn.Softmax(-1)(scores)
return torch.bmm(self.dropout(scores), values)
3.4 缩放点积注意力
缩放点积注意力
Q
Q
Q的维度query_size必须和
K
K
K的维度key_size相等,令
d
k
=
q
u
e
r
y
_
s
i
z
e
=
k
e
y
_
s
i
z
e
d_k=query\_size=key\_size
dk=query_size=key_size。缩放点积注意力的评分函数为
e
i
j
=
q
i
T
k
j
d
k
e_{ij}=\displaystyle\frac{q_i^Tk_j}{\sqrt{d_k}}
eij=dkqiTkj,其中查询
q
i
q_i
qi为
d
k
×
1
d_k\times1
dk×1的向量,键
k
j
k_j
kj为
d
k
×
1
d_k\times1
dk×1的向量。v
缩放点积注意力的矩阵表示为
A
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
A=\displaystyle softmax(\frac{QK^T}{\sqrt{d_k}})V
A=softmax(dkQKT)V,其中查询
Q
Q
Q的形状为
n
u
m
_
q
u
e
r
y
×
d
k
num\_query\times d_k
num_query×dk,键
K
K
K的形状为
s
e
q
_
l
e
n
×
d
k
seq\_len\times d_k
seq_len×dk,值
V
V
V的形状为
s
e
q
_
l
e
n
×
v
a
l
u
e
_
s
i
z
e
seq\_len\times value\_size
seq_len×value_size。
缩放点积注意力的PyTorch实现如下。
class ScaledDotProductAttention(nn.Module):
def __init__(self, dropout):
super().__init__()
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, mask=None):
assert keys.shape[1] == values.shape[1], "The sequence length of keys and values must be equal"
assert queries.shape[2] == keys.shape[2], "query_size must equal key_size"
scores = torch.bmm(queries, keys.transpose(1, 2)) / queries.shape[-1]
if mask is not None:
# mask matrix's shape: (1, num_query, seq_len) or (1, queries.shape[1], keys.shape[1])
scores.masked_fill_(mask, -float("inf"))
scores = nn.Softmax(-1)(scores)
return torch.bmm(self.dropout(scores), values)
3.5 多头注意力
多头注意力有
h
h
h个注意力头,这里的注意力头使用缩放点积注意力。查询
Q
Q
Q的形状为
n
u
m
_
q
u
e
r
y
×
q
u
e
r
y
_
s
i
z
e
num\_query\times query\_size
num_query×query_size,键
K
K
K的形状为
s
e
q
_
l
e
n
×
k
e
y
_
s
i
z
e
seq\_len\times key\_size
seq_len×key_size,值
V
V
V的形状为
s
e
q
_
l
e
n
×
v
a
l
u
e
_
s
i
z
e
seq\_len\times value\_size
seq_len×value_size。
W
q
W_q
Wq的形状为
q
u
e
r
y
_
s
i
z
e
×
d
query\_size\times d
query_size×d,
W
k
W_k
Wk的形状为
k
e
y
_
s
i
z
e
×
d
key\_size\times d
key_size×d,
W
v
W_v
Wv的形状为
v
a
l
u
e
_
s
i
z
e
×
d
value\_size\times d
value_size×d(
d
d
d是
h
h
h的倍数),
W
o
W_o
Wo的形状为
d
×
d
d\times d
d×d。
多头注意力的计算过程:
1.
Q
=
Q
W
q
Q=QW_q
Q=QWq,
K
=
K
W
k
K=KW_k
K=KWk,
V
=
V
W
v
V=VW_v
V=VWv。
2.经过上一步
Q
Q
Q、
K
K
K、
V
V
V形状的第二维都为
d
d
d,
Q
Q
Q的形状重塑成
n
u
m
_
q
u
e
r
y
×
h
×
d
h
\displaystyle num\_query\times h\times\frac{d}{h}
num_query×h×hd
,
K
K
K的形状重塑成
s
e
q
_
l
e
n
×
h
×
d
h
\displaystyle seq\_len\times h\times\frac{d}{h}
seq_len×h×hd,
V
V
V的形状重塑成
s
e
q
_
l
e
n
×
h
×
d
h
\displaystyle seq\_len\times h\times\frac{d}{h}
seq_len×h×hd,重塑后的
Q
Q
Q、
K
K
K、
V
V
V第二维和第一维互换。
3.经历重塑和互换后的
Q
Q
Q、
K
K
K、
V
V
V进行缩放点积注意力计算,得到输出O,形状为
h
×
n
u
m
_
q
u
e
r
y
×
d
h
\displaystyle h\times num\_query\times \frac{d}{h}
h×num_query×hd。
4.
O
O
O的第一维和第二维互换,然后形状重塑成
n
u
m
_
q
u
e
r
y
×
d
num\_query\times d
num_query×d。
5.最终输出为
O
W
o
OW_o
OWo。
多头注意力的PyTorch实现如下。
class MultiHeadAttention(nn.Module):
def __init__(self, query_size, key_size, value_size, num_hidden, num_head, dropout):
super().__init__()
assert num_hidden % num_head == 0, "num_hidden must be divisible by num_head"
self.num_head = num_head
self.wq = nn.Linear(query_size, num_hidden, bias=False)
self.wk = nn.Linear(key_size, num_hidden, bias=False)
self.wv = nn.Linear(value_size, num_hidden, bias=False)
self.attention = ScaledDotProductAttention(dropout)
self.wo = nn.Linear(num_hidden, num_hidden)
def transpose_qkv(self, x, num_head):
x = x.reshape(x.shape[0], x.shape[1], num_head, -1)
x = x.permute(0, 2, 1, 3)
return x.reshape(-1, x.shape[2], x.shape[3])
def transpose_output(self, x, num_head):
x = x.reshape(-1, num_head, x.shape[1], x.shape[2])
x = x.permute(0, 2, 1, 3)
return x.reshape(x.shape[0], x.shape[1], -1)
def forward(self, queries, keys, values, mask=None):
queries = self.transpose_qkv(self.wq(queries), self.num_head)
keys = self.transpose_qkv(self.wk(keys), self.num_head)
values = self.transpose_qkv(self.wv(values), self.num_head)
# mask matrix's shape: (1, num_query, seq_len) or (1, queries.shape[1], keys.shape[1])
scores = self.attention(queries, keys, values, mask)
scores_concat = self.transpose_output(scores, self.num_head)
return self.wo(scores_concat)
参考
Ashish Vaswani, Noam Shazeer, Niki Parmar and et al. Attention Is All You Need.
动手学深度学习 10.注意力机制