简介:这份资源面向具备一定Python与深度学习基础、希望动手实现目标检测的开发者与学习者,围绕YOLO、SSD、Faster R-CNN等主流检测器,结合TensorFlow框架讲解从数据预处理、模型构建、训练到评估预测的完整流程,帮助读者把目标检测理论落到可运行的代码实践中。压缩包共308个文件,约4.05MB,以210个py源码为主体,配合26个proto、15个config、12个build等配置与构建文件,另有16个md说明、9个sh脚本及少量图片、ipynb与pbtxt,覆盖数据加载、网络定义、训练评估等模块,目录结构清晰,便于按环节检索与调试。目前已有3265人学习下载,适合作为课程实验、项目练手或算法入门的参考代码,读者可借此理解检测模型的搭建思路与工程组织方式,并在此基础上迁移到自己的数据集与任务中。
1. 目标检测 Python 代码实现:从零跑通一套能用的检测流程
如果你手头正好有一份深度学习目标检测的 Python 代码实现,却不确定它能不能直接跑、依赖怎么装、参数怎么调,那这篇笔记就是写给你的。目标检测和图像分类最大的区别在于,分类只告诉你「图里有什么」,检测还要告诉你「在哪里、有几个」。这就决定了它的代码链路更长:数据标注格式、Anchor 设计、损失函数、后处理 NMS,每一环都可能让结果从「能看」变成「玄学」。
这份资源是一套基于 Python 的目标检测代码实现,核心依赖深度学习框架和张量计算库,覆盖从数据加载、模型搭建、训练到推理的完整流程。它适合两类人:一是刚学完卷积网络、想动手跑通检测流程的新手;二是需要快速搭一个检测 baseline、再往上改的从业者。下面我按「先立住原理、再动手复现、最后避坑」的顺序拆开讲,每一步都落到能抄的代码和能改的参数上。
2. 检测流程拆解:数据、模型、损失三件事怎么串起来
目标检测的代码看起来长,本质就三件事:把标注喂成模型能吃的格式、用一个网络同时预测类别和框、用损失把预测拉向真值。这一章先把这三件事的原理和选型理由讲清楚,再给出可执行的代码骨架。
2.1 数据加载与标注格式转换
检测任务的数据集通常有两种标注形式:一种是每张图对应一个 XML 或 JSON,里面记录每个目标的类别和左上角、右下角坐标;另一种是一个总的 CSV 或 TXT,每行是「图片路径, xmin, ymin, xmax, ymax, class」。代码实现里一般会统一成后者,因为读取快、便于打乱。
常见做法是写一个 Dataset 类,继承框架自带的基类,在__getitem__里做三件事:读图、把框坐标归一化、返回张量。下面是一个可复用的骨架:
import os import torch from torch.utils.data import Dataset from PIL import Image class DetectDataset(Dataset): def __init__(self, anno_file, img_dir, img_size=416, transform=None): # anno_file 每行: 图片名,xmin,ymin,xmax,ymax,class_id self.img_dir = img_dir self.img_size = img_size self.transform = transform self.samples = [] with open(anno_file, 'r') as f: for line in f: parts = line.strip().split(',') self.samples.append(parts) def __len__(self): return len(self.samples) def __getitem__(self, idx): parts = self.samples[idx] img_path = os.path.join(self.img_dir, parts[0]) img = Image.open(img_path).convert('RGB') w, h = img.size # 坐标归一化到 0~1,避免不同分辨率带来的尺度问题 boxes = [] for i in range(1, len(parts), 5): xmin = float(parts[i]) / w ymin = float(parts[i+1]) / h xmax = float(parts[i+2]) / w ymax = float(parts[i+3]) / h cls = int(parts[i+4]) boxes.append([xmin, ymin, xmax, ymax, cls]) img = img.resize((self.img_size, self.img_size)) if self.transform: img = self.transform(img) return img, torch.tensor(boxes, dtype=torch.float32)逻辑说明:__init__只做路径和标注的索引,不读图,避免内存爆掉;__getitem__里先归一化再 resize,顺序不能反,否则坐标会错位。参数上img_size一般取 416 或 608,取 416 是速度和精度的折中,显存小于 6G 就老实用 416。transform里至少要有ToTensor和归一化,均值方差用 ImageNet 的即可,不必自己重算。
提示:如果标注里有宽或高为 0 的脏框,归一化后会变成 NaN,训练几个 step 就 loss 爆炸。加载时加一句过滤,把
xmax<=xmin或ymax<=ymin的框直接丢掉。
2.2 模型结构选型:Anchor-based 还是 Anchor-free
代码实现里最常见的是 Anchor-based 思路,也就是在每个特征图格子上预设几种宽高比的框,网络预测的是相对这些预设框的偏移量。选它的理由是收敛稳、对小目标友好,缺点是 Anchor 的尺寸要靠聚类调,调不好召回率上不去。
另一种是 Anchor-free,直接预测中心点和宽高,省掉了聚类这一步,代码更短,但在密集小目标场景下容易漏。如果你这份代码实现里带的是 Anchor-based,建议先别急着换,把 Anchor 尺寸用 K-means 在自家数据上聚一遍,往往比换结构涨点更明显。
下面是一个极简的检测头,展示 Anchor-based 的输出维度怎么来的:
import torch.nn as nn class DetectHead(nn.Module): def __init__(self, in_ch, num_anchors, num_classes): super().__init__() # 每个 anchor 预测: 4 个偏移 + 1 个前景置信度 + num_classes 个类别 self.conv = nn.Conv2d(in_ch, num_anchors * (5 + num_classes), 1) def forward(self, x): # x: [B, in_ch, H, W] out = self.conv(x) B, _, H, W = out.shape # 重排成 [B, H*W*num_anchors, 5+num_classes] out = out.view(B, -1, 5 + self.num_classes) return out逻辑说明:1x1卷积不改变空间尺寸,只把通道数映射到「每个 Anchor 需要的预测数」。num_anchors通常取 3,对应三种宽高比。参数上num_classes不含背景,背景由前景置信度那一维负责。这里没写解码逻辑,解码时要把偏移量乘上 Anchor 的宽高再加上中心坐标,顺序写反是新手最常见的翻车点。
2.3 损失函数:分类损失和回归损失怎么配比
检测的损失是多项相加:分类用交叉熵,回归用 IoU 系列损失,再加一个前景/背景的二分类损失。配比很关键,回归损失权重给大了,框准了但类别乱;给小了,类别对但框飘。
常见做法是分类损失权重 1.0,回归损失权重 5.0 左右,前景损失 1.0。如果训练前期 loss 下降很慢,先把回归权重降到 2.0 试试。代码里一般会用一个loss_dict把三项分开记录,方便定位是哪一项不收敛。
def compute_loss(pred_cls, pred_box, pred_obj, gt_cls, gt_box, gt_obj): cls_loss = nn.functional.cross_entropy(pred_cls, gt_cls) box_loss = nn.functional.smooth_l1_loss(pred_box, gt_box, reduction='sum') obj_loss = nn.functional.binary_cross_entropy_with_logits(pred_obj, gt_obj) # 回归损失按正样本数归一化,避免 batch 内目标数波动导致 loss 抖动 num_pos = gt_obj.sum().clamp(min=1) total = cls_loss + 5.0 * box_loss / num_pos + obj_loss return total, {'cls': cls_loss.item(), 'box': box_loss.item(), 'obj': obj_loss.item()}逻辑说明:smooth_l1_loss比 MSE 对离群框更稳,reduction='sum'后除以正样本数是为了让不同 batch 的 loss 可比。参数上那个5.0就是回归权重,是调参时第一个该动的地方。clamp(min=1)是防止某张图没有目标时除零,这个边界不处理,训练到一半就会出 NaN。
3. 训练与推理落地:命令行、参数和验证指标
原理立住之后,这一章落到实际跑起来。训练脚本怎么调、推理怎么出框、指标怎么看,每一步都给到能直接用的命令和参数含义。
3.1 训练脚本的启动参数
训练入口一般是一个train.py,用argparse接参数。下面这条命令是我常用的起步配置:
python train.py \ --data ./data/anno.txt \ --img-dir ./data/images \ --epochs 100 \ --batch-size 8 \ --img-size 416 \ --lr 0.001 \ --weights ./weights/pretrain.pth \ --device cuda:0参数说明:batch-size取 8 是 6G 显存下的安全值,显存够可以上 16,但学习率要按比例放大到 0.002 左右。lr用 0.001 配 Adam 起步,如果 loss 在前 5 个 epoch 就震荡,降到 0.0005。weights指向预训练权重,检测任务从分类预训练权重起步比随机初始化收敛快一倍不止,这一步别省。device写cuda:0而不是cuda,多卡机器上能避免默认占满所有卡。
训练过程中要盯三个数:总 loss、验证集 mAP、学习率。总 loss 不降先查数据标注;mAP 不涨但 loss 降,多半是过拟合,加数据增强或早停;学习率如果用了余弦退火,后期降到 1e-5 以下就可以停了。
3.2 推理与 NMS 后处理
推理阶段网络会输出成千上万个框,必须用 NMS 去掉重叠的。下面是一个不依赖第三方库的 NMS 实现,方便你直接嵌进代码:
import torch def nms(boxes, scores, iou_thres=0.5): # boxes: [N, 4] 格式为 xmin,ymin,xmax,ymax keep = [] order = scores.argsort(descending=True) while order.numel() > 0: i = order[0].item() keep.append(i) if order.numel() == 1: break # 计算当前框与剩余框的 IoU xx1 = torch.clamp(boxes[order[1:], 0], min=boxes[i, 0]) yy1 = torch.clamp(boxes[order[1:], 1], min=boxes[i, 1]) xx2 = torch.clamp(boxes[order[1:], 2], max=boxes[i, 2]) yy2 = torch.clamp(boxes[order[1:], 3], max=boxes[i, 3]) inter = (xx2 - xx1).clamp(min=0) * (yy2 - yy1).clamp(min=0) area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) area_o = (boxes[order[1:], 2] - boxes[order[1:], 0]) * \ (boxes[order[1:], 3] - boxes[order[1:], 1]) iou = inter / (area_i + area_o - inter + 1e-6) order = order[1:][iou <= iou_thres] return keep逻辑说明:按置信度从高到低排序,每次取最高的留下,然后把它和剩下框的 IoU 算出来,超过阈值的丢掉。参数iou_thres取 0.5 是通用值,如果同类目标挨得很近(比如一排瓶子),调到 0.6 到 0.7,否则会把挨着的目标误删。1e-6是防除零,别省。
推理时还要设一个置信度阈值,低于它的框直接不要。这个阈值取 0.3 到 0.5 之间,取低了误检多,取高了漏检多。验证集上画一条 precision-recall 曲线,选 F1 最高的点对应的阈值,比拍脑袋定靠谱。
3.3 指标验证:mAP 怎么算、怎么看
mAP 是检测任务的核心指标,计算逻辑是:对每个类别,把预测框按置信度排序,逐个算 precision 和 recall,画出 PR 曲线,曲线下面积就是 AP,所有类别取平均就是 mAP。IoU 阈值取 0.5 时记作 mAP@0.5,取 0.5 到 0.95 平均记作 mAP@[.5:.95],后者更严格。
验证脚本一般会输出一张表,每个类别一行。看表时重点看两类:AP 特别低的类别,多半是样本太少或 Anchor 不匹配;AP 高但实际效果差的类别,可能是验证集和训练集分布太像,过拟合了。常见做法是留出 10% 的数据不参与训练,专门做验证,别用训练集算 mAP,那个数没有参考价值。
注意:mAP 计算时如果某个类别在验证集里一个目标都没有,AP 会算成 0,拉低整体均值。这种情况要么补数据,要么在评估时跳过该类,别让它污染指标。
4. 避坑与排查:训练不收敛、显存爆、框错位的真实原因
这一章是我踩过的坑里最高频的几条,每条按「现象 → 原因 → 解决」写,你对着查能省不少时间。
4.1 现象:loss 从第一个 epoch 就是 NaN
原因:九成是标注里有非法框,归一化后出现 NaN,或者学习率太大导致梯度爆炸。先查数据,再查学习率。
解决:在 Dataset 里加过滤,把xmax<=xmin或ymax<=ymin的框丢掉;学习率从 0.001 降到 0.0001 再跑一遍。如果还 NaN,在 loss 后面加torch.nn.utils.clip_grad_norm_(model.parameters(), 10)做梯度裁剪。
4.2 现象:训练 loss 正常降,但验证 mAP 一直不动
原因:最常见的是数据增强太弱,模型把训练集背下来了;其次是 Anchor 尺寸和真实框分布差太远,正样本匹配不上。
解决:加随机翻转、随机裁剪、色彩抖动,增强力度以验证集 mAP 为准调。Anchor 用 K-means 在自家标注上聚一遍,把聚类得到的宽高替换掉默认值,这一步通常能涨 3 到 5 个点。
4.3 现象:显存够但一跑就 OOM
原因:不一定是 batch-size 大,可能是验证阶段没加torch.no_grad(),或者 NMS 在 CPU 上对几万个框做循环,内存爆了。
解决:验证和推理全部包在with torch.no_grad():里;NMS 前先按置信度阈值过滤掉大部分框,再送进 NMS,输入从几万降到几百,内存和速度都会好很多。
4.4 现象:推理出的框整体偏移或大小不对
原因:解码时 Anchor 的中心坐标和宽高用错了顺序,或者归一化和反归一化没对上。
解决:检查解码公式,标准写法是x_center = (sigmoid(tx) + grid_x) / grid_w,w = anchor_w * exp(tw)。如果框整体偏左上,多半是 grid 的索引从 0 开始还是从 1 开始搞混了。反归一化时乘回原图宽高,别乘成 resize 后的尺寸。
4.5 现象:同一张图每次推理结果都不一样
原因:模型没切到 eval 模式,BatchNorm 还在用 batch 统计量;或者 Dropout 没关。
解决:推理前调model.eval(),并用torch.no_grad()包住。如果还有随机性,检查数据预处理里有没有随机增强没关掉,验证和推理阶段的 transform 必须是确定性的。
5. 进阶技巧:把检测结果落到业务里的两个实用手段
跑通训练只是第一步,真正落地时还有两件事值得做:一是把检测框和业务逻辑接起来,二是用可视化快速定位问题。这一章给两个具体技巧,都是我在实际项目里反复用的。
5.1 用置信度分层做业务过滤
业务场景里不同类别对误检的容忍度不一样。比如安全帽检测,漏检一个没戴帽子的代价远大于误检一个戴了的。这时候不要用统一的置信度阈值,而是按类别分层设。
# 每个类别一个阈值,漏检代价高的类别阈值调低 class_thres = { 0: 0.3, # 高风险类别,宁可误检不可漏检 1: 0.5, # 普通类别 2: 0.6, # 误检代价高的类别 } def filter_by_class(boxes, scores, labels): keep = [] for i in range(len(boxes)): cls = int(labels[i]) if scores[i] >= class_thres.get(cls, 0.5): keep.append(i) return keep逻辑说明:class_thres按类别给不同阈值,get的默认值 0.5 兜底。这个表要根据业务反馈调,上线后收集误检和漏检的 case,哪类问题多就动哪类的阈值。参数没有标准答案,关键是别用一刀切的 0.5。
5.2 可视化验证:把框画回原图
指标是数字,画出来才直观。下面这段代码把预测框和真值框画在同一张图上,一眼就能看出是漏检还是定位不准:
import cv2 def draw_boxes(img_path, pred_boxes, gt_boxes=None, save_path='vis.jpg'): img = cv2.imread(img_path) h, w = img.shape[:2] for box in pred_boxes: x1, y1, x2, y2 = [int(v) for v in box[:4]] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 绿色为预测 if gt_boxes is not None: for box in gt_boxes: x1, y1, x2, y2 = [int(v) for v in box[:4]] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) # 红色为真值 cv2.imwrite(save_path, img)逻辑说明:绿色画预测、红色画真值,重叠好的框会呈现黄绿色,偏了的框红绿分离,一眼能看出来。参数上2是线宽,图大就调粗。这个脚本我一般会在验证集里随机抽 20 张跑一遍,比看 mAP 表快得多,尤其是定位偏移这种指标反映不出来的问题。
从那以后我每次改完 Anchor 或损失权重,都强制走一遍「抽 20 张画框」的流程,指标涨了但框画出来不对的情况遇到过不止一次。希望帮到你。
本文还有配套的精品资源,点击获取