目标、边界与计算图
本章回答“损失怎样告诉每个参数该往哪里动”。前提是第五章的 pre-LN 块、矩阵乘法、一元导数和偏导含义;本章把多元链式法则展开到 head 与 LayerNorm,并用有限差分独立核验。结束时你应能区分损失的精确梯度、检查梯度的数值近似,以及优化器选择的更新规则。后两者不是同一个对象,也不能把损失下降一次当作全局收敛证明。
计算图是有向无环图:节点为中间实数或张量,边表示下游计算使用了上游值。参数是需要求导的输入,词元编号是离散数据,不对编号本身求导。标量损失记为 ,节点 的上游梯度记为 ,它描述改变该节点对最终损失的一阶影响。一个节点若被两条路径使用,两条链式贡献必须相加,而不是保留最后到达的一条。
例如 ,其中 为参数、 为给定输入。先有 ,再有 ;输入梯度沿直接加法支路给出 ,沿乘法支路给出 ,总计 ;参数梯度为 。这正是残差结构的反传。自动微分保存必要中间值并倒序执行这种局部规则,不是重新拟合一个“梯度模型”。
从 softmax 交叉熵推到输出矩阵
取一个有效预测位置。隐藏行向量为 ,输出权重为 ,其中 是词表大小;logits 行向量 。真实下一个词元编号为 。定义 、概率 、负对数似然 ,本章对数均为自然对数,损失单位为 nat。
这里 在编号相等时为一,否则为零。若从概率导数出发,商法则给 ;再将 乘到 的一行,也得到同一结果。这是第二条独立推导。求和检查为 ,对应全部 logits 加同一常数不改变概率与损失。
数值实现不能直接对大 logits 求指数。令 ,将损失等价写成 。这是代数恒等变换,指数都不超过一,减少上溢风险;不是把概率近似掉。手推梯度仍可从原恒等式计算,不需要把“最大值位置”当作真实模型的额外离散决策。有限浮点仍有舍入,极端差值也可能下溢,因此代码对照使用温和输入。
对多个有效位置,先逐位置产生同样形状的权重梯度,再求和或按有效词元数平均;第八章将严格处理 padding。这里先用一个位置,因此不额外除批量或词表大小。若框架 head.weight 存储形状为 ,它的梯度是上式的转置。形状对上只代表乘法合法,目标是否右移、平均分母是否正确还需要另查。
算例一:两类 head 的完整梯度与一次下降
取 , 的两行分别为 与 ,目标为零号。logits 是 ;减去最大值后指数为 ,归一化概率约 。损失为 ,所以 logits 梯度为 。
第一行权重梯度为隐藏第一坐标一乘该梯度,第二行为二乘,得到两行 、。隐藏梯度第一项为负零点五七四四乘零点二加正零点五七四四乘负零点一,约负零点一七二三三;第二项为正零点五七四四乘零点三,约正零点一七二三三。若误把目标写成一号,两类梯度会换成另一组,程序仍能下降,却在训练错误任务。
用学习率 的一次普通梯度下降,按 更新。新权重两行约为 和 ;新 logits 约为 ,目标类从落后变成领先,损失约零点五六五三。这个下降是该数值输入的观察,不是任意学习率都有效的定理;过大学习率仍可能破坏更复杂的网络。
import math
h=[1.0,2.0]; W=[[0.2,-0.1],[0.0,0.3]]; target=0
def loss(w):
s=[sum(h[i]*w[i][j] for i in range(2)) for j in range(2)]
top=max(s)
return -s[target]+top+math.log(sum(math.exp(v-top) for v in s))
s=[sum(h[i]*W[i][j] for i in range(2)) for j in range(2)]
e=[math.exp(v-max(s)) for v in s]; probs=[v/sum(e) for v in e]
gs=[p-(j==target) for j,p in enumerate(probs)]
gw=[[x*g for g in gs] for x in h]
step=1e-5; errors=[]
for i in range(2):
for j in range(2):
plus=[r[:] for r in W]; minus=[r[:] for r in W]
plus[i][j]+=step; minus[i][j]-=step
numerical=(loss(plus)-loss(minus))/(2*step)
errors.append(abs(numerical-gw[i][j]))
new=[[w-0.1*g for w,g in zip(row,grad)] for row,grad in zip(W,gw)]
assert max(errors)<1e-8
assert abs(sum(gs))<1e-12
assert loss(new)<loss(W)
print('loss',loss(W),'after',loss(new),'max gradient error',max(errors))LayerNorm 的反传为何不能冻结均值和方差
沿第五章记号,输入长度为 ,均值 ,中心化坐标 ,人口方差 ,,,输出 。稳定常数保持 。现在给定上游梯度 ,定义 ,则 ;仿射参数梯度分别为 与 。
为展开输入梯度,令 为任意微小扰动。均值变化为 ,故 。对平方求导得 。由于中心化坐标和为零,乘 的一项消去,剩下 。这些变化都是输入依赖,不可在反传时设为零。
损失一阶变化为 。代入上式后,第一项给每个 系数 ;均值项给所有坐标共同系数 ;方差项给第 坐标系数 。横线表示沿特征轴的算术平均,不表示跨批量平均。于是读出完整输入梯度:
对所有坐标求和,第一部分为零,第二部分含 ,所以输入梯度和为零。这是平移不变性的反向检查。另一个常被误用的检查是“梯度与标准化向量正交”:稳定常数非零时,归一化并非精确尺度不变,因此该内积一般不是严格零。不能为通过一个只适用于 eps=0 的检查而删掉稳定常数。多位置共享的 gamma、beta 梯度需沿批量和时间求和,不能只留下最后一个位置。
算例二:漏掉统计路径的失败梯度
取 ,默认 gamma 一、beta 零,损失仅取归一化输出第一坐标,即 。上游 ;均值二,,,,,。依公式,梯度依次是 、、,约 。三项之和为零。
失败实现把均值和方差当作常量,只对分子中的当前坐标求导,会给 ,约 。它不仅与中心差分相差很大,梯度和也非零;沿全部特征共同平移的方向,它错误预测损失变化。这样的双重证据比只检查某一坐标正负可靠:一种检查来自精确不变量,另一种来自独立数值扰动。
import math
x=[1.0,2.0,3.0]; eps=1e-5
mu=sum(x)/3; c=[v-mu for v in x]
r=math.sqrt(sum(v*v for v in c)/3+eps)
u=[1.0,0.0,0.0]; mean_u=sum(u)/3
mean_uc=sum(a*b for a,b in zip(u,c))/3
g=[(a-mean_u)/r-b*mean_uc/r**3 for a,b in zip(u,c)]
def loss(values):
avg=sum(values)/len(values)
variance=sum((v-avg)**2 for v in values)/len(values)
return (values[0]-avg)/math.sqrt(variance+eps)
for delta in [1e-3,1e-5,1e-7]:
numerical=[]
for i in range(3):
plus=x[:]; minus=x[:]; plus[i]+=delta; minus[i]-=delta
numerical.append((loss(plus)-loss(minus))/(2*delta))
error=max(abs(a-b) for a,b in zip(g,numerical))
print(delta,error)
assert error<1e-5
assert abs(sum(g))<1e-12
print('exact formula',g,'wrong frozen-statistics',[1/r,0,0])从局部梯度走回完整块
前馈线性层按刚才 head 的同一规则反传:权重梯度是输入与输出上游梯度的外积,输入梯度乘权重转置;共享偏置梯度沿所有位置求和。精确 GELU 的导数由乘积法则得到 ,其中 是正态密度,且 由积分上限求导得到。先过第二线性层,再乘这个逐元素导数,再过第一层、归一化,最后将分支输入梯度加到残差主路。
中心差分用 近似导数。若损失在邻域有三阶连续导数,泰勒展开的偶次项相消,截断误差为 ;浮点相减又会产生约随 放大的舍入影响,所以步长不是越小越好。检查前关闭 dropout、使用双精度、固定同一输入,并避免把优化器更新夹在正负扰动之间。本章的检查验证给定点,不替代全域解析推导。
补齐注意力分支的反向接线
为使链式法则走完一个块,再固定单头形状:序列长度为 、头宽为 ,查询、键和值矩阵 均为 ,分数 为 。因果掩码规定哪些位置合法,合法位置按行 softmax 得概率矩阵 ,头输出为 。这里大写 指值矩阵,不是本节前面词表大小;以下公式局部使用这个约定,输出词表仍由 head 负责。
给定头输出上游梯度 ,先展开 。对一个值元素求导并汇总所有查询行,得到 ;对一个概率元素求导并汇总输出特征,得到 。前者形状是头宽矩阵,后者为分数方阵。接下来每一行都应用已经推导的 softmax 雅可比,不可把交叉熵的简化公式直接用于任意上游梯度。
第一式来自逐项累加 ,把指示项留下,再把公共概率因子提出。后两式分别对分数里的查询和键求导,缩放因子保持不变。回到投影时,查询路径给输入梯度 ,键和值路径各给同类项,三者共同汇入归一化输出。拼头只是坐标重排,反传做逆重排;输出投影仍用线性层外积规则。因果掩码是固定约束而非可训练权重,反传时不得给禁止位置重新分配概率。检查顺序应先验证单行 softmax,再验证矩阵乘法,最后验证完整分支;否则一个总梯度错误无法告诉你究竟是哪一层先偏离。
检查每行分数梯度和为零:把第一式对列求和,第二项包含概率和一,正好抵消第一项。若第一位置只有它自己合法,概率恒为一,该行查询与键的梯度可能恰为零;这不是断图。参数有可达的梯度路径、某一批次梯度不为零、更新后参数改变,是三个不同断言。应在包含多个合法过去位置的输入上检查前两者,并用有限差分判断疑似错误,而不是要求所有元素每次都非零。
AdamW 是更新规则,不是链式法则的结论
把模型所有待更新实数排成参数向量 ,在第 次更新前计算当前梯度 。AdamW 维护逐坐标一阶滑动平均 和平方梯度平均 ;初值都为零。系数 处于零到一之间,学习率 ,衰减系数 ,优化器稳定常数 ,它与归一化的 eps 是两个不同超参数。
为什么要做偏置校正?若梯度在各步期望恒为同一个值,展开一阶递推得到系数和 ,所以未校正期望被零初值缩小;除回此系数去掉该模型下的初始化偏置。二阶递推对平方梯度有同样计算。这个推导解释校正因子,不证明实际非平稳训练的梯度分布恒定,更不证明优化必然收敛。
取单个参数初值二、梯度零点一、学习率零点零一、衰减零点零一、两个 beta 为零点九和零点九九九,优化 eps 为 。第一步矩为零点零一与零点零零零零一,校正后为零点一与零点零一;自适应步约零点零一,衰减步为零点零零零二,结果约一点九八九八。若把 先加进梯度,矩和分母也随之改变,那是另一种更新,不能称为本式的等价改写。
全局梯度裁剪先计算 ,阈值 ,再取 ,零范数时比例约定为一。梯度三、四的范数五,阈值一便变为零点六、零点八。裁剪是选用的优化启发,不是原损失解析梯度变了,也不一般等于对一个固定新目标求导。采用 AdamW 时先裁剪再更新矩;它会影响历史状态,不能在保存检查点时只记权重。
import math
parameter,grad,lr,decay=2.0,0.1,0.01,0.01
b1,b2=0.9,0.999
m=(1-b1)*grad; v=(1-b2)*grad*grad
mh=m/(1-b1); vh=v/(1-b2)
new=(1-lr*decay)*parameter-lr*mh/(math.sqrt(vh)+1e-8)
assert abs(new-1.989800001)<1e-10
raw=[3.0,4.0]; norm=math.sqrt(sum(x*x for x in raw))
clipped=[x*min(1.0,1.0/norm) for x in raw]
assert abs(sum(x*x for x in clipped)-1.0)<1e-12
print('AdamW first step',new,'clipped gradient',clipped)项目增量与后继依赖
项目在 train_step 中先清旧梯度、执行前向取得 loss、反向、检查有限性、全局裁剪,再调用优化器更新;若先更新后反向,更新用的就是空梯度或上一批梯度。自动微分可承担全部层的反传,但必须通过本章的 head 外积和 LayerNorm 小例复核关键路径。第七章检查完整模型每类参数是否参与计算,第八章再保存权重、矩、步数及采样状态;本章导数正确也不能替代这些系统检查。
TRY IT YOURSELF
练习一|基础:平移 logits 的检查
三类预测概率为 0.2、0.3、0.5,目标编号一。写出 logits 梯度及其和;给全部 logits 加一百是否改变损失?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习二|推导:归一化的共享参数梯度
同一套两特征 LayerNorm 在两个位置使用。标准化输出分别为 (−1,1) 与 (0.5,−0.5),输出上游梯度分别为 (2,3) 与 (4,−2)。求 gamma 和 beta 梯度。
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三|诊断:同样小的第一步为何不能证明裁剪无用
为便于精确算数,设标量 AdamW 第一步 beta1=beta2=0、优化 eps=0、参数一、学习率 0.1、衰减 0.1。正梯度二,裁剪到零点五前后更新结果是多少?说明这个特殊情况不能推出什么。