From 4732609c48f96369fe636df475aef22c73c391bb Mon Sep 17 00:00:00 2001 From: godofecht Date: Fri, 21 Aug 2026 12:40:37 +0100 Subject: [PATCH] fix: reorient KernelSVC per-pair intercepts to libsvm's convention and align iris max_iter kernel_svc_multi_fit pairs classes by discovery slot, so a pair whose slot order runs descending by label gets the higher label as its +1 side. libsvm always makes the lower label +1 for pair (i, j), i < j, so those pairs' decision functions, and therefore their intercepts, are the negative of sklearn's. Flow is internally consistent either way: predict votes for pair_a on a positive decision value, which is why predictions and dual coefficient sums already agreed. msd207_ksvc_state already sorted the emitted pairs into ascending label order but did not reorient them. It now negates the intercept for pairs whose pair_a carries the higher label. The solver is untouched, keeping svm.flow clear of compiler bug #469. iris declared max_iter 200 against sklearn's 1000. max_iter multiplies into an SMO step budget of max_iter * n; every iris pair exits on the libsvm KKT rule after at most 80 steps, one sweep-equivalent at n = 80. Fits are bit-identical for every cap from 1 to 5000, so the declaration is aligned to 1000 in both bench_flow_v2.flow and parity_contract.json. All 19 RESULT records identical. 144 of 146 DETAIL records identical; the two that move are the intercept_per_pair vectors, by sign only. Closes #471 Co-Authored-By: Claude Opus 5 (1M context) --- benchmarks/bench_flow_v2.flow | 18 ++++++++++++++++-- benchmarks/parity_contract.json | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/benchmarks/bench_flow_v2.flow b/benchmarks/bench_flow_v2.flow index e974151..27f762a 100644 --- a/benchmarks/bench_flow_v2.flow +++ b/benchmarks/bench_flow_v2.flow @@ -672,11 +672,20 @@ function msd207_ksvc_state(dataset: string, m: KernelSVCMulti) -> void { let p: i32 = order[k] let la: f32 = m.classes[m.pair_a[p]] let lb: f32 = m.classes[m.pair_b[p]] + # Issue #471: kernel_svc_multi_fit pairs classes by discovery slot, so + # for a pair whose slot order is descending by label the +1 side is the + # higher label and the whole decision function, intercept included, is + # the negative of libsvm's. Reorient here so the emitted intercept is + # the one for the pair oriented (lo, hi) with lo as +1, which is what + # sklearn's intercept_ holds. Nothing about the fitted model changes; + # predictions use the model's own orientation and are untouched. let mut lo: f32 = la let mut hi: f32 = lb + let mut orient: f32 = 1.0 if lb < la { lo = lb hi = la + orient = -1.0 } class_a[k] = lo class_b[k] = hi @@ -696,7 +705,7 @@ function msd207_ksvc_state(dataset: string, m: KernelSVCMulti) -> void { n_support[k] = sv n_bounded[k] = bounded dual_abs_sum[k] = total as f32 - intercepts[k] = m.models[p].b + intercepts[k] = orient * m.models[p].b total_support = total_support + sv } @@ -819,7 +828,12 @@ function main() -> i32 { array_free_f32(lsvc_i_pred); linear_svc_multi_free(lsvc_i) t0 = now_ns() - let ksvc_i: KernelSVCMulti = kernel_svc_multi_fit(Xi_tr, si.y_train, 3, 0.25, 1.0, 200) + # Issue #471: the cap is a backstop, not a schedule. max_iter multiplies + # into an SMO step budget of max_iter * n; every iris pair exits on the + # libsvm KKT rule after at most 80 steps, one sweep-equivalent at n = 80. + # Declared 200 here and 1000 on digits; sklearn declares 1000 on both. + # 1000 matches the sklearn side and leaves the fit bit-identical. + let ksvc_i: KernelSVCMulti = kernel_svc_multi_fit(Xi_tr, si.y_train, 3, 0.25, 1.0, 1000) t1 = now_ns() let ksvc_i_pred: ptr = kernel_svc_multi_predict(ksvc_i, Xi_te) t2 = now_ns() diff --git a/benchmarks/parity_contract.json b/benchmarks/parity_contract.json index e10e20d..7bc8a3e 100644 --- a/benchmarks/parity_contract.json +++ b/benchmarks/parity_contract.json @@ -34,7 +34,7 @@ "parity_level": "approximate", "score_abs_tolerance": 0.03, "timing_comparable": true, - "flow": {"kernel": "rbf", "gamma": 0.25, "C": 1.0, "max_iter": 200, "multiclass": "ovo"}, + "flow": {"kernel": "rbf", "gamma": 0.25, "C": 1.0, "max_iter": 1000, "multiclass": "ovo"}, "sklearn": {"kernel": "rbf", "gamma": 0.25, "C": 1.0, "max_iter": 1000, "multiclass": "ovo"} }, {