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/channels.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/channels.ipynb .. _sec_channels: 多个输入和输出通道 ================== 虽然我们已经描述了多个通道 包括每个图像的(例如,彩色图像具有标准RGB通道) 表示红色、绿色和蓝色的数量), 到目前为止,我们简化了所有的数值例子 只使用一个输入和一个输出通道。 这让我们能够思考我们的输入,卷积核, 并以二维数组的形式输出。 当我们在混合中加入频道时, 我们的输入和隐藏的表示 两者都变成了三维阵列。 例如,每个RGB输入图像的形状为\ :math:`3\times h\times w`\ 。 我们称这个轴为通道尺寸,大小为3。 在本节中,我们将更深入地了解 具有多个输入和多个输出通道的卷积核。 多输入通道 ---------- 当输入数据包含多个通道时, 我们需要构造一个卷积核 使用与输入数据相同数量的输入通道, 这样它就可以与输入数据进行互相关。 假设输入数据的通道数为\ :math:`c_i`\ , 卷积内核的输入通道数也需要是\ :math:`c_i`\ 。如果我们的卷积核的窗口形状是\ :math:`k_h\times k_w`\ , 然后当\ :math:`c_i=1`\ ,时,我们可以考虑我们的卷积核 就像形状\ :math:`k_h\times k_w`\ 的二维数组。 然而,当\ :math:`c_i>1`\ 时,我们需要一个内核 对于每个输入通道,\ *它包含一个形状为\ :math:`k_h\times k_w`*\ 的数组。 将这些\ :math:`c_i`\ 数组连接在一起 产生一个形状为\ :math:`c_i\times k_h\times k_w`\ 的卷积核。 由于输入和卷积内核都有\ :math:`c_i`\ 通道, 我们可以进行互相关运算 关于输入的二维数组 以及卷积核的二维核数组 对于每个频道,将\ :math:`c_i`\ 结果相加 (通过通道求和) 生成二维数组。 这是二维互相关的结果 在多通道输入数据和 一个\ *多输入通道*\ 卷积内核。 在 :numref:`fig_conv_multi_in`\ ,我们举一个例子 与两个输入通道的二维互相关。 阴影部分是第一个输出元素 以及计算中使用的输入和内核数组元素: :math:`(1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56`. .. _fig_conv_multi_in: .. figure:: https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/conv-multi-in.svg 使用2个输入通道进行互相关计算。阴影部分是第一个输出元素以及计算中使用的输入和内核数组元素: :math:`(1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56`. 为了确保我们真正了解这里发生了什么, 我们可以用多个输入通道实现互相关运算。 请注意,我们所做的只是执行一个互相关操作 然后使用\ ``sum()``\ 函数将结果相加。 让我们先导入这些库,然后再开始讨论多个输入和输出通道的概念。 .. code:: java %load ../utils/djl-imports .. code:: java NDManager manager = NDManager.newBaseManager(); public NDArray corr2D(NDArray X, NDArray K) { long h = K.getShape().get(0); long w = 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++) { NDArray temp = X.get(i + ":" + (i + h) + "," + j + ":" + (j + w)).mul(K); Y.set(new NDIndex(i + "," + j), temp.sum()); } } return Y; } public NDArray corr2dMultiIn(NDArray X, NDArray K) { long h = K.getShape().get(0); long w = K.getShape().get(1); // 首先,沿着'X'的第0维(通道维)进行遍历 // “K”。然后,把它们加在一起 NDArray res = manager.zeros(new Shape(X.getShape().get(0) - h + 1, X.getShape().get(1) - w + 1)); for (int i = 0; i < X.getShape().get(0); i++) { for (int j = 0; j < K.getShape().get(0); j++) { if (i == j) { res = res.add(corr2D(X.get(new NDIndex(i)), K.get(new NDIndex(j)))); } } } return res; } 我们可以构造输入数组\ ``X``\ 和内核数组\ ``K`` 对应于上图中的值 验证互相关操作的输出。 .. code:: java NDArray X = manager.create(new Shape(2, 3, 3), DataType.INT32); X.set(new NDIndex(0), manager.arange(9)); X.set(new NDIndex(1), manager.arange(1, 10)); X = X.toType(DataType.FLOAT32, true); NDArray K = manager.create(new Shape(2, 2, 2), DataType.INT32); K.set(new NDIndex(0), manager.arange(4)); K.set(new NDIndex(1), manager.arange(1, 5)); K = K.toType(DataType.FLOAT32, true); corr2dMultiIn(X, K); .. parsed-literal:: :class: output ND: (2, 2) gpu(0) float32 [[ 56., 72.], [104., 120.], ] 多输出通道 ---------- 无论输入通道的数量如何, 到目前为止,我们总是以一个输出通道结束。 但是,正如我们之前讨论的, 事实证明,在每一层有多个通道是必要的。 在最流行的神经网络架构中, 我们实际上增加了通道维度 当我们在神经网络中往上走的时候, 通常通过降低采样来权衡空间分辨率 更大的\ *通道深度*\ 。 凭直觉,你可以想到每个频道 作为对不同特征的回应。 现实比对这种直觉的最天真的解释要复杂一些,因为表征并不是独立学习的,而是经过优化以共同有用的。 因此,可能不是单个通道学习边缘检测器,而是通道空间中的某个方向对应于检测边缘。 用\ :math:`c_i`\ 和\ :math:`c_o`\ 表示数字 输入和输出通道, 让\ :math:`k_h`\ 和\ :math:`k_w`\ 作为内核的高度和宽度。 要获得具有多个通道的输出, 我们可以创建一个内核数组 形状为\ :math:`c_i\times k_h\times k_w` 对于每个输出通道。 我们在输出通道维度上连接它们, 所以卷积核的形状 是\ :math:`c_o\times c_i\times k_h\times k_w`\ 。 在互相关运算中, 计算每个输出通道上的结果 来自对应于该输出通道的卷积内核 并从输入阵列中的所有通道获取输入。 我们实现了一个互相关函数 计算多个通道的输出,如下所示。 .. code:: java public NDArray corrMultiInOut(NDArray X, NDArray K) { long cin = K.getShape().get(0); long h = K.getShape().get(2); long w = K.getShape().get(3); // 沿“K”的第0维遍历,每次执行 // 输入'X'的互相关运算。所有结果都是正确的 // 使用stack函数合并在一起 NDArray res = manager.create(new Shape(cin, X.getShape().get(1) - h + 1, X.getShape().get(2) - w + 1)); for (int j = 0; j < K.getShape().get(0); j++) { res.set(new NDIndex(j), corr2dMultiIn(X, K.get(new NDIndex(j)))); } return res; } 我们构造了一个具有3个输出通道的卷积核 通过将内核数组\ ``K``\ 与\ ``K+1``\ 连接起来 (\ ``K``\ 中每个元素加一个)和\ ``K+2``\ 。 .. code:: java K = NDArrays.stack(new NDList(K, K.add(1), K.add(2))); K.getShape() .. parsed-literal:: :class: output (3, 2, 2, 2) 下面,我们执行互相关操作 在输入数组\ ``X``\ 和内核数组\ ``K``\ 上。 现在输出包含3个通道。 第一个通道的结果是一致的 使用上一个输入数组的结果\ ``X`` 以及多输入通道, 单输出通道内核。 .. code:: java corrMultiInOut(X, K); .. parsed-literal:: :class: output ND: (3, 2, 2) gpu(0) float32 [[[ 56., 72.], [104., 120.], ], [[ 76., 100.], [148., 172.], ], [[ 96., 128.], [192., 224.], ], ] :math:`1\times 1` 卷积层 ------------------------ 首先,一个\ :math:`1 \times 1`\ 的卷积,即 :math:`k_h = k_w = 1`\ , 这似乎没有多大意义。 毕竟,卷积将相邻像素关联起来。 一个\ :math:`1 \times 1`\ 的卷积显然不是。 尽管如此,它们还是很受欢迎的业务,有时也包括在内 在复杂深层网络的设计中。 让我们详细了解一下它的实际功能。 因为使用了最小窗口, :math:`1\times 1` 卷积将失去该能力 更大的卷积层 识别由相互作用组成的模式 在高度和宽度尺寸中的相邻元素之间。 只会进行\ :math:`1\times 1`\ 的卷积运算 在通道维度上。 :numref:`fig_conv_1x1` 显示了互相关计算 使用\ :math:`1\times 1`\ 卷积内核 有3个输入通道和2个输出通道。 请注意,输入和输出具有相同的高度和宽度。 输出中的每个元素都是派生的 来自同一位置的元素的\ *线性组合* 在输入图像中。 你可以想象\ :math:`1\times 1`\ 的卷积层 构成一个应用于每个像素位置的完全连接层 将\ :math:`c_i`\ 对应的输入值转换为\ :math:`c_o`\ 输出值。 因为这仍然是一个卷积层, 权重在像素位置上绑定。 因此\ :math:`1\times 1`\ 卷积层需要\ :math:`c_o\times c_i`\ 权重 (加上偏差项)。 .. _fig_conv_1x1: .. figure:: https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/conv-1x1.svg The cross-correlation computation uses the :math:`1\times 1` convolution kernel with 3 input channels and 2 output channels. The inputs and outputs have the same height and width. 让我们看看这在实践中是否有效: 我们实现了\ :math:`1 \times 1`\ 的卷积运算 使用完全连接的层。 唯一的问题是我们需要做一些调整 矩阵乘法前后的数据形状。 .. code:: java public NDArray corr2dMultiInOut1x1(NDArray X, NDArray K) { long channelIn = X.getShape().get(0); long height = X.getShape().get(1); long width = X.getShape().get(2); long channelOut = K.getShape().get(0); X = X.reshape(channelIn, height * width); K = K.reshape(channelOut, channelIn); NDArray Y = K.dot(X); // 全连通层中的矩阵乘法 return Y.reshape(channelOut, height, width); } 执行\ :math:`1\times 1`\ 卷积时, 上述函数相当于之前实现的互相关函数 ``corrMultiInOut()``. 让我们用一些参考数据来验证这一点。 .. code:: java X = manager.randomUniform(0f, 1.0f, new Shape(3, 3, 3)); K = manager.randomUniform(0f, 1.0f, new Shape(2, 3, 1, 1)); NDArray Y1 = corr2dMultiInOut1x1(X, K); NDArray Y2 = corrMultiInOut(X, K); System.out.println(Math.abs(Y1.sum().getFloat() - Y2.sum().getFloat()) < 1e-6); .. parsed-literal:: :class: output true 总结 ---- - 可以使用多个通道来扩展卷积层的模型参数。 - 当以每像素为基础应用时,\ :math:`1\times 1`\ 卷积层相当于完全连接层。 - :math:`1\times 1`\ 卷积层通常用于调整网络层之间的通道数并控制模型复杂性。 练习 ---- 1. 假设我们有两个卷积核,大小分别为\ :math:`k_1`\ 和\ :math:`k_2`\ (中间没有非线性)。 - 证明运算结果可以用一次卷积来表示。 - 等效单卷积的维数是多少? - 反过来是真的吗? 2. 假设输入形状为\ :math:`c_i\times h\times w`\ ,卷积核形状为\ :math:`c_o\times c_i\times k_h\times k_w`\ ,填充为 :math:`(p_h, p_w)`\ ,步长为\ :math:`(s_h, s_w)`\ 。 - 正向计算的计算成本(乘法和加法)是多少? - 内存占用是多少? - 反向计算的内存占用是多少? - 反向计算的计算成本是多少? 3. 如果我们将输入通道数\ :math:`c_i`\ 和输出通道数\ :math:`c_o`\ 翻一番,计算的数量会增加多少?如果我们把填充物翻一番会怎么样? 4. 如果卷积核的高度和宽度是\ :math:`k_h=k_w=1`\ ,那么正向计算的复杂度是多少? 5. 本节最后一个示例中的变量\ ``Y1``\ 和\ ``Y2``\ 是否完全相同?为什么? 6. 当卷积窗口不是\ :math:`1\times 1`\ 时,如何使用矩阵乘法实现卷积?