机器学习基础(十):卷积神经网络CNN
机器学习基础(十):卷积神经网络CNN
一、引言
在前面的学习中,我们已经掌握了 PyTorch 的基础用法,能够构建简单的全连接神经网络。但当面对图像数据时,我们遇到了新的问题:
一张 224×224 的彩色图像有 150,528 个像素,如果用全连接层处理,参数量会爆炸!
全连接网络的困境:
- 参数过多:导致过拟合风险增加
- 忽略空间结构:将图像展平为一维向量,丢失了像素间的空间关系
- 计算效率低:大量冗余计算
卷积神经网络(Convolutional Neural Network, CNN) 正是为解决这些问题而生。它通过卷积操作提取图像特征,既减少了参数量,又保留了空间信息,是计算机视觉领域的基石。
二、什么是卷积?
1. 卷积的直观理解
卷积是一种特殊的线性运算,它使用一个卷积核(Kernel/Filter)在输入数据上滑动,进行局部加权求和。
类比:用放大镜观察图片
- 卷积核就像放大镜,一次只看图像的一小块区域
- 滑动扫描整幅图像,提取不同位置的特征
2. 卷积的数学定义
对于二维图像,卷积运算定义为:
其中:
- \(I\) 是输入图像
- \(K\) 是卷积核(通常大小为 3×3 或 5×5)
- \((i, j)\) 是输出位置
3. 卷积操作示例
假设有一个 5×5 的输入图像和一个 3×3 的卷积核:
输入图像: 卷积核:
1 1 1 0 0 1 0 1
0 1 1 1 0 0 1 0
0 0 1 1 1 1 0 1
0 0 1 1 0
0 1 1 0 0
卷积过程(步长=1,无填充):
- 将卷积核放在图像左上角
- 对应元素相乘后求和
- 向右滑动,重复计算
- 到达边界后向下滑动一行
输出特征图大小计算:
三、CNN 的核心组件
1. 卷积层(Convolutional Layer)
作用:提取图像的局部特征(如边缘、纹理、形状)
关键参数:
| 参数 | 说明 | 常用值 |
|---|---|---|
kernel_size |
卷积核大小 | 3×3, 5×5, 7×7 |
stride |
步长(滑动间隔) | 1, 2 |
padding |
边缘填充 | 0, 1, 'same' |
channels |
输出通道数(卷积核数量) | 32, 64, 128, 256 |
PyTorch 实现:
import torch.nn as nn
# 输入通道=3(RGB),输出通道=64,卷积核=3×3
conv_layer = nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
stride=1,
padding=1
)
2. 激活函数
卷积后通常接激活函数引入非线性:
# ReLU 是最常用的选择
relu = nn.ReLU()
# 卷积 + 激活
output = relu(conv_layer(input))
3. 池化层(Pooling Layer)
作用:降低特征图尺寸,减少计算量,增强平移不变性
类型:
| 类型 | 操作 | 特点 |
|---|---|---|
| Max Pooling | 取窗口内最大值 | 保留最强特征,最常用 |
| Average Pooling | 取窗口内平均值 | 保留背景信息 |
PyTorch 实现:
# 2×2 最大池化,步长=2
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 2×2 平均池化
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
池化效果:特征图尺寸减半(2×2 池化 + 步长2)
4. 批归一化(Batch Normalization)
作用:加速训练收敛,提高模型稳定性,减少对初始化的敏感
# 对 64 通道的特征图进行批归一化
bn = nn.BatchNorm2d(num_features=64)
5. 全连接层(Fully Connected Layer)
作用:将卷积层提取的特征映射到最终的分类结果
通常位于网络末端,前面需要将特征图展平:
# 假设经过卷积和池化后,特征图为 [batch, 512, 7, 7]
flatten = nn.Flatten() # 展平为 [batch, 512*7*7]
fc = nn.Linear(512*7*7, num_classes) # 映射到类别数
四、经典 CNN 架构
1. LeNet-5(1998)
开创者:Yann LeCun
结构:卷积 → 池化 → 卷积 → 池化 → 全连接
特点:
- 首个成功的 CNN 架构
- 用于手写数字识别(MNIST)
- 奠定了 CNN 的基本范式
2. AlexNet(2012)
突破:ImageNet 竞赛冠军,开启深度学习时代
创新点:
- 使用 ReLU 激活函数
- 引入 Dropout 防止过拟合
- 使用 GPU 加速训练
结构:5 层卷积 + 3 层全连接
3. VGGNet(2014)
核心思想:使用更小的卷积核(3×3)增加网络深度
特点:
- VGG-16:13 层卷积 + 3 层全连接
- VGG-19:16 层卷积 + 3 层全连接
- 结构规整,易于理解和实现
4. ResNet(2015)
革命性创新:残差连接(Residual Connection)
解决的问题:深层网络训练困难(梯度消失)
核心公式:
特点:
- ResNet-18/34/50/101/152
- 可以训练超过 100 层的网络
- 当前最广泛使用的骨干网络
五、CNN 的训练技巧
1. 数据增强(Data Augmentation)
通过对训练图像进行随机变换,增加数据多样性:
from torchvision import transforms
# 定义数据增强
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(10), # 随机旋转
transforms.ColorJitter(brightness=0.2, # 颜色抖动
contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
2. 学习率调度(Learning Rate Scheduling)
学习率调度是训练深度神经网络的关键技巧。为什么需要调整学习率?
直观理解:训练初期需要"大步流星"快速接近最优解,后期需要"小心翼翼"精细调整。
固定学习率的问题:
- 学习率太大 → 震荡不收敛,甚至发散
- 学习率太小 → 收敛极慢,陷入局部最优
常用学习率调度策略
(1)StepLR - 阶梯衰减
每隔固定轮数,学习率乘以衰减系数:
# 每 7 个 epoch,学习率乘以 0.1
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
# 训练循环中
for epoch in range(num_epochs):
train(...)
scheduler.step() # 更新学习率
print(f'当前学习率: {optimizer.param_groups[0]["lr"]}')
效果示例:初始 lr=0.1
- Epoch 1-7: lr = 0.1
- Epoch 8-14: lr = 0.01
- Epoch 15-21: lr = 0.001
(2)MultiStepLR - 多阶段衰减
在指定的里程碑 epoch 进行衰减:
# 在第 30、60、90 轮分别衰减
scheduler = optim.lr_scheduler.MultiStepLR(
optimizer, milestones=[30, 60, 90], gamma=0.1
)
(3)ExponentialLR - 指数衰减
每轮都按指数衰减:
# 每轮学习率乘以 0.95
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
公式:\(lr_{epoch} = lr_{initial} \times \gamma^{epoch}\)
(4)CosineAnnealingLR - 余弦退火 ⭐推荐
学习率按余弦曲线变化,常用于现代深度学习:
# T_max: 半个余弦周期的长度
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200)
公式:\(\eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})(1 + \cos(\frac{T_{cur}}{T_{max}}\pi))\)
特点:
- 平滑下降,避免学习率骤变
- 周期性重启,有助于跳出局部最优
- ResNet、EfficientNet 等现代架构常用
(5)ReduceLROnPlateau - 自适应衰减
当验证指标停止改善时自动降低学习率:
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='max', # 'max' 表示监控指标越大越好(如准确率)
factor=0.1, # 衰减系数
patience=5, # 容忍 5 轮不改善
verbose=True
)
# 训练循环中
for epoch in range(num_epochs):
train(...)
val_acc = validate(...)
scheduler.step(val_acc) # 传入监控指标
这是最实用的策略之一,无需预设衰减时机,自动适应训练情况。
(6)OneCycleLR - 单周期策略 ⭐⭐强烈推荐
先线性升温到最大学习率,再余弦降温到最小学习率:
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1, # 最大学习率
total_steps=num_epochs * len(train_loader), # 总步数
pct_start=0.3, # 升温阶段占比
anneal_strategy='cos' # 降温策略
)
# 注意:每个 batch 都要 step()
for epoch in range(num_epochs):
for batch in train_loader:
train_batch(...)
scheduler.step()
优势:
- 训练速度更快
- 通常能达到更好的最终性能
- Fast.ai 库的核心技巧
学习率调度选择建议
| 场景 | 推荐策略 |
|---|---|
| 快速实验 | StepLR、MultiStepLR |
| 追求最佳性能 | OneCycleLR、CosineAnnealingLR |
| 不想调参 | ReduceLROnPlateau |
| 训练不稳定 | 先用小学习率 + 线性升温 |
关键经验:
- 学习率通常是最重要的超参数
- 好的学习率调度可以让模型提升 1-3% 的准确率
- 建议配合 学习率预热(Warmup) 使用,前几个 epoch 从小学习率逐渐增大
3. 早停(Early Stopping)
当验证集性能不再提升时停止训练,防止过拟合:
best_acc = 0.0
patience = 5
counter = 0
for epoch in range(num_epochs):
train(...)
val_acc = validate(...)
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
counter = 0
else:
counter += 1
if counter >= patience:
print(f'早停于 epoch {epoch}')
break
六、代码实战:MNIST 手写数字识别
下面是一个完整的 MNIST 手写数字识别示例,使用简单的 CNN 架构:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import time
# 设置设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# ==================== 1. 数据准备 ====================
# MNIST 数据集:手写数字 0-9,28x28 灰度图像
transform = transforms.Compose([
transforms.ToTensor(), # 转换为 Tensor,归一化到 [0, 1]
transforms.Normalize((0.1307,), (0.3081,)) # MNIST 标准归一化参数
])
# 加载数据集
train_dataset = torchvision.datasets.MNIST(
root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.MNIST(
root='./data', train=False, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=64, shuffle=False)
print(f"训练集大小: {len(train_dataset)}")
print(f"测试集大小: {len(test_dataset)}")
# ==================== 2. 定义 CNN 模型 ====================
class CNN(nn.Module):
"""简单的 CNN 用于 MNIST 手写数字识别"""
def __init__(self):
super(CNN, self).__init__()
# 卷积层 1: 1@28x28 -> 32@28x28 -> 32@14x14 (池化后)
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(32)
# 卷积层 2: 32@14x14 -> 64@14x14 -> 64@7x7 (池化后)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
# 池化层
self.pool = nn.MaxPool2d(2, 2)
self.dropout = nn.Dropout(0.25)
# 全连接层: 64@7x7 = 3136 -> 128 -> 10
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)
self.relu = nn.ReLU()
def forward(self, x):
# 卷积块 1
x = self.relu(self.bn1(self.conv1(x))) # [batch, 32, 28, 28]
x = self.pool(x) # [batch, 32, 14, 14]
# 卷积块 2
x = self.relu(self.bn2(self.conv2(x))) # [batch, 64, 14, 14]
x = self.pool(x) # [batch, 64, 7, 7]
# 展平
x = x.view(x.size(0), -1) # [batch, 3136]
x = self.dropout(x)
# 全连接
x = self.relu(self.fc1(x)) # [batch, 128]
x = self.fc2(x) # [batch, 10]
return x
# ==================== 3. 训练 ====================
model = CNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 统计信息
train_losses = []
train_accuracies = []
test_accuracies = []
# 训练函数
def train(epoch):
model.train()
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (inputs, targets) in enumerate(train_loader):
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
epoch_loss = running_loss / len(train_loader)
epoch_acc = 100. * correct / total
train_losses.append(epoch_loss)
train_accuracies.append(epoch_acc)
print(f'Epoch {epoch}: 训练损失: {epoch_loss:.4f}, 训练准确率: {epoch_acc:.2f}%')
# 测试函数
def test():
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, targets in test_loader:
inputs, targets = inputs.to(device), targets.to(device)
outputs = model(inputs)
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
acc = 100. * correct / total
test_accuracies.append(acc)
print(f'测试准确率: {acc:.2f}%')
return acc
# 开始训练
print("\n开始训练...")
start_time = time.time()
num_epochs = 5
for epoch in range(1, num_epochs + 1):
train(epoch)
test()
end_time = time.time()
training_time = end_time - start_time
print(f'\n训练完成!总耗时: {training_time:.2f}秒')
print(f'最终测试准确率: {test_accuracies[-1]:.2f}%')
训练结果
在 NVIDIA GPU 上训练 5 个 epoch 的典型结果:
| Epoch | 训练损失 | 训练准确率 | 测试准确率 |
|---|---|---|---|
| 1 | 0.2156 | 93.45% | 98.20% |
| 2 | 0.0689 | 97.89% | 98.85% |
| 3 | 0.0512 | 98.42% | 99.05% |
| 4 | 0.0418 | 98.71% | 99.15% |
| 5 | 0.0356 | 98.89% | 99.20% |
关键结论:
- ✅ 仅需 5 轮训练,测试准确率即可达到 99% 以上
- ✅ 训练时间约 30-60 秒(GPU)或 2-3 分钟(CPU)
- ✅ CNN 对 MNIST 这种简单图像分类任务非常高效
- ✅ 模型参数量约 50 万,远小于全连接网络
七、CNN 的优缺点
优点
| 优点 | 说明 |
|---|---|
| 参数共享 | 卷积核在整个图像上共享,大幅减少参数量 |
| 局部连接 | 每个神经元只连接局部区域,提取局部特征 |
| 平移不变性 | 池化操作使网络对位置变化具有一定鲁棒性 |
| 层次化特征 | 浅层提取边缘纹理,深层提取语义特征 |
| 端到端学习 | 自动学习特征,无需手工设计 |
缺点
| 缺点 | 说明 |
|---|---|
| 需要大量数据 | 深层网络需要大量标注数据训练 |
| 计算资源需求高 | 训练需要 GPU 加速 |
| 可解释性较弱 | 深层特征难以直观理解 |
| 对旋转缩放敏感 | 需要数据增强来提高鲁棒性 |
八、应用场景
CNN 在计算机视觉领域无处不在:
- 图像分类:识别图像中的物体类别(ImageNet)
- 目标检测:定位并识别图像中的多个物体(YOLO、Faster R-CNN)
- 语义分割:像素级分类,识别每个像素的类别(U-Net、DeepLab)
- 人脸识别:FaceID、人脸验证系统
- 医学影像:肿瘤检测、病变分割
- 自动驾驶:车道检测、行人识别
九、总结
卷积神经网络是深度学习在计算机视觉领域取得突破的关键:
核心思想:通过卷积操作提取局部特征,通过池化降低维度,通过堆叠层数学习层次化表示。
关键点回顾:
| 要点 | 内容 |
|---|---|
| 卷积层 | 提取局部特征,参数共享 |
| 池化层 | 降维,增强平移不变性 |
| 激活函数 | 引入非线性(ReLU) |
| 批归一化 | 加速训练,提高稳定性 |
| 残差连接 | 解决深层网络训练困难 |
从 LeNet 到 ResNet,CNN 架构不断演进,但其核心思想——局部感受野、权值共享、层次化特征提取——始终未变。掌握 CNN,你就掌握了计算机视觉的基石。
标签:机器学习, 深度学习, CNN, 卷积神经网络, 计算机视觉, 图像分类, PyTorch, MNIST

浙公网安备 33010602011771号