简介
本文是使用PyTorch来实现经典神经网络结构LeNet5,并将其用于处理MNIST数据集。LeNet5出自论文Gradient-Based Learning Applied to Document Recognition,是由图灵奖获得者Yann LeCun等提出的一种用于手写体字符识别的非常高效的卷积神经网络。它曾经被应用于识别美国邮政服务提供的手写邮政编码数字,错误率仅1%。
LetNet5预备知识
本文的重点是分析LeNet5的网络结构,并且给出基于PyTorch的简易版本实现,因此需要读者具有基本的卷积、池化操作相关的知识。下面是对这些预备知识的一个简单补充,详细的可以参考斯坦福CS231n。
卷积操作
单通道卷积操作 单通道卷积操作动态示意图
上图的Image大小是5x5,卷积核大小为3x3,步长为1,最后的输出大小是3x3。假如输入图像大小是,卷积核的大小是,步长是,是最后输出的feature map大小,则o可由以下公式计算得到:
多通道卷积操作
多通道卷积操作示意图上图输入有3个通道,但是只有一个卷积核,故在计算的时候,每个通道都要通过卷积计算最后累加,最终的输出的通道数跟卷积核的数量一致。这里只有一个卷积核,故最后输出是一个通道。
池化操作
池化的定义比较简单,最直观的作用便是降维,常见的池化有最大池化、平均池化和随机池化。池化层不需要训练参数。
三种池化操作
LeNet5网络结构
LetNet5是一个简单的CNN结构,整体框图如下: LeNet5结构整个网络一共包含7层(不算输入层),分别是C1、S2、C3、S4、C5、F6、Output,其中Cx代表的是卷积层,Sx代表的是下采样层,接下来分别介绍每一层的作用。
1. 输入层
网络的输入是32x32大小的图像数据。
2. C1卷积层
C1层的输入是32x32的原始图像,卷积核的大小是5x5,深度为6,即有6个卷积核,不需要使用0填充,步长为1。由上述内容可知,输出的图像大小是28x28,又卷积核的深度决定了输出尺寸的深度,因为这里使用了6个卷积核,故C1层的输出尺寸是28x28x6。C1层的总共参数个数为(5x5+1)x6=156个参数,其中+1代表的是每个卷积操作之后需要有一个额外的偏置参数。
又C1层一共包含28x28x6=4704个像素点,而本层的每一个像素点都是由一个5x5的卷积操作外加一个偏置项操作得到的,故一个像素点的计算会产生5x5+1=26条连接,总共会产生4704x26=122304条连接。
3. S2池化层
S2层的输入是C1卷积层的输出,即28x28x6的特征图。这里使用的是核大小是2x2,步长为2,这意味着输入矩阵的每4个相邻元素经过池化操作之后只会输出1个元素,即大小变成了原先的四分之一,故输出大小为14x14x6。池化操作一般分为最大池化和平均池化,这里的池化操作稍微有点不同,它是对输入矩阵中2x2的区域中的全部元素先求和,接着乘上一个可训练的系数,再加上一个偏置项,最后通过一个sigmoid函数,得到最终的输出。因此在经过这样的操作之后,S2的输出的行和列分别变为了输入的一半,即14x14。
对一张特征图进行上述池化操作需要的参数只有2个,即系数和偏置,故总共需要6x2=12个参数。S2池化层的输出大小是14x14x6,其中每一个像素点都需要经过一次池化操作,又一次池化操作需要产生4+1条连接,故总共产生(4+1)x14x14x6=5880条连接。
4. C3卷积层
C3卷积层的输入是S2的输出,即14x14x6的特征图。C3卷积层使用的卷积核大小是5x5,深度为16,即包含了16个卷积核,不需要使用0填充,步长为1。故输出尺寸为10x10x16。但是这16个特征图是如何得到的呢?请看下图: S2层中特征图的组合表其中纵轴代表的是S2池化层输出的6张特征图,横轴代表的是C3卷积层的16个卷积核。这张表按照列可以分为4组,我分别用不同颜色的方框框出来了。其中绿色部分代表的是C3层中的前6个卷积与S2层中的连续的3张特征图相连,蓝色部分代表的是C3层中的6、7、8号卷积核与S2层中连续的4张特征图相连,红色部分代表的是C3层中的9、10、11、12、13、14号卷积核与S2层中不连续的4张特征图相连,黄色部分代表的是C3层中的最后一个卷积核与S2层中所有特征图相连。
为什么S2中的所有特征图不直接与C3中的每一个卷积核全部相连呢?作者认为有2点原因:第一是因为不使用全连接能够保证有连接的数量保持在一个合理的界限范围内可以减少参数。第二是通过这种方式可以打破对称性,不同的卷积核通过输入不同的特征图以期望得到互补的特征。
同样我们再来计算一下参数数量。对于绿色部分,C3中一个卷积核要对3张特征图进行卷积操作,一共有6个卷积核,故总共包含(5x5x3+1)x6=456个参数,同理,蓝色和红色部分总共(5x5x4+1)x9 = 909个参数,黄色部分(5x5x6+1)x1=151个参数。总共456+909+151=1516个参数。总共包含10x10x1516=151600个连接。
5. S4池化层
S4池化层与S2池化层方式相同。把输出降为输入的四分之一 ,即由C3层的输出尺寸10x10x16降到5x5x16大小。核大小为2x2,步长为2。S4层一共包含16x2=32个参数,与S3层一共有(4+1)x5x5x16=2000个连接。
6. C5卷积层
C5卷积层包含了120个卷积核,核大小为5x5,填充为0,步长为1。其中每一个卷积核与S4层的全部输入相连,故每一个卷积核的输出大小是1x1,即C5层的输出是一个120维的向量。C5层与S4层之间一共包含120x(5x5x16+1)=48120个连接。
7. F6全连接层
F6全连接层包含了84个节点,故一共包含了(120+1)x84=10164个参数。F6层通过将输入向量与权重向量求点积,然后在加上偏置项,最后通过一个sigmoid函数输出。
8. OutPut层
Output层也是全连接层,共有10个节点,分别代表数字0到9,且如果节点i的值为0,则网络识别的结果是数字。采用的是径向基函数(RBF)的网络连接方式。假设是上一层的输入,是RBF的输出,则RBF输出的计算方式是:
上式的值由的比特图编码确定,从0到9,取值从0到7*12-1。RBF输出的值越接近于0,则越接近于,即越接近于的ASCII编码图,表示当前网络输入的识别结果是字符。该层有84x10=840个参数和连接。
LeNet5识别数字3
LeNet5 网络识别数字3过程代码实践
论文中的LeNet5结构会稍微复杂点,尤其是C3卷积层的操作,我们这里实现的是一个简化版本。即不考虑卷积核之间的组合,直接利用PyTorch中内置的卷积操作来进行;同理,池化层的操作也是使用PyTorch内置的操作来进行。
总共代码一共包含3个文件,分别是模型文件LeNet5.py、模型训练文件LeNet5_Train.py、以及测试文件LeNet5_Test.py。数据集来自kaggle。
依赖环境:
- python3
- PyTorch
- pandas
- matplotlib
- numpy
模型部分代码
LeNet5.py代码如下:
import torch.nn as nn
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
# 包含一个卷积层和池化层,分别对应LeNet5中的C1和S2,
# 卷积层的输入通道为1,输出通道为6,设置卷积核大小5x5,步长为1
# 池化层的kernel大小为2x2
self._conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1),
nn.MaxPool2d(kernel_size=2)
)
# 包含一个卷积层和池化层,分别对应LeNet5中的C3和S4,
# 卷积层的输入通道为6,输出通道为16,设置卷积核大小5x5,步长为1
# 池化层的kernel大小为2x2
self._conv2 = nn.Sequential(
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1),
nn.MaxPool2d(kernel_size=2)
)
# 对应LeNet5中C5卷积层,由于它跟全连接层类似,所以这里使用了nn.Linear模块
# 卷积层的输入通特征为4x4x16,输出特征为120x1
self._fc1 = nn.Sequential(
nn.Linear(in_features=4*4*16, out_features=120)
)
# 对应LeNet5中的F6,输入是120维向量,输出是84维向量
self._fc2 = nn.Sequential(
nn.Linear(in_features=120, out_features=84)
)
# 对应LeNet5中的输出层,输入是84维向量,输出是10维向量
self._fc3 = nn.Sequential(
nn.Linear(in_features=84, out_features=10)
)
def forward(self, input):
# 前向传播
# MNIST DataSet image's format is 28x28x1
# [28,28,1]--->[24,24,6]--->[12,12,6]
conv1_output = self._conv1(input)
# [12,12,6]--->[8,8,,16]--->[4,4,16]
conv2_output = self._conv2(conv1_output)
# 将[n,4,4,16]维度转化为[n,4*4*16]
conv2_output = conv2_output.view(-1, 4 * 4 * 16)
# [n,256]--->[n,120]
fc1_output = self._fc1(conv2_output)
# [n,120]-->[n,84]
fc2_output = self._fc2(fc1_output)
# [n,84]-->[n,10]
fc3_output = self._fc3(fc2_output)
return fc3_output
模型训练部分
本文代码使用了交叉熵损失函数,SGD优化算法,设置学习率为0.001,动量设置为0.9,小批量数据集大小设置为30,迭代次数为1000次。
LeNet5_Train.py代码如下:
import torch
import torch.nn as nn
import torch.optim as optim
import pandas as pd
import matplotlib.pyplot as plt
from PyTorchVersion.Networks.LeNet5 import LeNet5
train_data = pd.DataFrame(pd.read_csv("../Data/mnist_train.csv"))
model = LeNet5()
print(model)
# 定义交叉熵损失函数
loss_fc = nn.CrossEntropyLoss()
# 用model的参数初始化一个随机梯度下降优化器
optimizer = optim.SGD(params=model.parameters(),lr=0.001, momentum=0.78)
loss_list = []
x = []
# 迭代次数1000次
for i in range(1000):
# 小批量数据集大小设置为30
batch_data = train_data.sample(n=30, replace=False)
# 每一条数据的第一个值是标签数据
batch_y = torch.from_numpy(batch_data.iloc[:,0].values).long()
#图片信息,一条数据784维将其转化为通道数为1,大小28*28的图片。
batch_x = torch.from_numpy(batch_data.iloc[:,1::].values).float().view(-1,1,28,28)
# 前向传播计算输出结果
prediction = model.forward(batch_x)
# 计算损失值
loss = loss_fc(prediction, batch_y)
# Clears the gradients of all optimized
optimizer.zero_grad()
# back propagation algorithm
loss.backward()
# Performs a single optimization step (parameter update).
optimizer.step()
print("第%d次训练,loss为%.3f" % (i, loss.item()))
loss_list.append(loss)
x.append(i)
# Saves an object to a disk file.
torch.save(model.state_dict(),"../TrainedModel/LeNet5.pkl")
print('Networks''s keys: ', model.state_dict().keys())
plt.figure()
plt.xlabel("number of epochs")
plt.ylabel("loss")
plt.plot(x,loss_list,"r-")
plt.show()
模型训练过程中迭代次数与损失之间的变化关系图:
迭代次数与损失之间的变化关系图
可以看到大概经过30次训练之后,损失就已经降到一个较低的水平了。
模型测试部分
总共进行了100次测试,每次测试从测试集中随机挑选50个样本,然后计算网络的识别准确率。
模型测试代码LeNet5_Train.py如下:
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PyTorchVersion.Networks.LeNet5 import LeNet5
model = LeNet5()
test_data = pd.DataFrame(pd.read_csv("../Data/mnist_test.csv"))
#Load model parameters
model.load_state_dict(torch.load("../TrainedModel/LeNet5.pkl"))
accuracy_list = []
testList = []
with torch.no_grad():
# 进行一百次测试
for i in range(100):
# 每次从测试集中随机挑选50个样本
batch_data = test_data.sample(n=50,replace=False)
batch_x = torch.from_numpy(batch_data.iloc[:,1::].values).float().view(-1,1,28,28)
batch_y = batch_data.iloc[:,0].values
prediction = np.argmax(model(batch_x).numpy(), axis=1)
acccurcy = np.mean(prediction==batch_y)
print("第%d组测试集,准确率为%.3f" % (i,acccurcy))
accuracy_list.append(acccurcy)
testList.append(i)
plt.figure()
plt.xlabel("number of tests")
plt.ylabel("accuracy rate")
plt.ylim(0,1)
plt.plot(testList, accuracy_list,"r-")
plt.legend()
plt.show()
测试结果:
测试准确率
平均准确率大概在96%左右。
网友评论