上一篇请移步【动手学深度学习PyTorch版】15 池化层_水w的博客-CSDN博客
目录
一、LeNet
1.1 手写数字识别
◼ 手写数字识别
◼ MNIST数据集
1.2 LeNet
1、INPUT层-输入层
2、C1层-卷积层
3、S2层-池化层(下采样层)
4、C3层-卷积层
5、S4层-池化层(下采样层)
6、C5层-卷积层
7、F6层-全连接层
8、Output层-全连接层
1.3 总结
二、代码实现
2.1 LeNet网络(使用自定义)
2.2 LeNet在Fashion-MNIST数据集上的表现
当年的大数据:5万个训练数据集数据,1万个测试数据集数据,图像的大小为28x28,一共10类。
全连接层的局限性:
卷积层的优势:
LeNet分为卷积层块和全连接层块两个部分。
卷积神经网络就是含卷积层的网络。 LeNet交替使用卷积层和最大池化层后接全连接层来进行图像分类。
LeNet5 这个网络虽然很小,但是它包含了深度学习的基本模块:卷积层,池化层,全连接层。是其他深度学习模型的基础, 这里我们对LeNet5进行深入分析。同时,通过实例分析,加深对与卷积层和池化层的理解。
上图就是LeNet的网络结构,LeNet又被称为LeNet-5,其之所以称为这个名称是由于原始的LeNet是一个5层的卷积神经网络。LeNet-5共有7层,不包含输入,每层都包含可训练参数;每个层有多个Feature Map,每个Feature Map是通过一种卷积滤波器提取输入的一种特征,然后每个Feature Map有多个神经元。
它主要包括两部分:
其中卷积层数为2,全连接层数为3。
首先是数据 INPUT 层,输入图像的尺寸统一归一化为32*32。
注意:本层不算LeNet-5的网络结构,传统上,不将输入层视为网络层次结构之一。
输入图片:32*32
卷积核大小:5*5
卷积核种类:6
输出feature map大小:28*28 ,因为(32-5+1)=28
神经元数量:28*28*6
可训练参数:(5*5+1) * 6(每个滤波器5*5=25个单元参数(unit)和一个偏置参数(bias),一共6个滤波器)
连接数:(5*5+1)*6*28*28=122304
说明:对输入图像进行第一次卷积运算(使用 6 个大小为 5*5 的卷积核),得到6个C1特征图(6个大小为28*28的 feature maps, 32-5+1=28)。先来看看需要多少个参数,卷积核的大小为5*5,总共就有6*(5*5+1)=156个参数,其中+1是表示一个核有一个bias。对于卷积层C1,C1内的每个像素都与输入图像中的5*5个像素和1个bias有连接,所以总共有156*28*28=122304个连接(connection)。有122304个连接,但是我们只需要学习156个参数,主要是通过权值共享实现的--可以理解为把(5*5+1) * 6个训练参数共享了28*28次。
为什么是卷积?卷积运算一个重要的特点就是,通过卷积运算,可以使原信号特征增强,并且降低噪音),由6个特征图Feature Map构成。特征图中每个神经元与输入中5*5的邻域相连。特征图的大小为28*28,这样能防止输入的连接掉到边界之外(是为了BP反馈时的计算,不致梯度损失,个人见解)。C1有156个可训练参数(每个滤波器5*5=25个unit参数和一个bias参数,一共6个滤波器,共(5*5+1)*6=156个参数),共156*(28*28)=122,304个连接。
采样区域:2*2
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid
采样种类:6
输出featureMap大小:14*14,因为(28/2)
神经元数量:14*14*6
可训练参数:2*6(和的权+偏置这两个参数)
连接数:(2*2+1)*14*14*6
S2中每个特征图的大小是C1中特征图大小的1/4。
说明:第一次卷积之后就是池化运算,使用 2*2核 进行池化,得到了S2,6个14*14的 特征图(28/2=14)。S2这个pooling层是对C1中的2*2区域内的像素求和乘以一个权值系数再加上一个偏置,然后将这个结果再做一次映射。于是每个池化核有两个训练参数,所以共有2x6=12个训练参数,但是有5x14x14x6=5880个连接。
为什么是下采样?利用图像局部相关性的原理,对图像进行子抽样,可以减少数据处理量同时保留有用信息),有6个14*14的特征图。特征图中的每个单元与C1中相对应特征图的2*2邻域相连接。S2层每个单元的4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid函数计算。可训练系数和偏置控制着sigmoid函数的非线性程度。如果系数比较小,那么运算近似于线性运算,亚采样相当于模糊图像。如果系数比较大,根据偏置的大小亚采样可以被看成是有噪声的“或”运算或者有噪声的“与”运算。每个单元的2*2感受野并不重叠,因此S2中每个特征图的大小是C1中特征图大小的1/4(行和列各1/2)。有6个14*14的特征图。特征图中的每个单元与C1中相对应特征图的2*2邻域相连接。S2层每个单元的4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。每个单元的2*2感受野并不重叠,因此S2中每个特征图的大小是C1中特征图大小的1/4(行和列各1/2)。S2层有12(6*(1+1)=12)个可训练参数和5880(14*14*(2*2+1)*6=5880)个连接。
输入:S2中所有6个或者几个特征map组合
卷积核大小:5*5
卷积核种类:16
输出featureMap大小:10*10 ,因为 (14-5+1)=10
可训练参数为:6*(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1*(6*5*5+1)=1516
连接数:10*10*6*{(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1*(6*5*5+1)}=151600
C3特征图不是直接由S2和16个卷积核卷积运算直接得来的,而是采取特征图组合方式得出的,C3中前6个特征图来自于S2中任意连续的3个特征图作为输入与5* 5 *3大小的卷积核运算得来,S2中连续的3个特征图共有6种组合,所以得出C3中6个特征图则需要6个5* 5* 3的卷积核。C3中后6个特征图来自于S2中任意连续的4个特征图,类似的最终需要6个5 *5 *4大小的卷积核。C3中再后面的3个特征图来自于S2中两两不相邻的4个特征图,共3种组合所以需要3个5* 5 *4大小的卷积核。C3最后的一个特征图来自于S2中所有的特征图,因此需要的1个大小为5* 5* 6的卷积核。S4与S2的下采样方式类似,在C3上选择的单位处理区域大小为2*2,最后得到5* 5*16大小的特征图。
说明:第一次池化之后是第二次卷积,第二次卷积的输出是C3,16个10x10的特征图,卷积核大小是 5*5. 我们知道S2 有6个 14*14 的特征图,怎么从6 个特征图得到 16个特征图了? 这里是通过对S2 的特征图特殊组合计算得到的16个特征图。具体如下:
C3的前6个feature map(对应上图第一个红框的6列)与S2层相连的3个feature map相连接(上图第一个框),后面6个feature map与S2层相连的4个feature map相连接(上图第二个框),后面3个feature map与S2层部分不相连的4个feature map相连接,最后一个与S2层的所有feature map相连。卷积核大小依然为5*5,所以总共有6*(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1*(6*5*5+1)=1516个参数。而图像大小为10*10,所以共有151600个连接。
输入:10*10
采样区域:2*2
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid
采样种类:16
输出featureMap大小:5*5 ,因为(10/2)
神经元数量:5*5*16=400
可训练参数:2*16=32(和的权+偏置)
连接数:16*(2*2+1)*5*5=2000
S4中每个特征图的大小是C3中特征图大小的1/4
说明:S4是pooling层,窗口大小仍然是2*2,共计16个feature map,C3层的16个10x10的图分别进行以2x2为单位的池化得到16个5x5的特征图。
特征图中的每个单元与C3中相应特征图的2*2邻域相连接,跟C1和S2之间的连接一样。S4层有2x16共32个可训练参数(每个特征图1个因子和一个偏置)和5x5x5x16=2000个连接。
输入:S4层的全部16个单元特征map(与s4全相连)
卷积核大小:5*5
卷积核种类:120
输出featureMap大小:1*1 ,因为(5-5+1)
可训练参数/连接:120*(16*5*5+1)=48120
说明:C5层是一个卷积层,有120个特征图,每个单元与S4层的全部16个单元的5*5邻域相连。
由于S4层的16个图的大小为5x5(同滤波器一样),与卷积核的大小相同,所以C5卷积后形成的特征图的大小为1x1,这构成了S4和C5之间的全连接。这里形成120个卷积结果。每个都与上一层的16个图相连。所以共有(5x5x16+1)x120 = 48120个参数,同样有48120个连接。
之所以仍将C5标示为卷积层而非全连接层,是因为如果LeNet-5的输入变大,而其他的保持不变,那么此时特征图的维数就会比1*1大。C5层有48120个可训练连接。
输入:c5 120维向量
计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过Tanh函数输出。
F6层的激活函数是双曲正切函数tanh,将输出映射到(-1,1),对应到ascll编码
可训练参数:84*(120+1)=10164
说明:第6层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164。ASCII编码图如下:
F6层有84个单元(之所以选这个数字的原因来自于输出层的设计),与C5层全相连。有10164个可训练参数。如同经典神经网络,F6层计算输入向量和权重向量之间的点积,再加上一个偏置。然后将其传递给sigmoid函数产生单元i的一个状态。
F6层的连接方式如下:
Output层也是全连接层,共有10个节点,分别代表数字0到9,且如果节点i的值为0,则网络识别的结果是数字i。
采用的是径向基函数(RBF)的网络连接方式。假设x是上一层的输入,y是RBF的输出,则RBF输出的计算方式是:
上式w_ij 的值由i的比特图编码确定,i从0到9,j取值从0到7*12-1。RBF输出的值越接近于0,则越接近于i,即越接近于i的ASCII编码图,表示当前网络输入的识别结果是字符i。该层有84x10=840个参数和连接,没有偏置了。
上图是LeNet-5识别数字3的过程。
LeNet-5是早期成功的神经网络,是一种用于手写体字符识别的非常高效的卷积神经网络。
卷积神经网络能够很好的利用图像的结构信息。
卷积层的参数相对较少,这也是由卷积层的主要特性是局部连接和共享权重所决定的
LeNet(LeNet-5) 由两个部分组成:卷积编码器和全连接层密集块。
(1)自定义类:将X放成一个批量数不变,通道数变为1的1X28X28。
(2)先把1X28X28的图片放入卷积层里:输入通道1,输出通道6,核5x5,填充为2(因为图片元原始输入为32x32,28X28图片把两边各自的2行删掉了,所以此处额外添加)。
为了得到非线性性,在卷积后面加入nn.Sigmoid()激活函数。
(3)均值池化层:stride=2使得2X2的窗口不会被重叠在一起,
(4)卷积层:输入通道6,输出通道16,核5x5,为了得到非线性性,在卷积后面加入nn.Sigmoid()激活函数。
(5)均值池化层:stride=2使得2X2的窗口不会被重叠在一起,因为卷积层出来的是4D,所以把最后通道数,高和宽变成一维向量,输入到多层感知机。
(6)有两个隐藏层的多层感知机:
# LeNet(LeNet-5) 由两个部分组成:卷积编码器和全连接层密集块
import torch
from torch import nn
from d2l import torch as d2l
class Reshape(torch.nn.Module):
"""自定义类:将X放成一个批量数不变,通道数变为1的1X28X28"""
def forward(self,x):
return x.view(-1,1,28,28) # 批量数自适应得到,通道数为1,图片为28X28
net = torch.nn.Sequential(
# 把1X28X28的图片放入卷积层里:输入通道1,输出通道6,核5x5,填充为2,为了得到非线性性,在卷积后面加入nn.Sigmoid()激活函数。
Reshape(), nn.Conv2d(1,6,kernel_size=5,padding=2),nn.Sigmoid(),
# 均值池化层:stride=2使得2X2的窗口不会被重叠在一起,
nn.AvgPool2d(2,stride=2),
# 卷积层:输入通道6,输出通道16,核5x5,为了得到非线性性,在卷积后面加入nn.Sigmoid()激活函数。
nn.Conv2d(6,16,kernel_size=5),nn.Sigmoid(),
# 均值池化层:stride=2使得2X2的窗口不会被重叠在一起,因为卷积层出来的是4D,所以把最后通道数,高和宽变成一维向量,输入到多层感知机
nn.AvgPool2d(kernel_size=2,stride=2),nn.Flatten(),
## 有两个隐藏层的多层感知机:
# 线性:输入为16x5x5,输出为120,做Sigmoid激活
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
# 线性:输入为120,降到输出为84,做Sigmoid激活
nn.Linear(120, 84), nn.Sigmoid(),
# 线性:输入为84,降到输出为10(因为类别为10),做Sigmoid激活
nn.Linear(84,10))
定义好之后,随机给定一个输入,对里面的每一层做迭代,其中上一层的输出为这一层的输入。
X = torch.rand(size=(1,1,28,28),dtype=torch.float32)
for layer in net:
X = layer(X) # 对里面的每一层做迭代
print(layer.__class__.__name__,'output shape:\t',X.shape) # 上一层的输出为这一层的输入
我们可以看到,每一层的输入和输出的具体变化情况。
(1)那么第一组模块:卷积+池化
Reshape output shape: torch.Size([1, 1, 28, 28]) Conv2d output shape: torch.Size([1, 6, 28, 28]) Sigmoid output shape: torch.Size([1, 6, 28, 28]) AvgPool2d output shape: torch.Size([1, 6, 14, 14])
卷积层干的事情就是把通道是加6,高宽没变,激活函数也没变。
池化层:通道没变,高宽变了。
所以说,第一组模块:卷积+池化,等于是说把1X28X28的图片变成了6X14X14,即高宽减半,但是通道数增加了6倍。所以其实信息是变多了。
(2)那么第二组模块:卷积+池化
AvgPool2d output shape: torch.Size([1, 6, 14, 14]) Conv2d output shape: torch.Size([1, 16, 10, 10]) Sigmoid output shape: torch.Size([1, 16, 10, 10]) AvgPool2d output shape: torch.Size([1, 16, 5, 5]) Flatten output shape: torch.Size([1, 400])
输入到第二组模块的输入是6X14X14,整个第二组模块的输出是16X5X5,也就是说高宽仍然被减了大概3倍的样子,通道数从6变成了16,最后Flatten拉直了之后,变成了400。
(3)那么第三组模块:有两个隐藏层的多层感知机
Flatten output shape: torch.Size([1, 400]) Linear output shape: torch.Size([1, 120]) Sigmoid output shape: torch.Size([1, 120]) Linear output shape: torch.Size([1, 84]) Sigmoid output shape: torch.Size([1, 84]) Linear output shape: torch.Size([1, 10])
线性:输入为16x5x5,输出为120,做Sigmoid激活;
线性:输入为120,降到输出为84,做Sigmoid激活;
线性:输入为84,降到输出为10(因为类别为10),做Sigmoid激活。
总结就是说,不断的把空间信息压缩压缩,通道数不断增加,也就是说把抽出来的压缩的信息放到不同的通道里去,最后MLP把这些所有的模式拿出来,然后做全连接输出。
# LeNet在Fashion-MNIST数据集上的表现
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
对evaluate_accuracy函数进行轻微的修改,
# 对evaluate_accuracy函数进行轻微的修改
def evaluate_accuracy_gpu(net, data_iter, device=None):
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # net.eval()开启验证模式,不用计算梯度和更新梯度
if not device:
device = next(iter(net.parameters())).device # 如果device没有给定,看net.parameters()中第一个元素的device为哪里
metric = d2l.Accumulator(2) # 累加器
for X, y in data_iter: #
if isinstance(X,list):
X = [x.to(device) for x in X] # 如果X是个List,则把每个元素都挪到device上
else:
X = X.to(device) # 如果X是一个Tensor,则只用移动一次,直接把X移动到device上
y = y.to(device) # 把y也挪过去
# 把X放到network中得到输出,计算accuracy,然后用y.numel()计算y元素个数
metric.add(d2l.accuracy(net(X),y),y.numel())
return metric[0]/metric[1] # 分类正确的个数除以整个y的大小得到accuracy
为了使用GPU,还需要一点小改动,需要把把输入和输出,参数都挪到gpu上去。
# 为了使用GPU,还需要一点小改动
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""Train a model with a GPU"""
def init_weights(m):
"""初始化weight"""
if type(m) == nn.Linear or type(m) == nn.Conv2d: # 如果是全连接层或者卷积层,使用xavier初始化
nn.init.xavier_uniform_(m.weight) # 根据输入、输出大小,使得随即初始化后,输入和输出的的方差是差不多的
net.apply(init_weights) # 应用到整个net上
print('training on',device) # 打印在哪个device上训练
net.to(device) # 把整个参数挪到gpu的内存上
optimizer = torch.optim.SGD(net.parameters(),lr=lr) # 给定lr,进行梯度下降
loss = nn.CrossEntropyLoss() # 多类分类问题
animator = d2l.Animator(xlabel='epoch',xlim=[1,num_epochs],
legend=['train loss', 'train acc', 'test acc']) # 动画效果
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs): # 对每次数据做迭代
metric = d2l.Accumulator(3)
net.train()
for i, (X,y) in enumerate(train_iter): # 每次数据做迭代时,拿一个batch数据出来
timer.start()
optimizer.zero_grad() # 梯度设置为0
X, y = X.to(device), y.to(device) # 把输入和输出挪到gpu上
y_hat = net(X) # 做前向操作
l = loss(y_hat, y) # 计算损失
l.backward() # 计算梯度
optimizer.step() # 迭代
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat,y),X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if(i+1) % (num_batches//5) == 0 or i == num_batches - 1:
animator.add(epoch + (i+1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f},train acc {train_acc:.3f},'
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec'
f'on{str(device)}')
训练和评估LeNet-5模型,
# 训练和评估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
我们可以看到,测试精度和训练精度基本上重合的,没看到特别多的过拟合。