TorchVision

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

简单来说,Torchvision库就是:常用数据集 + 常见网络模型 + 常用图像处理方法

Dataset类

PyTorch中的Dataset类是一个抽象类,它可以用来表示数据集。我们通过继承Dataset类来自定义数据集的格式、大小和其它属性,后面就可以供DataLoader类直接使用

当我们自定义数据集时,都"必须"继承Dataset类(这是一种规范或约束)。而在继承Dataset类时,至少需要重写以下几个方法:

  • __init__():构造函数,可自定义数据读取方法以及进行数据预处理
  • __len__():返回数据集大小
  • __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]


# 生成数据
data_tensor = torch.arange(30).reshape((10, 3))  # 数据是10行3列
target_tensor = torch.arange(1, 11)  # 标签是1到10

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

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

'''
使用索引调用数据
tensor_data[0]:  (tensor([0, 1, 2]), tensor(1))
'''
print('tensor_data[0]: ', my_dataset[0])

DataLoader类

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

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

import torch
from torch.utils.data import Dataset, DataLoader


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]


# 生成数据
data_tensor = torch.arange(30).reshape((10, 3))  # 数据是10行3列
target_tensor = torch.arange(1, 11)  # 标签是1到10

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

tensor_dataloader = DataLoader(dataset=my_dataset,  # 传入的数据集, 必须参数
                               batch_size=2,  # int类型,每个batch有多少个样本
                               shuffle=True,  # bool类型,在每个epoch开始的时候,是否对数据进行重新打乱
                               num_workers=0)  # int类型,加载数据的进程数,0意味着所有的数据都会被加载进主进程,默认为0

'''
以循环形式输出
tensor([[27, 28, 29],
        [24, 25, 26]]) tensor([10,  9])
tensor([[21, 22, 23],
        [ 6,  7,  8]]) tensor([8, 3])
tensor([[12, 13, 14],
        [ 3,  4,  5]]) tensor([5, 2])
tensor([[ 0,  1,  2],
        [18, 19, 20]]) tensor([1, 7])
tensor([[15, 16, 17],
        [ 9, 10, 11]]) tensor([6, 4])
'''
for data, target in tensor_dataloader:
    print(data, target)

'''
输出单个batch
One batch tensor data:  [tensor([[21, 22, 23],
        [ 3,  4,  5]]), tensor([8, 2])]
'''
print('One batch tensor data: ', next(iter(tensor_dataloader)))

利用Torchvision读取数据

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

详情

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

MNIST数据集是NIST数据集的一个子集,它包含了四个部分:

  • 训练集图片:train-images-idx3-ubyte.gz
  • 训练集标签:train-labels-idx1-ubyte.gz
  • 测试集图片:t10k-images-idx3-ubyte.gz
  • 测试集标签:t10k-labels-idx1-ubyte.gz
import torchvision

'''
root:数据保存路径
train:如果为True,则只加载训练数据。如果为False,则只加载测试数据集。这里需要注意,并不是所有的数据集都做了训练集和测试集的划分
transform:用于对图像进行预处理操作,例如数据增强、归一化、旋转或缩放等
target_transform:用于对图像标签进行预处理操作
download:如果为True,则会自动从网上下载这个数据集,存储到root指定的位置。如果指定位置已经存在数据集文件,则不会重复下载
'''
mnist_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=None, target_transform=None,
                                           download=True)
# [(<PIL.Image.Image image mode=L size=28x28 at 0x13BF80070>, 5), (<PIL.Image.Image image mode=L size=28x28 at 0x13BF800A0>, 9)...]
print(list(mnist_dataset))
# 元祖的第0个是一个PIL图片对象
mnist_dataset[0][0].show()
# 元祖的第1个是标签
print(mnist_dataset[0][1])

对于不同的数据集,数据格式都不尽相同,而torchvision.datasets则帮助我们完成了各种不同格式的数据的解析与读取,可以说十分便捷

而对于那些没有官方接口的图像数据集,我们也可以使用以torchvision.datasets.ImageFolder接口来自行定义

图像处理工具之torchvision.transforms

TorchVision库中的torchvision.transforms包中提供了常用的图像操作,包括对Tensor及PILImage对象的操作,例如:随机切割、旋转、数据类型转换等

按照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(默认值),则根据输入数据的维度进行推断:

  • 输入为3通道:mode为'RGB'
  • 输入为4通道:mode为'RGBA'
  • 输入为2通道:mode为'LA'
  • 输入为单通道:mode根据输入数据的类型确定具体模式
from PIL import Image
from torchvision import transforms

img = Image.open('./test.jpg')
# PIL.Image转换为Tensor
img1 = transforms.ToTensor()(img)
print(type(img1))

# Tensor转换为PIL.Image
img2 = transforms.ToPILImage()(img1)
print(type(img2))

Resize

from PIL import Image
from torchvision import transforms

'''
定义一个Resize操作
size:期望输出的尺寸。如果size是一个像(h,w)这样的元组,则图像输出尺寸将与之匹配。如果size是一个int类型的整数,图像较小的边将被匹配到该整数,另一条边按比例缩放
interpolation:插值算法,int类型,默认为2,表示PIL.Image.BILINEAR
'''
resize_img_oper = transforms.Resize((200, 200), interpolation=2)
# 原图
orig_img = Image.open('./test.jpg')
orig_img.show()

# Resize操作后的图
img = resize_img_oper(orig_img)
img.save('./test1.jpg')

torchvision.transforms提供了多种裁剪方法,例如中心裁剪、随机裁剪、四角和中心裁剪等

其中,size表示期望输出的剪裁尺寸。如果size是一个像(h,w)这样的元组,则剪裁后的图像尺寸将与之匹配。如果size是int类型的整数,剪裁出来的图像是(size,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('test.jpg')

# 中心裁剪
img1 = center_crop_oper(orig_img)
img1.save("test_center.jpg")

# 随机裁剪
img2 = random_crop_oper(orig_img)
img2.save("test_random.jpg")

# 四角和中心裁剪
imgs = five_crop_oper(orig_img)
for k, img in enumerate(imgs):
    img.save(f"test_{k}.jpg")

torchvision.transforms提供了两种翻转操作,分别是:以某一概率随机水平翻转图像和以某一概率随机垂直翻转图像

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('test.jpg')

# 水平翻转
img1 = h_flip_oper(orig_img)
img1.show()

# 垂直翻转
img2 = v_flip_oper(orig_img)
img2.show()

对Tensor进行变换

针对Tensor的变换操作很少,目前只有4个,分别是LinearTransformation(线性变换)、Normalize(标准化)、RandomErasing(随机擦除)、ConvertImageDtype(格式转换)

以标准化为例,标准化是指每一个数据点减去所在通道的平均值,再除以所在通道的标准差,数学的计算公式如下:

output = (input − mean) / std

而对图像进行标准化,就是对图像的每个通道利用均值和标准差进行正则化。这样做的目的,是为了保证数据集中所有的图像分布都相似,这样在训练的时候更容易收敛,既加快了训练速度,也提高了训练效果

标准化是一个常规做法,可以理解为无脑进行标准化后再训练的效果,大概率要好于不进行标准化

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

from PIL import Image
from torchvision import transforms

'''
定义标准化操作
mean:表示各通道的均值
std:表示各通道的标准差
inplace:表示是否原地操作,默认为否
'''
norm_oper = transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

# 原图
orig_img = Image.open('test.jpg')
orig_img.show()

# 图像转化为Tensor
img_tensor = transforms.ToTensor()(orig_img)

# 标准化
tensor_norm = norm_oper(img_tensor)

# Tensor转化为图像
img_norm = transforms.ToPILImage()(tensor_norm)
img_norm.show()

变换的组合

Compose类可以将多个变换组合到一起,比如:我们想要将图片变为200×200像素大小,并且随机裁切成80像素的正方形。那么我们可以组合Resize和RandomCrop变换

from PIL import Image
from torchvision import transforms

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

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

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

结合datasets使用

我们还是以读取MNIST数据集为例,看下如何在读取数据的同时,完成数据预处理等操作

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)
# (torch.Tensor, label)
print(item)

torchvision.models模块

Torchvision中的各种经典网络结构以及训练好的模型,都放在了torchvision.models模块中

这些网络模型可以解决以下四大类问题:图像分类、图像分割、物体检测和视频分类

  • 图像分类:指的是单纯把一张图片判断为某一类,如将下图左侧第一张判断为cat
  • 物体检测:首先检测出物体的位置,还要识别出对应物体的类别,如下图中间的那张图
  • 图像分割:对图像中每一个像素点进行分类,确定每个点的类别,从而进行区域划分,如下图右边的那张图

实例化一个GoogLeNet网络

GoogLeNet是Google推出的基于Inception模块的深度神经网络模型。GoogLeNet获得了2014年的ImageNet竞赛的冠军,并且相比之前的AlexNet、VGG等结构能更高效地利用计算资源

GoogLeNet也被称为Inception V1,在随后的两年中它一直在改进,形成了Inception V2、Inception V3等多个版本

我们可以使用随机初始化的权重,创建一个GoogLeNet模型

import torchvision.models as models
googlenet = models.googlenet()

这时候的GoogLeNet模型,相当于只有一个实例化好的网络结构,里面的参数都是随机初始化的,需要经过训练之后才能使用,并不能直接用于预测

torchvision.models模块除了包含了定义好的网络结构,还为我们提供了预训练好的模型,我们可以直接导入训练好的模型来使用

import torchvision.models as models
googlenet = models.googlenet(pretrained=True)

torchvision.models模块中所有预训练好的模型,都是在ImageNet数据集上训练的,它们都是由PyTorch的torch.utils.model_zoo模块所提供的

如果之前没有加载过带预训练参数的网络,在实例化一个预训练好的模型时,模型的参数会被下载至缓存目录中,下载一次后不需要重复下载

这个缓存目录可以通过环境变量TORCH_MODEL_ZOO来指定。当然,你也可以把自己下载好的模型,然后复制到指定路径中

模型微调

实例化了带预训练参数的网络有什么用呢?其实它除了可以直接用来做预测使用,还可以基于它做网络模型的微调,也就是fine-tuning

举个例子,假设你的老板给布置了一个有关于图片分类的任务,数据集是关于狗狗的图片,让你区分图片中狗的种类,例如金毛、柯基、边牧等

问题是数据集中狗的类别很多,但数据却不多。你发现从零开始训练一个图片分类模型,但这样模型效果很差,并且很容易过拟合

这种问题该如何解决呢?可以用已经在ImageNet数据集上训练好的模型来达成你的目的

例如上面我们已经实例化的GoogLeNet模型,只需要使用我们自己的数据集,重新训练网络最后的分类层,即可得到区分狗种类的图片分类模型

模型微调,简单来说就是先在一个比较通用、宽泛的数据集上进行大量训练得出了一套参数,然后再使用这套预训练好的网络和参数,在自己的任务和数据集上进行训练

使用经过预训练的模型,要比使用随机初始化的模型训练效果更好,更容易收敛,并且训练速度更快,在小数据集上也能取得比较理想的效果

ImageNet数据集共有1000个类别,而狗的种类远远达不到1000类。因此,加载了预训练好的模型之后,还需要根据你的具体问题对模型或数据进行一些调整,通常来说是调整输出类别的数量

假设狗的种类一共为10类,那么我们自然需要将GoogLeNet模型的输出分类数也调整为10。对预训练模型进行调整

首先,你需要加载预训练模型,然后提取预训练模型的分类层固定参数,最后修改预训练模型的输出分类数为10。根据输出结果,我们可以看到预训练模型的原始输出分类数是1000

import torch
import torchvision.models as models

# 加载预训练模型
googlenet = models.googlenet()

# 提取分类层的输入参数
fc_in_features = googlenet.fc.in_features
print("fc_in_features:", fc_in_features)

# 查看分类层的输出参数
fc_out_features = googlenet.fc.out_features
print("fc_out_features:", fc_out_features)

# 修改预训练模型的输出分类数
googlenet.fc = torch.nn.Linear(fc_in_features, 10)
print("fc_out_features_new:", googlenet.fc.out_features)
'''
输出:
fc_in_features: 1024
fc_out_features: 1000
fc_out_features_new:10
'''

注意,如果是加载预训练模型的话,我们可以把前面的参数锁死,只训练最后一层全连接层

googlenet = models.googlenet()
googlenet.load_state_dict(torch.load('./model/googlenet.pth'))
for param in googlenet.parameters():
    param.requires_grad = False

其他常用函数

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 = next(data_iter)
# torch.Size([32, 1, 28, 28])
print(img_tensor.shape)

'''
将32张图片拼接在一个网格中
tensor:类型是Tensor或列表,如果输入类型是Tensor,其形状应是(BxCxHxW);如果输入类型是列表,列表中元素应为相同大小的图片
nrow:表示一行放入的图片数量,默认为8
padding:子图像与子图像之间的边框宽度,默认为2像素
'''
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=8, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
grid_img.show()

save_img:Torchvision提供了save_image函数,能够直接将Tensor保存为图片,即使Tensor数据在CUDA上,也会自动移到CPU中进行保存

import torchvision

'''
tensor:类型是Tensor或列表,如果输入类型是Tensor,直接将Tensor保存;如果输入类型是列表,则先调用make_grid函数生成一张图片的Tensor,然后再保存
fp:保存图片的文件名
**kwargs:make_grid函数中的参数,前面已经讲过了
'''
torchvision.utils.save_image(tensor, fp, **kwargs)

# 输入为一张图片的tensor 直接保存
torchvision.utils.save_image(grid_tensor, 'grid.jpg')

# 输入为List 调用grid_img函数后保存
torchvision.utils.save_image(img_tensor, 'grid2.jpg', nrow=5, padding=2)

results matching ""

    No results matching ""