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) 就完事了。你得把能设的随机源全锁死:

  1. Python 随机数:random.seed(42)
  2. NumPy 随机数:np.random.seed(42)
  3. PyTorch CPU:torch.manual_seed(42)
  4. PyTorch GPU:torch.cuda.manual_seed_all(42)
  5. 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 图,把初始化固定这件事的完整流程画清楚了:

模型初始化固定流程 固定随机种子 关闭 cuDNN 随机性 选择初始化方法 验证一致性 Python random NumPy random Torch/TF random deterministic=True benchmark=False TF 操作级别种子 Xavier / He 正交初始化 自定义初始化 两次前向 输出一致 loss 一致 初始化完全可复现 训练结果稳定,可对比调参 注意:跨框架复现需手动对齐初始化公式,不能只依赖同名函数

4.4 实战中的几个建议

  • 调试阶段:固定种子 + 确定性算法,确保每次结果一致。我习惯在训练脚本开头就调用 set_seed(),避免后面忘了。
  • 对比实验:不同超参数对比时,用同一个种子。不同架构对比时,可以换 3-5 个种子取平均,避免某个种子恰好对某个架构有利。
  • 生产环境:如果不需要严格复现,可以关掉 deterministic 提升速度。但推理时的种子要固定,否则用户每次请求结果不同会很奇怪。
  • 跨框架迁移:如果从 PyTorch 转到 TensorFlow,不要想当然认为结果一样。手动写一个初始化函数,用 NumPy 生成权重再赋值,最保险。

一个小工具:我写过一个 compare_init.py 脚本,把同一个网络在 PyTorch 和 TensorFlow 里各初始化一次,打印前向输出和统计量。跨框架迁移时跑一遍,能快速发现差异。你也可以试试这个思路。

嗯,初始化固定这件事,说起来简单,做起来细节不少。种子设了、方法选了、框架差异也清楚了,下一步就可以放心地调模型了。记住:稳定的起点,才有可复现的终点。