教你从零开始检测皮卡丘-CNN目标检测入门教程(上)

本文先为大家介绍目前流行的目标检测算法SSD (Single-Shot MultiBox Object Detection)和实验过程中的数据集。训练、测试过程及结果参见《从零开始码一个皮卡丘检测器-CNN目标检测入门教程(下)》

目标检测通俗的来说是为了找到图像或者视频里的所有目标物体。在下面这张图中,两狗一猫的位置,包括它们所属的类(狗/猫),需要被正确的检测到。

所以和图像分类不同的地方在于,目标检测需要找到尽量多的目标物体,而且要准确的定位物体的位置,一般用矩形框来表示。

在接下来的章节里,我们先介绍一个流行的目标检测算法,SSD (Single-Shot MultiBox Object Detection)。

友情提示:本章节特别长,千万不要在蹲坑的时候点开。

本文中涉及MXNet 0.11最新的发布的gluon接口,参考MXNet 0.11发布,加入动态图接口Gluon,还有两位CMU教授的亲笔教程(https://zhuanlan.zhihu.com/p/28648399)

SSD: Single Shot MultiBox Detector

顾名思义,算法的核心是用卷积神经网络一次前向推导求出大量多尺度(几百到几千)的方框来表示目标检测的结果。网络的结构用下图表示。

跟所有的图像相关的网络一样,我们需要一个主干网络来提取特征,同时也是作为第一个预测特征层。网络在当前层产生大量的预设框,和与之对应的每个方框的分类概率(背景,猫,狗等等)以及真正的物体和预设框的偏移量。在完成当前层的预测后,我们会下采样当前特征层,作为新的预测层,重新产生新的预设框,分类概率,偏移量。这个过程往往会重复好几次,直到预测特征层到达全局尺度(1×1)。

接下来我们用例子解释每个细节实现。

预设框 Default anchor boxes

预设框的形状和大小可以由参数控制,我们往往设置一堆预设框,以期望任意图像上的物体都能有一个预设框能大致重合,由于每个预设框需要对应的预测网络预测值,所以希望对于每个物体都有100%重合的预设框是不现实的,可能会需要几十万甚至几百万的预设框,但是采样的预设框越多,重合概率越好,用几千到上万个预设框基本能实现略大于70%的最好重合率,同时保证了检测的速度。

为了保证重合覆盖率,对于每个特征层上的像素点,我们用不同的大小和长宽比来采样预设框。 假设在某个特定的特征层(w × h),每个预设框的中心点就是特征像素点的中心,然后我们用如下的公式采样预设框:

import mxnet as mx from mxnet import nd from mxnet.contrib.ndarray import MultiBoxPrior n=40 # 输入形状: batch x channel x height x weight x=nd.random_uniform(shape=(1,3,n,n)) y=MultiBoxPrior(x,sizes=[.5,.25,.1],ratios=[1,2,.5]) # 取位于 (20,20) 像素点的第一个预设框 # 格式为 (x_min, y_min, x_max, y_max) boxes=y.reshape((n,n,-1,4)) print('The first anchor box at row 21, column 21:',boxes[20,20,0,:])

The first anchor box at row 21, column 21: [ 0.26249999 0.26249999 0.76249999 0.76249999] <NDArray 4 @cpu(0)>

看着数字不够直观的话,我们把框框画出来。取最中心像素的所有预设框,画在图上的话,我们看到已经可以覆盖几种尺寸和位置的物体了。把所有位置的组合起来,就是相当可观的预设框集合了。

import matplotlib.pyplot as plt def box_to_rect(box,color,linewidth=3): """convert an anchor box to a matplotlib rectangle""" box=box.asnumpy() returnplt.Rectangle( (box[0],box[1]),(box[2]-box[0]),(box[3]-box[1]), fill=False,edgecolor=color,linewidth=linewidth) colors=['blue','green','red','black','magenta'] plt.imshow(nd.ones((n,n,3)).asnumpy()) anchors=boxes[20,20,:,:] for i in range(anchors.shape[0]): plt.gca().add_patch(box_to_rect(anchors[i,:]*n,colors[i])) plt.show()

分类预测 Predict classes

from mxnet.gluon import nn def class_predictor(num_anchors,num_classes): """return a layer to predict classes""" returnnn.Conv2D(num_anchors*(num_classes+1),3,padding=1) cls_pred=class_predictor(5,10) cls_pred.initialize() x=nd.zeros((2,3,20,20)) print('Class prediction',cls_pred(x).shape)

Class prediction (2, 55, 20, 20)

预测预设框偏移 Predict anchor boxes

def box_predictor(num_anchors): """return a layer to predict delta locations""" returnnn.Conv2D(num_anchors*4,3,padding=1) box_pred=box_predictor(10) box_pred.initialize() x=nd.zeros((2,3,20,20)) print('Box prediction',box_pred(x).shape)

Box prediction (2, 40, 20, 20)

下采样特征层 Down-sample features

每次我们下采样特征层到一半的长宽,用Pooling(池化)操作就可以轻松的做到,当然也可以用stride(步长)为2的卷积直接得到。在下采样之前,我们会希望增加几层卷积层作为缓冲,防止特征值对应多尺度带来的混乱,同时又能增加网络的深度,得到更好的抽象。

def down_sample(num_filters): """stack two Conv-BatchNorm-Relu blocks and then a pooling layer to halve the feature size""" out=nn.HybridSequential() for_inrange(2): out.add(nn.Conv2D(num_filters,3,strides=1,padding=1)) out.add(nn.BatchNorm(in_channels=num_filters)) out.add(nn.Activation('relu')) out.add(nn.MaxPool2D(2)) return out blk=down_sample(10) blk.initialize() x=nd.zeros((2,3,20,20)) print('Before',x.shape,'after',blk(x).shape)

Before (2, 3, 20, 20) after (2, 10, 10, 10)

整合多个特征层预测值 Manage predictions from multiple layers

# 随便创建一个大小为 20x20的预测层 feat1=nd.zeros((2,8,20,20)) print('Feature map 1',feat1.shape) cls_pred1=class_predictor(5,10) cls_pred1.initialize() y1=cls_pred1(feat1) print('Class prediction for feature map 1',y1.shape) # 下采样 ds=down_sample(16) ds.initialize() feat2=ds(feat1) print('Feature map 2',feat2.shape) cls_pred2=class_predictor(3,10) cls_pred2.initialize() y2=cls_pred2(feat2) print('Class prediction for feature map 2',y2.shape)

Feature map 1 (2, 8, 20, 20) Class prediction for feature map 1 (2, 55, 20, 20) Feature map 2 (2, 16, 10, 10) Class prediction for feature map 2 (2, 33, 10, 10)

def flatten_prediction(pred): return nd.flatten(nd.transpose(pred,axes=(0,2,3,1))) def concat_predictions(preds): return nd.concat(*preds,dim=1) flat_y1=flatten_prediction(y1) print('Flatten class prediction 1',flat_y1.shape) flat_y2=flatten_prediction(y2) print('Flatten class prediction 2',flat_y2.shape) print('Concat class predictions',concat_predictions([flat_y1,flat_y2]).shape)

Flatten class prediction 1 (2, 22000) Flatten class prediction 2 (2, 3300) Concat class predictions (2, 25300)

主干网络 Body network

主干网络用来从原始图像输入提取特征。 一般来说我们会用预先训练好的用于分类的高性能网络(VGG, ResNet等)来提取特征。

在这里我们就简单地堆叠几层卷积和下采样层作为主干网络的演示。

from mxnet import gluon def body(): """return the body network""" out=nn.HybridSequential() for nfilters in [16,32,64]: out.add(down_sample(nfilters)) return out bnet=body() bnet.initialize() x=nd.zeros((2,3,256,256)) print('Body network',[y.shape for y in bnet(x)])

Body network [(64, 32, 32), (64, 32, 32)]

设计一个简单的SSD示意网络 Create a toy SSD model

def toy_ssd_model(num_anchors,num_classes): """return SSD modules""" downsamples=nn.Sequential() class_preds=nn.Sequential() box_preds=nn.Sequential() downsamples.add(down_sample(128)) downsamples.add(down_sample(128)) downsamples.add(down_sample(128)) for scale in range(5): class_preds.add(class_predictor(num_anchors,num_classes)) box_preds.add(box_predictor(num_anchors)) return body(),downsamples,class_preds,box_preds print(toy_ssd_model(5,2))

(HybridSequential( (0): HybridSequential( (0): Conv2D(16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=16) (2): Activation(relu) (3): Conv2D(16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=16) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) (1): HybridSequential( (0): Conv2D(32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=32) (2): Activation(relu) (3): Conv2D(32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=32) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) (2): HybridSequential( (0): Conv2D(64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=64) (2): Activation(relu) (3): Conv2D(64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=64) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) ), Sequential( (0): HybridSequential( (0): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (2): Activation(relu) (3): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) (1): HybridSequential( (0): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (2): Activation(relu) (3): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) (2): HybridSequential( (0): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (2): Activation(relu) (3): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): BatchNorm(fix_gamma=False, axis=1, momentum=0.9, eps=1e-05, in_channels=128) (5): Activation(relu) (6): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False) ) ), Sequential( (0): Conv2D(15, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): Conv2D(15, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (2): Conv2D(15, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): Conv2D(15, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): Conv2D(15, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) ), Sequential( (0): Conv2D(20, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): Conv2D(20, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (2): Conv2D(20, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): Conv2D(20, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (4): Conv2D(20, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) ))

网络前向推导 Forward

既然我们已经设计完网络结构了,接下来可以定义网络前向推导的步骤。

首先得到主干网络的输出,然后对于每一个特征预测层,推导当前层的预设框,分类概率和偏移量。最后我们把这些输入摊平,连接,作为网络的输出。

打包收工 Put all things together

from mxnet import gluon class ToySSD(gluon.Block): def __init__(self, num_classes, **kwargs): super(ToySSD, self).__init__(**kwargs) # 5个预测层,每层负责的预设框尺寸不同,由小到大,符合网络的形状 self.anchor_sizes = [[.2, .272], [.37, .447], [.54, .619], [.71, .79], [.88, .961]] # 每层的预设框都用 1,2,0.5作为长宽比候选 self.anchor_ratios = [[1, 2, .5]] * 5 self.num_classes = num_classes with self.name_scope(): self.body, self.downsamples, self.class_preds, self.box_preds = toy_ssd_model(4, num_classes) def forward(self, x): default_anchors, predicted_classes, predicted_boxes = toy_ssd_forward(x, self.body, self.downsamples, self.class_preds, self.box_preds, self.anchor_sizes, self.anchor_ratios) # 把从每个预测层输入的结果摊平并连接,以确保一一对应 anchors = concat_predictions(default_anchors) box_preds = concat_predictions(predicted_boxes) class_preds = concat_predictions(predicted_classes) # 改变下形状,为了更方便地计算softmax class_preds = nd.reshape(class_preds, shape=(0, -1, self.num_classes + 1)) return anchors, class_preds, box_preds

网络输出示意 Outputs of ToySSD

# 新建一个2个正类的SSD网络 net = ToySSD(2) net.initialize() x = nd.zeros((1, 3, 256, 256)) default_anchors, class_predictions, box_predictions = net(x) print('Outputs:', 'anchors', default_anchors.shape, 'class prediction', class_predictions.shape, 'box prediction', box_predictions.shape)

Outputs: anchors (1, 5444, 4) class prediction (1, 5444, 3) box prediction (1, 21776)

数据集 Dataset

聊了半天怎么构建一个虚无的网络,接下来看看真正有意思的东西。

我们用3D建模批量生成了一个皮卡丘的数据集,产生了1000张图片作为这个展示用的训练集。这个数据集里面,皮神会以各种角度,各种姿势出现在各种背景图中,就像Pokemon Go里增强现实那样炫酷。

因为是生成的数据集,我们自然可以得到每只皮神的真实坐标和大小,用来作为训练的真实标记。

下载数据集 Download dataset

下载提前准备好的数据集并验证

from mxnet.test_utils import download import os.path as osp def verified(file_path, sha1hash): import hashlib sha1 = hashlib.sha1() with open(file_path, 'rb') as f: while True: data = f.read(1048576) if not data: break sha1.update(data) matched = sha1.hexdigest() == sha1hash if not matched: print('Found hash mismatch in file {}, possibly due to incomplete download.'.format(file_path)) return matched url_format = 'https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/datasets/pikachu/{}' hashes = {'train.rec': 'e6bcb6ffba1ac04ff8a9b1115e650af56ee969c8', 'train.idx': 'dcf7318b2602c06428b9988470c731621716c393', 'val.rec': 'd6c33f799b4d058e82f2cb5bd9a976f69d72d520'} for k, v in hashes.items(): fname = 'pikachu_' + k target = osp.join('data', fname) url = url_format.format(k) if not osp.exists(target) or not verified(target, v): print('Downloading', target, url) download(url, fname=fname, dirname='data', overwrite=True)

加载数据 Load dataset

加载数据可以用mxnet.image.ImageDetIter,同时还提供了大量数据增强的选项,比如翻转,随机截取等等。

DataBatch: data shapes: [(32, 3, 256, 256)] label shapes: [(32, 1, 5)]

示意图 Illustration

加载的训练数据还可以显示出来看看到底是怎么样的。

import numpy as np img = batch.data[0][0].asnumpy() # 取第一批数据中的第一张,转成numpy img = img.transpose((1, 2, 0)) # 交换下通道的顺序 img += np.array([123, 117, 104]) img = img.astype(np.uint8) # 图片应该用0-255的范围 # 在图上画出真实标签的方框 for label in batch.label[0][0].asnumpy(): if label[0] < 0: break print(label) xmin, ymin, xmax, ymax = [int(x * data_shape) for x in label[1:5]] rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False, edgecolor=(1, 0, 0), linewidth=3) plt.gca().add_patch(rect) plt.imshow(img) plt.show()

[ 0. 0.75724518 0.34316057 0.93332517 0.70017999]

原文发布于微信公众号 - AI研习社(okweiwu)

原文发表时间:2017-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏崔庆才的专栏

深度学习效果不好?试试 Batch Normalization 吧!

Batch Normalization(简称BN)自从提出之后,因为效果特别好,很快被作为深度学习的标准工具应用在了各种场合。BN大法虽然好,但是也存在一些局...

1.4K3
来自专栏书山有路勤为径

卡尔曼滤波器(Kalman Filters)

卡尔曼滤波器,这是一种使用噪声传感器测量(和贝叶斯规则)来生成未知量的可靠估计的算法(例如车辆可能在3秒内的位置)。

3853
来自专栏机器学习原理

机器学习篇(2)——最小二乘法概念最小二乘法

前言:主要介绍了从最小二乘法到 概念 顾名思义,线性模型就是可以用线性组合进行预测的函数,如图: ? image.png 公式如下: ? i...

6355
来自专栏杨熹的专栏

Machine Learning Notes-Linear Regression-Udacity

什么是 Regression? Regression 就是想找到因变量和自变量之间的关系,用一个函数来表示,并且可用这个函数来预测任意一个新的 x 会有怎样的 ...

3584
来自专栏老秦求学

[Deep-Learning-with-Python] Keras高级概念

目前为止,介绍的神经网络模型都是通过Sequential模型来实现的。Sequential模型假设神经网络模型只有一个输入一个输出,而且模型的网络层是线性堆叠在...

1621
来自专栏杨熹的专栏

双向 LSTM

本文结构: 为什么用双向 LSTM 什么是双向 LSTM 例子 ---- 为什么用双向 LSTM? 单向的 RNN,是根据前面的信息推出后面的,但有时候只看前面...

7046
来自专栏Coding迪斯尼

启动网络的自我训练流程,展示网络数字图片识别效果

1514
来自专栏AI研习社

教你从零开始检测皮卡丘-CNN目标检测入门教程(下)

本文为大家介绍实验过程中训练、测试过程及结果。算法和数据集参见《从零开始码一个皮卡丘检测器-CNN目标检测入门教程(上)》 训练 Train 损失函数 Lo...

3263
来自专栏大数据挖掘DT机器学习

NLP真实项目:利用这个模型能够通过商品评论去预测一个商品的销量

前言 由于是日语项目,用到的分词软件等,在中文任务中需要替换为相应的中文分词软件。例如结巴分词 : https://github.com/fxsjy/jieb...

68811
来自专栏个人分享

Spark MLlib知识点学习整理

MLlib的设计原理:把数据以RDD的形式表示,然后在分布式数据集上调用各种算法。MLlib就是RDD上一系列可供调用的函数的集合。

1532

扫码关注云+社区

领取腾讯云代金券