迁移学习:基于预训练ResNet网络的102种花卉分类任务(对唐宇迪课程的笔记和总结)<1>
一.必要准备和相关环境
本模型采用的深度学习框架为pytorch,以及辅助的部分python库,如用于绘图的matplotlib等
import json
import torch
import matplotlib.pyplot as plt
from torch import nn
%matplotlib inline
import numpy as np
from torchvision import datasets, transforms,models
import os
# 为了使图像出现在jupyter里,而不是出现在新窗口 %matplotlib inline
建议配备GPU,博主亲测在冻结了别人预训练模型参数,只训练自己定义的全连接层的情况下,即使使用GPU每次迭代也要三到四分钟(batch_size不同可能会有些区别,显存高的可以尝试batch_size搞大一点),最后25次迭代,博主用了一个多小时,cpu应该会翻倍,实在没gpu,用cpu跑只能减少训练次数意思一下了,最终在验证集上能取得80多一点的正确率,感觉还是可以的
二.数据集及对数据集的预处理
数据集可以在这边获取:
链接: https://pan.baidu.com/s/1Y6UGtJ69Ga08gU7uYToHwQ?pwd=ewx4 提取码: ewx4
首先我们先指定一下图像的路径
# 用于构建访问图片时的路径
data_dir = './flower_data/'
train_dir = data_dir + 'train'
valid_dir = data_dir + 'valid'
接下来我们就要正式对数据集中的图片进行预处理了
数据集长这样子
train文件夹里面的结构是这样的
每一个类别下是训练用的图片
验证集valid也是一样的结构,只是图片数量少一些
数据增强
整个数据集一共有几千个训练样本,和几百个验证样本,这对于通常依赖大力出奇迹的深度神经网络显然不够,于是我们可以使用数据增强来适当的增加训练样本数和测试样本数
打个简单的比方:
对于一只兔子图片,如果把他倒置,在我们看来他依旧是一个兔子,但是对于计算机而言,这两个图片是不同的,同样的,我们适当放大图片,然后截取和原来图片大小相同的一个区域,此时必然有一些区域会丢失,例如兔子的耳朵可能就没了,但是这张图片依旧是一个兔子,这对计算机而言也是值得学习的,通过类似的手法,我们可以把一张图片变成很多张图片,这样我们的训练样本和验证样本就会显著增多,训练效果也会明显提升.
那么具体在pytorch中如何数据增强呢
# 数据增强
# 什么是数据增强?
# 在数据不足时,为了更高效的利用数据,可以将图像进行翻转或者旋转,得到新的图像,也用于训练,对同一种图像利用更加高效
# 也可以将图像放大,将放大后图像截取和原来等大的一块用于训练,缩小原图像也是同理
# 制作好数据源
data_transforms = {
'train': transforms.Compose([transforms.Resize(256),
transforms.RandomRotation(45),# 随机旋转,-45度到45度之间随机选
# 由于拿到的图片有大有小,所以需要进行剪裁
transforms.CenterCrop(224), # 从中心开始剪裁,一般很多经典网络都用224x224的输入
transforms.RandomHorizontalFlip(p = 0.5),# 有0.5的概率会进行水平翻转
transforms.RandomVerticalFlip(p = 0.5), # 有0.5的概率会进行垂直翻转
transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),
# 调节图片的亮度,对比度,饱和度,色相
transforms.RandomGrayscale(p=0.025), # 有概率转化为灰度图,此时三个通道都相同R=B=G
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
# 别人通过imagenet计算的均值和标准差,为了在使用别人预训练的网络时效果更好,也要将我们的数据也按照别人imagenet上
# 的均值和标准差进行归一化,使得我们的数据也接近于别人在imagenet上的数据
# 以上都是对训练集的处理,下面处理验证集
'valid': transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
# 训练集怎么预处理的,验证集也必须同样的预处理
])
}
注释已经写得比较清楚了,下面我就简要说说
基本上首先就是调整尺寸,把原来大小不同的图片调整为统一的尺寸
transforms.Resize(256)
此外就是数据增强,可以进行水平或者垂直翻转,也可以设置明暗度,饱和度,也可以适当产生一些灰度图,概率方面,翻转的概率设置为0.5主要是为了让图片翻转得到的各种结果可能性基本相同
由于采用了迁移学习,主体的ResNet是直接用的model中封装好的,同时为了节省训练时间成本,我们直接使用别人在imageNet上训练好的参数,因此为了尽可能让我们的样本接近imageNet的样本,数据需要按照imageNet上数据的特点进行归一化.
值得注意的是,valid验证集必须和测试集做一样的剪裁处理和归一化处理
建立dataset和dataloader
# 构建训练用的数据集
batch_size = 8 # 设置mini-batch中每次处理的图像数
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir,x), transform=data_transforms[x])for x in ['train','valid']} # 首先要指定路径,再指定预处理方法
# 构建dataLoader
dataloaders = {x:torch.utils.data.DataLoader(image_datasets[x],batch_size=batch_size,shuffle=True) for x in ['train','valid']}
# 遍历train和valid分别传进去
dataset_sizes = {x:len(image_datasets[x]) for x in ['train','valid']} # 获得数据集的大小
class_names = image_datasets['train'].classes # 获取每个类别的代号
这里就是常规建立dataset的操作,和常规建立dataloader的操作
关于json文件的作用
json文件主要是记录每个类别代号对应的花的名字的,起到一个匹配的作用,以下是加载json文件的代码
with open('cat_to_name.json', 'r') as f: # 存储了每个类别对应代号对应的花的名字
cat_to_name = json.load(f)
预处理后的数据进行展示
代码如下:
# 如何展示预处理过后的数据
# 注意tensor的数据需要转换成numpy的格式,而且还需要还原回标准化的结果
def im_convert(tensor):
'''展示数据'''
image = tensor.to("cpu").clone().detach()
image = image.numpy().squeeze()
image = image.transpose((1, 2, 0)) # torch对通道有一个改变,需要还原
image = image * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406]) # 归一化的还原
image = image.clip(0, 1)
return image
fig = plt.figure(figsize=(20,12))
columns = 4
rows = 2 # 这个是有讲究的,batch_size = 8 所以行数乘以列数必须等于8
dataiter = iter(dataloaders['valid'])
inputs , classes = next(dataiter)
for idx in range(columns*rows):
ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])
ax.set_title(cat_to_name[str(int(class_names[classes[idx]]))])
plt.imshow(im_convert(inputs[idx]))
plt.show()
下面对注释没解释的一些可能不太好理解的点,我会简要的解释一下(这里视频教程讲的有些简略了)
(1)
image = tensor.to("cpu").clone().detach()
由于我们使用了GPU,pytorch在处理时是在GPU上进行的,所以当我们想展示这个mini_batch中到底抽到了哪几个图片时,需要先将张量从GPU转移到CPU上
tensor.to("cpu")
然后这里涉及到pytorch的一个特性,pytorch我们转移到cpu的张量其实和GPU上的还是同一个,只是添加了一个引用,对转移后的张量的操作会影响到GPU上的张量,因此我们需要创建一个副本,而不是简单的改引用,这里就涉及了clone操作
.clone()
在pytorch中对一个张量直接进行操作,他会认为我们是在构造计算图,所以需要将张量脱离计算图,告诉pytorch这个张量不需要梯度
.detach()
(2)
image = image.numpy().squeeze()
这里是把张量转换回numpy()形式的矩阵
.squeeze()
这个操作主要是为了去除只有一的维度,如果不加以去除,matplotlib可能就无法成功绘制图像
(4)
image = image.clip(0, 1)
clip(0,1)是为了将图像数据中每个像素点的值限制在0和1之间,确保像素值正确
这个博主并不是很理解,可能涉及了图像像素值的一些知识
(5)
fig = plt.figure(figsize=(20,12))
.figure(): 这是一个创建新图形窗口的函数。这个函数可以设置图形的大小、背景颜色等属性。
fig: 这是一个变量名,用来存储创建的图形对象的引用。你可以通过这个变量来访问和修改这个图形的属性。
figsize=(20,12): 这是figure()
函数的一个参数,用来设置图形窗口的大小。figsize
接受一个元组,表示图形的宽度和高度,单位是英寸。在这个例子中,(20,12)
表示创建一个宽度为20英寸、高度为12英寸的图形窗口。
总的来说,fig = plt.figure(figsize=(20,12))
这行代码的作用是创建一个新的图形窗口,并将这个窗口的大小设置为20英寸宽和12英寸高。这通常用于绘图前设置图形的大小,以便图形在显示或打印时具有合适的尺寸。
(6)
dataiter = iter(dataloaders['valid'])
inputs , classes = next(dataiter)
for idx in range(columns*rows):
ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])
ax.set_title(cat_to_name[str(int(class_names[classes[idx]]))])
plt.imshow(im_convert(inputs[idx]))
plt.show()
-
dataiter = iter(dataloaders['valid'])
:- 这行代码创建了一个迭代器
dataiter
,用于遍历dataloaders
字典中的'valid'
键对应的数据加载器。 dataloaders
是一个字典,通常包含了用于训练、验证和测试的数据加载器。iter()
函数将数据加载器转换成一个迭代器,使得可以通过next()
函数逐个获取数据批次。
- 这行代码创建了一个迭代器
-
inputs , classes = next(dataiter)
:- 这行代码使用
next()
函数从迭代器中获取下一个数据批次。 inputs
变量存储了这个批次的图像数据,通常是一个多维张量。classes
变量存储了对应图像的类别标签。
- 这行代码使用
-
接下来的
for
循环:for idx in range(columns*rows)
: 这个循环会迭代columns*rows
次,columns
和rows
定义了子图的列数和行数。
-
在循环内部:
-
ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])
:
- 这行代码为当前索引
idx
创建一个新的子图ax
,idx+1
是子图的序号。 xticks=[]
和yticks=[]
参数表示不显示x轴和y轴的刻度。
- 这行代码为当前索引
-
-
ax.set_title(cat_to_name[str(int(class_names[classes[idx]]))])
:- 这行代码设置子图的标题,使用
class_names
列表中对应的类别名称,cat_to_name
是一个将类别索引转换为可读名称的映射。
- 这行代码设置子图的标题,使用
-
plt.imshow(im_convert(inputs[idx]))
:- 这行代码使用
plt.imshow()
函数在当前子图ax
上显示图像。inputs[idx]
是当前索引的图像数据。 im_convert
是一个函数,用于将图像张量转换为可以显示的格式。这个函数可能涉及到将张量的像素值归一化、转换颜色通道等操作。
- 这行代码使用
-
plt.show()
:- 这行代码显示所有的子图。在循环结束后,所有的子图都会被绘制出来,然后显示给用户。
总结来说,这段代码的作用是从一个数据加载器中获取一批图像和它们的类别标签,然后在一个图形窗口中以子图的形式展示这些图像。每个子图都显示一张图像,并有相应的类别名称作为标题。最后,使用plt.show()
显示整个图形窗口。
上面的一些解释有的是博主自己查资料和个人理解,还有些是问的AI,个人感觉对于和博主一样的初学者,对于一些复杂不太理解的代码直接问AI,是一个很方便的做法,能够便于理解每句话的意思,查资料有的时候具体问题针对性不够强.
是时候展示一下我们预处理的结果啦
这篇博客是博主对自己这次学习的一个心得和体会,力求对绝大部分代码都进行较为详细的注释,一部分也是为了方便一些一样刚入门的伙伴,另一部分原因也是为了加深自己的理解,因此内容会非常长,所以博主会分成几篇文章来发布,希望能和大家共同进步,感谢您的阅读!