我接触目标检测这五年多,见过太多人把 YOLO 用得稀里糊涂:训练脚本照抄,loss 降了就说"跑通了",实际一测全是漏检;换了个数据集,指标直接崩盘;好不容易训出个模型,导出部署又卡半个月。这个领域看似门槛低——毕竟 YOLO 的代码仓库做得足够傻瓜化,填好参数就能跑——但真正能落地、能稳定交付的,靠的从来不是那几个命令,而是对检测原理、数据质量、训练节奏和部署链路的完整理解。这篇东西不打算给你复述论文公式,我想按我自己做项目的经验,把"目标检测 + YOLO"这条线从头到尾捋一遍:YOLO 到底在干什么,各个版本应该怎么选,数据要怎么做才不坑,训练到什么程度算好,部署有哪些隐藏暗坑。适合两类人看:一类是刚入门的同学,看完能建立一套正确的直觉;另一类是已经在用 YOLO 但总在项目后期返工的朋友,我会把容易出问题的环节全部点出来。
1. YOLO 的进化主线:从"暴力网格"到"工程化全家桶"
很多人以为 YOLO 就是 YOLOv5,或者最新版是 YOLOv8,其实 YOLO 已经迭代到 YOLO11(2024 年 9 月发布)。理解它为什么能活这么多代,关键是看它解决检测问题的最初思路,以及后来每一步改了什么痛点。
1.1 单阶段检测的核心思想:把检测当回归
目标检测要回答两个问题:框在哪、是什么。传统 two-stage 思路(如 Faster R-CNN 一脉)是先让网络"提议"一批可能包含目标的区域,再逐个区域做分类和坐标修正,特点是精度高但慢。YOLO 的创始团队选择了一条更激进的路:把检测直接定义成一个端到端的回归问题。
最早 YOLOv1 的做法可以这样理解:把整张图均匀切成 S×S 个网格,每个网格负责预测固定数量的边界框、置信度和类别概率。某个目标的中心点落在哪个网格,就由那个网格"负责"预测它。这种设计有个致命的物理局限——每个网格只认落在自己区域内的目标,如果两个目标靠得很近或者一个目标特别小,网格数量和感受野根本顾不过来。所以 v1 在密集小目标场景效果差是先天缺陷,不是调参能救的。
但这条路的关键意义在于:它第一次证明了一个卷积网络可以在一次前向传播里直接输出全部检测结果。此后所有 YOLO 版本都没有离开这个基本框架——单阶段、稀疏预测、全图感受野——只是在"如何让网格更聪明地承担责任"上不断打补丁。
1.2 从 v2 到 v5:anchor、多尺度、工程化三条主线
YOLOv2 引入了 anchor box(先验框)机制。道理不复杂:直接回归任意宽高比的框太难了,不如预先设定几个常见比例(比如人形框、车形框),网络只负责预测"相对于这些先验框的偏移量"。这让训练早期稳定很多。v2 还加了批归一化和高分辨率输入,这些在当年都是实用增益。
YOLOv3 是承上启下的关键版本,核心贡献是特征金字塔(FPN 结构)。它让网络在多个尺度上各自独立做检测——浅层特征图保留细节,负责小目标;深层特征图语义强,负责大目标。v3 之后的 YOLO 基本都沿用了"多尺度输出"这个骨架。从 v3 开始,YOLO 在工程上的可用性大增,很多老项目到现在还在用 v3 的魔改版本。
YOLOv5 的贡献不在算法革新,而在工程化体验。它把训练、验证、导出、部署整个链路做了极致的封装,PyTorch 生态、清晰的仓库结构、丰富的预训练权重、一键导出多种格式。我可以直说:YOLOv5 是很多公司的生产主力,哪怕 v8、v11 已经出了,存量项目迁移成本高,大家依然用 v5。它的 model zoo 成熟度是经过大规模社区验证的。
1.3 v8 与 v11:anchor-free、C2f 结构和任务的统一
YOLOv8 是 2023 年的重要分水岭,主要变化有三个:一是从 anchor-based 转成 anchor-free(不再预设先验框,直接预测目标中心点到四条边的距离),简化了后处理,对各类形状的目标更友好;二是主干网络用了 C2f 结构,梯度流更丰富,在同等参数量下特征表达能力更强;三是把检测、分割、姿态估计、分类统一到一个框架,同一套代码可以训多种任务。
YOLO11 更像是一个改进版:在 C2f 基础上进一步优化了特征融合路径,减少了参数量但提升了精度,推理速度也更快。如果你在新项目里,没有历史包袱,我建议直接考虑 v8 或 v11,后面选型部分我会展开说。
我用一句话总结这个主线:YOLO 的进化史就是"检测思想定型 → 多尺度能力补全 → 工程化封装 → 无锚框版本迭代"的过程。理解了这条线,你拿到任何一个版本,都能快速定位它在整个谱系里的位置,而不是看完 README 还是一个黑盒。
2. 版本选型决策:按场景选模型,别按编号迷信新版本
选 YOLO 版本这件事,我做项目时遇到最多的问题就是"用最新的总没错吧?"。其实不是。部署环境算力、任务类型、团队熟悉度、对外交付约束,每一条都可能把"最新版"的性价比打到很低。
2.1 三大主流版本的横向对比
我整理了一张表,方便你对着自己的场景做初步判断:
| 对比项 | YOLOv5 | YOLOv8 | YOLO11 |
|---|---|---|---|
| Anchor 方式 | Anchor-based | Anchor-free | Anchor-free |
| 核心结构 | CSPDarknet + PANet | C2f + PANet | C2f 改进版 + 融合优化 |
| 支持任务 | 检测 | 检测/分割/姿态/分类 | 检测/分割/姿态/分类/旋转框 |
| 预训练模型丰富度 | 极高(社区生态最成熟) | 高 | 中等(新发布) |
| 部署资料量 | 最多,遇到坑几乎都有答案 | 多 | 正在补齐 |
| 新增部署成本 | 最低 | 中 | 中 |
| 推荐倾向 | 存量维护、快速上线、找轮子 | 新项目首选、任务多样 | 探索新能力、对旋转框有需求 |
这里有个容易被忽略的点:如果你要分割或姿态估计,v8 的文档和教程比 v11 丰富得多,遇到 bug 时能找到的解决方案也多。我做过一个分割项目,当时 v8-seg 的坑在网上几乎都有人踩过,换个 v11 就得自己翻源码查 issue。
2.2 我自己的选型建议
三个原则供参考:
第一,新项目无特殊需求优先 YOLOv8。理由不是它比 v11 强,而是稳定性、社区成熟度、配套工具链的平衡最好。Ultralytics 库对 v8 的支持最完善,API 稳定,遇到问题基本都能搜到方案。
第二,老项目不要轻易换版本。我见过一个项目用 v5 训练好的权重直接上线,精度够用,推理速度也满足。团队为了"技术先进性"迁到 v8,结果重新标数据、重新调参数花了三个月,最后精度和 v5 持平。除非你有明确的新需求(比如要加分割分支),否则迁移的 ROI 多半为负。
第三,算力受限场景优先考虑 nano 和 small 规格,不要硬上 large。很多嵌入式设备(比如 Jetson 系列)跑 YOLOv8n 已经能到 30 FPS 以上,而 YOLOv8l 可能只有 5 FPS。检测任务的精度瓶颈很多时候不在模型大小,而在数据质量。模型小了,数据上用心补,效果往往比单纯堆参数量更实在。
2.3 从任务类型反推选型
再细化一点:如果是标准水平框检测,v5、v8、v11 都可以,我更建议 v8;如果是实例分割或者姿态估计,v8 起步,v11 可以在有精力踩坑时尝试;如果是旋转框(比如遥感目标、工厂里的倾斜工件),v11 原生的 OBB 支持是最大优势,老版本需要找第三方实现,维护风险高。
提示:选版本时先列出你的部署环境和精度要求,再去 GitHub 看对应版本的已知 issue。我见过有人因为某个版本对特定 GPU 的 CUDA 版本不兼容,被迫把整个环境重装的。版本选型本质是选生态,不是选算法名。
3. 完整实操链路:从数据标注到模型训练
选完版本就该动真格了。这一节我按自己跑项目时的标准流程走一遍:环境搭建、数据准备、数据校验、训练启动、初步评估。每一步我都会说明"为什么这么做",而不是光给命令。
3.1 环境搭建与项目初始化
我实验室的新人入职第一件事不是跑模型,而是先把环境固定下来。YOLO 项目最烦的是环境漂移——今天能跑,下周换台机器就报错。我的做法是用 conda 建独立环境,Python 版本锁在 3.9~3.11 之间,PyTorch 版本根据 GPU 驱动挑匹配的 CUDA 版本,然后固定 requirements。
实际操作分三步:
conda create -n yolo python=3.10 conda activate yolo # 先装 PyTorch,用官方命令匹配你的 CUDA 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装 ultralytics pip install ultralytics装完后建议先跑一个官方预训练权重验证环境:
yolo detect predict model=yolov8n.pt source='https://ultralytics.com/images/bus.jpg'能正常出框说明环境通了,这一步能提前过滤 CUDA、torchvision 版本错位等低级问题。我见过有人数据准备了三天,最后发现是 torch 里没有匹配的 cuDNN,白白浪费。环境验证务必放在最前面。
3.2 数据标注:细节决定上限
数据是检测项目最大的成本项。标注质量直接决定模型效果上限,不是说算法不重要,而是当你只有几千张图时,标注的边界框是否精确、类别是否一致、难例是否覆盖,对模型的影响比换任何模型结构都大。
工具层面,我建议分场景选:
- 小数据集、个人项目:LabelImg 老牌稳定,标注格式原生支持 YOLO 的 txt 格式,每个框一行"class_id x_center y_center width height",值是归一化后的相对坐标。
- 中等规模、需要辅助功能:X-AnyLabeling 支持自动标注(可以接 SAM),能大幅提高效率,适合先跑一版模型再人工校正的半自动流程。
- 团队协作、多数据集管理:可以考虑 CVAT 自建标注平台,但初期成本高,小团队不推荐。
标注时的几个硬性要求,是我用教训换来的:
边界框要贴边。很多人习惯框得比目标稍微大一点或者小一点,这种不一致会让模型在训练时学到错误的边界。我验收标注时会随机抽 100 个框看边缘贴合度,误差超过 3 像素的重标。这个标准听起来苛刻,但对小目标提升非常明显。
遮挡、截断目标不要跳过。实际场景里目标经常被遮挡,如果训练数据里全是完整目标,模型遇到遮挡就崩。正确的做法是:遮挡超过 50% 的可以不标,但遮挡 20%~50% 的应该标出来,让模型学到"不完整但真实"的目标。
类别不平衡要预判。如果你的数据集里"A 类有 5000 张、B 类只有 200 张",训练出来的模型会天然偏向 A。解决方向不是强行采样到一样多,而是给 B 类做针对性增强(旋转、亮度扰动、裁剪),以及考虑在损失函数里加类别权重。
3.3 数据划分与目录组织
YOLO 训练前需要把数据集组织成约定目录结构。虽然直接用脚本改也能跑,但我建议一开始就按约定来:
datasets/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamltrain/val/test 的比例,我的经验是 8:1:1 起步;数据集很小(少于 1000 张)时,val 占比要更低一点,但绝对不要低于 5%,否则验证结果不稳定。划分时用随机种子固定,保证每次训练的可复现性。
data.yaml 是告诉模型"类别有哪些、图片在哪"的配置文件,典型的写法:
path: ./datasets train: images/train val: images/val test: images/test names: 0: person 1: car 2: bicycle注意 class id 从 0 开始,txt 标注里的第一个数字必须和 names 对应的索引一致。很多人后面发现模型输出张冠李戴,一半是这里写错了。
3.4 启动训练:参数怎么设才合理
环境、数据就绪后,训练本身反而是最简单的一步。我用一个实际能跑的命令来说明:
yolo detect train \ model=yolov8n.pt \ data=datasets/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20逐个解释为什么这么设:
- model=yolov8n.pt:加载预训练权重做迁移学习。除非你的数据集是全新的极端分布(比如医学影像的某些模式),否则永远建议从预训练权重开始,收敛快得多。
- epochs:不要盲目设成 300。我习惯先设 100,配合早停(patience=20)——20 个 epoch 内验证集 mAP 没有提升就停下来。很多人喜欢几万张图训 300 epoch,其实后期基本在过拟合。
- imgsz=640:默认值。用 640 能直接复用预训练权重的最优感受野。如果目标特别小(比如航拍图像),可以试 imgsz=1280,但要接受显存翻倍、速度变慢的代价。
- batch:单卡显存不够就先降 batch,而不是降图片尺寸。batch=16 对大多数场景是一个稳妥值,损失波动不会太大。
- lr0=0.01:迁移学习场景下 0.01 是常用起点,但要注意调大 batch 时学习率要相应调大(线性缩放法则),否则收敛偏慢。
训练过程中的输出我会重点关注两个指标:box_loss 和 cls_loss 的下降趋势,以及每个 epoch 结束后的 mAP50/mAP50-95。loss 降但 mAP 不涨,说明模型在过拟合训练集,验证集表现没跟上;mAP 一直在涨但涨得越来越慢,说明接近收敛,可以等早停触发。
4. 判断模型好坏:别被 loss 曲线骗了,要看评估指标
很多初学者训练完看 loss 降了就认为模型好了,这是个危险的误解。我的判断标准从来是验证集的 mAP 和 PR 曲线,loss 只是辅助参考。
4.1 核心指标解读
检测任务的核心指标是 mAP(mean Average Precision),它衡量的是模型在不同置信度阈值下,精确率和召回率的综合表现。
- mAP50 和 mAP50-95:mAP50 是 IoU 阈值 0.5 时的平均精度,偏宽松;mAP50-95 是在 0.5 到 0.95 之间取多个阈值再求平均,更严格、更接近真实场景。我一般看 mAP50-95,因为它对框的位置精度更敏感。如果 mAP50 很高但 mAP50-95 低,说明框有大致的正确位置,但边缘对齐不行。
- Precision(精确率)和 Recall(召回率):精确率是"模型认为是目标的里面有多少是对的",召回率是"真实目标里面模型找回了多少"。两者天然此消彼长。做安防项目时漏检比误报严重(召回优先),做质检项目时误报比漏检更烦(精确优先)。这个取舍要你自己根据业务定,YOLO 只是给你一个可调的置信度阈值。
4.2 可视化评估工具
训练结束后,我会跑验证集生成一套评估报告:
yolo detect val model=runs/detect/train/weights/best.pt data=datasets/data.yaml这个命令会输出混淆矩阵、PR 曲线、F1 曲线,还会把每个类别的 mAP 单独列出来。重点看混淆矩阵——它告诉你模型在哪两个类别之间最常混淆。比如"货架上的纸箱"和"货架上的塑料筐",形状相似、颜色相近,模型就会在这两者之间反复横跳。
再进一步,可以随机抽取几十张验证集图片做 batch inference,人工检查三类错误:漏检(图中明显目标没框出来)、误检(空背景出了框)、定位偏差(框和真实边界错位大于 10%)。这一步花不了一小时,但它是排查数据瓶颈的最快路径。
4.3 指标好不代表能上线
最后补一个坑:实验室指标和现场指标往往差很远。训练集用的是理想光照、标准视角,现场可能是逆光、模糊、低分辨率、遮挡叠加。所以我在正式交付前一定会做"现场样本回测"——找一批没有参与训练的真实场景图片,按流程跑推理,统计这个"额外测试集"的 mAP。如果比验证集低很多,说明模型泛化不够,问题大概率出在训练数据多样性和现场场景差异上。
5. 部署与推理加速:训练完只完成一半工作
训练出 mAP 满意的模型只是开始。很多项目死在最后一步——模型导不出来、推理太慢、算子不支持。我在这部分写一套我常用的部署流程:导出 ONNX、用 ONNX Runtime 推理、常见加速手段。
5.1 导出 ONNX 模型
ONNX 是现在部署的主流中间格式,它可以被 ONNX Runtime、OpenVINO、TensorRT 等推理引擎加载。我用 Ultralytics 自带的导出功能:
yolo export model=best.pt format=onnx imgsz=640 opset=12导出时注意三个点:imgsz 必须和训练时一致;opset 别设太低,否则有些算子不支持;如果要用 TensorRT,建议导出时加 dynamic=False 固定输入形状。
提示:导出后的 onnx 文件先用 Netron(在线看图)打开,确认网络结构里输入名、输出名都正常。我遇到过一次导出后输出维度少了一个分支的情况,用 Netron 一眼就发现了。
5.2 ONNX Runtime 推理代码
我给出一个开箱即用的 ONNX Runtime 推理脚本,配合 OpenCV 做预处理:
import cv2 import numpy as np import onnxruntime as ort class YOLODetector: def __init__(self, onnx_path, conf_thres=0.25, iou_thres=0.45): self.session = ort.InferenceSession(onnx_path) self.input_name = self.session.get_inputs()[0].name self.input_shape = self.session.get_inputs()[0].shape self.conf_thres = conf_thres self.iou_thres = iou_thres def preprocess(self, img, size=640): img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] r = min(size / h, size / w) new_w, new_h = int(w * r), int(h * r) img = cv2.resize(img, (new_w, new_h)) canvas = np.full((size, size, 3), 114, dtype=np.uint8) canvas[0:new_h, 0:new_w] = img img = canvas.astype(np.float32) / 255.0 img = img.transpose(2, 0, 1)[None] return img, r, (new_w, new_h) def postprocess(self, outputs, orig_shape, r, new_shape, conf_thres, iou_thres): # outputs shape: [1, 84, 8400],先转置成 [8400, 84] preds = outputs[0].transpose(0, 2, 1)[0] # [8400, 84] boxes = [] for pred in preds: class_scores = pred[4:] class_id = np.argmax(class_scores) conf = class_scores[class_id] if conf < conf_thres: continue cx, cy, bw, bh = pred[:4] x1 = (cx - bw / 2) / r y1 = (cy - bh / 2) / r x2 = (cx + bw / 2) / r y2 = (cy + bh / 2) / r boxes.append([x1, y1, x2, y2, conf, class_id]) if not boxes: return [] boxes = np.array(boxes) # NMS keep = cv2.dnn.NMSBoxes( boxes[:, :4].astype(np.float32).tolist(), boxes[:, 4].astype(np.float32).tolist(), conf_thres, iou_thres ) if keep is None: return [] keep = keep.flatten() return boxes[keep] def detect(self, img_path): img = cv2.imread(img_path) orig = img.copy() inp, r, new_shape = self.preprocess(img) outputs = self.session.run(None, {self.input_name: inp}) return self.postprocess(outputs, orig.shape, r, new_shape, self.conf_thres, self.iou_thres)核心逻辑拆成预处理、推理、后处理三块:
- 预处理做的是"等比缩放 + 填充到 640x640",避免直接拉伸导致目标变形。记录缩放比例 r,后处理时要把坐标还原回原图尺寸。
- 后处理把 ONNX 的输出从 [1, 84, 8400] 转成可读的框坐标。84 = 4(框坐标)+ 80(COCO 类数)。如果是你自己的数据集,这个数字要改成 4 + 类别数。
- NMS 用一个置信度阈值和一个 IoU 阈值,把重叠框合并成一个。IoU 阈值 0.45 是一个比较中庸的值,太低会重复出框,太高会把相邻的同类目标合并掉。
推理测试时我给一个实测参考:同一张 640x640 图片,用 PyTorch 推理大约 30 毫秒,ONNX Runtime CPU 大约 12 毫秒,GPU 上用 TensorRT 可以压到 3 毫秒以内。部署选型时心里要有个数。
5.3 推理加速三板斧
部署环节的加速方案,按优先级排三种:
换推理引擎。ONNX Runtime CPU 到 GPU 是免费提升;再进一步用 TensorRT(主要面向 NVIDIA 显卡),有层融合、精度校准这些优化,速度提升通常 2~4 倍。注意 TensorRT 对输入尺寸变化不友好,固定尺寸推理最佳。
输入尺寸剪枝。如果你的场景里目标不会特别小,把推理尺寸从 640 降到 480,整个推理时间可能下降 40%。我做过一个项目,原厂图 1080p,目标都是车辆,用 512 推理完全够用,速度翻倍。前提是要回测确认 mAP 损失在可接受范围。
批量推理。如果业务场景是离线处理一堆图片,可以使用动态批处理,一次送入 4~8 张,整体吞吐量比逐张推理高不少。在线视频流场景用不了这个,但离线任务非常香。
5.4 一个部署防线:预热与性能基准
正式上线前,我习惯做一次性能基准测试:取 1000 张真实图片,统计 P95 延迟和平均延迟。为什么看 P95?因为平均延迟会被少数极快图片拉低,一旦系统出现偶发卡顿,P95 才是用户体感。如果 P95 超过业务要求,优先降输入尺寸或换引擎,而不是去调后处理代码——后端优化的收益大得多。
6. 实战踩坑记录:我替你们踩过的那些雷
最后这部分,我把做检测项目时最常见的几类问题整理一下,每一条都是我在真实项目里踩过或者帮别人排查过的,按"现象 → 原因 → 解法"的思路写。
6.1 mAP 死活上不去,先查数据不查模型
之前有一个项目:检测工厂流水线上的特定零件,训练了 50 epoch,mAP50 卡在 0.6 不动。换个模型结构、调学习率都试了,没变化。最后我仔细看了训练集,发现标注 txt 文件里有个别框的宽高填反了——x_center、y_center 数值写成了像素值,没有归一化,导致那一部分框全偏到图外。清洗数据后,mAP50 直接跳到 0.85。
解法是每次训练前做一次数据校验脚本:解析全部标注,统计框坐标是否在 0~1 之间、宽高是否为正、类别索引是否合法、每张图和标注文件是否一一对应。这些检查一分钟就能跑完,却能把大量低级错误扼杀在训练前。
6.2 训练 loss 下降但验证集 mAP 不升,多半是过拟合在作祟
loss 下降是模型在记住训练集,mAP 不升是验证集表现没跟上,这是典型的过拟合信号。原因通常是训练轮次太长、数据增强不够、或者模型相对数据量过大。
我的处理顺序是:先看训练日记里 mAP50-95 的峰值出现在哪个 epoch,如果早停截止前 20 轮就已经是峰值,说明早就过拟合了。然后按两个方向修正:增强数据多样性(引入随机旋转、色彩抖动、Mosaic 增强),或者换小一号模型(比如 v8s 换成 v8n)。这里一个核心认知是:小模型对大数据的拟合可能不够,但大模型对小数据一定过拟合。
6.3 部署时 ONNX 输出和 PyTorch 不一致
我遇到过一次:PyTorch 模型跑得好好的,导出 ONNX 后推理,发现所有框的置信度明显偏低。排查了很久,最后发现是导出时没有固定输入图像的归一化方式。PyTorch 训练时的预处理可能是 /255.0,但 ONNX Runtime 推理脚本里用了 /127.5 - 1(ImageNet 归一化),输入分布一变,输出置信度自然崩。
解法是导出前把预处理流程冻结,训练和推理完全共用一套预处理代码。这也是为什么我在上面给的推理脚本里明确写了 /255.0 和 transpose 顺序——这些细节只要训练和部署不一致,就是玄学 bug 的温床。
6.4 摄像头画面抖动导致的误检
有次做监控场景,模型偶尔在光线变化时闪出误检框,静态图片测试完全正常。原因在于摄像头自动白平衡和自动曝光导致输入帧的亮度/色温波动剧烈,模型对亮度极端值泛化不够。
解法有两条路:摄像头端关闭自动曝光、固定亮度参数(很多 IP 摄像头的 web 后台可以设);算法端给训练集做光照扰动增强(亮度 ±50%、色温偏移),让模型对这些变化不敏感。第二条路效果更通用,我后面所有视觉项目都会在增强管线里强制加入光照扰动。
7. 写在最后:YOLO 项目能做好的核心认知
这几年接触下来,我对 YOLO 类项目的最大体会是:模型本身不是瓶颈,数据质量、训练节奏、部署链路才是大部分团队真正花时间的地方。
模型选错了可以换,数据如果脏了,换什么模型都白搭。所以我格外强调数据校验和标注规范,这部分"笨功夫"是收益最高的投入。
如果你正准备开始第一个 YOLO 项目,我的建议是先做一个 20 张图的小 Demo 跑通全流程——标注、训练、导出、推理——再回头做完整数据集。小 Demo 会暴露环境问题、流程问题、代码问题,但消耗的时间是分钟级。等全链路顺畅了,再投入资源做大训练,效率完全不一样。
最后分享一个个人习惯:每次训练我都会同时保存 last.pt 和 best.pt,但评估只认 best.pt;每次导出 ONNX 前都会跑一次 PyTorch 和 ONNX 的结果对比;每跑完一个项目都会把"这次踩了什么坑、下一次哪里能省时间"记下来。这些习惯不花多少时间,但在长期项目积累里,能让你在一次又一次交付中越来越稳。