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 里的局限性
- 变长序列:Transformer 处理的是变长序列(padding 到最大长度),BN 的 batch 统计量会被 padding 影响
- 小 batch 训练:GPU 显存限制,batch size 可能很小(比如 8 或 16),BN 的统计量不准确
- 位置信息:BN 会在 batch 维度上混合不同样本的信息,可能破坏序列的位置信息
- 并行性: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?
- 不受 batch size 影响:每个样本独立归一化,batch size=1 也能用
- 支持变长序列:padding 不影响其他位置的归一化
- 保持位置信息:只在特征维度归一化,序列维度保持不变
- 训练稳定: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。
核心创新:
- 用 ViT 作为扩散模型的骨干网络
- 用 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 Normalization | Adaptive Layer Normalization |
|---|---|---|
| γ, β 来源 | 固定可学习参数 | 由条件动态生成 |
| 适用任务 | 无条件任务(分类、回归) | 条件生成任务(扩散、文本生成图像) |
| 计算量 | 小(只有 2D 参数) | 中等(需要 MLP 生成参数) |
| 训练稳定性 | 好 | 需要 zero init (AdaLN-Zero) |
| Transformer 应用 | 标准选择 | 扩散模型、多模态生成 |
8.2 一句话总结
- LayerNorm:“我对所有样本一视同仁”(固定归一化参数)
- AdaLN:“我根据条件来调整策略”(动态归一化参数)
8.3 在自动驾驶里的应用潜力
- 条件轨迹预测:根据车型、驾驶员类型调整预测
- 多传感器融合:根据不同传感器动态调整特征分布
- 端到端规划:根据导航指令调整驾驶策略
- 扩散模型规划:用 DiT 做轨迹生成(Sora 的同款技术)
参考资料
- Ba, Layer Normalization, arXiv 2016
- Peebles & Xie, Scalable Diffusion Models with Transformers (DiT), ICCV 2023
- Vaswani et al., Attention is All You Need, NeurIPS 2017
- He et al., Bag of Tricks for Image Classification, CVPR 2019 (Pre-Norm vs Post-Norm)
- PixArt-α, Fast Training of Diffusion Transformer, ICLR 2024 (AdaLN-single)
如果你觉得这篇文章有帮助,欢迎关注 Pulsar Guide 获取更多自动驾驶技术笔记!