- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
文章目录
- 1. 引言
- 2. 环境准备
- 3. 数据加载与预处理
- 4. 构建我们的CNN网络 (`torch.nn.Module`)
- CNN实现思路
- 4.2 CNN代码实现
- 4.3 手动推导核心层计算过程 (重点!)
- 🔍 逐步推导流程(详细版)
- 1. **输入层 (Input)**
- 2. **Conv1 (3→64, kernel=3, stride=1, padding=0)**
- 3. **Pool1 (MaxPool, kernel=2, stride=2)**
- 4. **Conv2 (64→64, kernel=3, stride=1, padding=0)**
- 5. **Pool2 (MaxPool, kernel=2, stride=2)**
- 6. **Conv3 (64→128, kernel=3, stride=1, padding=0)**
- 7. **Pool3 (MaxPool, kernel=2, stride=2)**
- 8. **Flatten**
- 9. **FC1 (全连接层1: 512 → 256)**
- 10. **FC2 (输出层: 256 → 10)**
- 5. 模型训练与验证
- 6. 运行结果与分析
- 7. 🎯总结与回顾
1. 引言
在上一篇博客中,我们一起探索了使用CNN识别MNIST手写数字的完整流程,相信你已经对深度学习项目的基本构建有了初步的实践体验。
但是,深度学习不仅仅是“调包”和“跑通代码”。真正掌握CNN的精髓,关键在于理解其核心组件—— 卷积层(Convolution) 和 池化层(Pooling) ——是如何对图像数据进行特征提取和空间降维的。很多教程会直接调用nn.Conv2d和nn.MaxPool2d这样的API,把计算过程当作“黑盒子”,这往往阻碍了我们灵活设计网络和调试问题。
本篇博客,我们将深入CNN的“心脏地带”! 🎯 不同于浅尝辄止,我们的核心目标是:手把手带你推导卷积层与池化层的具体计算过程,并将其应用到彩色图像识别任务(CIFAR-10数据集)的实战中!
2. 环境准备
- PyTorch框架安装与GPU加速设置
如果设备上支持GPU就使用GPU,否则使用CPU(一般游戏本都有GPU)
【超详细教程】2024最新Pytorch安装教程(同时讲解安装CPU和GPU版本) - 依赖库导入与硬件检查(torch.cuda验证)
import torch
import torch.nn as nn
import matplotlib.pyplot as plt # 图像可视化
import torchvision # 用于加载CIFAR-10
import numpy as np # 数据处理
# 设置硬件设备,如果有GPU则使用,没有则使用cpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
# 如果输出结果是“device(type='cuda')”,则说明GPU版本的pytorch安装成功
3. 数据加载与预处理
- 加载CIFAR-10
- 使用 torchvision.datasets.CIFAR10 加载训练集和测试集。
- 解释参数:root (数据存放路径), train (区分训练/测试), download (是否下载), transform (数据处理)。
- 数据预处理
- ToTensor():将PIL/Numpy图像转为PyTorch Tensor (C x H x W), 并归一化到 [0, 1]。
- 创建DataLoader:
- 使用 torch.utils.data.DataLoader 封装数据集。
- 解释参数:batch_size (批次大小), shuffle (训练集需打乱), num_workers (数据加载进程数)。
- 可视化: 展示几个批次中的图片样本(用matplotlib),让读者对数据有直观感受。
# 加载CIFAR-10
train_ds = torchvision.datasets.CIFAR10('data',
train=True,
transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
download=True)
test_ds = torchvision.datasets.CIFAR10('data',
train=False,
transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
download=True)
# 创建DataLoader
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_ds,
batch_size=batch_size,
shuffle=True)
test_dl = torch.utils.data.DataLoader(test_ds,
batch_size=batch_size)
# 可视化
import numpy as np
# 指定图片大小,图像大小为20宽、5高的绘图(单位为英寸inch)
plt.figure(figsize=(20, 5))
for i, imgs in enumerate(imgs[:20]):
# 进行轴变换
npimg = imgs.numpy().transpose((1, 2, 0))
# 将整个figure分成2行10列,绘制第i+1个子图。
plt.subplot(2, 10, i+1)
plt.imshow(npimg, cmap=plt.cm.binary)
plt.axis('off')
#plt.show() 如果你使用的是Pycharm编译器,请加上这行代码
- 可视化结果如下:
- 这里你的结果可能会和我不一样,这是因为DataLoader会把数据打乱,每次运行时DataLoader都不相同。
4. 构建我们的CNN网络 (torch.nn.Module
)
CNN实现思路
针对CIFAR-10(32x32像素的小尺寸彩色图像),一个有效的策略是采用多次堆叠[卷积+激活+池化]的模块化结构,最后连接全连接层进行分类;这样的设计是从LeNet-5中借鉴而来的,感兴趣的同学可以去了解一下LeNet-5并复现出来。下面列举了CNN中的各个部分的作用和用途。
模块 | 作用 | 关键设计点 |
---|---|---|
卷积层(Conv) | 提取图像特征(边缘、纹理、物体部分) | 小卷积核(3x3)、通道数逐步增加 |
激活函数(ReLU) | 引入非线性,增强表达能力 | 常用选择,计算高效 |
池化层(Pool) | 降维,提取显著特征,减少计算量 | 最大池化(MaxPool)、2x2核/步长2 |
展平层(Flatten) | 过渡多维特征为分类网络输入 | 将(C, H, W)展平为一维向量 |
全连接层(FC) | 组合高级特征进行最终分类 | 输入尺寸需精确计算 |
严谨的说,这里的“展平层”并不能被称作一个层,它只是对特征图的一个处理(把所有像素拉平)。
卷积层的输入是图像时,一次扫描会扫描所有通道的值再加和成一张特征图。
当卷积层的输入是上层的特征图时,特征图会被当做“通道”对待,一次扫描会扫描所有输入的特征
图,加和成新的feature map。无论在哪一层,生成的feature map的数量都等于这一层的扫描次数,也就是等于out_channels的值。下一层卷积的in_channels就等于上一层卷积out_channels。
我们的具体网络流程如下:
输入图像(3, 32, 32)
→ [Conv3x3(64) → ReLU → MaxPool2x2]
→ [Conv3x3(64) → ReLU → MaxPool2x2]
→ [Conv3x3(128) → ReLU → MaxPool2x2]
→ Flatten
→ FC(256) → ReLU
→ FC(10)
4.2 CNN代码实现
import torch.nn.functional as F
num_classes = 10 # 图片的类别数
class Model(nn.Module):
def __init__(self):
super().__init__()
# 特征提取网络
self.conv1 = nn.Conv2d(3, 64, kernel_size=3)
self.pool1 = nn.MaxPool2d(kernel_size=2) # 设置池化层,池化核大小为2*2
self.conv2 = nn.Conv2d(64, 64, kernel_size=3) # 第二层卷积,卷积核大小为3*3
self.pool2 = nn.MaxPool2d(kernel_size=2)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3) # 第二层卷积,卷积核大小为3*3
self.pool3 = nn.MaxPool2d(kernel_size=2)
# 分类网络
self.fc1 = nn.Linear(512, 256)
self.fc2 = nn.Linear(256, num_classes)
# 前向传播
def forward(self, x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = self.pool3(F.relu(self.conv3(x)))
x = torch.flatten(x, start_dim=1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
关键代码解释:
- nn.Conv2d参数:
- in_channels:输入数据的通道数(RGB为3)
- out_channels:卷积产生的特征图数量
- kernel_size:卷积核尺寸(这里是3*3)
- stride=1,padding=0:默认值(无填充)
- nn.MaxPool2d参数:
- kernel_size=2:池化窗口尺寸(2*2)
- stride=2:步长默认等于核尺寸(特征图尺寸减半)
- nn.Linear关键点:
- in_features:必须等于Flatten后的向量长度(需要手动计算推导)
- forward流程:
- 清晰的三阶段:特征提取 → 展平 → 分类
- F.relu:ReLU激活函数增强非线性
- torch.flatten(x, 1):从维度1开始展平(保留batch维度)
接下来我们可以通过torchinfo查看网络结果
from torchinfo import summary
# 将模型转移到GPU中(我们模型运行均在GPU中进行)
model = Model().to(device)
summary(model,[1, 3, 32, 32])
输出结果如下
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
Model [1, 10] --
├─Conv2d: 1-1 [1, 64, 30, 30] 1,792
├─MaxPool2d: 1-2 [1, 64, 15, 15] --
├─Conv2d: 1-3 [1, 64, 13, 13] 36,928
├─MaxPool2d: 1-4 [1, 64, 6, 6] --
├─Conv2d: 1-5 [1, 128, 4, 4] 73,856
├─MaxPool2d: 1-6 [1, 128, 2, 2] --
├─Linear: 1-7 [1, 256] 131,328
├─Linear: 1-8 [1, 10] 2,570
==========================================================================================
Total params: 246,474
Trainable params: 246,474
Non-trainable params: 0
Total mult-adds (M): 9.17
==========================================================================================
Input size (MB): 0.01
Forward/backward pass size (MB): 0.57
Params size (MB): 0.99
Estimated Total Size (MB): 1.56
==========================================================================================
4.3 手动推导核心层计算过程 (重点!)
为什么要手动推导?
理解每层输入/输出尺寸变化对于网络设计、调试(特别是全连接层尺寸错误)至关重要。我们以单张图像输入[1, 3, 32,32]为例逐步推导。
🔍 逐步推导流程(详细版)
1. 输入层 (Input)
- 输入尺寸:
[1, 3, 32, 32]
(Batch, Channels, Height, Width) - 原始图像尺寸:32×32像素,3个颜色通道(RGB)
2. Conv1 (3→64, kernel=3, stride=1, padding=0)
H_out = (H_in + 2×padding - kernel_size) / stride + 1
= (32 + 2×0 - 3) / 1 + 1
= (29) / 1 + 1
= 30
- 宽度计算同理:
W_out = 30
- 输出尺寸:
[1, 64, 30, 30]
- 解释:卷积核在图像上滑动,每次移动1像素,产生30×30的特征图
3. Pool1 (MaxPool, kernel=2, stride=2)
H_out = (H_in - kernel_size) / stride + 1
= (30 - 2) / 2 + 1
= 28 / 2 + 1
= 14 + 1
= 15
- 输出尺寸:
[1, 64, 15, 15]
- 解释:2×2窗口取最大值,步长为2,空间尺寸减半
4. Conv2 (64→64, kernel=3, stride=1, padding=0)
H_out = (15 + 2×0 - 3)/1 + 1
= (12)/1 + 1
= 13
- 输出尺寸:
[1, 64, 13, 13]
- 说明:保持64通道数,提取更复杂的特征
5. Pool2 (MaxPool, kernel=2, stride=2)
H_out = (13 - 2)/2 + 1
= 11/2 + 1
= 5.5 + 1
→ 取整 = 6(PyTorch自动向下取整)
- 输出尺寸:
[1, 64, 6, 6]
- 重要:尺寸非整除时执行floor操作
6. Conv3 (64→128, kernel=3, stride=1, padding=0)
H_out = (6 + 2×0 - 3)/1 + 1
= (3)/1 + 1
= 4
- 输出尺寸:
[1, 128, 4, 4]
- 特征:通道数翻倍(128),提升模型表达能力
7. Pool3 (MaxPool, kernel=2, stride=2)
H_out = (4 - 2)/2 + 1
= 2/2 + 1
= 1 + 1
= 2
- 输出尺寸:
[1, 128, 2, 2]
- 最终特征图:空间信息高度压缩,保留最显著特征
8. Flatten
特征图体积 = 通道 × 高度 × 宽度 = 128 × 2 × 2 = 512
- 输出尺寸:
[1, 512]
- 连接卷积网络和分类网络的关键操作
9. FC1 (全连接层1: 512 → 256)
矩阵乘法: [1,512] × [512,256] = [1,256]
- 输出尺寸:
[1, 256]
- 引入非线性特征组合能力
10. FC2 (输出层: 256 → 10)
矩阵乘法: [1,256] × [256,10] = [1,10]
- 输出尺寸:
[1, 10]
- 生成10个类别的预测分数(Logits)
- 强调:为什么关注尺寸计算?
- 连接层: 池化层之后的特征图尺寸决定了 Flatten 层的 in_features,进而决定了第一个全连接层 nn.Linear 的 in_features。手动推导尺寸的目的是为了正确配置后续的全连接层!(如本例中,第二个池化层后输出 [1,
64, 8, 8], Flatten 后 in_features=64 * 8 * 8=4096, 所以 nn.Linear(4096,
128))。这体现了网络构建的连贯性和理解的必要性。
5. 模型训练与验证
5.1 损失函数(CrossEntropyLoss)与优化器(SGD)选择
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-2 # 学习率
opt = torch.optim.SGD(model.parameters(),lr=learn_rate) # 这里使用SGD随机梯度下降,MNIST数据简单,SGD足以胜任
5.2 训练函数与测试函数的实现
训练函数
# 训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小,一共60000张图片
num_batches = len(dataloader) # 批次数目,1875(60000/32)
train_loss, train_acc = 0, 0 # 初始化训练损失和正确率
for X, y in dataloader: # 获取图片及其标签
X, y = X.to(device), y.to(device)
# 计算预测误差
pred = model(X) # 网络输出
loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
# 反向传播三部曲
optimizer.zero_grad() # grad属性归零 (梯度归零)
loss.backward() # 反向传播 (梯度计算)
optimizer.step() # 每一步自动更新 (梯度更新)
# 记录acc与loss
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
测试函数
def test (dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小,一共10000张图片
num_batches = len(dataloader) # 批次数目,313(10000/32=312.5,向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
5.4 模型训练与评估
epochs = 5
train_loss = []
train_acc = []
test_loss = []
test_acc = []
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%,Test_loss:{:.3f}')
print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss))
print('Done')
模型训练与预测结果:
6. 运行结果与分析
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 #分辨率
from datetime import datetime
current_time = datetime.now() # 获取当前时间
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time) # 打卡请带上时间戳,否则代码截图无效
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
7. 🎯总结与回顾
维度 | 核心收获 |
---|---|
目标达成 | 完成CIFAR-10全流程实战(数据→模型→训练→评估) |
核心突破 | ✅ 手推卷积/池化计算逻辑 ✅ 解析CNN架构设计原理 ✅ 攻克全连接层尺寸难题 |
能力进阶 | 获得白盒化建模能力(透明理解+自主设计+深度调试) |
终极价值 | 掌握深度学习黄金路径:理解原理→动手实践→迭代优化 |