基于Pytorch的mnist的分类
- 训练集和测试集的划分非常重要。在机器学习模型设计时必须有一个单独的测试集(不用于训练而是用来评估这个模型的性能),才能更容易把模型推广到其他数据集上
mnist中图片包含28x28
像素点,可以用一个数组来描述,这个数组可以张成一个向量,长度为784,保证每张照片以相同方式展开。从这个角度看,就是784维向量空间的点,然后可以进行比较。
借助Pytorch搭建网络
导入需要的库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
定义参数
BATCH_SIZE = 523 # batch_size即每批训练的样本数量
epochs = 20 # 循环次数
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu") #判断是否能在GPU上进行运算
下载数据集
- 训练集
train_loader = torch.utils.data.DataLoader( # vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网。
datasets.MNIST('data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(), # 图像转化为Tensor
transforms.Normalize((0.1307,), (0.3081,)) # 标准化
])),
batch_size=BATCH_SIZE, shuffle=True) # shuffle() 方法将序列的所有元素随机排序
torchvision.dataset
作用是下载数据集,第一次调用时会自动从网上获取数据,
- train来指定获取训练数据集或测试数据集
- transforms.ToTensor()是将数据转化为tensor
- transforms.Normalize((0.1307,), (0.3081,))是进行标准化
torch.utils.data.DataLoader()
利用下载的数据集来创建一个读取小批量数据样本的DataLoader实例- batch_size是每批训练的样本数量
- shuffle() 方法将序列的所有元素随机排序
- 测试集
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=BATCH_SIZE, shuffle=True) # shuffle() 方法将序列的所有元素随机排序
定义网络
- 网络的定义是最重要的部分,如何设置内部网络结构,往往就决定了你设置模型优劣
- 这里网络设置的是
三个个卷积层
,以及两个全连接层
,在卷积层以及全连接层之间分别设置了激活函数
,池化函数
分别试验他们各自作用 - 下面解析各自层之间参数传递关系
- conv1(1,12,5)由于该图片为黑白图输入通道为1,卷积核为5x5,输出通道为12,输出维度28-5+1=24,输出维度12x24x24
- 经过池化层维度减半为12,输出shape为12x12x12
- conv2(12,20,3)输入通道与上一层输出通道对应,卷积核为3,输出维度为12-3+1=10,输出shape为20x10x10
-
- conv2(20,40,3)输入通道与上一层输出通道对应,卷积核为3,输出维度为10-3+1=8,输出shape为40x8x8
- Linear需要接收的是展平后的多维的卷积成的特征图,需要使用
view()
函数,-1是根据输入数据自动设定 - nn.Linear(4088, 500),输出为50
- nn.Linear(500, 10),输出为10,进行10分类
class Net(nn.Module): # 继承model
def __init__(self):
super().__init__()
# 28x28
self.conv1=nn.Conv2d(1,12,5) # 12, 24x24
self.conv2=nn.Conv2d(12, 20,3) #20, 10x10
self.conv3=nn.Conv2d(20, 40,3) #20, 10x10
self.batchnorm2d = nn.BatchNorm2d(40)
self.fc1=nn.Linear(40*8*8, 500)
self.fc2=nn.Linear(500, 10)
def forward(self, x): #网络传播结构
in_size=x.size(0)# in_size 为 batch_size(一个batch中的Sample数)
# 卷积层 -> relu -> 最大池化
out = self.conv1(x) # 24
out = F.relu(out)
out = F.max_pool2d(out, 2, 2) # 12
out = self.conv2(out) # 10
out = F.relu(out)
out = self.conv3(out)
out = F.relu(out)
out = self.batchnorm2d(out)
out = out.view(in_size, -1) # view()函数作用是将一个多行的Tensor,拼接成行。
# 输出前的预处理
out = self.fc1(out)
out = F.relu(out)
out = self.fc2(out)
# softmax
out = F.log_softmax(out, dim=1)
# 返回值 out
return out
网络实例化与优化器
model = Net().to(DEVICE)
optimizer = optim.Adam(model.parameters())
定义训练函数
- 将训练的所有操作封装到函数Train中,这样在最后直接调用函数就显得非常简洁
- 采用的损失函数是
torch.functional.nll_loss
,后面讨论环节也会分析不同loss函数的结果差异 with torch.no_grad()
表示不反向求导。训练过程需要反向求导(去更新优化模型参数),测试过程不需要反向求导。- print(‘Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}’.format(
epoch_i, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))采用了比较简洁
方式将结果输出
def train(model, device, train_loader, optimizer, epoch):
model.eval()
for epoch_i in range(epoch+1):
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data, target # CPU转GPU
optimizer.zero_grad() # 优化器清零
output = model(data) # 由model,计算输出值
loss = F.nll_loss(output, target) # 计算损失函数loss
loss.backward() # loss反向传播
optimizer.step() # 优化器优化
if(batch_idx+1)%30 == 0: # 输出结果
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch_i, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
def test(model, device, test_loader):
test_loss = 0 # 损失函数初始化为0
correct = 0 # correct 计数分类正确的数目
with torch.no_grad():
for data, target in test_loader: # 遍历所有的data和target
data, target = data.to(device), target.to(device) # CPU -> GPU
output = model(data) # output为预测值,由model计算出
test_loss += F.nll_loss(output, target, reduction='sum').item() ### 将一批的损失相加
pred = output.max(1, keepdim=True)[1] ### 找到概率最大的下标
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset) # 总损失除数据集总数
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
函数调用
train(model, DEVICE, train_loader, optimizer, epoch)
test(model, DEVICE, test_loader)
结果分析
构建不同卷积层对输出结果分析
- 在将循环次数设置为20下,然后取平均值
- 两层卷积网,在测试集上平均损失为 0.0363,
- 三层卷积网,在测试集上平均损失为0.0296
- 对结果进行分析,对于精度略有提升,还可以进行更多层的对比
- 两层卷积层、
class Net(nn.Module): # 继承model
def __init__(self):
super().__init__()
# 28x28
self.conv1=nn.Conv2d(1,12,5)
self.conv2=nn.Conv2d(12, 20,3)
self.fc1=nn.Linear(20*10*10, 500)
self.fc2=nn.Linear(500, 10)
def forward(self, x): #网络传播结构
# 卷积层 -> relu -> 最大池化
out = self.conv1(x) # 24
out = F.relu(out)
out = F.max_pool2d(out, 2, 2) # 12
out = self.conv2(out) # 10
out = F.relu(out)
out = out.view(in_size=x.size(0), -1) # view()函数作用是将一个多行的Tensor,拼接成行。
# 输出前的预处理结束,提取完特征信息
out = self.fc1(out)
out = F.relu(out)
out = self.fc2(out)
# softmax
out = F.log_softmax(out, dim=1)
# 返回值 out
return out
Test set: Average loss: 0.0363, Accuracy: 9889/10000 (99%)
- 三层卷积层
class Net(nn.Module): # 继承model
def __init__(self):
super().__init__()
# 28x28
self.conv1=nn.Conv2d(1,12,5) # 12, 24x24
self.conv2=nn.Conv2d(12, 20,3) #20, 10x10
self.conv3=nn.Conv2d(20, 40,3) #20, 10x10
self.batchnorm2d = nn.BatchNorm2d(40)
self.fc1=nn.Linear(40*8*8, 500)
self.fc2=nn.Linear(500, 10)
def forward(self, x): #网络传播结构
in_size=x.size(0)# in_size 为 batch_size(一个batch中的Sample数)
# 卷积层 -> relu -> 最大池化
out = self.conv1(x) # 24
out = F.relu(out)
out = F.max_pool2d(out, 2, 2) # 12
out = self.conv2(out) # 10
out = F.relu(out)
out = self.conv3(out)
out = F.relu(out)
out = self.batchnorm2d(out)
out = out.view(in_size, -1) # view()函数作用是将一个多行的Tensor,拼接成行。
# 输出前的预处理
out = self.fc1(out)
out = F.relu(out)
out = self.fc2(out)
# softmax
out = F.log_softmax(out, dim=1)
# 返回值 out
return out
Test set: Average loss: 0.0296, Accuracy: 9898/10000 (99%)
卷积层之间的通道数设置
- 卷积层之间的通道数设置为超参,如何设置,影响着结果,以两层网络为例,改变第一层卷积网络输出通道,看测试集上的结果,循环次数设置为20
- 输出通道为10,11,12,13,14,测试集上平均损失为
- 0.0315,0.0313,0.03630,0386,0.0284
- 在经调试,第一层通道设置为14较好
- 输出通道为10,11,12,13,14,测试集上平均损失为
self.conv1=nn.Conv2d(1,10,5) # 改变输出通道10,11,12,13,14
self.conv2=nn.Conv2d(10, 20,3) #对应上一层输出通道
self.fc1=nn.Linear(20*10*10, 500)
self.fc2=nn.Linear(500, 10)
卷积层间的relu以及pooling
- 在卷积层后将
sigmoid()
都改成使用F.relu
(和nn.relu产不多),AlexNet相对于LeNet有一改变就是使用了relu函数,使用了relu克服sigmoid在反向求导时,导数不可算的情况 - 池化层作用:池化层能够克服
过拟合
。池化层的存在,会不断提取一定范围内最强烈的特征,并且缩小张量的大小,使得大范围内的特征组合也能够捕捉到,pooling的结果是使得特征减少,参数减少,但pooling的目的并不仅在于此。pooling目的是为了保持某种不变性(旋转、平移、伸缩等),常用的有mean-pooling,max-pooling和Stochastic-pooling三种,该文建立的网络中采取的是最大池化- 特征提取的误差主要来自两个方面:
- 邻域大小受限造成的估计值方差增大;
- 卷积层参数误差造成估计均值的偏移
- 一般来说,mean-pooling能
减小第一种误差
,更多的保留图像的背景信息; - max-pooling能
减小第二种误差
,更多的保留纹理信息; - Stochastic-pooling则
介于两者之间
,通过对像素点按照数值大小赋予概率,再按照概率进行亚采样,在平均意义上,与mean-pooling近似,在局部意义上,则服从max-pooling的准则。
- 特征提取的误差主要来自两个方面:
- 在设置了最大池化与不进行池化,测试集上平均损失进行对比,循环次数设置为20
损失函数
- 损失函数有许多,在模型中选择了不同的loss函数对于结果也通样是有影响的,在该文中选择的是
torch.functional.null_loss
,- 用于多分类的负对数似然损失函数(Negative Log Likelihood)
l o s s ( x , l a b e l ) = − X l a b e l loss(x,label)=-X_{label} loss(x,label)=−Xlabel
事实上,nn.CrossEntropyLoss 也是调用这个函数。注意这里的和交叉熵损失里的不一样,nll是经过 logSoftMax运算后的数值
- 用于多分类的负对数似然损失函数(Negative Log Likelihood)
小结
- 对于不同的问题设计结构,再进行调参,进行有目标的,设计,得到的效果也会更好,在此文中仅仅只是对模型中部分进行了对比测试,具体的参数也只是进行小范围,以测试集上的效果为目标进行调参
总结与感悟
- 在学习了Pytorch相关资料后,实现了mnist分类任务,从开始的一点不会,通过查资料,慢慢读懂一部分程序,这个过程还是学到了不少,但是由于里面内容是很丰富,还有许多问题理解得不够深入仅仅是会用,在下一步学习中,多思考,多查资料,多记笔记,也要提升自己的编程能力。
- 参考
https://blog.csdn.net/qq_41683065/article/details/91374189