任务:训练一个分类模型,使得其能够对第四套人民币中的 1 元和 100 元面额的纸币进行分类。
回顾一下上节课中学习的机器学习的 5 个步骤:
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
)
主要参数:
相关概念:
例如:
训练样本: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。
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 个方面:
这里的路径结构如下,有两类人民币图片:1 元和 100 元,每一类各有 100 张图片。
首先划分数据集为训练集、验证集和测试集,比例为 8:1:1。数据划分好后的路径构造如下:
实现读取数据的 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 步构建模型。
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)
net = LeNet(classes=2)
net.initialize_weights()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9) # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 设置学习率下降策略
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
...
...
我们可以看到每个 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)
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。
本小结主要学习 PyTorch 中的图像预处理模块 —— transforms
的运行机制,以及常用的数据标准化方法 transforms.Normalize
我们知道,深度学习是由数据驱动的,而数据的数量和分布对于模型的优劣具有决定性作用,所以我们需要对数据进行一定的预处理以及数据增强,用于提升模型的泛化能力。
上面的 64 张图片都来源于 1 张原始图片,它们是由原始图片经过一系列的缩放、裁剪、平移、变换等操作的组合生成的。如前所述,我们进行图片增强的原因是为了提升模型的泛化能力:如果我们在数据增强的过程中生成了一些与测试样本很相似的图片,那么模型的泛化能力自然将会得到提升。
当我们需要多个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)
回顾:
当我们需要多个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 中。
transforms.Normalize(
mean,
std,
inplace=False
)
功能:逐channel的对图像进行标准化 output = (input - mean) / std
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) 的使用原理。
在之前,我们已经熟悉了 PyTorch 中 transforms 的运行机制,它提供了大量的图像增强方法,例如裁剪、旋转、翻转等等,以及可以自定义实现增强方法。本节课中,我们将进一步学习 transforms 中的图像增强方法。
数据增强 (Data Augmentation) 又称为数据增广、数据扩增,它是对 训练集 进行变换,使训练集更丰富,从而让模型更具 泛化能力。
例子:
例子:
(1) transforms.CenterCrop :功能:从图像中心裁剪图片。
transforms.CenterCrop(size)
主要参数:size:所需裁剪图片尺寸。
代码示例:我们有一个 224 * 224 的图片,我们将其从中心裁剪为 196 * 196 的图片。
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'
)
主要参数:
(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:插值方法。
(4) transforms.FiveCrop:功能:在图像的上下左右以及中心裁剪出尺寸为 size 的 5 张图片。
transforms.FiveCrop(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
)
代码示例:
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])),
])
(1) transforms.RandomHorizontalFlip
transforms.RandomHorizontalFlip(p=0.5)
(2) transforms.RandomVerticalFlip
transforms.RandomVerticalFlip(p=0.5)
(1) transforms.RandomRotation
RandomRotation(
degrees,
resample=False,
expand=False,
center=None
)
主要参数:
例子:
本节课中,我们学习了数据预处理模块 transforms 中的数据增强方法:裁剪、翻转和旋转。在下次课程中 ,我们将会学习 transforms 中的其他数据增强方法。
transforms 图像变换、方法操作及自定义方法
上节中,我们学习了 transforms 中的裁剪、旋转和翻转,本节我们将继续学习 transforms 中的其他数据增强方法。
(1)transforms.Pad
transforms.Pad(
padding,
fill=0,
padding_mode='constant'
)
(2)transforms.ColorJitter
transforms.ColorJitter(
brightness=0,
contrast=0,
saturation=0,
hue=0
)
(3)transforms.Grayscale
transforms.Grayscale(num_output_channels)
(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:错切角度设置,有水平错切和垂直错切。
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
transforms.Lambda(lambd)
transforms.TenCrop(200, vertical_flip=True),
transforms.Lambda(lambda crops: torch.stack([transforms.Totensor()(crop) for crop in crops]))
我们已经学习了 transforms 中对图像的各种增强方法,下面我们将介绍对 transforms 方法的三种选择操作,它们可以使 transforms 数据增强方法更加灵活、丰富、多样。
(1)transforms.RandomChoice
transforms.RandomChoice([transforms1, transforms2, transforms3])
(2)transforms.RandomApply
transforms.RandomApply([transforms1, transforms2, transforms3], p=0.5)
(3)transforms.RandomOrder
transforms.RandomOrder([transforms1, transforms2, transforms3])
尽管 PyTorch 提供了许多 transforms 方法,然而在实际应用中,可能还需要根据项目需求来自定义一些 transforms 方法。下面我们将学习如何自定义 transforms 方法及其注意事项。
为了自定义 transforms 方法,首先需要了解其运行机制,在之前介绍数据读取机制 DataLoader
和 Dataset
时,我们提到过 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)
是衡量噪声的比例,在图像中为图像像素的占比。
下面是对一张小猫图像增加不同信噪比的椒盐噪声的效果图:
从左到右信噪比依次为 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
裁剪:
翻转和旋转:
图像变换:
transforms 操作:
原则:让训练集与测试集更接近。
例子:
我们看到,在训练集中,猫基本都处于图片的中央位置,而在测试集中的猫处于偏左/右,或者在角落的情况。对于这种情况,我们可以在数据增强时改变训练集中的空间位置,例如平移,来逼近测试集的图片。
例子:
我们看到,在训练集中,猫基本都是白色的,而在测试集中的猫是黑色的。对于这种情况,我们可以在数据增强时对训练集中的图片进行色彩抖动或者变换处理,来逼近测试集的图片。有时,训练集和测试集中猫的姿态差异很大,这种情况下,我们可以通过对训练集图片进行仿射变换处理来改变猫的形状。另外,还可以对比看下测试集中有无遮挡情况,可以对训练集进行遮挡、填充等相应处理。
人民币分类
在之前的人民币分类例子中,我们的数据集是面额为 1 元与 100 元的第四套人民币各 100 张,那么基于该数据集训练出的模型是否可以对第五套人民币的 100 元进行正确分类呢?
直观上,第五套人民币的 100 元与第四套人民币的 1 元在颜色上比较相近,而在面额上与第四套人民币的 1 00 元一样。实验证明,如果不进行额外的数据增强,模型会将第五套人民币的 100 元识别为 1 元,这很可能是由于二者在颜色上的相似性导致的。当我们对图像进行灰度处理后,模型将可以对第五套人民币的 100 元进行正确分类。
总结
在本节课中,我们学习了数据预处理 transforms 的图像变换、操作方法,以及自定义 transforms。到目前为止,PyTorch 中的数据模块我们已经学习完毕,在下节课中,我们将会学习 PyTorch 中的模型模块。