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

浪子不回头ぞ 提交于 2020-10-04 09:51:50

总所周知,对于训练模型,一定是数据量越大准去率越高,同时越难以过拟合,泛化能力更强。一些模型训练的数据太少,当应用的时候输入的数据范围又太大,导致最终训练的模型的准确性可能无法满足实际需求。
为了解决上面的问题,一个方法就是获取更多的数据,但是获取数据是一个比较浪费金钱以及时间的事情。另一个方法就是通过迁移学习,将学习到的知识从源数据集迁移到目标数据集。比如,ImageNet中的图像大部分与椅子无关,但是在此数据集上训练的模型可以提取更通用的图像特征,这些特征可以帮助识别边缘,纹理,形状以及对象组成。这些特性可能对椅子同样有效。
本篇使用迁移学习中的一种常见技术:Fine-Tuning:

  1. 在源数据集上训练模型(源模型)。
  2. 创建一个新的模型,即目标模型。目标模型复制所有源模型中的结构以及参数。可以认为源模型参数中包含了从源数据集中学到的知识,并且将这些知识应用与目标数据集。
  3. 将满足目标数据集的输出层添加到目标模型上,并初始话输出层的参数。
  4. 使用目标数据在组装之后的模型上训练。从头开始训练输出层,而且它层的参数根据源模型参数进行微调。

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!