深度模型中的优化 --- 完整知识点与代码案例
一、学习和纯优化有什么不同
1.1 核心区别
| 纯优化 | 深度学习中的优化 |
|---|---|
| 直接最小化目标函数 J(θ)J(\theta)J(θ) | 最小化的是训练数据上的期望损失 J(θ)=E(x,y)∼p^dataL(f(x;θ),y)J(\theta) = \mathbb{E}{(x,y)\sim\hat{p}{data}}L(f(x;\\theta), y)J(θ)=E(x,y)∼p^dataL(f(x;θ),y) |
| 到达极小值点即完成 | 目的是降低泛化误差(测试误差),不是训练误差 |
| 只关心优化过程本身 | 过度优化训练集会导致过拟合 |
1.2 关键区别详解
- 过拟合风险:纯优化追求训练损失最小,但深度学习必须在训练损失和泛化能力之间取得平衡
- 间接优化:我们真正关心的是测试集上的性能,训练损失只是一个代理指标
- 提前停止:当验证集误差不再下降时停止训练,即使训练损失仍在下降
1.3 代码案例:纯优化 vs 深度学习优化的区别
python
import torch # 导入PyTorch深度学习框架
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器模块
import numpy as np # 导入数值计算库
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例1:演示学习优化与纯优化的核心区别
# ============================================
torch.manual_seed(42) # 设置随机种子,确保结果可复现
# --- 1. 生成带有噪声的训练数据 ---
# 真实关系:y = 3x + 2
n_train = 100 # 训练样本数量
n_test = 50 # 测试样本数量
X_train = torch.randn(n_train, 1) * 2 # 训练输入,服从均值为0标准差为2的正态分布
noise_train = torch.randn(n_train, 1) * 0.5 # 训练数据的随机噪声
y_train = 3 * X_train + 2 + noise_train # 训练标签,包含噪声
X_test = torch.randn(n_test, 1) * 2 # 测试输入
noise_test = torch.randn(n_test, 1) * 0.5 # 测试数据的随机噪声
y_test = 3 * X_test + 2 + noise_test # 测试标签
# --- 2. 定义一个过于复杂的模型(容易过拟合)---
class OverComplexModel(nn.Module): # 继承nn.Module基类
"""一个参数过多的模型,用于演示过拟合现象"""
def __init__(self):
super(OverComplexModel, self).__init__() # 调用父类构造函数
self.fc1 = nn.Linear(1, 128) # 第一层:1个输入特征 -> 128个神经元
self.fc2 = nn.Linear(128, 128) # 第二层:128 -> 128个神经元
self.fc3 = nn.Linear(128, 64) # 第三层:128 -> 64个神经元
self.fc4 = nn.Linear(64, 1) # 输出层:64 -> 1个输出
self.relu = nn.ReLU() # 激活函数
def forward(self, 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 # 返回预测结果
model = OverComplexModel() # 实例化模型
criterion = nn.MSELoss() # 均方误差损失函数
optimizer = optim.SGD(model.parameters(), lr=0.001) # 随机梯度下降优化器
# --- 3. 训练过程:记录训练损失和测试损失 ---
train_losses = [] # 存储每个epoch的训练损失
test_losses = [] # 存储每个epoch的测试损失
epochs = 500 # 训练轮数
for epoch in range(epochs): # 遍历每个训练轮次
model.train() # 设置模型为训练模式
y_pred_train = model(X_train) # 对训练数据做前向传播
train_loss = criterion(y_pred_train, y_train) # 计算训练损失
optimizer.zero_grad() # 清除之前的梯度
train_loss.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
train_losses.append(train_loss.item()) # 记录训练损失值
# --- 纯优化:只关心训练损失,会一直优化直到极小值 ---
# --- 学习优化:同时监控测试损失,发现过拟合应停止 ---
model.eval() # 设置模型为评估模式
with torch.no_grad(): # 测试时不需要计算梯度
y_pred_test = model(X_test) # 对测试数据做前向传播
test_loss = criterion(y_pred_test, y_test) # 计算测试损失
test_losses.append(test_loss.item()) # 记录测试损失值
# --- 4. 可视化:展示学习优化中过拟合的现象 ---
plt.figure(figsize=(12, 5)) # 创建画布
plt.subplot(1, 2, 1) # 第一个子图
plt.plot(train_losses, label='Training Loss (train loss)', color='blue') # 绘制训练损失曲线
plt.plot(test_losses, label='Test Loss (test loss)', color='red') # 绘制测试损失曲线
plt.xlabel('Epoch') # x轴标签
plt.ylabel('MSE Loss') # y轴标签
plt.title('Pure Optimization vs Learning: Loss Curves') # 标题
plt.legend() # 显示图例
plt.grid(True, alpha=0.3) # 显示网格
# 找到测试损失最低的点(最佳停止点)
best_epoch = np.argmin(test_losses) # 找到最小测试损失对应的epoch
plt.axvline(x=best_epoch, color='green', linestyle='--',
label=f'Best epoch: {best_epoch}') # 标记最佳停止点
plt.legend() # 更新图例
plt.subplot(1, 2, 2) # 第二个子图
plt.scatter(X_train.numpy(), y_train.numpy(), alpha=0.3, label='Train Data') # 绘制训练数据点
with torch.no_grad(): # 不计算梯度
X_sorted, _ = torch.sort(X_train, dim=0) # 对输入排序以便画平滑曲线
y_sorted_pred = model(X_sorted) # 对排序后的输入做预测
plt.plot(X_sorted.numpy(), y_sorted_pred.numpy(), color='red',
linewidth=2, label='Model Prediction') # 绘制模型预测曲线
plt.xlabel('X') # x轴标签
plt.ylabel('y') # y轴标签
plt.title(f'Fitting Result (Epoch {epochs})') # 标题
plt.legend() # 显示图例
plt.grid(True, alpha=0.3) # 显示网格
plt.tight_layout() # 自动调整子图间距
plt.savefig('optimization_vs_learning.png', dpi=150) # 保存图片
plt.show() # 显示图片
print(f"最终训练损失: {train_losses[-1]:.4f}") # 输出最终训练损失
print(f"最终测试损失: {test_losses[-1]:.4f}") # 输出最终测试损失
print(f"最佳停止epoch: {best_epoch},测试损失: {test_losses[best_epoch]:.4f}") # 输出最佳停止信息
1.4 提前停止(Early Stopping)------学习优化的核心策略
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器模块
import numpy as np # 导入数值计算库
import copy # 用于深拷贝模型参数
torch.manual_seed(42) # 设置随机种子
# ============================================
# 案例2:提前停止策略的完整实现
# ============================================
# --- 1. 生成数据 ---
n_samples = 200 # 总样本数
X = torch.randn(n_samples, 1) # 输入特征
y = 2 * X ** 2 + 3 * X + 1 + torch.randn(n_samples, 1) * 0.3 # 二次函数关系 + 噪声
# 划分训练集和验证集(8:2比例)
split = int(0.8 * n_samples) # 80%作为训练集
X_train, X_val = X[:split], X[split:] # 拆分输入数据
y_train, y_val = y[:split], y[split:] # 拆分标签数据
# --- 2. 定义模型 ---
model = nn.Sequential( # 使用Sequential容器按顺序堆叠层
nn.Linear(1, 64), # 输入层 -> 隐藏层1(64个神经元)
nn.ReLU(), # ReLU激活函数
nn.Linear(64, 32), # 隐藏层1 -> 隐藏层2(32个神经元)
nn.ReLU(), # ReLU激活函数
nn.Linear(32, 1) # 隐藏层2 -> 输出层
)
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.Adam(model.parameters(), lr=0.01) # Adam优化器
# --- 3. 提前停止实现 ---
class EarlyStopping: # 提前停止类
"""当验证损失不再下降时提前停止训练
Args:
patience (int): 容忍验证损失不下降的epoch数
min_delta (float): 认为有改善的最小损失变化量
restore_best (bool): 是否在停止时恢复最佳模型参数
"""
def __init__(self, patience=10, min_delta=0.001, restore_best=True):
self.patience = patience # 设置容忍度
self.min_delta = min_delta # 设置最小改善阈值
self.restore_best = restore_best # 是否恢复最佳参数
self.best_loss = None # 记录最佳验证损失
self.counter = 0 # 计数器:记录连续未改善的epoch数
self.best_model_state = None # 保存最佳模型的状态字典
self.should_stop = False # 是否应该停止训练
def __call__(self, val_loss, model): # 使实例可调用
"""在每个epoch结束后调用
Args:
val_loss (float): 当前epoch的验证损失
model (nn.Module): 当前模型
"""
if self.best_loss is None: # 第一个epoch
self.best_loss = val_loss # 初始化最佳损失
self.best_model_state = copy.deepcopy(model.state_dict()) # 保存初始模型参数
elif val_loss < self.best_loss - self.min_delta: # 验证损失有显著改善
self.best_loss = val_loss # 更新最佳损失
self.best_model_state = copy.deepcopy(model.state_dict()) # 保存当前最佳模型参数
self.counter = 0 # 重置计数器
else: # 验证损失没有显著改善
self.counter += 1 # 计数器加1
if self.counter >= self.patience: # 超过容忍度
self.should_stop = True # 设置停止标志
if self.restore_best: # 如果需要恢复最佳参数
model.load_state_dict(self.best_model_state) # 恢复最佳模型参数
print(f"提前停止!恢复最佳模型参数(验证损失: {self.best_loss:.4f})")
# --- 4. 训练循环(使用提前停止)---
early_stopping = EarlyStopping(patience=20, min_delta=0.0001) # 实例化提前停止对象
epochs = 1000 # 最大训练轮数
for epoch in range(epochs): # 遍历每个epoch
# 训练阶段
model.train() # 设置为训练模式
y_pred = model(X_train) # 前向传播
train_loss = criterion(y_pred, y_train) # 计算训练损失
optimizer.zero_grad() # 清除梯度
train_loss.backward() # 反向传播
optimizer.step() # 更新参数
# 验证阶段
model.eval() # 设置为评估模式
with torch.no_grad(): # 验证时不需要梯度
val_pred = model(X_val) # 验证集前向传播
val_loss = criterion(val_pred, y_val) # 计算验证损失
# 每50个epoch打印一次信息
if (epoch + 1) % 50 == 0:
print(f"Epoch [{epoch+1}/{epochs}], "
f"Train Loss: {train_loss.item():.4f}, "
f"Val Loss: {val_loss.item():.4f}")
# 检查是否应该提前停止
early_stopping(val_loss.item(), model) # 传入验证损失和模型
if early_stopping.should_stop: # 如果应该停止
print(f"在第 {epoch+1} 轮提前停止训练")
break # 退出训练循环
print(f"最佳验证损失: {early_stopping.best_loss:.4f}") # 输出最佳验证损失
二、神经网络优化中的挑战
2.1 核心挑战
| 挑战 | 描述 |
|---|---|
| 病态条件(ill-conditioning) | Hessian矩阵条件数大,损失曲面呈椭圆形,梯度方向偏离最优方向 |
| 局部极小值(local minima) | 非凸函数存在多个局部极小值 |
| 鞍点(saddle points) | 某些维度是极小值,另一些维度是极大值,梯度接近零但不是最优 |
| 梯度消失/爆炸 | 深层网络中梯度在反向传播时逐层衰减或增长 |
| 高原和梯度停滞 | 损失函数在大范围内几乎平坦 |
2.2 代码案例:病态条件问题
python
import torch # 导入PyTorch
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例3:病态条件(ill-conditioning)可视化
# ============================================
# --- 1. 创建不同条件数的损失曲面 ---
def quadratic_loss(x, y, A_condition=1.0):
"""计算二次损失函数 f(x,y) = x^2 + A*y^2
Args:
x: 第一个参数
y: 第二个参数
A_condition: 条件数控制参数,越大病态越严重
Returns:
损失值
"""
return x**2 + A_condition * y**2 # 返回二次函数值
def compute_gradient(x, y, A_condition=1.0):
"""计算梯度
Args:
x: 第一个参数
y: 第二个参数
A_condition: 条件数控制参数
Returns:
(df/dx, df/dy) 梯度分量
"""
return 2 * x, 2 * A_condition * y # 返回梯度
# --- 2. 模拟梯度下降在不同条件数下的表现 ---
def simulate_gd(lr, condition, n_steps, start_point):
"""模拟梯度下降过程
Args:
lr: 学习率
condition: 损失曲面的条件数
n_steps: 迭代步数
start_point: 起始点 (x0, y0)
Returns:
轨迹列表 [(x0,y0), (x1,y1), ...]
"""
x, y = start_point # 解包起始坐标
trajectory = [(x, y)] # 存储轨迹
for step in range(n_steps): # 迭代指定次数
grad_x, grad_y = compute_gradient(x, y, condition) # 计算梯度
x = x - lr * grad_x # 更新x:x_new = x - lr * df/dx
y = y - lr * grad_y # 更新y:y_new = y - lr * df/dy
trajectory.append((x, y)) # 记录新位置
return trajectory # 返回完整轨迹
# --- 3. 对比良好条件和病态条件 ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5)) # 创建1行3列子图
conditions = [1, 50, 500] # 三种条件数
lr = 0.01 # 学习率
n_steps = 100 # 迭代步数
start = (5.0, 5.0) # 起始点
for idx, cond in enumerate(conditions): # 遍历每种条件数
ax = axes[idx] # 选择对应子图
# 绘制等高线图
x_range = np.linspace(-6, 6, 200) # x轴范围
y_range = np.linspace(-6, 6, 200) # y轴范围
X_grid, Y_grid = np.meshgrid(x_range, y_range) # 创建网格
Z = quadratic_loss(X_grid, Y_grid, cond) # 计算网格上每个点的损失值
ax.contour(X_grid, Y_grid, Z, levels=20, cmap='viridis', alpha=0.6) # 绘制等高线
# 运行梯度下降
trajectory = simulate_gd(lr, cond, n_steps, start) # 获取轨迹
traj_x = [p[0] for p in trajectory] # 提取x坐标序列
traj_y = [p[1] for p in trajectory] # 提取y坐标序列
ax.plot(traj_x, traj_y, 'r.-', markersize=3, linewidth=1) # 绘制优化轨迹
ax.plot(0, 0, 'k*', markersize=15, label='Global Minimum') # 标记全局最小值
ax.plot(start[0], start[1], 'go', markersize=10, label='Start') # 标记起始点
ax.set_title(f'Condition = {cond}\nSteps: {n_steps}, LR: {lr}') # 设置标题
ax.set_xlabel('x') # x轴标签
ax.set_ylabel('y') # y轴标签
ax.legend() # 显示图例
ax.set_aspect('equal') # 等比例显示
plt.tight_layout() # 调整子图间距
plt.savefig('ill_conditioning.png', dpi=150) # 保存图片
plt.show() # 显示图片
# --- 4. Hessian矩阵条件数计算 ---
def compute_hessian_condition(A_condition):
"""计算二次函数的Hessian矩阵条件数
Args:
A_condition: 条件数参数
Returns:
Hessian矩阵的条件数
"""
H = np.array([[2.0, 0.0], # Hessian矩阵第一行:[d²f/dx², d²f/dxdy]
[0.0, 2.0 * A_condition]]) # Hessian矩阵第二行:[d²f/dydx, d²f/dy²]
eigenvalues = np.linalg.eigvals(H) # 计算特征值
condition_number = np.max(eigenvalues) / np.min(eigenvalues) # 条件数 = 最大特征值/最小特征值
return condition_number, eigenvalues # 返回条件数和特征值
for cond in conditions: # 遍历每种条件数
cond_num, eigvals = compute_hessian_condition(cond) # 计算条件数
print(f"A={cond}: Hessian条件数 = {cond_num:.1f}, 特征值 = {eigvals}") # 输出结果
2.3 代码案例:鞍点问题
python
import torch # 导入PyTorch
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例4:鞍点问题的可视化与梯度行为分析
# ============================================
# --- 1. 定义包含鞍点的函数:f(x,y) = x^2 - y^2 ---
def saddle_function(x, y):
"""鞍点函数:在(0,0)处有鞍点
x方向是凸的(向上弯曲),y方向是凹的(向下弯曲)
Args:
x: 第一个参数
y: 第二个参数
Returns:
函数值
"""
return x**2 - y**2 # 返回鞍点函数值
def saddle_gradient(x, y):
"""鞍点函数的梯度
Args:
x: 第一个参数
y: 第二个参数
Returns:
(df/dx, df/dy) = (2x, -2y)
"""
return 2 * x, -2 * y # 返回梯度
# --- 2. 可视化鞍点的3D曲面和等高线 ---
fig = plt.figure(figsize=(14, 6)) # 创建画布
# 3D曲面图
ax1 = fig.add_subplot(121, projection='3d') # 添加3D子图
x_range = np.linspace(-3, 3, 100) # x范围
y_range = np.linspace(-3, 3, 100) # y范围
X_grid, Y_grid = np.meshgrid(x_range, y_range) # 创建网格
Z = saddle_function(X_grid, Y_grid) # 计算函数值
ax1.plot_surface(X_grid, Y_grid, Z, cmap='RdBu', alpha=0.7) # 绘制3D曲面
ax1.scatter([0], [0], [0], color='red', s=100, label='Saddle Point') # 标记鞍点
ax1.set_xlabel('x') # x轴标签
ax1.set_ylabel('y') # y轴标签
ax1.set_zlabel('f(x,y)') # z轴标签
ax1.set_title('Saddle Point: f(x,y) = x² - y²') # 标题
# 等高线图
ax2 = fig.add_subplot(122) # 添加2D子图
contour = ax2.contourf(X_grid, Y_grid, Z, levels=30, cmap='RdBu') # 绘制填充等高线
plt.colorbar(contour, ax=ax2) # 添加颜色条
ax2.plot(0, 0, 'k+', markersize=20, markeredgewidth=3) # 标记鞍点
ax2.set_title('Contour: Saddle Point at (0,0)') # 标题
ax2.set_xlabel('x') # x轴标签
ax2.set_ylabel('y') # y轴标签
plt.tight_layout() # 调整间距
plt.savefig('saddle_point.png', dpi=150) # 保存图片
plt.show() # 显示图片
# --- 3. 分析鞍点附近的梯度行为 ---
print("=== 鞍点附近梯度分析 ===")
print(f"{'位置':<20} {'梯度大小':<15} {'梯度方向':<20}") # 表头
print("-" * 55)
test_points = [ # 测试不同位置
(0.1, 0.1), # 鞍点附近
(0.01, 0.01), # 更接近鞍点
(0.001, 0.001), # 极接近鞍点
(1.0, 0.0), # x轴上(远离鞍点)
(0.0, 1.0), # y轴上(远离鞍点)
]
for (x, y) in test_points: # 遍历测试点
gx, gy = saddle_gradient(x, y) # 计算梯度
grad_magnitude = np.sqrt(gx**2 + gy**2) # 计算梯度大小
print(f"({x}, {y})".ljust(20), # 打印位置
f"{grad_magnitude:.6f}".ljust(15), # 打印梯度大小
f"({gx:.4f}, {gy:.4f})") # 打印梯度方向
# --- 4. 模拟从鞍点附近出发的梯度下降 ---
print("\n=== 从鞍点附近出发的优化 ===")
x, y = 0.1, 0.1 # 从鞍点附近开始
lr = 0.1 # 学习率
trajectory = [(x, y)] # 存储轨迹
for step in range(50): # 迭代50步
gx, gy = saddle_gradient(x, y) # 计算梯度
x = x - lr * gx # 更新x
y = y - lr * gy # 更新y
trajectory.append((x, y)) # 记录轨迹
if step < 5 or step % 10 == 0: # 前5步和每10步打印
loss = saddle_function(x, y) # 计算当前损失
print(f"Step {step}: x={x:.4f}, y={y:.4f}, loss={loss:.4f}") # 打印信息
print(f"\n注意:在鞍点处,y方向(不稳定方向)会被扰动放大,")
print(f"使得优化器可以逃离鞍点。这是SGD中随机性的一个好处。")
2.4 代码案例:梯度消失与梯度爆炸
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例5:梯度消失与梯度爆炸问题演示
# ============================================
# --- 1. 梯度消失问题演示 ---
class DeepNetwork(nn.Module): # 深层网络类
"""可以灵活控制层数的全连接网络
Args:
n_layers: 隐藏层数量
hidden_size: 每层的神经元数量
activation: 激活函数类型
"""
def __init__(self, n_layers=10, hidden_size=50, activation='sigmoid'):
super(DeepNetwork, self).__init__() # 调用父类构造函数
layers = [] # 存储网络层
layers.append(nn.Linear(1, hidden_size)) # 输入层
# 选择激活函数
if activation == 'sigmoid': # Sigmoid激活
act_fn = nn.Sigmoid() # 值域(0,1),导数最大0.25,易梯度消失
elif activation == 'tanh': # Tanh激活
act_fn = nn.Tanh() # 值域(-1,1),导数最大1.0
elif activation == 'relu': # ReLU激活
act_fn = nn.ReLU() # 正半轴导数为1,缓解梯度消失
for i in range(n_layers - 1): # 添加隐藏层
layers.append(nn.Linear(hidden_size, hidden_size)) # 全连接层
layers.append(act_fn) # 激活函数
layers.append(nn.Linear(hidden_size, 1)) # 输出层
self.network = nn.Sequential(*layers) # 使用Sequential组合所有层
def forward(self, x): # 前向传播
return self.network(x) # 返回网络输出
def analyze_gradient_flow(model, x_input, y_target):
"""分析模型的梯度流动情况
Args:
model: 神经网络模型
x_input: 输入数据
y_target: 目标标签
Returns:
各层梯度范数列表
"""
model.train() # 设置为训练模式
output = model(x_input) # 前向传播
loss = nn.MSELoss()(output, y_target) # 计算损失
loss.backward() # 反向传播
gradient_norms = [] # 存储各层梯度范数
for name, param in model.named_parameters(): # 遍历所有参数
if 'weight' in name and param.grad is not None: # 只关注权重参数
grad_norm = param.grad.norm().item() # 计算梯度的L2范数
gradient_norms.append(grad_norm) # 添加到列表
model.zero_grad() # 清除梯度
return gradient_norms # 返回梯度范数列表
# --- 2. 对比不同激活函数的梯度流动 ---
torch.manual_seed(42) # 设置随机种子
x = torch.randn(32, 1) # 随机输入(batch_size=32)
y = torch.randn(32, 1) # 随机标签
activations = ['sigmoid', 'tanh', 'relu'] # 三种激活函数
n_layers = 15 # 15层深度网络
fig, ax = plt.subplots(figsize=(10, 6)) # 创建画布
for activation in activations: # 遍历每种激活函数
model = DeepNetwork(n_layers=n_layers, hidden_size=50,
activation=activation) # 创建对应网络
grad_norms = analyze_gradient_flow(model, x, y) # 分析梯度流动
ax.plot(range(len(grad_norms)), grad_norms,
marker='o', markersize=4, label=f'{activation}') # 绘制梯度范数曲线
print(f"\n{activation} 激活函数各层梯度范数:") # 输出激活函数名
for i, gn in enumerate(grad_norms): # 遍历各层
print(f" Layer {i+1}: {gn:.8f}") # 输出该层梯度范数
ax.set_xlabel('Layer Index') # x轴标签
ax.set_ylabel('Gradient Norm (L2)') # y轴标签
ax.set_title('Gradient Flow with Different Activations (15 layers)') # 标题
ax.set_yscale('log') # y轴使用对数刻度
ax.legend() # 显示图例
ax.grid(True, alpha=0.3) # 显示网格
plt.tight_layout() # 调整间距
plt.savefig('gradient_vanishing.png', dpi=150) # 保存图片
plt.show() # 显示图片
# --- 3. 梯度爆炸演示 ---
print("\n=== 梯度爆炸演示 ===")
torch.manual_seed(42) # 设置随机种子
x_explode = torch.randn(16, 1) # 输入
y_explode = torch.randn(16, 1) # 标签
# 使用过大的初始权重会导致梯度爆炸
class ExplodingNetwork(nn.Module): # 容易梯度爆炸的网络
def __init__(self, n_layers=10, hidden_size=20):
super(ExplodingNetwork, self).__init__() # 调用父类构造函数
layers = [] # 存储层
layers.append(nn.Linear(1, hidden_size)) # 输入层
for _ in range(n_layers - 1): # 添加隐藏层
linear = nn.Linear(hidden_size, hidden_size) # 全连接层
# 使用过大的权重初始化 ------ 导致梯度爆炸
nn.init.normal_(linear.weight, mean=0, std=2.0) # 标准差设为2.0
layers.append(linear) # 添加到层列表
layers.append(nn.ReLU()) # ReLU激活
layers.append(nn.Linear(hidden_size, 1)) # 输出层
self.network = nn.Sequential(*layers) # 组合所有层
def forward(self, x): # 前向传播
return self.network(x) # 返回输出
model_explode = ExplodingNetwork(n_layers=10) # 创建梯度爆炸网络
grad_norms = analyze_gradient_flow(model_explode, x_explode, y_explode) # 分析梯度
print("梯度爆炸网络各层梯度范数:") # 输出标题
for i, gn in enumerate(grad_norms): # 遍历各层
status = "⚠️ 爆炸!" if gn > 100 else "✓ 正常" # 判断是否梯度爆炸
print(f" Layer {i+1}: {gn:.2f} {status}") # 输出梯度范数和状态
三、基本算法
3.1 随机梯度下降(SGD)
θt+1=θt−η⋅gt\theta_{t+1} = \theta_t - \eta \cdot g_tθt+1=θt−η⋅gt
其中 gt=∇θJ(θt;x(i);y(i))g_t = \nabla_\theta J(\theta_t; x^{(i)}; y^{(i)})gt=∇θJ(θt;x(i);y(i)) 是单个样本(或mini-batch)的梯度。
3.2 动量法(Momentum)
vt=αvt−1+ηgtv_t = \alpha v_{t-1} + \eta g_tvt=αvt−1+ηgt
θt+1=θt−vt\theta_{t+1} = \theta_t - v_tθt+1=θt−vt
3.3 Nesterov 动量
vt=αvt−1+η∇θJ(θt−αvt−1)v_t = \alpha v_{t-1} + \eta \nabla_\theta J(\theta_t - \alpha v_{t-1})vt=αvt−1+η∇θJ(θt−αvt−1)
θt+1=θt−vt\theta_{t+1} = \theta_t - v_tθt+1=θt−vt
3.4 代码案例
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器模块
from torch.utils.data import DataLoader, TensorDataset # 数据加载工具
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例6:手动实现SGD、Momentum、Nesterov
# ============================================
# --- 1. 手动实现各种优化器(从零开始,理解原理)---
class ManualSGD: # 手动实现SGD
"""随机梯度下降优化器
更新规则:θ = θ - lr * gradient
Args:
params: 需要优化的参数列表
lr: 学习率
"""
def __init__(self, params, lr=0.01):
self.params = list(params) # 将参数转为列表存储
self.lr = lr # 存储学习率
def step(self): # 执行一步参数更新
"""对每个参数执行 θ = θ - lr * grad"""
with torch.no_grad(): # 更新操作不需要计算梯度
for param in self.params: # 遍历每个参数
if param.grad is not None: # 如果该参数有梯度
param -= self.lr * param.grad # 执行SGD更新
def zero_grad(self): # 清除所有参数的梯度
for param in self.params: # 遍历每个参数
if param.grad is not None: # 如果梯度存在
param.grad.zero_() # 将梯度置零
class ManualMomentum: # 手动实现动量法
"""带动量的SGD优化器
更新规则:
v_t = momentum * v_{t-1} + lr * gradient
θ = θ - v_t
Args:
params: 需要优化的参数列表
lr: 学习率
momentum: 动量系数(通常设为0.9)
"""
def __init__(self, params, lr=0.01, momentum=0.9):
self.params = list(params) # 将参数转为列表
self.lr = lr # 存储学习率
self.momentum = momentum # 存储动量系数
# 为每个参数初始化速度缓冲区(初始为零)
self.velocities = [torch.zeros_like(p) for p in self.params] # 与参数同形状的零张量
def step(self): # 执行一步参数更新
"""对每个参数执行动量更新"""
with torch.no_grad(): # 不需要梯度计算
for i, param in enumerate(self.params): # 遍历每个参数
if param.grad is not None: # 如果有梯度
# v_t = momentum * v_{t-1} + lr * gradient(动量更新规则)
self.velocities[i] = self.momentum * self.velocities[i] + self.lr * param.grad
param -= self.velocities[i] # θ = θ - v_t(参数更新)
def zero_grad(self): # 清除梯度
for param in self.params: # 遍历每个参数
if param.grad is not None: # 如果梯度存在
param.grad.zero_() # 置零
class ManualNesterov: # 手动实现Nesterov动量
"""Nesterov加速梯度优化器
更新规则(look-ahead版本):
v_t = momentum * v_{t-1} + lr * gradient(θ - momentum * v_{t-1})
θ = θ - v_t
Args:
params: 参数列表
lr: 学习率
momentum: 动量系数
"""
def __init__(self, params, lr=0.01, momentum=0.9):
self.params = list(params) # 参数列表
self.lr = lr # 学习率
self.momentum = momentum # 动量系数
self.velocities = [torch.zeros_like(p) for p in self.params] # 速度缓冲区
def step(self):
"""执行Nesterov动量更新"""
with torch.no_grad():
for i, param in enumerate(self.params):
if param.grad is not None:
# 先应用"前瞻"步骤:临时更新参数
param.add_(self.momentum * self.velocities[i]) # θ_temp = θ + momentum * v
# 然后在前瞻位置计算梯度更新
self.velocities[i] = self.momentum * self.velocities[i] + self.lr * param.grad
param -= self.velocities[i] # 最终更新
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
# --- 2. 用Rosenbrock函数对比三种优化器 ---
def rosenbrock(x, y):
"""Rosenbrock函数(香蕉函数),经典优化测试函数
f(x,y) = (1-x)^2 + 100*(y-x^2)^2
全局最小值在 (1, 1),值为0
Args:
x: 第一个参数
y: 第二个参数
Returns:
函数值
"""
return (1 - x)**2 + 100 * (y - x**2)**2 # 返回Rosenbrock函数值
def rosenbrock_grad(x, y):
"""Rosenbrock函数的梯度
df/dx = -2(1-x) + 100 * 2(y-x^2) * (-2x) = -2(1-x) - 400x(y-x^2)
df/dy = 100 * 2(y-x^2) = 200(y-x^2)
Args:
x: 第一个参数
y: 第二个参数
Returns:
(df/dx, df/dy)
"""
dx = -2 * (1 - x) - 400 * x * (y - x**2) # 对x的偏导数
dy = 200 * (y - x**2) # 对y的偏导数
return dx, dy # 返回梯度
# --- 3. 在Rosenbrock上运行三种优化器 ---
def optimize_rosenbrock(optimizer_class, lr, momentum=0.9, n_steps=5000, start=(-2, 2)):
"""在Rosenbrock函数上运行优化
Args:
optimizer_class: 优化器类名
lr: 学习率
momentum: 动量系数
n_steps: 迭代步数
start: 起始点
Returns:
优化轨迹
"""
# 创建可训练参数(从起始点开始)
x_param = torch.tensor([start[0]], dtype=torch.float64, requires_grad=True) # x参数
y_param = torch.tensor([start[1]], dtype=torch.float64, requires_grad=True) # y参数
# 选择优化器
if optimizer_class == 'sgd':
optimizer = ManualSGD([x_param, y_param], lr=lr) # SGD优化器
elif optimizer_class == 'momentum':
optimizer = ManualMomentum([x_param, y_param], lr=lr, momentum=momentum) # 动量优化器
elif optimizer_class == 'nesterov':
optimizer = ManualNesterov([x_param, y_param], lr=lr, momentum=momentum) # Nesterov优化器
trajectory = [(x_param.item(), y_param.item())] # 记录初始位置
for step in range(n_steps): # 迭代
loss = rosenbrock(x_param, y_param) # 计算损失
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
optimizer.zero_grad() # 清除梯度
trajectory.append((x_param.item(), y_param.item())) # 记录当前位置
return trajectory # 返回轨迹
# 运行优化(使用较小的学习率以确保收敛)
trajectory_sgd = optimize_rosenbrock('sgd', lr=0.00005, n_steps=10000) # SGD轨迹
trajectory_momentum = optimize_rosenbrock('momentum', lr=0.00005,
momentum=0.9, n_steps=10000) # 动量轨迹
trajectory_nesterov = optimize_rosenbrock('nesterov', lr=0.00005,
momentum=0.9, n_steps=10000) # Nesterov轨迹
# --- 4. 可视化对比 ---
fig, ax = plt.subplots(figsize=(10, 8)) # 创建画布
# 绘制Rosenbrock等高线
x_range = np.linspace(-2.5, 2.5, 300) # x范围
y_range = np.linspace(-1, 3, 300) # y范围
X_grid, Y_grid = np.meshgrid(x_range, y_range) # 创建网格
Z = rosenbrock(X_grid, Y_grid) # 计算函数值
ax.contour(X_grid, Y_grid, Z, levels=np.logspace(-1, 3.5, 30), # 绘制对数间隔等高线
cmap='coolwarm', alpha=0.5)
# 绘制轨迹
for traj, name, color in [ # 遍历每种优化器的轨迹
(trajectory_sgd, 'SGD', 'blue'),
(trajectory_momentum, 'Momentum', 'red'),
(trajectory_nesterov, 'Nesterov', 'green')
]:
tx = [p[0] for p in traj] # 提取x坐标
ty = [p[1] for p in traj] # 提取y坐标
# 只绘制部分点以避免过于密集
step_size = max(1, len(traj) // 100) # 计算跳步
ax.plot(tx[::step_size], ty[::step_size], '-', color=color,
label=f'{name}', linewidth=1.5, alpha=0.8) # 绘制轨迹
ax.plot(tx[0], ty[0], 'o', color=color, markersize=8) # 标记起点
ax.plot(tx[-1], ty[-1], '*', color=color, markersize=15) # 标记终点
ax.plot(1, 1, 'k*', markersize=20, label='Global Minimum (1,1)') # 标记全局最小值
ax.set_xlabel('x') # x轴标签
ax.set_ylabel('y') # y轴标签
ax.set_title('SGD vs Momentum vs Nesterov on Rosenbrock Function') # 标题
ax.legend() # 图例
plt.tight_layout() # 调整间距
plt.savefig('basic_optimizers.png', dpi=150) # 保存图片
plt.show() # 显示图片
# 打印最终结果
print("=== 最终结果 ===")
for name, traj in [('SGD', trajectory_sgd), # 遍历轨迹
('Momentum', trajectory_momentum),
('Nesterov', trajectory_nesterov)]:
final = traj[-1] # 最终位置
final_loss = rosenbrock(torch.tensor(final[0]), torch.tensor(final[1])) # 最终损失
print(f"{name:>10}: 最终位置 = ({final[0]:.4f}, {final[1]:.4f}), "
f"损失 = {final_loss:.6f}") # 输出结果
3.5 使用PyTorch内置优化器
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器模块
import matplotlib.pyplot as plt # 绘图库
# ============================================
# 案例7:PyTorch内置优化器对比(在真实神经网络上)
# ============================================
torch.manual_seed(42) # 设置随机种子
# --- 1. 生成数据集 ---
n = 500 # 样本数量
X = torch.randn(n, 2) # 二维输入特征
# 非线性分类边界:根据是否在圆内分类
y = (X[:, 0]**2 + X[:, 1]**2 < 1.5).float().unsqueeze(1) # 圆内为1,圆外为0
# --- 2. 定义统一的网络结构 ---
def create_model():
"""创建一个用于二分类的全连接网络
Returns:
初始化好的网络模型
"""
model = nn.Sequential( # 使用Sequential容器
nn.Linear(2, 32), # 输入层:2 -> 32
nn.ReLU(), # ReLU激活
nn.Linear(32, 16), # 隐藏层:32 -> 16
nn.ReLU(), # ReLU激活
nn.Linear(16, 1), # 输出层:16 -> 1
nn.Sigmoid() # Sigmoid激活(输出概率)
)
return model # 返回模型
# --- 3. 训练函数 ---
def train_with_optimizer(optimizer_name, model, X, y, lr=0.01, epochs=200):
"""使用指定优化器训练模型
Args:
optimizer_name: 优化器名称(字符串)
model: 神经网络模型
X: 输入数据
y: 标签
lr: 学习率
epochs: 训练轮数
Returns:
损失历史列表
"""
criterion = nn.BCELoss() # 二元交叉熵损失
# 根据名称选择优化器
if optimizer_name == 'SGD':
optimizer = optim.SGD(model.parameters(), lr=lr) # 标准SGD
elif optimizer_name == 'SGD+Momentum':
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9) # SGD + 动量
elif optimizer_name == 'SGD+Nesterov':
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9,
nesterov=True) # SGD + Nesterov动量
elif optimizer_name == 'Adagrad':
optimizer = optim.Adagrad(model.parameters(), lr=lr) # Adagrad
elif optimizer_name == 'RMSProp':
optimizer = optim.RMSprop(model.parameters(), lr=lr) # RMSProp
elif optimizer_name == 'Adam':
optimizer = optim.Adam(model.parameters(), lr=lr) # Adam
else:
raise ValueError(f"Unknown optimizer: {optimizer_name}") # 未知优化器报错
loss_history = [] # 存储损失历史
for epoch in range(epochs): # 遍历每个epoch
y_pred = model(X) # 前向传播
loss = criterion(y_pred, y) # 计算损失
optimizer.zero_grad() # 清除梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
loss_history.append(loss.item()) # 记录损失值
return loss_history # 返回损失历史
# --- 4. 对比不同优化器 ---
optimizers_to_test = [ # 待测试的优化器列表
'SGD',
'SGD+Momentum',
'SGD+Nesterov',
'Adagrad',
'RMSProp',
'Adam'
]
fig, ax = plt.subplots(figsize=(12, 7)) # 创建画布
colors = ['blue', 'cyan', 'green', 'orange', 'purple', 'red'] # 颜色列表
for opt_name, color in zip(optimizers_to_test, colors): # 遍历每个优化器
torch.manual_seed(42) # 重置种子,确保相同初始化
model = create_model() # 创建新模型
losses = train_with_optimizer(opt_name, model, X, y, lr=0.01, epochs=200) # 训练
ax.plot(losses, label=opt_name, color=color, linewidth=1.5) # 绘制损失曲线
print(f"{opt_name:>15}: 最终损失 = {losses[-1]:.4f}") # 输出最终损失
ax.set_xlabel('Epoch') # x轴标签
ax.set_ylabel('Binary Cross-Entropy Loss') # y轴标签
ax.set_title('Comparison of Different Optimizers') # 标题
ax.legend() # 图例
ax.grid(True, alpha=0.3) # 网格
plt.tight_layout() # 调整间距
plt.savefig('optimizer_comparison.png', dpi=150) # 保存图片
plt.show() # 显示图片
四、参数初始化策略
4.1 核心原则
好的初始化应保持各层激活值和梯度的方差一致(既不消失也不爆炸)。
4.2 主流初始化方法
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Xavier (Glorot) | W∼U−6nin+nout,6nin+noutW \sim U-\\sqrt{\\frac{6}{n_{in}+n_{out}}}, \\sqrt{\\frac{6}{n_{in}+n_{out}}}W∼U−nin+nout6 ,nin+nout6 | Sigmoid / Tanh |
| Kaiming (He) | W∼N(0,2nin)W \sim N(0, \frac{2}{n_{in}})W∼N(0,nin2) | ReLU |
| 正交初始化 | WWW 是正交矩阵 | RNN |
4.3 代码案例
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例8:各种参数初始化策略的实现与对比
# ============================================
torch.manual_seed(42) # 设置随机种子
# --- 1. 手动实现各种初始化方法 ---
def manual_zero_init(tensor):
"""零初始化:所有参数设为0
问题:对称性问题------同一层所有神经元学到相同的特征
不推荐用于隐藏层权重,可用于偏置初始化
Args:
tensor: 需要初始化的张量
"""
with torch.no_grad(): # 不追踪梯度
tensor.fill_(0) # 全部填0
def manual_random_init(tensor, std=1.0):
"""随机初始化:从正态分布中采样
问题:标准差过大时会导致梯度爆炸/激活值饱和
Args:
tensor: 需要初始化的张量
std: 正态分布的标准差
"""
with torch.no_grad():
tensor.normal_(mean=0, std=std) # 从N(0, std)中采样
def manual_xavier_init(tensor, fan_in, fan_out):
"""Xavier (Glorot) 初始化
目的:保持前向传播时各层激活值方差一致
公式:W ~ U[-limit, limit], limit = sqrt(6 / (fan_in + fan_out))
推导:假设激活函数在0附近近似线性(如tanh)
要求 Var(W) = 2 / (fan_in + fan_out)
Args:
tensor: 需要初始化的张量
fan_in: 输入维度
fan_out: 输出维度
"""
limit = np.sqrt(6.0 / (fan_in + fan_out)) # 计算均匀分布的边界
with torch.no_grad():
tensor.uniform_(-limit, limit) # 从均匀分布中采样
def manual_kaiming_init(tensor, fan_in):
"""Kaiming (He) 初始化
目的:考虑ReLU激活函数会将一半神经元置零的特性
公式:W ~ N(0, std), std = sqrt(2 / fan_in)
推导:ReLU后方差变为原来的一半,所以需要2倍方差补偿
Args:
tensor: 需要初始化的张量
fan_in: 输入维度
"""
std = np.sqrt(2.0 / fan_in) # 计算标准差
with torch.no_grad():
tensor.normal_(mean=0, std=std) # 从正态分布采样
def manual_orthogonal_init(tensor, gain=1.0):
"""正交初始化
使用SVD分解创建正交矩阵
适合RNN,可以保持梯度在时间步上的稳定性
Args:
tensor: 需要初始化的张量(必须是2D的)
gain: 缩放增益
"""
with torch.no_grad():
# 从标准正态分布采样
flat = tensor.new_empty(tensor.size(0), tensor.size(1)).normal_() # 随机矩阵
# SVD分解
u, s, v = torch.svd(flat) # 奇异值分解
# 取U或V作为正交矩阵
if tensor.size(0) <= tensor.size(1]: # 如果行数 <= 列数
tensor.copy_(u * gain) # 使用U矩阵
else:
tensor.copy_(v.t() * gain) # 使用V^T矩阵
# --- 2. 使用PyTorch内置初始化方法对比 ---
# 创建一个用于演示的简单网络
class InitDemoNet(nn.Module): # 演示网络
"""用于展示不同初始化效果的多层网络
Args:
init_method: 初始化方法名称
"""
def __init__(self, init_method='default'):
super(InitDemoNet, self).__init__() # 调用父类构造函数
self.fc1 = nn.Linear(100, 256) # 第一层:100 -> 256
self.fc2 = nn.Linear(256, 256) # 第二层:256 -> 256
self.fc3 = nn.Linear(256, 256) # 第三层:256 -> 256
self.fc4 = nn.Linear(256, 10) # 输出层:256 -> 10
self.relu = nn.ReLU() # ReLU激活函数
self._initialize_weights(init_method) # 执行初始化
def _initialize_weights(self, method):
"""根据指定方法初始化所有层的权重
Args:
method: 初始化方法名称
"""
for m in self.modules(): # 遍历所有模块
if isinstance(m, nn.Linear): # 如果是线性层
if method == 'zeros': # 零初始化
nn.init.zeros_(m.weight) # 权重全设为0
nn.init.zeros_(m.bias) # 偏置全设为0
elif method == 'random_small': # 小方差随机初始化
nn.init.normal_(m.weight, mean=0, std=0.01) # N(0, 0.01)
nn.init.zeros_(m.bias) # 偏置设为0
elif method == 'random_large': # 大方差随机初始化
nn.init.normal_(m.weight, mean=0, std=1.0) # N(0, 1.0) ------ 可能导致问题
nn.init.zeros_(m.bias) # 偏置设为0
elif method == 'xavier': # Xavier初始化
nn.init.xavier_uniform_(m.weight) # Xavier均匀分布初始化
nn.init.zeros_(m.bias) # 偏置设为0
elif method == 'kaiming': # Kaiming初始化
nn.init.kaiming_normal_(m.weight, mode='fan_in',
nonlinearity='relu') # Kaiming正态初始化
nn.init.zeros_(m.bias) # 偏置设为0
elif method == 'orthogonal': # 正交初始化
nn.init.orthogonal_(m.weight, gain=nn.init.calculate_gain('relu')) # 正交初始化
nn.init.zeros_(m.bias) # 偏置设为0
elif method == 'default': # PyTorch默认初始化
pass # 使用PyTorch的默认初始化(Kaiming uniform)
# --- 3. 分析不同初始化的激活值分布 ---
def analyze_activations(model, x_input):
"""分析网络各层激活值的分布
Args:
model: 神经网络模型
x_input: 输入数据
Returns:
各层激活值列表
"""
activations = [] # 存储各层激活值
x = x_input # 当前输入
# 逐层前向传播并记录激活值
x = model.relu(model.fc1(x)) # 第一层 + ReLU
activations.append(x.detach().numpy().flatten()) # 记录激活值
x = model.relu(model.fc2(x)) # 第二层 + ReLU
activations.append(x.detach().numpy().flatten()) # 记录激活值
x = model.relu(model.fc3(x)) # 第三层 + ReLU
activations.append(x.detach().numpy().flatten()) # 记录激活值
return activations # 返回激活值列表
# --- 4. 可视化对比 ---
methods = ['zeros', 'random_small', 'random_large', 'xavier', 'kaiming', 'orthogonal'] # 初始化方法
x_input = torch.randn(1000, 100) # 1000个样本,每个100维
fig, axes = plt.subplots(2, 3, figsize=(18, 10)) # 2行3列子图
for idx, method in enumerate(methods): # 遍历每种方法
ax = axes[idx // 3, idx % 3] # 选择对应子图
torch.manual_seed(42) # 重置种子
model = InitDemoNet(init_method=method) # 创建使用对应初始化的模型
activations = analyze_activations(model, x_input) # 分析激活值
for layer_idx, act in enumerate(activations): # 遍历各层
ax.hist(act, bins=50, alpha=0.5, label=f'Layer {layer_idx+1}', density=True) # 绘制直方图
ax.set_title(f'Init: {method}') # 设置标题
ax.set_xlabel('Activation Value') # x轴标签
ax.set_ylabel('Density') # y轴标签
ax.legend() # 图例
# 计算并显示激活值统计信息
for layer_idx, act in enumerate(activations):
print(f"{method:>15} - Layer {layer_idx+1}: mean={act.mean():.4f}, "
f"std={act.std():.4f}, dead_relu={np.mean(act == 0)*100:.1f}%") # 输出统计
plt.suptitle('Effect of Weight Initialization on Activation Distributions', fontsize=14) # 总标题
plt.tight_layout() # 调整间距
plt.savefig('initialization_effects.png', dpi=150) # 保存图片
plt.show() # 显示图片
4.4 偏置初始化
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
# ============================================
# 案例9:偏置初始化策略
# ============================================
class CustomInitNet(nn.Module): # 自定义初始化的网络
"""演示偏置初始化策略的网络"""
def __init__(self):
super(CustomInitNet, self).__init__() # 调用父类构造函数
# 定义层
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) # 卷积层
self.bn1 = nn.BatchNorm2d(16) # 批归一化层
self.fc1 = nn.Linear(16 * 8 * 8, 128) # 全连接层
self.fc2 = nn.Linear(128, 10) # 输出层
self._custom_init() # 执行自定义初始化
def _custom_init(self):
"""演示各种偏置初始化策略"""
# 策略1:权重用Kaiming,偏置设为零
nn.init.kaiming_normal_(self.conv1.weight, mode='fan_out',
nonlinearity='relu') # Kaiming初始化卷积权重
nn.init.zeros_(self.conv1.bias) # 卷积偏置设为0
# 策略2:BatchNorm层的weight设为1,bias设为0
nn.init.ones_(self.bn1.weight) # BN的缩放参数设为1
nn.init.zeros_(self.bn1.bias) # BN的偏移参数设为0
# 策略3:全连接层使用Xavier初始化
nn.init.xavier_uniform_(self.fc1.weight) # Xavier均匀初始化
nn.init.zeros_(self.fc1.bias) # 偏置设为0
# 策略4:输出层使用较小的初始化(减少初始预测的置信度)
nn.init.xavier_uniform_(self.fc2.weight, gain=0.1) # 较小增益的Xavier初始化
nn.init.zeros_(self.fc2.bias) # 偏置设为0
def forward(self, x): # 前向传播
x = torch.relu(self.bn1(self.conv1(x))) # 卷积 + BN + ReLU
x = x.view(x.size(0), -1) # 展平
x = torch.relu(self.fc1(x)) # 全连接 + ReLU
x = self.fc2(x) # 输出层
return x # 返回输出
# 验证初始化效果
model = CustomInitNet() # 创建模型
print("=== 各层参数初始化统计 ===")
for name, param in model.named_parameters(): # 遍历所有参数
print(f"{name:>30}: shape={list(param.shape)}, "
f"mean={param.data.mean():.4f}, "
f"std={param.data.std():.4f}") # 输出参数统计信息
五、自适应学习率算法
5.1 算法汇总
| 算法 | 更新规则 | 特点 |
|---|---|---|
| AdaGrad | θt+1=θt−ηGt+ϵ⊙gt\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot g_tθt+1=θt−Gt+ϵ η⊙gt | 学习率单调递减,后期可能过小 |
| RMSProp | vt=βvt−1+(1−β)gt2v_t = \beta v_{t-1} + (1-\beta) g_t^2vt=βvt−1+(1−β)gt2,θt+1=θt−ηvt+ϵgt\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{v_t + \epsilon}} g_tθt+1=θt−vt+ϵ ηgt | 指数加权平均,解决AdaGrad学习率衰减问题 |
| Adam | 结合Momentum和RMSProp | 自适应学习率 + 动量 |
5.2 代码案例:从零实现 AdaGrad
python
import torch # 导入PyTorch
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 导入绘图库
# ============================================
# 案例10:从零实现AdaGrad优化器
# ============================================
class ManualAdaGrad:
"""手动实现AdaGrad优化器
核心思想:对每个参数使用不同的学习率,
频繁更新的参数使用较小学习率,稀疏特征使用较大学习率
更新规则:
G_t = G_{t-1} + g_t^2 (历史梯度平方和累加)
θ_t = θ_{t-1} - lr / sqrt(G_t + eps) * g_t
Args:
params: 参数列表
lr: 学习率(默认0.01)
eps: 数值稳定性项(默认1e-8)
"""
def __init__(self, params, lr=0.01, eps=1e-8):
self.params = list(params) # 参数列表
self.lr = lr # 学习率
self.eps = eps # 数值稳定性小常数
# 为每个参数创建梯度平方和累加器(初始为零)
self.sum_squared_grads = [torch.zeros_like(p) for p in self.params] # G_t
def step(self): # 执行一步参数更新
"""AdaGrad参数更新"""
with torch.no_grad(): # 不需要梯度计算
for i, param in enumerate(self.params): # 遍历每个参数
if param.grad is not None: # 如果有梯度
# 累加梯度的平方:G_t = G_{t-1} + g_t^2
self.sum_squared_grads[i] += param.grad ** 2
# 计算自适应学习率:lr / sqrt(G_t + eps)
adjusted_lr = self.lr / (torch.sqrt(self.sum_squared_grads[i]) + self.eps)
# 更新参数:θ = θ - adjusted_lr * g_t
param -= adjusted_lr * param.grad
def zero_grad(self): # 清除梯度
for param in self.params:
if param.grad is not None:
param.grad.zero_()
# --- 验证AdaGrad ---
torch.manual_seed(42) # 设置随机种子
# 创建参数
x = torch.tensor([5.0], requires_grad=True) # 参数x
y = torch.tensor([5.0], requires_grad=True) # 参数y
optimizer = ManualAdaGrad([x, y], lr=1.0) # 使用较大初始学习率
trajectory = [(x.item(), y.item())] # 记录轨迹
losses = [] # 记录损失
for step in range(200): # 迭代200步
loss = x**2 + 10 * y**2 # 椭圆形损失函数
loss.backward() # 反向传播
optimizer.step() # 更新参数
optimizer.zero_grad() # 清除梯度
trajectory.append((x.item(), y.item())) # 记录轨迹
losses.append(loss.item()) # 记录损失
print(f"起始点: (5.0, 5.0)") # 输出起始点
print(f"终点: ({x.item():.4f}, {y.item():.4f})") # 输出终点
print(f"最终损失: {losses[-1]:.6f}") # 输出最终损失
# 可视化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # 创建子图
# 等高线和轨迹
x_range = np.linspace(-6, 6, 200)
y_range = np.linspace(-6, 6, 200)
X_grid, Y_grid = np.meshgrid(x_range, y_range)
Z = X_grid**2 + 10 * Y_grid**2
ax1.contour(X_grid, Y_grid, Z, levels=20, cmap='coolwarm', alpha=0.5)
tx = [p[0] for p in trajectory]
ty = [p[1] for p in trajectory]
ax1.plot(tx, ty, 'g.-', markersize=2, linewidth=0.8)
ax1.plot(0, 0, 'r*', markersize=15)
ax1.set_title('AdaGrad Trajectory')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
# 损失曲线
ax2.plot(losses, color='blue')
ax2.set_title('AdaGrad Loss Curve')
ax2.set_xlabel('Step')
ax2.set_ylabel('Loss')
ax2.set_yscale('log')
plt.tight_layout()
plt.savefig('adagrad_demo.png', dpi=150)
plt.show()
5.3 代码案例:从零实现 RMSProp
python
import torch # 导入PyTorch
import numpy as np # 导入NumPy
# ============================================
# 案例11:从零实现RMSProp优化器
# ============================================
class ManualRMSProp:
"""手动实现RMSProp优化器
核心改进:使用指数移动平均代替AdaGrad的简单累加,
解决学习率单调递减的问题
更新规则:
E[g²]_t = β * E[g²]_{t-1} + (1-β) * g_t² (指数移动平均)
θ_t = θ_{t-1} - lr / sqrt(E[g²]_t + eps) * g_t
Args:
params: 参数列表
lr: 学习率(默认0.001)
alpha: 衰减率β(默认0.99)
eps: 数值稳定性项(默认1e-8)
"""
def __init__(self, params, lr=0.001, alpha=0.99, eps=1e-8):
self.params = list(params) # 参数列表
self.lr = lr # 学习率
self.alpha = alpha # 衰减率β
self.eps = eps # 数值稳定性项
# 为每个参数创建梯度平方的指数移动平均(初始为零)
self.avg_squared_grads = [torch.zeros_like(p) for p in self.params] # E[g²]
def step(self): # 参数更新
"""RMSProp参数更新"""
with torch.no_grad():
for i, param in enumerate(self.params): # 遍历参数
if param.grad is not None: # 如果有梯度
# E[g²]_t = α * E[g²]_{t-1} + (1-α) * g_t²
self.avg_squared_grads[i] = (
self.alpha * self.avg_squared_grads[i] + # 指数衰减项
(1 - self.alpha) * param.grad ** 2 # 当前梯度平方项
)
# 自适应学习率更新
adjusted_lr = self.lr / (
torch.sqrt(self.avg_squared_grads[i]) + self.eps # 分母
)
param -= adjusted_lr * param.grad # 参数更新
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
# --- 验证RMSProp ---
torch.manual_seed(42)
x = torch.tensor([5.0], requires_grad=True)
y = torch.tensor([5.0], requires_grad=True)
optimizer = ManualRMSProp([x, y], lr=0.1, alpha=0.9) # RMSProp优化器
print("RMSProp优化过程:")
for step in range(100):
loss = x**2 + 10 * y**2 # 椭圆损失函数
loss.backward()
optimizer.step()
optimizer.zero_grad()
if step % 20 == 0:
print(f" Step {step}: x={x.item():.4f}, y={y.item():.4f}, loss={loss.item():.4f}")
5.4 代码案例:从零实现 Adam
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
import numpy as np # 导入NumPy
# ============================================
# 案例12:从零实现Adam优化器(最核心)
# ============================================
class ManualAdam:
"""手动实现Adam优化器 (Adaptive Moment Estimation)
Adam = Momentum(一阶矩估计)+ RMSProp(二阶矩估计)+ 偏差修正
更新规则:
m_t = β₁ * m_{t-1} + (1 - β₁) * g_t (一阶矩:梯度的指数移动平均)
v_t = β₂ * v_{t-1} + (1 - β₂) * g_t² (二阶矩:梯度平方的指数移动平均)
m̂_t = m_t / (1 - β₁ᵗ) (一阶矩偏差修正)
v̂_t = v_t / (1 - β₂ᵗ) (二阶矩偏差修正)
θ_t = θ_{t-1} - lr * m̂_t / (√v̂_t + ε) (参数更新)
Args:
params: 参数列表
lr: 学习率(默认0.001)
betas: 动量系数元组 (β₁, β₂)(默认 (0.9, 0.999))
eps: 数值稳定性项(默认1e-8)
weight_decay: L2正则化系数(默认0)
"""
def __init__(self, params, lr=0.001, betas=(0.9, 0.999),
eps=1e-8, weight_decay=0):
self.params = list(params) # 参数列表
self.lr = lr # 学习率
self.beta1, self.beta2 = betas # 动量系数
self.eps = eps # 数值稳定性项
self.weight_decay = weight_decay # 权重衰减(L2正则化)
self.t = 0 # 时间步计数器(用于偏差修正)
# 为每个参数初始化一阶矩和二阶矩
self.m = [torch.zeros_like(p) for p in self.params] # 一阶矩 m(梯度均值)
self.v = [torch.zeros_like(p) for p in self.params] # 二阶矩 v(梯度方差)
def step(self): # 参数更新
"""Adam参数更新"""
self.t += 1 # 时间步递增
with torch.no_grad():
for i, param in enumerate(self.params):
if param.grad is not None:
grad = param.grad # 获取梯度
# L2正则化(权重衰减)
if self.weight_decay > 0: # 如果有权重衰减
grad = grad + self.weight_decay * param # g_t = g_t + λ * θ
# 更新一阶矩:m_t = β₁ * m_{t-1} + (1 - β₁) * g_t
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grad
# 更新二阶矩:v_t = β₂ * v_{t-1} + (1 - β₂) * g_t²
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grad ** 2
# 偏差修正(补偿初始化为零导致的偏差)
# m̂_t = m_t / (1 - β₁ᵗ)
m_hat = self.m[i] / (1 - self.beta1 ** self.t)
# v̂_t = v_t / (1 - β₂ᵗ)
v_hat = self.v[i] / (1 - self.beta2 ** self.t)
# 参数更新:θ = θ - lr * m̂_t / (√v̂_t + ε)
param -= self.lr * m_hat / (torch.sqrt(v_hat) + self.eps)
def zero_grad(self): # 清除梯度
for param in self.params:
if param.grad is not None:
param.grad.zero_()
# --- 1. 验证手动Adam与PyTorch Adam结果一致 ---
torch.manual_seed(42) # 设置随机种子
# 创建相同的模型
model_manual = nn.Sequential(nn.Linear(10, 1), nn.ReLU(), nn.Linear(1, 1)) # 手动Adam的模型
model_pytorch = nn.Sequential(nn.Linear(10, 1), nn.ReLU(), nn.Linear(1, 1)) # PyTorch Adam的模型
# 复制权重以确保初始状态相同
model_pytorch.load_state_dict(model_manual.state_dict()) # 复制参数
manual_optimizer = ManualAdam(model_manual.parameters(), lr=0.001) # 手动Adam
pytorch_optimizer = optim.Adam(model_pytorch.parameters(), lr=0.001) # PyTorch Adam
# 使用相同数据训练
X = torch.randn(32, 10) # 输入
y = torch.randn(32, 1) # 标签
criterion = nn.MSELoss() # 损失函数
print("手动Adam vs PyTorch Adam 对比:")
for epoch in range(10):
# 手动Adam
pred_manual = model_manual(X)
loss_manual = criterion(pred_manual, y)
manual_optimizer.zero_grad()
loss_manual.backward()
manual_optimizer.step()
# PyTorch Adam
pred_pytorch = model_pytorch(X)
loss_pytorch = criterion(pred_pytorch, y)
pytorch_optimizer.zero_grad()
loss_pytorch.backward()
pytorch_optimizer.step()
print(f" Epoch {epoch+1}: Manual Loss={loss_manual.item():.6f}, "
f"PyTorch Loss={loss_pytorch.item():.6f}") # 对比输出
# --- 2. 偏差修正的重要性演示 ---
print("\n=== 偏差修正的重要性 ===")
torch.manual_seed(0)
# 模拟梯度序列
true_mean_grad = 1.0 # 真实梯度均值
true_var_grad = 0.5 # 真实梯度方差
n_steps = 20 # 步数
beta1, beta2 = 0.9, 0.999 # 动量系数
m, v = 0.0, 0.0 # 初始化矩估计
print(f"{'Step':<6} {'m_t':<12} {'m_hat (corrected)':<20} {'v_t':<12} {'v_hat (corrected)':<20}")
print("-" * 70)
for t in range(1, n_steps + 1):
g = np.random.normal(true_mean_grad, np.sqrt(true_var_grad)) # 随机梯度
m = beta1 * m + (1 - beta1) * g # 更新一阶矩
v = beta2 * v + (1 - beta2) * g**2 # 更新二阶矩
# 偏差修正
m_hat = m / (1 - beta1**t) # 修正后的一阶矩
v_hat = v / (1 - beta2**t) # 修正后的二阶矩
if t <= 5 or t % 5 == 0: # 前5步和每5步输出
print(f"{t:<6} {m:<12.6f} {m_hat:<20.6f} {v:<12.6f} {v_hat:<20.6f}")
print(f"\n真实梯度均值: {true_mean_grad}")
print(f"可以看到:偏差修正后 m_hat 更快地接近真实均值")
5.5 AdamW 和其他变体
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
import matplotlib.pyplot as plt # 绘图库
# ============================================
# 案例13:AdamW vs Adam + L2正则化
# ============================================
torch.manual_seed(42) # 设置随机种子
# --- 1. 数据准备 ---
n = 300 # 样本数
X = torch.randn(n, 20) # 20维输入特征
# 真实权重(稀疏的)
true_w = torch.zeros(20) # 初始化真实权重为0
true_w[:5] = torch.tensor([3.0, -2.0, 1.5, -1.0, 0.5]) # 只有前5个特征有贡献
y = X @ true_w + torch.randn(n) * 0.1 # 线性关系 + 噪声
# --- 2. 使用Adam + L2正则化(在损失函数中加L2)---
def train_adam_with_l2_in_loss(X, y, l2_lambda=0.01, epochs=300):
"""Adam + L2正则化(在损失函数中实现)
注意:这种方式下L2惩罚也被自适应学习率缩放,
导致正则化效果不均匀
Args:
X: 输入数据
y: 标签
l2_lambda: L2正则化系数
epochs: 训练轮数
Returns:
学到的权重和损失历史
"""
model = nn.Linear(20, 1, bias=False) # 线性模型,无偏置
optimizer = optim.Adam(model.parameters(), lr=0.01) # Adam优化器
losses = [] # 损失历史
for epoch in range(epochs): # 训练循环
pred = model(X).squeeze() # 前向传播
mse_loss = nn.MSELoss()(pred, y) # MSE损失
# 在损失函数中添加L2正则化项
l2_reg = 0 # L2正则化项初始化
for param in model.parameters(): # 遍历参数
l2_reg += torch.sum(param ** 2) # 累加参数平方和
total_loss = mse_loss + l2_lambda * l2_reg # 总损失 = MSE + λ * ||θ||²
optimizer.zero_grad() # 清除梯度
total_loss.backward() # 反向传播
optimizer.step() # 更新参数
losses.append(total_loss.item()) # 记录损失
return model.weight.data.clone().squeeze(), losses # 返回权重和损失
# --- 3. 使用AdamW(解耦权重衰减)---
def train_adamw(X, y, weight_decay=0.01, epochs=300):
"""AdamW优化器(解耦权重衰减)
AdamW的关键改进:权重衰减不通过梯度传播,
而是直接在参数更新步骤中实现:
θ_t = θ_{t-1} - lr * (m̂_t / (√v̂_t + ε) + λ * θ_{t-1})
这样权重衰减不会被自适应学习率缩放
Args:
X: 输入数据
y: 标签
weight_decay: 权重衰减系数
epochs: 训练轮数
Returns:
学到的权重和损失历史
"""
model = nn.Linear(20, 1, bias=False) # 线性模型
optimizer = optim.AdamW(model.parameters(), lr=0.01,
weight_decay=weight_decay) # AdamW优化器
losses = [] # 损失历史
for epoch in range(epochs): # 训练循环
pred = model(X).squeeze() # 前向传播
mse_loss = nn.MSELoss()(pred, y) # MSE损失
optimizer.zero_grad() # 清除梯度
mse_loss.backward() # 反向传播(只计算MSE的梯度)
optimizer.step() # 更新参数(AdamW内部处理权重衰减)
losses.append(mse_loss.item()) # 记录损失
return model.weight.data.clone().squeeze(), losses # 返回权重和损失
# --- 4. 对比结果 ---
w_adam_l2, losses_adam_l2 = train_adam_with_l2_in_loss(X, y, l2_lambda=0.01) # Adam+L2
w_adamw, losses_adamw = train_adamw(X, y, weight_decay=0.01) # AdamW
print("=== 真实权重 ===")
print(f" {true_w.numpy()}") # 输出真实权重
print("\n=== Adam + L2 学到的权重 ===")
print(f" {w_adam_l2.numpy()}") # 输出Adam+L2学到的权重
print("\n=== AdamW 学到的权重 ===")
print(f" {w_adamw.numpy()}") # 输出AdamW学到的权重
# 可视化
fig, axes = plt.subplots(1, 3, figsize=(18, 5)) # 创建子图
# 权重对比
x_idx = np.arange(20) # 特征索引
width = 0.25 # 柱状图宽度
axes[0].bar(x_idx - width, true_w.numpy(), width, label='True', alpha=0.7) # 真实权重
axes[0].bar(x_idx, w_adam_l2.numpy(), width, label='Adam+L2', alpha=0.7) # Adam+L2
axes[0].bar(x_idx + width, w_adamw.numpy(), width, label='AdamW', alpha=0.7) # AdamW
axes[0].set_xlabel('Feature Index') # x轴标签
axes[0].set_ylabel('Weight Value') # y轴标签
axes[0].set_title('Weight Comparison') # 标题
axes[0].legend() # 图例
# 损失曲线
axes[1].plot(losses_adam_l2, label='Adam+L2', alpha=0.7) # Adam+L2损失曲线
axes[1].plot(losses_adamw, label='AdamW', alpha=0.7) # AdamW损失曲线
axes[1].set_xlabel('Epoch') # x轴标签
axes[1].set_ylabel('Loss') # y轴标签
axes[1].set_title('Training Loss') # 标题
axes[1].legend() # 图例
# 权重衰减效果(非零权重的L2范数)
nonzero_idx = [0, 1, 2, 3, 4] # 真实非零权重索引
axes[2].bar([0, 1], [
np.linalg.norm(w_adam_l2.numpy()[nonzero_idx]), # Adam+L2 非零权重范数
np.linalg.norm(w_adamw.numpy()[nonzero_idx]) # AdamW 非零权重范数
], tick_label=['Adam+L2', 'AdamW'], color=['blue', 'red'], alpha=0.7)
axes[2].set_title('L2 Norm of Important Weights') # 标题
axes[2].set_ylabel('L2 Norm') # y轴标签
plt.tight_layout() # 调整间距
plt.savefig('adamw_vs_adam_l2.png', dpi=150) # 保存图片
plt.show() # 显示图片
六、二阶近似方法
6.1 牛顿法
θt+1=θt−H−1∇θJ(θt)\theta_{t+1} = \theta_t - H^{-1} \nabla_\theta J(\theta_t)θt+1=θt−H−1∇θJ(θt)
其中 HHH 是 Hessian 矩阵。问题是计算 HHH 及其逆的代价为 O(n2)O(n^2)O(n2) 到 O(n3)O(n^3)O(n3)。
6.2 共轭梯度法(Conjugate Gradient)
不需要显式存储Hessian矩阵,而是通过迭代的方式求解 H−1gH^{-1}gH−1g。
6.3 BFGS / L-BFGS
通过迭代近似Hessian矩阵的逆。
6.4 代码案例
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import numpy as np # 导入NumPy
from scipy.optimize import minimize # SciPy优化工具
import matplotlib.pyplot as plt # 绘图库
# ============================================
# 案例14:牛顿法 vs 梯度下降
# ============================================
# --- 1. 二次函数上的牛顿法演示 ---
def quadratic_2d(x):
"""二维二次函数 f(x) = 0.5 * x^T A x + b^T x
Args:
x: 二维向量 [x1, x2]
Returns:
函数值
"""
A = np.array([[2.0, 1.0], # Hessian矩阵(常数矩阵)
[1.0, 4.0]])
b = np.array([1.0, -2.0]) # 线性项
return 0.5 * x @ A @ x + b @ x # 返回函数值
def quadratic_grad(x):
"""二次函数的梯度: ∇f = Ax + b
Args:
x: 二维向量
Returns:
梯度向量
"""
A = np.array([[2.0, 1.0],
[1.0, 4.0]])
b = np.array([1.0, -2.0])
return A @ x + b # 返回梯度
def quadratic_hessian(x):
"""二次函数的Hessian矩阵(对于二次函数是常数矩阵)
Args:
x: 二维向量(未使用,但保持接口一致)
Returns:
Hessian矩阵
"""
return np.array([[2.0, 1.0], # 常数Hessian矩阵
[1.0, 4.0]])
# --- 牛顿法实现 ---
def newton_method(x0, n_steps=10):
"""牛顿法优化
更新规则:x_{t+1} = x_t - H^(-1) * g
对于二次函数,牛顿法一步即可收敛到最优解!
Args:
x0: 起始点
n_steps: 迭代步数
Returns:
优化轨迹
"""
x = x0.copy() # 复制起始点
trajectory = [x.copy()] # 记录轨迹
for step in range(n_steps): # 迭代
g = quadratic_grad(x) # 计算梯度
H = quadratic_hessian(x) # 计算Hessian矩阵
H_inv = np.linalg.inv(H) # 计算Hessian逆矩阵
x = x - H_inv @ g # 牛顿法更新
trajectory.append(x.copy()) # 记录轨迹
print(f"Newton Step {step+1}: x={x}, f(x)={quadratic_2d(x):.6f}")
return trajectory # 返回轨迹
# --- 梯度下降实现 ---
def gradient_descent(x0, lr=0.1, n_steps=50):
"""标准梯度下降
更新规则:x_{t+1} = x_t - lr * g
Args:
x0: 起始点
lr: 学习率
n_steps: 迭代步数
Returns:
优化轨迹
"""
x = x0.copy() # 复制起始点
trajectory = [x.copy()] # 记录轨迹
for step in range(n_steps): # 迭代
g = quadratic_grad(x) # 计算梯度
x = x - lr * g # 梯度下降更新
trajectory.append(x.copy()) # 记录轨迹
return trajectory # 返回轨迹
# --- 2. 运行对比 ---
x0 = np.array([3.0, 3.0]) # 起始点
print("=== 牛顿法 ===")
traj_newton = newton_method(x0, n_steps=5) # 运行牛顿法
print("\n=== 梯度下降 ===")
traj_gd = gradient_descent(x0, lr=0.1, n_steps=50) # 运行梯度下降
print(f"GD最终: x={traj_gd[-1]}, f(x)={quadratic_2d(traj_gd[-1]):.6f}")
# --- 3. 可视化对比 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # 创建子图
# 等高线
x_range = np.linspace(-4, 5, 200)
y_range = np.linspace(-4, 5, 200)
X_grid, Y_grid = np.meshgrid(x_range, y_range)
Z = np.zeros_like(X_grid)
for i in range(X_grid.shape[0]):
for j in range(X_grid.shape[1]):
Z[i, j] = quadratic_2d(np.array([X_grid[i, j], Y_grid[i, j]]))
for ax in axes:
ax.contour(X_grid, Y_grid, Z, levels=20, cmap='coolwarm', alpha=0.5)
# 牛顿法轨迹
tn = np.array(traj_newton)
axes[0].plot(tn[:, 0], tn[:, 1], 'go-', markersize=8, linewidth=2, label='Newton')
axes[0].plot(tn[0, 0], tn[0, 1], 'rs', markersize=12, label='Start')
axes[0].plot(tn[-1, 0], tn[-1, 1], 'r*', markersize=20, label='End')
axes[0].set_title('Newton Method (Converges in 1-2 steps)')
axes[0].legend()
# 梯度下降轨迹
tg = np.array(traj_gd)
step_size = max(1, len(tg) // 20)
axes[1].plot(tg[::step_size, 0], tg[::step_size, 1], 'bo-',
markersize=4, linewidth=1, label='GD (lr=0.1)')
axes[1].plot(tg[0, 0], tg[0, 1], 'rs', markersize=12, label='Start')
axes[1].plot(tg[-1, 0], tg[-1, 1], 'r*', markersize=20, label='End')
axes[1].set_title('Gradient Descent (Many steps)')
axes[1].legend()
plt.tight_layout()
plt.savefig('newton_vs_gd.png', dpi=150)
plt.show()
6.5 代码案例:Hessian-vector product
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
# ============================================
# 案例15:使用PyTorch计算Hessian-vector product
# ============================================
# --- 1. 计算Hessian-vector product ---
def hessian_vector_product(loss, params, vector):
"""计算Hessian-vector product H*v(无需显式构造Hessian矩阵)
原理:H*v = ∇(∇f · v),即先计算梯度与向量的点积,
然后对该点积再求梯度
这比显式计算Hessian矩阵(O(n²)存储)更高效
Args:
loss: 标量损失值
params: 参数列表
vector: 与参数同形状的向量列表
Returns:
Hessian-vector product结果列表
"""
# 第一步:计算梯度 g = ∇f
grads = torch.autograd.grad(loss, params, create_graph=True) # 计算梯度,保留计算图
# 第二步:计算梯度与向量的点积 g·v
grad_vector_dot = sum(torch.sum(g * v) for g, v in zip(grads, vector)) # 点积
# 第三步:对点积再求梯度,得到 H*v
hv = torch.autograd.grad(grad_vector_dot, params) # Hessian-vector product
return hv # 返回结果
# --- 2. 演示 ---
torch.manual_seed(42) # 设置随机种子
# 创建简单模型
model = nn.Sequential( # 简单网络
nn.Linear(5, 10), # 输入层
nn.ReLU(), # ReLU激活
nn.Linear(10, 1) # 输出层
)
# 准备数据
x = torch.randn(8, 5) # 输入(batch_size=8)
y = torch.randn(8, 1) # 标签
# 前向传播
pred = model(x) # 预测
loss = nn.MSELoss()(pred, y) # 计算损失
# 创建随机向量v(与参数同形状)
params = list(model.parameters()) # 获取所有参数
vector = [torch.randn_like(p) for p in params] # 随机向量
# 计算Hessian-vector product
hv = hessian_vector_product(loss, params, vector) # 计算H*v
print("=== Hessian-vector product 结果 ===")
for i, (p, v, h) in enumerate(zip(params, vector, hv)): # 遍历每组参数
print(f"Param {i}: shape={list(p.shape)}, "
f"||v||={v.norm():.4f}, "
f"||Hv||={h.norm():.4f}") # 输出各层结果
# --- 3. 验证:与显式Hessian对比(小规模)---
print("\n=== 验证Hessian-vector product ===")
# 对于小模型,我们可以显式计算Hessian
model_small = nn.Linear(3, 1, bias=False) # 极简模型
x_small = torch.randn(4, 3) # 输入
y_small = torch.randn(4, 1) # 标签
pred = model_small(x_small) # 预测
loss = nn.MSELoss()(pred, y_small) # 损失
# 计算显式Hessian
params = list(model_small.parameters()) # 参数
n_params = sum(p.numel() for p in params) # 参数总数
hessian = torch.zeros(n_params, n_params) # 初始化Hessian矩阵
grads = torch.autograd.grad(loss, params, create_graph=True) # 一阶梯度
flat_grad = torch.cat([g.reshape(-1) for g in grads]) # 展平梯度
for i in range(n_params): # 遍历每个参数维度
grad2 = torch.autograd.grad(flat_grad[i], params, retain_graph=True) # 二阶梯度
hessian[i] = torch.cat([g.reshape(-1) for g in grad2]) # 填充Hessian矩阵
# 使用Hessian-vector product验证
v = torch.randn(n_params) # 随机向量
v_list = [v.reshape(p.shape) for p in params] # 转为参数形状
hv_product = hessian_vector_product(loss, params, v_list) # 计算H*v
hv_flat = torch.cat([h.reshape(-1) for h in hv_product]) # 展平
hv_explicit = hessian @ v # 显式计算 H*v
print(f"Hessian矩阵:\n{hessian}")
print(f"\nHv (Hessian-vector product): {hv_flat}")
print(f"Hv (explicit): {hv_explicit}")
print(f"差异: {(hv_flat - hv_explicit).norm():.10f}") # 应该非常小
6.6 L-BFGS使用
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
import numpy as np # 导入NumPy
# ============================================
# 案例16:PyTorch中使用L-BFGS优化器
# ============================================
torch.manual_seed(42) # 设置随机种子
# --- 1. 数据准备 ---
n = 200 # 样本数
X = torch.randn(n, 5) # 5维输入
true_w = torch.tensor([2.0, -1.0, 0.5, -0.3, 1.5]) # 真实权重
y = X @ true_w + torch.randn(n) * 0.1 # 线性关系 + 噪声
# --- 2. 使用L-BFGS优化 ---
model = nn.Linear(5, 1) # 线性模型
criterion = nn.MSELoss() # MSE损失
optimizer = optim.LBFGS(model.parameters(), lr=1.0, # L-BFGS优化器
history_size=10, # 存储的历史向量对数
max_iter=20, # 每次迭代的最大线搜索步数
line_search_fn='strong_wolfe') # 使用强Wolfe线搜索
# L-BFGS需要特殊的closure来多次计算损失和梯度
losses = [] # 记录损失
for epoch in range(20): # L-BFGS通常只需很少的迭代次数
def closure():
"""L-BFGS的闭包函数,需要计算并返回损失值
L-BFGS可能在单次step中多次调用此函数进行线搜索
Returns:
当前损失值
"""
optimizer.zero_grad() # 清除梯度
pred = model(X).squeeze() # 前向传播
loss = criterion(pred, y) # 计算损失
loss.backward() # 反向传播
return loss # 返回损失值
loss = optimizer.step(closure) # 执行L-BFGS步骤
losses.append(loss.item()) # 记录损失
if (epoch + 1) % 5 == 0: # 每5轮打印
print(f"Epoch {epoch+1}: Loss = {loss.item():.6f}")
print(f"\n真实权重: {true_w.numpy()}") # 真实权重
print(f"学到权重: {model.weight.data.squeeze().numpy()}") # 学到的权重
print(f"\nL-BFGS通常比一阶方法收敛更快,但需要更多内存")
print(f"适用于小规模全批量优化问题,不适合大规模SGD场景")
七、优化策略和元算法
7.1 学习率调度(Learning Rate Scheduling)
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
from torch.optim.lr_scheduler import ( # 导入各种学习率调度器
StepLR, # 等间隔衰减
MultiStepLR, # 多步衰减
ExponentialLR, # 指数衰减
CosineAnnealingLR, # 余弦退火
ReduceLROnPlateau, # 自适应衰减(基于指标)
CyclicLR, # 循环学习率
OneCycleLR, # 1Cycle策略
LambdaLR, # 自定义Lambda衰减
)
import matplotlib.pyplot as plt # 绘图库
import numpy as np # 导入NumPy
# ============================================
# 案例17:各种学习率调度策略
# ============================================
# --- 1. 创建模型和优化器 ---
model = nn.Linear(10, 1) # 简单线性模型
base_lr = 0.1 # 基础学习率
# --- 2. 定义各种调度器 ---
# (1) StepLR: 每隔固定步数,学习率乘以gamma
# lr_t = lr_0 * gamma^(floor(t/step_size))
scheduler_step = StepLR(optim.SGD(model.parameters(), lr=base_lr),
step_size=30, gamma=0.5) # 每30个epoch学习率乘以0.5
# (2) MultiStepLR: 在指定的epoch处衰减
scheduler_multi = MultiStepLR(optim.SGD(model.parameters(), lr=base_lr),
milestones=[30, 80, 120], gamma=0.1) # 在30,80,120处乘以0.1
# (3) ExponentialLR: 每个epoch学习率乘以gamma
# lr_t = lr_0 * gamma^t
scheduler_exp = ExponentialLR(optim.SGD(model.parameters(), lr=base_lr),
gamma=0.95) # 每个epoch乘以0.95
# (4) CosineAnnealingLR: 余弦退火
# lr_t = lr_min + 0.5*(lr_max - lr_min)*(1 + cos(πt/T))
scheduler_cos = CosineAnnealingLR(optim.SGD(model.parameters(), lr=base_lr),
T_max=100, eta_min=0.001) # 100个epoch的余弦退火
# (5) ReduceLROnPlateau: 当指标停止改善时衰减
scheduler_plateau = ReduceLROnPlateau(optim.SGD(model.parameters(), lr=base_lr),
mode='min', factor=0.1, patience=10) # 损失不降时衰减
# (6) CyclicLR: 循环学习率
scheduler_cyclic = CyclicLR(optim.SGD(model.parameters(), lr=base_lr),
base_lr=0.001, max_lr=0.1, # 学习率在0.001和0.1之间循环
step_size_up=20, mode='triangular') # 每20步完成一个循环
# (7) OneCycleLR: 1Cycle策略(超级收敛)
# 先增大学习率再减小,适合快速训练
scheduler_onecycle = OneCycleLR(optim.SGD(model.parameters(), lr=base_lr),
max_lr=0.1, total_steps=100) # 100步的1Cycle
# --- 3. 模拟各种调度器的学习率变化 ---
n_epochs = 150 # 总epoch数
# 为每种调度器创建独立的优化器和调度器
schedulers = {} # 存储调度器
opt = optim.SGD([torch.zeros(1, requires_grad=True)], lr=base_lr)
schedulers['StepLR (step=30, γ=0.5)'] = StepLR(opt, step_size=30, gamma=0.5)
opt = optim.SGD([torch.zeros(1, requires_grad=True)], lr=base_lr)
schedulers['MultiStepLR [30,80,120]'] = MultiStepLR(opt, milestones=[30, 80, 120], gamma=0.1)
opt = optim.SGD([torch.zeros(1, requires_grad=True)], lr=base_lr)
schedulers['ExponentialLR (γ=0.95)'] = ExponentialLR(opt, gamma=0.95)
opt = optim.SGD([torch.zeros(1, requires_grad=True)], lr=base_lr)
schedulers['CosineAnnealingLR'] = CosineAnnealingLR(opt, T_max=100, eta_min=0.001)
opt = optim.SGD([torch.zeros(1, requires_grad=True)], lr=base_lr)
schedulers['CyclicLR'] = CyclicLR(opt, base_lr=0.001, max_lr=0.1,
step_size_up=20, mode='triangular')
# 记录学习率
lr_histories = {name: [] for name in schedulers} # 为每种调度器创建历史记录
for epoch in range(n_epochs): # 遍历epoch
for name, scheduler in schedulers.items(): # 遍历每种调度器
current_lr = scheduler.optimizer.param_groups[0]['lr'] # 获取当前学习率
lr_histories[name].append(current_lr) # 记录学习率
scheduler.step() # 更新学习率
# --- 4. 可视化 ---
fig, ax = plt.subplots(figsize=(14, 7)) # 创建画布
colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6'] # 颜色列表
for (name, history), color in zip(lr_histories.items(), colors): # 遍历每种调度器
ax.plot(history, label=name, linewidth=1.5, color=color) # 绘制学习率曲线
ax.set_xlabel('Epoch') # x轴标签
ax.set_ylabel('Learning Rate') # y轴标签
ax.set_title('Learning Rate Scheduling Strategies') # 标题
ax.legend(fontsize=9) # 图例
ax.grid(True, alpha=0.3) # 网格
ax.set_yscale('log') # 对数刻度
plt.tight_layout() # 调整间距
plt.savefig('lr_schedulers.png', dpi=150) # 保存图片
plt.show() # 显示图片
7.2 Warmup 策略
python
import torch # 导入PyTorch
import torch.optim as optim # 导入优化器
import matplotlib.pyplot as plt # 绘图库
import math # 数学库
# ============================================
# 案例18:学习率Warmup策略实现
# ============================================
class WarmupCosineScheduler:
"""Warmup + 余弦退火学习率调度器
阶段1(Warmup):学习率从0线性增长到目标学习率
阶段2(余弦退火):学习率按余弦曲线衰减到最小值
Args:
optimizer: 优化器
warmup_epochs: warmup的epoch数
total_epochs: 总epoch数
base_lr: 目标学习率
min_lr: 最小学习率
"""
def __init__(self, optimizer, warmup_epochs, total_epochs,
base_lr, min_lr=1e-6):
self.optimizer = optimizer # 优化器
self.warmup_epochs = warmup_epochs # warmup轮数
self.total_epochs = total_epochs # 总轮数
self.base_lr = base_lr # 目标学习率
self.min_lr = min_lr # 最小学习率
self.current_epoch = 0 # 当前epoch
def step(self): # 更新学习率
"""根据当前epoch调整学习率"""
if self.current_epoch < self.warmup_epochs: # Warmup阶段
# 线性增长:lr = base_lr * (current_epoch / warmup_epochs)
lr = self.base_lr * (self.current_epoch / self.warmup_epochs)
else: # 余弦退火阶段
# 余弦退火公式
progress = (self.current_epoch - self.warmup_epochs) / \
(self.total_epochs - self.warmup_epochs) # 进度(0到1)
lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * \
(1 + math.cos(math.pi * progress)) # 余弦退火
# 更新优化器中所有参数组的学习率
for param_group in self.optimizer.param_groups: # 遍历参数组
param_group['lr'] = lr # 设置新学习率
self.current_epoch += 1 # epoch递增
return lr # 返回当前学习率
class WarmupLinearScheduler:
"""Warmup + 线性衰减调度器(常用于Transformer训练)
Args:
optimizer: 优化器
warmup_steps: warmup的步数
total_steps: 总步数
"""
def __init__(self, optimizer, warmup_steps, total_steps):
self.optimizer = optimizer # 优化器
self.warmup_steps = warmup_steps # warmup步数
self.total_steps = total_steps # 总步数
self.current_step = 0 # 当前步数
def step(self): # 更新学习率
"""Transformer风格的学习率调度"""
self.current_step += 1 # 步数递增
# 先增后降的调度公式(来自原始Transformer论文)
lr = (self.optimizer.defaults['lr'] *
min(self.current_step ** (-0.5), # 衰减项:step^(-0.5)
self.current_step * self.warmup_steps ** (-1.5))) # warmup项
for param_group in self.optimizer.param_groups: # 遍历参数组
param_group['lr'] = lr # 更新学习率
return lr # 返回学习率
# --- 模拟并对比不同warmup策略 ---
n_epochs = 100 # 总epoch数
base_lr = 0.01 # 基础学习率
# 创建不同的调度器
models = [torch.zeros(1, requires_grad=True) for _ in range(3)] # 创建参数
scheduler1 = WarmupCosineScheduler( # Warmup + 余弦退火
optim.SGD([models[0]], lr=base_lr),
warmup_epochs=10, total_epochs=n_epochs,
base_lr=base_lr, min_lr=1e-5
)
scheduler2 = WarmupCosineScheduler( # 更长Warmup + 余弦退火
optim.SGD([models[1]], lr=base_lr),
warmup_epochs=20, total_epochs=n_epochs,
base_lr=base_lr, min_lr=1e-5
)
# 无warmup的余弦退火
opt3 = optim.SGD([models[2]], lr=base_lr)
scheduler3_cos = optim.lr_scheduler.CosineAnnealingLR(opt3, T_max=n_epochs, eta_min=1e-5)
# 记录学习率
histories = {'Warmup10+Cosine': [], 'Warmup20+Cosine': [], 'No Warmup (Cosine)': []}
for epoch in range(n_epochs):
lr1 = scheduler1.step() # Warmup10 + 余弦
lr2 = scheduler2.step() # Warmup20 + 余弦
lr3 = scheduler3_cos.optimizer.param_groups[0]['lr'] # 无warmup余弦
scheduler3_cos.step() # 更新余弦调度器
histories['Warmup10+Cosine'].append(lr1) # 记录
histories['Warmup20+Cosine'].append(lr2) # 记录
histories['No Warmup (Cosine)'].append(lr3) # 记录
# 可视化
fig, ax = plt.subplots(figsize=(12, 6))
colors = ['#e74c3c', '#3498db', '#95a5a6']
for (name, history), color in zip(histories.items(), colors):
ax.plot(history, label=name, linewidth=2, color=color)
ax.axvline(x=10, color='red', linestyle=':', alpha=0.5, label='Warmup End (10)')
ax.axvline(x=20, color='blue', linestyle=':', alpha=0.5, label='Warmup End (20)')
ax.set_xlabel('Epoch')
ax.set_ylabel('Learning Rate')
ax.set_title('Warmup + Cosine Annealing vs No Warmup')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('warmup_strategies.png', dpi=150)
plt.show()
7.3 梯度裁剪(Gradient Clipping)
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import matplotlib.pyplot as plt # 绘图库
# ============================================
# 案例19:梯度裁剪的实现与效果
# ============================================
torch.manual_seed(42) # 设置随机种子
# --- 1. 创建容易梯度爆炸的RNN ---
class SimpleRNN(nn.Module): # 简单RNN
"""手动实现的简单RNN,用于演示梯度爆炸
Args:
input_size: 输入维度
hidden_size: 隐藏层维度
output_size: 输出维度
"""
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size # 隐藏层大小
# RNN权重矩阵
self.W_xh = nn.Linear(input_size, hidden_size) # 输入 -> 隐藏
self.W_hh = nn.Linear(hidden_size, hidden_size, bias=False) # 隐藏 -> 隐藏
self.W_hy = nn.Linear(hidden_size, output_size) # 隐藏 -> 输出
def forward(self, x):
"""前向传播
Args:
x: 输入序列 (batch_size, seq_len, input_size)
Returns:
输出序列和所有隐藏状态
"""
batch_size, seq_len, _ = x.size() # 获取维度信息
h = torch.zeros(batch_size, self.hidden_size) # 初始化隐藏状态
outputs = [] # 存储输出
hidden_states = [] # 存储隐藏状态
for t in range(seq_len): # 遍历时间步
h = torch.tanh(self.W_xh(x[:, t, :]) + self.W_hh(h)) # RNN更新公式
y = self.W_hy(h) # 输出
outputs.append(y) # 记录输出
hidden_states.append(h) # 记录隐藏状态
return torch.stack(outputs, dim=1), hidden_states # 返回输出和隐藏状态
# --- 2. 训练并对比有无梯度裁剪 ---
def train_rnn(use_clip_grad=False, max_norm=1.0, seq_len=50):
"""训练RNN,可选是否使用梯度裁剪
Args:
use_clip_grad: 是否使用梯度裁剪
max_norm: 梯度范数的最大值
seq_len: 序列长度
Returns:
梯度范数历史和损失历史
"""
model = SimpleRNN(input_size=10, hidden_size=64, output_size=10) # 创建RNN
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # SGD优化器
criterion = nn.MSELoss() # MSE损失
grad_norms = [] # 记录梯度范数
losses = [] # 记录损失
for step in range(100): # 训练100步
# 生成随机序列数据
x = torch.randn(16, seq_len, 10) # 输入(batch=16, 时间步=seq_len, 特征=10)
y = torch.randn(16, seq_len, 10) # 目标
output, _ = model(x) # 前向传播
loss = criterion(output, y) # 计算损失
optimizer.zero_grad() # 清除梯度
loss.backward() # 反向传播
# 计算梯度范数
total_norm = 0 # 总梯度范数平方
for p in model.parameters(): # 遍历参数
if p.grad is not None:
total_norm += p.grad.data.norm(2).item() ** 2 # 累加各参数梯度范数的平方
total_norm = total_norm ** 0.5 # 开方得到总梯度范数
grad_norms.append(total_norm) # 记录梯度范数
# 梯度裁剪
if use_clip_grad: # 如果使用梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_norm) # 按范数裁剪
optimizer.step() # 更新参数
losses.append(loss.item()) # 记录损失
return grad_norms, losses # 返回记录
# 运行对比
print("=== 训练无梯度裁剪的RNN ===")
grad_norms_no_clip, losses_no_clip = train_rnn(use_clip_grad=False)
print("=== 训练有梯度裁剪的RNN (max_norm=1.0) ===")
grad_norms_clip, losses_clip = train_rnn(use_clip_grad=True, max_norm=1.0)
# --- 3. 可视化 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 创建子图
# 梯度范数对比
axes[0].plot(grad_norms_no_clip, label='No Clipping', color='red', alpha=0.7) # 无裁剪
axes[0].plot(grad_norms_clip, label='Clip (max_norm=1.0)', color='blue', alpha=0.7) # 有裁剪
axes[0].set_xlabel('Step') # x轴标签
axes[0].set_ylabel('Gradient Norm') # y轴标签
axes[0].set_title('Gradient Norm Comparison') # 标题
axes[0].set_yscale('log') # 对数刻度
axes[0].legend() # 图例
axes[0].grid(True, alpha=0.3) # 网格
# 损失对比
axes[1].plot(losses_no_clip, label='No Clipping', color='red', alpha=0.7) # 无裁剪
axes[1].plot(losses_clip, label='Clip (max_norm=1.0)', color='blue', alpha=0.7) # 有裁剪
axes[1].set_xlabel('Step') # x轴标签
axes[1].set_ylabel('Loss') # y轴标签
axes[1].set_title('Loss Comparison') # 标题
axes[1].legend() # 图例
axes[1].grid(True, alpha=0.3) # 网格
plt.tight_layout() # 调整间距
plt.savefig('gradient_clipping.png', dpi=150) # 保存图片
plt.show() # 显示图片
# --- 4. 两种梯度裁剪方式演示 ---
print("\n=== 两种梯度裁剪方式 ===")
model = nn.Linear(5, 1) # 简单模型
x = torch.randn(8, 5) # 输入
y = torch.randn(8, 1) # 标签
loss = nn.MSELoss()(model(x), y) # 损失
loss.backward() # 反向传播
# 方式1:按范数裁剪(clip_grad_norm_)
# 如果总梯度范数超过max_norm,等比例缩放所有梯度
print("方式1: clip_grad_norm_(按范数裁剪)")
grad_norm_before = sum(p.grad.norm()**2 for p in model.parameters() if p.grad is not None)**0.5
print(f" 裁剪前梯度范数: {grad_norm_before:.4f}")
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 按范数裁剪
grad_norm_after = sum(p.grad.norm()**2 for p in model.parameters() if p.grad is not None)**0.5
print(f" 裁剪后梯度范数: {grad_norm_after:.4f} (限制在1.0以内)")
# 重新计算梯度
model.zero_grad()
loss = nn.MSELoss()(model(x), y)
loss.backward()
# 方式2:按值裁剪(clip_grad_value_)
# 将每个梯度值限制在[-clip_value, clip_value]范围内
print("\n方式2: clip_grad_value_(按值裁剪)")
print(f" 裁剪前: min={model.weight.grad.min():.4f}, max={model.weight.grad.max():.4f}")
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5) # 按值裁剪
print(f" 裁剪后: min={model.weight.grad.min():.4f}, max={model.weight.grad.max():.4f} (限制在±0.5)")
7.4 梯度累积(Gradient Accumulation)
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
# ============================================
# 案例20:梯度累积策略
# ============================================
# 梯度累积用于在GPU显存不足时模拟大batch训练
# 原理:多次前向+反向传播,累积梯度后再统一更新参数
torch.manual_seed(42) # 设置随机种子
# --- 1. 模型和数据准备 ---
model = nn.Sequential( # 简单网络
nn.Linear(20, 64), # 输入层
nn.ReLU(), # 激活
nn.Linear(64, 1) # 输出层
)
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam优化器
criterion = nn.MSELoss() # 损失函数
# 模拟大batch(1024样本)但只能放batch_size=128的GPU
total_samples = 1024 # 目标有效batch size
micro_batch_size = 128 # 每次实际处理的batch size
accumulation_steps = total_samples // micro_batch_size # 需要累积的步数 = 1024/128 = 8
print(f"目标有效batch size: {total_samples}") # 输出有效batch大小
print(f"实际micro batch size: {micro_batch_size}") # 输出micro batch大小
print(f"累积步数: {accumulation_steps}") # 输出累积步数
# --- 2. 生成完整数据 ---
X_all = torch.randn(total_samples, 20) # 所有输入数据
y_all = torch.randn(total_samples, 1) # 所有标签
# --- 3. 使用梯度累积训练 ---
model.train() # 训练模式
optimizer.zero_grad() # 初始清除梯度
total_loss = 0 # 累积损失
for step in range(accumulation_steps): # 遍历每个micro batch
# 获取当前micro batch
start_idx = step * micro_batch_size # 起始索引
end_idx = start_idx + micro_batch_size # 结束索引
X_batch = X_all[start_idx:end_idx] # 输入micro batch
y_batch = y_all[start_idx:end_idx] # 标签micro batch
# 前向传播
pred = model(X_batch) # 预测
loss = criterion(pred, y_batch) # 计算损失
# 关键:损失除以累积步数,使得梯度等效于大batch的平均梯度
loss_scaled = loss / accumulation_steps # 缩放损失
loss_scaled.backward() # 反向传播(梯度会自动累积到参数的.grad中)
total_loss += loss.item() # 累积原始损失
print(f" Micro-batch {step+1}/{accumulation_steps}: "
f"loss={loss.item():.4f}, accumulated_grad_norm={sum(p.grad.norm()**2 for p in model.parameters() if p.grad is not None)**0.5:.4f}")
# 累积完成后,执行一次参数更新
optimizer.step() # 更新参数(使用累积了8步的梯度)
optimizer.zero_grad() # 清除梯度,准备下一轮
print(f"\n总损失: {total_loss:.4f}")
print(f"等效大batch损失: {total_loss / accumulation_steps:.4f}")
# --- 4. 封装成通用的梯度累积训练函数 ---
def train_with_gradient_accumulation(model, dataloader, optimizer, criterion,
accumulation_steps=4, max_grad_norm=1.0):
"""使用梯度累积训练模型的通用函数
Args:
model: 神经网络模型
dataloader: 数据加载器
optimizer: 优化器
criterion: 损失函数
accumulation_steps: 梯度累积步数
max_grad_norm: 梯度裁剪阈值
Returns:
平均损失
"""
model.train() # 训练模式
optimizer.zero_grad() # 清除梯度
total_loss = 0 # 总损失
n_batches = 0 # batch计数
for i, (inputs, targets) in enumerate(dataloader): # 遍历数据
outputs = model(inputs) # 前向传播
loss = criterion(outputs, targets) # 计算损失
loss = loss / accumulation_steps # 缩放损失
loss.backward() # 反向传播(梯度累积)
total_loss += loss.item() * accumulation_steps # 记录原始损失
n_batches += 1 # batch计数加1
# 每accumulation_steps步更新一次参数
if (i + 1) % accumulation_steps == 0:
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)
optimizer.step() # 更新参数
optimizer.zero_grad() # 清除梯度
return total_loss / n_batches # 返回平均损失
7.5 综合实战:完整训练流程
python
import torch # 导入PyTorch
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器
from torch.utils.data import DataLoader, TensorDataset # 数据工具
import numpy as np # 导入NumPy
import matplotlib.pyplot as plt # 绘图库
# ============================================
# 案例21:完整训练流程------综合所有优化策略
# ============================================
torch.manual_seed(42) # 设置随机种子
np.random.seed(42) # NumPy随机种子
# --- 1. 数据准备 ---
n_train = 2000 # 训练样本数
n_val = 500 # 验证样本数
input_dim = 50 # 输入维度
n_classes = 5 # 类别数
# 生成多分类数据
X_train = torch.randn(n_train, input_dim) # 训练输入
y_train = torch.randint(0, n_classes, (n_train,)) # 训练标签
X_val = torch.randn(n_val, input_dim) # 验证输入
y_val = torch.randint(0, n_classes, (n_val,)) # 验证标签
# 创建DataLoader
train_dataset = TensorDataset(X_train, y_train) # 训练数据集
val_dataset = TensorDataset(X_val, y_val) # 验证数据集
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) # 训练数据加载器
val_loader = DataLoader(val_dataset, batch_size=64) # 验证数据加载器
# --- 2. 定义模型(使用Kaiming初始化)---
class OptimizedNet(nn.Module): # 优化的网络
"""综合使用多种优化策略的网络
Args:
input_dim: 输入维度
hidden_dims: 各隐藏层维度列表
n_classes: 类别数
dropout_rate: Dropout概率
"""
def __init__(self, input_dim, hidden_dims, n_classes, dropout_rate=0.3):
super(OptimizedNet, self).__init__()
layers = [] # 存储网络层
prev_dim = input_dim # 上一层维度
for hidden_dim in hidden_dims: # 遍历隐藏层维度
linear = nn.Linear(prev_dim, hidden_dim) # 全连接层
nn.init.kaiming_normal_(linear.weight, nonlinearity='relu') # Kaiming初始化
nn.init.zeros_(linear.bias) # 偏置初始化为0
layers.extend([ # 添加层
linear, # 线性层
nn.BatchNorm1d(hidden_dim), # 批归一化
nn.ReLU(), # ReLU激活
nn.Dropout(dropout_rate), # Dropout正则化
])
prev_dim = hidden_dim # 更新上一层维度
# 输出层
output_layer = nn.Linear(prev_dim, n_classes) # 输出全连接层
nn.init.xavier_uniform_(output_layer.weight, gain=0.1) # 较小增益Xavier初始化
layers.append(output_layer) # 添加输出层
self.network = nn.Sequential(*layers) # 组合所有层
def forward(self, x): # 前向传播
return self.network(x) # 返回输出
# --- 3. 训练函数(综合多种优化策略)---
def train_complete(model, train_loader, val_loader, epochs=100,
lr=0.001, weight_decay=0.01, patience=15):
"""完整训练流程
综合使用:
1. AdamW优化器(自适应学习率 + 解耦权重衰减)
2. OneCycleLR学习率调度
3. 梯度裁剪
4. 提前停止
5. 模型保存/恢复
Args:
model: 神经网络
train_loader: 训练数据加载器
val_loader: 验证数据加载器
epochs: 最大训练轮数
lr: 最大/基础学习率
weight_decay: 权重衰减系数
patience: 提前停止容忍度
Returns:
训练历史字典
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 选择设备
model = model.to(device) # 将模型放到设备上
# AdamW优化器
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay) # AdamW
# OneCycleLR调度器
total_steps = epochs * len(train_loader) # 总训练步数
scheduler = optim.lr_scheduler.OneCycleLR( # 1Cycle学习率调度
optimizer, max_lr=lr, total_steps=total_steps,
pct_start=0.3, # 前30%步数用于warmup
anneal_strategy='cos', # 余弦退火
div_factor=25, # 初始学习率 = max_lr / 25
final_div_factor=1000 # 最终学习率 = max_lr / (25*1000)
)
# 损失函数
criterion = nn.CrossEntropyLoss() # 交叉熵损失
# 训练历史
history = {
'train_loss': [], 'val_loss': [],
'train_acc': [], 'val_acc': [],
'lr': [], 'grad_norm': []
}
# 提前停止相关变量
best_val_loss = float('inf') # 最佳验证损失
best_model_state = None # 最佳模型参数
patience_counter = 0 # 忍耐计数器
for epoch in range(epochs): # 遍历每个epoch
# --- 训练阶段 ---
model.train() # 训练模式
train_loss = 0 # 训练损失
train_correct = 0 # 训练正确数
train_total = 0 # 训练总数
epoch_grad_norms = [] # epoch内梯度范数
for batch_X, batch_y in train_loader: # 遍历训练batch
batch_X, batch_y = batch_X.to(device), batch_y.to(device) # 放到设备上
optimizer.zero_grad() # 清除梯度
outputs = model(batch_X) # 前向传播
loss = criterion(outputs, batch_y) # 计算损失
loss.backward() # 反向传播
# 梯度裁剪
grad_norm = torch.nn.utils.clip_grad_norm_( # 按范数裁剪梯度
model.parameters(), max_norm=1.0
)
epoch_grad_norms.append(grad_norm.item()) # 记录梯度范数
optimizer.step() # 更新参数
scheduler.step() # 更新学习率(OneCycleLR按步更新)
train_loss += loss.item() * batch_X.size(0) # 累积损失
_, predicted = outputs.max(1) # 获取预测类别
train_total += batch_y.size(0) # 累积样本数
train_correct += predicted.eq(batch_y).sum().item() # 累积正确数
train_loss /= train_total # 平均训练损失
train_acc = 100.0 * train_correct / train_total # 训练准确率
# --- 验证阶段 ---
model.eval() # 评估模式
val_loss = 0 # 验证损失
val_correct = 0 # 验证正确数
val_total = 0 # 验证总数
with torch.no_grad(): # 不计算梯度
for batch_X, batch_y in val_loader: # 遍历验证batch
batch_X, batch_y = batch_X.to(device), batch_y.to(device) # 放到设备上
outputs = model(batch_X) # 前向传播
loss = criterion(outputs, batch_y) # 计算损失
val_loss += loss.item() * batch_X.size(0) # 累积损失
_, predicted = outputs.max(1) # 获取预测
val_total += batch_y.size(0) # 累积样本数
val_correct += predicted.eq(batch_y).sum().item() # 累积正确数
val_loss /= val_total # 平均验证损失
val_acc = 100.0 * val_correct / val_total # 验证准确率
# 记录历史
current_lr = optimizer.param_groups[0]['lr'] # 当前学习率
history['train_loss'].append(train_loss) # 记录训练损失
history['val_loss'].append(val_loss) # 记录验证损失
history['train_acc'].append(train_acc) # 记录训练准确率
history['val_acc'].append(val_acc) # 记录验证准确率
history['lr'].append(current_lr) # 记录学习率
history['grad_norm'].append(np.mean(epoch_grad_norms)) # 记录平均梯度范数
# 打印信息
if (epoch + 1) % 10 == 0: # 每10个epoch打印
print(f"Epoch [{epoch+1}/{epochs}] "
f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | "
f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}% | "
f"LR: {current_lr:.6f} | "
f"Grad Norm: {np.mean(epoch_grad_norms):.4f}")
# 提前停止检查
if val_loss < best_val_loss: # 如果验证损失有改善
best_val_loss = val_loss # 更新最佳验证损失
best_model_state = model.state_dict().copy() # 保存最佳模型参数
patience_counter = 0 # 重置忍耐计数器
else:
patience_counter += 1 # 增加忍耐计数器
if patience_counter >= patience: # 超过忍耐度
print(f"\n提前停止!在第 {epoch+1} 轮停止训练")
model.load_state_dict(best_model_state) # 恢复最佳模型
break
# 恢复最佳模型
if best_model_state is not None:
model.load_state_dict(best_model_state) # 确保使用最佳参数
print(f"\n最佳验证损失: {best_val_loss:.4f}") # 输出最佳结果
return history # 返回训练历史
# --- 4. 运行完整训练 ---
model = OptimizedNet(input_dim, hidden_dims=[128, 64, 32], n_classes=n_classes) # 创建模型
print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}") # 输出参数量
history = train_complete(model, train_loader, val_loader, epochs=100,
lr=0.01, weight_decay=0.01, patience=20) # 训练
# --- 5. 可视化训练历史 ---
fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # 创建子图
# 损失曲线
axes[0, 0].plot(history['train_loss'], label='Train Loss', color='blue') # 训练损失
axes[0, 0].plot(history['val_loss'], label='Val Loss', color='red') # 验证损失
axes[0, 0].set_title('Loss Curves') # 标题
axes[0, 0].legend() # 图例
axes[0, 0].grid(True, alpha=0.3) # 网格
# 准确率曲线
axes[0, 1].plot(history['train_acc'], label='Train Acc', color='blue') # 训练准确率
axes[0, 1].plot(history['val_acc'], label='Val Acc', color='red') # 验证准确率
axes[0, 1].set_title('Accuracy Curves') # 标题
axes[0, 1].legend() # 图例
axes[0, 1].grid(True, alpha=0.3) # 网格
# 学习率曲线
axes[1, 0].plot(history['lr'], color='green') # 学习率
axes[1, 0].set_title('Learning Rate (OneCycleLR)') # 标题
axes[1, 0].grid(True, alpha=0.3) # 网格
# 梯度范数曲线
axes[1, 1].plot(history['grad_norm'], color='purple') # 梯度范数
axes[1, 1].set_title('Average Gradient Norm') # 标题
axes[1, 1].set_yscale('log') # 对数刻度
axes[1, 1].grid(True, alpha=0.3) # 网格
plt.suptitle('Complete Training Pipeline with All Optimization Strategies', fontsize=14) # 总标题
plt.tight_layout() # 调整间距
plt.savefig('complete_training.png', dpi=150) # 保存图片
plt.show() # 显示图片
总结表
| 章节 | 核心知识点 | 关键代码实现 |
|---|---|---|
| 学习 vs 纯优化 | 过拟合、泛化误差、提前停止 | EarlyStopping 类 |
| 优化挑战 | 病态条件、鞍点、梯度消失/爆炸 | Hessian条件数、梯度流动分析 |
| 基本算法 | SGD、Momentum、Nesterov | ManualSGD、ManualMomentum、ManualNesterov |
| 初始化 | Xavier、Kaiming、正交、零初始化 | manual_xavier_init、manual_kaiming_init |
| 自适应学习率 | AdaGrad、RMSProp、Adam、AdamW | ManualAdaGrad、ManualRMSProp、ManualAdam |
| 二阶方法 | 牛顿法、Hessian-vector product、L-BFGS | newton_method、hessian_vector_product |
| 元算法 | 学习率调度、Warmup、梯度裁剪、梯度累积 | WarmupCosineScheduler、clip_grad_norm_、梯度累积训练 |