深度学习基础-pytorch1

发布时间 2023-04-04 18:43:56作者: ZZX11

DataSet DataLoader Torchvision 数据读取

训练开始的第一步,首先就是数据读取。PyTorch 为我们提供了一种十分方便的数据读取机制,即使用 Dataset 类与 DataLoader 类的组合,来得到数据迭代器。在训练或预测时,数据迭代器能够输出每一批次所需的数据,并且对数据进行相应的预处理与数据增强操作。

Dataset 类

PyTorch 中的 Dataset 类是一个抽象类,它可以用来表示数据集。

我们通过继承 Dataset 类来自定义数据集的格式、大小和其它属性,后面就可以供 DataLoader 类直接使用。其实这就表示,无论使用自定义的数据集,还是官方为我们封装好的数据集,其本质都是继承了 Dataset 类。

而在继承 Dataset 类时,至少需要重写以下几个方法:__init__():构造函数,可自定义数据读取方法以及进行数据预处理;__len__():返回数据集大小;__getitem__():索引数据集中的某一个数据。光看原理不容易理解,下面我们来编写一个简单的例子,看下如何使用 Dataset 类定义一个 Tensor 类型的数据集。

结合代码可以看到,我们定义了一个名字为 MyDataset 的数据集,在构造函数中,传入 Tensor 类型的数据与标签;在 __len__ 函数中,直接返回 Tensor 的大小;在__getitem__函数中返回索引的数据与标签。

import torch
from torch.utils.data import Dataset

class MyDataset(Dataset):
    # 构造函数
    def __init__(self, data_tensor, target_tensor):
        self.data_tensor = data_tensor
        self.target_tensor = target_tensor
    # 返回数据集大小
    def __len__(self):
        return self.data_tensor.size(0)
    # 返回索引的数据与标签
    def __getitem__(self, index):
        return self.data_tensor[index], self.target_tensor[index]

下面,我们来看一下如何调用刚才定义的数据集。

首先随机生成一个 10*3 维的数据 Tensor,然后生成 10 维的标签 Tensor,与数据 Tensor 相对应。利用这两个 Tensor,生成一个 MyDataset 的对象。查看数据集的大小可以直接用 len() 函数,索引调用数据可以直接使用下标。

# 生成数据
data_tensor = torch.randn(10, 3)
target_tensor = torch.randint(2, (10,)) # 标签是0或1

# 将数据封装成Dataset
my_dataset = MyDataset(data_tensor, target_tensor)

# 查看数据集大小
print('Dataset size:', len(my_dataset))
'''
输出:
Dataset size: 10
'''

# 使用索引调用数据
print('tensor_data[0]: ', my_dataset[0])
'''
输出:
tensor_data[0]:  (tensor([ 0.4931, -0.0697,  0.4171]), tensor(0))
'''

DataLoader 类

在实际项目中,如果数据量很大,考虑到内存有限、I/O 速度等问题,在训练过程中不可能一次性的将所有数据全部加载到内存中,也不能只用一个进程去加载,所以就需要多进程、迭代加载,而 DataLoader 就是基于这些需要被设计出来的。

DataLoader 是一个迭代器,最基本的使用方法就是传入一个 Dataset 对象,它会根据参数 batch_size 的值生成一个 batch 的数据,节省内存的同时,它还可以实现多进程、数据打乱等处理

DataLoader 类的调用方式如下:

from torch.utils.data import DataLoader
tensor_dataloader = DataLoader(dataset=my_dataset, # 传入的数据集, 必须参数
                               batch_size=2,       # 输出的batch大小
                               shuffle=True,       # 数据是否打乱
                               num_workers=0)      # 进程数, 0表示只有主进程

# 以循环形式输出
for data, target in tensor_dataloader: 
    print(data, target)
'''
输出:
tensor([[-0.1781, -1.1019, -0.1507],
        [-0.6170,  0.2366,  0.1006]]) tensor([0, 0])
tensor([[ 0.9451, -0.4923, -1.8178],
        [-0.4046, -0.5436, -1.7911]]) tensor([0, 0])
tensor([[-0.4561, -1.2480, -0.3051],
        [-0.9738,  0.9465,  0.4812]]) tensor([1, 0])
tensor([[ 0.0260,  1.5276,  0.1687],
        [ 1.3692, -0.0170, -1.6831]]) tensor([1, 0])
tensor([[ 0.0515, -0.8892, -0.1699],
        [ 0.4931, -0.0697,  0.4171]]) tensor([1, 0])
'''
 
# 输出一个batch
print('One batch tensor data: ', iter(tensor_dataloader).next())
'''
输出:
One batch tensor data:  [tensor([[ 0.9451, -0.4923, -1.8178],
        [-0.4046, -0.5436, -1.7911]]), tensor([0, 0])]
'''

结合代码,我们梳理一下 DataLoader 中的几个参数,它们分别表示:

  • dataset:Dataset 类型,输入的数据集,必须参数;
  • batch_size:int 类型,每个 batch 有多少个样本;
  • shuffle:bool 类型,在每个 epoch 开始的时候,是否对数据进行重新打乱;
  • num_workers:int 类型,加载数据的进程数,0 意味着所有的数据都会被加载进主进程,默认为 0。

Torchvision

PyTroch 官方为我们提供了一些常用的图片数据集,如果你需要读取这些数据集,那么无需自己实现,只需要利用 Torchvision 就可以搞定。

Torchvision 是一个和 PyTorch 配合使用的 Python 包。它不只提供了一些常用数据集,还提供了几个已经搭建好的经典网络模型,以及集成了一些图像数据处理方面的工具,主要供数据预处理阶段使用。简单地说,Torchvision 库就是常用数据集 + 常见网络模型 + 常用图像处理方法。

利用 Torchvision 读取数据

Torchvision 库中的torchvision.datasets包中提供了丰富的图像数据集的接口。常用的图像数据集,例如 MNIST、COCO 等,这个模块都为我们做了相应的封装。

MNIST 数据集是一个著名的手写数字数据集,因为上手简单,在深度学习领域,手写数字识别是一个很经典的学习入门样例。

数据读取

对于torchvision.datasets所支持的所有数据集,它都内置了相应的数据集接口。例如刚才介绍的 MNIST 数据集,torchvision.datasets就有一个 MNIST 的接口,接口内封装了从下载、解压缩、读取数据、解析数据等全部过程。这些接口的工作方式差不多,都是先从网络上把数据集下载到指定目录,然后再用加载器把数据集加载到内存中,最后将加载后的数据集作为对象返回给用户。

以 MNIST 为例,我们可以用如下方式调用:

# 以MNIST为例
import torchvision
mnist_dataset = torchvision.datasets.MNIST(root='./data',
                                       train=True,
                                       transform=None,
                                       target_transform=None,
                                       download=True)

torchvision.datasets.MNIST是一个类,对它进行实例化,即可返回一个 MNIST 数据集对象。

构造函数包括包含 5 个参数:

  • root:是一个字符串,用于指定你想要保存 MNIST 数据集的位置。如果 download 是 Flase,则会从目标位置读取数据集;
  • download:是布尔类型,表示是否下载数据集。如果为 True,则会自动从网上下载这个数据集,存储到 root 指定的位置。如果指定位置已经存在数据集文件,则不会重复下载;
  • train:是布尔类型,表示是否加载训练集数据。如果为 True,则只加载训练数据。如果为 False,则只加载测试数据集。这里需要注意,并不是所有的数据集都做了训练集和测试集的划分,这个参数并不一定是有效参数,具体需要参考官方接口说明文档;
  • transform:用于对图像进行预处理操作,例如数据增强、归一化、旋转或缩放等。这些操作我们会在下节课展开讲解;
  • target_transform:用于对图像标签进行预处理操作。

而对于那些没有官方接口的图像数据集,我们也可以使用以torchvision.datasets.ImageFolder接口来自行定义,在图像分类的实战篇中,就是使用 ImageFolder 进行数据读取的,你可以到那个时候再看一看。

数据预览

完成了数据读取工作,我们得到的是对应的 mnist_dataset,刚才已经讲过了,这是一个封装了的数据集。如果想要查看 mnist_dataset 中的具体内容,我们需要把它转化为列表。

(如果 IOPub data rate 超限,可以只加载测试集数据,令 train=False)

mnist_dataset_list = list(mnist_dataset)
print(mnist_dataset_list)

转换后的数据集对象变成了一个元组列表,每个元组有两个元素,第一个元素是图像数据,第二个元素是图像的标签。

Transform

图像处理工具之 torchvision.transforms

Torchvision 库中的torchvision.transforms包中提供了常用的图像操作,包括对 Tensor 及 PIL Image 对象的操作,例如随机切割、旋转、数据类型转换等等。按照torchvision.transforms 的功能,大致分为以下几类:数据类型转换、对 PIL.Image 和 Tensor 进行变化和变换的组合。下面我们依次来学习这些类别中的操作。

数据类型转换

在上一节课中,我们学习了读取数据集中的图片,读取到的数据是 PIL.Image 的对象。而在模型训练阶段,需要传入 Tensor 类型的数据,神经网络才能进行运算。

那么如何将 PIL.Image 或 Numpy.ndarray 格式的数据转化为 Tensor 格式呢?这需要transforms.ToTensor() 类。

而反之,将 Tensor 或 Numpy.ndarray 格式的数据转化为PIL.Image格式,则使用transforms.ToPILImage(mode=None) 类。它则是 ToTensor 的一个逆操作,它能把 Tensor 或 Numpy 的数组转换成 PIL.Image 对象。其中,参数 mode 代表PIL.Image的模式,如果 mode 为 None(默认值),则根据输入数据的维度进行推断:

torchvision.transforms 提供了丰富的图像变换方法,例如:改变尺寸、剪裁、翻转等。并且这些图像变换操作可以接收多种数据格式,不仅可以直接对 PIL 格式的图像进行变换,也可以对 Tensor 进行变换,无需我们再去做额外的数据类型转换。下面我们依次来看一看。

Resize

将输入的 PIL Image 或 Tensor 尺寸调整为给定的尺寸,具体定义为

torchvision.transforms.Resize(size, interpolation=2)

我们依次看下相关的参数:

size:期望输出的尺寸。如果 size 是一个像 (h, w) 这样的元组,则图像输出尺寸将与之匹配。如果 size 是一个 int 类型的整数,图像较小的边将被匹配到该整数,另一条边按比例缩放。

interpolation:插值算法,int 类型,默认为 2,表示 PIL.Image.BILINEAR。

有关 Size 中是 tuple 还是 int 这一点请你一定要注意。

在 resize 之后呢,一般会接一个 crop 操作,crop 到指定的大小。对于高与宽接近的图片来说,这么做问题不大,但是高与宽的差距较大时,就会 crop 掉很多有用的信息。关于这一点,我们在后续的图像分类部分还会遇到,到时我在详细展开。


from PIL import Image
from torchvision import transforms 

# 定义一个Resize操作
resize_img_oper = transforms.Resize((200,200), interpolation=2)

# 原图
orig_img = Image.open('jk.jpg') 
display(orig_img)
# 注意调用的方法
# Resize操作后的图
img = resize_img_oper(orig_img)
display(img)

剪裁

torchvision.transforms提供了多种剪裁方法,例如中心剪裁、随机剪裁、四角和中心剪裁等。我们依次来看下它们的定义。先说中心剪裁,顾名思义,在中心裁剪指定的 PIL Image 或 Tensor,其定义如下:

其中,size 表示期望输出的剪裁尺寸。如果 size 是一个像 (h, w) 这样的元组,则剪裁后的图像尺寸将与之匹配。如果 size 是 int 类型的整数,剪裁出来的图像是 (size, size) 的正方形。

torchvision.transforms.CenterCrop(size)

然后是随机剪裁,就是在一个随机位置剪裁指定的 PIL Image 或 Tensor,定义如下:

torchvision.transforms.RandomCrop(size, padding=None)
其中,size 代表期望输出的剪裁尺寸,用法同上。而 padding 表示图像的每个边框上的可选填充。默认值是 None,即没有填充。通常来说,不会用 padding 这个参数,至少对于我来说至今没用过。

最后要说的是 FiveCrop,我们将给定的 PIL Image 或 Tensor ,分别从四角和中心进行剪裁,共剪裁成五块,定义如下:

# size 可以是 int 或 tuple,用法同上。
torchvision.transforms.FiveCrop(size)

例子:

from PIL import Image
from torchvision import transforms 
# 定义剪裁操作
center_crop_oper = transforms.CenterCrop((60,70))
random_crop_oper = transforms.RandomCrop((80,80))
five_crop_oper = transforms.FiveCrop((60,70))
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# 中心剪裁
img1 = center_crop_oper(orig_img)
display(img1)#
随机剪裁
img2 = random_crop_oper(orig_img)
display(img2)
# 四角和中心剪裁
imgs = five_crop_oper(orig_img)
for img in imgs: display(img)

翻转

接下来,我们来看一看翻转操作。torchvision.transforms提供了两种翻转操作,

分别是:以某一概率随机水平翻转图像和以某一概率随机垂直翻转图像。我们分别来看它们的定义。

  • 以概率 p 随机水平翻转图像,

定义如下:

torchvision.transforms.RandomHorizontalFlip(p=0.5)
  • 以概率 p 随机垂直翻转图像,

定义如下:

torchvision.transforms.RandomVerticalFlip(p=0.5)

其中,p 表示随机翻转的概率值,默认为 0.5。这里的随机翻转,是为数据增强提供方便。如果想要必须执行翻转操作的话,将 p 设置为 1 即可。

from PIL import Image
from torchvision import transforms 

# 定义翻转操作
h_flip_oper = transforms.RandomHorizontalFlip(p=1)
v_flip_oper = transforms.RandomVerticalFlip(p=1)

# 原图
orig_img = Image.open('jk.jpg') 
display(orig_img)

# 水平翻转
img1 = h_flip_oper(orig_img)
display(img1)
# 垂直翻转
img2 = v_flip_oper(orig_img)
display(img2)

只对 Tensor 进行变换

目前版本的 Torchvision(v0.10.0)对各种图像变换操作已经基本同时支持 PIL Image 和 Tensor 类型了,因此只针对 Tensor 的变换操作很少,只有 4 个,分别是 LinearTransformation(线性变换)、Normalize(标准化)、RandomErasing(随机擦除)、ConvertImageDtype(格式转换)。

这里我们重点来看最常用的一个操作:标准化

标准化

标准化是指每一个数据点减去所在通道的平均值,再除以所在通道的标准差,

数学的计算公式如下:output=(input−mean)/std

而对图像进行标准化,就是对图像的每个通道利用均值和标准差进行正则化。这样做的目的,是为了保证数据集中所有的图像分布都相似,这样在训练的时候更容易收敛,既加快了训练速度,也提高了训练效果。让我来解释一下:首先,标准化是一个常规做法,可以理解为无脑进行标准化后再训练的效果,大概率要好于不进行标准化。

但是计算机(也就是卷积神经网络)不一定能分辨出来一个进行操作的图片和无任何操作的图片,因为卷积神经网络是通过图像的像素进行提取特征的,这两张图片像素的数值都不一样,凭什么还让神经网络认为是一张图片?

而标准化后的数据就会避免这一问题,标准化后会将数据映射到同一区间中,一个类别的图片虽说有的像素值可能有差异,但是它们分布都是类似的分布。

torchvision.transforms.Normalize(mean, std, inplace=False)

其中,每个参数的含义如下所示:

mean:表示各通道的均值;std:表示各通道的标准差;inplace:表示是否原地操作,默认为否。

变换的组合

其实前面介绍过的所有操作都可以用 Compose 类组合起来,进行连续操作。Compose 类是将多个变换组合到一起,它的定义如下。

torchvision.transforms.Compose(transforms)

其中,transforms 是一个 Transform 对象的列表,表示要组合的变换列表。

from PIL import Image
from torchvision import transforms 

# 原图
orig_img = Image.open('jk.jpg') 
display(orig_img)

# 定义组合操作
composed = transforms.Compose([transforms.Resize((200, 200)),
                               transforms.RandomCrop(80)])

# 组合操作后的图
img = composed(orig_img)
display(img)

结合 datasets 使用

Compose 类是未来我们在实际项目中经常要使用到的类,结合torchvision.datasets包,就可以在读取数据集的时候做图像变换与数据增强操作。

在利用torchvision.datasets 读取 MNIST 数据集时,有一个参数“transform”吗?它就是用于对图像进行预处理操作的,例如数据增强、归一化、旋转或缩放等。这里的“transform”就可以接收一个torchvision.transforms操作或者由 Compose 类所定义的操作组合。

我们在读取 MNIST 数据集时,直接读取出来的图像数据是 PIL.Image.Image 类型的。但是遇到要训练手写数字识别模型这类的情况,模型接收的数据类型是 Tensor,而不是 PIL 对象。这时候,我们就可以利用“transform”参数,使数据在读取的同时做类型转换,这样读取出的数据直接就可以是 Tensor 类型了。不只是数据类型的转换,我们还可以增加归一化等数据增强的操作,只需要使用上面介绍过的 Compose 类进行组合即可。这样,在读取数据的同时,我们也就完成了数据预处理、数据增强等一系列操作。

from torchvision import transforms
from torchvision import datasets

# 定义一个transform
my_transform = transforms.Compose([transforms.ToTensor(),
                                   transforms.Normalize((0.5), (0.5))
                                  ])
# 读取MNIST数据集 同时做数据变换
mnist_dataset = datasets.MNIST(root='./data',
                               train=False,
                               transform=my_transform,
                               target_transform=None,
                               download=True)

# 查看变换后的数据类型
item = mnist_dataset.__getitem__(0)
print(type(item[0]))
'''
输出:
<class 'torch.Tensor'>
'''

数据增强的方法有很多,不过根据我的经验来看,并不是用的越多,效果越好。

make_grid

make_grid 的作用是将若干幅图像拼成在一个网格中,它的定义如下。

torchvision.utils.make_grid(tensor, nrow=8, padding=2) 

定义中对应的几个参数含义如下:

  • tensor:类型是 Tensor 或列表,如果输入类型是 Tensor,其形状应是 (B x C x H x W);如果输入类型是列表,列表中元素应为相同大小的图片。

  • nrow:表示一行放入的图片数量,默认为 8。

  • padding:子图像与子图像之间的边框宽度,默认为 2 像素。

    make_grid 函数主要用于展示数据集或模型输出的图像结果。我们以 MNIST 数据集为例,整合之前学习过的读取数据集以及图像变换的内容,来看一看 make_grid 函数的效果。

import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader

# 加载MNIST数据集
mnist_dataset = datasets.MNIST(root='./data',
                               train=False,
                               transform=transforms.ToTensor(),
                               target_transform=None,
                               download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=mnist_dataset,
                               batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape)
'''
输出:torch.Size([32, 1, 28, 28])
'''
# 将32张图片拼接在一个网格中
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=8, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)

结合代码我们可以看到,程序首先利用torchvision.datasets加载 MNIST 的测试集,然后利用 DataLoader 类的迭代器一次获取到 32 张图片的 Tensor,最后利用 make_grid 函数将 32 张图片拼接在了一幅图片中。

MNIST 的测试集中的 32 张图片,如下图所示,这里我要特别说明一下,因为 MNIST 的尺寸为 28x28,注意torch和numpy中关于图像的存储似乎是不同的,在PyTorch里面,对于计算机视觉任务,如果图像数据是3维张量,那么张量维数的意义是 通道数x高x宽;如果是4维张量,那么张量维数的意义是 样本数x通道数x高x宽数据存储和表示方式似乎也不同?

所以测试集里的手写数字图片像素都比较低,但这并不影响咱们动手实践。你可以参照我给到的示范,自己动手试试看。