景的YOLOv3?;奋囕v小目標(biāo)識(shí)別實(shí)踐)
簡(jiǎn)介這份PDF文檔是發(fā)表于《山西交通科技》2020年第4期的技術(shù)論文面向交通管理、智能交通與計(jì)算機(jī)視覺方向的研究人員和工程實(shí)踐者聚焦高速公路場(chǎng)景下?;愤\(yùn)輸車輛貨物類型標(biāo)志的自動(dòng)識(shí)別問題。全文圍繞YOLOv3單階段目標(biāo)檢測(cè)模型展開介紹基于機(jī)器視覺的車輛抓拍系統(tǒng)、包含10000張車輛背面圖像與23類標(biāo)注的?;坟浳镱愋蛿?shù)據(jù)集構(gòu)建流程以及模型訓(xùn)練參數(shù)、損失函數(shù)和不同光照天氣條件下的實(shí)驗(yàn)結(jié)果。壓縮包僅含1個(gè)PDF文件大小約2.41MB便于直接閱讀、檢索與引用。已有107人學(xué)習(xí)適合需要了解目標(biāo)檢測(cè)落地交通監(jiān)管、數(shù)據(jù)集標(biāo)注和實(shí)時(shí)識(shí)別方案的讀者參考也可為危化品運(yùn)輸安全管理和后續(xù)智能交通系統(tǒng)研究提供方法借鑒。1. 一個(gè)反直覺的事實(shí)這類識(shí)別最難的地方不是分清是第幾類?;奋囕v貨物類型識(shí)別這套系統(tǒng)真正難的地方不是分辨是第幾類危險(xiǎn)品而是在多數(shù)時(shí)間里根本看不清。一輛車以八十公里時(shí)速通過卡口從觸發(fā)抓拍到駛離畫面不到三百毫秒車尾那塊菱形標(biāo)志牌在四千像素寬的原圖里也許只有一百多個(gè)像素等比縮到檢測(cè)網(wǎng)絡(luò)常用的四百一十六見方輸入上往往不足十個(gè)像素——比一個(gè)卷積核還小。所以這個(gè)任務(wù)的核心矛盾從來不是分類頭夠不夠強(qiáng)而是極小目標(biāo)與硬實(shí)時(shí)同時(shí)壓在同一張卡上。把 YOLOv3 這類單階段檢測(cè)器放進(jìn)高速公路場(chǎng)景做?;奋囕v貨物類型識(shí)別本質(zhì)上是在這個(gè)矛盾里做取舍讓出一部分定位精度換回每一幀都能在邊緣側(cè)算完的時(shí)間預(yù)算。搞懂這套取舍怎么落地比記住模型名字有用得多。2. 單階段回歸與兩階段流水線卡口場(chǎng)景為什么等不起 R-CNN 那條路深度學(xué)習(xí)做目標(biāo)檢測(cè)有兩條主干路線這不是風(fēng)格差異而是兩種完全不同的時(shí)間預(yù)算分配方式。兩階段檢測(cè)器把哪里有東西和這是什么東西拆成兩道工序單階段檢測(cè)器把它們壓成一次前向。?;奋囕v貨物類型識(shí)別這個(gè)任務(wù)恰好卡在必須選邊站的位置上標(biāo)志牌本身特征穩(wěn)定、外觀規(guī)范但它在畫面里極小而且系統(tǒng)必須在車輛通過卡口的瞬間給出結(jié)果晚一秒這條數(shù)據(jù)就只剩存檔價(jià)值。2.1 兩階段檢測(cè)器的時(shí)間究竟花在哪以 R-CNN 系列為代表的兩階段方案結(jié)構(gòu)上分三塊第一塊生成基于類別的候選區(qū)域也就是先把圖里可能是目標(biāo)的位置粗篩出來通常幾千個(gè)第二塊用深度卷積網(wǎng)絡(luò)對(duì)每個(gè)候選區(qū)域抽一個(gè)固定長(zhǎng)度的特征向量第三塊用線性分類器早期是 SVM判定類別并回歸邊框。這個(gè)設(shè)計(jì)的出發(fā)點(diǎn)很合理——先粗后精把算力集中在少量候選上背景區(qū)域只過一遍輕量篩選。問題出在第二塊。候選區(qū)域是任意尺寸的矩形要送進(jìn)全卷積網(wǎng)絡(luò)抽特征就得先做形變對(duì)齊或者用 RoI Pooling 之類的操作把它們拉成統(tǒng)一尺寸這一步在早期實(shí)現(xiàn)里是串行的幾千個(gè)候選就是幾千次前向。即便后續(xù)的改進(jìn)版本把特征抽取改成在共享特征圖上裁剪候選區(qū)域的數(shù)量級(jí)依然決定了它比單階段慢一個(gè)檔次。放到卡口場(chǎng)景里一幀 4K 圖要跑幾百毫秒甚至上秒級(jí)多路攝像頭匯到一臺(tái)邊緣設(shè)備上隊(duì)列立刻堆爆。我在別的項(xiàng)目里踩過一次類似的坑把一套兩階段方案直接搬到實(shí)時(shí)流上單幀延遲看著還能接受但吞吐量完全撐不住因?yàn)楹蜻x區(qū)域生成這步吃的是 GPU 的并行度而不是單純的計(jì)算量。降低候選數(shù)量能提速度但小目標(biāo)的召回會(huì)跟著掉——這正好是最不能犧牲的部分。2.2 單階段把檢測(cè)寫成一次回歸賭的是什么單階段檢測(cè)器的思路是把檢測(cè)直接當(dāng)回歸問題處理預(yù)定義一組錨點(diǎn)密集覆蓋整張圖的空間位置網(wǎng)絡(luò)對(duì)每個(gè)位置直接輸出這里有沒有目標(biāo)、框多大、是什么類不再有獨(dú)立的區(qū)域建議網(wǎng)絡(luò)。YOLO 系列、SSD、RetinaNet 都屬于這一類。這個(gè)賭注的核心是卷積網(wǎng)絡(luò)在提取特征時(shí)其實(shí)已經(jīng)隱含地知道哪個(gè)位置有東西了再單獨(dú)搞一個(gè)建議網(wǎng)絡(luò)去顯式生成候選是重復(fù)勞動(dòng)。把分類和定位塞進(jìn)同一個(gè)輸出張量一次前向就能拿到全部結(jié)果。代價(jià)是正負(fù)樣本極度不平衡——一張 416×416 的圖上十幾萬個(gè)預(yù)測(cè)位置里可能只有兩三個(gè)是真的目標(biāo)其余全是背景。這也是后來 RetinaNet 提出 Focal Loss 想解決的問題。?;奋囕v貨物類型識(shí)別選的正是這條路。理由不復(fù)雜標(biāo)志牌外觀規(guī)范、類別定義清晰網(wǎng)絡(luò)不需要太強(qiáng)的這是什么語義推理能力反而對(duì)快極其敏感。判斷一個(gè)技術(shù)選型是否合理看它犧牲了什么更值錢——這里犧牲的是定位精度上限換的是每幀幾十毫秒的確定性延遲。2.3 網(wǎng)格、錨框與三尺度特征圖標(biāo)志牌由哪個(gè)格子負(fù)責(zé)YOLOv3 的具體做法是把特征圖劃分為 S×S 個(gè)網(wǎng)格每個(gè)網(wǎng)格生成 B 個(gè)邊界框。如果只看一層特征圖這個(gè)劃分很粗暴一個(gè)格子只能負(fù)責(zé)一個(gè)目標(biāo)兩個(gè)標(biāo)志牌擠在同一個(gè)格子里就必然丟一個(gè)。YOLOv3 的解法是同時(shí)在三個(gè)尺度上做預(yù)測(cè)特征圖分別來自主干網(wǎng)絡(luò)的不同深度下采樣倍率通常是 32、16、8對(duì)應(yīng) 13×13、26×26、52×52以 416 輸入為例。淺層特征圖分辨率高、感受野小負(fù)責(zé)小目標(biāo)深層特征圖分辨率低、感受野大負(fù)責(zé)大目標(biāo)。每個(gè)尺度分配 3 個(gè)錨框九個(gè)錨框的寬高是先驗(yàn)值訓(xùn)練前用 k-means 在數(shù)據(jù)集上聚類得到而不是拍腦袋定。COCO 上那組經(jīng)典數(shù)值是 (10,13)、(16,30)、(33,23)、(30,61)、(62,45)、(59,119)、(116,90)、(156,198)、(373,326)但直接拿來用會(huì)翻車——危化品標(biāo)志牌的形狀分布和 COCO 里的行人車輛完全不是一回事。import numpy as np # 三個(gè)預(yù)測(cè)尺度對(duì)應(yīng)的錨框?qū)? 高按特征圖從粗到細(xì)排列 ANCHORS [ [(116, 90), (156, 198), (373, 326)], # 13x13, stride 32管大目標(biāo) [(30, 61), (62, 45), (59, 119)], # 26x26, stride 16管中目標(biāo) [(10, 13), (16, 30), (33, 23)], # 52x52, stride 8 管小目標(biāo) ] def assign_anchor(gt_wh): 把真值框分給 IOU 最大的錨框返回該錨框在全部 9 個(gè)里的全局索引 gt_w, gt_h gt_wh best_iou, best_idx -1.0, -1 for scale_id, scale_anchors in enumerate(ANCHORS): for a_id, (aw, ah) in enumerate(scale_anchors): # 左上角對(duì)齊后求交并比避免中心點(diǎn)偏移干擾形狀匹配 inter min(gt_w, aw) * min(gt_h, ah) union gt_w * ah aw * gt_h - inter iou inter / union if iou best_iou: best_iou, best_idx iou, scale_id * 3 a_id return best_idx, best_iou # 用法統(tǒng)計(jì)數(shù)據(jù)集里每個(gè)真值框落到哪個(gè)錨框形狀差太遠(yuǎn)就該重新聚類 for wh in [(24, 26), (18, 40), (60, 22)]: print(wh, assign_anchor(wh))這段代碼做的是責(zé)任劃分訓(xùn)練時(shí)每個(gè)真值框只交給形狀最接近的那個(gè)錨框去擬合其余的錨框在這個(gè)目標(biāo)上不產(chǎn)生定位損失。參數(shù)說明上gt_wh是標(biāo)注框的寬高像素單位或歸一化后乘輸入尺寸都行只要和錨框同量綱inter用min(w)相乘是一種簡(jiǎn)化寫法前提是兩個(gè)框左上角對(duì)齊對(duì)形狀匹配來說夠用best_idx除以 3 取整就是尺度編號(hào)取余就是該尺度內(nèi)的錨框編號(hào)。危險(xiǎn)貨物標(biāo)志牌的形狀差異很大主標(biāo)志是菱形正方形旋轉(zhuǎn) 45 度外接框仍是正方形 subsidiary 標(biāo)志有長(zhǎng)方形、三角形還有文字說明牌是細(xì)長(zhǎng)條。如果錨框全是接近正方形的那些細(xì)長(zhǎng)條標(biāo)志牌的匹配 IOU 會(huì)低到 0.2 以下訓(xùn)練時(shí)定位損失怎么都降不下去。常見的實(shí)現(xiàn)思路是在自己數(shù)據(jù)集上跑一遍 k-means把九組寬高重新算出來這一步的收益往往比換主干網(wǎng)絡(luò)更明顯。2.4 前向解碼從張量到屏幕上的框網(wǎng)絡(luò)輸出的是原始張量要把它們還原成圖上的坐標(biāo)中間有一層解碼。以某一尺度為例輸出通道數(shù)是3 × (4 1 類別數(shù))3 是錨框數(shù)4 是中心點(diǎn)和寬高1 是置信度剩下是類別概率。中心點(diǎn)用 sigmoid 壓到 0 到 1 之間再疊加上所在網(wǎng)格的左上角坐標(biāo)寬高用指數(shù)變換乘上錨框先驗(yàn)值。import numpy as np def sigmoid(x): return 1.0 / (1.0 np.exp(-x)) def decode(pred, anchors, stride, img_size416, conf_thres0.5): 把單尺度原始輸出解碼成 [x1, y1, x2, y2, conf, cls] 形式的框 grid img_size // stride pred pred.reshape(grid, grid, 3, 5 NUM_CLASSES) # 網(wǎng)格左上角坐標(biāo)形狀 (grid, grid, 1) grid_y, grid_x np.meshgrid(np.arange(grid), np.arange(grid), indexingij) grid_x grid_x[..., None] grid_y grid_y[..., None] # 中心點(diǎn)sigmoid 后加上網(wǎng)格偏移再乘 stride 回到原圖尺度 cx (sigmoid(pred[..., 0:1]) grid_x) * stride cy (sigmoid(pred[..., 1:2]) grid_y) * stride # 寬高exp 保證為正再乘錨框先驗(yàn) bw np.exp(pred[..., 2:3]) * np.array(anchors)[:, 0].reshape(1, 1, 3, 1) * stride bh np.exp(pred[..., 3:4]) * np.array(anchors)[:, 1].reshape(1, 1, 3, 1) * stride conf sigmoid(pred[..., 4:5]) cls_prob sigmoid(pred[..., 5:]) # 置信度過濾低于閾值的格子直接扔掉這是速度的第一道閘門 keep conf[..., 0] conf_thres boxes np.concatenate([(cx - bw / 2)[keep], (cy - bh / 2)[keep], (cx bw / 2)[keep], (cy bh / 2)[keep]], axis-1) return boxes解碼里有幾個(gè)容易寫錯(cuò)的地方。grid_x和grid_y的索引順序必須和 reshape 的張量布局一致否則框會(huì)整體轉(zhuǎn)置九十度——這種錯(cuò)誤在可視化時(shí)一眼能看出來但在指標(biāo)上只表現(xiàn)為 mAP 掉了幾個(gè)點(diǎn)很容易被誤判成模型能力不足。stride是特征圖相對(duì)原圖的降采樣倍率13×13 那層是 3252×52 那層是 8三個(gè)尺度必須分別傳對(duì)。置信度閾值在邊緣側(cè)通常設(shè)在 0.5 左右設(shè)太高小目標(biāo)直接被濾掉設(shè)太低則 NMS 前候選框數(shù)量爆炸后處理耗時(shí)反超網(wǎng)絡(luò)前向。三個(gè)尺度的解碼結(jié)果拼在一起之后才做 NMS這一步的順序不能反。按類別分別做 NMS 還是全局做也會(huì)影響結(jié)果如果先全局 NMS 再分類兩個(gè)不同類別但空間重疊的標(biāo)志牌會(huì)互相壓制先按類別分組再各自 NMS代價(jià)是后處理循環(huán)次數(shù)變多。?;奋囕v車尾經(jīng)常同時(shí)貼主標(biāo)志和副標(biāo)志這個(gè)問題必須提前想清楚。3. 損失函數(shù)四項(xiàng)拆解定位、置信度、分類為什么要分開算YOLOv3 的總損失是四項(xiàng)相加形式上看是簡(jiǎn)單的加法實(shí)際上每一項(xiàng)的量綱、樣本范圍、梯度尺度都不一樣混在一起很容易訓(xùn)崩。理解這四項(xiàng)各自負(fù)責(zé)什么比調(diào)參本身更重要。3.1 四項(xiàng)誤差各自的職責(zé)與量綱沖突第一項(xiàng)是中心點(diǎn)誤差衡量預(yù)測(cè)框中心和真值框中心的距離衡量的是位置對(duì)不對(duì)。第二項(xiàng)是寬高誤差衡量的是框大小對(duì)不對(duì)。第三項(xiàng)是置信度誤差衡量這個(gè)位置有沒有目標(biāo)這一項(xiàng)在正樣本和負(fù)樣本上都要算是整個(gè)損失里樣本量最大的一項(xiàng)。第四項(xiàng)是分類誤差只管正樣本里類別對(duì)不對(duì)。量綱沖突在這里非常具體。中心點(diǎn)坐標(biāo)歸一化后在 0 到 1 之間誤差數(shù)量級(jí)很小寬高是經(jīng)過 log 變換再和錨框比誤差可能到幾置信度和分類都是概率誤差在 0 到 1 之間。如果不做權(quán)重平衡訓(xùn)練早期會(huì)明顯偏向把置信度壓下去——因?yàn)樨?fù)樣本數(shù)量占絕對(duì)多數(shù)把置信度全預(yù)測(cè)成 0 就能讓總損失降一大截網(wǎng)絡(luò)會(huì)躺平在這個(gè)局部最優(yōu)里。常見的實(shí)現(xiàn)思路是給定位項(xiàng)加權(quán)通常中心點(diǎn)和寬高各乘一個(gè)系數(shù)比如 5置信度的負(fù)樣本項(xiàng)乘一個(gè)較小的系數(shù)比如 0.5分類項(xiàng)給 1。這組數(shù)字不是理論推導(dǎo)出來的是實(shí)踐里試出來的換數(shù)據(jù)集需要重新看曲線。def yolo_loss(pred, target, obj_mask, noobj_mask, cls_weight1.0): 四項(xiàng)損失分開算逐項(xiàng)打日志比只看總 loss 有用得多 # 1) 中心點(diǎn)sigmoid 輸出用二元交叉熵只在正樣本格子上生效 loss_xy BCE(pred[..., 0:2], target[..., 0:2]).sum() * obj_mask / N # 2) 寬高對(duì) log 尺度做平方誤差同樣只算正樣本 loss_wh MSE(pred[..., 2:4], target[..., 2:4]).sum() * obj_mask / N # 3) 置信度正負(fù)樣本都算但 IOU 落在忽略區(qū)間的框兩邊都不懲罰 loss_conf (BCE(pred[..., 4], target[..., 4]) * (obj_mask 0.5 * noobj_mask)).sum() / N # 4) 分類只算正樣本多標(biāo)簽用 sigmoid 而非 softmax loss_cls (BCE(pred[..., 5:], target[..., 5:]) * obj_mask).sum() / N * cls_weight return loss_xy loss_wh loss_conf loss_cls這段代碼有兩個(gè)設(shè)計(jì)點(diǎn)值得說。一是分類用的是 sigmoid 而不是 softmax也就是說每個(gè)類別獨(dú)立判斷不是互斥的。這對(duì)?;窐?biāo)志牌識(shí)別是必要的一塊標(biāo)志牌完全可能同時(shí)表達(dá)易燃和有毒兩種屬性softmax 會(huì)強(qiáng)迫網(wǎng)絡(luò)在兩者之間選一個(gè)直接限制了表達(dá)能力。二是忽略區(qū)間的處理——預(yù)測(cè)框和真值框的 IOU 高于閾值但又不是最大匹配的那個(gè)既不當(dāng)作正樣本也不當(dāng)作負(fù)樣本noobj_mask里把它置零這一項(xiàng)是防止訓(xùn)練早期差不多對(duì)的框被反復(fù)懲罰導(dǎo)致定位震蕩。3.2 寬高為什么不能直接回歸像素值一個(gè)容易被忽略的細(xì)節(jié)寬高項(xiàng)回歸的是尺度因子不是像素絕對(duì)值。如果網(wǎng)絡(luò)直接預(yù)測(cè)像素寬高那么同一個(gè)標(biāo)志牌在近處拍可能是 200 像素、遠(yuǎn)處拍可能只有 30 像素網(wǎng)絡(luò)要擬合的數(shù)值跨度差了近一個(gè)數(shù)量級(jí)梯度極不穩(wěn)定。改成預(yù)測(cè)log(真值寬 / 錨框?qū)?等于讓網(wǎng)絡(luò)只學(xué)這個(gè)框比先驗(yàn)大多少倍數(shù)值集中在 0 附近訓(xùn)練就平穩(wěn)了。解碼時(shí)對(duì)應(yīng)的就是exp(預(yù)測(cè)值) × 錨框?qū)?× stride。這個(gè)exp還有一個(gè)好處保證輸出恒正永遠(yuǎn)不會(huì)出現(xiàn)負(fù)寬高的退化框。對(duì)小目標(biāo)來說這個(gè)設(shè)計(jì)有個(gè)隱含代價(jià)。遠(yuǎn)處車輛上的標(biāo)志牌真值寬可能只有 12 像素錨框?qū)捠?16 像素比值 0.75log 后約 -0.29網(wǎng)絡(luò)預(yù)測(cè)值稍微偏一點(diǎn)比如偏 0.3exp之后就是 1.35 倍寬變成 21 像素。也就是說相對(duì)誤差被指數(shù)放大了小目標(biāo)的定位精度天然吃虧。這是為什么提高輸入分辨率對(duì)小目標(biāo)檢測(cè)的收益遠(yuǎn)大于換更大的主干網(wǎng)絡(luò)。3.3 訓(xùn)練節(jié)奏動(dòng)量 0.9、權(quán)重衰減 0.0005、300 輪與兩次衰減這套訓(xùn)練的配置是動(dòng)量 0.9權(quán)重衰減 0.0005訓(xùn)練 300 輪在第 70% 和第 90% 處把學(xué)習(xí)率乘 0.1 衰減兩次。這幾個(gè)數(shù)字放在一起看能讀出一些設(shè)計(jì)意圖。動(dòng)量 0.9 是 SGD 類優(yōu)化器的常用值作用是把歷史梯度累積起來抑制批次間的抖動(dòng)。目標(biāo)檢測(cè)的損失曲面在正負(fù)樣本極不平衡時(shí)特別抖純粹靠降低學(xué)習(xí)率來穩(wěn)定訓(xùn)練收斂會(huì)很慢加動(dòng)量等于給優(yōu)化過程加了一點(diǎn)慣性能穿過那些淺的局部坑。權(quán)重衰減 0.0005 是 L2 正則系數(shù)管的是防止權(quán)重?cái)?shù)值無限膨脹。檢測(cè)網(wǎng)絡(luò)的分類頭在長(zhǎng)尾類別上特別容易過擬合稀有類的樣本可能只有幾十個(gè)不加正則的話分類頭會(huì)把權(quán)重全推到高頻類上。0.0005 這個(gè)量級(jí)不會(huì)明顯損害擬合能力但能壓住一部分過擬合。300 輪配兩次衰減的節(jié)奏本質(zhì)上是粗調(diào)—精調(diào)—微調(diào)三段式。前 210 輪用大學(xué)習(xí)率把權(quán)重從隨機(jī)初始化拉到大致合理的位置中間 60 輪把學(xué)習(xí)率降一個(gè)數(shù)量級(jí)讓網(wǎng)絡(luò)在局部細(xì)化最后 30 輪再降一個(gè)數(shù)量級(jí)做收尾。這個(gè)安排有個(gè)前提數(shù)據(jù)集規(guī)模不大300 輪在大數(shù)據(jù)集上可能連第一段都跑不完。10000 張圖、22174 個(gè)框的規(guī)模300 輪正好是一個(gè)能讓 loss 走平又不至于過擬合的區(qū)間。提示衰減點(diǎn)的選擇要看 loss 曲線不看固定比例。如果 loss 在第 60% 就已經(jīng)開始震蕩衰減點(diǎn)前移往往比繼續(xù)硬訓(xùn)有效。曲線判讀上有個(gè)經(jīng)驗(yàn)訓(xùn)練到第一次衰減之前如果分類項(xiàng)和定位項(xiàng)的 loss 比例始終在 1:10 以上說明權(quán)重系數(shù)沒調(diào)對(duì)大概率是定位項(xiàng)權(quán)重給高了網(wǎng)絡(luò)把全部容量花在對(duì)齊框上分類頭學(xué)不動(dòng)。反過來如果置信度項(xiàng)在訓(xùn)練早期就迅速降到接近 0 并且不再變化說明負(fù)樣本權(quán)重壓得太狠網(wǎng)絡(luò)學(xué)會(huì)了全部猜沒有目標(biāo)這個(gè)捷徑。4. 數(shù)據(jù)集工程10000 張車尾圖與 22174 個(gè)標(biāo)注框是怎么攢出來的模型結(jié)構(gòu)再合理數(shù)據(jù)不過關(guān)一切都是空轉(zhuǎn)。這套識(shí)別系統(tǒng)最終能跑通一半功勞在標(biāo)注這一側(cè)。4.1 抓拍鏈路4K、紅外補(bǔ)光與邊緣側(cè)算力采集端是卡口上的抓拍單元輸出 4096×2160 的背面圖像配紅外閃光燈做弱光補(bǔ)光。這兩個(gè)參數(shù)合起來決定了整個(gè)系統(tǒng)的上限分辨率越高小目標(biāo)的原始像素越多紅外補(bǔ)光保證夜間也能拿到結(jié)構(gòu)清晰的反光標(biāo)志牌紋理而不是一團(tuán)噪點(diǎn)。抓拍單元和識(shí)別算力是分開的。抓拍端負(fù)責(zé)觸發(fā)、曝光、壓縮、上傳識(shí)別跑在邊緣側(cè)的嵌入式計(jì)算設(shè)備上結(jié)果和部分原始圖通過專有網(wǎng)絡(luò)回傳到監(jiān)測(cè)中心。這個(gè)拆分很重要抓拍是硬觸發(fā)不能等識(shí)別結(jié)果識(shí)別是軟實(shí)時(shí)允許排隊(duì)但隊(duì)列深度必須可控。把兩者耦合在一臺(tái)設(shè)備上一旦識(shí)別模型跑飛抓拍也跟著丟幀整個(gè)卡口就癱了。從工程角度看抓拍端的參數(shù)比模型參數(shù)更值得磨。曝光時(shí)間太長(zhǎng)八十公里時(shí)速的車尾會(huì)拖影標(biāo)志牌邊緣糊成一片曝光太短夜間信噪比不夠標(biāo)志牌的顏色信息直接丟失——而危險(xiǎn)貨物標(biāo)志牌的底色紅、橙、黃、藍(lán)、白本身就是最強(qiáng)的分類線索之一。紅外補(bǔ)光解決的是照度問題解決不了運(yùn)動(dòng)模糊這兩件事得分開調(diào)。4.2 23 類標(biāo)注體系與長(zhǎng)尾分布數(shù)據(jù)集是 10000 張背面圖標(biāo)注了 23 個(gè)類別、22174 個(gè)框。這 23 類拆成兩組5 種危險(xiǎn)化學(xué)品屬性分類共 8419 個(gè)框18 種危險(xiǎn)化學(xué)品分類共 13755 個(gè)框。兩組加起來正好是 22174。這個(gè)拆分方式反映了業(yè)務(wù)上的兩層需求。粗粒度那一層5 類屬性對(duì)應(yīng)的是應(yīng)急處置場(chǎng)景——消防或者路政到場(chǎng)后最先要知道的是這東西是易燃、易爆、有毒、腐蝕還是放射這決定了穿什么防護(hù)、用什么滅火劑、警戒半徑多大。細(xì)粒度那一層18 類對(duì)應(yīng)的是監(jiān)管和溯源需要精確到具體危險(xiǎn)性分類。兩套標(biāo)簽共用一個(gè)檢測(cè)頭等于讓網(wǎng)絡(luò)同時(shí)學(xué)粗分和細(xì)分細(xì)分學(xué)好了粗分自然就有了反過來不成立。平均每張圖 2.2 個(gè)框這個(gè)數(shù)字說明多數(shù)?;奋囕v尾部不止一塊標(biāo)志牌。實(shí)際場(chǎng)景里運(yùn)輸多類危險(xiǎn)品的車輛會(huì)掛主標(biāo)志加副標(biāo)志或者主標(biāo)志加一塊文字說明牌。這對(duì)檢測(cè)器提出了一個(gè)具體要求同一位置附近必須能同時(shí)輸出多個(gè)重疊框NMS 閾值不能太激進(jìn)。長(zhǎng)尾問題在這組數(shù)據(jù)里非常明顯。18 類細(xì)分里常見的易燃液體、易燃固體樣本量可能上千而某些放射性或者感染性物質(zhì)的樣本可能只有個(gè)位數(shù)——現(xiàn)實(shí)中這類運(yùn)輸本來就少。直接訓(xùn)練的結(jié)果是分類頭嚴(yán)重偏向高頻類稀有類的召回接近零但整體 mAP 看著還行因?yàn)楦哳l類把平均值拉上去了。這是最危險(xiǎn)的一種看起來沒問題。處理長(zhǎng)尾有幾種常見思路按類別頻率做損失加權(quán)、對(duì)稀有類做過采樣、把稀有類合并到粗粒度層級(jí)。第三種在業(yè)務(wù)上往往最實(shí)用——如果某類物質(zhì)一個(gè)月才出現(xiàn)一次識(shí)別成它的父類屬性對(duì)應(yīng)急處置來說已經(jīng)夠用硬要區(qū)分到子類反而增加了誤報(bào)。4.3 XML 標(biāo)注文件的結(jié)構(gòu)與一致性校驗(yàn)標(biāo)注用 labelImg 完成每張圖生成一個(gè)同名 xml里面記錄每個(gè)框的位置和類別。格式是 PASCAL VOC 那套size節(jié)點(diǎn)存原圖寬高object節(jié)點(diǎn)下是name和bndboxxmin、ymin、xmax、ymax。import xml.etree.ElementTree as ET from collections import Counter def parse_voc(xml_path): 解析單張標(biāo)注返回歸一化后的框和類別 root ET.parse(xml_path).getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) boxes [] for obj in root.findall(object): name obj.find(name).text.strip() bb obj.find(bndbox) # VOC 坐標(biāo)是 1-based 且是浮點(diǎn)字符串必須轉(zhuǎn) int 再減 1 x1 int(float(bb.find(xmin).text)) - 1 y1 int(float(bb.find(ymin).text)) - 1 x2 int(float(bb.find(xmax).text)) - 1 y2 int(float(bb.find(ymax).text)) - 1 # 越界裁剪防止歸一化后出現(xiàn)負(fù)數(shù)或大于 1 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w - 1, x2), min(h - 1, y2) if x2 x1 or y2 y1: continue # 退化框直接丟棄不要讓它進(jìn)訓(xùn)練集 boxes.append((name, (x1 x2) / 2 / w, (y1 y2) / 2 / h, (x2 - x1) / w, (y2 - y1) / h)) return boxes, w, h # 全量掃描統(tǒng)計(jì)類別分布和異???stat Counter() for p in xml_files: boxes, _, _ parse_voc(p) for name, *_ in boxes: stat[name] 1 print(stat.most_common())這段解析代碼里int(float(...))這個(gè)寫法不是啰嗦。標(biāo)注工具導(dǎo)出的坐標(biāo)有時(shí)帶小數(shù)點(diǎn)直接int()會(huì)在 Python 里報(bào)錯(cuò)減去 1 是因?yàn)?VOC 用 1-based 索引不減的話每個(gè)框整體偏移一個(gè)像素在 4K 圖上無所謂但在縮放后的小圖上會(huì)累積成可見誤差。退化框過濾同樣必要標(biāo)注時(shí)手抖畫出一個(gè)寬 0 高 0 的框歸一化后寬高都是 0訓(xùn)練時(shí)log(0)直接產(chǎn)生 NaN而且這種 NaN 會(huì)污染整個(gè) batch 的梯度。掃描完類別分布之后還有兩件事要做。一是檢查同一張圖里框的重疊情況發(fā)現(xiàn)大量 IOU 大于 0.9 的重復(fù)標(biāo)注說明標(biāo)注規(guī)范沒統(tǒng)一有人重復(fù)畫了同一個(gè)牌子。二是檢查尺寸分布把框面積畫成直方圖如果絕大部分框都落在極小的一側(cè)說明輸入分辨率設(shè)小了得往上調(diào)。5. 避坑與排查五條從訓(xùn)練日志和誤檢框里挖出來的經(jīng)驗(yàn)5.1 召回死活上不去但分類置信度都很高現(xiàn)象驗(yàn)證集上檢出的框分類都很準(zhǔn)但漏檢率明顯偏高尤其是遠(yuǎn)處車輛上的小標(biāo)志牌。原因訓(xùn)練和推理的輸入尺寸在縮放時(shí)把標(biāo)志牌壓得太小了。一張 4096 寬的圖縮到 416縮放比接近 10:1原本 80 像素的標(biāo)志牌只剩 8 像素而最細(xì)那層特征圖的 stride 是 8等于一個(gè)目標(biāo)只占一個(gè)格子。網(wǎng)絡(luò)再?gòu)?qiáng)也沒法從一個(gè)格子里分辨形狀和配色。解決優(yōu)先提高輸入分辨率而不是加深網(wǎng)絡(luò)。把輸入從 416 提到 608 或 832小目標(biāo)的像素占比能翻倍如果顯存或延遲不允許改用切片推理——把 4K 圖切成四塊分別檢測(cè)再合并代價(jià)是后處理要多做一次跨塊 NMS。另一個(gè)成本更低的辦法是只對(duì)車輛區(qū)域做二次裁切放大因?yàn)榭趫?chǎng)景里車輛位置基本可預(yù)測(cè)。5.2 同一塊區(qū)域的兩個(gè)標(biāo)志牌被合并成一個(gè)框現(xiàn)象車尾同時(shí)貼主標(biāo)志和副標(biāo)志時(shí)只檢出一個(gè)框另一個(gè)消失而且框的寬高明顯異常。原因NMS 閾值設(shè)得太高比如 0.45 以上兩個(gè)空間上相鄰、類別相同或者不同的標(biāo)志牌互相壓制。危化品車輛車尾標(biāo)志牌排布密集重疊度天然就高。解決把 NMS 閾值降到 0.3 到 0.4 之間并且改成按類別分組做 NMS不同類別的框不互相抑制。如果業(yè)務(wù)上允許還可以加一條規(guī)則同一車輛區(qū)域內(nèi)置信度最高的框如果寬高比接近 1菱形主標(biāo)志強(qiáng)制保留一個(gè)相鄰的副標(biāo)志框。5.3 稀有類完全檢不出來但訓(xùn)練 loss 一直在降現(xiàn)象訓(xùn)練日志里總 loss 穩(wěn)定下降驗(yàn)證集整體 mAP 看著還行但把類別拆開看18 類里有一半的 AP 接近 0。原因類別極度不平衡高頻類的樣本量是稀有類的上百倍分類頭的梯度被高頻類主導(dǎo)???loss 下降主要來自高頻類和置信度項(xiàng)稀有類的貢獻(xiàn)被淹沒了。解決訓(xùn)練時(shí)按類別頻率做損失加權(quán)權(quán)重取頻率倒數(shù)的平方根比較穩(wěn)或者對(duì)稀有類做數(shù)據(jù)增強(qiáng)式的過采樣。更務(wù)實(shí)的做法是把稀有類合并到粗粒度的屬性層業(yè)務(wù)上夠用指標(biāo)也好看。評(píng)估時(shí)必須逐類看 AP只看總 mAP 會(huì)掩蓋這個(gè)問題。5.4 訓(xùn)練到幾十輪突然出現(xiàn) NaN現(xiàn)象loss 曲線前幾十輪正常下降某一輪突然變成 NaN之后再也不恢復(fù)。原因標(biāo)注里有退化框或者越界框歸一化后寬高出現(xiàn) 0 或負(fù)數(shù)寬高項(xiàng)做了log運(yùn)算直接炸掉。另一種可能是學(xué)習(xí)率在衰減點(diǎn)之前太大某一批梯度的范數(shù)爆掉。解決訓(xùn)練前跑一遍全量標(biāo)注校驗(yàn)把寬高小于 2 像素的框、越界框、重復(fù)框清掉。訓(xùn)練時(shí)加梯度裁剪把范數(shù)上限定在 10 左右。另外學(xué)習(xí)率衰減點(diǎn)不要設(shè)得太晚在 loss 已經(jīng)開始震蕩的時(shí)候還不降很容易翻車。5.5 白天效果好傍晚和雨天大幅退化現(xiàn)象晴天白天的樣本召回和精度都不錯(cuò)換成逆光、雨天、夜間指標(biāo)掉得很明顯而且錯(cuò)的模式很一致——漏檢多于誤檢。原因訓(xùn)練集里的光照和天氣分布不均衡絕大多數(shù)樣本來自正常光照。標(biāo)志牌表面是反光材質(zhì)逆光時(shí)整塊高光過曝顏色特征完全丟失網(wǎng)絡(luò)只能靠形狀猜小目標(biāo)的形狀信息本來就少。解決按光照和天氣做分層采樣保證每個(gè)子集在訓(xùn)練集里都有足夠占比。數(shù)據(jù)增強(qiáng)上不要只做隨機(jī)亮度調(diào)整那模擬不出高光過曝的效果更有效的是隨機(jī)遮擋加局部對(duì)比度擾動(dòng)。評(píng)估也得分層做把白天的指標(biāo)和夜間的指標(biāo)分開報(bào)混在一起算平均值沒有意義。6. 設(shè)計(jì)取舍為什么夠快比最準(zhǔn)更適合這條路把整套方案攤開看最值得琢磨的不是 YOLOv3 的結(jié)構(gòu)而是它在這個(gè)場(chǎng)景里被迫做出的三個(gè)讓步。第一個(gè)讓步是精度上限。單階段檢測(cè)器在極小目標(biāo)上的定位精度天然不如兩階段這是結(jié)構(gòu)性差異不是調(diào)參能抹平的。接受這個(gè)讓步的前提是業(yè)務(wù)對(duì)框得多準(zhǔn)要求不高——應(yīng)急處置要知道的是這車裝了第幾類危險(xiǎn)品框稍微偏一點(diǎn)不影響判斷。如果換成需要精確測(cè)量標(biāo)志牌尺寸的任務(wù)這個(gè)方案就不合適了。第二個(gè)讓步是分辨率。4K 原圖縮到幾百像素的輸入信息損失是實(shí)打?qū)嵉摹L娲桨甘乔衅评砘蛘哕囕v區(qū)域二次裁切但兩者都會(huì)增加延遲和后處理復(fù)雜度。選縮圖本質(zhì)上是選了一個(gè)確定的延遲預(yù)算把不確定性留給精度。第三個(gè)讓步是類別粒度。18 類細(xì)分里有相當(dāng)一部分類別的樣本量撐不起一個(gè)可靠的分類頭把它們合并到 5 類屬性層是向數(shù)據(jù)現(xiàn)實(shí)低頭。這個(gè)讓步在工程上完全合理但必須寫進(jìn)文檔否則下游系統(tǒng)會(huì)以為拿到的是全粒度結(jié)果。這三個(gè)讓步有一個(gè)共同點(diǎn)都是在數(shù)據(jù)不夠、算力有限、時(shí)間很緊的約束下做出的而不是技術(shù)偏好。判斷一個(gè)檢測(cè)方案好不好不看它用了多新的結(jié)構(gòu)看它的每一個(gè)取舍能不能對(duì)應(yīng)到一個(gè)真實(shí)的約束條件上。對(duì)不上就是過度設(shè)計(jì)或者設(shè)計(jì)不足。注意這套方案在原研究里報(bào)告的是在不同照度和天氣下都有較好魯棒性但沒有給出分層量化的指標(biāo)。復(fù)現(xiàn)時(shí)不能把這句話當(dāng)成驗(yàn)收標(biāo)準(zhǔn)必須自己按光照、天氣、距離分桶測(cè)一遍。驗(yàn)證這套系統(tǒng)是否真的能落地我一般會(huì)走三步先用固定的一批歷史卡口圖跑離線評(píng)估逐類看 AP 和漏檢率再把模型放到邊緣設(shè)備上測(cè)端到端延遲注意是端到端——包含解碼、NMS、上傳不是只測(cè)網(wǎng)絡(luò)前向最后拿真實(shí)的實(shí)時(shí)流跑一周看隊(duì)列深度和丟幀率。前兩步能過、第三步過不了的方案我見過太多次。在那之后我每接一個(gè)類似的小目標(biāo)實(shí)時(shí)檢測(cè)任務(wù)都會(huì)先算一筆賬目標(biāo)在原圖里有多少像素、縮到網(wǎng)絡(luò)輸入后還剩多少、最細(xì)那層特征圖的 stride 是多少。這三個(gè)數(shù)一算完方案能不能成基本就有數(shù)了剩下的都是工程細(xì)節(jié)。希望這套思路能幫到你。本文還有配套的精品資源點(diǎn)擊獲取