在深度学习的计算机视觉任务中,卷积神经网络(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环境使用,实际参数应根据具体网络结构调整)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐