Teacher-Student Training

Training has two stages. First, a privileged teacher that observes the true wrench is trained with PPO. Then a student that only sees arm proprioception is trained with PPO, an intent estimator, and a KL term that pulls its action distribution towards the frozen teacher's.

Tasks

Register two tasks on the same transport environment: a teacher task whose actor observes the teacher observation, and a student task whose actor observes the student observation. Observation sizes, reward weights and the simulation settings are on the Transport Environment page.

# legged_gym/envs/paint/paint_config.py
class PaintCfg(LeggedRobotCfg):  # student task
    class env(LeggedRobotCfg.env):
        num_actions = 9
        num_observations = ...  # TODO: student history size
        num_privileged_obs = ...  # TODO: critic history size
        episode_length_s = 10
        teacher_mode = False

    class terrain(LeggedRobotCfg.terrain):
        ...  # TODO: rough terrain with curriculum

    class control(LeggedRobotCfg.control):
        ...  # TODO: arm PD gains, arm action scale 0.5, decimation 4

    class rewards(LeggedRobotCfg.rewards):
        class scales:
            ...  # TODO: weights of Table I


class PaintTeacherCfg(PaintCfg):  # teacher task
    class env(PaintCfg.env):
        num_observations = ...  # TODO: teacher history size
        teacher_mode = True


class PaintTeacherCfgPPO(LeggedRobotCfgPPO):
    ...  # TODO: PPO settings below, 15k iterations


class PaintCfgPPO(PaintTeacherCfgPPO):
    ...  # TODO: PaintRunner, lambda_KL, teacher checkpoint path, 20k iterations


# legged_gym/envs/__init__.py
task_registry.register("paint_teacher", PaintEnv, PaintTeacherCfg(), PaintTeacherCfgPPO())
task_registry.register("paint", PaintEnv, PaintCfg(), PaintCfgPPO())

# legged_gym/utils/task_registry.py
# TODO: in make_alg_runner, create the runner class named by train_cfg.runner_class_name

PPO settings

Both stages use the legged_gym PPO defaults with smaller networks:

Actor, critic, estimatorMLP 128-128-128, ELU
Initial action std1.0
Steps per environment per iteration24
Learning epochs / mini-batches5 / 4
Learning rate1e−3, adaptive (target KL 0.01)
γ / λ0.99 / 0.95
Clip / value loss coefficient / entropy coefficient0.2 / 1.0 / 0.01
Max gradient norm1.0
Iterations15k (teacher), 20k (student)

Stage 1: privileged teacher

The teacher sees the true wrench, so the unmodified rsl_rl PPO trains it. It learns an admittance-like mapping from the wrench to base and arm motion.

python legged_gym/scripts/train.py --task=paint_teacher --headless

Stage 2: student with intent estimator

At each step the intent estimator maps the student's observation history ot to an estimated normalized wrench ŵ, and the student actor acts on [ot, ŵ]. The estimate is detached before it enters the actor, so policy gradients do not flow into the estimator.

Intent estimator

An MLP with the same architecture as the actor (128-128-128, ELU). Its input is the student observation history (144 values for H = 8) and its output the estimated wrench βest = [F̂xy, Θ̂yaw], expressed normalized as c(t).

Losses

The student maximizes the expected return while staying close to the frozen teacher, which sees the teacher observation of the same transition (Eq. (8) and (9) of the paper):

maxθS  𝔼[Σt γtrt] − λKL JKL(θS),    JKL = 𝔼[ DKL(πS(· | ot, ŵt) ‖ πT(· | oTt)) ]

In practice JKL is added to the PPO loss with weight λKL = 10−6. Both policies are diagonal Gaussians, so the KL divergence has a closed form:

DKL(πS ‖ πT) = Σi [ log(σT,i / σS,i) + (σS,i2 + (μS,i − μT,i)2) / (2σT,i2) − ½ ]

The estimator is trained with the regression loss of Eq. (7), the squared error between the estimated and the true force and torque. It has its own Adam optimizer (learning rate 1e−3, not adapted) and is updated on the same mini-batches.

Changes to rsl_rl

  • Algorithm: extend PPO with the frozen teacher, the intent estimator and its optimizer, the KL term, and the estimator regression.
  • Storage: also store the teacher observation of every transition, so the teacher can be queried and the true wrench read on each mini-batch.
  • Runner: build the student actor-critic (input 144 + 3), the teacher (loaded from the stage-1 checkpoint) and the estimator, pass the teacher observation from the environment to the algorithm, and save the estimator with the policy.
# rsl_rl/modules/intent_estimator.py
class IntentEstimator(nn.Module):
    def __init__(self, num_obs, num_out=3, hidden_dims=(128, 128, 128)):
        super().__init__()
        # TODO: MLP num_obs -> hidden_dims -> num_out with ELU

    def forward(self, obs):
        # TODO: return the estimated normalized wrench
        ...


# rsl_rl/algorithms/paint_ppo.py
class PaintPPO(PPO):
    def __init__(self, actor_critic, teacher, estimator, distill_coef, **kwargs):
        super().__init__(actor_critic, **kwargs)
        # TODO: keep the frozen teacher, the estimator and a separate Adam optimizer for it

    def act(self, obs, teacher_obs, critic_obs):
        # TODO: w_hat = estimator(obs), detached; act on [obs, w_hat] as in PPO.act
        # TODO: also store teacher_obs in the transition
        ...

    def update(self):
        # TODO: per mini-batch, as in PPO.update, plus:
        #   - student distribution on [obs, w_hat.detach()],
        #     teacher distribution on teacher_obs
        #   - add lambda_KL * KL(student || teacher) to the PPO loss
        #   - estimator step: MSE between w_hat and the true wrench (from teacher_obs)
        ...


# rsl_rl/storage/rollout_storage.py
# TODO: add a teacher_observations buffer and return it from mini_batch_generator


# rsl_rl/runners/paint_runner.py
class PaintRunner(OnPolicyRunner):
    def __init__(self, env, train_cfg, log_dir=None, device="cpu"):
        # TODO: student ActorCritic (input = student obs + 3), teacher ActorCritic
        #       loaded from the stage-1 checkpoint, IntentEstimator, and PaintPPO
        ...

    def learn(self, num_learning_iterations, init_at_random_ep_len=False):
        # TODO: as OnPolicyRunner.learn, passing extras["teacher_obs"] to alg.act
        ...

    # TODO: save and load the estimator together with the policy
python legged_gym/scripts/train.py --task=paint --headless

Keep the teacher observation fixed

The teacher network is rebuilt from the stage-2 environment, so the teacher observation (terms, order and history length H) must be identical in both stages.

What to expect

  • The base velocity converges to the wrench reference, and the mean episode length approaches the full 500 steps.
  • The estimator loss is measured on the normalized wrench. Multiply by 40 N and 10 N·m to read it in physical units.
  • For reference, our student with H = 8 reaches these errors in simulation for 2 / 4 / 8 kg payloads (Table II of the paper):
Metric 2 kg 4 kg 8 kg
Linear tracking error (m/s)0.1190.1350.202
Angular tracking error (rad/s)0.1050.1500.252
Force estimation error (N)2.763.476.66
Torque estimation error (N·m)0.571.022.59