頭行為檢測實戰(zhàn):數(shù)據(jù)標注與訓(xùn)練全流程)
簡介面向?qū)W生課堂行為分析的目標檢測數(shù)據(jù)集由約2,400張已標注圖像構(gòu)成包含低頭、轉(zhuǎn)頭兩個類別采用YOLO標注格式便于直接接入YOLOv5等主流檢測框架適用于課堂紀律監(jiān)測、學生注意力評估、智慧教室系統(tǒng)開發(fā)等應(yīng)用場景。數(shù)據(jù)已完成訓(xùn)練集與驗證集劃分并附帶show.py可視化腳本可一鍵查看標注框與原始圖便于核對標簽質(zhì)量、定位錯標漏標降低數(shù)據(jù)檢查成本。壓縮包共2000個文件其中1999個為txt格式的標簽文件另含1個Python繪圖腳本整體體積298.38MB文件名與圖像一一對應(yīng)目錄簡潔易于按序加載和批量處理。已有129人學習/下載。作者還不斷更新YOLOv5改進實戰(zhàn)博客覆蓋結(jié)構(gòu)優(yōu)化、注意力機制嵌入與訓(xùn)練策略調(diào)參讀者可依托這套數(shù)據(jù)完成從標準檢測訓(xùn)練到精度提升的完整鏈路。1. 學生上課低頭、轉(zhuǎn)頭行為檢測從約2,400張YOLO標注數(shù)據(jù)切進來“學生上課低頭、轉(zhuǎn)頭行為檢測”這個標題本質(zhì)是一個面向課堂場景的圖像目標檢測加行為狀態(tài)判斷任務(wù)輸入教室攝像頭畫面輸出每個學生的頭部框和標簽比如 head_down低頭、head_turn轉(zhuǎn)頭。約2,400張、YOLO格式的已標注數(shù)據(jù)是把這類目標檢測項目拉下神壇的關(guān)鍵——你不需要自己從零標幾萬張也能把行為檢測閉環(huán)跑通。它適合正在做教育數(shù)字化、課堂專注度分析的一線工程師也適合想用真實業(yè)務(wù)數(shù)據(jù)入門目標檢測與數(shù)據(jù)標注流程的新手。難點不在于YOLO訓(xùn)練本身而在于“低頭”和“轉(zhuǎn)頭”的邊界定義、標注一致性以及小數(shù)據(jù)集的過擬合控制。2. 讀懂YOLO標注并整理數(shù)據(jù)目錄、label文件、class配置拿到約2,400張已標注圖片時最容易犯的錯誤是直接扔進YOLO訓(xùn)練命令完事。但YOLO格式的坑恰恰在“txt文件和圖片要配套”這六個字上目錄錯位、類別ID和names不匹配、坐標沒歸一化任何一個都會讓訓(xùn)練結(jié)果變成廢鐵。我拿到數(shù)據(jù)的第一件事永遠是先驗證標注而不是開訓(xùn)練。以下是我每次接手行為檢測數(shù)據(jù)的固定整理流程。2.1 五列數(shù)字的含義與格式檢查腳本YOLO格式的label是跟圖片同名的txt文件每一行五個數(shù)值類別ID、歸一化后的中心點x、中心點y、框?qū)?、框高。第一列決定這個框?qū)儆凇暗皖^”還是“轉(zhuǎn)頭”后四列全部是相對圖像寬高的比例區(qū)間理論上在0到1之間。最容易被忽略的一點這里存的是中心點坐標不是左上角坐標也不是像素坐標很多人第一次畫框就錯在這里。from pathlib import Path import numpy as np def check_yolo_labels(label_dir: Path, img_w: int 1920, img_h: int 1080): bad_lines [] for txt in label_dir.glob(*.txt): for line in txt.read_text().splitlines(): line line.strip() if not line: continue # 空行直接跳過不報錯 parts line.split() if len(parts) ! 5: bad_lines.append((txt.name, line, 字段數(shù)量不是5)) continue cls int(parts[0]) x_c, y_c, w, h map(float, parts[1:]) if x_c 0 or y_c 0 or w 0 or h 0: bad_lines.append((txt.name, line, 包含非正數(shù))) if x_c 1 or y_c 1 or w 1 or h 1: bad_lines.append((txt.name, line, 坐標超出歸一化范圍)) if bad_lines: print(f[FAIL] {len(bad_lines)} 條異常標注) for item in bad_lines[:10]: print(item) else: print(f[OK] {len(list(label_dir.glob(*.txt)))} 個txt均通過基礎(chǔ)檢查)這段檢查腳本的邏輯很簡單遍歷目錄下所有txt逐行判斷“是不是五個字段”“坐標是不是在0到1之間”。img_w和img_h參數(shù)在這里沒有直接參與運算但我建議保留因為下一步把歸一化坐標還原成像素畫框時會用到。這個腳本只能查出格式錯誤查不出“框沒貼住頭”這種標注質(zhì)量問題所以還要做可視化抽查。import cv2 from pathlib import Path def draw_boxes(img_path: Path, txt_path: Path, out_path: Path): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls, x_c, y_c, bw, bh parts x_c, y_c, bw, bh float(x_c) * w, float(y_c) * h, float(bw) * w, float(bh) * h x1, y1 int(x_c - bw / 2), int(y_c - bh / 2) x2, y2 int(x_c bw / 2), int(y_c bh / 2) color (0, 255, 0) if cls 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.imwrite(str(out_path), img)注意這里從歸一化坐標還原回像素時用的是“中心點減半寬、中心點減半高”的方式直接把YOLO坐標當左上角是最常見的低級錯誤。我一般會從train和val里各抽10到20張圖畫完框之后拼成一張網(wǎng)格圖快速掃一眼重點看兩類框的顏色是否和預(yù)期一致、頭部是否被完整包住。這一步能省掉后面大量無效訓(xùn)練。2.2 用固定隨機種子劃分train/val并生成classroom.yaml約2,400張是小數(shù)據(jù)val比例我習慣取15%到20%也就是360到480張。劃分策略上有個比比例更重要的點如果數(shù)據(jù)是按視頻幀連續(xù)截取的隨機按單張圖片劃分會造成同一學生的相鄰幀同時出現(xiàn)在訓(xùn)練和驗證里val指標虛高部署時原形畢露。更穩(wěn)的做法是先按視頻片段分組再按組劃分同時固定隨機種子保證可復(fù)現(xiàn)。import random from pathlib import Path import shutil random.seed(42) img_root Path(images) label_root Path(labels) # 收集圖片按拍攝片段分組例如 g001_0001.jpg 歸到 g001 組 groups {} for img in img_root.glob(*.jpg): prefix img.name.split(_)[0] groups.setdefault(prefix, []).append(img.stem) group_names list(groups.keys()) random.shuffle(group_names) val_count max(1, round(len(group_names) * 0.2)) val_groups set(group_names[:val_count]) for img in img_root.glob(*.jpg): stem img.stem prefix stem.split(_)[0] sub val if prefix in val_groups else train img_dst img_root / sub / img.name txt_dst label_root / sub / (stem .txt) shutil.copy(img, img_dst) shutil.copy(label_root / (stem .txt), txt_dst)這個腳本的關(guān)鍵在于“先按片段分組再切分”不是簡單調(diào)一個random_split完事。隨機種子固定為42后續(xù)換模型、調(diào)參數(shù)時驗證集保持不變指標才有可比性。如果數(shù)據(jù)文件名里沒有片段前綴至少也要按拍攝時間分成幾大塊再切否則val意義不大。劃分完的目錄結(jié)構(gòu)必須長成下面這樣這是YOLO訓(xùn)練器的硬性預(yù)期images/train/*.jpg images/val/*.jpg labels/train/*.txt labels/val/*.txt對應(yīng)寫入classroom.yamlpath: . train: images/train val: images/val names: 0: head_down 1: head_turn類別順序在這里不是小事。names里的0/1必須和標注txt第一列完全一致如果數(shù)據(jù)里head_down是0head_turn是1yaml也必須是這個順序?qū)懛戳司褪墙?jīng)典的自殺式訓(xùn)練。另外我要提醒一點很多行為檢測原始數(shù)據(jù)只標了兩個行為類漏掉了“正常聽講”這個背景類。如果訓(xùn)練時所有頭部都必須被分到低頭或轉(zhuǎn)頭其中一個類模型會把不確定樣本硬塞進去后期行為判定會很難受。正式項目里我通常會在標注階段補一個head_normal類哪怕只補一部分效果也比裸兩類好。2.3 與標注工具對接時的三類坑數(shù)據(jù)標注工具這塊開源路線里LabelStudio、LabelImg、CVAT都很常見。我習慣用LabelStudio做復(fù)核它能直接導(dǎo)出YOLO格式省去轉(zhuǎn)換。但用到實際數(shù)據(jù)上有三個坑基本每次都會碰到。第一LabelStudio導(dǎo)出類別順序可能按字母排序比如head_down和head_turn導(dǎo)出后ID順序和你建標簽時的順序不一致。解決辦法是導(dǎo)出后立刻跑2.1的檢查腳本并統(tǒng)計txt第一列出現(xiàn)過的類別ID最大值和yaml里的names數(shù)量對一遍。第二用預(yù)標注模型輔助標注時很多人會把置信度低于0.4的預(yù)測框直接丟進label集合作廢。在約2,400張數(shù)據(jù)里混進三四十個臟框就足夠把“轉(zhuǎn)頭”類帶偏。所以輔助標注后必須做人工抽檢而不是信任模型的黑盒子輸出。第三不要拿其他領(lǐng)域的標注習慣硬套。三維目標檢測、遙感圖像目標檢測用的是三維框或旋轉(zhuǎn)框跟這種課堂水平框行為數(shù)據(jù)完全不是一套東西。這個標題對應(yīng)的就是最簡單的水平框加互斥狀態(tài)老老實實按這個格式來別自己發(fā)明新標注。3. 用YOLOv8跑通訓(xùn)練與驗證命令、參數(shù)、產(chǎn)物YOLO系列迭代到現(xiàn)在v5、v8、v11的生態(tài)都很成熟訓(xùn)練體驗差別不大。我習慣用Ultralytics的YOLOv8一條命令把訓(xùn)練、驗證、導(dǎo)出全串起來對約2,400張數(shù)據(jù)來說少折騰環(huán)境比糾結(jié)選哪個版本更重要。這一章按“先冒煙、再訓(xùn)練、后驗證導(dǎo)出”的順序走。3.1 環(huán)境準備與標注冒煙測試環(huán)境配置不是這個項目最大的成本最大的成本是數(shù)據(jù)一致性。所以裝完ultralytics之后我不會直接開訓(xùn)練而是先跑一次推理冒煙測試確認整條鏈路能通。python -m venv .venv source .venv/bin/activate pip install ultralytics yolo predict modelyolov8n.pt sourceimages/val/img0001.jpg conf0.5這條命令會聯(lián)網(wǎng)下載YOLOv8n預(yù)訓(xùn)練模型權(quán)重如果服務(wù)器禁止外網(wǎng)下載就把權(quán)重文件手動放到項目根目錄yolo命令會自動識別。跑出來的檢測框是COCO的80類不是我們的head_down和head_turn這沒關(guān)系只要輸出有框說明讀圖、推理、后處理都正常。真正要在這個階段驗證的是圖片和label的對應(yīng)關(guān)系我用一段獨立小腳本處理。from pathlib import Path img_root Path(images/train) label_root Path(labels/train) missing_txt [p.name for p in img_root.glob(*.jpg) if not (label_root / (p.stem .txt)).exists()] missing_img [p.name for p in label_root.glob(*.txt) if not (img_root / (p.with_suffix(.jpg)).exists()] print(缺txt:, missing_txt[:10]) print(缺jpg:, missing_img[:10]) if not missing_txt and not missing_img: print([OK] 圖片與標注一一對應(yīng))這段腳本邏輯很直白檢查每個jpg有沒有同名txt每個txt有沒有同名jpg。很多數(shù)據(jù)集翻車就翻在某個子目錄少復(fù)制了一批文件訓(xùn)練器不會報錯只是默默訓(xùn)練最后val指標差你都不知道該查誰。冒煙測試通過后再進訓(xùn)練后面每一步出了問題都能快速定位。3.2 訓(xùn)練命令與2400張小數(shù)據(jù)集的參數(shù)選擇訓(xùn)練命令我通常寫成一行帶注釋的風格方便日志里追溯當時用了什么參數(shù)。以下是我在單卡V100或RTX 3080上訓(xùn)練這類數(shù)據(jù)的最小命令yolo detect train \ dataclassroom.yaml \ modelyolov8n.pt \ epochs120 \ imgsz640 \ batch16 \ lr00.01 \ patience30 \ projectruns \ nameclassroom_baseline \ device0參數(shù)說明如下model對約2,400張數(shù)據(jù)yolov8n夠用且跑得快如果顯卡顯存16G以上可以換yolov8s試試但對小數(shù)據(jù)量提升不會太大。我更推薦先拿n跑通再考慮s。imgsz這是最值得花時間調(diào)的參數(shù)。教室攝像頭原圖如果是1920×1080后排學生的頭往往只有三四十像素縮到640后更小。如果驗證階段發(fā)現(xiàn)后排漏檢直接升到800或1024不要動網(wǎng)絡(luò)結(jié)構(gòu)。batchBN層在batch太小時統(tǒng)計不穩(wěn)定我最低接受16。顯存不夠就降imgsz別降batch。epochs2400張、單卡跑100到150輪足夠patience30讓訓(xùn)練在驗證指標不再提升時自動停省算力。lr0遷移學習場景0.01是常規(guī)值。如果日志顯示損失在頭幾十輪就劇烈震蕩先把lr0降到0.001這是最便宜的一步。訓(xùn)練日志里的box_loss、cls_loss、dfl_loss多看看。對行為檢測這個任務(wù)cls_loss最關(guān)鍵如果你發(fā)現(xiàn)cls_loss降得很慢大概率是“低頭”和“轉(zhuǎn)頭”兩類在低置信度區(qū)間嚴重打架不是學習率的問題。loss曲線對新手像個黑匣子但它其實是最早暴露問題的地方下降正常說明數(shù)據(jù)沒問題異常震蕩說明訓(xùn)練配置或標注有問題別急著換網(wǎng)絡(luò)。3.3 驗證、混淆矩陣到ONNX導(dǎo)出訓(xùn)練完成后產(chǎn)物在runs/classroom_baseline/weights/best.pt接下來跑驗證命令yolo detect val \ modelruns/classroom_baseline/weights/best.pt \ dataclassroom.yaml \ batch16驗證結(jié)束后runs/classroom_baseline/下會生成confusion_matrix.png、PR_curve.png、F1_curve.png這些文件。它們不是給匯報用的裝飾圖混淆矩陣看的是兩個行為類之間有沒有系統(tǒng)性誤檢PR曲線決定你部署時置信度閾值卡0.4還是0.6。很多團隊只盯著mAP不打開這些圖真到上線才發(fā)現(xiàn)閾值選錯了。導(dǎo)出ONNX的命令也很短yolo export modelruns/classroom_baseline/weights/best.pt \ formatonnx imgsz640 opset12導(dǎo)出后我建議立刻在本地跑一次onnxruntime推理對比PyTorch模型的輸出而不是等到部署機器上才發(fā)現(xiàn)算子和精度對不上。另外要特別注意ONNX輸出的類別順序如果導(dǎo)出后head_down和head_turn的順序反了業(yè)務(wù)側(cè)畫框顏色就跟著反這種問題在課堂上演示時極其尷尬。4. 學生行為檢測訓(xùn)練避坑5個現(xiàn)象、原因與解決這一章全部是我實際跑行為檢測數(shù)據(jù)時反復(fù)踩過的坑每條按“現(xiàn)象、原因、解決”寫。前三條靠數(shù)據(jù)整理階段解決后兩條靠訓(xùn)練和后處理解決。4.1 損失下降但mAP0類別ID和names順序錯位現(xiàn)象訓(xùn)練時train/box_loss下降得很正常但驗證結(jié)束后mAP50是0PR曲線上一個有效點都沒有。原因最常見的是兩個類的標注txt第一列是0/1但classroom.yaml里names把兩個類順序?qū)懛戳艘灿锌赡苁莢al標簽?zāi)夸浽趧澐謺r沒同步導(dǎo)致驗證集為空訓(xùn)練器不報錯直接輸出空指標。解決先跑2.1的格式檢查腳本再看yaml的names與txt第一列的順序是否一致。然后數(shù)一下images/val與labels/val的圖片數(shù)量是否對得上。還有一個隱藏問題yaml里的path: .在換機器后可能會失效手寫的相對路徑不如改成絕對路徑穩(wěn)。遇到這種玄學mAP問題時把yaml路徑改成絕對路徑能排除一半嫌疑。4.2 低頭和轉(zhuǎn)頭互相誤檢標注邊界沒卡死現(xiàn)象把置信度閾值降到0.3之后兩類框大量疊在同一個人的頭上模型明顯分不清低頭和轉(zhuǎn)頭。原因這是行為檢測數(shù)據(jù)集的頭號問題?!暗皖^”和“轉(zhuǎn)頭”是視覺上的連續(xù)狀態(tài)沒有硬邊界。如果標注規(guī)范里沒寫清楚判定規(guī)則同一個頭在不同幀會一會兒標低頭、一會兒標轉(zhuǎn)頭模型學到的是模糊邊界而不是清晰狀態(tài)。解決我一般會定兩條硬規(guī)則。低頭頭部下傾超過約15度且下巴明顯低于肩線轉(zhuǎn)頭頭部繞垂直軸偏轉(zhuǎn)超過約30度且能看到側(cè)臉或耳廓。第二條規(guī)則是互斥的同一幀同一個人頭只能有一個狀態(tài)兩可的樣本寧可標head_normal也不硬塞。然后把標注結(jié)果隨機抽20人復(fù)核一致性達不到9成就回頭重標。這個功夫花在訓(xùn)練前永遠比訓(xùn)練后調(diào)參劃算。4.3 后排小目標漏檢imgsz和增強沒跟上現(xiàn)象驗證集mAP50看起來還行但細看結(jié)果全是前排大頭部貢獻的后排小頭幾乎沒被召回。原因約2,400張圖里后排頭部像素少縮到640后很多頭只有10×10像素模型根本學不到有效特征。這不是網(wǎng)絡(luò)結(jié)構(gòu)的問題是輸入分辨率和小目標樣本量的問題。解決先把imgsz從640提到800或1024用同一份數(shù)據(jù)跑一次對比觀察小目標召回是否上來。顯存不夠就減少batch或換yolov8n別一開始就上大模型。第二招是增強mosaic和copy-paste能通過拼接生成更多小目標樣本代碼里默認開啟但要確認訓(xùn)練配置里的hsv_h、hsv_s、hsv_v沒有被人為關(guān)掉。第三招是部署時把conf_thres降到0.25提升低置信度召回代價是誤檢變多用下一節(jié)的時序后處理兜住。4.4 訓(xùn)練中l(wèi)ossnan或BN崩潰學習率與batch失配現(xiàn)象訓(xùn)練到第20到40輪終端打印loss: nan或者loss從某一步開始指數(shù)級漲上去GPU顯存顯示還夠但已經(jīng)停不下來。原因?qū)πatch跑YOLOBN層統(tǒng)計不穩(wěn)定學習率過大時梯度先發(fā)散一發(fā)散就拉不回來。batch4或8、配合lr00.1的情況下中早期崩潰概率很高。解決先確認batch不是“自動選擇”的默認值顯存允許時至少batch16再把lr0降到0.001。如果還崩直接把預(yù)訓(xùn)練權(quán)重去掉用modelyolov8n.yaml從頭訓(xùn)練。約2,400張數(shù)據(jù)從頭訓(xùn)練一樣能收斂只是epoch要多給一些。第三個辦法是凍結(jié)backbone前幾層給訓(xùn)練命令加freeze10讓特征提取層先別亂動這招在目標檢測模型微調(diào)崩了時很管用我自己用過的成功率不低于一半。4.5 單幀檢測在視頻里抖動缺時序后處理現(xiàn)象單張圖片測試mAP很好但拿到連續(xù)視頻里一個人頭的標簽在低頭和轉(zhuǎn)頭之間來回跳肉眼看著就是殘影閃爍。原因行為檢測本質(zhì)是序列問題而當前模型只是逐幀目標檢測。兩幀之間頭部狀態(tài)恰好處于過渡帶分類置信度相持不下單幀閾值根本判不出穩(wěn)定狀態(tài)。解決在檢測結(jié)果后面加一個跟蹤器用ByteTrack或DeepSort給每個頭一個穩(wěn)定ID然后對同一個ID在最近5幀內(nèi)做狀態(tài)投票至少3幀是head_down才輸出低頭。這不需要上大模型一個滑動窗口就能消掉大半抖動。我在實際交付時一般把這種時序規(guī)則直接做進服務(wù)端而不是指望單幀網(wǎng)絡(luò)一步到位。提示以上五個避坑點都建立在一個前提上——數(shù)據(jù)標注本身沒有大問題。如果前兩步的格式檢查和可視化抽查沒做再好的訓(xùn)練參數(shù)也是徒勞。5. 進階驗收用低置信度難例和時序窗口把行為檢測做到可用5.1 把“低置信度樣本”撈出來回流標注混淆矩陣能告訴你兩個類有沒有互相誤檢但定位到具體哪張圖、哪個頭還需要逐圖翻。我常用的做法是寫一個難例抽取腳本把所有置信度落在0.3到0.55之間的框撈出來集中人工復(fù)核。這個區(qū)間最需要看低于0.3大概率是誤檢高于0.6說明模型已經(jīng)穩(wěn)了中間地帶才是標注質(zhì)量升級的抓手。from ultralytics import YOLO from pathlib import Path model YOLO(runs/classroom_baseline/weights/best.pt) hard_cases [] for img_p in Path(images/val).glob(*.jpg): res model(img_p, conf0.3, verboseFalse) for box in res[0].boxes: conf float(box.conf) cls int(box.cls) if 0.3 conf 0.55: hard_cases.append((str(img_p), cls, round(conf, 3))) hard_cases.sort(keylambda x: x[2]) for case in hard_cases[:20]: print(case)把腳本輸出結(jié)果攢20到30個樣本回到標注工具里統(tǒng)一復(fù)核能糾正一批邊界不清的低質(zhì)量標注。這一輪回流比堆epochs有效得多。如果數(shù)據(jù)來源分散最好按攝像頭編號或拍攝時段分組抽取避免偏斜。5.2 用跟蹤與滑動窗口輸出穩(wěn)定行為狀態(tài)給每個檢測框配一個跟蹤ID之后狀態(tài)判定可以從單幀分類改成滑動窗口投票。核心代碼很短用一個deque就能實現(xiàn)from collections import deque class HeadState: def __init__(self, window5): self.history deque(maxlenwindow) def update(self, cls_id): self.history.append(cls_id) if not self.history: return -1 avg sum(self.history) / len(self.history) return round(avg)這里cls_id為0或1窗口長度5意味著連續(xù)5幀里至少3幀是同一狀態(tài)輸出才切換。窗口越長越穩(wěn)但延遲也越高我用5剛好覆蓋課堂場景的眨眼級抖動。升級方向是把頭部姿態(tài)角或肩膀位置等幾何特征加進來參與狀態(tài)判斷也可以在后端融合音頻特征那就是多模態(tài)目標檢測的范疇了在數(shù)據(jù)量只有約2,400張的前提下先把時序規(guī)則用起來比試任何花哨模型都劃算。我第一次給學生行為檢測項目調(diào)模型時mAP到了0.83就興沖沖去做演示結(jié)果整段視頻里轉(zhuǎn)頭狀態(tài)幾乎全部漏報。后來把驗證集逐幀翻完才發(fā)現(xiàn)問題出在標注樣例太少、邊界混亂而不是網(wǎng)絡(luò)結(jié)構(gòu)。所以現(xiàn)在每次接手行為檢測數(shù)據(jù)我第一件事是抽20張圖疊框第二件事是統(tǒng)計每個類別的樣本數(shù)第三件事才是訓(xùn)練。希望幫到你。本文還有配套的精品資源點擊獲取