共计 3972 个字符,预计需要花费 10 分钟才能阅读完成。
为什么需要 CNN?
在计算机视觉任务中,传统的全连接神经网络面临两个主要问题:

- 参数爆炸:对于一张 100×100 像素的 RGB 图片,输入层就需要 3 万个节点,全连接层的参数量会变得极其庞大。
- 局部特征丢失:全连接网络无法有效利用图像中相邻像素之间的空间关系。
CNN 通过两种核心思想解决这些问题:
- 局部感受野:每个神经元只连接输入图像的局部区域
- 参数共享:同一组卷积核在整个图像上滑动使用
CNN 三大核心组件
1. 卷积层(Convolutional Layer)
卷积层是 CNN 的核心,通过卷积核(filter)在输入数据上滑动计算特征响应。举个例子:
- 一个 3 ×3 的卷积核在图像上滑动,计算每个位置的加权和
- 多个卷积核可以提取不同类型的特征(边缘、纹理等)
- 输出特征图的尺寸由输入大小、卷积核大小、步长 (stride) 和填充 (padding) 共同决定
2. 池化层(Pooling Layer)
池化层主要用于降维和保持平移不变性,常见的有:
- 最大池化(Max Pooling):取窗口内的最大值
- 平均池化(Average Pooling):取窗口内的平均值
池化层能有效减少计算量,同时使网络对小的平移变化更加鲁棒。
3. 全连接层(Fully Connected Layer)
全连接层通常位于网络最后,将学到的特征映射到样本的标记空间。在 CNN 中:
- 前面的卷积层和池化层提取低级到高级的特征
- 全连接层将这些特征组合起来进行分类决策
PyTorch 实战:手写数字分类
数据准备
我们使用经典的 MNIST 数据集,包含 0 - 9 的手写数字图片:
import torch
from torchvision import datasets, transforms
# 定义数据预处理
transform = transforms.Compose([transforms.ToTensor(), # 转为 Tensor 并归一化到[0,1]
transforms.Normalize((0.1307,), (0.3081,)) # MNIST 的均值标准差
])
# 加载数据集
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, 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=1000, shuffle=True)
网络定义
下面定义一个简单的 CNN 网络结构:
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
# 卷积层 1:输入 1 通道,输出 10 通道,卷积核 5x5
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
# 卷积层 2:输入 10 通道,输出 20 通道,卷积核 5x5
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
# Dropout 层
self.dropout = nn.Dropout2d()
# 全连接层 1:320 输入,50 输出
self.fc1 = nn.Linear(320, 50)
# 全连接层 2:50 输入,10 输出(对应 10 个类别)self.fc2 = nn.Linear(50, 10)
def forward(self, x):
# 第一层卷积 + ReLU + 最大池化
x = F.relu(F.max_pool2d(self.conv1(x), 2))
# 第二层卷积 + ReLU + 最大池化 + Dropout
x = F.relu(F.max_pool2d(self.dropout(self.conv2(x)), 2))
# 展平特征图
x = x.view(-1, 320)
# 全连接层 1 + ReLU
x = F.relu(self.fc1(x))
# 应用 Dropout
x = F.dropout(x, training=self.training)
# 全连接层 2 + LogSoftmax
x = self.fc2(x)
return F.log_softmax(x, dim=1)
训练过程
定义好网络后,我们需要设置优化器和损失函数,然后编写训练循环:
def train(model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}'
f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')
# 初始化模型和优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 训练 10 个 epoch
for epoch in range(1, 11):
train(model, device, train_loader, optimizer, epoch)
模型评估
训练完成后,我们需要评估模型在测试集上的表现:
def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
print(f'\nTest set: Average loss: {test_loss:.4f},'
f'Accuracy: {correct}/{len(test_loader.dataset)}'
f'({100. * correct / len(test_loader.dataset):.0f}%)\n')
# 评估模型
test(model, device, test_loader)
常见问题与优化
过拟合问题
过拟合表现为训练集表现很好但测试集表现差,常用解决方法:
- 数据增强:在训练时对图像进行随机变换
transform_train = transforms.Compose([transforms.RandomRotation(10), transforms.RandomAffine(0, translate=(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) - Dropout:随机丢弃一部分神经元
- L2 正则化:在优化器中设置 weight_decay 参数
模型调优技巧
- 学习率调整:使用学习率调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) - 批量归一化:在卷积层后添加 BatchNorm 层
- 更先进的架构:尝试 ResNet、EfficientNet 等现代架构
避坑指南
新手常犯的错误:
- 忘记调用
model.train()和model.eval()切换训练 / 评估模式 - 在反向传播前没有清零梯度(
optimizer.zero_grad()) - 输入数据没有归一化导致训练困难
- 卷积后特征图尺寸计算错误
- 全连接层输入尺寸与前面层输出不匹配
延伸思考
掌握了基础的图像分类后,可以思考:
- 如何将这个模型应用到其他视觉任务,如目标检测或语义分割?
- 如果要处理彩色图像,网络需要做哪些调整?
- 当遇到更大的图像(如 224×224)时,网络结构应该如何设计?
学习资源推荐
- PyTorch 官方教程:https://pytorch.org/tutorials/
- CS231n 课程笔记:https://cs231n.github.io/
- 《Deep Learning with PyTorch》书籍
希望这篇教程能帮助你快速入门 CNN 和 PyTorch!在实际项目中,建议从简单模型开始,逐步增加复杂度,并通过实验验证每项改进的效果。
正文完
