1. 项目概述与整体思路
做目标检测项目,最忌讳的就是“跑通代码就以为完事了”。这个基于 YOLOv8 的人体与行人检测实战项目,表面上是给你一套能用的源码,但真正吃透它,你会接触到从数据清洗、模型训练、超参调整到推理部署的完整链路。我接手这个模拟项目X的时候,目标很简单:识别画面里的人,并且尽量不误检、不漏检,最终交付的是一套可以在低配GPU甚至CPU上稳定运行的行人检测系统。
这类需求在现实里太常见了——智慧安防需要统计人流密度,辅助驾驶要识别道路上的行人,商场做客流分析也要圈出人体区域。但你真去跑一遍公开的检测模型就会发现,通用模型在“能检测”和“检测得准”之间差距很大。YOLOv8 作为目前最成熟的实时检测框架之一,在精度和速度的平衡上做得相当好,尤其是针对行人和人体这类目标,只要处理好数据集和超参数,效果提升非常明显。
这篇文章我会把整个实战过程拆开来讲,包括我踩过的坑、调参的底层逻辑、以及对源码里每个关键模块的理解。适合人群有一定的Python基础,刚接触YOLO系列、想系统上手一个检测项目,或者你已经跑通过YOLOv5但想看看v8的改进和训练细节。环境适合Linux下命令行操作,Windows上也能跑,只是个别路径问题需要注意。
整个项目最终交付的东西包括:处理好的数据集结构、一份可复用的训练配置、一个封装好的推理脚本、以及一个可以直接被其他程序调用的检测接口。下面我从环境这块开始,一步一步讲。
2. 环境准备与官方源码的正确打开方式
2.1 基于Conda搭建隔离环境
YOLOv8的依赖不算很多,但版本兼容问题在实机上踩一次就够难受的了。我强烈建议不要直接往系统Python里装,用Conda单独建一个虚拟环境,这样以后不管做部署还是跑其他实验,都不会互相污染依赖。
创建环境时我固定了Python版本,目前YOLOv8对Python 3.9到3.11支持都很稳定,我用的是3.10,搭配PyTorch 2.x版本。安装PyTorch的时候记得去官网选对应CUDA版本的命令,不要用默认的pip源装CPU版。虽然CPU也能训练小数据集,但时间成本会让你直接想放弃这个项目。
conda create -n yolo python=3.10 conda activate yolo下一步安装PyTorch。以CUDA 11.8为例,命令是:
pip install torch==2.2.1 torchvision==0.17.1 --index-url https://download.pytorch.org/whl/cu118这里有个细节:你的显卡驱动支持的CUDA版本可能会比这个高,这没关系,PyTorch是运行时依赖CUDA runtime的,只要显驱版本不低于runtime要求即可。装完以后用python -c "import torch;print(torch.cuda.is_available())"验证一下,输出True就说明环境没问题。
然后安装ultralytics库和Jupyter Lab等辅助工具,这个库已经把训练、验证、导出这些常用入口封装得很干净了:
pip install ultralytics jupyterlab最后把官方仓库克隆下来。之所以要克隆而不是直接用pip包里的代码,是因为后面改配置文件、替换数据集结构都会在仓库目录里操作,源码放在眼前查阅修改会方便很多:
git clone https://github.com/ultralytics/ultralytics cd ultralytics2.2 为什么选择YOLOv8做行人检测而不是其他版本
这个问题的答案写出来可能会有人觉得我在“厚此薄彼”,但用下来的感受很直接:YOLOv8在检测头、锚框策略和数据增强上都做了比较大的改动,尤其是针对小目标密集场景的适应能力比v5强一档。
传统YOLO用anchor box来预定义候选框,YOLOv8改成anchor-free方式,直接用中心点加尺寸回归目标框。这个改动对行人检测非常友好,因为遮挡很密集时,同一位置的多个行人会有大量重叠框,锚框预测的方式容易出现“一个目标被多个框抢”的混乱。anchor-free天然会减少这类冲突。
另一个让我很看重的改进是C2f模块。你可以把它理解为一种更高效的特征提取结构,它会把中间层的特征图重新组合再输入后续网络,用很小的计算代价换来了特征表达能力提升。实际训练下来的mAP分数提升不一定夸张,但误检率明显降低,这对行人检测来说比什么都重要。
骨干网络加检测头的组合还有一个好处:官方预训练权重是在COCO上训练的。COCO里包含person类别,这相当于给了我们一个已经“见过”大量人体样本的初始化状态,在这个基础上用行人数据做微调,收敛速度极快。第一个epoch结束Loss就已经掉到能用水平,几个小时后就能看到接近可用的效果。
3. 数据集构建:行人检测项目的隐形胜负手
3.1 公开数据集的筛选策略
做行人检测的数据,公开的选择很多,但直接拿来用的效果一般。原因在于:有的数据集是车载视角,人物偏小且密集;有的是监控视角,但遮挡严重;有的虽然分辨率高,但只有几千张图,训练完很容易过拟合。我的建议是不要只用一份数据,按场景混合,但这个“混合”要讲究比例。
以我这次的模拟项目X为例,主体用了约8000张行人数据,其中包含白天和夜间的场景。这部分数据用来教模型“什么是行人”。然后我额外混入2000张不含行人的负样本图片,比如街道、树林、建筑外墙之类。负样本的价值非常大,直接决定着模型会不会乱报警。
数据格式统一转成YOLO格式:每张图片对应一个同名txt文件,每一行是一个目标的类别、中心点x、中心点y、目标框宽、目标框高。其中坐标值都归一化到0~1之间,这样可以适配不同分辨率的图片。我写了一个小脚本,把XML或者JSON格式的标注统一转成这种格式,转换时有个重点:边界框坐标如果超出图片范围,要裁剪到0~1区间,否则训练时会出现NaN损失。
3.2 数据分布的检视与修复
数据集的质量比数量重要,这句是反复强调都不为过的。我建完数据集后会做一个分布统计,主要看每张图的目标数量分布、目标框尺寸分布、以及类别是否均衡。
有一个非常有用的经验:画目标框的宽高比分布图。行人目标通常是长条形的,宽高比集中在0.2到0.5之间。如果你的数据集里出现了大量接近正方形的框,多半是标注框选得不准,把上半身、推车等周边内容也包进去了。这种脏数据会让模型学到的特征变形,训练出来“半个人”也能触发报警。
另外看一下目标框尺寸的分布情况。YOLO图片默认缩放到640x640,如果标注框绝大部分小于20x40个像素,说明小目标占比极高,训练时要考虑加大输入分辨率,或者调整数据增强的裁剪策略。我用的是在推理时保持原图比例缩放而不是直接拉伸,这一点在部署时会讲到。
数据分布检视完,再做一次训练集、验证集、测试集的划分。行人数据集常有一个毛病:同一场景的连续帧图像非常相似,如果随机划分,验证集和训练集会出现大量相似图片,导致验证指标虚高。我按图片所属的场景文件夹来划分,确保同一场景的图片不会同时出现在训练集和验证集里。这个细节很多人忽略,但对于评估真实泛化能力非常关键。
清理完数据,项目里的数据集目录结构大致是这样的:
datasets/ pedestrians/ images/ train/ val/ labels/ train/ val/在Ultralytics框架里,训练用到的数据集配置是个YAML文件,需要把路径、类别数和类别名写清楚:
path: /path/to/datasets/pedestrians train: images/train val: images/val names: 0: person这里类别ID和标注文件里的第一个数字是对应关系,如果后续要用到多个类别,记得保持数字一一对应,否则训练报错你排查半天都找不到原因。
4. 训练流程与关键参数逐项解读
4.1 构建并理解训练配置参数
准备好数据集之后,不用急着敲训练命令,先打开模型配置文件把训练参数完整过一遍。YOLOv8的模型配置文件写的是网络结构的缩放系数,比如yolov8n.pt代表nano版本、yolov8s.pt代表small版本,不同的工程规模对应不同的深度和宽度倍数。
然后看一下训练入口的默认参数。这里挑几个重点说明,这些参数真的会被反复调整:
- model:加载预训练权重的路径。用n、s、m、l或x对应不同规模的YOLOv8模型,n最轻量,x最重但精度最高。
- data:数据集配置文件的路径,就是上文说的YAML文件。
- epochs:训练轮数,初始可以设100轮,然后根据验证集表现决定何时停止。
- imgsz:输入图片大小,默认640。如果显存够用,行人检测建议设到960或1280,对远处的行人效果提升明显。
- batch:批大小,一次喂给模型的图片数,根据显存调整。
- workers:数据加载的进程数,设成8基本够用。
- device:用哪张卡训练,单机多卡可以写0,1。
4.2 启动训练并看懂输出
启动训练的命令很简单:
yolo detect train model=yolov8s.pt data=pedestrians.yaml epochs=100 imgsz=640 batch=16 device=0跑起来以后,终端会实时显示每个epoch的loss、验证集mAP50、mAP50-95等指标。这里有几个坑值得提前说。
Loss曲线的下降趋势正常情况下应该是平滑的,第一个epoch会从一个比较大的值开始往下掉。如果你看到Loss在某个epoch突然飙高,不要马上终止训练,先确认是不是当前batch里出现了标注错误的数据,或者学习率震荡。经常出现的情况是后期Loss小幅震荡上升,然后回稳,只要趋势没坏,不用急着干预。
mAP50指的是IoU阈值取0.5时的平均精度,通俗点说是“框大概框对了就算对”的指标。mAP50-95则是在0.5到0.95区间取多个IoU阈值分别计算再平均,它对定位精度的要求更严格。行人检测场景下,我更看重mAP50这个值,因为实际部署时只要框能包住人,边界稍微松一点不会影响功能。但如果你做的是人流密度统计,mAP50-95也很重要,因为框不准会直接影响计数准确性。
训练过程中还有一个细节点:Precision和Recall曲线会告诉你模型在“误报”和“漏报”之间如何取舍。如果Precision很高但Recall偏低,说明模型很谨慎,但漏掉不少行人;反之则是把很多非行人物体也框了进来。实际场景中,我一般会偏向Recall高一些,因为漏报比误报更危险——监控里漏掉一个行人,比多报几个警报严重得多。
4.3 训练超参的微调方向
训练跑完一轮之后,如果验证集结果不错,可以开始做针对性微调。第一个要调的参数是学习率。默认的YOLOv8学习率是0.01,如果你用迁移学习的方式在COCO预训练权重上微调,一般不需要大动;但如果你从头训练或者数据量特别少,学习率建议调到0.005甚至更低,否则容易发散。
第二个要调的是输入尺寸。把imgsz从640提到960,相当于模型能看到更小、更远的行人目标。显存占用会增长,但回报通常很可观,尤其是对监控类场景。前提是你训练集里的目标框尺寸确实有很多小目标,否则这个调整只会拖慢训练速度。
第三个方向是数据增强的开关。YOLOv8内置了Mosaic增强,它会把四张图拼成一张图训练,增强模型的上下文感知能力。但这个增强在行人检测中偶尔会导致小目标被切割得过于碎片化。如果在验证集上小目标检测效果不理想,可以考虑调低mosaic参数,或者把flipud(上下翻转)关掉——行人并不会上下颠倒出现在画面里,这种增强反而会带偏模型。
我把最终常用的训练配置整理如下,这是一个平衡了精度与训练效率的组合:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| model | yolov8x.pt | 追求精度时用最大型号 |
| imgsz | 960 | 监控场景推荐,远处小目标更多 |
| batch | 8 | 具体视显卡而定,显存不够就下调 |
| lr0 | 0.005 | 微调时降低初始学习率 |
| mosaic | 0.5 | 降低Mosaic概率,保护小目标 |
| close_mosaic | 10 | 最后10轮关闭Mosaic,让模型适应真实构图 |
| fliplr | 0.5 | 水平翻转保持0.5,行人左右翻转是合理的 |
| flipud | 0 | 关闭上下翻转 |
这些参数的调整方向,本质上是在“让模型看到更多样的场景”和“不要让场景失真到学出错误特征”之间找平衡。
5. 推理与部署:从模型到可复用的检测模块
5.1 加载本地模型进行预测
检测模型训练完成后,得到的是best.pt和last.pt两个文件。best是验证集表现最好的权重,last是最后一个epoch的权重。实际使用基本选best,然后先做一次可视化预测,看看效果再封装。
YOLOv8做推理的方式非常简单。我可以写一个Python脚本,从摄像头、图片文件夹或者视频流中读取数据,然后逐帧送入模型。核心逻辑大概是这样:
from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("test.jpg", imgsz=640, conf=0.25, device=0)这里有两个概念需要特别解释。
conf是置信度阈值,只有模型认为“这个框是人”的概率超过0.25才会被输出。这个值调得越低,输出越多,漏检率低但误检率高;调得越高,输出越少,误检率低但漏检率高。日常运营场景里,我一般用0.3到0.5。如果场景光照变化大,建议做一个小批量实景测试,调出一个最适合当前场景的值。
iou是NMS的IoU阈值,用来抑制同一个目标被多个框重复检出的情况。行人密集情况下,两个距离很近的行人很容易被输出成两个极度重叠的框,这时候如果iou阈值设得太低,会错误地把其中一个框删除。我的建议是行人场景保持默认0.7,不要为了“去重”而调高。
5.2 视频流的实时检测实现
对于实际项目来说,只支持图片是不够的,能接入实时视频才是常态。改动并不大,只需要把source参数改成视频文件路径或摄像头设备号。
model.predict(source="busy_street.mp4", save=True, imgsz=640, conf=0.35, device=0)如果是摄像头输入,例如USB摄像头,设备的设备号通常是0。如果要做更复杂的帧处理,比如显示人数、追踪轨迹,那么把预测结果拿回去逐个解析会更灵活。YOLOv8的predict结果是一个列表,每个元素里包含了检测框坐标、置信度和类别ID,可以方便的取出来画画。
有个经常被问到的点:怎样提高视频推理速度?如果显卡算力不够,输入尺寸降回640是最直接的方式。另外确保推理时用的是GPU而不是CPU,默认情况下框架会自动选择设备,但如果你手动指定了device参数,要确认没写错。我用一块中端显卡跑640分辨率的视频流,大约可以到实时40帧以上,完全够用。
5.3 导出为ONNX并部署到实际业务环境
训练出来的PyTorch模型如果只放在自己的机器上玩,那是玩具。真实项目里,业务系统通常是部署到服务器上,由Java或者Go服务调用Python推理接口,这时候把模型导出为ONNX格式几乎是最稳妥的方案。
导出命令一行就够了:
yolo export model=best.pt format=onnx imgsz=640导出后我们会拿到一个best.onnx文件,它已经是与训练框架无关的通用模型格式,可以用ONNXRuntime做推理。这样做的好处是:第一,部署到生产环境时不需要安装PyTorch,只依赖ONNXRuntime,体积小、依赖少;第二,ONNX模型结构稳定,方便后续用TensorRT做推理加速。
在ONNXRuntime里跑模型的关键点在于输入输出的name和shape要搞清楚,可以用下面的方式快速查看:
import onnxruntime as ort sess = ort.InferenceSession("best.onnx") for i in sess.get_inputs(): print(i.name, i.shape)读取出来大概是images这个输入,形状是[1,3,640,640],输出则是个大数组,前几个位置是检测框信息。处理时需要做一次后处理,从输出里恢复出坐标并进行缩放,这部分的逻辑在不同部署环境略有差异,核心是不要搞错坐标系的换算。我见过不少朋友在部署阶段出现“检测框位置偏了”的情况,十有八九就是忘了把归一化坐标乘回原图尺寸。
如果觉得自己写后处理太麻烦,直接用ultralytics自带的推理入口跑ONNX模型也一样,但既然走到部署这步,我建议还是自己把后处理代码写一遍,这样以后换任何推理框架都不会慌。
6. 常见坑点与调试实况记录
6.1 数据集标注冲突导致的训练指标异常
先说一个我在模拟项目X期间踩过的经典坑:训练过程一切正常,指标也在逐步上涨,但验证集的mAP50一直徘徊在0.5上下,上不去了。调了半天参数没效果,最后排查到是数据集里有一个分类标签写反了,把“行人”标注成了“非行人”,负样本里又有一批人没标注出来,模型被反复教错,自然怎么调都上不去。
这个问题其实用脚本就可以查出来:把所有验证集标注和预测结果里的类别做一个交叉比对,看看哪些图片的错误率高得离谱。最笨但有效的办法是可视化标注文件,把标注画到图片上,人工扫一遍关键图片。数据质量检查这一步做得越充分,后面训练调参就越省心。
6.2 内存不足与训练中断的处理
训练大型模型时,最常见的就是CUDA out of memory。如果你显存只有8G,还非要用yolov8x加batch16加imgsz960跑,那大概率中途就崩了。解决方案按优先级排:先降batch到4或2,再降imgsz到640,最后再考虑换更小的模型版本。我通常在开训前用nvidia-smi看一眼显存可用量,再决定用哪个模型规模,尽量避免跑一半崩掉。
另外一点,训练中断了不要直接从头再来。Ultralytics会自动保存断点权重,在训练命令里加上resume=True就可以从最近的权重继续训练。这个功能看似不起眼,但在训练到几十个epoch后意外中断时能省下大量时间。
6.3 部署环境不支持GPU的运行优化
最终交付时,客户服务器没有独立显卡,只有一颗入门级CPU,这是很现实的问题。ONNXRuntime默认使用CPU推理,如果模型比较大,速度会比较慢。这时候有两个优化策略:
其一,在导出ONNX时直接指定opset=12并且simplify=True,让计算图更精简。其二,使用INT8量化,精度会下降一些,但推理速度提升会很可观。我测试过,行人检测这种对mAP要求不算苛刻的场景,INT8量化后mAP50下降约2-3个百分点,速度却可以提升到CPU都能跑实时帧率。量化前要先准备一小批校准图片,这是直接影响量化效果的关键步骤。
还有个不太被注意的地方:推理线程数。ONNXRuntime会在某些环境下开启太多线程反而导致上下文切换开销过大。设置一下sess_options.intra_op_num_threads,一般设成物理核心数而不是逻辑线程数,实测性能还会再涨一点。
7. 易用性优化:让检测能力可以被二次调用
完整源码交付的时候,我通常会让检测能力独立成服务接口,而不是散落在脚本里。最简单的方式是封装一个类,输入一张图片或者视频帧的numpy数组,返回检测框列表。这个列表每一项包含类别、置信度、以及x1、y1、x2、y2坐标。
class PedestrianDetector: def __init__(self, weights="best.pt", conf=0.35, device="cuda:0"): self.model = YOLO(weights) self.conf = conf self.device = device def detect(self, frame): results = self.model.predict(frame, imgsz=640, conf=self.conf, device=self.device) boxes = [] for result in results: for box in result.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() score = float(box.conf[0]) boxes.append({ "bbox": [x1, y1, x2, y2], "score": score, "class": int(box.cls[0]) }) return boxes这个接口封装好之后,再配合FastAPI或者Flask暴露成HTTP接口,就能被任意业务系统调用。比如后端收到一张实时帧,调用这个接口拿到行人框,再做计数或者轨迹分析,整个流程就闭合了。
接口层面还有几个细节要写清楚:输入图片形状如果是高分辨率大图,建议先做一次长边限制,比如最长边不超过1920,否则在线推理延迟会偏高。另外,接口不应该在每次请求时重新加载模型,应该在服务启动时加载一次,放在内存里复用。我见过不少人把模型加载写进请求函数里,结果服务压测时直接卡死,这是非常典型的低级问题。
8. 实战经验总结与个人心得收尾
项目本身跑到这里,从一个空文件夹变成了可直接运行的检测工具,再从训练代码延伸出可部署的导出产物和HTTP接口,整套链路算是完整闭环了。我个人做这类项目最大的体会是:目标检测任务真正费时间的不是训练本身,而是数据清洗、参数权衡和场景适配这三个环节。训练一个epoch可能只需要几分钟,但为了找合适的数据集、看分布、修标注,用掉一整天都是常事。
还有一点想特别对新手说:不要只盯着mAP这个数字。mAP更像是一个理论参考值,真正决定项目成败的是在你的业务场景里,模型对特定光线、特定视角、特定行为形态的行人是否稳定。很多项目交付后效果不佳,主因不是模型差,而是训练数据和真实场景的分布差距太大。所以有条件的话,尽量在真实场景里多录几个小时的视频,补充进数据集中。
最后再分享一个调参的小技巧:如果你发现模型在某个固定位置反复误检,比如反光地面、监控杆立柱,先别急着改模型参数。把误检的图片单独拎出来看看,如果它们长得确实像人,那就只能靠调高置信度阈值解决;如果它们的特征和行人差异其实很大,那就是训练数据里这类负样本太少。补一批负样本重新训一遍,通常比调任何超参数都管用。
这个项目后续还能扩展的方向不少,比如把检测结果接入多目标跟踪做客流轨迹,或者用人体关键点检测来判断跌倒、打架等异常行为。底层的行人检测算法做好之后,上层能叠加的东西非常多,这也是我为什么一直强调要把基础检测模块做得干净、稳定、易调用。