AI 解读

Layer Normalization 彻底搞懂:为什么 Transformer 用 LN 而 CNN 用 BN?

2026年7月23日◷ 8 分钟阅读
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 留言讨论

还没有评论,来抢个沙发,聊聊你的看法~

Michael.Meng

michaelnews@126.com
用 AI 记录,用文字沉淀

© 2026 Michael Meng · 保留所有权利 · Powered by FastAPI + Nuxt