6.3 卷积的基本原理

🏷️sec0603_Convolutional_Convolutional

在第 6.1 节中,我们提到机器学习的目标不是设计一个通用的最优算法去解决所有问题,而是去理解什么样的数据分布与人工智能获取经验的 “真实世界” 相关,以及什么样的学习算法可以帮助我们在 “真实任务” 所关注的数据上达到最优的效果。我们在上一章中所介绍的多层感知机是一种十分适合处理表格数据的模型,然而对于高维感知数据(例如图像),这种缺少空间结构的模型就变得不那么实用了。在探索图像数据特性的时候,我们知道局部特性是所有图像都具备的一种内在特性。卷积神经网络正是将这种空间不变性概念系统化,从而实现了高辨识度特征的提取,进而基于这些鲁棒的特征实现使用较少参数就能够从样本学习到更多有用的表达。

6.3.1 卷积的动机

卷积运算通过三个重要的思想来帮助改进机器学习系统:稀疏交互(sparse interactions)、参数共享(parameter sharing)和多模式学习(multi-model learning)。下图给出了这三种思想的可视化示例。

图6-10 卷积的三种重要思想

图6-10 卷积的三种重要思想

6.3.1.1 稀疏交互

传统神经网络使用矩阵乘法来建立输入与输出的连接关系,乘法的两个因子分别对应于输入神经元向量和输出神经元向量。在它们所构建的参数矩阵中,每个单独的元素都唯一描述了一个输入单元与一个输出单元之间的交互关系。这意味着每个输出单元与每个输入单元都会产生交互。以 图6-10(a) 为例,与第 6-1 节相同,我们依然假设存在一幅分辨率为 1000×10001000×1000 的图片,并且隐层神经元的数量还是 100,000100,000。如果隐藏层中的每个神经元都与图像的每个输出像素相连,那么将得到 (1000×1000)×100,000=1011(1000×1000)×100,000=10^{11} 个权重参数。其中,1000×10001000×1000 是输入图像的分辨率,每个像素都代表了一个输入神经元单元。100,000100,000 是隐层神经元的数量,也即输出神经元单元。101110^{11} 代表每个输入神经元与每个输出神经元都存在独立且唯一的交互关系。这个数量级的参数可以用海量来形容了。因此,无论是计算还是存储,都会给硬件带来巨大的负担,这就是全连接网络的弊端。

卷积神经网络具有稀疏交互(也叫作稀疏连接(sparse connectivity)或者局部感知(local perception))的特性,这种特性通过使过滤器(filter)的尺度远小于输入尺度来实现。更多的时候,我们会将过滤器称为卷积核(kernel)。图 6-11 给出了全连接网络和稀疏连接网络的对比。让我们重点关注一下左图和中图中的输出单元 o3o_3 以及在输入域 xx 中影响该单元的输入单元。这些影响输出 o3o_3 的输入单元都被称为 o3o_3 的感知域(receptive field)(也叫作感受野或接受域)。对于全连接网络来说,o3o_3 是由所有输入神经元的矩阵乘法产生,所有的输入神经元都会影响 o3o_3。在中间的稀疏连接网络中,输出 o3o_3 只接受一个宽度为3的感知域的输入,此时只有3个输入会影响输出 o3o_3 的值。也就是说,对于稀疏连接网络来说,相邻层之间的神经元并不是完全进行交互的,对于每个输出单元来说,只有很少的一部分输入单元会与之有关联。不过在深度卷积神经网络中,处在网络深层的神经元单元可能会与非常靠前的某一层的绝大部分输入神经元相关,但是这种相关是间接交互的, 图6-11(右)给出了可视化的演示效果。对于输出 o3o_3 来说,经过两层的通信,输入域 xx 中的所有神经元都与其建立了间接联系。这意味着在卷积网络中,尽管直接连接都是稀疏的,但处于更深层的神经元单元可以间接地连接到全部或大部分输入图像。这种机制使得网络可以通过只描述稀疏交互来高效地描述多个变量的复杂交互。

图6-11 全连接网络(左)、稀疏连接网络(中)、深层稀疏连接网络(右)

图6-11 全连接网络(左)、稀疏连接网络(中)、深层稀疏连接网络(右)

下面,我们继续观察前面 1000×10001000×1000 的图像在稀疏交互下的变化情况(图6-10(b))。假设我们使用一系列 10×1010×10 的局部算子作为过滤器来对输入图像进行特征提取。根据数学运算规则,在每个时刻每个过滤器只能与原始图像的一个局部区域进行关联,并且这个区域的尺度与过滤器的尺度相同,也是 10×1010×10。假设这一系列的局部算子能够完全覆盖整个输入图像,那么在所有这些局部算子的共同作用下,它们就能够实现对整幅图像的特征提取。此时,所有过滤器的参数总数就等于 n×(10×10)n×(10×10)。由于局部算子是覆盖整幅图像的,所以最终的参数数量应该刚好等于图像的像素数量,即 1000×1000=1061000×1000=10^6。这个数量比前面全连接网络中的参数数量 101110^{11},少了100,000倍。这种假设是基于我们前面对图像分析时所提出的目标的局部性来进行设计的,这种假设就是模型的局部感知特性。不难想象,无论局部算子的尺度是多少,关系都不大,因为这些局部算子最终都要覆盖整幅图像,所以参数的总数始终等于输入图像的像素总和。也就是说,图像的每个像素有且仅有一个输出神经元与之相连。

通过稀疏交互,卷积神经网络实现了模型参数的快速减少。这不仅意味着模型存储需求的降低和模型统计效率的提高,也代表着只需要更少的计算量就可以获得完整的输出。效率上的提高往往是显著的。假设存在一个CNN模型,它的输入神经元和输出神经元数量分别是 mmnn。那么在全连接网络中的矩阵乘法会产生 m×nm×n 个参数。此时,算法的时间复杂度为 O(m×n)O(m×n)。如果我们设置局部算子的像素个数为kk,那么在稀疏连接网络中,权重矩阵就只有 k×nk×n 个参数,其对应的时间复杂度为 O(k×n)O(k×n)。在实际应用中,只要能够保持 kkmm 小几个数量级,即 k<<mk<<m,就能让模型在任务中取得非常良好的性价比。换句话说,在处理一幅图像时,虽然原始图像可能包含数万个像素点,但是我们可以通过仅仅占用几十个像素的过滤器,就实现检测图像上一些小而有意义的特征,例如图像的边缘和色彩。

6.3.1.2 参数共享

在前面的稀疏交互中,我们让参数数量和计算复杂度缩减了10万倍。但这就够了吗?是否可以更进一步呢?毕竟包含 10610^6 个参数的模型依然不小。假设对应到图像每个独立区域的局部算子都使用同一个局部算子,会发生什么情况?如图 6-10(c) 所示,我们使用这个局部算子从左上角开始,按照从左到右、从上至下的顺序依次扫描整幅图像。最终它将完成对整幅图像每个像素区域的特征提取。由于只存在一个局部算子,所以整幅图像的参数就只剩下 10×10=10010×10=100 个了。这种假设就是模型的参数共享。参数共享就是指在一个模型的多个特征提取函数中使用相同的参数,从而实现将多个函数变成一个函数来将降低模型的复杂度。在卷积神经网络中,过滤器的每个元素都会作用在输入的每个位置上。在多层感知机中,权重矩阵中对应于每一对输入和输出的权重都只使用一次。也就是说,每个权重都唯一地与一个输入进行相乘,从而获得唯一的输出。作为参数共享的同义词,我们可以说一个网络含有绑定的权重(或者权重共享)。因为作用于一个输入(区域)的权重也可以被绑定到其他输入(区域)的权重上。卷积运算中的参数共享使得模型只需要学习一个参数集合,而不是为每个位置都学习一个参数集合。这虽然没有降低参数计算的时间复杂性,依然是 O(k×n)O(k×n),但显著地将存储需求从 H×W=mH×W=m 降低到了 kk。由于 kk 通常要远远比 mm 小很多个数量级,因此参数共享使得卷积运算与稠密矩阵的乘法运算相比,在存储需求和统计效率方面的性能再一次获得了极大的提升。

6.3.1.3 多模式学习

此时,你可能会问,仅仅100个参数就能学到一幅图像的所有特征了吗?答案当然是否定的。在第 6.1 节中,我们介绍过一种存在于大脑内侧颞叶区域的祖母细胞,这种细胞会对特殊的信息产生响应,并且不同的祖母细胞会对不同类型的特殊信息产生响应。如果假设一种局部算子就是一个祖母细胞,它能够提取图像中的一种局部特征。那么,如果在神经网络中存在多个不同的局部算子,是不是就可以同时提取多种不同的局部特征呢? 图6-10(d) 给出了这种思想的卷积实现。举个例子,对于这样一幅爱因斯坦的图片,如果我们设置128个局部算子来学习爱因斯坦的各种不同特征,那么我们应该可以得到128种关于爱因斯坦的局部特征。比如,有的算子用来学习他的卷发模式,有的用来学习他头发的颜色,也有的用来学习他胡子的形状或者是他喜欢的领结。最后,只要将这128个局部算子组合在一起,就可以判断这幅图像中的人是否就是爱因斯坦。当然,实际中的局部算子并没有那么绝对和明显的特征指向,它们可能是不同形式的边缘、色块、圆圈(底层特征),也可能是一些底层特征的组合(高层特征)。通过融合多种独特模式来实现混合特征的辨识,这种技术就是多模式学习。多模式学习可以帮助模型获取样本不同类型的独特模式,但同时它也会增加参数的数量和计算复杂性。假设输出通道的数量为 cc,则模型参数的数量就变成了 c×kc×k,而计算复杂度也上升到了 O(c×k×n)O(c×k×n)

到目前为止,我们已经介绍了卷积的三种重要思想,分别是稀疏交互、参数共享和多模式学习,这些重要思想是卷积神经网络为什么可以替代多层感知机成为当前最热、最实用模型的关键。需要注意的是,在上面的定性分析中,我们并没有考虑填充(padding)、步长(stride)和多通道(multi-channels)带来的影响,这些技术也是卷积极其重要的组成部分,我们将在后面的两个小节中进行介绍。下面我们先来简单介绍一下卷积的基本原理。

6.3.2 卷积运算

卷积是一个数学概念,它代表两个实变函数间的一种关系运算。为了引出卷积的定义,我们先举一个例子(Goodfellow, 2016)。假设我们使用一种探测器来追踪一艘正在飞行的宇宙飞船的位置。对于每一个单独的输出 x=f(t)x=f(t),都表示宇宙飞船在 tt 时刻的位置。参数 tt 和函数 ff 都是实值,因此探测器可以获得任意时刻飞船的位置 xx。现在假设在宇宙飞船飞行的路程中存在一定程度的噪声干扰,使得探测器获得的位置 f(t)f(t) 可能存在一定的偏差。为了获得更准确的信息,我们可以通过计算多个测量结果来平滑噪声,从而获得飞船位置的低噪声估计。显然,并非所有位置的测量结果对 tt 时刻的贡献都是均等,时间上越接近 tt 的测量结果应该与 f(t)f(t) 越相关。因此,我们可以使用加权平均法来给靠近 tt 时刻的测量值赋予更高的权重。设存在加权函数 w(a)w(a),其中 aa 表示 tat_a 时刻的实际测量结果距离时刻 tt 的时间间隔。那么,我们可以得到 tat_a 时刻对 tt 时刻的贡献值 f(a)w(ta)f(a)w(t-a)。如果我们对任意时刻 tit_i 都计算这种加权贡献值,就可以得到最终关于 tt 时刻的飞船位置的加权平均值估计函数 gg:
g(t)=f(a)w(ta)da(6.1)g(t) = \int f(a)w(t-a)da \tag{6.1}

以上公式就是数学上卷积(convolution)的定义。从形式上看,我们可以粗略的理解卷积就是将函数 ff 进行翻转并移位 tt,然后测量 ffww 的重叠的累加。卷积运算通常使用星号来表示:
g(t)=(xw)(t)(6.2)g(t) = (x*w)(t) \tag{6.2}

在上面的例子中,加权函数 ww 是一个有效的概率密度函数,否则输出就不再是一个加权平均值了。与此同时,这两个函数需要满足 f,w:RdRf,w:\mathbb{R}^d \rightarrow \mathbb{R}。在卷积神经网络的术语中,第一个参数 xx 称为输入(input),第二个参数 ww 称为核函数(kernel function)。它们卷积运算的结果就是输出(output),更多的时候被称为特征映射特征图(feature map)。

在本例中,要探测器去获取每一个瞬间的反馈是不切实际的。一般来说,计算机获取的数据都是离散的数据。因此,我们也需要将时间 tt 进行离散化,然后让探测器定期反馈测量数据。假设 xxww 都是定义在整数时刻 tt 上函数,于是有卷积的离散形式:
g(t)=(xw)(t)=a=f(a)w(ta)(6.3)g(t) = (x*w)(t) = \sum^{\infin}_{a=-\infin} f(a)w(t-a) \tag{6.3}

卷积的离散形式以求和的方式进行定义,这种定义更加符合机器学习的应用。因为机器学习的输入通常都是有限维度的离散序列。卷积神经网络的设计初衷是用来探索图像数据,所以它的输入通常是一个多维的数组,而卷积核也是由学习算法优化获得的多维数组。我们将这些多维数组称为张量。为了能在多维数组上进行卷积运算,我们需要对公式 6.36.3 进行一点拓展。例如,对于以常见的二维张量(灰度图)进行输入的图像 I:X(Hm,Wn)I:X(H \in m,W \in n),如果存在一个二维的卷积核 K:W(hi,wj)K:W(h \in i,w \in j),则它们的卷积 S(i,j)S(i,j) 可以定义为:
S(i,j)=(XW)(i,j)=mnX(m,n)W(im,jn))(6.4)S(i,j) = (X*W)(i,j) = \sum_m \sum_n X(m,n)W(i-m, j-n)) \tag{6.4}

在数学的规范中,卷积是可交换的,因此对于图像 II 与 卷积核 KK 来说,我们可以等价地将它们的卷积 S(i,j)S(i,j) 写作:
S(i,j)=(WX)(i,j)=mnX(im,jn)W(m,n))(6.5)S(i,j) = (W*X)(i,j) = \sum_m \sum_n X(i-m,j-n)W(m, n)) \tag{6.5}

6.3.3 互相关运算

在上面的内容中,我们提到卷积从形式上看可以理解为对两个相关函数的翻转(flip)和叠加。这其中的翻转正是卷积可交换性的保证。从 mm 增大的角度来看,输入 XX 的索引增大的同时,卷积核 KK 的索引也在相应地减小。翻转的唯一目的就是实现可交换性,并且可交换性在理论证明时是很有意义的。但在神经网络的设计中,可交换性并没有太多的实际的价值。为了便于计算,大多数神经网络的开发库都会实现一个与卷积非常相似的函数,称为互相关运算(cross-correlation)。互相关运算与数学的卷积运算几乎相同,只是少了对卷积核的翻转操作:
S(i,j)=(XW)(i,j)=mnX(i+m,j+n)W(m,n))(6.6)S(i,j) = (X*W)(i,j) = \sum_m \sum_n X(i+m,j+n)W(m, n)) \tag{6.6}

在卷积神经网络中,严格地说,卷积层的说法是错误的。在几乎所有的机器学习库中,“卷积”都是指输入张量与核张量通过互相关运算获得输出张量。换句话说,它们实现的都是互相关函数但是又都称为卷积。在本书中我们遵循这种习惯,在不特别说明的时候卷积就是指互相关运算。下面我们具体来看一个二维张量卷积的例子。在图 6-12 中,存在一个高度为3、宽度为4的二维张量,同时存在一个高度和宽度都是2的卷积核,它们具体的值如下图所示。

图6-12 二维互相关运算

图6-12 二维互相关运算

在二维的互相关运算中,卷积核从二维输入张量的左上角开始,从左到右、从上至下进行滑动。当卷积核滑动到一个新位置时,窗口中的张量与卷积核的张量将进行内积操作。两个量对应位置的元素进行按位进行点乘,再将相乘后得到的张量进行求和操作,最终获得一个标量值。如图 6-12 中的例子,我们可以依次计算出卷积核滑动过的每个位置的标量输出(公式 6.76.7),这些标量输出组合在一起就是最终的输出张量。
1×1+2×2+5×3+6×4=442×1+3×2+6×3+7×4=543×1+4×2+7×3+8×4=645×1+6×2+9×3+10×4=846×1+7×2+10×3+11×4=947×1+8×2+11×3+12×4=104\begin{align*} 1×1+2×2+5×3+6×4 &=44 \\ 2×1+3×2+6×3+7×4 &=54 \\ \tag{6.7} 3×1+4×2+7×3+8×4 &=64 \\ 5×1+6×2+9×3+10×4 &=84 \\ 6×1+7×2+10×3+11×4 &=94 \\ 7×1+8×2+11×3+12×4 &=104 \end{align*}

在上面的例子中,我们限制只对核完全的位置进行卷积输出,这种类型的卷积一般称为有效卷积(VALID)。另一种常用的卷积是同等卷积(SAME),它通过在图像边界外填充零来保证输出的尺度与输入尺度一致。在有效卷积的设定下,输出张量的尺度会略小于输入张量的尺度。假设输入张量的尺度为 [nh,nw][n_h,n_w],且卷积核的尺度为 [kh,kw][k_h, k_w],则输出张量的尺度为 [nhkh+1,nwkw+1][n_h-k_h+1, n_w-k_w+1]

接下来,我们在 cross_correlation 函数中实现上面所介绍的互相关运算。该函数包含两个参数,分别是输入张量 XX 和卷积核张量 KK,返回值为输出张量 YY

程序清单6-1 互相关运算

    display.display(plt.gcf())
    display.clear_output(wait=True)


# @save common.cross_correlation
# codes06001_cross_correlation
def cross_correlation(X, W):
    """计算输入和卷积核的互相关运算"""
    h, w = W.shape
    Y = paddle.zeros((X.shape[0]-h+1, X.shape[1]-w+1))

下面对图 6-12 进行实现,对应到函数中,输入的两个参数分别是输入张量 XX 和核张量 WW。通过 cross_correlation 函数,将输出这两个张量的互相关计算结果。

X = paddle.to_tensor([[1.0,2.0,3.0,4.0], [5.0,6.0,7.0,8.0], [9.0,10.0,11.0,12.0]])
W = paddle.to_tensor([[1.0,2.0], [3.0,4.0]])
cross_correlation(X, W)
Tensor(shape=[2, 3], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [[44. , 54. , 64. ],
        [84. , 94. , 104.]])

6.3.4 图像的卷积层

有了互相关的概念,我们来看看图像中卷积层是如何实现和运行的。与全连接层不同,卷积层致力于保持源目标的空间结构。如果存在一个 32×32×332×32×3 的图像,我们不再需要去将它拉伸成一个长向量,而是直接以三维形态进行输入。为了能够顺利从图像中获取信息,还需要构建一个与图像具有相同尺度的卷积核(过滤器),并用该卷积核与它在图像中所覆盖的区域执行卷积运算。这就是我们在第 6.3.1 小节中介绍的稀疏交互。由于图像通常是多通道的,例如RGB三个颜色通道,因此在进行卷积核设计的时候,必须要保证它具有和输入图像或者输入特征图相同的深度。

图6-13(a),假设我们使用的卷积核尺度为 5×5×35×5×3,那么与它进行卷积运算的区域同样也是 5×5×35×5×3。注意,这里的卷积运算就是上面提到的互相关运算。在图像的卷积运算中,我们还需要增加一个偏置标量来对输出进行调制,以防止分界面总是需要经过原点带来的限制。因此,图像的卷积运算可以理解为卷积核与它在图像中所覆盖的三维张量范围内的所有元素进行点乘运算,然后再将所有乘积进行累加,最后再加上一个偏置量 bb,最终得到一个标量输出。这个标量就是卷积核对所在空间位置所提取的特征。将这个过程公式化后可得:
f(x)=WTX+b(6.8)f(x) = W^T X + b \tag{6.8}

其中,WW 是卷积核张量,XX 是输入张量,bb 是偏置量。卷积核张量和输入张量可以是二维的也可以是三维,具体要根据输入来决定。如果输入是一个灰度图,那么 WWXX 就都是二维的;如果输入是彩色图或者是卷积网络的中间层,那么 WWXX 通常都是三维的。这个运算过程与上一章多层感知机中所介绍的数学模型很相似,区别只是输入的维度。因为,我们说过卷积网络最重要的改进就是致力于保持源目标的空间结构。在上面的 公式6.8 中还存在一个转置运算T。所谓转置,就是对矩阵 WW 进行上下、左右翻转,这涉及到一个直观理解和数学运算之间的差异。一种简单的理解是,它体现的正好是数学中的卷积与图像卷积(互相关运算)的区别。另一种理解是,翻转是大规模矩阵运算提高并行效率和运算速度的有效改进方法。前面对卷积的介绍,我们可以理解为是对图像卷积的直观理解。但是在实际的计算中,为了提高计算效率,我们会对一个批次的样本同时进行计算。此时,我们会将单一的三维卷积转换为向量乘法。例如 5×5×35×5×3 的卷积核与同样是 5×5×35×5×3 的输入的卷积运算实际上会被转换为两个 75×175×1 的向量乘法。然而,按照线性代数的计算规则,两个同样形态的向量是无法进行计算的。因此,必须要将其中一个向量转置成为 1×751×75 的行向量。至于为什么要对权重 WW 进行转置而不是输入 XX 呢?其实,无论是对 WW 还是对 XX 进行转置都是可行的,只要保证最终的输出是标量即可。换句话说,对于一个卷积层来说,无论卷积核和感知域的尺度是多大,一次卷积运算的最终输出结构都只能是一个神经元单元。

图6-13 从卷积运算到卷积层

图6-13 从卷积运算到卷积层

在图像卷积运算的基础上,我们再来看看卷积网络是如何利用参数共享思想实现卷积特征提取的。对于一幅二维图像来说,要实现参数共享有一个前提,卷积核必须保证扫过图像的所有空间位置。如 图6-13(b) 所示,初始时,卷积核的左上角与输入特征图的左上角是对齐的,之后按照从左到右、从上至下的顺序进行滑动。每次滑动都会生成一个标量,我们将这些标量按照卷积核滑动的顺序进行排列,最终将得到一个新的特征图。这个特征图就是我们所说的激活特征图,它能够表征卷积核对输入层信息汲取的结果。注意在卷积核滑动的过程中,它的值始终保持不变。在最基本的卷积运算中,卷积核每次只移动 11 个像素,但实际上每次移动多个像素也是可以的,原则上只要保证卷积核最终能覆盖输入层的所有空间位置。不难发现,从输入特征图到输出特征图,尺度变小了。原来的输入尺度是 32×3232×32,而输出尺度变成了 28×2828×28。这是因为卷积核的尺度为 5×55×5,而输出仅仅只是一个标量。这种变化主要是因为卷积核在进行卷积运算的时候,没有使用任何保持尺度不变的策略,这种卷积就是我们前面提到的有效卷积。

6.3.1 小节的卷积动机中,我们还介绍了另一种思想,那就是多模式学习。图 6-14(a) 展示了如何在卷积层中实现这种思想。相比单通道的卷积运算图,6-14(a) 中的两通道卷积层只是利用第二个卷积核(黄色)按照第一个卷积核(蓝色)同样的扫描方式生成了第二张激活特征图。当然,这里的第二个卷积核与第一个卷积核是具有相同维度的,不同的只是它的值与第一个卷积核不一样。因为它们代表不同类型的特征。

图6-14 多通道卷积层

图6-14 多通道卷积层

不难得出结论,只需要使用更多的卷积核并按照相同的方法去对输入进行扫描,就可以获得更多不同的激活特征图。在 图6-14(b)中,我们使用 66 个卷积核生成了 66 张不同的激活特征图。这 66 个卷积核都会分别去输入中搜寻一种特定类型的模式或者概念。最后,将这 66 张激活特征图全部按卷积核的顺序堆叠在一起,就可以得到最终的多层输出图。这组多层输出特征图就是新生成的卷积层。在该范例中,新卷积层是一个 28×28×628×28×6 的张量,而卷积核也是一个 5×5×65×5×6 的张量。以上就是如何使用卷积核对输入进行卷积运算并得到新卷积层的全部过程。

下面,我们使用Python代码来描述一下卷积层的工作过程。正如 公式6.8 所示,卷积层对输入张量和卷积核张量进行互相关运算,并在添加标量偏置后产生输出张量。所以,卷积层存在两个被训练的参数,分别是卷积核的权重和标量偏置。在下面的代码中我们会在初始化函数 __init__ 中分别定义这两个参数,并对权重执行随机初始化。在 forward 函数中,我们调用前面的自相关函数来计算卷积并添加偏置。

程序清单6-2 基于互相关运算的2D卷积

# codes06002_Conv2D
class Conv2D(paddle.nn.Layer):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = paddle.ParamAttr(paddle.rand(kernel_size))
        self.bias = paddle.ParamAttr(paddle.zeros(1))

    def forward(self, x):
        Y = cross_correlation(x, self.weight) + self.bias
        return Y  

6.3.5 基于卷积的边缘检测

边缘检测是计算机视觉中目标检测任务的常见应用,它可以用目标可见的轮廓来进行描述。定性地说,边缘通常出现在那些颜色、亮度或者纹理不同的区域的交界处。不幸的是,将图像分割成一致属性的区域是非常困难的。因此,对局部区域进行边缘检测后,再将这些局部区域内的边缘进行组合会更合适一些。基于这种设想,可以考虑将边缘定义为亮度剧烈变换的位置,然后使用一个局部算子来实现对这些位置的检测。卷积的局部特性正好可以满足于这种要求。

作为一个范例,首先构造一个9×9像素的黑白条纹图像,图像左边的3列和右边的3列为白色(像素值为1),中间的3列为黑色(像素值为0)。也就是说,这个图像存在两个边缘,分别是从白到黑的边缘和从黑到白的边缘。

程序清单6-3 创建边缘检测使用的图像矩阵

# codes06003_EdgeDetection_CreateImage
import paddle
# 生成9×9的垂直条纹图
X = paddle.ones((9, 9))
X[:, 3:6] = 0
print(X) # 输出图像矩阵

Tensor(shape=[9, 9], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [[1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.],
        [1., 1., 1., 0., 0., 0., 1., 1., 1.]])

下面我们将这个条纹图可视化出来。

程序清单6-4 显示边缘图像

# codes06004_EdgeDetection_ShowImage
# 可视化图像
import matplotlib.pylab as plt
plt.imshow(X, cmap=plt.cm.gray)

chapter06028

接下来,我们构造三个边缘检测器,分别是 3×33×3 的垂直边缘检测器 W1W_12×22×2 的垂直边缘检测器 W2W_2 以及 2×22×2 的水平边缘检测器 W3W_3。当它们与输入的条纹图进行互相关运算时,如果水平相邻的两个元素是相同的,则输出零,表示不存在边缘,否则输出为非零值,表示存在边缘。

程序清单6-5 创建边缘检测器

# codes06005_EdgeDetection_CreateKernels
# 1. 3×3的垂直边缘检测器
W1 = paddle.to_tensor([[1,0,-1], [1,0,-1], [1,0,-1]])
# 2. 2×2的垂直边缘检测器
W2 = paddle.to_tensor([[1,-1], [1,-1]])
# 3. 2×2的水平边缘检测器
W3 = paddle.to_tensor([[1,1], [-1,-1]])

这三个边缘检测器对输入 XX 执行互相关运算的代码和结果如下所示。在输出结果图中正值表示从白色到黑色的边缘,负值表示从黑色到白色的边缘,零值依然表示不存在边缘(注意输出图中由于绘图程序的色谱分配是自动的,因此同样的数值可能会呈现出不同的颜色)。例如,Y3检测器的输出就是全零的值,因为卷积核 W3W_3 只能检测水平边缘,而无法识别出原始图像的垂直边缘。让我们来观察一下检测器 W1W_1W2W_2 的输出结果。首先,输出张量Y1的尺度比Y2要更小一些。这主要是因为检测器 W1W_1W2W_2 更大,而它们的输出都是标量值。前者从 3×33×3 缩小为 1×11×1,后者从 2×22×2 缩小为 1×11×1。因此,最终的尺度Y1只有7×7,而Y2稍微大一点,高度和宽度都是8。其次,同样因为检测器 W1W_1W2W_2 的尺度更大,所以前者所获得的边缘也要更宽一些。换句话说,更大的检测器将更有可能检测到局部区域中存在边缘。水平边缘检测的情况和垂直边缘检测类似,此处不再赘述。

程序清单6-6 可视化使用边缘检测器获得的边缘信息矩阵

# codes06006_EdgeDetection_Output
# 使用3×3垂直边缘检测器获得的边缘
Y1 = common.cross_correlation(X, W1)
# 使用2×2垂直边缘检测器获得的边缘
Y2 = common.cross_correlation(X, W2)
# 使用2×2水平边缘检测器获得垂直条纹图
Y3 = common.cross_correlation(X, W3)
print('{} \n {} \n {}'.format(Y1, Y2, Y3))

chapter06029

6.3.6 学习卷积核

如果只需要检测黑白边缘,那么在上一小节中所定义的边缘检测器也就足够了。然而,对于更复杂的场景,例如自然图像,就不太可能使用这种手工设计的过滤器了。

下面我们尝试通过学习来生成一个垂直边缘检测器。此处,我们以上面手工定义的 3×33×3 垂直边缘检测器 W1W_1 为学习目标。在每次迭代中,通过计算预测输出与 W1W_1 生成的输出之间的均方误差的梯度来更新卷积核。为了方便起见,我们使用深度学习工具包内置的二维卷积类来构建卷积层并忽略偏置项。

程序清单6-7 使用卷积神经网络学习边缘检测器

# codes06007_EdgeDetection_LearnKernel
# 调用Paddle工具包,构造一个二维卷积层,它包含一个的形状为[3,3]的卷积核
conv2d = paddle.nn.Conv2D(1, 1, kernel_size=(3, 3))

# 卷积层使用四维张量进行输入和输出(批量大小、通道、高度、宽度)
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 9, 9))  # 输入
Y = Y1.reshape((1, 1, 7, 7)) # 输出
lr = 1e-3                    # 学习率

for i in range(100):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.clear_gradients()
    l.sum().backward()
    
    with paddle.no_grad():   # 迭代卷积核
        conv2d.weight[:] -= lr * conv2d.weight.grad
    if (i + 1) % 10 == 0:
        print(f'epoch {i+1}, loss {l.sum().item():.5f}')

# 同时输出手工定义和学习获得的卷积核
w_learned = conv2d.weight.reshape((3,3))
print('W_GT={} \n w_end={}'.format(W1, w_learned))

epoch 10, loss 0.51717
epoch 20, loss 0.02245
epoch 30, loss 0.00229
epoch 40, loss 0.00023
epoch 50, loss 0.00002
epoch 60, loss 0.00000
epoch 70, loss 0.00000
epoch 80, loss 0.00000
epoch 90, loss 0.00000
epoch 100, loss 0.00000

W_GT=[[ 1  0 -1]
      [ 1  0 -1]
      [ 1  0 -1]] 
w_start=[[-0.9103756  -0.31998962  0.27846253]
         [ 0.1869613  -0.0391564   0.33186862]
         [-0.1510132  -0.15523046  0.74735403]] 
w_end=[[ 0.38110092 -0.14853257 -1.1740984 ]
       [ 1.4784384   0.13230065 -1.1206924 ]
       [ 1.1404638   0.01622656 -0.7052062 ]]

经过100次迭代之后,误差降低到了足够低的值。并且经过学习得到的卷积核的权重w_end相比随机初始化的权重w_start,更加接近手工定义的卷积核 WGTW_{GT}。图 6-15 可视化了我们通过学习获得的卷积核对原始输入图像边缘检测的结果。从视觉上看,基本上和手工定义的卷积核检测结果差不多。

chapter06029

图6-15 手工定义的卷积核(中)与学习获得的卷积核(右)对自然图像垂直边缘检测的结果

6.3.7 从卷积层到卷积网络

与标准前馈神经网络相同,卷积神经网络也是一个以序列模式进行堆叠的模型。在这个意义上,一个个卷积层将按照一定的顺序进行首尾相连。除此之外,还需要增加一个激活函数层来扩展网络的非线性特性。大多数时候还会有一些汇聚层,就是池化层,来实现尺度不变性。于是基于 Conv+ReLU 的模块组就变成了卷积神经网络的核心结构。简单的说,卷积神经网络就是一个以激活函数为点缀的卷积层序列。

图6-16 由卷积层堆叠而得的卷积网络

图6-16 由卷积层堆叠而得的卷积网络

还是以 32×32×332×32×3 的彩色输入图为例。假设存在6个尺度为 5×55×5 的卷积核,则与之对应的输出特征图的维度就应该等于 6×28×286×28×28。注意样本输入卷积网络后,其张量的维度通常需要转换为 [批次大小,通道数,高度,宽度]。所以,此处如果忽略样本批次,则维度中的 66 就代表新卷积层的输入通道数,28×2828×28 则代表新卷基层的高度和宽度。该层的参数数量总和就等于卷积核张量的元素总数,即各维度的乘积 6×3×5×5=4506×3×5×5=450。在这个过程中,卷积核的数量 66 是超参数,需要设计人员根据经验进行设定,并且输出特征图的深度就等于这个值 66。在这里我们应该可以总结出三条结论:一是卷积特征图的深度应该等于上一层卷积核的个数,同时也等于当前层卷积核的深度;二是本层所提取的特征的种类数量与卷积核的个数一样;三是参数的个数由卷积核的尺度决定。根据这些结论,我们很容易得到第二层卷积核的尺度为 10×6×5×510×6×5×5 以及第二个输出特征图的尺度 10×24×2410×24×24。这里的 1010 同样是超参数,代表了第二个卷积层中卷积核的数量,同样也是第三个卷积层中特征图的深度。当我们按照这样的规律继续将这些卷积层进行堆叠下去,并使用它们进行特征学习的时候,你最终会发现这些堆叠在一起的卷积层,它们所学习到的信息是有差别的。

图6-16 VGG网络部分卷积层权重的可视化

图6-16 VGG网络部分卷积层权重的可视化

图6-16 所示的VGG模型的部分层的可视化结果为例,样本的分层概念被潜移默化地提取了出来。前面的一些层次(例如第1,2组卷积)通常包含的是低级的特征,你会在这些层中发现一些边缘或者色块的信息;在中间层(例如第3,4组卷积)中,你会获得一些更复杂的特征,比如角落、气泡等;而在更高级的区域(例如第5组卷积)中,你得到的就是一些更接近语义的东西,可能是目标局部甚至是目标整体的轮廓。在后面的课程中,我们将会介绍如何去可视化这些细节,并尝试去解释网络在各个位置到底学到了什么。不过,目前你只需要知道卷积神经网络的学习是一个从简单到复杂,从局部到整体,从像素逐渐抽象成语义的过程。

那么权重和卷积特征图到底长什么样子? 图6-17 展示了一些由卷积核生成的激活图的例子,它们来源于一个训练好的卷积网络。

图6-17 基于卷积特征生成的激活特征图

图6-17 基于卷积特征生成的激活特征图

在左上角中,我们给出了一张自然图像,看上去它是一辆小汽车的车灯。顶部是32个不同的卷积核,其中第一个红色框中的模板看上去是一个边缘,一个带方向的边缘。如果你使用它去扫过图像的话,那么,对于那些和这个带方向的边缘具有相似模式的区域就会具有较高的响应。仔细看红色车灯左右的两个斜向的竖条区域就是这种类型的区域。所以在下面的第一个激活图中,我们可以找到这样明显的灰白色线条。如果你觉得这还不是很明显的话,我们再来观察一下蓝色边框的卷积核,它是一个偏向橙红色的色块。当我们使用它去扫描图像的时候,可以发现在车灯的区域具有非常强的响应。简单的说,就是说该卷积核对应的蓝色边框的激活图在车灯的区域基本都是白色的。为什么是白色的呢,我们知道激活特征图都是二维矩阵,所以将其可视化之后,它就是一张灰度图。而在灰度图中,1是白色,0是黑色。所以,如果将激活特征图进行可视化,响应最强的部分就是白色,完全没有响应的部分就是黑色,而不同的灰度级则表示对卷积核不同的响应程度。这就是图像卷积的基本原理,我们可以简单的将它理解为一个过滤器,用来在图像上搜索与卷积核具有相同模式的区域。

6.3.8 计算复杂度

计算复杂度是指单个样本在模型中完成一次前向传播所发生的浮点运算次数,它也是用来描述模型时间复杂度的指标,其单位为FLOPS。对于单个卷积层来说,它的时间复杂度可以用如下公式描述:
TimeO(CinCoutKhKwmhmw)(6.9)Time \sim O(C_{in} · C_{out} · K_h · K_w · m_h · m_w) \tag{6.9}

在上面的公式中,我们可以知道,在该网络中存在输入 X:Cin×nh×nwX:C_{in}×n_h×n_w,输出 Y:Cout×mh×mwY: C_{out}×m_h×m_w,以及卷积核 W:Cout×Cin×kh×kwW: C_{out}×C_{in}×k_h×k_w 和偏置 B:Cout×CinB: C_{out}×C_{in},并且它们之间存在关系 Y=XW+BY=X*W+B。其中,CinC_{in}CoutC_{out} 分别表示每个卷积核的输入通道数,以及本卷积层卷积核的个数(也即输出通道数)。

举个例子,假设输入和输出通道数都是100,卷积核的大小为 5×55×5,输出的尺度为 64×6464×64,那么对于单个卷积层其计算复杂度就是1G Flops,也就是10亿次浮点计算。如果模型包含10个这样的卷积层,并且数据样本有100万(相当于ImageNet数据集的规模)。那么最终的时间复杂度将是10P Flops。注意,这个量只是前向传输的计算量,如果加上反向传播,时间复杂度还将翻一倍。所以说,卷积的计算复杂性并不低。但相对来说,卷积模型的空间复杂度是比较低的。对于一个卷积层来说,它的存储开销只需要一个 100×100×5×5100×100×5×5 的4D张量。

那么,是否有什么办法可以降低卷积神经网络的计算复杂度,从而加速运算呢?在标准的卷积结构中,一次计算只能针对一个感受野中的卷积。要完成整幅图的特征提取就必须不断地进行扫描。这样的计算方法需要消耗大量的计算资源和时间在数据寻址和内存读写上。一种有效的加速方法是基于矩阵运算的快速卷积。

图6-18 从卷积运算到矩阵运算

图6-18 从卷积运算到矩阵运算

如上图所示,在快速卷积的实现中卷积核与图像可以先分别展开成二维矩阵,然后再利用矩阵乘法来进行计算加速。这种方法将多次扫描加卷积的运算转变成了一次矩阵运算,可以大大地提高运算的速度和效率。此外,我们可以借助于一些硬件的指令集来实现矩阵计算的二次加速,包括面向CPU的并行计算工具包 Intel MKLOpenBLAS和面向GPU的并行计算框架CUDA,都是很不错的矩阵运算工具包。事实上,包括CaffepaddleTensorFlowPyTorch等常用的深度学习工具包,在底层都采用了快速卷积方法和硬件加速的实现。

6.3.9 小结

6.3.11 练习

  1. 尝试构建一个具有对角线边缘的图像I,然后使用本节中构建的水平边缘检测器和垂直边缘检测器去对它进行检测,并分析输出结果。

  2. 卷积神经网络依靠卷积运算来生成特征图,它使卷积神经网络具有()的特性。
    A. 局部感知
    B. 对象定位
    C. 参数共享
    D. 前景对象显著性

  3. 以下对于局部感知描述正确的项包括()。
    A. 卷积操作只关注局部像素,但神经元与采样特征图所有的像素均相连。
    B. 卷积操作只关注局部像素,神经元只与采样特征图局部区域的像素相连。
    C. 局部连接保证了卷积核对局部特征的最强响应
    D. 在同一层中,应用于不同特征图的卷积核的参数固定不变
    E. 在同一层中,应用于不同特征图的卷积核具有不同的参数

  4. 以下对于权重共享描述正确的项包括()。
    A. 权重共享只发生于同一通道,不同通道之间的权重不共享。
    B. 权重共享发生于所有通道,不同通道之间的权重也是相同的。
    C. 卷积核在划过整个图像时,每次划动都调整卷积核内部的参数。
    D. 卷积核在划过整个图像时,每次划动卷积核的参数都是固定不变的。

  5. 一般来说,一个卷积层只能学到一种特征。所以,如果想要学习图像多种不同的模式,就需要多个卷积层才能实现。
    A. 正确
    D. 错误

  6. 在卷积神经网络中,对于第L层,若特征图的数量为n,则该层卷积核的个数为:()。
    A. L
    B. n
    C. n2n^2
    D. 无法确定

  7. 若某层及其下一层的特征图维度均为 60×60×10,卷积核的维度为3×3×10,则该层的参数个数为()(忽略偏置项)。
    A. 36000
    B. 90
    C. 36000×90
    D. 36000×36000
    E. 90×90

  8. 卷积运算是卷积层的核心,它对卷积核与特征图执行(),然后将计算结果按元素进行累加。
    A. 外积运算
    B. 互相关运算
    C. 加和运算
    D. 求最大值运算

  9. 卷积核的个数等于()。
    A. 本层特征图的深度
    B. 下一层特征图的深度
    C. 下一层卷积层卷积核的深度
    D. 下一层卷积核的个数

6.3 卷积的基本原理

6.3.1 卷积的动机