1. 介绍
Lenet 是一系列网络的合称,包括 Lenet1 - Lenet5,由 Yann LeCun 等人 在1990年《Handwritten Digit Recognition with a Back-Propagation Network》中提出,是卷积神经网络的开山之作,也是将深度学习推向繁荣的一座里程碑。
LeNet首次采用了卷积层、池化层这两个全新的神经网络组件,接收灰度图像,并输出其中包含的手写数字,在手写字符识别任务上取得了瞩目的准确率。LeNet网络的一系列的版本,以LeNet-5版本最为著名,也是LeNet系列中效果最佳的版本。
2. 网络结构
Lenet是一个 7 层的神经网络,包含 3 个卷积层,2 个池化层,1 个全连接层,1个输出层。其中所有卷积层的卷积核都为 5x5,步长=1,池化方法都为平均池化,激活函数为 Sigmoid(目前使用的Lenet已改为ReLu),网络结构如下:
3. 模型特点
- 首次提出了卷积神经网络基本框架:卷积层,池化层,全连接层
- 卷积层的权重共享,比全连接层使用的参数更少,节省了计算量和存储空间
- 卷积层的局部连接,保证了图像的空间相关性
- 使用映射到空间均值下采样,减少特征数量
- 使用双曲线(tanh)或S型(sigmoid)形式的非线性激活函数
4. 卷积层输入输出计算
-
卷积层输入特征图(input feature map)的尺寸:
input_mapsize = Hin×Win {input\_mapsize}\ =\ H_{in} \times W_{in} input_mapsize = Hin×Win
其中Hin,WinH_{in},W_{in}Hin,Win依次为输入特征图的高,宽
-
输出通道数K(kernel即卷积核个数)
正方形卷积核的边长为FFF;步幅(stride)为SSS;补零的行数和列数(padding)为PPP
- 卷积核与输入图片(二维向量)的计算过程:
output_map = ∑v,winput_map[i+v,j+w]×kernel[v,w] output\_map\ =\ {\sum_{v,w}}{input\_map[i+v,j+w]}{\times}{kernel[v,w]} output_map = v,w∑input_map[i+v,j+w]×kernel[v,w]
其中v,wv,wv,w取值范围为kernel的长宽即[0,F−1][0,F-1][0,F−1]
-
偏置操作:
为了更好的拟合数据,卷积算子还需要加上偏置项,即上面得到的输出特征图每项都要加上一个偏置项。
-
填充(padding):
由于输入图像的边缘位置像素点无法进行卷积滤波,进而填充,即在边缘像素点周围填充“0”(即0填充),注意,在这种填充机制下,卷积后的图像分辨率将与卷积前图像分辨率一致,不存在下采样。
-
下采样(downsampling)
通常称为池化(Pooling),它的作用是减小特征图的空间尺寸。最常见的池化操作是最大池化(Max Pooling),它将原始特征图划分为不重叠的小区域,然后在每个区域中选择最大值作为采样点,从而减小特征图的宽度和高度。池化操作有助于提取特征的平移不变性,同时减少了特征图的尺寸,减少了参数量和计算量,缓解了过拟合。
-
上采样(upsampling)
常用于将特征图的空间尺寸恢复到原始输入尺寸,或者增加特征图的分辨率。常见的上采样操作包括反卷积(Deconvolution)或转置卷积(Transpose Convolution)和插值(Interpolation)。
反卷积操作通过学习可逆卷积核来进行上采样,而插值操作则通过插值算法(如最近邻插值、双线性插值等)对特征图进行填充和插值,从而增加特征图的尺寸。
-
步长(stride)
当Stride=1时,卷积核滑动跳过1个像素,这是最基本的单步滑动,也是标准的卷积模式。Stride=k表示卷积核移动跳过的步长是k。
-
输出特征图(output feature map)的尺寸:
Hout=Hin−kh+1Wout=Win−kw+1 H_{out} = H_{in}-k_h+1\\ W{out} = W{in}-k_w+1 Hout=Hin−kh+1Wout=Win−kw+1
其中kh,kwk_h,k_wkh,kw分别为卷积核的高宽,对上文即都为FFF如果在输入图片第一行之前填充Ph1P_{h1}Ph1行,在最后一行之后填充Ph2P_{h2}Ph2行;在图片第1列之前填充Pw1P_{w1}Pw1列,在最后1列之后填充Pw2P_{w2}Pw2列;则填充之后的图片尺寸为(Hin+Ph1+Ph2)×(Win+Pw1+Pw2)(H_{in}+P_{h1}+P_{h2})\times(W_{in}+P_{w1}+P_{w2})(Hin+Ph1+Ph2)×(Win+Pw1+Pw2)。经过大小为kh×kwk_h{\times}k_wkh×kw的卷积核操作之后,输出图片的尺寸为:
$$
H_{out}=H_{in}+P_{h1}+P_{h2}−k_h+1\W {out} = W{in}+P_{w1}+ P_{w2}−k_w+1
一般padding采取等量填充,且卷积核为正方形,即上式可化为 一般padding采取等量填充,且卷积核为正方形,即上式可化为 一般padding采取等量填充,且卷积核为正方形,即上式可化为
H_{out}=H_{in}+2P_h−F+1\
W {out} = W{in}+2P_w−F+1
$$
即当P=F−12P=\frac{F-1}{2}P=2F−1时,卷积后图像尺寸不变。当高和宽方向的步幅分别为Sh,SwS_h,S_wSh,Sw时,输出特征图尺寸的计算公式是:
Hout=Hin+2Ph−FSh+1Wout=Win+2Pw−FSw+1 H_{out}=\frac{H_{in}+2P_h−F}{S_h}+1\\ W _{out} = \frac{W_{in}+2P_w−F}{S_w}+1 Hout=ShHin+2Ph−F+1Wout=SwWin+2Pw−F+1-
多输入通道,多输出通道和批量操作
前面都为二维的卷积计算过程,但是对于彩色照片有RGB三个通道。
此时的形状为
input_mapsize = Hin×Win×Cin {input\_mapsize}\ =\ H_{in} \times W_{in} \times C_{in} input_mapsize = Hin×Win×Cin
其中Hin,Win,CinH_{in},W_{in},C_{in}Hin,Win,Cin依次为输入特征图的高,宽,通道数-
多输入通道场景
- 分别对每个通道设计一个2维数组作为卷积核,形状为kh×kw×Cink_h \times k_w \times C_{in}kh×kw×Cin
- 对任一通道Ci∈[0,Cin−1]C_i\in[0,C_{in}-1]Ci∈[0,Cin−1]分别用卷积核卷积
- 将多通道结果相加得到一个形状为Hout×WoutH_{out}\times W_{out}Hout×Wout的二维数组
-
多输出通道场景
适用于检测多种类型的特征。卷积核数组维度为Cout×Cin×kh×kwC_{out}\times{C_{in}}\times k_h\times k_wCout×Cin×kh×kw
- 对任一输出通道cout∈[0,Cout)c_{out}\in[0,C_{out})cout∈[0,Cout),分别使用上面描述的形状为kh×kw×Cink_h \times k_w \times C_{in}kh×kw×Cin的卷积核对输入图片做卷积。
- 将这CoutC_{out}Cout个形状为Hout×WoutH_{out}\times{W_{out}}Hout×Wout的二维数组拼接在一起,形成维度为Cout×Hout×WoutC_{out}\times H_{out}\times{W_{out}}Cout×Hout×Wout的三维数组。
-
批量操作
适用于对多帧样本放在一起形成一个mini-batch进行批量操作。此时输入维度为
Hin×Win×Cin×NH_{in} \times W_{in} \times C_{in}\times NHin×Win×Cin×N。对每个图片使用同样的卷积核进行卷积操作,设卷积核维度为Cout×Cin×kh×kwC_{out}\times{C_{in}}\times k_h\times k_wCout×Cin×kh×kw,那么最终的输出特征图维度为N×Cout×Hout×WoutN\times C_{out}\times H_{out}\times{W_{out}}N×Cout×Hout×Wout
-
-
图像卷积例子

分别为边缘检测,锐化,盒式模糊,高斯模糊等处理方式
-
5.LeNet-5网络框架
-
输入层
首先通过尺寸归一化,把输入图像全部转化为32×3232\times3232×32大小
-
第一层-卷积层C1
Parameters size input mapsize 32×3232\times3232×32 kernel size 5×55\times55×5 kernel count 6 featuremap size 28×2828\times2828×28 Neuron count 28×28×628\times28\times628×28×6 Training parameters (5×5+1)×6=156(5\times5+1)\times6=156(5×5+1)×6=156(1为偏置) Connection count 156×6×28×28=122304156\times6\times28\times28=122304156×6×28×28=122304 -
第二层-池化层S2(下采样)
Parameters size input mapsize 28×2828\times2828×28 pooling size 2×22\times22×2 pooling layers 6 featuremap size 14×1414\times1414×14 Neuron count 14×14×614\times14\times614×14×6 Training parameters 2×62\times62×6 Connection count (2×2+1)×6×14×14(2\times2+1)\times6\times14\times14(2×2+1)×6×14×14 作用就是特征映射(降维)。这里减半是因为池化单元没有重叠,也就是S=2S=2S=2,根据上面公式Hout=Hin−FS+1H_{out}=\frac{H_{in}-F}{S}+1Hout=SHin−F+1得到Hout=Hin2H_{out}=\frac{H_{in}}{2}Hout=2Hin,相当于图像大小减半。
池化层计算过程:2×2 单元里的值相加,然后再乘以训练参数w,再加上一个偏置参数b(每一个特征图共享相同的w和b),然后取sigmoid值(S函数:0-1区间),作为对应的该单元的值。下面是两种池化方法。
-
第三层-卷积层C3
Parameters size input mapsize S2中6个特征图组合 kernel size 5×55\times55×5 kernel count 161616 featuremap size 10×10(14−5+1)10\times10(14-5+1)10×10(14−5+1) Training parameters 6×(3×5×5+1)+6×(4×5×5+1)+3×(4×5×5+1)+1×(6×5×5+1)=15166×(3×5×5+1)+6×(4×5×5+1)+3×(4×5×5+1)\\+1×(6×5×5+1)=15166×(3×5×5+1)+6×(4×5×5+1)+3×(4×5×5+1)+1×(6×5×5+1)=1516 Connection count 10×10×1616=15160010\times10\times1616=15160010×10×1616=151600 输入的6个feature map与输出的16个feature map的关系图如下:

C3的前6个feature map(上图红框1的6列)与S2层相连的3个feature map相连接(上图红框1的某相邻的3行),后面6个feature map(上图红框2的6列)与S2层相连的4个feature map相连接(上图红框2的某相邻的4行),后面3个feature map(上图红框3的3列)与S2层部分不相连的4个feature map(上图红框3的某不相邻的4行)相连接,最后一个(上图红框4)与S2层的所有feature map(上图红框4的所有行)相连。
-
第四层-池化层S4
类似S2
Parameters size input mapsize 10×1010\times1010×10 pooling size 2×22\times22×2 pooling layers 161616 featuremap size 5×55\times55×5 Neuron count 5×5×16=4005\times5\times16=4005×5×16=400 Training parameters 2×162\times162×16 Connection count (2×2+1)×16×5×5(2\times2+1)\times16\times5\times5(2×2+1)×16×5×5 -
第五层-卷积层C5
Parameters size input mapsize S4中161616个5×55\times55×5特征图组合 kernel size 5×55\times55×5 kernel count 120120120 featuremap size 1×11\times11×1 Training parameters\Connection count 120×(16×5×5+1)=48120120\times(16\times5\times5+1)=48120120×(16×5×5+1)=48120 -
第六层-全连接层F6
Parameters size input mapsize 120×1×1120\times1\times1120×1×1 output mapsize 848484 Training parameters\Connection count (120+1)×84=10164(120+1)\times84=10164(120+1)×84=10164 第六层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164。
-
输出层-Output层
也为全连接层共十个节点,分别代表数字0到9。如果第i个节点的值为0,则表示网络识别的结果是数字i。采用的是径向基函数(RBF)的网络连接方式。假设x是上一层的输入,y是RBF的输出,则RBF输出的计算方式是:
yi=∑j(xj−wij)2 y_i=\sum_j(x_j-w_{ij})^2 yi=j∑(xj−wij)2
径向基神经网络:它基于距离进行衡量两个数据的相近程度的,RBF网最显著的特点是隐节点采用输人模式与中心向量的距离(如欧氏距离)作为函数的自变量,并使用径向基函数(如函数)作为激活函数。暂不扩展
6.LeNet代码实现
- model.py:定义LeNet网络模型
- train.py:加载数据集并训练,计算loss和accuracy,保存训练好的网络参数
- predict.py:用自己的数据集进行分类测试
-
model.py
# 导入pytorch库 import torch # 导入torch.nn模块 from torch import nn # 定义LeNet网络模型 # MyLeNet5(子类)继承nn.Module(父类) class MyLeNet5(nn.Module): # 子类继承中重新定义Module类的__init__()和forward()函数 # init()函数:进行初始化,申明模型中各层的定义 def __init__(self): # super:引入父类的初始化方法给子类进行初始化 super(MyLeNet5, self).__init__() # 卷积层,输入大小为28*28,输出大小为28*28,输入通道为1,输出为6,卷积核为5,扩充边缘为2 self.c1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2) # 使用sigmoid作为激活函数 self.Sigmoid = nn.Sigmoid() # AvgPool2d:二维平均池化操作 # 池化层,输入大小为28*28,输出大小为14*14,输入通道为6,输出为6,卷积核为2,步长为2 self.s2 = nn.AvgPool2d(kernel_size=2, stride=2) # 卷积层,输入大小为14*14,输出大小为10*10,输入通道为6,输出为16,卷积核为5 self.c3 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5) # 池化层,输入大小为10*10,输出大小为5*5,输入通道为16,输出为16,卷积核为2,步长为2 self.s4 = nn.AvgPool2d(kernel_size=2, stride=2) # 卷积层,输入大小为5*5,输出大小为1*1,输入通道为16,输出为120,卷积核为5 self.c5 = nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5) # Flatten():将张量(多维数组)平坦化处理,张量的第0维表示的是batch_size(数量),所以Flatten()默认从第二维开始平坦化 self.flatten = nn.Flatten() # 全连接层 # Linear(in_features,out_features) # in_features指的是[batch_size, size]中的size,即样本的大小 # out_features指的是[batch_size,output_size]中的output_size,样本输出的维度大小,也代表了该全连接层的神经元个数 self.f6 = nn.Linear(120, 84) # 全连接层&输出层 self.output = nn.Linear(84, 10) # forward():定义前向传播过程,描述了各层之间的连接关系 def forward(self, x): # x输入为28*28*1, 输出为28*28*6 x = self.Sigmoid(self.c1(x)) # x输入为28*28*6,输出为14*14*6 x = self.s2(x) # x输入为14*14*6,输出为10*10*16 x = self.Sigmoid(self.c3(x)) # x输入为10*10*16,输出为5*5*16 x = self.s4(x) # x输入为5*5*16,输出为1*1*120 x = self.c5(x) x = self.flatten(x) # x输入为120,输出为84 x = self.f6(x) # x输入为84,输出为10 x = self.output(x) return x # 测试代码 # 每个python模块(python文件)都包含内置的变量 __name__,当该模块被直接执行的时候,__name__ 等于文件名(包含后缀 .py ) # 如果该模块 import 到其他模块中,则该模块的 __name__ 等于模块名称(不包含后缀.py) # “__main__” 始终指当前执行模块的名称(包含后缀.py) # if确保只有单独运行该模块时,此表达式才成立,才可以进入此判断语法,执行其中的测试代码,反之不行 if __name__ == "__main__": # rand:返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数,此处为四维张量 x = torch.rand([1, 1, 28, 28]) # 模型实例化 model = MyLeNet5() y = model(x) -
train.py
import torch from torch import nn from model import MyLeNet5 # lr_scheduler:提供一些根据epoch训练次数来调整学习率的方法 from torch.optim import lr_scheduler # torchvision:PyTorch的一个图形库,服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型 # transforms:主要是用于常见的一些图形变换 # datasets:包含加载数据的函数及常用的数据集接口 from torchvision import datasets, transforms # os:operating system(操作系统),os模块封装了常见的文件和目录操作 import os # 数据转化为Tensor格式 # Compose():将多个transforms的操作整合在一起 # ToTensor(): 将numpy的ndarray或PIL.Image读的图片转换成形状为(C,H, W)的Tensor格式,且归一化到[0,1.0]之间 data_transform = transforms.Compose([ transforms.ToTensor() ]) # 加载训练数据集 # MNIST数据集来自美国国家标准与技术研究所, 训练集 (training set)、测试集(test set)由分别由来自250个不同人手写的数字构成 # MNIST数据集包含:Training set images、Training set images、Test set images、Test set labels # train = true是训练集,false为测试集 train_dataset = datasets.MNIST(root='./data', train=True, transform=data_transform, download=True) # DataLoader:将读取的数据按照batch size大小封装并行训练 # dataset (Dataset):加载的数据集 # batch_size (int, optional):每个batch加载多少个样本(默认: 1) # shuffle (bool, optional):设置为True时会在每个epoch重新打乱数据(默认: False) train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=True) # 加载测试数据集 test_dataset = datasets.MNIST(root='./data', train=False, transform=data_transform, download=True) test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=True) # 如果有NVIDA显卡,转到GPU训练,否则用CPU device = 'cuda' if torch.cuda.is_available() else 'cpu' # 模型实例化,将模型转到device model = MyLeNet5().to(device) # 定义损失函数(交叉熵损失) loss_fn = nn.CrossEntropyLoss() # 定义优化器(随机梯度下降法) # params(iterable):要训练的参数,一般传入的是model.parameters() # lr(float):learning_rate学习率,也就是步长 # momentum(float, 可选):动量因子(默认:0),矫正优化率 optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9) # 学习率,每隔10轮变为原来的0.1 # StepLR:用于调整学习率,一般情况下会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果 # optimizer (Optimizer):需要更改学习率的优化器 # step_size(int):每训练step_size个epoch,更新一次参数 # gamma(float):更新lr的乘法因子 lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 定义训练函数 def train(dataloader, model, loss_fn, optimizer): loss, current, n = 0.0, 0.0, 0 # dataloader: 传入数据(数据包括:训练数据和标签) # enumerate():用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,一般用在for循环当中 # enumerate返回值有两个:一个是序号,一个是数据(包含训练数据和标签) # x:训练数据(inputs)(tensor类型的),y:标签(labels)(tensor类型的) for batch, (x, y) in enumerate(dataloader): # 前向传播 x, y = x.to(device), y.to(device) # 计算训练值 output = model(x) # 计算观测值(label)与训练值的损失函数 cur_loss = loss_fn(output, y) # torch.max(input, dim)函数 # input是具体的tensor,dim是max函数索引的维度,0是每列的最大值,1是每行的最大值输出 # 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引 _, pred = torch.max(output, axis=1) # 计算每批次的准确率 # output.shape[0]一维长度为该批次的数量 # torch.sum()对输入的tensor数据的某一维度求和 cur_acc = torch.sum(y == pred) / output.shape[0] # 反向传播 # 清空过往梯度 optimizer.zero_grad() # 反向传播,计算当前梯度 cur_loss.backward() # 根据梯度更新网络参数 optimizer.step() # .item():得到元素张量的元素值 loss += cur_loss.item() current += cur_acc.item() n = n + 1 train_loss = loss / n train_acc = current / n # 计算训练的错误率 print('train_loss' + str(train_loss)) # 计算训练的准确率 print('train_acc' + str(train_acc)) # 定义验证函数 def val(dataloader, model, loss_fn): # model.eval():设置为验证模式,如果模型中有Batch Normalization或Dropout,则不启用,以防改变权值 model.eval() loss, current, n = 0.0, 0.0, 0 # with torch.no_grad():将with语句包裹起来的部分停止梯度的更新,从而节省了GPU算力和显存,但是并不会影响dropout和BN层的行为 with torch.no_grad(): for batch, (x, y) in enumerate(dataloader): # 前向传播 x, y = x.to(device), y.to(device) output = model(x) cur_loss = loss_fn(output, y) _, pred = torch.max(output, axis=1) cur_acc = torch.sum(y == pred) / output.shape[0] loss += cur_loss.item() current += cur_acc.item() n = n + 1 # 计算验证的错误率 print("val_loss:" + str(loss / n)) # 计算验证的准确率 print("val_acc:" + str(current / n)) # 返回模型准确率 return current / n # 开始训练 # 训练次数 epoch = 10 # 用于判断最佳模型 min_acc = 0 for t in range(epoch): print(f'epoch {t + 1}\n---------------') # 训练模型 train(train_dataloader, model, loss_fn, optimizer) # 验证模型 a = val(test_dataloader, model, loss_fn) # 保存最好的模型权重 if a > min_acc: folder = 'save_model' # path.exists:判断括号里的文件是否存在,存在为True,括号内可以是文件路径 if not os.path.exists(folder): # os.mkdir() :用于以数字权限模式创建目录 os.mkdir('save_model') min_acc = a print('save best model') # torch.save(state, dir)保存模型等相关参数,dir表示保存文件的路径+保存文件名 # model.state_dict():返回的是一个OrderedDict,存储了网络结构的名字和对应的参数 torch.save(model.state_dict(), 'save_model/best_model.pth') print('Done!') -
predict.py
import torch from model import MyLeNet5 from torch.autograd import Variable from torchvision import datasets, transforms from torchvision.transforms import ToPILImage # Compose():将多个transforms的操作整合在一起 data_transform = transforms.Compose([ # ToTensor():数据转化为Tensor格式 transforms.ToTensor() ]) # 加载训练数据集 train_dataset = datasets.MNIST(root='./data', train=True, transform=data_transform, download=True) # 给训练集创建一个数据加载器, shuffle=True用于打乱数据集,每次都会以不同的顺序返回 train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=True) # 加载测试数据集 test_dataset = datasets.MNIST(root='./data', train=False, transform=data_transform, download=True) test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=True) # 如果有NVIDA显卡,转到GPU训练,否则用CPU device = 'cuda' if torch.cuda.is_available() else 'cpu' # 模型实例化,将模型转到device model = MyLeNet5().to(device) # 加载train.py里训练好的模型 model.load_state_dict(torch.load("D:/pycharm/file/save_model/best_model.pth")) # 结果类型 classes = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ] # 把Tensor转化为图片,方便可视化 show = ToPILImage() # 进入验证阶段 for i in range(10): x, y = test_dataset[i][0], test_dataset[i][1] # show():显示图片 show(x).show() # unsqueeze(input, dim),input(Tensor):输入张量,dim (int):插入维度的索引,最终将张量维度扩展为4维 x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=False).to(device) with torch.no_grad(): pred = model(x) # argmax(input):返回指定维度最大值的序号 # 得到验证类别中数值最高的那一类,再对应classes中的那一类 predicted, actual = classes[torch.argmax(pred[0])], classes[y] # 输出预测值与真实值 print(f'predicted: "{predicted}", actual:"{actual}"')
本文介绍了LeNet卷积神经网络,它是深度学习的里程碑,首次采用卷积层和池化层,在手写字符识别任务准确率高。文中阐述了其网络结构、模型特点,详细讲解卷积层输入输出计算,介绍LeNet - 5网络框架,并给出代码实现,包括定义模型、训练和测试代码。

37万+

被折叠的 条评论
为什么被折叠?



