Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f2c1f4d
feat(model): bind TREE_MODEL capability to fitted estimator (refs #246)
stanlrt Jun 8, 2026
e07bfde
feat(model): add TabularTreeBackend shared base (refs #246)
stanlrt Jun 8, 2026
586c594
feat(model): add XGBoost tree backend for .ubj models (refs #246)
stanlrt Jun 8, 2026
4e95f22
feat(transparency): gate TreeExplainer on TREE_MODEL capability (refs…
stanlrt Jun 8, 2026
c0050c2
feat: stamp ForwardOutput output kind for probability backends (refs …
stanlrt Jun 8, 2026
5d2adb3
feat: skip softmax for probability forward outputs (refs #246)
stanlrt Jun 8, 2026
5759be7
build(deps): add tree extra with xgboost (refs #246)
stanlrt Jun 8, 2026
b55cacd
test(transparency): TreeExplainer end-to-end on XGBoost backend (refs…
stanlrt Jun 8, 2026
bd3e90e
docs(model): document tree backend + TreeExplainer support (refs #246)
stanlrt Jun 8, 2026
8684a78
ci(infra): install tree extra so TreeExplainer tests run on CI (refs …
stanlrt Jun 8, 2026
2b48a85
fix: resolve pyright unused-import and config-type errors in tree wir…
stanlrt Jun 8, 2026
cd1041d
build(deps): rename tree extra to xgboost for per-library consistency…
stanlrt Jun 8, 2026
b78a451
ci: rename tree extra to xgboost and smoke-test it (refs #246)
stanlrt Jun 8, 2026
0501432
fix: map .ubj model source to xgboost extra in deps inference (refs #…
stanlrt Jun 8, 2026
daad9f7
refactor(model): import xgboost_backend module for side-effect regist…
stanlrt Jun 8, 2026
463c185
docs(model): group source config by key-combo, not file extension (re…
stanlrt Jun 9, 2026
77bc3bc
docs(model): name the linked page in source config comment (refs #246)
stanlrt Jun 9, 2026
7d04555
docs(model): document forced weights=DEFAULT for built-in models (ref…
stanlrt Jun 9, 2026
5de4daa
refactor(deps): derive uv extras from backend @register decorators (r…
stanlrt Jun 9, 2026
b17fcb7
docs(infra): add task-family module option to issue templates (refs #…
stanlrt Jun 9, 2026
8781052
build(deps): bundle scikit-learn + CPU torch in xgboost extra (refs #…
stanlrt Jun 9, 2026
3dae593
fix(transparency): coerce config ListConfig feature_names to list in …
stanlrt Jun 9, 2026
3d988e4
docs: add tree/XGBoost SHAP contributor smoke config (refs #246)
stanlrt Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ body:
- pipeline
- configs
- deps
- task-family
- unsure
validations:
required: true
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ body:
- pipeline
- configs
- deps
- task-family
- unsure
validations:
required: false
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/new.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ body:
- pipeline
- configs
- deps
- task-family
- new (specify below)
- unsure
validations:
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/perf-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ body:
- pipeline
- configs
- deps
- task-family
- unsure
validations:
required: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
uses: ./.github/actions/setup-raitap-env
with:
python-version: "3.13"
sync-command: uv sync --group dev --extra torch-cpu --extra mlflow --extra shap --extra captum --extra metrics --extra onnx-cpu --extra reporting --extra torchattacks --extra foolbox
sync-command: uv sync --group dev --extra torch-cpu --extra mlflow --extra shap --extra captum --extra metrics --extra onnx-cpu --extra reporting --extra torchattacks --extra foolbox --extra xgboost

- name: Ruff
run: uv run ruff check .
Expand All @@ -123,7 +123,7 @@ jobs:
uses: ./.github/actions/setup-raitap-env
with:
python-version: "3.11"
sync-command: uv sync --group dev --extra torch-cpu --extra mlflow --extra shap --extra captum --extra metrics --extra onnx-cpu --extra reporting --extra torchattacks --extra foolbox
sync-command: uv sync --group dev --extra torch-cpu --extra mlflow --extra shap --extra captum --extra metrics --extra onnx-cpu --extra reporting --extra torchattacks --extra foolbox --extra xgboost

- name: Pytest on 3.11 (floor compatibility, no coverage)
run: uv run pytest -m "not e2e and not cuda" -v
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/package-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
run: |
set -euo pipefail
WHEEL=$(ls dist/raitap-*.whl | head -n1)
for extra in torch-cpu onnx-cpu shap captum torchattacks foolbox mlflow metrics reporting launcher; do
for extra in torch-cpu onnx-cpu shap captum torchattacks foolbox mlflow metrics reporting launcher xgboost; do
echo "::group::extra=${extra}"
rm -rf .venv
uv venv --python ${{ matrix.python-version }}
Expand Down
34 changes: 34 additions & 0 deletions contributor-configs/tree-xgboost-shap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Tree / XGBoost + SHAP TreeExplainer smoke config

Minimal end-to-end check for the tree/tabular model backend (#246): loads a
fitted XGBoost model (`.ubj`), runs `shap.TreeExplainer` over a tabular feature
matrix, and renders the attributions with the tabular `ShapBar` visualiser.

## Run

Artifacts (`artifacts/model.ubj`, `artifacts/features.csv`) are gitignored
(`contributor-configs/*/artifacts/`), so generate them first:

```bash
uv run --extra xgboost python contributor-configs/tree-xgboost-shap/build_model.py
```

Then run the assessment:

```bash
uv run raitap --config-dir contributor-configs/tree-xgboost-shap --config-name assessment
```

`raitap-deps` infers the `xgboost` (+ `shap`, `html`) extras and installs them.
The `xgboost` extra bundles scikit-learn (the backend loads via the sklearn-API
`XGBClassifier`) and CPU torch (raitap's tensor pipeline needs it, even though
XGBoost does the compute). The HTML report lands under
`outputs/<date>/<time>/reports/`.

## Notes

- Transparency-only (no metrics/labels) to keep it a focused backend smoke.
- `call.target: 1` picks the positive class — binary XGBoost SHAP returns a
stacked `(B, F, 2)`, one slice per class.
- Tabular input has no per-sample image thumbnail, so the reporter prints a
benign "skipping sample thumbnail" warning per pinned sample (tracked in #136).
46 changes: 46 additions & 0 deletions contributor-configs/tree-xgboost-shap/assessment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Tree/XGBoost + SHAP TreeExplainer smoke config for RAITAP (#246).
#
# Exercises the tree backend end-to-end via the CLI: loads a fitted XGBoost
# model (.ubj), runs TreeExplainer over a tabular feature matrix, and renders
# the attributions with the tabular ShapBar visualiser.
#
# Artifacts (artifacts/model.ubj, artifacts/features.csv) are committed; run
# build_model.py to regenerate them. Transparency-only (no metrics/labels) to
# keep it a minimal backend smoke.
defaults:
- raitap_schema
- _self_

hardware: cpu
experiment_name: tree-xgboost-shap

model:
source: ${hydra:runtime.cwd}/contributor-configs/tree-xgboost-shap/artifacts/model.ubj

data:
name: synthetic-tabular
source: ${hydra:runtime.cwd}/contributor-configs/tree-xgboost-shap/artifacts/features.csv
forward_batch_size: 8

transparency:
treeshap:
_target_: ShapExplainer
algorithm: TreeExplainer
call:
# Binary XGBoost SHAP returns stacked (B, F, 2); pick the positive class.
target: 1
raitap:
input_metadata:
kind: tabular
layout: "(B,F)"
show_sample_names: true
visualisers:
- _target_: ShapBarVisualiser
constructor:
feature_names: [f0, f1, f2, f3, f4, f5]

reporting:
_target_: HTMLReporter
filename: tree_xgboost_shap_report
include_config: true
include_metadata: true
52 changes: 52 additions & 0 deletions contributor-configs/tree-xgboost-shap/build_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Generate the artifacts for the tree/XGBoost SHAP smoke config (#246).

Trains a tiny XGBoost classifier on deterministic synthetic data (mirrors the
acceptance test in ``test_e2e_integration.py``) and writes:

- ``artifacts/model.ubj`` — fitted XGBClassifier in XGBoost's native format
- ``artifacts/features.csv`` — the (N, 6) feature matrix the assessment explains

Run once before the assessment (artifacts are also committed, so this is only
needed to regenerate them):

uv run --extra xgboost python contributor-configs/tree-xgboost-shap/build_model.py
"""

from __future__ import annotations

from pathlib import Path

import numpy as np
import xgboost

FEATURE_COUNT = 6
HERE = Path(__file__).resolve().parent
ARTIFACTS = HERE / "artifacts"


def main() -> None:
ARTIFACTS.mkdir(exist_ok=True)
rng = np.random.default_rng(0)
features = rng.normal(size=(64, FEATURE_COUNT)).astype(np.float32)
# Label depends on the feature sum, so SHAP attributions are meaningful.
labels = (features.sum(axis=1) > 0).astype(int)

clf = xgboost.XGBClassifier(n_estimators=16, max_depth=3)
clf.fit(features, labels)
clf.save_model(ARTIFACTS / "model.ubj")

# The assessment explains a readable subset of rows.
header = ",".join(f"f{i}" for i in range(FEATURE_COUNT))
np.savetxt(
ARTIFACTS / "features.csv",
features[:16],
delimiter=",",
header=header,
comments="",
fmt="%.6f",
)
print(f"Wrote {ARTIFACTS / 'model.ubj'} and {ARTIFACTS / 'features.csv'}")


if __name__ == "__main__":
main()
66 changes: 61 additions & 5 deletions docs/contributor/adding/adding-a-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ A backend is a one-file plugin. We will use a fictional backend called `MyBacken

1. **Subclass `ModelBackend`** and decorate with `@backends.register(provides=..., extensions=...)`.
2. **Declare `provides` and `extensions`**: `provides` is the `frozenset[Capability]` your backend offers; `extensions` is the set of file suffixes it loads. The decorator type-checks both, sets them as class variables, and indexes the backend by extension so model loading resolves the right backend for a given file.
3. **Implement the abstract methods**: `from_path` (construct from a model file), `__call__` (run inference), and the `hardware_label` property.
4. **`predict_callable` is inherited**: it returns `self.__call__`, the universal forward-only shape that model-agnostic explainers consume. You do not implement it.
5. **`autograd_module` is opt-in**: implement it (return the live torch `nn.Module`) and declare `Capability.AUTOGRAD` ONLY if your backend exposes a differentiable torch module. Gradient explainers and attacks get this shape; model-agnostic ones get the predict callable.
3. **Declare `extra` (and `supported_hardware` if hardware-split)**: `extra` is the uv extra that installs your runtime library (e.g. `"torch"`, `"xgboost"`). `raitap-deps` reads it — import-free, via an AST scan of the decorator — to tell users which extra to install for your file format. Add `supported_hardware={ResolvedHardware.cpu, ...}` only if your library ships a distinct wheel per accelerator; the installable extra is then `f"{extra}-{hw.pyproject_extra_suffix}"` (e.g. `torch-cpu`). Omit it for single-wheel runtimes (the extra is the bare `extra` on all hardware). A file-backed backend without `extra` is invisible to deps inference and falls back to the torch default.
4. **Implement the abstract methods**: `from_path` (construct from a model file), `__call__` (run inference), and the `hardware_label` property.
5. **`predict_callable` is inherited**: it returns `self.__call__`, the universal forward-only shape that model-agnostic explainers consume. You do not implement it.
6. **`autograd_module` is opt-in**: implement it (return the live torch `nn.Module`) and declare `Capability.AUTOGRAD` ONLY if your backend exposes a differentiable torch module. Gradient explainers and attacks get this shape; model-agnostic ones get the predict callable.

```python
from pathlib import Path
Expand All @@ -29,10 +30,15 @@ from torch import nn

from raitap import backends
from raitap.models.backend import ModelBackend
from raitap.types import Capability
from raitap.types import Capability, ResolvedHardware


@backends.register(provides={Capability.AUTOGRAD}, extensions={".pth", ".pt"})
@backends.register(
provides={Capability.AUTOGRAD},
extensions={".pth", ".pt"},
extra="mybackend",
supported_hardware={ResolvedHardware.cpu, ResolvedHardware.cuda}, # ships cpu + cuda wheels
)
class MyBackend(ModelBackend):
def __init__(self, model: nn.Module) -> None:
self.model = model
Expand All @@ -56,6 +62,56 @@ class MyBackend(ModelBackend):

A non-torch or forward-only backend (e.g. ONNX) declares `provides=FORWARD_ONLY` (the empty capability set, imported from `raitap.types`), skips `autograd_module`, and runs model-agnostic explainers only.

## Tree / tabular backend

Tree-ensemble runtimes (XGBoost, LightGBM, scikit-learn) follow a separate base class: `TabularTreeBackend`. It owns the torch-to-numpy bridge, the `fitted_estimator()` accessor, and the `(N, C)` probability output shape. Subclass it instead of raw `ModelBackend`.

The concrete subclass implements two methods:

- `from_path`: defer the library import inside the method so the import error surfaces only when the backend is actually used, not at module load. Raise `ImportError` with a pip install hint if the library is absent.
- `_predict_proba`: call the fitted estimator and return an `(N, C)` numpy array of class probabilities.

Register with `provides={Capability.TREE_MODEL, Capability.PREDICT_PROBA}`, a file extension, and `extra="xgboost"`. No `supported_hardware`: XGBoost ships a single wheel (the bare `xgboost` extra on all hardware). The `fitted_estimator()` accessor satisfies the `EstimatorProvider` protocol, which `shap.TreeExplainer` consumes directly. The `predict_callable` method (inherited) returns a callable over the numpy-bridge probabilities, which enables model-agnostic SHAP explainers (e.g. `KernelExplainer`) on tree backends for free.

```python
from pathlib import Path
from typing import Any

import numpy as np

from raitap import backends
from raitap.models.tree_backend import TabularTreeBackend
from raitap.types import Capability


@backends.register(
provides={Capability.TREE_MODEL, Capability.PREDICT_PROBA},
extensions={".ubj"},
extra="xgboost", # single wheel -> bare extra, no supported_hardware
)
class XGBoostBackend(TabularTreeBackend):
# __init__(estimator) is inherited from TabularTreeBackend.

@classmethod
def from_path(
cls, path: Path, *, model_cfg: Any, hardware: str, allow_unsafe_pickle: bool = False
) -> "XGBoostBackend":
try:
import xgboost # deferred: only required with --extra xgboost
except ImportError as exc:
raise ImportError(
"XGBoost is not installed. Run: uv sync --extra xgboost"
) from exc
estimator = xgboost.XGBClassifier()
estimator.load_model(str(path))
return cls(estimator)

def _predict_proba(self, x: np.ndarray) -> np.ndarray: # (N, C)
return self._estimator.predict_proba(x)
```

`TabularTreeBackend` inherits `hardware_label` and the CPU/classification defaults, so you do not need to override them unless your runtime supports GPU placement.

## Which capabilities to declare

Most backends provide `{Capability.AUTOGRAD}` (torch) or `FORWARD_ONLY` (forward-only, e.g. ONNX). See {doc}`../capabilities` for the full list, what each means, and which algorithms require it.
Expand Down
2 changes: 1 addition & 1 deletion docs/contributor/adding/adding-an-algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ The `requires` field on `ExplainerAlgorithmSpec` / `AssessorAlgorithmSpec` decla
| Gradient-based (IntegratedGradients, PGD, FGSM, ...) | `{Capability.AUTOGRAD}` | Blocked on ONNX (forward-only) backends |
| Model-agnostic (SHAP KernelExplainer, Occlusion, FeatureAblation, ...) | `frozenset()` (default) | Runs on any backend, including ONNX |

Import: `from raitap.types import Capability`. `Capability.AUTOGRAD` is the only live value; `TREE_MODEL` and `PREDICT_PROBA` are roadmap placeholders with no current providers. See {doc}`../capabilities` for the full capability reference.
Import: `from raitap.types import Capability`. `Capability.AUTOGRAD` and `Capability.TREE_MODEL` are live gate values; `PREDICT_PROBA` is provided by tree backends but is read by the forward pass, not used as an algorithm gate. See {doc}`../capabilities` for the full capability reference.

When `requires - backend.provides` is non-empty, `BackendIncompatibilityError` is raised (`from raitap.utils.errors import BackendIncompatibilityError`; also re-exported from `raitap.robustness` and `raitap.transparency`).

Expand Down
6 changes: 3 additions & 3 deletions docs/contributor/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ The inherited `AdapterMixin.check_backend_compat` enforces this and raises `Back
| Capability | Value | Status | Meaning | Provided by | Required by |
|---|---|---|---|---|---|
| `AUTOGRAD` | `"autograd"` | live | Differentiable live model with input gradients. | `TorchBackend` | captum gradient methods (IntegratedGradients, Saliency, GradCAM), torchattacks, foolbox, auto-LiRPA (CROWN/IBP) |
| `TREE_MODEL` | `"tree_model"` | roadmap | Access to tree-ensemble structure (splits, leaf values) for TreeSHAP-style methods. | none yet (a future sklearn / XGBoost / LightGBM backend) | none yet (SHAP `TreeExplainer`, see #246) |
| `PREDICT_PROBA` | `"predict_proba"` | roadmap | Calibrated class-probability outputs. | none yet | none yet |
| `TREE_MODEL` | `"tree_model"` | live | Access to tree-ensemble structure (splits, leaf values) for TreeSHAP-style methods. | `XGBoostBackend` (and future sklearn / LightGBM backends) | SHAP `TreeExplainer` (rejected on torch/ONNX backends) |
| `PREDICT_PROBA` | `"predict_proba"` | live | Calibrated class-probability outputs. | `XGBoostBackend` | Forward pass: detects `OutputKind.PROBABILITIES` and skips softmax so confidences and metrics stay correct. Not a gate-requirer. |

`OnnxBackend` provides the empty set: it runs only model-agnostic algorithms (those with empty `requires`, e.g. SHAP `KernelExplainer`, captum `Occlusion` / `FeatureAblation`).

Expand All @@ -32,7 +32,7 @@ The default `requires=frozenset()` means "needs nothing special", so the algorit

## Adding a capability

Add a member to `Capability` only when a real algorithm needs it and a backend can provide it (ship both in the same change). A capability that nothing requires is dead and untestable. The two roadmap members above are seeded but inert until their backend + algorithm land.
Add a member to `Capability` only when a real algorithm needs it and a backend can provide it (ship both in the same change). A capability that nothing requires is dead and untestable.

## See also

Expand Down
25 changes: 15 additions & 10 deletions docs/modules/model/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,29 @@ myst:
`false` since the state-dict already supplies the weights.

:yaml:
# Option A:
# Full pickled nn.Module (deprecated, unsafe).
# Only use for checkpoints from a fully trusted source; executes arbitrary code embedded in the checkpoint.
# Consent required at invocation time — see `--allow-unsafe-pickle` in the flags reference.
# Option A: single model file.
# `source` is the only key. The format is inferred from the extension
# (.pt/.pth, .onnx, .ubj). Per-format details and caveats (state-dict vs
# TorchScript vs pickled, the unsafe-pickle consent, the `--extra xgboost`
# requirement for .ubj) are on the "Using your own model" page (linked
# from the `source` option above).
model:
source: "myModel.pth"
source: "path/to/model.<ext>"

# Option B:
# state_dict + arch (recommended):
# Option B: state-dict file.
# A state-dict carries no architecture, so add `arch` + `num_classes` to
# rebuild the model before loading the weights.
model:
source: "weights.pth"
arch: "resnet18"
num_classes: 2

# Option C:
# TorchScript archive (env-independent):
# Option C: built-in torchvision model — `source` is the model name, not a
# path. Loaded with torchvision's `weights="DEFAULT"` (latest pretrained
# weights, not configurable). For demos / quick testing; load your own
# weights via a file path (Options A/B).
model:
source: "scripted.pt"
source: "resnet50"

:cli: model.source=resnet50
```
3 changes: 3 additions & 0 deletions docs/modules/model/own-vs-built-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ RAITAP allows you to use your own model in any of the following supported format
torch.save(m.state_dict(), "weights.pth")
```
- `.onnx`
- `.ubj`: XGBoost native binary (fitted `XGBClassifier` saved via `estimator.save_model("model.ubj")`).
Requires the `xgboost` extra: `uv sync --extra xgboost`.
Use with `shap.TreeExplainer` (also needs `--extra shap`) or any model-agnostic SHAP explainer.

Set the `source` option to the path of your model file (see
{doc}`configuration`). For state-dict loading also set `arch` and
Expand Down
Loading
Loading