简介:这份资源面向具备一定Python与深度学习基础、希望动手实现目标检测的开发者与学习者,围绕YOLO、SSD、Faster R-CNN等主流检测器,结合TensorFlow框架讲解从数据预处理、模型构建、训练到评估预测的完整流程,帮助读者把目标检测理论落到可运行的代码实践中。压缩包共308个文件,约4.05MB,以210个py源码为主体,辅以proto、config、build、sh等工程配置与编译脚本,另有md说明文档、ipynb笔记及少量jpg、png示例图片,整体构成一套结构完整的工程代码。目前已有3265人学习下载,适合对照代码理解网络结构定义、损失函数与训练评估环节,也可作为课程设计或项目练手的参考,便于读者在真实工程目录中排查配置、复现训练与预测流程。
1. 目标检测 Python 代码实现:从零跑通一套能用的检测流程
很多人第一次接触深度学习目标检测,卡住的地方不是理论,而是拿到一份 Python 代码后不知道从哪下手。这份资源就是一套完整的目标检测 Python 代码实现,覆盖数据准备、模型搭建、训练、推理到评估的全流程。它适合刚入门想跑通第一个检测项目的新手,也适合需要快速搭 baseline 的从业者。代码基于 PyTorch 和 torchvision,用 Tensor 做数据流转,结构清晰,改起来不费劲。下面我按实际拆包的顺序,把这份代码怎么用、参数怎么调、哪里容易翻车讲清楚。
2. 环境搭建与数据管道:把 Tensor 喂进模型之前
2.1 依赖安装与版本对齐
目标检测代码最容易翻车的地方不是模型本身,而是环境版本对不上。这份代码依赖 PyTorch、torchvision、OpenCV、numpy 和 Pillow,版本不匹配会直接报张量维度错误或者 CUDA 算子找不到。我一般会先建一个干净的虚拟环境,再按代码里 requirements 的版本装。
# 创建虚拟环境,避免污染全局 python -m venv detect_env source detect_env/bin/activate # Windows 用 detect_env\Scripts\activate # 安装核心依赖,注意 torch 和 torchvision 版本要配套 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy pillow matplotlib tqdm这里 torch 和 torchvision 必须版本对应,2.1.0 配 0.16.0 是官方验证过的组合。CUDA 版本根据自己显卡选,没有 GPU 就用 CPU 版,但训练会慢很多。装完之后跑一句验证:
import torch import torchvision print(torch.__version__, torchvision.__version__) print(torch.cuda.is_available()) # 有 GPU 应返回 True如果torch.cuda.is_available()返回 False,先检查显卡驱动,再检查装的 torch 是不是 CPU 版。这一步没过就别往下走,后面训练会一直报错。
2.2 数据集组织与 Tensor 转换
目标检测的数据标注格式常见的有 VOC XML、COCO JSON 和 YOLO txt 三种。这份代码默认读 VOC 格式,目录结构要求如下:
dataset/ ├── JPEGImages/ # 所有图片 ├── Annotations/ # 对应的 XML 标注 └── ImageSets/ └── Main/ ├── train.txt # 训练集图片名列表 └── val.txt # 验证集图片名列表如果手里是 YOLO 格式的 txt 标注,需要先转成 VOC XML。转换的核心是把归一化的中心点坐标还原成绝对像素坐标:
import xml.etree.ElementTree as ET from PIL import Image def yolo_to_voc(img_path, txt_path, class_names): """把 YOLO txt 标注转成 VOC XML""" img = Image.open(img_path) w, h = img.size root = ET.Element("annotation") ET.SubElement(root, "filename").text = img_path.split("/")[-1] size = ET.SubElement(root, "size") ET.SubElement(size, "width").text = str(w) ET.SubElement(size, "height").text = str(h) ET.SubElement(size, "depth").text = "3" with open(txt_path) as f: for line in f.readlines(): cls_id, cx, cy, bw, bh = map(float, line.strip().split()) # YOLO 是归一化中心点坐标,转成 VOC 的左上角和右下角 xmin = int((cx - bw / 2) * w) ymin = int((cy - bh / 2) * h) xmax = int((cx + bw / 2) * w) ymax = int((cy + bh / 2) * h) obj = ET.SubElement(root, "object") ET.SubElement(obj, "name").text = class_names[int(cls_id)] bbox = ET.SubElement(obj, "bndbox") ET.SubElement(bbox, "xmin").text = str(max(0, xmin)) ET.SubElement(bbox, "ymin").text = str(max(0, ymin)) ET.SubElement(bbox, "xmax").text = str(min(w, xmax)) ET.SubElement(bbox, "ymax").text = str(min(h, ymax)) tree = ET.ElementTree(root) tree.write(txt_path.replace(".txt", ".xml"))这段代码的关键点是坐标裁剪,max(0, xmin)和min(w, xmax)防止标注框超出图片边界。YOLO 标注里偶尔会有越界框,不裁剪的话后面做数据增强会报错。class_names 列表的顺序必须和标注时的类别索引一致,错一位整个训练就废了。
数据加载部分,代码用 torchvision 的 Dataset 封装,把图片转成 Tensor 并做归一化:
from torchvision import transforms transform = transforms.Compose([ transforms.Resize((416, 416)), # 统一输入尺寸 transforms.ToTensor(), # 转成 Tensor,像素值归到 [0,1] transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # ImageNet 统计值 ])Resize 到 416×416 是这份代码的默认设置,改这个尺寸要同步改模型里的输入层。Normalize 用的三个均值和标准差是 ImageNet 的统计值,如果自己数据集和 ImageNet 分布差很多,可以换成自己算的值,但影响通常不大。
3. 模型结构与训练循环:把检测头接上
3.1 骨干网络与检测头搭建
这份代码的模型结构是典型的单阶段检测器思路:骨干网络提特征,检测头出分类和回归。骨干网络用的是 ResNet 的变体,检测头接在最后两个 stage 上做多尺度预测。核心代码结构如下:
import torch.nn as nn import torchvision.models as models class Detector(nn.Module): def __init__(self, num_classes): super().__init__() # 骨干网络,去掉最后的全连接层 backbone = models.resnet50(pretrained=True) self.layer0 = nn.Sequential(*list(backbone.children())[:5]) self.layer1 = nn.Sequential(*list(backbone.children())[5:6]) self.layer2 = nn.Sequential(*list(backbone.children())[6:7]) # 检测头:每个尺度输出分类和回归 self.cls_head = nn.Conv2d(1024, num_classes, kernel_size=3, padding=1) self.reg_head = nn.Conv2d(1024, 4, kernel_size=3, padding=1) def forward(self, x): c3 = self.layer0(x) # 小尺度特征 c4 = self.layer1(c3) # 中尺度特征 c5 = self.layer2(c4) # 大尺度特征 # 这里简化处理,实际代码会对多尺度特征做融合 cls_out = self.cls_head(c5) reg_out = self.reg_head(c5) return cls_out, reg_outnum_classes要设成自己数据集的类别数加一,加的那个是背景类。pretrained=True 表示用 ImageNet 预训练权重,小数据集上必须开,否则很难收敛。检测头的卷积核用 3×3,padding 设 1 保持特征图尺寸不变。回归输出 4 个值,对应边界框的中心点偏移和宽高缩放。
3.2 损失函数与训练循环
目标检测的损失由分类损失和回归损失两部分组成。分类用交叉熵,回归用 Smooth L1:
import torch.nn.functional as F def detection_loss(cls_pred, reg_pred, cls_target, reg_target): """分类损失 + 回归损失""" # 分类损失,忽略背景类以外的负样本 cls_loss = F.cross_entropy(cls_pred, cls_target, reduction="mean") # 回归损失,只对正样本计算 pos_mask = cls_target > 0 if pos_mask.sum() > 0: reg_loss = F.smooth_l1_loss(reg_pred[pos_mask], reg_target[pos_mask]) else: reg_loss = torch.tensor(0.0, device=reg_pred.device) return cls_loss + reg_losspos_mask用来筛选正样本,只有包含目标的锚框才参与回归损失计算。如果一批数据里一个正样本都没有,reg_loss直接置零,避免 NaN。训练循环里学习率用余弦退火,初始值设 1e-3,每 10 个 epoch 降一次:
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() for images, cls_targets, reg_targets in train_loader: images = images.cuda() cls_pred, reg_pred = model(images) loss = detection_loss(cls_pred, reg_pred, cls_targets.cuda(), reg_targets.cuda()) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(f"Epoch {epoch}, Loss: {loss.item():.4f}")momentum 设 0.9 是 SGD 的常规配置,weight_decay 用 1e-4 做 L2 正则。T_max 等于总 epoch 数,余弦退火会在这个周期内把学习率从初始值降到接近零。训练时盯着 loss 曲线,如果前几个 epoch 就降到很低,大概率是数据标注有问题或者类别不平衡太严重。
4. 推理与评估:模型到底能不能用
4.1 单张图片推理与后处理
训练完保存权重后,推理流程是加载模型、读图、前向传播、后处理出框:
import cv2 import numpy as np def inference(model, img_path, conf_thresh=0.5, nms_thresh=0.4): model.eval() img = cv2.imread(img_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor = transform(img_rgb).unsqueeze(0).cuda() with torch.no_grad(): cls_pred, reg_pred = model(tensor) # 取分类置信度最高的类别 scores = F.softmax(cls_pred, dim=1) conf, labels = scores.max(dim=1) # 过滤低置信度 keep = conf > conf_thresh boxes = reg_pred[keep] labels = labels[keep] conf = conf[keep] # NMS 去重 keep_idx = torchvision.ops.nms(boxes, conf, nms_thresh) return boxes[keep_idx], labels[keep_idx], conf[keep_idx]conf_thresh 设 0.5 是保守值,漏检多就降到 0.3,误检多就升到 0.6。nms_thresh 控制重叠框的合并程度,0.4 适合大多数场景,密集小目标可以调到 0.5。后处理里torchvision.ops.nms要求框格式是 xyxy,如果模型输出的是 xywh 要先转换。
4.2 mAP 评估与指标解读
评估用 mAP(mean Average Precision),代码里实现了 VOC 风格的 11 点插值:
def compute_ap(recalls, precisions): """VOC 11 点插值计算 AP""" ap = 0.0 for t in np.arange(0, 1.1, 0.1): if np.sum(recalls >= t) == 0: p = 0 else: p = np.max(precisions[recalls >= t]) ap += p / 11.0 return aprecalls 和 precisions 是按置信度排序后逐样本累计算的。mAP 是所有类别 AP 的平均值,一般看 mAP@0.5 就够,要求高的话再看 mAP@0.5:0.95。评估时注意把模型切到 eval 模式,否则 BatchNorm 层会用当前 batch 的统计量,结果不稳定。
5. 避坑与排查:那些让我重跑训练的血泪经验
5.1 损失不下降或直接变 NaN
现象:训练几个 epoch 后 loss 突然变成 NaN,或者一直卡在 10 以上不降。原因通常是学习率太大或者标注框坐标有负数。解决:先把学习率降到 1e-4 试,再检查标注文件里有没有 xmin 大于 xmax 的情况。我写了个脚本专门扫标注:
def check_annotations(anno_dir): """扫描 VOC 标注里的非法框""" for xml_file in os.listdir(anno_dir): tree = ET.parse(os.path.join(anno_dir, xml_file)) for obj in tree.findall("object"): bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) xmax = int(bbox.find("xmax").text) if xmin >= xmax: print(f"非法框: {xml_file}, xmin={xmin}, xmax={xmax}")5.2 显存不够导致训练中断
现象:报 CUDA out of memory,训练到一半崩了。原因:batch size 太大或者输入分辨率太高。解决:把 batch size 减半,或者把 Resize 的尺寸从 416 降到 320。还有一个隐藏原因是数据加载的 num_workers 设太大,每个 worker 都会占显存,设成 4 就够。
5.3 推理结果框位置偏移严重
现象:模型能出框,但框的位置和实际目标差很远。原因:训练时的坐标编码方式和推理时的解码方式不一致。解决:检查训练时回归目标用的是中心点偏移还是角点偏移,推理时必须用对应的逆变换。这份代码用的是中心点偏移,解码时要先乘回宽高再加上中心点坐标。
5.4 验证集 mAP 远低于训练集
现象:训练 loss 很低,但验证集 mAP 只有 0.1 左右。原因:过拟合或者数据增强太弱。解决:加数据增强,随机翻转、随机裁剪、颜色抖动都加上。另外检查训练集和验证集的类别分布是否一致,如果验证集里有很多训练集没见过的类别,mAP 低是正常的。
6. 进阶技巧:把推理速度压到实时
模型训完之后,实际部署时最关心的是推理速度。这份代码默认的 ResNet50 骨干在 416×416 输入下,单张 GPU 推理大概 20ms 左右,但加上后处理和数据搬运,端到端可能到 50ms。想压到实时(30 FPS 以上),有几个方向可以试。
第一个是换轻量骨干。把 ResNet50 换成 MobileNetV3 或者 ShuffleNetV2,参数量降一个数量级,推理速度能翻倍。改法很简单,把models.resnet50换成models.mobilenet_v3_small,然后调整检测头的输入通道数。精度会掉几个点,但速度提升明显。
第二个是导出 ONNX 做推理加速。PyTorch 原生推理有框架开销,导出成 ONNX 后用 ONNX Runtime 跑,CPU 上都能快不少:
import torch.onnx model.eval() dummy_input = torch.randn(1, 3, 416, 416).cuda() torch.onnx.export( model, dummy_input, "detector.onnx", input_names=["input"], output_names=["cls", "reg"], dynamic_axes={"input": {0: "batch"}, "cls": {0: "batch"}, "reg": {0: "batch"}}, opset_version=11 )dynamic_axes 把 batch 维度设成动态的,这样导出后的模型能接受任意 batch size。opset_version 用 11 兼容性最好,新版本有些算子 ONNX Runtime 还不支持。
第三个是后处理优化。NMS 在框多的时候很耗时,可以先用置信度阈值砍掉大部分框,再做 NMS。另外把后处理从 Python 循环改成 Tensor 操作,速度能快好几倍。我一般会把置信度过滤和 NMS 都放在 GPU 上做,避免数据在 CPU 和 GPU 之间来回搬。
验证加速效果时,别只看单次推理时间,要跑 100 次取平均,并且第一次推理要跳过,因为第一次有初始化开销。我习惯用torch.cuda.Event做精确计时:
start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) # 预热 for _ in range(10): model(dummy_input) start.record() for _ in range(100): model(dummy_input) end.record() torch.cuda.synchronize() print(f"平均推理时间: {start.elapsed_time(end) / 100:.2f} ms")从那以后我每次改完模型结构或者换骨干,都强制走一遍这个计时流程,不然光看代码感觉挺快,实际跑起来可能差很远。希望帮到你。
本文还有配套的精品资源,点击获取