大型数据集是成功应用深度神经网络的先决条件。 图像增强(Image Augmentation)在对训练图像进行一系列的随机变化之后,生成相似但不同的训练样本,从而扩大了训练集的规模。此外,应用图像增强的原因是,随机改变训练样本可以减少模型对某些属性的依赖,从而提高模型的泛化能力。
例如,我们可以以不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,减少模型对于对象出现位置的依赖。 我们还可以调整亮度、颜色等因素来降低模型对颜色的敏感度。 可以说,图像增强技术对于 AlexNet 的成功是必不可少的。
本文将基于 torchvision
工具箱介绍常用的图像增强方法,有关该工具箱的介绍请看这篇文章。
torchvision.transforms
中提供了大量的用于图像变换/增强的类,绝大多数的类有其相对应的函数版本,它们可以在 torchvision.transforms.functional
中找到(好比 torch.nn
与 torch.nn.functional
之间的关系)。函数版本和类版本的区别在于,函数版本不包含用于其参数的随机数生成器,即你需要指定所有的参数才能够使用。此外,函数版本所产生的结果通常是可复现的。类版本通常采用的是随机变换,但是它会对同一个 batch 中的所有样本采用相同的随机变换,且调用类版本将导致结果不可复现。
由于之前我们已经介绍了函数版本(链接),故本文将主要聚焦于类版本的使用。
先导入本文所需要的包:
from torchvision import transforms
import matplotlib.pyplot as plt
from PIL import Image
为方便接下来更加直观地展示图像,这里定义一个图像展示器。它接受 PIL 图像作为输入参数,调用 apply
方法可对相应的图像应用图像增强并展示结果。
class ImageDisplay:
def __init__(self, img):
self.img = img
def apply(self, aug):
_, axes = plt.subplots(1, 4)
for i in range(4):
axes[i].imshow(aug(self.img))
axes[i].axes.get_xaxis().set_visible(False)
axes[i].axes.get_yaxis().set_visible(False)
plt.show()
之后,我们将以下面这张图为基准,对其进行图像增强:
假设该图路径为 ./pics/1.jpg
。
使用格式:
transforms.RandomHorizontalFlip(p=0.5)
以概率 p p p 水平翻转给定的图像。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomHorizontalFlip()
imgdp.apply(aug)
效果:
因为默认概率是 0.5 0.5 0.5,所以最终结果是有一半图片水平翻转了,另有一半没有翻转。
使用格式:
transforms.RandomVerticalFlip(p=0.5)
以概率 p p p 垂直翻转给定的图像。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomVerticalFlip(0.75)
imgdp.apply(aug)
效果:
因为概率为 0.75 0.75 0.75,所以有三张图片翻转了。但需要注意的是,概率并不代表真实情况,也有可能一张都不翻转。
使用格式:
transforms.RandomRotation(degrees, center=None, fill=0)
degrees
为元组或标量。当 degrees
为元组 (min, max)
时,会随机从其中挑选一个角度值进行旋转。当 degrees
为标量时,范围变为 (-degrees, degrees)
。center
为一个元组,代表围绕哪一个点进行旋转,默认为原图的中心。 x x x 轴方向沿图像的宽, y y y 轴方向沿图像的高。fill
为填充值,默认为0,即黑色。fill
可以为 RGB 元组也可以为标量,为标量时代表灰度。img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomRotation(45, fill=(255, 0, 0))
imgdp.apply(aug)
这段代码表示将原图在 ( − 4 5 ∘ , 4 5 ∘ ) (-45^{\circ},45^\circ) (−45∘,45∘) 内围绕原图中心随机旋转,并以红色填充其他区域。效果如下:
使用格式:
transforms.Resize(size)
size
为形如 (H, W)
的元组。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomVerticalFlip(0.75)
imgdp.apply(aug)
效果:
使用格式:
transforms.CenterCrop(size)
作用是裁剪图像的中心区域,且区域大小为 (H, W)
。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.CenterCrop((200, 200))
imgdp.apply(aug)
效果:
使用格式:
transforms.RandomCrop(size)
随机裁剪图像中的某个区域,且区域大小为 size
。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomCrop((200, 200))
imgdp.apply(aug)
效果:
使用格式:
transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.333))
以默认参数来解释。首先随机裁剪一个面积为原始面积 8 % 8\% 8% 到 100 % 100\% 100% 的区域,该区域的宽高比从 3 : 4 3:4 3:4 到 4 : 3 4:3 4:3 之间随机取值,最后将该区域调整为指定的 size
。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2))
imgdp.apply(aug)
效果:
使用格式:
transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
有点懒得码字了,这里直接放官网的截图:
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
imgdp.apply(aug)
效果:
使用格式:
transforms.RandomGrayscale(p=0.1)
以概率 p p p 将一张图转化为灰度图。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomGrayscale(p=0.5)
imgdp.apply(aug)
效果:
使用格式:
transforms.RandomInvert(p=0.5)
以概率 p p p 翻转图像的颜色。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomInvert(p=0.5)
imgdp.apply(aug)
效果:
使用格式:
transforms.RandomAdjustSharpness(sharpness_factor, p=0.5)
以概率 p p p 调整图像的锐度。
sharpness_factor
为锐度因子, 0 0 0 代表模糊, 1 1 1 代表原图,值越大代表锐度越高,没有上限。
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
aug = transforms.RandomAdjustSharpness(100)
imgdp.apply(aug)
效果:
img = Image.open('./pics/1.jpg')
imgdp = ImageDisplay(img)
augs = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2)),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5),
])
imgdp.apply(augs)
效果:
使用格式:
transforms.Normalize(mean, std)
对张量图像进行归一化操作(不支持 PIL 图像)。
假设有 n n n 个通道,则 mean=(mean[1], ..., mean[n])
,std=(std[1], ..., std[n])
。输出为 output[channel] = (input[channel] - mean[channel]) / std[channel]
。
from PIL import Image
from torchvision.transforms import ToTensor, ToPILImage, Normalize
pic = Image.open('./pics/1.jpg')
img2tensor, tensor2img = ToTensor(), ToPILImage()
normalizer = Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
pic = tensor2img(normalizer(img2tensor(pic)))
pic.show()
对图像进行归一化的效果如下:
使用 ResNet-18,在 CIFAR-10 上应用图像增强(仅对训练集应用),观察训练/测试效果:
import torchvision
import torch.nn.functional as F
from torch import nn
from torch.utils.data import DataLoader
from torchvision import transforms
from Experiment import Experiment as E
class Residual(nn.Module):
def __init__(self, in_channels, out_channels, stride=1, conv_1x1=False):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
)
self.conv_1x1 = nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride) if conv_1x1 else None
def forward(self, x):
y = self.block(x)
if self.conv_1x1:
x = self.conv_1x1(x)
return F.relu(y + x)
class ResNet(nn.Module):
def __init__(self):
super().__init__()
self.block_1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)
self.block_2 = nn.Sequential(
Residual(64, 64),
Residual(64, 64),
Residual(64, 128, stride=2, conv_1x1=True),
Residual(128, 128),
Residual(128, 256, stride=2, conv_1x1=True),
Residual(256, 256),
Residual(256, 512, stride=2, conv_1x1=True),
Residual(512, 512),
)
self.block_3 = nn.Sequential(
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.block_1(x)
x = self.block_2(x)
x = self.block_3(x)
return x
def init_net(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
if __name__ == '__main__':
train_augs = transforms.Compose([
transforms.Resize(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5),
transforms.ToTensor()
])
test_augs = transforms.Compose([transforms.Resize(224), transforms.ToTensor()])
train_data = torchvision.datasets.CIFAR10('/mnt/mydataset', train=True, transform=train_augs, download=True)
test_data = torchvision.datasets.CIFAR10('/mnt/mydataset', train=False, transform=test_augs, download=True)
train_loader = DataLoader(train_data, batch_size=128, shuffle=True, num_workers=4)
test_loader = DataLoader(test_data, batch_size=128, num_workers=4)
resnet = ResNet()
resnet.apply(init_net)
e = E(train_loader, test_loader, resnet, 20, 0.05)
e.main()
e.show()
在 NVIDIA GeForce RTX 3090 上的运行结果如下:
Epoch 20
--------------------------------------------------
Train Avg Loss: 0.054338, Train Accuracy: 0.982660
Test Avg Loss: 0.789593, Test Accuracy: 0.811900
--------------------------------------------------
3054.1 samples/sec
--------------------------------------------------
Done!
本文仅介绍了部分图像增强的手段,如需了解更多,可前往官方文档进一步学习。