- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
本周课程是 NYU-DLSP20(NYU Deep Learning Spring 2020,对应本仓库)优化与卷积主题的核心章节,横跨三大部分:Lecture A讲解梯度下降(GD)、随机梯度下降(SGD)与动量(Momentum)三类基础优化技术;Lecture B讲解 RMSprop、ADAM 等自适应方法、归一化层(Normalization Layers)以及深度学习加速 MRI 重建的工业级案例;Practicum则回到动手环节,手推 1D 卷积、用 PyTorch 验证 kernel 与输出维度、理解自动微分引擎并编写自定义梯度模块。读完本文,你将掌握优化器的数学原理、调参经验、归一化层的选型依据,并能用 PyTorch 独立复现卷积尺寸计算与autograd自定义梯度。
一、Lecture A:从梯度下降到带动量的 SGD
1.1 梯度下降(Gradient Descent):最基础也最"差"的方法
我们研究优化方法,通常从最基础也最朴素的方法开始——梯度下降。其目标是最小化一个连续可微函数:
问题:
$$ \min_w f(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$ 处的梯度。
这里的基本假设是函数 $f$ 连续且可微。我们的目标是找到优化函数的"谷底",但通往谷底的真实方向未知,只能做局部观察——因此负梯度方向是我们掌握的最好信息。沿该方向迈出一小步,就能更接近最小值;走完一步后重新计算梯度,再沿新方向移动,如此反复直至抵达谷底。本质上,梯度下降就是在沿着**最陡下降方向(负梯度方向)**前进。
$\gamma$ 被称为步长(学习率)。通常我们并不知道最优步长的取值,因此必须尝试不同数值;标准做法是在对数尺度(log-scale)上试一批值,再选用效果最好的一个。以 1D 二次函数为例,存在几种典型场景(见下图):
- 学习率过低:向最小值稳步前进,但耗时远超理想情况;
- 学习率略高于最优:实际上收敛最快,是最理想的选择;
- 学习率过大:迭代点离最小值越来越远,导致发散。
实践中我们通常希望学习率"比发散阈值略小一点"。
图片出处:本仓库 docs/images/week05/05-1/step-size.png,对应英文原笔记 docs/en/week05/05-1.md。
1.2 随机梯度下降(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} $$
若 $i$ 随机选取,则 $f_i$ 是 $f$ 的一个带噪声但无偏的估计,数学上写作:
$$ \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 更新在期望意义下都等价于全批量更新。但 SGD 并不仅仅是"更快的带噪梯度下降"——它还能带来更好的结果:SGD 的噪声可以帮助我们跳过较浅的局部最小值,找到更深的、更优的最小值。这一现象称为退火(annealing)。
总结 SGD 的优势:
- 各样本之间存在大量冗余信息,SGD 避免了大量冗余计算;
- 训练初期,噪声相对梯度中的信息量很小,因此一步 SGD几乎与一步 GD 同样有效;
- 退火效应——SGD 更新中的噪声可以避免收敛到糟糕(浅层)的局部最小值;
- SGD 的计算成本大幅降低(无需遍历所有数据点)。
1.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) $$
使用 mini-batch 而非单样本,往往能更充分地利用硬件:例如单样本训练时 GPU 利用率很差。分布式训练技术会把一个大的 mini-batch 拆分到集群的各台机器上,再聚合各自算出的梯度(英文原笔记中提到 Facebook 曾用分布式训练在一小时内完成 ImageNet 上的网络训练,此为课程讲述的行业案例,非本仓库实测数据)。
一个重要的实操提醒:梯度下降不应配合全量大小的 batch 使用。如果确实想在全量 batch 上训练,应使用 LBFGS 这类优化技术——PyTorch 与 SciPy 都提供了 LBFGS 的实现。
1.4 动量(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 动量。每一步更新时,先把旧动量按因子 $\beta$(取值在 0 到 1 之间)衰减,再加上当前随机梯度;因此 $p$ 可以理解为梯度的运行平均(running average)。最后让 $w$ 沿新动量 $p$ 的方向移动。
等价形式:随机重球法(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}$)与新的负梯度的组合。
直觉:物理中的动量
SGD 动量与物理学中的动量概念类似,优化过程就像一颗重球滚下坡:动量让球保持原有的运动方向,而梯度可以看作把球推向其他方向的力。与剧烈改变行进方向相比,动量只做温和的修正,能有效抑制纯 SGD 中常见的来回震荡。
$\beta$ 参数被称为阻尼因子(Dampening Factor):
- $\beta$ 必须大于 0——若等于 0,就退化为普通梯度下降;
- $\beta$ 必须小于 1——否则一切都会爆炸;
- $\beta$ 越小,转向越快;$\beta$ 越大,转向越慢。
实践指南
- 动量几乎总是应与 SGD 配合使用;
- $\beta = 0.9$ 或 $0.99$ 几乎总是表现良好;
- 增大动量参数时,通常需要减小步长以维持收敛:当 $\beta$ 从 0.9 变为 0.99 时,学习率大约需要缩小 10 倍。
为什么动量有效?
(1)加速(Acceleration)
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 动量可以获得加速收敛。但这只适用于凸问题,不适用于神经网络。很多人声称普通动量也是加速方法,但事实上它只对二次函数加速;而且加速与噪声不兼容,SGD 本身带噪,因此加速并不能很好解释 Momentum SGD 的高性能。
(2)噪声平滑(Noise Smoothing)
更实际、更可能的解释是噪声平滑:动量对梯度取平均,每次步进使用的是梯度的运行平均。理论上 SGD 要正常工作,应当对所有步进取平均:
$$ \bar w_k = \frac{1}{K} \sum_{k=1}^K w_k $$
SGD 加动量的好处在于不再需要这种显式平均:动量给优化过程引入了平滑,使每一步更新都成为对解的良好近似。纯 SGD 则往往需要平均一大批更新之后才朝该方向迈步。加速与噪声平滑共同贡献了动量的高性能。
从 docs/images/week05/05-1/sgd-vs-momentum.png 可以看出:纯 SGD 初期朝解的方向进展良好,但到达谷底(碗底)后会在底部来回弹跳;调整学习率只是让弹跳变慢;而加上动量后步长被平滑,几乎不再弹跳。
二、Lecture B:自适应优化、归一化层与"优化的终结"
2.1 为什么需要自适应方法(Adaptive Methods)
带动量的 SGD 目前仍是许多机器学习问题的先进优化方法;但还有一类"自适应方法",对**病态条件(poorly conditioned)**的问题尤其有用(即 SGD 失效的场景)。
在 SGD 公式中,网络中每一个权重都用同一个全局学习率 $\gamma$ 更新。自适应方法则为每个权重单独适配学习率,依据是每个权重各自的梯度信息。实际使用的网络在不同部分结构差异很大:例如 CNN 早期可能是在大图上做很浅的卷积,后期则是在小图上做大量通道的卷积,两者性质迥异,适合网络前期的学习率未必适合后期——这说明按层自适应的学习率是有价值的。网络后部的权重(例如 VGG16 中 4096 维的全连接层,见 docs/images/week05/05-2/5_2_vgg.png)直接决定输出,对输出影响极强,需要更小的学习率;而前期权重(尤其随机初始化时)对输出的个体影响更小。
2.2 RMSprop:用梯度均方根做归一化
Root Mean Square Propagation(均方根传播)的核心思想是:用梯度的均方根对梯度做归一化。下式中梯度平方表示对向量每个元素分别平方:
$$ \begin{aligned} v_{t+1} &= {\alpha}v_t + (1 - \alpha) \nabla f_i(w_t)^2 \ w_{t+1} &= w_t - \gamma \frac {\nabla f_i(w_t)}{ \sqrt{v_{t+1}} + \epsilon} \end{aligned} $$
其中:
- $\gamma$ 是全局学习率;
- $\epsilon$ 是接近机器精度 $\epsilon$ 的小量(量级为 $10^{-7}$ 或 $10^{-8}$),用于避免除零错误;
- $v_{t+1}$ 是二阶矩(second moment)估计。
我们用指数移动平均(exponential moving average)更新 $v$ 来估计这个带噪量——这是维护"可能随时间变化之量的平均"的标准方式:新值携带更多信息,应给予更大权重,于是用常数 $\alpha$(0 到 1 之间)对旧值逐级指数降权,直到旧值不再重要。原始 RMSprop 维护的是非中心二阶矩的指数移动平均,因此这里不减均值;二阶矩被用来逐元素归一化梯度,即每个梯度元素除以二阶矩估计的平方根。当梯度期望值很小时,这个过程近似于用标准差去除梯度。分母使用很小的 $\epsilon$ 并不会导致发散,因为当 $v$ 很小时,动量(梯度本身)也很小。
2.3 ADAM:RMSprop + 动量
ADAM(Adaptive Moment Estimation,自适应矩估计)可以理解为 RMSprop 加上动量,是目前更常用的方法。动量更新被改造成指数移动平均,且处理 $\beta$ 时无需改变学习率;与 RMSprop 一样,这里也对梯度平方取指数移动平均:
$$ \begin{aligned} m_{t+1} &= {\beta}m_t + (1 - \beta) \nabla f_i(w_t) \ v_{t+1} &= {\alpha}v_t + (1 - \alpha) \nabla f_i(w_t)^2 \ w_{t+1} &= w_t - \gamma \frac {m_{t}}{ \sqrt{v_{t+1}} + \epsilon} \end{aligned} $$
其中 $m_{t+1}$ 是动量的指数移动平均。上式中省略了偏差修正(bias correction)——该修正用于在早期迭代中保持移动平均无偏。
实践对比
训练神经网络时,SGD 在训练初期常常朝错误方向走,而 RMSprop 能较快瞄准正确方向;但 RMSprop 与普通 SGD 一样受噪声困扰,接近局部最小值后会在最优点附近显著弹跳。就像给 SGD 加动量一样,ADAM 带来了同样的改进:它是解的一个良好、无噪的估计,因此通常推荐使用 ADAM 而非 RMSprop(对比图见 docs/images/week05/05-2/5_2_comparison.png)。
ADAM 对训练某些语言模型网络是必需的;优化神经网络时,一般优先选择带动量的 SGD 或 ADAM。但 ADAM 的论文理论尚不完善,且存在若干缺点:
- 可以在非常简单的测试问题上证明该方法不收敛;
- 已知会带来泛化误差:网络在训练数据上损失为零,未必能在未见数据上保持零损失;尤其在图像问题上,其泛化误差往往比 SGD 更差(可能因素包括:找到的是最近的局部最小值、ADAM 噪声更小、或其自身结构等);
- ADAM 需要维护3 个缓冲区,而 SGD 只需 2 个。除非模型大到数 GB 量级、可能放不进内存,否则这影响不大;
- 需要调2 个动量参数,而不是 1 个。
2.4 归一化层(Normalization Layers)
与前文改进优化算法不同,归一化层改进的是网络结构本身:它们作为额外层插入已有层之间,目标是提升优化与泛化性能。神经网络通常在线性运算与非线性运算(激活函数,如 ReLU)之间交替;归一化层可以放在线性层之前或激活函数之后,最常见的做法是放在线性层与激活函数之间(在 CNN 中典型形态为:卷积 → 批量归一化 → ReLU,见图 docs/images/week05/05-2/5_2_norm_layer_c.png)。
需要注意:归一化层改变流经网络的数据,但并不改变网络的表达能力——只要权重配置得当,未归一化的网络仍可产生与归一化网络相同的输出。
归一化的通用记号
$$ y = \frac{a}{\sigma}(x - \mu) + b $$
其中:$x$ 为输入向量,$y$ 为输出向量,$\mu$ 为 $x$ 均值的估计,$\sigma$ 为 $x$ 标准差(std)的估计,$a$ 为可学习的缩放因子,$b$ 为可学习的偏置项。
如果没有可学习参数 $a$ 和 $b$,输出向量 $y$ 的分布将固定为均值 0、标准差 1。$a$ 与 $b$ 的作用是保持网络的表示能力——输出值仍可落在任意特定范围。注意 $a$、$b$ 并不会抵消归一化,因为它们是可学习参数,比 $\mu$、$\sigma$ 稳定得多。
四种归一化方式
对于包含 $N$ 张高 $H$ 宽 $W$、共 $C$ 个通道的 mini-batch 图像(示意图见 docs/images/week05/05-2/5_2_norm_operations.png),按选取的归一化样本范围不同,有 4 种典型做法:
| 方法 | 归一化范围 | 适用场景 |
|---|---|---|
| Batch Norm(批量归一化) | 只对输入的一个通道做归一化 | 首个被提出、最广为人知的方法;视觉任务效果好 |
| Layer Norm(层归一化) | 单张图像内跨全部通道 | 语言任务中大量使用 |
| Instance Norm(实例归一化) | 单张图像、单个通道 | 语言/风格类任务中大量使用 |
| Group Norm(分组归一化) | 单张图像、跨部分通道(如 0~9 一组、10~19 一组) | 视觉任务效果好,且与 SGD 不冲突 |
关于 Group Norm 的补充:实践中分组大小几乎总是32;这也是课程讲师 Aaron Defazio 推荐的做法——实际性能好,且不与 SGD 冲突。
为什么归一化有帮助?
虽然归一化在实践中效果很好,但其有效的原因仍有争议。最初它被提出用于减小"内部协变量偏移(internal covariate shift)",但已有学者通过实验证伪了这一解释。可以确认的是,归一化带来了以下多种因素的综合效果:
- 优化效果:带归一化层的网络更易优化,允许使用更大的学习率,从而加速训练;
- 正则化效果:由于批次内样本随机,均值/标准差估计带有噪声,这种额外"噪声"在某些情况下带来更好的泛化;
- 降低对权重初始化的敏感性。
因此归一化让你可以更"随意":几乎可以把任意网络构件组合在一起,而无需过多担心网络病态程度,仍有机会成功训练。
实践注意事项与 PyTorch 用法
反向传播必须经过均值/标准差的计算以及归一化的应用,否则网络训练会发散。这一反向传播计算相当繁琐且易错,而 PyTorch 能自动完成,非常省力。PyTorch 中对应的两个归一化层类:
torch.nn.BatchNorm2d(num_features, ...) torch.nn.GroupNorm(num_groups, num_channels, ...)Batch Norm 是最早提出、最广为人知的方法;但Aaron Defazio 推荐改用 Group Norm:它更稳定、理论上更简单,通常效果更好;分组大小 32 是不错的默认值。
另外注意:Batch Norm 与 Instance Norm 在训练结束后使用固定的均值/标准差(不再每次评估时重算),因为归一化需要多个训练样本;而 Group Norm 与 Layer Norm 只需单个训练样本即可归一化,因此无此问题。
2.5 "优化的终结":深度学习加速 MRI 重建
有时外行闯入一个陌生领域,反而能改善现状,深度学习用于 MRI 图像重建加速便是典型例子。
传统 MRI 重建
传统 MRI 重建中,原始数据来自 MRI 机器,用简单管线/算法重建图像。MRI 机器在二维傅里叶域逐行或逐列(每几毫秒一次)采集数据;原始输入由频率与相位两个通道组成,数值代表特定频率和相位的正弦波幅度——可简单理解为一张含实部、虚部通道的复值图像。对其做逆傅里叶变换(把这些正弦波按数值加权求和),即可得到原始解剖图像(见 docs/images/week05/05-2/5_2_mri.png)。
目前从傅里叶域到图像域存在一个非常高效的线性映射,无论图像多大都只需毫秒级完成。问题在于:能不能更快?
加速 MRI(Accelerated MRI)
加速的关键思路(迄今最成功)是不全采所有列:可以随机跳过部分列,但实践中最好保留中间列(它们横跨整幅图像、信息量大),外围则随机采样。问题随之而来——此时线性映射不再适用:对下采样后的傅里叶空间直接做线性映射,得到的结果毫无用处(见 docs/images/week05/05-2/5_2_acc_mri.png),显然需要更聪明的做法。
压缩感知(Compressed Sensing)
压缩感知是理论数学的重大突破之一:Candes 等人的论文证明,从下采样的傅里叶域图像出发在理论上可以获得完美重建——当目标信号稀疏或具有稀疏结构时,用更少的测量即可完美重建。但实际应用有若干前提:
- 不需要随机采样,而需要非相干采样(incoherent sampling)——不过实践中人们通常就是随机采样;
- 采一列或半列耗时相同,因此实践中整列采样;
- 图像需要具备稀疏性(大量零值/黑色像素)。原始输入经波长分解后可稀疏表示,但只是近似稀疏而非严格稀疏,所以压缩感知能得到相当好但不完美的重建(见 docs/images/week05/05-2/5_2_comp_sensing.png);若输入在波长域非常稀疏,则能获得完美图像。
压缩感知基于优化理论,其重建方式是求解一个带额外正则项的小型优化问题:
$$ \hat{x} = \arg\min_x \frac{1}{2} \Vert M (\mathcal{F}(x)) - y \Vert^2 + \lambda TV(x) $$
其中 $M$ 是掩码函数(把未采样位置置零),$\mathcal{F}$ 是傅里叶变换,$y$ 是观测到的傅里叶域数据,$\lambda$ 是正则化惩罚强度,$V$ 是正则化函数。这个优化问题必须对 MRI 扫描的每个时间步/每个切片求解一次,耗时常常超过扫描本身——这让我们有理由寻找更好的方案。
谁还需要优化?——用深度网络一步求解
与其每个时间步都解一个小优化问题,不如用一个大型神经网络直接产出所需解。希望在于:训练一个足够复杂的网络,让它一步基本解决优化问题,输出质量堪比逐时间步求解优化问题的结果:
$$ \hat{x} = B(y) $$
其中 $B$ 是深度学习模型,$y$ 是观测到的傅里叶域数据。15 年前这条路很难走,如今实现起来容易得多(结果对比见 docs/images/week05/05-2/5_2_dl_approach.png)。课程指出:用于生成该重建的模型采用ADAM 优化器 + Group Norm 归一化层 + 基于 U-Net 的卷积神经网络,这一方案已非常接近实际应用。
三、Practicum:手推卷积、PyTorch 尺寸验证与自动微分
3.1 理解 1D 卷积:堆叠与平移 kernel
这一部分探讨卷积,是因为我们希望利用数据的稀疏性(sparsity)、平稳性(stationarity)与组合性(compositionality)。
与上一周使用的矩阵 $A$(见 上一周笔记)不同,这里把矩阵宽度改为 kernel 大小 $k$,于是矩阵的每一行就是一个 kernel。通过堆叠(stacking)与平移(shifting)kernel(见 docs/images/week05/05-3/Illustration_1D_Conv.png),可以得到 $m$ 层高度为 $n-k+1$ 的输出,即 $m$(厚度)个大小为 $n-k+1$ 的向量(见 docs/images/week05/05-3/Result_1D_Conv.png)。
单个输入向量可以看作单声道(monophonic)信号(见 docs/images/week05/05-3/Monophonic_Signal.png)。此时输入 $x$ 是一个映射:
$$ x:\Omega\rightarrow\mathbb{R}^{c} $$
其中 $\Omega = \lbrace 1, 2, 3, \cdots \rbrace \subset \mathbb{N}^1$(因为这是 1 维信号/拥有 1 维定义域),此时通道数 $c=1$;当 $c=2$ 时即为立体声(stereophonic)信号。对 1D 卷积,只需逐 kernel 计算标量积(见 docs/images/week05/05-3/Layer_by_layer_scalar_product.png)。
3.2 PyTorch 中 kernel 与输出宽度的维度验证
技巧:在 IPython 中可以使用问号查看函数文档,例如输入nn.Conv1d?得到签名:
Init signature: nn.Conv1d( in_channels, # 输入图像中的通道数 out_channels, # 卷积产生的通道数 kernel_size, # 卷积核的大小 stride=1, # 卷积的步幅 padding=0, # 在输入两侧添加的零填充 dilation=1, # 卷积核元素之间的间距 groups=1, # 从输入到输出的阻塞连接数 bias=True, # 若为 True,则为输出添加可学习的偏置 padding_mode='zeros', # 填充模式,接受 'zeros' 与 'circular' )1D 卷积示例
假设做 1 维卷积,从 2 个通道(立体声信号)到 16 个通道(16 个 kernel),kernel 大小为 3、stride 为 1。于是共有 16 个厚度 2、长度 3 的 kernel。设输入信号 batch 大小为 1(一个信号)、2 个通道、64 个采样点。输出层为 1 个信号、16 个通道,信号长度 $62 = 64-3+1$。每个输出通道一个偏置,因此 bias 大小为 16。
conv = nn.Conv1d(2, 16, 3) # 2 通道(立体声),16 个大小为 3 的 kernel conv.weight.size() # 输出: torch.Size([16, 2, 3]) conv.bias.size() # 输出: torch.Size([16]) x = torch.rand(1, 2, 64) # batch 为 1,2 通道,64 个采样点 conv(x).size() # 输出: torch.Size([1, 16, 62]) conv = nn.Conv1d(2, 16, 5) # 2 通道,16 个大小为 5 的 kernel conv(x).size() # 输出: torch.Size([1, 16, 60])2D 卷积示例
输入为 1 个样本、20 个通道(例如高光谱图像),高 64、宽 128。2D 卷积输入 20 通道、16 个大小为 $3\times5$ 的 kernel。卷积后输出为 1 个样本、16 个通道,高 $62=64-3+1$,宽 $124=128-5+1$。执行 2D 卷积所需存储的 kernel 张量是4 维的。
x = torch.rand(1, 20, 64, 128) # 1 个样本,20 通道,高 64,宽 128 conv = nn.Conv2d(20, 16, (3, 5)) # 20 通道,16 个 kernel,kernel 大小为 3 x 5 conv.weight.size() # 输出: torch.Size([16, 20, 3, 5]) conv(x).size() # 输出: torch.Size([1, 16, 62, 124])若想保持输入输出维度一致,可以加 padding。延续上面代码,设置stride=1、padding=(1, 2):即在 $y$ 方向填充 1(顶部、底部各 1),在 $x$ 方向填充 2。此时输出与输入尺寸相同:
# 20 通道,16 个大小为 3 x 5 的 kernel,stride 为 1,padding 为 (1, 2) conv = nn.Conv2d(20, 16, (3, 5), 1, (1, 2)) conv(x).size() # 输出: torch.Size([1, 16, 64, 128])3.3 自动梯度(Autograd)是如何工作的?
本节让 torch 追踪张量上的所有计算,从而计算偏导数:
- 创建带梯度累积能力的 $2\times2$ 张量 $\boldsymbol{x}$;
- 对 $\boldsymbol{x}$ 的所有元素减 2 得到 $\boldsymbol{y}$。打印
y.grad_fn会得到<SubBackward0 object ...>,说明 $y$ 由减法模块($\boldsymbol{x}-2$)生成;也可用y.grad_fn.next_functions[0][0].variable反推原始张量; - 继续运算:$\boldsymbol{z} = 3\boldsymbol{y}^2$;
- 计算 $\boldsymbol{z}$ 的均值。
反向传播用于计算梯度。在本例中,反向传播过程可看作计算 $\frac{d\boldsymbol{a}}{d\boldsymbol{x}}$。手工验证 $\frac{d\boldsymbol{a}}{d\boldsymbol{x}}$ 后可以发现,执行a.backward()得到的x.grad与我们手算结果一致。手算过程如下:
$$ \begin{aligned} a &= \frac{1}{4} (z_1 + z_2 + z_3 + z_4) \ z_i &= 3y_i^2 = 3(x_i-2)^2 \ \frac{da}{dx_i} &= \frac{1}{4}\times3\times2(x_i-2) = \frac{3}{2}x_i-3 \ x &= \begin{pmatrix} 1&2\3&4\end{pmatrix} \ \left(\frac{da}{dx_i}\right)^\top &= \begin{pmatrix} 1.5-3&3-3\[2mm]4.5-3&6-3\end{pmatrix}=\begin{pmatrix} -1.5&0\[2mm]1.5&3\end{pmatrix} \end{aligned} $$
在 PyTorch 中使用偏导时,得到的梯度与原始数据形状相同;但从严格的 Jacobian 角度看,正确形式应是转置。
从基础到"更疯狂":循环中的梯度
设 $x$ 为 $1\times3$ 向量,令 $y$ 为 $x$ 的两倍,并不断把 $y$ 翻倍直到其范数小于 1000。由于 $x$ 是随机的,无法直接预知循环终止的迭代次数:
x = torch.randn(3, requires_grad=True) y = x * 2 i = 0 while y.data.norm() < 1000: y = y * 2 i += 1但借助梯度可以轻易推断:
gradients = torch.FloatTensor([0.1, 1.0, 0.0001]) y.backward(gradients) print(x.grad) tensor([1.0240e+02, 1.0240e+03, 1.0240e-01]) print(i) 9requires_grad 与 no_grad 的边界
用requires_grad=True标记希望追踪梯度累积。若在 $x$ 或 $w$ 的声明中省略requires_grad=True,再对 $z$ 调用backward(),将因缺少梯度累积而报运行时错误:
# x 与 w 都允许梯度累积 x = torch.arange(1., n + 1, requires_grad=True) w = torch.ones(n, requires_grad=True) z = w @ x z.backward() print(x.grad, w.grad, sep='\n')反向地,可用with torch.no_grad()关闭梯度累积:
x = torch.arange(1., n + 1) w = torch.ones(n, requires_grad=True) # 其中的所有 torch 张量都不会累积梯度 with torch.no_grad(): z = w @ x try: z.backward() # PyTorch 会在此抛错,因为 z 没有梯度累积 except RuntimeError as e: print('RuntimeError!!! >:[') print(e)3.4 自定义梯度(Custom Gradients)
除了基础数值运算,我们还可以生成自定义模块/函数,并将其插入神经网络计算图。配套 Jupyter Notebook 位于仓库 extra/b-custom_grads.ipynb。
做法是:继承torch.autograd.Function,并重写forward()与backward()。例如训练网络时,需要前向传播、并知道输入对输出的偏导数,才能把该模块用在代码的任何位置;只要掌握了输入对输出的偏导数,就可以通过反向传播(链式法则)把它插入运算链的任意位置。
Notebook 中有三个自定义模块示例:add、split与max。例如自定义加法模块:
# 自定义加法模块 class MyAdd(torch.autograd.Function): @staticmethod def forward(ctx, x1, x2): # ctx 是一个上下文,可以在其中保存 # backward 所需的中间计算。 ctx.save_for_backward(x1, x2) return x1 + x2 @staticmethod def backward(ctx, grad_output): x1, x2 = ctx.saved_tensors grad_x1 = grad_output * torch.ones_like(x1) grad_x2 = grad_output * torch.ones_like(x2) # 需要按 forward 输入顺序返回梯度(不含 ctx) return grad_x1, grad_x2两个输入相加得到输出,前向按上式重写;反向传播时梯度会复制到两侧,所以 backward 用"复制"实现。对于split:若多个分支来自同一张量,反向传播时应当把这些梯度相加/求和;对于argmax:它选出最大值的索引,因此最大值索引处梯度为 1、其余为 0。需要牢记:不同自定义模块要各自重写自己的 forward 与 backward 梯度逻辑。
四、在仓库中继续深入:配套代码与资源
本仓库为 NYU-DLSP20 课程的配套开源实现,围绕第五周主题提供了大量可直接运行的资源:
- 优化器可视化实验:extra/utils/optim.py 定义了一个继承
torch.optim.Optimizer的Optim基类,其中step()遍历param_groups并在每个参数上调用my_step();子类只需实现my_step即可定制更新规则。文件底部还包含一个病态二次目标 $f(x,y)=\frac{1}{2}xy^\top A xy + b^\top xy$($A=\mathrm{diag}(1,5)$,条件数高达 5)的等高线图绘制函数output(opt, nsteps, noise, fname),可用于直观对比不同优化器在病态问题上的收敛轨迹,并支持通过noise参数模拟随机梯度噪声(详见extra/optimization.ipynb)。 - 优化实验 Notebook:extra/optimization.ipynb 基于上述工具演示 GD/SGD/Momentum 等的收敛行为。
- 自定义梯度 Notebook:extra/b-custom_grads.ipynb 完整实现
add、split、max等自定义 autograd 模块(对应上文 3.4 节)。 - 英文原版笔记:docs/en/week05/05-1.md(Optimisation Techniques I)、docs/en/week05/05-2.md(Optimisation Techniques II)、docs/en/week05/05-3.md(Convolutions 与自动微分);中文读者可对照阅读 docs/pt/week05/05-1.md、docs/pt/week05/05-2.md、docs/pt/week05/05-3.md。
- 本讲幻灯:slides/01 - Spiral classification.pdf、slides/04 - RNN.pdf 之外,仓库根目录的 04-spiral_classification.ipynb、05-regression.ipynb、06-convnet.ipynb、07-listening_to_kernels.ipynb 等 Notebook 均会实际用到本章的优化器与卷积知识。
小结
第五周的内容构成了深度学习优化与卷积原理的完整闭环:GD → SGD → Momentum → RMSprop/ADAM的进化脉络清晰呈现了"如何让更新更稳、更快、更自适应";归一化层从结构上改善优化与泛化;加速 MRI案例则展示了"用深度网络一步替代逐时间步优化"的工程范式;最后的1D/2D 卷积手推与 PyTorch 尺寸验证、autograd 与自定义梯度,把理论落到可复现的代码上。若要在实际项目中应用,可遵循课程给出的经验法则:默认使用带动量 SGD($\beta=0.9/0.99$)或 ADAM;归一化层优先考虑 Group Norm(分组 32);卷积输出尺寸按 $n-k+1$(加 padding 后相应调整)快速心算校验。
- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
相关推荐
5 分钟让普通鼠标在 macOS 上比触控板好用:Mac Mouse Fix 上手指南
5 分钟让普通鼠标在 macOS 上比触控板好用:Mac Mouse Fix 上手指南 第一次按下侧键、光标毫无反应的那一刻,我就知道这只十几块的鼠标在 mac
示例工程NYU-DLSP20 第五周精读:梯度下降、Momentum 与自适应优化、归一化层,以及卷积与自动微分实战
NYU DLSP20 第五周精读:梯度下降、Momentum 与自适应优化、归一化层,以及卷积与自动微分实战 本文基于开源课程仓库 pytorch Deep L
示例工程NYU-DLSP20 第 5 周深度笔记:梯度下降、SGD 与动量、自适应优化、归一化层与 PyTorch 卷积/自动微分
NYU DLSP20 第 5 周深度笔记:梯度下降、SGD 与动量、自适应优化、归一化层与 PyTorch 卷积/自动微分 本文是 NYU Deep Learn
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考