供参考
CNN笔记:通俗理解卷积神经网络
Convolutional Neural Networks (CNNs / ConvNets)
卷积神经网络 (CNN) 基本原理和公式
零基础入门深度学习(4) - 卷积神经网络
深度学习:卷积神经网络中的卷积核
最容易理解的对卷积(convolution)的解释
【DL笔记6】从此明白了卷积神经网络(CNN)
在 CNN 中,为什么要逐渐增加特征图的通道数?
机器学习算法之——卷积神经网络(CNN)原理讲解
图解CNN
你必须要知道CNN模型:ResNet 残差学习
ResNet——CNN经典网络模型详解


卷积神经网络(Convolutional Neural Network,CNN)

卷积神经网络最早是为处理图像数据而设计的神经网络。其本质是对格状数据(grid-like data)施加局部连接 + 权重共享,也就是说CNN同样可以用于一维时间序列与三维视频,并不局限于图像。

过去,使用多层感知机做图像处理,可能需要数十亿个参数来表示网络中的一层,相比之下,卷积通过局部连接与参数共享大幅减少可训练参数量。

不过参数大幅减少也存在代价:所有权重学习都依赖归纳偏置。卷积网络默认局部性与平移不变性,当这些假设与任务的数据分布相符时,归纳偏置能显著提高样本效率与泛化;若偏置与真实分布不符,则需要更多数据、更适合的架构或额外机制来拟合与泛化。

根据生活中的先验知识,图像的空间不变性(spatial invariance)具备以下两种性质:

  1. 平移不变性(translation invariance):不管检测对象出现在图像中哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为平移不变性。
  2. 局部性(locality):卷积层采用有限的局部感受野,在浅层只处理邻近像素的局部模式;随层级加深网络的感受野会逐渐扩大,进而在深层能整合远距像素的信息为语义特征。最终,这些局部/层级特征通过汇聚被组合用于整图级别的预测。

严格来讲,卷积层输出的特征图随输入平移而平移属于translation equivariance(平移等变性),虽然通常简化理解为不变性但实际上是属于两个概念。卷积运算对输入的平移是等变的——输入平移会使特征图对应地平移;而要在输出上得到位置不敏感的”真正不变性”,通常需要池化、全局池化或后续的聚合策略来实现。

从全连接层到卷积中介绍了两种性质、卷积定义和通道的数学形式。

卷积层

卷积层将输入和卷积核做互相关运算,并加上标量偏差得到输出。在训练模型时,通常先对卷积核做随机初始化,然后迭代卷积核和偏差。
卷积有时被称为特征映射(feature map),因为它可以视为一个输入映射到下一层的空间维度的转换器。

1
2
3
4
5
6
7
8
9
# 继承于nn.Module,Conv2D是一个神经网络模块
class Conv2D(nn.Module):
def __init__(self, kernel_size): # __init__构造函数接收 kernel_size(卷积核形状)
super(Conv2D, self).__init__() # 调用父类的初始化函数,确保 nn.Module 内部机制正常运行
self.weight = nn.Parameter(torch.randn(kernel_size)) # 卷积核的参数,是二维张量不是向量
self.bias = nn.Parameter(torch.randn(1)) # 偏置项

def forward(self, x): # 前向传播
return corr2d(x, self.weight) + self.bias

感受野(Receptive Field)

参考
感受野的含义与计算

神经网络中神经元”看到的”输入区域,在卷积神经网络中,feature map上某个元素的计算受输入图像上某个区域的影响,这个区域即该元素的感受野。感受野是个相对概念,某层feature map上的元素看到前面不同层上的区域范围是不同的,通常在不特殊指定的情况下,感受野指的是看到输入图像上的区域。

卷积核大小(Kernel size)

常用大小为 3×3(最常用)、5×5,7x7,有时也用1×1(用于通道变换和升维降维)。较小卷积核组合层数可以实现更大感受野,且参数更少。
5x5的卷积核可以用两个3x3的卷积核代替,感受野一致,效率确实更好,但激活函数加强其映射的同时却会导致一定的细节丢失,所以高精度问题依然偏向使用大尺寸卷积核。

卷积层与汇聚层的对比

方面 卷积层 汇聚层
参数 可训练的卷积核参数 无参数,固定的聚合操作
主要功能 特征提取和变换 降维与特征汇聚
降维方式 通过步长(stride)和填充(padding)调整输出尺寸 通过池化窗口和步长缩小空间尺寸
信息保留 保留更多细节和复杂特征 牺牲部分细节,保留局部显著特征
计算复杂度 相对较高 较低
对模型鲁棒性的影响 依赖卷积核设计和训练 提高对平移、小变形的鲁棒性

卷积层保留更多特征细节,具备学习能力。
汇聚层用于降低空间维度和计算成本,增强模型对输入变换的鲁棒性。

互相关运算

卷积层得名于卷积运算,但通常在卷积层中使用更加直观的互相关(cross-correlation)运算。
二维互相关运算中,卷积窗口从输入数组的左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。
1

1
2
3
4
5
6
7
8
9
10
import torch 
from torch import nn
# X输入特征图,形状(H,W);K卷积核,形状(h,w);Y结果图,存放滑动窗口计算结果的二维张量
def corr2d(X, K):
h, w = K.shape # 卷积核的高宽
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 固定形状,存放输出结果,赋0无任何含义,只是用于填充
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() # 取出与核同形状的窗口,与K逐元素相乘后求和
return Y

卷积核

卷积核也被称作滤波器(Filter)。
PyTorch 的卷积核参数是随机初始化,输入图像通常也不属于高斯分布,是有纹理、有偏色、有边缘的图像,故卷积层输出的分布并不是天然稳定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 构造二维卷积层,(输出通道,输入通道,卷积核形状,是否使用偏置)
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
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 的梯度
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad # 更新卷积核参数
# .data[:] 表示对原张量的值做原地更新(不会创建新张量),不会被 autograd 记录,下面注释效果等价
# with torch.no_grad():
# conv2d.weight -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')

填充(padding)和步幅(stride)

填充

1

在输入图像的边界填充元素(通常填充0),填充0并不是为了还原边缘信息,而是让边缘能被卷积核覆盖到,通过训练去拟合边缘区域应该如何处理。
卷积不降维控制特征图的空间维度(H × W)变化策略能保持特征图大小不变(对于残差连接、对齐输出特征很重要),逐层控制感受野增长速度(稳定局部特征提取速度),预防过快地降维(避免剧烈的信息损失)
填充0虽然不直接增加感受野,但通过保持尺寸不变与多层卷积堆叠,能让更深层的卷积核感受到更大范围的输入区域。

CNN强调从细节到语义的渐进式建构,而不是突变式压缩。因此多层CNN去扩大特征通道量是必要的。

  1. 低层特征是高层语义的基础,图像语义的形成是逐层抽象的过程,如果低层细节在前几层就被池化或卷积快速抹去,那么深层根本没机会学习”从边缘->形状->物体”的演化路径。
  2. 很多任务需要细节,图像分割、图像修复、关键点检测等任务对局部精度要求极高。
  3. 梯度传播与训练稳定性,太快压缩特征图尺寸,会导致梯度传回路径过窄,信息瓶颈造成特征通道表达受限,导致训练困难。

卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足: 1. 卷积核的大小是奇数; 2. 所有边的填充行数和列数相同; 3. 输出与输入具有相同高度和宽度 则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from torch import nn
# 对输入 X 调整维度以适配 nn.Conv2d 的输入格式,并返回去掉批量和通道维度的卷积结果
def comp_conv2d(conv2d, X):
# nn.Conv2d 的输入必须是 4D 张量(batch_size, channels, height, width),其他可选项不计
# X 是一个二维张量 (H, W),扩充到四维,batch_size = 1(1 张图)。channels = 1(单通道灰度图)
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道,只是为了符合 nn.Conv2d 的输入输出格式添加的
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))
print(comp_conv2d(conv2d, X).shape) # 输出张量形状

当卷积核的高度和宽度不同时,可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。

1
2
3
# kernel_size高5宽3,padding表示上下两边各填充2行零左右两边各填充1行零,padding可选,nn.Conv2d本质传入还是4D
# kernel_size只有一个数会自动扩充成方阵
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))

步幅(Stride)

stride是卷积核在输入特征图上滑动时每一步移动的像素数

为了高效计算或是缩减采样次数,卷积窗口可以滑动多个元素。
1

1
2
# 卷积层
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))

汇聚层(pooling)

处理图像时逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
汇聚层具有双重目的:降低卷积层对位置的敏感性,降低对空间降采样表示的敏感性。
分辨率小的图像使用池化要慎重,因为容易丢失过多的信息,例如ImageNet图像224x224适宜使用池化,但是CIFAR-10图像32x32不适宜使用池化。

现代卷积网络中,除了最后做全局汇聚或保留非线性特征选择,池化层在现代CNN中基本被stride>1的卷积取代。
池化的优点是节省参数对轻量级模型友好,MaxPool突出局部最强响应(极端),正则化相对更鲁棒,全局汇聚。

最大汇聚层和平均汇聚层

池化是CNN中一种空间汇聚操作。通常,卷积得到的特征图需要池化层降低数据量。
由于池化层并不训练参数,所以计算模型深度时并不将其作为一层考虑在内。

缩小图像称为下采样(subsampling)或降采样(downsampled),放大图像称为上采样(upsampling)或图像插值(interpolating)。

汇聚层运算也由一个固定形状窗口组成(称为汇聚窗口),该窗口根据其步幅大小在输入的所有区域上滑动。汇聚层并不包含参数。池运算是确定的,通常为计算汇聚窗口中所有元素的最大值或平均值。这些操作层称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
1

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
from torch import nn

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

填充和步幅

汇聚层也可以改变输出形状。

1
2
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))

多个通道

在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。

1
X = torch.cat((X, X + 1), 1)  # 拼接

多输入多输出通道

添加通道时,我们的输入和隐藏的表示都变成了三维张量。

多输入通道

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
1

1
2
3
4
5
6
7
8
9
10
11
import torch
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
def corr2d_multi_in(X, K):
# 先遍历"X"和"K"的第0个维度(通道维度),再把它们加在一起
return sum(corr2d(x, k) for x, k in zip(X, K))

多输出通道

随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,可以将每个通道视作CNN对不同特征的响应。

1
2
3
4
5
6
def corr2d_multi_in_out(X, K):
# X: 输入特征图,形状 (C_in, H_in, W_in)
# K: 单个输出通道的卷积核,形状(C_out, C_in, kH, kW)
# 把二维图按通道维度排列在一起,形成一个三维张量 (C, H, W)
# torch.stack 的第二个参数 dim=0 表示沿着第0维堆叠
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

1X1卷积层

卷积的本质是有效提取相邻像素间的相关特征,而1X1卷积显然没有此作用。
1
1x1 卷积通常是用于为每个空间位置的通道做线性组合,实现通道变换或升降维。

1
2
3
4
5
6
7
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)) # 将1x1卷积核权重展平为二维矩阵,行是输出通道,列是输入通道
Y = torch.matmul(K, X) # 矩阵乘法,等价于对每个空间位置做线性变换
return Y.reshape((c_o, h, w)) # 恢复空间维,输出多通道特征图

BatchNorm层(BN层)

因为卷积的输出本身不稳定、不平衡,所以需要BN层强行校正,要在ReLU前做标准化,避免失去原始的统计信息导致的失真。

卷积神经网络框架

LeNet

LeNet(LeNet-5)由两个部分组成:

卷积编码器:由两个卷积层组成;

全连接层密集块:由三个全连接层组成。
1
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。

在卷积神经网络中,常在若干卷积层后接一个池化层(总体趋势是逐渐降低分辨率,但方式不限于池化),以降低特征图的空间分辨率,从而减少计算量,并增强网络对图像小幅平移、缩放等变换的鲁棒性(不变性)。

权值共享是卷积神经网络(CNN)的一大核心特点。具体来说,每个卷积核(滤波器)在输入特征图上滑动,使用相同的一组权重进行卷积操作。因此,卷积层的参数数量只等于卷积核中权重的总数加上一个偏置项。相比于传统的全连接神经网络(如 BP 网络),这种权值共享机制大幅减少了需要训练的参数数量,从而降低了模型复杂度并有助于防止过拟合。

权值共享让卷积核参数在空间上”全局固定”,即用同一组局部滤波器权重在整个图上滑动,保证网络能高效且一致地捕捉图像中的局部特征。

ResNet(残差网络)

为什么残差网络(Residual Block)看似简单却极为有效
深入理解残差模块(残差函数,残差块block,residual模块,残差结构)
残差连接skip connect
一文搞懂Batch Normalization 和 Layer Normalization
BatchNorm是对一个batch-size样本内的每个特征[分别]做归一化,LayerNorm是[分别]对每个样本的所有特征做归一化。
神经网络中BN层的作用

随着网络层数的增加,网络发生了退化(degradation)的现象:训练集loss逐渐下降,然后趋于饱和,再增加网络的深度反而使得训练集loss增大。

CNN2

  • 无论是在训练集还是测试集中,拥有更深层次的网络表现均比浅层次的网络差,显然不是过拟合导致退化。
  • BN层(Batch Normalize)可以通过规整数据的分布基本解决梯度消失/爆炸的问题,所以也不是导致深层网络退化的原因。

选择加深网络的层数,是希望深层的网络的表现能比浅层好,或者是希望它的表现至少和浅层网络持平(相当于直接复制浅层网络的特征)

由于非线性激活函数Relu的存在,每次输入到输出的过程都几乎是不可逆的,造成了许多不可逆的信息损失。一个特征的一些有用信息损失了,那他的表现还能做到持平吗?答案是否定的。
CNN1

ResNet的初衷,就是让网络拥有这种恒等映射的能力,能够在加深网络的时候,至少能保证深层网络的表现至少和浅层网络持平。

残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。
误差是衡量观测值和真实值之间的差距,残差是指预测值和观测值之间的差距。
在残差块中,输入可通过跨层数据线路更快地向前传播。

1

在实际中,当理想映射f(x)接近于恒等映射时,残差映射易于捕捉恒等映射的细微波动。
在残差块中,输入可以通过跨层数据线路更快地向前传播。

不同残差块

残差块使得深层网络更加容易训练(不管网络有多深,因为跨层数据通路连接的存在,使得始终能够包含小的网络,会先将下层的小型网络训练好再去训练更深层次的网络),甚至可以训练一千层的网络(只要内存足够,优化算法就能够实现,不过通常不需要这么深)。
学习嵌套函数是神经网络的理想情况,在深层神经网络中,学习另一层作为恒等映射比较容易
残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零
利用残差块可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播

ResNet在模型层数很深时(这里假设256维),会使用1x1卷积层降低通道数至64维以减少计算开销。尽管通道数降低,但是精度并不会明显下降,这是因为高维特征并不全部都是有效的,1x1卷积通过监督学习能够保留256维中最关键的64维信息,而在输出时升维回到256维是为了与输入的256维对齐。
直接使用64维特征,冗余特征混杂不易提取有效特征,效果相对较差。