7.4. 多输入多输出通道¶ 在 SageMaker Studio Lab 中打开 Notebook
虽然我们在 7.1.4节中描述了构成每个图像(例如,彩色图像具有标准的RGB通道来表示红色、绿色和蓝色的量)的多个通道,以及多通道的卷积层,但到目前为止,我们通过处理单个输入和单个输出通道简化了我们所有的数值例子。这使我们能够将输入、卷积核和输出都视为二维张量。
当我们将通道加入时,我们的输入和隐藏表示都变成了三维张量。例如,每个RGB输入图像的形状为\(3\times h\times w\)。我们将这个大小为3的轴称为通道(channel)维度。通道的概念与CNNs本身一样古老:例如,LeNet-5 (LeCun et al., 1995)就使用了通道。在本节中,我们将更深入地研究具有多个输入和多个输出通道的卷积核。
import torch
from d2l import torch as d2l
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
import jax
from jax import numpy as jnp
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
import tensorflow as tf
from d2l import tensorflow as d2l
7.4.1. 多输入通道¶
当输入数据包含多个通道时,我们需要构造一个与输入数据具有相同输入通道数的卷积核,以便它能与输入数据进行互相关运算。假设输入数据的通道数为\(c_\textrm{i}\),卷积核的输入通道数也需要是\(c_\textrm{i}\)。如果我们的卷积核的窗口形状是\(k_\textrm{h}\times k_\textrm{w}\),那么,当\(c_\textrm{i}=1\)时,我们可以认为我们的卷积核只是一个形状为\(k_\textrm{h}\times k_\textrm{w}\)的二维张量。
然而,当\(c_\textrm{i}>1\)时,我们需要一个包含每个输入通道的形状为\(k_\textrm{h}\times k_\textrm{w}\)的张量的核。将这\(c_\textrm{i}\)个张量连接在一起,得到一个形状为\(c_\textrm{i}\times k_\textrm{h}\times k_\textrm{w}\)的卷积核。由于输入和卷积核各有\(c_\textrm{i}\)个通道,我们可以对每个通道的输入的二维张量和卷积核的二维张量进行互相关运算,然后将这\(c_\textrm{i}\)个结果相加(在通道上求和)得到一个二维张量。这是多通道输入和多输入通道卷积核之间的二维互相关运算的结果。
图 7.4.1提供了一个具有两个输入通道的二维互相关示例。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:\((1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56\)。
图 7.4.1 具有两个输入通道的互相关计算。¶
为了确保我们真正理解这里发生了什么,我们可以自己实现多输入通道的互相关运算。注意,我们所做的只是对每个通道执行一个互相关运算,然后将结果相加。
def corr2d_multi_in(X, K):
# Iterate through the 0th dimension (channel) of K first, then add them up
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
def corr2d_multi_in(X, K):
# Iterate through the 0th dimension (channel) of K first, then add them up
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
def corr2d_multi_in(X, K):
# Iterate through the 0th dimension (channel) of K first, then add them up
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
def corr2d_multi_in(X, K):
# Iterate through the 0th dimension (channel) of K first, then add them up
return tf.reduce_sum([d2l.corr2d(x, k) for x, k in zip(X, K)], axis=0)
我们可以构造对应于图 7.4.1中值的输入张量X
和核张量K
,以验证互相关运算的输出。
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
tensor([[ 56., 72.],
[104., 120.]])
X = np.array([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = np.array([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
[22:10:49] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
array([[ 56., 72.],
[104., 120.]])
X = jnp.array([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = jnp.array([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
Array([[ 56., 72.],
[104., 120.]], dtype=float32)
X = tf.constant([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = tf.constant([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[ 56., 72.],
[104., 120.]], dtype=float32)>
7.4.2. 多输出通道¶
无论输入通道数是多少,到目前为止我们总是只得到一个输出通道。然而,正如我们在7.1.4节中讨论的,事实证明在每一层都有多个通道是至关重要的。在最流行的神经网络架构中,我们实际上随着神经网络的深入而增加通道维度,通常通过下采样来用空间分辨率换取更大的通道深度。直观上,你可以认为每个通道响应一组不同的特征。现实比这要复杂一些。一个天真的解释会认为表示是逐像素或逐通道独立学习的。相反,通道被优化为共同有用。这意味着,一些通道空间中的方向可能对应于检测边缘,而不是将单个通道映射到边缘检测器。
用\(c_\textrm{i}\)和\(c_\textrm{o}\)分别表示输入和输出通道的数量,用\(k_\textrm{h}\)和\(k_\textrm{w}\)表示核的高度和宽度。为了得到一个多通道的输出,我们可以为每个输出通道创建一个形状为\(c_\textrm{i}\times k_\textrm{h}\times k_\textrm{w}\)的核张量。我们在输出通道维度上将它们连接起来,这样卷积核的形状就是\(c_\textrm{o}\times c_\textrm{i}\times k_\textrm{h}\times k_\textrm{w}\)。在互相关运算中,每个输出通道上的结果都是由与该输出通道对应的卷积核计算得出的,并从输入张量的所有通道中获取输入。
我们实现一个互相关函数来计算多个通道的输出,如下所示。
def corr2d_multi_in_out(X, K):
# Iterate through the 0th dimension of K, and each time, perform
# cross-correlation operations with input X. All of the results are
# stacked together
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
def corr2d_multi_in_out(X, K):
# Iterate through the 0th dimension of K, and each time, perform
# cross-correlation operations with input X. All of the results are
# stacked together
return np.stack([corr2d_multi_in(X, k) for k in K], 0)
def corr2d_multi_in_out(X, K):
# Iterate through the 0th dimension of K, and each time, perform
# cross-correlation operations with input X. All of the results are
# stacked together
return jnp.stack([corr2d_multi_in(X, k) for k in K], 0)
def corr2d_multi_in_out(X, K):
# Iterate through the 0th dimension of K, and each time, perform
# cross-correlation operations with input X. All of the results are
# stacked together
return tf.stack([corr2d_multi_in(X, k) for k in K], 0)
我们通过将K
的核张量与K+1
和K+2
连接,构造一个具有三个输出通道的平凡卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
torch.Size([3, 2, 2, 2])
K = np.stack((K, K + 1, K + 2), 0)
K.shape
(3, 2, 2, 2)
K = jnp.stack((K, K + 1, K + 2), 0)
K.shape
(3, 2, 2, 2)
K = tf.stack((K, K + 1, K + 2), 0)
K.shape
TensorShape([3, 2, 2, 2])
下面,我们对输入张量X
和核张量K
执行互相关运算。现在输出包含三个通道。第一个通道的结果与之前的输入张量X
和多输入通道、单输出通道核的结果一致。
corr2d_multi_in_out(X, K)
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
corr2d_multi_in_out(X, K)
array([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
corr2d_multi_in_out(X, K)
Array([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]], dtype=float32)
corr2d_multi_in_out(X, K)
<tf.Tensor: shape=(3, 2, 2), dtype=float32, numpy=
array([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]], dtype=float32)>
7.4.3. \(1\times 1\) 卷积层¶
起初,一个\(1 \times 1\)卷积,即\(k_\textrm{h} = k_\textrm{w} = 1\),似乎没有多大意义。毕竟,卷积是关联相邻像素的。一个\(1 \times 1\)卷积显然不这样做。尽管如此,它们是受欢迎的操作,有时被包含在复杂的深度网络设计中 (Lin et al., 2013, Szegedy et al., 2017)。让我们详细看看它实际上做了什么。
因为使用了最小窗口,\(1\times 1\)卷积失去了较大卷积层识别由高度和宽度维度中相邻元素之间的相互作用组成的模式的能力。\(1\times 1\)卷积的唯一计算发生在通道维度上。
图 7.4.2展示了使用\(1\times 1\)卷积核(具有3个输入通道和2个输出通道)的互相关计算。注意,输入和输出具有相同的高度和宽度。输出中的每个元素都源于输入图像中相同位置元素的线性组合。你可以将\(1\times 1\)卷积层看作是在每个像素位置应用的全连接层,以将\(c_\textrm{i}\)个对应的输入值转换为\(c_\textrm{o}\)个输出值。因为这仍然是一个卷积层,所以权重在像素位置之间是共享的。因此,\(1\times 1\)卷积层需要\(c_\textrm{o}\times c_\textrm{i}\)个权重(加上偏置)。另请注意,卷积层通常后跟非线性。这确保了\(1 \times 1\)卷积不能简单地折叠到其他卷积中。
图 7.4.2 互相关计算使用具有三个输入通道和两个输出通道的\(1\times 1\)卷积核。输入和输出具有相同的高度和宽度。¶
让我们检查一下这在实践中是否有效:我们使用全连接层实现一个\(1 \times 1\)卷积。唯一的事情是,我们需要在矩阵乘法前后对数据形状进行一些调整。
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# Matrix multiplication in the fully connected layer
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# Matrix multiplication in the fully connected layer
Y = np.dot(K, X)
return Y.reshape((c_o, h, w))
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# Matrix multiplication in the fully connected layer
Y = jnp.matmul(K, X)
return Y.reshape((c_o, h, w))
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = tf.reshape(X, (c_i, h * w))
K = tf.reshape(K, (c_o, c_i))
# Matrix multiplication in the fully connected layer
Y = tf.matmul(K, X)
return tf.reshape(Y, (c_o, h, w))
在执行\(1\times 1\)卷积时,上述函数等同于之前实现的互相关函数corr2d_multi_in_out
。让我们用一些样本数据来检查一下。
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
X = np.random.normal(0, 1, (3, 3, 3))
K = np.random.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(np.abs(Y1 - Y2).sum()) < 1e-6
X = jax.random.normal(jax.random.PRNGKey(d2l.get_seed()), (3, 3, 3)) + 0 * 1
K = jax.random.normal(jax.random.PRNGKey(d2l.get_seed()), (2, 3, 1, 1)) + 0 * 1
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(jnp.abs(Y1 - Y2).sum()) < 1e-6
X = tf.random.normal((3, 3, 3), 0, 1)
K = tf.random.normal((2, 3, 1, 1), 0, 1)
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(tf.reduce_sum(tf.abs(Y1 - Y2))) < 1e-6
7.4.4. 小结¶
通道使我们能够结合两全其美的优点:MLP允许显著的非线性,而卷积允许对特征进行局部分析。特别是,通道允许CNN同时处理多种特征,例如边缘和形状检测器。它们还在由平移不变性和局部性引起的参数急剧减少与计算机视觉中对表达丰富和多样化模型的需求之间提供了一个实用的折衷方案。
但是请注意,这种灵活性是有代价的。给定一个大小为\((h \times w)\)的图像,计算一个\(k \times k\)卷积的成本是\(\mathcal{O}(h \cdot w \cdot k^2)\)。对于\(c_\textrm{i}\)和\(c_\textrm{o}\)个输入和输出通道,这增加到\(\mathcal{O}(h \cdot w \cdot k^2 \cdot c_\textrm{i} \cdot c_\textrm{o})\)。对于一个\(256 \times 256\)像素的图像,使用一个\(5 \times 5\)的核以及\(128\)个输入和输出通道,这相当于超过530亿次操作(我们将乘法和加法分开计算)。稍后我们将遇到降低成本的有效策略,例如,通过要求通道操作是块对角的,从而产生像ResNeXt这样的架构(Xie et al., 2017)。
7.4.5. 练习¶
假设我们有两个大小分别为\(k_1\)和\(k_2\)的卷积核(中间没有非线性)。
证明该操作的结果可以用单个卷积来表示。
等效的单个卷积的维度是多少?
反之亦然吗,即,你总是可以将一个卷积分解为两个较小的卷积吗?
假设输入形状为\(c_\textrm{i}\times h\times w\),卷积核形状为\(c_\textrm{o}\times c_\textrm{i}\times k_\textrm{h}\times k_\textrm{w}\),填充为\((p_\textrm{h}, p_\textrm{w})\),步幅为\((s_\textrm{h}, s_\textrm{w})\)。
前向传播的计算成本(乘法和加法)是多少?
内存占用是多少?
后向计算的内存占用是多少?
反向传播的计算成本是多少?
如果我们将输入通道数\(c_\textrm{i}\)和输出通道数\(c_\textrm{o}\)都加倍,计算量会增加多少倍?如果我们加倍填充会发生什么?
本节最后一个例子中的变量
Y1
和Y2
完全相同吗?为什么?将卷积表示为矩阵乘法,即使卷积窗口不是\(1 \times 1\)。
你的任务是实现一个使用\(k \times k\)核的快速卷积。其中一个候选算法是横向扫描源,读取一个\(k\)宽的条带,并一次计算一个\(1\)宽的输出条带。另一种方法是读取一个\(k + \Delta\)宽的条带,并计算一个\(\Delta\)宽的输出条带。为什么后者更可取?你选择\(\Delta\)的大小有限制吗?
假设我们有一个\(c \times c\)的矩阵。
如果矩阵被分解成\(b\)个块,与块对角矩阵相乘会快多少?
有\(b\)个块的缺点是什么?你如何至少部分地解决它?