練實(shí)戰(zhàn):從數(shù)據(jù)劃分到BN崩潰排查)
簡(jiǎn)介這份資源是面向溺水檢測(cè)場(chǎng)景的YOLO系列目標(biāo)檢測(cè)數(shù)據(jù)集適合計(jì)算機(jī)視覺(jué)初學(xué)者、算法工程師及安全監(jiān)控方向研究者使用可用于訓(xùn)練和驗(yàn)證溺水、出水、游泳等水上行為的識(shí)別模型。壓縮包共1018個(gè)文件包含339張jpg圖像、339個(gè)txt標(biāo)簽、339個(gè)xml標(biāo)簽以及1個(gè)yaml配置文件整體約14.6MB已按訓(xùn)練與驗(yàn)證需求劃分完畢。標(biāo)簽同時(shí)提供YOLO格式與VOC格式兩套YOLO格式以類別索引和歸一化中心點(diǎn)、寬高比例記錄目標(biāo)框VOC格式則以xml結(jié)構(gòu)保存方便直接接入yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等算法。目前已有303人學(xué)習(xí)下載讀者可快速獲得一份開(kāi)箱即用的水上安全檢測(cè)數(shù)據(jù)省去自行標(biāo)注與格式轉(zhuǎn)換的環(huán)節(jié)將精力集中在模型選型、訓(xùn)練調(diào)參與效果對(duì)比上也便于復(fù)現(xiàn)和擴(kuò)展溺水預(yù)警相關(guān)實(shí)驗(yàn)。1. 339 張溺水圖像數(shù)據(jù)集小樣本 YOLO 訓(xùn)練到底能不能落地溺水檢測(cè)這個(gè)方向真正卡住大多數(shù)團(tuán)隊(duì)的從來(lái)不是模型結(jié)構(gòu)而是數(shù)據(jù)。公開(kāi)可用的溺水圖像本身就少標(biāo)注質(zhì)量參差不齊涉及泳池、河道、海邊等不同水域場(chǎng)景時(shí)光照、水面反光、人體姿態(tài)差異極大。我拿到「339 張圖像帶標(biāo)簽」這個(gè)量級(jí)時(shí)第一反應(yīng)是這數(shù)據(jù)量放在常規(guī)目標(biāo)檢測(cè)任務(wù)里連熱身都不夠但放在溺水出水、游泳姿態(tài)識(shí)別這種垂直場(chǎng)景里如果標(biāo)注干凈、類別定義清晰反而有可能跑出一個(gè)能用的基線。這篇文章要解決的問(wèn)題很具體手里有一份 339 張帶標(biāo)簽的溺水/游泳圖像數(shù)據(jù)集想用 YOLO 訓(xùn)練一個(gè)能識(shí)別溺水出水狀態(tài)的檢測(cè)器該怎么配環(huán)境、怎么劃分?jǐn)?shù)據(jù)、參數(shù)怎么設(shè)、訓(xùn)練中 BN 崩潰和混淆矩陣異常怎么排查。適合已經(jīng)跑通過(guò)一次 YOLO 官方 demo、但沒(méi)在小樣本垂直數(shù)據(jù)集上踩過(guò)坑的工程師也適合想評(píng)估這個(gè)方向值不值得投入的技術(shù)負(fù)責(zé)人。我不會(huì)假裝見(jiàn)過(guò)這份數(shù)據(jù)的原始?jí)嚎s包所有步驟都按「339 張帶標(biāo)簽圖像」這個(gè)前提給出可復(fù)現(xiàn)路徑你按自己的實(shí)際目錄結(jié)構(gòu)替換即可。2. 從 339 張圖到可訓(xùn)練數(shù)據(jù)集劃分、增強(qiáng)與標(biāo)簽校驗(yàn)2.1 小樣本下為什么不能隨便按 8:2 切339 張圖像如果按常見(jiàn)的 8:2 劃分驗(yàn)證集只有 67 張左右。問(wèn)題在于溺水場(chǎng)景的圖像往往成組出現(xiàn)——同一段視頻抽幀出來(lái)的連續(xù)畫(huà)面人物姿態(tài)、光照、背景幾乎一致。如果隨機(jī)切分訓(xùn)練集和驗(yàn)證集里會(huì)混入高度相似的幀驗(yàn)證指標(biāo)虛高實(shí)際部署時(shí)換一個(gè)泳池就翻車。這是小樣本目標(biāo)檢測(cè)里最隱蔽的坑之一。我一般會(huì)先做一次「來(lái)源分組」把同一視頻、同一拍攝時(shí)段、同一水域的圖像歸為一組然后按組劃分而不是按單張圖像隨機(jī)劃分。339 張如果來(lái)自 20 到 30 個(gè)來(lái)源組按 7:2:1 分到訓(xùn)練/驗(yàn)證/測(cè)試每組整體進(jìn)同一個(gè)集合。這樣驗(yàn)證集指標(biāo)才有參考價(jià)值。如果數(shù)據(jù)來(lái)源無(wú)法追溯退而求其次的做法是按圖像感知哈希做聚類把相似度高的圖聚成組再切。import os import shutil import random from collections import defaultdict # 假設(shè)文件名前綴代表來(lái)源組例如 pool_A_001.jpg # 實(shí)際使用時(shí)按你的命名規(guī)則替換 group_key 的提取邏輯 def group_key(filename): return filename.split(_)[0] _ filename.split(_)[1] def split_dataset(img_dir, label_dir, out_dir, ratios(0.7, 0.2, 0.1)): groups defaultdict(list) for f in os.listdir(img_dir): if f.lower().endswith((.jpg, .png, .jpeg)): groups[group_key(f)].append(f) group_list list(groups.keys()) random.seed(42) random.shuffle(group_list) n len(group_list) n_train int(n * ratios[0]) n_val int(n * ratios[1]) train_groups group_list[:n_train] val_groups group_list[n_train:n_train n_val] test_groups group_list[n_train n_val:] for split_name, gs in [(train, train_groups), (val, val_groups), (test, test_groups)]: for g in gs: for f in groups[g]: stem os.path.splitext(f)[0] shutil.copy(os.path.join(img_dir, f), os.path.join(out_dir, images, split_name, f)) lbl stem .txt src_lbl os.path.join(label_dir, lbl) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(out_dir, labels, split_name, lbl)) split_dataset(./images, ./labels, ./dataset)這段腳本的核心邏輯是「先分組、再切分」group_key函數(shù)需要你根據(jù)實(shí)際文件名規(guī)則調(diào)整。random.seed(42)保證可復(fù)現(xiàn)ratios參數(shù)在 339 張這種量級(jí)下建議保持 7:2:1不要為了湊驗(yàn)證集數(shù)量改成 6:4那會(huì)進(jìn)一步壓縮本就不多的訓(xùn)練樣本。輸出目錄結(jié)構(gòu)直接對(duì)齊 YOLO 官方要求的images/train、labels/train格式省去后面再轉(zhuǎn)換。2.2 標(biāo)簽格式校驗(yàn)別讓一個(gè)越界框毀掉整個(gè)訓(xùn)練YOLO 的標(biāo)簽格式是class_id x_center y_center width height全部歸一化到 0 到 1。339 張圖像手工標(biāo)注或半自動(dòng)標(biāo)注出現(xiàn)越界坐標(biāo)、寬高為負(fù)、類別號(hào)超出范圍的概率不低。這些臟標(biāo)簽在訓(xùn)練初期不會(huì)報(bào)錯(cuò)但會(huì)導(dǎo)致?lián)p失函數(shù)震蕩、BN 層統(tǒng)計(jì)量異常甚至訓(xùn)練到一半突然崩潰。我習(xí)慣在訓(xùn)練前跑一遍校驗(yàn)?zāi)_本把問(wèn)題標(biāo)簽直接列出來(lái)。import os def validate_labels(label_dir, num_classes): issues [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue path os.path.join(label_dir, f) with open(path) as fh: for i, line in enumerate(fh): parts line.strip().split() if len(parts) ! 5: issues.append((f, i, 字段數(shù)不為5)) continue cls, x, y, w, h parts cls int(cls) x, y, w, h map(float, (x, y, w, h)) if cls 0 or cls num_classes: issues.append((f, i, f類別號(hào)越界: {cls})) if not (0 x 1 and 0 y 1): issues.append((f, i, f中心點(diǎn)越界: {x},{y})) if w 0 or h 0 or w 1 or h 1: issues.append((f, i, f寬高異常: {w},{h})) return issues issues validate_labels(./dataset/labels/train, num_classes2) for it in issues[:20]: print(it) print(f共發(fā)現(xiàn) {len(issues)} 處問(wèn)題)num_classes按你的類別數(shù)填溺水出水、游泳兩類就填 2。校驗(yàn)結(jié)果里如果出現(xiàn)大量寬高異常說(shuō)明標(biāo)注工具導(dǎo)出時(shí)可能用了像素坐標(biāo)沒(méi)歸一化需要回退到標(biāo)注環(huán)節(jié)重新導(dǎo)出。這一步花十分鐘能省掉后面幾小時(shí)的排查。2.3 小樣本增強(qiáng)哪些增強(qiáng)有用哪些是負(fù)優(yōu)化339 張圖像做增強(qiáng)思路和常規(guī)數(shù)據(jù)集不同。常規(guī)做法是 mosaic、mixup、隨機(jī)縮放、色彩抖動(dòng)全開(kāi)但在溺水場(chǎng)景里水面反光和人體姿態(tài)是核心判別特征過(guò)度色彩抖動(dòng)會(huì)讓水面紋理失真mixup 把兩個(gè)溺水目標(biāo)疊在一起會(huì)產(chǎn)生不存在的語(yǔ)義。我的經(jīng)驗(yàn)是幾何增強(qiáng)保留色彩增強(qiáng)減半mixup 直接關(guān)掉。具體配置上YOLOv8 的degrees可以設(shè)到 10 左右translate設(shè) 0.1scale設(shè) 0.5flipud和fliplr各 0.5。hsv_h降到 0.01hsv_s和hsv_v各 0.4 左右。mosaic保持 1.0 但配合close_mosaic在最后 10 個(gè) epoch 關(guān)閉讓模型在訓(xùn)練末期看到真實(shí)分布。這些參數(shù)不是拍腦袋是小樣本垂直場(chǎng)景下反復(fù)試出來(lái)的平衡點(diǎn)。3. YOLO 訓(xùn)練配置從環(huán)境搭建到損失函數(shù)收斂3.1 環(huán)境搭建與預(yù)訓(xùn)練模型選擇環(huán)境這塊CUDA 版本和 PyTorch 版本對(duì)不上是新手最常見(jiàn)的翻車點(diǎn)。我一般用 conda 建獨(dú)立環(huán)境Python 3.10 配 PyTorch 2.xCUDA 11.8 或 12.1 都行關(guān)鍵是torch.cuda.is_available()要返回 True。ultralytics 包直接 pip 裝最新穩(wěn)定版即可不用追 nightly。預(yù)訓(xùn)練模型的選擇直接影響小樣本訓(xùn)練的收斂速度。339 張圖從零訓(xùn)練基本沒(méi)戲必須用 COCO 預(yù)訓(xùn)練權(quán)重。YOLOv8n 或 YOLOv8s 是合理起點(diǎn)n 參數(shù)量小、過(guò)擬合風(fēng)險(xiǎn)低s 精度略高但需要更多數(shù)據(jù)支撐。我的建議是先跑 YOLOv8n 拿到基線如果驗(yàn)證集 mAP 能到 0.5 以上再考慮換 s。更大的 m 或 l 在 339 張圖上幾乎必然過(guò)擬合除非你做大量?jī)鼋Y(jié)層微調(diào)。conda create -n drowning python3.10 -y conda activate drowning pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics yolo checksyolo checks會(huì)輸出環(huán)境自檢結(jié)果重點(diǎn)看 CUDA 是否可用、顯存大小、ultralytics 版本。如果顯存小于 8GB訓(xùn)練時(shí)batch要降到 8 或 16配合ampTrue混合精度。V100 這類 16GB 以上的卡可以上 batch 32但小樣本下 batch 太大反而梯度更新次數(shù)少339 張圖一個(gè) epoch 才十幾次迭代batch 設(shè) 16 比較均衡。3.2 數(shù)據(jù)配置文件與訓(xùn)練命令YOLO 需要一個(gè) YAML 描述數(shù)據(jù)路徑和類別。這個(gè)文件寫(xiě)錯(cuò)路徑是另一個(gè)高頻翻車點(diǎn)path用絕對(duì)路徑最穩(wěn)train和val相對(duì)path寫(xiě)。# drowning.yaml path: /home/user/drowning_dataset train: images/train val: images/val test: images/test names: 0: drowning 1: swimming類別名要和你的標(biāo)注類別號(hào)嚴(yán)格對(duì)應(yīng)。如果標(biāo)注時(shí)溺水是 0、游泳是 1這里就不能反。訓(xùn)練命令如下yolo detect train \ datadrowning.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.001 \ lrf0.01 \ warmup_epochs5 \ patience30 \ close_mosaic10 \ ampTrue \ projectruns/drowning \ nameexp1lr00.001比默認(rèn)的 0.01 低一個(gè)量級(jí)是小樣本微調(diào)的常規(guī)做法避免預(yù)訓(xùn)練權(quán)重被大梯度沖垮。patience30表示 30 個(gè) epoch 驗(yàn)證指標(biāo)不提升就早停339 張圖通常 80 到 120 個(gè) epoch 就收斂了。close_mosaic10在最后 10 個(gè) epoch 關(guān)掉 mosaic讓模型適應(yīng)真實(shí)圖像分布。warmup_epochs5讓學(xué)習(xí)率從極小值線性爬升防止訓(xùn)練初期 BN 統(tǒng)計(jì)量被異常 batch 帶偏。3.3 損失函數(shù)三項(xiàng)到底在看什么YOLO 的損失由三部分組成邊界框回歸損失、分類損失、目標(biāo)置信度損失。小樣本訓(xùn)練時(shí)這三項(xiàng)的收斂曲線能直接反映問(wèn)題。邊界框損失下降慢說(shuō)明標(biāo)注框質(zhì)量差或增強(qiáng)過(guò)度導(dǎo)致目標(biāo)形變分類損失震蕩說(shuō)明類別定義模糊比如溺水出水和游泳的邊界在標(biāo)注時(shí)沒(méi)統(tǒng)一置信度損失居高不下往往是背景樣本太多或前景目標(biāo)太小。我習(xí)慣在訓(xùn)練時(shí)盯著box_loss、cls_loss、dfl_loss三條曲線。正常情況下三者同步下降cls_loss略高于box_loss。如果cls_loss突然飆升先檢查標(biāo)簽里有沒(méi)有類別號(hào)寫(xiě)錯(cuò)。如果box_loss降到很低但 mAP 不漲多半是過(guò)擬合驗(yàn)證集和訓(xùn)練集分布不一致。這些判斷不需要改代碼看 ultralytics 輸出的 results.csv 就行。4. 訓(xùn)練中的 BN 崩潰與混淆矩陣異常排查4.1 BN 崩潰現(xiàn)象、原因與三種解法BN 崩潰的典型現(xiàn)象是訓(xùn)練到某個(gè) epoch 突然報(bào)NaN損失或者驗(yàn)證 mAP 直接掉到 0日志里出現(xiàn)AssertionError: Torch not compiled with CUDA enabled之外的數(shù)值異常。根因通常是某個(gè) batch 的統(tǒng)計(jì)量方差接近零除零后梯度爆炸。小樣本 小 batch 是 BN 崩潰的高發(fā)組合因?yàn)?batch 內(nèi)樣本太少統(tǒng)計(jì)量估計(jì)不穩(wěn)。解法一把batch提到 16 以上讓 BN 有足夠樣本估計(jì)均值和方差。解法二改用SyncBN或凍結(jié) BN 層在 YOLOv8 里可以通過(guò)設(shè)置freeze參數(shù)凍結(jié) backbone 的前若干層。解法三降低學(xué)習(xí)率并加梯度裁剪lr0降到 0.0005同時(shí)在訓(xùn)練配置里開(kāi)clip_grad10.0。我一般先試提 batch不行再凍結(jié) BN最后才動(dòng)學(xué)習(xí)率因?yàn)楦膶W(xué)習(xí)率會(huì)影響整體收斂節(jié)奏。4.2 混淆矩陣總合不唯一是怎么回事有讀者反饋混淆矩陣的行列總和對(duì)不上懷疑代碼有 bug。這其實(shí)不是 bug是 YOLO 驗(yàn)證階段的多重匹配機(jī)制導(dǎo)致的。一個(gè)預(yù)測(cè)框可能同時(shí)匹配到多個(gè)真實(shí)框或者一個(gè)真實(shí)框被多個(gè)預(yù)測(cè)框匹配混淆矩陣在統(tǒng)計(jì)時(shí)按匹配對(duì)累加總合自然不等于圖像總數(shù)。判斷模型好壞要看對(duì)角線占比和各類的召回率不要糾結(jié)總合數(shù)字。如果發(fā)現(xiàn)溺水類被大量預(yù)測(cè)成游泳類先看兩類在標(biāo)注時(shí)的定義是否清晰。溺水出水往往只露出頭部和部分手臂游泳則有完整的身體姿態(tài)如果標(biāo)注時(shí)把「水中掙扎」也標(biāo)成游泳模型學(xué)到的邊界就是模糊的。這種情況回退到標(biāo)注環(huán)節(jié)重新定義類別比調(diào)模型參數(shù)有效得多。4.3 驗(yàn)證指標(biāo)虛高過(guò)擬合的五個(gè)信號(hào)339 張圖訓(xùn)練過(guò)擬合幾乎是必然要面對(duì)的。五個(gè)信號(hào)訓(xùn)練損失持續(xù)下降但驗(yàn)證損失在某個(gè) epoch 后回升訓(xùn)練 mAP 到 0.95 以上而驗(yàn)證 mAP 卡在 0.6驗(yàn)證集預(yù)測(cè)框大量重疊在同一目標(biāo)上模型對(duì)訓(xùn)練集圖像置信度普遍 0.9 以上對(duì)驗(yàn)證集圖像置信度集中在 0.3 到 0.5混淆矩陣?yán)锉尘氨淮罅空`檢為前景。應(yīng)對(duì)手段按優(yōu)先級(jí)先加數(shù)據(jù)增強(qiáng)的多樣性尤其是幾何變換再降模型容量從 s 換回 n然后加權(quán)重衰減weight_decay0.0005最后考慮凍結(jié) backbone 只訓(xùn)練 head。如果這些都不行說(shuō)明 339 張圖對(duì)這個(gè)場(chǎng)景確實(shí)不夠需要考慮半監(jiān)督或主動(dòng)學(xué)習(xí)補(bǔ)充數(shù)據(jù)。5. 小樣本溺水檢測(cè)的進(jìn)階技巧從基線到可用5.1 用測(cè)試時(shí)增強(qiáng)把 mAP 再抬幾個(gè)點(diǎn)訓(xùn)練完之后推理階段還有提升空間。測(cè)試時(shí)增強(qiáng)TTA對(duì)溺水這種目標(biāo)姿態(tài)多變的場(chǎng)景特別有效因?yàn)椴煌叨群头D(zhuǎn)下的預(yù)測(cè)可以互補(bǔ)。ultralytics 里開(kāi) TTA 很簡(jiǎn)單yolo detect val \ modelruns/drowning/exp1/weights/best.pt \ datadrowning.yaml \ augmentTrue \ imgsz640augmentTrue會(huì)同時(shí)做多尺度、翻轉(zhuǎn)的推理再融合。代價(jià)是推理速度降到原來(lái)的三分之一左右如果部署在邊緣設(shè)備上要權(quán)衡。我在 RK3588 這類邊緣板上一般不開(kāi) TTA在服務(wù)端 GPU 推理時(shí)開(kāi)mAP 通常能漲 2 到 4 個(gè)點(diǎn)。5.2 閾值調(diào)優(yōu)溺水檢測(cè)寧可誤報(bào)不可漏報(bào)溺水檢測(cè)的業(yè)務(wù)邏輯和常規(guī)目標(biāo)檢測(cè)不同漏報(bào)的代價(jià)遠(yuǎn)大于誤報(bào)。默認(rèn)置信度閾值 0.25 在這個(gè)場(chǎng)景下偏低會(huì)引入大量水面反光誤檢。我的做法是把置信度閾值提到 0.4 到 0.5同時(shí)把 NMS 的 IoU 閾值從 0.7 降到 0.5減少重疊框。然后在驗(yàn)證集上畫(huà) PR 曲線找到召回率 0.9 對(duì)應(yīng)的精度那個(gè)點(diǎn)才是業(yè)務(wù)可用的工作點(diǎn)。參數(shù)默認(rèn)值溺水場(chǎng)景建議值影響conf0.250.4~0.5提高減少誤報(bào)過(guò)高漏報(bào)增加iou0.70.5降低減少重疊框imgsz640640~960提高小目標(biāo)召回顯存換精度max_det30050溺水場(chǎng)景目標(biāo)少降低提速5.3 我踩過(guò)的最大一個(gè)坑最后說(shuō)一個(gè)血淚教訓(xùn)。我早期做溺水檢測(cè)時(shí)驗(yàn)證集 mAP 跑到 0.78興沖沖部署到泳池?cái)z像頭結(jié)果誤檢率高達(dá)每小時(shí)幾十次全是水面波紋和漂浮物。回頭查才發(fā)現(xiàn)驗(yàn)證集和訓(xùn)練集來(lái)自同一批視頻模型學(xué)到的是「這個(gè)泳池的水面紋理」而不是「溺水的人體姿態(tài)」。后來(lái)我把驗(yàn)證集換成完全沒(méi)見(jiàn)過(guò)的水域場(chǎng)景mAP 掉到 0.52但那個(gè)數(shù)字才是真實(shí)的。這件事之后我養(yǎng)成了一個(gè)習(xí)慣任何垂直場(chǎng)景的小樣本數(shù)據(jù)集先花時(shí)間確認(rèn)驗(yàn)證集的獨(dú)立性再談模型指標(biāo)。339 張圖不多但如果來(lái)源組劃分干凈、增強(qiáng)策略克制、閾值按業(yè)務(wù)調(diào)跑出一個(gè)能用的溺水檢測(cè)基線是完全可行的。值不值得做取決于你能不能持續(xù)補(bǔ)充新場(chǎng)景的數(shù)據(jù)而不是這 339 張本身。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取