Mxnet (32): 迁移学习(Fine-Tuning)进行热狗识别

    科技2022-07-10  103

    总所周知,对于训练模型,一定是数据量越大准去率越高,同时越难以过拟合,泛化能力更强。一些模型训练的数据太少,当应用的时候输入的数据范围又太大,导致最终训练的模型的准确性可能无法满足实际需求。 为了解决上面的问题,一个方法就是获取更多的数据,但是获取数据是一个比较浪费金钱以及时间的事情。另一个方法就是通过迁移学习,将学习到的知识从源数据集迁移到目标数据集。比如,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 224224像素的输入。测试过程中,将图片缩放为宽高为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.代码

    github

    Processed: 0.042, SQL: 8