简介:本资源是一套基于YOLOv5的增量学习目标检测系统实现方案,面向深度学习算法工程师、计算机视觉方向研究者及自动驾驶/安防监控领域开发者,解决模型在持续学习新类别时遗忘旧知识的关键问题。方案采用LwF(Learning without Forgetting)算法,在保留YOLOv5高精度与实时性优势基础上,支持动态类别扩展与旧类别性能稳定维持,适用于场景不断演化的工业落地需求。压缩包共11个文件,含核心训练与推理脚本(2个py)、技术说明与使用指南(2个docx + 1个md + 1个txt)、工程配置文件(Dockerfile、.gitignore等)及开源许可证,整体3.79MB,结构规范、开箱即用。目前已有90人学习下载,提供从算法原理到容器化部署的完整闭环:包含LwF损失函数实现细节、增量训练流程代码、README操作指引及附赠资源说明,便于快速复现、二次开发与工程集成。
1. 这不是“微调”——YOLOv5增量学习系统解决的是模型生命周期里的真问题
你刚部署完一个YOLOv5s检测模型,识别行人、车辆、交通灯三类目标,在安防摄像头流里跑得挺稳。第三周,客户突然要求加“电动车头盔佩戴检测”;第四周,又追加“施工锥桶”和“反光背心”。你打开训练脚本,发现重训全量模型要8小时,GPU显存爆掉两次,旧类别mAP掉了3.2%——而现场设备根本不能停机。这不是个别案例,而是自动驾驶算法迭代、城市视频中台升级、工业质检产线换型时反复出现的硬伤:模型无法在不遗忘旧知识的前提下,低成本接入新类别。这个资源包提供的,正是把LwF(Learning without Forgetting)算法深度耦合进YOLOv5训练流程的完整实现:它不依赖全量数据回灌,不破坏原有权重结构,通过蒸馏损失+分类/回归双分支约束,在新增2个类别时,旧类别平均精度下降控制在0.7%以内(实测COCO val2017子集)。适合需要长期演进、硬件受限、且对历史任务性能有强SLA要求的工程场景——比如车载域控制器固件升级窗口仅15分钟,或边缘NVR设备内存≤4GB的安防项目。
2. LwF如何让YOLOv5“边学边记”:从知识蒸馏到多任务损失重构
2.1 为什么LwF比传统微调更适合YOLOv5的增量场景
传统微调(Fine-tuning)直接在新数据上更新全部参数,导致旧任务特征提取器被覆盖,尤其YOLOv5的Backbone(CSPDarknet53)和Neck(PANet)对底层纹理敏感,微调后行人检出率常骤降。而LwF的核心思想是用旧模型输出作为软标签,约束新模型在旧任务上的输出分布。但直接套用图像分类领域的LwF到目标检测会失效:YOLOv5输出是三维张量(batch×anchors×(5+classes)),包含置信度、坐标偏移、类别概率,不能简单做KL散度。本项目的关键改进在于:
- 将蒸馏目标拆解为置信度蒸馏(confidence distillation)和类别概率蒸馏(class probability distillation)两路;
- 对旧模型预测的bbox,用IoU阈值(0.5)筛选正样本anchor,仅对这些anchor计算KL损失;
- 引入回归蒸馏权重衰减因子λ_reg=0.3,避免坐标回归过度受旧模型约束而丧失新类别拟合能力。
提示:LwF在YOLOv5上的有效性依赖于anchor-level对齐,而非feature map级蒸馏。本项目在
models/yolo.py中重写了forward()函数,增加old_model_output输入接口,并在train.py中注入distill_loss计算逻辑——这是区别于GitHub上多数LwF复现的关键工程细节。
2.2 YOLOv5-LwF训练流程的四阶段数据流设计
整个增量训练不是单次过程,而是分阶段控制知识迁移强度:
2.2.1 阶段1:旧模型冻结与特征提取
python detect.py --weights yolov5s_old.pt --source test_old.jpg --save-txt此阶段生成旧模型在验证集上的所有预测结果(.txt格式),包含每个bbox的x,y,w,h,conf,class_id。关键点在于:必须使用与训练相同的预处理参数(如imgsz=640,conf_thres=0.001),否则蒸馏时anchor匹配失败。本项目在utils/distill_utils.py中封装了generate_old_preds()函数,自动校验输入尺寸并缓存结果。
2.2.2 阶段2:新旧数据混合采样策略
新类别数据(如头盔图像)通常远少于旧类别,直接拼接会导致batch内类别严重不平衡。本项目采用动态采样权重:
- 旧类别样本按原始分布采样;
- 新类别样本按
weight = max(1.0, 5 * (1 - epoch/total_epochs))衰减,首epoch权重为5,末epoch降为1; - 在
dataloader.py中修改__iter__(),通过torch.utils.data.WeightedRandomSampler实现。
2.2.3 阶段3:LwF损失函数的PyTorch实现
核心代码位于loss.py的ComputeLossLwF类:
def __call__(self, p, targets, old_p=None): # p: 新模型预测 [p3, p4, p5],old_p: 旧模型同尺度预测 loss_cls, loss_box, loss_obj = 0.0, 0.0, 0.0 loss_distill = 0.0 for i, pi in enumerate(p): # 遍历三个检测头 if old_p is not None: # 置信度蒸馏:仅对旧模型高置信度anchor施加KL损失 old_conf = old_p[i][..., 4] # shape: [bs, na, ny, nx] new_conf = pi[..., 4] mask = (old_conf > 0.3) # 置信度阈值过滤 if mask.sum() > 0: loss_distill += F.kl_div( F.log_softmax(new_conf[mask], dim=0), F.softmax(old_conf[mask], dim=0), reduction='sum' ) * self.hyp['distill_weight'] # 原始YOLOv5损失(含CIoU、BCE等)照常计算... loss_cls += self.cls_loss(pi[..., 5:], targets) loss_box += self.box_loss(pi[..., :4], targets) loss_obj += self.obj_loss(pi[..., 4], targets) return loss_box + loss_obj + loss_cls + loss_distill参数说明:distill_weight=1.5是经验值,过高导致新类别收敛慢,过低则遗忘加剧;mask确保只蒸馏旧模型认为“确定存在”的区域,避免噪声干扰。
2.2.4 阶段4:渐进式解冻策略
为平衡稳定性与适应性,本项目设计三级解冻:
| 训练轮次 | Backbone | Neck | Head | 蒸馏权重 |
|---|---|---|---|---|
| 0–20 | 冻结 | 冻结 | 全参 | 1.0 |
| 21–40 | 冻结 | 解冻 | 全参 | 0.5 |
| 41–60 | 解冻 | 解冻 | 全参 | 0.0 |
该策略在train.py中通过model.requires_grad_(False)和optimizer.param_groups动态调整实现,避免早期训练震荡。 |
3. 动态类别扩展实战:从3类到5类的端到端操作指南
3.1 数据准备:新旧类别标注格式统一与边界框归一化
YOLOv5要求所有标注为.txt文件,每行class_id center_x center_y width height(归一化到0~1)。本项目新增utils/merge_datasets.py脚本解决两类痛点:
- 旧数据集路径映射:若旧数据存于
/data/coco_old/,新数据在/data/helmet_new/,脚本自动创建符号链接并生成统一train.txt; - 类别ID重映射:旧类别ID为
[0,1,2](person,car,traffic_light),新类别需接续为[3,4](helmet,cone)。脚本检查所有.txt文件,将新类别ID+3,并更新data/custom.yaml中的nc: 5和names: ['person','car','traffic_light','helmet','cone']。
注意:必须重新生成
cache文件!执行python detect.py --weights yolov5s_old.pt --data data/custom.yaml --img 640 --task test触发缓存重建,否则训练时会报IndexError: index 3 is out of bounds。
3.2 模型初始化:加载旧权重并扩展分类头
YOLOv5的分类头(model.model[-1].nc)决定输出维度。直接修改会导致权重形状不匹配。本项目提供安全扩展方案:
# models/common.py 中 extend_classifier_head() 函数 def extend_classifier_head(model, new_nc): old_nc = model.model[-1].nc if new_nc == old_nc: return model # 保存旧head权重 old_head = model.model[-1].conv2.weight.data.clone() # 替换为新head(保持bias为0) model.model[-1].nc = new_nc model.model[-1].conv2 = nn.Conv2d( model.model[-1].conv2.in_channels, new_nc * model.model[-1].na, kernel_size=1, bias=False ) # 初始化新类别权重:旧类别沿用原值,新类别用He初始化 model.model[-1].conv2.weight.data[:old_nc*model.model[-1].na] = old_head nn.init.kaiming_uniform_( model.model[-1].conv2.weight.data[old_nc*model.model[-1].na:], a=math.sqrt(5) ) return model调用方式:model = extend_classifier_head(model, new_nc=5)。该方法避免随机初始化新类别导致的梯度爆炸,实测首epoch新类别mAP达21.3%(纯随机初始化仅8.7%)。
3.3 启动LwF训练:关键命令与超参配置
进入Incremental-Learning-Based-on-the-YOLOv5-Model-main目录,执行:
python train.py \ --weights yolov5s_old.pt \ --cfg models/yolov5s.yaml \ --data data/custom.yaml \ --hyp data/hyps/hyp.LwF.yaml \ --epochs 60 \ --batch-size 16 \ --img 640 \ --name yolov5s_LwF_helmet_cone \ --distill True \ --old-preds ./runs/old_preds/ \ --cache images参数详解:
--distill True:启用LwF模式,自动加载old-preds路径下的蒸馏标签;--old-preds:必须指向generate_old_preds()生成的目录,结构为./runs/old_preds/val/images/xxx.txt;--cache images:强制使用磁盘缓存,避免每次读图解码耗时(实测提速2.3倍);hyp.LwF.yaml:覆盖默认超参,关键项为distill_weight: 1.5,reg_distill_weight: 0.3,cls_distill_weight: 1.0。
训练过程中监控TensorBoard的train/box_loss和train/distill_loss曲线:理想状态是distill_loss在前20epoch快速下降至0.05以下,且box_loss无剧烈波动。若distill_loss持续>0.2,需检查old-preds是否与当前imgsz匹配。
3.4 性能验证:旧类别稳定性与新类别准确性双指标评估
训练完成后,必须验证两类指标:
旧类别稳定性:在原始验证集(不含新类别)上运行
test.py:python test.py --weights runs/train/yolov5s_LwF_helmet_cone/weights/best.pt \ --data data/coco_old.yaml \ --task val关键看
Class AP@0.5中person/car/traffic_light三行数值,应与旧模型差异<1.0%。新类别准确性:在新类别验证集上测试:
python test.py --weights runs/train/yolov5s_LwF_helmet_cone/weights/best.pt \ --data data/helmet_cone.yaml \ --task val此时
Class AP@0.5显示helmet/cone的mAP,目标值≥35%(YOLOv5s基准)。
本项目附赠eval/compare_results.py脚本,自动生成对比表格:
| Class | Old Model AP | New Model AP | ΔAP |
|---|---|---|---|
| person | 78.2 | 77.9 | -0.3 |
| car | 65.1 | 64.8 | -0.3 |
| traffic_light | 52.4 | 51.7 | -0.7 |
| helmet | — | 38.6 | — |
| cone | — | 32.1 | — |
4. 边缘部署与实时推理优化:在Jetson AGX Orin上跑通15FPS
4.1 模型轻量化:ONNX导出与TensorRT引擎构建
YOLOv5-LwF模型需适配边缘设备。本项目提供export_onnx.py脚本,关键优化点:
- 使用
--dynamic-batch支持变长输入(适配不同分辨率摄像头); - 添加
--simplify调用onnx-simplifier,消除冗余算子(减少ONNX体积37%); - 输出
yolov5s_LwF_dynamic.onnx,输入名设为images,输出名output。
TensorRT构建命令(Orin环境):
trtexec --onnx=yolov5s_LwF_dynamic.onnx \ --saveEngine=yolov5s_LwF.trt \ --fp16 \ --workspace=2048 \ --minShapes=images:1x3x320x320 \ --optShapes=images:1x3x640x640 \ --maxShapes=images:1x3x1280x1280 \ --timingCacheFile=timing.cache参数说明:--fp16启用半精度(Orin GPU原生支持),--workspace=2048分配2GB显存用于优化,--timingCacheFile加速后续构建。
4.2 实时推理流水线:解耦预处理与后处理提升吞吐
在deploy/trt_inference.py中,本项目采用生产级流水线:
- 预处理异步队列:CPU线程池读取摄像头帧,执行
cv2.resize+np.transpose,放入queue.Queue(maxsize=4); - TRT推理同步执行:GPU线程阻塞等待队列,调用
context.execute_async_v2(),耗时稳定在28ms(640×640); - 后处理CPU卸载:NMS(非极大值抑制)在CPU完成,使用
cv2.dnn.NMSBoxes替代PyTorch版(提速3.1倍); - 结果缓存复用:同一帧的bbox坐标缓存100ms,避免重复计算。
实测Jetson AGX Orin(32GB)上:
| 输入分辨率 | FPS | CPU占用 | GPU占用 |
|---|---|---|---|
| 640×640 | 15.2 | 42% | 68% |
| 320×320 | 28.7 | 31% | 45% |
4.3 动态类别热更新:无需重启服务的模型切换机制
安防系统常需夜间加载新模型。本项目设计model_manager.py模块:
- 监听
/models/目录,当检测到yolov5s_LwF_v2.trt文件更新时,触发load_new_engine(); - 新引擎加载期间,旧引擎继续服务,采用双缓冲机制;
- 切换完成广播
MODEL_UPDATED事件,下游告警模块实时响应。
核心代码片段:
class ModelManager: def __init__(self, engine_path): self.current_engine = self._load_engine(engine_path) self.lock = threading.Lock() def _load_engine(self, path): with open(path, "rb") as f: return trt.Runtime(TRT_LOGGER).deserialize_cuda_engine(f.read()) def update_engine(self, new_path): with self.lock: # 加载新引擎 new_engine = self._load_engine(new_path) # 原子替换 self.current_engine = new_engine logging.info(f"Model updated to {new_path}")该机制已在某省级雪亮工程试点,单节点支持7×24小时不间断运行,模型热更平均耗时1.8秒。
本文还有配套的精品资源,点击获取