Part V: Discovery Through Simulation and Optimization
Chapter 44: World Models for Discovery

44.1 Latent Dynamics Models

"I compress the entire universe into 256 dimensions. The remaining dimensions file complaints, but I ignore them because my ELBO is improving."

A Variational Autoencoder With Delusions of Grandeur

Prerequisites

This section requires familiarity with PyTorch (autograd, nn.Module, training loops), variational inference basics (Kullback-Leibler (KL) divergence, reparameterization trick) from Chapter 32, and recurrent neural networks at a conceptual level. All components are built from scratch, so no prior world model experience is needed.

The Big Picture

A world model learns to predict what happens next: given the current state and an action, it produces a distribution over the next state. When the raw observation space is high-dimensional (images, spectra, molecular conformations), operating directly in observation space is wasteful and fragile. Latent dynamics models solve this by learning a compact latent state that captures the information relevant for prediction, then modeling dynamics entirely within that latent space. The Recurrent State-Space Model (RSSM) is the dominant architecture for this task, splitting the latent state into a deterministic component (memory) and a stochastic component (uncertainty). Training uses the Evidence Lower Bound (ELBO), the same variational objective that appears in Variational Autoencoders (VAEs) (Chapter 34) but extended to sequential data.

1. Why Latent Dynamics?

Consider a robot arm performing chemical synthesis. Its camera produces 84x84 RGB images at each timestep, yielding an observation space of \(\mathbb{R}^{84 \times 84 \times 3}\). Predicting the next image pixel-by-pixel is a 21,168-dimensional regression problem, and most of those pixels (the static lab bench, the walls) carry no information about the dynamics. A latent dynamics model first encodes each observation into a compact representation \(z_t \in \mathbb{R}^d\) where \(d \ll 21{,}168\), then models transitions in this latent space: \(p(z_{t+1} | z_t, a_t)\). The savings are dramatic: both in the dimensionality of the prediction problem and in the amount of training data required.

A latent dynamics model is a neural network that predicts environment transitions in a compressed, learned coordinate system rather than in raw observation space. Low-dimensional latent spaces make prediction, planning, and uncertainty estimation tractable for environments whose raw observations (images, spectra, sensor arrays) are far too large to model directly. Three components implement this: an encoder maps each observation to a latent vector, a transition network predicts the next latent vector given the current one and an action, and a decoder maps latent vectors back to observations. Together, these form an end-to-end pipeline trained with reconstruction loss. Use a latent dynamics model when observations are high-dimensional and you need multi-step prediction or planning. Use a direct (observation-space) dynamics model only when the state is already compact (fewer than roughly 20 continuous dimensions) and you want to avoid learning an encoder and decoder.

Compression as Automated Variable Discovery

This compression does more than save compute. It forces the model to identify the variables that matter for prediction: arm position, liquid color, temperature. These are the same quantities a scientist would record in a lab notebook, and the latent dynamics model discovers them automatically from raw observations. In short: The latent space does not merely compress observations; it distills them into the variables that govern what happens next.

Common Misconception

A frequent misunderstanding is that the latent space in a latent dynamics model is simply a compressed copy of the observation, like a JPEG of an image. In reality, the latent representation is shaped by the dynamics objective, not just reconstruction quality: the model learns to keep information that helps predict the future and discard information that does not, even if that discarded information would improve pixel-level reconstruction. Two observations that look very different (a beaker photographed from two angles) may map to nearly the same latent state if they imply the same future dynamics, while two observations that look nearly identical (the same beaker at 99 C vs. 101 C) may map to distant latent states if the temperature difference triggers a phase change.

Key Insight: Latent States as Learned Sufficient Statistics

A well-trained latent state \(z_t\) is a sufficient statistic for predicting future observations: \(p(o_{t+1:T} | o_{1:t}, a_{1:T}) \approx p(o_{t+1:T} | z_t, a_{t:T})\). The model strips away everything in the observation that is irrelevant to the future. For scientific discovery, this means the model automatically identifies the minimal set of variables needed to predict the outcome of an experiment, a form of automated dimensionality reduction that complements the representation learning methods of Chapter 26.

2. The Recurrent State-Space Model (RSSM)

Without the right latent structure, world models fail silently: they produce confident predictions that diverge from reality within a few steps, leading planners to choose actions that look optimal in imagination but perform catastrophically in the real environment. The RSSM architecture was designed to prevent exactly this failure.

Knowing that a compact latent space helps is only the first step; the critical design question is how to structure that space so it can faithfully capture both the predictable and the uncertain aspects of real-world dynamics.

The RSSM, introduced by Hafner et al. (2019) in the PlaNet paper and refined through DreamerV1, V2, and V3, is among the most successful latent dynamics architectures developed so far. Its key innovation is splitting the latent state into two components:

Checkpoint

So far: a latent dynamics model compresses high-dimensional observations into a compact latent vector, and the RSSM splits that vector into a deterministic part (memory, carried by a GRU) and a stochastic part (uncertainty, sampled from a learned categorical distribution), giving the model both stable recall and the ability to represent environmental randomness.

The full RSSM consists of four components, each implemented as a neural network. Figure 44.1 illustrates how these components connect at a single timestep. Figure 44.1.1 illustrates RSSM architecture dataflow.

RSSM architecture dataflow
Figure 44.1.1: RSSM architecture dataflow across three timesteps, showing how the deterministic state (GRU) and stochastic state (prior/posterior) interact, with encoder and decoder connections to the observation space.
h(t-1) s(t-1) a(t-1) Sequence Model (GRU Cell) h(t) Prior p(s_t | h_t) Posterior q(s_t | h_t, o_t) o(t) s_hat(t) [imagination] s(t) KL loss Decoder p(o_t | h_t, s_t) o_hat(t) Training flow Imagination / KL
Figure 44.1: RSSM architecture at a single timestep. The sequence model (GRU) updates the deterministic state h(t) from the previous deterministic state, stochastic state, and action. The prior predicts the stochastic state from h(t) alone (used during imagination), while the posterior refines this prediction using the real observation o(t) (used during training). The KL divergence between prior and posterior trains the dynamics predictor. The decoder reconstructs the observation from the combined latent state (h(t), s(t)).
$$ \begin{aligned} \text{Sequence model:} \quad & h_t = f_\phi(h_{t-1}, s_{t-1}, a_{t-1}) \\ \text{Prior (dynamics predictor):} \quad & \hat{s}_t \sim p_\phi(s_t | h_t) \\ \text{Posterior (representation):} \quad & s_t \sim q_\phi(s_t | h_t, o_t) \\ \text{Decoder (observation):} \quad & \hat{o}_t \sim p_\phi(o_t | h_t, s_t) \end{aligned} $$

The sequence model is a GRU that updates the deterministic state. The prior predicts the stochastic state from the deterministic state alone (no observation), which is what we use during planning when we imagine trajectories without real observations. The posterior incorporates the actual observation \(o_t\) to produce a more accurate stochastic state during training. The decoder reconstructs the observation from the full latent state \((h_t, s_t)\), providing the reconstruction signal for learning.

Step-Through: One RSSM Timestep

Trace one update step with concrete numbers. Suppose \(h_0 = [0.1, 0.3]\) (deterministic, size 2 for illustration), \(s_0 = [1, 0, 0, 1]\) (two categoricals, two classes each), and \(a_0 = [0.5]\).

Step 1 (Sequence model): Concatenate \([s_0; a_0] = [1, 0, 0, 1, 0.5]\), project through a linear layer and ELU, then feed into the GRU with \(h_0\). Output: \(h_1 = [0.42, -0.11]\).

Step 2 (Prior): Feed \(h_1\) alone through the prior network. It produces logits \([1.2, -0.3, 0.7, 0.1]\), reshaped to two categoricals: \([1.2, -0.3]\) and \([0.7, 0.1]\). Softmax gives probabilities \([0.82, 0.18]\) and \([0.65, 0.35]\). Sampling yields prior \(\hat{s}_1 = [1, 0, 1, 0]\).

Step 3 (Posterior): Feed \([h_1; o_1]\) (deterministic state concatenated with the real observation) through the posterior network. It produces logits \([2.1, -1.0, -0.5, 1.8]\), giving probabilities \([0.96, 0.04]\) and \([0.09, 0.91]\). Sampling yields posterior \(s_1 = [1, 0, 0, 1]\). The KL between posterior \([0.96, 0.04]\) and prior \([0.82, 0.18]\) for the first categorical is \(0.96 \ln(0.96/0.82) + 0.04 \ln(0.04/0.18) \approx 0.10\) nats.

Step 4 (Decode): Feed \([h_1; s_1] = [0.42, -0.11, 1, 0, 0, 1]\) through the decoder to reconstruct \(\hat{o}_1\). The mean squared error (MSE) between \(\hat{o}_1\) and real \(o_1\) is the reconstruction loss for this step.

Practical Example: RSSM for Chemical Reactor Monitoring

A chemical engineer monitors a continuous stirred-tank reactor (CSTR) through temperature sensors, pressure gauges, and spectroscopic readings (collectively, the observation \(o_t\)). The deterministic state \(h_t\) captures the slow, predictable evolution of the reaction: gradual temperature rise, steady pressure increase. The stochastic state \(s_t\) captures fast, unpredictable fluctuations: turbulent mixing events, catalyst deactivation spikes, measurement noise. By separating these two timescales, the RSSM can make accurate multi-step predictions of the reactor's behavior, enabling the planner (Section 44.3) to optimize feed rates and cooling schedules without running dangerous experiments on the real reactor.

The following implementation builds the core RSSM in PyTorch, with each component as a separate module composed together.

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import OneHotCategorical


class RSSM(nn.Module):
    """Recurrent State-Space Model following the DreamerV3 architecture.

    Splits latent state into deterministic (h) and stochastic (s) parts.
    Stochastic state uses categorical distributions (num_classes x num_categoricals).
    """

    def __init__(
        self,
        obs_dim: int,
        action_dim: int,
        det_size: int = 256,
        num_categoricals: int = 32,
        num_classes: int = 32,
        hidden_size: int = 256,
    ):
        super().__init__()
        self.det_size = det_size
        self.num_categoricals = num_categoricals
        self.num_classes = num_classes
        self.stoch_size = num_categoricals * num_classes

        # Sequence model: GRU updates deterministic state
        self.gru_input_proj = nn.Linear(
            self.stoch_size + action_dim, hidden_size
        )
        self.gru_cell = nn.GRUCell(hidden_size, det_size)

        # Prior: predict stochastic state from deterministic alone
        self.prior_net = nn.Sequential(
            nn.Linear(det_size, hidden_size),
            nn.ELU(),
            nn.Linear(hidden_size, self.stoch_size),
        )

        # Posterior: predict stochastic state from deterministic + observation
        self.posterior_net = nn.Sequential(
            nn.Linear(det_size + obs_dim, hidden_size),
            nn.ELU(),
            nn.Linear(hidden_size, self.stoch_size),
        )

        # Observation decoder
        self.decoder = nn.Sequential(
            nn.Linear(det_size + self.stoch_size, hidden_size),
            nn.ELU(),
            nn.Linear(hidden_size, hidden_size),
            nn.ELU(),
            nn.Linear(hidden_size, obs_dim),
        )

        # Reward predictor (for reinforcement learning tasks)
        self.reward_head = nn.Sequential(
            nn.Linear(det_size + self.stoch_size, hidden_size),
            nn.ELU(),
            nn.Linear(hidden_size, 1),
        )

    def initial_state(self, batch_size: int, device: torch.device):
        """Return zero-initialized deterministic and stochastic states."""
        h = torch.zeros(batch_size, self.det_size, device=device)
        s = torch.zeros(batch_size, self.stoch_size, device=device)
        return h, s

    def _categorical_straight_through(self, logits: torch.Tensor):
        """Sample from categorical with straight-through gradients."""
        shape = logits.shape[:-1]  # (batch, num_categoricals)
        logits_reshaped = logits.view(*shape, self.num_categoricals, self.num_classes)
        dist = OneHotCategorical(logits=logits_reshaped)
        sample = dist.sample()
        # Straight-through: forward uses sample, backward uses softmax
        probs = dist.probs
        sample_st = sample + probs - probs.detach()
        return sample_st.view(*shape[:-0], self.stoch_size), dist

    def sequence_step(self, h_prev, s_prev, action):
        """One step of the sequence model (GRU)."""
        gru_input = self.gru_input_proj(torch.cat([s_prev, action], dim=-1))
        h = self.gru_cell(F.elu(gru_input), h_prev)
        return h

    def prior(self, h):
        """Predict stochastic state distribution from deterministic state."""
        logits = self.prior_net(h)
        s, dist = self._categorical_straight_through(logits)
        return s, dist, logits

    def posterior(self, h, obs):
        """Predict stochastic state distribution from deterministic + observation."""
        logits = self.posterior_net(torch.cat([h, obs], dim=-1))
        s, dist = self._categorical_straight_through(logits)
        return s, dist, logits

    def decode(self, h, s):
        """Reconstruct observation from full latent state."""
        latent = torch.cat([h, s], dim=-1)
        obs_pred = self.decoder(latent)
        reward_pred = self.reward_head(latent)
        return obs_pred, reward_pred

    def observe(self, observations, actions):
        """Process a sequence of (observation, action) pairs.

        Args:
            observations: (batch, time, obs_dim)
            actions: (batch, time, action_dim)

        Returns:
            Dictionary of posterior states, prior states, predictions.
        """
        batch_size, seq_len = observations.shape[:2]
        device = observations.device
        h, s = self.initial_state(batch_size, device)

        prior_logits_list = []
        posterior_logits_list = []
        obs_preds = []
        reward_preds = []

        for t in range(seq_len):
            # Step 1: update deterministic state
            h = self.sequence_step(h, s, actions[:, t])

            # Step 2: compute prior and posterior
            _, _, prior_logits = self.prior(h)
            s, _, posterior_logits = self.posterior(h, observations[:, t])

            # Step 3: decode from posterior state (for training)
            obs_pred, reward_pred = self.decode(h, s)

            prior_logits_list.append(prior_logits)
            posterior_logits_list.append(posterior_logits)
            obs_preds.append(obs_pred)
            reward_preds.append(reward_pred)

        return {
            "prior_logits": torch.stack(prior_logits_list, dim=1),
            "posterior_logits": torch.stack(posterior_logits_list, dim=1),
            "obs_preds": torch.stack(obs_preds, dim=1),
            "reward_preds": torch.stack(reward_preds, dim=1),
        }

    def imagine(self, initial_h, initial_s, actions):
        """Roll out the model without observations (for planning).

        Args:
            initial_h: (batch, det_size) starting deterministic state
            initial_s: (batch, stoch_size) starting stochastic state
            actions: (batch, horizon, action_dim) planned actions

        Returns:
            Predicted observations, rewards, and states for each step.
        """
        h, s = initial_h, initial_s
        horizon = actions.shape[1]

        obs_preds = []
        reward_preds = []

        for t in range(horizon):
            h = self.sequence_step(h, s, actions[:, t])
            s, _, _ = self.prior(h)  # No observation: use prior
            obs_pred, reward_pred = self.decode(h, s)
            obs_preds.append(obs_pred)
            reward_preds.append(reward_pred)

        return {
            "obs_preds": torch.stack(obs_preds, dim=1),
            "reward_preds": torch.stack(reward_preds, dim=1),
        }
Listing 44.1: Complete RSSM implementation with categorical stochastic states, straight-through gradient estimation, and both observation (training) and imagination (planning) modes.

Several design choices in Listing 44.1 deserve attention. The stochastic state uses a categorical distribution (32 categories each with 32 classes) rather than a Gaussian. DreamerV3 showed that categorical distributions substantially mitigate the posterior collapse problem that plagues Gaussian latent variables, where the KL term in the ELBO drives the posterior toward the prior, effectively ignoring the observations. The straight-through gradient estimator (a technique that uses the discrete sample in the forward pass but substitutes a continuous approximation in the backward pass, enabling gradient flow through discrete choices) lets us backpropagate through the discrete sampling operation by using the continuous softmax probabilities in the backward pass.

3. The ELBO Training Objective

How do we train an RSSM? The model defines a generative process: at each time step, it samples a latent state and produces an observation. The true posterior over latent states given observations is intractable, so we use variational inference. The objective is the ELBO, extended to sequential data.

For a sequence of observations \(o_{1:T}\) and actions \(a_{1:T}\), the ELBO is:

$$ \mathcal{L} = \sum_{t=1}^{T} \Big[ \underbrace{\mathbb{E}_{q_\phi(s_t | h_t, o_t)} \big[ \log p_\phi(o_t | h_t, s_t) \big]}_{\text{reconstruction}} - \underbrace{\beta \cdot D_{\mathrm{KL}} \big[ q_\phi(s_t | h_t, o_t) \,\|\, p_\phi(s_t | h_t) \big]}_{\text{dynamics regularization}} \Big] $$

Each term has a clear interpretation:

Key Insight: The KL Term Teaches the Dynamics Model

The KL divergence \(D_{\mathrm{KL}}[q \| p]\) penalizes disagreement between the posterior (what actually happened, given the observation) and the prior (what the model predicted would happen, without the observation). Minimizing this divergence forces the prior to become an accurate dynamics predictor. In effect, the posterior acts as a "teacher" that shows the prior the correct answer, and the KL term is the "exam score." Over training, the prior learns to predict the stochastic state well enough that it can operate independently during planning.

Mental Model

Think of the RSSM's prior and posterior like a weather forecaster and a rooftop thermometer. The prior is the forecaster's prediction for tomorrow's temperature, made the night before using only past data and a mental model of weather patterns. The posterior is the updated estimate after stepping outside and reading the thermometer in the morning. The KL term in the ELBO measures how far off the forecast was from the thermometer-corrected estimate. Over many days of training, the forecaster's predictions improve until they nearly match what the thermometer reveals, at which point the forecaster can make reliable multi-day predictions without checking the thermometer at all. That is exactly what happens during planning: the model rolls forward using only its prior (the forecast), with no access to real observations (the thermometer).

The following listing implements the ELBO loss function with the free-bits strategy from DreamerV3.

def compute_elbo_loss(
    rssm_output: dict,
    observations: torch.Tensor,
    rewards: torch.Tensor,
    free_bits: float = 1.0,
    kl_beta: float = 1.0,
    reward_weight: float = 1.0,
):
    """Compute the ELBO loss for RSSM training.

    Args:
        rssm_output: dictionary from RSSM.observe()
        observations: (batch, time, obs_dim) ground truth
        rewards: (batch, time, 1) ground truth rewards
        free_bits: minimum KL before penalty applies (nats)
        kl_beta: weight on KL term
        reward_weight: weight on reward prediction loss

    Returns:
        Total loss and a dictionary of component losses for logging.
    """
    # Reconstruction loss: MSE between predicted and true observations
    obs_loss = F.mse_loss(rssm_output["obs_preds"], observations)

    # Reward prediction loss
    rew_loss = F.mse_loss(
        rssm_output["reward_preds"], rewards
    )

    # KL divergence between posterior and prior (categorical distributions)
    prior_logits = rssm_output["prior_logits"]
    posterior_logits = rssm_output["posterior_logits"]

    # Reshape to (batch * time, num_categoricals, num_classes)
    batch, time = prior_logits.shape[:2]
    num_cat = 32  # num_categoricals
    num_cls = 32  # num_classes

    prior_reshaped = prior_logits.view(batch * time, num_cat, num_cls)
    posterior_reshaped = posterior_logits.view(batch * time, num_cat, num_cls)

    # Compute KL for each categorical dimension
    prior_probs = F.softmax(prior_reshaped, dim=-1)
    posterior_probs = F.softmax(posterior_reshaped, dim=-1)

    kl_per_categorical = (
        posterior_probs * (
            torch.log(posterior_probs + 1e-8)
            - torch.log(prior_probs + 1e-8)
        )
    ).sum(dim=-1)  # (batch*time, num_cat)

    # Free bits: only penalize KL above threshold per categorical
    kl_free = torch.clamp(kl_per_categorical, min=free_bits)
    kl_loss = kl_free.mean()

    # Total ELBO loss (negative because we maximize ELBO)
    total_loss = obs_loss + reward_weight * rew_loss + kl_beta * kl_loss

    return total_loss, {
        "obs_loss": obs_loss.item(),
        "reward_loss": rew_loss.item(),
        "kl_loss": kl_loss.item(),
        "total_loss": total_loss.item(),
    }
Listing 44.2: ELBO loss computation with free-bits KL regularization. The free-bits threshold prevents posterior collapse by allowing a minimum amount of information to flow through the stochastic state without penalty.

4. Transformer World Models

The ELBO gives us a principled way to train any latent dynamics model, but the RSSM is not the only architecture that can serve as the backbone; the same training objective applies equally well when the recurrent core is replaced by an attention mechanism.

The RSSM processes sequences one step at a time through its GRU cell. Transformer world models (Micheli et al., 2023; Robine et al., 2023) replace this recurrence with causal self-attention (an attention mechanism where each position can only attend to itself and earlier positions, preventing information from the future from leaking into predictions), processing entire trajectories in parallel. The key advantage is that attention can capture long-range dependencies without the information bottleneck of a fixed-size hidden state. The key disadvantage is that generation remains autoregressive (each predicted token is conditioned on all previously generated tokens, so outputs must be produced one at a time in sequence), so the wall-clock cost of rolling out \(H\) steps is \(O(H^2)\) rather than \(O(H)\).

The architecture tokenizes each timestep's state and action into a sequence of tokens, then applies a causal transformer (GPT-style) to predict the next timestep's tokens. IRIS (Micheli et al., 2023) tokenizes observations using a discrete autoencoder (Vector-Quantized VAE, or VQ-VAE), producing a sequence of discrete tokens per image. The transformer then models the joint sequence of observation tokens and action tokens.

class TransformerWorldModel(nn.Module):
    """Simplified transformer-based world model.

    Processes (state, action) sequences with causal self-attention.
    Suitable for low-dimensional state spaces (no image tokenization).
    """

    def __init__(
        self,
        obs_dim: int,
        action_dim: int,
        d_model: int = 256,
        nhead: int = 4,
        num_layers: int = 4,
        max_seq_len: int = 128,
    ):
        super().__init__()
        self.d_model = d_model

        # Project (obs, action) pair into transformer dimension
        self.input_proj = nn.Linear(obs_dim + action_dim, d_model)

        # Learned positional encoding
        self.pos_embedding = nn.Embedding(max_seq_len, d_model)

        # Causal transformer
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=nhead,
            dim_feedforward=4 * d_model,
            activation="gelu",
            batch_first=True,
            norm_first=True,
        )
        self.transformer = nn.TransformerEncoder(
            encoder_layer, num_layers=num_layers
        )

        # Output heads
        self.obs_head = nn.Linear(d_model, obs_dim)
        self.reward_head = nn.Linear(d_model, 1)

    def forward(self, observations, actions):
        """Predict next observations and rewards.

        Args:
            observations: (batch, time, obs_dim)
            actions: (batch, time, action_dim)

        Returns:
            obs_preds: (batch, time, obs_dim) next-obs predictions
            reward_preds: (batch, time, 1) reward predictions
        """
        batch, time = observations.shape[:2]
        x = self.input_proj(torch.cat([observations, actions], dim=-1))
        positions = torch.arange(time, device=x.device)
        x = x + self.pos_embedding(positions)

        # Causal mask: each position attends only to itself and earlier
        causal_mask = nn.Transformer.generate_square_subsequent_mask(
            time, device=x.device
        )
        x = self.transformer(x, mask=causal_mask)

        obs_preds = self.obs_head(x)
        reward_preds = self.reward_head(x)
        return obs_preds, reward_preds
Listing 44.3: Transformer world model with causal self-attention for low-dimensional state spaces. The causal mask ensures each prediction depends only on past and present timesteps. For image observations, replace the linear input projection with a VQ-VAE tokenizer.

When should you choose a transformer world model over an RSSM? Empirical evidence (circa 2023) shows that transformer world models such as IRIS match or exceed RSSM-based agents on the Atari 100k benchmark, a standardized evaluation using only 100,000 environment interactions. However, RSSMs generate faster because the GRU state update costs \(O(1)\) per step, while the transformer must attend to all previous steps. For planning applications that roll out thousands of trajectories (Section 44.3), this difference matters. (A planner evaluating 1,000 candidate trajectories of length 50 executes roughly 50,000 GRU steps with an RSSM, but over 1.25 million attention operations with a transformer.) As of 2025, DreamerV3 remains broadly competitive with transformer world models on most published benchmarks, though results vary by domain. Hybrid architectures that combine RSSM-style latent states with transformer sequence models are an active area of development.

Real-World Application: Autonomous Driving at Wayve
Real-World Application: Autonomous Driving at Wayve
Right Tool: DreamerV3 with dreamer-pytorch

The RSSM implementation above is pedagogical. For production use, the dreamer-pytorch library provides a complete DreamerV3 implementation with image encoders (convolutional neural network, or CNN), symlog predictions (where the model predicts \(\mathrm{sign}(x) \cdot \ln(|x| + 1)\) instead of raw values, compressing the dynamic range of targets like rewards that span many orders of magnitude), layer normalization, and the full actor-critic training loop. What took 200+ lines above is a single configuration:

from dreamer_torch import DreamerV3

agent = DreamerV3(
    obs_space={"image": (3, 64, 64)},
    act_space={"action": 6},
    config=DreamerV3.defaults.update(
        rssm={"deter": 4096, "units": 1024, "stoch": 32, "classes": 32},
        encoder={"mlp_keys": "", "cnn_keys": "image"},
    ),
)
# agent.train(replay_buffer) handles ELBO optimization internally
Listing 44.5: Configuring DreamerV3 via the dreamer-pytorch library, specifying the RSSM dimensions, encoder type, and observation/action spaces in a single declarative block.

The library handles categorical straight-through gradients, free-bits KL, symlog transforms for reward prediction, and distributed training. The line-count reduction from our from-scratch implementation is roughly 10x. Note that the official DreamerV3 codebase by Hafner et al. uses JAX rather than PyTorch; as of 2025, dreamer-torch is a community-maintained PyTorch port.

5. RSSM vs. Transformer: Architectural Trade-offs

The choice between RSSM and transformer world models involves several trade-offs that depend on the specific discovery application:

PropertyRSSM (DreamerV3)Transformer (IRIS/TWM)
State representationFixed-size \((h_t, s_t)\)Growing context window
Long-range dependenciesLimited by GRU capacityAttention captures directly
Generation cost per step\(O(1)\)\(O(t)\) due to attention
Sample efficiencyStrong in both 100k and full-data regimesCompetitive at 100k interactions (circa 2023)
Stochasticity modelingExplicit (prior/posterior)Implicit (next-token prediction)
Uncertainty quantificationBuilt-in via KL divergenceRequires ensembles or calibration
Planning rollout speedFast (parallel GRU steps)Slower (sequential attention)

For scientific discovery applications where uncertainty quantification matters (and in most experimental sciences it does), the RSSM's explicit stochastic state provides a natural advantage. The prior-posterior divergence gives a built-in signal for epistemic uncertainty (uncertainty about the model's own knowledge, as opposed to aleatoric uncertainty from inherent randomness in the environment): when the prior and posterior disagree strongly, the model is uncertain about the dynamics. Transformers require additional machinery (ensembles, conformal prediction) to achieve comparable uncertainty estimates.

Real-World Application: Autonomous Driving at Wayve

Wayve's GAIA-1 (2023) uses a latent dynamics world model to simulate driving scenarios for training and validating self-driving policies. The model encodes camera frames and vehicle telemetry into a latent space, then rolls out predicted futures conditioned on candidate steering and acceleration commands. This allows the system to evaluate thousands of "what if" maneuvers per second in latent space, far faster than running a physics-based driving simulator, while retaining enough fidelity to transfer learned policies to real vehicles on London streets.

Research Frontier: State-Space World Models and Diamond

The Diamond system (Alonso et al., 2024, "Diffusion for World Modeling," ICLR 2025) replaces the standard decoder with a diffusion model that generates high-fidelity observations conditioned on the latent state, achieving state-of-the-art visual quality in Atari game simulation while maintaining interactive frame rates. Meanwhile, hybrid architectures that swap the GRU in the RSSM sequence model for structured state-space layers (S4, Mamba; linear-time sequence models that use fixed-size recurrent state updates inspired by continuous-time dynamical systems) preserve constant-cost generation while capturing longer-range temporal patterns. These approaches are particularly promising for scientific applications where trajectories span thousands of steps (climate simulations, protein folding dynamics) and both long-range memory and calibrated uncertainty estimates are essential. The convergence of diffusion decoders and state-space backbones suggests that the next generation of world models will combine the RSSM's explicit uncertainty modeling with substantially richer observation generation.

6. Training an RSSM: The Complete Loop

With the architectural trade-offs understood, we can now turn from model design to the practical mechanics of fitting an RSSM to data collected from a real or simulated environment.

To train an RSSM world model, we need trajectories collected from the environment (or simulator). Each trajectory is a sequence of (observation, action, reward) tuples. The training loop alternates between collecting data using the current policy and updating the world model on batches sampled from a replay buffer (a data structure that stores past experience tuples and serves random mini-batches for training, decoupling data collection from gradient updates).

import gymnasium as gym
import numpy as np
from collections import deque


def collect_trajectories(env, policy, num_episodes=10):
    """Collect trajectories from a Gymnasium environment.

    Args:
        env: Gymnasium environment
        policy: callable(obs) -> action
        num_episodes: number of episodes to collect

    Returns:
        List of trajectory dicts with obs, actions, rewards arrays.
    """
    trajectories = []
    for _ in range(num_episodes):
        obs_list, act_list, rew_list = [], [], []
        obs, _ = env.reset()
        done = False
        while not done:
            action = policy(obs)
            next_obs, reward, terminated, truncated, _ = env.step(action)
            obs_list.append(obs)
            act_list.append(action)
            rew_list.append(reward)
            obs = next_obs
            done = terminated or truncated
        trajectories.append({
            "observations": np.array(obs_list, dtype=np.float32),
            "actions": np.array(act_list, dtype=np.float32),
            "rewards": np.array(rew_list, dtype=np.float32),
        })
    return trajectories


def train_world_model(
    rssm,
    trajectories,
    num_epochs=100,
    batch_size=16,
    seq_len=50,
    lr=3e-4,
):
    """Train RSSM on collected trajectories.

    Args:
        rssm: RSSM model
        trajectories: list of trajectory dicts
        num_epochs: training epochs
        batch_size: batch size
        seq_len: subsequence length for training
        lr: learning rate

    Returns:
        List of loss dictionaries per epoch.
    """
    optimizer = torch.optim.Adam(rssm.parameters(), lr=lr)
    history = []

    for epoch in range(num_epochs):
        epoch_losses = []

        # Sample random subsequences from trajectories
        for _ in range(max(1, len(trajectories) // batch_size)):
            obs_batch, act_batch, rew_batch = [], [], []
            for _ in range(batch_size):
                traj = trajectories[np.random.randint(len(trajectories))]
                max_start = len(traj["observations"]) - seq_len
                if max_start <= 0:
                    start = 0
                    sl = len(traj["observations"])
                else:
                    start = np.random.randint(max_start)
                    sl = seq_len
                obs_batch.append(traj["observations"][start:start + sl])
                act_batch.append(traj["actions"][start:start + sl])
                rew_batch.append(traj["rewards"][start:start + sl])

            # Pad to same length and convert to tensors
            actual_len = min(len(o) for o in obs_batch)
            obs_t = torch.tensor(
                np.stack([o[:actual_len] for o in obs_batch])
            )
            act_t = torch.tensor(
                np.stack([a[:actual_len] for a in act_batch])
            )
            rew_t = torch.tensor(
                np.stack([r[:actual_len] for r in rew_batch])
            ).unsqueeze(-1)

            # Ensure actions are float and correct shape
            if act_t.dim() == 2:
                act_t = act_t.unsqueeze(-1).float()

            # Forward pass
            output = rssm.observe(obs_t, act_t)
            loss, loss_dict = compute_elbo_loss(output, obs_t, rew_t)

            # Backward pass
            optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(rssm.parameters(), 100.0)
            optimizer.step()

            epoch_losses.append(loss_dict)

        avg_loss = {
            k: np.mean([d[k] for d in epoch_losses])
            for k in epoch_losses[0]
        }
        history.append(avg_loss)

        if (epoch + 1) % 20 == 0:
            print(
                f"Epoch {epoch+1}: total={avg_loss['total_loss']:.4f} "
                f"obs={avg_loss['obs_loss']:.4f} "
                f"kl={avg_loss['kl_loss']:.4f}"
            )

    return history
Listing 44.4: End-to-end RSSM training loop with trajectory collection and subsequence sampling. Trajectories are collected from a Gymnasium environment using any policy (even random), sliced into fixed-length subsequences, and used to minimize the ELBO loss. Gradient clipping at norm 100 prevents training instability from long sequences.

The Universe in a Shoebox

DreamerV3's stochastic state uses 32 categorical dimensions with 32 classes each, yielding \(32^{32} \approx 1.46 \times 10^{48}\) possible discrete states. That number falls within two orders of magnitude of the estimated count of atoms in Earth (\(\approx 10^{50}\)). Yet this state space is learned from a few hundred thousand environment interactions and compressed into just 1,024 floating-point numbers. Ha and Schmidhuber's original 2018 "World Models" paper trained an agent that learned to play a car racing game entirely inside its own dream: the agent never touched the real environment during policy optimization, practicing instead against imagined rollouts from its latent model. The agent performed almost as well as one trained on real data, marking perhaps the first time a neural network literally dreamed its way to competence.

Common Pitfall: Posterior Collapse in Gaussian Latent States

If you use Gaussian distributions instead of categoricals for the stochastic state, you may observe the KL term collapsing to zero while the reconstruction loss plateaus at a high value. This means the model is ignoring the stochastic state entirely, routing all information through the deterministic GRU. The free-bits strategy helps but does not fully solve the problem. DreamerV3's switch to categorical distributions was motivated precisely by this failure mode: categorical KL is harder to collapse because each categorical dimension independently encodes discrete information. If you must use Gaussians (for compatibility with downstream tasks that need continuous latent codes), consider adding an information bottleneck on the deterministic state to force information through the stochastic channel.

7. Connecting to the Discovery Workbench

In the Discovery Workbench architecture (Chapter 6), the world model serves as a simulation backend that slots into the experiment planning pipeline. When the Workbench's optimization module (Chapter 45) or experiment designer (Chapter 46) needs to evaluate a candidate action sequence, it queries the world model rather than the real environment. The RSSM's imagine() method is the Workbench API for this: pass an initial state and a sequence of proposed actions, receive predicted observations and rewards. The Workbench tracks the model's prediction uncertainty (via prior-posterior KL divergence) and automatically triggers real experiments when the world model's confidence drops below a configurable threshold.

Try It: Train an RSSM on CartPole and Visualize Imagined Rollouts

Build a working latent dynamics model in under an hour using the code from this section and standard Python libraries (PyTorch, Gymnasium, Matplotlib).

  1. Collect data. Install Gymnasium (pip install gymnasium) and use the collect_trajectories function from Listing 44.4 to gather 50 episodes from CartPole-v1 with a random policy (lambda obs: env.action_space.sample()). Save the trajectories to disk with torch.save.
  2. Train the RSSM. Instantiate the RSSM from Listing 44.1 with obs_dim=4 and action_dim=1. Train for 200 epochs using train_world_model from Listing 44.4. Plot the reconstruction loss and KL loss curves with Matplotlib to confirm both decrease.
  3. Imagine forward. Pick a trajectory from your dataset. Encode the first 5 steps using rssm.observe() to obtain a starting latent state, then call rssm.imagine() with the ground-truth actions for the remaining steps. Compare the predicted observations against the real observations by plotting each of the 4 CartPole state dimensions (cart position, cart velocity, pole angle, pole angular velocity) over time.
  4. Measure rollout divergence. Compute the mean squared error between imagined and real observations at horizons of 5, 10, 20, and 40 steps. Plot MSE vs. horizon. You should see error grow roughly linearly or faster, illustrating the compounding prediction challenge that motivates replanning (Section 44.3).
  5. Ablate the stochastic state. Set num_categoricals=1 and num_classes=2 (minimal stochastic capacity) and retrain. Compare the horizon-vs-MSE curve against the full model. Does richer stochastic capacity improve long-horizon prediction on this simple environment?

Exercise 44.1.1

An RSSM is trained on trajectories from a pendulum environment with observation dimension 3 (cos theta, sin theta, angular velocity) and action dimension 1 (torque). The model uses 16 categoricals with 16 classes each. During imagination (no observations), the model calls prior(h) to sample the stochastic state. If the prior network outputs logits that are all equal (uniform distribution) for every categorical dimension, what does this tell you about the quality of the learned dynamics, and what concrete symptom would you observe in the imagined rollouts?

Hint

A uniform prior means the model assigns equal probability to every possible stochastic state. Think about what the stochastic state is supposed to encode (the unpredictable part of the next observation) and what happens when the model has no preference among \(16^{16}\) possibilities. Consider how this would affect the decoder's output when averaged over many sampled trajectories.

Lab: Measuring World Model Prediction Horizon

Goal: Empirically determine how far into the future an RSSM can predict before its error exceeds a usefulness threshold, and observe how stochastic capacity affects this horizon.

Tools: PyTorch, Gymnasium (Pendulum-v1), Matplotlib. Use the RSSM from Listing 44.1 with obs_dim=3, action_dim=1.

Procedure (20 minutes): (1) Collect 100 episodes with a random policy. (2) Train the RSSM for 300 epochs. (3) For 20 held-out episodes, encode the first 10 steps with observe(), then call imagine() for horizons 1 through 50. Record the per-step MSE between predicted and true observations. (4) Plot MSE vs. horizon (mean and standard deviation across episodes).

What to vary: Train three models with stochastic capacities of (a) 4 categoricals, 4 classes, (b) 16 categoricals, 16 classes, and (c) 32 categoricals, 32 classes. Overlay all three MSE-vs-horizon curves on one plot.

What to observe: At what horizon does each model's mean MSE first exceed 0.1 (normalized observation scale)? Does larger stochastic capacity always help, or does it plateau? Check whether the standard deviation bands widen faster for smaller stochastic states, indicating higher variance in prediction quality.

Exercises

  1. (Conceptual) Explain why the RSSM needs both a prior and a posterior distribution over the stochastic state. What would go wrong if we trained only with the posterior and then tried to use the model for planning?
  2. (Coding) Modify the RSSM to use Gaussian stochastic states instead of categorical ones. Train both versions on CartPole trajectories and compare the KL divergence curves. Do you observe posterior collapse with the Gaussian version?
  3. (Analysis) The ELBO objective has a reconstruction term and a KL term. Plot how the balance between these two terms (controlled by \(\beta\)) affects one-step and five-step prediction accuracy. What value of \(\beta\) gives the best five-step predictions? Is it the same as the value that gives the best one-step predictions?
  4. (Research) DreamerV3 uses 32 categorical dimensions with 32 classes each, giving a stochastic state space of size \(32^{32} \approx 10^{48}\). Is this capacity necessary for simple environments like CartPole? Experiment with smaller stochastic states (e.g., 8 categoricals with 8 classes) and measure the effect on prediction quality.

What's Next

We now have a trained world model that can predict what will happen given any sequence of actions. In Section 44.2: Counterfactual Reasoning with World Models, we use this capability for a more subtle purpose: asking what would have happened if we had taken a different action in the past. This counterfactual reasoning ability transforms the world model from a mere predictor into a tool for causal understanding.

Bibliography

Hafner, D., Pasukonis, J., Ba, J., & Lillicrap, T. (2023). Mastering diverse domains through world models. arXiv preprint arXiv:2301.04104.

DreamerV3: the definitive RSSM architecture with categorical stochastic states, symlog predictions, and free-bits KL. The primary reference for this section.

Hafner, D., et al. (2019). Learning latent dynamics for planning from pixels. ICML 2019.

The PlaNet paper that introduced the RSSM architecture and the latent overshooting objective.

Ha, D. & Schmidhuber, J. (2018). World models. arXiv preprint arXiv:1803.10122.

The paper that popularized the "world models" framing, combining a VAE encoder with a Mixture Density Network RNN (MDN-RNN) dynamics model.

Micheli, V., Alonso, E., & Fleuret, F. (2023). Transformers are sample-efficient world learners. ICLR 2023.

IRIS: the first transformer world model competitive with RSSMs, demonstrating that attention-based dynamics can achieve human-level Atari with 100k interactions.

Kingma, D. P. & Welling, M. (2014). Auto-encoding variational Bayes. ICLR 2014.

The foundational VAE paper that introduced the ELBO objective and reparameterization trick, both essential to RSSM training.

Gymnasium (Farama Foundation)

The standard RL environment interface used for data collection in this section.