LingBot-VA 2.0 的观测该按什么粒度喂 / 动作该按什么粒度取?

文章导读
观测粒度和动作粒度不是一个凭手感定的参数,通常要拆成两个可独立调整的量:观测侧先固定成一个定长输入窗口,把“喂进去的一帧”变成“喂进去的一段”;动作侧再单独改“一次推理输出几步动作”。判断标准不靠直觉,而是看日志里能直接数出来的三类信号:相邻动作的跳变、执行侧的超时次数、单位时间内的推理次数。这套做法在常见配置下可用,但因为不同本体、控制周期和推理延迟差别很大,具体取值需要在你的环境里跑对照实验确
📋 目录
  1. 一 把观测组织成定长窗口
  2. 二 固定观测窗口,只改动作粒度做对照
  3. 三 用可观察指标判断粒度是否合适
  4. 四 在仿真里复现同一段任务做对比
  5. 五 把结论写成配置项而不是散落在代码里
A A

观测粒度和动作粒度不是一个凭手感定的参数,通常要拆成两个可独立调整的量:观测侧先固定成一个定长输入窗口,把“喂进去的一帧”变成“喂进去的一段”;动作侧再单独改“一次推理输出几步动作”。判断标准不靠直觉,而是看日志里能直接数出来的三类信号:相邻动作的跳变、执行侧的超时次数、单位时间内的推理次数。这套做法在常见配置下可用,但因为不同本体、控制周期和推理延迟差别很大,具体取值需要在你的环境里跑对照实验确认,不要直接照搬别处的数字。

先把观测窗口固定成不变量,再只调动作粒度,这样每次实验只有一个变量在动。判断粒度是否合适,看执行日志里的动作跳变、超时次数和推理次数,而不是看一次任务跑得“顺不顺”。适用场景是有独立推理进程和执行进程的机器人策略接入;边界是控制周期不稳或推理延迟波动大的环境,结论需要重跑验证。

把观测组织成定长窗口

要让不同粒度可比,先得让每次送进策略的输入是同一个形状。这里定义三个量:窗口长度(一个输入单元包含多少帧观测)、采样间隔(每隔多少帧取一帧)、补帧规则(传感器丢帧时用什么填)。窗口长度和采样间隔一旦定下,在一轮实验里就不要再改,否则观测和动作两个变量会互相污染。

补帧通常有两种取舍:用上一帧的副本顶替,或者直接丢弃整窗。前者保持时序对齐,但要让下游能区分“真实观测”和“补出来的帧”,否则策略会把过时信息当成当前状态;后者更干净,代价是窗口就绪率下降。建议先采用“重复上一帧 + 打标记”的方式,把补帧次数也记进日志。

WINDOW_LEN = 8      # 窗口长度:一个输入单元含多少帧
SAMPLE_STRIDE = 1   # 采样间隔:每隔几帧取一帧

class ObsWindow:
    def __init__(self, length, stride):
        self.length = length
        self.stride = stride
        self.buf = []

    def push(self, frame, ts):
        # 缺帧判断:时间戳间隔明显超过采样间隔,视为丢帧
        if self.buf and ts - self.buf[-1]["ts"] > self.stride * 1.5:
            self.buf.append({"frame": self.buf[-1]["frame"],
                             "ts": ts, "stale": True})
        else:
            self.buf.append({"frame": frame, "ts": ts, "stale": False})
        if len(self.buf) > self.length:
            self.buf.pop(0)
        return self.ready()

    def ready(self):
        return len(self.buf) == self.length

这段骨架只负责拼窗口,frame 的编码方式、stride * 1.5 这个阈值都按你的传感器实际频率替换。把 stale 标记一路带到推理调用处,后面排查“动作为什么突然抖”时会用到。

固定观测窗口,只改动作粒度做对照

对照实验的原则是一次只动一个变量。观测窗口在这个阶段是常量,动作粒度(一次推理取几步动作)才是自变量。下表的取值只是示例,换成你自己的候选值即可。

  • 组号与变量:A1 动作粒度 1 步;A2 动作粒度 2 步;A3 动作粒度 4 步;A4 动作粒度 8 步。
  • 固定项:窗口长度、采样间隔、补帧规则、任务脚本、初始状态全部一致。
  • 重复次数:每组至少重复 3 轮,粒度越大越容易被单次偶然波动带偏。
  • 需要记录的字段:动作粒度值、轮次编号、结果摘要(是否跑完、在哪一步中断、异常类型)。

最小记录模板可以就用一张表,跑完一轮填一行:

LingBot-VA 2.0 的观测该按什么粒度喂 / 动作该按什么粒度取?
粒度(步) | 轮次 | 是否完成 | 中断位置 | 超时次数 | 推理次数 | 备注
1        | 1    |          |          |          |          |
1        | 2    |          |          |          |          |
2        | 1    |          |          |          |          |
4        | 1    |          |          |          |          |

注意“结果摘要”写事实,不写评价。例如写“在第 3 段轨迹处停止,执行侧报 timeout”,而不是写“表现变差”。前者能定位,后者不能。

用可观察指标判断粒度是否合适

粒度是否合适,落到日志上是三类可数的东西:

  • 动作抖动:相邻两次下发的动作差值。如果粒度太小,同一段任务里动作方向会频繁反复;如果粒度太大,动作序列会在切换点出现明显跳变。可以从动作日志里取相邻行的差,看最大值和变化频率。
  • 执行超时次数:执行侧等待新动作的时间超过预期,一般会打印 timeout 或 wait_exceed 之类的记录。这个数从 0 变成非 0,通常是粒度偏大的信号。
  • 单位时间内的推理次数:粒度越小,同样一段任务需要的推理次数越多。统计推理调用的日志行数,除以任务耗时,就能得到频率。

假设日志里每轮开始有 infer_begin,超时有 action_timeout,可以用最普通的命令行先做粗统计,不必先搭监控:

# 单位时间推理次数:行数除以任务耗时(耗时从任务起止日志行取)
grep -c "infer_begin" run_A1_r1.log

# 超时次数
grep -c "action_timeout" run_A1_r1.log

# 相邻动作差值最大值(动作值以两列上报时)
awk '{if (NR>1) {d=$1-p1; if (d<0) d=-d; if (d>m) m=d} p1=$1} END {print m}' action_A1_r1.log

这三个指标要一起看。推理次数高但超时为 0,说明粒度偏小、算力被浪费;超时次数上升,说明执行侧等不到动作,粒度可能偏大或者推理延迟本身就不稳。建议先把超时次数压到可接受范围,再在剩下的候选粒度里选推理次数更低的那个。

LingBot-VA 2.0 的观测该按什么粒度喂 / 动作该按什么粒度取?

在仿真里复现同一段任务做对比

仿真对比要控制住“同一任务”这个前提,否则粒度之间的差异会被任务本身的随机性盖过。

  • 同一初始状态:把初始位姿、关节角、场景物体位置固定下来,用固定随机种子加载,不要每次重置都由仿真器随机生成。
  • 同一任务脚本:目标点、动作上限、终止条件写在同一份脚本里,各组共用,不复制粘贴后再改。
  • 结果对齐方式:以任务脚本里的时间步或阶段编号为对齐点,把各组的指标按同一阶段切段比较,而不是拿总耗时直接对比。
  • 差异记录格式:每组记录“在哪个阶段开始偏离、偏离时的动作粒度和当时的状态量”,格式固定成一行,方便后面横向扫。

如果同一粒度下两轮结果差异本身就很大,说明任务脚本里的随机因素没控制住,此时先解决复现性,不要急着下粒度结论。

把结论写成配置项而不是散落在代码里

实验得到的取值如果硬编码在推理调用处,下次换本体就得改代码。建议抽到配置文件,让观测和动作两组参数放在一起:

policy:
  obs:
    window_len: 8          # 窗口长度
    sample_stride: 1       # 采样间隔
    pad_on_drop: repeat_last
  action:
    chunk_size: 4          # 一次推理输出几步动作
    replan_on_timeout: true
    timeout_ms: 200        # 按你的执行周期调整

改参后的复验步骤固定为:改配置 → 用同一份任务脚本重跑至少 3 轮 → 用上面那三条命令重新统计 → 把结果追加进记录表。不要只看一轮就下结论。

配置值和代码里的默认值冲突时,通常以配置文件为准,代码里的默认值只作为找不到配置时的兜底。但更稳妥的做法不是争论谁优先,而是让程序启动时把最终生效值打印出来,例如输出一行 resolved_config,把窗口长度、采样间隔、动作粒度都带上。之后出现分歧,以日志里那行生效值为准,这样排查时不用再去猜哪份配置被加载了。