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 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
x_norm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
"",
|