The Dreamer Family

Michael BrenndoerferJuly 15, 202653 min read

Part of World Models Handbook

Explains how Dreamer learns behaviors by latent imagination: RSSM dynamics, actor-critic training, and the V1, V2, and V3 design changes.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The Dreamer Family

The PlaNet agent from the preceding chapter learned a compact latent dynamics model from pixels, then planned inside it with online trajectory optimization. At every environment step, PlaNet's cross-entropy method evaluates candidate action sequences, refits its sampling distribution to elite sequences, and executes the current-step mean action from the optimized distribution. More candidates and a longer horizon increase this decision-time cost. The search itself is transient, although the world model continues to learn from experience.

The Dreamer family, developed by Danijar Hafner and colleagues across a sequence of papers from 2019 to 2023, makes a decisive move that separates it from the PlaNet-style planners. Instead of using the learned model only as a forward simulator for planning, Dreamer uses it as a training ground for policies and value functions. The actor and critic are the learned parameters that carry the reusable skill. The model is still a latent dynamics model learned from sensory inputs, including pixels and, in DreamerV3, vector observations. But the agent does not search over action sequences at every step. It learns an actor (a policy) and a critic (a value function) by imagining trajectories inside the learned model, then acts by simply querying the actor. This is the essence of "learning behaviors by latent imagination". It is what makes Dreamer both fast at action time and capable of improving the behaviors it uses.

To see the computational shift, compare the controllers. PlaNet searches, evaluates, and compares action sequences before each action; Dreamer spends that control-learning compute during training and retains the result in actor parameters. PlaNet still trains its world model, so not all of its expense occurs at decision time. The distinction is between repeating a transient controller search and amortizing a rule for action selection.

This reframing matters more than it might first appear. PlaNet improves its learned model, but its controller remains an online optimizer. Dreamer also trains an actor and critic, so behavioral knowledge persists in parameters instead of being recomputed by a fresh action-sequence search. Both approaches can exploit errors in the model; an amortized policy does not automatically become conservative around uncertainty.

The family spans three major versions, and each one solves a problem the previous version exposed. DreamerV1 established the core recipe, DreamerV2 scaled it with categorical states and KL balancing, and DreamerV3 added cross-domain robustness techniques.

  • DreamerV1 (Dream to Control; 2019 preprint, 2020 publication) established the core recipe: a recurrent state-space model (RSSM) for latent dynamics, imagined rollouts, and actor-critic learning through imagined dynamics. It reparameterized continuous actions and used straight-through gradients for discrete actions.
  • DreamerV2 (Mastering Atari with Discrete World Models; 2021 publication) replaced Gaussian stochastic states with 32 groups of 32 categorical variables, introduced KL balancing, and used REINFORCE actor gradients on Atari while retaining dynamics gradients for continuous control.
  • DreamerV3 appeared as a 2023 preprint and was published in Nature in 2025. It uses categorical states, split KL losses with free bits, one-percent uniform mixing, symlog vector inputs, symexp-spaced two-hot reward and value predictions, and percentile-normalized actor advantages. Image observations retain their convolutional path. Its fixed configuration was evaluated across a broad benchmark suite with benchmark-specific interfaces and budgets.

By the end of this chapter you should be able to explain why learning a policy in imagination differs from planning, how RSSM variants represent latent state, how their actor gradients differ, and what supports DreamerV3's cross-domain configuration robustness. We also build a compact Dreamer-style agent and compare its imagined training diagnostic with real-environment performance.

A note on how to read this chapter: the theory and the code are meant to be read together. We run a compact Dreamer-inspired agent end to end on a small "dot world," then define selected V2 and V3 components without training full variants. Try to keep the mapping between the math and the code in mind as you go, because the machinery of Dreamer is not exotic; it is a handful of well-understood components (a recurrent model, a variational latent, an actor, a critic) assembled in a careful way.

Recurrent state-space representations

Before we can imagine anything, we need a state representation that supports both perception (mapping observations to a belief) and prediction (rolling that belief forward under actions). Dreamer's representation is the recurrent state-space model, usually abbreviated RSSM. You may recognize the architecture if you worked through Part V, Chapter 1 on recurrent state-space models; here we focus on how Dreamer instantiates it and why each component is there.

The core problem is partial observability. From a single image, you cannot tell how fast an object is moving, what is behind it, or what happened a moment ago. A sufficient Markov state for control would include the physical configuration, velocities, and other variables that affect the future. In a POMDP, an exact belief is a distribution over environment states. An RSSM instead learns a compact history-dependent representation; its stochastic variables can encode ambiguity, but they are not automatically a calibrated Bayesian belief.

The RSSM splits the latent state into two pieces. The split is deliberate:

  • A deterministic recurrent state hth_t, produced by a GRU (or similar recurrent cell) that carries long-range memory across time. This is the "hidden" part of the RSSM: it is a function of all past latents and actions, so it accumulates history without stochasticity.
  • A stochastic latent state ztz_t, sampled from a learned distribution. This gives the representation stochastic capacity and can encode multiple possibilities, but stochasticity alone does not establish calibrated uncertainty.

The deterministic state hth_t is the recurrent memory. The stochastic code ztz_t is inferred by the posterior when an observation is available and predicted by the prior during imagination. Both variables participate in imagined rollouts. The stochastic code can represent ambiguity, but it need not correspond to calibrated uncertainty.

Writing this out, the RSSM has several learned components:

  • Recurrent transition: ht+1=fϕ(ht,zt,at)h_{t+1} = f_\phi(h_t, z_t, a_t), typically a GRU. It advances the deterministic memory by one step given the current latent and action.
  • Prior (transition) distribution: pϕ(zt+1∣ht+1)p_\phi(z_{t+1} \mid h_{t+1}), which predicts the successor latent from memory alone, without looking at the next observation. This is what you sample during imagination, when no observation is available.
  • Posterior (representation) distribution: qϕ(zt+1∣ht+1,ot+1)q_\phi(z_{t+1} \mid h_{t+1}, o_{t+1}), which infers the successor latent from memory and the next observation. This is what you sample during training, when observations are available.
  • Decoder (observation model): pϕ(ot+1∣ht+1,zt+1)p_\phi(o_{t+1} \mid h_{t+1}, z_{t+1}), which reconstructs the next observation from the successor latent. Reconstruction is the main signal that forces the latent to retain information about the world.
  • Reward model: pϕ(rt∣ht+1,zt+1)p_\phi(r_t \mid h_{t+1}, z_{t+1}), which predicts the reward received after action ata_t from the successor latent. This is essential because the agent needs to imagine rewards to train the critic.
  • Continue model: pϕ(ct∣ht+1,zt+1)p_\phi(c_t \mid h_{t+1}, z_{t+1}), which predicts whether the episode continues after action ata_t. It prevents rewards and value bootstraps from propagating across terminal boundaries.

The separation between prior and posterior is central to the design. During training, you have observations, so you can use the posterior to get an accurate latent. During imagination, we do not have future observations, so we must use the prior. If prior and posterior disagree badly, imagination drifts away from reality and the learned policy is trained on garbage. A central part of the RSSM loss is therefore the KL divergence between the posterior and the prior, which pulls the prior toward the posterior so that the two stay consistent. This is the same consistency pressure that the VAE world model in Part III, Chapter 3 relied on, but here it operates per timestep with a recurrent memory in the loop, so the prior must learn to predict a latent that is consistent with a whole history of observations, not only the current one.

Why have both parts? The recurrent state provides a persistent path for history, while the stochastic state gives the model additional representational capacity and a variational training objective. That design can represent ambiguity, but its samples should not be read as calibrated physical hypotheses without a separate calibration test.

RSSM vs. a plain VAE sequence model

A VAE whose per-step encoder sees only the current observation, with no recurrent state, temporal context, or transition prior, cannot represent long-range memory. An RSSM adds a recurrent path and a per-step KL that trains the prior to predict the posterior, allowing rollout without future observations.

The training objective for the representation combines reconstruction, reward prediction, continue prediction, and the KL term. Each term serves a distinct role: reconstruction forces the latent to retain information about the observation, reward and continue prediction give you the signals you need for imagination, and the KL keeps the prior and posterior aligned. A useful way to read the objective is as a tug-of-war between two pressures: the likelihood terms push the latent to be informative about the present, while the KL term pushes the prior to be a good predictor of the latent so that the model remains usable when observations are absent. The balance between these pressures is controlled by the weight β\beta, and choosing that weight is one of the more delicate parts of training a Dreamer-style agent.

Lmodel(ϕ)=E[∑t(−log⁡pϕ(ot+1∣ht+1,zt+1)−log⁡pϕ(rt∣ht+1,zt+1)−log⁡pϕ(ct∣ht+1,zt+1)+β KL ⁣(qϕ(zt+1∣ht+1,ot+1) ∥ pϕ(zt+1∣ht+1)))],\mathcal{L}_\text{model}(\phi) = \mathbb{E}\left[ \sum_t \left( -\log p_\phi(o_{t+1} \mid h_{t+1}, z_{t+1}) - \log p_\phi(r_t \mid h_{t+1}, z_{t+1}) - \log p_\phi(c_t \mid h_{t+1}, z_{t+1}) + \beta \, \mathrm{KL}\!\left( q_\phi(z_{t+1} \mid h_{t+1}, o_{t+1}) \,\|\, p_\phi(z_{t+1} \mid h_{t+1}) \right) \right) \right],

where the expectation is over replay sequences and posterior latent samples, and β\beta scales the KL penalty. With Gaussian latents, reparameterization carries gradients through sampling. Reconstruction, reward, and continuation contribute negative log-likelihoods; the KL aligns the prior and posterior. In the toy code, binary-frame reconstruction and continuation BCE are Bernoulli negative log-likelihoods. Reward MSE equals the negative log-likelihood of a fixed-variance Gaussian with variance 1/21/2, up to an additive constant. DreamerV2 and V3 instead use categorical samples with straight-through surrogate gradients.

The index tt names a replay transition here: ata_t takes the model from st=(ht,zt)s_t=(h_t,z_t) to st+1s_{t+1}, while rtr_t and ctc_t are its reward and continuation outcome. The toy replay stores that tuple together, and model_loss predicts the next frame, reward, and continuation from the same successor latent. The later lambda-return uses the same transition indexing.

Two subtleties matter for closed-loop performance. First, the KL treatment is generation-specific: V1 uses a scaled KL term, V2 balances gradient flow between representation and dynamics, and V3 uses separately scaled dynamics and representation losses with free bits. Second, reconstruction can emphasize perceptually salient information that is not useful for control. That tension motivates the value-equivalent models discussed next.

Let's build the RSSM on a small synthetic task whose observation is a binary raster of a moving dot. We keep the example CPU-sized, seed every random generator, and make trajectory shapes explicit. Computation cells prepare the data; figure cells only render values computed earlier.

In[3]:
Code
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)
np.random.seed(0)

The world is a dot in a 2D box that we can nudge up, down, left, or right with an action. The observation is a directly rendered 16x16 binary image, which forces the model to compress pixels into state. We store transitions as tuples of (observation, action, reward, next observation, done).

In[4]:
Code
class DotWorld:
    """A dot moves in a 2D box; action nudges it and we reward staying near center."""

    def __init__(self, size=16.0):
        self.size = size
        # All randomness flows through the rng argument, so behavior is reproducible.

    def reset(self, rng):
        pos = rng.uniform(4.0, self.size - 4.0, size=2)
        vel = np.zeros(2)
        return pos, vel

    def step(self, pos, vel, action, rng):
        # action: 0 = push up, 1 = push down, 2 = push left, 3 = push right
        thrust = {
            0: np.array([0.0, -1.0]),
            1: np.array([0.0, 1.0]),
            2: np.array([-1.0, 0.0]),
            3: np.array([1.0, 0.0]),
        }[int(action)]
        vel = 0.9 * vel + 0.5 * thrust
        pos = pos + vel
        # Wall reflection keeps the dot in the box
        for d in range(2):
            if pos[d] < 1.0:
                pos[d] = 1.0
                vel[d] = -0.5 * vel[d]
            elif pos[d] > self.size - 1.0:
                pos[d] = self.size - 1.0
                vel[d] = -0.5 * vel[d]
        center = np.array([self.size / 2, self.size / 2])
        reward = -0.03 * np.linalg.norm(pos - center)
        done = False
        return pos, vel, reward, done

    def render(self, pos, image_size=16):
        img = np.zeros((image_size, image_size), dtype=np.float32)
        x = int(
            np.clip(pos[0] / self.size * (image_size - 1), 0, image_size - 1)
        )
        y = int(
            np.clip(pos[1] / self.size * (image_size - 1), 0, image_size - 1)
        )
        img[y, x] = 1.0
        return img

This tiny environment is a stand-in for the real thing. Its simulator has a compact Markov state, with the dot position and velocity determining the next state, but the agent sees only a quantized position image, with velocity hidden. It must therefore infer motion from history. That is precisely the partial-observability structure the RSSM is designed to handle, making the dot world a fair testbed for the mechanism.

Now we collect a small dataset by running a random policy. In a full Dreamer setup, data collection is online. Here the fixed dataset is enough to exercise the RSSM and compare diagnostics; the experiment does not establish that its imagined images are plausible.

In[5]:
Code
def collect_random_data(world, episodes=60, steps=50, image_size=16):
    rng = np.random.default_rng(123)
    obs, acts, rews, next_obs, dones = [], [], [], [], []
    for _ in range(episodes):
        pos, vel = world.reset(rng)
        for _ in range(steps):
            action = rng.integers(0, 4)
            img = world.render(pos, image_size)
            pos, vel, reward, done = world.step(pos, vel, action, rng)
            next_img = world.render(pos, image_size)
            obs.append(img)
            acts.append(action)
            rews.append(reward)
            next_obs.append(next_img)
            dones.append(done)
    return (
        np.stack(obs),
        np.array(acts, dtype=np.int64),
        np.array(rews, dtype=np.float32),
        np.stack(next_obs),
        np.array(dones, dtype=np.float32),
    )


world = DotWorld()
obs, acts, rews, next_obs, dones = collect_random_data(world)
Out[6]:
Console
Reward statistics: mean=-0.171, std=0.063
Action counts (0..3): [760 755 763 722]

The dot rarely sits near the center under random actions, so rewards are mostly negative. Actions are sampled uniformly, but momentum, walls, and resets make the resulting spatial occupancy nonuniform. The data provide broad exploratory coverage, not an even sample of the box.

Now we define the RSSM. We keep it small so training on CPU is quick. The GRU carries the deterministic state; a small MLP produces the prior and posterior parameters; another MLP decodes observations and predicts rewards.

In[7]:
Code
class RSSM(nn.Module):
    def __init__(
        self,
        action_dim=4,
        deter_dim=64,
        latent_dim=16,
        hidden_dim=128,
        obs_size=16,
    ):
        super().__init__()
        self.action_dim = action_dim
        self.deter_dim = deter_dim
        self.latent_dim = latent_dim
        self.obs_size = obs_size

        self.input_proj = nn.Linear(action_dim + latent_dim, hidden_dim)
        self.gru = nn.GRUCell(hidden_dim, deter_dim)

        self.prior_net = nn.Sequential(
            nn.Linear(deter_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 2 * latent_dim),
        )
        self.post_net = nn.Sequential(
            nn.Linear(deter_dim + obs_size * obs_size, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 2 * latent_dim),
        )
        self.decoder = nn.Sequential(
            nn.Linear(deter_dim + latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, obs_size * obs_size),
        )
        self.reward_head = nn.Sequential(
            nn.Linear(deter_dim + latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 1),
        )
        self.continue_head = nn.Sequential(
            nn.Linear(deter_dim + latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 1),
        )

    def initial_state(self, batch_size, device):
        return torch.zeros(batch_size, self.deter_dim, device=device)

    def forward_step(self, prev_action, prev_latent, deter, obs=None):
        # prev_action: (B, action_dim) one-hot; obs: (B, obs_size*obs_size) flattened
        x = torch.cat([prev_action, prev_latent], dim=-1)
        x = torch.relu(self.input_proj(x))
        deter = self.gru(x, deter)

        prior_params = self.prior_net(deter)
        prior_mean, prior_logvar = prior_params.chunk(2, dim=-1)
        prior_logvar = torch.clamp(prior_logvar, -6.0, 2.0)

        if obs is not None:
            post_input = torch.cat([deter, obs], dim=-1)
            post_params = self.post_net(post_input)
            post_mean, post_logvar = post_params.chunk(2, dim=-1)
            post_logvar = torch.clamp(post_logvar, -6.0, 2.0)
        else:
            post_mean, post_logvar = None, None
        return deter, (prior_mean, prior_logvar), (post_mean, post_logvar)

    def decode(self, deter, latent):
        x = torch.cat([deter, latent], dim=-1)
        img = torch.sigmoid(self.decoder(x))
        reward = self.reward_head(x).squeeze(-1)
        cont = torch.sigmoid(self.continue_head(x)).squeeze(-1)
        return img, reward, cont

The forward_step method is where the prior and posterior branches live side by side. When an observation is supplied (training time), the posterior branch reads it; when it is absent (imagination time), only the prior is available. Notice that the deterministic state update happens before the branch, so both distributions are conditioned on the same memory. This ordering matters: the recurrent state summarizes the past. Both the prior and posterior then interpret that summary given different amounts of current information.

We need a helper that samples a latent using the reparameterization trick and computes the KL divergence between two diagonal Gaussians in closed form. This is standard VAE machinery, but here the KL is between the posterior and the prior at each step rather than against a fixed standard normal.

In[8]:
Code
def reparameterize(mean, logvar):
    std = torch.exp(0.5 * logvar)
    eps = torch.randn_like(std)
    return mean + eps * std


def gaussian_kl(post_mean, post_logvar, prior_mean, prior_logvar):
    # KL( N(post) || N(prior) ), diagonal covariance, summed over latent dims
    var_ratio = torch.exp(post_logvar - prior_logvar)
    diff = (post_mean - prior_mean) ** 2 / torch.exp(prior_logvar)
    return 0.5 * torch.sum(
        var_ratio + diff - 1.0 - (post_logvar - prior_logvar), dim=-1
    )

Now we write the sequence training step. We run the RSSM over a batch of sequences, using the posterior at each step because observations are available. The loss combines reconstruction (on the next observation, since we predict ot+1o_{t+1} from the state after applying ata_t), reward prediction, continue prediction, and the KL term. Next-observation reconstruction teaches the posterior representation to retain information about its observation; by itself it does not force dynamics learning because that posterior already receives the next observation. The action-conditioned prior and its KL agreement with the posterior provide the pressure to predict how latent state changes. That predictive pressure is essential for imagination to be meaningful.

In[9]:
Code
def sequence_batch(
    obs,
    acts,
    rews,
    next_obs,
    dones,
    batch_size,
    seq_len,
    device,
    episode_length=50,
):
    if len(obs) % episode_length:
        raise ValueError("transition count must contain complete episodes")
    if seq_len > episode_length:
        raise ValueError("seq_len must not exceed episode_length")
    episodes = len(obs) // episode_length
    episode_ids = np.random.randint(0, episodes, size=batch_size)
    offsets = np.random.randint(
        0, episode_length - seq_len + 1, size=batch_size
    )
    starts = episode_ids * episode_length + offsets
    obs_seq = np.stack([obs[s : s + seq_len] for s in starts]).astype(
        np.float32
    )
    act_seq = np.stack([acts[s : s + seq_len] for s in starts])
    rew_seq = np.stack([rews[s : s + seq_len] for s in starts])
    next_seq = np.stack([next_obs[s : s + seq_len] for s in starts]).astype(
        np.float32
    )
    done_seq = np.stack([dones[s : s + seq_len] for s in starts])

    obs_seq = torch.tensor(obs_seq, device=device).view(batch_size, seq_len, -1)
    next_seq = torch.tensor(next_seq, device=device).view(
        batch_size, seq_len, -1
    )
    act_seq = torch.tensor(act_seq, device=device)
    rew_seq = torch.tensor(rew_seq, device=device)
    done_seq = torch.tensor(done_seq, device=device)
    return obs_seq, act_seq, rew_seq, next_seq, done_seq


def one_hot_action(a, action_dim, device):
    return F.one_hot(a, num_classes=action_dim).float().to(device)


def model_loss(model, batch, beta=0.5):
    obs_seq, act_seq, rew_seq, next_seq, done_seq = batch
    B, T, _ = obs_seq.shape
    device = obs_seq.device

    deter = model.initial_state(B, device)
    zero_action = torch.zeros(B, model.action_dim, device=device)
    zero_latent = torch.zeros(B, model.latent_dim, device=device)
    deter, _, (q_mean, q_logvar) = model.forward_step(
        zero_action, zero_latent, deter, obs_seq[:, 0]
    )
    prev_latent = reparameterize(q_mean, q_logvar)

    rec_loss = 0.0
    rew_loss = 0.0
    cont_loss = 0.0
    kl_loss = 0.0

    for t in range(T):
        action = one_hot_action(act_seq[:, t], model.action_dim, device)
        deter, (p_mean, p_logvar), (q_mean, q_logvar) = model.forward_step(
            action, prev_latent, deter, next_seq[:, t]
        )
        latent = reparameterize(q_mean, q_logvar)

        img_hat, rew_hat, cont_hat = model.decode(deter, latent)
        rec_loss = (
            rec_loss
            + F.binary_cross_entropy(img_hat, next_seq[:, t], reduction="none")
            .sum(dim=-1)
            .mean()
        )
        rew_loss = (
            rew_loss
            + F.mse_loss(rew_hat, rew_seq[:, t], reduction="none").mean()
        )
        cont_target = 1.0 - done_seq[:, t]
        cont_loss = (
            cont_loss
            + F.binary_cross_entropy(
                cont_hat, cont_target, reduction="none"
            ).mean()
        )
        kl_loss = (
            kl_loss + gaussian_kl(q_mean, q_logvar, p_mean, p_logvar).mean()
        )

        prev_latent = latent

    rec_loss = rec_loss / T
    rew_loss = rew_loss / T
    cont_loss = cont_loss / T
    kl_loss = kl_loss / T
    total = rec_loss + rew_loss + cont_loss + beta * kl_loss
    return total, dict(
        rec=rec_loss.item(),
        rew=rew_loss.item(),
        cont=cont_loss.item(),
        kl=kl_loss.item(),
    )

Let's train the world model. We keep it short so the chapter remains runnable, and the run measures reconstruction, KL, and held-out pixel diagnostics without visually validating imagined trajectories.

In[10]:
Code
device = torch.device("cpu")
model = RSSM().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-3)

## Reserve the final eight complete episodes for evaluation.
train_end = 52 * 50
train_data = (
    obs[:train_end],
    acts[:train_end],
    rews[:train_end],
    next_obs[:train_end],
    dones[:train_end],
)

loss_history = []
for step in range(600):
    batch = sequence_batch(
        *train_data,
        batch_size=16,
        seq_len=20,
        device=device,
    )
    total, parts = model_loss(model, batch, beta=0.5)
    optimizer.zero_grad()
    total.backward()
    nn.utils.clip_grad_norm_(model.parameters(), 100.0)
    optimizer.step()
    if step % 100 == 0:
        loss_history.append((step, total.item(), parts["rec"], parts["kl"]))
Out[11]:
Console
Total loss: 177.7027 -> 6.3306
Reconstruction BCE: 176.8755 -> 6.2999
KL: 0.1613 -> 0.0478

The printed diagnostics are observations, not proofs. Falling reconstruction BCE shows progress on the stated likelihood, while the KL reports prior-posterior agreement. A small KL can mean an easy-to-predict representation or posterior collapse; a finite nonzero KL alone does not distinguish them. Rollout evaluation below tests what the learned prior predicts.

DotWorld never terminates naturally, so every continuation target in this toy dataset is one. The head is present to preserve the Dreamer objective, but this example does not test terminal prediction.

Now we can imagine: starting from a real posterior state, roll forward using only the prior and a sequence of actions. We compute open-loop rollouts and, separately, posterior reconstructions conditioned on each target frame. These diagnostics distinguish recursive prediction from observation-conditioned reconstruction.

In[12]:
Code
@torch.no_grad()
def imagine(model, obs_seed, start_action, horizon=15, action_dim=4):
    # Encode the seed observation to get a posterior latent, then roll forward on the prior.
    B = obs_seed.shape[0]
    device = obs_seed.device
    deter = model.initial_state(B, device)
    prev_action = torch.zeros(B, action_dim, device=device)
    prev_latent = torch.zeros(B, model.latent_dim, device=device)

    deter, (_, _), (q_mean, q_logvar) = model.forward_step(
        prev_action, prev_latent, deter, obs_seed.view(B, -1)
    )
    latent = q_mean  # use the mean for a deterministic seed

    images, rewards = [], []
    action = torch.as_tensor(start_action, dtype=torch.long, device=device)
    if action.ndim == 0:
        action = action.expand(B)
    elif action.shape != (B,):
        raise ValueError(
            f"start_action must be scalar or shape ({B},), got {tuple(action.shape)}"
        )
    for _ in range(horizon):
        next_action = one_hot_action(action, action_dim, device)
        deter, (p_mean, p_logvar), _ = model.forward_step(
            prev_action=next_action, prev_latent=latent, deter=deter, obs=None
        )
        latent = p_mean  # deterministic mean of the prior for a stable rollout
        img, rew, _ = model.decode(deter, latent)
        images.append(img.view(B, model.obs_size, model.obs_size))
        rewards.append(rew)
        prev_action = next_action
    images = torch.stack(images, dim=1)
    rewards = torch.stack(rewards, dim=1)
    return images, rewards


seed = torch.tensor(next_obs[:4], dtype=torch.float32)
seed_actions = torch.tensor([2, 3, 0, 1])  # left, right, up, down
imagined_imgs, imagined_rews = imagine(model, seed, seed_actions, horizon=15)

In this seeded run, the imagined_rews tensor computed above contains 60 predictions ranging from -0.1648 to -0.1514. All are negative, consistent with the environment's non-positive reward function; the hidden cell does not display them, and their sign alone does not establish reward calibration. The following hidden diagnostic compares one seed under constant-left and constant-right actions. It exercises action conditioning but, because it is neither displayed nor aggregated across seeds, does not establish general action sensitivity.

In[13]:
Code
## Take one seed and compare repeated left versus repeated right actions.
single_seed = torch.tensor(next_obs[:1], dtype=torch.float32)
imgs_left, rews_left = imagine(model, single_seed, start_action=2, horizon=15)
imgs_right, rews_right = imagine(model, single_seed, start_action=3, horizon=15)

Because this single-seed result is hidden, no conclusion about action sensitivity follows from it. The actor-critic section instead treats real-environment return as the transfer test.

Before we move on, compare posterior reconstruction with recursive open-loop rollout on the same held-out windows. The posterior diagnostic conditions on each target next frame and samples a latent; the open-loop diagnostic uses prior means after the seed. Their difference therefore combines observation conditioning with the sample-versus-mean choice.

In[14]:
Code
@torch.no_grad()
def open_loop_rollout_error(model, obs_seq, act_seq, next_seq, steps=15):
    # Seed with the first frame, then roll the prior forward with no further observations.
    B = obs_seq.shape[0]
    device = obs_seq.device
    deter = model.initial_state(B, device)
    prev_action = torch.zeros(B, model.action_dim, device=device)
    prev_latent = torch.zeros(B, model.latent_dim, device=device)

    deter, (_, _), (q_mean, _) = model.forward_step(
        prev_action, prev_latent, deter, obs_seq[:, 0].view(B, -1)
    )
    latent = q_mean

    open_loop_err = []
    for t in range(steps):
        action = one_hot_action(act_seq[:, t], model.action_dim, device)
        deter, (p_mean, _), _ = model.forward_step(
            prev_action=action, prev_latent=latent, deter=deter, obs=None
        )
        latent = p_mean
        img_hat, _, _ = model.decode(deter, latent)
        target = next_seq[:, t].view(B, -1)
        open_loop_err.append(
            F.mse_loss(img_hat, target, reduction="none").mean(dim=-1)
        )
        prev_action = action
    open_loop_err = torch.stack(open_loop_err, dim=1)
    return open_loop_err.mean(dim=0)


@torch.no_grad()
def posterior_reconstruction_error(model, obs_seq, act_seq, next_seq, steps=15):
    # Infer the seed, then update with the action and resulting next observation.
    B = obs_seq.shape[0]
    device = obs_seq.device
    deter = model.initial_state(B, device)
    zero_action = torch.zeros(B, model.action_dim, device=device)
    zero_latent = torch.zeros(B, model.latent_dim, device=device)
    deter, _, (q_mean, q_logvar) = model.forward_step(
        zero_action, zero_latent, deter, obs_seq[:, 0]
    )
    prev_latent = reparameterize(q_mean, q_logvar)

    one_step_err = []
    for t in range(steps):
        action = one_hot_action(act_seq[:, t], model.action_dim, device)
        deter, _, (q_mean, q_logvar) = model.forward_step(
            action, prev_latent, deter, next_seq[:, t].view(B, -1)
        )
        latent = reparameterize(q_mean, q_logvar)
        img_hat, _, _ = model.decode(deter, latent)
        target = next_seq[:, t].view(B, -1)
        one_step_err.append(
            F.mse_loss(img_hat, target, reduction="none").mean(dim=-1)
        )
        prev_latent = latent
    one_step_err = torch.stack(one_step_err, dim=1)
    return one_step_err.mean(dim=0)


## Use one fixed within-episode window from each reserved episode.
hold_out_start = train_end
T_eval = 15
ho_obs_seq = torch.stack(
    [
        torch.tensor(
            obs[hold_out_start + 50 * s : hold_out_start + 50 * s + T_eval],
            dtype=torch.float32,
        )
        for s in range(8)
    ],
    dim=0,
).view(8, T_eval, -1)
ho_next_seq = torch.stack(
    [
        torch.tensor(
            next_obs[
                hold_out_start + 50 * s : hold_out_start + 50 * s + T_eval
            ],
            dtype=torch.float32,
        )
        for s in range(8)
    ],
    dim=0,
).view(8, T_eval, -1)
ho_act_seq = torch.stack(
    [
        torch.tensor(
            acts[hold_out_start + 50 * s : hold_out_start + 50 * s + T_eval],
            dtype=torch.int64,
        )
        for s in range(8)
    ],
    dim=0,
)

posterior_curve = posterior_reconstruction_error(
    model, ho_obs_seq, ho_act_seq, ho_next_seq, steps=T_eval
).numpy()
open_loop_curve = open_loop_rollout_error(
    model, ho_obs_seq, ho_act_seq, ho_next_seq, steps=T_eval
).numpy()
Out[15]:
Visualization
Line chart of posterior reconstruction and open-loop pixel MSE as percent difference from the dashed all-black baseline over fifteen held-out steps; both curves stay within about one percent of zero.
Mean pixel MSE on eight held-out episode windows, plotted as a percentage difference from the 1/256 all-black baseline (zero); negative values are lower error than that baseline. Posterior reconstruction conditions on each target frame and samples a latent; open-loop rollout uses prior means after the seed, so the comparison includes both conditioning and sampling differences.

The curves must be read from the executed values. Their gap combines observation conditioning and different latent choices (posterior samples versus prior means). Every target contains one lit pixel, so an all-black prediction already has MSE 1/2561/256; the dashed zero line marks that trivial baseline, and negative percentages mean lower MSE. Both curves stay within about one percent of it. This small difference does not establish state tracking: even a predictor of average pixel occupancy could beat all-black without following the dot. Overlap alone is not diagnostic, since two accurate predictors could also overlap, and neither curve proves accurate long-horizon imagination.

A related evaluation pitfall appears in Part XI: Evaluation and Understanding: good one-step prediction need not yield useful multi-step imagined trajectories.

Latent imagination and actor-critic learning

We now have a model that predicts latent transitions, rewards, and continuations. Dreamer trains an actor and critic on imagined trajectories rather than unrolling the environment for each update. Depending on the generation and domain, the actor uses either analytic gradients through imagined dynamics or score-function gradients from returns while the model trajectory is stopped.

The actor update uses rewards and values predicted by the learned model rather than direct rewards from the environment. Those predictions are training targets, not ground truth. Real replay observations seed the imagined trajectories; after that, states and rewards come from the model. If the rollout leaves the data-supported region, the actor may optimize behavior that does not transfer.

The setup is as follows. At each iteration, you take a batch of real observations, encode them into latents, then roll out an imagined trajectory using the prior and the current policy for action selection. For each imagined step, we record the predicted reward, the predicted continuation probability, and the predicted value from the critic. The critic is trained against a continuation-weighted lambda-return that combines predicted rewards with critic bootstraps along the imagined trajectory and at its horizon. The actor is trained to maximize those imagined returns. This is essentially an actor-critic method, but the trajectory is entirely synthetic and lives in latent space.

Why use an actor instead of online planning? A policy amortizes decision-making across states and selects an action with one network evaluation rather than a fresh trajectory search. The actor-gradient estimator differs by generation: V1 backpropagates through imagined dynamics, reparameterizing continuous actions and using straight-through gradients for discrete actions; V2 uses REINFORCE on Atari and dynamics gradients on continuous control; V3 uses entropy-regularized REINFORCE and stops actor gradients through the world model.

An actor amortizes action selection across the states used in training. Unlike a fresh PlaNet-style CEM solve, it reuses learned parameters at the next decision. This does not guarantee smooth actions for nearby latents, and planners can also reuse computation through warm starts, learned priors, or value functions.

The toy implementation below uses a V3-style score-function update for discrete actions, but omits the cumulative imagination weights used in full Dreamer training. The world model generates states and rewards without gradients; the actor is then updated from the log-probabilities of exactly those sampled actions at exactly those decision states.

The toy actor loss maximizes expected advantage weighted by the log-probability of the chosen actions, with an entropy bonus to maintain exploration. Its unweighted surrogate is:

Lagent(θ)=−E[∑t=0H−1(log⁡πθ(at∣st) sg ⁣[Rtλ−Vψ(st)]+η H[πθ(⋅∣st)])],\mathcal{L}_\text{agent}(\theta) = -\mathbb{E}\left[\sum_{t=0}^{H-1} \left( \log \pi_\theta(a_t \mid s_t) \, \mathrm{sg}\!\left[ R^\lambda_t - V_\psi(s_t) \right] + \eta \, H[\pi_\theta(\cdot \mid s_t)] \right) \right],

where:

  • θ\theta: the actor parameters
  • ψ\psi: the critic parameters
  • sg[⋅]\mathrm{sg}[\cdot]: the stop-gradient operator
  • RtλR^\lambda_t: the lambda-return, a weighted average of multi-step returns and bootstrapped critic predictions
  • Vψ(st)V_\psi(s_t): the critic's value estimate at imagined state sts_t
  • η\eta: the entropy coefficient
  • HH: the imagination horizon
  • sts_t: the imagined decision state before action ata_t
  • H[πθ(⋅∣st)]H[\pi_\theta(\cdot \mid s_t)]: the entropy of the actor's action distribution at sts_t

The toy critic is trained to regress the lambda-return:

Lcritic(ψ)=E[∑t(Vψ(st)−sg ⁣[Rtλ])2].\mathcal{L}_\text{critic}(\psi) = \mathbb{E}\left[ \sum_t \left( V_\psi(s_t) - \mathrm{sg}\!\left[ R^\lambda_t \right] \right)^2 \right].

The lambda-return blends multi-step rewards with a value bootstrap at the imagination horizon. When λ=0\lambda = 0 it reduces to a one-step TD target. When λ=1\lambda = 1, it includes rewards until a zero continuation; the horizon bootstrap is weighted by the product of intervening continuation probabilities:

Rtλ=rt+γct[(1−λ)Vψ(st+1)+λRt+1λ],R^\lambda_t = r_t + \gamma c_t \left[ (1 - \lambda) V_\psi(s_{t+1}) + \lambda R^\lambda_{t+1} \right],

where γ\gamma is the discount factor, ctc_t is the predicted continuation probability, and the boundary is RHλ=Vψ(sH)R^\lambda_H=V_\psi(s_H). A zero continuation truncates the return; a probability between zero and one softly downweights future terms.

For an episodic Dreamer objective, imagined steps also receive cumulative weights w0=1w_0=1 and wt=∏i=0t−1γciw_t=\prod_{i=0}^{t-1}\gamma c_i. Without these weights, the actor, critic, and entropy losses would treat post-terminal imagined states as valid training data. Our toy code averages those losses across steps instead. DotWorld has no real terminal transitions and its continuation target is always one, so this example cannot test post-terminal masking. Even here, the code omits γt\gamma^t weighting and any downweighting from imperfect continuation predictions; it is not the published objective.

Why a lambda-return rather than the simple discounted return Rt=rt+γRt+1R_t = r_t + \gamma R_{t+1}? Because the imagination horizon is finite. An unbootstrapped recursion sets the unseen boundary RH=0R_H=0, so its last included target is RH−1=rH−1R_{H-1}=r_{H-1} and earlier near-horizon targets omit all rewards beyond the rollout. They are truncated rather than generally zero, which can bias long-horizon value estimates. Bootstrapping with the critic's own prediction at the horizon mitigates this, and λ\lambda interpolates between low-variance (small λ\lambda, mostly bootstrapped) and low-bias (large λ\lambda, mostly Monte Carlo) returns. This is the same TD(λ\lambda) idea that underlies much of modern deep RL.

Smaller λ\lambda relies more heavily on the critic; larger λ\lambda relies more heavily on accumulated model rewards. The familiar bias-variance description is conditional because model error and critic error both contribute. A value of 0.95 is an empirical setting, not a universal optimum.

The actor objective depends on both its action distribution and the action-conditioned latent trajectory. When every stochastic node on that path is reparameterizable, differentiating through the model gives a pathwise gradient of the learned-model objective; it can still be biased relative to real-environment return when the model is inaccurate and is often lower variance than REINFORCE. DreamerV2's categorical straight-through path is instead a biased surrogate estimator, and its Atari actor uses REINFORCE. DreamerV3 stops world-model gradients and uses a REINFORCE-style signal weighted by the critic's advantage.

Stopping the pathwise derivative does not prevent exploitation. A score-function update can still reinforce actions whose modeled transitions or rewards produce spuriously high returns. REINFORCE removes one gradient path and was reported to work across DreamerV3's domains; it does not make either learned head trustworthy outside the data distribution.

Now let's implement the actor and critic on top of our trained RSSM. We add a small MLP actor that takes the latent state and outputs a categorical action distribution, plus a critic that takes the latent state and outputs a scalar value.

In[16]:
Code
class Actor(nn.Module):
    def __init__(
        self, latent_dim=16, deter_dim=64, action_dim=4, hidden_dim=128
    ):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(deter_dim + latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, action_dim),
        )

    def forward(self, deter, latent):
        x = torch.cat([deter, latent], dim=-1)
        return torch.distributions.Categorical(logits=self.net(x))


class Critic(nn.Module):
    def __init__(self, latent_dim=16, deter_dim=64, hidden_dim=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(deter_dim + latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 1),
        )

    def forward(self, deter, latent):
        x = torch.cat([deter, latent], dim=-1)
        return self.net(x).squeeze(-1)

The rollout records each pre-action decision state, sampled action, predicted reward, and successor state. The world model is frozen, matching the stopped-dynamics estimator used by V3 and by this toy discrete-action example.

In[17]:
Code
@torch.no_grad()
def encode_seed(model, obs_seed):
    B = obs_seed.shape[0]
    device = obs_seed.device
    deter = model.initial_state(B, device)
    prev_action = torch.zeros(B, model.action_dim, device=device)
    prev_latent = torch.zeros(B, model.latent_dim, device=device)
    deter, (_, _), (q_mean, _) = model.forward_step(
        prev_action, prev_latent, deter, obs_seed.view(B, -1)
    )
    return deter, q_mean


@torch.no_grad()
def imagine_trajectory(model, actor, obs_seed, horizon=15, gamma=0.99):
    deter, latent = encode_seed(model, obs_seed)
    device = obs_seed.device
    B = obs_seed.shape[0]

    latents, deters, actions = [], [], []
    rewards, continues = [], []

    for _ in range(horizon):
        deters.append(deter)
        latents.append(latent)
        dist = actor(deter, latent)
        action = dist.sample()

        action_onehot = F.one_hot(action, num_classes=model.action_dim).float()
        deter, (p_mean, _), _ = model.forward_step(
            prev_action=action_onehot, prev_latent=latent, deter=deter, obs=None
        )
        latent = p_mean
        _, rew_hat, cont_hat = model.decode(deter, latent)

        actions.append(action)
        rewards.append(rew_hat)
        continues.append(cont_hat)

    return dict(
        deters=torch.stack(deters, dim=1),
        latents=torch.stack(latents, dim=1),
        actions=torch.stack(actions, dim=1),
        rewards=torch.stack(rewards, dim=1),
        continues=torch.stack(continues, dim=1),
        final_deter=deter,
        final_latent=latent,
        gamma=gamma,
    )

The imagine_trajectory function is the core of Dreamer. Notice that the model is asked for its prior at every step, never its posterior, because there is no new observation to condition on. The action is sampled from the current actor, the reward and continuation are read from the model's heads, and the latent is carried forward. The whole imagined episode is generated by the model playing both the world and the reward function, with the actor supplying the choices.

Now we compute the lambda-returns from the imagined rewards, continuation predictions, and critic values at each step. We use backward recursion, which is the cleanest way to compute and it avoids the recursion depth growing with the horizon.

In[18]:
Code
def compute_lambda_returns(rewards, continues, values, gamma=0.99, lam=0.95):
    # rewards and continues: (B, H); values: (B, H + 1)
    B, H = rewards.shape
    returns = torch.zeros_like(rewards)
    next_return = values[:, H]
    for t in reversed(range(H)):
        next_value = values[:, t + 1]
        bootstrap = (1.0 - lam) * next_value + lam * next_return
        next_return = rewards[:, t] + gamma * continues[:, t] * bootstrap
        returns[:, t] = next_return
    return returns


def actor_critic_loss(
    model,
    actor,
    critic,
    obs_seed,
    horizon=15,
    gamma=0.99,
    lam=0.95,
    entropy_coef=1e-3,
):
    traj = imagine_trajectory(
        model, actor, obs_seed, horizon=horizon, gamma=gamma
    )

    deters = traj["deters"]
    latents = traj["latents"]
    rewards = traj["rewards"]
    continues = traj["continues"]

    B, H, D = deters.shape
    flat_deter = deters.reshape(B * H, D)
    flat_latent = latents.reshape(B * H, -1)
    decision_values = critic(flat_deter, flat_latent).reshape(B, H)
    final_value = critic(traj["final_deter"], traj["final_latent"]).unsqueeze(1)
    values = torch.cat([decision_values, final_value], dim=1)

    returns = compute_lambda_returns(
        rewards, continues, values.detach(), gamma=gamma, lam=lam
    )
    advantage = (returns - decision_values).detach()

    # log_probs must be recomputed FROM the sampled actions so gradients flow to the actor;
    # the current log_probs come from @torch.no_grad() imagine_trajectory and carry no grad_fn.
    dist_for_loss = actor(flat_deter, flat_latent)
    log_probs = dist_for_loss.log_prob(traj["actions"].reshape(B * H)).reshape(
        B, H
    )
    actor_loss = -(log_probs * advantage).mean()

    critic_loss = F.mse_loss(decision_values, returns.detach())

    dist = actor(flat_deter, flat_latent)
    entropy = dist.entropy().mean()
    total = actor_loss + critic_loss - entropy_coef * entropy
    return total, dict(
        actor=actor_loss.item(),
        critic=critic_loss.item(),
        entropy=entropy.item(),
        return_mean=returns.mean().item(),
    )

The actor_loss line implements the toy's unweighted REINFORCE-with-advantage rule: the log-probability of the sampled action is multiplied by the advantage and used as a loss to maximize. The advantage is detached, so the actor is not tempted to move the value estimate itself. It can only change the action distribution. The critic_loss is a plain mean-squared error against the detached lambda-return. The entropy term is subtracted from the total loss because it is added to the objective, encouraging the actor to keep its action distribution spread out early in training. For an episodic task, the actor, critic, and entropy reductions would need the cumulative discount-and-continuation weights described above.

Now we train the actor and critic on imagined trajectories. Note that the model is frozen during this loop: only the actor and critic update. In a full Dreamer implementation, you would alternate between model updates and agent updates. You would also collect new real data between iterations. Here we keep the model fixed so we can isolate the actor-critic mechanism.

In[19]:
Code
actor = Actor()
critic = Critic()
for parameter in model.parameters():
    parameter.requires_grad_(False)
agent_optimizer = torch.optim.Adam(
    list(actor.parameters()) + list(critic.parameters()), lr=3e-4
)

seed_pool = torch.tensor(next_obs[:256], dtype=torch.float32)

agent_history = []
for step in range(400):
    idx = np.random.randint(0, len(seed_pool), size=16)
    seeds = seed_pool[idx]
    loss, parts = actor_critic_loss(model, actor, critic, seeds, horizon=15)
    agent_optimizer.zero_grad()
    loss.backward()
    nn.utils.clip_grad_norm_(
        list(actor.parameters()) + list(critic.parameters()), 100.0
    )
    agent_optimizer.step()
    if step % 50 == 0:
        agent_history.append(
            (step, parts["actor"], parts["critic"], parts["return_mean"])
        )
Out[20]:
Console
Imagined return: -0.9208 -> -15.8317

Several train/evaluation differences matter here. seed_pool takes only the first 256 of 2,600 training transitions as isolated replay frames, so encode_seed starts RSSM memory at zero for each imagined trajectory; real-environment evaluation starts fresh episodes and carries recurrent memory through the steps of each episode. The actor samples actions during imagination, while evaluation below selects the most probable action. These seed-distribution, memory, and action-selection differences can affect real performance, so comparing the two return measurements does not isolate world-model exploitation.

The mean imagined lambda-return is a changing model-side diagnostic, not real environment return. Because its critic bootstrap changes during training, movement in this quantity cannot be attributed to the actor alone. In this run it decreases while the trained policy also performs worse than the random baseline in the real environment. These results do not establish an imagination-to-reality transfer failure or identify why the policy underperforms. Model exploitation, estimator noise, optimization failure, inadequate state learning, and the seed-state and action-selection mismatches just described remain possibilities. In a full system, poor real performance motivates collecting fresh data and updating the model.

Let's evaluate the trained actor greedily on the real environment, unrolling it step by step. Does the actor, trained on imagined rollouts seeded from real observations, keep the dot near the center in the real world?

In[21]:
Code
@torch.no_grad()
def evaluate_actor(model, actor, world, episodes=20, steps=50, image_size=16):
    rng = np.random.default_rng(999)
    total_rewards = []
    for _ in range(episodes):
        pos, vel = world.reset(rng)
        episode_reward = 0.0
        deter = model.initial_state(1, torch.device("cpu"))
        prev_action = torch.zeros(1, model.action_dim)
        prev_latent = torch.zeros(1, model.latent_dim)
        for _ in range(steps):
            img = world.render(pos, image_size)
            obs_t = torch.tensor(img, dtype=torch.float32).unsqueeze(0)
            deter, _, (q_mean, _) = model.forward_step(
                prev_action, prev_latent, deter, obs_t.view(1, -1)
            )
            latent = q_mean
            dist = actor(deter, latent)
            action = int(dist.probs.argmax(dim=-1).item())
            pos, vel, reward, done = world.step(pos, vel, action, rng)
            episode_reward += reward
            prev_action = one_hot_action(
                torch.tensor([action]), model.action_dim, torch.device("cpu")
            )
            prev_latent = latent
        total_rewards.append(episode_reward)
    return np.mean(total_rewards), np.std(total_rewards)


## Compute the random-policy baseline for comparison
rng = np.random.default_rng(777)
random_episode_rewards = []
for _ in range(20):
    pos, vel = world.reset(rng)
    r = 0.0
    for _ in range(50):
        action = rng.integers(0, 4)
        pos, vel, reward, done = world.step(pos, vel, action, rng)
        r += reward
    random_episode_rewards.append(r)
random_mean = np.mean(random_episode_rewards)
random_std = np.std(random_episode_rewards)

trained_mean, trained_std = evaluate_actor(model, actor, world)
print(f"Random return: {random_mean:.4f} +/- {random_std:.4f}")
print(f"Trained return: {trained_mean:.4f} +/- {trained_std:.4f}")

This comparison is diagnostic rather than guaranteed. Improvement would support transfer on this toy run; a worse return can reflect model exploitation, an inadequate model, estimator noise, optimization failure, or the seed-state and action-selection differences between training and evaluation. In practice, Dreamer alternates data collection, model learning, and actor-critic learning. The benchmark systems also include architectural, optimization, replay, and interface details beyond scaling networks and training time.

The most important takeaways from this section:

  • Dreamer trains the actor and critic in imagination, using the learned model as a latent simulator.
  • The actor's signal comes from the critic's advantage, not from real rewards at training time.
  • The critic is trained with a lambda-return whose continuation factor truncates at zero and otherwise downweights future terms.
  • This toy implementation and DreamerV3 stop actor gradients through the world model; V1 and V2 use the generation-specific estimators described above.
  • The imagined return and the real return can diverge. Closing that gap is what the rest of the Dreamer pipeline is for.

DreamerV2 and discrete latents

The original Dreamer established the imagined actor-critic recipe primarily on continuous-control tasks and also evaluated discrete actions and early termination on selected Atari and DMLab tasks. DreamerV2 scaled the approach to the full 55-game Atari benchmark and found that categorical stochastic states outperformed Gaussian states on 42 of 55 games in the reported comparison. The paper presents possible explanations as hypotheses, and the full Atari result also depends on KL balancing, actor-gradient choices, architecture, replay, and optimization. These results indicate the categorical change was central, but not sufficient on its own.

Why would discrete latents help so much? There are several converging reasons, and it is worth understanding each because the same reasoning recurs in later architectures.

The paper reports the categorical parameterization as a decisive empirical change. Its discussion offers hypotheses rather than a theorem: categorical variables may be easier to optimize and may impose a useful inductive bias. They do not guarantee sharper reconstructions or physically valid latent states.

Categorical and Gaussian KL terms have different geometry. For a KK-way posterior against a uniform prior, KL(q∥u)≤log⁡K\mathrm{KL}(q\|u)\leq\log K. This is not a general bound for KL(q∥p)\mathrm{KL}(q\|p): it can grow without bound as a learned prior assigns probability near zero where the posterior has mass. Gaussian KL is likewise unbounded.

Third, there is nominal information capacity. A KK-way categorical variable carries at most log⁡2K\log_2 K bits. With 32 independent groups of 32 categories, DreamerV2's default stochastic state has maximum entropy of 160 bits, but its realized information content can be lower.

The categorical latent uses a straight-through estimator. It draws a one-hot sample, then adds the probabilities and subtracts detached probabilities, so the forward value stays discrete while the surrogate gradient follows the probabilities:

soft=softmax(logits),sample∼OneHotCategorical(soft),z=sample+soft−sg[soft],\text{soft} = \mathrm{softmax}(\text{logits}), \qquad \text{sample} \sim \mathrm{OneHotCategorical}(\text{soft}), \qquad z = \text{sample} + \text{soft} - \mathrm{sg}[\text{soft}],

where sg[⋅]\mathrm{sg}[\cdot] is stop-gradient. DreamerV2 draws a categorical one-hot sample, then uses sample + probs - stop(probs). This is a straight-through categorical estimator, not a deterministic argmax and not a Gumbel-softmax draw.

The categorical KL is computed for each group and summed. DreamerV2 also balances its gradients: one term fits the prior to a stopped posterior, while the other makes the posterior predictable by a stopped prior. With α=0.8\alpha=0.8, the reported objective gives greater weight to fitting the prior:

LKL=α KL ⁣(sg[q] ∥ p)+(1−α) KL ⁣(q ∥ sg[p]),\mathcal{L}_\text{KL} = \alpha \, \mathrm{KL}\!\left( \mathrm{sg}[q] \,\|\, p \right) + (1 - \alpha) \, \mathrm{KL}\!\left( q \,\|\, \mathrm{sg}[p] \right),

with α\alpha typically around 0.8. The first term is the "dynamics" term: it trains the prior to match the posterior without letting gradients flow into the posterior. The second term is the "representation" term: it trains the posterior to be predictable by the prior, without letting gradients flow into the prior. Together, the weighted terms encourage prior-posterior agreement; they do not guarantee it or prevent either distribution from dominating during training. Without balancing, the posterior can drift away from the prior and imagination can lose grounding. The asymmetry in the weights reflects a deliberate priority. The dynamics term is weighted more heavily because a well-trained prior is what makes imagination work, while the representation term is kept lighter to reduce pressure to distort the posterior's ability to encode observations accurately.

The official DreamerV2 implementation initializes recurrent states to zero. Layer normalization appears in the implementation, but the paper's ablation describes its contribution as marginal. KL balancing and the categorical state are the documented central changes; the paper does not isolate mixed precision as an explanation for the headline result.

At 200 million Atari frames, the DreamerV2 paper reported a median human-normalized score of 2.15 across 55 games. Its Rainbow and IQN comparisons in that table use the same 200-million-frame interaction budget, so this result is not evidence of fewer interactions than those baselines. The notable scope claim is the single agent configuration across the suite.

Let's implement the categorical latent mechanism for our dot world. The conceptual pipeline stays the same, but categorical rollout, actor, and critic wiring would need compatible latent shapes and return signatures; the Gaussian helpers below are not drop-in compatible.

In[22]:
Code
class CategoricalRSSM(nn.Module):
    def __init__(
        self,
        action_dim=4,
        deter_dim=64,
        num_groups=8,
        num_classes=8,
        hidden_dim=128,
        obs_size=16,
    ):
        super().__init__()
        self.action_dim = action_dim
        self.deter_dim = deter_dim
        self.num_groups = num_groups
        self.num_classes = num_classes
        self.latent_dim = num_groups * num_classes
        self.obs_size = obs_size

        self.input_proj = nn.Linear(action_dim + self.latent_dim, hidden_dim)
        self.gru = nn.GRUCell(hidden_dim, deter_dim)
        self.prior_net = nn.Sequential(
            nn.Linear(deter_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, num_groups * num_classes),
        )
        self.post_net = nn.Sequential(
            nn.Linear(deter_dim + obs_size * obs_size, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, num_groups * num_classes),
        )
        self.decoder = nn.Sequential(
            nn.Linear(deter_dim + self.latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, obs_size * obs_size),
        )
        self.reward_head = nn.Sequential(
            nn.Linear(deter_dim + self.latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 1),
        )
        self.continue_head = nn.Sequential(
            nn.Linear(deter_dim + self.latent_dim, hidden_dim),
            nn.ELU(),
            nn.Linear(hidden_dim, 1),
        )

    def initial_state(self, batch_size, device):
        return torch.zeros(batch_size, self.deter_dim, device=device)

    def _logits_to_latent(self, logits):
        # logits: (B, num_groups * num_classes)
        B = logits.shape[0]
        logits = logits.view(B, self.num_groups, self.num_classes)
        soft = F.softmax(logits, dim=-1)
        sample = torch.distributions.OneHotCategorical(logits=logits).sample()
        straight_through = sample + soft - soft.detach()
        return straight_through.view(B, self.latent_dim)

    def forward_step(self, prev_action, prev_latent, deter, obs=None):
        x = torch.cat([prev_action, prev_latent], dim=-1)
        x = torch.relu(self.input_proj(x))
        deter = self.gru(x, deter)
        prior_logits = self.prior_net(deter).view(
            -1, self.num_groups, self.num_classes
        )
        if obs is not None:
            post_input = torch.cat([deter, obs], dim=-1)
            post_logits = self.post_net(post_input).view(
                -1, self.num_groups, self.num_classes
            )
        else:
            post_logits = None
        return deter, prior_logits, post_logits

    def decode(self, deter, latent):
        x = torch.cat([deter, latent], dim=-1)
        img = torch.sigmoid(self.decoder(x))
        reward = self.reward_head(x).squeeze(-1)
        cont = torch.sigmoid(self.continue_head(x)).squeeze(-1)
        return img, reward, cont

The _logits_to_latent method implements the paper's stochastic straight-through categorical sample. The forward value is one-hot; the backward surrogate follows the probabilities.

Two new helper functions: a categorical KL over groups, and KL balancing. Then a training step for the categorical model.

In[23]:
Code
def categorical_kl(post_logits, prior_logits):
    # post_logits, prior_logits: (B, num_groups, num_classes)
    post = F.softmax(post_logits, dim=-1)
    prior = F.softmax(prior_logits, dim=-1)
    log_post = F.log_softmax(post_logits, dim=-1)
    log_prior = F.log_softmax(prior_logits, dim=-1)
    # Sum categories and independent groups; caller averages the batch.
    kl = (post * (log_post - log_prior)).sum(dim=-1).sum(dim=-1)
    return kl  # shape (B,)


def balanced_kl_loss(post_logits, prior_logits, alpha=0.8):
    # alpha weights the "dynamics" term KL(sg[post] || prior); (1-alpha) weights "representation" KL(post || sg[prior])
    post_detached = post_logits.detach()
    prior_detached = prior_logits.detach()
    dynamics_kl = categorical_kl(post_detached, prior_logits)
    representation_kl = categorical_kl(post_logits, prior_detached)
    return alpha * dynamics_kl.mean() + (1.0 - alpha) * representation_kl.mean()


def categorical_model_loss(model, batch, beta=1.0):
    obs_seq, act_seq, rew_seq, next_seq, done_seq = batch
    B, T, _ = obs_seq.shape
    device = obs_seq.device

    deter = model.initial_state(B, device)
    zero_action = torch.zeros(B, model.action_dim, device=device)
    zero_latent = torch.zeros(B, model.latent_dim, device=device)
    deter, _, post_logits = model.forward_step(
        zero_action, zero_latent, deter, obs_seq[:, 0]
    )
    prev_latent = model._logits_to_latent(post_logits.view(B, -1))

    rec_loss = rew_loss = cont_loss = kl_loss = 0.0
    for t in range(T):
        action = one_hot_action(act_seq[:, t], model.action_dim, device)
        deter, prior_logits, post_logits = model.forward_step(
            action, prev_latent, deter, next_seq[:, t]
        )
        latent = model._logits_to_latent(post_logits.view(B, -1))
        img_hat, rew_hat, cont_hat = model.decode(deter, latent)
        rec_loss = (
            rec_loss
            + F.binary_cross_entropy(img_hat, next_seq[:, t], reduction="none")
            .sum(dim=-1)
            .mean()
        )
        rew_loss = (
            rew_loss
            + F.mse_loss(rew_hat, rew_seq[:, t], reduction="none").mean()
        )
        cont_loss = (
            cont_loss
            + F.binary_cross_entropy(
                cont_hat, 1.0 - done_seq[:, t], reduction="none"
            ).mean()
        )
        kl_loss = kl_loss + balanced_kl_loss(
            post_logits, prior_logits, alpha=0.8
        )
        prev_latent = latent

    total = (rec_loss + rew_loss + cont_loss) / T + beta * kl_loss / T
    return total, dict(rec=(rec_loss / T).item(), kl=(kl_loss / T).item())

We do not train or benchmark the categorical model here. The code illustrates stochastic sampling, tensor shapes, the summed group KL, and intended gradient routing, but comparative stability requires a matched multi-seed experiment and is not established by this example.

Three takeaways from the DreamerV2 section:

  • KL to a uniform KK-way prior is bounded by log⁡K\log K, but KL to a learned categorical prior is not generally bounded.
  • Straight-through estimation lets you train through discrete sampling without giving up end-to-end gradient flow.
  • KL balancing weights prior fitting more heavily while retaining a smaller posterior-regularization gradient; it encourages agreement but does not guarantee it.

Part IX: Foundation and World-Action Models is planned to cover tokenized game models and video generators.

DreamerV3 and cross-domain scaling

Earlier Dreamer versions used configurations developed for narrower benchmark suites. DreamerV3 evaluates one reported configuration across Atari, DMLab, robot control, and a modified Minecraft task. This establishes configuration robustness across the tested suites, not held-out-domain generalization.

One configuration reduces per-environment tuning within the evaluated scope. It does not by itself rule out suite-level engineering or establish generalization to unseen domains.

The problem with cross-domain robustness is scale mismatch. Different domains have:

  • Very different reward scales. Atari games often use small discrete game rewards, continuous-control tasks accumulate dense rewards over an episode, and the modified Minecraft task has sparse rewards. The resulting return and value targets can occupy different numerical ranges under one optimizer configuration.
  • Very different observation statistics. Pixel observations are in [0,1][0, 1], but proprioceptive observations, robot states, and image statistics vary widely. A model that is not scale-invariant will be biased toward some domains.
  • Very different horizons. Some tasks are solved in tens of steps; others (like Minecraft) require thousands of steps with sparse reward.

The following DreamerV3 transforms mainly address numerical scale and optimization. Long-horizon exploration and model accuracy remain separate challenges.

Symlog transformation. DreamerV3 applies symlog to vector observations as both encoder inputs and decoder reconstruction targets, and uses symexp-spaced reward and value bins:

symlog(x)=sign(x)log⁡(1+∣x∣)\mathrm{symlog}(x) = \mathrm{sign}(x) \log(1 + |x|)

where:

  • xx: the input value (e.g., a reward or value target) in the original scale
  • sign(x)\mathrm{sign}(x): the sign of xx, preserving the direction of the reward or value
  • log⁡(1+∣x∣)\log(1 + |x|): a compressing transform of the magnitude, so large magnitudes grow logarithmically rather than linearly

This map is monotone, sign-preserving, logarithmically compressive, and unbounded. For magnitudes well above one, multiplying by 100 changes symlog by approximately log⁡(100)\log(100); that approximation fails near zero. Its inverse is symexp.

Two-hot prediction. In the published DreamerV3 formulation, the reward and critic heads predict categorical distributions over original-scale bins B=symexp(linspace(−20,20))B=\mathrm{symexp}(\mathrm{linspace}(-20,20)). A scalar target assigns weights to its two neighboring values in BB, providing linear interpolation within that finite support. Decoding takes the probability-weighted average of the original-scale bin locations, y^=softmax(f(x))⊤B\hat y=\mathrm{softmax}(f(x))^\top B; it is not symexp applied after an average in symlog space. Targets outside the support require clipping, so the representation is not precise at arbitrary unknown scales.

Return normalization. DreamerV3 divides the actor advantage by an exponentially tracked 95th-to-5th percentile range of returns, floored at one. It does not rescale rewards or critic targets this way. The entropy and KL loss scales are fixed configuration values, not adaptive coefficients.

Free bits. DreamerV3 clips each of its split KL losses from below at one nat. This removes additional KL pressure below the threshold; it does not guarantee a floor on mutual information or prevent the stochastic channel from becoming uninformative.

Together with one-percent uniform mixing, architectural choices, replay, and optimizer settings, these techniques supported one reported configuration across DM Control, DMLab, Atari, and a modified MineRL Diamond task. The final Nature paper reports that every training run discovered diamonds without human data or task-specific curricula. This is a milestone under that protocol, not evidence that long-horizon exploration is solved.

The following standalone helpers demonstrate symlog, symexp, and two-hot encoding. They are not integrated into the scalar critic above.

In[24]:
Code
def symlog(x):
    return torch.sign(x) * torch.log1p(torch.abs(x))


def symexp(x):
    return torch.sign(x) * torch.expm1(torch.abs(x))


def value_bins(num_bins=41, symlog_limit=20.0, device=None):
    symlog_bins = torch.linspace(
        -symlog_limit, symlog_limit, num_bins, device=device
    )
    return symexp(symlog_bins)


def two_hot(target, num_bins=41, symlog_limit=20.0):
    # Interpolate the original-scale target between symexp-spaced bins.
    bins = value_bins(num_bins, symlog_limit, target.device)
    clipped = torch.clamp(target, bins[0], bins[-1])
    upper = torch.searchsorted(bins, clipped).clamp(1, num_bins - 1)
    lower = upper - 1
    lower_bin, upper_bin = bins[lower], bins[upper]
    upper_weight = (clipped - lower_bin) / (upper_bin - lower_bin)
    lower_weight = 1.0 - upper_weight
    out = torch.zeros(target.shape[0], num_bins, device=target.device)
    out.scatter_(1, lower.unsqueeze(1), lower_weight.unsqueeze(1))
    out.scatter_add_(1, upper.unsqueeze(1), upper_weight.unsqueeze(1))
    return out


def two_hot_value(critic_logits, num_bins=41, symlog_limit=20.0):
    # Average original-scale bins; split signs to reduce cancellation error.
    probs = F.softmax(critic_logits, dim=-1)
    bins = value_bins(num_bins, symlog_limit, critic_logits.device)
    weighted = probs * bins
    midpoint = num_bins // 2
    negative = weighted[..., :midpoint].flip(-1).sum(dim=-1)
    if num_bins % 2:
        center = weighted[..., midpoint]
        positive = weighted[..., midpoint + 1 :].sum(dim=-1)
    else:
        center = torch.zeros_like(negative)
        positive = weighted[..., midpoint:].sum(dim=-1)
    return negative + center + positive

Now verify the symlog round trip and observe its logarithmic compression. This check does not imply scale invariance.

In[25]:
Code
sample_rewards = torch.tensor([-100.0, -1.0, 0.0, 1.0, 100.0, 10000.0])
compressed = symlog(sample_rewards)
round_trip = symexp(compressed)
Out[26]:
Console
Max round-trip error after symlog/symexp: 2.93e-03
Values from 1 to 10000 span four orders of magnitude before compression.

For inputs restricted to [−10000,10000][-10000,10000], symlog outputs lie near [−9.2,9.2][-9.2,9.2]. Symlog itself remains unbounded. Two-hot prediction is also limited by its chosen finite bin support.

A central engineering detail in the published DreamerV3 formulation is the combination of symlog vector inputs, reward and value predictions over symexp-spaced original-scale bins, and percentile-normalized advantages. Its actor uses REINFORCE with an entropy term and stopped world-model gradients. The reported configuration does not use per-environment reward scaling or discount tuning.

We will not re-train DreamerV3 here. These transforms are individually compact, but the benchmark result belongs to their tested combination with the categorical RSSM, uniform mixing, split KL objective, architecture, optimizer, and replay design. Adding three transforms to V1 does not establish the same robustness.

Several limitations of DreamerV3 deserve attention, because a chapter that only reports the wins is not honest:

  • Sample efficiency does not imply lower compute. RSSM training and imagination add work, but wall-clock comparisons with model-free methods depend on architectures, replay ratios, hardware, and implementations.
  • Minecraft remains protocol-specific. Diamond discovery in the modified MineRL setting is a notable result without human data or curricula, not a general solution to sparse long-horizon exploration.
  • Imagination rewards can diverge from real rewards. Advantage normalization and free bits address numerical scale and KL pressure; they do not by themselves prevent out-of-distribution model exploitation.
  • One configuration does not mean assumption-free. The method still depends on the observation/action interfaces, replay distribution, training budget, and an environment regular enough to learn predictive latent dynamics.
  • Interpretability is not guaranteed. Learned latent coordinates need not align with human concepts, although probes or visualizations can sometimes reveal structure. A decoder tests reconstructive information, not semantic transparency.

These limitations animate a great deal of subsequent work. Conservative and offline variants (discussed later in this part) address distribution shift. Value-equivalent and control-centric models (the next two chapters) address the perception-versus-control gap by making the latent directly optimized for decision quality rather than reconstruction. Tokenized and video-generator world models (in Part IX) revisit the representation question with larger-scale generative machinery. And evaluation frameworks (in Part XI) provide the tools for systematically measuring whether an imagined policy performs well in the environment, rather than only appearing self-consistently optimistic.

Now let's see whether the Dreamer-style policy we trained earlier performs better than a random policy in the real environment. This summary uses the imagined return curve already logged during training, but samples a fresh set of 30 episodes per policy for the real-return comparison.

In[27]:
Code
## Precompute the trained-policy and random-policy returns before the plot cell, so figures only render data.
rng_eval = np.random.default_rng(2024)
random_returns = []
for _ in range(30):
    pos, vel = world.reset(rng_eval)
    r = 0.0
    for _ in range(50):
        action = rng_eval.integers(0, 4)
        pos, vel, reward, done = world.step(pos, vel, action, rng_eval)
        r += reward
    random_returns.append(r)

trained_returns = []
with torch.no_grad():
    for _ in range(30):
        pos, vel = world.reset(rng_eval)
        r = 0.0
        deter = model.initial_state(1, torch.device("cpu"))
        prev_action = torch.zeros(1, model.action_dim)
        prev_latent = torch.zeros(1, model.latent_dim)
        for _ in range(50):
            img = world.render(pos, 16)
            obs_t = torch.tensor(img, dtype=torch.float32).unsqueeze(0)
            deter, _, (q_mean, _) = model.forward_step(
                prev_action, prev_latent, deter, obs_t.view(1, -1)
            )
            latent = q_mean
            dist = actor(deter, latent)
            action = int(dist.probs.argmax(dim=-1).item())
            pos, vel, reward, done = world.step(pos, vel, action, rng_eval)
            r += reward
            prev_action = one_hot_action(
                torch.tensor([action]), model.action_dim, torch.device("cpu")
            )
            prev_latent = latent
        trained_returns.append(r)

imagined_curve = [h[3] for h in agent_history]
Out[28]:
Visualization
Line chart of measured mean imagined return over logged actor-critic update steps.
Mean lambda-return from the learned model at logged actor-critic update steps. Its direction is an imagination diagnostic, not evidence of real-environment improvement.
Out[29]:
Visualization
Box plots comparing real-environment episode returns for the random baseline and final trained greedy toy actor.
Real-environment return distributions for a random baseline and the final trained greedy toy actor. This endpoint check complements the imagined-return training curve, but real returns were not measured at each logged update step.

Together, the plots show a changing imagined lambda-return diagnostic and measured real-environment return. Neither improves in this toy run: the imagined diagnostic decreases, and the trained greedy policy performs worse than the random baseline. These measurements use different horizons, seed states, and action-selection rules, so they cannot establish a transfer failure or identify why the policy underperforms.

PlaNet used an RSSM for online planning from pixels, building on earlier recurrent latent-state models. DreamerV1 retained PlaNet's RSSM-based world model and trained an actor and critic on imagined latent trajectories. DreamerV2 combined categorical states, KL balancing, and generation-specific actor gradients for the full Atari suite and continuous control. DreamerV3 evaluated a broader package (symlog vector encoder inputs and decoder targets, symexp two-hot reward and value heads, advantage normalization, free bits, uniform mixing, architecture, replay, and optimizer choices) with one reported configuration across several benchmark suites.

The next chapter on MuZero takes a different stance. MuZero learns latent dynamics that predict reward, value, and policy targets. When selecting actions in the real environment, its Monte Carlo tree search simulates transitions with that learned model rather than querying the environment for hypothetical outcomes.

Summary

The Dreamer family replaces per-step trajectory search with an actor and value function trained on latent imagination. Actor inference avoids a fresh online search. Published DreamerV3 experiments report one configuration across several benchmark suites, but additional imagination does not guarantee monotonic improvement or real-environment transfer.

The key ideas to take away:

  • The RSSM combines recurrent memory with stochastic latent variables. It separates the prior used in imagination from the observation-conditioned posterior. Stochastic state can encode ambiguity but is not automatically calibrated uncertainty.
  • Latent imagination rolls the model forward using the prior and current policy. V1 backpropagates through dynamics with reparameterized continuous actions or straight-through discrete actions; V2 selects between dynamics and REINFORCE estimators by domain, and V3 uses REINFORCE with stopped world-model gradients. The critic uses horizon-bootstrapped lambda-returns.
  • DreamerV2 combines categorical latents, straight-through sampling, KL balancing, and domain-specific actor-gradient estimators. Its Atari result belongs to the complete system, not the latent change alone.
  • DreamerV3 uses symlog vector encoder inputs and decoder targets, symexp-spaced two-hot reward and value predictions decoded as an original-scale weighted average, normalized actor advantages, free bits, uniform mixing, and further architecture and optimization choices; images retain their convolutional path. One reported configuration was evaluated across Atari, DMLab, control, and modified MineRL tasks.
  • Limitations are real: compute comparisons are implementation-dependent, long-horizon sparse-reward exploration is not solved, imagination can diverge from reality, and semantic interpretability is not guaranteed.

The version history is qualitative rather than a set of commensurate capability scores:

VersionStochastic stateActor-gradient estimatorDocumented scope
DreamerV1Diagonal GaussianDynamics/value gradients: reparameterized continuous actions, straight-through discrete actionsImage-based continuous control; selected Atari and DMLab tests
DreamerV232 groups of 32 categoriesREINFORCE on Atari; dynamics gradients on continuous controlAtari and continuous control
DreamerV3Categorical with uniform mixingEntropy-regularized REINFORCE; world-model gradients stoppedOne reported configuration across multiple benchmark suites

The most important skill to develop from this chapter is the ability to distinguish imagined performance from real performance. Dreamer's benchmark results show that latent imagination can train strong policies, while the toy experiment shows why transfer must be measured rather than inferred from the imagined objective.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about the Dreamer family of world-model agents.

The Dreamer Family

Question 1 of 70 of 7 completed
What is the central move that distinguishes Dreamer from PlaNet-style planners?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026dreamerfamily, author = {Michael Brenndoerfer}, title = {The Dreamer Family}, year = {2026}, url = {https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-30} }
APAAcademic
Michael Brenndoerfer (2026). The Dreamer Family. Retrieved from https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models
MLAAcademic
Michael Brenndoerfer. "The Dreamer Family." 2026. Web. September 30, 2026. <https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models>.
CHICAGOAcademic
Michael Brenndoerfer. "The Dreamer Family." Accessed September 30, 2026. https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models.
HARVARDAcademic
Michael Brenndoerfer (2026) 'The Dreamer Family'. Available at: https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models (Accessed: September 30, 2026).
SimpleBasic
Michael Brenndoerfer (2026). The Dreamer Family. https://mbrenndoerfer.com/writing/dreamer-family-latent-imagination-world-models

About the author

Continue with the full handbook

This chapter is part of World Models Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore World Models Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.