LeNet学习笔记(官方demo)

参考来自两位博主的博客:

深度学习在图像处理中的应用(tensorflow2.4以及pytorch1.10实现)-CSDN博客

pytorch图像分类篇:2.pytorch官方demo实现一个分类器(LeNet)-CSDN博客


1.model.py笔记 

1.1需要注意的一些知识点: 

pytorch 中 tensor(也就是输入输出层)的 通道排序为:

[batch, channel, height, width]
  • 数据布局由二维图像的四个字母表示:
    • N:Batch,批处理大小,表示一个batch中的图像数量
    • C:Channel,通道数,表示一张图像中的通道数
    • H:Height,高度,表示图像垂直维度的像素数
    • W:Width,宽度,表示图像水平维度的像素数

我们常用的卷积(Conv2d)在pytorch中对应的函数是:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')

其中:

  • in_channels:输入特征矩阵的深度。如输入一张RGB彩色图像in_channels=3
  • out_channels:输入特征矩阵的深度。也等于卷积核的个数,使用n个卷积核输出的特征矩阵深度就是n。
  • kernel_size:卷积核的尺寸。可以是int类型,如3 代表卷积核的height=width=3,也可以是tuple类型如(3, 5)代表卷积核的height=3,width=5
  • stride:卷积核的步长。默认为1,和kernel_size一样输入可以是int型,也可以是tuple类型
  • padding:补零操作,默认为0。可以为int型如1即补一圈0,如果输入为tuple型如(2, 1) 代表在上下补2行,左右补1列。

模块定义:

定义一个神经网络模型的一般方法是创建一个类(class),并继承自torch.nn.Module这个基类。这个类需要实现两个特殊的方法(method),分别是__init__(self)forward(self, x)

self是一个指代类实例(instance)本身的参数,它是Python中类方法的一个约定俗成的命名。当我们创建一个类的实例时,例如model = Model(),Python会自动将model作为第一个参数传递给类的方法,也就是self。因此,self可以访问类的属性(attribute)和方法,例如self.conv1或self.apply()。

__init__(self)方法中,我们一般用self来定义和注册模型的子模块(submodule),例如self.conv1 = nn.Conv2d(…)。在forward(self, x)方法中,我们一般用self来调用模型的子模块,例如x = self.conv1(x)。

x是一个表示模型的输入数据的参数,它可以是一个张量(tensor)或者一个元组(tuple)等任何类型。在forward(self, x)方法中,我们需要定义模型的前向传播(forward propagation)的逻辑,也就是如何根据输入x计算输出y。例如,y = self.fc(x)。

1.2池化(MaxPool2d):

池化层的目的就是对特征图进行稀疏处理,减少数据运算量。

特点:

  • 没有训练参数,只是在原始的特征图上进行求最大值或者平均值的操作;
  • 它只会改变特征矩阵的宽度(w)和高度(h),并不会改变深度(channel);
  • 一般池化核的大小(poolsize)和步长(stride)相同,可以将特征图进行一定比例的缩小,计算更加方便(这只是一般情况下,但并不绝对)
MaxPool2d(kernel_size, stride)

 1.3展平(view函数):

在经过第二个池化层后,数据还是一个三维的Tensor (32, 5, 5),需要先经过展平后(32*5*5)再传到全连接层:

  x = self.pool2(x)            # output(32, 5, 5)
  x = x.view(-1, 32*5*5)       # output(32*5*5)
  x = F.relu(self.fc1(x))      # output(120)

1.4全连接Linear:

Linear(in_features, out_features, bias=True)

代码如下: 

# 使用torch.nn包来构建神经网络.
import torch.nn as nn
import torch.nn.functional as F

class LeNet(nn.Module): 					# 继承于nn.Module这个父类
    def __init__(self):						# 初始化网络结构
        super(LeNet, self).__init__()    	# 多继承需用到super函数
        self.conv1 = nn.Conv2d(3, 16, 5)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(16, 32, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):			 # 正向传播过程
        x = F.relu(self.conv1(x))    # input(3, 32, 32) output(16, 28, 28)
        x = self.pool1(x)            # output(16, 14, 14)
        x = F.relu(self.conv2(x))    # output(32, 10, 10)
        x = self.pool2(x)            # output(32, 5, 5)
        x = x.view(-1, 32*5*5)       # output(32*5*5)
        x = F.relu(self.fc1(x))      # output(120)
        x = F.relu(self.fc2(x))      # output(84)
        x = self.fc3(x)              # output(10)
        return x

一般卷积层后的输出公式为:Output=(W−F+2P)/S​+1,Output为输出图片尺寸大小。

其中:

  • 输入图片大小 W×W(一般情况下Width=Height)
  • Filter大小 F×F
  • 步长 S
  • padding的像素数 P

1.5理解数据传输:

input(3,32,32)为3通道32*32大小的输入图像;

①经过第一个卷积层self.conv1 = nn.Conv2d(3, 16, 5),表示该层是一个3通道,16个卷积核,卷积核大小为5的层,经公式Output=(W−F+2P)/S​+1计算输出为Output=(32-5+2*0)/1+1=28,16个卷积核表示下一层图片的通道数,28为输出图片的大小,那么输出为output(16, 28, 28)。如果未给出具体代码形式,则表示补零Padding的P默认为0,表示步长的S默认为1。

②经过第一个池化层self.pool1 = nn.MaxPool2d(2, 2),表示该层是一个卷积核大小为2,步长为2的层,经公式Output=(W−F+2P)/S​+1计算输出为Output=(28-2+2*0)/2+1=14,那么下一层的输出为output(16,14,14)。

③经过第二个卷积层self.conv2 = nn.Conv2d(16, 32, 5),表示该层是一个16通道,32个卷积核,卷积核大小为5的层。上一层输出为output(16,14,14),那么这一卷积层必须有相同的通道数,即16,而这一层有32个卷积核,则下一层通道数须为32。经公式Output=(W−F+2P)/S​+1计算输出为Output=(14-5+2*0)/1+1=10,那么输出为output(32,10,10)。

④经过第二个池化层self.pool2 = nn.MaxPool2d(2, 2),表示该层是一个卷积核大小为2,步长为2 的层 ,经公式Output=(W−F+2P)/S​+1计算输出为Output=(10-2+2*0)/2+1=5,那么输出为output(32,5,5)。

⑤池化层到全连接层需要展平操作:x = x.view(-1, 32*5*5),view操作是PyTorch中用于改变张量形状的方法,也被称为reshape。在这里,-1的意思是根据张量的大小自动推断该维度的大小。如果x是一个张量,而35*5*5是目标形状中的一个维度,“-1”的作用就是自动计算该维度大小,确保整体的元素个数不变。展平操作一般用于卷积神经网络中的卷积层之后,将卷积层的输出展平为一个一维向量,以便输入到全连接层(全连接层接受一维向量作为输入)。

⑥第一个全连接层self.fc1 = nn.Linear(32*5*5, 120),输入长度为800的向量,输出为120.

⑦第二个全连接层self.fc2 = nn.Linear(120, 84),输入长度为120的向量,输出为84.

⑧第三个全连接层self.fc3 = nn.Linear(84, 10),输入长度为84,输出为10。对应10个类别。

2.train.py笔记

2.1导入主要的包(有一些是非必要的)

import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
import matplotlib.pyplot as plt  # 导入绘制图像的matplotlib包
import numpy as np
import time

from model import LeNet是一行Python代码,它的作用是从model这个模块(module)中导入(import)LeNet这个类(class)。模块是一种组织和复用代码的方式,它可以包含变量(variable)、函数(function)和类等定义。类是一种创建自定义数据类型的方式,它可以包含属性(attribute)和方法(method)等特征。

2.2定义主函数

def main():

2.2.1数据预处理

transform = transforms.Compose(
        [transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

 这段代码是使用PyTorch框架进行图像处理的一种常见方式,它的作用是定义一个变换(transform)对象,用于将图像数据转换为张量(tensor)并进行归一化(normalize)操作。

transforms是PyTorch中的一个模块,它提供了一些常用的图像变换函数,例如裁剪(crop)、旋转(rotate)、缩放(resize)等。transforms.Compose是一个类,它可以将多个变换函数组合在一起,按照顺序依次对图像数据进行处理。它的参数是一个列表,列表中的每个元素都是一个变换函数,上面代码列表中有两个变换函数。

transforms.ToTensor是一个函数,它可以将一个PIL库的图像或者numpy数组转换为PyTorch的张量,同时将图像的像素值范围从[0, 255]缩放到[0, 1]之间。这样做的好处是可以方便地进行数学运算和梯度计算。

transforms.Normalize是一个函数,它可以对张量进行归一化操作,使得每个通道的数据都符合标准正态分布。它的参数是两个元组,分别表示每个通道的均值(mean)和标准差(std)。

归一化的公式是:output[channel] = (input[channel] - mean[channel]) / std[channel]

这样做的好处是可以消除数据的量纲(scale)和偏移(shift),提高模型的收敛速度和泛化能力。综上所述,这段代码的意思是创建一个变换对象,它可以将图像数据转换为张量,并且使得每个通道的数据都在[-1, 1]之间,符合标准正态分布。这个变换对象可以用于图像数据的加载和预处理。

2.2.2导入数据集

利用torchvision.datasets函数可以在线导入pytorch中的数据集,包含一些常见的数据集。

此demo用的是CIFAR10数据集,是一个用于识别普适物体的小型数据集,一共包含 10 个类别的 RGB 彩色图片。

导入训练集和验证集: 

# 导入50000张训练图片
train_set = torchvision.datasets.CIFAR10(root='./data', 	 # 数据集存放目录
										 train=True,		 # 表示是数据集中的训练集
                                        download=True,  	 # 第一次运行时为True,下载数据集,下载完成后改为False
                                        transform=transform) # 预处理过程
# 加载训练集,实际过程需要分批次(batch)训练                                        
train_loader = torch.utils.data.DataLoader(train_set, 	  # 导入的训练集
										   batch_size=50, # 每批训练的样本数
                                          shuffle=False,  # 是否打乱训练集
                                          num_workers=0)  # 使用线程数,在windows下设置为0
    # 10000张验证图片
    # 第一次使用时要将download设置为True才会自动去下载数据集
    val_set = torchvision.datasets.CIFAR10(root='./data', train=False,# 表示是数据集中的测试集
                                           download=False, transform=transform)
    val_loader = torch.utils.data.DataLoader(val_set, batch_size=10000,# 每批用于验证的样本数
                                             shuffle=False, num_workers=0)
    # 获取测试集中的图像和标签,用于accuracy计算
    val_data_iter = iter(val_loader)
    val_image, val_label = next(val_data_iter)

 shuffle=False是一个参数,它表示是否在每个epoch(训练周期)中对数据进行随机打乱。如果设置为True,那么每个epoch中,数据的顺序都会被重新洗牌,这样可以增加数据的多样性,防止模型过拟合。如果设置为False,那么每个epoch中,数据的顺序都保持不变,这样可以保证数据的一致性,方便比较不同模型的性能。在PyTorch中,shuffle参数通常用在DataLoader类中,用于创建数据加载器对象。

torchvision.datasets.CIFAR10是一个Python模块,它可以用来加载和处理CIFAR10这个图像数据集。CIFAR10是一个包含10个类别的60000张32x32彩色图像的数据集,它可以用于图像分类等深度学习应用。torchvision.datasets.CIFAR10模块提供了一个CIFAR10类,它可以从网上下载数据集,并且对图像进行变换和归一化等操作。可以使用这个模块来创建一个数据加载器(data loader)对象,用于批量地提供数据给神经网络模型。

torch.utils.data.DataLoader是PyTorch框架中的一个类,它的作用是从一个数据集(dataset)对象中加载和处理数据样本。它可以支持多种类型的数据集,例如图像、文本、音频等,也可以自定义数据集的变换和归一化等操作。它还可以实现数据的批量(batching)、打乱(shuffling)、多进程(multiprocessing)等功能,提高数据加载的效率和灵活性。可以使用这个类来创建一个数据加载器(data loader)对象,用于向神经网络模型提供数据。

torch.utils是一个模块,它包含了一些常用的工具类和函数,例如数据加载、模型保存、分布式训练等。torch.utils.data是torch.utils模块的一个子模块,它专门用于处理数据相关的问题,例如数据集、数据加载器、数据采样器等。

后两行代码是使用PyTorch框架进行数据加载和处理的一种常见方式,它的作用是从一个数据加载器(data loader)对象中获取一个批次(batch)的数据样本,包括图像和标签。

val_loader是一个数据加载器对象,它是通过torch.utils.data.DataLoader类创建的,用于从一个数据集(dataset)对象中加载和处理验证(validation)数据。在创建val_loader时可以指定一些参数,例如batch_size(每个批次的样本数量)、shuffle(是否在每个epoch中对数据进行随机打乱)等。

iter是一个Python内置函数,它的作用是从一个可迭代对象(iterable)中创建一个迭代器(iterator)对象。一个迭代器对象可以一次返回一个元素,直到没有元素为止。val_data_iter = iter(val_loader)这行代码表示从val_loader这个数据加载器对象中创建一个迭代器对象,用于逐个获取批次数据。

next是一个Python内置函数,它的作用是从一个迭代器对象中返回下一个元素。如果没有元素了,就会抛出StopIteration异常。val_image, val_label = next(val_data_iter)这行代码表示从val_data_iter这个迭代器对象中获取下一个批次的数据,并且将其分别赋值给val_image和val_label这两个变量。val_image是一个张量(tensor),它的形状是(batch_size, 3, 32, 32),表示一个批次中的图像数据,每张图像有3个通道,高度和宽度都是32。val_label是一个张量,它的形状是(batch_size,),表示一个批次中的图像标签,每个标签是一个整数,表示图像属于哪个类别。

2.2.3训练过程

epoch对训练集的全部数据进行一次完整的训练,称为 一次 epoch
batch由于硬件算力有限,实际训练时将训练集分成多个批次训练,每批数据的大小为 batch_size
iteration 或 step对一个batch的数据训练的过程称为 一个 iteration 或 step

net = LeNet()  # 实际化模型
    loss_function = nn.CrossEntropyLoss()  # 定义一个交叉熵损失函数,用于计算模型输出和实际标签之间的损失。
    optimizer = optim.Adam(net.parameters(), lr=0.001)  # 定义一个Adam优化器,用于调整模型参数以最小化损失函数。学习率为0.001。

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    net = net.to(device)    #本例使用GPU
    time_start = time.perf_counter()
    for epoch in range(5):  # loop over the dataset multiple times 将训练集迭代5次

        running_loss = 0.0  # 累加训练过程中的损失
        for step, data in enumerate(train_loader, start=0):  # 遍历训练集样本
            #这是一个 Python 中用于循环遍历训练数据集的语句,其中 train_loader 是一个数据加载器对象,可以用于批量加载训练数据。
            #enumerate() 函数用于将数据集中的每个数据与其对应的索引一一对应起来,start=0 表示索引从 0 开始。这样,每次循环时,step 就是当前数据的索引,data 就是当前数据的内容。     
             # get the inputs; data is a list of [inputs, labels]
            inputs, labels = data  # 数据分离成输入与标签
            inputs, labels = inputs.to(device), labels.to(device); '''百度经验demo:train_cpu--→train_gpu'''

            # zero the parameter gradients
            optimizer.zero_grad()  # 将历史损失梯度清零 实现大batch数值的训练
            # forward + backward + optimize
            outputs = net(inputs)  # 输入到网络的输入图片正向传播,得到输出
            loss = loss_function(outputs, labels)  # 计算模型输出和实际标签之间的损失,1st参数维预测值 2nd参数为标签
            loss.backward()  #反向传播loss
            optimizer.step()  # 优化器参数更新

            # print statistics,打印耗时、损失、准确率等数据
            running_loss += loss.item()  # 累加计算的loss
            if step % 500 == 499:    # print every 500 mini-batches 每隔500步打印一次数据的信息
            #step % 500:得到当前迭代步数除以500的余数,== 499:检查余数是否等于499。当step的值是500的整数倍减1时,执行紧随其后的代码块。
                with torch.no_grad():  # with:上下文管理器,在以下步骤中(验证过程中)不用计算每个节点的损失梯度,防止内存占用
                    outputs = net(val_image)  # [batch, 10] 正向传播
                    predict_y = torch.max(outputs, dim=1)[1]  # 查找传输的最大index在什么位置/net预测最可能的类别
                    accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)  # item()拿到tensor里的数值 求准确率

                    print('[%d, %5d] train_loss: %.3f  test_accuracy: %.3f' %   # 打印epoch,step,loss,accuracy
                          (epoch + 1, step + 1, running_loss / 500, accuracy))
                    print('%f s' % (time.perf_counter() - time_start))  # 打印耗时
                    running_loss = 0.0

    print('Finished Training')

    save_path = './Lenet.pth'
    torch.save(net.state_dict(), save_path)  # net参数保存

2.3判断是否直接调用

if __name__ == '__main__':
    main()

用于判断当前模块是被直接运行还是被导入到其他模块中。如果是被直接运行,那么__name__变量的值就是’main’,如果是被导入,那么__name__变量的值就是模块的名称。因此,if__name__ == '__main__’这个条件就可以用来区分不同的情况,执行不同的代码。

main()是一个自定义的函数,通常用来封装当前模块的主要逻辑,例如测试代码、示例代码或者程序入口等。将这个函数放在if__name__ == '__main__’的下面,就意味着只有当当前模块被直接运行时,才会调用这个函数,而当当前模块被导入到其他模块时,就不会调用这个函数,从而避免了不必要的代码执行。

3.predict.py 

# 导入包
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet

# 数据预处理
transform = transforms.Compose(
    [transforms.Resize((32, 32)), # 首先需resize成跟训练集图像一样的大小
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 导入要测试的图像(自己找的,不在数据集中),放在源文件目录下
im = Image.open('horse.jpg')
im = transform(im)  # [C, H, W]
im = torch.unsqueeze(im, dim=0)  # 对数据增加一个新维度,因为tensor的参数是[batch, channel, height, width] 

# 实例化网络,加载训练好的模型参数
net = LeNet()
net.load_state_dict(torch.load('Lenet.pth'))

# 预测
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
with torch.no_grad():
    outputs = net(im)
    predict = torch.max(outputs, dim=1)[1].data.numpy()
print(classes[int(predict)])

可以自己找一些图片试试。

LeNet-5神经网络 C源代码,这个写的比较好,可以用gcc编译去跑,结合理论可以对深度学习有更深刻的了解 介绍 根据YANN LECUN的论文《Gradient-based Learning Applied To Document Recognition》设计的LeNet-5神经网络,C语言写成,不依赖任何第三方库。 MNIST手写字符集初代训练识别率97%,多代训练识别率98%。 DEMO main.c文件为MNIST数据集的识别DEMO,直接编译即可运行,训练集60000张,测试集10000张。 项目环境 该项目为VISUAL STUDIO 2015项目,用VISUAL STUDIO 2015 UPDATE1及以上直接打开即可编译。采用ANSI C编写,因此源码无须修改即可在其它平台上编译。 如果因缺少openmp无法编译,请将lenet.c中的#include和#pragma omp parallel for删除掉即可。 API #####批量训练 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 inputs: 要训练的多个图片对应unsigned char二维数组的数组,指向的二维数组的batchSize倍大小内存空间指针。在MNIST测试DEMO中二维数组为28x28,每个二维数组数值分别为对应位置图像像素灰度值 resMat:结果向量矩阵 labels:要训练的多个图片分别对应的标签数组。大小为batchSize batchSize:批量训练输入图像(二维数组)的数量 void TrainBatch(LeNet5 *lenet, image *inputs, const char(*resMat)[OUTPUT],uint8 *labels, int batchSize); #####单个训练 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 input: 要训练的图片对应二维数组 resMat:结果向量矩阵 label: 要训练的图片对应的标签 void Train(LeNet5 *lenet, image input, const char(*resMat)[OUTPUT],uint8 label); #####预测 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 input: 输入的图像的数据 labels: 结果向量矩阵指针 count: 结果向量个数 return 返回值为预测的结果 int Predict(LeNet5 *lenet, image input, const char(*labels)[LAYER6], int count); #####初始化 lenet: LeNet5的权值的指针,LeNet5神经网络的核心
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值