本实验过程需要用到torchvision包,没有安装的小伙伴,windows用户可直接使用cmd命令,输入命令行pip install torchvision即可。【仍安装不了的,建议csdn直接查找安装教程】
1.1 导入实验可能用到的包、库等
#导入所需要的包
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from torch.autograd import Variable
%matplotlib inline
1.2 加载数据集
使用pytorch自带的数据加载器,包括dataset(装载数据集),sampler(采样数据集),以及dataloader(迭代循环数据集)
#定义超参数
image_size = 28 #图像的总尺寸28*28
num_classes =10 #标签的种类数
num_epochs = 20 #训练的总循环周期
batch_size = 64 #一个撮批次的的等待小,64张图片
#加载MNIST数据,如果没有下载过,就会在当前路径下新建、data目录,并把文件存放在其中
#MNIST数据是属于torchvision包自带的数据,可以直接调用
#下载训练数据集
train_dataset=dsets.MNIST(root='./data', #文件存储路径
train=True, #提取训练集
transform=transforms.ToTensor(), #转为tensor类型,便于数据预处理
download=True) #找不到文件时,自动下载
#加载测试数据集
test_dataset = dsets.MNIST(root='./data',train=False,
transform=transforms.ToTensor())
#训练数据集的加载器,DataLoader方法,可以自动将数据分(批)割成batch,顺序随机打乱
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size = batch_size,
shuffle=True)
'''我们希望将测试数据分成两部分,一部分作为验证(校验)数据,一部分作为测试数据。
验证数据用于检测模型是否过拟合,并调整参数,测试数据检验整个模型的工作'''
#首先,我们定义下标数组indices,它相对于所有test_dataset中数据的代码
indices = range(len(test_dataset))
indices_val = indices[:5000] #取前5000份作为验证集数据的下标
indices_test = indices[5000:] #取后5000份作为测试集的下标
#根据这些下标,构造两个数据集的SubsetRandomSampler采样器
#校验集采样器的作用是:从数据集中indices_val,从indices_test抽取数据,它们会对下标进行采样
sampler_val = torch.utils.data.sampler.SubsetRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler.SubsetRandomSampler(indices_test)
#根据两个采样器来定义加载器,注意将sampler_val和sampler_test分别赋值给validation_load和test_loadad
#采样器和加载器连接到一起,就可以在加载数据的时候随机抽取
validation_loader = torch.utils.data.DataLoader(dataset =test_dataset,
batch_size =batch_size,
sampler = sampler_val)
test_loader = torch.utils.data.DataLoader(dataset =test_dataset,
batch_size =batch_size,
sampler = sampler_test)
这里提一个概念,在此作为笔记
下采样和上采样的区别:下采样就是缩小图像,在卷积神经网络中,池化层就是下采样,即对原图像进行减缩,使得原图像的某些信息被增强,丢弃多余的,冗余的信息
1.3 读取绘制手写数字图像
#随便从数据集中读入一张图片,并绘制出来
idx = 1001
#dataset支持下标索引,其中提取出来的每一个元素为features,target格式,即属性和标签
#因此train_dataset[idx][0] 表示取该元素中的features,即取第idx批次的features中第0个图像
muteimg = train_dataset[idx][0].numpy()
#由于一般的图像包含rgb三个通道,而MNIST数据集的图像都是灰度的,只有一个通道。
#用imshow画图,会将灰度矩阵自动展现为彩色,不同灰度对应不同颜色,从黄到紫
plt.imshow(muteimg[0,...]) #取muteimg中的28*28像素点画图
print('标签是:',train_dataset[idx][1]) #[idx][1]对应的是target
print(muteimg.shape) #(1,28,28)
2.1 构建网络
我们将要调用PyTorch强大的nn.Module这个类来构建卷积神经网络,我们分成如下几个步骤:
1.首先,我们构造ConvNet类,它是对类nn.Module的继承
2.接着,复写init,以及forward两个函数,第一个为构造函数,每当类ConvNet被具体化一个实例时,会被调用,forward则是在运行神经网络正向的时候被自动调用
3.自定义自己的方法
ConvNet其实也是一个大容器,它里面有Conv2d,MaxPool2d等组件
(按我自己理解就是,先构造定义卷积神经网络中的各种模块、函数,然后再拼接调用、实现真正的运算)
#定义卷积神经网络;4和8为人为指定的两个卷积层的厚度(feature map 的数量)
depth =[4,8]
class ConvNet(nn.Module):
def __init__(self):
#该函数在创建一个ConvNet对象的时候,即调用该语句:net=ConvNet()时,就会被调用
#首先调用父类相应的构造函数
super(ConvNet,self).__init__()
#其次构造ConvNet需要用到的各个神经模块
"""注意,定义组件并没有真正搭建这些组件,只是把基本建筑砖块先找好"""
self.conv1 = nn.Conv2d(1,4,5,padding=2) #定义一个卷积层,输入通道为1,输出通道为4,窗口为5*5,填充为2
self.pool = nn.MaxPool2d(2,2) #定义一个Pooling层,一个窗口为2*2的pooling运算
self.conv2 = nn.Conv2d(depth[0],depth[1],5,padding=2) #第二层卷积,输入通道为depth[0]=4
#输出通道为depth[1]=8,窗口为5*5,填充为2
#一个线性连接层,输入尺寸为最后一层立方体
self.fc1 = nn.Linear(image_size//4*image_size//4*depth[1], 512)
#最后一层线性分类单元,输入为512,输出为要做分类的类别
self.fc2 = nn.Linear(512,num_classes)
def forward(self,x):
#该函数完成神经网络真正的前向运算,我们会在这里把各个组件进行实际的拼装
#x的尺寸:(batch_size,num_filters,image_width,image_height)
x = F.relu(self.conv1(x)) #第一层卷积,激活函数用Relu,为了防止过拟合
#x的尺寸:(batch_size,num_filters,image_width,image_height)
x = self.pool(x)
#x的尺寸:(batch_size,depth[0],image_width,image_height)
x = F.relu(self.conv2(x)) #第三层又是卷积,窗口为5,输入输出通道分别是depth[0]=4,depth[1]=8
#x的尺寸:(batch_size,depth[1],image_width/2,image_height/2)
x = self.pool(x) #第四层poolings,将图片缩小到原大小的1/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_size//4*image_size//4)
x = F.relu(self.fc1(x)) #第五层为全链接,Relu激活函数
#x的尺寸:(batch_size,num_classes)
x = F.dropout(x,training=self.training) #以默认为0.5的概率为对这一层进行dropout操作
x = self.fc2(x) #全链接
x = F.log_softmax(x,dim=1) #输出层为log_softmax,即概率对数值log(p(x))
return x
def retrieve_features(self,x):
#用于提取卷积神经网络的特征图,返回feature_map1,feature_map2为前两层卷积层的特征图
feature_map1 = F.relu(self.conv1(x)) #完成第一层卷积
x = self.pool(feature_map1) #完成第一层pooling
feature_map2 = F.relu(self.conv2(x)) #第二层卷积,两层特征图都存储到了feature_map1,feature_map2中
return (feature_map1,feature_map2)
def rightness(predictions,labels):
'''计算预测错误率的函数,其中predictions是模型给出的一组预测结果,batch_size、num_classes列的矩阵'''
pred = torch.max(predictions.data,1)[1] #对任意一行(一个样本)的输出值的第一个维度,求最大
rights = pred.eq(labels.data.view_as(pred)).sum() #将下标与labels中包含的类别进行比较,并累计得到总值
return rights,len(labels) #返回正确的数量和这一次一共比较了多少元素
(之前记混或看不懂的地方)解释说明:
1.激活函数需作用于第五层全连接层,即fc1,而fc2不需被激活函数作用 。
2.经过两层的polling(池化),原图像变为原来的1/4,故有image_size//4
各函数作用:ConvNet定义卷积神经网络;forward完成前馈神经网络运算;retrieve_features存储特征图
2.2 运行模型
net=ConvNet() #此时会调用__init__()函数
criterion=nn.CrossEntropyLoss() #交叉熵损失函数
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
record=[] #记录准确率等数值
weights=[] #每若干步就记录一次卷积核
for epoch in range(num_epochs): #循环周期设定为20次
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)
net.train() #打开dropout,防止过拟合
output = net(data) #神经网络完成一次前馈的计算过程,得到预测输出output
#将output与target比较,计算误差,如标签是数字7,而通过net预测输出的output却是5,7与5之间存在误差
loss=criterion(output,target)
optimizer.zero_grad() #清空梯度,因为Variable中requires_grad=True会默认累加梯度
loss.backward() #反向传播
optimizer.step() #一步随机梯度下降算法
right=rightness(output,target)
train_rights.append(right)
if batch_idx %100 == 0:
net.eval()
val_rights=[]
'''开始在校验数据集上做循环,计算校验集上面的准确度'''
for (data,target)in validation_loader:
data,target=data.clone().requires_grad_(),target.clone().detach()
output=net(data)
right=rightness(output,target)
val_rights.append(right)
#train_r[0]/trai_r[1]是训练集的分类准确度,另一个是校验集的
train_r=(sum([tup[0] for tup in train_rights]),sum([tup[1] for tup in train_rights]))
val_r=(sum([tup[0] for tup in val_rights]),sum([tup[1] for tup in val_rights]))
print(val_r)
print('训练周期:{}[{}/{}({:.0f}%)]\tLoss:{:.6f}\t训练正确率:{:.2f}%\t校验正确率:{:.2f}%'.format(
epoch,batch_idx * batch_size,len(train_loader.dataset),
100.*batch_idx/len(t。rain_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]))
#weight记录了训练周期中所有卷积核的演化过程,net.conv1.weight提出了第一层卷积和的权重
#clone备份weight.data中得到数据,否则weight.data变化时,列表中的每一项数据也会联动
#所以这里使用clone函数十分重要
weights.append([net.conv1.weight.data.clone(),net.conv1.bias.data.clone(),
net.conv2.weight.data.clone(),net.conv2.bias.data.clone()])
以下是部分训练结果截图:
绘制训练过程中的误差曲线
#绘制训练过程的误差曲线,校验集和测试集上的错误率
plt.figure(figsize=(10,7))
plt.plot(record) #record记录了每一个打印周期记录的训练和校验数据集上的准确度
plt.xlabel('Steps')
plt.ylabel('Error rate')
结果如图:
2.3 在测试集上进行分类
#在测试集上分批运行,并计算总的正确率
net.eval() #关闭dropout,标志模型当前为运行阶段
vals = [] #记录准确率
#对测试数据集进行循环
for data,target in test_loader:
data.target = data.clone().detach().requires_grad_(True),target.clone().detach()
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 = 100.*rights[0].numpy() / rights[1]
print(right_rate)
#随便从测试集中读取一张图片,检验模型的分类结果,并绘制出来
idx = 2000
muteimg = test_dataset[idx][0].numpy()
plt.imshow(muteimg[0,...]) #test_dataset的每一个数据由data和target组成,其中data为像素点矩阵,target为该图像标签数字
print('标签是:',test_dataset[idx][1])
print(test_dataset[idx])
#提取第一层卷积层的卷积核
plt.figure(figsize=(10,7))
for i in range(4):
plt.subplot(1,4,i+1)
plt.axis('off') #关闭坐标轴
plt.imshow(net.conv1.weight.data.numpy()[i,0,...])
#第0个的第一行的所有数据为一个特征图,第1个的第一行的所有数据为第二个特征图,以此类推
#上面的卷积核我们不能够很好地解读,因此将其对应的特征图输出,便于理解
#先定义读入的图片,其中unsqueeze作用是在最前面添加一维
#目的是让input_x的tensor为四维,才能输入给net,添加的维是batch那一维
input_x = test_dataset[idx][0].unsqueeze(0)
#features是有两个元素的列表,分别表示第一层和第二层的所有特征图
feature_maps = net.retrieve_features(Variable(input_x))
plt.figure(figsize=(10,7))
#打印出4个特征图
for i in range(4):
plt.subplot(1,4,i+1)
plt.imshow(feature_maps[0][0,i,...].data.numpy())
2.4 滤波器(卷积核)的演化过程
#将记录在容器中的卷积核权重历史演化数据打印出来
i = 0
#tup是tuple元组的意思
for tup in weights:
if i % 10 ==0:
layer1 = tup[0]
fig = plt.figure(figsize=(10,7))
for j in range(4):
plt.subplot(1,4,j+1)
plt.axis('off')
plt.imshow(layer1.numpy()[j,0,...])
i +=1
部分结果如图所示:
由上图,我们可以看到,卷积核的演化过程共有4列,即以第一行的四个特征图为首,
依次不断地往下演化,即每一列对应一个卷积核 。
【绘制第二层卷积核】
#绘制第二次的卷积核,每一列对应一个卷积核,一共8个卷积核(人为设定的4,8两个层的卷积厚度)
plt.figure(figsize=(15,10))
for i in range(4):
for j in range(8):
plt.subplot(4,8,i*8+j+1)
plt.axis('off')
plt.imshow(net.conv2.weight.data.numpy()[j,i,...])
#绘制第二层的特征图,一共八个
plt.figure(figsize=(10,7))
for i in range(8):
plt.subplot(2,4,i+1)
plt.axis('off')
plt.imshow(feature_maps[1][0,i,...].data.numpy())
结论:从上述图像看出,图像的抽象程度变得更高了。由于池化的作用,即对图像进行模糊处理,
一些多余的图像信息被丢弃了,这也证明了卷积网络的抽象提取能力
2.5 卷积神经网络的健壮性
所谓健壮性,就是模型消除局部相关性的能力。即数字在图像平移后,模型是否仍能很好地正确预测出该数字
#随机挑选一张图片,把它往左平移w个单位,然后考察分类结果是否有变化
#提取test_dataset中第idx个批次第0个图像第0个通道对应的图像,定义为c
#原图像即为28*28, 因为这个是单通道图像
'''test_data[idx][0]的维度是(1,28,28),即1个28行28列的矩阵,当我们
取第1个时,即test_dataset[idx][0][0],可得到28行28列的二维张量'''
c = test_dataset[idx][0][0] #shape为28*28
d = torch.zeros(c.size()) #全0的28×28的矩阵
w = 3 #平移的长度为3个像素
#对于d中的任意像素i,j ,等于c中的i,j+w位置的像素(即d为c向左平移后的位置)
for i in range(c.size()[0]):
for j in range(0,c.size()[1]-w):
d[i,j] = c[i,j+w]
feature_maps = net.retrieve_features(Variable(c.unsqueeze(0).unsqueeze(0))) #
plt.figure(figsize=(10,7))
#打印出4个特征图
for i in range(4):
plt.subplot(1,4,i+1)
plt.imshow(feature_maps[0][0,i,...].data.numpy())
#将d输入神经网络,得到分类结果pred,并打印
prediction = net(d.unsqueeze(0).unsqueeze(0))
''' torch.max(input, dim, keepdim=False, out=None) -> (Tensor, LongTensor)
按维度dim 返回最大值,并且返回索引'''
pred = torch.max(prediction.data,1)[1] #[1]即返回最大值对应的索引
print(pred)
#提取d对应的features特征图结果
feature_maps = net.retrieve_features(Variable(d.unsqueeze(0).unsqueeze(0)))
plt.figure(figsize=(10,7))
#打印出4个特征图
for i in range(4):
plt.subplot(1,4,i+1)
plt.imshow(feature_maps[0][0,i,...].data.numpy())
plt.figure(figsize=(10,7))
for i in range(8):
plt.subplot(2,4,i+1)
plt.imshow(feature_maps[1][0,i,...].data.numpy())
结论:平移数字输入后得到第二层特征图 。首先,网络打印出来的结果是6,说明我们的模型训练得能正确得预测出输入的数字, 并且对3个像素点的平移具有很好干抗干扰性,体现出卷积神经网络很强的健壮性。