在这场比赛中,我们将识别120类不同品种的狗。 这个数据集实际上是著名的ImageNet的数据集子集
import os
from mxnet import autograd, gluon, init, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
获取和整理数据集
比赛数据集分为训练集和测试集,分别包含RGB(彩色)通道的10222张、10357张JPEG图像。 在训练数据集中,有120种犬类,如拉布拉多、贵宾、腊肠、萨摩耶、哈士奇、吉娃娃和约克夏等。
为了便于入门,[我们提供完整数据集的小规模样本]:train_valid_test_tiny.zip
。 如果要在Kaggle比赛中使用完整的数据集,则需要将下面的demo
变量更改为False
。
#@save
d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip',
'0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d')
# 如果使用Kaggle比赛的完整数据集,请将下面的变量更改为False
demo = True
if demo:
data_dir = d2l.download_extract('dog_tiny')
else:
data_dir = os.path.join('..', 'data', 'dog-breed-identification')
[整理数据集]
我们可以整理数据集,即从原始训练集中拆分验证集,然后将图像移动到按标签分组的子文件夹中。
下面的reorg_dog_data
函数读取训练数据标签、拆分验证集并整理训练集。
def reorg_dog_data(data_dir, valid_ratio):
labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv'))
d2l.reorg_train_valid(data_dir, labels, valid_ratio)
d2l.reorg_test(data_dir)
batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_dog_data(data_dir, valid_ratio)
net.hybridize():
- 这行代码调用了
hybridize
方法,它将网络设置为混合执行模式。在 MXNet 中,这允许模型在编译模式下运行,可以提高性能。hybridize
方法通常在模型定义之后和训练之前调用。reorg_dog_data 函数:
def reorg_dog_data(data_dir, valid_ratio)
: 这个函数接受数据集的目录data_dir
和验证集比例valid_ratio
作为参数。labels = d2l.read_csv_labels(...)
: 读取 CSV 文件来获取数据集的标签。d2l.reorg_train_valid(...)
: 调用自定义函数来组织训练和验证数据,根据valid_ratio
拆分验证集。d2l.reorg_test(...)
: 调用自定义函数来组织测试集数据。设置训练参数:
batch_size
: 设置批次大小。这里使用了一个条件表达式来根据变量demo
的值设置不同的批次大小。如果demo
为真,则使用较小的批次大小,这可能是为了演示或测试目的;否则,使用较大的批次大小。valid_ratio
: 设置验证集占总训练集的比例。调用 reorg_dog_data 函数:
reorg_dog_data(data_dir, valid_ratio)
: 使用指定的数据集目录和验证集比例来组织数据。
[图像增广]¶
transform_train = gluon.data.vision.transforms.Compose([
# 随机裁剪图像,所得图像为原始面积的0.08~1之间,高宽比在3/4和4/3之间。
# 然后,缩放图像以创建224x224的新图像
gluon.data.vision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
ratio=(3.0/4.0, 4.0/3.0)),
gluon.data.vision.transforms.RandomFlipLeftRight(),
# 随机更改亮度,对比度和饱和度
gluon.data.vision.transforms.RandomColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4),
# 添加随机噪声
gluon.data.vision.transforms.RandomLighting(0.1),
gluon.data.vision.transforms.ToTensor(),
# 标准化图像的每个通道
gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
测试时,我们只使用确定性的图像预处理操作。
transform_test = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.Resize(256),
# 从图像中心裁切224x224大小的图片
gluon.data.vision.transforms.CenterCrop(224),
gluon.data.vision.transforms.ToTensor(),
gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
[读取数据集]
我们可以读取整理后的含原始图像文件的数据集。
train_ds, valid_ds, train_valid_ds, test_ds = [
gluon.data.vision.ImageFolderDataset(
os.path.join(data_dir, 'train_valid_test', folder))
for folder in ('train', 'valid', 'train_valid', 'test')]
下面我们创建数据加载器实例
train_iter, train_valid_iter = [gluon.data.DataLoader(
dataset.transform_first(transform_train), batch_size, shuffle=True,
last_batch='discard') for dataset in (train_ds, train_valid_ds)]
valid_iter = gluon.data.DataLoader(
valid_ds.transform_first(transform_test), batch_size, shuffle=False,
last_batch='discard')
test_iter = gluon.data.DataLoader(
test_ds.transform_first(transform_test), batch_size, shuffle=False,
last_batch='keep')
数据迭代器创建:
train_iter
和train_valid_iter
:这两个迭代器分别用于普通的训练数据和训练加验证数据的联合数据集。使用列表推导式同时创建这两个迭代器,以便应用相同的转换和设置。valid_iter
和test_iter
:这两个迭代器分别用于验证集和测试集。它们使用相似的设置,但没有洗牌,因为验证集和测试集的数据顺序通常是固定的。DataLoader参数说明:
dataset.transform_first(transform_train)
和dataset.transform_first(transform_test)
:这些调用将预定义的变换transform_train
和transform_test
应用到数据集的每个元素上。transform_first
表示首先对数据集中的每个项目应用这些变换。batch_size
:每个批次加载的样本数量。这个变量需要在外部定义。shuffle=True
:在DataLoader
中,shuffle
参数默认为True
,表示在每个epoch开始时对数据进行洗牌。这里明确设置为True
以确保这一点。last_batch='discard'
:表示如果最后一个批次的样本数量少于batch_size
,则丢弃这个批次。这通常用于训练和验证迭代器。last_batch='keep'
:表示即使最后一个批次的样本数量少于batch_size
,也保留这个批次。这通常用于测试迭代器,以确保所有样本都被评估。变换流程:
transform_train
和transform_test
:这些变换流程应该已经定义好,用于训练和测试/验证数据的预处理。它们可能包括图像大小调整、裁剪、归一化等操作。
[微调预训练模型]
在这里,我们选择预训练的ResNet-34模型,我们只需重复使用此模型的输出层(即提取的特征)的输入。 然后,我们可以用一个可以训练的小型自定义输出网络替换原始输出层,例如堆叠两个完全连接的图层。回想一下,我们使用三个RGB通道的均值和标准差来对完整的ImageNet数据集进行图像标准化。 事实上,这也符合ImageNet上预训练模型的标准化操作。
def get_net(devices):
finetune_net = gluon.model_zoo.vision.resnet34_v2(pretrained=True)
# 定义一个新的输出网络
finetune_net.output_new = nn.HybridSequential(prefix='')
finetune_net.output_new.add(nn.Dense(256, activation='relu'))
# 共有120个输出类别
finetune_net.output_new.add(nn.Dense(120))
# 初始化输出网络
finetune_net.output_new.initialize(init.Xavier(), ctx=devices)
# 将模型参数分配给用于计算的CPU或GPU
finetune_net.collect_params().reset_ctx(devices)
return finetune_net
在[计算损失]之前,我们首先获取预训练模型的输出层的输入,即提取的特征。 然后我们使用此特征作为我们小型自定义输出网络的输入来计算损失。
loss = gluon.loss.SoftmaxCrossEntropyLoss()
def evaluate_loss(data_iter, net, devices):
l_sum, n = 0.0, 0
for features, labels in data_iter:
X_shards, y_shards = d2l.split_batch(features, labels, devices)
output_features = [net.features(X_shard) for X_shard in X_shards]
outputs = [net.output_new(feature) for feature in output_features]
ls = [loss(output, y_shard).sum() for output, y_shard
in zip(outputs, y_shards)]
l_sum += sum([float(l.sum()) for l in ls])
n += labels.size
return l_sum / n
定义[训练函数]
我们将根据模型在验证集上的表现选择模型并调整超参数。 模型训练函数train
只迭代小型自定义输出网络的参数。
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
# 只训练小型自定义输出网络
trainer = gluon.Trainer(net.output_new.collect_params(), 'sgd',
{'learning_rate': lr, 'momentum': 0.9, 'wd': wd})
num_batches, timer = len(train_iter), d2l.Timer()
legend = ['train loss']
if valid_iter is not None:
legend.append('valid loss')
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
for epoch in range(num_epochs):
metric = d2l.Accumulator(2)
if epoch > 0 and epoch % lr_period == 0:
trainer.set_learning_rate(trainer.learning_rate * lr_decay)
for i, (features, labels) in enumerate(train_iter):
timer.start()
X_shards, y_shards = d2l.split_batch(features, labels, devices)
output_features = [net.features(X_shard) for X_shard in X_shards]
with autograd.record():
outputs = [net.output_new(feature)
for feature in output_features]
ls = [loss(output, y_shard).sum() for output, y_shard
in zip(outputs, y_shards)]
for l in ls:
l.backward()
trainer.step(batch_size)
metric.add(sum([float(l.sum()) for l in ls]), labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[1], None))
if valid_iter is not None:
valid_loss = evaluate_loss(valid_iter, net, devices)
animator.add(epoch + 1, (None, valid_loss))
measures = f'train loss {metric[0] / metric[1]:.3f}'
if valid_iter is not None:
measures += f', valid loss {valid_loss:.3f}'
print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')
函数定义:
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay)
: 定义了一个训练函数,接受模型、训练数据迭代器、验证数据迭代器、训练轮数、学习率、权重衰减、设备列表、学习率衰减周期和衰减率作为参数。训练器初始化:
trainer
: 使用SGD优化器创建一个gluon.Trainer
实例,只针对模型的output_new
部分的参数进行训练。训练和验证设置:
num_batches
: 计算训练迭代器中的批次总数。timer
: 创建一个d2l.Timer
实例,用于测量训练和评估的时间。legend
: 定义了动画器图例,根据是否有验证迭代器,可能包含训练损失和验证损失。animator
: 使用d2l.Animator
创建一个动画器,用于可视化训练过程。训练循环:
- 外层循环遍历
num_epochs
指定的训练轮数。- 如果当前epoch是学习率衰减周期的整数倍,则更新学习率。
- 内层循环遍历训练迭代器中的所有批次。
- 使用
d2l.split_batch
函数将数据分发到多个设备上。- 计算每个批次的损失,执行反向传播,并更新模型参数。
- 更新动画器,显示训练进度和性能指标。
- 如果有验证迭代器,使用
evaluate_loss
函数评估验证集上的损失,并更新动画器。性能输出:
- 在每个epoch结束时,打印出训练损失和(如果有的话)验证损失。
- 计算并打印出模型在训练过程中处理的样本数的速度。
注意事项:
- 这段代码中使用了一些自定义函数和类,如
d2l.Accumulator
、d2l.split_batch
和evaluate_loss
,它们可能来自 "Dive into Deep Learning" 的代码库。net.features
和net.output_new
应该是模型的两个部分,其中output_new
是要训练的自定义输出网络。devices
参数应该是一个设备列表,用于指定模型应该在哪些设备上运行。lr_period
和lr_decay
参数用于实现学习率衰减策略。
[训练和验证模型]
现在我们可以训练和验证模型了,以下超参数都是可调的。 例如,我们可以增加迭代轮数。 另外,由于lr_period
和lr_decay
分别设置为2和0.9, 因此优化算法的学习速率将在每2个迭代后乘以0.9
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 5e-3, 1e-4
lr_period, lr_decay, net = 2, 0.9, get_net(devices)
net.hybridize()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)