Skip to content

梯度下降法


梯度下降法(Gradient Descent)是机器学习中最核心的优化算法之一,用于寻找使损失函数最小化的参数值。


基本概念


梯度下降法的核心思想是:沿着函数梯度的反方向逐步调整参数,使函数值不断减小。

从数学上看:

  • 梯度(∇f)方向是函数增长最快的方向
  • 梯度的反方向(-∇f)是函数下降最快的方向

在进行模型训练时,有三个基础的概念:

  • Epoch:使用全部数据对模型进行以此完整训练,训练轮次
  • Batch_size:使用训练集中的小部分祥本对模型权重进行以此反向传播的参数更新,每次训练每批次样本数量
  • Iteration:使用一个Batch数据对模型进行一次参数更新的过程

例如:

  • 假设数据集有50000个训练样本,现在选择Batch Size=256对模型进行训练:
  • 每个Epoch要训练的图片数量:50000
  • 训练集具有的Batch个数:50000 / 256 + 1 = 196
  • 每个Epoch具有的Iteration个数:196
  • 10个Epoch具有的Iteration个数:1960

公式定义


参数更新公式为:

\[W_{ij}^{new} = W_{ij}^{old} - \eta \frac{\partial E}{\partial w_{ij}} \]

其中:

  • \(W_{ij}^{new}\):更新后的权重
  • \(W_{ij}^{old}\):更新前的权重
  • \(\eta\):学习率(learning rate),控制每次更新的步长
  • \(\frac{\partial E}{\partial w_{ij}}\):损失函数 \(E\) 对权重 \(w_{ij}\) 的偏导数(梯度)

学习率 \(\eta\) 的选择至关重要:

  • 学习率太小:收敛速度慢,训练时间长
  • 学习率太大:可能跳过最优解,甚至导致发散(无法收敛)

解决方法:

  • 使用学习率衰减策略:随着训练进行逐渐减小学习率
  • 使用自适应学习率算法

根据训练集大小、批量大小和批次数量的不同,梯度下降主要分为三种形式:

类型训练集大小Batch SizeNumber of Batches说明
BGD (Batch Gradient Descent)\(N\)\(N\)\(1\)使用全部数据计算梯度
SGD (Stochastic Gradient Descent)\(N\)\(1\)\(N\)每次使用单个样本更新
Mini-Batch GD\(N\)\(B\)\(N/B+1\)折中方案,使用小批量数据

上述 Mini-Batch 的 Batch 的个数为 N / B + 1 是针对未整除的情况,整除情况下为 N / B


BGD


更新公式:

\[\theta_{t+1} = \theta_t - \eta \cdot \frac{1}{N} \sum_{i=1}^N \nabla_\theta J(\theta; x_i, y_i) \]

特点:

  • 每次迭代使用全部N个样本计算梯度
  • 梯度估计准确,但计算成本高
  • 可能陷入局部极小点

代码示例:

python
import torch
import torch.nn as nn
import torch.optim as optim

# 生成示例数据
X = torch.randn(100, 1)  # 100个样本
y = 3 * X + 2 + 0.1 * torch.randn(100, 1)  # 线性关系 + 噪声

# 定义模型
model = nn.Linear(1, 1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# BGD 训练
num_epochs = 100
for epoch in range(num_epochs):
    # 前向传播
    outputs = model(X)
    loss = criterion(outputs, y)
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

SGD


更新公式:

\[\theta_{t+1} = \theta_t - \eta \cdot \nabla_\theta J(\theta; x_i, y_i) \]

其中 \(i\) 随机选择

特点:

  • 每次随机使用1个样本更新
  • 计算快,适合在线学习
  • 梯度估计噪声大,收敛不稳定

代码示例:

python
import torch
import torch.nn as nn
import torch.optim as optim

# 生成示例数据
X = torch.randn(100, 1)  # 100个样本
y = 3 * X + 2 + 0.1 * torch.randn(100, 1)  # 线性关系 + 噪声

# 定义模型
model = nn.Linear(1, 1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# SGD 训练
num_epochs = 100
for epoch in range(num_epochs):
    for i in range(len(X)):  # 遍历每个样本
        # 取单个样本
        x_i = X[i].unsqueeze(0)
        y_i = y[i].unsqueeze(0)
        
        # 前向传播
        outputs = model(x_i)
        loss = criterion(outputs, y_i)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

Mini-Batch GD


更新公式:

\[\theta_{t+1} = \theta_t - \eta \cdot \frac{1}{B} \sum_{i=k}^{k+B-1} \nabla_\theta J(\theta; x_i, y_i) \]

其中 \(B\) 是 batch_size

特点:

  • 折中方案,通常B取32-256
  • 利用向量化加速计算
  • 梯度估计比SGD更稳定
python
from torch.utils.data import DataLoader, TensorDataset

# 将数据封装为 DataLoader
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)  # batch_size=16

# Minibatch GD 训练
num_epochs = 100
for epoch in range(num_epochs):
    for batch_x, batch_y in dataloader:  # 遍历每个小批次
        # 前向传播
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

梯度下降优化


梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到“鞍点”,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优

因此更加先进的优化算法,来改善参数更新策略,使得训练过程更稳定、更快速,并更容易找到更优的解,从而有效降低损失函数。


指数移动加权平均


公式定义


指数移动加权平均(Exponential Moving Average, EMA)是一种用于计算时间序列数据平均值的统计方法,其中最近的观测值被赋予更大的权重,而较旧的观测值权重按指数方式递减。公式如下:

\[S_t = \begin{cases} Y_1, & t = 0 \\ \beta \cdot S_{t-1} + (1 - \beta) \cdot Y_t, & t > 0 \end{cases} \]

其中:

  • \(S_t\):时间 \(t\) 的指数加权平均值。
  • \(Y_t\):时间 \(t\) 的实际观测值。
  • \(\beta\):平滑因子(介于0和1之间),控制权重的衰减速度。

展开 \(S_t\)

\[\begin{align*} S_t &= \beta S_{t-1} + (1 - \beta) Y_t \\ &= \beta (\beta S_{t-2} + (1 - \beta) Y_{t-1}) + (1 - \beta) Y_t \\ &= \beta^2 S_{t-2} + \beta (1 - \beta) Y_{t-1} + (1 - \beta) Y_t \\ &= \beta^3 S_{t-3} + \beta^2 (1 - \beta) Y_{t-2} + \beta (1 - \beta) Y_{t-1} + (1 - \beta) Y_t \\ &\ \vdots \\ &= (1 - \beta) Y_t + \beta (1 - \beta) Y_{t-1} + \beta^2 (1 - \beta) Y_{t-2} + \cdots + \beta^{t-1} (1 - \beta) Y_1 + \beta^t S_0 \\ &= (1 - \beta) \sum_{k=0}^{t-1} \beta^k Y_{t-k} + \beta^t Y_0 \end{align*} \]

可以看到,每个历史观测值 \(Y_{t-k}\) 的权重是 \((1 - \beta) \beta^k\),这是一个指数递减的权重:

  • \(\beta\) 越大(接近1),过去的平均值对当前的影响越大,新的观测值的影响越小,因此平均值变化更平缓(更“平滑”)。
  • \(\beta\) 越小(接近0),新的观测值的影响越大,平均值对近期变化更敏感,波动更大。

计算示例


模拟一个月(30天)的每日温度,并用指数移动平均(EMA)方法来平滑这些温度数据:

python
import torch
import matplotlib.pyplot as plt

ELEMENT_NUMBER = 30  # 定义元素数量为30

# 1. 实际平均温度
def dayTemperature():
    # 固定随机数种子
    torch.manual_seed(0)
    # 产生30天的随机温度,范围在10左右波动
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
    print(temperature)
    # 绘制平均温度
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, temperature, color='r', label='Daily Temperature')
    plt.scatter(days, temperature)
    plt.xlabel('Day')
    plt.ylabel('Temperature')
    plt.title('Actual Daily Temperature')
    plt.legend()
    plt.show()

# 2. 指数移动平均温度
def EMATemperature(beta=0.9):
    torch.manual_seed(0)  # 固定随机数种子
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10  # 产生30天的随机温度
    exp_weight_avg = []
    for idx, temp in enumerate(temperature, 1):  # 从下标1开始(实际是第0天)
        # 第一个元素的EMA值等于自身
        if idx == 1:
            exp_weight_avg.append(temp)
            continue
        # 后续元素的 EMA 值等于上一个 EMA * β + 当前气温乘以(1-β)
        new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
        exp_weight_avg.append(new_temp)
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, exp_weight_avg, color='b', label='EMA Temperature')
    plt.scatter(days, temperature, color='r', label='Daily Temperature')
    plt.xlabel('Day')
    plt.ylabel('Temperature')
    plt.title(f'Exponential Moving Average (beta={beta})')
    plt.legend()
    plt.show()

# 测试
dayTemperature()  # 绘制实际温度
EMATemperature()  # 绘制EMA温度(默认beta=0.9)
EMATemperature(beta=0.5)  # 绘制EMA温度(beta=0.5)

动量算法


公式定义


动量算法(Momentum)让优化过程逐渐趋于平缓:

  • 若梯度方向持续一致 → 动量累积 → 加速下降
  • 若梯度方向忽快忽慢 → 动量平均作用 → 减少震荡

梯度计算公式:

\[s_t = \beta s_{t-1} + (1-\beta)g_t \]
符号含义
\(s_t\)当前时刻 指数加权平均的梯度(动量)
\(s_{t-1}\)上一时刻的动量
\(g_t\)当前梯度(loss 对参数的偏导)
\(\beta\)动量系数(一般取 0.9 或 0.99),值越大,表示越信任历史加权累积的权重,惯性越强

梯度更新公式:

\[w_t = w_{t-1} - \eta s_t \]
符号含义
\(w_t\)当前权重参数
\(w_{t-1}\)上一时刻权重参数
\(\eta\)学习率(learning rate)
\(s_t\)累积动量(平滑梯度)

普通梯度下降是:

\[w_t = w_{t-1} - \eta g_t \]

但 Momentum 用的是 动量 \(s_t\) 而不是瞬时梯度 \(g_t\)


计算示例


实验:比较普通梯度下降(GD)与动量(Momentum)在一个条件数较大的二次函数上的表现:

  1. 在函数 \(f(x,y) = 0.5*(a*x^2 + b*y^2)\) 上做优化(a >> b 造成峡谷/震荡)

  2. 使用相同学习率分别运行普通 GD 和 Momentum(beta=0.9)

  3. 绘制出等高线与两条参数轨迹,以及每次迭代的 loss 曲线

python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数参数(条件数较大)
a = 100.0
b = 1.0

def loss(w):
    x, y = w
    return 0.5*(a*x**2 + b*y**2)

def grad(w):
    x, y = w
    return np.array([a*x, b*y])

# 优化器实现
def grad_descent(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

def momentum(w0, lr, beta, steps):
    w = w0.copy()
    v = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(steps):
        g = grad(w)
        v = beta * v + (1 - beta) * g  # 指数加权平均梯度
        w = w - lr * v
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 初始点与超参数
w0 = np.array([-3.0, 3.0])
lr = 0.01   # 学习率(对比同一学习率下的表现)
beta = 0.9
steps = 200

traj_gd, losses_gd = grad_descent(w0, lr, steps)
traj_mom, losses_mom = momentum(w0, lr, beta, steps)

# 绘图1:等高线 + 两条轨迹
# 生成网格与等高线
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(a*X**2 + b*Y**2)

plt.figure(figsize=(7,6))
cs = plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label='GD trajectory')
plt.plot(traj_mom[:,0], traj_mom[:,1], marker='x', markersize=3, linewidth=1, label='Momentum trajectory (beta=0.9)')
plt.scatter([w0[0]], [w0[1]], marker='s')  # 起点
plt.title('Trajectories on loss contours')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.axis('equal')
plt.grid(True)
plt.show()

# 绘图2:loss vs iteration
plt.figure(figsize=(7,5))
plt.semilogy(range(len(losses_gd)), losses_gd, linewidth=1, label='GD loss')
plt.semilogy(range(len(losses_mom)), losses_mom, linewidth=1, label='Momentum loss (beta=0.9)')
plt.title('Loss vs Iteration (log scale)')
plt.xlabel('Iteration')
plt.ylabel('Loss (log)')
plt.legend()
plt.grid(True)
plt.show()

# 输出一些数值比较:最终的坐标和损失
final_info = {
    'GD_final_w': traj_gd[-1].tolist(),
    'Momentum_final_w': traj_mom[-1].tolist(),
    'GD_final_loss': float(losses_gd[-1]),
    'Momentum_final_loss': float(losses_mom[-1])
}
final_info

RMSProp 算法


公式定义


RMSProp(Root Mean Square Propagation)是 AdaGrad 的改进版本

  • AdaGrad:对每个参数维度累积梯度平方,使陡峭方向学习率变小、平缓方向学习率相对大
  • 但 AdaGrad 累积所有历史梯度平方 → 会越来越大 → 学习率会越来越小 → 最终训练停滞

RMSProp 算法不对所有历史梯度平方平均,而是使用指数加权平均(EWMA)。

首先初始化:

  • 学习率:\(\eta\)
  • 初始参数:\(w_0\)
  • 小常数:\(\epsilon = 10^{-10}\)(用于数值稳定性)
  • 梯度平方的指数加权项:\(s_0 = 0\)

从 batch 中取 m 个样本,梯度计算公式:

\[g_t = \nabla_w L(w_t) \]

指数加权平均累积梯度平方:

\[s_t = \beta s_{t-1} + (1-\beta)(g_t \odot g_t) \]
符号含义
\(s_t\)梯度平方的指数加权平均(类似“最近梯度平方”)
\(\beta\)一般取 0.9,用于平滑
\(g_t ⊙ g_t\)梯度按元素平方:即 \(g_t^2\)

计算自适应学习率(RMS 部分):

\[\sqrt{s_t + \epsilon} \]

参数更新公式:

\[w_t = w_{t-1} - \eta \frac{g_t}{\sqrt{s_t + \epsilon}} \]

计算示例


比较 RMSProp 与 普通GD 的优化轨迹和收敛情况:

python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数
def loss(w):
    x, y = w
    return 0.5*(100*x**2 + y**2)

def grad(w):
    x, y = w
    return np.array([100*x, y])

def gd(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

def rmsprop(w0, lr, beta, eps, steps):
    w = w0.copy()
    s = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        s = beta*s + (1-beta)*(g*g)
        w = w - lr * g / (np.sqrt(s + eps))
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 参数
w0 = np.array([-3.0, 3.0])
lr = 0.01
beta = 0.9
eps = 1e-8
steps = 200

traj_gd, loss_gd = gd(w0, lr, steps)
traj_rms, loss_rms = rmsprop(w0, lr, beta, eps, steps)

# 图1:轨迹
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)

plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_rms[:,0], traj_rms[:,1], marker='x', markersize=3, linewidth=1, label="RMSProp")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories on Loss Contours")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.axis("equal")
plt.grid(True)
plt.show()

# 图2:Loss vs Iteration
plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_rms, linewidth=1, label="RMSProp loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()

{
    "GD_final_loss": float(loss_gd[-1]),
    "RMSProp_final_loss": float(loss_rms[-1]),
    "GD_final_w": traj_gd[-1].tolist(),
    "RMSProp_final_w": traj_rms[-1].tolist()
}

Adam 算法


公式定义


Adam 是目前深度学习中最常用的优化算法之一,它结合了:

  • Momentum(动量法):使用一阶矩,即梯度的指数加权平均
  • RMSProp:使用二阶矩,即梯度平方的指数加权平均
  • 偏差校正(bias correction):使指数加权平均在前期更准确

Adam 同时维护两个指数加权平均,计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。

一阶矩计算:

\[m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \]

二阶矩计算:

\[s_t = \beta_2 s_{t-1} + (1-\beta_2) g_t^2 \]

其中:

  • \(m_t\):一阶矩(梯度平均)
  • \(s_t\):二阶矩(平方梯度平均)
  • \(g_t\):当前梯度
  • \(\beta_{1}\) 通常取 0.9
  • \(\beta_{2}\) 通常取 0.999
  • 初始化 \(m_0 = 0, s_0 = 0\)

前期的 \(m_t\)\(s_t\) 会偏向 0,因为指数加权平均初始值为 0,所以要进行偏差修正:

\[\hat{m}_t = \frac{m_t}{1-\beta_1^t} \]
\[\hat{s}_t = \frac{s_t}{1-\beta_2^t} \]

参数更新:

\[w_t = w_{t-1} - \alpha \frac{\hat{m}_t}{\sqrt{\hat{s}_t} + \epsilon} \]

其中:

  • \(\hat{m}_t\):偏差修正后的一阶矩,控制方向
  • \(\hat{s}_t\):偏差修正后的二阶矩,控制自适应学习率
  • \(\epsilon\):防止除零(一般为 1e-8)
  • \(\alpha\):初始学习率(一般为 0.001)

计算示例


python
import numpy as np
import matplotlib.pyplot as plt

# 目标函数:强不等轴二次函数(常用于展示优化器差异)
def loss(w):
    x, y = w
    return 0.5 * (100*x**2 + y**2)

def grad(w):
    x, y = w
    return np.array([100*x, y])

# 普通梯度下降
def gd(w0, lr, steps):
    w = w0.copy()
    traj = [w.copy()]
    losses = [loss(w)]
    for _ in range(steps):
        g = grad(w)
        w = w - lr * g
        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# Adam
def adam(w0, lr, beta1, beta2, eps, steps):
    w = w0.copy()
    m = np.zeros_like(w)
    s = np.zeros_like(w)
    traj = [w.copy()]
    losses = [loss(w)]
    for t in range(1, steps+1):
        g = grad(w)
        m = beta1*m + (1-beta1)*g
        s = beta2*s + (1-beta2)*(g*g)

        # 偏差修正
        m_hat = m / (1 - beta1**t)
        s_hat = s / (1 - beta2**t)

        w = w - lr * m_hat / (np.sqrt(s_hat) + eps)

        traj.append(w.copy())
        losses.append(loss(w))
    return np.array(traj), np.array(losses)

# 参数设置
w0 = np.array([-3.0, 3.0])
steps = 200
lr_gd = 0.01
lr_adam = 0.01
beta1 = 0.9
beta2 = 0.999
eps = 1e-8

traj_gd, loss_gd = gd(w0, lr_gd, steps)
traj_adam, loss_adam = adam(w0, lr_adam, beta1, beta2, eps, steps)

xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)

plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_adam[:,0], traj_adam[:,1], marker='x', markersize=3, linewidth=1, label="Adam")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories of GD vs Adam")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.grid(True)
plt.axis("equal")
plt.show()


plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_adam, linewidth=1, label="Adam loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()

{
    "GD_final_loss": float(loss_gd[-1]),
    "Adam_final_loss": float(loss_adam[-1]),
    "GD_final_w": traj_gd[-1].tolist(),
    "Adam_final_w": traj_adam[-1].tolist()
}

学习率调整策略


由于模型训练初期:

  • 梯度大
  • 模型需要快速学习
  • 高学习率有利于跳过糟糕的局部最小值

中后期:

  • 参数需要更精细调整
  • 大学习率会震荡
  • 小学习率更稳定

因此需要对学习率做调整,使其逐渐衰减,有助于模型更好收敛。


等间隔学习率衰减


公式定义


等间隔学习率衰减(StepLR Step Learning Rate Decay)是一种简单有效的学习率调整策略

\[\text{lr}*{new} = \text{lr}*{old} \times \gamma \]

每隔固定的训练步数(或 epoch),就把当前学习率乘以一个衰减系数 \(\gamma\),衰减系数恒定。

lr

│   ──────────┐
│             └───────┐
│                     └───────┐
│                             └───────┐
└────────────────────────────────────────── epoch
      50       100      150      200

计算示例


使用等间学习率衰减对比不衰减学习率的情况:

python
import numpy as np
import matplotlib.pyplot as plt

# Simple quadratic function
def loss(w):
    return (w - 3)**2

def grad(w):
    return 2*(w - 3)

def gd(w0, lr, steps):
    w = w0
    traj = []
    for t in range(steps):
        w = w - lr * grad(w)
        traj.append(loss(w))
    return np.array(traj)

def gd_step_decay(w0, lr, steps, step_size, gamma):
    w = w0
    traj = []
    current_lr = lr
    for t in range(steps):
        if t > 0 and t % step_size == 0:
            current_lr *= gamma
        w = w - current_lr * grad(w)
        traj.append(loss(w))
    return np.array(traj)

# parameters
w0 = 20.0
lr = 0.1
steps = 200
step_size = 50
gamma = 0.5

loss_no_decay = gd(w0, lr, steps)
loss_step_decay = gd_step_decay(w0, lr, steps, step_size, gamma)

plt.figure(figsize=(7,5))
plt.semilogy(loss_no_decay, label="No LR Decay")
plt.semilogy(loss_step_decay, label="Step LR Decay")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("Comparison of Learning Rate Decay")
plt.legend()
plt.grid(True)
plt.show()

{
    "final_loss_no_decay": float(loss_no_decay[-1]),
    "final_loss_step_decay": float(loss_step_decay[-1])
}

指定间隔学习率衰减


公式定义


指定间隔学习率衰减(MultiStep LR)可以在不同的区间轮次上指定学习率:

  • 每隔固定 step_size(例如每 50 epoch)衰减一次
  • 指定多个不等间隔的衰减点

调度方式:

\[lr_{t+1} = \begin{cases} lr_t \cdot \gamma, & t \in \text{milestones}, \\ lr_t, & \text{otherwise}. \end{cases} \]
  • 当训练步数 \(t\) 到达预设的值 \(\text{milestones}\) 时,学习率乘以一个衰减因子 \(\gamma\)
  • 在其他步数,学习率保持不变
python
optim.lr_scheduler.MultiStepLR(
    optimizer,
    milestones = [50, 125, 160],
    gamma = 0.1
)

计算示例


指定间隔,在 milestones=[50, 125, 160] 分别衰减,对比等间隔衰减:

python
import numpy as np
import matplotlib.pyplot as plt

def loss(w):
    return (w - 3)**2

def grad(w):
    return 2 * (w - 3)


def gd_step_lr(w0, lr, steps, step_size, gamma):
    w = w0
    traj_loss = []
    traj_lr = []
    current_lr = lr

    for t in range(steps):
        # 等间隔衰减
        if t > 0 and t % step_size == 0:
            current_lr *= gamma
        
        w = w - current_lr * grad(w)
        traj_loss.append(loss(w))
        traj_lr.append(current_lr)

    return np.array(traj_loss), np.array(traj_lr)


def gd_multi_step_lr(w0, lr, steps, milestones, gamma):
    w = w0
    traj_loss = []
    traj_lr = []
    current_lr = lr

    for t in range(steps):
        # 指定间隔衰减
        if t in milestones:
            current_lr *= gamma
        
        w = w - current_lr * grad(w)
        traj_loss.append(loss(w))
        traj_lr.append(current_lr)

    return np.array(traj_loss), np.array(traj_lr)


w0 = 20.0
lr = 0.1
steps = 200

step_size = 50
gamma = 0.5

milestones = [50, 125, 160]  # 指定衰减时刻


loss_step, lr_step = gd_step_lr(w0, lr, steps, step_size, gamma)
loss_multi, lr_multi = gd_multi_step_lr(w0, lr, steps, milestones, gamma)


plt.figure(figsize=(8,5))
plt.semilogy(loss_step, label="StepLR")
plt.semilogy(loss_multi, label="MultiStepLR")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("Loss Curve: StepLR vs MultiStepLR")
plt.legend()
plt.grid(True)
plt.show()

plt.figure(figsize=(8,5))
plt.plot(lr_step, label="StepLR")
plt.plot(lr_multi, label="MultiStepLR")
plt.xlabel("Iteration")
plt.ylabel("Learning Rate")
plt.title("Learning Rate Schedule")
plt.legend()
plt.grid(True)
plt.show()

指数学习率衰减


公式定义


指数学习率衰减(Exponential LR)是指对于每个训练周期(epoch)按照指数形式衰减,其特点为:

  • 学习率按指数函数平滑下降
  • 每个 epoch 都以相同比例衰减
  • 理论上学习率无限趋近于0但永不等于0

公式为:

\[lr_t = lr_0 \cdot \gamma^t \]

其中:

  • \(lr_t\) 是第 \(t\) 个 epoch 的学习率
  • \(lr_0\) 是初始学习率
  • \(\gamma\) 是衰减率(\(0 < \gamma < 1\)
  • \(t\) 是当前 epoch 数(从0开始)

计算示例


对比等间衰减和指数学习率在函数 \(L(w) = (w - 3)^2\) 上的衰减:

python
import numpy as np
import matplotlib.pyplot as plt

def loss(w):
    return (w - 3)**2

def grad(w):
    return 2 * (w - 3)

def gd_step_lr(w0, lr, steps, step_size, gamma):
    w = w0
    traj_loss = []
    traj_lr = []
    current_lr = lr

    for t in range(steps):
        # StepLR:每 step_size 步衰减
        if t > 0 and t % step_size == 0:
            current_lr *= gamma
        
        w = w - current_lr * grad(w)
        traj_loss.append(loss(w))
        traj_lr.append(current_lr)

    return np.array(traj_loss), np.array(traj_lr)

def gd_exp_lr(w0, lr, steps, gamma):
    w = w0
    traj_loss = []
    traj_lr = []
    current_lr = lr

    for t in range(steps):
        # ExponentialLR:每一步都衰减 lr = lr * gamma
        if t > 0:
            current_lr *= gamma

        w = w - current_lr * grad(w)
        traj_loss.append(loss(w))
        traj_lr.append(current_lr)

    return np.array(traj_loss), np.array(traj_lr)

w0 = 20.0
lr = 0.1
steps = 200

step_size = 50
gamma_step = 0.5       # StepLR 的衰减系数
gamma_exp = 0.98       # ExponentialLR 的指数底

loss_step, lr_step = gd_step_lr(w0, lr, steps, step_size, gamma_step)
loss_exp, lr_exp = gd_exp_lr(w0, lr, steps, gamma_exp)

plt.figure(figsize=(8,5))
plt.semilogy(loss_step, label="StepLR (等间隔衰减)")
plt.semilogy(loss_exp, label="ExponentialLR (指数衰减)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("Loss Curve: StepLR vs ExponentialLR")
plt.legend()
plt.grid(True)
plt.show()

plt.figure(figsize=(8,5))
plt.plot(lr_step, label="StepLR")
plt.plot(lr_exp, label="ExponentialLR")
plt.xlabel("Iteration")
plt.ylabel("Learning Rate")
plt.title("Learning Rate Schedule Comparison")
plt.legend()
plt.grid(True)
plt.show()

正则化


基础概念


一个模型往往从下面两个维度上表现:

  • 泛化能力:模型在训练数据上表现良好,更希望它能够在从未见过的新数据上保持较高的预测准确度
  • 过拟合:模型在训练集上表现很好,但在测试集上表现很差

特别是在神经网络中,由于其极强的表达能力,更容易出现过拟合。

为了减少过拟合、提升泛化能力,人们设计了一系列策略来:

  • 限制模型的复杂度
  • 提高模型对噪声的鲁棒性
  • 让模型更稳健

这些策略被称作正则化,使得模型在不同数据上保持良好的表现。


Droupt 正则化


公式定义


在神经网络中模型参数较多,在数据量不足的情况下,多个神经元相互依赖,形成只对训练集有效的复杂特征组合,很容易导致模型过拟合。

使用 Droupt 方法,在训练过程中,随机“失活”一部分神经元,迫使网络在不稳定结构下学习鲁棒、可复用的特征。

在训练阶段,对每一层(通常是全连接层):

对每个神经元独立采样一个伯努利随机变量:

\[r_i \sim \text{Bernoulli}(p) \]

神经元输出被随机屏蔽:

\[\tilde{h}_i = r_i \cdot h_i \]

其中:

  • \(p\):保留概率(keep probability)
  • \(1-p\):dropout rate

每一次 mini-batch 都对应一个不同的子网络结构。

在推理阶段,不再丢弃神经元,使用完整网络,并进行尺度补偿:

训练时:

\[\tilde{h}_i = \frac{r_i}{p} \cdot h_i \]

测试时:

\[\tilde{h}_i = h_i \]

计算示例


实验:

  • 训练 不使用 Dropout 的模型
  • 训练 使用 Dropout 的模型
  • 记录训练 / 验证损失
python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

# 固定随机种子,保证实验可复现
torch.manual_seed(42)

# =========================================================
# 1. 构造一个小样本、带噪声的数据集(容易过拟合)
# =========================================================
def make_dataset(n_samples=200, input_dim=20):
    X = torch.randn(n_samples, input_dim)
    true_w = torch.randn(input_dim, 1)
    y = (X @ true_w + 0.5 * torch.randn(n_samples, 1) > 0).float()
    return X, y

X, y = make_dataset()

# 划分训练集 / 验证集
X_train, X_val = X[:150], X[150:]
y_train, y_val = y[:150], y[150:]

train_loader = DataLoader(
    TensorDataset(X_train, y_train),
    batch_size=32,
    shuffle=True
)

val_loader = DataLoader(
    TensorDataset(X_val, y_val),
    batch_size=32
)

# =========================================================
# 2. 定义 MLP 模型(是否使用 Dropout 作为参数)
# =========================================================
class MLP(nn.Module):
    def __init__(self, input_dim, use_dropout=False):
        super().__init__()

        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Dropout(p=0.5) if use_dropout else nn.Identity(),

            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(p=0.5) if use_dropout else nn.Identity(),

            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# =========================================================
# 3. 训练 / 验证函数
# =========================================================
def train_one_epoch(model, dataloader, optimizer, criterion):
    model.train()
    total_loss = 0.0

    for x, y in dataloader:
        optimizer.zero_grad()
        y_pred = model(x)
        loss = criterion(y_pred, y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(dataloader)


def evaluate(model, dataloader, criterion):
    model.eval()
    total_loss = 0.0

    with torch.no_grad():
        for x, y in dataloader:
            y_pred = model(x)
            loss = criterion(y_pred, y)
            total_loss += loss.item()

    return total_loss / len(dataloader)

# =========================================================
# 4. 运行实验(是否使用 Dropout)
# =========================================================
def run_experiment(use_dropout, num_epochs=50):
    model = MLP(input_dim=20, use_dropout=use_dropout)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.BCELoss()

    train_losses = []
    val_losses = []

    for epoch in range(num_epochs):
        train_loss = train_one_epoch(
            model, train_loader, optimizer, criterion
        )
        val_loss = evaluate(
            model, val_loader, criterion
        )

        train_losses.append(train_loss)
        val_losses.append(val_loss)

        print(
            f"[{'Dropout' if use_dropout else 'No Dropout'}] "
            f"Epoch {epoch:02d} | "
            f"Train Loss: {train_loss:.4f} | "
            f"Val Loss: {val_loss:.4f}"
        )

    return train_losses, val_losses

# =========================================================
# 5. 运行对比实验
# =========================================================
print("==== Training model WITHOUT Dropout ====")
train_no_dp, val_no_dp = run_experiment(use_dropout=False)

print("\n==== Training model WITH Dropout ====")
train_dp, val_dp = run_experiment(use_dropout=True)

# =========================================================
# 6. 可视化收敛曲线
# =========================================================
epochs = range(1, len(train_no_dp) + 1)

plt.figure(figsize=(10, 6))

# 无 Dropout
plt.plot(epochs, train_no_dp, '--', label="Train Loss (No Dropout)")
plt.plot(epochs, val_no_dp, '--', label="Val Loss (No Dropout)")

# 使用 Dropout
plt.plot(epochs, train_dp, label="Train Loss (Dropout)")
plt.plot(epochs, val_dp, label="Val Loss (Dropout)")

plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Dropout vs No Dropout: Training and Validation Loss")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

批量归一化


公式定义


在深度网络中,随着层数加深,每一层参数更新,会导致后续层输入分布不断变化,这被称为内部协变量偏移,这样会导致模型:

  • 训练不稳定
  • 对初始化和学习率敏感
  • 收敛速度慢

而批量归一化(Batch Normalization)的核心目标是在每一层中,强制中间激活保持稳定分布。

设某一层的输入(线性变换后、激活前)为:

\[z = Wx + b \]

首先计算 mini-batch 的均值与方差:

\[\mu_B = \frac{1}{m} \sum_{i=1}^{m} z_i \]
\[\sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (z_i - \mu_B)^2 \]

对每一层进行标准化:

\[\hat{z}_i = \frac{z_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \]

其中 \(\epsilon\) 是一个很小的常数,防止分母为 \(0\)

然后执行仿射变换,对标准化的数据进行重构:

\[y_i = \gamma \hat{z}_i + \beta \]

其中:

  • \(\gamma\):表示缩放参数
  • \(\beta\):表示平移参数

计算示例


批量归一化主要分为三类:

  • BatchNorm1d:主要用来处理一维数据,例如文本。输入数据的形状要求是(N条样本,num_features多个特征)
  • BatchNorm2d:主要用来处理二维数据,例如图片。输入数据的形状要求是(N,C,H,W)。 N表示N张图片,C表示颜色通道,H图片的高度,W图片的宽度
  • BatchNorm3d:主要处理三维数据,例如:视频、医学影像等。输入数据的形状要求是(N,C,D,H,W)

这里以一维批量归一化进行实验:

python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

torch.manual_seed(42)

# =====================================================
# 1. 构造数据(同样容易过拟合)
# =====================================================
def make_dataset(n_samples=300, input_dim=20):
    X = torch.randn(n_samples, input_dim)
    true_w = torch.randn(input_dim, 1)
    y = (X @ true_w + 0.8 * torch.randn(n_samples, 1) > 0).float()
    return X, y

X, y = make_dataset()

X_train, X_val = X[:200], X[200:]
y_train, y_val = y[:200], y[200:]

train_loader = DataLoader(
    TensorDataset(X_train, y_train),
    batch_size=32,
    shuffle=True
)

val_loader = DataLoader(
    TensorDataset(X_val, y_val),
    batch_size=32
)

# =====================================================
# 2. MLP 模型(是否使用 BatchNorm)
# =====================================================
class MLP(nn.Module):
    def __init__(self, input_dim, use_bn=False):
        super().__init__()

        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.BatchNorm1d(128) if use_bn else nn.Identity(),
            nn.ReLU(),

            nn.Linear(128, 64),
            nn.BatchNorm1d(64) if use_bn else nn.Identity(),
            nn.ReLU(),

            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# =====================================================
# 3. 训练 / 验证逻辑
# =====================================================
def train_one_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss = 0.0

    for x, y in loader:
        optimizer.zero_grad()
        y_pred = model(x)
        loss = criterion(y_pred, y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(loader)


def evaluate(model, loader, criterion):
    model.eval()
    total_loss = 0.0

    with torch.no_grad():
        for x, y in loader:
            y_pred = model(x)
            loss = criterion(y_pred, y)
            total_loss += loss.item()

    return total_loss / len(loader)

# =====================================================
# 4. 运行实验
# =====================================================
def run_experiment(use_bn, epochs=40):
    model = MLP(input_dim=20, use_bn=use_bn)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.BCELoss()

    train_losses, val_losses = [], []

    for epoch in range(epochs):
        train_loss = train_one_epoch(
            model, train_loader, optimizer, criterion
        )
        val_loss = evaluate(
            model, val_loader, criterion
        )

        train_losses.append(train_loss)
        val_losses.append(val_loss)

        print(
            f"[{'BatchNorm' if use_bn else 'No BN'}] "
            f"Epoch {epoch:02d} | "
            f"Train: {train_loss:.4f} | "
            f"Val: {val_loss:.4f}"
        )

    return train_losses, val_losses

# =====================================================
# 5. 对比运行
# =====================================================
print("==== Training WITHOUT Batch Normalization ====")
train_no_bn, val_no_bn = run_experiment(use_bn=False)

print("\n==== Training WITH Batch Normalization ====")
train_bn, val_bn = run_experiment(use_bn=True)

# =====================================================
# 6. 可视化
# =====================================================
epochs = range(1, len(train_bn) + 1)

plt.figure(figsize=(10, 6))

plt.plot(epochs, train_no_bn, '--', label="Train (No BN)")
plt.plot(epochs, val_no_bn, '--', label="Val (No BN)")

plt.plot(epochs, train_bn, label="Train (BN)")
plt.plot(epochs, val_bn, label="Val (BN)")

plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Batch Normalization vs No Normalization")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()