Craft object detection - #173
Conversation
3bee32a to
449e3a8
Compare
449e3a8 to
b9b940d
Compare
b9b940d to
0ac1eee
Compare
0ac1eee to
f89b53b
Compare
f72b85a to
4d52e85
Compare
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
c8e0808 to
ef68750
Compare
0e33a43 to
d6df434
Compare
|
Thank you very much for this PR, there is a ton of work that has been put into creating a lot of new features! The architecture that was implemented allows for a nice generalization across different object detection architectures and frameworks, with plenty of unit tests to ensure non-regression and that everything is working well, and the new functionality seems easy to use by new users. Documentation, the new plotting functions, and backward compatibility with the original Craft method are really nice to have, too. However, I spotted some potential issues that might need fixing before merging. Note: do not hesitate to argue if I'm wrong here. This PR is quite large, and I might have missed stuff:
I've left plenty of comments and questions. I hope I didn't miss anything! |
5f9f8e4 to
1735571
Compare
|
I pushed a new PR to take into account Agu's reviews, except the new FSITA solver (I am currently merging it) |
709d925 to
b5f8a10
Compare
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
…rmaps[] Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
…atrix - Add [project.optional-dependencies].torch in pyproject.toml so users can install optional PyTorch support via 'pip install "xplique[torch]"'. - Extend tox torch envs to cover tests/concepts/test_latent_extractor_torch.py and normalize indentation on the box-manager entries. - Document the new install path in docs/index.md.
- sanitize_input_output now forwards **kwargs so caller-provided options such as verbose=True reach the wrapped explainer. - HSIC estimators and attribution methods use consistent HWC ordering end-to-end. - Sobol/HSIC accept nb_channels as a keyword-only argument appended last, preserving the positional order of nb_design and other historical positional parameters (backwards-compatible for existing callers). - Add regression coverage in tests/attributions/test_hsic.py and tests/attributions/test_sobol.py. Notes: - Public API change: nb_channels is now keyword-only. Callers that were passing it positionally must switch to nb_channels=....
…nsors - TfClassifierTensor and TorchClassifierTensor now construct one-hot targets from a validated class_id and raise clearly when class_id is missing or out of range. - TorchClassifierTensor.filter preserves subclass identity via as_subclass so downstream code that isinstance-checks the tensor type continues to work regardless of upstream torch.zeros_like semantics. Notes: - Behavioral change: previous fallbacks that treated a missing class_id as an implicit whole-batch target now raise. Callers must supply a valid class_id for classification targeting.
69e697e to
5bd1324
Compare
…ruction
- filter() on classifier tensors is reverted to a pure no-op: classifiers
have no boxes to reduce, so class_id has no meaning at the filter stage.
- Add to_attribution_target(class_id) to the StructuredPrediction protocol:
- classifiers: builds one_hot(class_id, num_classes); actual logit/prob
values are discarded since only the output shape matters.
- OD: returns self — filtered boxes are already the correct target.
- holistic_craft.compute_explanation_per_concept now calls
filtered_result.to_attribution_target(class_id).to_batched_tensor() to
make the two responsibilities explicit.
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
- Gradient checkers (TF and Torch) run independent VJP probes and reject zero, NaN, Inf, and cancellation outcomes so silently broken gradient paths surface immediately. - Torch checker uses torch.autograd.grad on a properly device-placed leaf tensor, snapshots per-module training flags to restore them on exit, and never mutates parameter .grad attributes. - TorchWrapper honours requires_grad=False by raising a clear error when a gradient is requested through the wrapper, instead of silently returning zeros. Notes: - Behavioral change: TorchWrapper(requires_grad=False) now raises when a caller attempts to compute gradients. Callers relying on the silent zero-gradient behaviour must construct the wrapper with the default requires_grad=True.
- object_detection_operator handles zero-padded detection rows so variable-length detections can be represented as dense zero-padded tensors without altering explanations. - Replace the tf.cond rank branch with tf.reshape + tf.ensure_shape so the operator stays trace-safe under tf.function. - Downstream object_detection_operators and operators_operations wiring updated to preserve the new contract. - Regression coverage added in tests/commons/test_object_detection_operator.py and tests/commons/test_operators_operations.py, including an inside-function assertion to lock the graph-safe rank branch.
- NumPy/TF/Torch box managers promote integer inputs to floating point, preserve trailing metadata columns, and use graph-safe positive-size checks that trace cleanly under tf.function. - Box formatters accept rank-1 score tensors so single-detection inputs no longer require an explicit expand_dims. - TfMultiBoxTensor.__len__ returns the number of detections. TF and Torch MultiBoxTensor.filter now use '>=' on the confidence threshold in every branch for symmetric behaviour. - New regression coverage in tests/utils_functions/test_box_manager.py and tests/utils_functions/test_box_utilities.py.
- Introduce _pad_and_stack_box_predictions and wire it through the TF and Torch box model wrappers so variable-length detection outputs are stacked losslessly into zero-padded dense tensors. List-mode output remains unchanged. - OutputAsListMixin updated to interoperate with the padded stacking contract. - Add end-to-end coverage in tests/utils_functions/test_box_model_wrapper.py and expand tests/utils_functions/test_object_detection.py.
- image._resize adds and strips a singleton channel so 2D heatmaps can be resized without shape errors. - object_detection heatmap rendering now uses the image extent so overlays align with the underlying image regardless of aspect ratio. - plots.__init__ and plots.metrics adjusted to expose the corrected helpers. - Regression coverage in tests/plots/test_metric_plots.py and new tests/plots/test_object_detection_plots.py.
…ync, structured batching - TF layered builder splits Functional graphs via tf.keras.Model over the selected tensor and rejects branch-internal cuts with a clear error. The split layer is included in g for parity with the Torch path. - Torch layered split is inclusive and index-normalized; the split module is included in g. Empty and rank-invalid inputs raise clean errors. - Torch LatentExtractor selects the device automatically (CUDA if available, else CPU), validates any user-supplied device, and .to() keeps self.device in sync. - Generator-based extractors exit torch.no_grad() before yielding so downstream code can still compute gradients on the yielded batches. - Non-list structured outputs are routed through to_batched_tensor so detection-shaped predictions batch consistently. - New regression suites in tests/concepts/test_latent_extractor_tf.py and tests/concepts/test_latent_extractor_torch.py. Notes: - Behavioral change: TF layered splits that target a tensor inside a branch (rather than a Functional cut point) now raise instead of silently producing an incorrect graph. - Behavioral change: Torch LatentExtractor defaults to CUDA when available. Callers requiring CPU should pass device="cpu" explicitly.
…, invalid-shape errors - HolisticCraft derives explanation token shapes from enc.coeffs_u.shape and uses generic reduction axes so non-image and detection-shaped latent tensors are handled without hard-coded rank assumptions. - _to_numpy conversion helpers normalize framework tensors before matplotlib rendering. - Invalid latent shapes raise ValueError with actionable messages; empty inputs guard against silent divide-by-zero. - Factorization dataclass field order is restored to the historical positional ordering with Optional[np.ndarray] typing so pickled Factorization objects and positional constructors keep working. - Concept-order arguments are validated up front instead of failing deep in the plotting path. - Torch and TF HolisticCraft variants and their shared factorizers are updated in lockstep. - Documentation stub adjusted to match the corrected surface. Notes: - API compatibility: Factorization positional ordering is preserved; previously reordered fields are restored. Callers that adopted the transiently reordered signature must switch back to the documented positional order or use keyword arguments.
- ResNet50 fixtures load with weights=None so first-run test suites do not require network access to download pretrained weights. - Deterministic mock models replace ad-hoc random weights in factorizer and HolisticCraft test suites, removing flaky assertions. - Torch object-detection fixtures are function-scoped so state does not leak between parameterized cases. - TF object-detection gradient test asserts that produced gradients are finite and nonzero, catching regressions where a broken path returns a zero-filled tensor. - Padded-detection regression test guards tf.config.run_functions_eagerly(False) and asserts tf.inside_function() so the graph-mode branch is genuinely exercised. - Add tests/concepts/test_holistic_craft_regressions.py covering the invariants introduced by the preceding slices.
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
…ft for object detection models
5bd1324 to
b59c49b
Compare
Agustin-Picard
left a comment
There was a problem hiding this comment.
Awesome work, looks good to me!
Bump2version is not compatible with pyproject.toml. Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
CRAFT for Object Detection
This PR brings support of CRAFT for Object Detection.
Object detection models often have very different architectures and output their results in various formats, which frequently do not allow gradients to flow through. To 'normalize' these outputs and make them compatible with Xplique, several formatters are introduced in this PR. These formatters can reorganize or reformat the outputs and be integrated into different parts of the model pipeline to expose gradients.
So, this PR is composed of several patches that can be organized into the following parts:
Box Formatters
Those formatters are used to:
MultiBoxTensor
This class gathers the results of the model for 1 image. It offers a convenient way to filter those results by class_id or accuracy.
Supported Box Formats
(x_min, y_min, x_max, y_max)- corner coordinates format(center_x, center_y, width, height)- center-based format(x_min, y_min, width, height)- top-left corner formatSupported Models and Formatters
Several formatters are delivered, they can be used for most common models.
PyTorch Models:
DetrBoxFormatter): Processes transformer outputs with CXCYWH normalized formatTorchvisionBoxFormatter): Works with XYXY normalized format from torchvision modelsYoloResultBoxFormatter): Handles modern YOLO v8+ models that return Results objects containing boxes, scores, and classes in XYXY formatYoloRawBoxFormatter): Handles CXCYWH normalized format with differentiable head modifications for gradient flowTensorFlow Models:
BoxesModelWrappers
Those wrappers are shortcuts to wrap a model with its dedicated formatter.
Depending on where the formatter is plugged, gradients can or can not be propagated.
Detr,Retinanet,Fcos,SSD,YOLORetinanetflowchart LR subgraph BoxesModelWrapper["BoxesModelWrapper"] direction LR Model --> BoxFormatter end Images --> BoxesModelWrapper -->|MultiBoxTensor list| PredictionsIt is also possible to set the BoxesModelWrapper as "tensor mode output", to make them compatible with Xplique attribution framework (in this case the process is done image by image).
Visualization helpers
Some methods have been added to display the outputs in a common way for all the models (

display_image_with_boxes)They can also display boxes with heatmaps:
CRAFT
To use CRAFT, we must split the model 2 in parts. Problem with the object detection models is they are often not 'splittable' easily because they are not composed of a succession of layers.
A new class called LatentData is introduced to handle the activations on 1 side, and the remaining data that are not activations but need to be transmitted to the next part of the model (we need this to reencode a concept to compare their relative importance / attributions).
The LatentExtractor decomposes object detection models into two functions:
g(input -> latent data) andh(latent data -> predictions). This splitting enables concept extraction from intermediate representations.The LatentData acts as a buffer between the 2 parts of the split model, allowing to store the full context of execution in the middle of the model, therefore each model has a different LatentData implementation:
Detr: LatentDataDetr stores positional encoding & mask information for the transformerYOLO: LatentDataYolo stores skip connection outputs from earlier layersRetinaNet: LatentDataRetinaNet stores image shape for bounding box coordinate conversionSeveral builders are available to produce such extractors for the most common models:
Detr,YOLO(needs a YOLO specially trained with ReLU in order to be able to fit the NMF),Retinaner,SSDRetinanetHolisticCraftObjectDetection applies CRAFT to object detection models by extracting concepts from the latent space (LatentData) produced by the LatentExtractor.
Visualization Methods:
display_images_per_concept(): Shows spatial heatmaps of concept activations on imagesdisplay_top_images_per_concept(): Displays images with highest activation for each conceptCRAFT Fit
The CRAFT fitting operation gathers the activations resulting of the 1st part of the model applied on the input dataset, thanks to the classes described above (LatentExtractor & LatenData).
flowchart LR User([User]) User -->|1 fit| UC1Input subgraph UC1["1 CRAFT fit"] direction LR UC1Input[Training Images] --> UC1Fit([fit]) UC1Fit --> UC1Bank[W] end style UC1 fill:#e6f3ffflowchart LR User([User]) User -->|2 Images to Concepts| UC2Input subgraph UC2["2 CRAFT transform"] direction LR UC2Input[New Images] --> UC2Transform([transform]) UC2Transform --> UC2Encode([encode]) UC2Encode --> UC2LatentExtractor([LatentExtractor.input_to_latent]) UC2LatentExtractor --> UC2LatentData[LatentData] UC2LatentData --> UC2TransformLatent([transform_latent]) UC2TransformLatent --> UC2EncodedData["EncodedData<br> \(LatentData + U\)"] UC2EncodedData --> UC2Coeffs[U] end style UC2 fill:#e6ffe6Importance evaluation: the decode path
flowchart LR User([User]) User -->|3 Concepts ranking| UC3Input subgraph UC3["3 CRAFT.estimate_importance"] direction LR UC3Explainer[Explainer: GradientInput, Sobol] --> compute_explanation_per_concept UC3Operator[operator] --> compute_explanation_per_concept UC3Input[Images] --> compute_explanation_per_concept compute_explanation_per_concept --> UC3LatentExtractor subgraph UC3LatentExtractor["LatentExtractor"] direction LR UC3Encode([encode<br>images]) --> EncodedData -->|LatentData, U| UC3Decode([decode]) -->|MultiBoxTensor| filtered_targets["filter targets<br>class_id, accuracy"] --> explain EncodedData -->|LatentData| UC3MakeDec(["make_concept_decoder<br>\(TorchWrapper\)"]) -->|concept_decoder| Exp([explainer.model = concept_decoder]) --> explain["explainer.explain(U, targets)"] explain --> explanation end end style UC3 fill:#fff4e6Feature Visualization: the encode differentiable path
To do this, use the encode() method with argument differentiable=True, it will use a specific path on torch/tf framework (using torch.linalg.lstsq() for example on torch side) instead of the regular NMF operation proficed by
skleanrnpackage.Input (requires_grad) -> g (encoder) -> latent_activations -> lstsq -> concept_coeffs
It can be used with an external toolbox such as
horamato maximize a concept activation. See the notebooks for CRAFT application forDetrandYOLO.flowchart LR User([User]) User -->|4 Feature Visualization| UC4Input subgraph UC4["4 Visualize Concepts"] direction LR UC4Input(["make objective"]) --> objective subgraph objective direction LR images --> encode(["encode"]) -->|EncodeData| U --> mean["mean on concept id"] end mean --> maco([maco]) end style UC4 fill:#ffe6f3Support of classifiers
Classifiers are also supported by this Holistic CRAFT.
Support of external NMF (Overcomplete)
External factorizers are also supported, through a protocol "ConceptFactorizer". In particular Overcomplete NMF, SemiNMF and ConvexNMF are compatible.
Misc
In the last patch, I added several helper scripts in a new
examplesfolder: we might in the end only choose 1 or 2 to keep.Finally, maybe I should move each model specific code (in particular each LatentData) outside of the code of Xplique, for example in dedicated notebook for each model.