ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从头训练Faster R-CNN:环境配置、RPN原理与调参避坑指南

从头训练Faster R-CNN:环境配置、RPN原理与调参避坑指南 简介面向计算机视觉初学者的Faster R-CNN从零训练Python源码包源自案例89以代码驱动方式帮助开发者深入理解目标检测经典模型的工作原理从数据准备、网络构建到训练评估逐层拆解。压缩包共6个文件以4个Python脚本为核心覆盖数据预处理、模型定义、预测等环节另配1个命令说明txt和1个Markdown说明文档整体仅8KB体量轻巧却保留完整主线逻辑便于快速定位关键实现。目前已有821人浏览学习适合具备一定深度学习基础、正在学习目标检测与Faster R-CNN的开发者作为参考。源码重点演示RPN候选框生成、锚点机制、RoI池化以及两阶段分类回归损失的计算路径结合LISA数据集构建与推理脚本可辅助掌握各模块的实际写法与调参思路同时通过阅读代码还能理解特征共享、锚点覆盖策略与非极大值抑制等实现细节为后续在自动驾驶、工业检测等场景应用打下基础。1. 为什么要从头训练一个过时的Faster R-CNN在YOLO已经出到v8、v9的今天回头从零训练Faster R-CNN看起来像在考古。但如果你要处理的任务不是常规目标检测比如检测细长物体、密集小目标或者需要在自定义数据集上稳定收敛Faster R-CNN依然是很多工业方案的底座。更重要的是它的两阶段结构——RPNRegion Proposal Network加ROI Head——是理解几乎所有现代检测器的基础。我见过不少一开始就玩YOLO的同事遇到RPN相关的问题时一脸懵反过来从Faster R-CNN入手的人看什么模型都快。这篇笔记就围绕下载到一个名为从头开始训练Faster R-CNN-python源码.zip的压缩包之后怎么把训练跑起来、参数怎么调、坑在哪来展开。这里的从头开始有两层含义一是不用现成的模型文件做迁移学习从随机初始化开始训二是你手里可能只有一个源码包没有现成的数据配好需要从准备数据集那一步走起。读者如果是刚入目标检测的新手按步骤能跑通全流程如果已经跑过YOLO或者其他检测器这篇文章的调参和排错部分值得细看。2. 训练前要把环境、数据和源码结构攒明白2.1 用Python 3.6 PyTorch 1.x打造一个能跑的底座Faster R-CNN的源码包有很多个版本网上流传最广的通常是基于PyTorch的复现代码结构大致继承自jwyang的faster-rcnn.pytorch。这类源码对Python版本很敏感不是越新越好。我用的是Python 3.6加PyTorch 1.2左右的组合太新的PyTorch反而会因为API改动跑不起来。conda create -n fasterrcnn python3.6 conda activate fasterrcnn pip install torch1.2.0 torchvision0.4.0 pip install cython python-opencv easydict matplotlib这里有几个关键点Python 3.6是为了兼容源码里大量使用的f-string和旧的numpy语法PyTorch 1.2左右是因为这个时期的api比如torchvision.ops还没大改源码里自定义的ROI Pooling层能直接编译过。easydict是源码里读取配置用的没有它会直接报ImportError。装完这些以后如果你的显卡是30系之后的新卡建议直接放弃这套旧代码去用mmdetection的实现否则Pytorch版本和CUDA的匹配会让人想砸电脑。2.2 数据集先按VOC格式整理再做小样本冒烟测试不论你手里是什么数据Faster R-CNN的常见训练接口都期望VOC格式。这里说的VOC格式不是非得用PASCAL VOC数据集而是目录结构照搬VOC的布局。data/ ├── VOCdevkit2007/ │ └── VOC2007/ │ ├── Annotations/ # 存放xml标注文件 │ ├── JPEGImages/ # 存放jpg图片 │ ├── ImageSets/ │ │ └── Main/ # train.txt, val.txt等划分文件 └── pretrained/ # 预训练模型目录我第一次跑这个源码的时候直接在完整数据集上开始训练跑了两个小时loss纹丝不动。后来学乖了先复制20张图、20个xml放到上述目录强行过完一个epoch确认loss在下降才去碰全量数据。这个习惯一直保留到现在很管用。Anchor机制、ROI Pooling、NMS这些概念在这一步不需要想得太透先跑通再回头看代码。2.3 解读源码包五分钟版五个文件决定成败解压源码包以后先别急着装依赖。先把下面文件打开瞄一眼用不了五分钟后面少踩一半坑。文件路径作用改动频率lib/model/config.py全局配置anchor大小、nms阈值都在这里经常改lib/datasets/pascal_voc.py数据集加载逻辑类别名列表在这里换数据必改trainval_net.py训练入口batch size、lr都在命令行传每次训练都改lib/model/faster_rcnn.py模型核心逻辑两阶段forward都在这里很少改lib/model/rpn/*RPN的anchor生成和proposal逻辑调anchor才改从头开始训练在这个源码包里有个特殊含义你需要修改pascal_voc.py里的self._classes把默认的20类改成自己的类别。这个改动直接影响数据加载器对xml的解析不改的话训练会在第1000次迭代左右报keyError那是所有坑里最容易定位的一个。改完以后记得同时把config.py里的num_classes改成类别数加1这个加1是给背景类留的位置。3. 正式开训前必须搞懂的RPN和ROI Head3.1 为什么说RPN是两阶段检测器和单阶段的分水岭单阶段检测器如YOLO直接回归bbox和类别两阶段检测器则先让RPN快速筛选出可能是物体的候选框再做精分类和精回归。RPN的本质是在特征图上滑动每个位置生成9个anchor3种尺度乘以3种长宽比。源码里生成anchor的核心逻辑在lib/model/rpn/generate_anchor.py代码很短但值得看懂。# 在特征图的每个位置生成9个anchor def generate_anchors(base_size16, ratios[0.5, 1, 2], scales8*2**np.arange(3)): # base_size是特征图感受野对应的原图尺寸 # ratios是长宽比控制框的扁宽程度 # scales是面积缩放8、16、32三种 base_anchor np.array([1, 1, base_size, base_size]) - 1 ratio_anchors _ratio_enum(base_anchor, ratios) anchors np.vstack([_scale_enum(ratio, scales) for ratio in ratio_anchors]) return anchors.astype(np.float32)这段代码做的事情很直白先按比例把基准框拉宽或拉高再对每种比例做尺度缩放最后得到9个候选框。参数的含义值得细说base_size16意味着特征图上每个点对应原图16x16的区域这是VGG16做了4次池化的结果scales默认是8、16、32乘2也就是对应原图16x16、32x32、64x64。如果你的目标物体偏小比如遥感图里的汽车只有十几个像素这组参数得往小了调不然RPN根本生成不了足够小的框。3.2 RPN训练时的正负样本分配策略RPN不是直接拿所有anchor算损失而是先做正负样本分配。源码里的规则很简单和某个ground truth的IoU超过0.7的anchor算正样本低于0.3的算负样本介于中间的直接忽略不进入损失计算。每张图随机选256个anchor作为训练样本正负样本各占一半。# 从rpn层的loss计算中摘出的核心逻辑 for i in range(num_images): # 计算所有anchor和所有gt的iou得到矩阵形状为[num_anchors, num_gt] ious bbox_overlaps(anchors, gt_boxes) # 每个anchor取和它iou最大的gt的iou值 max_iou_each_anchor ious.max(dim1)[0] # iou大于阈值的标记为正样本 anchor_label[max_iou_each_anchor 0.7] 1 # iou小于阈值的标记为负样本 anchor_label[max_iou_each_anchor 0.3] 0看这个代码会明白一个训练要点正样本数量不够是Faster R-CNN在小数据集上不收敛的第一大原因。如果你的数据里目标普遍很小或者很稀疏0.7的IoU阈值可能让正样本数量非常少这时候需要在config.py里把RPN_POSITIVE_OVERLAP从0.7下调到0.5左右或者把RPN_BATCHSIZE从256下调到128保证每次迭代至少有几个正样本参与训练。3.3 ROI Head的分辨率和全连接层通过RPN选出大概2000个proposal之后ROI Head要把这些尺寸不一的框统一成固定尺寸的特征图。源码里用的是ROI Pooling把任意大小的ROI区域划分成7x7的网格对每个网格做最大池化。# 核心调用把roi区域映射到特征图并池化到7x7 rois torch.from_numpy(rois).float().cuda() pools self.RCNN_roi_pool(features, rois.view(-1, 5)) # 然后flatten后过两个全连接层 pools pools.view(pools.size(0), -1) fc7 self.RCNN_top(pools)注意roi_pool的输入rois.view(-1, 5)前一个数字是proposal数量后面5列的内容依次是batch索引加4个坐标。ROI Pooling是空间量化操作会有取整误差后来被ROI Align替代就是这个原因。在训练的时候proposal数量被限制为128个用于ROI Head的训练正负样本各占一部分。预测阶段会用全部2000个proposal做检测但输出前套一层NMS把高度重叠的框合并掉NMS阈值默认是0.7检测结果太密就把它调低到0.5检测框过于稀疏就调高。4. 跑通训练全流程从命令行到日志解读4.1 准备预训练权重但不是从零开始的地方从头开始训练在这个源码包里是可以从一张白纸开始的但以我的经验完全不加载预训练权重来训练RPN和ROI Head联合训练的模型收敛是真的慢而且结果通常不如载入在ImageNet上预训练过的特征提取网络。正确的打开方式是模型加载在ImageNet上预训练的VGG16或ResNet101的backbone权重而RPN和ROI Head的层从头开始训练。# 训练入口示例使用ResNet101作为backbone加载ImageNet预训练权重 python trainval_net.py \ --dataset pascal_voc \ --net res101 \ --bs 4 \ --nw 4 \ --lr 0.001 \ --lr_decay_step 8 \ --epochs 20 \ --cuda关于参数--bs是batch size建议从4开始显存不够再减到2或者1--nw是加载数据的worker进程数不是越大越好设成4基本够用Windows上设大了容易报内存错误--lr是初始学习率0.001是针对batch size 4的经验值如果你的显卡只跑得起batch size 1学习率要跟着降到0.00025左右不然loss大概率飞掉--lr_decay_step是学习率衰减的轮次间隔默认8个epoch衰减一次如果数据集小收敛快改成5可能更合适。4.2 训练日志里哪些数据值得看训练跑起来之后终端会刷出一行行的loss信息不要只盯着总loss看。解析日志要关注的是这些字段Epoch: [0][0/250] Loss: 1.8942 | RPN_Loss: 0.2312 | RCNN_Loss: 1.6630 | Time: 0.23sRPN_Loss和RCNN_Loss是分开的它们各自下降的趋势更有参考价值。RPN_Loss长期不动或者反向上升基本可以断定是anchor参数和你的数据分布不匹配先去查正负样本数量。RCNN_Loss下不来问题多半出在ROI Head的全连接层收敛太慢把初始学习率乘以0.5试试。总的来看前3个epoch loss波动是正常现象如果到第5个epoch还在波动没有任何下降趋势说明学习率设置高了。4.3 训练中断后怎么办断点续训的参数要改对训练到一半掉线或者显存溢出导致进程被杀是家常便饭。好在源码支持从checkpoint恢复前提是你在训练启动时加上了--save_dir参数保存模型的目录里有上一次训练的最近一个epoch的模型文件。恢复训练不是重新执行原命令必须带上对应的参数否则模型虽然加载了但优化器状态和学习率计划重置了效果大打折扣。# 从第7个epoch的checkpoint恢复训练 python trainval_net.py \ --dataset pascal_voc \ --net res101 \ --bs 4 \ --nw 4 \ --lr 0.0005 \ --lr_decay_step 8 \ --epochs 20 \ --resume \ --checkpoint 7 \ --cuda这里的--checkpoint 7表示加载第7个epoch保存的模型同时优化器也会恢复到那个时刻的状态。一个常见的坑是恢复训练时忘了同步修改学习率。如果原训练在第8个epoch衰减了恢复后手动指定的--lr应该小于0.001否则相当于学习率跳回了初始值可能出现loss突然升高看着像是模型退化了实际是优化器回到了比较激进的状态。5. 训练避坑手册四类高频翻车场景5.1 环境报错module numpy has no attribute bool在实际训练中这条报错出现频率极高。现象就是训练脚本一运行就报错堆栈指向lib/model/nms_wrapper.py或者lib/model/rpn/bbox_transform.py的某一行用了np.bool或者np.int。原因是numpy从1.24版本开始删除了np.bool和np.int这些别名而Faster R-CNN的源码大多写于PyTorch 0.4时代那时候numpy版本还停留在1.16左右。解决方式有两个要么把numpy降级到1.17版本要么在源码里把所有np.bool和np.int全局替换为bool和int。我建议用第二种方式因为降级numpy可能会连带影响其他依赖包。替换完后记得删除lib/build目录下的缓存文件再重新编译不然改了半天还是报同样的错误。5.2 显存不足OOM关键不在于batch sizebatch size设成1还是炸显存这是几乎每个用这套源码的人都会遇到的困惑。通常原因不是batch size而是图片在送入网络之前被等比例缩放到短边600像素、长边1000像素的上限。源码里的config.py有一段逻辑会把所有图片scale到600到1000这个范围如果你的显卡只有6G显存ResNet101加batch size 1依然可能爆掉。解决办法分两步第一步在config.py里把MAX_SIZE和SCALES调低一些例如SCALES(400,)MAX_SIZE600会让训练速度明显变快第二步如果显存还是不够把ROI Head的POOLING_SIZE从7改成5这个参数对精度影响在几个点以内但显存占用能降下来不少。需要注意的是修改SCALES会影响feature map的分辨率后面的anchorbase_size需要按比例跟着改。5.3 训练了十几个epochloss不降或者直接nan如果loss从一开始就不下降第一个要查的是数据集的标注是否有问题特别是xml标注的xmin和ymin是否出现过负数或者大于图像宽高的情况bbox回归对异常值是高度敏感的。建议先用一段脚本遍历所有xml打印出所有非法box的图片名然后用标注工具重新修正那几张图。如果loss在正常下降的途中突然变成nan大概率是学习率过大导致梯度爆炸。在源码里加一行梯度裁剪就能起到立竿见影的效果代码如下# 在trainval_net.py的backward之后加入梯度裁剪 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0) optimizer.step()梯度裁剪的上限10.0是一个经验值如果裁了之后loss还是偶尔爆炸继续往下降直到5.0或者3.0。还有一种可能是backbone的预训练权重加载失败权重是随机初始化的导致早期特征极端不稳定。排查方式是打印模型加载前后的部分权重数值如果变化幅度超过一个数量级说明预训练权重路径配错了或者load_state_dict时没有用strictFalse。5.4 检测结果全是一个个密集的小框训练跑完了测试出来的图上一堆密密麻麻的框甚至一个人身上能有几十个框。这几乎可以断定是NMS阈值设置的问题。源码里预测阶段的NMS默认阈值是0.7意味着两个IoU小于0.7的框都会被保留在密集场景下就会出现大量重叠框。把config.py里的TEST_NMS_THRESH从0.7改成0.4或者0.3能让输出框干净很多。这个改动会顺带影响检测的召回率如果主要目标是小物体宁可接受多一点重复框也别把阈值压得太低。另外检查一下TEST_RPN_NMS_THRESH这个RPN阶段的NMS阈值它的默认值0.7在提案阶段造成的重复框叠加问题也很常见降到0.6对最终结果帮助很大。6. 从训练到可用测试评估、可视化与模型选择的进阶习惯训练完成后优先关心模型在验证集上的表现。先跑一遍测试脚本拿到mAP这个数值是从头训练是否成功的第一步衡量标准。# 测试入口注意--checkpoint和训练时保持同一路径 python test_net.py \ --dataset pascal_voc \ --net res101 \ --checkpoint 20 \ --cuda这里有个容易被忽略的地方test_net.py默认使用的batch size是1图片的预处理方式要和训练时保持一致。如果训练时你改了SCALES测试时也要用同一份config.py否则特征分布不匹配mAP会掉很多。我刚跑通这套流程的时候犯过一个很蠢的错误就是只盯着mAP数字看完全不知道模型到底把哪些图搞错了。后来养成了一个习惯每次训练完必须做两件事第一件事是把验证集上的误检图按类别挑出来打印成网格图第二件事是在visdom里把训练时的loss曲线和当前epoch的RPN正负样本比例一起可视化。这两个操作说不上高级但对于快速定位问题是效率最高的。特别是RPN正负样本比例我见过很多mAP不高的情况根源就是某个类别的目标尺寸过小导致正样本数量长期为0这个只能通过可视化anchor和gt的匹配情况才能看到单纯看loss曲线是看不出来的。另一个值得养成的习惯是保存模型时不要只留最后一个epoch。源码的--save_dir会保留每个epoch的模型磁盘空间够的话建议全部保留。很多情况下第16个epoch的模型会比第20个好用因为最后的lr衰减可能已经在训练集上过拟合了。选模型不能只看训练loss我会在验证集上跑mAP专门选mAP最高的那个checkpoint用来部署而不是默认用最后一个。最后从模型选择上给出一个总体判断如果你只跑过一次Faster R-CNN的训练做完一次全流程的BN和数值稳定性处理整个过程会让你对检测器的理解比直接调YOLO要深得多。但是如果你已经确认任务只需要高吞吐而精度要求不高Faster R-CNN的推理速度可能不适合你在两阶段模型里跑一次NMS的时间YOLO已经推理完一张图了。反过来不追求速度、对检测精度和可控性有要求的场景比如工业缺陷检测、医学影像中的病灶识别Faster R-CNN从头训练这套流程依然是值得投入的方向。半年的血泪经验告诉我先把数据整得明明白白比模型选型重要十倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表