简介这份资源面向具备一定Python与深度学习基础、希望动手实现目标检测的开发者与学习者围绕YOLO、SSD、Faster R-CNN等主流检测器结合TensorFlow框架讲解从数据预处理、模型构建到训练评估与预测部署的完整流程帮助读者把目标检测理论落到可运行的代码实践中。压缩包共308个文件约4.05MB以210个Python源码为主体辅以26个proto接口定义、15个config配置、16个md说明文档及若干build、sh脚本与图片样本覆盖数据加载、网络定义、训练与推理等模块目录结构清晰便于按环节查阅与调试。目前已有3265人学习下载适合作为课程实验、项目练手或自学参考读者可借助其中的源码与配置理解检测模型的搭建思路并在此基础上迁移到自己的数据集与任务中。1. 从一张误检的工地安全帽说起这套目标检测 Python 代码到底能干什么去年帮一个做智慧工地的朋友看模型他拿 YOLO 权重去跑安全帽检测结果把反光背心上的高亮块全框成了“未戴帽”。问题不在权重而在他从 GitHub 上抄的那份推理脚本——预处理里 letterbox 的填充值写成了 0而训练时用的是 114输入分布直接对不上。这件事让我意识到目标检测这行当里代码实现的细节比模型结构本身更容易翻车。这份《深度学习中的目标检测 python 代码实现》资源就是冲着这类问题来的。它不是一篇讲 YOLO 原理的科普而是一套能直接跑的 Python 工程代码覆盖从数据加载、模型构建、训练循环到推理部署的完整链路。技术栈上走的是PyTorch torchvision OpenCV这条最主流的路线同时兼容Tensor操作和tensorboard可视化。适合两类人一是刚学完 CNN、想找个完整项目练手的学生二是手里有标注数据、需要快速搭一套检测 pipeline 的工程师。代码里对 anchor 生成、NMS、mAP 计算这些容易写错的环节都做了封装省得你再去踩我当年踩过的坑。2. 工程骨架拆解目录结构、依赖与数据流走向拿到一份检测代码我习惯先看目录再跑脚本。目录乱的项目后面调参一定痛苦。这套代码的结构算是比较规整的下面按我实际阅读的顺序拆一遍。2.1 目录布局与模块职责object_detection/ ├── configs/ │ └── default.yaml # 超参、路径、模型尺寸集中管理 ├── datasets/ │ ├── voc_dataset.py # VOC 格式解析 │ └── coco_dataset.py # COCO 格式解析 ├── models/ │ ├── backbone.py # 主干网络ResNet / MobileNet 可切换 │ ├── neck.py # FPN 特征金字塔 │ └── head.py # 检测头输出 cls reg ├── utils/ │ ├── boxes.py # 框编解码、IoU、NMS │ ├── anchors.py # anchor 生成与匹配 │ └── metrics.py # mAP 计算 ├── train.py # 训练入口 ├── eval.py # 验证入口 └── infer.py # 单图/批量推理这个布局的好处是职责边界清晰models/只管网络前向utils/boxes.py只管几何运算datasets/只管把标注转成 tensor。我见过太多项目把 NMS 写在模型 forward 里结果导出 ONNX 时直接报错。这套代码把后处理独立出来部署时替换起来方便很多。2.2 依赖安装与环境确认依赖不算多但版本要对齐。我一般会先建虚拟环境再装# 建议 Python 3.8CUDA 11.x 环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install torch1.13.1 torchvision0.14.1 pip install opencv-python numpy pyyaml tensorboard tqdm装完后跑一句验证确认 GPU 可用import torch print(torch.__version__, torch.cuda.is_available()) # 期望输出类似 1.13.1 True如果cuda.is_available()返回 False先别急着改代码八成是 torch 版本和驱动不匹配。我一般会去 PyTorch 官网用它的版本选择器重新拿一条安装命令比手动折腾 wheel 靠谱。2.3 配置文件驱动的数据流这套代码把几乎所有可变项都塞进了configs/default.yaml训练脚本只读配置不硬编码。核心字段如下字段含义常见取值dataset.name数据集格式voc/cocodataset.root数据根目录自定义路径model.backbone主干网络resnet50/mobilenet_v2model.num_classes类别数不含背景20 / 80train.batch_size批大小8 / 16train.lr初始学习率0.01 / 0.001anchor.scalesanchor 尺度[32,64,128,256,512]anchor.ratiosanchor 宽高比[0.5,1,2]数据流走向是datasets读图 → 归一化 → 按 config 里的尺寸 resize → collate 成 batch → 送进models前向 →head输出原始预测 →utils/boxes.py解码 →metrics.py算 loss 和 mAP。整条链路没有隐藏的全局变量调试时在任意环节打断点都能看清 tensor 形状。提示改num_classes时记得同步改数据集解析里的类别映射否则会出现标签越界报错信息往往指向 loss 计算容易误导排查方向。3. 核心模块代码逐段讲anchor、检测头与损失函数这一章是整份代码最值钱的部分。我把三个最容易写错、也最影响精度的模块拎出来逐段过一遍。3.1 anchor 生成与正负样本匹配anchor 是检测的基石。生成逻辑写错后面怎么调都白搭。import numpy as np def generate_anchors(feature_sizes, scales, ratios, stride): feature_sizes: [(H1,W1), (H2,W2), ...] 各层特征图尺寸 scales: anchor 面积尺度列表 ratios: 宽高比列表 stride: 各层下采样倍数 anchors [] for (h, w), s in zip(feature_sizes, stride): # 每个特征点对应原图上的中心坐标 shift_x np.arange(w) * s s / 2 shift_y np.arange(h) * s s / 2 cx, cy np.meshgrid(shift_x, shift_y) # 对每个尺度、每个比例生成基础框 for scale in scales: for ratio in ratios: area scale * scale bw np.sqrt(area / ratio) bh bw * ratio # 中心点 宽高 - 左上右下 x1 cx - bw / 2 y1 cy - bh / 2 x2 cx bw / 2 y2 cy bh / 2 anchors.append(np.stack([x1, y1, x2, y2], axis-1)) return np.concatenate([a.reshape(-1, 4) for a in anchors], axis0)这段代码的关键在shift_x和shift_y的偏移量计算。s / 2是让 anchor 中心落在特征点对应的原图感受野中心漏掉这个偏移会导致框整体偏半个 stride小目标上尤其明显。scales和ratios的笛卡尔积决定了每层特征图的 anchor 数量比如 3 尺度 × 3 比例 9 个 anchor这是比较通用的配置。匹配阶段用 IoU 把 anchor 分成正负样本常见做法是IoU 0.5 记为正IoU 0.4 记为负中间忽略。我一般会额外加一条规则——每个 GT 至少匹配一个 IoU 最大的 anchor防止某些小目标因为 IoU 不够而完全没有正样本。3.2 检测头的前向与框解码检测头负责把特征图变成类别得分和框偏移量。import torch import torch.nn as nn class DetectionHead(nn.Module): def __init__(self, in_channels, num_anchors, num_classes): super().__init__() self.num_classes num_classes self.num_anchors num_anchors # 分类分支每个 anchor 输出 num_classes 个得分 self.cls_head nn.Conv2d(in_channels, num_anchors * num_classes, 3, padding1) # 回归分支每个 anchor 输出 4 个偏移量 (dx, dy, dw, dh) self.reg_head nn.Conv2d(in_channels, num_anchors * 4, 3, padding1) def forward(self, x): cls self.cls_head(x) # [B, A*C, H, W] reg self.reg_head(x) # [B, A*4, H, W] B, _, H, W cls.shape # 重排成 [B, H*W*A, C] 和 [B, H*W*A, 4] cls cls.permute(0, 2, 3, 1).reshape(B, -1, self.num_classes) reg reg.permute(0, 2, 3, 1).reshape(B, -1, 4) return cls, regpermute加reshape这两步是固定套路目的是把空间维度和 anchor 维度合并方便后续和 anchor 一一对应。解码时用标准的偏移公式def decode_boxes(anchors, reg): anchors: [N, 4] 格式 (x1, y1, x2, y2) reg: [N, 4] 网络输出的 (dx, dy, dw, dh) aw anchors[:, 2] - anchors[:, 0] ah anchors[:, 3] - anchors[:, 1] acx anchors[:, 0] 0.5 * aw acy anchors[:, 1] 0.5 * ah dx, dy, dw, dh reg.unbind(-1) # 中心偏移按 anchor 宽高缩放宽高用 exp 保证为正 cx acx dx * aw cy acy dy * ah w aw * torch.exp(dw) h ah * torch.exp(dh) x1 cx - 0.5 * w y1 cy - 0.5 * h x2 cx 0.5 * w y2 cy 0.5 * h return torch.stack([x1, y1, x2, y2], dim-1)exp(dw)这个设计是为了让预测的宽高恒为正同时把回归目标压缩到对数空间数值更稳定。如果换成直接预测宽高训练初期很容易出现负值导致 loss 爆炸。3.3 多任务损失与正负样本平衡检测的 loss 是分类 loss 加回归 loss难点在于正负样本极度不平衡。import torch.nn.functional as F def detection_loss(cls_pred, reg_pred, cls_target, reg_target, pos_mask): cls_pred: [N, C] 分类预测 reg_pred: [N, 4] 回归预测 cls_target: [N] 类别标签-1 表示忽略 reg_target: [N, 4] 回归目标 pos_mask: [N] boolTrue 为正样本 # 分类用交叉熵忽略 -1 的样本 cls_loss F.cross_entropy(cls_pred, cls_target, ignore_index-1, reductionmean) # 回归只对正样本计算 smooth L1 if pos_mask.sum() 0: reg_loss F.smooth_l1_loss( reg_pred[pos_mask], reg_target[pos_mask], reductionsum ) / pos_mask.sum() else: reg_loss torch.tensor(0.0, devicecls_pred.device) return cls_loss reg_loss这里ignore_index-1处理的是那些 IoU 处于中间地带的 anchor不参与分类梯度。回归 loss 除以正样本数而不是总样本数是为了让不同 batch 的回归梯度量级一致。我一般还会给分类 loss 加 focal loss 的变体在正负比超过 1:100 时效果更稳但这份代码默认用交叉熵改起来也就几行的事。注意如果训练时 loss 一直不降先打印pos_mask.sum()正样本数为 0 的话回归分支根本拿不到梯度模型自然学不动。4. 训练、评估与推理三条命令跑通全流程代码能读懂是一回事能跑起来是另一回事。这一章按实际执行顺序把训练、评估、推理三条链路串一遍。4.1 训练入口与关键参数训练脚本读 yaml 配置命令行只覆盖少量高频参数python train.py \ --config configs/default.yaml \ --batch_size 16 \ --lr 0.01 \ --epochs 50 \ --gpu 0启动后会在runs/下生成带时间戳的目录里面存权重、日志和 tensorboard 事件文件。我一般会开两个终端一个跑训练一个跑tensorboard --logdir runs实时看 loss 曲线和 mAP 变化。训练循环里有个细节值得说学习率 warmup。前 500 个 iteration 从lr * 0.001线性升到lr避免初期梯度震荡。这份代码在train.py里用了一个简单的 lambda 实现def lr_lambda(step): if step warmup_steps: return step / warmup_steps # 余弦退火 progress (step - warmup_steps) / (total_steps - warmup_steps) return 0.5 * (1 math.cos(math.pi * progress))warmup_steps一般设成总步数的 5% 左右。如果数据集很小可以适当调低否则 warmup 阶段占掉太多训练时间。4.2 评估指标 mAP 的计算评估脚本会输出各类 AP 和总 mAPpython eval.py --config configs/default.yaml --weights runs/exp1/best.pthmAP 计算的核心是对每个类别把所有预测按置信度排序逐个计算 precision 和 recall画出 PR 曲线取曲线下面积。代码里utils/metrics.py做了向量化实现比逐条循环快很多。这里有个容易忽略的点——IoU 阈值。VOC 用 0.5COCO 用 0.5 到 0.95 每隔 0.05 算一次再平均。评估前先确认configs里的eval.iou_thresh和你的数据集标准一致否则指标没有可比性。4.3 单图推理与结果可视化推理脚本支持单图和目录批量python infer.py \ --weights runs/exp1/best.pth \ --source test_images/ \ --conf_thresh 0.3 \ --nms_thresh 0.45 \ --save_dir outputs/conf_thresh是置信度过滤阈值nms_thresh是 NMS 的 IoU 阈值。这两个参数对最终框的数量影响很大conf_thresh调低会召回更多目标但误检增加nms_thresh调低会抑制重叠框但可能把相邻目标误删。我一般先在验证集上扫一遍这两个参数的组合选 F1 最高的那组而不是拍脑袋定 0.5。推理输出的可视化用 OpenCV 画框加类别标签颜色按类别哈希生成保证同一类颜色一致。如果要在无 GUI 的服务器上跑记得把cv2.imshow换成cv2.imwrite这个坑我踩过不止一次。5. 避坑与排查五个真实翻车现场这一章记录的是我在跑这类检测代码时实际遇到的问题按「现象 → 原因 → 解决」写希望能帮你省点时间。现象一训练 loss 正常下降但 mAP 始终在 0.01 左右。原因类别标签从 1 开始编号而cross_entropy期望 0 起始导致所有标签错位一位。 解决在 dataset 的__getitem__里统一做label label - 1或者把num_classes加一并在映射表里补一个背景类。检查方法是打印一个 batch 的标签最大值确认小于num_classes。现象二推理时框的位置整体偏移尤其是图像边缘的目标。原因预处理 resize 时直接拉伸没有保持宽高比而 anchor 是按正方形感受野生成的。 解决改用 letterbox 填充记录缩放比例和填充偏移解码后再映射回原图坐标。填充值要和训练时一致常见是 114。现象三GPU 显存够但 batch_size 稍大就 OOM。原因FPN 的多层特征图在计算 loss 时全部保留中间变量没及时释放。 解决在 loss 计算前对不需要梯度的分支用torch.no_grad()包住或者在 config 里减少参与训练的 FPN 层数。另外torch.cuda.empty_cache()治标不治本关键还是控制中间张量的生命周期。现象四mAP 评估结果比训练日志里显示的低很多。原因评估时忘了把模型切到eval()模式BN 层还在用 batch 统计量。 解决评估和推理前固定加model.eval()并用with torch.no_grad():包住前向。这个错误很隐蔽因为 loss 看起来正常只是指标偏低。现象五换了自己的数据集后anchor 匹配的正样本数为 0。原因自定义数据集的框普遍偏小而默认 anchor 尺度最小是 32IoU 达不到 0.5。 解决用 k-means 对训练集框的宽高做聚类重新生成 anchor 尺度。代码里utils/anchors.py留了kmeans_anchors函数传入标注文件路径即可。6. 二次开发换主干、加类别与导出部署的实操思路跑通之后多数人下一步就是改成自己的项目。这一章讲三个最常改的方向以及我验证改动是否生效的习惯。换主干网络。models/backbone.py里用了一个注册表模式新增主干只要继承nn.Module并实现out_channels属性然后在BACKBONES字典里注册名字。比如换成mobilenet_v3改 config 里的model.backbone即可FPN 会自动读取out_channels做通道对齐。验证方法是打印各层输出形状确认和 FPN 输入匹配。增加类别。改三处config 的num_classes、dataset 的类别映射表、以及如果有预训练权重把检测头最后一层的输出通道替换掉。替换后预训练权重里分类分支的参数会失效但回归分支和主干可以继续用。我一般会冻结主干前几个 stage 训练几轮再解冻全量微调。导出 ONNX 部署。这套代码的后处理独立在utils/boxes.py导出时只导模型前向NMS 放在推理引擎里做。导出命令python export_onnx.py --weights best.pth --input_size 640 640 --output model.onnx导出后务必用onnxruntime跑一遍和 PyTorch 输出逐元素对比误差在 1e-3 以内才算通过。我见过太多导出后精度掉一截的案例根源都是预处理没对齐。改造方向改动文件验证方式换主干models/backbone.py config打印各层输出形状加类别config dataset head单图推理看标签导出部署export_onnx.pyonnxruntime 对比输出换 anchorutils/anchors.py打印正样本数从那以后我每次改完配置或网络结构都强制走一遍「单图过拟合」——拿一张图训练 100 步看能不能把 loss 压到接近 0。这一步能过说明数据流和梯度链路没问题再去跑全量数据才不至于浪费一晚上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?