利用卷积神经网络实现手写字体识别
前文 利用神经网络实现手写字体识别 中构建的模型只单单使用了全连接层, 其中每一层的神经元都会与前一层的所有神经元相连接,这种结构其实更适合于结构化数据或一维数据。而像手写字体识别之类的图像结构数据,使用卷积神经网络其实会更合适一点,一方面通过卷积层可以提取局部特征,另一方面经过池化层还能减少参数量,提高处理效率。
1.卷积神经网络
卷积神经网络(Convolutional Neural Network,简称CNN)是一种特别适合处理图像、视频、语音、文本等数据的神经网络结构,它通过模仿生物视觉系统的工作原理,利用 卷积层、池化层 和 全连接层 来提取数据中的空间特征,并通过训练来优化参数,这在计算机视觉任务中有着广泛应用。
1.基本原理
CNN的核心思想是“卷积”操作。简单来说,卷积是用一个小的矩阵(称为卷积核)在输入图像上滑动,通过和图像的局部区域进行逐点相乘并求和,提取出图像的局部特征。卷积操作通常会在多个卷积层次上进行,从而能够识别图像中的简单特征(如边缘、纹理)和复杂特征(如物体、面部等)。
2.基本结构
- 卷积层(Convolutional Layer):也是CNN核心部分,通过卷积核在输入数据(如图像)上滑动,逐步提取边缘、纹理、角点等局部特征。卷积层会将输入数据映射到多个特征图(Feature Maps),每个卷积核生成一个特征图,即输出的通道数等于卷积核的数量。
- 池化层(Pooling Layer):用于 下采样,减少特征图的尺寸,从而减少计算量、提高模型的计算效率,并防止过拟合。常用的池化方法包括 最大池化(Max Pooling)和 平均池化(Average Pooling)。
- 激活函数:卷积层和全连接层的输出通常会通过 激活函数(如 ReLU、Sigmoid 或 Tanh)进行非线性变换,以帮助模型学习复杂的特征。
- 全连接层(Fully Connected Layer):一般放在CNN的最后,通常会通过一个或多个全连接层将提取到的特征转换为最终输出。比如,图像分类的类别概率。
3.特点
- 局部连接:每个卷积核只与输入数据的一个小区域进行连接(局部感受野),这样可以有效地捕捉数据的局部特征(如图像中的边缘、角点、纹理等)
- 共享权重:同一个卷积核在所有输入区域使用相同的权重参数,这大大减少了参数的数量,提高了计算效率
- 层次化特征提取:CNN 可以通过多层卷积操作逐层提取数据的特征,从低级特征(如边缘)到高级特征(如物体的形状),这种层次化的特征提取方式能够捕捉复杂的模式
- 平移不变性:CNN 通过卷积操作具备一定的 平移不变性,即如果图像中的物体发生平移,卷积层仍能有效地识别物体。这是因为卷积核在图像中滑动,每个区域的特征都有相同的权重
- 高效的参数共享:由于卷积核共享权重,CNN 的参数数量远低于传统的全连接神经网络,这样使得 CNN 更适合处理大型图像数据集,且训练时所需的计算和内存资源较少
2.数据集处理
加载 MNIST 数据集,并对数据进行预处理。
1 | from torchvision import datasets, transforms |
1.加载数据
从 torchvision
的datasets中导入自带的MNIST数据集并保存在root
路径下,train=True
,表示加载的是训练集(train=False 则表示加载测试集)。
常用的数据集:
• 图像分类:CIFAR-10、ImageNet等
• 目标检测与分割:COCO、PASCAL VOC、Cityscapes等
• 人脸识别:LFW(Labeled Faces in the Wild)
• 支持学习与小样本学习:Omniglot
这些数据集大大方便了研究人员、开发者进行计算机视觉任务的训练和测试。随着计算机视觉的发展,datasets模块也在不断更新扩展中。
数据加载器(DataLoader)为了高效批量加载数据,batch_size为每个批次的数据量,shuffle表示是否打乱数据
2.预处理
transforms.ToTensor()
会将加载的图像数据转换为 张量(Tensor),并将像素值从 [0, 255] 范围缩放到 [0, 1] 范围。此转换是为了方便后续神经网络训练。
3.模型定义
Mnist_CNN
模型中,包括两部分构造函数和前向传播,前向传播函数会在模型训练的时候自动调用
1 | import torch.nn as nn |
1.构造函数
构造函数会初始化神经网络的各个层,模型中定义了三个卷积层和一个全连接层:
1.卷积层conv1
nn.Conv2d:这是一个二维卷积层,用来提取输入图像的局部特征
in_channels=1
:输入图像的通道数,MNIST 图像是单通道(灰度图),所以是 1out_channels=16
:卷积层输出的特征图的通道数,设置为16,意味着该层将学习16个不同的特征kernel_size=5
:卷积核的大小为5x5padding=2
:在图像边缘添加2个像素的零填充,目的是让输出图像的大小与输入相对称
nn.ReLU():这是一个激活函数层,使用 ReLU(Rectified Linear Unit)激活函数来对卷积结果进行非线性映射,ReLU 的作用是将所有负值置为零。
nn.MaxPool2d:池化层,进行下采样(降维)操作。kernel_size=2:池化窗口大小是2x2,表示每个2x2的区域都会被池化成一个最大值(最大池化),池化操作可以降低计算量并提取最重要的特征。
输出尺寸:原始输入图像的尺寸是 (28, 28),经过卷积后,尺寸为 (28, 28),再加上池化操作,最终输出为 (16, 14, 14),也就是16个14x14的特征图。
2.卷积层conv2
作用类似conv1,只是输入和输出的通道数发生了变化:
- 输入通道是16(来自上一层 conv1 的输出),输出通道是32
- 卷积核大小、步长和填充方式保持不变
输出尺寸:经过卷积和池化后,输出特征图的尺寸是 (32, 7, 7),即32个7x7的特征图。
3.卷积层conv3
作用同conv1和conv1,输入通道是32,输出通道是64,卷积核大小、步长和填充方式保持不变。
输出尺寸:经过这一层后,输出的特征图尺寸是 (64, 7, 7),即64个7x7的特征图。
4.全连接层out
做一个10分类的操作,将卷积层提取的特征映射到最终的分类结果
- 因为conv3输出的特征图的尺寸是 (64, 7, 7),输入特征的数量是 64 x 7 x 7 = 3136
- 因为MNIST数据集有10个数字类别(0-9),输出特征的数量是 10
2.前向传播
数据从输入到输出的流动过程,依次通过三个卷积层和一个全连接层
x = self.conv1(x)
:输入数据首先通过 conv1 层,输出经过 ReLU 激活和池化后的特征x = self.conv2(x)
:接着通过 conv2 层,继续提取特征x = self.conv3(x)
:然后经过 conv3 层,进一步提取深层特征x = x.view(x.size(0), -1)
:这里将卷积层输出的特征图展平(flatten)为一维向量,将(64, 7, 7)特征图转化为64 * 7 * 7 = 3136个特征。x.size(0) 是批次大小(batch_size),-1 代表根据批次大小自动计算展平后的大小output = self.out(x)
:将展平后的向量通过全连接层,输出 10 个分类结果
4.模型训练和验证
1.计算准确率
返回预测正确的样本数和总样本数
1 | def accuracy(predictions, labels): |
实现:
torch.max(predictions.data, 1)[1]
:返回每个样本在所有类别中最大概率的类别(即预测的类别)。torch.max 返回两个值,第一个是最大值,第二个是最大值对应的索引(即预测类别)pred.eq(labels.data.view_as(pred))
:eq 函数用于判断预测值和真实标签是否相等。如果相等返回 True,否则返回 Falsesum()
:统计预测正确的样本数
2.训练验证模型
训练模型的主函数,进行模型训练和评估
1 | def train(model, train_loader, test_loader, num_epochs): |
每个 epoch 中会执行以下步骤,每次迭代叫做一个epoch:
训练
for batch_idx, (data, target) in enumerate(train_loader):
:对于训练数据集(通过 train_loader 加载),每次获取一个批次的输入数据 data 和目标标签 targetmodel.train()
:将模型设置为训练模式(启用 dropout 和 batch normalization 等层)output = model(data)
:通过模型对输入 data 进行前向传播,得到输出 outputloss = criterion(output, target)
:计算预测值 output 与真实标签 target 之间的损失(如交叉熵损失)optimizer.zero_grad()
:在每个步骤之前清空梯度,以避免梯度累积loss.backward()
:做反向传播,计算损失的梯度optimizer.step()
:更新模型的参数(使用梯度下降或其他优化算法)
准确率计算
right = accuracy(output, target)
:通过 accuracy 函数计算当前批次的正确预测数量train_rights.append(right)
:将当前批次的正确预测数目添加到 train_rights 列表中
验证
model.eval()
:将模型设置为评估模式(禁用 dropout 和 batch normalization)val_rights = []
:初始化 val_rights 列表来存储验证集的准确率output = model(data)
:计算模型对测试集的预测right = accuracy(output, target)
:计算当前批次的正确预测数val_rights.append(right)
:将当前批次的预测结果添加到 val_rights
输出
三次epoch输出,测试集正确率相比单纯利用神经网络构建的97%要高一点。
1 | 当前epoch:1 训练进度:0% 损失:2.298599 训练集准确率: 14.06% 测试集正确率: 10.32% |
3.调用
1 | input_size = 28 # 图像的总尺寸28*28 |
损失函数
CrossEntropyLoss是PyTorch 中一个常用的损失函数,专门用于 分类任务,尤其是多类别分类。它结合了 Softmax 函数和 负对数似然损失(Negative Log-Likelihood Loss),用于处理分类任务中 概率输出 的问题。
适用范围:真实标签应该是一个 整数,表示每个样本的真实类别索引(而不是 one-hot 编码)
优化器
Adam 是一种常用的优化算法,全名为 Adaptive Moment Estimation,它结合了 Momentum 和 RMSprop 的优点,能够自动调整学习率来提高训练效果,Adam 优化器会通过计算一阶矩(梯度的均值)和二阶矩(梯度的平方均值)来动态调整每个参数的学习率,从而提高训练的效率和稳定性。
适用范围:一种自适应优化算法,非常适用于大多数任务,尤其是当模型参数多,训练过程复杂的时候。
5.总结
本文主要讲通过使用卷积神经网络来实现手写数字识别,相较于神经网络使用卷积神经网络更适合处理图像、视频、语音、文本等的数据,正确率也相对高一点。
6.备注
环境:
- mac: 15.2
- python: 3.12.4
- pytorch: 2.5.1
完整代码:
https://github.com/keychankc/dl_code_for_blog/tree/main/003_cnn_digital_recognition