李沐 -- 注意力评分函数代码分析
·
重点
加性注意力函数中的问题1有助于更好的理解注意力机制
掩蔽softmax操作
#@save
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)
函数1:dim()
valid_lens.dim() #获得维度个数
函数2:torch.repeat_interleave
torch.repeat_interleave(input, repeats, dim=None) → Tensor
# 不指定维度会输出一个向量
例子:
# 一维张量,不指定dim,每个元素重复2次
x = torch.tensor([1, 2, 3])
torch.repeat_interleave(x, 2)
# 输出: tensor([1, 1, 2, 2, 3, 3])
# 二维张量,不指定dim,每个元素重复2次
y = torch.tensor([[1, 2], [3, 4]])
torch.repeat_interleave(y, 2)
# 输出: tensor([1, 1, 2, 2, 3, 3, 4, 4])
# 二维张量,指定dim=0,每行元素重复3次
torch.repeat_interleave(y, 3, dim=0)
# 输出: tensor([[1, 2], [1, 2], [1, 2], [3, 4], [3, 4], [3, 4]])
# 二维张量,指定dim=1,每列元素重复3次
torch.repeat_interleave(y, 3, dim=1)
# 输出: tensor([[1, 1, 1, 2, 2, 2], [3, 3, 3, 4, 4, 4]])
# 二维张量,指定dim=0,不同行元素重复不同次数
torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0)
# 输出: tensor([[1, 2], [3, 4], [3, 4]])
函数3:sequence_mask
#代码十分优雅
def sequence_mask(X, valid_len, value=0):
"""在序列中屏蔽不相关的项"""
maxlen = X.size(1) # 获得数据表示范围的长度
mask = torch.arange((maxlen), dtype=torch.float32,
device=X.device)[None, :] < valid_len[:, None]
X[~mask] = value # ~mask取反,True -> False False -> True
return X
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))
结果:
mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]
关键代码分析
步骤1:创建位置索引
python
torch.arange((maxlen), dtype=torch.float32, device=X.device)
# 生成: [0, 1, 2, ..., maxlen-1]
# 例如 maxlen=5: [0, 1, 2, 3, 4]
步骤2:调整形状以便广播
python
[None, :] # 形状从 [maxlen] 变为 [1, maxlen]
valid_len[:, None] # 形状从 [batch_size] 变为 [batch_size, 1]
步骤3:创建布尔掩码
python
mask = position_indices < valid_len_expanded
举例:
text
假设:
- maxlen = 5
- valid_len = [3, 4] (批次大小=2)
位置索引: [[0, 1, 2, 3, 4]] (形状: [1, 5])
有效长度: [[3], [4]] (形状: [2, 1])
广播比较:
[[0,1,2,3,4] < [3]] → [T,T,T,F,F] # 第一个序列
[[0,1,2,3,4] < [4]] → [T,T,T,T,F] # 第二个序列
最终掩码:
[[True, True, True, False, False],
[True, True, True, True, False]]
加性注意力

#@save
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)
整体分析
输入输出分析
输入张量
queries:(batch_size, num_queries, query_size)keys:(batch_size, num_kv_pairs, key_size)values:(batch_size, num_kv_pairs, value_dim)valid_lens:(batch_size,)或(batch_size, num_queries)
输出张量
output:(batch_size, num_queries, value_dim)
逐步骤详细分析
步骤1: 线性投影
queries, keys = self.W_q(queries), self.W_k(keys)
作用: 将查询和键映射到相同的隐藏维度
W_q: 将查询从query_size投影到num_hiddensW_k: 将键从key_size投影到num_hiddens- 输出形状:
queries:(batch_size, num_queries, num_hiddens)keys:(batch_size, num_kv_pairs, num_hiddens)
步骤2: 维度扩展与广播相加
python
features = queries.unsqueeze(2) + keys.unsqueeze(1)
张量形状变换:
queries.unsqueeze(2):(batch_size, num_queries, 1, num_hiddens)keys.unsqueeze(1):(batch_size, 1, num_kv_pairs, num_hiddens)- 广播机制: 两个张量在维度1和2上分别扩展,形成所有可能的查询-键组合
- 输出形状:
(batch_size, num_queries, num_kv_pairs, num_hiddens)
数学意义:
- 为每个
(查询i, 键j)对创建一个组合特征 - 相当于:
features[i,j,k] = queries[i,j] + keys[i,k]
步骤3: 非线性激活
python
features = torch.tanh(features)
作用:
- 引入非线性变换,增强模型表达能力
- 将特征值限制在[-1, 1]范围内,提高数值稳定性
- 形状保持不变:
(batch_size, num_queries, num_kv_pairs, num_hiddens)
步骤4: 计算注意力分数
python
scores = self.w_v(features).squeeze(-1)
操作分解:
self.w_v(features): 将num_hiddens维特征映射到标量分数- 输入:
(batch_size, num_queries, num_kv_pairs, num_hiddens) - 输出:
(batch_size, num_queries, num_kv_pairs, 1)
- 输入:
squeeze(-1): 移除最后一个维度- 输出:
(batch_size, num_queries, num_kv_pairs)
- 输出:
数学表达: scores[i,j,k] = w_v^T · tanh(queries[i,j] + keys[i,k])
步骤5: 计算注意力权重
python
self.attention_weights = masked_softmax(scores, valid_lens)
关键功能:
- 应用softmax将分数转换为概率分布
- 使用
masked_softmax处理变长序列,确保padding位置权重为0 - 保存注意力权重便于可视化和分析
- 输出形状:
(batch_size, num_queries, num_kv_pairs)
步骤6: 输出计算
python
return torch.bmm(self.dropout(self.attention_weights), values)
操作流程:
self.dropout(attention_weights): 防止过拟合torch.bmm(): 批量矩阵乘法- 注意力权重:
(batch_size, num_queries, num_kv_pairs) - 值矩阵:
(batch_size, num_kv_pairs, value_dim) - 输出:
(batch_size, num_queries, value_dim)
- 注意力权重:
数学表达: output[i,j] = Σ_k attention_weights[i,j,k] · values[i,k]
函数1:传播机制
features = queries.unsqueeze(2) + keys.unsqueeze(1)
目的:queries 和 keys 加在一起
张量形状分析
假设:
queries形状:(batch_size, num_queries, num_hiddens)keys形状:(batch_size, num_kv_pairs, num_hiddens)
unsqueeze操作
queries.unsqueeze(2)
- 原始形状:
(batch_size, num_queries, num_hiddens) - unsqueeze(2)后:
(batch_size, num_queries, 1, num_hiddens) - 在第2个维度(0-based索引)插入一个长度为1的维度
keys.unsqueeze(1)
- 原始形状:
(batch_size, num_kv_pairs, num_hiddens) - unsqueeze(1)后:
(batch_size, 1, num_kv_pairs, num_hiddens) - 在第1个维度插入一个长度为1的维度
广播机制
现在两个张量的形状:
- queries:
(batch_size, num_queries, 1, num_hiddens) - keys:
(batch_size, 1, num_kv_pairs, num_hiddens)
PyTorch的广播规则:
- 从尾部维度开始比较
- 当维度大小相等或其中一个为1时,可以进行广播
- 大小为1的维度会被扩展以匹配另一个张量
广播后的实际计算:
text
queries 被广播为: (batch_size, num_queries, num_kv_pairs, num_hiddens)
keys 被广播为: (batch_size, num_queries, num_kv_pairs, num_hiddens)
函数2:torch.bmm
在 PyTorch 中,torch.bmm 函数用于执行批量矩阵乘法。具体来说,它计算两个 3D 张量的矩阵乘法。假设 input 是一个形状为 (b, n, m) 的张量,mat2 是一个形状为 (b, m, p) 的张量,那么 torch.bmm(input, mat2) 将返回一个形状为 (b, n, p) 的张量。
注意:
如果
input是一个 (b×n×m) 张量,mat2是一个 (b×m×p) 张量,则out将是一个 (b×n×p) 张量。
示例
import torch
# 创建两个 3D 张量
input = torch.randn(10, 3, 4)
mat2 = torch.randn(10, 4, 5)
# 执行批量矩阵乘法
result = torch.bmm(input, mat2)
# 打印结果的形状
print(result.size())
输出:
torch.Size([10, 3, 5])
问题1 :self.attention_weights为什么是这个形状的?
self.attention_weights形状分析
self.attention_weights的形状是(batch_size, num_queries, num_kv_pairs),这个形状的设计有深刻的数学和语义含义。
形状的数学含义
三维张量的语义
- 第0维
batch_size: 批量中的样本数量 - 第1维
num_queries: 每个样本中的查询数量 - 第2维
num_kv_pairs: 每个样本中的键值对数量
具体解释
对于批量中的每个样本i,每个查询j,每个键k:
text
attention_weights[i, j, k] = 第i个样本中,第j个查询对第k个键的注意力权重
为什么是这个形状?
1. 注意力机制的本质
注意力机制的核心思想是:每个查询应该关注所有键值对中的哪些部分
python
# 伪代码表示
for i in range(batch_size): # 遍历每个样本
for j in range(num_queries): # 遍历每个查询
for k in range(num_kv_pairs): # 遍历每个键
weight = 计算注意力(查询[j], 键[k])
attention_weights[i, j, k] = weight
2. 实际应用场景示例
机器翻译场景
text
输入句子: "I love machine learning" (4个词)
输出句子: "我 喜欢 机器 学习" (4个词)
batch_size = 1
num_queries = 4 (输出句子的4个词)
num_kv_pairs = 4 (输入句子的4个词)
attention_weights[0, :, :] 的形状是 (4, 4),表示:
- 行:输出词的查询(我,喜欢,机器,学习)
- 列:输入词的键(I, love, machine, learning)
- 每个元素:输出词对输入词的关注程度
文本分类场景
text
输入文档: 包含多个句子,每个句子有多个词
查询: [CLS] token (用于分类)
batch_size = N
num_queries = 1 ([CLS] token)
num_kv_pairs = 文档长度
attention_weights[:, 0, :] 表示每个文档中[CLS] token对所有词的关注程度
形状的合理性验证
与最终输出的匹配
python
# 注意力权重形状: (batch_size, num_queries, num_kv_pairs)
# 值矩阵形状: (batch_size, num_kv_pairs, value_dim)
# 输出形状: (batch_size, num_queries, value_dim)
output = torch.bmm(attention_weights, values)
矩阵乘法维度验证:
text
(batch, nq, nk) @ (batch, nk, v_dim) = (batch, nq, v_dim) ✓
与掩码机制的兼容
python
self.attention_weights = masked_softmax(scores, valid_lens)
scores形状:(batch_size, num_queries, num_kv_pairs)valid_lens形状:(batch_size,)或(batch_size, num_queries)- 掩码操作在最后一个维度
num_kv_pairs上应用,符合逻辑
为什么不是其他形状?
可能的替代形状及其问题
(batch_size, num_kv_pairs, num_queries)- 问题:与值矩阵的矩阵乘法无法直接进行
- 需要转置,增加计算复杂度
(batch_size, num_queries \* num_kv_pairs)- 问题:丢失了查询-键的对应关系结构
- 难以解释和可视化
(batch_size, 1, num_queries, num_kv_pairs)- 问题:不必要的维度,增加内存占用
- 对实际计算没有帮助
更多推荐


所有评论(0)