尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

单类别模型自动补标工作流与迭代优化记录

单类别模型自动补标工作流与迭代优化记录 一、我们现在要解决的核心问题我们目前的数据集包含personhelmetvesttractorslippersmoking共 6 个目标类别。现有数据已经有人工标注但随着数据量扩大发现部分图片存在漏标问题。例如一张图片实际上有 4 个人但原始标签只标了 3 个或者图片中有安全帽、拖鞋、吸烟等小目标但原始数据没有完整标出来。如果全部重新人工检查成本比较高。所以我们的思路不是重新做一套伪标签而是保留现有人工标签作为基准再利用已经训练好的高质量单类别模型对整个数据集进行一次“漏标扫描”只补充模型高置信度发现、同时又没有和原有标签重合的目标。目标是原标签模型辅助发现漏标保守自动补标人工抽样检查。二、为什么使用6个单类别模型目前我们分别有person modelhelmet modelvest modeltractor modelslipper modelsmoking model不是同时把 6 个模型全部加载进显存而是采用串行方式person 模型↓扫描 train / val / test↓释放模型helmet 模型↓扫描 train / val / test↓释放模型vest 模型↓扫描 train / val / testtractor 模型↓slipper 模型↓smoking 模型代码当前就是外层遍历类别再在每个模型内部遍历 train、val、test。假设整个数据集train val test N 张那么 6 个模型理论上就是6 × N次 image-model inference。例如有 20,000 张图20,000 × 6 120,000 次图片-模型推理虽然计算量比较大但是这样做有两个好处。第一不需要同时把 6 个模型放在 GPU 中显存压力更小。第二每一个类别都可以使用自己的模型和自己的置信度阈值。例如 smoking、slipper 这种比较难的类别可以和 person、helmet 使用不同标准。当前默认阈值就是按类别分别设置的例如person: auto0.75 review0.55helmet: auto0.75 review0.55vest: auto0.75 review0.55tractor: auto0.70 review0.50slipper: auto0.65 review0.45smoking: auto0.65 review0.40代码中已经将这些阈值独立配置。三、安全原则绝对不修改原标签这个方案最核心的设计之一是原始labels永远不动。程序首先把dataset/labels/复制一份成为out_root/labels_autofill_v1/代码就是先逐 split 复制原标签。所以一开始labels_autofill_v1原始 labels后面模型发现的新增标签全部只追加到labels_autofill_v1/代码的 append 操作也明确写的是输出标签目录而不是原始目录。即使模型补错很多也可以直接删除整个输出目录原始数据完全不会受到影响。四、模型到底怎么判断一个框是不是“漏标”以 person 模型为例。假设一张图实际有person Aperson Bperson Cperson D但是原标签只有person Aperson Bperson C模型预测P1 → Aconfidence0.96P2 → Bconfidence0.93P3 → Cconfidence0.91P4 → Dconfidence0.89我们不能直接把这 4个框全部追加否则 A、B、C 就会重复标注。所以当前做了一层非常重要的Existing Label Matching对于模型预测框Prediction计算它和原来同类别GT框的最大 IoU。例如P1 vs 原 GT 最大 IoU 0.94P2 vs 原 GT 最大 IoU 0.89P3 vs 原 GT 最大 IoU 0.91P4 vs 原 GT 最大 IoU 0.02默认iou_existing 0.50如果IoU 0.50说明这个目标大概率原来已经标过matched_existing→ continue→ 不再补当前代码就是这样处理。所以P1 → 已标P2 → 已标P3 → 已标P4 → 原标签附近没有 personP4 才会继续作为“疑似漏标”。五、为什么还要分AUTO和REVIEW剩下的框并不是全部自动写进去而是进一步按置信度分为低置信度中置信度高置信度以 person 为例0 -------- 0.55 -------- 0.75 -------- 1丢弃 REVIEW AUTO也就是说低于review thresholdconf 0.55模型自己都不太确定直接忽略。中等置信度0.55 conf 0.75进入REVIEW记录下来但是不修改标签。高置信度conf 0.75进入AUTO才允许自动追加到labels_autofill_v1当前代码中就是mode auto if conf auto_thr else review并且只有 batch_auto 最终会写入复制后的标签。所以整个系统实际上是一个模型发现目标│▼和已有 GT 比较│├── 已经标过 → 忽略│▼真正疑似漏标│├── 低置信度 → 忽略│├── 中置信度 → REVIEW│└── 高置信度 → AUTO这是一个比较保守的补标策略。六、第二层IoU解决“多个真实目标被错误去重”的问题最早版本里还有一个问题用于判断“已有标签”的 IoU 阈值和“预测框重复”的 IoU 阈值使用得比较接近。这会有风险。例如两个人站得比较近person A boxperson B box可能IoU 0.55但是他们确实是两个真实的人。如果把IoU 0.5直接当成重复预测就可能误删一个人。所以后面把两个概念彻底拆开existing IoU--iou-existing 0.50回答的问题是这个预测和原有人工标签是不是同一个目标candidate duplicate IoU--iou-candidate-duplicate 0.95回答的问题是两个新的模型预测是不是几乎完全相同的重复框当前代码已经把两者拆成两个参数。并且只有新的 candidate 之间IoU 0.95才会认为是重复预测。这样可以避免因为两个物体靠得近把真实目标错误删除。七、IoU计算也做过专项排查IoU 属于这套流程里非常关键的基础函数。因为如果 IoU 算错比如面积计算错误就可能出现真实 IoU 0.2错误计算 IoU 0.7于是系统会误认为这个目标原来已经标过。最终造成漏标依然没有补出来。所以后续我们专门检查了 IoU 实现。当前版本area_a (ax2 - ax1) × (ay2 - ay1)area_b (bx2 - bx1) × (by2 - by1)实现是正确的。这类 Bug 比程序直接报错更危险因为程序可能完整运行结束但是生成的数据质量是错的。所以现在我们的思路不仅是“代码能跑”还要对IoUclass idthresholdduplicate suppressionlabel append这些关键数据逻辑做专项验证。八、类别ID也不能直接写死还有一个比较重要的优化是类别映射。单类别模型内部可能都是class 0比如person_best.pt:0 personhelmet_best.pt:0 helmetslipper_best.pt:0 slipper但是最终总数据集可能是0 person1 helmet2 vest3 tractor4 slipper5 smoking所以helmet model 的 class 0不能直接写0否则最终会被训练程序解释成person因此现在程序从data.yaml动态读取真实类别顺序和 class ID并检查 nc、names 是否匹配。最终 candidate 创建时使用的是dataset_class_ids[class_name]而不是单模型自己的类别编号。这个优化主要是为了防止静默的数据污染。九、auto_samples为什么后来重新设计最初可视化逻辑是一个 Candidate→ 读取一次图片→ 只画这个 Candidate 的一个框→ 保存于是出现了一个明显问题。例如一张图片有 4 个 person但是一个自动补标 candidate 对应一张输出图所以最终打开auto_samples/person/xxx.jpg可能只看到一个框。这会给人工检查造成误导看起来像模型只检测到了一个人。还有一个问题如果两个 candidate 来自同一张图并且conf 0.92341conf 0.92336旧文件名只保留 3 位0.923就可能发生文件覆盖。所以后面把抽样单位从Candidate / box改成unique imageCandidateWriter 现在按mode class image进行分组只保存高置信度的有限数量不同图片。因此--draw-auto-samples 80表示的是每个类别最多抽 80 张不同图片。而不是每个类别只保存 80 个框。十、为什么仍然按类别分目录抽样我们的目的不是随机看所有模型结果而是分别评估person 补得怎么样helmet 补得怎么样vest 补得怎么样tractor 补得怎么样slipper 补得怎么样smoking 补得怎么样所以仍然保持auto_samples/├── person/├── helmet/├── vest/├── tractor/├── slipper/└── smoking/当前程序确实按类别分别维护 sample group并输出到对应类别目录。这里需要说明一个当前版本的实现细节目前目录是按类别抽样的但draw_sample_group()会先画这张图片完整的merged label状态再高亮当前候选。也就是说 person/ 中抽到的图片一定是因为存在 person 补标候选但画面中仍可能看到其他类别标签。当前代码在绘制 merged boxes 时没有按 class_id 再过滤。如果我们希望以后做得更纯粹可以进一步改成auto_samples/person/只显示 person 的 GT person 的 AUTOauto_samples/helmet/只显示 helmet 的 GT helmet 的 AUTO这个属于下一步比较小的可视化优化不影响真实补标结果。十一、GT和AUTO的区分可视化并不是简单把所有框都画一样。当前会根据标签来源标记GT personAUTO person原有标签GT自动新增标签AUTO当前代码还使用不同线宽GT → thickness 2AUTO → thickness 3然后当前触发抽样的 candidate 还会额外显示AUTO person 0.92这部分逻辑已经在可视化函数中实现。后续还可以继续优化成GT → 一种颜色AUTO → 另一种颜色让老师或标注人员一眼就能看出来哪些是人工标签、哪些是模型新增标签。十二、为什么经常出现显存OOM目前机器显存已经比较大但仍然会出现 CUDA OOM。这个问题并不能简单理解成显存容量小。因为推理阶段显存峰值与多个因素有关输入尺寸 imgszbatch size模型大小中间 feature mapYOLO NMS 前候选数量PyTorch caching allocatorGPU 上还存活的 tensor多模型切换后的缓存和碎片我们当前imgsz 832batch 32本身就会产生比较大的推理峰值。当前默认配置可见于参数定义。十三、当前已经实现的显存优化1.一个时间只加载一个模型不是personhelmetvesttractorslippersmoking全部同时进入 GPU而是load person→ scan→ delete personload helmet→ scan→ delete helmet每个类别完成后执行del modelgc.collect()torch.cuda.empty_cache()当前代码已经这样做。这是目前最重要的显存控制策略。2.使用FP16如果设备是 CUDAhalfTrue使用 FP16 推理。当前代码专门限制只有 CUDA 才启用 half precision。避免 CPU/MPS 等设备误传 FP16。FP16 对 YOLO 推理通常可以明显降低 activation 和 tensor 的显存占用。3. batch推理不是逐张image1image2image3...而是32 张→ 一次 batch inference这样 GPU 利用率更高。4.使用streamTrue当前 model.predict()streamTrue也就是结果逐步消费而不是要求所有结果长期堆在 Python 端。5.每批和模型切换后进行对象清理当前版本在 batch 结束后del resultsgc.collect()torch.cuda.empty_cache()模型完成后再次执行清理。这是目前采取的比较积极的显存回收方案。十四、CPU内存也做了控制不仅需要考虑 GPU 显存还需要考虑普通 RAM。如果一次扫描几十万图片而把所有预测所有 Candidate所有可视化信息全部保存在 list 中内存会不断增长。所以现在 CandidateWriter 的思路是Candidate 产生↓立即写 CSV而不是几百万 Candidate 全部保存在 RAM↓最后再写 CSV当前三个 CSVcandidates_auto.csvcandidates_review.csvcandidates_all.csv都是打开文件后持续流式写。内存中只保留用于最终可视化的有限数量 top sample。例如auto 每类最多 80 张review 每类最多 500 张因此候选数量即使变成几十万RAM 也不会和候选数量线性增长。十五、标签Cache也按单类别释放当前不是一次把6 个类别所有 GT全部加载到 RAM。而是在 person 模型开始时只 preload person labelsperson 完成后del label_cachehelmet 再重新加载 helmet labels。当前 preload_labels() 也是只筛选当前 class_id。这个思路同样是计算时间稍微增加但降低长期内存占用。十六、磁盘空间也做了优化如果最后需要形成一个标准 YOLO 可训练数据集可以使用--materialize-dataset程序生成trainable_dataset/├── images/├── labels/└── data.yaml其中图片默认不是再完整复制一份而优先hardlink这样原始图片新训练数据集图片在文件系统层面可以共享同一份数据块大幅减少磁盘占用。如果 hardlink 不支持再自动 fallback 到 copy。当前逻辑已经实现。十七、当前OOM还有什么不足这个地方汇报时建议如实说。我们之前已经讨论过一个更好的方案batch32OOM↓自动 batch16↓还 OOMbatch8↓直到成功但是我重新检查当前实际脚本后发现这个“自动降batch并重试”的机制目前还没有真正落进当前可下载版本。当前代码仍然是发生 CUDA OOM↓empty_cache↓抛出异常↓提示用户把 batch 减半重新运行当前源码也明确写的是Retry with --batch ...所以汇报时应该说目前已经有单模型串行、FP16、stream 推理和主动显存清理下一轮准备进一步实现 adaptive batch在发生 OOM 时让 batch 自动从 32 降到 16、8、4、2、1而不是整个任务退出。十八、下一轮显存优化方案下一版我准备重点优化以下几个地方。第一自适应batch把--batch 32理解成最大允许 batch。运行32↓ OOM16↓ OOM8↓ success然后从失败位置继续运行。这样不同类别可以自动找到适合自己的 batch。例如person → 32helmet → 32vest → 16tractor → 32slipper → 16smoking → 8因为不同模型本身的显存需求可能不一样。第二OOM重试必须做成事务式这里还有一个比较容易忽略的问题。使用streamTrue以后可能batch 32前 15 张已经返回结果第 16 张 OOM如果前 15 张已经写进CSVlabels然后 batch 缩小重新跑就会产生重复标注。所以正确设计应该是整个 batch 推理完成↓确认没有 OOM↓统一 commit CSV / labels如果中途 OOM整个 batch 不提交任何结果↓降低 batch↓重新运行也就是类似数据库batch inference transaction这是下一轮 OOM 优化里非常重要的正确性问题。第三把不需要继续留在GPU的tensor尽快转到CPU目前xywhnconfidenceclassIoU部分后处理仍然在 GPU tensor 上进行。实际上模型 forward 和 NMS 完成后这些数据量已经非常小。后续可以GPU prediction↓NMS↓tensor.cpu()↓CPU 上完成 IoU / threshold / candidate 构建这样 GPU 只负责最需要 GPU 的模型 forward。可以减少 tensor 生命周期重叠造成的峰值显存。第四不再每个batch都强制empty_cache当前实现比较激进每个 batch↓torch.cuda.empty_cache()这可以帮助释放 cache但频繁调用也会增加 CUDA allocator 重新申请显存的开销。后续可以优化成普通成功 batch→ 不 empty_cacheOOM→ empty_cache模型切换→ empty_cache让 PyTorch 正常利用 caching allocator。目标是在稳定显存和推理吞吐之间取得平衡。第五监控allocated / reserved / free以后发生 OOM 时不能只输出CUDA out of memory而应该记录GPU free memoryPyTorch allocatedPyTorch reservedpeak allocatedbatchimgszmodelclasssplit这样可以判断是真正 batch 太大还是其他程序占 GPU还是 reserved 过高还是长任务出现碎片这样显存优化就从凭经验调 batch变成有监控数据地优化。十九、CPU内存的下一步优化目前最大的 CPU cache 是当前类别的label_cache如果以后数据量从几万张扩大到几十万甚至百万张仍然可以继续优化。现在是一个 class→ train val test label 全部 cache未来可以改成person/train→ 只 cache train→ 完成释放person/val→ cache val→ 完成释放或者按图片 lazy load label这样进一步降低 RAM。不过当前数据规模下优先级没有 GPU OOM 高。二十、计算效率上的一个天然问题同一张图片要推理6次当前方法最大的问题不是内存而是计算量。假设一张图0001.jpg最终需要经过person modelhelmet modelvest modeltractor modelslipper modelsmoking model也就是 decode / preprocess / inference 六次。当前代码虽然把图片路径只扫描一次并复用避免了重复遍历目录但真正的神经网络 inference 仍然是六遍。这是单类模型方案天然的代价。我们暂时接受这个代价因为现阶段更关注补标准确率类别可控性不同类别模型独立优化而不是单纯追求最快速度。如果后续数据量变得非常大可以考虑6 个单类 teacher↓产生高质量补标数据↓训练一个统一的 6-class student model以后新的数据直接1 个模型扫描一次就能完成 6 类检测。这样相当于把当前单类别模型当成teacher ensemble /专项教师模型再通过补标后的数据反哺统一模型。二十一、最终我们会得到哪些结果程序运行完成后输出目录主要包括out_root/│├── labels_autofill_v1/│├── candidates_auto.csv├── candidates_review.csv├── candidates_all.csv│├── auto_samples/│ ├── person/│ ├── helmet/│ ├── vest/│ ├── tractor/│ ├── slipper/│ └── smoking/│├── review_images/│├── summary.json├── summary.txt│└── trainable_dataset/ # 可选其中labels_autofill_v1最重要。原始人工标签高置信度自动补标签但是原始 dataset/labels 完全没有修改。candidates_auto.csv记录到底自动补了哪些框。包括imageclassconfidenceIoUbounding boxcandidates_review.csv记录哪些候选模型认为可能漏标但是置信度不足以自动修改。方便人工二次审核。auto_samples用于抽样检查每个类别自动补标的实际效果。summary统计每类扫描多少图片产生多少 prediction多少与原标签匹配多少 duplicate最终 auto 多少review 多少error 多少当前 summary 已经记录这些核心指标。二十三、这样总结整个迭代过程最初我们只是希望用单类模型扫描数据把漏标补回来。但真正做以后发现它并不是简单的model.predict()→ 写 txt中间逐步暴露出很多工程问题。第一阶段解决的是数据安全问题不修改原始标签而是复制一套 labels在复制本上补。第二阶段解决的是重复标注问题通过同类别 IoU 判断预测是不是原来已经标过。第三阶段解决的是错误自动补标问题使用 auto/review 双阈值只允许高置信度自动写入中间候选人工审核。第四阶段解决的是真实目标被误去重的问题existing IoU 和 candidate duplicate IoU 分离后者提高到 0.95。第五阶段解决的是类别ID污染问题单类模型 class 0 不能直接写回总数据集而是动态读取 data.yaml 的真实 class id。第六阶段解决的是可视化误导问题从“一个框一张图”改成“按类别、按唯一图片抽样”避免多目标图片只显示一个框和文件覆盖。第七阶段解决的是运行安全问题对 output path 做保护防止 --force 配错路径把原数据或者模型删掉。当前代码已经包含这类安全校验。第八阶段开始重点解决大规模推理的GPU / RAM问题已经采用单模型串行FP16batch inferencestream predictionCSV streamingbounded visualization samples模型切换释放CUDA cache 清理下一轮重点实现adaptive batchOOM transaction retryGPU tensor 及时搬 CPU显存状态监控减少过度 empty_cache所以现在这项工作已经从一个简单的自动标注脚本 逐渐变成一个面向大规模YOLO数据集的、安全可回滚、分级补标、可审计、可视化质检并考虑GPU/CPU资源控制的数据增强流水线。二十四、老师如果问“为什么不直接让模型全部重标”我会回答我们没有选择全量覆盖原标签因为模型预测不可能比所有人工标签都可靠。现有人工标注仍然作为Ground Truth baseline模型只负责发现人工可能漏掉的目标。所以是人工 GT 为主模型补漏为辅而不是模型重新定义整个数据集。这样风险小得多。二十五、老师如果问“为什么不一次加载6个模型”可以回答一次加载 6 个模型虽然可能减少模型切换次数但是模型权重CUDA contextbatch activationNMS tensor会共同占用显存。尤其我们输入尺寸是 832并且希望保持较大的 batch因此目前选择时间换空间。一次只保留一个模型整个模型完成后释放再加载下一个。这样对长时间大数据任务更稳定。二十六、老师如果问“40GB显存为什么还会OOM”可以回答显存并不是只存模型权重。实际 peak memory 主要可能来自model weightsbatch × 832 × 832 输入网络多层 feature mapsprediction tensorsNMS intermediate tensorsPyTorch reserved memory之前仍未释放的 GPU tensor因此即使总显存很大batch32imgsz832仍可能在某些图片目标特别多、模型更复杂或者 CUDA 显存碎片较严重时出现峰值 OOM。所以我们的方向不是简单换更大的显卡而是让推理流水线具有自适应资源管理能力。最终目标应该是用户只指定 max_batch32程序自己根据显存32 → 16 → 8 → ...找到当前模型和数据能够稳定运行的最大 batch。二十七、最后一句话总结这项工作的核心不是“让模型替代人工标注”而是利用多个高质量单类别模型作为teacher对已有人工标注数据进行保守的漏标发现和增量修复通过原标签保护、IoU匹配、AUTO/REVIEW分级、候选去重、类别映射、按类别可视化抽查以及资源控制使补标结果既能提升数据完整性又能够回滚、检查和追踪。下一步重点是继续把显存管理从人工遇到 OOM 后调 batch升级到系统自动感知OOM、自适应batch、事务式重试并记录GPU使用情况。
返回列表