神经网络优化器与正则化方法


一、梯度下降优化算法

1.为什么需要优化器

标准梯度下降在训练神经网络时经常会遇到三类问题:

  • 平缓区域:梯度值绩效,参数几乎原地踏步,训练卡住不动
  • 鞍点:梯度为0,参数迭代停止
  • 局部最小值:继续训练损失值反而更高,以为已经是最优解,实则还有全局更优解
    在这里插入图片描述
    为了优化以上问题,Momentum(动量法)、AdaGrad(自适应梯度算法)、RMSProp(均方根传播)、Adam(自适应矩估计)等优化算法应运而生。

2.指数加权平均(EWA)

在讲具体的算法之前,需要先搞懂什么是指数加权平均(Exponentially Weighted Average)。

核心思想:当我们在预测明天的气温的时候,不能只看几天的气温,还要看之前的。只不过,距离今天越远的,对结果的影响就越小

St=β⋅St−1+(1−β)⋅YtS_t = \beta \cdot S_{t-1} + (1-\beta) \cdot Y_tSt=βSt1+(1β)Yt

  • StS_tSt:t时刻的加权平均值(可以理解为平滑后的梯度)
  • YtY_tYt:t时刻的真实观测值(当前梯度)
  • β\betaβ:权重系数,越大曲线越平缓,通常取0.9
import torch
import matplotlib.pyplot as plt

# 用来正常显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False    # 用来正常显示负号

ELEMENT_NUMBER = 30

# 1 模拟随机波动的温度
torch.manual_seed(0)
# 生成30天的气温
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
# 生成1~30的一维张量
days = torch.arange(start=1, end=ELEMENT_NUMBER + 1, step=1)
# 绘制原始数据的折线图
plt.plot(days, temperature, color="r", label="原始数据")
# 绘制原始数据的散点图
plt.scatter(days, temperature, color="r")

# 2 指数加权平均
beta = 0.9
exp_weight_avg = []
for idx, temp in enumerate(temperature, 1):
    """
    enumerate()用于在遍历可迭代对象(如列表、元组、字符串等)时
    同时获取元素及其对应的索引
    """
    if idx == 1:
        # 第一个元素的指数加权平均就是他自己,不用计算
        exp_weight_avg.append(temp)
        continue
    # 指数加权平均
    new_temp = exp_weight_avg[-1] * beta + (1 - beta) * temp
    exp_weight_avg.append(new_temp)

# 绘制指数加权平均后的折线图
plt.plot(days, exp_weight_avg, color="b", label=f"EWA(beta={beta})")
plt.legend()
plt.show()

在这里插入图片描述

β\betaβ越大,曲线越平缓;β\betaβ越小,曲线越贴近原始数据。


3.Momentum—动量法

普通的SGD(随机梯度下降)只用当前时刻的梯度方向更新参数,而动量法会把历史梯度也考虑进来。
动量法:
st=βst−1+(1−β)gts_t = \beta s_{t-1} + (1-\beta) g_tst=βst1+(1β)gt
SGD:
wt=wt−1−ηstw_t = w_{t-1} - \eta s_twt=wt1ηst

想象一个下山的过程:普通SGD像一个每一步都重新判断方向的人,而动量法想一个带“惯性”的人。即使当前坡度为0,由于之前积累的速度,动量法依然能凭借惯性冲出鞍点或局部最小值。

为什么动量法能跨过鞍点?
在鞍点处梯度为0,参数本应停止更新。但是动量法的计算方法会考虑到历史的梯度值——而不是仅参考当前梯度值,因此,动量法可以凭借历史的梯度值跨过鞍点。
为什么动量法能减少震荡?
mini-batch由于每次随机获取样本的一批数据,梯度方向波动大。动量法通过指数加权移动平均平滑了前进方向,训练过程更稳更快。

import torch
import torch.nn as nn

# 初始化权重 w = [1.0],损失函数 = w²/2
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()

# 使用带动量的SGD,beta=0.9
optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)

# 第1次更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第1次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

# 第2次更新(用更新后的w重新算loss)
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第2次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

输出示例:第1次: 梯度=1.000000, 更新后w=0.990000;第2次: 梯度=0.990000, 更新后w=0.971100


4.AdaGrad—自适应学习率开山之作

AdaGrad的核心思路:每个参数都有自己的学习率,梯度大的参数步子小一点,梯度小的参数步子大一点
AdaGrad通过累计历史梯度的平方来调整学习率:
st=st−1+gt⊙gts_t = s_{t-1} + g_t \odot g_tst=st1+gtgt

ηt=ηst+σ\eta_t = \frac{\eta}{\sqrt{s_t} + \sigma}ηt=st +ση

wt=wt−1−ηt⊙gtw_t = w_{t-1} - \eta_t \odot g_twt=wt1ηtgt
缺点:由于StS_tSt单调递增,导致模型训练到后面,学习率越来越小,模型就渐渐学不动了。

import torch

w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()

optimizer = torch.optim.Adagrad([w], lr=0.01)

optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第1次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第2次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

5.RMSProp—AdaGrad的进化版

AdaGrad的问题是StS_tSt单调递增,导致学习率早衰。RMSProp借鉴了EWA的思路,用指数加权平均替代简单累加:
st=βst−1+(1−β)(gt⊙gt)s_t = \beta s_{t-1} + (1-\beta)(g_t \odot g_t)st=βst1+(1β)(gtgt)

ηt=ηst+σ\eta_t = \frac{\eta}{\sqrt{s_t} + \sigma}ηt=st +ση

wt=wt−1−ηt⊙gtw_t = w_{t-1} - \eta_t \odot g_twt=wt1ηtgt
这样StS_tSt不再单调递增,而是受一段时间内的梯度强度影响,越近的梯度影响越大。

import torch

w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()

optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)

optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第1次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第2次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

6.Adam—Momentum + RMSProp的集大成者

Adam(Adaptive Moment Estimation)把两条路线合在一起:

  • Momentum路线:用EWA修正梯度方向(一阶距)
  • RMSProp路线:用EWA修正学习率(二阶距)

mt=β1mt−1+(1−β1)gt(梯度的一阶矩)m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \quad \text{(梯度的一阶矩)}mt=β1mt1+(1β1)gt(梯度的一阶矩)

st=β2st−1+(1−β2)(gt⊙gt)(梯度的二阶矩)s_t = \beta_2 s_{t-1} + (1-\beta_2)(g_t \odot g_t) \quad \text{(梯度的二阶矩)}st=β2st1+(1β2)(gtgt)(梯度的二阶矩)

由于 m 和 s 初始化为 0,开始几步会有偏差,所以需要偏差校正

mt^=mt1−β1t,st^=st1−β2t\hat{m_t} = \frac{m_t}{1 - \beta_1^t}, \quad \hat{s_t} = \frac{s_t}{1 - \beta_2^t}mt^=1β1tmt,st^=1β2tst

wt=wt−1−ηmt^st^+σw_t = w_{t-1} - \eta \frac{\hat{m_t}}{\sqrt{\hat{s_t}} + \sigma}wt=wt1ηst^ +σmt^

Adam相当于同时拥有了“方向感(Momentum)”和“步伐感(RMSProp)”,是比较常用的优化器。

import torch

w = torch.tensor([1.0], requires_grad=True)
loss = ((w ** 2) / 2.0).sum()

# betas = (beta1, beta2),分别控制一阶、二阶矩的EWA系数
optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])

optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第1次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'第2次: 梯度={w.grad.item():.6f}, 更新后w={w.detach().item():.6f}')

优化器 核心机制 优点 缺点
SGD 标准梯度 简单可控 易卡在鞍点/局部最小,震荡大
Momentum 梯度 EWA 跨鞍点、减少震荡 仍需手动调学习率
AdaGrad 自适应学习率 无需手动调学习率 学习率早衰,后期无力
RMSProp 梯度平方 EWA 自适应学习率 + 不早衰 收敛仍较慢
Adam Momentum + RMSProp 收敛快、自适应、鲁棒 容易过拟合(默认参数偏大)

二、学习率衰减策略

训练前期大步快跑,训练后期小步微调——这是学习率衰减的核心策略

1.等间隔衰减StepLR

每走过step_sizeepoch,学习率乘以gamma

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

LR = 0.1
max_epoch = 200
iteration = 10

w = torch.tensor([1.0], requires_grad=True)
y_true = torch.tensor([0])
x = torch.tensor([1.0])
optimizer = optim.SGD([w], lr=LR, momentum=0.9)

# 每50个epoch,学习率乘以0.5
scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)

lr_list, epoch_list = [], []
for epoch in range(max_epoch):
    lr_list.append(scheduler_lr.get_last_lr()[0])
    epoch_list.append(epoch)
    for i in range(iteration):
        loss = ((w * x - y_true) ** 2) / 2.0
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    scheduler_lr.step()

plt.plot(epoch_list, lr_list, label="Step LR (step=50, gamma=0.5)")
plt.xlabel("Epoch")
plt.ylabel("Learning Rate")
plt.legend()
plt.show()

在这里插入图片描述

3.指定间隔衰减MultiStepLR

不是均匀衰减,只在指定的epoch节点才调整:

scheduler_lr = optim.lr_scheduler.MultiStepLR(
    optimizer, milestones=[50, 145, 160], gamma=0.5
)

适用于我们知道模型在哪些节点需要降低学习率的场景,比等间隔更灵活

在这里插入图片描述

3.指数衰减ExponentialLR

每个epoch都衰减:
lrt=lr0×γepochlr_{t} = lr_{0} \times \gamma^{epoch}lrt=lr0×γepoch

scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)

在这里插入图片描述

gamma越接近1,衰减越慢;越接近0,衰减越快。


三、正则化方法

1.什么是正则化?

正则化的目标只有一个:提升模型在新样本上的泛化能力,也就是减少过拟合。
神经网络在参数多,数据少的情况下,过拟合是常态。常用的正则化策略有:

  • 范数惩罚(L1/L2)
  • Dropout(随机失活)
  • Batch Normalization(批量归一化)

2.Dropout——随机杀死神经元

在训练时:每个神经元有概率ppp被关闭(激活值置为0),存活下来的神经元缩放11−p\frac{1}{1-p}1p1
在测试时:所有神经元都工作,但输出需要乘(1−p)(1-p)(1p)(PyTorch默认训练时做了缩放,所以测试时无需额外处理)

import torch
import torch.nn as nn

dropout = nn.Dropout(p=0.4)  # 40% 的神经元被失活
inputs = torch.randint(0, 10, size=[1, 4]).float()
layer = nn.Linear(4, 5)
y = layer(inputs)

print("未失活 FC 层输出:", y)
y = dropout(y)
print("失活后 FC 层输出:", y)
"""
未失活 FC 层输出: tensor([[ 3.7053, -6.0588,  2.4036,  1.0904,  1.6961]],
       grad_fn=<AddmmBackward0>)
失活后 FC 层输出: tensor([[  6.1754, -10.0980,   0.0000,   1.8173,   2.8268]],
       grad_fn=<MulBackward0>)
"""

3.Batch Normalization(BN层)

BN的操作分两步:

  • 标准化:把输入变成均值0,方差1的分布
  • 重构:再做一个线性变换(缩放+平移),让模型自己学习最优的分布
    y=x−E(x)Var(x)+ϵ⋅λ+βy = \frac{x - E(x)}{\sqrt{Var(x) + \epsilon}} \cdot \lambda + \betay=Var(x)+ϵ xE(x)λ+β

其中 λ\lambdaλ(weight)和 β\betaβ(bias)是可学习参数。

import torch
import torch.nn as nn

# BatchNorm2d 用于 2D 特征图(卷积层后),num_features = 通道数
m = nn.BatchNorm2d(2, eps=1e-05, momentum=0.1, affine=True)
input = torch.randn(1, 2, 3, 4)
output = m(input)

print("input:", input)
print("output:", output)

BN层为什么有效?

  • 每一层输入的分布被拉回标准正态分布,避免梯度消失/爆炸
  • 相当于给每层输入做了一次预处理,让深层网络更容易训练
  • 在计算机视觉领域使用较多

四、总结

知识点 一句话总结
指数加权平均 距离越远权重越小,用来平滑序列数据
Momentum 对梯度做 EWA,赋予模型"下山惯性"
AdaGrad 对每个参数自适应学习率,但易早衰
RMSProp 用 EWA 替代累加,修复 AdaGrad 早衰问题
Adam Momentum + RMSProp,当代默认首选
StepLR 每 N 个 epoch 固定比例衰减学习率
MultiStepLR 在指定 epoch 节点衰减,更灵活
ExponentialLR 每个 epoch 连续指数衰减
Dropout 随机失活神经元,防止协同依赖
BN 层 标准化 + 可学习仿射变换,稳定训练
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐