- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
本篇文章源于 NYU Deep Learning Spring 2020(NYU-DLSP20,仓库目录pytorch-Deep-Learning)第五周第三节讲义(对应 docs/ja/week05/05-3.md 及英文原版 docs/en/week05/05-3.md),由 Alfredo Canziani 讲授。文章从数据的三重结构性质(稀疏性、平稳性、构成性)出发,系统地讲解一维卷积的数学表示与 PyTorch 中的维度计算,随后深入剖析自动微分(autograd)引擎的反向传播机制,最后介绍如何通过继承torch.autograd.Function自定义可微函数。读完本文,你将能够独立推导卷积层输出尺寸、手算并验证任意计算图的梯度,并能在自己的网络中以插件形式接入自定义前向/反向传播模块。
1. 从线性代数到卷积:用核替换全连接矩阵
在第四周讲义中,全连接层被表示为输入向量 $\boldsymbol{x}\in\mathbb{R}^n$ 与权重矩阵 $\boldsymbol{A}\in\mathbb{R}^{m\times n}$ 的乘法 $\boldsymbol{z}=\boldsymbol{A}\boldsymbol{x}$。该表示对短序列可行,但对于真实音频(例如 3.2 秒、采样率 22.05kHz,即 7 万多个样本)而言,权重矩阵会变得异常"肥胖",参数数量爆炸,难以训练。
卷积的出发点,就是利用自然信号的三条结构性质来约束并简化这一矩阵:
- 稀疏性(sparsity,局部性 locality):远离当前位置的数据点对当前输出几乎没有影响,因此矩阵中大部分位置可以填 0,只保留与核相关的非零块;
- 平稳性(stationarity):自然界的数据信号中,某些模式/动机(motif)会反复出现,因此同一个核可以被反复复用,即权重共享(weight sharing);
- 构成性(compositionality):信号是多个局部特征的组合,多层卷积通过叠加不同核来逐层构建更复杂的表示。
讲义给出的具体做法是:不再使用第 $k$ 列全连接矩阵 $\boldsymbol{A}$,而是将矩阵的宽度改为核大小 $k$,矩阵的每一行就是一个核。通过对核进行堆叠(stacking)与平移(shifting),可以得到高度为 $n-k+1$ 的若干层。下图直观展示了核组 $m\times k$ 如何在长度为 $n$ 的输入上滑动:
该结构正是上一周讲义中提到的Toeplitz 矩阵的推广:每条从左到右的下降对角线上的值恒定,且整体为稀疏矩阵。其输出是 $m$(厚度)个尺寸为 $n-k+1$ 的向量:
2. 信号的形式化建模:单声道与立体声
讲义将单个输入向量视作单声道(monophonic)信号。形式上,输入 $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)信号。这一建模与第四周讲义"不同数据集的维度"一节完全一致:音频数据的定义域是一维离散时间索引,通道数可以是 1(单声道)、2(立体声)、5+1(杜比 5.1)等。
对于一维卷积,计算方式非常朴素:逐核计算标量积(内积),即把每个核与输入中对应位置的局部窗口做内积,然后沿时间轴滑动:
这种"运行中的标量积(running scalar product)"正是卷积的实质——仓库中的 07-listening_to_kernels.ipynb 用一个真实音频片段(Windows 系统关机音)演示了用多个音高核卷积原信号,即可提取出旋律中的各个音符,是理解本节的绝佳动手实验。
3. PyTorch 中核的维度与输出宽度
3.1 用 IPython 问号查看文档
讲义给出一个实用技巧:在 IPython/Jupyter 中使用?(问号)可以直接调出函数的完整签名文档。例如:
nn.Conv1d?3.2 nn.Conv1d 完整签名
Init signature: nn.Conv1d( in_channels, # number of channels in the input image out_channels, # number of channels produced by the convolution kernel_size, # size of the convolving kernel stride=1, # stride of the convolution padding=0, # zero-padding added to both sides of the input dilation=1, # spacing between kernel elements groups=1, # nb of blocked connections from input to output bias=True, # if `True`, adds a learnable bias to the output padding_mode='zeros', # accepted values `zeros` and `circular` )各参数含义如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
in_channels | 必填 | 输入通道数(如立体声信号为 2) |
out_channels | 必填 | 卷积产生的通道数,即核的数量 |
kernel_size | 必填 | 卷积核大小(1D 时为单个整数) |
stride | 1 | 卷积滑动的步长 |
padding | 0 | 在输入两侧补零的宽度 |
dilation | 1 | 核元素之间的间距(空洞卷积) |
groups | 1 | 输入到输出的分组连接数(分组卷积) |
bias | True | 是否在输出上添加可学习的偏置 |
padding_mode | 'zeros' | 填充模式,接受zeros与circular |
对应地,1D 卷积输出长度遵循公式(stride=1、padding=0 时即 $n-k+1$):
$$ L_{\text{out}} = \left\lfloor \frac{L_{\text{in}} + 2\times\text{padding} - \text{dilation}\times(k-1) - 1}{\text{stride}} + 1 \right\rfloor $$
3.3 一维卷积实例:立体声 → 16 通道
考虑一个核大小为 $3$、步长为 $1$ 的一维卷积,将 $2$ 通道(立体声)映射为 $16$ 通道,即使用 $16$ 个核;每个核的厚度为 $2$(匹配输入通道数)、长度为 $3$。设输入批大小为 $1$、通道数 $2$、样本数 $64$,则输出层为 $1$ 个信号、$16$ 个通道、长度 $62 = 64 - 3 + 1$。由于每个输出通道对应一个偏置,偏置尺寸为 $16$。
conv = nn.Conv1d(2, 16, 3) # 2 channels (stereo signal), 16 kernels of size 3 conv.weight.size() # output: torch.Size([16, 2, 3]) conv.bias.size() # output: torch.Size([16]) x = torch.rand(1, 2, 64) # batch of size 1, 2 channels, 64 samples conv(x).size() # output: torch.Size([1, 16, 62]) conv = nn.Conv1d(2, 16, 5) # 2 channels, 16 kernels of size 5 conv(x).size() # output: torch.Size([1, 16, 60])注意权重张量的形状规律:[out_channels, in_channels, kernel_size],即[16, 2, 3]——16 个核,每个核覆盖 2 个输入通道、长度为 3。当核长度从 3 增至 5 时,输出长度相应地从 62 变为 60($64-5+1$),与公式吻合。
3.4 二维卷积实例:高光谱图像
将思路扩展到 2D。设输入为 $1$ 个样本、$20$ 个通道(例如高光谱图像)、高 $64$、宽 $128$。用 $20$ 个输入通道到 $16$ 个核、核大小为 $3\times 5$ 的二维卷积,输出为 $1$ 样本、$16$ 通道、高 $62=64-3+1$、宽 $124=128-5+1$:
x = torch.rand(1, 20, 64, 128) # 1 sample, 20 channels, height 64, and width 128 conv = nn.Conv2d(20, 16, (3, 5)) # 20 channels, 16 kernels, kernel size is 3 x 5 conv.weight.size() # output: torch.Size([16, 20, 3, 5]) conv(x).size() # output: torch.Size([1, 16, 62, 124])2D 卷积中,核的集合需要用4 个维度存储:[out_channels, in_channels, kernel_h, kernel_w],对应上面的[16, 20, 3, 5]。
3.5 用 padding 保持空间维度
若希望输出与输入尺寸一致,可以引入填充。在上例基础上添加stride=1与padding=(1, 2):即在 $y$ 方向各补 1(上 1、下 1),$x$ 方向各补 2,从而抵消核的收缩效应,输出恢复到 $64\times128$:
# 20 channels, 16 kernels of size 3 x 5, stride is 1, padding of 1 and 2 conv = nn.Conv2d(20, 16, (3, 5), 1, (1, 2)) conv(x).size() # output: torch.Size([1, 16, 64, 128])这与仓库中的 06-convnet.ipynb 所演示的卷积神经网络实践相呼应——网络正是依靠"卷积 + padding + 池化"的组合在控制特征图尺寸的同时逐层提取特征。
4. 自动微分引擎:从手算到 autograd
本节要求 PyTorch 追踪张量上的全部运算,从而自动完成偏导数的计算。仓库根目录下的 03-autograd_tutorial.ipynb 完整复现了本节的全部实验,是配套的最佳练习。
4.1 构建计算图
逐步构造一个标量输出 $a$:
- 创建具有梯度累积能力的 $2\times2$ 张量 $\boldsymbol{x}$;
- 所有元素减去 $2$ 得到 $\boldsymbol{y}$;
- 继续运算:$\boldsymbol{z}=3\boldsymbol{y}^2$;
- 对 $\boldsymbol{z}$ 取均值得到 $a$。
import torch # 创建 2x2 且可累积梯度的张量 x = torch.tensor([[1, 2], [3, 4]], requires_grad=True, dtype=torch.float32) y = x - 2 # y.grad_fn 为 <SubBackward0 ...>,表示 y 由减法 x-2 产生 z = y * y * 3 a = z.mean() # 均值(标量)关键观察点:打印y.grad_fn会得到形如<SubBackward0 object at 0x...>的结果,说明y是由减法模块生成的中间量;同时可以通过y.grad_fn.next_functions[0][0].variable追溯回原始张量 $\boldsymbol{x}$。这正体现了 autograd 的核心机制——define-by-run(动态定义):计算图由代码的实际执行顺序动态构建,每一次迭代都可以不同。
4.2 手算反向传播验证
反向传播即计算梯度 $\frac{d a}{d \boldsymbol{x}}$。以手算作为验证:
$$ \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\4.5-3&6-3\end{pmatrix}=\begin{pmatrix} -1.5&0\1.5&3\end{pmatrix} \end{aligned} $$
执行a.backward()后,x.grad将精确等于上述手算结果,从而验证了引擎的正确性。讲义同时提醒一个易错点:PyTorch 中偏导数的形状与原始数据一致,而数学上正确的雅可比(Jacobian)应是其转置。
4.3 动态图与"疯狂"的梯度
autograd 的强大之处在于它完全适应动态控制流。考虑 $1\times3$ 向量 $x$,令 $y=2x$,并不断将 $y$ 翻倍直到其范数不小于 1000:
x = torch.randn(3, requires_grad=True) y = x * 2 i = 0 while y.data.norm() < 1000: y = y * 2 i += 1由于 $x$ 的随机性,迭代次数 $i$ 无法预先得知,但可以借助梯度轻松反推。因为 $y$ 不是标量,backward()需要显式传入与输出同形的梯度种子(grad_output):
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) 9这里x.grad的值与 $i=9$($2^{10}$ 倍放大后除以 norm 判据)相互印证——每个元素恰好是初始梯度乘以 $2^{10}=1024$(对应1.0240e+02 = 0.1×1024、1.0240e+03 = 1.0×1024、1.0240e-01 = 0.0001×1024)。如讲义所述:"灵活性越大,责任越大"——动态图正是 PyTorch 区别于静态图框架的核心特性。
4.4 requires_grad 与 torch.no_grad()
梯度追踪的开关是requires_grad=True。若在 $x$ 或 $w$ 声明时省略该标记,又在 $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) # 该上下文内所有张量都不会累积梯度 with torch.no_grad(): z = w @ x try: z.backward() # PyTorch 在此抛出错误,因为 z 没有梯度累积 except RuntimeError as e: print('RuntimeError!!! >:[') print(e)5. 自定义梯度:扩展 torch.autograd.Function
除了内置运算,我们还可以自定义可微函数/模块并插入到神经网络的计算图中。仓库中的 extra/b-custom_grads.ipynb 提供了完整的示例代码(含add、split、max三个自定义模块),是本节的标准配套材料。
实现方法:继承torch.autograd.Function,并重写静态方法forward()与backward()。训练网络时,只要已知输出关于输入的偏导数,就可以利用反向传播的链式法则,把该模块插入到运算链的任意位置。forward()的签名约定是:第一个参数为上下文ctx(用于在反向传播时保存中间计算),随后是与输入对应的若干张量;backward()则接收与forward输出数量相同的梯度参数,并返回与forward输入数量相同的梯度(顺序一一对应、且不含ctx)。
5.1 自定义加法模块:梯度向两侧复制
加法运算的梯度规则是:反向传播时梯度被原样复制到两个输入上:
# Custom addition module class MyAdd(torch.autograd.Function): @staticmethod def forward(ctx, x1, x2): # ctx is a context where we can save # computations for 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) # need to return grads in order # of inputs to forward (excluding ctx) return grad_x1, grad_x2使用自定义函数时通过.apply调用(而不是直接实例化):
my_add = MyAdd.apply y = my_add(x1, x2) # x1, x2 均 requires_grad=True z = y.mean() z.backward() # x1.grad、x2.grad 被正确填充仓库 notebook 中还给出了一个更精细的变体AddAndAverage:它在前向中同时完成"求和 + 均值",因此反向时必须除以元素个数x1.numel(),这正是"不同自定义模块需按其自身前向逻辑推导反向规则"的生动示例。
5.2 自定义 split 模块:梯度在汇聚处求和
从同一输入分叉出两个输出的split模块,其反向规则是:把两条分支传回的梯度相加(因为 $x$ 同时影响 $x_1$ 与 $x_2$,属于多路径链式法则):
class MySplit(torch.autograd.Function): @staticmethod def forward(ctx, x): ctx.save_for_backward(x) x1 = x.clone() x2 = x.clone() return x1, x2 @staticmethod def backward(ctx, grad_x1, grad_x2): x = ctx.saved_tensors[0] print(f'grad_x1: {grad_x1}') print(f'grad_x2: {grad_x2}') return grad_x1 + grad_x25.3 自定义 max 模块:独热式梯度
max模块使用非 PyTorch 代码(numpy)计算最大值,其反向规则是独热(one-hot)式的:只有取得最大值的那个索引处梯度为 1,其余位置为 0。实现上通过argmax掩码与上游梯度相乘:
class MyMax(torch.autograd.Function): @staticmethod def forward(ctx, x): # example where we explicitly use non-torch code maximum = x.detach().numpy().max() argmax = x.detach().eq(maximum).float() ctx.save_for_backward(argmax) return torch.tensor(maximum) @staticmethod def backward(ctx, grad_output): argmax = ctx.saved_tensors[0] return grad_output * argmax注意forward中使用了x.detach()断开梯度追踪后再做numpy()转换,这是"在自定义函数中混用非张量计算"时的标准做法;而backward返回的grad_output * argmax恰好实现了"最高索引处为 1、其余为 0"的梯度传播。
6. 小结与延伸阅读
本文沿着"数据性质 → 卷积表示 → 维度计算 → 自动微分 → 自定义梯度"的主线,完整还原了 NYU-DLSP20 第五周第三节讲义的精华。要点回顾:
- 一维卷积通过核堆叠 + 滑动 + 标量积,将全连接矩阵压缩为稀疏、共享权重的 Toeplitz 结构,利用了数据的稀疏性、平稳性与构成性;
- 在 PyTorch 中,卷积层的输出维度由
in_channels、out_channels、kernel_size、stride、padding、dilation共同决定,权重张量的形状遵循[out_channels, in_channels, *kernel_size]规律; - autograd 是 define-by-run 的动态计算图引擎,
backward()沿图反向传播链式法则;标量输出可无参调用,非标量输出需传入同形梯度种子; requires_grad=True开启梯度追踪,torch.no_grad()上下文用于推理阶段关闭追踪;- 继承
torch.autograd.Function并重写forward/backward即可将任意(甚至含非 PyTorch 代码的)运算接入计算图。
进一步学习建议:动手运行仓库根目录下的 03-autograd_tutorial.ipynb 与 extra/b-custom_grads.ipynb,并结合 06-convnet.ipynb 与 07-listening_to_kernels.ipynb 观察卷积在真实网络与音频信号上的应用效果;前一讲 docs/en/week04/04-1.md 提供了卷积与线性代数之间过渡的完整推导。
- 示例工程
【免费下载链接】NYU-DLSP20
NYU Deep Learning Spring 2020
相关推荐
PyTorch 卷积核维度计算与自动微分引擎:NYU-DLSP20 第五周讲义 05-3 深度解读
PyTorch 卷积核维度计算与自动微分引擎:NYU DLSP20 第五周讲义 05 3 深度解读 本篇技术指南基于 NYU DLSP20(PyTorch 深度
示例工程Android Debug Database 项目推荐
Android Debug Database 项目推荐 1. 项目基础介绍和主要编程语言 Android Debug Database 是一个用于调试 Andr
示例工程PyTorch 卷积维度计算与自动微分引擎实战:NYU DLSP20 第五周实践课解析
PyTorch 卷积维度计算与自动微分引擎实战:NYU DLSP20 第五周实践课解析 本篇文章基于 NYU Deep Learning Spring 2020
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考