多头自注意力机制(Multi-Head Self-Attention)是深度学习中一种用于处理序列数据的重要机制,广泛应用于自然语言处理(NLP)和计算机视觉等领域。它最早出现在 Transformer 模型中。
1. Self Attention 机制图示

计算过程如下:
- 先将输入 Token 进行词嵌入计算;
- 为每个输入 Token 分别初始化注意力权重参数:w_q、w_k、w_v,输入维度为词嵌入维度,输出维度可指定;
- 将每个 Token 与各自的 w_q、w_k、w_v 分别计算,产生 q、k、v 三个张量;
- 以计算第一个字 “我” 的注意力张量为例:
- 将 “我” 的 q 张量分别与:“我”、“爱”、“你” 三个的 k 张量进行计算,得出三个 att_score
- 将计算得到的 3 个 att_score 经过 softmax 计算,得到三个 att_weight,表示当前 Token 对不同的 Token 的关注程度,也表示不同的 Token 对当前 Token 语义表示的贡献;
- 将 3 个 att_weight 与各自的对应的 v 进行计算,此时会得到 3 个向量,将这 3 个向量加起来或者拼接起来作为 “我” 对其他 Toekn 的注意力张量;
- 最后,将注意力张量加到 Token 上,即表示当前 Token 结合了上下文的语义表示的向量表示。
计算案例:
import torch
import torch.nn.functional as F
import torch.nn as nn
# 1. 注意力机制基本理解
def test01():
# 输入 shape=(2, 3), 表示2个词, 每个词3维
inputs = torch.tensor([[1, 0, 0],
[0, 2, 2]], dtype=torch.float)
# 初始化注意权重, shape=(3, 2)
w_q = torch.tensor([[1, 0], [1, 0], [0, 1]], dtype=torch.float)
w_k = torch.tensor([[0, 1], [1, 0], [1, 0]], dtype=torch.float)
w_v = torch.tensor([[2, 0], [3, 0], [0, 3]], dtype=torch.float)
# 计算得到每个词的 q、k、v, shape=(2, 2)
# 第一个词的 v 为: [0, 1]
# 第二个词的 v 为: [4, 0]
q = inputs @ w_q # tensor([[1, 0], [2, 2]])
k = inputs @ w_k # tensor([[0, 1], [4, 0]])
v = inputs @ w_v # tensor([[2, 0], [6, 6]])
# 计算注意力分数, shape=(2, 2)
# 第一个词 v: [2, 0] 对第一个词注意力分数为: 0, 对第二个词的注意力分数为: 4
# 第二个词 v: [6, 6] 对第一个词注意力分数为: 2, 对第二个词的注意力分数为: 8
att_score = q @ k.T # tensor([[0, 4], [2, 8]])
# 计算得到注意力权重
# 第一个 v: [0.0180, 0.9820] 对第一个词注意力权重为: 0.0180, 对第二个词的注意力权重为: 0.9820
# 第二个 v: [0.0025, 0.9975] 对第一个词注意力权重为: 0.0025, 对第二个词的注意力权重为: 0.9975
attn_weight = F.softmax(att_score, dim=1) # tensor([[0.0180, 0.9820], [0.0025, 0.9975]])
# 第一个词的注意力值
# [2, 0]、 [6, 6]
# 0.0180、 0.9820
# 第一个词对词1和词2的注意力值:[0.036, 0]、[5.8919, 5.8919]
# 第二个词的注意力值
# [2, 0]、 [6, 6]
# 0.0025、 0.9975
# # 第二个词对词1和词2的注意力值:[0.005, 0]、[5.985, 5.985]
# 最终注意力值
# 把对每个 v 的注意力加起来, 构成这个词的注意力值
# [0.036, 0] + [5.892, 5.892]
# [0.005, 0] + [5.985, 5.985]
# 得到
# [5.928, 5.892]
# [5.990, 5.985]
# tensor([[2, 0],
# [6, 6]])
# tensor([[0.0180, 0.9820],
# [0.0025, 0.9975]])
# (2, 1, 2)
# (2, 2, 2)
attn_values = v[:, None] * attn_weight.T[:, :, None]
print(attn_values.shape)
print(attn_values.sum(dim=0))
# v[:, None]
# tensor([[[2., 0.],
# [2., 0.]],
#
# [[6., 6.],
# [6., 6.]]])
# attn_weight.T[:, :, None]
# tensor([[[0.0180, 0.0180],
# [0.0025, 0.0025]],
#
# [[0.9820, 0.9820],
# [0.9975, 0.9975]]])
# 两个矩阵相加,维度不同需要先进行广播
# 扩展成相同维度,再进行点乘
# tensor([[[2 * 0.0180, 0 * 0.0180],
# [2 * 0.0025, 0 * 0.0025]],
#
# [[6 * 0.9820, 6 * 0.9820],
# [6 * 0.9975, 6 * 0.9975]]])
# tensor([[[0.036, 0],
# [0.005, 0]],
#
# [[5.8919, 5.8919],
# [5.985, 5.985]]])
print(attn_values[0])
print(attn_values[1])
# tensor([[2, 0], [6, 6]])
# tensor([[0.0180, 0.9820], [0.0025, 0.9975]])
# 2. 注意力机制的维度控制
def test02():
# 输入
input_shape = (2, 3)
inputs = torch.randint(0, 5, input_shape, dtype=torch.float)
# 初始化注意权重
# 设置自注意力张量维度
attn_dim = 8
# 计算得到每个词的 q、k、v
q_linear = nn.Linear(input_shape[1], attn_dim)
k_linear = nn.Linear(input_shape[1], attn_dim)
v_linear = nn.Linear(input_shape[1], attn_dim)
q = q_linear(inputs)
k = k_linear(inputs)
v = v_linear(inputs)
# 计算注意力分数
att_score = q @ k.T
# 计算得到注意力权重
attn_weight = F.softmax(att_score, dim=1)
print(attn_weight, attn_weight.shape)
attn_values = v[:, None] * attn_weight.T[:, :, None]
print(attn_values.sum(dim=0).shape)
if __name__ == '__main__':
test01()
在 test02 中,可以通过 attn_dim 变量来控制最终计算的注意力张量的维度.
官网也给出了一段关于自主力机制的封装函数:
import torch
import math
import torch.nn.functional as F
import torch.nn as nn
# 1. 接受的输入为已经计算得出的 query、key、value 矩阵
# 2. query、key、value 的 shape = (batch_size, multi_head, seq_length, dim)
# 3. query、key、value 的 shape = (批量, 头数量, 序列长度, 维度)
# 4. mask :
# 4.1 当使用普通注意力机制时, 需要使用 mask 掩盖未来词, 避免提前看到预测的词
# 4.2 当使用自注意力机制时, 需要对部分为 0 的位置进行特殊处理
# 5. dropout : 对计算的注意力权重进行随机丢弃
def attention_calculate(query, key, value, mask=None, dropout=None):
# 获得 q、k、v 维度
query_dim = query.shape[-1]
# 计算注意力分数
attn_scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(query_dim)
# 将 mask == 0 的位置的分数设置极小值, 计算 softmax 时使得该位置的权重接近 0
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, -1e-9)
# 使用 softmax 函数将注意力分数转换为注意力权重
attn_weight = F.softmax(attn_scores, dim=-1)
# 对注意力权重值进行随机丢弃
if dropout is not None:
attn_weight = dropout(attn_weight)
# 计算注意力值
attn_tensor = torch.matmul(attn_weight, value)
return attn_tensor, attn_weight
if __name__ == '__main__':
# (批量, 头数量, 序列长度, 维度)
# inputs = ['我爱你', '我恨你']
# 2 表示该批次共输入 2 个句子
# 8 表示对于批次中的每个句子每个字或者词使用8个注意力头
# 3 表示输入句子长度
# 256 表示维度
q = torch.randn(2, 8, 3, 256)
k = torch.randn(2, 8, 3, 256)
v = torch.randn(2, 8, 3, 256)
attn_tensor, attn_weight = attention_calculate(q, k, v, mask=None, dropout=None)
print('注意力张量:', attn_tensor.shape)
print('注意力权重:', attn_weight.shape)
2. 多头自注意力机制
多头注意力机制扩展了模型专注不同位置的能力,或者说专注了从不同的角度关注的能力,最终将将多个不同角度关注到的信息(张量)拼接起来。

假设:我们有 3 head,则:每个输入将会初始化 3 套 (q1, k1, v1)、(q2, k2, v2)、(q3, k3, v3),当计算第一个 Token 的注意张量时,会用第一 Token 的 q1、q2、q3 分别与其他 Token 的 k1、k2、k3 进行缩放点积计算,得到分数之后,softmax 得到注意力权重,在分别乘以其他 Token 的 v1、v2、v3,得到 3 个向量,这 3 个向量可以理解为从不同角度对其他 Token 关联程度的理解。
示例代码:
import torch
import torch.nn.functional as F
import torch.nn as nn
# 1. 单独计算每个注意力头张量
def test01():
# 固定随机数种子
torch.random.manual_seed(0)
# 输入 shape=(2, 3), 表示2个词, 每个词3维
inputs = torch.randint(1, 5, (2, 3), dtype=torch.float)
# 初始化注意权重
head_number = 2
# 一个头的的注意力权重: shape=(2, 3)
# 多个头的的注意力权重: shape=(2, 3 * head_number)
W_q = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
W_k = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
W_v = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
w_q1 = W_q[:, :3]
w_k1 = W_k[:, :3
w_v1 = W_v[:, :3]
w_k2 = W_k[:, 3:]
w_q2 = W_q[:, 3:]
w_v2 = W_v[:, 3:]
# 计算得到每个词的 q、k、v, shape=(2, 2)
q1 = inputs @ w_q1
k1 = inputs @ w_k1
v1 = inputs @ w_v1
q2 = inputs @ w_q2
k2 = inputs @ w_k2
v2 = inputs @ w_v2
# 计算注意力得分
att_score1 = q1 @ k1.T
att_score2 = q2 @ k2.T
# 计算注意力权重
attn_weight1 = F.softmax(att_score1, dim=1)
attn_weight2 = F.softmax(att_score2, dim=1)
attn_values1 = (v1[:, None] * attn_weight1.T[:, :, None]).sum(dim=0)
attn_values2 = (v2[:, None] * attn_weight2.T[:, :, None]).sum(dim=0)
# 将两头注意力张量拼接到一起
multi_head_attn = torch.cat([attn_values1, attn_values2], dim=1)
print(multi_head_attn)
# 2. 矩阵计算每个注意力头张量
def test02():
# 固定随机数种子
torch.random.manual_seed(0)
# 输入 shape=(2, 3), 表示2个词, 每个词3维
inputs = torch.randint(1, 5, (2, 3), dtype=torch.float)
# 初始化注意权重
head_number = 2
# 一个头的的注意力权重: shape=(2, 3)
# 多个头的的注意力权重: shape=(2, 3 * head_number)
W_q = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
W_k = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
W_v = torch.randint(1, 5, (3, 3 * head_number), dtype=torch.float)
# 计算得到每个词的 q、k、v, shape=(2, 2)
Q = inputs @ W_q
K = inputs @ W_k
V = inputs @ W_v
# 计算注意力得分
att_score = Q @ K.T
# 计算注意力权重
attn_weight = F.softmax(att_score, dim=1)
attn_values = (V[:, None] * attn_weight.T[:, :, None]).sum(dim=0)
print(attn_values)
if __name__ == '__main__':
test01()
print('-' * 30)
test02()

冀公网安备13050302001966号