|
23 | 23 | from codes.utils import nice_print |
24 | 24 |
|
25 | 25 |
|
| 26 | +def pareto_front(points: np.ndarray) -> np.ndarray: |
| 27 | + # lower-is-better for both objectives |
| 28 | + is_efficient = np.ones(points.shape[0], dtype=bool) |
| 29 | + for i, p in enumerate(points): |
| 30 | + if not is_efficient[i]: |
| 31 | + continue |
| 32 | + # any other point strictly better in both dims dominates p |
| 33 | + better = np.all(points <= p, axis=1) & np.any(points < p, axis=1) |
| 34 | + dominated = better & (np.arange(points.shape[0]) != i) |
| 35 | + if np.any(dominated): |
| 36 | + is_efficient[i] = False |
| 37 | + return points[is_efficient] |
| 38 | + |
| 39 | + |
| 40 | +def hypervolume_2d(pareto_points: np.ndarray, reference: np.ndarray) -> float: |
| 41 | + # assumes minimize-minimize; reference worse than all pareto_points |
| 42 | + if pareto_points.size == 0: |
| 43 | + return 0.0 |
| 44 | + pts = pareto_points[np.argsort(pareto_points[:, 0])] # sort by first objective |
| 45 | + hv = 0.0 |
| 46 | + prev_f2 = reference[1] |
| 47 | + for f1, f2 in pts: |
| 48 | + width = reference[0] - f1 |
| 49 | + height = prev_f2 - f2 |
| 50 | + if width > 0 and height > 0: |
| 51 | + hv += width * height |
| 52 | + prev_f2 = f2 |
| 53 | + return hv |
| 54 | + |
| 55 | + |
| 56 | +def compute_hypervolume_over_time(study: optuna.Study, ref_slack=1.1): |
| 57 | + from optuna.trial import TrialState |
| 58 | + |
| 59 | + completed = [t for t in study.trials if t.state == TrialState.COMPLETE] |
| 60 | + if not completed: |
| 61 | + return [], None |
| 62 | + |
| 63 | + # Order by completion time |
| 64 | + completed.sort(key=lambda t: t.datetime_complete or t.datetime_start) |
| 65 | + all_vals = np.array([t.values for t in completed]) # shape (N, 2) |
| 66 | + reference = all_vals.max(axis=0) * ref_slack # slightly worse than worst seen |
| 67 | + |
| 68 | + hypervolumes = [] |
| 69 | + for k in range(1, len(completed) + 1): |
| 70 | + subset = completed[:k] |
| 71 | + pts = np.array([t.values for t in subset]) |
| 72 | + pareto = pareto_front(pts) |
| 73 | + hv = hypervolume_2d(pareto, reference) |
| 74 | + hypervolumes.append(hv) |
| 75 | + return hypervolumes, reference |
| 76 | + |
| 77 | + |
26 | 78 | def load_loss_history(model_path: str) -> tuple[np.ndarray, np.ndarray, int]: |
27 | 79 | """ |
28 | 80 | Load loss histories from a saved model file (.pth). |
@@ -218,6 +270,49 @@ def evaluate_tuning( |
218 | 270 | print(f"Could not load study '{full_name}'") |
219 | 271 | continue |
220 | 272 |
|
| 273 | + # Compute hypervolume over time |
| 274 | + if len(study.directions) == 2: |
| 275 | + hvs, reference = compute_hypervolume_over_time(study) |
| 276 | + if hvs: |
| 277 | + # Normalize to final hypervolume for relative curve |
| 278 | + final_hv = hvs[-1] |
| 279 | + rel_hvs = [hv / final_hv if final_hv > 0 else 0 for hv in hvs] |
| 280 | + |
| 281 | + # Plot absolute and relative hypervolume |
| 282 | + plt.figure(figsize=(6, 4)) |
| 283 | + plt.plot(np.arange(1, len(hvs) + 1), hvs, label="Hypervolume") |
| 284 | + plt.xlabel("Completed Trials") |
| 285 | + plt.ylabel("Hypervolume") |
| 286 | + plt.title(f"{suffix} Hypervolume over trials") |
| 287 | + plt.grid(True) |
| 288 | + plt.tight_layout() |
| 289 | + plt.savefig( |
| 290 | + os.path.join(save_dir, f"hypervolume_{suffix}.png"), dpi=300 |
| 291 | + ) |
| 292 | + plt.close() |
| 293 | + |
| 294 | + plt.figure(figsize=(6, 4)) |
| 295 | + plt.plot( |
| 296 | + np.arange(1, len(rel_hvs) + 1), |
| 297 | + rel_hvs, |
| 298 | + label="Relative Hypervolume", |
| 299 | + ) |
| 300 | + plt.xlabel("Completed Trials") |
| 301 | + plt.ylabel("Fraction of Final HV") |
| 302 | + plt.title(f"{suffix} Relative Hypervolume") |
| 303 | + plt.grid(True) |
| 304 | + plt.tight_layout() |
| 305 | + plt.savefig( |
| 306 | + os.path.join(save_dir, f"hypervolume_relative_{suffix}.png"), |
| 307 | + dpi=300, |
| 308 | + ) |
| 309 | + plt.close() |
| 310 | + print(f"Saved hypervolume plots for {suffix} (final HV={final_hv:.3e})") |
| 311 | + else: |
| 312 | + print("No hypervolume computed (no complete trials).") |
| 313 | + else: |
| 314 | + print("Skipping hypervolume: study is not two-objective.") |
| 315 | + |
221 | 316 | best = get_best_trials(study, top_n) |
222 | 317 | if not best: |
223 | 318 | print(f"No completed trials in {full_name}") |
@@ -285,19 +380,19 @@ def parse_args(): |
285 | 380 | p.add_argument( |
286 | 381 | "--study_name", |
287 | 382 | type=str, |
288 | | - default="cloud_tuning_rough", |
| 383 | + default="cloud_tuning_fine", |
289 | 384 | help="Main study prefix (e.g. lvparams5)", |
290 | 385 | ) |
291 | 386 | p.add_argument( |
292 | 387 | "--storage_name", |
293 | 388 | type=str, |
294 | | - default="optuna_cloud", |
| 389 | + default="optuna_cloud_2", |
295 | 390 | help="Main study prefix (e.g. lvparams5)", |
296 | 391 | ) |
297 | 392 | p.add_argument( |
298 | 393 | "--top_n", |
299 | 394 | type=int, |
300 | | - default=10, |
| 395 | + default=20, |
301 | 396 | help="Number of top trials to plot per surrogate", |
302 | 397 | ) |
303 | 398 | return p.parse_args() |
|
0 commit comments