本章介绍的卷积神经⽹络(convolutional neural network, CNN)是⼀类强⼤的、为处理图像数据⽽设计的神经⽹络 。卷积神经⽹络需要的参数少于全连接架构的⽹络,⽽且卷积也很容易⽤GPU并⾏计算。因此卷积神经⽹络除了能够⾼效地采样从⽽获得精确的模型,还能够⾼效地计算。
在本章的开始,我们将介绍构成所有卷积⽹络主⼲的基本元素。这包括卷积层本⾝、填充(padding)和步幅(stride)的基本细节、⽤于在相邻区域汇聚信息的汇聚层(pooling)、在每⼀层中多通道(channel)的使⽤,以及有关现代卷积⽹络架构的仔细讨论
从全连接层到卷积
之前讨论的多层感知机⼗分适合处理表格数据,其中⾏对应样本,列对应特征。 然⽽对于⾼维感知数据,这种缺少结构的⽹络可能会变得不实⽤。
(一)、不变性
卷积神经⽹络正是将空间不变性(spatial invariance)的这⼀概念系统化,从⽽基于这个模型使⽤较少的参数来学习有⽤的表⽰。
-
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经⽹络的前⾯⼏层应该对相同的图像区域具有相似的反应,即为“平移不变性”
-
- 局部性(locality):神经⽹络的前⾯⼏层应该只探索输⼊图像中的局部区域,⽽不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进⾏预测。
简⽽⾔之, (6.1.3)是⼀个卷积层(convolutional layer),⽽卷积神经⽹络是包含卷积层的⼀类特殊的神经⽹络。在深度学习研究社区中, V被称为卷积核(convolution kernel)或者滤波器(flter),亦或简单地称之为该卷积层的权重,通常该权重是可学习的参数。
参数⼤幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每⼀层只包含局部的信息。 以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。但如果这偏置与现实不符时,⽐如当图像不满⾜平移不变时,我们的模型可能难以拟合我们的训练数据。
(二)、卷积
采用一个加权函数 w(a) 来实现,其中 a 表示测量结果距当前时刻的时间间隔。如果我们对任意时刻都采用这种加权平均的操作,就得到了一个新的对于飞船位置的平滑估计函数 s:这就是卷积运算。
- 通道:
由于输⼊图像是三维的,我们的隐藏表⽰H也最好采⽤三维张量。换句话说,对于每⼀个空间位置,我们想要采⽤⼀组⽽不是⼀个隐藏表⽰。这样⼀组隐藏表⽰可以想象成⼀些互相堆叠的⼆维⽹格。因此,我们可以把隐藏表⽰想象为⼀系列具有⼆维张量的通道(channel)。这些通道有时也被称为特征映射(featuremaps),因为每个通道都向后续层提供⼀组空间化的学习特征。直观上可以想象在靠近输⼊的底层,⼀些通道专⻔识别边缘,⽽⼀些通道专⻔识别纹理。
图像卷积
、互相关运算
在 图6.2.1中,输⼊是⾼度为3、宽度为3的⼆维张量(即形状为3 × 3)。卷积核的⾼度和宽度都是2,⽽卷积核窗⼝(或卷积窗⼝)的形状由内核的⾼度和宽度决定(即2 × 2)。
因为卷积核的宽度和⾼度⼤于1,⽽卷积核只与图像中每个⼤⼩完全适合的位置进⾏互相关运算。所以,输出⼤⼩等于输⼊⼤⼩nh × nw减去卷积核⼤⼩kh × kw,
稍后,我们将看到如何通过在图像边界周围填充零来保证有⾜够的空间移动卷积核,从⽽保持输出⼤⼩不变。接下来,我们在corr2d函数中实现如上过程,该函数接受输⼊张量X和卷积核张量K,并返回输出张量Y。
import torch
from torch import nn
from d2l import torch as d2l
# 二维互相关运算
def corr2d(X, K):
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]])
corr2d(X, K)
、卷积层
卷积层对输⼊和卷积核权重进⾏互相关运算,并在添加标量偏置之后产⽣输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置 。
X = torch.ones((6, 8))
X[:, 2:6] = 0
k = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, k)
corr2d(X.t(), k)
⾼度和宽度分别为h和w的卷积核可以被称为h × w卷积或h × w卷积核。我们也将带有h × w卷积核的卷积层称为h × w卷积层。
、图像中目标的边缘检测
通过找到像素变化的位置,来检测图像中不同颜⾊的边缘。
⾸先,我们构造⼀个6 × 8像素的⿊⽩图像。中间四列为⿊⾊(0),其余像素为⽩⾊(1)。
接下来,我们构造⼀个⾼度为1、宽度为2的卷积核K。当进⾏互相关运算时,如果⽔平相邻的两元素相同,则 输出为零,否则输出为⾮零。
现在,我们对参数X(输⼊)和K(卷积核)执⾏互相关运算。如下所⽰,输出Y中的1代表从⽩⾊到⿊⾊的边缘, -1代表从⿊⾊到⽩⾊的边缘,其他情况的输出为0。
x = torch.ones((6, 8))
x[:, 2:6] = 0
k = torch.tensor([[1.0, -1.0]])
Y = corr2d(x, k)
corr2d(x.t(), k)
、学习卷积核(卷积权重)
当我们需要去学习如何由X生成Y的卷积核时,如何实现呢
conv2d = nn.Conv2d(1, 1, (1, 2), bias=False)
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 0.03
for i in range(10):
Y_pre = conv2d(X)
l = (Y_pre - Y) ** 2 # 损失函数
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= lr * conv2d.weight.grad
print(f'epoch{i}, loss{l.sum():0.2f}')
print(f'w{conv2d.weight}')
、 填充(padding)
在应⽤多层卷积时,我们常常丢失边缘像素。由于我们通常使⽤⼩卷积核,因此对于任何单个卷
积,我们可能只会丢失⼏个像素。但随着我们应⽤许多连续卷积层,累积丢失的像素数就多了 。
填充(padding):在输⼊图像的边界填充元素(通常填充元素是0)。
如果我们添加ph⾏填充(⼤约⼀半在顶部,⼀半在底部)和pw列填充(左侧⼤约⼀半,右侧⼀半),则输出形状将为
这意味着输出的⾼度和宽度将分别增加ph和pw。
使⽤奇数的核⼤⼩和填充⼤⼩也提供了书写上的便利。对于任何⼆维张量X,当满⾜:
- 卷积核的大小是奇数;
- 所有边的填充⾏数和列数相同;
- 输出与输⼊具有相同⾼度和宽度则可以得出:输出Y[i, j]是通过以输⼊X[i, j]为中⼼,与卷积核进⾏互相关计算得到的。
import torch
import torch.nn as nn
X = torch.rand(size=(8, 8))
# 请注意,这⾥每边都填充了1⾏或1列,因此总共添加了2⾏或2列
conv2d = nn.Conv2d(1, 1, (3, 3), padding=1)
def comp_conv2d(conv2d, X):
# 这里(1,1)是指批量和通道数
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量⼤⼩和通道
return Y.reshape(Y.shape[2:])
comp_conv2d(conv2d=conv2d, X=X).shape
# 当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输入输出具有相同的高度和宽度
conv2d = nn.Conv2d(1, 1, (5, 3), padding=(2, 1))
comp_conv2d(conv2d=conv2d, X=X).shape
、 步幅(stride)
在计算互相关时,卷积窗⼝从输⼊张量的左上⻆开始,向下、向右滑动。在前⾯的例⼦中,我们默认每次滑动⼀个元素。但是,有时候为了⾼效计算或是缩减采样次数,卷积窗⼝可以跳过中间位置,每次滑动多个元素。我们将每次滑动元素的数量称为步幅(stride)
通常,当垂直步幅为sh、⽔平步幅为sw时,输出形状为
如果在卷积操作中将步幅设置为2,那么将会使输入的高度和宽度减半。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
print(comp_conv2d(conv2d=conv2d, X=X).shape)
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
print(comp_conv2d(conv2d=conv2d, X=X).shape)
、多输入输出通道
当我们添加通道时,我们的输⼊和隐藏的表⽰都变成了三维张量。例如,每个RGB输⼊图像具有3 × h × w的形状。我们将这个⼤⼩为3的轴称为通道(channel)维度。本节将更深⼊地研究具有多输⼊和多输出通道的卷积核 。
当输⼊包含多个通道时,需要构造⼀个与输⼊数据具有相同输⼊通道数的卷积核,以便与输⼊数据进⾏互相关运算 。
随着神经⽹络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更⼤的通道深度。
⽤ci和co分别表⽰输⼊和输出通道的数⽬,并让kh和kw为卷积核的⾼度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建⼀个形状为ci × kh × kw的卷积核张量,这样卷积核的形状是co × ci × kh × kw 。在互相关运算中,每个输出通道先获取所有输⼊通道,再以对应该输出通道的卷积核计算出结果。
卷积的本质是有效提取相邻像素间的相关特征,⽽1 × 1卷积显然没有此作⽤。 因为使⽤了最⼩窗⼝, 1 × 1卷积失去了卷积层的特有能⼒——在⾼度和宽度维度上,识别相邻元素间相互作⽤的能⼒。
# 1. 多通道输入
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在⼀起
return sum(d2l.corr2d(x, k) for x, k in zip(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]]])
print(corr2d_multi_in(X, K), X.shape, K.shape)
# 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 = torch.stack((K, K + 1, K + 2), 0)
print(corr2d_multi_in_out(X, K).shape, X.shape, K.shape)
# 3. 1*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))
Y = torch.matmul(K, X)
return Y.reshape(c_o, h, w)
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)
print(Y1,'\n', Y2, Y1.shape, Y2.shape)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
- <font style="color:rgb(0,0,0);">多输⼊多输出通道可以⽤来扩展卷积层的模型。</font>
- <font style="color:rgb(0,0,0);">当以每像素为基础应⽤时, 1 </font>_<font style="color:rgb(0,0,0);">× </font>_<font style="color:rgb(0,0,0);">1卷积层相当于全连接层。</font>
- <font style="color:rgb(0,0,0);">1 </font>_<font style="color:rgb(0,0,0);">× </font>_<font style="color:rgb(0,0,0);">1卷积层通常⽤于调整⽹络层的通道数量和控制模型复杂性</font>
、汇聚层**(pooling)**
当检测较底层的特征时 ,我们通常希望这些特征保持某种程度上的平移不变性。
** ****汇聚层**,它具有双重⽬的:降低卷积层对位置的敏感性,同时降低对空间降采样表⽰的敏感性。
与卷积层类似,汇聚层运算符由⼀个固定形状的窗⼝组成,该窗⼝根据其步幅⼤⼩在输⼊的所有区域上滑动,为固定形状窗⼝(有时称为汇聚窗⼝)遍历的每个位置计算⼀个输出。然⽽,不同于卷积层中的输⼊与卷积核之间的互相关计算,汇聚层不包含参数。相反,池运算是确定性的,我们通常计算汇聚窗⼝中所有元素的最⼤值或平均值。这些操作分别称为最⼤汇聚层(maximum pooling)和平均汇聚层(average pooling)。
def pool2d(X, poll_size, mode):
p_h, p_w = poll_size
Y = torch.zeros(X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)
print(range(Y.shape[1]))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
print(i, j, p_h, p_w,X[i:i + p_h, j:j + p_w].shape)
if mode == 'max':
Y[i][j] = X[i:i + p_h, j:j + p_w].max()
if mode == 'avg':
Y[i][j] = X[i:i + p_h, j:j + p_w].mean()
return Y
X = torch.tensor([[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]])
print(X.shape)
pool2d(X, (2, 2), 'max')
步幅和填充
与卷积层⼀样,汇聚层也可以改变输出形状。和以前⼀样,我们可以通过填充和步幅以获得所需的输出形状。下⾯,我们⽤深度学习框架中内置的⼆维最⼤汇聚层,来演⽰汇聚层中填充和步幅的使⽤。我们⾸先构造了⼀个输⼊张量X,它有四个维度,其中样本数和通道数都是1。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d(3)
pool2d(X)
# 填充和步幅可以⼿动设定。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
# 设定⼀个任意⼤⼩的矩形汇聚窗⼝,并分别设定填充和步幅的⾼度和宽度。
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
多个通道
在处理多通道输⼊数据时,汇聚层在每个输⼊通道上单独运算,⽽不是像卷积层⼀样在通道上对输⼊进⾏汇总。这**意味着汇聚层的输出通道数与输⼊通道数相同。****
X = torch.cat((X, X + 1), 1)
X
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
卷积神经网络(LeNet)
通过前面的学习,我们对构建一个完整的卷积神经网络所需的组件都有所了解了,那么接下来我们将会实现一个简单的卷积神经网络(LeNet)。
、LeNet
该卷积神经网络架构如下:
从总体来看,我们可以把LeNet-5分为两个部分:
- 卷积编码器:由两个卷积层组成
- 全连接层密集块:由三个全连接层组成
每个卷积块中的基本单元是⼀个卷积层、⼀个激活函数和汇聚层。这些层将输⼊映射到多个⼆维特征输出,通常同时增加通道的数量。第⼀卷积层有6个输出通道,⽽第⼆个卷积层有16个输出通道。每个2 × 2池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量⼤⼩、通道数、⾼度、宽度决定。
import torch
import torch.nn as nn
from d2l import torch as d2l
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2),
nn.Sigmoid(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.Sigmoid(),
nn.MaxPool2d(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))
# 打印每个过程的数据大小
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)
# 1. 加载数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 2. 模型评估
def evaluate_accuracy_gpu(net, data_iter, device = None):
''' 使用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):
# BERT微调所需的(之后将介绍)
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]
# 模型训练
'''
在进⾏正向和反向传播之前,我们需要将每⼀⼩批量数据移动到我们指定的设备(例如GPU)上。
由于我们将实现多层神经⽹络,因此我们将主要使⽤⾼级API。以下训练函数假定从⾼级API创建的模型作为输⼊,并进⾏相应的优化。我们使⽤在
4.8.2节中介绍的Xavier随机初始化模型参数。与全连接层⼀样,我们使⽤交叉熵损失函数和⼩批量随机梯度
下降。
'''
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
''' 用GPU训练模型 '''
def init_weight(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weight)
print('train on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(ylabel='percent(%)', ylim=[0, 100],
xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
# 训练
for epoch in range(num_epochs):
metric = d2l.Accumulator(3)
net.train()
# 256个样本
for i,(X,y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_predict = net(X)
ls = loss(y_predict, y)
ls.backward()
optimizer.step()
with torch.no_grad():
metric.add(ls * X.shape[0], d2l.accuracy(y_predict, 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 * 100, train_acc * 100, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1 ,(None, None, test_acc * 100))
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)}')
lr, num_epochs = 0.9, 20
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
到这里我们对卷积神经网络也开始有了一定的认识了,可以以此为基础进行更加复杂的卷积网络学习。