LeRobot Notes: The Basic Workflow
Notes on LeRobot datasets, training, and policies.
Translated from Chinese with AI · Read the original
Dataset
I use the simple PushT dataset as an example: a robot pushes an object to a target shape and position in 2D video.
Batch Dataset
Print the dataset’s basic information:
dataset = LeRobotDataset("lerobot/pusht")print(dataset)
# output# LeRobotDataset({# Repository ID: 'lerobot/pusht',# Number of selected episodes: '206',# Number of selected samples: '25650',# Features: '['observation.image', 'observation.state', 'action', 'episode_index', 'frame_index', 'timestamp', 'next.reward', 'next.done', 'next.success', 'index', 'task_index']',# })',Key variables:
- episode: A video segment, corresponding here to one task.
- observation
- image: A video frame with shape (H,W,C), 96x96x3, apparently from the robot’s viewpoint.
- state: (x,y) coordinates.
- action: Shape (2,), also x,y coordinates.
- reward: Shape (1,), a scalar reward.
LeRobotDataset loads the data directly into memory.
Visualization
python src/lerobot/scripts/lerobot_dataset_viz.py \ --repo-id lerobot/pusht \ --episode-index 1 \ --display-compressed-images trueResult:

I will explore more usage patterns later.
Streaming Dataset
dataset = StreamingLeRobotDataset("lerobot/pusht")for sample in dataset: print(sample['action'])Streaming Dataset uses PyTorch’s IterableDataset without loading everything into memory. The main method to inspect is __iter__(self).
def __iter__(self) -> Iterator[dict[str, torch.Tensor]]: # ...
# buffer 内打散 buffer_indices_generator = self._iter_random_indices(rng, self.buffer_size)
# 拆成多个 shard, backtrack_dataset 是和 delta_timestamp 相关,需要拿到前后的若干条数据 idx_to_backtrack_dataset = { idx: self._make_backtrackable_dataset(safe_shard(self.hf_dataset, idx, self.num_shards)) for idx in range(self.num_shards) }
frames_buffer = [] while available_shards := list(idx_to_backtrack_dataset.keys()): # 随便选一个 shard,拿到对应的 backtrack_dataset shard_key = next(self._infinite_generator_over_elements(rng, available_shards)) backtrack_dataset = idx_to_backtrack_dataset[shard_key] # selects which shard to iterate on
try: # make_frame 有可能会产生多个 frame,和 delta_timestamp 有关,下面再详细说说 for frame in self.make_frame(backtrack_dataset): # 蓄水池采样,如果满了就丢出去第 i 个,把当前的 frame 补到 i-th 里 if len(frames_buffer) == self.buffer_size: i = next(buffer_indices_generator) # samples a element from the buffer yield frames_buffer[i] frames_buffer[i] = frame else: frames_buffer.append(frame) break # random shard sampled, switch shard except ( RuntimeError, StopIteration, ): # NOTE: StopIteration inside a generator throws a RuntimeError since python 3.7 del idx_to_backtrack_dataset[shard_key] # Remove exhausted shard, onto another shardmake_frame
The important case involves delta_timestamp. For example, [-1, -0.5, -0.20, 0] requests frames from one second, half a second, and 0.2 seconds ago, plus the current frame.
Processing converts delta timestamps into frame offsets using delta ts * fps. At 30 fps, -0.20*30=-6 means six frames earlier. After calculating indices, the result is assembled in _get_delta_frames. To reuse frames, backtrack_dataset uses this structure:
self._source: Iterator[T] = iter(iterable)self._back_buf: deque[T] = deque(maxlen=history)self._ahead_buf: deque[T] = deque(maxlen=lookahead) if lookahead > 0 else deque()self._cursor: int = 0- Looking backward, with negative deltas: Normal iteration puts previously visited frames in _back_buf, ready for reuse.
- Looking forward, with positive deltas: Continue reading _source into _ahead_buf without moving the cursor.
torch.cdist determines which actual video frames to load.
for frame, pts in zip(frames_batch.data, frames_batch.pts_seconds, strict=True): loaded_frames.append(frame) loaded_ts.append(pts.item()) if log_loaded_timestamps: logging.info(f"Frame loaded at timestamp={pts:.4f}")
query_ts = torch.tensor(timestamps)loaded_ts = torch.tensor(loaded_ts)
# compute distances between each query timestamp and loaded timestampsdist = torch.cdist(query_ts[:, None], loaded_ts[:, None], p=1)min_, argmin_ = dist.min(1)Training
Example of running policy training:
python src/lerobot/scripts/lerobot_train.py \ --dataset.repo_id lerobot/pusht \ --policy.type act \ --policy.push_to_hub False \ --steps 50 \ --num_workers 0 \ --policy.device cpuThe main operation in lerobot_train.py:
# preprocessor 和 postprocessorpreprocessor, postprocessor = make_pre_post_processors( policy_cfg=cfg.policy, pretrained_path=cfg.policy.pretrained_path, **processor_kwargs, **postprocessor_kwargs,)logging.info(f"preprocessor: {preprocessor}, postprocessor: {postprocessor}")
# ...
# accelerator 根据加速器(如 deepspeed)等的封装,初始化几个关键对象policy, optimizer, dataloader, lr_scheduler = accelerator.prepare( policy, optimizer, dataloader, lr_scheduler)print(f"policy: {policy}, optimizer: {optimizer}, dataloader: {dataloader}, lr_scheduler: {lr_scheduler}")
# ...
for _ in range(step, cfg.steps): start_time = time.perf_counter() # dl_iter 是上面讲到的 dataloader batch = next(dl_iter)
batch = preprocessor(batch) train_tracker.dataloading_s = time.perf_counter() - start_time
train_tracker, output_dict = update_policy( train_tracker, policy, batch, optimizer, cfg.optimizer.grad_clip_norm, accelerator=accelerator, lr_scheduler=lr_scheduler, rabc_weights_provider=rabc_weights, )Preprocessor and Postprocessor
Each model can define its own configuration. ACT, for example, defines its model inputs in processor_act.py:
input_steps = [ RenameObservationsProcessorStep(rename_map={}), AddBatchDimensionProcessorStep(), DeviceProcessorStep(device=config.device), NormalizerProcessorStep( features={**config.input_features, **config.output_features}, norm_map=config.normalization_mapping, stats=dataset_stats, device=config.device, ),]output_steps = [ UnnormalizerProcessorStep( features=config.output_features, norm_map=config.normalization_mapping, stats=dataset_stats ), DeviceProcessorStep(device="cpu"),]
return ( PolicyProcessorPipeline[dict[str, Any], dict[str, Any]]( steps=input_steps, name=POLICY_PREPROCESSOR_DEFAULT_NAME, ), PolicyProcessorPipeline[PolicyAction, PolicyAction]( steps=output_steps, name=POLICY_POSTPROCESSOR_DEFAULT_NAME, to_transition=policy_action_to_transition, to_output=transition_to_policy_action, ),)update_policy
The workflow resembles ordinary PyTorch training. A policy inherits from nn.Module and includes training, evaluation, and action output. For ACT:
For the Action Chunk Transformer architecture, see https://arxiv.org/abs/2304.13705. Its main ideas include:
- Predict multiple steps for imitation learning rather than a single step, using the Transformer architecture to maintain smoothness.
- During training, use a VAE encoder for observations/state; during inference, use the prior mean z=0. The VAE compresses inputs into a normally distributed latent space and samples from it. Using the prior’s mean and standard deviation at inference improves generalization.
ACT training inputs and outputs:
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, tuple[Tensor, Tensor] | tuple[None, None]]: """A forward pass through the Action Chunking Transformer (with optional VAE encoder).
`batch` should have the following structure: { [robot_state_feature] (optional): (B, state_dim) batch of robot states.
[image_features]: (B, n_cameras, C, H, W) batch of images. AND/OR [env_state_feature]: (B, env_dim) batch of environment states.
[action_feature] (optional, only if training with VAE): (B, chunk_size, action dim) batch of actions. }
Returns: (B, chunk_size, action_dim) batch of action sequences Tuple containing the latent PDF's parameters (mean, log(σ²)) both as (B, L) tensors where L is the latent dimension. """ACT’s Transformer decoder uses an all-zero matrix, adding positional encodings to obtain embeddings for cross-attention.
decoder_in = torch.zeros( (self.config.chunk_size, batch_size, self.config.dim_model), dtype=encoder_in_pos_embed.dtype, device=encoder_in_pos_embed.device,)decoder_out = self.decoder( decoder_in, encoder_out, encoder_pos_embed=encoder_in_pos_embed, decoder_pos_embed=self.decoder_pos_embed.weight.unsqueeze(1),)
#...
# 最后用 action_head 把 model output 转成有限维度的 action# Final action regression head on the output of the transformer's decoder.self.action_head = nn.Linear(config.dim_model, self.config.action_feature.shape[0])Other Concepts Encountered
- AE (Autoencoder): Maps high-dimensional inputs into lower dimensions.
- VAE (Variational Autoencoder): Introduces a distribution to regularize embeddings in the lower-dimensional space.
- VQ-VAE (Vector Quantized VAE): Uses a finite codebook, {e₁, e₂, …, e_K}.
- KL divergence: Describes the difference between two probability distributions.