ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO网球检测实战:551张图像训练全流程与避坑指南

YOLO网球检测实战:551张图像训练全流程与避坑指南 简介面向YOLO系列算法实战的网球与球员检测数据集适合算法学习者、竞赛选手及体育场景项目开发者直接用于模型训练和验证测试。包内含551张jpg图像并配套513个txt标注与513个xml标注分别对应YOLO格式和VOC格式其中txt标签文件的每一行依次记录类别索引、归一化中心点坐标和宽高坐标均除以图像宽高转换为0到1之间的小数便于不同分辨率图像直接训练。资源另附data.yaml配置可接入YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等主流模型且已按训练集与验证集划分目录无需再整理数据。全部文件共1578个压缩包大小为27.67MB下载后即可开展目标检测实验图像涵盖多种比赛场景与球员姿态可用于迁移学习、算法对比和课设演示。目前已有138人学习适合准备算法课设、参加检测比赛或进行网球运动分析的读者快速启动项目。1. 这份551张的网球数据集到底能干什么先别急着解压训练拿到yolo算法-网球-球员数据集-551张图像带标签-球-球员.zip这个压缩包第一反应通常是解压、扔进 YOLO、跑起来看 mAP。但做过几次真实项目就知道551 张图像对于目标检测来说是个极其微妙的数量级——它不够你训练一个鲁棒的通用模型但又足够你在受限场景里做出能用的原型。这个数据集的定位是「专项场景验证」也就是网球比赛视频中的球与球员检测而不是通用目标检测。它的价值在于让你在几小时内部署一套从数据到模型的完整流程提前暴露小目标检测、类别不平衡、运动模糊这些真实问题而不是让你拿它冲击什么榜单。适合谁来用两类人。一类是刚接触 YOLO 生态的初学者用这个小数据集把数据集标注 - YAML 配置 - 训练 - 推理整条链路跑通另一类是做体育视频分析、需要快速验证球类检测可行性的工程师用它做 baseline再决定要不要花成本去扩充数据。本文就沿着这条链路把这 551 张图像从压缩包到可部署模型的全过程拆开讲清楚包括中间会踩到的坑。2. 拆开压缩包看门道YOLO标签格式、目录结构与标注质量自查2.1 目录结构与YOLO标注格式txt文件里的数字到底代表什么解压之后典型的 YOLO 格式数据集目录是这样的dataset/ ├── images/ │ ├── train/ │ │ ├── match_001.jpg │ │ ├── match_002.jpg │ │ └── ... │ └── val/ │ ├── match_050.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── match_001.txt │ │ └── ... │ └── val/ │ └── ... ├── classes.txt └── dataset.yaml这里要解释一个初学者容易忽略的点images和labels下的文件是一一对应的。每张match_001.jpg对应一个match_001.txt如果某张图没有标注任何目标对应的 txt 文件是空的——这种情况在球类数据里很常见比如球员特写镜头里根本没有球。空标签文件不要删删了 YOLO 训练时反而会报「image not found」之类的错。打开classes.txt通常两行ball player注意类别顺序。YOLO 标签文件里的第一列数字就是这个顺序的索引。如果classes.txt里ball是第 0 行player是第 1 行那么标签文件里0代表球1代表球员。顺序错乱是数据集中最常见的低级错误之一。再看一个标签文件match_001.txt的内容0 0.6210 0.4801 0.0187 0.0187 1 0.4552 0.3288 0.1426 0.2875每一行五个数字类别索引 x_center y_center width height。所有坐标都是相对于图像宽高的归一化值。比如第二行球员的边界框中心在图像宽度 45.52%、高度 32.88% 的位置框宽占全图 14.26%高占全图 28.75%。球的框是0.0187意味着球的直径只占图像宽度的 1.87%——这就是小目标检测的典型特征。2.2 用脚本做标注分布统计三个必须看的数据指标拿到标签文件第一步不是训练而是统计。我一般用一段简单的 Python 脚本把标注分布拉出来重点看三个指标每个类别的目标数量、目标框的尺寸分布、每张图的平均目标数。import os from collections import Counter import numpy as np label_dir dataset/labels/train class_names [ball, player] category_count Counter() box_sizes {ball: [], player: []} per_image_count [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: lines f.readlines() per_image_count.append(len(lines)) for line in lines: parts line.strip().split() cls int(parts[0]) w, h float(parts[3]), float(parts[4]) category_count[class_names[cls]] 1 box_sizes[class_names[cls]].append((w, h)) print(类别数量:, dict(category_count)) print(每图目标数均值: %.2f % np.mean(per_image_count)) for cls, sizes in box_sizes.items(): areas np.array([w * h for w, h in sizes]) print(f{cls}: 框面积中位数{np.median(areas):.4f}, f最小{areas.min():.4f}, 最大{areas.max():.4f})这段代码的逻辑很简单遍历标签目录逐行解析五个字段把类别和框尺寸收进列表最后打印统计结果。参数怎么理解框面积是归一化面积0.0187 * 0.0187 ≈ 0.00035的球的面积在 640x640 的输入分辨率下约等于 143 个像素。按照 COCO 数据集对小目标的定义面积小于 32x32 像素这个数据集里的球绝大多数属于小目标类别。你训练时会发现模型的ball类 AP 明显低于player类根因就在这。如果统计发现ball框面积分布极不均匀——比如既有 1% 的小球又有 30% 的大球特写——说明数据来源混杂标注尺度不统一需要清理。2.3 数据划分策略551 张图不能简单按 8:2 切551 张图像常规的 8:1:1 划分441 训练 / 55 验证 / 55 测试在这个规模下不适用。原因在于网球比赛视频的帧之间有极强的连续性——同一个回合的相邻帧高度相似。如果随机划分训练集和验证集可能来自同一段视频的相邻帧验证指标会虚高模型实际泛化能力远低于预期。我建议的做法是按视频片段分组划分。假设数据集包含多个比赛片段先确认元信息里是否有片段标记——很多公开数据集文件名带前缀如match_01_frame_120.jpg。如果文件名没有分组信息就用文件名排序后按固定间隔抽帧做验证集。import os import shutil import random # 按文件名前缀分组假设 match_01, match_02 ... image_files sorted(os.listdir(images)) groups {} for fname in image_files: group fname.split(_)[0] # 提取 match_01 groups.setdefault(group, []).append(fname) # 取 20% 的组做验证集 group_names list(groups.keys()) random.seed(42) val_groups set(random.sample(group_names, max(1, int(len(group_names) * 0.2)))) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) for group, files in groups.items(): dest_img dataset/images/val if group in val_groups else dataset/images/train dest_lbl dataset/labels/val if group in val_groups else dataset/labels/train for fname in files: shutil.copy(os.path.join(images, fname), os.path.join(dest_img, fname)) # labels 文件名和 images 一致只是扩展名不同 lbl_name fname.replace(.jpg, .txt) lbl_src os.path.join(labels, lbl_name) if os.path.exists(lbl_src): shutil.copy(lbl_src, os.path.join(dest_lbl, lbl_name))这个思路的核心在于保证验证集里出现的场景不在训练集里出现。551 张图按片段分组后如果验证组占比过多导致训练集不足 400 张可以把验证组比例降到 15%但绝不能为了让指标好看而改成随机划分——那是自欺欺人。3. 训练前必调的三件事图像分辨率、数据增强与小目标策略3.1 输入分辨率怎么选640 还是 1280基于球的实际像素决定YOLOv8 默认训练分辨率是 640x640。但对于网球检测这个任务640 分辨率下球往往只有十几个像素特征极其微弱。我把这个问题量化一下假设原图是 1920x1080 的电视转播画面球的直径约 30 像素归一化宽度就是 30/1920 ≈ 0.0156。缩放到 640x640 后球直径只剩约 10 像素。10 像素的圆形色块经过卷积下采样后在较深的特征层上可能只剩 1-2 个像素的响应几乎不可能稳定检出。所以训练分辨率我一般直接提到 1280。代价是显存和训练时间——1280 分辨率下 batch size 只能开到 640 时的四分之一。但换来的球类检测 AP 提升往往是 10 个点以上。# yolo-config.yaml 中的关键参数 train: dataset/images/train val: dataset/images/val nc: 2 names: [ball, player] # 训练参数在命令行或 python 中设置 # imgsz: 1280 # batch: 16 (RTX 3090 或以上)关于 anchor 的设置YOLOv8 是 anchor-free 的不需要手动配置 anchor 尺寸这对小目标是个利好。但如果你用的是 YOLOv5就必须注意——yolov5s.yaml默认的 anchor 是针对 COCO 通用目标设计的最小的 anchor 是[10, 13]对于网球仍然偏大。可以用python detect.py --autobias或手动在模型 yaml 里把前三组 anchor 改小比如anchors: - [4, 4, 6, 6, 8, 8] # 针对小目标的超小 anchor - [10, 12, 14, 16, 20, 22] - [30, 36, 45, 50, 60, 65]我自己做小目标检测的经验是与其调 anchor不如先把输入分辨率拉到 1280。anchor-free 架构在 1280 下对小目标的效果提升非常明显这一步是最值得的投入。3.2 数据增强用运动模糊和 mosaic 模拟真实比赛画面网球场景有两个区别于通用目标检测的特殊性运动模糊和球与球员的遮挡。网球时速可超 200 公里转播画面中球往往是拉长的拖影球员之间、球员与球之间的遮挡频繁出现。默认的 YOLO 数据增强管线mosaic random affine hsv 扰动能覆盖一部分亮度变化但覆盖不了运动模糊。我用 Albumentations 库在训练前做针对性增强重点加了两个操作运动模糊MotionBlur和随机遮挡CoarseDropout。import albumentations as A train_transform A.Compose([ A.RandomResizedCrop(height1280, width1280, scale(0.8, 1.0), p0.5), A.MotionBlur(blur_limit(5, 15), allow_shiftedTrue, p0.3), A.CoarseDropout( max_holes4, max_height120, max_width120, fill_value0, p0.2 ), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit20, val_shift_limit20, p0.5), A.Normalize(mean[0, 0, 0], std[1, 1, 1]), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels]))参数设置的逻辑MotionBlur的blur_limit设 5 到 15 像素模拟中度运动模糊CoarseDropout的max_holes4表示最多挖 4 个矩形空洞模拟球被球员身体或场地广告遮挡的情况。注意fill_value0填黑色区域因为转播画面里球场的暗部是黑色的自然遮挡也主要产生于球员身体深色覆盖球体。如果你用白底训练可以把 fill_value 改成 255 模拟白色背景遮挡。一个容易踩的坑Albumentations 的RandomResizedCrop和 YOLO 内置的 mosaic 是叠加关系不要在外部又做 resize 又让 YOLO 做 mosaic否则训练图会被重复变换导致模型过拟合到增强后的分布上。实际操作时我通常用 Ultralytics 的augmentTrue参数开启内置增强Albumentations 只做前置预处理。3.3 类别不平衡问题ball 只有 player 的三分之一怎么办统计完标注你会发现ball的目标数量通常只有player的三分之一甚至更少——因为很多帧里球不在画面内。这就是典型的类别不平衡。YOLO 自带cls损失权重参数可以调节Ultralytics YOLOv8 中通过loss_cls控制。但更有效的做法是两种第一是过采样。把包含球的图像在训练列表里重复多遍让每个 epoch 中球类样本的参与次数增加。在dataset.yaml里没法直接配过采样权重但可以在数据加载阶段实现或者用weights参数给类别分配权重。Ultralytics 支持class_weights直接在训练脚本里设。from ultralytics import YOLO model YOLO(yolov8s.pt) model.train( datadataset.yaml, imgsz1280, epochs100, batch16, class_weights{0: 3.0, 1: 1.0}, # 球类权重是球员的 3 倍 augmentTrue, patience15, )class_weights{0: 3.0, 1: 1.0}的含义是球类别 0的分类损失乘以 3 倍权重。注意这是一个权衡——权重太大会导致模型把球员误检成球实际调参时从 1.5 倍开始逐次上调观察验证集上ball类的 precision 和 recall 变化。第二个做法是在损失函数中启用focal loss的 gamma 参数。YOLOv8 默认使用BCEWithLogitsLoss结合 focal loss默认fl_gamma0.0即关闭。开启后等于把训练重心放在难分样本上——那些被背景干扰的模糊小球正是难分样本。yolo detect train datadataset.yaml modelyolov8s.pt imgsz1280 epochs100 batch16 fl_gamma1.5fl_gamma1.5表示正样本的 loss 占比在难分样本上更高。如果你发现训练后期ball类的 recall 很低球漏检多可以把这个值提到 2.0。但如果 precision 也低误检多降回 1.0 并加大class_weights更合适。4. 把551张图跑成模型YOLOv8训练全流程与loss曲线判读4.1 dataset.yaml 的路径写法与类别顺序一致性Ultralytics YOLOv8 的数据集配置是dataset.yaml文件。很多初学者在这个文件上翻车——路径用了绝对路径换机器就得改或者names列表顺序和标签里的索引不一致导致训练时类别错乱但 loss 还在下降直到推理才察觉。# dataset.yaml path: /home/user/dataset # 数据集根目录建议写绝对路径 train: images/train # 相对 path 的路径 val: images/val # test: images/test # 可选 nc: 2 names: 0: ball 1: player关键点有两个。第一path是根目录train和val是相对于根目录的路径不要写成/images/train这种带前导斜杠的形式。第二names的索引必须和标签文件第一列完全一致。如果classes.txt是player在前而 yaml 里ball在前训练会在你毫无察觉的情况下把类别 0 和 1 互换。我习惯在写完 yaml 后跑一段验证代码from ultralytics.data import YOLODataset ds YOLODataset(yaml_pathdataset.yaml, splittrain, imgsz640) sample ds[0] print(sample[cls]) # 打印类别索引 tensor打印出的cls如果包含tensor([1.])而你看过对应图像确实是球说明标注索引和 yaml 里的 names 顺序对不上需要统一。别小看这一步它能在训练前省下你两小时排查时间。4.2 训练命令与连贯的参数清单从预训练权重开始还是从零开始551 张图从零训练一个 YOLOv8s约 1100 万参数几乎必然过拟合。正确的做法是使用 COCO 预训练权重做迁移学习。yolov8s.pt是在 COCO 上预训练过的权重前几层学到的边缘、纹理特征对网球场景依然有效。有两种迁移方式# 方式一加载预训练权重冻结 backbone 前 10 层 yolo detect train datadataset.yaml modelyolov8s.pt imgsz1280 batch16 epochs100 freeze10 # 方式二全部层参与训练但用更小学习率 yolo detect train datadataset.yaml modelyolov8s.pt imgsz1280 batch16 epochs100 lr00.001freeze10的含义是冻结模型前 10 层backbone 的大部分卷积层只训练 head 部分和最后的检测层。这样训练速度快、显存占用小且在数据量不足时能有效防止过拟合。我认为在 551 张图规模下freeze10是更稳妥的选择——backbone 已经学到了足够好的图像特征需要更新的是针对网球场景的检测头。学习率方面lr00.001比默认的0.01小一个数量级。原因预训练权重已经收敛到一个较优解附近过大的学习率会破坏 backbone 学到的特征。lr00.001配合cos_lrTrue默认开启在前 3 个 epoch 用 warmup 过渡效果最稳。如果发现训练初期 loss 就出现nan先检查是不是 batch size 太大导致显存溢出再检查学习率。其他常用参数参数推荐值说明imgsz1280小目标检测的关键低于 960 不建议batch8~16由显存决定RTX 3090 以上可 16epochs100配合 early stop551 图没必要跑 300 epochpatience15验证集指标连续 15 轮不提升则停止workers4~8数据加载线程数CPU 瓶颈时调高optimizerAdamW小数据集上收敛更稳SGD 也可但需调 momentum4.3 训练过程怎么看loss 曲线、验证指标和 checkpoint 选择训练开始后终端会滚动输出每轮的 loss 和指标。你需要看的关键指标是P精确率、R召回率和mAP50、mAP50-95。对于球类检测mAP50比mAP50-95更重要——因为球太小边界框的 IoU 很难达到 0.75 以上mAP50-95会被严重拉低这不代表模型不好而是小目标的固有特性。Loss 曲线的解读有讲究。YOLOv8 的训练日志包含box_loss、cls_loss、dfl_loss。如果看到box_loss在持续下降但cls_loss在 40 轮后开始上升典型的过拟合信号——模型在训练集上把球和背景分的越来越细反而失去泛化。此时看验证集上的mAP50如果开始下滑果断停止训练取倒数第 10 轮左右的权重。训练结束后检查点文件保存在runs/detect/train/weights/ls runs/detect/train/weights/ # best.pt last.ptbest.pt是验证集 mAP 最高的权重last.pt是最后一轮的权重。我有个习惯如果best.pt出现在训练的前 30 轮而后续 70 轮 mAP 没有刷新说明模型早就收敛了best.pt之前的训练轮次是浪费。这种情况下把epochs降到 50 重新跑用省下来的时间调数据增强或分辨率比硬跑 100 轮更有价值。5. 避坑记录标注错误、漏检率居高不下与过拟合的5个实战案例5.1 案例一训练 loss 正常下降但推理时类别全部输出为 player现象训练 50 轮后 loss 降到 0.3 以下验证集 mAP 看着不错但实际跑视频推理时所有检测框都标记为player即使画面上是明显的球。原因classes.txt中ball在第 1 行player在第 0 行但dataset.yaml里names列表我写成了[player, ball]。Ultralytics 内部按 yaml 的 names 生成映射表而标注文件中的第一列是从classes.txt复制的。两者顺序错位所有类别标签整体移了一位。模型学到的类别 0标注里 0 代表球在 yaml 中对应的是player推理时自然全部归为球员。解决统一所有文件的类别顺序。以dataset.yaml的 names 顺序为唯一标准写一段脚本批量修正 labels 里的索引值把0-1互换。之后重训问题消失。这个坑在几乎所有数据集中都有出现属于标注流程管理问题拿到数据集先统一类别顺序再训练永远不要默认标注文件里的索引是对的。5.2 案例二mAP50 很高但实际视频里小球疯狂漏检现象验证集 mAP50 达到 0.87球类但把模型跑在实拍比赛视频上约 60% 的球完全没框出来偶尔框出来的位置偏移也很大。原因验证集划分时用了随机划分同一段视频的相邻帧同时出现在训练集和验证集。模型实际是在「记住」特定画面的特征而不是学习「球」的通用特征。一旦输入换到没见过的视频帧泛化能力崩塌。解决严格按视频片段分组做数据划分见 2.3 节并且把验证集换成不同机位、不同场地的图像。划分后球类的mAP50从 0.87 降到 0.63——这个降幅让我后怕原来之前的指标全是水分。从此我给自己定了个规矩小数据集上验证集的 mAP 只要高于训练集 mAP 3 个点以上就要怀疑数据泄露。5.3 案例三训练时显存溢出OOM不是真的显存不够现象imgsz1280、batch16在 RTX 308010GB上直接 OOM。把 batch 降到 4 依然 OOM但跑 640 分辨率 batch32 都没问题。原因1280 分辨率下特征图尺寸是 640 的四倍长宽各两倍中间激活值的显存占用暴增。但batch4还 OOM 就不只是分辨率问题了——Ultralytics 默认会缓存图像到显存加速训练551 张 1280x1280 的缓存本身就占好几 GB。解决加参数cacheFalse关闭图像缓存让数据从磁盘流式加载。batch4, imgsz1280在 10GB 显存上可以跑起来代价是每个 epoch 的数据加载时间变长。如果还想加速把rectTrue加上Ultralytics 会对同一批次内的图像按宽高比分组减少 padding 浪费的像素计算。5.4 案例四训练数据增强把球「变没了」现象开着 mosaic 增强训练loss 始终降不下去球类 recall 从第 20 轮开始反而越来越低。原因Mosaic 会把四张图拼成一张在拼接过程中球小目标被切到拼接缝的概率很高。如果球恰好被切掉一半或完全丢失而标签文件仍然标了原始框模型学到的就是这个框对应区域内并没有球——相当于给模型喂了大量噪声样本。解决关闭或降低 mosaic 的启用概率。Ultralytics 中mosaic0.5表示 50% 概率启用我直接改成mosaic0.0彻底关闭换用我自己写的 Albumentations 增强管线只包含运动模糊、轻微缩放和颜色抖动。开启close_mosaic10参数最后 10 轮关闭 mosaic也能缓解但 551 张图的数据量很小我倾向于从源头避开 mosaic 的风险。5.5 案例五用best.pt做推理结果反而比last.pt差现象训练结束时best.pt验证 mAP 是 0.72last.pt是 0.70但视频推理时last.pt的球类检测明显更稳定误检更少。原因best.pt是验证集上 mAP 最高的权重但验证集只有 55 张图统计波动极大。某一轮恰好在这 55 张图上表现好不代表泛化能力更强。小验证集上的指标噪声有时候超过训练本身的提升幅度导致选出的「最优」权重反而是过拟合验证集的产物。解决不要只信best.pt。把训练日志里的confusion_matrix.png和results.csv打开看最后 20 轮的 P/R 曲线走势。如果best.pt出现在末轮附近比如第 87 轮而末轮的 P/R 值接近可以直接用last.pt。我现在的习惯是保留两个权重用一段 30 秒的典型比赛视频做目测对比选实际效果好的——指标服务于场景不是场景服务于指标。6. 把训练好的模型做成能用的网球视频分析工具推理、弹球检测与参数固化模型训好后下一步是把它从「验证集表现不错」变成「真实视频里可靠工作」。这里有一个关键技巧不要直接用默认的 conf-thres0.25 推理先做 3 段不同场景视频的参数扫描。# 对一段比赛视频做参数扫描 yolo detect predict modelbest.pt sourcematch_clip.mp4 conf0.1 iou0.45 saveTrue yolo detect predict modelbest.pt sourcematch_clip.mp4 conf0.3 iou0.45 saveTrue yolo detect predict modelbest.pt sourcematch_clip.mp4 conf0.5 iou0.45 saveTrueconf从 0.1 到 0.5 分别跑一遍目测三组结果的差异。对于网球场景我观察到conf0.1时球员误检明显球员肢体容易和背景混淆被当作球conf0.5时球漏检率高。通常conf0.2~0.3是甜点区——球类置信度天然低于球员类约 0.15 左右因为球的纹理特征太弱。扫参的意义在于找到每个类别的差异化置信度阈值Ultralytics 允许在predict时传入classes[0]单独调球的阈值但更方便的是推理后用后处理过滤from ultralytics import YOLO model YOLO(best.pt) results model.predict(match_clip.mp4, conf0.15, iou0.45, verboseFalse) for r in results: boxes r.boxes cls boxes.cls.cpu().numpy() conf boxes.conf.cpu().numpy() # 球类0用更高阈值球员1用更低阈值 mask ((cls 0) (conf 0.25)) | ((cls 1) (conf 0.35)) filtered boxes[mask] # 此时 filtered 就是球和球员的最终检测结果这段代码的逻辑是后置差异化过滤球类置信度普遍低保留 0.25 以上的候选球员类置信度高提升到 0.35 过滤掉低质量误检。实际跑下来球类的 precision 提升约 8 个点代价是 recall 下降约 3 个点在弹球分析场景中误检比漏检更致命——误检的球会打乱轨迹跟踪逻辑。再进一步551 张图的模型直接做弹球轨迹分析是不够的——漏检率在快速击球瞬间会飙升。我常用的补救方案是ByteTrack 多目标跟踪 卡尔曼滤波插值用球员和球的检测框做跨帧关联在漏检的帧上根据前一帧速度和位置插值补球。from boxmot import ByteTrack tracker ByteTrack() tracked_balls {} for frame_idx, r in enumerate(results): dets filter_detections(r) # 上一段的差异化过滤 tracks tracker.update(dets, frame_idx) for t in tracks: if t.cls 0: # ball tracked_balls[t.id] {last_pos: t.xyxy, last_frame: frame_idx}ByteTrack 的update接受检测框列表内部做 IoU 匹配和轨迹管理。漏检时轨迹不会立刻中断而是保留一个缓冲期下几帧检测到球后能重新接上轨迹。这一步做完模型才真正能在「统计球员击球次数」「判断球是否出界」这类下游任务中顶用。最后说一个真实教训。我之前为了追求验证集 mAP花了大量时间调class_weights和增强参数把球类的 AP 从 0.55 磨到 0.68。但跑实际视频时发现瓶颈根本不在模型——是输入分辨率低了球的像素不够。把推理分辨率从 640 提到 1280 后球类 recall 直接涨了 15 个点。先检查输入分辨率和置信度阈值再回头调模型结构这个顺序能帮你少走很多弯路。希望这篇关于 551 张图像训练全流程的记录能帮到你尤其在你被小目标检测折磨得想放弃的时候——先加分辨率再谈其他。本文还有配套的精品资源点击获取
返回列表