
人脸检测是个老问题但每次训练配置都有人踩坑。最近我在整理一批历史训练实验时翻到了一行配置名基于RetinaNet的人脸检测_R101_FPN_8x8_AMP训练_200轮_COCO数据集_1。这个命名方式其实把一个实验的“配方”全写进去了——网络结构、骨干、多尺度策略、精度策略、训练轮数、数据格式、实验版本全都有。这篇内容我想把这份配置逐项拆开说说每个部分为什么这样选以及照这个配置跑训练时最容易在哪些地方翻车希望对正在做人脸检测项目的人有点参考价值。1. 逐项拆解配置RetinaNet、R101与FPN为什么这么搭1.1 RetinaNet人脸检测场景偏爱Focal Loss先说说RetinaNet。很多人对它的第一印象是“经典的一阶段检测器”但放到人脸检测这个具体场景里它有几个天生的优势。人脸检测的难点不是“找不找得到”而是“找得准不准、漏不漏”。尤其在一张多人合影里人脸有大有小、有正有侧还有互相遮挡背景里的非人脸区域又会贡献出大量“容易误判”的候选框。这种场景下一阶段检测器直接对密集anchor分类正负样本极其不平衡——负样本背景框可能占99%以上如果使用普通的交叉熵损失模型会被背景带偏。RetinaNet的核心是Focal Loss。这个损失函数做的事情很简单对于容易分类的样本不管它是正样本还是负样本都降低它的损失权重对于难分类的样本则提高它的损失权重。用人话说就是——模型已经学得很好的那些背景框别再给它们那么多“注意力”了把学习重心放到那些有挑战性的框上。我实际用过不少次Focal Loss在密集小脸场景下的体感非常明显。传统的交叉熵在大量负样本的包围下前几个epoch基本看不到有效训练信号而换用Focal Loss之后loss曲线下降得更平滑最终AP也更高。这背后的原因就是难易样本权重的再分配。当然代价也有Focal Loss自带两个超参数alpha和gamma。RetinaNet论文里常用alpha0.25、gamma2.0这个组合在多数目标检测场景下都够用。但人脸检测里如果你发现“漏检多”或者“误检多”可以分别微调这两个参数。我的经验是漏检多就稍微调低alpha或者调高gamma让难样本的权重更大误检多就反过来。这个后面第4节还会展开。1.2 R101骨干为什么不是R50也不是“越大越好”标题里的R101指的是ResNet-101。相比R50R101的特征提取能力更强尤其在中等尺寸和低分辨率人脸的表现上会有可见提升。但这里要说一个很多人容易误解的点R101不是用来“看得更远”的而是用来“看得更细”的。人脸检测里最麻烦的是小脸比如监控画面里远处的人一张脸可能只有十几个像素宽。这种目标光靠高层的语义特征是不够的因为多次下采样之后小脸的信息在高层特征图里已经被稀释得差不多了。R101的深度主要帮助中低层特征更丰富。配合FPN之后每一层特征金字塔都能“既懂语义、又保细节”。这也是为什么R101FPN在WIDER Face这类公开人脸基准上比“R50FPN”通常高一两个点的原因。那是不是直接上R152甚至更深不一定。人脸检测任务做到后期瓶颈往往不在骨干深度而在数据分布和anchor设计。更深的主干会显著提高训练时长和显存占用收益却不明显。我见过一些团队用R101FPN跑一个自建的数据集AP50在0.87左右换了更重的骨干只涨了0.2个点代价是训练时间翻倍。性价比不高。1.3 FPN在多人脸场景里做了什么FPNFeature Pyramid Network没有太多玄学它的本质是“把多个分辨率的特征图接在一起干活”。RetinaNet从P3到P7一共有五层特征图分辨率从高到低每一层负责不同尺度的目标。人脸检测里FPN的每一层都有明确的活干P3层分辨率最高感受野最小负责非常小的人脸。P4、P5层负责中等大小的人脸也是大多数自拍场景的主体。P6、P7层感受野大负责大尺度的近景脸和半身照。我在实际标注数据时经常发现不同标注员对人脸框的“松紧程度”理解不一样导致同一张图上P3层和P4层对同一目标的匹配结果不稳定。所以FPN调优的第一步不是改网络结构而是统一标注标准人脸框到底框到眉毛还是额头下巴和脸颊留多少边。这些差别会直接影响特征层分配。RetinaNetFPN还有一个隐藏优点因为每层anchor尺寸范围固定模型相当于被“分工约束”了——底层网络不会越权去预测超大脸高层网络也不会强行捕捉小脸。这让训练过程稳定不少也方便我们分析bad case时快速定位是哪一层出了问题。2. 数据侧的隐形门槛COCO格式的人脸框与类目处理2.1 “COCO数据集”到底指什么严格来说原始COCO数据集里并没有“人脸”这个类别它只有“人”的整体标注。所以标题里写“COCO数据集_1”大概率指的是“COCO注解格式的人脸数据”也就是把公开人脸数据集或自己标注的人脸框整理成COCO风格的JSON结构用COCO的评估工具来算AP、召回率这些指标。这种格式的优点是生态成熟。COCO格式的标注包含images、annotations、categories三个核心字段annotations里每条记录一个bbox格式是[x, y, width, height]注意这个框是左上角坐标加宽高不是中心点也不是[x1,y1,x2,y2]。我踩过一次坑有的标注工具导出的是两点式如果不转换直接用模型训练不会报错但评估时检测框对不上AP直接掉到0.2以下。如果你要自己整理数据我建议统一成COCO格式因为后续无论是用开源检测工具箱、还是自己写评估脚本都会省很多事。标题里这个“COCO数据集_1”的“_1”我理解为第一版数据或者第一次实验的配置版本号用来区分之后的数据增强策略和训练轮次改动。2.2 类别数只有一个但别在配置里写反人脸检测的类别数就是1这个看起来很简单但配置里很容易出问题。有些基于COCO预训练的模型默认是80类如果你直接微调最后分类头输出的形状对不上训练直接报错。需要把分类头的num_classes改成1同时注意分类头是否需要保留背景这一路。RetinaNet的一阶段结构里每个anchor会预测K个类别得分K为目标类别数背景通过阈值排除不需要单独占一个维度。但有些实现会写成“类别数1”比如把背景也算进去这时候K2。这两种写法没有绝对对错关键要跟损失函数和推理后处理逻辑保持一致。我在第一次复现时就在这里栽了跟头训练了20多轮才发现分类头输出维度不对白白浪费了时间。2.3 人脸框宽高比默认anchor策略要改COCO检测的默认anchor ratios一般是[0.5, 1.0, 2.0]因为通用目标有高有矮有宽有窄。但正常人脸的宽高比非常集中大约在0.7到1.3之间极少有那种极端的长条脸或横宽脸。如果你直接用默认的anchor ratios会导致大量anchor在形状上跟目标不匹配虽然FPN也能通过回归调整但回归的负担变大特别是在小脸区域反而更容易产生偏差。我把人脸检测里的anchor ratios改成[1.0, 1.5, 2.0]之后AP50大概涨了0.5到1个点。这不是固定的最优解具体取决于你的人脸框标注习惯但我建议先统计一下自己数据里所有GT框的宽高比分布再决定用哪组ratio。人脸检测数据集的标注通常比较统一统计出来的分布往往比较尖锐这会直接告诉你该用哪种anchor配置。3. 训练配置落地从命令行到AMP的精调过程3.1 一个可用的训练启动方案标题里最核心的训练策略信息是“8x8”和“AMP”。我结合常见实践把训练启动方案整理成下面这样具体框架不限于哪一种配置逻辑通用第一段是数据路径和类别设置。训练集和验证集的JSON路径分开写类别只有face。第二段是模型结构backbone用ResNet101neck用FPN输出五层特征图。第三段是训练迭代相关的参数epochs设为200batch size视显存而定。下面是一个PyTorch风格的关键代码示例# 训练启动示例简化逻辑 model RetinaNet( backboneresnet101, neckfpn, num_classes1, anchor_scales[8, 16, 32, 64, 128], anchor_ratios[1.0, 1.5, 2.0], feat_channels256, ) optimizer SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) # 启用AMP from torch.cuda.amp import GradScaler, autocast scaler GradScaler() for epoch in range(200): for images, targets in dataloader: optimizer.zero_grad() with autocast(): losses model(images, targets) scaler.scale(losses[total]).backward() scaler.step(optimizer) scaler.update()这里最值得注意的一点是梯度缩放器的位置。AMP并不是简单在forward时用半精度计算就行反向传播过程中小梯度可能低于float16能表示的最小值约6e-8梯度直接变成0。GradScaler的作用是先把loss放大一定的倍数完成反向传播后再缩放回去。如果你漏了scaler.step和scaler.update训练不会报错但模型大概率不收敛。3.2 学习率调度与batch size联动很多人一上来就把base_lr设成0.01这只是针对batch size约16的经验值。如果你因为显存限制把batch size改成8学习率也应当适当降低到0.005到0.008附近否则训到中途loss容易震荡。这个在标题里虽然没有直接写batch size但如果“8x8”指的是8卡每卡8张那总batch就是64学习率甚至可以调到0.04到0.08区间配合warmup会更稳。200轮这种长训练我一般不喜欢用阶梯式下降而是用余弦退火。阶梯式下降的问题是你在第120轮把lr降下来可能第121轮刚好遇到一个比较差的模型状态之后就一直沿着这个状态往下跑。余弦退火让学习率平滑变化长训练下更稳最终AP也通常更高一点。3.3 AMP不是“一键加速”有前提条件AMP的核心优势是显存占用低、训练速度快。对于检测模型而言卷积层和BatchNorm通常保留float32而其他大部分算子用float16。这样做的原因有两个卷积计算量大float16能显著降低显存带宽压力。BatchNorm在训练时需要统计均值和方差如果也用float16统计结果容易不稳定反而导致训练震荡。所以如果你用AMP后遇到loss异常不要第一时间赖AMP先检查有没有哪一层被强制转成了half特别是BN层。很多工具箱对“AMP模式”和“BN层是否自动转半精度”有各自的默认行为不同版本之间差异还挺大。另外多尺度训练配AMP是我比较推荐的组合。多尺度训练会频繁调整输入图像尺寸显存峰值很不稳定有了AMP之后整个batch可以塞得更大训练效率自然上去了。4. 我踩过的几个坑AMP精度、小脸漏检与长训练不稳定4.1 AMP训练中loss变成NaN的完整排查链路有一次我跑一个类似配置的实验前5个epoch一切正常到第6个epoch的时候loss突然变成NaN。很多人遇到这个问题都会先怀疑AMP但直接关掉AMP后问题依旧这说明问题不在AMP本身。我的排查链路是这样的第一步检查学习率。如果base_lr偏大加上warmup太短loss可能在前期出现一个尖峰后续再也回不来。我当时的配置里warmup只设了500步对200轮的长训练来说明显不足调整到2000步之后NaN没有再出现。第二步检查数据中是否有异常标注。比如某张图的bbox坐标出现负数或者width/height为0都会让回归损失变成无穷大。我写了一个简单脚本把所有GT框的坐标取值范围打出来发现确实有一条负坐标数据和两条空框数据。这些数据在训练初期影响不明显但一旦某个batch里它们集中出现loss就崩了。第三步检查是否用了较大的梯度裁剪值。RetinaNet对梯度裁剪不敏感但如果你设置了clip_grad_norm40这种较大的值在AMP下偶发的大梯度反而会破坏浮点稳定性。我把裁剪值降到10或者直接去掉训练恢复稳定。所以AMP本身其实很少是NaN的根因它更像一个“放大器”——数据或超参数里本来有的小问题在低精度下更容易暴露出来。4.2 小脸目标召回率低不要只调整anchor先看特征层分配小脸漏检是训练中最多人问的问题。我自己的经验是先画一个“GT框尺寸分布图”看看数据里到底有多少人脸是小于32×32的。如果小于32像素的人脸占比很低那P3层几乎学不到什么有效信息单纯增加小anchor数量只会带来更多误检。如果你的数据里确实有很多小脸可以做三件事增加输入图像分辨率让同一张人脸在特征图上占据更多像素。比如从800×1333提升到1000×1500。调整anchor scales把最小的一档从8降到4。在训练时做“随机裁剪放大”的增强让模型多看到“放大后的小脸”。我最推荐的是最后一种。它不改变网络结构不增加显存却能给小脸提供更多训练样本。配合多尺度训练来用小脸召回率的提升比单纯调anchor更明显。4.3 长训练的过拟合与评估波动200轮训练在自建数据集上很容易过拟合。我的判断标准是验证集AP还在涨但是训练集loss已经降到很低而且训练集和验证集之间的AP差距越来越大。这时候不要急着加权重衰减我通常优先检查数据增强是否太弱。人脸检测最常用的增强组合是随机翻转、随机裁剪、亮度对比度扰动。如果你的数据量只有几千张建议再加一个Mosaic或Cutout效果立竿见影。另外评估指标的选择也会影响你对“过拟合”的判断。人脸检测一般更看重AP50IoU阈值0.5下的平均精度因为人脸框标注本身有主观性0.5的阈值已经足够反映定位质量。如果只看AP50模型即使有一些框偏移分数也可能很高但如果同时看AP75过拟合和定位不稳的问题就会被放大。所以我建议在长训练时同时观察AP50和AP75两者差距过大时就要考虑是不是回归分支训练不充分。5. 训练完成之后评估、bad case与部署前的压缩5.1 用哪套指标判断人脸检测模型好不好我看到很多项目只看mAP但其实人脸检测里更该关注的是“每张图的人脸数”和“单张图刷新率”这个业务指标。比如一张聚合图里有100张脸AP再高如果模型最多只能同时输出80个框那在业务上就是无法接受的高漏检。所以训练完成之后我会分三步走跑一次COCO风格的评估输出AP50、AP75和AR平均召回率。写一个额外脚本统计每张测试图里GT框数量与检测框数量的差值找出“置信度高但框少”的图。把这些图可视化出来看是漏检还是误检。很多时候指标不好看不是模型问题而是后处理问题。NMS阈值太严格会压掉大量重叠候选框导致召回下降。人脸检测中同一张脸经常有多个高质量候选框NMS阈值从0.5调到0.6通常能提升不少召回。5.2 从bad case反推训练配置缺陷我会把bad case分成三类每一类对应不同的调优方向第一类是“框跑了”的检测框覆盖了人脸但明显偏移。这通常说明回归分支训练不足可以增加总损失里回归项的权重或者把回归损失的smooth L1换成IoU Loss——人脸框相对规则IoU Loss收敛速度很快。第二类是“多脸漏检”的相邻的人脸只检出一个。这往往是NMS把相邻的人脸候选框当成重复框压掉了可以尝试更低的置信度阈值再加Soft-NMS效果比较明显。第三类是“背景误检”的把帽子、背包、纹理图案当成脸。这类问题常见于训练数据里“长得像脸”的背景太少可以专门采集一些容易混淆的负样本加入训练。数据层面解决比调参数更有效。5.3 压缩和加速200轮训练出的模型如何落地训练完之后模型大概率要部署到某个线上环境。R101FPN在GPU上跑倒还好但如果要上CPU或者移动端就需要做两件事。第一件事是量化。AMP训练结束后模型权重通常是float32可以通过PTQ或者QAT量化到int8。因为模型在训练时已经适应了一定的动态范围量化损失通常可以控制在1个点左右。这是压缩模型的第一步也是最简单收益最大的一步。第二件事是蒸馏。用一个训练好的大模型做teacher蒸馏一个R50甚至更小的student模型。人脸检测的蒸馏关键点不在分类分支而在回归分支因为人脸框的回归输出相对集中蒸馏时可以用teacher的soft target来辅助student学习更平滑的边界预测。我实际做过一次teacher是R101student是R50蒸馏后AP50只掉了0.8个点但模型体积和推理延迟都明显下降。这就是训练阶段的成本没有白花。6. 最后一点个人经验这类标题里的配置看起来只是一串拼起来的词但它背后牵涉到数据格式、网络结构、损失设计、训练策略、评估指标和部署方案一整条链路。刚开始跑的时候我最容易犯的错误就是把默认配置当成“不用思考的东西”直接拿来就跑。后来发现真正花时间的其实都是在默认配置和实际数据之间寻找偏差的过程。根据我个人经验如果你准备复现这个配置优先做好三件事统一数据标注格式、确认anchor ratio与人脸形状匹配、把AMP的梯度缩放器和warmup设置好。这三件事做对了这个配置基本上就能稳定收敛到一个不错的水平。剩下的调优都是建立在跑通之后的bad case分析之上没必要一开始就追求极致。希望这份拆解对你有点帮助。如果你也在跑类似的训练欢迎一起交流实际踩坑的细节。