参考来自两位博主的博客:
深度学习在图像处理中的应用(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)])
可以自己找一些图片试试。