我们依然分别构建训练集和测试集:
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets,transforms
import matplotlib.pyplot as plt
import numpy as np
# 定义超参数
input_size = 28 # 图像的总尺寸28*28
num_classes = 10 # 标签的种类数
num_epochs = 3 # 训练的总循环周期,由于可供学习数据量不够,
# 我们要重复使用训练集进行学习
batch_size = 64 #一个撮(批次)的大小,64张图片
# 训练集
train_dataset = datasets.MNIST(root='./data',
train=True,
transform=transforms.ToTensor(), # 将读入数据转换成tensor
download=True)
print(train_dataset) # 查看训练集基本信息
# 测试集
test_dataset = datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor())
print(test_dataset) # 查看测试集基本信息
# 数据打包,64个一组
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=True)
卷积神经网络和全连接神经网络在结构上有很多相似点,因此我们着重讲述一下这两个网络中一个比较明显的区别:卷积核。在全连接神经网络中,我们主要通过wx+b的方式得到特征,因此我们输入的是一维向量,输出的也是一维特征向量。但在卷积神经网络中则有所不同。卷积神经网络依靠内容不同的卷积核分别与图像进行卷积以获得特征,假如我们希望从一幅图像中获得16种特征(需要16个不同卷积核),那么每输入一张28 * 28的图像矩阵,将会输出16个28 * 28的图像矩阵。
不难看出,这数据量一下就多了很多,对于计算机来说,这并不是一件好事,因为如果我们设置的隐层很多,那么需要计算机处理的数据量就会爆炸式的增长。为了缓解这一问题,就需要使用池化。池化,就是将我们得到的特征图进行优化。以前文提到的16张28 * 28大小的特征图举例,我们将每一张图都单独取出来,然后每2 * 2个像素点中,保留下一个最大的值,组成新的特征图,这样一来,我们就得到了16张14*14大小的特征图,节约了很多算力。
下面我们用代码实现网络构建:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 处理输入的第一层网络
self.conv1 = nn.Sequential( # 输入,大小为每一张图片的所有特征 (1, 28, 28)
nn.Conv2d( # 2D卷积针对图像,1D卷积针对结构化数据,3D卷积针对视频
in_channels=1, # 灰度图,输入是1个channel
out_channels=16, # 卷积核个数,决定能得到多少特征图,以及下一层输入多少channel
kernel_size=5, # 卷积核大小,通常选3*3因为可以得到更多的特征
stride=1, # 卷积核每次向右或下移动步长,步长越大特征越少
# 添加边缘信息,确保原图的边缘新西也能多次参与卷积过程。这里是图像边缘加入两圈0
padding=2, # 如果希望卷积后大小跟原来一样,需要设置padding=(kernel_size-1)/2 if stride=1
# 从公式中可以看出padding要根据kernel来选择
# 如果除不开会默认向下取整
), # 输出的特征图为 (16, 28, 28)
nn.ReLU(), # relu层,每提取一次特征都需要做一次非线性映射
nn.MaxPool2d(kernel_size=2), # 进行池化操作(2x2 区域), 输出长度不变但图片维度被压缩
# 此处的输出结果为: (16, 14, 14)
)
# 全连接层
self.conv2 = nn.Sequential( # 连多个隐层
nn.Conv2d(16, 32, 5, 1, 2), # 输出 (32, 14, 14)
nn.ReLU(), # relu层
nn.Conv2d(32, 32, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(2), # 输出 (32, 7, 7)
)
# 紧邻输出的最后一层
self.conv3 = nn.Sequential(
nn.Conv2d(32, 64, 5, 1, 2),
nn.ReLU(), # 输出 (64, 7, 7)
)
# 每张原图都会卷积出64个7*7的图片,因此共有64*7*7个特征
self.out = nn.Linear(64 * 7 * 7, 10) # 卷积得到的特征结果做一下全连接,分为10类
def forward(self, x): # 前向传播会在nn.Module里自行使用,我们的代码不需要对它进行调用,如不写,
# 计算机就不知道如何进行前向传播
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
# 此时的x包含四个参数:batch,c,h,w
x = x.view(x.size(0), -1) # flatten操作,将batch_size与输出特征分开
# -1为自动计算,结果为:(batch_size, 64 * 7 * 7)
output = self.out(x)
return output
这样我们的卷积网络模型就构建完毕了,代码中包括详细的注释,可以帮助大家理解卷积和池化。
之前我们已经讲到过,损失值不能直观的反应学习的好坏,因此我们需要自己拟定一个求取分类准确率的函数:
def accuracy(predictions, labels): # 预测值和真实值
pred = torch.max(predictions.data, 1)[1] # 获得预测分类之中得分最大值的索引
# 加入索引是8,那么分类结果就是数字8
rights = pred.eq(labels.data.view_as(pred)).sum() # 判断正确的数量
return rights, len(labels)
torch.max可以帮我们找到目标数据中的最大值,当然我们还可以调整第二个参数得到多个次大值。
这部分内容已经在本系列第一篇中详细提到过,因此不再赘述,代码中有详细的注释,不懂的小伙伴要认真学起来了:
# 实例化
net = CNN()
#损失函数
criterion = nn.CrossEntropyLoss()
#优化器
optimizer = optim.Adam(net.parameters(), lr=0.001) #定义优化器,普通的随机梯度下降算法
#开始训练循环
for epoch in range(num_epochs):
#当前epoch的结果保存下来
train_rights = []
for batch_idx, (data, target) in enumerate(train_loader): # train_loader将60000个数据分成938组,每组64个图
# 该循环会进行938次
net.train()
output = net(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
right = accuracy(output, target)
train_rights.append(right)
if batch_idx % 100 == 0: # 每一百次循环进行一下测试,检测学习结果
net.eval()
val_rights = []
for (data, target) in test_loader: # test_loader将10000个数据分成157组,每组64个数据
# 我们最多可以测试157次,这里只测了30次。相当于检测
# 学习成果的试卷足够多。我们可以增加外循环次数或者增加
# 每组测试集中的“试题量”让待测试的数据得到充分的利用
output = net(data)
right = accuracy(output, target)
val_rights.append(right)
#准确率计算
# 将这一组的所有判断结果
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('当前epoch: {} [{}/{} ({:.0f}%)]\t损失: {:.6f}\t训练集准确率: {:.2f}%\t测试集正确率: {:.2f}%'.format(
epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.data,
100. * train_r[0].numpy() / train_r[1],
100. * val_r[0].numpy() / val_r[1]))
大家可以把这次的训练结果和全连接神经网络的训练结果做一下对比,就可以发现效果还是有不小的提升的~
可能有小伙伴还是对增加“试题量”不太理解,其实我们只需要把读取数据中部分代码做以下替换即可:
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=330, # 我们将10000道题打包成31份试卷,
# 取前等量的30份试卷作测试用,每张试卷有330道题
shuffle=True)
至于说增加学习次数就更简单了,因为一共可以测试157次,一个小循环跑完会测试10次,也就是把num_epochs设置成15刚刚好~