從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(上)
本文作者Zhreshold,原文載於其知乎主頁,雷鋒網獲其授權發布。
本文先為大家介紹目前流行的目標檢測演算法SSD (Single-Shot MultiBox Object Detection)和實驗過程中的數據集。訓練、測試過程及結果參見《從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(下)》
目標檢測通俗的來說是為了找到圖像或者視頻里的所有目標物體。在下面這張圖中,兩狗一貓的位置,包括它們所屬的類(狗/貓),需要被正確的檢測到。
<img src="https://static.leiphone.com/uploads/new/article/pic/201708/6fa41e7d3df57bcba51e36858e998af6.png" data-rawwidth="200" data-rawheight="209" class="content_image" width="200" _src="https://static.leiphone.com/uploads/new/article/pic/201708/6fa41e7d3df57bcba51e36858e998af6.png"/>
所以和圖像分類不同的地方在於,目標檢測需要找到盡量多的目標物體,而且要準確的定位物體的位置,一般用矩形框來表示。
在接下來的章節里,我們先介紹一個流行的目標檢測演算法,SSD (Single-Shot MultiBox Object Detection).
SSD: Single Shot MultiBox Detector
顧名思義,演算法的核心是用卷積神經網路一次前向推導求出大量多尺度(幾百到幾千)的方框來表示目標檢測的結果。網路的結構用下圖表示。
<img src="https://static.leiphone.com/uploads/new/article/pic/201708/ed2959cf6e6ef5685cf21f53fc871110.png" data-rawwidth="1182" data-rawheight="616" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic4.zhimg.com/v2-31a700856e96ca271f313595e65107cf_r.png" _src="https://static.leiphone.com/uploads/new/article/pic/201708/ed2959cf6e6ef5685cf21f53fc871110.png"/>
跟所有的圖像相關的網路一樣,我們需要一個主幹網路來提取特徵,同時也是作為第一個預測特徵層。網路在當前層產生大量的預設框,和與之對應的每個方框的分類概率(背景,貓,狗等等)以及真正的物體和預設框的偏移量。在完成當前層的預測後,我們會下採樣當前特徵層,作為新的預測層,重新產生新的預設框,分類概率,偏移量。這個過程往往會重複好幾次,直到預測特徵層到達全局尺度()。
接下來我們用例子解釋每個細節實現。
預設框 Default anchor boxes
預設框的形狀和大小可以由參數控制,我們往往設置一堆預設框,以期望任意圖像上的物體都能有一個預設框能大致重合,由於每個預設框需要對應的預測網路預測值,所以希望對於每個物體都有100%重合的預設框是不現實的,可能會需要幾十萬甚至幾百萬的預設框,但是採樣的預設框越多,重合概率越好,用幾千到上萬個預設框基本能實現略大於70%的最好重合率,同時保證了檢測的速度。
為了保證重合覆蓋率,對於每個特徵層上的像素點,我們用不同的大小和長寬比來採樣預設框。 假設在某個特定的特徵層(),每個預設框的中心點就是特徵像素點的中心,然後我們用如下的公式採樣預設框:
預設框的中心為特徵像素點的中心
對於長寬比, 尺寸, 生成的預設框大小。
對於長寬比 0" eeimg="1" style="font-size:16px;font-style:normal;font-weight:normal;color:rgb(51, 51, 51);" />同時, 生成的預設框大小, 其中是第一個預設的尺寸。
這裡例子里,我們用事先實現的層MultiBoxPrior產生預設框,輸入
個預設尺寸,和個預設的長寬比,輸出為個方框而不是個。 當然我們完全可以使用其他的演算法產生不同的預設框,但是實踐中我們發現上述的方法覆蓋率和相應需要的預設框數量比較合適。
import mxnet as mx
from mxnet import nd
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,:])
看著數字不夠直觀的話,我們把框框畫出來。取最中心像素的所有預設框,畫在圖上的話,我們看到已經可以覆蓋幾種尺寸和位置的物體了。把所有位置的組合起來,就是相當可觀的預設框集合了。
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()
<img src="https://static.leiphone.com/uploads/new/article/pic/201708/6d735462c4bfe50691b8f98603b10982.png" data-rawwidth="255" data-rawheight="252" class="content_image" width="255" _src="https://static.leiphone.com/uploads/new/article/pic/201708/6d735462c4bfe50691b8f98603b10982.png"/>
分類預測 Predict classes
這個部分的目標很簡單,就是預測每個預設框對應的分類。我們用, Padding (填充) 的卷積來預測分類概率,這樣可以做到卷積後空間上的形狀不會變化,而且由於卷積的特點,每個卷積核會掃過每個預測特徵層的所有像素點,得到個預測值,對應了空間上所有的像素點,每個通道對應特定的預設框。假設有10個正類,每個像素5個預設框,那麼我們就需要個通道。 具體的來說,對於每個像素第個預設框:
通道的值對應背景(非物體)的得分
通道對應了第類的得分
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
為了找到物體準確的位置,光靠預設框本身是不行的,我們還需要預測偏移量以便把真正的物體框出來。
假設
是某個預設框,
是目標物體的真實矩形框,我們需要預測的偏移為,全都是長度為4的向量, 我們求得偏移
所有的偏移量都除以預設框的長或寬是為了更好的收斂。
類似分類概率,我們同樣用填充的卷積來預測偏移。這次不同的是,對於每個預設框,我們只需要個通道來預測偏移量, 一共需要個通道,第 個預設框對應的偏移量存在通道到通道 之間。
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
SSD演算法的一個關鍵點在於它用到了多尺度的特徵層來預測不同大小的物體。相對來說,淺層的特徵層的空間尺度更大,越到網路的深層,空間尺度越小,最後我們往往下採樣直到,用來預測全圖大小的物體。所以每個特徵層產生的預設框,分類概率,框偏移量需要被整合起來統一在全圖與真實的物體比較。 為了做到一一對應,我們統一把所有的預設框, 分類概率,框偏移量 平鋪再連接。得到的是按順序排列但是攤平的所有預測值和預設框。
# 隨便創建一個大小為 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
我們這裡介紹一個示意用的簡單SSD網路,出於速度的考量,輸入圖像尺寸定為。
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))
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/{}"
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]
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()
點擊展開全文
※自動駕駛公司Torc Robotics與達成NXP合作,布局雷達等系統感知技術研發
※Vincross孫天齊:人機界面的突破將引發科技革命
※谷歌新版語音交互套件 Voice Kit 開放預訂,開發者都能用它做什麼?
※京東推出教育 戰略,打造一站式教育服務平台
※賈躍亭姐弟掏空樂視:錢全抽走了;今日頭條給知乎挖牆角,雙方回應;三星在李在鎔一審判決後首發聲
TAG:雷鋒網 |
※使用SSD進行目標檢測:目標檢測第二篇
※SSD多盒實時目標檢測教程
※63頁【深度CNN-目標檢測】綜述【PDF下載】
※一個快速檢測系統CPU負載的小程序
※ctDNA提取檢測技術
※CSP行人檢測:無錨點框的檢測新思路
※教程 | 從零開始PyTorch項目:YOLO v3目標檢測實現(下)
※從R-CNN到RFBNet,目標檢測架構5年演進全盤點
※廈門無創DNA檢測免費在線諮詢
※收藏 | 目標檢測網路學習總結(RCNN --> YOLO V3),
※高效率的目標檢測網路RON
※曠視發布通用物體檢測數據集 Objects365,開啟 CVPR 物體檢測挑戰賽
※精華液檢測報告:排行榜幾強實拍測評,每個都不輸SK-II小燈泡!
※當 NGS 遇上 ctDNA 檢測
※從RCNN到SSD,這應該是最全的一份目標檢測演算法盤點
※目標檢測技術之Faster R-CNN詳解
※血液ctDNA-EGFR標準化檢測,三個關鍵環節全掌握
※SNP檢測方法盤點
※CFDA批准人類EGFR基因突變檢測試劑盒上市,開啟腫瘤液體活檢的新里程!
※早期腫瘤cfDNA的甲基化檢測