Planning, Control, and Policy Evaluation › Zero-Shot, Few-Shot, and Cross-Domain Transfer

Michael BrenndoerferAugust 3, 20269 min read
World Models HandbookAll chapters
Part 11 of 12Evaluation and Understanding
Chapter 64Planning, Control, and Policy Evaluation › Zero-Shot, Few-Shot, and Cross-Domain Transfer
Reading section 6 of 9 · Zero-Shot, Few-Shot, and Cross-Domain Transfer · 9 minSection 6 of 9 · 9 min

Book contents

Section 6 of 9 · 9 min read

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 NfewN_{\text{few}}) 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 NfewN_{\text{few}} (number of target-environment transitions allowed) and KadaptK_{\text{adapt}} (number of adaptation steps). The target evaluation rollouts are separate from the NfewN_{\text{few}} 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 ff), 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 g=1.0g=1.0 to g=0.7g=0.7; 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 1/0.7−1≈43%1/0.7-1\approx43\% 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.

In[22]:
Code
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_HORIZON
Out[23]:
Console
Zero-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 −6.050-6.050 on the target cases, compared with the target PD mean of −4.587-4.587. Its source-environment mean was −4.612-4.612 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 Nfew=400N_{\text{few}} = 400 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 NfewN_{\text{few}} 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.

In[24]:
Code
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_fine
In[25]:
Code
mpc_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()),
    },
]
Out[26]:
Console
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 −6.050-6.050; their mean differences are below 0.0010.001 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.

Out[27]:
Visualization
Bar chart comparing zero-shot, few-shot, target-only, and PD baselines on the target environment.
Mean target return after changing actuator gain from 1.0 to 0.7. Frozen, source-plus-target, and target-only MPC use paired cases and candidate streams and have nearly identical means in this run. Both refits share 400 target transitions. Error bars show empirical episode-return standard deviations, not confidence intervals; the run does not demonstrate appreciable adaptation gain or source-pretraining benefit.

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.

In[28]:
Code
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")
Out[28]:
Console
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