卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习神经网络结构,主要用于图像识别、计算机视觉等领域。该结构在处理图像等高维数据时表现出色,因为它具有共享权重和局部感知的特点,一方面减少了权值的数量使得网络易于优化,另一方面降低了模型的复杂度,也就是减小了过拟合的风险。
卷积神经网络主要由卷积层(Convolutional layer)、池化层(Pooling layer)、全连接层(Fully connected layer)和激活函数(Activation function)等组成。其中,卷积层是CNN的核心部分,它通过卷积操作提取输入图像的特征,并将这些特征作为下一层的输入。池化层则用于降采样,可以减少卷积层输出的特征图的大小,从而减少网络参数和计算量。全连接层则用于将卷积层和池化层的输出连接起来,以便最终进行分类和预测。
卷积神经网络的训练主要是通过反向传播算法(Back-propagation)来更新网络中的权重(类似于BP神经网络),从而使得网络能够逐步学习到输入数据的特征,并在最终的分类或预测任务中得到较好的性能。
目前,卷积神经网络已经在图像分类、物体识别、人脸识别、自然语言处理等领域取得了非常出色的成果,是现代深度学习领域的重要组成部分。
在数学中,两个函数(比如 f 和 g)之间的“卷积”被定义为:
也就是说,卷积是把一个函数“翻转”并移位x,测量f 和 g 之间的重叠。当为离散对象时,积分就变成求和。例如,对于索引为Z的、平方可和的、无限维向量集合中抽取的向量,我们可得到如下定义:
对于二维张量,则为 f 的索引(a,b)和 g 的索引(i-a, j-b)上的对应加和:
这里需要说明的是,在卷积神经网络中,卷积层的实现方式实际上是数学中定义的互相关 (cross-correlation)运算,与数学分析中的卷积定义有所不同,使用互相关运算作为卷积的定义。互相关(Cross-Correlation)是一个衡量两个序列相关性的函数,通常是用滑动窗口的点积计算来实现,而卷积则需要将滤波器经过反转。
一维卷积经常用在信号处理中,用于计算信号的延迟累积。假设一个信号发生器每个时刻t产生一个信号,其信息的衰减率为,即在k-1个时间步长后,信息为原来的倍。而在时刻t收到的信号为当前时刻产生的信息和以前时刻延迟信息的叠加,即:
其中,被称为滤波器(filter)或卷积核(convolution kernnel)。
在图像处理中,图像是以二维矩阵的形式输入到神经网络中,给定一个图像()和一个滤波器(),且有,则其卷积为:
具体计算过程如下图所示:
通常我们将一个输入信息X和滤波器W的二维卷积定义为。假设卷积核的高和宽分别为和,则将称其为卷积,如卷积,就是指卷积核的高为3,宽为5。在卷积神经网络中,一个卷积算子除了上面描述的卷积过程外,还包括加上偏置项的操作。
当卷积核尺寸大于1时,经过一次卷积之后,输出特征图的尺寸会小于输入图片尺寸,输出特征图尺寸计算方法有:
如果经过多次卷积,输出图片尺寸会不断减小,为避免卷积之后图片尺寸变小,通常会在图片的外围进行填充(padding),如下图所示:
若沿着图片高度方向,在第一行之前填充行,在最后一行之后填充行;沿着图片宽度方向,在第一列之前填充列,在最后一列之后填充列,则填充之后的图片经过大小为的卷积核操作之后,输出图片的尺寸为:
在卷积计算过程中,通常会在高度或宽度的两侧采取等量填充,即要求:
则变换后的尺寸变为:
卷积核通常用1,3,5,7这样的奇数。如果使用的填充大小为:
则卷积之后图像尺寸不变。例如,当卷积核大小为5,padding大小为2时,卷积之后图像尺寸就不会改变。
为加深理解,假设创建一个高度和宽度为3的二维卷积层,并在所有侧边填充一个像素,给定高度和宽度为8的输入,则输出的高度和宽度也是8:
import torch
from torch import nn
# 为了方便起⻅,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
return Y.reshape(Y.shape[2:])
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
## 结果
torch.Size([8, 8])
当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。若使用高度为5,宽度为3的卷积核,高度和宽度两边的填充则分别为2和1:
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
## 结果
torch.Size([8, 8])
步幅(stride)也会影响输出图片的尺寸,下图是步幅为2的卷积过程,卷积核在图片上移动时,每次移动大小为2 个像素点:
当宽和高方向的步幅分别为和时,输出特征图尺寸为:
我们可以使用torch库实现以上过程。设定如下输入数据与核函数,输出数据可表示为:
import torch
from torch import nn
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和卷积核张量k,可以得到图上的输出张量:
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]])
corr2d(X, K)
## 输出结果
tensor([[19., 25.],
[37., 43.]])
在二维卷积中,随着卷积层数的提高,输出特征图上每个点的数值代表的信息会更多。在一层卷积中,输出特征图上每个点的数值是由输入图片上大小为的区域中元素与卷积核每个元素相乘再相加得到的,所以输入图像上该区域内每个元素数值的改变都会影响输出点的像素值。我们可将这个区域叫做输出特征图上对应点的感受野。以卷积为例,对应的感受野为大小的区域:
而卷积层数为2时,感受野的大小会增加到,如下图所示:
在卷积层中每一个神经元都只和下一层中局部的神经元相连,构成一个局部连接网络。使用卷积层代替全连接层后,可以使得层和层之间的连接数大大减少,如图所示(一维与二维):
我们熟知的图像一般包含了三个通道(三个颜色),称为RGB。实际上,图像并不是二维张量,而是一个由高度、宽度和颜色组成的三位张量,如个像素,前两个轴与像素的空间位置有关,而第三个轴可以看作每个像素的多维表示。
(1)多输入通道场景
若要计算卷积的输出结果,卷积核的形式也会发生变化,假设输入图片的通道数为,输入数据的形状为,计算过程如下:
通过torch库实现以下两个输入通道之间的运算:
import torch
from d2l import torch as d2l
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
将上图中的值对应的张量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.]])
(2)多输出通道场景
卷积操作的输出特征图也会具有多个通道,这时要设计个维度为的卷积核,卷积核数组的维度是,如下图所示:
卷积核的输出通道数也称为卷积核的个数,图中包含了两个卷积核,红绿蓝代表第一个卷积核的三个输入通道,颜色稍浅的代表第二个卷积核的三个输入通道。
在一个卷积层中,一个卷积核可以学习并提取图像中的一种特征,但往往图片中包含多种不同的特征信息,因此我们需要多个不同的卷积核提取不同的特征。
同样我们实现一个计算多个通道的输出函数:
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。 # 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
将核张量k与k+1(k中每个元素加1)和k+2连接起来,构造一个具有3个输出通道的卷积核:
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
## 结果
torch.Size([3, 2, 2, 2])
我们对输入张量与卷积核张量k执行运算,现在输出包含3个通道,第一个通道结果与先前输入张量x和多输入单输出通道的结果一致:
corr2d_multi_in_out(X, K)
## 输出结果
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
(3)批量操作
在卷积神经网络计算过程中,通常将多个样本放在一起形成一个mini-batch进行批量操作,即输入数据的维度是。由于会对每张图片使用同样的卷积核进行卷积操作,卷积核的维度与上面多输出通道的情况一样,仍然是,输出特征图的维度是,如下图所示:
池化(pooling layer)也称汇聚层或子采样层,自主要作用是进行特征选择、降低特征数量从而减少参数数量。池化相当于在空间范围内做了维度删减,分别作用于每个输入的特征并减小其大小。
池化层包含预设定的池化函数,其功能是将特征图中单个点的结果替换为其相邻区域的特征图统计量。使用某一位置的相邻输出的总体统计特征代替网络在该位置的输出,其好处是当输入数据做出少量平移时,经过池化函数后的大多数输出还能保持不变。
池化通常有两种,分别为平均池化和最大池化,如下图所示:
与卷积核类似,池化窗口(用表示池化窗口)在图片上滑动时,每次移动的步长称为步幅,当宽和高方向的移动大小不一样时,分别用和表示。当然也可对需要进行池化的图片进行填充,填充方式与卷积类似,假设在第一行之前填充行,在最后一行后面填充行,在第一列之前填充列,在最后一列之后填充列,则池化层的输出特征图大小为:
通常使用大小的池化窗口,步幅也使用2,填充为0;通过这种方式的池化,输出特征图的高和宽都减半,但通道数不会改变。
为实现池化层的前向传播,我们仍可以使用corr2d函数,只是此处没有卷积核,输出为输入中每个区域的最大值或平均值:
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
构建输入张量x,验证二维最大池化层的输出:
## 最大池化
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
## 结果
tensor([[4., 5.],
[7., 8.]])
# 平均池化
pool2d(X, (2, 2), 'avg')
# 结果
tensor([[2., 3.],
[5., 6.]])
池化层不但可以有效地减少神经元的数量,还可以使得网络对一些小的局部形态改变保持不变性,并拥有更大的感受野,如下图所示:
全连接层(fully-connected layer)中每一个节点都与上一层所有节点相连,把前向层提取到的特征综合起来。由于其全相连的特征,一般全连接层的参数也是最多的。
在 CNN 结构中,经多个卷积层和池化层后,连接着1个或1个以上的全连接层,与 MLP 类似,全连接层中的每个神经元与其前一层的所有神经元进行全连接。为提升 CNN 网络性能,全连接层每个神经元的激励函数一般采用 ReLU 函数,最后一层全连接层的输出值被传递给一个输出,可以采用softmax逻辑回归进行分类。
LeNet是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。这个模型是由AT&T⻉尔实验室的研究员Yann LeCun在1989年提出。当时,LeNet取得了与支持向量机(support vector machines)性能相媲美的成果,成为监督学习的主流方法。LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字,到如今还有相当范围的使用。
LeNet网络由两个部分组成,分别为:
其网络架构如下图所示:
每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化层。每个卷积层使用5卷积核和一个sigmoid激活函数。这 些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道,卷积的输出形状由批量大 小、通道数、高度、宽度决定。
深度学习框架实现此类模型并不难,只需实例化一个Sequential模块并将需要的层连接在一起:
import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
对原始模型做一点小改动,去掉最后一层的高斯激活,其余与最初的网络一致。将一个大小为的单通道(黑白)图像通过LeNet,通过在每一层打印输出的形状,我们可以检查模型,以确保其操作与我们期望的一致,如下图:
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape: \t',X.shape)
## 输出结果
Conv2d output shape: torch.Size([1, 6, 28, 28])
Sigmoid output shape: torch.Size([1, 6, 28, 28])
AvgPool2d output shape: torch.Size([1, 6, 14, 14])
Conv2d output shape: torch.Size([1, 16, 10, 10])
Sigmoid output shape: torch.Size([1, 16, 10, 10])
AvgPool2d output shape: torch.Size([1, 16, 5, 5])
Flatten output shape: torch.Size([1, 400])
Linear output shape: torch.Size([1, 120])
Sigmoid output shape: torch.Size([1, 120])
Linear output shape: torch.Size([1, 84])
Sigmoid output shape: torch.Size([1, 84])
Linear output shape: torch.Size([1, 10])
在整个卷积块中,与上一层相比,每一层的特征高度和宽度都减小了。第一个卷积层使用2个像素的 填充,来补偿卷积核导致的特征减少。相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。同时,每个汇聚层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。
在实现LeNet的基础上,用其实现Fashion-MNIST数据集上的表现。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。通过使用GPU,可以用它加快训练。
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
与全连接层一样,我们使用交叉熵损失函数和小批量随机梯度下降:
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test cc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0],
d2l.accuracy(y_hat,y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
之后,我们可以训练和评估LeNet模型:
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
## 输出结果
loss 0.467, train acc 0.825, test acc 0.821
88556.9 examples/sec on cuda:0