Pytorch入门——搭建神经网络识别手写数字数据集Mnist(包含代码详解)
0.前言
本次博客的内容是Pytorch的入门:搭建一个简单的神经网络模型进行手写数字数据集Mnist的识别,这也是对我前一段Pytorch学习的一次总结和提高,为接下来的学习打下一个基础。系统的梳理知识有助于建立一个成熟的学习体系。
1.准备工作
本次代码需要用到torchvision包和pytorch框架中的utils部分进行图像数据的导入和后续处理以及pytorch框架中的torch.nn部分来进行神经网络的搭建和训练。故此节对上述提到的内容进行一些简单的介绍。
1.1 torchvision
该包的主要功能是实现数据的导入,处理和预览等.如果需要对计算机视觉的相关问题进行处理,就可以借用torchvision包中提供的大量的类来完成相应的工作。包中主要有四个大类:datasets/models/transforms/utils。它是独立于Pytorch框架的包,需要另外下载,下载的步骤非常简单在此不再赘述。
1.1.1 torchvision.datasets
有许多内置的图片数据集比如:MNIST/COCO/Captions/CIFAR等。其使用方法例如:
torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)
root :用于指定数据集在下载之后的存放路径。
train:如果为True,则从training.pt创建数据集,否则从test.pt创建数据集。
transform:用于指定导入数据集时需要对数据集进行哪种变换操作。这里注意,需要提前定义这些变换操作。
target_transform :对label进行变换
loader: 指定加载图片的函数,默认操作是读取PIL image对象。
1.1.2 torchvision.transforms
torchvision.transforms提供了丰富的类对载入的数据进行变换。pytorch中实际处理的是tensor类型的变量。所以首先解决的是数据类型转换的问题,如果获取到的数据是格式或者大小不一的,则还需要进行归一化和大小缩放等操作。这时就需要torch.transorms。用例如下:
transform = transforms.Compose([transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5,0.5], [0.5, 0.5, 0.5])])
对数据集进行的多个操作,可通过Compose将这些操作像管道一样拼接起来组成一个操作整体。
1.ToTensor(): 是把图片数据转换成tensor并转化范围在[0,1]。
2.Normalize(mean,std):是归一化的方法,mean = (R, G, B),std = (R, G, B),如[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225],是对三个通道的像素值进行相关的归一化处理。在上步的ToTensor将图像数据转换到[0,1]之后再调取标准化函数进行处理:(像素-均值)/标准差将图像数据再转换到[-1,1]区间。(均值和方差相等的情况下)查了相关资料说是这样的操作可以让收敛变快。
3.还有一些其他操作:
Resize:把给定的图片resize到given size;
ToPILImage: convert a tensor to PIL imageScale:目前已经不用了,推荐用ResizeCenterCrop;
ResizeCenterCrop:在图片的中间区域进行裁剪;
RandomCrop:在一个随机的位置进行裁剪;
RandomHorizontalFlip:以0.5的概率水平翻转给定的PIL图像;
RandomVerticalFlip:以0.5的概率竖直翻转给定的PIL图像;
RandomResizedCrop:将PIL图像裁剪成任意大小和纵横比;
Grayscale:将图像转换为灰度图像;
RandomGrayscale:将图像以一定的概率转换为灰度图像;
FiceCrop:把图像裁剪为四个角和一个中心T;
enCropPad:填充ColorJitter:随机改变图像的亮度对比度和饱和度
以上操作在本次实验中用不到,仅作拓展了解。
1.1.3 torchvision.models
torchvison.models这个包中包含了alexnet,densenet,inception,resnet,squeezenet,vgg等常用的网络结构,并且提供了预训练模型,可以通过简单调用来读取网络结构和预训练模型。本次实验采取手动搭建神经网络的方法,故此部分暂时用不到,仅作拓展了解。
1.2 torch.utils
主要用到此部分的.data,导入为
from torch.utils.data import DataLoader
查阅官方文档可知:torchvision.datasets是继承torch.utils.data.Dataset的子类. 因此,可以使用torch.utils.data.DataLoader可以对datasets导入的数据集进行多线程处理。
在训练模型时使用到此函数,用来把训练数据分成多个小组,此函数每次抛出一组数据。直至把所有的数据都抛出。就是做一个数据的初始化。官方文档的说明如下:
常用的参数解释如下:
1.dataset:(数据类型 dataset)
输入的待处理的数据集。
2.batch_size:(数据类型 int)
在进行深度学习处理时,常常将数据集划分为一个个的批次,每个批次有固定的数据数目,在此就是指定一个批次的数据量。
3.shuffle:(数据类型 bool)
是否对数据集进行洗牌操作。默认设置为False。在每次迭代训练时是否将数据洗牌,默认设置是False。将输入数据的顺序打乱,是为了使数据更有独立性,但如果数据是有序列特征的,就不要设置成True了。一般对训练集进行shuffle操作而对测试集保留原有的顺序结构。(原始的数据,在样本均衡的情况下可能是按照某种顺序进行排列,如前半部分为某一类别的数据,后半部分为另一类别的数据。但经过打乱之后数据的排列就会拥有一定的随机性,在顺序读取的时候下一次得到的样本为任何一类型的数据的可能性相同。减小模型抖动)
4.num_workers:(数据类型 Int)
用多少个子进程来导入数据。设置为0,就是使用主进程来导入数据。
1.3 torch.nn
此部分是定义神经网络层和构建网络的重要模块。
用pytorch构建神经网络结构的示意图如下:
详细功能简介可以查询链接:http://www.srcmini.com/31857.html。
1.3.1 nn.functional
import torch.nn.functional as F
包含了torch.nn库中所有的函数,由上图可以发现在定义神经网络的网络层时,可以用functional函数即F.xxx或者nn.xxx。那么两者有什么区别呢?如下进行相关的说明:
1.两者性质不同
F.xxx 是函数接口,nn.Xxx 是 .nn.functional.xxx 的类封装,并且nn.Xxx 都继承于一个共同祖先 nn.Module。
nn.Xxx 除了具有 nn.functional.xxx 功能之外,内部附带 nn.Module 相关的属性和方法,eg. train(), eval(), load_state_dict, state_dict.
2.两者调用方式不同
nn.xxx要先实例化再函数调用并传入数据,例如:
inputs = torch.rand(64, 3, 28, 28)
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)
out = conv(inputs)
而F.xxx传入数据和weight和bias等其他参数,例如:
weight = torch.rand(64, 3, 3, 3)
bias = torch.rand(64)
out = nn.functional.conv2d(inputs, weight, bias, padding=1)
3.与nn.Sequential结合情况
nn.Xxx 继承于 nn.Module,能够很好的与 nn.Sequential 结合使用,而 nn.functional.xxx 无法与 nn.Sequential 结合使用。
4.复用
nn.xxx便于程序复用而F.xxx使用时是一个纯函数,每次都要传入必要的参数不利于代码复用。要根据不同的情况选择方法:
具有学习参数的(eg. conv2d, linear, batch_norm) 采用 nn.Xxx
没有学习参数的(eg. maxpool, loss_func, activation func) 等根据个人选择使用 nn.functional.xxx 或 nn.Xxx。
1.3.2 nn.Sequential
当一个神经网络模型较简单的时候,我们可以使用torch.nn.Sequential类来实现简单的顺序连接模型,其函数功能就是将定义好的网络层拼接起来实现一个完整的神经网络。(容器作用)
1.3.3 构建神经网络模型的nn.XXX部分类封装简介
本次实验我们要实现的是一个简单的3层神经网络,中间隐藏层采用全连接层,且激活函数采用Relu函数。故在此部分介绍一下如何使用nn模块实现上述定义的网络层。
- 线性全连接层:
代码举例为:
import torch
from torch import nn
m = nn.Linear(20, 30)
input = torch.randn(128, 20)
output = m(input)
output.size()
其实现的运算过程中矩阵纬度变化为为:[128,20]×[20,30]=[128,30]
即如果输入数据的特征数为n则第一个隐藏层的in_features=n,out_features为第一隐藏层的神经元个数。以此类推
2. 归一化层:
此层主要用到的是nn.BatchNorm1d,关于归一化层的理论基础和相关代码的解读,建议参考博客:Batch Normalization原理。讲的非常详细,在此不再赘述。
3.激活函数
可以直接采用F.relu()来实现
1.4 优化器算法实现:torch.optim
在 PyTorch的torch.optim 包中提供了非常多的可实现参数自动优化的类,比如 SGD 、AdaGrad 、RMSProp 、Adam等优化算法,这些类都可以被直接调用。
本次实验使用了最基本的优化算法:SGD(将数据拆分后送入网络进行模型训练)。
创建优化器:
import torch.optim as optim
optimizer=optim.SGD(model.parameters(),lr,momentum)
参数中的lr和momentum分别为学习率和动量因子。
对参数的梯度清零操作为:
optimzer.zero_grad()
参数更新操作为:
optimizer.step()
1.5 MNIST数据集解读
1.先有如下代码对数据集进行处理:
train_batch_size=64
test_batch_size=128
transform=transforms.Compose([transforms.ToTensor(),transforms.Normalize([0.5],[0.5])])
train_dataset=mnist.MNIST('./data', train=True, transform=transform)
test_dataset=mnist.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False)
train_dataset和test_dataset数据点数目分别有60000和10000个。
mnist.MNIST返回值为一个元组(train_data,train_target)。
2.对图像数据集迭代器的理解:
L=list(train_loader)
len(L)#输出为938 即将训练集中的60000个数据按照64个样本一个批次划分,而最后一个批次数据不足64个
L[0]
#输出为:
[tensor([[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]],
[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]],
[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]],
...,
[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]],
[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]],
[[[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
...,
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.],
[-1., -1., -1., ..., -1., -1., -1.]]]]),
tensor([7, 6, 8, 2, 3, 1, 4, 2, 4, 1, 0, 7, 5, 1, 7, 2, 0, 0, 3, 6, 5, 3, 2, 6,
4, 4, 1, 4, 2, 0, 3, 6, 5, 7, 0, 2, 1, 4, 0, 6, 1, 5, 3, 9, 7, 6, 4, 0,
0, 2, 0, 1, 9, 9, 2, 6, 1, 9, 9, 9, 8, 5, 1, 6])]
L[0][0].shape
#输出为torch.Size([64, 1, 28, 28])
可知train_loader数据迭代器每个元素(即批次)包含两个tensor,第一个tensor包含64个数据样本的图像数据矩阵,第二个tensor是对应64个图像数据样本的标签。
2. 程序设计
此部分将整体程序拆分说明。
2.1 导入必要的包和模块
import numpy as np
import torch
from torch import nn
from torchvision.datasets import mnist
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
2.2 参数设定
#训练集,测试集合批次划分
train_batch_size=64
test_batch_size=128
#优化器参数设定
learning_rate=0.01
lr=0.01
momentum=0.5
#训练循环总次数
num_epoches=20
2.3 数据集操作和验证
#对数据集进行划分
transform=transforms.Compose([transforms.ToTensor(),transforms.Normalize([0.5],[0.5])])
train_dataset=mnist.MNIST('./data', train=True, transform=transform)
test_dataset=mnist.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False)
#打印图像数据
examples=enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
fig=plt.figure()
for i in range(6):
plt.subplot(2,3,i+1)
plt.tight_layout()
plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
plt.title("Ground Truth: {}".format(example_targets[i]))
plt.xticks([])
plt.yticks([])
输出为:
2.4 神经网络定义
class Net(nn.Module):
def __init__(self,in_dim,n_hidden_1,n_hidden_2,out_dim):
super(Net,self).__init__()
self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1),nn.BatchNorm1d(n_hidden_1))
self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, n_hidden_2),nn.BatchNorm1d(n_hidden_2))
self.layer3 = nn.Sequential(nn.Linear(n_hidden_2, out_dim))
def forward(self, x):
x = F.relu(self.layer1(x))
x = F.relu(self.layer2(x))
x = self.layer3(x)
return x
关于Python中类定义的相关知识可以参考博客:
Python中类的定义和对象的创建
2.5 神经网络类实例化和GPU加速
device=torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
model=Net(28*28,300,100,10)#神经网络实例化
model.to(device)#模型GPU加速
criterion=nn.CrossEntropyLoss()#实例化交叉熵损失函数
optimizer=optim.SGD(model.parameters(),lr=lr,momentum=momentum)#优化器实例化
2.6 模型训练和预测
losses = []
acces = []
eval_losses = []
eval_acces = []
for epoch in range(num_epoches):
train_loss = 0
train_acc = 0
model.train()
#动态修改参数学习速率
if epoch % 5==0:
optimizer.param_groups[0]['lr']*=0.1
for img,label in train_loader:
img=img.to(device)
label = label.to(device)
img = img.clone().view(img.size()[0], -1)
#前向传播
out=model(img)
loss=criterion(out,label)
#反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
#记录误差
train_loss+=loss.item()
#计算分类准确度
_, pred = out.max(1)
num_correct=(pred==label).sum().item()
acc = num_correct / img.shape[0]
train_acc += acc
losses.append(train_loss / len(train_loader))
acces.append(train_acc / len(train_loader))
#在测试集上测试模型
eval_loss=0
eval_acc=0
model.eval()
for img,label in test_loader:
img=img.to(device)
label=label.to(device)
img=img.view(img.size(0),-1)
out=model(img)
loss=criterion(out,label)
eval_loss+=loss.item()
_,pred=out.max(1)
num_correct=(pred == label).sum().item()
acc=num_correct / img.shape[0]
eval_acc+=acc
对此段代码的说明:
- 在动态修改学习率参数方面可以先查看optimizer.param_groups结构:[{‘params’,‘lr’, ‘momentum’, ‘dampening’, ‘weight_decay’, ‘nesterov’},{……}],集合了优化器的各项参数。列表内有两个字典数据结构,故在调整学习率时采用字典的搜索方式并用*=的原地操作改变学习率。
- 在训练和测试的时候要将数据集放入GPU进行加速运算。
img = img.clone().view(img.size()[0], -1)
的说明:
x = a.view(a.size(0), -1)
#等价于
x = x.view(x.size()[0], -1)
而:
L[0][0].size()
#输出为:torch.Size([64, 1, 28, 28])
故这个代码的作用就是将一个批次64个图像数据重整为64784形状的Tensor矩阵每一行代表一个图像数据的2828个特征,对应之前讲述的神经网络第一个隐藏层的计算形状。
-
交叉熵损失函数计算:
loss=criterion(out,label)使用注意:使用nn.CrossEntropyLoss()时,不需要现将输出经过softmax层,否则计算的损失会有误,即直接将网络输出用来计算损失即可 -
.在pytorch中,.item()方法 是得到一个元素张量里面的元素值具体就是用于将一个零维张量转换成浮点数进行后续的数学运算。
-
torch.max(input, dim, keepdim=False, out=None) -> (Tensor, LongTensor),当max函数中有维数参数的时候,它的返回值为两个,一个为最大值,另一个为最大值的索引,当dim为1时,max()返回的是每行的最大值以及各行最大值的索引。用法示例如下:
a=torch.randn(2,3)
X,Y=a.max(1)
print(a)
print(a.max(1))
print(X)
print(Y)
输出为:
tensor([[ 1.4038, 1.1906, 0.0224, 0.5196],
[ 0.6926, 0.6009, -2.0529, 1.7490],
[ 0.4606, 0.0464, 0.4818, -1.0575]])
torch.return_types.max(values=tensor([1.4038, 1.7490, 0.4818]),
indices=tensor([0, 3, 2]))
tensor([1.4038, 1.7490, 0.4818])#X
tensor([0, 3, 2])#Y
即pred变量存储了预测后的矩阵中每一行(即每个样本输出的十个数字预测概率)中最大数值的标号(即预测的数字标签)
注意到模型中计算的准确率和损失函数值是一个批次计算完就累加一次的,一个epoch处理完之后要对所有批次的两个值求一个平均。
最后在一个epoch更新完网络的所有参数之后,就立即将模型调整为model.eval()即测试状态,将测试集的数据送入神经网络模型进行训练,计算模型在测试集上的综合表现能力(代码步骤思路与训练集一样)。这样就可以得到每个epoch训练之后模型在测试集上的表现能力趋势。
3. 结语
总之本次实验结束后,我对pytorch框架有了更立体的感觉。同时,我也发现了自己在Python编程方面的欠缺:面向对象编程内容了解太少,未来我会向这一方向继续努力学习。