深度学习入门:卷积神经网络与 MNIST 手写数字识别
前言 :上一篇我们完成了 PyTorch 环境搭建,并用一个三层全连接神经网络实现了 MNIST 手写数字识别,测试准确率约为 72%。本篇我们将学习深度学习中最重要的网络结构------卷积神经网络(CNN)。相比全连接网络,CNN 通过卷积层自动提取图像的局部特征,参数量更少、识别效果更好,是图像分类任务的主流方案。
目录
- 一、为什么需要卷积神经网络
- 二、卷积神经网络的核心组件
- 三、CNN 网络结构设计
- 四、完整代码实现
- 五、运行结果
- 六、总结
一、为什么需要卷积神经网络
1.1 全连接网络处理图像的问题
上一篇我们使用全连接网络处理 MNIST 图像时,将 28×28 的图像展平成 784 维向量输入网络。这种方式存在两个问题:
| 问题 | 说明 |
|---|---|
| 参数量巨大 | 一张 28×28 的图片展平后是 784 维,如果隐藏层有 1000 个神经元,仅一层就有 78.4 万个参数 |
| 丢失空间信息 | 展平操作破坏了像素之间的空间关系,图像相邻像素的关联信息被丢弃 |
1.2 CNN 的优势
卷积神经网络通过局部连接 和权值共享解决了上述问题:
| 优势 | 说明 |
|---|---|
| 局部感知 | 每个卷积核只关注图像的一小块区域,捕捉局部特征 |
| 权值共享 | 同一个卷积核在整张图上滑动,大幅减少参数量 |
| 保留空间信息 | 不展平图像,保留像素间的空间关系 |
| 平移不变性 | 无论目标出现在图像哪个位置,都能被识别 |
二、卷积神经网络的核心组件
2.1 卷积层(Conv2d)
卷积层是 CNN 的核心,通过卷积核在图像上滑动进行特征提取。
核心参数:
| 参数 | 说明 |
|---|---|
in_channels |
输入通道数(灰度图为 1,RGB 图为 3) |
out_channels |
输出通道数(卷积核的个数) |
kernel_size |
卷积核大小(如 5×5) |
stride |
步长,卷积核每次滑动的距离 |
padding |
边界填充,保持输出尺寸不变时通常设为 (kernel_size-1)/2 |
输出尺寸计算公式:
out = in − kernel + 2 × padding stride + 1 \text{out} = \frac{\text{in} - \text{kernel} + 2 \times \text{padding}}{\text{stride}} + 1 out=stridein−kernel+2×padding+1
2.2 激活函数(ReLU)
ReLU(Rectified Linear Unit)是 CNN 中最常用的激活函数:
f ( x ) = max ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)
特点:计算简单、梯度不会消失、加速收敛。
2.3 池化层(MaxPool2d)
池化层用于降低特征图尺寸,减少参数量,同时保留主要特征。
python
nn.MaxPool2d(kernel_size=2) # 2×2 区域取最大值,尺寸减半
2.4 全连接层(Linear)
经过多次卷积和池化后,将特征图展平,通过全连接层输出分类结果。
三、CNN 网络结构设计
本案例使用的 CNN 网络结构如下:
输入: 1×28×28 (灰度图)
↓
Conv1: Conv2d(1, 16, 5, 1, 2) → ReLU → MaxPool2d(2)
→ 输出: 16×14×14
↓
Conv2: Conv2d(16, 32, 5, 1, 2) → ReLU → Conv2d(32, 32, 5, 1, 2) → ReLU → MaxPool2d(2)
→ 输出: 32×7×7
↓
Conv3: Conv2d(32, 64, 5, 1, 2) → ReLU
→ 输出: 64×7×7
↓
Flatten: 展平为 64×7×7 = 3136 维
↓
Linear: 3136 → 10 (对应数字 0-9)
3.1 各层输出尺寸变化
| 层级 | 操作 | 输出尺寸 |
|---|---|---|
| 输入 | - | 1×28×28 |
| Conv1 | Conv2d(1, 16, 5, 1, 2) | 16×28×28 |
| Pool1 | MaxPool2d(2) | 16×14×14 |
| Conv2 | Conv2d(16, 32, 5, 1, 2) | 32×14×14 |
| Conv2 | Conv2d(32, 32, 5, 1, 2) | 32×14×14 |
| Pool2 | MaxPool2d(2) | 32×7×7 |
| Conv3 | Conv2d(32, 64, 5, 1, 2) | 64×7×7 |
| Flatten | view | 3136 |
| Linear | 3136→10 | 10 |
四、完整代码实现
python
# 导入必要的库
import torch
import torchvision
import torchaudio
from torch import nn # 导入神经网络模块
from torch.utils.data import DataLoader # 数据包管理工具,打包数据
from torchvision import datasets # 封装了很多与图像相关的模型,数据集
from torchvision.transforms import ToTensor # 数据转换
from matplotlib import pyplot as plt
import torch.nn.functional as F
# ====================下载数据集====================
training_data = datasets.MNIST(root='data',
train=True,
download=True,
transform=ToTensor()
)
test_data = datasets.MNIST(root='data',
train=False,
download=True,
transform=ToTensor()
)
# ====================创建数据加载器====================
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 检查一批数据的形状
for X, y in test_dataloader:
print(f"Shape of X [N, C, H, W]: {X.shape}")
print(f"Shape of y: {y.shape} {y.dtype}")
break
# ====================选择设备====================
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
# ====================定义CNN模型====================
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)
self.conv2 = nn.Sequential(
nn.Conv2d(16, 32, 5, 1, 2),
nn.ReLU(),
nn.Conv2d(32, 32, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)
self.conv3 = nn.Sequential(
nn.Conv2d(32, 64, 5, 1, 2),
nn.ReLU(),
)
self.out = nn.Linear(64 * 7 * 7, 10)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
model = CNN().to(device)
print(model)
# ====================训练函数====================
def train(dataloader, model, loss_fn, optimizer):
model.train()
batch_size_num = 1
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_value = loss.item()
if batch_size_num % 100 == 0:
print(f"loss: {loss_value:>7f} [number:{batch_size_num}]")
batch_size_num += 1
# ====================测试函数====================
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test result: \n Accuracy: {(100 * correct)}%, Avg loss: {test_loss}")
# ====================损失函数和优化器====================
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(
model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
weight_decay=1e-4
)
# ====================开始训练====================
epochs = 20
for t in range(epochs):
print(f"Epoch {t + 1}\n====================================")
train(train_dataloader, model, loss_fn, optimizer)
print("Done!")
test(test_dataloader, model, loss_fn)
五、运行结果
5.1 训练过程输出
Epoch 1
====================================
loss: 0.086762 [number:100]
loss: 0.260086 [number:200]
......
Epoch 20
====================================
loss: 0.030822 [number:100]
loss: 0.000040 [number:200]
......
loss: 0.000118 [number:800]
loss: 0.000056 [number:900]
Done!
Test result:
Accuracy: 98.92%, Avg loss: 0.05751212948347662
5.2 结果对比
| 模型 | 测试准确率 | 特点 |
|---|---|---|
| 全连接网络(上一篇) | 约 72% | 参数量大,丢失空间信息 |
| CNN(本篇) | 约 99% | 参数少,自动提取特征 |
使用 CNN 后,MNIST 手写数字识别的准确率从 72% 提升到了 99% 以上,效果提升非常显著。
六、总结
核心知识点速查
| 知识点 | 关键概念 |
|---|---|
| CNN | 卷积神经网络,图像分类的主流方案 |
| 卷积层 | nn.Conv2d(),通过卷积核提取局部特征 |
| ReLU | 激活函数,nn.ReLU() |
| 池化层 | nn.MaxPool2d(),降低特征图尺寸 |
| 全连接层 | nn.Linear(),输出分类结果 |
| 参数计算 | 输出尺寸 = (输入 - 卷积核 + 2×padding) / stride + 1 |
核心 API 一览
| 用途 | 对应模块 / 方法 |
|---|---|
| 卷积层 | nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding) |
| 激活函数 | nn.ReLU() |
| 池化层 | nn.MaxPool2d(kernel_size) |
| 全连接层 | nn.Linear(in_features, out_features) |
| 展平 | x.view(x.size(0), -1) |
| 优化器 | torch.optim.AdamW() |
注意事项
| 要点 | 说明 |
|---|---|
| 输入通道 | 灰度图 in_channels=1,RGB 图 in_channels=3 |
| padding 计算 | 保持尺寸不变时,padding = (kernel_size - 1) / 2 |
| 展平维度 | 需根据前一层输出尺寸计算,如 64×7×7 = 3136 |
| 优化器选择 | AdamW 比 SGD 收敛更快,适合初学者 |
| 学习率 | 0.001 是 Adam 系列的常用初始值 |
系列直达
- 上篇 :深度学习入门:PyTorch 环境搭建与 MNIST 手写数字识别
- 本篇:深度学习入门:卷积神经网络与 MNIST 手写数字识别(本文)
- 下篇:敬请期待