4、缩放点积的意义:为什么除以√d?数值稳定性与梯度分析。
好,咱们接着聊。前面我们推导了自注意力的核心公式,就是那个 softmax(QK^T / √d)。很多同学第一次看到这个 √d 的时候,心里都会犯嘀咕:这玩意儿是干嘛的?
说实话,我刚开始学 Transformer 的时候,也觉得这个分母有点多余。心想,直接算 softmax(QK^T) 不就行了?多此一举嘛。直到后来我自己动手写了一个简易的注意力模块,训练的时候发现 loss 曲线抖得像心电图……嗯,我才意识到,这个 √d 可不是随便加的。
4.1 点积的“膨胀”问题
我们先来看一个简单的情况。假设 q 和 k 都是 d 维的向量,每个维度独立且服从标准正态分布 N(0, 1)。那么它们的点积:
score = q · k = Σ(q_i * k_i)
这个 score 的期望是 0,但方差是多少?
我们来算一下。每个 q_i * k_i 是两个独立标准正态变量的乘积,它的方差是 1。那么 d 个这样的项加起来,方差就是 d。也就是说,score ~ N(0, d)。
这意味着什么?
当 d 很大的时候,比如 d=512,点积的数值会变得非常大。你想想看,一个标准差就是 √512 ≈ 22.6。也就是说,大部分点积的绝对值会在 20~30 这个量级,甚至更大。
然后我们把这个巨大的数值扔进 softmax 里……
核心问题:softmax 对输入非常敏感。当输入值很大时,softmax 会输出一个近乎 one-hot 的分布——最大的那个值对应的概率接近 1,其他的几乎为 0。
这就麻烦了。注意力机制本来是想让模型“关注”多个位置,结果变成了“只关注一个位置”。梯度也变得非常小,因为 softmax 在极端值区域的梯度几乎为 0。模型根本学不动。
4.2 除以 √d 的数学直觉
既然点积的方差是 d,那我们把每个点积除以 √d,方差就变成了 1。这样,无论 d 取多大,点积的数值范围都稳定在 N(0, 1) 附近。
说白了,就是给点积“降降温”。
我个人的理解是:√d 起到了一个“归一化”的作用。它让 softmax 的输入保持在一个合理的范围内,不会因为维度增加而失控。
一个小技巧:如果你自己实现注意力机制,可以试试不除以 √d,看看 loss 曲线是不是会剧烈震荡。我当年就是这么踩坑的。
4.3 梯度分析:为什么稳定?
我们再来从梯度的角度看看。softmax 的梯度公式是这样的:
∂softmax(z_i) / ∂z_j = softmax(z_i) * (δ_ij - softmax(z_j))
其中 δ_ij 是克罗内克函数(当 i=j 时为 1,否则为 0)。
当 z 的值很大时,比如某个 z_k 远大于其他值,那么 softmax(z_k) ≈ 1,其他 softmax(z_i) ≈ 0。这时候梯度会怎样?
- 对于
i = k:梯度 ≈1 * (1 - 1) = 0 - 对于
i ≠ k:梯度 ≈0 * (0 - 0) = 0
看到了吗?梯度几乎全为 0!这就是所谓的“梯度消失”。模型参数几乎得不到更新,训练就卡住了。
而除以 √d 之后,z 的值被控制在合理范围内,softmax 的输出不会太极端,梯度就能顺畅地回传。
注意:这里说的“梯度消失”和深层网络中的梯度消失不太一样。这里是 softmax 的“饱和区”导致的梯度消失,而不是链式法则中连乘导致的。
4.4 代码验证:有/无缩放的效果对比
光说不练假把式。我们来写一小段代码,直观感受一下缩放的影响。
import numpy as np
def softmax(x):
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return e_x / np.sum(e_x, axis=-1, keepdims=True)
# 模拟一个 batch 的注意力分数
d = 64
batch_size = 4
seq_len = 10
# 随机生成 Q 和 K
Q = np.random.randn(batch_size, seq_len, d)
K = np.random.randn(batch_size, seq_len, d)
# 计算点积
scores = np.matmul(Q, K.transpose(0, 2, 1)) # shape: (batch, seq_len, seq_len)
# 不缩放
attn_no_scale = softmax(scores)
# 缩放
attn_with_scale = softmax(scores / np.sqrt(d))
# 看看分布
print("未缩放 softmax 的最大值:", np.max(attn_no_scale, axis=-1)[0])
print("缩放后 softmax 的最大值:", np.max(attn_with_scale, axis=-1)[0])
运行结果大概是这样:
未缩放 softmax 的最大值: [0.9999 0.9998 0.9999 0.9997 ...]
缩放后 softmax 的最大值: [0.2345 0.1987 0.3124 0.2765 ...]
看到了吗?不缩放的话,注意力几乎变成了 one-hot,模型只能关注到一个位置。缩放之后,注意力分布就平滑多了,模型可以同时关注多个位置。
4.5 为什么是 √d 而不是 d?
有人可能会问:为什么是除以 √d,而不是直接除以 d?
因为点积的方差是 d,标准差是 √d。我们要把方差归一化到 1,所以除以标准差 √d。如果除以 d,方差就变成了 1/d,反而把数值压得太小了,softmax 的输出会过于平滑,模型也难以区分不同位置的注意力权重。
嗯,这里有个平衡点。除以 √d 刚好把方差拉到 1,既不会太极端,也不会太平滑。
4.6 实际项目中的经验
我在做多模态模型的时候,遇到过一个问题:视觉特征的维度是 1024,文本特征的维度是 512。如果直接做 cross-attention,点积的方差会非常大。我当时就踩了这个坑,训练了好几天,模型就是不收敛。
后来排查发现,是 √d 中的 d 取错了。我取了视觉特征的维度 1024,但实际上应该取 Q 和 K 的投影维度。因为 Q 和 K 是经过线性投影得到的,它们的维度是投影后的维度,而不是原始特征的维度。
避坑指南:记住,√d 中的 d 是 Q 和 K 的投影维度,不是原始输入维度。很多初学者在这里搞混。
4.7 小结
总结一下,除以 √d 的意义:
- 数值稳定性:将点积的方差从
d归一化到 1,避免 softmax 进入饱和区。 - 梯度健康:防止梯度消失,让模型能够正常训练。
- 注意力分布合理:让模型能够关注多个位置,而不是只盯着一个。
说白了,这个 √d 就是自注意力机制的“稳定器”。没有它,Transformer 的训练会非常困难。我个人觉得,这是 Transformer 设计中一个非常精巧的细节,值得好好品味。
下一章,我们会深入探讨多头注意力的实现细节。到时候你会发现,每个头里的 √d 计算,用的都是每个头自己的维度 d_k,而不是总的 d_model。嗯,这个细节也很重要,我们到时候再细说。