PyTorch基础——使⽤卷积神经⽹络识别⼿写数字⼀、介绍
实验内容
内容包括⽤ PyTorch 来实现⼀个卷积神经⽹络,从⽽实现⼿写数字识别任务。
除此之外,还对卷积神经⽹络的卷积核、特征图等进⾏了分析,引出了过滤器的概念,并简单⽰了卷积神经⽹络的⼯作原理。知识点
使⽤ PyTorch 数据集三件套的⽅法
卷积神经⽹络的搭建与训练
可视化卷积核、特征图的⽅法
⼆、数据准备
引⼊相关包
import torch
as nn
from torch.autograd import Variable
import torch.optim as optim
functional as F
import torchvision.datats as dts
ansforms as transforms
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
定义超参数
# 定义超参数
image_size = 28 #图像的总尺⼨28*28
num_class = 10 #标签的种类数
num_epochs = 20 #训练的总循环周期
batch_size = 64 #⼀个撮(批次)的⼤⼩,64张图⽚
使⽤ PyTorch 数据加载三套件
PyTorch ⾃带的数据加载器,包括datat,sampler,以及data loader这三个对象组成的套件。
为什么要使⽤ PyTorch ⾃带的数据加载器?
当数据集很⼩,格式⽐较规则的时候,数据加载三套件的优势并不明显。
但是当数据格式⽐较特殊,以及数据规模很⼤(内存⽆法同时加载所有数据)的时候,三套件的威⼒就会显现出来了。
特别是,当需要⽤不同的处理器来并⾏加载数据的时候,PyTorch 数据加载器还可以⾃动进⾏数据的分布式加载。
本次使⽤datat来装载数据集,使⽤sampler采样数据集,使⽤data_loader完成数据集的迭代和循环。
下⾯⾸先创建 2 个datat,分别⽤来装载训练数据集和测试数据集。
# 加载 MNIST 数据,如果没有下载过,就会在当前路径下新建 /data ⼦⽬录,并把⽂件存放其中
# MNIST 数据是属于 torchvision 包⾃带的数据,所以可以直接调⽤。
train_datat = dts.MNIST(root='./data', #⽂件存放路径
train=True, #提取训练集
#将图像转化为 Tensor,在加载数据的时候,就可以对图像做预处理
transform=transforms.ToTensor(),
download=True) #当找不到⽂件的时候,⾃动下载
# 加载测试数据集
test_datat = dts.MNIST(root='./data',
train=Fal,
transform=transforms.ToTensor())
# 如果想要调⽤⾮ PyTorch 的⾃带数据,⽐如⾃⼰准备的数据集,
# 可以⽤ torchvision.datats.ImageFolder 或者 torch.utils.data.TensorDatat 来加载
data_loader⽤于完成数据集的迭代和循环,也称为数据加载器。
下⾯将为训练数据集创建⼀个数据加载器。
# 训练数据集的加载器,⾃动将数据分割成batch,顺序随机打乱
train_loader = torch.utils.data.DataLoader(datat=train_datat,
batch_size=batch_size,
shuffle=True)
下⾯将测试数据分成两部分,⼀部分作为校验数据,⼀部分作为测试数据。
校验数据⽤于检测模型是否过拟合,并调整参数,测试数据检验整个模型的⼯作。
演讲比赛活动策划方案
# ⾸先创建 test_datat 中所有数据的索引下标
indices = range(len(test_datat))
# 利⽤数据下标,将 test_datat 中的前 5000 条数据作为校验数据
indices_val = indices[:5000]
# 剩下的就作为测试数据了
indices_test = indices[5000:]
采样器(sampler)为加载器(data_loader)提供了⼀个从每⼀批数据中抽取样本的⽅法。
不同的采样器有不同的采样⽅法,有些采样器可以随机采样,有些可以根据权重进⾏采样,甚⾄还有些可以根据某种概率分布从数据集中进⾏采样。
# 根据分好的下标,构造两个数据集的 SubtRandomSampler 采样器,它会对下标进⾏采样
sampler_val = torch.utils.data.sampler.SubtRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler.SubtRandomSampler(indices_test)
# 校验数据集的加载器
validation_loader = torch.utils.data.DataLoader(datat =test_datat,
batch_size = batch_size,
sampler = sampler_val
)
# 验证数据集的加载器
test_loader = torch.utils.data.DataLoader(datat=test_datat,
batch_size=batch_size,
sampler = sampler_test
)
通过 Matplotlib 将数据集中的⼿写数字绘制出来
# 随便指定⼀个数据下标
idx = 100
# datat ⽀持下标索引
大爱无声作文
# 其中提取出来的每⼀个元素为 features,target格式,即属性和标签
# [0] 表⽰索引 features
muteimg = train_datat[idx][0].numpy()
# 由于⼀般的图像包含rgb三个通道,⽽MINST数据集的图像都是灰度的,只有⼀个通道
# 因此,我们忽略通道,把图像看作⼀个灰度矩阵
plt.imshow(muteimg[0,...], cmap ='gray')
print('标签是:',train_datat[idx][1])
基本的卷积神经⽹络
构建⽹络
下⾯将要调⽤ PyTorch 强⼤的nn.Module这个类来构建卷积神经⽹络。步骤如下:
1.⾸先构造 ConvNet 类,它是对类 nn.Module 的继承。
2.重写⽗类的init以及forward这两个函数。
3.在 ConvNet 类中编写⾃定义的⽅法。
在第⼆步中重写的init为构造函数,每当类ConvNet被具体化⼀个实例的时候,就会被调⽤。
forward函数则是在运⾏神经⽹络正向的时候会被⾃动调⽤。
# 定义卷积神经⽹络:4 和 8 为⼈为指定的两个卷积层的厚度(feature map的数量)
depth = [4, 8]
class ConvNet(nn.Module):
def __init__(lf):
# 该函数在创建⼀个 ConvNet 对象的时候,即调⽤如下语句:net=ConvNet(),就会被调⽤
# ⾸先调⽤⽗类相应的构造函数
super(ConvNet, lf).__init__()
# 其次构造ConvNet需要⽤到的各个神经模块。
'''注意,定义组件并没有真正搭建这些组件,只是把基本建筑砖块先找好'''
lf.pool = nn.MaxPool2d(2, 2) #定义⼀个Pooling层,⼀个窗⼝为2*2的pooling运算
#输出通道为depth[1],窗⼝为5,padding为2
lf.fc1 = nn.Linear(image_size // 4 * image_size // 4 * depth[1] , 512)
#⼀个线性连接层,输⼊尺⼨为最后⼀层⽴⽅体的平铺,输出层512个节点
lf.fc2 = nn.Linear(512, num_class) #最后⼀层线性分类单元,输⼊为512,输出为要做分类的类别数
def forward(lf, x):
#该函数完成神经⽹络真正的前向运算,我们会在这⾥把各个组件进⾏实际的拼装
#x的尺⼨:(batch_size, image_channels, image_width, image_height)
x = F.v1(x)) #第⼀层卷积,激活函数⽤ReLu,为了防⽌过拟合
#x的尺⼨:(batch_size, num_filters, image_width, image_height)
x = lf.pool(x) #第⼆层pooling,将图⽚变⼩
#x的尺⼨:(batch_size, depth[0], image_width/2, image_height/2)
x = F.v2(x)) #第三层⼜是卷积,窗⼝为5,输⼊输出通道分别为depth[0]=4, depth[1]=8
#x的尺⼨:(batch_size, depth[1], image_width/2, image_height/2)
x = lf.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(lf.fc1(x)) #第五层为全链接,ReLu激活函数
#x的尺⼨:(batch_size, 512)
x = F.dropout(x, aining) #以默认为0.5的概率对这⼀层进⾏dropout操作,为了防⽌过拟合
x = lf.fc2(x) #全链接
#x的尺⼨:(batch_size, num_class)
#输出层为log_softmax,即概率对数值log(p(x))。采⽤log_softmax可以使得后⾯的交叉熵计算更快
x = F.log_softmax(x, dim = 1)
return x
def retrieve_features(lf, x):
#该函数专门⽤于提取卷积神经⽹络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图
feature_map1 = F.v1(x)) #完成第⼀层卷积
x = lf.pool(feature_map1) # 完成第⼀层pooling
feature_map2 = F.v2(x)) #第⼆层卷积,两层特征图都存储到了feature_map1, feature_map2中
return (feature_map1, feature_map2)
训练卷积神经⽹络
⾸先编写⼀个计算预测错误率的函数,其中predictions是模型给出的⼀组预测结果,batch_size⾏num_class列的矩阵,labels是数据中的正确答案。
def rightness(predictions, labels):
# 对于任意⼀⾏(⼀个样本)的输出值的第1个维度,求最⼤,得到每⼀⾏的最⼤元素的下标
熟石灰化学式pred = torch.max(predictions.data, 1)[1]
# 将下标与labels中包含的类别进⾏⽐较,并累计得到⽐较正确的数量
rights = pred.eq(labels.data.view_as(pred)).sum()
# 返回正确的数量和这⼀次⼀共⽐较了多少元素
return rights, len(labels)
⾸先实例化模型,定义损失函数和优化器。
net = ConvNet() #新建⼀个卷积神经⽹络的实例,此时ConvNet的__init__函数就会被⾃动调⽤
criterion = nn.CrossEntropyLoss() #Loss函数的定义,交叉熵
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) #定义优化器,普通的随机梯度下降算法
把训练模型和验证模型的语句封装成函数
# 参数:
# data : torch.Variable
# target: torch.Variable
def train_model(data, target):
# 给⽹络模型做标记,标志说模型正在训练集上训练
# 这种区分主要是为了打开 net 的 training 标志
# 从⽽决定是否运⾏ dropout 与 batchNorm
output = net(data) #神经⽹络完成⼀次前馈的计算过程,得到预测输出output
loss = criterion(output, target) #将output与标签target⽐较,计算误差
<_grad() #清空梯度
loss.backward() #反向传播
optimizer.step() #⼀步随机梯度下降算法
right = rightness(output, target) #计算准确率所需数值,返回数值为(正确样例数,总样本数)
return right, loss
调⽤了net.eval(),将模型设置为Evaluation Mode,即验证模式。
# Evaluation Mode
def evaluation_model():
# net.eval() 给⽹络模型做标记,标志说模型现在是验证模式
# 此⽅法将模型 net 的 training 标志设置为 Fal
# 模型中将不会运⾏ dropout 与 batchNorm
关于大海的歌曲net.eval()
#记录校验数据集准确率的容器
val_rights = []
'''开始在校验数据集上做循环,计算校验集上⾯的准确度'''
for (data, target) in validation_loader:
data, target = Variable(data), Variable(target)
# 完成⼀次模型的 forward 计算过程,得到模型预测的分类概率
output = net(data)
# 统计正确数据次数,得到:(正确样例数,batch总样本数)
right = rightness(output, target)
# 加⼊到容器中,以供后⾯计算正确率使⽤
val_rights.append(right)
return val_rights
正式开始训练流程
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): #针对容器中的每⼀个批进⾏循环
# 将 Tensor 转化为 Variable,data 为⼀批图像,target 为⼀批标签
data, target = Variable(data), Variable(target)
# 调⽤模型训练函数
right, loss = train_model(data, target)
#将计算结果装到列表容器train_rights中
train_rights.append(right)
if batch_idx % 100 == 0: #每间隔100个batch执⾏⼀次打印等操作
# 调⽤模型验证函数
val_rights = evaluation_model()
# 统计验证模型时的正确率
# val_r为⼀个⼆元组,分别记录校验集中分类正确的数量和该集合中总的样本数
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
# 统计上⾯训练模型时的正确率
平安家庭事迹材料
# train_r为⼀个⼆元组,分别记录⽬前已经经历过的所有训练集中分类正确的数量和该集合中总的样本数,
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
# 计算并打印出模型在训练时和在验证时的准确率
# train_r[0]/train_r[1]就是训练集的分类准确度,同样,val_r[0]/val_r[1]就是校验集上的分类准确度
print('训练周期: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t训练正确率: {:.2f}%\t校验正确率: {:.2f}%'.format(
epoch, batch_idx * batch_size, len(train_loader.datat),
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 录了训练周期中所有卷积核的演化过程,v1.weight就提取出了第⼀层卷积核的权重
# clone的意思就是将 weight.data 中的数据做⼀个拷贝放到列表中,
# 否则当 weight.data 变化的时候,列表中的每⼀项数值也会联动
'''这⾥使⽤clone这个函数很重要'''
weights.append([v1.weight.data.clone(), v1.bias.data.clone(),
关注两个⼩的细节:朋友的英语怎么说
⼀个是出现在训练数据训话中的ain(),另⼀个是出现在校验数据循环中的net.eval(),它们是做什么的?
net.eval()会将模型设置为验证模式(evaluation mode),会关闭 Dropout。
观察并验证模型训练效果
将训练过程中的误差曲线打印出来,以观察训练的过程。
#绘制训练过程的误差曲线,校验集和测试集上的错误率。
plt.figure(figsize = (10, 7))
plt.plot(record) #record记载了每⼀个打印周期记录的训练和校验数据集上的准确度
plt.xlabel('Steps')
plt.ylabel('Error rate')
将训练过的模型在测试集上做测验
#在测试集上分批运⾏,并计算总的正确率
net.eval() #标志模型当前为运⾏阶段
vals = [] #记录准确率所⽤列表
#对测试数据集进⾏循环什么不在年高
for data, target in test_loader:
data, target = Variable(data, requires_grad=True), Variable(target)
output = net(data) #将特征数据喂⼊⽹络,得到分类的输出
val = rightness(output, target) #获得正确样本数以及总样本数
vals.append(val) #记录结果
#计算准确率
rights = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
right_rate = 1.0 * rights[0].data.numpy() / rights[1]
right_rate
随便从测试集中读⼊⼀张图⽚,检验模型的分类结果,并绘制出来。
idx = 4
muteimg = test_datat[idx][0].numpy()
plt.imshow(muteimg[0,...], cmap='gray')
print('正确标签是:', test_datat[idx][1])
# 使⽤torch.view()将输⼊变换形状,神经⽹络的输⼊为:(batch, 1, 28, 28)
# 测试的时候只有⼀个数据,所以 batch 为 1
test_input = torch.Tensor(muteimg).view(1, 1, 28, 28)
out = net(Variable(test_input))
print('模型预测结果是:', torch.max(out, 1)[1].data.numpy())
四、解剖卷积神经⽹络
第⼀层卷积核训练得到了什么
⾸先打印出搭建好的卷积神经⽹络 ConvNet 的组成元素(注意不是结构),以⽅便我们取得不同结构的相应参数。
net.parameters
观察第⼀层卷积核所对应的 4 个特征图(feature map)
由于第⼆层卷积层的输⼊的尺⼨是 (28,28,4),所以每个卷积核是⼀个 (5,5,4)的张量,所以下⾯每个卷积核都会绘制出 4 ⾏。idx = 4
# ⾸先定义读⼊的图⽚
# 它是从 test_datat 中提取第 idx 个批次的第 0 个图,其次 unsqueeze 的作⽤是在最前⾯添加⼀维
# ⽬的是为了让这个 input_x 的 tensor 是四维的,这样才能输⼊给 net,补充的那⼀维表⽰ batch
input_x = test_datat[idx][0].unsqueeze(0)
# 调⽤ net 的 retrieve_features ⽅法可以抽取出喂⼊当前数据后吐出来的所有特征图(第⼀个卷积和第⼆个卷积层)
feature_maps = ieve_features(Variable(input_x))
# feature_maps 是有两个元素的列表,分别表⽰第⼀层和第⼆层卷积的所有特征图
# 所以 feature_maps[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())
观察第⼆层卷积的卷积核