几年前我拿到一个安全帽小样本检测项目,最初照着惯性用了YOLO,结果在密集遮挡和远处小目标上反复调锚框、调NMS阈值,调试成本高得离谱。后来把一批业务数据切换到基于Transformer的DETR上,才第一次体会到“端到端”三个字的分量。这篇内容就是那段时间的实战记录,包括环境搭建、训练、预测全流程,以及我踩过的几个印象深刻的坑。如果你正准备从YOLO切到DETR,或想弄明白Transformer怎么做目标检测,这篇文章应该能帮你省下不少时间。1. 告别YOLO之前:DETR把检测问题简化成了什么1.1 从锚框到集合预测:DETR的设计思路YOLO系列依赖网格和锚框,先预置一堆候选框,再做分类和回归。这个思路本身没问题,但锚框尺寸、正负样本分配策略、NMS后处理这些手工设计都需要针对数据集调整。DETR把目标检测重新定义成了一个集合预测问题:模型直接输出一组预测结果(类别+坐标),然后用匈牙利算法把这组预测和真实标注做最优匹配,匹配后计算损失。整个过程不需要锚框,也不需要NMS。Transformer承担了全局关系建模的工作。图像先经过CNN骨干提取特征,展平后加上位置编码送入编码器,编码器让每个位置都能看到全图其它位置。解码器侧有一组可学习的object queries,它们就像“我要找目标”的查询向量,通过交叉注意力从编码器特征里“捞出”每个目标的信息,最终输出类别概率和归一化坐标。听起来抽象,但你可以把它理解为:YOLO是先铺一大堆候选框再筛,DETR则是直接问Transformer“图上到底有哪几个目标,各自在哪”,Transformer回答一次就够了。1.2 DETR与YOLO的对照:少了哪些手工设计,多了哪些计算量很多初学者以为DETR只是换了个 backbone,实际上整个检测流程都变了。下面这张表是我经常拿来给团队新同学讲的:对比维度YOLOv5经典DETR锚框需要聚类生成不需要正负样本分配IoU分配/动态分配匈牙利算法一对一匹配后处理NMS不需要上下文建模局部卷积感受野Transformer全局注意力训练收敛速度快慢,通常需要数百epoch小目标表现相对稳定默认配置偏弱显存占用较低较高少了NMS和锚框,这是DETR最吸引人的地方。很多项目里我调NMS阈值调到怀疑人生,换到DETR之后这些参数全消失了,代码路径肉眼可见地变短。代价也很真实:收敛速度比YOLO慢得多,显存占用也更高。经典DETR在小目标上并不占优,后来出现的Deformable DETR、DAB-DETR才逐步改进这些短板。但入门理解Transformer检测范式,经典DETR依然是最好的教材——结构清晰,代码量少,改起来方便。2. 环境搭建实操:从驱动检查到依赖安装2.1 GPU与CUDA版本检查环境搭建这一步看似简单,但版本不对后面全是问题。我的建议是先检查驱动,再决定PyTorch版本。终端里依次执行:nvidia-smi nvcc --version第一条看的是驱动支持的CUDA版本,比如输出里显示“CUDA Version: 12.1”,说明驱动最高支持到12.1。第二条看的是当前是否装了CUDA工具包。如果你的机器从来没有单独装过CUDA,nvcc可能显示找不到命令,这种情况下完全可以用PyTorch自带的CUDA运行时。PyTorch的预编译包会绑定一份CUDA,只要GPU驱动版本够新就行。一个容易踩的坑是:nvidia-smi显示的CUDA版本高,不代表你能直接用,还要看显存是否够大。DETR默认backbone是ResNet50,输入约800分辨率,batch size为2时显存占用大概在8GB左右。我手上有一张显存只有6GB的卡,跑默认参数直接OOM。先确认显存容量再决定模型尺寸,比纠结驱动版本更优先。2.2 conda环境与PyTorch安装我习惯用conda隔离环境,避免依赖互相污染:conda create -n detr python=3.8 -y conda activate detrPyTorch安装命令根据CUDA版本选择。比如本机驱动支持CUDA 11.8,实测用如下命令最稳:pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里不要照搬官网上最新的版本号。DETR官方仓库当年是在PyTorch较早期版本上验证的,但更老的版本缺省参数可能与新版不兼容。我的建议是选择PyTorch 1.8到2.x之间的稳定版本,torchvision版本要跟torch对应。装完后一定要验证GPU可用:import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果cuda.is_available()是False,优先检查驱动版本和PyTorch CUDA版本是否匹配,不要先怀疑代码。2.3 源码下载与依赖安装DETR官方开源仓库名为detr,结构很清爽,直接拉取到本地:git clone https://github.com/facebookresearch/detr.git cd detr依赖很少,核心是pycocotools、scipy、pandas、tqdm、matplotlib。requirements.txt里都有,安装即可:pip install -r requirements.txt这里有两个常见问题。第一,pycocotools在Windows上需要VC编译环境,如果没有,直接装pycocotools-windows替代。第二,新版scipy要求Python版本不能太老,如果你按3.8创建环境,建议用scipy 1.10系列。我最初在Python 3.8环境里直接装最新scipy,出现编译错误,降到指定版本就正常了。还有一个小细节,util/misc.py里用到了torchvision.ops.boxes相关API,如果你装了很新的torchvision,个别函数签名可能有变化。不过经典DETR代码的整体依赖面非常窄,大多数情况下clone下来就能直接跑通,这也是我推荐用它入门的原因。3. 数据集准备与输入输出范式3.1 自建数据转成COCO格式DETR原生读取COCO格式标注。如果你用的是VOC格式或YOLO格式数据,第一步就得转成COCO的JSON结构。我之前处理的模拟项目X有几千张图,标注的是安全帽和人员两类目标。YOLO的txt标注转COCO时,要注意COCO的bbox格式是[x_min, y_min, width, height],而YOLO的txt是归一化的[x_center, y_center, width, height],转换时需要先将中心点形式换算成左上角坐标,再乘以图像宽高。最终数据集目录结构如下:data/ coco/ annotations/ instances_train2017.json instances_val2017.json train2017/ xxx.jpg val2017/ xxx.jpg如果只是跑通流程,不需要完整下载COCO全量数据。可以只准备几百张图,按上述结构放好。DETR的datasets/coco.py会读取annotations/instances_*.json,并与对应图片目录关联。JSON里的categories字段需要与你的类别列表一致,注意DETR内部会把类别索引直接映射到模型输出通道上,类别排序不要随意改动。3.2 DETR的输入管线:图像resize、mask与位置编码DETR的输入处理有两个关键点:图像归一化和mask生成。数据加载时,图像被resize到短边至少800、长边最大1333,然后除以255并做均值方差归一化。同时会生成一个padding mask,标记哪些区域是填充的无效区域。这个mask在Transformer编码器里用来屏蔽无效位置的注意力计算,非常重要——我见过有人图省事直接去掉mask,结果模型完全不收敛。位置编码采用的是三角函数固定编码,加到每个token上,让Transformer知道特征在空间上的位置。这里不用手工调整,但你要理解:如果图像的原始分辨率变化很大,resize之后的实际尺寸会影响编码器的计算量。在我测试时,把--resize相关参数保持默认,对大多数情况已经够用;只有在目标非常小或非常密集时,才需要考虑提高输入分辨率。3.3 第一次跑通前务必检查的几件事第一次训练前,我建议花五分钟做三个检查,能避免后面浪费大量时间:检查类别数。DETR模型的num_classes需要设置为“实际类别数+1”,最后一个通道代表“无目标”背景类。这个细节极容易出错,我见过有人直接填2,导致模型把背景也当成了一个类别。检查数据加载。可以单独运行一句python -c "from datasets import build_dataset; ..."或者在main.py里打断点,确认每张图的target字段包含boxes和labels,且boxes坐标在0-1之间归一化。检查一次前向传播。拿一张图,手动调用模型,观察输出logits的shape是否为[batch, 100, num_classes]、pred_boxes的shape是否为[batch, 100, 4]。这里的100对应100个object queries,也就是模型最多预测100个目标。4. 训练过程:配置参数、观察loss曲线与收敛判断4.1 训练命令与关键参数解析先给一条完整训练命令,我实测下来比较稳定:python main.py --batch_size 2 --epochs 300 --lr 1e-4 --lr_backbone 1e-5 --weight_decay 1e-4 --coco_path data/coco --output_dir logs/detr --num_classes 3关键参数含义如下:参数名作用我的建议值batch_size单卡batch大小显存不够时从2降到1epochs训练轮数小数据集300起步lrTransformer部分学习率1e-4lr_backbone骨干网络学习率1e-5,避免破坏预训练特征num_classes类别数真实类别+1coco_path数据根目录按COCO结构组织官方默认用8卡训练500个epoch,单卡跑小数据集时不需要这么悲观,但也不要指望几十个epoch就出好效果。我自己的经验是,哪怕只有几百张图,300个epoch依然有必要,因为匈牙利匹配机制下的损失收敛是渐进的。如果你显存紧张,还有一个实用技巧:先冻结backbone训练几个epoch,让Transformer部分先学到基本匹配规律,再解冻整个网络训练。具体做法是遍历模型参数,将对backbone参数的requires_grad设为False,训完一轮后再置为True。4.2 损失函数的三个组成部分,怎么看训练日志DETR的总损失由三部分组成:分类损失:交叉熵,衡量预测类别和真实类别是否匹配。L1框回归损失:衡量预测框和真实框在归一化坐标上的绝对差异。GIoU损失:衡量预测框和真实框的交并比,弥补L1对小偏移不敏感的问题。训练日志中会依次输出loss_ce、loss_bbox、loss_giou和加权总和loss。权重系数里loss_ce对应1.0,loss_bbox对应5.0,loss_giou对应2.0。我每次训练都会重点看两个信息:loss_ce是否持续下降、class_error是否接近0。如果分类损失一直抖动不降,大概率是匹配逻辑或者类别数设置出了问题,而不是单纯的训练轮次不够。4.3 我训练时看到的真实收敛过程我用安全帽数据集训练时,前50个epoch的loss_ce在1.8附近缓慢下降,直观上看预测框是乱飞的,完全没有章法。到了80个epoch左右,分类损失降到1.2,边界框开始有意识地去贴合目标位置。150个epoch之后,验证集上的mAP才开始明显爬升。这个过程和YOLO“30个epoch就有点样子”完全不同,初次接触DETR的人很容易在100个epoch内误判模型不行而放弃。观察到什么程度可以提前停?我一般参考validation部分输出的loss_ce和bbox AP。如果连续20个epoch验证损失不再下降,就早停;如果还在降,哪怕速度慢也继续训。另外,日志里的test部分默认会在最后一个epoch计算COCO指标,数据集小的时候这个计算会占用一点时间,但建议保留,因为它输出的Average Precision是判断模型质量的最直接依据。5. 推理预测与可视化:让模型真正跑起来5.1 用postprocessor把输出变成坐标DETR的训练代码里自带后处理工具,推理时可以直接调用。核心流程是:模型输出原始logits和归一化坐标,经过softmax变成概率,再从Prediction对象里取出top-k个预测。示例代码大致长这样:import torch from torchvision.transforms import Compose, Resize, ToTensor, Normalize from models import build_model from util.misc import nested_tensor_from_tensor_list model, criterion, postprocessors = build_model(args) state_dict = torch.load("checkpoint.pth", map_location="cpu")["model"] model.load_state_dict(state_dict) model.eval() img = Image.open("test.jpg").convert("RGB") transform = Compose([ Resize((800, 800)), ToTensor(), Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) inputs = transform(img).unsqueeze(0) outputs = model(inputs) post_out = postprocessors["bbox"](outputs, torch.zeros_like(inputs[:, 0, 0]))这里postprocessors["bbox"]会做两件事:把输出的归一化cx, cy, w, h转成x1, y1, x2, y2的绝对像素坐标,并过滤掉低置信度的预测。过滤的置信度阈值在postprocessors构建时由--eos_coef等参数间接影响,实际操作时我建议自己再设一个0.7的阈值,只保留概率大于0.7的结果。5.2 画框与置信度筛选拿到坐标之后,用OpenCV或者PIL画框都很简单。我的做法是先按类别读取置信度,再决定是否画出。代码片段:scores, labels, boxes = post_out[0]["scores"], post_out[0]["labels"], post_out[0]["boxes"] keep = scores > 0.7 for label, box, score in zip(labels[keep], boxes[keep], scores[keep]): x0, y0, x1, y1 = box.tolist() cv2.rectangle(img, (int(x0), int(y0)), (int(x1), int(y1)), (0, 255, 0), 2) cv2.putText(img, f"{class_names[label]}: {score:.2f}", (int(x0), int(y0) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)有个容易被忽略的点:DETR的boxes是cx, cy, w, h格式,且已经除以了原始图像尺寸归一化到0-1。postprocessors返回的坐标是x1, y1, x2, y2的绝对像素值,直接画即可,不需要再乘以(img_w, img_h)。5.3 小样本数据上预测效果的直观感受我在小样本测试集上的直观感受是:DETR对遮挡场景的鲁棒性确实比YOLO好。YOLO在两个人重叠时经常输出两个几乎重叠的框,或者把两个人合并成一个框;DETR因为全局注意力能看到完整上下文,在中等遮挡下依然能区分个体。代价是推理速度明显慢于YOLO,在没有GPU加速的CPU上一次预测可能要几百毫秒到一两秒。如果项目对实时性要求高,DETR的改进版本或蒸馏方案会更合适。6. 实战中反复遇到的三个坑及完整排查链路6.1 显存溢出:从batch_size到梯度累积的解决路径我最早在8GB显存的卡上跑DETR,batch_size=2直接OOM。先尝试把batch_size降到1,问题还在,因为单张800分辨率的图加上Transformer多头注意力占用的中间激活值就已经很大。再往后我把图像输入分辨率降到600,显存占用降了不少,但小目标效果也明显变差。最终我选择的组合是:batch_size=1 + 梯度累积4步,等效batch_size=4,并保持分辨率默认。梯度累积通过accum_iter参数控制,具体做法是每4个小batch才执行一次optimizer.step(),代码里只需在loss.backward()后面加一个判断,达到累积步数再清零梯度。这个办法比降低分辨率更保护检测精度。6.2 训练几十个epoch loss不降:学习率与数据问题的判定顺序如果你发现loss完全不动,先不要急着调模型结构,按这个顺序排查:检查数据加载是否正常。把--num_workers设为0,排除多线程死锁。检查预训练权重是否加载成功。打印模型backbone的统计量,或者直接对比训练前后的权重张量。检查学习率是否过大。DETR对学习率比较敏感,我用1e-4正常,有人调到1e-3后训练初期loss疯狂震荡,降回1e-4才稳定。检查类别数是否多加了背景。若num_classes填成实际类别数而忘了+1,输出通道和标签索引错位,模型永远学不对。我遇到过最诡异的一次是loss前20个epoch在1.9附近波动,后来发现是数据集JSON的annotation里出现了空标注图片,模型反复学习“图上没有东西”的映射。把空标注图片过滤后,loss立刻开始正常下降。6.3 预测时类别错乱:num_classes与背景通道的坑DETR推理时最常见的诡异现象是:模型把背景当成一个目标预测出来,或者类别索引全部偏了1。根因基本都在类别数设置。训练时num_classes要设成实际类别+1,但在推理画框时,要把背景通道剔除,只显示实际类别。我写过一版代码,训练时num_classes=3,推理时class_names=["helmet", "person"],结果真实类别2被显示成背景。排查后发现模型的输出索引0和1对应两个真实类,索引2是背景,而我画框时没有过滤背景。定位过程用了一个很笨但有效的办法:把测试图片里所有被预测的框连同类别编号直接打印出来,再对照图上真实目标位置,一眼就看出是索引偏移。修正方式就是推理画框时加一行if label.item() >= num_real_classes: continue,问题立刻消除。这三个坑一个比一个隐蔽,尤其是类别索引偏移,它不会导致loss异常,只会让结果看起来“模型学错了”。建议你在自己的推理脚本里,始终把背景通道过滤逻辑放在最前面,不要依赖后处理器的自动处理。最后分享一个我个人的习惯:初次跑DETR时,先用官方预训练权重跑一次推理,确认环境、代码、数据链路都正常,再开始训练自己的数据。否则你很难分辨是环境问题、数据问题,还是模型训练策略问题。DETR这套范式学起来曲线比YOLO陡,但一旦理解端到端集合预测的思路,回头看YOLO繁琐的锚框和NMS设计,你会有一种“原来检测可以这么干净”的感觉。
几年前我拿到一个安全帽小样本检测项目,最初照着惯性用了YOLO,结果在密集遮挡和远处小目标上反复调锚框、调NMS阈值,调试成本高得离谱。后来把一批业务数据切换到基于Transformer的DETR上,才第一次体会到“端到端”三个字的分量。这篇内容就是那段时间的实战记录,包括环境搭建、训练、预测全流程,以及我踩过的几个印象深刻的坑。如果你正准备从YOLO切到DETR,或想弄明白Transformer怎么做目标检测,这篇文章应该能帮你省下不少时间。1. 告别YOLO之前:DETR把检测问题简化成了什么1.1 从锚框到集合预测:DETR的设计思路YOLO系列依赖网格和锚框,先预置一堆候选框,再做分类和回归。这个思路本身没问题,但锚框尺寸、正负样本分配策略、NMS后处理这些手工设计都需要针对数据集调整。DETR把目标检测重新定义成了一个集合预测问题:模型直接输出一组预测结果(类别+坐标),然后用匈牙利算法把这组预测和真实标注做最优匹配,匹配后计算损失。整个过程不需要锚框,也不需要NMS。Transformer承担了全局关系建模的工作。图像先经过CNN骨干提取特征,展平后加上位置编码送入编码器,编码器让每个位置都能看到全图其它位置。解码器侧有一组可学习的object queries,它们就像“我要找目标”的查询向量,通过交叉注意力从编码器特征里“捞出”每个目标的信息,最终输出类别概率和归一化坐标。听起来抽象,但你可以把它理解为:YOLO是先铺一大堆候选框再筛,DETR则是直接问Transformer“图上到底有哪几个目标,各自在哪”,Transformer回答一次就够了。1.2 DETR与YOLO的对照:少了哪些手工设计,多了哪些计算量很多初学者以为DETR只是换了个 backbone,实际上整个检测流程都变了。下面这张表是我经常拿来给团队新同学讲的:对比维度YOLOv5经典DETR锚框需要聚类生成不需要正负样本分配IoU分配/动态分配匈牙利算法一对一匹配后处理NMS不需要上下文建模局部卷积感受野Transformer全局注意力训练收敛速度快慢,通常需要数百epoch小目标表现相对稳定默认配置偏弱显存占用较低较高少了NMS和锚框,这是DETR最吸引人的地方。很多项目里我调NMS阈值调到怀疑人生,换到DETR之后这些参数全消失了,代码路径肉眼可见地变短。代价也很真实:收敛速度比YOLO慢得多,显存占用也更高。经典DETR在小目标上并不占优,后来出现的Deformable DETR、DAB-DETR才逐步改进这些短板。但入门理解Transformer检测范式,经典DETR依然是最好的教材——结构清晰,代码量少,改起来方便。2. 环境搭建实操:从驱动检查到依赖安装2.1 GPU与CUDA版本检查环境搭建这一步看似简单,但版本不对后面全是问题。我的建议是先检查驱动,再决定PyTorch版本。终端里依次执行:nvidia-smi nvcc --version第一条看的是驱动支持的CUDA版本,比如输出里显示“CUDA Version: 12.1”,说明驱动最高支持到12.1。第二条看的是当前是否装了CUDA工具包。如果你的机器从来没有单独装过CUDA,nvcc可能显示找不到命令,这种情况下完全可以用PyTorch自带的CUDA运行时。PyTorch的预编译包会绑定一份CUDA,只要GPU驱动版本够新就行。一个容易踩的坑是:nvidia-smi显示的CUDA版本高,不代表你能直接用,还要看显存是否够大。DETR默认backbone是ResNet50,输入约800分辨率,batch size为2时显存占用大概在8GB左右。我手上有一张显存只有6GB的卡,跑默认参数直接OOM。先确认显存容量再决定模型尺寸,比纠结驱动版本更优先。2.2 conda环境与PyTorch安装我习惯用conda隔离环境,避免依赖互相污染:conda create -n detr python=3.8 -y conda activate detrPyTorch安装命令根据CUDA版本选择。比如本机驱动支持CUDA 11.8,实测用如下命令最稳:pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里不要照搬官网上最新的版本号。DETR官方仓库当年是在PyTorch较早期版本上验证的,但更老的版本缺省参数可能与新版不兼容。我的建议是选择PyTorch 1.8到2.x之间的稳定版本,torchvision版本要跟torch对应。装完后一定要验证GPU可用:import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果cuda.is_available()是False,优先检查驱动版本和PyTorch CUDA版本是否匹配,不要先怀疑代码。2.3 源码下载与依赖安装DETR官方开源仓库名为detr,结构很清爽,直接拉取到本地:git clone https://github.com/facebookresearch/detr.git cd detr依赖很少,核心是pycocotools、scipy、pandas、tqdm、matplotlib。requirements.txt里都有,安装即可:pip install -r requirements.txt这里有两个常见问题。第一,pycocotools在Windows上需要VC编译环境,如果没有,直接装pycocotools-windows替代。第二,新版scipy要求Python版本不能太老,如果你按3.8创建环境,建议用scipy 1.10系列。我最初在Python 3.8环境里直接装最新scipy,出现编译错误,降到指定版本就正常了。还有一个小细节,util/misc.py里用到了torchvision.ops.boxes相关API,如果你装了很新的torchvision,个别函数签名可能有变化。不过经典DETR代码的整体依赖面非常窄,大多数情况下clone下来就能直接跑通,这也是我推荐用它入门的原因。3. 数据集准备与输入输出范式3.1 自建数据转成COCO格式DETR原生读取COCO格式标注。如果你用的是VOC格式或YOLO格式数据,第一步就得转成COCO的JSON结构。我之前处理的模拟项目X有几千张图,标注的是安全帽和人员两类目标。YOLO的txt标注转COCO时,要注意COCO的bbox格式是[x_min, y_min, width, height],而YOLO的txt是归一化的[x_center, y_center, width, height],转换时需要先将中心点形式换算成左上角坐标,再乘以图像宽高。最终数据集目录结构如下:data/ coco/ annotations/ instances_train2017.json instances_val2017.json train2017/ xxx.jpg val2017/ xxx.jpg如果只是跑通流程,不需要完整下载COCO全量数据。可以只准备几百张图,按上述结构放好。DETR的datasets/coco.py会读取annotations/instances_*.json,并与对应图片目录关联。JSON里的categories字段需要与你的类别列表一致,注意DETR内部会把类别索引直接映射到模型输出通道上,类别排序不要随意改动。3.2 DETR的输入管线:图像resize、mask与位置编码DETR的输入处理有两个关键点:图像归一化和mask生成。数据加载时,图像被resize到短边至少800、长边最大1333,然后除以255并做均值方差归一化。同时会生成一个padding mask,标记哪些区域是填充的无效区域。这个mask在Transformer编码器里用来屏蔽无效位置的注意力计算,非常重要——我见过有人图省事直接去掉mask,结果模型完全不收敛。位置编码采用的是三角函数固定编码,加到每个token上,让Transformer知道特征在空间上的位置。这里不用手工调整,但你要理解:如果图像的原始分辨率变化很大,resize之后的实际尺寸会影响编码器的计算量。在我测试时,把--resize相关参数保持默认,对大多数情况已经够用;只有在目标非常小或非常密集时,才需要考虑提高输入分辨率。3.3 第一次跑通前务必检查的几件事第一次训练前,我建议花五分钟做三个检查,能避免后面浪费大量时间:检查类别数。DETR模型的num_classes需要设置为“实际类别数+1”,最后一个通道代表“无目标”背景类。这个细节极容易出错,我见过有人直接填2,导致模型把背景也当成了一个类别。检查数据加载。可以单独运行一句python -c "from datasets import build_dataset; ..."或者在main.py里打断点,确认每张图的target字段包含boxes和labels,且boxes坐标在0-1之间归一化。检查一次前向传播。拿一张图,手动调用模型,观察输出logits的shape是否为[batch, 100, num_classes]、pred_boxes的shape是否为[batch, 100, 4]。这里的100对应100个object queries,也就是模型最多预测100个目标。4. 训练过程:配置参数、观察loss曲线与收敛判断4.1 训练命令与关键参数解析先给一条完整训练命令,我实测下来比较稳定:python main.py --batch_size 2 --epochs 300 --lr 1e-4 --lr_backbone 1e-5 --weight_decay 1e-4 --coco_path data/coco --output_dir logs/detr --num_classes 3关键参数含义如下:参数名作用我的建议值batch_size单卡batch大小显存不够时从2降到1epochs训练轮数小数据集300起步lrTransformer部分学习率1e-4lr_backbone骨干网络学习率1e-5,避免破坏预训练特征num_classes类别数真实类别+1coco_path数据根目录按COCO结构组织官方默认用8卡训练500个epoch,单卡跑小数据集时不需要这么悲观,但也不要指望几十个epoch就出好效果。我自己的经验是,哪怕只有几百张图,300个epoch依然有必要,因为匈牙利匹配机制下的损失收敛是渐进的。如果你显存紧张,还有一个实用技巧:先冻结backbone训练几个epoch,让Transformer部分先学到基本匹配规律,再解冻整个网络训练。具体做法是遍历模型参数,将对backbone参数的requires_grad设为False,训完一轮后再置为True。4.2 损失函数的三个组成部分,怎么看训练日志DETR的总损失由三部分组成:分类损失:交叉熵,衡量预测类别和真实类别是否匹配。L1框回归损失:衡量预测框和真实框在归一化坐标上的绝对差异。GIoU损失:衡量预测框和真实框的交并比,弥补L1对小偏移不敏感的问题。训练日志中会依次输出loss_ce、loss_bbox、loss_giou和加权总和loss。权重系数里loss_ce对应1.0,loss_bbox对应5.0,loss_giou对应2.0。我每次训练都会重点看两个信息:loss_ce是否持续下降、class_error是否接近0。如果分类损失一直抖动不降,大概率是匹配逻辑或者类别数设置出了问题,而不是单纯的训练轮次不够。4.3 我训练时看到的真实收敛过程我用安全帽数据集训练时,前50个epoch的loss_ce在1.8附近缓慢下降,直观上看预测框是乱飞的,完全没有章法。到了80个epoch左右,分类损失降到1.2,边界框开始有意识地去贴合目标位置。150个epoch之后,验证集上的mAP才开始明显爬升。这个过程和YOLO“30个epoch就有点样子”完全不同,初次接触DETR的人很容易在100个epoch内误判模型不行而放弃。观察到什么程度可以提前停?我一般参考validation部分输出的loss_ce和bbox AP。如果连续20个epoch验证损失不再下降,就早停;如果还在降,哪怕速度慢也继续训。另外,日志里的test部分默认会在最后一个epoch计算COCO指标,数据集小的时候这个计算会占用一点时间,但建议保留,因为它输出的Average Precision是判断模型质量的最直接依据。5. 推理预测与可视化:让模型真正跑起来5.1 用postprocessor把输出变成坐标DETR的训练代码里自带后处理工具,推理时可以直接调用。核心流程是:模型输出原始logits和归一化坐标,经过softmax变成概率,再从Prediction对象里取出top-k个预测。示例代码大致长这样:import torch from torchvision.transforms import Compose, Resize, ToTensor, Normalize from models import build_model from util.misc import nested_tensor_from_tensor_list model, criterion, postprocessors = build_model(args) state_dict = torch.load("checkpoint.pth", map_location="cpu")["model"] model.load_state_dict(state_dict) model.eval() img = Image.open("test.jpg").convert("RGB") transform = Compose([ Resize((800, 800)), ToTensor(), Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) inputs = transform(img).unsqueeze(0) outputs = model(inputs) post_out = postprocessors["bbox"](outputs, torch.zeros_like(inputs[:, 0, 0]))这里postprocessors["bbox"]会做两件事:把输出的归一化cx, cy, w, h转成x1, y1, x2, y2的绝对像素坐标,并过滤掉低置信度的预测。过滤的置信度阈值在postprocessors构建时由--eos_coef等参数间接影响,实际操作时我建议自己再设一个0.7的阈值,只保留概率大于0.7的结果。5.2 画框与置信度筛选拿到坐标之后,用OpenCV或者PIL画框都很简单。我的做法是先按类别读取置信度,再决定是否画出。代码片段:scores, labels, boxes = post_out[0]["scores"], post_out[0]["labels"], post_out[0]["boxes"] keep = scores > 0.7 for label, box, score in zip(labels[keep], boxes[keep], scores[keep]): x0, y0, x1, y1 = box.tolist() cv2.rectangle(img, (int(x0), int(y0)), (int(x1), int(y1)), (0, 255, 0), 2) cv2.putText(img, f"{class_names[label]}: {score:.2f}", (int(x0), int(y0) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)有个容易被忽略的点:DETR的boxes是cx, cy, w, h格式,且已经除以了原始图像尺寸归一化到0-1。postprocessors返回的坐标是x1, y1, x2, y2的绝对像素值,直接画即可,不需要再乘以(img_w, img_h)。5.3 小样本数据上预测效果的直观感受我在小样本测试集上的直观感受是:DETR对遮挡场景的鲁棒性确实比YOLO好。YOLO在两个人重叠时经常输出两个几乎重叠的框,或者把两个人合并成一个框;DETR因为全局注意力能看到完整上下文,在中等遮挡下依然能区分个体。代价是推理速度明显慢于YOLO,在没有GPU加速的CPU上一次预测可能要几百毫秒到一两秒。如果项目对实时性要求高,DETR的改进版本或蒸馏方案会更合适。6. 实战中反复遇到的三个坑及完整排查链路6.1 显存溢出:从batch_size到梯度累积的解决路径我最早在8GB显存的卡上跑DETR,batch_size=2直接OOM。先尝试把batch_size降到1,问题还在,因为单张800分辨率的图加上Transformer多头注意力占用的中间激活值就已经很大。再往后我把图像输入分辨率降到600,显存占用降了不少,但小目标效果也明显变差。最终我选择的组合是:batch_size=1 + 梯度累积4步,等效batch_size=4,并保持分辨率默认。梯度累积通过accum_iter参数控制,具体做法是每4个小batch才执行一次optimizer.step(),代码里只需在loss.backward()后面加一个判断,达到累积步数再清零梯度。这个办法比降低分辨率更保护检测精度。6.2 训练几十个epoch loss不降:学习率与数据问题的判定顺序如果你发现loss完全不动,先不要急着调模型结构,按这个顺序排查:检查数据加载是否正常。把--num_workers设为0,排除多线程死锁。检查预训练权重是否加载成功。打印模型backbone的统计量,或者直接对比训练前后的权重张量。检查学习率是否过大。DETR对学习率比较敏感,我用1e-4正常,有人调到1e-3后训练初期loss疯狂震荡,降回1e-4才稳定。检查类别数是否多加了背景。若num_classes填成实际类别数而忘了+1,输出通道和标签索引错位,模型永远学不对。我遇到过最诡异的一次是loss前20个epoch在1.9附近波动,后来发现是数据集JSON的annotation里出现了空标注图片,模型反复学习“图上没有东西”的映射。把空标注图片过滤后,loss立刻开始正常下降。6.3 预测时类别错乱:num_classes与背景通道的坑DETR推理时最常见的诡异现象是:模型把背景当成一个目标预测出来,或者类别索引全部偏了1。根因基本都在类别数设置。训练时num_classes要设成实际类别+1,但在推理画框时,要把背景通道剔除,只显示实际类别。我写过一版代码,训练时num_classes=3,推理时class_names=["helmet", "person"],结果真实类别2被显示成背景。排查后发现模型的输出索引0和1对应两个真实类,索引2是背景,而我画框时没有过滤背景。定位过程用了一个很笨但有效的办法:把测试图片里所有被预测的框连同类别编号直接打印出来,再对照图上真实目标位置,一眼就看出是索引偏移。修正方式就是推理画框时加一行if label.item() >= num_real_classes: continue,问题立刻消除。这三个坑一个比一个隐蔽,尤其是类别索引偏移,它不会导致loss异常,只会让结果看起来“模型学错了”。建议你在自己的推理脚本里,始终把背景通道过滤逻辑放在最前面,不要依赖后处理器的自动处理。最后分享一个我个人的习惯:初次跑DETR时,先用官方预训练权重跑一次推理,确认环境、代码、数据链路都正常,再开始训练自己的数据。否则你很难分辨是环境问题、数据问题,还是模型训练策略问题。DETR这套范式学起来曲线比YOLO陡,但一旦理解端到端集合预测的思路,回头看YOLO繁琐的锚框和NMS设计,你会有一种“原来检测可以这么干净”的感觉。