文章目录
1 数据增强
**数据增强(Data Augmentation)**是指通过对已有数据进行变换,来增加其多样性,从而提高模型的泛化能力。虽然这个概念不仅限于图片,也可以应用于文本和语音等数据。

1.1 案例
-
案例: 一家做智能售货机的国内公司,在拉斯维加斯CES展会上进行产品演示时,发现模型效果很差。
-
原因分析:
- 光照与色温问题: CES展馆的灯光昏暗且偏黄,色温(约3000K)与公司内部测试环境(约5000K)差异很大,导致图片颜色发生严重偏差。
- 反光问题: 机器被放置在一个反光的亮面桌子上,灯光反射导致采集到的图片质量变差。
-
解决方法:
- 色温问题: 工程师连夜在现场采集新数据,发回国后重新训练模型。
- 反光问题: 购买桌布铺在桌子上,解决了反光问题。

总结: 这个案例说明了训练环境和实际部署环境可能存在巨大差异。数据增强的目标,就是在训练时尽可能地模拟部署时可能遇到的各种场景(如不同的光照、背景噪音等),从而提高模型对未见过数据的泛化能力。
1.2 常用方法
图片数据增强的方法有很多,最常用的包括:
1.2.1 翻转 (Flipping)
- 左右翻转: 将图片沿垂直中线进行翻转。这是最常用的方法之一。
- 上下翻转: 将图片沿水平中线进行翻转。但这种方法要根据数据集的性质来决定是否使用。例如,翻转猫的图片会导致头朝下,这在现实中很不常见;但如果是叶子分类等对称性较强的数据集,则可以考虑使用。

1.2.2 裁剪 (Cropping)
- 目的: 从图片中裁剪出一部分区域,并将其缩放到固定尺寸(例如,ImageNet常用的 224x224)。
- 常用做法: 随机裁剪。每次训练时,从同一张图片中随机选择不同的区域进行裁剪。这种随机性通常体现在三个方面:
- 随机宽高比: 在一个预设的范围(如 3/4 到 4/3)内随机选取宽高比。
- 随机缩放比例: 在一个预设的比例(如 8% 到 100%)内随机选取裁剪区域占原图的百分比。
- 随机位置: 在图片中的随机位置进行裁剪。

1.2.3 颜色变换 (Color Jittering)
- 目的: 改变图片的颜色属性,以模拟不同的光照条件。
- 常用属性:
- 色调(Hue): 改变图片的颜色倾向(如偏黄、偏蓝、偏红)。
- 饱和度(Saturation): 改变颜色的“浓度”。
- 明亮度(Brightness): 改变图片的亮度。
- 实现方式: 通常是在当前值的基础上,在一定范围内随机调整。例如,将明亮度在 0.5 到 1.5 的范围内随机调整,这意味着亮度可以降低 50% 或增加 50%。

1.3 实现细节

- 在线生成: 数据增强通常在训练过程中“在线”进行。这意味着,你不会预先生成并保存所有增强后的图片。每次训练时,从原始数据集中读取图片,然后随机应用不同的增强方法,生成一张新的图片后,立即将其送入模型进行训练。
- 仅用于训练: 数据增强只在训练阶段使用,而测试或验证时通常不使用。这是因为数据增强可以被视为一种正则化手段,它通过增加训练数据的多样性来防止模型过拟合,提高泛化能力。

1.4 其他方法
除了上述三种最常用的方法外,还有许多其他高级方法,例如:
- 高斯模糊(Gaussian Blur)
- 锐化(Sharpening)
- 添加随机黑块
- 几何变形(Geometric Transformation)
这些方法可以看作是图片处理软件(如 Photoshop)中的各种滤镜和工具。是否使用这些方法,取决于你认为在实际部署环境中是否可能出现类似效果的图片。

1.5 代码实现
from io import BytesIO
import requests
import os
from PIL import Image
from matplotlib import pyplot as plt
import torch
import torchvision
from torch import nn
url = "https://pytorch.org/assets/images/pytorch-ecosystem.png"
# 发送请求获取图片
response = requests.get(url, stream=True)
response.raise_for_status() # 检查请求是否成功
# 将响应内容转换为PIL Image对象
image = Image.open(BytesIO(response.content))
# 数据增强方法
rhf_aug = torchvision.transforms.RandomHorizontalFlip(p=1) # 水平翻转概率为 1
rvf_aug = torchvision.transforms.RandomVerticalFlip(p=1) # 垂直翻转概率为 1
shape_aug = torchvision.transforms.RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2)) # 随机裁剪到200x200大小
# 亮度(brightness)、对比度(contrast)、饱和度(saturation)、色调(hue)
cj_aug = torchvision.transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0.5) # 随机颜色抖动
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
# axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
plt.show()
return axes
# 显示图片
show_images(
[image, rhf_aug(image), rvf_aug(image), shape_aug(image), cj_aug(image)],
1, 5,
titles=['原图', '水平翻转', '垂直翻转', '随机裁剪', '颜色抖动'],
scale=2.5
)

2 微调
微调(Fine-tuning),也称为迁移学习(Transfer Learning),是计算机视觉,特别是深度学习领域中一个至关重要的技术。它的核心思想是:在一个大型数据集上预先训练好的模型,可以作为基础,来帮助我们提升在小型数据集上的任务表现。
这个思想类似于人类的学习方式:一个具有丰富经验的人,在面对一个新事物时,通常只需要很少的例子就能快速掌握,而不是从头开始学习。

- 数据集大小的差距: 像 ImageNet 这样的大型数据集包含了 1000 多万张图片和 1000 个类别,其标注成本高昂。而我们实际工作中遇到的数据集通常要小得多,比如可能只有 5 万张图片和 100 个类别。
- 小数据集的局限性: 即使是 5 万张图片,对于训练一个大型深度学习模型来说也可能不够,容易导致过拟合,模型泛化能力差。
- 利用先验知识: 微调允许我们利用在大规模数据集上学到的通用知识(如边缘、纹理、形状等特征),并将其迁移到我们的特定任务中。

2.1 工作原理
一个典型的深度神经网络通常可以分为两部分:
- 特征提取层: 位于网络的前几层,负责将原始像素信息转换为更抽象、更有意义的特征表示。
- 分类层: 位于网络的最后一层,通常是一个全连接层加上一个 Softmax 回归,负责根据特征进行最终的分类。

微调的核心思路是:
-
加载预训练模型: 使用一个在大数据集(如 ImageNet)上训练好的模型,这个模型通常被称为预训练模型(Pre-trained Model)。
-
复用特征提取层: 认为预训练模型的特征提取层已经学习了通用的视觉特征,这些特征对于我们的新任务同样有用。因此,我们将这些层的权重(参数)原封不动地复制过来,作为我们新模型的初始化参数。
-
替换并重新训练分类层: 由于我们的新任务的类别数量与预训练模型不同,所以需要替换掉预训练模型的最后一层分类器,并用随机初始化的新分类层代替。
-
进行训练: 在我们自己的小型数据集上,使用上述初始化的模型进行训练。这时,模型会根据我们自己的数据,对特征提取层进行“微调”,同时训练新的分类层。
通过这种方式,模型一开始就拥有了很好的特征表示能力,只需对网络进行小幅度的调整,就能很快地适应新的任务,从而实现更高的精度和更快的训练速度。
2.2 微调的常用技巧
- 更小的学习率: 在微调时,通常会使用比从头训练时更小的学习率。因为预训练模型的参数已经很接近最优解,我们只需要对其进行小幅度的调整,而不是大刀阔斧地改变。
- 更少的迭代次数: 微调通常只需要更少的 epochs(数据迭代次数)。

- 冻结底层: 一个更强的正则化方法是**冻结(freeze)**底层。由于神经网络的底层通常学习的是更通用的特征(如边缘、颜色),而高层学习的是与具体任务更相关的语义特征。当你的数据集非常小时,可以固定住底层参数不进行更新,只训练高层参数。这能有效降低模型复杂度,防止过拟合。

- 预训练模型的质量: 预训练模型的质量至关重要。一个在更大、更复杂数据集上训练出来的模型,其特征表示能力通常也更强,微调效果也会更好。

2.3 代码步骤
2.3.1 获取和准备数据集
首先,你需要准备好你的目标数据集。在这个例子中,我们使用一个包含热狗和非热狗图像的数据集。
-
下载数据集: 使用
d2l
库中的download_extract
函数下载并解压数据集。数据集结构通常是train
和test
文件夹,每个文件夹下又包含按类别命名的子文件夹(hotdog
和not-hotdog
)。#@save d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip', 'fba480ffa8aa7e0febbb511d181409f899b9baa5') data_dir = d2l.download_extract('hotdog')
-
加载数据: 使用
torchvision.datasets.ImageFolder
来加载数据集,它可以自动识别文件夹结构作为类别。train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train')) test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))
-
定义数据增强和标准化:
- 训练集(
train_augs
): 为了增加数据多样性并防止过拟合,对训练集应用数据增强技术。常用的包括随机裁剪(RandomResizedCrop)和随机水平翻转(RandomHorizontalFlip)。 - 测试集(
test_augs
): 测试集通常只进行标准化处理,以保证评估的准确性。通常包括调整大小(Resize)和中心裁剪(CenterCrop)。 - 标准化(Normalize): 对图片的 RGB 三个通道进行标准化,即减去均值并除以标准差。这有助于模型更快地收敛。
# 定义标准化方法,使用在ImageNet上训练模型的均值和标准差 normalize = torchvision.transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 训练集的数据增强 train_augs = torchvision.transforms.Compose([ torchvision.transforms.RandomResizedCrop(224), torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.ToTensor(), normalize]) # 测试集的数据增强 test_augs = torchvision.transforms.Compose([ torchvision.transforms.Resize([256, 256]), torchvision.transforms.CenterCrop(224), torchvision.transforms.ToTensor(), normalize])
- 训练集(
2.3.2 定义和初始化模型
这是微调的核心步骤。你需要加载一个预训练模型,并对其进行修改。
- 加载预训练模型: 使用
torchvision.models
加载一个预训练模型,例如 ResNet-18。通过设置pretrained=True
,PyTorch 会自动下载在 ImageNet 上训练好的模型参数。
pretrained_net = torchvision.models.resnet18(pretrained=True)
- 修改输出层: 检查预训练模型的输出层。对于 ResNet-18,输出层是一个全连接层(
fc
),其输出维度是 1000(因为 ImageNet 有 1000 个类别)。我们需要将其替换为适应我们新任务(热狗和非热狗,共 2 个类别)的新层。 - 随机初始化新层: 新的全连接层参数需要随机初始化,因为它是针对新任务从零开始学习的。使用
nn.init.xavier_uniform_
可以进行良好的随机初始化。
# 获取预训练模型
finetune_net = torchvision.models.resnet18(pretrained=True)
# 获取原全连接层的输入特征数
num_features = finetune_net.fc.in_features
# 替换为新的全连接层,输出类别数为2
finetune_net.fc = nn.Linear(num_features, 2)
# 随机初始化新层的参数
nn.init.xavier_uniform_(finetune_net.fc.weight)
2.3.3 设置训练参数
在微调过程中,训练参数的设置至关重要。
- 不同的学习率: 预训练部分的参数(所有层,除了最后一层)已经训练得很好,我们只需要对其进行微调,所以使用较小的学习率。而新初始化的输出层需要从头开始训练,所以使用更大的学习率。通常,输出层的学习率设置为其他层的几倍(例如 10 倍)。
- 定义优化器: 使用
torch.optim.SGD
优化器,并为不同参数组设置不同的学习率。
# 将参数分为两组:预训练层和新输出层
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x}, # 预训练层
{'params': net.fc.parameters(), 'lr': learning_rate * 10}], # 新输出层
lr=learning_rate,
weight_decay=0.001)
2.3.4 训练模型
最后,调用训练函数进行微调。
- 训练函数: 使用一个通用的训练函数,它接受网络、学习率、批量大小和迭代次数等参数。
- 比较: 为了证明微调的有效性,可以与一个从头开始训练的相同模型进行比较。从头训练的模型需要更大的学习率,并且通常表现不如微调模型,因为它的初始参数是随机的。
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
param_group=True):
train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train'), transform=train_augs),
batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'test'), transform=test_augs),
batch_size=batch_size)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss(reduction="none")
if param_group:
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x},
{'params': net.fc.parameters(),
'lr': learning_rate * 10}],
lr=learning_rate, weight_decay=0.001)
else:
trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
weight_decay=0.001)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
# 微调模型,使用较小的学习率
train_fine_tuning(finetune_net, 5e-5)
# 从头训练的模型,使用较大的学习率
# scratch_net = torchvision.models.resnet18()
# scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
# train_fine_tuning(scratch_net, 5e-4, param_group=False)