
简介手骨骨龄检测是医学影像与深度学习结合的典型课题。这套基于Python与YOLOv5的实现方案面向计算机视觉方向的毕业设计、课程设计与项目开发覆盖从数据处理、模型训练到效果验证的完整流程适合具备Python基础并希望快速搭建检测系统的学习者参考。压缩包共189个文件大小约436.79MB以py源码、yaml配置、pt权重、md文档和ipynb示例为核心另含shell脚本、Dockerfile及少量图片数据文件类型多样、分区清晰便于按模块查阅和二次开发目前已有198人学习下载。源码经过严格测试预处理部分针对骨龄影像常见的雾感问题采用直方图均衡化与CLAHE增强并完成数据集切分、图像增广至每份1800张检测侧调整bone.yaml、yolov5s.yaml与train.py中的参数完成YOLOv5训练分类侧使用ResNet18等网络对DIP、MCP、MIP、PIP、Radius、Ulna等手骨关键区域进行识别测试准确度达到90%以上。随包附带项目文档和视频演示能帮助理解整体训练链路与排错思路方便在此基础上扩展。1. 手骨骨龄检测为什么YOLOv5成了课程设计和毕业设计的首选方案医学影像相关的毕设里“手骨骨龄检测”是一个出现频率很高的题目。它既有真实业务背景——儿科内分泌科评估儿童生长发育时需要读骨龄又有公开数据集可以下载而且视觉特征相对清晰手骨X光片里骨化中心的出现、大小、形态和年龄强相关。不少学生选这个题时会直接用 Python YOLOv5 来做因为 YOLOv5 生态成熟、训练资料多、容易出效果一套代码就能同时覆盖“目标检测”和“年龄回归”两条线评阅老师看到的是可交互的演示视频而不是一张干巴巴的曲线图。这篇文章会从数据集准备、环境配置、模型训练、常见坑到骨龄后处理把你自己复现一套手骨骨龄检测系统的完整路径讲清楚新手能照做熟手能拿去做参数调整的参照。2. 把骨龄检测拆成目标检测任务数据集准备与标注格式转换2.1 手骨X光片上到底要检测什么骨化中心与评级区域骨龄检测在放射科医生眼里是有一套成熟标准的最常见的是 Greulich-PyleGP图谱法和 Tanner-WhitehouseTW3法。GP 法简单粗暴把待测 X 光片和标准图谱放在一起找最像的那张直接读出骨龄。TW3 法则要细得多它把腕骨、指骨、掌骨、桡骨远端的骨化中心按成熟度分成了若干个等级每个等级对应一个分数最后求和查出骨龄。用 YOLOv5 做骨龄检测本质上是在模仿 TW3 的观察过程先检测出手骨X光片里感兴趣的关键区域——腕骨骨化中心、桡骨远端、尺骨远端、各指骨的骨骺板然后根据检测结果的数量、大小或成熟度评分去映射骨龄。课程设计通常不会做完整的多等级分类因为那需要非常精细的标注。简化做法是标注一个或者几个类别比如把“腕骨骨化中心”作为一个类别检测出每个骨化中心再统计数量用数量去近似骨龄。虽然精度不高但作为毕设演示和验证流程完全够用而且能让你把整个训练链路跑通。这里的关键是目标检测模型输出的不是“年龄”而是“位置和类别”。你必须先有一个明确的检测目标才能设计标注方案。我的建议是第一版只做一个类别名字叫ossification代表所有可见的骨化中心区域后续再加类别比如radius、ulna、phalanx这样可以对应 TW3 的不同评分点。类别越少标注成本越低模型也更容易训练。2.2 从 RSNA/TW2 数据集到 YOLO 格式标注转换脚本公开数据集中RSNA 骨龄检测挑战赛的数据集最容易拿到。它的原始标注是每个样本对应一个实际骨龄值月龄或岁数并没有目标检测框。所以你需要做两件事一是自己标注框二是把标注转成 YOLO 需要的 txt 格式。自己标注可以用 LabelImg 或 Labelme导出为 PASCAL VOC 的 XML 或 COCO 的 JSON然后再转成 YOLO 格式。YOLO 的标注格式是每行一个目标class x_center y_center width height坐标都是归一化后的值0~1由像素坐标除以图片宽高得到。下面是一个把 COCO JSON 转成 YOLO txt 的常用脚本我一般会放在tools/convert_coco2yolo.pyimport os import json import cv2 def convert_coco_json_to_yolo(coco_json_path, image_dir, output_dir): with open(coco_json_path, r) as f: data json.load(f) # 建立图片id到文件名的映射 img_dict {} for img in data[images]: img_id img[id] img_dict[img_id] { file_name: img[file_name], width: img[width], height: img[height] } # 类别id到类别索引的映射YOLO类别索引从0开始 categories {} for idx, cat in enumerate(data[categories]): categories[cat[id]] idx # 按图片id分组整理检测框 annotations_by_img {} for ann in data[annotations]: img_id ann[image_id] annotations_by_img.setdefault(img_id, []).append(ann) os.makedirs(output_dir, exist_okTrue) for img_id, anns in annotations_by_img.items(): img_info img_dict[img_id] width img_info[width] height img_info[height] base_name os.path.splitext(img_info[file_name])[0] with open(os.path.join(output_dir, base_name .txt), w) as f: for ann in anns: cls categories[ann[category_id]] # COCO的bbox是[x, y, width, height]注意是左上角坐标 x, y, w, h ann[bbox] x_center (x w / 2) / width y_center (y h / 2) / height w_norm w / width h_norm h / height # 避免越界和零尺寸目标 if w_norm 0 or h_norm 0: continue f.write(f{cls} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(f转换完成输出目录{output_dir})这个脚本的核心逻辑是读取 COCO 的 JSON 文件把每个检测框的左上角坐标转成中心点坐标再除以图片宽高得到归一化坐标。参数说明coco_json_path是你的标注文件路径image_dir是存放原始图片的目录脚本里只用来映射文件名不会重新读图片像素output_dir是生成的标签目录。注意COCO 的bbox是[x, y, width, height]而 YOLO 需要的是[x_center, y_center, width, height]所以转换时必须用x w/2和y h/2这一步出错会导致所有框偏移半个框距训练直接翻车。转换完成后你还需要生成一个bone.yaml数据集配置文件内容大致如下# 数据集根目录 path: ./datasets/bone # 数据集根路径 train: images/train # 训练集图片文件夹 val: images/val # 验证集图片文件夹 # 类别数这里的nc必须与classes一致 nc: 1 # 类别名称从0开始索引 names: [ossification]path指向数据集根目录train和val是相对路径。YOLOv5 会从这两个目录里读取所有图片并在同名的 txt 文件上找标注比如images/train/001.png会找labels/train/001.txt。很多新手会把 labels 文件夹放错位置导致训练时一个框都没加载进来。2.3 数据增强与训练集划分的参数策略YOLOv5 自带了一套默认数据增强策略定义在data/hyp.scratch.yaml里。对骨龄检测这种目标相对小、背景相对干净的医学图像我一般会做三处调整。第一是关掉 Mosaic 或者把 Mosaic 概率调低因为手骨的骨骼区域相互有解剖结构关系Mosaic 会把四张不同手的 X 光片拼在一起让模型学到扭曲的上下文第二是调低 HSV 饱和度和亮度扰动把hsv_s: 0.0、hsv_v: 0.1设为较低值因为 X 光片是灰度图颜色扰动没有意义第三是把fliplr: 0.5保留左右翻转对骨骼形态是合理的。训练集和验证集的划分我习惯按 8:1:1 分为 train / val / test并且先按病人 ID 分组再划分避免同一病人的左右手或不同角度照片同时出现在训练和验证集里。如果直接随机 split验证集会包含训练时见过的病人mAP 会虚高后期部署到新手上效果不稳定。划分完以后最好再写一段代码检查每张图片的 txt 文件是否都存在以及标注框是否都在图片边界内。import os def check_labels(img_dir, label_dir): img_files os.listdir(img_dir) missing [] for img in img_files: base os.path.splitext(img)[0] label_file os.path.join(label_dir, base .txt) if not os.path.exists(label_file): missing.append(img) if missing: print(以下图片缺少标注文件) for m in missing: print(m) else: print(所有图片都有对应的标注文件)这个检查看似简单但能避免训练时静默跳过无标注图片。要知道YOLOv5 在加载数据时如果找不到某张图的标注文件默认会直接跳过而不是报错。如果你有一批图没标注模型会在“部分样本无监督”的情况下训练Loss 会偏高但你不一定能第一时间察觉。3. 搭建 YOLOv5 训练环境Python 环境配置与依赖安装的完整步骤3.1 Python 虚拟环境与 CUDA 版本匹配yolov5 环境配置的常见做法YOLOv5 官方推荐的 Python 版本是 3.8 及以上实际测试 3.9、3.10 都没问题。但注意PyTorch 对 Python 版本有官方支持范围如果 Python 版本太新比如 3.12有些预编译的 CUDA 绑定可能还没有到时候 pip 安装会触发源码编译非常痛苦。我一般用 Anaconda 创建虚拟环境先固定 Python 版本。# 创建虚拟环境 conda create -n yolov5 python3.9 -y # 激活环境 conda activate yolov5激活环境后下一步是安装 PyTorch。这一步最容易出错的是 CUDA 版本和 PyTorch 版本不匹配。你可以先用nvidia-smi查看自己显卡驱动的 CUDA 版本注意这不是运行时版本而是驱动支持的最大版本。然后去 PyTorch 官网选择对应的安装命令。比如 CUDA 11.8 对应# 安装 PyTorch with CUDA 11.8 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果你只是用 CPU 跑小实验可以装 CPU 版pip install torch torchvision torchaudioCPU 版训练会非常慢一张 640×640 的图片要好几秒但至少能把流程跑通。对于毕业设计最好还是找一台有 NVIDIA 显卡的机器哪怕 GTX 1060 6G 也能勉强训练小模型。另外看完 PyTorch 之后安装 YOLOv5 依赖之前先确认torch.cuda.is_available()是否为 True避免后面训练时莫名其妙跑到 CPU 上。3.2 下载 YOLOv5 源码与预训练权重跑通 Detect 验证环境YOLOv5 的源码托管在 GitHub 上仓库名就是ultralytics/yolov5。你不需要去搜索什么破解版或者二次开发版官方仓库就是最稳定、文档最全的。下载方式有两种直接下载 zip 包或者用 git clone。我推荐 git clone因为后续要切 tag、拉新 package 都方便。git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtrequirements.txt是依赖清单包含了opencv-python、matplotlib、pandas、seaborn等常用库。安装完以后建议先跑一次官方预训练模型的检测验证整个环境是通的# 下载官方预训练权重并运行检测 python detect.py --weights yolov5s.pt --source data/images/bus.jpgdetect.py的--source参数可以传图片路径、文件夹路径、视频路径或者 0 代表摄像头。第一次运行会自动从官方权重地址下载yolov5s.pt如果网络不通也可以手动去 GitHub Releases 页面下载再放到根目录。运行成功后在runs/detect/exp目录下会生成标注了检测框的图片。这一步走通说明环境配置没问题后面训练自己的数据集才能跑起来。下载预训练模型这一环节很多同学喜欢折腾“最新版”或者“增强版”其实没必要。YOLOv5 的 s / m / l / x 系列里课程设计选yolov5s.pt就够了体积小、速度快、对硬件要求低如果你追求更高精度再考虑yolov5m.pt。更大的模型在骨龄检测这种小目标场景下不一定有明显收益反而显著提高训练时间和显存占用。3.3 训练脚本的启动命令与关键参数yolov5 超参数解读环境验证通过后你就可以开始训练自己的骨龄检测模型了。最标准的训练命令长这样python train.py \ --data bone.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --img 640 \ --cache \ --device 0这里每个都值得说清楚。--data指向你刚才写好的bone.yaml--weights是预训练权重路径它决定了你是在预训练模型基础上微调还是从随机权重开始训练--epochs是训练轮数课程设计一般 100 轮左右能收敛--batch-size是单卡一次处理的图片数这个值受显存限制不能盲调大--img是训练时缩放后的图片边长YOLOv5 会等比例缩放并填充640 是默认值也是速度和精度的平衡点--cache表示把图片提前加载到内存里能省去每轮运行时的磁盘 IO但如果数据集太大内存不够建议去掉--device 0指定用 0 号显卡如果没有 GPU 就改成--device cpu但要做好训练一天的心理准备。YOLOv5 也支持通过--hyp指定超参数文件默认是data/hyp.scratch.yaml。你可以复制一份然后改里面的初始学习率lr0、L2 正则化系数weight_decay、以及增强参数。比如手骨数据集样本量不大可以适当降低lr0从 0.01 到 0.005避免后期振荡。4. 训练手骨检测模型从零开始训练与迁移学习的选型4.1 迁移学习 vs 从零训练什么时候用 yolov5s.pt什么时候用空权重训练自己的模型时--weights参数有两种典型写法使用预训练权重--weights yolov5s.pt模型会加载在 COCO 上训练好的所有权重然后继续在你的骨龄数据上微调。随机初始化权重--weights 或--weights yolov5s.yaml模型从零开始学。从零训练不是不行但需要足够多的数据和更长的训练时间。手骨骨龄数据集一般几百到几千张而 COCO 预训练模型已经学会了基本的纹理和边缘特征迁移学习能让你在 50 轮内达到从零训练 200 轮的效果。我建议一律先用预训练权重除非你想深入研究网络结构本身。这里有一个容易踩的细节使用预训练权重时YOLOv5 默认会冻结前几层后面处理模块如果你的数据分布和 COCO 差异很大X 光片和自然照片差异确实很大冻结前层可能限制模型的表达能力。你可以在训练命令里加--freeze 0表示不冻结任何层让整个网络都适应灰度图像。对于课程设计建议加--freeze 0因为手骨 X 光片和 COCO 的域差异实在太大保留预训练权重只为了初始梯度方向稳定全部层参与微调能更好适应新域。Python 环境中预训练权重的下载无需额外操作train.py会检测本地文件是否存在如果不存在会自动下载。如果你有多个数据集要训练最好手动把预训练权重放到项目根目录避免每次重复下载。4.2 训练中的 Loss 曲线诊断如何判断模型真的在收敛训练开始后终端会打印每轮的 loss 和指标。YOLOv5 的 loss 分为三部分box_loss预测框与真实框的 IoU 损失、obj_loss目标置信度损失、cls_loss类别分类损失。对于单类别任务cls_loss很小是正常的核心看box_loss和obj_loss。正常情况下这些 loss 会随着 epoch 下降然后趋于平缓。但要注意训练集 loss 和验证集 loss 的关系才是重点。如果你看到训练集 loss 持续下降验证集 loss 在第 30 轮左右开始回升这就是过拟合信号。手骨数据集通常不大过拟合非常常见。缓解手段包括增大数据增强、加--dropoutYOLOv5 没直接暴露 dropout但可以通过--hyp调整mixup、关闭 Mosaic 的权重、或者缩小模型换yolov5s为yolov5n。我一般会在每轮训练结束后去runs/train/exp目录看两张图results.png和val_batch0_pred.jpg。前者把所有指标曲线画在一起后者是验证集第一批图片的预测叠加。如果发现预测框几乎都框在骨头边缘或背景说明模型学到了错误的特征这时候先别急着调参数回去检查标注质量。4.3 模型评估mAP、Precision、Recall、Confusion Matrix 怎么看训练结束后终端会打印一个汇总表里面有mAP0.5和mAP0.5:0.95等指标。这两个指标是目标检测领域的通用评价标准。mAP0.5表示 IoU 阈值取 0.5 时的平均精度课程设计做到 0.85 以上就算不错mAP0.5:0.95是更严格的指标从 0.5 到 0.95 间隔 0.05 取十个 IoU 阈值再求平均数值会偏低但更有参考意义。对于骨龄检测你还需要关注Precision和Recall的平衡。如果Precision高但Recall低说明模型漏检多骨化中心没找全骨龄评分会偏低如果Recall高但Precision低说明误检多虚警框会影响计数评分会偏高。你可以通过调整--conf-thres和--iou-thres来适应应用侧需求。比如部署到演示视频里要求不遗漏重要骨化中心可以把置信度阈值调低到 0.3牺牲一点精确度换召回。关于类别混淆因为只有单类别Confusion Matrix很简单主要看背景被误判为骨化中心的比例。如果这个比例高可能是标注时把太多含软组织的区域框进去了也可能是模型过拟合了背景纹理。5. 手骨检测模型训练常见问题排查数据、显存、过拟合三大类踩坑记录5.1 训练 Loss 不降反升现象、原因与解决现象box_loss在训练到第 10 轮以后不但没有下降反而从 0.08 爬升到 0.12验证集 mAP 也一直停在 0.3 附近。原因最常见的是学习率设置过高导致优化过程在损失曲面边缘反复弹跳。另一种常见情况是标注文件里的坐标值超出 0~1 范围YOLOv5 在加载时做了归一化失败把很多目标当成了背景参与 loss 计算。解决先用--hyp文件降低初始学习率比如把lr0: 0.01改成lr0: 0.003同时把lrf: 0.2终点学习率比例调大一点让学习率下降更平缓。然后严格检查标签文件写脚本统计每个 txt 文件中是否有坐标大于 1 或者小于 0 的值import os label_dir ./labels/train for file in os.listdir(label_dir): with open(os.path.join(label_dir, file)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f{file} 格式错误) break x_center, y_center, w, h map(float, parts[1:]) if not (0 x_center 1 and 0 y_center 1): print(f{file} 中心坐标越界: {parts}) break另外检查类别索引是否从 0 开始。如果标注工具导出的是从 1 开始而 YAML 里nc又是 1那所有类别索引都会被当成背景训练效果自然拉胯。这个检查成本很低每次做新数据集我都会先跑一遍。5.2 显存 OOMbatch_size 与图像尺寸的权衡现象训练命令执行到第一个前向传播就报错RuntimeError: CUDA out of memory卡在 8GB 显存左右。原因--img 640加上默认的--batch-size 16在 8GB 显存的 GTX 1080 上很容易爆掉。YOLOv5 的显存占用与 batch size 和图片面积成正比图片边长翻倍显存占用约翻 4 倍。解决优先调小--batch-size比如从 16 降到 8如果还报错就降到 4。课程设计数据集本身不大batch size 小一点只是训练时间变长收敛效果不会差多少。其次可以调--img从 640 降到 512。注意--img是训练尺寸推理时可以单独指定更大或更小的尺寸。还可以开启--cache减少磁盘 IO但显存不够时这个选项反而帮不上忙。如果显存是 4GB建议直接用yolov5n--img 512--batch-size 4不要硬上yolov5s。5.3 检测框偏移与漏检标注边界框过小或不准确的修正现象训练完成后在验证集上很多骨化中心的实际位置是空的而背景区域却出现了一批置信度高的框。把预测图放大看框要么偏上要么偏下根本没有贴住骨头边缘。原因这个问题八成出在标注上。骨化中心在 X 光片上通常是白色的小片区域如果标注时只框了中心亮点没有包含整个骨化轮廓YOLO 在回归边界框时就会一直学不到合适的宽高。另外手骨 X 光片边缘外侧有黑色背景如果标注框内混入大量背景空白模型学到的是“浅色区域周围深色背景”的模式遇到对比度变化就会偏。解决重新打开 LabelImg用“笔触”模式精确沿着骨化中心边缘描点生成多边形后转矩形框。若嫌工作量太大至少保证每类样本在不同手型、不同年龄上有足够变化。检查一下标注框的宽度高度分布如果大多集中在 10×10 像素以下说明目标尺度太小可以提升--img到 704 或 768让模型在更高分辨率下看到小目标。5.4 验证集 Mosaic 污染为什么 mAP 虚高但实测拉胯现象训练时 mAP0.5 到了 0.95但把模型部署到摄像头或者新图片视频上时经常漏检或者误检。原因YOLOv5 默认在训练集和验证集上都会做 Mosaic、MixUp 等增强。如果val阶段开启了增强验证集的预测图不是真实的原始图像模型看到的验证数据是被拼接、翻转、加噪声过的算出来的 mAP 自然虚高。更隐蔽的问题是train.py 在每轮验证时用的是与训练相同的预处理流水线没有单独关闭增强。解决使用 YOLOv5 官方推荐做法——在val.py推理时用原始图或者用detect.py对原始图片做一批预测统计。手动检查训练脚本中--val阶段的增强配置确认在验证期间关闭 Mosaic 和 MixUp。通常做法是修改hyp文件把mosaic: 0.0和mixup: 0.0设置在验证阶段不生效或者直接跑单独的val.py来评估模型python val.py --data bone.yaml --weights runs/train/exp/weights/best.pt --img 640val.py默认不做训练阶段的恒等增强得到的 mAP 更可信。如果你发现这个 mAP 比训练过程中打印的低于 5 个百分点以上就要审视一下训练时的验证评测是不是被增强“骗了”。6. 把检测结果换算成骨龄后处理与评分逻辑的落地技巧6.1 从检测框到骨龄评分的映射逻辑模型训练完你得到的是一个检测器而不是骨龄计算器。要把检测框数量转换成骨龄需要写一个后处理脚本。最简化的 PW 风格做法是检测手部 X 光片中的腕骨骨化中心骨化中心数量越少骨龄越小随着年龄增长骨化中心逐渐增多并融合。一个粗糙但能自洽的公式是estimated_age min_age (max_age - min_age) * (num_ossification_centers / max_centers)。更讲究一点的做法是按解剖位置把检测框分组比如腕骨区的框计数桡骨区和尺骨区的框单独计数再查 TW3 评分表。我给你一个可运行的示例展示如何把 YOLO 检测输出变成骨龄值import cv2 import torch def detect_and_predict_age(image_path, model, devicecpu): # 加载图像 img cv2.imread(image_path) results model(img, devicedevice) # YOLOv5的模型输入是numpy数组或路径 detections results.pandas().xyxy[0] # 返回DataFrame包含每一检测框 # 假设只有一个类别ossification boxes detections[detections[confidence] 0.3] num_centers len(boxes) # 简单映射假定最大可见骨化中心为10个对应年龄18岁 estimated_age round((num_centers / 10) * 18, 1) return estimated_age, boxes这个脚本里的model是你用torch.hub.load(ultralytics/yolov5, custom, pathbest.pt)加载出来的模型。confidence阈值设定很关键阈值太高会漏检阈值太低会把软组织误判成骨化中心。课程里可以先试0.250.4在验证集上挑一个令计数最稳定的值。如果你觉得这种线性映射太简陋可以再进一步直接用检测到的骨化中心面积总和作为评分特征加一个简单的线性回归。甚至可以训练一个小型 MLP输入每个检测框的面积、数量、位置坐标输出骨龄。这个方案的优势是让后处理不再是“拍脑袋公式”而是一个有监督学习层但需要额外准备一批 X 光片和真实骨龄标签。对于毕设而言线性映射已经足够讲清原理再加上误差对比表就很有说服力。6.2 生成检测视频演示用 Detect 输出并叠加结果视频演示是课程设计和毕业设计验收的重头戏。YOLOv5 自带的detect.py支持直接处理视频会把检测框实时叠加到视频帧上省去你自己写 OpenCV 循环的功夫python detect.py \ --weights runs/train/exp/weights/best.pt \ --source demo.mp4 \ --conf-thres 0.3 \ --save-txt \ --project runs/detect_demo--source传入视频路径输出的标注视频保存在runs/detect_demo/exp下--save-txt会额外把每帧的检测结果保存为 txt 文件方便你用来统计骨化中心数量。如果你想把骨龄估算结果也显示在视频画面上可以改用下面的 Python 方式逐帧处理import cv2 from ultralytics import YOLO # YOLOv5的最新推理API也支持这个用法 model YOLO(runs/train/exp/weights/best.pt) cap cv2.VideoCapture(demo.mp4) writer cv2.VideoWriter(output.mp4, cv2.VideoWriter_fourcc(*mp4v), 30, (1280, 720)) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame) annotated results[0].plot() # 绘制检测框 # 这里可以获取检测数量并绘制文本 cv2.putText(annotated, fCenters: {len(results[0].boxes)}, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) writer.write(annotated) cap.release() writer.release()注意results[0].boxes返回的是检测框对象列表长度就是当前帧检测到的骨化中心数。视频分辨率要尽量和训练尺寸一致否则小骨化中心可能因为缩放变形而漏检。我自己做项目时习惯把每个 epoch 的验证集预测图保存下来翻车时能看到是漏检还是误检时间长了就发现训练曲线只能反映趋势真正的模型质量还得看实际预测图。这个习惯帮我少走了很多弯路也希望帮到你。本文还有配套的精品资源点击获取