What it actually does
Every method so far has learned what to do without ever learning what happens. A transition gives a model-free agent one scalar of information — the reward — and it throws away the rest, which is the next state, a full observation of how the world responded. Model-based methods keep it. Fit a dynamics model that predicts the next state from the current state and action, and suddenly each interaction is a complete supervised example, which is why this branch routinely learns from one or two orders of magnitude fewer interactions than a model-free one. Once a model exists there are two things to do with it. Plan with it: search over action sequences by rolling them out in imagination, pick the best one, execute only its first action, then throw the rest away and replan from the new state. That last part sounds wasteful and is the entire mechanism — replanning every step means model error never gets a chance to accumulate, and it is why model predictive control is deployed across process industries where no other optimal-control method is. Or generate with it: use the model to synthesize experience and train an ordinary model-free agent on the mix, which is Dyna and its descendants. The catch is the one that keeps recurring in this category. A planner searching for the best action sequence under a learned model is an optimizer pointed at a learned function, and it will find the places where the model is wrong and optimistic — except here the errors compound step by step down the rollout, so a model that is slightly wrong per step can be arbitrarily wrong at horizon twenty. Every serious technique in this area is a response to that.
- The next state is observable and predictable from the current state and action, which is what makes the dynamics a supervised problem at all
- The state is Markov in what the model is given; a partially observed system needs history or a latent state, and a model fitted without one is fitting noise
- The reward function is known or separately learnable, since the planner scores imagined trajectories and must be able to evaluate states it has never visited
- Model error stays small enough over the planning horizon that the optimized sequence is meaningful, which is the assumption the receding horizon exists to weaken
- The data covers the state-action region the planner will search, or an explicit uncertainty penalty stands in for the coverage that is missing
- The decision-time compute budget accommodates the planning loop, or the model is used to train a policy instead
Likelihood
Two objectives, one stacked on the other, and they fail in different ways. The first is a plain likelihood: fit the transition distribution. It behaves like supervised learning because it is supervised learning, with the usual diagnostics and the usual notion of held-out error. The second is a search: find the action sequence that the fitted model says is best. Nothing about the second objective is estimated from data — it is an optimization over a function, and its value is only as meaningful as the function is accurate. That mismatch is the heart of the method. A model with one percent error per step and a twenty-step horizon has no useful accuracy at the end of the rollout, and the planner will preferentially choose sequences whose predicted return is high, which correlates with wherever the model has drifted optimistically. This is the same optimizer-exploits-model failure that a deterministic actor has against its critic, with compounding added. Two structural responses appear in the formula itself. The receding horizon is the first: only the first action is executed, so the plan is recomputed from a real observed state every step and the model never has to be right for more than one step at a time in practice. The second is the horizon length, which is a genuine bias-variance trade rather than a tuning detail — too short and the plan is myopic, too long and the later terms are fiction. The usual third response is not visible here: replace the point prediction with a distribution and an ensemble, so the planner can be told how uncertain the model is and penalized for exploiting places where it does not know.
- the dynamics likelihood — an ordinary supervised objective over transitions, which is what makes this branch sample-efficient
- the planning horizon, and the single most important knob: long enough to see consequences, short enough that compounding model error has not ruined the rollout
- execute only the FIRST action of the optimized sequence, then replan — receding horizon, and the reason model error never accumulates in practice
- the expectation is over the model's own predictions, not the world; everything the planner believes comes from a function that was fitted rather than observed
How it is minimized
Supervised fitting of a dynamics model, then either decision-time planning by sampling-based optimization over action sequences (random shooting, CEM, MPPI) or policy learning on model-generated rollouts (Dyna, MBPO)
- the elite set — the best k of N sampled action sequences under the model, which is the only thing the next iteration is fitted to
- a distribution over action SEQUENCES, refitted each iteration; the cross-entropy method is nothing more than repeatedly resampling from the elites
- candidates per iteration, which is pure parallel compute — the one place in this category where more hardware straightforwardly buys a better decision
- elite count; small makes the search greedy and prone to exploiting model error, large makes it slow to converge within the time budget
The design decisions divide cleanly into the model and what is done with it. On the model side, the single most valuable choice is to predict a distribution rather than a point and to keep an ensemble of them, because that gives two distinct kinds of uncertainty: the spread within a model captures noise in the environment, and the disagreement between models captures ignorance about it. Only the second is reducible by collecting data, and only the second should make a planner cautious — conflating them is a common and consequential error. Predicting state deltas rather than absolute next states is the other standard choice and it matters more than it looks, because consecutive states are highly correlated and a model predicting absolutes can achieve excellent held-out error while being useless for planning. On the planning side, sampling-based optimizers dominate gradient-based ones because the model is often non-differentiable, the reward usually is, and the action space is low-dimensional enough that sampling works. Random shooting is the baseline; the cross-entropy method refits a distribution to the elite candidates and converges far faster for the same budget; MPPI weights candidates softly rather than truncating to elites and is smoother. Horizon length is the decisive hyperparameter, and there is a useful heuristic: extend the horizon only until the model's multi-step prediction error stops being small, and measure that rather than guessing. The alternative use of the model — generating synthetic experience for a model-free learner — trades differently: it is cheap at decision time, since there is no planning loop, and it moves the compounding-error problem into the training data instead, which is why MBPO-style methods branch short model rollouts off real states rather than generating long imagined trajectories.
planning horizon
The decisive knob. Long enough to see the consequences of an action, short enough that compounding model error has not turned the later terms into fiction. Set it from measured multi-step error, not by guessing (5 to 30 steps)
candidates per iteration
How many action sequences are evaluated. Pure parallel compute, and the one place in this category where more hardware straightforwardly buys a better decision (100 to 1,000)
elite fraction
What the cross-entropy distribution is refitted to. Too greedy and the search converges onto whatever the model is most optimistic about; too loose and it does not converge in the time available (top 10%)
ensemble size
Members of the dynamics ensemble. Their disagreement is the estimate of what the model does not know, which is the only uncertainty a planner should be penalized by (5 to 7 models)
uncertainty penalty
How much predicted return is discounted where the ensemble disagrees. This is the explicit defence against the planner exploiting model error, and it is essential offline (problem-specific; tune against held-out rollouts)
model rollout length (Dyna-style)
How far to imagine when generating synthetic experience. Short rollouts branched off real states beat long imagined trajectories, for exactly the compounding reason (1 to 5 steps)
model retrain cadence
How often the dynamics model is refitted as new data arrives. The data distribution shifts as the policy improves, so a model fitted once is fitted to a policy that no longer exists (every few hundred environment steps)
replan frequency
How often the plan is recomputed. Every step is the standard and is what keeps model error from accumulating; anything less trades robustness for compute (every step)
There is no convergence guarantee for the learned-model case, and the theory that does exist is instructive about why. Classical MPC with a known model and a convex problem has real stability guarantees, and it is one of the few genuinely deployed optimal-control technologies — so the guarantees are lost precisely at the step where the model stops being derived and starts being fitted. What replaces them is a set of characteristic failures. Model exploitation is the first and the defining one: the planner searches for high predicted return and finds regions where the model is optimistically wrong, so predicted return climbs while realized return does not, and the gap between the two is the diagnostic that matters. Compounding error is the second and it is mathematically unavoidable — a per-step error that looks negligible is multiplied down the rollout, which is why a horizon that worked at ten steps can be useless at thirty, and why measured multi-step error should set the horizon rather than intuition. Distribution shift is the third: the model is fitted on data from the current policy, the planner immediately moves the policy, and the new states are exactly the ones the model has not seen. This is why interleaving model fitting with data collection is mandatory rather than optional, and why the offline setting — no new data at all — needs an explicit pessimism penalty to be workable. The fourth is quieter: a model can have excellent one-step held-out error and still be useless, because one-step error on highly correlated consecutive states is an easy target. Multi-step rollout error against real trajectories is the honest evaluation and is routinely skipped.
Model fitting is ordinary supervised training and is cheap relative to everything else. Decision-time planning is where the cost lives: N candidate sequences times H horizon steps times the ensemble size, per decision, which is an enormous amount of compute inside a control loop — and it is all embarrassingly parallel, so it maps onto batched hardware essentially perfectly. That combination is unusual in this category: the wall-clock constraint is a real-time deadline rather than a training budget, and the work fits a GPU. The Dyna-style alternative inverts this: planning cost moves to training time, decision time becomes a single policy forward pass, and what is paid instead is the risk of training on synthetic data. Sample complexity is the reason for the whole branch — one to two orders of magnitude fewer environment interactions than a model-free method is typical on continuous control, which is the difference between feasible and infeasible when interactions are physical.
How, where, and why it is used
Time-Series Forecasting
AdaptedThe dynamics model is a multivariate forecaster conditioned on actions: it predicts the system's next state given its current state and what is done to it, which is a forecasting problem with an exogenous input the forecaster controls.
- Forecasting a controlled process, where the future depends on decisions still to be made and an uncontrolled forecast is answering the wrong question
- Scenario generation: rolling the model forward under different action sequences, which is what a planner does and what a what-if analysis wants
- Multi-step rollout error as the evaluation that matters, which is the same discipline good forecasting practice already has
- Model predictive control as the bridge between a forecast and a decision, which is the form the forecast actually gets used in
Genuinely adjacent, and the adjacency is worth being precise about. A dynamics model is a forecaster — same architecture choices, same multi-step error problem, same temptation to report one-step accuracy that means nothing. What differs is that the future depends on actions not yet taken, so the object being learned is conditional on a decision rather than unconditional on time. That changes the evaluation in a way forecasters recognize immediately: you cannot score it on a historical trajectory alone, because the interesting queries are about action sequences that were never executed, and no amount of held-out data answers those. The transfer runs both directions and is unusually clean. Forecasting practice supplies the discipline this area needs — rolling-origin evaluation, multi-step error, honest baselines, predicting deltas rather than levels. Model-based control supplies the framing forecasting often lacks: a forecast exists to support a decision, and the horizon that matters is the one the decision acts on. Where this stops being a forecasting problem is the moment the planner enters: optimizing over the model's output is not something a forecast evaluation covers, and it introduces a failure — the optimizer finding the model's errors — that no forecasting metric detects.
- Predict state deltas rather than absolute next states; consecutive states are highly correlated and predicting levels gives excellent one-step error with no useful signal
- Evaluate on multi-step rollout error against real trajectories, not one-step held-out error, which is an easy target and nearly uninformative
- Normalize per state dimension, since a dynamics vector mixes quantities on wildly different scales and the loss will otherwise be dominated by the largest
- Model the action conditioning explicitly rather than treating past actions as ordinary exogenous features, because the queries that matter are counterfactual
Multi-step rollout error at the horizon the planner actually uses, measured against real trajectories from a held-out period — that number sets the horizon and nothing else should. Report calibration of the predictive intervals too, since a planner penalized by uncertainty is only as good as that uncertainty is honest, and an overconfident model is worse than a less accurate well-calibrated one.
- One-step held-out error reported as model quality, when consecutive states are correlated enough to make it trivially good
- Evaluating only on historical action sequences, which never covers the counterfactual queries the planner will make
- Predicting absolute states rather than deltas, which hides the actual difficulty of the problem
- Assuming a forecast-quality model is a plan-quality model, when the planner will search exactly where the model is wrong
Anomaly Detection
Not applicableDetection scores observations without acting, and this entry is about optimizing a decision against a learned model of consequences — the related idea of treating dynamics-model prediction error as a novelty signal belongs to the density and reconstruction methods, which is where that argument is made properly.
Optimization
Primary fitLearn how the system responds to actions, then optimize over action sequences against that learned model, executing only the first action and replanning from the next real observation.
- Sample efficiency as a structural property: every transition is a full supervised example rather than one scalar reward
- The receding horizon as the mechanism that keeps model error from accumulating, and the reason MPC ships where other optimal control does not
- Optimizer-exploits-model with compounding error, which is the sharpest instance of a failure that recurs throughout this category
- Epistemic versus aleatoric uncertainty as an operational distinction: only ignorance should make a planner cautious, and only ignorance is reducible
The branch with the best sample efficiency and the most instructive failure mode, and the lessons here generalize further than anything else in this category. The first is about information: a scalar reward is an extraordinarily thin channel, and if the system's response is observable then learning it turns every interaction into dense supervision. Any time a pipeline is learning from a summary statistic when the full outcome was available, that comparison applies. The second is the receding horizon, which is a genuinely deep idea stated simply — plan far ahead, commit only to the next step, and recompute. It converts a method that requires a model accurate over twenty steps into one that requires a model accurate over one, and the same pattern appears anywhere a plan meets an imperfect forecast. The third is the uncertainty distinction: the variance within a model and the disagreement between models mean different things, and only the second tells you where more data would help. Conflating them produces a planner that is cautious about a noisy but well-understood region and confident in a region it has never seen. Where this branch is the wrong choice is sharply defined. If a simulator already exists, you already have a model and the entire value proposition evaporates — use it directly with a model-free method. If the dynamics are harder to learn than the policy, which is common in contact-rich or chaotic systems, the model will be the bottleneck and a model-free method will win. And if the decision has a real-time deadline that a planning loop cannot meet, the Dyna-style use of the model is the only viable form.
- Interleave model fitting with data collection, because the planner immediately moves the policy into states the model has not seen
- Keep an ensemble and separate its disagreement from its predicted noise; only the first should make the planner cautious
- Set the horizon from measured multi-step rollout error rather than intuition, since that is precisely where compounding bites
- Check whether a simulator already exists — if it does, the sample-efficiency argument for learning a model is gone
Predicted return from the planner plotted against realized return on the same axes. Their divergence is model exploitation, it is the failure the entire branch is engineered around, and no single-series metric reveals it. Alongside that, report multi-step rollout error at the planning horizon and the environment interactions consumed — sample efficiency is the claim this branch makes and it should be measured rather than asserted.
- A model fitted once and then planned against indefinitely, while the policy moves into states it never saw
- A horizon chosen by intuition, where compounding error has already made the later terms fiction
- Aleatoric and epistemic uncertainty conflated, producing caution in noisy regions and confidence in unknown ones
- Reaching for a learned model when a simulator already exists, which discards the only reason to accept the extra machinery
Control & Operations
Primary fitFit a model of the plant from operating data, then optimize a control sequence against it each cycle under explicit constraints, executing the first action and replanning from the next measurement.
- Process control, where model predictive control with a first-principles model has been the deployed standard for decades
- Systems with hard operating constraints, which MPC handles natively inside the optimization rather than through a reward penalty
- Settings where interactions are physical and expensive, so one to two orders of magnitude better sample efficiency decides feasibility
- Hybrid designs where a learned residual corrects a first-principles model, which is usually the strongest option available
The strongest applied fit in this entire category, and the one place where the industrial track record predates the machine-learning framing by decades. Model predictive control is deployed across refining, chemicals, power and manufacturing, and the reasons are exactly its properties: constraints are handled inside the optimization rather than expressed as reward penalties an optimizer will trade away, the receding horizon absorbs disturbance and model error, and the whole thing is inspectable — a plan is a sequence of actions and their predicted consequences, which an engineer can read and challenge. What machine learning contributes is the model, in settings where deriving one from first principles is impractical, and the honest recommendation is almost always the hybrid: keep the physics where physics is available and learn a residual for what it misses. That structure preserves the constraint handling and the interpretability while fixing the part that was wrong. Two cautions. The guarantees classical MPC carries come from the model being correct, so a learned model buys flexibility at the cost of exactly the property that made the method trustworthy — and the planner will search wherever the learned part is optimistic. And a real-time deadline is a hard constraint on the planner: candidate count and horizon are bounded by the control cycle, which makes this one of the few settings where decision-time compute is the binding engineering problem.
- Start from a first-principles model and learn a residual where it is wrong; the hybrid keeps the constraint handling and fixes the part that was failing
- Encode constraints inside the optimization rather than as reward penalties, since a penalty is a price and a constraint is not
- Size candidate count and horizon against the control cycle, because a plan that arrives late is not a plan
- Refit on recent operating data as the plant drifts, since a model fitted to last year's configuration is confidently wrong about this one
Closed-loop performance against the incumbent controller on matched scenarios, with constraint violations counted separately rather than averaged in. Report multi-step model error by operating regime, since a plant model that is accurate at the usual setpoint and wrong near a limit is wrong exactly where the controller will be pushed.
- A purely learned model where physics was available, discarding both the guarantees and the interpretability for no gain
- Constraints as reward penalties, which an explicit optimizer will trade against return
- A model fitted on data from the incumbent controller, which never visited the regions the new one will
- A planning budget that does not fit the control cycle, which silently degrades to a stale plan
Causal Inference & Uplift
AdaptedA dynamics model answers interventional queries — what happens if this action is taken — and a planner issues a great many of them, which makes the model's behaviour under intervention rather than under observation the property that matters.
- Planning as a sequence of interventional queries, which is exactly what a do-operator expresses
- Learning a model from logged operating data, where the action was chosen by an existing controller and is therefore confounded with the state
- Offline model-based methods, whose pessimism penalties are a response to the same extrapolation problem causal inference calls positivity
- Distinguishing a model that predicts well from one that supports intervention, which is the central distinction in both fields
Included because the connection is structural rather than decorative, and because it names the failure precisely. A dynamics model fitted on logged data learns the conditional distribution of the next state given the state and the action that was actually taken — but the action was chosen by some controller as a function of the state, so action and state are confounded, and a model that has learned the resulting correlation will confidently predict the consequence of an action nobody would have taken in that state. That is the observational-versus-interventional gap, and a planner is a machine for finding exactly the action sequences the logged controller never produced. The correspondence extends to the remedies. Positivity — the requirement that every action have some probability in every state — is what the logged data fails, and the model-based response is an uncertainty penalty that refuses to trust predictions outside the support, which is the same idea as trimming or bounding in a causal estimator. Randomized exploration in the logged system is the analogue of a randomized experiment and is worth the cost for the same reason. The practical instruction is short: a dynamics model evaluated only on held-out prediction accuracy has been validated as an observational model and not as a causal one, and the planner will use it as the latter.
- Check action coverage per state region before trusting the model there; a logged deterministic controller supplies none
- Add randomized exploration to the logged system where it is safe, which is the cheapest way to buy the support the model needs
- Penalize predictions where the ensemble disagrees, which is the operational form of refusing to extrapolate beyond support
- Validate on trajectories generated under a different action policy, not only on held-out data from the same controller
Prediction error on trajectories generated under a deliberately different action policy, which is the only evaluation that tests the interventional claim. Held-out accuracy on data from the logging controller validates the model as an observational one and says nothing about the queries a planner will actually make.
- Held-out accuracy taken as evidence the model supports intervention, when it only evidences observational fit
- Logged data from a deterministic controller, which gives no support for any action it did not take
- No uncertainty penalty offline, leaving the planner free to exploit exactly the unsupported regions
- Treating the action as an ordinary feature, which hides that it was chosen as a function of the state
What it costs to run
The cheapest branch here in environment interactions — one to two orders of magnitude fewer than model-free on continuous control, because every transition is a full supervised example — and the most expensive at decision time if planning is used. Model fitting is ordinary supervised training. The planning loop is N candidates times H steps times ensemble size per decision, which is a large amount of compute inside a control cycle and is embarrassingly parallel, so it maps onto batched hardware almost perfectly. That inversion is unusual: the binding constraint is a real-time deadline rather than a training budget. Illustrative, not a measured benchmark.
Two very different profiles depending on how the model is used. A planning deployment runs the full optimization every control cycle, so latency is the design constraint and candidate count and horizon are chosen to fit it — the payoff is a decision that is inspectable, since the plan and its predicted consequences can be read directly. A Dyna-style deployment ships only the policy, so inference is one forward pass and the model never leaves training. Choosing between them is mostly a question of whether the control cycle can afford a search.
Frequent, and more so than elsewhere in this category, because the failure it prevents is specific: the planner moves the system into states the model has not seen, so a model fitted once is fitted to a policy that no longer exists. Online settings interleave fitting with collection every few hundred steps. Deployed process control refits on recent operating data as the plant drifts, and the trigger is usually a rise in multi-step prediction error rather than a schedule.
- Predicted return from the planner against realized return — their divergence is model exploitation, and it is the failure this branch is engineered around
- Multi-step rollout error at the planning horizon, measured against real trajectories; one-step error is an easy target and nearly uninformative
- Ensemble disagreement over the states actually visited, which is the direct read on whether the planner is operating inside its support
- Calibration of the predictive intervals, since an uncertainty penalty is only as good as the uncertainty is honest
- Planning latency against the control cycle, because a plan that arrives late has silently become a stale plan
- State-distribution shift between the fitting data and where the controller now operates, which is what makes a once-fitted model wrong
- One-step held-out error is close to meaningless on its own. Consecutive states are highly correlated, so a model can score beautifully and be useless at horizon ten — multi-step rollout error is the number that matters
- The planner will find the model's errors, because searching for high predicted return correlates with searching for optimistic error, and compounding makes it worse the longer the horizon
- Predict state deltas, not absolute next states. Predicting levels makes the task artificially easy and hides how wrong the model is about change
- Aleatoric and epistemic uncertainty are different things. Environment noise should not make a planner cautious; model ignorance should, and only the second shrinks with data
- A model fitted once and planned against indefinitely is being used outside its support within a few policy improvements. Interleaving fitting with collection is mandatory rather than a refinement
- Learning from logs produced by a deterministic controller gives no information about actions it never took, and the planner will confidently choose exactly those
- If a simulator already exists you already have a model, and the sample-efficiency argument that justifies this entire branch no longer applies
- Classical MPC's stability guarantees come from the model being correct. Replacing a derived model with a fitted one keeps the structure and gives up the guarantee, which is worth stating explicitly to anyone who expects both
Pros and cons, in context
By far the best sample efficiency in this category
Every transition is a full supervised example rather than one scalar reward, which typically means one to two orders of magnitude fewer environment interactions. When interactions are physical, that is the difference between a feasible project and an impossible one
The receding horizon makes an imperfect model usable
Plan far ahead, execute one step, replan from a real observation. It converts a method that would need a model accurate over twenty steps into one that needs it accurate over one, and it is why MPC ships where other optimal control does not
Constraints are handled inside the optimization
A planner can refuse infeasible sequences outright, rather than expressing a limit as a reward penalty that an optimizer treats as a price. For any system with hard operating limits this is a categorical advantage over every other entry here
The decision is inspectable
A plan is a sequence of actions and their predicted consequences, which an engineer can read, challenge and overrule. No policy network offers anything comparable, and in regulated or safety-relevant settings it is often the deciding property
The model transfers across objectives
Dynamics do not depend on the reward, so a fitted model can be replanned against a new objective with no retraining at all. A value function or a policy has to be relearned from scratch when the goal changes
Planning compute is embarrassingly parallel
Thousands of candidate rollouts are independent, so the decision-time cost maps onto batched hardware almost perfectly. This is the one place in this category where more hardware straightforwardly buys a better decision
The planner exploits the model, and compounding makes it worse
Searching for high predicted return correlates with searching for optimistic model error, and per-step errors multiply down the rollout. Every technique in this area is a response to this, and none of them removes it
Decision-time cost can be prohibitive
Candidates times horizon times ensemble size, every control cycle. Where the cycle is short the planning budget binds hard, and the Dyna-style alternative — train a policy on model rollouts — is the only viable form
Model quality is not the same as plan quality
A model with excellent held-out one-step error can be useless for planning, because consecutive states are correlated and the error that matters accumulates. Multi-step rollout evaluation is the honest measure and is routinely skipped
Two learning problems instead of one
A dynamics model and a controller, each with their own failure modes, interacting. When a run underperforms, isolating whether the model or the planner is at fault is genuinely harder than debugging a model-free method
Some systems are harder to model than to control
Contact-rich manipulation and chaotic dynamics are the standard examples: the policy is learnable and the dynamics are not, so the model becomes the bottleneck and a model-free method wins outright
The value proposition disappears if a simulator exists
A simulator is a model. Where one is already available, learning another buys nothing and costs the compounding-error failure mode — yet reaching for model-based methods anyway is a common and expensive mistake
Intuitive, idiomatic, optimized
The same algorithm in three languages, each progressing through Kent Beck's three stages. Every optimization names the lever it pulls and the trade-off it accepts — cost profiles are illustrative, not measured benchmarks.
The math, transcribed literally. No dependencies.
"""Model-based RL with random-shooting MPC, transcribed.
Two pieces. A dynamics model fitted by ordinary regression - the next state IS
the label, which is why every transition here carries a whole state vector of
information instead of a single scalar reward. And a planner that searches over
action sequences by rolling them out through that model.
Watch the last line of the control loop: only the FIRST action of the optimized
sequence is executed, and the rest is thrown away and recomputed. That looks
wasteful and is the entire mechanism - replanning from a real observation every
step means model error never gets a chance to accumulate.
"""
import random
def make_model(n_state, n_action):
"""Predicts the state DELTA: s' - s = A s + B a + c.
Predicting the delta rather than the absolute next state matters more than
it looks. Consecutive states are highly correlated, so a model predicting
absolutes scores beautifully on held-out error while having learned almost
nothing about how the system actually changes.
"""
return {
"a": [[0.0] * n_state for _ in range(n_state)],
"b": [[0.0] * n_action for _ in range(n_state)],
"c": [0.0] * n_state,
}
def predict(model, state, action):
next_state = []
for row in range(len(model["c"])):
delta = model["c"][row]
for index, value in enumerate(state):
delta += model["a"][row][index] * value
for index, value in enumerate(action):
delta += model["b"][row][index] * value
next_state.append(state[row] + delta)
return next_state
def fit_step(model, state, action, next_state, lr):
"""One SGD step on squared error over the delta.
Ordinary supervised learning: the label was handed over by the environment
for free, alongside the reward that a model-free method would have kept
instead. That asymmetry is the whole sample-efficiency argument.
"""
for row in range(len(model["c"])):
target = next_state[row] - state[row]
prediction = model["c"][row]
for index, value in enumerate(state):
prediction += model["a"][row][index] * value
for index, value in enumerate(action):
prediction += model["b"][row][index] * value
error = prediction - target
for index, value in enumerate(state):
model["a"][row][index] -= lr * error * value
for index, value in enumerate(action):
model["b"][row][index] -= lr * error * value
model["c"][row] -= lr * error
def rollout_return(model, reward_fn, state, sequence, gamma):
"""Score one candidate action sequence by imagining it.
Every term past the first has the model predicting from its OWN previous
prediction. Errors therefore compound: a per-step error that looks
negligible is multiplied down the rollout, which is why the horizon is the
most important knob here and why it should be set from measured multi-step
error rather than guessed.
"""
imagined = list(state)
total = 0.0
discount = 1.0
for action in sequence:
imagined = predict(model, imagined, action)
total += discount * reward_fn(imagined, action)
discount *= gamma
return total
def plan_random_shooting(model, reward_fn, state, n_action, horizon, candidates,
action_low, action_high, gamma):
"""The simplest planner there is: sample sequences, keep the best.
No gradients and no structure, which is exactly why it works when the model
is non-differentiable. It is also the baseline the cross-entropy method
improves on, by refitting a distribution to the best candidates instead of
resampling uniformly every iteration.
"""
best_sequence = None
best_score = -float("inf")
for _ in range(candidates):
sequence = [
[random.uniform(action_low, action_high) for _ in range(n_action)]
for _ in range(horizon)
]
score = rollout_return(model, reward_fn, state, sequence, gamma)
if score > best_score:
best_sequence, best_score = sequence, score
return best_sequence
def run(env, reward_fn, n_state, n_action, steps=10_000, horizon=15,
candidates=200, lr=1e-3, gamma=0.99, action_low=-1.0, action_high=1.0,
warmup=200):
model = make_model(n_state, n_action)
state = env.reset()
for step in range(steps):
if step < warmup:
# Planning against a model that knows nothing is searching for the
# maxima of noise, and the planner is very good at finding them.
action = [random.uniform(action_low, action_high) for _ in range(n_action)]
else:
sequence = plan_random_shooting(
model, reward_fn, state, n_action, horizon, candidates,
action_low, action_high, gamma,
)
action = sequence[0] # only the first. Receding horizon.
next_state, reward, done = env.step(action)
# Fit as we go. The planner immediately moves the system into states the
# model has not seen, so a model fitted once is fitted to a policy that
# no longer exists.
fit_step(model, state, action, next_state, lr)
state = env.reset() if done else next_state
return modelCandidates x horizon model evaluations per decision, each a scalar loop over the state dimension. Nothing is reused between decisions.
How this language actually wants it written.
"""Model-based RL - probabilistic ensemble, CEM planner, uncertainty penalty."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import torch
from torch import Tensor, nn
LOG_VAR_BOUNDS = (-10.0, 0.5)
@dataclass(frozen=True)
class PlannerConfig:
n_state: int
n_action: int
action_low: float = -1.0
action_high: float = 1.0
# The decisive knob. Set it from MEASURED multi-step rollout error, not from
# intuition: compounding is what makes a horizon that worked at ten steps
# useless at thirty.
horizon: int = 15
candidates: int = 500
elite_fraction: float = 0.1
iterations: int = 5
gamma: float = 0.99
# How much predicted return is discounted where the ensemble disagrees.
# This is the explicit defence against the planner exploiting model error.
uncertainty_penalty: float = 1.0
def __post_init__(self) -> None:
if self.horizon < 1:
raise ValueError("horizon must be at least one step")
if not 0.0 < self.elite_fraction <= 1.0:
raise ValueError(f"elite fraction must be in (0, 1], got {self.elite_fraction}")
if self.action_low >= self.action_high:
raise ValueError("action bounds must be ordered")
@property
def elite_count(self) -> int:
return max(2, int(self.candidates * self.elite_fraction))
class ProbabilisticDynamics(nn.Module):
"""Predicts a DISTRIBUTION over the state delta, not a point.
The variance head is not decoration. It separates the two kinds of
uncertainty a planner has to treat differently:
- the predicted variance is ALEATORIC - noise in the environment, which
more data will not reduce and which should NOT make a planner cautious
- disagreement ACROSS ensemble members is EPISTEMIC - ignorance about the
dynamics, which more data does reduce and which absolutely should
Conflating them yields a planner that is timid in a noisy but well-mapped
region and confident in a region it has never visited.
"""
def __init__(self, config: PlannerConfig, hidden: int = 200) -> None:
super().__init__()
width = config.n_state + config.n_action
self.trunk = nn.Sequential(
nn.Linear(width, hidden), nn.SiLU(),
nn.Linear(hidden, hidden), nn.SiLU(),
)
self.mean_head = nn.Linear(hidden, config.n_state)
self.log_var_head = nn.Linear(hidden, config.n_state)
def forward(self, states: Tensor, actions: Tensor) -> tuple[Tensor, Tensor]:
features = self.trunk(torch.cat([states, actions], dim=-1))
# Bounded: an unconstrained log-variance runs away to explain a hard
# sample as pure noise, and the Gaussian likelihood rewards it for that.
log_var = self.log_var_head(features).clamp(*LOG_VAR_BOUNDS)
return self.mean_head(features), log_var
def gaussian_nll(mean: Tensor, log_var: Tensor, target_delta: Tensor) -> Tensor:
"""Negative log-likelihood of the observed delta.
The target is the DELTA, never the absolute next state: consecutive states
are correlated enough that predicting levels gives excellent held-out error
and almost no information about how the system changes.
"""
inverse_var = torch.exp(-log_var)
return (((mean - target_delta) ** 2) * inverse_var + log_var).mean()
class DynamicsEnsemble(nn.Module):
"""Independently initialized members, trained on bootstrapped batches.
Independence is the point: correlated members agree everywhere, including
where they are all wrong, and the disagreement signal disappears exactly
when it is needed.
"""
def __init__(self, config: PlannerConfig, size: int = 5) -> None:
super().__init__()
self.members = nn.ModuleList([ProbabilisticDynamics(config) for _ in range(size)])
def predict(self, states: Tensor, actions: Tensor) -> tuple[Tensor, Tensor]:
means = torch.stack([member(states, actions)[0] for member in self.members])
# Mean prediction, and the epistemic term as spread ACROSS members.
return means.mean(dim=0), means.std(dim=0).sum(dim=-1)
def cross_entropy_plan(
ensemble: DynamicsEnsemble,
reward_fn: Callable[[Tensor, Tensor], Tensor],
state: Tensor,
config: PlannerConfig,
warm_start: Tensor | None = None,
) -> Tensor:
"""Refit a distribution over action SEQUENCES to its own elites.
Random shooting resamples uniformly every time and learns nothing from what
it already tried. CEM keeps a Gaussian over sequences and refits it to the
best candidates each iteration, which concentrates the search where the
returns are and converges far faster for the same evaluation budget.
"""
mean = (
warm_start
if warm_start is not None
else torch.zeros(config.horizon, config.n_action)
)
std = torch.full_like(mean, (config.action_high - config.action_low) / 2.0)
for _ in range(config.iterations):
noise = torch.randn(config.candidates, config.horizon, config.n_action)
sequences = (mean + std * noise).clamp(config.action_low, config.action_high)
returns = evaluate_sequences(ensemble, reward_fn, state, sequences, config)
elites = sequences[returns.topk(config.elite_count).indices]
mean = elites.mean(dim=0)
std = elites.std(dim=0) + 1e-6
return mean
@torch.no_grad()
def evaluate_sequences(
ensemble: DynamicsEnsemble,
reward_fn: Callable[[Tensor, Tensor], Tensor],
state: Tensor,
sequences: Tensor,
config: PlannerConfig,
) -> Tensor:
"""Roll every candidate forward through the model and score it.
Each step predicts from the model's OWN previous prediction, so error
compounds down the horizon - which is why the epistemic penalty accumulates
alongside the reward rather than being applied once at the end.
"""
imagined = state.expand(sequences.shape[0], -1).clone()
total = torch.zeros(sequences.shape[0])
discount = 1.0
for step in range(config.horizon):
actions = sequences[:, step, :]
delta, disagreement = ensemble.predict(imagined, actions)
imagined = imagined + delta
reward = reward_fn(imagined, actions)
# Penalize where the ensemble disagrees, not where it predicts noise:
# only ignorance is reducible, and only ignorance should deter a planner.
total += discount * (reward - config.uncertainty_penalty * disagreement)
discount *= config.gamma
return total
def control_loop(env, ensemble, reward_fn, config: PlannerConfig, steps: int) -> None:
state = torch.as_tensor(env.reset(), dtype=torch.float32)
previous_plan: Tensor | None = None
for _ in range(steps):
plan = cross_entropy_plan(ensemble, reward_fn, state, config, previous_plan)
# Receding horizon: execute the first action only, then replan from a
# real observation. The plan is a sequence and all but one step of it is
# discarded, which is what stops model error from accumulating.
next_state, _, done = env.step(plan[0].tolist())
# Warm start: shift the surviving plan forward one step. The world moved
# by one step and the old plan is still nearly right for the rest.
previous_plan = torch.cat([plan[1:], plan[-1:].clone()])
state = torch.as_tensor(env.reset() if done else next_state, dtype=torch.float32)
if done:
previous_plan = NoneWhat changed: Random shooting becomes the cross-entropy method, which refits a distribution over sequences to its own elites and converges far faster for the same evaluation budget. The point-prediction model becomes a probabilistic ensemble, which separates the two uncertainties a planner must treat differently: predicted variance is environment noise and should not deter it, while disagreement across independently initialized members is ignorance and should. That disagreement becomes an explicit penalty accumulated down the rollout, the target becomes the state delta, and the plan is warm-started by shifting the previous solution forward one step.
Iterations x candidates x horizon x ensemble model evaluations per decision, batched across candidates.
Faster, and the justification for each change.
"""MPC - the whole planner as one batched tensor program.
Planning cost is candidates x horizon x ensemble per decision, inside a control
cycle. It is also embarrassingly parallel in candidates AND in ensemble
members, which makes this one of the few places in this category where the
work maps onto batched hardware essentially perfectly.
The horizon is the one axis that genuinely cannot be parallelized: step k+1
depends on step k. Everything else collapses.
"""
from __future__ import annotations
from typing import Callable
import torch
from torch import Tensor
class BatchedEnsemble(torch.nn.Module):
"""All ensemble members as one weight tensor with a leading member axis.
Separate modules mean E small GEMMs per layer, each too small to saturate
anything, plus E kernel launches. A leading member dimension turns every
layer into one baddbmm over (E, batch, width) - identical arithmetic, one
launch - and the members stay independent, which is what the disagreement
signal requires.
"""
def __init__(self, size: int, n_input: int, n_state: int, hidden: int = 200) -> None:
super().__init__()
self.w1 = torch.nn.Parameter(torch.empty(size, n_input, hidden))
self.b1 = torch.nn.Parameter(torch.zeros(size, 1, hidden))
self.w2 = torch.nn.Parameter(torch.empty(size, hidden, n_state))
self.b2 = torch.nn.Parameter(torch.zeros(size, 1, n_state))
for tensor in (self.w1, self.w2):
torch.nn.init.orthogonal_(tensor, gain=1.0)
def forward(self, joint: Tensor) -> Tensor:
"""joint is (E, batch, n_input); returns (E, batch, n_state) deltas."""
hidden = torch.baddbmm(self.b1, joint, self.w1)
hidden = torch.nn.functional.silu(hidden)
return torch.baddbmm(self.b2, hidden, self.w2)
class PlanBuffers:
"""Every tensor the planner needs, allocated once at construction.
A planning loop that allocates runs inside a control cycle and does so
thousands of times per second. Sizing the buffers once turns the planner
into a fixed-cost operation, which is what a real-time deadline requires.
"""
def __init__(self, ensemble_size: int, candidates: int, horizon: int,
n_state: int, n_action: int, device: torch.device) -> None:
# Single dtype throughout, contiguous, reused every iteration.
self.imagined = torch.zeros((ensemble_size, candidates, n_state),
dtype=torch.float32, device=device)
self.joint = torch.zeros((ensemble_size, candidates, n_state + n_action),
dtype=torch.float32, device=device)
self.returns = torch.zeros(candidates, dtype=torch.float32, device=device)
self.noise = torch.zeros((candidates, horizon, n_action),
dtype=torch.float32, device=device)
self.n_state = n_state
@torch.no_grad()
def evaluate_batched(
ensemble: BatchedEnsemble,
reward_fn: Callable[[Tensor, Tensor], Tensor],
state: Tensor,
sequences: Tensor,
buffers: PlanBuffers,
gamma: float,
penalty: float,
) -> Tensor:
"""Every candidate and every ensemble member, advanced together.
One model call per HORIZON STEP covers the entire search - the naive form
makes candidates x ensemble separate calls per step. The horizon loop is
all that remains, and it has to: step k+1 predicts from step k's output.
"""
ensemble_size, candidates, n_state = buffers.imagined.shape
buffers.imagined.copy_(state.view(1, 1, n_state).expand(ensemble_size, candidates, n_state))
buffers.returns.zero_()
discount = 1.0
for step in range(sequences.shape[1]):
actions = sequences[:, step, :].unsqueeze(0).expand(ensemble_size, -1, -1)
# Written into the pre-allocated joint buffer rather than concatenated
# into a fresh tensor on every one of thousands of steps.
buffers.joint[..., :n_state].copy_(buffers.imagined)
buffers.joint[..., n_state:].copy_(actions)
deltas = ensemble(buffers.joint) # (E, candidates, n_state)
buffers.imagined.add_(deltas) # in place; deltas, not levels
# Epistemic term: spread ACROSS members, computed on the same tensor
# that was just produced rather than from a second pass.
disagreement = buffers.imagined.std(dim=0).sum(dim=-1)
mean_state = buffers.imagined.mean(dim=0)
reward = reward_fn(mean_state, sequences[:, step, :])
buffers.returns.add_(discount * (reward - penalty * disagreement))
discount *= gamma
return buffers.returns
@torch.no_grad()
def cem_plan(
ensemble: BatchedEnsemble,
reward_fn: Callable[[Tensor, Tensor], Tensor],
state: Tensor,
buffers: PlanBuffers,
previous_plan: Tensor | None,
horizon: int,
n_action: int,
iterations: int,
elite_count: int,
bounds: tuple[float, float],
gamma: float,
penalty: float,
) -> Tensor:
"""CEM, warm-started from the surviving tail of the last plan.
Warm-starting is the cheapest real speedup in MPC and it is not an
approximation: the world advanced one step, so last cycle's plan shifted
forward by one is already close to optimal, and the search starts from
there instead of from scratch. It typically removes an entire CEM iteration.
"""
if previous_plan is None:
mean = torch.zeros(horizon, n_action, device=state.device)
else:
mean = torch.cat([previous_plan[1:], previous_plan[-1:]])
std = torch.full_like(mean, (bounds[1] - bounds[0]) / 2.0)
for _ in range(iterations):
# normal_ fills the pre-allocated noise buffer in place: no allocation
# anywhere in the planning loop.
buffers.noise.normal_()
sequences = torch.addcmul(mean, buffers.noise, std).clamp_(*bounds)
returns = evaluate_batched(
ensemble, reward_fn, state, sequences, buffers, gamma, penalty
)
elites = sequences[returns.topk(elite_count).indices]
mean = elites.mean(dim=0)
std = elites.std(dim=0).clamp_min_(1e-6)
return meanWhat changed: Planning is candidates × horizon × ensemble per decision, inside a control cycle — and it is parallel in candidates and in members, so both axes collapse into batched tensor operations and one model call per horizon step covers the entire search. The horizon loop is the one axis that genuinely cannot go, because step k+1 predicts from step k. Every tensor is allocated once, since a planner that allocates does so thousands of times per second against a deadline. And the search warm-starts from the previous plan shifted forward, which is not an approximation — the world advanced one step and last cycle's solution is already close.
Batch work to amortize interpreter overhead
All candidates and all ensemble members advance in one model call per horizon step, replacing candidates × ensemble separate calls and collapsing the two parallel axes of the search.
Trade-off: Peak memory is now candidates × ensemble × state width, so a long horizon with a large search must trade candidate count against the memory the batch occupies.
Pre-allocate output arrays and use in-place operations
The imagined states, joint inputs, noise and returns are sized once and written in place, so a planning loop running against a real-time deadline performs no allocation at all.
Trade-off: Candidate count, horizon and ensemble size are fixed at construction, so adapting the search budget to remaining cycle time means rebuilding the buffers.
Vectorize to NumPy/BLAS (@, np.dot, einsum)
A leading member dimension makes each layer one batched matmul instead of E small GEMMs that individually cannot saturate the hardware.
Trade-off: The ensemble shares one module and one optimizer, so bootstrapping members on different data subsets — which is what keeps their errors independent — has to be arranged by masking rather than by separate training.
Fuse adjacent operations so intermediates are never materialized
The epistemic spread and the mean state are reductions over the same just-written tensor, and addcmul builds the candidate sequences without materializing the scaled noise separately.
Trade-off: The per-member predictions are overwritten in place, so a diagnostic wanting individual member trajectories has to re-run the rollout rather than read them back.
One model call per horizon step for the whole search; nothing allocated inside the planning loop. Illustrative, not a measured benchmark.