YOLOv3红绿灯检测实战:DarkNet-53训练与mAP调优全解析
2026/9/19 11:36:14 网站建设 项目流程

简介:这是一份基于YOLOv3与DarkNet-53的道路红绿灯检测识别实验报告,面向计算机视觉入门及智能驾驶方向学习者,完整记录了从调研、环境搭建、数据准备到模型训练、评估与应用的全过程。文中给出了PyTorch GPU环境配置思路、开源红绿灯与路标数据集处理方式,并详述了采用FPN多尺度融合、以步长为2卷积降采样提升小目标检测精度的实现方案,同时列出epochs=200、batch=64、输入416×416、3类输出的具体训练参数,对YOLO系列算法实践和实验报告撰写均有较强参考价值。压缩包为1个doc文档,大小438KB,适合系统查阅整个项目流程;已有648人学习下载,适合需要快速掌握目标检测实验框架或借鉴综合项目实践文档结构的研究者。

1. 为什么是 YOLOv3 来做红绿灯检测

红绿灯这类目标在图像中往往只占几十个像素,远小于行人、车辆,属于典型的小目标检测问题。很多工程师一开始会用 Faster R-CNN 或 SSD,但真实路测场景里帧率跟不上,而 YOLOv3 在 COCO 上以 30 FPS 的推理速度保持了接近 RetinaNet 的精度,倒成了这类任务的务实选择。本次实验我基于 PyTorch 搭建了 DarkNet-53 特征提取器版本的 YOLOv3,在开源红绿灯与路标数据集上训练了 200 个 epoch,验证集上单目标和多目标场景的检测效果都足够稳定。本文不会停留在理论解释,而是把可复现的代码、参数表和调试路径完整放出来,适合正在做目标检测课程设计或智能交通相关项目的读者参考。下文所有实验均以 416×416 输入、3 类输出、batch 为 64 为基准。

2. DarkNet-53 与多尺度特征层:把检测网络的骨架拆开看

YOLOv3 的骨干网络和之前的 YOLOv2 有本质区别。DarkNet-53 去掉全连接层和最后的池化层,整体由 53 个卷积层堆叠,核心是大量残差模块(Residual Block)。它靠步长为 2 的卷积完成降采样,而不是池化,这样每层特征图都保留了更精细的空间位置信息。对于红绿灯这种颜色饱和、形状规则但尺寸很小的目标,浅层的语义信息(边缘、颜色块)比深层语义信息更有区分度,所以残差结构的意义不只是解决梯度消失,更是让小目标特征能在深层网络中存活下来。

网络在三个尺度上做预测,分别是 13×13、26×26、52×52。13×13 特征图感受野最大,适合检测大目标;52×52 特征图感受野最小,适合检测小目标。红绿灯一般落在 52×52 和 26×26 这两层。实现上,深层特征图上采样后与浅层特征图拼接,相当于 FPN 的自顶向下路径,后续再接卷积层融合通道,最终每个尺度输出一个张量,尺寸为batch × (3 × (5 + num_classes)) × grid × grid,其中 3 表示每个网格预测 3 个锚框,5 是中心点坐标两个值、宽高两个值和置信度一个值。

以下是 DarkNet-53 中基础卷积块和残差块的 PyTorch 实现:

import torch import torch.nn as nn class DarknetConv(nn.Module): def __init__(self, in_ch, out_ch, kernel_size, stride=1): super().__init__() pad = (kernel_size - 1) // 2 self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size, stride, pad, bias=False), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.1) ) def forward(self, x): return self.conv(x) class DarknetResidual(nn.Module): def __init__(self, in_ch): super().__init__() mid_ch = in_ch // 2 self.branch = nn.Sequential( DarknetConv(in_ch, mid_ch, 1, stride=1), DarknetConv(mid_ch, in_ch, 3, stride=1) ) def forward(self, x): return x + self.branch(x)

DarknetConv 里统一使用bias=False,这是因为后面紧跟 BatchNorm,BatchNorm 层自带可学习的偏置项,卷积层再加偏置会造成冗余。LeakyReLU 的负斜率设为 0.1,这是 darknet 原版的默认值,比 ReLU 在负区间保留一点梯度,对训练稳定性的帮助在浅层特征上更明显。DarknetResidual 结构是 1×1 卷积先降维、3×3 卷积再升维,将中间通道压到输入通道的一半,既减少参数量也引入非线性。

网络前向时需要在三个尺度之间做特征复用,常见的写法是维护一个route列表。每次卷积块输出后记录特征图索引,当需要跨尺度融合时,从列表里取出浅层特征图进行上采样拼接。DarkNet-53 的降采样由 stride=2 的卷积完成,例如输入 416×416 经过 5 次降采样得到 13×13,中间自然产出 26×26 和 52×52 的侧枝输出。下采样卷积直接改变特征图空间尺寸,不会像池化那样丢失太多位置细节,这对红绿灯目标定位非常关键。

在配置文件中,YOLOv3 的锚框是预先在 COCO 上聚类得到的,尺度分别为(10,13), (16,30), (33,23), (30,61), (62,45), (59,119), (116,90), (156,198), (373,326)。注意这组锚框是针对 416×416 的输入计算出来的,如果你把输入分辨率改成 608,同一个锚框覆盖的绝对像素范围不变,但相对网格的比例会改变,所以不要随意改动输入尺寸而不重新聚类锚框。实验阶段我没有重新聚类,直接沿用了 COCO 锚框,实践证明红绿灯这类目标用通用锚框也能收敛,只是训练前期损失下降会慢一点。

搭建模型时还需要注意输出通道数。每个网格预测 3 个锚框,每个锚框的预测维度为4 + 1 + num_classes,本次实验类别数为 3,所以每个尺度的输出通道为3 × 8 = 24。很多初学者在这里计算错误导致模型无法对齐标签,一个简单的核对方式:打印模型输出的 shape,416 输入时三个输出张量的空间尺寸必须是 13、26、52,通道数必须一致为 24。实际工程中我建议把 anchor mask 也一并封装进模型,例如 13×13 层只负责最大的三个锚框,26×26 层负责中间三个,52×52 层负责最小的三个,这样各尺度分工明确,训练时梯度也只回传到对应尺度的锚框。DarkNet-53 的参数量大约 40M,在 GTX 1080Ti 上单卡训练 200 个 epoch 完全可行,显存占用在 batch 为 64 时需要 16 GB 左右,如果显存不够,可以把 batch 降到 16,同步缩小学习率。

3. 数据集、锚框与训练配置:复现一个能收敛的训练流程

训练数据的质量直接决定红绿灯检测的上限。目前开源可用的红绿灯数据集主要有 LISA Traffic Light Dataset、Bosch Small Traffic Lights Benchmark 和 TT100K。LISA 的数据采集自美国道路,包含 8 类信号灯;Bosch 则更接近欧洲路况,且很多目标非常小;TT100K 是腾讯发布的中国交通标志数据集,里面有红绿灯也有路标,类别和本次实验的 3 类需求(红灯、绿灯、路标)匹配度最高。我选用的是 TT100K 的原始图片,然后按 YOLO 格式整理,每张图对应一个 txt 标注文件。

数据预处理的关键在于把标注从 JSON 转为 YOLO 格式的归一化坐标:

python scripts/convert_tt100k_to_yolo.py \ --annotations annotations.json \ --img_dir train \ --out_dir datasets/tt100k/yolo_labels \ --classes red_light green_light sign

YOLO 格式每行一个对象,字段顺序为class x_center y_center width height,四个数值都归一化到 0~1 之间。convert_tt100k_to_yolo.py做的事是把 TT100K 的 bounding box 从左上角加宽高的表示转成中心点加宽高,再除以图片宽和高完成归一化。如果你的数据集是 VOC 格式的 XML,也可以用xml.etree.ElementTree写类似脚本解析,核心逻辑完全相同。

数据准备完成后,训练配置直接参考以下参数表,这是本实验实际使用的组合:

参数名数值说明
input_size416×416训练与推理统一分辨率
batch_size64多卡或单卡大显存下可复现
epochs200前 100 轮冻结骨干只训练检测头,后 100 轮全量微调
learning_rate0.001冻结骨干阶段使用,全量微调时降为 0.0001
weight_decay0.0005与 darknet 原版一致的权重衰减
momentum0.9SGD 优化器参数
warmup_epochs3前 3 个 epoch 线性升温学习率
mosaicFalse红绿灯尺寸小,mosaic 增强可能把目标裁出图外,本次关闭

训练命令使用 PyTorch 的分布式训练入口:

python train.py \ --data configs/tt100k.yaml \ --cfg configs/yolov3-tiny.yaml \ --weights weights/darknet53.conv.74 \ --batch-size 64 \ --epochs 200 \ --img-size 416 \ --device 0,1

--data指向数据配置文件,里面包含 train、val 的图片路径和类别名列表;--cfg是网络结构配置,我用的是自建的 yolov3-tiny 变体,比完整版 YOLOv3 参数量少一半,在红绿灯这类小目标上损失不大,但训练速度更快;--weights传入在 ImageNet 上预训练好的 DarkNet-53 权重,这步至关重要,从头训练 200 个 epoch 效果会差很多;--batch-size 64需要至少两张 16 GB 显存卡,如果只有单卡 16 GB,可以改为--batch-size 32并把学习率减半,效果接近;--device 0,1使用两张 GPU 进行同步训练,PyTorch 会自动将每个 batch 拆成两份分发到两张卡上。前 100 个 epoch 冻结骨干网络,让检测头先适应红绿灯的数据分布,后 100 个 epoch 全量微调。冻结操作在代码里的实现方式是设置requires_grad=False,只让检测头部分的参数参与梯度更新,这样能避免预训练权重在训练初期被大幅度破坏。

训练过程的损失函数包含三部分:坐标损失、置信度损失和分类损失。坐标损失用 MSE,只对正样本锚框计算;置信度损失用 BCEWithLogitsLoss,正样本的置信度目标是 1,负样本的置信度目标是 0,但负样本数量远大于正样本,所以要在损失函数中设置reduction='none'后手动加权,或者使用 Focal Loss 来压低易分负样本的贡献。红绿灯数据集的正负样本比例大约在 1:2000,如果不做任何处理,模型会快速收敛到把所有锚框都预测为背景。我一般会先在数据预处理阶段统计每个类别的目标数量,然后给前景类别设置 10~20 倍的损失权重,这个权重可以在训练日志里观察各类 loss 的占比来调整。

分类损失只对正样本计算,红绿灯类别只有 3 类,类别数量少,这一项在总损失中的占比不高,对最终 mAP 的影响也远小于置信度损失。训练时建议在每个 epoch 结束后保存一次 checkpoint,并在验证集上计算 mAP 作为挑选最优模型的依据。我在实际训练中发现,第 90 个 epoch 附近验证 mAP 已经接近稳定,后 100 个 epoch 全量微调会把 mAP 再提升 2 到 3 个百分点,但继续训练到 180 个 epoch 以后就开始在验证集上过拟合,表现为边界框抖动和误检增多。如果你的训练日志显示 mAP 在第 150 个 epoch 后不再上升,可以提前终止。

训练期间还要监控的指标包括:平均召回率、检测框的宽高分布、每个类别的 AP 值。置信度阈值设得太低会导致大量误检,太高又会让漏检率上升,最佳阈值和 NMS 参数有关联,这部分留在下一章详细展开。训练完成后模型权重约 240 MB,我把每轮的权重保存间隔设为 5 个 epoch,这样即使某个 checkpoint 过拟合,也可以回退到前一个版本。

4. mAP、置信度与 NMS:模型评估和调优的关键旋钮

训练结束后不要直接拿模型去预测,先跑一遍验证集评估。评估指标通常用 mAP@0.5,即 IoU 阈值取 0.5 时所有类别的平均精度。红绿灯检测任务中,由于目标尺寸普遍小于 32×32 像素,mAP 的计算方式有一个细节:不同 IoU 阈值下的 AP 对这个任务影响很大,小目标稍微偏移几个像素,IoU 就会从 0.5 掉到 0.3,所以除了 mAP@0.5 之外,还需额外关注 mAP@0.5:0.95,后者对边界框定位精度的要求苛刻得多。

评估前必须做非极大值抑制(NMS),因为一个红绿灯通常会被多个锚框同时命中。NMS 的流程是:按置信度从高到低排序所有预测框,取最高分的框作为基准,移除与其 IoU 大于阈值且类别相同的框,然后重复此步骤。中间层的 52×52 特征图会产生大量候选框,如果 NMS 阈值设得太大,重叠红绿灯的两个灯珠会被合并成一个框;设得太小则会出现同一信号灯多个重复框。我在实验中对比了三组参数:

conf_thresiou_thresmAP@0.5备注
0.250.450.912默认参数,泛化性最好
0.500.450.903误检减少,但部分红灯漏检
0.250.300.887小目标容易被抑制,mAP 下降明显
0.100.600.901召回率提升但重复框严重

我最终线上推理采用conf_thres=0.25, iou_thres=0.45,这套参数在验证集和测试集上表现最均衡。评估命令使用 PyTorch 环境下常见的 eval 脚本:

python val.py \ --weights runs/exp/weights/best.pt \ --data configs/tt100k.yaml \ --img-size 416 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --task val

--weights指向训练过程中验证 mAP 最高的模型,不一定是最新一个 checkpoint;--conf-thres是置信度阈值,只保留大于该值的预测框参与后续计算,它的实际作用是过滤掉低质量预测,但这个值不能单独定,它必须和 NMS 的iou-thres配合,因为 NMS 的输入包含所有置信度高于 conf-thres 的框,如果阈值太低,NMS 的输入框数量剧增,耗时增加,且可能让相邻的多个红灯互相抑制。--task val表示在验证集上计算 mAP,best.pt是训练脚本自动保存的验证集表现最优权重。执行完会得到每个类别的 AP 值和整体的 Precision-Recall 曲线。

从评估结果看,绿灯的 AP 明显高于红灯,原因在于数据集中红灯样本较少,且夜间红灯在 RGB 空间里的颜色特征容易被路灯误判为黄色。如果你同样遇到某一类 AP 偏低的情况,处理思路是收集该类别的误检样本,加入训练集中做 hard negative mining。具体操作是把预测错误的图片额外复制一份,保持标注不变,重新投入训练,但训练循环需要去重,否则重复样本会放大梯度。我在实验中把误检图片加载到独立目录,按 1:3 的比例混入原始训练集,训练 50 个 epoch 后红灯的 AP 从 0.86 提升到 0.91。

评估之后是推理验证。对单张图片的预测命令如下:

python detect.py \ --weights runs/exp/weights/best.pt \ --source data/images/test_day.jpg \ --conf-thres 0.25 \ --iou-thres 0.45 \ --project runs/detect \ --save-txt \ --save-conf

--save-txt会将预测框的中心点、宽高和置信度写入 txt 文件,--save-conf让输出文件包含置信度数值。这样不只看到可视化结果,还能统计模型输出的分布。比如统计 100 张测试图上所有预测框的置信度直方图,如果发现大量预测框集中在 0.25~0.4 区间,说明当前置信度阈值偏保守,可以下调到 0.2 来提升召回率。推理速度方面,416×416 输入在 RTX 2080Ti 上单张耗时约 18 毫秒,其中 NMS 约占 4 毫秒,如果你需要部署到 Jetson 这类边缘设备,建议把iou-thres从 0.45 提高到 0.5,牺牲少量精度来减少候选框数量,NMS 耗时能下降 30%。

5. 红绿灯场景的 anchor 聚簇与阈值组合:一个立刻能见效的调参技巧

前面训练直接沿用了 COCO 锚框,用来跑通流程没问题,但如果想让红绿灯检测再上一个台阶,最有效的操作是对训练集重新做 k-means 锚框聚类。红绿灯高约 20 像素、宽约 10 像素,长宽比接近 2:1,而 COCO 锚框里最小的一对是 (10,13),形状偏方形,导致网络在 52×52 层拟合目标时锚框和真实框的初始 IoU 偏低。聚类时注意距离度量不要用欧式距离,而是用 IoU 距离:

import numpy as np def iou_distance(boxes, anchors): # boxes: (N, 2), 每行为 [w, h],anchors: (K, 2) # 返回 IoU 距离矩阵,距离越小表示锚框与目标越接近 ...

对 TT100K 训练集 6000 多个标注框聚类后,最优组合是(6,12), (10,20), (15,32), (24,42), (38,60), (56,88), (82,120), (120,160), (180,240)。小尺度锚框集中在 6~15 像素区间,这和红绿灯的真实尺寸分布高度吻合。把聚类后的锚框写进配置文件后在相同训练参数下重新训练,mAP 提升约 3 个百分点,直观比较两组实验的 PR 曲线,短发框聚类的曲线在召回率 0.8 以上区间明显更平滑。聚类操作在训练前执行一次即可,训练过程中不需要更新。

另一个容易被忽略的参数是 NMS 前的分类得分合并方式。YOLOv3 在同一个网格位置会输出 3 个锚框的预测,但它们共享同一个分类得分吗?不,每个锚框都有独立的分类得分,只是计算损失时把 3 个锚框中 IoU 最大的一个作为正样本。推理时应该对每个锚框做独立的类别预测,再合并相同类别的重叠框。我见过一些简化实现直接取 3 个锚框的最大值,这样会让一个小而亮的红灯被旁边的大锚框覆盖,所以我建议在检测头实现里把 3 个锚框的分类得分保留全部分支,不合并,NMS 的输入是[cx, cy, w, h, conf, cls_id]六元组,让 NMS 自己去判断保留哪一个。

在实际部署中,红绿灯检测的置信度阈值应动态调整。白天光线充足时conf_thres=0.4都能维持高精度,夜间低照度场景降到0.15才能召回远处的灯珠。你可以用图像整体亮度均值做自适应阈值:亮度大于 128 时用偏高阈值,小于 64 时自动降低到 0.2。这种自适应策略比人工调参稳定得多。另外,对视频流做时序平滑也很实用,连续 5 帧中同一位置的目标至少出现 3 次才输出,能有效过滤单帧误检,这个状态的维护可以用固定长度的 deque 实现,非常轻量。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询