Evaluate prediction postprocessing during cross-validation - #448
Draft
gbeane wants to merge 2 commits into
Draft
Conversation
Adds a per-behavior "Evaluate in Cross-Validation" toggle to the Prediction Postprocessing dialog. When on, each binary CV fold re-predicts the held-out group's full tracks, applies the postprocessing pipeline, and scores the labeled frames, so the training report shows raw and postprocessed metrics side by side. Also fixes SettingsManager.save_behavior writing a new behavior's settings into the shared project defaults.
gbeane
force-pushed
the
cv-postprocessing-evaluation
branch
from
September 1, 2026 15:37
878cd42 to
37b570e
Compare
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.
KLAUS-588
Adds an option to measure how prediction postprocessing (stitching, duration filtering, gap interpolation) affects classifier performance, by reporting postprocessed metrics alongside the raw ones in cross-validation.
Draft: the design decisions below are worth agreeing on before review effort goes into the details.
Where the setting lives
A per-behavior Cross-Validation Evaluation group at the bottom of the Tools → Prediction Postprocessing dialog, stored as
behavior[<name>].evaluate_postprocessing_in_cv.It went there rather than into Project Settings because the postprocessing stage configuration is itself behavior-scoped, the toggle is meaningless without seeing which stages are enabled next to it, and postprocessing is binary-only while Project Settings is shared with multi-class mode. No new controls on the main window.
Why the held-out tracks are re-predicted
This is the part most worth scrutinising.
Cross-validation normally scores a fold using only the labeled frames of the held-out group. Those rows are a sparse, gap-collapsed subset of the video:
collect_binary_labeled_featureskeeps only labeled frames, and_extract_identity_featuresreturns a DataFrame with a freshRangeIndex, so frame numbers are gone and two labeled bouts thousands of frames apart become adjacent rows. Under Video or Filename Pattern grouping a fold also concatenates several identities back to back.Running the pipeline on that array would stitch gaps that do not exist and measure bout durations in "labeled frames" rather than frames. The numbers would look entirely plausible and be wrong, which is worse than not shipping the feature.
So each fold instead re-predicts the held-out group's full tracks the way the classify path does, applies the pipeline to the full-length vectors, and only then restricts to the labeled frames where ground truth exists. That also means the
-1(no-pose) runsGapInterpolationStagefills actually exist — on the labeled-rows-only path they never do, so that stage would silently be a no-op.tests/classifier/test_cv_postprocessing.py::test_postprocessing_uses_full_track_not_just_labeled_framespins this: two 1-frame gaps that stitch over the full track would form one unstitchable 2-frame gap on the labeled rows alone, so the test fails if the implementation ever regresses to the sparse approach.Implementation
jabs/classifier/inference.py—predict_identity(), extracted fromClassifyThreadso cross-validation and classification share one implementation and cannot drift. This is what makes the reported numbers reflect what classification actually produces.jabs/classifier/cv_postprocessing.py— loads ground truth, applies the pipeline to full-length prediction vectors, masks to labeled frames. Members are processed grouped by video so each pose file opens once, and one identity's features are released before the next loads (a full-video feature matrix for a long video runs to roughly a gigabyte, so a whole group's worth at once is not viable).Project._assign_cv_group_idsnow records amemberslist of(video, identity)pairs per group. Video and Filename Pattern entries carriedidentity: None, so there was previously no way to know which identities to re-predict.labels=[0, 1]. Postprocessing can in principle leave a frame with no prediction, and letting sklearn infer the label set would make-1its own class and silently shift which array element belongs to which metric.PostprocessedMetricsonBinaryCVResult, rendered as summary means plus a second iteration table, with the evaluated stage configuration recorded so a saved report is self-describing. Raw metrics are untouched so the two are directly comparable, and the session tracker still receives raw accuracy.jabs-cli cross-validationhonours the same behavior setting, with--postprocessing/--no-postprocessingto override, and two comparison columns in its console table.Cost
Each group is the held-out group in exactly one fold, so the extra prediction work totals one pass over the labeled identities per training run — not one pass per fold. Features come from the cache the preceding labeled-features pass already wrote, so the per-fold cost is a disk read plus
predict_probarather than a recompute. A cold or invalidated feature cache makes the first run slower; that is called out in the dialog help text and the user guide. Progress still advances once per fold, with per-identity status text.Guard rails
Drive-by fix
SettingsManager.save_behaviordidmerged_data = all_behavior_data.get(behavior, defaults)and then mutated the result, so saving settings for a behavior with no entry yet wrote them straight into the shared projectdefaultsdict. Verified against a real project file: enabling this toggle on a new behavior would have made it the default for every behavior created afterwards. Fixed by copying, with a regression test. The bug already affectedpostprocessingand every other behavior setting, so this is broader than the feature needed.Testing
New:
tests/classifier/test_inference.py,tests/classifier/test_cv_postprocessing.py. Extended: the cross-validation, training-report, settings-manager, CV-grouping, settings-dialog, training-thread and CLI test modules.Rebased onto
93991bccand re-verified in the full environment (uv sync --all-packages --all-extras --all-groups): root 1105, jabs-io 299, jabs-vision 177, jabs-core 88, jabs-behavior 113 — all passing.ruff checkandruff format --checkclean. The new setting was also round-tripped through the realSettingsManagerrather than only a fake.Docs
The in-app user guide page (
src/jabs/resources/docs/user_guide/postprocessing.md) documents the option. The published site has no postprocessing page at all, tracked separately in KLAUS-587.