
简介面向夜间低光环境下的行人检测需求这份资源整合了5000张真实场景图片的完整数据集覆盖夜间街景、道路、遮挡及严重遮挡行人等多样情况既能支撑公共场所监控场景的夜间行人检测项目也可作为通用行人检测模型的夜间补充数据。资源采用labelimg工具标注提供VOC(xml)、COCO(json)、YOLO(txt)三种标准格式标签检测框与类别信息一应俱全省去格式转换成本可直接接入YOLO等检测框架训练同时附赠YOLO11一键训练脚本支持GPU(GPUs)、CPU、Mac(M芯片)三平台运行并附带博主训练结果日志供效果参考便于对照调参。下载包共1个文件为PDF说明文档大小6.07MB内含数据集情况介绍、缩略图、标注截图及百度网盘获取方式适合先了解数据规模与场景样例再完整下载。目前已有611人学习下载适合从事目标检测、安防监控或低光视觉研究的开发者快速上手并复用。1. 夜间行人检测为什么难这套数据集和脚本到底给了你什么夜间行人目标检测是目标检测里最容易被低估的场景。白天跑得好好的模型一放到路灯昏暗、行人只有几十像素的监控画面上漏检率直接飙升。你要解决的不是“检测”本身而是低照度下的可见性、小目标分辨率、以及标签格式和训练环境带来的附加成本。这套东西的构成很直接5000张夜间行人图每张都带VOC、COCO、YOLO三种格式标签另外配一个能在GPU(GPUs)、CPU、Mac三平台跑的YOLO11一键训练脚本。它适合安防监控、辅助驾驶和智慧交通方向的工程师也适合刚入门目标检测但不想把时间耗在标注和格式转换上的人。下面我按“数据怎么组织、脚本怎么跑、参数怎么调、坑在哪”这个顺序把这套方案拆开讲。2. 5000张夜间行人图数据从哪来、怎么组织才不会训练翻车2.1 数据集目录结构与训练前的检查清单拿到数据集先别急着训练先看目录。常见做法是images和annotations分开训练集、验证集、测试集按比例切好。例如night_pedestrian/ ├── images/ │ ├── train/ # 约4000张 │ ├── val/ # 约700张 │ └── test/ # 约300张 ├── annotations/ │ ├── voc/ # 每个图像同名.xml │ ├── coco/ # coco.json │ └── yolo/ # 每个图像同名.txt └── data.yaml # YOLO训练入口配置这里为什么把annotations分成三种格式因为YOLO11原生吃YOLO txt但很多公开标签是VOC XML或COCO JSON。你如果只做YOLO训练可以只用yolo目录但如果要跑Faster R-CNN、Mask R-CNN或做对比实验就要用到VOC和COCO。这三种格式并不是等价货坐标体系、类别定义、难点都不一样。训练前我一般会跑一个快速检查脚本统计每张图的标签数量、标注框面积、是否有空标注文件。这个脚本比训练本身更重要因为很多翻车都是从这里开始的。比如一个常见问题是训练集里混入了几张没有任何标注的负样本YOLO的loss会变奇怪另一个问题是标注框坐标出现负数或超出图像边界训练时数据增强会崩。我这里给一个简单的检查脚本框架import os import glob img_dir images/train yolo_dir annotations/yolo empty_txt [] for img_path in glob.glob(os.path.join(img_dir, *.jpg)): stem os.path.splitext(os.path.basename(img_path))[0] txt_path os.path.join(yolo_dir, stem .txt) if not os.path.exists(txt_path) or os.path.getsize(txt_path) 0: empty_txt.append(img_path) continue with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: empty_txt.append(img_path) break _, cx, cy, w, h parts if float(cx) 0 or float(cx) 1 or float(w) 0: empty_txt.append(img_path) break print(异常文件数:, len(empty_txt)) for p in empty_txt[:10]: print(p)这段脚本做的事情是按图像文件名找同名txt读每一行的5个值判断归一化坐标是否在0到1之间、宽高是否大于0。注意YOLO格式是class_id cx cy w hcx、cy是中心点归一化坐标不是左上角坐标。如果你发现很多异常文件大概率是格式转换时把坐标算错了而不是数据本身坏了。说到这里就要提一个关键点5000张图说多不多说少不少。夜间行人目标往往很小标注框可能只有10×20像素。如果imgsz默认640小目标会被压缩得更小所以后面我们会主张把imgsz调到960甚至1280。但调高imgsz要看显卡显存训练时间也会变长这个在第4章参数表里会再讲。2.2 VOC/COCO/YOLO三种格式的差异与为什么都要给VOC格式是每个图像一个XML文件标签结构大致是object name, bndbox xmin, ymin, xmax, ymax坐标是像素绝对值。COCO格式是所有标注集中在一个JSON文件里用images、annotations、categories三个数组组织标注框是[x, y, width, height]左上角加宽高。YOLO格式是每个图像一个txt文件每行class_id cx cy w hcx和cy是相对图像宽高的比例w和h也是相对宽高的比例。三种格式的转换是最容易埋雷的地方。网上流传的转换脚本很多但经常忽略“图像实际宽高”和“XML里记录的宽高”不一致的边界情况。比如有些标注工具在XML里写width1920但图像实际尺寸是1280×960转换脚本如果直接读XML里的width坐标会整体偏移或缩放错误。正确做法是用PIL或cv2重新读取图像尺寸再拿这个尺寸做归一化。下面这段是VOC XML转YOLO txt的参考实现import os import glob import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_path, class_names, out_txt): with Image.open(img_path) as img: img_w, img_h img.size tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 像素坐标转归一化中心坐标 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 裁剪到[0,1]防止越界 cx max(0, min(1, cx)); cy max(0, min(1, cy)) w max(0, min(1, w)); h max(0, min(1, h)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines))逻辑说明先读图像真实宽高img_w, img_h再解析XML的四个角坐标用(xminxmax)/2和(yminymax)/2算中心点再用宽高归一化。最后做一次裁剪防止标注越界导致后续训练数据增强报错。参数说明class_names是类别列表要按你的标签名字排序比如class_names [pedestrian]这个顺序必须和data.yaml里的names一致out_txt是输出路径。如果你只有单类上面逻辑没问题如果有多类注意类别ID从0开始不是从1开始。2.3 一套数据跑三种格式用COCO JSON统一管理标签的转换思路上面讲了VOC转YOLO。如果手头是COCO JSON我一般会先转成COCO的中间结构再生成YOLO。原因很简单COCO JSON里包含images的file_name、width、height还有annotations里的bbox和category_id信息最全不容易丢。转换时只要遍历annotation把[x, y, w, h]改成归一化中心点即可import json import os def coco_to_yolo(coco_json, out_dir): with open(coco_json) as f: data json.load(f) id_to_name {cat[id]: cat[name] for cat in data[categories]} for img in data[images]: img_id img[id] img_w img[width] img_h img[height] lines [] for ann in data[annotations]: if ann[image_id] ! img_id: continue x, y, w, h ann[bbox] cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h cat_id ann[category_id] lines.append(f{cat_id - 1} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(out_dir, img[file_name] .txt), w) as f: f.write(\n.join(lines))逻辑说明这段代码的关键是cat_id - 1因为COCO的类别ID一般从1开始比如人1而YOLO要求从0开始。如果你的COCO JSON里类别ID已经是从0开始就别减。参数说明coco_json是输入文件out_dir是输出目录输出文件名用图像的file_name茎干名。这个脚本只做了单向转换实际项目中我建议保留COCO JSON作为主标注VOC和YOLO都从它生成这样改标签时不用同步改三份。还有一个容易被忽略的是数据划分。很多数据集给的是train/val/test已经分好的目录但如果标签格式是你自己转换的你重新划分数据时一定要同步移动同名文件不能只移动图片不移动标签。我见过有人用脚本随机抽20%图片做验证集结果没把对应txt移过去训练时YOLO自动忽略没有标签的图片导致训练集变小、验证集没有标签可评估。这种翻车非常隐蔽因为YOLO训练日志里不会主动报错只会打印“Scanning labels”时跳过。如果你要自己划分最简单的做法是生成一个data.yaml指向train、val、test的图片目录YOLO会自动去同名目录下找标签。Ultralytics的约定是图片在images/train/xxx.jpg标签就在labels/train/xxx.txt。如果Annotations和Images分开放你需要在训练脚本里把标签路径也映射好或者干脆把标签文件复制到labels目录。我们这套数据集里面已经按YOLO习惯放好了data.yaml里只需要写path: /path/to/night_pedestrian train: images/train val: images/val test: images/test names: 0: pedestrian注意path最好写绝对路径因为一键脚本在Mac和服务器之间切换时相对路径容易因为工作目录不同而找不到数据。这一点在跨平台训练时特别重要Mac上路径是/Users/xxx/...服务器上是/home/ubuntu/...写死在data.yaml里会好排查。以上我们把这套数据的目录结构、三种格式差异和转换脚本都讲了。下一章进入YOLO11训练脚本本身。3. 用YOLO11训练夜间行人模型一键脚本背后的参数与硬件调度3.1 YOLO11模型选型与网络结构里的关键点YOLO11是Ultralytics系列的目标检测模型延续了C3K2、SPPF、C2PSA这类结构适合在中小数据集上快速出效果。对于夜间行人这种任务我一般不会上来就选最大的YOLO11x而是先在YOLO11n和YOLO11s之间试一轮。原因很简单夜间行人目标小模型容量大不一定能提升小目标召回反而更容易过拟合到噪声背景。YOLO11n只有几百万参数跑一轮很快能摸清数据baselineYOLO11s在精度上有小幅提升但训练时间会变成两倍左右。如果你想把小目标检测做得更细可以关注YOLO11的检测头输出层数和特征融合。默认YOLO11从8倍、16倍、32倍下采样特征图输出三个尺度。夜间行人目标往往在8倍特征图上才能保留足够信息如果目标太小三个尺度都不够用就需要加一个4倍下采样的P2检测头或者用一些基于YOLO11的小目标增强模块。这属于模型改进不是必选但对做比赛或论文复现的读者可能是刚需。训练脚本里我们先用官方结构跑通你想要加P2头改动点在模型配置文件的head部分后面可以单独写一篇。3.2 一键训练脚本自动识别平台并调用YOLO11标题里说支持GPU(GPUs)/CPU/Mac三平台真正实现起来不是单靠Ultralytics就能自动选设备的。Ultralytics默认device0表示用第一张CUDA显卡devicecpu用CPUdevicemps用Apple Silicon的Metal加速。我们要做的训练脚本核心是让用户不需要关心该填哪个值脚本自己判断。这里给一份我常用的训练启动脚本基于Python编写既能在Windows直接在命令行运行也能在Linux服务器和macOS上跑import platform import torch from ultralytics import YOLO def detect_device(): if torch.cuda.is_available(): return 0 # 使用第一张/主GPU elif platform.system() Darwin and torch.backends.mps.is_available(): return mps else: return cpu if __name__ __main__: device detect_device() print(f[NightPedestrian] 当前训练设备: {device}) model YOLO(yolo11n.pt) # 可以换成 yolo11s.pt model.train( datadata.yaml, epochs100, imgsz960, batch16, devicedevice, projectruns/night_pedestrian, nameyolo11n_960, ampTrue, patience20, save_period10, )逻辑说明detect_device里第一优先级是CUDA第二是苹果的MPS第三是CPU。torch.backends.mps.is_available()只会返回True前提是你在Mac上装了支持MPS的PyTorch。训练参数里datadata.yaml指向上一章的数据配置文件imgsz960是针对小目标调的输入尺寸patience20是早停轮数模型连续20轮没提升就停防止把时间耗在过拟合阶段save_period10表示每10轮存一次中间权重虽然占磁盘但对夜间场景的调参很有用可以回看哪一轮的置信度更稳定。这里要解释为什么用yolo11n.pt而不是直接用模型配置文件。用预训练权重做初始模型可以继承COCO上学习到的通用特征对夜间行人这种下游任务来说收敛更快。如果你的GPU显存够大想冲更高精度把yolo11n.pt换成yolo11s.pt或yolo11m.pt即可。注意第一次运行会从Ultralytics拉取权重如果你在离线环境需要提前手动下载权重文件放到项目目录脚本里改成YOLO(weights/yolo11n.pt)。3.3 GPU/CPU/Mac三平台的依赖安装与运行条件三平台训练最麻烦的不是训练本身而是环境配置。GPU(GPUs)平台要求NVIDIA显卡、CUDA驱动、PyTorch的CUDA版本匹配CPU和Mac平台主要是慢但能跑YOLO11的依赖也一样。常见做法是用pip install ultralytics它会自动安装一个CPU版或当前环境的PyTorch。注意Mac上如果直接pip安装默认可能是CPU版PyTorchMPS后端不生效训练起来比CPU快不了多少。你需要在安装PyTorch时选择MPS支持的版本。我给一个最简单的环境确认命令python -c import torch; print(torch.__version__); print(torch.backends.mps.is_available())在Mac上如果输出True说明MPS可用在Linux上检查CUDA用python -c import torch; print(torch.cuda.is_available())如果这条命令输出False说明PyTorch装成了CPU版需要到官网选对应的CUDA版本重装。这个过程是yolo11环境配置里最常见的坑很多人训练时报错“Torch not compiled with CUDA enabled”然后以为是显卡坏了其实是驱动和torch版本不匹配。CPU平台训练不是不能跑而是要认命。同样5000张图、imgsz960、epochs100在RTX 4090上大约几小时CPU可能跑一天一夜。我的建议是CPU只做调试把epochs设成10imgsz设成640先确认数据管道跑通。如果数据没问题再切到GPU上做正式训练。Mac的MPS速度介于CPU和主流GPU之间小模型完全够用但有些YOLO11运算在MPS后端还没有全支持遇到不支持的算子Ultralytics会报错。我的经验是安装Ultralytics版本越新MPS兼容性越好但训练时最好也用ampFalse因为AMP和MPS混合精度在某些torch版本上会不稳定。在多卡GPU服务器上Ultralytics也支持device0,1但一键脚本默认只写第一张卡。如果你有多卡可以手动改成device0,1。这时batch size要相应放大因为两张卡并行处理。脚本里的batch16是每张卡的batch总数等于16×卡数注意显存是否够用。常见的坑是用户以为batch16是全局batch结果单卡显存不够却不知道把batch调小。我在脚本里特意用batch16而不是更大的值就是为了兼容6GB以上显存的大部分消费卡。如果你的卡只有4GB把batch降到8imgsz降到640或者开启梯度累积。Ultralytics本身的batch参数可以传-1让它自动检测最优batch但对新手我不推荐因为自动检测会占用很多显存做测试有时意外被杀进程。训练完成后权重保存在runs/night_pedestrian/yolo11n_960/weights/best.pt和last.pt。best.pt是按验证集指标挑出来的last.pt是最后一轮的状态。如果你要做严谨实验别直接用last.pt作为最终产物一定用best.pt。后面第6章的推理我会用best.pt。4. 夜间行人训练的参数边界batch、imgsz、epochs与数据增强怎么调4.1 必调参数速查表附夜场景建议夜间行人检测和白天任务最大的区别是输入图像亮度低、目标小、容易漏检。经过前面用ImageNet/COCO预训练模型跑一版baseline之后我一般会按下面这张表去调参参数默认值(官方)夜间行人建议理由imgsz640960 或 1280小目标在640下只有十几个像素检测头难以响应batch168~16 视显存batch太大会显存溢出太小则BN统计不准epochs100100~200夜间数据多样性不高但小目标收敛慢lr00.010.005~0.01预训练模型收敛快学习率过大容易震荡patience2020~50夜间小目标mAP提升是爬坡式的太早停会错过mosaic1.00.5~0.8夜间目标数量少过度mosaic会生成大量无意义组合hsv_v0.40.2~0.3夜间亮度本身低过强亮度扰动会破坏真实分布degrees0.010~20监控视角下行人姿态多变轻微旋转可打数据增强这张表不是拍脑袋。imgsz960意味着输入分辨率提高50%显存占用大约升高到原来的2.25倍所以batch要从16降到8才能不溢出。如果你用GPU(GPUs)训练8G显存可以先试imgsz960, batch8如果显存不够可以把rectTrue打开让YOLO按图像长宽比分批计算输入尺寸而不是全部resize到正方形能省很多显存。这里有三个容易误设的参数说明第一是mosaic。Ultralytics默认前10轮会关闭mosaic因为mosaic生成的拼接图会让模型在早期看到过多“假”目标。夜间数据本身图像整体偏暗mosaic比例调小到0.5可以减少把行人切碎变成噪声的概率。第二是hsv_v。默认的HSV亮度扰动范围是0.4对夜间数据来说太大了一个本来就不亮的目标可能被增强成纯黑或者过曝反而让模型学不到稳定的亮度特征我一般调到0.2。第三是patience。很多新看到loss不再下降就手动停了但夜间小目标的mAP在60轮以后仍然可能缓慢上升因为小目标位置回归的难度大需要更多轮次微调。建议设成30以上让它自己早停。4.2 夜间数据增强策略亮度、噪声、模糊与mixup除了上面几个参数我还建议在训练脚本里显式配置数据增强。Ultralytics的model.train()接受augment相关参数夜间场景我常用的组合是model.train( datadata.yaml, imgsz960, batch8, devicedevice, hsv_h0.015, hsv_s0.5, hsv_v0.25, degrees10, translate0.1, scale0.5, fliplr0.5, mosaic0.6, mixup0.2, copy_paste0.1, erasing0.2, )逻辑说明hsv_h、hsv_s、hsv_v控制HSV空间的色相、饱和度和亮度扰动。夜间图像饱和度本来就低hsv_s我反而调高到0.5让模型不去依赖颜色更多依赖形状和边缘。degrees10允许行人轻微旋转模拟不同监控角度。translate0.1允许目标在图像内平移10%这比翻转更接近夜间行人从画面边缘走入的情形。copy_paste和erasing是Ultralytics较新版本支持的数据增强前者把目标复制粘贴到其他位置后者随机擦除部分区域模拟行人被遮挡的情况。参数说明mixup0.2表示有20%概率将两张训练图混合这对夜间行人样本少的情况有正向帮助。但如果你发现验证集mAP不升反降先把erasing和copy_paste关掉因为它们会改变目标的上下文而夜间目标在上下文极度缺失时很容易被当成背景。数据增强不是越多越好我在夜间场景吃过亏为了提升泛化开满了所有增强结果模型在真实监控视频里把路灯杆识别成人因为它学会了把“亮斑细长形状”当成人增强过度导致目标语义被破坏。如果你用的是YOLO11的训练命令行而非Python API也可以把上面的参数写进一个yaml文件用data参数里附带的augment配置。Python API更容易排查因为报错会直接告诉你哪个参数名不合法。注意不同Ultralytics版本对数据增强参数的默认值和拼写有差异以你安装版本的官方文档为准不要照抄旧博客。4.3 评价指标怎么读mAP0.5与mAP0.5:0.95的取舍训练日志最后会打印一张指标表包括mAP50(B)、mAP50-95(B)等。很多人只看mAP50这在夜间行人任务上是危险的。mAP50只看IoU阈值0.5只要框大概压在行人身上就算对。夜间小目标本身框小IoU稍微偏一点可能就超过0.5所以mAP50会显得很好看但真实部署时框的抖动很大。我会更看重mAP0.5:0.95它把IoU从0.5到0.95每隔0.05算一次平均对框的精度更敏感。夜间行人还有一个指标被忽略小目标AP。Ultralytics的训练日志不会直接给你分尺寸AP但可以额外写一段验证脚本用COCO的评估工具pycocotools计算AP_small。如果AP_small很低但整体mAP还行说明模型漏掉的主要是那些距离远、像素小的行人。这种场景下盲目调参不如提高输入分辨率或加P2检测头。我之前做过一个对比在同样的数据集上把imgsz从640升到1280mAP0.5:0.95提升了接近20个百分点而mAP50只提升了不到5个点。原因是mAP50对框位置不敏感而小目标在1024以上分辨率下才能被正确回归。所以你在评估自己模型有没有进步时别只看一张图表要分开看。关于置信度阈值训练时YOLO默认用conf0.001做mAP计算这是为了画出完整PR曲线。推理时你会自己设conf0.25或更高。调conf会直接改变precision和recall的平衡但要记住mAP是模型能力的体现conf只是你查输出的尺度不能通过调高conf去刷mAP。这是新手常见误区统一在这里说明。5. 避坑清单夜间行人数据集翻车的五个现场与排查方法5.1 现象loss不断下降但mAP一直贴着0.1不动这是夜间数据集训练里最常见的怪事。训练日志里box_loss、cls_loss都在降看起来一切正常但验证集mAP就是上不去。原因有三个层面一是标签格式转换把类别ID写错了所有目标都被当作背景YOLO的cls_loss虽然降了但学不到任何有效位置二是图像和标签整体没对齐YOLO扫描标签时生成了空cache训练时没有真实目标参与三是数据集里存在大量难例比如几十像素的行人模型把这类目标当作背景拟合了。解决方法是先回到第2章的检查脚本随机抽20张训练图把YOLO txt画到图上肉眼看一下框是不是对准了行人。如果框全部偏移检查转换脚本里的坐标归一化是否用了图像真实尺寸而不是XML里的声明尺寸。如果框是对的再检查data.yaml的names列表和txt里的class_id是否一致。很多时候是类别ID从1开始YOLO按0开始导致所有目标被判为“非目标”。这一步排查比调参重要得多夜间行人数据集的标签一旦错位后面所有参数都白调。5.2 现象单卡训练到一半爆显存报CUDA out of memory训练到第30轮突然报RuntimeError: CUDA out of memory看起来莫名其妙。原因多半不是初始显存不够而是训练过程中图像随机resize和mosaic产生了一张超大组合图加上batch中每张图尺寸不一显存峰值瞬间抬高。解决方法是不要只减小batch也要检查cache参数。Ultralytics默认cacheFalse但如果开了cacheTrue会把数据集完整放进RAMRAM不足时也报类似错误。我建议cacheTrue只用于小数据集5000张图在imgsz960时大概要占几十GB内存不是每台机器都吃得消。如果你需要稳定训练把batch8、imgsz960、cacheFalse同时加一行workers4来限制数据加载进程数减少内存突发。还有一种情况是AMP混合精度打开但显卡太老导致某一轮loss变为NaN后续数值溢出占满显存。这种报错前面会跟着loss: nan解决方法是设置ampFalse或者更新显卡驱动。我遇到过同一台服务器上换了新驱动之后原来稳定的训练突然爆显存最后发现是驱动版本和CUDA库不一致导致显存分配异常。5.3 现象Mac上训练时提示MPS后端不可用或者算得非常慢在Mac上跑标题里的一键脚本最常见的报错是torch.backends.mps.is_available()返回 False脚本就乖乖落到CPU然后训练慢到怀疑人生。原因一般是你通过pip安装的torch是CPU版本没有编译MPS支持。解决方法是重装PyTorch选macOS/Apple Silicon对应的安装命令。装好后再确认python -c import torch; print(torch.backends.mps.is_available())输出True。另一个问题是MPS虽然可用但Ultralytics在训练某些模块时会报“operation not implemented for MPS”那是PyTorch还没有覆盖全部算子。我的做法是在训练脚本里加一个异常回退检测到MPS训练失败时自动改回CPU并在日志里提示。这种回退虽然牺牲速度但至少不让训练中断。try: model.train(..., devicemps) except NotImplementedError: print([NightPedestrian] MPS算子不支持自动回退到CPU训练) model.train(..., devicecpu)逻辑说明NotImplementedError是PyTorch在不支持的操作上会抛出的异常。参数说明这个try包住整个train调用但要注意如果训练中断在某个epoch之后模型权重在内存里可能已经更新回退到CPU会从当前状态继续不会完全从头开始。不过Ultralytics内部可能有缓存最保险的是重新加载best.pt再继续。5.4 现象验证时把人影识别成人路灯和树干全是误检这是一个经典的夜间行人误检。训练集里都是有人标注框的图模型学会了“暗背景下一块亮斑”就是人结果部署到真实夜间视频里路灯、树干、汽车后视镜反光全亮了。原因有两点第一是负样本不足。目标检测模型在正样本上过拟合后会把大量背景误检。解决方法是给数据集加一批“没有人”的夜间背景图标注文件为空重新训练。第二是推理置信度阈值太低。训练时mAP按conf0.001计算推理时如果也用默认conf0.25误检会很多。夜间场景我建议推理阈值调到0.4到0.5宁漏勿错。如果你的场景是安防报警漏检和误检的代价不一样阈值要响应用户风险偏好不能一个值走天下。注意空标签文件在YOLO里不能直接丢弃因为负样本帮助模型学习“这里没有目标”。如果训练时用过滤空标签的脚本把空标注图片删了等于放弃了负样本。第2章检查脚本里我把空txt标记为异常但在负样本场景里空txt是合理的要区别对待。另外我还习惯在训练前看一遍验证集里误检最多的图片保存下来拼接成一张大图快速判断是目标太小、太暗还是背景干扰这比只盯mAP数字有效得多。5.5 现象使用不同版本Ultralytics后同样参数训练结果差距很大YOLO11还在快速迭代Ultralytics的几个版本之间默认增强、坐标损失定义都有变化。你在项目里跑通的参数升级到新版本后可能mAP掉点或者直接报参数名错误。原因就是训练配置没有锁定。解决方法是把环境依赖版本固定建议在你的训练环境里用requirements.txt固定ultralytics版本并记录pip安装时的torch版本。这样以后复现实验时不会出现“昨天跑0.2今天只有0.15”的尴尬。如果你在多个服务器上跑同一个项目记得先pip install -r requirements.txt再跑一键脚本不要自信到认为最新版一定更好。新特性比如后续目标检测版本里的结构变化只在新版本支持但稳定性不如老版本做生产环境时不要追新。另外当你把训练日志里的mAP和其他人报告的数字对比时要确认你们用的是同一版本、同一种数据划分。我见过有人把验证集和测试集混着评估报告出来的分数虚高等上了生产才发现泛化不够。所以我在项目里习惯把data.yaml的test目录固定住验证只用val只有全部训练完才用test做一次最终评估。这样能避免数据泄露和“指标好看但部署翻车”的尴尬。6. 模型没白练从测试集评估到视频推理的实际效果6.1 用val模式替代predict做效果评估训练结束看到best.pt之后先别急着跑视频。我建议先用val模式在测试集上做一次定量评估yolo val modelruns/night_pedestrian/yolo11n_960/weights/best.pt \ datadata.yaml splittest imgsz960 device0逻辑说明splittest是Ultralytics新版支持的参数老版本需要自己把数据目录换成test图片目录。device0在Mac上可以改成mps但val过程很快用CPU也没关系。这条命令会输出mAP50、mAP50-95等指标同时保存PR曲线到结果目录。参数说明imgsz960要和训练时一致如果训练用1280验证用960指标会失真因为小目标在不同分辨率下的难度不一样。这一步是模型效果的客观基准后续调推理阈值都在这个基准上做。6.2 夜间视频推理与置信度阈值调试评估通过后我一般直接用best.pt跑一段真实监控视频。如果视频太长先取前500帧做快速验证yolo predict modelruns/night_pedestrian/yolo11n_960/weights/best.pt \ sourcenight_walk.mp4 device0 conf0.4 save_txtTrue这里conf0.4是夜间场景我比较常用的起点。如果误检多就往上调到0.5如果漏检多就往下调到0.3。调的时候不要只看单帧要看连续帧的稳定性。夜间画面噪声大同一个行人在某一帧被检出、下一帧消失通常是置信度刚好卡在阈值边缘可以适当降低conf并配合IoU NMS的调整来提升时序稳定。如果你发现路灯反光被稳定识别成人那多半不是阈值问题而要回到第5章加负样本重新训练。6.3 导出ONNX/TorchScript做跨平台部署训练和验证完成后如果要部署到Jetson、Windows服务端或者云端函数我习惯先导出ONNXfrom ultralytics import YOLO model YOLO(runs/night_pedestrian/yolo11n_960/weights/best.pt) model.export(formatonnx, imgsz960, dynamicTrue)逻辑说明dynamicTrue允许动态输入尺寸部署时灵活性更高但推理速度会比固定尺寸慢一点。如果追求速度可以把dynamicFalse输入尺寸固定为960。参数说明导出后的ONNX文件可以用ONNX Runtime或TensorRT加载Mac上也能用CoreML继续导出。我自己的一个教训是导出ONNX时imgsz必须和训练时一致否则你拿到线上跑出来的效果会和训练时差一大截那时的“玄学掉点”多半不是模型问题而是尺寸没对齐。最后我想说一个习惯夜间行人模型不能只看mAP我会额外拿一段从未出现在数据集里的夜间视频按时间段切成凌晨、黄昏、深夜三段分别统计漏检数。只有这三段的数字都稳定我才敢把模型交出去。希望帮到你。本文还有配套的精品资源点击获取