深度学习学习教程,从入门到精通,深度模型中的优化 — 完整知识点与代码案例(8)

深度模型中的优化 --- 完整知识点与代码案例


一、学习和纯优化有什么不同

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 ManualSGDManualMomentumManualNesterov
初始化 Xavier、Kaiming、正交、零初始化 manual_xavier_initmanual_kaiming_init
自适应学习率 AdaGrad、RMSProp、Adam、AdamW ManualAdaGradManualRMSPropManualAdam
二阶方法 牛顿法、Hessian-vector product、L-BFGS newton_methodhessian_vector_product
元算法 学习率调度、Warmup、梯度裁剪、梯度累积 WarmupCosineSchedulerclip_grad_norm_、梯度累积训练
相关推荐
lifallen1 小时前
短暂 Agent 与持久化工程:让理解脱离对话
人工智能·学习·ai·重构·ai编程
javaDocker1 小时前
金融级 AIOps 架构跃迁(从“1-5-10“快恢目标到 AI Agent 告警收敛的完整技术实践)
人工智能·金融·架构
多看书少吃饭1 小时前
GPT‑6 Astra 与 AGI 时代:当 AI 开始承担完整的工作
人工智能·gpt·agi
浩风祭月1 小时前
GPT-6 Astra API怎么接?Responses API迁移、异步工具调用与兼容项清单
人工智能·chatgpt·大模型·openai api·gpt-6 astra
FPC工厂——皇榜科技1 小时前
FPC拼板的利用率怎么算?排版方式不同,材料成本差15%
人工智能·科技·pcb工艺
互联网叫兽1 小时前
redis深入学习二
数据库·redis·学习
Raspberry_Pi_官方账号2 小时前
Raspberry Pi+TinyML:基于边缘计算的低成本离线皮肤癌AI诊断系统
人工智能·边缘计算·树莓派·raspberry pi·tinyml
阿里云大数据AI技术2 小时前
闲鱼「鱼卖卖」的长期记忆实践:用阿里云 Milvus 记住卖家的经营偏好
人工智能·agent
疯神NB2 小时前
循环神经网络RNN
人工智能·rnn·深度学习