The AY-Robots failure-mode index at /fix, listing common robot policy failures such as policy freezes mid-motion and policy only works in one setup, each with its fix
DAggerImitation LearningSO-100LeRobotPolicy Debugging

DAgger: How to Fix an SO-100 Policy That Drifts

AY-Robots ResearchAugust 23, 202617 min read

Behaviour cloning errors compound quadratically in the episode length. DAgger and HG-DAgger fix that by training on the states your policy actually visits. Here is the loop on an SO-100.

Your policy works for two seconds. The arm leaves home, tracks toward the cube, and the wrist ends up a centimetre off. From there everything it sees is new, the gripper closes on air, the run is over. Nothing in the training data showed the arm what to do from a centimetre off, so the policy has nothing to say.

That is not a bug in your training run. It is the defining failure of behaviour cloning, it has a published bound, and the fix predates every VLA: DAgger, short for Dataset Aggregation, from Ross, Gordon and Bagnell at AISTATS 2011. Below: the bound, the human-gated variant, and the loop on an SO-100.

What you need to know

  • Behaviour cloning on expert states carries a worst-case cost bound growing with T squared, the episode length. DAgger reduces it to linear in T.
  • The fix is not more demonstrations. It is expert labels on the states your own policy visits when it goes wrong.
  • A real arm cannot label states the learner is driving through, so robotics uses HG-DAgger: the human takes over when failure looks imminent, and only those segments become data.
  • LeRobot 0.6.1 ships this as lerobot-rollout --strategy.type=dagger, and needs a leader arm whose motors can be driven under software.
  • Budget from the literature: collect corrections until they are a third of your dataset in samples, retrain, repeat three times.
  • One action step costs 20 to 485 ms here. A takeover crossing the public internet arrives late.

Why behaviour cloning drifts, with the actual bound

Behaviour cloning is supervised learning with one twist that breaks supervised learning. You fit observation to action on pairs from a human driving the arm, then deploy. The deployed policy picks its own next state, so training happens on the expert's state distribution and testing on the policy's own. Those coincide only if it never errs.

Ross and Bagnell put a number on that gap in 2010, restated as Theorem 2.1 of the DAgger paper. With per-step error epsilon under the expert's state distribution and cost bounded in the unit interval, over an episode of length T the total cost can exceed the expert's by T squared times epsilon. The paper calls the bound tight.

The two bounds side by side

Behaviour cloning: J(pi) ≤ J(pi*) + T2ε, with ε measured under the expert's state distribution.

DAgger: after Õ(uT) iterations some policy in the sequence satisfies J(pi) ≤ J(pi*) + uTεN + O(1), for εN the loss of the best policy in hindsight and u a bound on what one wrong action costs in cost-to-go. Linear in T, not quadratic.

Episode length TAt 30 HzBC worst case, T squared times epsilonDAgger worst case, u times T times epsilon
60 steps2 s360.6
300 steps10 s9003
900 steps30 s81009
1800 steps60 s3240018

Published bounds at epsilon = 0.01 and u = 1, not a measurement of your arm, and the two epsilons are not the same quantity: the behaviour cloning one is measured under the expert's state distribution, the DAgger one under the distributions the learner itself induced. Cost is bounded in the unit interval, so the behaviour cloning column saturates at T; only its growth rate still says anything. Doubling the horizon quadruples the behaviour cloning worst case and doubles the DAgger one, which is why long-horizon tasks dominate pages like policy only works in one setup and policy freezes mid-motion.

DAgger in one page

  1. 1
    Start from demonstrations

    Collect a normal dataset by teleoperation and train the first policy with plain behaviour cloning. This is the iteration where beta equals 1: the expert drives.

  2. 2
    Roll out the mixed policy

    At iteration i the system executes pi_i = beta_i * expert + (1 - beta_i) * learner, beta_i being the probability of taking the expert's action.

    python
    # DAgger, Ross, Gordon and Bagnell, AISTATS 2011, Algorithm 3.1
    D = []
    pi_hat = init_policy()
    
    for i in range(1, N + 1):
        beta = 1.0 if i == 1 else 0.0          # the parameter-free variant
        pi_i = lambda s: expert(s) if random() < beta else pi_hat(s)
    
        states = rollout(pi_i, horizon=T)      # states the LEARNER visits
        D_i = [(s, expert(s)) for s in states] # labels the EXPERT gives
    
        D = D + D_i                            # aggregate, never replace
        pi_hat = train(D)
    
    return best_on_validation(pi_hat_history)
  3. 3
    Label the visited states

    Record the states the rollout passed through and the action the expert would have taken in each. That asymmetry is the whole idea.

  4. 4
    Aggregate, do not replace

    Union the new set into the running dataset; every earlier round stays in. That is where the name comes from.

  5. 5
    Retrain from the aggregate

    Train the next policy on all of D, then return to step 2. Keep the checkpoint that scores best on validation.

Any beta schedule works as long as its running average goes to zero, and the paper reports that the simplest choice, beta = 1 on the first iteration and 0 after, often performs best in practice. On Super Tux Kart the controller stopped falling off the track after 15 iterations, while the supervised baseline did not improve as more expert laps were added.

The step that does not survive contact with a real arm

Step 3 asks the expert to label every state the rollout visits, including states the policy is driving through right now. In a racing game you pause and move a joystick. On an SO-100 you cannot: the arm is moving, the leader is not tracking the follower, and the label you record is what the human would have done a few hundred milliseconds ago.

HG-DAgger: the human holds the gate

HG-DAgger, from Kelly, Sidrane, Driggs-Campbell and Kochenderfer, keeps the aggregation and drops the mixing. HG stands for human-gated: the novice drives, the human watches, and when things go wrong the human takes over. Only the timesteps where the human has control are recorded.

Two things follow. Labels stay clean, because the human produces a continuous trajectory instead of annotating a state they do not control. And the data is recovery data: it starts where the policy got into trouble and ends where the human considers it salvaged. The same run yields a risk threshold learned from where the human actually intervened; rollouts started inside the resulting permissible set had a mean collision rate 12 times lower than those started outside.

The AY-Robots SO-100 hub page linking to data collection, imitation learning and LeRobot guides for the arm
Correction rounds run on the same recording and training stack as the base dataset.
MethodWho drivesWhat gets recordedWhere it costs you
DAgger (Ross et al., 2011)Expert and policy mixedEvery visited stateLabels states nobody drives
SafeDAgger (Zhang and Cho, 2016)Policy, safety net switchesExpert segmentsA safety classifier
EnsembleDAgger (Menda et al., 2018)Policy, gated by varianceExpert segmentsThresholds you pick
HG-DAgger (Kelly et al., 2019)Policy, human takes overHuman segments onlyReacting in time
IWR (Mandlekar et al., 2020)Policy, human takes overBoth, corrections upweightedA balanced sampler
ThriftyDAgger (Hoque et al., 2021)Policy, gated by novelty and riskExpert segmentsA risk estimate
HIL-SERL (Luo et al., 2024)Policy, human interrupts RLTransitions, learned rewardA reward classifier
RaC (Hu et al., 2025)Policy, human takes overRecovery, then correctionOperator discipline
Correction rounds instead of more demonstrations
Advantages
  • Targets the states your policy actually fails in. More demonstrations of the happy path never visit them.
  • Recovery is a skill. A policy that has seen a retry can retry; one trained on clean demos cannot.
  • Cheap per round: you record only while the arm is in trouble.
  • Mandlekar et al. report 87.3% on a simulated threading task against 76.7% for an equal budget of fresh demos.
  • Model-agnostic: the same rounds feed ACT, SmolVLA, GR00T N1.7 or Pi0.5.
Trade-offs
  • Every round needs a human at the arm for the whole rollout, costlier per minute than clean teleoperation.
  • You retrain after every round: 3 to 6 hours and 4 to 12 USD each on the A100 or H100 tier.
  • Corrections from a barely working policy are near useless; the published base policies sat at 52 to 58 percent.

Running the loop on an SO-100 with LeRobot

LeRobot ships this as a rollout strategy. In 0.6.1 on PyPI, released 2026-08-03, lerobot-rollout takes --strategy.type with five registered values, base, sentry, highlight, episodic and dagger, and --inference.type with two, sync and rtc. Pin the version: 0.5.1 has no lerobot-rollout entry point, the command first appears in 0.6.0 of 2026-07-06.

bash
pip install "lerobot==0.6.1"

# confirm the registered strategy, robot and teleoperator names on your own install
lerobot-rollout --help
The docs list the compatible teleoperator families as so_leader, bi_so_leader and bi_openarm_mini, but so_leader is not a registered CLI value in 0.6.1. The registry exposes so100_leader and so101_leader, and both decorate the same config class, so either name gives you the actuated SOLeader whose non-empty feedback_features the dagger handover checks for.
  1. 1
    Record the base dataset

    An ordinary teleoperated recording. Defaults are 60 s per episode, 60 s reset, 50 episodes. The recording walkthrough covers the camera and calibration setup that must stay identical across rounds.

    bash
    lerobot-record \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.id=my_follower \
      --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30} }" \
      --teleop.type=so100_leader \
      --teleop.port=/dev/ttyACM1 \
      --teleop.id=my_leader \
      --dataset.repo_id=me/cube-v0 \
      --dataset.num_episodes=50 \
      --dataset.single_task="Pick up the cube and put it in the box"
  2. 2
    Train the base policy

    ACT is the sensible start, for reasons below. The training walkthrough and the ACT on SO-100 guide have the hyperparameters.

    bash
    lerobot-train \
      --dataset.repo_id=me/cube-v0_20260823_101500 \
      --policy.type=act \
      --output_dir=outputs/train/act_cube_v0 \
      --job_name=act_cube_v0 \
      --policy.device=cuda \
      --wandb.enable=true
  3. 3
    Run the policy with a human on the gate

    The policy drives; you pause when failure looks imminent; the leader powers up onto the follower's pose; you take over with torque released, teleoperate back to a familiar state, finish the sub-task, hand control back. Nothing is recorded during the pause. Space pauses, tab starts and stops a correction, escape ends the session. Use the same leader you recorded round zero with.

    bash
    lerobot-rollout --strategy.type=dagger \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.id=my_follower \
      --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30} }" \
      --teleop.type=so100_leader \
      --teleop.port=/dev/ttyACM1 \
      --teleop.id=my_leader \
      --policy.path=outputs/train/act_cube_v0/checkpoints/last/pretrained_model \
      --dataset.repo_id=me/rollout_cube_hil_r1 \
      --dataset.single_task="Pick up the cube and put it in the box" \
      --dataset.fps=30 \
      --strategy.record_autonomous=true \
      --duration=1800
    
    # for Pi0, Pi0.5 or SmolVLA add real-time chunking so the arm does not stutter:
    #   --inference.type=rtc
  4. 4
    Merge round zero with round one

    A first-class merge operation exists, so no stitching parquet by hand. It refuses unless fps, robot_type and the feature set match, and that is where a dagger dataset trips: it carries an extra boolean intervention column your demonstrations do not have.

    bash
    # 1. drop the column the demo dataset does not have
    lerobot-edit-dataset \
      --operation.type=remove_feature \
      --operation.feature_names='[intervention]' \
      --repo_id=me/rollout_cube_hil_r1_20260823_143000 \
      --new_repo_id=me/rollout_cube_hil_r1_flat
    
    # 2. aggregate round 0 and round 1 into one dataset
    lerobot-edit-dataset \
      --operation.type=merge \
      --operation.repo_ids='[me/cube-v0_20260823_101500, me/rollout_cube_hil_r1_flat]' \
      --new_repo_id=me/cube-v1-aggregate
  5. 5
    Fine-tune on the aggregate

    Point the trainer at the merged dataset, not the corrections alone. The LeRobot page says to fine-tune on combined data, but its own example command points at the HIL dataset by itself.

    bash
    lerobot-train \
      --dataset.repo_id=me/cube-v1-aggregate \
      --policy.type=act \
      --policy.pretrained_path=outputs/train/act_cube_v0/checkpoints/last/pretrained_model \
      --output_dir=outputs/train/act_cube_v1 \
      --job_name=act_cube_v1 \
      --policy.device=cuda
  6. 6
    Read the intervention rate, not the loss

    LeRobot's HIL-SERL guide calls a run successful when the human intervenes a lot at the start and less as it goes on. If interventions per episode stay flat across two rounds, loss falls but the policy does nothing is the better next stop.

Two naming rules that kill the run before it starts

Rollout datasets must be named rollout_something. lerobot-rollout raises Dataset names for rollout must start with 'rollout_' and exits, so --dataset.repo_id=me/cube-v0-hil-round1 never records a frame. lerobot-record refuses the mirror case, eval_.

Both then rename the dataset behind you: a date-time tag is appended at creation, so me/rollout_cube_hil_r1 lands on disk as me/rollout_cube_hil_r1_20260823_143000. Pass --dataset.no_stamp, or read the real name out of the log before writing the merge command.

The default is not the mode you probably want

--strategy.record_autonomous defaults to false. In that mode the dagger strategy keeps only the human correction windows, each saved as its own episode, and discards the autonomous frames. That is plain HG-DAgger, but it is not what IWR-style weighting needs: no non-intervention set is left to balance against. Set it to true and both segment types are recorded, corrections tagged intervention=true.

The flag also changes what ends the run. --strategy.num_episodes only bounds the corrections-only loop; with record_autonomous=true the session runs until you press escape, so give it --duration in seconds if you want it to stop on its own.

The leader arm is the bottleneck, and gearing decides

The dagger strategy needs a teleoperator whose motors can be driven under software: torque on to move the leader onto the follower's pose during the pause, torque off so you can drive. Both so100_leader and so101_leader qualify, but LeRobot's HIL-SERL guide notes the SO101 leader has reduced gears that let it track the follower, making takeover smoother than the gearless SO100. One trap sits in the source: if a rename_map renames action keys, the smooth handover silently does nothing. And STS3215 servos run at 7.4 V; 12 V destroys them.

The AY-Robots tutorial page for recording a first LeRobot dataset, showing the episode and camera setup steps
Round zero is an ordinary recording session; later rounds start wherever the policy left the arm.

How much correction data, and how to weight it

The most useful number here is not a bound, it is a budget. Mandlekar et al. started from 30 demonstrations, then ran three rounds, collecting in each until intervention samples reached roughly 33 percent of the initial dataset's sample count. That keeps human-labelled samples equal across methods regardless of base policy quality.

RoundWhat you runHuman-controlled data to collectWhat you train
0Teleoperated demonstrations50 episodes (SmolVLA needs 30)Base policy
1Rollouts with takeoverA third of round 0 in framesFine-tune on rounds 0 and 1
2 and 3Rollouts with takeoverThe same again, at whatever fails nowFine-tune on everything so far
Aggregation is not the same as concatenation

Plain HG-DAgger discards the autonomous frames and trains on human segments plus the original demonstrations. IWR keeps both and weights them so the sampler draws from the intervention and non-intervention sets in equal proportion; on the same data it beat HG-DAgger on both tasks. Do not expect the trainer to do this for you: --dataset.repo_id is documented as accepting a list, but in 0.6.1 that path raises The MultiLeRobotDataset isn't supported for now. One merged directory means plain concatenation.

MethodThreading taskCoffee machine task
Base policy (30 demonstrations)58.0 +/- 9.252.0 +/- 3.5
Equal budget of fresh demonstrations76.7 +/- 2.364.9 +/- 8.3
HG-DAgger, 3 rounds75.3 +/- 8.169.6 +/- 10.1
IWR, 3 rounds87.3 +/- 5.087.5 +/- 9.4

Success rates in percent over 3 seeds, simulated Sawyer arm in robosuite, 2-layer LSTM policies; threading is one operator, coffee machine averages three. Not SO-100 numbers, so take the ordering, not the absolutes. Three rounds of corrections beat an equal budget of fresh demonstrations on both tasks. The data collection guide covers the case where the answer is still more demonstrations.

Does it survive outside a simulator

Two 2025 results say the loop scales. RaC runs this recovery-then-correction protocol on three real bimanual tasks and reports beating the prior state of the art with 10 times less data collection time and 10 times fewer samples. RECAP, behind Physical Intelligence's pi-star 0.6, folds teleoperated interventions from autonomous execution into an RL objective and reports more than double the throughput and roughly half the failure rate on its hardest tasks.

Two ways to run the loop

You need the arm, a leader arm with drivable motors, two cameras, a 24 GB card for ACT or SmolVLA, and uninterrupted time next to the robot. What costs you is the part nobody writes down.

  • Round 0 is bench time: at 60 s per episode plus 60 s of reset, 50 episodes is about 100 minutes at the arm before a single correction exists.
  • Camera positions and calibration must not move between rounds. A shifted camera turns correction data into a different task.
  • Merging LeRobot datasets is one command, but strict: same fps, same robot_type, identical features.
  • Appending has its own rule: --resume=true works, but --dataset.num_episodes then means additional episodes, and --dataset.root is required.
  • You own the failures: camera not detected, dataset rejected as v3, out of memory.

Where this does not help, and the latency wall

Remote takeover has a physics problem

HG-DAgger only works if the human can spot a bad state and react before it is unrecoverable; the authors rule the method out where that is impossible. Add this platform's inference latency: 20 ms per action step with ACT, 152 ms with GR00T N1.7, 165 ms with GR00T N1.5, 245 ms with SmolVLA, 485 ms with Pi0.5. Put a public-internet round trip in front of that and remote takeover is viable for slow pick and place, not fast reactive motion.

One class of failure sits outside DAgger's reach. If the policy fails identically in every episode from the first frame, that is a broken input or a broken training run, not compounding error: check loss falls but the policy does nothing and policy only works in one setup. A gripper that never closes is mechanical first, and a joint stopping short is joint stops early.

The other limit is the dataset format. GR00T N1.7 and N1.5 take LeRobot v2.0 or v2.1, and a v3.0 dataset crashes the GR00T loader, so every correction round against GR00T has to come out in the accepted version. ACT, SmolVLA and Pi0.5 take v3.0; the dataset rejected as v3 page has the conversion.

Which policy to run the loop against

PolicyParamsPer action stepMin episodesDataset formatRetrain per round
ACT~80 M20 ms50LeRobot v3.01 to 3 USD
SmolVLA~450 M245 ms30LeRobot v3.01 to 3 USD
GR00T N1.7~3 B, ~40 M trained152 ms50LeRobot v2.0 or v2.14 to 12 USD
GR00T N1.5~3 B165 ms50LeRobot v2.0 or v2.14 to 12 USD
Pi0.5~3 B, PaliGemma backbone485 ms50LeRobot v3.04 to 12 USD
The AY-Robots policy comparison table with parameters, GPU tier, inference latency and minimum episode count for ACT, SmolVLA, GR00T N1.5, GR00T N1.7 and Pi0.5
Two columns dominate a correction loop: latency and retrain cost.

ACT is the obvious start: 20 ms per action step means your takeover lands roughly when you decide it, and four rounds cost 4 to 12 USD. It has no base model, so round zero must be real demonstrations. SmolVLA is the compromise at 30 minimum episodes, compared under ACT against SmolVLA. GR00T N1.7 and Pi0.5 are cloud-only. See the policy overview.

Action chunking means the model commits to a block of future actions at once, so a takeover cannot land mid-chunk without something absorbing the discontinuity; that is what --inference.type=rtc is for. For the counterweight on data scale see the BC-Z write-up, for the hardware end the SO-100 complete guide, and for the loop on your own arm imitation learning on the SO-100.

Your policy drifts. Find out which failure it actually is.

The failure-mode pages take the drift signatures one at a time, each with the check that separates it from the others, so you know whether you need correction rounds or a different fix entirely.

Open the fix index
Is DAgger the same thing as fine-tuning on failure cases?

No. Fine-tuning on failures means fresh demonstrations of the failing cases, each from a clean reset. DAgger records from the state the policy actually reached, mid-episode, which no reset produces.

How many correction rounds before it stops helping?

The published experiments use three rounds, and the first does most of the work: on the threading task IWR went from 58.0 percent at the base policy to 84.0 after round one, then 90.7 and 87.3, moves of the same order as the reported standard deviations of 3 to 5 points.

Does LeRobot implement DAgger or HG-DAgger?

The dagger rollout strategy in 0.6.1 is the human-gated loop, and what it records depends on one flag. With the default --strategy.record_autonomous=false you get plain HG-DAgger, only the correction windows, each its own episode. Set it to true and you get both segment types, corrections tagged intervention=true. Its docs cite Ross et al. 2011, Kelly et al. 2019, RaC and RECAP.

Why does my rollout exit before recording anything?

Most likely the dataset name: lerobot-rollout requires the repo name to start with rollout_ and exits otherwise. The dagger strategy also refuses to start without --teleop.type and --dataset.repo_id.

Can I run this with the AY-Robots browser teleoperation?

You can run the coarse version: drive the policy from an inference pod, stop it when it drifts, record correction episodes from the state the arm reached. What it cannot give you is a low-latency mid-episode takeover, and a late takeover produces a late label.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started