]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
perf(qav3): switch to Chebyshev distance method for MO optimization
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 25 May 2025 12:33:40 +0000 (14:33 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 25 May 2025 12:33:40 +0000 (14:33 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index 1e2dc857fe73080cc2f63fabbb572f9a080c0209..233d90d9974a819e2ebf893e03d98e9b0f94bb2c 100644 (file)
@@ -45,7 +45,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.7.64"
+    version = "3.7.65"
 
     @cached_property
     def _optuna_config(self) -> dict:
@@ -397,59 +397,60 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     def get_multi_objective_study_best_trial(
         self, namespace: str, study: optuna.study.Study
     ) -> Optional[optuna.trial.FrozenTrial]:
+        if namespace != "label":
+            raise ValueError(f"Unsupported namespace: {namespace}")
+
         if not QuickAdapterRegressorV3.optuna_study_has_best_trials(study):
             return None
-        best_trials = study.best_trials
-        if len(best_trials) == 1:
-            return best_trials[0]
-        if namespace == "label":
-            pivots_sizes = [trial.values[1] for trial in best_trials]
-            quantile_pivots_size = np.quantile(
-                pivots_sizes, self.ft_params.get("label_quantile", 0.5)
+
+        n_objectives = len(study.directions)
+        if n_objectives != 2:
+            raise ValueError(
+                f"Expected 2 objectives for {namespace} namespace, but got {n_objectives}"
             )
-            equal_quantile_pivots_size_trials = [
-                trial
-                for trial in best_trials
-                if np.isclose(trial.values[1], quantile_pivots_size)
-            ]
-            if equal_quantile_pivots_size_trials:
-                return max(
-                    equal_quantile_pivots_size_trials, key=lambda trial: trial.values[0]
-                )
-            nearest_above_quantile = (
-                np.inf,
-                -np.inf,
-                None,
-            )  # (trial_pivots_size, trial_scaled_natr, trial_index)
-            nearest_below_quantile = (
-                -np.inf,
-                -np.inf,
-                None,
-            )  # (trial_pivots_size, trial_scaled_natr, trial_index)
-            for idx, trial in enumerate(best_trials):
-                pivots_size = trial.values[1]
-                if pivots_size >= quantile_pivots_size:
-                    if pivots_size < nearest_above_quantile[0] or (
-                        pivots_size == nearest_above_quantile[0]
-                        and trial.values[0] > nearest_above_quantile[1]
-                    ):
-                        nearest_above_quantile = (pivots_size, trial.values[0], idx)
-                if pivots_size <= quantile_pivots_size:
-                    if pivots_size > nearest_below_quantile[0] or (
-                        pivots_size == nearest_below_quantile[0]
-                        and trial.values[0] > nearest_below_quantile[1]
-                    ):
-                        nearest_below_quantile = (pivots_size, trial.values[0], idx)
-            if nearest_above_quantile[2] is None or nearest_below_quantile[2] is None:
-                return None
-            above_quantile_trial = best_trials[nearest_above_quantile[2]]
-            below_quantile_trial = best_trials[nearest_below_quantile[2]]
-            if above_quantile_trial.values[0] >= below_quantile_trial.values[0]:
-                return above_quantile_trial
+
+        best_trials = [
+            trial
+            for trial in study.best_trials
+            if trial.values is not None and len(trial.values) == n_objectives
+        ]
+        if not best_trials:
+            return None
+
+        objective_values = np.array([trial.values for trial in best_trials]).T
+        normalized_values_list = []
+        for values, direction in zip(objective_values, study.directions):
+            min_value, max_value = np.min(values), np.max(values)
+            range_value = max_value - min_value
+
+            if np.isclose(range_value, 0):
+                normalized_values = np.full_like(values, 0.5)
             else:
-                return below_quantile_trial
-        else:
-            raise ValueError(f"Invalid namespace: {namespace}")
+                if direction == optuna.study.StudyDirection.MAXIMIZE:
+                    normalized_values = (values - min_value) / range_value
+                else:
+                    normalized_values = (max_value - values) / range_value
+
+            normalized_values_list.append(normalized_values)
+
+        trial_chebyshev_distances = []
+        for i, trial in enumerate(best_trials):
+            try:
+                chebyshev_distance = np.max(
+                    [
+                        1.0 - normalized_values[i]
+                        for normalized_values in normalized_values_list
+                    ]
+                )
+                trial_chebyshev_distances.append((trial, chebyshev_distance))
+            except (IndexError, TypeError, ValueError):
+                continue
+
+        return (
+            min(trial_chebyshev_distances, key=lambda k: k[1])[0]
+            if trial_chebyshev_distances
+            else None
+        )
 
     def optuna_optimize(
         self,
index f4b0b39280b63d5b637591c2a1a028431fd9beca..0ad3a8cbd2d4fad1534b1cd7d20a457236d92567 100644 (file)
@@ -60,7 +60,7 @@ class QuickAdapterV3(IStrategy):
     INTERFACE_VERSION = 3
 
     def version(self) -> str:
-        return "3.3.66"
+        return "3.3.67"
 
     timeframe = "5m"