Dreamer
A from-scratch DreamerV3 (Hafner et al., "Mastering Diverse Domains through World Models", https://arxiv.org/abs/2301.04104) agent: an RSSM world model with categorical latents, trained jointly with an actor and critic on imagined rollouts. Implements the paper's five headline robustness techniques, cross-checked directly against the official implementation (github.com/danijar/dreamerv3, dreamerv3/rssm.py and embodied/jax/agent.py) rather than assumed from the paper text alone:
- Symlog inputs/reconstruction (
rlax.signed_logp1/signed_expm1, used by theSymlogEncoderandTwoHotHeadin.models). - Categorical latents (
num_latentsindependent categoricals ofnum_classeseach, "stoch"/"classes" in the official implementation) with straight-through gradients and 1% "unimix" - mixing a little uniform mass into every categorical, so no class ever gets a literal zero probability - for both the prior and posterior. - KL balancing with free bits: two separate KL terms with different stop-gradient placement and independent free-nats floors, not one combined KL clamped by a single scalar.
- Symexp-twohot regression for reward and value (
TwoHotHead), not a Gaussian/MSE head - a discrete classification loss over an exponentially-spaced grid of bins, which is far less sensitive to reward-scale outliers than a Gaussian likelihood. - Return normalization: an EMA-tracked 5th-95th percentile range of returns rescales advantages, so the policy gradient's magnitude stays stable across environments with very different reward scales, without per-environment tuning.
Also matches the official implementation's actor loss shape: REINFORCE (log_prob(action) * advantage + entropy bonus), not backpropagation through sampled discrete actions - discrete distrax.Categorical.sample() has no gradient path back to its logits, so naively differentiating an imagined-rollout return through a sampled discrete action (as an earlier draft of this agent did) trains nothing through that path at all - and an EMA "slow" target critic that the online critic is regularized toward, for training stability.
Deliberate simplifications, kept for navix's small grid-world
observations rather than image-scale ones: a plain nn.GRUCell for the
deterministic recurrent state (not the official "block GRU", a
parameter-efficiency optimization for much larger deter sizes); a plain
symlog+MSE decoder for observation reconstruction (not the official
implementation's image-specific CNN decoder); ELU activations and no
RMSNorm (not load-bearing for correctness, just a smaller/simpler net);
num_latents/num_classes default to 8x8 rather than the paper's 32x32, sized
for navix's small grids rather than Atari-scale observations. None of
these are the algorithmic identity of DreamerV3 - the five techniques
above are.
The world model's reusable building blocks (categorical-latent
utilities, the symexp-twohot head, and the RSSM's encoder/decoder/prior/
posterior networks) live in .models, alongside PPO's shared network
components; this module holds what's specific to Dreamer itself: the
WorldModel that wires those blocks into an RSSM, the actor/critic
heads, and the Dreamer agent's collection/replay/training loop.
Dreamer
Bases: Agent
collect_experience(ts)
Runs hparams.num_steps steps in hparams.num_envs parallel
envs, carrying the per-env posterior latent (h, z, a_prev_oh)
across steps so the policy always acts on an up-to-date belief.
train_first_update(rng)
Runs initialisation plus exactly one update() call. Mainly
useful for tests/debugging, where running a full train() (sized
by hparams.budget) is either unnecessary or awkward to size
exactly.
DreamerHparams
Bases: HParams
Hyperparameters for Dreamer (DreamerV3). Frozen; .replace(...)
for a variant. Groups the world-model, actor and critic knobs plus
the imagination-rollout schedule; sized for navix's small
observations rather than Atari.
actor_entropy = 0.0003
class-attribute
instance-attribute
Entropy bonus coefficient in the actor's REINFORCE loss.
actor_lr = 0.0003
class-attribute
instance-attribute
Learning rate for the actor's optimizer.
actor_unimix = 0.05
class-attribute
instance-attribute
Fraction of uniform probability mixed into the actor's action
distribution wherever it's sampled from or scored (data collection,
imagination, and the REINFORCE loss) - the same unimix_categorical
technique already used for the world model's own categorical latents
(see unimix_categorical's docstring), applied here too. Without
this, the actor's entropy can reach exactly zero: once it does,
collect_experience (which samples actions from this same distribution) stops
exploring too, so real data collection narrows to whatever the
collapsed policy repeats, the world model overfits to that narrow
trajectory, and there is no path back - a self-reinforcing collapse
verified empirically (entropy hits exactly 0.0 and success rate
permanently flatlines at 0%, independent of how many actor gradient
steps are taken per update - slowing that down only delays the same
terminal collapse, it doesn't prevent it). A structural floor on the
minimum action probability makes that specific failure mode
impossible rather than merely less likely. Higher than the world
model's 0.01 default since the action space here is much smaller
(a handful of actions vs. many latent classes), so the same
probability mass floor matters proportionally more per action.
batch_size = struct.field(pytree_node=False, default=64)
class-attribute
instance-attribute
Number of sequences sampled per world-model gradient step.
bins = struct.field(pytree_node=False, default=41)
class-attribute
instance-attribute
Number of bins for the reward/value symexp-twohot heads (paper: 255; reduced here since navix's rewards/returns span a far smaller dynamic range than Atari's).
bins_high = 20.0
class-attribute
instance-attribute
Upper edge of the symlog-space bin range.
bins_low = -20.0
class-attribute
instance-attribute
Lower edge of the symlog-space bin range.
budget = struct.field(pytree_node=False, default=1000000)
class-attribute
instance-attribute
Number of environment frames to train for.
critic_lr = 0.0003
class-attribute
instance-attribute
Learning rate for the critic's optimizer.
discount = 0.99
class-attribute
instance-attribute
Discount factor used in the imagined-rollout lambda-returns.
dyn_scale = 1.0
class-attribute
instance-attribute
Weight of the KL(sg(post)||prior) ("dynamics") term.
embed_size = 128
class-attribute
instance-attribute
Size of the encoder's output embedding.
free_nats = 1.0
class-attribute
instance-attribute
Per-(batch,time) KL floor - below this, dyn/rep loss is zero.
hidden_size = 200
class-attribute
instance-attribute
Hidden layer size used throughout the model/actor/critic MLPs.
imag_horizon = struct.field(pytree_node=False, default=15)
class-attribute
instance-attribute
Length of the imagined rollouts used to train the actor and critic.
lam = 0.95
class-attribute
instance-attribute
Lambda parameter of the imagined-rollout lambda-returns.
latents_flat
property
Flattened size of the categorical latent (num_latents *
num_classes), i.e. how much of feat = concat([h, z_flat]) the
latent occupies.
max_grad_norm = 100.0
class-attribute
instance-attribute
Maximum gradient norm for clipping, applied to each of the three optimizers independently.
model_lr = 0.0003
class-attribute
instance-attribute
Learning rate for the world model's optimizer.
num_actor_updates = struct.field(pytree_node=False, default=128)
class-attribute
instance-attribute
Number of actor gradient steps per update (see num_model_updates for why the default is high).
num_classes = struct.field(pytree_node=False, default=8)
class-attribute
instance-attribute
Number of classes per categorical latent variable ("classes" in the official implementation).
num_critic_updates = struct.field(pytree_node=False, default=128)
class-attribute
instance-attribute
Number of critic gradient steps per update (see num_model_updates for why the default is high).
num_envs = struct.field(pytree_node=False, default=16)
class-attribute
instance-attribute
Number of parallel environments to run.
num_latents = struct.field(pytree_node=False, default=8)
class-attribute
instance-attribute
Number of independent categorical latent variables ("stoch" in the official implementation).
num_model_updates = struct.field(pytree_node=False, default=128)
class-attribute
instance-attribute
Number of world-model gradient steps per update. The default is
deliberately high relative to the frames collected per update (a
replay ratio in the spirit of the official implementation's
train_ratio): DreamerV3 is designed to be sample-efficient by
gradient-stepping far more often than it collects. Concretely, with
a sparse reward the rewarded transitions can be ~0.2% of the replay
data, and a mean-reduced twohot cross-entropy pushes the reward head
toward the base-rate prediction (~0) until it has seen enough
positive examples to separate them - at 32 steps/update the reward
head was still predicting ~0.004 at real goal transitions after a
full 100k-frame run (policy stuck at random-walk success), while at
128 it reaches ~0.98 and the same run ends at 100% success.
num_steps = struct.field(pytree_node=False, default=128)
class-attribute
instance-attribute
Number of environment steps to collect per update.
recurrent_size = 200
class-attribute
instance-attribute
Size of the RSSM's deterministic (GRU) hidden state ("deter" in the official implementation).
rep_scale = 0.1
class-attribute
instance-attribute
Weight of the KL(post||sg(prior)) ("representation") term.
replay_capacity = struct.field(pytree_node=False, default=500000)
class-attribute
instance-attribute
Maximum number of environment frames kept in the replay buffer (rounded down to whole collection rollouts, and never allocated larger than the training budget itself can fill).
return_norm_limit = 1.0
class-attribute
instance-attribute
Floor on the return-normalization scale (perc95 - perc5), so a near-constant reward signal doesn't blow the advantage up by dividing by a near-zero scale.
return_norm_rate = 0.01
class-attribute
instance-attribute
EMA rate for the return-normalization percentile tracker.
seq_len = struct.field(pytree_node=False, default=32)
class-attribute
instance-attribute
Length of the sequences sampled for world-model training.
slow_critic_rate = 0.02
class-attribute
instance-attribute
EMA rate the slow critic's params track the online critic at.
slow_critic_reg = 1.0
class-attribute
instance-attribute
Weight of the online critic's regularization loss toward the slow critic's prediction, on top of its lambda-return regression loss.
unimix = 0.01
class-attribute
instance-attribute
Fraction of uniform probability mixed into every categorical.
DreamerTrainState
Bases: PyTreeNode
Wraps three independent TrainStates, one per network - the
world model, actor, and critic each have their own optimizer,
learning rate and step counter, and are updated via their own
apply_gradients(). An earlier draft instead subclassed TrainState
directly and shared one tx/step field meant to be swapped between
the three networks' updates; the swap never actually took effect
before each network's update ran, so actor and critic gradients were
silently applied through the model's optimizer (and learning rate)
instead of their own, and the step counter never incremented since
apply_gradients() was never called. Three separate TrainStates
make both bugs structurally impossible instead of relying on
remembering to swap a shared field correctly.
slow_critic_params is a plain EMA-tracked copy of the critic's
params (not a fourth optimized TrainState - nothing ever computes a
gradient w.r.t. it, it only ever gets updated by exponential
averaging toward critic.params), used both to regularize the online
critic's training and to compute the return-normalization statistics.
create(rng, hparams, env, world, actor, critic)
classmethod
Builds an initial DreamerTrainState: inits the world model/
actor/critic's params and optimizers, resets hparams.num_envs
environments, and allocates an empty replay buffer sized to
whichever is smaller of hparams.replay_capacity and what
hparams.budget can actually fill. Mirrors the flax.training.
train_state.TrainState.create convention PPO's own TrainingState
relies on (construction logic lives on the state itself, not on
the agent that produces it).
Replay
Bases: PyTreeNode
A fixed-capacity FIFO replay buffer of whole collection rollouts
("blocks" of num_steps x num_envs transitions), carried inside
DreamerTrainState so it lives through the jax.lax.scan training
loop. DreamerV3 is a replay-based algorithm: the world model must
keep training on past experience, not only the rollout just
collected - with a sparse reward, a rare success transition seen only
in the update it happened in is forgotten by the reward head one
update later, so imagination goes back to predicting zero reward
everywhere and the actor's learning signal vanishes as soon as it
appears. (An earlier version had no replay at all - _sample_batch
read directly from the latest rollout - which is exactly the failure
mode that produced isolated windows of success that never
consolidated.)
Blocks are written whole (one per Dreamer.update() call) at a
rolling index; sampled sequences never straddle two blocks, so the
rolling overwrite boundary can't splice unrelated timelines together.
Only what world-model training needs is stored (obs flattened, action,
reward, done, termination) - not the logging-only fields of Buffer.
WorldModel
Bases: Module
imagine(start_h, start_z_flat, actor_logits_fn, horizon, rng)
Rolls the RSSM forward purely through its own prior (no real
observations), sampling actions from actor_logits_fn at each
step - the "imagined" trajectories the actor and critic train on.
Returns (feats, rewards, terms, actions, action_logps), all
(B, H, ...) (batch-major). rewards is the reward head's
decoded mean (plain array, not a TwoHotHead logits tensor);
actions/action_logps are returned because the actor loss
needs log_prob(action) under the current actor params for its
REINFORCE term - recomputed at loss-value-and-grad time from the
stored actions, not reused from here (this rollout's actor calls
may run in a different tracing context).
init_probe(obs, a_prev_oh)
Touches every submodule exactly once, purely so WorldModel.
init() can discover every parameter's shape. Parameters are keyed
by submodule path, not by how many times a submodule is called in
the "real" forward pass, so a single non-scanned pass creates the
identical parameter tree observe() would - deliberately used
instead of observe() for .init(), because .init() does its
own internal tracing to discover shapes, which doesn't compose
safely with tracing through observe()'s internal jax.lax.scan
while already inside an outer jax.jit (surfaces as a confusing
UnexpectedTracerError pointing at an unrelated submodule).
observe(obs_seq, act_seq, first_seq)
Runs the RSSM over an observed sequence, computing the posterior latent at every step and decoding obs/reward/term from it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs_seq
|
Array
|
|
required |
act_seq
|
Array
|
|
required |
first_seq
|
Array
|
|
required |
Returns:
| Type | Description |
|---|---|
Tuple[Array, Array]
|
`((h_seq, z_seq), (dyn_kls, rep_kls), feats, obs_pred, rew_logits, |
Tuple[Array, Array]
|
term_logits) |
Array
|
|
Array
|
their own |
Array
|
|
Array
|
distribution objects) because jax.lax.scan only restacks a |
Tuple[Tuple[Array, Array], Tuple[Array, Array], Array, Array, Array, Array]
|
distrax.Distribution's array leaves across the new leading |
Tuple[Tuple[Array, Array], Tuple[Array, Array], Array, Array, Array, Array]
|
axis; its batch_shape/event_shape (computed once from the |
Tuple[Tuple[Array, Array], Tuple[Array, Array], Array, Array, Array, Array]
|
un-stacked per-step arrays) go stale, so calling e.g. |
Tuple[Tuple[Array, Array], Tuple[Array, Array], Array, Array, Array, Array]
|
.kl_divergence() after the scan raises a shape mismatch deep |
Tuple[Tuple[Array, Array], Tuple[Array, Array], Array, Array, Array, Array]
|
inside distrax/tfp. |
posterior_step(h, z_flat, a_prev_oh, obs, rng)
One environment-collection step: advances the RSSM's belief with
the previous action, then updates it to the posterior given the new
observation. SymlogEncoder/RSSM/posterior are submodules bound to this
WorldModel instance, so - unlike a bare Dense/Sequential module -
they can only be called from inside a WorldModel.apply() trace,
which is what this method (called via self.world.apply(...,
method=WorldModel.posterior_step)) provides for collect_experience.
prior_step(h, z_flat, a_oh, rng)
One imagined (prior/dynamics) transition with a caller-supplied
action - the same mechanics as a single imagine rollout step,
but decoupled from the actor. Returns (feat_next, reward_mean,
term_prob). Used for diagnosis: comparing the reward/term heads'
predictions on prior-sampled latents at known real transitions
(e.g. forcing the recorded goal-reaching action from the recorded
pre-goal posterior state) against their predictions on posterior
latents isolates whether a learned signal actually survives into
imagination, where the actor is trained.