【深度学习精通】第6章 | 权重初始化与优化策略 - 让训练更高效的技巧
环境声明
- Python版本:Python 3.10+
- PyTorch版本:PyTorch 2.0+
- 开发工具:PyCharm / VS Code / Jupyter Notebook
- 操作系统:Windows / macOS / Linux(通用)
- GPU支持:CUDA 11.8+(可选,但推荐)
学习目标
通过本章学习,你将掌握:
- 理解权重初始化对深度神经网络训练的重要性
- 掌握Xavier/Glorot初始化的原理和适用场景
- 掌握He初始化的原理和适用场景
- 了解正交初始化和谱初始化的应用场景
- 学会使用各种学习率调度策略优化训练过程
- 能够根据网络结构选择合适的初始化方法
内容摘要
权重初始化是深度学习训练的基础环节,良好的初始化可以加速收敛、防止梯度消失或爆炸。本章将深入讲解Xavier、He等经典初始化方法,以及学习率调度策略中的Step、Cosine Annealing和Warmup技术,帮助你构建更稳定、更高效的神经网络。
1. 权重初始化的重要性和影响
1.1 为什么权重初始化如此重要
想象你正在建造一座摩天大楼,如果地基打得不好,无论上面的建筑多么精美,最终都可能倒塌。权重初始化就是深度学习模型的"地基"。
权重初始化不当会导致的问题:
- 梯度消失(Vanishing Gradient):当权重过小,信号在深层网络中逐层衰减,导致前面层几乎无法学习
- 梯度爆炸(Exploding Gradient):当权重过大,信号在传播过程中指数级增长,导致参数更新失控
- 对称性问题:如果所有权重初始化为相同值,神经元将学习到相同的特征,失去表达能力
- 收敛缓慢:不合适的初始化可能导致模型需要更多轮次才能收敛
1.2 前向传播与反向传播中的信号流动
在深度神经网络中,信息通过前向传播计算输出,通过反向传播更新参数。我们来看信号是如何流动的:
设第l层的输入为 x(l)x^{(l)}x(l),权重为 W(l)W^{(l)}W(l),激活函数为 fff,则:
z(l)=W(l)x(l)+b(l)z^{(l)} = W^{(l)} x^{(l)} + b^{(l)}z(l)=W(l)x(l)+b(l)
x(l+1)=f(z(l))x^{(l+1)} = f(z^{(l)})x(l+1)=f(z(l))
在反向传播中,梯度通过链式法则传递:
∂L∂W(l)=∂L∂x(l+1)⋅∂x(l+1)∂z(l)⋅∂z(l)∂W(l)\frac{\partial L}{\partial W^{(l)}} = \frac{\partial L}{\partial x^{(l+1)}} \cdot \frac{\partial x^{(l+1)}}{\partial z^{(l)}} \cdot \frac{\partial z^{(l)}}{\partial W^{(l)}}∂W(l)∂L=∂x(l+1)∂L⋅∂z(l)∂x(l+1)⋅∂W(l)∂z(l)
关键洞察:如果每一层的权重方差控制不当,多层累积后信号的方差会发生剧烈变化。
1.3 方差分析的基本思想
假设输入 xxx 和权重 www 都是独立同分布的随机变量,均值为0,方差分别为 Var(x)Var(x)Var(x) 和 Var(w)Var(w)Var(w)。对于线性变换 y=∑i=1nwixiy = \sum_{i=1}^{n} w_i x_iy=∑i=1nwixi:
Var(y)=∑i=1nVar(wixi)=n⋅Var(w)⋅Var(x)Var(y) = \sum_{i=1}^{n} Var(w_i x_i) = n \cdot Var(w) \cdot Var(x)Var(y)=i=1∑nVar(wixi)=n⋅Var(w)⋅Var(x)
为了保持信号强度稳定,我们希望 Var(y)≈Var(x)Var(y) \approx Var(x)Var(y)≈Var(x),这意味着:
n⋅Var(w)≈1⇒Var(w)≈1nn \cdot Var(w) \approx 1 \Rightarrow Var(w) \approx \frac{1}{n}n⋅Var(w)≈1⇒Var(w)≈n1
这就是权重初始化方法设计的核心思想。
2. Xavier/Glorot初始化
2.1 方法背景
Xavier初始化(又称Glorot初始化)由Xavier Glorot和Yoshua Bengio在2010年的论文《Understanding the difficulty of training deep feedforward neural networks》中提出。这是深度学习领域最具影响力的初始化方法之一。
2.2 核心原理
Xavier初始化的目标是:保持前向传播和反向传播时信号方差的一致性。
假设:
- 权重 wijw_{ij}wij 独立同分布,均值为0
- 输入 xjx_jxj 独立同分布,均值为0
- 激活函数关于0对称(如tanh、sigmoid在0附近近似线性)
对于前向传播,输出方差为:
Var(y)=nin⋅Var(w)⋅Var(x)Var(y) = n_{in} \cdot Var(w) \cdot Var(x)Var(y)=nin⋅Var(w)⋅Var(x)
为了保持 Var(y)=Var(x)Var(y) = Var(x)Var(y)=Var(x),需要:
Var(w)=1ninVar(w) = \frac{1}{n_{in}}Var(w)=nin1
对于反向传播,考虑梯度回传:
Var(∂L∂x)=nout⋅Var(w)⋅Var(∂L∂y)Var(\frac{\partial L}{\partial x}) = n_{out} \cdot Var(w) \cdot Var(\frac{\partial L}{\partial y})Var(∂x∂L)=nout⋅Var(w)⋅Var(∂y∂L)
为了保持梯度方差稳定,需要:
Var(w)=1noutVar(w) = \frac{1}{n_{out}}Var(w)=nout1
2.3 Xavier初始化公式
为了同时满足前向和反向传播的要求,Xavier初始化取两者的调和平均:
Var(w)=2nin+noutVar(w) = \frac{2}{n_{in} + n_{out}}Var(w)=nin+nout2
均匀分布版本:
w∼U[−6nin+nout,6nin+nout]w \sim U\left[-\sqrt{\frac{6}{n_{in} + n_{out}}}, \sqrt{\frac{6}{n_{in} + n_{out}}}\right]w∼U[−nin+nout6,nin+nout6]
正态分布版本:
w∼N(0,2nin+nout)w \sim N\left(0, \sqrt{\frac{2}{n_{in} + n_{out}}}\right)w∼N(0,nin+nout2)
其中:
- ninn_{in}nin:输入神经元数量(输入维度)
- noutn_{out}nout:输出神经元数量(输出维度)
2.4 PyTorch实现
import torch
import torch.nn as nn
# 方法1:使用PyTorch内置的Xavier初始化
layer = nn.Linear(784, 256)
nn.init.xavier_uniform_(layer.weight) # 均匀分布
# 或者
nn.init.xavier_normal_(layer.weight) # 正态分布
# 方法2:在模型定义中应用
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
# 应用Xavier初始化
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
2.5 适用场景
推荐使用Xavier初始化的场景:
- 激活函数为tanh或sigmoid:这些函数在0附近近似线性,符合Xavier的假设
- 对称激活函数:输出关于0对称的激活函数
- 浅层到中层网络:网络深度不是特别极端时表现良好
- 全连接层和卷积层:都可以应用Xavier初始化
不推荐的场景:
- 使用ReLU激活函数时(应使用He初始化)
- 非常深的网络可能需要更精细的初始化策略
3. He初始化
3.1 方法背景
He初始化(又称Kaiming初始化)由Kaiming He等人在2015年的论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》中提出,专门解决ReLU激活函数的初始化问题。
3.2 为什么Xavier不适用于ReLU
ReLU激活函数定义为 f(x)=max(0,x)f(x) = max(0, x)f(x)=max(0,x),它将所有负值置为0。这破坏了Xavier初始化关于激活函数对称性的假设。
关键问题:当输入为负时,ReLU输出为0,导致大约50%的神经元在初始化时被"关闭"。这使得实际参与计算的神经元数量减少,信号方差发生变化。
3.3 He初始化原理
对于ReLU激活函数,考虑到大约一半的神经元被置零,前向传播的方差变为:
Var(y)=12nin⋅Var(w)⋅Var(x)Var(y) = \frac{1}{2} n_{in} \cdot Var(w) \cdot Var(x)Var(y)=21nin⋅Var(w)⋅Var(x)
为了保持 Var(y)=Var(x)Var(y) = Var(x)Var(y)=Var(x),需要:
Var(w)=2ninVar(w) = \frac{2}{n_{in}}Var(w)=nin2
3.4 He初始化公式
均匀分布版本:
w∼U[−6nin,6nin]w \sim U\left[-\sqrt{\frac{6}{n_{in}}}, \sqrt{\frac{6}{n_{in}}}\right]w∼U[−nin6,nin6]
正态分布版本:
w∼N(0,2nin)w \sim N\left(0, \sqrt{\frac{2}{n_{in}}}\right)w∼N(0,nin2)
对于LeakyReLU的变体:
如果LeakyReLU的负斜率为 aaa,则:
Var(w)=2(1+a2)⋅ninVar(w) = \frac{2}{(1 + a^2) \cdot n_{in}}Var(w)=(1+a2)⋅nin2
3.5 PyTorch实现
import torch
import torch.nn as nn
# 方法1:使用PyTorch内置的He初始化
layer = nn.Linear(784, 256)
nn.init.kaiming_uniform_(layer.weight, mode='fan_in', nonlinearity='relu')
# 或者
nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')
# 方法2:在CNN模型中应用He初始化
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
# He初始化适用于ReLU
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = torch.relu(self.conv1(x))
x = torch.max_pool2d(x, 2)
x = torch.relu(self.conv2(x))
x = torch.max_pool2d(x, 2)
x = x.view(x.size(0), -1)
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
3.6 适用场景
推荐使用He初始化的场景:
- ReLU激活函数:这是He初始化的主要设计目标
- LeakyReLU、PReLU等ReLU变体:可以使用带参数的He初始化
- 深层卷积神经网络:ResNet、VGG等现代CNN架构
- 计算机视觉任务:图像分类、目标检测等
4. 正交初始化与谱初始化
4.1 正交初始化
正交初始化通过生成正交矩阵来初始化权重,确保变换保持向量长度不变。
数学原理:
正交矩阵 QQQ 满足 QTQ=IQ^T Q = IQTQ=I,这意味着:
- 变换保持向量长度:∥Qx∥=∥x∥\|Qx\| = \|x\|∥Qx∥=∥x∥
- 特征值绝对值为1,避免梯度消失或爆炸
PyTorch实现:
import torch
import torch.nn as nn
# 正交初始化
layer = nn.Linear(256, 256)
nn.init.orthogonal_(layer.weight, gain=1.0)
# gain参数用于缩放,对于ReLU通常使用sqrt(2)
nn.init.orthogonal_(layer.weight, gain=torch.sqrt(torch.tensor(2.0)))
适用场景:
- 循环神经网络(RNN、LSTM)的隐藏层到隐藏层连接
- 需要保持信号强度的深层网络
- 对抗训练中的生成器和判别器
4.2 谱初始化
谱初始化(Spectral Normalization)主要用于生成对抗网络(GAN)中的判别器,通过限制权重矩阵的谱范数来控制Lipschitz常数。
核心思想:
谱范数 σ(W)\sigma(W)σ(W) 是矩阵 WWW 的最大奇异值。谱初始化确保:
σ(W)=1\sigma(W) = 1σ(W)=1
这限制了梯度的大小,使训练更加稳定。
PyTorch实现:
import torch
import torch.nn as nn
# 使用spectral_norm包装层
layer = nn.utils.spectral_norm(nn.Linear(256, 256))
# 在GAN判别器中的应用示例
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.fc1 = nn.utils.spectral_norm(nn.Linear(784, 512))
self.fc2 = nn.utils.spectral_norm(nn.Linear(512, 256))
self.fc3 = nn.utils.spectral_norm(nn.Linear(256, 1))
def forward(self, x):
x = torch.leaky_relu(self.fc1(x), 0.2)
x = torch.leaky_relu(self.fc2(x), 0.2)
x = self.fc3(x)
return x
5. 初始化方法对比
| 初始化方法 | 适用激活函数 | 方差公式 | 主要应用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| Xavier/Glorot | tanh、sigmoid、softsign | 2nin+nout\frac{2}{n_{in} + n_{out}}nin+nout2 | 全连接层、浅层网络 | 平衡前向/反向传播 | 不适用于ReLU |
| He/Kaiming | ReLU、LeakyReLU | 2nin\frac{2}{n_{in}}nin2 | CNN、深层网络 | 专为ReLU设计 | 对tanh效果一般 |
| 正交初始化 | 任意 | 保持范数 | RNN、深层网络 | 防止梯度消失/爆炸 | 计算成本较高 |
| 谱初始化 | 任意 | 谱范数=1 | GAN判别器 | 训练稳定 | 限制表达能力 |
| 随机初始化 | 任意 | 自定义 | 简单实验 | 实现简单 | 需要仔细调参 |
选择建议:
- 使用ReLU族激活函数 -> 选择He初始化
- 使用tanh/sigmoid激活函数 -> 选择Xavier初始化
- 训练RNN/LSTM -> 考虑正交初始化
- 训练GAN -> 判别器使用谱初始化
6. 学习率调度策略
6.1 为什么需要学习率调度
学习率是深度学习训练中最重要的超参数之一。固定的学习率往往不能适应训练的不同阶段:
- 训练初期:需要较大学习率快速接近最优解
- 训练中期:需要适中学习率稳定收敛
- 训练后期:需要较小学习率精细调整
6.2 StepLR:阶梯式衰减
StepLR在固定的训练轮次后按因子衰减学习率。
原理:
lrepoch=lrinitial×γ⌊epoch/step_size⌋lr_{epoch} = lr_{initial} \times \gamma^{\lfloor epoch / step\_size \rfloor}lrepoch=lrinitial×γ⌊epoch/step_size⌋
PyTorch实现:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
# 定义模型、损失函数和优化器
model = NeuralNetwork()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# 每30个epoch将学习率乘以0.1
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
# 训练循环
for epoch in range(100):
# 训练代码...
train(model, optimizer, criterion)
# 更新学习率
scheduler.step()
print(f"Epoch {epoch}, LR: {scheduler.get_last_lr()[0]}")
适用场景:
- 训练过程相对稳定的任务
- 需要明确控制学习率下降时机的场景
- 传统图像分类任务
6.3 ExponentialLR:指数衰减
指数衰减使学习率按指数函数连续下降。
原理:
lrepoch=lrinitial×γepochlr_{epoch} = lr_{initial} \times \gamma^{epoch}lrepoch=lrinitial×γepoch
PyTorch实现:
from torch.optim.lr_scheduler import ExponentialLR
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 每个epoch学习率乘以0.95
scheduler = ExponentialLR(optimizer, gamma=0.95)
for epoch in range(100):
train(model, optimizer, criterion)
scheduler.step()
6.4 Cosine Annealing:余弦退火
余弦退火使用余弦函数平滑地降低学习率,在训练结束时接近0。
原理:
lrt=lrmin+12(lrmax−lrmin)(1+cos(tTπ))lr_{t} = lr_{min} + \frac{1}{2}(lr_{max} - lr_{min})(1 + cos(\frac{t}{T}\pi))lrt=lrmin+21(lrmax−lrmin)(1+cos(Ttπ))
其中 ttt 是当前步数,TTT 是总步数。
PyTorch实现:
from torch.optim.lr_scheduler import CosineAnnealingLR
optimizer = optim.SGD(model.parameters(), lr=0.1)
# T_max是余弦周期的半周期长度
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=0)
for epoch in range(100):
train(model, optimizer, criterion)
scheduler.step()
Cosine Annealing with Warm Restarts(带热重启的余弦退火):
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
optimizer = optim.SGD(model.parameters(), lr=0.1)
# T_0是第一次重启的周期,T_mult是重启后周期倍数
scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)
for epoch in range(100):
train(model, optimizer, criterion)
scheduler.step()
7. 预热(Warmup)与余弦退火详解
7.1 Warmup的原理
Warmup是训练初期逐渐增加学习率的策略,主要用于:
- 稳定训练初期:避免大学习率导致的不稳定
- 适应大批量训练:大批量训练需要更大的学习率,但需要渐进式增加
- 防止早期过拟合:小学习率有助于模型先学习简单特征
线性Warmup公式:
lrt=lrbase×twarmup_steps,t<warmup_stepslr_{t} = lr_{base} \times \frac{t}{warmup\_steps}, \quad t < warmup\_stepslrt=lrbase×warmup_stepst,t<warmup_steps
7.2 Warmup + Cosine Annealing组合
这是现代深度学习训练的标准配置,特别是在Transformer和大型模型训练中。
完整学习率曲线:
学习率
^
| /-------------------\
| / \
| / \
| / \
| / \
|/ \
+-----------------------------------> 训练步数
0 warmup end
PyTorch实现(自定义):
import torch
from torch.optim.lr_scheduler import _LRScheduler
class WarmupCosineScheduler(_LRScheduler):
"""
Warmup + Cosine Annealing 学习率调度器
"""
def __init__(self, optimizer, warmup_steps, total_steps, base_lr, last_epoch=-1):
self.warmup_steps = warmup_steps
self.total_steps = total_steps
self.base_lr = base_lr
super(WarmupCosineScheduler, self).__init__(optimizer, last_epoch)
def get_lr(self):
if self.last_epoch < self.warmup_steps:
# Warmup阶段:线性增加
warmup_factor = self.last_epoch / self.warmup_steps
return [self.base_lr * warmup_factor for _ in self.base_lrs]
else:
# Cosine Annealing阶段
progress = (self.last_epoch - self.warmup_steps) / (self.total_steps - self.warmup_steps)
cosine_decay = 0.5 * (1 + torch.cos(torch.tensor(progress * 3.14159265359)))
return [self.base_lr * cosine_decay.item() for _ in self.base_lrs]
# 使用示例
optimizer = optim.AdamW(model.parameters(), lr=1e-4)
scheduler = WarmupCosineScheduler(
optimizer,
warmup_steps=2000,
total_steps=100000,
base_lr=1e-4
)
for step in range(100000):
train_step(model, optimizer)
scheduler.step()
7.3 Hugging Face Transformers中的实现
from transformers import get_cosine_schedule_with_warmup
optimizer = optim.AdamW(model.parameters(), lr=5e-5)
# 创建调度器
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=500, # 预热步数
num_training_steps=10000 # 总训练步数
)
# 训练循环
for step in range(10000):
loss = train_step(model, batch)
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
8. 初始化方法对比实验
下面是一个完整的实验代码,对比不同初始化方法在相同网络结构上的表现:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
# 设置随机种子确保可复现
torch.manual_seed(42)
np.random.seed(42)
# 数据准备
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_dataset = torchvision.datasets.MNIST(
root='./data', train=True, download=True, transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_dataset = torchvision.datasets.MNIST(
root='./data', train=False, download=True, transform=transform
)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)
# 定义测试模型
class TestModel(nn.Module):
def __init__(self):
super(TestModel, self).__init__()
self.flatten = nn.Flatten()
self.fc1 = nn.Linear(28*28, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 128)
self.fc4 = nn.Linear(128, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.relu(self.fc3(x))
x = self.fc4(x)
return x
# 初始化函数定义
def initialize_model(model, init_type):
"""根据类型初始化模型权重"""
for m in model.modules():
if isinstance(m, nn.Linear):
if init_type == 'xavier_uniform':
nn.init.xavier_uniform_(m.weight)
elif init_type == 'xavier_normal':
nn.init.xavier_normal_(m.weight)
elif init_type == 'he_uniform':
nn.init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='relu')
elif init_type == 'he_normal':
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
elif init_type == 'orthogonal':
nn.init.orthogonal_(m.weight, gain=np.sqrt(2))
elif init_type == 'random_normal':
nn.init.normal_(m.weight, mean=0, std=0.01)
elif init_type == 'random_uniform':
nn.init.uniform_(m.weight, -0.1, 0.1)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
return model
def train_epoch(model, loader, criterion, optimizer, device):
"""训练一个epoch"""
model.train()
total_loss = 0
correct = 0
total = 0
for batch_idx, (data, target) in enumerate(loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
total_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
return total_loss / len(loader), 100. * correct / total
def evaluate(model, loader, criterion, device):
"""评估模型"""
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for data, target in loader:
data, target = data.to(device), target.to(device)
output = model(data)
loss = criterion(output, target)
total_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
return total_loss / len(loader), 100. * correct / total
def run_experiment(init_type, epochs=10):
"""运行单个初始化方法的实验"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TestModel().to(device)
model = initialize_model(model, init_type)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
train_losses = []
train_accs = []
test_losses = []
test_accs = []
for epoch in range(epochs):
train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)
test_loss, test_acc = evaluate(model, test_loader, criterion, device)
train_losses.append(train_loss)
train_accs.append(train_acc)
test_losses.append(test_loss)
test_accs.append(test_acc)
print(f"[{init_type}] Epoch {epoch+1}/{epochs}: "
f"Train Loss={train_loss:.4f}, Train Acc={train_acc:.2f}%, "
f"Test Loss={test_loss:.4f}, Test Acc={test_acc:.2f}%")
return {
'train_losses': train_losses,
'train_accs': train_accs,
'test_losses': test_losses,
'test_accs': test_accs
}
# 运行对比实验
if __name__ == "__main__":
init_methods = [
'xavier_uniform',
'xavier_normal',
'he_uniform',
'he_normal',
'orthogonal',
'random_normal'
]
results = {}
for init_method in init_methods:
print(f"\n{'='*50}")
print(f"Testing: {init_method}")
print('='*50)
results[init_method] = run_experiment(init_method, epochs=10)
# 可视化结果
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 训练损失
for init_method in init_methods:
axes[0, 0].plot(results[init_method]['train_losses'], label=init_method)
axes[0, 0].set_title('Training Loss')
axes[0, 0].set_xlabel('Epoch')
axes[0, 0].set_ylabel('Loss')
axes[0, 0].legend()
axes[0, 0].grid(True)
# 训练准确率
for init_method in init_methods:
axes[0, 1].plot(results[init_method]['train_accs'], label=init_method)
axes[0, 1].set_title('Training Accuracy')
axes[0, 1].set_xlabel('Epoch')
axes[0, 1].set_ylabel('Accuracy (%)')
axes[0, 1].legend()
axes[0, 1].grid(True)
# 测试损失
for init_method in init_methods:
axes[1, 0].plot(results[init_method]['test_losses'], label=init_method)
axes[1, 0].set_title('Test Loss')
axes[1, 0].set_xlabel('Epoch')
axes[1, 0].set_ylabel('Loss')
axes[1, 0].legend()
axes[1, 0].grid(True)
# 测试准确率
for init_method in init_methods:
axes[1, 1].plot(results[init_method]['test_accs'], label=init_method)
axes[1, 1].set_title('Test Accuracy')
axes[1, 1].set_xlabel('Epoch')
axes[1, 1].set_ylabel('Accuracy (%)')
axes[1, 1].legend()
axes[1, 1].grid(True)
plt.tight_layout()
plt.savefig('initialization_comparison.png', dpi=150)
plt.show()
# 打印最终对比结果
print("\n" + "="*60)
print("最终测试结果对比")
print("="*60)
print(f"{'初始化方法':<20} {'最终训练准确率':>15} {'最终测试准确率':>15}")
print("-"*60)
for init_method in init_methods:
final_train_acc = results[init_method]['train_accs'][-1]
final_test_acc = results[init_method]['test_accs'][-1]
print(f"{init_method:<20} {final_train_acc:>14.2f}% {final_test_acc:>14.2f}%")
9. 避坑小贴士
9.1 常见错误1:所有层使用相同的初始化
错误做法:
# 错误:所有层使用相同的初始化,不考虑激活函数类型
for m in model.modules()():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight) # 对ReLU层也使用Xavier
正确做法:
# 正确:根据激活函数选择合适的初始化
for m in model.modules():
if isinstance(m, nn.Linear):
if isinstance(m, nn.ReLU):
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
else:
nn.init.xavier_normal_(m.weight)
9.2 常见错误2:忘记初始化偏置项
错误做法:
nn.init.xavier_normal_(layer.weight)
# 忘记初始化bias,可能导致bias初始值过大或过小
正确做法:
nn.init.xavier_normal_(layer.weight)
if layer.bias is not None:
nn.init.constant_(layer.bias, 0) # 偏置通常初始化为0
9.3 常见错误3:学习率调度器step调用位置错误
错误做法:
for epoch in range(epochs):
scheduler.step() # 错误:在epoch开始时调用
for batch in dataloader:
# 训练代码...
正确做法:
for epoch in range(epochs):
for batch in dataloader:
# 训练代码...
optimizer.step()
scheduler.step() # 正确:在每个epoch结束时调用
9.4 常见错误4:Warmup步数设置不当
错误做法:
# Warmup步数过长,浪费训练时间
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=5000, # 对于小数据集来说太长
num_training_steps=10000
)
正确做法:
# Warmup步数通常占总步数的5%-10%
warmup_steps = int(0.1 * total_steps)
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps
)
9.5 常见错误5:Batch Normalization与初始化顺序
错误做法:
# 先应用初始化,再添加BN层,可能导致BN统计不准确
model = initialize_model(model, 'he')
model = add_batch_norm(model)
正确做法:
# 先构建完整模型(包括BN层),再应用初始化
model = build_model_with_bn()
model = initialize_model(model, 'he')
10. 本章小结和知识点回顾
核心知识点
-
权重初始化的重要性
- 防止梯度消失和梯度爆炸
- 加速模型收敛
- 打破对称性,确保神经元学习不同特征
-
Xavier/Glorot初始化
- 适用于tanh、sigmoid等对称激活函数
- 方差公式:Var(w)=2nin+noutVar(w) = \frac{2}{n_{in} + n_{out}}Var(w)=nin+nout2
- 平衡前向和反向传播的方差
-
He/Kaiming初始化
- 专为ReLU激活函数设计
- 方差公式:Var(w)=2ninVar(w) = \frac{2}{n_{in}}Var(w)=nin2
- 现代CNN的标准初始化方法
-
正交初始化与谱初始化
- 正交初始化:保持向量范数,适用于RNN
- 谱初始化:限制Lipschitz常数,适用于GAN
-
学习率调度策略
- StepLR:阶梯式衰减,简单可控
- ExponentialLR:指数衰减,平滑连续
- Cosine Annealing:余弦退火,现代训练标配
- Warmup:训练初期渐进增加学习率,提高稳定性
一句话总结
合适的初始化让网络有一个好的起点,合理的学习率调度让网络稳步走向最优解。
实践建议
- 使用ReLU激活函数时,优先选择He初始化
- 训练大型模型时,使用Warmup + Cosine Annealing组合
- 实验不同初始化方法,选择最适合你任务的策略
- 监控训练过程中的梯度大小,及时调整初始化或学习率
补充:权重初始化领域的研究仍在不断发展。2024年以来,一些研究开始探索基于数据驱动的初始化方法,以及针对Transformer架构的自适应初始化策略。建议读者关注NeurIPS、ICML等顶级会议的最新论文。
本文是《深度学习精通》系列教程的第4章,专注于权重初始化与优化策略。如有疑问或建议,欢迎在评论区留言讨论。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)