0. 简介

X-NavDP 处理的是 RGB-D 视觉导航扩散策略里一个很具体的缺口:NavDP 这类策略在预训练阶段由全知全局规划器提供专家轨迹,真机部署时机器人却只有机载局部观测,一旦进入死胡同或者面对一整面长墙这种需要倒车、横移、绕行才能脱身的状态,策略就会在原地反复试探直到超时;把同一份权重换到四足和人形机器人上,成功率还会进一步下滑。论文用两个互补设计做在线强化学习后训练:复用模型自带的无目标条件分支构造结构化轨迹扰动,以及把 Q 值归一化限制在同一观测采样出的候选轨迹组内,再做加权得分匹配,也就是 GQRM(Group Q-score Reweighted Matching)。在 IsaacLab 的 40 个未见场景上,整体成功率从 61.20% 提升到 84.28%,SPL 从 58.95% 提升到 77.19%,真实世界困难场景从 10% 提升到 65%,整套后训练只花 12 小时。下面结合论文与仓库里的 x-navdp 代码,重点拆解扰动怎么造出来、分组归一化为什么能救难样本、以及跨形态调制与推理一致性这两块工程实现。

在这里插入图片描述

先用一张总览图把贡献层级压平。X-NavDP 的主线不是把所有模块并列堆在一起,而是围绕一个在线后训练闭环展开:先用自引导扰动为同一观测生成既多样又可执行的候选轨迹,再由评论家在候选组内比较相对价值,最后只更新扩散策略的低噪声去噪端。FiLM 负责让这条主线适配轮式、四足和人形机器人,RTC 则在部署时约束相邻动作块的近端位置连续;前两项决定策略能否学会脱困,后两项分别解决跨形态执行和时序稳定性。

在这里插入图片描述

1. X-NavDP 要解决的导航难题

1.1 特权监督与机载观测之间的错位

NavDP 的训练范式是典型的特权信息蒸馏:仿真器里跑一个知道全局占据栅格的规划器,把它算出的最优路径当作专家标签,让扩散策略去拟合局部 RGB-D 观测到这条路径的映射。这套做法在数据规模上非常划算,一天能造出几十万条轨迹,但它有一个绕不开的结构性问题:专家看到的是全局地图,学生看到的是一个 90 度视场角的深度图。当机器人站在一个丁字路口,全局规划器知道左边那条路 8 米后是死胡同,于是它给出的标签永远是"往右";学生从局部观测里根本区分不出左右两条走廊的差别,它只能学到一个在视觉上无法辨别的映射,最终在这类状态下输出模糊、抖动、甚至互相矛盾的轨迹。

核心问题在于这种错位不是靠加数据能补上的。加更多专家轨迹只会让同一个视觉输入对应更多种"正确答案",监督信号的方差反而更大。更麻烦的是脱困行为完全不在专家轨迹的支撑集里:全局规划器从起点就选了正确的路,它永远不会演示"走进死胡同再倒出来"这个过程,所以模仿学习出来的策略里天然缺少倒车、原地转向、大幅横移这几类动作。论文在失败案例分析里点得很直接,原始 NavDP 在长障碍物绕行和死角脱困上几乎没有可用行为。

1.2 形态盲的数据生成管线

第二个短板出在数据侧。NavDP 的轨迹是按一个标称机器人生成的,规划器不区分轮式底盘的差速约束、四足的转弯半径、人形上身的宽度与摆臂占用。这意味着同一条"贴着桌角擦过去"的轨迹,对 Dingo 是安全的,对 Unitree G1 就会撞到手臂。表 1 里 iPlanner 在人形机器人居家场景只有 5.95% 成功率、ViPlanner 30.25%、NavDP 50.70%,而这三个方法在轮式机器人上都能跑到 34% 到 68%,形态之间的落差比方法之间的落差还大,说明跨形态失效是这一代局部导航策略的共性问题,不是某个方法的实现瑕疵。

1.3 已有的扩散 RL 微调路线各自卡在哪

用在线强化学习补模仿学习的短板,思路上是顺的,难点全在扩散策略这个载体上。扩散策略的似然不可解析,反向去噪是一条几十步的链,想算某条轨迹的对数概率就得沿着整条链累乘,这让所有依赖似然比的优化方法都变得脆弱。三条主流路线各有硬伤,论文在表 3 里把它们放在同一个 24 场景设定下做了对照,数字很能说明问题。

路线代表方法机制24 场景 SR/SPL失效原因
策略梯度DPPO把整条去噪链当作二层 MDP,反传似然比33.98/33.28链式似然计算让梯度剧烈震荡,轨迹变混乱
隐空间优化DSRL冻结扩散主干,只优化初始噪声13.65/12.26探索被限制在预训练动作流形内,学不出新行为
加权得分匹配DPMD用 exp(Q/λ) 给去噪回归加权,绕开配分函数71.57/67.32Q 值在整个 minibatch 全局归一化,难状态梯度被淹没
监督微调SFT直接在三形态轨迹上做行为克隆53.50/50.84没有价值信号,只是换了一批模仿数据
本文X-NavDP分组归一化 + 自引导扰动80.48/74.78—

解读:DPMD 已经是一条相当强的基线,它证明"加权回归去噪得分"这个框架本身是稳的。X-NavDP 在同设定下比 DPMD 高出 8.91 个 SR,靠的不是换框架,而是把归一化的作用域从 minibatch 收缩到单个状态。这个改动在代码上只有几行,收益却是所有消融项里最大的一档。

2. 整体框架:一条被切成两半的扩散策略

2.1 输入输出与预训练基座

X-NavDP 的骨架继承 NavDP:输入是 8 帧历史 RGB-D 观测(memory_size: 8,分辨率 224)加一个点目标,输出是 24 步未来航点构成的动作块(predict_size: 24)。视觉侧用 RGB 与深度双分支编码后融合成条件记忆,目标点经过一个 point encoder 变成条件 token,动作侧是一个 16 层的 Transformer 解码器做噪声预测。前向加噪与反向去噪就是标准 DDPM 那一套:

q ( a k ∣ a k − 1 ) = N ( a k ; 1 − β k   a k − 1 ,   β k I ) , L dp = E k , ϵ [ w ( k ) ∥ ϵ − ϵ ϕ ( a k , k , o t , g ) ∥ 2 ] q(a^{k}\mid a^{k-1})=\mathcal{N}\bigl(a^{k};\sqrt{1-\beta_{k}}\,a^{k-1},\ \beta_{k}\mathbf{I}\bigr),\qquad \mathcal{L}_{\text{dp}}=\mathbb{E}_{k,\boldsymbol{\epsilon}}\Bigl[w(k)\bigl\|\boldsymbol{\epsilon}-\boldsymbol{\epsilon}_{\phi}(a^{k},k,\mathbf{o}_{t},\mathbf{g})\bigr\|^{2}\Bigr] q(ak∣ak−1)=N(ak;1−βk​ ​ak−1, βk​I),Ldp​=Ek,ϵ​[w(k) ​ϵ−ϵϕ​(ak,k,ot​,g) ​2]

其中 β k \beta_{k} βk​ 是噪声调度系数, w ( k ) w(k) w(k) 是与扩散步相关的权重, o t \mathbf{o}_{t} ot​ 是局部观测, g \mathbf{g} g 是目标条件。推理时从 a K ∼ N ( 0 , I ) a^{K}\sim\mathcal{N}(\mathbf{0},\mathbf{I}) aK∼N(0,I) 迭代去噪 10 步得到完整轨迹。这里要厘清的是 X-NavDP 并不重训整条链条,配置里 ft_denoise_step: 4 明确只微调低噪声端的若干步,高噪声端仍然走预训练权重。

2.2 关键的不对称设计:只动低噪声端

这是整个方法里最值得先讲清楚的工程决策。扩散去噪从高噪声到低噪声,粗略可以理解成"先决定大方向,再打磨细节"。高噪声步决定的是这条轨迹大体朝哪个方向去,低噪声步决定的是航点的精细排布与平滑度。X-NavDP 在代码里给微调部分单独准备了一整套参数:input_embed_ft、decoder_ft、layernorm_ft、action_head_ft,而预训练的 predict_noise 那一路完全冻结。去噪循环里用一个阈值把两条路径切开。

# src/x_navdp/models/x_navdp_policy.py  generate_action_mix
naction = torch.randn((bsz * num_samples, self.predict_size, 3), device=self.device)
for k in self.noise_scheduler.timesteps[:]:
    if k >= self.ft_step or is_old_policy:
        noise_pred = self.predict_noise(naction, k.unsqueeze(0), goal_embed, rgbd_embed)
    else:
        noise_pred = self.predict_noise_ft(
            naction, k.unsqueeze(0), goal_embed, rgbd_embed, embodiment=embody_for_ft
        )
    naction = self.noise_scheduler.step(
        model_output=noise_pred, timestep=k, sample=naction
    ).prev_sample

这段代码只有一个 if,但它承担了三件事。第一,把预训练的导航先验锁死在高噪声端,RL 再怎么折腾也不会把"往开阔处走"这种基本常识优化掉,从机制上避免灾难性遗忘。第二,把可训练参数压到一个很小的子集,8 卡 24000 步就能收敛,对应论文里 12 小时的后训练开销。第三,is_old_policy 这个开关让同一份代码既能跑当前策略,也能跑纯预训练策略做对照采样。训练调度上真正参与反传的只有低噪声区间的 ft_step 步,论文附录写的是 10 步去噪里微调最低噪声的 6 步,仓库默认配置给的是 4,这个数是可调的。

难点提示(为什么只调低噪声端就够了):把扩散去噪想象成拉坯。高噪声端是决定这是个碗还是个瓶的手法,低噪声端是修边、抹平、开口。X-NavDP 要教的新技能是"遇到墙先退两步再绕",这属于修边级别的调整——大方向仍然是朝目标去,只是局部走法变了。所以动低噪声端就够,动高噪声端反而会把整个器型捏坏。

在这里插入图片描述

3. 第一条核心机制:自引导轨迹扰动

3.1 高斯噪声为什么不管用

在线强化学习要有效,采样出来的候选动作必须覆盖到那些值得强化的行为。预训练的目标条件策略有很强的先验,它采出来的 64 条轨迹几乎全是"朝目标方向小幅摆动的前进弧线",方差集中在一个很窄的锥形区域里。在死胡同状态下,这 64 条轨迹全都撞墙,Q 值全都很低,评论家没法从里面挑出任何有价值的方向。

最直接的补救是往轨迹上加高斯噪声。这条路走不通的原因很具体:航点序列是有时序结构的,相邻航点的间距对应速度、曲率对应角速度。独立地给每个航点加噪声,得到的是一条锯齿状折线,它既不满足差速底盘的运动学约束,MPC 也跟不住,跑出来的实际轨迹和策略输出的轨迹严重不一致。这意味着采样分布和执行分布对不上,价值估计从源头就是错的。表 6 里 “w/o no-goal perturbation and reverse” 那一行整体 SR 只有 6.23%,训练直接崩掉,就是这条路的实测结果。

3.2 复用无目标分支作为扰动源

论文的切入点很巧:NavDP 预训练时本身带一个无目标条件分支(把目标 embedding 置零,让策略做纯探索式的可通行区域预测)。这个分支的输出天然满足两个性质,一是场景一致(它是从同一张深度图推出来的,不会穿墙),二是比目标条件分支激进得多(没有目标牵引,它会往各个开阔方向铺开)。把它当作扰动源,等于用模型自己的另一个模态给自己造多样性。混合公式是:

τ mixed = s ⊙ ( τ ~ pointgoal + λ   τ ~ nogoal ) , s = ( ( − 1 ) B 1 , ( − 1 ) B 2 ) , B 1 , B 2 ∼ Bernoulli ⁡ ( ϵ ) ,   B 1 ⊥ B 2 \boldsymbol{\tau}_{\text{mixed}}=\mathbf{s}\odot\bigl(\tilde{\boldsymbol{\tau}}_{\text{pointgoal}}+\lambda\,\tilde{\boldsymbol{\tau}}_{\text{nogoal}}\bigr),\quad \mathbf{s}=\bigl((-1)^{B_{1}},(-1)^{B_{2}}\bigr),\quad B_{1},B_{2}\sim\operatorname{Bernoulli}(\epsilon),\ B_{1}\perp B_{2} τmixed​=s⊙(τ~pointgoal​+λτ~nogoal​),s=((−1)B1​,(−1)B2​),B1​,B2​∼Bernoulli(ϵ), B1​⊥B2​

其中 λ \lambda λ 是带符号的混合系数, ⊙ \odot ⊙ 是 Hadamard 乘积, s \mathbf{s} s 把整条航点序列的 x x x 或 y y y 坐标整体取反。这里的关键是符号翻转作用在整个 chunk 上而不是单点上,所以翻转之后轨迹的平滑性和运动学可行性都保留着,只是方向反了——一条前进弧线整体翻转 x x x 就变成一条倒车弧线,翻转 y y y 就变成一条镜像绕行。脱困行为不是学出来的,是从已有的前进行为里"翻"出来的,这是这个设计最漂亮的地方。

3.3 代码里比论文更细的六层随机

仓库实现比论文公式复杂不少,generate_action_mix 的后半段叠了六层独立随机操作,每一层都对应一类想要覆盖的行为。论文公式只写了混合与翻转两步,代码里还有方向对齐、尺度归一化、幅度缩放和若干条概率性保留通道。先看方向对齐和混合这一段,它决定了两条轨迹相加之后会不会退化:

# src/x_navdp/models/x_navdp_policy.py  generate_action_mix
# 第 1 个点的 x 方向相反时,反转 nogoal_actions,避免混合后互相抵消
nogoal_dir_x = nogoal_actions[:, 1, 0]
origin_dir_x = origin_action[:, 1, 0]
is_opposite_x = (origin_dir_x * nogoal_dir_x) < 0
nogoal_actions[is_opposite_x, :, 0] = -nogoal_actions[is_opposite_x, :, 0]

len_origin = self.calculate_length(origin_action)
len_nogoal = self.calculate_length(nogoal_actions)
length_ratio = (len_origin / len_nogoal.clamp(min=1e-6)).unsqueeze(1).unsqueeze(2)

u_sign = torch.rand(final_actions.shape[0], device=final_actions.device)
u_mag = torch.rand(final_actions.shape[0], device=final_actions.device)
mag = u_mag * 0.5
sign = torch.where(u_sign < 0.5, -1.0, 1.0)
ratio_origin = (sign * mag).unsqueeze(1).unsqueeze(2)
final_actions = final_actions + ratio_origin * length_ratio * nogoal_actions

三个细节值得拆开说。方向对齐那一步解决的是两条轨迹反向时相加会互相抵消、混出一条长度接近零的退化轨迹的问题。length_ratio 做的是尺度归一化:无目标轨迹通常比目标条件轨迹长,不除掉这个比例,混合系数的实际作用强度会随场景漂移,同一个 λ \lambda λ 在开阔大厅和狭窄走廊里效果完全不同。sign 取 ± 1 \pm 1 ±1 而 mag 在 [ 0 ,   0.5 ) [0,\,0.5) [0,0.5) 上均匀采样,对应论文里说的带符号外插——负号那一半是外插而不是内插,它会把轨迹推到两条原始轨迹连线的外侧,也就是预训练动作流形之外。表 6 里 “w/o extrapolation”(固定权重 1,纯内插)只有 77.84/71.74,比完整版低 2.64 个 SR,验证了外插这一步的必要性。

进一步看,混合完之后还有第二组操作,负责把候选组的多样性再撑开一层,同时保证组里始终留有几条保守选项供评论家做基准。这一段包含坐标翻转、各轴独立的幅度缩放、三条概率性保留通道,以及最后的运动学修正与重采样收口,顺序是有讲究的——翻转和缩放先把方向与步长的空间铺开,保留通道再往里塞回几条基准,运动学修正放在最末保证不管前面怎么折腾,交出去的都是可执行轨迹:

# src/x_navdp/models/x_navdp_policy.py  generate_action_mix
is_reverse_x = (torch.rand(final_actions.shape[0]) < self.reverse_prob_x).to(final_actions.device)
is_reverse_y = (torch.rand(final_actions.shape[0]) < self.reverse_prob_y).to(final_actions.device)
final_actions[is_reverse_x, :, 0] = -final_actions[is_reverse_x, :, 0]
final_actions[is_reverse_y, :, 1] = -final_actions[is_reverse_y, :, 1]

_sx = torch.empty(final_actions.shape[0], device=final_actions.device,
                  dtype=final_actions.dtype).uniform_(0.75, 1.25)
_sy = torch.empty(final_actions.shape[0], device=final_actions.device,
                  dtype=final_actions.dtype).uniform_(0.75, 1.25)
final_actions[..., :, 0] = _sx.unsqueeze(-1) * final_actions[..., :, 0]
final_actions[..., :, 1] = _sy.unsqueeze(-1) * final_actions[..., :, 1]

is_keep_origin = torch.rand(final_actions.shape[0], device=final_actions.device) < self.keep_origin_prob
final_actions = torch.where(is_keep_origin.unsqueeze(-1).unsqueeze(-1), origin_action, final_actions)

do_truncate = torch.rand(final_actions.shape[0], device=final_actions.device) < self.truncate_prob
final_actions = self._truncate_origin_action_after_first_x_segment(
    final_actions, batch_mask=do_truncate, enable_negative_traj_length_scale=True)
final_actions = self.apply_trajectory_kinematic_correction(final_actions)
final_actions = self.smooth_trajectory(final_actions, num_points=25, smooth_factor=0.2, weight=self.weight)

除了论文提到的 x / y x/y x/y 翻转,代码还加了 [ 0.75 ,   1.25 ] [0.75,\,1.25] [0.75,1.25] 的各轴独立幅度缩放(等价于让候选轨迹在步长上有差异,为速度选择提供梯度),以及三条概率性保留通道:keep_line_prob 直接换成直线轨迹、keep_nogoal_prob 直接用纯无目标轨迹、keep_origin_prob 直接回退到未扰动轨迹。这三条通道保证候选组里始终混有"保守选项",评论家因此总有基准可比。最后三行是收口:随机截断制造不同长度的候选,apply_trajectory_kinematic_correction 按最小转弯半径把不可行的急弯拉回可行域,smooth_trajectory 用 B 样条重采样到 25 点。扰动全程只在可执行轨迹的空间里做,这是它和高斯噪声的本质区别。

在这里插入图片描述

3.4 采集与更新用同一批扰动

一个容易被忽略但很重要的实现约束:同一批扰动候选既用于环境交互采样,也用于演员更新。论文的措辞是 “The same perturbed set is used for both rollout collection and actor updates”。这样做的意义在于让行为策略 π o l d \pi_{\mathrm{old}} πold​ 和加权回归的提议分布严格一致,避免额外的分布偏移修正项。仓库里 rollout 阶段每个状态生成 8 条未扰动加 8 条扰动候选,训练时按 0.5 概率选 Q 值最高的扰动候选、否则从 8 条里随机选一条,评估回合则固定执行 Q 值最高的未扰动候选。

4. 第二条核心机制:GQRM 分组 Q 值加权匹配

4.1 DPMD 的全局归一化在难状态上失效

DPMD 的思路来自策略镜像下降(PMD),后者和 TRPO、PPO 属于同一个信任域家族,区别在于它不施加硬约束,而是用一个 KL 邻近项软性地把新策略拉在旧策略附近。对导航来说这个软约束正是需要的,因为预训练先验是要保住的资产。PMD 对每个状态 s s s 的策略改进目标写作

π M D ( ⋅ ∣ s ) = arg ⁡ max ⁡ π   E a ∼ π ( ⋅ ∣ s ) [ Q π o l d ( s , a ) ] − λ D K L ( π ( ⋅ ∣ s )   ∥   π o l d ( ⋅ ∣ s ) ) \pi_{\mathrm{MD}}(\cdot\mid s)=\arg\max_{\pi}\ \mathbb{E}_{a\sim\pi(\cdot\mid s)}\bigl[Q^{\pi_{\mathrm{old}}}(s,a)\bigr]-\lambda D_{\mathrm{KL}}\bigl(\pi(\cdot\mid s)\,\|\,\pi_{\mathrm{old}}(\cdot\mid s)\bigr) πMD​(⋅∣s)=argπmax​ Ea∼π(⋅∣s)​[Qπold​(s,a)]−λDKL​(π(⋅∣s)∥πold​(⋅∣s))

它的闭式解是 π M D ( a ∣ s ) = π o l d ( a ∣ s ) exp ⁡ ( Q ( s , a ) / λ ) / Z M D ( s ) \pi_{\mathrm{MD}}(a\mid s)=\pi_{\mathrm{old}}(a\mid s)\exp\bigl(Q(s,a)/\lambda\bigr)/Z_{\mathrm{MD}}(s) πMD​(a∣s)=πold​(a∣s)exp(Q(s,a)/λ)/ZMD​(s),其中 Z M D ( s ) Z_{\mathrm{MD}}(s) ZMD​(s) 是配分函数。扩散策略没法直接从这个分布采样,DPMD 的办法是构造一个加权得分匹配目标,把配分函数吸收进重加权函数里,最终落到一个可采样的去噪回归损失,权重是 exp ⁡ ( Q ( s , a 0 ) / λ ) \exp\bigl(Q(s,a_{0})/\lambda\bigr) exp(Q(s,a0​)/λ)。

问题出在这个权重的实际计算方式上。DPMD 把 Q 值放在整个 minibatch 里归一化再取指数,而一个 minibatch 混着来自完全不相关状态的候选。简单状态(一条空走廊,直着走就能拿高回报)的绝对 Q 值天然比困难状态(死胡同,所有动作回报都低)高一大截,指数放大之后,简单状态的样本几乎吃掉全部梯度权重,困难状态的候选权重被压到接近零。结果就是最需要学习的脱困行为拿不到有效监督。

4.2 把归一化收缩到同一状态内

GQRM 的改动就是把统计量的作用域从 minibatch 换成同一观测采样出的候选组 G ( s ) \mathcal{G}(s) G(s),也就是每个状态用自己的均值和方差做中心化与缩放,互不干扰。这样一来权重表达的不再是"这个动作的绝对回报有多高",而是"在这个状态能采到的所有动作里,它排在什么位置"。三个公式分别是组内统计量、归一化后的优势值、以及最终的加权得分匹配目标:

Q ˉ G ( s ) = E a 0 ∼ π o l d ( ⋅ ∣ s ) [ Q ( s , a 0 ) ] , σ G ( s ) = E a 0 ∼ π o l d ( ⋅ ∣ s ) [ ( Q ( s , a 0 ) − Q ˉ G ( s ) ) 2 ] \bar{Q}_{G}(s)=\mathbb{E}_{a_{0}\sim\pi_{\mathrm{old}}(\cdot\mid s)}\bigl[Q(s,a_{0})\bigr],\qquad \sigma_{G}(s)=\sqrt{\mathbb{E}_{a_{0}\sim\pi_{\mathrm{old}}(\cdot\mid s)}\bigl[(Q(s,a_{0})-\bar{Q}_{G}(s))^{2}\bigr]} Qˉ​G​(s)=Ea0​∼πold​(⋅∣s)​[Q(s,a0​)],σG​(s)=Ea0​∼πold​(⋅∣s)​[(Q(s,a0​)−Qˉ​G​(s))2] ​

Q ~ G ( s , a 0 ) = c l i p  ⁣ ( c   ( Q ( s , a 0 ) − Q ˉ G ( s ) ) σ G ( s ) + ε ,   − h ,   h ) \widetilde{Q}_{G}(s,a_{0})=\mathrm{clip}\!\left(\frac{c\,\bigl(Q(s,a_{0})-\bar{Q}_{G}(s)\bigr)}{\sigma_{G}(s)+\varepsilon},\ -h,\ h\right) Q ​G​(s,a0​)=clip(σG​(s)+εc(Q(s,a0​)−Qˉ​G​(s))​, −h, h)

L GQRM ( θ ; s , t ) = E a 0 ∼ π o l d ( ⋅ ∣ s ) a t ∼ q t ∣ 0 ( ⋅ ∣ a 0 )  ⁣ [ exp ⁡  ⁣ ( Q ~ G ( s , a 0 ) / λ ) ∥ s θ ( a t ; s , t ) − ∇ a t log ⁡ q t ∣ 0 ( a t ∣ a 0 ) ∥ 2 ] \mathcal{L}_{\text{GQRM}}(\theta;s,t)=\mathbb{E}_{\substack{a_{0}\sim\pi_{\mathrm{old}}(\cdot\mid s)\\ a_{t}\sim q_{t\mid 0}(\cdot\mid a_{0})}}\!\left[\exp\!\bigl(\widetilde{Q}_{G}(s,a_{0})/\lambda\bigr)\bigl\|s_{\theta}(a_{t};s,t)-\nabla_{a_{t}}\log q_{t\mid 0}(a_{t}\mid a_{0})\bigr\|^{2}\right] LGQRM​(θ;s,t)=Ea0​∼πold​(⋅∣s)at​∼qt∣0​(⋅∣a0​)​​[exp(Q ​G​(s,a0​)/λ) ​sθ​(at​;s,t)−∇at​​logqt∣0​(at​∣a0​) ​2]

其中 c c c 是归一化尺度, h h h 是裁剪上限, ε \varepsilon ε 防止退化组除零, λ \lambda λ 是控制权重尖锐度的温度。论文附录 A 用一个修正比 r G ( s , a 0 ) = exp ⁡ ( Q ~ G / λ ) / exp ⁡ ( Q / λ ) r_{G}(s,a_{0})=\exp\bigl(\widetilde{Q}_{G}/\lambda\bigr)/\exp\bigl(Q/\lambda\bigr) rG​(s,a0​)=exp(Q ​G​/λ)/exp(Q/λ) 把这个替换形式化,说明 GQRM 没有改动目标分布 p 0 p_{0} p0​、加噪边缘 p t p_{t} pt​ 和条件得分目标,唯一变的是演员优化时去噪样本的加权方式。

直觉理解(分组归一化在做什么):这和考试按班级排名而不是按全校绝对分排名是一回事。全校排名下,重点班的学生垄断了所有名额,普通班里那个从 40 分进步到 65 分的学生完全看不见。按班级排名,每个班都能选出自己的前几名。困难状态就是那个普通班——它的绝对分永远上不去,但组内相对优劣真实存在,而且正是这个相对优劣告诉策略"往这个方向偏一点会好一些"。

4.3 训练代码里的分组归一化

gqrm_trainer.py 的演员更新段是整篇论文最核心的实现,也是最值得逐行读的一段。它做的事情按顺序是:为同一批观测采 64 条扰动候选、用双评论家给每条打分、按状态重排成组、算组内均值与归一化优势、把非正优势清零、再取 Q 值最高的 5 条留下。整段跑在 torch.no_grad() 里,因为权重只是一个标量系数,不需要对它反传梯度:

# src/x_navdp/trainer/gqrm_trainer.py  GQRMTrainer.update
with torch.no_grad():
    _, perturb_act_batch = self.policy.module.generate_action_mix(
        next_obs_batch, num_samples=self.num_samples,
        is_nogoal_perturb=True, is_old_policy=False, embodiment=embodiment_batch)
    q1_next, q2_next = self.policy.module.generate_q_value(
        next_obs_batch, perturb_act_batch, embodiment=embodiment_batch, is_target=False)
    q_next = torch.minimum(q1_next, q2_next)
    q_next = q_next.reshape(-1, self.num_samples)      # 关键一步:按状态重排成 (B, 64)
    _, topk_indices = torch.topk(q_next, k=self.topk, dim=1)

    q_mean = q_next.mean(dim=-1).unsqueeze(1)          # 沿候选维求均值,即组内均值
    q_std = q_next.std()

    norm_q = self.norm_scale * (q_next - q_mean) / q_std
    scaled_q = norm_q.clip(-3., 3.) / torch.exp(self.policy.module.log_alpha)
    q_weights = torch.exp(scaled_q)
    q_weights[norm_q <= 0] = 0                         # 优势非正的候选权重直接归零

    q_weights = q_weights.gather(dim=1, index=topk_indices)

reshape(-1, self.num_samples) 是全部改动的枢纽。它把展平的 B × 64 B\times 64 B×64 条候选重排成 ( B ,   64 ) (B,\,64) (B,64),之后 mean(dim=-1) 就是沿候选维度求组内均值,每个状态得到自己的中心。self.num_samples = 64、self.topk = 5、self.norm_scale = 2,裁剪区间是 [ − 3 ,   3 ] [-3,\,3] [−3,3],这些超参和论文附录 B.2 的描述一致。两处细节和论文正文略有出入:一是这里的 Q Q Q 是在 next_obs_batch 上算的,二是 q_std 用的是整个 minibatch 的标准差而不是组内标准差——代码里被注释掉的 q_std = q_next.std(dim=1, keepdim=True) 才是纯组内版本,作者在旁边标了 # better 说明当前写法是实测更优的选择。均值按组、方差跨批,等于在"每个状态有自己的中心"和"尺度估计足够稳"之间做了折中,样本量只有 64 时组内方差估计噪声偏大,这个折中是合理的。

q_weights[norm_q <= 0] = 0 与 topk 组成双重过滤:先把组内优势为负的候选权重清零,再只保留 Q 值最高的 5 条。这两步的动机不同,前者保证策略只朝比组内平均更好的方向移动,后者是为了降方差和省算力——64 条候选全参与反传,显存和时间都撑不住。评论家用的是 min ⁡ ( Q 1 , Q 2 ) \min(Q_{1},Q_{2}) min(Q1​,Q2​) 的保守双评论家估计,避免高估导致演员追着虚高的价值跑偏。

4.4 加权损失怎么落到去噪回归上

拿到权重之后,损失的组装反而很朴素,就是给标准去噪 MSE 乘一个标量。代码先把筛选出来的候选按 ft_step 个时间步分别加噪缓存好,再逐步做噪声预测并加权回归。这里值得留意的是加噪与预测被拆成了两个独立循环,前一个在无梯度上下文里预先算好所有带噪样本,后一个才真正建图反传:

# src/x_navdp/trainer/gqrm_trainer.py  GQRMTrainer.update
    x_noisy, noise_list = [], []
    noise = torch.normal(mean=0.0, std=1.0, size=perturb_act_batch.shape).to(self.device)
    for t in range(0, self.policy.module.ft_step):
        noise_list.append(noise)
        k = torch.full((perturb_act_batch.shape[0],), t, dtype=torch.int64)
        x_noisy.append(self.policy.module.q_sample(k, perturb_act_batch, noise))

for t in range(0, self.policy.module.ft_step):
    k = torch.full((perturb_act_batch.shape[0],), t, dtype=torch.int64)
    noise_pred = self.policy(next_obs_batch, x_noisy[t], embodiment_batch, k, is_critic=False)
    loss_t = (weights * torch.square(noise_pred - noise_list[t])).mean()
    loss_t.backward()
    policy_loss_output += loss_t.detach()

这段代码对应论文附录 A 里的有限样本目标,外层对状态、候选组与前向扩散噪声求期望,内层对候选和被微调的低噪声时间步求和。换句话说,同一条候选轨迹会在多个噪声水平上被反复回归,而权重在这些噪声水平之间是共享的,因为它衡量的是这条轨迹本身的优劣,与噪声水平无关:

J G Q R M = E s , G , ϵ [ ∑ i = 1 N exp ⁡  ⁣ ( Q ~ G ( s , a 0 , i ) / λ ) ∑ t = 0 K f t − 1 ∥ ϵ ϕ ′ ( a t , i , t , s ) − ϵ ∥ 2 ] \mathcal{J}_{\mathrm{GQRM}}=\mathbb{E}_{s,\mathcal{G},\boldsymbol{\epsilon}}\left[\sum_{i=1}^{N}\exp\!\bigl(\widetilde{Q}_{G}(s,a_{0,i})/\lambda\bigr)\sum_{t=0}^{K_{\mathrm{ft}}-1}\bigl\|\boldsymbol{\epsilon}_{\phi'}(a_{t,i},t,s)-\boldsymbol{\epsilon}\bigr\|^{2}\right] JGQRM​=Es,G,ϵ​[i=1∑N​exp(Q ​G​(s,a0,i​)/λ)t=0∑Kft​−1​ ​ϵϕ′​(at,i​,t,s)−ϵ ​2]

对 ϵ \epsilon ϵ-prediction 的 DDPM 来说,条件得分匹配等价于预测注入噪声(差一个与时间步相关的尺度),所以实现上就是给标准 MSE 去噪损失乘一个标量权重。两处工程细节值得注意:所有 ft_step 个时间步共用同一份采样噪声 noise,这降低了权重估计的方差;每个时间步单独 backward() 而不是求和后一次反传,是为了省显存——64 条候选乘以批大小再乘以时间步,一次性建图会撑不住。温度 λ \lambda λ 通过 log_alpha 参数化并且是可学的,由一个 SAC 风格的熵目标 target_entropy = -self.act_dim * 2.0 驱动,避免手调这个最敏感的超参。

在这里插入图片描述

5. 跨形态适配:用 FiLM 而不是拼接 Token

5.1 两处注入点

要让一套权重同时伺候轮式、四足、人形三种机器人,最省事的做法是给每类机器人学一个嵌入向量,然后把它当作额外的条件 token 拼到解码器输入里。论文管这叫 Soft-Prompt Concat,并把它当作对照组。X-NavDP 用的是双点注入:先给每个动作 token 加一个偏移,再在解码器输出上做 FiLM 缩放平移。

u e = u + f Δ ( z e ) , [ Δ γ e , Δ β e ] = f F i L M ( z e ) , h e = ( 1 + Δ γ e ) ⊙ h + Δ β e \mathbf{u}_{e}=\mathbf{u}+f_{\Delta}(\mathbf{z}_{e}),\qquad [\Delta\boldsymbol{\gamma}_{e},\Delta\boldsymbol{\beta}_{e}]=f_{\mathrm{FiLM}}(\mathbf{z}_{e}),\qquad \mathbf{h}_{e}=(1+\Delta\boldsymbol{\gamma}_{e})\odot\mathbf{h}+\Delta\boldsymbol{\beta}_{e} ue​=u+fΔ​(ze​),[Δγe​,Δβe​]=fFiLM​(ze​),he​=(1+Δγe​)⊙h+Δβe​

其中 z e = E e m b ( e ) \mathbf{z}_{e}=E_{\mathrm{emb}}(e) ze​=Eemb​(e) 是形态 e e e 的嵌入, u \mathbf{u} u 是动作 token 特征, h \mathbf{h} h 是解码器输出的轨迹特征,最后再过去噪头 H a c t H_{\mathrm{act}} Hact​。这里的关键是这两个注入点作用在扩散解码器的前后两端:前端偏移改变的是"从什么样的动作假设出发",后端 FiLM 改变的是"输出轨迹整体的尺度与偏置"。对导航来说后者的物理意义相当直白——人形机器人需要更大的避障间隙和更大的转弯半径,这在特征空间里近似于一个仿射变换。

5.2 代码实现与零初始化

# eval/src/policy_network_embodiment.py  NavDP_Policy_Embodiment
self.embody_out_film = nn.Linear(token_dim, 2 * token_dim)
nn.init.zeros_(self.embody_out_film.weight)
nn.init.zeros_(self.embody_out_film.bias)

def predict_noise_ft(self, last_actions, timestep, goal_embed, rgbd_embed, embodiment):
    """Predict noise with fine-tuned decoder for specific embodiment."""
    action_embeds = self.input_embed_ft(last_actions)
    ...
    input_embedding = action_embeds + self.out_pos_embed(action_embeds)
    embody_vec, cond_embedding = None, base_cond
    if self.distinguish_embodiment:
        embody_idx = self._align_embody_idx(embodiment, input_embedding.shape[0])
        embody_vec = self.embodiment_embedding(embody_idx)
        cond_embedding = torch.cat([cond_embedding, embody_vec.unsqueeze(1)], dim=1)
        input_embedding = input_embedding + self.embody_tgt_delta(embody_vec).unsqueeze(1)
    output = self.decoder_ft(tgt=input_embedding, memory=cond_embedding,
                             tgt_mask=self.tgt_mask.to(self.device))
    output = self.layernorm_ft(output)
    if embody_vec is not None:
        dgamma, dbeta = self.embody_out_film(embody_vec).chunk(2, dim=-1)
        output = (1.0 + dgamma.unsqueeze(1)) * output + dbeta.unsqueeze(1)
    return self.action_head_ft(output)

nn.init.zeros_ 那两行是这段代码里最关键的工程细节。FiLM 层权重和偏置都初始化为零,意味着训练刚开始时 Δ γ = Δ β = 0 \Delta\boldsymbol{\gamma}=\Delta\boldsymbol{\beta}=0 Δγ=Δβ=0,于是 h e = ( 1 + 0 ) ⊙ h + 0 = h \mathbf{h}_{e}=(1+0)\odot\mathbf{h}+0=\mathbf{h} he​=(1+0)⊙h+0=h,形态调制完全是恒等映射。这保证了后训练的第 0 步严格等价于预训练策略,形态差异是从零开始逐渐学出来的,不会在训练初期就把预训练特征打乱。 同一份 FiLM 参数还被评论家复用(predict_pointgoal_q 里对 critic_output 做同样的调制),所以演员和评论家对形态的理解是共享的。注意到形态嵌入同时进了条件 memory(torch.cat)和动作 token(加法偏移)两条路,评论家侧还配了专门的 cond_critic_mask_one_embod 注意力掩码来适配多出来的这个 token。

5.3 消融数据支持哪一种注入方式

论文表 5 把两种注入方式在 24 场景和 56 场景两个设定下都测了,这个双设定的安排本身就是结论的一部分:如果只看单一场景规模,很容易得出两种方案等价的结论。实际的结论不是"FiLM 一定更好",而是FiLM 的扩展性更好,它随训练场景增多获得的收益更大。

注入方式24 场景 SR/SPL56 场景 SR/SPL扩展增益
Soft-Prompt Concat80.24/74.3982.84/75.99+2.60 SR
Embodiment FiLM80.48/74.7884.28/77.19+3.80 SR

工程价值:24 场景下两者只差 0.24 SR,基本在噪声范围内。场景数翻倍到 56 之后差距拉到 1.44 SR,人形居家场景从 80.55 拉到 84.50,差了近 4 个点。这说明拼接 token 的方式在数据量小时够用,但它把形态信息交给注意力机制自己去发现,容量上限较低;FiLM 是直接在特征通道上做仿射变换,表达形态差异的路径更短更直接,数据一多优势才显出来。选方案时要看的是随数据量扩展的斜率,不是单点数值。

6. 分层控制与宏步奖励:RL 的时间尺度怎么定

6.1 三层控制栈

扩散策略输出的是航点,不是关节力矩,所以中间必须垫一层控制器。X-NavDP 的栈是三层:导航策略每 3 秒输出一段 24 点航点,统一的 MPC 以 25 Hz 把航点转成底盘速度指令,各形态专属的预训练运动策略再把速度指令变成关节动作。MPC 用单车模型建模底盘:

x ˙ = v cos ⁡ θ , y ˙ = v sin ⁡ θ , θ ˙ = ω , ∣ v ∣ ≤ 0.5   m / s ,   ∣ ω ∣ ≤ 0.5   r a d / s \dot{x}=v\cos\theta,\qquad \dot{y}=v\sin\theta,\qquad \dot{\theta}=\omega,\qquad |v|\le 0.5\ \mathrm{m/s},\ |\omega|\le 0.5\ \mathrm{rad/s} x˙=vcosθ,y˙​=vsinθ,θ˙=ω,∣v∣≤0.5 m/s, ∣ω∣≤0.5 rad/s

Dingo 用 30 步 × 0.1 秒,G1 和 Go2 用 75 步 × 0.04 秒,都对应 3 秒的跟踪视界;状态权重 d i a g ( 10 , 10 , 0 ) \mathrm{diag}(10,10,0) diag(10,10,0),控制权重 d i a g ( 0.05 , 0.05 ) \mathrm{diag}(0.05,0.05) diag(0.05,0.05),用 SQP 配显式 Runge-Kutta 积分求解,仓库里靠 acados 做实时求解。这套解耦的好处是换机器人时上层策略不用重训,代价是新形态必须先有一个能用的底层运动控制器,论文在局限性里明确承认了这一点。

6.2 为什么把 3 秒当作一个 RL 步

这是整个训练设计里另一个关键决策。如果按仿真器步(40 毫秒)定义 RL 转移,一段 3 秒的轨迹会被切成 75 个转移,但这 75 步里策略只做了一个决策,其余全是 MPC 在跟踪,价值函数会被大量无决策的步骤稀释。X-NavDP 把整个 3 秒窗口压成一个宏步转移,累计奖励做归一化:

r m a c r o = ∑ ℓ = 0 L − 1 r t + ℓ e n v + r p r o g L / ( 3 κ e ) , κ e ∈ { 1 ,   2.5 ,   2.5 } r_{\mathrm{macro}}=\frac{\sum_{\ell=0}^{L-1}r^{\mathrm{env}}_{t+\ell}+r_{\mathrm{prog}}}{L/(3\kappa_{e})},\qquad \kappa_{e}\in\{1,\,2.5,\,2.5\} rmacro​=L/(3κe​)∑ℓ=0L−1​rt+ℓenv​+rprog​​,κe​∈{1,2.5,2.5}

其中 L L L 是实际执行的仿真步数, κ e \kappa_{e} κe​ 按 Dingo、G1、Go2 取不同值,用来抹平三种形态控制器步频不同带来的奖励尺度差异。不做这个归一化,四足和人形每个宏步累积的原始奖励会是轮式的 2.5 倍,共享评论家会误以为四足天生更优。 这意味着价值学习的粒度和动作块的粒度对齐了,一个 Q 值评估的正是"这一段 3 秒轨迹好不好",而不是某个瞬时状态好不好。

6.3 奖励设计里的反直觉一项

奖励一共六项:存活正则(每仿真步 − 0.025 -0.025 −0.025)、障碍感知的路径进展(权重 0.075 0.075 0.075)、目标到达( 2.0 2.0 2.0)、碰撞( 2.0 2.0 2.0)、静止不动( 0.2 0.2 0.2)、人形稳定性( − 0.4 -0.4 −0.4)。碰撞在接触力超过 6.5 6.5 6.5 牛或增量超过 2.0 2.0 2.0 牛时触发,且一米范围内的重复接触按 0.9 0.9 0.9 逐次折扣,避免机器人贴着一面墙被反复重罚。路径进展这一项最值得单独看,它不是简单的欧氏距离缩短,而是在占据栅格上跑 A* 搜索,格子代价为

c = c l i p ( 1 + 0.1 / ( d o b s + 10 − 3 ) ,   1 ,   1000 ) c=\mathrm{clip}\bigl(1+0.1/(d_{\mathrm{obs}}+10^{-3}),\ 1,\ 1000\bigr) c=clip(1+0.1/(dobs​+10−3), 1, 1000)

d o b s d_{\mathrm{obs}} dobs​ 是该格到最近障碍物的距离,所以贴墙的格子代价高,A* 自然偏好间隙大的路径。这解释了定性对比里 X-NavDP 会主动选宽走廊而不是窄缝——这个偏好是从奖励的代价图里长出来的,不是额外加的安全约束。真正反直觉的是进展函数对转向的处理:

# src/environment/tasks/reward_utils.py
def _progress_reward_with_static_turn(distance_change, is_same_dir, turn_delta, progress_threshold):
    """Reward forward progress, penalize backward motion and idle non-turning plans."""
    if distance_change > 2:
        env_reward = distance_change * is_same_dir
        return torch.clamp(env_reward, 0.0, 15.0)

    if distance_change < -1:
        if turn_delta > STATIC_TURN_THRESHOLD_RAD2:
            return torch.zeros_like(distance_change)      # 大幅转向:不罚
        penalized_change = distance_change - progress_threshold
        return torch.clamp(penalized_change * 1.5, -15.0, 0.0)

    if turn_delta > STATIC_TURN_THRESHOLD_RAD1:
        return torch.zeros_like(distance_change)          # 中等转向:不罚
    penalized_change = distance_change - progress_threshold
    return torch.clamp(penalized_change * 1.5, -15.0, 0.0)

两个返回零的分支是专门为脱困留的口子。倒车会让 A* 距离变大(distance_change < -1),按常规逻辑该重罚;但如果这一步同时伴随大幅偏航变化,说明机器人正在原地调头准备绕行,这时奖励置零而不是罚负。is_same_dir 则保证只有当位移和机体朝向同向时才给正奖励,防止机器人倒着"蹭"向目标骗奖励。如果没有这两个零分支,RL 会把刚刚靠扰动采出来的脱困行为全部罚掉,前面第 3 节的努力等于白费。 探索机制和奖励设计在这里是一对必须配套的组合。

在这里插入图片描述

7. 训练目标与完整训练流程

7.1 评论家侧:标准 SAC 骨架

演员侧是 GQRM 这套非常规做法,评论家侧则回到很规矩的双评论家 TD 学习,目标值用目标网络的 min ⁡ ( Q 1 , Q 2 ) \min(Q_{1},Q_{2}) min(Q1​,Q2​) 做保守估计,损失是两个头各自对同一个自举目标做 MSE。整篇方法真正新颖的部分集中在演员,评论家几乎是照搬 SAC,这种"一侧激进一侧保守"的搭配降低了整体调参难度,因为评论家那边的行为是可预期的:

# src/x_navdp/trainer/gqrm_trainer.py  GQRMTrainer.update
with torch.no_grad():
    next_act_batch, _ = self.policy.module.generate_action_mix(
        next_obs_batch, num_samples=1, is_nogoal_perturb=False,
        is_old_policy=False, embodiment=embodiment_batch)
    q1_target, q2_target = self.policy.module.generate_q_value(
        next_obs_batch, next_act_batch, embodiment=embodiment_batch, is_target=True)
    q_target = torch.minimum(q1_target, q2_target)
    q_backup = reward_batch + (1 - done_batch) * self.gamma * q_target

q1, q2 = self.policy(obs_batch, act_batch, embodiment_batch)
value_loss = self.mse_loss(q_backup, q1) + self.mse_loss(q_backup, q2)
value_loss.backward()

这里有个和演员侧对照鲜明的细节:算 TD 目标时用的是 is_nogoal_perturb=False,也就是评论家的自举目标用未扰动的策略动作,而演员更新用扰动候选。这个不对称是有道理的——自举目标要反映策略实际会执行什么(评估时执行未扰动候选),而演员更新需要看到足够宽的动作分布才能挑出改进方向。 γ = 0.99 \gamma=0.99 γ=0.99、 τ = 0.005 \tau=0.005 τ=0.005、目标网络每两次更新做一次 Polyak 软更新、梯度裁剪到 5.0,都是常规配置。仓库里还有一个 depth_mask_prob 开关,按概率把 batch 内部分样本的深度图整体置零,作为对深度传感器失效的鲁棒性增强,默认关闭。

7.2 三段式调度与更新节奏

训练分三个阶段,代码里由几个计数器阈值控制。第一段是回放预热:每个进程头 3000 个向量化仿真步只采数据不更新梯度。第二段是评论家预热:self.start_learn = 100,前 100 次 minibatch 更新只训双评论家,演员冻结不动。第三段是联合迭代:从第 101 次更新开始,评论家每个 minibatch 都更新,演员按 self.delay_update = 2 每两次更新一次。

难点提示(为什么演员必须等评论家):GQRM 的权重完全由 Q 值决定,如果评论家还没学出任何有意义的价值估计就开始更新演员,等于让一个随机数发生器来指挥策略往哪走,会把预训练的好行为迅速破坏掉。先把尺子刻好,再拿它去量东西,这是所有 actor-critic 方法里演员延迟更新的共同动机,在扩散策略上尤其重要,因为被破坏的先验很难恢复。

7.3 并行规模与场景轮换

论文的训练拓扑是每个场景一个进程,每进程 8 个同步 IsaacLab 环境,56 个场景对应 448 个并行环境,README 里发布的模型实际训了 24000 步、72 个场景(16 个 clutter 加 56 个居家商用)。优化节奏是所有进程每 75 个向量化仿真步同步做一轮优化,每轮每进程从本地回放缓冲区取 192 个转移拆成 8 个大小 24 的 minibatch,梯度用 DDP 同步。学习率从 2 × 10 − 5 2\times 10^{-5} 2×10−5 线性衰减到 1 × 10 − 5 1\times 10^{-5} 1×10−5,跨 10000 次优化更新。

有意思的是形态轮换策略:每个进程固定守着自己那个场景不换,但会周期性地换机器人——首次切换在 30000 仿真步,之后每 24000 步换一次,切换时保留本地回放缓冲区和 DDP 同步的网络参数。这个设计让同一个场景的地形知识在不同形态之间迁移,同时避免了频繁重载 USD 场景资产的开销,场景加载在 IsaacLab 里是最慢的一环。启动命令本身很简洁:

export SCENE_DIR=/path/to/NavDP/baselines/x-navdp/data/scenes
export NPROC_PER_NODE=8

bash scripts/run_ddp_train.sh --config_file config/x-navdp_config.yaml

把前面几节的模块按时间顺序串起来,整套后训练闭环的形状就出来了:环境采样产出宏步转移进回放池,评论家先单独预热出一把可用的尺子,演员再用这把尺子对同一状态的候选组做分组加权,而扰动模块持续为候选组注入结构化多样性。

在这里插入图片描述

8. 推理阶段的 RTC 时序一致性引导

8.1 动作块拼接处的抖动从哪来

策略每 3 秒重新规划一次,两段轨迹之间没有任何显式约束,于是在拼接点上很容易出现方向突变:上一段决定从左边绕,新一段因为视角变化决定从右边绕,机器人就在原地摆头。这个问题在窄通道里尤其致命,一次犹豫就可能擦到墙。X-NavDP 借用 Real-Time Chunking 的思路,在推理时给去噪过程加一个一致性引导项,只在部署阶段启用,不参与训练。

g = ∇ x t ( k ) ⟨ sg ⁡  ⁣ ( w t ⊙ ( y t − x ^ t ( 0 , k ) ) ) ,   x ^ t ( 0 , k ) ⟩ , x t ( k − 1 ) = μ k + σ k z + α ˉ k   η guide   g \mathbf{g}=\nabla_{\mathbf{x}_{t}^{(k)}}\bigl\langle\operatorname{sg}\!\bigl(\mathbf{w}_{t}\odot(\mathbf{y}_{t}-\hat{\mathbf{x}}_{t}^{(0,k)})\bigr),\ \hat{\mathbf{x}}_{t}^{(0,k)}\bigr\rangle, \qquad \mathbf{x}_{t}^{(k-1)}=\boldsymbol{\mu}_{k}+\sigma_{k}\mathbf{z}+\sqrt{\bar{\alpha}_{k}}\ \eta_{\text{guide}}\ \mathbf{g} g=∇xt(k)​​⟨sg(wt​⊙(yt​−x^t(0,k)​)), x^t(0,k)​⟩,xt(k−1)​=μk​+σk​z+αˉk​ ​ ηguide​ g

其中 y t \mathbf{y}_{t} yt​ 是上一段已提交轨迹变换到当前机体坐标系的结果, x ^ t ( 0 , k ) \hat{\mathbf{x}}_{t}^{(0,k)} x^t(0,k)​ 是第 k k k 步反向过程对干净轨迹 x 0 \mathbf{x}_{0} x0​ 的一步估计, sg ⁡ ( ⋅ ) \operatorname{sg}(\cdot) sg(⋅) 是停止梯度, w t \mathbf{w}_{t} wt​ 是一组递减的前缀权重, μ k \boldsymbol{\mu}_{k} μk​ 与 σ k \sigma_{k} σk​ 是 DDPM 后验均值与标准差, η guide \eta_{\text{guide}} ηguide​ 控制引导强度。这里的关键是 w t \mathbf{w}_{t} wt​ 只在近端航点上取较大值,远端权重衰减到零——近期要一致,远期允许重规划,这样既消抖动又不牺牲对新观测的响应。

8.2 前缀权重与引导注入的实现

…详情请参照古月居

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐