单阶段实例分割新思路:EmbedMask的嵌入耦合机制解析
直接说结论如果你觉得“单阶段实例分割”就是 YOLACT、SOLO 那一类方法的天下那么 EmbedMask 值得你再花半小时重新理解一遍。它没有把实例分割变成一个完全检测驱动的任务也没有回到两阶段那种“先框后分割”的串联老路而是把目标检测和目标掩码统一到同一个嵌入空间里用“嵌入耦合”的方式同时解决找目标、分实例、抠形状三件事。这篇博客我会沿着论文思路把前向流程、训练细节、损失设计、推理部署这些环节全部拆开讲顺带把我自己复现和调参时踩过的坑也放进来。标题里的“Embedding Coupling”翻译过来就是“嵌入耦合”很多人第一次看到这个名词会觉得玄实际上它就是论文最核心的动机给每个像素位置一个可学习的嵌入向量给每个候选实例也构造一个实例级嵌入掩码就是这两个嵌入做某种关联计算的产物。下面我按理解链条一步步展开。1. 为什么单阶段实例分割这么难——从两阶段范式说起1.1 两阶段范式“准是准就是重”要理解 EmbedMask 做了什么得先回到两阶段实例分割的老大难问题。以 Mask R-CNN 为代表的那套框架逻辑非常简单先用区域提议网络扫一遍图片找出几千个可能有物体的矩形框然后对每个矩形框做 RoIAlign把特征抠出来最后在这个固定大小的特征块上并行预测类别、边界框回归以及一个像素级的掩码。这个流程的优点是干净、直接每个阶段都有对应的监督训练收敛也稳定。但它的问题同样明显掩码分支依赖 RoI 的裁剪和池化ROI 一多计算量就成倍上涨。尤其在分辨率高或者目标密集的场景里每个目标都要单独走一次抠特征、上采样、算掩码的通路速度很难压下来。两阶段方法在精度上长期占据优势可工业场景里大家要的是一个“检测和分割同时出结果、延迟还要低”的方案两阶段这种重活并不适合所有场景。1.2 单阶段想省掉什么又补回什么单阶段实例分割的出发点很直接能不能不做区域提议不做 RoI 池化直接从密集特征图上一把输出“哪些像素属于哪个实例”早期的单阶段做法大多是让掩码生成依附在检测框上。比如 YOLACT 的做法是给每个检测框生成一组线性组合系数然后把它和一组原型掩码线性叠加最后裁剪到框内。这个思路把分割从“逐像素分类”变成了“系数组合”速度很快但受限于原型数量边界细粒度一般。SOLO 则是换了一个完全不同的角度把图像划分成网格每个网格负责预测一个实例掩码相当于用位置来编码实例归属。这套思路对中心型目标很好用但多个中心靠近的目标容易分不清。EmbedMask 介于两者之间主干上仍然保留密集检测器的设计但它不直接输出掩码也不做原型组合而是把掩码问题转成“同一个实例的嵌入应该互相靠近”的问题。换句话说它让网络先学一个嵌入空间再做解码这是它区别于前两类方法的关键。2. EmbedMask 的核心思路统一嵌入空间2.1 一张图两个分支一个空间EmbedMask 的网络结构可以粗略理解成三个子任务共享一套特征目标分类、边框回归、嵌入生成。前两个子任务和常见的单阶段检测器一致第三个子任务是整个方法的关键。具体来说从特征金字塔的每一层特征图出发EmbedMask 会并行接出两个分支我会把它们称为“检测分支”和“掩码嵌入分支”。检测分支输出的是每个位置属于某个类别的置信度以及该位置到目标四边的距离或者相对偏移这部分和 FCOS 这类 anchor-free 检测器非常接近。掩码嵌入分支则输出一张通道数为 D 的嵌入特征图。这张图上的每个像素都有一个 D 维向量它不直接告诉网络“你是前景还是背景”而是告诉网络“你和哪个实例在嵌入空间里是一伙的”。如果来自同一个目标实例的两个像素它们的嵌入向量应该很接近如果来自不同实例即使它们在原图上是相邻的像素嵌入向量也应该明显分离。这里的“统一”指的就是检测和分割用同一个嵌入空间做归一化匹配。检测分支找到了目标它会在目标中心或者采样点位置取出一个向量作为这个实例的“实例嵌入”掩码嵌入分支的像素嵌入和这个实例嵌入做关联计算就得到掩码。这是一个非常优雅的对称设计左边负责定义“谁是谁”右边负责回答“谁属于谁”。2.2 耦合到底是怎么发生的很多人看论文会卡在“耦合”这个词上其实它指的就是最后那步相似度计算。给你一个实例级嵌入向量 e尺寸是 D×1再给你一整张像素嵌入图 E尺寸是 H×W×D。那么每个像素的掩码分值可以写成先做逐像素点乘对每个像素把它对应的 D 维向量和实例嵌入 e 做内积得到一个标量。再用 sigmoid 或者近似函数把标量映射到 0 到 1 之间代表这个像素属于当前实例的概率。这就是“嵌入耦合”的计算本质。你可以把它理解成在嵌斥空间里查“谁离这个实例更近”算出来的值越大说明这个像素在嵌斥空间中与实例中心越聚拢就越应该被当成掩码的一部分。整个过程不需要对每个框重新跑一次前向网络也不需要裁剪 RoI本质上是矩阵运算GPU 执行起来非常友好。2.3 与 YOLACT / SOLO 最直观的三点区别我把三类方法放在一个表里对比看起来会更清楚。对比项YOLACTSOLOEmbedMask掩码如何产生原型掩码线性组合网格分类嵌入向量耦合实例归属靠什么检测框裁剪网格位置嵌入距离是否依赖锚框是否否重叠目标处理一般容易混淆取决于嵌入分离度掩码边界细腻度受限较高较高YOLACT 的问题在于它把掩码自由度限制在原型空间里原型数不够掩码就是“拼出来的”SOLO 则是把空间划分死板地绑定在网格上目标中心一旦与网格边界重合收敛就会打架EmbedMask 的嵌斥空间是连续可学习的它不需要枚举网格也不需要预定义原型这让它在实例重叠和形状复杂的目标上有明显优势。3. 前向流程逐层拆解3.1 骨干网络与 FPN 输入先讲输入侧。EmbedMask 的主干用经典的 ResNet 加 FPN 组合就够了实际复现时我用的是 ResNet-50 和 ResNet-101 各跑过一遍输出特征层一般取从 P3 到 P7 五层。P3 分辨率最高、感受野最小适合小目标P7 分辨率最低、感受野最大适合大目标。每一层都会独立进入检测分支和掩码嵌入分支这意味着嵌斥空间的尺度是金字塔式的不同尺度的目标在各自的层级分离。特征金字塔带来的一个问题是高层的语义信息未必能很好约束底层细节。为了避免底层只关心纹理而忽略实例归属在 EmbedMask 的训练里每一层 FPN 输出都会被强制约束到统一的语义嵌入空间而不是各学各的。这一点在实际跑代码时尤其重要如果只对最高层做掩码监督底层很容易学出一堆“边缘响应”而不是“实例响应”。3.2 embedding head 的具体结构我在复现时采用的嵌入分支是一个比较轻量的结构3×3 卷积加 1×1 卷积。3×3 卷积先把当前 FPN 层特征做一次局部融合1×1 卷积负责把融合后的特征投影到 D 维嵌入空间。D 一般取 128也有些实现取 64 或 256需要根据目标尺度和显存做取舍。D 越大嵌入空间的表达能力越强但过大的维度也会让训练集在有限样本下难以把同类拉近、异类推远容易过拟合。我这里建议优先从 D128 起步。需要特别提醒的是嵌入分支的输出一定要做归一化。如果不做归一化内积数值会随着向量模长失真模型很容易靠“谁模长更大”而不是“谁方向更一致”去判断相似性这样训练出来的嵌斥空间非常脆弱。实际做法是在计算耦合之前对嵌入向量做 L2 归一化把模长固定成单位长度让内积只反映余弦相似度。3.3 掩码解码的计算细节假设某张输入图像上的一个目标被检测分支锁定检测框的中心点落在 FPN 的某个位置那么这个位置对应的 D 维嵌入向量就被当成该实例的“实例嵌入”e。掩码嵌入分支对整张特征图计算出了像素级嵌入图 E。接下来要做的是两个操作第一构造一个所谓“全局 RoI”GRoI。这里的“全局”不是说用整张图做掩码而是指在计算掩码之前不在特征空间上做任何裁剪保留当前 FPN 层在原图尺度上对应的完整区域。这样做的意义在于像素嵌入能在全局上下文中判断归属而不是像两阶段方法那样只在小框里抠特征这能大幅减少边缘误判。第二把实例嵌入 e 与 GRoI 区域内的每一个像素嵌入做点乘得到掩码 logits 图。之后用一个固定阈值把 logits 二值化再经过简单后处理填充孔洞就可以了。整个过程等效于一次大规模矩阵乘加操作计算成本远低于每个目标单独跑一次动态卷积。这里有一个容易理解偏差的地方GRoI 并不是最终掩码的范围它只是计算掩码得分时使用的特征范围。最终掩码仍然会被裁剪到预测框内部因为实例不可能超出检测框太远。这和“不做 RoI 池化”并不矛盾裁剪是后处理池化是特征提取手段两者不是一回事。3.4 级联细化第二遍修正第一遍论文里还有一处值得说的设计是级联细化。第一遍耦合出来的掩码毕竟是直接从嵌入距离阈值得到的会存在两类问题一类是掩码边界过于软化靠近实例边缘的像素置信度不高另一类是掩码偶尔会“溢出”连着一段背景区域。为了修正这些问题EmbedMask 在粗掩码之后还会接一个轻量的细化分支做法是把第一遍得到的掩码 logits 和原始 FPN 特征拼接起来再过几个卷积层输出一个更锐利的边界预测。这个细化模块的参数量不大但对最终分割质量的提升非常明显尤其是在目标边缘细节要求高的场景下。训练时这个细化分支同样也会受到掩码损失的监督不存在“先冻结再修”的训练阶段割裂问题。这个设计本质上是在学一种“偏移修正”嵌入耦合负责从全局上把实例主体找出来细化分支负责把边界修正到像素级准确。前者侧重召回后者侧重精确。4. 训练阶段最容易踩的坑4.1 正负样本与全局 RoI 区域训练 EmbedMask 的过程中我先遇到的就是标签分配问题。检测分支的正负样本分配相对成熟可以直接沿用 anchor-free 检测器的中心采样策略同样特征层上预测位置落在某个目标框中心区域就算正样本否则视作负样本。但掩码分支的监督区域要小心。因为掩码嵌入分支是对全局 RoI 区域内的像素做预测如果直接用整个 GRoI 内所有像素做 loss那么绝大多数像素都是背景前景和背景的比例严重失衡模型可能只学会把所有位置都预测成背景。论文的处理思路是把掩码监督限制在正样本目标框内同时对框内的像素按照真实掩码分别打上正负标签。这里我建议再做一层平滑在目标框边缘保留一定范围的“忽略区域”这些像素不参与掩码 loss可以有效避免边界附近那个“到底是框内还是框外”的模糊问题。忽略区域太大损失了监督信息太小又起不到平滑作用我实测下来原图边界的 1 到 2 个像素是比较合理的范围。4.2 三个损失分别管什么权重怎么给EmbedMask 的总损失大致是三块。分类损失负责“这里有没有目标”边框损失负责“目标在哪里”掩码损失负责“目标长什么样”。三者必须同时训练但权重需要根据任务调整。分类损失我用的是 Focal Loss它对正负样本不均衡天然友好尤其适合单阶段检测这种“每层特征图都有大量负样本”的场景。边框损失用了 IoU Loss它比纯粹的平滑 L1 损失更贴合“框得准不准”的最终目标。掩码损失这里有两种选择一种是逐像素交叉熵一种是 Dice Loss。如果按默认权重走我建议的三块权重大致是分类 1.0、边框 1.0、掩码 1.5 到 2.0。掩码权重稍微加大是因为嵌入空间的学习本质上依赖掩码信号的反馈如果掩码权重太小嵌入分支就会被分类和回归带偏最后处理出来的嵌入区分度不够。如果数据集里小目标很多还可以把底层 FPN 的掩码损失权重再往上调一点。4.3 训练数据与超参选择数据增强上我建议保持和主流单阶段检测器一致随机水平翻转、随机缩放、随机裁剪都上但要特别注意尺度抖动范围。EmbedMask 对尺度比较敏感因为嵌斥空间的收敛依赖 FPN 不同层之间的协同如果增强分布过于极端小目标和大目标的嵌入向量很容易互相对齐反而让实例难以区分。训练超参方面我复现时的常用配置是输入短边 800 像素长边不超过 1333 像素初始学习率 0.01使用 SGD动量 0.9权重衰减 1e-4批大小 16 张图训练 24 万次迭代学习率在第 16 万和第 22 万次迭代处衰减。如果显存不够批大小减半的同时最好把学习率也按比例调低这种情况我实测下来效果比武力拉大学习率要稳定得多。还有一个容易被新手忽略的细节是嵌入维度初始化。嵌入分支最后一层的初始化方式对训练初期的稳定性影响很大如果初始化数值范围过宽一开始耦合出来的掩码 logits 会非常大sigmoid 直接饱和梯度消失。我通常会把最后一层卷积的权重初始化到标准差 0.01偏置设 0让初始 logits 控制在 0 附近。5. 推理与部署速度到底从哪来5.1 不需要动态生成的头部参数很多人喜欢拿 EmbedMask 和 CondInst 这类动态实例分割方法对比。CondInst 的思路是为每个实例生成一组动态卷积权重然后让这组权重在实例特征图上做卷积生成掩码EmbedMask 则是把掩码生成统一为嵌入耦合计算。这两种方法的共同点是不再用一个全局静态掩码头对整图预测不同点在于 EmbedMask 在推理阶段不需要为每个实例单独生成卷积核也不需要在实例之间切换计算图。我实际跑下来EmbedMask 在推理阶段的掩码生成成本几乎是一笔恒定的矩阵开销实例嵌入和特征嵌入做点乘随后是一次轻量细化。目标数量从几个增加到几十个掩码部分增加的计算量十分有限。这一点对视频流或自动驾驶这类对推断延迟敏感的场景有很强的吸引力。5.2 后处理流程推理时推荐的后处理流程大概是这样。先对每个特征层的分类分数做阈值筛选我习惯先取 0.05把低于阈值的预测直接丢点。在分数筛完的候选框中再结合掩码置信度做一次调整当然也可以通过掩码均值重新加权分类分数。接着做 NMSIoU 阈值设 0.5过滤掉重叠框。最后才把掩码 logits 二值化。有人会忍不住把二值化阈值当作超参去调其实我更建议保持阈值固定通过调整掩码损失和细化分支把 logits 分布校准好。因为二值化阈值不是一个能“优雅调优”的参数它太依赖经验挪一点都会让掩码面积剧烈变化。如果真觉得掩码偏大优先检查的是掩码损失权重和 GRoI 忽略了多少边界像素而不是硬调阈值。5.3 硬件上的实测对比以常用的 GPU 为参考在 ResNet-50 骨干、输入短边 800 的条件下EmbedMask 的推理速度大致能压到两阶段 Mask R-CNN 的 1.5 倍左右。掩码质量在中等分辨率目标上基本追平 Mask R-CNN只有几种极端细长目标和密集小目标场景会略逊一点。这个结论符合论文预期嵌入空间善于保持目标内部连续性但在极度依赖边界高频信息的目标上细化分支的修正能力还不够。曾经我所在的小组也做过一组对比实验把同一条分割链路分别接两阶段和 EmbedMask两阶段的平均精度略高但单帧延迟差出一倍多。如果你的业务目标是“质量允许小幅下降、延迟必须大幅下降”这个权衡通常是可以接受的。6. 一些进一步思考6.1 嵌入空间分离度到底够不够有人说嵌入耦合方法最大的隐患是“相似目标互相粘连”这点我部分认同。当两个同类目标紧贴在一起且外观高度接近时像素级嵌入很难分开它们因为它们在嵌斥空间中天然靠近。细化分支能修正边界却很难解决“两个目标本质上没有明确可分特征”的难题。我在实际使用中会用一个小 trick 缓解在掩码嵌入分支的监督里不仅要求正负像素区分开还额外加入一个对比约束让相邻但属于不同实例的嵌入向量相互推开。这样做会稍微增加实现复杂度但对重叠拥挤场景的掩码质量有明显改善代价是训练收敛会慢一些。6.2 与其他单阶段方法融合的可能EmbedMask 的思路并不封闭。它把掩码生成抽象成了“实例嵌入 全局嵌入耦合 细化”这意味着检测分支完全可以替换为其他更先进的检测器只要它能在每个目标中心或采样点给出一个可靠的实例嵌入。我把这个思路跟一个做边缘部署的朋友聊过他甚至把实例嵌入换成目标追踪的轨迹 ID embedding顺带解决了视频实例分割中“同一目标跨帧归属”的问题。另外原型掩码方法和嵌入方法也不一定对立。有人尝试把 EmbedMask 的耦合结果作为 YOLACT 原型空间的更高维输入相当于用嵌斥距离代替简单的线性组合系数虽然工程实现麻烦但确实给掩码生成提供了更连续的表达。总之单阶段实例分割还远没到“定式”的时候EmbedMask 最大的价值是它证明了检测和分割可以在一个空间里互相成就而不是永远走各自独立的流程。如果你正准备在自己的项目里引入单阶段实例分割我的建议是先别急着换主干、调 D 维数而是老老实实把嵌斥分支的训练稳定下来尤其是正样本区域和掩码损失的配合这一关过了后续所有优化才有意义。