首页 > 其他分享 >手撸代码#1:从0开始的LeNet5(PyTorch框架)

手撸代码#1:从0开始的LeNet5(PyTorch框架)

时间:2024-01-31 14:23:16浏览次数:26  
标签:LeNet5 框架 self torch PyTorch 训练 nn out

摘要:

本文介绍了如何从0开始构建 LeNet5 去识别手写数字(在MNIST数据集上)。代码包括三大部分:网络结构部分、训练部分、测试部分。在编LeNet5部分代码之前,本文详细地梳理了LeNet5的结构,对于初学者十分友好。训练和测试部分也都有详细的代码说明。

在实现 LeNet5 手写数字识别的同时,补充了很多CNN的基础概念和Python编程知识。包括:PyTorch中的常用库和其中的模块、特征图在卷积过程中尺寸如何变化、如何把数据加载进训练程序等。

本文不是通过复制粘贴代码介绍如何实现 LeNet5 的手写数字识别,而是通过内在逻辑,深层次地阐述这一过程,力求“知其然,知其所以然。”


温馨提示:
(1)本文主要介绍如何从0实现LeNet5,注重编程思路的讲解,对于一些前置知识不做赘述。
(2)请确保你已经配置好并进入了深度学习环境
(3)目录导航见页面右上角黄色方块。

前置知识:
(1)概念:PyTorch、卷积、池化、全连接、ReLU、前向传播、反向传播
(2)对python语法有基本的了解


在从0开始编程前,我们首先思考一下,一个由LeNet5完成的图像分类任务(如:手写数字识别),都需要哪些组成部分?

首先,肯定要有LeNet5网络结构的代码。
其次,还要有在训练集上训练的代码,让网络学习特征表示。
最后,训练完要在测试集上测试,不然咋知道训练得效果怎样呢?


1. 引入库(Import the Libraries)

“库”是一组已经写好的代码,可以理解为一个“工具箱”。对于某些功能的实现,开发者可以从引入的“工具箱”中拿出工具直接使用,而不是从头开始“造工具”(即编代码)。所以在所有工作之前,要把“工具箱”引入进来,以方便后续编程。但有时候,我们引入库时可能会漏掉一些库,在编程到后面才意识到。不用担心,我们再回到开头这里引入库就好了。

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
  • torch库提供了各种用于张量(tensor)操作、神经网络搭建、优化算法等方面的函数。
    什么是张量?如果你对张量(tensor)不了解,不用担心,你只需要记住这是图片经过某种处理之后的一种形式,就像你已知的图片有.jpg和.png格式一样。但对于.jpg和.png格式的图像,神经网络并不喜欢,无法直接处理它们。而张量(tensor)这种形式,适用于神经网络的处理。

  • torch.nn库可以理解为大工具箱torch里面的小工具箱nn。这个模块里包含构建神经网络层和模型的类和函数。import torch.nn as nn表示,引入之后,模块torch.nn的名字就可以简称nn了。

  • torchvision库提供了一系列用于图像处理、计算机视觉数据集加载、图像变换、以及许多流行的计算机视觉模型的实现。

  • torchvision.transforms模块用于进行图像的变换和预处理。这个模块包含了一系列用于处理图像的转换函数,可用于数据增强、数据清理和准备图像数据以输入神经网络等任务。


2. 选择在哪个设备上训练模型(GPU或CPU)

通常情况下,我们都会选择在GPU上训练网络模型,因为神经网络的训练需要大量的计算,而英伟达的GPU提供了CUDA(一个加速计算库)。但如果你的电脑显卡是AMD的,那么有很大概率不支持使用CUDA,此时只能用CPU训练。但在CPU上训练模型是十分缓慢的。如果你暂时没法换电脑,那我建议你去租一个服务器。或者使用阿里云、百度飞桨、谷歌Colab等平台。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  • torch.cuda.is_available()函数的功能是检查系统中是否安装了可用的 CUDA并且 GPU 是可用的。如果 GPU 可用,返回 True,否则返回 False。

  • 'cuda' if torch.cuda.is_available() else 'cpu'表示如果GPU可用,则返回字符串'cuda'。如果不可用,则返回字符串'cpu'

  • 如果函数torch.device(...)接收到的是'cuda',则选择在GPU上计算,如果接收到的是'cpu',则选择在CPU上进行计算。


3. 认识网络

充分地认识网络,才能在编程时思路清晰、游刃有余。下图是LeNet5的结构图,请你直观地认识一下LeNet5。如果你不想了解,可以直接去 4. 构建网络,不过我不建议你这样。
本例中LeNet5处理的MNIST数据集图片尺寸都是32×32. 所以下图的输入图片是32×32.

image

我把上图的网络结构具体为下面的表格,以便我们后续编程。注意:画表格并不是编程所需的必要步骤,只是我希望表达得更清晰。

层的名称 具体操作 输入通道数 输出通道数 核或池的尺寸 步幅
卷积层1 卷积 1 6 5×5 1
批归一化 6 6 -- --
ReLU 6 6 -- --
下采样 最大池化 6 6 2×2 2
卷积层2 卷积 6 16 5×5 1
批归一化 16 16 -- --
ReLU 16 16 -- --
下采样 最大池化 16 16 2×2 2
全连接层1 全连接 400 120 -- --
ReLU 120 120 -- --
全连接层2 全连接 120 84 -- --
ReLU 84 84 -- --
高斯连接 全连接 84 类别总数10 -- --

下面我们梳理一下LeNet5的详细流程,这样到后面编程的时候不会懵。


(1)卷积层1:提取低级特征

a) 第一次卷积

在图中可以看到,第一次卷积操作之后,不仅通道由1变6,原图32×32的尺寸也变成了28×28,这与卷积核大小、步幅和 \(padding\) 有关(LeNet5中,2次卷积padding都为0)。输出特征图像尺寸公式如下:

\[\text{output size} = \frac{W - \text{kernel size} + 2 \times \text{padding}}{\text{stride}} + 1 \]

其中,\(W\)表示输入图像的宽度。

将数代入公式:

\[\text{output size} = \frac{32 - 5 + 2 \times 0}{1} + 1=28 \]

b) 批归一化

批归一化 (Batch Normalization) 是一种用于提高神经网络训练稳定性和加速收敛的方法。在卷积神经网络中,每个通道都有一个独立的归一化参数。因此输入是6通道,输出还是6通道。

c) ReLU激活函数

产生非线性映射,通道数还是6,不变。


(2)下采样

最大池化,通道数不变,还是6,特征图尺寸由28×28下降到14×14. 计算公式如下:

\[\text{output size} = \frac{W - \text{pool size}}{\text{stride}} + 1=\frac{28 - 2}{2} + 1=14 \]


(3)卷积层2:提取高级特征

a) 第二次卷积

通道由6变16,输出特征图尺寸为10×10.具体计算公式如下:

\[\text{output size} = \frac{W - \text{kernel size} + 2 \times \text{padding}}{\text{stride}} + 1=\frac{14 - 5 + 2 \times 0}{1} + 1=10 \]

b) 批归一化+ReLU

输出通道还是16


(4)下采样

特征图尺寸由10×10变成5×5,输出通道还是16

\[\text{output size} = \frac{W - \text{pool size}}{\text{stride}} + 1=\frac{10 - 2}{2} + 1=5 \]


(5)全连接层1

将输入维度为 400 的向量(一维数组)映射到维度为 120 的输出向量。
其中,400为16个通道的5×5大小的所有像素数量。\(16×5×5=400\)
120是研究人员通过在实验中不断调整得到的。

(6)全连接层2

将前一层的 120 个节点映射到 84 个节点。

(7)高斯连接(全连接层3)

将84个节点映射到10个具体类别上,即0~9这10个数字上。


4. 搭建网络

在搭建之前,我将介绍几个“工具”。nn.Sequential()是 PyTorch 中用于构建容器(container)的类。通俗地讲,就是把好几个操作串到一起,按顺序执行。

# 定义名为 LeNet5 的类,该类继承自 nn.Module
class LeNet5(nn.Module):
    def __init__(self, num_classes):
        super(ConvNeuralNet, self).__init__()
        # 卷积层 1
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),	# 卷积
            nn.BatchNorm2d(6),		# 批归一化
            nn.ReLU(),)
        # 下采样
        self.subsampel1 = nn.MaxPool2d(kernel_size = 2, stride = 2)		# 最大池化
        # 卷积层 2
        self.layer2 = nn.Sequential(
            nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(16),
            nn.ReLU(),)
        # 下采样
        self.subsampel2 = nn.MaxPool2d(kernel_size = 2, stride = 2)
        # 全连接
        self.L1 = nn.Linear(400, 120)
        self.relu = nn.ReLU()
        self.L2 = nn.Linear(120, 84)
        self.relu1 = nn.ReLU()
        self.L3 = nn.Linear(84, num_classes)
    # 前向传播
    def forward(self, x):
        out = self.layer1(x)
        out = self.subsampel1(out)
        out = self.layer2(out)
        out = self.subsampel2(out)
        # 将上一步输出的16个5×5特征图中的400个像素展平成一维向量,以便下一步全连接
        out = out.reshape(out.size(0), -1)
        # 全连接
        out = self.L1(out)
        out = self.relu(out)
        out = self.L2(out)
        out = self.relu1(out)
        out = self.L3(out)
        return out

发现没有?套路就是:先self.各种层,一顿定义。然后到前向传播(forward)那里,开始用上一步定义的self.something()。最后,return out返回输出值。

对于初学者而言,如果Python基础不牢,最开始那三行代码理解起来可能比较吃力。其实用多了就会发现,这就是个套路,不理解也不耽误编程。


前三行代码
class LeNet5(nn.Module):
    def __init__(self, num_classes):
        super(ConvNeuralNet, self).__init__()
  • 我们来看第一行代码。这行代码定义了一个名为 LeNet5 的类,它继承自 nn.Module(别忘了,在最开始引入库时,引入过nn),说明这是一个 PyTorch 框架中神经网络模型的基类。所有的神经网络模型都应该继承自 nn.Module,这样它们就能够利用 PyTorch 提供的模型管理和训练的功能。

  • 第二行代码:是类的构造函数(initializer)。构造函数用于初始化类的实例。num_classes是类别数,这里是我们构造的网络所接收的参数,后续要用到这个参数。

  • 第三行代码:调用了父类 nn.Module 的构造函数,确保正确地初始化 LeNet5 类的父类部分。这是 Python 中用于调用父类方法的一种方式。

如果读完这些解释还是不理解,没关系,你可以把这三行当作一个套路,用多了自然就会记住。


5. 准备(加载)数据集

在上一步中,LeNet5已经搭建好了,现在该编写训练部分的程序了。但是在这之前有一步不能落下,那就是数据加载。

要把数据集里的一堆数据“码好”了,一批一批地“喂”给LeNet5.

MNIST 数据集是一个手写数字图像数据集,包含了大量的手写数字图片,每张图片都标注了对应的数字。
torchvision.datasets.MNIST(...)是 PyTorch 中用于加载 MNIST 数据集的类。
torch.utils.data.DataLoader(...) 是 PyTorch 中用于批量加载数据的工具类,是一个数据加载器。

注意:前者用于加载数据,后者用于加载数据,不要混淆。

把一个数据集比作一副扑克牌,那么一张扑克牌就是一个数据。把DataLoader比作神经网络的手,手去抓牌。一次抓几张,抓牌有没有顺序,等等,这些都是通过设置DataLoader的参数决定的。batch_size等于几就是一次抓几张牌,即一次“喂”给神经网络几张图片。

# 加载训练集
train_dataset = torchvision.datasets.MNIST(root = './data',	# 数据集保存路径
                                           train = True,	# 是否为训练集
                                           # 数据预处理
                                           transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1307,), 
												                     std = (0.3081,))]),
                                           download = True)	#是否下载

# 加载测试集
test_dataset = torchvision.datasets.MNIST(root = './data',
                                          train = False,
                                          transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1325,), 
												                     std = (0.3105,))]),
                                          download=True)
# 一次抓64张牌
batch_size = 64
# 加载训练数据
train_loader = torch.utils.data.DataLoader(dataset = train_dataset,
                                           batch_size = batch_size,
                                           shuffle = True)	# 是否打乱
# 加载测试数据
test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
                                           batch_size = batch_size,
                                           shuffle = False)	# 是否打乱
  • 训练阶段的shuffle=True:将数据集打乱顺序,有助于模型学习更泛化的特征。通过打乱数据顺序,模型在每个 epoch 中都能够看到不同的样本,防止模型过度拟合训练集中的特定顺序。

  • 测试阶段的shuffle=False:在测试阶段,通常不需要打乱数据的顺序。测试时模型是在未见过的数据上进行评估,因此希望模型看到的是原始数据的有序顺序,以便能够更好地评估模型的泛化性能。如果在测试时也打乱数据,可能会导致模型在评估时看到的数据分布与实际场景不一致。(其实如果是True影响也不大)


6. 设置超参数

在训练之前,我们需要设置一些超参数,为训练阶段要用到的模型、损失函数、优化器做准备。

(1)创建 LeNet5 模型

num_classes = 10
model = LeNet5(num_classes).to(device)
  • LeNet5(num_classes):创建一个 LeNet5 类的实例。还记得么?前文提到过,该类是继承自 nn.Module 的神经网络模型,接受 num_classes 参数,表示模型输出的类别数目。
  • to(device):将模型移动到指定的设备上,其中 device 是一个设备对象,可以是 'cuda'(GPU)或者 'cpu'。这一步是为了确保模型在训练和推理时使用的是正确的计算设备。
  • model = ...:将创建并移动到指定设备的模型赋值给变量 model,以便后续对模型的引用和操作。

(2)创建损失函数

在这里,损失函数设置为交叉熵损失函数

cost = nn.CrossEntropyLoss()
  • nn.CrossEntropyLoss()是 PyTorch 中用于计算多类别交叉熵损失的损失函数。交叉熵损失通常用于分类问题,特别是当目标是多类别标签时。它的计算涉及到模型的预测值和实际类别标签之间的比较,以衡量模型输出与真实标签之间的差异。

(3)创建优化器

learning_rate = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
  • torch.optim.Adam:这是 PyTorch 中提供的 Adam 优化器的实现。Adam 是一种常用的随机梯度下降算法的变体,它通过自适应地调整学习率来优化模型参数。
  • model.parameters():指定要被优化的参数,就是告诉优化器去优化谁,这里选择了模型 model 中的所有参数。
  • lr=learning_rate:设置学习率,即每次参数更新时的步进大小。

(4)确定每轮共需几步

total_step = len(train_loader)
  • len(train_loader) 返回加载器中的批次数量。
  • MNIST数据集中有 60000 张图片作为训练集。我们每次抓64张牌,那么一共要抓\(\frac{60000}{64}=937.5\),937.5 向上取整是 938 个批次。也就是说,由于数据加载器DataLoader的存在,LeNet5 把训练集所有图片遍历一遍要 938 步(step)。训练一轮(epoch)需要 938 步(step)。总结一下,一轮需要很多步,一步就是一个批次

7. 训练

我们将用 2 个 for 循环的嵌套来实现训练过程。

先让我们梳理一下应该如何训练。首先,训练分很多轮(epoch),每轮训练都需要把全部训练集过一遍。所以需要一个 for 循环,一轮一轮地进行循环。这个“过一遍”是通过数据加载器 DataLoader 实现的。其次,在一轮训练中,完整遍历一次训练集需要一个 for 循环,去循环从 DataLoader 加载过来的每个批次的图片(本例为64张图)。

在本例中,我设置共训练 10 轮。

# 设置一共训练几轮(epoch)
num_epochs = 10
# 外部循环用于遍历轮次
for epoch in range(num_epochs):
    # 内部循环用于遍历每轮中的所有批次
    for i, (images, labels) in enumerate(train_loader):  
        images = images.to(device)
        labels = labels.to(device)

        # 前向传播
        outputs = model(images)   # 通过模型进行前向传播,得到模型的预测结果 outputs
        loss = cost(outputs, labels)	# 计算模型预测与真实标签之间的损失

        # 反向传播和优化
        optimizer.zero_grad()	# 清零梯度,以便在下一次反向传播中不累积之前的梯度
        loss.backward()		# 进行反向传播,计算梯度
        optimizer.step()	# 根据梯度更新(优化)模型参数

        # 定期输出训练信息
        # 在每经过一定数量的批次后,输出当前训练轮次、总周轮数、当前批次、总批次数和损失值
        if (i+1) % 400 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
        		           .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
  • 在内部循环for i, (images, labels) in enumerate(train_loader):enumerate(train_loader) 返回一个可迭代的对象,其中包含每个批次的图像和标签。

  • images = images.to(device)labels = labels.to(device):将加载的图像和标签移动到设备(通常是 GPU),以便在设备上执行模型的前向和后向传播。

  • 对初学者而言,“定期输出训练信息”部分的代码用print输出即可,后期熟练了可以尝试用tqdm库显示进度条。这不是本文重点,有兴趣的可以自行了解。

定期输出训练信息
if (i+1) % 400 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))

8. 测试

with torch.no_grad():	# 指示 PyTorch 在接下来的代码块中不要计算梯度
    # 初始化计数器
    correct = 0		# 正确分类的样本数
    total = 0		# 总样本数

    # 遍历测试数据集的每个批次
    for images, labels in test_loader:
        # 将加载的图像和标签移动到设备(通常是 GPU)上
        images = images.to(device)
        labels = labels.to(device)

        # 模型预测
        outputs = model(images)

        # 计算准确率
        # 从模型输出中获取每个样本预测的类别
        _, predicted = torch.max(outputs.data, 1)
        # 累积总样本数
        total += labels.size(0)
        # 累积正确分类的样本数
        correct += (predicted == labels).sum().item()

    # 输出准确率,正确的 / 总的
    print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))
  • 在测试阶段,我们通常不需要计算梯度,以提高内存效率。
  • 注意看 _, predicted = torch.max(outputs.data, 1) 中的 torch.max(xxx, 1)表示对于每个输入 xxx 而言,torch.max 返回一个元组,其中包含两个张量,第一个张量是最大值,第二个张量是最大值所在的索引(也就是属于哪一类)。
    举个例子,在图像处理中,神经网络通常输出一个二维张量。什么样的二维张量呢?在3分类问题中输出二维张量:
    tensor([[0.1, 0.8, 0.3],
            [0.4, 0.2, 0.9],
            [0.7, 0.5, 0.2],
            [0.6, 0.2, 0.4]])
    
    上面的二维张量,由 4 个长度为 3 的一维张量构成。torch.max(xxx, 1)在每一行中寻找最大值。
    torch.max(xxx, 1)返回结果为元组:
    (tensor([0.8, 0.9, 0.7, 0.6]), tensor([1, 2, 0, 0]))
    
    其中,0.8, 0.9, 0.7, 0.6 分别为属于索引(类别)1, 2, 0, 0 的概率(或得分)。
  • _, predicted = torch.max(outputs.data, 1) 中的 _ 。它是一个通常用作占位符的变量名。在 Python 中,通常使用 _ 表示一个临时或不使用的变量。在本例中,我们不要属于索引(类别)的概率(或得分),只要最终结果,即属于哪个索引(类别),把它的值赋给predicted

以上,就是从0开始敲LeNet5的全部过程。

标签:LeNet5,框架,self,torch,PyTorch,训练,nn,out
From: https://www.cnblogs.com/xing9/p/17997507/LeNet5-PyTorch

相关文章

  • 在gin-gonic框架下,gin.context 输出json, 默认会将&转义为\u0026, 怎么将这个转义关
    在gin-gonic中,如果你想要禁止对&等字符进行转义,可以使用gin.Context的PureJSON方法。这个方法允许你自己控制JSON输出,而不会进行字符的转义。以下是一个简单的例子:packagemainimport( "github.com/gin-gonic/gin" "net/http")funcmain(){ router:=gin.Default() ......
  • IOS自动化测试框架appium
    由于公司的产品坐落于不同的平台,如ios、mac、Android、windows、web。因此每次有新需求的时候,开发结束后,留给测试的时间也不多。此外,一些新的功能实现,偶尔会影响其他的模块功能正常的使用。网上的ios自动化方面的内容也是少之又少。由于本人对ios自动化初次接触,花了两天......
  • android 框架搭建
    1.下载androidstdio工具:如下:![image.png](/img/bVc2Mvo)2.下载对应的SDKtools.最好下载SDKzip.访问地址:https://www.androiddevtools.cn/![image.png](/img/bVc2Mvp)3.选择SDKpath.将解压后的目录进行选择。删除原下载文件。![image.png](/img/bVc2Mvl), 4.下载工程导入,这......
  • Java微服务框架开发
    Swagger常见问题:Swagger与高版本SpringBoot不兼容问题  分析源码查找问题解决springboot2.6和swagger冲突的四种方法解决方法:按如下配置修改策略,如仍然不需,需按照上述四种方法第四种添加Beanspring:mvc:path-match:matching-strategy:ant_p......
  • 安卓之测试框架优劣分析
    一、引言安卓测试是确保移动应用质量的重要环节,它涉及多种测试类型和技术。有效的测试可以帮助开发者发现潜在问题,改善用户体验,并提高应用的稳定性和性能。本文将深入探讨安卓测试的分类、主要测试框架以及它们的优劣分析。二、测试的分类2.1、功能测试这是最基本的测试类型,旨在验......
  • 构建高效外卖系统:利用Spring Boot框架实现
    在当今快节奏的生活中,外卖系统已经成为人们生活中不可或缺的一部分。为了构建一个高效、可靠的外卖系统,我们可以利用SpringBoot框架来实现。本文将介绍如何利用SpringBoot框架构建一个简单但功能完善的外卖系统,并提供相关的技术代码示例。1.准备工作首先,确保你已经安装了Java开......
  • vue配合什么css框架
    在Vue中使用CSS框架可以提高开发效率和网站外观的一致性。下面是一些与Vue兼容的常见CSS框架:BootstrapVue:BootstrapVue是一个基于Vue.js的Bootstrap组件库。它提供了一套完整的Vue.js组件,同时也支持Bootstrap的所有特性。ElementUI:ElementUI是一套基于Vue.js的组件库......
  • 【Celery】异步任务框架入门使用
    背景.项目中需要用到后台数据爬取更新的功能,同步做起来web页面毫无用户体验可言。使用celery异步任务框架来解决这个问题简单、高效。用了一段时间比较稳定,现在有空梳理下文档。简介Celery是一个强大的分布式任务队列系统,它允许你将工作以异步的方式排队执行,这对于执行耗时......
  • Pytorch分布式训练,其他GPU进程占用GPU0的原因
    问题最近跑师兄21年的论文代码,代码里使用了Pytorch分布式训练,在单机8卡的情况下,运行代码,出现如下问题。也就是说GPU(1..7)上的进程占用了GPU0,这导致GPU0占的显存太多,以至于我的batchsize不能和原论文保持一致。解决方法我一点一点进行debug。首先,在数据加载部分,由于没有将lo......
  • Solon 框架启动为什么特别快?
    思来想去!可能与Solon容器的独立设计有一定关系。1、Solon注解容器的运行特点有什么注解要处理的(注解能力被规范成了四种),提前注册登记全局只扫描一次,并在扫描过程中统一处理注解相关扫描注入时,目标有即同步注入,没有时则订阅注入自动代理。即自动发现AOP需求,并按需动态代理......