【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)

笔记转载自:https://blog.csdn.net/m0_37867091/article/details/107136477
一定要搭配b站博主的视频一起食用啊!新手特别友好!!
https://www.bilibili.com/video/BV187411T7Ye

前言

最近在b站发现了一个非常好的 计算机视觉 + pytorch实战 的教程,相见恨晚,能让初学者少走很多弯路。
因此决定按着up给的教程路线:图像分类→目标检测→…一步步学习用 pytorch 实现深度学习在 cv 上的应用,并做笔记整理和总结。

up主教程给出了pytorch和tensorflow两个版本的实现,我暂时只记录pytorch版本的笔记。

参考内容来自:

up主的b站链接:https://space.bilibili.com/18161609/channel/index
up主将代码和ppt都放在了github:https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
up主的CSDN博客:https://blog.csdn.net/qq_37541097/article/details/103482003

此篇博文链接地址:https://www.bilibili.com/video/BV187411T7Ye

pytorch官方英文文档:https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#
pytorch官方中文文档:https://pytorch.apachecn.org/docs/1.4/blitz/cifar10_tutorial.html
pytorch中的卷积操作详解:https://blog.csdn.net/qq_37541097/article/details/102926037

demo的流程

  1. model.py ——定义LeNet网络模型
  2. train.py ——加载数据集并训练,训练集计算loss,测试集计算accuracy,保存训练好的网络参数
  3. predict.py——得到训练好的网络参数后,用自己找的图像进行分类测试

1. model.py

先给出代码,模型是基于LeNet做简单修改,层数很浅,容易理解:

# 使用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)    # 第一个参数代表输入特征矩阵的深度,16个卷积核,大小是5*5
        self.pool1 = nn.MaxPool2d(2, 2)     # 采用池化核大小2*2,步长stride=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

需注意:

  • ·pytorch 中 tensor(也就是输入输出层)的 通道排序为:[batch, channel, height, width]
  • ·pytorch中的卷积、池化、输入输出层中参数的含义与位置,可配合下图一起食用:

【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)_第1张图片

1.1 卷积 Conv2d

我们常用的卷积(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) 代表在上方补两行下方补一行,左边补两列,右边补一列。
经卷积后的输出层尺寸计算公式为:

在这里插入图片描述

  • 输入图片大小 W×W(一般情况下Width=Height)
  • Filter大小 F×F
  • 步长 S
  • padding的像素数 P
    若计算结果不为整数呢?参考 pytorch中的卷积操作详解

1.2 池化 MaxPool2d

最大池化(MaxPool2d)在 pytorch 中对应的函数是:

MaxPool2d(kernel_size, stride)

1.3 Tensor的展平:view()

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

  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)在 pytorch 中对应的函数是:

Linear(in_features, out_features, bias=True)

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
import numpy as np
import time

数据预处理

对输入的图像数据做预处理,即由shape (H x W x C) in the range [0, 255] → shape (C x H x W) in the range [0.0, 1.0]

  • transform是对图像进行预处理的函数,加所用的预处理方法打包成一个整体
    ToTensor函数是将一个PIL图像或者时numpy.ndarray的数据转化为tensor形式,将图片的格式顺序改为【深度、高度、宽度】
  • Normalize函数是一个标准化的函数,使用均值和标准差去标准化tensor
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

数据集介绍

利用***torchvision.datasets***函数可以在线导入pytorch中的数据集,包含一些常见的数据集如MNIST等
pytorch官方给了很多的数据集
【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)_第2张图片
此demo用的是CIFAR10数据集,也是一个很经典的图像分类数据集,由 Hinton 的学生 Alex Krizhevsky 和 Ilya Sutskever 整理的一个用于识别普适物体的小型数据集,一共包含 10 个类别的 RGB 彩色图片。
【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)_第3张图片

导入、加载 训练集

第一次运行时为True,下载数据集,下载完成后改为False
当train为True时,会导入训练集的样本
导入数据集后,通过transform这个预处理函数去预处理

# 导入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

导入、加载 测试集

test_loader将训练集分成一个批次一个批次的,每一批随机拿出36个,shuffle表示是否要将数据集进行打乱,num_workers为载入数据的线程数

将train设置成false,说明不导入训练集,而是导入测试集

test_data_iter 将刚才导入到的测试集数据转换成可迭代的迭代器,
通过next方法可获得一批数据,包括测试集的图像以及标签
classes对应的标签,用()括起来是元组类型,是不能改变的,第一个plane对应0,第二个1,以此类推

# 导入10000张测试图片
test_set = torchvision.datasets.CIFAR10(root='./data', 
										train=False,	# 表示是数据集中的测试集
                                        download=False,transform=transform)
# 加载测试集
test_loader = torch.utils.data.DataLoader(test_set, 
										  batch_size=10000, # 每批用于验证的样本数
										  shuffle=False, num_workers=0)
# 获取测试集中的图像和标签,用于accuracy计算
test_data_iter = iter(test_loader)
test_image, test_label = test_data_iter.next()

# classes = ('plane', 'car', 'bird', 'cat',
    #            'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

【补充】可以使用官方的imshow函数查看图片
前面的normalize对数据进行了标准化处理,下面代码中的img=img/2+0.5,对图像进行了反标准化处理,还原成原来的样子
再将原来的图像转化成numpy格式,将图像中表示顺序(C x H x W)还原成原来的图片表示顺序(H x W x C)
然后show展示图片【要将之前测试集中10000张图片的10000改为4,10000张图片没办法展示】
【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)_第4张图片
以上这个补充的代码只是为了看数据集中的图片,不用可以删掉了

2.2 训练过程

loss_function = nn.CrossEntropyLoss()
【为什么最后一层没包含softmax函数?】因为这个交叉熵损失函数已经包含了softmax函数

Adam优化器:传入的第一个参数是网络所需要训练的参数,learningrate学习率

  • for循环是个完整的训练过程
    • epoch表示训练集能迭代多少轮,range(5)表示训练集迭代5次
    • running_loss = 0.0用来累加训练过程中产生的一个损失
  • 第二个for循环为了遍历训练集样本
    • 使用enumerate函数返回每批数据对应的data,还会返回data对应的索引index或者step
    • optimizer.zero_grad() 将历史瞬时梯度清0,
      【其实batch_size设置越大,训练效果越好,但是由于硬件设备的限制,不能无限大,使用这个zero_grad() 函数可以实现很大的batch_size的训练】
    • 很重要with torch.no_grad(): # 在以下步骤中(验证过程中)不用计算每个节点的损失梯度,防止内存占用
    • torch.max(outputs, dim=1)[1] :寻找输出的最大的index在什么位置,可以理解为网络预测最可能归为哪个类别
      dim=1 表示是在维度1——channel上寻找最大值,因为第0个维度是batch
      [1] 表示只需要一个index值,即索引,并不需要具体的最大值是多少
    • (predict_y == test_label).sum().item() / test_label.size(0): 判等符号将真实标签与预测标签进行比较,truth记为1,否则记为0; 求和函数计算本次预测正确了多少个样本; 由于这些计算是以tensor格式计算的,并不是期望的一个数值,所以要使用item()函数拿到数值,再除以测试样本的数目就得到了准确率
    • 打印函数:epoch表示训练迭代到了第几轮,step表示某一轮的多少步,每500步打印一个训练的误差,准确率
    • 将running_loss清0,进行下500次的迭代过程
  • 最后保存模型save_path = ‘./Lenet.pth’
  • 还要保存模型的参数torch.save(net.state_dict(), save_path)
    【学习笔记】【深度学习】pytorch官方demo实现一个分类器(LeNet)_第5张图片
    以本demo为例,训练集一共有50000个样本,batch_size=50,那么完整的训练一次样本:iteration或step=1000,epoch=1
net = LeNet()						  				# 定义训练的网络模型
loss_function = nn.CrossEntropyLoss() 				# 定义损失函数为交叉熵损失函数 
optimizer = optim.Adam(net.parameters(), lr=0.001)  # 定义Adam优化器(训练参数,学习率)

for epoch in range(5):  # 一个epoch即对整个训练集进行一次训练
    running_loss = 0.0
    time_start = time.perf_counter()
    
    for step, data in enumerate(train_loader, start=0):   # 遍历训练集,step从0开始计算
        inputs, labels = data 	# 获取训练集的图像和标签
        optimizer.zero_grad()   # 清除历史梯度
        
        # forward + backward + optimize
        outputs = net(inputs)  				  # 将输入的图片传入网络,进行正向传播
        loss = loss_function(outputs, labels) # 计算损失,第一参数是网络的预测值,第二个对应的输入图片的真实标签
        loss.backward() 					  # 误差反向传播
        optimizer.step() 					  # 使用优化器更新参数

        # 打印耗时、损失、准确率等数据
        running_loss += loss.item()
        if step % 1000 == 999:    # print every 1000 mini-batches,每1000步打印一次
            with torch.no_grad(): # 在以下步骤中(验证过程中)不用计算每个节点的损失梯度,防止内存占用
                outputs = net(test_image) 				 # 测试集传入网络(test_batch_size=10000),output维度为[10000,10]
                predict_y = torch.max(outputs, dim=1)[1] # 以output中值最大位置对应的索引(标签)作为预测输出
                accuracy = (predict_y == test_label).sum().item() / test_label.size(0)
                
                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)

打印信息如下:

[1,  1000] train_loss: 1.537  test_accuracy: 0.541
35.345407 s
[2,  1000] train_loss: 1.198  test_accuracy: 0.605
40.532376 s
[3,  1000] train_loss: 1.048  test_accuracy: 0.641
44.144097 s
[4,  1000] train_loss: 0.954  test_accuracy: 0.647
41.313228 s
[5,  1000] train_loss: 0.882  test_accuracy: 0.662
41.860646 s
Finished Training

2.3 使用GPU/CPU训练

使用下面语句可以在有GPU时使用GPU,无GPU时使用CPU进行训练

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

也可以直接指定

device = torch.device("cuda")
# 或者
# device = torch.device("cpu")

对应的,需要用to()函数来将Tensor在CPU和GPU之间相互移动,分配到指定的device中计算

net = LeNet()
net.to(device) # 将网络分配到指定的device中
loss_function = nn.CrossEntropyLoss() 
optimizer = optim.Adam(net.parameters(), lr=0.001) 

for epoch in range(5): 

    running_loss = 0.0
    time_start = time.perf_counter()
    for step, data in enumerate(train_loader, start=0):
        inputs, labels = data
        optimizer.zero_grad()
        outputs = net(inputs.to(device))				  # 将inputs分配到指定的device中
        loss = loss_function(outputs, labels.to(device))  # 将labels分配到指定的device中
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        if step % 1000 == 999:    
            with torch.no_grad(): 
                outputs = net(test_image.to(device)) # 将test_image分配到指定的device中
                predict_y = torch.max(outputs, dim=1)[1]
                accuracy = (predict_y == test_label.to(device)).sum().item() / test_label.size(0) # 将test_label分配到指定的device中

                print('[%d, %5d] train_loss: %.3f  test_accuracy: %.3f' %
                      (epoch + 1, step + 1, running_loss / 1000, 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)

打印信息如下:

cuda
[1,  1000] train_loss: 1.569  test_accuracy: 0.527
18.727597 s
[2,  1000] train_loss: 1.235  test_accuracy: 0.595
17.367685 s
[3,  1000] train_loss: 1.076  test_accuracy: 0.623
17.654908 s
[4,  1000] train_loss: 0.984  test_accuracy: 0.639
17.861825 s
[5,  1000] train_loss: 0.917  test_accuracy: 0.649
17.733115 s
Finished Training

可以看到,用GPU训练时,速度提升明显,耗时缩小。

3. predict.py

调用模型权重进行预测

  • Resize((32, 32)预处理中加了一步,因为下载的图片大小不一定是标准的,而规定的输入图像的大小是32*32
  • 测试集要和训练集进行同样的标准化处理
  • 由于图片要有4个维度(batch,深度,高度,宽度),这里图片预处理后少一个batch的维度,
    所以要加一个维度im = torch.unsqueeze(im, dim=0) ,dim=0表示在最前面加一个新的维度
  • with torch.no_grad():不求瞬时梯度
  • print(classes[int(predict)]):将index这个索引传入回分类再打印输出,这样出来不是索引值,是分类值
  • 最后怎么使用softmax函数?——输出的是各种分类所对应的比例(softmax返回比率)
    在这里插入图片描述
# 导入包
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)])

输出即为预测的标签。

其实预测结果也可以用 softmax 表示,输出10个概率:

with torch.no_grad():
    outputs = net(im)
    predict = torch.softmax(outputs, dim=1)
print(predict)

输出结果中最大概率值对应的索引即为 预测标签 的索引。

tensor([[2.2782e-06, 2.1008e-07, 1.0098e-04, 9.5135e-05, 9.3220e-04, 2.1398e-04,
         3.2954e-08, 9.9865e-01, 2.8895e-08, 2.8820e-07]])

你可能感兴趣的:(深度学习,网络)