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