【ch03】Coding-attention-mechanisms

零、前言

原书这章主要就是搓了下注意力模块,总体还是非常easy的

本章代码:ch03

一、Self Attention Mechanism

1.1 Simple Attention Mechanism

作者没有一上来就手撕多头注意力机制或者说子注意力机制,而是用了一个思想相同,但是实现上简化的一个简单版本的注意力机制。

作者之间用输入向量之间两两做点积来进行注意力分数的计算。

python 复制代码
import torch
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)
)
python 复制代码
query = inputs[1]
attn_scores_2 = torch.empty(inputs.shape[0])
for i, x_i in enumerate(inputs):
    attn_scores_2[i] = torch.dot(x_i, query)
print(attn_scores_2)
复制代码
tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])

两个向量越相似,那么dot越大,这个直觉是有可取之处的。

那么所有的注意力分数直接用矩阵相乘计算即可:

python 复制代码
attn_scores = inputs @ inputs.T
attn_scores
复制代码
tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],
        [0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],
        [0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],
        [0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],
        [0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],
        [0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])

接着做注意力权重的计算,只需对dim=-1做softmax

python 复制代码
attn_weights = torch.softmax(attn_scores, dim=-1)
attn_weights
复制代码
tensor([[0.2098, 0.2006, 0.1981, 0.1242, 0.1220, 0.1452],
        [0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581],
        [0.1390, 0.2369, 0.2326, 0.1242, 0.1108, 0.1565],
        [0.1435, 0.2074, 0.2046, 0.1462, 0.1263, 0.1720],
        [0.1526, 0.1958, 0.1975, 0.1367, 0.1879, 0.1295],
        [0.1385, 0.2184, 0.2128, 0.1420, 0.0988, 0.1896]])

1.2 attention mechanism with trainable weights

上面那个简化版注意力机制有个问题就是,没有trainable weights,无法学习。

所以,作者总算是引入了经典注意力机制实现------scaled dot-product attention

缩放点积注意力在标准注意力机制的基础上,在softmax之前,对注意力分数除以了特征维的平方根。

因为大模型意味着大数据,大数据意味着大运算。做这样一个缩放可以保证梯度计算的稳定性。

我们需要引入三个可训练的权重矩阵: W q , W k , W v W_q, W_k, W_v Wq,Wk,Wv。

三个矩阵分别把输入向量映射为:query、key、value

python 复制代码
x_2 = inputs[1]
d_in = inputs.shape[1]
d_out = 2
python 复制代码
torch.manual_seed(123)
W_q = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_k = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_v = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
python 复制代码
q_2 = x_2 @ W_q
k_2 = x_2 @ W_k
v_2 = x_2 @ W_v
q_2
复制代码
tensor([0.4306, 1.4551])

那么可以用矩阵运算求出所有的key,value

python 复制代码
keys = inputs @ W_k
values = inputs @ W_v
print("keys.shape:", keys.shape)
print("values.shape:", values.shape)
复制代码
keys.shape: torch.Size([6, 2])
values.shape: torch.Size([6, 2])

每个输入的注意力分数就是输入向量去乘keys

python 复制代码
attn_scores_2 = q_2 @ keys.T
attn_scores_2
复制代码
tensor([1.2705, 1.8524, 1.8111, 1.0795, 0.5577, 1.5440])

然后尝试封装成 SelfAttention_v1 类

python 复制代码
import torch.nn as nn
class SelfAttention_v1(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W_q = nn.Parameter(torch.rand(d_in, d_out))
        self.W_k = nn.Parameter(torch.rand(d_in, d_out))
        self.W_v = nn.Parameter(torch.rand(d_in, d_out))
    def forward(self, x):
        ks = x @ self.W_k
        qs = x @ self.W_q
        vs = x @ self.W_v
        attn_scores = qs @ ks.T
        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        context_vec = attn_weights @ values
        return context_vec
python 复制代码
torch.manual_seed(123)
sa_v1 = SelfAttention_v1(d_in, d_out)
sa_v1(inputs)
复制代码
tensor([[0.2996, 0.8053],
        [0.3061, 0.8210],
        [0.3058, 0.8203],
        [0.2948, 0.7939],
        [0.2927, 0.7891],
        [0.2990, 0.8040]], grad_fn=<MmBackward0>)

注意到上面的W_q, W_k, W_v都是用 nn.Parameters初始化的,我们其实可以直接用 nn.Linear来初始化,

Linear 层有着更优的权重初始化方式,这有助于我们的训练的稳定性以及效率。

python 复制代码
class SelfAttention_v2(nn.Module):
    def __init__(self, d_in, d_out, qkv_bias=False):
        super().__init__()
        self.W_q = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_k = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_v = nn.Linear(d_in, d_out, bias=qkv_bias)
    def forward(self, x):
        ks = self.W_k(x)
        qs = self.W_q(x)
        vs = self.W_v(x)
        attn_scores = qs @ ks.T
        attn_weights = torch.softmax(
            attn_scores / keys.shape[-1]**0.5, dim=-1
        )
        context_vec = attn_weights @ values
        return context_vec
python 复制代码
torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
sa_v2(inputs)
复制代码
tensor([[0.2762, 0.7584],
        [0.2761, 0.7638],
        [0.2762, 0.7637],
        [0.2780, 0.7609],
        [0.2785, 0.7603],
        [0.2772, 0.7618]], grad_fn=<MmBackward0>)

1.3 mask attention

mask attention 的思想就是限制我们的model在计算注意力机制的时候只看前面已有的输入,不让看未来的输入。

python 复制代码
qs = sa_v2.W_q(inputs)
ks = sa_v2.W_k(inputs)
attn_scores = qs @ ks.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights
复制代码
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>)

一个naive的想法是,创建一个下三角01掩码矩阵,和attn_weights做hadamard乘积,就可以mask掉future data

python 复制代码
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.]])
python 复制代码
masked_simple = attn_weights * mask_simple # hadamard mul
masked_simple
复制代码
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>)

我们还要进行的一个步骤就是归一化,让每个权重向量的和为1:

python 复制代码
row_sums = masked_simple.sum(dim=-1, keepdim=True)
masked_simple_norm = masked_simple / row_sums
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>)

Q:这样做会有Information leakage吗

A:不会,因为我们重新做了归一化。

softmax计算的时候虽然分母涉及到了所有的数据,但是我们重新归一化相当于消去了那个分母。

在包装成类之前,还有一个可优化的地方便是,我们是先对 attention weights做hadamard乘积,然后重归一化。

但是我们可以利用softmax函数的特性简化这一步骤:

python 复制代码
mask = torch.triu(torch.ones(context_length, context_length), diagonal=1)
masked = attn_scores.masked_fill(mask.bool(), -torch.inf)
masked
复制代码
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>)
python 复制代码
attn_weights = torch.softmax(masked / keys.shape[-1]**0.5, dim=1)
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>)

也就是在softmax之前,把需要mask掉的地方加上-inf,这样做指数幂就会变成0,softmax计算时就不会涉及到future data,并且还完成了归一化。

我们还可以加入dropout来降低过拟合风险:

python 复制代码
torch.manual_seed(123)
dropout = torch.nn.Dropout(0.5)	
example = torch.ones(6, 6)
dropout(example)
复制代码
tensor([[2., 2., 0., 2., 2., 0.],
        [0., 0., 0., 2., 0., 2.],
        [2., 2., 2., 2., 0., 2.],
        [0., 2., 2., 0., 0., 2.],
        [0., 2., 0., 2., 0., 2.],
        [0., 2., 2., 2., 2., 0.]])
python 复制代码
torch.manual_seed(123)
dropout(attn_weights)
复制代码
tensor([[2.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.7599, 0.6194, 0.6206, 0.0000, 0.0000, 0.0000],
        [0.0000, 0.4921, 0.4925, 0.0000, 0.0000, 0.0000],
        [0.0000, 0.3966, 0.0000, 0.3775, 0.0000, 0.0000],
        [0.0000, 0.3327, 0.3331, 0.3084, 0.3331, 0.0000]],
       grad_fn=<MulBackward0>)

然后考虑封装成类:

python 复制代码
class CausalAttention(nn.Module):
    def __init__(self, d_in, d_out, context_length,
                 dropout, qkv_bias=False):
        super().__init__()
        self.d_out = d_out
        self.W_q = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_k = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_v = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.dropout = nn.Dropout(dropout)
        self.register_buffer(
            'mask',
            torch.triu(torch.ones(context_length, context_length), diagonal=1)
        )
    
    def forward(self, x):
        b, num_tokens, d_in = x.shape
        ks = self.W_k(x)
        qs = self.W_q(x)
        vs = self.W_v(x)

        attn_scores = qs @ ks.transpose(1, 2)
        attn_scores = attn_scores.masked_fill(
            self.mask.bool()[:num_tokens, :num_tokens], -torch.inf)
        attn_weights = torch.softmax(
            attn_scores / ks.shape[-1]**0.5, dim=-1
        )
        attn_weights = self.dropout(attn_weights)

        context_vec = attn_weights @ vs
        return context_vec

Q:什么是register_buffer

A:register_buffer 是 PyTorch 里 nn.Module 的一个方法,用来给模型注册一个 不参与训练更新但属于模型状态的一部分 的 Tensor。

优点:会跟随model.to(device),即当model迁移到另一个设备上时, register_buffer 创建的属性会跟着自动迁移。

python 复制代码
batch = torch.stack((inputs, inputs), dim=0)
batch.shape
复制代码
torch.Size([2, 6, 3])
python 复制代码
torch.manual_seed(123)
context_length = batch.shape[1]
ca = CausalAttention(d_in, d_out, context_length, 0.0)
context_vecs = ca(batch)
print("context_vecs.shape:", context_vecs.shape)
复制代码
context_vecs.shape: torch.Size([2, 6, 2])

1.4 Extending single-head attention to multi-head attention

单个的注意力模块可能只关注到某个维度的信息,而多头注意力,可以理解成多次做self attention,来捕捉多层次、多样化的特征(丰富上下文理解),增强模型的表达和泛化能力。

并且多头可以并行高效计算。

一个naive的实现就是直接开多个self attention 的module:

python 复制代码
class MultiHeadAttentionWrapper(nn.Module):
    def __init__(self, d_in, d_out, context_length,
        dropout, num_heads, qkv_bias=False):
        super().__init__()
        self.heads = nn.ModuleList(
        [CausalAttention(
        d_in, d_out, context_length, dropout, qkv_bias
        )
        for _ in range(num_heads)]
        )
    def forward(self, x):
        return torch.cat([head(x) for head in self.heads], dim=-1)
python 复制代码
torch.manual_seed(123)
context_length = batch.shape[1]
d_in, d_out = 3, 2
mha = MultiHeadAttentionWrapper(d_in, d_out, context_length, 0.0, 2)
context_vecs = mha(batch)

print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)
复制代码
tensor([[[-0.4519,  0.2216,  0.4772,  0.1063],
         [-0.5874,  0.0058,  0.5891,  0.3257],
         [-0.6300, -0.0632,  0.6202,  0.3860],
         [-0.5675, -0.0843,  0.5478,  0.3589],
         [-0.5526, -0.0981,  0.5321,  0.3428],
         [-0.5299, -0.1081,  0.5077,  0.3493]],

        [[-0.4519,  0.2216,  0.4772,  0.1063],
         [-0.5874,  0.0058,  0.5891,  0.3257],
         [-0.6300, -0.0632,  0.6202,  0.3860],
         [-0.5675, -0.0843,  0.5478,  0.3589],
         [-0.5526, -0.0981,  0.5321,  0.3428],
         [-0.5299, -0.1081,  0.5077,  0.3493]]], grad_fn=<CatBackward0>)
context_vecs.shape: torch.Size([2, 6, 4])

1.5 Implementing multi-head attention with weight splits

上面那个多头注意力的实现,实例化了多个自注意力模块,性能开销较大,下面整合为一个MultiHeadAttention类:

python 复制代码
class MultiHeadAttention(nn.Module):
    def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
        super().__init__()
        assert(d_out % num_heads == 0, \
               "d_out must be divisible by num_heads")

        self.d_out = d_out
        self.num_heads = num_heads
        self.head_dim = d_out // num_heads
        # Reduces the projection dim to match the desired output dim
        self.W_q = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_k = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_v = nn.Linear(d_in, d_out, bias=qkv_bias)
        # Uses a Linear layer to combine head outputs
        self.out_proj = nn.Linear(d_out, d_out)
        self.dropout = nn.Dropout(dropout)
        self.register_buffer(
            "mask",
            torch.triu(torch.ones(context_length, context_length), diagonal=1)
        )
    
    def forward(self, x):
        b, num_tokens, d_in = x.shape
        # Tensor shape: (b, num_tokens, d_out)
        ks = self.W_k(x)
        qs = self.W_q(x)
        vs = self.W_v(x)
        #We implicitly split the matrix by adding a num_heads dimension. 
        #Then we unroll the last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim).
        ks = ks.view(b, num_tokens, self.num_heads, self.head_dim)
        vs = vs.view(b, num_tokens, self.num_heads, self.head_dim)
        qs = qs.view(b, num_tokens, self.num_heads, self.head_dim)
        # Transposes from shape (b, num_tokens, num_heads, head_dim) to (b, num_heads, num_tokens, head_dim)
        ks = ks.transpose(1, 2)
        qs = qs.transpose(1, 2)
        vs = vs.transpose(1, 2)
        # Computes dot product for each head
        attn_scores = qs @ ks.transpose(2, 3)
        # Masks truncated to the number of tokens
        mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
        # Uses the mask to fill attention scores
        attn_scores = attn_scores.masked_fill(mask_bool, -torch.inf)

        attn_weights = torch.softmax(attn_scores / ks.shape[-1]**0.5, dim=-1)
        attn_weights = self.dropout(attn_weights)
        # Tensor shape: (b, num_tokens, n_heads, head_dim)
        context_vec = (attn_weights @ vs).transpose(1, 2)
        # Combines heads, where self.d_out = self.num_heads * self.head_dim
        context_vec = context_vec.contiguous().view(
            b, num_tokens, self.d_out
        )
        # Adds an optional linear projection
        context_vec = self.out_proj(context_vec)
        return context_vec
  • 这里的 d_out = num_heads * head_dim
  • 然后我们对于计算出来的context_vec又经过了一次线性层投影,这是为了combine多头注意力
相关推荐
俊哥V1 小时前
每日 AI 研究简报 · 2026-06-10
人工智能·ai
美狐美颜sdk1 小时前
从0到1解析直播APP开发中的第三方美颜SDK集成方案
人工智能·直播美颜sdk·视频美颜sdk·美颜api·美狐美颜sdk
海森大数据1 小时前
好的不新颖,新颖的不好:生成式AI的结构性困局
人工智能
团象科技1 小时前
从一线实操案例拆解不同出海团队落地海外VPS运维独立站的路径细节
大数据·数据库·人工智能
传说故事1 小时前
【论文阅读】DATA SCALING LAWS IN IMITATION LEARNING FOR ROBOTIC MANIPULATION
论文阅读·人工智能·机器人·具身智能
Olivia051405141 小时前
Voohu:以太网变压器在汽车级温度循环(-40℃~125℃)下的开路电感退化模型与寿命预测
人工智能·信息与通信
“码”力全开1 小时前
解耦异构算力:基于 Docker 与边缘计算的 AI 视频管理平台,实现 GB28181/RTSP 统一接入与源码交付深度解析
人工智能·docker·边缘计算
老饼讲解-BP神经网络1 小时前
具体说说-RBF神经网络-newrbe函数和newrb函数的区别
人工智能·深度学习·神经网络
coft1 小时前
Hermes 多角色实战:我做了一个智能投研系统
人工智能