MuZero and Value-Equivalent Models › Representation, Dynamics, and Prediction Networks

Michael BrenndoerferJuly 16, 202625 min read
World Models HandbookAll chapters
Part 8 of 12Decision-Centric Research Lineages
Chapter 46MuZero and Value-Equivalent Models › Representation, Dynamics, and Prediction Networks
Reading section 2 of 8 · Representation, Dynamics, and Prediction Networks · 25 minSection 2 of 8 · 25 min

Book contents

Section 2 of 8 · 25 min read

MuZero and Value-Equivalent Models › Representation, Dynamics, and Prediction NetworksOne section · Sections in this chapter2 / 8 ↓
Reading tools Definitions & inline links

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

MuZero uses a learned latent model for planning instead of querying the environment during search. It does not plan in observation space. It plans in a representation learned from reward, value, and policy targets. This is a substantial shift in perspective. In a classical state-space model, the state variables may be supplied by the problem: the position of a chess piece, the reading of a sensor. In MuZero, the latent state is learned from data, and its usefulness must be evaluated through predictions and decisions. If planning fails, training and evaluation must diagnose and improve the model; MuZero does not automatically discard and relearn the entire state space.

Formally, in a Markov decision process the agent interacts with an environment over discrete time steps t=0,1,2,…t = 0, 1, 2, \ldots. At each step the environment is in some state, the agent selects an action, and the environment returns a reward and a new state. The learned model is called latent because its state sks^k is not the environment's state; it is a vector of the network's own invention. Throughout this chapter we distinguish:

  • the observation oto_t (what the agent sees at wall-clock time tt);
  • the action ata_t (what the agent chooses at wall-clock time tt);
  • the reward utu_t (what the environment returns at wall-clock time tt);
  • the policy π\pi (a distribution over actions);
  • the transition p(st+1∣st,at)p(s_{t+1}\mid s_t, a_t) (how the environment's state evolves);
  • the observation model p(ot∣st)p(o_t\mid s_t) (how observations are generated from states);
  • the latent state sks^k and unrolled horizon kk used inside the learned model.

Keeping these separate matters, because the central confusion you may run into is mixing up the unrolled time inside the learned model with the wall-clock time of the real environment. The superscript kk counts steps taken inside the imagination of the network. The subscript tt counts steps taken in the world. In our tutorial, a single observation oto_t is encoded into the initial latent state s0s^0. Published MuZero's representation instead takes an observation-and-action history. After the root is encoded, search rolls forward through learned transitions rather than querying new environment transitions, though it may query the environment for legal actions at the root.

The three functions

Write the environment's observation at time tt as oto_t (what the agent sees), the action as ata_t (what the agent chooses), the reward as utu_t (what the environment returns), and the latent state as sks^k (the compressed summary the network invents at model step kk). The superscript kk indexes steps after the encoded root, not wall-clock time. In the training equations below, KK is the replay-supervised unroll length; search depth is a separate choice. MuZero parameterizes three functions with a shared network θ\theta (layers within each function have their own parameters, conventionally grouped as θ\theta). The representation function encodes available history into an initial latent state; our compact tutorial uses only the current observation. The dynamics function advances that latent state under an action while predicting the reward, and the prediction function reads a policy and a value out of a latent state:

s0=hθ(ot),(sk+1,rk+1)=gθ(sk,ak),(pk,vk)=fθ(sk).\begin{aligned} s^0 &= h_\theta(o_t), \\ (s^{k+1}, r^{k+1}) &= g_\theta(s^k, a^k), \\ (p^k, v^k) &= f_\theta(s^k). \end{aligned}

where:

  • oto_t: the observation the agent sees at time tt
  • hθh_\theta: the representation function, which encodes available history into an initial latent state s0s^0; the displayed hθ(ot)h_\theta(o_t) is our single-observation tutorial specialization
  • aka^k: the action taken at unrolled step kk (encoded as a one-hot or embedding)
  • gθg_\theta: the dynamics function, which maps a latent state and an action to the next latent state sk+1s^{k+1} and the reward rk+1r^{k+1} on the transition into it
  • sks^k: the latent state at unrolled step kk; the tutorial implementation below bounds it with a tanh activation
  • fθf_\theta: the prediction function, which reads a latent state sks^k and emits a policy pkp^k (a distribution over actions, used as a prior during search) and a value vkv^k (the expected discounted return from that latent state)
  • k∈{0,1,…,K}k \in \{0, 1, \ldots, K\}: the model-step index within the training unroll, not wall-clock time
  • KK: the number of model steps used for a replay-supervised training unroll; it is not the tree-search depth

Read the three equations top to bottom and the whole algorithm is visible. The first line says "look at the world and compress it." The second says "given a situation and an action, imagine the resulting situation and the reward it brings." The third says "given a situation, what would you do and how good is it?" Everything else in the chapter is a consequence of these three lines plus a search procedure.

Notice what is missing: there is no inverse dynamics function, no decoder, and no reconstruction head. The latent state sks^k is not constrained to be interpretable and is not compared to a real observation. The states produced by the dynamics function are trained to support reward, value, and policy predictions useful for planning. The model need not reproduce every observation detail, although its latent state can still retain irrelevant information.

The original MuZero dynamics function is deterministic: given a latent state and an action, it returns one next latent state rather than a sampled distribution. Our gridworld's position dynamics are deterministic, although its observation includes independent noise. A deterministic model can still predict useful expected values in a stochastic environment, but a single imagined branch does not represent the full outcome distribution. Search quality must be checked against real-environment outcomes.

Latent state

In this chapter a latent state is a vector produced by the representation or dynamics network. It is not a belief state: as discussed in Part II: Inference, Decisions, and Control, a belief state is a distribution over environment states. Our tutorial bounds its latent coordinates with tanh⁡\tanh; this is an implementation choice, not a property of all MuZero models or a guarantee of useful planning.

The training objective

Suppose we sample a trajectory from the replay buffer and pick a starting index tt. We run the network forward from s0=hθ(ot)s^0 = h_\theta(o_t), unrolling KK steps with the actual actions the agent took:

s0→  at  (s1,r1)→  at+1  (s2,r2)→  at+2  ⋯→  at+K−1  (sK,rK).\begin{aligned} s^0 &\xrightarrow{\;a_t\;} (s^1, r^1) \\ &\xrightarrow{\;a_{t+1}\;} (s^2, r^2) \\ &\xrightarrow{\;a_{t+2}\;} \cdots \\ &\xrightarrow{\;a_{t+K-1}\;} (s^K, r^K). \end{aligned}

At the root and each unrolled state we predict value vkv^k and policy pkp^k; each modeled transition predicts reward rk+1r^{k+1}. The reward target is the observed reward on that transition. The policy target πt+k\pi_{t+k} comes from search at the corresponding real state. In the original MuZero training objective, the value target zt+kz_{t+k} is the final outcome for board games or an observed nn-step return bootstrapped from a later search value for Atari. With our reward indexing, a schematic loss is:

L(θ)=Eτ∼D[∑k=1Kℓr(ut+k,rk)+∑k=0K(ℓv(zt+k,vk)+ℓp(πt+k,pk))]+c∥θ∥22\mathcal{L}(\theta) = \mathbb{E}_{\tau \sim \mathcal{D}} \left[\sum_{k=1}^{K} \ell^{r}(u_{t+k}, r^k) + \sum_{k=0}^{K} \bigl(\ell^{v}(z_{t+k}, v^k) + \ell^{p}(\pi_{t+k}, p^k)\bigr)\right] + c \lVert \theta \rVert_2^2

where:

  • τ∼D\tau \sim \mathcal{D}: a trajectory sampled from the replay buffer D\mathcal{D}, from which the starting index tt is drawn
  • L(θ)\mathcal{L}(\theta): the total training loss, minimized over the shared parameters θ\theta
  • KK: the number of model steps in this training unroll
  • ut+ku_{t+k}: the observed reward on the kkth transition after the starting state, for k=1,…,Kk=1,\ldots,K
  • rkr^k: the reward predicted by dynamics on that transition; no root reward r0r^0 is predicted
  • zt+kz_{t+k}: the outcome or nn-step bootstrapped value target appropriate to the domain
  • vkv^k: the value predicted by the prediction function at unrolled step kk
  • πt+k\pi_{t+k}: the policy target produced by the search at time t+kt+k
  • pkp^k: the policy prior predicted by the prediction function at unrolled step kk
  • ℓr\ell^{r}: the reward loss, whose form depends on the domain and target representation
  • ℓv\ell^{v}: the value loss, likewise chosen for the domain and target representation
  • ℓp\ell^{p}: the policy loss, a cross-entropy between the search visit-count distribution πt+k\pi_{t+k} and the network prior pkp^k
  • cc: the coefficient of the L2L_2 weight-regularization term
  • ∥θ∥22\lVert \theta \rVert_2^2: the squared L2L_2 norm of all parameters in the shared network θ\theta, which penalizes large weights

In published MuZero, board games use a squared-error value loss and, because those experiments have no intermediate rewards, omit the reward-prediction loss. Atari uses categorical-support cross-entropy for transformed reward and value targets. The policy loss compares search visit counts with the network prior. The tutorial below uses simple scalar mean-squared reward and value losses for ease of inspection, not the complete published objective.

The expectation is over trajectories in replay. In published MuZero, actors repeatedly generate new trajectories using MCTS; replay is refreshed, not a permanently fixed dataset. Our tutorial instead uses a fixed exploratory dataset so that the small experiment is reproducible.

Search provides policy targets at replayed states; the value target also incorporates actual outcomes or later bootstrapped search estimates. This can improve decision targets, but it does not guarantee that a model trained on them will improve, especially outside the replay distribution. Our fixed-data experiment should be read as a controlled demonstration, not as a reproduction of MuZero's self-play loop.

This objective is unusual and worth examining closely. It does not ask the latent to reconstruct the next observation or resemble the environment state. Reward, value, and policy losses instead constrain the quantities used for decisions. Many different latent encodings can satisfy those targets, and stable learning depends on data, architecture, optimization, and target quality; the losses alone do not guarantee it.

The latent consistency term, and why it matters here

The original MuZero objective supervises unrolled reward, value, and policy predictions, but does not include an explicit loss matching a predicted latent to the representation of the next observation. Correct predictions constrain the dynamics indirectly, while multiple latent representations may remain equivalent for the supervised targets.

An auxiliary consistency objective can encourage the predicted state and the encoding of the next observation to agree. It is an additional inductive bias, not a required component of MuZero or a proof that search will be reliable.

For example, EfficientZero uses a self-supervised temporal consistency component. Our toy model uses a simpler squared-distance variant. Write s~k+1\tilde s^{k+1} for the state component of gθ(sk,ak)g_\theta(s^k,a^k); then:

ℓcons=∥ s~k+1−sg⁡ ⁣(hθ(ot+k+1)) ∥22\ell^{\text{cons}} = \big\lVert\, \tilde s^{k+1} - \operatorname{sg}\!\big(h_\theta(o_{t+k+1})\big) \,\big\rVert_2^2

where:

  • ℓcons\ell^{\text{cons}}: the latent consistency loss at unrolled step kk
  • s~k+1\tilde s^{k+1}: the state component of the dynamics prediction after applying aka^k at sks^k
  • sg⁡(⋅)\operatorname{sg}(\cdot): the stop-gradient operator, which treats its argument as a constant during backpropagation
  • hθ(ot+k+1)h_\theta(o_{t+k+1}): the latent state the representation function would have produced had it seen the observation k+1k+1 steps later
  • kk: the unrolled step at which the consistency loss is computed, ranging over 0,…,K−10, \ldots, K-1

This term compares the predicted state with a stopped-gradient next-observation encoding. It is self-predictive rather than reconstructive: nothing decodes pixels. Stop-gradient blocks the direct gradient through the target occurrence of the encoder. The encoder can still change through the online branch and the reward, value, and policy losses. The squared distance is a tutorial choice, not a latent probability model.

Without a stopped target, both branches can move toward one another. Stopping one branch changes the gradient direction but does not by itself prevent representational collapse: a constant zero latent satisfies the consistency term and survives tanh⁡\tanh. Decision losses and training design must preserve useful information.

The toy experiment asks whether this combination of losses reduces the representation of four unpredictable, decision-irrelevant observation bits. We will measure the result rather than assume that the bits disappear.

Why the latent state is allowed to forget

Consider the kinds of information a world model might be asked to represent:

  • Decision-relevant, predictable. Where the agent is, where the goal is, how much health remains. When these factors affect reward or action quality, the learned model needs enough information to predict relevant rewards, values, and policy targets. It need not store each variable literally in the latent; a fixed goal, for example, can be implicit in the network parameters.
  • Decision-relevant, unpredictable. An opponent's future move in a stochastic game may affect the return, but its realized outcome is not known at the current state. The representation may need information that predicts the distribution of possible moves or their expected effect on value; a scalar value target alone does not guarantee that it identifies the full distribution.
  • Decision-irrelevant, predictable. The scrolling scoreboard animation. The dynamics can predict it, and nothing needs it. It may survive or not; nothing punishes its presence, and nothing rewards it.
  • Decision-irrelevant, unpredictable. A random distractor bit. Consistency may discourage encoding it, but finite-capacity training can still leave measurable information in the latent.

These two axes distinguish decision relevance from predictability. A decision-relevant but unpredictable variable may call for uncertainty-aware values; a predictable but irrelevant variable need not be retained. An unpredictable irrelevant variable is a plausible candidate to discard, not something the objective is guaranteed to remove.

A reconstruction loss can reward encoding distractor detail, depending on its weighting and the model's capacity. MuZero's original decision-target objective lacks that direct incentive, but it can still retain distractors incidentally. The later probe measures how much remains in this particular learned latent.

Let us now build the smallest possible environment in which this effect is visible.

In[3]:
Code
import copy

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

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

We use a 4×44 \times 4 gridworld. The agent starts somewhere other than the goal, moves in one of four directions with wall clamping, and the goal cell is absorbing. Reaching the goal yields a reward of 1; every other transition yields 0. Episodes run for a fixed horizon of 20 steps, so there is no termination signal to model. The choice of a tiny gridworld is deliberate: it is small enough that we can compute the exact optimal value function and compare against it, and large enough that a search of depth four cannot trivially exhaust it. You should think of it as a magnifying glass under which the mechanism of MuZero becomes visible in a way it never is on Atari.

The observation is deliberately redundant: a 16-dimensional one-hot of the agent's position, concatenated with 4 bits of pure uniform noise resampled independently at every step. The noise is unpredictable and decision-irrelevant. A model that plans well has no reason to represent it. The four noise channels play the role of the blinking cursor from the opening paragraph. They are always present, always random, and never useful. If a model wastes latent capacity on them, you will be able to see it directly.

In[4]:
Code
N = 4  # grid side length
N_STATES = N * N  # 16 cells
N_ACTIONS = 4  # up, right, down, left
GOAL = (0, 0)
HORIZON = 20
GAMMA = 0.95
N_DISTRACT = 4  # pure-noise observation channels
OBS_DIM = N_STATES + N_DISTRACT
LATENT_DIM = 8


def one_hot_position(pos):
    v = np.zeros(N_STATES, dtype=np.float32)
    v[pos[0] * N + pos[1]] = 1.0
    return v


def observe(pos, rng):
    """Position one-hot plus independent uniform noise bits."""
    noise = rng.integers(0, 2, size=N_DISTRACT).astype(np.float32)
    return np.concatenate([one_hot_position(pos), noise])


def transition(pos, action):
    """Advance one grid step and reward first entry into the absorbing goal."""
    if pos == GOAL:
        return GOAL, 0.0
    row, col = pos
    if action == 0:
        row = max(row - 1, 0)
    elif action == 1:
        col = min(col + 1, N - 1)
    elif action == 2:
        row = min(row + 1, N - 1)
    else:
        col = max(col - 1, 0)
    next_pos = (row, col)
    return next_pos, (1.0 if next_pos == GOAL else 0.0)

The transition function used everywhere below is the deterministic gridworld step: it moves the agent in the requested direction if the destination is inside the grid, otherwise it stays put, and it returns a reward of 1 exactly when the agent reaches the goal. Because this function is a plain Python routine, we can use it both to generate the dataset and to compute the exact optimum by value iteration, which gives us a ground truth against which every learned quantity can be measured.

We collect data with a behaviour policy that, at non-goal cells, with probability 0.6 takes a step reducing Manhattan distance to the goal (breaking ties randomly) and otherwise acts uniformly at random. At the absorbing goal it acts uniformly. Its empirical returns leave a gap to the exact optimal values. That gap is an opportunity for search, not evidence that this particular learned-model search will close it.

In[5]:
Code
def behaviour_action(pos, rng, bias=0.6):
    if pos != GOAL and rng.random() < bias:
        candidates = []
        for a in range(N_ACTIONS):
            nxt, _ = transition(pos, a)
            d = abs(nxt[0] - GOAL[0]) + abs(nxt[1] - GOAL[1])
            candidates.append((d, a))
        best_d = min(d for d, _ in candidates)
        return int(rng.choice([a for d, a in candidates if d == best_d]))
    return int(rng.integers(N_ACTIONS))


def collect_dataset(n_episodes, rng, bias=0.6):
    obs, nxt, act, rew, pos, lengths = [], [], [], [], [], []
    for _ in range(n_episodes):
        p = (int(rng.integers(N)), int(rng.integers(N)))
        while p == GOAL:
            p = (int(rng.integers(N)), int(rng.integers(N)))
        start = len(obs)
        current_obs = observe(p, rng)
        for _ in range(HORIZON):
            a = behaviour_action(p, rng, bias)
            p2, r = transition(p, a)
            next_obs = observe(p2, rng)
            obs.append(current_obs)
            act.append(a)
            rew.append(r)
            nxt.append(next_obs)
            pos.append(p)
            p = p2
            current_obs = next_obs
        lengths.append(len(obs) - start)
    return (
        np.asarray(obs, np.float32),
        np.asarray(act, np.int64),
        np.asarray(rew, np.float32),
        np.asarray(nxt, np.float32),
        np.asarray(pos, np.int64),
        lengths,
    )


rng = np.random.default_rng(SEED)
OBS, ACT, REW, NXT, POS, LENGTHS = collect_dataset(150, rng)
offset = 0
for episode_length in LENGTHS:
    assert np.array_equal(
        NXT[offset : offset + episode_length - 1],
        OBS[offset + 1 : offset + episode_length],
    )
    offset += episode_length
assert offset == len(OBS)

Because the grid is small and the dynamics are deterministic, we can compute the exact optimal value function by value iteration. That gives us a reference for measuring the learned model and planner. This reference is not the definition of value equivalence: Grimm et al. compare models by selected Bellman updates, which need not guarantee agreement on every optimal value. Value iteration repeatedly applies the Bellman optimality operator. The code below runs 300 sweeps, but from a zero initialization the reward signal reaches the far corner by sweep six in this deterministic grid. The result is the best possible discounted return from every cell.

In[6]:
Code
def monte_carlo_returns(rewards, lengths, gamma):
    out = np.zeros_like(rewards)
    i = 0
    for L in lengths:
        g = 0.0
        for t in range(L - 1, -1, -1):
            g = rewards[i + t] + gamma * g
            out[i + t] = g
        i += L
    return out


MC_RETURNS = monte_carlo_returns(REW, LENGTHS, GAMMA)


def value_iteration(n_sweeps=300):
    V = np.zeros(N_STATES)
    for _ in range(n_sweeps):
        V_new = V.copy()
        for s in range(N_STATES):
            p = (s // N, s % N)
            if p == GOAL:
                V_new[s] = 0.0
                continue
            qs = []
            for a in range(N_ACTIONS):
                p2, r = transition(p, a)
                qs.append(r + GAMMA * V[p2[0] * N + p2[1]])
            V_new[s] = max(qs)
        V = V_new
    return V


def optimal_q(V):
    Q = np.zeros((N_STATES, N_ACTIONS))
    for s in range(N_STATES):
        p = (s // N, s % N)
        for a in range(N_ACTIONS):
            p2, r = transition(p, a)
            Q[s, a] = r + GAMMA * V[p2[0] * N + p2[1]]
    return Q


V_STAR = value_iteration()
Q_STAR = optimal_q(V_STAR)

STATE_IDS = POS[:, 0] * N + POS[:, 1]
BEHAVIOUR_VALUES = np.array(
    [
        MC_RETURNS[STATE_IDS == s].mean() if np.any(STATE_IDS == s) else 0.0
        for s in range(N_STATES)
    ]
)
Out[7]:
Console
Transitions collected: 3000
Observation dimension: 20 (16 position + 4 noise)
Mean logged-transition return-to-go under behaviour: 0.224
Optimal value at the furthest cell from the goal: 0.774
Behaviour value at that same cell: 0.700

The empirical behaviour value at the far corner is below the exact optimum. The undiscounted return-to-go is the sum of subsequent rewards; discounting with γ=0.95\gamma = 0.95 weights earlier rewards more heavily. Let ss denote an environment state, rt+1r_{t+1} the reward received after moving from sts_t, and π\pi the behaviour policy. The expected discounted return is

Vπ(s)=Eπ ⁣[∑i=0∞γi rt+i+1 ∣ st=s]V^\pi(s) = \mathbb{E}_\pi\!\left[\sum_{i=0}^{\infty} \gamma^{i}\, r_{t+i+1} \,\Big|\, s_t = s\right]

where:

  • Vπ(s)V^\pi(s): the expected discounted return when starting in state ss and following π\pi thereafter
  • Eπ[⋅]\mathbb{E}_\pi[\cdot]: the expectation over actions drawn from π\pi and over the environment's transitions
  • rt+i+1r_{t+i+1}: the reward received at step t+i+1t+i+1
  • γ∈[0,1)\gamma \in [0, 1): the discount factor, weighting rewards ii steps away by γi\gamma^i

The Monte Carlo returns estimate this quantity from sampled 20-step episodes, with any reward beyond the cutoff set to zero. They are therefore truncated targets, not unbiased infinite-horizon VπV^\pi targets for late-episode transitions that have not reached the goal. The V⋆V^\star reference is infinite-horizon, so RMSE gaps against these empirical returns combine policy quality with truncation and sampling error. Detours delay the goal reward and lower its discounted contribution. We will compare later searches and target changes against this baseline, without assuming they remove the gap.

Now the network. It is deliberately tiny: hidden layers of width 64, an 8-dimensional latent state, and tanh⁡\tanh squashing on the encoder and on the state component of dynamics. The squash bounds magnitudes; it does not prevent collapse, because the zero vector is still in its range. Reward, value, and policy supervision are needed to keep the representation useful.

In[8]:
Code
class MuZeroNet(nn.Module):
    def __init__(
        self,
        obs_dim=OBS_DIM,
        n_actions=N_ACTIONS,
        latent_dim=LATENT_DIM,
        hidden=64,
    ):
        super().__init__()
        self.latent_dim = latent_dim
        self.n_actions = n_actions
        self.encoder = nn.Sequential(
            nn.Linear(obs_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, latent_dim),
        )
        self.dynamics = nn.Sequential(
            nn.Linear(latent_dim + n_actions, hidden),
            nn.ReLU(),
            nn.Linear(hidden, latent_dim + 1),
        )
        self.prediction = nn.Sequential(
            nn.Linear(latent_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, n_actions + 1),
        )

    def encode(self, obs):
        return torch.tanh(self.encoder(obs))

    def next_state(self, z, action_onehot):
        out = self.dynamics(torch.cat([z, action_onehot], dim=-1))
        z_next = torch.tanh(out[..., : self.latent_dim])
        reward = out[..., self.latent_dim]
        return z_next, reward

    def predict(self, z):
        out = self.prediction(z)
        return out[..., : self.n_actions], out[..., self.n_actions]

Notice how the three heads map onto the three lines of the MuZero equations. The encode method implements hθh_\theta, the next_state method implements gθg_\theta and splits its output into a latent half and a scalar reward half, and the predict method implements fθf_\theta and splits its output into action logits and a single value. Keeping this correspondence explicit makes the code much easier to read than if the heads were scattered across separate modules.

The training loop uses torch.no_grad() for the target branch of the auxiliary consistency loss. This blocks direct gradients through that occurrence of the encoder, but the encoder remains trainable through the online state and other losses. The loop is a one-step decision-target surrogate: it supervises the root value with behavior-policy Monte Carlo returns, the root policy with sampled behavior actions, and one predicted reward. It does not implement MuZero's multi-step unroll or search-generated policy targets.

In[9]:
Code
def train_model(
    model,
    obs,
    act,
    rew,
    nxt,
    value_target,
    policy_target,
    epochs=200,
    batch_size=256,
    lr=1e-3,
    seed=0,
    use_consistency=True,
    use_reconstruction=False,
):
    torch.manual_seed(seed)
    opt = torch.optim.Adam(model.parameters(), lr=lr)

    obs_t = torch.tensor(obs)
    nxt_t = torch.tensor(nxt)
    act_t = torch.tensor(act)
    rew_t = torch.tensor(rew)
    val_t = torch.tensor(np.asarray(value_target, dtype=np.float32))
    pol_t = torch.tensor(np.asarray(policy_target, dtype=np.float32))

    n = obs_t.shape[0]
    history = {
        k: []
        for k in (
            "total",
            "value",
            "reward",
            "consistency",
            "policy",
            "reconstruction",
        )
    }

    for _ in range(epochs):
        perm = torch.randperm(n)
        sums = np.zeros(6)
        n_batches = 0
        for i in range(0, n, batch_size):
            idx = perm[i : i + batch_size]
            z = model.encode(obs_t[idx])
            logits, v = model.predict(z)
            z_next, r_hat = model.next_state(
                z, F.one_hot(act_t[idx], N_ACTIONS).float()
            )

            with torch.no_grad():
                z_target = model.encode(nxt_t[idx])

            value_loss = F.mse_loss(v, val_t[idx])
            reward_loss = F.mse_loss(r_hat, rew_t[idx])
            consistency_loss = F.mse_loss(z_next, z_target)
            policy_loss = (
                -(pol_t[idx] * F.log_softmax(logits, dim=-1)).sum(-1).mean()
            )
            recon_loss = torch.tensor(0.0)
            if use_reconstruction:
                recon_loss = F.mse_loss(model.decode(z), obs_t[idx])

            loss = value_loss + reward_loss + policy_loss
            if use_consistency:
                loss = loss + consistency_loss
            if use_reconstruction:
                loss = loss + recon_loss

            opt.zero_grad()
            loss.backward()
            opt.step()

            sums += np.array(
                [
                    loss.item(),
                    value_loss.item(),
                    reward_loss.item(),
                    consistency_loss.item(),
                    policy_loss.item(),
                    recon_loss.item(),
                ]
            )
            n_batches += 1

        sums /= n_batches
        for j, key in enumerate(
            (
                "total",
                "value",
                "reward",
                "consistency",
                "policy",
                "reconstruction",
            )
        ):
            history[key].append(sums[j])
    return history


VE_MODEL = MuZeroNet()
BEHAVIOUR_POLICY = np.eye(N_ACTIONS, dtype=np.float32)[ACT]
hist_ve = train_model(
    VE_MODEL, OBS, ACT, REW, NXT, MC_RETURNS, BEHAVIOUR_POLICY, epochs=200
)
Out[10]:
Visualization
Log-scale line chart: reward and value losses decline, policy loss stays near 1.23, and latent-consistency loss varies.
Training losses for the one-step decision-target model over 200 epochs. Reward and value losses decrease; policy loss plateaus near 1.23 and latent consistency is nonmonotone. The objective contains no observation-reconstruction term.

The reward and value losses decrease, while the policy loss plateaus and the consistency loss varies and rises slightly late in training. A one-hot sampled action is a noisy target for the policy head; its plateau does not establish the entropy of the behavior policy. This plot alone cannot show whether the latent has discarded distractor bits; the probe below tests that separately.

The consistency loss can be inspected as a distribution rather than as a single scalar. If the dynamics has learned to match the encoder, the per-transition squared error should concentrate at a small value across the dataset.

In[11]:
Code
with torch.no_grad():
    z_online = VE_MODEL.encode(torch.tensor(OBS))
    z_next_pred, _ = VE_MODEL.next_state(
        z_online, F.one_hot(torch.tensor(ACT), N_ACTIONS).float()
    )
    z_next_target = VE_MODEL.encode(torch.tensor(NXT))
    consistency_errors = (
        ((z_next_pred - z_next_target) ** 2).sum(dim=-1).numpy()
    )

UNTRAINED_MODEL = MuZeroNet()
with torch.no_grad():
    z_online_untrained = UNTRAINED_MODEL.encode(torch.tensor(OBS))
    z_next_untrained, _ = UNTRAINED_MODEL.next_state(
        z_online_untrained, F.one_hot(torch.tensor(ACT), N_ACTIONS).float()
    )
    z_target_untrained = UNTRAINED_MODEL.encode(torch.tensor(NXT))
    consistency_errors_untrained = (
        ((z_next_untrained - z_target_untrained) ** 2).sum(dim=-1).numpy()
    )
Out[12]:
Visualization
Overlaid histograms compare trained and untrained latent-prediction errors, with substantial overlap.
Per-transition squared latent-consistency errors for the trained model and one coherent untrained model. This is an error in learned coordinates, not raw observation space; raw distractor variance is not a lower bound.

We evaluate the value head by encoding several noisy observations of each cell and averaging the predictions. Small within-cell spread would indicate that this head is relatively insensitive to the four distractor bits. That is a useful diagnostic, but it does not establish formal value equivalence or prove that the full latent is noise-invariant.

In[13]:
Code
@torch.no_grad()
def predict_values_per_state(model, n_noise=8, seed=0):
    rng_local = np.random.default_rng(seed)
    obs_list, state_ids = [], []
    for s in range(N_STATES):
        p = (s // N, s % N)
        for _ in range(n_noise):
            obs_list.append(observe(p, rng_local))
            state_ids.append(s)
    stacked = torch.tensor(np.stack(obs_list))
    z = model.encode(stacked)
    _, v = model.predict(z)
    v = v.numpy()
    state_ids = np.array(state_ids)
    means = np.array([v[state_ids == s].mean() for s in range(N_STATES)])
    stds = np.array([v[state_ids == s].std() for s in range(N_STATES)])
    return means, stds


def value_rmse(model, n_noise=8, seed=0):
    means, _ = predict_values_per_state(model, n_noise=n_noise, seed=seed)
    return float(np.sqrt(np.mean((means - V_STAR) ** 2)))


MODEL_VALUES, MODEL_VALUE_STD = predict_values_per_state(VE_MODEL)
ERR_VE = value_rmse(VE_MODEL)
ERR_BEHAVIOUR = float(np.sqrt(np.mean((BEHAVIOUR_VALUES - V_STAR) ** 2)))
Out[14]:
Console
Decision-target model value RMSE: 0.104
Empirical behaviour-value RMSE: 0.105
Out[15]:
Visualization
Grouped bar chart comparing optimal, behaviour-policy, and learned model values for 16 grid cells.
Value predictions by grid cell, sorted by optimal value. On this run, the learned model's RMSE against infinite-horizon optimal values is marginally lower than the empirical 20-step behaviour-return baseline's; this comparison includes truncation and sampling effects.

The learned value head is trained against behavior-policy Monte Carlo returns. Its RMSE of 0.104 is only marginally below the empirical behavior baseline's 0.105 in this run; the difference does not establish a robust improvement. There is no multi-step unrolled loss in the tutorial training loop, so attributing value behavior to such a loss would be unsupported. Search may use the learned reward and dynamics predictions to change action rankings, which we test next.

The gap to the optimal curve gives room for search or revised targets to help, but their benefit must be measured; neither is guaranteed by the architecture.