4. 模型初始化固定:权重初始化方法选择、初始化种子固定、不同框架的初始化差异
模型训练跑出来的结果忽高忽低,很多时候问题就出在初始化这一步。你想想看,同样的网络结构,同样的数据,就因为权重初始值不一样,最后收敛到的位置可能天差地别。这一节我们就专门聊聊怎么把初始化这关守死。
4.1 权重初始化方法怎么选
选初始化方法,说白了就是给每层的权重定一个合适的“起跑线”。起跑线太靠前(值太大),梯度容易爆炸;太靠后(值太小),信号传几层就没了。
我个人习惯,先看激活函数,再选初始化器。下面这张表是我常用的搭配:
| 激活函数 | 推荐初始化方法 | 核心思想 |
|---|---|---|
| Sigmoid / Tanh | Xavier (Glorot) 均匀或正态 | 保持输入输出方差一致 |
| ReLU / Leaky ReLU | He (Kaiming) 均匀或正态 | 考虑 ReLU 将一半神经元置零 |
| Swish / GELU | He 初始化或 LeCun 初始化 | 视具体实现,He 更通用 |
| 无激活(回归头) | 小随机数(如 0.01 倍正态) | 避免初始输出过大 |
关键点:Xavier 假设激活函数是线性的,ReLU 不满足这个假设,所以用 He。我在项目中遇到过有人对 ReLU 网络用 Xavier,结果深层梯度消失,训练半天 loss 不动——换 He 之后立马见效。
代码里怎么配?以 PyTorch 为例:
import torch.nn as nn
def init_weights(m):
if isinstance(m, nn.Linear):
# He 初始化,ReLU 网络推荐
nn.init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
model.apply(init_weights)
小技巧:BatchNorm 层的 gamma 初始化为 1,beta 初始化为 0。但如果你做小批量微调,有时把 gamma 设成 0.1 或 0.01 反而更稳——我在迁移学习场景试过,收敛速度快了不少。
4.2 初始化种子固定——最容易被忽略的一步
选好了初始化方法,还得保证每次跑出来的初始值一模一样。否则你调个学习率,结果两次实验的起点不同,对比结果根本没意义。
固定种子,不是只写一行 torch.manual_seed(42) 就完事了。你得把能设的随机源全锁死:
- Python 随机数:
random.seed(42) - NumPy 随机数:
np.random.seed(42) - PyTorch CPU:
torch.manual_seed(42) - PyTorch GPU:
torch.cuda.manual_seed_all(42) - cuDNN 确定性:设置
torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False
我曾经踩过的坑:只设了 torch.manual_seed,没关 cuDNN benchmark。结果 cuDNN 每次选不同的卷积算法,虽然权重一样,但浮点运算顺序不同,导致 loss 曲线在前几步就有微小差异。排查了两天才找到原因。
完整的种子固定代码,我一般封装成一个函数:
import random
import numpy as np
import torch
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# TensorFlow 用户还需要:
# tf.random.set_seed(seed)
set_seed(2024)
注意:设了 deterministic=True 之后,训练速度会慢一些(大概 10%-30%)。我的建议是:调试阶段必须开,上线推理时如果追求极致性能可以关掉,但要确保推理结果可复现。
4.3 不同框架的初始化差异
同一个网络,在 PyTorch、TensorFlow、JAX 里跑,即使种子一样,初始权重也可能不同。为什么?因为每个框架的默认随机数生成器和初始化公式实现有细微差别。
我整理了一个对比表,方便你心里有数:
| 框架 | 默认初始化 | 种子设置方式 | 注意事项 |
|---|---|---|---|
| PyTorch | Kaiming 均匀 (Linear/Conv) | torch.manual_seed | cuDNN 确定性需额外设置 |
| TensorFlow (Keras) | Glorot 均匀 (Dense/Conv) | tf.random.set_seed | 操作级别种子也需设置 |
| JAX | 无默认,需显式传入 key | jax.random.PRNGKey | 函数式风格,key 要拆分 |
| PaddlePaddle | Xavier 均匀 (默认) | paddle.seed | 需配合 paddle.framework.random |
跨框架复现的难点:即使种子和初始化方法名一样,底层公式也可能不同。比如 PyTorch 的 kaiming_uniform_ 和 TensorFlow 的 he_uniform,边界计算方式有细微差异。如果你需要跨框架完全一致,建议手动实现初始化逻辑,用同一个随机数生成器。
下面这张 SVG 图,把初始化固定这件事的完整流程画清楚了:
4.4 实战中的几个建议
- 调试阶段:固定种子 + 确定性算法,确保每次结果一致。我习惯在训练脚本开头就调用
set_seed(),避免后面忘了。 - 对比实验:不同超参数对比时,用同一个种子。不同架构对比时,可以换 3-5 个种子取平均,避免某个种子恰好对某个架构有利。
- 生产环境:如果不需要严格复现,可以关掉
deterministic提升速度。但推理时的种子要固定,否则用户每次请求结果不同会很奇怪。 - 跨框架迁移:如果从 PyTorch 转到 TensorFlow,不要想当然认为结果一样。手动写一个初始化函数,用 NumPy 生成权重再赋值,最保险。
一个小工具:我写过一个 compare_init.py 脚本,把同一个网络在 PyTorch 和 TensorFlow 里各初始化一次,打印前向输出和统计量。跨框架迁移时跑一遍,能快速发现差异。你也可以试试这个思路。
嗯,初始化固定这件事,说起来简单,做起来细节不少。种子设了、方法选了、框架差异也清楚了,下一步就可以放心地调模型了。记住:稳定的起点,才有可复现的终点。