fix: reorient KernelSVC per-pair intercepts to libsvm's convention and align iris max_iter - #475
Merged
Merged
Conversation
…d 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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #471
Two entangled items on the KernelSVC rows. Neither is a fitted-model change: all 19
RESULT|records are identical before and after.Item 1: the intercept relationship is pair orientation, sign is a consequence
It is not a global sign flip. On iris one pair of three is negated; on digits, 19 of 45. The rule behind which ones is exact.
libsvm/sklearn's convention, verified
Checked against sklearn 1.9.0 with
decision_function_shape='ovo'on iris (gamma=0.25, C=1.0) rather than from recollection:For pair
(i, j)withi < j, the numerically smaller label is the +1 side, andintercept_[p]is the constant term of that same decision function.What Flow does
kernel_svc_multi_fitinlib/scikit/svm.flowforms pairs over class-discovery slots, not labels:a < bindexesclasses, whichdistinct_f32fills in order of first appearance iny_train.kernel_svc_multi_predictvotes forpair_aon a positive decision value, so Flow agrees with itself. It agrees with libsvm only when discovery order happens to be ascending by label.Instrumented fit on the canonical iris split:
Discovery order is
[0, 2, 1]. Pair(2,1)comes out with 2 as +1, so its entire decision function, intercept included, is the negative of libsvm's.msd207_ksvc_pair_orderinbenchmarks/bench_flow_v2.flowalready sorted the emitted pairs into ascending(lo, hi)label order, which is whypair_class_a/pair_class_bmatched exactly. It did not reorient the pair, so the intercept shipped with Flow's orientation.Option chosen: (b), transform at emission
The solver is left alone.
svm.flowis 76KB and the known victim of compiler bug #469, and the sign lives in they_dualencoding whose orientation is read back bykernel_svc_multi_predict,kernel_svc_multi_decision_function, and thecoef_allscatter. Flipping it would have to be verified through all three to keep predictions bit-identical, for no gain in the fitted model.msd207_ksvc_statenow negates the emitted intercept for pairs whosepair_acarries the higher label, with the mapping documented at the site.First three pairs, old and new
iris, emitted order
(0,1), (0,2), (1,2):digits, first three pairs
(0,1), (0,2), (0,3):-0.709698141, -0.740054965, -0.481934667, unchanged, all three already in libsvm orientation. The first digits pair that moves is index 4, pair(0,5):0.592164516becomes-0.592164516against sklearn's-0.591559288. In full, digits flips at indices 4, 12, 14, 19, 21, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 39, 40, 41, 44 and is unchanged at the other 26.Model-state diagnostics, regenerated locally
Both benchmark runs on this machine, one commit apart, fed through
generate_disparity_report.enrich_state_from_raw_detailsagainst the committedsklearn_results_v2.txt.intercept_per_pair_max_abs_diffintercept_per_pair_max_relative_diffintercept_per_pair_max_abs_diffintercept_per_pair_max_relative_diffintercept_per_pair_first_divergent_indexstays 0 on iris and 1 on digits. It does not clear, and I want to be plain about that rather than claim it did. Its tolerance is1e-9 + 1e-6 * max(|a|, |b|), and the residual after reorientation is 1.4e-4 (iris) and 1.1e-3 (digits) of f32-solver-versus-f64-libsvm noise. That is the same noise floor that already pinsdual_coef_abs_sum_per_pair_first_divergent_indexat 0 and 1, which the issue itself reads as agreement ("dual-coefficient sums match to 6.5e-5"). What changes is that the intercept vector no longer carries a signal 4 orders of magnitude above that floor.The rows remain
model_state_diverges: Trueeither way. The hits that survive, and are genuine:Item 2: iris
max_iter200 vs 1000_kernel_svc_smo_precomputedsetsbudget = max_iter * nand counts individual SMO pair updates, somax_iteris a sweep-equivalent multiplier rather than a sweep count. Instrumented on the canonical iris split (n = 80 per pair), cap swept over{1, 2, 3, 5, 10, 26, 50, 200, 1000, 5000}:Every pair exits on the libsvm KKT rule
gmax + gmax2 < tol, never on the budget. The worst pair needs 80 steps, one sweep-equivalent, against a declared budget of 16000, so the cap has 200x headroom. Accuracy and all three intercepts are bit-identical across the whole sweep:0.966666639and-0.134948671 / 0.012464760 / -0.017334666. The declaration is now 1000 inbench_flow_v2.flowand inparity_contract.json.Changing only the
flowblock of that contract row is whatcheck_disparity_regression.implementation_contract_resetsallows: every gate-defining field outsideflow/sklearnis unchanged, so the row establishes a fresh disparity baseline instead of tripping the gate. It also removes aconfiguration_differencesentry, andconfiguration_difference_countis one-sided atmax_increase: 0.The instrumentation for both items was a temporary
printfin_kernel_svc_smo_precomputedplus a scratch harness. Both were reverted;lib/is byte-identical tomainin this branch, andgit diff --name-only origin/maintouches nothing underlib/.Verification
bench_flow_v2.flowrun onorigin/mainand on this branch, same machine,FLOW_HOST=python FLOW_OPT_LEVEL=0 FLOW_LDFLAGS=-framework Accelerate:Both changed records differ from their predecessors by sign alone, at exactly the predicted indices. The other 144 DETAIL records are byte-identical.
Per #471's instruction on #469, with
lib/untouched:Both match
main.No generated artifacts are in this diff. Other agents were compiling into the shared
~/.local/bin/buildconcurrently; the scratch harness used a_diag471_prefix and has been deleted.Closed-form confirmation of the mechanism
Reading the class-discovery order straight out of the committed split fixtures, forming Flow's slot pairs, sorting them the way
msd207_ksvc_pair_orderdoes, and marking every pair whosepair_alabel exceeds itspair_blabel reproduces both observed flip sets exactly, without running Flow at all:Those are the same 1 and 19 indices the benchmark diff moves. Nothing else about the mechanism is left to inference.
Interaction with #474
#474 landed on
mainafter this branch was cut. It introduced declared configuration equivalences but deliberately leftmax_iterout of them, on the grounds that a parameter both sides record should always be compared. Its own before/after table listsKernelSVC_RBF / irisas surviving withmax_iter flow=200 sklearn=1000, the last remaining entry on that row.This change removes that entry the way #474 intended it to be removed: by making the declaration true rather than by exempting it.
rows_with_configuration_differencegoes 5 -> 4, and the gate is one-sided atmax_increase: 0, so a decrease cannot trip it. The two diffs share no files.