CoolFace
Apppublic

robometer/rewardeval_ui

sourceHugging Faceupdated 7mo agoView on Hugging Face
4likes
pref.py371 linesDownload Raw Back to samplers
1#!/usr/bin/env python32"""3PrefSampler class for producing batches of preference data.4"""5 6from typing import Dict, List, Optional, Any7 8import random9 10from robometer.data.dataset_types import PreferenceSample, Trajectory11from robometer.data.samplers.base import RBMBaseSampler12from robometer.data.datasets.helpers import (13    DataGenStrat,14    convert_continuous_to_discrete_bins,15)16from robometer.utils.logger import get_logger, rank_0_info, trace17from robometer.utils.timer import timer18 19logger = get_logger()20 21 22class PrefSampler(RBMBaseSampler):23    """Data generator for producing batches of preference prediction data."""24 25    def __init__(self, is_evaluation=False, **kwargs):26        super().__init__(**kwargs)27 28        self.dataset_preference_ratio = self.config.dataset_preference_ratio29        self.preference_strategy_ratio: List[float] = self.config.preference_strategy_ratio30        self._has_suboptimal = (31            any(len(indices) > 0 for indices in self.suboptimal_by_task.values()) if self.suboptimal_by_task else False32        )33        rank_0_info(f"[PREF SAMPLER] Has suboptimal: {self._has_suboptimal}")34 35        # Initialize preference dataset36        self._load_preference_dataset()37 38    def _generate_sample(self, item: dict, preferred_strategy: Optional[DataGenStrat] = None):39        """Generate a preference sample from an item.40 41        If the item has a non-successful quality label, it will be used as the rejected42        trajectory and an optimal trajectory from the same task will be found as the chosen one.43        Otherwise, normal preference sampling logic is used.44 45        Args:46            item: The trajectory item47            preferred_strategy: Optional strategy to use (if None, will select strategy based on ratios)48        """49        quality_label = item["quality_label"]50        use_partial_success = item.get("partial_success") is not None51 52        # Handle non-successful trajectories: use as rejected, find optimal from same task as chosen53        # skip this for trajectories with partial_success which we will handle with partial success logic54        if quality_label != "successful" and not use_partial_success:55            traj_id = item["id"]56            task_name = item["task"]57 58            logger.trace(59                f"[PREF SAMPLER] Non-successful quality detected for ID={traj_id}, using as rejected trajectory, task={task_name}"60            )61 62            # Find optimal trajectories from the same task63            same_task_optimal_indices = self.optimal_by_task.get(task_name, [])64 65            if not same_task_optimal_indices:66                logger.trace(67                    f"[PREF SAMPLER] No optimal trajectories found for task '{task_name}', falling through to normal sampling"68                )69                return self._create_pref_sample(item, preferred_strategy=preferred_strategy)70 71            # Select a random optimal trajectory from the same task as chosen72            chosen_idx = random.choice(same_task_optimal_indices)73            chosen_traj_dict = self.dataset[chosen_idx]74 75            chosen_trajectory = self._get_traj_from_data(chosen_traj_dict)76            rejected_trajectory = self._get_traj_from_data(item)77 78            sample = PreferenceSample(79                chosen_trajectory=chosen_trajectory,80                rejected_trajectory=rejected_trajectory,81                data_gen_strategy=DataGenStrat.SUBOPTIMAL.value,82            )83 84            logger.trace(85                f"[PREF SAMPLER] Created preference sample for non-successful traj ID={traj_id} with optimal traj from same task"86            )87            return sample88 89        return self._create_pref_sample(item, preferred_strategy=preferred_strategy)90 91    def _execute_strategy(92        self, strategy: DataGenStrat, chosen_traj: Dict[str, Any], use_partial_success: bool93    ) -> tuple[Dict[str, Any], str, Dict[str, Any]] | None:94        """Execute a strategy to get rejected trajectory.95 96        Args:97            strategy: The strategy to execute98            chosen_traj: The chosen trajectory99            use_partial_success: Whether this trajectory uses partial_success100 101        Returns:102            Tuple of (rejected_traj, rejected_subsample_strategy, chosen_traj) or None if failed103            Note: chosen_traj may be swapped with rejected_traj for partial_success trajectories104        """105        max_retries = 3106        rejected_subsample_strategy = None107        rejected_traj = None108 109        if strategy == DataGenStrat.REWIND:110            rejected_traj = chosen_traj.copy()111            rejected_subsample_strategy = "subsample_rewind"112        elif strategy == DataGenStrat.SUBOPTIMAL:113            for _ in range(max_retries):114                rejected_traj = self._get_same_task_suboptimal(chosen_traj)115                if rejected_traj is not None:116                    # For trajectories with partial_success, if the returned trajectory has higher partial_success, swap them117                    if use_partial_success:118                        chosen_partial_success = chosen_traj.get("partial_success")119                        rejected_partial_success = rejected_traj.get("partial_success")120                        if rejected_partial_success is not None and chosen_partial_success is not None:121                            if rejected_partial_success > chosen_partial_success:122                                logger.trace(123                                    f"[PREF SAMPLER] Swapping trajectories: found higher partial_success "124                                    f"({rejected_partial_success} > {chosen_partial_success})"125                                )126                                rejected_traj, chosen_traj = chosen_traj, rejected_traj127                    break128            rejected_subsample_strategy = "subsample_forward"129        elif strategy == DataGenStrat.DIFFERENT_TASK:130            for _ in range(max_retries):131                rejected_traj = self._get_different_video_traj(chosen_traj)132                if rejected_traj is not None:133                    break134            rejected_subsample_strategy = "subsample_forward"135        elif strategy == DataGenStrat.REVERSE_PROGRESS:136            rejected_traj = chosen_traj.copy()137            rejected_subsample_strategy = "subsample_reverse"138        else:139            return None140 141        if rejected_traj is None:142            return None143 144        return (rejected_traj, rejected_subsample_strategy, chosen_traj)145 146    def _create_pref_sample_from_dataset(self) -> PreferenceSample:147        """Create a preference sample from the loaded preference dataset."""148        if not self.preferences:149            return None150 151        # For now, return a simple preference sample152        # This can be enhanced later when we have actual preference data153        random.choice(self.preferences)154 155        # This is a placeholder - would need to be implemented based on actual preference data structure156        return None157 158    def _load_preference_dataset(self):159        """Load the preference dataset from disk or hub if provided."""160        self.preferences = []161 162        # For now, we'll use empty preferences since the config structure has changed163        # This can be updated later if needed164        rank_0_info("[PREF SAMPLER] No preference dataset provided, will use random sampling for preferences")165        return166 167    def _create_preference_sample(self) -> PreferenceSample:168        """Create a preference prediction sample: chosen vs rejected where chosen is preferred.169        Either from dataset or from generated trajectories.170 171        Returns:172            PreferenceSample: A preference sample with chosen (preferred) vs rejected173            (suboptimal) trajectories and associated metadata174        """175 176        with timer("create_preference_sample", verbose=False):177            if random.random() < self.dataset_preference_ratio and self.preferences:178                # Use preference trajectories from dataset179                return self._create_pref_sample_from_dataset()180            else:181                return self._create_pref_sample()182 183    def _create_pref_sample(184        self, chosen_traj: Optional[Dict[str, Any]] = None, preferred_strategy: Optional[DataGenStrat] = None185    ) -> PreferenceSample:186        """Create a preference prediction sample using various rejected trajectory generation strategies.187 188        Rewind Same Task189        - Creates a suboptimal trajectory by rewinding the chosen trajectory190 191        Suboptimal/Failure Same Task192        - Uses existing suboptimal/failure trajectories from the same task193 194        Different Task195        - Uses trajectories from completely different tasks196 197        Returns:198            PreferenceSample: A preference sample with chosen (preferred) vs rejected199            (suboptimal) trajectories and associated metadata200 201        Raises:202            ValueError: If no chosen trajectories are available for preference generation203            RuntimeError: If all strategies fail and fallback rewind also fails204        """205        # Log when preference sampler is called206        traj_id = chosen_traj["id"] if chosen_traj is not None else "sampling_new"207        logger.trace(f"[PREF SAMPLER] Creating preference sample for trajectory ID: {traj_id}")208 209        # Use provided chosen trajectory if given; otherwise sample one210        if chosen_traj is None:211            # Use preprocessed chosen trajectories from index maps212            if not self.optimal_by_task:213                return None214 215            # Filter out tasks with empty optimal_indices to avoid infinite loop216            valid_tasks = {217                task: indices218                for task, indices in self.optimal_by_task.items()219                if indices  # Only include tasks with non-empty indices220            }221 222            if not valid_tasks:223                # No valid tasks with optimal trajectories available224                return None225 226            # Get a random task and chosen trajectory from it227            task_name = random.choice(list(valid_tasks.keys()))228            optimal_indices = valid_tasks[task_name]229 230            # Double-check that we have valid indices (should always be true now)231            if not optimal_indices:232                return None233 234            chosen_idx = random.choice(optimal_indices)235            chosen_traj = self.dataset[chosen_idx]236 237        # Initialize variables for strategy selection238        rejected_traj = None239        strategy_used = None240        rejected_subsample_strategy = None241 242        # Check if this trajectory uses partial_success243        use_partial_success = chosen_traj.get("partial_success") is not None244        if use_partial_success:245            partial_success = chosen_traj.get("partial_success")246            logger.trace(247                f"[PREF SAMPLER] Trajectory with partial_success detected (ID: {chosen_traj.get('id', 'unknown')}, partial_success: {partial_success})"248            )249 250        # Strategy selection: use preferred_strategy if provided, otherwise select based on ratios251        if preferred_strategy is not None:252            # Use the preferred strategy directly253            logger.trace(f"[PREF SAMPLER] Using preferred strategy: {preferred_strategy.value}")254            result = self._execute_strategy(preferred_strategy, chosen_traj, use_partial_success)255            if result is None:256                logger.trace(f"[PREF SAMPLER] Preferred strategy {preferred_strategy.value} failed, returning None")257                return None258            rejected_traj, rejected_subsample_strategy, chosen_traj = result259            strategy_used = preferred_strategy260            attempt = 1  # Set attempt for preferred strategy path261        else:262            # Strategy selection with rebalancing on failure263            strategies = []264            if self.preference_strategy_ratio[0] > 0:265                strategies.append((DataGenStrat.REWIND, self.preference_strategy_ratio[0]))266            if self._has_suboptimal and self.preference_strategy_ratio[1] > 0:267                strategies.append((DataGenStrat.SUBOPTIMAL, self.preference_strategy_ratio[1]))268            if self.preference_strategy_ratio[2] > 0:269                strategies.append((DataGenStrat.DIFFERENT_TASK, self.preference_strategy_ratio[2]))270            if self.preference_strategy_ratio[3] > 0:271                strategies.append((DataGenStrat.REVERSE_PROGRESS, self.preference_strategy_ratio[3]))272 273            max_attempts = 10  # Limit retry attempts to prevent infinite loops274            max_strategy_attempts = 3  # Maximum attempts per strategy before removing it275            attempt = 0276 277            # Track attempts per strategy278            strategy_attempt_counts = {strat: 0 for strat, _ in strategies}279 280            while rejected_traj is None and attempt < max_attempts:281                attempt += 1282 283                # Check if we have any strategies left284                if not strategies:285                    return None286 287                # Rebalance probabilities based on remaining strategies288                total_prob = sum(prob for _, prob in strategies)289                if total_prob == 0:290                    return None291 292                # Normalize probabilities293                normalized_strategies = [(strat, prob / total_prob) for strat, prob in strategies]294 295                # Select strategy based on rebalanced probabilities296                prob = random.random()297                cumulative_prob = 0.0298                selected_strategy = None299 300                for strat, normalized_prob in normalized_strategies:301                    cumulative_prob += normalized_prob302                    if prob <= cumulative_prob:303                        selected_strategy = strat304                        break305 306                # Log strategy attempt307                logger.trace(308                    f"[PREF SAMPLER] Attempt {attempt}/{max_attempts}: Trying strategy {selected_strategy.value if selected_strategy else 'None'}"309                )310 311                # Execute selected strategy312                result = self._execute_strategy(selected_strategy, chosen_traj, use_partial_success)313                if result is not None:314                    rejected_traj, rejected_subsample_strategy, chosen_traj = result315                    strategy_used = selected_strategy316                    logger.trace(f"[PREF SAMPLER] Strategy {selected_strategy.value} succeeded on attempt {attempt}")317                else:318                    # Strategy failed - increment attempt count319                    strategy_attempt_counts[selected_strategy] = strategy_attempt_counts.get(selected_strategy, 0) + 1320                    failed_count = strategy_attempt_counts[selected_strategy]321 322                    logger.trace(323                        f"[PREF SAMPLER] Strategy {selected_strategy.value} failed (failure count: {failed_count}/{max_strategy_attempts})"324                    )325 326                    # Only remove strategy if it has failed max_strategy_attempts times327                    if strategy_attempt_counts[selected_strategy] >= max_strategy_attempts:328                        logger.trace(329                            f"[PREF SAMPLER] Removing strategy {selected_strategy.value} after {max_strategy_attempts} consecutive failures"330                        )331                        strategies = [(strat, prob) for strat, prob in strategies if strat != selected_strategy]332                        continue333 334            # If we still don't have a sample after all attempts, return None335            if rejected_traj is None:336                logger.trace(337                    f"[PREF SAMPLER] Failed to generate preference sample after {max_attempts} attempts - all strategies exhausted"338                )339                return None340 341        chosen_subsample_strategy = "subsample_forward"342        chosen_trajectory = self._get_traj_from_data(chosen_traj, subsample_strategy=chosen_subsample_strategy)343 344        rejected_trajectory = self._get_traj_from_data(rejected_traj, subsample_strategy=rejected_subsample_strategy)345 346        if rejected_trajectory is None or chosen_trajectory is None:347            return None348 349        # If our strategy is different task, make sure the rejected trajectory has 0 progress and 0 success labels350        if strategy_used in [351            DataGenStrat.DIFFERENT_TASK,352            DataGenStrat.DIFFERENT_TASK_INSTRUCTION,353        ]:354            rejected_trajectory.target_progress = [0.0] * len(rejected_trajectory.target_progress)355            if self.config.progress_loss_type.lower() == "discrete":356                rejected_trajectory.target_progress = convert_continuous_to_discrete_bins(357                    rejected_trajectory.target_progress, self.config.progress_discrete_bins358                )359            # Also set success labels to 0.0 (predict 0 success for different task trajectories)360            if rejected_trajectory.success_label is not None:361                rejected_trajectory.success_label = [0.0] * len(rejected_trajectory.success_label)362 363        # Create preference sample structure364        sample = PreferenceSample(365            chosen_trajectory=chosen_trajectory,366            rejected_trajectory=rejected_trajectory,367            data_gen_strategy=strategy_used.value,368        )369        sample.resample_attempts = attempt370        return sample371