主要是使用LeNet进行手写数字识别。
一、介绍LeNet网络
- 结构图
- 详细展开
它的输入尺寸是32×32。
- 重点介绍一下卷积层、池化层和全连接层
卷积层:通过卷积运算,可以使原信号特征增强,并且降低噪音。
# ------------------------------------------------------------------
解析代码:nn.Conv2d(1, 6, 5, stride=1, padding=2)
第一个参数:输入图片的维数(灰度为1,彩色图像为3)
第二个参数:使用5*5大小的过滤器6个
第三个参数:卷积核(过滤器)大小为5*5
第四个参数:stride为每次移动卷积核的步长为1
第五个参数:padding为填充
如上代码, 对于mnist内的图片大小为28*28,而LeNet网络的输入大小为32*32
根据padding的大小,将mnist填充为32*32
经过卷积层(32 - 5 + stride)输出的特征图大小为28*28*6
# ----------------------------------------------------------------
池化层(下采样层):利用图像局部相关性的原理,对图像进行子抽样,可以1.减少数据处理量同时保留有用信息,2.降低网络训练参数及模型的过拟合程度
# ------------------------------------------------------------------
解析代码:nn.ReLU(True)
nn.MaxPool2d(2, 2)
激活后池化,使用2*2大小的过滤器,步长为2,padding=0.
如上代码,输入28*28*6大小的特征图,池化后,输出14*14*6大小的特征图
# ----------------------------------------------------------------
全连接层:
# ------------------------------------------------------------------
解析代码:nn.Linear(400, 120)
每个单元与上一次的全部5*5*16(400)个单元直接进行全连接。120*(400+1)=48120个可训练参数。如同经典神经网络,全连接层层计算输入向量和权重向量之间的点积,再加上一个偏置。
# ----------------------------------------------------------------
输出层:
# ------------------------------------------------------------------
输出层由欧式径向基函数(Euclidean Radial Basis Function)单元组成,每类一个单元,每个有84个输入。
换句话说,每个输出RBF单元计算输入向量和参数向量之间的欧式距离。
输入离参数向量越远,RBF输出的越大。
用概率术语来说,RBF输出可以被理解为F6层配置空间的高斯分布的负log-likelihood。
给定一个输式,损失函数应能使得F6的配置与RBF参数向量(即模式的期望分类)足够接近。
# ----------------------------------------------------------------
- LeNet网络的特点
1)、每个卷积层包含三部分:卷积、池化和非线性激活函数
2)、使用卷积提取空间特征
3)、降采样(Subsample)的平均池化层
4)、双曲正切(tanh)和S型(Sigmoid)的激活函数,MLP作为最后的分类器
5)、层与层之间的稀疏连接减少计算复杂度
二、加载数据集
在这之前先看一下数据预处理:
直接看https://pytorch.org/docs/stable/torchvision/transforms.html#
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
# 下载训练集 MNIST 手写数字训练集
train_dataset = datasets.MNIST(
root='./mnist', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(
root='./mnist', train=False, transform=transforms.ToTensor())
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# -------------------------------------------------------------------------------
torchvision.datasets.MNIST(
root,
train = True,
transform = None,
target_transform = None,
download = False )
root(string):保存数据集的根目录
train(bool):如果为true,则从training.pt中创建数据集,否则从test.pt中创建数据集
transform(可调用的):一个方法或者transform,它接受一个PIL图像并返回transform,例如
transforms.RandomCrop
download(bool):如果true,则从网上下载数据集并放在根目录下,如果已经下载了就不会再次下载。
target_transform :接受目标并对其进行转换的函数/转换
# -----------------------------------------------------------------------------------
DataLoader(
dataset, # 数据集
batch_size=1, # int,每个批次要加载的样本数,默认为1
shuffle=False, # bool,设置为true,则每个epoch,reshuffle数据,默认为false
sampler=None, # sample,定义数据集中提取样本的策略,如果指定,则shuffle必须为false
batch_sampler=None, # 类似sampler,但是一次返回一批索引。互斥有batch_size,shuffle,
# sampler,和drop_last。
num_workers=0, # int,用于数据加载的子进程数。0表示将在主进程中加载数据,默认值为0
collate_fn=None, # 合并样本列表以形成张量的小批量
pin_memory=False,
drop_last=False,
timeout=0,
worker_init_fn=None)
# --------------------------------------------------------------------------------------
三、定义LeNet网络模型
# 定义LeNet网络
class Cnn(nn.Module):
# nn.Moudle子类的函数必须在构造函数中执行父类的构造函数
# 下式等价与nn.Moudle.__init__(self)
def __init__(self, in_dim, n_class):
super(Cnn, self).__init__()
# super用法:Cnn继承父类nn.Model的属性,并用父类的方法初始化这些属性
self.conv = nn.Sequential(
# padding=2保证输入输出尺寸相同(参数依次是:输入维度,输出深度,ksize,步长,填充)
# 卷积层‘1’表示输入图片为单通道,‘6’表示输出通道数
# ‘5’表示卷积核为5*5
nn.Conv2d(in_dim, 6, 5, stride=1, padding=2),
nn.ReLU(True),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5, stride=1, padding=0),
nn.ReLU(True),
nn.MaxPool2d(2, 2))
# 全连接层
self.fc = nn.Sequential(
nn.Linear(400, 120),
nn.Linear(120, 84),
nn.Linear(84, n_class))
# 前向传播
def forward(self, x):
out = self.conv(x)
# print(out.size()):torch.Size([128, 16, 5, 5])
# 而经过卷积池化后的特征图大小为5*5*16
# 但是全连接层的输入为400,所有要将out的大小改为400
# 使用view
out = out.view(out.size(0), -1)
# 此时out的size为400,由于批次为128,所有128*400
out = self.fc(out)
# 返回out的size为128*10
return out
# 实例化LeNet网络
model = Cnn(1, 10) # 图片大小为28*28,图片维度为1,最终输出的是10类
四、定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 交叉熵损失
# 随机梯度下降法SGD,指定要调整的参数和学习率
# learning_rate = 1e-2 # 学习率
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
五、训练阶段
num_epoches = 20 # 遍历训练集的次数
for epoch in range(num_epoches):
running_loss = 0.0 # 运行损失
running_acc = 0.0 # 精度
# ----------------------------------------------
# 对enumerate(iterator, start)的解释:
# numerate()用于将可迭代、可遍历的数据对象组合为一个索引序列,同时列出数据和数据下标。
# e2 = enumerate(list, 4)
# for i in e2:
# print(i)
# 输出结果:
# (4, 'A')
# (5, 'B')
# (6, 'C')
# (7, 'D')
# ----------------------------------------------
# 遍历训练集的每一张图片
for i, data in enumerate(train_loader, 1):
img, label = data
#-------------------------------
# cuda
#if use_gpu:
#img = img.cuda()
#label = label.cuda()
# ------------------------------
# 将img和label转换为Variable
img = Variable(img)
label = Variable(label)
# 前向传播
out = model(img)
# 计算损失
loss = criterion(out, label) # 计算交叉熵损失
running_loss += loss.item() * label.size(0) #
_, pred = torch.max(out, 1) # 预测最大值所在位置的标签,即预测的数字。维度设为1
num_correct = (pred == label).sum() # 预测正确的数目
accuracy = (pred == label).float().mean()
running_acc += num_correct.item() # 统计预测正确的总数
# 反向传播
# grad在反向传播过程中是累加的,这意味着每次运行反向传播,梯度都会累加之前的梯度,
# 所以反向传播之前需把梯度清零。
optimizer.zero_grad()
loss.backward()
optimizer.step() # 更新参数
print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
epoch + 1, running_loss / (len(train_dataset)), running_acc / (len(train_dataset))))
六、测试阶段
# model.train() :启用 BatchNormalization 和 Dropout
# model.eval() :不启用 BatchNormalization 和 Dropout
# 训练阶段需要启用,而测试阶段不需要。
model.eval() # 模型评估
eval_loss = 0 # 评估损失
eval_acc = 0 # 评估精度
for data in test_loader:
img, label = data
# -------------------------------------------------
# if use_gpu:
# img = Variable(img, volatile=True).cuda()
# label = Variable(label, volatile=True).cuda()
# else:
# img = Variable(img, volatile=True)
# label = Variable(label, volatile=True)
# --------------------------------------------------
# 开始测试图片
out = model(img)
loss = criterion(out, label)
eval_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1) # 预测最大值所在位置的标签,即预测的数字。维度设为1
num_correct = (pred == label).sum() # 预测正确的数目
eval_acc += num_correct.item() # 统计预测正确的数目
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
test_dataset)), eval_acc / (len(test_dataset))))
七、保存和加载模型
- 保存模型
保存整个模型的结构和参数,保存对象为model
torch.save(model,’./model.pth’)
保存对象的参数,保存的对象是模型的状态model.state_dict()
torch.save(model.state_dict(),’./model_state.pth)
# 保存模型
# 将状态为state_dict()的模型保存在当前目录下,命名为cnn.pth
torch.save(model.state_dict(), './cnn.pth')
- 加载模型
加载模型结构和参数
load_model=torch.load('model.pth')
加载模型参数信息,需要先导入模型结构,然后通过model.load_state_dic(torch.load(‘model_state.pth’))导入
# 加载模型
load_model = torch.load('cnn.pth')
八、总结
- 在处理一个数据集时,首先要查看该数据集的结构,每张图片的大小。
- 设置好超参数,比如学习率、训练次数等。
- 了解采用网络的整体框架,知道每一步的流程。
- 要知道训练模型与测试模型之间的区别。
- 测试模型时,不需要网络的后面两层。(视情况而定)
- 在后向传播之前要记得梯度清零。
- 熟悉所采用的损失函数,为什么要采用损失函数。