用PyTorch构建基于卷积神经网络的手写数字识别模型
Posted Mr.长安
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用PyTorch构建基于卷积神经网络的手写数字识别模型相关的知识,希望对你有一定的参考价值。
本文参加新星计划人工智能(Pytorch)赛道:https://bbs.csdn.net/topics/613989052
目录
一、MINST数据集介绍与分析
MINST数据库是机器学习领域非常经典的一个数据集,其由Yann提供的手写数字数据集构成,包含了0-9共10类手写数字图片,每张图片都做了尺寸归一化,都是28x28大小的灰度图。每张图片中像素值大小在0-255之间,其中0是黑色背景,255是白色前景。
编写程序导入数据集并展示如下所示:
from sklearn.datasets import fetch_mldata
from matplotlib import pyplot as plt
mnist = fetch_mldata('MNIST original', data_home='./dataset')
X, y = mnist["data"], mnist["target"]
print("MNIST数据集大小为:".format(X.shape))
for i in range(25):
digit = X[i * 2500]
# 将图片重新resize到28*28大小
digit_image = digit.reshape(28, 28)
plt.subplot(5, 5, i + 1)
# 隐藏坐标轴
plt.axis('off')
# 按灰度图绘制图片
plt.imshow(digit_image, cmap='gray')
plt.show()
在控制台可以看到的输出为:MNIST数据集大小为:(70000, 784)。一共有70000张数字,且784=28*28,即每一张手写数字图片存成了一维的数据格式。
可视化前25张图片以及中间的数据可得如图所示:
手写数字的识别是一个多分类任务,一张手写数字图片的特征提取任务也需要我们自己实现,将28*28的图片直接序列化为784维的向量也是一种特征提取的方式,但经过一些处理,可以获得更反映出图片内容的信息,例如使用在原图中使用sift、surf等算子后的特征,或者使用最新的一些深度学习预训练模型来提取特征。MNIST数据集样例数目较多且为图片信息,近些年随着深度学习技术的发展,对于大多数视觉任务,通过构造并训练卷积神经网络可以获得更高的准确率,本项目将基于PyTorch框架完成网络的训练以及识别的任务。
二、卷积神经网络
卷积神经网络(CNN)是深度神经网络中的一种,其受生物视觉认知机制启发而来,神经元之间使用类似动物视觉皮层组织的链接方式,大多数情况下用于处理计算机视觉相关的任务,例如分类、分割、检测等。与传统方法相比较,卷积神经网络不需要利用先验知识进行特征设计,预处理步骤较少,在大多数视觉相关任务上获得了不错的效果。卷积神经网络最先出现于20世纪80年代到90年代,LeCun提出了LeNet用于解决手写数字识别的问题,随着深度学习理论的不断完善,计算机硬件水平的提高,卷积神经网络也随之快速发展。
卷积神经网络通常由一个输入层(Input Layer)和一个输出层(Output Layer)以及多个隐藏层组成。隐藏包括卷积层(Convolutional Layer)、激活层(Activation Layer)、池化层(Pooling Layer)以及全连接层(Fully-connected Layer)等。如上图所示为一个LeNet神经网络的结构,目前大多数研究者针对于不同任务对层或网络结构进行设置,从而获得更优的效果。
卷积神经网络的输入层可以对多维数据进行处理,常见的二维卷积神经网络可以接受二维或三维数据作为输入,对于图片类任务,一张RGB图片作为输入的大小可写为C×H×W,C为通道数,H为长,W为宽。对于视频识别类任务,一段视频作为输入的大小可写为T×C×H×W,T为视频帧的数目,对于三维重建任务,一个三维体素模型,其作为输入的大小可写为1×H×L×W,H、L、W分别为模型的高、长、宽。与其他神经网络算法相似,在训练时会使用梯度下降法对参数进行更新,因此所有的输入都需要进行在通道或时间维度归一化或标准化的预处理过程。归一化是通过计算极值将所有样本的特征值映射到之间。而标准化是通过计算均值、方差将数据分布转化为标准正态分布,本项目中所有的数据预处理均使用标准化的方法。
卷积层是卷积神经网络所特有的一种子结构,一个卷积层包含多个卷积核,卷积核在输入数据上进行卷积计算从而提取得到特征。在前向传播中,如图11-13所示,中间为一个3×3的卷积核,卷积核在输入上进行滑动,每次滑动都计算逐像素相乘再相加的结果,作为输出特征上某一点的值,一个卷积操作一般由四个超参数组成,卷积核大小F(kernel size),步长S(stride),填充P(padding),以及卷积核数目C(number ofnels),具体来说,假设输入的特征大小为N×W×H,则输出特征的维度W'、H'以及N'为:
激活层有Sigmoid、ReLU、Tanh等常用的激活函数可供使用, 如下图所示:
池化层一般包括两种,一种是平均池化层(Average Pooling)、另一种是最大值池化(Max Pooling),池化层可以起到保留主要特征,减少下一层的参数量和计算量的作用,从而防止过拟合风险。
全连接层一般用于分类网络最后面,起到类似于“分类器”的作用,将数据的特征映射到样本标记特征,相比卷积层的某一位置的输出仅与上一层中相邻位置有关,全连接层中每一个神经元都会与前一层的所有神经元有关,因此全连接层的层数量也是很大的。
归一化层包括了BatchNorm, LayerNorm, InstanceNorm, GroupNorm等方法,本项目仅使用了BatchNorm。BatchNorm在batch的维度上进行归一化,使得深度网络中间卷积的结果也满足正态分布,整个训练过程更快,网络更容易收敛。
前面介绍的这些部件组合起来就能构成一个深度学习的分类器,基于大量的训练集从而在某些任务上可以获得与人类相当准确性,科学家们也在不断实践如何去构建一个深度学习的网络,如何设计并搭配这些部件,从而获得更优异的分类性能,下面是较为经典的一些网络结构,甚至其中有一些依旧活跃在科研的一线。
LeNet卷积神经网络由LeCun在1998年提出,这个网络仅由两个卷积层、两个池化层以及两个全连接层组成,在当时用以解决手写数字识别的任务,也是早期最具有代表性的卷积神经网络之一,同时也奠定了卷积神经网络的基础架构,包含了卷积层、池化层、全连接层。
2012年,Alex提出的Alexnet在ImageNet比赛上取得了冠军,其正确率远超第二名。AlexNet成功使用Relu作为激活函数,并验证了在较深的网络上,Relu效果好于Sigmoid,同时成功实现在GPU上加速卷积神经网络的训练过程。另外Alex在训练中使用了dropout和数据扩增以防止过拟合的发生,这些处理成为后续许多工作的基本流程。为从而开启了深度学习在计算机视觉领域的新一轮爆发。
GoogleNet,2014年ImageNet比赛的冠军模型,证明了使用更多的卷积层可以得到更好的结果。其巧妙地在不同的深度增加了两个损失函数来保证梯度在反向传播时不会消失。
VGGNet是牛津大学计算机视觉组和Google DeepMind公司的研究员一起研发的深度卷积神经网络。他探索了卷积神经网络的性能与深度的关系,通过不断叠加3×3的卷积核与2×2的最大池化层,从而成功构建了一个16到19层深的卷积神经网络,并大幅下降了错误率。虽然VGGNet简化了卷积神经网络的结构,但训练中的需要更新的参数量依旧非常巨大。
虽然卷积深度的不断上升会带来效果的提升,但当深度超过一定数目后又会引入新的问题,即梯度消失的现象出现的越来越明显,反而导致无法提升网络的效果。ResNet提出了残差模块来解决这一问题,允许原始信息可以直接输入到后面的层之中。传统的卷积层或全连接层在进行信息传递时,每一层只能接受其上一层的信息,导致可能会存在信息丢失的问题,ResNet在一定程度上缓解了该问题,通过残差的方式,提供了让信息从输入传到输出的途径,保证了信息的完整性。
使用深度模型时需要注意的一点在于由于模型参数较多,因此要求数据集也不能太小,否则会出现过拟合的现象,还有一种使用深度模型的方法是,使用在ImageNet上预训练好的模型,固定除了全连接层外所有的参数,只在当前数据集下训练全连接层参数,这种方式可以大大减小训练的参数量,使深度模型在较小的数据集上也能得到应用。
三、基于卷积神经网络的手写数字识别
前面已经介绍了几种经典的卷积神经网络模型,MNIST数据集中图片的尺寸仅为28*28,相比ImageNet中224*224的图片尺寸显得十分小,因此在模型的选取上,不能选择太过于复杂,参数量过多的模型,否则会带来过拟合的风险,本项目自定义了一个仅包含2个卷积层的卷积神经网络以及经过一些调整的AlexNet。首先是定义网络的类,该类在mnist_models.py内,继承了torch.nn.Module类,并需要重新实现forword函数,即一张图作为输入,如何通过卷积层得到最后的输出。
class ConvNet(torch.nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(1, 10, 5, 1, 1),
torch.nn.MaxPool2d(2),
torch.nn.ReLU(),
torch.nn.BatchNorm2d(10)
)
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(10, 20, 5, 1, 1),
torch.nn.MaxPool2d(2),
torch.nn.ReLU(),
torch.nn.BatchNorm2d(20)
)
self.fc1 = torch.nn.Sequential(
torch.nn.Linear(500, 60),
torch.nn.Dropout(0.5),
torch.nn.ReLU()
)
self.fc2 = torch.nn.Sequential(
torch.nn.Linear(60, 20),
torch.nn.Dropout(0.5),
torch.nn.ReLU()
)
self.fc3 = torch.nn.Linear(20, 10)
如上面的代码块所示,在构造函数中,定义了网络的结构,主要包含了两个卷积层以及三个全连接层的参数设置。
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(-1, 500)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
接下来在forward函数中x为该网络的输入,经过前面定义的网络结构按顺序进行计算后,返回结果。同样,可以定义AlexNet的网络结构以及forword函数如下所示:
class AlexNet(torch.nn.Module):
def __init__(self, num_classes=10):
super(AlexNet, self).__init__()
self.features = torch.nn.Sequential(
torch.nn.Conv2d(1, 64, kernel_size=5, stride=1, padding=2),
torch.nn.ReLU(inplace=True),
torch.nn.MaxPool2d(kernel_size=3, stride=1),
torch.nn.Conv2d(64, 192, kernel_size=3, padding=2),
torch.nn.ReLU(inplace=True),
torch.nn.MaxPool2d(kernel_size=3, stride=2),
torch.nn.Conv2d(192, 384, kernel_size=3, padding=1),
torch.nn.ReLU(inplace=True),
torch.nn.Conv2d(384, 256, kernel_size=3, padding=1),
torch.nn.ReLU(inplace=True),
torch.nn.Conv2d(256, 256, kernel_size=3, padding=1),
torch.nn.ReLU(inplace=True),
torch.nn.MaxPool2d(kernel_size=3, stride=2),
)
self.classifier = torch.nn.Sequential(
torch.nn.Dropout(),
torch.nn.Linear(256 * 6 * 6, 4096),
torch.nn.ReLU(inplace=True),
torch.nn.Dropout(),
torch.nn.Linear(4096, 4096),
torch.nn.ReLU(inplace=True),
torch.nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), 256 * 6 * 6)
x = self.classifier(x)
return x
定义完网络结构后,新建一个新的.py脚本完成网络训练和预测的过程。一般来说一个Pytorch项目主要包含几大模块,数据集加载、模型定义及加载、损失函数以及优化方法设置,训练模型,打印训练中间结果,测试模型。对于MNIST这样小型的项目,可以将除了数据集加载和模型定义外所有的代码使用一个函数实现。首先是加载相应的包以及设置超参数,EPOCHS指在数据集上训练多少个轮次,而SAVE_PATH指中间以及最终模型保存的路径。
import torch
from torchvision.datasets import mnist
from mnist_models import AlexNet, ConvNet
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
from torch.autograd import Variable
# 设置模型超参数
EPOCHS = 50
SAVE_PATH = './models'
核心训练函数以模型、训练集、测试集作为输入。首先定义损失函数为交叉熵函数以及优化方法选取了SGD,初始学习率为1E-2。
def train_net(net, train_data, test_data):
losses = []
acces = []
# 测试集上Loss变化记录
eval_losses = []
eval_acces = []
# 损失函数设置为交叉熵函数
criterion = torch.nn.CrossEntropyLoss()
# 优化方法选用SGD,初始学习率为1e-2
optimizer = torch.optim.SGD(net.parameters(), 1e-2)
接下来,一共有50个训练轮次,使用for循环实现,在训练过程中记录在训练集以及测试集上Loss以及Acc的变化情况。在训练过程中,net.train()是指将网络前向传播的过程设为训练状态,在类似Droupout以及归一化层中,对于训练和测试的处理过程是不一样的,因此每次进行训练或测试时,最好显式的进行设置,防止出现一些意料之外的错误。
for e in range(EPOCHS):
train_loss = 0
train_acc = 0
# 将网络设置为训练模型
net.train()
for image, label in train_data:
image = Variable(image)
label = Variable(label)
# 前向传播
out = net(image)
loss = criterion(out, label)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录误差
train_loss += loss.data
# 计算分类的准确率
_, pred = out.max(1)
num_correct = (np.array(pred, dtype=np.int) == np.array(label, dtype=np.int)).sum()
acc = num_correct / image.shape[0]
train_acc += acc
losses.append(train_loss / len(train_data))
acces.append(train_acc / len(train_data))
# 在测试集上检验效果
eval_loss = 0
eval_acc = 0
net.eval() # 将模型改为预测模式
for image, label in test_data:
image = Variable(image)
label = Variable(label)
out = net(image)
loss = criterion(out, label)
# 记录误差
eval_loss += loss.data
# 记录准确率
_, pred = out.max(1)
num_correct = (np.array(pred, dtype=np.int) == np.array(label, dtype=np.int)).sum()
acc = num_correct / image.shape[0]
eval_acc += acc
eval_losses.append(eval_loss / len(test_data))
eval_acces.append(eval_acc / len(test_data))
print('epoch: , Train Loss: :.6f, Train Acc: :.6f, Eval Loss: :.6f, Eval Acc: :.6f'
.format(e, train_loss / len(train_data), train_acc / len(train_data),
eval_loss / len(test_data), eval_acc / len(test_data)))
torch.save(net.state_dict(), SAVE_PATH + '/Alex_model_epoch' + str(e) + '.pkl')
return eval_losses, eval_acces
在训练集上训练完一个轮次之后,在测试集上进行验证,并记录结果,保存模型参数,并打印数据,方便后续进行调参。训练完成后返回测试集上Acc和Loss的变化情况。
最后完成Loss和Acc变化曲线的绘制函数以及主函数main如下所示:
if __name__ == "__main__":
train_set = mnist.MNIST('./data', train=True, download=True, transform=transforms.ToTensor())
test_set = mnist.MNIST('./data', train=False, download=True, transform=transforms.ToTensor())
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
test_data = DataLoader(test_set, batch_size=64, shuffle=False)
a, a_label = next(iter(train_data))
net = AlexNet()
eval_losses, eval_acces = train_net(net, train_data, test_data)
draw_result(eval_losses, eval_acces)
def draw_result(eval_losses, eval_acces):
x = range(1, EPOCHS + 1)
fig, left_axis = plt.subplots()
p1, = left_axis.plot(x, eval_losses, 'ro-')
right_axis = left_axis.twinx()
p2, = right_axis.plot(x, eval_acces, 'bo-')
plt.xticks(x, rotation=0)
# 设置左坐标轴以及右坐标轴的范围、精度
left_axis.set_ylim(0, 0.5)
left_axis.set_yticks(np.arange(0, 0.5, 0.1))
right_axis.set_ylim(0.9, 1.01)
right_axis.set_yticks(np.arange(0.9, 1.01, 0.02))
# 设置坐标及标题的大小、颜色
left_axis.set_xlabel('Labels')
left_axis.set_ylabel('Loss', color='r')
left_axis.tick_params(axis='y', colors='r')
right_axis.set_ylabel('Accuracy', color='b')
right_axis.tick_params(axis='y', colors='b')
plt.show()
运行脚本,等待控制台逐渐输出训练过程的中间结果如下图所示,随着训练的进行,可以发现在测试集上分类的正确率不断上升且Loss稳步下降,到第20轮左右后,正确率基本不再变化,网络收敛。
【小技巧】在进行深度学习方法进行训练时,一定要将中间结果打印出来,因为模型训练往往会比较慢,如果中间感到哪里不对时可以及时停止,节省时间,另外,训练的中间模型一定要保存下来!
等待程序运行结束,可以得到绘制结果如下图所示,最终分类正确率可达99.1%左右。
卷积神经网络 手写数字识别(包含Pytorch实现代码)
Hello!欢迎来到六个核桃Lu!
运用卷积神经网络 实现手写数字识别
1 算法分析及设计
卷积神经网络:
图1-2
如图1-2,卷积神经网络由若干个方块盒子构成,盒子从左到右仿佛越来越小,但却越来越厚;最左边是一张图像,最右边则变成了两排园圈。其实,每—个方块都是由大量神经元细胞构成的,只不过它们排成了立方体的形状。左边图像上的每个元素相当于一个神经元细胞,构成了这个卷积神经网络的输入单元。最右侧的圆圈也是神经元细胞,它们排列成了两条直线,构成了该网络的输出,这与普通神经网络中的细胞没有区别。
卷积神经网络其实也是一个前馈神经网络,承载了深层的信息处理过程。信息从左侧输人一层一层加工处理,最后到右侧输出。对图像分类任务而言,输人的是一张图像,历经一系列卷积层、池化层和完全连接层的运算,最终得到输出,输出;是一组分类的概率,要分成多少类别就有多少个输出层神经元。相邻两层的神经元连按用图中的小立体锥形近似表示,实际上这种锥形遍布更高一层(右侧)立方体中的所有神经元。低层(左侧)到高层(右侧)的运算主要分为两大类:卷积和池化。一层卷积,一层池化,这两种运算交替进行,直到最后一层,我们又把立方体中的神经元拉平成了线性排列的神经元,与最后的输出层进行全连接。
2 程序设计
2.1 手写数字识别任务的卷积神经网络及运算过程
灰度图是计算机中最简单的一种图像,如图2-1,数字8栅格图像和图像的二维矩 阵数据。数字8的灰度图图像只有明暗的区别,图像的二维矩阵数据都在0~255之间,只用一个数字表示出不同的灰度,0表示最明亮的白色,255表示最暗的黑色,介于0~255之间的整数则表示不同明暗程序的灰色。
图2-1
在计算机中,这张输人的图像被表示成了一个尺寸为(28,28)的张量(一个28行28列的矩阵),其中张量的任意一个元素都是一个0~255的数宇,表示这一个像素点的灰度值,越接近255,这个点就会越白。这些输人像素点就自然构成了卷积神经网络的输人层神经元细胞,因此,输人层神经元排布成了一个正方形。
为了完成这个手写数字识别任务,我们设计了如图2-2所示架构的卷积神经网络(在具体设计网络架构的时候,网络有名少层,每一层有多少神经元,这些都可以作为超参数而重新选择)。
图2-2
整个架构可以分为两大部分,第一部分是由输人图像和4个立方体构成的图像处理部分,在这部分中,图像被不断加工成尺寸更小、数量更多的图像;第二部分则是由一系列线性排布的神经元构成的普通前馈多层神经网络。
首先,输入图像经过一层卷积操作,变成了图中第一个立方体,尺寸为(28,28,4)。实际上这是4张28×28像素的图像;之后,这些图像经过池化的操作,尺寸缩小了一半,变成了4张14×14像素的小图,排布成了厚度为4、长宽为14的立方体;之后,这些图像又经历了一次卷积运算,变成了8张图像,尺寸仍然为14×14像素;最后,这8张图像又经历了一次池化操作,尺寸又变小一半,成为8张7×7像素的小图片。
至此,第一部分的图像操作完成,下面就要进入第二部分。首先,我们将392(8×7×7)像素的神经元拉伸为一个长度为392的向量,这些神经元构成了前馈网络部分的输入单元,之后经过一层隐含层,再映射到输出层单元,输出10个(0,1)区间中的小数,表示隶属于0~9这10个数字的概率,且这些数字加起来等于1。最后,我们再选取最大的数值所对应的数字,作为最后的分类输出所有的卷积、池化运算都是依靠两层之问的神经元连按完成的(在图2-2中,这些连接表示为小立方锥体),这些连接与普通的前馈神经网络的连接并无本质区别,也对应了一组权重值。用这组权重值乘以相应的输入神经元就得到了计算结果。同理,第二部分网络的一层层运算也是由层与层之问的神经元连接完成的,它们也有相应的权重。
整个卷积神经网络的运作分成了两个阶段:前馈运算阶段和反馈学习阶段。在网络的前馈阶段(从输人图像到输出数字),所有连接的权重值都不改变,系统会根据输人图像计算输出分类,并根据网络的分类与数据中的标签(标准答案)进行比较计算出交叉熵作为损失函数。接下来,在反馈阶段,根据前馈阶段的损失两数调整所有连接上的权重值,从而完成神经网络的学习过程。
4 总结
首先,卷积运算可以实现各种各样的图像操作。我们知道,绝大部分的图像处理操作,诸如锐化图像(强调细节)、模糊图像(减少细节)都可以看作某种特定权重的卷积核在原始图像上的卷积操作。换向话说,这些操作都是可被卷积神经网络学习到的。于是,学好的卷积核就能对图像进行去噪、提炼等信息过滤和提取的工作。
其次,卷积神经网络中的池化操作可以提取大尺度特征。池化操作就是忽略掉图像中的细节信息,从而保留、提炼出;图像中的大尺度信息。这些信息可以帮助卷积神经网络从鉴体把握图像的分类。
卷积操作虽然与普通的神经网络运算非常相似,只不过卷积核会沿着输人图像平移;池化操作则将图像压缩。因此,看起来普通神经网络的反向传播BP算法并不能直接用在卷积和化操作上,而需要给出适合它们的特殊BP算法。
所幸,卷积与池化以及构建卷积神经网络过程中的所有计算都是可微分的,可以利用PyTorch的动态计算图调用backward两数自动计算出每个参数的梯度,并最终完成BP算法。
所谓的卷积操作,其实可以看作一种模板匹配的过程。卷积核就是模板,特征图则是这个模板匹配的结果显示。池化操作则是一种对原始图像进行更大尺度的特征提取过程,它可以提炼出数据中的高尺度信息。将卷积和池化交替组装成多层的卷积神经网络模型,便有了强大的多尺度特征提取能力。
参考资料:
- 集智俱乐部. 《深度学习原理与PyTorch》. 人民邮电出版社
- https://www.bilibili.com/video/BV1T64y167fK?p=13
- https://blog.csdn.net/Bokman/article/details/109549191
- https://blog.csdn.net/qq_41503660/article/details/103833961
- https://zhuanlan.zhihu.com/p/139052035
- Ian Goodfellow,Yoshua Bengio. 《深度学习》. 人民邮电出版社
——代码——
import torch
import torch.nn as nn
from torch. autograd import Variable
import torch.optim as optim
import torch.nn. functional as F
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt # %matplotlib inline 可以让Jupyter Notebook直接输出图像
import pylab # 但我写上%matplotlib inline就报错 所以我就用了pylab.show()函数显示图像
# 接着定义一些训练用的超参数:
image_size = 28 # 图像的总尺寸为 28x28
num_classes = 10 # 标签的种类数
num_epochs = 20 # 训练的总猜环周期
batch_size = 64 # 一个批次的大小,64张图片
'''______________________________开始获取数据的过程______________________________'''
# 加载MNIST数据 MNIST数据属于 torchvision 包自带的数据,可以直接接调用
# 当用户想调用自己的图俱数据时,可以用torchvision.datasets.ImageFolder或torch.utils.data. TensorDataset来加载
train_dataset = dsets.MNIST(root='./data', # 文件存放路径
train=True, # 提取训练集
# 将图像转化为 Tensor,在加载數据时,就可以对图像做预处理
transform=transforms.ToTensor(),
download=True) # 当找不到文件的时候,自动下載
# 加载测试数据集
test_dataset = dsets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor())
# 训练数据集的加载器,自动将数据切分成批,顺序随机打乱
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
'''
将测试数据分成两部分,一部分作为校验数据,一部分作为测试数据。
校验数据用于检测模型是否过拟合并调整参数,测试数据检验整个模型的工作
'''
# 首先,定义下标数组 indices,它相当于对所有test_dataset 中数据的编码
# 然后,定义下标 indices_val 表示校验集数据的下标,indices_test 表示测试集的下标
indices = range(len(test_dataset))
indices_val = indices[: 5000]
indices_test = indices[5000:]
# 根据下标构造两个数据集的SubsetRandomSampler 来样器,它会对下标进行来样
sampler_val = torch.utils.data.sampler.SubsetRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler. SubsetRandomSampler(indices_test)
# 根据两个采样器定义加载器
# 注意将sampler_val 和sampler_test 分别賦值给了 validation_loader 和 test_loader
validation_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False,
sampler=sampler_val)
test_loader = torch. utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False,
sampler=sampler_test)
# 随便从数据集中读入一张图片,并绘制出来
idx = 70 # random.randint(1, 100)
# dataset支特下标索引,其中提取出来的元素为 features、target 格式,即属性和标签。[0]表示索引 features
muteimg = train_dataset[idx][0].numpy()
# 一般的图像包含RGB 这了个通道,而 MNIST 数据集的因像都是交度的,只有一个通道
# 因此,我们忽略通道,把图像看作一个灰度矩阵
# 用 imshow 画图,会将交度矩阵自动展现为彩色,不同灰度对应不同的颜色:从黄到紫
plt.imshow(muteimg[0, ...])
plt.title(": ".format('image sample', train_dataset[idx][1])) # 显示获取到的图片的标签
pylab.show()
'''______________________________获取数据的过程完成______________________________'''
'''______________________________开始构建网络的过程______________________________'''
# 定义卷积神经网络:4和8为人为指定的两个卷积层的厚度(feature map的数量)
depth = [4, 8]
class ConvNet(nn.Module):
def __init__(self):
# 该函数在创建一个ConvNet对象即调用语句net=ConvNet()时就会被调用
# 首先调用父类相应的构造函数
super(ConvNet, self).__init__()
# 其次构造ConvNet需要用到的各个神经模块
# 注意,定义组件并不是卖正搭建组件,只是把基本建筑砖块先找好
self.conv1 = nn. Conv2d(1, 4, 5, padding=2) # 定义一个卷积层,输入通道为1,输出通道为4,窗口大小为5,padding为2
self.pool = nn.MaxPool2d(2, 2) # 定义一个池化层,一个窗口为2x2的池化运箅
# 第二层卷积,输入通道为depth[o],输出通道为depth[2],窗口为 5,padding 为2
self.conv2 = nn. Conv2d(depth[0], depth[1], 5, padding=2) # 输出通道为depth[1],窗口为5,padding为2
# 一个线性连接层,输入尺寸为最后一层立方体的线性平铺,输出层 512个节点
self.fc1 = nn. Linear(image_size // 4 * image_size // 4 * depth[1], 512)
self. fc2 = nn. Linear(512, num_classes) # 最后一层线性分类单元,输入为 512,输出为要做分类的类别数
def forward(self, x): # 该函数完成神经网络真正的前向运算,在这里把各个组件进行实际的拼装
# x的尺寸:(batch_size, image_channels, image_width, image_height)
x = self.conv1(x) # 第一层卷积
x = F.relu(x) # 激活函数用ReLU,防止过拟合
# x的尺寸:(batch_size, num_filters, image_width, image_height)
x = self.pool(x) # 第二层池化,将图片变小
# x的尺寸:(batch_size, depth[0], image_width/ 2, image_height/2)
x = self.conv2(x) # 第三层又是卷积,窗口为5,输入输出通道分列为depth[o]=4,depth[1]=8
x = F.relu(x) # 非线性函数
# x的尺寸:(batch_size, depth[1], image_width/2, image_height/2)
x = self.pool(x) # 第四层池化,将图片缩小到原来的 1/4
# x的尺寸:(batch_size, depth[1], image_width/ 4, image_height/4)
# 将立体的特征图 tensor 压成一个一维的向量
# view 函数可以将一个tensor 按指定的方式重新排布
# 下面这个命令就是要让x按照batch_size * (image_ size//4)^2*depth[1]的方式来排布向量
x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
# x的尺寸:(batch_ size, depth[1J*image width/4*image height/4)
x = F.relu(self.fc1(x)) # 第五层为全连接,ReLU激活函数
# x的尺才:(batch_size, 512)
# 以默认0.5的概率对这一层进行dropout操作,防止过拟合
x = F.dropout(x, training=self.training)
x = self.fc2(x) # 全连接
# X的尺寸:(batch_size, num_classes)
# 输出层为 log_Softmax,即概率对数值 log(p(×))。采用log_softmax可以使后面的交叉熵计算更快
x = F.log_softmax(x, dim=1)
return x
def retrieve_features(self, x):
# 该函数用于提取卷积神经网络的特征图,返回feature_map1,feature_map2为前两层卷积层的特征图
feature_map1 = F.relu(self.conv1(x)) # 完成第一层卷积
x = self.pool(feature_map1) # 完成第一层池化
# 第二层卷积,两层特征图都存储到了 feature_map1,feature map2 中
feature_map2 = F.relu(self.conv2(x))
return (feature_map1, feature_map2)
'''______________________________构造网络的过程完成______________________________'''
'''________________________________开始训练的过程________________________________'''
net = ConvNet() # 新建一个卷积神经网络的实例。此时convNet的__init()__函数会被自动调用
criterion = nn.CrossEntropyLoss() # Loss 函数的定义,交叉熵
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # 定义优化器,普通的随机梯度下降算法
record = [] # 记录准确率等数值的容器
weights = [] # 每若干步就记录一次卷积核
# 开始训练循环
def rightness(output, target):
preds = output.data.max(dim=1, keepdim=True)[1]
return preds.eq(target.data.view_as(preds)).cpu().sum(), len(target)
for epoch in range(num_epochs):
train_rights = [] # 记录训练数据集准确率的容器
'''
下面的enumerate起到构道一个枚举器的作用。在对train_loader做循环选代时,enumerate会自动输出一个数宇指示循环了几次,
并记录在batch_idx中,它就等于0,1,2,...
train_loader 每选代一次,就会输出一对数据data和target,分别对应一个批中的手写数宇图及对应的标签。
'''
for batch_idx, (data, target) in enumerate(train_loader): # 针对容器中的每一个批进行循环
# 将 Tensor 转化为 Variable, data 为一批图像,target 为一批标签
data, target = Variable(data), Variable(target)
# 给网络模型做标记,标志着模型在训练集上训练
# 这种区分主要是为了打开关闭net的training标志,从而决定是否运行dropout
net.train()
output = net(data) # 神经网络完成一次前馈的计算过程,得到预测输出output
loss = criterion(output, target) # 将output与标签target比较,计算误差
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 一步随机梯度下降算法
right = rightness(output, target) # 计算准确率所需数值,返回数值为(正确样例数,总样本数)
train_rights.append(right) # 将计算结果装到列表容器train_rights中
if batch_idx % 100 == 0: # 每间隔100个batch 执行一次打印操作
net.eval() # 给网络楧型做标记,标志着模型在训练集上训练
val_rights = [] # 记录校验数据集准确率的容器
# 开始在校验集上做循环,计算校验集上的准确度
for (data, target) in validation_loader:
data, target = Variable(data), Variable(target)
# 完成一次前馈计算过程,得到目前训练得到的模型net在校验集上的表现
output = net(data)
# 计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
right = rightness(output, target)
val_rights.append(right)
# 分别计算目前已经计算过的测试集以及全部校验集上模型的表现:分类准确率
# train_r为一个二元组,分别记录经历过的所有训练集中分类正确的数量和该集合中总的样本数
# train_r[0]/train_r[1]是训练集的分类准殖度,val_[0]/val_r[1]是校验集的分类准确度
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
# val_r为一个二元组,分别记录校验集中分类正确的数量和该集合中总的样本数
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
# 打印准确率等数值,其中正确率为本训练周期epoch 开始后到目前批的正确率的平均值
print(val_r)
print('训练周期: [/ (:.0f%)]\\tLoss: :.6f\\t训练正确率: :.2f%\\t校验正确率: :.2f%'.format(
epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.data,
100. * train_r[0].numpy() / train_r[1],
100. * val_r[0].numpy() / val_r[1]))
# 将准确率和权重等数值加载到容器中,方便后续处理
record.append((100 -100.* train_r[0] / train_r[1], 100 - 100. * val_r[0] / val_r[1]))
# weights 记录了训练周期中所有卷积核的演化过程,net.conv1.weight 提取出了第一层卷积核的权重
# Clone 是将weight.data 中的数据做一个备份放到列表中
# 否则当 weight.data 变化时,列表中的每一项数值也会联动
# 这里使用clone这个函数很重要
weights.append([net.conv1.weight.data.clone(), net.conv1.bias.data.clone(),
net.conv2.weight.data.clone(), net.conv2.bias.data.clone()])
'''______________________________训练的过程完成______________________________'''
# 绘制训练过程的误差曲线,校验集和测试集上的错误率。
plt.figure(figsize=(10, 7))
plt.title('Training loss curve')
plt.plot(record) # record记载了每一个打印周期记录的训练和校验数据集上的准确度
plt.xlabel('Steps')
plt.ylabel('Error rate')
pylab.show()
''' 可视化第一层卷积核与特征图 '''
# 提取第一层卷积层的卷积核
plt.figure(figsize=(10, 7))
for i in range(4):
plt.subplot(1, 4, i+1)
# 提取第一层卷积核中的权重值,注意conv1是net的属性
plt.imshow(net.conv1.weight.data.numpy()[i, 0, ...])
plt.title('Convolution kernel of the first convolution layer')
pylab.show()
# 调用net的retrieve_features方法可以抽取出输入当前数据后输出的所有特征图(第一个卷积层和第二个卷积层)
# 首先定义读入的图片,它是从test_dataset中提取第idx个批次的第0个图
# 其次unsqueeze的作用是在最前面添加一维
# 目的是让这个input_x的tensor是四维的,这样才能输给net。补充的那一维表示batch
input_x = test_dataset[idx][0].unsqueeze(0)
# feature maps是有两个元素的列表,分别表示第一层和第二层卷积的所有特征因
feature_maps = net. retrieve_features(Variable(input_x))
plt.figure(figsize=(10, 7))
# 打印出4个特征图
for i in range(4):
plt.subplot(1, 4, i + 1)
plt.imshow(feature_maps[0][0, i, ...].data.numpy())
plt.title('Characteristic diagram of the first layer of convolution')
pylab.show()
''' 可视化第二层卷积核与特征图 '''
# 绘制第二层的卷积核,每一列对应一个卷积核,一共有8个卷积核
plt.figure(figsize=(15, 10))
plt.title('Characteristic diagram of the second layer of convolution')
for i in range(4):
for j in range(8):
plt.subplot(4, 8, i * 8 + j + 1)
plt.imshow(net.conv2.weight.data.numpy()[j, i, ...])
pylab.show()
# 绘制第二层的特征图,一共有8个
plt.figure(figsize=(10, 7))
plt.title('Characteristic diagram of the second layer of convolution')
for i in range(8):
plt. subplot(2, 4, i + 1)
plt. imshow(feature_maps[1][0, i, ...].data.numpy())
pylab.show()
以上是关于用PyTorch构建基于卷积神经网络的手写数字识别模型的主要内容,如果未能解决你的问题,请参考以下文章
基于FPGA的手写数字识别神经网络加速——pytorch网络搭建及训练
联邦学习实战基于FATE框架的MNIST手写数字识别——卷积神经网络
[Pytorch系列-34]:卷积神经网络 - 搭建LeNet-5网络与MNIST数据集手写数字识别
[Pytorch系列-40]:卷积神经网络 - 模型的恢复/加载 - 搭建LeNet-5网络与MNIST数据集手写数字识别