【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet

上一篇请移步【动手学深度学习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数据集上的表现


一、LeNet

1.1 手写数字识别

◼ 手写数字识别

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第1张图片

◼ MNIST数据集

当年的大数据:5万个训练数据集数据,1万个测试数据集数据,图像的大小为28x28,一共10类。

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第2张图片

 

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第3张图片

1.2 LeNet

全连接层的局限性:

  • 图像在同一列邻近的像素在这个向量中可能相距较远。它们构成的模式可能难以被模型识别。
  • 对于大尺寸的输入图像,使用全连接层容易导致模型过大。

卷积层的优势:

  • 卷积层保留输入形状。
  • 卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算,从而避免参数尺寸过大。

LeNet分为卷积层块和全连接层块两个部分。

卷积神经网络就是含卷积层的网络。 LeNet交替使用卷积层和最大池化层后接全连接层来进行图像分类。

LeNet5 这个网络虽然很小,但是它包含了深度学习的基本模块:卷积层,池化层,全连接层。是其他深度学习模型的基础, 这里我们对LeNet5进行深入分析。同时,通过实例分析,加深对与卷积层和池化层的理解。

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第4张图片

上图就是LeNet的网络结构,LeNet又被称为LeNet-5,其之所以称为这个名称是由于原始的LeNet是一个5层的卷积神经网络。LeNet-5共有7层,不包含输入,每层都包含可训练参数;每个层有多个Feature Map,每个Feature Map是通过一种卷积滤波器提取输入的一种特征,然后每个Feature Map有多个神经元。

它主要包括两部分:

  • 卷积层
  • 全连接层

其中卷积层数为2,全连接层数为3。

1、INPUT层-输入层

首先是数据 INPUT 层,输入图像的尺寸统一归一化为32*32。

注意:本层不算LeNet-5的网络结构,传统上,不将输入层视为网络层次结构之一。

2、C1层-卷积层

输入图片: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个连接。

3、S2层-池化层(下采样层)

采样区域: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)个连接。

4、C3层-卷积层

输入: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个特征图。具体如下:
【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第5张图片

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个连接。

C3与S2中前3个图相连的卷积结构如下图所示:
【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第6张图片

 

5、S4层-池化层(下采样层)

输入: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个连接。

6、C5层-卷积层

输入: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个可训练连接。


7、F6层-全连接层

输入:c5 120维向量

计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过Tanh函数输出。

F6层的激活函数是双曲正切函数tanh,将输出映射到(-1,1),对应到ascll编码

可训练参数:84*(120+1)=10164

说明:第6层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164。ASCII编码图如下:

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第7张图片

F6层有84个单元(之所以选这个数字的原因来自于输出层的设计),与C5层全相连。有10164个可训练参数。如同经典神经网络,F6层计算输入向量和权重向量之间的点积,再加上一个偏置。然后将其传递给sigmoid函数产生单元i的一个状态。

F6层的连接方式如下:

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第8张图片

 8、Output层-全连接层

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个参数和连接,没有偏置了。

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第9张图片

上图是LeNet-5识别数字3的过程。

1.3 总结

LeNet-5是早期成功的神经网络,是一种用于手写体字符识别的非常高效的卷积神经网络。
卷积神经网络能够很好的利用图像的结构信息。
卷积层的参数相对较少,这也是由卷积层的主要特性是局部连接和共享权重所决定的

二、代码实现

2.1 LeNet网络(使用自定义)

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)有两个隐藏层的多层感知机:

  • 线性:输入为16x5x5,输出为120,做Sigmoid激活;
  • 线性:输入为120,降到输出为84,做Sigmoid激活;
  • 线性:输入为84,降到输出为10(因为类别为10),做Sigmoid激活;
# 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) # 上一层的输出为这一层的输入

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第10张图片

 我们可以看到,每一层的输入和输出的具体变化情况。

(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把这些所有的模式拿出来,然后做全连接输出。

2.2 LeNet在Fashion-MNIST数据集上的表现

# 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())

【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第11张图片

 【动手学深度学习PyTorch版】16 经典卷积神经网络 LeNet_第12张图片

我们可以看到,测试精度和训练精度基本上重合的,没看到特别多的过拟合。

你可能感兴趣的:(#,深度学习,深度学习,pytorch,计算机视觉,python,神经网络)