1. 从热力图到坐标:为什么需要DSNT这层“翻译”
做人体姿态估计、人脸关键点检测或者目标中心点定位的朋友,大概率都经历过这样一个尴尬时刻:模型输出的是一张热力图(heatmap),每个关键点对应一个高斯分布似的亮斑,训练的时候用MSE损失去拟合这张图,效果看着挺好,可一到推理阶段要输出具体坐标时,就得做argmax或者加权平均。argmax不可导,意味着你没法把坐标误差直接反传回去;加权平均虽然可导,但它在数值上并不等价于真正的坐标期望,尤其是当热力图存在多峰或者边缘截断时,算出来的坐标偏差能大到让你怀疑人生。
这就是**可微分空间到数值转换(Differentiable Spatial to Numerical Transform,简称DSNT)**要解决的核心问题。它做的事情说起来很纯粹:给定一张空间概率分布图(也就是热力图),用一种完全可微的方式计算出对应的数值坐标,并且让这个坐标的梯度能够顺畅地流回网络。这样一来,你就可以直接用坐标级别的损失函数(比如L2损失)来训练整个网络,而不必再依赖热力图级别的监督信号。
我第一次接触DSNT是在一个单人姿态估计的小项目里。当时用的是一个轻量级骨干网络输出14个关节点的热力图,训练损失用的是MSE,结果模型在验证集上的热力图看起来非常漂亮,但把坐标解码出来一算PCK(Percentage of Correct Keypoints),比预期低了将近8个百分点。排查了一圈才发现,问题出在解码方式上——argmax取的是峰值位置,而峰值位置受量化误差影响很大,尤其是当热力图分辨率低于输入图像分辨率时,一个像素的偏移映射回原图可能就是好几个像素的误差。后来换成DSNT,不仅坐标精度上去了,训练过程也更稳定,因为梯度直接作用在坐标上,网络学到的热力图更“聚焦”。
DSNT适合谁呢?如果你正在做以下任何一件事,它都值得你花时间了解一下:
- 人体姿态估计:需要输出每个关节的精确坐标,且希望端到端训练。
- 人脸对齐:68个关键点或者更密集的 landmarks,坐标精度要求高。
- 目标中心点检测:比如CenterNet这类框架,需要从热力图回归中心坐标。
- 任何需要从空间分布中提取数值坐标的任务:包括但不限于手势关键点、医学图像标志点定位。
它的核心价值在于:把“找最大值”这个不可微操作,替换成了一个可微的期望计算,并且通过一个温度参数和正则化项,让这个期望尽可能接近真正的峰值位置。下面我会从原理、实现、训练技巧、踩坑记录几个方面,把DSNT拆开揉碎讲清楚。
2. DSNT的数学本质:softmax期望与正则化如何配合
2.1 从热力图到坐标的两种传统路径及其缺陷
在DSNT出现之前,从热力图解码坐标主要有两条路:
第一条路是argmax。对每个通道的热力图,找到最大值所在的像素位置,然后把这个位置作为坐标输出。这种做法的问题很明显:argmax本身不可导,你没法把它嵌入到网络中间进行端到端训练。通常的做法是先用热力图损失训练网络,推理时再argmax,这就造成了训练目标和推理目标的不一致。更麻烦的是,argmax对噪声敏感,热力图稍微有点多峰,取到的峰值可能就跳到了错误的位置。
第二条路是soft-argmax。先对热力图做softmax归一化,然后计算每个像素坐标的加权和,也就是期望值。这个操作是可导的,梯度可以回传。但soft-argmax有一个隐藏的坑:softmax的温度参数如果设置不当,要么分布太“软”,期望值被周围像素拉偏;要么分布太“硬”,数值不稳定,梯度容易爆炸。而且soft-argmax没有显式的约束让期望值靠近峰值,它只是单纯地算了个期望。
DSNT在soft-argmax的基础上做了两处关键改进:一是引入了一个可学习的温度参数(或者叫缩放因子),让网络自己决定分布的“尖锐程度”;二是增加了一个正则化项,显式地鼓励热力图接近一个以目标坐标为中心的高斯分布。这两点结合起来,既保证了可微性,又让期望值尽可能接近真实峰值。
2.2 DSNT的完整计算流程
假设网络输出的热力图张量形状为(B, C, H, W),其中B是batch size,C是关键点数量,H和W是空间尺寸。DSNT对每个通道独立执行以下步骤:
第一步:空间softmax。把热力图的每个通道展平成H*W维向量,然后做softmax归一化,得到一个概率分布。这里可以引入一个温度参数T,实际计算的是softmax(logits / T)。T越小,分布越尖锐;T越大,分布越平滑。在原始论文中,T通常设为1,但也可以让网络自己学。
第二步:生成归一化坐标网格。创建两个与热力图同尺寸的矩阵,分别存储每个像素的x坐标和y坐标,并且归一化到[-1, 1]区间。比如对于宽度W,第j列的x坐标可以表示为2*j/(W-1) - 1。这样做的目的是让坐标输出与输入分辨率无关,便于后续映射回原图。
第三步:计算期望坐标。把softmax后的概率分布与坐标网格做加权求和,得到每个通道的期望x坐标和期望y坐标。公式上就是sum(p * x_grid)和sum(p * y_grid)。这一步是完全可微的,梯度会通过概率分布回传到热力图的logits上。
第四步:正则化损失。为了让期望坐标尽可能接近真实峰值,DSNT引入了一个正则化项,通常是计算热力图分布与目标高斯分布之间的JS散度(Jensen-Shannon divergence)或者KL散度。目标高斯分布以真实坐标为中心,方差可以设为固定值或者可学习。这个正则化项的作用是“塑形”,让热力图不要跑得太偏。
整个流程下来,最终输出的是每个关键点的(x, y)坐标,以及一个可以反向传播的损失。你可以直接用L2损失去监督坐标,也可以把坐标损失和热力图正则化损失加权组合。
2.3 为什么正则化项不能省
我见过不少人实现DSNT时只用了softmax期望,把正则化项去掉了,结果训练出来的模型坐标精度总是差那么一点。原因在于:如果没有正则化,softmax期望只要求“加权平均等于目标坐标”,但并没有约束分布的形状。网络完全可以学出一个双峰分布,只要两个峰对称地分布在目标坐标两侧,期望值照样正确。但这种分布在推理时非常脆弱,输入稍微扰动一下,两个峰的权重就失衡了,期望值立刻偏移。
加上正则化项之后,网络被鼓励输出单峰的高斯状分布,期望值不仅数值上接近目标,而且分布本身也是“健康”的。实测下来,加了正则化项的DSNT在PCK@0.05指标上通常能比不加的高出3到5个百分点,尤其是在小尺度关键点上提升更明显。
注意:正则化项的权重需要调。权重太大,热力图会被强行拉成固定方差的高斯,反而限制了网络的表达能力;权重太小,又起不到塑形作用。一般从0.1开始试,根据验证集坐标误差调整。
3. 把DSNT嵌入网络:层设计、坐标映射与损失组合
3.1 DSNT层的输入输出约定
在实际代码中,DSNT通常被实现为一个独立的层(或者模块),放在网络末端。它的输入是热力图张量,输出是坐标张量和正则化损失。以PyTorch为例,一个典型的DSNT层大概长这样:
import torch import torch.nn as nn import torch.nn.functional as F class DSNT(nn.Module): def __init__(self, temperature=1.0, reg_weight=0.1): super().__init__() self.temperature = temperature self.reg_weight = reg_weight def forward(self, heatmaps, target_coords=None): B, C, H, W = heatmaps.shape # 空间softmax flat = heatmaps.view(B, C, -1) / self.temperature prob = F.softmax(flat, dim=-1).view(B, C, H, W) # 生成归一化坐标网格 x_grid = torch.linspace(-1, 1, W, device=heatmaps.device) y_grid = torch.linspace(-1, 1, H, device=heatmaps.device) x_grid = x_grid.view(1, 1, 1, W).expand(B, C, H, W) y_grid = y_grid.view(1, 1, H, 1).expand(B, C, H, W) # 计算期望坐标 x = (prob * x_grid).sum(dim=(2, 3)) y = (prob * y_grid).sum(dim=(2, 3)) coords = torch.stack([x, y], dim=-1) # (B, C, 2) # 正则化损失 reg_loss = 0.0 if target_coords is not None: target_norm = self.normalize_coords(target_coords, H, W) target_dist = self.make_gaussian(target_norm, H, W) reg_loss = self.js_divergence(prob, target_dist) return coords, reg_loss * self.reg_weight这里有几个细节值得展开说。
温度参数的位置:我把温度放在softmax之前做除法,等价于对logits进行缩放。有些实现是放在softmax之后对概率做幂运算,数学上等价,但数值稳定性略有差异。放在之前更常见,因为可以直接复用F.softmax。
坐标网格的归一化范围:用[-1, 1]而不是[0, 1],主要是为了让坐标原点在图像中心,后续做旋转、缩放等几何变换时更方便。当然你用[0, 1]也没问题,只要前后一致就行。
正则化损失的计算:JS散度比KL散度更对称,数值也更稳定。计算时要注意两个分布都必须是概率分布,所以目标高斯也要做归一化。
3.2 坐标映射:从归一化空间回到原图
DSNT输出的坐标是在[-1, 1]范围内的归一化坐标,实际使用时需要映射回原图尺寸。映射公式很简单:
def denormalize(coords, orig_w, orig_h): # coords: (B, C, 2), 最后一维是(x, y) x = (coords[..., 0] + 1) / 2 * (orig_w - 1) y = (coords[..., 1] + 1) / 2 * (orig_h - 1) return torch.stack([x, y], dim=-1)这里有个容易踩的坑:热力图的空间尺寸和原图尺寸往往不一样。比如原图是256x256,经过骨干网络下采样后热力图可能是64x64。DSNT输出的归一化坐标是相对于热力图尺寸的,映射回原图时要用原图尺寸,而不是热力图尺寸。但如果你在计算正则化损失时用的目标坐标是相对于热力图的,那就要先做一次转换。我建议统一用归一化坐标做监督,这样无论热力图多大,目标坐标都是[-1, 1],省去了尺寸换算的麻烦。
3.3 损失函数组合:坐标损失与热力图正则化的平衡
DSNT的训练损失通常由两部分组成:
- 坐标损失:直接监督DSNT输出的坐标,用L2损失或者Smooth L1损失。这是主损失,权重设为1。
- 正则化损失:DSNT层内部计算出的JS散度,权重需要调。
我试过几种组合方式,下面这个配置在多个任务上表现比较稳:
| 损失项 | 类型 | 权重 | 作用 |
|---|---|---|---|
| 坐标损失 | Smooth L1 | 1.0 | 直接优化坐标精度 |
| 正则化损失 | JS散度 | 0.1~0.5 | 塑形热力图分布 |
| 热力图MSE | MSE | 0.0~0.1 | 可选,辅助监督 |
坐标损失用Smooth L1而不是纯L2,是因为L2对离群点太敏感,而关键点标注偶尔会有噪声。Smooth L1在误差小的时候等价于L2,误差大的时候等价于L1,更鲁棒。
正则化损失的权重我一般从0.1开始,如果发现热力图太“散”,就加到0.5;如果发现热力图过于尖锐导致梯度不稳定,就降到0.05。这个权重和温度参数是耦合的,温度低的时候分布本来就尖锐,正则化权重可以小一点;温度高的时候分布平滑,正则化权重需要大一点才能起到塑形作用。
提示:如果你用的是预训练骨干网络,刚开始训练时可以把正则化权重设小一点,让坐标损失主导,等网络初步收敛后再加大正则化权重。这样训练更稳定。
4. 训练动态与调参:温度、正则权重、学习率的联动关系
4.1 温度参数到底该固定还是可学习
DSNT原始论文里温度参数是固定的,通常设为1。但在实际项目中,我发现固定温度往往不是最优的。原因在于:不同关键点的空间不确定性不一样。比如人脸关键点中,眼角点的定位精度要求很高,热力图应该很尖锐;而脸颊轮廓点的定位本身就有一定模糊性,热力图可以稍微平滑一些。如果所有通道共用一个固定温度,网络很难同时满足这两种需求。
一种做法是让温度参数可学习,每个通道一个温度。这样网络可以自己决定每个关键点的分布尖锐程度。但可学习温度会带来一个问题:训练初期温度可能跑偏,导致softmax数值不稳定。我的经验是给温度加一个下界约束,比如T = max(learnable_T, 0.1),防止温度趋近于0导致梯度爆炸。
另一种做法是分阶段设置温度。训练初期用较大的温度(比如2.0),让分布平滑一些,梯度更稳定;训练后期逐渐降到0.5,让分布尖锐,坐标更精确。这种“温度退火”策略在多个项目上都有效,尤其是当训练数据标注噪声较大的时候。
4.2 正则化权重与温度的关系
前面提到正则化权重和温度是耦合的,这里展开说一下为什么。正则化项用的是JS散度,它衡量的是网络输出的分布与目标高斯分布之间的差异。目标高斯的方差通常是固定的(比如设为1个像素对应的归一化方差)。当温度较高时,网络输出的分布本身比较平滑,与目标高斯的差异主要体现在“峰值不够高”,JS散度会比较大,梯度信号强,正则化效果好。当温度较低时,网络输出已经很尖锐,与目标高斯的差异变小,JS散度自然就小,正则化项的影响也减弱。
所以如果你调低了温度,正则化权重可以保持不变甚至略微降低;如果你调高了温度,正则化权重需要相应加大,否则塑形效果不够。我一般会做一个简单的网格搜索:温度取[0.5, 1.0, 2.0],正则化权重取[0.05, 0.1, 0.5],在验证集上找最优组合。大多数情况下,温度=1.0,正则化权重=0.1是一个不错的起点。
4.3 学习率与优化器的选择
DSNT本身没有引入太多额外参数,所以优化器选择跟骨干网络保持一致就行。但有一点需要注意:坐标损失的梯度尺度与热力图损失的梯度尺度不同。如果你同时用了热力图MSE损失和坐标损失,两者的梯度可能会相互干扰。我的做法是先用热力图MSE预训练几个epoch,让热力图初步成型,然后切换到坐标损失为主、正则化为辅的训练模式。这样网络不会一开始就被坐标梯度带偏。
学习率方面,如果是从头训练,初始学习率可以设1e-3,用余弦退火或者阶梯下降。如果是微调预训练模型,学习率降到1e-4或者1e-5。我试过用AdamW优化器,权重衰减设1e-4,效果比纯Adam略好,尤其是当训练数据量不大的时候。
还有一个细节:DSNT输出的坐标是归一化的,数值范围在[-1, 1]之间,而热力图logits的数值范围可能大得多。如果你把坐标损失和热力图损失直接相加,坐标损失的梯度可能会被热力图损失的梯度淹没。解决办法是给坐标损失乘一个放大系数,或者把热力图损失乘一个缩小系数。我通常把热力图MSE损失的权重设为0.01到0.1之间,让坐标损失主导。
5. 实测中的坑:从梯度消失到坐标偏移的排查链路
5.1 热力图全黑或全白:softmax温度过低
第一次跑DSNT的时候,我遇到过一个诡异现象:训练了几个epoch之后,热力图可视化出来几乎全黑,只有一个像素是亮的。坐标输出倒是对的,但热力图的分布完全失去了空间信息。排查了一圈,发现是温度参数设得太低(我一开始设了0.01),导致softmax输出接近one-hot,梯度几乎为零,网络没法从坐标损失中得到有效的回传信号。
排查思路:先看热力图的可视化,如果发现分布过于尖锐,就检查温度参数。温度低于0.1时,softmax的梯度会变得非常小,因为指数函数的导数在饱和区趋近于零。解决办法是把温度调回1.0左右,或者给温度加一个下界。
修复方案:把温度设为可学习参数,初始值1.0,并加下界约束T >= 0.1。同时监控训练过程中温度的变化,如果温度持续下降,说明网络倾向于更尖锐的分布,这时候可以适当降低正则化权重,避免两者冲突。
5.2 坐标系统性偏移:坐标网格生成错误
另一个常见的坑是坐标网格生成时搞错了对齐方式。比如用torch.linspace(-1, 1, W)生成x坐标,这假设像素中心在-1和1之间均匀分布。但有些实现用的是torch.arange(W) / (W-1) * 2 - 1,两者在W较大时几乎一样,但在W较小时会有细微差异。更严重的是,如果你在生成网格时把x和y搞反了,或者expand的维度不对,坐标就会系统性偏移。
排查思路:写一个单元测试,构造一个已知峰值位置的热力图,比如在(H/2, W/2)处放一个高斯,然后看DSNT输出的坐标是否接近(0, 0)。如果偏移,就检查网格生成代码。
修复方案:统一用torch.linspace生成网格,并且确保x_grid的形状是(1, 1, 1, W),y_grid的形状是(1, 1, H, 1),expand到(B, C, H, W)。在计算期望时,prob * x_grid和prob * y_grid要分别求和。
5.3 训练后期坐标误差反弹:正则化权重过大
有一次我为了追求热力图的美观,把正则化权重设到了1.0,结果训练前期坐标误差下降很快,但到了后期反而反弹了。可视化热力图发现,分布确实很漂亮,单峰、对称、方差刚好,但坐标就是偏了那么一点点。原因是正则化项太强,网络为了迎合目标高斯分布,牺牲了坐标精度。目标高斯的中心是真实坐标,但网络输出的期望坐标与真实坐标之间总有微小偏差,正则化项把这个偏差“锁死”了,坐标损失没法把它拉回来。
排查思路:看训练曲线,如果坐标损失在下降到一定程度后开始上升,而正则化损失持续下降,那就是正则化权重过大了。
修复方案:降低正则化权重,或者让正则化权重随训练轮次衰减。我通常用余弦退火把正则化权重从0.5降到0.01,这样前期塑形,后期让坐标损失主导。
5.4 多关键点相互干扰:通道间softmax误用
DSNT要求对每个通道独立做softmax。如果你不小心对整个热力图张量做了softmax(比如F.softmax(heatmaps, dim=1)),那就变成了通道间的竞争,不同关键点的热力图会相互抑制。这个错误很隐蔽,因为训练损失可能还在下降,但坐标精度就是上不去。
排查思路:检查softmax的dim参数。正确的做法是把热力图reshape成(B, C, H*W),然后对最后一维做softmax,再reshape回(B, C, H, W)。
修复方案:确保softmax只在空间维度上进行,通道之间完全独立。可以在代码里加一行断言,检查softmax后的概率在每个通道上的和是否为1。
6. 从姿态估计到目标检测:DSNT的延展用法与边界
6.1 在多人姿态估计中的适配
DSNT最初是为单人姿态估计设计的,每个关键点对应一个通道,输出一组坐标。但在多人场景下,热力图会有多个峰值,DSNT的期望计算会把多个峰值的坐标平均掉,导致输出一个“中间位置”的坐标,这显然不对。
解决办法有两种:一是先用检测器把人框出来,对每个人单独跑DSNT;二是用Part Affinity Fields或者Associative Embedding先把关键点分组,再对每组单独做DSNT。我试过第一种方案,在一个人数不多的场景下效果很好,因为每个人的热力图区域被裁剪出来后,基本就是单峰分布,DSNT的期望计算是准确的。
第二种方案更优雅但实现复杂,需要对热力图做实例分割。如果你要做多人姿态估计,建议先从单人DSNT加检测框的方案入手,跑通了再考虑更复杂的端到端方案。
6.2 在目标检测中心点回归中的尝试
CenterNet这类框架用热力图预测目标中心点,推理时取峰值位置。如果把DSNT嵌入进去,就可以直接用坐标损失训练中心点回归。我做过一个实验:在同一个骨干网络上,一组用热力图MSE损失加argmax解码,另一组用DSNT加坐标损失。结果DSNT组的中心点定位误差比argmax组低了约15%,尤其是在目标尺寸较小的时候优势更明显。
但DSNT在目标检测中有一个限制:它假设每个通道最多一个目标。如果一张图里同一类别的目标有多个,热力图就会有多个峰值,DSNT的期望计算会失效。所以DSNT更适合“每类一个目标”的场景,比如人脸检测中的单张人脸,或者交通标志检测中每类标志只出现一次的情况。
6.3 DSNT的适用边界与替代方案
DSNT不是万能的,以下几种情况需要谨慎使用:
- 多峰分布:如前所述,DSNT的期望计算会把多个峰值平均掉。如果热力图天然是多峰的,DSNT不适用。
- 极端长宽比:坐标网格是矩形均匀分布的,如果目标区域是极端长宽比,期望计算可能会有偏差。这时候可以考虑用可变形卷积或者空间变换网络先做区域归一化。
- 需要输出多个候选:DSNT输出的是单一坐标,如果你需要输出多个候选位置(比如Top-K),DSNT不直接支持。可以结合NMS或者soft-argmax的变体来实现。
替代方案方面,如果你不需要可微性,argmax加后处理仍然是最简单直接的选择。如果你需要可微性但不想引入正则化项,纯soft-argmax也可以凑合用,只是精度会差一些。DSNT的价值在于它在可微性和精度之间找到了一个很好的平衡点,而且实现简单,几乎不增加计算量。
7. 一些实操心得与参数速查
7.1 初始化与训练节奏
DSNT层本身没有太多参数需要初始化,但温度参数如果可学习,初始值建议设为1.0。正则化权重建议从0.1开始,根据热力图可视化效果调整。训练节奏上,我通常分三个阶段:
- 预热阶段(前5到10个epoch):只用坐标损失,正则化权重设为0,让网络先学会输出大致正确的坐标。
- 塑形阶段(中间20到30个epoch):加入正则化损失,权重从0.1逐渐升到0.5,让热力图分布变得健康。
- 精调阶段(最后10到20个epoch):正则化权重逐渐降到0.01,坐标损失主导,微调坐标精度。
这个三阶段策略在多个项目上都能带来比固定权重更好的最终精度。
7.2 监控指标与可视化
训练过程中除了看损失曲线,建议定期可视化热力图和坐标。我通常每5个epoch保存一次验证集的热力图叠加图,把预测坐标和真实坐标都画上去。这样能直观地看到热力图是否在正确的位置聚焦,坐标是否在逐渐逼近真实值。
另外,可以监控温度参数的变化。如果温度持续下降,说明网络倾向于更尖锐的分布,这时候要注意梯度是否稳定。如果温度持续上升,说明网络觉得分布太平滑了,可能需要降低正则化权重。
7.3 常见参数速查表
| 参数 | 推荐范围 | 作用 | 调整方向 |
|---|---|---|---|
| 温度T | 0.5~2.0 | 控制分布尖锐程度 | 精度不够时降低,梯度不稳时升高 |
| 正则化权重 | 0.05~0.5 | 塑形热力图分布 | 热力图太散时升高,坐标反弹时降低 |
| 坐标损失权重 | 1.0 | 主监督信号 | 一般不动 |
| 热力图MSE权重 | 0.0~0.1 | 辅助监督 | 数据少时升高,数据多时设为0 |
| 学习率 | 1e-4~1e-3 | 优化步长 | 从头训练用大,微调用小 |
7.4 一个容易忽略的细节:坐标损失的归一化
DSNT输出的坐标是归一化的,范围在[-1, 1]。如果你直接用原图坐标做监督,损失值会很大,梯度也会很大。所以一定要把真实坐标也归一化到[-1, 1]再计算损失。归一化公式是2 * x / (W - 1) - 1,其中W是原图宽度。这个细节看起来简单,但我见过不止一个人在这里翻车,导致训练初期损失爆炸。
另外,如果你的数据集标注坐标是(x, y)格式,而DSNT输出的是(x, y),顺序要对齐。有些数据集用(row, col)也就是(y, x),如果不注意就会把x和y搞反,训练出来的模型坐标完全是错的。
8. 写在最后:DSNT给我的启发
DSNT这个技术本身并不复杂,核心思想就是“用可微的期望代替不可微的argmax”,但它在实际项目中的价值远超我的预期。它让我意识到,很多看似必须用不可微操作解决的问题,其实都可以通过巧妙的数学设计变成可微的。这种思路在深度学习里非常通用,比如可微的渲染、可微的采样、可微的排序,都是同一个哲学。
我在实际使用中最大的体会是:不要为了可微而可微,可微的目的是让训练目标和推理目标一致。DSNT之所以有效,是因为它让网络在训练时就直接优化坐标精度,而不是间接优化热力图。这种端到端的监督信号,比任何后处理技巧都来得实在。
如果你正在做关键点检测相关的项目,我强烈建议花半天时间把DSNT集成进去试试。代码量不大,但效果提升往往是立竿见影的。唯一需要注意的是,DSNT不是银弹,它有自己的适用边界,用之前先确认你的热力图是单峰分布,否则期望计算会带来意想不到的偏差。