CNN手写数字识别:从原理到实战(附PyTorch代码详解)
MNIST数据集(Modified National Institute of Standards and Technology database)是一个广泛使用的机器学习基准数据集,主要用于手写数字识别任务。它由美国国家标准与技术研究所(NIST)收集并修改而来,是计算机视觉和深度学习领域的经典入门数据集。图像内容:包含0到9的手写数字图片,均为灰度图像。数据规模:60,000张训练图像 + 1
CNN手写数字识别
引言
手写数字识别是计算机视觉领域的“Hello World”,而MNIST数据集和卷积神经网络(CNN)的结合,则是这一领域的经典组合。无论是入门深度学习,还是验证模型性能,CNN+MNIST的组合都值得深入探索。本文将从数据集解析、CNN原理、PyTorch实战到模型优化,带你全面掌握这一经典任务。
1. MNIST数据集:计算机视觉的基石
什么是MNIST?
MNIST数据集(Modified National Institute of Standards and Technology database)是一个广泛使用的机器学习基准数据集,主要用于手写数字识别任务。它由美国国家标准与技术研究所(NIST)收集并修改而来,是计算机视觉和深度学习领域的经典入门数据集。
-
图像内容:包含0到9的手写数字图片,均为灰度图像。
-
数据规模:60,000张训练图像 + 10,000张测试图像。
-
图像格式:28x28像素的灰度图,像素值范围0-255(黑色为0,白色为255)。
-
标签内容:每张图像对应一个0-9的标签,表示手写数字的真实值。
文件结构与内容示例
MNIST数据以二进制文件存储,文件命名遵循特定规则:
-
训练图像:train-images-idx3-ubyte
-
训练标签:train-labels-idx1-ubyte
-
测试图像:t10k-images-idx3-ubyte
-
测试标签:t10k-labels-idx1-ubyte
图像内容示例
获取方式
可通过主流的深度学习框架(如TensorFlow、PyTorch、Keras)直接加载:
batch_size = 64
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_dataset = datasets.MNIST(root='../dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = datasets.MNIST(root='../dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)
也可以在官网下载:mnist数据集官网
2. 为什么选择CNN?——从LeNet到现代网络的进化
CNN的发展简史
-
Neocognitron(1980):福岛邦彦提出“Neocognitron”,奠定卷积结构基础。
-
LeNet-5(1998):Yann LeCun 提出LeNet-5,首次成功应用于手写数字识别。包括卷积层、池化层和全连接层。
-
AlexNet(2012):AlexNet在ImageNet竞赛中夺冠,CNN成为深度学习的主流架构。包括多个卷积层、池化层、ReLU激活函数和全连接层。关键改进:ReLU激活函数、Dropout正则化、GPU并行训练。
-
ZFNet(2013): Matthew D. Zeiler、Rob Fergus 提出了一种新的可视化方法,是AlexNet的一个改进版本,即Deconvolutional Networks。与AlexNet相似,但有细微的结构差异和优化。
-
VGGNet(2014): Karen Simonyan、Andrew Zisserman 通过使用更小的卷积核(3x3)和更深的网络结构,在ImageNet比赛中取得了优异的成绩。结构主要由3x3的卷积层和2x2的池化层组成,深度可选为VGG16或VGG19。通过堆叠小卷积核(3x3)构建深层网络,证明深度对性能的重要性。
-
GoogLeNet(2014):Christian Szegedy等(Google Research)引入了“Inception模块”来提取多尺度的特征,以及全局平均池化来减少参数数量。结构包括多个Inception模块,与传统的卷积神经网络有很大的不同。
-
ResNet(2015):Kaiming He等(Microsoft Research)通过引入残差连接(Residual Connection)解决了深度卷积神经网络训练过程中的梯度消失和梯度爆炸问题。结构包括多个残差块(Residual Block),可以构建非常深的网络。
-
DenseNet(2017): Gao Huang等进一步提出了密集连接(Dense Connection)来增强特征重用和梯度流动。结构上每个层与所有前面的层直接连接。
-
EfficientNet(2019): Mingxing Tan、Quoc V. Le通过网络缩放方法在网络深度、宽度和分辨率上进行均衡,提高了模型的性能和计算效率。
-
Vision Transformers(ViT)(2020): Alexey Dosovitskiy等(Google Research)首次将Transformer架构应用于计算机视觉任务,取得了与卷积神经网络相当的性能。
-
自适应卷积网络(2021):Xin Li等通过自适应地调整卷积核形状和大小,实现了更高效的特征提取。
CNN的核心优势
-
高效的特征提取
自动学习图像的多层次特征,无需手动设计特征(如传统SIFT、HOG)。 -
参数共享与稀疏连接
参数量远小于全连接网络(例如:一个3x3卷积核仅9个参数,可扫描整张图像)。 -
平移、旋转和缩放鲁棒性
通过卷积和池化对输入的小变化不敏感。 -
端到端学习
从原始像素到分类结果全程优化,无需分步处理。 -
在MNIST任务中的表现
LeNet-5在MNIST上可达99%+准确率,远超传统方法(如SVM约95%)。
3. PyTorch:灵活与效率的平衡
为什么选择PyTorch?
-
优点
-
动态计算图:调试灵活,适合科研与快速迭代。
-
Python语法:代码简洁易读,学习成本低。
-
强大的生态系统:TorchVision、TorchText等工具库完善。
-
GPU加速:一行代码切换CPU/GPU(model.to(device))。
-
-
缺点:
- 动态图可能导致初学者调试困难(需熟悉张量形状变化)。
4. PyTorch实战:手写数字识别代码详解
代码来源与运行环境
- 推荐资源
- 依赖库
import torch
import torch.nn as nn
import torchvision
from torchvision import transforms
代码分步解析
Step 1:导入必要的库
-
torch:PyTorch深度学习框架核心库
-
transforms:数据预处理工具(如归一化、格式转换)
-
datasets:内置数据集加载工具
-
DataLoader:数据批量加载器(支持多线程加载)
-
F:PyTorch的函数式API(包含激活函数等)
-
optim:优化算法模块(如SGD、Adam)
-
matplotlib:可视化库(用于绘制准确率曲线)
import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
import time
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" # 解决某些系统下的OpenMP库冲突问题
Step 2: 数据准备与预处理
- ToTensor:将PIL图像或NumPy数组转换为[通道, 高度, 宽度]格式的Tensor,并自动将像素值从[0,255]缩放到[0.0,1.0]。
- Normalize:使用MNIST数据集的标准均值和标准差进行归一化:(像素值 - 0.1307) / 0.3081 → 数据分布更接近标准正态分布,加速训练收敛。
batch_size = 128
transform = transforms.Compose([
transforms.ToTensor(), # 将PIL图像转换为Tensor格式([C, H, W])
transforms.Normalize((0.1307,), (0.3081,)) # MNIST专用归一化参数
])
Step 3: 加载MNIST数据集
-
root:数据集存储路径(自动下载到指定目录)
-
train:True表示加载训练集(60k样本),False表示测试集(10k样本)
-
download:若本地无数据集,则自动下载
-
DataLoader参数:
-
shuffle=True:训练集打乱顺序,避免模型记忆样本顺序
-
batch_size=128:每个批次加载128张图像
-
测试集无需打乱(shuffle=False)
train_dataset = datasets.MNIST(
root='../dataset/mnist/',
train=True,
download=True,
transform=transform
)
test_dataset = datasets.MNIST(
root='../dataset/mnist/',
train=False,
download=True,
transform=transform
)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)
Step 4: 定义CNN模型
-
输入:[128, 1, 28, 28](128张1通道28x28图像)
-
conv1:
-
使用10个5x5卷积核 → 输出[128, 10, 24, 24]
-
公式:输出尺寸 = (输入尺寸 - 核大小 + 1) = 28-5+1=24
-
-
pooling:
- 2x2最大池化,步长2 → 输出[128, 10, 12, 12]
-
conv2:
-
20个5x5卷积核 → 输出[128, 20, 8, 8]
-
尺寸计算:12-5+1=8
-
-
pooling:
- 输出[128, 20, 4, 4]
-
view:
- 展平为[128, 2044=320]
-
fc:
- 全连接层将320维特征映射到10维(对应0-9数字)
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5) # [1,28,28] -> [10,24,24] ; pooling to [10,12,12]
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5) # [10,12,12] -> [20,8,8]; pooling to [20,4,4]
# self.conv3 = torch.nn.Conv2d(20,320,kernel_size=3) # [20,4,4] -> [30,2,2]; pooling to [30,1,1]
self.pooling = torch.nn.MaxPool2d(2)
self.fc = torch.nn.Linear(320, 10)
def forward(self, x):
# flatten data from (n,1,28,28) to (n, 784)
batch_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = F.relu(self.pooling(self.conv2(x)))
# x = F.relu(self.pooling(self.conv3(x)))
x = x.view(batch_size, -1) # -1 此处自动算出的是320
# print("x.shape",x.shape)
x = self.fc(x)
return x
Step 5: 设备配置与模型部署
-
torch.cuda.is_available():检测系统是否支持CUDA(NVIDIA GPU)
-
model.to(device):将模型参数和缓冲区转移到指定设备(GPU/CPU)
-
注意:输入数据也需要通过.to(device)转移到相同设备
start_time = time.time()
model = Net()
if torch.cuda.is_available() == 1:
device = torch.device("cuda")
print("CNN is running on GPU")
else:
device = torch.device("cpu")
print("CNN is running on CPU")
model.to(device)
Step 6:定义损失函数与优化器
- criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失(内置Softmax)
- optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
criterion = torch.nn.CrossEntropyLoss() # 交叉熵损失(内置Softmax)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
补充说明:
1.交叉熵损失包括softmax和NLLloss
与Softmax函数结合使用,将模型输出转换为概率分布。
2.优化器
(1)model.parameters()
获取模型中所有需要训练的参数(torch.nn.Parameter 对象)。在CNN中,model.parameters() 包括:卷积层的权重(conv1.weight, conv2.weight)卷积层的偏置(conv1.bias, conv2.bias)
全连接层的权重和偏置(fc1.weight, fc1.bias)
(2)lr=0.01(学习率)
控制每次参数更新的步长。
学习率越大,参数更新幅度越大,收敛速度越快,但可能错过最优解。
学习率越小,参数更新幅度越小,收敛速度越慢,但更可能找到精确解。
初始学习率通常设置为 0.01 或 0.001。
可通过学习率调度器(如 StepLR 或 ReduceLROnPlateau)动态调整。
(3)momentum=0.5(动量)
加速梯度下降并减少震荡。
动量引入“惯性”概念,使参数更新方向不仅依赖当前梯度,还依赖历史梯度。
动量系数通常设置为 0.9 或 0.5。值越大,历史梯度影响越大,更新方向更平滑。
Step 7: 训练函数实现
- enumerate(train_loader, 0):遍历数据加载器,返回批次索引和数据(从0开始计数)
- loss.item():将单元素张量转换为Python数值(避免内存累积)
- 梯度清零:必须调用optimizer.zero_grad(),否则梯度会累加导致训练异常
def train(epoch):
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):
inputs, target = data
inputs, target = inputs.to(device), target.to(device)
optimizer.zero_grad() # 清空历史梯度
outputs = model(inputs) # 前向传播
loss = criterion(outputs, target)
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新权重参数
running_loss += loss.item()
if batch_idx % 300 == 299: # 每300个batch打印一次损失
print('[%d, %5d] loss: %.3f' % (epoch+1, batch_idx+1, running_loss/300))
running_loss = 0.0
Step 8:测试函数实现
-
torch.no_grad():上下文管理器,关闭自动求导,减少内存消耗
-
torch.max(outputs.data, dim=1):沿维度1(类别维度)取最大值,返回(最大值,对应索引)
-
predicted == labels:生成布尔张量,sum().item()统计True的数量(正确预测数)
-
torch.max(outputs.data, dim=1):在 outputs 的第1维度(类别维度)上取最大值,返回最大值及其索引。返回值:第一个返回值是最大值(未使用,用 _ 忽略)。第二个返回值是最大值的索引(即预测的类别)。predicted:模型预测的类别索引,形状为 [batch_size]。例如,MNIST数据集的 predicted 形状为 [128],每个元素是一个0到9的整数。
-
labels.size(0):获取 labels 在第0维度上的大小,即当前批次的样本数量(batch_size)。
def test():
correct = 0
total = 0
with torch.no_grad(): # 禁用梯度计算(节省内存)
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, dim=1) # 取最大概率的类别
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = 100 * correct / total
print(f'Accuracy: {acc}%')
return acc
Step 9 :主训练循环与可视化
-
训练10个epoch:每个epoch遍历全部训练集一次,并在测试集评估
-
可视化:使用Matplotlib绘制准确率随epoch变化的曲线,直观反映模型学习进度
if __name__ == '__main__':
epoch_list = []
acc_list = []
for epoch in range(10):
train(epoch)
acc = test()
epoch_list.append(epoch)
acc_list.append(acc)
end_time = time.time()
print(f"Total time: {end_time - start_time:.2f}s")
plt.plot(epoch_list, acc_list)
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Training Progress')
plt.show()
完整代码
import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
import time
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
# prepare dataset
batch_size = 128
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# # 定义数据预处理操作
# data_transforms = transforms.Compose([
# transforms.Resize((224, 224)), # 调整图像大小
# transforms.ToTensor(), # 转化为张量
# # 归一化至 [0, 1] 范围内(假设图像为 RGB)
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
# ])
train_dataset = datasets.MNIST(root='../dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = datasets.MNIST(root='../dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)
# design model using class
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5) # [1,28,28] -> [10,24,24] ; pooling to [10,12,12]
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5) # [10,12,12] -> [20,8,8]; pooling to [20,4,4]
# self.conv3 = torch.nn.Conv2d(20,320,kernel_size=3) # [20,4,4] -> [30,2,2]; pooling to [30,1,1]
self.pooling = torch.nn.MaxPool2d(2)
self.fc = torch.nn.Linear(320, 10)
def forward(self, x):
# flatten data from (n,1,28,28) to (n, 784)
batch_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = F.relu(self.pooling(self.conv2(x)))
# x = F.relu(self.pooling(self.conv3(x)))
x = x.view(batch_size, -1) # -1 此处自动算出的是320
# print("x.shape",x.shape)
x = self.fc(x)
return x
start_time = time.time()
model = Net()
#device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if torch.cuda.is_available() == 1: # '1' -> GPU ; '0' -> CPU;
device = torch.device("cuda")
print("CNN is running on GPU")
else:
device = torch.device("cpu")
print("CNN is running on CPU")
model.to(device)
#定义一个损失函数,来计算我们模型输出的值和标准值的差距
criterion = torch.nn.CrossEntropyLoss()
#定义一个优化器,训练模型咋训练的,就靠这个,他会反向的更改相应层的权重
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)#lr为学习率
# training cycle forward, backward, update
def train(epoch):
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):#每次取一个样本
inputs, target = data
inputs, target = inputs.to(device), target.to(device)
optimizer.zero_grad()# 优化器清零
outputs = model(inputs)# 正向计算一下
loss = criterion(outputs, target) # 计算损失
loss.backward()# 反向求梯度
optimizer.step()#更新权重
running_loss += loss.item()#把损失加起来
if batch_idx % 300 == 299:#每300次输出一下数据
print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0.0
def test():
correct = 0
total = 0
with torch.no_grad():#不用算梯度
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, dim=1)#我们取概率最大的那个数作为输出
total += labels.size(0)
correct += (predicted == labels).sum().item()#计算正确率
print('accuracy on test set: %d %% ' % (100 * correct / total))
return correct / total
if __name__ == '__main__':
epoch_list = []
acc_list = []
for epoch in range(10):
train(epoch)
acc = test()
epoch_list.append(epoch)
acc_list.append(acc)
end_time = time.time()
execution_time = end_time - start_time
print(f"神经网络生成与运行时间为:{execution_time}秒")
plt.plot(epoch_list, acc_list)
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.show()
运行结果
5. 模型优化:平衡速度与精度
- 模型复杂度统计
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数量:{total_params}") # 示例模型约1.2M参数
- FLOPs估算:使用thop库:
from thop import profile
flops, _ = profile(model, inputs=(torch.randn(1,1,28,28),))
print(f"FLOPs: {flops / 1e6:.2f}M") # 示例模型约50M FLOPs
Pytorch 计算模型复杂度 (Params 和 FLOPs)
结语
通过MNIST数据集和PyTorch实战,我们不仅掌握了CNN的基础原理,还体验了从数据加载到模型优化的完整流程。尽管MNIST看似简单,但它为理解更复杂的视觉任务(如目标检测、图像分割)奠定了重要基础。读者可尝试修改网络结构、调整超参数,或挑战更高难度的数据集,进一步深化对CNN的理解。
扩展阅读与参考:
- 卷积神经网络CNN中的参数量(parameters)和计算量(FLOPs )
- 选择哪个batchsize对模型效果最好?
- 神经网络之梯度下降法及其实现
- 深入浅出——搞懂卷积神经网络的过拟合、梯度弥散、batchsize的影响的问题(二)
- 如何对卷积核数量和卷积步长进行选择?
- 卷积神经网络(CNN)详细介绍及其原理详解
- 李西明/(纯C语言)神经网络手写数字识别
- FPGA识别MNIST(1):先用pytorch训练一个简单的网络
- (纯C语言)神经网络手写数字识别 如何准备自己的数据
- [ 数据集 ] MINIST 数据集介绍
- 【一站式详细教程】PyCharm安装与Anaconda下PyTorch环境配置
- 深度学习环境配置CUDA、cuDNN、Tensorrt及Anaconda虚拟环境
更多推荐
所有评论(0)