第七篇:自注意力(二)—— 缩放、Softmax 与信息融合
2026/8/1 1:39:26 网站建设 项目流程

第七篇:自注意力(二)—— 缩放、Softmax 与信息融合

系列文章:

第一篇:预训练模型——站在巨人的肩膀上

第二篇:分词——文字如何变成数字

第三篇:向量与矩阵——理解一切的基石

第四篇:Embedding 层——从 ID 到向量的第一步

第五篇:位置编码——给 Transformer 装上顺序感

第六篇:自注意力(一)—— Q、K、V 的由来

📖 本文目录

  • 开篇
  • 一、为什么需要缩放?
  • 二、Softmax:把分数变成概率
  • 三、加权求和:融合信息
  • 完整的注意力公式
  • 代码实现:完整的单头自注意力
  • 关键结果:上下文感知
  • 参考资源
  • 本篇小结
  • 参考答案

开篇

前六篇,我们完成了从文本到注意力分数的全部准备:

文本 → 分词 → Embedding → 位置编码 → Q、K、V → Q × Kᵀ = 注意力分数 "我喜欢人工智能" → [101, 2769, 4263, ...] → [0.021, -0.015, ...] → + 位置向量 → × W_Q/W_K/W_V → Q, K, V → Q × Kᵀ = [9, 9] 分数矩阵 ← 上一篇到这里

但我们拿到的原始分数有两个问题:

  1. 数值范围不稳定:点积结果随向量维度增大而发散
  2. 不是概率:无法直接作为"权重"来加权求和

这篇我们将完成自注意力的后半程:缩放 → Softmax → 加权求和。三个步骤,一步比一步精妙。到这篇文章结束,你将完整理解自注意力机制的每一个数学细节,并能在代码中实现它。


一、为什么需要缩放?

问题:点积随维度变大而发散

先看一个简单例子:

2 维向量: q = [0.5, 0.5], k = [0.5, 0.5] q·k = 0.25 + 0.25 = 0.5 4 维向量: q = [0.5, 0.5, 0.5, 0.5], k = [0.5, 0.5, 0.5, 0.5] q·k = 0.25×4 = 1.0 64 维向量(BERT 每个注意力头的维度): q = [0.5, 0.5, ..., 0.5], k = [0.5, 0.5, ..., 0.5] q·k = 0.25×64 = 16.0

维度越大,点积越大。

快速回顾:方差和标准差

方差衡量一组数据"有多分散"。算法:每个数减去平均值,平方,再求平均。

数据:[1, 2, 3, 4, 5],平均值 = 3 方差 = ((1-3)² + (2-3)² + (3-3)² + (4-3)² + (5-3)²) / 5 = (4 + 1 + 0 + 1 + 4) / 5 = 2 标准差 = √方差 = √2 ≈ 1.41

方差大→ 数据很散(比如 [1, 100],方差很大)
方差小→ 数据很集中(比如 [49, 50, 51],方差很小)
标准差→ 方差的平方根,和原始数据同单位,更直观

虽然实际向量不是全 0.5,但统计上,如果 q 和 k 是独立同分布随机变量(均值为 0,方差为 1),则点积的均值为 0,方差为 d_k(d_k 就是向量的维度,BERT 每个注意力头是 64)。维度 d_k 越大,分数的分布越"散"。

这就是所谓的"方差膨胀"问题——注意力分数的方差正比于向量维度 d_k。

为什么分数太散是问题?——梯度消失

来比较有缩放和没缩放的效果:

假设一组分数:[10, 11, 9](d_k=64 时可能的量级) Softmax (无缩放): e¹⁰ ≈ 22026, e¹¹ ≈ 59874, e⁹ ≈ 8103 总和 = 90003 P = [0.245, 0.665, 0.090] ← 基本正常 假设分数:[40, 44, 36](d_k=1024 时可能的量级) Softmax (无缩放): e⁴⁰ ≈ 2.35e17, e⁴⁴ ≈ 1.29e19, e³⁶ ≈ 4.31e15 总和 = 1.31e19 P = [0.018, 0.982, 0.0003] ← 最大值几乎占据全部概率!

当 softmax 输出极度接近 one-hot 时:

  • 最大值位置以外的梯度 ≈ 0
  • 被压制的 token 对应的梯度在反向传播中几乎为零
  • 模型学不动了

这个问题的数学根源是:指数函数 exp(x) 对大的输入非常敏感——exp(44) / exp(36) ≈ e⁸ ≈ 2980,小的差距被指数函数急剧放大。

解法:除以 √d_k

BERT base: hidden_size = 768 num_heads = 12 d_k = hidden_size ÷ num_heads = 768 ÷ 12 = 64 √d_k = √64 = 8 缩放公式:scaled_score = raw_score ÷ √d_k

除以 √d_k 后,分数的方差稳定在 1 左右,softmax 的梯度始终处于"甜区"。

缩放要解决什么问题?

不缩放时,分数会很大: q·k = 64个数相加,每个约±1 → 总和可能达到 ±20 甚至更大 大分数对 softmax 的影响: 输入 [20, -10, 5] softmax → [0.9999, 0.0000, 0.0001] ← 极端分布,接近 one-hot 问题:这种极端分布下,梯度几乎为 0: 假设正确答案是第1个 梯度 = [0.9999-1, 0.0000-0, 0.0001-0] ≈ [-0.0001, 0, 0] → 模型几乎学不到东西

缩放后的效果:

除以 √64 = 8 后,分数变小了: 原分数 [20, -10, 5] → 缩放后 [2.5, -1.25, 0.625] 缩放后 softmax 的输出: [0.76, 0.02, 0.22] ← 有区分度但不极端 梯度正常了: 梯度 = [0.76-1, 0.02-0, 0.22-0] = [-0.24, 0.02, 0.22] → 模型能正常学习

总结:缩放让 softmax 的输入保持在合理范围,避免输出极端化,从而保证梯度能正常传播。


二、Softmax:把分数变成概率

Softmax 公式与直觉

exp(xᵢ) softmax(xᵢ) = ──────────────── Σⱼ exp(xⱼ)

两个关键操作:

操作作用直观理解
exp(指数函数)放大分数差距好的更好、差的更差
归一化(除以总和)保证权重之和为 1形成竞争性概率分布

为什么不用简单归一化(除以最大值)?简单归一化允许负值,且不是概率分布。Softmax 保证所有输出为正且和为 1——这是"概率"的基本要求。

完整演算:以"喜"为例

以"喜"这行为例,展示完整的缩放 + Softmax 过程:

第1步:原始分数(来自 Q×Kᵀ) [15.3, 22.7, 18.2, 2.1, 1.8, 3.2, 2.9, 5.6, 4.1] 对应: 我 喜 欢 人 工 智 能 CLS SEP 第2步:缩放(除以 √d_k = 8) [1.9125, 2.8375, 2.275, 0.2625, 0.225, 0.4, 0.3625, 0.7, 0.5125] 第3步:对每个数求 exp e^1.9125=6.77, e^2.8375=17.07, e^2.275=9.73, e^0.2625=1.30, e^0.225=1.25, e^0.4=1.49, e^0.3625=1.44, e^0.7=2.01, e^0.5125=1.67 第4步:分母 = 所有 exp 之和 = 42.73 第5步:每个 exp ÷ 分母,得到概率(权重) 6.77/42.73=0.16 ← "我":16% 的注意力 17.07/42.73=0.40 ← "喜":40%(自己最高) 9.73/42.73=0.23 ← "欢":23%(组成"喜欢") 1.30/42.73=0.03 1.25/42.73=0.03 1.49/42.73=0.03 1.44/42.73=0.03 2.01/42.73=0.05 1.67/42.73=0.04 ──── 合计 1.00 ✅

Softmax 效果深度分析

缩放前分数:15.3 22.7 18.2 2.1 1.8 3.2 2.9 5.6 4.1 缩放后分数:1.91 2.84 2.28 0.26 0.22 0.40 0.36 0.70 0.51 Softmax概率:0.16 0.40 0.23 0.03 0.03 0.03 0.03 0.05 0.04 效果对比: - 最高分(22.7) 在 softmax 后占比 40% ← 强者恒强 - 次高分(18.2) 在 softmax 后占比 23% ← 第二强 - 低分(1.8) 在 softmax 后占比 3% ← 弱者愈弱 - 分数差别被"非线性放大"了

缩放对 softmax 的影响实验:

importnumpyasnpdefsoftmax(x):e_x=np.exp(x-np.max(x))# 数值稳定版returne_x/e_x.sum()scores=np.array([15.3,22.7,18.2,2.1,1.8])# 不缩放print("不缩放:",softmax(scores))# [0.001, 0.997, 0.002, ≈0, ≈0]# 缩放(÷8)print("缩放:",softmax(scores/8))# [0.187, 0.473, 0.270, 0.036, 0.035]

不缩放的 softmax 几乎退化为 argmax(hard attention),梯度消失。

缩放后的 softmax 保留了更丰富的权重分布(soft attention),梯度良好。


三、加权求和:融合信息

计算过程

有了注意力权重(概率),最后一步就是用这些权重去加权求和所有字的 Value 向量:

"喜"的新向量 = 0.16 × V("我") + 0.40 × V("喜") + 0.23 × V("欢") + 0.03 × V("人") + 0.03 × V("工") + 0.03 × V("智") + 0.03 × V("能") + 0.05 × V(CLS) + 0.04 × V(SEP)

展开前几步(只展示前 3 维,真实是 768 维):

V₁("我") = [ 0.31, -0.12, 0.45, ...] × 0.16 → [ 0.050, -0.019, 0.072, ...] V₂("喜") = [ 0.42, 0.15, -0.08, ...] × 0.40 → [ 0.168, 0.060, -0.032, ...] V₃("欢") = [ 0.05, 0.28, -0.18, ...] × 0.23 → [ 0.012, 0.064, -0.041, ...] ... 全部9个加起来 ... 结果:"喜"的新向量 = [0.215, 0.142, -0.018, ...] (768维)

代码实现:

# 假设已经算好了 attention_weights [1, 9, 9] 和 V [1, 9, 768]attn_output=torch.matmul(attention_weights,V)print(attn_output.shape)# torch.Size([1, 9, 768])# 第2个字("喜")的新向量print(attn_output[0,1,:5])

融合后的效果

加权求和前("喜"的原始向量): [0.862, 0.525, 0.043, ...] ← 只有"喜"自己的信息 加权求和后("喜"的新向量): [0.215, 0.142, -0.018, ...] ← 已融合了"我"和"欢"的信息

这个新向量的关键属性:

信息来源权重效果
"欢"的信息23%"喜欢"整体含义被编码 ✅
"我"的信息16%"谁喜欢"也被编码了 ✅
“人、工、智、能”各 ~3%本字不太需要宾语信息 ✅
"喜"自身40%保留自身语义 ✅

一个类比:集体智慧

自注意力就像一个团队讨论: 每个成员(字)先写下自己的想法(V), 再告诉别人"我擅长什么"(K), 同时提出"我需要什么"(Q)。 然后大家投票决定:谁的发言对我最有价值? 票数 = softmax(Q×K/√d) 最后每个人综合所有人的意见更新自己的认识: 新想法 = 加权求和(所有人的意见) 这个过程在每一层重复一次。

完整的注意力公式

三步合在一起,就是论文里的那个经典公式:

Attention(Q, K, V) = softmax(Q × Kᵀ / √d_k) × V 拆开看: ① Q × Kᵀ → 原始注意力分数 [9, 9] ② ÷ √d_k → 缩放,方差稳定到1 ③ softmax → 变成概率分布,每行和为1 ④ × V → 加权求和,输出 [9, 768]

代码实现:完整的单头自注意力

importtorchimporttorch.nnasnnimportmathclassSingleHeadAttention(nn.Module):"""完整的单头自注意力"""def__init__(self,hidden_size=768):super().__init__()self.W_Q=nn.Linear(hidden_size,hidden_size)self.W_K=nn.Linear(hidden_size,hidden_size)self.W_V=nn.Linear(hidden_size,hidden_size)self.d_k=hidden_size# 每个头的维度defforward(self,x):Q=self.W_Q(x)# [B, L, 768]K=self.W_K(x)# [B, L, 768]V=self.W_V(x)# [B, L, 768]# ① Q × Kᵀ → 注意力分数scores=torch.matmul(Q,K.transpose(-2,-1))# [B, L, L]# ② 缩放scores=scores/math.sqrt(self.d_k)# ③ Softmax → 权重attention_weights=torch.softmax(scores,dim=-1)# [B, L, L]# ④ 加权求和 Voutput=torch.matmul(attention_weights,V)# [B, L, 768]returnoutput,attention_weights# 模拟 "我喜欢人工智能" (9个token)x=torch.randn(1,9,768)attn=SingleHeadAttention()output,weights=attn(x)print(f"输出形状:{output.shape}")# [1, 9, 768]print(f"权重形状:{weights.shape}")# [1, 9, 9]print(f"权重每行之和:{weights[0,2].sum()}")# ≈ 1.0

输出:

输出形状: torch.Size([1, 9, 768]) 权重形状: torch.Size([1, 9, 9]) 权重每行之和: tensor(1.0000)

关键结果:上下文感知

对比一下加权求和前后的变化:

"喜" 的原始向量(Embedding + 位置编码后): [0.862, 0.525, 0.043, ...] ← 只有"喜"自己的信息 "喜" 经过自注意力后的新向量: [0.215, 0.142, -0.018, ...] ← 融合了"我"(16%)和"欢"(23%)的信息

这个新向量已经不再是孤立的"喜"——它包含了"我"(主语)和"欢"(搭配词)的语义信息。这就是"上下文感知"的含义:同一个字,在不同句子里,经过自注意力后会得到不同的向量。


参考资源

[1] Vaswani et al., “Attention Is All You Need”, 2017. NeurIPS 2017 — 缩放点积注意力的原始论文
[2] Jay Alammar, “The Illustrated Transformer”, 2018. Blog — Self-Attention 计算流程的可视化讲解
[3] Stack Overflow, “Why do we scale by sqrt(d_k) in attention?”, 2020. Link — 关于缩放因子的讨论


本篇小结

本篇完成了自注意力的后半程:缩放 → Softmax → 加权求和

步骤做了什么为什么
缩放(÷√d_k)把分数除以 √64=8防止方差膨胀导致梯度消失
Softmax把分数变成概率(和为1)形成竞争性权重分布
加权求和(×V)按权重融合所有字的信息输出包含上下文的新向量
完整公式:Attention(Q, K, V) = softmax(QKᵀ/√d_k) × V 输入 X → Q,K,V → Q×Kᵀ → ÷√d_k → softmax → ×V → 输出 (上一篇) (本篇三步) (上下文感知的向量)

下一篇:我们深入解读注意力矩阵的语言模式,解释为什么 Q、K、V 必须是三个独立矩阵,并用完整代码实现自注意力,亲眼见证"上下文感知"如何诞生。


思考题

题1:为什么 Softmax 前要除以 √d_k?

题2:Softmax 的两个关键操作分别是什么?

题3:加权求和后,"喜"的新向量里包含了谁的信息?

答案见文末 → 参考答案


参考答案

题1:为什么 Softmax 前要除以 √d_k?

因为点积的方差正比于维度 d_k。不缩放的话,分数范围随维度增大而膨胀,Softmax 输出会退化为接近 one-hot 的分布,导致梯度消失,模型无法学习。

题2:Softmax 的两个关键操作分别是什么?

exp(指数函数)放大分数差距,让好的更好、差的更差;归一化(除以总和)保证所有权重为正且和为 1,形成概率分布。

题3:加权求和后,"喜"的新向量里包含了谁的信息?

主要包含"我"(16%)和"欢"(23%)的信息——因为"喜欢"是一个词,"我"是主语。"人、工、智、能"各只占约3%,因为和"喜"语义关系较远。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询