R-FCN源码拆解:位置敏感池化与PSROIPooling实现 简介R-FCN 源代码包面向目标检测学习者与 Caffe 实践者围绕 ResNet 主干、位置敏感得分图、分类与回归分支提供从网络配置到训练测试的完整参考适合理解区域全卷积检测流程并做二次配置应用。压缩包共 94 个文件、约 349KB以 64 个 m 脚本、12 个 prototxt 网络配置为主辅以 jpg 示例图、mat 数据、cpp/cu 扩展代码及说明文档覆盖数据准备、模型构建、训练验证与演示等环节。已有 552 人学习下载可帮助读者梳理 R-FCN 在 Caffe 下的目录组织、参数设置与实验复用思路便于对照复现和适配不同场景。1. R-FCN 能不能省掉那一层从一次检测的耗时分布说起拿一张 600×800 的图跑两阶段检测分类头之前要按每个候选框抠一次特征几百个框就对应几百次子网络前传耗时随框数量线性上涨。这份 R-FCN 源代码要解决的就是这段重复计算它把位置信息直接编码进卷积通道让所有候选框共享同一张卷积特征图框越多越能看出差别。代码栈是 Caffe Python主干网络用 ResNet训练和测试沿用 py-faster-rcnn 那套脚手架。适合已经跑通过两阶段检测、想弄清楚“无 RoI 子网络”到底怎么落地的人也适合手上有一个老 Caffe 工程、需要继续二次开发的场景。第一次翻这套代码容易懵目录长得跟 Faster R-CNN 几乎一样差别全藏在两个 1×1 卷积和一个自定义池化层里。2. 目录与配置拆解一份 R-FCN 工程的总账把这套代码当成一个普通深度学习项目看会很快迷路因为它同时塞了三样东西一个改过的 Caffe 分支、一份 Python 训练脚手架、一堆 prototxt 网络描述。三者的边界必须先划清楚否则改错地方会白折腾一天。2.1 五个目录各管一摊根目录下的结构大致是这样我按职责标注R-FCN/ ├── caffe/ # 改过的 Caffe 分支多了 PSROIPooling 层 │ ├── include/caffe/layers/ # psroi_pooling_layer.hpp │ ├── src/caffe/layers/ # psroi_pooling_layer.cpp / .cu │ └── Makefile.config.example # 编译配置模板 ├── data/ # 数据集软链、ImageNet 预训练模型、下载脚本 ├── experiments/ │ ├── cfgs/rfcn_voc0712.yml # 训练/测试的全局配置入口 │ └── scripts/rfcn_end2end.sh # 一键训练脚本内部调 tools/train_net.py ├── lib/ │ ├── datasets/ # imdb 注册表与 VOC/COCO 解析 │ ├── fast_rcnn/ # train.py / test.py / config.py │ ├── rpn/ # proposal / anchor_target / proposal_target 三个 Python 层 │ └── utils/ # 计时、绘图、blob 辅助 ├── models/ │ └── rfcn_end2end/ # train.prototxt / test.prototxt / solver.prototxt └── tools/ ├── train_net.py # 训练入口 ├── test_net.py # 评测入口 └── demo_rfcn.py # 单图推理演示caffe/是唯一需要编译的部分也是这套代码和上游 Caffe 的分叉点。位置敏感池化不是用 Python 层硬拼出来的而是写成了 C/CUDA 的 layer注册名PSROIPooling所以必须用仓库自带的这份 Caffe系统里pip install的 caffe 用不了。lib/rpn/里三个 Python 层负责候选框的生成和标签分配lib/fast_rcnn/负责把它们串成训练循环和推理流程。models/rfcn_end2end/只放网络结构文本一行代码都没有但改网络全部在这里。提示很多人第一反应是把lib/里的 Python 文件复制到自己的工程里复用可以但要把caffe/一起带上否则PSROIPooling这一层解析不了。2.2 yml 配置怎么应用到训练这套代码的配置分两层yml 管超参和开关prototxt 管网络拓扑。lib/fast_rcnn/config.py里维护了一个全局cfg字典默认值写死在代码里启动时用 yml 覆盖。典型的experiments/cfgs/rfcn_voc0712.ymlEXP_DIR: rfcn_end2end # 输出目录名快照和日志都落在 output/ 下 TRAIN: HAS_RPN: True # 用 RPN 出候选框False 则退回外部 proposal 文件 BATCH_SIZE: 1 # 每张卡一次迭代用几张图OHEM 很吃显存 SCALES: [600,] # 短边缩放到 600长边由 MAX_SIZE 截断 SNAPSHOT_ITERS: 10000 # 每 1w 次迭代存一次 caffemodel RPN_POSITIVE_OVERLAP: 0.7 # RPN 里判正样本的 IoU 阈值 RPN_BATCHSIZE: 256 # RPN 一个 batch 采样的 anchor 数 TEST: HAS_RPN: True SCALES: [600,] NMS: 0.3 # 测试阶段 NMS 阈值 MAX_SIZE: 1000 POOLING_SIZE: 6 # RoI 输出特征的空间尺寸加载逻辑在lib/fast_rcnn/config.py里核心是cfg_from_file加_merge_a_into_b的递归合并def cfg_from_file(filename): with open(filename, r) as f: yaml_cfg edict(yaml.safe_load(f)) _merge_a_into_b(yaml_cfg, __C) # 递归把 yml 的值压进全局 __C def _merge_a_into_b(a, b): for k, v in a.items(): if k not in b: raise KeyError({} is not a valid config key.format(k)) # 拼错键直接报错 if isinstance(v, dict): _merge_a_into_b(v, b[k]) else: b[k] vedict是支持属性访问的字典所以后面代码里可以直接写cfg.TRAIN.SCALES而不用cfg[TRAIN][SCALES]。_merge_a_into_b里那句raise KeyError很关键yml 里写错一个键名程序启动时就炸而不是静默用默认值跑完 8 小时你才发现学习率根本没改。配置应用的实际顺序是tools/train_net.py先cfg_from_file(args.cfg_file)再cfg_from_list(args.set_cfgs)。后者允许命令行临时覆盖比如追加--set TRAIN.BATCH_SIZE 2跑小规模试验时省得改文件。我一般会把固定不变的写进 yml把要对比的几组超参放到--set里配一层 shell 循环就能刷一轮实验。2.3 prototxt 里的三条主线models/rfcn_end2end/train.prototxt是网络全貌往下拆可以分成三条主线改网络的时候按线找位置最省事。第一条是主干data→ 若干conv→res4f→res5a/res5b/res5cResNet-101 在这里输出 stride 为 16 的特征图。R-FCN 的取舍是把 stride 停在 16不再往下做 stride 32因为位置敏感分数图对空间分辨率敏感再降采样位置信息就糊了。第二条是 RPN 头在res4f之后接一个 3×3 卷积然后分两路 1×1 卷积出rpn_cls_score和rpn_bbox_pred。训练时接AnchorTargetLayer打标签测试时接ProposalLayer出框和 Faster R-CNN 一模一样。第三条是位置敏感头也是这套代码区别于别的两阶段检测的地方# 分类分支输出通道 (类别数 1) * k * kVOC 上 (201)*9 189 layer { name: rfcn_cls type: Convolution bottom: res5c top: rfcn_cls convolution_param { num_output: 189 kernel_size: 1 pad: 0 weight_filler { type: gaussian std: 0.01 } bias_filler { type: constant value: 0 } } } # 位置敏感池化group_size3 表示把 RoI 切成 3x3 九宫格 # 每个格子只从对应的 1/9 通道组里取值 layer { name: psroi_pool_cls type: PSROIPooling bottom: rfcn_cls bottom: rois top: psroi_pool_cls psroi_pooling_param { spatial_scale: 0.0625 # 1/16特征图相对原图的缩放比 output_dim: 21 # 每组通道数等于类别数背景 group_size: 3 # k九宫格边长 } }spatial_scale: 0.0625是从 stride 16 来的改主干网络的降采样倍数就得同步改这个值这是最容易漏的一处。output_dim必须等于类别数加一num_output必须等于output_dim * group_size * group_size三个数之间是乘法关系改一个就得改另外两个。回归分支结构相同只是num_output: 36即 4×9、output_dim: 4。两条分支池化完之后各接一个 3×3 的 AVE 池化把九宫格压成一个标量再 reshape、softmax 出分类概率回归分支直接接 SmoothL1 损失。训练版还会多出proposal_target_layer和OHEM两个非学习层测试版则把这两个换成proposal_layer其余部分共用。3. 核心代码链路位置敏感分数图与 PS-RoI 池化理解了配置下一步是把一次前传真正跟下来。R-FCN 的思路说穿了就一句话不让每个 RoI 单独过子网络而是让卷积图自己“知道”自己负责目标的哪个部位。3.1 位置敏感分数图怎么来普通分类卷积输出的第 c 个通道表示“这个位置像不像第 c 类”。R-FCN 把它拆细第 c 类的判断被分摊到 9 个通道上每个通道只负责 RoI 里 3×3 九宫格中的一个格子。左上角的通道只管“目标左上角应该长什么样”右下角的通道只管右下角。有了这张图一个 RoI 的分类分数就不需要额外的全连接或卷积只要按格子去对应通道组里取值、求平均即可。好处是显而易见的所有 RoI 共享同一张rfcn_cls特征图卷积只算一次RoI 数量从几十涨到几百计算量几乎不变。代价是定位精度对group_size敏感k 取太小位置信息不够细取太大通道数按平方增长显存吃不消。工程上默认 k3是个折中。rfcn_cls的通道排布也有讲究Caffe 的 blob 是 NCHW通道索引c对应的组是c // (k*k)组内的位置偏移是c % (k*k)。所以同一个类别的 9 个位置通道是连续排布的池化层才能用整除的方式直接切分不需要额外的索引表。3.2 PSROIPooling 的正向与反向池化层的实现在caffe/src/caffe/layers/psroi_pooling_layer.cu逻辑不复杂但索引计算容易写错。下面是正向传播的关键部分去掉边界检查后大致长这样// 每个线程处理一个 (roi, 类别组, 格子) 的输出单元 template typename Dtype __global__ void PSROIPoolForward(const int nthreads, const Dtype* bottom_data, const Dtype spatial_scale, const int channels, const int height, const int width, const int pooled_height, const int pooled_width, const Dtype* bottom_rois, const int output_dim, const int group_size, Dtype* top_data, int* mapping_channel) { CUDA_KERNEL_LOOP(index, nthreads) { // index 的拆解顺序n - roi 序号, c - 输出通道(类别), ph/pw - 格子坐标 int pw index % pooled_width; int ph (index / pooled_width) % pooled_height; int c (index / pooled_width / pooled_height) % output_dim; int n index / pooled_width / pooled_height / output_dim; // 取出第 n 个 RoI 的坐标并映射回特征图尺度 bottom_rois n * 5; int roi_batch_ind bottom_rois[0]; Dtype roi_start_w bottom_rois[1] * spatial_scale; Dtype roi_start_h bottom_rois[2] * spatial_scale; Dtype roi_end_w bottom_rois[3] * spatial_scale; Dtype roi_end_h bottom_rois[4] * spatial_scale; // 把 RoI 在特征图上切成 group_size x group_size 个格子 Dtype roi_width roi_end_w - roi_start_w 1; Dtype roi_height roi_end_h - roi_start_h 1; Dtype bin_size_w roi_width / group_size; Dtype bin_size_h roi_height / group_size; // 当前格子 (ph, pw) 的实际范围 int hstart floor(ph * bin_size_h roi_start_h); int wstart floor(pw * bin_size_w roi_start_w); int hend ceil((ph 1) * bin_size_h roi_start_h); int wend ceil((pw 1) * bin_size_w roi_start_w); // 关键一步只从本格子对应的那组通道里取值 int gw pw, gh ph; int ic (c * group_size gh) * group_size gw; bottom_data (roi_batch_ind * channels ic) * height * width; // 格子内做平均池化 Dtype out_sum 0; for (int h hstart; h hend; h) for (int w wstart; w wend; w) out_sum bottom_data[h * width w]; int bin_area (hend - hstart) * (wend - wstart); top_data[index] out_sum / bin_area; mapping_channel[index] ic; // 反向传播要靠它找回原通道 } }逐段解释参数和逻辑。bottom_rois每 5 个数是一组[batch_idx, x1, y1, x2, y2]坐标是原图尺度乘spatial_scale才落到特征图上。group_size同时决定了两件事把 RoI 切成几乘几的格子以及通道分成几组两者必须一致这也是为什么这套设计叫“位置敏感”。ic那一行是整个层的灵魂。输出第 c 个通道的第 (ph, pw) 个格子输入只能来自通道(c * k ph) * k pw也就是“这一类 这一格”。卷积核在训练中会学到负责左上角的通道看到目标左上角就激活别的位置不响应。位置信息就这样被固化进了权重而不是靠额外的坐标回归去补。mapping_channel是反向传播的索引表池化本身没有参数梯度只是按原路散射回去。反向 kernel 里对每个输出单元把梯度除以bin_area之后累加到mapping_channel[index]指向的输入位置。注意这里是累加不是赋值同一个输入通道可能被多个 RoI 的同一个格子引用。注意分数图上的坐标如果越界正向要 clamp 到[0, height-1]否则会读到别的 batch 的显存症状是大 batch 训练时 loss 突然跳变很难查。3.3 train_net.py 到 im_detect 的一次完整前传训练入口在tools/train_net.py它做的事情很薄解析参数、加载配置、拿到 imdb、交给lib/fast_rcnn/train.py的SolverWrapper。def parse_args(): parser argparse.ArgumentParser() parser.add_argument(--gpu, destgpu_id, default0) parser.add_argument(--solver, destsolver, defaultmodels/rfcn_end2end/solver.prototxt) parser.add_argument(--weights, destpretrained_model, defaultdata/imagenet_models/ResNet-101.caffemodel) parser.add_argument(--imdb, destimdb_name, defaultvoc_2007_trainval) parser.add_argument(--cfg, destcfg_file, defaultexperiments/cfgs/rfcn_voc0712.yml) parser.add_argument(--set, destset_cfgs, nargsargparse.REMAINDER) return parser.parse_args()几个参数值得说清楚。--weights是 ImageNet 预训练权重不是检测模型_init_solver_weights会遍历solver.net.params按名字匹配形状不一致的直接跳过——分类头的通道数从 1000 变成了 189必然不匹配跳过是正常行为。如果这里报“没有匹配到任何层”八成是权重文件的层命名和 prototxt 对不上。solver.prototxt里控制迭代节奏的几个值base_lr一般设 0.001 量级stepsize配合gamma做阶梯衰减momentum0.9、weight_decay0.0005 是老 Caffe 的常见配方。iter_size做梯度累积显存不够又想凑大 batch 时用它。推理侧的im_detect在lib/fast_rcnn/test.py流程是预处理、reshape、前传、取输出def im_detect(net, im, boxesNone): blobs, im_scales _get_blobs(im, boxes) im_blob blobs[data] # im_info 打包成 [高, 宽, 缩放比]网络内部据此把框映射回原图 blobs[im_info] np.array( [[im_blob.shape[2], im_blob.shape[3], im_scales[0]]], dtypenp.float32) net.blobs[data].reshape(*(blobs[data].shape)) net.blobs[im_info].reshape(*(blobs[im_info].shape)) forward_kwargs {data: blobs[data].astype(np.float32, copyFalse)} forward_kwargs[im_info] blobs[im_info].astype(np.float32, copyFalse) blobs_out net.forward(**forward_kwargs) scores blobs_out[cls_prob] # 形状 [num_rois, num_classes] boxes blobs_out[bbox_pred] # 已是原图尺度需要除以缩放比 return scores, boxes, im_scalescls_prob是 softmax 之后的类别概率bbox_pred是回归分支输出的偏移量已经解码成绝对坐标解码在 prototxt 里由bbox_transform层完成。这里有个细节容易被忽略测试阶段test.prototxt的输入 blob 是动态尺寸必须显式reshape否则换一张不一样大的图就报维度错误。这也是demo_rfcn.py每跑一张图都要重新 reshape 的原因。4. 从零把它跑起来编译 Caffe 到 demo 出框代码读得再明白编译不过都是零。这一套是 2016 年前后的 Caffe 工程在新环境上翻车概率不低按顺序走能少踩几个。4.1 编译带自定义层的 Caffe先处理依赖主流发行版上大概这几样libprotobuf-dev、libleveldb-dev、libsnappy-dev、libopencv-dev、libhdf5-serial-dev、libboost-all-dev、libgflags-dev、libgoogle-glog-dev、liblmdb-dev加上 CUDA 和对应版本的 cuDNN。Python 侧要numpy、protobuf、scikit-image、matplotlib。然后复制配置模板并改三处cp caffe/Makefile.config.example caffe/Makefile.config打开后重点看这几行# CUDA 路径按实际安装位置改 CUDA_DIR : /usr/local/cuda # cuDNN 开关没有装就注释掉但训练会慢很多 USE_CUDNN : 1 # Python 头文件路径用 python2 -c import sysconfig; ... 查 PYTHON_INCLUDE : /usr/include/python2.7 \ /usr/lib/python2.7/dist-packages/numpy/core/include # 关键把自带的 python 层加进来否则 proposal 层解析失败 PYTHONPATH : $(HOME)/R-FCN/python:$(PYTHONPATH)最后那条PYTHONPATH是我见过最容易漏的。lib/rpn/proposal_layer.py这些是 Python 层Caffe 在解析 prototxt 时要能 import 到路径没配好训练脚本一启动就报Unknown layer type: Python。编译本身cd caffe make -j$(nproc) # 先跑 CPU 部分报错信息更易读 make pycaffe # 生成 Python 绑定 make test -j$(nproc) # 可选验证层有没有编坏编完之后把caffe/python加进PYTHONPATH把caffe/build/tools加进PATH再验证export PYTHONPATH$HOME/R-FCN/caffe/python:$HOME/R-FCN/lib:$PYTHONPATH python -c import caffe; print(caffe.__file__)能打印出仓库里的路径就对了。如果打印的是site-packages下的路径说明装过别的 caffe先卸掉或者调换PYTHONPATH顺序。4.2 数据集与预训练权重就位数据目录靠软链组织脚本不会替你下载数据集只负责建链接。VOC 的话先把VOCdevkit放到data/下面再跑cd data ln -sf $PWD/VOCdevkit VOCdevkit2007 ./scripts/fetch_imagenet_models.sh # ResNet-101 的 ImageNet 预训练权重fetch_imagenet_models.sh里会调tools/download_model.py网络不通的时候这个脚本会卡很久我一般手动把权重放到data/imagenet_models/下跳过下载步骤。文件名必须和lib/fast_rcnn/train.py里默认的名字对上否则--weights那一关过不去。数据集的注册在lib/datasets/factory.py启动时__sets字典里会预先塞好一批# 只截取 VOC 相关部分 for year in [2007, 2012]: for split in [train, val, trainval, test]: name voc_{}_{}.format(year, split) __sets[name] (lambda splitsplit, yearyear: datasets.pascal_voc(split, year))所以--imdb voc_2007_trainval这种写法能直接找到对应数据集。自己加了新数据集就得在这个文件里补一行注册否则报Unknown dataset。4.3 demo、test、train 三条命令先跑 demo 验证整条链路通不通这一步不需要数据集cd $HOME/R-FCN python ./tools/demo_rfcn.py --gpu 0 \ --def models/rfcn_end2end/test.prototxt \ --net output/rfcn_end2end/voc_2007_trainval/rfcn_end2end_iter_80000.caffemodel \ --cfg experiments/cfgs/rfcn_voc0712.yml--def是网络结构文件--net是训练出来的权重两个必须配套用 21 类的test.prototxt配 81 类的 COCO 权重会在 reshape 阶段直接崩。demo 脚本默认读data/demo/下的图片输出检测结果图。评测python ./tools/test_net.py --gpu 0 \ --def models/rfcn_end2end/test.prototxt \ --net output/rfcn_end2end/voc_2007_trainval/rfcn_end2end_iter_80000.caffemodel \ --imdb voc_2007_test \ --cfg experiments/cfgs/rfcn_voc0712.yml \ --comp standard--comp standard表示用 VOC 官方的 11 点或全点 AP 计算输出会写进output/下的结果目录一个.pkl加一份 txt 报告。训练python ./tools/train_net.py --gpu 0 \ --solver models/rfcn_end2end/solver.prototxt \ --weights data/imagenet_models/ResNet-101.caffemodel \ --imdb voc_2007_trainval \ --cfg experiments/cfgs/rfcn_voc0712.yml \ --iters 80000--iters覆盖 solver 里的max_iter做短周期试验时很方便。训练日志里重点盯两个数rpn_cls_loss和rfcn_cls_loss前者不降说明 anchor 尺度不合适后者不降多半是 OHEM 的阈值设得不合理。5. 避坑与排查五个编译过之后才暴露的问题5.1 报错 Unknown layer type: Python现象训练脚本一启动就退出提示 prototxt 里有无法识别的层类型。原因Caffe 编的时候没带 Python 层支持或者运行时PYTHONPATH里找不到lib/rpn/。解决确认Makefile.config里WITH_PYTHON_LAYER : 1已经打开并重新编译再确认环境变量里同时包含caffe/python和仓库的lib/。这两个路径缺一不可只加前者会找不到自定义的 proposal 层。5.2 训练几个 iter 后 loss 变 nan现象前几百次迭代正常之后 loss 突然变成 nan权重全部失效。原因九宫格切分时 RoI 的宽高可能小于group_sizebin_size算出小于 1 的值hstart和hend相等除以 0。另外学习率过大也会在 OHEM 层放大难样本梯度。解决在池化层里对bin_size做下限保护或者把spatial_scale与最小 RoI 尺寸对齐学习率从 0.001 往下调同时把iter_size提到 2 让梯度更稳。这类问题真的是玄学同一个配置换张卡就复现不了。5.3 PSROIPooling 形状不匹配现象Check failed: channels output_dim * group_size * group_size。原因改了类别数却只改了num_output没同步改output_dim或者两个都没改。解决记住三者的乘法关系。VOC 20 类时num_output 21 * 3 * 3 189COCO 80 类时是81 * 3 * 3 729回归分支固定4 * 3 * 3 36。改数据集的时候两个卷积层的num_output和两个池化层的output_dim要一起改。5.4 GPU 显存不够现象Check failed: error cudaSuccess (2 vs. 0) out of memory。原因TRAIN.BATCH_SIZE加SCALES决定的输入尺寸再乘以 OHEM 采样量显存占用是指数级涨的。解决把BATCH_SIZE降到 1SCALES从 600 降到 480 先跑通不行再开iter_size做梯度累积用时间换显存。降尺度会影响小目标召回最终还是要回到能承受的最大尺寸。5.5 评测结果和论文差好几个点现象能跑通AP 也正常但明显偏低。原因test.prototxt里的 NMS 阈值和MAX_SIZE跟训练配置不一致或者用了voc_2007_trainval训出来的权重去评voc_2007_test之外的数据。解决对齐TEST.NMS、TEST.SCALES、TEST.MAX_SIZE三项和训练时保持同一套缩放逻辑。还有一点常被忽略评测要在原图尺度上做im_detect返回的框已经除了缩放比如果外面又除了一次框全部缩在角落AP 会掉得莫名其妙。6. 二次开发换数据集、换主干的改造路径真正落地时改的地方集中在四个位置我整理成一张表照着找比翻目录快改造目标改动文件具体动作换类别数models/rfcn_end2end/*.prototxt改rfcn_cls的num_output与池化的output_dim换数据集lib/datasets/factory.pylib/datasets/新增解析类并在__sets注册换主干models/*.prototxtlib/fast_rcnn/config.py替换 conv 段同步改spatial_scale跑单图服务tools/demo_rfcn.py抽出im_detect封装成常驻进程换数据集是最常见的需求。假设手上一批标注是自定义格式做法是照lib/datasets/pascal_voc.py抄一个类实现image_path_at、_load_annotations两个方法然后在factory.py里加注册# lib/datasets/factory.py 追加 from datasets.mydataset import my_dataset # 自己写的解析类 # 注册名就是命令行 --imdb 后面跟的字符串 __sets[my_train] lambda: my_dataset(train) __sets[my_test] lambda: my_dataset(test)注册名一旦写进--imdbtrain_net.py就会去__sets里查查不到直接抛异常。这套注册机制其实挺方便多个数据集并存时不用改任何主流程代码。新增类里要注意类别列表的顺序必须和 prototxt 的num_output对上背景不算在内类的顺序变了旧的权重就废了。换主干的坑更多。R-FCN 对 stride 很敏感如果用了一个总降采样 32 倍的主干spatial_scale要改成 1/32同时九宫格覆盖的区域会变粗小目标基本没戏。我的做法是先只换主干的前几段、保留 res5 的结构训一轮看rpn_cls_loss的收敛速度能收敛再往下深调。验证是否改对最直接的办法是把test.prototxt的第一层输出维度打印出来和预期的手算值逐个比对比看 loss 曲线快得多。还有一个容易被忽略的点OHEM 的thresh参数和类别数相关。类别少的时候难样本集中阈值可以设高一点类别多了之后前景样本被打散同样的阈值可能采不到足够的难例loss 会一直震荡。这个我有过血泪经验换到 80 类的数据集上直接沿用 21 类的配置白了半张卡的时间。从那以后我每次动 prototxt都强制先跑一遍test_net.py用一张图出结果确认框能画在正确位置再开训——后悔药这东西还是在训练前吃比较便宜。希望帮到你。本文还有配套的精品资源点击获取