- 原文地址:Real-time object detection with YOLO
- 原文做者:Matthijs Hollemans
- 譯文出自:掘金翻譯計劃
- 譯者:Danny Lau
- 校對者:Dalston Xu ,DeepMissea
譯者注:
在閱讀這篇文章以前可能會遇到的一些名詞,這裏是解釋(我本身也查了至關多的資料,爲了翻譯地儘量的簡單易懂一些)javascript
- Metal:Metal 是蘋果在 iOS 8 以後 提供的一種低層次的渲染應用程序編程接口,提供了軟件所需的最低層,保證軟件能夠運行在不一樣的圖像芯片上。(和 OpenGL ES 是並列關係)
- 分類器:該函數或模型可以把數據庫中的數據紀錄映射到給定類別中的某一個,從而能夠應用於數據預測。
- 批量歸一化:解決在訓練過程當中,中間層數據分佈發生改變的問題,以防止梯度消失或爆炸、加快訓練速度。
- 文中術語主要參照孫遜等人對斯坦福大學深度學習教程UFLDL Tutorial的翻譯
在計算機視覺領域,物體檢測是經典問題之一:
識別一張給定的圖像中包含的物體是什麼,和它們在圖像中的位置。php
檢測是比分類更復雜的一個問題,雖然分類也要識別物體,可是它不須要告訴你物體在圖像中的位置,而且分類沒法識別包含多個物體的圖像。前端
YOLO 是一個用來處理實時物體檢測的聰明的神經網絡。java
在這篇博客裏面我將介紹如何經過 Metal Performance Shaders 讓「迷你」版的 YOLOv2 在 iOS 上運行(譯:MetalPerformanceShaders 是 iOS 9 中 Metal Kit新增的方法)。react
在你繼續看下去以前,務必先看下這個使人震驚的 YOLOv2 預告。 😎android
你能夠用一個相似於 VGGNet 或 Inception 的分類器,經過在圖像上移動一個小的窗口將分類器轉換成物體檢測器。在每一次移動中,運行分類器來獲取對當前窗口內物體類型的推測。經過滑動窗口能夠得到成百上千個關於該圖像的推測,可是隻有那個分類器最肯定的那個選項會被保留。ios
這個方案雖然是可行的可是很明顯它會很是的慢,由於你須要屢次運行分類器。一種能夠略微改善的方法是首先預測哪些部分的圖片可能包含有趣的信息 - 所謂的區域建議 - 而後只在這些區域運行分類器。相比移動窗口來講,分類器確實減小了很多工做量,可是它仍會運行較屢次數。git
YOLO 採用了一種徹底不一樣的實現方式。它不是傳統的分類器,而是被改形成了對象探測器。YOLO 實際上只會看圖像一次(所以得名:You Only Look Once(你只用看一次)),可是是經過一種聰明的方式。
YOLO 把圖像分割爲 13 乘 13 單元的網格:github
每一個單元都負責預測 5 個邊界框。邊界框表明着這個矩形包含着一個物體。算法
YOLO 也會輸出一個 確信值 來告訴咱們它有多肯定邊界框裏是否包含某個物體。這個分數不會包含任何關於邊界框內的物體是什麼的信息,只是這個框是否符合標準。
預測以後的邊界框可能看上去像下面這樣(確信值越高,盒子的邊界畫的越寬)
對每一個邊界框,單元也會推測一個類別。這就像分類器同樣:它提供了全部可能類的可能性分佈狀況。這個版本的 YOLO 咱們是經過 PASCAL VOC dataset 來訓練的,它能夠識別 20 種不一樣的類,好比:
邊界框的確信值和類的預測組合成一個最終分數,告訴咱們邊界框中包含一個特定類型的物體的可能性。舉個例子,左側這個又大又粗的黃色方框認爲有 85% 的可能性它包含了「狗」這個物體。
一共有 13×13 = 169 個單元格,每一個單元格預測 5 個邊界框,最終咱們會有 845 個邊界框。事實證實,大部分的框的確信值都很低,因此咱們只保留那些最終得分在 30% 及以上的值(你能夠根據你所須要的精確程度來修改這個下限)。
接下來是最後的預測:
從總共 845 的個邊界框中咱們只保留了這三個,由於它們給出了最好的結果。可是請注意雖然是 845 個獨立的預測,它們都是同時運行的 - 神經網絡只會運行一次。這也是爲何 YOLO 是如此的強大和快速。
(上圖來自 pjreddie.com。)
YOLO 的架構是很簡單的,它就是一個卷積神經網絡:
Layer kernel stride output shape
---------------------------------------------
Input (416, 416, 3)
Convolution 3×3 1 (416, 416, 16)
MaxPooling 2×2 2 (208, 208, 16)
Convolution 3×3 1 (208, 208, 32)
MaxPooling 2×2 2 (104, 104, 32)
Convolution 3×3 1 (104, 104, 64)
MaxPooling 2×2 2 (52, 52, 64)
Convolution 3×3 1 (52, 52, 128)
MaxPooling 2×2 2 (26, 26, 128)
Convolution 3×3 1 (26, 26, 256)
MaxPooling 2×2 2 (13, 13, 256)
Convolution 3×3 1 (13, 13, 512)
MaxPooling 2×2 1 (13, 13, 512)
Convolution 3×3 1 (13, 13, 1024)
Convolution 3×3 1 (13, 13, 1024)
Convolution 1×1 1 (13, 13, 125)
---------------------------------------------複製代碼
這種神經網絡只使用了標準的層類型:3x3 核心的卷積層和 2x2 的最大值池化層,沒有複雜的事務。YOLOv2 中沒有全鏈接層。
注意: 咱們將要使用的「迷你」版本的 YOLO 只有 9 個卷積層和 6 個池化層。完整版的 YOLOv2 模型的層數是「迷你」版的 3 倍,而且有一個略微複雜的形狀,但它仍然是一個常規的轉換。
最後的卷積層有個 1x1 的核心用於下降數據到 13x13x125 的尺寸。這個 13x13 看上去很熟悉:這正是圖像原來分割以後的網格尺寸。
因此最終咱們給每一個網格單元生成了 125 個通道。這 125 個數字包含了邊界框中的數據和類型預測。爲何是 125 個呢?恩,每一個單元格預測 5 個邊界框,而且一個邊界框經過 25 個數據元素來描述:
使用 YOLO 很簡單:你給它一個輸入圖像(尺寸調節到 416x416 像素),它在單一傳遞下經過卷積網絡,最後轉變爲 13x13x125 的張量來描述這些網格單元的邊界框。你所須要作的只是計算這些邊界框的最終分數,將那些小於 30% 的分數遺棄。
提示: 爲了學習更多關於 YOLO 的工做原理和訓練方式,看下這個其中一位發明者的精彩的演講。這個視頻實際上描述的是 YOLOv1,一個在構建方面略微有點不一樣的老版本,可是其主要思想仍是同樣的。值得一看!
我剛剛描述的架構是迷你 YOLO 的,正是咱們將在 iOS app 中使用的那個。完整的 YOLOv2 網絡包含 3 倍的層數,而且這對於目前的 iPhone 來講想快速運行它,有點太大了。所以,迷你 YOLO 用了更少的層數,這使它比它哥哥快了很多,可是也損失了一些精確度。
YOLO 是用 Darknet 寫的,YOLO 做者的一個自定義深度學習框架。可下載到的權重只有 Darknet 格式。雖然 Darknet 已經開源了,可是我不是很願意花太多的時間來弄清楚它是怎麼工做的。
幸運的是,有人已經嘗試並把 Dardnet 模型轉換爲 Keras,剛好是我所用的深度學習工具。所以我惟一要作的就是執行這個 」YAD2K「 的腳原本把 Darknet 格式的權重轉換到 Keras 格式,而後再寫我本身的腳本,把 Keras 權重轉換到 Metal 的格式。
可是,仍然有些奇怪…… YOLO 在卷積層以後使用的是一個常規的技術叫作批量歸一化。
在」批量歸一化「背後的想法是數據乾淨的時候神經網絡工做效果最好。理想狀況下,輸入到層的數據的均值是 0 而且沒有太多的分歧。任何作過任意機器學習的人應該很熟悉這個,由於咱們常用一個叫作」特徵縮放「或者」白化「在咱們的輸入數據上來實現這一效果。
批量歸一化在層與層之間對數據作了一個相似的特徵縮放的工做。這個技術讓神經網絡表現的更好由於它暫停了數據因爲在網絡中流動而致使的污染。
爲了讓你大體瞭解批量歸一的做用,看一看下面這兩個直方圖,分別是第一次應用卷積層後進行歸一化與不進行歸一化的不一樣結果。
在訓練深度網絡的時候,批量歸一化很重要,可是咱們證明在推斷時能夠不用這個操做。這樣效果不錯,由於不作批量歸一化的計算會讓咱們的 app 更快。並且任何狀況下,Metal 都沒有一個 MPSCNNBatchNormalization
層。
批量歸一化一般在卷積層以後,在激活函數(在 YOLO 中叫作」泄露「的 Relu )生效以前。既然卷積和批量統一都是對數據的線性轉換,咱們能夠把批量統一層的參數和卷積的權重組和到一塊兒。這叫作把批量統一層」摺疊「到卷積層。
長話短說,經過一些數學運算,咱們能夠移除批量歸一層,可是並不意味着咱們在卷積層以前必須去改變權重。
關於卷積層計算內容的快速總結:若是 x
是輸入圖像的像素,w
是這層的權重,卷積根本上來講就是按下面的方式計算每一個輸出像素:
out[j] = x[i]*w[0] + x[i+1]*w[1] + x[i+2]*w[2] + ... + x[i+k]*w[k] + b複製代碼
這是輸入像素和卷積權重點積和加上一個偏置值 b
,
下面這是批量歸一化對上述卷積輸出結果進行的計算操做:
gamma * (out[j] - mean)
bn[j] = ---------------------- + beta
sqrt(variance)複製代碼
它先減去了輸出像素的平均值,除以方差,再乘以一個縮放參數 gamma,而後加上偏移量 beta。這四個參數 — mean
,variance
, gamma
,和 beta
。- 正是批量統一層隨着網絡訓練以後學到的內容。
爲了移除批量歸一化,咱們能夠把這兩個等式調整一下來給卷積層計算新的權重和偏置量:
gamma * w
w_new = --------------
sqrt(variance)
gamma*(b - mean)
b_new = ---------------- + beta
sqrt(variance)複製代碼
用這個基於輸入 x
的新權重和偏置項來進行卷積操做會獲得和以前卷積加上批量歸一化同樣的結果。
如今咱們能夠移除批量歸一化層只用卷積層了,可是因爲調整了權重和新的偏置項 w_new
和 b_new
。咱們要對網絡中全部的卷積層都重複這個操做。
注意: 實際上在 YOLO 中,卷積層並無使用偏置量,因此 b
在上面的等式中始終是 0 。可是請注意在摺疊批量歸一化參數的以後,卷積層真獲得了一個偏置項。
一旦咱們把全部的批量歸一化層都摺疊到它們的以前卷積層中時,咱們就能夠把權重轉換到 Metal 了。這是一個很簡單的數組轉換(Keras 與 Metal 相比是用不一樣的順序來存儲),而後把它們寫入到一個 32 位浮點數的二進制文件中。
若是你好奇的話,看下這個轉換腳本 yolo2metal.py 能夠了解更多。爲了測試這個摺疊工做,這個腳本生成了一個新的模型,這個模型沒有批量歸一化層而是用了調整以後的權重,而後和以前的模型的推測進行一個比較。
毋庸置疑地,我用了 Forge 來構建 iOS 應用。
😂 你能夠在 YOLO 的文件夾中找到代碼。想試的話:下載或者 clone Forge,在 Xcode 8.3 或者更新的版本中打開 Forge.xcworkspace ,而後在 iPhone 6 或者更高版本的手機上運行 YOLO 這個 target 。
測試這個應用的最簡單的方法是把你的 iPhone 對準這些 YouTube 視頻上:
有趣的代碼是在 YOLO.swift 中。首先它初始化了卷積網絡:
let leaky = MPSCNNNeuronReLU(device: device, a: 0.1)
let input = Input()
let output = input
--> Resize(width: 416, height: 416)
--> Convolution(kernel: (3, 3), channels: 16, padding: true, activation: leaky, name: "conv1")
--> MaxPooling(kernel: (2, 2), stride: (2, 2))
--> Convolution(kernel: (3, 3), channels: 32, padding: true, activation: leaky, name: "conv2")
--> MaxPooling(kernel: (2, 2), stride: (2, 2))
--> ...and so on...複製代碼
先把來自攝像頭的輸入縮放至 416x416 像素,而後輸入到卷積和最大池化層中。這和其餘的轉換操做都很是類似。
有趣的是在輸出以後的操做。回想一下輸出的轉換以後是一個 13x13x125 的張量:圖片中的每一個網格的單元都有 125 個通道的數據。這 125 數據包含了邊界框和類型的預測,而後咱們須要以某種方式把輸出排序。這些都在函數 fetchResult()
中進行。
注意: fetchResult()
中的代碼是在 CPU 中執行的,不是在 GPU 中。這樣的方式更容易實現。話句話說,這個嵌套的循環在 GPU 中並行執行可能效果會更好。將來我也許會研究這個,而後再寫一個 GPU 的版本。
下面介紹了 fetchResult() 是如何工做的:
public func fetchResult(inflightIndex: Int) -> NeuralNetworkResult<Prediction> {
let featuresImage = model.outputImage(inflightIndex: inflightIndex)
let features = featuresImage.toFloatArray()複製代碼
在卷積層的輸出是以 MPSImage
的格式的。咱們先把它轉換到一個叫作 features 的 Float 值類型的數組,以便咱們更好的使用它。
fetchResult()
的主體是一個大的嵌套循環。它包含了全部的網格單元和每一個單元的五次預測:
for cy in0..<13 {
for cx in0..<13 {
for b in0..<5 {
. . .
}
}
}複製代碼
在這個循環裏面,咱們給網格單元 (cy, cx)
計算了邊界框 b
。
首先咱們從 features
數組中讀取邊界框的 x, y, width 和 height ,也包括確信值。
let channel = b*(numClasses + 5)
let tx = features[offset(channel, cx, cy)]
let ty = features[offset(channel + 1, cx, cy)]
let tw = features[offset(channel + 2, cx, cy)]
let th = features[offset(channel + 3, cx, cy)]
let tc = features[offset(channel + 4, cx, cy)]複製代碼
幫助函數 offset()
用來定位數組中合適的讀取位置。Metal 以每次 4 個通道一組來把數據存在紋理片中,這意味着 125 個通道不是連續存儲,而是分散存儲的。(想深刻分析的話能夠去看源碼)。
咱們仍然須要處理 tx
, ty
, tw
, th
, tc
這五個參數 ,由於它們的格式有點奇怪。若是你不知道這些處理方法哪來的話,能夠看下這篇論文 (這是訓練這個神經網絡的附加產物之一)。
譯者注:這篇論文就是 YOLO 的做者寫的。做者在訓練的過程當中造成了這篇論文,並做爲訓練過程的一個更詳細的描述。
llet x = (Float(cx) + Math.sigmoid(tx)) * 32
let y = (Float(cy) + Math.sigmoid(ty)) * 32
let w = exp(tw) * anchors[2*b ] * 32
let h = exp(th) * anchors[2*b + 1] * 32
let confidence = Math.sigmoid(tc)複製代碼
如今 x
和 y
表明了在咱們使用的輸入到神經網絡的 416x416 的圖像中邊界框的中心;w
和 h
則是上述圖像空間中邊界框的寬度和高度。邊界框的確信值是 tc
,咱們經過 sigmoid 函數把它轉換到百分比。
如今咱們有了咱們的邊界框,而且咱們知道了 YOLO 對這個框中是否包含着某個對象的確信度。接下來,讓咱們看下類型預測,來看看 YOLO 認爲框中究竟是個什麼類型的物體:
var classes = [Float](repeating: 0, count: numClasses)
for c in 0..< numClasses {
classes[c] = features[offset(channel + 5 + c, cx, cy)]
}
classes = Math.softmax(classes)
let (detectedClass, bestClassScore) = classes.argmax()複製代碼
從新調用 features
數組中包含着對邊界框中物體預測的 20 個通道。咱們讀取到一個新的數組 classes
中。由於是用來作分類器的,咱們經過 softmax 把這個數組轉換成可能的分配狀況,而後咱們選擇最高分數的類做爲最後的勝者。
如今咱們能夠計算邊界框的最終分數了 - 舉個例子,「這個邊界框有 85% 的機率包含一條狗」。因爲一共有 845 個邊界框,而咱們只想要那些分數高於某個值的邊界框。
let confidenceInClass = bestClassScore * confidence
if confidenceInClass > 0.3 {
let rect = CGRect(x: CGFloat(x - w/2), y: CGFloat(y - h/2),
width: CGFloat(w), height: CGFloat(h))
let prediction = Prediction(classIndex: detectedClass,
score: confidenceInClass,
rect: rect)
predictions.append(prediction)
}複製代碼
上面的代碼是對網格內的每一個單元進行循環。當循環結束後,咱們一般會有了一個包含了 10 到 20 個預測 predictions
數組。
咱們已通過濾掉了那些低分數的邊界框,可是仍然有些框的和其餘的框有較多的重疊。所以,在最後一步咱們須要在 fetchResult()
裏面作的事叫作 非極大抑制 ,用來去掉那些重複的框。
var result = NeuralNetworkResult<Prediction>()
result.predictions = nonMaxSuppression(boxes: predictions,
limit: 10, threshold: 0.5)
return result
}複製代碼
nonMaxSuppression()
函數使用的算法很簡單:
這會移除那些有高分數可是和其餘框有太多重複部分的框。只會保留最好的那些框。
上面這些差很少就是這個意思:一個常規的卷積網絡加上對結果的一系列處理。
YOLO 網站聲稱迷你版本的 YOLO 能夠實現 200 幀每秒。可是固然這是在一個桌面級的 GPU 上,不是在移動設備上。因此在 iPhone 上它能跑多快呢?
在個人 iPhone 6s 上面處理一張圖片大約須要 0.15 秒 。幀率只有 6 ,這幀率基本知足實時的調用。若是你把你的手機對着開過的汽車,你能夠看到有個邊界框在車子後面不遠的地方跟着它。儘管如此,我仍是被這個技術深深的震驚了。 😁
注意: 正如我上面所解釋的,邊界框的處理是在 CPU 而不是 GPU 上的。若是徹底在 GPU 上運行是否是會更快呢?可能,可是 CPU 的代碼只用了 0.03 秒, 20% 的運行時間。在 GPU 上處理一部分的工做是可行的,可是我不肯定這樣是否值得,由於轉換層仍然佔用了 80% 的時間。
我認爲慢的主要緣由之一是因爲卷積層包含了 512 和 1024 個輸出通道。在個人實驗中,彷佛 MPSCNNConvolution
在處理多通道的小圖片比少通道的大圖片時更吃力。
一個讓我想去嘗試的是採用不一樣的網絡構建方式,好比 SqueezeNet ,而後從新訓練網絡來在最後一層進行邊界框的預測。換句話說,採用 YOLO 的想法並將它在一個更小更快的轉換之上實現。用準確度的降低來換取速度的提高的作法是否值得呢?
注意: 另外,最近發佈的 Caffe2 框架一樣是經過 Metal 來實如今 iOS 上運行的。Caffe2-iOS 項目來自於迷你 YOLO 的一個版本。它彷佛比純 Metal 版本運行的慢 0.17 秒每幀。
想了解更多關於 YOLO 的信息,看下如下由它的做者們寫的論文吧:
個人實現是部分基於 TensorFlow 的 Android demo TF Detect, Allan Zelener 的YAD2K, 和 Darknet的源碼。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。