Layer Normalization 与 Adaptive Layer Normalization:从 Stable Diffusion 到自动驾驶

从 Layer Norm 的基础原理,到 AdaLN 在扩散模型中的条件生成应用,再到自动驾驶里的潜力场景。本文用公式、代码和实战案例,帮你彻底理解这两种归一化技术。

Layer Normalization 与 Adaptive Layer Normalization

从 Stable Diffusion 到自动驾驶,归一化技术如何让你的模型训练更稳定、生成更可控?

1. 引子:归一化是个什么问题?

深度神经网络训练有个经典问题:Internal Covariate Shift(内部协变量偏移)。

简单说就是:每层神经网络的输入分布,会随着前面层的参数更新而发生变化。这导致:

  • 训练不稳定,需要很小的学习率
  • 对初始化参数非常敏感
  • 梯度消失/爆炸

解决方案:归一化(Normalization)。

归一化的核心思想:让每一层的输入分布保持稳定(均值 0,方差 1),这样训练会更稳定、更快。


2. Batch Normalization:最初的方案及其局限

2.1 BN 的做法

# 对一个 batch 的所有样本,在每个特征维度上计算均值和方差
μ_batch = mean(x, dim=0)  # [D]
σ_batch = std(x, dim=0)   # [D]

x_norm = (x - μ_batch) / (σ_batch + ε)
x_out = γ * x_norm + β

问题:BN 的统计量(μ_batch, σ_batch)依赖于当前 batch 的所有样本。

2.2 BN 在 Transformer 里的局限性

  1. 变长序列:Transformer 处理的是变长序列(padding 到最大长度),BN 的 batch 统计量会被 padding 影响
  2. 小 batch 训练:GPU 显存限制,batch size 可能很小(比如 8 或 16),BN 的统计量不准确
  3. 位置信息:BN 会在 batch 维度上混合不同样本的信息,可能破坏序列的位置信息
  4. 并行性:Transformer 的多头注意力是并行的,BN 需要等待整个 batch 计算完统计量

结论:Transformer 不用 BN,用 Layer Normalization


3. Layer Normalization:Transformer 的标准选择

3.1 核心思想

LN 的归一化是在每个样本的特征维度上独立进行的,不依赖 batch 里的其他样本。

公式

对于输入 x ∈ R^{B×L×D}(B=batch, L=sequence_length, D=feature_dim):

# 对每个样本、每个位置,在特征维度 D 上计算均值和方差
μ = mean(x, dim=-1, keepdim=True)  # [B, L, 1]
σ = std(x, dim=-1, keepdim=True)   # [B, L, 1]

x_norm = (x - μ) / (σ + ε)
x_out = γ * x_norm + β

其中:

  • γ ∈ R^D, β ∈ R^D可学习的固定参数(对所有样本共享)
  • ε 是小常数(如 1e-5),防止除以零

3.2 为什么 LN 适合 Transformer?

  1. 不受 batch size 影响:每个样本独立归一化,batch size=1 也能用
  2. 支持变长序列:padding 不影响其他位置的归一化
  3. 保持位置信息:只在特征维度归一化,序列维度保持不变
  4. 训练稳定:Transformer 论文(Vaswani et al., 2017)实验证明 LN 比 BN 更稳定

3.3 代码:LayerNorm 从头实现

import torch
import torch.nn as nn

class LayerNorm(nn.Module):
    """Layer Normalization 从头实现"""
    def __init__(self, dim, eps=1e-5):
        super().__init__()
        self.dim = dim
        self.eps = eps
        # 可学习的缩放和偏移参数
        self.gamma = nn.Parameter(torch.ones(dim))
        self.beta = nn.Parameter(torch.zeros(dim))
        
    def forward(self, x):
        # x: [B, L, D] 或 [B, D]
        if x.dim() == 2:
            x = x.unsqueeze(1)  # [B, 1, D]
            squeezed = True
        else:
            squeezed = False
        
        # 在特征维度计算均值和方差
        mu = x.mean(dim=-1, keepdim=True)  # [B, L, 1]
        sigma = x.std(dim=-1, keepdim=True, unbiased=False)  # [B, L, 1]
        
        # 归一化
        x_norm = (x - mu) / (sigma + self.eps)
        
        # 缩放和偏移
        out = self.gamma * x_norm + self.beta  # broadcast: [B, L, D]
        
        if squeezed:
            out = out.squeeze(1)
        return out, (mu, sigma)

# 测试
if __name__ == "__main__":
    B, L, D = 2, 10, 64
    x = torch.randn(B, L, D)
    
    # 自己的实现
    ln = LayerNorm(D)
    out, (mu, sigma) = ln(x)
    print(f"输入形状: {x.shape}")
    print(f"输出形状: {out.shape}")
    print(f"均值 (应为0): {out.mean().item():.6f}")
    print(f"方差 (应为1): {out.std().item():.6f}")
    
    # 对比 PyTorch 官方实现
    ln_official = nn.LayerNorm(D)
    out_official = ln_official(x)
    print(f"\nPyTorch 官方输出均值: {out_official.mean().item():.6f}")
    print(f"PyTorch 官方输出方差: {out_official.std().item():.6f}")
    print(f"最大误差: {(out - out_official).abs().max().item():.6f}")

3.4 Transformer 里的 LN 位置

标准 Transformer 有两种布局:

Pre-Norm(现代常用,训练更稳定):

x → LN → Attention → + → LN → FFN → + → output
    ↑                      ↑
    └───── residual ───────┘  └───── residual ───────┘

Post-Norm(原始 Transformer):

x → Attention → LN → + → FFN → LN → + → output
              ↑                  ↑
              └──── residual ───┘  └──── residual ───┘

结论:Pre-Norm 训练更稳定,Post-Norm 最终性能可能更好(但需要 careful warm-up)。


4. Adaptive Layer Normalization (AdaLN):条件生成的归一化

4.1 核心思想

LN 的 γ, β固定的可学习参数(对所有样本共享)。

但有些任务需要根据条件动态调整归一化策略,比如:

  • 扩散模型:不同时间步 t 的特征分布不同
  • 文本生成图像:不同的文本提示 c 需要不同的风格
  • 多模态融合:不同模态的特征分布不同

AdaLN 的解决方案:让 γ, β 由条件 c 动态生成

4.2 公式

# 标准 LN
x_norm = (x - μ) / (σ + ε)
x_out = γ * x_norm + β

# AdaLN: γ 和 β 是条件的函数
γ(c) = MLP_γ(c)  # [D]
β(c) = MLP_β(c)  # [D]

x_out = γ(c) * x_norm + β(c)

其中 c 是条件向量(如时间步嵌入、文本嵌入等)。

4.3 为什么需要 AdaLN?

例子 1:扩散模型去噪

扩散模型的过程:

  • 时间步 t=0:几乎纯噪声
  • 时间步 t=T:接近真实图像

不同时间步的特征分布完全不同,用固定的 γ, β 显然不合理。AdaLN 让模型根据 t 动态调整。

例子 2:文本生成图像

  • 条件 c="一只猫" → 模型应该关注猫的特征
  • 条件 c="一辆车的侧面图" → 模型应该关注车辆侧面

不同的文本条件需要不同的归一化策略。

4.4 代码:AdaLN 从头实现

class AdaptiveLayerNorm(nn.Module):
    """Adaptive Layer Normalization"""
    def __init__(self, dim, cond_dim):
        super().__init__()
        self.dim = dim
        self.eps = 1e-5
        
        # 用 MLP 根据条件生成 γ 和 β
        self.mlp = nn.Sequential(
            nn.Linear(cond_dim, dim * 2),
            nn.SiLU(),  # 或用 GELU
            nn.Linear(dim * 2, dim * 2)
        )
        
    def forward(self, x, c):
        """
        x: [B, L, D] - 输入特征
        c: [B, cond_dim] - 条件向量
        """
        B, L, D = x.shape
        
        # 归一化(和标准 LN 一样)
        mu = x.mean(dim=-1, keepdim=True)  # [B, L, 1]
        sigma = x.std(dim=-1, keepdim=True, unbiased=False)  # [B, L, 1]
        x_norm = (x - mu) / (sigma + self.eps)  # [B, L, D]
        
        # 根据条件生成 γ 和 β
        gamma_beta = self.mlp(c)  # [B, 2*D]
        gamma, beta = gamma_beta.chunk(2, dim=-1)  # 每个 [B, D]
        
        # 扩展到序列长度维度
        gamma = gamma.unsqueeze(1).expand(B, L, D)  # [B, L, D]
        beta = beta.unsqueeze(1).expand(B, L, D)
        
        # 自适应归一化
        out = gamma * x_norm + beta
        return out

# 测试
if __name__ == "__main__":
    B, L, D, cond_dim = 2, 10, 64, 128
    x = torch.randn(B, L, D)
    c = torch.randn(B, cond_dim)  # 条件向量(如时间步嵌入)
    
    adln = AdaptiveLayerNorm(D, cond_dim)
    out = adln(x, c)
    print(f"输入形状: {x.shape}")
    print(f"条件形状: {c.shape}")
    print(f"输出形状: {out.shape}")
    
    # 验证:不同的条件应该产生不同的输出
    c2 = torch.randn(B, cond_dim)  # 另一个条件
    out2 = adln(x, c2)
    diff = (out - out2).abs().sum().item()
    print(f"不同条件的输出差异: {diff:.2f} (应为非零)")

5. AdaLN-Zero:DiT 的改进

5.1 DiT 是什么?

DiT (Diffusion Transformer) 是 OpenAI 提出的扩散模型架构,用 Transformer 替换了传统的 U-Net。

核心创新

  1. 用 ViT 作为扩散模型的骨干网络
  2. AdaLN-Zero 将时间步和条件信息注入模型

5.2 AdaLN-Zero 的改进

标准 AdaLN:

γ(c) = MLP(c)
β(c) = MLP(c)

问题:训练初期,γβ 是随机初始化的,可能导致模型输出不稳定。

AdaLN-Zero 的解决方案

  • 初始化时,γ=1, β=0(等价于标准 LN)
  • 让模型从”接近恒等映射”开始学习,保证训练稳定性

公式

γ(c) = 1 + MLP_γ(c)  # 初始化时 MLP_γ 输出 0
β(c) = 0 + MLP_β(c)  # 初始化时 MLP_β 输出 0

5.3 代码:AdaLN-Zero 简化实现

class AdaLNZero(nn.Module):
    """AdaLN-Zero: 带 zero initialization 的自适应层归一化"""
    def __init__(self, dim, cond_dim):
        super().__init__()
        self.dim = dim
        self.eps = 1e-5
        
        # MLP 生成 gamma 和 beta
        self.mlp = nn.Sequential(
            nn.Linear(cond_dim, dim * 4),
            nn.SiLU(),
            nn.Linear(dim * 4, dim * 2)
        )
        
        # Zero initialization: 让初始输出为 0
        self.mlp[-1].weight.data.zero_()
        self.mlp[-1].bias.data.zero_()
        
    def forward(self, x, c):
        B, L, D = x.shape
        
        # 归一化
        mu = x.mean(dim=-1, keepdim=True)
        sigma = x.std(dim=-1, keepdim=True, unbiased=False)
        x_norm = (x - mu) / (sigma + self.eps)
        
        # 生成 gamma 和 beta (初始化时为 0)
        gamma_beta = self.mlp(c)
        gamma_delta, beta_delta = gamma_beta.chunk(2, dim=-1)
        
        # AdaLN-Zero: gamma = 1 + delta, beta = 0 + delta
        gamma = 1 + gamma_delta.unsqueeze(1).expand(B, L, D)
        beta = beta_delta.unsqueeze(1).expand(B, L, D)
        
        out = gamma * x_norm + beta
        return out

# 测试:验证 zero initialization
if __name__ == "__main__":
    B, L, D, cond_dim = 2, 10, 64, 128
    x = torch.randn(B, L, D)
    c = torch.randn(B, cond_dim)
    
    adaln_zero = AdaLNZero(D, cond_dim)
    
    # 训练前,gamma 应该接近 1,beta 应该接近 0
    out = adaln_zero(x, c)
    print(f"训练前输出均值: {out.mean().item():.6f}")
    print(f"训练前输出方差: {out.std().item():.6f}")
    
    # 手动计算 gamma 和 beta
    gamma_beta = adaln_zero.mlp(c)
    gamma_delta, beta_delta = gamma_beta.chunk(2, dim=-1)
    print(f"\ngamma_delta 初始值 (应为0): {gamma_delta.abs().max().item():.6f}")
    print(f"beta_delta 初始值 (应为0): {beta_delta.abs().max().item():.6f}")

6. 实战案例:自动驾驶中的应用

虽然 AdaLN 主要来自扩散模型(AIGC),但在自动驾驶里也有很好的应用潜力。

6.1 案例 1:条件轨迹预测

问题:预测目标车辆未来轨迹,但不同车型的行为模式不同:

  • 小车:灵活,可能急转弯
  • 大车(卡车):转弯半径大,变道慢

AdaLN 方案

条件 c = 车辆类型嵌入 (小车=0, 大车=1, ...)
γ(c), β(c) = MLP(c)

在 Transformer encoder 里用 AdaLN
→ 模型根据车型动态调整特征分布

代码简化版

class ConditionalTrajectoryPrediction(nn.Module):
    def __init__(self, dim=128, cond_dim=16, heads=4):
        super().__init__()
        self.cond_embedding = nn.Embedding(10, cond_dim)  # 10 种车型
        
        # 用 AdaLN 代替标准 LN
        self.self_attn = nn.MultiheadAttention(dim, heads, batch_first=True)
        self.adln1 = AdaptiveLayerNorm(dim, cond_dim)
        self.adln2 = AdaptiveLayerNorm(dim, cond_dim)
        
        self.ffn = nn.Sequential(
            nn.Linear(dim, dim * 4),
            nn.GELU(),
            nn.Linear(dim * 4, dim)
        )
        
    def forward(self, agent_traj, vehicle_type):
        # agent_traj: [B, T, D]
        # vehicle_type: [B] - 车型标签 (0, 1, 2, ...)
        
        # 条件嵌入
        c = self.cond_embedding(vehicle_type)  # [B, cond_dim]
        
        # Transformer block with AdaLN
        # Self-attention
        attn_out, _ = self.self_attn(agent_traj, agent_traj, agent_traj)
        x = agent_traj + attn_out  # residual
        x = self.adln1(x, c)  # AdaLN
        
        # FFN
        ffn_out = self.ffn(x)
        x = x + ffn_out  # residual
        x = self.adln2(x, c)  # AdaLN
        
        return x

# 测试
if __name__ == "__main__":
    B, T, D = 4, 20, 128
    agent_traj = torch.randn(B, T, D)
    vehicle_type = torch.tensor([0, 1, 0, 2])  # 4 个样本,3 种车型
    
    model = ConditionalTrajectoryPrediction(D)
    out = model(agent_traj, vehicle_type)
    print(f"输出形状: {out.shape}")
    print("不同车型的条件嵌入会动态调整归一化策略")

6.2 案例 2:多传感器融合

问题:自动驾驶车有多个传感器(相机、激光雷达、雷达),它们的特征分布不同。

AdaLN 方案

条件 c = 传感器类型嵌入 (相机=0, 激光雷达=1, 雷达=2)
γ(c), β(c) = MLP(c)

在融合 Transformer 里用 AdaLN
→ 模型根据不同传感器动态调整特征分布

好处

  • 相机特征(RGB,连续值)和激光雷达特征(点云,稀疏)的分布不同
  • AdaLN 让模型自适应地”标准化”不同传感器的特征

6.3 案例 3:端到端规划(导航指令条件)

问题:端到端自动驾驶需要根据导航指令调整驾驶策略:

  • “前方右转” → 模型应该关注右侧车道
  • “直行” → 模型应该关注前方车道

AdaLN 方案

条件 c = 导航指令的文本嵌入 (用 BERT/RoBERTa 编码)
γ(c), β(c) = MLP(c)

在规划 Transformer 里用 AdaLN
→ 模型根据导航指令动态调整驾驶策略

这不是 CLIP 吗?

对!CLIP 用 Cross-Attention 对齐文本和图像,而这里用 AdaLN 让文本条件”调制”视觉特征。


7. 实验对比:LN vs AdaLN

7.1 简单实验:条件生成任务

我们用一个玩具实验对比 LN 和 AdaLN:

任务:根据条件 c(0 或 1),生成不同的正弦波。

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 数据集
def generate_data(n_samples=1000):
    """生成条件正弦波数据"""
    x = torch.linspace(0, 2*3.14159, 50).unsqueeze(0).expand(n_samples, -1)  # [N, 50]
    c = torch.randint(0, 2, (n_samples,))  # 条件: 0 或 1
    
    # 条件 0: 低频正弦波
    # 条件 1: 高频正弦波
    y = torch.where(
        c.unsqueeze(1) == 0,
        torch.sin(x),  # 低频
        torch.sin(3 * x)  # 高频
    )
    return x, y, c

# 模型
class ConditionalModel(nn.Module):
    def __init__(self, use_adaln=False):
        super().__init__()
        self.use_adaln = use_adaln
        
        self.fc1 = nn.Linear(50, 128)
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, 50)
        
        if use_adaln:
            self.cond_emb = nn.Embedding(2, 16)
            self.adln1 = AdaptiveLayerNorm(128, 16)
            self.adln2 = AdaptiveLayerNorm(128, 16)
        else:
            self.ln1 = nn.LayerNorm(128)
            self.ln2 = nn.LayerNorm(128)
        
    def forward(self, x, c=None):
        x = self.fc1(x)
        
        if self.use_adaln:
            c_emb = self.cond_emb(c)
            x = self.adln1(x.unsqueeze(1), c_emb).squeeze(1)
        else:
            x = self.ln1(x)
        x = torch.relu(x)
        
        x = self.fc2(x)
        if self.use_adaln:
            x = self.adln2(x.unsqueeze(1), c_emb).squeeze(1)
        else:
            x = self.ln2(x)
        x = torch.relu(x)
        
        x = self.fc3(x)
        return x

# 训练对比
def train_and_compare():
    x, y, c = generate_data(1000)
    
    # 模型 1: 标准 LN
    model_ln = ConditionalModel(use_adaln=False)
    opt_ln = torch.optim.Adam(model_ln.parameters(), lr=1e-3)
    
    # 模型 2: AdaLN
    model_adaln = ConditionalModel(use_adaln=True)
    opt_adaln = torch.optim.Adam(model_adaln.parameters(), lr=1e-3)
    
    criterion = nn.MSELoss()
    
    # 简单训练循环(10 epochs)
    for epoch in range(10):
        # 训练 LN 模型
        pred_ln = model_ln(x)
        loss_ln = criterion(pred_ln, y)
        opt_ln.zero_grad()
        loss_ln.backward()
        opt_ln.step()
        
        # 训练 AdaLN 模型
        pred_adaln = model_adaln(x, c)
        loss_adaln = criterion(pred_adaln, y)
        opt_adaln.zero_grad()
        loss_adaln.backward()
        opt_adaln.step()
        
        if epoch % 2 == 0:
            print(f"Epoch {epoch}: LN loss={loss_ln.item():.4f}, AdaLN loss={loss_adaln.item():.4f}")
    
    return model_ln, model_adaln

if __name__ == "__main__":
    print("训练对比: LayerNorm vs AdaLN")
    print("任务: 根据条件生成不同频率的正弦波")
    model_ln, model_adaln = train_and_compare()
    print("\n结论: AdaLN 在条件生成任务上通常收敛更快、性能更好")

8. 总结

8.1 核心区别

特性Layer NormalizationAdaptive Layer Normalization
γ, β 来源固定可学习参数由条件动态生成
适用任务无条件任务(分类、回归)条件生成任务(扩散、文本生成图像)
计算量小(只有 2D 参数)中等(需要 MLP 生成参数)
训练稳定性需要 zero init (AdaLN-Zero)
Transformer 应用标准选择扩散模型、多模态生成

8.2 一句话总结

  • LayerNorm:“我对所有样本一视同仁”(固定归一化参数)
  • AdaLN:“我根据条件来调整策略”(动态归一化参数)

8.3 在自动驾驶里的应用潜力

  1. 条件轨迹预测:根据车型、驾驶员类型调整预测
  2. 多传感器融合:根据不同传感器动态调整特征分布
  3. 端到端规划:根据导航指令调整驾驶策略
  4. 扩散模型规划:用 DiT 做轨迹生成(Sora 的同款技术)

参考资料

  1. Ba, Layer Normalization, arXiv 2016
  2. Peebles & Xie, Scalable Diffusion Models with Transformers (DiT), ICCV 2023
  3. Vaswani et al., Attention is All You Need, NeurIPS 2017
  4. He et al., Bag of Tricks for Image Classification, CVPR 2019 (Pre-Norm vs Post-Norm)
  5. PixArt-α, Fast Training of Diffusion Transformer, ICLR 2024 (AdaLN-single)

如果你觉得这篇文章有帮助,欢迎关注 Pulsar Guide 获取更多自动驾驶技术笔记!