卷积神经网络 CNN 是专为图像识别和处理设计的一类深度神经网络,它可以有效地捕捉图像中的空间结构信息 ,通过局部感受野、权重共享、下采样 等机制,大大减少了参数数量,同时保留了重要特征。我们用另外一个经典的教学数据集CIFAR-10,来看下CNN的训练过程
先来看下 CIFAR-10 数据集的基本信息:
- • 每条数据是一个 32x32 的彩色图像,每个图像打标了一个类别。
- • 共包含10个类别,60,000张图像(50,000训练 + 10,000测试)
在 Jupyter notebook 中,可以用如下代码,抽样查看数据集图片:
import matplotlib.pyplot as plt
import numpy as np
。。。
# 获取5个样本
samples = []
classes = train\_set.classes # CIFAR10的类别名称
for i in range(5):
# 直接从训练集中获取样本 (会自动应用数据增强)
image, label = train\_set[i]
samples.append((image, label))
# 创建可视化
fig, axes = plt.subplots(1, 5, figsize=(15, 3))
for i, (image, label) in enumerate(samples):
# 显示图像
ax = axes[i]
ax.imshow(image)
ax.set\_title(classes[label])
ax.axis('off')
plt.tight\_layout()
plt.show()
其中的 train_set 是之前代码加载好的数据集对象。执行后可以看到如下输出:
这里是这个数据集的主页 : http://www.cs.toronto.edu/~kriz/cifar.html 里面有更详细的描述
我们用上一篇《深度学习模型训练的一般过程》中介绍的多层感知机MLP模型对这个数据集进行训练,结果如下:
Epoch: 5 [0/50000 (0%)] Loss: 1.601727
Epoch: 5 [12800/50000 (26%)] Loss: 1.473466
Epoch: 5 [25600/50000 (51%)] Loss: 1.570823
Epoch: 5 [38400/50000 (77%)] Loss: 1.668174
测试集: 平均损失: 0.0182, 准确率: 4358/10000 (43.58%)
可见图像复杂之后,MLP模型最后的准确率就不高了。我们接下来看看CNN模型在这个数据集上的表现怎么样。
下面代码就是一个简单的 CNN 模型训练过程:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 设置随机种子确保可复现性
torch.manual\_seed(42)
# 检查GPU可用性
device = torch.device('cuda' if torch.cuda.is\_available() else 'cpu')
print(f"使用设备: {device}")
# 数据预处理和增强
transform\_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
transform\_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 下载并加载数据集
train\_set = torchvision.datasets.CIFAR10(
root='./data',
train=True,
download=True,
transform=transform\_train
)
test\_set = torchvision.datasets.CIFAR10(
root='./data',
train=False,
download=True,
transform=transform\_test
)
train\_loader = DataLoader(train\_set, batch\_size=128, shuffle=True, num\_workers=2)
test\_loader = DataLoader(test\_set, batch\_size=100, shuffle=False, num\_workers=2)
# 定义CNN模型类,继承自nn.Module
class CNN(nn.Module):
# 初始化函数,定义网络层
def \_\_init\_\_(self):
super(CNN, self).\_\_init\_\_() # 调用父类nn.Module的初始化方法
# 卷积层部分 - 使用Sequential容器按顺序组织各层
self.conv\_layers = nn.Sequential(
# 第一个卷积块
nn.Conv2d(3, 32, 3, padding=1), # 输入通道3(RGB), 输出通道32, 3x3卷积核, padding=1保持尺寸
nn.ReLU(), # 激活函数引入非线性
nn.Conv2d(32, 32, 3, padding=1), # 再次卷积,加深特征提取
nn.ReLU(),
nn.MaxPool2d(2, 2), # 2x2最大池化,尺寸减半(32x32 -> 16x16)
nn.Dropout(0.25), # 随机丢弃25%神经元,防止过拟合
# 第二个卷积块
nn.Conv2d(32, 64, 3, padding=1), # 增加通道数到64
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 尺寸再减半(16x16 -> 8x8)
nn.Dropout(0.25),
# 第三个卷积块
nn.Conv2d(64, 128, 3, padding=1), # 增加通道数到128
nn.ReLU(),
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 尺寸再减半(8x8 -> 4x4)
nn.Dropout(0.25)
)
# 全连接层部分
self.fc\_layers = nn.Sequential(
nn.Flatten(), # 将多维特征图展平为一维向量 (128 * 4 * 4 = 2048)
nn.Linear(128 * 4 * 4, 1024), # 全连接层,2048输入 -> 1024输出
nn.ReLU(),
nn.Dropout(0.5), # 更高的丢弃率,防止过拟合
nn.Linear(1024, 10) # 最终输出层,10个类别对应CIFAR-10
)
# 前向传播函数,定义数据如何通过网络
def forward(self, x):
x = self.conv\_layers(x) # 数据通过卷积层
x = self.fc\_layers(x) # 再通过全连接层
return x
# 创建模型实例并移动到设备(GPU/CPU)
model = CNN().to(device)
# 定义损失函数 - 交叉熵损失,适用于多分类问题
criterion = nn.CrossEntropyLoss()
# 定义优化器 - Adam优化器,学习率0.001,添加L2正则化(weight\_decay)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight\_decay=1e-5)
# 定义学习率调度器 - 当验证损失不再下降时降低学习率
scheduler = optim.lr\_scheduler.ReduceLROnPlateau(
optimizer,
'min', # 监控验证损失最小值
patience=3, # 等待3个epoch无改善
factor=0.5 # 学习率减半
)
# 训练函数
def train(epoch):
model.train() # 设置模型为训练模式(启用dropout等)
running\_loss = 0.0 # 累计损失
# 遍历训练集所有批次
for i, (inputs, labels) in enumerate(train\_loader):
# 将数据移动到设备(GPU/CPU)
inputs, labels = inputs.to(device), labels.to(device)
# 梯度清零 - 防止梯度累积
optimizer.zero\_grad()
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播计算梯度
loss.backward()
# 更新权重
optimizer.step()
# 累计损失
running\_loss += loss.item()
# 每100个batch打印一次损失
if i % 100 == 99:
print(f'Epoch: {epoch+1}, Batch: {i+1}, Loss: {running\_loss/100:.3f}')
running\_loss = 0.0 # 重置累计损失
return loss.item() # 返回最后一个batch的损失
# 测试函数
def test():
model.eval() # 设置模型为评估模式(禁用dropout等)
correct = 0 # 正确预测数
total = 0 # 总样本数
test\_loss = 0.0 # 测试损失
# 禁用梯度计算,节省内存和计算资源
with torch.no\_grad():
# 遍历测试集所有批次
for inputs, labels in test\_loader:
inputs, labels = inputs.to(device), labels.to(device)
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
test\_loss += loss.item()
# 获取预测结果(最大概率的类别)
\_, predicted = outputs.max(1)
# 统计正确预测数
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 计算准确率
accuracy = 100. * correct / total
# 打印测试结果
print(f'测试集平均损失: {test\_loss/len(test\_loader):.4f}, 准确率: {accuracy:.2f}%')
return test\_loss/len(test\_loader), accuracy # 返回平均损失和准确率
# 主训练循环
best\_accuracy = 0.0 # 记录最佳准确率
# 遍历所有epoch
for epoch in range(30):
print(f"\n开始训练第 {epoch+1} 个epoch...")
# 训练一个epoch
train\_loss = train(epoch)
# 在测试集上评估
test\_loss, accuracy = test()
# 根据测试损失调整学习率
scheduler.step(test\_loss)
# 如果当前准确率优于历史最佳,保存模型
if accuracy > best\_accuracy:
best\_accuracy = accuracy
# 保存模型权重
torch.save(model.state\_dict(), 'best\_model.pth')
print(f"保存新的最佳模型,准确率: {best\_accuracy:.2f}%")
# 训练结束
print(f"\n训练完成!最佳测试准确率: {best\_accuracy:.2f}%")
将如上代码保存为 cnn.py ,在上一篇《深度学习模型训练的一般过程》中提到的容器环境中执行:
root@cb82d3b9af0a:/workspace# python cnn.py
使用设备: cuda
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 170M/170M [03:20<00:00, 852kB/s]
Extracting ./data/cifar-10-python.tar.gz to ./data
Files already downloaded and verified
开始训练第 1 个epoch...
Epoch: 1, Batch: 100, Loss: 2.112
Epoch: 1, Batch: 200, Loss: 1.866
Epoch: 1, Batch: 300, Loss: 1.676
测试集平均损失: 1.4287, 准确率: 45.92%
保存新的最佳模型,准确率: 45.92%
...
开始训练第 30 个epoch...
Epoch: 30, Batch: 100, Loss: 0.603
Epoch: 30, Batch: 200, Loss: 0.605
Epoch: 30, Batch: 300, Loss: 0.611
测试集平均损失: 0.5210, 准确率: 82.82%
保存新的最佳模型,准确率: 82.82%
训练完成!最佳测试准确率: 82.82%
可以看到,结果准确率比 MLP 模型的 43.58% 有了大幅提升。
期间的资源消耗:
+-----------------------------------------+----------------------+----------------------+
| 4 NVIDIA GeForce RTX 4090 On | 00000000:81:00.0 Off | Off |
| 30% 35C P2 70W / 450W | 696MiB / 24564MiB | 10% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
因数据集不大,显存和算力用得都不多。我们来看看发生了什么,首先看下模型结构
上面代码中定义了模型结构的片段如下:
class CNN(nn.Module):
# 初始化函数,定义网络层
def \_\_init\_\_(self):
super(CNN, self).\_\_init\_\_() # 调用父类nn.Module的初始化方法
# 卷积层部分 - 使用Sequential容器按顺序组织各层
self.conv\_layers = nn.Sequential(
# 第一个卷积块
nn.Conv2d(3, 32, 3, padding=1), # 输入通道3(RGB), 输出通道32, 3x3卷积核, padding=1保持尺寸
nn.ReLU(), # 激活函数引入非线性
nn.Conv2d(32, 32, 3, padding=1), # 再次卷积,加深特征提取
nn.ReLU(),
nn.MaxPool2d(2, 2), # 2x2最大池化,尺寸减半(32x32 -> 16x16)
nn.Dropout(0.25), # 随机丢弃25%神经元,防止过拟合
# 第二个卷积块
nn.Conv2d(32, 64, 3, padding=1), # 增加通道数到64
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 尺寸再减半(16x16 -> 8x8)
nn.Dropout(0.25),
# 第三个卷积块
nn.Conv2d(64, 128, 3, padding=1), # 增加通道数到128
nn.ReLU(),
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 尺寸再减半(8x8 -> 4x4)
nn.Dropout(0.25)
)
# 全连接层部分
self.fc\_layers = nn.Sequential(
nn.Flatten(), # 将多维特征图展平为一维向量 (128 * 4 * 4 = 2048)
nn.Linear(128 * 4 * 4, 1024), # 全连接层,2048输入 -> 1024输出
nn.ReLU(),
nn.Dropout(0.5), # 更高的丢弃率,防止过拟合
nn.Linear(1024, 10) # 最终输出层,10个类别对应CIFAR-10
)
# 前向传播函数,定义数据如何通过网络
def forward(self, x):
x = self.conv\_layers(x) # 数据通过卷积层
x = self.fc\_layers(x) # 再通过全连接层
return x
这段代码定义的 CNN 网络由两个主要部分构成:
卷积层部分(self.conv_layers)用于从图像中提取局部空间特征。
- • 卷积层 nn.Conv2d(in_channels, out_channels, kernel_size, padding)
- • 扫描图像的局部区域,提取边缘、纹理、形状等低/高层次特征。
- • padding=1 表示边缘补零,使输出大小与输入相同(便于尺寸控制)。
- • 激活函数 nn.ReLU(),引入非线性,帮助模型学习更复杂的函数映射。
- • 池化层 nn.MaxPool2d(kernel_size, stride)
- • 减少特征图尺寸(降低计算量),保留最显著特征。
- • 2x2 最大池化意味着每 2x2 区域保留最大值 → 尺寸减半。
- • Dropout 层 nn.Dropout(p) 用于训练时随机丢弃一部分神经元,防止过拟合。
结构总结如下:
| 卷积块 | 输入尺寸 | 输出通道 | 空间尺寸变化 | | Block 1 | 3x32x32 → Conv2d(3→32) → Conv2d(32→32) | 32 | 32x32 → 16x16 | | Block 2 | Conv2d(32→64) → Conv2d(64→64) | 64 | 16x16 → 8x8 | | Block 3 | Conv2d(64→128) → Conv2d(128→128) | 128 | 8x8 → 4x4 |
全连接层部分("self.fc_layers"):用于将提取的高维特征映射为分类结果。
- • Flatten 层:将形状为 "[batch_size, 128, 4, 4]" 的张量拉平成 "[batch_size, 2048]",供全连接层使用。
- • 全连接层 "nn.Linear(in, out)":将扁平向量映射到新的空间,模拟传统神经网络的分类过程。
- • 最后一层 "nn.Linear(1024, 10)":输出一个大小为 10 的向量,对应 CIFAR-10 数据集的 10 个类别。
前向传播过程 "forward"
def forward(self, x):
x = self.conv\_layers(x) # 特征提取
x = self.fc\_layers(x) # 分类预测
return x
- • 输入图像:"[batch_size, 3, 32, 32]"
- • 输出:"[batch_size, 10]",每个元素是一个类别得分(通常接 softmax)
以上代码中 nn.Conv2d(3, 32, 3, padding=1) 是一个标准的PyTorch 2D卷积层定义,其作用是在神经网络中创建特征提取器:
nn.Conv2d(
in\_channels=3, # 输入通道数 (RGB图像)
out\_channels=32, # 输出通道数/卷积核数量
kernel\_size=3, # 卷积核尺寸 3×3
padding=1, # 边界填充量
)
这行代码对应的模型权重,是 32 个“3×3×3” 的卷积核张量,也即3个输入通道RGB,每个通道对应一个3×3的卷积核。每个输出通道对应一个这样的卷积核组。
- • 输出通道 0 使用的权重是一个 [3×3×3] 张量(3 个通道,每个是 3×3 的卷积核)
- • 输出通道 1 再使用另一个 [3×3×3] 卷积核组
- • …
- • 输出通道 31 用的是第 32 个 [3×3×3] 卷积核组
对于单个通道来说,假设你有:
- • 输入图像 "X ∈ ℝ^{C×H×W}"
- • 卷积核 "K ∈ ℝ^{C×k×k}",其中 C 是通道数,k 是核大小(3)
则第 m 个输出通道的某个像素点 (i, j) 的计算公式为:
其中:
- •
是输出通道
对输入通道
的卷积核(在改例中是3*3的矩阵)
- •
是偏置项
这个公式的含义就是,对每个输入通道C(如该例中的RGB), 用一个 3×3 的矩阵(
),对输入中通道C对应的数据 H×W(该例中是 32*32 图像)做扫描,和扫描到的图像子矩阵做点积。所有输入通道的点积结果之和,加上一个偏置,作为结果特征图的一个像素点。考虑到边缘像素的公平性,计算前在图像周围加上一圈 padding; 计算过程如下图示意:(下图简化了输入和输出维度大小)
经过多层次这样的卷积扫描(以及ReLU,池化等),通过在训练中反复的前向传播,损失计算,反向传播,梯度下降的迭代,试图让模型学习到不同层次的图像特征。为图像分类判断提供依据。这个过程,就像模拟人眼在看到图像时,先扫描图像识别局部特性,再抽象整体概念的过程。
CNN 最神奇的地方是 :不同的卷积层会学习到图像不同角度的特征。比如第一个卷积层,往往会学习到图像的边缘,纹理等特征。例如这个例子中,第一张图片,前6个通道,学习到的权重,生成的数据,可视化后如下:
只是设计好了网络结构,通过数据样本反复学习,权重在网络结构不同层次的分布就会呈现出这样有规律的特点。这大概就是复杂系统科学中所指的涌现吧
是哪个天才设计出了这样的神经网络结构呢?正是下面这位大佬:
图灵奖得主,AI科研三巨头之一,前 Meta(前Facebook)AI 掌门人:杨立昆(Yann André Le Cun),也被称为CNN之父。
相比MLP这样的全连接层网络,CNN 具备如下优势:
- • 权重共享 : 同一个卷积核在整张图上滑动,大幅减少参数量。
- • 局部连接 :卷积核只处理局部区域,有效提取局部特征。
- • 层级特征抽取 : 越往后抽象层级越高(从边缘到复杂结构)。
- • 可泛化性强 :适合各种图像分类、目标检测、语义分割任务。
CNN的应用场景
-
• 人脸识别
-
• 医疗影像诊断
-
• 安全监控中的目标检测
-
• 自动驾驶中的道路识别