前言
文章性质:学习笔记 📖
学习资料:吴茂贵《 Python 深度学习基于 PyTorch ( 第 2 版 ) 》【ISBN】978-7-111-71880-2
主要内容:根据学习资料撰写的学习笔记,该篇主要介绍了卷积神经网络的卷积层部分。
预:从全连接层到卷积层
使用多层感知机网络来处理图像存在的不足:
1)把图像展平为向量,极易丢失图像的部分固有属性;
2)使用全连接层极易导致参数量呈指数级增长。
图像最具代表性的两个特征:
1)平移不变性:不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应。
2)局部性:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远的区域的关系。最终,在后续神经网络中,整个图像级别上可以集成这些局部特征用于预测。
一、卷积神经网络
卷积神经网络(Convolutional Neural Network, CNN)是一种前馈神经网络,由一个或多个卷积层和顶端的全连接层组成,同时包括关联权重和池化层等。图 6-1 是一个典型的卷积神经网络架构。
import torch
import torch.nn as nn
import torch.nn.functional as F
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
class CNNNet(nn.Module):
def __init__(self):
super(CNNNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=16, out_channels=36, kernel_size=3, stride=1)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
self.fc1 = nn.Linear(1296, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
# print(x.shape)
x = x.view(-1, 36*6*6)
x = F.relu(self.fc2(F.relu(self.fc1(x))))
return x
net = CNNNet().to(device)
二、卷积层
卷积层是卷积神经网络的核心层,而卷积又是卷积层的核心。卷积,直观理解就是两个函数的一种运算,也被称为 卷积运算 。
卷积核窗口从输入张量的左上角开始,从左到右、从上到下滑动。当卷积核窗口滑动到新的位置时,包含在该窗口中的部分张量与卷积核张量按元素相乘,再将得到的张量求和得到一个单一的标量值。
说明:输入矩阵 与卷积核 运算后的输出矩阵的形状为
用 PyTorch 代码实现卷积运算过程:
import torch
"""定义卷积运算函数"""
def cust_conv2d(X, K):
# 获取卷积核形状
h, w = K.shape
# 初始化输出值 Y
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([[1.0, 1.0, 1.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 0.0, 0.0]])
K = torch.tensor([[1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 1.0]])
print(cust_conv2d(X, K))
1、卷积核
卷积核 也称为 过滤器 ,是整个卷积过程的核心,比较简单的卷积核有垂直卷积核 Horizontal Filter、水平卷积核 Vertical Filter、索贝尔卷积核 Sobel Filter 等。这些卷积核能够检测图像的水平边缘、垂直边缘,增强图像中心区域权重等。
卷积核的作用:检测垂直边缘,检测水平边缘,以及检测其他边缘特征。
关于如何确定卷积核:卷积核类似于标准神经网络中的权重矩阵 W ,W 需要通过梯度下降算法反复迭代所得。同样,在深度学习中,卷积核也需要通过模型训练所得。卷积神经网络的主要目的是计算出这些卷积核的数值。确定得到这些卷积核后,卷积神经网络的浅层网络也就实现了对图像所有边缘特征的检测。
【代码实操】取图 6-7 为例,给定输入 X 及 输出 Y ,根据卷积运算,通过多次迭代,可以得到卷积核的近似值。
import torch
from torch import nn
"""定义输入和输出"""
X = torch.tensor([[10., 10., 10., 0.0, 0.0, 0.0], [10., 10., 10., 0.0, 0.0, 0.0], [10., 10., 10., 0.0, 0.0, 0.0],
[10., 10., 10., 0.0, 0.0, 0.0], [10., 10., 10., 0.0, 0.0, 0.0], [10., 10., 10., 0.0, 0.0, 0.0]])
Y = torch.tensor([[0.0, 30., 30., 0.0], [0.0, 30., 30., 0.0], [0.0, 30., 30., 0.0], [0.0, 30., 30., 0.0]])
"""训练卷积层"""
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 3), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度)
X = X.reshape((1, 1, 6, 6))
Y = Y.reshape((1, 1, 4, 4))
lr = 0.001
# 定义损失函数
loss_fn = nn.MSELoss()
for i in range(400):
Y_pre = conv2d(X)
loss = loss_fn(Y_pre, Y)
conv2d.zero_grad()
loss.backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i+1) % 100 == 0:
print(f'epoch {i+1}, loss: {loss.item():.4f}')
"""查看卷积核"""
print(conv2d.weight.data.reshape((3, 3)))
2、步幅
步幅 strides 就是卷积核在输入矩阵中每次移动的格数,在图像中就是跳过的像素个数。
在小窗口移动过程中,卷积核的值始终保持不变,即卷积核的值在整个过程中是共享的,称其为 共享变量 。卷积神经网络采用参数共享的方法大大降低了参数的数量。在用 PyTorch 实现时,strides 参数格式为单个整数或两个整数的元组。
3、填充
当输入图像与卷积核不匹配或卷积核超过图像边界时,可以采用边界填充的方法,即对图像尺寸进行扩展,扩展区域补零。补零填充 zero padding 对于图像边缘部分的特征提取是很有帮助的,可以防止信息丢失。
根据是否扩展可将填充方式分为 Same 和 Valid 两种。采用 Same 时,对图像扩展并补零;采用 Valid 时,不对图像进行扩展。在实际训练过程中,通常选择 Same 方式,因为使用这种方式不会丢失信息。
假设填充零的圈数为 p ,输入数据大小为 n ,卷积核大小为 f ,步幅大小为 s ,则有:,卷积后的大小: 。
4、多通道上的卷积
前面对卷积在输入数据、卷积核的维度上进行了扩展,但输入数据、卷积核都是单一的。从图像的角度来说,二者都是灰色的,没有考虑彩色图像的情况。在实际应用中,输入数据往往是多通道的,如彩色图像是 3 通道,即 R 、G 、B 通道。
(1)多输入通道
3 通道图像 的卷积运算和单通道图像的卷积运算基本一致,对于 3 通道的 RGB 图像,其对应的卷积核算子同样也是 3 通道的。例如图 6-14 中的输入图像是 3×5×5 的,3 个维度分别表示 通道数 channel 、高度 height 、宽度 width 。卷积过程就是将每个单通道与对应的卷积核进行卷积运算,然后将 3 通道的和相加,得到输出图像的一个像素值。
用 PyTorch 代码实现卷积运算过程:
import torch
"""定义卷积运算函数"""
def cust_conv2d(X, K):
# 获取卷积核形状
h, w = K.shape
# 初始化输出值 Y
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
"""定义多输入通道卷积运算函数"""
def conv2d_multi_in(X, K):
h, w = K.shape[1], K.shape[2]
value = torch.zeros(X.shape[0]-h+1, X.shape[1]-w+1)
for x, k in zip(X, K):
value = value + cust_conv2d(x, k)
return value
"""定义输入数据"""
X = torch.tensor([[[1., 0., 1., 0., 2.], [1, 1, 3, 2, 1], [1, 1, 0, 1, 1], [2, 3, 2, 1, 3], [0, 2, 0, 1, 0]],
[[1., 0., 0., 1., 0.], [2, 0, 1, 2, 0], [3, 1, 1, 3, 0], [0, 3, 0, 3, 2], [1, 0, 3, 2, 1]],
[[2., 0., 1., 2., 1.], [3, 3, 1, 3, 2], [2, 1, 1, 1, 0], [3, 1, 3, 2, 0], [1, 1, 2, 1, 1]]])
K = torch.tensor([[[0., 1., 0.], [0, 0, 2], [0, 1, 0]],
[[2., 1., 0.], [0, 0, 0], [0, 3, 0]],
[[1., 0., 0.], [1, 0, 0], [0, 0, 2]]])
print(conv2d_multi_in(X, K))
(2)多输出通道
为了实现更多边缘检测,可以增加更多 卷积核组 。不同卷积核组卷积得到不同的输出,个数由卷积核组决定。
(3)1×1 卷积核
1×1 卷积核 在很多经典网络结构中都有使用,例如 Inception 网络、ResNet 网络、YOLO 网络和 Swin-Transformer 网络等。
在网络中增加 1×1 卷积核的主要作用:
① 增加或降低通道数:如果卷积的输入输出都只是一个二维数据,那么 1×1 卷积核意义不大,它完全不考虑像素与周边其他像素之间的关系。但如果卷积的输入输出是多维矩阵,则可以通过 1×1 卷积核不同的通道数,增加或降低卷积后的通道数。
② 增加非线性:1×1 卷积核利用后接的非线性激活函数,可以在保持特征图尺度不变的前提下大幅增加非线性特性,使网络更深,同时提升网络的表达能力。
③ 跨通道信息交互:使用 1×1 卷积核可以增加或降低通道数,也可以组合来自不同通道的信息。
图 6-17 为通过 1×1 卷积核改变通道数的示例:
用 PyTorch 代码实现卷积运算过程:
import torch
"""生成输入及卷积核数据"""
X = torch.tensor([[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[1, 1, 1], [1, 1, 1], [1, 1, 1]],
[[2, 2, 2], [2, 2, 2], [2, 2, 2]]])
K = torch.tensor([[[[1]], [[2]], [[3]]],
[[[4]], [[1]], [[1]]],
[[[5]], [[3]], [[3]]]])
print(K.shape) # torch.Size([3, 3, 1, 1])
"""定义卷积运算函数"""
def cust_conv2d(X, K):
# 获取卷积核形状
h, w = K.shape
# 初始化输出值 Y
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
"""定义多输入通道卷积运算函数"""
def conv2d_multi_in(X, K):
h, w = K.shape[1], K.shape[2]
value = torch.zeros(X.shape[0]-h+1, X.shape[1]-w+1)
for x, k in zip(X, K):
value = value + cust_conv2d(x, k)
return value
"""定义卷积函数"""
def conv2d_multi_in_out(X, K):
return torch.stack([conv2d_multi_in(X, k) for k in K])
print(conv2d_multi_in_out(X, K))
5、激活函数
卷积神经网络与标准的神经网络类似,为了保证非线性,也需要使用 激活函数 ,即在卷积运算后,把输出值另加偏移量输入激活函数,作为下一层的输入。
常用的激活函数有 torch.nn.functional.sigmoid、torch.nn.functional.relu、torch.nn.functional.softmax、torch.nn.functional.tanh、torch.nn.functional.dropout 等。类对象方式的激活函数有 torch.nn.Sigmoid、torch.nn.ReLU、torch.nn.Softmax、torch.nn.Tanh、torch.nn.Dropout 等。
6、卷积函数
卷积函数 是构建神经网络的重要支架,通常 PyTorch 的卷积运算是通过 nn.Conv2d 来完成的。
(1)nn.Conv2d 函数
nn.Conv2d (
in_channels: int,
out_channels: int,
kernel_size: Union[int, Tuple[int, int]],
stride: Union[int, Tuple[int, int]] = 1,
padding: Union[int, Tuple[int, int]] = 0,
dilation: Union[int, Tuple[int, int]] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = 'zeros',
)
主要参数说明:
参数 | 说明 |
---|---|
in_channels(int) | 输入信号的通道 |
out_channels(int) | 卷积产生的通道 |
kernel_size(int or tuple) | 卷积核的尺寸 |
stride(int or tuple, optional) | 卷积步长 |
padding(int or tuple, optional) | 输入的每一条边补充 0 的层数 |
dilation(int or tuple, optional) | 卷积核元素之间的间距 |
groups(int, optional) | 控制输入和输出之间的连接 |
bias(bool, optional) | 如果 bias=True 则添加偏置 |
padding_mode | 可选模式:zeros、reflect、replicate、circular |
说明:当 group=1 时,输出是所有的输入的卷积;当 group=2 时,相当于有并排的两个卷积层,每个卷积层计算输入通道的一半,并且产生的输出是输出通道的一半,随后将这两个输出连接起来。 在 bias 中,参数 kernel_size、stride、padding、dilation 可以是整型数值 int ,此时卷积的 height 和 width 值相同,也可以是 tuple 数组,第一维度表示 height 值,第二维度表示 width 值。
(2)输出形状
卷积函数 nn.Conv2d 参数中输出形状的计算公式:
Input:
Output:
weight:(out_channels, in_channels/groups, kernel_size[0], kernel_size[1])
import torch
import torch.nn as nn
"""当 groups=1 时"""
conv = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=1, groups=1)
conv.weight.data.size() # torch.Size([12, 6, 1, 1])
print(conv.weight.data.size())
"""当 groups=2 时"""
conv = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=1, groups=2)
conv.weight.data.size() # torch.Size([12, 3, 1, 1])
print(conv.weight.data.size())
"""当 groups=3 时"""
conv = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=1, groups=3)
conv.weight.data.size() # torch.Size([12, 2, 1, 1])
print(conv.weight.data.size())
7、转置卷积
转置卷积 Transposed Convolution 在某些文献中也称为反卷积 Deconvolution 或部分跨越卷积 Fractionally-strided Convolution 。
通过卷积的正向传播的图像通常会越来越小,类似于下采样。卷积的反向传播实际上就是一种转置卷积,类似于上采样。
我们先来简单回顾卷积的正向传播,假设卷积操作的输入大小 n 为 4 ,卷积核大小 f 为 3 ,步幅 s 为 1 ,填充 p 为 0 。
对于上述卷积运算,我们将 3×3 卷积核展平为 [4,16] 的稀疏矩阵 C :
我们再将 4×4 的输入特征展平为 [16,1] 的矩阵 X ,那么 Y = CX 将是 [4,1] 的输出特征矩阵,将其重新排列成 2×2 的输出特征。
那么反向传播时又会如何呢?假设损失函数为 L ,则反向传播时,可以利用链式法则得到对 L 关系的求导:
由此,可得 ,即反卷积就是要对这个矩阵运算过程进行逆运算。转置卷积主要用于生成式对抗网络 GAN 。
PyTorch 中二维转置卷积的格式为:
torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0,
output_padding=0, groups=1, bias=True, dilation=1, padding_mode='zeros')
8、特征图与感受野
输出的卷积层有时被称为 特征图 Feature Map ,因为它可以被视为一个输入映射到下一层的空间维度的转换器。在 CNN 中,对于某一层的任意元 x ,其 感受野 Receptive Field 是指在正向传播期间可能影响 x 计算的所有元素(来自所有先前层)。
感受野的覆盖率可能大于某层输入的实际区域大小。感受野的定义是卷积神经网络每一层输出的特征图上的像素点在输入图像上映射的区域大小。再通俗点的解释是,感受野是特征图上的一个点对应输入图上的区域。
由图可知,经过几个卷积层后,特征图逐渐变小,一个特征所表示的信息量变多,如 表示了 、 、 、 、 的信息。
9、全卷积网络
利用卷积神经网络进行图像分类或回归任务时,我们通常会在卷积层之后接上若干个全连接层,将卷积层产生的特征图映射成一个固定长度的特征向量。因为最终都期望得到整个输入图像属于哪类对象的概率值,如图 6-22 所示,AlexNet 的 ImageNet 模型输出一个 1000 维的向量表示输入图像属于每一类的概率(经过 softmax 归一化)。
与通常用于分类或回归任务的卷积神经网络不同,全卷积网络 Fully Convolutional Network 可以接收任意尺寸的输入图像,将上图中的 3 个全连接层改成卷积核尺寸为 1×1,通道数为向量长度的卷积层。然后,采用转置卷积运算对最后一个卷积层的特征图进行上采样,使其恢复到与输入图像相同的尺寸,从而可以对每个像素都产生一个预测,同时保留原始输入图像中的空间信息。接着在上采样的特征图上进行逐像素分类,最后逐个像素计算分类的损失,相当于每一个像素对应一个训练样本。这样整个网络都使用卷积层,没有全连接层,这也许就是全卷积网络名称的由来。该网络的输出类别预测与输入图像在像素级别上具有一一对应关系,其通道维度的输出为该位置对应像素的类别预测,如图 6-23 所示。