MuZero and Value-Equivalent Models › Representation, Dynamics, and Prediction Networks
World Models HandbookAll chapters 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 . 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 is not the environment's state; it is a vector of the network's own invention. Throughout this chapter we distinguish:
- the observation (what the agent sees at wall-clock time );
- the action (what the agent chooses at wall-clock time );
- the reward (what the environment returns at wall-clock time );
- the policy (a distribution over actions);
- the transition (how the environment's state evolves);
- the observation model (how observations are generated from states);
- the latent state and unrolled horizon 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 counts steps taken inside the imagination of the network. The subscript counts steps taken in the world. In our tutorial, a single observation is encoded into the initial latent state . 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 as (what the agent sees), the action as (what the agent chooses), the reward as (what the environment returns), and the latent state as (the compressed summary the network invents at model step ). The superscript indexes steps after the encoded root, not wall-clock time. In the training equations below, is the replay-supervised unroll length; search depth is a separate choice. MuZero parameterizes three functions with a shared network (layers within each function have their own parameters, conventionally grouped as ). 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:
where:
- : the observation the agent sees at time
- : the representation function, which encodes available history into an initial latent state ; the displayed is our single-observation tutorial specialization
- : the action taken at unrolled step (encoded as a one-hot or embedding)
- : the dynamics function, which maps a latent state and an action to the next latent state and the reward on the transition into it
- : the latent state at unrolled step ; the tutorial implementation below bounds it with a tanh activation
- : the prediction function, which reads a latent state and emits a policy (a distribution over actions, used as a prior during search) and a value (the expected discounted return from that latent state)
- : the model-step index within the training unroll, not wall-clock time
- : 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 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.
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 ; 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 . We run the network forward from , unrolling steps with the actual actions the agent took:
At the root and each unrolled state we predict value and policy ; each modeled transition predicts reward . The reward target is the observed reward on that transition. The policy target comes from search at the corresponding real state. In the original MuZero training objective, the value target is the final outcome for board games or an observed -step return bootstrapped from a later search value for Atari. With our reward indexing, a schematic loss is:
where:
- : a trajectory sampled from the replay buffer , from which the starting index is drawn
- : the total training loss, minimized over the shared parameters
- : the number of model steps in this training unroll
- : the observed reward on the th transition after the starting state, for
- : the reward predicted by dynamics on that transition; no root reward is predicted
- : the outcome or -step bootstrapped value target appropriate to the domain
- : the value predicted by the prediction function at unrolled step
- : the policy target produced by the search at time
- : the policy prior predicted by the prediction function at unrolled step
- : the reward loss, whose form depends on the domain and target representation
- : the value loss, likewise chosen for the domain and target representation
- : the policy loss, a cross-entropy between the search visit-count distribution and the network prior
- : the coefficient of the weight-regularization term
- : the squared norm of all parameters in the shared network , 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 for the state component of ; then:
where:
- : the latent consistency loss at unrolled step
- : the state component of the dynamics prediction after applying at
- : the stop-gradient operator, which treats its argument as a constant during backpropagation
- : the latent state the representation function would have produced had it seen the observation steps later
- : the unrolled step at which the consistency loss is computed, ranging over
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 . 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.
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 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.
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.
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.
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)
]
)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 weights earlier rewards more heavily. Let denote an environment state, the reward received after moving from , and the behaviour policy. The expected discounted return is
where:
- : the expected discounted return when starting in state and following thereafter
- : the expectation over actions drawn from and over the environment's transitions
- : the reward received at step
- : the discount factor, weighting rewards steps away by
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 targets for late-episode transitions that have not reached the goal. The 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 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.
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 , the next_state method implements and splits its output into a latent half and a scalar reward half, and the predict method implements 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.
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
)
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.
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()
)
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.
@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)))Decision-target model value RMSE: 0.104 Empirical behaviour-value RMSE: 0.105

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.