简介:这份资源是面向计算机视觉初学者与目标检测实践者的Faster R-CNN训练源码包,围绕「从头开始训练Faster R-CNN」这一案例,帮助读者理解两阶段检测器从数据准备到模型推理的完整流程。压缩包共6个文件,以4个Python脚本为主,辅以1个txt命令说明和1个md说明文档,整体约8KB,体量轻巧,便于快速阅读与本地复现。内容涉及RPN区域提议网络、锚点机制、RoI池化、分类与回归损失、非极大值抑制及mAP评估等关键知识点,并包含数据预处理、网络构建、训练与预测等模块化代码,可对照PASCAL VOC或COCO等数据集进行参数调优实验。目前已有821人学习下载,适合希望深入理解Faster R-CNN原理、并具备一定PyTorch或深度学习基础的开发者参考,为自动驾驶、监控系统等实际应用打下技术基础。
1. 从零训练 Faster R-CNN:这份 Python 源码包到底能不能直接跑
如果你手头有一批标注好的图片,想训练一个能框出目标并分类的检测器,又不想从 PyTorch 官方那套庞大工程里一层层扒代码,那这份「从头开始训练 Faster R-CNN」的 Python 源码包值得先看一眼。它把目标检测里最经典的两阶段结构拆成了几个独立脚本:tfannotation.py负责把 XML 标注转成统一格式,build_lisa_records.py把图片和标注打包成 TFRecord,lisa_config.py集中管理路径与超参,predict.py做推理可视化,command.txt记录训练命令。整套流程围绕 LISA 交通标志数据集设计,但脚本结构是通用的,换成自己的数据只需改配置和标注解析。适合已经装好 Python 环境、懂一点卷积网络、想亲手跑通 RPN 加 RoI 池化这条链路的人。它不依赖复杂框架封装,反而把数据管道和训练入口都摊开给你看,踩坑空间大,但学到的东西也扎实。
2. 环境与数据管道:把标注喂进网络之前要做的三件事
2.1 为什么选 TensorFlow 1.x 风格而不是直接上 PyTorch
这份源码的目录结构和脚本命名带有明显的 TensorFlow 1.x 时代特征:build_lisa_records.py生成 TFRecord,lisa_config.py里定义的是tf.app.flags风格的配置项,训练入口通常配合slim或原生tf.Session。这不是说它过时,而是说它的依赖边界很清晰——你不需要装 Detectron2 或 MMDetection,只要一个能跑 TensorFlow 的 Python 环境就能把整条链路走通。
常见做法是建一个独立虚拟环境,避免和系统里已有的 PyTorch 或新版 TensorFlow 冲突。我一般会这样处理:
python -m venv frcnn_env source frcnn_env/bin/activate # Windows 用 frcnn_env\Scripts\activate pip install tensorflow==1.15.0 pip install pillow lxml numpy matplotlib逻辑说明:TensorFlow 1.15 是最后一个支持 1.x 计算图模式的稳定版本,能兼容源码里可能出现的tf.placeholder、tf.Session等写法。lxml用于解析 XML 标注,pillow处理图片读写,matplotlib在predict.py里做结果可视化。参数上,如果你机器有 NVIDIA 显卡且 CUDA 版本匹配,可以换成tensorflow-gpu==1.15.0,但要注意 CUDA 10.0 和 cuDNN 7.4 的对应关系,版本错一位就是ImportError。
提示:如果安装 TensorFlow 1.15 时提示找不到匹配版本,先确认 Python 版本是否在 3.5 到 3.7 之间。3.8 以上装不了 1.15,这是最常见的翻车点。
2.2 标注格式转换:tfannotation.py 到底改了什么
目标检测的数据标注常见的有 PASCAL VOC 的 XML、COCO 的 JSON,以及 LabelImg 直接导出的 XML。tfannotation.py的作用是把这些 XML 里的bndbox坐标读出来,统一成[xmin, ymin, xmax, ymax, class_id]的列表,再写回一个中间文件供后续打包使用。
假设你的标注文件长这样:
<annotation> <filename>road_001.jpg</filename> <object> <name>speed_limit</name> <bndbox> <xmin>112</xmin><ymin>86</ymin> <xmax>203</xmax><ymax>175</ymax> </bndbox> </object> </annotation>tfannotation.py里通常会有一个类似下面的解析函数:
import xml.etree.ElementTree as ET def parse_xml(xml_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find('filename').text boxes = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_map: continue bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) boxes.append([xmin, ymin, xmax, ymax, class_map[cls_name]]) return filename, boxes逻辑说明:class_map是一个字典,把类别名映射成从 1 开始的整数,0 通常留给背景。findall('object')遍历所有目标,跳过不在类别表里的噪声标注。坐标转成 int 是因为后续生成 TFRecord 时需要固定长度特征,浮点数会带来对齐麻烦。参数上,如果你的数据集类别超过 10 个,建议把class_map单独写进lisa_config.py,不要硬编码在脚本里,否则换数据集时改到崩溃。
2.3 生成 TFRecord:build_lisa_records.py 的参数怎么调
TFRecord 是 TensorFlow 生态里读数据最快的方式,把图片二进制和标注打包成 protobuf 格式,训练时用tf.data.TFRecordDataset流式读取,不用每次从磁盘加载 JPEG。build_lisa_records.py干的就是这件事。
典型调用方式:
python build_lisa_records.py \ --data_dir=./LISA/train \ --output_path=./records/train.record \ --class_map=./lisa_config.py脚本内部会做几件事:遍历data_dir下所有图片,用tf.gfile.GFile读原始字节,把图片高度、宽度、通道数、编码格式(通常是'jpg'或'png')以及标注列表序列化成tf.train.Example。关键参数是--output_path,它决定 record 文件写到哪里,训练脚本里的train_input_reader必须指向同一个路径,否则会报NotFoundError。
我一般会额外加一个--shuffle标志,在写入前把文件列表打乱。因为 LISA 数据集按类别分文件夹存放,如果不打乱,训练时一个 batch 里全是同一类,BatchNorm 的统计量会偏得厉害,loss 震荡到怀疑人生。
注意:TFRecord 文件一旦生成,图片路径和标注就固定在里面了。如果后面改了类别映射或增删了图片,必须重新生成 record,不能只改配置文件。
3. 训练配置与 RPN 锚点:lisa_config.py 里哪些参数不能乱动
3.1 锚点尺度和宽高比:和你的目标尺寸强相关
Faster R-CNN 的核心是 RPN 在特征图上每个位置生成一组锚点,然后判断这些锚点里有没有目标、偏移量是多少。锚点的尺度和宽高比直接决定召回率。lisa_config.py里通常有类似这样的定义:
ANCHOR_SCALES = [8, 16, 32] ANCHOR_RATIOS = [0.5, 1.0, 2.0]这三个尺度对应特征图上 8×8、16×16、32×32 的窗口,经过骨干网络的下采样后映射回原图,实际覆盖范围会放大 16 倍左右(取决于 backbone 的 stride)。如果你的目标在图片里普遍很小,比如交通标志只占 30×30 像素,那ANCHOR_SCALES的最小值要往下调,否则 RPN 根本覆盖不到。
判断方法很简单:用predict.py加载一张训练图,看输出的候选框是不是集中在目标附近。如果框都偏大或者偏小,就改尺度。宽高比方面,交通标志接近正方形,1.0权重最高;如果是行人检测,0.5和2.0要保留,因为人体是瘦高型。
3.2 学习率和 batch size:显存不够时的取舍
lisa_config.py里一般会有LEARNING_RATE、BATCH_SIZE、MOMENTUM、WEIGHT_DECAY这几个优化器参数。Faster R-CNN 原论文用的是 SGD,学习率 0.001,momentum 0.9,weight decay 0.0005。这套参数在 batch size 为 1 或 2 的时候也能收敛,但需要把学习率再降一点。
LEARNING_RATE = 0.0005 BATCH_SIZE = 2 MOMENTUM = 0.9 WEIGHT_DECAY = 0.0005 MAX_ITER = 50000逻辑说明:BATCH_SIZE受显存限制,1080Ti 上跑 VGG16 骨干大概只能放 2 张图。如果显存更小,改成 1,同时把LEARNING_RATE降到 0.0001,否则梯度噪声太大,loss 会来回跳。MAX_ITER是训练总步数,不是 epoch 数,50000 步在 LISA 这种几千张图的数据集上大概相当于几十个 epoch,够用了。
提示:训练时如果看到
loss在前几百步一直不降,先检查build_lisa_records.py生成的 record 里标注数量对不对。用tf.python_io.tf_record_iterator读一条出来打印,比盲调学习率快得多。
3.3 训练入口和 command.txt 的用法
command.txt通常记录的是完整的训练命令,比如:
python train.py \ --train_dir=./models/frcnn_lisa \ --pipeline_config_path=./lisa_config.py \ --logtostderr但这份源码包里不一定有train.py,可能训练逻辑写在另一个脚本里,或者需要你根据lisa_config.py里的配置自己拼一个入口。常见做法是找 README.md 里提到的训练脚本名,如果没有,就搜tf.app.run或者slim.learning.train关键字,定位到实际执行训练的模块。
参数上,--train_dir是 checkpoint 和 event 文件的输出目录,训练中断后重新跑同一个命令会从最新 checkpoint 恢复。--pipeline_config_path指向配置文件,如果lisa_config.py是 Python 模块而不是 protobuf 文本,那训练脚本里应该用import lisa_config的方式加载,而不是当文件路径传。
4. 推理与可视化:predict.py 跑通之后怎么验证模型没白训
4.1 加载 checkpoint 并输出检测框
predict.py的职责是加载训练好的权重,对单张图片或一个文件夹做前向推理,然后画框、标类别、存图。典型流程是:构建图、恢复 checkpoint、读图、预处理、跑sess.run、取boxes、scores、classes、num_detections,最后用 PIL 或 matplotlib 画出来。
import numpy as np import tensorflow as tf from PIL import Image, ImageDraw def detect(sess, image_path, detection_graph): image = Image.open(image_path).convert('RGB') image_np = np.array(image) input_tensor = detection_graph.get_tensor_by_name('image_tensor:0') boxes = detection_graph.get_tensor_by_name('detection_boxes:0') scores = detection_graph.get_tensor_by_name('detection_scores:0') classes = detection_graph.get_tensor_by_name('detection_classes:0') num = detection_graph.get_tensor_by_name('num_detections:0') (boxes, scores, classes, num) = sess.run( [boxes, scores, classes, num], feed_dict={input_tensor: np.expand_dims(image_np, axis=0)}) draw = ImageDraw.Draw(image) h, w = image_np.shape[:2] for i in range(int(num[0])): if scores[0][i] < 0.5: continue ymin, xmin, ymax, xmax = boxes[0][i] draw.rectangle([xmin*w, ymin*h, xmax*w, ymax*h], outline='red', width=3) draw.text((xmin*w, ymin*h), str(int(classes[0][i])), fill='red') image.save('output.jpg')逻辑说明:detection_boxes返回的是归一化坐标[ymin, xmin, ymax, xmax],乘以宽高才是像素坐标。scores是置信度,阈值 0.5 是常用起点,如果漏检多就降到 0.3,误检多就升到 0.7。classes是类别索引,需要和lisa_config.py里的class_map反查才能显示可读标签。
4.2 非极大值抑制在源码里的位置
Faster R-CNN 输出几百个候选框,其中大量重叠。NMS 的作用是保留置信度最高的框,抑制和它 IoU 超过阈值的其他框。源码里 NMS 可能出现在两个地方:RPN 阶段筛候选框,以及最终检测阶段筛输出框。predict.py里如果直接用了tf.image.non_max_suppression,那阈值通常在 0.5 左右。
如果你发现输出图里同一个目标被框了好几次,先看 NMS 的 IoU 阈值是不是设太高了。常见做法是把它调到 0.4 到 0.6 之间,具体看目标密集程度。交通标志一般比较分散,0.5 够用;如果是密集行人,得降到 0.3 才能把重叠的人分开。
注意:NMS 是在 CPU 上跑的,如果候选框数量特别大(比如 RPN 输出两万个),推理速度会明显变慢。可以在 RPN 之后先按 score 排序取前 300 个,再做 NMS,这是标准做法。
4.3 用 mAP 判断训练是否收敛
光看 loss 曲线不够,目标检测的 loss 降了不代表框得准。predict.py只能看单张图,要量化评估得算 mAP。源码包里不一定带评估脚本,但你可以用build_lisa_records.py生成的验证集 record,跑一遍推理,把结果写成 COCO 格式的 JSON,再用pycocotools算 mAP。
常见做法是:对验证集每张图输出[image_id, category_id, bbox, score],bbox 格式是[x, y, width, height]的绝对坐标。然后:
from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt = COCO('annotations/instances_val.json') coco_dt = coco_gt.loadRes('detections.json') evaluator = COCOeval(coco_gt, coco_dt, 'bbox') evaluator.evaluate() evaluator.accumulate() evaluator.summarize()逻辑说明:COCOeval会输出 AP@0.5、AP@0.75、AP@[0.5:0.95] 等指标。如果 AP@0.5 能到 0.5 以上,说明模型基本可用;低于 0.2 就得回头查数据管道和锚点设置。参数上,loadRes要求检测结果的 JSON 里category_id和 ground truth 对齐,类别映射错一位,mAP 直接归零。
5. 避坑与排查:训练 Faster R-CNN 时最常见的五类翻车
5.1 现象:loss 变成 NaN,训练几步就崩
原因:学习率太大,或者 TFRecord 里有标注坐标超出图片边界,导致回归损失爆炸。LISA 数据集里偶尔有标注框的xmax大于图片宽度,生成 record 时没截断,训练时 RoI 池化取到非法区域。
解决:在tfannotation.py解析 XML 时加一行坐标裁剪,把xmin/xmax限制在[0, width],ymin/ymax限制在[0, height]。同时把LEARNING_RATE从 0.001 降到 0.0001,跑几百步看 loss 是否稳定。
5.2 现象:RPN 的 objectness loss 一直不降
原因:锚点和目标尺寸不匹配,或者正负样本比例失衡。默认情况下 RPN 采样 256 个锚点,正样本可能只有几个,大部分是背景,loss 被背景主导。
解决:检查lisa_config.py里的ANCHOR_SCALES是否覆盖了目标实际尺寸。用predict.py把 RPN 输出的候选框画出来,看有没有框在目标附近。如果全是背景框,把正样本的 IoU 阈值从 0.7 降到 0.5,让更多锚点被标为正。
5.3 现象:训练时显存溢出,batch size 降到 1 还是 OOM
原因:图片分辨率太高,或者骨干网络用了 VGG16 这种参数量大的结构。Faster R-CNN 在特征图上做 RoI 池化,特征图越大,显存占用越高。
解决:在build_lisa_records.py之前先把图片统一缩放到短边 600 像素,长边不超过 1000。如果还不行,把骨干换成 MobileNet,参数量少一个数量级,精度掉几个点但能跑起来。
5.4 现象:predict.py 报KeyError: 'detection_boxes'
原因:加载的 checkpoint 和当前图结构不匹配,或者predict.py里用的 tensor 名字和训练时定义的不一样。TensorFlow 1.x 的 tensor 名字依赖变量作用域,改过网络结构后旧 checkpoint 就废了。
解决:用tf.train.list_variables(checkpoint_path)打印 checkpoint 里的变量名,和predict.py里get_tensor_by_name的名字对比。如果不一致,要么改推理脚本的名字,要么重新训练。
5.5 现象:mAP 算出来是 0,但可视化明明有框
原因:检测结果 JSON 里的坐标格式写错了。COCO 要求[x, y, width, height],而predict.py输出的是[ymin, xmin, ymax, xmax]归一化坐标,没转换就直接写 JSON,评估时框全在图片外面。
解决:写 JSON 前做两步转换:先乘宽高变绝对坐标,再把[ymin, xmin, ymax, xmax]转成[xmin, ymin, xmax-xmin, ymax-ymin]。转换完随便挑一张图,用matplotlib画出来和原图对比,确认框的位置对得上。
6. 进阶技巧:把训练好的 Faster R-CNN 导出成冻结图并验证输入输出
训练完拿到 checkpoint 只是第一步,实际部署时更常用的是冻结图(frozen graph),把变量固化成常量,推理时不需要再恢复 checkpoint,加载速度快,也方便跨平台。这份源码包里不一定带导出脚本,但你可以自己写一个,核心是tf.graph_util.convert_variables_to_constants。
import tensorflow as tf from tensorflow.python.framework import graph_util def freeze_graph(checkpoint_path, output_node_names, output_graph): saver = tf.train.import_meta_graph(checkpoint_path + '.meta', clear_devices=True) with tf.Session() as sess: saver.restore(sess, checkpoint_path) output_graph_def = graph_util.convert_variables_to_constants( sess, sess.graph.as_graph_def(), output_node_names.split(',') ) with tf.gfile.GFile(output_graph, 'wb') as f: f.write(output_graph_def.SerializeToString()) print('frozen graph saved to', output_graph) freeze_graph('./models/frcnn_lisa/model.ckpt-50000', 'detection_boxes,detection_scores,detection_classes,num_detections', './frozen/frcnn_lisa.pb')逻辑说明:output_node_names是要保留的输出节点,逗号分隔。convert_variables_to_constants会把图中所有Variable替换成Const,同时裁剪掉和输出无关的分支。参数上,checkpoint_path不要带.meta后缀,但函数内部会自动找.meta文件;output_graph是生成的.pb文件路径,建议放在独立目录,避免和 checkpoint 混在一起。
导出之后,验证冻结图能不能用:
import cv2 import numpy as np import tensorflow as tf def load_frozen_graph(pb_path): with tf.gfile.GFile(pb_path, 'rb') as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) with tf.Graph().as_default() as graph: tf.import_graph_def(graph_def, name='') return graph graph = load_frozen_graph('./frozen/frcnn_lisa.pb') with tf.Session(graph=graph) as sess: input_tensor = graph.get_tensor_by_name('image_tensor:0') boxes = graph.get_tensor_by_name('detection_boxes:0') scores = graph.get_tensor_by_name('detection_scores:0') img = cv2.imread('test.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) out_boxes, out_scores = sess.run([boxes, scores], feed_dict={input_tensor: np.expand_dims(img, axis=0)}) print('top score:', out_scores[0][0])逻辑说明:tf.import_graph_def把.pb文件里的图定义导入当前默认图,name=''表示不加前缀,这样 tensor 名字和训练时一致。image_tensor的输入形状是[1, None, None, 3],所以expand_dims加 batch 维。如果top score低于 0.5,说明冻结图有问题,常见原因是导出时output_node_names写错,导致输出节点被裁掉了。
我自己的习惯是:每次导出冻结图后,一定拿同一张测试图分别跑一遍 checkpoint 和.pb,对比detection_boxes的前五个值。如果完全一致,说明导出成功;如果有偏差,多半是预处理不一致,比如 checkpoint 推理时做了归一化而冻结图验证时忘了。从那以后我每次导出都强制走一遍这个对比,省得部署到一半才发现框全偏了。希望帮到你。
本文还有配套的精品资源,点击获取