| |
| """ |
| Evaluation utilities for Motus. |
| Implements inference sampling and metrics computation for validation. |
| """ |
|
|
| import torch |
| import torch.nn.functional as F |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import matplotlib |
| |
| matplotlib.set_loglevel("WARNING") |
| from PIL import Image |
| from typing import Dict, List, Tuple, Optional |
| from collections import defaultdict |
| import logging |
| import os |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def create_video_grid(predicted_frames: torch.Tensor, ground_truth_frames: torch.Tensor, |
| num_samples: int = 4) -> Image.Image: |
| """ |
| Create a grid visualization comparing predicted and ground truth video frames. |
| |
| Args: |
| predicted_frames: (B, T, C, H, W) predicted video frames |
| ground_truth_frames: (B, T, C, H, W) ground truth video frames |
| num_samples: number of samples to visualize |
| |
| Returns: |
| PIL Image of the comparison grid |
| """ |
| batch_size = min(predicted_frames.shape[0], num_samples) |
| num_frames = predicted_frames.shape[1] |
| |
| |
| pred_np = predicted_frames[:batch_size].detach().cpu().permute(0, 1, 3, 4, 2).numpy() |
| gt_np = ground_truth_frames[:batch_size].detach().cpu().permute(0, 1, 3, 4, 2).numpy() |
|
|
| |
| pred_np = np.clip(pred_np, 0, 1) |
| gt_np = np.clip(gt_np, 0, 1) |
| |
| |
| fig, axes = plt.subplots(batch_size, num_frames * 2, figsize=(4 * num_frames * 2, 4 * batch_size)) |
| if batch_size == 1: |
| axes = axes.reshape(1, -1) |
| elif num_frames * 2 == 1: |
| axes = axes.reshape(-1, 1) |
| |
| for i in range(batch_size): |
| for t in range(num_frames): |
| |
| axes[i, t].imshow(gt_np[i, t]) |
| axes[i, t].set_title(f'GT Frame {t+1}') |
| axes[i, t].axis('off') |
| |
| |
| axes[i, t + num_frames].imshow(pred_np[i, t]) |
| axes[i, t + num_frames].set_title(f'Pred Frame {t+1}') |
| axes[i, t + num_frames].axis('off') |
| |
| plt.tight_layout() |
| |
| |
| fig.canvas.draw() |
| buf = fig.canvas.buffer_rgba() |
| img_array = np.asarray(buf) |
| img_array = img_array[:, :, :3] |
| |
| plt.close(fig) |
| |
| return Image.fromarray(img_array) |
|
|
|
|
| @torch.no_grad() |
| def inference_sample(model, batch: Dict, config) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Run inference to predict future video frames and actions using UniDiffuser's native inference method. |
| |
| Args: |
| model: UniDiffuser model (LatentActionWorldModel) |
| batch: Input batch containing observations, states, language embeddings, text instructions |
| config: Configuration object containing inference parameters |
| |
| Returns: |
| Tuple of (predicted_frames, predicted_actions) |
| - predicted_frames: (B, num_pred_frames, C, H, W) in pixel space [0, 255] |
| - predicted_actions: (B, action_chunk_size, action_dim) |
| """ |
| model.eval() |
| |
| |
| num_inference_steps = config.model.inference.num_inference_timesteps |
| |
| |
| device = next(model.parameters()).device |
| first_frame = batch['first_frame'].to(device) |
| video_frames = batch['video_frames'].to(device) |
| |
| state = batch['initial_state'].to(device) if 'initial_state' in batch and batch['initial_state'] is not None else None |
| |
| language_embeddings = batch['language_embedding'] |
| if language_embeddings is not None: |
| language_embeddings = language_embeddings.to(device) |
| |
| vlm_inputs = batch['vlm_inputs'] |
| if vlm_inputs is not None: |
| |
| vlm_inputs = {k: v.to(device) if isinstance(v, torch.Tensor) else v |
| for k, v in vlm_inputs.items()} |
| |
| with torch.no_grad(): |
| predicted_frames, predicted_actions = model.inference_step( |
| first_frame=first_frame, |
| state=state, |
| num_inference_steps=num_inference_steps, |
| language_embeddings=language_embeddings, |
| vlm_inputs=vlm_inputs, |
| ) |
| |
| model.train() |
| return predicted_frames, predicted_actions |
|
|
|
|
| def compute_action_metrics(predicted_actions: torch.Tensor, ground_truth_actions: torch.Tensor) -> Dict[str, float]: |
| """ |
| Compute action prediction metrics (MSE and L2 error). |
| |
| Args: |
| predicted_actions: (B, T, action_dim) predicted actions |
| ground_truth_actions: (B, T, action_dim) ground truth actions |
| |
| Returns: |
| Dictionary containing MSE and L2 error metrics |
| """ |
| |
| mse_loss = F.mse_loss(predicted_actions, ground_truth_actions, reduction='none').float() |
| mse_loss_per_sample = mse_loss.reshape(predicted_actions.shape[0], -1).mean(1) |
| |
| |
| l2_loss = mse_loss.sqrt() / (1 + 1e-3) |
| l2_loss_per_sample = l2_loss.reshape(predicted_actions.shape[0], -1).mean(1) |
| |
| return { |
| 'mse_loss': mse_loss_per_sample.mean().item(), |
| 'l2_error': l2_loss_per_sample.mean().item(), |
| 'mse_std': mse_loss_per_sample.std().item(), |
| 'l2_std': l2_loss_per_sample.std().item() |
| } |
|
|
|
|
| @torch.no_grad() |
| def evaluate_model(model, dataloader, accelerator, config, num_eval_batches: int = 2) -> Dict[str, float]: |
| """ |
| Local-only evaluation: no distributed aggregation; safe for rank0-only evaluation. |
| """ |
| logger.info(f"Running UniDiffuser evaluation for {num_eval_batches} batches...") |
| model.eval() |
| |
| from collections import defaultdict |
| metrics = defaultdict(list) |
| visual_samples = [] |
| |
| for step, batch in enumerate(dataloader): |
| if step >= num_eval_batches: |
| break |
| if batch is None: |
| continue |
| |
| |
| predicted_frames, predicted_actions = inference_sample(model, batch, config) |
| gt_frames = batch['video_frames'].to(predicted_frames.device) |
| predicted_frames = predicted_frames.permute(0, 2, 1, 3, 4) |
| |
| |
| video_mse = F.mse_loss(predicted_frames, gt_frames, reduction='mean').item() |
| metrics['video_mse'].append(video_mse) |
| |
| |
| if 'action_sequence' in batch and predicted_actions is not None: |
| gt_actions = batch['action_sequence'][:, :predicted_actions.shape[1]].to(predicted_actions.device) |
| action_metrics = compute_action_metrics(predicted_actions, gt_actions) |
| for key, value in action_metrics.items(): |
| metrics[f'action_{key}'].append(value) |
| |
| |
| if step == 0: |
| visual_samples.append({ |
| 'predicted_frames': predicted_frames[:4], |
| 'ground_truth_frames': gt_frames[:4], |
| 'predicted_actions': predicted_actions[:4] if predicted_actions is not None else None, |
| 'ground_truth_actions': batch.get('action_sequence', None)[:4] if batch.get('action_sequence', None) is not None else None |
| }) |
| |
| |
| final_metrics = {} |
| for key, values in metrics.items(): |
| if values: |
| final_metrics[key] = float(np.mean(values)) |
| final_metrics[f'{key}_std'] = float(np.std(values)) |
| |
| if visual_samples: |
| sample = visual_samples[0] |
| grid_visualization = create_video_grid( |
| sample['predicted_frames'], |
| sample['ground_truth_frames'], |
| num_samples=4 |
| ) |
| final_metrics['visualization'] = grid_visualization |
| |
| model.train() |
| return final_metrics |
|
|
|
|
| def log_evaluation_metrics(metrics: Dict, writer, accelerator, global_step: int): |
| """ |
| Log evaluation metrics to tensorboard and wandb. |
| |
| Args: |
| metrics: Dictionary containing evaluation metrics |
| writer: TensorBoard writer (can be None) |
| accelerator: HuggingFace accelerator |
| global_step: Current training step |
| """ |
| if accelerator.is_main_process: |
| |
| log_dict = {} |
| for key, value in metrics.items(): |
| if key not in ['visualization', 'visual_samples'] and isinstance(value, (int, float)): |
| log_dict[f'eval/{key}'] = value |
| |
| |
| if log_dict: |
| accelerator.log(log_dict, step=global_step) |
| |
| |
| if writer is not None: |
| |
| for key, value in log_dict.items(): |
| writer.add_scalar(key, value, global_step) |
| |
| |
| if 'visualization' in metrics: |
| img_array = np.array(metrics['visualization']).transpose(2, 0, 1) |
| writer.add_image('eval/video_grid', img_array, global_step) |
|
|
| |
| logger.info("=== UniDiffuser Evaluation Results ===") |
| for key, value in metrics.items(): |
| if key != 'visualization' and isinstance(value, (int, float)): |
| logger.info(f" {key}: {value:.4f}") |