WorldSense 技术笔记

从代码理解 RSSM(一):RSSM 在 DreamerV3 中的位置与 stochastic 状态

2026年8月19日 · 阅读约10分钟 · RSSM, DreamerV3, 世界模型, 状态空间模型, 代码解析, RSSM系列
目录

《从代码理解 RSSM》系列 · 第 1 篇 / 共 6 篇

系列目录(当前在第 1 篇,已加粗;上/下一篇见文末导航): 1. (一)RSSM 的位置与 stochastic 状态 2. (二)先验/后验、straight-through 与 unimix 3. (三)_core()、deter=8192 与 Block GRU 4. (四)KL balancing、Free Nats 与最终 KL 5. (五)Imagine、Observe/Imagine 区别与 Reset 6. (六)默认配置、四条公式与对照表

源码阅读提示

本文有意不从"标准 RSSM 伪代码"开始,而是按照 rssm.py 的实际执行路径展开。阅读时可以重点关注下面几个函数:

observe()   → sequence-level wrapper,沿时间维调用 _observe()
_observe()  → 单个时间步的状态推断(真实轨迹)
_core()     → 单个时间步的 deterministic dynamics
imagine()   → 无 observation 的 latent rollout

这里最容易混淆的一点是:observe() 并不是一个"处理单步 transition"的函数。它只是 sequence-level 的封装,内部通过 Ninjax 的 nj.scan(...) 沿着时间维度反复调用 _observe();真正处理单个时间步的是 _observe()_core()。把 observe() / _observe() / _core() / imagine() 这一层调用关系理清,基本就能理解 DreamerV3 RSSM 的主体。

(你后面会在"Sequence training"一节看到 nj.scan 的具体展开;这里先记一句话:第一次出现 observe() 时,它和 _observe()nj.scan 是同一件事的三个层次。)

前面两篇文章分别介绍了 RSSM 的基本原理和世界模型的发展路线。

理论解决的是"RSSM 为什么这样设计",但真正打开 DreamerV3 的源码后,会发现还有很多细节是论文公式没有直接告诉你的:

这篇文章不再从一个通用的 Gaussian RSSM 伪代码出发,而是直接沿着 DreamerV3 仓库中的 dreamerv3/rssm.py 追踪数据流,把源码中的计算路径和 RSSM 数学公式对应起来。

源码说明

本文参考的是 DreamerV3 开源仓库当前 main 分支中的 dreamerv3/rssm.pyconfigs.yaml。该仓库 README 将自身描述为 DreamerV3 的 reimplementation,因此本文统一称其为"DreamerV3 开源实现",而不是 Google/DeepMind 官方代码。

为了方便阅读,文中会对 JAX、Ninjax、dtype 和 scan 等工程代码进行适当简化,但核心计算逻辑以源码为准。文中凡是涉及"默认配置"的地方,都明确区分了 RSSM 架构参数World model / agent 的 loss 配置,避免把某个 agent 的超参误读成 RSSM 本身的结构。


一、先看 RSSM 在 DreamerV3 中的位置

DreamerV3 的世界模型可以粗略理解为:

Observation
  Encoder
 observation token
 ┌─────────────────────────────┐
 │            RSSM             │
 │                             │
 │  deterministic state h_t    │
 │            +                │
 │  stochastic state z_t       │
 └─────────────────────────────┘
 latent feature
     ├──► Decoder       重建 observation
     ├──► Reward Head   预测 reward
     ├──► Continue Head
     └──► Actor/Critic  想象轨迹上的策略与价值

RSSM 要解决的问题可以概括成一句话:

根据过去的 latent state 和 action,维护一个可以不断向未来滚动的隐状态。

这个状态由两部分组成:

s_t = (h_t, z_t)

其中:

这里要先强调一个重要区分(后面"为什么 8192"一节还会展开):最终喂给 Decoder / Reward / Actor-Critic 的不是单独的 h_tz_t,而是二者的拼接 feature = concat(h_t, z_t)。所以 8192 并不是"latent state 总维度"。

DreamerV3 的一个关键变化就在这里:

z_t 不是传统连续 Gaussian RSSM 中的一个普通向量,而是多个 categorical latent variable。


二、先解决一个最容易误解的问题:stoch 到底是什么?

很多 RSSM 教程会直接写:

z_t ~ Normal(μ_t, σ_t)

然后通过:

z = μ + σ × ε

完成采样。

这种写法可以帮助理解经典连续 RSSM,但不能直接套到 DreamerV3 的实现上

DreamerV3 使用的是 categorical latent。

默认配置(注意:下面混排了"RSSM 架构"与"world-model 训练"两类参数,正式拆分见"默认 RSSM 配置"一节):

rssm:
  deter: 8192
  hidden: 1024
  stoch: 32
  classes: 64
  unimix: 0.01
  blocks: 8

因此 stochastic state 的形状是:

[B, 32, 64]

也就是说:

如果把它展平:

32 × 64 = 2048

所以可以把整个 stochastic state 看成一个 2048 维向量,但语义上不能简单把它理解成一个 2048 维普通 categorical variable

更准确地说:

z_t = [z_t^1, z_t^2, ..., z_t^32]

其中:

z_t^i ∈ {1, ..., 64}

每个 z_t^i 都是一个 64 类 categorical variable,它们构成的是一个 factorized categorical distribution(因子化的类别分布),而不是一个 2048 类的单一分布。

源码中的 _logit() 正是在做这件事:

x = Linear(..., self.stoch * self.classes)(x)
return x.reshape(
    x.shape[:-1] + (self.stoch, self.classes)
)

也就是:

Linear
32 × 64 logits
[B, 32, 64]

三、Observe:真实 observation 是怎么进入 RSSM 的?

理解 DreamerV3 RSSM,最重要的入口就是:

observe(...)

但回到开头那句提醒:公开接口 observe() 本身不处理单步 transition。它的核心作用是 sequence-level 的封装,通过 nj.scan 把下面这段 _observe() 沿着时间维反复调用。所以读者继续往下看源码时,应当把"处理单个时间步逻辑"的那段代码理解为 _observe(),而不是 observe()

_observe() 的核心逻辑可以简化成:

def _observe(carry, tokens, action, reset, training):

    deter, stoch, action = mask(
        carry["deter"],
        carry["stoch"],
        action,
        ~reset
    )

    action = preprocess_action(action)

    # 关键:这里没有 observation
    deter = self._core(
        deter,
        stoch,
        action
    )

    # observation token 在这里才进入
    x = concat([deter, tokens])

    logit = posterior_network(x)

    stoch = sample(logit)

    return {
        "deter": deter,
        "stoch": stoch,
    }

这里有一个非常关键的细节:

_core() 不读取当前 observation。

当前 observation embedding,也就是 tokens,是在 deterministic transition 完成以后,才进入 posterior 网络。

所以整个过程实际上是:

z_{t-1} ─────┐
h_{t-1} ─────┼──► RSSM Core ──► h_t
a_{t-1} ─────┘

                  posterior network
                      token_t
                       z_t

这和很多"GRU 输入 observation + action"的简化 RSSM 写法是不一样的。



← 世界模型风口被放大了:技术辨析与冷思考 从代码理解 RSSM(二):先验/后验、straight-through 采样与 unimix →

评论

W
侯晓琴

西北工业大学硕士,十余年自动化与 AI 工程经验。著有《Visual C++入门很容易》《C++程序设计经典300例》。目前聚焦世界模型与具身智能方向,记录从传统自动化到机器人 AI 的转型之路。