似乎所有程序员在学习一个新的程序语言时,都想要打印输出一个“hello world”,它代表了你入门了这门语言。那么,MNIST手写数字识别便是入门机器学习和深度学习的“hello world”。跑通MNIST程序便能大致了解机器学习的流程,包括数据的读取、转换(totensor)、归一化、神经网络模型设计、超参数设计、训练、前向传播、后向传播等等。在入门机器学习之前先自己跑通一遍MNIST识别程序具有非凡的意义。
MNIST
(Mixed National Institute of Standards and Technologydatabase)是一个手写数字的大型数据库,拥有60,000个示例的训练集和10,000个示例的测试集。更详细的介绍可以查看 Yann LeCun的MNIST数据集官网。
本程序来自pytorch官方提供的MNIST示例代码,链接:
https://github.com/pytorch/examples/blob/master/mnist/main.py
在经过修改并添加训练结果可视化和特征图可视化等功能,github链接在本文最下方。
下面讲解train.py中的代码:
from __future__ import print_function
import argparse
import os
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.optim.lr_scheduler import StepLR
from pathlib import Path
import time
# import network
from model.network.LeNet import LeNet
from model.network.MyNetV1 import MyNetV1
from model.network.MyNetV2 import MyNetV2
from model.network.DefaultNet import DefaultNet
from model.network.LeNet5 import LeNet5
from model.network.MyFullConvNet import MyFullConvNet
from model.network.MyVggNet import MyVggNet
导入训练网络需要的模块,其中值得注意的是:
• argparse模块,该模块允许你在运行.py
文件时可以附带参数,如:python train.py --model lenet
• torch基本模块,即pytorch基本的库
• matplotlib模块,用于绘制loss曲线和acc曲线图,也用于显示模型中各层特征图即特征图可视化
通过argparse
模块,可以在运行文件时添加运行所需要的参数。这些参数可以用于设置网络模型的超参数,如学习率
、batch-size
、epochs
、训练模型等等。下面贴出代码:
# Training settings
parser = argparse.ArgumentParser(description="Pytorch MNIST Example")
parser.add_argument("--batch-size", type=int, default=64, metavar="N",
help="input batch size for training (default : 64)")
parser.add_argument("--test-batch-size", type=int, default=1000, metavar="N",
help="input batch size for testing (default : 1000)")
parser.add_argument("--epochs", type=int, default=64, metavar="N",
help="number of epochs to train (default : 64)")
parser.add_argument("--learning-rate", type=float, default=0.1, metavar="LR",
help="number of epochs to train (default : 14)")
parser.add_argument("--gamma", type=float, default=0.5, metavar="M",
help="Learning rate step gamma (default : 0.5)")
parser.add_argument("--no-cuda", action="store_true", default=True,
help="disables CUDA training")
parser.add_argument("--dry-run", action="store_true", default=False,
help="quickly check a single pass")
parser.add_argument("--seed", type=int, default=1, metavar="S",
help="random seed (default : 1)")
parser.add_argument("--log-interval", type=int, default=10, metavar="N",
help="how many batches to wait before logging training status")
parser.add_argument("--save-model", action = "store_true", default=True,
help="For saving the current Model")
parser.add_argument("--load_state_dict", type=str, default="no",
help="load the trained model weights or not (default: no)")
parser.add_argument("--model", type=str, default="LeNet",
help="choose the model to train (default: LeNet)")
args = parser.parse_args()
值得注意的是:
2^n
为大小。如果batch-size过小,就好像你每次数钱只数一张(而不是好几张一起数),训练数据效率就低下,且收敛困难;如果batch-size过大,虽然相对处理速度加快,但是所需要的内容容量增加,可能会出现 CPU/GPU 内存容量不足等情况,所以需要根据图片具体大小、模型复杂度和计算机性能之间权衡batch-size的大小其他参数大概就是字面意思。要注意必须有args = parse.parse_args()
这一句,意思是把爬取到的参数信息赋值到变量args上,后续便可以通过args得到参数值,比如args.model, args.learning_rate。
以下代码是程序加载数据和对数据进行转化(ToTensor)的代码:
train_kwargs = {
"batch_size": args.batch_size}
test_kwargs = {
"batch_size": args.test_batch_size}
if use_cuda:
cuda_kwargs = {
"num_workers": 1, "pin_memory": True, "shuffle": True}
train_kwargs.update(cuda_kwargs)
test_kwargs.update(cuda_kwargs)
transform = transforms.Compose([
transforms.ToTensor(),
# normalize(mean, std, inplace=False) mean各通道的均值, std各通道的标准差, inplace是否原地操作
# 这里说的均值是数据里的均值
# output = (input - mean) / std
# 归一化到-1 ~ 1,也不一定,但是属于标准化
transforms.Normalize((0.1307, ), (0.3081, ))
])
dataset1 = datasets.MNIST("./data", train=True, download=True,
transform=transform)
dataset2 = datasets.MNIST("./data", train=False,
transform=transform)
train_loader = torch.utils.data.DataLoader(dataset1, **train_kwargs)
test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)
num_workers
是多进程的加载数,pin_memory
是是否将数据保存在pin memory
区,pin memory
中的数据转到CPU会比较快。另外几个值得注意的点是:
mean
是数据里的均值,std
是各通道的标准差graph_loss = []
graph_acc = []
def train(args, model, device, train_loader, optimizer, epoch):
# 这里的train和上面的train不是一个train
model.train()
start_time = time.time()
tmp_time = start_time
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % args.log_interval == 0:
print("Train Epoch: {} [{}/{} ({:.0f}%)]\t Loss: {:.6f}\t Cost time: {:.6f}s".format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item(), time.time() - tmp_time
))
tmp_time = time.time()
graph_loss.append(loss.item())
if args.dry_run:
break
end_time = time.time()
print("Epoch {} cost {} s".format(epoch, end_time - start_time))
定义全局变量graph_loss
和 graph_acc
分别记录训练过程中的准确率和损失,最后写入.txt
文件中,方便后续查看和数据可视化。简单的说一下训练的流程:
测试训练结果在代码在test_model.py
里,测试内容是测试集的前1000张图片(这个数字可以在argparse里面修改)。输入:
python test_model.py --model lenet
在训练的过程中,我将训练的结果存放在graph_loss
和graph_acc
里,并且在训练结束之后,将两个列表中的数据存储到.txt文件当中。现在,便可以从.txt文件中读取训练结果并显示出来。用于画图的工具是matplotlib,而相关的代码文件是draw_graph.py,以下为代码:
import matplotlib
import os
import numpy as np
from matplotlib import pyplot as plt
import argparse
import sys
parser = argparse.ArgumentParser()
parser.add_argument("--model", type=str, default="lenet")
args = parser.parse_args()
#file_loss_path = "E:/WorkSpace/Pytorch/mnist/model/result/{}_loss.txt".format(args.model)
file_loss_path = sys.path[0] + "/model/result/{}_loss.txt".format(args.model)
lst_loss = list()
with open(file_loss_path) as file_object:
for line in file_object:
if "e" in line:
lst_loss.append(eval(line))
else:
lst_loss.append(float(line[:-2]))
file_object.close()
#file_acc_path = "E:/WorkSpace/Pytorch/mnist/model/result/{}_acc.txt".format(args.model)
file_acc_path = sys.path[0] + "/model/result/{}_acc.txt".format(args.model)
lst_acc = list()
with open(file_acc_path) as file_object:
for line in file_object:
if "e" in line:
lst_acc.append(eval(line))
else:
lst_acc.append(float(line[:-2]))
file_object.close()
print(lst_acc)
plt.title("{} loss".format(args.model))
plt.plot(lst_loss)
plt.xlim(0 - len(lst_loss) / 20, len(lst_loss))
plt.ylim(0, 1.5)
plt.grid()
plt.savefig(file_loss_path[:-3] + "jpg")
plt.title("{} acc".format(args.model))
plt.plot(lst_acc)
plt.xlim(0 - len(lst_acc) / 20, len(lst_acc))
plt.ylim(min(lst_acc) - 1, max(max(lst_acc) + 1, 100))
plt.savefig(file_acc_path[:-3] + "jpg")
通过matplotlib,读取.txt文件中的数据,将其以图表的形式显示并且保存下来,以下为效果:
64个epcoh的训练准确率:
训练过程中的损失:
在学习本程序的过程中,我也在学习一些经典网络,比如LeNet, AlexNet, VggNet等,所以尝试着自己搭建网络并将经典网络中的优点融入其中,以下为不同网络准确率:
光是看代码,是难以理解卷积神经网络是如何识别数字的。所以不如将卷积神经网络中每一层输出的特征图显示出来,便能知道在卷积神经网络这个黑盒子里,到底发生了什么。
实现特征图可视化的基本思想是:卷积神经网络处理的数据类型是tensor(张量),张量是无法用于显示图片的,所以需要将其转换成可以显示为图片的数据类型,比如numpy。再通过matplotlib,将其显示出来。
具体实现通过我定义的函数show_graph()
实现,定义在了./model/network/LeNet.py
中,你可以去掉相应的注释来显示特征图或者保存特征图。
具体代码请到我的github上查看,链接在本文的最下方。
通过观察图片和观察具体tensor的输出可以发现,MNIST数据集存放的是一通道的黑白图片,其中的像素值是0~255,其特征较为简单。
下图是经过一次卷积之后的特征图:
为什么会有6张呢,因为第一层的卷积层输出通道数是6,所以会生成6张不同的特征图。虽然看着感觉六个特征图差别不大,那是因为数据集过于简单,如果是复杂一些的图片,便能看到其不同。图片经过卷积之后,图像像素从28 * 28变成了26 * 26。现在用肉眼还能勉强看出来是个数字8。
下面显示卷积->ReLu激活函数后的图像:
这一层的输出同样是6张,但是由于篇幅就不全部贴出来了。看了图像之后马上就能理解,ReLu激活函数干了什么。通俗的讲:将黑的地方变得更黑,白的地方保持不变。ReLu的表达式:
使小于0的数字等于0,大于0的数字则保持不变。(因为训练过程中,图像数据都是经过归一化处理的,使得像素值的范围为-1~1)。
下面显示卷积->ReLu激活函数->最大池化层的图像:
显而易见,图像经过最大池化层之后,像素缩小了一半,这也正是池化层(下采样)的作用:缩小图像尺寸。能减少网络的计算量,也能在一定程度上缓解过拟合的问题。但这只是一定程度上,并且池化层可能会过滤掉很多有用的特征。
值得注意的是,本程序用的池化层都是最大池化max_pooling,所以可以发现,相比较前一层的特征图,整体图片的亮度变得更亮了,因为最大池化是选择区域中值最大的值进行保留。
下图显示的是多次卷积和激活函数和最大池化后的图片:
可以看到,经过多次卷积、激活函数和池化层之后,图像已经“面目全非”,肉眼已经完全分辨不出图片原本代表着什么数字。但是对于神经网络来说,图片永远只是一堆数字,这张图片也是神经网络计算出来的数字特征。在经过这一步之后,通过将图片展平即变为一维向量x = x.view(x.size(0), -1)
。由于是最后一层是4 * 4 * 16的输出,所以展平后就得到了长度为4 * 4 * 16 = 256的一维向量。再通过全连接层,可将一维向量变为长度为10的输出(0-9共10类)。
因为进入全连接层之后,tensor已经变为了一维向量,无法以图片的形式显示,所以只能输出看具体的数字。以下为LeNet全连接层的代码:
self.fc1 = nn.Linear(16 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
并且输出全连接层各个位置的张量大小以及最后一层的输出:
x = x.view(x.size(0), -1)
print(x[0].size())
x = self.fc1(x)
print(x[0].size())
x = F.relu(x)
x = self.fc2(x)
print(x[0].size())
x = F.relu(x)
x = self.fc3(x)
print(x[0])
输出结果如下:
可以看到,展平后的张量长度的确为256,经过一层全连接层之后长度变为120,然后是84,最后输出最后一层的张量。可以看到,张量每个元素的值代表着该类的概率,由于该图片识别为8,所以在index为8的地方值最大,最后得出该手写体为数字“8”的结论。softmax也是根据张量计算出最后的预测值。
完整程序放在了我的github上,觉得有帮助的话,点个star☆
吧~
如果有大佬路过的话,请多多指教~~
链接:https://github.com/cssdcc1997/pytorch-mnist