從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(下)

2020-12-27 雷鋒網

本文作者Zhreshold,原文載於其知乎主頁,雷鋒網(公眾號:雷鋒網(公眾號:雷鋒網))獲其授權發布。

本文為大家介紹實驗過程中訓練、測試過程及結果。算法和數據集參見《從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(上)》

訓練 Train

損失函數 Losses

通過定義損失函數,我們可以讓網絡收斂到我們希望得到的目標檢測功能,也就是說,我們希望網絡能正確預測物體的類別,同時能預測出準確的預設框偏移量,以正確地顯示物體的真正大小和位置。

這個預測的類別和偏移量都是可以通過真實標籤和網絡的當前預測值得到,在這裡我們用MultiBoxTarget層來計算,其中包含了預測框和真實標籤的匹配,正類和負類的選擇,就不一一詳述了。(詳情見論文 SSD: Single Shot MultiBox Detector)。

from mxnet.contrib.ndarray import MultiBoxTarget

def training_targets(default_anchors, class_predicts, labels):
   class_predicts = nd.transpose(class_predicts, axes=(0, 2, 1))
   z = MultiBoxTarget(*[default_anchors, labels, class_predicts])
   box_target = z[0]  # 預設框偏移量 (x, y, width, height)
   box_mask = z[1]  # box_mask用來把負類的偏移量置零,因為背景不需要位置!
   cls_target = z[2]  # 每個預設框應該對應的分類
   return box_target, box_mask, cls_target

gluon.loss中有很多預設的損失函數可以選擇,當然我們也可以快速地手寫一些損失函數。

首先,對於物體分類的概率,平時我們往往用交叉墒,不過在目標檢測中,我們有大量非平衡的負類(背景),那麼 Focal Loss會是一個很好的選擇(詳情見論文 Focal Loss for Dense Object Detection)。

class FocalLoss(gluon.loss.Loss):
   def __init__(self, axis=-1, alpha=0.25, gamma=2, batch_axis=0, **kwargs):
       super(FocalLoss, self).__init__(None, batch_axis, **kwargs)
       self._axis = axis
       self._alpha = alpha
       self._gamma = gamma

   def hybrid_forward(self, F, output, label):
       output = F.softmax(output)
       pt = F.pick(output, label, axis=self._axis, keepdims=True)
       loss = -self._alpha * ((1 - pt) ** self._gamma) * F.log(pt)
       return F.mean(loss, axis=self._batch_axis, exclude=True)


# cls_loss = gluon.loss.SoftmaxCrossEntropyLoss()

cls_loss = FocalLoss()

print(cls_loss)

FocalLoss(batch_axis=0, w=None)

接下來是一個流行的 SmoothL1 損失函數,用來懲罰不準確的預設框偏移量。

class SmoothL1Loss(gluon.loss.Loss):
   def __init__(self, batch_axis=0, **kwargs):
       super(SmoothL1Loss, self).__init__(None, batch_axis, **kwargs)

   def hybrid_forward(self, F, output, label, mask):
       loss = F.smooth_l1((output - label) * mask, scalar=1.0)
       return F.mean(loss, self._batch_axis, exclude=True)


box_loss = SmoothL1Loss()

print(box_loss)

SmoothL1Loss(batch_axis=0, w=None)

衡量性能指標 Evaluate metrics

我們在訓練時需要一些指標來衡量訓練是否順利,我們這裡用準確率衡量分類的性能,用平均絕對誤差衡量偏移量的預測能力。這些指標對網絡本身沒有任何影響,只是用於觀測。

cls_metric = mx.metric.Accuracy()

box_metric = mx.metric.MAE()  # measure absolute difference between prediction and target

選擇訓練用的設備 Set context for training

ctx = mx.gpu()  # 用GPU加速訓練過程

try:
   _ = nd.zeros(1, ctx=ctx)
   # 為了更有效率,cuda實現需要少量的填充,不影響結果
   train_data.reshape(label_shape=(3, 5))
   train_data = test_data.sync_label_shape(train_data)

except mx.base.MXNetError as err:
   # 沒有gpu也沒關係,交給cpu慢慢跑
   print('No GPU enabled, fall back to CPU, sit back and be patient...')
   ctx = mx.cpu()

初始化網絡參數 Initialize parameters

net=ToySSD(num_class)

net.initialize(mx.init.Xavier(magnitude=2),ctx=ctx)

用gluon.Trainer簡化訓練過程 Set up trainer

gluon.Trainer能簡化優化網絡參數的過程,免去對各個參數單獨更新的痛苦。

net.collect_params().reset_ctx(ctx)

trainer=gluon.Trainer(net.collect_params(),'sgd',{'learning_rate':0.1,'wd':5e-4})

開始訓練 Start training

既然是簡單的示例,我們不想花費太多的時間來訓練網絡,所以會預加載訓練過一段時間的網絡參數繼續訓練。

如果你感興趣的話,可以設置

from_scratch=True

這樣網絡就會從初始的隨機參數開始訓練。

一般從頭訓練用單個gpu會花費半個多小時。

epochs = 150  # 設大一點的值來得到更好的結果

log_interval = 20

from_scratch = False  # 設為True就可以從頭開始訓練

if from_scratch:
   start_epoch = 0

else:
   start_epoch = 148
   pretrained = 'ssd_pretrained.params'
   sha1 = 'fbb7d872d76355fff1790d864c2238decdb452bc'
   url = 'https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/models/ssd_pikachu-fbb7d872.params'
   if not osp.exists(pretrained) or not verified(pretrained, sha1):
       print('Downloading', pretrained, url)
       download(url, fname=pretrained, overwrite=True)
   net.load_params(pretrained, ctx)

喝咖啡的時間

import time

from mxnet import autograd as ag

for epoch in range(start_epoch, epochs):
   # 重置iterator和時間戳
   train_data.reset()
   cls_metric.reset()
   box_metric.reset()
   tic = time.time()
   # 迭代每一個批次
   for i, batch in enumerate(train_data):
       btic = time.time()
       # 用autograd.record記錄需要計算的梯度
       with ag.record():
           x = batch.data[0].as_in_context(ctx)
           y = batch.label[0].as_in_context(ctx)
           default_anchors, class_predictions, box_predictions = net(x)
           box_target, box_mask, cls_target = training_targets(default_anchors, class_predictions, y)
           # 損失函數計算
           loss1 = cls_loss(class_predictions, cls_target)
           loss2 = box_loss(box_predictions, box_target, box_mask)
           # 1比1疊加兩個損失函數,也可以加權重
           loss = loss1 + loss2
           # 反向推導
           loss.backward()
       # 用trainer更新網絡參數
       trainer.step(batch_size)
       # 更新下衡量的指標
       cls_metric.update([cls_target], [nd.transpose(class_predictions, (0, 2, 1))])
       box_metric.update([box_target], [box_predictions * box_mask])
       if (i + 1) % log_interval == 0:
           name1, val1 = cls_metric.get()
           name2, val2 = box_metric.get()
           print('[Epoch %d Batch %d] speed: %f samples/s, training: %s=%f, %s=%f'
                 %(epoch ,i, batch_size/(time.time()-btic), name1, val1, name2, val2))

   # 列印整個epoch的的指標
   name1, val1 = cls_metric.get()
   name2, val2 = box_metric.get()
   print('[Epoch %d] training: %s=%f, %s=%f'%(epoch, name1, val1, name2, val2))
   print('[Epoch %d] time cost: %f'%(epoch, time.time()-tic))


# 還可以把網絡的參數存下來以便下次再用

net.save_params('ssd_%d.params' % epochs)

[Epoch 148 Batch 19] speed: 109.217423 samples/s, training: accuracy=0.997539, mae=0.001862
[Epoch 148] training: accuracy=0.997610, mae=0.001806
[Epoch 148] time cost: 17.762958
[Epoch 149 Batch 19] speed: 110.492729 samples/s, training: accuracy=0.997607, mae=0.001824
[Epoch 149] training: accuracy=0.997692, mae=0.001789
[Epoch 149] time cost: 15.353258

測試 Test

接下來就是   的時刻,我們用訓練好的網絡來測試一張圖片。

網絡推導的過程和訓練很相似,只不過我們不再需要計算真值和損失函數,也不再需要更新網絡的參數,一次推導就可以得到結果。

準備測試數據 Prepare the test data

我們需要讀取一張圖片,稍微調整到網絡需要的結構,比如說我們需要調整圖片通道的順序,減去平均值等等慣用的方法。

import numpy as np

import cv2

def preprocess(image):
   """Takes an image and apply preprocess"""
   # 調整圖片大小成網絡的輸入
   image = cv2.resize(image, (data_shape, data_shape))
   # 轉換 BGR 到 RGB
   image = image[:, :, (2, 1, 0)]
   # 減mean之前先轉成float
   image = image.astype(np.float32)
   # 減 mean
   image -= np.array([123, 117, 104])
   # 調成為 [batch-channel-height-width]
   image = np.transpose(image, (2, 0, 1))
   image = image[np.newaxis, :]
   # 轉成 ndarray
   image = nd.array(image)
   return image


image = cv2.imread('img/pikachu.jpg')

x = preprocess(image)

print('x', x.shape)

x (1, 3, 256, 256)

網絡推導 Network inference

只要一行代碼,輸入處理完的圖片,輸出我們要的所有預測值和預設框。

# 如果有預先訓練好的網絡參數,可以直接加載

# net.load_params('ssd_%d.params' % epochs, ctx)

anchors, cls_preds, box_preds = net(x.as_in_context(ctx))

print('anchors', anchors)

print('class predictions', cls_preds)

print('box delta predictions', box_preds)

anchors
[[[-0.084375 -0.084375 0.115625 0.115625 ]
[-0.12037501 -0.12037501 0.15162501 0.15162501]
[-0.12579636 -0.05508568 0.15704636 0.08633568]
...,
[ 0.01949999 0.01949999 0.98049998 0.98049998]
[-0.12225395 0.18887302 1.12225389 0.81112695]
[ 0.18887302 -0.12225395 0.81112695 1.12225389]]]
<NDArray 1x5444x4 @gpu(0)>
class predictions
[[[ 0.33754104 -1.64660323]
[ 1.15297699 -1.77257478]
[ 1.1535604 -0.98352218]
...,
[-0.27562004 -1.29400492]
[ 0.45524898 -0.88782215]
[ 0.20327765 -0.94481993]]]
<NDArray 1x5444x2 @gpu(0)>
box delta predictions
[[-0.16735925 -0.13083346 -0.68860865 ..., -0.18972112 0.11822788
-0.27067867]]
<NDArray 1x21776 @gpu(0)>

是不是看著還很奇怪,別著急,還差最後一步

轉換為可讀的輸出 Convert predictions to real object detection results

要把網絡輸出轉換成我們需要的坐標,還要最後一步,比如我們需要softmax把分類預測轉換成概率,還需要把偏移量和預設框結合來得到物體的大小和位置。

非極大抑制(Non-Maximum Suppression)也是必要的一步,因為一個物體往往有不只一個檢測框。

from mxnet.contrib.ndarray import MultiBoxDetection

# 跑一下softmax, 轉成0-1的概率

cls_probs = nd.SoftmaxActivation(nd.transpose(cls_preds, (0, 2, 1)), mode='channel')

# 把偏移量加到預設框上,去掉得分很低的,跑一遍nms,得到最終的結果

output = MultiBoxDetection(*[cls_probs, box_preds, anchors], force_suppress=True, clip=False)

print(output)

[[[ 0. 0.61178613 0.51807499 0.5042429 0.67325425 0.70118797]
[-1. 0.59466797 0.52491206 0.50917625 0.66228026 0.70489514]
[-1. 0.5731774 0.53843218 0.50217044 0.66522425 0.7118448 ]
...,
[-1. -1. -1. -1. -1. -1. ]
[-1. -1. -1. -1. -1. -1. ]
[-1. -1. -1. -1. -1. -1. ]]]
<NDArray 1x5444x6 @gpu(0)>

結果中,每一行都是一個可能的結果框,表示為[類別id, 得分, 左邊界,上邊界,右邊界,下邊界],有很多-1的原因是網絡預測到這些都是背景,或者作為被抑制的結果。

顯示結果 Display results

數字永遠不如圖片來得直觀

把得到的轉換結果畫在圖上,就得到我們期待已久的幾十萬伏特圖了!

def display(img, out, thresh=0.5):
   import random
   import matplotlib as mpl
   mpl.rcParams['figure.figsize'] = (10,10)
   pens = dict()
   plt.clf()
   plt.imshow(img)
   for det in out:
       cid = int(det[0])
       if cid < 0:
           continue
       score = det[1]
       if score < thresh:
           continue
       if cid not in pens:
           pens[cid] = (random.random(), random.random(), random.random())
       scales = [img.shape[1], img.shape[0]] * 2
       xmin, ymin, xmax, ymax = [int(p * s) for p, s in zip(det[2:6].tolist(), scales)]
       rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False,
                            edgecolor=pens[cid], linewidth=3)
       plt.gca().add_patch(rect)
       text = class_names[cid]
       plt.gca().text(xmin, ymin-2, '{:s} {:.3f}'.format(text, score),
                      bbox=dict(facecolor=pens[cid], alpha=0.5),
                      fontsize=12, color='white')
   plt.show()


display(image[:, :, (2, 1, 0)], output[0].asnumpy(), thresh=0.45)

小結 Conclusion

目標檢測不同於分類任務,需要考慮的不只是全圖尺度的單一分類,而是需要檢測到不同大小,不同位置的物體,難度自然提升了許多,用掃窗之類的傳統方法早已不適合神經網絡這種需要大量計算需求的新結構。幸好我們可以用本章節介紹的方法,利用卷積網絡的特性,一次推導得到全部的預測結果,相對來說快速且準確。

我們希望能用較短的篇幅來描述一個足夠簡單的過程,但是難免會有疏漏,歡迎各種問題和建議,與此同時,我們會不斷更新教程,並且會帶來更多不同的算法,敬請期待。

相關連結

Apache MXNet官方網站:https://mxnet.incubator.apache.org/

Github Repo: zackchase/mxnet-the-straight-dope

英文版教程: Object Detection Using Convolutional Neural Networks

Eric知乎介紹0.11 新特性:https://zhuanlan.zhihu.com/p/28648399

0.11 Release:https://github.com/apache/incubator-mxnet/releases

安裝指南:https://mxnet.incubator.apache.org/versions/master/get_started/install.html

其他Gluon教程:http://gluon.mxnet.io/

Gluon講座PPT: https://github.com/zackchase/mxnet-slides/blob/master/kdd-mxnet-slides.pdf

Gluon深度學習樣例:https://github.com/apache/incubator-mxnet/tree/master/example/gluon

SSD: Single Shot MultiBox Detector

Focal Loss: [1708.02002] Focal Loss for Dense Object Detection

雷鋒網版權文章,未經授權禁止轉載。詳情見轉載須知。

相關焦點

  • 從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(上)
    本文先為大家介紹目前流行的目標檢測算法SSD (Single-Shot MultiBox Object Detection)和實驗過程中的數據集。訓練、測試過程及結果參見《從零開始碼一個皮卡丘檢測器-CNN目標檢測入門教程(下)》目標檢測通俗的來說是為了找到圖像或者視頻裡的所有目標物體。在下面這張圖中,兩狗一貓的位置,包括它們所屬的類(狗/貓),需要被正確的檢測到。
  • 目標檢測之基於CNN的兩階段檢測器
    由於手動設計特徵的性能已經飽和,在2010到2012年之間,目標檢測已經到了瓶頸,幾乎沒有任何的收益。在2012年,世界見證了卷積神經網絡的重生。隨著深度卷積網絡能夠學習到圖像的穩定高層特徵描述,一個很自然的問題是是否能夠將其引入到目標檢測中?R.
  • 增加檢測類別?這是一份目標檢測的一般指南
    這篇文章將從討論圖像分類和目標檢測之間的本質區別開始,其中包括判斷一個圖像分類網絡是否可以用於目標檢測,以及在什麼情況下可以這樣使用等話題。當我們理解了什麼是目標檢測時,隨後會概述一個深度學習目標檢測器的核心模塊。它一般包括目標檢測架構和基本模型,不熟悉目標檢測的讀者可能會誤解這兩個部分。
  • 從零開始PyTorch項目:YOLO v3目標檢測實現
    機器之心編譯目標檢測是深度學習近期發展過程中受益最多的領域。隨著技術的進步,人們已經開發出了很多用於目標檢測的算法,包括 YOLO、SSD、Mask RCNN 和 RetinaNet。在本教程中,我們將使用 PyTorch 實現基於 YOLO v3 的目標檢測器,後者是一種快速的目標檢測算法。
  • 如何評價Sparse R-CNN,目標檢測未來會如何發展?
    昨晚看完直接失眠了,躺在床上捋清楚了幾個細節,比如學出來的anchor為啥不會很相似,為什麼這樣能去掉nms之類,越想越覺得太秒了由於dense的anchor設計加上cnn feature map的平滑性質,導致我們在正負樣本劃分不得不使用一對多的形式以及使用nms這樣的後處理,這兩個東西很蛋疼,從而從去年開始導致了一系列做正負樣本劃分以及設計loss適應nms的工作。
  • Python 從零開始--入門篇
    ,目的只有一個是大家一起能夠使用 python 寫自己的爬蟲,能夠達到公司要求的基本水平。就是很簡答相比於 C語言 的指針的混亂,和 Java 的繁瑣,python 更加適合作為非計算機專業的第一個入門的程式語言。(計算機專業最好還是以C語言為第一門語言入門為好,後面寫文章出來介紹)從語法上面來說也擁有更多的第三方庫,避免了很多「重複製造輪子「。
  • yolov5實戰之皮卡丘檢測
    記得之前在剛接觸的時候看到博客中寫到,深度學習分三個層次,第一個層次是分類,第二個層次是檢測,第三個層次是分割。人臉識別算是分類問題,也就是一直在第一個層次···一直都想有機會了解下第二個層次,奈何精力有限,工作中也沒有實際的項目需要。最近正好有個不急的檢測項目,趁此機會入門檢測吧。工作中聽同事提到yolov5效果不錯,而且文檔指導也比較豐富,因此選擇從此入手,順便也熟悉下pytorch。
  • 訓練輪數降至1/10、性能卻更好,商湯等提出升級版DETR目標檢測器
    機器之心專欄 機器之心編輯部 今年 5 月底,Facebook AI 提出了DETR,利用 Transformer 去做目標檢測,該方法去除了許多目標檢測中的人工設計組件,同時展現了非常好的性能。
  • 深度學習目標檢測系列:faster RCNN實現|附python源碼
    目標檢測一直是計算機視覺中比較熱門的研究領域,有一些常用且成熟的算法得到業內公認水平,比如RCNN系列算法、SSD以及YOLO等。如果你是從事這一行業的話,你會使用哪種算法進行目標檢測任務呢?在我尋求在最短的時間內構建最精確的模型時,我嘗試了其中的R-CNN系列算法,如果讀者們對這方面的算法還不太了解的話,建議閱讀《目標檢測算法圖解:一文看懂RCNN系列算法》。
  • 卷積神經網絡(CNN)簡易教程
    簡單的單元有助於特徵檢測,而複雜的單元則結合了來自小空間鄰域的多個這樣的局部特徵。空間池有助於實現平移不變特徵。當我們看到一個新的圖像時,我們可以從左到右和從上到下掃描圖像,以了解圖像的不同特徵。我們的下一步是結合我們掃描的不同的局部特徵來對圖像進行分類。CNN就是這樣工作的平移不變特徵是什麼意思?
  • RCNN- 將CNN引入目標檢測的開山之作
    RCNN (論文:Rich feature hierarchies for accurate object detection and semantic segmentation) 是將CNN方法引入目標檢測領域, 大大提高了目標檢測效果,可以說改變了目標檢測領域的主要研究思路, 緊隨其後的系列文章:( RCNN),Fast RCNN, Faster RCNN代表該領域當前最高水準。
  • 適合零基礎學習的英語教程:教你如何從零開始記單詞,迅速入門
    適合零基礎學習的英語教程
  • PP-YOLO超越YOLOv4-目標檢測的進步
    YOLO發展史YOLO最初是由Joseph Redmon編寫的,用於檢測目標。目標檢測是一種計算機視覺技術,它通過在目標周圍畫一個邊界框來定位和標記對象,並確定一個給定的框所屬的類標籤。和大型NLP transformers不同,YOLO設計得很小,可為設備上的部署提供實時推理速度。
  • 零基礎看得懂的C語言入門教程
    一、前言 距離上一次編寫C語言的教程是5年前了(2015年),由於自己是從初一時開始學習編程,深知學習編程的痛苦。當時正在念大學的我想著分享自己自學編程的經驗,搭建了一個社區(社區之後自己維護太懶,維護了一年不到關閉了)第一次編寫了C語言教程,分享自己的學習經驗。社區關閉後,於2017年上傳至了CSDN留存至今。 現在回過頭查看當時自己編寫的教程,由於當時技術的淺薄,編寫的教程還是存在很多的不足,故有了一個重新編寫C語言教程的想法。
  • 愷明大神 Mask R-CNN 超實用教程
    在上周的博客文章中,你學習了如何使用Yolo物體探測器來檢測圖像中物體(https://www.pyimagesearch.com/2018/11/12/yolo-object-detection-with-opencv/)。對象檢測器,如yolo、faster r-cnn和ssd,生成四組(x,y)坐標,表示圖像中對象的邊界框。
  • CVPR 2019 | 天秤座R-CNN:全面平衡的目標檢測器
    其實如果不對 detector 的結構做功夫的話,針對這些 imbalance 改進的其實就是為了把 detector 的真正功效給展現出來,就是如果把一個目標檢測器 train 好的問題。λ 用於在多任務學習下調整損失權重。 之所以會提出 Balanced L1 loss,是因為這個損失函數是兩個 loss 的相加,如果分類做得很好地話一樣會得到很高的分數,而導致忽略了回歸的重要性,一個自然的想法就是調整 λ 的值。我們把樣本損失大於等於 1.0 的叫做 outliers,小於的叫做 inliers。
  • 皮卡丘怎麼畫 - 可愛的皮卡丘簡筆畫教程
    皮卡丘(Pikachu)是《口袋妖怪》中的一種神奇寶貝,它全身的皮毛是黃色的,耳朵很長,尖端是黑色的,有一雙黑色的眼睛,臉頰兩邊各有一個紅色圓圈,是小小的電力袋,尾巴是閃電形狀的。皮卡丘遇到危險時就會放電。會把尾巴豎起來,去感覺周圍是否安全。
  • 愷明大神 Mask R-CNN 超實用教程
    在上周的博客文章中,你學習了如何使用Yolo物體探測器來檢測圖像中物體(https://www.pyimagesearch.com/2018/11/12/yolo-object-detection-with-opencv/)。對象檢測器,如yolo、faster r-cnn和ssd,生成四組(x,y)坐標,表示圖像中對象的邊界框。
  • 從RCNN到SSD,深度學習目標檢測算法盤點
    本文對目標檢測進行了整體回顧,第一部分從RCNN開始介紹基於候選區域的目標檢測器,包括Fast R-CNN、Faster R-CNN 和 FPN等。第二部分則重點討論了包括YOLO、SSD和RetinaNet等在內的單次檢測器,它們都是目前最為優秀的方法。機器之心之前已經討論過非常多的目標檢測算法,對計算機視覺感興趣的讀者也可以結合以前的文章加強理解。
  • 從零開始的Python爬蟲教程(一):獲取HTML文檔
    從零開始的Python爬蟲教程(零):粗識HTML結構中,粗略給大家介紹了一下HTML文檔,是為了在接下來的教程中讓大家更容易理解和掌握。在接下來的教程中,需要大家提前安裝python3.x版本,大家不必拘泥於具體的版本,不管安裝的是3.0還是最新的3.7,都不影響接下來的操作。至於安裝教程,這裡就不過多贅述了,讀者可自行搜索到詳細的教程。