:從模型訓(xùn)練到前后端部署全解析)
做一個(gè)能真正落地用的疲勞駕駛識別系統(tǒng)比想象中要折騰得多。這個(gè)項(xiàng)目是我自己從模型選型到前后端部署完整跑了一遍的核心就是用 YOLOv8/v10/v11/v12 這幾個(gè)版本的檢測模型做眼睛、嘴巴、頭部的狀態(tài)識別后端用 SpringBoot 搭一套前后端分離的 Web 服務(wù)前端提供實(shí)時(shí)監(jiān)控和交互界面最后再接一個(gè)大模型接口對連續(xù)幀檢測結(jié)果做更聰明的綜合判斷。整個(gè)過程踩了不少坑也摸出了一些特別值得分享的經(jīng)驗(yàn)。這篇文章就圍繞這個(gè)系統(tǒng)的完整實(shí)現(xiàn)路徑來寫從模型怎么選、數(shù)據(jù)怎么標(biāo)、服務(wù)怎么拆到部署調(diào)試、常見問題排查一次性講清楚適合想自己動手做一遍目標(biāo)檢測Web應(yīng)用的同學(xué)參考。1. 項(xiàng)目背景與整體設(shè)計(jì)思路1.1 為什么疲勞駕駛檢測值得用 YOLO 系列來做疲勞駕駛識別的核心任務(wù)是判斷駕駛員的眼睛是否閉合、嘴巴是否打哈欠、頭部是否頻繁點(diǎn)頭或傾斜。這些行為在視頻里其實(shí)是典型的“小目標(biāo)”和“局部遮擋”問題但好在檢測對象相對固定只需要在駕駛員面部區(qū)域做精細(xì)分析。傳統(tǒng)的圖像處理方法比如人臉關(guān)鍵點(diǎn)EAR計(jì)算也能做但魯棒性不夠好一旦駕駛員戴墨鏡、歪頭、光照變化劇烈關(guān)鍵點(diǎn)就容易飄。換成目標(biāo)檢測模型之后可以直接把“閉眼”“張嘴”等作為一個(gè)類別框出來模型自己學(xué)習(xí)特征視覺干擾的容忍度高很多。YOLO 系列在這里的優(yōu)勢很明確單階段檢測、速度快、工程化成熟。從 v8 到 v12模型結(jié)構(gòu)一直在優(yōu)化但共同點(diǎn)是部署方便從訓(xùn)練到導(dǎo)出 ONNX 再到用 TensorRT 加速鏈路很順暢。對一個(gè)需要實(shí)時(shí)處理攝像頭視頻流的系統(tǒng)來說檢測延遲每毫秒都很寶貴YOLO 在這方面的表現(xiàn)比兩階段模型更適合實(shí)際場景。1.2 前后端分離和 SpringBoot 的定位為什么后端要用 SpringBoot原因是我自己最熟的 Java 技術(shù)棧是一方面另一方面是 SpringBoot 在業(yè)務(wù)接口、權(quán)限控制、定時(shí)任務(wù)、數(shù)據(jù)庫操作這些方面生態(tài)太完善了。疲勞駕駛系統(tǒng)除了要處理模型推理還需要管理用戶、設(shè)備、報(bào)警記錄、歷史查詢這些工程化能力用 SpringBoot 寫起來效率很高。而模型推理部分則是典型的 Python 強(qiáng)項(xiàng)。用 Python 加載 PyTorch 模型、處理圖像、調(diào)用 GPU 加速比在 Java 里折騰 JNI 和 PyTorch 的 Java API 要省事得多。所以最好的架構(gòu)就是把系統(tǒng)拆成“SpringBoot 業(yè)務(wù)服務(wù) Python 推理服務(wù)”兩個(gè)獨(dú)立進(jìn)程中間用 HTTP 通信。前端完全獨(dú)立部署通過 RESTful API 和 WebSocket 跟后端交互這就是典型的前后端分離。這套結(jié)構(gòu)的好處是每一層都能單獨(dú)擴(kuò)容比如攝像頭多了推理服務(wù)可以多起幾個(gè)實(shí)例業(yè)務(wù)層完全不用動。1.3 系統(tǒng)整體架構(gòu)與數(shù)據(jù)流向整體流程可以概括成一句話視頻流進(jìn)前端前端抽幀后發(fā)給后端后端再轉(zhuǎn)發(fā)給推理服務(wù)做檢測檢測結(jié)果回到后端記錄同時(shí)大模型綜合分析結(jié)果給出疲勞等級最終推送到前端大屏展示和報(bào)警。在物理部署上Python 推理服務(wù)和 SpringBoot 服務(wù)可以放在同一臺 GPU 服務(wù)器上也可以分開。前端頁面用 Nginx 托管訪問后端 API。視頻流的話既可以前端直接訪問攝像頭 RTSP 流也可以后端的 WebSocket 主動推送處理后的結(jié)果幀。架構(gòu)里比較關(guān)鍵的是數(shù)據(jù)協(xié)議。檢測結(jié)果要包含目標(biāo)類別閉眼、打哈欠、低頭、正常、邊界框坐標(biāo)、置信度、時(shí)間戳、駕駛員編號。這些信息經(jīng)過后端結(jié)構(gòu)化以后一部分存入數(shù)據(jù)庫一部分推送給前端繪制框另一部分喂給大模型做語義分析。設(shè)計(jì)好這個(gè)數(shù)據(jù)模型后面每一步都會順很多。2. 模型選型與訓(xùn)練要點(diǎn)2.1 YOLOv8/v10/v11/v12 的核心差異對比很多第一次做選型的人會糾結(jié)到底用哪個(gè)版本我的建議是先別盯著“最新”兩個(gè)字要看自己的硬件條件和場景要求。四代模型我都實(shí)際訓(xùn)練過總結(jié)起來各有脾氣版本核心特點(diǎn)推理速度GPU小目標(biāo)效果易用性/生態(tài)YOLOv8anchor-free結(jié)構(gòu)成熟官方支持全流程快中上極高文檔多社區(qū)問題基本都能搜到Y(jié)OLOv10去掉 NMS端到端部署更簡潔更快中等相對較高但需要留意輸出格式變化YOLOv11改進(jìn) Backbone增強(qiáng)特征提取mAP 有提升和 v8 接近更好較高訓(xùn)練接口延續(xù) v8 習(xí)慣YOLOv12加入注意力機(jī)制小目標(biāo)更友好略慢于 v10最好相對較低部分訓(xùn)練細(xì)節(jié)需要自己調(diào)實(shí)際對比下來v8 最適合當(dāng)基線模型因?yàn)橛?xùn)練參數(shù)大家都熟踩坑成本低v10 因?yàn)槿サ袅?NMS確有速度優(yōu)勢但部署時(shí)輸出層變了解析要跟著改v11 在 mAP 上比 v8 有小幅提升算是不錯(cuò)的中期選擇v12 在閉眼這類小目標(biāo)上提升最明顯但 GPU 稍微差一點(diǎn)的話速度下降也明顯。所以我的選型邏輯是如果算力充足、要求精度最高用 v12如果要在老顯卡或者 CPU 上跑用 v8 最穩(wěn)。2.2 疲勞駕駛數(shù)據(jù)集構(gòu)建與標(biāo)注規(guī)范訓(xùn)練模型的根基是數(shù)據(jù)。閉環(huán)的數(shù)據(jù)集至少要有四類正常睜眼、閉眼、打哈欠張嘴、低頭/左顧右盼。有人會想用公開數(shù)據(jù)集但疲勞駕駛場景非常特殊不同攝像頭安裝角度、車內(nèi)光照、駕駛員臉型差異都很大公開數(shù)據(jù)很難完全覆蓋。我自己是采用“公開數(shù)據(jù)集模擬駕駛環(huán)境自采”的方式在模型車和實(shí)驗(yàn)室里架攝像頭找?guī)讉€(gè)人分別模擬正常駕駛和疲勞狀態(tài)錄了上百段視頻再抽幀篩選。標(biāo)注的時(shí)候特別注意三點(diǎn)。第一閉眼和半閉眼要區(qū)分開或者在標(biāo)注時(shí)統(tǒng)一成“閉眼”但邊界要清晰眼睛只剩一條縫時(shí)算閉眼還是睜眼最好在標(biāo)注規(guī)范里明確。第二打哈欠時(shí)嘴巴張開的程度也要定義好張嘴超過一定閾值才算正樣本。第三樣本均衡很關(guān)鍵正常睜眼的幀數(shù)會遠(yuǎn)多于閉眼和打哈欠需要做欠采樣或數(shù)據(jù)增強(qiáng)。我用的是某標(biāo)注工具導(dǎo)出 YOLO 格式的 txt 文件每行是類別 x_center y_center width height。2.3 訓(xùn)練配置與調(diào)參經(jīng)驗(yàn)訓(xùn)練 YOLO 的配置有很多可調(diào)項(xiàng)但核心就幾個(gè)輸入分辨率、batch size、epochs、優(yōu)化器、數(shù)據(jù)增強(qiáng)策略。輸入分辨率我建議用 640 起步如果閉眼檢測精度不夠再嘗試 960。分辨率提高能明顯改善小目標(biāo)檢測但訓(xùn)練和推理速度都會下降。batch size 受顯存限制我實(shí)測 8GB 顯存下用 640 分辨率batch 8 差不多再大就 OOM。epochs 一般 100 到 200配合早停機(jī)制看驗(yàn)證集 mAP 變化。優(yōu)化器直接用默認(rèn)的 SGD 或 AdamW 都行關(guān)鍵是學(xué)習(xí)率策略。數(shù)據(jù)增強(qiáng)里最有用的幾個(gè)是隨機(jī)亮度對比度模擬光線變化、隨機(jī)旋轉(zhuǎn)和透視模擬頭部姿態(tài)變化、隨機(jī)裁剪模擬攝像頭角度差異。但注意不要增強(qiáng)過頭比如翻轉(zhuǎn)就只做水平翻轉(zhuǎn)垂直翻轉(zhuǎn)會讓臉變成倒著的真實(shí)場景不可能出現(xiàn)。訓(xùn)練完成后先用測試集看各類別的 mAP。如果閉眼類別 mAP 明顯低優(yōu)先補(bǔ)閉眼樣本而不是盲目加訓(xùn)練輪數(shù)。我遇到過閉眼和正常睜眼的 mAP 相差十幾個(gè)點(diǎn)補(bǔ)了一批閉眼數(shù)據(jù)后效果立竿見影。2.4 模型導(dǎo)出與轉(zhuǎn)換細(xì)節(jié)訓(xùn)練出的 PyTorch 模型權(quán)重不能直接塞給 Java 或者 Python 推理服務(wù)使用通常要轉(zhuǎn)換成 ONNX 再轉(zhuǎn)成推理引擎格式。如果部署在 GPU 上轉(zhuǎn) TensorRT 是首選如果只有 CPU轉(zhuǎn) OpenVINO 更合適。導(dǎo)出 ONNX 時(shí)要注意幾個(gè)坑。第一YOLO 模型的輸出形狀是[batch, 4 classes, grid_h * grid_w]或類似不同版本結(jié)構(gòu)不同導(dǎo)出后需要寫后處理邏輯。第二opset版本盡量選高一點(diǎn)否則某些算子會丟失。第三輸入動態(tài)維度建議關(guān)閉固定為 640x640能提升 TensorRT 轉(zhuǎn)換成功率代價(jià)是送入模型的圖像必須做 letterbox 縮放并記錄縮放比例。我在導(dǎo)出 v10 的時(shí)候踩過一次坑因?yàn)?v10 去掉了 NMS輸出層不同直接用 v8 的后處理解析會導(dǎo)致結(jié)果全是零。后來查看模型結(jié)構(gòu)文檔才明白需要根據(jù)輸出 shape 判斷特征圖類別數(shù)再寫對應(yīng)的解碼函數(shù)。3. 后端 SpringBoot 服務(wù)與模型推理集成3.1 項(xiàng)目模塊劃分與工程結(jié)構(gòu)整個(gè)后端工程我分成了三個(gè) Maven 模塊server-adminSpringBoot 主服務(wù)負(fù)責(zé)鑒權(quán)、設(shè)備管理、報(bào)警存儲、接口封裝。detect-client封裝對 Python 推理服務(wù)的 HTTP 客戶端提供統(tǒng)一的檢測請求方法。common存放公共實(shí)體、DTO、常量。獨(dú)立推理服務(wù)是一個(gè) FastAPI 應(yīng)用單獨(dú)一個(gè)項(xiàng)目倉庫。用 FastAPI 而不是 Flask是因?yàn)樗蓄愋托r?yàn)、自動文檔、異步支持接口對接起來體驗(yàn)更好。為什么推理服務(wù)一定要獨(dú)立除了技術(shù)棧差異還因?yàn)槟P屯评硎?CPU/GPU 密集型操作如果和 SpringBoot 跑在同一個(gè) JVM 里一旦檢測請求并發(fā)上來JVM 的 GC 和 GPU 爭用會讓系統(tǒng)抖動。分開之后SpringBoot 只做業(yè)務(wù)編排Python 服務(wù)只做推理職責(zé)清晰也方便橫向擴(kuò)容。3.2 推理服務(wù)接口設(shè)計(jì)Python 推理服務(wù)暴露的接口要設(shè)計(jì)得簡單而穩(wěn)定。我常用的是POST /detect Content-Type: multipart/form-data file: 圖像文件 params: - driver_id: 駕駛員編號 返回 { code: 0, data: { boxes: [ {class: 1, class_name: closed_eye, confidence: 0.92, x1: 120, y1: 80, x2: 180, y2: 140} ], image_width: 640, image_height: 640, inference_ms: 28 } }接口內(nèi)部先做圖像解碼再做 letterbox 縮放轉(zhuǎn)為張量推理后解析輸出最后把檢測框坐標(biāo)還原到原始圖像坐標(biāo)系。這里要特別小心坐標(biāo)還原很多人在前端 canvas 上畫框偏移就是因?yàn)橥税纯s放比例換算。除了單幀檢測推理服務(wù)還要支持連續(xù)幀序列檢測或者配合視頻流抽幀。因?yàn)?SpringBoot 每次轉(zhuǎn)發(fā)一張圖的話網(wǎng)絡(luò)開銷比較大。更高效的方式是推理服務(wù)內(nèi)部維護(hù)一個(gè)消息隊(duì)列SpringBoot 只把視頻幀通過 WebSocket 推送到推理服務(wù)由推理服務(wù)批量處理再把結(jié)果批量返回。不過我用的實(shí)現(xiàn)比較簡單HTTP 單次請求也能滿足 10 路攝像頭的需求關(guān)鍵看幀率限制。3.3 SpringBoot 核心業(yè)務(wù)實(shí)現(xiàn)SpringBoot 這邊的接口設(shè)計(jì)主要圍繞這個(gè)幾個(gè)場景用戶登錄和權(quán)限管理JWT 令牌角色包括管理員、監(jiān)控員。設(shè)備管理增刪改查攝像頭每個(gè)攝像頭綁定一個(gè)駕駛員或車輛。檢測請求接收前端上傳的圖像調(diào)用推理服務(wù)拿到結(jié)果后保存到數(shù)據(jù)庫并根據(jù)規(guī)則判斷是否觸發(fā)報(bào)警。報(bào)警記錄查詢按時(shí)間、人員、設(shè)備篩選歷史記錄。WebSocket 推送檢測結(jié)果實(shí)時(shí)推送到前端。調(diào)用 Python 推理服務(wù)的代碼我用的是 Spring 的RestTemplate現(xiàn)在也可以用WebClient但RestTemplate足夠穩(wěn)定。注意超時(shí)時(shí)間一定要設(shè)檢測接口如果 GPU 排隊(duì)可能耗時(shí)超過 5 秒。我設(shè)置的是連接超時(shí)2秒、讀取超時(shí)10秒同時(shí)用線程池隔離避免一個(gè)慢請求拖垮整個(gè) Web 服務(wù)。保存到數(shù)據(jù)庫時(shí)除了保存檢測框還要保存原始圖片的 Base64 編碼或者文件路徑。這個(gè)很重要否則事后復(fù)盤報(bào)警記錄時(shí)沒有圖像證據(jù)。數(shù)據(jù)庫表設(shè)計(jì)至少包括detect_recordid, driver_id, device_id, timestamp, image_path, result_json, fatigue_level, status。3.4 前后端分離鑒權(quán)與會話設(shè)計(jì)前后端分離后傳統(tǒng)的 Cookie Session 不再適用我用 JWT 做無狀態(tài)鑒權(quán)。用戶在登錄接口換取 token后續(xù)請求在 Header 里帶Authorization: Bearer token。SpringBoot 里用一個(gè)攔截器或 Spring Security 過濾器校驗(yàn) token 即可。WebSocket 的鑒權(quán)要單獨(dú)處理。因?yàn)?WebSocket 握手走的是 HTTP 協(xié)議升級可以在握手請求的 header 里帶上 token在后端實(shí)現(xiàn)一個(gè)HandshakeInterceptor校驗(yàn)。我踩過一個(gè)坑前端用原生 WebSocket 時(shí)header 里不能隨意自定義字段所以很多前端會選擇把 token 放到 URL 查詢參數(shù)里雖然不優(yōu)雅但簡單可行。生產(chǎn)環(huán)境建議用 STOMP 框架支持 CONNECT 幀的傳參能正規(guī)一點(diǎn)??缬蚺渲靡惨龊谩G岸隧撁嫒绻渴鹪趆ttp://localhost:8081后端 API 在http://localhost:8080那 SpringBoot 里要配置 CORS 允許指定源、指定請求頭、指定方法否則瀏覽器會攔截所有 AJAX 請求。我習(xí)慣把允許的域名放到配置文件里方便切換環(huán)境。4. Web 交互界面與大模型智能分析集成4.1 前端框架與頁面布局前端我用的技術(shù)棧是 Vue 3 Element Plus ECharts這套組合開發(fā)監(jiān)控類界面效率很高。頁面核心功能包括登錄/權(quán)限控制。實(shí)時(shí)監(jiān)控大屏左側(cè)顯示攝像頭視頻流中間用 canvas 覆蓋顯示檢測框右側(cè)是實(shí)時(shí)統(tǒng)計(jì)圖表疲勞指數(shù)曲線、各類別檢測計(jì)數(shù)。歷史記錄頁面支持按時(shí)間范圍查詢報(bào)警記錄回看抓拍圖片和當(dāng)時(shí)的檢測結(jié)果。設(shè)備管理頁面增刪改查攝像頭信息。大屏的視覺設(shè)計(jì)要點(diǎn)是信息密度高但不能亂。我習(xí)慣把屏幕劃分成三欄中間大畫布放視頻流和檢測框左側(cè)放設(shè)備列表右側(cè)放圖表和實(shí)時(shí)報(bào)警列表。視頻流用 H5 的 video 標(biāo)簽播放 RTSP 流時(shí)瀏覽器原生不支持 RTSP需要先經(jīng)過后端轉(zhuǎn)碼成 WebRTC 或 HLS 流。更輕量的方案是前端通過 WebSocket 接收后端推送的 JPEG 幀流然后利用 canvas 逐幀繪制同時(shí)繪制檢測框。這種方式延遲低實(shí)現(xiàn)也簡單很適合內(nèi)網(wǎng)局域網(wǎng)使用。4.2 實(shí)時(shí)視頻流傳輸與處理這里有個(gè)取舍直接讓前端拉攝像頭 RTSP 流并解碼對瀏覽器來說基本不可能。所以常見做法是后端用 FFmpeg 拉流并轉(zhuǎn)成 MJPEG 或 HLS。我采用了 WebSocket JPEG 幀的推送方式。具體流程是SpringBoot 每 100ms 從攝像頭拉一幀圖像轉(zhuǎn)成長度較小的 JPEG質(zhì)量調(diào)成 70 左右通過 WebSocketBinaryMessage推送給前端。前端收到二進(jìn)制數(shù)據(jù)后用Blob轉(zhuǎn)成圖片再畫到 canvas 上。接下來這一幀也可以發(fā)給推理服務(wù)做檢測檢測框的坐標(biāo)通過另一條 WebSocket 消息推送。前端收到檢測結(jié)果后在 canvas 上重新繪制圖像和框。這個(gè)方式的優(yōu)點(diǎn)是實(shí)時(shí)性高能保持端到端延遲在 300ms 以內(nèi)。缺點(diǎn)是對前端的繪圖性能有一定要求如果一幀圖像是 1280x720每秒20幀客戶端如果性能不好會卡頓。優(yōu)化的辦法是只繪制檢測區(qū)域的縮放圖或者降低到15幀。4.3 大模型智能分析模塊的設(shè)計(jì)與接入檢測模型只能告訴你“這一幀里有哪些目標(biāo)”但疲勞駕駛是一個(gè)時(shí)間維度上的判斷。單幀眼睛閉合可能是因?yàn)檎Q蹘酌雰?nèi)頻繁閉合才更有可能是疲勞。大模型的價(jià)值就在這里把一段時(shí)間內(nèi)的檢測序列拼接成自然語言提示讓大模型做推理和總結(jié)。我接的大模型接口是一個(gè)通用的中文對話 API。接入之前要先把檢測結(jié)果轉(zhuǎn)成文本比如每 10 秒為一個(gè)窗口統(tǒng)計(jì)這個(gè)窗口內(nèi)閉眼幀數(shù)占比、打哈欠次數(shù)、低頭次數(shù)然后構(gòu)造提示詞你是駕駛安全分析助手。以下是一段10秒內(nèi)的檢測統(tǒng)計(jì)數(shù)據(jù) 閉眼幀占比15% 打哈欠次數(shù)2 低頭次數(shù)1 平均置信度0.85 請判斷駕駛員疲勞程度等級正常、輕度疲勞、中度疲勞、高度疲勞并給出理由和安全建議。用JSON輸出。大模型返回的內(nèi)容可以是 JSON 格式方便直接解析。我在提示詞里明確要求“只輸出JSON”并且配置了輸出格式參數(shù)。如果用的是支持結(jié)構(gòu)化輸出的API也可以直接用 schema 約束。接入的關(guān)鍵細(xì)節(jié)是控制成本和延遲。每 10 秒調(diào)用一次大模型對一個(gè)攝像頭來說一天有 8640 次調(diào)用費(fèi)用和響應(yīng)壓力不小。我的優(yōu)化策略是先用規(guī)則判斷是否到了需要大模型介入的程度。比如如果閉眼占比小于 5%直接用規(guī)則結(jié)果返回“正常”不調(diào)用大模型只有當(dāng)閉眼占比超過閾值或者短時(shí)內(nèi)出現(xiàn)多次哈欠才觸發(fā)大模型做深度分析。這樣可以省下大量調(diào)用。另外大模型調(diào)用是 IO 密集型操作不能放在處理視頻流的線程里否則會阻塞實(shí)時(shí)性。我用了一個(gè)異步任務(wù)池處理大模型請求完成后把結(jié)果寫入數(shù)據(jù)庫并通過 WebSocket 推送給前端。4.4 報(bào)警與數(shù)據(jù)可視化報(bào)警規(guī)則最常用的是 PERCLOS單位時(shí)間內(nèi)眼睛閉合時(shí)間占比。行業(yè)里一般以閉眼幀數(shù)占總幀數(shù)的比例來判斷比如連續(xù) 20 秒內(nèi)閉眼比例超過 20%就認(rèn)為是嚴(yán)重疲勞?;跈z測結(jié)果我做了兩層報(bào)警即時(shí)報(bào)警某幀檢測到閉眼且置信度高觸發(fā)提示。周期報(bào)警按時(shí)間窗口聚合模型輸出結(jié)合大模型疲勞等級觸發(fā)不同程度的聲光報(bào)警。前端收到報(bào)警消息后頁面頂部彈出紅色告警條同時(shí)播放聲音。ECharts 用來畫疲勞指數(shù)曲線橫軸是時(shí)間縱軸是每個(gè)時(shí)間窗口的綜合疲勞分曲線平滑處理后會更有趨勢感。安全提醒要注意報(bào)警信息不能只展示給監(jiān)控員還要支持短信或企業(yè)微信消息推送。我的系統(tǒng)預(yù)留了消息推送接口目前用的是一個(gè)模擬的 HTTP 回調(diào)實(shí)際使用時(shí)可以對接第三方短信服務(wù)。5. 部署實(shí)施與性能優(yōu)化5.1 Docker 化部署方案整套系統(tǒng)的部署我完全用 Docker Compose 編排包含五個(gè)容器容器鏡像基礎(chǔ)說明web-servereclipse-temurin:17-jreSpringBoot 服務(wù)端口 8080detect-serverpython:3.10-slimFastAPI 推理服務(wù)掛載模型文件GPU 時(shí)啟用 nvidia runtimeweb-frontendnginx:alpine前端靜態(tài)資源端口 80反向代理后端 APIdatabasemysql:8.0業(yè)務(wù)數(shù)據(jù)存儲redisredis:7-alpine緩存、WebSocket 會話管理、任務(wù)隊(duì)列如果推理服務(wù)需要用 GPUdocker-compose.yml里要指定gpus: all同時(shí)需要機(jī)器上已安裝 NVIDIA Container Toolkit。我第一次部署時(shí)忘了加這個(gè)容器跑起來之后 Python 代碼報(bào)找不到 CUDA排查了半天才發(fā)現(xiàn)是 runtime 沒配置。Dockerfile 寫 Python 推理服務(wù)時(shí)要注意減小體積只拷貝requirements.txt、模型文件和源碼不要拷貝訓(xùn)練用的數(shù)據(jù)集。模型文件我直接打進(jìn)鏡像因?yàn)槟P腕w積不會太大一般在幾十 MB避免啟動時(shí)去外部下載。5.2 性能瓶頸與調(diào)優(yōu)系統(tǒng)上線后最直接的瓶頸往往是推理服務(wù)。檢測一張 640x640 圖像在 GTX 1660 上大約需要 20ms但在 CPU 上可能要 500ms 以上。如果所有攝像頭都要實(shí)時(shí)檢測必須對幀率做限制。我的做法是每個(gè)攝像頭抽幀率限制為 5 FPS也就是每 200ms 處理一幀。對于有 10 個(gè)攝像頭的情況總推理負(fù)載是每秒 50 幀GPU 利用率大約在 60%~80%。這個(gè)抽幀率對疲勞檢測完全夠用閉眼動作持續(xù)至少 300ms5 FPS 能捕獲到關(guān)鍵狀態(tài)。推理服務(wù)內(nèi)部也用上了隊(duì)列。FastAPI 是異步框架但模型推理是 CPU/GPU 同步操作并發(fā)請求會排隊(duì)。我用了asyncio的 Lock 限制同時(shí)只能有一個(gè)推理任務(wù)否則顯存會爆。如果要提升并發(fā)可以起多個(gè)推理進(jìn)程每個(gè)進(jìn)程各自加載模型再用 Redis 做任務(wù)分發(fā)。網(wǎng)絡(luò)傳輸也是隱性瓶頸。前端上傳原始圖像再返回檢測結(jié)果圖像體積會直接影響延遲。我在推理服務(wù)里加了圖像壓縮邏輯先按比例縮小到最長邊 800px再轉(zhuǎn)成 JPEG quality80這樣單張圖不超過 100KB走內(nèi)網(wǎng)非???。5.3 合規(guī)與隱私保護(hù)疲勞駕駛系統(tǒng)會持續(xù)采集駕駛員面部圖像屬于敏感個(gè)人信息落地時(shí)必須做隱私保護(hù)。我的方案是所有圖像數(shù)據(jù)默認(rèn)不保留原始視頻流只保留觸發(fā)報(bào)警前后的關(guān)鍵幀。數(shù)據(jù)庫中的圖像路徑不能包含人員姓名等直接標(biāo)識信息用隨機(jī) UUID 命名。攝像頭采集區(qū)域要避開無關(guān)路人。系統(tǒng)權(quán)限要分級只有授權(quán)管理員才能查看帶有面部的抓拍圖。這些都是經(jīng)過實(shí)際項(xiàng)目驗(yàn)證的底線尤其在企業(yè)內(nèi)部項(xiàng)目里數(shù)據(jù)合規(guī)問題不是技術(shù)問題是能不能上線的問題。即使技術(shù)實(shí)現(xiàn)再完善隱私保護(hù)不到位也會被叫停。6. 踩坑記錄與問題排查6.1 常見問題速查表我把這大半年開發(fā)維護(hù)中遇到的典型問題整理成了一張表遇到類似情況可以先對著查現(xiàn)象可能原因解決方案前端檢測框位置錯(cuò)位letterbox 縮放后沒有還原坐標(biāo)原圖在推理服務(wù)統(tǒng)一負(fù)責(zé)坐標(biāo)還原前端直接用原始坐標(biāo)WebSocket 頻繁斷開代理服務(wù)器對空閑連接超時(shí)前端每 30 秒發(fā)心跳包后端設(shè)置超過 60 秒無消息自動斷開推理服務(wù)顯存泄漏推理輸入張量沒有釋放使用torch.no_grad()并在每次推理后主動清理變量大模型返回內(nèi)容解析失敗提示詞沒有嚴(yán)格要求 JSON 格式增加輸出格式約束并做異常重試與默認(rèn)值回退第一次請求特別慢模型和 CUDA 上下文初始化在服務(wù)啟動時(shí)執(zhí)行一次預(yù)熱推理把 CUDA 模塊加載好攝像頭 RTSP 流斷流網(wǎng)絡(luò)抖動或攝像頭連接數(shù)限制后端增加自動重連機(jī)制啟動守護(hù)線程定期檢查流狀態(tài)數(shù)據(jù)庫記錄增長過快每幀都保存檢測結(jié)果只保存報(bào)警幀和周期統(tǒng)計(jì)結(jié)果單幀正常狀態(tài)不落庫6.2 推理精度與速度權(quán)衡心得我在實(shí)際使用中最大的體會是不要盲目追求模型最強(qiáng)。v12 在閉眼檢測精度上確實(shí)好一些但如果目標(biāo)是做到 5 路以上攝像頭并發(fā)實(shí)時(shí)檢測v8 的推理速度優(yōu)勢更能保證體驗(yàn)。后來我把生產(chǎn)環(huán)境穩(wěn)定在 YOLOv8s 上閉眼檢測的 mAP 達(dá)到了 0.895 FPS 下延遲 30ms 左右完全夠用。v10 和 v12 還是要留在實(shí)驗(yàn)環(huán)境里對比著玩。v10 的無 NMS 設(shè)計(jì)確實(shí)讓推理延后處理簡單了很多但官方權(quán)重在閉眼這個(gè)小類別上表現(xiàn)略遜于 v11。v12 的提升需要更大的訓(xùn)練數(shù)據(jù)量才能發(fā)揮出來如果只是幾百張標(biāo)注圖反而容易過擬合。模型蒸餾也是個(gè)可以考慮的方向。用 v12 作為教師模型把知識蒸餾給 v8s 的學(xué)生模型可以在精度和速度之間找到一個(gè)更好的平衡點(diǎn)。我做過一次蒸餾實(shí)驗(yàn)v8s 的 mAP 提升了大約 3%基本接近 v11 的效果但速度沒有犧牲太多。這種方案適合有一定調(diào)優(yōu)時(shí)間的團(tuán)隊(duì)。6.3 實(shí)用的調(diào)試技巧調(diào)試這類多服務(wù)系統(tǒng)最怕的就是問題出現(xiàn)后難以定位。我養(yǎng)成了一套自己的調(diào)試習(xí)慣分享出來很有用。日志必須全鏈路帶 traceId。在 SpringBoot 里用攔截器生成一個(gè) traceId放入 MDC然后在調(diào)用 Python 推理服務(wù)時(shí)把這個(gè) traceId 放在 HTTP Header 里傳給 FastAPIFastAPI 的日志同樣打印這個(gè) traceId。這樣不管是前端報(bào)錯(cuò)還是后端報(bào)錯(cuò)只要拿著 traceId 就能把整個(gè)請求鏈路串起來看。離線回放功能一定要做。我寫了一個(gè)模擬推送腳本可以從本地視頻文件讀取幀按照時(shí)間間隔把幀發(fā)送給 SpringBoot 服務(wù)。這樣不用天天坐在車?yán)餃y試只要拿著錄制好的疲勞駕駛視頻就能反復(fù)驗(yàn)證檢測邏輯和報(bào)警規(guī)則。壓測別忽略大模型環(huán)節(jié)。很多人壓測時(shí)只關(guān)注檢測接口忽略了前端不斷調(diào)用大模型分析接口的并發(fā)。我用 JMeter 模擬多個(gè)設(shè)備同時(shí)觸發(fā)疲勞分析結(jié)果發(fā)現(xiàn)線程池滿了導(dǎo)致接口超時(shí)。后來把線程池參數(shù)調(diào)大并且增加了一個(gè)排隊(duì)緩沖問題就解決了。最后再分享一點(diǎn)個(gè)人體會這套系統(tǒng)做完之后我最意外的不是模型精度而是工程整合要花的時(shí)間遠(yuǎn)超訓(xùn)練模型。模型訓(xùn)練可能只占三成工作量剩下七成都在做前后端聯(lián)調(diào)、接口協(xié)議對齊、異常處理、部署上線。如果你也想從零做一套類似的項(xiàng)目建議先定好接口協(xié)議和數(shù)據(jù)結(jié)構(gòu)再動手實(shí)現(xiàn)各個(gè)模塊。還有一個(gè)深坑是不同 YOLO 版本的輸出格式看起來差不多實(shí)際上差別不小換模型版本時(shí)后處理代碼一定要寫單元測試否則上線那天絕對會踩雷。希望這篇文章能幫你繞開我走過的彎路。