Frontier Questions and Research Roadmaps › Experiment 1: Prediction Fidelity Versus Decision Usefulness

Michael BrenndoerferAugust 11, 202615 min read
World Models HandbookAll chapters
Part 12 of 12Reliable World Models
Chapter 72Frontier Questions and Research Roadmaps › Experiment 1: Prediction Fidelity Versus Decision Usefulness
Reading section 5 of 11 · Experiment 1: Prediction Fidelity Versus Decision Usefulness · 15 minSection 5 of 11 · 15 min

Book contents

Section 5 of 11 · 15 min read

Frontier Questions and Research Roadmaps › Experiment 1: Prediction Fidelity Versus Decision UsefulnessOne section · Sections in this chapter5 / 11 ↓
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

An aggregate observation metric can rank two models in the opposite order from their usefulness for a particular control task. We construct an example, not a general ranking of world-model architectures. All learned models receive the same transitions and actions. The state-focused model additionally receives the declared task-relevant coordinate set, so this is an information-abstraction comparison rather than a parameter-matched architecture benchmark.

The observation contains a small controlled state and a large, persistent background. A model can devote its limited latent capacity to reconstructing that background, reduce observation error, and still omit the coordinates the controller needs. We measure the variance hierarchy, per-coordinate prediction errors, and real-simulator control costs rather than assuming that the intended mechanism occurred.

A Stable Controlled Process and an Autonomous Background

The decision-relevant state is st=(xt,vt)∈R2s_t=(x_t,v_t)\in\mathbb R^2. Both coordinates and the scalar action use dimensionless units. The environment evolves as

xt+1=0.85xt+0.2vt,vt+1=0.8vt+0.2at−0.01vt∣vt∣,at∈[−1,1].\begin{aligned} x_{t+1} &= 0.85x_t+0.2v_t,\\ v_{t+1} &= 0.8v_t+0.2a_t-0.01v_t|v_t|,\qquad a_t\in[-1,1]. \end{aligned}

Here xtx_t is the positioning coordinate, vtv_t is a motion coordinate, and ata_t is a normalized command. This is a discrete synthetic law, not an Euler approximation to a specified physical robot. The decay terms prevent the unbounded random walk that a long randomly accelerated free particle would exhibit. Starting from ∣x0∣≤1.5|x_0|\leq1.5 and ∣v0∣≤1|v_0|\leq1, the rectangle ∣x∣≤1.5|x|\leq1.5, ∣v∣≤1|v|\leq1 is invariant: the first update has magnitude at most 0.85(1.5)+0.2=1.4750.85(1.5)+0.2=1.475, and the monotone velocity map on [−1,1][-1,1] has magnitude at most 0.8+0.2−0.01=0.990.8+0.2-0.01=0.99. The small drag term makes the learned linear predictors approximations rather than exact copies of the plant.

An autonomous nuisance state nt∈R2n_t\in\mathbb R^2 follows

nt+1=0.95nt+1−0.952 ϵt,ϵt∼N(0,I2).n_{t+1}=0.95n_t+\sqrt{1-0.95^2}\,\epsilon_t,\qquad \epsilon_t\sim\mathcal N(0,I_2).

The initial n0∼N(0,I2)n_0\sim\mathcal N(0,I_2) makes this process stationary in distribution, with unit marginal variance. Its eight observed channels are dt=20ntWd_t=20n_tW, where W∈R2×8W\in\mathbb R^{2\times8} has its first row equal to one in the first four columns and zero elsewhere, and its second row equal to one in the final four columns. Each nuisance channel therefore has marginal standard deviation 20, but the eight channels have rank two and are not independent observations. This deliberately redundant background strongly influences unnormalized observation loss. Its transition law contains neither the controlled state, action nor goal. That autonomy does not imply statistical independence from chosen actions: a learned feedback controller can use nuisance observations and thereby couple its commands, and subsequent controlled states, to the background.

The observation is ot=[xt,vt,dt]∈R10o_t=[x_t,v_t,d_t]\in\mathbb R^{10}. Its coordinates have different meanings and scales; averaging their squared errors embeds a measurement choice. The task is to approach an episode-specific target x⋆∈[−0.8,0.8]x^\star\in[-0.8,0.8]. Nuisance channels do not appear in the task cost.

In[7]:
Code
import numpy as np

AR_N = 0.95
INNOV_N = np.sqrt(1.0 - AR_N**2)
NUISANCE_STD = 20.0
W_NUISANCE = np.zeros((2, 8))
W_NUISANCE[0, :4] = 1.0
W_NUISANCE[1, 4:] = 1.0


def true_step(x, v, action):
    return 0.85 * x + 0.2 * v, 0.8 * v + 0.2 * action - 0.01 * v * np.abs(v)


def nuisance_obs(n):
    return NUISANCE_STD * (n @ W_NUISANCE)


def nuisance_step(n, rng):
    return AR_N * n + INNOV_N * rng.normal(size=2)

The eight observation channels cannot reveal future nuisance innovations. Even the analytic conditional-mean reference must predict their mean, not substitute realized future values. That restriction will keep the reference causal.

Independent Trajectories and Declared Splits

We collect one hundred training trajectories, each with forty random-action transitions and an independent reset. The test set uses a different seed and a fixed task-like behavior policy. Every complete trajectory belongs to only one split. We concatenate transition pairs, not adjacent rows across resets, so the last state of one episode never becomes the first state of another.

In[8]:
Code
def collect_transitions(n_episodes, n_steps, seed, task_like=False):
    rng = np.random.default_rng(seed)
    previous, following, actions = [], [], []
    for _ in range(n_episodes):
        x, v = rng.uniform(-1.5, 1.5), rng.uniform(-1.0, 1.0)
        n = rng.normal(size=2)
        target = rng.uniform(-0.8, 0.8)
        for _ in range(n_steps):
            obs = np.concatenate(([x, v], nuisance_obs(n)))
            action = (
                1.8 * (target - x) - 1.2 * v + 0.2 * rng.normal()
                if task_like
                else rng.uniform(-1.0, 1.0)
            )
            action = float(np.clip(action, -1.0, 1.0))
            x, v = true_step(x, v, action)
            n = nuisance_step(n, rng)
            previous.append(obs)
            following.append(np.concatenate(([x, v], nuisance_obs(n))))
            actions.append(action)
    return np.array(previous), np.array(following), np.array(actions)


train_prev, train_next, act_train = collect_transitions(100, 40, seed=1)
test_prev, test_next, act_test = collect_transitions(
    100, 40, seed=2, task_like=True
)
train_std = train_prev.std(axis=0)
variance_ratio = float(np.mean(train_std[2:]) / np.mean(train_std[:2]))
Out[9]:
Console
Train/test transition shapes: (4000, 10), (4000, 10)
Relevant channel standard deviations: [0.33752297 0.23911176]
Nuisance channel standard deviations: [19.3815531  19.3815531  19.3815531  19.3815531  19.47159052 19.47159052
 19.47159052 19.47159052]
Mean nuisance/relevant standard-deviation ratio: 67.4
Maximum training |x| and |v|: [1.48468296 0.99669796]

These diagnostics describe the actual finite sample. The analytic standard deviation of 20 is a population quantity, not a claim that a sampled trajectory has exactly that standard deviation. The stable controlled process keeps the relevant coordinates bounded without clipping them at a hidden threshold. Random actions provide variation over the allowed command interval; they do not establish coverage of every state-action pair.

Three Learned Models and a Conditional-Mean Reference

The observation models obtain a PCA basis from training inputs and fit a linear action-conditioned transition in that basis. We compare two and four components. The four-dimensional observation subspace contains both relevant coordinates and the two nuisance dimensions. The state-focused model fits only the first two coordinates and fills unmodeled background predictions with the training mean.

These models differ in what they represent and in parameter count. The state-focused model knows which coordinates the task cost uses. PCA knows neither the cost nor that coordinate assignment. The comparison tests the mismatch between an unweighted reconstruction objective and this declared task, not whether supervised coordinate selection is automatically available for a real robot.

In[10]:
Code
mean_o = train_prev.mean(axis=0)
_, singular_values, Vt = np.linalg.svd(train_prev - mean_o, full_matrices=False)


def fit_latent_model(k):
    P = Vt[:k].T
    current = (train_prev - mean_o) @ P
    following = (train_next - mean_o) @ P
    features = np.column_stack([current, act_train, np.ones(len(act_train))])
    coef, *_ = np.linalg.lstsq(features, following, rcond=None)
    return {"P": P, "A": coef[:k].T, "B": coef[k], "c": coef[k + 1]}


def fit_state_model():
    features = np.column_stack(
        [train_prev[:, :2], act_train, np.ones(len(act_train))]
    )
    coef, *_ = np.linalg.lstsq(features, train_next[:, :2], rcond=None)
    return {"A": coef[:2].T, "B": coef[2], "c": coef[3]}


m_obs2, m_obs4, m_state = (
    fit_latent_model(2),
    fit_latent_model(4),
    fit_state_model(),
)
relevant_loading = float(np.linalg.norm(m_obs2["P"][:2]))
Out[11]:
Console
Two-component basis loading on relevant coordinates: 0.00021
Two/four-component action coefficient norms: 0.10790, 0.22639
State-model action coefficient: [3.68779459e-17 2.00045596e-01]

A small relevant-coordinate loading supports the intended compression mechanism in this sample. It need not be exactly zero: finite-sample correlations can leak action-related information into the nuisance-dominated basis. A nonzero action coefficient alone does not establish useful action effects; we also measure predictions and control outcomes.

For a latent dimension kk, the fitted matrices have shapes P∈R10×kP\in\mathbb R^{10\times k}, A∈Rk×kA\in\mathbb R^{k\times k}, B,c∈RkB,c\in\mathbb R^k. A batch of NN latent rows is advanced as LA⊤+aB⊤+cLA^\top+aB^\top+c, giving N×kN\times k predictions. The state model has k=2k=2 and receives state rows directly.

One-Step Error Without Future Leakage

All predictors receive the current observation and action. The analytic reference uses the true controlled law and the nuisance conditional mean 0.95dt0.95d_t. It does not observe the next nuisance innovation. Its aggregate error is therefore positive (the nuisance channels contribute the residual variance of the process) even though its prediction of the noiseless relevant transition is exact.

In[12]:
Code
def latent_prediction(model, observations, actions):
    latent = (observations - mean_o) @ model["P"]
    following = (
        latent @ model["A"].T + np.outer(actions, model["B"]) + model["c"]
    )
    return mean_o + following @ model["P"].T


def state_prediction(model, observations, actions):
    following = (
        observations[:, :2] @ model["A"].T
        + np.outer(actions, model["B"])
        + model["c"]
    )
    background = np.tile(mean_o[2:], (len(observations), 1))
    return np.column_stack([following, background])


def oracle_prediction(observations, actions):
    x, v = true_step(observations[:, 0], observations[:, 1], actions)
    return np.column_stack([x, v, AR_N * observations[:, 2:]])


predictions = {
    "M_obs (2 comps)": latent_prediction(m_obs2, test_prev, act_test),
    "M_obs (4 comps)": latent_prediction(m_obs4, test_prev, act_test),
    "M_state (relevant)": state_prediction(m_state, test_prev, act_test),
    "Oracle": oracle_prediction(test_prev, act_test),
}
per_channel = {
    name: np.mean((prediction - test_next) ** 2, axis=0)
    for name, prediction in predictions.items()
}
agg_mse = {name: float(error.mean()) for name, error in per_channel.items()}
relevant_rmse = {
    name: float(np.sqrt(error[:2].mean()))
    for name, error in per_channel.items()
}
metric_names = list(predictions)
metric_values = np.array([agg_mse[name] for name in metric_names])
relevant_values = np.array([relevant_rmse[name] for name in metric_names])
Out[13]:
Console
model                    aggregate MSE   relevant RMSE
M_obs (2 comps)                 31.793          0.2638
M_obs (4 comps)                 31.844          0.0004
M_state (relevant)             342.107          0.0004
Oracle                          31.707          0.0000

The state-focused model's background mean is an explicit output-completion rule, not an optimal nuisance predictor. A learned nuisance predictor could change its aggregate error and ranking without changing its controller. The direction and size of that change would need measurement. The relevant-coordinate RMSE and controller cost therefore answer questions that the aggregate score alone cannot.

Out[14]:
Visualization
Aggregate one-step error for three learned models and a causal analytic reference.
One-step error averaged over all ten observation channels. Learned models use the same training transitions; the analytic reference predicts conditional means without future innovations. Background scale and the state-focused model's mean completion rule influence this metric.
Out[15]:
Visualization
One-step RMSE on the two task-relevant coordinates for the four predictors.
One-step RMSE restricted to the two coordinates used by the positioning cost. The reduced-coordinate metric checks whether the compressed representation retained the controlled state; it does not by itself establish closed-loop competence.

The observed comparison is an existence demonstration. In particular, the state-focused model's mean-completion rule produces a far larger aggregate observation error than the two-component observation model (because the mean-completion residuals on the large-scale nuisance channels dominate), even though the state-focused model has the smaller relevant-coordinate error. This shows that an aggregate score dominated by decision-irrelevant channels and a task-relevant score can rank the models oppositely. The four-component model is a capacity ablation, not a failure that must be manufactured. Retaining the controlled subspace can improve both prediction and control.

Closed-Loop Evaluation with Matched Candidate Actions

All model-based controllers use random-shooting MPC with K=200K=200 sequences of H=8H=8 actions. They minimize the same predicted cost,

J=∑h=0H−1[(x^h+1−x⋆)2+0.05v^h+1 2+0.01ah2].J=\sum_{h=0}^{H-1}\left[(\hat{x}_{h+1}-x^\star)^2+0.05\hat{v}_{h+1}^{\,2}+0.01a_h^2\right].

Here x^h+1,v^h+1\hat{x}_{h+1},\hat{v}_{h+1} are the model states after candidate action aha_h, and x⋆x^\star is the known episode goal. Each controller executes only the first action and replans from the next observation. The real simulator measures cost after that action, using the same coordinate weights. This keeps the prediction horizon's indexing consistent with evaluation.

A model-free PD controller provides a baseline. Its gains are tuned on separate simulator episodes, and the final comparison uses held-out episodes. The analytic MPC controller has the true dynamics, but finite-horizon random shooting is not an optimal-control solver; its cost is a reference, not a universal lower bound. Candidate action arrays and environmental resets use the same seeds across model-based controllers. The prediction model is the changed component; earlier selected actions can then produce different observations and subsequent decisions.

In[16]:
Code
def make_linear_step(model, latent):
    def step(states, actions):
        following = (
            states @ model["A"].T + np.outer(actions, model["B"]) + model["c"]
        )
        decoded = mean_o + following @ model["P"].T if latent else following
        return following, decoded[:, 0], decoded[:, 1]

    return step


def oracle_batch_step(states, actions):
    x, v = true_step(states[:, 0], states[:, 1], actions)
    return np.column_stack([x, v]), x, v


def plan(step_fn, initial, target, rng, K=200, H=8):
    actions = rng.uniform(-1.0, 1.0, size=(K, H))
    states = np.repeat(np.asarray(initial)[None, :], K, axis=0)
    costs = np.zeros(K)
    for h in range(H):
        states, x, v = step_fn(states, actions[:, h])
        costs += (x - target) ** 2 + 0.05 * v**2 + 0.01 * actions[:, h] ** 2
    return float(actions[np.argmin(costs), 0])
In[17]:
Code
def model_controller_factory(step_fn, initial_fn):
    def factory(seed):
        rng = np.random.default_rng(seed)
        return lambda obs, target: plan(step_fn, initial_fn(obs), target, rng)

    return factory


def pd_factory(kp, kd):
    return lambda seed: (
        lambda obs, target: float(
            np.clip(kp * (target - obs[0]) - kd * obs[1], -1.0, 1.0)
        )
    )


def evaluate(factory, n_episodes=20, n_steps=40, seed=7):
    rng = np.random.default_rng(seed)
    costs = []
    for episode in range(n_episodes):
        controller = factory(1000 + episode)
        x, v = rng.uniform(-1.5, 1.5), rng.uniform(-1.0, 1.0)
        target = rng.uniform(-0.8, 0.8)
        n, total = rng.normal(size=2), 0.0
        for _ in range(n_steps):
            observation = np.concatenate(([x, v], nuisance_obs(n)))
            action = float(np.clip(controller(observation, target), -1.0, 1.0))
            x, v = true_step(x, v, action)
            n = nuisance_step(n, rng)
            total += (x - target) ** 2 + 0.05 * v**2 + 0.01 * action**2
        costs.append(total)
    return np.array(costs)


pd_grid = [
    (kp, kd) for kp in (0.5, 1.0, 1.5, 2.0) for kd in (0.5, 1.0, 1.5, 2.0)
]
pd_scores = {
    g: evaluate(pd_factory(*g), n_episodes=10, seed=11).mean() for g in pd_grid
}
best_pd = min(pd_scores, key=pd_scores.get)
controller_factories = {
    "PD (no model)": pd_factory(*best_pd),
    "MPC M_obs (2 comps)": model_controller_factory(
        make_linear_step(m_obs2, True), lambda obs: (obs - mean_o) @ m_obs2["P"]
    ),
    "MPC M_obs (4 comps)": model_controller_factory(
        make_linear_step(m_obs4, True), lambda obs: (obs - mean_o) @ m_obs4["P"]
    ),
    "MPC M_state (relevant)": model_controller_factory(
        make_linear_step(m_state, False), lambda obs: obs[:2]
    ),
    "MPC oracle": model_controller_factory(
        oracle_batch_step, lambda obs: obs[:2]
    ),
}
cl_costs = {
    name: evaluate(factory) for name, factory in controller_factories.items()
}
cl_mean = {name: float(costs.mean()) for name, costs in cl_costs.items()}
cl_sem = {
    name: float(costs.std(ddof=1) / np.sqrt(len(costs)))
    for name, costs in cl_costs.items()
}
controller_names = list(cl_mean)
controller_values = np.array([cl_mean[name] for name in controller_names])
controller_errors = np.array([cl_sem[name] for name in controller_names])

prediction_to_controller = {
    "M_obs (2 comps)": "MPC M_obs (2 comps)",
    "M_obs (4 comps)": "MPC M_obs (4 comps)",
    "M_state (relevant)": "MPC M_state (relevant)",
}
scatter_names = list(prediction_to_controller)
scatter_x = np.array([agg_mse[name] for name in scatter_names])
scatter_y = np.array(
    [cl_mean[prediction_to_controller[name]] for name in scatter_names]
)
Out[18]:
Console
PD gains selected on tuning episodes: (2.0, 0.5)
controller                  mean cost       SEM
PD (no model)                   3.222     0.795
MPC M_obs (2 comps)             9.769     1.841
MPC M_obs (4 comps)             2.596     0.810
MPC M_state (relevant)          2.596     0.810
MPC oracle                      2.596     0.810
Pairwise aggregate error: {'M_obs (2 comps)': 31.793339997706347, 'M_state (relevant)': 342.10720636320696}
Pairwise control cost: {'M_obs (2 comps)': 9.769208180140867, 'M_state (relevant)': 2.596215849016851}

The SEMs describe sampling variability over the twenty simulated episodes, conditional on these fitted models and this fixed experiment. They do not measure variation over real systems or independent training runs. Common random numbers can reduce comparison noise when the paired outcomes have positive covariance; they do not remove environmental randomness or guarantee smaller variance for every pair. Estimate uncertainty in the paired cost differences when assessing a particular comparison.

Out[19]:
Visualization
Closed-loop cost and standard errors for the PD baseline and four MPC controllers.
True-simulator closed-loop cost over twenty held-out episodes with standard error bars. All MPC controllers use the same candidate-action budgets and random seeds; the PD gains were tuned on separate episodes. The analytic MPC reference is not an optimal-control bound.
Out[20]:
Visualization
Prediction error against control cost for three learned models with explicit model labels.
Aggregate one-step observation error and true-simulator cost for the same three learned models. Explicit dictionary mappings connect each predictor with its MPC controller. Compare the two-component observation model with the state-focused model to inspect the pairwise ranking discrepancy.

Reading the Result Without Overclaiming

The two-component observation model retains little of the controlled state and its controller incurs greater cost than the state-focused model in this test. Yet completing the state-focused model's nuisance outputs with the training mean penalizes it heavily in aggregate observation error. This pair reverses the error-versus-control ordering. The four-component model is a useful counterpoint: retaining the relevant subspace can make an observation model useful for control too. There is no claim that the aggregate metric is flat across dimension counts, or that its globally best scorer must be the worst controller.

Several design choices matter. We specified a large background whose transition law is unaffected by actions; gave the state-focused learner task-coordinate knowledge; used a particular output-completion rule; and chose one goal family and planner. Changing any of these can change the rankings. In particular, adding a nuisance predictor to the state-focused model can lower its aggregate error without changing its decisions. Normalizing channels or using task-sensitive loss weights are possible alternatives, but this notebook has not trained and tested those alternatives, so it does not claim they recover a ranking.

Report coordinate-wise errors alongside the full observation score and evaluate actions in an independent environment. Good one-step predictions do not certify multi-step reliability, calibration, or robustness after a dynamics change. Conversely, an aggregate score containing coordinates outside a model's declared prediction contract may say little about its intended consumer. The example makes that distinction inspectable with code; it supplies no hardware evidence or architecture-wide conclusion.