资讯详情

资讯详情

单卡多目标检测实战:SSD Caffe模型结构、训练与部署避坑

简介SSD caffe模型配套文件面向计算机视觉学习者与目标检测实践者聚焦在Caffe框架下复现并调用SSD 300x300检测网络适合已具备深度学习基础、希望打通训练配置与推理部署流程的读者。压缩包共6个文件约93.16MB包含4个prototxt网络与求解器配置、1个Python脚本和1个caffemodel权重覆盖网络结构定义、超参数设置及模型加载等关键环节便于理解SSD多尺度预测机制和OpenCV调用前的文件准备。已有715人学习说明该组合在入门复现与课程实验场景中具有一定参考价值。读者可据此梳理基础网络与检测层的组织方式认识类别标签、均值处理和预训练权重在检测流程中的作用并对照配置理解非极大值抑制、框坐标映射及实时场景测试所需的模型基础为后续微调与部署提供清晰入口。1. 单卡也要跑多目标检测SSD caffe模型解决的是什么麻烦产线相机一秒拍十二张一张图平均三四个待检目标甲方给的那台机器只有一张老显卡。两阶段检测器精度够但一帧两百毫秒压不下来早期单阶段方案对小目标又老漏。折腾一圈SSD caffe模型成了当时最能落地的选择单次前向在多个尺度的特征图上直接出框DL 里这套 single shot 的思路在算力和精度之间给了一个能接受的折中。这个模型本质上就是 Caffe 版的 Single Shot MultiBox Detector——一个 VGG16 带六路卷积分支、靠 PriorBox 铺先验锚框、用 MultiBoxLoss 做定位与分类联合训练、最后用 DetectionOutput 层一次吐出所有框和分数的完整工程。它适合手里只有 Caffe 工具链、需要离线部署、还想改网络结构做定制的开发者如果你追的是最新 SOTA那它确实老了。2. 网络结构拆解六路卷积分支是怎么写进 prototxt 的SSD 在 Caffe 里不是“一个模型文件”而是十来份 prototxt 加上几个自定义层的组合。看懂结构重点不在 backbone而在多尺度头和那两个原生 Caffe 根本没有的层。2.1 骨干网络与六个预测分支的取舍骨干用的是经典 16 层卷积网络结构前 5 个卷积块保留原来的两个全连接层fc6、fc7改写成卷积层其中 conv6 用空洞卷积dilation6把感受野撑大但不增加参数。再往后接 conv8_2、conv9_2、conv10_2、conv11_2 四组额外卷积输出通道逐级减半。取哪几层做预测是有讲究的特征层特征图尺寸每位置先验框数该层框数主要管什么conv4_338×3845776小目标conv719×1962166中小目标conv8_210×106600中目标conv9_25×56150中大目标conv10_23×3436大目标conv11_21×144全局把 300×300 输入送进去六层加起来正好 8732 个先验框。这里有个容易被忽略的细节conv4_3 的数值尺度和其他层差得远直接接分类头会训练不动所以中间要插一个 Normalize 层把它的响应按通道压到同一量级初始 scale 一般设 20。layer { name: conv4_3_norm type: Normalize bottom: conv4_3 top: conv4_3_norm norm_param { across_spatial: false scale_filler { type: constant value: 20 } channel_shared: false # 逐通道一个可学习缩放不是全局一个 } } layer { name: conv4_3_norm_mbox_conf type: Convolution bottom: conv4_3_norm top: conv4_3_norm_mbox_conf param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } # bias 学得快一点是常见做法 convolution_param { num_output: 84 # (20 类 1 背景) × 每位置 4 个框 kernel_size: 3 pad: 1 stride: 1 weight_filler { type: xavier } bias_filler { type: constant value: 0 } } }num_output这个数是最容易算错的地方它是“类别数 × 该层每位置先验框数”。上面这层是 21×484换成自建数据集的 5 类那就得是 6×424。一处没改后面 MultiBoxLoss 会直接报维度不匹配。每个预测位置实际输出了两组张量一组是mbox_loc4 个偏移量一组是mbox_conf类别概率。六层各自算出这两组最后全部 Concat 到一起。名字前缀必须严格一致因为拼接靠的是按名字排序改成conv4_3_mbox_loc这种漏了下划线的写法拼接顺序就乱训练出来的框会整体错位。2.2 PriorBox 和 DetectionOutput原生 Caffe 不带的那两层普通 Caffe 里没有 PriorBox、DetectionOutput、MultiBoxLoss、AnnotatedData 这几个层。也就是说你必须用带检测扩展的那份分支源码并且caffe.proto里得有对应的 Parameter 定义。这几层干的事用一句话概括AnnotatedData读 LMDB同时输出图像和归一化的标注框PriorBox根据特征图尺寸和 min/max_size 现算出所有先验框坐标不学参数MultiBoxLoss做真值匹配、困难负样本挖掘输出定位 分类联合 lossDetectionOutput解码偏移量、按类别做 NMS、输出最终框。推理时的尾巴长这样layer { name: detection_out type: DetectionOutput bottom: mbox_loc bottom: mbox_conf bottom: mbox_priorbox top: detection_out include { phase: TEST } detection_output_param { num_classes: 21 share_location: true background_label_id: 0 nms_param { nms_threshold: 0.45 top_k: 400 } code_type: CENTER_SIZE keep_top_k: 200 confidence_threshold: 0.01 } }参数逐个说清楚因为它们直接决定你看到什么结果。background_label_id: 0意味着类别 0 是背景你的 labelmap 必须从 0 开始编号且 0 给背景nms_threshold: 0.45是同类框之间 IoU 超过就压掉密集小目标场景调到 0.5 甚至 0.55 才不会互相吃掉confidence_threshold: 0.01是输出的下限只是过滤掉明显没戏的框别指望它替代后处理keep_top_k: 200是每张图最多留多少个框接下游业务时按需压到几十个能省不少解析时间。注意num_classes在三处 prototxttrain / test / deploy和 labelmap 里都要对上少改一处就是一个下午。2.3 一份完整工程的文件清单抛开脚本不看真正决定模型行为的是这几份文件我按改动频率排了序文件作用换数据集时的改动量train.prototxt训练网络定义改 num_output、num_classes、data 层路径test.prototxt评估网络定义同 train另需 test_name_sizedeploy.prototxt推理网络定义去 data 层、去 loss 层、保留 DetectionOutputsolver.prototxt优化器与学习率策略改 batch 相关、迭代数labelmap.prototxt类别 id 到名字重写trainval / test 的 LMDB数据重新生成deploy.prototxt 是最容易漏改的一份很多人从 train.prototxt 直接改忘了删掉 MultiBoxLoss 那几层一加载就报 “Unknown bottom blob”。正确的做法是把 loss 层整段删掉输入层换成input: data / input_shape { dim: 1 dim: 3 dim: 300 dim: 300 }其余保持与训练一致。3. 跑通流程从编译带检测层的 Caffe 到一张图出框结构看明白接下来是动手。这一段按真实顺序走一遍每一步都写清楚失败时该去看哪。3.1 编译带检测层的那份 Caffe先明确一点用系统包管理器装的那份 Caffe一定报Unknown layer type: DetectionOutput。必须拿带检测扩展的分支源码自己编。cd ~/caffe-ssd cp Makefile.config.example Makefile.config # 打开这几个开关 # USE_CUDNN : 1 # USE_OPENCV : 1 # PYTHON_LIBRARIES : boost_python3 python3.xm # WITH_PYTHON_LAYER : 1 # 关掉 USE_NCCL单卡用不上还容易和驱动版本打架 make clean make -j$(nproc) make pycaffe # 验证自定义层有没有编译进去 python -c import caffe; print(caffe.Net)编译完先别急着训练跑一次最小验证拿 deploy.prototxt 随便初始化一个网络看能不能正常 forward。这一步能提前暴露 prototxt 语法错误和层注册问题比等到训练第 500 iter 才发现强得多。python - PY import caffe net caffe.Net(deploy.prototxt, caffe.TEST) # 不加载权重只看结构 out net.forward() print(out[detection_out].shape) # 期望 [1, 1, N, 7] PY如果caffe.proto是后改的一定要先make clean再编否则 protobuf 生成的caffe_pb2.py是旧的表现是参数解析不出来报Message type caffe.PriorBoxParameter has no field named variance之类的错。3.2 数据准备LMDB、labelmap 与 test_name_size 三件套数据先按标准检测数据集的三件套组织JPEGImages/、Annotations/每张图一个 XML、ImageSets/Main/trainval.txt、test.txt。目录名不能随便改因为转换脚本里写死了。# 1. 生成文件名列表 python scripts/create_list.sh # 2. 转 LMDB 和 test_name_size.txt python scripts/create_data.sh # 产物 # lmdb/trainval_lmdb/ # lmdb/test_lmdb/ # lmdb/labelmap.prototxt # lmdb/test_name_size.txttest_name_size.txt记录测试集每张图的高宽评估脚本靠它把归一化的预测框还原回原图坐标。生成之后核对一下行数和 test.txt 是否一致少一行后面所有 mAP 都是错的而且不会报错这个坑极其隐蔽。注意转换脚本里如果按比例抽样比如只取 60% 做测试集评估出来的数字跟你预期会差一截正式跑之前把抽样关掉。3.3 训练batch、iter_size 与学习率的关系solver 的配置里真正影响能不能收敛的就四个数train_net: models/ssd/train.prototxt test_net: models/ssd/test.prototxt test_iter: 200 test_interval: 5000 base_lr: 0.001 momentum: 0.9 weight_decay: 0.0005 lr_policy: multistep gamma: 0.1 stepvalue: 80000 stepvalue: 100000 max_iter: 120000 iter_size: 4 average_loss: 10 snapshot: 10000 snapshot_prefix: models/ssd/snapshot/ssd300 solver_mode: GPU原始配置是四卡 batch 32 跑出来的你只有单卡的话别直接改base_lr——学习率是跟着总 batch 走的。正确姿势是batch_size: 8配iter_size: 4等效 batch 还是 32梯度累积在 Caffe 里由 iter_size 自动完成学习率一行不用动。这个组合我踩过太多次直接 batch 8 lr 0.001前两千次迭代 loss 掉得飞快然后原地打转最后 mAP 差两个点。启动训练GLOG_logtostderr1 ~/caffe-ssd/build/tools/caffe train \ --solvermodels/ssd/solver.prototxt \ --weightsmodels/ssd/vgg16_pretrained.caffemodel \ --gpu 0 21 | tee train.log--weights那行很关键。骨干部分的预训练权重必须加载从零训 SSD 在单卡上基本不可能收敛——六路分支的梯度差异太大浅层很容易被打散。加载时 Caffe 会打印哪些层没匹配上mbox 系列那几个新层那是正常的其余层几乎应该全部匹配。日志里最该盯的是loss的两个分量。正常情况下 loc loss 快速降到 24 之间然后缓慢震荡conf loss 从 8 左右稳步往下走。如果 loc loss 一开始就是 0别高兴那说明匹配阶段一个正样本都没匹配上。3.4 推理一张图走完前向与后处理DetectionOutput 的输出格式是[image_id, label, score, xmin, ymin, xmax, ymax]后四个是除以输入尺寸后的归一化值不是像素坐标。import numpy as np import caffe net caffe.Net(deploy.prototxt, ssd300.caffemodel, caffe.TEST) transformer caffe.io.Transformer({data: net.blobs[data].data.shape}) transformer.set_transpose(data, (2, 0, 1)) # HWC - CHW transformer.set_mean(data, np.array([104, 117, 123])) # BGR 下逐通道减 transformer.set_raw_scale(data, 255) # [0,1] - [0,255] transformer.set_channel_swap(data, (2, 1, 0)) # RGB - BGR img caffe.io.load_image(demo.jpg) # 读成 [0,1] 的 RGB net.blobs[data].data[...] transformer.preprocess(data, img) det net.forward()[detection_out][0, 0] # 去掉 batch 和 image_id 两维 keep det[det[:, 2] 0.5] for _, label, score, xmin, ymin, xmax, ymax in keep: print(int(label), round(float(score), 3), [round(v * 300, 1) for v in (xmin, ymin, xmax, ymax)])均值[104, 117, 123]和通道交换这两步是绑死的。Caffe 的load_image返回 RGB 浮点模型训练时喂的是 BGR 减均值顺序错一位颜色通道就反了检测结果会变成“什么都框不到”或者框得离谱但程序不报错。这种问题排查起来特别费劲我一般会在预处理之后立刻存一张图肉眼看一眼。还原到原图尺寸时别直接乘 300因为原图通常不是 300×300被 resize 过。换算方式是先乘 300 得到输入图上的像素坐标再按原图与 300 的比例缩放回去。4. 核心代码逐段拆解先验框、真值匹配与 NMS这一节的代码是理解 SSD 的关键也是二次开发时最常改的三段。写的是 numpy 版本逻辑和 Caffe 里的 PriorBox、MultiBoxLoss 完全对得上拿来对账很方便。4.1 先验框min_size / max_size / aspect_ratio 怎么算出 8732先验框的尺寸不是拍的按一个线性插值公式生成第 k 层的尺度s_k s_min (s_max - s_min) × (k-1) / (m-1)s_min0.2、s_max0.9、m6于是六层得到 0.2、0.34、0.48、0.62、0.76、0.9再乘输入边长 300 就是各层的 min_size。import numpy as np def ssd_priors(feat_h, feat_w, min_ratio, max_ratio, ratios, step, img300, offset0.5, clipTrue): feat_h/feat_w: 特征图尺寸step: 输入图上的步长 img / feat_w boxes [] for i in range(feat_h): for j in range(feat_w): # 中心点落在像素格中心offset0.5 就是标准的格子中心 cx (j offset) * step / img cy (i offset) * step / img # 两个正方形min_size 和 sqrt(min_size * max_size) sizes [min_ratio, np.sqrt(min_ratio * max_ratio)] for r in ratios: # 每个长宽比衍生两个框r 和 1/r sizes.append(min_ratio * np.sqrt(r)) sizes.append(min_ratio / np.sqrt(r)) for s in sizes: boxes.append([cx, cy, s, s]) # 存的是 cx, cy, w, h boxes np.array(boxes, dtypenp.float32) return np.clip(boxes, 0, 1) if clip else boxes # conv4_3 层38×38min 30/300max 60/300长宽比只有 2 - 每位置 4 个框 p ssd_priors(38, 38, 30/300, 60/300, [2], step8) print(p.shape) # (5776, 4) 38*38*4参数说明ratios传[2]出 4 个框传[2, 3]出 6 个框这正好对应前面表格里 4/6 的分配。step是输入图上的步长38×38 对应 819×19 对应 1610×10 对应 32以此类推传错一个框会整体偏移半格到几格。clipTrue把超出边界的框裁掉训练时建议开着否则边缘的先验框中心点算出来是负数匹配时会引入噪声。改小目标检测能力改的就是这里。把 conv4_3 的min_size从 30 调到 24、aspect_ratio加上 3小目标的召回率通常能提一两个点代价是框数变多、推理慢一点。4.2 真值匹配IoU 0.5 的二分匹配加阈值兜底SSD 的匹配策略是两部分叠加每个真值框找 IoU 最大的那个先验框强制配对保证每个 GT 至少有一个正样本然后所有与任一 GT 的 IoU 超过 0.5 的先验框也算正样本。def jaccard(gt, priors): gt: [G,4] xyxypriors: [P,4] cxcywh返回 [G,P] 的 IoU 矩阵 g np.stack([gt[:, 0], gt[:, 1], gt[:, 2], gt[:, 3]], 1) px priors[:, 0] - priors[:, 2] / 2 py priors[:, 1] - priors[:, 3] / 2 p np.stack([px, py, px priors[:, 2], py priors[:, 3]], 1) inter_xmin np.maximum(g[:, None, 0], p[None, :, 0]) inter_ymin np.maximum(g[:, None, 1], p[None, :, 1]) inter_xmax np.minimum(g[:, None, 2], p[None, :, 2]) inter_ymax np.minimum(g[:, None, 3], p[None, :, 3]) inter np.clip(inter_xmax - inter_xmin, 0, None) * \ np.clip(inter_ymax - inter_ymin, 0, None) area_g (g[:, 2] - g[:, 0]) * (g[:, 3] - g[:, 1]) area_p priors[:, 2] * priors[:, 3] union area_g[:, None] area_p[None, :] - inter return inter / np.maximum(union, 1e-10) def match(gt_boxes, gt_labels, priors, threshold0.5): overlaps jaccard(gt_boxes, priors) # [G, P] best_prior_idx overlaps.argmax(1) # 每个 GT 最配的框 best_prior_overlap overlaps.max(1) best_truth_idx overlaps.argmax(0) # 每个框最配的 GT # 强制配对保证每个 GT 至少有一个正样本否则这个类直接学不到 best_truth_idx[best_prior_idx] np.arange(gt_boxes.shape[0]) best_truth_overlap overlaps[best_truth_idx, np.arange(overlaps.shape[1])] best_truth_overlap[best_prior_idx] 2.0 # 强制配对的分数压到最高 labels gt_labels[best_truth_idx].copy() labels[best_truth_overlap threshold] 0 # 0 是背景 return labels, gt_boxes[best_truth_idx]这段最值得留意的是best_truth_overlap[best_prior_idx] 2.0那一行。没有它某些特别小的 GT 可能跟任何先验框 IoU 都不到 0.5强制配对完又被阈值判回背景这类目标在整轮训练里一个正样本都没有。表现就是训练日志一切正常但那一类 mAP 永远是 0。负样本数量是正样本的 3 倍靠 MultiBoxLoss 里的困难负样本挖掘挑出来所以neg_pos_ratio这个参数不要随手调大调到 10 会让 loss 被背景主导模型倾向于全预测背景框直接消失。4.3 解码与 NMS绕开 DetectionOutput 之后要自己做的事DetectionOutput 层内部已经做完了解码和 NMS但如果为了灵活控制阈值、或者要在其他框架里复现就得手算。解码的公式是先验框的中心加上偏移量乘以方差再乘先验宽高。def decode(loc, priors, variances(0.1, 0.1, 0.2, 0.2)): loc: [P,4] 网络输出priors: [P,4] cxcywh返回 xyxy 归一化坐标 boxes np.concatenate([ priors[:, :2] loc[:, :2] * variances[0] * priors[:, 2:], priors[:, 2:] * np.exp(loc[:, 2:] * variances[1]), ], 1) boxes[:, :2] - boxes[:, 2:] / 2 boxes[:, 2:] boxes[:, :2] return boxes def nms(dets, scores, thresh0.45): dets: [N,4] xyxy按类别调用一次 order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(dets[i, 0], dets[order[1:], 0]) yy1 np.maximum(dets[i, 1], dets[order[1:], 1]) xx2 np.minimum(dets[i, 2], dets[order[1:], 2]) yy2 np.minimum(dets[i, 3], dets[order[1:], 3]) inter np.clip(xx2 - xx1, 0, None) * np.clip(yy2 - yy1, 0, None) area_i (dets[i, 2] - dets[i, 0]) * (dets[i, 3] - dets[i, 1]) area_o (dets[order[1:], 2] - dets[order[1:], 0]) * \ (dets[order[1:], 3] - dets[order[1:], 1]) iou inter / (area_i area_o - inter 1e-10) order order[1:][iou thresh] # 超过阈值的一律压掉 return keepvariances必须和 prototxt 里 PriorBox 的variance四个值完全一致默认是0.1, 0.1, 0.2, 0.2。改过训练配置却忘了同步推理脚本效果是框整体缩小一圈分数还挺高看起来像“模型精度不行”其实是解码参数错了。NMS 是按类别分开做的别混在一起压。混着压会出问题两个不同类别的目标靠得很近分数高的那个把另一个压掉表现是“同一位置只识别出一种东西”。5. 避坑与排查loss 不降、mAP 归零、加载报 Unknown layer下面这几条都是我在真实项目里撞过的墙按“现象 → 原因 → 解决”记遇到的时候可以直接对号。5.1 训练阶段的四类现场现象loss 从第一次迭代就是 0 或者 nan。原因九成是类别数对不上。train.prototxt里 mbox_conf 的num_output是按 21 类写的labelmap 换成了 6 类维度错位后 softmax 分母直接爆掉。另一种情况是background_label_id没有跟 labelmap 对齐——labelmap 里背景不是 0 号但 MultiBoxLoss 仍然按 0 处理。 解决把 train / test / deploy 三份 prototxt 的num_classes、六层 conf 的num_output、labelmap 的条目数、solver 无关全部重新对账一遍。我一般写个 grep 一次性列出来核对。现象conf loss 缓慢下降loc loss 一直在 30 以上不收敛。原因通常是--weights没加载上骨干从随机初始化开始训。日志开头会有一行 “Loading weights”往下翻能看到哪些层被跳过如果 conv1_1、conv2_1 这些基础层都被跳过了说明权重文件格式不对或路径错了。 解决先单独用这份权重初始化一个纯骨干网络forward 一次确认能用再接检测头。另外检查lr_mult新加的 mbox 层如果用默认学习率配上 0.001 的 base_lr 一般没问题如果是从别的配置拷过来的、lr_mult设成了 10那前几千次迭代 loss 会剧烈震荡。现象单卡 OOM报Check failed: error cudaSuccess (2 vs. 0) out of memory。原因是照着四卡 batch 32 的配置直接跑单卡显存要的是四倍。也有人把 batch 改小了但忘了同步 test_iter评估阶段一次性把测试集全塞进显存训练好好的一到 test_interval 就崩。 解决batch_size: 8iter_size: 4保持等效 batchtest_iter按测试集数量 / test 阶段 batch_size重算不要沿用旧值。现象训练日志里 loss 正常但某一类目标从头到尾学不出来。原因是这一类的标注框尺寸特别小或者长宽比特别极端匹配阶段全被判成背景了。 解决临时打印匹配后的正样本数看看这一类有没有被分配到如果确实是 0把该层的aspect_ratio补上更极端的比例或者把这类的 GT 框在数据增强时不做缩小。5.2 推理与部署阶段的三类翻车现象编译明明过了一加载模型就报Unknown layer type: DetectionOutput。原因是用错了源码分支——主干的 Caffe 里没有这几个层或者虽然换了源码但caffe.proto没重新生成Python 侧读到的还是旧定义。 解决确认源码目录里能 grep 到DetectionOutputLayer然后make clean make -j make pycaffe完整重编。只make不 clean 是最常见的无效操作。现象mAP 全程是 0.00x检测框散落在全图。原因有一半是test_name_size.txt行数和测试集数量对不上评估脚本读取图片尺寸时整体错位NMS 之后剩下的框全是乱的另一半是 test.prototxt 的 Anchors 配置和训练时的 PriorBox 参数不一致导致解码基准不同。 解决先核对wc -l test_name_size.txt和wc -l test.txt再 diff 一遍 train 和 test 里的 PriorBox 参数段两边的min_size / max_size / aspect_ratio / variance必须逐字相同。现象漏检严重但分数高的框位置很准。原因是confidence_threshold在 deploy 里设得太高或者nms_threshold太小把密集目标互相压掉了。 解决把 deploy 里的阈值降到 0.01 先看全量输出再决定业务侧阈值密集场景把 nms_threshold 从 0.45 提到 0.55。这一步不要动模型纯调参就能救回不少召回。注意每次改完 prototxt先跑一次python -c import caffe; caffe.Net(deploy.prototxt, caffe.TEST)语法和维度问题十秒内就能暴露。6. 二次开发换数据集、改类别数、剪分支做提速真正落到项目上改动最大的往往是这三件事我按改动顺序列清楚。换成自建数据集第一件事是重写 labelmap背景固定 0 号剩下的按你业务类别排。然后是一张统一的改动清单改动点位置注意类别数三份 prototxt 的num_classes三处必须完全一致分类头通道六层mbox_conf的num_output等于 (类别数1) × 该层框数先验框尺度各层min_size / max_size按目标实际像素尺寸重算长宽比aspect_ratio目标细长就加 3、5数据路径AnnotatedData 的data_param.sourcetrain / test 分别指评估脚本test_name_size.txt行数必须与测试集一致先验框尺度这一步最值得花时间。假设你的目标在原图上普遍是 40120 像素输入是 300×300、原图 1200×1600缩放后目标大概只剩 1030 像素。这时候 conv4_3 层的min_size还留着 30就明显偏大小目标全是负样本。按缩放后的实际尺寸重新插值这一处改动带来的提升通常比调学习率大得多。剪分支提速是另一条路。六层里 conv10_2、conv11_2 的贡献集中在大目标上如果业务里根本没有大目标这两层连同它们的 mbox 头可以整段删掉参数量和计算量都降且不影响小目标。删的时候要同步删三处train / test / deploy 里的分支定义、对应的 PriorBox 和 mbox 头以及 deploy 里 Concat 层的 bottom 列表。少删一处就是维度不匹配。验证方法我固定走三步先看每一类的 AP别只看 mAP——总 mAP 涨了但某一类掉了说明改动只对部分尺度有效再按 small / medium / large 三档看分组 AP确认改动打在了预期的尺度上最后跑一遍纯推理计时把预处理、前向、后处理分开记因为大部分“模型变慢了”其实慢在后处理上。import time t0 time.time(); net.blobs[data].data[...] transformer.preprocess(data, img) t1 time.time(); det net.forward()[detection_out] t2 time.time(); keep det[det[0, 0, :, 2] 0.5] t3 time.time() print(fpre {t1-t0:.3f}s fwd {t2-t1:.3f}s post {t3-t2:.3f}s)这段计时代码看着简陋但每次都跑。有一次我以为是模型剪枝没生效结果计时打出来发现前向快了 30%后处理因为框数暴增反而慢了一倍整体耗时没变。从那以后我每改一次num_classes都强制走一遍“labelmap → train → test → deploy”四点对账再空跑一次 forward 确认输出维度。这个习惯救回来的时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →