动手实现基础CNN:从结构设计到PyTorch代码详解
·
在深度学习的计算机视觉任务中,卷积神经网络(CNN)是最核心的模型之一。本文将带你从零开始设计一个经典的CNN结构,并用PyTorch完整实现。通过一个简单的图像分类任务,你会清楚地理解每一层的作用、参数的含义,以及如何调试常见问题。
实验任务概览
本次实验包含两个主要任务:
1. 设计CNN结构:完成从输入层到输出层的网络结构图,并在图中标注每层的参数(特征图尺寸、通道数、卷积核大小等)。
2. PyTorch实现:根据设计的网络结构编写可运行的代码,并确保理解每个函数/参数的含义。
CNN结构设计
我们设计一个适用于灰度图像(如MNIST,尺寸64×64)的二分类或N分类网络。典型结构为:
输入 → 卷积+ReLU → 池化 → 卷积+ReLU → 池化 → 展平 → 全连接+ReLU → 输出(Softmax)
实际上,为了计算清晰且与常见实践一致,采用如下精确设计(与示例图对齐):
| 层名 | 操作 | 输出尺寸 | 参数数量 |
|----------------|--------------------------------|---------------------|-----------------------------------|
| Input | 64×64 灰度图 | (64,64,1) | - |
| Conv1+ReLU | 3×3, 32核, stride=1, padding=0 | (62,62,32) | (3×3×1+1)×32 = 320 |
| Pool1 | MaxPool 2×2, stride=2 | (31,31,32) | - |
| Conv2+ReLU | 3×3, 64核, stride=1, padding=0 | (29,29,64) | (3×3×32+1)×64 = 18496 |
| Pool2 | MaxPool 2×2, stride=2 | (14,14,64) | - |
| Flatten | 展平 | 14×14×64 = 12544 | - |
| FC1+ReLU | 全连接128 | 128 | 12544×128 + 128 = 1,605,760 |
| Output (Softmax) | 全连接N类(如10) | N | 128×N + N |
> 注:示例图中Pool2后得到15×15是因为其在Conv2中使用了padding=1,输出30×30。不同设置不影响原理学习。我们下文代码将采用padding=0的真实计算结果。
## PyTorch代码实现
### 定义网络结构
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super(SimpleCNN, self).__init__()
# 卷积层1: 输入1通道,输出32通道,卷积核3x3
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=0)
# 池化层1: 2x2, stride=2
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 卷积层2: 输入32通道,输出64通道,卷积核3x3
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=0)
# 池化层2: 2x2, stride=2
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层1: 输入特征数需要手动计算,输出128
# 经过两次卷积和池化后的特征图尺寸计算:
# 输入64 -> conv1 -> 62 -> pool1 -> 31 -> conv2 -> 29 -> pool2 -> 14 (向下取整)
self.fc1 = nn.Linear(14 * 14 * 64, 128)
# 输出层
self.fc2 = nn.Linear(128, num_classes)
# 激活函数
self.relu = nn.ReLU()
def forward(self, x):
# 卷积+激活+池化
x = self.pool1(self.relu(self.conv1(x))) # (batch,32,31,31)
x = self.pool2(self.relu(self.conv2(x))) # (batch,64,14,14)
# 展平
x = x.view(x.size(0), -1) # 等价于 torch.flatten(x, start_dim=1)
# 全连接层
x = self.relu(self.fc1(x))
x = self.fc2(x) # 输出层,不加Softmax,因为CrossEntropyLoss自带
return x
关键函数参数详解
| 函数 | 参数 | 含义说明 |
|---|---|---|
nn.Conv2d(in, out, k, s, p) |
in_channels, out_channels, kernel_size, stride, padding | 卷积层,输入/输出通道数,卷积核大小,步长,填充。输出尺寸公式:(H+2p - k)/s +1 |
nn.MaxPool2d(k, s) |
kernel_size, stride | 最大池化,窗口尺寸和步长。默认stride=kernel_size,如果不相等需显式指定 |
torch.flatten(x, start_dim) |
start_dim=1 | 保留batch维度,从第1维开始拉直(即通道、高、宽合并) |
nn.Linear(in_f, out_f) |
in_features, out_features | 全连接层,输入特征数和输出特征数 |
nn.ReLU() |
无额外参数 | 激活函数,对负值置0 |
完整训练示例(以MNIST为例)
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 超参数
BATCH_SIZE = 64
EPOCHS = 5
LEARNING_RATE = 0.001
NUM_CLASSES = 10
数据预处理与模型训练流程
数据预处理步骤
transform = transforms.Compose([
transforms.Resize((64, 64)),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
- 图像尺寸调整为64x64像素
- 转换为PyTorch张量格式
- 像素值归一化到[-1,1]范围
数据集加载
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
- 使用PyTorch内置MNIST数据集
- 自动下载并存储在指定目录
- 分别创建训练集和测试集
模型训练配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleCNN(num_classes=NUM_CLASSES).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
- 自动检测可用计算设备
- 初始化CNN模型并移至计算设备
- 使用交叉熵损失函数
- 采用Adam优化器
训练过程与评估
训练循环实现
for epoch in range(EPOCHS):
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
- 显式设置模型为训练模式
- 每个epoch重置梯度
- 前向传播计算输出
- 反向传播更新参数
- 累计损失用于监控
模型评估方法
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
- 显式设置模型为评估模式
- 禁用梯度计算节省内存
- 统计预测正确的样本数
- 计算总体准确率
关键知识点与问题解决
卷积网络核心概念
- 输出尺寸计算公式: $H_{out} = \frac{H_{in} + 2 \times padding - kernel_size}{stride} + 1$
- 通道数量变化由卷积核数量决定
- 展平操作前需准确计算特征图元素总数
常见问题解决方案
- 维度不匹配错误:检查各层输入输出维度
- 训练不收敛:调整学习率或添加归一化
- 显存溢出:减小批次大小或图像尺寸
- 模式不一致:确保正确调用train()/eval()
模型优化建议
结构改进方向
- 添加批量归一化层稳定训练
- 引入Dropout防止过拟合
- 采用残差连接解决梯度消失
- 使用更深的网络结构
训练技巧
- 实现学习率调度策略
- 添加早停机制防止过训练
- 使用数据增强提升泛化能力
- 监控验证集性能选择最佳模型
实验效果示例
典型训练过程输出:
Epoch 1/5, Loss: 0.3521
Epoch 2/5, Loss: 0.1234
Epoch 3/5, Loss: 0.0845
Epoch 4/5, Loss: 0.0652
Epoch 5/5, Loss: 0.0521
Test Accuracy: 98.35%
实验总结
卷积层尺寸计算
H_out = (H_in + 2*padding - kernel_size)/stride + 1
池化层输出尺寸需满足整除关系。例如kernel_size=2且stride=2时,输入尺寸必须为偶数。
通道变化规则
- 第一个卷积层:输入通道1(灰度图),输出通道32
- 第二个卷积层:输入通道32,输出通道64
卷积核数量直接决定输出通道数。
全连接层输入维度
必须精确计算展平后的特征图总元素数。错误示例:
# 错误:未计算实际展平维度
self.fc1 = nn.Linear(64*7*7, 128) # 假设输入224x224图片
# 正确:根据实际特征图尺寸计算
print(x.shape) # 调试输出展平前的维度
网络层顺序规范
Conv -> ReLU -> Pool
ReLU置于池化前可保留更多有效梯度。
Softmax使用场景
- 训练阶段:CrossEntropyLoss自动包含LogSoftmax
- 推理阶段:需手动添加
F.softmax(output, dim=1)
常见问题解决方案
维度不匹配错误
RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x12544 and 1440x128)
- 检查展平后的特征维度是否与
nn.Linear输入维度一致 - 使用
print(x.shape)调试各层输出
训练停滞问题
- 数据标准化:确认
transforms.Normalize参数合理 - 学习率调整:尝试
lr=0.001或使用学习率调度器 - 梯度裁剪:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
测试准确率异常
- 训练模式:确保
model.train() - 评估模式:测试前调用
model.eval() - BN/Dropout:模式切换影响这些层的计算方式
显存优化策略
- 降低
batch_size(如从64降至32) - 缩小输入尺寸(如从64x64改为48x48)
- 使用混合精度训练:
scaler = torch.cuda.amp.GradScaler()
进阶改进方向
- 添加正则化:
nn.Dropout(p=0.5) - 引入归一化:
nn.BatchNorm2d(num_features) - 结构优化:替换为ResNet残差块
- 数据增强:增加随机旋转/翻转等transforms
(注:所有代码示例需配合PyTorch环境使用,实际参数应根据具体网络结构调整)
更多推荐


所有评论(0)