为了实现二维卷积的反向传播,我们(可能)需要一个能够翻转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 的梯度。(这样做是为了我们不必实现 GetItemSetItem 操作,如果没有额外的优化,这些操作在反向传播时可能会非常低效。)

nn.Conv 反向传播

寻找二维多通道卷积的梯度在技术上可能相当具有挑战性(特别是“严格”地)。我们将在这里提供一些有用的提示。基本上,我们鼓励你利用一个令人惊讶的事实:通常,任何能使维度匹配的方法都是正确的。

最终,卷积的反向传播可以通过卷积操作本身来完成,通过使用 flipdilate 和多次应用 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_gradWX 是适用于二维多通道卷积的张量而不是矩阵):

X.grad = ≈conv(≈out_grad, ≈W) \

W.grad = ≈conv(≈X, ≈out_grad)

在这里,“≈”表示你需要对这些项应用一些额外的操作,以使维度匹配,比如排列/转置轴,扩张,改变卷积函数的 padding= 参数,或者排列/转置卷积的结果轴。

正如我们在课堂上的 最后几张幻灯片 中看到的,卷积的转置可以通过简单地翻转核来找到。由于我们在二维而不是一维上工作,这意味着我们需要在垂直和水平方向上翻转核(这就是为什么我们实现了 flip)。

总结一下对于 X.gradW.grad 的一些提示:

X.grad

W.grad

一般提示

提前跳到 nn.Conv,通过前向测试,然后使用下面的测试和 nn.Conv 反向测试来调试你的实现,这可能也很有用。