ch02-PyTorch数据预处理

ch02-PyTorch数据预处理

    • 0.引言
    • 1.数据读取机制 Dataloader 与 Dataset
      • 1.1.纸币二分类
      • 1.2.DataSet与DataLoader
        • 1.2.1.torch.utils.data.DataLoader:构建可迭代的数据装载器
        • 1.2.2.torch.utils.data.Dataset:Dataset抽象类
        • 1.2.3.以人民币分类为例
    • 2.数据预处理 transforms 模块机制
      • 2.1.transforms 的运行机制
        • 2.1.1.transforms: torchvision.transforms : 常用的图像预处理方法
        • 2.1.2.例子:人民币二分类中的 transforms
        • 2.1.3.PyTorch 中的数据预处理流程图
      • 2.2.数据标准化:transforms.Normalize:加速运算
    • 3.transforms图像增强(一)裁剪、翻转及旋转
      • 3.1.数据增强
      • 3.2. transforms--Crop 裁剪
      • 3.3. transforms--翻转、旋转
        • 3.3.1 transforms 翻转
        • 3.3.2 transforms 旋转
      • 3.4. 总结
    • 4.transforms图像增强(二)图像变换、方法操作及自定义方法
      • 4.1. transforms 图像变换
      • 4.2. transforms 选择操作
      • 4.3. 自定义 transforms
    • 5.transforms 方法总结
    • 6.数据增强实战应用

0.引言

1.数据读取机制 Dataloader 与 Dataset

1.1.纸币二分类

ch02-PyTorch数据预处理_第1张图片

  • 图自

ch02-PyTorch数据预处理_第2张图片

任务:训练一个分类模型,使得其能够对第四套人民币中的 1 元和 100 元面额的纸币进行分类。

回顾一下上节课中学习的机器学习的 5 个步骤:

ch02-PyTorch数据预处理_第3张图片
其中,数据模块又可以分为以下子模块:

1.2.DataSet与DataLoader

1.2.1.torch.utils.data.DataLoader:构建可迭代的数据装载器

DataLoader(
    dataset,
    batch_size=1,
    shuffle=False,
    sampler=None,
    batch_sampler=None,
    num_workers=0,
    collate_fn=None,
    pin_memory=False,
    drop_last=False,
    timeout=0,
    worker_init_fn=None,
    multiprocessing_context=None
)

主要参数:

  • dataset: Dataset类,决定数据从哪读取及如何读取
  • batchsize : 批大小
  • num_works: 是否多进程读取数据
  • shuffle: 每个epoch是否乱序
  • drop_last:当样本数不能被batchsize整 除时,是否舍弃最后一批数据相关

相关概念:

  • Epoch: 所有训练样本都已经输入到模型中,称为一个 Epoch
  • Iteration: 一批样本输入到模型中,称为一个 Iteration
  • Batchsize: 批大小,决定一个 iteration 有多少样本,也决定了一个 Epoch 有多少个 Iteration

例如:

  • 训练样本:80
    Batchsize:8
    1 epoch = 10 iteration

  • 训练样本:87
    Batchsize:8
    drop_last = True:1 epoch = 10 iteration
    drop_last = False:1 epoch = 11 iteration

假设样本总数有 80,设置 Batchsize 为 8,则共有 80 ÷ 8 = 10 80 \div 8=10 80÷8=10 个 Iteration。这里 1 Epoch = 10 Iteration。

假设样本总数有 87,设置 Batchsize 为 8。

  • 如果drop_last=True则共有 10 个 Iteration;
  • 如果drop_last=False则共有 11 个 Iteration。

1.2.2.torch.utils.data.Dataset:Dataset抽象类

torch.utils.data.Dataset:Dataset抽象类,所有自定义的 Dataset需要继承它,并重写 __ getitem __() 方法

class Dataset(object):

    def __getitem__(self, index):
        raise NotImplementedError

    def __add__(self, other):
        return ConcatDataset([self, other])

主要参数:

  • getitem:接收一个索引,返回一个样本。

  • 功能:Dataset 是抽象类,所有自定义的 Dataset 都需要继承该类,并且重写__getitem()__方法和__len__()方法 。__getitem()__方法的作用是接收一个索引,返回索引对应的样本和标签,这是我们自己需要实现的逻辑。__len__()方法是返回所有样本的数量。

数据读取包含 3 个方面:

  • 读取哪些数据:每个 Iteration 读取一个 Batchsize 大小的数据,每个 Iteration 应该读取哪些数据。
  • 从哪里读取数据:如何找到硬盘中的数据,应该在哪里设置文件路径参数
  • 如何读取数据:不同的文件需要使用不同的读取方法和库。

1.2.3.以人民币分类为例

这里的路径结构如下,有两类人民币图片:1 元和 100 元,每一类各有 100 张图片。

  • RMB_data
    • 1
    • 100

首先划分数据集为训练集、验证集和测试集,比例为 8:1:1。数据划分好后的路径构造如下:

  • rmb_split
    • train
      • 1
      • 100
    • valid
      • 1
      • 100
    • test
      • 1
      • 100

实现读取数据的 Dataset,编写一个get_img_info()方法,读取每一个图片的路径和对应的标签,组成一个元组,再把所有的元组作为 list 存放到self.data_info变量中,这里需要注意的是标签需要映射到 0 开始的整数: rmb_label = {“1”: 0, “100”: 1}。

def get_img_info(data_dir):
    data_info = list()
    # data_dir 是训练集、验证集或者测试集的路径
    for root, dirs, _ in os.walk(data_dir):
        # 遍历类别
        # dirs ['1', '100']
        for sub_dir in dirs:
            # 文件列表
            img_names = os.listdir(os.path.join(root, sub_dir))
            # 取出 jpg 结尾的文件
            img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))
            # 遍历图片
            for i in range(len(img_names)):
                img_name = img_names[i]
                # 图片的绝对路径
                path_img = os.path.join(root, sub_dir, img_name)
                # 标签,这里需要映射为 0、1 两个类别
                label = rmb_label[sub_dir]
                # 保存在 data_info 变量中
                data_info.append((path_img, int(label)))
    return data_info

然后在Dataset 的初始化函数中调用get_img_info()方法(这里就回答了 2.从哪里读)。

def __init__(self, data_dir, transform=None):
    """
    rmb面额分类任务的Dataset
    :param data_dir: str, 数据集所在路径
    :param transform: torch.transform,数据预处理
    """
    # data_info存储所有图片路径和标签,在DataLoader中通过index读取样本
    self.data_info = self.get_img_info(data_dir)
    self.transform = transform

然后在__getitem__()方法中根据index 读取self.data_info中路径对应的数据,并在这里做 transform 操作,返回的是样本和标签。

def __getitem__(self, index):
    # 通过 index 读取样本
    path_img, label = self.data_info[index]
    # 注意这里需要 convert('RGB')
    img = Image.open(path_img).convert('RGB')     # 0~255
    if self.transform is not None:
        img = self.transform(img)   # 在这里做transform,转为tensor等等
    # 返回是样本和标签
    return img, label

__len__()方法中返回self.data_info的长度,即为所有样本的数量。

    # 返回所有样本的数量
    def __len__(self):
        return len(self.data_info)

train_lenet.py中,分 5 步构建模型。

  • 第 1 步设置数据。首先定义训练集、验证集、测试集的路径,定义训练集和测试集的 transforms。然后构建训练集和验证集的 RMBDataset对象,把对应的路径和transforms传进去。再构建DataLoder,设置 batch_size,其中训练集设置shuffle=True,表示每个 Epoch 都打乱样本。
# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
# 其中训练集设置 shuffle=True,表示每个 Epoch 都打乱样本
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)
  • 第 2 步构建模型,这里采用经典的 Lenet 图片分类网络。
net = LeNet(classes=2)
net.initialize_weights()
  • 第 3 步设置损失函数,这里使用交叉熵损失函数。
criterion = nn.CrossEntropyLoss()
  • 第 4 步设置优化器。这里采用 SGD 优化器。
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)                        # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)     # 设置学习率下降策略
  • 第 5 步迭代训练模型,在每一个 epoch 里面,需要遍历 train_loader 取出数据,每次取得数据是一个 batchsize 大小。这里又分为 4 步。第 1 步进行前向传播,第 2 步进行反向传播求导,第 3 步使用optimizer更新权重,第 4 步统计训练情况。每一个 epoch 完成时都需要使用scheduler更新学习率,和计算验证集的准确率、loss。
for epoch in range(MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    net.train()
    # 遍历 train_loader 取数据
    for i, data in enumerate(train_loader):

        # forward
        inputs, labels = data
        outputs = net(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率
    # 每个 epoch 计算验证集得准确率和loss
    ...
    ...

ch02-PyTorch数据预处理_第4张图片

我们可以看到每个 iteration,我们是从train_loader中取出数据的。

def __iter__(self):
    if self.num_workers == 0:
        return _SingleProcessDataLoaderIter(self)
    else:
        return _MultiProcessingDataLoaderIter(self)

这里我们没有设置多进程,会执行_SingleProcessDataLoaderIter的方法。我们以_SingleProcessDataLoaderIter为例。在_SingleProcessDataLoaderIter里只有一个方法_next_data(),如下:

def _next_data(self):
    index = self._next_index()  # may raise StopIteration
    data = self._dataset_fetcher.fetch(index)  # may raise StopIteration
    if self._pin_memory:
        data = _utils.pin_memory.pin_memory(data)
    return data

在该方法中,self._next_index()是获取一个 batchsize 大小的 index 列表(这里就回答了 1.读取那些数据),代码如下:

def _next_index(self):
    return next(self._sampler_iter)  # may raise StopIteration

其中调用的sampler类的__iter__()方法返回 batch_size 大小的随机 index 列表。

def __iter__(self):
    batch = []
    for idx in self.sampler:
        batch.append(idx)
        if len(batch) == self.batch_size:
            yield batch
            batch = []
    if len(batch) > 0 and not self.drop_last:
        yield batch

然后再返回看 dataloader的_next_data()方法:

def _next_data(self):
    index = self._next_index()  # may raise StopIteration
    data = self._dataset_fetcher.fetch(index)  # may raise StopIteration
    if self._pin_memory:
        data = _utils.pin_memory.pin_memory(data)
    return data

在第二行中调用了self._dataset_fetcher.fetch(index)获取数据。这里会调用_MapDatasetFetcher中的fetch()函数:

def fetch(self, possibly_batched_index):
    if self.auto_collation:
        data = [self.dataset[idx] for idx in possibly_batched_index]
    else:
        data = self.dataset[possibly_batched_index]
    return self.collate_fn(data)

这里调用了self.dataset[idx],这个函数会调用dataset.__getitem__()方法获取具体的数据,所以__getitem__()方法是我们必须实现的(这里就回答了 2.怎么读)。我们拿到的data是一个 list,每个元素是一个 tunple,每个 tunple 包括样本和标签。所以最后要使用self.collate_fn(data)把 data 转换为两个 list,第一个 元素 是样本的batch 形式,形状为 [16, 3, 32, 32] (16 是 batch size,[3, 32, 32] 是图片像素);第二个元素是标签的 batch 形式,形状为 [16]。

所以在代码中,我们使用inputs, labels = data来接收数据。


完整代码:

  • 将数据集划分为训练集、验证集和测试集:
import os
import random
import shutil


def makedir(new_dir):
    if not os.path.exists(new_dir):
        os.makedirs(new_dir)


if __name__ == '__main__':

    random.seed(1)

    # 人民币图片数据所在目录:"../../data/RMB_data"
    dataset_dir = os.path.join("..", "..", "data", "RMB_data")
    # 划分数据集所在目录:"../../data/rmb_split"
    split_dir = os.path.join("..", "..", "data", "rmb_split")
    # 训练集目录:"../../data/rmb_split/train"  
    train_dir = os.path.join(split_dir, "train")
    # 验证集目录:"../../data/rmb_split/valid"  
    valid_dir = os.path.join(split_dir, "valid")
    # 测试集目录:"../../data/rmb_split/test"  
    test_dir = os.path.join(split_dir, "test")  

    train_pct = 0.8
    valid_pct = 0.1
    test_pct = 0.1

    for root, dirs, files in os.walk(dataset_dir):
        # os.walk() 方法用于通过在目录树中游走输出在目录中的文件名,向上或者向下,
        # 返回一个三元元组 (root, dirs, files):
        #   root:当前正在遍历的这个文件夹的本身的地址,这里为
        #         "/Users/andy/PycharmProjects/hello_pytorch/data/RMB_data"
        #   dirs:是一个 list ,内容是该文件夹中所有的目录的名字(不包括子目录),这里为 ["1", "100"]
        #   files:同样是 list , 内容是该文件夹中所有的文件(不包括子目录),这里为 []

        for sub_dir in dirs:
            # os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表

            # 这里返回的是目录 "1" 或 "100" 下的文件或文件夹名字的列表
            imgs = os.listdir(os.path.join(root, sub_dir))

            # 仅保留列表中文件名后缀为 '.jpg' 的元素,即图片数据
            imgs = list(filter(lambda x: x.endswith('.jpg'), imgs))
  
            random.shuffle(imgs)
            img_count = len(imgs)

            train_point = int(img_count * train_pct)
            valid_point = int(img_count * (train_pct + valid_pct))

            for i in range(img_count):
                if i < train_point:
                    out_dir = os.path.join(train_dir, sub_dir)
                elif i < valid_point:
                    out_dir = os.path.join(valid_dir, sub_dir)
                else:
                    out_dir = os.path.join(test_dir, sub_dir)

                makedir(out_dir)

                target_path = os.path.join(out_dir, imgs[i])
                src_path = os.path.join(dataset_dir, sub_dir, imgs[i])

                # 拷贝文件和权限,这里表示将原始数据集中的图片文件拷贝到目标路径文件名下
                shutil.copy(src_path, target_path)  

            print('Class: {}, train: {}, valid :{}, test: {}'.format(sub_dir, \
            train_point, valid_point-train_point, img_count-valid_point))
  • 输出结果:
Class: 1, train: 80, valid :10, test: 10
Class: 100, train: 80, valid :10, test: 10
  • 数据读取:
import os
import random
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from model.lenet import LeNet
from tools.my_dataset import RMBDataset


def set_seed(seed=1):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)


set_seed()  # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ========================= step 1/5 数据 ===============================
split_dir = os.path.join("..", "..", "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
    # 将图像缩放到 32*32 大小
    transforms.Resize((32, 32)),
    # 对图像进行随机裁剪(数据增强)
    transforms.RandomCrop(32, padding=4),
    # 将图片转成张量形式,并进行归一化操作,把像素值区间从 0~255 归一化到 0~1
    transforms.ToTensor(),
    # 数据标准化,均值为 0,标准差为 1:output = (input - mean) / std
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建 MyDataset 实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建 DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)
  • RMBDataset 类实现:
import os
import random
from PIL import Image
from torch.utils.data import Dataset

random.seed(1)
rmb_label = {"1": 0, "100": 1}


class RMBDataset(Dataset):
    def __init__(self, data_dir, transform=None):
        """
        rmb 面额分类任务的 Dataset
        :param data_dir: str, 数据集所在路径
        :param transform: torch.transform, 数据预处理
        """
        self.label_name = {"1": 0, "100": 1}
        self.data_info = self.get_img_info(data_dir)  # data_info 存储所有t图片路径和标签,在 DataLoader 中通过 index 读取样本
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')   # 0~255

        if self.transform is not None:
            img = self.transform(img)   # 在这里做 transform,转为 tensor 等等

        return img, label

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

    @staticmethod
    def get_img_info(data_dir):
        data_info = list()
        for root, dirs, _ in os.walk(data_dir):
            # 遍历类别
            for sub_dir in dirs:
                img_names = os.listdir(os.path.join(root, sub_dir))
                img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

                # 遍历图片
                for i in range(len(img_names)):
                    img_name = img_names[i]
                    path_img = os.path.join(root, sub_dir, img_name)
                    label = rmb_label[sub_dir]
                    data_info.append((path_img, int(label)))

        return data_info

PyTorch 数据读取流程图

首先在 for 循环中遍历DataLoader,然后根据是否采用多进程,决定使用单进程或者多进程的DataLoaderIter。在DataLoaderIter里调用Sampler生成Index的 list,再调用DatasetFetcher根据index获取数据。在DatasetFetcher里会调用Dataset__getitem__()方法获取真正的数据。这里获取的数据是一个 list,其中每个元素是 (img, label) 的元组,再使用 collate_fn()函数整理成一个 list,里面包含两个元素,分别是 img 和 label 的tenser。

ch02-PyTorch数据预处理_第5张图片

2.数据预处理 transforms 模块机制

本小结主要学习 PyTorch 中的图像预处理模块 —— transforms 的运行机制,以及常用的数据标准化方法 transforms.Normalize

2.1.transforms 的运行机制

  • torchvision:计算机视觉工具包
    • torchvision.transforms : 常用的图像预处理方法 --> 本节内容
    • torchvision.datasets : 常用数据集的dataset实现,MNIST,CIFAR-10,ImageNet等
    • torchvision.model : 常用的模型预训练,AlexNet,VGG, ResNet,GoogLeNet等

2.1.1.transforms: torchvision.transforms : 常用的图像预处理方法

  • 数据中心化
  • 数据标准化
  • 缩放
  • 裁剪
  • 旋转
  • 翻转
  • 填充
  • 噪声添加
  • 灰度变换
  • 线性变换
  • 仿射变换
  • 亮度、饱和度及对比度变换

我们知道,深度学习是由数据驱动的,而数据的数量和分布对于模型的优劣具有决定性作用,所以我们需要对数据进行一定的预处理以及数据增强,用于提升模型的泛化能力。


上面的 64 张图片都来源于 1 张原始图片,它们是由原始图片经过一系列的缩放、裁剪、平移、变换等操作的组合生成的。如前所述,我们进行图片增强的原因是为了提升模型的泛化能力:如果我们在数据增强的过程中生成了一些与测试样本很相似的图片,那么模型的泛化能力自然将会得到提升。

2.1.2.例子:人民币二分类中的 transforms

当我们需要多个transforms操作时,需要作为一个list放在transforms.Compose中。需要注意的是transforms.ToTensor()是把图片转换为张量,同时进行归一化操作,把每个通道 0~255 的值归一化为 0~1。在验证集的数据增强中,不再需要transforms.RandomCrop()操作。然后把这两个transform操作作为参数传给Dataset,在Dataset的__getitem__()方法中做图像增强。

import os
import random
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from model.lenet import LeNet
from tools.my_dataset import RMBDataset


def set_seed(seed=1):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)


set_seed()  # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ========================= step 1/5 数据 ===============================
split_dir = os.path.join("..", "..", "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

# Compose 会将一系列的 transforms 操作进行组合包装,按顺序执行
train_transform = transforms.Compose([
    # 将图像缩放到 32*32 大小
    transforms.Resize((32, 32)),
    # 对图像进行随机裁剪(数据增强)
    transforms.RandomCrop(32, padding=4),
    # 将图片转成张量形式,并进行归一化操作,把像素值区间从 [0, 255] 归一化到 [0, 1]
    transforms.ToTensor(),
    # 数据标准化,均值为 0,标准差为 1:output = (input - mean) / std
    transforms.Normalize(norm_mean, norm_std),
])

# 注意:测试数据不需要进行数据增强
valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建 MyDataset 实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建 DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

2.1.3.PyTorch 中的数据预处理流程图

回顾:

当我们需要多个transforms操作时,需要作为一个list放在transforms.Compose中。需要注意的是transforms.ToTensor()是把图片转换为张量,同时进行归一化操作,把每个通道 0~255 的值归一化为 0~1。在验证集的数据增强中,不再需要transforms.RandomCrop()操作。然后把这两个transform操作作为参数传给Dataset,在Dataset的__getitem__()方法中做图像增强。

def __getitem__(self, index):
    # 通过 index 读取样本
    path_img, label = self.data_info[index]
    # 注意这里需要 convert('RGB')
    img = Image.open(path_img).convert('RGB')     # 0~255
    if self.transform is not None:
        img = self.transform(img)   # 在这里做transform,转为tensor等等
    # 返回是样本和标签
    return img, label

其中self.transform(img)会调用Compose的__call__()函数:

def __call__(self, img):
    for t in self.transforms:
        img = t(img)
    return img

可以看到,这里是遍历transforms中的函数,按顺序应用到 img 中。

2.2.数据标准化:transforms.Normalize:加速运算

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

功能:逐channel的对图像进行标准化 output = (input - mean) / std

  • mean:各通道的均值
  • std:各通道的标准差
  • inplace:是否原地操作
train_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.RandomCrop(32, padding=4),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])
valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

该方法调用的是F.normalize(tensor, self.mean, self.std, self.inplace)

而`F.normalize()方法如下:

def normalize(tensor, mean, std, inplace=False):
    if not _is_tensor_image(tensor):
        raise TypeError('tensor is not a torch image.')

    if not inplace:
        tensor = tensor.clone()

    dtype = tensor.dtype
    mean = torch.as_tensor(mean, dtype=dtype, device=tensor.device)
    std = torch.as_tensor(std, dtype=dtype, device=tensor.device)
    tensor.sub_(mean[:, None, None]).div_(std[:, None, None])
    return tensor

首先判断是否为 tensor,如果不是 tensor 则抛出异常。然后根据inplace是否为 true 进行 clone,接着把mean 和 std 都转换为tensor (原本是 list),最后减去均值除以方差:tensor.sub_(mean[:, None, None]).div_(std[:, None, None])

对数据进行均值为 0,标准差为 1 的标准化,可以加快模型的收敛。

在逻辑回归的实验中,我们的数据生成代码如下:

sample_nums = 100
mean_value = 1.7
bias = 1
n_data = torch.ones(sample_nums, 2)
# 使用正态分布随机生成样本,均值为张量,方差为标量
x0 = torch.normal(mean_value * n_data, 1) + bias      # 类别0 数据 shape=(100, 2)
# 生成对应标签
y0 = torch.zeros(sample_nums)                         # 类别0 标签 shape=(100, 1)
# 使用正态分布随机生成样本,均值为张量,方差为标量
x1 = torch.normal(-mean_value * n_data, 1) + bias     # 类别1 数据 shape=(100, 2)
# 生成对应标签
y1 = torch.ones(sample_nums)                          # 类别1 标签 shape=(100, 1)
train_x = torch.cat((x0, x1), 0)
train_y = torch.cat((y0, y1), 0)

生成的数据均值是mean_value+bias=1.7+1=2.7,比较靠近 0 均值。模型在 380 次迭代时,准确率就超过了 99.5%。

如果我们把 bias 修改为 5。那么数据的均值变成了 6.7,偏离 0 均值较远,这时模型训练需要更多次才能收敛 (准确率达到 99.5%)。

为什么要对数据进行标准化?

数据标准化可以加快模型的收敛过程:因为模型初始化通常是零均值的,所以通过标准化,模型可以在初始位置附近找到最优分界平面。

本节介绍了数据的预处理模块 transforms 的运行机制,数据在读取之后通常都需要进行预处理,包括尺寸缩放、转换张量、数据中心化或标准化等等,这些操作都是通过 transforms 进行的,所以这里我们重点学习了 transforms 的运行机制,并介绍了数据标准化 (Normalize) 的使用原理。

3.transforms图像增强(一)裁剪、翻转及旋转

在之前,我们已经熟悉了 PyTorch 中 transforms 的运行机制,它提供了大量的图像增强方法,例如裁剪、旋转、翻转等等,以及可以自定义实现增强方法。本节课中,我们将进一步学习 transforms 中的图像增强方法。

3.1.数据增强

数据增强 (Data Augmentation) 又称为数据增广、数据扩增,它是对 训练集 进行变换,使训练集更丰富,从而让模型更具 泛化能力。

例子:

ch02-PyTorch数据预处理_第6张图片

例子:

3.2. transforms–Crop 裁剪

(1) transforms.CenterCrop :功能:从图像中心裁剪图片。

transforms.CenterCrop(size)
  • 主要参数:size:所需裁剪图片尺寸。

  • 代码示例:我们有一个 224 * 224 的图片,我们将其从中心裁剪为 196 * 196 的图片。

ch02-PyTorch数据预处理_第7张图片

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # CenterCrop,如果 size 大于原始尺寸,多余部分将用黑色 (即像素值为 0) 填充
    transforms.CenterCrop(196)
])

(2) transforms.RandomCrop 功能:从图片中随机裁剪出尺寸为 size 的图片。

transforms.RandomCrop(
    size,
    padding=None,
    pad_if_needed=False,
    fill=0,
    padding_mode='constant'
)

主要参数:

  • size:所需裁剪图片尺寸。
  • padding:设置填充大小。
    • 当为 a 时,上下左右均填充 a 个像素。
    • 当为 (a, b) 时, 上下填充 b 个像素, 左右填充 a 个像素。
    • 当为 (a, b, c, d) 时,左、上、右、下分别填充 a、b、c、d 个像素。
  • pad_if_need:若图像小于设定 size,则填充,此时该项需要设置为 True。
  • padding_mode:填充模式,有 4 种模式:
    • constant:像素值由 fill 设定。
    • edge:像素值由图像边缘像素决定。
    • reflect:镜像填充,最后一个像素不镜像,例如 [1, 2, 3, 4] --> [3, 2, 1, 2, 3, 4, 3, 2]。
    • symmetric:镜像填充,最后一个像素镜像,例如 [1, 2, 3, 4] --> [2, 1, 1, 2, 3, 4, 4, 3]。
  • fill:padding_mode = ‘constant’ 时,设置填充的像素值。

(3) transforms.RandomResizedCrop: 功能:随机大小、长宽比裁剪图片。

RandomResizedCrop(
    size,
    scale=(0.08, 1.0),
    ratio=(3/4, 4/3),
    interpolation
)
  • 主要参数:

  • size:所需裁剪图片尺寸。

  • scale:随机裁剪面积比例,默认 (0.08, 1)。

  • ratio:随机长宽比,默认 (3/4, 4/3)。

  • interpolation:插值方法。

    • PIL.Image.NEAREST
    • PIL.Image.BILINEAR
    • PIL.Image.BICUBIC

(4) transforms.FiveCrop:功能:在图像的上下左右以及中心裁剪出尺寸为 size 的 5 张图片。

transforms.FiveCrop(size)
  • 主要参数:size:所需裁剪图片尺寸。

代码示例:

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    # 注意:由于生成了 5 张图片,返回的是一个元组,我们需要将其转换为 PIL Image 或者 ndarray 的形式。
    transforms.FiveCrop(112),
    transforms.Lambda(lambda crops: torch.stack([(transforms.ToTensor()(crop)) for crop in crops]))
])

(5) transforms.TenCrop
功能:在图像的上下左右以及中心裁剪出尺寸为 size 的 5 张图片,并对这 5 张图片进行水平或者垂直镜像获得 10 张图片。

transforms.TenCrop(
    size,
    vertical_flip=False
)
  • 主要参数:
    • size:所需裁剪图片尺寸。
    • vertical_flip:是否垂直翻转。

代码示例:

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    # 注意:由于生成了 10 张图片,返回的是一个元组,我们需要将其转换为 PIL Image 或者 ndarray 的形式。
    transforms.TenCrop(112, vertical_flip=False),
    transforms.Lambda(lambda crops: torch.stack([(transforms.ToTensor()(crop)) for crop in crops])),
])

3.3. transforms–翻转、旋转

3.3.1 transforms 翻转

(1) transforms.RandomHorizontalFlip

  • 功能:依概率水平(左右)翻转图片。
    transforms.RandomHorizontalFlip(p=0.5)
    
  • 主要参数:p:翻转概率。

ch02-PyTorch数据预处理_第8张图片


(2) transforms.RandomVerticalFlip

  • 功能:依概率垂直(上下)翻转图片。
transforms.RandomVerticalFlip(p=0.5)
  • 主要参数:p:翻转概率。

ch02-PyTorch数据预处理_第9张图片

3.3.2 transforms 旋转

(1) transforms.RandomRotation

  • 功能:随机旋转图片。
RandomRotation(
    degrees,
    resample=False,
    expand=False,
    center=None
)
  • 主要参数:

    • degrees:旋转角度。
      • 当为 a 时,在 (-a, a) 之间随机选择旋转角度。
      • 当为 (a, b) 时,在 (a, b) 之间随机选择旋转角度。
    • resample:重采样方法。
    • expand:是否扩大图片,以保持原图信息。
    • center:旋转点设置,默认中心旋转。

例子:

ch02-PyTorch数据预处理_第10张图片

3.4. 总结

本节课中,我们学习了数据预处理模块 transforms 中的数据增强方法:裁剪、翻转和旋转。在下次课程中 ,我们将会学习 transforms 中的其他数据增强方法。

4.transforms图像增强(二)图像变换、方法操作及自定义方法

transforms 图像变换、方法操作及自定义方法
上节中,我们学习了 transforms 中的裁剪、旋转和翻转,本节我们将继续学习 transforms 中的其他数据增强方法。

4.1. transforms 图像变换

(1)transforms.Pad

  • 功能:对图片边缘进行填充。
transforms.Pad(
    padding,
    fill=0,
    padding_mode='constant'
)
  • 主要参数:
  • padding:设置填充大小。
    • 当为 a 时,上下左右均填充 a 个像素。
    • 当为 (a, b) 时,上下填充 b 个像素,左右填充 a 个像素。
    • 当为 (a, b, c, d) 时,左、上、右、下分别填充 a、b、c、d 个像素。
  • padding_mode:填充模式,有 4 种模式:
    • constant
    • edge
    • reflect
    • symmetric
  • fill:当 padding_mode=‘constant’ 时,设置填充的像素值,(R, G, B) 或 (Gray)。

(2)transforms.ColorJitter

  • 功能:调整亮度、对比度、饱和度和色相。
transforms.ColorJitter(
    brightness=0,
    contrast=0,
    saturation=0,
    hue=0
)
  • 主要参数:
  • brightness:亮度调整因子。
    • 当为 a 时, 从 [max(0, 1-a), 1+a] 中随机选择。
    • 当为 (a, b) 时, 从 [a, b] 中随机选择。
  • contrast :对比度参数,同 brightness。
  • saturation:饱和度参数,同 brightness。
  • hue:色相参数。
    • 当为 a 时,从 [-a, a] 中选择参数。注:0 <= a <= 0.5。
    • 当为 (a, b) 时,从 [a, b] 中选择参数。注:-0.5 <= a <= b <= 0.5。

(3)transforms.Grayscale

  • 功能:将图片转换为灰度图。
transforms.Grayscale(num_output_channels)
  • 主要参数:num_ouput_channels:输出通道数,只能设为 1 或 3。

(3)transforms.RandomGrayscale

  • 功能:依概率将图片转换为灰度图。
transforms.RandomGrayscale(
    num_output_channels,
    p=0.1
)
  • 主要参数:

  • num_ouput_channels:输出通道数,只能设为 1 或 3。

  • p:概率值,图像被转换为灰度图的概率。


(4)transforms.RandomAffine
功能:对图像进行仿射变换,仿射变换是二维的线性变换,由五种基本原子变换构成:旋转、平移、缩放、错切 和 翻转。

transforms.RandomAffine(
    degrees,
    translate=None,
    scale=None,
    shear=None,
    resample=False,
    fillcolor=0
)
  • 主要参数:

  • degrees:旋转角度设置。

  • translate:平移区间设置,如 (a, b), a 设置宽 (width),b 设置高 (height)。图像在宽维度平移的区间为 -img_width * a < dx < img_width * a。

  • scale:缩放比例 (以面积为单位)。

  • fill_color:填充颜色设置。

  • shear:错切角度设置,有水平错切和垂直错切。

    • 若为 a,则仅在 x 轴错切,错切角度在 (-a, a) 之间。
    • 若为 (a, b),则 a 设置 x 轴角度,b 设置 y 轴角度。
    • 若为 (a, b, c, d),则 a、b 设置 x 轴角度,c、d 设置 y 轴角度。
  • resample:重采样方式,有 NEAREST、BILINEAR、BICUBIC 三种。


(5)transforms.RandomErasing

  • 功能:对图像进行随机遮挡。
transforms.RandomErasing(
    p=0.5,
    scale=(0.02, 0.33),
    ratio=(0.3, 3,3),
    value=0,
    inplace=False
)
  • 主要参数:

  • p:概率值,执行该操作的概率。

  • scale:遮挡区域的面积。

  • ratio:遮挡区域长宽比。

  • value:设置遮挡区域的像素值,(R, G, B) 或者 (Gray)。

  • 参考文献:Random Erasing Data Augmentation


(6)transforms.Lambda

  • 功能:用户自定义 lambda 方法。
transforms.Lambda(lambd)
  • 主要参数:lambd:lambda 匿名函数,lambda [arg1 [,arg2, … , argn]]: expression。
    代码示例:
transforms.TenCrop(200, vertical_flip=True),
transforms.Lambda(lambda crops: torch.stack([transforms.Totensor()(crop) for crop in crops]))

4.2. transforms 选择操作

我们已经学习了 transforms 中对图像的各种增强方法,下面我们将介绍对 transforms 方法的三种选择操作,它们可以使 transforms 数据增强方法更加灵活、丰富、多样。

(1)transforms.RandomChoice

  • 功能:从一系列 transforms 方法中随机挑选一个。
transforms.RandomChoice([transforms1, transforms2, transforms3])

(2)transforms.RandomApply

  • 功能:依据概率执行一组 transforms 操作。
transforms.RandomApply([transforms1, transforms2, transforms3], p=0.5)

(3)transforms.RandomOrder

  • 功能:对一组 transforms 操作打乱顺序。
transforms.RandomOrder([transforms1, transforms2, transforms3])

4.3. 自定义 transforms

尽管 PyTorch 提供了许多 transforms 方法,然而在实际应用中,可能还需要根据项目需求来自定义一些 transforms 方法。下面我们将学习如何自定义 transforms 方法及其注意事项。

为了自定义 transforms 方法,首先需要了解其运行机制,在之前介绍数据读取机制 DataLoaderDataset 时,我们提到过 transforms 方法是在 Compose 类中的 __call__ 函数中被调用的。我们对一组 transforms 方法进行 for 循环,每次按顺序挑选出我们的 transforms 方法 t 并执行它。可以看到,每个 transforms 方法仅接收一个参数,并返回一个参数。另外注意,由于是通过 for 循环调用,当前 transforms 方法的输出就是下一个 transforms 方法的输入。

class Compose(object):
    def __call__(self, img):
        for t in self.transforms:
            img = t(img)
        return img

自定义 transforms 要素:

  • 仅接收一个参数,返回一个参数。
  • 注意上下游的输出与输入之间的数据类型必须要匹配。

我们在设计 transforms 方法的时候可能需要多个参数,比如设置概率值、信噪比等,这些可以通过类方法实现。

通过类实现多参数传入:

class YourTransforms(object):
    def __init__(self, ...):
        ...
    def __call__(self, img):
        ...
        return img

上面是一个自定义 transforms 方法的基本结构。首先是一个初始化 __init__ 方法,在初始化的时候我们可以传入想要的参数,比如概率值、信噪比等等。然后,这个类中还必须有一个 __call__ 函数,即这个类的实例可以被调用,__call__ 函数只接受一个 input 参数,然后执行自定义的一些功能,最后返回一个 output,并且输入与输出的数据类型必须匹配,比如都是 img、tensor、list、turple 或者 dict 等。

例子:椒盐噪声

椒盐噪声 (salt pepper noise) 又称为 脉冲噪声,是一种随机出现的白点或者黑点,白点称为 盐噪声,黑点称为 椒噪声。

信噪比 (Signal-Noise Rate, SNR) 是衡量噪声的比例,在图像中为图像像素的占比。

下面是对一张小猫图像增加不同信噪比的椒盐噪声的效果图:

ch02-PyTorch数据预处理_第11张图片

从左到右信噪比依次为 0.9、0.7、0.5、0.3。可以看到,随着信噪比的减小,即信号的减少,图片丢失的信息越来越多。当信噪比为 0.9 时,我们还可以清晰地看到这是一张小猫的图像;而当信噪比降低到 0.3 时,我们已经很难辨别图像的真实内容了。

下面,我们通过自定义 transforms 方法对图像添加椒盐噪声:

class AddPepperNoise(object):

    def __init__(self, snr, p):
        self.snr = snr  # 设置信噪比
        self.p = p  # 设置概率值

    def __call__(self, img):

        ...  # 添加椒盐噪声具体实现过程

        return img

Python 代码示例:

class AddPepperNoise(object):
    """增加椒盐噪声
    Args:
        snr (float): 信噪比,Signal Noise Rate
        p (float): 概率值,依概率执行该操作
    """

    def __init__(self, snr, p=0.9):
        assert isinstance(snr, float) or (isinstance(p, float))
        self.snr = snr
        self.p = p

    def __call__(self, img):
        """
        Args:
            img (PIL Image): PIL Image
        Returns:
            PIL Image: PIL image.
        """
        if random.uniform(0, 1) < self.p:
            img_ = np.array(img).copy()
            h, w, c = img_.shape
            signal_pct = self.snr
            noise_pct = (1 - self.snr)
            mask = np.random.choice((0, 1, 2), size=(h, w, 1), p=[signal_pct, noise_pct/2., noise_pct/2.])
            mask = np.repeat(mask, c, axis=2)
            img_[mask == 1] = 255   # 盐噪声
            img_[mask == 2] = 0     # 椒噪声
            return Image.fromarray(img_.astype('uint8')).convert('RGB')
        else:
            return img

5.transforms 方法总结

  • 裁剪:

    • transforms.CenterCrop
    • transforms.RandomCrop
    • transforms.RandomResizedCrop
    • transforms.FiveCrop
    • transforms.TenCrop
  • 翻转和旋转:

    • transforms.RandomHorizontalFlip
    • transforms.RandomVerticalFlip
    • transforms.RandomRotation
  • 图像变换:

    • transforms.Pad
    • transforms.ColorJitter
    • transforms.Grayscale
    • transforms.RandomGrayscale
    • transforms.RandomAffine
    • transforms.LinearTransformation
    • transforms.RandomErasing
    • transforms.Lambda
    • transforms.Resize
    • transforms.Totensor
    • transforms.Normalize
  • transforms 操作:

    • transforms.RandomChoice
    • transforms.RandomApply
    • transforms.RandomOrder

6.数据增强实战应用

原则:让训练集与测试集更接近。

  • 空间位置:平移
  • 色彩:灰度图,色彩抖动
  • 形状:仿射变换
  • 上下文场景:遮挡, 填充
    ……

例子:

ch02-PyTorch数据预处理_第12张图片

我们看到,在训练集中,猫基本都处于图片的中央位置,而在测试集中的猫处于偏左/右,或者在角落的情况。对于这种情况,我们可以在数据增强时改变训练集中的空间位置,例如平移,来逼近测试集的图片。

例子:

ch02-PyTorch数据预处理_第13张图片

我们看到,在训练集中,猫基本都是白色的,而在测试集中的猫是黑色的。对于这种情况,我们可以在数据增强时对训练集中的图片进行色彩抖动或者变换处理,来逼近测试集的图片。有时,训练集和测试集中猫的姿态差异很大,这种情况下,我们可以通过对训练集图片进行仿射变换处理来改变猫的形状。另外,还可以对比看下测试集中有无遮挡情况,可以对训练集进行遮挡、填充等相应处理。

人民币分类

ch02-PyTorch数据预处理_第14张图片

在之前的人民币分类例子中,我们的数据集是面额为 1 元与 100 元的第四套人民币各 100 张,那么基于该数据集训练出的模型是否可以对第五套人民币的 100 元进行正确分类呢?

直观上,第五套人民币的 100 元与第四套人民币的 1 元在颜色上比较相近,而在面额上与第四套人民币的 1 00 元一样。实验证明,如果不进行额外的数据增强,模型会将第五套人民币的 100 元识别为 1 元,这很可能是由于二者在颜色上的相似性导致的。当我们对图像进行灰度处理后,模型将可以对第五套人民币的 100 元进行正确分类。

总结

在本节课中,我们学习了数据预处理 transforms 的图像变换、操作方法,以及自定义 transforms。到目前为止,PyTorch 中的数据模块我们已经学习完毕,在下节课中,我们将会学习 PyTorch 中的模型模块。

你可能感兴趣的:(PyTorch,pytorch,深度学习,机器学习,数据加载,数据预处理)