介绍
因果注意力(也称为掩码注意力)是一种特殊的自注意力形式。它限制模型在处理任何给定词元时,只能基于序列中的 先前 和 当前输入 来计算 注意力分数,而标准的自注意力机制可以一次性访问整个输入序列。
如下图,从一个简化的注意力机制开始,添加了可训练的权重,接下来引入 因果注意力掩码。

应用场景
- 用于Transformer 解码器(如GPT、LLaMA、Transformer-TTS);
- 适合 生成类 任务:文本生成、机器翻译(解码阶段)、语音合成、时间序列预测;
- 核心价值:保障生成的时序逻辑,避免模型“提前看到未来信息”而作弊。
简单理解
比如在写作文时,手写一句话 我喜欢吃苹果,写第2个字 喜 时,还没写 欢 和 吃 ,所以只能看到已经写的 我 和 喜 ——这就是因果注意力的逻辑。
可视化(同样用“句子+表格”)
还是句子[我, 喜, 欢, 吃](位置1/2/3/4),因果注意力的“可见范围”:
| 正在处理的位置(行) | 能参考的位置(列) | 通俗解释 |
|---|---|---|
| 位置1(我) | 1 | 写“我”时,只看到自己(还没写任何其他字) |
| 位置2(喜) | 1、2 | 写“喜”时,只能看到已经写的“我”和刚写的“喜” |
| 位置3(欢) | 1、2、3 | 写“欢”时,只能看到“我、喜、欢”(还没写“吃”) |
| 位置4(吃) | 1、2、3、4 | 写“吃”时,能看到前面所有已写的字 |
提示: 只能看“当前位置及左边(历史)”,右边(未来)全屏蔽,这就是“下三角掩码”的本质——因为这个图案像一个“下三角”:
用“画图”的方式
位置1:√ × × × (只能看自己)
位置2:√ √ × × (能看1、2)
位置3:√ √ √ × (能看1、2、3)
位置4:√ √ √ √ (能看1、2、3、4)
将上图简化如下(下三角区域全是√,上三角全是×,所以叫 下三角掩码):
√ × × ×
√ √ × ×
√ √ √ ×
√ √ √ √
因果注意力原理
修改标准自注意力机制来创建因果注意力机制,对于每个处理的词元,需要 掩码当前词元之后的后续词元,如图:
在因果注意力机制中,掩码对角线以上的注意力权重,以确保在计算上下文向量时,大语言模型 无法访问 未来的词元。
例如:对于第2行的单词“journey”,仅保留当前词(“journey”)和之前词(“Your”)的注意力权重
然后,归一化未掩码的注意力权重,使得 每一行的权重之和为1。
实现
代码使用 大模型原理:实现带可训练权重的自注意力机制-封装类计算所有上下文向量 中的 实现方式2(改进版)
准备-封装类
import torch
import torch.nn as nn
class SelfAttention_v2(nn.Module):
def __init__(self, d_in, d_out, qkv_bias=False):
super().__init__()
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
def forward(self, x):
keys = self.W_key(x)
queries = self.W_query(x)
values = self.W_value(x)
attn_scores = queries @ keys.T
attn_weights = torch.softmax(
attn_scores / keys.shape[-1]**0.5, dim=-1
)
context_vec = attn_weights @ values
return context_vec
准备
# 输入的嵌入向量
inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your (x^1)
[0.55, 0.87, 0.66], # journey (x^2)
[0.57, 0.85, 0.64], # starts (x^3)
[0.22, 0.58, 0.33], # with (x^4)
[0.77, 0.25, 0.10], # one (x^5)
[0.05, 0.80, 0.55]] # step (x^6)
)
# 输入嵌入维度d_in=3
d_in = inputs.shape[1]
# 输出嵌入维度d_out=2
d_out = 2
torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
第1步
通过 softmax 函数计算注意力权重
# 第1步:通过 `softmax` 函数计算注意力权重
print("\n\n第1步:通过 `softmax` 函数计算注意力权重---------")
queries = sa_v2.W_query(inputs)
keys = sa_v2.W_key(inputs)
attn_scores = queries @ keys.T
print("注意力分数:\n", attn_scores)
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
print("注意力权:\n", attn_weights)
执行结果:
第1步:通过 `softmax` 函数计算注意力权重---------
注意力分数:
tensor([[ 0.2899, 0.0716, 0.0760, -0.0138, 0.1344, -0.0511],
[ 0.4656, 0.1723, 0.1751, 0.0259, 0.1771, 0.0085],
[ 0.4594, 0.1703, 0.1731, 0.0259, 0.1745, 0.0090],
[ 0.2642, 0.1024, 0.1036, 0.0186, 0.0973, 0.0122],
[ 0.2183, 0.0874, 0.0882, 0.0177, 0.0786, 0.0144],
[ 0.3408, 0.1270, 0.1290, 0.0198, 0.1290, 0.0078]],
grad_fn=<MmBackward0>)
注意力权:
tensor([[0.1921, 0.1646, 0.1652, 0.1550, 0.1721, 0.1510],
[0.2041, 0.1659, 0.1662, 0.1496, 0.1665, 0.1477],
[0.2036, 0.1659, 0.1662, 0.1498, 0.1664, 0.1480],
[0.1869, 0.1667, 0.1668, 0.1571, 0.1661, 0.1564],
[0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.1585],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<SoftmaxBackward0>)
第2步
使用PyTorch的 tril()函数 创建一个对角线以上元素为 0 的掩码
# 第2步:使用PyTorch的 `tril` 函数创建一个对角线以上元素为0的掩码
print("\n\n第2步:使用PyTorch的 `tril` 函数创建一个对角线以上元素为0的掩码-------")
context_length = attn_scores.shape[0]
mask_simple = torch.tril(torch.ones(context_length, context_length))
print(mask_simple)
执行结果:
tensor([[1., 0., 0., 0., 0., 0.],
[1., 1., 0., 0., 0., 0.],
[1., 1., 1., 0., 0., 0.],
[1., 1., 1., 1., 0., 0.],
[1., 1., 1., 1., 1., 0.],
[1., 1., 1., 1., 1., 1.]])
使对角线上方的值变为0
# 掩码矩阵和注意力权重矩阵相乘,设置权重对角线上方的值变为0:
print("\n\n设置权重对角线上方的值变为0-----")
masked_simple = attn_weights*mask_simple
print("对角线上方的元素:\n", masked_simple)
执行结果:
设置权重对角线上方的值变为0-----
对角线上方的元素:
tensor([[0.1921, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.2041, 0.1659, 0.0000, 0.0000, 0.0000, 0.0000],
[0.2036, 0.1659, 0.1662, 0.0000, 0.0000, 0.0000],
[0.1869, 0.1667, 0.1668, 0.1571, 0.0000, 0.0000],
[0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<MulBackward0>)
第3步
重新 归一化注意力权重,使每一行的总和再次为1。
计算方式:每行中的每个元素除以每行中的和来实现这一点
实现
# 归一化注意力权重
print("\n\n归一化注意力权重-------")
row_sums = masked_simple.sum(dim=-1, keepdim=True)
masked_simple_norm = masked_simple / row_sums
print("归一化注意力权重:\n", masked_simple_norm)
执行结果:
归一化注意力权重:
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],
[0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],
[0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],
[0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<DivBackward0>)
优化代码
优化方式:
softmax函数会将其输入转换为一个概率分布。当输入中出现 负无穷大值 时,softmax函数会将这些值视为 0概率。(从数学角度来看,这是因为 e^{-inf} 无限接近于0)
关键步骤:创建一个 对角线以上 是 1 的掩码,并将这些 1 替换为 负无穷大( -inf )值
第1步:对注意力分数进行掩码
# 优化上面的代码
print("\n\n优化上面代码----------")
mask = torch.triu(torch.ones(context_length, context_length), diagonal=1)
print("下三角掩码(对角线及对角线以下为0):\n", mask)
mask_bool = mask.bool()
print("将掩码转为bool值(0为false、非0为True):\n",mask_bool)
masked = attn_scores.masked_fill(mask_bool , -torch.inf)
print("注意力分数,对角线以上为 -inf:\n", masked)
执行结果:
优化上面代码----------
下三角掩码(对角线及对角线以下为0):
tensor([[0., 1., 1., 1., 1., 1.],
[0., 0., 1., 1., 1., 1.],
[0., 0., 0., 1., 1., 1.],
[0., 0., 0., 0., 1., 1.],
[0., 0., 0., 0., 0., 1.],
[0., 0., 0., 0., 0., 0.]])
将掩码转为bool值(0为false、非0为True):
tensor([[False, True, True, True, True, True],
[False, False, True, True, True, True],
[False, False, False, True, True, True],
[False, False, False, False, True, True],
[False, False, False, False, False, True],
[False, False, False, False, False, False]])
注意力分数,对角线以上为 -inf:
tensor([[0.2899, -inf, -inf, -inf, -inf, -inf],
[0.4656, 0.1723, -inf, -inf, -inf, -inf],
[0.4594, 0.1703, 0.1731, -inf, -inf, -inf],
[0.2642, 0.1024, 0.1036, 0.0186, -inf, -inf],
[0.2183, 0.0874, 0.0882, 0.0177, 0.0786, -inf],
[0.3408, 0.1270, 0.1290, 0.0198, 0.1290, 0.0078]],
grad_fn=<MaskedFillBackward0>)
第2步:执行 softmax函数
对这些掩码结果应用 softmax 函数:
attn_weights = torch.softmax(masked / keys.shape[-1]**0.5, dim=1)
print("归一化注意力权重:\n", attn_weights)
执行结果:
归一化注意力权重:
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],
[0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],
[0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],
[0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<SoftmaxBackward0>)



