Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_convolutional-neural-networks/conv-layer.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_convolutional-neural-networks/conv-layer.ipynb .. _sec_conv_layer: 图像的卷积 ========== 现在我们了解了卷积层在理论上的工作原理, 我们准备看看它们在实践中是如何工作的。 基于我们对卷积神经网络的研究 作为探索图像数据结构的有效架构, 我们坚持以图像为榜样。 互关联算子 ---------- 回想一下,严格来说,\ *卷积*\ 层 是一个(轻微的)用词不当,因为它们表达的操作 更准确地说是互相关。 在卷积层中,输入数组 和一个\ *相关内核*\ 数组相结合 通过互相关操作生成输出阵列。 让我们暂时忽略频道,看看它是如何工作的 具有二维数据和隐藏表示。 In :numref:`fig_correlation`\ , 输入是一个二维数组 高度为3,宽度为3。 我们将数组的形状标记为\ :math:`3 \times 3` 或 (:math:`3`, :math:`3`). 内核的高度和宽度都是\ :math:`2`. 请注意,在深度学习研究社区, *一个过滤器*\ ,或者只是图层的\ *权重*\ 。 内核窗口的形状 由内核的高度和宽度给出 (这里是:math:`2 \times 2`)。 .. _fig_correlation: .. figure:: https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/correlation.svg Two-dimensional cross-correlation operation. The shaded portions are the first output element and the input and kernel array elements used in its computation: :math:`0\times0+1\times1+3\times2+4\times3=19`. 在二维互相关运算中, 我们从卷积窗口开始 在输入数组的左上角 然后将其滑动到输入阵列上, 从左到右,从上到下。 当卷积窗口滑动到某个位置时, 该窗口中包含的输入子数组 内核数组相乘(elementwise) 然后将得到的数组求和 产生单个标量值。 这个结果给出了输出数组的值 在相应的位置。 这里,输出数组的高度为2,宽度为2 这四个元素是从 二维互相关运算: .. math:: 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. 请注意,沿每个轴,输出 略小于输入。 因为内核的宽度和高度都大于1, 我们只能正确地计算互相关 对于内核完全位于图像中的位置, 输出大小由输入大小 :math:`H \times W`\ 给出 减去卷积内核的大小 :math:`h \times w` 通过 :math:`(H-h+1) \times (W-w+1)`\ 。 这是因为我们需要足够的空间 在图像上'移动'卷积核 (稍后我们将看到如何保持大小不变 通过在图像边界周围填充零 这样就有足够的空间来移动内核)。 接下来,我们在 ``corr2d`` 函数中实现这个过程, 它接受输入数组 ``X`` 和内核数组 ``K`` 并返回输出数组 ``Y``. 但首先我们将导入相关的库。 .. code:: java %load ../utils/djl-imports .. code:: java public NDArray corr2d(NDArray X, NDArray K){ // 计算二维互关联。 int h = (int) K.getShape().get(0); int w = (int) K.getShape().get(1); NDArray Y = manager.zeros(new Shape(X.getShape().get(0) - h + 1, X.getShape().get(1) - w + 1)); for(int i=0; i < Y.getShape().get(0); i++){ for(int j=0; j < Y.getShape().get(1); j++){ Y.set(new NDIndex(i + "," + j), X.get(i + ":" + (i+h) + "," + j + ":" + (j+w)).mul(K).sum()); } } return Y; } 我们可以构造输入数组\ ``X``\ 和内核数组\ ``K`` 从上图来看 验证上述实现的输出 二维互相关运算。 .. code:: java NDManager manager = NDManager.newBaseManager(); NDArray X = manager.create(new float[]{0,1,2,3,4,5,6,7,8}, new Shape(3,3)); NDArray K = manager.create(new float[]{0,1,2,3}, new Shape(2,2)); System.out.println(corr2d(X, K)); .. parsed-literal:: :class: output ND: (2, 2) gpu(0) float32 [[19., 25.], [37., 43.], ] 卷积层 ------ 卷积层将输入和内核相互关联 并添加标量偏差以产生输出。 卷积层的两个参数 是内核和标量偏差。 当训练基于卷积层的模型时, 我们通常会随机初始化内核, 就像我们使用完全连接的层一样。 我们现在准备实现一个二维卷积层 基于上面定义的 ``corr2d`` 函数。 在 ``ConvolutionalLayer`` 构造函数中, 我们声明 ``weight`` 和 ``bias`` 为两个类参数。 正向计算函数 ``forward`` 调用\ ``corr2d`` 函数并添加偏差。 与 :math:`h \times w` 互相关一样 我们也提到卷积层 作为 :math:`h \times w` 卷积。 .. code:: java public class ConvolutionalLayer{ private NDArray w; private NDArray b; public NDArray getW(){ return w; } public NDArray getB(){ return b; } public ConvolutionalLayer(Shape shape){ NDManager manager = NDManager.newBaseManager(); w = manager.create(shape); b = manager.randomNormal(new Shape(1)); w.setRequiresGradient(true); } public NDArray forward(NDArray X){ return corr2d(X, w).add(b); } } 图像中的目标边缘检测 -------------------- 让我们花一点时间来分析卷积层的简单应用: 检测图像中物体的边缘 通过找到像素变化的位置。 首先,我们构造一个 :math:`6\times 8` 像素的'图像'。 中间四列为黑色(0),其余为白色(1)。 .. code:: java X = manager.ones(new Shape(6,8)); X.set(new NDIndex(":" + "," + 2 + ":" + 6), 0f); System.out.println(X); .. parsed-literal:: :class: output ND: (6, 8) gpu(0) float32 [[1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], ] 接下来,我们构造一个内核\ ``K`` ,高度为\ :math:`1`\ ,宽度为\ :math:`2`\ 。 当我们对输入进行互相关运算时, 如果水平相邻的元素相同, 输出为0。否则,输出为非零。 .. code:: java K = manager.create(new float[]{1, -1}, new Shape(1,2)); 我们已经准备好执行互相关操作 参数为\ ``X``\ (我们的输入)和\ ``K``\ (我们的内核)。 正如你所看到的,我们从白色到黑色的边缘检测到1 和-1表示从黑色到白色的边缘。 所有其他输出值为\ :math:`0`\ 。 .. code:: java NDArray Y = corr2d(X, K); Y .. parsed-literal:: :class: output ND: (6, 7) gpu(0) float32 [[ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], ] 我们现在可以将内核应用于转置图像。 正如所料,它消失了。内核\ ``K``\ 只检测垂直边缘。 .. code:: java corr2d(X.transpose(), K); .. parsed-literal:: :class: output ND: (8, 5) gpu(0) float32 [[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], ] 学习内核 -------- 用有限差分\ ``[1, -1]``\ 设计一个边缘检测器非常简洁 如果我们知道这正是我们想要的。 然而,当我们看到更大的内核时, 并考虑连续的卷积层, 可能无法具体说明 每个过滤器应该手动执行的操作。 现在让我们看看是否可以学习从\ ``X``\ 生成\ ``Y``\ 的内核 只查看(输入、输出)对。 我们首先构造一个卷积层 并将其内核初始化为随机数组。 接下来,在每次迭代中,我们将使用平方误差 将\ ``Y`` 与卷积层的输出进行比较。 然后我们可以计算梯度来更新权重。 为了简单起见,在这个卷积层中, 我们将忽略这种偏见。 这一次,我们将使用DJL内置的\ ``Block``\ 和\ ``Conv2d``\ 类。 .. code:: java X = X.reshape(1,1,6,8); Y = Y.reshape(1,1,6,7); Loss l2Loss = Loss.l2Loss(); .. code:: java // 构造一个具有1个输出通道和一个 // 形核(1,2)。为了简单起见,我们忽略了这里的偏见 Block block = Conv2d.builder() .setKernelShape(new Shape(1, 2)) .optBias(false) .setFilters(1) .build(); block.setInitializer(new NormalInitializer(), Parameter.Type.WEIGHT); block.initialize(manager, DataType.FLOAT32, X.getShape()); // 二维卷积层使用四维输入和输出 // 输出格式为(例如,通道、高度、宽度),其中批次 // 大小(批次中的示例数)和通道数均为1 ParameterList params = block.getParameters(); NDArray wParam = params.get(0).getValue().getArray(); wParam.setRequiresGradient(true); NDArray lossVal = null; ParameterStore parameterStore = new ParameterStore(manager, false); NDArray lossVal = null; for (int i = 0; i < 10; i++) { wParam.setRequiresGradient(true); try (GradientCollector gc = Engine.getInstance().newGradientCollector()) { NDArray yHat = block.forward(parameterStore, new NDList(X), true).singletonOrThrow(); NDArray l = l2Loss.evaluate(new NDList(Y), new NDList(yHat)); lossVal = l; gc.backward(l); } // 更新内核 wParam.subi(wParam.getGradient().mul(0.40f)); if((i+1)%2 == 0){ System.out.println("batch " + (i+1) + " loss: " + lossVal.sum().getFloat()); } } .. parsed-literal:: :class: output batch 2 loss: 0.12571818 batch 4 loss: 0.09935227 batch 6 loss: 0.07851635 batch 8 loss: 0.062050212 batch 10 loss: 0.049037326 请注意,经过10次迭代后,错误已降至一个较小的值。现在我们来看看我们学习的内核数组。 .. code:: java ParameterList params = block.getParameters(); NDArray wParam = params.get(0).getValue().getArray(); wParam .. parsed-literal:: :class: output weight: (1, 1, 1, 2) gpu(0) float32 hasGradient [[[[ 0.4475, -0.4477], ], ], ] 事实上,学习到的内核阵列正在接近 到我们之前定义的内核数组\ ``K``\ 。 互关联和卷积 ------------ 回想一下我们在信件前一节中的观察 在互相关算子和卷积算子之间。 上图显示了这种对应关系。 只需将内核从左下角翻转到右上角。 在这种情况下,总和中的索引被恢复, 然而,同样的结果也可以得到。 与深度学习文献中的标准术语保持一致, 我们将继续提到互相关运算 作为一种卷积,严格地说,它略有不同。 总结 ---- - 二维卷积层的核心计算是二维互相关运算。在最简单的形式中,它对二维输入数据和内核执行互相关操作,然后添加偏差。 - 我们可以设计一个内核来检测图像中的边缘。 - 我们可以从数据中学习内核的参数。 练习 ---- 1. 构造一个带有对角边的图像\ ``X``\ 。 - 如果对其应用内核\ ``K``\ ,会发生什么? - 如果变换\ ``X``\ 顺序,会发生什么? - 如果变换\ ``K``\ 顺序,会发生什么? 2. 当您尝试自动查找我们创建的 ``Conv2d`` 类的渐变时,您会看到什么样的错误消息? 3. 如何通过更改输入和内核数组将互相关运算表示为矩阵乘法? 4. 手动设计一些内核。 - 二阶导数的核的形式是什么? - 拉普拉斯算子的核心是什么? - 积分的核心是什么? - 获得\ :math:`d`\ 导数的内核最小大小是多少?