为了实现二维卷积的反向传播,我们(可能)需要一个能够翻转ndarray 轴的函数。我们说“可能”,因为你可能会巧妙地实现你的卷积前向函数来避免这个问题。然而,我们认为,如果你有能力沿着垂直和水平维度“翻转”核,那么这将更容易理解。
我们将在下面尝试建立你对“翻转”操作的直觉,以帮助你弄清楚如何在 ndarray.py
中实现它。为此,我们将在下面探讨 numpy 的 np.flip
函数。需要注意的一点是,flip
通常是通过使用负的步长和改变底层数组的偏移量来实现的。
例如,在一个数组的所有轴上翻转相当于反转数组。在这种情况下,你可以想象我们希望所有的步长都是负的,偏移量是数组的长度(从数组的末尾开始并“步进”回退)。
由于我们在上次作业的实现中没有明确支持负步长,我们将仅仅使用它们调用 NDArray.make
来创建我们的“翻转”数组,然后立即调用 .compact()
。除了在几个地方将无符号整数更改为有符号整数之外,我们怀疑你现有的 compact
函数不需要做任何改变就能适应负步长。在我们分发的.cc 和.cu 文件中,我们已经更改了函数签名以反映这一点。
或者,你可以通过复制内存在 CPU 后端简单地实现 flip
,你可能会觉得这更直观。我们建议你按照下面的迷你教程进行操作,以保持你的实现以 Python 为中心,因为我们认为在 C 中稍微天真地实现它所需的努力大约相同。
膨胀操作(dilation operator)在 ndarray 的元素之间插入零。当我们计算步长大于 1 的卷积的反向传播时,我们需要使用膨胀操作。以一个例子来说明,当一个 2x2 矩阵在两个轴上都以 1 的膨胀率进行膨胀时,它应该变成:
[[1, 0, 2, 0], [0, 0, 0, 0], [3, 0, 4, 0], [0, 0, 0, 0]]
为了理解为什么我们需要在步长大于 1 的卷积的反向传播中使用膨胀,考虑一个步长为 2,填充方式为 "same",输入通道数等于输出通道数为 8 的卷积,应用于大小为 (10, 32, 32, 8) 的输入。由于步长的原因,输出的结果将是 (10, 16, 16, 8),因此 out_grad
的形状将是 (10, 16, 16, 8)。然而,输入的梯度当然需要有形状 (10, 32, 32, 8)——所以我们必须以某种方式增加 out_grad
的大小。同时,也可以考虑将步长卷积实现为 Conv(x)[:, ::2, ::2, :]
,即在空间维度上只保留每隔一个像素。
在 ops.py
中实现 Dilate
函数。这个函数在属性(attrs)中接受两个额外的参数:膨胀量 dilation
和要膨胀的 axes
。你还必须实现相应的操作 UnDilate
,其前向传播将被用来实现 Dilate
的梯度。(这样做是为了我们不必实现 GetItem
和 SetItem
操作,如果没有额外的优化,这些操作在反向传播时可能会非常低效。)
nn.Conv
反向传播
寻找二维多通道卷积的梯度在技术上可能相当具有挑战性(特别是“严格”地)。我们将在这里提供一些有用的提示。基本上,我们鼓励你利用一个令人惊讶的事实:通常,任何能使维度匹配的方法都是正确的。
最终,卷积的反向传播可以通过卷积操作本身来完成,通过使用 flip
、dilate
和多次应用 transpose
到参数和结果上进行一些巧妙的操作。
在最后一部分,我们基本上将卷积实现为矩阵乘法:忽略各种 restride 和 reshape 操作,我们基本上有类似于 X @ W
的东西,其中 X
是输入,W
是权重。我们还有 out_grad
,它的形状与 X @ W
相同。现在,你已经在之前的作业中实现了矩阵乘法的反向传播,我们可以利用这些知识来洞察卷积的反向传播。特别是,参考你的 matmul 反向实现,你可能会注意到(这里从启发式的角度来说):
X.grad = out_grad @ W.transpose
\
W.grad = X.transpose @ out_grad
令人惊讶的是,如果我们假设这些也是卷积(现在假设 out_grad
、W
和 X
是适用于二维多通道卷积的张量而不是矩阵):
X.grad = ≈conv(≈out_grad, ≈W)
\
W.grad = ≈conv(≈X, ≈out_grad)
在这里,“≈”表示你需要对这些项应用一些额外的操作,以使维度匹配,比如排列/转置轴,扩张,改变卷积函数的 padding=
参数,或者排列/转置卷积的结果轴。
正如我们在课堂上的 最后几张幻灯片 中看到的,卷积的转置可以通过简单地翻转核来找到。由于我们在二维而不是一维上工作,这意味着我们需要在垂直和水平方向上翻转核(这就是为什么我们实现了 flip
)。
总结一下对于 X.grad
和 W.grad
的一些提示:
X.grad
out_grad
和W
的卷积,对这些操作应用了一些操作W
应该在核的维度上翻转- 如果卷积是步长的,用相应的扩张增加
out_grad
的大小 - 做一个例子来分析维度:注意你想要的
X.grad
的形状,并思考你必须如何排列/转置参数并在卷积中添加填充以实现这个形状- 这个填充取决于核的大小和卷积的
padding
参数
- 这个填充取决于核的大小和卷积的
W.grad
X
和out_grad
的卷积,对这些操作应用了一些操作W
的梯度必须在批次上累积;你如何让卷积操作本身完成这个累积?- 考虑通过转置/排列将批次变成通道
- 分析维度:你如何修改
X
和out_grad
,使它们的卷积形状与W
的形状匹配?你可能需要转置/排列结果。- 记住考虑传递给卷积的
padding
参数
- 记住考虑传递给卷积的
一般提示
- 最后处理步长卷积(你应该能够在通过大多数测试后直接插入
dilate
) - 从
padding=0
的情况开始,然后考虑改变padding
参数 - 你可以使用多次
transpose
调用来“排列”轴
提前跳到 nn.Conv,通过前向测试,然后使用下面的测试和 nn.Conv 反向测试来调试你的实现,这可能也很有用。