把 SAM2 从“只会推理”推到“能自己训练”,这一步的跳跃远比想象中陡峭。SAM2 跟踪总结系列走到第三篇,前两篇我聊的是推理链路和说不清的边界条件,这次把视野拉回模型内部,专门处理跟 SAM2 训练有关的疑问。目录里的配置、权重文件、数据加载脚本看着都挺全,可真拿到自己的一小批视频标注,想把参数接着训起来,我身边几乎没人一次成功过。这篇文章会把我实际跑通自定义微调的过程、翻车点、以及后来整理出的排查顺序都写出来,适合已经跑过 SAM2 推理、正准备进入训练阶段的读者,也适合已经启动训练但总感觉哪里不对的人。几个核心问题先摆在前头:SAM2 训练到底在训什么?视频掩码怎么组织才符合数据加载器的脾气?哪些层该冻结、哪些层要放开?损失曲线不降到底是代码问题还是数据问题?下文逐个拆。
1. 先说清楚:SAM2 训练的和普通分割模型根本不一样
很多人按着以前训练分割模型的经验去套 SAM2,第一步就理解偏了。普通分割模型是“单张图进去,单张掩码出来”,训练时网络输出和标注直接比 IoU 就算完事。SAM2 不是这么工作的,它的能力建立在“连续帧序列”之上,模型里其实挂着三个各司其职的模块,训练目标也不止一个。
1.1 三部分结构与损失分支
SAM2 可以粗略拆成图像编码器、记忆注意力模块、掩码解码器三块。图像编码器负责把当前帧变成高维特征;记忆注意力模块负责把历史帧的信息汇总成“记忆”,再跟当前帧特征做融合;掩码解码器最终输出当前帧的分割结果。这种结构决定了它训练时的监督信号不是简单的“一图一掩码”,而是要让网络同时学会两件事:当前帧该分割成什么样,以及跨帧的同一目标该怎么保持对应关系。
实际训练里会看到不止一份损失项:主掩码分支通常用二值交叉熵加 Dice 来做分割损失;IoU 预测分支计算预测 IoU 与真实 IoU 之间的误差;如果开启了中间掩码监督,还会把几个辅助分支的损失按权重加进来;多掩码模式下还得对多余的候选掩码做惩罚。这些看起来繁琐的设计,其实都是为了推理阶段服务——推理时模型会先生成多个候选掩码,再用 IoU 得分排序选一个,如果训练时没有训练好这个得分分支,推理时选出来的未必是最优掩码。
1.2 冻结与微调的边界到底在哪
训练 SAM2 时最纠结的问题基本都是“到底要不要冻结”。我直接给结论:默认情况下,图像编码器体量最大、也最耗时,绝大多数自定义场景不需要从头训练它,冻结掉或者只给它极低的学习率就行;记忆注意力模块和掩码解码器才是最需要更新权重的部分。如果数据分布跟训练时的自然图像差得不太远,比如做监控视频、交通场景、普通物体跟踪,图像编码器冻结后效果依然很好。但如果目标域非常特殊,比如医学内镜画面、卫星遥感图、显微镜下的微观视频,图像编码器冻结会让模型始终学不到目标域的纹理特征,这时可以把最后一两层解冻,学习率压到主干其他模块的十分之一左右。
我自己刚开始微调时犯过一个错:把所有参数都放开,按默认学习率跑。结果显存直接爆掉,降 batch 之后又发现前几百步损失波动剧烈,收敛速度反而比只训解码器更慢。后来把图像编码器冻住,训练量一下降下来,收敛也稳了很多。这个边界值得每个人根据自己的数据先探一次。
1.3 很多人分不清的两件事:训练片段和推理跨度
还有一个隐蔽的概念区分:推理时 SAM2 的视频可以长达几百上千帧,历史信息会不断累积进记忆;但训练时我们一般不会把整段长视频一次性喂进去。常见做法是把视频切成固定长度的片段,比如每次取 16 帧作为一个训练样本,片段与片段之间可以重叠,也可以按时间滑动采样。这样做的原因是显存有限,另一个原因是训练时梯度要穿过时间维度回传,片段太长会让梯度计算极不稳定。
这里有个容易踩的坑:训练脚本里说的 batch_size,含义可能跟你想的不一样。它往往不是“一次塞进去多少帧”,而是“一次塞进去多少条视频片段”,每个片段内部又包含几十帧。假设你设了 batch_size=2,每片段取 16 帧,那一轮实际送入 GPU 的就是 32 帧,而梯度是在片段级别聚合的。如果有人在代码里看到 batch_size 很小就以为是显存占得少,基本都会在第一步就 OOM。
2. 通往训练的第一道坎:视频掩码数据到底怎么摆
数据组织问题看起来琐碎,却是训练跑不起来的最常见原因。SAM2 的训练数据既有图像也有视频,但既然名字叫“Segment Anything”,它的加载器对视频数据有明确的时序要求。很多人的首轮失败不是模型问题,而是标注文件放的位置和格式不符合约定。
2.1 数据目录与掩码格式怎么摆最稳
我建议直接采用一种最适合 SAM2 训练脚本的目录结构:视频帧按序列存放,掩码按同序列名存放,每帧掩码与每帧图像一一对应。一个通用且稳妥的布局类似:
datasets/custom/ train/ seq_001/ frames/ 00000.jpg 00001.jpg masks/ 00000.png 00001.png seq_002/ frames/ ... masks/ ...这个布局不是唯一标准,但胜在直观:每个序列目录里,帧和掩码用同样的数字命名顺序对齐,后面写数据加载器或者改训练脚本时不会搞乱。掩码文件本身建议用 PNG,值 0 表示背景,目标区域用具体类别或实例编号表示。如果某帧没有任何目标,掩码就是全零,这个状态要保留,不能删掉那一帧,否则帧序列会对不齐,时间维度的记忆学习会被打断。
2.2 二进制掩码重叠、缺帧和目标 ID 的坑
视频标注和静态图标注最大的差异在于“目标 ID 必须跨帧一致”。很多人画掩码时只求像素准,忽略了同一个目标在前后帧掩码里的编号要相同。比如第一帧编号 1,第二帧换个标注工具输出成 2,模型就会把这些帧当成两个不同目标,训练出来跟踪能力必然崩。这里没有捷径,只能建立一套命名规则:目标 ID 就是掩码灰度值,同一条视频里同一目标永远用同一个值。
还有一个更容易被忽略的坑:目标重叠。二值掩码里一个像素只能属于一个目标,但视频里两个目标经常互相遮挡、重叠。如果标注时把两个目标以不同灰度值放进同一张 PNG,很多加载器默认只会取其中一个通道,或者直接拒绝多值掩码。更常见的处理是给每个目标单独生成一张二值掩码文件,或做成多个目标共存但重叠区域按遮挡关系只给一个目标。我在这块吃过不小的亏,后面专门用一个小节复盘。
2.3 用少量数据先跑通闭环
不要一开始就追求大规模高质量数据。我的建议是先挑 20 到 50 帧、包含一个目标移进移出的短视频,手工标几帧中间帧,剩下的让标注工具或脚本自动插值补充,然后立刻把训练脚本跑起来。这样做的目的不是获得好模型,而是验证从数据读取、数据增强、网络前向反向、到 loss 打印的整条链路都是通的。
等这一步跑通,再逐步把数据加到几百帧、几十个序列。很多人直接拿两千帧视频从头标注、跑了一整天,最后发现数据加载器里掩码读取函数的一个参数写错,所有掩码读出来都是全黑,损失函数输出恒为 0。这种浪费是最不值的,小数据闭环十分钟就能暴露。
3. 配置文件、损失函数和优化器里值得较真的细节
SAM2 的训练配置通常是一堆 YAML 文件配一个主训练脚本。YAML 里的字段看着都认识,实际改起来却很容易误伤。我觉得最值得花时间理解的是三类:模型结构对应的配置、数据与优化器配置、以及训练钩子相关的配置。
3.1 配置文件里值得较真的字段
模型结构配置里,除了输入分辨率、主干类型,还要特别看是否开启了多掩码分支、是否输出中间掩码、记忆注意力模块的层数。这些字段直接决定训练时的输出通道和损失计算方式。如果预训练权重是在某个配置下训练出来的,你改配置去微调时,加载权重很可能出现结构对不齐的问题,常见表现是“部分参数未初始化”。解决办法是尽量沿用跟预训练权重一致的配置,只改数据路径和训练参数。
优化器配置通常包含学习率、权重衰减、warmup 和迭代总步数。我曾经为图省事直接把官方默认学习率往下减一位,结果训练变慢到无法接受。实际上迁移学习微调时,解码器和记忆模块可以用 1e-4 这个量级,冻结的骨干即使不更新也会有少量梯度累积,但学习率太小确实没意义。最稳的做法是以官方默认值为基准,只调一个量级,不要几个参数一起动。
3.2 损失、学习率和冻结策略的具体组合
损失权重方面,我的习惯是主掩码损失权重设为 1.0,IoU 预测损失权重在 0.5 左右,辅助中间掩码损失权重设置在 0.1 到 0.5 之间。这些数值不是官方铁律,但符合一般经验:主分支决定生成掩码的精度,辅助分支只是帮助中间层更平滑地学习梯度,权重给太大会干扰主分支。
冻结策略需要通过代码里的“requires_grad”或者训练脚本的冻结列表来设置。建议的分类如下表:
| 模块 | 参数量级 | 作用 | 微调建议 |
|---|---|---|---|
| 图像编码器 | 大 | 提取单帧特征 | 默认冻结,特殊域可解冻末尾层 |
| 记忆注意力 | 中 | 融合历史与当前帧 | 重点训练,学习率 1e-4 量级 |
| 掩码解码器 | 中 | 生成最终掩码与 IoU 得分 | 重点训练,学习率 1e-4 量级 |
表中这种分隔是符合工程直觉的:图像编码器学到的通用视觉特征,在很多数据集之间是可迁移的;而记忆注意力和解码器承担了大量跟“任务”相关的语义,必须靠你的数据训起来。如果你连图像编码器一起解冻,一定要把它的学习率单独压低,否则微调过程很容易把原本良好的通用特征破坏掉。
3.3 分辨率、batch 和显存的三方平衡
SAM2 默认训练分辨率很高,常见配置在 1024 左右。视频训练的内存开销大头在记忆注意力的时间维展开,显存不够时优先把分辨率降到 768 或 720,而不是急着调 batch。分辨率降低会直接减少单帧计算量,对性能的影响在大多数任务上小于 batch 减半带来的收敛不稳。
batch 和片段长度之间的取舍也很重要。片段长度建议在 8 到 16 帧之间,太短学不到跨帧记忆,太长梯度回传容易出 NaN。显存紧张时先降 batch 再降片段长度,优先级是“保持片段长度,尽量少动时序范围”。训练时如果 GPU 利用率不高,可以适当调大加载数据的 worker 数,但别超过 CPU 的逻辑核心数,否则数据排队会把瓶颈从 GPU 转到 CPU。
4. 在训练循环里把不确定变成可检查的东西
训练脚本跑起来之后,日志里那一堆数字很容易让人抓不住重点。我推荐从一开始就建立三个检查点:权重加载是否匹配、前几步 loss 是否在合理区间、以及周期性保存的可视化预测结果是否跟标注对得上。
4.1 权重加载与断点续训别踩的几个点
加载预训练权重微调时,脚本通常会打印哪些参数成功加载、哪些被跳过。很多人不看这些日志,直接盯着 loss 变化,结果模型在随机初始化状态下强行训练。正确做法是在训练开始前,显式检查一层层参数名是否匹配到了预训练权重。冻结的骨干权重一般能完整加载;掩码解码器如果输出分支的数量和你没有对齐,可能会整块跳过。
断点续训时,需要注意是否同时恢复了优化器状态和步数计数器。如果只恢复模型权重复训,学习率 schedule 会重新从头算,常见症状是曲线先掉下去,几十步之后突然又跳高,形成明显的“锯齿”。这种锯齿很容易被误判成模型不稳定。解决办法是保存断点时把 model、optimizer、lr_scheduler、epoch/step 四个内容一起打包。
4.2 混合精度、多卡和训练的稳定性
视频模型的显存压力很大,混合精度几乎是必须开的。但 SAM2 这种多分支多损失的结构在 fp16 下非常容易出现 NaN,导致 loss 变成 inf 或 nan。我遇到过的原因通常是某个损失分支在 fp16 下数值溢出,而不是梯度爆炸。排查方法很简单:关闭混合精度跑几十步,如果 loss 恢复合理,就确定是精度问题;再开启混合精度并把最小损失阈值调大,或用动态损失缩放重试。多卡训练时 loss 是所有卡的平均值,如果用的是不同厂商的卡或者显存不一致,也会出现步数不同步导致 loss 剧烈抖动,所以多卡环境最好保证硬件规格一致。
4.3 训练过程中每步看什么才算数
观察训练日志,我一般只看三个指标:总 loss、minibatch 的 mask loss 分量、以及可视化输出。可视化输出比数值更直接。训练一两百步之后,把当前模型跑在验证序列的同一帧上,看预测掩码有没有大致轮廓,如果轮廓是乱的但 loss 在降,多半是损失权重分配出了问题;如果轮廓不出现但 loss 恒为 0,几乎可以肯定是标注或数据读出问题。每个 epoch 保存一次可视化结果,能很直观地看到训练效果的演变,这种“眼见为实”的判断方法比盯着浮点数字更可靠。
5. 训练失败排查表:从损失不变到显存溢出
训练跑不通的时候,最忌讳的是瞎改配置。我建议按顺序排查:先查数据和标注,再查前向与损失计算,最后才动模型和优化器。这个顺序能覆盖绝大多数问题。
5.1 排查顺序:损失不动先别改模型
第一类常见现象是 loss 恒为 0 或几乎不变。优先检查掩码读取:用脚本手动加载一张 PNG,打印像素值分布。如果发现掩码全是 0 或全是白色,那就是读取路径或通道顺序不对;再检查归一化:很多加载器会假设背景为 0、前景为 1 或 255,如果你的标注恰好反了,网络看到的是一张前景与背景颠倒的图,损失当然在下降但模型在往反方向学。确认完后检查数据增强里有没有对掩码做跟图像不一样的随机翻转和旋转。这些看起来低级的问题,往往最耗时。第二类现象是 loss 在降,但验证结果不行,这多半是数据量太小或冻结过度导致的过拟合,或者是验证序列本身与训练分布差异太大。第三类是显存溢出,先不要降分辨率,先检查是不是 batch 里包含的视频片段数太多,把片段数从 2 降到 1,再考虑分辨率。
| 现象 | 优先检查 | 修复方法 |
|---|---|---|
| loss 恒为 0 | 掩码读取、标注反转、加载器路径 | 手动加载掩码并打印像素统计 |
| loss 为 NaN | 混合精度、学习率、片段长度 | 关闭混合精度试跑,减少片段长度 |
| loss 下降但验证差 | 冻结策略、数据量、验证分布 | 解冻部分层,增加数据多样性 |
| 显存溢出 | batch、片段数、分辨率 | 先减 batch,再降采样帧数 |
5.2 我踩过的相似目标重叠案例复盘
有一次我拿到一批监控视频,画面里好几只外观几乎一样的目标在交叉走动。我手工标注了一个序列的掩码,把不同目标用不同灰度值存在同一张 PNG 里,然后启动训练。训练 loss 一路下降,我当时很开心,结果在视频回放里一看,模型把两个目标合并成了一个,ID 完全错乱。排查了很久才发现,加载器在读取 PNG 时只保留了二值通道,多元标注里第二个目标的灰度值被识别成背景。后来我改成每个目标单独一张掩码文件,再在训练脚本里按帧堆叠成多通道掩码,这个问题才解决。这个案例说明,视频目标跟踪的数据标注和静态分割完全不是一回事,稍不注意就会让模型在一个错误的目标定义下学习。
6. 训完之后,怎样验证跟踪能力而不是只看掩码
训练结束不代表工作结束。很多人训练完了只跑一下单帧 IoU,发现很高就宣布成功,实际上跟踪能力可能一塌糊涂。SAM2 的核心能力是视频级的目标保持能力,验证时要把它当成跟踪器来测,而不是当成分割模型。
6.1 视频级别的验证:只看 IoU 远远不够
单帧 IoU 只反映“这一帧分割得准不准”,它掩盖了跨帧出现的问题:目标被遮挡后重新出现时 ID 是否延续、两个相似目标交错后是否反击、镜头快速移动时记忆模块是否把历史信息错误地延续下去。跑验证时,我习惯准备几条包含典型难点的短视频:目标出画面再回来、目标被遮挡数秒、快速旋转、光照突变、多目标交叉。在这些视频上分别记录三组指标:掩码 IoU、目标丢失帧数、ID 切换次数。掩码 IoU 反映分割质量,丢失和 ID 切换反映跟踪稳定性,两者有时会给出完全相反的结论——一个 IoU 很高但 ID 混乱的模型,在真实场景里基本不可用。
6.2 先见后补:训练结果怎么和 SAM2 跟踪链路联动
验证脚本里,训练好的模型权重要能替换掉推理时默认加载的那份权重,再走一遍完整的推理链路,知道权重加载的接口要同时兼容状态字典的键名。我的做法是把训练完的模型导出成推理格式,在导出时确保状态字典的键名与推理脚本完全一致,避免在验证时临时硬编码处理键名差异。接着,用第一帧的框或点给它一个提示,然后让它自己跑完整条视频。这样跑出来的结果才是训练效果的真正答卷。若模型在训练时把历史记忆学得太强,而当前帧特征被削弱,就会出现“掩码跟随上一个位置”的现象,哪怕当前目标已经移动了,这也要通过视频回放才能发现。
7. 我自己沉淀下来的一页备注
这些备注不是教程里的通用结论,是我在多次微调里反复试出来的习惯,供你参考。
7.1 关于数据、算力和期望值的现实建议
先定义清楚“要训到什么程度”。如果你只是在自有数据上微调,让模型适应当前场景,那么花小半天数据准备、训练几百到一两千步,就能得到明显好于原始权重的效果。如果你是想把模型训成某个新任务的大模型,那么单卡和少量数据完全不够,必须有成体系的数据管线、分布式训练和无数的排查手段,这完全是另一个工程量级。我给大部分咨询者的建议是:默认选微调,不要选全量训练;一个几百帧的小数据集,分阶段微调是最划算的。
算力方面,消费级显卡也能跑,但建议把视频片段长度控制在 8 帧,分辨率降到 768 以下,并开启混合精度。宁可慢一点,也不要一次把显存打满。会有人问我用不用多卡,我建议单卡稳定跑通后再考虑多卡,否则排查问题的时候会多出一堆分布式相关的变量,训练失败的根因会变得很难定位。
7.2 一些可以继续往深做的方向
训练跑通之后,进一步的方向主要有三个:一是针对你的业务场景构造更有难度的负例,比如把没有目标但容易误判的帧塞进验证集,专门测误检率;二是尝试让图像编码器在低学习率下部分微调,观察到底能带来多少提升;三是针对长期跟踪做记忆机制方面的模型修改,比如增加记忆淘汰策略或对不同帧的记忆做权重衰减。这三个方向都建立在“训练流程已经稳定可靠”的基础上,如果流程还时不时出问题,扩展方向的收益也会大打折扣。
在我个人的实际体会里,SAM2 训练最大的门槛不是模型结构有多复杂,而是太多细节需要同时盯住:数据格式得严谨,冻结策略得按数据域灵活调整,损失曲线得结合可视化一起看,验证环节必须放到完整视频链路里。把这套流程理顺之后,再回头处理新任务就会顺手很多。希望这篇训练疑问篇能帮你省掉一些我当年反复折腾的时间。