qinghuan
← 神经网络与自动微分 / 全部笔记

单元 18 / 神经网络与自动微分

P0 · 必会主线L1 · 基础

初始化、归一化与残差:让梯度走得更远

跟踪每层信号和梯度怎样变化,再看初始化、LayerNorm 的统计轴和残差的恒等路径各自起什么作用。

本篇目录 · 按需要跳转

读完这篇,你能做什么

验证 LayerNorm 手算与 PyTorch 一致、常量输入不会产生非有限值,并用梯度实验观察残差中的恒等路径。

学习理由、准备要求与知识点

为什么学它

网络能运行,信号和梯度仍可能越来越大或越来越小。初始化尺度、归一化统计轴选错后,优化器也未必能补救。

先读这些内容

知识点清单

  • fan_in与fan_out
  • Xavier与Kaiming适用假设
  • LayerNorm归一化轴
  • 总体方差与样本方差
  • epsilon的作用
  • gamma和beta
  • 残差雅可比I+J
  • PreNorm与PostNorm

运行前先确认PyTorch 2.9.1+cpu、Python 3.14、Java 17。CPU 张量与梯度实验;不宣称小模型结果等价于深层大模型训练稳定性。

01先弄懂它是什么

网络还没开始学习时,参数的初始大小就会影响信号能否顺利传下去。设输入 x 有 d 个分量,权重 W 的形状为 [h,d],输出 y=Wx 的形状为 [h]。若输入和权重近似独立、均值接近零,每个输出的方差约为 d乘权重方差乘输入方差。这个倍率经过多层反复相乘,数值可能越来越大或越来越小。初始化要做的是按这些近似关系选好尺度,不是随手填一批很小的数。

不同初始化方法会考虑不同条件。Xavier 同时考虑输入和输出的连接数,Kaiming 则针对 ReLU 等非线性调整尺度。fan_in 表示一个输出接收多少个输入,fan_out 表示反向传播时相关的输出连接数。PyTorch 线性层的权重按 [out,in] 存储;自己写矩阵乘法时若方向不同,就要重新核对。这些方法依赖独立性和分布等假设,不能保证任意深度的网络都精确保留方差。

归一化和残差各解决一类问题。LayerNorm 先在指定的特征维度上算均值和方差,调整数值尺度,再逐项乘可学习的 gamma、加可学习的 beta;后两步就是这里说的缩放和平移。残差则保留原输入,再加上分支算出的变化,写成 y=x+F(x)。

由于 x 直接进入加法,反向传播也多了一条不必经过 F 的路径,称为恒等通路。前者整理特征尺度,后者提供直接传递的路线;二者可以配合,但不能替代正确的损失、合理的学习率和有效数据。

文中这几个词,先认一认

fan_in
算出一个输出数时,会用到多少个输入连接。初始化方法用它来决定权重应该取多大。
LayerNorm
按 normalized_shape 指定的最后几个维度,先算这一组数的均值和方差、调整尺度,再按设置做可学习的缩放与平移。比如 [B,T,D] 只指定 D 时,每个位置各算自己的 D 个特征,不把不同样本混着统计。
残差路径
把原输入直接加到分支处理结果上的路线。两边形状必须能对应,或先通过明确的投影变换对齐。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

LayerNorm 可以按一行特征来读:x=(x1,...,xd),先算均值 mu=sum(x)/d,再算方差 var=sum((x-mu)^2)/d,接着归一化为 (x-mu)/sqrt(var+eps),最后逐元素乘 gamma、加 beta。

这里方差分母用 d,不是无偏样本方差的 d-1,eps 也放在平方根内部。如果输入全相同,减均值后是零,结果仍应有限;有 beta 时输出的是 beta,不一定为零。

输入形状为 [B,T,D]、normalized_shape=D 时,每个样本的每个位置都会单独用自己的 D 个特征计算统计量。若改成 [T,D],就把位置一起纳入计算,参数形状也随位置长度变化,模型做的事已经不同。LayerNorm 在训练和评估时都使用当前输入的统计量;它不同于维护运行统计的 BatchNorm,切到 eval 也不会让它忽略当前输入。

残差的雅可比为 I+J_F,可以读成“直接传递输入的部分”加上“分支带来的变化”。分支梯度很小时,前一部分仍能传递信号;分支尺度失控时,整体仍可能爆炸。PreNorm 在进分支前归一化,PostNorm 在相加后归一化,两者的梯度路径不同。本实验只用零初始化线性分支检查恒等梯度,并用固定随机样本观察 Kaiming 方差量级,不把一次小实验当作任意网络的训练保证。

先跑通一个最小例子

CPU固定种子,比较手算LayerNorm、常量边界、初始化方差和残差反向梯度。

experiment.py · python
import torch
from torch import nn
torch.manual_seed(18)
torch.set_num_threads(1)
x = torch.tensor([[1., 2., 3.], [7., 7., 7.]], dtype=torch.float64)
layer = nn.LayerNorm(3, eps=1e-5, elementwise_affine=False, dtype=torch.float64)
manual = (x - x.mean(-1, keepdim=True)) / torch.sqrt(x.var(-1, correction=0, keepdim=True) + 1e-5)
assert torch.allclose(layer(x), manual, atol=1e-12)  # 方差分母为特征数
assert torch.equal(layer(x)[1], torch.zeros(3, dtype=torch.float64))
assert torch.isfinite(layer(x)).all()
layer.train()
train_out = layer(x)
layer.eval()
assert torch.equal(train_out, layer(x))  # LayerNorm不切换到历史批统计
weight = torch.empty(512, 128)
nn.init.kaiming_normal_(weight, mode="fan_in", nonlinearity="relu")
assert abs(weight.var(correction=0).item() - 2/128) < .001
branch = nn.Linear(3, 3, bias=False, dtype=torch.float64)
nn.init.zeros_(branch.weight)
z = torch.tensor([[1., 2., 3.]], dtype=torch.float64, requires_grad=True)
out = z + branch(layer(z))
out.sum().backward()
assert torch.equal(z.grad, torch.ones_like(z))  # 零分支仍有恒等梯度通路
assert branch.weight.grad.shape == (3, 3)
print("7 normalization initialization residual checks passed")

核对运行结果打印7项检查通过;只验证张量机制与初始化量级,不提供深层模型训练成绩。

正在加载流程图…

PreNorm示意:归一化控制分支输入尺度,残差保留输入通路。
查看流程图源码(Mermaid)
flowchart LR
A["输入 x"] --> B["LayerNorm"]
B --> C["变换分支 F"]
A --> D["残差相加"]
C --> D
D --> E["输出与梯度"]

按顺序看,每一步做了什么

  1. 标明输入和权重维度,再计算fan_in。
  2. 手算一行均值、总体方差和epsilon位置。
  3. 比较常量输入与普通输入的归一化结果。
  4. 令残差分支为零,检查输入梯度是否保留。

03对照实现代码

LayerNorm记录末尾维度并委托functional层 (新标签页)↗

PyTorch v2.9.1 · LayerNorm.__init__;LayerNorm.forward;LayerNorm.reset_parameters

原创单行LayerNorm教学实现,不是框架源码节选;仅支持一维特征,不包含融合内核或低精度累积策略。

核心机制代码 · python
import math
def layer_norm_row(values, gamma, beta, eps=1e-5):
    if not values or eps <= 0:
        raise ValueError("shape or eps")
    mean = sum(values) / len(values)  # 本行特征自己的均值
    variance = sum((v - mean)**2 for v in values) / len(values)
    scale = math.sqrt(variance + eps)  # 使用总体方差,epsilon在根号里
    return [(v - mean) / scale * g + b
            for v, g, b in zip(values, gamma, beta)]  # gamma与beta是逐特征参数

真实LayerNorm保存normalized_shape、eps及可选仿射参数,默认gamma初始化为一、beta为零。

forward委托F.layer_norm;Python模块不意味着实际大量数值计算逐元素在Python循环里进行。

源码文档明确采用有偏方差估计,即correction=0,并使用末尾指定维度。

教学行函数假定gamma与beta长度匹配;完整Java算法另外执行形状与有限数值验证。

代码先调用谁,再调用谁

  1. LayerNorm构造归一化形状
  2. 初始化仿射参数
  3. forward调用F.layer_norm
  4. 后端计算统计量与变换
  5. autograd传播梯度

04动手写一个例子

Welford统计量驱动的单行LayerNorm

给定一行有限double特征、等长gamma和beta,以及正epsilon,返回归一化结果。采用Welford在线均值和平方差累积,拒绝无法表示的统计量。

输入 1
x=[1,2,3],gamma全1,beta全0
输出 1
约[-1.2247,0,1.2247],epsilon使幅度略小
输入 2
x=[7,7],beta=[2,3]
输出 2
[2,3]

直接计算平方均值减均值平方会在大偏置小差异时发生严重抵消;Welford更新中心化平方差。

每读入x,用delta=x-mean更新mean,再累积delta*(x-newMean),最后除以d得到总体方差。

统计完成后统一归一化并应用仿射变换;边界检查避免空行、形状错配与NaN静默传播。

为什么这样写是对的处理k个元素后mean为其均值,m2为关于当前均值的平方差和,浮点结果受舍入误差影响。

时间 O(d)额外空间 O(d)输出,O(1)额外统计空间

两次线性扫描分别计算统计和输出,不保存中间平方差数组。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 39 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
  static double[] norm(double[] x,double[] gamma,double[] beta,double eps){
    if(x.length==0||gamma.length!=x.length||beta.length!=x.length||!Double.isFinite(eps)||eps<=0)
      throw new IllegalArgumentException();
    double mean=0,m2=0;int n=0;
    for(double v:x){
      if(!Double.isFinite(v))throw new IllegalArgumentException();
      n++;double delta=v-mean;mean+=delta/n;
      m2+=delta*(v-mean); // 更新后的均值用于稳定平方差累积
    }
    double variance=m2/n;
    if(!Double.isFinite(variance)||variance<0)throw new ArithmeticException("range");
    double denominator=Math.sqrt(variance+eps); // 总体方差而非除以n-1
    double[] result=new double[n];
    for(int i=0;i<n;i++){
      if(!Double.isFinite(gamma[i])||!Double.isFinite(beta[i]))throw new IllegalArgumentException();
      result[i]=(x[i]-mean)/denominator*gamma[i]+beta[i];
      if(!Double.isFinite(result[i]))throw new ArithmeticException("range"); // 拒绝不可表示输出
    }
    return result;
  }
  static void check(boolean v){if(!v)throw new AssertionError();}
  static void close(double a,double b){check(Math.abs(a-b)<1e-9);}
  public static void main(String[] args){
    double[] r=norm(new double[]{1,2,3},new double[]{1,1,1},new double[3],1e-5);
    close(r[1],0);close(r[0],-r[2]);
    check(Arrays.equals(norm(new double[]{7,7},new double[]{1,1},new double[]{2,3},1e-5),new double[]{2,3}));
    close(norm(new double[]{5},new double[]{9},new double[]{4},1e-5)[0],4);
    double[] shifted=norm(new double[]{1e12+1,1e12+2,1e12+3},new double[]{1,1,1},new double[3],1e-5);
    close(shifted[0],r[0]); // 大公共偏置不应改变这里可精确表示的差值
    boolean bad=false;
    try{norm(new double[]{Double.NaN},new double[]{1},new double[]{0},1e-5);}catch(IllegalArgumentException e){bad=true;}
    check(bad);
    bad=false;try{norm(new double[0],new double[0],new double[0],1e-5);}catch(IllegalArgumentException e){bad=true;}
    check(bad);
    System.out.println("6 LayerNorm checks passed");
  }
}

这些特殊情况也要试一试

  • 单特征归一化后的中心值为零,输出由beta决定。
  • 常量输入仍需正epsilon以避免零除。
  • double的有限表示范围不是无限精度,极端平方差可能需更高精度算法。
进阶选读 · 生产拓展:并行归约与低精度统计

场景大批量特征在设备上并行计算统计量,需要兼顾吞吐和数值误差。

做法将分块均值、样本数和平方差用可合并Welford公式归约,在较高精度中积累;与参考实现比较误差和反向梯度。

复杂度总工作O(d),理想并行归约深度O(log d),实际受内存与同步限制。

好处与代价归约顺序改变浮点舍入,位级一致通常比容差一致成本更高。

05项目里怎么用

初始化要跟实际非线性与权重方向匹配,不能看到ReLU就对全部矩阵机械套用同一个函数。输出投影、嵌入、归一化参数与残差分支可能有不同约定;复现模型时应记录这些细节。

监控激活均值、方差、梯度范数和非有限值,比只盯损失更能定位传播问题。若某层输出已溢出,调小最后一层学习率未必解决根因,应检查前向尺度与归一化轴。

残差提升可优化性但不保证深层模型一定更好。改变PreNorm/PostNorm、epsilon或初始化增益应作为独立消融,并在同数据和预算下比较。CPU张量测试适合排查公式和轴,不代表GPU混合精度下误差与吞吐相同。

容易踩的坑

  • 用d-1方差替代LayerNorm规定的d分母。
  • 把eps加在根号外却认为公式等价。
  • 误把批次轴也纳入LayerNorm。
  • 认为残差自动消除所有梯度爆炸。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
Xavier初始化平衡线性层输入输出尺度需考虑非线性适配相应增益下的线性或对称激活
Kaiming初始化针对ReLU传播尺度依赖fan和激活假设ReLU类网络
残差加归一化改善尺度和梯度路径结构与参数约定更多深层模块

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01LayerNorm为何不使用d-1?

展开参考答案与推导

它按当前特征向量计算总体方差,目标不是无偏估计未知总体。

02y=x+F(x)的雅可比是什么?

展开参考答案与推导

I+J_F,恒等项提供直接梯度路径。

03eval会让LayerNorm改用历史统计吗?

展开参考答案与推导

不会,它在训练和评估均使用当前输入统计。

换个情况,还会处理吗

01常量输入产生NaN,先看哪里?

展开参考答案与推导

检查epsilon是否为正且位于根号内,以及是否错误除零。

02[B,T,D]使用[T,D]归一化有什么变化?

展开参考答案与推导

每个样本跨位置和特征共同统计,参数形状也不同,不再逐位置归一化。

03初始化后激活方差迅速增长,如何排查?

展开参考答案与推导

核对fan定义、矩阵方向、激活增益和残差叠加尺度,逐层记录统计。

写代码试一试

01[2,2]且beta=[1,3]输出多少?

输入 / 条件
正eps,gamma任意有限值
输出 / 预期
[1,3]

提示 中心化为零。

展开参考答案与推导

归一化部分全零,仿射后只剩beta。

02[1,2,3]总体方差是多少?

输入 / 条件
d=3
输出 / 预期
2/3

提示 平方差和为2。

展开参考答案与推导

除以3而非2。

03零残差分支的out.sum对输入梯度是多少?

输入 / 条件
out=x+0
输出 / 预期
全1

提示 恒等映射导数。

展开参考答案与推导

每个输入分量直接贡献一个输出分量。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

验证 LayerNorm 手算与 PyTorch 一致、常量输入不会产生非有限值,并用梯度实验观察残差中的恒等路径。

本篇重点加练

归一化轴和残差雅可比共同改变信息与梯度流,需用变换条件检验而非只记公式。

  • LayerNorm归一化轴
  • 残差雅可比I+J

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    为形状 [8,4] 的线性层推导 fan_in、fan_out,并对两种初始化策略计算你采用的分布参数;写明激活函数假设。

    提交内容
    初始化配置及推导
    怎样算完成
    • 权重布局一致
    • 假设与分布参数配套

    练到的知识点fan_in与fan_out · Xavier与Kaiming适用假设

  2. 作业 02原理问答

    对形状 [2,3,4] 的输入,比较 normalized_shape=4 与 [3,4] 的统计范围、仿射参数形状和各样本间的依赖。

    提交内容
    轴图与参数表
    怎样算完成
    • 归一化轴完整
    • 总体方差与样本方差区分

    练到的知识点LayerNorm归一化轴 · 总体方差与样本方差 · epsilon的作用 · gamma和beta

  3. 作业 03原理问答

    对每个样本分别增加常数或乘正比例系数,分析 LayerNorm 的输出在何种 gamma、beta、epsilon 条件下保持哪些关系。

    提交内容
    条件化等式与轴说明
    怎样算完成
    • 样本和归一化轴明确
    • epsilon 不被无条件忽略

    练到的知识点LayerNorm归一化轴

  4. 作业 04代码推演

    令二维线性残差分支 F(x)=Ax,自拟一个含负系数的 A,比较两层残差的总映射与单层,并分析重复堆叠时的信息保留条件。

    提交内容
    矩阵推导与输入测试
    怎样算完成
    • 组合顺序完整
    • “存在直连”与可逆性不混用

    练到的知识点残差雅可比I+J

综合应用

  1. 作业 05编程实践

    实现 CPU 双精度 LayerNorm,测试常量行、极小方差和 gamma/beta 非默认三类输入;与独立参考实现核对输出及梯度。

    提交内容
    实现与数值检验
    怎样算完成
    • epsilon 约定明确
    • 前向反向分别检查

    练到的知识点LayerNorm归一化轴 · epsilon的作用 · gamma和beta

  2. 作业 06排错实验

    创建十层小网络,固定输入比较有无残差及两种初始化的激活、梯度范数;记录至少五个种子。

    提交内容
    CPU 实验与分层曲线
    怎样算完成
    • 各层统计口径一致
    • 未把单次现象写成定律

    练到的知识点fan_in与fan_out · Xavier与Kaiming适用假设 · 残差雅可比I+J

  3. 作业 07面试场景

    同参数量的 PreNorm 和 PostNorm 网络训练稳定性不同。设计控制实验,明确比较的损失、梯度与学习率条件。

    提交内容
    对照方案与失败记录格式
    怎样算完成
    • 除结构外变量受控
    • 残差形状验收完整

    练到的知识点残差雅可比I+J · PreNorm与PostNorm

  4. 作业 08编程实践

    在 CPU 双精度上对小残差块 y=x+F(LN(x)) 计算输入方向变化与梯度,分别改动归一化轴和残差系数,记录两个因素的影响。

    提交内容
    小块实现与方向检验
    怎样算完成
    • 变量一次改变一项
    • 前向差异与雅可比差异分开

    练到的知识点LayerNorm归一化轴 · 残差雅可比I+J

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    评审“残差连接保证任意深度都能稳定训练”的论断;给出可分析的标量残差迭代并在 CPU 上检验不同分支尺度。

    提交内容
    论证、程序与适用边界
    怎样算完成
    • 理论条件写明
    • 有限实验不外推无限深度

    练到的知识点总体方差与样本方差 · epsilon的作用 · 残差雅可比I+J · PreNorm与PostNorm

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    能否构造两个不同输入,经同一归一化后无法区分,而接入残差后仍可区分?给出满足条件的输入与分支,讨论信息通路的作用。

    提交内容
    成对输入与机制论证
    怎样算完成
    • 输入确实不同
    • 归一化和残差条件均声明

    练到的知识点LayerNorm归一化轴 · 残差雅可比I+J

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗