StarProject

黄色的树林中岔开两条路, 可惜我不能同时踏足, 我一介旅人,久久伫立着 沿着其中一条路极目望去, 直到它拐弯没入灌丛。

随后我选择另一条路,它同样可行, 或许更值得选择, 因为它荒草萋萋,盼人涉足, 尽管说到底,这两条路 被来往的脚步磨得相差无几。

那天清晨,两条路静卧着, 覆满落叶,未被足迹踏黑, 哦,我把第一条路留待他日再走! 可我深知,此路连着彼路, 恐怕我将不会再折返。

很久很久以后,在某处地方 我叹息着讲起此事: 树林中岔开两条路,而我—— 我选择了那条少有人走的路, 所有的一切皆因此而变得不同。

——《未选择的路》,弗罗斯特

Season 1

Week 1

4 月 13 日。与许老师进行一番推心置腹的谈话,然后正式开始科研。他曾反复问我是否下定决心搞科研,而我回答“是”。随后许老师找来我曾跟过一段时间的刘业鹏学长,准许我加入他的课题组。当日夜晚,获得一个 8x3090 服务器的使用权,配置 ssh 并成功登录,然后进行开发环境的调整(python、pip、conda)。

4 月 14 日。阅读 VGGT4D,其引入动态目标分割以把静态背景与动态物体分离,用以排除动态物体对 VGGT 的干扰。复现 VGGT,输入随手拍摄的几张照片进行推理,取得可观效果;输入含有动态物体的照片,则发现动态物体被扁平化;仔细阅读其代码,大致理解其模型架构。但对实验计划却不如前一日更理解。

4 月 15 日。复现 SAM2,仍然做 zero-shot:输入随手拍摄的照片,生成分割图。初步编写 RL head 并把 VGGT 与 SAM2 连接,形成基本框架。现虽不能进行训练但可进行推理,预测的结果是随机产生一个坐标并生成其掩码。推理阶段占用显存约 23G,已逼近 RTX3090 的显存极限。

4 月 16 日,完成连接 VGGT 与 SAM2 的采样部分,基本完成此模型的架构。开始写损失函数与奖励函数。

4 月 17 日,完成损失函数与奖励函数,初步撰写把不同模块加载到不同显卡上的简易 train.py。训练器可以运行,但没有训练效果(不拟合)。

4 月 19 日,修复模型中与 train.py 中的 bug,具备开始训练的条件。下载 DAVIS2017、tum-dynamic 与 Bonn 数据集并进行初步训练,有拟合效果,即分割出输入图像序列中存在的动态物体。未进行基准测试。

Week 2

4 月 20 日,发现训练样本增大(输入图像序列的帧数增多、分辨率提高)会 OOM 的问题。故进行优化:把 VGGT 的主干放到 cuda:2、把 VGGT 的其他头与 SAM2 放到 cuda:3,此时占用显存的峰值约为 33G,最多单卡占用显存约为 18G。

既然已具备训练的条件,那接下来的计划是进行一些小规模训练,或许可以暴露出代码或是模型的问题。因此现在的 8x3090 服务器完全够用,不急于进一步索要算力资源。

4 月 21 日,在 DAVIS2017(train) 与 tum-dynamic 数据集上进行训练,在 DAVIS2017(val) 数据集上进行验证。训练到一定阶段会 OOM,经排查发现,这是数据集内图像大小不同导致的,故选择把所有导入的图像进行压缩。验证即是进行动态目标分割的基准测试,每隔一部分 epoch 就输出在验证集上测得的 J-MEAN 和 F-MEAN。与作为对照的 VGGT4D 相比,J-MEAN 指标略低,但 F-MEAN 指标已偶尔逼近(49.3<51.1)。

4 月 22 日,发现问题:在部分 epoch 结束后会大幅度降低指标,使得最终结果甚至不如训练初期。故选择分开训练:只在 DAVIS2017(train) 上训练,指标变得稳定,F-MEAN 的峰值甚至超过 VGGT4D(54.2>51.1);只在 tum-dynamic 上训练,效果出奇地烂,J-MEAN 更是低到惊人的 0.23(VGGT4D 是 56.4),简直是随机乱选。tum-dynamic 的场景较为单一,或许是这一点导致了模型学习到的特征并非可泛化的特征。

4 月 23 日,对模型作 zero-shot,果然发现它会在无动态物体时乱选而非报告无动态物体,推测是损失函数与奖励函数的问题。翻看 plan.pdf,突然发现之前的理解有一定偏差——$\tau$ 值的选取仍是当初设置的占位常数而非根据验证集数据计算得出的 95 分位数。故写 getTau.py 以计算 $\tau$ 值,得到数值:0.136512。整理代码结构,把导入数据集的功能与测试验证集的功能从 train.py 中分离。

4 月 25 日,下载了 Sintel 数据集却发现这并非现实场景。根据昨日训练得到的权重,编写 test_combine.py 对模型的表现进行可视化。从结果来看,模型至少存在三个问题:训练初期效果较好,但随着训练的深入,指标会越来越低;当输入静态图像序列时仍选择与动态图像序列相同的选点,可能存在过拟合,学习到与是否动态无关的特征;当选点正确时,SAM2 的分割并不全面,可能只分割出动态物体的一部分。

4 月 26 日,放任训练器进行长时间训练,发现第一个问题不再明显,指标逐渐趋于稳定,前期的高指标或许并不能反映模型性能;第三个问题并不如第二个问题紧要,故试图解决第二个问题。使用 Copilot 分析 VGGT 原有 head(camera_head、dpt_head 与 track_head)的内部结构与 rl_head 存在的问题,着手调整 rl_head 的架构。放弃仅有几层卷积层的简单架构,修改为使用 dpt_head 进行特征提取、grid_head 输出与 null_head 输出分离的架构(Copilot 推荐)。

Week 3

4 月 27 日,修复了代码中存在的诸多小问题,进行重构,试图整合代码删除冗余部分。被 Gemini 3.1 Pro(Preview) 背刺,代码彻底瘫痪,不得不花费大量精力 debug。或许这就是贸然选择重构不得不承担的后果……吸取教训,以后严格审查 agent 提交的修改建议。

4 月 28 日,依然 debug。进行了更多可视化操作,比如渲染出 rl_head 得分的热力图。无论是 grid_logits 还是 null_logits,静态场景与动态场景的得分差不多,所以直接设置静态阈值会波及真正的动态场景,并不可行。对于这个问题,我注意到:无论其是否运动,rl_head 都会对相同的物体作出相同的预测,可能说明它更倾向于依据帧内注意力而不是全局注意力进行预测,没有学习到真正用于鉴别动态物体的特征。

开始使用接入 deepseek API 的 claude code,修复了 copilot 免费额度很快用光的问题。

4 月 29 日,添加了选点准确率的指标。在这里,我把选点准确率定义为“采样点属于动态物体掩码区域的帧数占总帧数的比例”,其数值一般而言比 F mean 要高,也就是说在选点准确时分割掩码不一定覆盖全面。发现并修复相机位姿未成功导入光度误差计算函数的问题,此前一直使用 torch.eye 的默认位姿。基于个人想法(让 rl head 更侧重帧间注意力、对奖励与惩罚进行缩放)调整 rl head 结构与奖励函数,却造成欠拟合灾难,在验证集测得的指标一片飘零。无语。

4 月 30 日,优化光度误差计算函数,并根据优化后的函数改进 getTau.py 。重新计算 $\tau$ 值,新结果为 0.017420,比原结果小了一个数量级;顺便计算了一下整个 DAVIS 数据集的 $\tau$ 值,其结果为 0.022345,与验证集结果相差不大。修改奖励函数的不合理设置,使模型重新变得可训练,但效果依然不好。贸然增大 K 值(训练阶段采样点数)或许能加速训练过程,但使模型的表现雪上加霜。考虑到光度误差是基于两个图像计算得出、而掩码只对应单个图像,或许掩码会覆盖不全;故计算奖励函数时取相邻两个掩码的并集,效果依然不好。总感觉后续一堆方案都不如最开始的方案……

回顾之前的 rl_head 架构:第一版是简单的几层卷积层,虽然在验证集上取得较好指标,但过拟合严重;第二版引入了 dpt_head,极度放大 rl_head 的总参数量,而强化学习的信噪比较低,难以支撑这种规模的训练;第三版改回卷积层,采用了 1x1 卷积与 3x3 卷积交叉的设计,并增强对全局注意力的感受,效果依然不佳。

5 月 1 日,五一放假,所以来点好玩的:编写 reward_demo.py,可以交互式地计算奖励函数。这个 demo 的工作流程为:加载数据集并预缓存图片、加载 VGGT 与 SAM2、启动多线程 http 服务以提供网页界面、根据网页内的交互情况动态处理。当使用者点击切换场景按钮时,VGGT 导入图片序列并预测相机位参与深度图;当使用者点击界面中的图片时,SAM2 输出基于点击位置的分割掩码并将其可视化,同时调用奖励函数计算奖励值。使用者点击图片可以模拟模型选择采样点,直观地察看当前掩码使模型获得的奖励/惩罚。

5 月 2 日,不断调整奖励函数的计算方式,根据 demo 观察奖励函数的取值。发现足以动摇核心假设的重大问题:背景噪声的光度误差并不一定会比动态物体小。准确来说,静态背景的平均光度误差不会很大,但静态背景内的单个物体不一定。以 DAVIS2017 数据集的 bear 场景为例,动态物体是一头正在移动的熊,主要背景噪声的来源是一旁日光下的树木;相机移动方向与熊的移动方向相同,这在一定程度上降低了熊与相机的相对位移,而且熊的体表纹理较为均匀一致,其内部(以二维图像来看,距离边缘较远的中心部分)光度误差较低;树叶反照率较高,反射太阳光从而表现出强烈的非朗伯属性,相机的运动更增大了背景噪声。这一来一去,使得“树叶掩码”内的平均光度误差与最大光度误差均大于“熊掩码”。

这也可以解释在之前代码并不完善时跑出的更优指标——当时光度误差计算函数使用的相机位姿是默认的静态位姿,而非 VGGT 预测的真实相机位姿,静态位姿降低了相机移动的场景中静态背景的噪声,使得真正的动态物体得以与背景区分。

或许以后试试重投影的几何误差而非光度误差。

Week 4

5 月 4 日,编写重投影几何误差计算函数,根据重投影后的边缘与纹理的对齐程度判断物体是否运动,发现其结果与光度误差大同小异。在纹理较为密集的区域,重投影几何误差也会因相机位姿的改变而剧烈增大,效果与光度误差基本一致。使用 vggt_demo.py 重建 DAVIS 中的 bear 场景,发现引起静态背景噪声的树叶部分置信度较低(小于 50%),由此引申,想到利用 VGGT 输出的深度图置信度排除背景误差的干扰。经过反复测试,发现效果最好的方案是:设置置信度阈值(这里设为 45%),放弃低置信度区域的误差值。这种方案的弊端有两方面:一方面是动态物体也有置信度较低的区域,会受到牵连;一方面是高置信度区域的背景噪声被完全保留。“背景噪声的光度误差并不一定会比动态物体小”这个问题仍未彻底解决。

5 月 5 日,使用半成品奖励函数投入训练,取得惊人效果:只训练 DAVIS train 数据集时,选点准确率就超过 97%,F-mean 也超过 58%。从这个结果来看,之前的我对奖励函数如此苛求,有点本末倒置:奖励函数不是 ground truth,没必要做到十全十美。模型会学习很多很多场景,而背景噪声大的场景毕竟是少数;模型更倾向于学习不同场景下共同的特征,因而背景噪声的影响相应减弱。但是,随着训练的深入,预测准确率与其他指标一并下降。似乎回到半个月前的状态。

我目前使用多个数据集分段训练,效果如下:训练完 DAVIS train 之后指标达到峰值,训练 tum-dynamic 的过程中仍能保持峰值,训练 Sintel train 使得指标略微下降,训练 bonn 使得指标大幅度下跌。我尝试重复训练 DAVIS train,依然是第一遍完成训练时指标峰值,随后逐渐下降。查看 RL head 得分热力图发现,训练后期的 RL head 反而会更关注背景,可能是奖励函数的不完美导致的,也可能是一种策略上的过拟合。

5 月 6 日,一边跑着训练一边改代码。试着把光度误差更换为几何误差,效果不好,遂采用光度误差。

Loss J-mean J-recall F-mean F-recall J-F mean Pt Acc
光度误差 51.75 51.27 54.53 52.63 53.14 97.41
几何误差 41.75 41.70 45.23 43.02 43.49 78.21

对连接 RL head 与 SAM2 的采样器进行优化:此前采样器在推理阶段只采样一个点,存在 SAM2 输出分割掩码不全的问题;于是想到采样多个点作为 prompt 输入 SAM2。得分高的点周围大概率是得分次高的点,如果取 RL head 得分最高的 k 个点,那这 k 个点容易挤作一团;所以想到取图中所有得分峰值点,以与 RL head 得分最大值的某比值(这里暂时取 50%)为阈值,只有得分高于阈值的峰值点才被采样。采样点变多会导致选点准确率下降,但以此为代价换取更好的分割效果无疑是值得的。

以 DAVIS2017(train)为训练集,每个场景下无重叠选取连续 40 张图像作为训练样本(若发现某场景下剩余图片过少会与前一个样本相融合),共计训练 129 个样本,训练结果如下。单采样与多采样的区别仅在于采样器的行为,RL head 的权重完全相同。

此表为 DAVIS2017(val) 上测得的结果。

Model J-mean J-recall F-mean F-recall J-F mean Pt Acc
EASI3R(monst) 56.5 68.6 - - - -
VGGT4D 56.4 65.6 51.1 56.9 - -
单采样 51.8 51.3 54.5 52.6 53.1 97.4
多采样 73.6 83.2 69.4 76.0 71.5 79.9

此表为 DAVIS2017(all) 上测得的结果。

Model J-mean J-recall F-mean F-recall J-F mean Pt Acc
EASI3R(monst) 53.0 63.4 - - - -
VGGT4D 50.8 55.6 47.0 46.4 - -
单采样 46.6 46.0 50.3 49.2 48.5 93.0
多采样 70.7 80.1 67.2 75.0 68.9 80.0

5 月 7 日,整理汇总思路与实验结果,制作汇报 ppt。试着引入光流残差,对光流残差进行可视化,发现其对动态物体极其敏感,几乎可以直接输出分割掩码;但其缺点也很明显,受光源变化影响大。经多次尝试,使用光流残差过滤静态背景噪声的效果并不好。

5 月 8 日,继续尝试光流残差,并没有做到比未引入时更好。总结目前仍未解决的问题:未能证明 RL head 成功学习到动态特征;在其他测试集上的表现未知;奖励函数仍未做到彻底区分动态物体与静态背景噪声(有点困难);区分动态场景与静态场景的阈值虽训练深入而改变。

5 月 10 日,尝试复现 5 月 6 日的结果,失败。所幸当时我对那次训练得到权重进行备份,现在仍能加载那份权重进行推理,跑出相同指标;或许是因为学习率过大等原因,即使用相同的代码、相同的参数也无法训练得出相同指标的权重,每次训练得到的指标都比那一次低 3 ~ 6 个百分点。查询 bash 记录发现,此前那次得到非常优秀的权重的训练犯了个错误:未清除上次训练得到的权重。我认为的 129 轮训练,实则是 20 轮 + 129 轮,且前 20 轮的动量等优化器相关变量未能继承到后续训练中。这算什么……

Week 5

5 月 11 日,试图复现此前训练的过程失败,与服务器断开连接,陷入停滞。与服务器重新连接后,继续尝试光流先验。继续此前置信度处理方式(弱化)已证明不可行,故这次尝试根据光流残差对光度误差进行增强,但效果不好。

5 月 12 日,整理思路,向学长汇报近期进展。得知当前的指标只是过拟合的产物、并不能真实地反映模型效果后,不再执着于复现此前的训练过程,转而进行其他方面的尝试:测试模型是否能区分动态场景与静态场景、尝试着寻找其他数据集以供训练。在汇报后向学长提出想法:应使用一些移动视角、静态场景的数据集进行训练,使得 RL head 接受“在静态场景中仍照常选点而非报告无动态物体”的惩罚。

5 月 13 日,发现此前“区分静态场景与动态场景的阈值随训练深入不断变化”的结论是错误的。事实上,这个阈值根本不存在。重新计算 $\tau$ 值,依旧选取 95 分位数作为在奖励函数中分类静态场景与动态场景的标准,但 RL head 在动态场景与静态场景下的得分依旧没有差距。此前的优化确实使得模型在“已知有动态物体时寻找哪里最有可能存在动态物体”的任务下表现较好,但模型本身无法区分是否有动态物体。这是个严峻的问题。

5 月 14 日,搜集并下载其他数据集,尤其是静态场景数据集 CO3D。针对 CO3D 进行分析,发现严峻问题:若静态场景的前景物体包含很多细节,则其光度误差也会很大,与背景物体拉开差距;这种场景下的全局光度误差平均值也与真正的动态场景拉不开差距。简而言之,仅仅依赖光度误差是无法区分静态场景和动态场景的,从奖励函数层面就出了问题。

5 月 15 日,针对 CO3D 中静态场景的光流残差进行分析,并无有效结论。向班级导师蔡朝晖老师汇报科研进展。

5 月 16 日,旁听葛旭迪学长有关视触觉的汇报,对“感知空间的方式”有了新的理解。貌似与目前的工作关系不大。对于区分静态场景与动态场景的问题,搜索相关论文,我查找到的方法均与当前的模型并不适配。

5 月 17 日,阅读 VGGT-Ω 论文。相比于引入的新架构(Register Attention)、提升性能、设计教师-学生模型,我更关注动态重建部分。与 VGGT4D 类似,VGGT-Ω 对运动的感知依旧来源于 VGGT 内部,并未针对这一个问题进行训练;VGGT-Ω 也具备提取动态掩码的功能,但其采用 Grounding DINO 完成这个任务。Grounding DINO 是根据文字提示分割图像的模型,其承担的功能或许类似于我们工作中的 SAM2。

VGGT-Ω 优秀的动态重建效果无疑是一个重大的挑战。

Week 6

这一周的时间比较零碎,因而没有每日总结。

聚焦于一个问题:如何区分静态场景(无动态物体的场景)与动态场景。按照学长的假设,以“静态背景平均光度误差”的 95 分位数为静态阈值,整幅图片的平均光度误差如小于阈值则判定为静态场景,反之则判定为动态场景。此前认为“仅仅依赖光度误差无法区分静态场景和动态场景”是基于其实际表现的主观感受,并没有进行量化分析。

遂进行量化分析。

统计 DAVIS 数据集(动态)的静态背景平均光度误差,得出分布直方图,绝大多数图片的平均光度误差都低于 0.01;统计 DAVIS 数据集(动态)的全图平均光度误差,得出分布直方图,大多数图片的平均光度误差也低于 0.01,甚至峰值位于 0.002 左右。这也就意味着,如果设定静态阈值、判定低于静态阈值的场景为静态场景,则一定会把很多动态场景误判为静态场景,出现大量假阳性(假设问题为“判定静态场景”)。统计 CO3D 数据集(静态)的全图平均光度误差,其均值为 0.011301。这也就是说,静态场景下的平均光度误差分布与动态场景并无明显区别,无论如何设置静态阈值都会产生大量假阴性。两方面数据结合来看,光度误差法在这个问题上无法接受。

于是尝试其他方法:基础矩阵与单应性矩阵、光流残差。第一种方法是 Gemini 推荐的,使用 Shi-Tomasi 角点和 Lucas-Kanade 光流追踪,并通过竞争拟合单应矩阵和基础矩阵判断场景是否包含动态物体。从实际表现上来看,其效果并没有比光度误差法更好。第二种方法为基于此前的思路延伸而来,虽然光流残差可能会在强光照、图像边缘、镜面等情况下错位、失效,但可能在不需要精确定位、只需要判定整个场景的运动属性的任务下表现较好。目前这种方法仍在尝试中。

Week 7

继续针对光流残差展开分析。

对光流残差进行统计,发现不同场景下的光流残差值无法比较。某些场景下图片的背景光流残差可能会很大,而且真正动态物体的光流残差数量级也不同,所以无法仅仅通过平均值、最大值之类的统计数据判断是否是动态场景。

考虑光流残差在图像中的分布。与地形地貌相类比,主要分为四种情况:一种情况是很清晰的动态物体,局部光流残差很大、背景光流残差很小,且边缘梯度较大,在热力图上形成“高原”;一种情况是边缘比较模糊的静态物体,受深度图预测误差影响边缘的光流残差较大,物体内部的光流残差较小,形成“角峰刃脊”;一种情况是弱纹理区域、非朗伯表面或光照变化,使得 RAFT 提取的光流发生系统性误差或是光流本身不可靠,出现梯度较小的光流残差峰值,形成“丘陵”;一种情况就是完美的静态场景,全图光流残差都较小,形成“平原”。

根据上述特征可进行分类:出现“高原”可判定为动态场景,无“高原”则判定为静态场景。目前已编写好分类器,但是分类效果并不理想,仍在改进中。

其他方面:修复服务器上的 frp 内网穿透。