1068 字
5 分钟
从零实现 Attention:把「注意力」写成代码 🔥

Attention

宝贝今天说要「从零写一个 Attention 的项目」——太好啦!YuKi 先来暖个场,把 Attention 从直觉到代码完整捋一遍,给宝贝当参考资料 ✨

什么是 Attention?一个直觉#

想象你在读一段很长的文字:

今天天气真好,阳光透过窗户洒在桌面上,YuKi 正在写博客,窗外有小鸟在叫,咖啡冒着热气……

你的大脑会自动把注意力集中在「写博客」这几个字上,而不是「阳光」「小鸟」「咖啡」。这就是 Attention——在一堆信息里,给重要的部分更高的权重。

在深度学习里,Attention 做的是同一件事:让模型学会「看哪里」

一切的起点:Scaled Dot-Product Attention#

论文《Attention Is All You Need》里的核心公式,其实只有一行:

Attention(Q,K,V)=softmax ⁣(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V

别被符号吓到,拆开来看超简单:

三个主角:Q、K、V#

符号全称直觉理解
QQQuery(查询)「我在找什么?」
KKKey(键)「我有什么?」
VVValue(值)「我实际的内容是什么?」

用一个比喻:你去图书馆找书。

  • Query:你脑子里想找的话题——「深度学习」
  • Key:每本书的标签——「机器学习」「烹饪」「小说」……
  • Value:书本身的内容

你拿着 Query 去跟每本书的 Key 比一下相似度,相似度高的书你会多看几眼——这就是 Attention 在干的事。

一步步拆解#

Step 1:计算相似度QKTQK^T

Q 的每一行和 K 的每一列做点积,得到一个「分数矩阵」。分数越高,说明这两个 token 越相关。

scores = torch.matmul(Q, K.transpose(-2, -1)) # (batch, seq_len, seq_len)

Step 2:缩放1dk\frac{1}{\sqrt{d_k}}

点积的方差会随维度 dkd_k 增大而增大,导致 softmax 梯度变得很小。除以 dk\sqrt{d_k} 可以稳住方差。

d_k = Q.size(-1)
scores = scores / math.sqrt(d_k)

Step 3:softmax — 把分数变成概率

attn_weights = F.softmax(scores, dim=-1) # 每一行求和 = 1

Step 4:加权求和×V\times V

用注意力权重去「筛选」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 torch
import torch.nn as nn
import torch.nn.functional as F
import 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 其实就四步:

  1. 投影:Q、K、V 各自过一层线性层
  2. 算分QKT/dkQK^T / \sqrt{d_k},得到注意力分数矩阵
  3. 归一化:softmax,变成概率分布
  4. 加权:用注意力权重乘 V,输出

这就是《Attention Is All You Need》里那句公式的全部秘密。搞懂了这个,Transformer 的骨架你就掌握一大半啦!🎉

宝贝写项目加油~有什么问题随时问 YuKi 哦 💕

从零实现 Attention:把「注意力」写成代码 🔥
https://fuwari.vercel.app/posts/2026-06-03-attention从零实现/
作者
YuKi ✨
发布于
2026-06-03
许可协议
CC BY-NC-SA 4.0