总所周知,对于训练模型,一定是数据量越大准去率越高,同时越难以过拟合,泛化能力更强。一些模型训练的数据太少,当应用的时候输入的数据范围又太大,导致最终训练的模型的准确性可能无法满足实际需求。
为了解决上面的问题,一个方法就是获取更多的数据,但是获取数据是一个比较浪费金钱以及时间的事情。另一个方法就是通过迁移学习,将学习到的知识从源数据集迁移到目标数据集。比如,ImageNet中的图像大部分与椅子无关,但是在此数据集上训练的模型可以提取更通用的图像特征,这些特征可以帮助识别边缘,纹理,形状以及对象组成。这些特性可能对椅子同样有效。
本篇使用迁移学习中的一种常见技术:Fine-Tuning:
- 在源数据集上训练模型(源模型)。
- 创建一个新的模型,即目标模型。目标模型复制所有源模型中的结构以及参数。可以认为源模型参数中包含了从源数据集中学到的知识,并且将这些知识应用与目标数据集。
- 将满足目标数据集的输出层添加到目标模型上,并初始话输出层的参数。
- 使用目标数据在组装之后的模型上训练。从头开始训练输出层,而且它层的参数根据源模型参数进行微调。
Fine-Tuning实战:热狗识别
通过热狗识别的例子了解Fine-Tuning的用法。这里使用基于ImageNet数据集上训练的ResNet模型进行微调。这个热狗数据集包含千张图片,其中包含一些热狗的图片。通过微调而来的模型来识别图片中是否含有热狗。
Gluon的model_zoo软件包提供了通用的预训练模型。如果要获得更多的计算机视觉预训练模型,可以使用GluonCV Toolkit。
from d2l import mxnet as d2l
from mxnet import gluon, np, npx, init, autograd
from mxnet.gluon import nn
from plotly import graph_objs as go, express as px
from plotly.subplots import make_subplots
from IPython.display import Image
import plotly.io as pio
import os
pio.kaleido.scope.default_format = "svg"
npx.set_np()
1. 获取数据集
热狗数据集来自在线图像,包含1400个热狗的阳性图像和包含其他食物的阴性图像数量相同。1,000各种类别的图像用于训练,其余的用于测试,就是一共2800个样本,1400个是热狗,1400个不是,其中1000个热狗样本和1000个非热狗作为训练,剩余800个作为测试。
将数据下载到本地,然后解压,然后使用ImageFolderDataset加载数据。
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL+'hotdog.zip',
'fba480ffa8aa7e0febbb511d181409f899b9baa5')
data_dir = d2l.download_extract('hotdog')
train_imgs = gluon.data.vision.ImageFolderDataset(os.path.join(data_dir, 'train'))
test_imgs = gluon.data.vision.ImageFolderDataset(os.path.join(data_dir, 'test'))
获取8个阳性结果(热狗),以及8个阴性结果(其他图片)
def show_imgs(imgs, num_rows=2, num_cols=4, scale=0.8) :
fig = make_subplots(num_rows, num_cols)
for i in range(num_rows):
for j in range(num_cols):
fig.add_trace(go.Image(z=imgs[num_cols*i+j].asnumpy()),i+1,j+1)
fig.update_xaxes(visible=False, row=i+1, col=j+1)
fig.update_yaxes(visible=False, row=i+1, col=j+1)
img_bytes = fig.to_image(format="png", scale=scale, engine="kaleido")
return img_bytes
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
Image(show_imgs(hotdogs + not_hotdogs, 2, 8, scale=1.4))
首先处理图片:从图片中裁剪出具有随机大小和随机纵横比的区域,然后将区域缩放为 224 ∗ 224 224*224 224∗224像素的输入。测试过程中,将图片缩放为宽高为256像素,然后在图片的中心区域截取宽高224的区域作为输入。此外将RGB三个通道归一化。
# 归一化
normalize = gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
train_augs = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.RandomResizedCrop(224), # 随机裁剪并resize
gluon.data.vision.transforms.RandomFlipLeftRight(), # 左右翻转
gluon.data.vision.transforms.ToTensor(),
normalize])
test_augs = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.Resize(256), # resize
gluon.data.vision.transforms.CenterCrop(224), # 中间裁剪
gluon.data.vision.transforms.ToTensor(),
normalize])
2. 初始化模型
使用在ImageNet数据集上经过预训练的ResNet-18作为源模型。通过gluon.model_zoo模块获取模型,pretrained=True自动下载并加载预训练的模型参数。首次使用的话需要在网上下载模型和参数。
pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True)
预训练的源模型有两个成员变量:features和output。前者为除去输出层的所有层,后者为输出层。这样划分主要是方便衔接自己模型的输出层,而对其他层进行微调。
这是创建目标模型,同样使用model_zoo获取模型,通过classes指定分类的类型,即输出数量。
finetune_net = gluon.model_zoo.vision.resnet18_v2(classes=2)
将预训练模型的features复制给目标模型,并初始化目标模型的输出层。成员变量output中的模型参数是随机初始化的,通常需要更高的学习率才能从头开始学习,需要调高学习率。这里设置输出层的lr_mult为10,意味着输出层的学习率为其他层的10倍。
finetune_net.features = pretrained_net.features
finetune_net.output.initialize(init.Xavier())
# 设置输出层学习率为10倍
finetune_net.output.collect_params().setattr('lr_mult', 10)
3. 微调模型
定义train_fine_tuning函数用于训练模型
def accuracy(y_hat, y):
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.astype(y.dtype) == y
return float(cmp.sum())
def train_batch(net, features, labels, loss, trainer, devices, split_f=d2l.split_batch):
X_shards, y_shards = split_f(features, labels, devices)
with autograd.record():
pred_shards = [net(X_shard) for X_shard in X_shards]
ls = [loss(pred_shard, y_shard) for pred_shard, y_shard
in zip(pred_shards, y_shards)]
for l in ls:
l.backward()
# ignore_stale_grad代表可以使用就得梯度参数
trainer.step(labels.shape[0], ignore_stale_grad=True)
train_loss_sum = sum([float(l.sum()) for l in ls])
train_acc_sum = sum(accuracy(pred_shard, y_shard)
for pred_shard, y_shard in zip(pred_shards, y_shards))
return train_loss_sum, train_acc_sum
def train(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus(), split_f=d2l.split_batch):
num_batches, timer = len(train_iter), d2l.Timer()
epochs_lst, loss_lst, train_acc_lst, test_acc_lst = [],[],[],[]
for epoch in range(num_epochs):
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch(
net, features, labels, loss, trainer, devices, split_f)
metric.add(l, acc, labels.shape[0], labels.size)
timer.stop()
if (i + 1) % (num_batches // 5) == 0:
epochs_lst.append(epoch + i / num_batches)
loss_lst.append(metric[0] / metric[2])
train_acc_lst.append(metric[1] / metric[3])
test_acc_lst.append(d2l.evaluate_accuracy_gpus(net, test_iter, split_f))
print(f"[epock {epoch+1}] train loss: {metric[0] / metric[2]:.3f} train acc: {metric[1] / metric[3]:.3f}",
f" test_loss: {test_acc_lst[-1]:.3f}")
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc_lst[-1]:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')
fig = go.Figure()
fig.add_trace(go.Scatter(x=epochs_lst, y=loss_lst, name='train loss'))
fig.add_trace(go.Scatter(x=epochs_lst, y=train_acc_lst, name='train acc'))
fig.add_trace(go.Scatter(x=list(range(1,len(test_acc_lst)+1)), y=test_acc_lst, name='test acc'))
fig.update_layout(width=800, height=480, xaxis_title='epoch', yaxis_range=[0, 1])
fig.show()
def train_fine_tuning(net, learning_rate, batch_size=64, num_epochs=5):
train_iter = gluon.data.DataLoader(train_imgs.transform_first(train_augs), batch_size, shuffle=True)
test_iter = gluon.data.DataLoader(test_imgs.transform_first(test_augs), batch_size)
net.collect_params().reset_ctx(npx.gpu())
net.hybridize()
loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {
'learning_rate': learning_rate, 'wd': 0.001})
train(net, train_iter, test_iter, loss, trainer, num_epochs, [npx.gpu()])
进行训练, 由于预训练模型已经训练过因此学习率给的比较低:0.01
train_fine_tuning(finetune_net, 0.01)
可以看到仅仅5个epoch就有0.94的准度,确实很快
为了比较迁移学习的效果,这里创建一个同样的模型,但是不复制预训练模型的参数,而是全部初始化,对比一些训练效果。由于是重新开始训练,因此提高了学习率:0.1
scratch_net = gluon.model_zoo.vision.resnet18_v2(classes=2)
scratch_net.initialize(init=init.Xavier())
train_fine_tuning(scratch_net, 0.1)
很明显由于参数的初始值更好,因此经过微调的模型倾向于在同一时期获得更高的精度。
4.参考
https://d2l.ai/chapter_computer-vision/fine-tuning.html
5.代码
来源:oschina
链接:https://my.oschina.net/u/4365283/blog/4659347