参数初始化
本教程共 60 篇 · 第 29 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:搞懂参数初始化的作用和两种主流方案,学会自定义初始化,再掌握从别的模型借参数的热启动技巧。
为什么初始化值得认真对待
训练神经网络,第一步不是前向传播,而是给参数填初始值。你随手创建 nn.Linear,PyTorch 已经悄悄帮你填好了。这步看着不起眼,其实决定了训练的起点。
打个比方,初始化就像马拉松的起跑位置。站位太偏,跑很久都到不了终点;站位合理,很快就上正轨。初始化不当,训练会掉进三个坑:
- 对称问题:所有参数一样,梯度也一样,网络学不出差异。
- 梯度消失:参数太小,信号传几层就没了。
- 梯度爆炸:参数太大,数值一路飙升出 NaN。
所以初始化的目标很明确:让每层输出保持合适的数值范围,让梯度能顺畅地从输出层流回输入层。
PyTorch 的默认初始化
好消息是,PyTorch 的默认初始化做得不错,大多数场景不用改。
nn.Linear 和 nn.Conv2d 的权重默认用 kaiming_uniform 初始化,偏置(bias)用一个小范围的均匀分布初始化。具体说,nn.Linear(784, 128) 的权重会均匀落在 ±1/√784 ≈ ±0.036 之间。可以验证一下:
import torch
import torch.nn as nn
fc = nn.Linear(784, 128)
print(fc.weight.min().item(), fc.weight.max().item())
跑出来能看到最大最小值,正好在 ±0.036 附近。
Note均匀分布的区间大小和输入数量有关:输入越多,区间越窄。直觉是输入多了,求和结果就大,得用更小的数压住。
想快速判断初始化是否合理,看标准差就行:print(fc.weight.std().item())。标准差大得离谱,警惕梯度爆炸;小到接近零,警惕梯度消失。
Xavier 与 Kaiming:两种经典方案
除了默认值,你还会在论文里常见两个名字:Xavier 和 Kaiming(何恺明)。它们回答同一个问题:权重方差取多少,信号经过一层后不放大也不缩小。
先认识两个小概念:
- fan_in:一个神经元的输入连接数,对
Linear(in, out)就是 in。 - fan_out:输出连接数,对应 out。
Xavier 初始化(也叫 Glorot 初始化)假设激活函数接近线性,适合 tanh、sigmoid 这类对称函数。它的取值区间是 gain × √(6/(fan_in+fan_out)),输入输出都照顾到。
Kaiming 初始化专门为 ReLU 设计。ReLU 会把一半输入置零,信号减半,所以 Kaiming 的取值要更大一些,在 √(2/fan_in) 量级。PyTorch 默认选它,正是因为现在 ReLU 用得最多。
import torch.nn.init as init
# Xavier:适合 tanh / sigmoid
init.xavier_uniform_(fc.weight)
# Kaiming:适合 ReLU 一族
init.kaiming_normal_(fc.weight, nonlinearity="relu")
Tip选型口诀:激活函数是 ReLU 或它的变体,用 Kaiming;是 tanh / sigmoid,用 Xavier。拿不准就用默认值,它属于 Kaiming 系。
自定义初始化
想自己控制初始化,有两条路:直接改某个层,或对整个模型统一处理。
直接改最简单:
init.normal_(fc.weight, mean=0, std=0.01)
init.zeros_(fc.bias)
偏置一般初始化为 0,因为权重已经负责打破对称性。
批量处理用 apply 更省事:
def init_weights(m):
if isinstance(m, nn.Linear):
init.xavier_uniform_(m.weight)
init.zeros_(m.bias)
net.apply(init_weights)
apply 会递归遍历模型里所有子模块,对每个模块调用你给的函数。nn.init 里还有 constant_、orthogonal_、sparse_ 等十几种,用到时查文档就行。
Warning初始化要在创建优化器之前完成,也别在训练中途乱改参数,否则优化器的内部状态会和新参数对不上。
几种特殊层的初始化习惯
除了全连接和卷积,还有几类层的初始化有约定俗成的做法:
- BatchNorm 的权重默认初始化为 1,偏置为 0。这样归一化后的数据一开始原样通过,缩放能力交给训练慢慢学。这个默认值很好,别乱改。
nn.Embedding默认用标准正态分布初始化,查表类任务基本够用。- RNN 类模型常用正交初始化(orthogonal_),让初始权重保持正交性,有助于缓解长期依赖问题。
这些细节知道就行,遇到时能想起来「原来还有这么回事」。
初始化前先设好随机种子
初始化依赖随机数,不设种子跑两次结果不一样,实验就没法复现。训练脚本开头加几行:
import random
import numpy as np
import torch
random.seed(42)
np.random.seed(42)
torch.manual_seed(42)
这样每次跑,初始权重和数据的打乱顺序都一致。第 33 章讲调试技巧时还会再见到它。
什么时候别动初始化
有一种情况要特别小心:迁移学习。
加载预训练权重之后,千万别再对整个模型跑一遍 apply(init_weights),那会把辛苦训好的参数全冲掉。正确做法是:先加载权重,只对新增的层做初始化:
# 片段示例:net 是加载完预训练权重的模型
net.load_state_dict(sd, strict=False)
init.xavier_uniform_(net.new_fc.weight) # 只初始化新加的层
加载、初始化、再创建优化器,顺序别乱。
初始化效果怎么检查
初始化合不合理,有个简单的自测:随机造一批输入,跑一次前向(不训练),看各层输出的均值、标准差:
# 沿用上面的 fc:nn.Linear(784, 128)
x = torch.randn(64, 784)
with torch.no_grad():
h = torch.relu(fc(x))
print(h.mean().item(), h.std().item())
正常情况:ReLU 之后大约一半是 0,剩余数值保持在个位数级别。如果某层输出全部是 0,或数值爆炸到上千,初始化就有问题,回去检查这层的 fan_in 和方差设置。
从别的模型借参数:热启动
初始化不一定从随机开始。拿别人训好的模型参数当起点,收敛会快得多,这叫热启动(warm start)。
第 25 章讲过 load_state_dict。热启动的关键在于:两边的网络不必完全一样,对不上的键可以忽略。
import torch
import torch.nn as nn
net_a = nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 2))
net_b = nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 5))
sd = net_a.state_dict()
net_b.load_state_dict(sd, strict=False) # 只载入形状对得上的部分
strict=False 表示允许键不匹配,能对上多少就载入多少。键名对不上但形状一样的,改个名就能用:
sd = {"0.weight": torch.randn(20, 10)}
sd["input.weight"] = sd.pop("0.weight") # 改键名后再载入
加载大模型时还有三个省内存的技巧(PyTorch 2.1 起可用):
sd = torch.load("checkpoint.pth", mmap=True, weights_only=True)
with torch.device("meta"):
m = nn.Sequential(nn.Linear(1000, 1000)) # 只建形状,不分配内存
m.load_state_dict(sd, assign=True) # 直接换引用,不复制
mmap=True:把文件映射进虚拟内存,不用整份读进内存。torch.device("meta"):创建「空壳」模型,只有形状没有数据。assign=True:直接替换参数张量,省掉一次复制。
Note用了
assign=True之后,优化器要重新创建,因为参数对象整个换过了,旧引用会失效。
小结
初始化决定训练起点,PyTorch 默认值已经够好。要自定义就用 nn.init 加 apply,要省时间就用热启动。下一章进入可视化与调试部分,先给训练过程装个「监控摄像头」——TensorBoard。