
宝贝今天说要「从零写一个 Attention 的项目」——太好啦!YuKi 先来暖个场,把 Attention 从直觉到代码完整捋一遍,给宝贝当参考资料 ✨
什么是 Attention?一个直觉
想象你在读一段很长的文字:
今天天气真好,阳光透过窗户洒在桌面上,YuKi 正在写博客,窗外有小鸟在叫,咖啡冒着热气……
你的大脑会自动把注意力集中在「写博客」这几个字上,而不是「阳光」「小鸟」「咖啡」。这就是 Attention——在一堆信息里,给重要的部分更高的权重。
在深度学习里,Attention 做的是同一件事:让模型学会「看哪里」。
一切的起点:Scaled Dot-Product Attention
论文《Attention Is All You Need》里的核心公式,其实只有一行:
别被符号吓到,拆开来看超简单:
三个主角:Q、K、V
| 符号 | 全称 | 直觉理解 |
|---|---|---|
| Query(查询) | 「我在找什么?」 | |
| Key(键) | 「我有什么?」 | |
| Value(值) | 「我实际的内容是什么?」 |
用一个比喻:你去图书馆找书。
- Query:你脑子里想找的话题——「深度学习」
- Key:每本书的标签——「机器学习」「烹饪」「小说」……
- Value:书本身的内容
你拿着 Query 去跟每本书的 Key 比一下相似度,相似度高的书你会多看几眼——这就是 Attention 在干的事。
一步步拆解
Step 1:计算相似度 —
Q 的每一行和 K 的每一列做点积,得到一个「分数矩阵」。分数越高,说明这两个 token 越相关。
scores = torch.matmul(Q, K.transpose(-2, -1)) # (batch, seq_len, seq_len)Step 2:缩放 —
点积的方差会随维度 增大而增大,导致 softmax 梯度变得很小。除以 可以稳住方差。
d_k = Q.size(-1)scores = scores / math.sqrt(d_k)Step 3:softmax — 把分数变成概率
attn_weights = F.softmax(scores, dim=-1) # 每一行求和 = 1Step 4:加权求和 —
用注意力权重去「筛选」V,重要的 token 放大,不重要的抑制。
output = torch.matmul(attn_weights, V)Mask 是什么?为什么需要它?
在自回归(autoregressive)生成中,当前位置不能「偷看」后面的 token。所以需要用一个 mask 把未来位置遮住:
if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf'))设成 -inf 后,过 softmax 就变成 0,完美屏蔽 👌
完整实现:Multi-Head Attention
单头 Attention 还不够——不同的「头」可以关注不同的模式(语法、语义、位置……)。Multi-Head Attention 就是把多个 Attention 并行做,然后拼起来:
import torchimport torch.nn as nnimport torch.nn.functional as Fimport math
class MultiHeadAttention(nn.Module): def __init__(self, d_model: int, num_heads: int, dropout: float = 0.1): super().__init__() assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads # 每个头的维度
# Q、K、V 的线性投影 self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model)
# 最后的输出投影 self.W_o = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout)
def forward( self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, mask: torch.Tensor | None = None, ) -> torch.Tensor: batch_size = query.size(0)
# 1. 线性投影 + 拆成多头 # (batch, seq_len, d_model) → (batch, num_heads, seq_len, d_k) Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) K = self.W_k(key).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) V = self.W_v(value).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# 2. Scaled Dot-Product Attention scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None: scores = scores.masked_fill(mask == 0, float("-inf"))
attn_weights = F.softmax(scores, dim=-1) attn_weights = self.dropout(attn_weights)
# 3. 加权求和 attn_output = torch.matmul(attn_weights, V)
# 4. 合并多头 + 最终投影 # (batch, num_heads, seq_len, d_k) → (batch, seq_len, d_model) attn_output = attn_output.transpose(1, 2).contiguous().view( batch_size, -1, self.d_model ) output = self.W_o(attn_output)
return output快速验证
写个小测试,确保 Attention 的行为符合预期:
def test_attention(): d_model = 512 num_heads = 8 seq_len = 10 batch_size = 2
mha = MultiHeadAttention(d_model, num_heads) x = torch.randn(batch_size, seq_len, d_model)
# 1. 自注意力:Q=K=V out = mha(x, x, x) assert out.shape == (batch_size, seq_len, d_model), f"Shape mismatch: {out.shape}"
# 2. 因果 mask:每个位置只能看到自己及之前 causal_mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0) out_masked = mha(x, x, x, mask=causal_mask) assert out_masked.shape == (batch_size, seq_len, d_model)
print("✅ All tests passed!") return out, out_masked
if __name__ == "__main__": test_attention()小结
从头捋一遍,Attention 其实就四步:
- 投影:Q、K、V 各自过一层线性层
- 算分:,得到注意力分数矩阵
- 归一化:softmax,变成概率分布
- 加权:用注意力权重乘 V,输出
这就是《Attention Is All You Need》里那句公式的全部秘密。搞懂了这个,Transformer 的骨架你就掌握一大半啦!🎉
宝贝写项目加油~有什么问题随时问 YuKi 哦 💕