☰
Faster R-CNN PyTorch 1.0复现调试全记录:从环境配置到训练推理避坑指南
2026/10/16 6:15:40 网站建设 项目流程

学目标检测的人,应该有很长一段时间都在跟Faster R-CNN打交道。以前我用Caffe版本跑通了一套检测流程,后来导师让我把整套东西迁移到PyTorch上,我就选了当时比较稳定的Faster RCNN pytorch 1.0版本作为基础,在复现和调参的过程中,前前后后踩了将近一个月的坑。这篇文章就是一次完整的Faster RCNN pytorch 1.0调试过程记录,把我遇到的环境配置、代码实现、训练复现、推理验证这些环节里的问题全部摊开来讲,尽量让后来的人少走一点弯路。

我默认看这篇记录的人已经对深度学习和卷积网络有基本了解,但如果只是刚开始接触检测方向,也不用怕,我会把涉及到的原理和参数用白话再解释一遍。整个调试过程我分成了几个大块来回顾:环境搭建、核心模块的代码核对、训练阶段的异常排查、推理阶段的表现分析。每个部分里我都会附上自己实际的报错信息、当时的排查逻辑和最终解决办法,有些坑现在看起来很简单,但当时确实花了很久才定位到根因。

1. 动手调试前,先把这个模型的全链路想清楚

1.1 Faster R-CNN并不是一个黑盒:完整数据流梳理

Faster R-CNN是典型的二阶段检测器,整个数据流可以拆成四段:特征提取、候选框生成、区域特征聚合、分类回归。我当时调试的前提条件是采用ResNet-50作为骨干网络,输入图像先被resize到较短边800像素、较长边不超过1333像素,然后经过骨干网络下采样,最终得到相对于原图1/16尺寸的特征图。这个特征图就是RPN和后续检测头的共同输入。

重点说一下RPN这层,它是Faster R-CNN最核心的模块。RPN在特征图每个位置生成一组不同尺度和长宽比的anchor,常见的配置是3种尺度(128、256、512)和3种长宽比(0.5、1.0、2.0),所以每个位置有9个anchor。对于一张800x1333的输入,经过16倍下采样之后特征图是50x83,那么总的anchor数量就是50x83x9,大概三万七千个左右。这个数量级在GPU上做IoU计算是完全没问题的,但如果你在CPU上尝试调试,就会明显感受到瓶颈,我第一次在只有8G内存的机器上跑前向,直接卡了四五分钟,后来才发现问题不在模型本身,而是我用了过多的CPU进程去算IoU。

从RPN出来之后,模型需要从三万个anchor中筛选出大小合适的候选框,然后送入ROI Pooling层。在PyTorch 1.0版本的常见实现中,这里有几种选择,可以直接调用torchvision.ops里的RoIPool,也可以自己写,甚至有人直接用ROI Align。我在调试中发现,很多旧代码仓库使用的是自己实现的ROI Pooling,接口非常奇怪,输入是feature map和roi boxes拼接后的tensor,非常容易在维度上踩坑。我建议初学者在看到这类代码时,先花半小时把整个forward的数据维度变化写在纸上,不要直接硬跑,不然报错会让你怀疑人生。

1.2 PyTorch 1.0这个版本节点的特殊性

PyTorch 1.0是PyTorch进入稳定版的标志性版本,2018年底发布的,和之前的0.x系列相比,它把原先分散的模块整合起来,引入了torch.jit和更稳定的分布式接口。但问题也随之而来,很多在0.4、0.3上能跑的代码到了1.0就会报错,因为很多API的调用方式变了,比如Variable、Tensor的合并,还有一些函数从torch.nn.functional迁移到了torchvision.ops。

我用的Faster RCNN pytorch 1.0版仓库是当时很流行的几个开源版本之一,但这个版本本身基于的PyTorch版本并不是纯粹的1.0,它可能用了一些更早期的API写法。我在初次运行的时候,光是import就报了好几个AttributeError,比如torch.Tensor.new_tensor这类方法在某些版本里面根本不存在。所以说,调试这个项目的第一步,不是改模型,而是把代码和当前的框架版本做一次全面适配,搞清楚哪些是框架原生API,哪些是仓库自己封装的方法。

2. 环境配置阶段的那些坑:版本搭配比想象中更致命

2.1 CUDA、cuDNN、PyTorch、torchvision的对应关系

如果你在配置环境的时候随便装,那大概率会遇到“非法指令”“找不到CUDA driver”“段错误”这类问题。我的第一台调试机装的是CUDA 11.2和cuDNN 8.1,然后强行把PyTorch 1.0对应的代码用PyTorch 1.7的环境跑,结果一进forward就报错,后来发现是部分算子不兼容。

这里我必须整理一个我自己实测过相对稳定的组合,供参考:

组件版本搭配建议
Python3.7
PyTorch1.0.0(或1.1.0)
torchvision0.2.1(或0.3.0)
CUDA9.2(或10.0)
cuDNN7.4及以上
依赖库numpy 1.16、opencv-python 3.4、matplotlib 3.1

我说的“相对稳定”不代表完全没有坑,比如CUDA 9.2和cuDNN 7.6.5的组合,在我的机器上可以正常运行,但换到另一台相同型号的机器上就会出现随机性的段错误。后来查到是cuDNN的缓存机制在某些算子上有问题,解决办法是把环境变量TORCH_CUDA_ARCH_LIST设置成和你GPU算力一致的版本,再重新编译。比如我的显卡算力是7.5,就设置成7.5,很多时候这个设置能解决莫名其妙的报错。

2.2 搭建可复现环境的具体步骤

由于这个项目太老,我建议不要直接用最新版的conda去创建环境,而是用requirements.txt锁定版本。先把Python环境建好:

conda create -n frcnn python=3.7 conda activate frcnn pip install torch==1.0.0 torchvision==0.2.1 pip install numpy==1.16 opencv-python==3.4.2.17 matplotlib==3.1.0 pillow==7.0.0

这里有一个小坑,PyTorch 1.0的安装包在pip源里的依赖很严格,如果提前装好了numpy 1.20,那安装torch的时候会被自动升级,导致版本冲突。所以我的经验是先创建一个全新的环境,然后一次性把torch和torchvision装上,再装别的依赖,不要分批次操作,不然很容易出现依赖错乱。

编译ROI模块这一步也很关键,老版本仓库里通常都有一个setup.py,需要先执行:

python setup.py build develop

这个命令的作用是把项目里的C++/CUDA扩展编译好,并链接到当前Python环境中。我遇到最多的问题是在这一步缺少gcc或者CUDA toolkit没有安装完整,导致编译直接失败。常见的报错是找不到cuda_runtime.h,这时候就要检查一下你的CUDA_HOME环境变量是不是正确指向了CUDA安装目录。

2.3 配置阶段三个隐蔽错误

第一个隐蔽错误是设错了torchvision的版本,很多人使用pip install torchvision自动装,结果装上的是新版0.5,和新版的PyTorch可以互通,但和1.0的接口不匹配,一段代码都跑不起来。第二个是忽略了下采样倍率的定义,有些仓库使用的骨干网络是VGG16,不是ResNet,它的下采样倍率是1/16,但如果换成其他网络,就必须修改RPN和ROI相关的空间比例参数,不修改的话anchor位置全歪,训练也不收敛。第三个是路径配置问题,数据集路径、预训练权重路径里面的中文目录或者特殊符号会导致读取失败,我在一个同事的机器上见过因为Windows用户名带了中文字符,导致整个数据加载崩溃的情况。

3. 代码实现里的核心模块与高频报错点

3.1 RPN层的anchor生成与标签分配逻辑

在我这个调试版本中,RPN的anchor生成是在一个独立的py文件里写的,使用base_size=16、ratios=[0.5, 1, 2]、scales=[8, 16, 32]。这里注意,scales的值和我在前面说的128、256、512其实用的是不同的表达方式,前者表示在基础框上的乘数,后者是实际边长,换算关系是128/16=8,256/16=16,512/16=32。很多新手在这里会对不上,然后设置调试断点看anchor的形状是没问题的,但和GT计算IoU的时候,坐标偏移却完全对不上,这就是scale表达方式没弄清楚。

在RPN进行标签分配时,需要给每个anchor打上正样本、负样本和忽略样本的标记。正样本的判定条件是IoU大于0.7,或者是该anchor与某个GT的IoU最大,负样本是IoU小于0.3,介于中间的忽略。之后再从正负样本中采样,保证一个batch里正负样本数量总和为256,如果正样本不足256,就用负样本补足。

实际调试中,我发现很多仓库在实现“某个anchor与GT的IoU最大”这一逻辑时,是用了两个argmax来做的,一个沿着anchor维度找每个GT的最大IoU索引,一个沿着GT维度找每个anchor的最大IoU。如果这两个argmax的顺序写反了,就会导致大量正样本丢失,训练时RPN的loss会一直下降得很慢,最终的proposal质量很差。

3.2 特征图尺寸对齐与ROI Pooling的维度地狱

ROI Pooling是整个模型中最容易出维度错误的地方。我当时用的Polygon库版本,ROI Pooling的输入是特征图加上所有ROI的坐标,输出为固定尺寸(例如7x7)的特征。这里有个关键问题,ROI坐标是基于原始图像尺寸的还是基于特征图尺寸的,两个仓库可能采用完全不同的约定。

我在这个项目里遇到过一次运行时错误RuntimeError: invalid argument 2: size ‘7 x 7 x 512’ is invalid for input with 10240 elements,这个报错让我一度以为batch size设置错了,后来查了很久才发现是把一个batch的ROI数量当成了单个图像的ROI数量,导致维度变成了512倍,正确做法是把这个维度在forward之前先展开。

还有一点是ROI Pooling的spatial_scale参数,这个参数表示原图坐标到特征图坐标的缩放因子。如果用1/16下采样的骨干网络,这个值就是0.0625。有次我把这个值写成了1.0,跑起来没有报错,但是训练出来的模型检测框位置完全不对,变成了所有框都集中在图像的左上角。从这里我学到一句话:不报错的错误往往比报错的错误更可怕,坐标变换类的bug必须用可视化验证。

3.3 边界框回归的编码与解码

目标检测最后回归的不是边界框的绝对坐标,而是相对于anchor的偏移量,这种编码方式在PyTorch实现中有非常明确的公式:

tx = (x - xa) / wa ty = (y - ya) / ha tw = log(w / wa) th = log(h / ha)

公式里x、y分别代表GT框中心点坐标,xa、ya、wa、ha是anchor的坐标和尺寸。回归网络的输出就是对这四个偏移量的预测,预测之后需要经过解码得到最终的预测框。我在调试中发现,解码部分的实现最容易出现的问题是坐标变换后没有做边界截断,导致预测框超出图像范围,在训练时不明显,但在测试阶段会造成mAP异常低。

建议大家在调试时打印一下预测框解码前后的坐标范围,一般如果解码前的tx在-1到1之间,解码后的框应该不会跑出图像太远,如果出现几十甚至上百的坐标偏移,基本可以确定是编码公式写错了。我调试过程中还踩过一个很隐蔽的坑,就是网络的回归输出没有经过sigmoid或者tanh限制,导致tw无限大,解码之后框的宽高爆炸,loss直接为NaN。

3.4 损失函数与样本采样的隐藏问题

Faster R-CNN包含两个损失:RPN的分类和回归损失,以及RCNN的分类和回归损失。常见的实现是分类用交叉熵,回归用smooth L1。我在代码里排查的时候发现,很多实现会在计算RCNN回归loss时,只对正样本进行计算,而负样本的回归值直接忽略,这一点一般不会写错,但容易出现的问题是正样本数量为0时,回归损失分母为0,导致除零错误。

这个问题在训练前期尤为常见,因为随机初始化的RPN生成的proposal质量很差,可能一个正样本都没有。稳妥的做法是在计算损失之前判断正样本数量,如果为0就返回一个零损失。我当时加了这样一个分支之后,训练稳定了很多,虽然看起来只是几行代码,但解决了一个非常核心的稳定性问题。这里也提醒一点,损失函数是所有梯度汇聚的终点,任何一个分支的不稳定都会直接影响整个模型,调试的时候一定要保持“最小化影响原则”,改一处、测一处。

4. 训练过程:那些让人想砸电脑的异常排查

4.1 loss不下降、直接爆成NaN,从哪里入手?

我第一轮训练遇到的最典型问题是loss在第一个epoch后就变成了NaN。这个问题的排查链路其实有先后顺序:先看数据,再看学习率,再看损失函数,最后看梯度。按这个顺序走能省很多时间。

数据侧,主要是检查GT框是否存在面积为0的框、图片是否存在全黑或者全白的异常样本、标注文件里是否有超出图像边界的坐标。我那次排查发现数据集里有几张图,标注文件用的是左上角坐标和右下角坐标,但是有一张的右下角比左上角还小,导致宽高为负数,在计算IoU时出现了log(负数)的NaN。把这几条标注清洗掉之后,NaN就消失了。

学习率侧,我当时用的是0.02的初始学习率,配合一个10倍衰减的step策略。但在PyTorch 1.0版本,不同优化器的默认权重衰减和动量设置可能差异很大,特别是SGD的momentum默认是0,很多人会忽略这个,导致模型在初期不稳定。我建议新手先用SGD,momentum=0.9,weight_decay=5e-4,学习率从0.005开始,逐步往上调,不要一上来就追求0.02这种大学习率。

还有一个被我忽略很久的点是BN层。如果使用ResNet-50作为骨干网络,并且加载了在ImageNet上预训练的权重,那么BN层的running_mean和running_var是已经冻结好的。但在PyTorch的默认训练模式下,这些BN层仍然会被更新,如果你使用的数据集和目标域差异巨大,BN的统计量会迅速偏移,导致loss爆炸。解决方法是把骨干网络的前几层在训练时固定住,或者在加载预训练权重后对BN层设置requires_grad=False。我实测之后,前者的效果更稳定。

4.2 显存溢出OOM的排查与缓解方案

我在调试时,因为图像尺寸是800x1333,一张图大概占800MB左右的显存,batch size一设为4就显存爆了。排查方法很简单,先用pytorch的torch.cuda.max_memory_allocated打印当前占用,然后逐步减小batch size,直到能运行,再通过观察占用来估算模型的显存费用。

除了降低batch size,我实际采用的方法还有梯度累积,就是把4个batch的梯度累积成一个batch再更新,这样可以在不降低有效训练规模的情况下减少显存占用。PyTorch实现梯度累积的做法很直接,在循环里累计梯度,每N个iter才更新一次参数,注意别忘了除以N来缩放梯度。

显存泄漏也是训练中会遇到的坑,表现是跑着跑着显存占用越来越高,最后OOM。我排查下来原因通常是DataLoader的num_workers设置不当,或者某些tensor没有及时释放。后来我在每个epoch结束之后调用torch.cuda.empty_cache(),显存占用就稳定下来了。

4.3 GPU利用率低的定位方法

训练速度慢不一定全是模型复杂,很多情况是GPU在空转。我用nvidia-smi观察GPU利用率,发现只有30%左右,说明数据加载是瓶颈。问题一般出在DataLoader的num_workers设太小、没有开pin_memory、以及预处理时用到了CPU上的opencv操作。

我的调整方案是把num_workers设为4(具体看CPU核数)、pin_memory=True,同时把数据预处理(resize、normalize)尽量放到GPU上做,比如用torchvision的transforms或者是CUDA tensor的归一化操作。经过这一轮优化,GPU利用率从30%提升到了80%以上。

还有一个小技巧是,如果训练数据量特别大,建议先把图片解码成numpy数组保存到内存中,在dataloader里直接读内存,省去磁盘IO。我踩过的坑是,如果图片存在机械硬盘,那数据加载时间会占整个训练时间的70%,这种情况下不管怎么调num_workers,GPU利用率都上不去,唯一的解法是把数据拷贝到固态硬盘或者是放到内存里。

4.4 断点续训时踩过的坑

模型训练到一半断掉是常事,所以我提前写好了checkpoint保存和恢复的逻辑。我保存的checkpoint包含模型权重、优化器状态、epoch和当前的学习率,这样恢复之后可以无缝衔接。但这里有个坑,PyTorch不同版本保存的checkpoint格式不完全兼容,我遇到过用PyTorch 1.0保存的权重在1.7环境下加载,部分key出现了mismatch,后来统一用同一版本的环境来续训。

还有一个容易被忽略的地方是,恢复训练的时候要把随机种子重新设置,不然数据增强的随机性会完全打乱,前几个epoch相当于重新开始,损失会有一次很大波动。我自己写了个简单的seed设置函数,在恢复时重新调用即可。

5. 推理与验证阶段的调试心得

5.1 NMS阈值对结果的影响有多大

推理的时候,模型会输出大量冗余的检测框,需要用NMS来去重。NMS有个核心参数是IoU阈值,阈值越高,保留的框就越多,检测结果越精细,但容易出现多个框重叠;阈值越低,去重越狠,但可能会把两个相邻的目标当成一个。

我在调试时用0.5和0.7分别测了mAP,发现0.5阈值下的检测框更密集,但很多是假阳性,0.7阈值下mAP更高,尤其对于小目标更友好。另外还要注意PyTorch 1.0自带的torchvision.ops.nms在3D tensor上可能不支持,所以如果你处理的是视频流,需要自己把每一帧循环调用。这种推理阶段的问题定位要依靠可视化,我自己写了一个可视化脚本,把GT框和预测框同时画出来,这样能直接看出问题是出在回归精度不高还是分类置信度不够。

5.2 mAP的计算细节与自查方法

mAP计算是整个检测流程里最容易被低估的部分。不同的实现会用不同的IoU阈值、不同的匹配规则,导致结果相差很大。我使用的评估脚本主要参考COCO的评估方式,对每个类别分别计算AP,然后平均得到mAP。调试时我发现很多人的mAP很低,不是模型训练不好,而是评估脚本里在匹配检测框和GT框时,没有按照置信度从高到低排序,导致匹配失败。

我建议评估的时候,把每个类别的PR曲线画出来,这样能清晰地看到阈值对精度和召回率的影响,比单纯看一个mAP数字有用得多。还有一个坑是要把“置信度为0”的检测框从评估中过滤掉,不然它们会拉低精度。我在评估脚本中加了一个过滤条件,置信度小于0.05的框直接跳过,mAP立即提升了2个点。

5.3 可视化调试的利器

在不熟悉模型输出结构时,最简单的可视化是打印tensor的shape、mean、std。但更高阶的做法是利用TensorBoard来记录loss曲线和梯度范数,这样能直观看出模型是否收敛。my项目里我用的是visdom,虽然它比较老,但在PyTorch 1.0时代还是很好用的。

如果你也想快速在训练时看看特征图,可以把特征图的数值缩放到0-255之间,再用matplotlib画成热力图,我能从热力图中看到网络是否存在“特征响应集中在某一个区域”的问题,这个信息对调整损失权重很有用。后来我还加了一个中间层输出的可视化,在调RPN网络时非常有效,能直接看到anchor在特征图上的分布是否合理。

6. 最后的经验沉淀:调试Faster R-CNN该有的心态和顺序

我个人调试完这个项目最深刻的体会是,Faster R-CNN的调试不是找单一bug,而是不断试错并建立一套“现象-原因”的映射关系。遇到一种报错,先记录下环境、输入、报错和你的处理方式,下次再遇到就可以直接秒杀。

在心态上,我想建议大家准备一定的心理预期,这个模型的调试周期不会太短,尤其是从零开始训练的话,至少需要跑十几个epoch才能看到明显的效果。不要因为loss在前两个epoch没降就怀疑模型结构,检测模型的收敛速度要比分类模型慢得多。

在顺序上,如果你想要快速复现,建议先加载预训练权重,然后在预训练基础上微调,这样比从随机初始化开始训练要靠谱得多。我最后稳定下来的方案是:预训练权重加载ResNet部分,然后冻结前两个stage,只用后面几个stage和RPN、RCNN头去训练。这个方法在小数据集上的表现非常好,在完整的数据集上也能在一个到两个epoch内就开始收敛。

这篇调试记录写到这也就差不多了,都是我一步步试出来的结果。最后再分享一个小技巧,每次改一个参数或者一段代码,都把当时的loss曲线截图保存下来,过几天再回来看,你会发现很多当时觉得离奇的问题,其实早就有迹象,只是你没有记录,也就没有引起注意。祝大家调参顺利,少踩坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询