- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
本文基于 NYU Deep Learning Spring 2020(NYU-DLSP20)第 5 周第 1 讲的课程笔记(仓库中的 docs/en/week05/05-1.md 与 docs/es/week05/05-1.md,讲师 Aaron Defazio)展开。文章以优化方法中最基础也最"原始"的梯度下降(Gradient Descent)为起点,依次讲透随机梯度下降(SGD)、小批量训练(Mini-batching)与动量(Momentum)的数学形式、直观理解与工程调参规律,并结合本仓库 extra/utils/optim.py 与 extra/optimization.ipynb 中的可运行代码,从零实现 vanilla SGD 与 SGD+Momentum 两个优化器。读完本文,你将掌握这三类优化器的更新公式、核心超参数(学习率 γ、阻尼因子 β)的调节准则,以及它们在一个 2D 二次型目标上被可视化的完整实验方法。
1. 梯度下降(Gradient Descent):最基础、也是"最差"的优化器
课程笔记开宗明义:我们从一个最基础、同时也是最"差"(后续会给出理由)的方法开始研究优化方法,它就是梯度下降。
1.1 问题定义与迭代式
我们要解决的核心问题是:
$$ \min_w f(w) $$
即寻找使目标函数 $f(w)$ 最小化的参数 $w$。梯度下降给出如下的迭代解:
$$ w_{k+1} = w_k - \gamma_k \nabla f(w_k) $$
其中:
- $w_{k+1}$:第 $k$ 次迭代后更新得到的参数值;
- $w_k$:第 $k$ 次迭代前的初始参数值;
- $\gamma_k$:步长(step size),即学习率;
- $\nabla f(w_k)$:目标函数 $f$ 在 $w_k$ 处的梯度。
1.2 局部视角:为什么只能沿负梯度方向走
这里的基本假设是函数 $f$连续且可微。我们的目标是找到优化函数的最低点(即"山谷"),但通向山谷的真实方向是未知的——我们只能局部地观测函数,因此负梯度方向就是我们能获得的最好信息。朝该方向迈出一小步,只会让我们更接近最小值。每走一小步,我们就重新计算新的梯度,再沿新方向移动一小段距离,如此反复直至抵达山谷。
本质上,梯度下降所做的全部事情就是:沿着最陡下降方向(负梯度方向)前进。
1.3 步长 γ:三种情形与对数刻度调参
迭代更新式中的参数 $\gamma$ 被称为步长(step size)。我们通常不知道最优步长的取值,因此必须尝试不同的值。标准做法是在对数刻度(log-scale)上试一组值,然后选取效果最好的那一个(课程用一维二次函数演示了这些情形):
- 学习率过低:会朝最小值稳定前进,但耗时可能远超理想情况;
- 接近最优的学习率:一般很难(甚至不可能)得到一个恰好直接把我们带到最小值的步长;理想上我们希望步长略大于最优值,实践中这能带来最快的收敛;
- 学习率过大:迭代点会离最小值越来越远,最终导致发散(divergence)。
因此,工程上的经验准则是:实际使用的学习率,应取"刚刚小于会发散的那个值"——即在发散临界点附近往回退一小步。
2. 随机梯度下降(Stochastic Gradient Descent,SGD)
2.1 用单样本梯度做无偏估计
在 SGD 中,我们用真实梯度向量的随机估计来替代真实梯度向量。具体到神经网络,这个随机估计就是单个数据点(单个样本)的损失梯度。
设 $f_i$ 为网络在第 $i$ 个样本上的损失:
$$ f_i = l(x_i, y_i, w) $$
我们最终要最小化的函数 $f$ 是所有样本上的总损失:
$$ f = \frac{1}{n}\sum_i^n f_i $$
在 SGD 中,我们根据 $f_i$ 的梯度来更新权重(而不是总损失 $f$ 的梯度):
$$ \begin{aligned} w_{k+1} &= w_k - \gamma_k \nabla f_i(w_k) & \quad\text{(i 均匀随机选取)} \end{aligned} $$
2.2 期望步长与全批量一致
如果 $i$ 是随机选取的,那么 $f_i$ 是 $f$ 的一个**有噪声但无偏(unbiased)**的估计,数学上写作:
$$ \mathbb{E}[\nabla f_i(w_k)] = \nabla f(w_k) $$
由此,SGD 第 $k$ 步的期望与完整梯度下降的第 $k$ 步相同:
$$ \mathbb{E}[w_{k+1}] = w_k - \gamma_k \mathbb{E}[\nabla f_i(w_k)] = w_k - \gamma_k \nabla f(w_k) $$
因此,任何一次 SGD 更新,在期望意义下都等同于一次全批量(full-batch)更新。
2.3 Annealing:噪声带来的"退火"收益
然而,SGD 并不仅仅是"带噪声的更快梯度下降"。除了更快之外,SGD 还能得到比全批量梯度下降更好的结果:SGD 更新中的噪声可以帮助我们避开浅的局部最小值,找到更优(更深)的最小值。这一现象被称为退火(annealing)。
2.4 SGD 的四大优势
课程总结的 SGD 优势如下:
- 大量冗余信息被消除:不同样本之间存在大量重复信息,SGD 避免了大量冗余计算;
- 早期质量几乎不损失:在训练初期,相对于梯度中的信息量,噪声很小,因此一步 SGD几乎与一步 GD 同样好;
- 退火效应:SGD 更新中的噪声可以防止收敛到坏的(浅的)局部最小值;
- 计算成本大幅降低:SGD 每次计算梯度不需要遍历所有数据点(只用一个样本),因此计算代价急剧下降。
3. 小批量训练(Mini-batching)
在mini-batching中,我们不再只计算单个样本的损失,而是考虑多个随机选取的样本上的损失,这会降低每次更新中的噪声:
$$ w_{k+1} = w_k - \gamma_k \frac{1}{|B_i|} \sum_{j \in B_i}\nabla f_j(w_k) $$
其中 $B_i$ 为第 $i$ 个 mini-batch,$|B_i|$ 为其大小。
使用 mini-batch 而非单样本,往往能更好地利用硬件:例如,用单个样本训练时 GPU 的利用率很低。分布式网络训练技术会把一个大的 mini-batch拆分到集群中的多台机器上,然后再聚合得到的梯度。课程中还提到,Facebook 曾借助分布式训练在一小时内完成对 ImageNet 数据的网络训练(此为课程讲义中的历史陈述)。
此外课程给出一个重要告诫:永远不要对全尺寸批(full-sized batch)使用梯度下降。如果确实需要在完整批量上训练,请使用名为LBFGS的优化技术——PyTorch 和 SciPy 都提供了 LBFGS 的实现。
4. 动量(Momentum)
4.1 双迭代变量与更新规则
在 Momentum 中,我们有两个迭代变量($p$ 和 $w$)而非一个。更新规则如下:
$$ \begin{aligned} p_{k+1} &= \hat{\beta_k}p_k + \nabla f_i(w_k) \ w_{k+1} &= w_k - \gamma_kp_{k+1} \ \end{aligned} $$
$p$ 被称为SGD 动量(SGD momentum)。在每一步更新中,我们先把旧的动量值衰减一个因子 $\beta$(取值在 0 和 1 之间),再把随机梯度加进去。$p$ 可以看作梯度的运行平均(running average)。最后,我们让 $w$ 沿着新动量 $p$ 的方向移动。
4.2 等价形式:随机重球法(Stochastic Heavy Ball Method)
另一种写法在数学上与前面的形式完全等价:
$$ \begin{aligned} w_{k+1} &= w_k - \gamma_k\nabla f_i(w_k) + \beta_k(w_k - w_{k-1}) & 0 \leq \beta < 1 \end{aligned} $$
在这种形式下,下一步是上一步的方向($w_k - w_{k-1}$)与新的负梯度方向的组合。
4.3 物理直觉:重球滚下山坡
SGD Momentum 与物理学中的动量概念相似:优化过程就像一个重球滚下山坡。动量让球保持它原本的运动方向,而梯度可以被理解为把球推向另一个方向的力。与在行进方向上做出剧烈改变(就像只用 SGD 时那样)相比,动量只做温和的改变——它抑制了单独使用 SGD 时常见的振荡。
4.4 阻尼因子 β 与调参规律
参数 $\beta$ 被称为阻尼因子(Dampening Factor),其约束与含义如下:
- $\beta$必须大于 0:因为若 $\beta = 0$,就退化成了普通的梯度下降;
- $\beta$必须小于 1:否则一切都会"爆炸"(不收敛);
- 较小的 $\beta$会让方向变化得更快;较大的 $\beta$则需要更长时间才能"转弯"。
实践准则(Practical guidelines):
- Momentum 几乎总是应该配合 SGD 使用;
- $\beta = 0.9$ 或 $0.99$ 几乎总是能工作得很好;
- 增大动量参数时通常需要减小步长以保证收敛:若 $\beta$ 从 0.9 调到 0.99,学习率必须缩小 10 倍(factor of 10)。
5. 为什么 Momentum 有效?
5.1 加速(Acceleration)与 Nesterov 动量
以下为Nesterov 动量的更新规则:
$$ p_{k+1} = \hat{\beta_k}p_k + \nabla f_i(w_k) \ w_{k+1} = w_k - \gamma_k(\nabla f_i(w_k) +\hat{\beta_k}p_{k+1}) $$
如果非常仔细地选择常数,Nesterov 动量可以获得加速收敛——但这一点只对凸问题成立,并不适用于神经网络。
很多人说普通动量也是一种加速方法,但实际上它只对二次函数(quadratics)加速。此外,加速方法与 SGD 配合并不好:SGD 本身带有噪声,而加速方法并不擅长处理噪声。因此,虽然 Momentum SGD 中存在一定的加速成分,但加速本身不足以解释该技术的高性能。
5.2 噪声平滑(Noise Smoothing)
更实际、也更可能成立的原因是噪声平滑(Noise Smoothing):
Momentum 对梯度做平均——每次更新使用的,是梯度的运行平均。理论上,为了让 SGD 正常工作,我们应当对所有步的更新取平均:
$$ \bar w_k = \frac{1}{K} \sum_{k=1}^K w_k $$
SGD 配合 Momentum 的最大优点正在于:这个平均过程不再必要了。Momentum 为优化过程添加了平滑,使得每一次更新本身都是对解的一个良好近似;而纯 SGD 则希望你先平均一大堆更新,再朝那个方向迈一步。
结论:加速与噪声平滑共同贡献了 Momentum 的高性能。直观上,纯 SGD 在初始阶段朝向解的进展不错,但当它到达"碗底"(谷底)时会在底部来回弹跳;调整学习率只会让它弹得更慢。而 Momentum 把每一步都平滑掉了,从而不再有弹跳。
6. 仓库实战:从零实现 SGD 与 SGD+Momentum
课程笔记讲的是理论,而本仓库的 extra/optimization.ipynb 提供了完整的动手练习:在一个病态条件数较高的 2D 二次型目标上,亲手实现并可视化这些优化器。
6.1 可视化基础设施:优化器基类与二次型目标
extra/utils/optim.py 首先定义了一个继承自torch.optim.Optimizer的基类Optim(见 extra/utils/optim.py)。它的step(closure)遍历每个参数组与每个参数,把真正更新逻辑委托给my_step(p, state, group)——子类只需实现这个方法即可完成一个优化器:
class Optim(Optimizer): def __init__(self, params, defaults): super(Optim, self).__init__(params, defaults) def step(self, closure=None): loss = None if closure is not None: loss = closure() for group in self.param_groups: for p in group['params']: if p.grad is None: continue self.my_step(p, self.state[p], group) return loss def my_step(self, p, state, group): raise NotImplementedError可视化实验使用的目标函数是一个二次型(见 extra/utils/optim.py):
A = torch.tensor([[1.0, 0.0,], [0.0, 5.0]]) b = torch.tensor([0.0, 0.0]) def objective(x, y): xy = torch.tensor([x, y]) return (0.5 * xy @ (A @ xy) + b @ xy).item()注意 $A = \mathrm{diag}(1, 5)$:该函数在 $x$ 方向曲率小、在 $y$ 方向曲率大,是一个典型的病态(ill-conditioned)二次问题,正好用来放大不同优化器在方向选择上的差异。output(opt, nsteps=10, noise=0.0)从 $w = (1,1)$ 出发迭代指定步数并绘制等高线轨迹;当noise > 0时,线性项会加上torch.normal(b, noise)的随机扰动,从而在损失面上注入噪声(extra/utils/optim.py)。
6.2 实现 vanilla SGD
Notebook 中SGDOptimizer的实现(extra/optimization.ipynb)与讲义公式 $w_{k+1} = w_k - \gamma_k \nabla f_i(w_k)$ 一一对应:
class SGDOptimizer(Optim): def __init__(self, params, lr): defaults = dict(lr=lr) super(SGDOptimizer, self).__init__(params, defaults) def my_step(self, p, state, group): lr = group['lr'] d_p = p.grad.data p.data.add_(-group['lr'], d_p)Notebook 用两个学习率做了对照实验:output(SGDOptimizer([w], lr=0.33333))与output(SGDOptimizer([w], lr=0.1)),并指出:使用大的学习率时,优化方向的方差很大;使用小的学习率时,又无法有效地最小化函数——这正是讲义中"步长过大发散、过小太慢"两种情形的直观呈现。
6.3 实现 SGD + Momentum
SGDMomentumOptimizer的实现(extra/optimization.ipynb)与讲义的双变量更新式 $p_{k+1} = \hat{\beta_k}p_k + \nabla f_i(w_k)$、$w_{k+1} = w_k - \gamma_k p_{k+1}$ 一致:用state['v']保存动量缓冲,w -= lr * v:
class SGDMomentumOptimizer(Optim): def __init__(self, params, lr, momentum): defaults = dict(lr=lr, momentum=momentum) super(SGDMomentumOptimizer, self).__init__(params, defaults) def my_step(self, p, state, group): lr = group['lr'] momentum = group['momentum'] d_p = p.grad.data if 'v' not in state: state['v'] = torch.clone(d_p).detach() else: state['v'] = d_p + momentum*state['v'] d_p = state['v'] p.data.add_(-group['lr']*d_p)从源码结构看,该实现与 PyTorch 内置优化器torch.optim.SGD的接口风格一致(params、lr、momentum三个超参数),其中momentum即讲义中的阻尼因子 $\beta$。
6.4 学习率与动量的联动调整
讲义给出了定性的调参法则($\beta$ 增大时学习率要缩小,$\beta$ 从 0.9 变到 0.99 时学习率约缩小 10 倍);Notebook 则给出了一个可操作的定量经验式:
$$ lr_{new} = lr_{old} * (1 - m) $$
其中 $m$ 为动量值。Notebook 的实验调用即遵循这一规律,例如output(SGDMomentumOptimizer([w], lr=0.15*(1-0.5), momentum=0.5))、lr=0.15*(1-0.25), momentum=0.25、lr=0.15*(1-0.75), momentum=0.75三组对照。Notebook 同时坦率地提醒:调节这些超参数本身就是有效训练神经网络不可或缺的一部分。
6.5 带噪声场景下的对比实验
为了凸显 Momentum 的"噪声平滑"优势,Notebook 在损失面上注入噪声后做了两组实验(nsteps=30, noise=1.0):
output(SGDMomentumOptimizer([w], lr=0.1, momentum=0.0), nsteps=30, noise=1.0) # vanilla SGD output(SGDMomentumOptimizer([w], lr=0.1*(1-0.8), momentum=0.8), nsteps=30, noise=1.0) # SGD + momentumNotebook 明确指出,在注入噪声、增加损失面非凸性之后,Momentum 能比 vanilla SGD 更有效地忽略这些噪声——这正是讲义第 5 节"噪声平滑"结论的可运行佐证。运行本实验时,将 notebook 置于extra/目录下(from utils.optim import Optim, output, w的导入依赖该目录布局),依赖环境可参考仓库根目录的 environment.yml。
7. 小结
| 方法 | 更新规则 | 关键超参数 | 适用要点 |
|---|---|---|---|
| 梯度下降 GD | $w_{k+1} = w_k - \gamma_k \nabla f(w_k)$ | 学习率 $\gamma_k$ | 全批量、计算昂贵,步长过大易发散;不要用于全尺寸批量 |
| 随机梯度下降 SGD | $w_{k+1} = w_k - \gamma_k \nabla f_i(w_k)$($i$ 均匀随机) | 学习率 $\gamma_k$ | 单样本梯度、无偏但有噪声,噪声带来退火(annealing)收益 |
| Mini-batching | $w_{k+1} = w_k - \gamma_k \frac{1}{|B_i|}\sum_{j \in B_i}\nabla f_j(w_k)$ | 批量大小 $|B_i|$ | 降低噪声、充分利用 GPU、支持分布式梯度聚合 |
| SGD + Momentum | $p_{k+1} = \hat{\beta_k}p_k + \nabla f_i(w_k)$;$w_{k+1} = w_k - \gamma_k p_{k+1}$ | 阻尼因子 $\beta \in (0,1)$ | 抑制振荡、平滑噪声;$\beta=0.9/0.99$ 常用;增大 $\beta$ 需相应缩小学习率 |
本文完整覆盖了 NYU-DLSP20 第 5 周第 1 讲的核心内容:从梯度下降的问题定义、迭代式与步长调参,到 SGD 的无偏估计与退火收益,再到小批量训练与 Momentum 的数学形式、物理直觉、阻尼因子调参规律及其"加速 + 噪声平滑"两大生效机理。配合仓库 extra/utils/optim.py 与 extra/optimization.ipynb 的可运行实现,你可以亲手在二次型目标上复现"大学习率发散、小学习率过慢、Momentum 平滑噪声"等全部经典现象。本仓库配套 notebook 还进一步实现了 RMSProp 与 Adam(属于下一讲"优化技术 II"的内容),可作为后续延伸阅读。
- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
相关推荐
NYU-DLSP20 优化技术指南(一):梯度下降、随机梯度下降与动量机制
NYU DLSP20 优化技术指南(一):梯度下降、随机梯度下降与动量机制 本文源自 NYU Deep Learning Spring 2020 课程笔记 do
示例工程NYU-DLSP20 第 2 周(一):梯度下降与反向传播算法——从计算图到 PyTorch 实现
NYU DLSP20 第 2 周(一):梯度下降与反向传播算法——从计算图到 PyTorch 实现 本文整理自 NYU Deep Learning Spring
示例工程NYU-DLSP20 第二周:梯度下降、反向传播与 PyTorch 神经网络实战指南
NYU DLSP20 第二周:梯度下降、反向传播与 PyTorch 神经网络实战指南 本篇技术指南基于 NYU Deep Learning Spring 202
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考