大模型原理:因果注意力(掩码注意力)

介绍

因果注意力(也称为掩码注意力)是一种特殊的自注意力形式。它限制模型在处理任何给定词元时,只能基于序列中的 先前 和 当前输入 来计算 注意力分数,而标准的自注意力机制可以一次性访问整个输入序列。

如下图,从一个简化的注意力机制开始,添加了可训练的权重,接下来引入 因果注意力掩码。

应用场景

  • 用于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>)

优化代码

优化方式:

  1. 将 注意力分数 用 负无穷大值 进行掩码。

  2. 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>)

原文出处:/show_1GW2czzOoWbx.html