基于DyHead与YOLOv11的错题自动切分:目标检测实现版面分析

📅 发布时间:2026/10/17 3:22:02
基于DyHead与YOLOv11的错题自动切分:目标检测实现版面分析
错题切分这个方向我这半年做得最多的一件事就是怎么让模型在一张乱糟糟的错题照片里准确找到每一道题的范围。标题里的“基于DyHead和YOLOv11的错题自动切分系统”听起来像论文选题其实落到业务上就是一句话学生拍一页错题本系统自动把这一页里每一道错题裁剪成独立图片方便后续逐题OCR识别、打标签、进复习库。这篇文章我把从需求拆解、数据集构建、模型选型、DyHead接入到部署上线和踩坑复盘整个流程都写清楚适合正在做版面分析、教育场景CV落地的开发者参考。1. 项目要解决什么错题切分不是图片裁剪是目标检测1.1 先看业务场景为什么必须“先切分再识别”做过教育类产品的人应该都遇到过这个困扰一张错题本照片里经常有3到5道错题有些是选择题有些是解答题旁边还写着老师的批注、自己的备注、甚至贴了一张便利贴。如果直接把整页丢给OCR识别结果会非常乱——题干、选项、手写解答、批注文字全部混在一起根本没法做结构化入库。更麻烦的是后续要做错题归类、相似题推荐、知识点标签每一步都依赖“单道错题”这个最小单元。我见过某团队最初的做法是让用户手动框选错题区域但真实用户根本不愿意慢慢框很多人拍完照就关了页面留存率掉得非常厉害。所以系统必须做到一拍完照就能自动把每道错题切出来用户只需要确认一下结果。这一步的本质不是图像分割而是目标检测输入一张错题本页面图片输出若干个矩形框每个框代表一道错题的边界范围。模型只需要定位“题在哪里”不需要精细到题目文字的像素级掩码。这大大降低了问题难度也让YOLO这类检测器成为最顺手的工具。1.2 为什么不直接用投影切分或轮廓查找我最初推演方案时也考虑过传统图像处理路线先做二值化再做水平投影和垂直投影根据文字行的空白间隙来切分区域或者用连通域分析提取题目块。这套思路在“干净扫描件”上效果还可以但一旦遇到手机拍摄的真实场景就崩了——纸张弯曲、光照不均、手写笔迹深浅不一、题目之间的间距忽大忽小投影法经常把一道题拦腰切断或者把两道相邻的题粘连成一块。连通域分析的另一个致命问题是题目区域并不是一个独立的连通域。题干、选项、手写解答之间有大片空白这些空白把同一个题目的各个部分切成了好几块很难自动归并。就算加上一些启发式规则比如“根据文字行高合并块”规则一多就变成一堆if-else维护成本极高换一批数据效果还飘。目标检测方案就没有这些问题。检测器学习的是“错题区域”这个语义概念它不会因为区域内存在空白就被切开也不会因为题目之间贴得近就轻易合并。手写题、打印题、带图的题都能用一个统一的检测框来表达。这也是我最终选择检测路线的核心原因。1.3 为什么选YOLOv11又为什么非要引入DyHead模型选型上我最初对比过Faster R-CNN、YOLOv8和YOLOv11。错题切分场景对速度的要求很高用户拍完照不可能等两三秒才出结果所以两阶段的Faster R-CNN首先被排除。YOLOv8和YOLOv11都是一阶段检测器YOLOv11在特征提取和head设计上又做了一些优化对小目标的召回和边界定位都有提升直接拿来当底座非常合适。但在实际测试中我发现YOLOv11的基线模型在错题页上还有一个明显短板错题框的尺度差异非常大。有时一道选择题只占页面的十分之一有时一道大题占了半边页面而且页面边缘、纸张弯折处的题目很容易被漏检或者框偏。这正好是DyHead能发力的地方——Dynamic Head通过尺度感知、空间感知和任务感知三个维度的注意力机制让检测头更灵活地处理不同大小、不同位置、不同任务需求的候选区域。简单来说YOLOv11负责把特征提取得足够好DyHead负责让检测头在复杂版面下“想得更明白”。这两个组合起来才能应对真实错题照片的混乱情况。2. 数据集构建错题切分系统的命根子2.1 采集真实错题页数据量其实不用“海量”很多同学一提数据集就想着要几万张但错题切分这个任务比较特殊它不像通用目标检测那样类别多、场景杂。错题页的目标类别只有一个错题区域。所以数据规模可以控制在比较务实范围我这次用的自建数据集一共2200张真实错题页照片标注出的错题框大约9000多个人均框数在4个左右。采集时我刻意覆盖了几类常见变化设备差异主力是手机拍摄也混入少量扫描仪输出的PDF转图拍摄角度正拍、斜拍、俯拍、甚至带一点透视变形的照片书写状态印刷体标题、手写解答、铅笔草稿、红笔批注纸张背景普通笔记本、活页纸、试卷、带着折痕的旧纸张版面复杂程度单题页、三四题混排、含图片的选择题、跨页拼接的解答题。要特别提醒的是数据采集不要只找干净页面拍。我见过不少团队用自己工位上摆拍的数据训练模型效果虚高一上真实用户数据就掉点。错题切分是个强场景依赖的任务训练集里必须有相当比例的“脏数据”包括手指遮挡、阴影、纸张边角翘起。真实环境里什么情况都有模型必须在这些噪声下仍然稳定出框。2.2 标注三原则边界、题干、排除项标注质量直接决定检测上限。错题数据集里容易出现一个很迷惑的问题标注员不知道“一道题”的边界到底在哪里。我初期踩过这个坑不同标注员画的框五花八门有的只框题干有的把整页都框进去模型训练出来自然乱七八糟。后来我定了一套明确的标注规范框住题干、选项、解题区和订正区如果题目是从试卷上剪下来的连同题号一起框进去不框页眉页脚、页码、自己写的“错题本”标题这类装饰性内容如果两个错题中间只有一条手画的分隔线分隔线本身不单独成框它属于两道题之间的边界一道题跨页时每一页上的部分都单独标注后续后处理后端再合并。标注工具我用的是一套开源图像标注平台多人并行标注。标完一轮后我会抽20%的图片做交叉复核把框偏移超过阈值的数据退回修改。这一步看起来很费时间但能省掉后面大量返工成本。2.3 数据增强里最容易翻车的几个操作错题切分场景对数据增强的要求比较特殊因为目标本身是文字区域很多通用检测的增强策略直接套上来会出问题。先说马赛克增强。YOLO系列训练的标配是马赛克把四张图拼在一起训练。这种增强能提升模型对不同尺度目标的适应能力但对错题数据有个隐藏风险拼图时边缘裁剪很容易把某个题的题干切掉一半模型反而学会了在这种残缺数据上硬找框。所以我在马赛克增强后面加了一个约束如果某个标注框在马赛克拼接后被裁掉超过30%就把这个框丢弃而不是保留半截框让模型去学。随机缩放也要注意下限。错题页里的文字本来就小如果图像被缩得太小文字变成一团模糊边界信息直接丢失。我把训练尺寸固定到640x640最短边缩放限制在0.5倍以上低于这个倍数的样本直接不参与增强。颜色抖动、亮度对比度调整这类增强可以适当加但要控制幅度。真实错题照片的光照不均问题很常见但过度调色会让模型产生错误依赖。我实测下来亮度扰动幅度控制在正负20%以内比较稳妥。3. YOLOv11 基线搭建与评估3.1 版本与预训练选择直接拿YOLOv11-m起步YOLOv11有n、s、m、l、x几个版本我第一轮实验直接选的m。原因很简单n和s太轻复杂版面下漏检率偏高l和x精度会上来但训练和推理成本都翻倍对早期迭代不友好。m是个比较折中的起点先跑通完整链路后面如果精度不够再往上升级。预训练权重我用的是COCO上的通用权重。这里有个很多人会忽略的点COCO有80类错题区域不在其中所以加载权重时需要跳过最后一层的类别数从头训练检测头。这个过程在脚本里就是一个参数设置的事但如果不注意容易把类别维度搞错训练时报维度不匹配的错误。关于训练代码我建议直接用YOLOv11官方仓库的train脚本先把数据格式转成标准的YOLO格式每张图片对应一个txt文件每一行是“类别id x_center y_center width height”坐标全部归一化到0-1。数据划分我按8:1:1分成了训练集、验证集和测试集为了保证测试集能反映真实分布划分前先按“同一次拍摄批次”做了分组避免同一页面的多张照片同时出现在训练集和测试集里。3.2 基线训练从命令到第一次出结果训练配置上我第一轮并没有追求极致调参先采用一套相对稳妥的默认参数跑通流程python train.py --model yolov11m.pt --data custom.yaml \ --epochs 200 --batch-size 16 --imgsz 640 \ --device 0 --workers 8几个关键参数说下我的考虑epoch设200配合余弦退火学习率调度让模型有足够时间收敛。早期实验我试过100轮发现mAP还在缓慢上升就直接改成200轮batch-size 16在单卡24G显存下比较安全如果显存紧张优先降batch而不是降分辨率学习率选了默认的0.01配合warmup前3轮跑下来比较稳数据增强方面我在配置里关掉了垂直翻转错题页倒过来的情况太少见翻转容易让模型产生错误的“倒置文字也能识别”的倾向。第一轮基线训练在单卡上跑了一天多。验证集上的结果是mAP0.5约0.86mAP0.5:0.95约0.58。这个数字看似还行但实际去看检测结果时发现数据里的“简单题”单独一面只拍了一道题、纸张平整确实框得很好但页面边缘的题目、被阴影遮挡的题目、靠近装订线的题目漏检率明显偏高。3.3 看一眼baseline到底漏了什么我整理了几类典型的失败案例大题里嵌套小题的复合错题页面上有一道大题大题里又有几个小问模型有时会只框出大题的一部分把另外的小问当成一个独立区域紧贴页面边缘的错题纸张边缘和桌面背景对比度低模型把边缘处的题目和背景混在一起框的位置明显偏移同页双栏排版的错题两栏之间间距很小模型偶尔会把两栏的相邻题目合并成一个框。这三类问题里有两类和“尺度多样性”“空间位置敏感性”高度相关正好是DyHead设计的发力点。于是下一步就非常明确了保持数据和训练配置不变把DyHead接入模型看它能不能解决baseline的这些短板。4. DyHead原理与接入方式让检测头学会“挑重点”4.1 三个感知维度是怎么“动”起来的DyHead的全称是Dynamic Head它的核心思想是把目标检测头里的多个感知维度统一到一个注意力框架里。要理解这个设计可以从检测头的工作方式说起。一个常规检测头接收来自骨干网络的特征图然后并行做两件事分类判断这个区域里是什么和回归预测边界框位置。问题是骨干网络输出的特征图包含多个尺度、多个空间位置的信息检测头需要自己决定“该重点关注哪部分信息”。普通卷积头是静态的对每个位置一视同仁。而错题页面里一道小题和大题的检测难度完全不同页中央和页边缘的检测难度也不同静态头很难同时照顾好这些情况。DyHead把这个问题拆成三个维度来处理尺度感知通过注意力机制对不同尺度的特征图做加权让模型在检测小题时更依赖高分辨率特征在检测大题时更依赖语义信息丰富的低分辨率特征空间感知让模型关注那些真正包含目标的空间位置。错题页面上有大面积空白空间感知能把这些不重要区域抑制下去把注意力集中到题目所在区域任务感知分类和回归这两个任务对特征的需求不完全一样分类更在意语义区分回归更在意边界细节。任务感知让同一条特征流能适应不同任务的需求。这三个维度不是独立运作的而是像流水线一样串联起来。一个候选区域的特征先经过尺度感知的加权再经过空间感知的空间筛选最后经过任务感知的任务调制输出的特征自然比原始卷积头更具判别力。整个模块可以重复堆叠多次每一次堆叠都可以让特征更精细。用大白话打个比方如果把模型比作一个老师批改试卷尺度感知是“知道这道题是选择题还是大题分配对应的注意力”空间感知是“知道答题区在哪里不看试卷空白处”任务感知是“打对错和圈出错点分开处理各有侧重”。三个能力合在一起批改效率就高了。4.2 接入点选择为什么加在检测头之前确定了用DyHead接下来要回答“加在哪”。YOLOv11的完整结构大致是骨干网络提取特征颈部网络做多尺度特征融合然后接入检测头输出预测。DyHead可以直接替换检测头也可以在检测头之前作为特征增强模块接入。我的做法是后者在颈部网络输出的特征图之后、检测头分类和回归分支之前插入DynamicHead模块。这个位置的选择基于一个考虑颈部网络输出的多尺度特征已经融合了不同层的信息但还没有针对“检测任务”做过专门的调整。在这个位置加入DyHead相当于给检测头一个经过“预筛选”的特征检测头可以更轻松地完成分类和回归。如果直接把DyHead作为整个检测头替换掉改动太大训练难度也会上升尤其是在数据量有限的情况下容易过拟合。保留YOLOv11原生的解耦检测头只在前面加一个注意力增强模块是一个性价比更高的方案。4.3 一份可以直接套用的核心改造代码代码实现上DyHead本身已经有一些开源实现但直接拿过来用时会发现接口和YOLOv11的channels对不上。我这边的改造思路是先把DyHead封装成一个独立的nn.Module保证输入和输出的形状一致然后找到模型构建函数里检测头初始化的位置在中间插进去。核心结构大致是这样import torch import torch.nn as nn class DynamicHead(nn.Module): 简化版DyHead核心逻辑尺度感知 空间感知 任务感知 输入形状: [B, num_scale_feats, C, H, W] 已做多尺度cat 输出形状: 与输入一致 def __init__(self, in_channels, num_heads6, num_scales3): super().__init__() self.num_scales num_scales self.num_heads num_heads self.in_channels in_channels # 尺度感知对每个尺度特征图上采样后做加权 self.scale_attn nn.Sequential( nn.Conv2d(in_channels * num_scales, in_channels, 1), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels, in_channels * num_scales, 1), ) # 空间感知通过可变形卷积实现空间位置的动态采样 # 这里用普通卷积示意实际项目中可替换为可变形卷积 self.offset_conv nn.Conv2d(in_channels, in_channels, 3, padding1) self.spatial_attn nn.Sequential( nn.Conv2d(in_channels, in_channels // 2, 1), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels // 2, 1, 1), nn.Sigmoid(), ) # 任务感知用动态卷积/动态归一化调制特征 self.task_attn nn.Sequential( nn.Conv2d(in_channels, in_channels, 1), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels, in_channels * 2, 1), ) def forward(self, feats): # feats是list包含多个尺度的特征图 # 先把不同尺度上采样到同一个空间尺寸 target_h, target_w feats[0].shape[-2:] resized [] for feat in feats: if feat.shape[-2:] ! (target_h, target_w): feat nn.functional.interpolate( feat, size(target_h, target_w), modebilinear, align_cornersFalse ) resized.append(feat) cat_feat torch.cat(resized, dim1) # 尺度感知加权 scale_weight self.scale_attn(cat_feat) scale_weight scale_weight.view( cat_feat.shape[0], self.num_scales, self.in_channels, target_h, target_w ) scale_weight torch.softmax(scale_weight, dim1) scale_feat (cat_feat.view( cat_feat.shape[0], self.num_scales, self.in_channels, target_h, target_w ) * scale_weight).sum(dim1) # 空间感知加权 spatial_weight self.spatial_attn(scale_feat) spatial_feat scale_feat * spatial_weight # 任务感知调制示意 task_ctx self.task_attn(spatial_feat) gate torch.sigmoid(task_ctx[:, :self.in_channels]) shift task_ctx[:, self.in_channels:] out spatial_feat * gate shift return out这段代码是简化示意版实际工程里要根据YOLOv11的通道数把in_channels对齐。接入方式是在模型构建流程里取出颈部输出的特征列表经过这个模块后再送入检测头的分类和回归分支。改造完成后建议先用一张单图跑一次前向确认形状没有报错再开始训练。5. 训练调参与效果对比不只是mAP变高了5.1 同数据集同配置控制变量做对比要验证DyHead带来的增益最重要的一点是控制变量。我把训练集、验证集、测试集完全固定batch-size、学习率、epoch、数据增强策略完全一致唯一的不同就是“有没有DyHead模块”。否则参数一变结果差异说不清楚到底是谁贡献的。训练时我注意了一个细节加载预训练权重时DyHead模块的权重是随机初始化的而YOLOv11其它部分加载的是预训练参数。这种情况下如果学习率太大随机初始化的模块可能会产生很大的梯度把已经预训练好的特征破坏掉。我的做法是给DyHead模块单独设置一个较小的初始学习率为其设置不同的参数组。实际操作时我给DyHead模块的学习率设置为其他层的0.1倍训练前期效果非常稳定。因为引入了DyHead每轮训练时间比基线大约增加了15%到20%。这个成本完全可以接受毕竟我们要看的是最终精度收益。训练完成后我用同一套测试集上做推理记录每个类别的AP值和典型样例。5.2 切分效果变化边界贴得更紧小目标召回高直接看量化指标我在自建测试集上的对比结果大概是这样配置mAP0.5mAP0.5:0.95小目标AP (面积32^2)单图平均推理耗时GPUYOLOv11-m 基线0.8610.5830.41218msYOLOv11-m DyHead0.8870.6190.47323msmAP0.5提升了2.6个点mAP0.5:0.95提升了3.6个点小目标AP提升了6个点左右。如果只看总数提升幅度好像没那么夸张但具体到刚才说的失败案例效果差异非常明显页面边缘的题目漏检数量从基线的大约7%降到2.5%左右两栏排版的错题误合并的情况大幅减少大题嵌套小题的场景检测框更倾向于覆盖整个大题的完整范围而不是只框住局部。我更关注的其实是边界贴合度。错题切分对框的精确度要求比普通目标检测更高因为后续切出来的图片要直接拿去做OCR。如果框边界把下一道题的题干切进来OCR混入无关文字下游任务就乱了。加了DyHead之后检测框和人工标注框的平均IoU从0.82提升到0.86这是很值得的收益。5.3 速度代价GPU能扛CPU有压力精度提升不是免费的。DyHead在检测头前面增加了几个卷积和注意力操作单图推理耗时从18ms涨到23ms左右。在GPU上这个代价完全无所谓部署服务端的话每秒还能处理三四十张图远超人眼速度。但如果你打算部署到CPU推理或者边缘设备上需要格外谨慎。我在一台普通CPU机器上实测加了DyHead之后的推理时间从大约250ms涨到400ms。如果是一个实时视频流场景这个延迟可能接受不了但错题切分本来就是“拍一张处理一张”的交互场景400ms用户也能接受只是体验没那么好。稳妥起见服务端部署建议至少配一块入门级GPU或者导出ONNX后用推理框架进一步加速。6. 部署与上线后处理是切分系统的另一半6.1 检测完不等于切完扩边、过滤和排序模型输出的是一堆检测框但直接拿着框去切图效果会很生硬。我在后处理里加了一整套规则。第一步是置信度过滤和NMS。我会把置信度阈值设在0.35左右偏保守一些宁可多给出几个候选框让用户确认也不能漏掉一道错题。NMS的IoU阈值设在0.5避免同一道题输出多个重叠框。第二步是对切图区域做向外扩边。模型给出的框通常紧贴题目内容但如果直接按框裁剪题干边缘的手写笔画可能刚好处在边界上被切掉一截。我的做法是每个方向向外扩展8到12个像素。扩边不能太多否则会把邻题的边角带进来。第三步是排序。错题页面的阅读顺序通常是先从左到右再从上到下。切分后的图片按“行优先”排序也就是先按框中心点的纵坐标分桶同一行的框再按横坐标从左到右排。这个排序逻辑在后续生成错题列表时非常重要否则用户的错题顺序会乱掉。排序这里有个容易忽视的坑如果拍摄时页面是倾斜的同一行的框的纵坐标差异可能很大简单按纵坐标排序会把本应同一行的题拆到不同行。我的解决办法是先对页面做个粗略的倾斜校正或者使用基于空间关系的聚类排序先聚成行再排顺序。6.2 部署端加速导出ONNX再转TensorRT模型训练用PyTorch但线上推理我通常不直接用PyTorch而是导成ONNX再用推理框架加速。导出时要注意几个opset版本问题DyHead里用到的上采样操作在某些旧版本opset下会报错建议直接用opset 13以上。# 将训练好的模型导出为ONNX python export.py --weights best_with_dyhead.pt --include onnx --opset 13 --imgsz 640 # GPU环境下转成TensorRT引擎 trtexec --onnxbest_with_dyhead.onnx --saveEnginebest_with_dyhead.trt \ --fp16 --workspace4096TensorRT用FP16量化后推理速度比PyTorch快了一倍以上。我在部署服务器上实测FP16的TensorRT引擎单图推理耗时可以压到10ms以内完全满足线上调用需求。如果你没有GPU环境只有CPU那么可以考虑用ONNX Runtime做推理加速。虽然速度不如TensorRT但比原生的PyTorch CPU推理快不少。还有一个技巧是开启ONNX Runtime的动态形状优化把batch维度设为-1这样一个引擎可以同时处理多张图片请求。6.3 漏检兜底和版面分析做联动没有任何模型能做到100%不漏检。上线后我发现一个高频场景有些错题本页面用荧光笔做了高亮标记荧光色和纸张的对比度极低检测器偶尔会漏掉整个区域还有页面被透明胶带贴过折痕处的文字变形严重也会导致检测框偏移。这时候不能只靠检测器硬扛我加了一道兜底逻辑在检测的同时并行跑一个轻量级的版面分析识别页面中的“文字块”区域。如果检测器给出的框数量明显少于版面分析识别出的文本块数量就触发“待确认”状态把这些区域展示给用户让用户手动点选补充。这个兜底虽然简单但极大减少了用户的挫败感。另一个兜底是联动OCR。切图后如果OCR识别结果为空说明这个区域大概率切错了可以自动标记为低置信度提醒用户人工检查。7. 踩坑记录与经验清单这里全是钱买来的教训7.1 标注框贴太紧导致切题切歪第一个大坑是我自己踩的。一开始做标注规范时我要求标注员“框紧贴题目内容”。结果训练出来的模型虽然框很紧但切出来的图总是把题目边缘的字切一半。后来我反思了一下模型学习到的“紧贴”会让预测框也紧贴内容边缘这正好和后处理的扩边需求相冲突。现在我的做法是训练时把标注框往外扩一圈让模型学习到“错题区域包含一点边距”这个先验。这个改动让切图结果的完整度明显提升OCR识别率也上涨了几个点。标注规范这种事要跟后处理策略联动考虑不能只顾着“框得准”。7.2 mAP不骗人但也说明不了全部问题刚开始做这个系统时我特别关注mAP每次训练完先看mAP涨没涨。后来发现mAP高的模型在真实场景里不一定好用。原因在于mAP是按“所有预测框”统一计算的但错题切分业务真正在意的是“每个页面的错题数是否完整、每道题是否切干净”。一个模型可能在100张图上都检测得很完美但偏偏漏掉某一张图里最重要的一道大题这在业务上就是一次严重事故。所以后来我改成了两层评估体系第一层看标准的mAP指标判断模型整体能力第二层建立一套“页面级评估”——单张页面里的错题数是否和人工标注一致、切出来的图是否每一张都有可识别的完整题干。页面级准确率比mAP更能反映真实业务表现。建议做这类场景的开发者都建立同样的评估视角。7.3 后面还打算怎么玩这个系统错题切分只是整个错题管理流程的第一步。切出来的图片现在会进入OCR识别和知识点标注但我发现还有几个可以继续深挖的地方。一个是自动去手写。有些用户希望切出来的错题图能自动抹掉手写答案方便以后重新做一遍。这个需求可以做成一个图像修复模型检测到手写区域后用背景纹理填充实际效果比想象中复杂因为手写笔迹和印刷字体的交叉区域很容易修出残影。另一个是相似错题聚类。切分准确之后可以利用题目文本和版面结构做向量化把同一知识点的错题自动聚合帮用户发现“自己在哪些知识点上反复出错”。这个功能对学习效果的提升价值非常大也是我下一步最想做的方向。最后说一个我反复体会到的点这类教育场景的CV项目光有模型远远不够数据规范、后处理规则、兜底策略、甚至UI交互里的“手动确认”入口一个都不能少。模型能帮你解决90%的问题剩下10%要靠产品逻辑去兜住。这大概也是做行业落地和单纯刷榜之间最大的区别。