权重初始化与训练稳定性
初始化错误,训练永远无法启动。初始化正确,50 层网络的训练会像 3 层一样流畅。
类型: 构建 语言: Python 前置条件: 第 03.04 课(激活函数)、第 03.07 课(正则化) 时间: 约 90 分钟
学习目标
- 实现零初始化、随机初始化、Xavier/Glorot 初始化和 Kaiming/He 初始化策略,并测量其在 50 层网络中对激活值量级的影响
- 推导 Xavier 初始化使用 Var(w) = 2/(fan_in + fan_out) 以及 Kaiming 初始化使用 Var(w) = 2/fan_in 的原因
- 演示零初始化的对称性问题,并解释为何随机规模本身并不充分
- 将正确的初始化策略与激活函数匹配:sigmoid/tanh 用 Xavier,ReLU/GELU 用 Kaiming
问题所在
将所有权重初始化为零,什么都学不到。每个神经元计算相同的函数,接收相同的梯度,做出相同的更新。经过 10,000 个 epoch 后,你的 512 个神经元隐藏层仍然是同一个神经元的 512 份副本。你付出了 512 个参数的代价,却只得到 1 个的效果。
将它们初始化得太大,激活值在网络中爆炸。到第 10 层时,数值达到 1e15。到第 20 层时,它们溢出为无穷大。梯度以相反的轨迹重演同样的过程。
从标准正态分布中随机初始化。3 层网络可以工作。但到 50 层时,信号会收缩至零或爆炸为无穷大,取决于随机规模是略微偏小还是略微偏大。"有效"与"崩溃"之间的边界极为微细。
权重初始化(Weight Initialization)是深度学习中最被低估的决策。网络架构有论文,优化器有博客,初始化只是一个脚注。但如果搞错了,其他一切都无关紧要——你的网络在训练开始之前就已死亡。
核心概念
对称性问题
一层中的每个神经元都有相同的结构:将输入乘以权重,加上偏置,应用激活函数。如果所有权重的初始值相同(零是极端情况),每个神经元的输出相同。在反向传播(Backpropagation)期间,每个神经元接收相同的梯度。在更新步骤中,每个神经元的变化量也相同。
陷入僵局。网络有数百个参数,但它们都同步移动。这称为对称性(Symmetry),随机初始化是打破它的暴力方法。每个神经元从权重空间的不同点出发,因此每个神经元学习不同的特征。
但"随机"还不够。随机性的规模决定了网络能否训练。
方差通过各层的传播
考虑一个有 fan_in 个输入的单层:
z = w1*x1 + w2*x2 + ... + w_n*x_n如果每个权重 wi 来自方差为 Var(w) 的分布,每个输入 xi 的方差为 Var(x),则输出方差为:
Var(z) = fan_in * Var(w) * Var(x)如果 Var(w) = 1,fan_in = 512,输出方差是输入方差的 512 倍。经过 10 层后:512^10 = 1.2e27。信号已经爆炸。
如果 Var(w) = 0.001,每层输出方差收缩为 0.001 * 512 = 0.512。经过 10 层后:0.512^10 = 0.00013。信号已经消失。
目标:选择 Var(w) 使得 Var(z) = Var(x)。信号量级在各层保持不变。
Xavier/Glorot 初始化
Glorot 和 Bengio(2010)推导出了适用于 sigmoid 和 tanh 激活函数的解。为使前向传播和反向传播中的方差保持不变:
Var(w) = 2 / (fan_in + fan_out)实践中,权重从以下分布中采样:
w ~ Uniform(-limit, limit) where limit = sqrt(6 / (fan_in + fan_out))或:
w ~ Normal(0, sqrt(2 / (fan_in + fan_out)))这之所以有效,是因为 sigmoid 和 tanh 在零附近近似线性,而正确初始化的激活值就处于这个区间。方差在数十层中保持稳定。
Kaiming/He 初始化
ReLU 会消灭一半的输出(所有负值变为零)。由于平均有一半的输入被置零,有效 fan_in 减半。Xavier 初始化没有考虑这一点——它低估了所需的方差。
He 等人(2015)调整了公式:
Var(w) = 2 / fan_in权重从以下分布中采样:
w ~ Normal(0, sqrt(2 / fan_in))因子 2 补偿了 ReLU 将一半激活值置零的影响。没有它,信号每层会收缩约 0.5 倍。经过 50 层:0.5^50 = 8.8e-16。Kaiming 初始化防止了这种情况。
Transformer 初始化
GPT-2 引入了一种不同的模式。残差连接将每个子层的输出加到其输入上:
x = x + sublayer(x)每次加法都会增大方差。有 N 个残差层时,方差与 N 成比例增长。GPT-2 将残差层的权重缩放为 1/sqrt(2N),其中 N 是层数。这使累积的信号量级保持稳定。
Llama 3(4050 亿参数,126 层)使用类似的方案。若没有这种缩放,残差流在经过 126 层注意力和前馈块后将无限增大。
flowchart TD
subgraph "零初始化"
Z1["第1层<br/>所有权重 = 0"] --> Z2["第2层<br/>所有神经元相同"]
Z2 --> Z3["第3层<br/>仍然相同"]
Z3 --> ZR["结果:无论宽度如何<br/>只有1个有效神经元"]
end
subgraph "Xavier 初始化"
X1["第1层<br/>Var = 2/(fan_in+fan_out)"] --> X2["第2层<br/>信号稳定"]
X2 --> X3["第50层<br/>信号稳定"]
X3 --> XR["结果:sigmoid/tanh<br/>可成功训练"]
end
subgraph "Kaiming 初始化"
K1["第1层<br/>Var = 2/fan_in"] --> K2["第2层<br/>信号稳定"]
K2 --> K3["第50层<br/>信号稳定"]
K3 --> KR["结果:ReLU/GELU<br/>可成功训练"]
end50 层中的激活值量级
graph LR
subgraph "平均激活值量级"
direction LR
L1["第1层"] --> L10["第10层"] --> L25["第25层"] --> L50["第50层"]
end
subgraph "结果"
R1["随机 N(0,1):第5层即爆炸"]
R2["随机 N(0,0.01):第10层即消失"]
R3["Xavier + Sigmoid:第50层约为 1.0"]
R4["Kaiming + ReLU:第50层约为 1.0"]
end选择正确的初始化
flowchart TD
Start["使用什么激活函数?"] --> Act{"激活函数类型?"}
Act -->|"Sigmoid / Tanh"| Xavier["Xavier/Glorot<br/>Var = 2/(fan_in + fan_out)"]
Act -->|"ReLU / Leaky ReLU"| Kaiming["Kaiming/He<br/>Var = 2/fan_in"]
Act -->|"GELU / Swish"| Kaiming2["Kaiming/He<br/>(与 ReLU 相同)"]
Act -->|"Transformer 残差"| GPT["缩放为 1/sqrt(2N)<br/>N = 层数"]
Xavier --> Check["验证:各层激活值量级<br/>保持在 0.5 到 2.0 之间"]
Kaiming --> Check
Kaiming2 --> Check
GPT --> Check构建实现
第一步:初始化策略
四种权重矩阵初始化方式。每种方式都返回一个列表的列表(二维矩阵),有 fan_in 列和 fan_out 行。
import math
import random
def zero_init(fan_in, fan_out):
return [[0.0 for _ in range(fan_in)] for _ in range(fan_out)]
def random_init(fan_in, fan_out, scale=1.0):
return [[random.gauss(0, scale) for _ in range(fan_in)] for _ in range(fan_out)]
def xavier_init(fan_in, fan_out):
std = math.sqrt(2.0 / (fan_in + fan_out))
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
def kaiming_init(fan_in, fan_out):
std = math.sqrt(2.0 / fan_in)
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]第二步:激活函数
我们需要 sigmoid、tanh 和 ReLU,以便用各自对应的激活函数测试每种初始化策略。
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def tanh_act(x):
return math.tanh(x)
def relu(x):
return max(0.0, x)第三步:经过 50 层的前向传播
将随机数据通过深层网络传递,并在每层测量平均激活值量级。
def forward_deep(init_fn, activation_fn, n_layers=50, width=64, n_samples=100):
random.seed(42)
layer_magnitudes = []
inputs = [[random.gauss(0, 1) for _ in range(width)] for _ in range(n_samples)]
for layer_idx in range(n_layers):
weights = init_fn(width, width)
biases = [0.0] * width
new_inputs = []
for sample in inputs:
output = []
for neuron_idx in range(width):
z = sum(weights[neuron_idx][j] * sample[j] for j in range(width)) + biases[neuron_idx]
output.append(activation_fn(z))
new_inputs.append(output)
inputs = new_inputs
magnitudes = []
for sample in inputs:
magnitudes.append(sum(abs(v) for v in sample) / width)
mean_mag = sum(magnitudes) / len(magnitudes)
layer_magnitudes.append(mean_mag)
return layer_magnitudes第四步:实验
运行所有组合:零初始化、随机 N(0,1)、随机 N(0,0.01)、Xavier+sigmoid、Xavier+tanh、Kaiming+ReLU。打印关键层处的量级。
def run_experiment():
configs = [
("Zero init + Sigmoid", lambda fi, fo: zero_init(fi, fo), sigmoid),
("Random N(0,1) + ReLU", lambda fi, fo: random_init(fi, fo, 1.0), relu),
("Random N(0,0.01) + ReLU", lambda fi, fo: random_init(fi, fo, 0.01), relu),
("Xavier + Sigmoid", xavier_init, sigmoid),
("Xavier + Tanh", xavier_init, tanh_act),
("Kaiming + ReLU", kaiming_init, relu),
]
print(f"{'Strategy':<30} {'L1':>10} {'L5':>10} {'L10':>10} {'L25':>10} {'L50':>10}")
print("-" * 80)
for name, init_fn, act_fn in configs:
mags = forward_deep(init_fn, act_fn)
row = f"{name:<30}"
for idx in [0, 4, 9, 24, 49]:
val = mags[idx]
if val > 1e6:
row += f" {'EXPLODED':>10}"
elif val < 1e-6:
row += f" {'VANISHED':>10}"
else:
row += f" {val:>10.4f}"
print(row)第五步:对称性演示
展示零初始化会产生相同的神经元。
def symmetry_demo():
random.seed(42)
weights = zero_init(2, 4)
biases = [0.0] * 4
inputs = [0.5, -0.3]
outputs = []
for neuron_idx in range(4):
z = sum(weights[neuron_idx][j] * inputs[j] for j in range(2)) + biases[neuron_idx]
outputs.append(sigmoid(z))
print("\nSymmetry Demo (4 neurons, zero init):")
for i, out in enumerate(outputs):
print(f" Neuron {i}: output = {out:.6f}")
all_same = all(abs(outputs[i] - outputs[0]) < 1e-10 for i in range(len(outputs)))
print(f" All identical: {all_same}")
print(f" Effective parameters: 1 (not {len(weights) * len(weights[0])})")第六步:逐层量级报告
打印 50 层中激活值量级的文本条形图。
def magnitude_report(name, magnitudes):
print(f"\n{name}:")
for i, mag in enumerate(magnitudes):
if i % 5 == 0 or i == len(magnitudes) - 1:
if mag > 1e6:
bar = "X" * 50 + " EXPLODED"
elif mag < 1e-6:
bar = "." + " VANISHED"
else:
bar_len = min(50, max(1, int(mag * 10)))
bar = "#" * bar_len
print(f" Layer {i+1:3d}: {bar} ({mag:.6f})")实际使用
PyTorch 将这些作为内置函数提供:
import torch
import torch.nn as nn
layer = nn.Linear(512, 256)
nn.init.xavier_uniform_(layer.weight)
nn.init.xavier_normal_(layer.weight)
nn.init.kaiming_uniform_(layer.weight, nonlinearity='relu')
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
nn.init.zeros_(layer.bias)当你调用 nn.Linear(512, 256) 时,PyTorch 默认使用 Kaiming 均匀初始化。这就是为什么大多数简单网络"开箱即用"的原因——PyTorch 已经做出了正确选择。但当你构建自定义架构或深度超过 20 层时,你需要了解背后发生的事情,并可能需要覆盖默认值。
对于 Transformer,HuggingFace 模型通常在其 _init_weights 方法中处理初始化。GPT-2 的实现将残差投影缩放为 1/sqrt(N)。如果你从零构建 Transformer,需要自行添加这一步。
交付成果
本课产出:
outputs/prompt-init-strategy.md-- 一个诊断权重初始化问题并推荐正确策略的提示词
练习
添加 LeCun 初始化(Var = 1/fan_in,为 SELU 激活设计)。用 LeCun 初始化 + tanh 运行 50 层实验,与 Xavier + tanh 进行比较。
实现 GPT-2 的残差缩放:在加入残差流之前,将每层输出乘以 1/sqrt(2*N)。分别在有无缩放的情况下运行 50 层,测量残差量级增长速度。
创建一个"初始化健康检查"函数,接受网络层维度和激活类型,推荐正确的初始化方式,并在当前初始化会导致问题时发出警告。
分别用 fan_in = 16 和 fan_in = 1024 运行实验。Xavier 和 Kaiming 会适应 fan_in,但随机初始化不会。展示随着层变宽,"有效"和"失效"之间的差距如何扩大。
实现正交初始化(生成随机矩阵,计算其 SVD,使用正交矩阵 U)。与 Kaiming 初始化在 50 层 ReLU 网络上进行比较。
关键术语
| 术语 | 常见说法 | 实际含义 |
|---|---|---|
| 权重初始化(Weight Initialization) | "随机设置初始权重" | 选择初始权重值的策略,决定网络能否进行训练 |
| 对称性破除(Symmetry Breaking) | "让神经元各不相同" | 使用随机初始化确保神经元学习不同特征,而非计算相同函数 |
| 输入扇入(Fan-in) | "神经元的输入数量" | 输入连接数,决定加权求和中输入方差的累积方式 |
| 输出扇出(Fan-out) | "神经元的输出数量" | 输出连接数,与反向传播时保持梯度方差有关 |
| Xavier/Glorot 初始化 | "sigmoid 初始化" | Var(w) = 2/(fan_in + fan_out),设计用于保持方差通过 sigmoid 和 tanh 激活 |
| Kaiming/He 初始化 | "ReLU 初始化" | Var(w) = 2/fan_in,考虑了 ReLU 将一半激活值置零的影响 |
| 方差传播(Variance Propagation) | "信号在各层如何增大或缩小" | 基于权重规模分析激活方差逐层变化的数学方法 |
| 残差缩放(Residual Scaling) | "GPT-2 的初始化技巧" | 将残差连接权重缩放为 1/sqrt(2N),防止方差在 N 个 Transformer 层中增长 |
| 死亡网络(Dead Network) | "什么都学不到" | 因初始化不当导致所有梯度为零或所有激活值饱和的网络 |
| 激活值爆炸(Exploding Activations) | "数值趋于无穷" | 当权重方差过大时,激活值量级在各层呈指数增长 |
延伸阅读
- Glorot & Bengio, "Understanding the difficulty of training deep feedforward neural networks" (2010) -- Xavier 初始化原始论文,包含方差分析
- He et al., "Delving Deep into Rectifiers" (2015) -- 为 ReLU 网络引入 Kaiming 初始化
- Radford et al., "Language Models are Unsupervised Multitask Learners" (2019) -- GPT-2 论文,包含残差缩放初始化
- Mishkin & Matas, "All You Need is a Good Init" (2016) -- 层序列单位方差初始化,解析公式的经验替代方案