摘要:实现对手写数字数据集的准确识别。在本文中使用经典的MNIST数据集作为实验对象,并构建了一个基于卷积神经网络(AlexNet)的模型。通过数据预处理和模型优化,达到了高准确率的识别结果。同时提供相关的代码示例,供读者参考和复现。
备注:通过介绍基于ALexNet的手写数字识别模型,教大家神经网络的基础以及如何通过网络结构复现代码等。
一、数据来源
1、数据集简介
本研究所使用的数据来自于MNIST手写数字数据集,这是一个经典的、广泛应用于机器学习和计算机视觉领域的数据集。MNIST数据集由Yann LeCun等人于1998年创建,旨在提供一个用于验证和比较机器学习算法性能的基准数据集。该数据集包含了大量的手写数字图像,总计有60000个训练样本和10000个测试样本。每个样本都是一个32x32像素的灰度图像(单通道图像),对应一个0到9之间的数字标签。下图为MNIST样例。
备注:数据集采用MNIST数据集,它包含了6万个训练样本和1万个测试样本。每个样本都是28x28x1像素的灰度图像即单通道图像,下面是数据集样本。
2、研究背景
手写数字识别在现实世界中有着广泛的应用。例如,在邮件服务中,自动识别手写邮政编码可以提高邮件分拣的效率。此外,手写数字识别还可以应用于银行支票识别、身份证号码识别等领域。准确地识别手写数字对于实现自动化和提高工作效率具有重要意义。因此,开发高性能的手写数字识别模型对于实际应用具有重要的实用价值。通过研究和实践,本研究旨在探索基于PyTorch的深度学习手写数字识别模型,为相关行业提供可靠的解决方案。
二、问题分析
1、问题描述
在手写数字识别任务中,我们面临的主要问题是如何准确地将手写数字图像分类为对应的数字标签。给定一个输入的手写数字图像,我们的目标是训练一个深度学习模型,使其能够对图像进行准确的分类,即将图像与正确的数字标签相匹配。
备注:手写数字识别,实际为分类模型,即输出一张图像,经过神经网络分类后得到该图像属于数字某个数字的最大概率。这里假设给神经网络输入的图像是5,经过神经网络后,得到维度为1×10的输出,即[0,0,0,0,0,1,0,0,0,0],其中索引6位置的数值最大,即神经网络得到了正确的分类结果。
2、 数据分析目标
- 实现对手写数字图像的高准确率分类,确保模型能够正确识别出每个图像所代表的数字。
- 确定在手写数字识别任务中可能出现的难点,例如数字的相似形状、模糊的图像边界等,以便在模型设计和训练过程中有针对性地解决这些问题。
- 选择适当的性能指标来评估模型的性能,例如准确率、精确率、召回率等,以便在模型求解过程中进行评估和对比。
三、基础知识
1、卷积与卷积核
神经网络的卷积过程是卷积神经网络(CNN)中的核心操作之一,用于从输入数据中提取特征。卷积是一种数学运算,它在神经网络中用于图像处理和其他二维数据(如声音信号或文本数据)的特征提取。卷积操作的基本思想是在输入数据上滑动一个小的窗口,称为卷积核或滤波器,以执行局部区域的点积操作。卷积核的参数是可学习的,它可以捕获输入数据中的不同特征。
备注:图像是由像素点构成的,彩色图像有三个通道,灰度图像有一个通道。对于三通道彩色图,卷积操作如上图所示,对应位置相乘再相加。
备注:卷积操作的一些相关概念。
以下是卷积操作的关键概念:
-
卷积核:卷积核是一个小的二维矩阵,它定义了如何从输入数据中提取特征。卷积核的大小通常是3x3或5x5,并且包含了权重值。
-
滑动窗口:卷积核在输入数据上滑动,从左上角开始,逐步向右和向下滑动。在每个位置,卷积核与输入数据的局部区域进行点积操作。
-
卷积操作:在每个滑动位置,卷积核与输入数据的局部区域进行点积操作,生成一个输出值。这个输出值代表了卷积核检测到的特定特征。
-
步幅(Stride):步幅定义了卷积核在输入数据上的移动步长。如果步幅为1,卷积核每次移动一个像素;如果步幅为2,卷积核每次移动两个像素。步幅的选择会影响输出的大小。
-
填充(Padding):填充是在输入数据周围添加额外的值(通常是0)以控制输出的大小。常见的填充方式有“有效填充”(valid padding,不添加额外值)和“相同填充”(same padding,添加额外值使输出与输入的大小相同)。
-
输出特征图:卷积操作的结果是一个输出特征图,它是输入数据上卷积核滑动的过程中生成的所有输出值的集合。每个输出值都代表了卷积核检测到的特征。
卷积操作的一个关键优势是它的局部感知性。卷积核只与输入数据的小局部区域进行交互,这使得神经网络能够自动学习局部特征,例如边缘、纹理等,而不需要关注整个输入图像的细节。在卷积神经网络中,通常有多个卷积层,它们逐渐提取越来越抽象的特征。这些特征在后续的层中被组合和用于最终的分类或回归任务。卷积神经网络的能力在图像处理、计算机视觉和其他领域中得到广泛应用,因为它可以高效地处理复杂的二维数据。
2、最大池化
备注:最大池化的目的是减少图像的空间维度,保留关键特征。
最大池化(Max Pooling)是一种常用的池化操作,通常用于卷积神经网络(CNN)中,以减小图像的空间维度(宽度和高度),同时保留图像的关键特征。最大池化的主要思想是在每个池化窗口中选择最大值作为输出,丢弃其他值。
备注:最大池化就是保留窗口中的最大值。
最大池化的工作过程如下:
-
定义一个固定大小的池化窗口(通常是2x2或3x3)。
-
将这个窗口从左上角开始在输入图像上滑动,每次滑动的步幅(stride)通常与窗口大小相同。
-
在每个窗口内,选择窗口内的最大值,并将其作为输出。
-
将输出的最大值组成的新特征图作为池化后的结果。
最大池化的优点包括:
- 减小特征图的尺寸,降低计算复杂度。
- 提取图像的关键特征,保留较显著的信息。
- 增强模型的平移不变性,使模型对目标物体的位置变化不敏感。
最大池化通常与卷积层交替使用,用于构建深度卷积神经网络。在卷积神经网络中,池化层有助于减少参数数量,降低过拟合的风险,并提高模型的计算效率。
3、ReLU激活函数
ReLU(Rectified Linear Unit,修正线性单元)是一种常用的激活函数,用于深度神经网络中的神经元。ReLU激活函数的定义如下:
备注:激活函数,修正线性单元。图像如上图所示。
f(x)=max(0,x)
其中,xx 是输入,f(x)f(x) 是激活函数的输出。
ReLU激活函数的主要特点包括:
-
非线性:尽管ReLU看起来很简单,但它是一个非线性激活函数。这意味着它可以帮助神经网络学习复杂的非线性关系。
-
稀疏激活性:当输入 xx 大于零时,激活值是 xx,而当输入小于等于零时,激活值是零。这种性质使得神经元具有稀疏激活性,即在正数输入时激活,负数输入时不激活。这有助于减少神经网络中的冗余信息。
-
解决梯度消失问题:相对于一些传统的激活函数(如Sigmoid和Tanh),ReLU在反向传播过程中更不容易出现梯度消失问题,因此更容易训练深度神经网络。
-
计算高效:ReLU的计算非常简单,只需要比较输入是否大于零,因此计算速度较快。
虽然ReLU激活函数在深度学习中取得了显著的成功,但它也存在一些问题,如可能出现神经元死亡问题(神经元在训练过程中可能永远不会被激活),为了克服这些问题,研究人员提出了一些ReLU的变种,如Leaky ReLU、Parametric ReLU(PReLU)、Exponential Linear Unit(ELU)等。这些变种在一定程度上改进了ReLU的性能,并用于各种深度学习任务。
4、损失函数
损失函数(也称为目标函数或代价函数)在机器学习和深度学习中起着至关重要的作用。它的主要作用如下:
-
衡量模型的性能:损失函数用于度量模型的预测与实际目标之间的差距。它提供了一个数值指标,用于评估模型的性能。优化的目标是最小化损失函数,使模型的预测尽量接近真实值。
-
指导参数优化:损失函数是训练过程中优化算法的关键部分。通过计算损失函数的梯度,可以确定参数更新的方向和幅度,以使损失函数减小。这是通过反向传播算法来实现的。
-
正则化:损失函数可以包括正则化项,帮助防止模型过拟合训练数据。正则化项通常包括L1正则化和L2正则化,它们惩罚模型的参数,使其趋向于较小的值,从而提高泛化性能。
-
类别不平衡处理:在分类任务中,损失函数可以被设计成对不同类别的错误分类赋予不同的惩罚,以处理类别不平衡问题。例如,交叉熵损失函数对错误分类的类别分配较大的损失。
-
自定义目标函数:根据具体的任务需求,可以设计自定义的损失函数,以满足特定的性能指标或任务要求。这可以根据问题的特点来灵活调整损失函数。
总之,损失函数在监督学习中扮演着关键角色,它帮助模型学习和优化,以便使其在给定任务上表现得更好。选择适当的损失函数对于模型的性能和泛化能力至关重要。不同的任务和问题可能需要不同的损失函数来达到最佳的训练和预测效果。
四、AlexNet简介
1、AlexNet基本结构
AlexNet输入为RGB三通道的224 × 224 × 3大小的图像。AlexNet 共包含5 个卷积层(包含3个池化)和 3 个全连接层。其中,每个卷积层都包含卷积核、偏置项、ReLU激活函数等模块。第1、2、5个卷积层后面都跟着一个最大池化层,后三个层为全连接层。最终输出层为softmax,将网络输出转化为概率值,用于预测图像的类别。
备注:AlexNet是一个非常简单的神经网络。官方代码中是 三通道的224 × 224 × 3大小的图像。
卷积操作中长和宽减少、维度增加的设计目的是通过逐渐提取和表示不同级别的特征来更好地理解图像,同时减少计算复杂性,并使网络对平移不变性更加敏感。这种层次化的特征表示有助于 CNN 在图像分类、物体检测、语义分割等计算机视觉任务中取得良好的性能。
1、卷积+池化层(前五层)
AlexNet共有五个卷积层,每个卷积层都包含卷积核、偏置项、ReLU激活函数。
卷积层C1:使用96个核对224 × 224 × 3的输入图像进行卷积,卷积核大小为11 × 11 × 3,步长为4。
卷积核大小(Filter Size)、步长(Stride)和输入输出特征图大小之间的关系可以通过以下公式来计算:输出特征图的大小(O)可以根据输入特征图的大小(I)、卷积核大小(K)、填充(P)和步长(S)来计算:
将一对55×55×48的特征图分别放入ReLU激活函数,生成激活图。激活后的图像进行最大池化,size为3×3,stride为2,池化后的特征图size为27×27×48。
卷积层C2:使用卷积层C1的输出(响应归一化和池化)作为输入,并使用256个卷积核进行滤波,核大小为5 × 5 × 48。
卷积层C3:有384个核,核大小为3 × 3 × 256,与卷积层C2的输出(归一化的,池化的)相连。
卷积层C4:有384个核,核大小为3 × 3 × 192。
卷积层C5:有256个核,核大小为3 × 3 × 192。卷积层C5与C3、C4层相比多了个池化,池化核size同样为3×3,stride为2。
其中,卷积层C3、C4、C5互相连接,中间没有接入池化层或归一化层。
2、全连接层(后三层)
全连接层F6:因为是全连接层,卷积核size为6×6×256,4096个卷积核生成4096个特征图,尺寸为1×1。然后放入ReLU函数、Dropout处理。值得注意的是AlexNet使用了Dropout层,以减少过拟合现象的发生。
全连接层F7:同F6层。
全连接层F8:最后一层全连接层的输出是1000维softmax的输入,softmax会产生1000个类别预测的值。
五、相关代码、代码复现
1、从导入模块开始
import torch # 导入PyTorch库
import torch.nn as nn # 导入神经网络模块
import torch.nn.functional as F # 导入函数模块
import torchvision.transforms as transforms # 导入数据转换模块
import torch.optim as optim # 导入优化器模块
这些导入语句是通常在PyTorch项目中使用的一些基本库的引用,用于构建和训练深度学习模型。在编写深度学习代码时,这些库和模块通常是必需的,因为它们提供了构建神经网络、计算损失和优化网络权重所需的工具和功能。
备注:这里面比较重要的就是以下两个模块。
1、torch.nn提供了许多用于创建神经网络层、损失函数、优化器等的类和函数。
2、torch.nn.functional提供了一系列的函数,用于执行各种神经网络相关的操作,如激活函数、损失函数、卷积、池化等。与 torch.nn
模块中的类不同,torch.nn.functional
中的函数通常是无状态的,不包含可训练的参数。这些函数以张量作为输入,并返回张量作为输出。
2、定义神经网络ALexNet
class AlexNet(nn.Module):
def __init__(self, width_mult=1):
super(AlexNet, self).__init__()
# 定义每一个卷积层和池化层
self.layer1 = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1), # 输入通道1,输出通道32,卷积核大小3x3,填充1
nn.MaxPool2d(kernel_size=2, stride=2), # 最大池化,池化核大小2x2,步幅2
nn.ReLU(inplace=True), # 使用ReLU激活函数
)
class AlexNet(nn.Module):
:定义了一个名为AlexNet
的PyTorch模型类,继承自nn.Module
。def __init__(self, width_mult=1):
:模型的初始化方法,可以传入一个可选的width_mult
参数,通道数。self.layer1 = nn.Sequential(...)
:创建了一个名为layer1
的卷积层序列。这个序列包含卷积层、池化层和ReLU激活函数。nn.Conv2d(1, 32, kernel_size=3, padding=1)
:创建一个卷积层,输入通道数为1(灰度图像),输出通道数为32,卷积核大小为3x3,填充为1,这一层会将输入图像变换为具有32个通道的特征图。nn.MaxPool2d(kernel_size=2, stride=2)
:创建一个最大池化层,池化核大小为2x2,步幅为2,用于减小特征图的分辨率。nn.ReLU(inplace=True)
:创建一个ReLU激活函数层,该层会在特征图上应用ReLU激活函数,激活函数的输出会替代输入中的负值。- 卷积核大小为3且填充为1的操作通常不会改变图像的尺寸,特别是在典型的卷积层中。这是因为填充(padding)的作用是在输入图像的周围添加额外的像素值,以确保卷积操作后输出特征图的空间尺寸与输入特征图相同。
- 在这里,
inplace=True
是一个参数,它表示是否在原地修改输入,也就是是否直接修改输入张量的值,而不是返回一个新的张量。当inplace=True
时,函数会修改输入张量的值,当inplace=False
时,函数会返回一个新的张量,而不会修改输入张量。
接下来的代码段中的self.layer2
、self.layer3
等部分也定义了类似的卷积、池化和激活函数层。它们用于构建AlexNet的卷积层部分。
self.layer2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.ReLU(inplace=True),
)
这段代码定义了layer2
,包括一个卷积层、池化层和ReLU激活函数,用于进一步提取特征。
self.layer3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
)
这段代码定义了layer3
,包括一个卷积层,用于进一步提取高级特征。
self.layer4 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, padding=1),
)
这段代码定义了layer4
,包括一个卷积层,用于进一步提取高级特征。
self.layer5 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.ReLU(inplace=True),
)
这段代码定义了layer5
,包括一个卷积层、池化层和ReLU激活函数,用于进一步提取特征。
接下来的代码段定义了全连接层:
# 定义全连接层
self.fc1 = nn.Linear(256 * 3 * 3, 1024) # 将最后一层卷积的输出展平,接入全连接层
self.fc2 = nn.Linear(1024, 512)
self.fc3 = nn.Linear(512, 10) # 10个输出,对应10个类别
nn.Linear(in_features, out_features)
:创建一个全连接层,in_features
表示输入特征的数量,out_features
表示输出特征的数量。
最后,forward
方法定义了模型的前向传播逻辑:
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.layer5(x)
x = x.view(-1, 256 * 3 * 3) # 将特征张量展平
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
x = self.layer1(x)
到x = self.layer5(x)
:将输入数据通过卷积层、池化层和ReLU激活函数传递,逐渐提取特征。x = x.view(-1, 256 * 3 * 3)
:将最后一层卷积的输出展平,以便将其输入到全连接层。x = self.fc1(x)
到x = self.fc3(x)
:将特征张量传递给全连接层,进行分类预测。
这个AlexNet
模型用于图像分类任务,通过一系列卷积和全连接层操作来提取图像特征并生成类别预测。这是一个经典的深度学习模型。
备注:构建神经网络首先要从forward函数开始,通过与AlexNet网络结构对比。forward应该包含……
3、训练代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torch.optim as optim
import torchvision.transforms as transforms
from alexnet import AlexNet
import cv2
from utils import plot_image,plot_curve,one_hot
这些导入语句是为了在后续的代码中使用这些库、模块和函数,以构建、训练和评估深度学习模型,特别是使用了自定义的 AlexNet
模型和一些辅助函数。导入所需的PyTorch库和自定义的AlexNet模型以及其他辅助工具。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
检查GPU是否可用,并将设备设置为GPU(如果可用),否则使用CPU。
epochs = 30 # 训练轮数
batch_size = 256 # 批次大小
lr = 0.01 # 学习率
设置超参数,包括训练轮数(epochs)、批次大小(batch_size)、学习率(lr)等。
# 创建训练数据加载器
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('mnist_data', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
# 数据归一化
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True
)
# 创建测试数据加载器
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('mnist_data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=256, shuffle=False
)
创建训练集和测试集的数据加载器(train_loader
和test_loader
),这些数据加载器用于加载MNIST数据集,并将数据进行归一化处理。
# 定义损失函数为交叉熵损失
criterion = nn.CrossEntropyLoss()
定义损失函数为交叉熵损失(nn.CrossEntropyLoss()
)。
交叉熵损失函数(Cross-Entropy Loss),也称为对数损失函数(Logarithmic Loss),通常用于分类问题中,特别是多类别分类问题。它在深度学习中被广泛应用,是分类任务的一种常见损失函数。交叉熵损失函数的公式如下:
损失函数是训练过程中优化算法的关键部分。通过计算损失函数的梯度,可以确定参数更新的方向和幅度,以使损失函数减小。这是通过反向传播算法来实现的。
神经网络训练的目的就是使损失函数的损失值最小。
# 创建AlexNet模型,并将其移动到GPU(如果可用)
net = AlexNet().to(device)
创建AlexNet模型(net
),并将其移动到设备(GPU或CPU)上。
# 定义优化器为随机梯度下降(SGD)
optimzer = optim.SGD(net.parameters(), lr=lr, momentum=0.9)
定义优化器为随机梯度下降(SGD)优化器,用于更新模型的权重。
# 训练循环
train_loss = []
for epoch in range(epochs):
sum_loss = 0.0
for batch_idx, (x, y) in enumerate(train_loader):
print(x.shape)
x = x.to(device) # 将输入数据移动到GPU
y = y.to(device) # 将标签数据移动到GPU
# 清零优化器的梯度
optimzer.zero_grad()
pred = net(x) # 前向传播
loss = criterion(pred, y) # 计算损失
loss.backward() # 反向传播
optimzer.step() # 更新模型参数
train_loss.append(loss.item())
sum_loss += loss.item()
if batch_idx % 100 == 99:
print('[%d, %d] loss: %.03f'
% (epoch + 1, batch_idx + 1, sum_loss / 100))
sum_loss = 0.0
# 保存训练好的模型参数到文件
torch.save(net.state_dict(), '/home/lwf/code/pytorch学习/alexnet图像分类/model/model.pth')
# 绘制训练损失曲线
plot_curve(train_loss)
开始训练循环(for epoch in range(epochs):
):
a. 在每个epoch内,循环遍历训练数据批次。
b. 将输入数据(x
)和标签(y
)移动到设备上。
c. 清零优化器的梯度。
d. 将输入数据通过模型进行前向传播,得到预测值(pred
)。
e. 计算预测值与真实标签之间的损失。
f. 反向传播梯度并更新模型参数。
g. 记录训练损失到train_loss
列表中,并在每100个批次后打印一次损失。
最后,保存训练好的模型参数到文件(model.pth
)。
调用plot_curve
函数,绘制训练损失曲线。