據(jù)訓練與部署實戰(zhàn))
簡介面向課堂教學行為分析場景一套已標注的學生低頭、轉頭目標檢測數(shù)據(jù)集可直接用于YOLO系列模型的訓練與評估。數(shù)據(jù)共約2400張圖像標注類別為低頭、轉頭兩類并已劃分好訓練集與驗證集降低預處理成本適合教育場景智能化開發(fā)者、算法入門者以及希望改進YOLO檢測效果的工程師。包體包含2000個文件以1999個txt格式的標注文件為主每個txt對應一張圖像的類別與邊界框坐標信息另有1個python可視化腳本可快速預覽標注效果整體壓縮包約298.38MB。該資源已有130人學習下載借助標注數(shù)據(jù)和驗證劃分可快速訓練學生行為檢測模型并配合作者提供的YOLOv5改進實戰(zhàn)專欄及主頁相關項目便于進一步做模型優(yōu)化、消融實驗和場景適配擴展。1. 學生低頭、轉頭行為檢測2400張YOLO標注數(shù)據(jù)到底能做什么學生在課堂上的低頭與轉頭是最容易被攝像頭捕獲、也最能反映課堂參與度的兩類行為信號。所謂學生上課低頭、轉頭行為檢測就是通過圖像目標檢測模型在教室畫面中實時框出頭部區(qū)域并打上標簽再由上層邏輯統(tǒng)計成低頭時長、轉頭頻次這些可量化指標。這里的2400張已標注圖像是整個方案的地基它決定了一個小團隊能不能在不對數(shù)據(jù)做大規(guī)模補標的情況下用YOLO系列模型快速訓練出可用的行為識別器。適合兩類人往下讀一類是做課堂分析產(chǎn)品、要把行為指標落到實處的算法工程師另一類是手握教室攝像頭視頻、正在糾結要不要自己標數(shù)據(jù)的人。2. 讀懂2400張數(shù)據(jù)的標注結構YOLO txt每行數(shù)字的含義與類別定義拿到這類數(shù)據(jù)集第一件要做的事不是急著訓練而是把標注文件逐行讀明白。YOLO標注格式下每張圖像對應一個同名txt文件放在labels目錄里txt里每一行代表一個目標實例。格式看起來簡單但坐標做了歸一化直接決定了后續(xù)data.yaml怎么寫、可視化腳本怎么寫以及訓練報錯時該往哪排查。2.1 標注格式逐字段拆解歸一化坐標讓數(shù)據(jù)與分辨率解耦打開一個txt你會看到類似0 0.5123 0.3845 0.0831 0.1582這樣的行。五個數(shù)字的含義分別是類別ID、目標框中心點的x坐標、中心點的y坐標、框的寬度、框的高度其中坐標全部是0到1的歸一化值不是像素絕對值。所謂歸一化是指這些數(shù)字都是相對圖像寬高的比例和圖像原始分辨率沒有關系。這個設計帶來的直接好處是同一份標注可以喂給任意輸入分辨率。訓練時用640后續(xù)想用960或1280做推理不需要重新標注縮放圖像時坐標會跟著等比換算。我在拿到一批新標注時會先寫一個小腳本把所有txt掃一遍確認格式和類別分布沒有明顯異常import os label_dir labels/train class_count {} total_boxes 0 bad_files [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname), r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: bad_files.append((fname, line)) continue cls int(parts[0]) class_count[cls] class_count.get(cls, 0) 1 total_boxes 1 print(各類別目標數(shù):, class_count) print(總目標框數(shù):, total_boxes) print(格式異常文件:, len(bad_files))邏輯說明先用len(parts) ! 5過濾掉字段數(shù)不對的行再按類別ID統(tǒng)計目標數(shù)量。這一步能同時暴露兩類問題——臟數(shù)據(jù)和類別分布嚴重失衡。如果兩個類別的目標數(shù)相差三五倍以上訓練時模型會偏向多數(shù)類后續(xù)要考慮調loss權重或補標。參數(shù)說明cls就是類別ID必須和data.yaml里names的順序嚴格對應。如果這批數(shù)據(jù)約定0是低頭、1是轉頭那么names第一項必須是head_down、第二項必須是head_turn。順序寫反模型訓練出來就是兩個類別互相調換的廢品而且指標還看不出任何異常。2.2 目錄組織與訓練/驗證劃分小數(shù)據(jù)集的切分腳本和泄漏坑數(shù)據(jù)集的目錄組織常見做法是images和labels分開放各自再按train和val劃分。2400張屬于中小偏小的規(guī)模驗證集切多了訓練樣本不足切少了驗證指標波動大。我一般按9:1左右劃分訓練集約2160張、驗證集約240張。如果你拿到的原始數(shù)據(jù)已經(jīng)是整理好的標準目錄跳過切分腳本直接用如果是散裝文件下面這個腳本可以快速完成劃分import os import random from shutil import move random.seed(42) os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) images sorted(os.listdir(images/all)) random.shuffle(images) split_idx int(len(images) * 0.9) for i, fname in enumerate(images): part train if i split_idx else val base os.path.splitext(fname)[0] move(fimages/all/{fname}, fimages/{part}/{fname}) move(flabels/all/{base}.txt, flabels/{part}/{base}.txt)邏輯說明先把所有圖像路徑讀進來按9:1切分后圖像和對應的txt一起移動。random.seed(42)保證每次運行切分結果一致方便復現(xiàn)。參數(shù)說明這個隨機切分有個隱蔽的坑——如果原始數(shù)據(jù)是同一段視頻里連續(xù)抽幀來的相鄰幀內容幾乎一樣隨機切分會導致訓練集和驗證集存在大量重復畫面驗證指標虛高。遇到這種情況要按視頻片段切分而不是按單幀切分比如把前70%的視頻幀都歸train、后30%歸val再補一點不同教室的數(shù)據(jù)做驗證。切分完成后data.yaml的寫法如下path: ./dataset train: images/train val: images/val names: 0: head_down 1: head_turn注意YOLOv8會自動從names的長度推斷nc不寫也能跑。但如果你的ultralytics版本偏舊建議補上nc: 2不報錯也不影響結果。2.3 標注質量靜態(tài)核查先用可視化腳本抽檢再決定要不要補標標注質量決定了訓練上限。2400張圖在訓練前值得先做一輪人工抽檢否則后期返工成本更高。我通常每50張抽1張把圖像和對應txt框疊加畫出來用OpenCV直接可視化import cv2 img_path images/train/IMG_0001.jpg label_path labels/train/IMG_0001.txt img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts list(map(float, line.strip().split())) cls, cx, cy, bw, bh parts x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) color (0, 0, 255) if int(cls) 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(check.jpg, img)邏輯說明把歸一化坐標還原成像素坐標并繪制檢測框。人工看畫框結果重點檢查三件事框是否貼合頭部、有沒有大面積漏標、類別ID是否符合直覺。如果抽檢100張里有超過5張明顯有問題寧可把問題樣本挑出來修也不要直接訓練。參數(shù)說明還原坐標最容易出錯的地方是x1和y1要用中心點減半寬/半高而不是直接用cx乘w。cls只決定畫框顏色紅色是0類、綠色是1類這跟模型無關純可視化習慣。3. 用YOLOv8把行為檢測跑起來環(huán)境安裝、訓練命令與五個必調參數(shù)數(shù)據(jù)就位后訓練本身不復雜。Ultralytics YOLOv8是目前跑通這類小數(shù)據(jù)集最快的路徑安裝、寫配置、執(zhí)行訓練三步就能看到第一條loss曲線。選它不是因為一定比YOLOv5指標高而是生態(tài)太順報錯信息直觀對2400張規(guī)模的課堂場景特別友好。3.1 用conda搭好環(huán)境Python版本、依賴與首次推理驗證常見做法是用miniconda建一個獨立環(huán)境避免污染系統(tǒng)Python。在普通PC、工作站或AGX Orin這類邊緣設備上都能跑但Python版本不要低于3.9torch和ultralytics的依賴在3.8以下經(jīng)常出現(xiàn)兼容性問題conda create -n yolo_class python3.10 -y conda activate yolo_class pip install ultralytics邏輯說明conda環(huán)境解決的是多項目依賴隔離。之前踩過的坑是系統(tǒng)Python里已經(jīng)裝了舊版numpy和opencv直接pip裝ultralytics會把整個環(huán)境的依賴版本攪亂。獨立環(huán)境建好后所有yolo相關包裝在新環(huán)境里互不影響。參數(shù)說明python3.10是目前ultralytics全功能支持比較穩(wěn)的版本。ARM架構平臺比如AGX Orin上pip會自動拉取對應平臺的torch版本不需要手動處理CUDA。環(huán)境裝好后最快驗證方式是拿一張課堂圖片跑一次推理from ultralytics import YOLO model YOLO(yolov8n.pt) results model(test.jpg) print(len(results[0].boxes))邏輯說明用官方預訓練權重跑一次推理沒報錯并輸出了檢測框數(shù)量說明依賴鏈是完整的。這一步把環(huán)境問題和數(shù)據(jù)問題分開后面訓練報錯時能更快定位方向。3.2 寫data.yaml與訓練命令行imgsz、batch、epochs、patience、mosaic環(huán)境就緒后直接用ultralytics的CLI訓練。它會自動讀取data.yaml下載基礎權重然后開始訓練循環(huán)yolo detect train \ modelyolov8n.pt \ datadataset/data.yaml \ imgsz640 \ epochs100 \ batch16 \ patience20 \ mosaic0.5 \ projectruns/train \ nameclass_behavior邏輯說明model指定預訓練權重yolov8n是從COCO上訓好的基礎版本用它做起點比隨機初始化收斂快得多。data指向data.yamlultralytics會根據(jù)path字段拼接圖像和標簽的相對路徑。參數(shù)說明imgsz是訓練輸入分辨率640對課堂場景是保守選擇。如果攝像頭是1080P或更高后排學生的頭部在畫面里往往只有30到50像素這時把imgsz提到960甚至1280小目標召回率會有明顯提升代價是顯存占用和單epoch時間上漲約30%到50%。batch是單批次圖像數(shù)16在12G顯存下跑n模型是安全的8G顯存就降到8或4收斂會慢一點但不是不能訓。epochs100對2400張的小數(shù)據(jù)集是合理設置但要不要訓滿100輪主要看patience什么時候觸發(fā)。patience20的意思是驗證集指標連續(xù)20輪沒有提升就提前結束。mosaic是默認開啟的數(shù)據(jù)增強把四張圖拼成一張訓練。小數(shù)據(jù)集上容易讓模型學到過于激進的拼接特征我一般會降到0.5。如果訓練中發(fā)現(xiàn)驗證指標抖動得很厲害把這個參數(shù)再往下降或者干脆關掉。提示如果你用的是8G顯存的老卡batch降到8同時把數(shù)據(jù)加載的workers設成2避免磁盤I/O拖慢訓練。3.3 訓練日志與損失曲線box_loss、cls_loss和dfl_loss怎么看訓練時終端會滾動輸出每一輪的GPU內存、損失值和mAP。真正要盯的是驗證集上的mAP50和mAP50-95以及三條loss的走勢。YOLOv8的損失函數(shù)是三項加權組合box_loss負責回歸檢測框的位置和大小誤差cls_loss負責類別判斷誤差dfl_loss負責邊界框的分布建模讓模型在框邊緣附近也能輸出較平滑的坐標估計??辞€時注意三點只要mAP50-95在漲單輪抖動不用管cls_loss下降慢是正常的因為低頭和轉頭兩個類別本身非常相似特征區(qū)別只在頭部角度和視線方向如果訓練后期val loss開始反彈而訓練loss還在降說明過擬合已經(jīng)開始了這時候提前停止或降低增強強度比硬加數(shù)據(jù)更有效。訓練結束后runs/train/class_behavior/weights/下會生成best.pt和last.pt。best.pt是驗證集指標最好的那一輪權重后面做推理和部署都該拿它。4. 行為檢測訓練避坑四條從實踐中踩出來的經(jīng)驗這章寫的是我在課堂行為檢測上遇到的最典型的坑。每條都按現(xiàn)象、原因、解決三步走。能看到現(xiàn)象描述是因為自己也翻過車這些經(jīng)驗比任何調參技巧都值錢。4.1 后排小目標全漏檢mAP不低真實場景卻框不住人現(xiàn)象訓練完驗證集mAP50達到0.85看起來效果不錯。但拿另一間教室的實拍視頻測試時后排學生幾乎全部漏檢檢測框密集地落在前三排。原因訓練數(shù)據(jù)里后排樣本本身就少或者后排人頭在640分辨率下只有約25到35像素。模型沒見過足夠多的小尺度正樣本自然學不到對應特征。驗證集指標好看是因為驗證集跟訓練集同源小目標比例一樣稀缺mAP被前排樣本撐高了。解決優(yōu)先把imgsz提到960或1280重訓一輪做對比。其次用切片推理工具比如SAHI這種思路把原圖切成重疊塊分別檢測再合并。還有一種做法是統(tǒng)計訓練集中小目標占比如果面積小于32×32像素的框占比不足5%人工補標100到200張后排圖像這往往比調整網(wǎng)絡結構更直接。4.2 低頭和轉頭互相混淆邊界樣本決定了類別邊界現(xiàn)象訓練日志里的混淆矩陣顯示head_down和head_turn之間有10%到15%的互相誤判。實際觀察視頻發(fā)現(xiàn)模型把低頭看手機的側臉頻繁標成轉頭。原因低頭和轉頭在視覺上存在大量過渡姿態(tài)。比如頭低下去但眼睛看向側邊或者轉頭時只有頸部偏轉、身體沒動標注員面對這類邊界樣本時往往憑感覺打標導致同一個姿態(tài)在不同圖像里可能被標成兩個不同類別。模型學到的類別邊界是標注員主觀邊界的平均系統(tǒng)性偏差就這么來的。解決先統(tǒng)計全量標注里兩個類別的框數(shù)如果數(shù)量差距過大說明標注口徑已失衡。再抽看被標為不同類別但視覺特征接近的樣本統(tǒng)一口徑。更穩(wěn)妥的做法是把分類損失的權重調高一點比如把默認的cls系數(shù)翻倍讓模型更重視區(qū)分這兩個容易混淆的類別但根本上還是要統(tǒng)一標注標準。4.3 loss曲線先降后升2400張小數(shù)據(jù)集的過擬合信號現(xiàn)象訓練到第30輪左右訓練集的box_loss持續(xù)走低驗證集的box_loss開始反彈但mAP50還在緩慢上升。到第50輪驗證loss已經(jīng)比第20輪高出不少。原因2400張圖像對YOLO來說不算多。模型早期學到的是通用特征后期開始記住訓練圖像里的背景和噪聲模式比如特定教室的課桌椅顏色、窗戶光線。驗證集和訓練集如果來自同一間教室這種記憶行為不會立刻讓mAP崩盤但換到新教室就現(xiàn)原形。解決不要盲目訓滿100個epoch用patience提前結束是最簡單的后悔藥。如果確認過擬合我把mosaic降到0甚至關掉同時適當提高顏色增強參數(shù)讓模型沒法穩(wěn)定記住背景色。如果條件允許能湊到4000到5000張并覆蓋多間不同教室過擬合問題會自然緩解。4.4 訓練報錯loss為nan問題絕大多數(shù)出在標注文件里現(xiàn)象訓練跑到某個batchloss輸出nan后面所有指標全是nan訓練直接報廢。原因最常見的情況是標注文件里有寬度或高度為0的框或者坐標值大于1。這些通常是導出程序向上取整出錯或者手動編輯txt時多打了一個小數(shù)點。如果txt里有空行或者只有類別ID沒有坐標的行也會讓損失計算遇到非法值。解決訓練前用腳本把整個labels目錄掃一遍定位到非法樣本所在文件和行號import os for fname in os.listdir(labels/train): path os.path.join(labels/train, fname) with open(path) as f: for line_idx, line in enumerate(f.readlines()): parts line.strip().split() if len(parts) ! 5: print(f{fname}:{line_idx} 字段數(shù)異常) continue cls, cx, cy, bw, bh map(float, parts) if bw 0 or bh 0: print(f{fname}:{line_idx} 寬高異常 bw{bw} bh{bh}) if not (0 cx 1 and 0 cy 1): print(f{fname}:{line_idx} 中心坐標越界)邏輯說明腳本在訓練前跑一遍把所有非法框全部暴露出來。定位到具體文件和行號后刪除該樣本或修正坐標即可。參數(shù)說明坐標越界的判定條件0 cx 1和0 cy 1是YOLO格式的硬性規(guī)范。如果發(fā)現(xiàn)少量越界是把0.9寫成了9.9這類筆誤直接修正坐標如果是寬高為0多半是標注時框選操作失誤那張圖建議整個樣本重標。5. 從檢測框到課堂指標模型規(guī)模取舍、推理腳本與統(tǒng)計邏輯訓練出best.pt只是第一步。課堂行為檢測真正落地的難點在于攝像頭輸出的是連續(xù)視頻流檢測框本身不能直接作為行為指標中間還有模型選型、閾值設定、結果聚合這三層邏輯。5.1 模型規(guī)模怎么選n、s還是m以及邊緣部署的導出路徑先看硬指標YOLOv8n參數(shù)量約300萬s約1100萬m約2500萬。對課堂行為檢測這類只有兩個類別、目標尺度相對集中的任務n在常見GPU上推理一張640分辨率圖片只需幾毫秒在邊緣設備上也能跑到實時。s精度提升有限但顯存和延遲翻倍。我的經(jīng)驗是如果攝像頭分辨率是1080P且學生數(shù)在30人以內用n做初版完全夠用如果要覆蓋大教室且后排占畫面比例小就上s并配imgsz1280訓練。m和更大版本在2400張的小數(shù)據(jù)集上收益不明顯反而過擬合得更快。模型參數(shù)量640分辨率單幀延遲參考適用場景YOLOv8n約3M低小教室、實時互動提醒YOLOv8s約11M中大教室、1280推理分辨率YOLOv8m約25M較高不推薦小數(shù)據(jù)容易過擬合之后如果要把模型部署到AGX Orin這類邊緣設備常見做法是先用yolo export modelbest.pt formatonnx導出ONNX再轉成TensorRT engine格式推理延遲會比直接跑PyTorch低一截。5.2 推理腳本與閾值過濾把best.pt變成標簽流訓練收斂后用best.pt做推理只需要幾行代碼。這里要做兩個工程化處理一是用confidence閾值過濾低質量框二是把類別ID映射為可讀標簽from ultralytics import YOLO model YOLO(runs/train/class_behavior/weights/best.pt) results model.predict( sourceclassroom.mp4, imgsz960, conf0.35, iou0.5, verboseFalse ) for frame_idx, r in enumerate(results): for box in r.boxes: cls int(box.cls[0]) conf float(box.conf[0]) label head_down if cls 0 else head_turn print(f幀 {frame_idx}: {label} 置信度 {conf:.2f})邏輯說明model.predict直接接受視頻路徑并逐幀推理r.boxes里包含當前幀的所有檢測框??虻腸ls給出類別索引conf給出置信度。映射成標簽后就能進入后續(xù)的行為統(tǒng)計邏輯。參數(shù)說明conf0.35是經(jīng)驗值。課堂場景誤檢主要來自投影屏幕上的文字和后排雜物誤檢多就上調到0.45如果出現(xiàn)大量漏檢說明閾值偏高。iou0.5是NMS閾值決定兩個重疊框是否合并。課堂場景一個人只應該有一個框保持默認的0.5足夠。imgsz在推理時可以比訓練時更高比如訓練用640、推理用960能緩解小目標漏檢但幀率會下降實時性要求高的場景要權衡。5.3 從單幀到統(tǒng)計檢測框如何變成低頭時長和轉頭頻次單幀檢測結果沒有時間維度沒法回答這節(jié)課學生的狀態(tài)怎么樣這類問題。我常用的做法是在推理循環(huán)外面加一個基于時間窗口的統(tǒng)計層按窗口聚合輸出from collections import defaultdict window_stats defaultdict(lambda: {down_frames: 0, turn_frames: 0, total_frames: 0}) def update_window(frame_idx, label, window_id0): stats window_stats[window_id] stats[total_frames] 1 if label head_down: stats[down_frames] 1 elif label head_turn: stats[turn_frames] 1 if stats[total_frames] 300: down_ratio stats[down_frames] / stats[total_frames] turn_ratio stats[turn_frames] / stats[total_frames] print(f窗口 {window_id}: 低頭占比 {down_ratio:.2f}, 轉頭占比 {turn_ratio:.2f}) window_stats[window_id] {down_frames: 0, turn_frames: 0, total_frames: 0}邏輯說明用固定幀數(shù)作為聚合窗口輸出窗口內低頭幀占比和轉頭幀占比。這種統(tǒng)計方式能平滑逐幀的檢測抖動。如果要做單人行為分析還需要用IoU匹配或ByteTrack這類跟蹤器把每一幀的框關聯(lián)到具體座位或個人這是從畫面統(tǒng)計升級到個人分析的關鍵一步。參數(shù)說明total_frames 300 是按25fps幀率算出的12秒窗口具體數(shù)值要按你的實際幀率調整。想要更快反饋就把窗口縮到150幀想要更穩(wěn)的復盤數(shù)據(jù)就用600幀。6. 進階用法檢測加時序平滑把行為判斷做得更貼近課堂單幀模型能輸出檢測框但老師真正關心的是這個學生低頭了多久和轉頭是否頻繁。落到真實課堂里單幀檢測結果直接被當結論用會顯得很不穩(wěn)定因為模型偶爾會把低頭看書的瞬間誤判成轉頭也會因為遮擋漏掉一幀。需要一個輕量的時序記憶系統(tǒng)來兜底。一個簡單實用的方案是用滑動窗口隊列平滑幀級決策。維護一個長度為30的標簽隊列只有當?shù)皖^標簽在隊列里占比超過六成時才判定為一次低頭事件開始占比低到兩成以下時判定事件結束。比起對單幀做閾值判斷誤報會少非常多from collections import deque label_history deque(maxlen30) def smooth_decision(label): label_history.append(label) if label_history.count(head_down) 18: return head_down if label_history.count(head_turn) 18: return head_turn return unknown這個做法的實質是給檢測結果加了一道低通濾波不改模型結構把訓練好的best.pt當黑匣子用就能明顯提升后端指標的穩(wěn)定性。隊列長度和閾值按幀率調整30幀對應25fps下約1.2秒這個粒度剛好能過濾短促誤檢又不會吞掉持續(xù)1秒以上的真實行為。另一個進階方向是引入座位區(qū)域劃分。如果攝像頭位置固定先在畫面里人工圈出每個座位區(qū)域再把檢測框中心點與區(qū)域做包含關系匹配。只要某個座位區(qū)域內低頭框占比持續(xù)超過閾值就記錄為該座位的一次低頭事件。這個思路比起整幀統(tǒng)計更能回答誰在低頭、低了多少次這類班主任真正關心的問題。我自己的教訓是第一次做課堂行為統(tǒng)計時把全部精力花在了模型精度上mAP已經(jīng)很高但后端反饋還是不準。后來才意識到老師看到的不是單幀框而是某個人在某個時間段是否頻繁低頭。這個語義比單幀檢測框粗放但恰好需要時序聚合才能實現(xiàn)。模型指標是很好的起點但不是終點。如果你手頭也有一套2400張左右的已標注數(shù)據(jù)先把第2章的數(shù)據(jù)清洗腳本跑一遍再按第3章的命令訓練一輪最后把時序平滑接上基本就能得到一套可用的課堂行為檢測原型。希望幫到你。本文還有配套的精品資源點擊獲取