08. Finetune a pretrained detection model

Fine-tuning is commonly used approach to transfer previously trained model to a new dataset. It is especially useful if the targeting new dataset is relatively small.

Finetuning from pre-trained models can help reduce the risk of overfitting. Finetuned model may also generalizes better if the previously used dataset is in the similar domain of the new dataset.

This tutorial opens up a good approach for fine-tuning object detection models provided by GluonCV. More Specifically, we show how to use a customized Pikachu dataset and illustrate the finetuning fundamentals step by step. You will be familiarize the steps and modify it to fit your own object detection projects.

import time
from matplotlib import pyplot as plt
import numpy as np
import mxnet as mx
from mxnet import autograd, gluon
import gluoncv as gcv
from gluoncv.utils import download, viz

Pikachu Dataset

First we will start with a nice Pikachu dataset generated by rendering 3D models on random real-world scenes. You can refer to Prepare custom datasets for object detection for tutorial of how to create your own datasets.

url = 'https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/pikachu/train.rec'
idx_url = 'https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/pikachu/train.idx'
download(url, path='pikachu_train.rec', overwrite=False)
download(idx_url, path='pikachu_train.idx', overwrite=False)


Downloading pikachu_train.rec from https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/pikachu/train.rec...

  0%|          | 0/85604 [00:00<?, ?KB/s]
  1%|1         | 916/85604 [00:00<00:14, 5901.62KB/s]
  5%|4         | 4185/85604 [00:00<00:04, 18739.15KB/s]
 10%|#         | 8738/85604 [00:00<00:02, 29625.62KB/s]
 17%|#6        | 14453/85604 [00:00<00:01, 38119.91KB/s]
 25%|##4       | 21265/85604 [00:00<00:01, 48164.18KB/s]
 33%|###3      | 28381/85604 [00:00<00:01, 55603.92KB/s]
 40%|###9      | 34107/85604 [00:00<00:00, 53033.90KB/s]
 48%|####8     | 41130/85604 [00:00<00:00, 58176.13KB/s]
 56%|#####6    | 48289/85604 [00:00<00:00, 62190.38KB/s]
 64%|######3   | 54610/85604 [00:01<00:00, 58774.00KB/s]
 72%|#######2  | 61677/85604 [00:01<00:00, 62185.75KB/s]
 79%|#######9  | 67990/85604 [00:01<00:00, 58992.69KB/s]
 87%|########7 | 74844/85604 [00:01<00:00, 61679.37KB/s]
 96%|#########5| 81814/85604 [00:01<00:00, 63979.27KB/s]
85605KB [00:01, 53444.27KB/s]
Downloading pikachu_train.idx from https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/pikachu/train.idx...

  0%|          | 0/11 [00:00<?, ?KB/s]
12KB [00:00, 7024.65KB/s]

We can load dataset using RecordFileDetection

dataset = gcv.data.RecordFileDetection('pikachu_train.rec')
classes = ['pikachu']  # only one foreground class here
image, label = dataset[0]
print('label:', label)
# display image and label
ax = viz.plot_bbox(image, bboxes=label[:, :4], labels=label[:, 4:5], class_names=classes)
finetune detection


label: [[309.6292  205.79944 355.75494 274.14044   0.     ]]

Pre-trained models

Now we can grab a pre-trained model to finetune from. Here we have so many choices from Detection Model Zoo. Again for demo purpose, we choose a fast SSD network with MobileNet1.0 backbone.

net = gcv.model_zoo.get_model('ssd_512_mobilenet1.0_voc', pretrained=True)


/usr/local/lib/python3.6/dist-packages/mxnet/gluon/block.py:1512: UserWarning: Cannot decide type for the following arguments. Consider providing them as input:
        data: None
  input_sym_arg_type = in_param.infer_type()[0]
Downloading /root/.mxnet/models/ssd_512_mobilenet1.0_voc-37c18076.zip from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/ssd_512_mobilenet1.0_voc-37c18076.zip...

  0%|          | 0/50216 [00:00<?, ?KB/s]
  2%|1         | 985/50216 [00:00<00:06, 7084.99KB/s]
 10%|#         | 5030/50216 [00:00<00:02, 19796.16KB/s]
 25%|##4       | 12390/50216 [00:00<00:00, 39915.50KB/s]
 39%|###9      | 19780/50216 [00:00<00:00, 50246.75KB/s]
 54%|#####4    | 27318/50216 [00:00<00:00, 58430.57KB/s]
 68%|######8   | 34212/50216 [00:00<00:00, 59677.08KB/s]
 83%|########3 | 41850/50216 [00:00<00:00, 64765.03KB/s]
 98%|#########7| 49165/50216 [00:00<00:00, 64953.39KB/s]
50217KB [00:00, 54283.99KB/s]

reset network to predict pikachus!

# now the output layers that used to map to VOC classes are now reset to distinguish pikachu (and background).

There is a convenient API for creating custom network with pre-trained weights. This is equivalent to loading pre-trained model and call net.reset_class.

net = gcv.model_zoo.get_model('ssd_512_mobilenet1.0_custom', classes=classes,
    pretrained_base=False, transfer='voc')


/usr/local/lib/python3.6/dist-packages/mxnet/gluon/block.py:1512: UserWarning: Cannot decide type for the following arguments. Consider providing them as input:
        data: None
  input_sym_arg_type = in_param.infer_type()[0]

By loading from fully pre-trained models, you are not only loading base network weights (mobilenet for example), but also some additional blocks for object detection specifically.

Pretrained model from detection task is more relevant and adaptive than pretrained_base network which is usually trained on ImageNet for image classification task.

Therefore finetuning may converge significantly faster and better in some situations.

Finetuning is a new round of training


You will find a more detailed training implementation of SSD here: Download train_ssd.py

def get_dataloader(net, train_dataset, data_shape, batch_size, num_workers):
    from gluoncv.data.batchify import Tuple, Stack, Pad
    from gluoncv.data.transforms.presets.ssd import SSDDefaultTrainTransform
    width, height = data_shape, data_shape
    # use fake data to generate fixed anchors for target generation
    with autograd.train_mode():
        _, _, anchors = net(mx.nd.zeros((1, 3, height, width)))
    batchify_fn = Tuple(Stack(), Stack(), Stack())  # stack image, cls_targets, box_targets
    train_loader = gluon.data.DataLoader(
        train_dataset.transform(SSDDefaultTrainTransform(width, height, anchors)),
        batch_size, True, batchify_fn=batchify_fn, last_batch='rollover', num_workers=num_workers)
    return train_loader

train_data = get_dataloader(net, dataset, 512, 16, 0)

Try use GPU for training

    a = mx.nd.zeros((1,), ctx=mx.gpu(0))
    ctx = [mx.gpu(0)]
    ctx = [mx.cpu()]

Start training(finetuning)

trainer = gluon.Trainer(
    net.collect_params(), 'sgd',
    {'learning_rate': 0.001, 'wd': 0.0005, 'momentum': 0.9})

mbox_loss = gcv.loss.SSDMultiBoxLoss()
ce_metric = mx.metric.Loss('CrossEntropy')
smoothl1_metric = mx.metric.Loss('SmoothL1')

for epoch in range(0, 2):
    tic = time.time()
    btic = time.time()
    net.hybridize(static_alloc=True, static_shape=True)
    for i, batch in enumerate(train_data):
        batch_size = batch[0].shape[0]
        data = gluon.utils.split_and_load(batch[0], ctx_list=ctx, batch_axis=0)
        cls_targets = gluon.utils.split_and_load(batch[1], ctx_list=ctx, batch_axis=0)
        box_targets = gluon.utils.split_and_load(batch[2], ctx_list=ctx, batch_axis=0)
        with autograd.record():
            cls_preds = []
            box_preds = []
            for x in data:
                cls_pred, box_pred, _ = net(x)
            sum_loss, cls_loss, box_loss = mbox_loss(
                cls_preds, box_preds, cls_targets, box_targets)
        # since we have already normalized the loss, we don't want to normalize
        # by batch-size anymore
        ce_metric.update(0, [l * batch_size for l in cls_loss])
        smoothl1_metric.update(0, [l * batch_size for l in box_loss])
        name1, loss1 = ce_metric.get()
        name2, loss2 = smoothl1_metric.get()
        if i % 20 == 0:
            print('[Epoch {}][Batch {}], Speed: {:.3f} samples/sec, {}={:.3f}, {}={:.3f}'.format(
                epoch, i, batch_size/(time.time()-btic), name1, loss1, name2, loss2))
        btic = time.time()


[Epoch 0][Batch 0], Speed: 8.497 samples/sec, CrossEntropy=11.958, SmoothL1=1.986
[Epoch 0][Batch 20], Speed: 19.821 samples/sec, CrossEntropy=4.359, SmoothL1=1.229
[Epoch 0][Batch 40], Speed: 21.074 samples/sec, CrossEntropy=3.300, SmoothL1=0.966
[Epoch 1][Batch 0], Speed: 20.852 samples/sec, CrossEntropy=1.530, SmoothL1=0.327
[Epoch 1][Batch 20], Speed: 19.898 samples/sec, CrossEntropy=1.613, SmoothL1=0.455
[Epoch 1][Batch 40], Speed: 20.199 samples/sec, CrossEntropy=1.578, SmoothL1=0.458

Save finetuned weights to disk


Predict with finetuned model

We can test the performance using finetuned weights

test_url = 'https://raw.githubusercontent.com/zackchase/mxnet-the-straight-dope/master/img/pikachu.jpg'
download(test_url, 'pikachu_test.jpg')
net = gcv.model_zoo.get_model('ssd_512_mobilenet1.0_custom', classes=classes, pretrained_base=False)
x, image = gcv.data.transforms.presets.ssd.load_test('pikachu_test.jpg', 512)
cid, score, bbox = net(x)
ax = viz.plot_bbox(image, bbox[0], score[0], cid[0], class_names=classes)
finetune detection


Downloading pikachu_test.jpg from https://raw.githubusercontent.com/zackchase/mxnet-the-straight-dope/master/img/pikachu.jpg...

  0%|          | 0/88 [00:00<?, ?KB/s]
89KB [00:00, 22138.12KB/s]
/usr/local/lib/python3.6/dist-packages/mxnet/gluon/block.py:1512: UserWarning: Cannot decide type for the following arguments. Consider providing them as input:
        data: None
  input_sym_arg_type = in_param.infer_type()[0]

In two epochs and less than 5 min, we are able to detect pikachus perfectly!


This finetune tutorial is not limited to SSD, you can extend it to Faster-RCNN, YOLO training by adapting a training blocks in the following examples:

Download train_faster_rcnn.py Download train_yolo.py

Total running time of the script: ( 1 minutes 32.007 seconds)

Gallery generated by Sphinx-Gallery