卷积与池化的后向传播:参考答案#
说明#
以下答案与正文 10 道题逐题对应。推导题给出主要步骤;编程和实验题给出参考程序和检查方法,并说明结果适用于哪些条件。
链式法则给出 \(\partial\mathcal J/\partial W_{q,c,u,v}^{[l]}=\sum_{i,r,s}(\partial\mathcal J/\partial Z_{i,q,r,s}^{[l]})(\partial Z_{i,q,r,s}^{[l]}/\partial W_{q,c,u,v}^{[l]})\),而第二个因子是 \(A_{i,c,r+u-1,s+v-1}^{[l-1]}\)。由于 \(\partial\mathcal J/\partial Z_{i,q,r,s}^{[l]}=\mathrm dZ_{i,q,r,s}^{[l]}\),所以得到正文公式。一个卷积核元素会用于 \(m\) 个样本中的所有合法输出位置,因此必须累加这些位置的贡献。若 \(\mathcal J\) 已经是批量平均损失,因子 \(1/m\) 已包含在 \(\mathrm d\bZ^{[l]}\) 中,最后不能再次除以 \(m\)。
卷积核的四个位置分别收集所有对应输入,因此 \(\mathrm d\bW^{[l]}=\begin{bmatrix}1+2+4+5&2+3+5+6\\4+5+7+8&5+6+8+9\end{bmatrix}=\begin{bmatrix}12&16\\24&28\end{bmatrix}\)。全 1 核把 \(\mathrm d\bZ^{[l]}\) 中的每个数加回它所覆盖的输入位置,故 \(\mathrm d\bA^{[l-1]}=\begin{bmatrix}1&2&1\\2&4&2\\1&2&1\end{bmatrix}\)。中心位置被四个窗口覆盖,因此梯度为 4。
最大池化把 \(\mathrm dP_{i,c,r,s}^{[l+1]}=6\) 送到前向传播选中的最大值 4,因此该窗口对输入的梯度为 \(\begin{bmatrix}0&6\\0&0\end{bmatrix}\)。平均池化对四个元素的局部导数均为 \(1/4\),所以梯度为 \(\begin{bmatrix}1.5&1.5\\1.5&1.5\end{bmatrix}\)。若窗口内有并列最大值,程序必须按照框架规定的规则选定接收梯度的位置,并保证后向传播使用前向传播保存的同一位置。
Taylor 展开 \(\mathcal J(\theta\pm h)=\mathcal J(\theta)\pm h\mathcal J'(\theta)+h^2\mathcal J''(\theta)/2\pm h^3\mathcal J'''(\theta)/6+\cdots\),相减再除以 \(2h\) 后首个截断项为 \(h^2\mathcal J'''(\theta)/6\)。但两次接近数值相减会产生约 \(\varepsilon_{\mathrm{mach}}\) 量级误差,除以 \(h\) 后被放大。较大 \(h\) 受截断误差控制,过小 \(h\) 受舍入误差控制,因而存在中间最优范围。
后向程序复用前向传播保存的窗口位置。下面的
a_pad是填充后的 \(\bA^{[l-1]}\),dz对应 \(\mathrm d\bZ^{[l]}\):import numpy as np def conv2d_backward(dz, a_pad, w, padding=0, stride=1, dilation=1): da_pad = np.zeros_like(a_pad, dtype=np.result_type(dz, w)) dw = np.zeros_like(w, dtype=np.result_type(dz, a_pad)) db = dz.sum(axis=(0, 2, 3)) batch_size, c_out, h_out, w_out = dz.shape kh, kw = w.shape[2:] for i in range(batch_size): for q in range(c_out): for r in range(h_out): for s in range(w_out): rows = slice(r*stride, r*stride+dilation*kh, dilation) cols = slice(s*stride, s*stride+dilation*kw, dilation) g = dz[i, q, r, s] dw[q] += g * a_pad[i, :, rows, cols] da_pad[i, :, rows, cols] += g * w[q] if padding == 0: da = da_pad else: da = da_pad[:, :, padding:-padding, padding:-padding] return da, dw, db
这里假定高、宽两端使用相同的整数填充;返回的
da已裁掉人工边界,因而与未填充的 \(\bA^{[l-1]}\) 维度相同。若前向传播支持不同方向的填充,缓存中也应保存四个填充值,并分别裁掉相应边界。对参数 \(\theta_j\) 的相对误差可用 \(|g_j-\widetilde g_j|/\max(1,|g_j|,|\widetilde g_j|)\)。中心差分 只适合
float64小张量并应尝试多个 \(h\);将手工后向传播与 PyTorch 自动微分 对照时,必须复制完全相同的 \(\bA^{[l-1]}\)、\(\bW^{[l]}\)、\(\bb^{[l]}\) 和 \(\mathrm d\bZ^{[l]}\)。若自动微分结果与手工结果一致而中心差分不稳定,先调整 \(h\);若两种检查都不一致,则检查核方向、填充裁剪和重叠累加。全 1 核与全 1 的 \(\mathrm d\bZ^{[l]}\) 应复现第 2 题的重叠计数;代码中应检查
db == dz.sum((0, 2, 3))、da.shape == a.shape和dw.shape == w.shape。池化测试应使用具有唯一最大值的窗口和可以手算的平均值;dz=0时三个卷积梯度都应为 0;批量、通道或空间维度不兼容时应报告错误。还应检查所有结果都是有限数,并用非对称卷积核确认核方向正确。三种后向实现使用相同的 \(\bW^{[l]}\)、批次顺序、随机种子和参数更新次数,并先确认 \(\mathrm d\bA^{[l-1]}\)、\(\mathrm d\bW^{[l]}\) 与 \(\mathrm d\bb^{[l]}\) 在允许误差内一致。朴素 NumPy 通常训练最慢,向量化实现会增加保存窗口所需的内存,框架自动微分 通常能调用经过优化的程序;具体差异取决于硬件。结果表应包含测试指标、梯度最大误差、训练时间、参数量和峰值内存。模型预测只进行前向传播,因此后向实现不会直接改变预测所需的计算。
最大池化通常只在每个窗口的最大位置产生非零梯度,平均池化则把梯度分到全部元素,因此非零比例和范数分布不同。两组固定划分、种子、初始化、更新次数和通道数,报告多种子任务指标、梯度统计、训练与固定批量预测时间、参数量和峰值内存。池化本身都无参数;性能差异不能仅由梯度稀疏性单独解释。
float64用于小规模参考,float32是常见基线,混合精度需使用损失缩放并检查非有限梯度。三组共享数据、种子、更新次数和学习率规则,报告任务指标及波动、失败步数、训练时间、参数量与峰值内存。参数个数相同,但存储字节数不同;混合精度可能降低内存,也可能因硬件或算子不支持而没有收益。