从零训练Faster R-CNN:Python源码实战与避坑指南

📅 发布时间:2026/9/28 7:24:33
从零训练Faster R-CNN:Python源码实战与避坑指南
简介这份资源是面向计算机视觉初学者与目标检测实践者的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把图片和标注打包成 TFRecordlisa_config.py集中管理路径与超参predict.py做推理可视化command.txt记录训练命令。整套流程围绕 LISA 交通标志数据集设计但脚本结构是通用的换成自己的数据只需改配置和标注解析。适合已经装好 Python 环境、懂一点卷积网络、想亲手跑通 RPN 加 RoI 池化这条链路的人。它不依赖复杂框架封装反而把数据管道和训练入口都摊开给你看踩坑空间大但学到的东西也扎实。2. 环境与数据管道把标注喂进网络之前要做的三件事2.1 为什么选 TensorFlow 1.x 风格而不是直接上 PyTorch这份源码的目录结构和脚本命名带有明显的 TensorFlow 1.x 时代特征build_lisa_records.py生成 TFRecordlisa_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 tensorflow1.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-gpu1.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 filenameroad_001.jpg/filename object namespeed_limit/name bndbox xmin112/xminymin86/ymin xmax203/xmaxymax175/ymax /bndbox /object /annotationtfannotation.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 生成 TFRecordbuild_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.001momentum 0.9weight 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, axis0)}) 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], outlinered, width3) draw.text((xmin*w, ymin*h), str(int(classes[0][i])), fillred) 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会输出 AP0.5、AP0.75、AP[0.5:0.95] 等指标。如果 AP0.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_devicesTrue) 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(graphgraph) 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, axis0)}) 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 推理时做了归一化而冻结图验证时忘了。从那以后我每次导出都强制走一遍这个对比省得部署到一半才发现框全偏了。希望帮到你。本文还有配套的精品资源点击获取