温馨提示:这篇文章已超过410天没有更新,请注意相关的内容是否还可用!
摘要:本文介绍了使用PyTorch实现深度学习手写数字识别器的相关内容。通过搭建神经网络模型,利用PyTorch的强大的计算能力和优化器,实现对手写数字的准确识别。该识别器具有良好的可移植性和可扩展性,可广泛应用于数字识别、图像识别等领域。
目录

概要
一、代码概览

二、使用步骤
1.引入库
2.超参数的定义和数据集的预处理
3.构建网络
4.训练
5.测试
三、剖析
1.各层运算
2.健壮性实验
总结
概要
系列文章为《深度学习原理与Pytorch实践》学习笔记
Pytorch 2.2.1 (CPU) Python 3.6.13|Anaconda 环境
一、代码概览
手写数字识别器_minst_convnet.py
二、使用步骤
1.引入库
import torch import torch.nn as nn 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 import numpy as np
代码导入了torchvision中的datasets模块和transforms模块,它们通常用于加载和预处理图像数据。
- 首先,我们需要学习PyTorch自带的数据加载器,包括dataset,sampler,以及data loader这三个对象组成的套件。
- 当数据集很小,格式比较规则的时候,数据加载三套件的优势并不明显。但是当数据格式比较特殊,以及数据规模很大(内存无法同时加载所有数据) 的时候,特别是,我们需要用不同的处理器来加载数据的时候,三套件的威力就会显现出来了。它会将数据加载、分布的任务自动完成。
- 在使用的时候,我们用dataset来装载数据集,用sampler来采样数据集。而对数据集的迭代、循环则主要通过data_loader来完成。
- 创建一个data_loader就需要一个dataset和一个datasampler,它基本实现的就是利用sampler自动从dataset种采样
2.超参数的定义和数据集的预处理
# 定义超参数 image_size = 28 #图像的总尺寸28*28 num_classes = 10 #标签的种类数 num_epochs = 20 #训练的总循环周期 batch_size = 64 #一个撮(批次)的大小,64张图片 # 加载MINIST数据,如果没有下载过,就会在当前路径下新建/data子目录,并把文件存放其中 # MNIST数据是属于torchvision包自带的数据,所以可以直接调用。 # 在调用自己的数据的时候,我们可以用torchvision.datasets.ImageFolder或者torch.utils.data.TensorDataset来加载 train_dataset = dsets.MNIST(root='./data', #文件存放路径 train=True, #提取训练集 transform=transforms.ToTensor(), #将图像转化为Tensor,在加载数据的时候,就可以对图像做预处理 download=True) #当找不到文件的时候,自动下载 # 加载测试数据集 test_dataset = dsets.MNIST(root='./data', train=False, transform=transforms.ToTensor()) # 训练数据集的加载器,自动将数据分割成batch,顺序随机打乱 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, sampler = sampler_val ) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, sampler = sampler_test ) #随便从数据集中读入一张图片,并绘制出来 idx = 1000 #dataset支持下标索引,其中提取出来的每一个元素为features,target格式,即属性和标签。[0]表示索引features muteimg = train_dataset[idx][0].numpy() #由于一般的图像包含rgb三个通道,而MINST数据集的图像都是灰度的,只有一个通道。因此,我们忽略通道,把图像看作一个灰度矩阵。 #用imshow画图,会将灰度矩阵自动展现为彩色,不同灰度对应不同颜色:从黄到紫 plt.imshow(muteimg[0,...]) print('标签是:',train_dataset[idx][1]) plt.show()
SubsetRandomSampler是PyTorch中的一个采样器,用于从数据集中随机地选择子集进行训练。它通常与DataLoader一起使用,可以通过指定indices参数来实现从数据集中随机选择样本的功能。
3.构建网络
# 定义卷积神经网络: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) # 定义一个Pooling层,一个窗口为2*2的pooling运算 self.conv2 = nn.Conv2d(depth[0], depth[1], 5, padding=2) # 第二层卷积,输入通道为depth[0], # 输出通道为depth[1],窗口为5,padding为2 self.fc1 = nn.Linear(image_size // 4 * image_size // 4 * depth[1], 512) # 一个线性连接层,输入尺寸为最后一层立方体的平铺,输出层512个节点 self.fc2 = nn.Linear(512, num_classes) # 最后一层线性分类单元,输入为512,输出为要做分类的类别数 def forward(self, x): # 该函数完成神经网络真正的前向运算,我们会在这里把各个组件进行实际的拼装 # x的尺寸:(batch_size, image_channels, image_width, image_height) x = F.relu(self.conv1(x)) # 第一层卷积,激活函数用ReLu,为了防止过拟合 # x的尺寸:(batch_size, num_filters, image_width, image_height) x = self.pool(x) # 第二层pooling,将图片变小 # x的尺寸:(batch_size, depth[0], image_width/2, image_height/2) x = F.relu(self.conv2(x)) # 第三层又是卷积,窗口为5,输入输出通道分别为depth[0]=4, depth[1]=8 # x的尺寸:(batch_size, depth[1], image_width/2, image_height/2) x = self.pool(x) # 第四层pooling,将图片缩小到原大小的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[1]*image_width/4*image_height/4) x = F.relu(self.fc1(x)) # 第五层为全链接,ReLu激活函数 # x的尺寸:(batch_size, 512) x = F.dropout(x, training=self.training) # 以默认为0.5的概率对这一层进行dropout操作,为了防止过拟合 x = self.fc2(x) # 全链接 # x的尺寸:(batch_size, num_classes) x = F.log_softmax(x, dim=0) # 输出层为log_softmax,即概率对数值log(p(x))。采用log_softmax可以使得后面的交叉熵计算更快 return x def retrieve_features(self, x): # 该函数专门用于提取卷积神经网络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图 feature_map1 = F.relu(self.conv1(x)) # 完成第一层卷积 x = self.pool(feature_map1) # 完成第一层pooling feature_map2 = F.relu(self.conv2(x)) # 第二层卷积,两层特征图都存储到了feature_map1, feature_map2中 return (feature_map1, feature_map2) def rightness(predictions, labels): """计算预测错误率的函数,其中predictions是模型给出的一组预测结果,batch_size行num_classes列的矩阵,labels是数据之中的正确答案""" pred = torch.max(predictions.data, 1)[1] # 对于任意一行(一个样本)的输出值的第1个维度,求最大,得到每一行的最大元素的下标 rights = pred.eq(labels.data.view_as(pred)).sum() # 将下标与labels中包含的类别进行比较,并累计得到比较正确的数量 return rights, len(labels) # 返回正确的数量和这一次一共比较了多少元素
构建ConvNet类(继承nn.Module)并复写 __init__()和forward(),定义retrieve_features()。
对于构造函数__init__():
卷积层 self.conv1:
- 输入通道数:1(灰度图像)
- 输出通道数:4
- 卷积核大小:5x5
- 填充(padding):2
- 该层用于提取图像中的特征,通过卷积操作可以捕获局部信息。
池化层 self.pool:
- 池化类型:最大池化(MaxPooling)
- 池化核大小:2x2
- 步长(stride):2
- 池化层的作用是降低特征图的空间尺寸,保留重要特征并减少计算量。
第二个卷积层 self.conv2:
- 输入通道数:depth[0](根据之前某处定义的值)
- 输出通道数:depth[1](根据之前某处定义的值)
- 卷积核大小:5x5
- 填充(padding):2
- 第二个卷积层用于进一步提取图像特征,可以学习到更高级别的特征表示。
全连接层 self.fc1:
- 输入节点数:经过卷积和池化后的特征图的大小
- 输出节点数:512
- 该层将卷积层提取到的特征进行展开,并通过全连接层进行特征映射,以便进行分类任务。
输出层 self.fc2:
- 输入节点数:512
- 输出节点数:num_classes(分类类别数)
- 最后一层全连接层,用于将前面层提取到的特征映射到类别概率空间,进行图像分类任务。
整体结构为:卷积层 -> 池化层 -> 卷积层 -> 全连接层 -> 输出层。通过卷积和池化层,模型可以逐渐提取并组合图像特征,最终通过全连接层和输出层完成图像分类任务。这种结构在处理图像数据上表现出色,适合用于识别和分类图像数据集。
对于前向传播函数forward():
卷积层(Convolutional Layers):
- 通过 self.conv1 和 self.conv2 两个卷积层实现特征提取。卷积操作可以提取输入图像的局部特征。
- 使用 ReLU 激活函数 F.relu 对卷积层的输出进行非线性变换。
池化层(Pooling Layers):
- 通过 self.pool 池化层对特征图进行下采样操作,减少参数数量和计算量,同时保留重要特征。
- 这里使用的是最大池化(Max Pooling)操作。
全连接层(Fully Connected Layers):
- 通过 self.fc1 和 self.fc2 两个全连接层实现特征的组合和分类。
- 在全连接层上再次使用 ReLU 激活函数对输出进行非线性变换。
Dropout 层(随机失活,防止过拟合):
- 通过 F.dropout 函数实现 Dropout 操作,以一定的概率(这里是默认的0.5)随机丢弃神经元,防止过拟合。
Softmax 层:
- 最后一层使用 F.log_softmax 函数对输出进行 softmax 操作,得到类别的概率分布。
- softmax 操作可以将神经网络输出的原始分数转换为概率形式,便于后续计算交叉熵损失。
该函数定义了网络结构,通过卷积、池化和全连接等层的堆叠,将输入的图像数据逐步转换为最终的类别预测概率分布。整个过程包括特征提取、特征组合和分类等步骤,是神经网络模型中至关重要的一部分。
对retrieve_features()和rightness()的解释参见代码,不再赘述。
4.训练
net = ConvNet() # 新建一个卷积神经网络的实例,此时ConvNet的__init__函数就会被自动调用 criterion = nn.CrossEntropyLoss() # Loss函数的定义,交叉熵 optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # 定义优化器,普通的随机梯度下降算法 record = [] # 记录准确率等数值的容器 weights = [] # 每若干步就记录一次卷积核 # 开始训练循环 for epoch in range(num_epochs): train_rights = [] # 记录训练数据集准确率的容器 ''' 下面的enumerate是构造一个枚举器的作用。就是我们在对train_loader做循环迭代的时候,enumerate会自动吐出一个数字指示我们循环了几次 这个数字就被记录在了batch_idx之中,它就等于0,1,2,…… train_loader每迭代一次,就会吐出来一对数据data和target,分别对应着一个batch中的手写数字图,以及对应的标签。''' for batch_idx, (data, target) in enumerate(train_loader): # 针对容器中的每一个批进行循环 data, target = data.clone().requires_grad_(True), target.clone().detach() # data为一批图像,target为一批标签 net.train() # 给网络模型做标记,标志说模型正在训练集上训练, # 这种区分主要是为了打开关闭net的training标志,从而决定是否运行dropout output = net(data) # 神经网络完成一次前馈的计算过程,得到预测输出output loss = criterion(output, target) # 将output与标签target比较,计算误差 optimizer.zero_grad() # 清空梯度 loss.backward() # 反向传播 optimizer.step() # 一步随机梯度下降算法 right = ConvNet.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 = data.clone().requires_grad_(True), target.clone().detach() output = net(data) # 完成一次前馈计算过程,得到目前训练得到的模型net在校验数据集上的表现 right = ConvNet.rightness(output, target) # 计算准确率所需数值,返回正确的数值为(正确样例数,总样本数) val_rights.append(right) # 分别计算在目前已经计算过的测试数据集,以及全部校验集上模型的表现:分类准确率 # train_r为一个二元组,分别记录目前已经经历过的所有训练集中分类正确的数量和该集合中总的样本数, # train_r[0]/train_r[1]就是训练集的分类准确度,同样,val_r[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.plot(record) #record记载了每一个打印周期记录的训练和校验数据集上的准确度 plt.xlabel('Steps') plt.ylabel('Error rate') plt.show()
训练过程,涉及损失函数、优化器等:
- 创建卷积神经网络:通过 net = ConvNet() 创建一个卷积神经网络。
- 定义损失函数和优化器:使用交叉熵损失函数 nn.CrossEntropyLoss() 和随机梯度下降优化器 optim.SGD。
- 记录训练过程中的数据:定义了两个列表 record 和 weights 用于记录准确率和卷积核的参数。
- 训练循环:使用 for 循环遍历每个 epoch,然后在每个 epoch 中遍历训练数据集。
- 前向传播和反向传播:对每个 batch 的数据进行前向传播,计算损失,反向传播更新参数。
- 计算准确率:计算训练集和校验集上的准确率,并记录在 train_rights 和 val_rights 中。
- 打印训练信息:每训练一定次数的 batch 后,打印当前训练周期、损失、训练正确率和校验正确率等信息。
- 记录准确率和权重:将准确率和权重记录到容器中,用于后续处理。
- 绘制误差曲线:最后利用 matplotlib 绘制训练过程中的误差曲线,展示训练和校验数据集上的错误率变化。
这段代码实现了神经网络的训练过程,包括了模型的训练、参数更新、准确率计算以及结果的可视化。
5.测试
#在测试集上分批运行,并计算总的正确率 net.eval() #标志模型当前为运行阶段 vals = [] #记录准确率所用列表 #对测试数据集进行循环 for data, target in test_loader: data, target = data.clone().detach().requires_grad_(True), target.clone().detach() output = net(data) #将特征数据喂入网络,得到分类的输出 val = ConvNet.rightness(output, target) #获得正确样本数以及总样本数 vals.append(val) #记录结果 #计算准确率 rights = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals])) right_rate = 100. * rights[0].numpy() / rights[1] print(right_rate) #随便从测试集中读入一张图片,并检验模型的分类结果,并绘制出来 idx = 4000 muteimg = test_dataset[idx][0].numpy() plt.imshow(muteimg[0,...]) print('标签是:',test_dataset[idx][1]) plt.show()
三、剖析
1.各层运算
输出第一层、第二层的卷积核及得到的特征图
#提取第一层卷积层的卷积核 plt.figure(figsize = (10, 7)) for i in range(4): plt.subplot(1,4,i + 1) plt.axis('off') plt.imshow(net.conv1.weight.data.numpy()[i,0,...]) #提取第一层卷积核中的权重值,注意conv1是net的属性 plt.show() plt.subplot(1,4,2) plt.show() # 将记录在容器中的卷积核权重历史演化数据打印出来 i = 0 for tup in weights: if i % 10 == 0 : layer1 = tup[0] fig = plt.figure(figsize = (10, 7)) for j in range(4): plt.subplot(1, 4, j + 1) plt.axis('off') plt.imshow(layer1.numpy()[j,0,...]) i += 1 plt.show() #调用net的retrieve_features方法可以抽取出喂入当前数据后吐出来的所有特征图(第一个卷积和第二个卷积层) #首先定义读入的图片 #它是从test_dataset中提取第idx个批次的第0个图,其次unsqueeze的作用是在最前面添加一维, #目的是为了让这个input_x的tensor是四维的,这样才能输入给net。补充的那一维表示batch。 input_x = test_dataset[idx][0].unsqueeze(0) feature_maps = net.retrieve_features(input_x) #feature_maps是有两个元素的列表,分别表示第一层和第二层卷积的所有特征图 plt.figure(figsize = (10, 7)) #有四个特征图,循环把它们打印出来 for i in range(4): plt.subplot(1,4,i + 1) plt.axis('off') plt.imshow(feature_maps[0][0, i,...].data.numpy()) plt.show() # 绘制第二层的卷积核,每一列对应一个卷积核,一共8个卷积核 plt.figure(figsize = (15, 10)) for i in range(4): for j in range(8): plt.subplot(4, 8, i * 8 + j + 1) plt.axis('off') plt.imshow(net.conv2.weight.data.numpy()[j, i,...]) plt.show() # 绘制第二层的特征图,一共八个 plt.figure(figsize = (10, 7)) for i in range(8): plt.subplot(2,4,i + 1) plt.axis('off') plt.imshow(feature_maps[1][0, i,...].data.numpy()) plt.show()
2.健壮性实验
# 提取中test_dataset中的第idx个批次的第0个图的第0个通道对应的图像,定义为a。 a = test_dataset[idx][0][0] # 平移后的新图像将放到b中。根据a给b赋值。 b = torch.zeros(a.size()) #全0的28*28的矩阵 w = 3 #平移的长度为3个像素 # 对于b中的任意像素i,j,它等于a中的i,j+w这个位置的像素 for i in range(a.size()[0]): for j in range(0, a.size()[1] - w): b[i, j] = a[i, j + w] # 将b画出来 muteimg = b.numpy() plt.axis('off') plt.imshow(muteimg) plt.show() # 把b喂给神经网络,得到分类结果pred(prediction是预测的每一个类别的概率的对数值),并把结果打印出来 prediction = net(b.unsqueeze(0).unsqueeze(0)) pred = torch.max(prediction.data, 1)[1] print(pred) #提取b对应的featuremap结果 feature_maps = net.retrieve_features(b.unsqueeze(0).unsqueeze(0)) plt.figure(figsize = (10, 7)) for i in range(4): plt.subplot(1,4,i + 1) plt.axis('off') plt.imshow(feature_maps[0][0, i,...].data.numpy()) plt.figure(figsize = (10, 7)) for i in range(8): plt.subplot(2,4,i + 1) plt.axis('off') plt.imshow(feature_maps[1][0, i,...].data.numpy()) plt.show()
总结
手写数字识别器_minst_convnet
还没有评论,来说两句吧...