實踐)
簡介面向畢業(yè)設計與課程作業(yè)場景的錯題自動切分系統(tǒng)完整實現(xiàn)基于DyHead與YOLOv11雙模型架構(gòu)前者負責試卷題目區(qū)域精準分割后者識別錯號、斜線、半對、問號、圓圈五類錯誤標記。系統(tǒng)內(nèi)置四層匹配策略中心點包含、重疊面積、IOU、最近距離可將錯誤符號與對應題目自動關聯(lián)同時支持訓練、驗證、推理全流程為計算機視覺、教育信息化方向的學生提供可直接復用的基線方案。資料共984個文件以Python腳本、YAML配置、Markdown文檔為主涵蓋模型訓練、推理、評估及項目說明等模塊壓縮包總大小51.75MB目錄結(jié)構(gòu)便于按需查閱。目前已有44人瀏覽學習。從內(nèi)容預覽看包含torchvision相關C/CUDA算子源碼與Shell輔助腳本便于深入調(diào)試與二次開發(fā)適合需要快速搭建錯題識別系統(tǒng)或開展YOLO系列算法實驗的讀者。1. 這套錯題切分系統(tǒng)憑什么比普通YOLO更值得做做過錯題本數(shù)字化的人都有同感拍一張滿是紅筆批改、貼紙、水印和歪斜題號的卷面想讓模型只把“題目區(qū)域”切出來遠沒有想象中順利。普通YOLO在均勻分布的目標上很強但錯題頁恰恰是“密集、長寬比極端、目標邊界模糊”的典型場景——同一道題可能跨欄出現(xiàn)題號被批改筆跡蓋住半邊相鄰題目之間幾乎沒有留白。這時候用固定檢測頭去回歸候選框經(jīng)常把兩題切在一起甚至把批注區(qū)域當成題目。這個項目的核心思路是用DyHead把多尺度特征融合的靈活性引入YOLOv11的檢測頭再配合一套按列排序與重疊合并的后處理規(guī)則把“題區(qū)域檢測”變成“可落地的切分管線”。這套思路適合正在做試卷識別、錯題采集、OCR預處理方向的人也適合想在自己數(shù)據(jù)集上把YOLOv11檢測精度再往上推一把的工程師。2. 先交代DyHead為什么能補上YOLOv11在這類場景的短板2.1 錯題頁檢測的難點到底在哪錯題頁和常規(guī)目標檢測數(shù)據(jù)集最大的差別是目標沒有“標準姿態(tài)”。常規(guī)檢測里人、車、貓都有自己的典型外觀和比例。錯題區(qū)域的邊界是由排版、字號、題號、留白共同決定的。同一個頁面里有的題只有一行有的題占據(jù)三分之一頁還有的題被兩道批注夾在中間。YOLOv11的C3k2骨干網(wǎng)絡和SPPF結(jié)構(gòu)能提取多尺度特征但檢測頭對每個尺度特征是“平等看待”的。小目標那頭關注紋理細節(jié)大目標那頭關注語義信息錯題區(qū)域需要的是“動態(tài)地決定該聽誰的”——而這就是DyHead設計的出發(fā)點。DyHead的完整名字是Dynamic Head它把尺度感知、空間感知、任務感知三個注意力機制串聯(lián)在檢測頭里。尺度感知用類似SE的注意力機制去加權(quán)不同level的特征圖讓模型自己學會“這道題要看淺層的邊緣還是深層的語義”空間感知用稀疏注意力讓每個位置關注其他位置對判斷它有用處的上下文任務感知則用動態(tài)ReLU的風格讓分類和回歸兩個分支各自拿到對自己更友好的特征表達。放到錯題切分這個具體任務里最直接的好處是跨頁長題不會被特征融合平均掉邊界小字號題號也不會因為細節(jié)和語義信號打架而漏檢。2.2 不改主干、只換頭的工程收益很多做YOLOv11改進的人第一反應是動Backbone換注意力模塊、換卷積但我要說一個相對務實的判斷錯題切分的數(shù)據(jù)量通常不會特別大改動主干會讓預訓練權(quán)重基本失效訓練難度倍增。我一般建議把改動集中到Head部分。因為YOLOv11的檢測頭本身結(jié)構(gòu)清晰每一個檢測層有獨立的卷積路徑接入DyHead時不需要動C3k2和SPPF預訓練權(quán)重中Backbone和Neck的部分可以完整加載訓練收斂速度會快非常多。從部署角度看DyHead增加的顯存開銷集中在特征圖加權(quán)計算上相比給Backbone增加一個注意力模塊它在TensorRT和ONNX導出時的算子兼容性也更好。對Jetson Nano這類邊緣設備DyHead的ExtraStage如果開啟會帶來額外延遲但如果關掉ExtraStage只保留尺度感知和空間感知整體推理速度基本與原始YOLOv11在同一個量級。這類“換頭不換干”的改動是我更推薦給有落地壓力的團隊的。注意DyHead的實現(xiàn)版本很多有的魔改代碼把三個attention模塊全堆到Detect之前有的放在each level之后。選型時優(yōu)先看結(jié)構(gòu)清晰、能單獨開關每個attention的版本方便做消融實驗。2.3 配套的YOLOv11版本怎么選YOLOv11本身有幾個變體n、s、m、l、x對應從輕量到高精度。錯題切分這個場景我實測下來的結(jié)論是YOLOv11n當base精度確實欠一點特別是壓縮后的小字題號容易漏YOLOv11s是性能和速度的平衡點考慮到切分系統(tǒng)后續(xù)要跑OCR模型的輸出還要喂給后處理排序算法YOLOv11m更穩(wěn)妥。不要在邊緣設備上一上來就挑戰(zhàn)YOLOv11x錯題頁的檢測難度沒有高到需要這個級別的模型容量換來的是Jetson上兩倍以上的推理延遲。用YOLOv11s起步如果小目標漏檢率超過5%再升級到m這是比較理性的路徑。下面這段配置一個以YOLOv11s為baseline、加載YOLOv11m預訓練權(quán)重做遷移的YAML是我常用的初始配置# yolov11s_dyhead.yaml —— 基于YOLOv11s做DyHead替換的最小配置 # 用m權(quán)重初始化s結(jié)構(gòu)能讓小目標檢測頭的特征表達起點更高 # 訓練時關閉mosaic因為錯題題區(qū)域往往橫跨上下邊界mosaic拼接反而制造偽邊界 python train.py \ --model yolov11s_dyhead.yaml \ --pretrained yolov11m.pt \ --data mistakes.yaml \ --epochs 200 \ --batch 16 \ --imgsz 1280 \ --mosaic 0.0這個命令里imgsz 1280不是隨便寫的。錯題頁里的題號區(qū)域通常只有幾十個像素寬輸入分辨率降到640的話小目標特征在下采樣后只剩不到十個像素DyHead的尺度感知注意力再強也拿不到有效信號。mosaic 0.0是為了防止跨樣本拼接把題目邊界切成幾段。這樣做的代價是訓練速度變慢、顯存占用增加但換來的是小目標召回率顯著上升。3. 把DyHead裝進YOLOv11結(jié)構(gòu)改動和訓練配置3.1 修改模型定義文件的三處關鍵位置以Ultralytics的工程結(jié)構(gòu)為例接入DyHead需要在ultralytics/nn/modules/head.py里新增DynamicHead模塊并且讓Detect類的forward調(diào)用它。很多網(wǎng)上魔改代碼喜歡把DyHead塞在model/yolo.py的parse_model里直接替換Detect但那樣會導致detect的輸入特征層格式不一致。更穩(wěn)的做法是保留Detect的對外接口在其forward入口前加一道DyHead。# head.py 中新增的DyHead核心模塊簡化版 class DyHead(nn.Module): def __init__(self, in_channels, num_heads6): super().__init__() # 尺度感知對多尺度特征做加權(quán)本質(zhì)是SE注意力 self.scale_attention nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, in_channels // 4, 1), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels // 4, in_channels, 1), nn.Sigmoid() ) # 空間感知1x1卷積做空間注意力不引入額外超參 self.spatial_attention nn.Conv2d(in_channels, 1, 1) # 任務感知對分類/回歸分支做動態(tài)激活 self.task_attention nn.Sigmoid() def forward(self, x): # x是list包含三個尺度的特征圖 multi_scale torch.stack(x, dim0) # [num_levels, B, C, H, W] # 尺度維度上做注意力 scale_weight self.scale_attention( multi_scale.mean(dim(2, 3, 4), keepdimTrue) ) x [multi_scale[i] * scale_weight[:, i] for i in range(len(x))]這段代碼的邏輯不復雜先把三個尺度的特征圖在通道維度上做全局池化算出每個尺度對當前樣本的重要程度然后乘回去??臻g注意力采用1x1卷積相當于給每個像素位置學一個重要性分數(shù)。num_heads控制任務感知里的分組數(shù)錯題檢測里有“題號”和“題區(qū)域”兩類目標任務沖突不算嚴重默認6組夠用不要為了堆參數(shù)加到16訓練容易早停不動。3.2 訓練超參的四個必調(diào)項接入DyHead之后原來YOLOv11訓練腳本里的四個參數(shù)需要單獨調(diào)整。首先是lr0原始YOLOv11s默認學習率是0.01帶DyHead的檢測頭收斂節(jié)奏更慢建議降到0.005不然前10個epoch loss震蕩得很厲害。然后是lrf余弦退火的最終學習率系數(shù)從0.01改成0.001讓末期的權(quán)重更新更細。第三個是box_loss權(quán)重DyHead會增強回歸分支的梯度信號box權(quán)重從默認7.5降到5.0防止定位分支過度自信把小目標框震蕩掉。最后是hsv_h錯題采集時不同手機拍出來的色溫偏差很大把色調(diào)增強從0.015調(diào)到0.05增加對偏色卷面的魯棒性。# 訓練啟動腳本帶DyHead的YOLOv11s完整參數(shù) python train.py \ --model yolov11s_dyhead.yaml \ --data mistakes.yaml \ --epochs 300 \ --batch 16 \ --imgsz 1280 \ --optimizer AdamW \ --lr0 0.005 \ --lrf 0.001 \ --box 5.0 \ --cls 0.5 \ --dfl 1.5 \ --hsv_h 0.05 \ --patience 30patience 30也很關鍵DyHead在150到200個epoch之間會出現(xiàn)一個平臺期loss幾乎不動但之后會突然掉一截。如果patience設太小比如默認的100會在這個平臺期提前保存權(quán)重錯過后面更好的收斂點。optimizer AdamW對DyHead這類帶注意力分支的模型比SGD更友好注意力權(quán)重的更新幅度小AdamW的自適應步長能避免梯度消失。當然顯存不夠時SGD也能跑只是需要把lr再調(diào)低三分之一。3.3 數(shù)據(jù)集標注的格式約定錯題切分的目標類別建議只設兩類question和question_number。很多人會加第三類answer想把答案解析區(qū)域也切出來但這是給自己挖坑。因為解析區(qū)域通常緊挨著題目檢測框重疊嚴重后處理合并規(guī)則會變得極度復雜。我見過加了answer類的項目最終切分準確率反而下降6個百分點原因就是目標重疊導致的NMS誤抑制。標注規(guī)則上question框要比題目印刷區(qū)域外擴5-10個像素把輕微傾斜的邊界兜住。question_number框則標注題號數(shù)字的外接框不要包含頓號或括號。這個細節(jié)對切分排序至關重要——排序時先用題號的橫坐標定位列再用縱坐標定序如果框里包含了頓號坐標會產(chǎn)生幾個像素的抖動在密集排版下足以讓排序錯位。標注工具用LabelImg或X-AnyLabeling都行導出YOLO格式即可。數(shù)據(jù)增強方面錯題頁的干擾項主要來自紅筆批改、水印、訂正貼紙因此建議在訓練時增加三類增強RandPerspective的度數(shù)在5度以內(nèi)卷面傾斜是這個范圍、CopyPaste關閉會把題目區(qū)域黏到別的圖上制造假邊界、Mosaic關閉前面已提到。Albumentations里的RandomBrightnessContrast開啟幅度0.2模擬不同光照條件下的拍照效果。這些增強不增加標注成本但對錯題切分這種邊界敏感任務價值極大。4. 訓練、驗證與切分規(guī)則從檢測框到干凈題目圖4.1 在千張級數(shù)據(jù)上快速驗證DyHead是否有效錯題頁的數(shù)據(jù)集和通用檢測數(shù)據(jù)集不同通常一個學生能提供的有效標注樣本在幾百張級別。這個量級下跑300個epoch驗證集波動會很大所以評估要看兩個指標的組合mAP0.5和Recall0.5。錯題切分更關心漏檢——漏掉一題學生那頁的題目就不完整。相比之下多切一塊區(qū)域的后處理更可控。# 驗證腳本保存每張圖的檢測結(jié)果方便排查漏檢模式 python val.py \ --model runs/train/exp/weights/best.pt \ --data mistakes.yaml \ --imgsz 1280 \ --conf 0.25 \ --iou 0.45 \ --save_json \ --save_conf驗證時--save_json會把每張圖的檢測框坐標保存下來推薦配合--save_conf一起用。排查漏檢時只看mAP數(shù)字不夠需要把漏檢樣本框出來、按“排版密度高”、“紅筆遮蓋嚴重”、“題號字號小”分桶看哪類占比高。如果紅筆遮蓋類占比超過一半說明訓練數(shù)據(jù)里這類樣本不夠需要去采集更多紅筆批改的卷面而不是盲目加迭代輪數(shù)。4.2 切分規(guī)則列排序與重疊合并檢測模型輸出的是一堆帶置信度的question框還不是可直接入庫的“錯題圖”。要讓這些框變成按順序排列的題目列表需要兩步后處理列排序和目標合并。錯題頁一般是單欄或雙欄排版雙欄時兩列題目在版面上左右并列如果只按y坐標排序左右兩欄的題會交叉排。正確做法是先按題號框或題目框的x中心聚類成列再在每個列內(nèi)按y中心排序。# 切分排序算法按列優(yōu)先排序 IOU重疊合并 import numpy as np def sort_and_merge(boxes, iou_thres0.5, col_gap_ratio0.3): boxes: np.array, shape(N,5)每行是 [x1, y1, x2, y2, conf] 返回排序后的題目區(qū)域列表 if len(boxes) 0: return [] # 1. 按x中心粗聚類成列雙欄排版用單欄時所有框在一列 x_centers (boxes[:, 0] boxes[:, 2]) / 2 page_width boxes[:, 2].max() - boxes[:, 0].min() # 兩欄的列間距通常是頁寬的25%~45%取0.3作為列分隔閾值 col_labels np.zeros(len(boxes), dtypeint) col_id 0 for i in range(1, len(boxes)): if abs(x_centers[i] - x_centers[i-1]) page_width * col_gap_ratio: col_id 1 col_labels[i] col_id # 2. 每列內(nèi)按y中心排序 result [] for col_id in np.unique(col_labels): col_boxes boxes[col_labels col_id] y_order np.argsort((col_boxes[:, 1] col_boxes[:, 3]) / 2) col_boxes col_boxes[y_order] # 3. 合并重疊區(qū)域如果一個框的中心落在另一個框的內(nèi)部只保留大框 merged [] for box in col_boxes: if not merged: merged.append(box) continue last merged[-1] iou calc_iou(box, last) if iou iou_thres: if (box[2] - box[0]) * (box[3] - box[1]) (last[2] - last[0]) * (last[3] - last[1]): merged[-1] box else: merged.append(box) result.extend(merged) return result def calc_iou(box_a, box_b): x1 max(box_a[0], box_b[0]) y1 max(box_a[1], box_b[1]) x2 min(box_a[2], box_b[2]) y2 min(box_a[3], box_b[3]) inter_area max(0, x2 - x1) * max(0, y2 - y1) area_a (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b (box_b[2] - box_b[0]) * (box_b[3] - box_b[1]) return inter_area / (area_a area_b - inter_area 1e-6)這段代碼里的col_gap_ratio0.3是經(jīng)驗值如果你采集的錯題頁經(jīng)常是單欄建議把閾值提高到0.5避免把左右兩題誤判成不同列。iou_thres0.5控制“同一道題被檢測模型切成上下兩段”時的合并強度錯題頁中跨頁或跨欄的長題模型很可能給出多個框這時需要靠這個參數(shù)做區(qū)間合并。合并策略是“保大框”因為大框覆蓋了更多題目內(nèi)容而小框往往只是局部——這個策略是基于錯題頁的題目區(qū)域連續(xù)且完整這一假設在OCR后續(xù)處理前是合理的。4.3 切分效果的量化驗證切分系統(tǒng)不能只看檢測mAP還要看“一次切分成功率”。定義一個簡單的評估指標每張試卷的標準題目數(shù)G切分結(jié)果數(shù)P匹配上且IOU大于0.7的算作成功匹配M。切分準確率 M/G切分完整率 M/P。這兩者分別對應“有沒有漏”和“有沒有多切”。只有當準確率和完整率都超過95%時這套系統(tǒng)才算可以推到學生端。低于這個閾值后端OCR會頻繁看到被截斷或拼接的題目直接拉低整個錯題本應用的體驗。5. 踩坑記錄從環(huán)境配置到推理部署的五個真實教訓5.1 環(huán)境安裝Ultralytics版本陷阱現(xiàn)象按網(wǎng)上教程pip install ultralytics之后訓練能跑但自己改的DyHead代碼報錯提示Detect類參數(shù)數(shù)量不匹配。原因YOLOv11在8.3.0版本后重構(gòu)了Detect類nc從初始化參數(shù)移到了__init__內(nèi)部邏輯老代碼直接失效。解決固定版本安裝pip install ultralytics8.3.2并確保項目的requirements.txt鎖死這個版本。不要追新Ultralytics每隔兩周發(fā)一次小版本對魔改代碼極不友好。5.2 Jetson Nano部署ONNX導出算子問題現(xiàn)象在PC上導出的ONNX模型在Jetson Nano的TensorRT上跑報錯OP: [GridSample]。原因DyHead里的雙線性插值采樣部分實現(xiàn)用了F.grid_sample在TensorRT 8.x上不支持。解決檢查自己用的DyHead實現(xiàn)如果里面有g(shù)rid_sample替換成nn.functional.interpolate的版本或者直接把空間注意力改成普通卷積形式。Jetson Nano部署yolov11還有一個細節(jié)輸出層如果帶有nn.functional.softmax的變體TensorRT的--fp16開關下精度會掉建議先用FP32完成功能驗證再考慮半精度。5.3 訓練不收斂DyHead的多尺度特征stack順序現(xiàn)象loss在前50個epoch不降反升判斷是DyHead加入后梯度異常。原因有些DyHead魔改代碼把特征圖stack的順序?qū)懛戳薖3小目標特征放在了索引0但loss計算時按P5大目標特征優(yōu)先梯度傳播對不上。解決在訓練腳本里打印DyHead的輸入特征shape確認符合[B, C, H/8, W/8]到[B, C, H/32, W/32]的排列P3是索引0P5是索引2。這個順序在多數(shù)開源實現(xiàn)里搞反是網(wǎng)上YOLOv11改進教程里最隱蔽的坑。5.4 切分結(jié)果大量偏移標注框坐標系的四舍五入現(xiàn)象模型訓練時mAP很好0.98但切出來的題目圖位置整體偏右上偏移10個像素左右。原因標注工具導出的坐標是浮點數(shù)但內(nèi)部某次轉(zhuǎn)int時用round()而不是int(x)導致邊界框統(tǒng)一向上取整。解決寫一個數(shù)據(jù)清洗腳本把所有標注框的坐標統(tǒng)一轉(zhuǎn)float并保留兩位小數(shù)訓練前再檢查一遍labels目錄下的txt文件。這類問題用肉眼很難察覺建議在數(shù)據(jù)集準備階段加一個可視化抽查腳本把標注框畫出來保存成jpg隨機抽50張看一遍再開訓。5.5 推理結(jié)果保存被遺忘的原始圖像分辨率現(xiàn)象部署后保存的切分圖比原圖小一圈坐標對不上。原因模型輸入imgsz 1280推理輸出框坐標是640x640歸一化坐標后處理時沒有按原始圖像尺寸做等比縮放。YOLOv11推理后保存結(jié)果有個常見誤區(qū)——results[0].plot()直接用了letterbox后的坐標畫圖而不是原圖。解決在推理腳本里顯式調(diào)用result.boxes.xyxy.cpu().numpy() * scale_factorscale_factor original_size / model_input_size。這個問題會導致錯題切分框整體偏移在邊緣設備上更常見因為容易忽視預處理和解碼的差異。注意錯題頁拍攝時的透視畸變和邊緣畸變是切分正確率的天花板。手機廣角鏡頭拍攝A4紙邊緣的題區(qū)域?qū)嶋H是梯形的但檢測框是矩形的。如果發(fā)現(xiàn)切分框整體偏移集中在紙張邊緣不要調(diào)模型先去給手機裝個文檔掃描類的梯形校正功能。6. 把切分結(jié)果喂給OCR一個必須做的排版恢復技巧檢測模型把題區(qū)域從卷面上一一框出來但下一個問題立馬出現(xiàn)OCR閱讀順序和題目排版不一定一致。尤其是雙欄錯題頁OCR引擎按閱讀順序會把文字逐行讀取而不是按我們切分框的語義順序輸出。這時候不能直接把切分框里的圖丟給OCR要先做“框內(nèi)區(qū)域到文本行的重投影”。做法是把切分框的四個頂點坐標記錄下來在OCR返回的文字行坐標上做一個仿射映射把文字行歸屬到對應的切分框ID上。# 將OCR識別的文字行歸屬到錯題切分框 def assign_text_to_box(ocr_results, split_boxes): ocr_results: list of dict每個含text和poly字段 split_boxes: 切分后的題目區(qū)域框列表每個是 [x1, y1, x2, y2] 返回每個切分框?qū)奈谋酒唇咏Y(jié)果 assignments [[] for _ in range(len(split_boxes))] for res in ocr_results: poly res[poly] # 取文字行的幾何中心 cx sum(p[0] for p in poly) / 4 cy sum(p[1] for p in poly) / 4 # 歸屬到包含該中心點的最小切分框 best_idx, best_area -1, float(inf) for i, box in enumerate(split_boxes): x1, y1, x2, y2 box if x1 cx x2 and y1 cy y2: area (x2 - x1) * (y2 - y1) if area best_area: best_area area best_idx i if best_idx ! -1: assignments[best_idx].append(res[text]) return [ .join(assignments[i]) for i in range(len(split_boxes))]這段代碼的價值在于它把檢測模型定義的“矩形區(qū)域”和OCR引擎定義的“文本行”做了對齊。很多團隊跑通了切分和識別但發(fā)現(xiàn)輸出的題目文字順序是亂的就是因為少了這一步歸屬。best_area條件用來處理文字行恰好落在兩個切分框邊界的情況——歸屬到面積更小的框通常更準因為小框一般對應單題區(qū)域大框的語義范圍更寬松。驗證邏輯上建議每次跑完一個錯題頁把切分框、OCR文字行歸屬結(jié)果疊加到原圖上存一份HTML預覽。肉眼快速掃一遍如果兩道題的文字互相穿插優(yōu)先檢查列排序的col_gap_ratio參數(shù)其次檢查合并閾值大概率是這兩個參數(shù)在特定頁面上失效了。純看結(jié)果數(shù)字很難定位問題可視化預覽是這套方案里最值得花半個小時搭建的調(diào)試工具。我在自建數(shù)據(jù)集上把這套方案完整跑通過之后最大的習慣變化是每次訓練完不直接看mAP而是固定挑那張最難的雙欄紅筆批改卷看切分后的題目順序和完整度——一個反例比一堆曲線都更能說明模型到底有沒有進步。這個習慣也幫我最快發(fā)現(xiàn)了DyHead輸入順序、坐標縮放和列間距這三個坑希望這套路徑能幫你少走幾趟。錯題自動切分系統(tǒng)的落地價值不在于單張圖切得有多準而在于整頁錯題的排列是否穩(wěn)定可復現(xiàn)——穩(wěn)定了OCR、錯題歸檔、知識點推薦才有可靠地基。希望幫到你。本文還有配套的精品資源點擊獲取