Part III: Running a trained policy · §31 of 43
31.1 Inputs checklist
| You need | Where it comes from | Check |
|---|---|---|
| A trained policy | lerobot-train (§24.3) or a Hub model id | §31.2 |
| The dataset it was trained on (locally) | lerobot-record (§24.1) | Needed only to look up the start pose and camera settings (§31.3) |
The exact --robot.cameras string used when recording | Your recording command / shell history (history | grep lerobot-record) | Copy it, don’t retype it |
| The same physical scene | Table, cameras, lighting, objects | Compare against a recorded frame (step 5 of §35) |
| Working robot setup | setup_openarm_lerobot.sh (§23.1) | 16/16 motors answer |
| GPU | RTX 5050 in the lerobot env | python -c "import torch; print(torch.cuda.is_available())" prints True |
| A second person (first runs) | Hand on the motor power switch |
31.2 Find and check the trained model
lerobot-train writes into the --output_dir you gave it, relative to the folder you ran it from. With the command of §24.3 run from ~/Documents/lerobot:
~/Documents/lerobot/outputs/train/act_openarm_pick_cube/
├── checkpoints/
│ ├── 020000/ one folder per saved checkpoint (every --save_freq steps, default 20 000)
│ │ ├── pretrained_model/ ← what --policy.path points to
│ │ └── training_state/ optimiser state, only for resuming training
│ ├── 040000/
│ │ └── …
│ └── last -> 100000 link to the newest checkpoint
└── … training logs
and inside every pretrained_model/:
| File | What it is |
|---|---|
config.json | The policy’s settings: its type (act), the inputs it expects (input_features), what it outputs (output_features), chunk_size, n_action_steps, … |
model.safetensors | The trained weights (ACT: a few hundred MB) |
train_config.json | The full training command, including which dataset was used |
policy_preprocessor.json, policy_postprocessor.json (+ .safetensors files) | The normalisation steps and the stats of the training data |
Point
--policy.pathatpretrained_model, not at the checkpoint folderRight:
…/checkpoints/last/pretrained_model. Wrong:…/checkpoints/last. Use an absolute path ($HOME/Documents/lerobot/outputs/…): a relative one only works from the folder you trained in, and a path that doesn’t exist locally is treated as a Hugging Face Hub repo name, which fails with a confusing “repository not found” style error.
Check the folder and print what the policy expects:
conda activate lerobot
P=$HOME/Documents/lerobot/outputs/train/act_openarm_pick_cube/checkpoints/last/pretrained_model
ls -l "$P"
python - "$P" <<'EOF'
import json, sys, pathlib
p = pathlib.Path(sys.argv[1])
c = json.loads((p / "config.json").read_text())
print("policy type :", c.get("type"))
print("device :", c.get("device"))
for k, v in c["input_features"].items():
print("input ", k, v["type"], v["shape"])
for k, v in c["output_features"].items():
print("output ", k, v["type"], v["shape"])
for k in ("chunk_size", "n_action_steps", "n_obs_steps", "temporal_ensemble_coeff"):
if k in c:
print(f"{k:<24}", c[k])
t = json.loads((p / "train_config.json").read_text())
print("trained on :", t["dataset"]["repo_id"])
EOFWhat you should see for a bimanual ACT trained with one camera called top (illustrative):
policy type : act
device : cuda
input observation.state STATE [16]
input observation.images.top VISUAL [3, 480, 640]
output action ACTION [16]
chunk_size 100
n_action_steps 100
n_obs_steps 1
temporal_ensemble_coeff None
trained on : <hf_user>/openarm_pick_cube
How to read it:
observation.state [16]andaction [16]: 8 values per arm, so it’s a bimanual policy and needs--robot.type=bi_openarm_follower.[8]would be a single-arm policy (§39).- Every
observation.images.<name>line is a camera the robot must provide under exactly that<name>, at that resolution ([channels, height, width]). trained ontells you which dataset to look at in §31.3.
A policy uploaded to the Hub (--policy.push_to_hub=true during training) is used with --policy.path=<hf_user>/act_openarm_pick_cube instead of a folder. It’s downloaded on first use.
31.3 Find out how the training data started
Two things come from the training dataset: the start pose and the camera settings. Datasets live in ~/.cache/huggingface/lerobot/<repo_id>/ (the repo_id printed above).
The fps and cameras the data was recorded with:
D=$HOME/.cache/huggingface/lerobot/<hf_user>/openarm_pick_cube
python - "$D" <<'EOF'
import json, sys
info = json.load(open(f"{sys.argv[1]}/meta/info.json"))
print("fps:", info["fps"], "| episodes:", info["total_episodes"], "| robot:", info.get("robot_type"))
for k, v in info["features"].items():
if v["dtype"] in ("video", "image"):
print("camera", k, v["shape"])
EOFThe --fps of the rollout must equal that fps, and every camera listed must be given in --robot.cameras with the same name and resolution.
The start pose. The policy has only ever seen the arms begin the task from wherever your recordings began. The safest place to start a rollout is therefore the average first frame of the training episodes. This prints it, per joint:
python - "$D" <<'EOF'
import glob, json, sys
import numpy as np, pandas as pd
root = sys.argv[1]
names = json.load(open(f"{root}/meta/info.json"))["features"]["observation.state"]["names"]
files = sorted(glob.glob(f"{root}/data/*/*.parquet"))
df = pd.concat(pd.read_parquet(f, columns=["episode_index", "frame_index", "observation.state"]) for f in files)
first = np.stack(df[df.frame_index == 0].sort_values("episode_index")["observation.state"].to_numpy())
print(f"{len(first)} episodes. First-frame joint angles (degrees):")
print(f"{'joint':<22}{'mean':>8}{'min':>8}{'max':>8}")
for i, n in enumerate(names):
print(f"{n:<22}{first[:, i].mean():8.1f}{first[:, i].min():8.1f}{first[:, i].max():8.1f}")
EOFIf your recordings started with the arms hanging and the grippers closed, every mean is within a few degrees of 0. A wide min–max spread on a joint means the episodes started in different places. That’s fine, it makes the policy more tolerant, but stay inside that range when you position the arms.
Write the start pose down. It’s the target of step 6 in §35.