028、解耦头 Decoupled Head:YOLOX到YOLOv8 分类与回归分家的梯度解耦原理
028、解耦头 Decoupled HeadYOLOX到YOLOv8 分类与回归分家的梯度解耦原理去年有个项目我在YOLOv5的检测头上硬塞了一个额外的分类分支想同时检测车辆和车牌。训练完一测召回率直接崩了——车倒是能检出但车牌框全飘到天上去了。当时我盯着loss曲线看了半天分类loss降得挺漂亮回归loss却像心电图一样乱跳。后来翻YOLOX的论文才明白问题出在共享参数上——分类和回归任务在同一个特征空间里打架梯度互相污染了。耦合头的历史包袱YOLOv3到YOLOv5检测头一直是个“大锅饭”结构。一个3×3卷积接两个1×1卷积分别输出分类和回归结果。这两个分支共享前面的特征提取层但任务目标完全不同分类关心“这是什么”回归关心“它在哪里”。分类任务对平移敏感度低一个猫不管在图像左边还是右边分类器都应该认出是猫回归任务恰恰相反偏移一个像素框的位置就得跟着变。这种矛盾在训练时表现为梯度冲突。分类分支的梯度告诉共享层“特征要更语义化”回归分支的梯度要求“特征要更位置化”。共享层夹在中间只能学一个折中方案——结果两个任务都做不好。我在YOLOv5上做的那个实验就是因为加了车牌分类分支后回归分支的梯度被严重稀释导致定位精度断崖式下跌。YOLOX的解耦头设计YOLOX的Decoupled Head把这个问题一刀切了。它把检测头拆成两条独立的流水线一条走分类一条走回归每条都有自己的3×3卷积和1×1输出层。两条分支完全独立不共享任何参数。具体实现上输入特征图先过一个1×1卷积降维到256通道这里有个细节YOLOX的neck输出通道是512降维是为了减少计算量。然后分叉分类分支接两个3×3卷积最后接1×1卷积输出类别数×8585是COCO的80类5个obj相关值回归分支同样接两个3×3卷积最后输出4个坐标值加1个objness。这里有个容易踩坑的地方——YOLOX的官方实现里分类分支的最后一个卷积输出通道是类别数回归分支输出4但objness是单独从回归分支输出的。我一开始没注意把objness放到了分类分支结果训练时分类loss和obj loss互相干扰收敛速度慢了一倍。别这样写objness本质上是回归任务——它判断的是“这个位置有没有目标”和坐标回归一样对位置敏感应该和回归分支共享特征。梯度解耦的数学直觉从梯度反向传播的角度看解耦头的优势很明显。假设共享层的参数为W分类分支的loss为L_cls回归分支的loss为L_reg。在耦合头中W的梯度是∂L_cls/∂W ∂L_reg/∂W。这两个梯度的方向和尺度可能完全不同——分类梯度可能指向“增强语义特征”回归梯度指向“增强位置特征”。如果两个方向夹角大于90度梯度更新就会互相抵消训练效率极低。解耦头把共享层拆成了两个独立的子网络W_cls和W_reg。每个子网络只接收对应任务的梯度互不干扰。这样分类分支可以放心地学语义特征回归分支专注学位置特征。实验数据也支持这个设计YOLOX在COCO上比YOLOv5 mAP高了3个点其中解耦头贡献了大约1.5个点。YOLOv6和YOLOv8的演进YOLOv6在解耦头基础上做了个改进——它把分类和回归分支的输入特征做了差异化处理。分类分支用更深的网络堆了更多3×3卷积回归分支保持浅层。理由是分类任务需要更强的语义抽象能力回归任务更依赖原始位置信息。这个设计在YOLOv6的论文里叫“Hybrid Decoupled Head”实际测试下来在目标尺度变化大的场景比如航拍图像效果提升明显。YOLOv8进一步简化了解耦头。它去掉了objness分支把分类和回归直接输出。分类分支输出类别数×1每个anchor点一个类别概率回归分支输出4×reg_maxreg_max是分布回归的bin数YOLOv8默认16。这里有个关键变化YOLOv8的回归分支不再输出具体的坐标值而是输出一个分布通过积分得到最终坐标。这种设计让回归任务变成了一个分类问题——预测坐标落在哪个bin里。解耦头在这里的优势更明显了因为分类分支和回归分支现在本质上都是分类任务但语义不同共享参数依然会冲突。实际部署的坑与经验解耦头虽然效果好但部署时有个头疼的问题——参数量和计算量增加了。YOLOX的解耦头比YOLOv5的耦合头多了大约30%的参数。在边缘设备上这个开销可能不可接受。我做过一个实验在Jetson Nano上跑YOLOX-S解耦头占了整个模型推理时间的15%。后来用TensorRT做层融合把两个3×3卷积合并成一个才把时间压下来。另一个经验是解耦头的初始化。分类分支的bias初始化要特别小心——YOLOX的做法是把分类分支最后一个卷积的bias初始化为负值比如-4.6这样初始时所有类别的预测概率都很低避免正负样本不平衡导致的训练崩溃。回归分支的bias初始化为0但坐标输出要乘以一个缩放因子比如0.01防止初始预测框太大导致loss爆炸。这些细节在官方代码里都有但很多人直接抄网络结构忽略了初始化结果训练三天loss不降。如果你要在自己的数据集上微调预训练模型建议保留解耦头的结构但把分类分支的输出类别数改成你的类别数。回归分支不用动因为坐标回归是通用的。微调时分类分支的学习率可以设大一点比如2倍回归分支保持默认。这样分类分支能快速适应新类别回归分支保留原有的定位能力。最后说个个人习惯——我每次改检测头结构都会在训练初期打印分类和回归loss的比值。如果比值大于10:1说明分类任务主导了梯度回归分支被压制了。这时候要么调loss权重要么检查解耦头是否真的解耦了比如是不是不小心共享了某个卷积层。这个比值在训练过程中应该稳定在3:1到5:1之间偏离太多就要警惕了。