读完这篇,你能做什么
验证 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、常量边界、初始化方差和残差反向梯度。
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项检查通过;只验证张量机制与初始化量级,不提供深层模型训练成绩。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["输入 x"] --> B["LayerNorm"]
B --> C["变换分支 F"]
A --> D["残差相加"]
C --> D
D --> E["输出与梯度"]按顺序看,每一步做了什么
- 标明输入和权重维度,再计算
fan_in。 - 手算一行均值、总体方差和epsilon位置。
- 比较常量输入与普通输入的归一化结果。
- 令残差分支为零,检查输入梯度是否保留。
03对照实现代码
PyTorch v2.9.1 · LayerNorm.__init__;LayerNorm.forward;LayerNorm.reset_parameters
原创单行LayerNorm教学实现,不是框架源码节选;仅支持一维特征,不包含融合内核或低精度累积策略。
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算法另外执行形状与有限数值验证。
代码先调用谁,再调用谁
LayerNorm构造归一化形状- 初始化仿射参数
- forward调用
F.layer_norm - 后端计算统计量与变换
- 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为关于当前均值的平方差和,浮点结果受舍入误差影响。
两次线性扫描分别计算统计和输出,不保存中间平方差数组。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
为形状 [8,4] 的线性层推导
fan_in、fan_out,并对两种初始化策略计算你采用的分布参数;写明激活函数假设。- 提交内容
- 初始化配置及推导
- 怎样算完成
- 权重布局一致
- 假设与分布参数配套
练到的知识点fan_in与fan_out · Xavier与Kaiming适用假设
- 作业 02原理问答
对形状 [2,3,4] 的输入,比较
normalized_shape=4 与 [3,4] 的统计范围、仿射参数形状和各样本间的依赖。- 提交内容
- 轴图与参数表
- 怎样算完成
- 归一化轴完整
- 总体方差与样本方差区分
练到的知识点LayerNorm归一化轴 · 总体方差与样本方差 · epsilon的作用 · gamma和beta
- 作业 03原理问答
对每个样本分别增加常数或乘正比例系数,分析
LayerNorm的输出在何种 gamma、beta、epsilon 条件下保持哪些关系。- 提交内容
- 条件化等式与轴说明
- 怎样算完成
- 样本和归一化轴明确
- epsilon 不被无条件忽略
练到的知识点LayerNorm归一化轴
- 作业 04代码推演
令二维线性残差分支
F(x)=Ax,自拟一个含负系数的 A,比较两层残差的总映射与单层,并分析重复堆叠时的信息保留条件。- 提交内容
- 矩阵推导与输入测试
- 怎样算完成
- 组合顺序完整
- “存在直连”与可逆性不混用
练到的知识点残差雅可比I+J
综合应用
- 作业 05编程实践
实现 CPU 双精度
LayerNorm,测试常量行、极小方差和 gamma/beta非默认三类输入;与独立参考实现核对输出及梯度。- 提交内容
- 实现与数值检验
- 怎样算完成
- epsilon 约定明确
- 前向反向分别检查
练到的知识点LayerNorm归一化轴 · epsilon的作用 · gamma和beta
- 作业 06排错实验
创建十层小网络,固定输入比较有无残差及两种初始化的激活、梯度范数;记录至少五个种子。
- 提交内容
- CPU 实验与分层曲线
- 怎样算完成
- 各层统计口径一致
- 未把单次现象写成定律
练到的知识点fan_in与fan_out · Xavier与Kaiming适用假设 · 残差雅可比I+J
- 作业 07面试场景
同参数量的
PreNorm和PostNorm网络训练稳定性不同。设计控制实验,明确比较的损失、梯度与学习率条件。- 提交内容
- 对照方案与失败记录格式
- 怎样算完成
- 除结构外变量受控
- 残差形状验收完整
练到的知识点残差雅可比I+J · PreNorm与PostNorm
- 作业 08编程实践
在 CPU 双精度上对小残差块 y=x+F(
LN(x)) 计算输入方向变化与梯度,分别改动归一化轴和残差系数,记录两个因素的影响。- 提交内容
- 小块实现与方向检验
- 怎样算完成
- 变量一次改变一项
- 前向差异与雅可比差异分开
练到的知识点LayerNorm归一化轴 · 残差雅可比I+J
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
评审“残差连接保证任意深度都能稳定训练”的论断;给出可分析的标量残差迭代并在 CPU 上检验不同分支尺度。
- 提交内容
- 论证、程序与适用边界
- 怎样算完成
- 理论条件写明
- 有限实验不外推无限深度
练到的知识点总体方差与样本方差 · epsilon的作用 · 残差雅可比I+J · PreNorm与PostNorm
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
能否构造两个不同输入,经同一归一化后无法区分,而接入残差后仍可区分?给出满足条件的输入与分支,讨论信息通路的作用。
- 提交内容
- 成对输入与机制论证
- 怎样算完成
- 输入确实不同
- 归一化和残差条件均声明
练到的知识点LayerNorm归一化轴 · 残差雅可比I+J
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch2.9.1 LayerNorm源码 (新标签页)↗
核对统计维度、方差与仿射参数。
- 深入参考文档英文
PyTorch2.9.1初始化源码 (新标签页)↗
阅读fan计算和Kaiming增益约定。
- 深入参考文档英文
Layer Normalization原论文 (新标签页)↗
理解层归一化的原始动机与定义。