1. 先把 VisDrone 摸清楚别急着写转换脚本用过 VisDrone 训练 YOLOv5 的人大概都有同一个体会真正耗时间的不是敲训练命令而是前面那段“数据到底长什么样”的摸索期。VisDrone 是一套无人机视角的目标检测数据集图像全部由低空飞行平台俯拍采集视角高、覆盖范围大、目标密集。这个数据集最核心的价值在于它把“高空小目标 极端密集 严重遮挡”这三件事一次性摆到了你面前逼着你去处理普通街景数据集里根本不会遇到的场景。YOLOv5 这边生态成熟、配置直观、日志可读性好很适合作为验证想法的第一站。这篇文章面向三类人刚把 YOLOv5 跑通 COCO、想换一个更有挑战性的数据集练手的朋友手里有无人机航拍图、想快速搭一套检测流程的开发者以及已经能训起来但对指标不满意、想搞清楚“为什么加了分辨率还是提不上去”的同学。我不打算讲 YOLOv5 的骨干网络结构那是另一篇文章的事这里只聊从原始压缩包到能出指标这一条链路上的具体操作、参数取舍和我自己踩过的坑。1.1 十个类别和标注文件的真实结构VisDrone 检测任务一共十个类别顺序固定这套顺序在后面写 yaml 和做可视化时都会反复用到pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor。注意pedestrian和people是两个不同的类前者指在行走或站立状态下的行人后者泛指人群聚集、坐卧、难以单独界定的个体实际图像里这两类边界很模糊是最容易混淆、AP 也往往最低的一对。官方的标注文件是每张图对应一个同名.txt里面一行一个目标字段用逗号分隔顺序是bbox_left, bbox_top, bbox_width, bbox_height, score, object_category, truncation, occlusion。这八个字段里前四个是像素坐标但注意它是从 1 开始计数的左上角坐标加宽高不是右下角score字段取 0 或 10 表示这块区域是“忽略区”比如拍到天空、地面纹理、镜头遮挡产生的无意义区域object_category取 0 到 100 同样是忽略标记1 到 10 才对应上面那十个类truncation是 0 到 1 的截断程度occlusion是 0 到 2 的遮挡等级。最容易出事的地方就在这里。很多人写转换脚本时只看object_category忘了过滤score 0结果大批“忽略区”被当成真实目标塞进了训练集模型被迫去学一些本来就不该被检测的区域mAP 莫名其妙掉好几个点。我见过最典型的情况是有人反馈“训练 loss 一直在抖收敛不了”查下来就是忽略区没过滤。1.2 三个容易被忽略的数据特征第一个特征是图像尺寸不统一。VisDrone 的图并不是清一色 1920×1080常见的有一批在 2000×1500 上下也夹杂着 1344×756、2686×1512 这种尺寸。这意味着你绝不能硬编码图片宽高去做归一化必须每张图单独用 PIL 或 OpenCV 读一次真实尺寸。硬编码的后果不是报错而是标签全部偏移而且偏移量不固定训练时看起来 loss 在降验证时框得莫名其妙。第二个特征是目标尺寸分布极端偏小。VisDrone 里相当比例的目标框边长在 20 像素以内甚至十几个像素。你可以自己跑一遍统计把所有 W、H 算出来画个直方图会发现峰值压在很低的区间。这个特征直接决定了后面的分辨率选择在 640 输入下一个 20 像素的框被缩放后只剩 6 个像素左右卷积网络基本没法从 6 个像素里提取出有判别力的特征。第三个特征是验证集很小只有 548 张。这个数量在指标上会带来很大的抖动一个 epoch 之间 mAP0.5 上下波动两三个点是很正常的。更麻烦的是你如果反复盯着这 548 张调超参很快就会对这批数据过拟合换到测试集上效果打折。我的做法是从训练集里再切一小块做本地验证官方 val 集只在关键节点上评估一次。数据划分图像数量有无公开标注典型用途train6471有训练主干与增强策略val548有中期评估、挑选权重test-dev1610无官方在线评测test-challenge1580无竞赛场景提示官方测试集的标注不公开本地能拿到的只有 train 和 val。不要指望在测试集上做任何“本地调试”所有对比实验都只能基于 val 或者自己从 train 里切出来的子集。2. 从原始标注到 YOLO 标签转换脚本与目录搭建YOLO 系列要求的标签格式是每行class_id x_center y_center width height后四个值都是相对原图宽高归一化到 0 到 1 之间的小数。这个格式本身很简单麻烦的是从 VisDrone 的绝对像素坐标、1-based 起始、附带忽略标记的格式转过去中间每一步都要小心。2.1 目录结构和文件命名我建议养成一套固定的目录习惯这样后面写 yaml、写脚本、换数据集都不用重新想路径。VisDrone 的原始图像命名类似0000006_00159_d_0000006.jpg前面是序列号中间是帧号后缀是d。这种命名方式在不同序列之间有重复风险但 VisDrone 官方已经保证了全局唯一直接沿用原名即可不需要重命名。我用的目录结构大致是这样datasets/ visdrone/ images/ train/ val/ labels/ train/ val/ raw/ # 原始解压目录保留不动 VisDrone2019-DET-train/ images/ annotations/ VisDrone2019-DET-val/ images/ annotations/保留一份完整的 raw 目录很重要。我早期为了“干净”直接把原始标注删了后来想统计遮挡分布、想按 truncation 过滤重做一遍又得重新下一次压缩包白等半小时。硬盘不紧张的话原始数据一律别删。2.2 转换脚本逐段拆解下面这个脚本我在多个小目标数据集上复用改一下类别表和文件名规则就能直接用。核心逻辑有四步过滤无效标注、坐标归一化、越界裁剪、写入目标路径。import os from pathlib import Path from PIL import Image CLASSES [ pedestrian, people, bicycle, car, van, truck, tricycle, awning-tricycle, bus, motor ] # occlusion2 表示严重遮挡这里保留但可随时调整策略 MAX_OCCLUSION 2 # 小于这个像素面积的目标直接丢弃避免出极端的退化框 MIN_AREA 4 def convert_split(img_dir, ann_dir, dst_img_dir, dst_lbl_dir): os.makedirs(dst_img_dir, exist_okTrue) os.makedirs(dst_lbl_dir, exist_okTrue) stats {total: 0, kept: 0, skip_ignore: 0, skip_tiny: 0} for ann_path in Path(ann_dir).glob(*.txt): img_path Path(img_dir) / (ann_path.stem .jpg) if not img_path.exists(): continue with Image.open(img_path) as im: W, H im.size lines [] with open(ann_path, r, encodingutf-8) as f: for raw in f: raw raw.strip() if not raw: continue parts raw.split(,) if len(parts) ! 8: continue stats[total] 1 bx, by, bw, bh [float(v) for v in parts[:4]] score int(float(parts[4])) cat int(float(parts[5])) occl int(float(parts[7])) # 1. 忽略区过滤 if score 0 or cat 0: stats[skip_ignore] 1 continue # 2. 遮挡等级过滤 if occl MAX_OCCLUSION: continue # 3. 退化框过滤 if bw * bh MIN_AREA: stats[skip_tiny] 1 continue # 4. VisDrone 坐标是 1-based 左上角 宽高 x1 bx - 1.0 y1 by - 1.0 x2 x1 bw y2 y1 bh # 5. 越界裁剪 x1 max(0.0, min(x1, W - 1)) y1 max(0.0, min(y1, H - 1)) x2 max(0.0, min(x2, W - 1)) y2 max(0.0, min(y2, H - 1)) if x2 - x1 1 or y2 - y1 1: continue xc (x1 x2) / 2.0 / W yc (y1 y2) / 2.0 / H nw (x2 - x1) / W nh (y2 - y1) / H cls_id cat - 1 # 1..10 - 0..9 lines.append(f{cls_id} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) stats[kept] 1 stem ann_path.stem # 图像可以做软链接省空间这里直接复制 target_img Path(dst_img_dir) / (stem .jpg) if not target_img.exists(): with open(img_path, rb) as src, open(target_img, wb) as dst: dst.write(src.read()) with open(Path(dst_lbl_dir) / (stem .txt), w, encodingutf-8) as f: f.write(\n.join(lines)) return stats if __name__ __main__: root Path(datasets/visdrone/raw) dst Path(datasets/visdrone) for split, folder in [(train, VisDrone2019-DET-train), (val, VisDrone2019-DET-val)]: s convert_split( root / folder / images, root / folder / annotations, dst / images / split, dst / labels / split, ) print(split, s)几个必须强调的点。第一cls_id cat - 1这一步看着不起眼但如果漏了整个类别标签会整体错一位pedestrian变成peoplecar变成van训练照样能跑、loss 照样会降只是最后每类 AP 全是乱的排查起来非常痛苦。第二x1 bx - 1.0这个减一是 VisDrone 特有的因为它的坐标从 1 开始。第三归一化用的是裁剪之后的框不是裁剪前的否则会出现大于 1 的坐标YOLOv5 加载时会报越界警告。跑完之后打印的统计信息值得看一眼。正常情况下一张训练集图片平均有几十个目标框如果算出来平均只有一两个那基本可以确定过滤条件写错了。2.3 数据集 yaml 与首轮自检转换完成之后写一份visdrone.yaml放在项目的data/目录下path: ../datasets/visdrone train: images/train val: images/val nc: 10 names: [pedestrian, people, bicycle, car, van, truck, tricycle, awning-tricycle, bus, motor]path是数据根目录train和val是相对路径YOLOv5 会自动去对应位置找images/xxx然后把路径里的images替换成labels去找标签。这个约定必须严格遵守如果你把标签放在别的地方框架找不到只会静默地把这批图当作无标签负样本处理训练时 loss 里的目标项一直是 0很多人卡在这里。自检我一般做两件事。第一件是随机抽十张图把标签画上去看一眼红色的框是不是正好套在车和人身上框的方向有没有整体偏移。第二件是跑一次极短的训练--epochs 1 --batch 8 --img 640看日志里train和val的实例数量是不是合理数量级。如果 val 的实例数比你预估的少了一个数量级回去查转换脚本。注意如果你打算从 train 里切一部分做本地验证记得在 yaml 里用绝对或相对路径明确指向切出来的那份别直接改train字段否则训练集少了却没人发现指标对比全部失真。3. 训练配置的取舍分辨率、锚框与检测头数据这一层理顺之后接下来是决定上限的三个配置。它们的影响远大于学习率、权重衰减这些细节参数前面没调好后面再怎么炼丹都是徒劳。3.1 输入分辨率是第一优先级VisDrone 上我见过的最大误区就是“先用 640 跑通再说”。这个数据集的问题不是模型不行是信息在缩放那一步就丢了。假设原图 2000×1500 缩到 640×480缩放比大约是 0.32一个原本 25×25 像素的汽车进去只剩 8×8。8 像素在 P3 特征图stride 8上对应 1 个格子到 P4stride 16上连半个格子都不到。也就是说模型在语义层根本“看不见”这个目标。把--img提到 1280同样那个目标变成 16×16在 P3 上占两个格子才能被正常激活。公开评测里的量级大致是这样YOLOv5s 在 640 输入下 mAP0.5 落在二三十这个区间换成 1280 通常能往上走七八个点甚至更多提升幅度比换更大的骨干网络明显得多。这不是玄学就是有效像素数量的问题。代价也很直接显存和耗时。1280 输入的张量体积是 640 的四倍训练时激活值占用同步涨。以单卡 16GB 显存为例YOLOv5s 在 1280 下 batch 大概只能开到 8 到 16还要把--workers调低一点防止内存爆掉。如果显存更紧张可以用--img 1024折中1024 相对 640 的提升同样很可观。输入尺寸单卡 16GB 可行 batch小目标有效像素相对 640 的提升幅度量级参考64032 及以上严重不足基线102416 左右勉强明显提升12808 到 12基本够用提升显著15364 到 6充足继续提升但边际递减3.2 重新聚类锚框YOLOv5 自带的默认锚框是在 COCO 上用 K-means 聚出来的九个尺度从小到大的分布跟航拍视角完全不是一回事。VisDrone 里大量框集中在很小的尺寸区间直接用默认锚框匹配到的正样本 anchor 会严重偏大回归目标偏差大训练前期 loss 掉得很慢。好在 YOLOv5 训练启动时会自动跑一次 anchor 检查日志里会打出AutoAnchor相关的信息如果它认为当前锚框跟数据不匹配会提示重新聚类。你可以手动跑一次聚类把结果写回模型配置python utils/autoanchor.py --data data/visdrone.yaml \ --weights yolov5s.pt --img-size 1280聚完会得到九个如果加了 P2 层就是十二个新的宽高对形如[12,15, 19,22, 27,31, ...]前面的数字普遍比默认值小一圈这是符合预期的。把这些值替换到models/yolov5s.yaml或者其他对应模型配置的anchors:字段下。注意顺序必须是从小到大排因为 YOLO 是把锚框按面积排序后分给不同尺度的检测层的顺序错了会导致小锚框配到 P5 上去效果比不换还差。这一步的收益不像分辨率那么立竿见影但属于“几乎零成本、普遍有正收益”的操作没有理由跳过。3.3 加一个 P2 检测头值不值YOLOv5 标准配置有三个检测头分别接在 stride 8、16、32 的特征图上。对于 VisDrone 这种极小目标密集的数据集社区里很常见的做法是再加一个 stride 4 的 P2 头。做法是把models/hub/目录下现成的 P2 版配置复制过来调整一下nc和anchors作为--cfg传入。加了 P2 之后理论上 8 到 16 像素的目标终于有了一个高分辨率的检测层可以负责。实测下来这种改造对小目标 AP 的提升确实存在尤其是pedestrian、people、motor这几类小目标占比高的类别。但代价必须说清楚。P2 特征图的分辨率是 P3 的两倍通道数也不少显存占用和计算量会上一个台阶。在同样的显存条件下你可能被迫把 batch 降到 4 甚至 2或者把输入尺寸从 1280 退回到 1024。这时候就出现一个权衡1280 P3 三头和1024 P2 四头哪个更好我的经验是前者通常更稳因为分辨率带来的信息增益是全局的而 P2 头只对极小目标那一小部分样本起作用还容易引入大量低质量正样本让训练变得更不稳定。如果你的显存足够撑住 1280 P2那就两个都要如果只能二选一优先保分辨率。4. 训练实操命令、超参与过程监控前面三步定下来之后训练本身就相对机械了。这一节把命令拆开讲清楚每个参数为什么这么给再聊聊增强策略和日志阅读。4.1 完整训练命令与参数逐条解释我常用的起手命令是这样python train.py \ --img 1280 \ --batch 8 \ --epochs 100 \ --data data/visdrone.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s-p2.yaml \ --hyp data/hyps/hyp.visdrone.yaml \ --device 0 \ --workers 8 \ --project runs/visdrone \ --name exp_s_1280_p2 \ --exist-ok--weights yolov5s.pt用的是 COCO 预训练权重。有人会问航拍视角跟街景差别那么大预训练还有用吗有用而且很有用。浅层学到的边缘、角点、纹理响应是通用的深层虽然语义有偏移但整体收敛速度比从零开始快得多。我在 VisDrone 上做过对比从零训练到同等 mAP 大概需要两到三倍的 epoch 数。--hyp指向自定义的超参文件这是很多人忽略的一步。YOLOv5 默认的 hyp 是针对 COCO 调的直接套过来在 VisDrone 上不一定合适。我一般从默认文件复制一份改几个关键项初始学习率从 0.01 降到 0.005 左右数据集小、batch 小学习率太大会震荡warmup_epochs从 3 提到 5小数据集前期更脆弱需要更长的预热weight_decay保持 0.0005 不动。--workers的设置要看你机器的 CPU 核数和内存。VisDrone 的图很大1280 输入下每张图解码加增强的内存开销不小workers 开太高容易触发 OOM Killer。8 是一个比较安全的起点如果nvidia-smi显示 GPU 利用率长期低于 60%说明数据加载是瓶颈可以往上加如果系统内存吃紧就往下减。--name里把关键配置都写进去exp_s_1280_p2一眼能看出是 small 模型、1280 输入、带 P2 头。我吃过这个亏跑了一堆实验名字全是exp、exp2、exp3回头根本分不清哪次对应哪个配置只能翻日志。4.2 数据增强参数该怎么调YOLOv5 的增强项挺多VisDrone 上不是每项都开得越大越好。mosaic这一项需要重点说。它把四张图缩小后拼成一张对普通数据集能显著提升小目标数量和上下文多样性但对 VisDrone 这种本来就全是小目标的数据集拼完之后每张子图被缩到原尺寸的一半目标变得更小了。在 640 输入下开满 mosaic很多目标会缩到三四个像素基本等于噪声。所以我的建议是 1280 输入下保持默认的 1.0 但配合close_mosaic提前关闭或者干脆把概率降到 0.5 左右640 输入下反而不建议开满。flipud垂直翻转我一般设成 0。无人机俯拍图像有明确的上下语义天空在上、地面在下垂直翻转后出现了物理上不合理的场景虽然模型未必学得出来但没有收益的事情不做。fliplr水平翻转设 0.5 没问题。scale随机缩放默认 0.5这个对 VisDrone 是有益的能让模型见到同一目标在不同尺度下的样子。但因为原图本身目标就小缩放的负向区间缩小要小心缩过头目标直接消失。可以把scale调到 0.3 到 0.4 之间减小向下缩放的比例。hsv_h/s/v三个颜色扰动保持默认即可无人机在不同光照时段拍的图色温差异大颜色扰动对这个数据集是有实际意义的。degrees旋转默认 0如果图像本身没有明显的方向先验可以开到 5 到 10 度但别开太大旋转会让边界框变大、引入大量背景。4.3 训练曲线和日志怎么读YOLOv5 会把结果写到runs/visdrone/exp_xxx/下面results.csv里逐 epoch 记录所有指标results.png是汇总曲线。我通常先看三个东西。第一个是train/box_loss和val/box_loss的走向。正常情况是前期快速下降大概二三十个 epoch 后趋于平缓。如果 val 的 box_loss 在中后期开始往上翘而 train 还在降那是过拟合的信号。VisDrone 训练集六千多张对 YOLOv5s 来说不算多过拟合很常见早停或者加强正则都是选项。第二个是metrics/mAP_0.5的曲线。这条线在 VisDrone 上不会像 COCO 那样平滑上升它会抖。我见过在 60 epoch 附近突然掉两个点又涨回来的情况这种单点波动不要去调参看趋势。判断收敛的依据是最近二十个 epoch 的峰值不再刷新。第三个是各类别的 AP。YOLOv5 会在验证时打印每个类别的 P、R、mAP0.5。VisDrone 上car、van、truck通常表现不错awning-tricycle、tricycle、bicycle这几类往往很差因为它们数量少、形态相似、目标又小。如果发现某一类 AP 接近 0先别急着改模型回头看看这类在训练集里到底有多少个实例是不是样本太少导致的。心得训练初期最值得看的是锚框匹配情况。日志里会打印每个检测层匹配到的正样本数量如果某一层的数字明显偏少说明那层的锚框跟数据分布不匹配重新聚类能解决。5. 评估、推理与问题排查实录模型训出来之后评估和排查是另一半工作量。VisDrone 的指标解读跟 COCO 有一些不同值得单独讲。5.1 指标怎么看才有意义YOLOv5 默认报告的mAP0.5是 IoU 阈值 0.5 下的平均精度mAP0.5:0.95是 0.5 到 0.95 步长 0.05 的平均。VisDrone 上这两个数字差距会很大前者可能三四十后者只有二十上下这很正常——小目标的定位精度天然就差框稍微偏几个像素 IoU 就掉到 0.5 以下了。对比实验时只用 mAP0.5 会掩盖定位质量的变化。比如你换了更大的输入尺寸mAP0.5 涨了 3 个点但 mAP0.5:0.95 只涨了 0.5说明模型找到了更多目标但框得不够准。反之如果后者涨得更多说明定位质量有实质改善。我一般两个都记录横向对比时一起看。还有一个常见误区是拿不同输入尺寸下的结果直接对比。1280 训出来的模型在验证时也是按 1280 推理640 训出来的按 640 推理这个对比是公平的。但如果你把 1280 训的模型拿去做 640 推理指标会掉得厉害这不能说明模型差只是训练和推理的尺度不匹配。另外VisDrone 的官方评测用的是一个特定的评估脚本它对低矮目标、密集重叠的处理跟 YOLOv5 内置的 pycocotools 评估有一些细节差异。如果你的目标是对标公开榜单最终结果应该用官方脚本再跑一遍别直接拿训练日志里的数字去比。5.2 常见问题速查表下面这些是我在实际项目里遇到过、并且被问得最多的几个问题整理成表方便对照。现象最可能的原因排查与解决训练能跑但 mAP 一直是 0标签路径不对被当负样本检查 images 与 labels 目录是否同级对应框整体偏移一个固定量忘记 VisDrone 坐标是 1-based转换脚本加x1 bx - 1所有类别混淆cls_id忘了减 1核对 yaml 中 names 顺序loss 剧烈震荡不收敛学习率过大或 batch 过小lr 降到 0.005用--accumulatemAP 比预期低一截输入尺寸仍为 640提到 1024 或 1280 再试小目标召回极低锚框与数据不匹配重新聚类并写回配置显存 OOM输入尺寸与 batch 组合过激降 batch 或开--amp混合精度验证指标波动大val 只有 548 张多次评估取平均或切分自建验证集推理速度慢得离谱密集小目标导致 NMS 耗时高调--conf-thres和--iou-thres出现大量重复框NMS 阈值过高iou-thres 从 0.45 降到 0.35表里最后两条值得展开说一下。VisDrone 一张图动辄上百个目标NMS 阶段的计算量跟候选框数量是平方关系推理变慢很正常。把置信度阈值从默认 0.25 提到 0.4 左右能砍掉大量低分候选框速度快不少召回损失在可接受范围内。重复框的问题则往往出现在密集区域两辆车贴得很近IoU 天然就高NMS 阈值设太高会把它们都留下适当降低能缓解。还有一个不那么常见但很折磨人的问题训练时一切正常推理时发现某些图的检测结果整体偏了一个方向。这通常是推理时的 letterbox 填充参数和训练时不一致导致的检查一下推理脚本里--img-size是否跟训练时一致以及是否用了--rect矩形推理。矩形推理会改变填充方式如果训练时是方形填充、推理时是矩形坐标还原就会错。5.3 推理与可视化的两个实用技巧推理命令本身很简单python detect.py \ --weights runs/visdrone/exp_s_1280_p2/weights/best.pt \ --source datasets/visdrone/images/val \ --img-size 1280 \ --conf-thres 0.35 \ --iou-thres 0.4 \ --save-txt \ --project runs/detect \ --name visdrone_val--save-txt会同时把检测结果存成 YOLO 格式的 txt方便后面做定量分析或者喂给下游任务。第一个技巧是切片推理。原图两千多像素宽直接缩到 1280 会损失细节。如果把原图切成四块甚至九块每块单独送进网络推理再把结果映射回原图坐标做一次全局 NMS小目标的检出率会有明显改善。代价是推理时间成倍增加而且块与块交界处的目标容易被切断需要在切片时留一定的重叠区域。这套方案在对精度要求高、对速度不敏感的离线场景里很实用。第二个技巧是按类别调阈值。全局一个置信度阈值对 VisDrone 不太公平car这种大类模型很自信0.35 就够awning-tricycle这种小类分数普遍偏低用同样的阈值会漏掉一大批。可以在后处理阶段给每个类别单独设阈值大类严一点、小类松一点整体 F1 会有改善。这不是什么高深技巧但确实能立竿见影地改善可视化效果。6. 我个人在几轮实验后的几点体会折腾这个数据集断断续续有段时间了说几个不太会写在文档里的感受。关于分辨率和模型容量的取舍我现在的默认策略是先把输入尺寸顶到显存允许的上限再考虑换模型。同样一份显存预算从 YOLOv5s 换到 YOLOv5m 带来的提升往往不如把 640 提到 1024 来得直接。这个结论在别的数据集上未必成立但在 VisDrone 这种小目标主导的场景里信息量比参数量更稀缺。关于验证集548 张真的太少了我后来的做法是从训练集里按序列切出大约 800 张做本地验证跟官方 val 分开。序列切分很关键同一段视频的相邻帧高度相似随机切会把几乎一样的图分到训练和验证两边指标虚高。按序列前缀切才能真实反映泛化能力。还有一个容易忽略的点是训练完成之后的权重选择。YOLOv5 默认保存best.pt和last.ptbest.pt是按验证集 mAP0.5:0.95 挑的。如果你后面要做的是高召回场景比如先粗筛再人工复核那按 mAP 挑出来的模型未必最合适可能要回头在results.csv里找一个召回更高的 epoch 对应的权重。这个细节值得在项目开始前就想清楚评估指标应该对齐最终业务目标而不是默认值。最后提一个数据层面的思路VisDrone 的十个类别里pedestrian和people的语义边界本身就模糊强行让模型区分这两类往往会拉低整体指标。如果下游任务并不需要区分“走着的人”和“站着坐着的人”把它们合并成一个行人类其余八类保持不动重新训一遍mAP 通常会有肉眼可见的提升。这不算作弊是对任务定义的合理简化前提是你要清楚自己在做什么。