Planning, Control, and Policy Evaluation › Zero-Shot, Few-Shot, and Cross-Domain Transfer
World Models HandbookAll chapters Planning, Control, and Policy Evaluation › Zero-Shot, Few-Shot, and Cross-Domain TransferOne section · Sections in this chapter6 / 9 ↓
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
Transfer evaluation has a protocol problem before it has a metric problem. The words "zero-shot," "few-shot," and "cross-domain" are used loosely, so let's fix operational definitions that survive peer review.
Transfer needs a protocol before a metric because the same word, "zero-shot," can mean at least three different things. It can mean the model is frozen and no target data is seen at all. It can mean the model is frozen but a small amount of target data was used to select hyperparameters. It can mean the model is frozen but the planner's hyperparameters were tuned on target rollouts. These settings use different information and can have different interaction budgets and expected performance. Calling all of them zero-shot conceals those protocol differences. The operational definitions below make the information access and budgets explicit.
- Zero-shot means no target-domain training, adaptation, or selection information is used before evaluation. Model, policy, planner, and learned hyperparameters are frozen. Target rollouts used for checkpoint or hyperparameter selection count as validation interactions and violate this strict zero-shot definition, even without gradient updates.
- Few-shot permits a stated, bounded number of target-environment transitions (call it ) and a stated number of adaptation steps (gradient updates, closed-form refits, or meta-updates). Both numbers are reported. Define the target adaptation budget as (number of target-environment transitions allowed) and (number of adaptation steps). The target evaluation rollouts are separate from the transitions used for adaptation; if an evaluation rollout is used to select among adaptation settings, it is not an evaluation rollout anymore.
- Cross-domain transfer names the specific shift: shifted dynamics (different ), shifted observations (different sensor model), shifted reward (different task objective), or shifted task (different target). A claim like "our method transfers well" is not operational without the shift label.
To test a source-pretraining benefit at a given target-data budget, compare against a target-only baseline with the same allowed target interactions. Matching that budget removes a data-quantity confound, not every source of variation. More target data do not guarantee better performance.
The source and target environments differ only in actuator gain, from to ; observations, reward, and task stay fixed. We also pair initial cases and planner streams for the frozen-controller comparison. The physical source actuator coefficient exceeds the target coefficient by relative to the target. This concerns the magnitude of a nonzero action's incremental actuator effect, not every total next-state prediction. The fitted affine coefficient need not equal the physical coefficient exactly.
def target_step_batch(S, A):
return true_step_batch(S, A, gain=GAIN_TARGET)
S0_target = (
S0_eval.copy()
) # pair source/target cases; only actuator gain changes
mpc_frozen = make_mpc_factory(learned_step_batch, H=10, K=64)
pd_target = lambda _seed: make_pd_policy(kp=4.0, kd=3.0)
zero_shot_returns = evaluate_policy(target_step_batch, mpc_frozen, S0_target)
pd_target_returns = evaluate_policy(target_step_batch, pd_target, S0_target)
TARGET_EVAL_TRANSITIONS = len(S0_target) * EPISODE_HORIZONZero-shot (frozen source model, no target training): MPC sample mean: -6.050 std: 8.540 PD baseline: -4.587 std: 6.162 target episodes per controller: 20 target transitions per controller: 600
The frozen MPC mean is about on the target cases, compared with the target PD mean of . Its source-environment mean was under paired cases and streams. The gain change therefore changes this controller's measured return, but the gap to PD need not be entirely caused by the model shift; search limitations were already visible in the source sweep. Adaptation tests whether a refit improves the controller, not whether it must recover that entire gap.
We now spend target-environment transitions to fit a new model. To make the transfer claim operational, we compare three configurations against the PD baseline, all evaluated on the same target evaluation cases:
- Few-shot fine-tune: refit on the union of the source pretraining set and the new target transitions, keeping the same linear model class.
- Target-only baseline: refit from scratch on the target transitions alone. Same budget on target transitions, no source advantages.
- PD baseline: the chosen no-model, no-fitting-data return comparator for the target task.
The two refits use identical handcrafted features and the same 400 target transitions. The source-plus-target fit adds 3000 source records with equal per-record weighting, so old-domain data dominate its loss numerically. Paired candidate streams control search-draw differences at corresponding decisions, but one collection seed and one stream per case do not establish a population pretraining benefit. PD uses no model-fitting data, though its target evaluation still consumes interactions.
Counting the source-plus-target fit's target records as free would invalidate its stated adaptation budget. All 400 target records must be reported regardless of whether the fit also uses source data.
FEW_SHOT_INTERACTIONS = 400
adapt_rng = np.random.default_rng(55)
S_adapt, A_adapt, S_next_adapt = collect_transitions(
n_episodes=20,
horizon=FEW_SHOT_INTERACTIONS // 20,
s0_low=-0.5,
s0_high=0.5,
rng=adapt_rng,
gain=GAIN_TARGET,
)
assert len(S_adapt) == FEW_SHOT_INTERACTIONS
X_adapt = np.column_stack([S_adapt, A_adapt, np.ones(len(S_adapt))])
W_target_only, *_ = np.linalg.lstsq(X_adapt, S_next_adapt, rcond=None)
def target_only_step_batch(S, A):
S = np.atleast_2d(S).astype(float)
A = np.atleast_1d(A).astype(float)
X = np.column_stack([S, A, np.ones(len(S))])
return X @ W_target_only
X_fine = np.vstack([X_train, X_adapt])
Y_fine = np.vstack([S_next_train, S_next_adapt])
W_fine, *_ = np.linalg.lstsq(X_fine, Y_fine, rcond=None)
def fine_step_batch(S, A):
S = np.atleast_2d(S).astype(float)
A = np.atleast_1d(A).astype(float)
X = np.column_stack([S, A, np.ones(len(S))])
return X @ W_finempc_fine = make_mpc_factory(fine_step_batch, H=10, K=64)
mpc_target_only = make_mpc_factory(target_only_step_batch, H=10, K=64)
fine_returns = evaluate_policy(target_step_batch, mpc_fine, S0_target)
target_only_returns = evaluate_policy(
target_step_batch, mpc_target_only, S0_target
)
transfer_results = [
{
"setting": "Zero-shot (frozen source model)",
"mean": float(zero_shot_returns.mean()),
"std": float(zero_shot_returns.std()),
},
{
"setting": f"Few-shot fine-tune (source + {FEW_SHOT_INTERACTIONS} target)",
"mean": float(fine_returns.mean()),
"std": float(fine_returns.std()),
},
{
"setting": f"Target-only baseline ({FEW_SHOT_INTERACTIONS} target)",
"mean": float(target_only_returns.mean()),
"std": float(target_only_returns.std()),
},
{
"setting": "PD baseline (no model)",
"mean": float(pd_target_returns.mean()),
"std": float(pd_target_returns.std()),
},
]Target-environment return on 20 held-out target initial states setting mean std Zero-shot (frozen source model) -6.050 8.540 Few-shot fine-tune (source + 400 target) -6.050 8.540 Target-only baseline (400 target) -6.050 8.539 PD baseline (no model) -4.587 6.162
Frozen, source-plus-target, and target-only MPC all round to ; their mean differences are below in this paired run. There is no appreciable adaptation gain here and no demonstrated source-pretraining benefit. This null result does not diagnose non-transferable features: the features are identical. Mixed-domain weighting, model mismatch, and search sensitivity would need separate tests.
The bars report mean target return. Error bars are the empirical standard deviation of the 20 evaluated episode returns, using denominator 20 (NumPy's default), not standard errors or confidence intervals. MPC episodes vary both initial state and prescribed candidate stream, so this spread does not isolate either source of variation. The three model-based means are nearly identical; PD has the higher mean.
Being explicit about what this experiment cannot establish is essential. The shift here is one scalar change in a low-dimensional, deterministic, fully observed system. It cannot speak to representation mismatch, partial observability, visual distribution shift, or high-dimensional action spaces. It cannot speak to whether some other pretrained architecture would have transferred better. It is an illustration of the protocol, not a claim about transfer in general.

Target adaptation uses 400 transitions shared by the two refits. Each target controller is evaluated for 20 episodes of 30 steps, or 600 transitions; four controllers consume 2400 target evaluation transitions in total. Those outcomes are not used to fit parameters or choose settings in this notebook. If outcomes are used for selection, label their interactions validation and obtain a fresh held-out test for an unselected performance estimate. The same reused score should not be presented as untouched test evidence.
The accounting below counts reference-simulator transition calls made by the experiment, including repeated evaluations of the same cases. It is not a count of unique observations or of physical deployments. Source-side evaluations are diagnostic comparisons at predeclared settings, not held-out evidence for a final controller chosen after inspecting their results. The target settings are fixed before their target outcomes are inspected. No separate validation collection is run. Model-rollout transitions, including true-dynamics calls used inside oracle search, belong to planning compute rather than this environment-interaction tally.
SOURCE_EVAL_PER_CONTROLLER = len(S0_eval) * EPISODE_HORIZON
interaction_accounting = {
"Source model training": PRETRAIN_INTERACTIONS,
"Source one-step diagnostics": len(S_test),
"Source PD evaluation": SOURCE_EVAL_PER_CONTROLLER,
"Source horizon sweep (5 settings, 2 controllers)": len(budget_sweep)
* 2
* SOURCE_EVAL_PER_CONTROLLER,
"Source predicted/actual comparison (3 actual runs)": len(comparison_set)
* SOURCE_EVAL_PER_CONTROLLER,
"Source fixed-policy ranking (6 actual runs)": len(candidates)
* SOURCE_EVAL_PER_CONTROLLER,
"Target adaptation records (shared by two refits)": len(S_adapt),
"Target evaluation (4 controllers)": len(transfer_results)
* TARGET_EVAL_TRANSITIONS,
}
assert interaction_accounting["Target evaluation (4 controllers)"] == 2400
assert sum(interaction_accounting.values()) == 18400
for bucket, count in interaction_accounting.items():
print(f"{bucket}: {count} simulator transitions")
print(
f"Total reference-environment interactions: {sum(interaction_accounting.values())}"
)
print("Separate validation transitions: 0")
print(
"MPC search: 640 candidate transition evaluations per decision in the sweep"
)
print("Wall-clock latency and memory footprint: not measured")Source model training: 3000 simulator transitions Source one-step diagnostics: 600 simulator transitions Source PD evaluation: 600 simulator transitions Source horizon sweep (5 settings, 2 controllers): 6000 simulator transitions Source predicted/actual comparison (3 actual runs): 1800 simulator transitions Source fixed-policy ranking (6 actual runs): 3600 simulator transitions Target adaptation records (shared by two refits): 400 simulator transitions Target evaluation (4 controllers): 2400 simulator transitions Total reference-environment interactions: 18400 Separate validation transitions: 0 MPC search: 640 candidate transition evaluations per decision in the sweep Wall-clock latency and memory footprint: not measured