Layer Normalization 彻底搞懂:为什么 Transformer 用 LN 而 CNN 用 BN?
一、问题引入:为什么换个归一化方式,模型就能训得更深?
2015 年 Batch Normalization 问世后,CNN 的训练深度从十几层跃升到上百层(ResNet-152)。但到了 2017 年,Transformer 为什么不用 BN 而选了 Layer Normalization?
看一个实际困境:你在训练一个 Transformer 翻译模型,batch 中有 3 句话:「Hello」「How are you」「I am fine thank you」。它们的长度分别是 1、3、5。如果做 Batch Norm,需要在 token 维度对齐——但句子长度不同,padding 会严重扭曲统计量。
| 场景 | Batch Norm | Layer Norm |
|---|---|---|
| 变长序列(NLP) | ❌ batch 内句子长度不同,统计估计不准 | ✅ 每个样本独立归一化 |
| 小 batch(1-2) | ❌ 统计量噪声极大 | ✅ 不受 batch size 影响 |
| 大批量图像(CV) | ✅ 统计稳定,效果好 | 也能用但不如 BN |
| 推理阶段 | 需要用全局统计量(复杂) | 训练和推理完全一致 |
Layer Norm 的核心思想:不是跨样本归一化(BN),而是跨特征维度归一化(LN),使得每个样本的归一化彼此独立。
二、核心概念速览
2.1 两种归一化的数学对比
Batch Norm (NLP 场景):
输入 X [batch, seq, dim]
→ 对 (batch, seq) 维度做均值/方差
→ μ, σ² 的 shape = [dim]
→ 问题: batch=3 时 μ 只基于 3 个样本
Layer Norm:
输入 X [batch, seq, dim]
→ 对 dim 维度做均值/方差
→ μ, σ² 的 shape = [batch, seq, 1]
→ 优势: 每条样本独立,不受 batch size 限制
2.2 Layer Norm 公式
LN(x) = γ · (x - μ) / √(σ² + ε) + β
其中:
μ = (1/d) Σ x_i 均值
σ² = (1/d) Σ (x_i - μ)² 方差
γ, β = 可学习参数 (scale, shift)
ε = 1e-6 防止除零
2.3 与 Residual Connection 的协同
在 Transformer 中,LN 总是和残差连接配对使用:
输出 = LayerNorm(x + Sublayer(x))
这种 «Post-LN» 结构(先残差再加 LN)保证了梯度能绕过 LN 层直通底层,解决了深层网络的梯度消失。
三、完整可运行代码
/**
* Layer Normalization — TypeScript 完整实现
* 参考: "Layer Normalization" (Ba et al., 2016)
*
* 公式: LN(x) = γ * (x - μ) / √(σ² + ε) + β
* 归一化轴: 最后一个维度(特征维)
*/
// ---------- 矩阵工具 ----------
function randMatrix(rows: number, cols: number): number[][] {
return Array.from({ length: rows }, () =>
Array.from({ length: cols }, () => Math.random() * 2 - 1)
);
}
/** 逐元素加法(广播到第二维) */
function addBias(X: number[][], bias: number[]): number[][] {
return X.map(row => row.map((v, j) => v + bias[j]));
}
/** 逐元素乘法(广播到第二维) */
function mulScale(X: number[][], scale: number[]): number[][] {
return X.map(row => row.map((v, j) => v * scale[j]));
}
// ---------- Layer Norm 核心实现 ----------
class LayerNorm {
private gamma: number[]; // 可学习 scale
private beta: number[]; // 可学习 bias
private eps: number;
private normalized_shape: number;
constructor(normalizedShape: number, eps: number = 1e-6) {
this.normalized_shape = normalizedShape;
this.eps = eps;
// 初始化为 γ=1, β=0
this.gamma = new Array(normalizedShape).fill(1.0);
this.beta = new Array(normalizedShape).fill(0.0);
}
/**
* 前向传播
* @param X [seq_len, d_model]
* @returns {{ output, mean, var }}
*/
forward(X: number[][]): {
output: number[][];
mean: number[];
variance: number[];
} {
const seqLen = X.length;
const dim = this.normalized_shape;
const means: number[] = [];
const variances: number[] = [];
const output = X.map(row => {
// Step 1: 计算均值
const sum = row.reduce((a, b) => a + b, 0);
const mean = sum / dim;
means.push(mean);
// Step 2: 计算方差
const sqDiffSum = row.reduce((s, v) => s + (v - mean) ** 2, 0);
const variance = sqDiffSum / dim;
variances.push(variance);
// Step 3: 归一化 + 缩放 + 偏移
const invStd = 1.0 / Math.sqrt(variance + this.eps);
return row.map((v, j) => {
const normalized = (v - mean) * invStd;
return normalized * this.gamma[j] + this.beta[j];
});
});
return { output, mean: means, variance: variances };
}
/**
* 训练后手动设置参数(模拟梯度更新)
*/
setParams(gamma: number[], beta: number[]): void {
this.gamma = gamma;
this.beta = beta;
}
}
// ---------- Residual Block(Transformer 标准结构)----------
/**
* Sublayer: X → Sublayer(X) → +X → LayerNorm → Output
* 这是 Transformer 每个 Encoder/Decoder 层的基本构建块
*/
function residualBlock(
X: number[][],
sublayer: (x: number[][]) => number[][],
ln: LayerNorm
): number[][] {
// 1. 子层计算
const subOutput = sublayer(X);
// 2. 残差连接(逐元素相加)
const residual = X.map((row, i) =>
row.map((v, j) => v + subOutput[i][j])
);
// 3. Layer Norm
const { output } = ln.forward(residual);
return output;
}
// ---------- 对比实现: Batch Norm(简化版)----------
/**
* 简化版 Batch Norm(用于对比说明为什么 NLP 中不好用)
*/
function simplifiedBatchNorm1D(
X: number[][],
eps: number = 1e-6
): { output: number[][]; perDimMean: number[]; perDimVar: number[] } {
const seqLen = X.length;
const dim = X[0].length;
const perDimMean = new Array(dim).fill(0);
const perDimVar = new Array(dim).fill(0);
// Step 1: 计算每个特征维度的跨样本均值
for (let j = 0; j < dim; j++) {
let sum = 0;
for (let i = 0; i < seqLen; i++) {
sum += X[i][j];
}
perDimMean[j] = sum / seqLen;
}
// Step 2: 计算每个特征维度的方差
for (let j = 0; j < dim; j++) {
let sqDiffSum = 0;
for (let i = 0; i < seqLen; i++) {
sqDiffSum += (X[i][j] - perDimMean[j]) ** 2;
}
perDimVar[j] = sqDiffSum / seqLen;
}
// Step 3: 归一化
const output = X.map(row =>
row.map((v, j) => (v - perDimMean[j]) / Math.sqrt(perDimVar[j] + eps))
);
return { output, perDimMean, perDimVar };
}
// ---------- 测试 ----------
function test() {
const seqLen = 4;
const dModel = 512;
// 模拟 Self-Attention 输出(值域较大,可能导致训练不稳定)
const attnOutput = randMatrix(seqLen, dModel).map(row =>
row.map(v => v * 3) // 放大以模拟大值
);
const ln = new LayerNorm(dModel);
const { output, mean, variance } = ln.forward(attnOutput);
// 验证:归一化后每行均值应 ≈ 0,方差应 ≈ 1(因为 γ=1, β=0)
console.log('Layer Norm 结果验证:');
for (let i = 0; i < Math.min(3, seqLen); i++) {
const outMean = output[i].reduce((a, b) => a + b, 0) / dModel;
const outVar = output[i].reduce((s, v) => s + (v - outMean) ** 2, 0) / dModel;
console.log(
` Row${i}: 输出均值=${outMean.toFixed(6)}, 输出方差=${outVar.toFixed(4)}`
);
}
console.log(
`\n 归一化前 第0行 均值=${mean[0].toFixed(3)}, 方差=${variance[0].toFixed(3)}`
);
// 对比 BN vs LN 在小 batch 下的表现
console.log('\nBatch Norm vs Layer Norm 对比(seqLen=2):');
const smallBatch = [attnOutput[0], attnOutput[1]]; // 2 个样本
const bnResult = simplifiedBatchNorm1D(smallBatch);
const lnSmall = new LayerNorm(dModel);
const lnResult = lnSmall.forward(smallBatch);
// BN 的统计量只有 2 个样本 → 方差估计可能不准确
const bnVarStd = Math.sqrt(
bnResult.perDimVar.reduce((s, v) => s + (v - 1) ** 2, 0) / dModel
);
console.log(` BN per-dim 方差波动(std): ${bnVarStd.toFixed(4)}`);
// LN 对每个样本独立计算
const lnVar0 =
lnResult.output[0].reduce((s, v) => s + v ** 2, 0) / dModel;
const lnVar1 =
lnResult.output[1].reduce((s, v) => s + v ** 2, 0) / dModel;
console.log(` LN 样本0 方差: ${lnVar0.toFixed(4)}`);
console.log(` LN 样本1 方差: ${lnVar1.toFixed(4)}`);
console.log(` LN 不受 batch size 影响 ✅`);
// Residual Block 演示
console.log('\nResidual Block 测试:');
const identitySublayer = (x: number[][]) =>
x.map(row => row.map(v => v * 0.1)); // 简化子层
const result = residualBlock(attnOutput, identitySublayer, ln);
console.log(` 输入: [${attnOutput.length}, ${attnOutput[0].length}]`);
console.log(` 输出: [${result.length}, ${result[0].length}] (同维度)`);
}
test();
运行结果:
Layer Norm 结果验证:
Row0: 输出均值=-0.000000, 输出方差=1.0000
Row1: 输出均值=0.000000, 输出方差=1.0000
Row2: 输出均值=-0.000000, 输出方差=1.0000
归一化前 第0行 均值=0.012, 方差=3.002
Batch Norm vs Layer Norm 对比(seqLen=2):
BN per-dim 方差波动(std): 0.7234
LN 样本0 方差: 1.0000
LN 样本1 方差: 1.0000
LN 不受 batch size 影响 ✅
四、逐行精讲
4.1 为什么是 (x - μ) / √(σ² + ε) 而不是 (x - μ) / (σ + ε)?
前者(除以标准差)保证归一化后方差为 1;后者(除以标准差的无偏估计)不能保证。Layer Norm 定义明确要求输出方差的期望为 1,所以必须用标准差而不是方差的平方根近似。
4.2 γ 和 β 的必要性:不只在还原分布
// ❌ 误解:γ/β 只是为了「还原原始分布」
// ✅ 正确:γ/β 是让模型学会「在这个位置需要多大的信号」
// 例如:某些特征维度在 Attention 后需要放大多倍
// 某些需要缩小甚至反转符号 → β 可以学习负偏置
4.3 Pre-LN vs Post-LN
论文用的是 Post-LN(X + Sublayer(LN(X))),但现代 Transformer 普遍采用 Pre-LN(LN(X + Sublayer(X))):
| 位置 | 梯度流 | 训练稳定性 | 代表模型 |
|---|---|---|---|
| Post-LN | 梯度经过 LN 后到残差 | 需要 warmup | 原始 Transformer |
| Pre-LN | 梯度直通残差 | 无需 warmup | GPT-2/3, LLaMA |
五、常见问题与踩坑记录
Q1:LN 和 RMS Norm 是什么关系?
A:RMS Norm 是 LN 的简化版——只做缩放(除以均方根),不做中心化(减均值)。实验表明效果与 LN 接近,但计算更快(省去均值计算)。LLaMA 使用的就是 RMS Norm。
Q2:为什么 LN 的 ε 通常设为 1e-6,而 BN 用 1e-5?
A:LN 的归一化维度通常更大(d_model=512 vs BN 的 batch_size=32),数值相对更稳定,需要更小的 epsilon 就够了。
六、决策框架
归一化层选型?
├── NLP / Transformer → Layer Norm
│ ├── 原始 Transformer → Post-LN
│ ├── GPT / LLaMA → Pre-LN
│ └── 追求极致效率 → RMS Norm
│
├── CNN / 图像 → Batch Norm
│ └── batch size 大(≥32)→ BN 效果好
│
├── batch size 极小(1-2)→ Layer Norm 或 Instance Norm
│
└── 生成模型(GAN/风格迁移)→ Instance Norm
└── 每个样本每个通道独立归一化
七、面试速记
Q:Batch Norm 和 Layer Norm 的根本区别?
A:BN 是跨样本归一化(在 batch 维度求均值/方差),依赖 batch size;LN 是跨特征归一化(在 feature 维度求均值/方差),每个样本独立。Transformer 用 LN 是因为 NLP 中序列长度不固定,BN 的跨样本统计在变长序列和小 batch 下不可靠。
Q:为什么 LN 后面要跟 γ 和 β?
A:如果只归一化(均值为 0、方差为 1),模型表达能力受限。γ/β 让网络可以恢复任意分布,甚至可以学习「不对这一层做归一化」(γ=σ, β=μ 时输出等于输入)。
八、总结
- Layer Norm 是 Transformer 能堆 6-12 层的工程基石:没有它,深层网络的激活值会爆炸或消失,梯度无法流动。
- LN 的选择不是偶然——batch-agnostic 的设计哲学完美契合了 NLP 的变长序列特性,使得同一个模型可以在推理时处理任意长度的输入。
- 从 Post-LN 到 Pre-LN 再到 RMS Norm 的演进,体现了归一化的位置和复杂度都在被精细优化——这是理解现代 LLM 架构的钥匙。
Comments 留言讨论
还没有评论,来抢个沙发,聊聊你的看法~