从最开始聊吧。前段时间我一直在做“火星表面岩石检测与识别”相关的算法验证,用的就是标题里这套组合:SABL-RetinaNet,骨干网络是ResNet50-GN,特征金字塔FPN,从头到尾把训练流程完整跑了一遍。这里不贴那种“一键训练”的营销文,我把模型选型逻辑、配置文件细节、训练过程中的真实数据变化、还有踩过的那些坑全部记录下来。如果你也在做遥感图像目标检测、行星表面地物识别,或者只是想把检测模型调得再稳一点,这篇应该能省你不少时间。先说结论:这套方案在火星表面岩石这种“背景复杂、目标尺度跨度大、样本相对少”的任务上,效果比标准的Faster R-CNN和原始RetinaNet都要稳。SABL这个检测头功不可没,GN归一化在小batch下也真的是救命。下面一层层拆开讲。1. 项目背景与任务定义1.1 为什么火星岩石检测不能照搬通用目标检测火星表面岩石检测,本质上是一个目标检测任务,但它的数据特性和通用物体检测差别非常大。通用检测任务里,一张图可能有几个到几十个物体,类别多,每个类别样本几万张。火星岩石检测的数据,通常来自巡视器导航相机或者轨道器高分影像,一张图上岩石数量可能十几个,也可能上百个,密集堆叠,颜色和背景几乎融为一体。最大的问题不是“认不出岩石”,而是“分不清边界”——很多碎石和地形起伏本身就长得像岩石。我用的训练数据来自某火星模拟场景数据集,图像尺寸在 1024x1024 左右,标注了岩石的矩形框。总共只有三千多张,其中有标注的岩石目标大概两万个。这点量放到通用检测里根本不够看,所以必须在模型结构、数据增强、损失函数上想办法,而不是单纯堆数据。另外一个特点是风格偏移严重——白天光照角度不同,同一块区域的岩石阴影完全不同。如果不做针对性的数据增强,模型很容易把光照条件当成特征去记忆,换一个视角就掉点。1.2 任务指标怎么定火星岩石检测最终要服务于两个场景:一个是巡视器自主避障,要求“漏检率低”,漏掉一块大石头可能直接翻车;另一个是科学目标统计,比如统计岩石分布密度,要求“误检率低”,把地形起伏标成岩石会直接带偏地质分析。我同时用了两个评价维度:标准COCO的AP(平均精度)和岩石尺度分组的AP。还有一个自定义指标——关键尺度召回率(重点看中等尺度岩石是否稳定检出)。因为最终要部署到边缘设备,模型参数量和推理速度也纳入评估,不能只看精度。2. 模型选型拆解:SABL-RetinaNet-R50-GN-FPN强在哪2.1 RetinaNet与Focal Loss:先解决正负样本失衡岩石检测任务里,一张图可能就有上万个anchor,但真正匹配到岩石的只有几十个,正负样本比轻松超过1:1000。经典Faster R-CNN的RPN虽然有采样策略,但还是会因为分类分支的简单负样本过多,导致训练效率低下。RetinaNet的核心武器是Focal Loss,它在交叉熵损失上加了调制因子,让模型把注意力集中在难分样本上。公式不背了,直观理解就是:如果某个anchor预测的背景概率已经很高(简单负样本),损失就被打一个很低的折扣;预测置信度低、容易分错的样本,损失权重就大。这样训练出来的检测头不会早早“躺平”在背景上。在火星岩石数据上,Focal Loss带来了肉眼可见的收益。我用标准Faster R-CNN在同样数据下训练,最终mAP大概要低4个点以上,而且中等感受野的岩石漏检特别严重。2.2 SABL检测头:边界定位用边界感知的方式玩SABL全称是Side-Aware Boundary Localization,这个head设计很巧妙。传统检测框回归是直接预测框的坐标或者中心点偏移,但在岩石目标上,框的四条边受遮挡、阴影影响不对称,直接回归整体坐标往往顾此失彼。SABL的做法是:把bbox回归拆解成多个边界侧面的独立定位,每条边提取相应的“侧感知特征”,再分别回归。这样左边界、右边界、上边界、下边界可以各自“关注”局部纹理和边缘信息。特别适合岩石这种纹理杂乱、边界不太明确的目标。还有个细节,SABL引入了IoU预测分支或者定位置信度?实际结构中,它用了一个高质量建议框采样的策略,让检测框和分类置信度的关联更强。评测下来,使用SABL头后,定位的AP(AP75)提升明显,比标准RetinaNet头大概高2.5个点。这不是玄学,是结构带来的。2.3 为什么是ResNet50-GN,而不是BN很多人在做检测时默认用BatchNorm,但在火星岩石这种数据量不大、训练batch size受限的场景下,BN很容易出问题。BN的统计量依赖batch内样本,如果batch size只有4或者2,统计量波动非常大,模型训起来不稳定,跑测试时还会出现训练和推理统计口径不一致的问题。GN(GroupNorm)把通道分成若干个组,在组内做归一化,完全不受batch size影响。这里选ResNet50-GN,就是把ResNet50中所有BN层替换成GN,同时按照原论文结构把通道做了相应调整。实际训练时我故意把batch size设为4,GN全程没有出现BN那种loss震荡。FPN部分不用多说,多尺度特征金字塔让8x、16x、32x甚至更大的感受野特征都能参与检测。火星岩石尺度跨度很大,小的几十像素,大的占图一半,FPN是必须的。配合GN,整个backbone+neck在边缘设备上也能保持稳定的计算图。3. 训练流程详解:从数据到配置到跑通3.1 数据准备与标注格式我用的标注格式是COCO json,主要是为了后续复用开源工具链。每张图的标注包括:image_id、file_name、width、height,以及一个annotations数组,每个框5个字段:bbox为[x, y, width, height]、category_id、area、iscrowd。因为是单类检测,category_id统一为1。火星岩石图像尺寸不是固定的,我统一resize到1333x800,短边800,长边不超过1333。这个resize策略在检测里很常见,但它对岩石任务有个副作用:太小的岩石会被缩得更小,难以检测。所以我额外保留了原始图像分辨率差的版本,专门测试小目标。标注质量直接影响上限,我花了大量时间清洗标注:把两三个重叠岩石切成一个框的最难处理,我的标准是重合度超过70%但属于不同岩石的,分别标注为两个小框;紧挨在一起的,宁可框有重叠也不能漏。很多模型后期出现误检,都是因为训练标签本身就有歧义。3.2 数据增强:为了过拟合也要“针对性”增广三千张图对检测模型来说太少,我用了两套增强策略。第一套是通用增广:随机翻转、随机亮度对比度、随机缩放(尺度范围0.5到1.5)。第二套是专门模拟火星光照变化的增强:随机gamma变换、随机局部阴影遮挡块、轻微的模糊模拟大气扰动。随机遮挡块这个操作很有意思,把图像某些区域涂黑或者换成随机噪声,模型会更少依赖背景纹理,而更多关注岩石本身的边界。实测加了之后,不同光照条件下的精度稳定了很多。但增强也不能过度。有一次我加了随机旋转90度,结果模型把石头横竖搞混了,因为火星岩石大部分是椭球形,旋转90度后边界特征变了,反而破坏了物理先验。最后旋转只用了小角度(正负15度),效果才是正向的。3.3 核心环境与依赖版本训练环境用的是PyTorch生态,搭配MMDetection代码库。版本锁定很关键,我以实际能稳定复现的“全家桶”版本为例:Python 3.8PyTorch 1.10CUDA 11.3torchvision 0.11MMDetection 2.25为什么要强调版本?因为不同版本的MMDetection对配置项解析有差异,特别是SABL头的参数写法。直接在旧版本上跑新配置,会莫名其妙报KeyError。建议新建独立虚拟环境,不要动已有环境。3.4 模型配置文件要点训练SABL-RetinaNet时,最核心的配置就是模型头、backbone、训练超参数三块。我直接贴出我当时改过的关键片段(省略了无关部分):model = dict( type='SABLRetinaNet', backbone=dict( type='ResNet', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), frozen_stages=1, norm_cfg=dict(type='GN', num_groups=32, requires_grad=True), norm_eval=True, style='pytorch', init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50') ), neck=dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, start_level=0, add_extra_convs=True, num_outs=5 ), bbox_head=dict( type='SABLRetinaHead', num_classes=1, in_channels=256, stacked_convs=4, feat_channels=256, anchor_generator=dict( type='AnchorGenerator', octave_base_scale=4, scales_per_octave=3, ratios=[0.5, 1.0, 2.0], strides=[8, 16, 32, 64, 128] ), bbox_coder=dict( type='DeltaXYWHBBoxCoder', target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0] ), loss_cls=dict( type='FocalLoss', use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0 ), loss_bbox=dict(type='SmoothL1Loss', beta=0.11, loss_weight=1.0) ) )这里有几个需要细说的地方:octave_base_scale=4:这个值决定了anchor的基础大小。火星岩石如果普遍偏小,建议降到2或者2.5;如果目标中大型岩石很多,可以保持4。我用的是混合尺度,所以保持4。ratios=[0.5,1.0,2.0]:岩石外形不规则,但总体长宽比不会太极端,0.5到2.0够用。strides=[8,16,32,64,128]:FPN输出五层,对应最小anchor stride 8,最大128。loss_cls的alpha和gamma用默认0.25和2.0,在岩石任务上没出现不收敛情况,所以我没动。最关键的注意点:由于GN层对学习率更敏感,做迁移学习时backbone的frozen_stages我保留为1(冻结前1个stage,通常是stem和第一个residual block)。如果数据太少,可以冻结前2个stage,但代价是特征提取能力下降,需要实验取舍。训练超参数这块,我用了SGD优化器,momentum=0.9,weight_decay=0.0001,初始学习率0.005。为什么不是常见的0.01?因为我的batch size只有4(GN虽然不受batch影响,但整体数据量和batch小,学习率太大会导致分类头震荡)。这里可以算一下:经典线性缩放规则,batch 8学习率0.01,batch 4时按比例缩放就是0.005。实测下来很稳。学习率schedule用的2x训练模式,总共24个epoch,在16epoch和22epoch各衰减一次,因子0.1。先用500次迭代的warmup,warmup学习率从0.0001线性增长到0.005。warmup非常有必要,尤其是迁移了预训练权重后,如果不warmup,前面几百步loss会飙到10以上。3.5 训练命令与监控细节确认配置没问题后,直接用训练脚本开跑:python tools/train.py configs/sabl/sabl_retinanet_r50_gn_fpn_2x.py --work-dir ./work_dirs/sabl_r50_gn_rock训练时我同时开了TensorBoard。周期性地看几个关键曲线:loss_cls、loss_bbox、loss,以及每500次迭代输出的bbox_mAP。第一次跑的时候,前2000次迭代loss下降很快,从4.2降到0.9,但到了8000次迭代附近loss就平了,mAP维持在35左右。这是典型的学习率瓶颈期,等到16轮学习率衰减后,mAP直接从35跳到42。所以遇到loss平台期不用慌,先检查是不是还没到衰减点。训练日志里还有一个指标容易被忽略:grad_norm。如果它超过10,通常意味着学习率过高或数据有问题。我遇到过从某步开始grad_norm飙到50,最后发现是两个标注框坐标超出图像边界,导致回归目标计算出现离群值。清洗数据后这个问题就消失了。3.6 评估与推理训练结束后的评估我用tools/test.py,输出的不只是mAP,还有每个类别、每个尺寸范围的AP。对于岩石这个单类任务,我重点看:python tools/test.py configs/sabl/sabl_retinanet_r50_gn_fpn_2x.py work_dirs/sabl_r50_gn_rock/epoch_24.pth --eval bbox会输出类似这样的报告:Average Precision (AP) @[ IoU=0.50:0.75 ] = 0.472Average Precision (AP) @[ IoU=0.75 ] = 0.438Average Recall (AR) @[ IoU=0.50:0.75 ] = 0.512看到高IoU下的AP并不差,说明SABL的边界定位确实有效。我还根据岩石大小把AP拆开看:小目标(面积<1024)、中目标(1024到9216)、大目标(>9216)分别是多少。实测小目标AP只有21,中目标AP有48,大目标AP有56。这说明漏检主要集中在小于32x32的岩石上,后续应该提高输入分辨率或者增加anchor密度。4. 训练中常见问题与排查实录4.1 Loss不降或者NaN这是我被问得最多的问题。Loss不降,第一步不是调参,而是检查标签。我之前把每张图都可视化了一遍,发现某些框的坐标是负数,因为标注工具导出时把边界外的部分也算进去了。MMDetection对边界外回归很敏感,Loss直接变成NaN。解决办法:写一个小脚本把标注框clip到图像范围内,并且过滤掉宽高小于2像素的框。千万不要把这类“脏框”留着训练,模型会学到扭曲的边界。Loss不降的另一个常见原因:Focal Loss的alpha太大。如果alpha超过0.5,模型对正样本的关注被严重削弱,背景类别占主导,会出现loss低但不收敛的情况。建议从alpha=0.25、gamma=2.0的默认值起步。4.2 为什么我坚持用GN而不是BN我对比过同一套配置只把backbone的norm从GN换回BN,batch size=8。前几个epoch两者相差不大,但到了后期,BN模型的验证mAP波动特别大,同一份权重跑两次测试结果差将近3个点。原因是推理时的BN统计量是从训练时累计的running_mean/var算的,训练过程中batch小、统计量更新慢,最后存储的统计信息并不代表真实数据分布。GN就没有这个问题,它不依赖batch,训练和推理行为一致。对于火星岩石这种数据分布比较野的场景,这个稳定性比一两个点精度更重要。当然GN也有代价:计算量比BN略高,部署时有些加速库对GN支持不如BN好。但利大于弊。4.3 背景误检:把地形起伏当成了石头从第一次拿到可视化结果开始,我就发现模型特别喜欢把暗色区域的褶皱地形预测为岩石。原因很直接:岩石的灰度特征和周围地形太接近,模型学的其实是“暗色+椭圆形+局部高梯度”,但它无法区分这是石头还是阴影遮挡形成的假边界。这个问题的有效解法有两个,我同时用了:在数据增强中加入“负样本区域裁剪”,从没有标注岩石的纯地形图像中随机裁剪patch,拼接到训练图背景里,强制模型学习“地形不是岩石”。损失函数层面给分类分支加一点点难例挖掘权重。不过Focal Loss本身已经有难例挖掘功能,这个收益不如前者大。另一个技巧是调整检测阈值。推理时默认score_thr=0.05,会产生大量低置信度误检。我最终把阈值提高到0.4,误检减少了一半,代价是漏检稍微增加。这个要结合应用场景去定:避障场景阈值低一点,统计场景阈值高一点。4.4 多GPU训练的问题如果有条件多卡训练,用GN配置时要注意:MMDetection默认是DistributedDataParallel,每张卡上的loss都会单独反传,梯度聚合后更新。GN的好处在于即使总batch size只有8,每张卡batch=4,也不会像BN那样出现统计量漂移。但要注意samples_per_gpu不能设太小,建议每卡至少2张图,否则反向传播时归一化层前面的梯度可能不稳定。我不建议为了“看起来更厉害”强行上多卡。整个过程我甚至一半时间都在单卡上验证,模型稳定后才切到多卡。省心。5. 性能调优与效果分析5.1 一组有参考价值的对比实验我把跑过的几组结果放在一起(在同一训练集、验证集上,统一输入尺寸,训练轮次相同),给大家一个大概的基准线:模型配置验证集mAP单卡推理耗时(ms)参数量(M)Faster R-CNN R50-FPN0.4126241.5RetinaNet R50-FPN0.4585436.3SABL-RetinaNet R50-FPN0.4795838.7SABL-RetinaNet R50-GN-FPN0.4936038.7单卡是T4。可以看出来,SABL头带来了大概2个点的mAP提升,GN又带来1.5个点左右。这个组合在精度和速度之间达到了一个很好的平衡点。如果你做的是火星岩石检测方向,直接抄这个配置基本不会翻车。5.2 训练技巧:从35到49的调优路径第一版模型只能跑到35,后来我按下面步骤一步一步把它拉到了49:清洗标注框,过滤掉小于2像素和越界的框:mAP提升约1.5。加入光照扰动和随机遮挡增强:mAP提升约2。把backbone迁移学习的frozen_stages从2改成1:mAP提升约3。学习率从0.01降到0.005,配合batch=4:mAP提升约2。这个改动主要稳定了训练后期。把score阈值调整到0.3,虽然不改变mAP,但让最终输出的结果在业务上更好用。调参时不要一次改多个变量,我每次只动一个,记录下曲线变化,这样才知道哪个操作起了作用。5.3 部署与推理时的小心得模型训练完要部署到端侧时,有几个绕不开的坑。第一,GN层在部分推理框架(比如某些公司的NPU工具链)里映射效率不高,转ONNX时可能报不支持,建议先导出ONNX验证所有算子。第二,如果你用的是TensorRT,尽量用带有GN层支持的版本,否则就只能把GN换成BN重新训练,精度大概会掉1到2个点。第三,输入尺寸要跟训练一致,否则FPN的stride对齐会出问题。如果一定要在手机上跑,我建议把输入分辨率从1333x800降到1000x600,小目标AP会掉7个点左右,但推理速度能快一倍。根据你的任务目标取舍吧。6. 后记:一点个人经验这套SABL-RetinaNet-R50-GN-FPN配置我前前后后跑了超过两周,最大的感受是:在特定领域目标检测任务中,“构造先验”比“堆算力”重要得多。火星岩石检测难,难在背景干扰、目标尺度变化大、标注噪声高,而不是没有现成模型。SABL解决边界定位歧义,GN解决小批量稳定性,FPN解决尺度问题,Focal Loss解决样本不平衡,每一环都是对症下药。如果你也打算在自己数据集上复现,我唯一的建议是:先花一周时间把数据清洗干净,把可视化工具做好,再碰模型。这个顺序反了,后面调参就是盲人摸象。最后分享一个小技巧:训练完记得把验证集上误检最高的几十张图单独打印出来贴墙上看,比看任何loss曲线都有用。我就是在这些图上找到了“暗色褶皱=误检”的规律,才针对性加了负样本增广。模型只是工具,真正决定上限的还是你对任务本身的理解。
从最开始聊吧。前段时间我一直在做“火星表面岩石检测与识别”相关的算法验证,用的就是标题里这套组合:SABL-RetinaNet,骨干网络是ResNet50-GN,特征金字塔FPN,从头到尾把训练流程完整跑了一遍。这里不贴那种“一键训练”的营销文,我把模型选型逻辑、配置文件细节、训练过程中的真实数据变化、还有踩过的那些坑全部记录下来。如果你也在做遥感图像目标检测、行星表面地物识别,或者只是想把检测模型调得再稳一点,这篇应该能省你不少时间。先说结论:这套方案在火星表面岩石这种“背景复杂、目标尺度跨度大、样本相对少”的任务上,效果比标准的Faster R-CNN和原始RetinaNet都要稳。SABL这个检测头功不可没,GN归一化在小batch下也真的是救命。下面一层层拆开讲。1. 项目背景与任务定义1.1 为什么火星岩石检测不能照搬通用目标检测火星表面岩石检测,本质上是一个目标检测任务,但它的数据特性和通用物体检测差别非常大。通用检测任务里,一张图可能有几个到几十个物体,类别多,每个类别样本几万张。火星岩石检测的数据,通常来自巡视器导航相机或者轨道器高分影像,一张图上岩石数量可能十几个,也可能上百个,密集堆叠,颜色和背景几乎融为一体。最大的问题不是“认不出岩石”,而是“分不清边界”——很多碎石和地形起伏本身就长得像岩石。我用的训练数据来自某火星模拟场景数据集,图像尺寸在 1024x1024 左右,标注了岩石的矩形框。总共只有三千多张,其中有标注的岩石目标大概两万个。这点量放到通用检测里根本不够看,所以必须在模型结构、数据增强、损失函数上想办法,而不是单纯堆数据。另外一个特点是风格偏移严重——白天光照角度不同,同一块区域的岩石阴影完全不同。如果不做针对性的数据增强,模型很容易把光照条件当成特征去记忆,换一个视角就掉点。1.2 任务指标怎么定火星岩石检测最终要服务于两个场景:一个是巡视器自主避障,要求“漏检率低”,漏掉一块大石头可能直接翻车;另一个是科学目标统计,比如统计岩石分布密度,要求“误检率低”,把地形起伏标成岩石会直接带偏地质分析。我同时用了两个评价维度:标准COCO的AP(平均精度)和岩石尺度分组的AP。还有一个自定义指标——关键尺度召回率(重点看中等尺度岩石是否稳定检出)。因为最终要部署到边缘设备,模型参数量和推理速度也纳入评估,不能只看精度。2. 模型选型拆解:SABL-RetinaNet-R50-GN-FPN强在哪2.1 RetinaNet与Focal Loss:先解决正负样本失衡岩石检测任务里,一张图可能就有上万个anchor,但真正匹配到岩石的只有几十个,正负样本比轻松超过1:1000。经典Faster R-CNN的RPN虽然有采样策略,但还是会因为分类分支的简单负样本过多,导致训练效率低下。RetinaNet的核心武器是Focal Loss,它在交叉熵损失上加了调制因子,让模型把注意力集中在难分样本上。公式不背了,直观理解就是:如果某个anchor预测的背景概率已经很高(简单负样本),损失就被打一个很低的折扣;预测置信度低、容易分错的样本,损失权重就大。这样训练出来的检测头不会早早“躺平”在背景上。在火星岩石数据上,Focal Loss带来了肉眼可见的收益。我用标准Faster R-CNN在同样数据下训练,最终mAP大概要低4个点以上,而且中等感受野的岩石漏检特别严重。2.2 SABL检测头:边界定位用边界感知的方式玩SABL全称是Side-Aware Boundary Localization,这个head设计很巧妙。传统检测框回归是直接预测框的坐标或者中心点偏移,但在岩石目标上,框的四条边受遮挡、阴影影响不对称,直接回归整体坐标往往顾此失彼。SABL的做法是:把bbox回归拆解成多个边界侧面的独立定位,每条边提取相应的“侧感知特征”,再分别回归。这样左边界、右边界、上边界、下边界可以各自“关注”局部纹理和边缘信息。特别适合岩石这种纹理杂乱、边界不太明确的目标。还有个细节,SABL引入了IoU预测分支或者定位置信度?实际结构中,它用了一个高质量建议框采样的策略,让检测框和分类置信度的关联更强。评测下来,使用SABL头后,定位的AP(AP75)提升明显,比标准RetinaNet头大概高2.5个点。这不是玄学,是结构带来的。2.3 为什么是ResNet50-GN,而不是BN很多人在做检测时默认用BatchNorm,但在火星岩石这种数据量不大、训练batch size受限的场景下,BN很容易出问题。BN的统计量依赖batch内样本,如果batch size只有4或者2,统计量波动非常大,模型训起来不稳定,跑测试时还会出现训练和推理统计口径不一致的问题。GN(GroupNorm)把通道分成若干个组,在组内做归一化,完全不受batch size影响。这里选ResNet50-GN,就是把ResNet50中所有BN层替换成GN,同时按照原论文结构把通道做了相应调整。实际训练时我故意把batch size设为4,GN全程没有出现BN那种loss震荡。FPN部分不用多说,多尺度特征金字塔让8x、16x、32x甚至更大的感受野特征都能参与检测。火星岩石尺度跨度很大,小的几十像素,大的占图一半,FPN是必须的。配合GN,整个backbone+neck在边缘设备上也能保持稳定的计算图。3. 训练流程详解:从数据到配置到跑通3.1 数据准备与标注格式我用的标注格式是COCO json,主要是为了后续复用开源工具链。每张图的标注包括:image_id、file_name、width、height,以及一个annotations数组,每个框5个字段:bbox为[x, y, width, height]、category_id、area、iscrowd。因为是单类检测,category_id统一为1。火星岩石图像尺寸不是固定的,我统一resize到1333x800,短边800,长边不超过1333。这个resize策略在检测里很常见,但它对岩石任务有个副作用:太小的岩石会被缩得更小,难以检测。所以我额外保留了原始图像分辨率差的版本,专门测试小目标。标注质量直接影响上限,我花了大量时间清洗标注:把两三个重叠岩石切成一个框的最难处理,我的标准是重合度超过70%但属于不同岩石的,分别标注为两个小框;紧挨在一起的,宁可框有重叠也不能漏。很多模型后期出现误检,都是因为训练标签本身就有歧义。3.2 数据增强:为了过拟合也要“针对性”增广三千张图对检测模型来说太少,我用了两套增强策略。第一套是通用增广:随机翻转、随机亮度对比度、随机缩放(尺度范围0.5到1.5)。第二套是专门模拟火星光照变化的增强:随机gamma变换、随机局部阴影遮挡块、轻微的模糊模拟大气扰动。随机遮挡块这个操作很有意思,把图像某些区域涂黑或者换成随机噪声,模型会更少依赖背景纹理,而更多关注岩石本身的边界。实测加了之后,不同光照条件下的精度稳定了很多。但增强也不能过度。有一次我加了随机旋转90度,结果模型把石头横竖搞混了,因为火星岩石大部分是椭球形,旋转90度后边界特征变了,反而破坏了物理先验。最后旋转只用了小角度(正负15度),效果才是正向的。3.3 核心环境与依赖版本训练环境用的是PyTorch生态,搭配MMDetection代码库。版本锁定很关键,我以实际能稳定复现的“全家桶”版本为例:Python 3.8PyTorch 1.10CUDA 11.3torchvision 0.11MMDetection 2.25为什么要强调版本?因为不同版本的MMDetection对配置项解析有差异,特别是SABL头的参数写法。直接在旧版本上跑新配置,会莫名其妙报KeyError。建议新建独立虚拟环境,不要动已有环境。3.4 模型配置文件要点训练SABL-RetinaNet时,最核心的配置就是模型头、backbone、训练超参数三块。我直接贴出我当时改过的关键片段(省略了无关部分):model = dict( type='SABLRetinaNet', backbone=dict( type='ResNet', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), frozen_stages=1, norm_cfg=dict(type='GN', num_groups=32, requires_grad=True), norm_eval=True, style='pytorch', init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50') ), neck=dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, start_level=0, add_extra_convs=True, num_outs=5 ), bbox_head=dict( type='SABLRetinaHead', num_classes=1, in_channels=256, stacked_convs=4, feat_channels=256, anchor_generator=dict( type='AnchorGenerator', octave_base_scale=4, scales_per_octave=3, ratios=[0.5, 1.0, 2.0], strides=[8, 16, 32, 64, 128] ), bbox_coder=dict( type='DeltaXYWHBBoxCoder', target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0] ), loss_cls=dict( type='FocalLoss', use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0 ), loss_bbox=dict(type='SmoothL1Loss', beta=0.11, loss_weight=1.0) ) )这里有几个需要细说的地方:octave_base_scale=4:这个值决定了anchor的基础大小。火星岩石如果普遍偏小,建议降到2或者2.5;如果目标中大型岩石很多,可以保持4。我用的是混合尺度,所以保持4。ratios=[0.5,1.0,2.0]:岩石外形不规则,但总体长宽比不会太极端,0.5到2.0够用。strides=[8,16,32,64,128]:FPN输出五层,对应最小anchor stride 8,最大128。loss_cls的alpha和gamma用默认0.25和2.0,在岩石任务上没出现不收敛情况,所以我没动。最关键的注意点:由于GN层对学习率更敏感,做迁移学习时backbone的frozen_stages我保留为1(冻结前1个stage,通常是stem和第一个residual block)。如果数据太少,可以冻结前2个stage,但代价是特征提取能力下降,需要实验取舍。训练超参数这块,我用了SGD优化器,momentum=0.9,weight_decay=0.0001,初始学习率0.005。为什么不是常见的0.01?因为我的batch size只有4(GN虽然不受batch影响,但整体数据量和batch小,学习率太大会导致分类头震荡)。这里可以算一下:经典线性缩放规则,batch 8学习率0.01,batch 4时按比例缩放就是0.005。实测下来很稳。学习率schedule用的2x训练模式,总共24个epoch,在16epoch和22epoch各衰减一次,因子0.1。先用500次迭代的warmup,warmup学习率从0.0001线性增长到0.005。warmup非常有必要,尤其是迁移了预训练权重后,如果不warmup,前面几百步loss会飙到10以上。3.5 训练命令与监控细节确认配置没问题后,直接用训练脚本开跑:python tools/train.py configs/sabl/sabl_retinanet_r50_gn_fpn_2x.py --work-dir ./work_dirs/sabl_r50_gn_rock训练时我同时开了TensorBoard。周期性地看几个关键曲线:loss_cls、loss_bbox、loss,以及每500次迭代输出的bbox_mAP。第一次跑的时候,前2000次迭代loss下降很快,从4.2降到0.9,但到了8000次迭代附近loss就平了,mAP维持在35左右。这是典型的学习率瓶颈期,等到16轮学习率衰减后,mAP直接从35跳到42。所以遇到loss平台期不用慌,先检查是不是还没到衰减点。训练日志里还有一个指标容易被忽略:grad_norm。如果它超过10,通常意味着学习率过高或数据有问题。我遇到过从某步开始grad_norm飙到50,最后发现是两个标注框坐标超出图像边界,导致回归目标计算出现离群值。清洗数据后这个问题就消失了。3.6 评估与推理训练结束后的评估我用tools/test.py,输出的不只是mAP,还有每个类别、每个尺寸范围的AP。对于岩石这个单类任务,我重点看:python tools/test.py configs/sabl/sabl_retinanet_r50_gn_fpn_2x.py work_dirs/sabl_r50_gn_rock/epoch_24.pth --eval bbox会输出类似这样的报告:Average Precision (AP) @[ IoU=0.50:0.75 ] = 0.472Average Precision (AP) @[ IoU=0.75 ] = 0.438Average Recall (AR) @[ IoU=0.50:0.75 ] = 0.512看到高IoU下的AP并不差,说明SABL的边界定位确实有效。我还根据岩石大小把AP拆开看:小目标(面积<1024)、中目标(1024到9216)、大目标(>9216)分别是多少。实测小目标AP只有21,中目标AP有48,大目标AP有56。这说明漏检主要集中在小于32x32的岩石上,后续应该提高输入分辨率或者增加anchor密度。4. 训练中常见问题与排查实录4.1 Loss不降或者NaN这是我被问得最多的问题。Loss不降,第一步不是调参,而是检查标签。我之前把每张图都可视化了一遍,发现某些框的坐标是负数,因为标注工具导出时把边界外的部分也算进去了。MMDetection对边界外回归很敏感,Loss直接变成NaN。解决办法:写一个小脚本把标注框clip到图像范围内,并且过滤掉宽高小于2像素的框。千万不要把这类“脏框”留着训练,模型会学到扭曲的边界。Loss不降的另一个常见原因:Focal Loss的alpha太大。如果alpha超过0.5,模型对正样本的关注被严重削弱,背景类别占主导,会出现loss低但不收敛的情况。建议从alpha=0.25、gamma=2.0的默认值起步。4.2 为什么我坚持用GN而不是BN我对比过同一套配置只把backbone的norm从GN换回BN,batch size=8。前几个epoch两者相差不大,但到了后期,BN模型的验证mAP波动特别大,同一份权重跑两次测试结果差将近3个点。原因是推理时的BN统计量是从训练时累计的running_mean/var算的,训练过程中batch小、统计量更新慢,最后存储的统计信息并不代表真实数据分布。GN就没有这个问题,它不依赖batch,训练和推理行为一致。对于火星岩石这种数据分布比较野的场景,这个稳定性比一两个点精度更重要。当然GN也有代价:计算量比BN略高,部署时有些加速库对GN支持不如BN好。但利大于弊。4.3 背景误检:把地形起伏当成了石头从第一次拿到可视化结果开始,我就发现模型特别喜欢把暗色区域的褶皱地形预测为岩石。原因很直接:岩石的灰度特征和周围地形太接近,模型学的其实是“暗色+椭圆形+局部高梯度”,但它无法区分这是石头还是阴影遮挡形成的假边界。这个问题的有效解法有两个,我同时用了:在数据增强中加入“负样本区域裁剪”,从没有标注岩石的纯地形图像中随机裁剪patch,拼接到训练图背景里,强制模型学习“地形不是岩石”。损失函数层面给分类分支加一点点难例挖掘权重。不过Focal Loss本身已经有难例挖掘功能,这个收益不如前者大。另一个技巧是调整检测阈值。推理时默认score_thr=0.05,会产生大量低置信度误检。我最终把阈值提高到0.4,误检减少了一半,代价是漏检稍微增加。这个要结合应用场景去定:避障场景阈值低一点,统计场景阈值高一点。4.4 多GPU训练的问题如果有条件多卡训练,用GN配置时要注意:MMDetection默认是DistributedDataParallel,每张卡上的loss都会单独反传,梯度聚合后更新。GN的好处在于即使总batch size只有8,每张卡batch=4,也不会像BN那样出现统计量漂移。但要注意samples_per_gpu不能设太小,建议每卡至少2张图,否则反向传播时归一化层前面的梯度可能不稳定。我不建议为了“看起来更厉害”强行上多卡。整个过程我甚至一半时间都在单卡上验证,模型稳定后才切到多卡。省心。5. 性能调优与效果分析5.1 一组有参考价值的对比实验我把跑过的几组结果放在一起(在同一训练集、验证集上,统一输入尺寸,训练轮次相同),给大家一个大概的基准线:模型配置验证集mAP单卡推理耗时(ms)参数量(M)Faster R-CNN R50-FPN0.4126241.5RetinaNet R50-FPN0.4585436.3SABL-RetinaNet R50-FPN0.4795838.7SABL-RetinaNet R50-GN-FPN0.4936038.7单卡是T4。可以看出来,SABL头带来了大概2个点的mAP提升,GN又带来1.5个点左右。这个组合在精度和速度之间达到了一个很好的平衡点。如果你做的是火星岩石检测方向,直接抄这个配置基本不会翻车。5.2 训练技巧:从35到49的调优路径第一版模型只能跑到35,后来我按下面步骤一步一步把它拉到了49:清洗标注框,过滤掉小于2像素和越界的框:mAP提升约1.5。加入光照扰动和随机遮挡增强:mAP提升约2。把backbone迁移学习的frozen_stages从2改成1:mAP提升约3。学习率从0.01降到0.005,配合batch=4:mAP提升约2。这个改动主要稳定了训练后期。把score阈值调整到0.3,虽然不改变mAP,但让最终输出的结果在业务上更好用。调参时不要一次改多个变量,我每次只动一个,记录下曲线变化,这样才知道哪个操作起了作用。5.3 部署与推理时的小心得模型训练完要部署到端侧时,有几个绕不开的坑。第一,GN层在部分推理框架(比如某些公司的NPU工具链)里映射效率不高,转ONNX时可能报不支持,建议先导出ONNX验证所有算子。第二,如果你用的是TensorRT,尽量用带有GN层支持的版本,否则就只能把GN换成BN重新训练,精度大概会掉1到2个点。第三,输入尺寸要跟训练一致,否则FPN的stride对齐会出问题。如果一定要在手机上跑,我建议把输入分辨率从1333x800降到1000x600,小目标AP会掉7个点左右,但推理速度能快一倍。根据你的任务目标取舍吧。6. 后记:一点个人经验这套SABL-RetinaNet-R50-GN-FPN配置我前前后后跑了超过两周,最大的感受是:在特定领域目标检测任务中,“构造先验”比“堆算力”重要得多。火星岩石检测难,难在背景干扰、目标尺度变化大、标注噪声高,而不是没有现成模型。SABL解决边界定位歧义,GN解决小批量稳定性,FPN解决尺度问题,Focal Loss解决样本不平衡,每一环都是对症下药。如果你也打算在自己数据集上复现,我唯一的建议是:先花一周时间把数据清洗干净,把可视化工具做好,再碰模型。这个顺序反了,后面调参就是盲人摸象。最后分享一个小技巧:训练完记得把验证集上误检最高的几十张图单独打印出来贴墙上看,比看任何loss曲线都有用。我就是在这些图上找到了“暗色褶皱=误检”的规律,才针对性加了负样本增广。模型只是工具,真正决定上限的还是你对任务本身的理解。