YOLACT++实时实例分割:原型mask与系数组合的工程实践 1. 从Mask R-CNN的痛点说起为什么实时实例分割这么难做计算机视觉的同行大概都有体会实例分割这个任务在2019年之前一直处在一个很尴尬的位置。Mask R-CNN精度确实能打COCO榜单上常年霸榜但那个速度实在让人爱不起来——一张1080Ti上跑一张图要几百毫秒别说实时了连准实时都够呛。而另一条路线像FCIS、DeepMask这些基于proposal的方法速度稍好一些但精度又掉得厉害尤其是物体重叠、小目标密集的场景mask质量惨不忍睹。这个矛盾的本质在于实例分割需要同时解决在哪里和是什么形状两个问题。两阶段方法如Mask R-CNN先检测再分割精度高但步骤多、耗时长单阶段方法想一步到位但特征图上的每个位置既要负责分类又要负责回归mask信息耦合严重精度上不去。YOLACT系列就是冲着这个矛盾去的。它的核心思路非常巧妙把实例分割拆成两个可以并行执行的子任务——一个分支生成一组原型maskprototype masks另一个分支预测每个实例对这些原型的线性组合系数。最后把两者做矩阵乘法就得到了每个实例的mask。这个设计的好处是原型mask只需要计算一次所有实例共享而组合系数只是一个向量计算量极小。整个流程可以完全并行速度自然就上去了。YOLACT是在初代YOLACT基础上的改进版主要解决了三个问题一是原型mask的生成质量不够高二是检测分支的anchor设计不够高效三是训练时的mask监督信号不够强。下面我会从这几个角度逐一拆解把论文里的关键设计和背后的为什么讲清楚。2. 原型mask生成网络的设计取舍与改进细节2.1 为什么选择全卷积生成原型而不是逐实例生成YOLACT的原型生成分支是一个全卷积网络输入是骨干网络ResNet-101 FPN输出的特征图输出是k个原型mask每个原型的分辨率是原图的1/4。论文里k取32这个数字不是随便定的——太小了表达能力不够太大了计算量和内存都吃不消32是一个经过实验验证的平衡点。这里有个关键设计决策原型mask是类别无关的class-agnostic。也就是说这32个原型不针对任何特定类别它们只是捕捉图像中各种可能的形状和纹理模式。具体某个实例的mask是通过组合系数从这些原型里拼出来的。这个设计的好处是原型生成网络不需要关心类别信息可以专注于学习通用的形状表示泛化能力更强。我一开始看论文的时候有个疑问为什么不直接让网络输出每个实例的mask答案很简单——如果每个实例都要单独生成mask那计算量就爆炸了。假设一张图有100个实例每个实例的mask是128×128那就是100×128×128的输出而原型方案只需要32×128×128加上100×32的组合系数计算量差了将近两个数量级。2.2 原型mask的分辨率与上采样策略原型mask的分辨率是原图的1/4这个选择也值得说道。分辨率太高计算量上去了而且后续做mask裁剪和上采样的时候反而容易引入噪声分辨率太低小目标的mask细节就丢了。1/4是一个折中既保留了足够的空间细节又不至于让计算量失控。在YOLACT里原型生成分支用了FPN的P3层作为输入因为P3层的分辨率最高原图的1/8语义信息也足够丰富。然后通过几个卷积层和上采样操作把分辨率提到1/4。这里有个细节上采样用的是双线性插值而不是转置卷积原因是转置卷积容易产生棋盘格伪影而且参数量更大双线性插值虽然简单但效果更稳定。另外原型生成分支的最后一层用了ReLU激活函数保证原型mask的值非负。这个设计是有讲究的——因为后续的组合系数可以是正也可以是负如果原型本身有负值那组合出来的mask就可能出现奇怪的抵消效应。保证原型非负可以让组合过程更直观正系数就是加上这个形状负系数就是减去这个形状。2.3 原型mask的多样性约束论文里提到一个容易被忽略的细节如果没有额外的约束原型mask容易变得很相似因为网络会倾向于用少数几个原型来表达所有实例其他的原型就退化了。为了解决这个问题YOLACT在训练时加了一个多样性损失diversity loss鼓励不同的原型之间保持差异。具体来说这个损失计算的是原型之间的余弦相似度矩阵然后惩罚非对角线元素。也就是说如果两个原型太像了就罚它。这个损失的权重不大但效果很明显——加了之后原型mask的利用率明显提高mask质量也有提升。我在复现的时候试过不加这个损失结果确实如论文所说32个原型里有将近一半几乎是空的剩下的原型承担了所有工作。加了多样性损失之后原型的分工更明确有的负责圆形物体有的负责细长物体有的负责背景纹理整体mask的IoU能涨1-2个点。3. 组合系数预测与mask组装的计算逻辑3.1 检测分支如何同时输出类别、框和系数YOLACT的检测分支基于RetinaNet的结构但在每个anchor的输出上多了一个分支mask系数分支。也就是说对于每个anchor网络要输出四组信息类别置信度C维、边界框偏移量4维、mask组合系数k维k32。这三组信息共享同一个特征但各自有独立的卷积头。这里有个工程上的细节mask系数分支的输出维度是k但实际使用时只取对应类别的系数。也就是说如果一个anchor被判定为人那它只用那32个系数来组合原型生成人的mask。这个设计让系数分支不需要为每个类别单独预测系数大大减少了参数量。但这里有个潜在问题不同类别的实例可能共享相同的系数模式因为系数分支不知道类别信息。论文里的解决方案是在训练时对系数分支也施加分类损失让系数分支间接学到类别相关的信息。不过实测下来这个影响不大因为原型本身已经包含了足够的形状信息系数只需要负责选哪些原型和选多少。3.2 mask组装的矩阵乘法与裁剪mask组装的过程非常直接把原型mask矩阵k×H×W和系数向量N×k做矩阵乘法得到N×H×W的mask输出。这里N是实例数量H和W是原型的分辨率。这个操作在GPU上就是一个标准的批量矩阵乘法效率极高。但组装完之后还有一个关键步骤用预测的边界框对mask进行裁剪。因为原型mask是全局的组合出来的mask可能会超出实例的实际范围。裁剪的操作是把边界框外的mask值置零只保留框内的部分。这个步骤看似简单但非常重要——如果不裁剪mask会溢出到其他实例上尤其是当两个实例靠得很近的时候。裁剪之后mask还需要上采样到原图分辨率。这里用的是双线性插值然后通过一个阈值通常取0.5二值化得到最终的二值mask。整个流程下来一张图的前向时间可以控制在30ms以内1080Ti比Mask R-CNN快了将近一个数量级。3.3 系数分支的归一化与激活函数选择系数分支的输出没有用任何激活函数直接是线性输出。这个选择是有道理的系数可以是正也可以是负如果用ReLU会把负系数截断损失表达能力如果用Sigmoid会把系数限制在0-1之间也不合适。线性输出让系数可以自由取值配合原型的非负约束组合出来的mask可以表达各种形状。不过线性输出也有个问题系数的数值范围可能很大导致训练不稳定。论文里的解决方案是在系数分支后面加了一个BatchNorm层把系数的分布标准化。这个细节在论文里没有特别强调但在复现的时候很关键——不加BN的话训练初期loss会震荡得很厉害。另外在计算mask损失的时候论文用的是二值交叉熵BCE而不是Dice损失。BCE对每个像素独立计算损失梯度更稳定适合这种密集预测任务。Dice损失虽然对类别不平衡更鲁棒但在训练初期容易导致梯度消失。我试过两种损失的组合最终发现纯BCE的效果已经足够好加Dice反而需要调权重增加了调参成本。4. YOLACT的三个核心升级点拆解4.1 可变形卷积DCN的引入与位置选择YOLACT相比初代最大的改进之一是在骨干网络里引入了可变形卷积Deformable Convolution。具体来说是在ResNet的3×3卷积层里加了可学习的偏移量让卷积核的采样位置可以根据内容自适应调整。这个改进的动机很直接实例分割对物体的几何形变非常敏感。比如一个弯曲的手臂标准的3×3卷积核只能采样固定位置的像素而可变形卷积可以让采样点跟着手臂的曲线走捕捉到更准确的形状信息。论文里的实验表明在ResNet的stage 3、4、5里加DCNmask的AP能涨2-3个点。但DCN也不是没有代价的——它会增加计算量因为偏移量的预测需要额外的卷积层。而且DCN对训练数据量要求更高数据少的时候容易过拟合。论文里的做法是只在stage 3、4、5加DCNstage 2不加因为浅层特征的分辨率高加DCN的计算开销太大收益也不明显。我在复现的时候试过在stage 2也加DCN结果速度掉了15%精度只涨了0.3个点性价比很低。所以论文的选择是合理的DCN要用在语义信息丰富、分辨率适中的层浅层和深层都不太适合。4.2 优化后的anchor设计与尺度分配YOLACT的第二个改进是anchor设计的优化。初代YOLACT用的是RetinaNet的默认anchor设置也就是每个位置9个anchor3种尺度×3种长宽比。但实例分割和纯检测不一样mask的质量对anchor的尺度更敏感——如果anchor和物体的尺度不匹配mask的细节就会丢失。YOLACT的做法是根据FPN的层级重新分配anchor尺度。具体来说P3层负责小物体anchor尺度设为24P4层负责中等物体尺度设为48P5层负责大物体尺度设为96P6层负责超大物体尺度设为192P7层负责极端大的物体尺度设为384。每个层级只用一种长宽比1:1因为实例分割对长宽比的敏感度不如检测高。这个调整看起来简单但效果很明显。论文里的消融实验显示优化anchor之后小物体的mask AP涨了将近4个点。原因是小物体在P3层上的特征最丰富如果anchor尺度不匹配正样本的匹配率就会下降导致小物体的mask质量差。4.3 掩码重打分Mask Re-scoring的工程价值第三个改进是掩码重打分Mask Re-scoring。这个改进的思路是检测分支的类别置信度和mask的实际质量并不总是一致。有时候一个anchor的类别置信度很高但生成的mask很粗糙有时候置信度一般但mask很准。如果能用一个额外的网络来评估mask的质量然后把这个质量分和类别置信度结合起来就能提升最终的AP。具体实现上YOLACT加了一个小型的卷积网络输入是裁剪后的mask分辨率28×28输出是一个质量分数。这个分数和类别置信度相乘作为最终的排序依据。这个网络很小只有几个卷积层计算量可以忽略不计。这个改进的工程价值在于它不需要重新训练整个网络只需要在预训练好的模型上加一个分支用少量的数据微调即可。论文里的实验显示加了mask re-scoring之后AP涨了1.5个点而且几乎不增加推理时间。这个性价比非常高值得在实际项目里借鉴。我在自己的项目里试过这个方案发现它对置信度高但mask差的情况改善明显尤其是当物体被遮挡或者边界模糊的时候。不过需要注意的是mask re-scoring的训练数据需要精心构造——正样本是IoU高的mask负样本是IoU低的mask如果样本不均衡效果会打折扣。5. 训练策略与损失函数的实战配置5.1 损失函数的组成与权重分配YOLACT的损失函数由三部分组成分类损失、框回归损失、mask损失。分类损失用的是标准的交叉熵框回归用的是smooth L1mask损失用的是二值交叉熵。三者的权重分别是1、1.5、6.125。这个权重分配是经过大量实验调出来的mask损失的权重最大因为mask是最终目标分类和框回归只是中间任务。这里有个细节值得注意mask损失只在正样本上计算负样本不参与。因为负样本没有对应的实例计算mask损失没有意义。正样本的判定标准是anchor和GT的IoU大于0.5这个阈值比检测任务常用的0.5要高一些目的是保证mask的质量。另外mask损失计算的时候用的是裁剪后的mask和GT mask的逐像素BCE。裁剪的操作很重要如果不裁剪背景像素会主导损失导致mask边界模糊。论文里的做法是先用GT框裁剪再用预测框裁剪两者取交集。这个细节在复现的时候很容易忽略但影响很大。5.2 数据增强与训练调度YOLACT的训练用了标准的数据增强随机翻转、随机缩放、随机裁剪。缩放的范围是[0.5, 2.0]裁剪的大小是原图的[0.5, 1.0]倍。这些增强操作对实例分割很重要因为mask的标注成本高数据量通常不够增强可以显著提升泛化能力。训练调度方面论文用的是SGD优化器初始学习率0.001动量0.9权重衰减0.0005。学习率在第280k和360k迭代时分别除以10总共训练400k迭代。batch size是8用了4张GPU。这个配置在当时的硬件条件下是合理的但现在如果只有单卡可以适当减小batch size同时按比例调整学习率。有个坑需要注意YOLACT对学习率很敏感尤其是加了DCN之后。如果学习率太大DCN的偏移量会震荡导致训练不收敛。我在复现的时候试过用0.002的学习率结果loss直接飞了。后来降到0.001才稳定下来。所以如果你要复现建议从0.0005开始试稳定之后再往上加。5.3 推理阶段的NMS与mask后处理推理阶段YOLACT先用标准的NMSIoU阈值0.5过滤掉重叠的检测框然后对每个保留的检测框用对应的系数组合原型mask裁剪、上采样、二值化得到最终的mask。这里有个细节NMS是在检测框上做的不是在mask上做的。因为mask的计算成本高如果先算mask再做NMS速度会慢很多。但这样做也有个问题两个检测框的IoU可能不高但mask的重叠可能很大。论文里的解决方案是在NMS之后加一个mask的IoU过滤把mask重叠度太高的实例去掉。这个步骤是可选的但在密集场景下很有用。另外mask的二值化阈值取0.5这个值是固定的不需要调。但如果你的应用对mask的边界要求很高可以适当降低阈值比如0.4让mask稍微胖一点避免边界像素丢失。反过来如果要求mask精确可以提高到0.6但可能会丢失一些细节。6. 实测性能与部署时的坑点记录6.1 速度与精度的实际表现在COCO数据集上YOLACT的AP是34.1比Mask R-CNN的35.7略低但速度快了将近4倍33.5 FPS vs 8.6 FPS1080Ti。如果换成ResNet-50骨干速度可以到40 FPSAP降到31.2。这个 trade-off 在实际项目里是可以接受的尤其是对实时性要求高的场景。但要注意论文里的FPS是在特定硬件和输入分辨率下测的。输入分辨率是550×550如果换成1080p速度会掉到10 FPS左右。所以如果你的应用需要高分辨率输入YOLACT可能达不到实时需要考虑更轻量的骨干或者降低输入分辨率。我在自己的项目里做过测试在1080Ti上输入550×550YOLACTResNet-101的端到端时间是29ms其中骨干网络占了18ms原型生成占了5ms检测分支占了4msmask组装占了2ms。瓶颈主要在骨干网络如果要提速换MobileNet或者用TensorRT优化是最直接的办法。6.2 部署时的常见坑与解决方案第一个坑是原型mask的内存占用。32个原型每个128×128float32存储就是32×128×128×4 2MB。看起来不大但如果batch size是8就是16MB再加上中间激活值显存占用会很快上去。解决方案是用float16存储原型或者把原型分辨率降到64×64精度损失很小。第二个坑是mask组装的矩阵乘法在TensorRT上的支持。YOLACT的mask组装是一个批量矩阵乘法TensorRT对它的支持不如标准卷积好可能需要手动实现或者用插件。我在部署的时候试过用ONNX导出结果矩阵乘法那一步的算子不被支持后来改成了逐实例循环计算速度慢了不少。如果要用TensorRT建议提前确认算子支持情况。第三个坑是NMS的实现。YOLACT的NMS是在CPU上做的如果实例数量多NMS会成为瓶颈。解决方案是用GPU版的NMS比如CUDA实现的NMS或者用soft-NMS替代。但soft-NMS会改变AP的计算方式需要重新评估。6.3 适用场景与不适用场景的判断YOLACT适合的场景是对实时性要求高、对mask精度要求不是极端苛刻、物体尺度分布比较均匀。比如自动驾驶中的道路物体分割、视频监控中的人体分割、AR/VR中的实时交互这些场景下YOLACT的表现都不错。不适合的场景是小物体密集、物体重叠严重、需要像素级精确mask。比如医学图像分割、遥感图像分割这些场景下Mask R-CNN或者更精细的方法更合适。另外如果实例数量特别多比如超过100个YOLACT的mask组装和NMS会成为瓶颈速度优势就不明显了。我在一个密集人群分割的项目里试过YOLACT当人数超过50的时候mask的重叠问题很严重很多人的mask粘在一起。后来换成了基于关键点的方法效果才好起来。所以选型的时候一定要根据具体场景来判断不要盲目追求速度。7. 从YOLACT看实时实例分割的演进方向YOLACT的核心贡献不在于某个单点创新而在于它把原型系数这个思路工程化到了可用的程度。后续的很多工作比如SOLO、CondInst、Mask2Former都在不同程度上借鉴了这个思路。SOLO直接预测每个位置的实例mask省去了原型组合的步骤但计算量更大CondInst用动态卷积生成mask灵活性更高但训练更复杂。从趋势上看实时实例分割正在从两阶段向单阶段、从固定原型向动态生成、从密集预测向稀疏预测演进。YOLACT处在一个承上启下的位置它证明了单阶段实例分割可以做到实时同时保留了足够的精度。对于刚入门实例分割的同行来说YOLACT是一个很好的起点——它的结构清晰、代码开源、复现难度低适合用来理解实例分割的基本流程。如果你要基于YOLACT做改进我建议从这几个方向入手一是换更轻量的骨干比如MobileNetV3、EfficientNet-Lite提升速度二是改进原型生成网络用注意力机制或者动态卷积提升原型质量三是优化mask组装用稀疏表示或者量化降低计算量。这些方向在近两年的论文里都有探索效果也不错。最后分享一个我在复现过程中总结的小技巧训练的时候先用小分辨率比如300×300预热然后再切换到550×550微调。这样做的好处是训练初期速度快能快速看到loss下降增强信心后期切换到大分辨率mask的细节会明显提升。这个技巧在论文里没有提但实测下来能省不少时间。