深度学习——卷积神经网络(CNN)基础一
之前的多层感知机十分适合处理表格数据。对于表格数据,我们寻找的模式可能涉及特征之间的交互,但是我们不能预先假设任何与特征交互相关的先验结构(先验结构指的是我们在进行建模之前对数据中特征之间关系的假设或预设)。 此时,多层感知机可能是最好的选择,
然而对于高维感知数据,这种缺少结构的网络可能会变得不实用(参数过多),这时候就需要用到卷积神经网络了
参考书:
《动手学深度学习》
假设我们想从一张图片中找到某个物体。 合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。 理想情况下,我们的系统应该能够利用常识:猪通常不在天上飞,飞机通常不在水里游泳。 但是,如果一只猪出现在图片顶部,我们还是应该认出它。
以游戏”沃尔多在哪里”为例:
在这个游戏中包含了许多充斥着活动的混乱场景,而沃尔多通常潜伏在一些不太可能的位置,我们的目标就是找出他。
我们可以使用一个“沃尔多检测器”扫描图像。 该检测器将图像分割成多个区域,并为每个区域包含沃尔多的可能性打分。
卷积神经网络正是将空间不变性的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
我们将上述想法总结一下:
多层感知机的输入是二维图像X,其隐藏表示H在数学上是一个矩阵,在代码中表示为二维张量。 其中X和H具有相同的形状。
使用[X]i,j和[H]i,j分别表示输入图像和隐藏表示中位置(i,j)处的像素。 为了使每个隐藏神经元都能接收到每个输入像素的信息,我们将参数从权重矩阵替换为四阶权重张量W。假设U包含偏置参数,我们可以将全连接层形式化地表示为
[ H ] i , j = [ U ] i , j + ∑ k ∑ l [ W ] i , j , k , l [ X ] k , l = [ U ] i , j + ∑ a ∑ b [ V ] i , j , a , b [ X ] i + a , j + b . \begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned} [H]i,j=[U]i,j+k∑l∑[W]i,j,k,l[X]k,l=[U]i,j+a∑b∑[V]i,j,a,b[X]i+a,j+b.
其中,从 W \mathsf{W} W到 V \mathsf{V} V的转换只是形式上的转换,因为在这两个四阶张量的元素之间存在一一对应的关系。
我们只需重新索引下标 ( k , l ) (k, l) (k,l),使 k = i + a k = i+a k=i+a、 l = j + b l = j+b l=j+b,由此可得 [ V ] i , j , a , b = [ W ] i , j , i + a , j + b [\mathsf{V}]_{i, j, a, b} = [\mathsf{W}]_{i, j, i+a, j+b} [V]i,j,a,b=[W]i,j,i+a,j+b。
索引 a a a和 b b b通过在正偏移和负偏移之间移动覆盖了整个图像。
对于隐藏表示中任意给定位置( i i i, j j j)处的像素值 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j,可以通过在 x x x中以 ( i , j ) (i, j) (i,j)为中心对像素进行加权求和得到,加权使用的权重为 [ V ] i , j , a , b [\mathsf{V}]_{i, j, a, b} [V]i,j,a,b。
这意味着检测对象在输入 X \mathbf{X} X中的平移,应该仅导致隐藏表示 H \mathbf{H} H中的平移。也就是说, V \mathsf{V} V和 U \mathbf{U} U实际上不依赖于 ( i , j ) (i, j) (i,j)的值,即 [ V ] i , j , a , b = [ V ] a , b [\mathsf{V}]_{i, j, a, b} = [\mathbf{V}]_{a, b} [V]i,j,a,b=[V]a,b。
并且 U \mathbf{U} U是一个常数,比如 u u u。因此,我们可以简化 H \mathbf{H} H定义为:
[ H ] i , j = u + ∑ a ∑ b [ V ] a , b [ X ] i + a , j + b . [\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}. [H]i,j=u+a∑b∑[V]a,b[X]i+a,j+b.
这就是卷积(convolution)。我们是在使用系数 [ V ] a , b [\mathbf{V}]_{a, b} [V]a,b对位置 ( i , j ) (i, j) (i,j)附近的像素 ( i + a , j + b ) (i+a, j+b) (i+a,j+b)进行加权得到 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j。
注意, [ V ] a , b [\mathbf{V}]_{a, b} [V]a,b的系数比 [ V ] i , j , a , b [\mathsf{V}]_{i, j, a, b} [V]i,j,a,b少很多,因为前者不再依赖于图像中的位置。这就是显著的进步!
如上所述,为了收集用来训练参数 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j的相关信息,我们不应偏离到距 ( i , j ) (i, j) (i,j)很远的地方。这意味着在 ∣ a ∣ > Δ |a|> \Delta ∣a∣>Δ或 ∣ b ∣ > Δ |b| > \Delta ∣b∣>Δ的范围之外,我们可以设置 [ V ] a , b = 0 [\mathbf{V}]_{a, b} = 0 [V]a,b=0。因此,我们可以将 [ H ] i , j [\mathbf{H}]_{i,j} [H]i,j重写为
[ H ] i , j = u + ∑ a = − Δ Δ ∑ b = − Δ Δ [ V ] a , b [ X ] i + a , j + b . [\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}. [H]i,j=u+a=−Δ∑Δb=−Δ∑Δ[V]a,b[X]i+a,j+b.
简而言之, 该式是一个卷积层(convolutional layer),而卷积神经网络是包含卷积层的一类特殊的神经网络。
V \mathbf{V} V被称为卷积核(convolution kernel)或者滤波器(filter),亦或简单地称之为该卷积层的权重,通常该权重是可学习的参数。
当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:
以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。
参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。
以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。
但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。
在数学中,两个函数(比如 f , g : R d → R f, g: \mathbb{R}^d \to\mathbb{R} f,g:Rd→R)之间的“卷积”被定义为
( f ∗ g ) ( x ) = ∫ f ( z ) g ( x − z ) d z . (f * g)(\mathbf{x}) = \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}. (f∗g)(x)=∫f(z)g(x−z)dz.
也就是说,卷积是当把一个函数“翻转”并移位 x \mathbf{x} x时,测量 f f f和 g g g之间的重叠。
当为离散对象时,积分就变成求和。例如,对于由索引为 Z \mathbb{Z} Z的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:
( f ∗ g ) ( i ) = ∑ a f ( a ) g ( i − a ) . (f * g)(i) = \sum_a f(a) g(i-a). (f∗g)(i)=a∑f(a)g(i−a).
对于二维张量,则为 f f f的索引 ( a , b ) (a, b) (a,b)和 g g g的索引 ( i − a , j − b ) (i-a, j-b) (i−a,j−b)上的对应加和:
( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) . (f * g)(i, j) = \sum_a\sum_b f(a, b) g(i-a, j-b). (f∗g)(i,j)=a∑b∑f(a,b)g(i−a,j−b).
回到上面的“沃尔多在哪里”游戏,。卷积层根据滤波器 V \mathbf{V} V选取给定大小的窗口,并加权处理图片,我们的目标是学习一个模型,以便探测出在“沃尔多”最可能出现的地方。
然而这种方法有一个问题:我们忽略了图像一般包含三个通道/三种原色(红色、绿色和蓝色)。
实际上,图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含 1024 × 1024 × 3 1024 \times 1024 \times 3 1024×1024×3个像素。
前两个轴与像素的空间位置有关,而第三个轴可以看作每个像素的多维表示。
因此,我们将 X \mathsf{X} X索引为 [ X ] i , j , k [\mathsf{X}]_{i, j, k} [X]i,j,k。由此卷积相应地调整为 [ V ] a , b , c [\mathsf{V}]_{a,b,c} [V]a,b,c,而不是 [ V ] a , b [\mathbf{V}]_{a,b} [V]a,b。
此外,由于输入图像是三维的,我们的隐藏表示 H \mathsf{H} H也最好采用三维张量。
换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
因此,我们可以把隐藏表示想象为一系列具有二维张量的通道 这些通道有时也被称为特征映射(feature maps),
因为每个通道都向后续层提供一组空间化的学习特征。 直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。
为了支持输入 X \mathsf{X} X和隐藏表示 H \mathsf{H} H中的多个通道,我们可以在 V \mathsf{V} V中添加第四个坐标,即 [ V ] a , b , c , d [\mathsf{V}]_{a, b, c, d} [V]a,b,c,d。
[ H ] i , j , d = ∑ a = − Δ Δ ∑ b = − Δ Δ ∑ c [ V ] a , b , c , d [ X ] i + a , j + b , c , [\mathsf{H}]_{i,j,d} = \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c}, [H]i,j,d=a=−Δ∑Δb=−Δ∑Δc∑[V]a,b,c,d[X]i+a,j+b,c,
其中隐藏表示 H \mathsf{H} H中的索引 d d d表示输出通道,而随后的输出将继续以三维张量 H \mathsf{H} H作为输入进入下一个卷积层。
所以, 该式可以定义具有多个通道的卷积层,而其中 V \mathsf{V} V是该卷积层的权重。
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。
在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在下图中,输入是高度为 3 3 3、宽度为 3 3 3的二维张量。卷积核的高度和宽度都是 2 2 2,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即 2 × 2 2 \times 2 2×2)。
二维互相关运算。阴影部分是第一个输出元素,以及用于计算输出的输入张量元素和核张量元素:
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。
当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为 2 2 2、宽度为 2 2 2,如下所示:
0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19 , 1 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 25 , 3 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 37 , 4 × 0 + 5 × 1 + 7 × 2 + 8 × 3 = 43. 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. 0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,
而卷积核只与图像中每个大小完全适合的位置进行互相关运算。
所以,输出大小等于输入大小 n h × n w n_h \times n_w nh×nw减去卷积核大小 k h × k w k_h \times k_w kh×kw,即:
( n h − k h + 1 ) × ( n w − k w + 1 ) . (n_h-k_h+1) \times (n_w-k_w+1). (nh−kh+1)×(nw−kw+1).
这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到如何通过在图像边界周围填充零来保证有足够的空间移动卷积核,从而保持输出大小不变。
接下来,我们在corr2d
函数中实现如上过程,该函数接受输入张量X
和卷积核张量K
,并返回输出张量Y
。
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(x, k): # @save
"""二维互相关运算"""
h, w = k.shape
y = torch.zeros((x.shape[0] - h + 1, x.shape[1] - w + 1)) #输出大小
for i in range(y.shape[0]):
for j in range(y.shape[1]):
y[i, j] = (x[i:i + h, j:j + w] * k).sum() #从左到右,从上到下滑动。 按元素相乘
return y
x = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
print(corr2d(x,K))
#结果:
tensor([[19., 25.],
[37., 43.]])
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
基于上面定义的corr2d函数实现二维卷积层:
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size)) #将张量标记为模型参数
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
高度和宽度分别为h和w的卷积核可以被称为h×w卷积或h×w卷积核。 我们也将带有h×w卷积核的卷积层称为h×w卷积层。
如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
首先,我们构造一个6×8像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)。
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(X)
#结果:
tensor([[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.]])
接下来,构造一个高度为1、宽度为2的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
K = torch.tensor([[1.0, -1.0]])
print(K)
#结果:
tensor([[ 1., -1.]])
现在,我们对参数X和K执行互相关运算。 输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0。
y = corr2d(X,K)
print(y)
#结果:
tensor([[ 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只可以检测垂直边缘],无法检测水平边缘。
print(corr2d(X.t(),K))
#结果:
tensor([[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的卷积核
"""
现在让我们看看是否可以通过仅查看“输入-输出”对来学习由X生成Y的卷积核。
我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,
我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,
我们在此使用内置的二维卷积层,并忽略偏置。
"""
# 构造一个二维卷积层,它具有1个通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1,kernel_size=(1,2),bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
#结果:
epoch 2, loss 23.267
epoch 4, loss 8.334
epoch 6, loss 3.213
epoch 8, loss 1.282
epoch 10, loss 0.520
#来看看我们所学的卷积核的权重张量(可以看到学习到的卷积核权重非常接近我们之前定义的卷积核K)
print(conv2d.weight.data.reshape((1, 2)))
#结果:
tensor([[ 1.0626, -0.9143]])
回想一下前面的互相关和卷积运算之间的对应关系。 为了得到正式的卷积运算输出,我们需要执行严格卷积运算,而不是互相关运算。
幸运的是,它们差别不大,我们只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
值得注意的是,由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。
输出的卷积层有时被称为特征映射(feature map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。
在卷积神经网络中,对于某一层的任意元素 x x x,其感受野(receptive field)是指在前向传播期间可能影响 x x x计算的所有元素(来自所有先前层)。
请注意,感受野可能大于输入的实际大小。让我们用下图为例来解释感受野:
给定 2 × 2 2 \times 2 2×2卷积核,阴影输出元素值 19 19 19的感受野是输入阴影部分的四个元素。
假设之前输出为 Y \mathbf{Y} Y,其大小为 2 × 2 2 \times 2 2×2,现在我们在其后附加一个卷积层,该卷积层以 Y \mathbf{Y} Y为输入,输出单个元素 z z z。
在这种情况下, Y \mathbf{Y} Y上的 z z z的感受野包括 Y \mathbf{Y} Y的所有四个元素,而输入的感受野包括最初所有九个输入元素。
因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
本文算是一个从之前的全连接层到卷积层过渡。开始先记录了图像识别中的两个重要概念:平移不变性、局部性。之后介绍了在图像卷积中的严格卷积运算和互相关运算的关系,并代码简单实现了二维卷积核的学习。最后也算初步介绍了特征映射和感受野的概念。
黄昏的地平线,划出一句离别。
–2023-10-10 进阶篇