anhtld commited on
Commit
bf57df9
·
verified ·
1 Parent(s): d815c14

Add pairwise K16 CTT dominance objective

Browse files
workspace/scripts/eval_learned_dominance_selector.py CHANGED
@@ -119,6 +119,22 @@ def main(argv: list[str] | None = None) -> int:
119
  "threshold per visible task_id bucket using calibration rows only."
120
  ),
121
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  parser.add_argument("--bootstrap-samples", type=int, default=1000)
123
  args = parser.parse_args(argv)
124
 
@@ -127,6 +143,8 @@ def main(argv: list[str] | None = None) -> int:
127
  lambdas = [float(item.strip()) for item in args.ridge_lambdas.split(",") if item.strip()]
128
  if not lambdas or any(value < 0.0 for value in lambdas):
129
  parser.error("--ridge-lambdas must contain non-negative values")
 
 
130
 
131
  out_dir = args.out_dir
132
  out_dir.mkdir(parents=True, exist_ok=True)
@@ -171,6 +189,8 @@ def main(argv: list[str] | None = None) -> int:
171
  calibration_dataset,
172
  lambdas=lambdas,
173
  threshold_scope=args.threshold_scope,
 
 
174
  )
175
  eval_cases = _evaluate_dataset(eval_dataset, best["weights"], best["mean"], best["std"], tau=best["tau"])
176
  calibration_cases = _evaluate_dataset(
@@ -205,12 +225,15 @@ def main(argv: list[str] | None = None) -> int:
205
  "k": args.k,
206
  "feature_set": args.feature_set,
207
  "target": args.target,
 
 
208
  "threshold_scope": args.threshold_scope,
209
  "chart_feature_mode": chart_feature_mode,
210
  "feature_names": _feature_names(args.feature_set),
211
  "ridge_lambdas": lambdas,
212
  "selected_lambda": best["lambda"],
213
  "tau": best["tau"],
 
214
  "weights": best["weights"].tolist(),
215
  "feature_mean": best["mean"].tolist(),
216
  "feature_std": best["std"].tolist(),
@@ -248,6 +271,7 @@ def main(argv: list[str] | None = None) -> int:
248
  (out_dir / "report.md").write_text(_report(metrics) + "\n")
249
  (out_dir / "train.log").write_text(
250
  "trained ridge dominance calibrator on calibration measured rows only\n"
 
251
  f"selected_lambda={best['lambda']}\n"
252
  f"tau={_format_tau(best['tau'])}\n"
253
  f"calibration_input={args.calibration_input}\n"
@@ -628,24 +652,40 @@ def _fit_select_ridge(
628
  *,
629
  lambdas: list[float],
630
  threshold_scope: str = "global",
 
 
631
  ) -> dict[str, Any]:
632
  samples = dataset["samples"]
633
  if not samples:
634
  raise ValueError("cannot fit learned dominance selector without candidates")
635
- x = np.stack([sample["feature"] for sample in samples], axis=0)
636
- y = np.asarray([float(sample["target_margin"]) for sample in samples], dtype=float)
637
- mean = np.zeros(x.shape[1], dtype=float)
638
- std = np.ones(x.shape[1], dtype=float)
639
- mean[1:] = x[:, 1:].mean(axis=0)
640
- std[1:] = x[:, 1:].std(axis=0) + 1.0e-6
641
- x_norm = (x - mean) / std
642
- x_norm[:, 0] = 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  best: dict[str, Any] | None = None
644
  for ridge_lambda in lambdas:
645
  penalty = ridge_lambda * np.eye(x_norm.shape[1], dtype=float)
646
  penalty[0, 0] = 0.0
647
  weights = np.linalg.pinv(x_norm.T @ x_norm + penalty) @ (x_norm.T @ y)
648
- predictions = x_norm @ weights
649
  tau, selection = _choose_thresholds(
650
  dataset,
651
  predictions,
@@ -666,11 +706,94 @@ def _fit_select_ridge(
666
  "std": std,
667
  "tau": tau,
668
  "selection": selection,
 
669
  }
670
  assert best is not None
671
  return best
672
 
673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  def _choose_thresholds(
675
  dataset: dict[str, Any],
676
  predictions: np.ndarray,
@@ -921,6 +1044,7 @@ def _report(metrics: dict[str, Any]) -> str:
921
  f"Selected ridge lambda: `{metrics['selected_lambda']}`",
922
  f"Tau: `{_format_tau(metrics['tau'])}`",
923
  f"Threshold scope: `{metrics.get('threshold_scope', 'global')}`",
 
924
  f"Feature set: `{metrics['feature_set']}`",
925
  f"Target: `{metrics['target']}`",
926
  "",
 
119
  "threshold per visible task_id bucket using calibration rows only."
120
  ),
121
  )
122
+ parser.add_argument(
123
+ "--fit-objective",
124
+ choices=("pointwise", "pairwise", "hybrid_pairwise"),
125
+ default="pointwise",
126
+ help=(
127
+ "Fit the linear utility proxy from candidate-level margins, "
128
+ "within-chart pairwise causal comparisons, or both. Pairwise "
129
+ "comparisons are built from calibration rows only."
130
+ ),
131
+ )
132
+ parser.add_argument(
133
+ "--pairwise-weight",
134
+ type=float,
135
+ default=1.0,
136
+ help="Relative weight for pairwise rows when --fit-objective=hybrid_pairwise.",
137
+ )
138
  parser.add_argument("--bootstrap-samples", type=int, default=1000)
139
  args = parser.parse_args(argv)
140
 
 
143
  lambdas = [float(item.strip()) for item in args.ridge_lambdas.split(",") if item.strip()]
144
  if not lambdas or any(value < 0.0 for value in lambdas):
145
  parser.error("--ridge-lambdas must contain non-negative values")
146
+ if args.pairwise_weight <= 0.0:
147
+ parser.error("--pairwise-weight must be positive")
148
 
149
  out_dir = args.out_dir
150
  out_dir.mkdir(parents=True, exist_ok=True)
 
189
  calibration_dataset,
190
  lambdas=lambdas,
191
  threshold_scope=args.threshold_scope,
192
+ fit_objective=args.fit_objective,
193
+ pairwise_weight=args.pairwise_weight,
194
  )
195
  eval_cases = _evaluate_dataset(eval_dataset, best["weights"], best["mean"], best["std"], tau=best["tau"])
196
  calibration_cases = _evaluate_dataset(
 
225
  "k": args.k,
226
  "feature_set": args.feature_set,
227
  "target": args.target,
228
+ "fit_objective": args.fit_objective,
229
+ "pairwise_weight": args.pairwise_weight,
230
  "threshold_scope": args.threshold_scope,
231
  "chart_feature_mode": chart_feature_mode,
232
  "feature_names": _feature_names(args.feature_set),
233
  "ridge_lambdas": lambdas,
234
  "selected_lambda": best["lambda"],
235
  "tau": best["tau"],
236
+ "fit_design": best["fit_design"],
237
  "weights": best["weights"].tolist(),
238
  "feature_mean": best["mean"].tolist(),
239
  "feature_std": best["std"].tolist(),
 
271
  (out_dir / "report.md").write_text(_report(metrics) + "\n")
272
  (out_dir / "train.log").write_text(
273
  "trained ridge dominance calibrator on calibration measured rows only\n"
274
+ f"fit_objective={args.fit_objective}\n"
275
  f"selected_lambda={best['lambda']}\n"
276
  f"tau={_format_tau(best['tau'])}\n"
277
  f"calibration_input={args.calibration_input}\n"
 
652
  *,
653
  lambdas: list[float],
654
  threshold_scope: str = "global",
655
+ fit_objective: str = "pointwise",
656
+ pairwise_weight: float = 1.0,
657
  ) -> dict[str, Any]:
658
  samples = dataset["samples"]
659
  if not samples:
660
  raise ValueError("cannot fit learned dominance selector without candidates")
661
+ if fit_objective not in {"pointwise", "pairwise", "hybrid_pairwise"}:
662
+ raise ValueError(f"unknown fit_objective: {fit_objective}")
663
+ point_x = np.stack([sample["feature"] for sample in samples], axis=0)
664
+ mean = np.zeros(point_x.shape[1], dtype=float)
665
+ std = np.ones(point_x.shape[1], dtype=float)
666
+ mean[1:] = point_x[:, 1:].mean(axis=0)
667
+ std[1:] = point_x[:, 1:].std(axis=0) + 1.0e-6
668
+ x_norm, y, fit_design = _fit_design_matrix(
669
+ dataset,
670
+ fit_objective=fit_objective,
671
+ pairwise_weight=pairwise_weight,
672
+ mean=mean,
673
+ std=std,
674
+ )
675
+ if x_norm.shape[0] == 0:
676
+ raise ValueError("fit objective produced no training rows")
677
+ candidate_x_norm = _normalized_candidate_features(dataset, mean=mean, std=std)
678
+ fit_design = {
679
+ **fit_design,
680
+ "num_candidate_rows": int(candidate_x_norm.shape[0]),
681
+ "num_fit_rows": int(x_norm.shape[0]),
682
+ }
683
  best: dict[str, Any] | None = None
684
  for ridge_lambda in lambdas:
685
  penalty = ridge_lambda * np.eye(x_norm.shape[1], dtype=float)
686
  penalty[0, 0] = 0.0
687
  weights = np.linalg.pinv(x_norm.T @ x_norm + penalty) @ (x_norm.T @ y)
688
+ predictions = candidate_x_norm @ weights
689
  tau, selection = _choose_thresholds(
690
  dataset,
691
  predictions,
 
706
  "std": std,
707
  "tau": tau,
708
  "selection": selection,
709
+ "fit_design": fit_design,
710
  }
711
  assert best is not None
712
  return best
713
 
714
 
715
+ def _normalized_candidate_features(
716
+ dataset: dict[str, Any],
717
+ *,
718
+ mean: np.ndarray,
719
+ std: np.ndarray,
720
+ ) -> np.ndarray:
721
+ x = np.stack([sample["feature"] for sample in dataset["samples"]], axis=0)
722
+ x_norm = (x - mean) / std
723
+ x_norm[:, 0] = 1.0
724
+ return x_norm
725
+
726
+
727
+ def _fit_design_matrix(
728
+ dataset: dict[str, Any],
729
+ *,
730
+ fit_objective: str,
731
+ pairwise_weight: float,
732
+ mean: np.ndarray,
733
+ std: np.ndarray,
734
+ ) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
735
+ point_x = np.stack([sample["feature"] for sample in dataset["samples"]], axis=0)
736
+ point_y = np.asarray([float(sample["target_margin"]) for sample in dataset["samples"]], dtype=float)
737
+ point_x_norm = (point_x - mean) / std
738
+ point_x_norm[:, 0] = 1.0
739
+ if fit_objective == "pointwise":
740
+ return point_x_norm, point_y, {
741
+ "fit_objective": fit_objective,
742
+ "num_pointwise_rows": int(point_x.shape[0]),
743
+ "num_pairwise_rows": 0,
744
+ "pairwise_weight": 0.0,
745
+ }
746
+
747
+ pair_x, pair_y = _pairwise_design_matrix(dataset)
748
+ pair_x_norm = pair_x / std
749
+ pair_x_norm[:, 0] = 0.0
750
+ if fit_objective == "pairwise":
751
+ return pair_x_norm, pair_y, {
752
+ "fit_objective": fit_objective,
753
+ "num_pointwise_rows": 0,
754
+ "num_pairwise_rows": int(pair_x.shape[0]),
755
+ "pairwise_weight": 1.0,
756
+ }
757
+
758
+ if fit_objective == "hybrid_pairwise":
759
+ scale = math.sqrt(float(pairwise_weight))
760
+ x = np.concatenate([point_x_norm, pair_x_norm * scale], axis=0)
761
+ y = np.concatenate([point_y, pair_y * scale], axis=0)
762
+ return x, y, {
763
+ "fit_objective": fit_objective,
764
+ "num_pointwise_rows": int(point_x.shape[0]),
765
+ "num_pairwise_rows": int(pair_x.shape[0]),
766
+ "pairwise_weight": float(pairwise_weight),
767
+ }
768
+
769
+ raise ValueError(f"unknown fit_objective: {fit_objective}")
770
+
771
+
772
+ def _pairwise_design_matrix(dataset: dict[str, Any]) -> tuple[np.ndarray, np.ndarray]:
773
+ x_rows: list[np.ndarray] = []
774
+ y_rows: list[float] = []
775
+ for sample_indices in dataset["by_row"].values():
776
+ for left_pos, left_index in enumerate(sample_indices):
777
+ left = dataset["samples"][left_index]
778
+ left_target = float(left["target_margin"])
779
+ for right_index in sample_indices[left_pos + 1 :]:
780
+ right = dataset["samples"][right_index]
781
+ right_target = float(right["target_margin"])
782
+ target_delta = left_target - right_target
783
+ if abs(target_delta) <= 1.0e-9:
784
+ continue
785
+ feature_delta = np.asarray(left["feature"], dtype=float) - np.asarray(
786
+ right["feature"], dtype=float
787
+ )
788
+ x_rows.append(feature_delta)
789
+ y_rows.append(target_delta)
790
+ x_rows.append(-feature_delta)
791
+ y_rows.append(-target_delta)
792
+ if not x_rows:
793
+ raise ValueError("pairwise objective requires at least one non-tied within-row comparison")
794
+ return np.stack(x_rows, axis=0), np.asarray(y_rows, dtype=float)
795
+
796
+
797
  def _choose_thresholds(
798
  dataset: dict[str, Any],
799
  predictions: np.ndarray,
 
1044
  f"Selected ridge lambda: `{metrics['selected_lambda']}`",
1045
  f"Tau: `{_format_tau(metrics['tau'])}`",
1046
  f"Threshold scope: `{metrics.get('threshold_scope', 'global')}`",
1047
+ f"Fit objective: `{metrics.get('fit_objective', 'pointwise')}`",
1048
  f"Feature set: `{metrics['feature_set']}`",
1049
  f"Target: `{metrics['target']}`",
1050
  "",