【pytorch图像视觉】lesson17(上)数据篇:认识经典数据+使用自己的数据/图片创造数据集+图片数据的基本与处理与数据增强

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

内容来源于【菜菜&九天深度学习实战课程】上班以后时间少,为了提高效率,通过抄课件的方式加深记忆,而非听课。所以所有笔记仅仅是学习工具,并非抄袭!


在传统机器学习中,通常会区分有监督、无监督、分类、回归、聚类等人物类别,在不同的任务重会指向不同形式的标签、不同的评估指标、不同的损失函数,这些内容会影响我们的训练和建模流程。在深度视觉以外,除了区分“回归、分类”之外,还需要区分众多的、视觉应用类别。如果只考虑图像的内容,至少有常见的三种任务:识别(recognition) 、检测(detection)、分割(segmentation)。
在这里插入图片描述
图像识别以图像中的单一对象为核心,采集信息并作出判断,任何超过单一对象的任务都不是单纯的图像识别。用于图像识别的数据集往往比较规整、比较简单,被识别物体基本轮廓完整、拍摄清晰、是图像中最容易被人眼注意到的对象。因此,图像识别适用于机场人脸识别这种简单的应用场景。

检测任务和分割任务是针对图像中多个或者单个对象进行判断的任务。因此分割和检测所使用的图像往往复杂很多。在检测任务重,首先要使用边界框(Bounding Box, bbx)对图像中的多个对象所在位置进行判断,在一个边界框容纳一个对象的前提下,再对边界框内每一个单一对象进行图像识别。因此。

检测任务的标签有两个:(1)以坐标方式确定的边界框的位置;(2)每个边界框中的物体属性。检测任务的训练流程也氛围两步:(1)定位;(2)判断。检测任务的训练数据也都是带边框的。我们可以只使用其中一个标签进行训练,但是检测任务同时训练两个标签才是最常见的情况。检测任务比较适用于大规模动态影像的识别,比如识别道路车辆、识别景区人群等。检测也是现在实际应用最为广泛的视觉任务。
在这里插入图片描述

分割任务是像素级别的密集任务,需要对图像中的每个像素进行分类,因此不需要定义边界框就能够找到每个对象的“精准边界”。分割任务的标签往往只有一个,通常是对象的定义或性质(比如,这个像素是猫,这个像素是蓝天),但是标签中的类别会非常对,对于复杂的图像,标签类别可能成百上千。分割任务是现在图像领域对“理解图像”探索的前沿部分,许多具体的困难还未解决,同时,在许多实际应用场景并不需要分割任务这么”精准“的判断,因此分割的实际落地场景并不如检测来的多。比较知名的实际落地场景是美颜相机、抖音换脸特效等。

综上所述,三种任务在输出的结果及标签有所不同,但它们在一定程度上共享训练数据集,这主要是因为图像中的“对象”概念是可以人为定义的。用于图像识别的训练数据只要有适当的标签,也可以被用来检测和分割。例如,对于只有一张人脸的图像,只要训练数据存在边界框,那标签中就可以进行检测。相对的,用于检测和分割的数据如果含有大量的对象,可以被标注为“人群”这样的标签来进行识别(不过,用于检测、分割的数据拥有可以作为识别数据的标签非常少,因为检测分割数据集往往是多对象的)。

在这里插入图片描述

在单个任务重,也可能会遇到不同的“标签”。例如,对人脸数据,我们可以进行“属性识别”(attribute recognition)、“个体识别”(identity recognition)、“情绪识别”(emotion recognition)等不同的任务,对于同一张图像,我们的识别结果可能完全不同。

在这里插入图片描述
在情绪识别中,我们只拥有“情绪”这一标签,但标签类别中包含不同的情绪。在属性识别中,我们可以执行属性有限的多分类任务,也可以让每一种属性都有一个单独的标签,针对一个标签来完成二分类任务。而个体识别则是经典的人脸识别,在CelebA数据中我们使用人名作为标签来进行判断。

同样的,在场景识别、物体识别数据下(比如大规模场景识别数据LSUN),我们可能无法使用全部的数据集,因为全数据集可能会非常巨大并且包含许多我们不需要的信息。在这种情况下,标签可能是分层的,例如,场景可能分为室内和室外两种,而室内又分为卧式、客厅、厨房,室外则分为自然精光、教堂、其他建筑等,在这种情况下,室内和室外就是“上层标签”,具体的房间或景色则被认为是“下层标签”。我们通常会选择某个下层标签下的数据进行学习,例如:在LSUN中选择“教堂”或“卧室”标签进行学习:

在这里插入图片描述

对检测任务,我们也可能会检测不同的对象,例如:检测车牌号和检测车辆就是完全不同的标签,我们也需要从可以选择的标签中进行挑选。对于分割,则有更多的选项,我们可以执行将不同性质的物体分割开来的”语义分割“,也可以执行将每个独立对象都分割开来的”实例分割“,还可以执行使用多边形或颜色进行分割的分割方法。同时,根据分割的“细致程度”,还可以分为“粗粒度分割”(Coarse)和“细粒度分割”(Fine-grained),具体的分割程度由训练图像而定。

在这里插入图片描述
在这里插入图片描述
因此,在图像数据集的读取过程中,可能会发现一个图像数据集会带有很多个指向不同任务的标签、甚至很多个不同任务的训练集。遗憾的是,数据集本身并不会说明数据集所指向的任务,因此我们必须从中辨别出自己所需要的部分。torchvision.datasets模块中自带的CelebA人脸数据集就是这种情
况。如下图所示,类datasets.CelebA下没有任何文字说明,只有一个指向数据源的链接,光看PyTorch官网,我们并不能判断这个数据集是什么样的数据集。不过,这个类由参数“target_type”,这个参数可以控制标签的类型,四种标签类型分别是属性(attr)、个体(identity)、边界框(bbox)和特征(landmarks)。根据参数说明,可以看出属性是40个二分类标签,可以从中选择一个进行分类,个体是判断这个人类是哪个具体任务,这两个标签指向的是识别任务。边界框和特征都以坐标形式表示,这两个标签都指向检测任务。

在这里插入图片描述

同样情况的还有Cityscapes,这个数据集的类dataset.Cityscapes下没有任何说明,但它含有参数mode和target_type,其中mode由两个模式:fine以及coarse,这两个模式表示Cityscape是一个用于分割任务额度数据集。同时,targe-type下面还有instance(实例分割)、semantic(语义分割)、polygon(多边形分割)、color(颜色分割)四种选项,所以要清晰了解自己的需求以及每种分割的含义才能正确填写这些参数。同时,我们可以一次性训练导入多个标签进行使用,如何混用这些标签来达成训练目标也成为难点之一。
在这里插入图片描述
再看看LSUN数据集,需要自己填写标签名称的情况:
在这里插入图片描述
现在我们对视觉任务有了一定了解:图像数据丰富多样,在具体任务不明确的情况下,我们连最基本的数据导入都存在问题。基于对图像任务的理解,本课内容将分为上、下两部分。上以图像识别任务为主,下以视觉任务中的其他任务为主。

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

1、认识经典数据

1.1入门数据:MNIST、其他数字与字母识别

第一部分要介绍的是最适合用于教学和实验、几乎对所有的电脑都无负担的MNIST一族。MNIST一族是数据和字母识别的最基本的数据集,这些数据集户全都是小尺寸图像的简单识别,可以被轻松放入任意神经网络中进行训练。具体如下:

数据名称数据说明
FashionMINST衣物用品数据集
MINST手写数字数据集
KuzushijiMNIST日语手写平假名识别,包含48个平假名字符和一个平假名迭代标记,一个高度
不平衡的数据集
QMNIST与MNIST高度相似的手写数字数据集
EMNIST与MNIST高度相似,在MNIST的基础上拓展的手写数字数据集
Omniglot全语种手写字母数据集,包含来自50个不同字母的1623个不同的手写字符,专用于“一次性学习"
USPS另一个体系的手写数字数据集,常用来与MNIST对比
SVHN实拍街景数字数据集(Street View House Number),是数字识别和检测中非常不同的一个数据集。有原始尺寸数据集可以下载,但在PyTorch中内置的是32x32的识别数据。注意,使用本数据需要SciPy模块的支持。

在这里插入图片描述
在PyTorch中,提供了三个与MNIST数据集相对比的数据集,分别是用于一次性学习的字母识别数据集Omniglot,另一个体系的手写数据集USPS,以及SVHN实拍街景数字数据集。这几个数据集与MNIST的区别如下图。
在这里插入图片描述
在深度视觉的研究中,我们很少专门就MNIST进行研究,但我们在这些简单识别数据集上设置了其他值得研究的问题。比如,在我们撰写论文或检验自己的架构时,MNIST一族是很好的基准线——他们尺寸很小,容易训练,很简单却又没有那么“简单”。一流的架构往往能够在MNIST数据集上取得99%以上的高分,而发表论文时,MNIST数据集的结果低于97%是不能接受的。单一机器学习算法能够在Fashion-MNIST数据集上取得的分数基本都在90%左右,而一流的深度学习架构至少需要达到95%以上的水准。再比如,我们常常使用平假名识别的数据集来研究深度学习中的样本不平衡问题,我们还使用Omniglot数据集来研究人脸识别(主要是个体识别 identity recognition)中常见的“一次性学习”问题(one-shot learning)。我们来重点讲讲这个“一次性学习”的问题。
在这里插入图片描述
在这里插入图片描述

在人脸识别中,我们有两种识别策略:

第一种策略是以人名为标签进行多分类,在训练样本中包含大量的同一个人的照片,测试集中也包含这个人的照片,看CNN能否正确预测出这个人的名字;而第二种策略则是一种二分类策略,在训练样本中给与算法两张照片,通过计算距离或计算某种相似性,来判断两张照片是否是同一个人,输出的标签为“是/否相似或一致”,在这种策略中,测试集的样本也是两张照片,并且测试集的样本不需要出现在训练集中。

如果基于第一种策略来执行人脸识别,则机场、火车站的人脸识别算法必须把全国人民的人脸数据都学习一遍才可能进行正确的判断。而在第二种策略中,算法只需要采集身份证/护照上的照片信息,再把它与摄像头中拍摄到的影像进行对比,就可以进行人脸识别了。这种“看图A,判断图B上的人是否与图A上的人是同一人”的学习方法,就叫做一次性学习,因为对于单一样本,算法仅仅见过一张图A而已。不难想象,实际落地的人脸识别项目都是基于一次性学习完成的。Omniglot数据集就是专门训练一次性学习的数据集。从上图可以看出,Omniglot数据集中的字母/符号对我们而言是完全陌生的,因此我们并无法判断出算法是否执行了正确的“识别”结果。而在Omniglot数据集上,算法是通过学习图像与图像之间的相似性来判断两个符号是否是一致的符号,至于这个符号是什么,代表什么含义,对Omniglot数据集来说并无意义。

字母和数字识别的数据集的尺寸都较小,因此PyTorch对以上每个数据集都提供了下载接口,因此我们无需自行下载数据,就可以使用torchvision.datasets.xxxx的方式来对他们进行调用。在网速没有太大问题的情况下,只要将download设置为True,并确定VPN是关闭状态,就可以顺利下载。注意:下载之后最好将download参数设置为False,否则只要调用目录写错,就会重新进行下载,费时也费流量。

(1)数据下载

方式一:通过torchvision.datasets.MNIST下载数据

import torch
import torchvision
import torch.nn as nn
import torchvision.transforms as transforms


train_data = torchvision.datasets.MNIST(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset',
                                        train=True,
                                        transform=transforms.ToTensor(),
                                        download=True)

test_data = torchvision.datasets.MNIST(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset',
                                        train=True,
                                        transform=transforms.ToTensor(),
                                        download=True)

请添加图片描述
方式二:下载了数据存储在自己电脑上并调用:
请添加图片描述

import torchvision
import torchvision.transforms as transforms


fminst = torchvision.datasets.FashionMNIST(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset',
                                           train=True,
                                           download=False,
                                           transform=transforms.ToTensor())




svhn = torchvision.datasets.SVHN(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/SVHN',
                                split="train",
                                download=False,
                                transform=transforms.ToTensor())


omniglot = torchvision.datasets.Omniglot(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset',
                                         background=True,  #在ominglot论文中,作者将训练集称为background,因此background代表训练集
                                         download=False,
                                         transform=transforms.ToTensor())

print(fminst)
'''
Dataset FashionMNIST
    Number of datapoints: 60000
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset
    Split: Train
    StandardTransform
Transform: ToTensor()

'''
print(svhn)
'''
Dataset SVHN
    Number of datapoints: 73257
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/SVHN
    Split: train
    StandardTransform
Transform: ToTensor()
'''
print(omniglot)
'''
Dataset Omniglot
    Number of datapoints: 19280
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/omniglot-py
    StandardTransform
'''

(2)查看数据的特征和标签

需要注意的是:由于深度视觉中的数据属性均有不同,如果简单得调用.data或者.targets很容易报错。本小结最后给出了一个万能方式。

方式一:.data/.targets查看数据特征和标签

当我们查看数据集时,只给出了Number of datapoints、Root location等,但我们想了解的是数据集的输入和标签,针对fminst来说,可以通过调.data的方式查看特征,.target的方式查看标签。

print(fminst.data)
'''
tensor([[[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]],

        [[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]],

        [[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]],

        ...,

        [[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]],

        [[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]],

        [[0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         ...,
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0],
         [0, 0, 0,  ..., 0, 0, 0]]], dtype=torch.uint8)
'''
print(fminst.targets)
'''
tensor([9, 0, 0,  ..., 3, 0, 5])
'''

但是并不是所有的数据集都可以通过这种方式来查阅。这是因为:当面临的任务不同时,每个数据集的标签排布方式和意义也都不同,因此不太可能使用相同的API进行调用。

for i in [fminst, svhn, omniglot]:
    print(i.data.shape)
'''
torch.Size([60000, 28, 28])
(73257, 3, 32, 32)
AttributeError: 'Omniglot' object has no attribute 'data'
'''

for i in [fminst, svhn, omniglot]:
    print(i.targets.shape)
'''
tensor([9, 0, 0,  ..., 3, 0, 5])
torch.Size([60000])
AttributeError: 'SVHN' object has no attribute 'targets'
'''
方式二:通过查看某一个索引值的方式来查看特征和标签

因此,我们可以看到omniglot数据并没有data和targets属性,那么我们应该如何查看omniglot数据呢?——通过索引的方式查看某一个单独的数据

#方式一:查看某一个索引的情况
print(omniglot[0])
'''
(tensor([[[1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         ...,
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.]]]), 0)
'''
#由此可以看出omniglot的每个索引是一个元组

#紧接着我们可以调用每个元组中的第一个切片来查看图像数据的特征和形状
print(omniglot[0][0])
'''
tensor([[[1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         ...,
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.],
         [1., 1., 1.,  ..., 1., 1., 1.]]])
'''
print(omniglot[0][0].shape)
#torch.Size([1, 105, 105]) ,omniglot数据的图像仅有一个通道,图片的形状是105*105

#查看样本量
print(len(omniglot)) #19280
方式三:万能方法

如果通过索引的方式还是报错,那就要使用报错概率最低的方式:


for i in [fminst, svhn, omniglot]:
    for x,y in i:
        print(x.shape, y)
        break
'''
torch.Size([1, 28, 28]) 9
torch.Size([3, 32, 32]) 1
torch.Size([1, 105, 105]) 0
'''

(3)数据可视化

方式一:PIL直接可视化

如果需要查看图片,可以通过去掉transforms.Totensor()来查看

fminst = torchvision.datasets.FashionMNIST(root='/Users/gaoyuxing/Desktop/all_file/torchvision_dataset',
                                           train=True,
                                           download=False)
                                           #transform=transforms.ToTensor())

#print(fminst)
#print(fminst[0])
#(<PIL.Image.Image image mode=L size=28x28 at 0x7FB843C23BB0>, 9)
print(fminst[0][0]) #(<PIL.Image.Image image mode=L size=28x28 at 0x7FB843C23BB0>, 9)
image = fminst[0][0]
image.show()

请添加图片描述

方式二:用一个万能公式来随机可视化五张图

注意:该方法要求已经将图片转换为tensor格式,即要求含有tansform = transforms.ToTensor()

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(omniglot)
plt.show()

在这里插入图片描述

plotsample(svhn)
plt.show()

在这里插入图片描述

plotsample(fminst)
plt.show()

在这里插入图片描述
根据类的不同,参数train可能变为split,还可能增加一些其他的参数,具体可以参照datasets页面。MNIST一组的数据集户都可以被用于简单的识别项目,是测试架构的最佳数据。在提出新架构或新方法时,学者们总会在MNIST或Fashion-MNIST数据集上进行测试,并将这些数据拿到高分(>95%)作为新架构有效的证明之一。

1.2竞赛数据:ImageNet、COCO、VOC、LSUN

除了数字和字母识别以外,最熟悉且最瞩目的就是各大竞赛的主力数据。之前在讲解大规模视觉挑战赛ILSVRC的时候,介绍过ImageNet数据集,和ImageNet数据一样,竞赛数据往往诞生于顶尖大学、顶尖科研机构或大型互联网公司的人工智能实验室,属于推动整个深度学习向前发展的数据集,因此这些数据集通畅数据量巨大、涵盖类别广泛、标签异常丰富、可以被用于各类图像任务,并且每年会更新迭代,且在相关竞赛停止或关闭之后会下架数据集。作为计算机视觉的学习者,我们没有用过这些数据,但是必须要知道它们的名字和基本信息。作为计算机视觉工程师,在每个项目上线之前,都需要使用这些数据来进行测试。现在就来了解一下这些数据集吧。

数据名称数据说明
ImageNet2012ImageNet大规模视觉挑战赛(ILSVRC)在2012年所使用的比赛数据。1000分类通用数据,涵盖了动植物、人类、生活用品、食物、景色、交通工具等类别。任何互联网大厂在深度学习模型上线之前必用的数据集。2012年版本数据集由于版权原因已在全网下架,现只分享给拥有官方学术头衔的机构或个人(这意味着,在申请数据集时必须使用xxx@xxx.edu的电子邮件,任何免费的、非学术机构性质的电子邮件地址都不能获得数据申请。因此,PyTorch中的torchvision.datasets.ImageNet类已经失效。
ImageNet2019ILSVRC在2017年之后就取消了识别任务,并将比赛赚到Kagg了上举办,现在ImageNet2019版本可以在Kaggle上免费现在,主要用于检测任务。
PASCAL VOCSegmentation PASCAL VOCDetection模式分析,统计建模和计算机学习大赛(Pattern Analysis, Statistical Modeling and Computation Learning, PSACAL),视觉对象分类(Visual Object Classes)数据集。与ImageNet类型相似,覆盖动植物、人类、生活用品、食物、交通等类别,但数据量相对较小。在PyTorch中被分为VOCSegementation与VOCDetection两个类,分别支持分割和检测任务,虽然带有类别标签但基本不支持识别任务。同时,PyTorch支持从2007到2012年的五个版本的下载,通畅我们都是用最新版本。
CocoCaptions CocoDetection微软Microsoft Common Objects in Context数据集,是大规模场景理解挑战赛(Large-scale Scene Understanding challenge,LSUN)中的核心数据,主要覆盖复杂的日常场景,是继ImageNet之后,最受关注的物体检测、语义分割、图像理解(Caption)方面的数据集,也是唯一关注图像理解的大规模挑战赛。其中,用于图像理解的部分是2015年之前的数据,用于检测和分割的则是2017年及之后的版本。需要安装COCO API才可以调用。PyTorch没有提供用于分割的API,但我们依然可以自己下载数据集用于分割。
LSUN城市景观、城市建筑、人文风光数据集,包含10中场景、20中对象的大猩猩数据集。其中,场景图像被用于LSUN大规模题场景理解挑战赛。20分类包含交通工具、动物、人类等图像,可用于识别任务。在我们导入数据时,我们会选择LSUN中的某一个场景或对象进行训练,因此个人给予LSUN进行的识别任务是二分类的。

在这里插入图片描述

各个数据集的样图如下所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
这类数据集最大的特点就是数据量大、原图尺寸很大,因此整个数据集所占用的存储空间也会更大。最小的VOC数据集3.6G左右,其他都在20G以上。PyTorch只提供了VOC的下载通道,但这个下载通道极不稳定,因此最好提前下载好将数据放入根目录中进行读取。

竞赛数据都来自于各大机构和大学的研究,因此其风格和调用流程不可能一致,使用每个数据集都需要进行一定的探索,还必须具备一定的英文阅读、谷歌翻译能力、GitHub使用、python脚本编程能力。课程准备了2012年的ImageNet数据集、VOC以及LSUN数据集中较小的两类数据,并准备了可以运行下载LSUN其他类别数据的python脚本和readme文档。其中VOC适用于分割和检测任务,ImageNet和LSUN适用于分类任务。

值得一提的是,LSUN竞赛现已关闭,因此测试集已无法下载,但训练集和验证集还是可以下载。LSUN各个类别的数据集大小如下所示。在课程中,我给大家下载了户外教堂以及教室两个类别,可以用于分类。

请添加图片描述

如何还需要LSUN其他类别的数据时,可以在cmd(Windows)/terminal(MAC)中执行以下命令:

#首先将cmd根目录切换到download.py所在的目录
#切换到同一个盘的不同目录
C:\Users\Admin>cd C:\dataset2\lsun-master
#切换到不同盘的目录
C:\Users\Admin>cd /d F:datasets2\lsun-master
#切换到相应目录后运行下载,其中church_outdoor是其中一个类别
F:datasets2\lsun-master>python download.py -c church_outdoor

LSUN数据集下载后是压缩文件,解压之后是LMBD(Lightning Memory-Mapped Database)数据库的文件。在深度学习中,有许多大型突袭那个数据集都是存储为LMBD文件的,因为框架Caffe和TensorFlow在早起使用了大量存储为LMBD格式的数据集。从LMBD数据库中读取的代码并不复杂,但需要多数据库和LMBD相关的基础知识。幸运的是:LSUN的LMBD文件可以直接通过PyTorch中datasets下的类来直接调用(下载老师提供的数据之后一定要把里面的数据解压出来,因为torchvision无法识别压缩包),具体的代码如下:
请添加图片描述

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data",
                                     classes=["church_outdoor_val"],
                                     transform=transforms.ToTensor())

print(data_val)
'''
Dataset LSUN
    Number of datapoints: 300
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data
    Classes: ['church_outdoor_val']
    StandardTransform
Transform: ToTensor()
'''


print(data_val[0])
'''
(tensor([[[0.4627, 0.4627, 0.4667,  ..., 0.7137, 0.7098, 0.7098],
         [0.4627, 0.4627, 0.4667,  ..., 0.7255, 0.7176, 0.7098],
         [0.4627, 0.4627, 0.4667,  ..., 0.7333, 0.7255, 0.7176],
         ...,
         [0.3137, 0.3412, 0.3216,  ..., 0.6471, 0.6392, 0.6353],
         [0.1961, 0.2353, 0.2078,  ..., 0.6471, 0.6392, 0.6314],
         [0.1843, 0.2235, 0.1765,  ..., 0.6510, 0.6392, 0.6314]],

        [[0.5647, 0.5647, 0.5686,  ..., 0.7059, 0.7020, 0.7020],
         [0.5647, 0.5647, 0.5686,  ..., 0.7176, 0.7098, 0.7020],
         [0.5647, 0.5647, 0.5686,  ..., 0.7255, 0.7176, 0.7098],
         ...,
         [0.3137, 0.3412, 0.3216,  ..., 0.6510, 0.6431, 0.6392],
         [0.2000, 0.2392, 0.2118,  ..., 0.6510, 0.6431, 0.6353],
         [0.1882, 0.2275, 0.1804,  ..., 0.6549, 0.6431, 0.6353]],

        [[0.7882, 0.7882, 0.7922,  ..., 0.7098, 0.7059, 0.7059],
         [0.7882, 0.7882, 0.7922,  ..., 0.7216, 0.7137, 0.7059],
         [0.7882, 0.7882, 0.7922,  ..., 0.7294, 0.7216, 0.7137],
         ...,
         [0.3059, 0.3333, 0.3137,  ..., 0.6706, 0.6627, 0.6588],
         [0.1804, 0.2196, 0.1922,  ..., 0.6706, 0.6627, 0.6549],
         [0.1647, 0.2039, 0.1608,  ..., 0.6745, 0.6627, 0.6549]]]), 0)
'''

#随机查看五张图
plotsample(data_val)
plt.show()

在这里插入图片描述
只导入一个类别时,该类别是没有标签的,可以通过以下代码来验证:

check_ = 0
for x,y in data_val:
    check_ += y
print(check_)
#0

想要进行训练,至少得导入两个类别,进行二分类:

data_train = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data",
                                     classes=["church_outdoor_train", "classroom_train"],
                                     transform=transforms.ToTensor())



data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data",
                                     classes=["church_outdoor_val", "classroom_val"],
                                     transform=transforms.ToTensor())


print(data_val)
'''
Dataset LSUN
    Number of datapoints: 600
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data
    Classes: ['church_outdoor_val', 'classroom_val']
    StandardTransform
Transform: ToTensor()
'''


#此时标签自动被标注为0和1
#由于循环代码在data_train上运行会爆炸,因此在验证集上跑
'''
for x,y in data_val:
    print(y)
'''


check_ = 0
for x,y in data_val:
    check_ += y
print(check_)
#300

很遗憾的是,ImageNet数据集并不能使用PyTorch中的datasets下的类来直接调用(能够被pytorch直接调用的都是tar.gz格式文件或者是tar.gz解压后的文件),而VOC不能作为识别数据被使用,因此希望调用竞赛数据来完成分类任务则需要更多的技巧,接下来会在《一、2.使用自己的数据/图片创造数据集》中,将ImageNet和LSUN数据作为案例来说明,如何将压缩文件/数据库文件中的图片导出为四维tensor。

如果没有GPU,不推荐使用ImageNet。虽然比起LSUN和VOC来,ImageNet含有更丰富的数据,但是没有GPU的支持,很难对该数据进行适当的训练。如果要使用ImageNet数据,建议使用Colab等线上平台的大型GPU上训练。

1.3景物、人脸、通用、其他

如果入门数据太简单,竞赛数据又太大该怎么办呢?难道就没有尺寸适中,又非常适合初学者练习和试验的数据集吗?当然有。除了竞赛数据和入门数据,我们还有不少通用的数据集,比如:

请添加图片描述
在这里插入图片描述
部分数据集的样例如下所示:
【CelebA】强烈推荐,一共有40种属性类别,1000个个体标签,可以用来做识别任务。
在这里插入图片描述
【CIFAR10 & CIFAR100】CIFAR多用于模型验证,很标准也很简单。基本上模型创新都会在CIFAR数据上跑一跑。

在这里插入图片描述
【STL-10】不是很推荐,多用于无监督学习任务。

在这里插入图片描述
【Cityscapes】
在这里插入图片描述
【Place365】场景非常丰富,有400多个类别,可以用于多分类任务,由MIT支持。
在这里插入图片描述
在图像识别中我们比较常用的是CIFAR,因此我们以CIFAR为例来调用下这个数据集:

data = torchvision.datasets.CIFAR10(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar",
                                     train=True,
                                     download=False,
                                     transform=transforms.ToTensor())

print(data)
'''
Dataset CIFAR100
    Number of datapoints: 50000
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar
    Split: Train
    StandardTransform
Transform: ToTensor()
'''


for x,y in data:
    print(x, y)
    break

#通过结果可以看出来x,y没有打包成元组的形式,是独立的

print(data.data.shape)
#(50000, 32, 32, 3) 一共5万张图,每张图的尺寸是(32, 32),三通道图
#print(data.classes)
'''
['apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle', 'bicycle', 'bottle', 'bowl', 'boy', 'bridge', 'bus', 'butterfly', 'camel', 'can', 'castle', 'caterpillar', 'cattle', 'chair', 'chimpanzee', 'clock', 'cloud', 'cockroach', 'couch', 'crab', 'crocodile', 'cup', 'dinosaur', 'dolphin', 'elephant', 'flatfish', 'forest', 'fox', 'girl', 'hamster', 'house', 'kangaroo', 'keyboard', 'lamp', 'lawn_mower', 'leopard', 'lion', 'lizard', 'lobster', 'man', 'maple_tree', 'motorcycle', 'mountain', 'mouse', 'mushroom', 'oak_tree', 'orange', 'orchid', 'otter', 'palm_tree', 'pear', 'pickup_truck', 'pine_tree', 'plain', 'plate', 'poppy', 'porcupine', 'possum', 'rabbit', 'raccoon', 'ray', 'road', 'rocket', 'rose', 'sea', 'seal', 'shark', 'shrew', 'skunk', 'skyscraper', 'snail', 'snake', 'spider', 'squirrel', 'streetcar', 'sunflower', 'sweet_pepper', 'table', 'tank', 'telephone', 'television', 'tiger', 'tractor', 'train', 'trout', 'tulip', 'turtle', 'wardrobe', 'whale', 'willow_tree', 'wolf', 'woman', 'worm']
'''

print(np.unique(data.targets)) #一共100个标签
'''
[0 1 2 3 4 5 6 7 8 9]
'''

data_test = torchvision.datasets.CIFAR10(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar",
                                          train=False,
                                          download=False,
                                          transform=transforms.ToTensor())

print(data_test)
'''
Dataset CIFAR10
    Number of datapoints: 10000
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar
    Split: Test
    StandardTransform
Transform: ToTensor()
'''

plotsample(data)
plt.show()

在这里插入图片描述


data100 = torchvision.datasets.CIFAR100(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar",
                                          train=False,
                                          download=False,
                                          transform=transforms.ToTensor())
print(data100)
'''
Dataset CIFAR100
    Number of datapoints: 10000
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/cifar
    Split: Test
    StandardTransform
'''


print(np.unique(data100.targets))
'''
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 96 97 98 99]
'''
plotsample(data100)
plt.show()

在这里插入图片描述
以上是对常用数据的介绍,对于未介绍的数据,一般如何去找?一般不推荐PyTorch自带的download功能,由于数据集尺寸不小,官网下载速度比较慢,还容易出现超时的问题,因此最好不好使用download参数。

如下图所示,对于PyTorch自带的数据集,可以从数据说明中找到这个数据集的官网或者原始地址,进入原始地址后,大部分可以找到数据的下载渠道。当然,通过原始地址下载的数据很有可能不能用torchvision.datasets来读取。

在这里插入图片描述

由于图像数据调用接口多不相同,在调用的时候会出现各种各样的Bug,因此如果不能用torchvision来调用时,我们可以将图片数据作为自己的数据进行处理。于是就引出了下一节的内容: 使用自己的数据/图片创造数据集。

2、使用自己的数据/图片创造数据集

如果我们拥有自己的数据集,首先要考虑的是如何将自己的数据输入到PyTorch中去。如果数据来自于网络(Kaggle下载,从论文作者处获取,从某个数据集官网下载的),那原始数据的格式可能是各种情况,最常见的是各类压缩文件、pt文件、数据库格式文件或者是png/jpg/webp等原始图像。如果数据来自于实验室、公司数据库或者是领导/导师给的数据,大概率是csv/txt/mat等结构的二维数据表。无论我们的原始数据集呈现什么样的格式,必须将其转换为四维张量,数据才能被卷积神经网络处理。对于任意压缩文件,先解压查看内部是什么内容,对于其他格式文件,可以根据需求查看本节中对应的小节。

2.1从图像png/jpg到四维tensor

ImageFolder

当我们的数据是一系列图像时,并且每个标签对应的图像是存放在单独的文件夹中时,处理方式比较简单。在torchvision中存在直接将文件夹中图片打包成tensor的类:ImageFolder,它的参数和torchvision.datasets中其他数据导入类的参数非常相似,其中root是原始图像所在的根目录,transform是希望对图像执行的具体操作。


train_dataset = torchvision.datasets.ImageFolder(root='xxx',
                                              transform=torchvision.transforms.ToTensor())

这个类可以接受.jpg、.jpeg、.png、.ppm、.bmp、 .pgm、.tif、.tiff、.webp这九种不同的图片格式输入,并且还能够通过文件夹的分类自动识别标签。 如果图片打包成一下所示的特定格式,就是用于ImageFolder这个类:
在这里插入图片描述
在根目录下,每个类别需要有单独的文件夹,如上图所示,cat和dog就是两个种类,而类别文件夹下可以存放多个子文件夹或者直接存放图片。图片的格式不需要统一,只要是ImageFolder能接受的9种格式即可。接下来就以celebA数据中随机提取出来的子集为例来做实验。
请添加图片描述

train_dataset = torchvision.datasets.ImageFolder(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/Train",
                                                 transform=torchvision.transforms.ToTensor())

print(train_dataset)
'''
Dataset ImageFolder
    Number of datapoints: 60
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/Train
    StandardTransform
Transform: ToTensor()
'''

for x, y in train_dataset:
    print(x, y)
    break

'''
tensor([[[0.9922, 0.9922, 0.9922,  ..., 0.9961, 0.9961, 0.9961],
         [0.9922, 0.9922, 0.9922,  ..., 0.9961, 0.9961, 0.9961],
         [0.9922, 0.9922, 0.9922,  ..., 0.9961, 0.9961, 0.9961],
         ...,
         [0.9922, 0.9922, 0.9922,  ..., 0.9569, 0.9255, 0.9373],
         [0.9922, 0.9922, 0.9922,  ..., 0.9843, 0.9373, 0.9333],
         [0.9922, 0.9922, 0.9922,  ..., 1.0000, 0.9490, 0.9412]],

        [[0.9059, 0.9059, 0.9059,  ..., 0.9569, 0.9569, 0.9569],
         [0.9059, 0.9059, 0.9059,  ..., 0.9569, 0.9569, 0.9569],
         [0.9059, 0.9059, 0.9059,  ..., 0.9569, 0.9569, 0.9569],
         ...,
         [0.8549, 0.8549, 0.8549,  ..., 0.6784, 0.6667, 0.7020],
         [0.8549, 0.8549, 0.8549,  ..., 0.7059, 0.6784, 0.6980],
         [0.8549, 0.8549, 0.8549,  ..., 0.7255, 0.6941, 0.6980]],

        [[0.7529, 0.7529, 0.7529,  ..., 0.8588, 0.8588, 0.8588],
         [0.7529, 0.7529, 0.7529,  ..., 0.8588, 0.8588, 0.8588],
         [0.7529, 0.7529, 0.7529,  ..., 0.8588, 0.8588, 0.8588],
         ...,
         [0.6902, 0.6902, 0.6902,  ..., 0.4745, 0.4706, 0.5059],
         [0.6902, 0.6902, 0.6902,  ..., 0.4941, 0.4745, 0.5020],
         [0.6902, 0.6902, 0.6902,  ..., 0.4941, 0.4824, 0.4980]]]) 0
'''

#几种可以调用的属性
print(train_dataset.classes)
#['female', 'male']
print(train_dataset.targets)
'''
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
'''

#查看具体的图像地址
print(train_dataset.imgs)
#这里展示的是每张图的图纸

plotsample(train_dataset)
plt.show()

在这里插入图片描述

#测试集-注意更换根目录
test_dataset = torchvision.datasets.ImageFolder(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/Test",
                                                transform=torchvision.transforms.ToTensor())

print(test_dataset)
'''
Dataset ImageFolder
    Number of datapoints: 62
    Root location: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/Test
    StandardTransform
Transform: ToTensor()
'''
plotsample(test_dataset)
plt.show()

在这里插入图片描述
注意,ImageFolder只能读取根目录的子文件夹中的图片,并且一定会将子文件夹的名称作为类别。当根目录中只有一个文件夹时,则会将有图片的标签标注为0。当根目录中没有文件夹,而是直接存放图片时则会报错。

毫无疑问,ImageFolder是一个省时省力的方式,但是简单也意味着它不够灵活。当图像相关标签类别数量很少,我们还能够将图像按照它们所在的标签类别打包,当数据的标签类别比较多,或者样本量比较大的时,要将同一标签类别的样本分到不同类比的文件夹中就变得不再“省时省力”。但幸运的是,在图像的世界里,许多数据集、尤其是巨大数据集是提前按照标签“分好”的,比如我们之前看过的LSUN数据集。

对类似于LSUN的数据集,我们会按照标签类别分别对数据进行下载,如果下载后获得的数据是这个类别下的图片文件,那毫无疑问这些文件是可以按照类别被储存在单独的文件夹里的。此时我们就可以使用ImageFolder来对数据进行读取。当然了,如果你的数据不是按照标签类别进行下载,或你的标签类别是单独储存在excel或txt文件当中,我们就需要别的操作来读取数据了。

使用ImageFolder读取后的数据是无法轻易更改标签的,这是因ImageFolder继承自pytorch中的visiondataset类,标签在这个父类中生成,并与特征图一起被固定为一个元组(用来表示从特征到标签的映射)。我们可以通过ImageFolder的各种属性、或索引等方式调用出这个元组的一份复制来进行展
示,却无法直接触及到元组中的数据本身,因此我们无法通过ImageFolder的读取出的标签进行改变。虽然我们可以先从ImageFolder的结果中复制出特征图,再使用TensorDatasets重新对特征图和标签进行拼接,但Python并不支持对元组的批量操作,如果需要复制每个特征图,就必须对每个元组进行循
环。但当数据量很大时,从ImageFolder的结果中提取全部样本就会需要很多时间和算力。因此,当数据不能按照类别进行下载时,大部分深度学习研究者都不会使用ImageFolder对数据进行读取,而会选择更加灵活的方式:自己写一个读取数据用的类。

Clasee torch.utils.data.Dataset

在PyTorch中存在一个专门帮我们构建数据集的类:Dataset,这个类在torch.utils.data模块下。属于PyTorch数据处理的经典父类之一(另一个总是使用的经典父类是nn.Moudle)。在PyTorch中,许多torchvision.datasets中读数据的类,以及TensorDataset这些合并张量来生成数据的类,都继承自Dataset。如果一个读取数据的类继承自Dataset,那它读取出的数据一定是可以通过索引的方式进行调用和查看的,而继承自其他父类的、读取数据集的功能却不一定能使用索引进行查看,这种性质让Dataset子类的构成也与其他类不同。

Dataset中规定,如果一个子类要继承Dataset,则必须在子类中定义__getitem__() 方法。从这个方法的名字(get item,获取对象)也可以看出,它是帮助我们“获取对象”的方法。这个方法中的代码必须满足三个功能:

1)读取单个图片并转化为张量
2)读取该图片对应的标签
3)将该图片的张量与对应标签打包成一个样本并输出

该样本的形式是一个元组,元组中的第一个对象是图像张量,第二个对象是该图像对应的标签。

Dataset类中包含自动循环__getitem__() 并拼接其输出结果的功能。也就是说,对于任意继承自Dataset的子类,只要我们恰当地定义了__getitem__() ,该子类的输出就一定是打包好的整个数据集。我们可以根据数据的实际情况定义__getitem__() ,可以说是实现了最大程度上的灵活性。

现在,我们使用celebA数据集举个例子。完整的celebA数据集中包含图片20万+张(图像大小20G),其中个体识别的标签为“人名”,类别有10,177个,属性识别的标签有40个,每个标签下是二分类,两种标签类别在txt中的格式不同。如果你感兴趣源文件,你可以在课程数据集的dataset3中找到它。将压缩文件解压后,即可获得具体的图像。

在这里插入图片描述

在课程中,我准备了包含1000张图片的celebA的子集dataset4\picturetotensor\celebAsubset文件夹中。

在这里插入图片描述

这个文件夹中的目录层次与dataset3中的celebA的原始数据集完全一致,只不过这个文件夹的图像和标签都只有前一千个样本。该子集仅作为读取数据用的例图,并不能被用于建模,如果需要建模请使用原始的20G大小的完整数据集。

在例图上,我们将展示如何使用继承自Dataset的类读取不同的图片和标签类别,你可以自由将数据更换为你的数据进行相同的操作。以下是我的根目录、个体识别的标签txt以及属性识别的标签txt:

在这里插入图片描述

在这里插入图片描述

在写具体的类之前,我们可以先定义__getitem__() 方法中要求的内容,试着读取一张图片并生成样本的元组。在CV课程最开始的时候,我们使opencv中的cv.imread函数进行过图像的读取。事实上,有大量的库中都包含能够将图像转化为像素值的函数,原则上我们可以使用任何自己熟悉的函数。在本节课中我们pytorch官方推荐的scikit-learn图像处理库scikit-image来进行处理,只要你的环境中安装有sklearn。我们可以通过下面的代码进行检查:

from skimage import io

#包含了所有图像的目录,没有具体到某一张图像
imgpath=r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba'

#标签文件的目录
csvpath = r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Anno/identity_CelebA_1000.txt'

identity = pd.read_csv(csvpath, sep=" ", header=None) #txt文件是以空格为分割符的

#print(identity.head())
'''
            0     1
0  000001.jpg  2880
1  000002.jpg  2937
2  000003.jpg  8692
3  000004.jpg  5805
4  000005.jpg  9295

'''

#读一张图
image = io.imread("/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba/000001.jpg")
plt.imshow(image)
plt.axis("off")
plt.show()

在这里插入图片描述

#将读取的标签和图像合并

idx = 0
imgpath=r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba'
#标签文件的目录
csvpath = r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Anno/identity_CelebA_1000.txt'
identity = pd.read_csv(csvpath, sep=" ", header=None) #txt文件是以空格为分割符的

imgdic = os.path.join(imgpath, identity.iloc[idx, 0])
#print(io.imread(imgdic))
image = torch.tensor(io.imread(imgdic))

sample = (image, identity.iloc[idx, 1])


#print(image.shape) #torch.Size([687, 409, 3])
#print(sample)
'''
(tensor([[[253, 231, 192],
         [253, 231, 192],
         [253, 231, 192],
         ...,
         [254, 244, 219],
         [254, 244, 219],
         [254, 244, 219]],

        [[253, 231, 192],
         [253, 231, 192],
         [253, 231, 192],
         ...,
         [254, 244, 219],
         [254, 244, 219],
         [254, 244, 219]],

        [[253, 231, 192],
         [253, 231, 192],
         [253, 231, 192],
         ...,
         [254, 244, 219],
         [254, 244, 219],
         [254, 244, 219]],

        ...,

        [[253, 218, 176],
         [253, 218, 176],
         [253, 218, 176],
         ...,
         [244, 173, 121],
         [236, 170, 120],
         [239, 179, 129]],

        [[253, 218, 176],
         [253, 218, 176],
         [253, 218, 176],
         ...,
         [251, 180, 126],
         [239, 173, 121],
         [238, 178, 128]],

        [[253, 218, 176],
         [253, 218, 176],
         [253, 218, 176],
         ...,
         [255, 185, 126],
         [242, 177, 123],
         [240, 178, 127]]], dtype=torch.uint8), 2880)
'''

plt.imshow(image)
plt.show()

根据以上内容可以先写一个初级版本的类来构建图像数据,将图片数据与个体标签属性组装在一起。

from torch.utils.data import Dataset
import os
from skimage import io



class CustomDataset(Dataset):
    def __init__(self, root_dir, csv_file, transform = None):
        super(CustomDataset, self).__init__( )
        self.imgpath = root_dir
        self.identity = pd.read_csv(csv_file, sep=" ", header=None)
        self.transform = transform


    def __getitem__(self, idx):
        imgdic = os.path.join(self.imgpath, self.identity.iloc[idx, 0])
        label = self.identity.iloc[idx, 1]
        image = io.imread(imgdic)
        sample = (image, label)
        
        return sample 

接下来再写一个升级版的:

import pandas as pd
import torch
from torch.utils.data import Dataset
from torchvision import transforms
import os
import numpy as np
from skimage import io


class CustomDataset(Dataset):
    def __init__(self, root_dir, csv_file, transform = None):
        super(CustomDataset, self).__init__()
        self.imgpath = root_dir
        self.identity = pd.read_csv(csv_file, sep=" ", header=None)
        self.transform = transform

    def __len__(self):
        return len(self.identity)

    def __info__(self):
        print("CustomData")
        print('\t Number of sample:{}'.format(len(self.identity)))
        print("\t Number of classes:{}".format(len(np.unique(self.identity.iloc[:, 1]))))
        print("\t root_dir:{}".format(self.imgpath))


    def __getitem__(self, idx):
        #保证 idx 不是一个tensor
        if torch.is_tensor(idx):
            idx = idx.tolist()

        imgdic = os.path.join(self.imgpath, self.identity.iloc[idx, 0])
        label = self.identity.iloc[idx, 1]
        image = io.imread(imgdic)


        if self.transform != None:
            image = self.transform(image)

        sample = (image, label)
        return sample




imgpath=r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba'
#标签文件的目录
csvpath = r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Anno/identity_CelebA_1000.txt'

data = CustomDataset(root_dir=imgpath, csv_file=csvpath, transform = transforms.ToTensor())

print(data.__info__())
'''
CustomData
	 Number of sample:1000
	 Number of classes:922
	 root_dir:/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba
'''
print(data.__len__())
#1000


for x, y in data:
    print(x.shape, y)
    #torch.Size([3, 687, 409]) 2880
    break

接下来,我们读取属性标签,并构建图片数据和标签数据的数组

#读取属性识别的标签
imgpath=r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba'
#标签文件的目录
csvpath = r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Anno/list_attr_celeba_1000.txt'

#attr_ = pd.read_csv(csvpath, sep=" ",header=None)
#print(attr_.head())
#报错,在属性txt中,-1和1之间、-1和-1之间以及1和-1之间的间距并不相同,有的是一个空格的间隔,有的是两个个空格的间隔,因此这句代码就会报错。


attr_ = pd.read_csv(csvpath, header=None)
print(attr_.head())
#此时我们会发现所有的属性列都放在都一列里面,这个时候我们想到了split函数

#print(attr_.iloc[0, 0].split())
#此时我们会发现属性字段被拆分成单独的数据
#print(len(attr_.iloc[0, 0].split()))
#41

attr_ = pd.DataFrame(attr_.iloc[1:, 0].str.split().tolist(),
                     columns=attr_.iloc[0, 0].split())

#属性值

print(attr_.head())

#然后我们取一列来作为我们的标签值
label = attr_.loc[:, "Attractive"]
#print(label)

构建属性数据

class CustomDataset_attr(Dataset):
    def __init__(self, csv_file, root_dir, labelname, transform=None):
        self.root_dir = root_dir
        self.attr_ = pd.read_csv(csvpath,header=None)
        self.labelname = labelname
        self.transform = transform

    def __len__(self):
        return len(self.attr_)

    def __info__(self):
        print("CustomData")
        print("\t Number of samples: {}".format(len(self.attr_) - 1))
        print("\t root_dir: {}".format(self.root_dir))


    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.tolist()

        self.attr_ = pd.DataFrame(self.attr_.iloc[1:, 0].str.split().tolist(),
                                  columns=self.attr_.iloc[0, 0].split())

        imgdic = os.path.join(self.root_dir, self.attr_.iloc[idx, 0])
        image = io.imread(imgdic) #将图片转换成数组形式
        label = int(self.attr_.loc[idx, self.labelname])

        if self.transform != None:
            image = self.transform(image)

        sample = (image, label)
        return sample

imgpath=r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba'
#标签文件的目录
csvpath = r'/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Anno/list_attr_celeba_1000.txt'
labelname = "Attractive"

data = CustomDataset_attr(csvpath, imgpath, labelname)


print(data)
#<__main__.CustomDataset_attr object at 0x7ff3e7247880>

print(data.__info__())
'''
CustomData
	 Number of samples: 1000
	 root_dir: /Users/gaoyuxing/Desktop/all_file/torchvision_dataset/picturestotensor/celebAsubset/Img/Img_celeba.7z/img_celeba
'''


print(data[500])
'''
(array([[[ 56,  44,  32],
        [ 56,  44,  32],
        [ 56,  44,  32],
        ...,
        [ 46,  38,  19],
        [ 46,  38,  19],
        [ 46,  38,  19]],

       [[ 56,  44,  32],
        [ 56,  44,  32],
        [ 56,  44,  32],
        ...,
        [ 46,  38,  19],
        [ 46,  38,  19],
        [ 46,  38,  19]],

       [[ 56,  44,  32],
        [ 56,  44,  32],
        [ 56,  44,  32],
        ...,
        [ 46,  38,  19],
        [ 46,  38,  19],
        [ 46,  38,  19]],

       ...,

       [[ 54,  52,  37],
        [ 53,  51,  36],
        [ 53,  51,  36],
        ...,
        [196, 210, 211],
        [197, 211, 212],
        [198, 212, 213]],

       [[ 56,  54,  39],
        [ 55,  53,  38],
        [ 55,  53,  38],
        ...,
        [197, 211, 212],
        [198, 212, 213],
        [199, 213, 214]],

       [[ 57,  55,  40],
        [ 57,  55,  40],
        [ 56,  54,  39],
        ...,
        [199, 213, 214],
        [200, 214, 215],
        [201, 215, 216]]], dtype=uint8), 1)
'''

2.2从二维表(csv/txt)到四维tensor

当我们的训练数据是图像时,我们总是将其放入卷积神经网络中进行训练,也因此卷积网络需要的输入结构为(sample,channels,weight,height),那如果我们是二维表格数据,也可以放入卷积网络中吗?卷积网络常常表现出比普通机器学习算法更为强大的学习能力,如果二维数据结构也可以使用卷积网络来进行处理,或许可以获得更好的结果。首先二维数据基本都储存在csv/txt这些表格结构中,如果我们遇见任意表格结构,可以先将其读入Python:

#csv
import pandas as pd
data = pd.read_csv(r"......")

#txt
import pandas as pd
data = pd.read_csv(r"....", sep=" ")

导入数据之后,将二维表格整理成更高维的数据,就可以放入卷积网络进行训练:

data = np.random.randint(0, 255, (10, 10000)) #假设现在是10个样本,每个样本10000个特征
#print(data.shape)
#(10, 10000)


data = data.reshape(10, 1, 100, 100)
#print(data.shape)
#(10, 1, 100, 100)

data = torch.tensor(data) #将数据放入tensor中转换格式
plt.imshow(data.view(-1, 100, 100, 1)[0])
#在imshow中需要把通道放在最后一维,而在torch中通道要放在第二唯
plt.show()

在这里插入图片描述
画出来的是一个灰度图,灰度图默认是绿色的,也可以是其他颜色,值越接近255点越亮,越接近0越暗。

reshape并不是一个很难的功能,对于大多数二维数据表而言,真正的问题是没有足够的特征用于变形。大部分二维表格数据的特征数都不是很多,甚至在传统机器学习中,人们害怕高维数据而努力对数据进行降维以提高计算效率。但对卷积神经网络来说,我们至少也需要784(28*28)个特征。那低维数据要怎样才能够放入卷积网络中进行训练呢?答案是先升维,再变化结构。在这里,我介绍一种常见的升维方式:多项式升维。

这是一种将特征数据交互相乘来增加特征维度的方法,它靠增加自变量上的次数来提升维度。只要我们设定一个自变量上的次数(大于1),就可以将特征映射到高维空间、并获得数据投影在高次方的空间中的结果。这种方法可以非常容易地通过sklearn中的类PolynomialFeatures来实现。

class sklearn.preprocessing.PolynomialFeatures (degree=2, interaction_only=False,include_bias=True)

from sklearn.preprocessing import PolynomialFeatures as PF

#构建初始数据
x = np.arange(1, 4).reshape(-1, 1)
print(x)
'''
[[1]
 [2]
 [3]]
'''

#二次多项式,参数degree控制多项式的次数
poly = PF(degree=2)

#借口transform直接调用
x_ = poly.fit_transform(x)
print(x_)
'''
[[1. 1. 1.]
 [1. 2. 4.]
 [1. 3. 9.]]
'''

print(x_.shape)
#(3, 3)

#三次多项式
x1 = PF(degree=3).fit_transform(x)
print(x1)
'''
[[ 1.  1.  1.  1.]
 [ 1.  2.  4.  8.]
 [ 1.  3.  9. 27.]]
'''

在这里插入图片描述

x = np.arange(6).reshape(3, 2)
print(x)
'''
[[0 1]
 [2 3]
 [4 5]]
'''


x2 = PF(2).fit_transform(x)
#print(x2)
'''
[[ 1.  0.  1.  0.  0.  1.]
 [ 1.  2.  3.  4.  6.  9.]
 [ 1.  4.  5. 16. 20. 25.]]
'''

在这里插入图片描述

x3 = PF(3).fit_transform(x)
print(x3)
'''
[[  1.   0.   1.   0.   0.   1.   0.   0.   0.   1.]
 [  1.   2.   3.   4.   6.   9.   8.  12.  18.  27.]
 [  1.   4.   5.  16.  20.  25.  64.  80. 100. 125.]]
'''

在这里插入图片描述
在机器学习课程中,我们曾经证明这样的升维方式可以大幅度提升线性模型处理非线性数据的能力。对于二维表格数据来说,多项式变化能够有效将维度升高。但这种方式并不是所有时候都可以用。多项式变化是对原始特征进行重组后形成新的特征,并没有在原始特征基础上进行深层特征提取,因此当原始特征本来就非常少时,多项式变化非常容易导致过拟合。我们来看一个例子:

from sklearn.datasets import fetch_california_housing as FCH
from sklearn.preprocessing import PolynomialFeatures as PF
from sklearn.linear_model import LinearRegression as LR
from sklearn.model_selection import train_test_split as TTS
from sklearn.metrics import mean_squared_error as MSE

data = FCH() ##实例化
X = data.data
y = data.target
#print(y) ##0~5之间的小数
#print(X.shape)
#(20640, 8) #一共20640条数据,每条数据有8个特征
#print(y.shape)
#(20640,)

Xtrain, Xtest, Ytrain, Ytest = TTS(X, y, test_size=0.3, random_state=430)
reg = LR().fit(Xtrain, Ytrain)
mse = MSE(reg.predict(Xtrain), Ytrain)
print(mse)
#0.5313839898257883


mse_test = MSE(reg.predict(Xtest), Ytest)
print(mse_test)
#0.5084519092802269
##训练集和测试集结果非常相近,虽然表现不佳但是不存在太多过拟合的情况

poly = PF(degree=4).fit(Xtrain)
Xtrain_ = poly.transform(Xtrain)
Xtest_ =poly.transform(Xtest)

print(Xtrain_.shape)
#(14448, 495)
print(Xtest_.shape)
#(6192, 495)
#此时我们可以看出来特征维度已经增加为495

reg = LR().fit(Xtrain_, Ytrain)
mes_1 = MSE(reg.predict(Xtrain_), Ytrain)
print(mes_1)
#0.3077989530008953
mse_2 = MSE(reg.predict(Xtest_), Ytest)
print(mse_2)
#692760.8648647917
#此时我们可以从mes值看出来,过拟合情况已经发生了

最终得出结论,多项式升维方法不适合fetch_california_housing数据集。因此,在我们对数据进行升维、并考虑将数据放入卷积网络进行学习时,必须要考虑数据本身的复杂程度是否足够。当数据过于简单、特征量过少的时候,多项式操作只能加重过拟合,卷积网络对于这样的数据来说也是过于复杂的、并不经济的模型。我们可以通过升维后的表现来判断数据是否具有更强大的潜力(即,特征本身含有较多信息,在升维之后也不会那么容易过拟合,比较适合放入卷积网络进行训练)。来看这一组数据:

from sklearn.datasets import fetch_covtype as FC
from sklearn.preprocessing import PolynomialFeatures as PF
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split as TTS


model = LogisticRegression(penalty='l2', dual=False, tol=0.0001, C=1.0, fit_intercept=True, intercept_scaling=1,
         class_weight=None, random_state=420, solver='liblinear',
         max_iter=1000, multi_class='ovr', verbose=0, warm_start=False, n_jobs=1)

data = FC() #首次加载会比较耗时,需要进行数据下载
print(data.data.shape)
##(581012, 54)
print(data.target)
#[5 5 2 ... 3 3 3]

X = data.data[:2000]
print(X.shape)
#(2000, 54)
y = data.target[:2000]
print(np.unique(y))
#[1 2 3 4 5 6 7]

Xtrain, Xtest, Ytrain, Ytest = TTS(X,y,test_size=0.3,random_state=420)

clf = model.fit(Xtrain,Ytrain)

print(clf.score(Xtrain, Ytrain))
#0.735
print(clf.score(Xtest, Ytest))
#0.7083333333333334

poly = PF(degree=2, interaction_only=True).fit(Xtrain) #不包含各特征的平方项

Xtrain_ = poly.transform(Xtrain)
Xtest_ = poly.transform(Xtest)

print(Xtrain_.shape)
#(1400, 1486)
#我们可以看出来特征维度从54增长为1486个,此时我们就要看看是否存在训练过拟合的问题

clf_ = model.fit(Xtrain_, Ytrain)

print(clf_.score(Xtrain_, Ytrain))
#0.8128571428571428
print(clf_.score(Xtest_, Ytest))
#0.7583333333333333

#从结果可以看出来并不存在过拟合的问题,此时我们就考虑如何将1486转换成两个数值相乘的情况,转换成图片的形式
#但是1486的根号是一个小数,因此我们考虑取1444=38*38的数值,接下来问题就变成了如何取这1444个特征

现在我们总共有1486个特征,我们既可以调整成宽>高,例如52x28的形式,也可以调整为两数相等,取,即38x38的形式。但无论如何,我们只能够选择相乘后的总特征量小于1486的结构(在这个约束下,信息损失最小的结构是39x38)。当然,我们可以选择将特征信息保留最多的形式,例如,令结构为39x38,可最大程度上保留信息。当输入卷积网络后,再使用图像预处理的方式进行特征筛选。但图像处理上的特征筛选大部分是随机的,比较适用于图像数据,却不太适用于表格数据,因此最好的方法还是我们手动筛选特征。假设我们现在,剔除42个特征,令特征总量下降到1444(38x38),然后再将数据整理为四维结构。那我们怎么剔除这42个特征呢?我们利用逻辑回归的权重进行筛选。逻辑回归和线性回归一样,本质上都是单层神经网络,他们的权重可以被追溯到具体的特征值上,而多层神经网络的权重却无法被追溯。因此单层神经网络的权重可以被用来衡量特征的重要性。我们可以令逻辑回归的权重从高到低进行排列,并删掉对模型影响最小的42个特征,再将剩下的特征调整为四维:

#首先我们来看下特征贡献度
print(clf_.coef_.shape)
#(7, 1486)
weights = pd.DataFrame(abs(clf_.coef_).mean(axis=0))
#因此coef是所有特征针对7个标签的贡献程度,因此我们取绝对值之后再去平均值

idx = weights.sort_values(by=0,ascending=False).iloc[:1444,0].index

X_ = poly.transform(X)
X_ = X_[:,idx]
print(X_.shape)
#(2000, 1444)

X_ = X_.reshape(2000, 1, 38, 38)
print(X_.shape)
#2000, 1, 38, 38)

当我们已经获得了四维的数据后,我们就可以使用之前学过的TensorDataset将数据打包了:

#接下来将我们的特征高维矩阵与我们的标签结合起来
from torch.utils.data import TensorDataset

print(y.shape)

data = TensorDataset(torch.tensor(X_), torch.tensor(y))

for x, y in data:
    print(x.shape)
    #torch.Size([1, 38, 38])
    print(y)
    #tensor(5, dtype=torch.int32)
    break


print(data[0])
'''
(tensor([[[   0.,   51.,    0.,  ...,    0.,    0.,    0.],
         [   0.,    0.,    0.,  ...,    0.,    0.,    0.],
         [   0.,    0., 7548.,  ...,    0.,    0.,    0.],
         ...,
         [   0.,    0.,    0.,  ...,    0.,    0.,    0.],
         [   0.,    0.,    0.,  ...,    0.,    0.,    0.],
         [   0.,    0.,    0.,  ...,    0.,    0.,    0.]]],
       dtype=torch.float64), tensor(5, dtype=torch.int32))
'''

#通过验证可知,可以被卷积神经网络识别的图像数据就生成了。

最终生成的结果就是我们的数据,其结构与我们直接从torchvision.datasets读出来的内容非常相似。现在只要将数据放入random_split就可以分割训练集测试集,再放入DataLoader分割批次,就可以导入训练了。

2.3从mat/pt/lmdb到四维tensor

除了二维表格数据和图片数据,我们还可能遇见其他各种各样的数据格式,常见的有matlab中导出的.mat格式,储存图片的pt格式,caffe中常用的数据库格式lmdb等。不同格式需要使用不同的方式进行导入,其中mat格式与pt格式较为简单,具体如下所示:

import torch
from torch.utils.data import TensorDataset

x, y = torch.load(r"/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/FashionMNIST/processed/test.pt")

print(x.shape)
x = x.reshape(10000, 1, 28, 28)
#torch.Size([10000, 28, 28])
print(y)
#tensor([9, 2, 1,  ..., 8, 1, 5])


data = TensorDataset(x, y)

for x,y in data:
    print(x.shape)
    #torch.Size([1, 28, 28])
    print(y)
    #tensor(9)
    break

print(data[0])



#mat格式,SVHN就是mat格式数据集,我们使用scipy中的sio模块进行读取
#通常来说,scipy属于anaconda自带库,无需额外安装。如果你需要安装scipy,搜索pip安装scipy即可
import scipy.io as sio

load_mat = sio.loadmat(r"/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/SVHN/train_32x32.mat")
#print(load_mat)
X = torch.tensor(load_mat['X'])
print(X.shape)
#torch.Size([32, 32, 3, 73257])
#形状不符合卷积网络要求的形状,需要reshape
X = X.reshape(-1, 3, 32, 32) ##(sample, channels, weight, height)
y = load_mat['y']
print(y)
'''
[[1]
 [9]
 [2]
 ...
 [1]
 [6]
 [9]]
'''

lmdb格式文件大多出现在早年的数据中,大部分用于读取lmdb文件的代码也年久失修,就连github上的众多代码也不能顺利跑通,因此我对PyTorch源码稍作修改,构造了用于读取单一的lmdb文件的类ImageFolderLMDB 。通常当我们的数据储存为lmdb格式时,每个lmdb文件中都是单一的标签类别,因
此ImageFolderLMDB中会允许我们输入这个lmdb文件中的标签类别。当我们需要多个类别或多个imdb文件时,我们可以使用ImageFolderLMDB多次读取出不同的数据,然后使用torch.utils.data中的ConcatDataset 类将不同的数据集合并起来。从代码的角度来说,这段代码还有非常多可以优化的地方
(有许多可能的不规范输入没有被限制,当不规范输入发生时,我也没注明有指导意义的报错信息),但限于课时和时间,我们只能将其修缮到能够顺利跑通并执行完整任务的程度。之后我们会持续迭代具体的代码。

#本段代码已超出深度学习范围,仅供使用,不做讲解。
#如果你希望,可以将其保存在torchlearning.py文件中方便导入

import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import six
import string
import pickle
import bisect
import lmdb
from PIL import Image

import torch
from torch.utils.data import DataLoader, Dataset, IterableDataset
from torchvision.transforms import transforms
from torchvision.datasets import ImageFolder
from torchvision import transforms, datasets

class ImageFolderLMDB(Dataset):
    """
    用于从单一lmdb文件中提取出数据集的类
    只适用于lmdb文件中只包含一个标签类别的情况
    不同的标签类别需要使用不同的ImageFolderLMDB进行提取
    """
    def __init__(self, db_path, classes: int, transform=None, target_transform=None):
        """
        参数说明
        db_path: 字符串,需要读取的lmdb文件所在的根目录
        classes: int,给现有数据集打上的单一标签。注意该标签是人工标注的,不一定是数据中心的客观标签
        """
        super().__init__()
        self.db_path = db_path
        self.classes = classes
        self.transform = transform
        self.target_transform = target_transform

        #首先使用lmdb库从lmdb文件中提出数据集
        self.env = lmdb.open(db_path, max_readers=1, readonly=True, lock=False,
                             readahead=False, meminit=False)
        with self.env.begin(write=False) as txn:
            self.length = txn.stat()['entries']
        cache_file = '_cache_' + ''.join(c for c in db_path if c in string.ascii_letters)
        if os.path.isfile(cache_file):
            self.keys = pickle.load(open(cache_file, "rb"))
        else:
            with self.env.begin(write=False) as txn:
                self.keys = [key for key in txn.cursor().iternext(keys=True, values=False)]
            pickle.dump(self.keys, open(cache_file, "wb"))
	
    def __getitem__(self, index):
        img, target = None, None
        env = self.env
        with env.begin(write=False) as txn:
            imgbuf = txn.get(self.keys[index])

        # 导入图像
        buf = six.BytesIO()
        buf.write(imgbuf)
        buf.seek(0)
        img = Image.open(buf).convert('RGB')

        # 导入标签
        target = self.classes

        if self.transform is not None:
            img = self.transform(img)

        if self.target_transform is not None:
            target = self.target_transform(target)

        return img, target

    def __len__(self):
        return self.length

    def __repr__(self):
        return self.__class__.__name__ + ' (' + self.db_path + ')'

我们在LSUN数据集上试试这两个类:

from torch.utils.data import ConcatDataset

#先使用ImageFolderLMDB单独读取数据中的两个类别
data_church = ImageFolderLMDB(r"F:\datasets2\lsun-master\data\church_outdoor_train_lmdb",classes=0
                      ,transform=transforms.ToTensor())

print(data_church[0][0].shape)

for x,y in data_church:
    print(x.shape)
    print(y)
    break

data_classroom = ImageFolderLMDB(r"F:\datasets2\lsun-master\data\classroom_train_lmdb",classes=1
                      ,transform=transforms.ToTensor()


print(data_classroom[0][0].shape)

#使用ConcatDataset将其合并
data = ConcatDataset([data_church,data_classroom])

#数据尺寸已经超出了可以做循环的程度
print(data.__len__())
print(data_church.__len__())
print(data_classroom.__len__())

print(data[120000])
print(data[160000])

3 图片数据的基本预处理与数据增强

3.1数据预处理

当顺利导入数据后,我们就可以依据图像的具体情况对图像进行预处理了。与机器学习中较为固定的预处理流程不同,图像的预处理基本完全与数据本身有关。从数据采集的瞬间开始,我们就需要考虑预处理的事项。如果我们的数据是自行从网络爬取或搜索引擎采集,我们可能需要对图像进行去重、删除无效样本等操作,如果数据是自行拍摄、实验提取,那可能也需要根据实验要求进行一些删除、增加的处理。当我们将所有有效数据导入后,我们至少需要确保:

  1. 全部样本的尺寸是一致的(同时,全部样本的通道数是一致的)
  2. 图像最终以Tensor形式被输入卷积网络
  3. 图像被恰当地归一化

其中,前两项是为了卷积神经网络能够顺利地运行起来,第三项是为了让训练过程变得更加流畅快速。在PyTorch中,所有的数据预处理都可以在导入数据的时候,通过transform参数来完成,我们通常在transform参数中填写torchvision.transforms这个模块下的类。在预处理时,我们需要使用的常规类如下所示:

说明
Composetransform专用的,类似于nn.Sequential的打包功能,可以将数个transforms以下的类打包,形成类似于管道的结构来统一执行。
CenterCrop中心裁剪。需要输入最终希望得到的图像尺寸。
Resize尺寸调整。需要输入最终希望得到的图像尺寸。注意跟centercrop的区别在于Resize是使用裁剪缩小尺寸或者使用填充放大尺寸的。
Normalize归一化(Tensor Only)。对每张图的每个通道进行归一化,每个通道上的每个像素会减去通道像素值的平均值,并处以该通道像素值的方差。
ToTensor(PIL Only)将任意图片转化为Tensor格式。

中心裁剪:transforms.CenterCrop()

无论使用怎样的卷积网络,我们都倾向于将图像调整到接近28x28或224x224的尺寸。当原图尺寸与目标尺寸较为接近时,我们可以使用“裁剪”功能。裁剪是会按照我们输入的目标尺寸,将大于目标尺寸的
像素点丢弃的功能,因此使用裁剪必然会导致信息损失,过多的信息损失会导致卷积网络的结果变差。当需要检测或识别的对象位于图像的中心时,可以使用中心裁剪。中心裁剪会以图像中心点为参照,按照输入的尺寸从外向内进行裁剪,被裁剪掉的像素会被直接丢弃。如果输入的尺寸大于原始图像尺寸,则在原始图像外侧填充0,再进行中心裁剪。

调整尺寸:transforms.Resize()

当图像尺寸与目标尺寸相差较大,我们不能接受如此多的信息被丢弃时,就需要使用类Resize。Resize使用像素聚类、像素插补等一定程度上对信息进行提取或选择、并按要求的尺寸重排像素点的功能。一般来说,Resize过后的图片会呈现出与原图较为相似的信息,但图片尺寸会得到缩放。如果原始图像尺寸很大,目标尺寸很小,一般会优先使用Resize将图像尺寸缩小到接近目标尺寸的程度,在用裁剪让图片尺寸完全等于目标尺寸。例如:600*800的图像,先Resize将尺寸讲到256x256,再裁剪到224x224。

import torch
import torchvision
from torchvision import transforms
from torch import nn

transform = transforms.Compose([transforms.Resize(256),
                               transforms.CenterCrop(224)])

transform = nn.Sequential(transforms.Resize(256),
                          transforms.CenterCrop(224))

数据归一化:transforms.Normalize()

从理论上来说,图像数据的归一化不是必须的,但历史的经验告诉我们,归一化能够非常有效地改善整体训练过程速度,并对最终模型的结果造成巨大的影响,因此各大经典架构的论文和PyTorch官方都强烈建议我们进行归一化。这里的归一化与BN等训练过程中存在的归一化有较大的区别,这里的归一化主要是让像素值减去一个数(默认为均值)、再除以另一个数(默认是标准差),以实现对像素值大小的改变,让模型在一个较高的起点上训练,但并不能像BN一样改变数据的分布。

对表格数据而言,归一化是以特征为单位进行的,每个特征会单独减去自己这个特征的均值,再除以这个特征的标准差。对任意图像而言,归一化都是以通道为单位进行的,每个通道上的全部样本的全部像素点会减去通道像素的均值,再除以通道像素的标准差。为了能够对通道上的全部像素进行计算,图像在被归一化之前必须被转化为Tensor。因此在实际中,我们常常将transforms.Normalize() 常常和transforms.ToTensor() 连用,具体如下:

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

其实transforms.ToTensor()本身已经带有归一化功能,这个类按照最大值255,最小值0对图片进行归一化,将所有图像的像素值压缩在[0,1]之间。

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     ,transform=transforms.ToTensor())

print(data_val[0][0].max())
#tensor(1.)
print(data_val[0][0].min())
#tensor(0.)

因此类transforms.Normalize() 往往是在[0,1]区间上执行。唯一的例外可能是表格数据,如果输入transforms.ToTensor() 的数据原本是二维表,那其最大值可能会远远超出255,那经过归一化后数字范围也不会在[0,1]之间。为了避免这种情况的出现,我们可以提前将二维表的数据压缩到[0,255]之间。

在类transforms.Normalize() 中有两个参数,一个是mean,另一个是std,分别代表需要减去的值和需要除以的值。比较常见的填写方式有以下三种:

#1) 常见且通用的做法,该写法只适用于三通道图像
transforms.Normalize(mean=[0.5, 0.5, 0.5], #代表三个通道上需要减去的值分别是0.5
std=[0.5, 0.5, 0.5]) #代表三个通道上需要除以的值分别是0.5
#在保证数据范围在[0,1]的前提下,使用这个值可以令数据范围拓展到[-1,1]


#也可写作:
transforms.Normalize(0.5,0.5)
#这种写法中,Normalize类会根据通道数进行相应的计算,任意通道数的图像都可以使用
#注意区分,这种写法只能用于单通道(灰度)图像
transforms.Normalize([0.5],[0.5])


#2) ImageNet数据集上的均值和方差,可被用于任意实物照片分类
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])

#3) MNIST数据集上的均值和方差,可被用于MNIST系列
transforms.Normalize((0.1307), (0.3081))
#你也可以根据自己的数据集和自己希望实现的数值范围,来计算放入Normalize的值

#在LSUN数据集上尝试一下
transform2 = transforms.Compose([transforms.ToTensor(),
                                 transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                                                      std=[0.229, 0.224, 0.225])])

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     ,transform=transform2)

print(data_val[0][0].max())
#tensor(2.6400)
print(data_val[0][0].min())
#tensor(-2.1179)


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


data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     ,transform=transform3)

print(data_val[0][0].max())
#tensor(1.)
print(data_val[0][0].min())
#tensor(-1.)

对图像而言,必须完成的预处理就只有尺寸调整和归一化而已。接下来我们来看数据增强(dataaugmentation)。

3.1数据增强

在讲解“不变性”时,我们曾详细地介绍过数据增强技术。数据增强是数据科学体系中常用的一种增加数据量的技术,它通过添加略微修改的现有数据、或从现有数据中重新合成新数据来增加数据量。使用数据增强技术可以极大程度地减弱数据量不足所带来的影响,还可以提升模型的鲁棒性、为模型提供各种“不变性”、增加模型抗过拟合的能力。常见的数据增强手段如下:
在这里插入图片描述
以及水平、垂直、镜面翻转:
在这里插入图片描述
在PyTorch当中,只要利用torchvision.transforms中包含的类,就能够很容易地实现几乎所有常见的增强手段。如下图所示,torchvision.transforms下的类可以分为四大类别:尺寸变化、像素值变化、视角变化以及其他变化。在能够让尺寸变化的类中,各类随机裁剪图像的类可以支持数据增强中的“缩放”功能(可放大,可缩小)。通常来说,如果裁剪是“随机”的,这个类一定是被用于数据增强,而不是被用
于数据预处理的。这其中最常用的是transforms.RandomCrop() ,常常被放在transforms.Resize() 后面替代中心裁剪。
在这里插入图片描述

在负责像素变化的类中,色彩抖动一个类就包含了亮度、对比度、饱和度、色相四种控制颜色的关键指标。同样的,各类“随机”调整色彩或清晰度的类一定都是被用于数据增强的。调整颜色的类都需要输入相当多的参数,因此需要相当多传统视觉领域的知识,相比之下放射变换、线性变换这些基于数学逻辑的类更容易理解。同时,灰度图像上能够做的色彩调整很有限。因此在像素变化类别中,最常用的类除了归一化,就是transforms.RandomAffine() 。
在这里插入图片描述
这里是负责变形、透视、旋转等数据增强方法的类,被使用的频率相当高、每个类都很常用:
在这里插入图片描述
最后剩下的是用于转换格式、或将数据处理流程打包的类,其中最常用的就是transforms.ToTensor() 。

在这里插入图片描述

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     #,transform=transform3
                                     )


image = data_val[0][0]
image.show()

请添加图片描述

transform_aug = transforms.Compose([transforms.Resize(256),
                                    transforms.RandomCrop(size=224),
                                    transforms.RandomHorizontalFlip(p=1)
                                    ])

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     ,transform=transform_aug
                                     )

#查看修改过后的图片
image = data_val[0][0]
image.show()
#对于景色数据,水平翻转和随机裁剪都可能会比较有利
#因为建筑可能位于图像的任何地方,而根据尝试,水平翻转后的图像也能够被一眼看出是什么景色

请添加图片描述
可以尝试更换不同的数据增强方式,查看数据增强的各个操作如何改变图像。在实际执行代码时,我们往往将数据增强和数据预处理的代码写在一起,如下所示:

transform_aug = transforms.Compose([transforms.Resize(256),
                                    transforms.RandomCrop(size=224),
                                    transforms.RandomHorizontalFlip(p=1),
                                    transforms.RandomRotation(degrees=(-70, 70)),
                                    transforms.ToTensor(),
                                    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                                         std=[0.229, 0.224, 0.225])
                                    ])

data_val = torchvision.datasets.LSUN(root="/Users/gaoyuxing/Desktop/all_file/torchvision_dataset/LSUN/data"
                                     ,classes=["church_outdoor_val","classroom_val"]
                                     ,transform=transform_aug
                                     )

#查看修改过后的图片
print(data_val[0][0].shape)
#torch.Size([3, 224, 224])

数据增强是增加数据量的技术,而上面的操作哪里增加数据量了呢?

看上去这些进行数据增强的类只是对数据集进行了一个整体的转化而已,并没有真正实现“数据增强”。

回顾我们构建的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就会一致,这就和一次性对数据进行处理后再带入训练没有区别,这会让数据增强操作失去意义。但无法控制的随机性可能意味着模型的效果会略为不稳定,对写论文或上线之前进行测试的代码来说,每次运行都得出迥然不同的结果显然是很令人头疼的。因此使用数据增强的模型往往只能够得到一个“结果的范围”,论文中报告的往往是这个范围的上限。


总结

本篇主要学习了图像视觉中常见的数据、如何将自己的数据/图片转换成神经网络能够识别的数据(图像——>四维tensor、二维表csv/txt——>四维tensor、mat/pt/lmdb——>四维tensor)、以及对图片数据的基本与处理与数据增强。下一篇章主要介绍训练与算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值