一、前置工作—确定神经网络结构

搭建神经网络完成某项特定任务,就像做一项工程,需要了解该项目的大致基本情况,准备图纸,才能确定每一步的落实方案。我在学校学习时忽视了这一点,只是老师下达了任务便埋头去构建网络,这其实不利于我对神经网络的深入理解。
首先,应该了解数据的基本情况,如手写数字识别任务中,图片的大小是多少,数据集规模有多大等。
在构建网络的部分,我们需要了解确定的是需要多少层卷积、全连接层,当然,为了填入具体的参数,也必须了解图片大小。通常深度学习项目需要一个卷积神经网络结构图,后续搭建网络都依据该图展开。现实中,设计每层卷积层输出通道也是需要通过训练才能得出最好参数的(相当于确定隐藏层),但在学习过程中直接采用较好的参数训练即可。
已知MNIST数据集图像大小为28×28的灰度图,设置神经网络卷积核为5*5,第一层输出通道数为6,第二层输出通道数为16,那么可以大致绘出神经网络结构图如下:

pytorchLeNet5数字手写体识别 pytorch 手写数字_神经网络


从左至右,最左的1@28×28表示输入图像(其实一般LeNet图不会标出,此处是为了方便理解)。输入进入网络,进行第一层卷积,第一层卷积输出通道为6,原图像大小不变,便得出6@28×28;卷积下一步即池化,这里采用2*2最大池化,即将原图像缩小到原来的一半,缩小的每个区域取最大值代替,依照这个流程完成若干层卷积池化的构造。在进入全连接层前,要注意先展平,最后输出分类结果数即可(0-9有10个数字,所以最终输出应为10).

二、开始搭建神经网络

根据上面画出的神经网络结构图,已经能够开始搭建神经网络了。
首先导入必要的库:

# 实现基本运算:
import torch
# 搭建网络结构:
import torch.nn as nn
# 实现前馈运算:
import torch.functional as f

开始编写神经网络架构:

# file:CNN.py
Class ConvNet(nn.Module):
	"""编写一个卷积神经网络类"""
	def __init__(self):
		""" 初始化网络,将网络需要的模块拼凑出来。 """
		super(ConvNet, self).__init__()
		# 卷积层:
		self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
		self.conv2 = nn.Conv2d(6, 16, 5, padding=2)
		# 最大池化处理:
		self.pooling = nn.MaxPool2d(2, 2)
		# 全连接层:
		self.fc1 = nn.Linear(16*7*7, 512)
		self.fc2 = nn.Linear(512, 10)

	def forward(self, x):
		"""前馈函数"""
		x = f.relu(self.conv1(x)) # = [b, 6, 28, 28]
		x = self.pooling(x)       # = [b, 6, 14, 14]
		x = f.relu(self.conv2(x)) # = [b, 16, 14, 14]
		x = self.pooling(x)		  # = [b, 16, 7, 7]
		x = x.view(x.shape[0], -1)# = [b, 16 * 7 * 7]
		x = f.relu(self.fc1(x))
		x = self.fc2(x)
		output = f.log_softmax(x, dim=1)
		return output

为了后续计算准确率需要,此处还应加上一个计算准确率的方法:

def cal_correction(output, target):
	"""计算准确率"""
	# 返回预测结果:
	pred = torch.argmax(output)
	# 比对并统计:
	correct = pred.eq(target.data.view_as(output)).sum()
	# 返回百分比:
	percentage = (correct / len(target)) * 100 
	return percentage

神经网络的结构搭建完成了,有几个地方我在学习的时候比较有疑惑,应该解释一下。

①关于nn.Module和super().init()
理解nn.Module为何物,首先就应理解python中类的定义。在默认情况下,python类定义时为:class Class(object). object表示父类,python中默认所有的类都可以继承父类object. 这里填入nn.Module的含义即把其当作父类继承,而当填入继承对象时,下文必须搭配super().init()语句,才能够完成继承父类的操作。
nn.Module为神经网络父类模型,继承了nn.Module后,模型能够完成forward以及bp的操作。
②[b, c, w, h]
在编写初始化方法中第一层全连接层输入时,常常难以确定,其实非常简单,只要在forward方法中推一遍即可。[b, c, w, h]含义从左到右为[batch_size, channels(输出通道数), 图像宽度, 图像高度], 已知图像尺寸为28×28,输出通道数即每层卷积核的输出数量。进入全连接层前先展平,只保留batch_size,其他元素相乘输入全连接层,这样一来就解释得清楚了。

三、导入数据

按理来说导入数据应该是第一步,因为需要将数据导入进来,对数据有初步的认识。这里是基于我对Mnist数据有了大致的认识的情况下,我会选择先搭建好神经网络。

# file:Dataloader.py
"""导入必要的库"""
# 实现基本运算:
import torch
# 数据装载器:
from torch.utils.data import DataLoader as dataloader
# 读取数据集:
import torchvision.datasets as datasets
# 数据转换器:
import torchvision.transforms as transforms

设置一个数据转换器,否则原始数据无法直接用来训练。

transformer = transforms.Compose([
	# 转化成张量:
	transforms.ToTensor(),
	# 标准化数据:
	transforms.Normalize((0.1307,),(0.3081,))]

编写一个函数一步获取训练数据:

def get_train_data():
	"""获取训练数据"""
	# 下载训练数据:
	train_data = datasets.MNIST(root='./',
								download=True,
								train=True,
								transform=transformer)
	# 装载训练数据:
	train_loader = dataloader(train_data,
							  batch_size=64,
							  shuffle=True)
	return train_data, train_loader

获取测试、验证数据:

def test_val_data():
	"""获取测试、验证数据"""
	# 下载数据
	t_v_data = datasets.MNIST(root='./',
							   download=True,
							   train=False,
							   transform=transformer)
	# 设置采样器(一半用于验证、一半用于测试):
	indices = range(len(t_v_data))
	test_sampler = torch.utlis.data.sampler.SubsetRandomSampler(indices[:5000])
	val_sampler = torch.utlis.data.sampler.SubsetRandomSampler(indices[5000:])
	# 装载数据:
	test_dataloader = dataloader(t_v_data,
								 sampler=test_sampler,
								 batch_size=64)
	val_dataloader = dataloader(t_v_data,
								sampler=val_sampler,
								batch_size=64)
	return t_v_data, test_dataloader, val_dataloader

在主函数中调用这两个方法即可实现数据的读取。

四、开始训练

训练过程按照前馈神经网络的流程编写即可:

pytorchLeNet5数字手写体识别 pytorch 手写数字_pytorch_02

# file:train_test.py
# 将先前编写好的计算准确率方法导入:
from CNN import cal_corrction
def train_model(model, train_data, val_data, batch, epoch, loss_func, opt)
	""" @参数说明:
		model - 需要训练的模型;
		train_data - 训练数据;
		val_data - 验证数据;
		batch - 每次训练的批量;
		epoch - 训练轮数;
		loss_func - 损失函数;
		opt - 优化器."""
	for i in range(epoch):
		# 定义记录损失值、精度的容器:
		train_corrections = []
		train_losses = []
		for idx, (img, label) in enumerate(train_data):
			# 数据处理:
			# 转化成cuda类型:
			img, label = img.to('cuda'), label.to('cuda')
			# 保留梯度:
			img = img.clone().requires_grad_(True)
			label = label.clone().detach()
			""" ----- 前向传播 ----- """ 
			# 设置模型为训练模式:
			model.train()
			# 将数据喂入网络:
			output = model(img)
			# 计算精度:
			train_acc = cal_correction(output, label)
			train_corrections.append(train_acc)
			# 计算损失值:
			train_loss = loss_func(output, label)
			train_losses.append(train_loss)
			# 清空优化器梯度:
			opt.zero_grad()
			""" ----- 反向传播 ----- """
			# 反传损失值:
			train_loss.backward()
			# 用优化器梯度下降:
			opt.step()
			
			if idx % (batch * 100) == 0:
				"""每训练好100批次验证并展示训练结果"""
				model.eval()
				val_record = []
				for (data, target) in val_data:
					# 转化数据类型:
					data, target = data.to('cuda'), target.to('cuda')
					data, target = data.clone().requires_grad_(True), target.clone().detach()
					# 将数据喂入:
					out = model(data)
					# 预测准确率:
					val_acc = cal_correction(out, target)
					val_record.append(val_acc)
					# 打印训练、验证结果:
					print(f'epoch{i+1}: Train Acc={train_corrections[-1]} Train Loss={train_losses[-1]} Val Acc={val_record[-1]}')

还应该加入测试环节,检验该模型的泛化能力。

def test_model(model, test_data):
	"""测试模型"""
	# 定义记录测试精度的容器:
	test_acc = []
	for idx, (img, label) in enumerate(test_data):
		# 处理数据:
		img, label = img.to('cuda'), label.to('cuda')
		img, label = img.clone().requires_grad_(True), label.clone().detach()
		# 投入数据:
		output = model(img)
		# 比对结果:
		acc = cal_correction(output, label)
		test_acc.append(acc)
		print(f'Test Acc={test_acc[-1]}%')

完成模型训练、测试环节的编写后,观察其训练过程:

# file:main.py
# 实现相关运算(这里主要是损失函数、优化器等):
import torch
# 导入神经网络模型:
from CNN import ConvNet
# 导入数据:
from Dataloader import get_train_data, test_val_data
# 导入训练、测试方法:
from train_test import train_model, test_model
"""超参数"""
batch_size = 64
epochs = 10
learning_rate = 0.001
momentum = 0.9

# 实例化神经网络模型:
cnn_model = ConvNet().cuda()
# 定义损失函数(交叉熵):
loss_func = torch.nn.CrossEntropyLoss()
# 定义优化器(随机梯度下降):
sgd_opt = torch.optim.SGD(cnn_model.parameters(),
						  # 学习率:
						  lr=learning_rate,
						  # 动量:
						  momentum=momentum)

# 读取数据:
_, train_loader = get_train_data()
_, test_loader, val_loader = test_val_data()

# 开始训练:
train_model(cnn_model, train_loader, val_loader, batch_size, epochs, loss_func, sgd_opt)
test_model(cnn_model, test_loader)
# 保存模型,以便之后直接使用模型进行可视化结果测试:
torch.save(cnn_model.state_dict(), './cnnmodel')

pytorchLeNet5数字手写体识别 pytorch 手写数字_卷积_03


pytorchLeNet5数字手写体识别 pytorch 手写数字_pytorch_04

五、可视化测试模型识别能力

上面已经完成了神经网络的训练,如果能够带上图片观察其识别结果,那么将能够更直观地了解该模型的识别能力:

# file:test.py
import random
import torch
import pylab
import matplotlib.pyplot as plt
from Dataloader import get_train_data
from ConvNetwork import ConvNet

# 实例化模型
test_net = ConvNet()
# 读取已保存的模型
test_net.load_state_dict(torch.load('./Models/ConvNet4Mnist1217.pth'))

# 随机生产一张图片
x = random.randint(0, 59999)
train_data, _ = get_train_data()

# 丢入网络预测
output = test_net(train_data[x][0].unsqueeze(0))
pred = torch.argmax(output)
print(f"这张图片被识别为数字{pred}.")
print(f"这张图片实际为{train_data[x][1]}.")
if pred == train_data[x][1]:
    print("识别正确!")
else:
    print("识别错误!")

# 显示图像
plt.imshow(train_data[x][0].squeeze(0))
pylab.show()

pytorchLeNet5数字手写体识别 pytorch 手写数字_卷积_05


ok了,基本都能识别正确。