深度学习10. CNN经典网络 LeNet-5实现MNIST

一、CNN简介

卷积神经网络(Convolutional Neural Network,CNN)是一种常用的深度学习模型,用于图像和视频识别、语音识别、自然语言处理等领域。

CNN的核心思想是采用卷积层、池化层和全连接层等结构,使得模型能够从数据中提取出更加高层次的特征,从而实现更加准确的分类和识别。

各种层的作用:

  1. 在卷积层中,通过不断卷积提取数据的局部特征,最终得到整个输入数据的特征图。
  2. 在池化层中,采用最大值池化等方式进行下采样,降低特征图的维度和复杂度,同时增强特征的鲁棒性和稳定性。
  3. 在全连接层中,将特征图展开为一维向量,并通过多层全连接层对样本进行分类或回归。

常见CNN网络Top-1准确率对比:
在这里插入图片描述

常见CNN网络Top-1准确率与计算量对比:
在这里插入图片描述

二、 LeNet-5简介

1. LeNet-5来源

LeNet-5是一个经典的卷积神经网络模型,1998年被提出,论文题目是 “Gradient-Based Learning Applied to Document Recognition” ,作者为 Yann LeCun, Léon Bottou, Yoshua Bengio, and Patrick Haffner。

LeNet-5是一个用于手写数字识别的深度神经网络模型,由两个卷积层和三个全连接层组成。

LeNet-5是深度神经网络的开创者之一,对后来的深度学习算法发展产生了重要的影响。

2. LeNet-5的结构

  • 输入层:32x32的图像
  • 卷积层C1:使用6个5x5的卷积核,步长为1,激活函数为sigmoid
  • 池化层S2:使用2x2的最大池化操作,步长为2
  • 卷积层C3:使用16个5x5的卷积核,步长为1,激活函数为sigmoid
  • 池化层S4:使用2x2的最大池化操作,步长为2
  • 全连接层F5:输出120个神经元,激活函数为sigmoid
  • 全连接层F6:输出84个神经元,激活函数为sigmoid
  • 输出层:10个神经元,对应10个手写数字类别,使用softmax激活函数

论文地址

在这里插入图片描述

识别动图:

在这里插入图片描述

三、网络详解

1. 输入图像

LeNet-5使用32*32图像。
本文示例将会使用MNIST实现LeNet-5,数据集包含 60000张28x28像素的训练图像和10000张测试图像。

2. 卷积层 C1

C1 用来提取输入图像的特征,输入是一个2828的灰度图像,共6个卷积核,每 个卷积核大小55,卷积核的深度与输入图像的深度相同(即为1)。

卷积核的参数需要进行学习,每个卷积核都学习一个5x5的参数矩阵,这些参数在整个网络训练的过程中进行更新。

在进行卷积计算之前,C1卷积层对输入图像进行了一些预处理。首先,将28x28的输入图像填充成32x32的大小,这样可以使得卷积核的中心始终在输入图像内部移动,从而避免了边缘像素的信息丢失。然后,对填充后的图像进行了局部响应归一化(local response normalization)操作,这个操作可以增强图像特征的对比度。

接着,C1卷积层对输入图像进行6次卷积计算,每次计算都使用一个卷积核。卷积操作的结果是得到6个28x28的特征图,这些特征图分别对应着6个卷积核在输入图像上的响应。

最后,将得到的6个特征图进行叠加,得到的结果是一个6 x 28 x 28的立方体,这个立方体可以被看作是一个三维的特征图。这个特征图将作为下一个卷积层的输入。

使用PyTorch模拟此层可以使用:
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1)

  • 其中参数1 是输入数据通道数,灰度图为1;
  • 参数6是输出数据通道数,即卷积核数量;
  • kernel_size=5是卷积核大小;
  • stride=1是步长。
    此层的输入为 batch_size x 1 x 28 x 28,输出图像的形状为 batch_size x 6 x 24 x 24,卷积操作后图像大小为24 x 24。

3. 池化层S2

在 LeNet-5 中,池化层 S2 的作用是对卷积层 C1 的输出进行下采样,减少模型的参数数量,提高模型的泛化能力。

S2 层的输入是 C1 层的输出,即经过卷积操作后的特征图,其中每个特征图的大小为 24 x 24 x 6。S2 层采用的是 2x2 的池化窗口,因此每个池化单元会处理一个 2x2 的区域,将其中的四个值取平均作为输出。

这样,经过 S2 层的处理,每个特征图变成了 1 x 6 x 12 x 12。这样做的好处是能够降低特征图的大小和数量,减少了参数数量,避免过拟合。同时,池化操作也能够使特征图的位置对平移更加鲁棒,从而提高模型的泛化能力。

LeNet-5里使用的是最大池化层,下文使用PyTorch模拟LeNet-5将使用平均池化层。
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)

4. 卷积层C3

它的输入是池化层S2的输出,该层的作用是提取更高级别的特征。

C3层使用 16 个 5 x 5 的卷积核对 S2 的输出进行卷积,每个卷积核对应一个特征图,因此 C3 层输出的通道数为 16。卷积操作的步长为 1,对边缘进行了补零,因此输出的特征图大小为 8 x 8。

C3层的输出经过ReLU激活函数,然后传递到下一层,即池化层S4。

C3 输出的形状是:1 x 16 x 8 x 8。

5. 池化层 S4

S4层的输入是C3层的输出,它对输入进行降采样(最大池化)。S4层使用的池化窗口大小为2×2,步长为2。

S4层会将C3层的输出沿着宽和高两个维度分别缩小一半。

在LeNet-5中,S4层与C5层之间没有使用任何归一化、正则化等操作,只是简单地使用了最大池化降采样,以保留最显著的特征。

S4输出的形状是: 1 x 16 x 4 x 4

6. 全连接层F5,F6

在 LeNet-5 中,全连接层分为 F5 和 F6 两个部分,其中 F5 包含 120 个神经元,F6 包含 84 个神经元。这两个全连接层负责将前面卷积和池化层的特征进行组合,产生分类所需的输出。

F5 层的输入为一个形状为 1x120 的向量,它与每个神经元都进行连接。每个神经元都有 120 个输入连接,其中每个连接都与 C3 层的一个 5x5 的卷积核进行卷积运算,将其结果相加后再加上一个偏置项进行激活。这样,F5 层产生的输出是一个 1x120 的向量。

F6 层与 F5 层类似,输出是1 x 84的向量。权重在训练期间通过反向传播算法进行优化,以最小化训练数据上的损失函数。

7. 输出层

输出层是一个全连接层,用于将前面的特征提取和处理结果映射到目标类别上。输出层的输出是一个大小为 10 10 10 的向量,每个元素表示模型属于对应类别的概率。

输出层的激活函数使用 softmax 函数,它能够将原始的输出值转换为每个类别的概率分布。softmax 函数的公式如下:

s o f t m a x ( x i ) = e x i ∑ j e x j softmax(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}} softmax(xi)=jexjexi

其中 x i x_i xi 表示输入向量的第 i i i 个元素, ∑ j e x j \sum_j e^{x_j} jexj 表示所有输入元素的指数函数之和。

对于 LeNet-5 模型中的输出层,假设 y 1 , y 2 , . . . , y 10 y_1, y_2, ..., y_{10} y1,y2,...,y10 分别表示该模型属于 0 − 9 0-9 09 十个数字的概率,那么该层的输出可以表示为:

[ y 1 , y 2 , . . . , y 10 ] [y_1, y_2, ..., y_{10}] [y1,y2,...,y10]

最终的预测结果是概率最大的那个数字,即 argmax ⁡ ( y 1 , y 2 , . . . , y 10 ) \operatorname{argmax}(y_1, y_2, ..., y_{10}) argmax(y1,y2,...,y10)

四、 PyTorch的实现

import os

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader


# 定义 LeNet-5 模型
class LeNet5(nn.Module):
    def __init__(self):
        super(LeNet5, self).__init__()
        # 定义卷积层C1,输入通道数为1,输出通道数为6,卷积核大小为5x5
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1)
        # 定义池化层S2,池化核大小为2x2,步长为2
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
        # 定义卷积层C3,输入通道数为6,输出通道数为16,卷积核大小为5x5
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1)
        # 定义池化层S4,池化核大小为2x2,步长为2
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
        # 定义全连接层F5,输入节点数为16x4x4=256,输出节点数为120
        self.fc1 = nn.Linear(16 * 4 * 4, 120)
        # 定义全连接层F6,输入节点数为120,输出节点数为84
        self.fc2 = nn.Linear(120, 84)
        # 定义输出层,输入节点数为84,输出节点数为10
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # 卷积层C1
        x = self.conv1(x)
        # print('卷积层C1后的形状:', x.shape)
        # 池化层S2
        x = self.pool1(torch.relu(x))
        # print('池化层S2后的形状:', x.shape)
        # 卷积层C3
        x = self.conv2(x)
        # print('卷积层C3后的形状:', x.shape)
        # 池化层S4
        x = self.pool2(torch.relu(x))
        # print('池化层S4后的形状:', x.shape)
        # 全连接层F5
        x = x.view(-1, 16 * 4 * 4)
        x = self.fc1(x)
        # print('全连接层F5后的形状:', x.shape)
        x = torch.relu(x)
        # 全连接层F6
        x = self.fc2(x)
        # print('全连接层F6后的形状:', x.shape)
        x = torch.relu(x)
        # 输出层
        x = self.fc3(x)
        # print('输出层后的形状:', x.shape)
        return x


# 设置超参数
batch_size = 64
learning_rate = 0.01
epochs = 10

# 准备数据
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor(), download=True)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

# 实例化模型和优化器
model = LeNet5()
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

# 定义模型保存路径和文件名
model_path = 'model.pth'
if os.path.exists(model_path):
    # 存在,直接加载模型
    model.load_state_dict(torch.load(model_path))
    print('Loaded model from', model_path)
else:
    # 训练模型
    for epoch in range(epochs):
        model.train()
        for images, labels in train_loader:
            # 将图像展平
            # images = images.view(images.size(0), -1)
            images = images.view(-1, 1, 28, 28)
            # 将数据放入模型
            optimizer.zero_grad()
            outputs = model(images)
            loss = nn.functional.cross_entropy(outputs, labels)
            loss.backward()
            optimizer.step()

        # 在测试集上测试模型
        model.eval()
        correct = 0
        with torch.no_grad():
            for images, labels in test_loader:
                # 将图像展平
                images = images.view(-1, 1, 28, 28)
                # 将数据放入模型
                outputs = model(images)
                _, predicted = torch.max(outputs, 1)
                correct += (predicted == labels).sum().item()

        accuracy = 100 * correct / len(test_dataset)
        print('Epoch [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'.format(epoch + 1, epochs, loss.item(), accuracy))

    torch.save(model.state_dict(), 'model.pth')

for i in range(10):
    img, label = next(iter(test_loader))
    img = img[i].unsqueeze(0)

    # 使用模型进行预测
    model.eval()
    with torch.no_grad():
        output = model(img)

    # 解码预测结果
    pred = output.argmax(dim=1).item()
    print(f'Predicted class: {
      
      pred}, actual value: {
      
      label[i]}')


打印的各层的形状:
在这里插入图片描述
部分内容解释 :

图像展平

images = images.view(-1, 1, 28, 28)

输入图像 images 要进行形状的变换。
首先,使用 view 函数将 images 变换为 4 维张量,其中:

  • 第 1 维的大小被设置为 -1,表示这一维度的大小应该是根据输入数据的总大小和其他维度的大小来自动计算的;
  • 第 2 维是 1,表示输入图像只有一个通道;
  • 第 3 维和第 4 维的大小都是 28,表示输入图像的高和宽都是 28。

损失函数

loss = nn.functional.cross_entropy(outputs, labels)
outputs 是模型的输出,labels 是对应的真实标签。

猜你喜欢

转载自blog.csdn.net/xundh/article/details/129581787