在讲解“不变性”时,我们曾详细地介绍过数据增强技术。数据增强是数据科学体系中常用的一种增加数据量的技术,它通过添加略微修改的现有数据、或从现有数据中重新合成新数据来增加数据量。使用数据增强技术可以极大程度地减弱数据量不足所带来的影响,还可以提升模型的鲁棒性、为模型提供各种“不变性”、增加模型抗过拟合的能力。常见的数据增强手段如下:
以及水平、垂直、镜面翻转:
在PyTorch当中,只要利用torchvision.transforms中包含的类,就能够很容易地实现几乎所有常见的增强手段。如下图所示,torchvision.transforms下的类可以分为四大类别:尺寸变化、像素值变化、视角变化以及其他变化。在能够让尺寸变化的类中,各类随机裁剪图像的类可以支持数据增强中的“缩放”功能(可放大,可缩小)。通常来说,如果裁剪是“随机”的,这个类一定是被用于数据增强,而不是被用于数据预处理的。这其中最常用的是transforms.RandomCrop() ,常常被放在transforms.Resize() 后面替代中心裁剪。
在负责像素变化的类中,色彩抖动一个类就包含了亮度、对比度、饱和度、色相四种控制颜色的关键指标。同样的,各类“随机”调整色彩或清晰度的类一定都是被用于数据增强的。调整颜色的类都需要输入相当多的参数,因此需要相当多传统视觉领域的知识,相比之下放射变换、线性变换这些基于数学逻辑的类更容易理解。同时,灰度图像上能够做的色彩调整很有限。因此在像素变化类别中,最常用的类除了归一化,就是 transforms.RandomAffine()。
这里是负责变形、透视、旋转等数据增强方法的类,被使用的频率相当高、每个类都很常用:
最后剩下的是用于转换格式、或将数据处理流程打包的类,其中最常用的就是
transforms.ToTensor() 。
我们可以选取其中任意的类来尝试一下:
data_val = torchvision.datasets.LSUN(root="/Users/zhucan/Desktop/lsun-master/data"
,classes=["church_outdoor_val","classroom_val"]
# ,transform = transforms.ToTensor()
)
#原图
data_val[350][0]
data_val = torchvision.datasets.LSUN(root="/Users/zhucan/Desktop/lsun-master/data"
,classes=["church_outdoor_val","classroom_val"]
,transform = transforms.ToTensor()
)
data_val[288][0].shape #尺寸一致 -> resize 256x256, RandomCrop 224x224
#torch.Size([3, 256, 358])
data_val[244][0].shape #尺寸一致 -> resize 256x256, RandomCrop 224x224
#torch.Size([3, 382, 256])
你可以尝试更换不同的数据增强方式,查看数据增强的各个 *** 作如何改变图像。在实际执行代码时,我们往往将数据增强和数据预处理的代码写在一起,如下所示:
transform_aug = transforms.Compose([transforms.Resize(256)
,transforms.RandomCrop(size=(224))
,transforms.RandomHorizontalFlip(p=1)
# ,transforms.ToTensor()
])
data_val_aug = torchvision.datasets.LSUN(root="/Users/zhucan/Desktop/lsun-master/data"
,classes=["church_outdoor_val","classroom_val"]
,transform = transform_aug
)
data_val[380][0]
#修改过后的图片
data_val_aug[380][0]
#对于景色数据,水平翻转和随机裁剪都可能会比较有利
#因为建筑可能位于图像的任何地方,而根据尝试,水平翻转后的图像也能够被一眼看出是什么景色
data_val_aug[250][0].shape
#torch.Size([3, 224, 224])
你可以尝试更换不同的数据增强方式,查看数据增强的各个 *** 作如何改变图像。在实际执行代码时,我们往往将数据增强和数据预处理的代码写在一起,如下所示:
#定义transform
transform = transforms.Compose([transforms.Resize(256) #先对尺寸进行 *** 作
,transforms.RandomCrop(size=(224))
,transforms.RandomHorizontalFlip(p=1) #再进行翻转、旋转等 *** 作
,transforms.RandomRotation(degrees=(-70,70))
,transforms.ToTensor() #对图片都处理完成后,转换为Tensor
,transforms.Normalize(mean=[0.485, 0.456, 0.406] #最后进行归一化
,std=[0.229, 0.224, 0.225])])
#导入数据
data_train = torchvision.datasets.LSUN(root="/Users/zhucan/Desktop/lsun-master/data"
,classes=["church_outdoor_train","classroom_train"]
,transform = transform)
data_train[0][0].shape
#torch.Size([3, 224, 224])
data_train[0][0]
# tensor([[[-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179],
# [-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179],
# [-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179],
# ...,
# [-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179],
# [-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179],
# [-2.1179, -2.1179, -2.1179, ..., -2.1179, -2.1179, -2.1179]],
# [[-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357],
# [-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357],
# [-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357],
# ...,
# [-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357],
# [-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357],
# [-2.0357, -2.0357, -2.0357, ..., -2.0357, -2.0357, -2.0357]],
# [[-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044],
# [-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044],
# [-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044],
# ...,
# [-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044],
# [-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044],
# [-1.8044, -1.8044, -1.8044, ..., -1.8044, -1.8044, -1.8044]]])
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import matplotlib.pyplot as plt
import random
import numpy as np
def plotsample(data):
fig, axs = plt.subplots(1,5,figsize=(10,10)) #建立子图
for i in range(5):
num = random.randint(0,len(data)-1) #首先选取随机数,随机选取五次
#抽取数据中对应的图像对象,make_grid函数可将任意格式的图像的通道数升为3,而不改变图像原始的数据
#而展示图像用的imshow函数最常见的输入格式也是3通道
npimg = torchvision.utils.make_grid(data[num][0]).numpy()
nplabel = data[num][1] #提取标签
#将图像由(3, weight, height)转化为(weight, height, 3),并放入imshow函数中读取
axs[i].imshow(np.transpose(npimg, (1, 2, 0)))
axs[i].set_title(nplabel) #给每个子图加上标签
axs[i].axis("off") #消除每个子图的坐标轴
#可以自行修改plotsample函数,为可视化实现更高的自由度
plotsample(data_train)
现在你知道如何应用数据增强和数据预处理的相关功能了。使用这些代码并不难,真正难的地方在于看透代码底层实现的实际 *** 作。在这段代码中,有一个非常容易被忽略、但一旦注意到之后却百思不得其解的问题:数据增强是增加数据量的技术,而上面的 *** 作哪里增加数据量了呢?(甚至有许多对PyTorch很熟悉的人都并不知道这个问题的答案,可见跑代码是多么地简单)。
如果我们更换不同的数据集、不同的数据增强方式,很快就会发现,除了明确标明会生成多张裁剪图片的 FiveCrop 以及 TenCrop 两个类,其他的类都不能改变数据集中的数据总量。看上去这些进行数据增强的类只是对数据集进行了一个整体的转化而已,并没有真正实现“数据增强”。但深度学习框架在设计上总是非常巧妙的设计。虽然代码上无法直接看出来,但当我们将使用transform处理过的数据放入训练过程时,数据增强就会被实现。我们来看看具体是怎么回事。
回顾我们构建的CustomDataset类:
我们在构建任意继承自Dataset类的、用于读取和构建数据的类CustomDataset时,我们将transform的使用流程写在了 __ getitem__() 中,而没有放在 __ init__() 中,因此CustomDataset被运行时并不会自动对数据执行transform中的 *** 作。相对的,由于继承自Dataset类,CustomDataset会将数据进行读取,并将源数据本身放入内存。当我们通过 __ getitem__() 方法调用数据集中的任意数据时,CustomDataset会从已经储存好的原始数据中,复制出我们希望调用的那些样本,同时激活transform的相关流程,将调用的数据进行transform处理。在这种情况下,返回到我们面前的是从原始数据中复制出的样本经过transform处理后的样子,储存在内存中的原始数据并没有被改变。
事实上,任何torchvision.datasets下用于提取数据或处理数据的类都遵守这一原则:只保存原始数据,仅在调用数据时才对数据进行transform处理,这既有利于节省内存空间(只需要保存一份数据),也有利于计算速度(只对需要使用的样本才进行处理)。一般数据被读取后,可能经过分训练集测试集的sample_split,分批次的DataLoader,但他们都不会触发transform。因此当我们读取数据、分割数据时,没有任何预处理或者数据增强的 *** 作被执行。那数据增强什么时候被执行呢?——当我们从分割好的batch_size中提取出数据进行训练时。
这是我们在Lesson 11中用于训练的代码,这段代码有些简陋,但依然能够展现出我们训练时的基本流程。训练时,我们会一个epoch一个epoch地进行循环,并且在每个epochs中循环所有批次。每次当batchdata中的x和y被调用来训练时,transform就会作用于该批次中所有被提取出的样本。也就是说,在红色箭头指向处,每个批次中的样本都会经过随机裁剪、随机旋转等图像增强 *** 作。
在没有transform的时候,全部数据被分割为不同批次,因此一个epoch中每个批次的数据是不同的,但全部批次组成的这个epoch的数据都是一致的,分割批次只不过改变了样本的训练顺序,并没有增加新的样本,而循环epochs只是在原有的数据上不断进行学习。在存在transform之后,尽管每个批次中的原始数据是一致的,但在每次被调用时,这些数据都被加上了随机裁剪、随机旋转、随机水平翻转等 *** 作,因此每个批次中的样本都变成了“新样本”,这让整个被训练的epoch都与之前的epoch产生了差异。因此在transform存在时,无论我们循环多少epochs,每个epochs都是独一无二的,这就相当于增加了数据量,实现了“数据增强”。相对的,当transform存在时,我们的模型一次也不会见到原始的数据。
这种做法非常巧妙,并且非常节约内存。每次训练时,我们只需要保留原始数据,每个batch训练结束之后,被transform处理过的数据就可以被释放。并且,由于全部batch中的样本加起来一定是小于等于一个epochs中的样本量,所以调用batch时才进行transform *** 作和一次性对全部数据进行transform *** 作的计算量理论上没有太大差异。相对的,如果我们一次性对全部数据进行transform *** 作,也只能得到一组和原始数据不同的数据,但每次在调用batch时进行 *** 作,却可以创造出和循环次数N一样多的N组新数据,实现了在不增大训练负担的情况下、增加样本量。总之,这是一本万利的做法。
当然啦,数据增强并非只有好处。根据经验,大多数时候,如果存在数据增强 *** 作,模型的迭代周期会更长,毕竟数据中存在大量的随机性,模型收敛得会更慢。但这样得到的模型的鲁棒性和泛化能力都会更强。另一个显而易见的缺点是,数据增强中的随机性无法使用随机数种子进行控制。如果使用随机性种子进行控制,那每次进行的随机 *** 作就会是一致的,每个epochs就会一致,这就和一次性对数据进行处理后再带入训练没有区别,这会让数据增强 *** 作失去意义。但无法控制的随机性可能意味着模型的效果会略为不稳定,对写论文或上线之前进行测试的代码来说,每次运行都得出迥然不同的结果显然是很令人头疼的。因此使用数据增强的模型往往只能够得到一个“结果的范围”,论文中报告的往往是这个范围的上限。
现在你了解数据增强是怎样被实现的了,在自己的数据上尝试做做看吧。到此我们对数据的说明就结束了,下一节我们将会开始详谈模型的训练流程。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)