Skip to content

Craft object detection - #173

Merged
fredericboisnard merged 48 commits into
deel-ai:masterfrom
fredericboisnard:craft_object_detection
Jul 27, 2026
Merged

Craft object detection#173
fredericboisnard merged 48 commits into
deel-ai:masterfrom
fredericboisnard:craft_object_detection

Conversation

@fredericboisnard

@fredericboisnard fredericboisnard commented Nov 5, 2025

Copy link
Copy Markdown
Contributor

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:

  • BoxFormatters -> building blocks for representing the models results in a unified way
  • BoxModelWrappers -> wrap models with a formatter, allow diplaying results and use of Xplique attribution methods
  • CRAFT itself, with LatentData and LatentExtractor -> use all of the above + other concepts to bring support of CRAFT for object detection models

Box Formatters

Those formatters are used to:

  • standardize predictions: some models output results as list of predictions in their own data format, some others as a single dictionary containing the predictions ; the BoxFormattes now always return a list of type MultiBoxTensor
  • translate coordinate systems
  • preserve gradients (pytorch & tf implementations)

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.

flowchart LR
  Images --> Model -->BoxFormatter --> |MultiBoxTensor list| Predictions
Loading

Supported Box Formats

  • XYXY: (x_min, y_min, x_max, y_max) - corner coordinates format
  • CXCYWH: (center_x, center_y, width, height) - center-based format
  • XYWH: (x_min, y_min, width, height) - top-left corner format

Supported Models and Formatters

Several formatters are delivered, they can be used for most common models.

PyTorch Models:

  • DETR (DetrBoxFormatter): Processes transformer outputs with CXCYWH normalized format
  • FCOS, RetinaNet, SSD (TorchvisionBoxFormatter): Works with XYXY normalized format from torchvision models
  • YOLO (YoloResultBoxFormatter): Handles modern YOLO v8+ models that return Results objects containing boxes, scores, and classes in XYXY format
  • YOLO (YoloRawBoxFormatter): Handles CXCYWH normalized format with differentiable head modifications for gradient flow

TensorFlow Models:

  • RetinaNet: Dedicated TensorFlow support with corresponding formatters

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.

  • Pytorch: Detr, Retinanet, Fcos, SSD, YOLO
  • Tf: Retinanet
flowchart LR
  subgraph BoxesModelWrapper["BoxesModelWrapper"]
    direction LR
    Model --> BoxFormatter
  end
  Images --> BoxesModelWrapper -->|MultiBoxTensor list| Predictions
Loading

It 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:
image

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) and h (latent data -> predictions). This splitting enables concept extraction from intermediate representations.

flowchart LR
inputs --> g(["g \(input to latent\)"]) --> LatentData --> h(["h \(latent to logit\)"]) --> Formatter([Formatter]) --> MultiBoxTensor
Loading

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 transformer
  • YOLO: LatentDataYolo stores skip connection outputs from earlier layers
  • RetinaNet: LatentDataRetinaNet stores image shape for bounding box coordinate conversion
  • etc...

Several builders are available to produce such extractors for the most common models:

  • Pytorch: Detr, YOLO (needs a YOLO specially trained with ReLU in order to be able to fit the NMF), Retinaner, SSD
  • Tf: Retinanet

HolisticCraftObjectDetection 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 images
  • display_top_images_per_concept(): Displays images with highest activation for each concept

CRAFT 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:#e6f3ff
Loading
flowchart 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:#e6ffe6
Loading

Importance evaluation: the decode path

  • GradientInput and Sobol attributions are supported to evaluate the importance of the concepts (GradientInput needs the model to be differenciable).
  • More generally all attributions methods provided by Xplique should work.
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:#fff4e6
Loading

Feature 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 skleanrn package.

Input (requires_grad) -> g (encoder) -> latent_activations -> lstsq -> concept_coeffs

It can be used with an external toolbox such as horama to maximize a concept activation. See the notebooks for CRAFT application for Detr and YOLO.

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:#ffe6f3
Loading

Support 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 examples folder: 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.

Comment thread xplique/attributions/base.py Outdated
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch 2 times, most recently from f72b85a to 4d52e85 Compare February 24, 2026 16:09
Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch 6 times, most recently from c8e0808 to ef68750 Compare April 8, 2026 15:45
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch 5 times, most recently from 0e33a43 to d6df434 Compare April 10, 2026 12:21
Comment thread xplique/utils_functions/object_detection/torch/box_formatter.py Outdated
Comment thread xplique/utils_functions/object_detection/base/box_manager.py Outdated
Comment thread xplique/utils_functions/object_detection/base/box_manager.py Outdated
Comment thread xplique/utils_functions/object_detection/base/box_manager.py Outdated
Comment thread tests/concepts/test_holistic_craft_classification_torch.py Outdated
Comment thread tests/concepts/test_holistic_craft_classification_torch.py Outdated
Comment thread tests/concepts/test_holistic_craft_classification_torch.py Outdated
Comment thread tests/concepts/test_holistic_craft_object_detection_torch.py Outdated
Comment thread tests/concepts/test_holistic_craft_object_detection_torch.py
@Agustin-Picard

Copy link
Copy Markdown
Member

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:

  • The most important one is the encode method in the Torch and TF NMF Factorizers. These factorizers apply a non-linear transformation that can be implemented through specific solvers (like they do in scikit-learn and overcomplete). However, linalg.lstsq is being used, which solves a linear least-squares problem. I think we should consider whether we should try to find another library that already provides the NMF transform method, or implement it ourselves. In practice, we're computing a SemiNMF but with the non-negative constraint on the concept bank (which is the opposite of the SemiNMF in overcomplete, for example).

  • On the TorchBoxedModelWrapper, you override the __call__ method of a nn.Module. I'm worried this might affect torch hooks, since that is where they are implemented.

  • I think we should also add some tests on the new Factorizer classes, to make sure that they are working as intended (ie, NMF gives non-negative banks and coefficients, reconstruction loss is low, etc.).

  • Finally, for the holistic Craft tests, you download images from the internet. I'm concerned that network issues might cause unstable unit tests.

I've left plenty of comments and questions. I hope I didn't miss anything!

@Agustin-Picard Agustin-Picard added the concept New feature or issue concerning Concept based method label Apr 13, 2026
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch 2 times, most recently from 5f9f8e4 to 1735571 Compare April 17, 2026 16:12
@fredericboisnard

Copy link
Copy Markdown
Contributor Author

I pushed a new PR to take into account Agu's reviews, except the new FSITA solver (I am currently merging it)

@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch 2 times, most recently from 709d925 to b5f8a10 Compare April 24, 2026 12:17
fredericboisnard and others added 5 commits July 27, 2026 10:33
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.
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch from 69e697e to 5bd1324 Compare July 27, 2026 08:45
fredericboisnard and others added 17 commits July 27, 2026 10:53
…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>
@fredericboisnard
fredericboisnard force-pushed the craft_object_detection branch from 5bd1324 to b59c49b Compare July 27, 2026 08:53

@Agustin-Picard Agustin-Picard left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, looks good to me!

Bump2version is not compatible with pyproject.toml.

Signed-off-by: Frederic Boisnard <frederic.boisnard@irt-saintexupery.com>
@fredericboisnard
fredericboisnard merged commit 43cf783 into deel-ai:master Jul 27, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concept New feature or issue concerning Concept based method

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants