供参考
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(平移等变性),虽然通常简化理解为不变性但实际上是属于两个概念。卷积运算对输入的平移是等变的——输入平移会使特征图对应地平移;而要在输出上得到位置不敏感的“真正不变性”,通常需要池化、全局池化或后续的聚合策略来实现。

从全连接层到卷积中介绍了两种性质、卷积定义和通道的数学形式,不过思想懂了也没什么必要看,用不到,下面简要讲讲两种性质。

平移不变性(translation invariance)

输入矩阵 的平移,只会导致其隐藏表示 的平移。 被称为卷积核(convolution kernel)或者滤波器(filter)。 是一个常数。

实际上不依赖于 的具体位置,即:

因此,我们可以将 的定义简化为:

$$
[H]{i,j} = u + \sum{a=-\Delta}^{\Delta} \sum_{b=-\Delta}^{\Delta} [V]{a,b} [X]{i+a, j+b}.
$$

使用系数 $[V]{a,b}(i,j)[X]{i+a, j+b}[H]_{i,j}$。

$[V]{a,b}V{i,j,a,b}$ 少很多,因为前者摆脱了对图像位置的依赖。

局部性(locality)

收集用来训练参数 $[H]{i,j}(i,j)|a| > \Delta \quad \text{或} \quad |b| > \Delta[V]{a,b} = 0$ 。

因此,可以将 重写为:

通道

图像一般包含三个通道(红、绿、蓝),是一个三维张量,比如尺寸为 ,其中前两个轴是空间位置,第三个轴是颜色通道。

因此,将张量 索引为:

对应地,卷积权重张量扩展为四维:

此外,输入图像是三维的,隐藏表示 也最好用三维张量表示:对每个空间位置采用一组隐藏表示(多个通道)。可以把隐藏表示想象为多个二维张量堆叠而成的通道(channel),也称为特征映射(feature maps)。

多通道卷积层可表示为:

其中隐藏表示 中的索引 是输出通道,输出继续作为三维张量输入下一卷积层。

互相关运算

严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。

感受野(Receptive Field)

参考
彻底搞懂感受野的含义与计算

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

卷积核大小(Kernel size)

常用大小为 3×3、5×5,有时也用 1×1(用于通道变换和升维降维)。较小卷积核组合层数可以实现更大感受野,且参数更少。

汇聚层与卷积层的区别与作用总结

降维(减少空间尺寸),从而减少后续计算量和参数规模,提高模型对平移和微小变形的鲁棒性,助于防止过拟合,增强模型的泛化能力。

通过局部汇聚(最大池化或平均池化)提取局部显著信息,虽然会丢失部分细节,但保留最重要的特征。

卷积层(Convolutional Layer)与汇聚层的对比

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

从信息保留角度看,卷积层保留更多特征细节,且具有学习能力。
汇聚层主要用于降低空间维度和计算成本,并增强模型对输入变换的鲁棒性。两者在卷积神经网络中常常配合使用,互为补充。

图像卷积

卷积神经网络(convolutional neural network)是含有卷积层(convolutional layer)的神经网络。
基于图像的空间局部相关性假设——邻近像素更相关,局部特征更有意义。

conv 是 convolution(卷积) 的缩写,卷积是一个滑动窗口求加权和的过程。

互相关运算

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

1
2
3
4
5
6
7
8
9
10
11
12
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

卷积层

卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。

1
2
3
4
5
6
7
8
9
10
# 继承 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

学习卷积核

卷积核也称为滤波器(Filter)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 构造二维卷积层,(输出通道,输入通道,卷积核形状,是否使用偏置)
# ps:PyTorch 的 Conv2d 参数是 [out_channels, in_channels, height, width],上面是自定义的
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}')

特征映射和感受野

卷积层有时被称为特征映射(feature map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。 在卷积神经网络中,对于某一层的任意元素
,其感受野(receptive field)是指在前向传播期间可能影响
计算的所有元素(来自所有先前层)。

填充(padding)和步幅(stride)

填充

填充(padding):在输入图像的边界填充元素(通常填充元素是0)

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

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 是卷积核在输入特征图上滑动时每一步移动的像素数

有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。将每次滑动元素的数量称为步幅(stride)。

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

多输入多输出通道

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

多输入通道

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

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))

多输出通道

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

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卷积显然没有此作用。

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)) # 恢复空间维,输出多通道特征图

汇聚层

通常处理图像时,希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

最大汇聚层和平均汇聚层

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

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

与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层,汇聚层并不包含参数。池运算是确定性的,通常计算汇聚窗口中所有元素的最大值或平均值。这些操作层分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。

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)  # 拼接

卷积神经网络(LeNet)

LeNet

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

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

全连接层密集块:由三个全连接层组成。

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

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

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

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

残差网络(ResNet)

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

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

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

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

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

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

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

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

不同残差块

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

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