zero2robot · Phase 4 · Capstonech4.3-serl · serl.py

Chapter 4.3

RL Post-TrainingHIL-SERL in Sim

By the end you can

  1. Assemble HIL-SERL from the pieces the learner already built: corrections (ch4.2) -> an AWAC offline prior (the primer, reusing ch2.2's twin-Q) -> online SAC fine-tuning (ch2.2) with the corrections kept in the replay (RLPD-style)
  2. Explain WHY corrections-as-prior buy sample efficiency: an offline prior extracted from the corrections starts the policy at a good return with ZERO online interaction, so online RL needs far fewer environment samples than starting from scratch
  3. Measure, don't assert, the sample efficiency: samples-to-threshold for HIL-SERL (primed) vs SAC-from-scratch (cold), with ch1.6 error bars (Wilson CI + a difference CI) — HIL-SERL clears the bar at ~0 online samples, scratch needs thousands
  4. Run the honest ablation (prior-alone vs from-scratch-online vs HIL-SERL) and read what each piece buys — on this free-tier task the corrections/prior deliver essentially all the efficiency and online fine-tuning HOLDS the prior; report that truthfully rather than overclaiming
  5. Tie the mechanism to the real thing: the same corrections->prior->online-fine-tune loop is how HIL-SERL/RLPD post-train real robot policies, and where the online phase pays off (harder tasks, the gated capstone suite)

See it work

live · P2

Sample efficiency — 0 vs ~10k online samples

Eval reach distance vs online environment samples, same task, one shared axis. HIL-SERL starts already below the reach threshold — the corrections-as-prior put it there with zero online interaction. From-scratch SAC must discover the reach from nothing and crawls to the same line over ~10k online samples. The horizontal gap is the win.

0.000.050.100.150.20random ≈ 0.176 mreach threshold 0.1 m≈10,000 online samples — the winHIL clears · 0scratch clears · 10k02k4k6k8k10k12konline environment samples →dist (m)prior · 0.06 m
HIL-SERL corrections prior + onlinefrom scratch no prior, no corrections
5,000 samples
samples-to-threshold · seeds 0, 1, 2N = 30 pooled rollouts / seed
seed 0seed 1seed 2HIL-SERL000from scratch10,00011,00010,000

HIL-SERL clears the bar at 0 online samples on every seed; from-scratch needs ~10,000–11,000. On the pooled eval, HIL-SERL's success (37% on seed 0) is at least from-scratch's (10%) on all three seeds, and the difference CI excludes zero on two of them (seed 0: +0.05..+0.46 · significant).

Online-sample budget 5,000. HIL-SERL cleared the 0.1 metre reach threshold at 0 online samples — the corrections-as-prior put it there. From-scratch SAC has not cleared it yet; it needs about 5,000 more online samples. The win is the sample axis — about 10,000 online samples saved — not a stronger final policy: both arms finish near the same distance (0.065 m versus 0.086 m).

The win is sample efficiency from the corrections prior, not a stronger final policy. On this small dense-reward task both arms top out near the same short-horizon SAC ceiling (HIL best 0.065 m, scratch best 0.086 m), so online fine-tuning holds the prior rather than beating it — and the HIL curve even wobbles up during the offline-to-online transition, which is why we keep the corrections in the replay and return the best checkpoint (the prior). On harder tasks — the real HIL-SERL paper's contact-rich manipulation and the gated capstone suite — the prior is far from optimal and the online phase is where the policy actually gets good. Real measured curve + scalars from serl.py (60 corrections, seed 0 curve, seeds 0, 1, 2 ledger, cpu); poster reads with JS off.

Open in Colabsoon

Free-tier notebook — the button goes live when the course repository is published.

Everything you built, in one loop

This is the convergence chapter. Trace what you already own:

  • A base policy by cloning demos (BC, 1.1) — and the covariate-shift death you measured when it drifted into states no demo covered.
  • Corrections (4.2): you rolled out that policy, caught it failing, and had an expert — the scripted stand-in for your own teleop hand — label the states it actually visits. On-policy corrections, exactly where the policy is weak.
  • An offline prior (the primer): given a fixed log, don't just clone it — fit a Q-function and extract the above-average actions with advantage-weighted regression (AWAC). The reward BC threw away, put to work.
  • Off-policy RL (2.2): SAC, with its replay buffer, twin-Q critics, target networks, and squashed-Gaussian policy — learning from every transition many times.

HIL-SERL (Human-in-the-Loop Sample-Efficient RL, Luo et al.) is those four things composed into one loop. Take the corrections. Turn them into an offline prior with AWAC. Then fine-tune that prior online with SAC, keeping the corrections in the replay buffer the whole time — so the policy reaches a good return in far fewer online environment samples than RL starting from scratch. Sample-efficiency is the whole point, and this chapter measures it.

Why "sample-efficient" is the right obsession: online RL's currency is environment interaction, and on a real robot every environment step is a real second of real hardware wearing out. RL-from-scratch on a manipulation task can cost millions of steps. If your correction data can buy back most of that, that is the difference between "trains overnight on a robot in a lab" and "never."

The setup, honestly scaled

We run the real algorithm on the public pusher_reach env (2.2's — dense reward, a 2-link arm reaching a seeded target) so every line is readable and reproducible on a laptop. The map's capstone suite is hidden-seed graded (grader/hidden_seeds); the leaderboard number is the gated capstone (4.4). This chapter teaches the mechanism; the leaderboard scores it. Nothing here claims a strong trained robot — it is the algorithm, shown honestly at free-tier scale.

The corrections are scripted-expert transitions — the analytic IK reacher driving the task to the goal, our offline stand-in for a human teleoperator taking over on the policy's failures (4.2's mechanism, exactly). This is the only data the offline prior ever sees.

serl.py#datasha256:9f63e451fa…
# THE CORRECTIONS. ch4.2's mechanism: the scripted expert (reach_action, the# offline stand-in for a human teleoperator) takes over on the task and drives it# to the goal; we record its full transitions — a human-intervention log, exactly# HIL-SERL's input. This is the ONLY data the offline prior sees; the same# transitions are later pre-loaded into the online replay and STAY there (RLPD).def collect_corrections(num_episodes: int) -> dict[str, torch.Tensor]:    env = PusherReachEnv()    cols: dict[str, list] = {k: [] for k in ("obs", "action", "reward", "next_obs", "terminated")}    returns = []    for ep in range(num_episodes):        obs = env.reset(seed=args.seed + ep)  # episode ep uses a distinct, reproducible seed        ep_return, done = 0.0, False        while not done:            action = reach_action(env)  # the correction: what the expert/teleop would do HERE            next_obs, reward, done, info = env.step(action)            for key, val in zip(cols, (obs, action, reward, next_obs, float(info["terminated"]))):                cols[key].append(val)            obs = next_obs            ep_return += reward        returns.append(ep_return)    data = {k: torch.tensor(np.array(v), dtype=torch.float32, device=device) for k, v in cols.items()}    data["reward"], data["terminated"] = data["reward"].unsqueeze(1), data["terminated"].unsqueeze(1)    print(f"corrections: {num_episodes} episodes / {len(data['obs'])} transitions  "          f"expert return {np.mean(returns):.2f} (the prior's whole data)")    return data

Build the prior: corrections → AWAC

The prior is the primer's AWAC, run on the corrections. A twin-Q critic learns the reward-to-go on the fixed correction transitions; then the policy is extracted by advantage-weighted regression — behavior cloning's MSE onto the correction action, but each sample scaled by exp(A/beta), where A = Q(s, a_corr) - Q(s, pi(s)) is how much better the correction was than what the current policy would do. This anchors the policy to actions the corrections actually contain (no out-of-distribution extrapolation, the failure the primer's --naive showed) and warm-starts both the actor and the critics that SAC will continue from.

serl.py#priorsha256:7a2a96fc15…
# THE OFFLINE PRIOR (the primer's AWAC, on the corrections). A twin-Q critic learns# the reward-to-go on the fixed corrections; the policy is extracted by advantage-# weighted regression — ch1.1's BC loss on the correction action, each sample scaled# by exp(A/beta), A = Q(s, a_corr) - Q(s, pi(s)). This anchors the policy to actions# the corrections contain (no OOD extrapolation) and warm-starts BOTH the actor AND# the critics online SAC continues from — the "corrections as prior."def train_prior(corr: dict[str, torch.Tensor]):    torch.manual_seed(args.seed)  # net init + sampling reproducible regardless of upstream RNG use    actor = Actor(args.hidden_dim).to(device)    q1, q2 = Critic(args.hidden_dim).to(device), Critic(args.hidden_dim).to(device)    q1_targ = copy.deepcopy(q1).requires_grad_(False)    q2_targ = copy.deepcopy(q2).requires_grad_(False)    actor_opt = torch.optim.Adam(actor.parameters(), lr=args.lr)    critic_opt = torch.optim.Adam(list(q1.parameters()) + list(q2.parameters()), lr=args.lr)    o, a, r, no, t = (corr[k] for k in ("obs", "action", "reward", "next_obs", "terminated"))    n = len(o)    for step in range(args.prior_steps):        idx = torch.randint(0, n, (args.batch_size,))        obs, action, reward, next_obs, term = o[idx], a[idx], r[idx], no[idx], t[idx]        with torch.no_grad():  # critic: TD toward r + gamma * min_targ Q(s', pi(s'))            next_action, _, _ = actor.sample(next_obs)            min_next_q = torch.min(q1_targ(next_obs, next_action), q2_targ(next_obs, next_action))            target_q = reward + args.gamma * (1.0 - term) * min_next_q        critic_loss = F.mse_loss(q1(obs, action), target_q) + F.mse_loss(q2(obs, action), target_q)        critic_opt.zero_grad()        critic_loss.backward()        critic_opt.step()        _, _, pred = actor.sample(obs)  # deterministic mean action        with torch.no_grad():  # AWAC advantage weight            q_data = torch.min(q1(obs, action), q2(obs, action))            value = torch.min(q1(obs, pred), q2(obs, pred))            weight = torch.exp((q_data - value) / args.beta).clamp(max=args.weight_clip)        actor_loss = (weight * (pred - action).pow(2).mean(1, keepdim=True)).mean()        actor_opt.zero_grad()        actor_loss.backward()        actor_opt.step()        soft_update(((q1, q1_targ), (q2, q2_targ)))        if args.rerun and step % 100 == 0:            rr.set_time("prior_step", sequence=step)            rr.log("prior/critic_loss", rr.Scalars([critic_loss.item() / 2]))    return actor, q1, q2, q1_targ, q2_targ  def soft_update(pairs):    """Polyak-average targets a hair toward online (ch2.2)."""    with torch.no_grad():        for online, targ in pairs:            for p, tp in zip(online.parameters(), targ.parameters()):                tp.mul_(1.0 - args.tau).add_(args.tau * p)

Note one small but load-bearing detail in the actor: we initialize log_std small, so a freshly-primed policy is near-deterministic. A wide-open stochastic policy would explore off the prior's good manifold on the very first online steps and unlearn it — the offline-to-online collapse. Starting quiet is half the battle of not throwing the prior away.

Fine-tune online: SAC with the corrections still in the replay

Now the online phase. It is ch2.2's SAC, unchanged — one env step, one gradient step on a replay batch — with two differences that make it HIL-SERL:

  1. The actor and critics start from the prior, not from random.
  2. The replay buffer is pre-loaded with the corrections, and they never get evicted (RLPD-style). Every online gradient step samples a mix of fresh online transitions and the original corrections — the corrections keep pulling the critic toward known-good actions while online data refines it.

The same loop runs the from-scratch baseline: identical algorithm, but cold nets and an empty replay with a random warmup. The only differences between the two arms are the starting weights and the replay's initial contents. That is the experiment.

serl.py#onlinesha256:2fe6c52ac7…
# ONE online SAC loop, called TWICE: warm (HIL-SERL — primed nets + corrections# pre-loaded in replay) and cold (from-scratch — fresh nets, empty replay, random# warmup). Identical algorithm; the ONLY differences are the starting weights and# the replay's initial contents. It returns the sample-efficiency curve, the FIRST# online step it clears the threshold, and the BEST checkpoint over the run (ch4.2's# return-the-best: online can dip before recovering, and the prior may be best).def run_online(actor, q1, q2, q1_targ, q2_targ, buffer, total_steps, primed: bool, tag: str):    actor_opt = torch.optim.Adam(actor.parameters(), lr=args.lr)    critic_opt = torch.optim.Adam(list(q1.parameters()) + list(q2.parameters()), lr=args.lr)    env = PusherReachEnv()    episode = 0    obs = env.reset(seed=100_000 + args.seed + episode)    episode += 1    curve, sts = [], None    best_dist, best_state = float("inf"), copy.deepcopy(actor.state_dict())    if primed:  # evaluate the prior at ZERO online samples — the head start corrections buy        d0, _, _ = evaluate(actor, args.eval_episodes)        curve.append((0, round(d0, 5)))        best_dist = d0        sts = 0 if d0 < args.threshold else None    for step in range(1, total_steps + 1):        if not primed and step <= args.learning_starts:  # cold start: random warmup fills the buffer            action = rng.uniform(-1.0, 1.0, size=act_dim).astype(np.float32)        else:            with torch.no_grad():                action, _, _ = actor.sample(torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0))            action = action[0].cpu().numpy()        next_obs, reward, done, info = env.step(action)        buffer.add(obs, action, reward, next_obs, info["terminated"])        obs = env.reset(seed=100_000 + args.seed + episode) if done else next_obs        episode += 1 if done else 0        if primed or step > args.learning_starts:  # one SAC gradient step per env step            sac_update(actor, q1, q2, q1_targ, q2_targ, actor_opt, critic_opt, buffer)        if step % args.eval_interval == 0 or step == total_steps:            eval_dist, _, _ = evaluate(actor, args.eval_episodes)            curve.append((step, round(eval_dist, 5)))            if eval_dist < best_dist:                best_dist, best_state = eval_dist, copy.deepcopy(actor.state_dict())            if sts is None and eval_dist < args.threshold:                sts = step            if args.rerun:                rr.set_time("online_step", sequence=step)                rr.log(f"eval/{tag}/dist", rr.Scalars([eval_dist]))            print(f"  [{tag}] step {step:6d}/{total_steps}  eval_dist {eval_dist:.4f}m  best {best_dist:.4f}m")    actor.load_state_dict(best_state)  # return the BEST policy over the run (prior included)    return curve, sts, best_dist  def sac_update(actor, q1, q2, q1_targ, q2_targ, actor_opt, critic_opt, buffer):    """One SAC gradient step on a replay batch (ch2.2), fixed entropy temperature.    For HIL-SERL the batch mixes fresh online transitions with the pre-loaded    corrections, which keep pulling the critic toward known-good actions."""    obs, action, reward, next_obs, terminated = buffer.sample(args.batch_size)    with torch.no_grad():        next_action, next_logp, _ = actor.sample(next_obs)        min_next_q = torch.min(q1_targ(next_obs, next_action), q2_targ(next_obs, next_action)) - args.alpha * next_logp        target_q = reward + args.gamma * (1.0 - terminated) * min_next_q    critic_loss = F.mse_loss(q1(obs, action), target_q) + F.mse_loss(q2(obs, action), target_q)    critic_opt.zero_grad()    critic_loss.backward()    critic_opt.step()    new_action, logp, _ = actor.sample(obs)    actor_loss = (args.alpha * logp - torch.min(q1(obs, new_action), q2(obs, new_action))).mean()    actor_opt.zero_grad()    actor_loss.backward()    actor_opt.step()    soft_update(((q1, q1_targ), (q2, q2_targ)))

What we measure: samples-to-threshold

The headline is a curve: eval reach-distance versus online environment samples, for both arms. We read off samples-to-threshold — the first online step at which the eval mean final distance drops below the bar (--threshold, 0.10 m; random leaves ~0.176 m).

The result, stated truthfully. HIL-SERL clears the threshold at zero online samples on every seed — because the offline prior, built entirely from the corrections with no environment interaction, is already past the bar (eval distance 0.061 / 0.074 / 0.069 m across seeds 0–2, well under the 0.10 m threshold). From-scratch SAC has to discover the reach from nothing and needs ~10,000–11,000 online samples to reach the same line. That horizontal gap on the sample axis is the sample efficiency, and it is large and seed-robust. On the pooled held-out eval, HIL-SERL's success rate is at least from-scratch's on all three seeds, and the difference interval excludes zero on two of them.

And the honest part, which is itself the lesson: on this small dense-reward task, online fine-tuning holds the prior rather than beating it. The prior already sits near what SAC can reach on this task in a short horizon (2.2's own SAC needs ~18k steps just to solve it), so there is little headroom left for online RL to add — and the offline-to-online transition can wobble the policy before it recovers, which is exactly why we (a) keep the corrections in the replay to anchor it and (b) return the best checkpoint over the run (4.2's return-the-best idiom), prior included. On harder tasks — the real HIL-SERL paper's contact-rich manipulation, and the gated capstone suite — the prior is far from optimal and the online phase is where the policy actually gets good. Here, the corrections do the work, and the honest headline is the sample axis.

The ablation: what each piece buys

serl.py#reportsha256:35d82e158b…
# Build the prior from corrections, run BOTH arms, grade with error bars. The# headline is sample-efficiency: samples-to-threshold, HIL-SERL vs scratch.corr = collect_corrections(args.corr_episodes)prior_actor, q1, q2, q1_targ, q2_targ = train_prior(corr)prior_dist, prior_k, _ = evaluate(prior_actor, args.eval_episodes, args.n_seeds)print(f"\noffline prior (AWAC on corrections): eval_dist {prior_dist:.4f}m  (0 online samples)") hil_buffer = ReplayBuffer(args.buffer_size)hil_buffer.preload(corr)  # the corrections STAY in the replay (RLPD)print(f"\nHIL-SERL: primed + {hil_buffer.size} corrections in replay, fine-tune {args.hil_steps} online steps")hil_curve, hil_sts, hil_best = run_online(prior_actor, q1, q2, q1_targ, q2_targ, hil_buffer, args.hil_steps, True, "hil") torch.manual_seed(args.seed)  # from-scratch SAC: fresh nets, empty replay, random warmupscr_actor = Actor(args.hidden_dim).to(device)s1, s2 = Critic(args.hidden_dim).to(device), Critic(args.hidden_dim).to(device)s1_targ, s2_targ = copy.deepcopy(s1).requires_grad_(False), copy.deepcopy(s2).requires_grad_(False)print(f"\nfrom-scratch SAC: cold, empty replay, {args.scratch_steps} online steps")scr_curve, scr_sts, scr_best = run_online(scr_actor, s1, s2, s1_targ, s2_targ, ReplayBuffer(args.buffer_size), args.scratch_steps, False, "scratch") n_pool = args.n_seeds * args.eval_episodes_, hil_pk, _ = evaluate(prior_actor, args.eval_episodes, args.n_seeds)_, scr_pk, _ = evaluate(scr_actor, args.eval_episodes, args.n_seeds)gap = diff_ci(hil_pk, n_pool, scr_pk, n_pool)  # HIL - scratchgap_real = gap[0] > 0 or gap[1] < 0 print(f"\n[headline: sample-efficiency] samples-to-threshold (eval_dist < {args.threshold}m):")print(f"  HIL-SERL (primed) : {hil_sts if hil_sts is not None else '>' + str(args.hil_steps)} online samples  (prior clears it at 0; best {hil_best:.4f}m)")print(f"  SAC from scratch  : {scr_sts if scr_sts is not None else '>' + str(args.scratch_steps)} online samples  (best {scr_best:.4f}m)")if hil_sts == 0 and scr_sts:    print(f"  -> corrections-as-prior clear the bar with ZERO online samples; scratch needs ~{scr_sts}. That gap IS the sample efficiency.") print(f"\n[ablation: what each piece buys] pooled N={n_pool} held-out rollouts each:")for name, k, dist in (("prior alone (offline, 0 online)", prior_k, prior_dist),                      ("scratch (online, no prior/corr)", scr_pk, scr_best),                      ("HIL-SERL (prior+corr+online)", hil_pk, hil_best)):    print(f"  {name:<34s} success {k}/{n_pool}={k/n_pool:.2f} {tuple(round(x,2) for x in wilson_ci(k,n_pool))}  best_dist {dist:.4f}m")print(f"  diff CI (HIL-SERL - scratch): {gap[0]:+.2f}..{gap[1]:+.2f}  ({'SIGNIFICANT' if gap_real else 'not significant at this N'})")print(f"  (random ~{RANDOM_DIST}m; prior & HIL-SERL sit near this task's short-horizon SAC ceiling, so online fine-tuning")print("   HOLDS the prior rather than beating it — the corrections buy the efficiency. Online post-training on harder")print("   tasks is scored on the GATED capstone suite [ch4.4, hidden-seed].)") if args.rerun:    for name, k in (("prior", prior_k), ("scratch", scr_pk), ("hil", hil_pk)):        rr.log(f"final/{name}/success_rate", rr.Scalars([k / n_pool])) torch.save(prior_actor.state_dict(), args.out / "serl_actor.pt")  # HIL-SERL best checkpointtorch.save(scr_actor.state_dict(), args.out / "scratch_actor.pt")  # from-scratch SAC baseline (site ONNX demo)metrics = {    "threshold": args.threshold, "corr_episodes": args.corr_episodes,    "prior_eval_dist": round(prior_dist, 5), "prior_success_rate": round(prior_k / n_pool, 6),    "hil_steps_to_threshold": hil_sts, "scratch_steps_to_threshold": scr_sts,    "hil_best_dist": round(hil_best, 5), "scratch_best_dist": round(scr_best, 5),    "hil_success_rate": round(hil_pk / n_pool, 6), "scratch_success_rate": round(scr_pk / n_pool, 6),    "diff_ci_lo": round(gap[0], 6), "diff_ci_hi": round(gap[1], 6), "gap_significant": bool(gap_real),    "hil_curve": hil_curve, "scratch_curve": scr_curve, "n_pooled": n_pool,    "seed": args.seed, "smoke": bool(args.smoke),}(args.out / "metrics.json").write_text(json.dumps(metrics, indent=2, sort_keys=True) + "\n")print(f"\nmetrics: {args.out / 'metrics.json'}")if args.rerun:    print(f"recording: {args.out / 'serl.rrd'} — open it with: rerun {args.out / 'serl.rrd'}")

Read the three arms together. Prior aloneHIL-SERLfrom-scratch at the same small online budget (measured pooled success 0.37 / 0.33 / 0.33 for the prior and for HIL-SERL, versus 0.10 / 0.07 / 0.27 for scratch across seeds 0–2). The corrections-as-prior are the sample-efficiency win; online fine-tuning maintains it. If a piece did not help on the free tier, we report that — not paper over it (the ch2.1 spike doctrine). The mechanism is what transfers: corrections become a prior, the prior gives you a running start, and RL post-training refines from there.

Wall-clock (free tier)

wall-clock · rendered from wallclock.csvone source · every tier
cpu-laptopexpected wall-clock on cpu-laptop: ~9.67 min (measured)measured
mpswall-clock on mps: not yet measuredpending
t4wall-clock on t4: not yet measuredpending
4090wall-clock on 4090: not yet measuredpending

Read the real thing

The single-file HIL-SERL you just built is the mechanism with the scaffolding stripped away. Two papers are the natural graduation. The HIL-SERL paper (Luo et al., "Precise and Dexterous Robotic Manipulation via Human-in-the-Loop RL") is the grown-up version of this exact loop: a real human teleoperator in the loop online instead of a scripted stand-in, a learned reward classifier instead of a dense analytic reward, and contact-rich tasks where the prior is far from optimal and the online phase genuinely earns its keep. RLPD (Ball et al., "Efficient Online RL with Offline Data") is where the corrections-in-replay trick comes from, plus the stabilizers this file leaves out — explicit symmetric sampling, a high update-to-data ratio, and LayerNorm critics — which are what let online fine-tuning improve a prior on hard tasks rather than merely hold it. (The exact upstream commits are pinned by the read-the-real-thing build step.)

Exercises

Two, both predict-then-run and both graded on a seed-robust signal rather than one lucky run.

  • Predict the sample-efficiency gap (ex1_predict_sample_efficiency.py): does HIL-SERL clear the threshold in fewer online samples than from-scratch, on every one of seeds 0–2? Predict before you run.
  • Investigate the corrections (ex2_investigate_corrections.py): starve the correction data and watch the prior — and HIL-SERL's head start — weaken. The corrections are the prior; the prior is the sample efficiency.

Practice

practice · candidate exercisesdrafted by the exercise generator, pending human promotion. Answers reveal only after you predict — honor system.

  1. predict-then-run

    Exercise 1

    SUGGESTED exercise candidate (humans promote) — predict-then-run, ch4.3 HIL-SERL.

    Objective tested: the chapter's headline — SAMPLE EFFICIENCY. HIL-SERL starts from an offline prior built from the corrections, so it should clear the reach THRESHOLD in FAR FEWER online environment samples than SAC-from-scratch, which has to discover the reach from nothing. And the RL-doctrine reading underneath (ch2.1 spike, H2): grade the signal ACROSS seeds, not on one run.

    THE SETUP. Both arms run the SAME online SAC loop on pusher_reach. HIL-SERL starts warm — actor + critics pre-trained on the corrections (AWAC), corrections pre-loaded in the replay. From-scratch starts cold — fresh nets, empty replay, random warmup. We measure samples-to-threshold: the first online env step at which the eval mean final distance drops below --threshold (0.10 m; random ~0.176 m).

    THE QUESTION. Does HIL-SERL reach the threshold in fewer online samples than from-scratch SAC, on EVERY one of seeds 0, 1, 2?

    PREDICT before you run: (a) yes, HIL-SERL clears the bar with far fewer online samples on all three seeds (the corrections-as-prior are a head start); (b) they need about the same (the prior does not really help); (c) from-scratch gets there first. Write your choice and one sentence of why in PREDICTION.

    Then run this file. It runs the full artifact on each of seeds 0, 1, 2 (each run is bit-reproducible on CPU) and prints per-seed samples-to-threshold for both arms. The variance you read is ACROSS seeds — which is why HIL-SERL, like all RL, is graded on a multi-seed signal.

    Estimated learner time: ~10 minutes (three full runs; each trains the prior, the HIL-SERL fine-tune, and the from-scratch baseline).

    Run it locally:

    pytest curriculum/phase4_capstone/ch4.3_serl/exercises/suggested/checks.py -k ex1
  2. hyperparameter-investigation

    Exercise 2

    SUGGESTED exercise candidate (humans promote) — investigation, ch4.3 HIL-SERL.

    Objective tested: WHERE the sample efficiency comes from. The chapter's measured claim is that the CORRECTIONS-AS-PRIOR do the heavy lifting — so the amount of correction data should move the prior's quality, and with it HIL-SERL's head start. This is an investigation: you change ONE knob (how many correction episodes feed the prior) and read the effect on the offline prior and on HIL-SERL's samples-to-threshold.

    THE KNOB. --corr_episodes controls how many scripted-expert correction episodes build the prior. We compare a STARVED prior (few corrections) against the DEFAULT (many). More corrections -> a better-covered offline prior -> it should clear the threshold from closer to zero online samples.

    PREDICT before you run: with far fewer corrections, does the offline prior (a) get clearly worse (higher eval distance, later or no threshold crossing), (b) stay about the same (the reach generalizes from a handful of episodes), or (c) get better? Write your choice + one sentence in PREDICTION.

    Then run this file. It runs the artifact at two correction budgets on seeds 0, 1 and prints the prior's eval distance and HIL-SERL's samples-to-threshold for each.

    RL-doctrine note (ch2.1 spike): the seed-robust, gated claim is only that the DEFAULT (full) corrections build a prior that clears the threshold. The direction and size of the starvation effect is left for YOU to read and interpret — a free-tier trend, not a guaranteed-monotone law.

    Estimated learner time: ~12 minutes (four runs).

    Run it locally:

    pytest curriculum/phase4_capstone/ch4.3_serl/exercises/suggested/checks.py -k ex2

Colophon · provenance

The code on this page is not pasted — each panel is included by region straight fromserl.py, and these fingerprints are its sha256, the same ones check_prose_code_drift re-checks on every PR. Edit a shown region without re-rendering and CI turns red.

#setup
986219171467d0ff2c1a8ab11d613d382a947245a2fafd762b39e7cf324c0595
#stats
b7d3a0697fb4c48448703e44a3b64b2c41b94d25a8f37670c831ccfb8d6a5f60
#model
c6272bff804871a12537aec8c1500c13c2513a3947864a09447d0290c3cc9056
#data
9f63e451fa216bbc3da3bf0bbe0e9e36709ea01f520b5ea35430aae596d92277
#replay
3fd4bf5802ace1d939993d39bcec2ddc31238c85a5a0f264ccb48a98f8b24e83
#prior
7a2a96fc154c3bc6845c49fd2bcfcf01067bff78a9e78f2f2f46974843f36641
#online
2fe6c52ac76d1e6807a8ebb70956a53d71f7f191297e6c5958e7c1f7e29383b3
#eval
7aef2649da3e1aa397ec18a23c53c00bfdcba1e340c31bd24e94a6cf326ec212
#report
35d82e158be9eb6d0cc3db40d5b58c7a858a08703c451656fcd0157a7f15d57d