卷积神经网络CNN
🤔 为什么需要CNN?
在CNN出现之前,处理图像等数据通常使用全连接神经网络。这种方式存在两个致命缺陷:
- 参数爆炸:一张224x224像素的RGB彩色图像,展平后就有超过15万个输入维度。如果连接到一个仅有1000个神经元的全连接层,仅这一层的参数就高达1.5亿个,导致模型极难训练且容易过拟合。
- 丢失空间信息:将图像展平为一维向量,完全破坏了像素之间的空间位置关系和局部相关性,而这对理解图像内容至关重要。
CNN通过其独特的设计思想完美解决了这些问题。
CNN的核心设计思想
- 局部连接 (Local Connectivity):网络中的每个神经元只与输入数据的一个局部小区域(称为"感受野")相连。这使得网络能够首先关注并提取图像的局部特征,如边缘、角点等。
- 权值共享 (Weight Sharing):同一个"特征探测器"(即卷积核)会滑过整张图像。这意味着,无论一个特征(比如猫的眼睛)出现在图像的哪个位置,都由同一组参数来识别。这极大地减少了模型参数量,并赋予了模型一定的平移不变性。
- 层次化特征提取 (Hierarchical Feature Extraction):通过堆叠多个卷积层,网络能够自动学习从简单到复杂的特征。浅层网络学习低级特征(边缘、颜色),深层网络则将这些低级特征组合成高级语义特征(如眼睛、车轮、完整的物体)。
CNN的典型架构
一个经典的CNN可以看作一个特征提取器(卷积层+池化层)和一个分类器(全连接层)的组合。
- 卷积层 (Convolutional Layer)
- 作用:特征提取的核心。
- 工作原理 :使用一个小的矩阵(称为卷积核 或滤波器 )在输入图像上滑动。在每个位置,将卷积核与覆盖的图像区域进行点积运算(对应元素相乘再求和),得到一个值。这个过程会生成一个二维的特征图 (Feature Map)。使用多个不同的卷积核,就能提取多种不同的特征。
- 关键超参数:
- 卷积核大小 (Kernel Size):通常是 3x3 或 5x5。
- 步长 (Stride) :卷积核每次滑动的像素数。步长越大,输出特征图的尺寸越小,但同时也会增大每个神经元在输入数据上的感受野,这意味这每个神经元能够捕捉到更大范围的输入信息。
- 填充 (Padding) :在输入图像边缘补零。常用"Same Padding"来保持输入输出尺寸一致,防止边缘信息丢失。
- 激活函数 (Activation Function)
- 作用:引入非线性。卷积运算是线性的,如果没有激活函数,无论堆叠多少层,整个网络仍然等价于一个线性模型,无法学习复杂模式。
- 常用函数 :ReLU (Rectified Linear Unit) 是最主流的选择,其公式为
f(x) = max(0, x)。它计算简单,能有效加速训练并缓解梯度消失问题。
- 池化层 (Pooling Layer)
- 作用:也称为下采样层,用于降低特征图的空间尺寸,减少计算量和参数,同时增强模型对微小平移的鲁棒性。
- 常用方法 :最大池化 (Max Pooling),例如在一个 2x2 的窗口中取最大值作为输出。这相当于保留了该区域内最显著的特征。
- 全连接层 (Fully Connected Layer)
- 作用:通常位于网络末端。它将前面所有卷积层和池化层提取到的高级特征图"展平"成一维向量,并整合所有信息,进行最终的分类或回归决策。
典型数据流:输入图像 → [卷积层 → 激活函数 → 池化层] (重复N次) → 展平 → 全连接层 → 输出
卷积计算
卷积计算是卷积神经网络(CNN)最核心的运算过程。你可以把它理解为一个"特征提取器",通过一个小的矩阵(卷积核)在大的矩阵(输入图像)上滑动,来探测图像中的特定模式,如边缘、纹理等。
卷积计算的核心步骤
卷积计算的本质是"滑动窗口"下的"乘法-加法"运算。下面通过一个详细的例子来拆解整个过程。
1. 准备工作:定义输入与卷积核
假设我们有一张 5x5 的灰度图像(输入矩阵)和一个 3x3 的卷积核(也叫滤波器)。
- 输入图像 (Input): 一个 5x5 的矩阵。
- 卷积核 (Kernel/Filter): 一个 3x3 的矩阵,其内部的数值是模型学习到的权重。
- 偏置 (Bias): 一个标量值,会加到卷积运算的结果上。
- 步长 (Stride): 卷积核每次滑动的像素距离,这里我们假设步长为 1。
- 填充 (Padding): 为了简化,我们先假设不进行填充(即 "Valid" Padding)。
tex
输入图像 (5x5) 卷积核 (3x3)
[1, 0, 1, 0, 1] [1, 0, 1]
[0, 1, 0, 1, 0] [0, 1, 0]
[1, 0, 1, 0, 1] [1, 0, 1]
[0, 1, 0, 1, 0]
[1, 0, 1, 0, 1]
2. 单次卷积运算:点积求和
卷积核会从输入图像的左上角开始,覆盖一个 3x3 的区域。
- 对齐 (Alignment): 将 3x3 的卷积核覆盖在输入图像左上角的 3x3 区域上。
- 逐元素相乘 (Element-wise Multiplication): 将卷积核中的每个数字与输入图像对应位置的数字相乘。
- 求和 (Summation): 将所有 9 个乘积结果相加。
- 加偏置 (Add Bias): 将上一步得到的和加上一个偏置值
b。
这个过程可以用一个公式来表示,输出特征图在位置 (i, j) 的值 O(i, j) 为:
O(i, j) = Σ(输入区域 * 卷积核权重) + 偏置
让我们计算输出特征图的第一个值(左上角):
-
输入区域:
tex[1, 0, 1] [0, 1, 0] [1, 0, 1] -
卷积核:
tex[1, 0, 1] [0, 1, 0] [1, 0, 1] -
逐元素相乘再求和:
(1×1) + (0×0) + (1×1) + (0×0) + (1×1) + (0×0) + (1×1) + (0×0) + (1×1)
= 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1
= 5 -
加上偏置 (假设 b=0):
5 + 0 = 5
所以,输出特征图的左上角第一个值就是 5。
3. 滑动窗口,生成特征图
完成第一次计算后,卷积核会根据设定的步长向右滑动。
- 向右滑动一格 (步长=1): 卷积核现在覆盖了输入图像顶部中间的 3x3 区域。重复上述的"乘法-加法"过程,得到输出特征图第一行的第二个值。
- 继续滑动: 重复此过程,直到卷积核无法再向右移动。
- 换行: 当一行结束后,卷积核会回到最左侧,并向下滑动一格(步长=1),然后开始计算下一行。
这个过程会一直持续,直到卷积核遍历完输入图像所有可能的位置。
4. 计算输出尺寸
对于一个输入尺寸为 H_in × W_in,卷积核尺寸为 K × K,步长为 S,填充为 P 的情况,输出特征图的尺寸 H_out × W_out 可以通过以下公式计算:
H_out = ⌊(H_in - K + 2P) / S⌋ + 1
W_out = ⌊(W_in - K + 2P) / S⌋ + 1
在我们的例子中:
H_in = 5,W_in = 5K = 3P = 0(无填充)S = 1
代入公式:
H_out = ⌊(5 - 3 + 2×0) / 1⌋ + 1 = ⌊2 / 1⌋ + 1 = 3
W_out = ⌊(5 - 3 + 2×0) / 1⌋ + 1 = ⌊2 / 1⌋ + 1 = 3
因此,最终生成的特征图 (Feature Map) 是一个 3x3 的矩阵。这个特征图就是卷积层提取出的某种特定特征(由该卷积核的权重决定)在原始图像上的分布情况。
多通道卷积计算
多通道卷积是卷积神经网络处理真实世界数据(如彩色图像)的基础。它的核心思想是**"先分后合"**:对每个输入通道分别进行卷积,然后将所有通道的结果相加,融合成一个输出通道的特征图。
计算准备:定义输入与卷积核
假设我们要处理一张小型的彩色图像,它有RGB三个通道。
- 输入 (Input): 一个尺寸为
5x5x3的三维矩阵。可以看作是3张5x5的二维图像堆叠而成,分别代表R、G、B通道。 - 卷积核 (Filter/Kernel): 为了生成一个 输出通道,我们需要一个同样具有3个通道的卷积核,其尺寸为
3x3x3。它由3个3x3的二维矩阵堆叠而成,每个矩阵对应输入的一个通道。 - 偏置 (Bias): 一个标量值
b,会加到最终求和的结果上。 - 步长 (Stride) 和 填充 (Padding): 假设步长为1,无填充("Valid" Padding)。
tex
输入 (5x5x3) 卷积核 (3x3x3)
[ R通道 ] [ G通道 ] [ B通道 ] [ K_r ] [ K_g ] [ K_b ]
5x5 5x5 5x5 3x3 3x3 3x3
多通道卷积计算步骤
计算的目标是得到一个 3x3 的输出特征图。我们以计算输出特征图左上角第一个值为例。
- 分通道卷积 (Channel-wise Convolution)
将卷积核的每个通道与输入的对应通道进行独立的二维卷积运算。- R通道计算: 用卷积核的R通道部分
K_r在输入图像的R通道上进行卷积,得到一个数值S_r。
S_r = Σ(输入R区域 * K_r) - G通道计算: 用卷积核的G通道部分
K_g在输入图像的G通道上进行卷积,得到一个数值S_g。
S_g = Σ(输入G区域 * K_g) - B通道计算: 用卷积核的B通道部分
K_b在输入图像的B通道上进行卷积,得到一个数值S_b。
S_b = Σ(输入B区域 * K_b)
- R通道计算: 用卷积核的R通道部分
- 结果求和 (Summation)
将三个通道卷积得到的结果S_r,S_g,S_b相加,并加上偏置b,得到输出特征图在该位置的最终值O。
O = S_r + S_g + S_b + b
这个 O 就是融合了三通道信息后,在输出特征图左上角的值。
- 滑动生成完整特征图
完成一次计算后,整个3x3x3的卷积核会作为一个整体,在5x5x3的输入数据上按照设定的步长滑动,重复上述"分通道卷积,再求和"的过程,直到遍历完所有位置,最终生成一个完整的3x3输出特征图。
如何得到多个输出通道?
上面的过程只生成了一个输出通道的特征图。在实际的CNN中,我们希望提取多种不同的特征(如边缘、颜色、纹理等)。
为了得到多个输出通道,我们只需要使用多个不同的卷积核(Filters)。
- 如果我们想输出
N个通道的特征图,我们就需要准备N个独立的3x3x3卷积核。 - 每个卷积核都会独立地与原始输入
5x5x3进行一次完整的多通道卷积,生成一个3x3的特征图。 - 最终,我们将这
N个3x3的特征图堆叠起来,就得到了一个3x3xN的输出张量。
卷积层参数总量计算
一个卷积层的参数总量(即需要学习的权重和偏置数量)由以下因素决定:
总参数量 = (卷积核高度 × 卷积核宽度 × 输入通道数 × 输出通道数) + 输出通道数 (偏置)
在我们的例子中,如果输入是 5x5x3,我们想得到 N 个输出通道,使用 3x3 的卷积核,那么参数总量为:
(3 × 3 × 3 × N) + N = 27N + N = 28N 个参数。
PyTorch卷积层API
核心 API:torch.nn.Conv2d
这是最常用的接口,继承自 nn.Module。你需要先在 __init__ 中实例化它,然后在 forward 中调用它。
常用参数详解
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
in_channels |
int | - | 输入通道数。例如 RGB 图像为 3,灰度图为 1。 |
out_channels |
int | - | 输出通道数(即卷积核的个数)。决定了输出特征图的深度。 |
kernel_size |
int 或 tuple | - | 卷积核大小。3 代表 3x3,(3, 5) 代表高3宽5。 |
stride |
int 或 tuple | 1 | 步长。控制卷积核滑动的距离。步长越大,输出尺寸越小。 |
padding |
int 或 tuple | 0 | 填充。在输入四周补零。常用 padding=1 配合 kernel_size=3 保持尺寸不变。 |
bias |
bool | True | 是否添加偏置项。 |
dilation |
int 或 tuple | 1 | 空洞率。用于空洞卷积,控制卷积核点的间距(例如 2 代表中间隔一个像素)。 |
groups |
int | 1 | 分组卷积。控制输入输出通道的连接方式。groups=in_channels 时为深度可分离卷积。 |
代码示例
python
import torch
import torch.nn as nn
# 1. 定义卷积层
# 输入3通道,输出16通道,卷积核3x3,步长1,填充1(保持尺寸不变)
conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
# 2. 准备输入数据 (Batch_Size, Channels, Height, Width)
# 假设输入是一张 32x32 的 RGB 图片,批次大小为 1
input_tensor = torch.randn(1, 3, 32, 32)
# 3. 执行前向传播
output = conv_layer(input_tensor)
print(f"输入尺寸: {input_tensor.shape}") # torch.Size([1, 3, 32, 32])
print(f"输出尺寸: {output.shape}") # torch.Size([1, 16, 32, 32])
池化层计算
池化层(Pooling Layer)的计算逻辑比卷积层简单得多。它的核心思想是"降维 "和"统计":通过固定大小的窗口在特征图上滑动,对窗口内的数值进行统计计算(取最大值或平均值),从而减小特征图的空间尺寸。
与卷积层不同,池化层没有需要学习的参数(权重和偏置),它只是一个固定的数学运算过程。
核心计算公式
池化层输出尺寸的计算公式与卷积层非常相似。假设输入尺寸为 Hin×WinH_{in}×W_{in}Hin×Win ,输出尺寸 Hout×WoutH_{out}×W_{out}Hout×Wout 的计算公式为:
Hout=⌊Hin+2×padding−dilation×(kernel_size−1)−1stride+1⌋H_{out}=⌊\frac{H_{in}+2×padding−dilation×(kernel\_size−1)−1}{stride}+1⌋Hout=⌊strideHin+2×padding−dilation×(kernel_size−1)−1+1⌋
- kernel_size: 池化窗口大小(如 2x2)。
- stride: 滑动步长(通常等于 kernel_size)。
- padding: 填充大小。
- dilation: 空洞大小(通常为 1)。
- ⌊⋅⌋⌊⋅⌋ : 向下取整符号(Floor)。
详细计算步骤(以最大池化为例)
假设我们有一个 4x4 的输入特征图,使用 2x2 的池化核,步长为 2 ,无填充。
输入矩阵:
tex
[1, 3, 2, 4]
[5, 6, 8, 7]
[4, 2, 1, 0]
[9, 7, 3, 2]
1. 划分窗口
池化核从左上角开始,覆盖 2x2 的区域。由于步长为 2,窗口之间不会重叠。
左上窗口:覆盖
tex
[1, 3]
[5, 6]
右上窗口:覆盖
tex
[2, 4]
[8, 7]
左下窗口:覆盖
tex
[4, 2]
[9, 7]
右下窗口:覆盖
tex
[1, 0]
[3, 2]
2.执行运算
根据池化类型(最大池化或平均池化)对每个窗口内的数值进行计算。
- 最大池化 (Max Pooling) :取窗口内的最大值 。
- 左上: max(1,3,5,6)=6max(1,3,5,6)=6max(1,3,5,6)=6
- 右上: max(2,4,8,7)=8max(2,4,8,7)=8max(2,4,8,7)=8
- 左下: max(4,2,9,7)=9max(4,2,9,7)=9max(4,2,9,7)=9
- 右下: max(1,0,3,2)=3max(1,0,3,2)=3max(1,0,3,2)=3
- 平均池化 (Average Pooling) :取窗口内的平均值 。
- 左上: (1+3+5+6)/4=3.75(1+3+5+6)/4=3.75(1+3+5+6)/4=3.75
- 右上: (2+4+8+7)/4=5.25(2+4+8+7)/4=5.25(2+4+8+7)/4=5.25
- ...以此类推。
3. 生成输出
将计算结果组合成新的特征图。对于上述最大池化,输出为 2x2 的矩阵:
tex
[6, 8]
[9, 3]
多通道池化层计算
在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着池化层的输出和输入的通道数是相等。
常见池化类型与计算特点
| 类型 | 计算逻辑 | 特点与应用 |
|---|---|---|
| 最大池化 (Max Pooling) | 取窗口内的最大值 | 保留最显著特征(如边缘、纹理)。抗噪能力强,是最常用的池化方式。 |
| 平均池化 (Average Pooling) | 取窗口内的平均值 | 保留整体背景信息,特征图更平滑。常用于网络末端(如全局平均池化)。 |
| 全局池化 (Global Pooling) | 窗口大小 = 特征图大小 | 将整个 H×WH×WH×W 压缩为 1×11×11×1 。常用于替代全连接层,防止过拟合。 |
PyTorch 代码
在 PyTorch 中,我们通常使用 nn.MaxPool2d 或 nn.AvgPool2d。
python
import torch
import torch.nn as nn
# 1. 定义输入 (1, 1, 4, 4) -> (Batch, Channel, Height, Width)
input_tensor = torch.tensor([[[[1., 3, 2, 4],
[5, 6, 8, 7],
[4, 2, 1, 0],
[9, 7, 3, 2]]]])
# 2. 定义最大池化层 (2x2 窗口, 步长 2)
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 3. 计算
output = max_pool(input_tensor)
print("输入尺寸:", input_tensor.shape) # torch.Size([1, 1, 4, 4])
print("输出尺寸:", output.shape) # torch.Size([1, 1, 2, 2])
print("输出结果:\n", output)
# 输出结果: tensor([[[[6., 8.], [9., 3.]]]])
进阶:自适应池化 (Adaptive Pooling)
有时候我们不想手动计算 kernel_size 和 stride,而是希望指定输出尺寸 (例如无论输入多大,输出都必须是 7x7)。这时可以使用 自适应池化 (如 nn.AdaptiveAvgPool2d)。
- 计算逻辑 :库函数会自动根据输入尺寸和期望的输出尺寸,反推合适的
kernel_size和stride。 - 应用场景:处理不同分辨率的输入图像,或者在 CNN 末端替代全连接层。
python
# 无论输入是 32x32 还是 64x64,输出强制变为 1x1
global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
CNN的主要应用
- 计算机视觉:图像分类、目标检测(如YOLO)、语义分割、人脸识别、图像生成等。
- 自然语言处理:文本分类、情感分析等,通过一维卷积提取文本的局部语义特征。
- 语音信号处理:语音识别,通常将音频信号转换为语谱图后,使用二维卷积进行处理。
- 时序数据分析:金融预测、工业故障诊断等。
CNN的优势与局限
- 优势
- 参数效率高:局部连接和权值共享使其参数量远小于全连接网络。
- 自动特征提取:无需人工设计特征,能自动学习最适合任务的层次化特征。
- 平移不变性:对目标在图像中的位置变化具有一定的鲁棒性。
- 局限
- 全局建模能力弱:传统CNN的感受野有限,难以捕捉图像中相距较远像素间的长距离依赖关系。
- 对几何变换敏感:对图像的旋转、缩放等非刚性形变泛化能力有限,通常需要大量数据增强来弥补。