前面几章,我们制造了很多轮子,也已经对卷积神经网络有一个较为透彻的理解。卷积神经网络的理论还是需要放在实际应用中来服务于我们。本次实验我们将实现一个经典的卷积神经网络结构——LeNet,并用其在MNIST数据集上的训练和识别进行体验和研究。
MNIST数据集在很早之前就已经介绍过了,这边只讲方便地使用该数据集。
我们当然可以通过官网下载得到这个数据集。但是作为经典的数据集,在torchvision
的数据集中就已经被包含了。因此,要使用MNIST数据集,我们可以直接调用torchvision中带自动下载的MNIST数据集的DataLoader:
import os
import numpy as np
import torch
from torchvision.datasets import mnist
dataset_train = mnist.MNIST('./mnist',
download=False if os.path.exists('./mnist/MNIST') else True, # 避免重复下载
train=True
)
dataset_test = mnist.MNIST('./mnist',
download=False,
train=False
)
print('train size:%s'%(','.join(torch.tensor(dataset_train.data.shape).numpy().astype(np.str_))))
print('test size:%s'%(','.join(torch.tensor(dataset_test.data.shape).numpy().astype(np.str_))))
输出如下,这里已经下载完毕,如果没有下载会有下载内容的输出:
然后对数据集进行加载,这边我们使用torch自带的DataLoader。
from torch.utils.data import DataLoader
from torch.nn.functional import one_hot
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
dataset_train.targets = one_hot(dataset_train.targets)
dataset_test.targets = one_hot(dataset_test.targets)
dataloader_train = DataLoader(dataset_train, 8, True)
dataloader_test = DataLoader(dataset_test, 8, False)
fig, *ax = plt.subplots(2,4)
for i in range(8):
ax[0][i//4][i%4].set_xticks([])
ax[0][i//4][i%4].set_yticks([])
ax[0][i//4][i%4].imshow(dataset_train.data[i],cmap='gray')
显示一个批中的八张数字图片:
我们发现,图像大小为28×28,然而我们后面使用的LeNet - 5 模型输入模型大小为32×32。这时,就需要我们使用PIL中的图像大小变换函数。我们在创建DataLoader前插入如下代码:
import torchvision.transforms as transforms
dataset_train.data = transforms.Resize((32,32),transforms.InterpolationMode.BICUBIC)(dataset_train.data)
dataset_test.data = transforms.Resize((32,32),transforms.InterpolationMode.BICUBIC)(dataset_test.data)
我们再输出一下,可以发现图像大小均变为32×32.这边使用双三次插值是因为我觉得这个缩放方法听起来很牛。
自定义算子计算方法为矩阵运算,效率仅仅受到python这一解释型语言的速度限制:
model = [
Conv2D(in_channels, 6, (5, 5), padding='valid'),
ReLU(),
MaxPool2D((2,2), 2),
Conv2D(6, 16, (5, 5), padding='valid'),
ReLU(),
MaxPool2D((2,2), 2),
Conv2D(16, 120, (5, 5), padding='valid'),
ReLU(),
View(-1),
Linear(120, 84),
ReLU(),
Linear(84, o_classes)
]
模型结构可视化图像如下:
我们的模型构建使用LeNet-5的在原始版本上有所变化的模型结构:
可以非常方便地得到模型构建代码。这边由于可能有多个通道,我们使用之前介绍过的LazyConv2d来自适应地获得输入通道:
class LeNet(torch.nn.Module):
def __init__(self, o_classes=10):
super(LeNet, self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.LazyConv2d(6, (5, 5)),
torch.nn.ReLU(),
torch.nn.MaxPool2d((2, 2), 2)
)
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(6, 16, (5, 5)),
torch.nn.ReLU(),
torch.nn.MaxPool2d((2, 2), 2)
)
self.conv3 = torch.nn.Sequential(
torch.nn.Conv2d(16, 120, (5, 5)),
torch.nn.ReLU()
)
self.linear1 = torch.nn.Linear(120, 84)
self.activa1 = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(84, o_classes)
def forward(self, x):
x = self.conv3(self.conv2(self.conv1(x)))
x = x.view(x.shape[0], -1)
return self.linear2(self.activa1(self.linear1(x)))
pytorch中本身并不包含对于参数的summary功能,但是我们使用第三方库torchsummary则可以得到这些数据:
from torchsummary import summary
model = model()
params_info = summary(model, (1, 32, 32))
print(params_info)
计算参数量这一操作在pytorch中也没有。我们使用torchstat来进行输出:
from torchstat import stat
print(stat(model, (1, 32,32)))
模型训练和之前的模型没有任何区别,构建完损失函数(CrossEntropy)、优化器(SGD)后,我们使用runner类进行训练。
import runner
runner = runner.Runner(LeNet, torch.nn.CrossEntropyLoss, torch.optim.SGD, lr=0.01)
runner.train(xs_train, ys_train, 100, None, None, 10, mertics=[{'accuracy' : calc_acc}])
runner.eval(xs_test, ys_test)
由于量大,这次的训练是一个非常非常非常缓慢的过程。在经过漫长的等待之后,我们得到如下输出:
我们通过cv2读入摄像头进行手写数字的测试:
import cv2
from PIL import Image
import numpy as np
cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
FPS = 24
cap.set(cv2.CAP_PROP_FPS, FPS)
while True:
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
frame = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
frame = Image.fromarray(frame)
frame = frame.resize((28,28),Image.BICUBIC)
frame = np.asarray(frame)
img2=np.zeros((28,28,1),dtype='uint8')
frame=cv2.addWeighted(frame,-1,img2,0,255,0)
print(datetime.datetime.now(),torch.argmax(model(torch.tensor([[frame / 255.]],dtype=torch.float))),end='\r')
cv2.namedWindow('frame', cv2.WND_PROP_FULLSCREEN)
cv2.imshow('frame', frame)
if cv2.waitKey(1) == ord('q'): break
cap.release()
cv2.destroyAllWindows()
前面已经完成了使用前馈神经网络实现MNIST手写数字识别的实验(传送门),和LeNet相比:
本次实验,我们实现了最为经典的一个卷积神经网络:LeNet。通过实际应用,我领略到了神经网络在特征提取和分类等操作上的强大力量。同时,我发现在模型训练的过程中还是容易出现过拟合的情况。这时我们可能就会用到Dropout层来进行优化。
另外,这边还找到一个网页,在里面你可以自定义网络,并将所有的卷积核和特征图可视化。网站还包含训练过程的可视化,最终效果如下: