注意
点击此处下载完整示例代码
4. 使用自己的图像数据集进行迁移学习¶
数据集大小是深度学习模型性能的一个重要因素。ImageNet
拥有超过一百万张带标签的图像,但在其他领域,我们通常没有如此多的带标签数据。在小型数据集上训练深度学习模型可能导致严重的过拟合。
迁移学习是解决这个问题的一种技术。其思想很简单:我们可以从预训练模型开始训练,而不是从头开始。正如艾萨克·牛顿所说:“如果我看得更远,那是因为我站在巨人的肩膀上”。
在本教程中,我们将解释迁移学习的基础知识,并将其应用于 MINC-2500
数据集。
数据准备¶
MINC 是 Materials in Context Database 的缩写,由康奈尔大学提供。MINC-2500
是 MINC
的一个调整大小的子集,包含 23 个类别,每个类别有 2500 张图像。它标签良好且规模适中,因此非常适合作为我们的示例。
首先,我们从此处下载 MINC-2500
。假设我们将数据下载到 ~/data/
并解压到 ~/data/minc-2500
。
解压后,它占用大约 2.6GB 磁盘空间,结构如下:
minc-2500
├── README.txt
├── categories.txt
├── images
└── labels
images
文件夹包含 23 个子文件夹对应 23 个类别,labels
文件夹包含五个不同的训练、验证和测试分割。
我们编写了一个脚本为你准备数据:
运行方式:
现在我们有如下结构:
minc-2500
├── categories.txt
├── images
├── labels
├── README.txt
├── test
├── train
└── val
为了在合理的时间内完成本教程,我们准备了 MINC-2500
数据集的一个小 subset,但你在实验中应该将其替换为原始数据集。我们可以通过以下方式下载并解压它:
import zipfile, os
from gluoncv.utils import download
file_url = 'https://raw.githubusercontent.com/dmlc/web-data/master/gluoncv/classification/minc-2500-tiny.zip'
zip_file = download(file_url, path='./')
with zipfile.ZipFile(zip_file, 'r') as zin:
zin.extractall(os.path.expanduser('./'))
输出
Downloading ./minc-2500-tiny.zip from https://raw.githubusercontent.com/dmlc/web-data/master/gluoncv/classification/minc-2500-tiny.zip...
0%| | 0/8037 [00:00<?, ?KB/s]
8038KB [00:00, 92173.94KB/s]
超参数¶
首先,让我们导入所有其他必需的库。
import mxnet as mx
import numpy as np
import os, time, shutil
from mxnet import gluon, image, init, nd
from mxnet import autograd as ag
from mxnet.gluon import nn
from mxnet.gluon.data.vision import transforms
from gluoncv.utils import makedirs
from gluoncv.model_zoo import get_model
我们设置超参数如下:
classes = 23
epochs = 5
lr = 0.001
per_device_batch_size = 1
momentum = 0.9
wd = 0.0001
lr_factor = 0.75
lr_steps = [10, 20, 30, np.inf]
num_gpus = 1
num_workers = 8
ctx = [mx.gpu(i) for i in range(num_gpus)] if num_gpus > 0 else [mx.cpu()]
batch_size = per_device_batch_size * max(num_gpus, 1)
需要记住的事项:
epochs = 5
仅适用于本教程中的小型数据集。在你的实验中请将其更改为更大的数字,例如 40。per_device_batch_size
也设置为一个较小的数字。在你的实验中可以尝试更大的数字,例如 64。记住根据你的机器调整
num_gpus
和num_workers
。预训练模型已经处于相当好的状态。所以我们可以从较小的
lr
开始。
数据增强¶
在迁移学习中,数据增强也能有所帮助。我们在训练中使用以下增强方法:
随机裁剪图像并将其调整为 224x224
随机水平翻转图像
随机抖动颜色并添加噪声
将数据从 height*width*num_channels 转换为 num_channels*height*width,并将值从 [0, 255] 映射到 [0, 1]
使用 ImageNet 数据集的均值和标准差进行归一化。
jitter_param = 0.4
lighting_param = 0.1
transform_train = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomFlipLeftRight(),
transforms.RandomColorJitter(brightness=jitter_param, contrast=jitter_param,
saturation=jitter_param),
transforms.RandomLighting(lighting_param),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
transform_test = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
通过数据增强函数,我们可以定义数据加载器:
path = './minc-2500-tiny'
train_path = os.path.join(path, 'train')
val_path = os.path.join(path, 'val')
test_path = os.path.join(path, 'test')
train_data = gluon.data.DataLoader(
gluon.data.vision.ImageFolderDataset(train_path).transform_first(transform_train),
batch_size=batch_size, shuffle=True, num_workers=num_workers)
val_data = gluon.data.DataLoader(
gluon.data.vision.ImageFolderDataset(val_path).transform_first(transform_test),
batch_size=batch_size, shuffle=False, num_workers = num_workers)
test_data = gluon.data.DataLoader(
gluon.data.vision.ImageFolderDataset(test_path).transform_first(transform_test),
batch_size=batch_size, shuffle=False, num_workers = num_workers)
注意,只有 train_data
使用 transform_train
,而 val_data
和 test_data
使用 transform_test
来产生确定性的评估结果。
模型和训练器¶
我们使用预训练的 ResNet50_v2
模型,它在准确性和计算成本之间取得了平衡。
model_name = 'ResNet50_v2'
finetune_net = get_model(model_name, pretrained=True)
with finetune_net.name_scope():
finetune_net.output = nn.Dense(classes)
finetune_net.output.initialize(init.Xavier(), ctx = ctx)
finetune_net.collect_params().reset_ctx(ctx)
finetune_net.hybridize()
trainer = gluon.Trainer(finetune_net.collect_params(), 'sgd', {
'learning_rate': lr, 'momentum': momentum, 'wd': wd})
metric = mx.metric.Accuracy()
L = gluon.loss.SoftmaxCrossEntropyLoss()
输出
Downloading /root/.mxnet/models/resnet50_v2-ecdde353.zip from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/resnet50_v2-ecdde353.zip...
0%| | 0/92862 [00:00<?, ?KB/s]
0%| | 100/92862 [00:00<01:58, 780.93KB/s]
1%| | 507/92862 [00:00<00:42, 2184.10KB/s]
2%|2 | 2179/92862 [00:00<00:12, 7118.55KB/s]
8%|8 | 7641/92862 [00:00<00:03, 23097.88KB/s]
14%|#3 | 12735/92862 [00:00<00:02, 32134.74KB/s]
22%|##1 | 20388/92862 [00:00<00:01, 46198.04KB/s]
29%|##8 | 26538/92862 [00:00<00:01, 50964.15KB/s]
37%|###7 | 34677/92862 [00:00<00:01, 57370.37KB/s]
46%|####5 | 42517/92862 [00:01<00:00, 63571.97KB/s]
53%|#####3 | 49220/92862 [00:01<00:00, 64596.44KB/s]
62%|######1 | 57553/92862 [00:01<00:00, 70161.55KB/s]
70%|######9 | 64781/92862 [00:01<00:00, 70790.83KB/s]
78%|#######8 | 72893/92862 [00:01<00:00, 73875.46KB/s]
86%|########6 | 80320/92862 [00:01<00:00, 73447.70KB/s]
96%|#########5| 88777/92862 [00:01<00:00, 76757.41KB/s]
92863KB [00:01, 55900.26KB/s]
这是预训练模型和我们新定义模型的示意图:
具体来说,我们通过以下步骤定义新模型:
加载预训练模型
为新任务重新定义输出层
训练网络
这被称为“微调”,即我们有一个在其他任务上训练过的模型,并希望针对我们手头的数据集对其进行调整。
我们定义一个评估函数用于验证和测试。
def test(net, val_data, ctx):
metric = mx.metric.Accuracy()
for i, batch in enumerate(val_data):
data = gluon.utils.split_and_load(batch[0], ctx_list=ctx, batch_axis=0, even_split=False)
label = gluon.utils.split_and_load(batch[1], ctx_list=ctx, batch_axis=0, even_split=False)
outputs = [net(X) for X in data]
metric.update(label, outputs)
return metric.get()
训练循环¶
以下是主要的训练循环。它与CIFAR10 和 ImageNet 中的循环相同。
注意
再次强调,为了更快地完成教程,我们正在原始 MINC-2500
数据集的一个小 subset 上进行训练,并且只训练 5 个 epoch。通过在完整数据集上训练 40 个 epoch,预计在测试数据上可以达到大约 80% 的准确率。
lr_counter = 0
num_batch = len(train_data)
for epoch in range(epochs):
if epoch == lr_steps[lr_counter]:
trainer.set_learning_rate(trainer.learning_rate*lr_factor)
lr_counter += 1
tic = time.time()
train_loss = 0
metric.reset()
for i, batch in enumerate(train_data):
data = gluon.utils.split_and_load(batch[0], ctx_list=ctx, batch_axis=0, even_split=False)
label = gluon.utils.split_and_load(batch[1], ctx_list=ctx, batch_axis=0, even_split=False)
with ag.record():
outputs = [finetune_net(X) for X in data]
loss = [L(yhat, y) for yhat, y in zip(outputs, label)]
for l in loss:
l.backward()
trainer.step(batch_size)
train_loss += sum([l.mean().asscalar() for l in loss]) / len(loss)
metric.update(label, outputs)
_, train_acc = metric.get()
train_loss /= num_batch
_, val_acc = test(finetune_net, val_data, ctx)
print('[Epoch %d] Train-acc: %.3f, loss: %.3f | Val-acc: %.3f | time: %.1f' %
(epoch, train_acc, train_loss, val_acc, time.time() - tic))
_, test_acc = test(finetune_net, test_data, ctx)
print('[Finished] Test-acc: %.3f' % (test_acc))
输出
[Epoch 0] Train-acc: 0.026, loss: 4.044 | Val-acc: 0.065 | time: 4.6
[Epoch 1] Train-acc: 0.017, loss: 4.177 | Val-acc: 0.022 | time: 3.0
[Epoch 2] Train-acc: 0.035, loss: 4.017 | Val-acc: 0.043 | time: 3.0
[Epoch 3] Train-acc: 0.009, loss: 3.971 | Val-acc: 0.022 | time: 3.0
[Epoch 4] Train-acc: 0.009, loss: 3.643 | Val-acc: 0.043 | time: 3.0
[Finished] Test-acc: 0.087