From 914646a9a6380e5c68527b32a5187b4574925f2b Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:40:06 +0200 Subject: [PATCH 001/144] build: add nlp dependencies --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8e69a987..56a76d65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,15 @@ report = [ "panel>=1.8.10", ] + +nlp = [ + "torch>=2.12.1", + "transformers>=5.12.1", + "datasets>5.0.0", + "sentence-transformers>=5.6.0", + "pacmap>=0.9.1" +] + xgboost = ["xgboost>=2.1.0"] catboost = ["catboost>=1.2.8"] lightgbm = ["lightgbm>=4.4.0"] From 770492774b177bdc287677f8414d698fd78a9176 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:43:56 +0200 Subject: [PATCH 002/144] feat: create nlp backend --- shapash/backend/nlp_backend.py | 231 +++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 shapash/backend/nlp_backend.py diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py new file mode 100644 index 00000000..fa3244b2 --- /dev/null +++ b/shapash/backend/nlp_backend.py @@ -0,0 +1,231 @@ +"""NLP backend infrastructure — shared base for text explainability backends. + +Two dataclasses form the typed pipeline between raw explainer output and the +final explanation object: + +* ``NlpRawExplanation`` — returned by every concrete ``run_explainer``. + Carries ``contributions``, ``base_values``, and ``data`` as typed fields + instead of an untyped dict. +* ``NlpContributions`` — returned by ``get_local_contributions`` to callers. + Adds word-importance aggregation and optional metadata (label names, index). + +``NlpBackend`` is the abstract base class that owns the common ``__init__`` +skeleton and the ``get_local_contributions`` implementation. Concrete +subclasses (``NlpShapBackend``, ``NlpLimeBackend``) only need to implement +``run_explainer``. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +import numpy as np +import pandas as pd + +from shapash.backend.base_backend import BaseBackend + + +@dataclass +class NlpRawExplanation: + """Typed intermediate output of ``NlpBackend.run_explainer``. + + Every concrete backend returns this instead of a plain dict so that the + three-field contract is enforced structurally rather than by convention. + ``get_local_contributions`` consumes it and produces ``NlpContributions``. + + Attributes + ---------- + contributions : list[np.ndarray] + Per-token contribution values, one array per sample. + Shape per sample: ``(n_tokens_i, n_classes)`` or ``(n_tokens_i,)``. + base_values : np.ndarray + Prediction baseline for each sample — SHAP base values or LIME + intercepts depending on the backend. Shape ``(n_samples, n_classes)`` + or ``(n_samples,)``. + data : list[list[str]] + Token or word strings per sample (variable length). + """ + + contributions: list[np.ndarray] + base_values: np.ndarray + data: list[list[str]] + + +@dataclass +class NlpContributions: + """Token-level contributions for a batch of text samples. + + Attributes + ---------- + token_strings : list[list[str]] + Tokenized representation of each sample (variable length per sample). + values : list[np.ndarray] + Per-token contribution values, one array per sample. + Each array has shape ``(n_tokens_i, n_classes)`` for multi-class models + or ``(n_tokens_i,)`` for binary/regression. + base_values : np.ndarray + Baseline prediction for each sample, shape ``(n_samples, n_classes)`` + or ``(n_samples,)``. + label_names : list[str] or None + Human-readable class names; set by the caller after construction. + index : pd.Index or None + Row index from the source ``pd.Series``; set by the caller after construction. + """ + + token_strings: list[list[str]] + values: list[np.ndarray] + base_values: np.ndarray + label_names: list[str] | None = field(default=None) + index: pd.Index | None = field(default=None) + + _SPECIAL_RE: re.Pattern = re.compile(r"^\[.*\]$|^##|^\s*$") + + def __len__(self) -> int: + return len(self.token_strings) + + def word_importance( + self, + label_idx: int, + n_top: int = 20, + filter_special: bool = True, + filter_sign: str = "all", + exclude_words: set[str] | None = None, + sample_indices: list[int] | None = None, + ) -> pd.Series: + """Aggregate token contributions by word across all samples for one class. + + For each unique (stripped) token string that appears in the batch, + computes the mean contribution across all its occurrences. + + Parameters + ---------- + label_idx : int + Index of the class to compute importance for. + n_top : int + Maximum number of words to return, sorted by ``|mean contribution|``. + filter_special : bool + If ``True``, skip empty strings, ``[CLS]``/``[SEP]``-style bracket + tokens, and ``##subword`` wordpiece prefixes. + filter_sign : {"all", "positive", "negative"} + If ``"positive"``, keep only words with positive mean contribution. + If ``"negative"``, keep only words with negative mean contribution. + exclude_words : set[str], optional + Additional set of word strings to exclude (e.g. user-selected + stopwords or corpus words to hide). + sample_indices : list[int], optional + Positional indices of the samples to include in the aggregation. + When ``None`` (default) all samples are used. + + Returns + ------- + pd.Series + Word → mean contribution, sorted by absolute value descending. + """ + _exclude: set[str] = exclude_words or set() + word_contribs: dict[str, list[float]] = {} + iter_data = ( + ((self.token_strings[i], self.values[i]) for i in sample_indices) + if sample_indices is not None + else zip(self.token_strings, self.values, strict=True) + ) + for tokens, vals in iter_data: + vals_1d: np.ndarray = vals[:, label_idx] if vals.ndim == 2 else vals + for tok, val in zip(tokens, vals_1d, strict=True): + tok_clean = tok.strip() + if filter_special and self._SPECIAL_RE.match(tok_clean): + continue + if tok_clean in _exclude: + continue + if tok_clean not in word_contribs: + word_contribs[tok_clean] = [] + word_contribs[tok_clean].append(float(val)) + + importance = pd.Series({w: float(np.mean(vs)) for w, vs in word_contribs.items()}) + importance = importance.reindex(importance.abs().sort_values(ascending=False).index) + + if filter_sign == "positive": + importance = importance[importance > 0] + elif filter_sign == "negative": + importance = importance[importance < 0] + + return importance.head(n_top) + + +class NlpBackend(BaseBackend): + """Abstract base class for NLP explainability backends. + + Owns the ``__init__`` skeleton shared by all text backends and the + ``get_local_contributions`` implementation. Concrete subclasses must + implement ``run_explainer`` and return the standard dict described in the + module docstring. + + Skips ``BaseBackend.__init__`` because ``check_model`` cannot handle text + pipelines or raw callables. + + Parameters + ---------- + model : callable + Text model or pipeline accepted by the concrete backend. + preprocessing : None + Unused; accepted for interface compatibility with ``BaseBackend``. + label_names : list[str] or None + Class names in the same order as the model output columns. + explainer_args : dict, optional + Keyword arguments forwarded to the underlying explainer constructor. + explainer_compute_args : dict, optional + Keyword arguments forwarded to the explainer call / ``explain_instance``. + """ + + column_aggregation = "sum" + support_groups = False + supported_cases = ["classification"] + + def __init__( + self, + model, + preprocessing=None, + label_names: list[str] | None = None, + explainer_args: dict | None = None, + explainer_compute_args: dict | None = None, + ) -> None: + # Do NOT call super().__init__() — check_model fails for text pipelines. + self.model = model + self.preprocessing = None + self.explain_data = None + self.state = None + self._case = "classification" + self._classes = label_names or [] + self.explainer_args = explainer_args or {} + self.explainer_compute_args = explainer_compute_args or {} + + def get_local_contributions( + self, x, explain_data: NlpRawExplanation, subset: list[int] | None = None + ) -> NlpContributions: + """Convert a ``NlpRawExplanation`` to ``NlpContributions``. + + Parameters + ---------- + x : list[str] or pd.Series + Text samples (not used here; kept for interface compatibility). + explain_data : NlpRawExplanation + Typed output of ``run_explainer``. + subset : list[int], optional + Positional indices to select a subset of samples. + + Returns + ------- + NlpContributions + Token strings, contribution values, and baseline predictions for + each sample. + """ + token_strings: list[list[str]] = [list(s) for s in explain_data.data] + values: list[np.ndarray] = list(explain_data.contributions) + base_values: np.ndarray = np.asarray(explain_data.base_values) + + if subset is not None: + token_strings = [token_strings[i] for i in subset] + values = [values[i] for i in subset] + base_values = base_values[subset] + + return NlpContributions(token_strings=token_strings, values=values, base_values=base_values) From 3feabf093a3d3ac4bef4c384250cc400f4249da1 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:44:26 +0200 Subject: [PATCH 003/144] feat: add nlp shap backend --- shapash/backend/nlp_shap_backend.py | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 shapash/backend/nlp_shap_backend.py diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py new file mode 100644 index 00000000..eece9459 --- /dev/null +++ b/shapash/backend/nlp_shap_backend.py @@ -0,0 +1,84 @@ +"""NLP SHAP backend — token-level SHAP contributions for text classification models. + +``NlpShapBackend`` wraps ``shap.Explainer`` for text inputs and implements +``run_explainer``. All shared infrastructure (``NlpContributions`` dataclass, +``get_local_contributions``, common ``__init__`` skeleton) lives in +``NlpBackend`` (see ``nlp_backend.py``). +""" + +from __future__ import annotations + +import shap + +from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation + + +class NlpShapBackend(NlpBackend): + """SHAP backend for text classification models (HuggingFace pipelines, etc.). + + Wraps ``shap.Explainer`` for text inputs and returns ``NlpContributions`` + via the shared ``get_local_contributions`` in ``NlpBackend``. + + Parameters + ---------- + model : callable + A text pipeline callable accepted by ``shap.Explainer`` (e.g. a + ``transformers.pipeline`` with ``return_all_scores=True``). + preprocessing : None + Unused; accepted for interface compatibility with ``BaseBackend``. + label_names : list[str] or None + Class names in the same order as the model output columns. + masker : any, optional + Forwarded to ``shap.Explainer`` when ``explainer_args`` is not given. + Typically ``None`` for text (SHAP auto-selects a ``TextMasker``). + explainer_args : dict, optional + Keyword arguments forwarded to ``shap.Explainer.__init__``. + Use ``{"explainer": SomeExplainerClass, ...}`` to inject a custom + explainer class (the ``"explainer"`` key selects the class; all other + keys are forwarded as its constructor arguments). + explainer_compute_args : dict, optional + Keyword arguments forwarded to the explainer call (``__call__``). + """ + + name = "nlp_shap" + + def __init__( + self, + model, + preprocessing=None, + label_names: list[str] | None = None, + masker=None, + explainer_args: dict | None = None, + explainer_compute_args: dict | None = None, + ) -> None: + super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) + self.masker = masker + + if "explainer" in self.explainer_args: + shap_params = {k: v for k, v in self.explainer_args.items() if k != "explainer"} + self.explainer = self.explainer_args["explainer"](**shap_params) + elif self.explainer_args: + self.explainer = shap.Explainer(**self.explainer_args) + else: + self.explainer = shap.Explainer(model) + + def run_explainer(self, x) -> NlpRawExplanation: + """Run the SHAP text explainer and return all explanation components. + + Parameters + ---------- + x : list[str] or pd.Series + Text samples to explain. + + Returns + ------- + NlpRawExplanation + Ragged list of value arrays, baseline predictions, and token + strings per sample. + """ + shap_explanation = self.explainer(x, **self.explainer_compute_args) + return NlpRawExplanation( + contributions=shap_explanation.values, + base_values=shap_explanation.base_values, + data=shap_explanation.data, + ) From d323ae052552023b9adbbdfa082f78210277206c Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:44:54 +0200 Subject: [PATCH 004/144] feat: add nlp lime backend --- shapash/backend/nlp_lime_backend.py | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 shapash/backend/nlp_lime_backend.py diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py new file mode 100644 index 00000000..5cd66d24 --- /dev/null +++ b/shapash/backend/nlp_lime_backend.py @@ -0,0 +1,192 @@ +"""NLP LIME backend — word-level LIME contributions for text classification models. + +``NlpLimeBackend`` wraps ``LimeTextExplainer`` and implements ``run_explainer``. +All shared infrastructure (``NlpContributions`` dataclass, +``get_local_contributions``, common ``__init__`` skeleton) lives in +``NlpBackend`` (see ``nlp_backend.py``). + +LIME works at word level (bag-of-words by default) rather than at the subword +or token level used by SHAP. Each sample's ``token_strings`` is therefore the +list of unique vocabulary words found by the ``split_expression`` tokeniser, not +HuggingFace subword tokens. Only the top ``num_features`` words per label +receive a non-zero weight; all others are zero in the dense matrix. +""" + +from __future__ import annotations + +import numpy as np + +try: + from lime.lime_text import LimeTextExplainer + + _lime_available = True +except ImportError: + _lime_available = False + +from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation + + +class NlpLimeBackend(NlpBackend): + """LIME backend for text classification models. + + Wraps ``LimeTextExplainer`` and returns ``NlpContributions`` via the shared + ``get_local_contributions`` in ``NlpBackend``. + + Parameters + ---------- + model : callable + A function ``f(texts: list[str]) -> np.ndarray`` of shape + ``(n_texts, n_classes)`` returning class probabilities. + HuggingFace pipelines require a thin wrapper — see the example below. + preprocessing : None + Unused; accepted for interface compatibility with ``BaseBackend``. + label_names : list[str] or None + Class names in the same order as the model output columns. + Forwarded to ``LimeTextExplainer`` as ``class_names``. + mask_string : str or None + Token used to replace masked words when ``bow=False``. Mirrors the + ``masker`` parameter of ``NlpShapBackend``. Defaults to + ``'UNKWORDZ'`` inside LIME. + explainer_args : dict, optional + Keyword arguments forwarded to ``LimeTextExplainer.__init__``. + Supported keys: ``kernel_width``, ``kernel``, ``verbose``, + ``feature_selection``, ``split_expression``, ``bow``, + ``random_state``, ``char_level``. + Use ``{"explainer": SubclassOfLimeTextExplainer, ...rest...}`` to + inject a custom explainer class (mirrors the SHAP escape hatch). + explainer_compute_args : dict, optional + Keyword arguments forwarded to ``LimeTextExplainer.explain_instance`` + on every call. Supported keys: ``num_features`` (default 10), + ``num_samples`` (default 5000), ``distance_metric`` (default + ``'cosine'``), ``model_regressor``, ``labels``, ``top_labels``. + If neither ``labels`` nor ``top_labels`` is provided, ``labels`` is + automatically set to ``range(len(label_names))``. + + Example + ------- + >>> import numpy as np + >>> from transformers import pipeline + >>> pipe = pipeline( + ... "text-classification", + ... model="distilbert-base-uncased-finetuned-sst-2-english", + ... return_all_scores=True, + ... ) + >>> def classifier_fn(texts): + ... return np.array([[s["score"] for s in row] for row in pipe(texts)]) + >>> backend = NlpLimeBackend( + ... classifier_fn, + ... label_names=["NEGATIVE", "POSITIVE"], + ... explainer_compute_args={"num_features": 15, "num_samples": 3000}, + ... ) + """ + + name = "nlp_lime" + + def __init__( + self, + model, + preprocessing=None, + label_names: list[str] | None = None, + mask_string: str | None = None, + explainer_args: dict | None = None, + explainer_compute_args: dict | None = None, + ) -> None: + if not _lime_available: + raise ImportError("lime is required for NlpLimeBackend — pip install lime") + + super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) + self.mask_string = mask_string + + if "explainer" in self.explainer_args: + lime_params = {k: v for k, v in self.explainer_args.items() if k != "explainer"} + self.explainer = self.explainer_args["explainer"](**lime_params) + else: + self.explainer = LimeTextExplainer( + class_names=label_names or None, + mask_string=self.mask_string, + **self.explainer_args, + ) + + def _classifier_fn(self, texts: list[str]) -> np.ndarray: + """Wrap self.model to guarantee a float numpy array of shape (n, n_classes). + + Handles three output formats: + + * ``np.ndarray`` — returned as-is. + * ``list[list[dict]]`` — HuggingFace pipeline with ``return_all_scores=True``. + Scores are extracted by label name using ``self._classes`` as the column + order, so ``label_names`` must be provided when the model uses this format. + * Anything else — coerced with ``np.array(..., dtype=float)``. + + LIME's internals index the result with ``[:, label_idx]``, so the array + must be 2-D and numeric. + """ + result = self.model(texts) + if isinstance(result, np.ndarray): + return result + # HuggingFace pipeline with return_all_scores=True → list[list[dict]] + if result and isinstance(result[0], list) and isinstance(result[0][0], dict): + label_to_idx = {name: i for i, name in enumerate(self._classes)} + matrix = np.zeros((len(result), len(self._classes)), dtype=np.float64) + for i, preds in enumerate(result): + for pred in preds: + idx = label_to_idx.get(pred["label"]) + if idx is not None: + matrix[i, idx] = pred["score"] + return matrix + return np.array(result, dtype=np.float64) + + def run_explainer(self, x) -> NlpRawExplanation: + """Run LimeTextExplainer on each sample and normalise output. + + Converts LIME's sparse per-label ``{label_id: [(word_id, weight)]}`` + representation into a dense ``(n_words, n_classes)`` array so that the + returned ``NlpRawExplanation`` matches the same field shapes as + ``NlpShapBackend``. + + Parameters + ---------- + x : list[str] or pd.Series + Text samples to explain. + + Returns + ------- + NlpRawExplanation + Dense weight arrays, LIME intercepts as base values, and unique + vocabulary words per sample. + """ + texts = list(x) + n_classes = len(self._classes) + + compute_args = dict(self.explainer_compute_args) + if "labels" not in compute_args and "top_labels" not in compute_args and n_classes: + compute_args["labels"] = list(range(n_classes)) + + contributions: list[np.ndarray] = [] + base_values_list: list[list[float]] = [] + data: list[list[str]] = [] + + for text in texts: + exp = self.explainer.explain_instance(text, self._classifier_fn, **compute_args) + + indexed_string = exp.domain_mapper.indexed_string + vocab: list[str] = list(indexed_string.inverse_vocab) + n_words = len(vocab) + effective_n_classes = n_classes or len(exp.local_exp) + + # Dense weight matrix — same shape contract as NlpShapBackend values. + weight_matrix = np.zeros((n_words, effective_n_classes), dtype=float) + for label_idx in range(effective_n_classes): + if label_idx in exp.local_exp: + for word_id, weight in exp.local_exp[label_idx]: + weight_matrix[word_id, label_idx] = weight + + contributions.append(weight_matrix) + base_values_list.append([exp.intercept.get(i, 0.0) for i in range(effective_n_classes)]) + data.append(vocab) + + return NlpRawExplanation( + contributions=contributions, + base_values=np.array(base_values_list), + data=data, + ) From 5812343ae03474a15585edf44d490943c5937146 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:49:29 +0200 Subject: [PATCH 005/144] docs: update docstring in shap backend --- shapash/backend/shap_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shapash/backend/shap_backend.py b/shapash/backend/shap_backend.py index a45ebc9e..58f6d639 100644 --- a/shapash/backend/shap_backend.py +++ b/shapash/backend/shap_backend.py @@ -56,8 +56,8 @@ def run_explainer(self, x: pd.DataFrame) -> dict: Returns ------- - explain_data : pd.DataFrame or list of pd.DataFrame - local contributions + explain_data : dict + Dictionary with ``contributions`` key containing the SHAP values array. """ print("INFO: Shap explainer type -", self.explainer) contributions = self.explainer(x, **self.explainer_compute_args) From fd87bd0cda020b72c1220bb51fb4bdaeebac40bf Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:52:59 +0200 Subject: [PATCH 006/144] feat: create nlp explainer object --- shapash/explainer/nlp_explainer.py | 225 +++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 shapash/explainer/nlp_explainer.py diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py new file mode 100644 index 00000000..9d9aef3e --- /dev/null +++ b/shapash/explainer/nlp_explainer.py @@ -0,0 +1,225 @@ +"""Prototype NLP explainer for text classification models. + +This module is a **bridge prototype** toward Phase 6 of the refactoring plan, +where token-level NLP explanations will be integrated into ``SmartExplainer`` +via ``TextDataset`` and ``ExplanationSession``. Once those are in place, users +will call ``SmartExplainer.compile(x=TextDataset(texts))`` and this class will +be removed. + +For now, ``NlpExplainer`` provides a minimal compile → plot → webapp workflow +for text without touching the tabular path in ``SmartExplainer.compile()``. +""" + +from __future__ import annotations + +import hashlib +import pickle +from pathlib import Path + +import pandas as pd +from plotly import graph_objs as go + +from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.plots.plot_token_highlight import plot_token_highlight +from shapash.webapp.nlp_app import NlpWebApp + + +def _hash_texts(text_list: list[str]) -> str: + h = hashlib.md5(usedforsecurity=False) + for t in text_list: + h.update(t.encode()) + return h.hexdigest() + + +def _cache_file(data_hash: str, cache_dir: Path) -> Path: + return cache_dir / f"{data_hash}.pkl" + + +class NlpExplainer: + """Minimal explainer for text classification — token-level contributions. + + Uses ``NlpShapBackend`` by default. Pass a pre-built backend instance to + switch to a different explainer (e.g. ``NlpLimeBackend``). + + Parameters + ---------- + model : callable + Text pipeline or callable accepted by the chosen backend. + label_names : list[str], optional + Class names in the same order as the model output columns. + Used in plot titles and the webapp class selector. + backend : NlpBackend, optional + Pre-built backend instance. When provided, ``explainer_args`` and + ``explainer_compute_args`` are ignored — configure the backend directly + before passing it in. Defaults to ``NlpShapBackend``. + explainer_args : dict, optional + Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. + explainer_compute_args : dict, optional + Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. + + Examples + -------- + >>> import transformers + >>> pipe = transformers.pipeline("text-classification", model="...", return_all_scores=True) + >>> xpl = NlpExplainer(pipe, label_names=["sadness", "joy", "love", "anger", "fear", "surprise"]) + >>> xpl.compile(texts[:100]) + >>> xpl.run_app(port=8050) + + >>> from shapash.backend.nlp_lime_backend import NlpLimeBackend + >>> lime_backend = NlpLimeBackend(classifier_fn, label_names=[...], explainer_compute_args={"num_features": 15}) + >>> xpl = NlpExplainer(classifier_fn, label_names=[...], backend=lime_backend) + """ + + def __init__( + self, + model, + label_names: list[str] | None = None, + backend: NlpBackend | None = None, + explainer_args: dict | None = None, + explainer_compute_args: dict | None = None, + ) -> None: + self.model = model + self.label_names = label_names + self.backend: NlpBackend = backend or NlpShapBackend( + model=model, + label_names=label_names, + explainer_args=explainer_args or {}, + explainer_compute_args=explainer_compute_args or {}, + ) + self.contributions: NlpContributions | None = None + self.texts: pd.Series | None = None + self.y_pred: pd.Series | None = None + self.y_true: pd.Series | None = None + self._data_hash: str | None = None + + def compile( + self, + x: list[str] | pd.Series, + y_true: list | pd.Series | None = None, + cache_dir: str | Path | None = None, + ) -> None: + """Compute token-level contributions and model predictions. + + Results are cached in memory keyed by a hash of the input texts, so + re-calling ``compile`` with the same data on the same instance skips the + expensive explainer run. Passing only ``y_true`` (same ``x``) is always + a lightweight metadata update. For persistence across kernel restarts, + pass a ``cache_dir`` path to enable an opt-in disk cache. + + Parameters + ---------- + x : list[str] or pd.Series + Text samples to explain. + y_true : list or pd.Series, optional + Ground-truth labels for each sample. When provided, the webapp will + show a "Ground Truth" column alongside "Prediction" in the dataset + table, making it easy to spot misclassifications. + cache_dir : str or Path, optional + If provided, contributions and predictions are also persisted to + ``/.pkl`` and reloaded on subsequent calls — even + after a kernel restart. Disabled by default. + """ + texts = x if isinstance(x, pd.Series) else pd.Series(x) + text_list = texts.tolist() + new_hash = _hash_texts(text_list) + + if new_hash != self._data_hash: + self.texts = texts + cache_path = _cache_file(new_hash, Path(cache_dir)) if cache_dir is not None else None + + if cache_path is not None and cache_path.exists(): + with cache_path.open("rb") as f: + cached = pickle.load(f) # noqa: S301 + self.contributions = cached["contributions"] + self.y_pred = cached["y_pred"] + else: + explain_data = self.backend.run_explainer(text_list) + self.contributions = self.backend.get_local_contributions(text_list, explain_data) + self.y_pred = self._predict_labels(text_list) + if cache_path is not None: + cache_path.parent.mkdir(parents=True, exist_ok=True) + with cache_path.open("wb") as f: + pickle.dump({"contributions": self.contributions, "y_pred": self.y_pred}, f) + + self.contributions.label_names = self.label_names + self.contributions.index = self.texts.index + self._data_hash = new_hash + + if y_true is not None: + self.y_true = ( + y_true + if isinstance(y_true, pd.Series) + else pd.Series(y_true, index=self.texts.index, name="ground_truth") + ) + + def text_plot(self, pos: int = 0, label_idx: int = 0, max_tokens: int | None = None) -> go.Figure: + """Plot token-level contributions for one sample and one class. + + Parameters + ---------- + pos : int + Positional index of the sample within the compiled batch. + label_idx : int + Index of the class to display (matches ``label_names`` order). + max_tokens : int, optional + If set, show only the top-``max_tokens`` tokens by absolute + contribution magnitude (in their original sentence order). + + Returns + ------- + go.Figure + """ + if self.contributions is None: + raise RuntimeError("Call compile() before text_plot().") + + tokens = self.contributions.token_strings[pos] + values = self.contributions.values[pos] + + if values.ndim == 2: + values = values[:, label_idx] + + label_name = ( + self.label_names[label_idx] if self.label_names is not None and label_idx < len(self.label_names) else None + ) + title = f"Token contributions — {label_name}" if label_name else "Token contributions" + + return plot_token_highlight(tokens=tokens, values=values, title=title, max_tokens=max_tokens) + + def run_app(self, port: int = 8050, debug: bool = False, scatter_xy=None) -> None: + """Launch the NLP explanation webapp. + + Parameters + ---------- + port : int + Port for the Dash development server. + debug : bool + Enable Dash debug mode (hot reload, error overlay). + scatter_xy : np.ndarray, optional + Pre-computed 2-D projection of the text samples, shape + ``(n_samples, 2)``. When provided, a scatter panel appears in the + webapp that can be used to lasso/box-select a subset of samples, + filtering both the dataset table and the global word importance + plot to that subset. Compute with any projection method + (PaCMAP, UMAP, PCA, t-SNE …) and pass the result here. + """ + if self.contributions is None: + raise RuntimeError("Call compile() before run_app().") + NlpWebApp(self, scatter_xy=scatter_xy).run(port=port, debug=debug) + + # ------------------------------------------------------------------ + # Private + # ------------------------------------------------------------------ + + def _predict_labels(self, text_list: list[str]) -> pd.Series: + """Run the pipeline once to obtain predicted class labels. + + Handles both ``return_all_scores=True`` (list of lists of dicts) and + single-prediction (list of dicts) pipeline output formats. + """ + raw = self.model(text_list) + if raw and isinstance(raw[0], list): + labels = [max(preds, key=lambda p: p["score"])["label"] for preds in raw] + else: + labels = [p["label"] for p in raw] + return pd.Series(labels, index=self.texts.index, name="prediction") From 14177ec28465f769b3f7a0702308455ff1d544cc Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:55:38 +0200 Subject: [PATCH 007/144] feat: add new plot functions for nlp web app --- shapash/plots/plot_sentence_highlight.py | 148 ++++++++++++++++++++ shapash/plots/plot_token_highlight.py | 90 ++++++++++++ shapash/plots/plot_waterfall.py | 166 +++++++++++++++++++++++ shapash/plots/plot_word_importance.py | 76 +++++++++++ 4 files changed, 480 insertions(+) create mode 100644 shapash/plots/plot_sentence_highlight.py create mode 100644 shapash/plots/plot_token_highlight.py create mode 100644 shapash/plots/plot_waterfall.py create mode 100644 shapash/plots/plot_word_importance.py diff --git a/shapash/plots/plot_sentence_highlight.py b/shapash/plots/plot_sentence_highlight.py new file mode 100644 index 00000000..077acea6 --- /dev/null +++ b/shapash/plots/plot_sentence_highlight.py @@ -0,0 +1,148 @@ +"""Inline sentence highlight for NLP token-level SHAP contributions. + +Renders a sentence as a sequence of coloured ``html.Span`` elements where each +token's background intensity is proportional to its SHAP contribution magnitude. +Positive contributions are blue (#1f77b4), negative are red (#d62728), matching +``plot_word_importance`` and ``plot_waterfall``. +""" + +from __future__ import annotations + +import re + +import numpy as np +from dash import html + +_SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") +_POS_RGB = (31, 119, 180) # #1f77b4 +_NEG_RGB = (214, 39, 40) # #d62728 + + +def _shap_color(val: float, max_abs: float) -> str: + """Interpolate from white toward the sign-appropriate colour.""" + if max_abs == 0 or val == 0: + return "transparent" + t = min(abs(val) / max_abs, 1.0) + r_base, g_base, b_base = _POS_RGB if val >= 0 else _NEG_RGB + r = int(255 + t * (r_base - 255)) + g = int(255 + t * (g_base - 255)) + b = int(255 + t * (b_base - 255)) + return f"rgb({r},{g},{b})" + + +def plot_sentence_highlight( + tokens: list[str], + values: np.ndarray, + base_value: float | None = None, + title: str = "Sentence Highlight", +) -> html.Div: + """Inline sentence with background-coloured spans proportional to SHAP contribution. + + Each token is rendered as an inline block whose background colour intensity + is proportional to its contribution magnitude. Special tokens + (``[CLS]``, ``[SEP]``, ``##subwords``, empty strings) are shown in muted + grey. Hover over any token to see its exact SHAP value. + + Parameters + ---------- + tokens : list[str] + Token strings in sentence order (same length as ``values``). + values : np.ndarray + 1-D array of per-token SHAP contributions. + base_value : float, optional + Model baseline for this sample. When provided, a summary line shows + ``base + Σ contributions = total``. + title : str + Unused (callers render their own ``html.H6`` header); kept for API + symmetry with the other plot functions. + + Returns + ------- + html.Div + A Dash component ready to be placed in a layout or returned from a + callback targeting ``"children"``. + """ + if values.ndim != 1: + raise ValueError(f"values must be 1-D, got shape {values.shape}") + + max_abs = float(np.abs(values).max()) if len(values) > 0 else 1.0 + if max_abs == 0: + max_abs = 1.0 + + _span_base = { + "padding": "3px 5px", + "borderRadius": "3px", + "margin": "2px 1px", + "display": "inline-block", + "cursor": "default", + "lineHeight": "2.2", + } + _span_special = { + **_span_base, + "backgroundColor": "#eeeeee", + "color": "#aaaaaa", + "fontSize": "0.8em", + } + + spans: list = [] + for tok, val in zip(tokens, values, strict=False): + is_special = bool(_SPECIAL_RE.match(tok.strip())) + tooltip = f"{tok}: {float(val):.4f}" + if is_special: + spans.append(html.Span(tok + " ", style=_span_special, title=tooltip)) + else: + spans.append( + html.Span( + tok + " ", + style={**_span_base, "backgroundColor": _shap_color(float(val), max_abs)}, + title=tooltip, + ) + ) + + total_shap = float(np.sum(values)) + if base_value is not None: + total = base_value + total_shap + summary = html.Div( + [ + html.Span(f"Base: {base_value:.3f}", style={"color": "#777", "marginRight": "14px"}), + html.Span(f"Σ contributions: {total_shap:+.3f}", style={"color": "#444", "marginRight": "14px"}), + html.Span(f"Total: {total:.3f}", style={"fontWeight": "bold", "color": "#111"}), + ], + style={"marginTop": "8px", "fontSize": "0.82em"}, + ) + else: + summary = html.Div( + html.Span(f"Σ contributions: {total_shap:+.3f}", style={"color": "#444"}), + style={"marginTop": "8px", "fontSize": "0.82em"}, + ) + + legend = html.Div( + [ + html.Span("■ ", style={"color": "#1f77b4"}), + html.Span("positive ", style={"fontSize": "0.78em", "color": "#555"}), + html.Span("■ ", style={"color": "#d62728"}), + html.Span("negative", style={"fontSize": "0.78em", "color": "#555"}), + ], + style={"marginBottom": "6px"}, + ) + + return html.Div( + [ + legend, + html.Div( + spans, + style={ + "lineHeight": "2.5", + "fontSize": "1.05em", + "padding": "12px", + "backgroundColor": "#fafafa", + "borderRadius": "4px", + "border": "1px solid #eeeeee", + "minHeight": "80px", + "overflowY": "auto", + "maxHeight": "300px", + }, + ), + summary, + ] + ) diff --git a/shapash/plots/plot_token_highlight.py b/shapash/plots/plot_token_highlight.py new file mode 100644 index 00000000..c22e960c --- /dev/null +++ b/shapash/plots/plot_token_highlight.py @@ -0,0 +1,90 @@ +"""Token-level contribution plot for NLP explanations.""" + +from __future__ import annotations + +import numpy as np +from plotly import graph_objs as go + +_COLOR_POSITIVE = "#1f77b4" +_COLOR_NEGATIVE = "#d62728" + + +def plot_token_highlight( + tokens: list[str], + values: np.ndarray, + title: str = "Token contributions", + max_tokens: int | None = None, + width: int = 900, + height: int | None = None, +) -> go.Figure: + """Horizontal bar chart of token-level SHAP contributions for one sample. + + Positive contributions are shown in blue, negative in red. + Tokens are displayed in sentence order (top to bottom). When ``max_tokens`` + is set, only the highest-magnitude tokens are kept but their relative order + in the sentence is preserved. + + Parameters + ---------- + tokens : list[str] + Token strings for one sample. + values : np.ndarray + 1-D contribution array of shape ``(n_tokens,)`` for a single class. + title : str + Figure title. + max_tokens : int, optional + If provided, keep only the ``max_tokens`` tokens with the largest + absolute contribution, in their original sentence order. + width : int + Figure width in pixels. + height : int, optional + Figure height in pixels. Defaults to ``max(400, 30 * n_tokens + 120)``. + + Returns + ------- + go.Figure + + Examples + -------- + >>> fig = plot_token_highlight(["I", "love", "this"], np.array([0.1, 0.5, 0.3])) + >>> fig.show() + """ + tokens = list(tokens) + values = np.asarray(values, dtype=float) + + if max_tokens is not None and max_tokens < len(tokens): + top_idx = np.argsort(np.abs(values))[::-1][:max_tokens] + keep = np.sort(top_idx) + tokens = [tokens[i] for i in keep] + values = values[keep] + + colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + + # Plotly renders y-axis bottom-to-top; reverse so sentence reads top-to-bottom. + fig = go.Figure( + go.Bar( + x=values[::-1], + y=tokens[::-1], + orientation="h", + marker_color=colors[::-1], + hovertemplate="%{y}: %{x:.4f}", + ) + ) + + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title="Contribution", + width=width, + height=height or max(400, 30 * len(tokens) + 120), + plot_bgcolor="white", + xaxis=dict( + zeroline=True, + zerolinecolor="#333333", + zerolinewidth=1, + gridcolor="#eeeeee", + ), + yaxis=dict(automargin=True), + margin=dict(l=20, r=20, t=60, b=40), + ) + + return fig diff --git a/shapash/plots/plot_waterfall.py b/shapash/plots/plot_waterfall.py new file mode 100644 index 00000000..521fc47e --- /dev/null +++ b/shapash/plots/plot_waterfall.py @@ -0,0 +1,166 @@ +"""Waterfall chart of per-token SHAP contributions for NLP explanations. + +Tokens whose absolute contribution falls below a configurable threshold are +collapsed into a single "other (N tokens)" bar so the chart stays readable even +for long sequences. Tokens are sorted by absolute contribution descending +(standard SHAP convention), giving the most important tokens top billing. +""" + +from __future__ import annotations + +import re +from collections import Counter + +import numpy as np +from plotly import graph_objs as go + +_SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") + + +def plot_waterfall( + tokens: list[str], + values: np.ndarray, + base_value: float | None = None, + min_pct: float = 0.10, + filter_special: bool = True, + title: str = "Token contributions", + width: int | None = None, +) -> go.Figure: + """Horizontal waterfall chart decomposing a single prediction into token SHAP contributions. + + Each token appears as a horizontal bar segment. Positive contributions + extend right (blue), negative extend left (red). Connectors show the + cumulative path from the baseline to the final prediction. + + Tokens with ``|value| < min_pct * max_abs`` are aggregated into a single + "other (N tokens)" bar to reduce visual clutter. + + Parameters + ---------- + tokens : list[str] + Token strings in sentence order. + values : np.ndarray + 1-D array of per-token SHAP contributions (same length as ``tokens``). + base_value : float, optional + Model baseline for this sample. Shown as the first "absolute" bar when + provided. + min_pct : float + Fraction of the maximum absolute contribution below which tokens are + grouped into the "other" bar. Range [0, 1]. Default 0.10 (10 %). + filter_special : bool + If ``True``, skip empty strings, ``[CLS]``/``[SEP]``-style tokens, and + ``##subword`` prefixes before computing the threshold. + title : str + Figure title. + width : int, optional + Figure width in pixels. Defaults to Plotly responsive behaviour. + + Returns + ------- + go.Figure + """ + if values.ndim != 1: + raise ValueError(f"values must be 1-D, got shape {values.shape}") + + # ── Filter special tokens ─────────────────────────────────────────── + pairs = [ + (tok, float(val)) + for tok, val in zip(tokens, values, strict=False) + if not (filter_special and _SPECIAL_RE.match(tok.strip())) + ] + + if not pairs: + return go.Figure() + + raw_toks = [p[0] for p in pairs] + all_vals_f = [p[1] for p in pairs] + + # Give each occurrence of a repeated token a unique label so Plotly + # renders them as separate bars instead of merging them on the same row. + tok_counts = Counter(raw_toks) + occurrence: dict[str, int] = {} + all_toks: list[str] = [] + for tok in raw_toks: + if tok_counts[tok] > 1: + occurrence[tok] = occurrence.get(tok, 0) + 1 + all_toks.append(f"{tok} [{occurrence[tok]}]") + else: + all_toks.append(tok) + + max_abs = max(abs(v) for v in all_vals_f) or 1.0 + threshold = float(min_pct) * max_abs + + significant = [(t, v) for t, v in zip(all_toks, all_vals_f, strict=False) if abs(v) >= threshold] + small = [(t, v) for t, v in zip(all_toks, all_vals_f, strict=False) if abs(v) < threshold] + + # Sort significant by |value| descending (SHAP convention) + significant.sort(key=lambda p: abs(p[1]), reverse=True) + + # ── Build waterfall arrays ────────────────────────────────────────── + y_labels: list[str] = [] + x_vals: list[float] = [] + measures: list[str] = [] + + if base_value is not None: + y_labels.append("Base") + x_vals.append(float(base_value)) + measures.append("absolute") + + for tok, val in significant: + y_labels.append(tok) + x_vals.append(val) + measures.append("relative") + + if small: + n_small = len(small) + sum_small = sum(v for _, v in small) + y_labels.append(f"other ({n_small} token{'s' if n_small != 1 else ''})") + x_vals.append(sum_small) + measures.append("relative") + + running_total = (float(base_value) if base_value is not None else 0.0) + sum(all_vals_f) + y_labels.append("Total") + x_vals.append(0) # value is ignored for measure="total"; Plotly computes it + measures.append("total") + + # Per-bar text labels + texts: list[str] = [] + for val, measure in zip(x_vals, measures, strict=False): + texts.append(f"{running_total:+.3f}" if measure == "total" else f"{val:+.3f}") + + n_bars = len(y_labels) + + fig = go.Figure( + go.Waterfall( + orientation="h", + y=y_labels, + x=x_vals, + measure=measures, + text=texts, + textposition="outside", + increasing=dict(marker=dict(color="#1f77b4")), + decreasing=dict(marker=dict(color="#d62728")), + totals=dict(marker=dict(color="#7f7f7f")), + connector=dict(line=dict(color="#dddddd", width=1, dash="dot")), + cliponaxis=False, + ) + ) + + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title="SHAP contribution", + height=max(300, 36 * n_bars + 100), + width=width, + plot_bgcolor="white", + xaxis=dict( + zeroline=True, + zerolinecolor="#888888", + zerolinewidth=1.5, + gridcolor="#eeeeee", + ), + yaxis=dict(automargin=True), + margin=dict(l=20, r=80, t=50, b=40), + showlegend=False, + ) + + return fig diff --git a/shapash/plots/plot_word_importance.py b/shapash/plots/plot_word_importance.py new file mode 100644 index 00000000..54b953e7 --- /dev/null +++ b/shapash/plots/plot_word_importance.py @@ -0,0 +1,76 @@ +"""Word-level importance plot for NLP explanations.""" + +from __future__ import annotations + +import pandas as pd +from plotly import graph_objs as go + +_COLOR_POSITIVE = "#1f77b4" +_COLOR_NEGATIVE = "#d62728" + + +def plot_word_importance( + word_imp: pd.Series, + title: str = "Word importance", + width: int = 900, + height: int | None = None, +) -> go.Figure: + """Horizontal bar chart of mean token-level SHAP contributions per word. + + Words are displayed sorted by absolute mean contribution (highest at top). + Positive mean contributions are shown in blue, negative in red. + + Parameters + ---------- + word_imp : pd.Series + Word → mean SHAP contribution, already sorted by ``|value|`` descending + (as returned by ``NlpContributions.word_importance``). + title : str + Figure title. + width : int + Figure width in pixels. + height : int, optional + Figure height in pixels. Defaults to ``max(400, 30 * n_words + 120)``. + + Returns + ------- + go.Figure + + Examples + -------- + >>> imp = contributions.word_importance(label_idx=1) + >>> fig = plot_word_importance(imp, title="Word importance — joy") + >>> fig.show() + """ + words = list(word_imp.index) + values = list(word_imp.values) + colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + + # Plotly renders y-axis bottom-to-top; reverse so highest-importance word appears at top. + fig = go.Figure( + go.Bar( + x=values[::-1], + y=words[::-1], + orientation="h", + marker_color=colors[::-1], + hovertemplate="%{y}: %{x:.4f}", + ) + ) + + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title="Mean SHAP contribution", + width=width, + height=height or max(400, 30 * len(words) + 120), + plot_bgcolor="white", + xaxis=dict( + zeroline=True, + zerolinecolor="#333333", + zerolinewidth=1, + gridcolor="#eeeeee", + ), + yaxis=dict(automargin=True), + margin=dict(l=20, r=20, t=60, b=40), + ) + + return fig From 320961c0b08e4f03239a63b6e08c7970e365d2b9 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:57:32 +0200 Subject: [PATCH 008/144] feat: create new dash webapp for nlp --- shapash/webapp/nlp_app.py | 642 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 642 insertions(+) create mode 100644 shapash/webapp/nlp_app.py diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py new file mode 100644 index 00000000..1cc0e110 --- /dev/null +++ b/shapash/webapp/nlp_app.py @@ -0,0 +1,642 @@ +"""Minimal Dash webapp for NLP text classification explanations. + +Prototype bridge toward Phase 5b (composable WebappComponents). The three +panels here (global word importance, dataset table, local token contributions) +map directly onto the three ``WebappComponent`` instances that Phase 5b would +register for a text modality. All tabular-only SmartApp panels (violin, cluster, +scatter prediction picking, confusion matrix) are absent rather than disabled. +""" + +from __future__ import annotations + +import re + +import dash +import dash_bootstrap_components as dbc +import numpy as np +import pandas as pd +import plotly.graph_objs as go +from dash import Input, Output, callback_context, dash_table, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_sentence_highlight import plot_sentence_highlight +from shapash.plots.plot_waterfall import plot_waterfall +from shapash.plots.plot_word_importance import plot_word_importance + +_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} +_SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") +_HIDDEN = {"display": "none"} +_VISIBLE = {"display": "block"} +_PALETTE = [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +] + + +class NlpWebApp: + """Minimal Dash webapp driven by an ``NlpExplainer``. + + Renders four panels: + - **Controls bar** — top-K slider, positive/negative sign filter, corpus + word multi-select for manual stopword exclusion. + - **Global word importance** — mean SHAP contribution per unique word for + the selected class (updates on any control change or scatter selection). + - **Dataset table** — text samples with predicted (and optional ground-truth) + labels; click a row to populate the local contribution panels. Filtered + to the scatter selection when one is active. + - **Local contributions** — inline sentence highlight as the primary view. + An optional waterfall chart (toggle with a radio button) groups tokens + below a configurable contribution threshold into a single "other" bar. + - **Sample scatter** *(optional)* — 2-D projection of the text embeddings, + coloured by prediction or ground-truth label. Draw a box or lasso to + filter the table and word importance to the selected subset. + + Parameters + ---------- + explainer : NlpExplainer + A compiled ``NlpExplainer`` instance (``compile()`` must have been called). + scatter_xy : np.ndarray, optional + Pre-computed 2-D projection, shape ``(n_samples, 2)``. When provided, + a scatter panel is added to the layout. Compute with PaCMAP, UMAP, + t-SNE, PCA, etc. and pass the result here — Shapash does not perform + the projection itself to avoid heavy optional dependencies. + """ + + def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: + if explainer.contributions is None: + raise RuntimeError("NlpExplainer.compile() must be called before launching the webapp.") + self.explainer = explainer + if scatter_xy is not None: + scatter_xy = np.asarray(scatter_xy) + n = len(explainer.texts) + if scatter_xy.shape != (n, 2): + raise ValueError(f"scatter_xy must have shape ({n}, 2), got {scatter_xy.shape}") + self._scatter_xy: np.ndarray | None = scatter_xy + self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + self.app.title = "Shapash — NLP Explainer" + self._build_layout() + self._register_callbacks() + + # ------------------------------------------------------------------ + # Layout + # ------------------------------------------------------------------ + + def _build_layout(self) -> None: + contrib = self.explainer.contributions + label_names = contrib.label_names or [str(i) for i in range(self._n_classes())] + n = len(self.explainer.texts) + + # ── Table records — always include _orig_idx for scatter filtering ── + records: dict[str, list] = { + "_orig_idx": list(range(n)), + "text": self.explainer.texts.tolist(), + "prediction": (self.explainer.y_pred.tolist() if self.explainer.y_pred is not None else [""] * n), + } + if getattr(self.explainer, "y_true", None) is not None: + records["ground_truth"] = self.explainer.y_true.tolist() + table_df = pd.DataFrame(records) + + # Full records cached for scatter-driven re-filtering in callbacks + self._full_table_records: list[dict] = table_df.to_dict("records") + + table_columns = [{"name": "Text", "id": "text"}, {"name": "Prediction", "id": "prediction"}] + if "ground_truth" in table_df.columns: + table_columns.append({"name": "Ground Truth", "id": "ground_truth"}) + + # ── Corpus word list for the exclusion multi-select ──────────── + all_words = sorted( + { + tok.strip() + for sample_tokens in contrib.token_strings + for tok in sample_tokens + if tok.strip() and not _SPECIAL_RE.match(tok.strip()) + } + ) + + # ── Scatter panel content (only when scatter_xy is provided) ── + scatter_col_content = None + if self._scatter_xy is not None: + color_options = [{"label": "Prediction", "value": "prediction"}] + if getattr(self.explainer, "y_true", None) is not None: + color_options.append({"label": "Ground Truth", "value": "ground_truth"}) + + scatter_col_content = html.Div( + [ + dbc.Row( + [ + dbc.Col( + html.H6("Sample Space", className="fw-bold mb-0"), + width="auto", + className="align-self-center", + ), + dbc.Col( + dcc.Dropdown( + id="color-by", + options=color_options, + value="prediction", + clearable=False, + style={"width": "160px", "fontSize": "0.9em"}, + ), + width="auto", + ), + dbc.Col( + dbc.Button( + "× clear", + id="scatter-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0", + style={"display": "none", "fontSize": "0.8em"}, + ), + width="auto", + className="align-self-center", + ), + ], + className="align-items-center mb-2", + ), + html.Small( + "Click a point or box/lasso to filter — click again or × clear to reset.", + className="text-muted d-block mb-2", + ), + dcc.Graph( + id="scatter-plot", + figure=self._build_scatter_fig("prediction"), + config={ + "displayModeBar": True, + "modeBarButtonsToRemove": ["autoScale2d", "resetScale2d"], + }, + style={"height": "340px"}, + ), + ], + style=_CARD_STYLE, + ) + + self.app.layout = dbc.Container( + [ + # ── Header ────────────────────────────────────────────────── + dbc.Row( + [ + dbc.Col(html.H3("Shapash — NLP Explainer", className="mb-0"), width=8), + dbc.Col( + [ + html.Label("Class", className="fw-bold small"), + dcc.Dropdown( + id="class-selector", + options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + value=0, + clearable=False, + ), + ], + width=4, + ), + ], + className="mb-2 mt-3 align-items-center", + ), + # ── Controls row ───────────────────────────────────────────── + dbc.Row( + [ + dbc.Col( + [ + html.Label("Top-K words", className="fw-bold small mb-1"), + dcc.Slider( + id="topk-slider", + min=1, + max=50, + step=1, + value=20, + marks={1: "1", 10: "10", 20: "20", 30: "30", 50: "50"}, + tooltip={"placement": "bottom", "always_visible": False}, + ), + ], + width=4, + ), + dbc.Col( + [ + html.Label("Contributions", className="fw-bold small mb-1"), + dcc.RadioItems( + id="sign-filter", + options=[ + {"label": " All", "value": "all"}, + {"label": " Positive", "value": "positive"}, + {"label": " Negative", "value": "negative"}, + ], + value="all", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "14px"}, + ), + ], + width=3, + ), + dbc.Col( + [ + html.Label("Exclude words", className="fw-bold small mb-1"), + dcc.Dropdown( + id="word-filter", + options=[{"label": w, "value": w} for w in all_words], + value=[], + multi=True, + placeholder="Select words to exclude…", + style={"fontSize": "0.9em"}, + ), + ], + width=5, + ), + ], + className="mb-3 p-2 bg-light rounded", + ), + # ── Main row ──────────────────────────────────────────────── + dbc.Row( + [ + dbc.Col( + html.Div( + [ + html.H6("Global Word Importance", className="fw-bold"), + dcc.Graph( + id="global-importance-graph", + config={"displayModeBar": False}, + style={"height": "420px"}, + ), + ], + style=_CARD_STYLE, + ), + width=5, + ), + dbc.Col( + html.Div( + [ + html.H6("Text Samples — click a row to inspect", className="fw-bold"), + dash_table.DataTable( + id="dataset-table", + data=self._full_table_records, + columns=table_columns, + row_selectable="single", + selected_rows=[0], + page_action="none", + style_table={"maxHeight": "400px", "overflowY": "auto"}, + style_cell={ + "textAlign": "left", + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": "0", + "padding": "6px 12px", + }, + style_cell_conditional=[ + {"if": {"column_id": "text"}, "width": "65%"}, + {"if": {"column_id": "prediction"}, "width": "17%"}, + {"if": {"column_id": "ground_truth"}, "width": "18%"}, + ], + style_header={"fontWeight": "bold", "backgroundColor": "#f8f9fa"}, + style_data_conditional=[ + { + "if": {"state": "selected"}, + "backgroundColor": "#e8f4f8", + "border": "1px solid #1f77b4", + }, + ], + tooltip_data=[ + {"text": {"value": r["text"], "type": "markdown"}} + for r in self._full_table_records + ], + tooltip_duration=None, + ), + ], + style=_CARD_STYLE, + ), + width=7, + ), + ], + className="mb-3", + ), + # ── Local contributions (+ optional scatter on the left) ────── + dbc.Row( + [ + *([dbc.Col(scatter_col_content, width=4)] if scatter_col_content is not None else []), + dbc.Col( + html.Div( + [ + html.H6( + id="sentence-highlight-title", + children="Token Contributions", + className="fw-bold", + ), + html.Div(id="sentence-highlight"), + html.Hr(style={"margin": "14px 0 10px"}), + dbc.Row( + [ + dbc.Col( + dcc.RadioItems( + id="show-waterfall", + options=[ + {"label": " Show waterfall", "value": "show"}, + {"label": " Hide", "value": "hide"}, + ], + value="hide", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "16px"}, + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + html.Div( + id="waterfall-threshold-wrapper", + children=[ + html.Label( + "Group tokens below (% of max contribution)", + className="small fw-bold mb-0 me-2", + ), + dcc.Slider( + id="waterfall-threshold", + min=0, + max=50, + step=1, + value=10, + marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, + tooltip={ + "placement": "bottom", + "always_visible": False, + }, + ), + ], + style=_HIDDEN, + ), + width=True, + ), + ], + className="align-items-center mb-2", + ), + html.Div( + id="waterfall-container", + children=[ + dcc.Graph( + id="waterfall-graph", + config={"displayModeBar": False}, + ), + ], + style=_HIDDEN, + ), + ], + style=_CARD_STYLE, + ), + width=8 if scatter_col_content is not None else 12, + ), + ], + className="mb-3", + ), + # ── Hidden store: scatter selection (always present) ────────── + dcc.Store(id="scatter-selected-indices", data=None), + ], + fluid=True, + ) + + # ------------------------------------------------------------------ + # Callbacks + # ------------------------------------------------------------------ + + def _register_callbacks(self) -> None: + contrib = self.explainer.contributions + full_records = self._full_table_records + + # ── Global word importance ─────────────────────────────────────── + @self.app.callback( + Output("global-importance-graph", "figure"), + [ + Input("class-selector", "value"), + Input("topk-slider", "value"), + Input("sign-filter", "value"), + Input("word-filter", "value"), + Input("scatter-selected-indices", "data"), + ], + ) + def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, selected_indices): + if label_idx is None: + raise PreventUpdate + word_imp = contrib.word_importance( + label_idx=int(label_idx), + n_top=int(topk or 20), + filter_sign=sign_filter or "all", + exclude_words=set(exclude_words_list or []) or None, + sample_indices=selected_indices, + ) + label_name = (contrib.label_names or [])[int(label_idx)] if contrib.label_names else str(label_idx) + suffix = f" ({len(selected_indices)} samples)" if selected_indices is not None else "" + return plot_word_importance( + word_imp, + title=f"Word importance — {label_name}{suffix}", + width=None, + height=390, + ) + + # ── Sentence highlight ─────────────────────────────────────────── + @self.app.callback( + [ + Output("sentence-highlight", "children"), + Output("sentence-highlight-title", "children"), + ], + [ + Input("dataset-table", "selected_rows"), + Input("dataset-table", "data"), + Input("class-selector", "value"), + ], + ) + def update_sentence_highlight(selected_rows, table_data, label_idx): + if not selected_rows or not table_data or label_idx is None: + raise PreventUpdate + pos = int(table_data[selected_rows[0]]["_orig_idx"]) + label_idx = int(label_idx) + tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) + sample_text = self.explainer.texts.iloc[pos] + preview = (sample_text[:60] + "…") if len(sample_text) > 60 else sample_text + highlight = plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) + title = f'Token Contributions — {label_name} | "{preview}"' + return highlight, title + + # ── Waterfall show/hide ────────────────────────────────────────── + @self.app.callback( + [ + Output("waterfall-container", "style"), + Output("waterfall-threshold-wrapper", "style"), + ], + Input("show-waterfall", "value"), + ) + def toggle_waterfall(show): + return (_VISIBLE, _VISIBLE) if show == "show" else (_HIDDEN, _HIDDEN) + + # ── Waterfall chart ────────────────────────────────────────────── + @self.app.callback( + Output("waterfall-graph", "figure"), + [ + Input("dataset-table", "selected_rows"), + Input("dataset-table", "data"), + Input("class-selector", "value"), + Input("waterfall-threshold", "value"), + ], + ) + def update_waterfall(selected_rows, table_data, label_idx, threshold_pct): + if not selected_rows or not table_data or label_idx is None: + raise PreventUpdate + pos = int(table_data[selected_rows[0]]["_orig_idx"]) + label_idx = int(label_idx) + tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) + min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 + return plot_waterfall( + tokens=tokens, + values=vals, + base_value=base_value, + min_pct=min_pct, + title=f"Token contributions — {label_name}", + ) + + # ── Scatter-specific callbacks (registered only when xy provided) ── + if self._scatter_xy is not None: + + @self.app.callback( + Output("scatter-plot", "figure"), + Input("color-by", "value"), + ) + def update_scatter_color(color_by): + return self._build_scatter_fig(color_by or "prediction") + + @self.app.callback( + Output("scatter-selected-indices", "data"), + Input("scatter-plot", "selectedData"), + Input("scatter-plot", "clickData"), + Input("scatter-clear-btn", "n_clicks"), + ) + def update_scatter_selection(selected_data, click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if "scatter-clear-btn" in trigger: + return None + if "clickData" in trigger: + if not click_data or not click_data.get("points"): + return None + return [int(click_data["points"][0]["customdata"][0])] + if not selected_data or not selected_data.get("points"): + return None + return [int(pt["customdata"][0]) for pt in selected_data["points"]] + + @self.app.callback( + Output("scatter-clear-btn", "style"), + Input("scatter-selected-indices", "data"), + ) + def toggle_clear_button(selected_indices): + visible = {"display": "inline", "fontSize": "0.8em"} + hidden = {"display": "none", "fontSize": "0.8em"} + return visible if selected_indices else hidden + + @self.app.callback( + [ + Output("dataset-table", "data"), + Output("dataset-table", "selected_rows"), + Output("dataset-table", "tooltip_data"), + ], + Input("scatter-selected-indices", "data"), + ) + def filter_table(selected_indices): + if selected_indices is None: + recs = full_records + else: + idx_set = set(selected_indices) + recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records + tooltips = [{"text": {"value": r["text"], "type": "markdown"}} for r in recs] + return recs, [0], tooltips + + # ------------------------------------------------------------------ + # Public + # ------------------------------------------------------------------ + + def run(self, port: int = 8050, debug: bool = False) -> None: + """Launch the Dash development server.""" + self.app.run(port=port, debug=debug) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _n_classes(self) -> int: + sample = self.explainer.contributions.values[0] + return sample.shape[1] if sample.ndim == 2 else 1 + + def _sample_data(self, pos: int, label_idx: int): + """Return tokens, 1-D values, base_value, and label_name for one sample.""" + contrib = self.explainer.contributions + tokens = contrib.token_strings[pos] + vals = contrib.values[pos] + if vals.ndim == 2: + vals = vals[:, label_idx] + + bv = contrib.base_values + base_value: float | None = None + if bv is not None and bv.ndim == 2 and pos < bv.shape[0] and label_idx < bv.shape[1]: + base_value = float(bv[pos, label_idx]) + elif bv is not None and bv.ndim == 1 and pos < bv.shape[0]: + base_value = float(bv[pos]) + + label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) + return tokens, vals, base_value, label_name + + def _build_scatter_fig(self, color_by: str) -> go.Figure: + """2-D scatter coloured by prediction or ground-truth label. + + One trace per class so the legend works correctly and Plotly's + box/lasso select dims unselected points across all traces. + ``customdata`` stores the original sample index so ``selectedData`` + callbacks can recover it regardless of which trace a point is in. + """ + n = len(self.explainer.texts) + contrib = self.explainer.contributions + + if color_by == "ground_truth" and getattr(self.explainer, "y_true", None) is not None: + labels = [str(label) for label in self.explainer.y_true.tolist()] + elif self.explainer.y_pred is not None: + labels = [str(label) for label in self.explainer.y_pred.tolist()] + else: + labels = [""] * n + + label_names = contrib.label_names or sorted(set(labels)) + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self.explainer.texts] + xy = self._scatter_xy + + fig = go.Figure() + for i, name in enumerate(label_names): + mask = [j for j, lbl in enumerate(labels) if lbl == name] + if not mask: + continue + mask_arr = np.array(mask) + fig.add_trace( + go.Scattergl( + x=xy[mask_arr, 0].tolist(), + y=xy[mask_arr, 1].tolist(), + mode="markers", + marker=dict(color=_PALETTE[i % len(_PALETTE)], size=7, opacity=0.75), + customdata=mask_arr.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in mask], + hovertemplate=f"{name}
%{{text}}", + name=name, + ) + ) + + fig.update_layout( + dragmode="select", + uirevision="scatter", + xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + plot_bgcolor="#f9f9f9", + paper_bgcolor="white", + margin=dict(l=10, r=10, t=10, b=10), + height=360, + legend=dict(itemsizing="constant", orientation="v", title_text=""), + showlegend=True, + ) + return fig From ce1912b154a965359b11163e3dd13006d96567c6 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 15:58:01 +0200 Subject: [PATCH 009/144] chore: add new objects in init --- shapash/backend/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shapash/backend/__init__.py b/shapash/backend/__init__.py index 0627fb74..9faa455d 100644 --- a/shapash/backend/__init__.py +++ b/shapash/backend/__init__.py @@ -3,6 +3,9 @@ from .base_backend import BaseBackend from .lime_backend import LimeBackend +from .nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation +from .nlp_lime_backend import NlpLimeBackend +from .nlp_shap_backend import NlpShapBackend from .shap_backend import ShapBackend From a252d90322de19771744caf28a937f9ebaeba04d Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 16:02:28 +0200 Subject: [PATCH 010/144] test: add test for the nlp part --- tests/integration_tests/test_nlp_prototype.py | 739 ++++++++++++++++++ 1 file changed, 739 insertions(+) create mode 100644 tests/integration_tests/test_nlp_prototype.py diff --git a/tests/integration_tests/test_nlp_prototype.py b/tests/integration_tests/test_nlp_prototype.py new file mode 100644 index 00000000..4fd956da --- /dev/null +++ b/tests/integration_tests/test_nlp_prototype.py @@ -0,0 +1,739 @@ +"""Integration tests for the NLP explanation prototype. + +Covers the full wiring between NlpContributions (backend), word_importance +aggregation, plot_token_highlight / plot_word_importance (plots), NlpExplainer +(explainer), and NlpWebApp (webapp layout construction). A real NLP model is +not required — synthetic NlpContributions data is used throughout so the suite +runs in CI without transformers/datasets. +""" + +import unittest +from unittest.mock import patch + +import numpy as np +import pandas as pd +import plotly.graph_objs as go +from dash import html + +from shapash.backend.nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation +from shapash.backend.nlp_lime_backend import NlpLimeBackend +from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.plots.plot_sentence_highlight import plot_sentence_highlight +from shapash.plots.plot_token_highlight import plot_token_highlight +from shapash.plots.plot_waterfall import plot_waterfall +from shapash.plots.plot_word_importance import plot_word_importance +from shapash.webapp.nlp_app import NlpWebApp + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + + +def _make_contributions() -> NlpContributions: + """Synthetic NlpContributions for 3 samples, 6 classes.""" + rng = np.random.default_rng(42) + token_strings = [ + ["", "i", "feel", "so", "happy", "today", ""], + ["", "this", "is", "terrible", "and", "sad", ""], + ["", "what", "a", "wonderful", "day", ""], + ] + values = [ + rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) + for t in token_strings + ] + base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) + return NlpContributions( + token_strings=token_strings, + values=values, + base_values=base_values, + label_names=LABEL_NAMES, + index=pd.RangeIndex(3), + ) + + +def _make_explainer(compiled: bool = True) -> NlpExplainer: + """NlpExplainer with synthetic data, bypassing __init__ to avoid shap.Explainer(None). + + ``compiled=True`` simulates a post-compile() instance with contributions set. + ``compiled=False`` simulates a pre-compile() instance (contributions=None). + """ + xpl = object.__new__(NlpExplainer) + xpl.model = None + xpl.label_names = LABEL_NAMES + xpl.backend = None + xpl.contributions = None + xpl.texts = None + xpl.y_pred = None + xpl.y_true = None + if compiled: + xpl.texts = pd.Series( + ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], + index=pd.RangeIndex(3), + ) + xpl.contributions = _make_contributions() + xpl.y_pred = pd.Series(["joy", "sadness", "joy"], index=pd.RangeIndex(3), name="prediction") + return xpl + + +# --------------------------------------------------------------------------- +# NlpContributions +# --------------------------------------------------------------------------- + + +class TestNlpContributions(unittest.TestCase): + def setUp(self): + self.contrib = _make_contributions() + + def test_len(self): + self.assertEqual(len(self.contrib), 3) + + def test_word_importance_returns_series(self): + imp = self.contrib.word_importance(label_idx=1) + self.assertIsInstance(imp, pd.Series) + + def test_word_importance_filters_special_tokens(self): + imp = self.contrib.word_importance(label_idx=0, filter_special=True) + self.assertNotIn("", imp.index) + self.assertNotIn(" ", imp.index) + + def test_word_importance_keeps_special_when_disabled(self): + imp = self.contrib.word_importance(label_idx=0, filter_special=False) + # Empty strings (BOS/EOS) should now be present + self.assertIn("", imp.index) + + def test_word_importance_respects_n_top(self): + imp = self.contrib.word_importance(label_idx=1, n_top=3) + self.assertLessEqual(len(imp), 3) + + def test_word_importance_sorted_by_absolute_value(self): + imp = self.contrib.word_importance(label_idx=2, n_top=20) + abs_vals = imp.abs().tolist() + self.assertEqual(abs_vals, sorted(abs_vals, reverse=True)) + + def test_word_importance_aggregates_repeated_words(self): + # "feel" only appears once; check it has a single contribution value + imp = self.contrib.word_importance(label_idx=1, n_top=20, filter_special=True) + self.assertIn("feel", imp.index) + # Should be a scalar (mean of one occurrence) + self.assertIsInstance(imp["feel"], float) + + def test_word_importance_all_labels(self): + for idx in range(N_CLASSES): + imp = self.contrib.word_importance(label_idx=idx) + self.assertIsInstance(imp, pd.Series) + self.assertGreater(len(imp), 0) + + def test_word_importance_filter_sign_positive(self): + imp = self.contrib.word_importance(label_idx=0, filter_sign="positive") + if len(imp) > 0: + self.assertTrue((imp > 0).all(), "positive filter should return only positive values") + + def test_word_importance_filter_sign_negative(self): + imp = self.contrib.word_importance(label_idx=0, filter_sign="negative") + if len(imp) > 0: + self.assertTrue((imp < 0).all(), "negative filter should return only negative values") + + def test_word_importance_exclude_words(self): + imp_full = self.contrib.word_importance(label_idx=1, filter_special=True) + if len(imp_full) == 0: + return + word_to_exclude = imp_full.index[0] + imp_filtered = self.contrib.word_importance( + label_idx=1, filter_special=True, exclude_words={word_to_exclude} + ) + self.assertNotIn(word_to_exclude, imp_filtered.index) + + def test_word_importance_exclude_words_empty_set(self): + imp_no_exclude = self.contrib.word_importance(label_idx=1, exclude_words=set()) + imp_none_exclude = self.contrib.word_importance(label_idx=1, exclude_words=None) + pd.testing.assert_series_equal(imp_no_exclude, imp_none_exclude) + + def test_word_importance_sample_indices(self): + imp = self.contrib.word_importance(label_idx=0, sample_indices=[0], n_top=50) + self.assertGreater(len(imp), 0) + # "terrible" only exists in sample 1 — must not appear in the sample-0 subset + self.assertNotIn("terrible", imp.index) + + +# --------------------------------------------------------------------------- +# plot_sentence_highlight +# --------------------------------------------------------------------------- + + +class TestPlotSentenceHighlight(unittest.TestCase): + def setUp(self): + self.tokens = ["[CLS]", "i", "feel", "happy", "[SEP]"] + self.values = np.array([0.01, 0.05, 0.30, -0.20, 0.01]) + + def test_returns_html_div(self): + result = plot_sentence_highlight(self.tokens, self.values) + self.assertIsInstance(result, html.Div) + + def test_has_children(self): + result = plot_sentence_highlight(self.tokens, self.values) + self.assertIsNotNone(result.children) + # legend + spans div + summary + self.assertGreaterEqual(len(result.children), 3) + + def test_raises_on_2d_values(self): + with self.assertRaises(ValueError): + plot_sentence_highlight(self.tokens, np.zeros((5, 3))) + + def test_with_base_value_does_not_raise(self): + result = plot_sentence_highlight(self.tokens, self.values, base_value=0.15) + self.assertIsInstance(result, html.Div) + + def test_empty_tokens_does_not_raise(self): + result = plot_sentence_highlight([], np.array([])) + self.assertIsInstance(result, html.Div) + + +# --------------------------------------------------------------------------- +# plot_waterfall +# --------------------------------------------------------------------------- + + +class TestPlotWaterfall(unittest.TestCase): + def setUp(self): + self.tokens = ["[CLS]", "i", "feel", "so", "happy", "today", "[SEP]"] + self.values = np.array([0.01, 0.08, 0.35, 0.02, -0.20, 0.05, 0.01]) + + def test_returns_figure(self): + fig = plot_waterfall(self.tokens, self.values) + self.assertIsInstance(fig, go.Figure) + + def test_has_waterfall_trace(self): + fig = plot_waterfall(self.tokens, self.values) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Waterfall) + + def test_filters_special_tokens_by_default(self): + fig = plot_waterfall(self.tokens, self.values) + y_labels = list(fig.data[0].y) + for label in y_labels: + self.assertNotIn("[CLS]", label) + self.assertNotIn("[SEP]", label) + + def test_keeps_special_tokens_when_disabled(self): + # min_pct=0 disables grouping so every token gets its own bar + fig = plot_waterfall(self.tokens, self.values, filter_special=False, min_pct=0.0) + y_labels = list(fig.data[0].y) + self.assertTrue(any("[CLS]" in lbl for lbl in y_labels)) + + def test_total_bar_present(self): + fig = plot_waterfall(self.tokens, self.values) + measures = list(fig.data[0].measure) + self.assertIn("total", measures) + + def test_grouping_reduces_bar_count(self): + # With min_pct=0.0 (no grouping), every non-special token is its own bar + fig_no_group = plot_waterfall(self.tokens, self.values, min_pct=0.0) + # With min_pct=0.5, small tokens are lumped + fig_grouped = plot_waterfall(self.tokens, self.values, min_pct=0.5) + self.assertLessEqual(len(fig_grouped.data[0].y), len(fig_no_group.data[0].y)) + + def test_other_bar_present_when_grouping_active(self): + fig = plot_waterfall(self.tokens, self.values, min_pct=0.5) + y_labels = list(fig.data[0].y) + self.assertTrue(any("other" in lbl for lbl in y_labels)) + + def test_no_other_bar_when_min_pct_zero(self): + fig = plot_waterfall(self.tokens, self.values, min_pct=0.0) + y_labels = list(fig.data[0].y) + self.assertFalse(any("other" in lbl for lbl in y_labels)) + + def test_base_value_creates_absolute_bar(self): + fig = plot_waterfall(self.tokens, self.values, base_value=0.20) + measures = list(fig.data[0].measure) + self.assertEqual(measures[0], "absolute") + y_labels = list(fig.data[0].y) + self.assertEqual(y_labels[0], "Base") + + def test_empty_tokens_returns_figure(self): + fig = plot_waterfall([], np.array([])) + self.assertIsInstance(fig, go.Figure) + + def test_all_special_tokens_returns_figure(self): + fig = plot_waterfall(["[CLS]", "[SEP]"], np.array([0.01, -0.01])) + self.assertIsInstance(fig, go.Figure) + + def test_custom_title(self): + fig = plot_waterfall(self.tokens, self.values, title="joy waterfall") + self.assertIn("joy waterfall", fig.layout.title.text) + + +# --------------------------------------------------------------------------- +# plot_token_highlight +# --------------------------------------------------------------------------- + + +class TestPlotTokenHighlight(unittest.TestCase): + def setUp(self): + self.tokens = ["i", "feel", "happy", "today"] + self.values = np.array([0.1, 0.4, -0.2, 0.05]) + + def test_returns_figure(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertIsInstance(fig, go.Figure) + + def test_has_one_bar_trace(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Bar) + + def test_max_tokens_limits_bars(self): + fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) + self.assertLessEqual(len(fig.data[0].x), 2) + + def test_max_tokens_preserves_sentence_order(self): + # top-2 by |value| are "feel" (0.4) and "happy" (-0.2) + # sentence order: "feel" before "happy" + fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) + # y-axis is reversed for display (highest importance at top → reversed list) + displayed_labels = list(fig.data[0].y) + feel_pos = displayed_labels.index("feel") + happy_pos = displayed_labels.index("happy") + # In reversed display: "feel" appears after "happy" (lower index = further down) + self.assertNotEqual(feel_pos, happy_pos) + + def test_custom_title(self): + fig = plot_token_highlight(self.tokens, self.values, title="Test title") + self.assertIn("Test title", fig.layout.title.text) + + def test_orientation_horizontal(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertEqual(fig.data[0].orientation, "h") + + +# --------------------------------------------------------------------------- +# plot_word_importance +# --------------------------------------------------------------------------- + + +class TestPlotWordImportance(unittest.TestCase): + def setUp(self): + self.word_imp = pd.Series( + {"happy": 0.35, "terrible": -0.28, "wonderful": 0.20, "feel": -0.10}, + ) + + def test_returns_figure(self): + fig = plot_word_importance(self.word_imp) + self.assertIsInstance(fig, go.Figure) + + def test_has_one_bar_trace(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Bar) + + def test_all_words_rendered(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(len(fig.data[0].x), len(self.word_imp)) + + def test_custom_title(self): + fig = plot_word_importance(self.word_imp, title="Joy importance") + self.assertIn("Joy importance", fig.layout.title.text) + + def test_orientation_horizontal(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(fig.data[0].orientation, "h") + + +# --------------------------------------------------------------------------- +# NlpExplainer (no real model) +# --------------------------------------------------------------------------- + + +class TestNlpExplainer(unittest.TestCase): + def setUp(self): + self.xpl = _make_explainer() + + def test_text_plot_returns_figure(self): + fig = self.xpl.text_plot(pos=0, label_idx=1) + self.assertIsInstance(fig, go.Figure) + + def test_text_plot_all_samples(self): + for pos in range(3): + fig = self.xpl.text_plot(pos=pos, label_idx=0) + self.assertIsInstance(fig, go.Figure) + + def test_text_plot_all_labels(self): + for label_idx in range(N_CLASSES): + fig = self.xpl.text_plot(pos=0, label_idx=label_idx) + self.assertIsInstance(fig, go.Figure) + + def test_text_plot_max_tokens(self): + fig = self.xpl.text_plot(pos=0, label_idx=1, max_tokens=3) + self.assertIsInstance(fig, go.Figure) + self.assertLessEqual(len(fig.data[0].x), 3) + + def test_text_plot_raises_before_compile(self): + xpl = _make_explainer(compiled=False) + with self.assertRaises(RuntimeError): + xpl.text_plot(pos=0) + + def test_run_app_raises_before_compile(self): + xpl = _make_explainer(compiled=False) + with self.assertRaises(RuntimeError): + xpl.run_app() + + def test_y_pred_stored(self): + self.assertIsNotNone(self.xpl.y_pred) + self.assertEqual(len(self.xpl.y_pred), 3) + + def test_label_names_propagated(self): + self.assertEqual(self.xpl.contributions.label_names, LABEL_NAMES) + + def test_y_true_is_none_by_default(self): + xpl = _make_explainer(compiled=True) + self.assertIsNone(xpl.y_true) + + +# --------------------------------------------------------------------------- +# NlpWebApp layout (no server launch) +# --------------------------------------------------------------------------- + + +class TestNlpWebApp(unittest.TestCase): + def setUp(self): + self.xpl = _make_explainer() + self.webapp = NlpWebApp(self.xpl) + + def test_layout_built(self): + self.assertIsNotNone(self.webapp.app.layout) + + def test_class_selector_options(self): + # Walk layout to find the Dropdown with id "class-selector" + dropdown = self._find_component(self.webapp.app.layout, "class-selector") + self.assertIsNotNone(dropdown, "class-selector dropdown not found in layout") + self.assertEqual(len(dropdown.options), N_CLASSES) + labels = [opt["label"] for opt in dropdown.options] + self.assertEqual(labels, LABEL_NAMES) + + def test_dataset_table_populated(self): + table = self._find_component(self.webapp.app.layout, "dataset-table") + self.assertIsNotNone(table, "dataset-table not found in layout") + self.assertEqual(len(table.data), 3) + self.assertIn("text", table.data[0]) + self.assertIn("prediction", table.data[0]) + + def test_raises_without_compile(self): + xpl = _make_explainer(compiled=False) + with self.assertRaises(RuntimeError): + NlpWebApp(xpl) + + def test_graph_ids_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("global-importance-graph", ids) + self.assertIn("dataset-table", ids) + self.assertIn("class-selector", ids) + # token bar chart removed; sentence-highlight replaced it + self.assertNotIn("local-contributions-graph", ids) + + def test_control_ids_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("topk-slider", ids) + self.assertIn("sign-filter", ids) + self.assertIn("word-filter", ids) + + def test_sentence_highlight_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("sentence-highlight", ids) + + def test_waterfall_controls_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("show-waterfall", ids) + self.assertIn("waterfall-threshold", ids) + self.assertIn("waterfall-graph", ids) + self.assertIn("waterfall-container", ids) + + def test_dataset_table_no_ground_truth_by_default(self): + table = self._find_component(self.webapp.app.layout, "dataset-table") + col_ids = [c["id"] for c in table.columns] + self.assertNotIn("ground_truth", col_ids) + + def test_dataset_table_with_y_true(self): + xpl = _make_explainer(compiled=True) + xpl.y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") + webapp = NlpWebApp(xpl) + table = self._find_component(webapp.app.layout, "dataset-table") + col_ids = [c["id"] for c in table.columns] + self.assertIn("ground_truth", col_ids) + self.assertEqual(table.data[0]["ground_truth"], "sadness") + + def test_scatter_store_always_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("scatter-selected-indices", ids) + + def test_scatter_absent_when_no_xy(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertNotIn("scatter-plot", ids) + self.assertNotIn("color-by", ids) + + def test_scatter_present_when_xy_given(self): + xpl = _make_explainer(compiled=True) + xy = np.zeros((3, 2)) + webapp = NlpWebApp(xpl, scatter_xy=xy) + ids = self._collect_ids(webapp.app.layout) + self.assertIn("scatter-plot", ids) + self.assertIn("color-by", ids) + + def test_scatter_wrong_shape_raises(self): + xpl = _make_explainer(compiled=True) + with self.assertRaises(ValueError): + NlpWebApp(xpl, scatter_xy=np.zeros((5, 2))) # 5 rows but only 3 samples + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _find_component(self, node, component_id): + """Depth-first search for a Dash component by id.""" + if hasattr(node, "id") and node.id == component_id: + return node + children = getattr(node, "children", None) + if children is None: + return None + if not isinstance(children, list): + children = [children] + for child in children: + result = self._find_component(child, component_id) + if result is not None: + return result + return None + + def _collect_ids(self, node) -> set: + """Collect all component ids in the layout tree.""" + ids = set() + if hasattr(node, "id") and isinstance(node.id, str): + ids.add(node.id) + children = getattr(node, "children", None) + if children is None: + return ids + if not isinstance(children, list): + children = [children] + for child in children: + ids |= self._collect_ids(child) + return ids + + +# --------------------------------------------------------------------------- +# NlpLimeBackend +# --------------------------------------------------------------------------- + +# Keep num_samples tiny so LIME tests run fast in CI. +_LIME_COMPUTE_ARGS = {"num_samples": 50, "num_features": 5} +_SAMPLE_TEXTS = ["i feel so happy today", "this is terrible and sad"] + + +def _fake_classifier(texts: list[str]) -> np.ndarray: + """Deterministic fake classifier returning (n_texts, N_CLASSES) probabilities.""" + rng = np.random.default_rng(0) + probs = rng.random((len(texts), N_CLASSES)).astype(np.float32) + probs /= probs.sum(axis=1, keepdims=True) + return probs + + +def _make_lime_backend() -> NlpLimeBackend: + return NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + + +class TestNlpLimeBackend(unittest.TestCase): + + def setUp(self): + self.backend = _make_lime_backend() + + # --- init / config --- + + def test_name(self): + self.assertEqual(self.backend.name, "nlp_lime") + + def test_inherits_nlp_backend(self): + self.assertIsInstance(self.backend, NlpBackend) + + def test_label_names_stored(self): + self.assertEqual(self.backend._classes, LABEL_NAMES) + + def test_mask_string_stored(self): + backend = NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + mask_string="[MASK]", + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + self.assertEqual(backend.mask_string, "[MASK]") + + def test_explainer_args_forwarded(self): + backend = NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + explainer_args={"bow": False}, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + self.assertFalse(backend.explainer.bow) + + # --- _classifier_fn --- + + def test_classifier_fn_converts_list_to_array(self): + def list_model(texts): + return [[0.5] * N_CLASSES for _ in texts] + + backend = NlpLimeBackend(list_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["hello"]) + self.assertIsInstance(result, np.ndarray) + self.assertEqual(result.shape, (1, N_CLASSES)) + + def test_classifier_fn_passes_through_array(self): + arr = np.ones((2, N_CLASSES), dtype=np.float32) + + def array_model(texts): + return arr + + backend = NlpLimeBackend(array_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["a", "b"]) + self.assertIs(result, arr) + + def test_classifier_fn_converts_hf_pipeline_format(self): + # HuggingFace pipeline with return_all_scores=True returns list[list[dict]]. + def hf_model(texts): + return [ + [{"label": name, "score": 1.0 / N_CLASSES} for name in LABEL_NAMES] + for _ in texts + ] + + backend = NlpLimeBackend(hf_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["hello", "world"]) + self.assertIsInstance(result, np.ndarray) + self.assertEqual(result.shape, (2, N_CLASSES)) + self.assertEqual(result.dtype, np.float64) + # Each score should be 1/N_CLASSES + np.testing.assert_allclose(result, 1.0 / N_CLASSES, atol=1e-6) + + # --- run_explainer --- + + def test_run_explainer_returns_raw_explanation(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertIsInstance(raw, NlpRawExplanation) + + def test_run_explainer_contributions_count(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(len(raw.contributions), len(_SAMPLE_TEXTS)) + + def test_run_explainer_contributions_shape(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + for arr in raw.contributions: + self.assertEqual(arr.ndim, 2) + self.assertEqual(arr.shape[1], N_CLASSES) + + def test_run_explainer_base_values_shape(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(raw.base_values.shape, (len(_SAMPLE_TEXTS), N_CLASSES)) + + def test_run_explainer_data_is_word_list(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(len(raw.data), len(_SAMPLE_TEXTS)) + for word_list in raw.data: + self.assertIsInstance(word_list, list) + self.assertTrue(all(isinstance(w, str) for w in word_list)) + + def test_run_explainer_sparse_weights(self): + # LIME fills at most num_features non-zero weights per label column. + raw = self.backend.run_explainer(_SAMPLE_TEXTS[:1]) + matrix = raw.contributions[0] + for col in range(N_CLASSES): + self.assertLessEqual( + np.count_nonzero(matrix[:, col]), + _LIME_COMPUTE_ARGS["num_features"], + ) + + # --- get_local_contributions --- + + def test_get_local_contributions_returns_nlp_contributions(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + self.assertIsInstance(contrib, NlpContributions) + + def test_get_local_contributions_token_strings_match_data(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + self.assertEqual(contrib.token_strings, raw.data) + + def test_get_local_contributions_values_match_contributions(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + for got, expected in zip(contrib.values, raw.contributions): + np.testing.assert_array_equal(got, expected) + + def test_get_local_contributions_subset(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw, subset=[0]) + self.assertEqual(len(contrib.token_strings), 1) + self.assertEqual(len(contrib.values), 1) + self.assertEqual(contrib.base_values.shape[0], 1) + + +# --------------------------------------------------------------------------- +# NlpExplainer with NlpLimeBackend +# --------------------------------------------------------------------------- + + +class TestNlpExplainerWithLimeBackend(unittest.TestCase): + + def _make_explainer_lime(self, compiled: bool = True) -> NlpExplainer: + """NlpExplainer backed by NlpLimeBackend, with synthetic contributions.""" + xpl = _make_explainer(compiled=compiled) + xpl.backend = _make_lime_backend() + return xpl + + def test_backend_is_lime_instance(self): + xpl = self._make_explainer_lime() + self.assertIsInstance(xpl.backend, NlpLimeBackend) + + def test_text_plot_works_with_lime_backend(self): + xpl = self._make_explainer_lime(compiled=True) + fig = xpl.text_plot(pos=0, label_idx=0) + self.assertIsInstance(fig, go.Figure) + + def test_text_plot_raises_before_compile(self): + xpl = self._make_explainer_lime(compiled=False) + with self.assertRaises(RuntimeError): + xpl.text_plot(pos=0) + + def test_compile_sets_contributions(self): + backend = _make_lime_backend() + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) + fake_preds = pd.Series( + ["joy"] * len(_SAMPLE_TEXTS), + index=pd.RangeIndex(len(_SAMPLE_TEXTS)), + name="prediction", + ) + with patch.object(xpl, "_predict_labels", return_value=fake_preds): + xpl.compile(_SAMPLE_TEXTS) + self.assertIsInstance(xpl.contributions, NlpContributions) + self.assertEqual(len(xpl.contributions), len(_SAMPLE_TEXTS)) + self.assertEqual(xpl.contributions.label_names, LABEL_NAMES) + + def test_compile_caching_skips_rerun(self): + backend = _make_lime_backend() + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) + fake_preds = pd.Series( + ["joy"] * len(_SAMPLE_TEXTS), + index=pd.RangeIndex(len(_SAMPLE_TEXTS)), + name="prediction", + ) + with patch.object(xpl, "_predict_labels", return_value=fake_preds): + xpl.compile(_SAMPLE_TEXTS) + hash_after_first = xpl._data_hash + xpl.compile(_SAMPLE_TEXTS) # same data — must hit in-memory cache + self.assertEqual(xpl._data_hash, hash_after_first) + + +if __name__ == "__main__": + unittest.main() From 42da3063cb64e812c5baf850f3863c32649b9684 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 16:03:41 +0200 Subject: [PATCH 011/144] chore: add script to launch and test nlp webapp --- tests/integration_tests/test_webapp_nlp.py | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/integration_tests/test_webapp_nlp.py diff --git a/tests/integration_tests/test_webapp_nlp.py b/tests/integration_tests/test_webapp_nlp.py new file mode 100644 index 00000000..b69631a6 --- /dev/null +++ b/tests/integration_tests/test_webapp_nlp.py @@ -0,0 +1,94 @@ +import datasets +import pandas as pd +import transformers + +import torch +import numpy as np + +from shapash.explainer.nlp_explainer import NlpExplainer +import pacmap + + +def project_embeddings(embeddings, n_components=2): + + projector = pacmap.PaCMAP( + n_components=n_components, + n_neighbors=5, + MN_ratio=0.5, + FP_ratio=2.0, + # random_state=1 + ) + + return projector.fit_transform( + embeddings, init="pca" + ) + + +def get_embeddings(texts, classifier, tokenizer, batch_size=32): + + device = classifier.device + + + all_embeddings = [] + for i in range(0, len(texts), batch_size): + batch = tokenizer(texts[i:i+batch_size], padding=True, truncation=True, return_tensors="pt") + batch = {k: v.to(device) for k, v in batch.items()} # fix 1: move to GPU + with torch.no_grad(): + out = classifier(**batch, output_hidden_states=True) # fix 2: request hidden states + mask = batch["attention_mask"].unsqueeze(-1) + hidden = out.hidden_states[-1] # last layer, shape [batch, seq_len, hidden_dim] + emb = (hidden * mask).sum(1) / mask.sum(1) + all_embeddings.append(emb.cpu().numpy()) + return np.vstack(all_embeddings) + + +if __name__ == "__main__": + + N = 100 + + # load the emotion dataset + dataset = datasets.load_dataset("dair-ai/emotion", split="train") + data = pd.DataFrame({"text": dataset["text"], "emotion": dataset["label"]}) + + sentences = dataset["text"][:N] + labels = dataset["label"][:N] + + # load the model and tokenizer + tokenizer = transformers.AutoTokenizer.from_pretrained( + "bhadresh-savani/distilbert-base-uncased-emotion", + #"nateraw/bert-base-uncased-emotion", + use_fast=True) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained( + "bhadresh-savani/distilbert-base-uncased-emotion", + #"nateraw/bert-base-uncased-emotion" + ).cuda() + + # build a pipeline object to do predictions + pred = transformers.pipeline( + "text-classification", + model=classifier, + tokenizer=tokenizer, + batch_size=4, + top_k=None, + ) + + embeddings = get_embeddings(sentences, classifier, tokenizer) + projected = project_embeddings(embeddings, n_components=2) + + label_names = ["sadness", "joy", "love", "anger", "fear", "surprise"] + + # from shapash.backend.nlp_lime_backend import NlpLimeBackend + # lime_backend = NlpLimeBackend( + # pred, + # label_names=label_names, + # explainer_compute_args={ + # "num_samples": 300, # default is 5000 — this is the main lever + # "num_features": 10, # words explained per sample per label + # } + # ) + # xpl = NlpExplainer(pred, label_names=label_names, backend=lime_backend) + + xpl = NlpExplainer(pred, label_names=label_names) + + xpl.compile(sentences, y_true=[label_names[i] for i in labels]) + xpl.run_app(port=8050, debug=False, scatter_xy=projected) From 3bc694d2107e04897a9b4b6ddcc7fa6efc9c2b9c Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Thu, 25 Jun 2026 16:15:13 +0200 Subject: [PATCH 012/144] fix: fix version for datasets --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56a76d65..13133bf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ report = [ nlp = [ "torch>=2.12.1", "transformers>=5.12.1", - "datasets>5.0.0", + "datasets>=5.0.0", "sentence-transformers>=5.6.0", "pacmap>=0.9.1" ] From a057f68e6e14b3c6caa84e3bec808cad1ae47c11 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 10:18:56 +0200 Subject: [PATCH 013/144] feat: add class probability to text samples table --- shapash/explainer/nlp_explainer.py | 31 +++++++++++++++++++++++++----- shapash/webapp/nlp_app.py | 31 +++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 9d9aef3e..dd8b5044 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -90,6 +90,7 @@ def __init__( self.contributions: NlpContributions | None = None self.texts: pd.Series | None = None self.y_pred: pd.Series | None = None + self.y_prob: pd.DataFrame | None = None self.y_true: pd.Series | None = None self._data_hash: str | None = None @@ -133,14 +134,19 @@ def compile( cached = pickle.load(f) # noqa: S301 self.contributions = cached["contributions"] self.y_pred = cached["y_pred"] + self.y_prob = cached.get("y_prob") else: explain_data = self.backend.run_explainer(text_list) self.contributions = self.backend.get_local_contributions(text_list, explain_data) - self.y_pred = self._predict_labels(text_list) + pred_df = self._predict(text_list) + self.y_pred = pred_df["prediction"] + self.y_prob = pred_df.drop(columns=["prediction"]) if cache_path is not None: cache_path.parent.mkdir(parents=True, exist_ok=True) with cache_path.open("wb") as f: - pickle.dump({"contributions": self.contributions, "y_pred": self.y_pred}, f) + pickle.dump( + {"contributions": self.contributions, "y_pred": self.y_pred, "y_prob": self.y_prob}, f + ) self.contributions.label_names = self.label_names self.contributions.index = self.texts.index @@ -211,8 +217,13 @@ def run_app(self, port: int = 8050, debug: bool = False, scatter_xy=None) -> Non # Private # ------------------------------------------------------------------ - def _predict_labels(self, text_list: list[str]) -> pd.Series: - """Run the pipeline once to obtain predicted class labels. + def _predict(self, text_list: list[str]) -> pd.DataFrame: + """Run the pipeline and return a unified DataFrame of predictions and probabilities. + + The first column is always ``"prediction"`` (the argmax label). + Subsequent columns hold class probabilities: one per class when the + pipeline returns all scores (``return_all_scores=True``), or a single + ``"probability"`` column (the winning class confidence) otherwise. Handles both ``return_all_scores=True`` (list of lists of dicts) and single-prediction (list of dicts) pipeline output formats. @@ -220,6 +231,16 @@ def _predict_labels(self, text_list: list[str]) -> pd.Series: raw = self.model(text_list) if raw and isinstance(raw[0], list): labels = [max(preds, key=lambda p: p["score"])["label"] for preds in raw] + col_labels = [d["label"] for d in raw[0]] + if self.label_names and len(self.label_names) == len(col_labels): + col_labels = self.label_names + probs = [[d["score"] for d in preds] for preds in raw] + result = pd.DataFrame(probs, index=self.texts.index, columns=col_labels) else: labels = [p["label"] for p in raw] - return pd.Series(labels, index=self.texts.index, name="prediction") + result = pd.DataFrame( + {"probability": [p["score"] for p in raw]}, + index=self.texts.index, + ) + result.insert(0, "prediction", pd.Series(labels, index=self.texts.index)) + return result diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 1cc0e110..925d2ea1 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -102,6 +102,14 @@ def _build_layout(self) -> None: } if getattr(self.explainer, "y_true", None) is not None: records["ground_truth"] = self.explainer.y_true.tolist() + + # Single "Probability" column — confidence of the predicted class. + # max(axis=1) works for both binary and multiclass since the predicted + # label is always the argmax, regardless of how columns are named. + y_prob: pd.DataFrame | None = getattr(self.explainer, "y_prob", None) + if y_prob is not None: + records["probability"] = [f"{v:.1%}" for v in y_prob.max(axis=1).tolist()] + table_df = pd.DataFrame(records) # Full records cached for scatter-driven re-filtering in callbacks @@ -110,6 +118,23 @@ def _build_layout(self) -> None: table_columns = [{"name": "Text", "id": "text"}, {"name": "Prediction", "id": "prediction"}] if "ground_truth" in table_df.columns: table_columns.append({"name": "Ground Truth", "id": "ground_truth"}) + if y_prob is not None: + table_columns.append({"name": "Probability", "id": "probability"}) + + # ── Table column widths — computed to accommodate optional columns ───── + has_gt = "ground_truth" in table_df.columns + has_prob = y_prob is not None + _pred_w, _gt_w, _prob_w = 12, 12, 10 + other_pct = _pred_w + (_gt_w if has_gt else 0) + (_prob_w if has_prob else 0) + text_pct = max(30, 100 - other_pct) + style_cell_conditional: list[dict] = [ + {"if": {"column_id": "text"}, "width": f"{text_pct}%"}, + {"if": {"column_id": "prediction"}, "width": f"{_pred_w}%"}, + ] + if has_gt: + style_cell_conditional.append({"if": {"column_id": "ground_truth"}, "width": f"{_gt_w}%"}) + if has_prob: + style_cell_conditional.append({"if": {"column_id": "probability"}, "width": f"{_prob_w}%"}) # ── Corpus word list for the exclusion multi-select ──────────── all_words = sorted( @@ -290,11 +315,7 @@ def _build_layout(self) -> None: "maxWidth": "0", "padding": "6px 12px", }, - style_cell_conditional=[ - {"if": {"column_id": "text"}, "width": "65%"}, - {"if": {"column_id": "prediction"}, "width": "17%"}, - {"if": {"column_id": "ground_truth"}, "width": "18%"}, - ], + style_cell_conditional=style_cell_conditional, style_header={"fontWeight": "bold", "backgroundColor": "#f8f9fa"}, style_data_conditional=[ { From a010aea3159717745ac8c2eac47a4d3e268fcf9c Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 10:50:40 +0200 Subject: [PATCH 014/144] build: add dash-ag-grid dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13133bf3..2f670d60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,8 @@ nlp = [ "transformers>=5.12.1", "datasets>=5.0.0", "sentence-transformers>=5.6.0", - "pacmap>=0.9.1" + "pacmap>=0.9.1", + "dash-ag-grid>=35.2.0" ] xgboost = ["xgboost>=2.1.0"] From 29ae2a2ae55a592dbcbf383406e46984cdc414d5 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 10:52:07 +0200 Subject: [PATCH 015/144] feat: replace dash table with ag grid --- shapash/webapp/nlp_app.py | 98 ++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 925d2ea1..ba6d211d 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -12,11 +12,12 @@ import re import dash +import dash_ag_grid as dag import dash_bootstrap_components as dbc import numpy as np import pandas as pd import plotly.graph_objs as go -from dash import Input, Output, callback_context, dash_table, dcc, html +from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate from shapash.plots.plot_sentence_highlight import plot_sentence_highlight @@ -108,33 +109,33 @@ def _build_layout(self) -> None: # label is always the argmax, regardless of how columns are named. y_prob: pd.DataFrame | None = getattr(self.explainer, "y_prob", None) if y_prob is not None: - records["probability"] = [f"{v:.1%}" for v in y_prob.max(axis=1).tolist()] + records["probability"] = y_prob.max(axis=1).tolist() table_df = pd.DataFrame(records) # Full records cached for scatter-driven re-filtering in callbacks self._full_table_records: list[dict] = table_df.to_dict("records") - table_columns = [{"name": "Text", "id": "text"}, {"name": "Prediction", "id": "prediction"}] - if "ground_truth" in table_df.columns: - table_columns.append({"name": "Ground Truth", "id": "ground_truth"}) - if y_prob is not None: - table_columns.append({"name": "Probability", "id": "probability"}) - - # ── Table column widths — computed to accommodate optional columns ───── has_gt = "ground_truth" in table_df.columns has_prob = y_prob is not None - _pred_w, _gt_w, _prob_w = 12, 12, 10 - other_pct = _pred_w + (_gt_w if has_gt else 0) + (_prob_w if has_prob else 0) - text_pct = max(30, 100 - other_pct) - style_cell_conditional: list[dict] = [ - {"if": {"column_id": "text"}, "width": f"{text_pct}%"}, - {"if": {"column_id": "prediction"}, "width": f"{_pred_w}%"}, + column_defs: list[dict] = [ + {"field": "text", "headerName": "Text", "flex": 3, "tooltipField": "text", "filter": "agTextColumnFilter"}, + {"field": "prediction", "headerName": "Prediction", "flex": 1, "filter": "agTextColumnFilter"}, ] if has_gt: - style_cell_conditional.append({"if": {"column_id": "ground_truth"}, "width": f"{_gt_w}%"}) + column_defs.append( + {"field": "ground_truth", "headerName": "Ground Truth", "flex": 1, "filter": "agTextColumnFilter"} + ) if has_prob: - style_cell_conditional.append({"if": {"column_id": "probability"}, "width": f"{_prob_w}%"}) + column_defs.append( + { + "field": "probability", + "headerName": "Probability", + "flex": 1, + "filter": "agNumberColumnFilter", + "valueFormatter": {"function": "params.value != null ? params.value.toFixed(3) : ''"}, + } + ) # ── Corpus word list for the exclusion multi-select ──────────── all_words = sorted( @@ -300,35 +301,18 @@ def _build_layout(self) -> None: html.Div( [ html.H6("Text Samples — click a row to inspect", className="fw-bold"), - dash_table.DataTable( + dag.AgGrid( id="dataset-table", - data=self._full_table_records, - columns=table_columns, - row_selectable="single", - selected_rows=[0], - page_action="none", - style_table={"maxHeight": "400px", "overflowY": "auto"}, - style_cell={ - "textAlign": "left", - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": "0", - "padding": "6px 12px", + rowData=self._full_table_records, + columnDefs=column_defs, + defaultColDef={"resizable": True, "sortable": True}, + dashGridOptions={ + "rowSelection": "single", + "tooltipShowDelay": 300, }, - style_cell_conditional=style_cell_conditional, - style_header={"fontWeight": "bold", "backgroundColor": "#f8f9fa"}, - style_data_conditional=[ - { - "if": {"state": "selected"}, - "backgroundColor": "#e8f4f8", - "border": "1px solid #1f77b4", - }, - ], - tooltip_data=[ - {"text": {"value": r["text"], "type": "markdown"}} - for r in self._full_table_records - ], - tooltip_duration=None, + selectedRows=[self._full_table_records[0]], + style={"height": "400px"}, + className="ag-theme-alpine", ), ], style=_CARD_STYLE, @@ -466,15 +450,14 @@ def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, s Output("sentence-highlight-title", "children"), ], [ - Input("dataset-table", "selected_rows"), - Input("dataset-table", "data"), + Input("dataset-table", "selectedRows"), Input("class-selector", "value"), ], ) - def update_sentence_highlight(selected_rows, table_data, label_idx): - if not selected_rows or not table_data or label_idx is None: + def update_sentence_highlight(selected_rows, label_idx): + if not selected_rows or label_idx is None: raise PreventUpdate - pos = int(table_data[selected_rows[0]]["_orig_idx"]) + pos = int(selected_rows[0]["_orig_idx"]) label_idx = int(label_idx) tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) sample_text = self.explainer.texts.iloc[pos] @@ -498,16 +481,15 @@ def toggle_waterfall(show): @self.app.callback( Output("waterfall-graph", "figure"), [ - Input("dataset-table", "selected_rows"), - Input("dataset-table", "data"), + Input("dataset-table", "selectedRows"), Input("class-selector", "value"), Input("waterfall-threshold", "value"), ], ) - def update_waterfall(selected_rows, table_data, label_idx, threshold_pct): - if not selected_rows or not table_data or label_idx is None: + def update_waterfall(selected_rows, label_idx, threshold_pct): + if not selected_rows or label_idx is None: raise PreventUpdate - pos = int(table_data[selected_rows[0]]["_orig_idx"]) + pos = int(selected_rows[0]["_orig_idx"]) label_idx = int(label_idx) tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 @@ -558,9 +540,8 @@ def toggle_clear_button(selected_indices): @self.app.callback( [ - Output("dataset-table", "data"), - Output("dataset-table", "selected_rows"), - Output("dataset-table", "tooltip_data"), + Output("dataset-table", "rowData"), + Output("dataset-table", "selectedRows"), ], Input("scatter-selected-indices", "data"), ) @@ -570,8 +551,7 @@ def filter_table(selected_indices): else: idx_set = set(selected_indices) recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records - tooltips = [{"text": {"value": r["text"], "type": "markdown"}} for r in recs] - return recs, [0], tooltips + return recs, [recs[0]] # ------------------------------------------------------------------ # Public From 8a07dbfbfd7c3bfb5a12a76a018cfde376686f86 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 11:08:54 +0200 Subject: [PATCH 016/144] feat: allow filtering text samples when clicking on a word in global importance --- shapash/webapp/nlp_app.py | 98 ++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 17 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index ba6d211d..10e28149 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -300,7 +300,33 @@ def _build_layout(self) -> None: dbc.Col( html.Div( [ - html.H6("Text Samples — click a row to inspect", className="fw-bold"), + dbc.Row( + [ + dbc.Col( + html.H6( + "Text Samples — click a row to inspect", + className="fw-bold mb-0", + id="table-title", + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + dbc.Button( + "× clear word filter", + id="word-filter-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0", + style={"display": "none", "fontSize": "0.8em"}, + ), + width="auto", + className="align-self-center", + ), + ], + className="align-items-center mb-2", + ), dag.AgGrid( id="dataset-table", rowData=self._full_table_records, @@ -399,8 +425,9 @@ def _build_layout(self) -> None: ], className="mb-3", ), - # ── Hidden store: scatter selection (always present) ────────── + # ── Hidden stores (always present) ─────────────────────────── dcc.Store(id="scatter-selected-indices", data=None), + dcc.Store(id="word-click-filter", data=None), ], fluid=True, ) @@ -501,6 +528,58 @@ def update_waterfall(selected_rows, label_idx, threshold_pct): title=f"Token contributions — {label_name}", ) + # ── Word-bar click → filter table ──────────────────────────────── + @self.app.callback( + Output("word-click-filter", "data"), + Input("global-importance-graph", "clickData"), + Input("word-filter-clear-btn", "n_clicks"), + ) + def update_word_click_filter(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if "word-filter-clear-btn" in trigger: + return None + if not click_data or not click_data.get("points"): + raise PreventUpdate + return click_data["points"][0]["y"] + + @self.app.callback( + Output("word-filter-clear-btn", "style"), + Input("word-click-filter", "data"), + ) + def toggle_word_clear_button(word_filter): + if word_filter: + return {"display": "inline", "fontSize": "0.8em"} + return {"display": "none", "fontSize": "0.8em"} + + # ── Unified table filter (scatter + word-bar click) ─────────────── + @self.app.callback( + [ + Output("dataset-table", "rowData"), + Output("dataset-table", "selectedRows"), + Output("table-title", "children"), + ], + [ + Input("scatter-selected-indices", "data"), + Input("word-click-filter", "data"), + ], + ) + def filter_table(selected_indices, word_filter): + if selected_indices is None: + recs = full_records + else: + idx_set = set(selected_indices) + recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records + if word_filter: + word_lower = word_filter.lower() + filtered = [r for r in recs if word_lower in r["text"].lower()] + recs = filtered or recs + title = ( + f'Text Samples — filtered by "{word_filter}" (click bar again to clear)' + if word_filter + else "Text Samples — click a row to inspect" + ) + return recs, [recs[0]], title + # ── Scatter-specific callbacks (registered only when xy provided) ── if self._scatter_xy is not None: @@ -538,21 +617,6 @@ def toggle_clear_button(selected_indices): hidden = {"display": "none", "fontSize": "0.8em"} return visible if selected_indices else hidden - @self.app.callback( - [ - Output("dataset-table", "rowData"), - Output("dataset-table", "selectedRows"), - ], - Input("scatter-selected-indices", "data"), - ) - def filter_table(selected_indices): - if selected_indices is None: - recs = full_records - else: - idx_set = set(selected_indices) - recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records - return recs, [recs[0]] - # ------------------------------------------------------------------ # Public # ------------------------------------------------------------------ From e15c53357346479104b2e67510ae77213ddf2805 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 14:32:28 +0200 Subject: [PATCH 017/144] feat: change webapp layout --- shapash/webapp/nlp_app.py | 373 ++++++++++++++++++++------------------ 1 file changed, 200 insertions(+), 173 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 10e28149..290b1f82 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -25,6 +25,8 @@ from shapash.plots.plot_word_importance import plot_word_importance _CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} +# Single source of truth so the graph container and the Plotly figure agree. +_WORD_IMPORTANCE_HEIGHT = 390 _SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") _HIDDEN = {"display": "none"} _VISIBLE = {"display": "block"} @@ -45,20 +47,26 @@ class NlpWebApp: """Minimal Dash webapp driven by an ``NlpExplainer``. - Renders four panels: - - **Controls bar** — top-K slider, positive/negative sign filter, corpus - word multi-select for manual stopword exclusion. + The layout follows an "overview → filter → detail" funnel: + + Top row — global "where to look" panels that filter the table below: - **Global word importance** — mean SHAP contribution per unique word for the selected class (updates on any control change or scatter selection). + Its own controls (top-K slider, positive/negative sign filter, corpus + word multi-select for manual stopword exclusion) live inside this panel. + - **Sample scatter** *(optional)* — 2-D projection of the text embeddings, + coloured by prediction or ground-truth label. Draw a box or lasso to + filter the table and word importance to the selected subset. + + Hub — full-width: - **Dataset table** — text samples with predicted (and optional ground-truth) - labels; click a row to populate the local contribution panels. Filtered - to the scatter selection when one is active. + labels; click a row to populate the local contribution panel. Filtered + to the scatter selection and/or a clicked word importance bar. + + Detail-on-demand — full-width: - **Local contributions** — inline sentence highlight as the primary view. An optional waterfall chart (toggle with a radio button) groups tokens below a configurable contribution threshold into a single "other" bar. - - **Sample scatter** *(optional)* — 2-D projection of the text embeddings, - coloured by prediction or ground-truth label. Draw a box or lasso to - filter the table and word importance to the selected subset. Parameters ---------- @@ -199,35 +207,23 @@ def _build_layout(self) -> None: config={ "displayModeBar": True, "modeBarButtonsToRemove": ["autoScale2d", "resetScale2d"], + "responsive": True, }, - style={"height": "340px"}, + # Grow to fill the card's remaining height so the scatter + # matches the taller word-importance panel beside it. + style={"flex": "1 1 auto", "minHeight": "340px"}, ), ], - style=_CARD_STYLE, + # Flex column so the graph above can stretch to the card height. + style={**_CARD_STYLE, "display": "flex", "flexDirection": "column"}, ) - self.app.layout = dbc.Container( + # ── Global Word Importance panel — controls now live inside it ── + # (Top-K / Sign / Exclude only affect this panel, so they are + # co-located here rather than in a misleading full-width top bar.) + word_importance_panel = html.Div( [ - # ── Header ────────────────────────────────────────────────── - dbc.Row( - [ - dbc.Col(html.H3("Shapash — NLP Explainer", className="mb-0"), width=8), - dbc.Col( - [ - html.Label("Class", className="fw-bold small"), - dcc.Dropdown( - id="class-selector", - options=[{"label": name, "value": i} for i, name in enumerate(label_names)], - value=0, - clearable=False, - ), - ], - width=4, - ), - ], - className="mb-2 mt-3 align-items-center", - ), - # ── Controls row ───────────────────────────────────────────── + html.H6("Global Word Importance", className="fw-bold mb-2"), dbc.Row( [ dbc.Col( @@ -243,7 +239,7 @@ def _build_layout(self) -> None: tooltip={"placement": "bottom", "always_visible": False}, ), ], - width=4, + width=7, ), dbc.Col( [ @@ -258,173 +254,203 @@ def _build_layout(self) -> None: value="all", inline=True, inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "14px"}, + labelStyle={"marginRight": "12px"}, ), ], - width=3, + width=5, + ), + ], + className="align-items-center mb-2", + ), + html.Div( + [ + html.Label("Exclude words", className="fw-bold small mb-1"), + dcc.Dropdown( + id="word-filter", + options=[{"label": w, "value": w} for w in all_words], + value=[], + multi=True, + placeholder="Select words to exclude…", + style={"fontSize": "0.9em"}, ), + ], + className="mb-2", + ), + dcc.Graph( + id="global-importance-graph", + config={"displayModeBar": False}, + style={"height": f"{_WORD_IMPORTANCE_HEIGHT}px"}, + ), + ], + style=_CARD_STYLE, + ) + + # ── Text Samples panel (full-width hub) ─────────────────────── + text_samples_panel = html.Div( + [ + dbc.Row( + [ dbc.Col( - [ - html.Label("Exclude words", className="fw-bold small mb-1"), - dcc.Dropdown( - id="word-filter", - options=[{"label": w, "value": w} for w in all_words], - value=[], - multi=True, - placeholder="Select words to exclude…", - style={"fontSize": "0.9em"}, - ), - ], - width=5, + html.H6( + "Text Samples — click a row to inspect", + className="fw-bold mb-0", + id="table-title", + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + dbc.Button( + "× clear word filter", + id="word-filter-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0", + style={"display": "none", "fontSize": "0.8em"}, + ), + width="auto", + className="align-self-center", ), ], - className="mb-3 p-2 bg-light rounded", + className="align-items-center mb-2", + ), + dag.AgGrid( + id="dataset-table", + rowData=self._full_table_records, + columnDefs=column_defs, + defaultColDef={"resizable": True, "sortable": True}, + dashGridOptions={ + "rowSelection": "single", + "tooltipShowDelay": 300, + "rowHeight": 38, + }, + selectedRows=[self._full_table_records[0]], + # Taller table + larger type to emphasise this hub panel. + style={"height": "460px", "--ag-font-size": "15px", "--ag-header-font-size": "14px"}, + className="ag-theme-alpine", ), - # ── Main row ──────────────────────────────────────────────── + ], + style=_CARD_STYLE, + ) + + # ── Token Contributions panel (full-width detail-on-demand) ─── + token_contributions_panel = html.Div( + [ + html.H6( + id="sentence-highlight-title", + children="Token Contributions", + className="fw-bold", + ), + # Min-height + vertical centring so short samples still give the + # panel presence instead of collapsing to a thin strip. + html.Div( + id="sentence-highlight", + style={ + "minHeight": "150px", + "display": "flex", + "flexDirection": "column", + "justifyContent": "center", + }, + ), + html.Hr(style={"margin": "14px 0 10px"}), dbc.Row( [ dbc.Col( - html.Div( - [ - html.H6("Global Word Importance", className="fw-bold"), - dcc.Graph( - id="global-importance-graph", - config={"displayModeBar": False}, - style={"height": "420px"}, - ), + dcc.RadioItems( + id="show-waterfall", + options=[ + {"label": " Show waterfall", "value": "show"}, + {"label": " Hide", "value": "hide"}, ], - style=_CARD_STYLE, + value="hide", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "16px"}, ), - width=5, + width="auto", + className="align-self-center", ), dbc.Col( html.Div( - [ - dbc.Row( - [ - dbc.Col( - html.H6( - "Text Samples — click a row to inspect", - className="fw-bold mb-0", - id="table-title", - ), - width="auto", - className="align-self-center", - ), - dbc.Col( - dbc.Button( - "× clear word filter", - id="word-filter-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0", - style={"display": "none", "fontSize": "0.8em"}, - ), - width="auto", - className="align-self-center", - ), - ], - className="align-items-center mb-2", + id="waterfall-threshold-wrapper", + children=[ + html.Label( + "Group tokens below (% of max contribution)", + className="small fw-bold mb-0 me-2", ), - dag.AgGrid( - id="dataset-table", - rowData=self._full_table_records, - columnDefs=column_defs, - defaultColDef={"resizable": True, "sortable": True}, - dashGridOptions={ - "rowSelection": "single", - "tooltipShowDelay": 300, + dcc.Slider( + id="waterfall-threshold", + min=0, + max=50, + step=1, + value=10, + marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, + tooltip={ + "placement": "bottom", + "always_visible": False, }, - selectedRows=[self._full_table_records[0]], - style={"height": "400px"}, - className="ag-theme-alpine", ), ], - style=_CARD_STYLE, + style=_HIDDEN, ), - width=7, + width=True, ), ], - className="mb-3", + className="align-items-center mb-2", ), - # ── Local contributions (+ optional scatter on the left) ────── + html.Div( + id="waterfall-container", + children=[ + dcc.Graph( + id="waterfall-graph", + config={"displayModeBar": False}, + ), + ], + style=_HIDDEN, + ), + ], + style=_CARD_STYLE, + ) + + # ── Top row: two global "where to look" panels side by side ─── + # Word importance shares the row with the sample-space scatter when a + # projection is provided; otherwise it spans the full width. + if scatter_col_content is not None: + top_row_cols = [ + dbc.Col(word_importance_panel, width=6), + dbc.Col(scatter_col_content, width=6), + ] + else: + top_row_cols = [dbc.Col(word_importance_panel, width=12)] + + self.app.layout = dbc.Container( + [ + # ── Header (Class selector is the only truly global control) ── dbc.Row( [ - *([dbc.Col(scatter_col_content, width=4)] if scatter_col_content is not None else []), + dbc.Col(html.H3("Shapash — NLP Explainer", className="mb-0"), width=8), dbc.Col( - html.Div( - [ - html.H6( - id="sentence-highlight-title", - children="Token Contributions", - className="fw-bold", - ), - html.Div(id="sentence-highlight"), - html.Hr(style={"margin": "14px 0 10px"}), - dbc.Row( - [ - dbc.Col( - dcc.RadioItems( - id="show-waterfall", - options=[ - {"label": " Show waterfall", "value": "show"}, - {"label": " Hide", "value": "hide"}, - ], - value="hide", - inline=True, - inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "16px"}, - ), - width="auto", - className="align-self-center", - ), - dbc.Col( - html.Div( - id="waterfall-threshold-wrapper", - children=[ - html.Label( - "Group tokens below (% of max contribution)", - className="small fw-bold mb-0 me-2", - ), - dcc.Slider( - id="waterfall-threshold", - min=0, - max=50, - step=1, - value=10, - marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, - tooltip={ - "placement": "bottom", - "always_visible": False, - }, - ), - ], - style=_HIDDEN, - ), - width=True, - ), - ], - className="align-items-center mb-2", - ), - html.Div( - id="waterfall-container", - children=[ - dcc.Graph( - id="waterfall-graph", - config={"displayModeBar": False}, - ), - ], - style=_HIDDEN, - ), - ], - style=_CARD_STYLE, - ), - width=8 if scatter_col_content is not None else 12, + [ + html.Label("Class", className="fw-bold small"), + dcc.Dropdown( + id="class-selector", + options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + value=0, + clearable=False, + ), + ], + width=4, ), ], - className="mb-3", + className="mb-3 mt-3 align-items-center", ), + # ── Overview / controls row (filters the funnel below) ──────── + dbc.Row(top_row_cols, className="mb-3"), + # ── Hub: full-width text samples table ─────────────────────── + dbc.Row(dbc.Col(text_samples_panel, width=12), className="mb-3"), + # ── Detail-on-demand: full-width token contributions ───────── + dbc.Row(dbc.Col(token_contributions_panel, width=12), className="mb-3"), # ── Hidden stores (always present) ─────────────────────────── dcc.Store(id="scatter-selected-indices", data=None), dcc.Store(id="word-click-filter", data=None), @@ -467,7 +493,7 @@ def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, s word_imp, title=f"Word importance — {label_name}{suffix}", width=None, - height=390, + height=_WORD_IMPORTANCE_HEIGHT, ) # ── Sentence highlight ─────────────────────────────────────────── @@ -574,7 +600,7 @@ def filter_table(selected_indices, word_filter): filtered = [r for r in recs if word_lower in r["text"].lower()] recs = filtered or recs title = ( - f'Text Samples — filtered by "{word_filter}" (click bar again to clear)' + f'Text Samples — filtered by "{word_filter}"' if word_filter else "Text Samples — click a row to inspect" ) @@ -700,7 +726,8 @@ def _build_scatter_fig(self, color_by: str) -> go.Figure: plot_bgcolor="#f9f9f9", paper_bgcolor="white", margin=dict(l=10, r=10, t=10, b=10), - height=360, + # No fixed height — the responsive Graph stretches it to fill the card. + autosize=True, legend=dict(itemsizing="constant", orientation="v", title_text=""), showlegend=True, ) From f646f07365d802be487caf321894cddfd671ce19 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 16:10:27 +0200 Subject: [PATCH 018/144] test: update nlp tests --- tests/integration_tests/test_nlp_prototype.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/integration_tests/test_nlp_prototype.py b/tests/integration_tests/test_nlp_prototype.py index 4fd956da..5aea9023 100644 --- a/tests/integration_tests/test_nlp_prototype.py +++ b/tests/integration_tests/test_nlp_prototype.py @@ -416,9 +416,9 @@ def test_class_selector_options(self): def test_dataset_table_populated(self): table = self._find_component(self.webapp.app.layout, "dataset-table") self.assertIsNotNone(table, "dataset-table not found in layout") - self.assertEqual(len(table.data), 3) - self.assertIn("text", table.data[0]) - self.assertIn("prediction", table.data[0]) + self.assertEqual(len(table.rowData), 3) + self.assertIn("text", table.rowData[0]) + self.assertIn("prediction", table.rowData[0]) def test_raises_without_compile(self): xpl = _make_explainer(compiled=False) @@ -452,17 +452,17 @@ def test_waterfall_controls_present(self): def test_dataset_table_no_ground_truth_by_default(self): table = self._find_component(self.webapp.app.layout, "dataset-table") - col_ids = [c["id"] for c in table.columns] - self.assertNotIn("ground_truth", col_ids) + col_fields = [c["field"] for c in table.columnDefs] + self.assertNotIn("ground_truth", col_fields) def test_dataset_table_with_y_true(self): xpl = _make_explainer(compiled=True) xpl.y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") webapp = NlpWebApp(xpl) table = self._find_component(webapp.app.layout, "dataset-table") - col_ids = [c["id"] for c in table.columns] - self.assertIn("ground_truth", col_ids) - self.assertEqual(table.data[0]["ground_truth"], "sadness") + col_fields = [c["field"] for c in table.columnDefs] + self.assertIn("ground_truth", col_fields) + self.assertEqual(table.rowData[0]["ground_truth"], "sadness") def test_scatter_store_always_present(self): ids = self._collect_ids(self.webapp.app.layout) @@ -709,12 +709,11 @@ def test_text_plot_raises_before_compile(self): def test_compile_sets_contributions(self): backend = _make_lime_backend() xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) - fake_preds = pd.Series( - ["joy"] * len(_SAMPLE_TEXTS), + fake_pred_df = pd.DataFrame( + {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, index=pd.RangeIndex(len(_SAMPLE_TEXTS)), - name="prediction", ) - with patch.object(xpl, "_predict_labels", return_value=fake_preds): + with patch.object(xpl, "_predict", return_value=fake_pred_df): xpl.compile(_SAMPLE_TEXTS) self.assertIsInstance(xpl.contributions, NlpContributions) self.assertEqual(len(xpl.contributions), len(_SAMPLE_TEXTS)) @@ -723,12 +722,11 @@ def test_compile_sets_contributions(self): def test_compile_caching_skips_rerun(self): backend = _make_lime_backend() xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) - fake_preds = pd.Series( - ["joy"] * len(_SAMPLE_TEXTS), + fake_pred_df = pd.DataFrame( + {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, index=pd.RangeIndex(len(_SAMPLE_TEXTS)), - name="prediction", ) - with patch.object(xpl, "_predict_labels", return_value=fake_preds): + with patch.object(xpl, "_predict", return_value=fake_pred_df): xpl.compile(_SAMPLE_TEXTS) hash_after_first = xpl._data_hash xpl.compile(_SAMPLE_TEXTS) # same data — must hit in-memory cache From 8fd3a689a3ffb9c51f03b9dc2218fd7406bd7ac3 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 16:11:51 +0200 Subject: [PATCH 019/144] chore: move nlp tests into unit tests folder --- tests/{integration_tests => unit_tests/nlp}/test_nlp_prototype.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{integration_tests => unit_tests/nlp}/test_nlp_prototype.py (100%) diff --git a/tests/integration_tests/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py similarity index 100% rename from tests/integration_tests/test_nlp_prototype.py rename to tests/unit_tests/nlp/test_nlp_prototype.py From 8bd6ee09c484309f136ff4cdaf2ee15648b79147 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 17:19:27 +0200 Subject: [PATCH 020/144] chore: move nlp prototype to demo --- .../test_webapp_nlp.py | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) rename {tests/integration_tests => demo}/test_webapp_nlp.py (60%) diff --git a/tests/integration_tests/test_webapp_nlp.py b/demo/test_webapp_nlp.py similarity index 60% rename from tests/integration_tests/test_webapp_nlp.py rename to demo/test_webapp_nlp.py index b69631a6..bd28977f 100644 --- a/tests/integration_tests/test_webapp_nlp.py +++ b/demo/test_webapp_nlp.py @@ -1,16 +1,32 @@ +from pathlib import Path + import datasets +import numpy as np +import pacmap import pandas as pd -import transformers - import torch -import numpy as np +import transformers from shapash.explainer.nlp_explainer import NlpExplainer -import pacmap +_HERE = Path(__file__).parent -def project_embeddings(embeddings, n_components=2): +def project_embeddings(embeddings, n_components=2): + """Project high-dimensional embeddings to a lower-dimensional space using PaCMAP. + + Parameters + ---------- + embeddings : np.ndarray, shape (n_samples, hidden_dim) + Dense vector representations of the input texts. + n_components : int, optional + Number of output dimensions. Default is 2 (for 2-D scatter plots). + + Returns + ------- + np.ndarray, shape (n_samples, n_components) + Low-dimensional projection initialised with PCA. + """ projector = pacmap.PaCMAP( n_components=n_components, n_neighbors=5, @@ -19,19 +35,37 @@ def project_embeddings(embeddings, n_components=2): # random_state=1 ) - return projector.fit_transform( - embeddings, init="pca" - ) + return projector.fit_transform(embeddings, init="pca") def get_embeddings(texts, classifier, tokenizer, batch_size=32): - + """Extract mean-pooled last-layer hidden states from a HuggingFace classifier. + + Runs inference in batches without gradient computation. Tokens are pooled + using the attention mask so padding tokens do not contribute to the mean. + + Parameters + ---------- + texts : list of str + Raw input texts to encode. + classifier : transformers.PreTrainedModel + A HuggingFace model that accepts ``output_hidden_states=True``. + The model's ``device`` attribute is used to move batches automatically. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer matching the classifier; applied with padding and truncation. + batch_size : int, optional + Number of texts processed per forward pass. Default is 32. + + Returns + ------- + np.ndarray, shape (n_samples, hidden_dim) + One embedding vector per input text. + """ device = classifier.device - all_embeddings = [] for i in range(0, len(texts), batch_size): - batch = tokenizer(texts[i:i+batch_size], padding=True, truncation=True, return_tensors="pt") + batch = tokenizer(texts[i : i + batch_size], padding=True, truncation=True, return_tensors="pt") batch = {k: v.to(device) for k, v in batch.items()} # fix 1: move to GPU with torch.no_grad(): out = classifier(**batch, output_hidden_states=True) # fix 2: request hidden states @@ -43,7 +77,6 @@ def get_embeddings(texts, classifier, tokenizer, batch_size=32): if __name__ == "__main__": - N = 100 # load the emotion dataset @@ -56,12 +89,13 @@ def get_embeddings(texts, classifier, tokenizer, batch_size=32): # load the model and tokenizer tokenizer = transformers.AutoTokenizer.from_pretrained( "bhadresh-savani/distilbert-base-uncased-emotion", - #"nateraw/bert-base-uncased-emotion", - use_fast=True) + # "nateraw/bert-base-uncased-emotion", + use_fast=True, + ) classifier = transformers.AutoModelForSequenceClassification.from_pretrained( "bhadresh-savani/distilbert-base-uncased-emotion", - #"nateraw/bert-base-uncased-emotion" - ).cuda() + # "nateraw/bert-base-uncased-emotion" + ).cuda() # build a pipeline object to do predictions pred = transformers.pipeline( @@ -91,4 +125,5 @@ def get_embeddings(texts, classifier, tokenizer, batch_size=32): xpl = NlpExplainer(pred, label_names=label_names) xpl.compile(sentences, y_true=[label_names[i] for i in labels]) + xpl.save_snapshot(_HERE / "explainer_snapshot.pkl", scatter_xy=projected) xpl.run_app(port=8050, debug=False, scatter_xy=projected) From 63b5cd28061143895664feb9b388d89eeb18845c Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 17:20:08 +0200 Subject: [PATCH 021/144] build: build nlp webapp with docker compose --- demo/Dockerfile | 25 +++++++++++++++ demo/README.md | 59 ++++++++++++++++++++++++++++++++++++ demo/docker-compose.yml | 8 +++++ demo/explainer_snapshot.pkl | Bin 0 -> 156540 bytes 4 files changed, 92 insertions(+) create mode 100644 demo/Dockerfile create mode 100644 demo/README.md create mode 100644 demo/docker-compose.yml create mode 100644 demo/explainer_snapshot.pkl diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 00000000..8279e880 --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,25 @@ +# Build from the repo root: +# docker build -f demo/Dockerfile -t shapash-nlp . +# +# The image contains the snapshot baked in, so colleagues only need the image: +# docker run --rm -p 8050:8050 shapash-nlp +# Then open http://localhost:8050 + +FROM python:3.11-slim + +WORKDIR /app + +# Install shapash base dependencies (dash, plotly, pandas, numpy, …) +# then add dash-ag-grid which is in the [nlp] optional group but needed by NlpWebApp. +# Heavy ML deps (torch, transformers, datasets, pacmap) are intentionally omitted. +COPY pyproject.toml README.md MANIFEST.in ./ +COPY shapash/ ./shapash/ +RUN pip install --no-cache-dir "." "dash-ag-grid>=35.2.0" "pyarrow>=17.0.0" + +# Serve script + pre-computed snapshot (no model required at runtime) +COPY demo/serve_nlp.py ./serve_nlp.py +COPY demo/explainer_snapshot.pkl ./explainer_snapshot.pkl + +EXPOSE 8050 + +CMD ["python", "serve_nlp.py"] diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 00000000..e9003a41 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,59 @@ +# Shapash NLP Explainer — Demo + +Interactive webapp that explains a DistilBERT emotion classifier at the token level using SHAP values. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) installed and running +- The snapshot file `demo/explainer_snapshot.pkl` (see Step 1 below) + +--- + +## Step 1 — Generate the snapshot (one-time, requires GPU) + +This step runs the model and computes SHAP values for 100 samples. It only needs to be done once; the result is saved as a self-contained file. + +```bash +# From the repo root +pip install "shapash[nlp]" +python demo/test_webapp_nlp.py +``` + +This writes `demo/explainer_snapshot.pkl`. The file bundles texts, predictions, SHAP contributions, and the 2-D scatter projection — everything the webapp needs. + +> **Already have the snapshot?** Skip to Step 2. + +--- + +## Step 2 — Build the Docker image + +Run from the **repo root** (the build context needs access to the `shapash/` package): + +```bash +docker compose -f demo/docker-compose.yml build +``` + +The image installs only the lightweight serving dependencies (Dash, Plotly, pandas …). No model, no GPU, no heavy ML libraries. + +--- + +## Step 3 — Run the app + +```bash +docker compose -f demo/docker-compose.yml up +``` + +Then open **http://localhost:8050** in your browser. + +Press `Ctrl+C` to stop. + +--- + +## Webapp overview + +| Panel | Description | +|---|---| +| **Global word importance** | Mean SHAP contribution per word for the selected class. Use the top-K slider, sign filter, and word exclusion list to explore. | +| **Sample scatter** | 2D PaCMAP projection of text embeddings. Draw a box, lasso or click to filter the table and word importance to a subset. | +| **Dataset table** | All samples with predicted label, ground truth, and predicted probability. Click a row to inspect it. Click a bar in the word importance chart to filter to samples containing that word. | +| **Local contributions** | Token-level SHAP highlight for the selected sample. Toggle to a waterfall chart for a ranked view. | diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml new file mode 100644 index 00000000..16d6c99c --- /dev/null +++ b/demo/docker-compose.yml @@ -0,0 +1,8 @@ +services: + shapash-nlp: + build: + context: .. + dockerfile: demo/Dockerfile + image: shapash-nlp + ports: + - "8050:8050" diff --git a/demo/explainer_snapshot.pkl b/demo/explainer_snapshot.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d81091ab4489d02532af17b331ccdf271fae544 GIT binary patch literal 156540 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 z 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 Date: Mon, 29 Jun 2026 17:21:45 +0200 Subject: [PATCH 022/144] build: add serve nlp script --- demo/serve_nlp.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 demo/serve_nlp.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py new file mode 100644 index 00000000..cc290352 --- /dev/null +++ b/demo/serve_nlp.py @@ -0,0 +1,24 @@ +"""Serve the NLP explainer webapp from a pre-computed snapshot. + +Usage +----- + python serve_nlp.py [path/to/explainer_snapshot.pkl] + +The snapshot is produced by running ``demo/test_webapp_nlp.py``, which calls +``NlpExplainer.save_snapshot()`` after compile. No model, dataset, or GPU is +required at serve time. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from shapash.explainer.nlp_explainer import NlpExplainer + +snapshot = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("explainer_snapshot.pkl") +if not snapshot.exists(): + raise FileNotFoundError(f"Snapshot not found: {snapshot}\nRun demo/test_webapp_nlp.py first to generate it.") + +xpl, scatter_xy = NlpExplainer.from_snapshot(snapshot) +xpl.run_app(port=8050, debug=False, host="0.0.0.0", scatter_xy=scatter_xy) # noqa: S104 From 3a515167ab7f1d0bff9fb93d91c88edc6e06917b Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Mon, 29 Jun 2026 17:22:41 +0200 Subject: [PATCH 023/144] refactor: adapt the webapp --- shapash/explainer/nlp_explainer.py | 59 +++++++++++++++++++++++++++++- shapash/webapp/nlp_app.py | 32 +++++++++------- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index dd8b5044..c839e9ed 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -192,7 +192,62 @@ def text_plot(self, pos: int = 0, label_idx: int = 0, max_tokens: int | None = N return plot_token_highlight(tokens=tokens, values=values, title=title, max_tokens=max_tokens) - def run_app(self, port: int = 8050, debug: bool = False, scatter_xy=None) -> None: + def save_snapshot(self, path: str | Path, scatter_xy=None) -> None: + """Persist compiled results (no model or backend) for offline serving. + + The saved file contains only the data required by ``NlpWebApp`` — + texts, contributions, predictions and the optional 2-D projection. + It can be loaded back with :meth:`from_snapshot` without installing + torch, transformers, or any other heavy inference dependency. + + Parameters + ---------- + path : str or Path + Destination ``.pkl`` file. + scatter_xy : np.ndarray, optional + Pre-computed 2-D projection to bundle with the snapshot (same + array you would pass to ``run_app``). + """ + if self.contributions is None: + raise RuntimeError("Call compile() before save_snapshot().") + state = { + "texts": self.texts, + "contributions": self.contributions, + "y_pred": self.y_pred, + "y_prob": self.y_prob, + "y_true": self.y_true, + "label_names": self.label_names, + "scatter_xy": scatter_xy, + } + with Path(path).open("wb") as f: + pickle.dump(state, f) + + @classmethod + def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: + """Restore a snapshot saved by :meth:`save_snapshot`. + + Returns + ------- + explainer : NlpExplainer + Ready-to-serve instance (``model`` and ``backend`` are ``None``). + scatter_xy : np.ndarray or None + The projection array bundled at save time, or ``None``. + """ + with Path(path).open("rb") as f: + state = pickle.load(f) # noqa: S301 + xpl = cls.__new__(cls) + xpl.model = None + xpl.backend = None + xpl.label_names = state.get("label_names") + xpl.texts = state["texts"] + xpl.contributions = state["contributions"] + xpl.y_pred = state["y_pred"] + xpl.y_prob = state.get("y_prob") + xpl.y_true = state.get("y_true") + xpl._data_hash = None + return xpl, state.get("scatter_xy") + + def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1", scatter_xy=None) -> None: """Launch the NLP explanation webapp. Parameters @@ -211,7 +266,7 @@ def run_app(self, port: int = 8050, debug: bool = False, scatter_xy=None) -> Non """ if self.contributions is None: raise RuntimeError("Call compile() before run_app().") - NlpWebApp(self, scatter_xy=scatter_xy).run(port=port, debug=debug) + NlpWebApp(self, scatter_xy=scatter_xy).run(port=port, debug=debug, host=host) # ------------------------------------------------------------------ # Private diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 290b1f82..0cd8f3c8 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -25,8 +25,11 @@ from shapash.plots.plot_word_importance import plot_word_importance _CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} -# Single source of truth so the graph container and the Plotly figure agree. -_WORD_IMPORTANCE_HEIGHT = 390 +# min() only shrinks on small screens; returns the natural size on large ones. +# 800 px CSS (HiDPI laptop): 224 px chart / 240 px table — enough to show token contributions +# 1080 px CSS (external): 302 px chart / 324 px table — close to original feel +_WORD_IMPORTANCE_HEIGHT_CSS = "min(390px, 28vh)" +_TABLE_HEIGHT_CSS = "min(460px, 30vh)" _SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") _HIDDEN = {"display": "none"} _VISIBLE = {"display": "block"} @@ -278,8 +281,8 @@ def _build_layout(self) -> None: ), dcc.Graph( id="global-importance-graph", - config={"displayModeBar": False}, - style={"height": f"{_WORD_IMPORTANCE_HEIGHT}px"}, + config={"displayModeBar": False, "responsive": True}, + style={"height": _WORD_IMPORTANCE_HEIGHT_CSS}, ), ], style=_CARD_STYLE, @@ -326,8 +329,11 @@ def _build_layout(self) -> None: "rowHeight": 38, }, selectedRows=[self._full_table_records[0]], - # Taller table + larger type to emphasise this hub panel. - style={"height": "460px", "--ag-font-size": "15px", "--ag-header-font-size": "14px"}, + style={ + "height": _TABLE_HEIGHT_CSS, + "--ag-font-size": "15px", + "--ag-header-font-size": "14px", + }, className="ag-theme-alpine", ), ], @@ -489,12 +495,14 @@ def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, s ) label_name = (contrib.label_names or [])[int(label_idx)] if contrib.label_names else str(label_idx) suffix = f" ({len(selected_indices)} samples)" if selected_indices is not None else "" - return plot_word_importance( + fig = plot_word_importance( word_imp, title=f"Word importance — {label_name}{suffix}", width=None, - height=_WORD_IMPORTANCE_HEIGHT, + height=None, ) + fig.layout.height = None # let the CSS container height take over + return fig # ── Sentence highlight ─────────────────────────────────────────── @self.app.callback( @@ -513,10 +521,8 @@ def update_sentence_highlight(selected_rows, label_idx): pos = int(selected_rows[0]["_orig_idx"]) label_idx = int(label_idx) tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) - sample_text = self.explainer.texts.iloc[pos] - preview = (sample_text[:60] + "…") if len(sample_text) > 60 else sample_text highlight = plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) - title = f'Token Contributions — {label_name} | "{preview}"' + title = f"Token Contributions — {label_name}" return highlight, title # ── Waterfall show/hide ────────────────────────────────────────── @@ -647,9 +653,9 @@ def toggle_clear_button(selected_indices): # Public # ------------------------------------------------------------------ - def run(self, port: int = 8050, debug: bool = False) -> None: + def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> None: """Launch the Dash development server.""" - self.app.run(port=port, debug=debug) + self.app.run(port=port, debug=debug, host=host) # ------------------------------------------------------------------ # Helpers From 62aab07045197994d5090aa6e932c51f21d53361 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 30 Jun 2026 11:02:06 +0200 Subject: [PATCH 024/144] fix: fix dockerfile with setuptools-scm --- demo/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demo/Dockerfile b/demo/Dockerfile index 8279e880..bad0c501 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -14,6 +14,8 @@ WORKDIR /app # Heavy ML deps (torch, transformers, datasets, pacmap) are intentionally omitted. COPY pyproject.toml README.md MANIFEST.in ./ COPY shapash/ ./shapash/ +# setuptools-scm can't find .git in the build context; tell it the version explicitly. +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=2.9.0 RUN pip install --no-cache-dir "." "dash-ag-grid>=35.2.0" "pyarrow>=17.0.0" # Serve script + pre-computed snapshot (no model required at runtime) From 44d4cbd3813e6cb3fa1f81f26a634c0e8fcd8d4d Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 30 Jun 2026 11:12:18 +0200 Subject: [PATCH 025/144] chore: change to dev version since any placeholder works --- demo/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/Dockerfile b/demo/Dockerfile index bad0c501..412d1339 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -14,8 +14,9 @@ WORKDIR /app # Heavy ML deps (torch, transformers, datasets, pacmap) are intentionally omitted. COPY pyproject.toml README.md MANIFEST.in ./ COPY shapash/ ./shapash/ -# setuptools-scm can't find .git in the build context; tell it the version explicitly. -ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=2.9.0 +# setuptools-scm can't find .git in the build context; any placeholder works since +# the running app doesn't use the installed package version at runtime. +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=0.0.0.dev RUN pip install --no-cache-dir "." "dash-ag-grid>=35.2.0" "pyarrow>=17.0.0" # Serve script + pre-computed snapshot (no model required at runtime) From 9db5e851570ca6bf6914397bbd8654afa4781baf Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 30 Jun 2026 11:33:54 +0200 Subject: [PATCH 026/144] feat: add filter to display misclassification errors --- shapash/webapp/nlp_app.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 0cd8f3c8..16a1fe9f 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -128,6 +128,7 @@ def _build_layout(self) -> None: self._full_table_records: list[dict] = table_df.to_dict("records") has_gt = "ground_truth" in table_df.columns + self._has_gt = has_gt has_prob = y_prob is not None column_defs: list[dict] = [ {"field": "text", "headerName": "Text", "flex": 3, "tooltipField": "text", "filter": "agTextColumnFilter"}, @@ -315,6 +316,17 @@ def _build_layout(self) -> None: width="auto", className="align-self-center", ), + dbc.Col( + dbc.Switch( + id="errors-only-switch", + label="Errors only", + value=False, + className="small mb-0", + style={} if has_gt else {"display": "none"}, + ), + width="auto", + className="align-self-center ms-2", + ), ], className="align-items-center mb-2", ), @@ -471,6 +483,7 @@ def _build_layout(self) -> None: def _register_callbacks(self) -> None: contrib = self.explainer.contributions full_records = self._full_table_records + has_gt = self._has_gt # ── Global word importance ─────────────────────────────────────── @self.app.callback( @@ -583,7 +596,7 @@ def toggle_word_clear_button(word_filter): return {"display": "inline", "fontSize": "0.8em"} return {"display": "none", "fontSize": "0.8em"} - # ── Unified table filter (scatter + word-bar click) ─────────────── + # ── Unified table filter (scatter + word-bar click + errors-only) ── @self.app.callback( [ Output("dataset-table", "rowData"), @@ -593,14 +606,18 @@ def toggle_word_clear_button(word_filter): [ Input("scatter-selected-indices", "data"), Input("word-click-filter", "data"), + Input("errors-only-switch", "value"), ], ) - def filter_table(selected_indices, word_filter): + def filter_table(selected_indices, word_filter, errors_only): if selected_indices is None: recs = full_records else: idx_set = set(selected_indices) recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records + if errors_only and has_gt: + misclassified = [r for r in recs if str(r.get("prediction", "")) != str(r.get("ground_truth", ""))] + recs = misclassified or recs if word_filter: word_lower = word_filter.lower() filtered = [r for r in recs if word_lower in r["text"].lower()] From f860b0856693bdf0bd36e3b3fe4246af1c2c9af7 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 30 Jun 2026 11:56:04 +0200 Subject: [PATCH 027/144] docs: simplify run steps in readme --- demo/README.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/demo/README.md b/demo/README.md index e9003a41..2dc1cde6 100644 --- a/demo/README.md +++ b/demo/README.md @@ -25,23 +25,15 @@ This writes `demo/explainer_snapshot.pkl`. The file bundles texts, predictions, --- -## Step 2 — Build the Docker image +## Step 2 — Build and run the app Run from the **repo root** (the build context needs access to the `shapash/` package): ```bash -docker compose -f demo/docker-compose.yml build +docker compose -f demo/docker-compose.yml up --build ``` -The image installs only the lightweight serving dependencies (Dash, Plotly, pandas …). No model, no GPU, no heavy ML libraries. - ---- - -## Step 3 — Run the app - -```bash -docker compose -f demo/docker-compose.yml up -``` +The image installs only the lightweight serving dependencies (Dash, Plotly, pandas …). No model, no GPU, no heavy ML libraries. Drop `--build` on subsequent runs if nothing has changed. Then open **http://localhost:8050** in your browser. From f3c114b2ad3fc3bf8e9d5d2365398480fd6aca67 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 30 Jun 2026 16:18:26 +0200 Subject: [PATCH 028/144] feat: add contributions of a selected word in scatter plot after the user picks a token (e.g. great, terrible), color all points by that token's SHAP contribution --- shapash/webapp/nlp_app.py | 145 ++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 12 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 16a1fe9f..2ff91bae 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -165,6 +165,7 @@ def _build_layout(self) -> None: color_options = [{"label": "Prediction", "value": "prediction"}] if getattr(self.explainer, "y_true", None) is not None: color_options.append({"label": "Ground Truth", "value": "ground_truth"}) + color_options.append({"label": "Word contribution", "value": "word_contribution"}) scatter_col_content = html.Div( [ @@ -185,6 +186,19 @@ def _build_layout(self) -> None: ), width="auto", ), + dbc.Col( + dcc.Dropdown( + id="scatter-word-select", + options=[{"label": w, "value": w} for w in all_words], + value=[], + multi=True, + clearable=True, + placeholder="Select words…", + style={"display": "none", "minWidth": "180px", "fontSize": "0.9em"}, + ), + width="auto", + className="align-self-center", + ), dbc.Col( dbc.Button( "× clear", @@ -202,7 +216,7 @@ def _build_layout(self) -> None: className="align-items-center mb-2", ), html.Small( - "Click a point or box/lasso to filter — click again or × clear to reset.", + "Box/lasso or click to filter. Click a word bar to color by its SHAP contribution.", className="text-muted d-block mb-2", ), dcc.Graph( @@ -634,10 +648,40 @@ def filter_table(selected_indices, word_filter, errors_only): @self.app.callback( Output("scatter-plot", "figure"), + [ + Input("color-by", "value"), + Input("scatter-word-select", "value"), + Input("class-selector", "value"), + ], + ) + def update_scatter_color(color_by, words, label_idx): + return self._build_scatter_fig( + color_by or "prediction", words=words or [], label_idx=int(label_idx or 0) + ) + + @self.app.callback( + Output("color-by", "value"), + Input("scatter-word-select", "value"), + ) + def sync_color_by(words): + if not words: + raise PreventUpdate + return "word_contribution" + + @self.app.callback( + Output("scatter-word-select", "value"), + Input("word-click-filter", "data"), + ) + def sync_word_select_from_bar(word_filter): + return [word_filter] if word_filter else [] + + @self.app.callback( + Output("scatter-word-select", "style"), Input("color-by", "value"), ) - def update_scatter_color(color_by): - return self._build_scatter_fig(color_by or "prediction") + def toggle_word_select(color_by): + base = {"minWidth": "180px", "fontSize": "0.9em"} + return base if color_by == "word_contribution" else {**base, "display": "none"} @self.app.callback( Output("scatter-selected-indices", "data"), @@ -700,16 +744,95 @@ def _sample_data(self, pos: int, label_idx: int): label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) return tokens, vals, base_value, label_name - def _build_scatter_fig(self, color_by: str) -> go.Figure: - """2-D scatter coloured by prediction or ground-truth label. - - One trace per class so the legend works correctly and Plotly's - box/lasso select dims unselected points across all traces. - ``customdata`` stores the original sample index so ``selectedData`` - callbacks can recover it regardless of which trace a point is in. + def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: + """Per-sample sum of SHAP contributions for all tokens matching *word*.""" + contrib = self.explainer.contributions + n = len(self.explainer.texts) + result = np.zeros(n) + word_lower = word.lower() + for i in range(n): + tokens = contrib.token_strings[i] + vals = contrib.values[i] + if vals.ndim == 2: + vals = vals[:, label_idx] + for j, tok in enumerate(tokens): + if tok.strip().lower() == word_lower: + result[i] += vals[j] + return result + + def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, label_idx: int = 0) -> go.Figure: + """2-D scatter coloured by prediction, ground-truth label, or word SHAP contribution. + + One trace per class for label-based coloring so the legend works correctly + and Plotly's box/lasso select dims unselected points across all traces. + Word-contribution mode uses a single diverging-colorscale trace. + ``customdata`` always stores the original sample index. """ n = len(self.explainer.texts) contrib = self.explainer.contributions + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self.explainer.texts] + xy = self._scatter_xy + + if color_by == "word_contribution" and words: + contributions = sum(self._word_contributions(w, label_idx) for w in words) + max_abs = float(np.abs(contributions).max()) or 1.0 + present_mask = np.where(contributions != 0.0)[0] + absent_mask = np.where(contributions == 0.0)[0] + colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" + + fig = go.Figure() + # Gray context layer — only absent points so hover doesn't overlap + if len(absent_mask) > 0: + fig.add_trace( + go.Scattergl( + x=xy[absent_mask, 0].tolist(), + y=xy[absent_mask, 1].tolist(), + mode="markers", + marker=dict(color="#b0b0b0", size=5, opacity=0.35), + customdata=absent_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in absent_mask], + hovertemplate="%{text}absent", + showlegend=False, + ) + ) + # Colored overlay — only samples where at least one word contributes + if len(present_mask) > 0: + fig.add_trace( + go.Scattergl( + x=xy[present_mask, 0].tolist(), + y=xy[present_mask, 1].tolist(), + mode="markers", + marker=dict( + color=contributions[present_mask].tolist(), + colorscale="RdBu", + cmin=-max_abs, + cmax=max_abs, + size=9, + opacity=0.9, + colorbar=dict( + title=dict(text=colorbar_title, side="right"), + thickness=12, + tickformat=".2f", + ), + ), + customdata=present_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in present_mask], + hovertemplate="SHAP: %{marker.color:.3f}
%{text}", + showlegend=False, + ) + ) + fig.update_layout( + dragmode="select", + uirevision="scatter", + xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + plot_bgcolor="#f9f9f9", + paper_bgcolor="white", + margin=dict(l=10, r=10, t=10, b=10), + autosize=True, + showlegend=False, + ) + return fig if color_by == "ground_truth" and getattr(self.explainer, "y_true", None) is not None: labels = [str(label) for label in self.explainer.y_true.tolist()] @@ -719,8 +842,6 @@ def _build_scatter_fig(self, color_by: str) -> go.Figure: labels = [""] * n label_names = contrib.label_names or sorted(set(labels)) - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self.explainer.texts] - xy = self._scatter_xy fig = go.Figure() for i, name in enumerate(label_names): From 3bf7dbb18c9fedcfb38e89b42b50270f8ebaf4d6 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Wed, 1 Jul 2026 12:16:27 +0200 Subject: [PATCH 029/144] style: modify buttons --- shapash/webapp/nlp_app.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 2ff91bae..5295ce7d 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -333,7 +333,7 @@ def _build_layout(self) -> None: dbc.Col( dbc.Switch( id="errors-only-switch", - label="Errors only", + label="Model Errors", value=False, className="small mb-0", style={} if has_gt else {"display": "none"}, @@ -389,16 +389,11 @@ def _build_layout(self) -> None: dbc.Row( [ dbc.Col( - dcc.RadioItems( + dbc.Switch( id="show-waterfall", - options=[ - {"label": " Show waterfall", "value": "show"}, - {"label": " Hide", "value": "hide"}, - ], - value="hide", - inline=True, - inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "16px"}, + label="Show waterfall", + value=False, + # className="small mb-0", ), width="auto", className="align-self-center", @@ -561,7 +556,7 @@ def update_sentence_highlight(selected_rows, label_idx): Input("show-waterfall", "value"), ) def toggle_waterfall(show): - return (_VISIBLE, _VISIBLE) if show == "show" else (_HIDDEN, _HIDDEN) + return (_VISIBLE, _VISIBLE) if show else (_HIDDEN, _HIDDEN) # ── Waterfall chart ────────────────────────────────────────────── @self.app.callback( From d92c342bfb761e22be52bef1c18a3ae7079728a3 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:30:05 +0200 Subject: [PATCH 030/144] feat: add counterfactual generators modules --- shapash/compute/__init__.py | 8 ++ shapash/compute/generators/__init__.py | 29 +++++ shapash/compute/generators/base.py | 127 +++++++++++++++++++ shapash/compute/generators/cf_utils.py | 39 ++++++ shapash/compute/generators/hotflip.py | 163 +++++++++++++++++++++++++ 5 files changed, 366 insertions(+) create mode 100644 shapash/compute/__init__.py create mode 100644 shapash/compute/generators/__init__.py create mode 100644 shapash/compute/generators/base.py create mode 100644 shapash/compute/generators/cf_utils.py create mode 100644 shapash/compute/generators/hotflip.py diff --git a/shapash/compute/__init__.py b/shapash/compute/__init__.py new file mode 100644 index 00000000..28b39c4a --- /dev/null +++ b/shapash/compute/__init__.py @@ -0,0 +1,8 @@ +"""Compute layer — heavy computation behind modality-agnostic interfaces. + +Seeded by the NLP prototype ahead of the master refactoring. Today it holds only the +``generators`` sub-package (counterfactual / what-if generation). The master plan's +``analyses/{local,global,diagnostics}`` interpreters will move here in later phases; +``generators`` is a *new* sibling axis for generative components (LIT's ``Generator``, +as opposed to explanation ``Interpreter``s). +""" diff --git a/shapash/compute/generators/__init__.py b/shapash/compute/generators/__init__.py new file mode 100644 index 00000000..3bb1eee9 --- /dev/null +++ b/shapash/compute/generators/__init__.py @@ -0,0 +1,29 @@ +"""Counterfactual / what-if generators — the generative axis of the compute layer. + +Mirrors Google PAIR LIT's ``Generator`` component: given one input, produce synthetic variants +that change the model's prediction. Generators declare their model-capability requirements via +``is_compatible`` and their tunable knobs via ``config_spec`` (auto-rendered by the webapp). + +Ships :class:`HotFlipGenerator` (gradient-based token substitution). :class:`AblationFlipGenerator` +(forward-pass-only token removal) is a planned sibling using the same ABC. +""" + +from __future__ import annotations + +from shapash.compute.generators.base import ( + Counterfactual, + CounterfactualGenerator, + Field, + IntField, + TokenListField, +) +from shapash.compute.generators.hotflip import HotFlipGenerator + +__all__ = [ + "Counterfactual", + "CounterfactualGenerator", + "Field", + "IntField", + "TokenListField", + "HotFlipGenerator", +] diff --git a/shapash/compute/generators/base.py b/shapash/compute/generators/base.py new file mode 100644 index 00000000..cb9d9e93 --- /dev/null +++ b/shapash/compute/generators/base.py @@ -0,0 +1,127 @@ +"""Counterfactual generator interface + declarative config spec. + +Design mirrors both LIT's ``Generator`` (``generate`` + ``config_spec`` + compatibility check) and +the shapash ``NlpBackend`` ABC style. The tiny :class:`Field` spec types let a generator declare its +tunable parameters once; the webapp renders controls from that declaration automatically, so adding +a new knob or a new generator needs no UI changes. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any + +from shapash.model.base import TextModel + + +@dataclass(frozen=True) +class Counterfactual: + """One generated counterfactual for a single input text. + + Attributes + ---------- + original_text : str + The input text the counterfactual was derived from. + new_text : str + The perturbed text (reconstructed from the modified tokens). + tokens : list[str] + The original content tokens the generator operated on. + flipped_positions : list[int] + Positions (into ``tokens``) that were modified. + substitutions : list[tuple[int, str, str]] + ``(position, old_token, new_token)`` for each modification. + orig_label : str + Predicted label of ``original_text``. + new_label : str + Predicted label of ``new_text`` (differs from ``orig_label`` on a successful flip). + orig_prob : float + Probability of ``orig_label`` on the original text. + new_prob : float + Probability of ``new_label`` on the counterfactual text. + prob_delta : float + Drop in the original label's probability (``orig_prob`` minus its probability on the CF). + """ + + original_text: str + new_text: str + tokens: list[str] + flipped_positions: list[int] + substitutions: list[tuple[int, str, str]] + orig_label: str + new_label: str + orig_prob: float + new_prob: float + prob_delta: float + + +@dataclass(frozen=True) +class Field: + """Base declarative spec for a configurable generator parameter.""" + + label: str + default: Any + + +@dataclass(frozen=True) +class IntField(Field): + """Integer parameter rendered as a bounded numeric control.""" + + minimum: int = 1 + maximum: int = 10 + + +@dataclass(frozen=True) +class TokenListField(Field): + """List-of-strings parameter (e.g. tokens to ignore) rendered as a tag/multi-select input.""" + + default: list[str] = field(default_factory=list) + + +class CounterfactualGenerator(ABC): + """Abstract base for counterfactual generators. + + Concrete generators implement :meth:`config_spec`, :meth:`is_compatible` and :meth:`generate`. + They depend on a :class:`~shapash.model.base.TextModel` through its declared capabilities, never + on a concrete model class. + """ + + name: str = "counterfactual" + + def __init__(self, model: TextModel) -> None: + self.model = model + + @abstractmethod + def config_spec(self) -> dict[str, Field]: + """Return the tunable parameters as ``name -> Field`` (drives the webapp controls).""" + + @classmethod + @abstractmethod + def is_compatible(cls, model: TextModel) -> bool: + """Return ``True`` when ``model`` exposes the capabilities this generator needs.""" + + @abstractmethod + def generate(self, text: str, target_label: str | None = None, config: dict | None = None) -> list[Counterfactual]: + """Generate counterfactuals for ``text``. + + Parameters + ---------- + text : str + Input text to perturb. + target_label : str or None + Desired label to flip *towards*. When ``None``, any label change counts as success. + config : dict or None + Overrides for :meth:`config_spec` defaults (keys match the spec). + + Returns + ------- + list[Counterfactual] + Successful counterfactuals, minimal and ordered by increasing number of flips. + """ + + def resolve_config(self, config: dict | None) -> dict: + """Merge user ``config`` over the spec defaults into a plain value dict.""" + resolved = {name: fld.default for name, fld in self.config_spec().items()} + if config: + resolved.update({k: v for k, v in config.items() if k in resolved}) + return resolved diff --git a/shapash/compute/generators/cf_utils.py b/shapash/compute/generators/cf_utils.py new file mode 100644 index 00000000..690b790f --- /dev/null +++ b/shapash/compute/generators/cf_utils.py @@ -0,0 +1,39 @@ +"""Counterfactual utility functions (ports of LIT ``cf_utils``). + +Small, pure helpers shared by generators: detecting a prediction flip and measuring how far a +counterfactual moved the original class probability. +""" + +from __future__ import annotations + +import numpy as np + + +def is_prediction_flip(orig_probs: np.ndarray, cf_probs: np.ndarray, target_class: int | None = None) -> bool: + """Return ``True`` when the counterfactual changes the predicted class. + + Parameters + ---------- + orig_probs : np.ndarray, shape (n_classes,) + Class probabilities for the original text. + cf_probs : np.ndarray, shape (n_classes,) + Class probabilities for the counterfactual text. + target_class : int or None + When given, success requires the argmax to become exactly ``target_class``; otherwise any + change of argmax counts. + + Returns + ------- + bool + Whether the prediction flipped (towards ``target_class`` if specified). + """ + orig_class = int(np.argmax(orig_probs)) + cf_class = int(np.argmax(cf_probs)) + if target_class is not None: + return cf_class == target_class and cf_class != orig_class + return cf_class != orig_class + + +def prediction_difference(orig_probs: np.ndarray, cf_probs: np.ndarray, class_idx: int) -> float: + """Return how much ``class_idx``'s probability dropped from original to counterfactual.""" + return float(orig_probs[class_idx] - cf_probs[class_idx]) diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py new file mode 100644 index 00000000..fb853b2f --- /dev/null +++ b/shapash/compute/generators/hotflip.py @@ -0,0 +1,163 @@ +"""HotFlip counterfactual generator — a port of Google PAIR LIT's ``hotflip.py``. + +HotFlip finds a *minimal* set of token substitutions that changes the model's prediction, using a +single backward pass to estimate each token's impact: + +1. Rank content tokens by the L2 norm of their input-embedding gradient (largest first). +2. For each position, shortlist the top-K **word** candidates by the first-order estimate + (``embedding_matrix @ token_gradient``, most-negative first), then **re-score that shortlist + against the model** and keep the one that most reduces the original-class probability. The linear + estimate alone is an unreliable proxy — its single best pick often does not flip at all — so + model verification is what surfaces genuine substitutions. Sub-word (``##``) and non-word tokens + are excluded so rebuilt text stays well-formed. +3. Try flip combinations of increasing size (1..``max_flips``), rebuild the text, re-predict, and + keep those that flip the prediction — enforcing **minimality** (never return a combination that + is a superset of an already-successful one). + +Requires a model exposing :class:`~shapash.model.base.SupportsGradients` and +:class:`~shapash.model.base.SupportsEmbeddings`. +""" + +from __future__ import annotations + +from itertools import combinations +from typing import Protocol, cast + +import numpy as np + +from shapash.compute.generators.base import ( + Counterfactual, + CounterfactualGenerator, + Field, + IntField, + TokenListField, +) +from shapash.compute.generators.cf_utils import is_prediction_flip +from shapash.model.base import SupportsEmbeddings, SupportsGradients, TextModel, has_capabilities + +_MAX_FLIPPABLE_TOKENS = 10 +_CANDIDATES_PER_POSITION = 50 + + +class _GradientModel(Protocol): + """The exact capability surface HotFlip uses (predict + gradients + embeddings + detokenize).""" + + label_names: list[str] | None + + def predict(self, texts: list[str]) -> np.ndarray: ... + + def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.ndarray]: ... + + def get_embedding_table(self) -> tuple[list[str], np.ndarray]: ... + + def detokenize(self, tokens: list[str]) -> str: ... + + +class HotFlipGenerator(CounterfactualGenerator): + """Gradient-based token-substitution counterfactuals (LIT HotFlip).""" + + name = "hotflip" + + def config_spec(self) -> dict[str, Field]: + """Expose ``num_examples``, ``max_flips`` and ``tokens_to_ignore`` as tunable knobs.""" + return { + "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), + "max_flips": IntField(label="Max token flips", default=3, minimum=1, maximum=5), + "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), + } + + @classmethod + def is_compatible(cls, model: TextModel) -> bool: + """Compatible only with models that expose gradients *and* an embedding table.""" + return has_capabilities(model, SupportsGradients, SupportsEmbeddings) + + def generate(self, text: str, target_label: str | None = None, config: dict | None = None) -> list[Counterfactual]: + """Generate minimal HotFlip counterfactuals for ``text`` (see module docstring).""" + cfg = self.resolve_config(config) + num_examples = int(cfg["num_examples"]) + max_flips = int(cfg["max_flips"]) + ignore = {t.strip().lower() for t in cfg["tokens_to_ignore"]} + + model = cast(_GradientModel, self.model) # is_compatible guarantees these capabilities + label_names = model.label_names or [] + + orig_probs = model.predict([text])[0] + orig_class = int(np.argmax(orig_probs)) + orig_label = label_names[orig_class] if orig_class < len(label_names) else str(orig_class) + target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None + + tokens, grads = model.token_gradients(text, orig_class) + if not tokens: + return [] + + # Rank content tokens by gradient L2 norm; keep the top-K flippable positions. + grad_l2 = np.sum(grads * grads, axis=-1) + ranked = np.argsort(-grad_l2).tolist() + flippable = [p for p in ranked if tokens[p].strip().lower() not in ignore][:_MAX_FLIPPABLE_TOKENS] + + vocab, matrix = model.get_embedding_table() + replacement: dict[int, str] = {} + for pos in flippable: + # Shortlist the top-K word candidates by the first-order estimate (most-negative first)... + scores = matrix @ grads[pos] + shortlist: list[str] = [] + for cand in np.argsort(scores): + cand_tok = vocab[int(cand)] + if cand_tok != tokens[pos] and _is_word_token(cand_tok): + shortlist.append(cand_tok) + if len(shortlist) >= _CANDIDATES_PER_POSITION: + break + if not shortlist: + continue + # ...then re-score the shortlist against the model and keep the one that most reduces the + # original-class probability (the linear estimate's top pick is an unreliable proxy). + cand_texts = [model.detokenize([*tokens[:pos], tok, *tokens[pos + 1 :]]) for tok in shortlist] + cand_orig_probs = model.predict(cand_texts)[:, orig_class] + replacement[pos] = shortlist[int(np.argmin(cand_orig_probs))] + + flippable = [p for p in flippable if p in replacement] + + results: list[Counterfactual] = [] + successful: list[frozenset[int]] = [] + for size in range(1, max_flips + 1): + for combo in combinations(flippable, size): + combo_set = frozenset(combo) + if any(prev <= combo_set for prev in successful): + continue # minimality: skip supersets of a smaller success + new_tokens = list(tokens) + subs: list[tuple[int, str, str]] = [] + for pos in combo: + subs.append((pos, tokens[pos], replacement[pos])) + new_tokens[pos] = replacement[pos] + new_text = model.detokenize(new_tokens) + cf_probs = model.predict([new_text])[0] + if is_prediction_flip(orig_probs, cf_probs, target_class): + cf_class = int(np.argmax(cf_probs)) + results.append( + Counterfactual( + original_text=text, + new_text=new_text, + tokens=list(tokens), + flipped_positions=list(combo), + substitutions=subs, + orig_label=orig_label, + new_label=label_names[cf_class] if cf_class < len(label_names) else str(cf_class), + orig_prob=float(orig_probs[orig_class]), + new_prob=float(cf_probs[cf_class]), + prob_delta=float(orig_probs[orig_class] - cf_probs[orig_class]), + ) + ) + successful.append(combo_set) + if len(results) >= num_examples: + return results + return results + + +def _is_word_token(token: str) -> bool: + """True for plausible standalone word tokens. + + Rejects sub-word continuations (``##ing``), special/placeholder tokens (``[CLS]``), punctuation + and numerics — substituting any of these for a whole word yields malformed text once detokenized. + ``str.isalpha`` handles all of these in one check (``##ing``/``[CLS]``/``1b`` are not alphabetic). + """ + return token.isalpha() From 9fc2aa2591abd458354965b8d18cda1bacecf142 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:31:59 +0200 Subject: [PATCH 031/144] feat: adapt explainer with interactive model --- shapash/explainer/interactive.py | 61 +++++++++++++++++ shapash/explainer/nlp_explainer.py | 101 ++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 shapash/explainer/interactive.py diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py new file mode 100644 index 00000000..46438b53 --- /dev/null +++ b/shapash/explainer/interactive.py @@ -0,0 +1,61 @@ +"""Interactive (live) compute contract for what-if tooling. + +The master refactoring's ``ExplainerView`` is a **read-only** DTO over a *compiled* result — it +deliberately cannot run the model on new inputs. Interactive what-if (re-predicting text the user +edits, generating counterfactuals on the fly) needs the opposite: live access to the model, +explanation backend and generator. + +``InteractiveEngine`` is that live counterpart, kept separate from the read-only view (mirroring LIT +keeping ``Model`` separate from data). Webapp components that mutate/regenerate inputs *require* an +engine and self-disable when one is absent (e.g. an explainer restored from a snapshot, which holds +no model). ``NlpExplainer`` implements this Protocol. + +The capability flags (``can_edit`` / ``can_counterfactual``) let components check exactly what the +bound engine supports, the same way generators check model capabilities. +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from shapash.backend.nlp_backend import NlpContributions +from shapash.compute.generators.base import Counterfactual, Field + + +@runtime_checkable +class InteractiveEngine(Protocol): + """Live compute surface for interactive text what-if components.""" + + def can_edit(self) -> bool: + """Whether live re-prediction / re-explanation of edited text is available.""" + ... + + def can_counterfactual(self) -> bool: + """Whether automated counterfactual generation is available.""" + ... + + def predict(self, text: str) -> tuple[str, dict[str, float]]: + """Return ``(predicted_label, {label: probability})`` for a single text.""" + ... + + def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, float]]: + """Re-explain a single (possibly edited) text. + + Returns + ------- + contributions : NlpContributions + Token-level contributions for the single text (one sample). + label : str + Predicted label. + probabilities : dict[str, float] + Per-class probabilities. + """ + ... + + def generate_counterfactuals(self, text: str, config: dict | None = None) -> list[Counterfactual]: + """Generate counterfactuals for ``text`` using the bound generator.""" + ... + + def cf_config_spec(self) -> dict[str, Field]: + """Return the bound generator's config spec (empty when no generator).""" + ... diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index c839e9ed..a65131c2 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -21,10 +21,19 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field +from shapash.compute.generators.hotflip import HotFlipGenerator +from shapash.model.base import TextModel +from shapash.model.hf import HFPipelineModel from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp +def _looks_like_pipeline(model: object) -> bool: + """Heuristic: a HuggingFace ``text-classification`` pipeline is callable and has a tokenizer.""" + return callable(model) and hasattr(model, "tokenizer") + + def _hash_texts(text_list: list[str]) -> str: h = hashlib.md5(usedforsecurity=False) for t in text_list: @@ -76,17 +85,42 @@ def __init__( model, label_names: list[str] | None = None, backend: NlpBackend | None = None, + cf_generator: CounterfactualGenerator | None = None, explainer_args: dict | None = None, explainer_compute_args: dict | None = None, ) -> None: self.model = model self.label_names = label_names + + # Wrap the model in a capability-aware TextModel adapter when possible. A raw HuggingFace + # pipeline becomes an HFPipelineModel (predict-only); a TextModel is used as-is; anything + # else (e.g. a LIME classifier_fn) leaves _text_model as None and the legacy path is kept. + if isinstance(model, TextModel): + self._text_model: TextModel | None = model + if label_names is None: + self.label_names = model.label_names + elif _looks_like_pipeline(model): + self._text_model = HFPipelineModel(model, label_names) + else: + self._text_model = None + + # The SHAP/LIME backend consumes a plain callable; a TextModel exposes it via shap_callable. + shap_model = self._text_model.shap_callable if self._text_model is not None else model self.backend: NlpBackend = backend or NlpShapBackend( - model=model, - label_names=label_names, + model=shap_model, + label_names=self.label_names, explainer_args=explainer_args or {}, explainer_compute_args=explainer_compute_args or {}, ) + + # Counterfactual generator: explicit > auto-built (HotFlip) when the model is capable > none. + if cf_generator is not None: + self.cf_generator: CounterfactualGenerator | None = cf_generator + elif self._text_model is not None and HotFlipGenerator.is_compatible(self._text_model): + self.cf_generator = HotFlipGenerator(self._text_model) + else: + self.cf_generator = None + self.contributions: NlpContributions | None = None self.texts: pd.Series | None = None self.y_pred: pd.Series | None = None @@ -238,6 +272,8 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: xpl = cls.__new__(cls) xpl.model = None xpl.backend = None + xpl._text_model = None + xpl.cf_generator = None xpl.label_names = state.get("label_names") xpl.texts = state["texts"] xpl.contributions = state["contributions"] @@ -268,10 +304,62 @@ def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1" raise RuntimeError("Call compile() before run_app().") NlpWebApp(self, scatter_xy=scatter_xy).run(port=port, debug=debug, host=host) + # ------------------------------------------------------------------ + # InteractiveEngine — live what-if surface (see explainer/interactive.py) + # ------------------------------------------------------------------ + + def can_edit(self) -> bool: + """Whether edited text can be re-predicted and re-explained live. + + Requires a live model adapter and backend (both are ``None`` after ``from_snapshot``). + """ + return getattr(self, "_text_model", None) is not None and getattr(self, "backend", None) is not None + + def can_counterfactual(self) -> bool: + """Whether a counterfactual generator is bound and ready.""" + return getattr(self, "cf_generator", None) is not None + + def predict(self, text: str) -> tuple[str, dict[str, float]]: + """Predict a single text, returning ``(label, {label: probability})``.""" + text_model = self._require_text_model() + probs = text_model.predict([text])[0] + names = text_model.label_names or self.label_names or [str(i) for i in range(len(probs))] + idx = int(probs.argmax()) + return names[idx], {name: float(p) for name, p in zip(names, probs, strict=False)} + + def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, float]]: + """Re-explain one (possibly edited) text: contributions + prediction + probabilities. + + Bypasses the batch hash cache — this is a single, on-demand explanation of new input. + """ + if getattr(self, "backend", None) is None: + raise RuntimeError("explain_text() requires a live backend (unavailable on a snapshot).") + raw = self.backend.run_explainer([text]) + contributions = self.backend.get_local_contributions([text], raw) + contributions.label_names = self.label_names + label, probabilities = self.predict(text) + return contributions, label, probabilities + + def generate_counterfactuals(self, text: str, config: dict | None = None) -> list[Counterfactual]: + """Generate counterfactuals for ``text`` via the bound generator.""" + if getattr(self, "cf_generator", None) is None: + raise RuntimeError("No counterfactual generator is configured for this explainer.") + return self.cf_generator.generate(text, config=config) + + def cf_config_spec(self) -> dict[str, Field]: + """Return the generator's tunable config spec (empty when no generator).""" + return self.cf_generator.config_spec() if getattr(self, "cf_generator", None) is not None else {} + # ------------------------------------------------------------------ # Private # ------------------------------------------------------------------ + def _require_text_model(self) -> TextModel: + text_model = getattr(self, "_text_model", None) + if text_model is None: + raise RuntimeError("Live prediction requires a TextModel-backed explainer (unavailable on a snapshot).") + return text_model + def _predict(self, text_list: list[str]) -> pd.DataFrame: """Run the pipeline and return a unified DataFrame of predictions and probabilities. @@ -283,6 +371,15 @@ def _predict(self, text_list: list[str]) -> pd.DataFrame: Handles both ``return_all_scores=True`` (list of lists of dicts) and single-prediction (list of dicts) pipeline output formats. """ + text_model = getattr(self, "_text_model", None) + if text_model is not None: + probs = text_model.predict(text_list) + names = text_model.label_names or self.label_names or [str(i) for i in range(probs.shape[1])] + result = pd.DataFrame(probs, index=self.texts.index, columns=list(names)) + labels = [names[int(i)] for i in probs.argmax(axis=1)] + result.insert(0, "prediction", pd.Series(labels, index=self.texts.index)) + return result + raw = self.model(text_list) if raw and isinstance(raw[0], list): labels = [max(preds, key=lambda p: p["score"])["label"] for preds in raw] From 0e64f2bb33f926faeefd73ef95e3baa0dcc81f36 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:33:27 +0200 Subject: [PATCH 032/144] feat: add new webapp components --- shapash/webapp/nlp_components/__init__.py | 33 ++++ shapash/webapp/nlp_components/base.py | 90 +++++++++ .../webapp/nlp_components/counterfactual.py | 180 ++++++++++++++++++ shapash/webapp/nlp_components/data_editor.py | 118 ++++++++++++ shapash/webapp/nlp_components/datapoint.py | 122 ++++++++++++ 5 files changed, 543 insertions(+) create mode 100644 shapash/webapp/nlp_components/__init__.py create mode 100644 shapash/webapp/nlp_components/base.py create mode 100644 shapash/webapp/nlp_components/counterfactual.py create mode 100644 shapash/webapp/nlp_components/data_editor.py create mode 100644 shapash/webapp/nlp_components/datapoint.py diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py new file mode 100644 index 00000000..c4262997 --- /dev/null +++ b/shapash/webapp/nlp_components/__init__.py @@ -0,0 +1,33 @@ +"""Composable webapp components for the NLP what-if tools. + +Prototype of the master plan's Phase-5b ``WebappComponent`` contract, applied first to the +interactive what-if panels (data editor + counterfactuals). Each component owns its layout and +callbacks and declares its ``requires`` capabilities, self-disabling when the bound view/engine +cannot satisfy them. + +Notably this **extends** the Phase-5b ``requires`` idea from *data* capabilities (e.g. +``proba_values``) to *engine/model* capabilities (``engine:predict``, ``engine:counterfactual``), +so a panel can require a live, gradient-capable model — the refinement to fold back into the master +contract. +""" + +from __future__ import annotations + +from shapash.webapp.nlp_components.base import WebappComponent, available_capabilities +from shapash.webapp.nlp_components.counterfactual import CounterfactualComponent +from shapash.webapp.nlp_components.data_editor import DataEditorComponent +from shapash.webapp.nlp_components.datapoint import ( + datapoint_from_contributions, + pack_datapoint, + unpack_datapoint, +) + +__all__ = [ + "WebappComponent", + "available_capabilities", + "DataEditorComponent", + "CounterfactualComponent", + "pack_datapoint", + "unpack_datapoint", + "datapoint_from_contributions", +] diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py new file mode 100644 index 00000000..84f49950 --- /dev/null +++ b/shapash/webapp/nlp_components/base.py @@ -0,0 +1,90 @@ +"""``WebappComponent`` contract + capability resolution for what-if panels. + +A component declares the capabilities it needs via ``requires``; :func:`available_capabilities` +computes what the bound view + engine actually provide, and :meth:`WebappComponent.is_available` +gates mounting on ``requires <= available``. This is the mechanism that makes the What-if Lab +appear only when the explainer holds a live (and, for counterfactuals, gradient-capable) model. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from shapash.explainer.interactive import InteractiveEngine +from shapash.webapp.nlp_view import NlpView + +# Capability tokens components may require. +CAP_PREDICT = "engine:predict" +CAP_COUNTERFACTUAL = "engine:counterfactual" +CAP_GRADIENTS = "model:gradients" + + +def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> frozenset[str]: + """Return the capability tokens the given view + engine satisfy. + + Parameters + ---------- + view : NlpView + Read-only view (data capabilities could be added here later). + engine : InteractiveEngine or None + Live engine, or ``None`` for a snapshot (no live capabilities). + + Returns + ------- + frozenset[str] + Satisfied capability tokens (e.g. ``{"engine:predict", "engine:counterfactual", + "model:gradients"}``). + """ + caps: set[str] = set() + if engine is not None: + if engine.can_edit(): + caps.add(CAP_PREDICT) + if engine.can_counterfactual(): + caps.add(CAP_COUNTERFACTUAL) + caps.add(CAP_GRADIENTS) # the auto-built generator (HotFlip) is gradient-based + return frozenset(caps) + + +class WebappComponent(ABC): + """Base class for a self-contained, registrable webapp panel. + + Subclasses set ``id``/``name``/``scope``/``requires`` and implement :meth:`layout` and + :meth:`register_callbacks`. All Dash ids a component creates must be namespaced with its ``id`` + to avoid collisions. + """ + + id: str = "component" + name: str = "Component" + scope: str = "local" # "global" | "local" + requires: frozenset[str] = frozenset() + + @classmethod + def is_available(cls, view: NlpView, engine: InteractiveEngine | None) -> bool: + """Whether the component's ``requires`` are satisfied by the view + engine.""" + return cls.requires <= available_capabilities(view, engine) + + @abstractmethod + def layout(self, view: NlpView, engine: InteractiveEngine | None = None): + """Return the Dash layout for this component. + + ``engine`` is provided for components whose initial UI depends on live capabilities — e.g. the + counterfactual panel renders its config controls from the generator's spec, so they must exist + in the initial layout rather than be injected by a later callback. + """ + + @abstractmethod + def register_callbacks(self, app, view: NlpView, engine: InteractiveEngine, stores: dict) -> None: + """Register this component's Dash callbacks. + + Parameters + ---------- + app : dash.Dash + The Dash application. + view : NlpView + Read-only data accessor. + engine : InteractiveEngine + Live engine for prediction / counterfactual generation. + stores : dict + Shared ``dcc.Store`` ids the What-if Lab wires between components + (e.g. ``{"apply": "whatif-apply-store"}``). + """ diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py new file mode 100644 index 00000000..2815fc56 --- /dev/null +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -0,0 +1,180 @@ +"""Counterfactual component: generate what-if flips and apply them into the editor. + +Controls are rendered automatically from the bound generator's ``config_spec()`` (so a new knob or a +new generator needs no UI change). Generated counterfactuals are shown as a table; an *Apply* button +publishes the chosen text to a shared store the data editor subscribes to. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +from dash import ALL, Input, Output, State, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.compute.generators.base import Field, IntField, TokenListField +from shapash.webapp.nlp_components.base import CAP_COUNTERFACTUAL, CAP_PREDICT, WebappComponent +from shapash.webapp.nlp_components.datapoint import datapoint_from_contributions + +_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} + + +class CounterfactualComponent(WebappComponent): + """Spec-driven counterfactual generation panel.""" + + id = "counterfactual" + name = "Counterfactual Suggestions" + scope = "local" + # Requires the editor too (it reads the editor's text and applies flips back into it). + requires = frozenset({CAP_COUNTERFACTUAL, CAP_PREDICT}) + + def _config_fields(self, engine) -> list[tuple[str, Field]]: + """Ordered ``(name, field)`` pairs from the engine's config spec.""" + return list(engine.cf_config_spec().items()) + + def _config_controls(self, engine) -> list: + """Render one control per config field (IntField → number, TokenListField → text).""" + rows = [] + for name, fld in self._config_fields(engine): + cid = f"{self.id}-cfg-{name}" + if isinstance(fld, IntField): + control = dcc.Input( + id=cid, + type="number", + min=fld.minimum, + max=fld.maximum, + step=1, + value=fld.default, + style={"width": "90px"}, + ) + elif isinstance(fld, TokenListField): + control = dbc.Input( + id=cid, + type="text", + value=",".join(fld.default), + placeholder="comma,separated", + style={"width": "180px"}, + ) + else: # pragma: no cover - future field types + control = dbc.Input(id=cid, type="text", value=str(fld.default)) + rows.append( + dbc.Row( + [ + dbc.Col(html.Label(fld.label, className="small fw-bold mb-0"), width="auto"), + dbc.Col(control, width="auto"), + ], + className="align-items-center mb-2 g-2", + ) + ) + return rows + + def layout(self, view, engine=None) -> html.Div: + """Return the counterfactual card with config controls rendered from the generator spec. + + The controls are built here (not injected by a callback) so their ids exist in the initial + layout — otherwise the ``generate`` callback's ``State`` references a not-yet-created object. + """ + controls = self._config_controls(engine) if engine is not None else [] + return html.Div( + [ + html.H6("Counterfactual Suggestions", className="fw-bold mb-2"), + html.Small( + "Find minimal token substitutions that flip the prediction of the edited text.", + className="text-muted d-block mb-2", + ), + html.Div(controls, id=f"{self.id}-controls"), + dbc.Button("Generate", id=f"{self.id}-generate-btn", color="secondary", size="sm", className="mb-2"), + dcc.Loading(html.Div(id=f"{self.id}-results")), + dcc.Store(id=f"{self.id}-store", data=[]), + ], + style=_CARD_STYLE, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Wire control rendering, Generate, and per-row Apply (→ shared editor store).""" + apply_store = stores["apply"] + current_store = stores["current"] + fields = self._config_fields(engine) + + # Config controls are rendered in `layout()` (they must be in the initial layout), so no + # separate render callback is needed here. + config_states = [State(f"{self.id}-cfg-{name}", "value") for name, _ in fields] + + # Reads the current datapoint (a selected dataset row *or* an edited/predicted text) so + # counterfactuals can be generated for both, not only hand-typed editor text. + @app.callback( + Output(f"{self.id}-results", "children"), + Output(f"{self.id}-store", "data"), + Input(f"{self.id}-generate-btn", "n_clicks"), + State(current_store, "data"), + *config_states, + ) + def generate(n_clicks, datapoint, *config_values): + text = (datapoint or {}).get("text", "") + if not n_clicks or not text or not text.strip(): + raise PreventUpdate + config = {} + for (name, fld), value in zip(fields, config_values, strict=True): + if isinstance(fld, IntField): + config[name] = int(value) if value is not None else fld.default + elif isinstance(fld, TokenListField): + config[name] = [t.strip() for t in (value or "").split(",") if t.strip()] + else: # pragma: no cover + config[name] = value + cfs = engine.generate_counterfactuals(text, config=config) + if not cfs: + return html.Div("No counterfactual found within these limits.", className="text-muted"), [] + return _results_table(cfs, self.id), [cf.new_text for cf in cfs] + + # "Inspect" loads the chosen counterfactual into the editor (apply_store → textarea) *and* + # makes it the current datapoint, so its token contributions render immediately in the shared + # Sentence/Waterfall panels without a separate Predict click. + @app.callback( + Output(apply_store, "data"), + Output(current_store, "data", allow_duplicate=True), + Input({"type": f"{self.id}-apply", "index": ALL}, "n_clicks"), + State(f"{self.id}-store", "data"), + prevent_initial_call=True, + ) + def apply(n_clicks_list, cf_texts): + if not any(n_clicks_list or []): + raise PreventUpdate + triggered = callback_context.triggered_id + if not triggered: + raise PreventUpdate + index = triggered["index"] + if not cf_texts or not (0 <= index < len(cf_texts)): + raise PreventUpdate + text = cf_texts[index] + contributions, label, _probs = engine.explain_text(text) + return text, datapoint_from_contributions(text, contributions, label) + + +def _results_table(cfs, component_id: str): + """Render counterfactuals as a table with per-row Apply buttons.""" + header = html.Thead( + html.Tr([html.Th("→ Label"), html.Th("Δ prob"), html.Th("Substitutions"), html.Th("Text"), html.Th("")]) + ) + rows = [] + for i, cf in enumerate(cfs): + subs = ", ".join(f"{old}→{new}" for _, old, new in cf.substitutions) + rows.append( + html.Tr( + [ + html.Td(html.B(cf.new_label)), + html.Td(f"{cf.prob_delta:.2f}"), + html.Td(subs, style={"fontSize": "0.85em"}), + html.Td(cf.new_text, style={"fontSize": "0.85em"}), + html.Td( + dbc.Button( + "Inspect", + id={"type": f"{component_id}-apply", "index": i}, + color="link", + size="sm", + className="p-0", + title="Load into the editor and show its token contributions on the right", + ) + ), + ] + ) + ) + return dbc.Table([header, html.Tbody(rows)], bordered=False, hover=True, size="sm", className="mb-0") diff --git a/shapash/webapp/nlp_components/data_editor.py b/shapash/webapp/nlp_components/data_editor.py new file mode 100644 index 00000000..9e30ce30 --- /dev/null +++ b/shapash/webapp/nlp_components/data_editor.py @@ -0,0 +1,118 @@ +"""Data-editor component: edit a sample's text and see a live prediction + token highlight. + +Prefills from the selected table row (or an applied counterfactual), re-predicts and re-explains the +edited text on demand through the :class:`~shapash.explainer.interactive.InteractiveEngine`, and +renders the result with the existing pure ``plot_sentence_highlight`` renderer. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +import plotly.graph_objs as go +from dash import Input, Output, State, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.webapp.nlp_components.base import CAP_PREDICT, WebappComponent +from shapash.webapp.nlp_components.datapoint import datapoint_from_contributions + +_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} + + +def _prob_bar(probs: dict[str, float]) -> go.Figure: + """Horizontal bar of class probabilities, highest on top.""" + items = sorted(probs.items(), key=lambda kv: kv[1]) + labels = [k for k, _ in items] + values = [v for _, v in items] + fig = go.Figure( + go.Bar( + x=values, + y=labels, + orientation="h", + marker_color="#1f77b4", + text=[f"{v:.2f}" for v in values], + textposition="auto", + ) + ) + fig.update_layout( + margin=dict(l=10, r=10, t=6, b=6), + height=max(120, 26 * len(labels)), + xaxis=dict(range=[0, 1], showgrid=True, gridcolor="#eee"), + plot_bgcolor="white", + paper_bgcolor="white", + ) + return fig + + +class DataEditorComponent(WebappComponent): + """Editable text box with live re-prediction and token-contribution highlight.""" + + id = "data-editor" + name = "Data Editor" + scope = "local" + requires = frozenset({CAP_PREDICT}) + + def layout(self, view, engine=None) -> html.Div: + """Return the editor card: textarea, Predict button, class-probability bar. + + Token contributions for the edited text are shown by the shared lower-right panel (it reads the + ``current-datapoint`` store this component writes on Predict), so they are not duplicated here. + """ + return html.Div( + [ + html.Small( + "Edit the text below and click Predict. " + "Token contributions appear in the Sentence panel on the right.", + className="text-muted d-block mb-2", + ), + dcc.Textarea( + id=f"{self.id}-input", + style={"width": "100%", "height": "90px", "fontSize": "0.95em"}, + ), + dbc.Button("Predict", id=f"{self.id}-predict-btn", color="primary", size="sm", className="mt-2"), + # Hidden until the first Predict, so an empty plot area is not shown on tab open. + html.Div( + [ + html.Hr(style={"margin": "12px 0 8px"}), + dcc.Graph(id=f"{self.id}-prob", config={"displayModeBar": False}, style={"height": "150px"}), + ], + id=f"{self.id}-result", + style={"display": "none"}, + ), + ], + style=_CARD_STYLE, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Wire prefill (row/apply → textarea) and Predict (textarea → prediction + current datapoint).""" + apply_store = stores["apply"] + current_store = stores["current"] + + @app.callback( + Output(f"{self.id}-input", "value"), + Input("dataset-table", "selectedRows"), + Input(apply_store, "data"), + ) + def prefill(selected_rows, applied_text): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if apply_store in trigger and applied_text: + return applied_text + if selected_rows: + return selected_rows[0].get("text", "") + raise PreventUpdate + + # Predict publishes the edited text as the *current datapoint* (a scratch point with + # ``orig_idx=None``), so the shared highlight/waterfall/counterfactual panels all update to + # reflect it. The editor only renders the class-probability bar locally. + @app.callback( + Output(f"{self.id}-prob", "figure"), + Output(f"{self.id}-result", "style"), + Output(current_store, "data", allow_duplicate=True), + Input(f"{self.id}-predict-btn", "n_clicks"), + State(f"{self.id}-input", "value"), + prevent_initial_call=True, + ) + def predict(n_clicks, text): + if not n_clicks or not text or not text.strip(): + raise PreventUpdate + contributions, label, probs = engine.explain_text(text) + return _prob_bar(probs), {"display": "block"}, datapoint_from_contributions(text, contributions, label) diff --git a/shapash/webapp/nlp_components/datapoint.py b/shapash/webapp/nlp_components/datapoint.py new file mode 100644 index 00000000..46147e62 --- /dev/null +++ b/shapash/webapp/nlp_components/datapoint.py @@ -0,0 +1,122 @@ +"""Serializable "current datapoint" payload shared across the NLP webapp panels. + +A single ``dcc.Store(id="current-datapoint")`` is the app's *primary selection* (LIT's term): the one +text every per-instance panel points at. It is written from two sources — a selected dataset row +(precomputed contributions) or the data editor's live ``explain_text`` on a scratch text — and read by +the sentence-highlight, waterfall, and counterfactual panels. Keeping both writers and all readers on a +single JSON-serialisable shape is what lets "select a row" and "edit + predict a new text" flow through +the same downstream code. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np + + +def pack_datapoint( + *, + text: str, + orig_idx: int | None, + tokens: list[str], + values: Any, + base_values: Any, + label: str | None = None, +) -> dict: + """Serialise one sample into the ``current-datapoint`` store shape. + + Parameters + ---------- + text : str + The raw text of the datapoint. + orig_idx : int or None + Row index in the compiled batch, or ``None`` for a scratch (editor) datapoint that has no + precomputed row. + tokens : list of str + Token strings for the sample. + values : array-like + Per-token SHAP values, shape ``(n_tokens,)`` or ``(n_tokens, n_classes)``. Stored whole so the + detail panels can re-slice by the active class without re-selecting. + base_values : array-like or None + Per-class base value(s) for the sample, or ``None``. + label : str or None, optional + Predicted label, when known. + + Returns + ------- + dict + JSON-serialisable payload with keys ``text``, ``orig_idx``, ``tokens``, ``values``, + ``base_values``, ``label``. + """ + values_arr = np.asarray(values, dtype=float) + base_list = None if base_values is None else np.asarray(base_values, dtype=float).reshape(-1).tolist() + return { + "text": text, + "orig_idx": orig_idx, + "tokens": list(tokens), + "values": values_arr.tolist(), + "base_values": base_list, + "label": label, + } + + +def datapoint_from_contributions(text: str, contributions: Any, label: str | None = None) -> dict: + """Pack a scratch datapoint from a freshly computed single-text explanation. + + Shared by the data editor (Predict) and the counterfactual panel (Apply): both call + ``engine.explain_text`` and turn its ``NlpContributions`` (batch of one) into the store shape. + + Parameters + ---------- + text : str + The explained text. + contributions : NlpContributions + The result of ``engine.explain_text(text)`` — a one-sample batch. + label : str or None, optional + Predicted label, when known. + + Returns + ------- + dict + A :func:`pack_datapoint` payload with ``orig_idx=None`` (scratch). + """ + base_values = contributions.base_values + return pack_datapoint( + text=text, + orig_idx=None, + tokens=contributions.token_strings[0], + values=contributions.values[0], + base_values=(base_values[0] if base_values is not None else None), + label=label, + ) + + +def unpack_datapoint(dp: dict, label_idx: int) -> tuple[list[str], np.ndarray, float | None, str | None]: + """Slice a stored datapoint down to one class for rendering. + + Parameters + ---------- + dp : dict + A payload produced by :func:`pack_datapoint`. + label_idx : int + Index of the class to render. + + Returns + ------- + tokens : list of str + values : numpy.ndarray + 1-D per-token values for ``label_idx``. + base_value : float or None + Base value for ``label_idx`` (falls back to the first entry when there is a single scalar). + label : str or None + The stored predicted label, if any. + """ + tokens = dp["tokens"] + values = np.asarray(dp["values"], dtype=float) + vals = values[:, label_idx] if values.ndim == 2 else values + base_list = dp.get("base_values") + base_value: float | None = None + if base_list: + base_value = float(base_list[label_idx]) if label_idx < len(base_list) else float(base_list[0]) + return tokens, vals, base_value, dp.get("label") From 7dc007aa4197c533d1b7cc652f142c258ca103f3 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:34:26 +0200 Subject: [PATCH 033/144] feat: add readonly nlp view --- shapash/webapp/nlp_view.py | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 shapash/webapp/nlp_view.py diff --git a/shapash/webapp/nlp_view.py b/shapash/webapp/nlp_view.py new file mode 100644 index 00000000..75922086 --- /dev/null +++ b/shapash/webapp/nlp_view.py @@ -0,0 +1,66 @@ +"""Read-only view over a compiled ``NlpExplainer`` for webapp components. + +Prototype of the master plan's ``ExplainerView`` (Phase 5) for the text modality: webapp components +read compiled explanation data **only** through this contract, never by reaching into explainer +internals. It is deliberately read-only — live actions (re-predicting edited text, generating +counterfactuals) live on the separate :class:`~shapash.explainer.interactive.InteractiveEngine`. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd + + +class NlpView: + """Read-only accessor over a compiled ``NlpExplainer``. + + Parameters + ---------- + explainer : NlpExplainer + A compiled explainer (``compile()`` already called). + """ + + def __init__(self, explainer) -> None: + self._explainer = explainer + + @property + def texts(self) -> pd.Series | None: + """The compiled input texts.""" + return self._explainer.texts + + @property + def contributions(self): + """The compiled ``NlpContributions``.""" + return self._explainer.contributions + + @property + def y_pred(self) -> pd.Series | None: + """Predicted labels for the compiled batch.""" + return self._explainer.y_pred + + @property + def y_prob(self) -> pd.DataFrame | None: + """Per-class probabilities for the compiled batch, if available.""" + return getattr(self._explainer, "y_prob", None) + + @property + def y_true(self) -> pd.Series | None: + """Ground-truth labels, if provided at compile time.""" + return getattr(self._explainer, "y_true", None) + + @property + def label_names(self) -> list[str] | None: + """Class names in output-column order.""" + return self._explainer.label_names or (self.contributions.label_names if self.contributions else None) + + @property + def has_ground_truth(self) -> bool: + """Whether ground-truth labels are available.""" + return self.y_true is not None + + @property + def n_classes(self) -> int: + """Number of classes inferred from the compiled contributions.""" + sample = self.contributions.values[0] + return sample.shape[1] if np.ndim(sample) == 2 else 1 From e5c1140fe9f150cbe46552c7bb15b8408e206ee5 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:35:35 +0200 Subject: [PATCH 034/144] feat: add model abstraction layer --- shapash/model/__init__.py | 37 ++++++ shapash/model/base.py | 128 +++++++++++++++++++++ shapash/model/hf.py | 234 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 shapash/model/__init__.py create mode 100644 shapash/model/base.py create mode 100644 shapash/model/hf.py diff --git a/shapash/model/__init__.py b/shapash/model/__init__.py new file mode 100644 index 00000000..3a8e2d17 --- /dev/null +++ b/shapash/model/__init__.py @@ -0,0 +1,37 @@ +"""Model capability layer — modality-agnostic adapters for the shapash engine. + +This package introduces a first-class ``Model`` abstraction (a peer to the planned +``shapash/data/`` layer). Explanation backends and counterfactual generators depend on +*declared capabilities* rather than a concrete model type, so a component can call +``is_compatible(model)`` and only run when the required capabilities are present. + +The design deliberately adopts the *intent* of Google PAIR LIT's ``Model`` API — components +stay model-agnostic by introspecting what a model can do — but expresses it Pythonically with +abstract base classes and capability mixins (``SupportsTokenization`` / ``SupportsEmbeddings`` / +``SupportsGradients``) instead of LIT's runtime ``JsonDict``/``LitType`` specs. This matches the +typed-dataclass style already used elsewhere in shapash (e.g. ``NlpContributions``). + +Currently ships text adapters for HuggingFace models. ``SentenceTransformerModel`` and +``TorchModel`` are documented extension points (see ``hf.py`` roadmap notes). +""" + +from __future__ import annotations + +from shapash.model.base import ( + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, + has_capabilities, +) +from shapash.model.hf import HFClassifierModel, HFPipelineModel + +__all__ = [ + "TextModel", + "SupportsTokenization", + "SupportsEmbeddings", + "SupportsGradients", + "has_capabilities", + "HFPipelineModel", + "HFClassifierModel", +] diff --git a/shapash/model/base.py b/shapash/model/base.py new file mode 100644 index 00000000..8762a627 --- /dev/null +++ b/shapash/model/base.py @@ -0,0 +1,128 @@ +"""Capability-based model interface for text explainability. + +``TextModel`` is the minimal contract every adapter implements: turn a list of strings into a +probability matrix. Optional *capabilities* are expressed as separate mixin ABCs so that a +component can require exactly what it needs and no more: + +* ``SupportsTokenization`` — split text into tokens and rebuild text from tokens. +* ``SupportsEmbeddings`` — expose the input-embedding table and mean-pooled sentence embeddings. +* ``SupportsGradients`` — expose per-token gradients of a target-class logit. + +A prediction-only model (e.g. a HuggingFace pipeline) implements only ``TextModel``; a raw +classifier with tokenizer access implements all three. Generators check compatibility with +``has_capabilities(model, SupportsGradients, SupportsEmbeddings)`` rather than isinstance-ing a +concrete class — this is what keeps HotFlip and friends model-agnostic. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + +import numpy as np + + +class TextModel(ABC): + """Abstract text-classification model — the minimal capability every adapter provides. + + Attributes + ---------- + label_names : list[str] or None + Human-readable class names in the same column order as ``predict`` output. + task : str + Task kind; only ``"classification"`` is supported for now. + """ + + task: str = "classification" + + def __init__(self, label_names: list[str] | None = None) -> None: + self.label_names = label_names + + @abstractmethod + def predict(self, texts: list[str]) -> np.ndarray: + """Return class probabilities for each input text. + + Parameters + ---------- + texts : list[str] + Raw input strings. + + Returns + ------- + np.ndarray, shape (n_texts, n_classes) + Row-wise class probabilities. + """ + + @property + def n_classes(self) -> int | None: + """Number of classes, or ``None`` when unknown before the first prediction.""" + return len(self.label_names) if self.label_names is not None else None + + +class SupportsTokenization(ABC): + """Capability: split text into tokens and rebuild text from tokens.""" + + @abstractmethod + def tokenize(self, text: str) -> list[str]: + """Return the token strings for a single text (model's own tokenization).""" + + @abstractmethod + def detokenize(self, tokens: list[str]) -> str: + """Rebuild a display string from a list of tokens.""" + + +class SupportsEmbeddings(ABC): + """Capability: expose the input-embedding table and mean-pooled sentence embeddings.""" + + @abstractmethod + def get_embedding_table(self) -> tuple[list[str], np.ndarray]: + """Return ``(vocab, matrix)`` where ``matrix`` has shape ``(vocab_size, hidden_dim)``.""" + + @abstractmethod + def embed(self, texts: list[str]) -> np.ndarray: + """Return one dense vector per text, shape ``(n_texts, hidden_dim)``.""" + + +class SupportsGradients(ABC): + """Capability: per-token gradients of a target-class logit w.r.t. input embeddings.""" + + @abstractmethod + def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.ndarray]: + """Return ``(tokens, grads)`` for one text and target class. + + Parameters + ---------- + text : str + Input string. + target_class : int + Class index whose logit is differentiated. + + Returns + ------- + tokens : list[str] + Tokens aligned with the gradient rows (special tokens excluded). + grads : np.ndarray, shape (n_tokens, hidden_dim) + Gradient of the target-class logit w.r.t. each token's input embedding. + """ + + +def has_capabilities(model: object, *capabilities: type) -> bool: + """Return ``True`` when ``model`` is an instance of every capability ABC given. + + Parameters + ---------- + model : object + The model adapter to test. + *capabilities : type + Capability ABCs (e.g. ``SupportsGradients``) the model must satisfy. + + Returns + ------- + bool + ``True`` only if ``model`` inherits from all requested capabilities. + + Examples + -------- + >>> has_capabilities(model, SupportsGradients, SupportsEmbeddings) + True + """ + return all(isinstance(model, cap) for cap in capabilities) diff --git a/shapash/model/hf.py b/shapash/model/hf.py new file mode 100644 index 00000000..111cbb2e --- /dev/null +++ b/shapash/model/hf.py @@ -0,0 +1,234 @@ +"""HuggingFace text-model adapters. + +Two concrete :class:`~shapash.model.base.TextModel` implementations: + +* :class:`HFPipelineModel` — wraps a ``transformers.pipeline`` and exposes **prediction only** + (plus tokenization). This is the backward-compatible default: everything the existing prototype + passes to ``NlpExplainer`` is a pipeline, so it is wrapped here transparently. +* :class:`HFClassifierModel` — wraps a raw ``AutoModelForSequenceClassification`` + tokenizer and + implements **all** capabilities (tokenization, embeddings, gradients), which gradient-based + counterfactual methods such as HotFlip need. + +Roadmap (documented extension points, not implemented): ``SentenceTransformerModel`` (encoder + +classifier head) and ``TorchModel`` (hand-rolled ``nn.Module``) would subclass ``TextModel`` and the +relevant capability mixins the same way — generators and the webapp need no changes because they +depend on capabilities, not concrete classes. +""" + +from __future__ import annotations + +import numpy as np + +from shapash._optional import import_optional_module +from shapash.model.base import ( + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, +) + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' + + +def _probs_from_pipeline_output(raw: list, order: list[str]) -> np.ndarray: + """Convert ``pipeline`` output to a probability matrix aligned to ``order``. + + Handles both ``top_k=None`` output (list of per-class dict lists) and single-prediction output + (list of dicts). Aligning by label name makes the result robust to per-row score sorting. + """ + rows = raw if raw and isinstance(raw[0], list) else [[r] for r in raw] + matrix = np.zeros((len(rows), len(order)), dtype=np.float64) + for i, row in enumerate(rows): + scores = {d["label"]: float(d["score"]) for d in row} + for j, label in enumerate(order): + matrix[i, j] = scores.get(label, 0.0) + return matrix + + +class HFPipelineModel(TextModel, SupportsTokenization): + """Prediction-only adapter over a HuggingFace ``text-classification`` pipeline. + + Parameters + ---------- + pipeline : transformers.Pipeline + A ``text-classification`` pipeline. ``top_k=None`` is requested at call time so all class + scores are returned. + label_names : list[str] or None + Class names in output-column order. When ``None``, inferred from the pipeline's first + prediction (the labels it emits). + """ + + def __init__(self, pipeline, label_names: list[str] | None = None) -> None: + super().__init__(label_names=label_names) + self.pipeline = pipeline + self.tokenizer = getattr(pipeline, "tokenizer", None) + self._order: list[str] | None = list(label_names) if label_names else None + + @property + def shap_callable(self): + """Callable SHAP's text explainer can wrap directly (the pipeline itself).""" + return self.pipeline + + def predict(self, texts: list[str]) -> np.ndarray: + """Return ``(n_texts, n_classes)`` probabilities via the pipeline.""" + raw = self.pipeline(list(texts), top_k=None) + if self._order is None: + first = raw[0] if raw and isinstance(raw[0], list) else raw + labels = [d["label"] for d in first] + self._order = ( + list(self.label_names) if (self.label_names and set(self.label_names) <= set(labels)) else labels + ) + if self.label_names is None: + self.label_names = list(self._order) + return _probs_from_pipeline_output(raw, self._order) + + def tokenize(self, text: str) -> list[str]: + """Tokenize with the pipeline's tokenizer.""" + if self.tokenizer is None: + raise RuntimeError("Pipeline has no tokenizer; cannot tokenize.") + return self.tokenizer.tokenize(text) + + def detokenize(self, tokens: list[str]) -> str: + """Rebuild a display string from tokens.""" + if self.tokenizer is None: + raise RuntimeError("Pipeline has no tokenizer; cannot detokenize.") + return self.tokenizer.convert_tokens_to_string(tokens) + + +class HFClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): + """Full-capability adapter over a raw ``AutoModelForSequenceClassification`` + tokenizer. + + Provides prediction, tokenization, the input-embedding table, mean-pooled sentence embeddings, + and per-token gradients — everything gradient-based counterfactual generators require. + + Parameters + ---------- + classifier : transformers.PreTrainedModel + A sequence-classification model whose ``forward`` accepts ``inputs_embeds`` and supports + ``output_hidden_states=True``. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer matching the classifier. + label_names : list[str] or None + Class names in class-index (id) order. When ``None``, read from ``classifier.config.id2label``. + batch_size : int, optional + Batch size for ``predict`` and ``embed``. Default 32. + """ + + def __init__(self, classifier, tokenizer, label_names: list[str] | None = None, batch_size: int = 32) -> None: + if label_names is None: + id2label = getattr(getattr(classifier, "config", None), "id2label", None) + if id2label: + label_names = [id2label[i] for i in sorted(id2label)] + super().__init__(label_names=label_names) + self.classifier = classifier + self.tokenizer = tokenizer + self.batch_size = batch_size + self._pipeline = None # lazily built for SHAP + + # ------------------------------------------------------------------ + # Prediction + # ------------------------------------------------------------------ + + def predict(self, texts: list[str]) -> np.ndarray: + """Return ``(n_texts, n_classes)`` softmax probabilities in class-index order.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + device = self.classifier.device + texts = list(texts) + out = [] + for i in range(0, len(texts), self.batch_size): + batch = self.tokenizer(texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt") + batch = {k: v.to(device) for k, v in batch.items()} + with torch.no_grad(): + logits = self.classifier(**batch).logits + probs = torch.softmax(logits, dim=-1) + out.append(probs.cpu().numpy()) + return np.vstack(out) + + @property + def shap_callable(self): + """A ``text-classification`` pipeline built from the classifier, for SHAP's TextMasker.""" + if self._pipeline is None: + transformers = import_optional_module("transformers", extra=_NLP_EXTRA) + self._pipeline = transformers.pipeline( + "text-classification", + model=self.classifier, + tokenizer=self.tokenizer, + top_k=None, + ) + return self._pipeline + + # ------------------------------------------------------------------ + # Tokenization + # ------------------------------------------------------------------ + + def tokenize(self, text: str) -> list[str]: + """Return the classifier tokenizer's tokens for ``text``.""" + return self.tokenizer.tokenize(text) + + def detokenize(self, tokens: list[str]) -> str: + """Rebuild a display string from tokens (merges ``##`` word-pieces).""" + return self.tokenizer.convert_tokens_to_string(tokens) + + # ------------------------------------------------------------------ + # Embeddings + # ------------------------------------------------------------------ + + def get_embedding_table(self) -> tuple[list[str], np.ndarray]: + """Return ``(vocab, matrix)`` from the model's input-embedding layer.""" + weight = self.classifier.get_input_embeddings().weight + matrix = weight.detach().cpu().numpy() + vocab = self.tokenizer.convert_ids_to_tokens(range(matrix.shape[0])) + return vocab, matrix + + def embed(self, texts: list[str]) -> np.ndarray: + """Return mean-pooled last-layer hidden states, shape ``(n_texts, hidden_dim)``. + + Padding tokens are excluded from the mean via the attention mask. + """ + torch = import_optional_module("torch", extra=_NLP_EXTRA) + device = self.classifier.device + texts = list(texts) + out = [] + for i in range(0, len(texts), self.batch_size): + batch = self.tokenizer(texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt") + batch = {k: v.to(device) for k, v in batch.items()} + with torch.no_grad(): + hidden = self.classifier(**batch, output_hidden_states=True).hidden_states[-1] + mask = batch["attention_mask"].unsqueeze(-1) + emb = (hidden * mask).sum(1) / mask.sum(1) + out.append(emb.cpu().numpy()) + return np.vstack(out) + + # ------------------------------------------------------------------ + # Gradients + # ------------------------------------------------------------------ + + def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.ndarray]: + """Return ``(tokens, grads)`` of the target-class logit w.r.t. input embeddings. + + Special tokens (``[CLS]``/``[SEP]``/padding) are dropped so the returned tokens and gradient + rows are aligned to substitutable content tokens only. + """ + device = self.classifier.device # torch is already present (classifier is a torch model) + enc = self.tokenizer(text, truncation=True, return_tensors="pt") + input_ids = enc["input_ids"].to(device) + attention_mask = enc["attention_mask"].to(device) + + embed_layer = self.classifier.get_input_embeddings() + inputs_embeds = embed_layer(input_ids) + inputs_embeds.requires_grad_(True) + inputs_embeds.retain_grad() + + self.classifier.zero_grad() + logits = self.classifier(inputs_embeds=inputs_embeds, attention_mask=attention_mask).logits + logits[0, target_class].backward() + + grads = inputs_embeds.grad[0].detach().cpu().numpy() # (seq, hidden) + ids = input_ids[0].tolist() + tokens = self.tokenizer.convert_ids_to_tokens(ids) + special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) + + keep = [i for i, is_special in enumerate(special_mask) if not is_special] + kept_tokens = [tokens[i] for i in keep] + kept_grads = grads[keep] + return kept_tokens, kept_grads From 06973aa5433cd9843b00cd4211bb381e2aafe52d Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:37:41 +0200 Subject: [PATCH 035/144] feat: add serving for the extended webapp --- demo/serve_nlp.py | 2 +- demo/serve_nlp_ext.py | 145 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 demo/serve_nlp_ext.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index cc290352..4df32e27 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -16,7 +16,7 @@ from shapash.explainer.nlp_explainer import NlpExplainer -snapshot = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("explainer_snapshot.pkl") +snapshot = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent / "explainer_snapshot.pkl" if not snapshot.exists(): raise FileNotFoundError(f"Snapshot not found: {snapshot}\nRun demo/test_webapp_nlp.py first to generate it.") diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py new file mode 100644 index 00000000..b771ab55 --- /dev/null +++ b/demo/serve_nlp_ext.py @@ -0,0 +1,145 @@ +"""Serve the NLP explainer **with the What-if Lab** (live model, cached compute). + +Unlike ``serve_nlp.py``, the What-if Lab needs a live model in memory — editing a +sample, re-predicting, and generating counterfactuals all call the real model, so a +model-free snapshot (``from_snapshot``) makes the lab self-disable. There is therefore +no way to serve what-if without loading the model. + +What *can* be avoided on every restart is recomputing the expensive stuff around the +model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): + +* **SHAP contributions + predictions** — via the built-in ``compile(cache_dir=...)``. +* **The 2-D PaCMAP projection** — via a small ``.proj.npy`` written here (the + library deliberately does not own a projection method). + +The caching is automatic: each run **loads** the cache if one exists for these exact +texts, otherwise it **computes and writes** it. So the first run is slow; every run after +pays only the model load (a few seconds on CPU). Pass ``--recompute`` to force a fresh +compute (e.g. after changing the explainer). Iterating on the webapp itself? Don't +restart at all — keep this in a REPL and re-call ``run_app`` after reloading the webapp +modules; the model stays resident. + +The model runs on CPU when no GPU is present, so this serves fine on a plain box. + +Usage +----- + python demo/serve_nlp_ext.py [--n 100] [--cache-dir demo/nlp_ext_cache] [--port 8051] + python demo/serve_nlp_ext.py --recompute # ignore the cache and recompute + +First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). +""" + +from __future__ import annotations + +import argparse +import logging +from pathlib import Path + +import datasets +import numpy as np +import pacmap +import torch +import transformers + +from shapash.compute.generators import HotFlipGenerator +from shapash.explainer.nlp_explainer import ( + NlpExplainer, + _hash_texts, # same keying as the compile cache +) +from shapash.model import HFClassifierModel + +_HERE = Path(__file__).parent +_MODEL_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" +_LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] + +logger = logging.getLogger("serve_nlp_ext") + + +def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> np.ndarray: + """Return a 2-D PaCMAP projection, reading from / writing to ``cache_dir``. + + Cached alongside the ``compile`` results under the same text hash so a single + ``cache_dir`` holds everything the app needs after the first run. + """ + cache_file = cache_dir / f"{_hash_texts(list(sentences))}.proj.npy" + if cache_file.exists(): + logger.info("Projection cache hit — loading %s", cache_file) + return np.load(cache_file) + + logger.info("Projection cache miss — computing PaCMAP projection (embedding %d texts)", len(sentences)) + projector = pacmap.PaCMAP(n_components=n_components, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) + projected = projector.fit_transform(model.embed(sentences), init="pca") + + cache_dir.mkdir(parents=True, exist_ok=True) + np.save(cache_file, projected) + logger.info("Projection computed and cached to %s", cache_file) + return projected + + +def main() -> None: + """Load the model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--n", type=int, default=100, help="Number of samples to load.") + parser.add_argument("--cache-dir", type=Path, default=_HERE / "nlp_ext_cache") + parser.add_argument("--port", type=int, default=8051) + parser.add_argument("--host", default="0.0.0.0") # noqa: S104 + parser.add_argument( + "--recompute", + action="store_true", + help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") + # Quiet noisy third-party INFO logs (HF Hub freshness checks, download chatter) so the + # device/cache messages below stand out. + for noisy in ("httpx", "httpcore", "urllib3", "filelock", "datasets"): + logging.getLogger(noisy).setLevel(logging.WARNING) + + dataset = datasets.load_dataset("dair-ai/emotion", split="train") + sentences = dataset["text"][: args.n] + labels = dataset["label"][: args.n] + y_true = [_LABEL_NAMES[i] for i in labels] + logger.info("Loaded %d samples from dair-ai/emotion", len(sentences)) + + text_hash = _hash_texts(list(sentences)) + if args.recompute: + # Drop this text set's cached artifacts so the steps below recompute + overwrite. + logger.info("--recompute: dropping cached SHAP + projection for these texts") + (args.cache_dir / f"{text_hash}.pkl").unlink(missing_ok=True) + (args.cache_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) + + # Select the device and report it — GPU when available, CPU otherwise. + if torch.cuda.is_available(): + device = "cuda" + logger.info("CUDA available — using GPU: %s", torch.cuda.get_device_name(0)) + else: + device = "cpu" + logger.info("No CUDA device found — using CPU") + + tokenizer = transformers.AutoTokenizer.from_pretrained(_MODEL_NAME, use_fast=True) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(_MODEL_NAME).to(device) + # Confirm the weights actually landed on the intended device. + logger.info("Loaded model %s on device: %s", _MODEL_NAME, next(classifier.parameters()).device) + + # Full-capability adapter: predict + embeddings + gradients (what HotFlip needs). + model = HFClassifierModel(classifier, tokenizer, label_names=_LABEL_NAMES) + + xpl = NlpExplainer(model, label_names=_LABEL_NAMES, cf_generator=HotFlipGenerator(model)) + # Both steps load from cache_dir if a cache for these exact texts exists, otherwise they + # compute and write it. So the first run is slow; later runs only pay the model load. + compile_cache = args.cache_dir / f"{text_hash}.pkl" + if compile_cache.exists(): + logger.info("SHAP cache hit — loading %s", compile_cache) + else: + logger.info("SHAP cache miss — computing contributions for %d texts (this is the slow part)", len(sentences)) + xpl.compile(sentences, y_true=y_true, cache_dir=args.cache_dir) + projected = load_or_project(sentences, model, args.cache_dir) + + logger.info("can_edit=%s | can_counterfactual=%s", xpl.can_edit(), xpl.can_counterfactual()) + logger.info("Serving on http://%s:%d (Ctrl+C to stop)", args.host, args.port) + xpl.run_app(port=args.port, debug=False, host=args.host, scatter_xy=projected) + + +if __name__ == "__main__": + main() From d7d8ce770c0dd32fbeb81b8d33482bd32a82e4d8 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:38:36 +0200 Subject: [PATCH 036/144] build: create docker for serving the new webapp --- demo/Dockerfile.nlp_ext | 52 ++++++++++++ demo/README.md | 78 +++++++++++++++++- demo/docker-compose.yml | 10 +++ .../9df5b93eecbd2abe512c3624c69dfef1.pkl | Bin 0 -> 140774 bytes .../9df5b93eecbd2abe512c3624c69dfef1.proj.npy | Bin 0 -> 928 bytes 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 demo/Dockerfile.nlp_ext create mode 100644 demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl create mode 100644 demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy diff --git a/demo/Dockerfile.nlp_ext b/demo/Dockerfile.nlp_ext new file mode 100644 index 00000000..e8709693 --- /dev/null +++ b/demo/Dockerfile.nlp_ext @@ -0,0 +1,52 @@ +# NLP Explainer Extended image (CPU-only). Unlike demo/Dockerfile (model-free snapshot), +# this one carries a live model because the What-if Lab needs it. +# +# The expensive part — SHAP + the projection — is computed ONCE outside Docker and copied +# in as a cache (see demo/README.md); the build never runs SHAP. The model + dataset are +# downloaded in a build layer (fast, ~270 MB, cached like any pip layer), so runtime is +# fully offline: no downloads, no SHAP, no GPU at run time. +# +# Build from the repo root (fails fast if nlp_ext_cache is missing — do Step 1 first): +# docker build -f demo/Dockerfile.nlp_ext -t shapash-nlp-ext . +# docker run --rm -p 8051:8051 shapash-nlp-ext +# Then open http://localhost:8051 + +FROM python:3.11-slim + +WORKDIR /app + +# CPU-only torch first (from PyTorch's CPU index — smaller, no CUDA), so the [nlp] extra +# sees torch already satisfied and doesn't pull the CUDA build from PyPI. +RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu "torch>=2.12.1" + +COPY pyproject.toml README.md MANIFEST.in ./ +COPY shapash/ ./shapash/ +# setuptools-scm can't find .git in the build context; any placeholder works since the +# running app doesn't use the installed package version at runtime. +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=0.0.0.dev +RUN pip install --no-cache-dir ".[nlp]" "pyarrow>=17.0.0" + +# Download the model + dataset into the image (a plain download, not SHAP compute). +ENV HF_HOME=/app/hf_cache +RUN python -c "\ +from transformers import AutoTokenizer, AutoModelForSequenceClassification; \ +import datasets; \ +m = 'bhadresh-savani/distilbert-base-uncased-emotion'; \ +AutoTokenizer.from_pretrained(m, use_fast=True); \ +AutoModelForSequenceClassification.from_pretrained(m); \ +datasets.load_dataset('dair-ai/emotion', split='train')" + +COPY demo/serve_nlp_ext.py ./serve_nlp_ext.py +# Pre-computed SHAP + projection cache (built by running serve_nlp_ext.py once — see README). +COPY demo/nlp_ext_cache/ ./nlp_ext_cache/ + +# Serve fully offline: model from the baked HF cache, SHAP from nlp_ext_cache, no network. +# PYTHONUNBUFFERED streams logs; TOKENIZERS_PARALLELISM avoids a fork deadlock. +ENV HF_HUB_OFFLINE=1 \ + TRANSFORMERS_OFFLINE=1 \ + TOKENIZERS_PARALLELISM=false \ + PYTHONUNBUFFERED=1 + +EXPOSE 8051 + +CMD ["python", "serve_nlp_ext.py"] diff --git a/demo/README.md b/demo/README.md index 2dc1cde6..fd861e59 100644 --- a/demo/README.md +++ b/demo/README.md @@ -2,6 +2,18 @@ Interactive webapp that explains a DistilBERT emotion classifier at the token level using SHAP values. +The base app ([below](#nlp-explainer)) is served read-only from a snapshot. An optional +[**Extended**](#nlp-explainer-extended) variant adds a live-model What-if Lab on top. + +| Variant | Port | Live model? | Image | What it adds | +|---|---|---|---|---| +| **NLP Explainer** | 8050 | No — served from a snapshot | Lightweight (no torch/GPU) | Explore precomputed SHAP explanations | +| **NLP Explainer Extended** | 8051 | Yes — model in memory (CPU) | Heavier; model + caches baked in | The above **plus** live editing, re-prediction, and counterfactual suggestions | + +--- + +# NLP Explainer + ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) installed and running @@ -30,7 +42,7 @@ This writes `demo/explainer_snapshot.pkl`. The file bundles texts, predictions, Run from the **repo root** (the build context needs access to the `shapash/` package): ```bash -docker compose -f demo/docker-compose.yml up --build +docker compose -f demo/docker-compose.yml up --build shapash-nlp ``` The image installs only the lightweight serving dependencies (Dash, Plotly, pandas …). No model, no GPU, no heavy ML libraries. Drop `--build` on subsequent runs if nothing has changed. @@ -49,3 +61,67 @@ Press `Ctrl+C` to stop. | **Sample scatter** | 2D PaCMAP projection of text embeddings. Draw a box, lasso or click to filter the table and word importance to a subset. | | **Dataset table** | All samples with predicted label, ground truth, and predicted probability. Click a row to inspect it. Click a bar in the word importance chart to filter to samples containing that word. | | **Local contributions** | Token-level SHAP highlight for the selected sample. Toggle to a waterfall chart for a ranked view. | + +--- + +# NLP Explainer Extended + +Adds a **Data Editor** tab: edit a sample's text for a live prediction + token-contribution highlight, and a **Counterfactuals** tab to +get auto-generated minimal token flips (counterfactuals) that change the prediction. + +This needs the live model in memory, so it can't use the snapshot — but SHAP, the projection, and the +model/dataset downloads are all cached, so only the first run pays for it. Runs on **CPU**. + +**New webapp layout** with three panels: +- Left: control panel with **Dataset** (Dash AG Grid to explore the dataset and click on texts), **Embeddings** (Scatter plot, used to lasso/box select, hihglight word importance), and **Data Editor** +- Upper Right: **Word Importance** (Global explainability) tab and **Counterfactuals** tab. +- Lower Right: **Local contributions** divided as sentence highlights tab and waterfall plot tab. + +Same idea as the snapshot: compute the heavy stuff (SHAP) **once, up front**, then Docker just loads +it. The model + dataset are downloaded by the Docker build itself, so you don't stage them. + +### Step 1 — Pre-compute the SHAP cache (one-time, GPU optional) + +Needs the `[nlp]` extra (torch, transformers, datasets, pacmap). Run the script once — it computes SHAP ++ the projection (a few minutes on CPU, faster on GPU), writes the cache, then starts serving. Once it +prints the local URL, the cache is on disk — press `Ctrl+C`: + +```bash +pip install "shapash[nlp]" +python demo/serve_nlp_ext.py # Ctrl+C once it starts serving +``` + +This writes `demo/nlp_ext_cache/` — SHAP contributions, predictions, and the 2-D projection, keyed by a +hash of the input texts. The Docker build copies it in, so this expensive step never runs in Docker. + +> **Already have `demo/nlp_ext_cache/`?** Skip to Step 2. + +### Step 2 — Build and run + +Run from the **repo root**: + +```bash +docker compose -f demo/docker-compose.yml up --build shapash-nlp-ext +``` + +The build downloads the model + dataset into the image and copies in the SHAP cache; at **run** time it +serves fully offline — no downloads, no SHAP, no GPU. (The build fails fast if `demo/nlp_ext_cache/` is +missing — do Step 1 first.) + +Then open **http://localhost:8051** in your browser. Press `Ctrl+C` to stop. + +## Run without Docker + +Just run the same script — it serves on http://127.0.0.1:8051 and reuses the `demo/nlp_ext_cache/` +from Step 1 (recomputing only if the input texts change, or if you pass `--recompute`): + +```bash +python demo/serve_nlp_ext.py # default 100 samples +python demo/serve_nlp_ext.py --n 200 # more samples (recomputes + re-caches) +``` + +It uses the GPU if present, otherwise CPU. + +> **Iterating on the webapp code?** Don't restart the process — run `serve_nlp_ext.py`'s body in a REPL +> once, then re-call `xpl.run_app(...)` after reloading the changed webapp modules. The model stays +> resident, so you never pay the reload. diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 16d6c99c..b3e68e2b 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -6,3 +6,13 @@ services: image: shapash-nlp ports: - "8050:8050" + + # NLP Explainer Extended (CPU, live model). Heavier image; loads pre-computed caches + # (run serve_nlp_ext.py once first to build them — see demo/README.md). + shapash-nlp-ext: + build: + context: .. + dockerfile: demo/Dockerfile.nlp_ext + image: shapash-nlp-ext + ports: + - "8051:8051" diff --git a/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl b/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..99757b1729a08813ec684ab98a81280849e3addc GIT binary patch literal 140774 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�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

%E)k)9MyY=a)6{KMKH+Hovai0Zokx2R`Mn%UC<$ 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 G@qYkA#ao^L literal 0 HcmV?d00001 diff --git a/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy b/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy new file mode 100644 index 0000000000000000000000000000000000000000..6f36275df20fbd136d4519c11d551eabfdbe0a9b GIT binary patch literal 928 zcmbWr|5MX-9Ki7nt|1&+SAOkQtjYX{&I<{%@nw6zV$DNrcqcfED6XhXEzF&KNqD_(WLY5!0_WBQ2a({|ot}<6AkF@Tp1(aGN42_Fx8?~2 z6@quUVpF-%!lg*Kw31{_F5ybdmi-pP-XgQ5*z})k3}xk}M_#_$@TTd}uU?;+D3>H9 z$R+Pd{%0}6>G|Xfj^PeXK2<(>2Zxlo$`oS-)aZFAp7t@v6a0EHPw z_Vqh{9Dh+wXBLaWlo4Q4*;lc@h=*~lk98QfQgq@RO5F>gq5d@PGc8mVT(UxB+e^fi z#8ShtNmgn51%FU*kSYHO8@&d~yAr_Kf*jf&A3@F4xull&Vv<%(eu)@bKMg8(Y3*>f zX(wAA@?&^kfMF}*%#twctLVn?Vx_7j?jX#rNg!S49JaXD(5cjUJnhue=%x@V?nHNg zZ?l6)G{(L;JdQ*95Sx4a5@ssY8)c^OSGed156uS#*`C}Z>>+uv^OM_MC%iUz z`gSZ>1Q#*TvyrBM6q2HUGF|F<3%F!8#jF%Vj^_$HAUKJdETAp5VeHjusd3;wo^+P7 z)@VOEc8FlxiTk*g=fLr08%Bh}?3vH|P$cEy+vEVF4Qg5tF9uDjnqsA5X#DITD{J>+ zx~^B*!tIA#$A_xSZaXwBTSsv>?%~Z3cvxW_V)^wlSW?%9AFs`!_*Hi?tz|c*But^O zP6oBkF1$RPLt`&YB6mwe=bo5H`R^ID^T8%;{a}&aI>lQ09H$*pr@`I9Zrw( z)_EI*BVVJL^V2x9YGrrVIy=~!npMW26}CH$sw&UffnWhRy}zQ^@;pqmxbcfu0_;Z7 zMZ7Fq0eO)g9MNy1q5=WEov0?gObipdc&Iw=Ws6(`7navpAvKAkQ^qhV-WXJloUHVP-9`3}lm`GT!`wKe~GRU13L5;yG)puKL@GqQCvH1W1 literal 0 HcmV?d00001 From 10962287f60f3fb12614f7192a66f23406e5fb1f Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:39:21 +0200 Subject: [PATCH 037/144] feat: create new version of the webapp --- shapash/webapp/nlp_app.py | 642 ++++++++++++++++++++++++-------------- 1 file changed, 408 insertions(+), 234 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 5295ce7d..5c92e618 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -23,16 +23,23 @@ from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance +from shapash.webapp.nlp_components import ( + CounterfactualComponent, + DataEditorComponent, + pack_datapoint, + unpack_datapoint, +) +from shapash.webapp.nlp_view import NlpView + +_APPLY_STORE = "whatif-apply-store" +_CURRENT_STORE = "current-datapoint" -_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} -# min() only shrinks on small screens; returns the natural size on large ones. -# 800 px CSS (HiDPI laptop): 224 px chart / 240 px table — enough to show token contributions -# 1080 px CSS (external): 302 px chart / 324 px table — close to original feel -_WORD_IMPORTANCE_HEIGHT_CSS = "min(390px, 28vh)" -_TABLE_HEIGHT_CSS = "min(460px, 30vh)" _SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") _HIDDEN = {"display": "none"} -_VISIBLE = {"display": "block"} +# The visible tab body is a flex-column item that fills the bodies container (which itself fills the +# card). This unbroken flex chain is what lets a body's inner content use height:100% / flex:1 (e.g. the +# dataset grid) to fill the panel; a plain `display:block` here would collapse to content height. +_VISIBLE = {"display": "flex", "flexDirection": "column", "flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"} _PALETTE = [ "#1f77b4", "#ff7f0e", @@ -92,8 +99,40 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: if scatter_xy.shape != (n, 2): raise ValueError(f"scatter_xy must have shape ({n}, 2), got {scatter_xy.shape}") self._scatter_xy: np.ndarray | None = scatter_xy + + # What-if Lab: read-only view + live engine (the explainer itself when it is live). + # Components self-disable via their `requires` when the engine lacks a capability + # (e.g. an explainer restored from a snapshot holds no model). + self._view = NlpView(explainer) + self._engine = explainer + self._components = [ + comp + for comp in (DataEditorComponent(), CounterfactualComponent()) + if type(comp).is_available(self._view, self._engine) + ] + self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) self.app.title = "Shapash — NLP Explainer" + # Pin the document to the viewport so only the inner panels scroll, never the page. The + # 100vh shell needs html/body at full height with no default margin; overflow:hidden is the + # guarantee against any residual sub-pixel overflow producing a page scrollbar. + self.app.index_string = """ + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + + + {%app_entry%} +

{%config%}{%scripts%}{%renderer%}
+ +""" self._build_layout() self._register_callbacks() @@ -199,19 +238,6 @@ def _build_layout(self) -> None: width="auto", className="align-self-center", ), - dbc.Col( - dbc.Button( - "× clear", - id="scatter-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0", - style={"display": "none", "fontSize": "0.8em"}, - ), - width="auto", - className="align-self-center", - ), ], className="align-items-center mb-2", ), @@ -232,21 +258,21 @@ def _build_layout(self) -> None: style={"flex": "1 1 auto", "minHeight": "340px"}, ), ], - # Flex column so the graph above can stretch to the card height. - style={**_CARD_STYLE, "display": "flex", "flexDirection": "column"}, + # Flex column so the graph above can stretch to the panel height. + style={"display": "flex", "flexDirection": "column", "height": "100%"}, ) - # ── Global Word Importance panel — controls now live inside it ── - # (Top-K / Sign / Exclude only affect this panel, so they are - # co-located here rather than in a misleading full-width top bar.) + # ── Global Word Importance panel — controls live inside it ── + # (Top-K / Sign / Exclude only affect this panel, so they are co-located here.) + # The tab label already names the panel, so no H6; a tight controls row + a graph that + # flex-grows to fill the panel means the whole chart is visible without scrolling. word_importance_panel = html.Div( [ - html.H6("Global Word Importance", className="fw-bold mb-2"), dbc.Row( [ dbc.Col( [ - html.Label("Top-K words", className="fw-bold small mb-1"), + html.Label("Top-K words", className="fw-bold small mb-0"), dcc.Slider( id="topk-slider", min=1, @@ -257,11 +283,11 @@ def _build_layout(self) -> None: tooltip={"placement": "bottom", "always_visible": False}, ), ], - width=7, + width=5, ), dbc.Col( [ - html.Label("Contributions", className="fw-bold small mb-1"), + html.Label("Contributions", className="fw-bold small mb-0"), dcc.RadioItems( id="sign-filter", options=[ @@ -275,36 +301,38 @@ def _build_layout(self) -> None: labelStyle={"marginRight": "12px"}, ), ], - width=5, + width="auto", + className="align-self-start", ), - ], - className="align-items-center mb-2", - ), - html.Div( - [ - html.Label("Exclude words", className="fw-bold small mb-1"), - dcc.Dropdown( - id="word-filter", - options=[{"label": w, "value": w} for w in all_words], - value=[], - multi=True, - placeholder="Select words to exclude…", - style={"fontSize": "0.9em"}, + dbc.Col( + [ + html.Label("Exclude words", className="fw-bold small mb-0"), + dcc.Dropdown( + id="word-filter", + options=[{"label": w, "value": w} for w in all_words], + value=[], + multi=True, + placeholder="Exclude…", + style={"fontSize": "0.85em"}, + ), + ], + width=True, ), ], - className="mb-2", + className="align-items-start g-2 mb-1", + style={"flex": "0 0 auto"}, ), dcc.Graph( id="global-importance-graph", config={"displayModeBar": False, "responsive": True}, - style={"height": _WORD_IMPORTANCE_HEIGHT_CSS}, + style={"flex": "1 1 auto", "minHeight": "0"}, ), ], - style=_CARD_STYLE, + style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) - # ── Text Samples panel (full-width hub) ─────────────────────── - text_samples_panel = html.Div( + # ── Dataset table body (left-panel default tab) ─────────────── + text_samples_body = html.Div( [ dbc.Row( [ @@ -317,19 +345,6 @@ def _build_layout(self) -> None: width="auto", className="align-self-center", ), - dbc.Col( - dbc.Button( - "× clear word filter", - id="word-filter-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0", - style={"display": "none", "fontSize": "0.8em"}, - ), - width="auto", - className="align-self-center", - ), dbc.Col( dbc.Switch( id="errors-only-switch", @@ -339,11 +354,13 @@ def _build_layout(self) -> None: style={} if has_gt else {"display": "none"}, ), width="auto", - className="align-self-center ms-2", + className="align-self-center ms-auto", ), ], className="align-items-center mb-2", + style={"flex": "0 0 auto"}, ), + # Grid grows to fill the tab body (now that the editor lives on its own tab). dag.AgGrid( id="dataset-table", rowData=self._full_table_records, @@ -356,18 +373,19 @@ def _build_layout(self) -> None: }, selectedRows=[self._full_table_records[0]], style={ - "height": _TABLE_HEIGHT_CSS, + "flex": "1 1 auto", + "minHeight": "0", "--ag-font-size": "15px", "--ag-header-font-size": "14px", }, className="ag-theme-alpine", ), ], - style=_CARD_STYLE, + style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) - # ── Token Contributions panel (full-width detail-on-demand) ─── - token_contributions_panel = html.Div( + # ── Sentence highlight body (lower-right default tab) ───────── + highlight_body = html.Div( [ html.H6( id="sentence-highlight-title", @@ -385,78 +403,129 @@ def _build_layout(self) -> None: "justifyContent": "center", }, ), - html.Hr(style={"margin": "14px 0 10px"}), - dbc.Row( + ], + style={"height": "100%"}, + ) + + # ── Waterfall body (lower-right second tab) ─────────────────── + waterfall_body = html.Div( + [ + html.Div( [ - dbc.Col( - dbc.Switch( - id="show-waterfall", - label="Show waterfall", - value=False, - # className="small mb-0", - ), - width="auto", - className="align-self-center", - ), - dbc.Col( - html.Div( - id="waterfall-threshold-wrapper", - children=[ - html.Label( - "Group tokens below (% of max contribution)", - className="small fw-bold mb-0 me-2", - ), - dcc.Slider( - id="waterfall-threshold", - min=0, - max=50, - step=1, - value=10, - marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, - tooltip={ - "placement": "bottom", - "always_visible": False, - }, - ), - ], - style=_HIDDEN, - ), - width=True, + html.Label( + "Group tokens below (% of max contribution)", + className="small fw-bold mb-1 d-block", ), - ], - className="align-items-center mb-2", - ), - html.Div( - id="waterfall-container", - children=[ - dcc.Graph( - id="waterfall-graph", - config={"displayModeBar": False}, + dcc.Slider( + id="waterfall-threshold", + min=0, + max=50, + step=1, + value=10, + marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, + tooltip={"placement": "bottom", "always_visible": False}, ), ], - style=_HIDDEN, + className="mb-2", ), + dcc.Graph(id="waterfall-graph", config={"displayModeBar": False}), ], - style=_CARD_STYLE, + style={"height": "100%"}, + ) + + # ── Selection bar: persistent, always-visible filter state + clears ── + # Lives above the left tabs so a scatter/word selection stays visible whichever + # left tab (table or embeddings) is active. Clear buttons are consolidated here + # from the individual panels they used to live in. + selection_children: list = [ + html.Span("Showing:", className="text-muted small me-2"), + html.Span("all samples", id="selection-summary", className="small fw-bold me-3"), + ] + if scatter_col_content is not None: + selection_children.append( + dbc.Button( + "× clear selection", + id="scatter-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0 me-3", + style={"display": "none", "fontSize": "0.8em"}, + ) + ) + selection_children.append( + dbc.Button( + "× clear word filter", + id="word-filter-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0", + style={"display": "none", "fontSize": "0.8em"}, + ) + ) + selection_bar = html.Div( + selection_children, + style={ + "border": "1px solid #dee2e6", + "borderRadius": "4px", + "padding": "6px 12px", + "display": "flex", + "alignItems": "center", + "flexWrap": "wrap", + "flex": "0 0 auto", + }, ) - # ── Top row: two global "where to look" panels side by side ─── - # Word importance shares the row with the sample-space scatter when a - # projection is provided; otherwise it spans the full width. + # ── Assemble the three panels as tab groups (all bodies stay mounted) ── + self._tab_groups = {} + + editor_comp = next((c for c in self._components if isinstance(c, DataEditorComponent)), None) + cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) + + left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_col_content is not None: - top_row_cols = [ - dbc.Col(word_importance_panel, width=6), - dbc.Col(scatter_col_content, width=6), - ] - else: - top_row_cols = [dbc.Col(word_importance_panel, width=12)] + left_tabs.append(("scatter", "Embeddings", scatter_col_content)) + if editor_comp is not None: + left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._view, self._engine))) + + upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] + if cf_comp is not None: + upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) + + lower_right_tabs: list = [ + ("highlight", "Sentence", highlight_body), + ("waterfall", "Waterfall", waterfall_body), + ] + + left_column = html.Div( + [selection_bar, self._tabbed_card("left-tabs", left_tabs)], + style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, + ) + right_column = html.Div( + [ + self._tabbed_card("upper-right-tabs", upper_right_tabs), + self._tabbed_card("lower-right-tabs", lower_right_tabs), + ], + style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, + ) + + stores: list = [ + # current-datapoint is the app's primary selection: the one text every + # per-instance panel (highlight, waterfall, counterfactuals) reads from. + dcc.Store(id=_CURRENT_STORE, data=None), + dcc.Store(id="scatter-selected-indices", data=None), + dcc.Store(id="word-click-filter", data=None), + ] + if self._components: + stores.append(dcc.Store(id=_APPLY_STORE, data=None)) self.app.layout = dbc.Container( [ # ── Header (Class selector is the only truly global control) ── dbc.Row( [ - dbc.Col(html.H3("Shapash — NLP Explainer", className="mb-0"), width=8), + dbc.Col(html.H4("Shapash — NLP Explainer", className="mb-0"), width=8), dbc.Col( [ html.Label("Class", className="fw-bold small"), @@ -470,19 +539,67 @@ def _build_layout(self) -> None: width=4, ), ], - className="mb-3 mt-3 align-items-center", + className="py-2 align-items-center", + style={"flex": "0 0 auto"}, ), - # ── Overview / controls row (filters the funnel below) ──────── - dbc.Row(top_row_cols, className="mb-3"), - # ── Hub: full-width text samples table ─────────────────────── - dbc.Row(dbc.Col(text_samples_panel, width=12), className="mb-3"), - # ── Detail-on-demand: full-width token contributions ───────── - dbc.Row(dbc.Col(token_contributions_panel, width=12), className="mb-3"), - # ── Hidden stores (always present) ─────────────────────────── - dcc.Store(id="scatter-selected-indices", data=None), - dcc.Store(id="word-click-filter", data=None), + # ── Three-panel body: left (data) | right (global over local) ── + dbc.Row( + [ + dbc.Col(left_column, width=5, style={"height": "100%"}), + dbc.Col(right_column, width=7, style={"height": "100%"}), + ], + # gx-3 = horizontal gutter only. A vertical gutter (g-3) adds a 1rem net excess + # that pushes the 100vh shell past the viewport and makes the whole page scroll. + className="gx-3", + style={"flex": "1 1 auto", "minHeight": "0"}, + ), + *stores, ], fluid=True, + # Full-viewport shell: panels scroll internally, the page itself never scrolls. + style={"height": "100vh", "display": "flex", "flexDirection": "column", "overflow": "hidden"}, + ) + + def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: + """Wrap ``(tab_id, label, body)`` tuples into a card with a tab header. + + Every body stays mounted in the DOM; a registered callback toggles ``display`` so + cross-panel callbacks that target a component on an inactive tab keep working (Dash + errors if an Output/Input component is absent). The tab ids are recorded on + ``self._tab_groups`` for that toggle callback. + """ + active = tabs[0][0] + self._tab_groups[tabs_id] = [tid for tid, _, _ in tabs] + headers = [dbc.Tab(label=label, tab_id=tid) for tid, label, _ in tabs] + bodies = [ + html.Div(body, id=f"{tabs_id}-body-{tid}", style=(_VISIBLE if tid == active else _HIDDEN)) + for tid, _, body in tabs + ] + return html.Div( + [ + dbc.Tabs(headers, id=tabs_id, active_tab=active), + # Flex column so the single visible body (the others are display:none) fills the height. + html.Div( + bodies, + style={ + "flex": "1 1 auto", + "minHeight": "0", + "paddingTop": "8px", + "display": "flex", + "flexDirection": "column", + }, + ), + ], + style={ + "border": "1px solid #dee2e6", + "borderRadius": "4px", + "padding": "12px", + "display": "flex", + "flexDirection": "column", + "overflow": "hidden", + "flex": "1 1 0", + "minHeight": "0", + }, ) # ------------------------------------------------------------------ @@ -526,6 +643,27 @@ def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, s fig.layout.height = None # let the CSS container height take over return fig + # ── Primary selection: selected table row → current-datapoint ──── + # The editor's Predict callback also writes this store (see DataEditorComponent), + # so every per-instance panel reads one source regardless of where it came from. + @self.app.callback( + Output(_CURRENT_STORE, "data"), + Input("dataset-table", "selectedRows"), + ) + def set_current_from_row(selected_rows): + if not selected_rows: + raise PreventUpdate + pos = int(selected_rows[0]["_orig_idx"]) + base_values = contrib.base_values + base = base_values[pos] if base_values is not None else None + return pack_datapoint( + text=selected_rows[0].get("text", ""), + orig_idx=pos, + tokens=contrib.token_strings[pos], + values=contrib.values[pos], + base_values=base, + ) + # ── Sentence highlight ─────────────────────────────────────────── @self.app.callback( [ @@ -533,46 +671,35 @@ def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, s Output("sentence-highlight-title", "children"), ], [ - Input("dataset-table", "selectedRows"), + Input(_CURRENT_STORE, "data"), Input("class-selector", "value"), ], ) - def update_sentence_highlight(selected_rows, label_idx): - if not selected_rows or label_idx is None: + def update_sentence_highlight(datapoint, label_idx): + if not datapoint or label_idx is None: raise PreventUpdate - pos = int(selected_rows[0]["_orig_idx"]) label_idx = int(label_idx) - tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) + tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) + label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) highlight = plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) title = f"Token Contributions — {label_name}" return highlight, title - # ── Waterfall show/hide ────────────────────────────────────────── - @self.app.callback( - [ - Output("waterfall-container", "style"), - Output("waterfall-threshold-wrapper", "style"), - ], - Input("show-waterfall", "value"), - ) - def toggle_waterfall(show): - return (_VISIBLE, _VISIBLE) if show else (_HIDDEN, _HIDDEN) - # ── Waterfall chart ────────────────────────────────────────────── @self.app.callback( Output("waterfall-graph", "figure"), [ - Input("dataset-table", "selectedRows"), + Input(_CURRENT_STORE, "data"), Input("class-selector", "value"), Input("waterfall-threshold", "value"), ], ) - def update_waterfall(selected_rows, label_idx, threshold_pct): - if not selected_rows or label_idx is None: + def update_waterfall(datapoint, label_idx, threshold_pct): + if not datapoint or label_idx is None: raise PreventUpdate - pos = int(selected_rows[0]["_orig_idx"]) label_idx = int(label_idx) - tokens, vals, base_value, label_name = self._sample_data(pos, label_idx) + tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) + label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 return plot_waterfall( tokens=tokens, @@ -582,19 +709,28 @@ def update_waterfall(selected_rows, label_idx, threshold_pct): title=f"Token contributions — {label_name}", ) - # ── Word-bar click → filter table ──────────────────────────────── - @self.app.callback( - Output("word-click-filter", "data"), - Input("global-importance-graph", "clickData"), - Input("word-filter-clear-btn", "n_clicks"), - ) - def update_word_click_filter(click_data, _clear_clicks): - trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" - if "word-filter-clear-btn" in trigger: - return None - if not click_data or not click_data.get("points"): - raise PreventUpdate - return click_data["points"][0]["y"] + # ── Word-bar click / clear → table word filter ─────────────────── + # Resetting the graph's clickData to None after each event lets the SAME bar be clicked + # again (Plotly does not re-fire clickData when the value is unchanged). Without a scatter + # the bar click drives word-click-filter directly; WITH a scatter the filter is derived + # from scatter-word-select instead (see the scatter block), so editing/removing the word + # there also clears this filter. + if self._scatter_xy is None: + + @self.app.callback( + Output("word-click-filter", "data"), + Output("global-importance-graph", "clickData"), + Input("global-importance-graph", "clickData"), + Input("word-filter-clear-btn", "n_clicks"), + prevent_initial_call=True, + ) + def update_word_click_filter(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if "word-filter-clear-btn" in trigger: + return None, None + if not click_data or not click_data.get("points"): + raise PreventUpdate + return [click_data["points"][0]["y"]], None @self.app.callback( Output("word-filter-clear-btn", "style"), @@ -606,11 +742,14 @@ def toggle_word_clear_button(word_filter): return {"display": "none", "fontSize": "0.8em"} # ── Unified table filter (scatter + word-bar click + errors-only) ── + # Also writes the selection-summary readout: it already knows the resulting row count, so the + # count and the filter description stay consistent (no separate, drift-prone callback). @self.app.callback( [ Output("dataset-table", "rowData"), Output("dataset-table", "selectedRows"), Output("table-title", "children"), + Output("selection-summary", "children"), ], [ Input("scatter-selected-indices", "data"), @@ -619,6 +758,8 @@ def toggle_word_clear_button(word_filter): ], ) def filter_table(selected_indices, word_filter, errors_only): + # word_filter may be a list of words (multi-select in the scatter) or None. + words = word_filter if isinstance(word_filter, list) else ([word_filter] if word_filter else []) if selected_indices is None: recs = full_records else: @@ -627,16 +768,27 @@ def filter_table(selected_indices, word_filter, errors_only): if errors_only and has_gt: misclassified = [r for r in recs if str(r.get("prediction", "")) != str(r.get("ground_truth", ""))] recs = misclassified or recs - if word_filter: - word_lower = word_filter.lower() - filtered = [r for r in recs if word_lower in r["text"].lower()] + if words: + lowers = [w.lower() for w in words] + # Rows containing ANY of the selected words (matches the scatter's word colouring). + filtered = [r for r in recs if any(w in r["text"].lower() for w in lowers)] recs = filtered or recs - title = ( - f'Text Samples — filtered by "{word_filter}"' - if word_filter - else "Text Samples — click a row to inspect" - ) - return recs, [recs[0]], title + + total = len(full_records) + parts = [] + if selected_indices is not None: + parts.append(f"{len(selected_indices)} selected in embeddings") + if words: + parts.append("containing " + ", ".join(f'"{w}"' for w in words)) + if errors_only and has_gt: + parts.append("model errors only") + if parts: + summary = " · ".join(parts) + f" ({len(recs)} of {total})" + title = "Text Samples — filtered" + else: + summary = f"all {total} samples" + title = "Text Samples — click a row to inspect" + return recs, [recs[0]], title, summary # ── Scatter-specific callbacks (registered only when xy provided) ── if self._scatter_xy is not None: @@ -663,12 +815,31 @@ def sync_color_by(words): raise PreventUpdate return "word_contribution" + # Bar click / clear → scatter word selection (the single source of truth for the word + # filter when a scatter exists). clickData is reset so the same bar can be re-clicked. @self.app.callback( Output("scatter-word-select", "value"), - Input("word-click-filter", "data"), + Output("global-importance-graph", "clickData"), + Input("global-importance-graph", "clickData"), + Input("word-filter-clear-btn", "n_clicks"), + prevent_initial_call=True, ) - def sync_word_select_from_bar(word_filter): - return [word_filter] if word_filter else [] + def set_scatter_words(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if "word-filter-clear-btn" in trigger: + return [], None + if not click_data or not click_data.get("points"): + raise PreventUpdate + return [click_data["points"][0]["y"]], None + + # Table word filter follows the scatter selection: removing the word in the dropdown (or + # hitting clear) unfilters the table and hides the clear button. + @self.app.callback( + Output("word-click-filter", "data"), + Input("scatter-word-select", "value"), + ) + def word_filter_from_scatter(words): + return words or None @self.app.callback( Output("scatter-word-select", "style"), @@ -705,6 +876,25 @@ def toggle_clear_button(selected_indices): hidden = {"display": "none", "fontSize": "0.8em"} return visible if selected_indices else hidden + # ── Tab visibility: toggle display of always-mounted bodies ────── + # Bodies stay in the DOM (see _tabbed_card); only their `display` flips so the + # cross-panel callbacks above keep firing for panels on inactive tabs. + for tabs_id, tab_ids in self._tab_groups.items(): + + @self.app.callback( + [Output(f"{tabs_id}-body-{tid}", "style") for tid in tab_ids], + Input(tabs_id, "active_tab"), + ) + def _toggle_tab_bodies(active, _tab_ids=tab_ids): + return [(_VISIBLE if tid == active else _HIDDEN) for tid in _tab_ids] + + # (The selection-summary readout is written by filter_table, which knows the row count.) + + # ── What-if Lab component callbacks (self-contained, capability-gated) ── + stores = {"apply": _APPLY_STORE, "current": _CURRENT_STORE} + for comp in self._components: + comp.register_callbacks(self.app, self._view, self._engine, stores) + # ------------------------------------------------------------------ # Public # ------------------------------------------------------------------ @@ -721,24 +911,6 @@ def _n_classes(self) -> int: sample = self.explainer.contributions.values[0] return sample.shape[1] if sample.ndim == 2 else 1 - def _sample_data(self, pos: int, label_idx: int): - """Return tokens, 1-D values, base_value, and label_name for one sample.""" - contrib = self.explainer.contributions - tokens = contrib.token_strings[pos] - vals = contrib.values[pos] - if vals.ndim == 2: - vals = vals[:, label_idx] - - bv = contrib.base_values - base_value: float | None = None - if bv is not None and bv.ndim == 2 and pos < bv.shape[0] and label_idx < bv.shape[1]: - base_value = float(bv[pos, label_idx]) - elif bv is not None and bv.ndim == 1 and pos < bv.shape[0]: - base_value = float(bv[pos]) - - label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) - return tokens, vals, base_value, label_name - def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: """Per-sample sum of SHAP contributions for all tokens matching *word*.""" contrib = self.explainer.contributions @@ -776,46 +948,48 @@ def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, labe colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" fig = go.Figure() - # Gray context layer — only absent points so hover doesn't overlap - if len(absent_mask) > 0: - fig.add_trace( - go.Scattergl( - x=xy[absent_mask, 0].tolist(), - y=xy[absent_mask, 1].tolist(), - mode="markers", - marker=dict(color="#b0b0b0", size=5, opacity=0.35), - customdata=absent_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in absent_mask], - hovertemplate="%{text}absent", - showlegend=False, - ) + # Both layers are ALWAYS added (even when a mask is empty) so the trace structure stays + # constant across word additions/removals. With a stable `uirevision`, a changing WebGL + # (Scattergl) trace count leaves ghost/"shadow" points from the previous render — keeping + # exactly two traces avoids that. + # Gray context layer — absent points (no selected word contributes to them). + fig.add_trace( + go.Scattergl( + x=xy[absent_mask, 0].tolist(), + y=xy[absent_mask, 1].tolist(), + mode="markers", + marker=dict(color="#b0b0b0", size=5, opacity=0.35), + customdata=absent_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in absent_mask], + hovertemplate="%{text}absent", + showlegend=False, ) - # Colored overlay — only samples where at least one word contributes - if len(present_mask) > 0: - fig.add_trace( - go.Scattergl( - x=xy[present_mask, 0].tolist(), - y=xy[present_mask, 1].tolist(), - mode="markers", - marker=dict( - color=contributions[present_mask].tolist(), - colorscale="RdBu", - cmin=-max_abs, - cmax=max_abs, - size=9, - opacity=0.9, - colorbar=dict( - title=dict(text=colorbar_title, side="right"), - thickness=12, - tickformat=".2f", - ), + ) + # Colored overlay — samples where at least one selected word contributes. + fig.add_trace( + go.Scattergl( + x=xy[present_mask, 0].tolist(), + y=xy[present_mask, 1].tolist(), + mode="markers", + marker=dict( + color=contributions[present_mask].tolist(), + colorscale="RdBu", + cmin=-max_abs, + cmax=max_abs, + size=9, + opacity=0.9, + colorbar=dict( + title=dict(text=colorbar_title, side="right"), + thickness=12, + tickformat=".2f", ), - customdata=present_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in present_mask], - hovertemplate="SHAP: %{marker.color:.3f}
%{text}", - showlegend=False, - ) + ), + customdata=present_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in present_mask], + hovertemplate="SHAP: %{marker.color:.3f}
%{text}", + showlegend=False, ) + ) fig.update_layout( dragmode="select", uirevision="scatter", From ce9d26ec1dc423bc674492f8da82b6d64be32099 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Fri, 3 Jul 2026 16:40:04 +0200 Subject: [PATCH 038/144] test: add tests for new functionalities --- tests/integration_tests/test_nlp_whatif.py | 209 ++++++++++++++++ .../compute/generators/test_hotflip.py | 143 +++++++++++ tests/unit_tests/model/test_model_base.py | 84 +++++++ tests/unit_tests/nlp/test_nlp_prototype.py | 3 +- .../unit_tests/webapp/test_nlp_whatif_app.py | 231 ++++++++++++++++++ 5 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 tests/integration_tests/test_nlp_whatif.py create mode 100644 tests/unit_tests/compute/generators/test_hotflip.py create mode 100644 tests/unit_tests/model/test_model_base.py create mode 100644 tests/unit_tests/webapp/test_nlp_whatif_app.py diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py new file mode 100644 index 00000000..0d295a8f --- /dev/null +++ b/tests/integration_tests/test_nlp_whatif.py @@ -0,0 +1,209 @@ +"""Integration tests for the NLP what-if stack against a real transformer. + +Exercises the full-capability HuggingFace adapter (predict / embeddings / gradients) and the +HotFlip generator on the emotion distilbert model used by the demos. Skipped automatically when +``transformers``/``torch`` are not installed. +""" + +import numpy as np +import pytest + +transformers = pytest.importorskip("transformers") +pytest.importorskip("torch") + +from shapash.compute.generators import HotFlipGenerator # noqa: E402 +from shapash.explainer.interactive import InteractiveEngine # noqa: E402 +from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 +from shapash.model import HFClassifierModel, HFPipelineModel # noqa: E402 +from shapash.model.base import SupportsEmbeddings, SupportsGradients, has_capabilities # noqa: E402 +from shapash.webapp.nlp_app import NlpWebApp # noqa: E402 + +MODEL_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" +LABELS = ["sadness", "joy", "love", "anger", "fear", "surprise"] + + +@pytest.fixture(scope="module") +def hf(): + try: + tokenizer = transformers.AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=True) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(MODEL_NAME) + except Exception as exc: # network / cache miss + pytest.skip(f"model unavailable: {exc}") + pipe = transformers.pipeline("text-classification", model=classifier, tokenizer=tokenizer, top_k=None) + return classifier, tokenizer, pipe + + +def test_pipeline_adapter_predict_and_capabilities(hf): + _, _, pipe = hf + model = HFPipelineModel(pipe, label_names=LABELS) + probs = model.predict(["i am so happy today", "i feel terrified and alone"]) + assert probs.shape == (2, len(LABELS)) + np.testing.assert_allclose(probs.sum(axis=1), 1.0, atol=1e-4) + assert not has_capabilities(model, SupportsGradients) + + +def test_classifier_adapter_full_capabilities(hf): + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + assert has_capabilities(model, SupportsGradients, SupportsEmbeddings) + + probs = model.predict(["i am so happy today"]) + assert probs.shape == (1, len(LABELS)) + np.testing.assert_allclose(probs.sum(axis=1), 1.0, atol=1e-4) + + vocab, matrix = model.get_embedding_table() + assert matrix.shape[0] == tokenizer.vocab_size == len(vocab) + assert matrix.shape[1] == classifier.config.dim + + emb = model.embed(["i am so happy today", "hello world"]) + assert emb.shape == (2, classifier.config.dim) + + tokens, grads = model.token_gradients("i am so happy today", int(probs[0].argmax())) + assert len(tokens) == grads.shape[0] > 0 + assert np.isfinite(grads).all() + assert (np.abs(grads).sum(axis=1) > 0).all() + assert all(not (t.startswith("[") and t.endswith("]")) for t in tokens) + + +def test_hotflip_finds_flip(hf): + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + gen = HotFlipGenerator(model) + cfs = gen.generate("i am so happy today", config={"num_examples": 3, "max_flips": 2}) + assert cfs, "HotFlip found no counterfactual on a confidently-classified sample" + for cf in cfs: + assert cf.new_label != cf.orig_label + assert cf.new_text != cf.original_text + # minimality: no returned set is a strict superset of another + sets = [frozenset(cf.flipped_positions) for cf in cfs] + for i, a in enumerate(sets): + for j, b in enumerate(sets): + if i != j: + assert not (b < a) + + +def test_hotflip_counterfactuals_are_wellformed_words(hf): + """Replacements are real words, not sub-word pieces — and are model-verified, not linear guesses. + + Regression test for two HotFlip bugs: (1) committing to the single first-order-best candidate per + position, which is an unreliable proxy that often does not flip; and (2) admitting sub-word + (``##``) / non-word tokens as replacements, which produced malformed text like ``"i beforewu"``. + The generator now shortlists top-K word candidates and re-scores them against the model. + """ + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + gen = HotFlipGenerator(model) + + cfs = gen.generate("i feel great", config={"num_examples": 5, "max_flips": 2}) + assert cfs, "HotFlip found no counterfactual on a confidently-classified sample" + for cf in cfs: + # Every substituted replacement is a plausible standalone word (no '##' pieces, no specials). + for _pos, _old, new in cf.substitutions: + assert new.isalpha(), f"replacement {new!r} is not a well-formed word token" + # Rebuilt text stays well-formed and the reported flip really holds under the model. + assert "##" not in cf.new_text and "[" not in cf.new_text + assert cf.new_label != cf.orig_label + cf_probs = model.predict([cf.new_text])[0] + assert LABELS[int(cf_probs.argmax())] == cf.new_label + + +def test_explainer_interactive_engine_with_classifier(hf): + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + xpl = NlpExplainer(model, label_names=LABELS) + + assert isinstance(xpl, InteractiveEngine) + assert xpl.can_edit() + assert xpl.can_counterfactual() # HotFlip auto-built from the gradient-capable model + assert isinstance(xpl.cf_generator, HotFlipGenerator) + + label, probs = xpl.predict("i am so happy today") + assert label in LABELS + assert abs(sum(probs.values()) - 1.0) < 1e-4 + + xpl.compile(["i am so happy today", "i feel terrified and alone"], y_true=["joy", "fear"]) + contribs, elabel, eprobs = xpl.explain_text("i am furious about this") + assert len(contribs) == 1 + assert elabel in LABELS + assert contribs.label_names == LABELS + + cfs = xpl.generate_counterfactuals("i am so happy today", config={"num_examples": 2, "max_flips": 2}) + assert all(cf.new_label != cf.orig_label for cf in cfs) + + +def test_explainer_pipeline_is_editable_but_no_counterfactuals(hf): + _, _, pipe = hf + xpl = NlpExplainer(pipe, label_names=LABELS) # predict-only pipeline + assert xpl.can_edit() + assert not xpl.can_counterfactual() + assert xpl.cf_generator is None + assert xpl.cf_config_spec() == {} + + +def _find_callback_key(app, output_substr): + """Return the callback_map key whose outputs mention ``output_substr``.""" + for key in app.callback_map: + if output_substr in key: + return key + raise KeyError(output_substr) + + +def _post_callback(app, output_key, inputs, state): + """Drive one Dash callback via the Flask test client, returning the parsed response.""" + spec = app.callback_map[output_key] + raw_out = spec["output"] + raw_out = raw_out if isinstance(raw_out, list) else [raw_out] + outputs = [{"id": o.component_id, "property": o.component_property} for o in raw_out] + body = { + "output": output_key, + "outputs": outputs if len(outputs) > 1 else outputs[0], + "inputs": inputs, + "state": state, + "changedPropIds": [f"{i['id']}.{i['property']}" for i in inputs], + } + resp = app.server.test_client().post("/_dash-update-component", json=body) + return resp + + +def test_whatif_app_callbacks_end_to_end(hf): + """Drive the real Predict and Generate callbacks in-process against the live model.""" + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + xpl = NlpExplainer(model, label_names=LABELS) + xpl.compile(["i am so happy today", "i feel terrified and alone"], y_true=["joy", "fear"]) + webapp = NlpWebApp(xpl) + app = webapp.app + + # Predict: edited text -> probability figure + current-datapoint (drives the shared highlight). + predict_key = _find_callback_key(app, "data-editor-prob") + r = _post_callback( + app, + predict_key, + inputs=[{"id": "data-editor-predict-btn", "property": "n_clicks", "value": 1}], + state=[ + {"id": "data-editor-input", "property": "value", "value": "i am furious about this"}, + ], + ) + assert r.status_code == 200 + payload = r.get_json()["response"] + assert "figure" in payload["data-editor-prob"] + assert payload["current-datapoint"]["data"]["text"] == "i am furious about this" + assert payload["current-datapoint"]["data"]["tokens"] + + # Generate: current datapoint text -> counterfactual results table + store of new texts. + gen_key = _find_callback_key(app, "counterfactual-results") + r = _post_callback( + app, + gen_key, + inputs=[{"id": "counterfactual-generate-btn", "property": "n_clicks", "value": 1}], + state=[ + {"id": "current-datapoint", "property": "data", "value": {"text": "i am so happy today"}}, + {"id": "counterfactual-cfg-num_examples", "property": "value", "value": 3}, + {"id": "counterfactual-cfg-max_flips", "property": "value", "value": 2}, + {"id": "counterfactual-cfg-tokens_to_ignore", "property": "value", "value": ""}, + ], + ) + assert r.status_code == 200 + payload = r.get_json()["response"] + assert payload["counterfactual-results"]["children"] is not None + assert isinstance(payload["counterfactual-store"]["data"], list) diff --git a/tests/unit_tests/compute/generators/test_hotflip.py b/tests/unit_tests/compute/generators/test_hotflip.py new file mode 100644 index 00000000..2ec8a3c3 --- /dev/null +++ b/tests/unit_tests/compute/generators/test_hotflip.py @@ -0,0 +1,143 @@ +"""Unit tests for the HotFlip counterfactual generator. + +A deterministic linear fake model (2-D embeddings = per-class logit contributions) lets us assert +the full HotFlip contract — replacement selection, prediction flip, minimality — without a real +transformer or torch. +""" + +import unittest + +import numpy as np + +from shapash.compute.generators import Counterfactual, HotFlipGenerator +from shapash.compute.generators.cf_utils import is_prediction_flip, prediction_difference +from shapash.model.base import ( + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, +) + +# Embedding == [neg_logit_contribution, pos_logit_contribution] for each vocab token. +_EMB = { + "this": [0.1, 0.1], + "is": [0.1, 0.1], + "great": [0.0, 2.0], + "good": [0.0, 1.0], + "terrible": [3.0, 0.0], + "awful": [3.0, 0.0], + "ok": [0.2, 0.2], +} +_VOCAB = list(_EMB) + + +class LinearFakeModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): + """Bag-of-embeddings linear classifier: logits = sum of per-token 2-vectors.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + self._matrix = np.array([_EMB[t] for t in _VOCAB], dtype=float) + + def _logits(self, text): + toks = [t for t in text.split() if t in _EMB] + return np.sum([_EMB[t] for t in toks], axis=0) if toks else np.zeros(2) + + def predict(self, texts): + out = [] + for t in texts: + z = self._logits(t) + e = np.exp(z - z.max()) + out.append(e / e.sum()) + return np.vstack(out) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + def get_embedding_table(self): + return list(_VOCAB), self._matrix + + def embed(self, texts): + return np.vstack([self._logits(t) for t in texts]) + + def token_gradients(self, text, target_class): + # Linear model ⇒ d logit[target] / d e_i = one-hot(target); identical per token. + toks = text.split() + grad = np.zeros(2) + grad[target_class] = 1.0 + return toks, np.tile(grad, (len(toks), 1)) + + +class TestCfUtils(unittest.TestCase): + def test_is_prediction_flip_any(self): + self.assertTrue(is_prediction_flip(np.array([0.2, 0.8]), np.array([0.7, 0.3]))) + self.assertFalse(is_prediction_flip(np.array([0.2, 0.8]), np.array([0.4, 0.6]))) + + def test_is_prediction_flip_targeted(self): + self.assertTrue(is_prediction_flip(np.array([0.8, 0.2]), np.array([0.1, 0.9]), target_class=1)) + self.assertFalse(is_prediction_flip(np.array([0.8, 0.2]), np.array([0.1, 0.9]), target_class=0)) + + def test_prediction_difference(self): + self.assertAlmostEqual(prediction_difference(np.array([0.9, 0.1]), np.array([0.4, 0.6]), 0), 0.5) + + +class TestHotFlipCompat(unittest.TestCase): + def test_incompatible_without_gradients(self): + class PredictOnly(TextModel): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + self.assertFalse(HotFlipGenerator.is_compatible(PredictOnly())) + + def test_compatible_with_full_model(self): + self.assertTrue(HotFlipGenerator.is_compatible(LinearFakeModel())) + + +class TestHotFlipGenerate(unittest.TestCase): + def setUp(self): + self.model = LinearFakeModel() + self.gen = HotFlipGenerator(self.model) + + def test_config_spec_defaults(self): + spec = self.gen.config_spec() + self.assertEqual(spec["num_examples"].default, 5) + self.assertEqual(spec["max_flips"].default, 3) + self.assertEqual(self.gen.resolve_config({"max_flips": 2})["max_flips"], 2) + + def test_generates_flip(self): + cfs = self.gen.generate("this is great", config={"num_examples": 3, "max_flips": 1}) + self.assertTrue(cfs, "expected at least one counterfactual") + cf = cfs[0] + self.assertIsInstance(cf, Counterfactual) + self.assertEqual(cf.orig_label, "pos") + self.assertEqual(cf.new_label, "neg") + # Replacement must be one of the strongly-negative tokens. + self.assertTrue(all(new in {"terrible", "awful"} for _, _, new in cf.substitutions)) + # Verify the reported flip actually holds under the model. + self.assertTrue( + is_prediction_flip(self.model.predict([cf.original_text])[0], self.model.predict([cf.new_text])[0]) + ) + + def test_minimality(self): + cfs = self.gen.generate("this is great", config={"num_examples": 10, "max_flips": 3}) + sets = [frozenset(cf.flipped_positions) for cf in cfs] + for i, a in enumerate(sets): + for j, b in enumerate(sets): + if i != j: + self.assertFalse(a < b, "returned a non-minimal (superset) counterfactual") + + def test_tokens_to_ignore(self): + # Ignoring every content token yields no flips. + cfs = self.gen.generate("this is great", config={"tokens_to_ignore": ["this", "is", "great"], "max_flips": 2}) + self.assertEqual(cfs, []) + + def test_no_flip_returns_empty(self): + # Already strongly negative; ignore the negative words so nothing can push further. + cfs = self.gen.generate("terrible awful", config={"tokens_to_ignore": ["terrible", "awful"]}) + self.assertEqual(cfs, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/model/test_model_base.py b/tests/unit_tests/model/test_model_base.py new file mode 100644 index 00000000..f4057ccb --- /dev/null +++ b/tests/unit_tests/model/test_model_base.py @@ -0,0 +1,84 @@ +"""Unit tests for the model capability layer (no torch/transformers required). + +Uses a tiny deterministic fake model implementing the capability mixins, so the interface and the +``has_capabilities`` discovery helper are exercised without a real transformer. +""" + +import unittest + +import numpy as np + +from shapash.model.base import ( + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, + has_capabilities, +) + + +class PredictOnlyModel(TextModel): + """Minimal model implementing only ``predict``.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + +class FullModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): + """Model implementing every capability.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.3, 0.7], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + def get_embedding_table(self): + return ["a", "b"], np.eye(2) + + def embed(self, texts): + return np.zeros((len(texts), 2)) + + def token_gradients(self, text, target_class): + toks = text.split() + return toks, np.ones((len(toks), 2)) + + +class TestCapabilities(unittest.TestCase): + def test_predict_only_lacks_optional_capabilities(self): + m = PredictOnlyModel() + self.assertIsInstance(m, TextModel) + self.assertFalse(has_capabilities(m, SupportsGradients)) + self.assertFalse(has_capabilities(m, SupportsEmbeddings)) + self.assertFalse(has_capabilities(m, SupportsTokenization)) + + def test_full_model_has_all_capabilities(self): + m = FullModel() + self.assertTrue(has_capabilities(m, SupportsTokenization)) + self.assertTrue(has_capabilities(m, SupportsEmbeddings)) + self.assertTrue(has_capabilities(m, SupportsGradients)) + self.assertTrue(has_capabilities(m, SupportsGradients, SupportsEmbeddings)) + + def test_predict_shape_and_n_classes(self): + m = FullModel() + probs = m.predict(["x", "y", "z"]) + self.assertEqual(probs.shape, (3, 2)) + self.assertEqual(m.n_classes, 2) + + def test_n_classes_none_without_labels(self): + m = FullModel() + m.label_names = None + self.assertIsNone(m.n_classes) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 5aea9023..618b1c9e 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -445,10 +445,9 @@ def test_sentence_highlight_present(self): def test_waterfall_controls_present(self): ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("show-waterfall", ids) + # Waterfall is now a tab (no show/hide switch); the threshold slider + graph live in it. self.assertIn("waterfall-threshold", ids) self.assertIn("waterfall-graph", ids) - self.assertIn("waterfall-container", ids) def test_dataset_table_no_ground_truth_by_default(self): table = self._find_component(self.webapp.app.layout, "dataset-table") diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py new file mode 100644 index 00000000..106c353c --- /dev/null +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -0,0 +1,231 @@ +"""Unit tests for the What-if Lab wiring in ``NlpWebApp`` (no real model, no server). + +A fake explainer/engine with synthetic contributions drives layout construction and capability +gating, verifying that the data-editor and counterfactual components mount only when the engine +reports the matching capabilities. +""" + +import unittest + +import numpy as np +import pandas as pd + +from shapash.backend.nlp_backend import NlpContributions +from shapash.compute.generators.base import Counterfactual, IntField, TokenListField +from shapash.webapp.nlp_app import NlpWebApp + +LABEL_NAMES = ["neg", "pos"] + + +def _contributions() -> NlpContributions: + token_strings = [["i", "am", "happy"], ["this", "is", "bad"]] + values = [np.random.randn(3, 2), np.random.randn(3, 2)] + base_values = np.zeros((2, 2)) + c = NlpContributions(token_strings=token_strings, values=values, base_values=base_values) + c.label_names = LABEL_NAMES + c.index = pd.RangeIndex(2) + return c + + +class FakeEngine: + """Minimal explainer/engine stand-in exposing the InteractiveEngine surface + compiled data.""" + + def __init__(self, can_edit: bool, can_cf: bool): + self._can_edit = can_edit + self._can_cf = can_cf + self.label_names = LABEL_NAMES + self.texts = pd.Series(["i am happy", "this is bad"], index=pd.RangeIndex(2)) + self.contributions = _contributions() + self.y_pred = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="prediction") + self.y_prob = pd.DataFrame({"neg": [0.2, 0.8], "pos": [0.8, 0.2]}, index=pd.RangeIndex(2)) + self.y_true = None + + def can_edit(self): + return self._can_edit + + def can_counterfactual(self): + return self._can_cf + + def cf_config_spec(self): + return { + "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), + "max_flips": IntField(label="Max token flips", default=3, minimum=1, maximum=5), + "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), + } + + def predict(self, text): + return "pos", {"neg": 0.3, "pos": 0.7} + + def explain_text(self, text): + c = _contributions() + return c, "pos", {"neg": 0.3, "pos": 0.7} + + def generate_counterfactuals(self, text, config=None): + return [ + Counterfactual( + original_text=text, + new_text=text.replace("happy", "bad"), + tokens=["i", "am", "happy"], + flipped_positions=[2], + substitutions=[(2, "happy", "bad")], + orig_label="pos", + new_label="neg", + orig_prob=0.8, + new_prob=0.7, + prob_delta=0.5, + ) + ] + + +def _collect_ids(node, found): + """Recursively collect all string component ids in a Dash layout tree.""" + cid = getattr(node, "id", None) + if isinstance(cid, str): + found.add(cid) + children = getattr(node, "children", None) + if children is None: + return + if isinstance(children, (list, tuple)): + for ch in children: + _collect_ids(ch, found) + else: + _collect_ids(children, found) + + +class TestWhatIfMounting(unittest.TestCase): + def _ids(self, engine): + app = NlpWebApp(engine) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + def test_full_capabilities_mount_both(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("data-editor-input", ids) + self.assertIn("data-editor-predict-btn", ids) + self.assertIn("counterfactual-generate-btn", ids) + self.assertEqual(len(app._components), 2) + + def test_counterfactual_config_controls_in_initial_layout(self): + # The generate callback's State references these ids, so they must exist in the initial + # layout (rendered from the engine's cf_config_spec), not be injected by a later callback. + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + for name in ("num_examples", "max_flips", "tokens_to_ignore"): + self.assertIn(f"counterfactual-cfg-{name}", ids) + + def test_edit_only_mounts_editor_not_counterfactual(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) + self.assertIn("data-editor-input", ids) + self.assertNotIn("counterfactual-generate-btn", ids) + self.assertEqual(len(app._components), 1) + + def test_no_capabilities_hides_whatif_lab(self): + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertNotIn("data-editor-input", ids) + self.assertNotIn("counterfactual-generate-btn", ids) + self.assertNotIn("whatif-apply-store", ids) + self.assertEqual(app._components, []) + + def test_callbacks_registered_when_mounted(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("data-editor-prob.figure", outputs) + self.assertIn("counterfactual-results.children", outputs) + + +def _callback_binding_ids(app, output_substr): + """Return the (id, property) pairs bound as inputs+state for the callback with this output.""" + for key, spec in app.app.callback_map.items(): + if output_substr in key: + pairs = [(i["id"], i["property"]) for i in spec["inputs"]] + pairs += [(s["id"], s["property"]) for s in spec.get("state", [])] + return pairs + raise KeyError(output_substr) + + +class TestThreePanelLayout(unittest.TestCase): + """The LIT-style three-panel shell: tab groups, mounted bodies, and the current-datapoint store.""" + + def _ids(self, engine, **kwargs): + app = NlpWebApp(engine, **kwargs) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + def test_full_tab_groups_when_all_panels_available(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + self.assertEqual( + app._tab_groups, + { + "left-tabs": ["table", "scatter", "editor"], + "upper-right-tabs": ["importance", "counterfactual"], + "lower-right-tabs": ["highlight", "waterfall"], + }, + ) + # Every tab body is mounted in the DOM (visibility is toggled, not the mount). + for body_id in ( + "left-tabs-body-table", + "left-tabs-body-scatter", + "left-tabs-body-editor", + "upper-right-tabs-body-importance", + "upper-right-tabs-body-counterfactual", + "lower-right-tabs-body-highlight", + "lower-right-tabs-body-waterfall", + ): + self.assertIn(body_id, ids) + + def test_tabs_degrade_without_scatter_or_whatif(self): + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertEqual(app._tab_groups["left-tabs"], ["table"]) + self.assertEqual(app._tab_groups["upper-right-tabs"], ["importance"]) + self.assertEqual(app._tab_groups["lower-right-tabs"], ["highlight", "waterfall"]) + self.assertNotIn("left-tabs-body-scatter", ids) + self.assertNotIn("left-tabs-body-editor", ids) + self.assertNotIn("upper-right-tabs-body-counterfactual", ids) + + def test_counterfactual_tab_only_with_editor(self): + # Editor without CF: editor tab present on the left, no counterfactual tab on the right. + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) + self.assertIn("editor", app._tab_groups["left-tabs"]) + self.assertNotIn("counterfactual", app._tab_groups["upper-right-tabs"]) + + def test_selection_bar_present(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + self.assertIn("selection-summary", ids) + self.assertIn("word-filter-clear-btn", ids) + self.assertIn("scatter-clear-btn", ids) + + def test_scatter_clear_absent_without_scatter(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("word-filter-clear-btn", ids) + self.assertNotIn("scatter-clear-btn", ids) + + def test_current_datapoint_store_present(self): + _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertIn("current-datapoint", ids) + + def test_detail_panels_read_current_datapoint(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + # Highlight and waterfall render off the shared primary-selection store... + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "sentence-highlight.children")) + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "waterfall-graph.figure")) + # ...and counterfactuals generate from it too (so a selected row works, not only editor text). + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "counterfactual-results.children")) + + def test_current_datapoint_written_by_row_and_editor(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + # The table-selection writer keys off the selected row. + self.assertIn(("dataset-table", "selectedRows"), _callback_binding_ids(app, "current-datapoint.data")) + # The editor's Predict also writes it (allow_duplicate) — its combined key carries the prob figure. + editor_key = next(k for k in app.app.callback_map if "data-editor-prob.figure" in k) + self.assertIn("current-datapoint.data", editor_key) + + def test_tab_toggle_callbacks_registered(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("left-tabs-body-table.style", outputs) + self.assertIn("lower-right-tabs-body-waterfall.style", outputs) + + +if __name__ == "__main__": + unittest.main() From bf425ff7bcb1455e2d78a66ae378c12b5e6c3672 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 11:26:15 +0200 Subject: [PATCH 039/144] feat: move model error switch and highlight errors in embeddings scatterplot --- shapash/webapp/nlp_app.py | 87 ++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 5c92e618..b7aa812b 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -345,17 +345,6 @@ def _build_layout(self) -> None: width="auto", className="align-self-center", ), - dbc.Col( - dbc.Switch( - id="errors-only-switch", - label="Model Errors", - value=False, - className="small mb-0", - style={} if has_gt else {"display": "none"}, - ), - width="auto", - className="align-self-center ms-auto", - ), ], className="align-items-center mb-2", style={"flex": "0 0 auto"}, @@ -464,6 +453,17 @@ def _build_layout(self) -> None: style={"display": "none", "fontSize": "0.8em"}, ) ) + # Right-aligned (ms-auto) and here — rather than inside the Dataset tab body — so it stays + # usable from the Embeddings tab too, where it now also drives point highlighting. + selection_children.append( + dbc.Switch( + id="errors-only-switch", + label="Model Errors", + value=False, + className="small mb-0 ms-auto", + style={} if has_gt else {"display": "none"}, + ) + ) selection_bar = html.Div( selection_children, style={ @@ -799,11 +799,15 @@ def filter_table(selected_indices, word_filter, errors_only): Input("color-by", "value"), Input("scatter-word-select", "value"), Input("class-selector", "value"), + Input("errors-only-switch", "value"), ], ) - def update_scatter_color(color_by, words, label_idx): + def update_scatter_color(color_by, words, label_idx, errors_only): return self._build_scatter_fig( - color_by or "prediction", words=words or [], label_idx=int(label_idx or 0) + color_by or "prediction", + words=words or [], + label_idx=int(label_idx or 0), + errors_only=bool(errors_only), ) @self.app.callback( @@ -927,18 +931,59 @@ def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: result[i] += vals[j] return result - def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, label_idx: int = 0) -> go.Figure: + def _error_mask(self) -> np.ndarray | None: + """Boolean array, ``True`` where the prediction disagrees with the ground truth. + + ``None`` when either is unavailable. Matches the string comparison used by the + "Model Errors" table filter so both stay consistent. + """ + y_true = getattr(self.explainer, "y_true", None) + y_pred = self.explainer.y_pred + if y_true is None or y_pred is None: + return None + return np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) + + @staticmethod + def _emphasize_errors( + idx_arr: np.ndarray, + base_opacity: float, + base_size: float, + error_mask: np.ndarray | None, + ) -> tuple[list[float] | float, list[float] | float]: + """Per-point opacity/size that pops model errors and shadows everything else. + + Keeps the caller's coloring untouched — only opacity and marker size change — + so points stay grouped/colored however the "Color by" dropdown already draws them. + Returns scalars (the unmodified base values) when ``error_mask`` is unavailable. + """ + if error_mask is None or len(idx_arr) == 0: + return base_opacity, base_size + is_error = error_mask[idx_arr] + opacity = np.where(is_error, max(base_opacity, 0.9), 0.12).tolist() + size = np.where(is_error, base_size + 3, max(base_size - 2, 3)).tolist() + return opacity, size + + def _build_scatter_fig( + self, + color_by: str, + words: list[str] | None = None, + label_idx: int = 0, + errors_only: bool = False, + ) -> go.Figure: """2-D scatter coloured by prediction, ground-truth label, or word SHAP contribution. One trace per class for label-based coloring so the legend works correctly and Plotly's box/lasso select dims unselected points across all traces. Word-contribution mode uses a single diverging-colorscale trace. - ``customdata`` always stores the original sample index. + ``customdata`` always stores the original sample index. When ``errors_only`` is + set, misclassified points are emphasized (larger, opaque) and the rest are + shadowed (small, faint) without altering their color. """ n = len(self.explainer.texts) contrib = self.explainer.contributions texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self.explainer.texts] xy = self._scatter_xy + error_mask = self._error_mask() if errors_only else None if color_by == "word_contribution" and words: contributions = sum(self._word_contributions(w, label_idx) for w in words) @@ -952,13 +997,16 @@ def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, labe # constant across word additions/removals. With a stable `uirevision`, a changing WebGL # (Scattergl) trace count leaves ghost/"shadow" points from the previous render — keeping # exactly two traces avoids that. + absent_opacity, absent_size = self._emphasize_errors(absent_mask, 0.35, 5, error_mask) + present_opacity, present_size = self._emphasize_errors(present_mask, 0.9, 9, error_mask) + # Gray context layer — absent points (no selected word contributes to them). fig.add_trace( go.Scattergl( x=xy[absent_mask, 0].tolist(), y=xy[absent_mask, 1].tolist(), mode="markers", - marker=dict(color="#b0b0b0", size=5, opacity=0.35), + marker=dict(color="#b0b0b0", size=absent_size, opacity=absent_opacity), customdata=absent_mask.reshape(-1, 1).tolist(), text=[texts_short[j] for j in absent_mask], hovertemplate="%{text}absent", @@ -976,8 +1024,8 @@ def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, labe colorscale="RdBu", cmin=-max_abs, cmax=max_abs, - size=9, - opacity=0.9, + size=present_size, + opacity=present_opacity, colorbar=dict( title=dict(text=colorbar_title, side="right"), thickness=12, @@ -1018,12 +1066,13 @@ def _build_scatter_fig(self, color_by: str, words: list[str] | None = None, labe if not mask: continue mask_arr = np.array(mask) + opacity, size = self._emphasize_errors(mask_arr, 0.75, 7, error_mask) fig.add_trace( go.Scattergl( x=xy[mask_arr, 0].tolist(), y=xy[mask_arr, 1].tolist(), mode="markers", - marker=dict(color=_PALETTE[i % len(_PALETTE)], size=7, opacity=0.75), + marker=dict(color=_PALETTE[i % len(_PALETTE)], size=size, opacity=opacity), customdata=mask_arr.reshape(-1, 1).tolist(), text=[texts_short[j] for j in mask], hovertemplate=f"{name}
%{{text}}", From 67ea30f748c438e5f23b70cd3dcf300e00d67daa Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:00:53 +0200 Subject: [PATCH 040/144] refactor: add serve config to make script more customizable --- demo/serve_nlp_ext.py | 181 ++++++++++++++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 43 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index b771ab55..7bb34ba8 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -21,10 +21,27 @@ The model runs on CPU when no GPU is present, so this serves fine on a plain box. +Model and dataset are configurable (see ``ServeConfig``): swap ``--model-name`` for any HF +sequence-classification checkpoint (its label names are read from the model's own config) and +``--dataset-name`` for any HF ``datasets`` classification dataset (point ``--text-column`` / +``--label-column`` at the right fields if they aren't ``text`` / ``label``). The on-disk cache is +keyed by a hash of the input texts only (not the model), so each ``--model-name`` gets its own +subdirectory under ``--cache-dir`` — otherwise switching models with the same dataset/``--n`` +would silently reload another model's cached contributions/embeddings. + +Dataset and model label strings aren't guaranteed to match even when their class order does (e.g. +a dataset's ``ClassLabel`` names ``"neg"``/``"pos"`` vs. a model's ``config.id2label`` values +``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the +model's spelling so predicted vs. ground-truth comparisons in the webapp line up. + Usage ----- python demo/serve_nlp_ext.py [--n 100] [--cache-dir demo/nlp_ext_cache] [--port 8051] python demo/serve_nlp_ext.py --recompute # ignore the cache and recompute + python demo/serve_nlp_ext.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ + --dataset-name sst2 --dataset-split validation --text-column sentence + python demo/serve_nlp_ext.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ + --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). """ @@ -32,7 +49,9 @@ from __future__ import annotations import argparse +import json import logging +from dataclasses import dataclass, field from pathlib import Path import datasets @@ -49,12 +68,115 @@ from shapash.model import HFClassifierModel _HERE = Path(__file__).parent -_MODEL_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" -_LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] logger = logging.getLogger("serve_nlp_ext") +@dataclass +class ServeConfig: + """Everything needed to load data + model and serve the What-if webapp. + + A single object (rather than argparse's ``Namespace``) so the loading helpers below + stay reusable from a REPL too — see the module docstring on iterating without restarting. + """ + + model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" + dataset_name: str = "dair-ai/emotion" + dataset_split: str = "train" + text_column: str = "text" + label_column: str = "label" + label_map: dict[str, str] = field(default_factory=dict) + n: int = 100 + cache_dir: Path = _HERE / "nlp_ext_cache" + port: int = 8051 + host: str = "0.0.0.0" # noqa: S104 + recompute: bool = False + + +def _model_cache_dir(config: ServeConfig) -> Path: + """Return ``config.cache_dir`` namespaced by model. + + The compile/projection caches are keyed by a hash of the input texts only (see + ``_hash_texts``), so two different models run over the same dataset/``--n`` would + otherwise collide on the same cache file. A per-model subdirectory keeps them apart. + """ + return config.cache_dir / config.model_name.replace("/", "__") + + +def parse_args(argv: list[str] | None = None) -> ServeConfig: + """Parse CLI args into a ``ServeConfig``, defaulting every flag from the dataclass.""" + defaults = ServeConfig() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--model-name", default=defaults.model_name, help="HF model id (label names are read from its config)." + ) + parser.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") + parser.add_argument("--dataset-split", default=defaults.dataset_split) + parser.add_argument("--text-column", default=defaults.text_column, help="Column holding the raw text.") + parser.add_argument("--label-column", default=defaults.label_column, help="Column holding the ground-truth label.") + parser.add_argument( + "--label-map", + type=json.loads, + default=defaults.label_map, + help=( + "JSON object renaming dataset label strings to match the model's own label names, e.g. " + '\'{"neg": "NEGATIVE", "pos": "POSITIVE"}\'. Needed when the dataset and model disagree on ' + "label spelling/casing even though their class order matches." + ), + ) + parser.add_argument("--n", type=int, default=defaults.n, help="Number of samples to load.") + parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) + parser.add_argument("--port", type=int, default=defaults.port) + parser.add_argument("--host", default=defaults.host) + parser.add_argument( + "--recompute", + action="store_true", + help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", + ) + args = parser.parse_args(argv) + return ServeConfig(**vars(args)) + + +def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: + """Load ``config.n`` samples and their display label strings from the configured dataset. + + Ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when + present (the usual shape for HF classification datasets); otherwise the raw values are + stringified as-is. ``config.label_map`` then renames them to match the model's label + names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model + ``"NEGATIVE"``) despite sharing the same class order. + """ + dataset = datasets.load_dataset(config.dataset_name, split=config.dataset_split) + sentences = dataset[config.text_column][: config.n] + raw_labels = dataset[config.label_column][: config.n] + names = getattr(dataset.features.get(config.label_column), "names", None) + y_true = [names[i] for i in raw_labels] if names is not None else [str(v) for v in raw_labels] + if config.label_map: + y_true = [config.label_map.get(v, v) for v in y_true] + logger.info("Loaded %d samples from %s (split=%s)", len(sentences), config.dataset_name, config.dataset_split) + return sentences, y_true + + +def load_model(config: ServeConfig) -> HFClassifierModel: + """Load the tokenizer + classifier on the best available device. + + Label names aren't passed explicitly: ``HFClassifierModel`` reads them from the model's + own ``config.id2label`` when none are given, so a new ``--model-name`` brings its own + classes automatically. + """ + if torch.cuda.is_available(): + device = "cuda" + logger.info("CUDA available — using GPU: %s", torch.cuda.get_device_name(0)) + else: + device = "cpu" + logger.info("No CUDA device found — using CPU") + + tokenizer = transformers.AutoTokenizer.from_pretrained(config.model_name, use_fast=True) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) + logger.info("Loaded model %s on device: %s", config.model_name, next(classifier.parameters()).device) + return HFClassifierModel(classifier, tokenizer) + + def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> np.ndarray: """Return a 2-D PaCMAP projection, reading from / writing to ``cache_dir``. @@ -77,18 +199,8 @@ def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> def main() -> None: - """Load the model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--n", type=int, default=100, help="Number of samples to load.") - parser.add_argument("--cache-dir", type=Path, default=_HERE / "nlp_ext_cache") - parser.add_argument("--port", type=int, default=8051) - parser.add_argument("--host", default="0.0.0.0") # noqa: S104 - parser.add_argument( - "--recompute", - action="store_true", - help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", - ) - args = parser.parse_args() + """Load config + data + model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" + config = parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") # Quiet noisy third-party INFO logs (HF Hub freshness checks, download chatter) so the @@ -96,49 +208,32 @@ def main() -> None: for noisy in ("httpx", "httpcore", "urllib3", "filelock", "datasets"): logging.getLogger(noisy).setLevel(logging.WARNING) - dataset = datasets.load_dataset("dair-ai/emotion", split="train") - sentences = dataset["text"][: args.n] - labels = dataset["label"][: args.n] - y_true = [_LABEL_NAMES[i] for i in labels] - logger.info("Loaded %d samples from dair-ai/emotion", len(sentences)) + sentences, y_true = load_data(config) + cache_dir = _model_cache_dir(config) text_hash = _hash_texts(list(sentences)) - if args.recompute: + if config.recompute: # Drop this text set's cached artifacts so the steps below recompute + overwrite. logger.info("--recompute: dropping cached SHAP + projection for these texts") - (args.cache_dir / f"{text_hash}.pkl").unlink(missing_ok=True) - (args.cache_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) - - # Select the device and report it — GPU when available, CPU otherwise. - if torch.cuda.is_available(): - device = "cuda" - logger.info("CUDA available — using GPU: %s", torch.cuda.get_device_name(0)) - else: - device = "cpu" - logger.info("No CUDA device found — using CPU") - - tokenizer = transformers.AutoTokenizer.from_pretrained(_MODEL_NAME, use_fast=True) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(_MODEL_NAME).to(device) - # Confirm the weights actually landed on the intended device. - logger.info("Loaded model %s on device: %s", _MODEL_NAME, next(classifier.parameters()).device) + (cache_dir / f"{text_hash}.pkl").unlink(missing_ok=True) + (cache_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) - # Full-capability adapter: predict + embeddings + gradients (what HotFlip needs). - model = HFClassifierModel(classifier, tokenizer, label_names=_LABEL_NAMES) + model = load_model(config) - xpl = NlpExplainer(model, label_names=_LABEL_NAMES, cf_generator=HotFlipGenerator(model)) + xpl = NlpExplainer(model, label_names=model.label_names, cf_generator=HotFlipGenerator(model)) # Both steps load from cache_dir if a cache for these exact texts exists, otherwise they # compute and write it. So the first run is slow; later runs only pay the model load. - compile_cache = args.cache_dir / f"{text_hash}.pkl" + compile_cache = cache_dir / f"{text_hash}.pkl" if compile_cache.exists(): logger.info("SHAP cache hit — loading %s", compile_cache) else: logger.info("SHAP cache miss — computing contributions for %d texts (this is the slow part)", len(sentences)) - xpl.compile(sentences, y_true=y_true, cache_dir=args.cache_dir) - projected = load_or_project(sentences, model, args.cache_dir) + xpl.compile(sentences, y_true=y_true, cache_dir=cache_dir) + projected = load_or_project(sentences, model, cache_dir) logger.info("can_edit=%s | can_counterfactual=%s", xpl.can_edit(), xpl.can_counterfactual()) - logger.info("Serving on http://%s:%d (Ctrl+C to stop)", args.host, args.port) - xpl.run_app(port=args.port, debug=False, host=args.host, scatter_xy=projected) + logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) + xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) if __name__ == "__main__": From 3a30f9057893e456c78e4c55146711917add2579 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:01:40 +0200 Subject: [PATCH 041/144] chore: put device and batch size inside model pipeline --- shapash/model/hf.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 111cbb2e..fb1dca9b 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -17,6 +17,8 @@ from __future__ import annotations +import warnings + import numpy as np from shapash._optional import import_optional_module @@ -111,10 +113,22 @@ class HFClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, Sup label_names : list[str] or None Class names in class-index (id) order. When ``None``, read from ``classifier.config.id2label``. batch_size : int, optional - Batch size for ``predict`` and ``embed``. Default 32. + Batch size for ``predict``, ``embed`` and the SHAP pipeline. Default 32. + device : int or str or torch.device or None, optional + Device for the SHAP pipeline (e.g. ``0``, ``"cuda"``, ``"cpu"``). When ``None`` (default), + the pipeline inherits the device the classifier already lives on, so CPU-only environments + work with no configuration. An explicit CUDA request is silently downgraded to CPU (with a + warning) when no GPU is available. """ - def __init__(self, classifier, tokenizer, label_names: list[str] | None = None, batch_size: int = 32) -> None: + def __init__( + self, + classifier, + tokenizer, + label_names: list[str] | None = None, + batch_size: int = 32, + device: int | str | object | None = None, + ) -> None: if label_names is None: id2label = getattr(getattr(classifier, "config", None), "id2label", None) if id2label: @@ -123,6 +137,7 @@ def __init__(self, classifier, tokenizer, label_names: list[str] | None = None, self.classifier = classifier self.tokenizer = tokenizer self.batch_size = batch_size + self.device = device self._pipeline = None # lazily built for SHAP # ------------------------------------------------------------------ @@ -144,6 +159,24 @@ def predict(self, texts: list[str]) -> np.ndarray: out.append(probs.cpu().numpy()) return np.vstack(out) + def _resolve_pipeline_device(self): + """Pick the pipeline device, degrading an unavailable-GPU request to CPU with a warning.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + if self.device is None: + # Inherit whatever device the classifier already lives on (CPU-safe by construction). + return self.classifier.device + wants_cuda = (isinstance(self.device, str) and self.device.startswith("cuda")) or ( + isinstance(self.device, int) and self.device >= 0 + ) + if wants_cuda and not torch.cuda.is_available(): + warnings.warn( + f"Requested device {self.device!r} but no CUDA GPU is available; " + "falling back to CPU for the SHAP pipeline.", + stacklevel=2, + ) + return torch.device("cpu") + return self.device + @property def shap_callable(self): """A ``text-classification`` pipeline built from the classifier, for SHAP's TextMasker.""" @@ -154,6 +187,9 @@ def shap_callable(self): model=self.classifier, tokenizer=self.tokenizer, top_k=None, + device=self._resolve_pipeline_device(), + batch_size=self.batch_size, + truncation=True, ) return self._pipeline From 2e19416f24f57a5d7eeffe03c6d9c2e4491c1c39 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:02:06 +0200 Subject: [PATCH 042/144] feat: add plot confusion matrix --- shapash/plots/plot_confusion_matrix.py | 100 +++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 shapash/plots/plot_confusion_matrix.py diff --git a/shapash/plots/plot_confusion_matrix.py b/shapash/plots/plot_confusion_matrix.py new file mode 100644 index 00000000..abedd1fc --- /dev/null +++ b/shapash/plots/plot_confusion_matrix.py @@ -0,0 +1,100 @@ +"""Confusion-matrix heatmap for NLP error analysis.""" + +from __future__ import annotations + +import numpy as np +from plotly import graph_objs as go + +_COLORSCALE = "Blues" + + +def plot_confusion_matrix( + cm: np.ndarray, + labels: list[str], + normalize: str | None = None, + title: str = "Confusion matrix", + width: int | None = None, + height: int | None = None, +) -> go.Figure: + """Heatmap of a confusion matrix with click-identifiable cells. + + Rows are the **true** class, columns are the **predicted** class (the standard + scikit-learn orientation). Each cell carries ``customdata = [pred_idx, true_idx]`` + so a Dash ``clickData`` handler can recover which (predicted, true) pair was clicked. + + Parameters + ---------- + cm : numpy.ndarray + Square confusion-matrix counts of shape ``(n_classes, n_classes)`` with + ``cm[true, pred]`` giving the number of samples whose true class is ``true`` + and predicted class is ``pred``. + labels : list of str + Class display names, ordered to match the rows/columns of ``cm``. + normalize : {None, "true"}, optional + If ``"true"``, divide each row by its sum so cells show recall (the fraction + of each true class routed to every predicted class). If ``None`` (default), + show raw counts. + title : str + Figure title. + width : int, optional + Figure width in pixels. Defaults to a size scaled to the number of classes. + height : int, optional + Figure height in pixels. Defaults to a size scaled to the number of classes. + + Returns + ------- + plotly.graph_objs.Figure + """ + counts = np.asarray(cm, dtype=float) + n = counts.shape[0] + + if normalize == "true": + row_sums = counts.sum(axis=1, keepdims=True) + # Guard against empty true-classes (row sum 0) producing NaNs. + z = np.divide(counts, row_sums, out=np.zeros_like(counts), where=row_sums != 0) + text = np.array([[f"{v:.0%}" for v in row] for row in z]) + hover_val = "%{z:.1%}" + colorbar_title = "recall" + else: + z = counts + text = np.array([[f"{int(v)}" for v in row] for row in counts]) + hover_val = "%{z:.0f}" + colorbar_title = "count" + + # customdata[true][pred] = [pred_idx, true_idx] — the order a click handler reads. + customdata = np.empty((n, n, 2), dtype=int) + for i in range(n): + for j in range(n): + customdata[i, j] = (j, i) + + fig = go.Figure( + go.Heatmap( + z=z, + x=labels, + y=labels, + customdata=customdata, + text=text, + texttemplate="%{text}", + colorscale=_COLORSCALE, + colorbar=dict(title=colorbar_title), + hovertemplate=("true: %{y}
predicted: %{x}
" + hover_val + ""), + ) + ) + + size = max(360, 90 * n + 120) + # The x-axis sits on top, so keep room for its labels even without a title. + top_margin = 80 if title else 40 + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title="Predicted class", + yaxis_title="True class", + width=width or size, + height=height or size, + plot_bgcolor="white", + # Put the first true class at the top so the diagonal reads top-left to bottom-right. + yaxis=dict(autorange="reversed", automargin=True), + xaxis=dict(automargin=True, side="top"), + margin=dict(l=20, r=20, t=top_margin, b=20), + ) + + return fig From 95717edbd0134836d012637e33c9766dcabbd390 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:02:46 +0200 Subject: [PATCH 043/144] feat: add error analysis tab and review filtering layers --- shapash/webapp/nlp_app.py | 281 +++++++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 7 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index b7aa812b..9f22d17f 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -20,6 +20,7 @@ from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate +from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance @@ -54,6 +55,57 @@ ] +def _compose_selection( + selected_indices: list[int] | None, + cell_indices: list[int] | None, + error_positions: set[int] | None, +) -> list[int] | None: + """Intersect the app's active sample filters into one index list. + + Each argument is an independent filter that may be inactive (``None``): the scatter box/lasso + selection, the confusion-matrix cell, and — when the errors-only switch is on — the set of + misclassified positions. Active filters intersect; returns ``None`` when none are active. + """ + combined = selected_indices + if cell_indices is not None: + cell_set = set(cell_indices) + combined = list(cell_indices) if combined is None else [i for i in combined if i in cell_set] + if error_positions is not None: + combined = sorted(error_positions) if combined is None else [i for i in combined if i in error_positions] + return combined + + +def _cell_from_click(click_data: dict | None, name_to_idx: dict[str, int]) -> tuple[int, int] | None: + """Resolve a confusion-matrix cell click to ``(pred_idx, true_idx)``. + + Heatmap ``clickData`` does not reliably include ``customdata`` the way scatter traces do, so + prefer it when present but fall back to mapping the predicted (``x``) and true (``y``) axis label + names back to their class indices. Returns ``None`` for an empty or unrecognised click. + """ + if not click_data or not click_data.get("points"): + return None + point = click_data["points"][0] + custom = point.get("customdata") + if custom is not None and len(custom) == 2: + return int(custom[0]), int(custom[1]) + if point.get("x") in name_to_idx and point.get("y") in name_to_idx: + return name_to_idx[point["x"]], name_to_idx[point["y"]] + return None + + +def _empty_word_fig(message: str) -> go.Figure: + """Placeholder figure with a centred hint, for the per-cell word charts before a cell is picked.""" + fig = go.Figure() + fig.add_annotation(text=message, showarrow=False, font=dict(color="#888888"), xref="paper", yref="paper") + fig.update_layout( + xaxis=dict(visible=False), + yaxis=dict(visible=False), + plot_bgcolor="white", + margin=dict(l=20, r=20, t=20, b=20), + ) + return fig + + class NlpWebApp: """Minimal Dash webapp driven by an ``NlpExplainer``. @@ -262,6 +314,107 @@ def _build_layout(self) -> None: style={"display": "flex", "flexDirection": "column", "height": "100%"}, ) + # ── Error Analysis panel (confusion matrix + per-cell word importance) ── + # Only meaningful with ground truth. The matrix is a global selection driver like the + # scatter: clicking a cell writes ``error-cell`` (see callbacks), which composes with any + # scatter selection to filter the table and the main Word Importance tab, and drives the two + # per-cell word charts below (words toward the predicted vs. the true class). + error_analysis_body = None + if has_gt: + idx_of = {name: i for i, name in enumerate(label_names)} + true_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_true.tolist()]) + pred_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_pred.tolist()]) + k = len(label_names) + cm = np.zeros((k, k), dtype=int) + for t, p in zip(true_arr, pred_arr, strict=True): + if t >= 0 and p >= 0: + cm[t, p] += 1 + self._cm = cm + self._cm_true_idx = true_arr + self._cm_pred_idx = pred_arr + + error_analysis_body = html.Div( + [ + dbc.Row( + [ + dbc.Col( + html.H6( + "Confusion Matrix — click a cell to inspect that error group", + className="fw-bold mb-0", + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + dcc.RadioItems( + id="cm-normalize", + options=[ + {"label": " Counts", "value": "count"}, + {"label": " Recall", "value": "recall"}, + ], + value="count", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "12px"}, + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + dbc.Button( + "× clear cell", + id="error-cell-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0", + style={"fontSize": "0.8em"}, + ), + width="auto", + className="align-self-center ms-auto", + ), + ], + className="align-items-center mb-2", + style={"flex": "0 0 auto"}, + ), + dcc.Graph( + id="confusion-matrix-graph", + figure=plot_confusion_matrix(cm, label_names, title="", width=None, height=None), + config={"displayModeBar": False, "responsive": True}, + style={"height": "360px", "flex": "0 0 auto"}, + ), + html.Small( + id="error-cell-caption", + children="Click a cell to see the words behind those errors.", + className="text-muted d-block my-2", + style={"flex": "0 0 auto"}, + ), + dbc.Row( + [ + dbc.Col( + dcc.Graph( + id="error-pred-importance", + config={"displayModeBar": False, "responsive": True}, + style={"height": "320px"}, + ), + width=6, + ), + dbc.Col( + dcc.Graph( + id="error-true-importance", + config={"displayModeBar": False, "responsive": True}, + style={"height": "320px"}, + ), + width=6, + ), + ], + className="g-2", + style={"flex": "0 0 auto"}, + ), + ], + style={"height": "100%", "display": "flex", "flexDirection": "column", "overflowY": "auto"}, + ) + # ── Global Word Importance panel — controls live inside it ── # (Top-K / Sign / Exclude only affect this panel, so they are co-located here.) # The tab label already names the panel, so no H6; a tight controls row + a graph that @@ -489,7 +642,11 @@ def _build_layout(self) -> None: if editor_comp is not None: left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._view, self._engine))) + # Error Analysis sits beside Word Importance: it *is* an aggregated word-importance view + # (per confusion-matrix cell), so it belongs with the other global "why" panels on the right. upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] + if error_analysis_body is not None: + upper_right_tabs.append(("errors", "Error Analysis", error_analysis_body)) if cf_comp is not None: upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) @@ -516,6 +673,8 @@ def _build_layout(self) -> None: dcc.Store(id=_CURRENT_STORE, data=None), dcc.Store(id="scatter-selected-indices", data=None), dcc.Store(id="word-click-filter", data=None), + # Selected confusion-matrix cell: {"pred": idx, "true": idx, "indices": [...]} or None. + dcc.Store(id="error-cell", data=None), ] if self._components: stores.append(dcc.Store(id=_APPLY_STORE, data=None)) @@ -611,6 +770,21 @@ def _register_callbacks(self) -> None: full_records = self._full_table_records has_gt = self._has_gt + def _compose_indices(selected_indices, error_cell, errors_only=False): + """Combine the scatter selection, the confusion-cell selection, and the errors toggle. + + All three are optional filters that intersect: the embedding selection *within* the + chosen confusion cell, further restricted to misclassified samples when ``errors_only`` + is set. Returns a list of original sample indices, or ``None`` when nothing is active. + """ + cell_indices = error_cell.get("indices") if error_cell else None + error_positions: set[int] | None = None + if errors_only and has_gt: + mask = self._error_mask() + if mask is not None: + error_positions = set(np.where(mask)[0].tolist()) + return _compose_selection(selected_indices, cell_indices, error_positions) + # ── Global word importance ─────────────────────────────────────── @self.app.callback( Output("global-importance-graph", "figure"), @@ -620,20 +794,25 @@ def _register_callbacks(self) -> None: Input("sign-filter", "value"), Input("word-filter", "value"), Input("scatter-selected-indices", "data"), + Input("error-cell", "data"), + Input("errors-only-switch", "value"), ], ) - def update_global_importance(label_idx, topk, sign_filter, exclude_words_list, selected_indices): + def update_global_importance( + label_idx, topk, sign_filter, exclude_words_list, selected_indices, error_cell, errors_only + ): if label_idx is None: raise PreventUpdate + effective_indices = _compose_indices(selected_indices, error_cell, bool(errors_only)) word_imp = contrib.word_importance( label_idx=int(label_idx), n_top=int(topk or 20), filter_sign=sign_filter or "all", exclude_words=set(exclude_words_list or []) or None, - sample_indices=selected_indices, + sample_indices=effective_indices, ) label_name = (contrib.label_names or [])[int(label_idx)] if contrib.label_names else str(label_idx) - suffix = f" ({len(selected_indices)} samples)" if selected_indices is not None else "" + suffix = f" ({len(effective_indices)} samples)" if effective_indices is not None else "" fig = plot_word_importance( word_imp, title=f"Word importance — {label_name}{suffix}", @@ -755,15 +934,17 @@ def toggle_word_clear_button(word_filter): Input("scatter-selected-indices", "data"), Input("word-click-filter", "data"), Input("errors-only-switch", "value"), + Input("error-cell", "data"), ], ) - def filter_table(selected_indices, word_filter, errors_only): + def filter_table(selected_indices, word_filter, errors_only, error_cell): # word_filter may be a list of words (multi-select in the scatter) or None. words = word_filter if isinstance(word_filter, list) else ([word_filter] if word_filter else []) - if selected_indices is None: + effective_indices = _compose_indices(selected_indices, error_cell) + if effective_indices is None: recs = full_records else: - idx_set = set(selected_indices) + idx_set = set(effective_indices) recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records if errors_only and has_gt: misclassified = [r for r in recs if str(r.get("prediction", "")) != str(r.get("ground_truth", ""))] @@ -776,6 +957,11 @@ def filter_table(selected_indices, word_filter, errors_only): total = len(full_records) parts = [] + if error_cell: + names = contrib.label_names or [] + pred_name = names[error_cell["pred"]] if error_cell["pred"] < len(names) else str(error_cell["pred"]) + true_name = names[error_cell["true"]] if error_cell["true"] < len(names) else str(error_cell["true"]) + parts.append(f"predicted {pred_name} · true {true_name}") if selected_indices is not None: parts.append(f"{len(selected_indices)} selected in embeddings") if words: @@ -867,8 +1053,11 @@ def update_scatter_selection(selected_data, click_data, _clear_clicks): if not click_data or not click_data.get("points"): return None return [int(click_data["points"][0]["customdata"][0])] + # An empty selectedData here is almost always plotly re-emitting on a figure recolor + # (color-by / word / errors-only toggle), NOT a user deselect — ignore it so the box + # survives. Genuine clears go through the clear button or a point click above. if not selected_data or not selected_data.get("points"): - return None + raise PreventUpdate return [int(pt["customdata"][0]) for pt in selected_data["points"]] @self.app.callback( @@ -880,6 +1069,84 @@ def toggle_clear_button(selected_indices): hidden = {"display": "none", "fontSize": "0.8em"} return visible if selected_indices else hidden + # ── Error Analysis callbacks (registered only with ground truth) ── + if has_gt: + names = contrib.label_names or [str(i) for i in range(self._cm.shape[0])] + name_to_idx = {name: i for i, name in enumerate(names)} + cm = self._cm + pred_idx_arr = self._cm_pred_idx + true_idx_arr = self._cm_true_idx + + @self.app.callback( + Output("confusion-matrix-graph", "figure"), + Input("cm-normalize", "value"), + ) + def update_confusion_matrix(normalize): + fig = plot_confusion_matrix( + cm, + names, + normalize="true" if normalize == "recall" else None, + title="", # the tab header already labels this panel + ) + # Let the container height drive size (this panel is only half-column tall). + fig.layout.width = None + fig.layout.height = None + return fig + + # Cell click / clear → the shared error-cell selection. A single owner (dispatching on the + # trigger) keeps this the only writer, so no allow_duplicate coordination is needed. + @self.app.callback( + Output("error-cell", "data"), + Output("confusion-matrix-graph", "clickData"), + Input("confusion-matrix-graph", "clickData"), + Input("error-cell-clear-btn", "n_clicks"), + prevent_initial_call=True, + ) + def set_error_cell(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if "error-cell-clear-btn" in trigger: + return None, None + cell = _cell_from_click(click_data, name_to_idx) + if cell is None: + raise PreventUpdate + pred_i, true_i = cell + indices = np.where((pred_idx_arr == pred_i) & (true_idx_arr == true_i))[0].tolist() + # Reset clickData so re-clicking the *same* cell registers as a change and re-fires. + return {"pred": pred_i, "true": true_i, "indices": indices}, None + + # Per-cell word importance: words driving the (wrong) predicted class vs. the true class. + @self.app.callback( + Output("error-pred-importance", "figure"), + Output("error-true-importance", "figure"), + Output("error-cell-caption", "children"), + Input("error-cell", "data"), + ) + def update_error_word_charts(error_cell): + if not error_cell: + empty = _empty_word_fig("Click a confusion-matrix cell to see the words behind those errors.") + return empty, empty, "Click a cell to see the words behind those errors." + pred_i, true_i, indices = error_cell["pred"], error_cell["true"], error_cell["indices"] + pred_name, true_name = names[pred_i], names[true_i] + if not indices: + empty = _empty_word_fig("No samples for this (predicted, true) pair.") + return empty, empty, f"predicted {pred_name} · true {true_name}: 0 samples" + wi_pred = contrib.word_importance(label_idx=pred_i, n_top=15, sample_indices=indices) + wi_true = contrib.word_importance(label_idx=true_i, n_top=15, sample_indices=indices) + fig_pred = plot_word_importance( + wi_pred, title=f"Words toward predicted: {pred_name}", width=None, height=None + ) + fig_true = plot_word_importance( + wi_true, title=f"Words toward true: {true_name}", width=None, height=None + ) + fig_pred.layout.height = None + fig_true.layout.height = None + caption = f"predicted {pred_name} · true {true_name}: {len(indices)} sample(s)" + if pred_i == true_i: + caption += " — correct predictions (diagonal)" + elif len(indices) < 5: + caption += " — few samples; interpret with caution" + return fig_pred, fig_true, caption + # ── Tab visibility: toggle display of always-mounted bodies ────── # Bodies stay in the DOM (see _tabbed_card); only their `display` flips so the # cross-panel callbacks above keep firing for panels on inactive tabs. From 242fe27bfeeb06c68af129bb55bd4e18a903aa06 Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:03:11 +0200 Subject: [PATCH 044/144] test: add tests for recent changes --- tests/unit_tests/nlp/test_nlp_prototype.py | 140 ++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 618b1c9e..6269035e 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -19,11 +19,12 @@ from shapash.backend.nlp_lime_backend import NlpLimeBackend from shapash.backend.nlp_shap_backend import NlpShapBackend from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance -from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.nlp_app import NlpWebApp, _cell_from_click, _compose_selection LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] N_CLASSES = len(LABEL_NAMES) @@ -342,6 +343,54 @@ def test_orientation_horizontal(self): self.assertEqual(fig.data[0].orientation, "h") +# --------------------------------------------------------------------------- +# plot_confusion_matrix +# --------------------------------------------------------------------------- + + +class TestPlotConfusionMatrix(unittest.TestCase): + def setUp(self): + # 3-class matrix: rows = true, cols = predicted. Row 2 (index 2) is empty. + self.cm = np.array([[5, 2, 0], [1, 4, 0], [0, 0, 0]]) + self.labels = ["A", "B", "C"] + + def test_returns_heatmap_figure(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertIsInstance(fig, go.Figure) + self.assertIsInstance(fig.data[0], go.Heatmap) + + def test_axes_are_labelled_true_and_predicted(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertEqual(list(fig.data[0].x), self.labels) + self.assertEqual(list(fig.data[0].y), self.labels) + + def test_customdata_encodes_pred_then_true(self): + # customdata[true][pred] must be [pred_idx, true_idx] for the click handler. + fig = plot_confusion_matrix(self.cm, self.labels) + cd = np.asarray(fig.data[0].customdata) + self.assertEqual(list(cd[0, 1]), [1, 0]) # true=0, pred=1 + self.assertEqual(list(cd[1, 2]), [2, 1]) # true=1, pred=2 + + def test_counts_shown_as_text(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertEqual(fig.data[0].text[0][0], "5") + + def test_normalize_true_is_row_recall(self): + fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") + z = np.asarray(fig.data[0].z) + np.testing.assert_allclose(z[0], [5 / 7, 2 / 7, 0.0]) + + def test_normalize_true_handles_empty_row_without_nan(self): + fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") + z = np.asarray(fig.data[0].z) + self.assertFalse(np.isnan(z).any()) + np.testing.assert_array_equal(z[2], [0.0, 0.0, 0.0]) + + def test_custom_title(self): + fig = plot_confusion_matrix(self.cm, self.labels, title="Errors") + self.assertIn("Errors", fig.layout.title.text) + + # --------------------------------------------------------------------------- # NlpExplainer (no real model) # --------------------------------------------------------------------------- @@ -485,6 +534,66 @@ def test_scatter_wrong_shape_raises(self): with self.assertRaises(ValueError): NlpWebApp(xpl, scatter_xy=np.zeros((5, 2))) # 5 rows but only 3 samples + # ── Error Analysis tab (confusion matrix) ───────────────────────── + + def _make_webapp_with_gt(self) -> NlpWebApp: + # y_pred is ["joy", "sadness", "joy"]; make sample 0 a sadness→joy error, others correct. + xpl = _make_explainer(compiled=True) + xpl.y_true = pd.Series(["sadness", "sadness", "joy"], index=pd.RangeIndex(3), name="ground_truth") + return NlpWebApp(xpl) + + def test_error_analysis_absent_without_ground_truth(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertNotIn("confusion-matrix-graph", ids) + self.assertNotIn("error-pred-importance", ids) + + def test_error_cell_store_always_present(self): + # The store is created unconditionally so cross-panel callbacks can read it even without gt. + self.assertIn("error-cell", self._collect_ids(self.webapp.app.layout)) + + def test_error_analysis_present_with_ground_truth(self): + ids = self._collect_ids(self._make_webapp_with_gt().app.layout) + for cid in ("confusion-matrix-graph", "error-pred-importance", "error-true-importance", "cm-normalize"): + self.assertIn(cid, ids) + + def test_confusion_matrix_counts(self): + webapp = self._make_webapp_with_gt() + cm = webapp._cm + # LABEL_NAMES index: sadness=0, joy=1. Rows=true, cols=pred. + self.assertEqual(cm[0, 1], 1) # true sadness predicted joy (the error) + self.assertEqual(cm[0, 0], 1) # true sadness predicted sadness + self.assertEqual(cm[1, 1], 1) # true joy predicted joy + self.assertEqual(cm.sum(), 3) + + def test_confusion_matrix_index_arrays(self): + webapp = self._make_webapp_with_gt() + self.assertEqual(webapp._cm_true_idx.tolist(), [0, 0, 1]) # sadness, sadness, joy + self.assertEqual(webapp._cm_pred_idx.tolist(), [1, 0, 1]) # joy, sadness, joy + + def test_confusion_matrix_figure_customdata_orientation(self): + webapp = self._make_webapp_with_gt() + graph = self._find_component(webapp.app.layout, "confusion-matrix-graph") + cd = np.asarray(graph.figure.data[0].customdata) + self.assertEqual(list(cd[0, 1]), [1, 0]) # cell (true=0, pred=1) → [pred_idx, true_idx] + + def test_cell_from_click_uses_label_names_when_no_customdata(self): + # Heatmap clicks may omit customdata; the x (pred) / y (true) labels must still resolve. + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + click = {"points": [{"x": "joy", "y": "sadness"}]} + self.assertEqual(_cell_from_click(click, name_to_idx), (1, 0)) + + def test_cell_from_click_prefers_customdata(self): + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + click = {"points": [{"x": "joy", "y": "sadness", "customdata": [2, 3]}]} + self.assertEqual(_cell_from_click(click, name_to_idx), (2, 3)) + + def test_cell_from_click_none_on_empty_or_unknown(self): + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + self.assertIsNone(_cell_from_click(None, name_to_idx)) + self.assertIsNone(_cell_from_click({"points": []}, name_to_idx)) + self.assertIsNone(_cell_from_click({"points": [{"x": "??", "y": "??"}]}, name_to_idx)) + + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -519,6 +628,35 @@ def _collect_ids(self, node) -> set: return ids +class TestComposeSelection(unittest.TestCase): + """The three sample filters (scatter box, confusion cell, errors-only) must intersect.""" + + def test_nothing_active_returns_none(self): + self.assertIsNone(_compose_selection(None, None, None)) + + def test_scatter_only(self): + self.assertEqual(_compose_selection([1, 2, 3], None, None), [1, 2, 3]) + + def test_cell_only(self): + self.assertEqual(_compose_selection(None, [4, 5], None), [4, 5]) + + def test_errors_only(self): + self.assertEqual(_compose_selection(None, None, {2, 7}), [2, 7]) + + def test_scatter_intersect_cell(self): + self.assertEqual(_compose_selection([1, 2, 3], [2, 3, 4], None), [2, 3]) + + def test_scatter_intersect_errors(self): + # This is the box-then-errors case: errors *within* the selected points. + self.assertEqual(_compose_selection([1, 2, 3, 4], None, {2, 4, 9}), [2, 4]) + + def test_all_three_intersect(self): + self.assertEqual(_compose_selection([1, 2, 3, 4], [2, 3, 4], {3, 4}), [3, 4]) + + def test_empty_intersection_is_empty_list(self): + self.assertEqual(_compose_selection([1, 2], None, {8, 9}), []) + + # --------------------------------------------------------------------------- # NlpLimeBackend # --------------------------------------------------------------------------- From ae6e53b9c87322741713665044d3dc14f81873ae Mon Sep 17 00:00:00 2001 From: Milton Minervino Date: Tue, 7 Jul 2026 17:05:38 +0200 Subject: [PATCH 045/144] chore: add nlp cache for tests --- .../9df5b93eecbd2abe512c3624c69dfef1.pkl | Bin 140774 -> 0 bytes .../9df5b93eecbd2abe512c3624c69dfef1.proj.npy | Bin 928 -> 0 bytes .../9b53acb06c765f0a3a16e3605e17c8ff.pkl | Bin 0 -> 1370959 bytes .../9b53acb06c765f0a3a16e3605e17c8ff.proj.npy | Bin 0 -> 8128 bytes .../d7a88ad33d547dd8231f8b1dd0d037aa.pkl | Bin 0 -> 673247 bytes .../d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy | Bin 0 -> 4128 bytes .../717d02a26c2b6b25b7d9583195fd207a.pkl | Bin 0 -> 772326 bytes .../717d02a26c2b6b25b7d9583195fd207a.proj.npy | Bin 0 -> 928 bytes 8 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl delete mode 100644 demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy create mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl create mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.proj.npy create mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.pkl create mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy create mode 100644 demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl create mode 100644 demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy diff --git a/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl b/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.pkl deleted file mode 100644 index 99757b1729a08813ec684ab98a81280849e3addc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140774 zcmbqc2Ut``*S^BC3)nFBzIKcaqeiTERqPEFu|{`+1(vo8yEN-n6hTn|QPEhicg5b_ zwPVNL4KXGfqYCC}R{dE4J`iOw05n-m$(!VoO-1>!?x_tJ+oQ#H( zGT1;CBl?dvW3Y%IbH;ZW?)bkHE+fUkpw3891Q-G$@NWzL?G&ijhXGxKqr(kh2AxG8 zkP+;vNB3Y?skgr_0v#NKjI4WKlroWF=!`Foc#298rl9OCay4p#S%t6lz!1s0eWbQBU?mO7&Ih|M5Kt-nG2N2 z934fSu~JNW@>I~pp2wnJRatbQk`5V3qzY&tMCf1$!snuN;rJ(0C-zWjF@&?_FA=5p zH-?A997K+E;ZPYSYEds74DxqWfx56THb0fl9H!Ts@C~L7iZCO+fahilTTBH^jvkVA zIO)QeM$1~^6lMse$%*>x6c`;5@tG{?70HMkb;Pd=tT-^*%(yGi1B?>LMqynT7cLeE zUl+s}aAb27-&cg_0`y>p%s^~m-JRm~IwmD!7<&?~i?A0iu@S6`p>N2m>J)3R1k)fU zy#>;RO6w$%Dgs5B(vZaRXJ$~KiGP1Bm~rn4?ZW`!Y=%y;dZx#cRXT-(c^Z+_$SF8F zhzKP#Hqc-x%C00e7dQc3goW7+tkd`p-Q;x;d6lLrS#vmi0KOv|n>ji#P*1Lp7@;+F zAj?&m^^%)$3fD8A=0Z-!5{!;TzUdesIy=!f$^vnY!rDs;%bX9Jq$ST#G5jq+J@n2W7$q;3*MC-_1xL|bB zcYRb5mX$P*PK^vxvS!(pj1HAo>EExzc1b zh;}X-tDR}Mz?+*5exk#It}&siVj(nUTq;XcytHhhxC0F=f;h>-V695lpJZdOceeh< zC~zS9AzBUDkmco-Z7hOmjM)&xf+brxN2X*-n7GJyn4XqgsS6Y18q=QQrf9z~gPC-Y zB*78@9a9Zx9aAzTEekW!&pk83fZpF=jQ-5U68lVd6%owE$?N4B1sxzS&0?!ABF><< z(6Wg^R17apdYwPH3x(ML|5GFhuuB~2|S<#@vg#8TkuY!*`1&UdX zG!2#65NCn;6`2VZh=qOG=n&JOx#0#eZb%HdnnU#_h=@(=GvO#>bYanjmaL?#gvH+I z2r+?Rj5@^{qsS+S&c`GLxxd8?i3e9h6ikLvK)fPx;1Z<|!R$qeldg+tjY=00FKRKS zE$J?rm@mc4VmMQob&}mV%g>4kK}@OaRzvy^Gzp4l_+6fdavXn%D1P1TPF`B&z-@?4KIG9tQ zfrWE&q{!mYnF&BjLnVu$OxGf%)sEn$h=2>F#nMnaNh{2(h0-{TtkQ)`8H>E`0s1gQ zQ1E})4u~%}=|p!cg$`NX{=t-DFl(cPhq+4@8|8UA#fuVmHbg-d2i5wkVht z88V5qFvcp8nX~NJegbm3M_pNJE@fyzVa(1I_!1r=KViA)e`eU>?;b`MhHr_x0Ld;s z%RE$p(b$VIM-ojchSf1f2a(@&_S5_8AVvIBT&Mt}oFF?#n-RXnyjh%xBb$x_jp+Va z#8HJK;$XpxtYpDnz$pw?K`$|#73VB18iZ_S9$egXAP;2fBxQSwV750BS4Y~^v8bg& zKoZkX7mG0r;g)TbWcQ^Z z9M(r1KxKhxi6XU|lM{R_S-5CZe%O$(V6N0f1TjyiLat>N_eJ6cQPdYH@TqiRvATHr zNJ$fn+Tb4@ri&7T7WN!$`Pk_4EKPbN{0_PoZb2n^Il%!j*U#)l{D_?zA|hd1`p*GR z+6EgUf(@7hmQ9=}^C8Z5n_R?rO8SXo=TL*iU;L=7deXMmq=QgIcPmQ$A4X7_4E_*m z(demyhKdC5e!-5f`1wM0+ge$)rSAax4XDi#!FnJpy}d znx*JDoRJELVOG?@{tF#u8wiOdM{MBm2%ZSSVY(g6)WFHl$ZXUFsXXO1Or7)=+Iu^g zOhs&`AWwm5(HgtJ`5;kedmyI(SUcLBkqc0fg&1KE*zQ?n&?DAUYUadpg(3qhf>=uD z5M+SD2Z&CCVit`@vJ_u+MqvaQh3M1h15BmaE((^#lv0K5kr-s5))D>&Rv9pd8lsAB znh=k}k!2PAgpQAMDhvJOQ6Y@^6rQuLxvpxr)Zh3NyStDR_E4aj=WUe37MnIiUuQc#rR zzMZ3zQU=-ASd7TpnQ&cn{{BdJ#Z?reyOUYRxGLa8E{`!if~H8X78hTYk`ifgKFj{3 zf`QUM%xJ$(u1LCB_+u+=3XV5R*|sz0V-bUvw4?<*D9rjeQh`_&eV9}onMhp3eLb5% z33IfcSsxjV6$&f59%8y8Z_%XU8R85JjRj&xDMc(ii8TRPUc{HY?6e{Mr`)>0p8sCs zlTDXgF-8yA$AL6o)k+d zX%Zdf=VM6|c9N>fa^fs!kS=Dpn+WVnF(`ai7~7nqo<~U;RfU-^vD=D;7nzY96hyy> z*i7$5MYG`JOtkI=47_6a` zI}|rWP?-&?&yseQPKYiD4mctJ!$}U1Cc8IMGDK# z;wS%>WjWjJK;$wf%Zoo(0|a#ZI6vuYNmRyxVwZ`Gq8Ba zMqqh8VvHzL7TOeMQ#8&IoTPfHy;GIaAG@NAn3c*=QQ>DahT7|*T$!i31Y1PW>WpH= zRD4-sU}mE*#!?Si!lIfK97`bH;vG70ab&5GG*`*?WdA0mAY$@tmk22RNDoL-QYo=; zr{*NNHGB1ULXJzlWe+5ANE;B^8re^fddY?%Rb*U{av*0A6Eh@BW{R0?hgD$7@)pBM zuK$MUS=mNm3}hZY0ENB)ak0fxh6%@e#J&_HRPUwq z1v_sR4PwQinF`qHBF+!|u=7)hZc&ND0Va9L_nAbSGwIj?pVTO3$Ec>Ta|?g@oQtWI zTzi(6fPK{w+fw)oQeu@Z5ao=*@Fe*;N#&#!R(w9DBfNf*)%g3Q6;x!wGRa}ZE=17k z$uW`Da}*IQT`sJ^xY7#ARW6qR1D!L#i4^%Lw&-FVhncOj8O45sm}KD?lBOh0S^>QS z#pa8!AI0{yg?3oDbV7V5P#;D@C1Yo*>`ckL zxRSE5Vn<~xtQFdE<|GAm1%f|G2r-XU%c$Z?PEbtABtau#6C<*m=$ct+MqY+OQAi?S z>`D2lYf!WpxuP7PL($B{ zHj`rdAa+O5kp(Tumw#R#lNc|u(DsS7LM-o#=j75Js89&B>UNe#4AX*$U;Yn1$37QE zHB<7(%p*!$4keD1nU*;Pi+g(Un>a*`uuI-VpDJehen$39C)^5P#Zl%!Ln$ z)5I&Hrpd(A%bsl%!~ocY(zSuY?81(vBC(^0zmu2ZAL?$hJvmd>B9+&~*{jSrkwWdGs7{ClQU`J} z4)Hj|R^S+omiqJK8-2Z8TtVQ9Df`bX;II{3jz&h$o%)^-d zQDAX2n~l^LFF2dw!^KlMR~&@E1B*LaIV_l2AVi`=f)^7c$56xu)JK?a5etTFM)Gz- z8K4sjI1WLmqbQ@K+KrOkFQM6rWd$ZhC#i&3EV_scWo3~wKgn~t`r!Z;hd3+;Kwip% zw`h}r(jL$LOhHLo^1?#}XUtwYQ%0GHokSJ=ESw3=ONHXA6rSmXqwr({H36is;(Viw z;u%)qwnTK`*sw^ITd__oZh3H2?Jw4}Mf((YiPClm$xJW`#w25Lm&A6e_I2=Jxu!{5 zh!}$ua{kl)oa)9HNcMHiF&3U_Go>mlDN42=>KckD_7%ndyls~3jBLOmmKDnUp-fUT z%l1|FhsaV@UCP_T^vD*Aiab=jPhw}Q#jzjNAySJKJy+PR3A;8?1)1YTL7B7}PuS51 zZL%rfV0WhER8HC>NL_Ix%Ze3Z2$QlAS2JA(5U+@Reo9-g<(EQ)9dojm6E}@w*pW6D zR6vjmqcH}UrD&Aclf_AQ4p0VxVo3+a6oVvVKo*W@!^&atEEQnmA_AG|J{0bQLD=M_ z3O>#R?dTs3ujnMb=_DoTD7Qti#9!J0ibW6Uk`x6Ru|_O~5OIHru7O3@MT#!seozt4 zZmTiHr;{>&`2t$3j-A@!X4-z=R%lmDAw_L>!ez6+iBlmu7SS59Nf2ucl8nWOCKf_S2vjj)GAwQh zh7tu8oE3-3$NS>?v$};SVKG+P>8Yey&LlrkwF+;EDdh`v;^7(lyo*1wHc1Ld>9M>2 z2dT?$*U^4^1Fr=0!x~bYkPn;+ZX+s@2{P?=_0#KcCsVq-Na;VxT+9Mo1N5laOO8$2 z!HP?%#8R)CE$oA#UX@l=%E$?7Npv=cmm;s-6@%m3Po}hJrG=r z6|II8WMGBg!0PKMcpzcH4wP*>3i4zz=EUmtq;Ymq$GKk=sWJkp>!;maVQ^GD-IysXB;v8rQ!ceT2nbI=)mn^Ip1Ge7($#oT!L`m6I5jcn! zOTwrcA?ad_Ni5+&m$B~{pr>@h$V#!|S}>iGV=#`ibz>m*f6xcJe9kThbE16_IB=p{ zwm>cZn8JPp`KYE#eyt7GZbOTMHgYF zqjH)(&I!RX^M>MqENU@D#VBtIMU6&@gc*f1yg0Hknv%Q@a&bd0p+?AshJxESlrUfm z!!#zwfP~`j8)`|m^36uOU5yI$dz@L5eEp5`eNHikxJV}yj2r3Hil)v&DxCph8QFYY z$e}@1SVu|BpY8OEM@PuP9Uqc*qe?OQ6%mYhu|bU5On6cu3*I1_-Nr1Eu1L$r>X;<^ zLS2$8A@Zc{?A1xGEd}auB}ep2g;#S6Z@@YQ(Ctj9F`K5mZ59IyZM^)*y-2oMA{rI7 zC<~o1i*ZTZQWx@ruHu_G%*R)Y%v;{!OQJ;Qf-0M=0cFPvy zw3vvqoi|+%DmrhZx zyH!qfDF6eAYam5koVyiVvcof3S}x%z<6^O?KQ>(K!!CaC-qUbg6@>lz82YZD$NrW1zcKY+ROan=QBjUAX3A0UTL?6ye3%rljyv z*-v7L_MYq>gD4kyMndh?Y?Da>gGV(- zo5*lbeYVpxN@A#+ozGMGh3Uk5fS*_SaUInh&8nO9yA~3zc=Z+6SebWY9`N&hlGL#z z&eBQs;Xj%DVViUxA47ajSJH}Y-fB162I26eB2b&GJV3_u}>%`HIW z!hkv<<4;^imDxYoXe4pl`wcrk!1|)nCH+82s-voKwMW_}hzZ5#HYf*MXF47i7u~-0 z!#-7b4N+2ABzSc9zsLms;RcpeGNKR+5wQZhb6_;?(XilOq$sOG-3+zfqC*#GhLqkZ z@$)~EojK#OoIifkKuWK~m5IV1lQ~oJBwe8tf4n5c3}&qO1>ooYui!h<8j358vvgyu zoewU|NSx>iKB@{f;*t|}vETKv15W0U3W6zVqk#ij`BI)}58_V+VHKq566bb0tT2a17u}c^_|<}AVd=ol zfvzz_7p3Bh#FnVO@=D5|pvtjf41H9NzQw5uP;fCHdMMsFX5P}_^V4~HOLDfNHoLN* zBOR2g0-()oeZi@ChpMoyz;1kt;fikLM+XJTcOUK20*qGlnCWwVKEI=_2ov|W;twyx zy_9&xPf9hV1SSBdSA|lR@54wFafOvd8R(Zx=pyMkm1&tqiTiTVIkJ<{B6cEgK8oZ- z;cy}JL9w{_88gm0L)7Qj+GU9<#G;;j(XV)65tzh|awIdWg5SS>b}{{L7Z_B9*P$&& z@w$tc0KmG$qZaXgVZqjsu00|?5!a&Ba6^yML28iRXMTfeuQDiFDoRUNG0066ibl@Y z#VcwxY_XFu^&oHDUc{^2NvlJ;n~??l5w=i`*qo#vC`%jr&yDWn%Fa6o*CYF2pp+%Chj zH<&^%^!eVm@D2!F#-Uk=R$Ju!S*~%3Ln?8Xh1s?k_@tkeiz|<;T|Rz-*5Y@@IPOR8 zs811pZ^-_qBvL9wM2DN=n~p|$tZy2Qy|;mW^a(}cO{@8zdbCwLho^%` zd%jW|NwntWvuQlXneJ@*G!?WdCu8n(;GXfbt7oCz);G3f0%*6~u}7x>Ic|9Sfk4~L zm|gxr^{xE#W}vs}$h^5c#|h{7B@;kr&6$`G4djIR?_+=+TqoSSM9;fk^-Tr6=hns9 zM}f{g$8X;a%)Y;R>LMWb>>IboJjV^GT|e;-Xu&PI!&YE^q)+qxz`8f{?;i$cu3Z_t z5%^_)8IWwqG%QBQbo__bDkKH}kQb{WTdWMUK>Ug?8(Qi;u3AJ0&vDwjF}r2}xqHu2 z!--A*-uHDt&0kZmj3+#Is8Iy9_j#K79Sg_cpM z(R@f(AeY~JVq3zzZ!>wIrp@C$1FU%OWJsrB)V?XUcz4jkj|0ne1*+4O(X_l8r))JX zE7z<=aSdo;(Zg8k$K_0aLE{KX=PL{VdjGur+jT&$!VmqZ-KGf(CO!kSc{gZzv^}@K znL+qdP%Tm)&tv_E%;!1oL-(kOZ21rL*gO&R#-=_UGw|G$8!>AEP;;~6wXs&t7I4eQ z7t5=@e=ExHGuk}@PrY6Z{5a<0{gpuBuL>L10o98qrlu3^dGj%;pQ+ikP(NUvIp@JJ zYQH+U6RBVRnfC)`@!<2mPB{?B-F!A%kM^+56?aSmt-k!Mg%4=%yBR+YCtQ8~bO7PX zzztmpW5;$e@f^2ytwm4cc(m)B6N2_jZw8!Q1=`l5(SB01?BJgQ$$WBz+rB$653cG^ zzZ-bocEFR0^U&U8QRbojz-6^hygCY8(l*0|)LdwH?j&2^A3t310<9ie_7LeoYO|kj zh4L7G{hub{OH=NqcQ0UG-0gId>l^QdrKbVak9Lm|fZD#wuY7rqEAcMu!8{<>F6mlb zvbx0nf|bSh$|dK5B?l z-Nm5y9Pp_@^3hbj?n&e93y-prT;^Tg)jS=~^L%zq?hahk#lP%ipwRf;W8$B?^*DVt zXs@Y{5~l$(9nX0z1@7qo&4na9zti>7?iIki+`9{)KOEO(_bK{r>fH22$v|Pu#gqMk zwvL+z=y|xQ{j>Ly`x7d}PaFi~vWF$~26}%p_$Be>{@tvDH2=Kc^J{6*p8YDSJFPpn z&ye7YcJB1lnbDvZT@JdJ2E3H>r}qZ5n^X&*!~s7%F7;*w+Bx0q)ufMK_V+LDr-YsR z(KvkB&{FRKy{iQgTa&`Okr-?jyR45VS~yq31e)8qcQHM;mD8=H_WYj`tIh{%YL0DS z!Sn22e|}DC#Cca*3oh$3n%XaB_8>FSyjk~_@u|J} z3%#$YkUViJFz@5Vn_+m)X_}jrpoQ&oN0EEw)K6+KH!PHQBlY2)Ox#6ks=d^5K7Fr} zYN^9<(AksLo3nu)bBc}KH}(G(?Sda39|E>)(gb2?pZllIg*&t-vuAZu#8=Ivd{)eZX^^U++KM$?IPl6;*o=u*de4wj_S_!lw2BMOpt-o=sqF|gW1kF1yY1ZXrF1~<(e(0_&@NO?I!f7rX?KNkiJ`5859kgyVh|ixe0CEgYTlyu8s;nngZlJj=k#d%I9<6)SA^gSb8*jo@Ka>guYDc$vJdJ4Y1$k`#!`g<^b6dSZ zDU+>eKNo(7^ib2Y{t6mDZ-^$2*+pK!O0pNtus+KJftjl|*Idk_f70rdZ$R75xBXoY z4l}v;+&h}& z={EkaU$cPR+G0uG^!$td{>7a*+qo0rf7`oMZqS3VpS(LE4Qy%~D5g&#Hgl_ATMpXR zaInil(3;CNmL>vCt4xMepyJ~T_pNyDw&*|~iawks%}DB;%HQg?47BOYn6=Y@JvLR% zCwFnL%+A9n@LZi);{=&0SMKRb>i4MpX~i~Rj~72JA+^pO7I61_9?9&iy00&Q_Bh|MxG; zRmN_)JRJY~VeNWq7BrX&Hoc1lFQ8@7UU)Yx3aFWMWHDKPR*i$TDYp~c(vtnq&IyBl zr|0T@b#_wDmDe#0Xc3%I7JIvx%IOH@mv!+&^8ow z-qhRbv7osR?gnNR(Jt-!6V17_n*nS*?Q*A$K->6DmJz^Gih(J#OhVt*E%czZ_xp;` zMLT_BI@&dl4zyc9?e9<5A^wDsOYaULdRbm}BxtU-&A&Tnj`Ow51)832YeVmGBS)*L zzjole1txlKUdD1|u0{QQIQ}oePsr`b~n|C?3ay_v6kH4*02ds3e@*8UBvc}c* zF*zzYPuNznW2=nG8-X(c?$INVQ1_~wJwhco&_id?B0_a^S-HNXSEj(@WvIXe# zNsnB~YnptwmZNpkHhmuN1LWc~uV{Ql;Lfi}{WOh(HhBRb^slvnqCx)gPS2x(-W3jf zpeQ9&{Q5NUH{01W@FeJ=V;0tOWX<;4^r=icLeD32gF(;T#v zXxo(|6@X6?>iADZd*0-^-IIWsm-eo*069(hnv?K6b@5$QD$(VF2fKsj8h8D@CE@dv zn~K0)N{ZEk1Pa?Af!8ZMzRGz~7|V`5l7-@wbg5cHV4PlT+us z+f*>~==X8X=twJ`R)}ibpWb)uwDHvEsd# z!%O<`cyB_R8d3EAiRct&jy7wp`qaOY;$kB8tI_gLvlZ{%%Q-NE-k;=J!bIOq*$_XR z#{c;B);Q{)-SeoK`j0Z#GZP;NhZ!bP|H`Q+Js3ZHN5+qHSj`dio=5k}?7Q{9z1f8L zc;~k+oKR`g52F6te{&l^<9hiV4y68lt~xEDamVI34BjDimRTHS+j|&e^Aih?= z`JoGcwb!P<5;~Zzjdl*Km{QJYb^Bmj!#Q-aez28}95O7zdbsolhnNvF`2J6h&H5gt znGx&e4$QgQm7h0!U55{^Eml6?v2?T34Xk;p2|e7$w6RXE`Q~o_Z_K=ZgP7?-rM~5d zgsifd_671g2R7Q>x&3VZ&7_*;dOaAyPsmfe({auCf&!hx32OHdD`i>K34Ga z{nlym)_h0axm~&LJl?x^dQT8x?<>9h_?yX#Z@4-R=5Hh~Ez~rZM>A`Il|f?rndU+ycv9h7{B5xL%pqj4OZ6}{A4J{`GuuBM0!JCe8Rh?MMI9YEA5$`3_kij5hBPfYi^u5K<=Hp_`U;6ntzP-Vc}}f z%N_C1a~q$;N^ipW-BW0@l~=Y~cmmqFuP?3k0SXy=hUf@OHBE{mOt_j%*$4OA1eYux zX1=1+OmfR_(q<0X3z~a7*_rHm&+F1}wg9toLYp53dUTrWbQsSy)i-eyfZW5ETiB*W zuV2PC5tl5U0{U^KR$pL#q5k4MG3Y0l(k4>!&y~&aNTBz=YWxG6N6lxqXQN#`#cK!) zQ_s0v3g^yAmG)ZEUa54&s+WO6=mf`kKyCTt#tVR+3h$$5fjM^^n*G9Kyf6Cu7k6IH z&Yj!-r))_WvaKscXOF~z{2Z{S{jL8sNW6?E zb3d-wfp$&wZ_Q#s5B2RILs7Ix=Er9HsD0}FF6{kB^=>yoJGb=8G8Ww??!Q7&-}a!T zVGR1&nk>9bNia9Bp`!=rGht=sT?f5#ciZ#FfhUftRNn(Vf4X^Xf|bj^{A%I5`{>vC z+Z{(U(4N|*Uj8!Rjh2Ud5ZZ=R9@PV=&itX;D4r7zwO#XjHV?H3^qe{jC_GTFA~jo7 zX?^5mqPMS~*&p4qmnok%(S)F#d)2(r!Tb`|7ClSa+9$wHD?K$}+ zlPOBqT|1)_@o(FGOw|R*{W1G~YaZjSxu_xjxOX+%HwB$%Dib>bc%{M*4^x0y0lG8f z{%>3zFp{#EYoSZaE(fYFUU^08*l|;fhUv)4TpArx?LrcOZ3n)hW`HZlDB98zn|G`CcLdS0W>D9Vz! z8!M|*mMT;oxQzH2KSTA+X<(JdLuRhABFnlsp=%m2?&jmcYk@u1{CsOW@J8dMm8iW{ zqp;37JhrmGzg$TDkDa`6gW4-?Yx9QY(<5lcrES3M3m@l@`EX8+Zg&T2*9WdAJyUaU z>W=~nN0+2hR;|9Z_orkY{Vo}lEePNCJ5FZn{NiJ=$#|YO%y)%1P{@gxNZGmOCpU*k zAm?m-SRL)zFZ!huJv7#%H}r)YdZrV?4c;?WAItRd;gA}0@myWf)kll=xVv*bNDmg( z(vF`7I(V-`9&p@}>64Jjm0CS5U z8OOZhZ5!WasyoFTfUUsYuZPK`=LF{!|9IL zzvV3>JgTD(;YcgazR{2H<(-v9gOI zfmz*sEn2MaaNgz;MCiv|N70Z z=T|_#)YmbSqJVD8_m?xH-_F1v9m7F;lzO(A=FO@9>OGJ84b)Ai_4EGd8A0uDPImj9 z*2mU!Zm=)Vr*TB~JRs-RVB%ID%d5%=BDZoSW+9&jER~cvY!1+t=KgXBFnGj7-<3cv z!0Q^xvqzV>S4%)&@oZje4X|m_zWmj|O753_O9!fVUUrKDLUKPg;2}rc`W^_>l$}{+ zFxrJQ#S^x^Cu%#Berku^%OgHH_f+db(CS<3hLXHBo_8Y$0Ryh@*_KStKP`14xe5O3 z!I6>Q~z(Jy0Vc9G9%%}g@gR(m?-)y^?u?N_>u2Z)>pzzmM zr+(uRb6ehvk1B1{yv1q!X2j(`qsEWNh0U= zD4v-){`uek{fju=?8M3aPst&Y^$YP}GtQ{3nMUj$+0lR(-FS89;?d17fN_sf$x{+ip?eEOm zn9gIIx4vaa`)z+DbPEG|dkz1YtV5XE>ux6?SI!bdUfz^A^DnY;ZJSGTwbUL^<99vU z-6jUHgNOV}jZRQ9!6_dE&qaF))~24k<==GxG;7y(6-fFvMW%im0WKFp8E`6RB;zQA6Y(+ z)Gh1rlziexee|gpE$^emw*#1bGFu;~@t-{3xOfZNwKoog_yM_Fn_sL&d+N=gU#5e$ z1=p|8jj+)UpGQby%N28(aszafdGYmSz`F^%?wkXz?fUI6q`vpUo4GFna>x3HE#t`x z&dx{#?KZvUUXm|2t@UFK+O-QD)}(-bGuYCf@x$fxDU0?+tp_-v$) z(aw!c=DUE_MlW<74&)vkH>aSz-2EJGE@bSmE;xo5dZ6D^fG@B!c&N_7XW-2}Z{%{2K z&vl+UF#)tjADBz>u;+zZxN68hE@HF#9+&;6vNd1`(pDbIAt|hdNLTeXKSs~ z^gicyp~4OxpP5^435jEE{+XdmfZmpEo#qo>O3J0=B6IbWIZUki?HACp34L0>BXt&1 z)+}iU9J)6?jQXiR{P~cAUEZ~+Ge!V8Q|s`lJjQ9seMfxfwg00SnTPFZZLSN@<7LGI z#82MpCS~G)oQK++gm%u;GleaCgL^BQfp)*ESV8J1tb4BY0_Fu?KNtw))E~Pt^>aA% zp*#9zKVCjX3v~O{c$<9;JT2ex*vaa@7xURwD&K-<{^85D%MKQ~9!1A3T8{1QMkch^kwvQ-aB_vbOcLqpp1 z25OEDb{GWAYFzz^9;mK#A<)-~?v*0vky&Pq9`l~$rAZsxlGf#AmHu1OfZpcCjbo{O z+p@NOfygq=eR#l}Iu*!GsT;2zOY0-N?&LQd?ZOsY{l38V?VsgOLOWN^)@==FZX6#= z>s8L>&eQ~;diBVqH1GVG&8yP*J*qEg$L5=D>_u*r`zyJ@cW5{59y*TAXW7*dAJE*N z39r<6ULxtMs?&%rH~aER(A+tn!L)83smFJc-lk@cZao{A74q^1xsCi)wfZpmzZqqk z$Aiy~+*RUVShi|0>5Jm`#@nUCrF^}ngj=OC!d;fb_eL}lht-E0Op1L**YEanO}Rz-k(96w!}1<1N^kW`t~7@=U?Z1Ls2|$ z;Fe!mSb-@1`@ertXIDFQ&imhTdG5{RwqTF*9{lJVpl#u%59FmZ-?pEb!9(0LY6wub#AwLs{lF^yjz+ATC)GO*sqEYK^pm$FHXZR0hi#rb|r8wXl=J$+h#JP$tq>M)I; ztJva6>glnxU-$9AtgJnAn3?4rY(vZcpxL!nq$a}t-WO>;?i2S%9!5Xmh;wC1sEA^ueCRd`i={zwC5P!t-S>=2V?zwOkxrvT@B6 zYqyGbuh&0j27VV6kv9zQSt1uE&LO+*``<<{l+-e4zXmS9b3y6*=Z6I(BK zonI}b+eAE{(J^cDQu>y75Bg0yBY5dp2;^{M*Jjfz7)$k>b61^|<`5+78_?&c#;7u4AmuHOD=9vep~p9n740b{yuvZ@u^C z4%Ym`sx29(>+^5&5;hNd5n_crzg`+RiTa0qwS16u#f(uUI`vHsXx`J+UcsU+t;J6n08 zeN^LMApzs9U7v9;i26;A>fVaK)OCJ9U6*Lqf5?l;827t7b9(lo{$XGHO=0~HtO!QG ziiZ;uy!c1Ud;4xiGEH(lw{j`TDeXw&Q1G3*byn*I_}+To*smt>mANkqMbNqcg>C0MQAwf(1Yg})x`QrYGqaGd1d#XM#arvP zj#}}PcOUDqejA^@IBwvZy}Vy}@Kc{#|+T111nqGBp+Syx! zsDG0mH6g5j|F6dK%PWq#duDn!>&wo~6uBXzdFKgv(~g*iS!dnJDfzK%V{3NVcJ;Qu zuWfxabyWN6ZyWI#H~ReE#?~>*DjwlHe7Sa8t(TRxalV}M zR+$^eqrWwBQWDOQzwK}|$sc&=RcN&d)W2oZ(*ZcYb=mV(!{Io8&$(G;Yb2fXSLoj; zmim{Rn;dHG|3v+rdF*tYU(YxGnw*CBp45C3slj*BwkN$e(mDC%(xawWaqhct%H9Fg zze&RZQ>p*ai8E8d-=DMF1o-0oH=@Mq2Z3}>K4Z_a6dM2Fuxvk^8~>2LSuqX$M*Tj{ zZzA!}-{@Qy=j>IxWc)T8eBJnS(#$OM`+i(nT_eqD4Sk>Xv{~-!Zgo< z9#EETo7A%PC?EpAZYa_5Vc$|#-FWKx?UWvHqh2j@BGht+U*h|XkLA>uPxQOWJ19HU zM827U@2WRs4t;XiigT(n`+L#+G#Lk9QJ8-)E9?iB2D$I=MA>Rq-N==cT|bEYeiNl3 zM+Q5EO$FNAW}B%0#`l5q*Yg2 zwL{MU)tzpv_zCS1z0Pbwwyxe6I%5OUD$SejL;D`Va|`G7p3;&p`}>!469I-do^xC^FrFXpbI_s zxjL>d(Zb?>G#_q#oOv*{_h~z$2T)j2_qd+0YxvHA)E*quR71!;4P|ClVxGE-6=bXO zwRO}!q08dVpl#C+Z({WExz)*Rz3TI+3jwP(HCRj($zv%B@ z)Xl{%yUqUJHao)9CCOkT)0Wn6A|?5naqTh_fLzNq83Bac{+9_n z`uBX)FbQ<3<)VVDHLrwMf7;8d`_wy2w7TIX4^}GSY9G>q=1%r`8VI!g=0BILJv(*I zN3vQ?(uaZ6&o=X7$q*o?Dg6hjL)@EAZx#W)D<5*4jP@RlUBgK&Y*R8CCj+^f3)nAl zIMeHrP$R*A#HR|E(LeKf#fN0oAx#Xg4gvF?T0fE6hkBAj3|0v(hyVXA49@Lb_#mF+ruTjPY$I^b3bzz;s;Sl2PA4nV1ie+x1}-mi z?l4V{d+vg}wkUM>Y&wg)faz#hhd5yF>N-1Pf$o0|(2^L3f8YJ=G$40kLqaf6s59T4 zf{gq7z~80!l~{C!k-Fp>r3S&!55H#m>R=&emaj=K;BrW6I10YH#?R zHsZPV4`(CsJM?_S_L)Gpqwjr54NT3lvzYpDDZe@J@K#;Ymrg@Fm+|{iJ?PXLgS_H^ z!u8Sb`=Q-aadV{+G=9RM9R`d$m>$Q}W1Ro1mej7RJ;)Q7*P(ieSm2wSKTJAm_g+yu z5SY?oNzi;N=y%ntj|81Nde+4;z@e>oR26{kryh$o?cYwXM}N<* zUb_=PKdPn}Jqftu#m?Cy@96NgT~W3JvzSWhcId{H|f({E22lmR||H5&h7lEhXr)l zrNa~6wkRm@e#zou<1ZO1L*99P4cL}kag-^C!kOD3(rU&bDP~x z8x5NCbgWADl()(E#zeHM=RcdI2kmX`+Lh#7X-KmPG*7pS@4ja1Xxnpj0BCJ*8}2OQ z{rPVnjvzc{o=Wo*9)(9~3D0(@?MdxHwrEa7WU9wF57%Omm@@>-l{84}DNYAyaF0>A&_AmPT7xz}y zE}2RFpRNrE*@ot{tit#f8ZrwmD#h1CY`T9_H;vGCddfm-pB&he+y!@)ckK_%>ynm0 zW}|JhwHf8w+>K&k%v^24MP`N(8)}o9UnzC75!@12$?anMO!W8EY#WsUI?wrLi)5elbU`dMUTIW4JaR;#1AJ;ySo7l6_ zdD3p6rtdl}Me(!y&mW%y+Rgvsc3Qrar+1#G0yBm6#VkPWqCZX&zufY5>o)OFzr16t z;%%EccMX|O-tV8LN7C~>bv(Nhj^E8L*TH?9N<@EdYw^XTR<_??O^Cwt{5gMJpmv)j zv2!@+)P7zyR{*(<6Iv}tyXH#o6F#6dsqqs@t+E%%~E_{?`n+yw@L@ z8%ncQB{!%!Xn3ooeghmv>WOxGG+bU_p2f0QQ+t(8huL5c=)R-_2C|r8-lGM@T)R8-Ld0YYBy{P&Qpr*y} zVex3sA9m{_sawvDaUpAfNE3V@4xuSuH6{=Arat5AoCG%B)XpypSo$}gz{fm}oxR&G zBJ=V-`ef%^phx(k2DB`K|3#)wLeiqUKIq5UT797G2G?`GR|AFA+t*1A%I>s!9Ra51 zoL3OPzOP+2leu&Gs-9EOUMXhI02=>etC@Y~0M$QLKSO3BjC0sb>XthFMuiX{r|ld| z>LFbE!JE9Xrf0+LeL?4VX58EWI#^XhPwE!@$6zJ%>e{yDD0`~6~a3*7f*`Ue4R?*8$KM9&(UC`N_VhsN@tQ%7dgym5In z&W)ZQxDyfv6tp!Qsef+IsV_vex3wX+qbcU!n#pU`Pl2pmXY5Sl*~}dm&IEc~Sd_=U z<9#QA=EW_Jw~`*HkG5OM)E!O#{olW+b5px$l!b#MupMxH$zb~I<0@nI_^dE<^7wv) z=A<5T(u*|U20>POQT;!gcZ{qvOCuXI3@rNdFO-mH|-abtj`9NyI4hW6Y~z5%4J z>e21)(=z8At+a@iX;GDv<@fT?zG|WT44`+Fr-w3tr(J*fVJDtzpEqSU^RruIwuk{e zdGLVQ+wk1hez=PTm~($%{gp_*E1rJxoXkcD*ep;q%bp%}2+JYNaJq8p7W&S0{Sm~?l&`;arY#EYM{;{2_NKH1TS6xTTmOHcWOp=dn|H8Uvv=23Puf7PV zEuJ)83pCwQ74rk$Xu0O?TpoNhh#WTyn3wpx^ladpz$WbYSeSa!V-Wh;`pi}7(C*zP z>^04AXT?%6E{)(5T8D*hlf z&pLK&EUAAF%Ws~v9$BZnA5S72F#H!9N7MIl{{f&ilXjo+1`5^+8v=VQQq`n*!QY=|u&S>z(tja?{Z+Ji47mdbG&n&h})q zf3;*&A0|(=c~o(?~C^Y=kmixU>t7Cif9ed zrL0wHy*1|kenZf1`!>%u05tcd{fC}BJPhx-)jJhR>&$s>u22PZR@1F{AwaF~_MLt} zTVmJqY<}6r?)Lz#p6(QDvZB9gLc30&HFpo3V9&RoocIm(U%e!75RelD#d{RoZD*Fd zlbvwoR(gF!?W?X1?N4}XOV1`~w}tFHIFRTs?%&Y2EZ*0QQBMp9a^<%VZ-a4!E7LCw zq4CN#?AID-JN>W?%hH4|>5r@R>z*20(3ShyV`q=XRr;%a*3M?@@j!`#J;>?l#+U%B z@8H1=%fBv9SMk57Kd$VT_uTXy-uwBK%cx2G`t>2Z?p~sGR9F7J0{NfZYcWC;jUVE$ zpdDzgs_v*aP*^a&`5>UJXZCm|_n)3LZpT9oBYOFhK4{A?QfWbRwo(tsZ*Z0X1qxlC)HGB7kWE3P zPl9Da;wXB4x#{rXXcxAu-a-1u<@Rew>&cBRHjw1O-RWM3`g1#t*T|lQU%OnTxFq;T z6bqyF{PIsd30L0rVg22gyRrUrPM%}-f4TUOk*vRasGj<*f0IT0ay3J0kUY6y?~`gm zzscbwSK-&`HA6rP{x4dP9dL&n7qb5QYt>`@llD}qMg8a1G>HAze&w->u@m*;z@!gzW<&fA0L$<@EJ%$Kn0ovcxWOWZ&D05#7>+oteXzUV&dm|m+B zL%yTu4o5<1eFfzo;bc#?l3sJC15+=AA6w3oz3Oj9g3fjyFn2ON_dcmg1aessSIKWU zd(0l14cfH$O8--!Z7w<9KvKp%$bilGGw0leXi~KlSd$051Z4~LAdupir3ajpmFEV2Ho2$W;o)K7S0=sI<{c$pA z>7vQfP-u3~zJZ#7c|II6fyk7l#zJF!kDphf$ zgR1O~LJJ@NY@)Ec#8fQQ=zsO?hcJos8uhMdi z{YcEcjPzO$)x0gxwy&Dlt_9b3e3f_vY+NvDL>|bFPA*9$s{e7?%i%hwR3D+~R~fo# zCzY2aU)@K?2`RtVk```V>QO=V=0zIUIFxVT?Y?s8svnVwm0)fE$jCz=f2HW*M)2SL z{X_TZ_aEJ-_Wx|(AZu5FTSxf&@dZ(fHEPWIEb78VxgrN;rHjN(V@fyOxMl_i$^%O_ zfQ_vOZd{IHW=;RKD-4`ftl_&7G(G9FG#F&xW^9V&$Uk%`RhRPE_{-Mdz7cV!sF^Ca zUI?RZD4P(}-V0PZ8py?Jt#yj8gbq(M(4%TA?BKpC8=MuqxgY^d;~Uf{$`yyMDWLAb z%0H`Ta_Am`x9ljNjhj1VN5al;uZ*PbsT|yMC1C@P)`tFzyF}_j&~Krdp@l54Ud5wUYt8W?p6$_JlXVgD5&0xS-FA2 zb(5l5AY=Q>trXBdIvJaSlBo~hh{M7Ea-tEXSC>`w#C8ompAB;BFr7kvUQ>UWlJ-}g%OHPaU4!}XQ~I=g5=8zX`X({7T}89qMXuQP;n}T=lRQZ; z3G17VbUbuhLFE@-ozL?EYgfsSB~dxtFWJ&I9PKmPhX&y~)+epG1cy_wqp|ZC_EZuv~^0^1i!|6~FJsbgs3Q{FOUr()B7;KMF!H>xbk#+_8jK4VUkj zI>0~jvgP6wq_cW{Xes%RW2bzeC6S@EtKnz3YearJ*v)&oYXtmX4*g;4LH=@er5>t;W|TfPx|KKOFTZfh685y3``KbT>($L1=_P4drB*nue9QJAOAdZdz4tOY zuFBnTHtd~M#nUn=zu@_CyE)iDrnaZy@9Sn(vl@Q84U=;+z()(5I9l}=>z4J7Cx6NB zodxhOY2!844LZArrWN_Enm1-$B+3)Aaz!@PukzriP1K&K@t?yvfDc+aob*Pwahjx) zb~V@$+l=}w_NT#H`+8Hl%B3HLv+_p|z6U_tewPN&`1sjpWHilYQZr>e&93~=IT|(~ z?rnDq<6v*8o?#E|y=VALTTsLI&>8BtN~5|_W?=}|SeqmiG} zwA`o3$ZyDp)Gi6&dX`$?tGKi8-odehs2&&_cyt5t}x3eFrIlVSaIn?^Tg*TgQ(nLm!BX4QO_|RioEAZCpSd-qI{i454>WpSf~*{CFsbr zYWUmPO>e^;(`nyqYth&0Ol`ByvW#2R9}O;bS$FDswKfXaH10(8KZO5o{}KKh|KD!S z_uuxB!kx-N%RPedvtm^+v42?`#aTP1uWb2q$ij`PlZ89$XnIW}wh2ZF+%lzx67yXC&0!XB@7@o_M5j&lZS_OX15 z_#v`Vkv>)df44btJ|G)&yO9=`!)_aYRVY|_#Z!rs)#ihe$;T}N!R0?Yw%i2jY;Zgl0?KzR8n%@~{*umHoI&~C*+zD-OM6!Trp2Q4 zMaXd)UVQb2iF2XZZI29JXz8%Zj!U8W{anc$5FhDmME$1L!=LQW4>4?f?YPJV#|ztFkM_q8l`mR?f@>`K8CsP&np~8t$h0QD%2Cqxuv~N5oUuZ9HrDYYE7BlLj&T z9;+93N0GLS>r3e@V>6@hr@B&SL}5T!QF=X;?2?ZCV>sxqTQ9FC_D(xEh0>30olId$ zDead@{Ypq){+d3lFx8+|iLeVdM`}`-6&zd_T7b%u6Z02>jZY5Vr&TP=@#xu|qx_ex zqW+oIp+_R!JW;M_x4;8b^_0C_0%{z;J1iO$%%1;!bxH{R;0P@n+-g6CFV@#_ml%dO zpIAWGooHnLocb+a-EuHb>4JM2wHu#xw4BPtUQemACu;ic90$AX^We2~ezwN8!w|B| z?PBS=n4_au?<}WVm*&ud?W1E<4*vbPnkdo%U#`<~1dk2&pbs^?ZB2F(?5ZgvH_~3x6Dtik@R zf1+X!bvOM7vjQpq#D&Yfp)*NS&0!6z1BOBpBv$xSnCh)Oyo(X`D+<1Nk45)-3^5DOcPn zJTRT+H8jh%|2}QwIu6>wZ`lW0h1dP+N2_AKS;{f7em;&AtjXW)ff;>Rv9M1$T>^i3 zRN_}LJVfo&kAq!k$Pm9llfIZ1Bify#a;Tm+nkV|vuq*GDc;5|VUl)qo=?kN~L{NF9 zMn z&&|g1X)|Z_qiKY_DDxACVaCwUlaS87v*5|F^TS#t9Dz3ZSTTJs^phD&*U>Oxa|V2) z`cdiy^ZuZa8r;!_R@o)1XBT00JNQnepB$@K|4#eWh+*b6fpPyo<2n!1vJz(S-E-BIbylLb@ z#oIR*!>(?gPx(hW97$3nB_^uROmpTscS`Tt{DtBFi@zegeht^DQ+jN-zLv;;r%z0| zBjsN+V0*HnbEi)IZ_l?t{+)8w{0$X%_br;Y^88e+R&Erl9|*-N=JJmiU7yoPkRyD6(2Y)p9*Zw&E#+mm%SY?%24eaJc`9I#JX-)ZhDt!XQ{CgZ9O!;SQ zu`ne4ul$eev|ssCJCQTI^r8EJRzZr726K*btE?4R1?}lI)LRk1P@~tq`>qPC!b-;m zS}NfG*wkVb`P&aD3MT*Pul=3K|LV+%qW`2+yf6>{)7|_<|CL9ECXxS*l<^^o`iMN^ zS0U5L|Cm~+FXeCTK2h|)_`uPU`qNxDKZ;i@#4((v2chiYiLtP=s4caj#M4oK+7lyl z=SaYR>#y3;Ats)~@%@y%I6ilHX^)`%Vr*{F_+mkKk_HoB$|AMFf9>xVQI^c1+>1;G zFT?RtACI96D8H(~Migh*iyyoZu}?oYqN+jXUK|Ji4*IP^prvQd^%AGwa6Nx^Slt*+Q4bqsq@BB@zBC>f}9&d&1GQgJUQ?QoTkjzuEN{rYxH z)b1Or8BfPol#LY2V_GyP7!27g3V5QB> z5@1&rYx_!}g<$8CW?zD0C+!5|i@xzN1h+F+X=urF-#^1C##o>6{z+0rpJy>fS2ujrC`zVfwxAq@H@xJza zi*ovu#uC*U#pS_JJN5BM7p4pgm;$@RtY=0Jba?%u=q%`3Ya@j#$mD8;4#bJ?uS`I? zVEA-D`SUMLZc_VNcF1}_^&>e}!DzXHM+koC5VKU8U69U#mMW-y`P)5@Q$1KlRE>y+ zmW=g1Oz|6UBN&SFPgc+P9!O`8PCujeVxL(7eTq~%XzccaeRq@QBeTHr-H+a=J2?hBI- zx1{rwtzI#O<}ulj7yH*iE5|-l27v7C-LNK4RhFWVm$fzzqz4DJbK-CAgC%z7Lc0xDm(o)?I8Wv!(?jV}%D@|$A*f9lTB zcwp;YeRPShr}w6D{crtM!$WPC;5ZkL!qrqC8a5?(ISlKk(u?KP4$^TQ8r1v`r$ zJ)P=J`MvJBC)t&2Z@7~^qttW^wD5A!urdF1@BDw?>=56LeqV&s3R4G|r-RC(NjE}q zdbYToNi0!i6+QzLTISbLw-@HG^FP7ia(vyQ&x5KZGhc?muJbz8`ygoQ>9wO4v`o&@ zTnnxx9RO135E~qf8qiBMi`DNy8~UT zd*;bLP!RU|<$OQbg$wg0IVw zuB_Ftr*6Z#O`a|X_4gD6ZUt3uxBgsB>8*mBECJb+PD8el?!0H^GSb@I^*GSGL;ckf z1@ddX$()9p(znlY>b8tGwcZLXjBY=Ix*=m{Mo<{#t?k7J)1>F??kCk~?X3O#;^??4G#PacsfkStuELE&85;Y~huf2M4)R&39(gSTkd zD%O9TYq4H<*WGL2XSx1cH|n1lxbZHakXLb4 zY>yPf-C{Wp2lW)|PkJnp%FA{ySU_Q#DJNs^SJ3bErUhgV%&f417ACHC3L?$ya!iQX z?_1cyj&=IkxkQa`Pw9BU)?|WMFLQ73Vt+WD);fXwoo&Lza7u^#*MI-;Y4-nzjr-;E zBOFjX;pC+~3yFgptGwtkLbzX~mFKQrq2a7tc1`95&0L07Btxst=PjY?O;b9aPXze~ z7SC6MQOA!@-_79*!0$E58=#eIw;NP|gC1#hr)X$mtJ`ENIO6^D;C&$fZh4DZ4jVd5 zG&_?8GM(AC=MwXxhFTIAetK#H3iD0N{6YTJi9giMReM8rhC}O*&v||uta+J`qy*~~ zNgp;L{q?plg*9MEoMFxluxq`%Z#L-g)?^KZle`I?hMj}1ud&Tv54P{!m{I`yOXX@$ zvbT8o^&6G<-~IhVbN%mcMexe0pQ-n!yvUoFiQ=m4pY{r-Vk&F(tfBdk!U56HLRqs3 z?qJ=m>^oWjnO^y28XexFyKJW3 zTfX=F;!v`C4ZRw};WXQ?){0T+OZbneu)iFW)f?3;v`D*Gc?Y`SbN&4SP}w{2*h~07 z8FWW_3ObK_qoigppZBCq2Ke>aFSSgNZ!)#UmqY&hA55m|XU&s)(|P&r1DlJB_W8cz zR5_U$q|O&%ZLJD9Z|J19d6g zfAwNIUfSYNFhzIPQ9oQPvuyEKW{x{Z4(hdr&WsfwbXT+Q(a z&1$$Qdt@~1f_Xou0BGf@^-;^g+W7dVX&}FUM;*-?!kbwmEJ3EzvgJw+r&(&ZLyVq3 zB|B)?s=EjpOOamV(j+$(TG|ja!UGz2nVKa*=L8?TOZClMk2o7aOKw*6p<(^3zROH1 z58vnc2(cd0a&CpeUi;w7k7$s2tMlrl*J^d9dI`$^`1i-f)O(FIg=V}(7M(}v*yE}n zX`#ci&LFRQ(vtd%vb|Xem5W8D*i!xB)5jZXAGR|w%!Sy>)>C9bYKA>@xVPa!n#F__ z>KeWvZo0-&`B~gMyg?6pn@ve#d&<(^jX}Dq-TNJ4y`Nif*OC13t}fjZWU&WP0y$D;pTlGY6qqC_1Wb}XF+S@#CExBJ!&wtv|^{b71D*J zaYM!Pvg5Vpt6;i3 z{_y7Z0)--17TZL@d=(r4|NbT|(> zZPMpk)J<5q)|=MQmh;N84{_MzudlvC4eSz+o}tUY)>aShBp|)!$ffrAAm8C}4_DZq zTo{*K3M!xXtQ5nlOZ9jsq@UKT)G7kiFZwT(fh=ogkPiIHZ)QYV^sS1Jh28iXts! zFIxZ#FZd?ZP32>q`^8fFB-0YHJLBK~{_h{UTgU&nm0$b+^qi2~?1d{uU6wZZVmXQ_ z-;wc-qHm*Kp!l0~Qn_Y+2Bq(LBW?qv6Ljk=jdG@Q|5+vc`8`+U+0ZI|ecW0@Hm$E- zME7ILBjxX?TZ9)VE2#P~EnMBtp*yJWa1fxK)wJ)g12^u+R==R|;OIR^P}yPLL~359 zlXPhh2YtL#yA~Ojq#^Xa6x%azc7kxKyVo+E>z zcEZjoQ!}nW>+EZGbTf3bj<14g80~MnDv^el#_KEtx=v}wrOl{2qQftmMts(I#-Dh& z@0{LX`LvO9!a3;7&@+~x@@nHFF$(cDo5k+m`MbF|yBu&aFs1Yt=NiTIyxpT6p)GHy zB4{>Z1}{u8%tC8f9U@Id$g4sR~gdf z2v;W!a0l7+!9HOMFj2FE6=}BM@13aS4IQZ4)UNKTo(OyG0PpNr|^*44Mx4)StR1n~~#noI|s&U9Cw`=<6Iy?OgCUKh19&>@x3e*14d@ z!;w4WAk#H8r|!w;nO)3)emT0s`ooCo?_L}+zX|(rc3!9izgl-XK~YfkB4)~S_?5TO zYf9a_xN%Jy4e!%KZ4EZV&Ku-(u>+Y})Nkr`c=^m?IkeE>?zB*7{>|i!D6(6Bwl*eu z9EhBUbnJ6t98DD10rF!rhI)8I3uVsZ>3Su<2D(vq6U-f30ieb*-ikNYEob%k!{hZeDO~1YcU@({nvm0(B1y`&n|4txus$? z{1`h!T-({M93U3k)_uMe72kbSt7syx{25HVcBROjs5x^FMI%;P?>89~-o5Qiqn1q! zyz_TdJ@&SRR(2UEZt2OA_x+^i=KU9qq0uYvaeIQ;t)+V`C(&{2eeiHf*SWYx?6wn+ z>|RUuwts>az+WhC7vv5t+&(!r2Ac6x1Q%lKy^Z49bjaFE5u~f@*Diz~Yus!=qn(`} zeba|1%sEfZF5HUA6QeU5?#wHYP1zqcDi3>dDcqcRcEv_I5Bk&Q*`$NEPB(!T4Cd6W zBK?nkcaa}a z(nm?3UHI*K8g7v#CF-n z8;_#IN%f ziT1m%K8`}VP@?fo9A47dQPgh)-(C{2dHgo|_Ib9bZ4O-`-cuR3VOTcw%7))wO*k-3dSm0J-h_! zG}-m5fx~f6R<`wn=DYNCO9I*Pa?9Cd@A}?KN-V!Ceo@5K9e?>lD?6<083aFJ`#`B! zZ|v*G-S{G1i0o=h^}!m?Y!myjeA{Sy__JSMs>!ZAFhnKx^C$0a>(TK$s}=@`>DrZH z#M<{d)LtyP>=?B#)3(i?Mtc3)**3&}# z!Bi_s|5tzi5Z<)^V=<|0DdzmWdCU-;P8j-7BA)hh^nfnnX@@-PPOQ>dGlBSVGv5c~ z`&9)vbEvJ{ftNg>>zdw^iNnBBFD(^zq21hB;jqge%SSB+`30LZ=fm!_qIF6-w8PPQ z@$oVyUvN4Wc0TA;Z30noVG}i*)H1aBLa@@Mo47|NE9LvQg=Ui0;)d(2^T%*o_zPp| zgQkPh^YOna9AphBn4Sv1a+iK@#BLyKwv@Vm`R(K9DNM4x_m5Kb#|q}w8_8Za`x%9c zv>y99i_IMs|AM-ogLHFn0qkr-{he#}JH(UUxSQs7(5WA8XT;I-E~ihIhjBrgr_8dFwecK^Z-4LQaSF{-R=oZY zf$}R(UppN-u=J!=G(Gpyb@+pL4#&@)t~C|M`#ual5u-R2=sdc4(-6wvI$c;u`8RX2 zNTmFSI6H)JINtTP(KO`m^fmn6R`TB!+Kz@9Yhp7sQi1d5-|Vy={=*&>?b*nk3f$QG z#y&6j`v&gsy_(8@Rgn@){Ru^G%xbmY(vxp3%lLl*-fpdxAjuv)QMH()I2-=PO?C?!sn{D8J^pS$A}C zKJ!c8?qzfE=PZsylfPZ(%Ry8g)7ko=lz&N=ovo-mLcgR%3M-eXaUJ?Oa|!Qr&0c?v zS44Dp>e#{2jN5WJUbjmDg>!bg=QBUfWopEMY9lkQ&eG*x%lRG(_*T(IK;6 zv$+cRYdyIrPIK$#211QX}VvUl; z-1}Z<&yDNVkNePT*!Up7t>pi3)~ghh@0iAdz(B>W{KKBYr!{mvrin5sm3sqzVwda( zgMPbEdEV`_kD>CfFTXMi$M-rLG-)p7?|kuPB$YcdLs!a;&zf7*>bs!meb(;4?l`_Bwn}QZR1RCSH8iZGkA^) z{)UrUt)~1Nuj6q>lw*`N-renec86b!q%`>NzHeZXM)fu?LgG&4F?Y$MIKxUO;2km8 zyY;$j%pHke<&ih%B=WO4`R&nOMb=HrdpcDa3Y2&3mXVw3dT*Y=4?H~8>zcF}8|k2a zjLIDIseTT;Uz&(=AA9T=Bi4`kS&tabBhBwu- z+t!N{sQj~Yw~NcVsN7qDRQ{?^D;p~R;_4o2>3Ub*J&{i3H?8gNPW_=Rz9^&ef9#SS zLiJwnW@*QvywP_@1yXtXM@)*xaqn~L!!7B0U-n2{K>j+n+!s*(vgM`o$$z17+aPXg z#068%KV5OwZ^$KOa9_&5^MLcq$lv=~&&A|FD`?JK?x~*bklPcp$^UfMaT#>IvnHNh zLj9;vSK&(jQrp#YNMATvIvMFxTjbSQQF)q}FN;^89P#6GO)33pFGkZXYhFD%3ibY8 zH*@)T@w!f%nUep)XUn6hUuc|j9Yp;zXsJOc`J2wjauxln)pe;niOz5O~t@?00 zT4LrsdR~qRb9YsvJiU6gDfp+Sq(}e9bXP0BGR_>Q6q{2qg~?nXsMH;mElywi0zdVD z7Mf%(8B4_)wMlOZ@!$G)Xg$vi`QT>2-i=^+*NRe_ZkT;8eghNvKOcN-9O>*xPh;ZK zBl%)^SkHlCc`{lp63cVru#s4vbGz1w0R8z_uQ*qS9)%7HXiWt;Jt}u1 zX?e=5m83Jw%g9fo{gVVQF!q|uUNP)e_o(a+Exf;2Bn~5fld-sZW%||S)wt)QsTV3m zdLnab>rH;!+WHtN$UiU53YzgJ6N8~y+S{mM&^iZFx;lZ0eB~gTc7$isqQrJH*5YW@ zhd69XhP0q2Xgxt z9V&mr+2QfN`KIlbxIy1>>UVPl`M;iV!T~R3>??a3?uz_$%1eg)_0RN}r`WgXQ0)|t zd3bDa(S;6WlkwQ$>P}k0{^H|@A7s@28hPtZ#o)2Uof>1YYB$Z$o?c};N@1CyJ@o#U z_eT2{9gDMdjwSzg1HV!@j$giR#5SD&=7aQwYw$9~ySN`&OHr<}WZP3xJjR!69Gblr zFJl<&i(-c{-A|UBXl9oO zgY@|C3;GEP!%KVG<~ju7d{ZxWy)%aDH_Ri5mOX;m{$J%Bnt?N~B7I?SL`XXLDf+;& zT(Hq5HHbbH=-4FwUH+zOO{VseWbYgv54(5G{@c`!#aw$CQMs~h%oI@as?TDvecdy5 zuj0^e?yTBC%QAKI(bMLE&40b0Bme=ztcrK?Zc(~{yw z>z8v<13;lr=QH)c+8n#TFQ4_u%B$ee{jd3-%!FMh`nERuqwq9Gt7|^AeA!qZAF%OV z)7x^8ZJPG@G>81m?rhltKf~$|$wgQu7tL=tz8mSa(TlEzk>8TJD|Cq|UA8B}pXn`5 zq<-5dX=~#I2EDuE6;0{7#S>{fvL(~aCJ}$;|4!jhPN(~7)bEtdj(O>UL2ZuO(Wg(g z{%3?a$hV&N$cL6`zuiXC#}2lC^fgz8zg1gYw#$DxPgZy0UX0@ts|r+fLKym3-$@v)ng;*MIRgjwr|4U1#nGlD~vdYaF)#quv3>LA9$(g$T!zZyp#BrGKV z<8E8mfF<#`9@K?G5mGzJUmC&p_$h(S6tSp8~thqJDb@m za60+(22LwTyN7F;L)WW)nj`}ot(S?vf5qm@Y3apT5J@Zb6~ zpZ>k3<2YpI;>c31Wt6BlOU^6O# z`i*RcPYm@3>AYsl4tC|kR>vuxvu|52Q2pU{*fLMp*~P%+V%(nB$6TBra&Podr*wz; zOzmI$|HG&K|9t}}c5P3g4;5SJBkouuTxfP;DT=9#?&4@g+ zMo8T_5Z|;rL0h_87Otf9Z7suT`NYrv-aVYs2c5K_aKZGCc#a^dhRariLPU=mYouEm zAJ+*bhP_!A1`4HRSmomQlhyZ@DBv&K*nK@TdlHjR!=4XFJL3YDy8OPFM(J-I2U2%R zD;hYA%FBLs^4QN|crz?&gPW@|=F$k-S4UQjJ%OR+o zb&I6@oSm;~?gPuU63-BoE%J`jr@g422cPW%nPs~t*E!t8_;b33!uOvi-m`Xsujbre zPy^1Y`czT~sv8W75$j3X_;?eC%P{giO5wHml+Q70XL-p7W2ztJWgB%bXx6-SHMJk( zpLwLiF1&gn{sM`hcHnAo!5zQ_ZE zhSO!LE+iPmz%H*XyqHeKw(BFKRjja7>x~C#wsGSWV%M%8cn;ksZqvA6kf}bsq*rE1=dT;c~+))VzXxdIF1i!A)6uVLFzRW9u@WV!Ay42jvsT{wYLX%F@RGKZ-XAbQx zc4Pj+tyh@s8dDomGRa@|bOD`5?pw5qhL_GT$7pKS@}yz=6F^x)z)hMB`5gzuO^5Lk z^d!nprB<&=^`Lxhs7u3#z1Q_03p=*l9!8X%_%w{VD>KS#E<-?YDn5@cf@YCq&~U8#F!?}64GHLAA)OuFCVGKB$obH`iT!GHJn z58bEte|#8J|35n>R83xuYQkUQQ6qx(xT?Rpjj^a}ZSc!w_Uwk&Zv3GeM*;TO%j!8k5cEvXW>GHP~feWFv1N#i7?l9zh z$2U~%($9)mvZom={vJIIX4#&M7k-cp3k}&uQZB89_fKJf)-|cYen5g=oRcHMm@X5WlLaZUq_7XM1Jw@*Lf0~>s_Ps3jM!l&OmzC zw1YWxy+X~xq{*-=-JP^(xJZ3R{)vQUb*B1M5At?3m#E%~J&T?$0poM{mg0H8wv?Mf z3*naWWXJZO;yx~X!FSmr=(MwQ#a$yuj6P~l-CX$b?y@87I(2#$vE*Mr-IKbtu=~Rg zZ!mSx?6a91zQ9`Vbu$}eE}%P zcfzc}pk?vxr>kHWwzT@004Sldo5UEJmGeC;omiD?)+iKrX&}M8nauqHvoW6aqRHV{t9KifPtYNq#N` zv8Axf?GHVs^GofudQ!dCJgliG2HCBF2PmxS$a=L*=Ad;7N~u2!6O2#V5Qn7qG=p6} zCa9L`m&uNI=?Cf$`l=?@kJ5Z_Hu+ue97*lMzcl=2#nG(3WXeL2>E8KfjPjIS?fv;K z(q9$+(s~Jdfl?56Tm9r5q;?K=(~~=L4ujGudP6cm`KT=qWbhLncG{2&)~1A3QU4G& zXnq&Vd$2-IvmrnJ;VysThl*X)&)BQSpFKcj;N?N?p#SD69Vu)oUvxZWLQL{_JQsG} z%5E)Puh3z~-&vmDUP{;Zaou+Jc=C&S8Z;GJwN|GM^;>Dobv^`k7Te!14I1scXbf1p zbn}-$*rmJP-=Vm{%9U>?k$w`LkruN*+q;G zG(wo3vB<&zhQIS%)_E)=Uy6-eeuE@qHfc(5LDLp{z286IMpDP zhKG=(^E3dOl{E4X zVZX$m5f@2(@b8Vbu;ZnN;2hYM%evwh=i#6Gqc3$=+tTT_+wi+3CFfrqxeUL`x|;*( z{Ng|c4a@ouRcEMM;j7UH&EY4wGer`}m;9*lCf%^I z6@|UDfZt=H6)1ndsrWrJlz+cNrr7Ngs^T{yT{+|NY^pE*Q>Rn*VELRYF%+&m^UHFm zo|rW2>?f-C#BM#Pdq$0Ys9S&ZG`3QowHg%7MB)^EOnYe15Ke zw2#tRania@(8akf#bP*l&BXAmY>1lcNa@nT{uEAB2HLME{7KZiZKL5SHyLQ}r@&|* zb^Kfm$nxjpQMbi*tYemfRnri(+G2@G|`8yQ%)*!Bfskteh-4iiKO#D!CBH|Iw+s$)rv>DP;Dbne>2!s zxoH`tzni*44`eJNG7xtDxvaVAIuE}3*~NZeGUE?)l-_d7#kZXa@&SWAOmHc*TyApkc?0r0^F$wz+PP z&-8KSE4wB_OIy|7k|MpNXGxSXwD)Mk42#vnq zH{r8>+f#kcdqUsXyiC7BVb=|9KkyX%-T%B9pzeg^3l7)0( zaF#BGBZnqBYMY_iyB&|+ps{Q6+jvlrr^Znj5?o_!sl3XBz&@d%MWy4g90mMhQui1U zRRv?H9#!$bovB@TKaK0?9B7^=`G;b+drF8IZcPG@}cphtkJnm;aD|kQ6(ME zkN3Pl?ZPZ`i-I|5`<6{zK{lf9^9b1G-|mRN!Hs29 zKP|tOaYW<&kES5q|InS8`Ordnhilek*WMMd6?S3U@L+e+Z9>;r(LI{1pT}v#pok5I zR|?TC!$x}R`9j-WceCCAe|G(hkss+c*%|az>oC7bZCAiAzVcI033N$z>I{A4WA}Px z$WG+TuXXlLCjS?w3fzcxHlDY{4QHONUX6QPc6Kv&DMi|KQ;2cd-r|a=^}O~O{X_Wg z^B=2c+27T(p!4pym<>yMN@0gJWS$oF;|qI2!R- zs^(oXK&5HArv=jG{RfPr^WeJ~E1H(%Z6>ZKEsMJpL#vSTMK6ZZ^doE=zgGv8pHZv# z2ATSSQW`FNZt-XbXnyWUHS)`m6*#9*`asJxEu>4rI(d>E&)c08S9@#ri*Itv?E3Ga zuyp?2H-jvYpZY_*mBmV%TaLzYOt~wW!W*;nwrxY{KTLj77(DIm9+dzJ6)h*v0VRLB zmU)0!?uz#W~?0I!QQMhJN2)i)n#TIH;;rYQS;xv=G?BX`03tyBx zO)m!*4)ojs?klX_QUli6yfI&kRp6IwqyBr~w`I)FPF3LDu?=fi;#PF|=9K%Fp$k{c znoHrV>fx)76vmom`5rC;yEgn#m4dGd62)oMa^GlA3OkJNSw92h&qO9VA)QTA=`8|v z)TE1-g2L92=cZt}?v4eS9MZL`BM?HdT2lUq!Jr3#ou&TtFpk>RXDzSZ;X76Fpk_Yn1Gi;c~o*O6q=tF{BR1Wxj3i;^*`(^bYn4Ct6a)xnmgU|Qs;26Ds12SXpni7R(gS5 z%g&z7=Ai2{&ZR)BnjV_(3#L5xbf$JY9eXx1n$lm*6?XvP^Qy}2Nw?l>nhWyNLt0R~ zvnNSrgF*jMUsUVB^0Lo)ZlHXJ_fG03O#O5LEhCiWuP=%IaV{h1x^&bJiNEn66n=6W z3$4nXaK=OQx2zS*srtdpaa`k_v>sFrL4D|VM`-4*_oy>;&c1-}0?5vX{gIP><;Kn8 zG~#zJ#|QS)sk;rS|Kqn#4o?JYug*#jCGFjR-+Beuwnx+e=*A`2nni&+O`=K?K>l(+ zbLwy2C250_K&CV950$5J)yZ{1VEN-apQt?29^`1 ztgW;PreT2%tZvId$<0M_ci1J<3c8y@EB)fzQMYHi$G4#HF3oQC_a2^9)p`xm`CgCj zM8fXhCACEqsQ&t1KWc8d?~_Ctc6{GO9=0G;nr|0}v9bE#DbQbcJ&B@K1)J+uO<_UE zw7~Xqc(7>V`?9$-3tu1CB$zld#%n0-kxFUaT+HH;C1vv#amemli&YNLk1Qu};&zF4 zM=y9lm)vT<#}WQDt)gD|<9Ne^Avttjq1n&2vpGAv6P>qBNkV$b*`-dK$^XolszB)R zwYg3!;U8i)IfbrYSuxm?!jgY{--8I3$l!a`Udm@|A9268KCjF8 z3u7fnuZ!!qOB`;h&yKm!61BnNTMGs2Tjwr=f8t$-9%6r7R9QjoRi2mAn%a|%;sh90(;r z8h*rEfkAZrtnD;4H=?jyC5GvWoplQo(Dn-+n!&E_mF+?0)((9+F(2*R_l{;F+r>i1hr zZM9Z|(lyKT!@z>ERbRz&ho35>`cYk$9a;o?sC#K8U2pue<^w5=sm?a6r1~9_^3q2J zvZ~%C863)~un44a)Ux-X^Hgqcojz@d>YtlSsa_n$@4hXzYk7RHWDdurwLM1Hn=)&E zJ=M46COvlwgM80UJuSeyiDO!3gRIu|&0Y@G6F`lW#)pZaZA^9xOKK*8yrE?vjJ`}>F0?f>QGhEOO!@HaZ= zh^t#=&BebrGdwJ|igx}St!i<1!RH9*U>@W{)r_x93iCMhI(|Y3Rae%#!E?ppzAt1% zW%LDgG$WHa9kF+ZA5%8Tqvn&k=6)&(k4voUc=Y-8xOntw+|y@K)msP ze=X9LCI=)m8`i3Q^P_H6+i~b4YJO#}^sf#e)Aw^2N#w7EF6F>$k9Lg!)An2c_JCdG zZP10vf&GA{&m{fx`Ir=tRVwCEb1AQ!v|J1w@yBKCR#4M%+aqd*@(6u%8aC1(nRv5> z$u8BWaX7A_V?O0KBxdF(v0Dv45NQR?9_#A4fc%~JZKdQV?=1d&B+KqRelF~FM;fLr zfn9jn{4#|@rgNjC28FN6!md6DbJ&3GMFgnlExWl8;jLHQ3kz59;ESIvXjrLVw?8QE z!a3ty)8P~bODZO#{BYgqlr1r*l2 zeCtb;Ud=QH`7uYU+!f@%pm{p9>dSpCs&{3qy7*0_w6fuK2()z2={y=n`1R88RUm6U z)2p0AIsKpgrh2x_eAt4*vVK`!Y9gq9P<=B^r`iFzT?;_=_1NGV4*8tPe@5;1v}yC7 zWdG4A#Dv-{wuQwMx^DKUM?gHW{LX`74*ugpn?1YRb?Z#%S7$4+=*>5@q+nL^&d){f&_S?v35?e6)#)N!@by0i2eW)^MkV z1}Jyh?KYpp@XSAWmkBMDe-rmd99H04PSXcJa!tZCx;~lm!zYaQI?1jPw7g+_!YQhE zq4KWYT=-dsW-OrbAiFV4A&zgABB0R->Be8Q9bw19WjO~5MziNp*lc|wazYUKuYRqf z{vz%6!iSb8g6kIXrWPxXvCAiYx$XtUgIL`XD_X7;n5@f<2Q80$Zto9DcRHJ`Q6T$R z55H3A;>Kl@sec_`c)2zm+_uj3Ax#&;xAPt7It9lMKWTbkrrURH;Ba#@W5@$)fAtye zNlU>T3mfqpT%rH^I`Wg8mYtptGWku8&dWAGbQuQnO{Y6kT$6V`nkLrI=jjoBpw-=d zW2wL6zX0lAs>fSw>^YowUf^tsN2RN6QiDOuSi5;tPvt9AcRfJVh`9STYr1=#7Ifm0 zqkeG;q{|*V20|-8W_A?EB_BQVs2G2R*YuzxYwYOyPSnS@|6M)5jHB=D@Ez&34ga)y zmi))f4TjeuDD3ncy3dHhL&>mp)x=(dE{OM1I)v_^)3E}*L*l)Y>u-FB4Q=|)=g4o! zK6-5YrOuv1?&z-94|iBemnXD+ipQ#P4`;#yg(r2(PXj*tk^eA@AZ>4N8V>yWhW@hxzu>IdKrLfQN-8OdQj3A>}_a<8lJ*+iLEMZ>QDj zN9#Fbo5hEt+BA>GaG$hnfQgiQ`$lbvb!P|odD=JG7DoIOJ7yg6_rB|&>j%G@O^$D6 z*#0rm;!tX9go{``s|TU9346~bNtU!p`t<(PX;2L)ot#+k=R(ML(^iT&6dkFO%&){jFW?GORUjgw6n)ssV7ZmxQY3QhAmUx z=}Xg%#6cl$=ABl+6OC@9H-%{?9=g%HK=W*7DMgL^q!(ZLROhwd#(QlXhN!D$mTegJesN9eTCX-#RFB+u!b znFLw}_RSajLDgJ?*3eAn!HaAK@`)@RZAE@bJtq<+5qsBV5?j3bLRw~WVFUW_<4S%<>3x|g%KO`N9G>ft(QLC-u0^~Zz@CF7~xtq;qlCZS%J zeO~2L2L07FG>7`zli1{?bUu7coU>H{I@ENb_~G1lr#khY!tgxt=2>Hc@Q~VFX=u&F zc=PJ+6tNxb-gTqvlDru9X$ENS`S6}tAD0@|_J%f14!^q{6uwxuyvX6QpPN|3L9e&} z(x(*V`EY-I&>?7PtbI-bh!%cSCr$^#wkGgrlK0~qNlW~^YDMOS-xSlQbwBk_)5ZS} z+s_#_T(+iSIb_@Hq2gQ`w1F;%-{xCJ!%diO`c53CJ`)tQDv)h{n~mWz>oEH7C$sAje|CYz%InLyt`^p9E>j_mtN9?+^s`ZmF38^~0Hj~oYw zTv$2nA%|OV2j6&Jf*!LyTtdS#?Ru@H_@ORzK_G=oRAthVQ))x3tt$P`XX0kT6gPQ)EzgQ_df>#24vo z;GUyfKw(m+2I_uQzMXrnr*yXCjF|pAQhO%Sb&mXeEP%|(dVUVEUv@Ngla05o?kM5l z|N6^MPqND^Hiv;irIHtqzg>1pydF@Y(Co&S0P5p8rA7pb(oz5ejO>;a|0_|*OqgMf18qB?iVTtZIXHGoGlqol2 zIF#$`&@E?4zj0W(7Fz4s4ZjnhGI{IZ2#{&as1GC#+-gkymZ?jxFDD*12#keYQZ!>8 zO>^?)KVro8%+desP3iyY?;masX#L0PQ2O_!Syovw9j9fd_iyS-r#t;X?HfgpwU47{ zm@w7dof^a}?}1{J$?=#@#WuJ&W^fUQ=#{5?ItseD*A>@jQ1UtQO9tumi)xEOmfn06 z>BC*O8H(MD1aCLIPhJPISo6)4Z{5`kqo^B=IGAlq;X}FM z(FZE0d6$!WUvT)5>m@&C-G_d&Wa|(bX8Ntn5~%q)3aRSU%oY|?dauQ5R zjXvZ%b$Z+?meN(NGU(;hwn2|IXCmG1`VxD4wjL7_SQ07pG4~zKenlsN7bgy*xx-W?o<}9yR%%P{|?MyC9=m+si zYC>MoPrB4tO<%H!{1+W<5`p|Z`e{T^c;F8>75TvLW#9J!4oLt1nEUdun!dO1g%Co> zP{=`PP&AU1y(!J5S+moqNu5SXWj7B>qegK`B^4@T!a3$Cgpm1|GZ!I*_rCY<{p0(` zdtKkx^SsY>z5U_ibFX{d`(E>K_FB8)sHyUL2=^S&n7Zd}$hVid#^Z_po)?dJ8Kxh6ALi-ILh9pb?yAG2r;YBV0 zzk`zAp!ddn^o=#BzrEls>Nh{RT(|}0m-ZcH_nh5tR`MYJDcMVQTS2aTWMQZk@JrDa zDX7oY{PK{8`biXJ1K2Q{<7NZ)Q`g)|$%0GpZ_OH?X$gF5?co8o{;iy0u@d4pjCr50 zgZf@e7P70HCxw3NK>lV{h`$@imCb!L*>3rG!+Zybe=91!ko^=-!<^eQ(f@?(Ex3u( zz?`<3LxKGO%SkM%33&)WA0(Zsio6H$eS+5;18M z?7?^G9L}Yn-c;OPf%Pr$>ToR_ez~jG-irhNrnu?N8o{b}XSVpchf!V%#S7eyRtVmm zv3l@!oi!EhrBLKOCXYe`;?+pT} zr7i1sU5^vo&8!@2axuM!PY-0EKTK`c3VhC;?pL{(KOySvrKmr56~`U*4S%mm#r$ad zrr{3pT2?LH?t%IF;cHnmq~qq(DN~a%e~t*vA_agcXLe|zU3Ke#jp$E_)UjZ+6D@Jm z6zy+$oEU`o~sQ$Z|3l9KS-yO?z!o)=+92$9cwTj zvX1ieLGNk|HI_m9)Gmim0qVb>u@?P{^lb@Z{WpfqBY2MW)ebL;3keKaJFrYK+KUo{ zQ*@}`sNsqJ&$&D_7oO97{i=jN##?<-DFNeMSGygC_7`&CQ$Um8!AtWWECVwN#p4Q2 zKswKCflsw%R(59|R~DXlf5z%vw&JA|sP}X84tDmr)p+a>=$GL|>~uJ{8khzDn3L3^ zz-;(WdXa6BqWKW>|NhNj%*+d#*#9$8%da#dK0cs;(+=x~PJ?{Fm@`vOR{{2Np0a!Q zusf8`#qe?A?4|8Oj&_g(1+$Q46nhs3WBGBVJ%rQkZ|_8boGhllWB;MsubO)dkQd!E z5CP$%Q#PO50~lv8x)-MjYJD#&aJpf^bGY0M<5q1%eA5l!H9PJrZUy|Uw`m+ZeTovx zF9t4YyM0O?VD_8&PgwnxK6;(NeWcauaQd{YrgkKbPvo1AS}maDCXGeg0GWFcb8tG= zvuBDzDR7anGCu%NtuxUIrx(Hy8Hpr7{@8$AoOY9ltwuO4q?K}a2P5|Fo$p8iy337O z4aj7V-3{X`u?|pq%dVz~{1?m*qZf`zRe+rzJrBkRpg)OH3|$MzJZSoc`AQZkRO54C z+vJZW;J}?OiyT1iRhME<3Z~7U#;|XekW+gqj-QR&NAlJ5Y9`^F~|9duw`<1JRsLeXA6|mtUmFJ!F zHN3q+6zcP*&X6hsdH6(-#)tv1b$dgzd4c3+wn+|q3?YMh)+-O~N}DcL+zFk`{6H&)QC>{@($ zeZekc_;?ys>guj50DZ3`+l&N^O!n1Sy zJo0qdXRbhUNT8bQ_$8=slOo7P{j?Dtg{U7{5wjfi^WVg}v+4C*W{&zipT%kddTh>% z9Zw1cTfLG#Zbj)Vr`}kG z>OP9Lr@-&co2V6n*1WSXHi>Hr45obA`&fRt;BkN5pP3tU|5`sy<-k3M**t2D^Mz`9 zwvV8z;?aBZYPBFQRb@fs^Pz&+o70~b4Cqo||FGj2UBMV<|ARA*1hM*OiN9dmqy7(5 zTV@L`te7(O`|4?e{<_6yay*n!|NMrHO9fx*O6bFzy}(Yz?~a%R3hcKYeeVc*qiq^Uv5rD{q$FGZ7k?#&VB5N`jhBymLTu+ z=s)5}^+0|;{$@{EhsG`yE>;&fU3eOjKhXg7m2^j2qrUfr?dIrz>!qLG=)Xsh7>{br zON|!lEuws8Z#K_=yb$s~d|c@VQ$c9#O{X!zrvKmRLwb#7hdNS_pR325OQh--AD>|! zZ3N}GZRiivSi$TuOLiGvS|iXo{(ATPaBIw;ztV^La3@&R5Awa@Dr2@D%GYB{T%s-7 zFIGJif%X^QGj_uKN!@;S71p1>(ueMDl(e!3bZhv=wl8#NTsx0|)qo_xt<46|`gQQv zU<~(cyP}DF=}LVZ9$_EE6XS88)ugfdZKt=pV)*V6r&#?&-V}Av12!zW0N)dJ4k>)FFDFUhfw>LBOZ-8pOxa&q#l&=$}-e)Kv& zkdv^7=NAKS?nqVeMowN_a04#%aNT7DNNSWN^#JK#;)<3S?tFC=>T}Ds*5j}M`eV^w z;erQ&tbRCOiw4e5xbn&g!`p^lMt?}X^fY{dnGnHa8;}dnmARRqyjpX{Nub zlxmJG`Y-6ygCl(nFdlPfdk_V~N!F5)nBLyT(asJiU(OAifcVX56`P*F)E6etY`V#mdfGlXiK6 z+);j}nI~)C{go-&KPZ1H9QDtrbflpE<`0J);F5)^_PJkOAzoA9JNRBZ`17^-r!ic1 zu=K};f=s~AUH28Y!=(&M{Ws@_Li|Nx@#EqkUdvQB)iAhRAv-hv6BjOD@My?2*o)s@ z3QCPSMLjt5%Oi1k4A|9wtXy3R`U%Co`VDZILR~F0g$H(rod`OUg7$rM?YDvb`)xl| z1JR#Zr?)eJo|C59qkW5SAD3|j4GnK5%O(W?HXO~Hk_w0Pm9u_)aD;Lps)x&iV7aDz zJ#?NUE?)}*eQi+xpkCEl)YomFlZ^V8X2ab}ps&Ap3%&!OJi+ZM`y26#Czo)!d)!_e zE~v{jJAN7f`VL zeCV^*3-p<*4=#s;eY57N99-rHR+gA?1x~QOem+qg^c-%jPqf2r5vc_iP0^pyKiBk- z4_&0a0{nNl?{#1Mul2#~&!8eaAm9)sZ?PWjbxA+ReuR|ITw{mn^?be01+ZbP+@j^6 z_jmdbzoy8=74os6VJmMr=BLY|K6|v^oMxzu=~|J0gU0+;eKEukpZ8b#kUz5n_sOH3 zgJTqVXkU5CMt0Y@=;|V4wAW{|&l>GN+{rP={P`<=D4!L)4eQYUm(f z`bNWLK#(8XQ`UO0&kqXx4L|n&oj&;I^J%;t1^KI-eK{8M*`$*_&qWOW><&TuA>m8i z(EddwTfF??@AM(w1&*c(+HYu9HNo;NnsS-#pPQ?G%tg6eU%w{i&wEmCDEM1_h?jRm zeJR>e_Vi`9?W8VBv-a&*r5j*=*=o!;M*H#FUPc1A^kVk)at^D%*2owAky@mN{XXfs zMvPJah623f0s0T6$FSvB6DV&-0dKJKG(~;;P!)E(dnYex4tA7HQ?J;f{yI@Vz8j(} zchc1v^UERryeoz)KU%@ohj*I8{-eLO!v*y%uPU?cH-60JJc!p2D{kEg@g}QAdxU~N zeAy!}3L!t_6W@No12ut*^|-h_=HFIq6p_ zAa`p5`)N4xwW*Jb;T0oe*wyOU5$Crc*Sqr(pG(woQgF2_tkFK14s!m<1NJ=NOqWii zKZf6HaYzO^;WFn?kHjq~L;FPXry1_Tkp0>p`5-`kE>B(i^U{dg$u6_n~u{2MnaSo z0@!r@?I$51Indg1NC249*HHqzd1o!VT9%U7!QTmTW~$-&ObjQI*WNAzaPx{jjy#@s z>pbZ5&&Yf21>SjGZz?{Ilx}Fj=}@PWL|zeu6N%#B5|p5=6z;LL12>002DI{n=YV6W2Mp@<~MvC%5~BxS>B#eIKx{;I~#A$aTdy_b{J(bzh#^2FOT1pZTBi3NU8Nk;t8&1#(jDpAb#K zbk1yqKAWH7ylQrud&&7GJFQcrWs6X+x4i`Okw_hu3I_c=kD=OI0m(kiU15k)GY(=t z!Kd8)FrB2cF0v5iyOw-f4Jb;?=CHdgceFo8qC92)bJiZI*RDm*nP!aX6Tz)c7@vRc z;R@V^fdW#WhT*zhYuWZdf8Csn^4@DQWhmD_C5!2zFQ3;%dyGS?Y7~YOjp@@^lo*2L zNVq?rY^DHv!;aVi3f1JK*!(bgX`+YnN29iTBIb1s*&_g?O&8@OH?FCP1r!at;m1eb zbFS7L!~g2? zMea}CJAnGnKFeYChx31T*k+Qa&)#=VoDNy!qI}-m>y?N)X7;RpGG$WE>I)=d5E<7K z*vv_Xrvz)?Xbn}s$`dS4quip$k1g(PmM2+1jvaX2g5kZ*wQO;n6aLOaP8SYgo7Kyr zDQtDrdF{+T*C}~PJ%;O4d2B|M=tyU~DbA)dCn@0BBdS?H2d;NuGa~%53#@<5UDt6F z_3!@vrMO1_V{{SyXJ`2D?86-}D#ydoZ5IdlZ3$2$UCj0dR}U;pM85JtDz+A3Qkrxa zAQ@c8{*;}~;mc#PxTf3Wu$4$=+z-Z9z}bDe18Zxi*bnxmE$-#4K`h>Enc^MTO2dY` z4(|kRJSLpe1)TRNdvhc3ylUo493cH<)#QDE5e>D+v!e?$qE!lcdz~8Er2FI`xfs4xhS-0 z0rmoFm9@jLw`It|3s~$DRdI1g0Dl=airt|gU*6}_s6TCv-t7~B+g|dck^kM_zf_0+ z2RA!)j#s9^T}YToH6Y_$<(LX?svXqN!%k7yZ!w?EzE=+vVu90!5pSbFk1*{CY-hdg z#HT=z6Z4%dYKT2$+6I8!F-;U(eSUs(vPFI2%$7(DC$`V;!Qe^{OG&p!c@3{<4dTu< z0VaS0oC(94P_KE2Rwr=l^^;=o`42ud6=3!r=~o=m3fQx*Y{6DQaw;#~22r@hc?+QM z3JGaQg1+65zr~HOJ7;BkGGeTYw*H;qr@t zcUscJF#XJwZBFc@fO9jq7{W=&-VTf}6n0&B45d#?xRIrx7k66S2DE; z!aL22q%l7^#y#xe`s~Seqw|6D#5#*Go{8zr=QV&NPO|bG1^my>vH1KH<-Q=SR_EH{ zcvx-X#(FqmJf=0{NfaRAhX*B7z}E#utwk<;xA8j6S%sEX3!IQ=?5&?`EViGz53e@S~C zpqlZur#MJ8$Ihu|2WL?IzyJG}>iplo*@=)W?*wpLSiV3Uk?CCP%4V0v!VRXtiF?Wx zY_?J$r zSF>01aQz0ZWA!0VE-z%~ux>jX?0^ez9{bN>TOR#-7wF$OW$tnUkT!E)j@9PevfP}* zpx52<{t;GZ=KRu!wt$4*dv7a*yM2APrVemg&o0Ltkau(Y`*Cm+>J{9@YR)TrC&O0D z7~gEHmR`zZTK0pSXl5Km|ApyXd-TUTaiLQKAf3N(ya!^*<=$6-n?|+^uL$7%yzhGR zu(?ee{UIFVkJDWfa})5Pse9!CK={1D>jXfqS09D>$aH#FIs*z-#`i>1z_SaOIK;(+ zJKO-_!^zU9moeV<5LRQ!?~AzTFKxX?EgX=?JuJ3|0&G0N$7=X@fB#Z_M*d@PwEiy* zj*R0#ZxOghtf%EI0%V?QhyAD5I#SFD*9-0l0fie+ zS7(EqGi)<;0kG3ZRD;DO{Q%2P5dOl>|(l)%);U&k{{1EgFeFzTe}^&P`h?7`bodCorKj*&DVO* zUf_}{@ke$7l8gx&=Rw~2`G?UV;7&5PUe%!dum1ie`(^%-{i6T9^6l-(giZ-QuA$V- zVn=BzB9r^{6w8U^*<~1h;6e-Fz`Id}rQl}oqr6pEyDm(+r+~G_KPUb*+o^V}SeXo* ziELB}1|*Lx->e3`p7i1ASlgIL4Nt6nY9|v~DnU-Hhjnp*lOxl7?GRsHIl36+y|T~t zvr{>pnkkOJX}t)(f8ue+8)aO!V2Y_j^FUUhbP$CdfGn2M%L-h#nRE$OcYig?5Vod*_V) zjp>Ik-;D`}aPr423d_HH!&1jxfTBy?uh@M0T>KsDThn)~M|ptc)6s1?fWljI*p;D> zQTT%8!rwpY5X$q?5>>nb=@a^|9RbPL&*j+=PE<40tO2bXr?RgzGQ;MuU*Hl6?`_BY z;B1)yw<&|3)?IJRXNG%rIoeU{bF5CKAY3uP9tU}+SF_VGoM+#<4(lC}i&c(?aFTea z1nqQZA0Jf&ymR%qh9uz3ySM)8z||H#t;6(?t|>frvf#KS*c9a4h5hlU?-W1oVlIRW zXZGnB0dik?-elX=;I$!a{b`uYH3L0b%>PUrgo`H5Qx8V|etMPxqTc7zn13X%;KWJ@ zXFgmRiuJiw^ZoZjfWbQ!wl;yB^M12-8lalg?miriNlf`A?CzLN^JAfW$%BpMMKu&` zL@A|RsRB+PwXntK(ZT1b5I|z6emDuj;nQhaSbmBVbds=s@RSXHl>w*wUY4dHYTbHV z0Z2ZUooxYR@^ueQ*)^w{N59@ ze!X;jw=)HYPhNh=TMB35I#%TUTqO8geTaW+L)+VdanDua3!E+?1&kL$z!?eW1l5A@URPSO(02btAVjRkPHr(?_! zD>zJ~*%H>x{thJPr)Y%x!vE9y2R98-L_cgx(hV^`zvsQ3#O7<-t)=KEXC1Yi!bNaT+03)CEB~( z5cm5d7iP!f{{I?XZ+sn7-fxm-1pH2L>sa7-2mHO@k*W>m|G}_lK2Z?=sHpa59olaU zoR3!^NSl0{T?%qDZe}$84mVk7bsxK-u1Oz%?SO8+Pi8&S3HjE$@vIPs1!*(sOBW&h zL5Xf0P9qdMpZMdnf{t6Z&kL|))559s6m$c{9hdEacSosK*8p1QTC!IN2-9ljSpom% zra!h7aP4&aW%nrPR%9>l%oh-T#0t$xH$XlR>~jj9Ppt2D!WnI7N6G&8v?wTFIfz^+D<%#vd zWAzjx)c>|Jfx>#S&lP?bgZ8=5aY;1xBi~#VhoF8y>x>N)#DB5>`6^07Q?sIPogC^d z;Kwe-_H0`16^n=DN)q}_z+a-2vpfOoe`pE3F9R6VGD;EK@%$NQ9nhacOC!9oo(>A= zg{WUM!^;Kr4JL}oV1Mu;>)BF)q2Zaa_OmwQb1Z&MXD_gN7^Y^0;p*DLOnC0ggVu~6 zrsGVtP!Zez{b|QHz;mQkT^3_GI;cuiIpT9N90diGzn|YD-^X*|klu&l#Vf3^olZ9} zU=Qg{IeoVw1k1;#Gu@9CK@WAQoZxs4{M@7O%V9P^`;wvPq>CB}~r!PmGOWbET~ zQSW=*v<$?<-Fe2?KV3Y(%>m8?e^6U-F9hV$H(ItbSPv)rsas$=?aslCJ%Cadh%MUH z|L*Q0keo0f%;y5OUx{7*Ql;pB*pzH1^q(IxsRHx4G)c|??V1gSt;F)pAFl0!&s{w+ zo{RqbMyBAQ#}2*n2kSB3XO$_g6zKVGve$z0U;2C4j7$j67PEO+)@98fqyFM7kD?LKesl%d5LGZ{Zta3@KJyD>qBX%XR#U%heG|? z6?)NE0RH?;4)MYE7-;ouF6y6?A03SO;BPy`2mKeFgJB!W>$6JCdeon=LBbf*8L0HtV#vAl{#V1MObddZAC^Q6lr_21K$;e~)2{=uoOF!+wc>cp> ztBL?)3Om>@h>{uJFVGL}z1vaofTU~4J?Pg6?Q=!d6RsJMdjD^~vr7JBdPRmo zq`&@SjC)=;fg7CBxZ-~I+ z6V8vnQw}K7+UJYe$%xl&D1z|%$?Hc{0rEcfuotvl__#n9XQ$Rv72p&H#8b^&jk4X^DOuiAk26$YAd<6Q*%r;=Z*+b`g z(AmH_xlxW)_;Dq^g>=JC|5WPIBRHzQJ!*{k|bGVS0fEOV# zRJKZb4e0qy7hjFli!V49gqs9pMtepw$a`9X%h=~XKCiGE1dPd#(;yKJ1nC>E|Pfo!xfN7%^fZaa`^U5pg-`~5#sF2 z>GYzTZ7V@O=vnx51L|E}@n9z6aFa@Hkdyjls#wi?zK@@a{?NsbCt3h;my(buLY20OaJ6Wip%QE%--@7ai1y)DWhhl?#0aJ1~_`L)IZ=PVuFXAL-b za$IIK$mtPJ?im7Sbmfa_z`xeNtsjc{9(z|+E*&s0e?q)3;HJG>7j2_(^YKa-b_;ak zPR0q4=-qyY-Oh2N-2K>1=Dj`xhYg`VZ;qAYQ^0pcoyScG`g>sm)>~%n9jXpiADjHz zR~!W{S9tJ9hJc91kEBkW1AU*!!Kt{Zl2=Ymazs51dR8jlph%Y&(^)39w($$^r9}qbdoX^aB4|cS;mcz)Q5>x&dc?goLgF zBnxV*lL2FcziOoba^l5qV0+qR>$AA&ASb2fHkcn0N?Jd#JrU`d zPZj{@G|&0Uwu4;{du)I+T?P_t|G>RH9ot{phvd!B?g=wDKR^#~=v&j0RMhi6J4zoo z89F9yE^wkccpwXS+VhP^G5~u&rv3B;OuV+>TqT5yLiC5M22LDGelA8dleS+ED6v|< z2kVzmU<9iGFt@`v9_`aZtBx1|lE%iBYKS*3mq$am!~>~O*p7ueZn|UtG*oa)HVwnw z{KUk8lY|kJ3UYt<8VBHNB=wCQ%01VKb3iURkQ;#cO3Tcw!}O8dn}+&`qKgskfNElQ z{XHlM-`ib@&lio~r)~_KTxCwqLL6oD4$JYc^_jN}7viZE#r9!8Mt^YP(!hnYlq)e` zh0=GGF+HR;>WKo#;r{Ty(;w)Gvx8Z0aUf?=7zMjc70q|$VK(i&u)!+;X36`xW`;I^ z5%pX1u$<|H(5JgW-*0!!8%>y9=h<1cCj;uXP0wfsy_+BFIwEkEu4q?jg8oQqT#JD4 zrmOuyArveYr=@$Wh45&y0WNL>`&pA7tpWTz>-HM7PZYP^%?5pYH#5gfkQ>yS{PG6= zj^6(uN&q)S9sCgHiuSLiDzf8ugd*3dxs;K9Ko%s6Qi>oPEz3mxZG7VOvk&CJ6Au#J-9izyxqmH$^=3m~8 z{R;V!ej6=F(-aQ= zeVAprA�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

%E)k)9MyY=a)6{KMKH+Hovai0Zokx2R`Mn%UC<$ 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 G@qYkA#ao^L diff --git a/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy b/demo/nlp_ext_cache/9df5b93eecbd2abe512c3624c69dfef1.proj.npy deleted file mode 100644 index 6f36275df20fbd136d4519c11d551eabfdbe0a9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 928 zcmbWr|5MX-9Ki7nt|1&+SAOkQtjYX{&I<{%@nw6zV$DNrcqcfED6XhXEzF&KNqD_(WLY5!0_WBQ2a({|ot}<6AkF@Tp1(aGN42_Fx8?~2 z6@quUVpF-%!lg*Kw31{_F5ybdmi-pP-XgQ5*z})k3}xk}M_#_$@TTd}uU?;+D3>H9 z$R+Pd{%0}6>G|Xfj^PeXK2<(>2Zxlo$`oS-)aZFAp7t@v6a0EHPw z_Vqh{9Dh+wXBLaWlo4Q4*;lc@h=*~lk98QfQgq@RO5F>gq5d@PGc8mVT(UxB+e^fi z#8ShtNmgn51%FU*kSYHO8@&d~yAr_Kf*jf&A3@F4xull&Vv<%(eu)@bKMg8(Y3*>f zX(wAA@?&^kfMF}*%#twctLVn?Vx_7j?jX#rNg!S49JaXD(5cjUJnhue=%x@V?nHNg zZ?l6)G{(L;JdQ*95Sx4a5@ssY8)c^OSGed156uS#*`C}Z>>+uv^OM_MC%iUz z`gSZ>1Q#*TvyrBM6q2HUGF|F<3%F!8#jF%Vj^_$HAUKJdETAp5VeHjusd3;wo^+P7 z)@VOEc8FlxiTk*g=fLr08%Bh}?3vH|P$cEy+vEVF4Qg5tF9uDjnqsA5X#DITD{J>+ zx~^B*!tIA#$A_xSZaXwBTSsv>?%~Z3cvxW_V)^wlSW?%9AFs`!_*Hi?tz|c*But^O zP6oBkF1$RPLt`&YB6mwe=bo5H`R^ID^T8%;{a}&aI>lQ09H$*pr@`I9Zrw( z)_EI*BVVJL^V2x9YGrrVIy=~!npMW26}CH$sw&UffnWhRy}zQ^@;pqmxbcfu0_;Z7 zMZ7Fq0eO)g9MNy1q5=WEov0?gObipdc&Iw=Ws6(`7navpAvKAkQ^qhV-WXJloUHVP-9`3}lm`GT!`wKe~GRU13L5;yG)puKL@GqQCvH1W1 diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl new file mode 100644 index 0000000000000000000000000000000000000000..bf5e32d8b45b2f488fa51934ad0d9651e560556e GIT binary patch literal 1370959 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 z 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

p|tziu4FO0pz~L?pVs= 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

~wY-4~G{b9=ZyMBgwEqrVVuSeYX9_kC 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 z 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

9Z3gt7!CB^kJ?DB;%m*ZAPk)U8ISDiAL;cj( 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�)gf|qY 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�J9}>y4Woh#8yR{oBU)5J?Sff)yNmKoqL=VP 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
EGtaxbGD{iOX0g-sB9noa$A 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

VgQ77i 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Σ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

    J`S@I=3!{ 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

    3s}LT=9)^bg^9VlFl# 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 ZZ6H1DExj!E$-my7vbVg%<Qd5p{|osuhd}@U literal 0 HcmV?d00001 diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy new file mode 100644 index 0000000000000000000000000000000000000000..cc1eac929e4d598935cf6f6161706c7475546b8d GIT binary patch literal 4128 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 CnI0Gb literal 0 HcmV?d00001 diff --git a/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl b/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f48efec27526f4e5abf0a3ba57b5c4b2e6a3205a GIT binary patch literal 772326 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%0DvvW+lvzKgH|BsVl6 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 XIw1r|)lSI@gHRDbs-w6xsZ<XD_uV&{ literal 0 HcmV?d00001 diff --git a/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy b/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy new file mode 100644 index 0000000000000000000000000000000000000000..8411c01fae4d2c931d06af467c41ce2dfcf8db48 GIT binary patch literal 928 zcmbWr|5MX-9Ki7*6vPR6n^+Av2Pk9X+XUr~F+Q)?J8)bgAbN9r32}`fmrHQ-%r67I z_|^r2*GQ%Y%NR0h1yj^K2;Of?1Jn5CBA~)35iGAX4fH6pf1&4Z&xbi(m9{C{jklL~ zP+XuhY7Jt!NUSJaD~=F}3yTeV44Pf}#fAdif3DQ%jk>39EYg(do_^RGnJhvSBZ&}| zivDNbSv3MOwr>>HoQh0rZ+;gCa&Kdo;wx^XArBuu34nyC?JPd+2DQytvGkL6;2)8) z3x!2A^L{va``KK$GSiAZasH?rf0=OKy2Dtsf@B{Sg2%9wZRtKpADj`8U(;>y&Grpo zk#>RRP8HT#?ZDqDW83~V)5gC#xWV6?U|Tl{cW>&UNt?|YZe69rOohAs<oJ6qu$4Wn zv|=b6UXA_?8vA4Lmuzn=N?t+u7tg_KpI(5m#Xb17_o2ka**`#(<jKmWRdlCQ-8=dI z0lfB)KR$S75SBM;xO=DC@bbl<uwwKQ)LcR0)gu7Q6E)*K?x!<$7n)pwP?6x^rb<p? zMv0ItO}GZtgKC!A)=&LfGMV;VA5AIuCGX|W!?7AWm)Bs%15E-_blnca<yxHk^3C4! zt#;hCFb(VlA$iLy3!;PTQU3fYtPZ$`zS=~{xuIq2_2cxN4F>E=S<<JAk&qTyJbd>G z66?Y;IPVjKX;Kbp?@R2cvyS@fJGmTrJw$Cx#_}{7EOakYXHF8`G26j?f7FZ<%?s3( zv6c>!+td*%p`k~Xv5N3qntE7;UXNsW{ni{Pf0zTm`~p_*K0&R;m7ljL0?}?+$CXxF zQ80p}^Ry3y%<xGH&%wR1%8Z+<EPWq8myB#60NqVjvHSxExSiBsh+`hktRIK{)l#hU ztB0_abyzt+g+9BF!3aNy4Kx_(>(&sao7U634o`Nuax=Xd?7@suCB0wph->qmf;(Xn z*4<P_SG$L^y($xxELGz+(;?24yn=y9LSHPK#Is!%i1Y}AS)a3T++oA2IyV%@30P{8 zka|bCF}0_X@{Y%Xv-bqveVoo(rn;#hGKj3s_yV?O3dxUKO^`9YgzRW`LBh|3DK53s ztmUCFZ!_aZAzoyz*afk1L9FxPJ{r4!LOzyMi)F)GU^M<i%&pC1WN3)~nYalnPq%ab E0uZ8(!~g&Q literal 0 HcmV?d00001 From 6465ab73e6e15f978a12a009de713b6ce672c47b Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 8 Jul 2026 10:00:08 +0200 Subject: [PATCH 046/144] chore: minor update on serving --- demo/Dockerfile.nlp_ext | 2 +- demo/README.md | 15 +++++++++++++-- demo/serve_nlp_ext.py | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/demo/Dockerfile.nlp_ext b/demo/Dockerfile.nlp_ext index e8709693..70c9fa7a 100644 --- a/demo/Dockerfile.nlp_ext +++ b/demo/Dockerfile.nlp_ext @@ -34,7 +34,7 @@ import datasets; \ m = 'bhadresh-savani/distilbert-base-uncased-emotion'; \ AutoTokenizer.from_pretrained(m, use_fast=True); \ AutoModelForSequenceClassification.from_pretrained(m); \ -datasets.load_dataset('dair-ai/emotion', split='train')" +datasets.load_dataset('dair-ai/emotion', split='test')" COPY demo/serve_nlp_ext.py ./serve_nlp_ext.py # Pre-computed SHAP + projection cache (built by running serve_nlp_ext.py once — see README). diff --git a/demo/README.md b/demo/README.md index fd861e59..d490d501 100644 --- a/demo/README.md +++ b/demo/README.md @@ -80,6 +80,17 @@ model/dataset downloads are all cached, so only the first run pays for it. Runs Same idea as the snapshot: compute the heavy stuff (SHAP) **once, up front**, then Docker just loads it. The model + dataset are downloaded by the Docker build itself, so you don't stage them. +**`demo/serve_nlp_ext.py` is the serving script**, and everything below runs it with its defaults +(the emotion model/dataset). Model, dataset, split, columns, sample count, and a ground-truth +label-renaming map are all configurable via `ServeConfig` in that file — see its module docstring +for `--model-name`/`--dataset-name`/`--dataset-split`/`--text-column`/`--label-column`/`--label-map`/ +`--n` and worked examples (e.g. swapping in an IMDB sentiment model/dataset). `Dockerfile.nlp_ext` +itself stays hardcoded to the emotion example on purpose (it's meant to run out of the box for +anyone cloning the repo) — its build-time warm-up (`RUN python -c "..."`) downloads exactly that +model/dataset/split, so it always matches `ServeConfig`'s defaults. Serving a different model/dataset +combo in Docker means editing that warm-up line to match, then redoing Step 1 below with the same +flags before rebuilding. + ### Step 1 — Pre-compute the SHAP cache (one-time, GPU optional) Needs the `[nlp]` extra (torch, transformers, datasets, pacmap). Run the script once — it computes SHAP @@ -116,8 +127,8 @@ Just run the same script — it serves on http://127.0.0.1:8051 and reuses the ` from Step 1 (recomputing only if the input texts change, or if you pass `--recompute`): ```bash -python demo/serve_nlp_ext.py # default 100 samples -python demo/serve_nlp_ext.py --n 200 # more samples (recomputes + re-caches) +python demo/serve_nlp_ext.py # ServeConfig defaults (500 samples, dair-ai/emotion test split) +python demo/serve_nlp_ext.py --n 1000 # more samples (recomputes + re-caches) ``` It uses the GPU if present, otherwise CPU. diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index 7bb34ba8..c70456c8 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -82,11 +82,11 @@ class ServeConfig: model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" dataset_name: str = "dair-ai/emotion" - dataset_split: str = "train" + dataset_split: str = "test" text_column: str = "text" label_column: str = "label" label_map: dict[str, str] = field(default_factory=dict) - n: int = 100 + n: int = 500 cache_dir: Path = _HERE / "nlp_ext_cache" port: int = 8051 host: str = "0.0.0.0" # noqa: S104 From fa08b5df034a9bf6367272325206f99c92c7d34a Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:40:32 +0200 Subject: [PATCH 047/144] build: add captum --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2f670d60..c7a72f1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ nlp = [ "transformers>=5.12.1", "datasets>=5.0.0", "sentence-transformers>=5.6.0", + "captum>=0.9.0", "pacmap>=0.9.1", "dash-ag-grid>=35.2.0" ] From 5bae8b94d9b3f4bc73a9a9d1546836dc28b92a44 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:41:08 +0200 Subject: [PATCH 048/144] feat: add captum lig backend --- shapash/backend/__init__.py | 1 + shapash/backend/nlp_captum_lig_backend.py | 201 ++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 shapash/backend/nlp_captum_lig_backend.py diff --git a/shapash/backend/__init__.py b/shapash/backend/__init__.py index 9faa455d..e94152e0 100644 --- a/shapash/backend/__init__.py +++ b/shapash/backend/__init__.py @@ -4,6 +4,7 @@ from .base_backend import BaseBackend from .lime_backend import LimeBackend from .nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation +from .nlp_captum_lig_backend import NlpCaptumLigBackend from .nlp_lime_backend import NlpLimeBackend from .nlp_shap_backend import NlpShapBackend from .shap_backend import ShapBackend diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py new file mode 100644 index 00000000..dc5d8f07 --- /dev/null +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -0,0 +1,201 @@ +"""NLP Captum backend — token-level Layer Integrated Gradients contributions. + +``NlpCaptumLigBackend`` is a second attribution method alongside ``NlpShapBackend``: it attributes a +target-class logit through the model's word-embedding layer with Captum's +``LayerIntegratedGradients`` (LIG), then sums over the embedding dimension to obtain one scalar per +token — the same per-token, per-class shape SHAP produces, so the webapp and the +``plot_sentence_highlight`` / ``plot_token_highlight`` renderers consume it unchanged. + +LIG attributes at the tokenizer's *subword* granularity (``[CLS]``, ``[SEP]``, ``##`` continuation +pieces). To match the word-level highlights the SHAP backend produces, ``_aggregate_subwords`` merges +each word's subwords into a single contribution and folds the special tokens' attribution into the +baseline (see below) — so the output carries whole words, not ``##`` fragments. + +LIG is a *completion* method: for each class ``c`` the token attributions sum to +``logits(x)[c] - logits(baseline)[c]``. We therefore report ``base_values[c] = logits(baseline)[c]`` +so the additive ``base + Σ = total`` summary in ``plot_sentence_highlight`` stays consistent (values +live in logit space, like the additive SHAP output). + +Unlike the SHAP/LIME backends (which wrap a plain text callable), this backend needs the embedding +module and a logits forward pass, so it consumes a :class:`~shapash.model.base.TextModel` that +implements :class:`~shapash.model.base.SupportsCaptumIG` (e.g. ``HFClassifierModel``). +""" + +from __future__ import annotations + +import re +from collections.abc import Iterable + +import numpy as np + +from shapash._optional import import_optional_module +from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation +from shapash.model.base import SupportsCaptumIG, has_capabilities + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' + +# ``[CLS]``/``[SEP]``/``[PAD]``-style bracket tokens (and blanks); their attribution is folded into the +# baseline during word aggregation so it is neither shown nor lost. +_SPECIAL_RE = re.compile(r"^\[.*\]$|^\s*$") + + +def _aggregate_subwords( + tokens: list[str], contributions: np.ndarray, base_values: np.ndarray +) -> tuple[list[str], np.ndarray, np.ndarray]: + """Merge WordPiece subwords into whole words and fold special tokens into the baseline. + + LIG attributes at the tokenizer's subword granularity, so raw output carries ``[CLS]``/``[SEP]`` + specials and ``##``-prefixed continuation pieces. This collapses each word's subword attributions + into a single value (matching the word-level highlights the SHAP backend produces) while + **preserving LIG's completeness relation**: special-token attributions are added to ``base_values`` + rather than discarded, so ``base + Σ(word contributions)`` still equals ``logits(x)``. + + Parameters + ---------- + tokens : list[str] + Subword token strings for one sample (length ``seq``). + contributions : np.ndarray + Per-subword contributions, shape ``(seq, n_classes)``. + base_values : np.ndarray + Baseline logits for the sample, shape ``(n_classes,)``. + + Returns + ------- + tuple[list[str], np.ndarray, np.ndarray] + Word strings, per-word contributions ``(n_words, n_classes)``, and the adjusted baseline + ``(n_classes,)`` with special-token attribution folded in. + """ + words: list[str] = [] + word_rows: list[np.ndarray] = [] + base = base_values.astype(float).copy() + for tok, row in zip(tokens, contributions, strict=True): + stripped = tok.strip() + if _SPECIAL_RE.match(stripped): + base = base + row # fold special-token attribution into the baseline (keep completeness) + elif stripped.startswith("##") and words: + words[-1] += stripped[2:] + word_rows[-1] = word_rows[-1] + row + else: + words.append(stripped) + word_rows.append(row.astype(float).copy()) + + stacked = np.stack(word_rows, axis=0) if word_rows else np.zeros((0, contributions.shape[-1])) + return words, stacked, base + + +class NlpCaptumLigBackend(NlpBackend): + """Layer Integrated Gradients backend for text classification models. + + Parameters + ---------- + model : SupportsCaptumIG + A text model exposing the Captum attribution surface (embedding module, ``encode``, + ``reference_ids``, ``logits``) — typically an ``HFClassifierModel``. + preprocessing : None + Unused; accepted for interface compatibility with ``BaseBackend``. + label_names : list[str] or None + Class names in the same order as the model output columns. + explainer_args : dict, optional + Unused; accepted for interface parity with the other NLP backends. + explainer_compute_args : dict, optional + Keyword arguments forwarded to ``LayerIntegratedGradients.attribute`` for every sample and + class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` defaults to 50. + show_progress : bool, default False + When True, wrap the per-sample attribution loop in a ``tqdm`` progress bar (LIG runs one + integration per class per sample, so a batch is slow). Best-effort: if ``tqdm`` is not + installed the loop runs silently. Left off by default so the library emits no stdout. + + Raises + ------ + TypeError + If ``model`` does not implement :class:`~shapash.model.base.SupportsCaptumIG`. + """ + + name = "nlp_captum_lig" + + def __init__( + self, + model, + preprocessing=None, + label_names: list[str] | None = None, + explainer_args: dict | None = None, + explainer_compute_args: dict | None = None, + show_progress: bool = False, + ) -> None: + if not has_capabilities(model, SupportsCaptumIG): + raise TypeError( + "NlpCaptumLigBackend requires a model implementing SupportsCaptumIG " + "(e.g. HFClassifierModel); got a model without the Captum attribution surface." + ) + super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) + self.show_progress = show_progress + captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) + self.explainer = captum_attr.LayerIntegratedGradients(model.logits, model.embedding_layer) + + def run_explainer(self, x) -> NlpRawExplanation: + """Attribute each text with LIG, one pass per class, and return per-token contributions. + + Parameters + ---------- + x : list[str] or pd.Series + Text samples to explain. + + Returns + ------- + NlpRawExplanation + Ragged list of ``(n_tokens_i, n_classes)`` value arrays, per-sample baseline logits, and + token strings per sample. + """ + model: SupportsCaptumIG = self.model + attribute_args = {"n_steps": 50, **self.explainer_compute_args} + + contributions: list[np.ndarray] = [] + base_values: list[np.ndarray] = [] + data: list[list[str]] = [] + + for text in self._progress_iter(list(x)): + input_ids, attention_mask, tokens = model.encode(text) + ref_ids = model.reference_ids(input_ids) + base_logits = model.logits(ref_ids, attention_mask)[0].detach().cpu().numpy() + n_classes = int(base_logits.shape[-1]) + + per_class = [] + for target in range(n_classes): + attributions = self.explainer.attribute( + input_ids, + baselines=ref_ids, + target=target, + additional_forward_args=(attention_mask,), + **attribute_args, + ) + # (1, seq, hidden) -> (seq,): sum the attribution over the embedding dimension. + token_scores = attributions.sum(dim=-1).squeeze(0).detach().cpu().numpy() + per_class.append(token_scores) + + # Collapse subwords to whole words (dropping special tokens) so LIG highlights read like + # the SHAP backend's; special-token attribution is folded into base_logits to stay additive. + word_tokens, word_contribs, base_logits = _aggregate_subwords( + list(tokens), np.stack(per_class, axis=-1), base_logits + ) + contributions.append(word_contribs) # (n_words, n_classes) + base_values.append(base_logits) + data.append(word_tokens) + + return NlpRawExplanation( + contributions=contributions, + base_values=np.stack(base_values, axis=0), + data=data, + ) + + def _progress_iter(self, items: list[str]) -> Iterable[str]: + """Wrap ``items`` in a ``tqdm`` bar when ``show_progress`` is set, else return it unchanged. + + Best-effort and dependency-free: ``tqdm`` is imported with ``errors="ignore"`` so a missing + install simply yields the plain list rather than raising. + """ + if not self.show_progress: + return items + tqdm_mod = import_optional_module("tqdm", errors="ignore") + if tqdm_mod is None: + return items + return tqdm_mod.tqdm(items, desc="LIG attribution", unit="text") From f3603773e265fc7312f0ba174d854d35df992587 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:41:49 +0200 Subject: [PATCH 049/144] feat: add captum ablation flip cf method --- shapash/compute/generators/__init__.py | 6 +- shapash/compute/generators/ablation_flip.py | 145 ++++++++++++++++++++ shapash/compute/generators/cf_utils.py | 11 ++ shapash/compute/generators/hotflip.py | 14 +- 4 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 shapash/compute/generators/ablation_flip.py diff --git a/shapash/compute/generators/__init__.py b/shapash/compute/generators/__init__.py index 3bb1eee9..bf93b8a9 100644 --- a/shapash/compute/generators/__init__.py +++ b/shapash/compute/generators/__init__.py @@ -4,12 +4,13 @@ that change the model's prediction. Generators declare their model-capability requirements via ``is_compatible`` and their tunable knobs via ``config_spec`` (auto-rendered by the webapp). -Ships :class:`HotFlipGenerator` (gradient-based token substitution). :class:`AblationFlipGenerator` -(forward-pass-only token removal) is a planned sibling using the same ABC. +Ships :class:`HotFlipGenerator` (gradient-based token substitution) and :class:`AblationFlipGenerator` +(forward-pass-only token removal, via Captum ``FeatureAblation``) — both using the same ABC. """ from __future__ import annotations +from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import ( Counterfactual, CounterfactualGenerator, @@ -20,6 +21,7 @@ from shapash.compute.generators.hotflip import HotFlipGenerator __all__ = [ + "AblationFlipGenerator", "Counterfactual", "CounterfactualGenerator", "Field", diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py new file mode 100644 index 00000000..99dec0b3 --- /dev/null +++ b/shapash/compute/generators/ablation_flip.py @@ -0,0 +1,145 @@ +"""AblationFlip counterfactual generator — forward-pass-only token removal. + +The perturbation-based sibling of :class:`~shapash.compute.generators.hotflip.HotFlipGenerator`. Where +HotFlip *substitutes* tokens using embedding gradients, AblationFlip *removes* tokens it scores as most +supportive of the current prediction, until the prediction flips: + +1. Score each content token with Captum ``FeatureAblation``: starting from the full text, ablate one + token at a time (to an empty baseline) and measure the drop in the original class's probability. + Positive score = the token supports the original class (removing it hurts that class most). +2. Greedily remove the highest-scoring tokens in combinations of increasing size (1..``max_ablations``), + rebuild the text, re-predict, and keep those that flip the prediction — enforcing **minimality** + (never return a removal set that is a superset of an already-successful one). + +It needs only forward passes (``predict``) plus tokenization, so — unlike HotFlip — it also works on +prediction-only models such as ``HFPipelineModel``. The Captum scoring step (:meth:`_ablation_scores`) +is isolated from the pure greedy-flip loop so the flip/minimality logic is testable without Captum. +""" + +from __future__ import annotations + +from itertools import combinations +from typing import TYPE_CHECKING + +import numpy as np + +from shapash._optional import import_optional_module +from shapash.compute.generators.base import ( + Counterfactual, + CounterfactualGenerator, + Field, + IntField, + TokenListField, +) +from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token +from shapash.model.base import SupportsTokenization, TextModel, has_capabilities + +if TYPE_CHECKING: + from collections.abc import Sequence + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' +_MAX_ABLATABLE_TOKENS = 10 + + +class AblationFlipGenerator(CounterfactualGenerator): + """Perturbation-based token-removal counterfactuals (Captum ``FeatureAblation``).""" + + name = "ablation_flip" + + def config_spec(self) -> dict[str, Field]: + """Expose ``num_examples``, ``max_ablations`` and ``tokens_to_ignore`` as tunable knobs.""" + return { + "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), + "max_ablations": IntField(label="Max token removals", default=3, minimum=1, maximum=5), + "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), + } + + @classmethod + def is_compatible(cls, model: TextModel) -> bool: + """Compatible with any tokenizable model — only forward passes and tokenization are needed.""" + return has_capabilities(model, SupportsTokenization) + + def generate(self, text: str, target_label: str | None = None, config: dict | None = None) -> list[Counterfactual]: + """Generate minimal token-removal counterfactuals for ``text`` (see module docstring).""" + cfg = self.resolve_config(config) + num_examples = int(cfg["num_examples"]) + max_ablations = int(cfg["max_ablations"]) + ignore = {t.strip().lower() for t in cfg["tokens_to_ignore"]} + + model = self.model + if not isinstance(model, SupportsTokenization): # is_compatible guarantees this; guard direct use + raise TypeError("AblationFlipGenerator requires a model implementing SupportsTokenization.") + label_names = model.label_names or [] + + orig_probs = model.predict([text])[0] + orig_class = int(np.argmax(orig_probs)) + orig_label = label_names[orig_class] if orig_class < len(label_names) else str(orig_class) + target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None + + tokens = model.tokenize(text) + content_positions = [i for i, t in enumerate(tokens) if is_word_token(t) and t.lower() not in ignore] + if not content_positions: + return [] + + scores = self._ablation_scores(tokens, content_positions, orig_class) + # Keep the most supportive positions (largest drop when removed) as removal candidates. + ranked = [content_positions[j] for j in np.argsort(-scores)][:_MAX_ABLATABLE_TOKENS] + + results: list[Counterfactual] = [] + successful: list[frozenset[int]] = [] + for size in range(1, max_ablations + 1): + for combo in combinations(ranked, size): + combo_set = frozenset(combo) + if any(prev <= combo_set for prev in successful): + continue # minimality: skip supersets of a smaller success + new_tokens = [tok for i, tok in enumerate(tokens) if i not in combo_set] + new_text = model.detokenize(new_tokens) + cf_probs = model.predict([new_text])[0] + if is_prediction_flip(orig_probs, cf_probs, target_class): + cf_class = int(np.argmax(cf_probs)) + results.append( + Counterfactual( + original_text=text, + new_text=new_text, + tokens=list(tokens), + flipped_positions=list(combo), + substitutions=[(pos, tokens[pos], "") for pos in combo], + orig_label=orig_label, + new_label=label_names[cf_class] if cf_class < len(label_names) else str(cf_class), + orig_prob=float(orig_probs[orig_class]), + new_prob=float(cf_probs[cf_class]), + prob_delta=float(orig_probs[orig_class] - cf_probs[orig_class]), + ) + ) + successful.append(combo_set) + if len(results) >= num_examples: + return results + return results + + def _ablation_scores(self, tokens: list[str], content_positions: Sequence[int], orig_class: int) -> np.ndarray: + """Score each content token by the drop in ``orig_class`` probability when it is removed. + + Uses Captum ``FeatureAblation`` over a token-presence mask: the input is an all-ones mask (every + content token present); ablating a feature to the zero baseline drops that token from the + rebuilt text. Returns one score per entry of ``content_positions`` (aligned by order). + """ + torch = import_optional_module("torch", extra=_NLP_EXTRA) + captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) + + model = self.model + if not isinstance(model, SupportsTokenization): # is_compatible guarantees this; guard direct use + raise TypeError("AblationFlipGenerator requires a model implementing SupportsTokenization.") + pos_index = {p: j for j, p in enumerate(content_positions)} + + def forward(mask): # mask: (batch, n_content) float tensor of 0/1 + active = mask.detach().cpu().numpy() > 0.5 + texts = [ + model.detokenize([tok for i, tok in enumerate(tokens) if i not in pos_index or row[pos_index[i]]]) + for row in active + ] + return torch.tensor(model.predict(texts), dtype=torch.float32) + + ablator = captum_attr.FeatureAblation(forward) + inputs = torch.ones((1, len(content_positions)), dtype=torch.float32) + attributions = ablator.attribute(inputs, baselines=0.0, target=orig_class) + return attributions.squeeze(0).detach().cpu().numpy() diff --git a/shapash/compute/generators/cf_utils.py b/shapash/compute/generators/cf_utils.py index 690b790f..94901c74 100644 --- a/shapash/compute/generators/cf_utils.py +++ b/shapash/compute/generators/cf_utils.py @@ -37,3 +37,14 @@ def is_prediction_flip(orig_probs: np.ndarray, cf_probs: np.ndarray, target_clas def prediction_difference(orig_probs: np.ndarray, cf_probs: np.ndarray, class_idx: int) -> float: """Return how much ``class_idx``'s probability dropped from original to counterfactual.""" return float(orig_probs[class_idx] - cf_probs[class_idx]) + + +def is_word_token(token: str) -> bool: + """True for plausible standalone word tokens. + + Rejects sub-word continuations (``##ing``), special/placeholder tokens (``[CLS]``), punctuation + and numerics — substituting or removing any of these yields malformed text once detokenized. + ``str.isalpha`` handles all of these in one check (``##ing``/``[CLS]``/``1b`` are not alphabetic). + Shared by the token-substitution (HotFlip) and token-removal (ablation) generators. + """ + return token.isalpha() diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index fb853b2f..f96911e1 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -32,7 +32,7 @@ IntField, TokenListField, ) -from shapash.compute.generators.cf_utils import is_prediction_flip +from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token from shapash.model.base import SupportsEmbeddings, SupportsGradients, TextModel, has_capabilities _MAX_FLIPPABLE_TOKENS = 10 @@ -103,7 +103,7 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No shortlist: list[str] = [] for cand in np.argsort(scores): cand_tok = vocab[int(cand)] - if cand_tok != tokens[pos] and _is_word_token(cand_tok): + if cand_tok != tokens[pos] and is_word_token(cand_tok): shortlist.append(cand_tok) if len(shortlist) >= _CANDIDATES_PER_POSITION: break @@ -151,13 +151,3 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No if len(results) >= num_examples: return results return results - - -def _is_word_token(token: str) -> bool: - """True for plausible standalone word tokens. - - Rejects sub-word continuations (``##ing``), special/placeholder tokens (``[CLS]``), punctuation - and numerics — substituting any of these for a whole word yields malformed text once detokenized. - ``str.isalpha`` handles all of these in one check (``##ing``/``[CLS]``/``1b`` are not alphabetic). - """ - return token.isalpha() From 11ef5df993d55aa8d4a1ce17c30d62acf8a56910 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:43:30 +0200 Subject: [PATCH 050/144] refactor: add support gradients for tokens view --- shapash/webapp/nlp_components/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 84f49950..61eb2d93 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -11,6 +11,7 @@ from abc import ABC, abstractmethod from shapash.explainer.interactive import InteractiveEngine +from shapash.model.base import SupportsGradients, has_capabilities from shapash.webapp.nlp_view import NlpView # Capability tokens components may require. @@ -41,7 +42,11 @@ def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> f caps.add(CAP_PREDICT) if engine.can_counterfactual(): caps.add(CAP_COUNTERFACTUAL) - caps.add(CAP_GRADIENTS) # the auto-built generator (HotFlip) is gradient-based + # Advertise gradients only when the *bound* generator actually operates on a + # gradient-capable model — a forward-pass-only generator (AblationFlip) must not. + generator = getattr(engine, "cf_generator", None) + if has_capabilities(getattr(generator, "model", None), SupportsGradients): + caps.add(CAP_GRADIENTS) return frozenset(caps) From 7bcbdaa576fdc4d731b421b4cd13919b11831c47 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:44:10 +0200 Subject: [PATCH 051/144] feat: add captum capabilities to model --- shapash/model/base.py | 41 +++++++++++++++++++++++++++++++++++++++++ shapash/model/hf.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/shapash/model/base.py b/shapash/model/base.py index 8762a627..3c9f7d9c 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -7,6 +7,8 @@ * ``SupportsTokenization`` — split text into tokens and rebuild text from tokens. * ``SupportsEmbeddings`` — expose the input-embedding table and mean-pooled sentence embeddings. * ``SupportsGradients`` — expose per-token gradients of a target-class logit. +* ``SupportsCaptumIG`` — expose the embedding module + a logits forward pass so a layer-attribution + method (Captum ``LayerIntegratedGradients``) can attribute through the embeddings. A prediction-only model (e.g. a HuggingFace pipeline) implements only ``TextModel``; a raw classifier with tokenizer access implements all three. Generators check compatibility with @@ -17,6 +19,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import Any import numpy as np @@ -105,6 +108,44 @@ def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.n """ +class SupportsCaptumIG(ABC): + """Capability: expose the pieces Captum ``LayerIntegratedGradients`` needs. + + A layer-attribution method attributes a target logit *through* the word-embedding module, so it + needs three things a plain :meth:`TextModel.predict` cannot provide: the embedding ``nn.Module`` + to attribute through, a way to turn text into model input tensors (and their reference/baseline + ids), and a raw-logits forward pass to use as the Captum ``forward_func``. Kept separate from + :class:`SupportsGradients` — which only yields per-token gradient vectors — so a backend can + require exactly this surface. Tensor-typed arguments/returns are annotated ``Any`` to keep this + module free of a hard ``torch`` import. + """ + + @property + @abstractmethod + def embedding_layer(self) -> Any: + """Return the word-embedding ``nn.Module`` to compute layer attributions through.""" + + @abstractmethod + def encode(self, text: str) -> tuple[Any, Any, list[str]]: + """Return ``(input_ids, attention_mask, tokens)`` for one text. + + ``input_ids`` and ``attention_mask`` are batch-size-1 tensors on the model device; ``tokens`` + are the aligned token strings (special tokens included). + """ + + @abstractmethod + def reference_ids(self, input_ids: Any) -> Any: + """Return baseline input ids: non-special tokens replaced by a reference (pad/mask) id. + + Same shape/device as ``input_ids``. Special tokens (e.g. ``[CLS]``/``[SEP]``) are kept so the + baseline is a well-formed empty-content sequence. + """ + + @abstractmethod + def logits(self, input_ids: Any, attention_mask: Any) -> Any: + """Return raw classification logits, shape ``(batch, n_classes)`` (the Captum forward func).""" + + def has_capabilities(model: object, *capabilities: type) -> bool: """Return ``True`` when ``model`` is an instance of every capability ABC given. diff --git a/shapash/model/hf.py b/shapash/model/hf.py index fb1dca9b..38c25416 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -23,6 +23,7 @@ from shapash._optional import import_optional_module from shapash.model.base import ( + SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, SupportsTokenization, @@ -97,11 +98,12 @@ def detokenize(self, tokens: list[str]) -> str: return self.tokenizer.convert_tokens_to_string(tokens) -class HFClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): +class HFClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG): """Full-capability adapter over a raw ``AutoModelForSequenceClassification`` + tokenizer. Provides prediction, tokenization, the input-embedding table, mean-pooled sentence embeddings, - and per-token gradients — everything gradient-based counterfactual generators require. + per-token gradients, and the Captum layer-attribution surface — everything gradient-based + counterfactual generators and ``LayerIntegratedGradients`` require. Parameters ---------- @@ -268,3 +270,36 @@ def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.n kept_tokens = [tokens[i] for i in keep] kept_grads = grads[keep] return kept_tokens, kept_grads + + # ------------------------------------------------------------------ + # Captum layer-attribution surface + # ------------------------------------------------------------------ + + @property + def embedding_layer(self): + """Return the word-embedding ``nn.Module`` (what ``LayerIntegratedGradients`` attributes through).""" + return self.classifier.get_input_embeddings() + + def encode(self, text: str): + """Return ``(input_ids, attention_mask, tokens)`` — batch-size-1 tensors + aligned token strings.""" + device = self.classifier.device + enc = self.tokenizer(text, truncation=True, return_tensors="pt") + input_ids = enc["input_ids"].to(device) + attention_mask = enc["attention_mask"].to(device) + tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0].tolist()) + return input_ids, attention_mask, tokens + + def reference_ids(self, input_ids): + """Return baseline ids: content tokens replaced by the pad/mask reference, special tokens kept.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + ids = input_ids[0].tolist() + special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) + ref_id = self.tokenizer.pad_token_id + if ref_id is None: + ref_id = self.tokenizer.mask_token_id or self.tokenizer.unk_token_id or 0 + ref = [tid if is_special else ref_id for tid, is_special in zip(ids, special_mask, strict=True)] + return torch.tensor([ref], device=input_ids.device) + + def logits(self, input_ids, attention_mask): + """Return raw classification logits ``(batch, n_classes)`` (the Captum forward func).""" + return self.classifier(input_ids=input_ids, attention_mask=attention_mask).logits From 8859b0683a7a4e5b5ceb831bb6b5f2d000c37742 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:44:52 +0200 Subject: [PATCH 052/144] refactor: add ablation to explainer --- shapash/explainer/nlp_explainer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index a65131c2..45ec4b99 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -21,6 +21,7 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator from shapash.model.base import TextModel @@ -113,11 +114,15 @@ def __init__( explainer_compute_args=explainer_compute_args or {}, ) - # Counterfactual generator: explicit > auto-built (HotFlip) when the model is capable > none. + # Counterfactual generator: explicit > auto-built > none. HotFlip (gradient-based, richer + # substitutions) is preferred; AblationFlip (forward-pass-only removal) is the fallback so a + # prediction-only model such as a plain pipeline still gets a What-if Lab. if cf_generator is not None: self.cf_generator: CounterfactualGenerator | None = cf_generator elif self._text_model is not None and HotFlipGenerator.is_compatible(self._text_model): self.cf_generator = HotFlipGenerator(self._text_model) + elif self._text_model is not None and AblationFlipGenerator.is_compatible(self._text_model): + self.cf_generator = AblationFlipGenerator(self._text_model) else: self.cf_generator = None From 43ae57694ab9c091bfbb0e76f804ebf055b0f2a0 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:45:23 +0200 Subject: [PATCH 053/144] test: add tests of new functionalities --- .../integration_tests/test_captum_backends.py | 108 +++++++++++++ .../backend/test_nlp_captum_lig_backend.py | 97 ++++++++++++ .../compute/generators/test_ablation_flip.py | 148 ++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 tests/integration_tests/test_captum_backends.py create mode 100644 tests/unit_tests/backend/test_nlp_captum_lig_backend.py create mode 100644 tests/unit_tests/compute/generators/test_ablation_flip.py diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py new file mode 100644 index 00000000..3264da6f --- /dev/null +++ b/tests/integration_tests/test_captum_backends.py @@ -0,0 +1,108 @@ +"""Integration tests for the Captum-backed NLP features against a real transformer. + +Exercises the LayerIntegratedGradients attribution backend (:class:`NlpCaptumLigBackend`) and the +FeatureAblation counterfactual generator (:class:`AblationFlipGenerator`) end to end on the emotion +distilbert model used by the demos. Skipped automatically when ``transformers`` / ``torch`` / ``captum`` +are not installed (or the model is unavailable). +""" + +import numpy as np +import pytest + +transformers = pytest.importorskip("transformers") +pytest.importorskip("torch") +pytest.importorskip("captum") + +from shapash.backend import NlpCaptumLigBackend # noqa: E402 +from shapash.backend.nlp_backend import NlpContributions # noqa: E402 +from shapash.compute.generators import AblationFlipGenerator # noqa: E402 +from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 +from shapash.model import HFClassifierModel # noqa: E402 + +MODEL_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" +LABELS = ["sadness", "joy", "love", "anger", "fear", "surprise"] + + +@pytest.fixture(scope="module") +def model(): + try: + tokenizer = transformers.AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=True) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(MODEL_NAME) + except Exception as exc: # network / cache miss + pytest.skip(f"model unavailable: {exc}") + return HFClassifierModel(classifier, tokenizer, label_names=LABELS) + + +def test_captum_ig_surface(model): + """The classifier adapter exposes the LayerIntegratedGradients surface with sane shapes.""" + input_ids, attention_mask, tokens = model.encode("i am so happy today") + assert input_ids.shape == attention_mask.shape + assert len(tokens) == input_ids.shape[1] + ref_ids = model.reference_ids(input_ids) + assert ref_ids.shape == input_ids.shape + # Special tokens are preserved in the baseline; content ids are replaced. + assert ref_ids[0, 0].item() == input_ids[0, 0].item() # [CLS] + assert ref_ids[0, -1].item() == input_ids[0, -1].item() # [SEP] + logits = model.logits(input_ids, attention_mask) + assert logits.shape == (1, len(LABELS)) + + +def test_lig_backend_contributions(model): + """LIG produces per-token, per-class contributions that satisfy the completeness relation.""" + backend = NlpCaptumLigBackend(model, label_names=LABELS) + raw = backend.run_explainer(["i am so happy today", "i feel terrified"]) + + assert len(raw.contributions) == 2 + assert raw.base_values.shape == (2, len(LABELS)) + for values, tokens in zip(raw.contributions, raw.data): + assert values.shape == (len(tokens), len(LABELS)) + assert np.isfinite(values).all() + # Subwords are merged to whole words and specials dropped — no [CLS]/[SEP]/## leak into highlights. + assert all(not (t.startswith("##") or (t.startswith("[") and t.endswith("]"))) for t in tokens) + + # LIG is a completion method: base + sum(attributions) ≈ logits(x) for each class. + input_ids, attention_mask, _ = model.encode("i am so happy today") + logits_x = model.logits(input_ids, attention_mask)[0].detach().cpu().numpy() + recon = raw.base_values[0] + raw.contributions[0].sum(axis=0) + np.testing.assert_allclose(recon, logits_x, atol=0.2) + + +def test_lig_backend_through_explainer_and_word_importance(model): + """The LIG backend drives NlpExplainer.compile and the shared word-importance aggregation.""" + xpl = NlpExplainer(model, label_names=LABELS, backend=NlpCaptumLigBackend(model, label_names=LABELS)) + xpl.compile(["i am so happy today", "i feel terrified and alone"]) + assert isinstance(xpl.contributions, NlpContributions) + assert len(xpl.contributions) == 2 + joy_idx = LABELS.index("joy") + word_imp = xpl.contributions.word_importance(joy_idx, n_top=5) + assert len(word_imp) > 0 # some words survive special-token filtering + + +def test_ablation_flip_finds_flip(model): + """FeatureAblation-scored token removal flips a confidently classified sample, minimally.""" + gen = AblationFlipGenerator(model) + cfs = gen.generate("i am so happy today", config={"num_examples": 3, "max_ablations": 3}) + assert cfs, "AblationFlip found no counterfactual on a confident sample" + for cf in cfs: + assert cf.new_label != cf.orig_label + assert cf.new_text != cf.original_text + assert all(new == "" for _, _, new in cf.substitutions) # removals record an empty replacement + # Removed tokens really are gone and the reported flip holds under the model. + assert model.predict([cf.new_text])[0].argmax() != model.predict([cf.original_text])[0].argmax() + # minimality: no returned removal set is a strict superset of another + sets = [frozenset(cf.flipped_positions) for cf in cfs] + for i, a in enumerate(sets): + for j, b in enumerate(sets): + if i != j: + assert not (b < a) + + +def test_ablation_flip_auto_bound_for_pipeline(model): + """A prediction-only pipeline model gets AblationFlip auto-bound (HotFlip cannot apply).""" + pipe = transformers.pipeline("text-classification", model=model.classifier, tokenizer=model.tokenizer, top_k=None) + from shapash.model import HFPipelineModel + + pm = HFPipelineModel(pipe, label_names=LABELS) + xpl = NlpExplainer(pm, label_names=LABELS) + assert xpl.can_counterfactual() + assert isinstance(xpl.cf_generator, AblationFlipGenerator) diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py new file mode 100644 index 00000000..7ba8f33b --- /dev/null +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -0,0 +1,97 @@ +"""Unit tests for ``NlpCaptumLigBackend`` construction and registration (captum-free). + +The capability guard rejects incompatible models *before* captum is imported, so these run without +torch/captum installed. The real LayerIntegratedGradients attribution path is covered by the +integration tests (which require a live transformer). +""" + +import unittest + +import numpy as np + +from shapash.backend import NlpCaptumLigBackend, get_backend_cls_from_name +from shapash.backend.nlp_captum_lig_backend import _aggregate_subwords +from shapash.model.base import TextModel + + +class PredictOnlyModel(TextModel): + """A model without the Captum attribution surface (no ``SupportsCaptumIG``).""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + +class TestNlpCaptumLigBackend(unittest.TestCase): + def test_registered_by_name(self): + self.assertIs(get_backend_cls_from_name("nlp_captum_lig"), NlpCaptumLigBackend) + + def test_backend_name_attribute(self): + self.assertEqual(NlpCaptumLigBackend.name, "nlp_captum_lig") + + def test_requires_captum_capability(self): + with self.assertRaises(TypeError): + NlpCaptumLigBackend(PredictOnlyModel(), label_names=["neg", "pos"]) + + +class TestAggregateSubwords(unittest.TestCase): + """``_aggregate_subwords`` is pure numpy — no torch/captum needed.""" + + def test_merges_subwords_and_drops_specials(self): + tokens = ["[CLS]", "i", "am", "hap", "##py", "[SEP]"] + contribs = np.array( + [[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float + ) + base = np.array([10.0, 20.0]) + + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + + self.assertEqual(words, ["i", "am", "happy"]) + # "happy" = "hap" + "##py": contributions summed. + np.testing.assert_allclose(word_contribs, [[2.0, 1.0], [3.0, 2.0], [9.0, 7.0]]) + # [CLS] + [SEP] attribution folded into the baseline. + np.testing.assert_allclose(new_base, [10.0 + 1.0 + 6.0, 20.0 + 0.0 + 5.0]) + + def test_preserves_completeness(self): + tokens = ["[CLS]", "great", "mov", "##ie", "[SEP]"] + contribs = np.random.default_rng(0).normal(size=(5, 3)) + base = np.array([0.5, -0.5, 1.0]) + + _, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + + # base + Σ over words must equal the original base + Σ over every subword (nothing lost). + np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) + + def test_all_special_returns_empty_words(self): + tokens = ["[CLS]", "[SEP]"] + contribs = np.array([[1.0, 2.0], [3.0, 4.0]]) + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(2)) + self.assertEqual(words, []) + self.assertEqual(word_contribs.shape, (0, 2)) + np.testing.assert_allclose(new_base, [4.0, 6.0]) + + +class TestProgressIter(unittest.TestCase): + """``_progress_iter`` is captum-free — exercise it via ``object.__new__`` to skip ``__init__``.""" + + @staticmethod + def _backend(show_progress): + backend = object.__new__(NlpCaptumLigBackend) # bypass captum import in __init__ + backend.show_progress = show_progress + return backend + + def test_returns_items_unchanged_when_disabled(self): + items = ["a", "b", "c"] + self.assertIs(self._backend(show_progress=False)._progress_iter(items), items) + + def test_iterates_all_items_when_enabled(self): + # With tqdm installed it wraps in a bar; without it, the plain list — either way the loop must + # still visit every element. + items = ["a", "b", "c"] + self.assertEqual(list(self._backend(show_progress=True)._progress_iter(items)), items) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/compute/generators/test_ablation_flip.py b/tests/unit_tests/compute/generators/test_ablation_flip.py new file mode 100644 index 00000000..3d6388e6 --- /dev/null +++ b/tests/unit_tests/compute/generators/test_ablation_flip.py @@ -0,0 +1,148 @@ +"""Unit tests for the AblationFlip counterfactual generator. + +The Captum scoring step (:meth:`AblationFlipGenerator._ablation_scores`) is stubbed with an exact, +captum-free re-implementation (measure the drop in the original class when a token is removed) so the +greedy-flip / minimality logic is exercised without torch or captum. A deterministic bag-of-words fake +model provides predictions. The real Captum path is covered by the integration tests. +""" + +import unittest + +import numpy as np + +from shapash.compute.generators import AblationFlipGenerator, Counterfactual +from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token +from shapash.model.base import SupportsTokenization, TextModel + +# Per-token [neg_logit, pos_logit] contributions; logits = sum over content tokens. +_EMB = { + "this": [0.1, 0.1], + "is": [0.1, 0.1], + "great": [0.0, 2.0], + "good": [0.0, 1.0], + "super": [0.0, 2.0], + "meh": [3.0, 0.0], + "terrible": [3.0, 0.0], +} + + +class BagOfWordsModel(TextModel, SupportsTokenization): + """Prediction-only (plus tokenization) linear bag-of-words classifier.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def _logits(self, text): + toks = [t for t in text.split() if t in _EMB] + return np.sum([_EMB[t] for t in toks], axis=0) if toks else np.zeros(2) + + def predict(self, texts): + out = [] + for t in texts: + z = self._logits(t) + e = np.exp(z - z.max()) + out.append(e / e.sum()) + return np.vstack(out) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + +def _exact_ablation_scores(self, tokens, content_positions, orig_class): + """Captum-free stand-in for ``_ablation_scores``: exact drop in ``orig_class`` per removed token.""" + base = self.model.predict([self.model.detokenize(list(tokens))])[0][orig_class] + scores = [] + for p in content_positions: + remaining = [t for i, t in enumerate(tokens) if i != p] + scores.append(base - self.model.predict([self.model.detokenize(remaining)])[0][orig_class]) + return np.array(scores) + + +def _make_generator(): + gen = AblationFlipGenerator(BagOfWordsModel()) + gen._ablation_scores = _exact_ablation_scores.__get__(gen, AblationFlipGenerator) + return gen + + +class TestIsWordToken(unittest.TestCase): + def test_accepts_words_rejects_subwords_and_specials(self): + self.assertTrue(is_word_token("happy")) + self.assertFalse(is_word_token("##ing")) + self.assertFalse(is_word_token("[CLS]")) + self.assertFalse(is_word_token("1b")) + self.assertFalse(is_word_token("!")) + + +class TestAblationFlipCompat(unittest.TestCase): + def test_compatible_with_tokenizable_predict_only_model(self): + self.assertTrue(AblationFlipGenerator.is_compatible(BagOfWordsModel())) + + def test_incompatible_without_tokenization(self): + class PredictOnly(TextModel): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + self.assertFalse(AblationFlipGenerator.is_compatible(PredictOnly())) + + +class TestAblationFlipGenerate(unittest.TestCase): + def setUp(self): + self.gen = _make_generator() + + def test_config_spec_defaults(self): + spec = self.gen.config_spec() + self.assertEqual(spec["num_examples"].default, 5) + self.assertEqual(spec["max_ablations"].default, 3) + self.assertEqual(self.gen.resolve_config({"max_ablations": 2})["max_ablations"], 2) + + def test_generates_flip_by_removing_supportive_token(self): + cfs = self.gen.generate("this is great", config={"num_examples": 3, "max_ablations": 1}) + self.assertTrue(cfs, "expected at least one counterfactual") + cf = cfs[0] + self.assertIsInstance(cf, Counterfactual) + self.assertEqual(cf.orig_label, "pos") + self.assertEqual(cf.new_label, "neg") + # The removed token is the strongly-positive one; removals record an empty replacement. + self.assertTrue(all(new == "" for _, _, new in cf.substitutions)) + self.assertIn("great", [old for _, old, _ in cf.substitutions]) + self.assertNotIn("great", cf.new_text.split()) + # The reported flip actually holds under the model. + self.assertTrue( + is_prediction_flip(self.gen.model.predict([cf.original_text])[0], self.gen.model.predict([cf.new_text])[0]) + ) + + def test_minimality(self): + cfs = self.gen.generate("this is great good", config={"num_examples": 10, "max_ablations": 3}) + sets = [frozenset(cf.flipped_positions) for cf in cfs] + for i, a in enumerate(sets): + for j, b in enumerate(sets): + if i != j: + self.assertFalse(a < b, "returned a non-minimal (superset) counterfactual") + + def test_respects_num_examples_cap(self): + # "meh great super": removing either strong-positive token alone flips, so two size-1 + # counterfactuals exist; the cap stops generation after the first. + capped = self.gen.generate("meh great super", config={"num_examples": 1, "max_ablations": 1}) + self.assertEqual(len(capped), 1) + uncapped = self.gen.generate("meh great super", config={"num_examples": 5, "max_ablations": 1}) + self.assertEqual(len(uncapped), 2) + + def test_targeted_flip(self): + cfs = self.gen.generate("this is great", target_label="neg", config={"max_ablations": 2}) + self.assertTrue(cfs) + self.assertTrue(all(cf.new_label == "neg" for cf in cfs)) + + def test_tokens_to_ignore_blocks_flip(self): + cfs = self.gen.generate("this is great", config={"tokens_to_ignore": ["great", "good"], "max_ablations": 2}) + self.assertEqual(cfs, []) + + def test_no_content_tokens_returns_empty(self): + cfs = self.gen.generate("! ? .", config={"max_ablations": 2}) + self.assertEqual(cfs, []) + + +if __name__ == "__main__": + unittest.main() From 295adbbf1fb85039710655144e4104bc7d838395 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 09:45:51 +0200 Subject: [PATCH 054/144] refactor: update serving script with new captum methods --- demo/serve_nlp_ext.py | 139 +++++++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 22 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index c70456c8..4c6377c8 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -34,10 +34,26 @@ ``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the model's spelling so predicted vs. ground-truth comparisons in the webapp line up. +Attribution and counterfactual methods are both selectable (Captum-backed alternatives to the +defaults, see the ``[nlp]`` extra): + +* ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` + (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can + flip between them freely without ``--recompute``. +* ``--counterfactual {hotflip,ablation}`` — What-if generator: ``hotflip`` (gradient-based token + substitution, default) or ``ablation`` (Captum ``FeatureAblation`` token removal). Runs live in the + lab, so it is not cached. + +The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: +``<model>/<dataset>__<split>/`` holds the (backend-independent) ``<hash>.proj.npy`` projection, and a +per-backend ``<...>/nlp_shap/`` or ``<...>/nlp_captum_lig/`` subdirectory holds that method's +``<hash>.pkl`` contributions. + Usage ----- python demo/serve_nlp_ext.py [--n 100] [--cache-dir demo/nlp_ext_cache] [--port 8051] python demo/serve_nlp_ext.py --recompute # ignore the cache and recompute + python demo/serve_nlp_ext.py --attribution lig --counterfactual ablation # Captum methods python demo/serve_nlp_ext.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ --dataset-name sst2 --dataset-split validation --text-column sentence python demo/serve_nlp_ext.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ @@ -60,7 +76,8 @@ import torch import transformers -from shapash.compute.generators import HotFlipGenerator +from shapash.backend import NlpCaptumLigBackend +from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.explainer.nlp_explainer import ( NlpExplainer, _hash_texts, # same keying as the compile cache @@ -87,20 +104,44 @@ class ServeConfig: label_column: str = "label" label_map: dict[str, str] = field(default_factory=dict) n: int = 500 + attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) + counterfactual: str = "hotflip" # what-if generator: "hotflip" | "ablation" (Captum FeatureAblation) cache_dir: Path = _HERE / "nlp_ext_cache" port: int = 8051 host: str = "0.0.0.0" # noqa: S104 recompute: bool = False -def _model_cache_dir(config: ServeConfig) -> Path: - """Return ``config.cache_dir`` namespaced by model. +# Short ``--attribution`` choice → the attribution backend's registered ``.name`` (used as the cache +# subdirectory *and* to pick the backend in ``build_backend``). +_ATTRIBUTION_BACKENDS = {"shap": "nlp_shap", "lig": "nlp_captum_lig"} + + +def _dataset_slug(config: ServeConfig) -> str: + """Filesystem-safe ``<dataset>__<split>`` tag for the cache hierarchy.""" + return f"{config.dataset_name}__{config.dataset_split}".replace("/", "__") + + +def _dataset_cache_dir(config: ServeConfig) -> Path: + """Return ``cache_dir/<model>/<dataset>__<split>`` — the level the projection is cached at. - The compile/projection caches are keyed by a hash of the input texts only (see - ``_hash_texts``), so two different models run over the same dataset/``--n`` would - otherwise collide on the same cache file. A per-model subdirectory keeps them apart. + The compile/projection caches are keyed by a hash of the input texts only (see ``_hash_texts``), + so different models or datasets would otherwise collide on the same cache file; the ``<model>`` and + ``<dataset>`` path levels keep them apart. The PaCMAP projection embeds the texts with + ``model.embed`` and never touches the attribution backend, so every ``--attribution`` choice shares + one projection cache at *this* level (below it, contributions split per backend). """ - return config.cache_dir / config.model_name.replace("/", "__") + return config.cache_dir / config.model_name.replace("/", "__") / _dataset_slug(config) + + +def _compile_cache_dir(config: ServeConfig) -> Path: + """Return ``<dataset-dir>/<backend>`` — the level contributions are cached at. + + Contributions *do* depend on the attribution backend, so SHAP and LIG get separate subdirectories. + Switching ``--attribution`` then reads/writes a different ``<hash>.pkl`` instead of silently + reusing the other method's cached highlights — no ``--recompute`` needed to swap methods. + """ + return _dataset_cache_dir(config) / _ATTRIBUTION_BACKENDS[config.attribution] def parse_args(argv: list[str] | None = None) -> ServeConfig: @@ -125,6 +166,24 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: ), ) parser.add_argument("--n", type=int, default=defaults.n, help="Number of samples to load.") + parser.add_argument( + "--attribution", + choices=list(_ATTRIBUTION_BACKENDS), + default=defaults.attribution, + help=( + "Sentence-highlight attribution method: 'shap' (KernelSHAP, the default) or 'lig' " + "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." + ), + ) + parser.add_argument( + "--counterfactual", + choices=["hotflip", "ablation"], + default=defaults.counterfactual, + help=( + "What-if counterfactual generator: 'hotflip' (gradient-based token substitution, the " + "default) or 'ablation' (Captum FeatureAblation token removal). Runs live — not cached." + ), + ) parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) parser.add_argument("--port", type=int, default=defaults.port) parser.add_argument("--host", default=defaults.host) @@ -177,6 +236,25 @@ def load_model(config: ServeConfig) -> HFClassifierModel: return HFClassifierModel(classifier, tokenizer) +def build_backend(config: ServeConfig, model: HFClassifierModel) -> NlpCaptumLigBackend | None: + """Return the attribution backend selected by ``--attribution``. + + Returns ``None`` for ``"shap"`` so ``NlpExplainer`` builds its default ``NlpShapBackend`` (which + needs the model's ``shap_callable`` wiring); ``"lig"`` returns an explicit ``NlpCaptumLigBackend``. + """ + if config.attribution == "lig": + # LIG runs one integration per class per sample — show a progress bar over the batch. + return NlpCaptumLigBackend(model, label_names=model.label_names, show_progress=True) + return None + + +def build_cf_generator(config: ServeConfig, model: HFClassifierModel) -> HotFlipGenerator | AblationFlipGenerator: + """Return the counterfactual generator selected by ``--counterfactual`` (both fit an HF classifier).""" + if config.counterfactual == "ablation": + return AblationFlipGenerator(model) + return HotFlipGenerator(model) + + def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> np.ndarray: """Return a 2-D PaCMAP projection, reading from / writing to ``cache_dir``. @@ -209,29 +287,46 @@ def main() -> None: logging.getLogger(noisy).setLevel(logging.WARNING) sentences, y_true = load_data(config) - cache_dir = _model_cache_dir(config) + dataset_dir = _dataset_cache_dir(config) # projection lives here (shared across attribution backends) + compile_dir = _compile_cache_dir(config) # contributions live here (one subdir per attribution backend) text_hash = _hash_texts(list(sentences)) if config.recompute: - # Drop this text set's cached artifacts so the steps below recompute + overwrite. - logger.info("--recompute: dropping cached SHAP + projection for these texts") - (cache_dir / f"{text_hash}.pkl").unlink(missing_ok=True) - (cache_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) + # Drop this text set's cached artifacts so the steps below recompute + overwrite. Only the + # selected backend's contributions are dropped; other backends' caches are left intact. + logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) + (compile_dir / f"{text_hash}.pkl").unlink(missing_ok=True) + (dataset_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) model = load_model(config) - xpl = NlpExplainer(model, label_names=model.label_names, cf_generator=HotFlipGenerator(model)) - # Both steps load from cache_dir if a cache for these exact texts exists, otherwise they - # compute and write it. So the first run is slow; later runs only pay the model load. - compile_cache = cache_dir / f"{text_hash}.pkl" + xpl = NlpExplainer( + model, + label_names=model.label_names, + backend=build_backend(config, model), + cf_generator=build_cf_generator(config, model), + ) + # Both steps load from cache if one exists for these exact texts, otherwise they compute and write + # it. So the first run is slow; later runs only pay the model load. + compile_cache = compile_dir / f"{text_hash}.pkl" if compile_cache.exists(): - logger.info("SHAP cache hit — loading %s", compile_cache) + logger.info("Contributions cache hit (%s) — loading %s", config.attribution, compile_cache) else: - logger.info("SHAP cache miss — computing contributions for %d texts (this is the slow part)", len(sentences)) - xpl.compile(sentences, y_true=y_true, cache_dir=cache_dir) - projected = load_or_project(sentences, model, cache_dir) - - logger.info("can_edit=%s | can_counterfactual=%s", xpl.can_edit(), xpl.can_counterfactual()) + logger.info( + "Contributions cache miss (%s) — computing for %d texts (this is the slow part)", + config.attribution, + len(sentences), + ) + xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) + projected = load_or_project(sentences, model, dataset_dir) + + logger.info( + "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s", + config.attribution, + config.counterfactual, + xpl.can_edit(), + xpl.can_counterfactual(), + ) logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) From 66e613a22315cdb9e9f5341b361fa414d425861f Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 15:44:04 +0200 Subject: [PATCH 055/144] refactor: allow both counterfactual methods in webapp --- shapash/compute/generators/ablation_flip.py | 1 + shapash/compute/generators/base.py | 2 + shapash/compute/generators/hotflip.py | 1 + shapash/explainer/interactive.py | 17 ++- shapash/explainer/nlp_explainer.py | 75 +++++++--- .../webapp/nlp_components/counterfactual.py | 129 +++++++++++++----- 6 files changed, 169 insertions(+), 56 deletions(-) diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py index 99dec0b3..5db67f71 100644 --- a/shapash/compute/generators/ablation_flip.py +++ b/shapash/compute/generators/ablation_flip.py @@ -45,6 +45,7 @@ class AblationFlipGenerator(CounterfactualGenerator): """Perturbation-based token-removal counterfactuals (Captum ``FeatureAblation``).""" name = "ablation_flip" + display_name = "Ablation" def config_spec(self) -> dict[str, Field]: """Expose ``num_examples``, ``max_ablations`` and ``tokens_to_ignore`` as tunable knobs.""" diff --git a/shapash/compute/generators/base.py b/shapash/compute/generators/base.py index cb9d9e93..ef7fff74 100644 --- a/shapash/compute/generators/base.py +++ b/shapash/compute/generators/base.py @@ -87,6 +87,8 @@ class CounterfactualGenerator(ABC): """ name: str = "counterfactual" + #: Human-readable label for the webapp method selector (falls back to a title-cased ``name``). + display_name: str = "Counterfactual" def __init__(self, model: TextModel) -> None: self.model = model diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index f96911e1..73d1d828 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -57,6 +57,7 @@ class HotFlipGenerator(CounterfactualGenerator): """Gradient-based token-substitution counterfactuals (LIT HotFlip).""" name = "hotflip" + display_name = "HotFlip" def config_spec(self) -> dict[str, Field]: """Expose ``num_examples``, ``max_flips`` and ``tokens_to_ignore`` as tunable knobs.""" diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py index 46438b53..e056289c 100644 --- a/shapash/explainer/interactive.py +++ b/shapash/explainer/interactive.py @@ -52,10 +52,19 @@ def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, floa """ ... - def generate_counterfactuals(self, text: str, config: dict | None = None) -> list[Counterfactual]: - """Generate counterfactuals for ``text`` using the bound generator.""" + def available_cf_generators(self) -> list[tuple[str, str]]: + """Return ``(name, display_name)`` for each selectable counterfactual generator. + + Empty when no generator is bound. The webapp renders a method selector from this list. + """ + ... + + def generate_counterfactuals( + self, text: str, config: dict | None = None, generator: str | None = None + ) -> list[Counterfactual]: + """Generate counterfactuals for ``text`` using the selected (or active) generator.""" ... - def cf_config_spec(self) -> dict[str, Field]: - """Return the bound generator's config spec (empty when no generator).""" + def cf_config_spec(self, generator: str | None = None) -> dict[str, Field]: + """Return the selected (or active) generator's config spec (empty when no generator).""" ... diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 45ec4b99..f640bd2d 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -29,6 +29,11 @@ from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp +# Built-in counterfactual generators, in preference order: HotFlip (gradient-based, richer +# substitutions) first, AblationFlip (forward-pass-only removal) as the broader fallback. Every entry +# compatible with the bound model is offered in the webapp's method selector. +_BUILTIN_CF_GENERATORS: tuple[type[CounterfactualGenerator], ...] = (HotFlipGenerator, AblationFlipGenerator) + def _looks_like_pipeline(model: object) -> bool: """Heuristic: a HuggingFace ``text-classification`` pipeline is callable and has a tokenizer.""" @@ -114,17 +119,21 @@ def __init__( explainer_compute_args=explainer_compute_args or {}, ) - # Counterfactual generator: explicit > auto-built > none. HotFlip (gradient-based, richer - # substitutions) is preferred; AblationFlip (forward-pass-only removal) is the fallback so a - # prediction-only model such as a plain pipeline still gets a What-if Lab. + # Counterfactual generators: explicit > auto-discovered > none. An explicit ``cf_generator`` + # is used verbatim (no surprise additions). Otherwise every built-in compatible with the model + # is offered — so the webapp can switch methods live — with HotFlip preferred and AblationFlip + # (forward-pass-only) covering prediction-only models such as a plain pipeline. ``cf_generator`` + # stays the *active/default* generator for callers that don't select one. if cf_generator is not None: - self.cf_generator: CounterfactualGenerator | None = cf_generator - elif self._text_model is not None and HotFlipGenerator.is_compatible(self._text_model): - self.cf_generator = HotFlipGenerator(self._text_model) - elif self._text_model is not None and AblationFlipGenerator.is_compatible(self._text_model): - self.cf_generator = AblationFlipGenerator(self._text_model) + generators: list[CounterfactualGenerator] = [cf_generator] + elif self._text_model is not None: + generators = [ + cls(self._text_model) for cls in _BUILTIN_CF_GENERATORS if cls.is_compatible(self._text_model) + ] else: - self.cf_generator = None + generators = [] + self.cf_generators: dict[str, CounterfactualGenerator] = {g.name: g for g in generators} + self.cf_generator: CounterfactualGenerator | None = generators[0] if generators else None self.contributions: NlpContributions | None = None self.texts: pd.Series | None = None @@ -279,6 +288,7 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: xpl.backend = None xpl._text_model = None xpl.cf_generator = None + xpl.cf_generators = {} xpl.label_names = state.get("label_names") xpl.texts = state["texts"] xpl.contributions = state["contributions"] @@ -345,15 +355,46 @@ def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, floa label, probabilities = self.predict(text) return contributions, label, probabilities - def generate_counterfactuals(self, text: str, config: dict | None = None) -> list[Counterfactual]: - """Generate counterfactuals for ``text`` via the bound generator.""" - if getattr(self, "cf_generator", None) is None: - raise RuntimeError("No counterfactual generator is configured for this explainer.") - return self.cf_generator.generate(text, config=config) + def available_cf_generators(self) -> list[tuple[str, str]]: + """Return ``(name, display_name)`` for each bound generator, in preference order. + + Drives the webapp's counterfactual-method selector; empty when no generator is configured + (e.g. a snapshot-restored explainer). + """ + return [(g.name, g.display_name) for g in getattr(self, "cf_generators", {}).values()] + + def generate_counterfactuals( + self, text: str, config: dict | None = None, generator: str | None = None + ) -> list[Counterfactual]: + """Generate counterfactuals for ``text`` via the selected (or active) generator. - def cf_config_spec(self) -> dict[str, Field]: - """Return the generator's tunable config spec (empty when no generator).""" - return self.cf_generator.config_spec() if getattr(self, "cf_generator", None) is not None else {} + Parameters + ---------- + text : str + Text to perturb. + config : dict, optional + Overrides for the generator's config spec. + generator : str, optional + ``name`` of one of :meth:`available_cf_generators`. Defaults to the active generator. + """ + gen = self._select_cf_generator(generator) + if gen is None: + raise RuntimeError("No counterfactual generator is configured for this explainer.") + return gen.generate(text, config=config) + + def cf_config_spec(self, generator: str | None = None) -> dict[str, Field]: + """Return the tunable config spec of the selected (or active) generator, empty when none.""" + gen = self._select_cf_generator(generator) + return gen.config_spec() if gen is not None else {} + + def _select_cf_generator(self, generator: str | None) -> CounterfactualGenerator | None: + """Resolve a generator by ``name`` (or the active one when ``generator`` is ``None``).""" + generators = getattr(self, "cf_generators", {}) + if generator is not None: + if generator not in generators: + raise KeyError(f"Unknown counterfactual generator {generator!r}; available: {list(generators)}.") + return generators[generator] + return getattr(self, "cf_generator", None) # ------------------------------------------------------------------ # Private diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index 2815fc56..78df196c 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -1,8 +1,11 @@ """Counterfactual component: generate what-if flips and apply them into the editor. -Controls are rendered automatically from the bound generator's ``config_spec()`` (so a new knob or a -new generator needs no UI change). Generated counterfactuals are shown as a table; an *Apply* button -publishes the chosen text to a shared store the data editor subscribes to. +When the explainer binds more than one generator (e.g. HotFlip *and* AblationFlip on a gradient model) +a **method selector** lets the user switch between them live; with a single generator the selector is +hidden. Each generator's controls are rendered automatically from its ``config_spec()`` (so a new knob +or a new generator needs no UI change) into its own visibility-toggled group. Generated counterfactuals +are shown as a table; an *Apply* button publishes the chosen text to a shared store the data editor +subscribes to. """ from __future__ import annotations @@ -17,6 +20,12 @@ _CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} +# Inline flex styles (not the Bootstrap ``d-flex`` class, whose ``!important`` would defeat the +# ``display: none`` visibility toggle on the per-generator control groups). +_INLINE = {"display": "flex", "alignItems": "center", "gap": "0.4rem"} +_GROUP_STYLE = {"display": "flex", "flexWrap": "wrap", "alignItems": "center", "gap": "0.75rem"} +_HIDDEN = {"display": "none"} + class CounterfactualComponent(WebappComponent): """Spec-driven counterfactual generation panel.""" @@ -27,15 +36,19 @@ class CounterfactualComponent(WebappComponent): # Requires the editor too (it reads the editor's text and applies flips back into it). requires = frozenset({CAP_COUNTERFACTUAL, CAP_PREDICT}) - def _config_fields(self, engine) -> list[tuple[str, Field]]: - """Ordered ``(name, field)`` pairs from the engine's config spec.""" - return list(engine.cf_config_spec().items()) + def _generators(self, engine) -> list[tuple[str, str]]: + """Ordered ``(name, display_name)`` pairs the engine offers (empty when none).""" + return engine.available_cf_generators() if engine is not None else [] + + def _fields(self, engine, generator: str) -> list[tuple[str, Field]]: + """Ordered ``(name, field)`` pairs from one generator's config spec.""" + return list(engine.cf_config_spec(generator).items()) - def _config_controls(self, engine) -> list: - """Render one control per config field (IntField → number, TokenListField → text).""" - rows = [] - for name, fld in self._config_fields(engine): - cid = f"{self.id}-cfg-{name}" + def _config_controls(self, engine, generator: str) -> list: + """Render each config field of ``generator`` as an inline label+control (IntField → number).""" + items = [] + for name, fld in self._fields(engine, generator): + cid = f"{self.id}-cfg-{generator}-{name}" if isinstance(fld, IntField): control = dcc.Input( id=cid, @@ -44,7 +57,7 @@ def _config_controls(self, engine) -> list: max=fld.maximum, step=1, value=fld.default, - style={"width": "90px"}, + style={"width": "80px"}, ) elif isinstance(fld, TokenListField): control = dbc.Input( @@ -52,36 +65,66 @@ def _config_controls(self, engine) -> list: type="text", value=",".join(fld.default), placeholder="comma,separated", - style={"width": "180px"}, + style={"width": "150px"}, ) else: # pragma: no cover - future field types control = dbc.Input(id=cid, type="text", value=str(fld.default)) - rows.append( - dbc.Row( - [ - dbc.Col(html.Label(fld.label, className="small fw-bold mb-0"), width="auto"), - dbc.Col(control, width="auto"), - ], - className="align-items-center mb-2 g-2", + items.append( + html.Div( + [html.Label(fld.label, className="small fw-bold mb-0"), control], + style=_INLINE, ) ) - return rows + return items def layout(self, view, engine=None) -> html.Div: - """Return the counterfactual card with config controls rendered from the generator spec. + """Return the counterfactual card with a method selector and per-generator config controls. - The controls are built here (not injected by a callback) so their ids exist in the initial - layout — otherwise the ``generate`` callback's ``State`` references a not-yet-created object. + The selector and *every* generator's controls are built here (not injected by a callback) so + their ids exist in the initial layout — otherwise the ``generate`` callback's ``State`` would + reference not-yet-created objects. Only the active generator's control group is visible; the + selector callback toggles the rest. The selector is hidden when a single generator is bound. """ - controls = self._config_controls(engine) if engine is not None else [] + generators = self._generators(engine) + default_gen = generators[0][0] if generators else None + + # Method selector — kept in the layout even for a single generator (so its id/State exists), + # just hidden. Its config controls live in per-generator groups toggled by the selector. + selector = html.Div( + [ + html.Label("Method", className="small fw-bold mb-0"), + dbc.Select( + id=f"{self.id}-generator", + options=[{"label": label, "value": name} for name, label in generators], + value=default_gen, + size="sm", + style={"width": "170px"}, + ), + ], + style=_INLINE if len(generators) > 1 else _HIDDEN, + ) + groups = [ + html.Div( + self._config_controls(engine, name), + id=f"{self.id}-cfg-group-{name}", + style=_GROUP_STYLE if name == default_gen else _HIDDEN, + ) + for name, _ in generators + ] + # Selector + the active generator's controls all flow on one line to save vertical space. + controls_row = html.Div( + [selector, *groups], + id=f"{self.id}-controls", + className="d-flex flex-wrap align-items-center gap-3 mb-2", + ) return html.Div( [ html.H6("Counterfactual Suggestions", className="fw-bold mb-2"), html.Small( - "Find minimal token substitutions that flip the prediction of the edited text.", + "Find minimal token edits that flip the prediction of the edited text.", className="text-muted d-block mb-2", ), - html.Div(controls, id=f"{self.id}-controls"), + controls_row, dbc.Button("Generate", id=f"{self.id}-generate-btn", color="secondary", size="sm", className="mb-2"), dcc.Loading(html.Div(id=f"{self.id}-results")), dcc.Store(id=f"{self.id}-store", data=[]), @@ -90,14 +133,26 @@ def layout(self, view, engine=None) -> html.Div: ) def register_callbacks(self, app, view, engine, stores) -> None: - """Wire control rendering, Generate, and per-row Apply (→ shared editor store).""" + """Wire the method selector, Generate, and per-row Apply (→ shared editor store).""" apply_store = stores["apply"] current_store = stores["current"] - fields = self._config_fields(engine) - # Config controls are rendered in `layout()` (they must be in the initial layout), so no - # separate render callback is needed here. - config_states = [State(f"{self.id}-cfg-{name}", "value") for name, _ in fields] + generators = self._generators(engine) + gen_names = [name for name, _ in generators] + # Flat, ordered list of every (generator, field name, field) — one State per rendered control. + all_fields = [(gen, name, fld) for gen in gen_names for name, fld in self._fields(engine, gen)] + config_states = [State(f"{self.id}-cfg-{gen}-{name}", "value") for gen, name, _ in all_fields] + + # Show only the selected generator's control group. Skipped for a single-method explainer + # (nothing to toggle); config controls themselves live in `layout()` so their ids always exist. + if len(gen_names) > 1: + + @app.callback( + [Output(f"{self.id}-cfg-group-{gen}", "style") for gen in gen_names], + Input(f"{self.id}-generator", "value"), + ) + def toggle_controls(selected): + return [_GROUP_STYLE if gen == selected else _HIDDEN for gen in gen_names] # Reads the current datapoint (a selected dataset row *or* an edited/predicted text) so # counterfactuals can be generated for both, not only hand-typed editor text. @@ -106,21 +161,25 @@ def register_callbacks(self, app, view, engine, stores) -> None: Output(f"{self.id}-store", "data"), Input(f"{self.id}-generate-btn", "n_clicks"), State(current_store, "data"), + State(f"{self.id}-generator", "value"), *config_states, ) - def generate(n_clicks, datapoint, *config_values): + def generate(n_clicks, datapoint, selected_gen, *config_values): text = (datapoint or {}).get("text", "") if not n_clicks or not text or not text.strip(): raise PreventUpdate + gen_name = selected_gen or (gen_names[0] if gen_names else None) config = {} - for (name, fld), value in zip(fields, config_values, strict=True): + for (gen, name, fld), value in zip(all_fields, config_values, strict=True): + if gen != gen_name: + continue # only the active generator's controls feed its config if isinstance(fld, IntField): config[name] = int(value) if value is not None else fld.default elif isinstance(fld, TokenListField): config[name] = [t.strip() for t in (value or "").split(",") if t.strip()] else: # pragma: no cover config[name] = value - cfs = engine.generate_counterfactuals(text, config=config) + cfs = engine.generate_counterfactuals(text, config=config, generator=gen_name) if not cfs: return html.Div("No counterfactual found within these limits.", className="text-muted"), [] return _results_table(cfs, self.id), [cf.new_text for cf in cfs] From 42f01bca6ecaeaafed16d02e70bd8029457f4065 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 15:44:34 +0200 Subject: [PATCH 056/144] test: update tests --- tests/unit_tests/nlp/test_nlp_prototype.py | 105 ++++++++++++++++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 28 ++++- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 6269035e..8ac49c58 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -18,7 +18,9 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation from shapash.backend.nlp_lime_backend import NlpLimeBackend from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight @@ -441,6 +443,109 @@ def test_y_true_is_none_by_default(self): self.assertIsNone(xpl.y_true) +# --------------------------------------------------------------------------- +# NlpExplainer — counterfactual generator discovery / selection (captum-free) +# --------------------------------------------------------------------------- + + +class _FullCapModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): + """A model exposing every capability — both HotFlip and AblationFlip are compatible.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + def get_embedding_table(self): + return (["a"], np.zeros((1, 2))) + + def embed(self, texts): + return np.zeros((len(texts), 2)) + + def token_gradients(self, text, target_class): + toks = text.split() + return toks, np.zeros((len(toks), 2)) + + @property + def shap_callable(self): + return self.predict + + +class _TokenizeOnlyModel(TextModel, SupportsTokenization): + """Tokenizable but gradient-free — only AblationFlip is compatible.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + @property + def shap_callable(self): + return self.predict + + +class TestNlpExplainerGenerators(unittest.TestCase): + """Generator auto-discovery drives the webapp's method selector (no captum needed here).""" + + def test_full_capability_model_offers_both_methods(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + self.assertEqual( + xpl.available_cf_generators(), + [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")], + ) + # The preferred (first-discovered) generator stays the active default. + self.assertIsInstance(xpl.cf_generator, HotFlipGenerator) + self.assertEqual(set(xpl.cf_generators), {"hotflip", "ablation_flip"}) + + def test_tokenize_only_model_offers_ablation_only(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) + self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) + self.assertIsInstance(xpl.cf_generator, AblationFlipGenerator) + + def test_explicit_generator_used_verbatim_no_extras(self): + model = _FullCapModel() + gen = AblationFlipGenerator(model) + xpl = NlpExplainer(model, backend=object(), cf_generator=gen) + # An explicit choice is not augmented with the other compatible built-ins. + self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) + self.assertIs(xpl.cf_generator, gen) + + def test_cf_config_spec_selected_by_generator(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + self.assertIn("max_flips", xpl.cf_config_spec("hotflip")) + self.assertIn("max_ablations", xpl.cf_config_spec("ablation_flip")) + # No argument → the active generator's spec. + self.assertIn("max_flips", xpl.cf_config_spec()) + + def test_unknown_generator_raises(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + with self.assertRaises(KeyError): + xpl.cf_config_spec("does_not_exist") + with self.assertRaises(KeyError): + xpl.generate_counterfactuals("hi there", generator="does_not_exist") + + def test_no_generators_without_text_model(self): + # A plain callable is neither a TextModel nor a pipeline → no generators, empty selector. + xpl = NlpExplainer(lambda texts: np.tile([0.5, 0.5], (len(texts), 1)), label_names=["neg", "pos"], backend=object()) + self.assertEqual(xpl.available_cf_generators(), []) + self.assertEqual(xpl.cf_config_spec(), {}) + self.assertIsNone(xpl.cf_generator) + + # --------------------------------------------------------------------------- # NlpWebApp layout (no server launch) # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 106c353c..07621c23 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -46,10 +46,14 @@ def can_edit(self): def can_counterfactual(self): return self._can_cf - def cf_config_spec(self): + def available_cf_generators(self): + return [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")] + + def cf_config_spec(self, generator=None): + max_field = "max_ablations" if generator == "ablation_flip" else "max_flips" return { "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), - "max_flips": IntField(label="Max token flips", default=3, minimum=1, maximum=5), + max_field: IntField(label="Max token edits", default=3, minimum=1, maximum=5), "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), } @@ -60,7 +64,7 @@ def explain_text(self, text): c = _contributions() return c, "pos", {"neg": 0.3, "pos": 0.7} - def generate_counterfactuals(self, text, config=None): + def generate_counterfactuals(self, text, config=None, generator=None): return [ Counterfactual( original_text=text, @@ -108,10 +112,24 @@ def test_full_capabilities_mount_both(self): def test_counterfactual_config_controls_in_initial_layout(self): # The generate callback's State references these ids, so they must exist in the initial - # layout (rendered from the engine's cf_config_spec), not be injected by a later callback. + # layout (rendered per generator from cf_config_spec), not be injected by a later callback. _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) for name in ("num_examples", "max_flips", "tokens_to_ignore"): - self.assertIn(f"counterfactual-cfg-{name}", ids) + self.assertIn(f"counterfactual-cfg-hotflip-{name}", ids) + for name in ("num_examples", "max_ablations", "tokens_to_ignore"): + self.assertIn(f"counterfactual-cfg-ablation_flip-{name}", ids) + + def test_counterfactual_method_selector_present_with_multiple_generators(self): + # A method selector and one visibility-toggled control group per generator are in the layout. + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("counterfactual-generator", ids) + self.assertIn("counterfactual-cfg-group-hotflip", ids) + self.assertIn("counterfactual-cfg-group-ablation_flip", ids) + + def test_selector_toggle_callback_registered_with_multiple_generators(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("counterfactual-cfg-group-hotflip.style", outputs) def test_edit_only_mounts_editor_not_counterfactual(self): app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) From c24bee3c946e683f04434930d710fcbe9ce1d601 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 15:44:47 +0200 Subject: [PATCH 057/144] refactor: update main serving script --- demo/serve_nlp_ext.py | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index 4c6377c8..851a78e7 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -34,15 +34,16 @@ ``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the model's spelling so predicted vs. ground-truth comparisons in the webapp line up. -Attribution and counterfactual methods are both selectable (Captum-backed alternatives to the -defaults, see the ``[nlp]`` extra): +The sentence-highlight attribution method is selectable (a Captum-backed alternative to the default, +see the ``[nlp]`` extra): * ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can flip between them freely without ``--recompute``. -* ``--counterfactual {hotflip,ablation}`` — What-if generator: ``hotflip`` (gradient-based token - substitution, default) or ``ablation`` (Captum ``FeatureAblation`` token removal). Runs live in the - lab, so it is not cached. + +Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token +substitution) and ``ablation`` (Captum ``FeatureAblation`` token removal) — and are switched from a +**method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: ``<model>/<dataset>__<split>/`` holds the (backend-independent) ``<hash>.proj.npy`` projection, and a @@ -53,7 +54,7 @@ ----- python demo/serve_nlp_ext.py [--n 100] [--cache-dir demo/nlp_ext_cache] [--port 8051] python demo/serve_nlp_ext.py --recompute # ignore the cache and recompute - python demo/serve_nlp_ext.py --attribution lig --counterfactual ablation # Captum methods + python demo/serve_nlp_ext.py --attribution lig # Captum LayerIntegratedGradients highlights python demo/serve_nlp_ext.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ --dataset-name sst2 --dataset-split validation --text-column sentence python demo/serve_nlp_ext.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ @@ -77,7 +78,6 @@ import transformers from shapash.backend import NlpCaptumLigBackend -from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.explainer.nlp_explainer import ( NlpExplainer, _hash_texts, # same keying as the compile cache @@ -105,7 +105,6 @@ class ServeConfig: label_map: dict[str, str] = field(default_factory=dict) n: int = 500 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) - counterfactual: str = "hotflip" # what-if generator: "hotflip" | "ablation" (Captum FeatureAblation) cache_dir: Path = _HERE / "nlp_ext_cache" port: int = 8051 host: str = "0.0.0.0" # noqa: S104 @@ -175,15 +174,6 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." ), ) - parser.add_argument( - "--counterfactual", - choices=["hotflip", "ablation"], - default=defaults.counterfactual, - help=( - "What-if counterfactual generator: 'hotflip' (gradient-based token substitution, the " - "default) or 'ablation' (Captum FeatureAblation token removal). Runs live — not cached." - ), - ) parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) parser.add_argument("--port", type=int, default=defaults.port) parser.add_argument("--host", default=defaults.host) @@ -248,13 +238,6 @@ def build_backend(config: ServeConfig, model: HFClassifierModel) -> NlpCaptumLig return None -def build_cf_generator(config: ServeConfig, model: HFClassifierModel) -> HotFlipGenerator | AblationFlipGenerator: - """Return the counterfactual generator selected by ``--counterfactual`` (both fit an HF classifier).""" - if config.counterfactual == "ablation": - return AblationFlipGenerator(model) - return HotFlipGenerator(model) - - def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> np.ndarray: """Return a 2-D PaCMAP projection, reading from / writing to ``cache_dir``. @@ -300,11 +283,12 @@ def main() -> None: model = load_model(config) + # No cf_generator is passed: NlpExplainer auto-discovers every built-in compatible with the model + # (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's method dropdown. xpl = NlpExplainer( model, label_names=model.label_names, backend=build_backend(config, model), - cf_generator=build_cf_generator(config, model), ) # Both steps load from cache if one exists for these exact texts, otherwise they compute and write # it. So the first run is slow; later runs only pay the model load. @@ -323,7 +307,7 @@ def main() -> None: logger.info( "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s", config.attribution, - config.counterfactual, + ",".join(name for name, _ in xpl.available_cf_generators()) or "none", xpl.can_edit(), xpl.can_counterfactual(), ) From 6d7919641cfb065194d8ffab82c703dd203e0daa Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 16:59:38 +0200 Subject: [PATCH 058/144] feat: integrate two different global and local class selector --- shapash/webapp/nlp_app.py | 103 +++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 9f22d17f..63453798 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -195,6 +195,9 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: def _build_layout(self) -> None: contrib = self.explainer.contributions label_names = contrib.label_names or [str(i) for i in range(self._n_classes())] + # Predicted-label name → class index, shared by the confusion matrix and by the sync + # callback that resets the Sentence Highlight class picker to the predicted class. + self._label_to_idx = {name: i for i, name in enumerate(label_names)} n = len(self.explainer.texts) # ── Table records — always include _orig_idx for scatter filtering ── @@ -321,7 +324,7 @@ def _build_layout(self) -> None: # per-cell word charts below (words toward the predicted vs. the true class). error_analysis_body = None if has_gt: - idx_of = {name: i for i, name in enumerate(label_names)} + idx_of = self._label_to_idx true_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_true.tolist()]) pred_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_pred.tolist()]) k = len(label_names) @@ -416,13 +419,26 @@ def _build_layout(self) -> None: ) # ── Global Word Importance panel — controls live inside it ── - # (Top-K / Sign / Exclude only affect this panel, so they are co-located here.) + # (Top-K / Sign / Exclude / Class only affect this panel, so they are co-located here — + # independent from the local class picker in the Sentence Highlight panel below.) # The tab label already names the panel, so no H6; a tight controls row + a graph that # flex-grows to fill the panel means the whole chart is visible without scrolling. word_importance_panel = html.Div( [ dbc.Row( [ + dbc.Col( + [ + html.Label("Class", className="fw-bold small mb-0"), + dcc.Dropdown( + id="global-class-selector", + options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + value=0, + clearable=False, + ), + ], + width=3, + ), dbc.Col( [ html.Label("Top-K words", className="fw-bold small mb-0"), @@ -527,12 +543,28 @@ def _build_layout(self) -> None: ) # ── Sentence highlight body (lower-right default tab) ───────── + # Local class picker lives here (and drives the sibling Waterfall tab too, since both + # render the same selected sentence): it defaults to the predicted class of the initially + # selected row and is reset to the newly-selected text's prediction by a sync callback — + # see sync_local_class_to_prediction — so switching sentences always starts on "why did the + # model predict this", while still letting the user override it for the current sentence. + default_local_class = self._label_to_idx.get(self._full_table_records[0].get("prediction"), 0) highlight_body = html.Div( [ - html.H6( - id="sentence-highlight-title", - children="Token Contributions", - className="fw-bold", + # One inline phrase instead of a title + a same-info dropdown: the class name would + # otherwise appear twice (once written out, once as the dropdown's selected value). + html.Div( + [ + html.Span("Token Contributions for", className="fw-bold small me-2"), + dcc.Dropdown( + id="local-class-selector", + options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + value=default_local_class, + clearable=False, + style={"width": "180px"}, + ), + ], + className="d-flex align-items-center mb-2", ), # Min-height + vertical centring so short samples still give the # panel presence instead of collapsing to a thin strip. @@ -681,22 +713,12 @@ def _build_layout(self) -> None: self.app.layout = dbc.Container( [ - # ── Header (Class selector is the only truly global control) ── + # ── Header — the Class selector now lives with the panel it drives: Word + # Importance (global) and Sentence Highlight (local, see highlight_body) each + # get their own, so switching one no longer silently reinterprets the other. ── dbc.Row( [ - dbc.Col(html.H4("Shapash — NLP Explainer", className="mb-0"), width=8), - dbc.Col( - [ - html.Label("Class", className="fw-bold small"), - dcc.Dropdown( - id="class-selector", - options=[{"label": name, "value": i} for i, name in enumerate(label_names)], - value=0, - clearable=False, - ), - ], - width=4, - ), + dbc.Col(html.H4("Shapash — NLP Explainer", className="mb-0"), width=12), ], className="py-2 align-items-center", style={"flex": "0 0 auto"}, @@ -789,7 +811,7 @@ def _compose_indices(selected_indices, error_cell, errors_only=False): @self.app.callback( Output("global-importance-graph", "figure"), [ - Input("class-selector", "value"), + Input("global-class-selector", "value"), Input("topk-slider", "value"), Input("sign-filter", "value"), Input("word-filter", "value"), @@ -811,11 +833,11 @@ def update_global_importance( exclude_words=set(exclude_words_list or []) or None, sample_indices=effective_indices, ) - label_name = (contrib.label_names or [])[int(label_idx)] if contrib.label_names else str(label_idx) + # No class name here — the Class dropdown right above the chart already shows it. suffix = f" ({len(effective_indices)} samples)" if effective_indices is not None else "" fig = plot_word_importance( word_imp, - title=f"Word importance — {label_name}{suffix}", + title=f"Word importance{suffix}", width=None, height=None, ) @@ -841,17 +863,31 @@ def set_current_from_row(selected_rows): tokens=contrib.token_strings[pos], values=contrib.values[pos], base_values=base, + label=selected_rows[0].get("prediction"), ) + # ── Local class picker: reset to the newly-selected text's predicted class ─────── + # Fires only on current-datapoint changes (row click, editor Predict, counterfactual + # Apply) — not on manual dropdown edits — so a user's in-place class override survives + # until they actually switch sentences. + @self.app.callback( + Output("local-class-selector", "value"), + Input(_CURRENT_STORE, "data"), + ) + def sync_local_class_to_prediction(datapoint): + if not datapoint or datapoint.get("label") is None: + raise PreventUpdate + label_idx = self._label_to_idx.get(str(datapoint["label"])) + if label_idx is None: + raise PreventUpdate + return label_idx + # ── Sentence highlight ─────────────────────────────────────────── @self.app.callback( - [ - Output("sentence-highlight", "children"), - Output("sentence-highlight-title", "children"), - ], + Output("sentence-highlight", "children"), [ Input(_CURRENT_STORE, "data"), - Input("class-selector", "value"), + Input("local-class-selector", "value"), ], ) def update_sentence_highlight(datapoint, label_idx): @@ -859,17 +895,16 @@ def update_sentence_highlight(datapoint, label_idx): raise PreventUpdate label_idx = int(label_idx) tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) - label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) - highlight = plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) - title = f"Token Contributions — {label_name}" - return highlight, title + return plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) # ── Waterfall chart ────────────────────────────────────────────── + # Shares the local class picker with Sentence Highlight — both render the same selected + # sentence, so they should always agree on which class's contributions are shown. @self.app.callback( Output("waterfall-graph", "figure"), [ Input(_CURRENT_STORE, "data"), - Input("class-selector", "value"), + Input("local-class-selector", "value"), Input("waterfall-threshold", "value"), ], ) @@ -984,7 +1019,7 @@ def filter_table(selected_indices, word_filter, errors_only, error_cell): [ Input("color-by", "value"), Input("scatter-word-select", "value"), - Input("class-selector", "value"), + Input("global-class-selector", "value"), Input("errors-only-switch", "value"), ], ) From 8b44159239106a022b1fadde2d94b4122e81515c Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 16:59:50 +0200 Subject: [PATCH 059/144] test: update tests --- tests/unit_tests/nlp/test_nlp_prototype.py | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 8ac49c58..69722a3b 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -560,12 +560,20 @@ def test_layout_built(self): self.assertIsNotNone(self.webapp.app.layout) def test_class_selector_options(self): - # Walk layout to find the Dropdown with id "class-selector" - dropdown = self._find_component(self.webapp.app.layout, "class-selector") - self.assertIsNotNone(dropdown, "class-selector dropdown not found in layout") - self.assertEqual(len(dropdown.options), N_CLASSES) - labels = [opt["label"] for opt in dropdown.options] - self.assertEqual(labels, LABEL_NAMES) + # Class selector is now two independent dropdowns: "local-class-selector" (Sentence + # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). + for dropdown_id in ("local-class-selector", "global-class-selector"): + dropdown = self._find_component(self.webapp.app.layout, dropdown_id) + self.assertIsNotNone(dropdown, f"{dropdown_id} dropdown not found in layout") + self.assertEqual(len(dropdown.options), N_CLASSES) + labels = [opt["label"] for opt in dropdown.options] + self.assertEqual(labels, LABEL_NAMES) + + def test_local_class_selector_defaults_to_predicted_class(self): + # Defaults to the predicted class of the initially selected row (row 0). + dropdown = self._find_component(self.webapp.app.layout, "local-class-selector") + row0_prediction = self.webapp._full_table_records[0]["prediction"] + self.assertEqual(dropdown.value, LABEL_NAMES.index(row0_prediction)) def test_dataset_table_populated(self): table = self._find_component(self.webapp.app.layout, "dataset-table") @@ -583,7 +591,8 @@ def test_graph_ids_present(self): ids = self._collect_ids(self.webapp.app.layout) self.assertIn("global-importance-graph", ids) self.assertIn("dataset-table", ids) - self.assertIn("class-selector", ids) + self.assertIn("local-class-selector", ids) + self.assertIn("global-class-selector", ids) # token bar chart removed; sentence-highlight replaced it self.assertNotIn("local-contributions-graph", ids) From 46ee78bcd48d57aa40ca756ffeb8827de2489993 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 17:13:40 +0200 Subject: [PATCH 060/144] fix: aggregate token contributions to word contribution in shap backend --- shapash/backend/nlp_shap_backend.py | 86 ++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index eece9459..3cfa8222 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -4,14 +4,78 @@ ``run_explainer``. All shared infrastructure (``NlpContributions`` dataclass, ``get_local_contributions``, common ``__init__`` skeleton) lives in ``NlpBackend`` (see ``nlp_backend.py``). + +SHAP's ``Text`` masker (``shap.maskers.Text.token_segments``) reconstructs each token as the slice +of the original string running up to the start of the next token, rather than the tokenizer's raw +WordPiece strings. A subword glued directly onto the previous piece (e.g. ``"up"`` + ``"dating "`` +for "updating") therefore carries no trailing whitespace, while the piece that actually ends a word +carries the whitespace that follows it in the source sentence. ``_aggregate_subwords`` uses that +signal to merge subwords back into whole words — matching the word-level highlights +``nlp_captum_lig_backend`` produces — and folds special ([CLS]/[SEP]-equivalent blank) tokens' +attribution into the baseline so ``base + Σ(word contributions)`` keeps SHAP's additive guarantee. """ from __future__ import annotations +import re + +import numpy as np import shap from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation +# ``[CLS]``/``[SEP]``-style bracket tokens (and blanks) SHAP's ``Text`` masker reports as empty +# strings; their attribution is folded into the baseline during word aggregation. +_SPECIAL_RE = re.compile(r"^\[.*\]$|^\s*$") + + +def _aggregate_subwords( + tokens: list[str], contributions: np.ndarray, base_values: np.ndarray +) -> tuple[list[str], np.ndarray, np.ndarray]: + """Merge SHAP's subword fragments into whole words and fold specials into the baseline. + + Parameters + ---------- + tokens : list[str] + Token strings for one sample (length ``seq``), as produced by SHAP's text masker. + contributions : np.ndarray + Per-token contributions, shape ``(seq, n_classes)``. + base_values : np.ndarray + Baseline SHAP values for the sample, shape ``(n_classes,)``. + + Returns + ------- + tuple[list[str], np.ndarray, np.ndarray] + Word strings, per-word contributions ``(n_words, n_classes)``, and the adjusted baseline + ``(n_classes,)`` with special-token attribution folded in. + """ + words: list[str] = [] + word_rows: list[np.ndarray] = [] + base = base_values.astype(float).copy() + buffer_text = "" + buffer_row: np.ndarray | None = None + + def _flush() -> None: + nonlocal buffer_text, buffer_row + if buffer_row is not None: + words.append(buffer_text.strip()) + word_rows.append(buffer_row) + buffer_text, buffer_row = "", None + + for tok, row in zip(tokens, contributions, strict=True): + if _SPECIAL_RE.match(tok.strip()): + _flush() + base = base + row + continue + buffer_text += tok + buffer_row = row.astype(float).copy() if buffer_row is None else buffer_row + row + if tok != tok.rstrip(): # trailing whitespace in the source text -> word boundary + _flush() + _flush() + + stacked = np.stack(word_rows, axis=0) if word_rows else np.zeros((0, contributions.shape[-1])) + return words, stacked, base + class NlpShapBackend(NlpBackend): """SHAP backend for text classification models (HuggingFace pipelines, etc.). @@ -65,6 +129,10 @@ def __init__( def run_explainer(self, x) -> NlpRawExplanation: """Run the SHAP text explainer and return all explanation components. + Subword tokens are merged into whole words via ``_aggregate_subwords`` before being + returned, so callers (word importance, sentence/token highlight plots) always see + word-level contributions. + Parameters ---------- x : list[str] or pd.Series @@ -77,8 +145,20 @@ def run_explainer(self, x) -> NlpRawExplanation: strings per sample. """ shap_explanation = self.explainer(x, **self.explainer_compute_args) + + contributions: list[np.ndarray] = [] + base_values: list[np.ndarray] = [] + data: list[list[str]] = [] + for tokens, values, base in zip( + shap_explanation.data, shap_explanation.values, shap_explanation.base_values, strict=True + ): + words, word_contribs, word_base = _aggregate_subwords(list(tokens), np.asarray(values), np.asarray(base)) + data.append(words) + contributions.append(word_contribs) + base_values.append(word_base) + return NlpRawExplanation( - contributions=shap_explanation.values, - base_values=shap_explanation.base_values, - data=shap_explanation.data, + contributions=contributions, + base_values=np.stack(base_values, axis=0), + data=data, ) From 8e16a9fb5ca50cb362cdb5d71bda40a60c5cb2cf Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 9 Jul 2026 17:14:05 +0200 Subject: [PATCH 061/144] test: add test to shap backend --- .../backend/test_nlp_shap_backend.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/unit_tests/backend/test_nlp_shap_backend.py diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py new file mode 100644 index 00000000..27a417bf --- /dev/null +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -0,0 +1,114 @@ +"""Unit tests for ``NlpShapBackend`` — subword aggregation and explainer wiring. + +``_aggregate_subwords`` is pure numpy (no live shap.Explainer needed). ``run_explainer`` is exercised +with a fake explainer injected via ``explainer_args={"explainer": ...}`` so the test stays offline. +""" + +import unittest + +import numpy as np + +from shapash.backend import NlpShapBackend, get_backend_cls_from_name +from shapash.backend.nlp_shap_backend import _aggregate_subwords + + +class TestNlpShapBackend(unittest.TestCase): + def test_registered_by_name(self): + self.assertIs(get_backend_cls_from_name("nlp_shap"), NlpShapBackend) + + def test_backend_name_attribute(self): + self.assertEqual(NlpShapBackend.name, "nlp_shap") + + +class TestAggregateSubwords(unittest.TestCase): + """``_aggregate_subwords`` merges SHAP's whitespace-delimited subword fragments into words.""" + + def test_merges_subwords_and_drops_specials(self): + # SHAP's Text masker: a subword glued to the previous piece carries no trailing space; + # the piece that ends the word does (see shap.maskers.Text.token_segments). + tokens = ["", "i ", "am ", "hap", "py ", ""] + contribs = np.array( + [[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float + ) + base = np.array([10.0, 20.0]) + + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + + self.assertEqual(words, ["i", "am", "happy"]) + # "happy" = "hap" + "py ": contributions summed. + np.testing.assert_allclose(word_contribs, [[2.0, 1.0], [3.0, 2.0], [9.0, 7.0]]) + # Leading/trailing blank (CLS/SEP-equivalent) attribution folded into the baseline. + np.testing.assert_allclose(new_base, [10.0 + 1.0 + 6.0, 20.0 + 0.0 + 5.0]) + + def test_merges_punctuation_glued_contraction(self): + # "don't" is tokenized as "don" + "'" + "t " — none carry inter-token whitespace. + tokens = ["", "i ", "don", "'", "t ", "feel ", "sad", ""] + contribs = np.arange(8 * 2, dtype=float).reshape(8, 2) + base = np.zeros(2) + + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + + self.assertEqual(words, ["i", "don't", "feel", "sad"]) + np.testing.assert_allclose(word_contribs[1], contribs[2] + contribs[3] + contribs[4]) + + def test_preserves_completeness(self): + tokens = ["", "great ", "mov", "ie ", ""] + contribs = np.random.default_rng(0).normal(size=(5, 3)) + base = np.array([0.5, -0.5, 1.0]) + + _, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + + # base + Σ over words must equal the original base + Σ over every token (nothing lost). + np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) + + def test_all_special_returns_empty_words(self): + tokens = ["", ""] + contribs = np.array([[1.0, 2.0], [3.0, 4.0]]) + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(2)) + self.assertEqual(words, []) + self.assertEqual(word_contribs.shape, (0, 2)) + np.testing.assert_allclose(new_base, [4.0, 6.0]) + + def test_last_token_without_trailing_space_is_flushed(self): + # The final real word before the closing special token often has no trailing space. + tokens = ["", "hello ", "world", ""] + contribs = np.array([[0.0], [1.0], [2.0], [0.0]]) + words, word_contribs, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + self.assertEqual(words, ["hello", "world"]) + np.testing.assert_allclose(word_contribs, [[1.0], [2.0]]) + + +class FakeShapExplanation: + def __init__(self, data, values, base_values): + self.data = data + self.values = values + self.base_values = base_values + + +class FakeShapExplainer: + def __call__(self, x, **kwargs): + return FakeShapExplanation( + data=[["", "up", "dating ", ""]], + values=[np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0]])], + base_values=np.array([[0.1, 0.2]]), + ) + + +class TestRunExplainer(unittest.TestCase): + def test_aggregates_subwords_end_to_end(self): + backend = NlpShapBackend( + model=lambda texts: np.zeros((len(texts), 2)), + label_names=["neg", "pos"], + explainer_args={"explainer": FakeShapExplainer}, + ) + + raw = backend.run_explainer(["updating"]) + + self.assertEqual(raw.data, [["updating"]]) + np.testing.assert_allclose(raw.contributions[0], [[5.0, 3.0]]) + # CLS ([1,0]) + SEP ([4,3]) folded into the base. + np.testing.assert_allclose(raw.base_values, [[0.1 + 1.0 + 4.0, 0.2 + 0.0 + 3.0]]) + + +if __name__ == "__main__": + unittest.main() From f2db5eb4ef2cb594669587a4767b17b80ab71da1 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 10 Jul 2026 11:43:47 +0200 Subject: [PATCH 062/144] fix: validate label_names against pipeline output in HFPipelineModel --- shapash/model/hf.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 38c25416..04844d5e 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -58,14 +58,15 @@ class HFPipelineModel(TextModel, SupportsTokenization): scores are returned. label_names : list[str] or None Class names in output-column order. When ``None``, inferred from the pipeline's first - prediction (the labels it emits). + prediction (the labels it emits). When given, validated against those labels on the first + prediction — a mismatched set raises ``ValueError`` rather than silently misaligning columns. """ def __init__(self, pipeline, label_names: list[str] | None = None) -> None: super().__init__(label_names=label_names) self.pipeline = pipeline self.tokenizer = getattr(pipeline, "tokenizer", None) - self._order: list[str] | None = list(label_names) if label_names else None + self._order: list[str] | None = None @property def shap_callable(self): @@ -78,10 +79,16 @@ def predict(self, texts: list[str]) -> np.ndarray: if self._order is None: first = raw[0] if raw and isinstance(raw[0], list) else raw labels = [d["label"] for d in first] - self._order = ( - list(self.label_names) if (self.label_names and set(self.label_names) <= set(labels)) else labels - ) - if self.label_names is None: + if self.label_names is not None: + if set(self.label_names) != set(labels): + raise ValueError( + f"label_names {self.label_names!r} do not match the labels the pipeline " + f"emits {labels!r}; pass the exact label set (any order), or leave " + "label_names=None to infer it from the pipeline." + ) + self._order = list(self.label_names) + else: + self._order = labels self.label_names = list(self._order) return _probs_from_pipeline_output(raw, self._order) From 041711c6638ccc8bd326d1117f56e72cb4e21129 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 10 Jul 2026 11:44:10 +0200 Subject: [PATCH 063/144] test: add unit test for hf model --- tests/unit_tests/model/test_hf.py | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/unit_tests/model/test_hf.py diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py new file mode 100644 index 00000000..b7f57992 --- /dev/null +++ b/tests/unit_tests/model/test_hf.py @@ -0,0 +1,93 @@ +"""Unit tests for ``HFPipelineModel``'s label-order resolution. + +Uses a fake ``pipeline`` callable (plain function, no ``transformers``/``torch``) so these stay fast +unit tests: only the label-name/column-order contract of ``predict`` is under test here. +""" + +import unittest + +from shapash.model.hf import HFPipelineModel + + +class FakePipeline: + """Mimics a HuggingFace ``text-classification`` pipeline with ``top_k=None``. + + Returns, per text, a list of ``{"label", "score"}`` dicts in a fixed (non-alphabetical, + non-id) order to make sure column alignment is name-based, not position-based. + """ + + def __init__(self, rows: list[list[dict]]) -> None: + self.rows = rows + self.tokenizer = None + + def __call__(self, texts: list[str], top_k=None): + assert len(texts) <= len(self.rows) + return self.rows[: len(texts)] + + +def _rows(*label_score_pairs_per_row): + return [[{"label": label, "score": score} for label, score in row] for row in label_score_pairs_per_row] + + +class TestHFPipelineModelLabelOrder(unittest.TestCase): + def test_infers_order_and_label_names_from_pipeline_when_none_given(self): + pipeline = FakePipeline(_rows([("negative", 0.1), ("positive", 0.9)])) + model = HFPipelineModel(pipeline) + + probs = model.predict(["great movie"]) + + self.assertEqual(model.label_names, ["negative", "positive"]) + self.assertEqual(list(probs[0]), [0.1, 0.9]) + + def test_provided_label_names_become_the_column_order(self): + # Pipeline's own row order is "negative" first; the caller asserts the opposite order. + pipeline = FakePipeline(_rows([("negative", 0.1), ("positive", 0.9)])) + model = HFPipelineModel(pipeline, label_names=["positive", "negative"]) + + probs = model.predict(["great movie"]) + + self.assertEqual(model.label_names, ["positive", "negative"]) + self.assertEqual(list(probs[0]), [0.9, 0.1]) + + def test_order_and_label_names_stay_in_sync_after_predict(self): + pipeline = FakePipeline(_rows([("negative", 0.1), ("positive", 0.9)])) + model = HFPipelineModel(pipeline, label_names=["positive", "negative"]) + + model.predict(["great movie"]) + + self.assertEqual(model._order, model.label_names) + + def test_mismatched_label_names_raise_instead_of_silently_diverging(self): + pipeline = FakePipeline(_rows([("negative", 0.1), ("positive", 0.9)])) + model = HFPipelineModel(pipeline, label_names=["negative", "possitive"]) # typo + + with self.assertRaises(ValueError): + model.predict(["great movie"]) + + def test_label_names_proper_subset_raises(self): + # Pipeline emits 3 classes; caller only declares 2 — must not silently drop the third. + pipeline = FakePipeline(_rows([("negative", 0.1), ("neutral", 0.2), ("positive", 0.7)])) + model = HFPipelineModel(pipeline, label_names=["negative", "positive"]) + + with self.assertRaises(ValueError): + model.predict(["great movie"]) + + def test_label_names_valid_regardless_of_first_row_score_sort_order(self): + # Different rows may come back sorted by score (per-row), not by a stable id order; the + # caller's declared order must still win via name-based lookup. + pipeline = FakePipeline( + _rows( + [("positive", 0.9), ("negative", 0.1)], + [("negative", 0.8), ("positive", 0.2)], + ) + ) + model = HFPipelineModel(pipeline, label_names=["negative", "positive"]) + + probs = model.predict(["good", "bad"]) + + self.assertEqual(list(probs[0]), [0.1, 0.9]) + self.assertEqual(list(probs[1]), [0.8, 0.2]) + + +if __name__ == "__main__": + unittest.main() From ab3f45a5ac09e76aecff50577f1128100b2872ed Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 15 Jul 2026 11:40:49 +0200 Subject: [PATCH 064/144] refactor(backend): extract modality-agnostic Backend ABC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NlpBackend inherited from BaseBackend but reused none of its behavior — it skipped __init__, fully overrode get_local_contributions, and carried cargo-culted tabular attrs (column_aggregation, support_groups, supported_cases) that no reader on the text path ever consumed. Introduce a thin Backend(ABC) — run_explainer + get_local_contributions, typed Any pending tabular/text convergence — and reparent both BaseBackend (tabular, body unchanged) and NlpBackend under it. Move `name` up to Backend and repoint get_backend_cls_from_name at Backend so the concrete NLP backends stay name-discoverable. --- shapash/backend/__init__.py | 3 +- shapash/backend/backend.py | 83 +++++++++++++++++++++++++++++++++ shapash/backend/base_backend.py | 14 +++--- shapash/backend/nlp_backend.py | 22 ++------- 4 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 shapash/backend/backend.py diff --git a/shapash/backend/__init__.py b/shapash/backend/__init__.py index e94152e0..64f54224 100644 --- a/shapash/backend/__init__.py +++ b/shapash/backend/__init__.py @@ -1,6 +1,7 @@ import inspect import sys +from .backend import Backend from .base_backend import BaseBackend from .lime_backend import LimeBackend from .nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation @@ -19,7 +20,7 @@ def get_backend_cls_from_name(name): for _, cls in inspect.getmembers(sys.modules[__name__]) if ( inspect.isclass(cls) - and issubclass(cls, BaseBackend) + and issubclass(cls, Backend) and cls.name.lower() == name.lower() and cls.name.lower() != "base" ) diff --git a/shapash/backend/backend.py b/shapash/backend/backend.py new file mode 100644 index 00000000..849870b6 --- /dev/null +++ b/shapash/backend/backend.py @@ -0,0 +1,83 @@ +"""Modality-agnostic backend contract. + +``Backend`` is the abstract base shared by every explainability backend, tabular +or text. It declares the two-method seam — ``run_explainer`` then +``get_local_contributions`` — without committing to concrete return types, so a +single interface spans both modalities. + +Return-type contract (migration target) +---------------------------------------- +Text backends already return typed structures (``NlpRawExplanation`` from +``run_explainer``, ``NlpContributions`` from ``get_local_contributions``). +Tabular backends still return ``dict`` / ``pd.DataFrame`` pending migration to +the same typed contract. ``Backend`` therefore types both methods loosely +(``Any``); tighten these annotations once the tabular path has been migrated to +the typed structures. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + + +class Backend(ABC): + """Abstract base class shared by all explainability backends. + + Concrete subclasses implement two methods: + + * ``run_explainer`` — run the underlying explainer and return its raw, + backend-specific output. + * ``get_local_contributions`` — convert that raw output into the + caller-facing local-contribution structure. + + The concrete input and return types depend on the modality (tabular vs + text); see the module docstring for the intended convergence. + """ + + # ``name`` identifies the backend for name-based construction via + # ``get_backend_cls_from_name``. The ``"base"`` sentinel marks abstract + # bases (this class and per-modality ABCs), which are skipped by the + # registry; concrete backends override it with a unique name. + name = "base" + + @abstractmethod + def run_explainer(self, x: Any) -> Any: + """Run the explainer and return its raw, backend-specific output. + + Parameters + ---------- + x : Any + The observations to explain (a ``pd.DataFrame`` for tabular + backends, a sequence of strings for text backends). + + Returns + ------- + Any + Raw explainer output consumed by ``get_local_contributions``. + """ + raise NotImplementedError( + f"`{self.__class__.__name__}` is a subclass of Backend and must implement `run_explainer`" + ) + + @abstractmethod + def get_local_contributions(self, x: Any, explain_data: Any, subset: list[int] | None = None) -> Any: + """Convert raw explainer output into caller-facing local contributions. + + Parameters + ---------- + x : Any + The same observations passed to ``run_explainer``. + explain_data : Any + The raw output returned by ``run_explainer``. + subset : list[int], optional + Positional indices selecting a subset of samples. + + Returns + ------- + Any + The caller-facing local-contribution structure. + """ + raise NotImplementedError( + f"`{self.__class__.__name__}` is a subclass of Backend and must implement `get_local_contributions`" + ) diff --git a/shapash/backend/base_backend.py b/shapash/backend/base_backend.py index 82b1363c..09bd7e51 100644 --- a/shapash/backend/base_backend.py +++ b/shapash/backend/base_backend.py @@ -1,17 +1,18 @@ -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import Any import numpy as np import pandas as pd +from shapash.backend.backend import Backend from shapash.utils.check import check_contribution_object, check_model from shapash.utils.transform import adapt_contributions, get_preprocessing_mapping from shapash.utils.utils import choose_state -class BaseBackend(ABC): - """``BaseBackend`` is the base class for all backends. - All explainability implementations should extend this abstract class +class BaseBackend(Backend): + """``BaseBackend`` is the base class for all tabular backends. + All tabular explainability implementations should extend this abstract class and implement the methods marked as abstract. """ @@ -22,9 +23,8 @@ class BaseBackend(ABC): # It allows to compute (column-wise) aggregation of local contributions. column_aggregation = "sum" - # `name` defines the string name of the backend allowing to identify and - # construct the backend from it. - name = "base" + # `name` is inherited from `Backend` (default sentinel "base"); concrete + # tabular backends override it with a unique name. support_groups = True supported_cases = ["classification", "regression"] diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index fa3244b2..40d90888 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -23,7 +23,7 @@ import numpy as np import pandas as pd -from shapash.backend.base_backend import BaseBackend +from shapash.backend.backend import Backend @dataclass @@ -152,23 +152,20 @@ def word_importance( return importance.head(n_top) -class NlpBackend(BaseBackend): +class NlpBackend(Backend): """Abstract base class for NLP explainability backends. Owns the ``__init__`` skeleton shared by all text backends and the ``get_local_contributions`` implementation. Concrete subclasses must - implement ``run_explainer`` and return the standard dict described in the - module docstring. - - Skips ``BaseBackend.__init__`` because ``check_model`` cannot handle text - pipelines or raw callables. + implement ``run_explainer`` and return an ``NlpRawExplanation``. Parameters ---------- model : callable Text model or pipeline accepted by the concrete backend. preprocessing : None - Unused; accepted for interface compatibility with ``BaseBackend``. + Unused; accepted only for signature compatibility with the concrete + subclasses' ``super().__init__`` calls. label_names : list[str] or None Class names in the same order as the model output columns. explainer_args : dict, optional @@ -177,10 +174,6 @@ class NlpBackend(BaseBackend): Keyword arguments forwarded to the explainer call / ``explain_instance``. """ - column_aggregation = "sum" - support_groups = False - supported_cases = ["classification"] - def __init__( self, model, @@ -189,12 +182,7 @@ def __init__( explainer_args: dict | None = None, explainer_compute_args: dict | None = None, ) -> None: - # Do NOT call super().__init__() — check_model fails for text pipelines. self.model = model - self.preprocessing = None - self.explain_data = None - self.state = None - self._case = "classification" self._classes = label_names or [] self.explainer_args = explainer_args or {} self.explainer_compute_args = explainer_compute_args or {} From b11d697a609a042dd4a858f55d49b8c78bc666a2 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 15 Jul 2026 14:28:55 +0200 Subject: [PATCH 065/144] refactor(explainer): restructure results with dataclasses --- shapash/explainer/nlp_explainer.py | 108 ++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index f640bd2d..aaf80c22 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -14,6 +14,7 @@ import hashlib import pickle +from dataclasses import dataclass from pathlib import Path import pandas as pd @@ -51,6 +52,59 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.pkl" +@dataclass +class InferenceResults: + """Model + explainer output for a batch — a pure function of the input texts. + + This is exactly the payload the disk cache persists: recomputing it is the + expensive step ``compile`` memoizes. It deliberately excludes anything that + is *not* a function of the input-text hash — ground truth, class names, the + 2-D projection — so a hash-keyed cache file can never carry a stale ``y_true``. + + Attributes + ---------- + contributions : NlpContributions + Token-level contributions for every sample in the batch. + y_pred : pd.Series + Argmax label per sample. + y_prob : pd.DataFrame or None + Per-class probabilities, one column per class (or a single confidence + column), aligned to the sample index. + """ + + contributions: NlpContributions + y_pred: pd.Series + y_prob: pd.DataFrame | None = None + + +@dataclass +class TextResults: + """Portable results bundle for offline webapp serving. + + Wraps the computed :class:`InferenceResults` with the dataset context needed + to render without a live model — the source texts, optional ground truth, and + class names. This is the object :meth:`NlpExplainer.save_snapshot` serializes; + the scatter projection travels in the snapshot envelope alongside it, not here, + because it is a webapp view artifact rather than an explanation result. + + Attributes + ---------- + texts : pd.Series + The source text samples. + computed : InferenceResults + Contributions and predictions — the memoizable model/explainer output. + y_true : pd.Series or None + Ground-truth labels, when supplied to ``compile``. + label_names : list[str] or None + Human-readable class names in model-output order. + """ + + texts: pd.Series + computed: InferenceResults + y_true: pd.Series | None = None + label_names: list[str] | None = None + + class NlpExplainer: """Minimal explainer for text classification — token-level contributions. @@ -179,22 +233,24 @@ def compile( if cache_path is not None and cache_path.exists(): with cache_path.open("rb") as f: - cached = pickle.load(f) # noqa: S301 - self.contributions = cached["contributions"] - self.y_pred = cached["y_pred"] - self.y_prob = cached.get("y_prob") + computed: InferenceResults = pickle.load(f) # noqa: S301 else: explain_data = self.backend.run_explainer(text_list) - self.contributions = self.backend.get_local_contributions(text_list, explain_data) + contributions = self.backend.get_local_contributions(text_list, explain_data) pred_df = self._predict(text_list) - self.y_pred = pred_df["prediction"] - self.y_prob = pred_df.drop(columns=["prediction"]) + computed = InferenceResults( + contributions=contributions, + y_pred=pred_df["prediction"], + y_prob=pred_df.drop(columns=["prediction"]), + ) if cache_path is not None: cache_path.parent.mkdir(parents=True, exist_ok=True) with cache_path.open("wb") as f: - pickle.dump( - {"contributions": self.contributions, "y_pred": self.y_pred, "y_prob": self.y_prob}, f - ) + pickle.dump(computed, f) + + self.contributions = computed.contributions + self.y_pred = computed.y_pred + self.y_prob = computed.y_prob self.contributions.label_names = self.label_names self.contributions.index = self.texts.index @@ -258,17 +314,16 @@ def save_snapshot(self, path: str | Path, scatter_xy=None) -> None: """ if self.contributions is None: raise RuntimeError("Call compile() before save_snapshot().") - state = { - "texts": self.texts, - "contributions": self.contributions, - "y_pred": self.y_pred, - "y_prob": self.y_prob, - "y_true": self.y_true, - "label_names": self.label_names, - "scatter_xy": scatter_xy, - } + results = TextResults( + texts=self.texts, + computed=InferenceResults(contributions=self.contributions, y_pred=self.y_pred, y_prob=self.y_prob), + y_true=self.y_true, + label_names=self.label_names, + ) + # Snapshot envelope: the portable results bundle plus the scatter projection, + # which is a webapp view artifact and so rides alongside TextResults, not within it. with Path(path).open("wb") as f: - pickle.dump(state, f) + pickle.dump({"results": results, "scatter_xy": scatter_xy}, f) @classmethod def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: @@ -283,18 +338,19 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: """ with Path(path).open("rb") as f: state = pickle.load(f) # noqa: S301 + results: TextResults = state["results"] xpl = cls.__new__(cls) xpl.model = None xpl.backend = None xpl._text_model = None xpl.cf_generator = None xpl.cf_generators = {} - xpl.label_names = state.get("label_names") - xpl.texts = state["texts"] - xpl.contributions = state["contributions"] - xpl.y_pred = state["y_pred"] - xpl.y_prob = state.get("y_prob") - xpl.y_true = state.get("y_true") + xpl.label_names = results.label_names + xpl.texts = results.texts + xpl.contributions = results.computed.contributions + xpl.y_pred = results.computed.y_pred + xpl.y_prob = results.computed.y_prob + xpl.y_true = results.y_true xpl._data_hash = None return xpl, state.get("scatter_xy") From 6e38bd47cc9dedef7018e2a33911b97776aef6c9 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 15 Jul 2026 14:30:36 +0200 Subject: [PATCH 066/144] chore: clean artifacts --- .../9b53acb06c765f0a3a16e3605e17c8ff.pkl | Bin 1370959 -> 0 bytes .../9b53acb06c765f0a3a16e3605e17c8ff.proj.npy | Bin 8128 -> 0 bytes .../d7a88ad33d547dd8231f8b1dd0d037aa.pkl | Bin 673247 -> 0 bytes .../d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy | Bin 4128 -> 0 bytes .../717d02a26c2b6b25b7d9583195fd207a.pkl | Bin 772326 -> 0 bytes .../717d02a26c2b6b25b7d9583195fd207a.proj.npy | Bin 928 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl delete mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.proj.npy delete mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.pkl delete mode 100644 demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy delete mode 100644 demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl delete mode 100644 demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.pkl deleted file mode 100644 index bf5e32d8b45b2f488fa51934ad0d9651e560556e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1370959 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 diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.proj.npy b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/9b53acb06c765f0a3a16e3605e17c8ff.proj.npy deleted file mode 100644 index 5d90859493235991790dbb254cce391b5324fc6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8128 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 L$U(y*KjHrXiR*wZ diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.pkl b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.pkl deleted file mode 100644 index f2f35796a6f3e5a1f40baddb7eecd16ea09bb178..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 673247 zcmbTf36!kqSsvC?dv)It*epTBfr&$m@BtwVHW-LCM%W>U$ixURSVU9ZUEN*xc2_l3 zUAO0sFlP)%NH7?It^nJh2+R_*D+CB2!zjVTEXE=S5>8};5P}fm1lc%j_ObIU@B97V z|5x{bqoXr*tNLHQ<z1h5`JNuUakBTw9{=|@9=_n2M~_y^tM%e=d$m|CHxHlr4>xDC z%h~4ag@?1Fd-LV-3zz4YuZ({`eCEAhc7FMi)(vlX_+fwZp&oR#!u7A*;9<+t&BM=o z_yYX@nZ1Y4^cNS8J$z<zGM}I0FZkzpJ-a%aul29h)zy57fAu#j{#Ta{ma8j#8Q!0- zAK-I;c5%4C=(Zbt8LbzmXZ&k_xw?LMwm-*9&i3QR-u4ntKb~FrJ^c&bK0G{M@vW1? z`O$2<nd6_m1%Ky&@HyI?Ev~K}G|$psN6R^0&&w`m_ju31Bc5*Nmoto?KMhaTD^9I{ z!EfE?4~|!ySWi>Qhh5K>{2mYMUvbhokm314Z_@jFS~Kj!>|%b*i>?<}XZY-67@eUG z&*vw4NnWC9PYzex&)?3qRI3xdbFBvrW=lSA{9t~5zPiT0dRJRK_I!b*>uH6z%VW*l z%Qu*<JKHzioO2J(wYV3nH79?ty1ZIkEH+n*BYYcPoy|D+;q{p|S2G&V*6YRn`Eh6T zgX<aJT9HR@hVSEZY|6P-`TlIN^RU73LaTChw&30U)pFi>ZvSYu%+<f1o!{ed7E3L- zUNGF8tuFE7bhdneVQLnm{D%hIKj!WvF$T3fx?LWvE>2eKtJ&ds_np|ub1wL3wmdcQ z!~*nZ+I3Jpp9rqPM_;?5vFu;X&*ztCm<0cPG&?`<OlC5F^77ovFt|SB!kb8s&bQ(y z6Ox|w=z4w*1^_RgEjYg<9Q|={xZ=%thNkAF?|XSKun@X__JB(|U0htuuC`j2pqbtl zE855O@!yNZ6`pe?lA0XPH<t^s#K2QLm_K=hA1>yic5gq{dwZ@;JDc6t#vaZ^9o5zk zu&{Xd<aB-&JUpEp9nCNOp4`89$>d_Txtgy<J(ug%>3Sw+te5&=y9CLtH;XGy%_}~+ zzgV9xmWx?ujg$R?DPxfFuHgoJh({b8uICV5=kra6X#1jKE%F$bn1e?1nd!jK9Bk&A z_w|{^h@Cm702)K!UTP%<%lVU6oniIv&N1>0=|}6B(xP{|#_!t;vMnb!x>~dtd~m6) zgKQ!jY_MCsH5q*-nRk7*IHEYwBd1usLy)u9NQ*dtI>2AS#~RmYb{fRDxZ=ErtEIU= zc2LtEUeD*`vLF_V^3e=ph>!C63{Do!7v0qdZ{nV{gge+6mrn1`jzqzHm<b|wVYJy^ zT<pldmY_{Gg^-3U+MdtW8tG(qy10VW?n<9r%{zzl88qO@_FR|m&o5TwAuZPA9#oAr z3I!V{Fv7w}lZ>ucSYXai+zf#yjo_WSx13YJ4)1QgeUxhxNXBElhQD81>P*g7=f|nB zBmpd<Q+>@2d85~6u-Zy*-o@u~-c?$I%Nh4@e@3Os@$){d_~3Xx2c<ytX#Z+KSh1fc zF*=-kG?T^hJ}8+y8=E>go1b5j_&Y?JQ_SVu2OY1${#wjCmB7TKQXu7?o~~D03M#*R zd^um?4ZJSaT5AbeT%<X4sr12euAwErE{=KE=;Fa<etyy&-^s#S&wu6HB=lkogVix7 zIlWv#QpB=pKdZGl*laH^A@(6paA|MVeZ{UGGszFn<{(9I`Qe)O5nnA)Vg49yEMfci zX1J!mT)+a876xDJEOOAcg-p?p;Baz~mHF^$bv)~mMea(GaJGw*iPtNwHxwPI6%=hT z-9O|+V|A}E349wqF#U}-v&Fe6sbzEYE+wlr3tS0ffZ{j^Xa9JyIfC5?vfSAkn_)>p zy~88Cb@deP)kH@pY4BZhJOii(eq79?Lv29KXA)+k71)x)>~WcUTl{APgKJ6lNi$B* z+`rr&o-d%G$St~faPG%iePIpOQdJ|eKId8FSTNvFqq=P>VaN?3ka=^g!RT_k25IAs z{!#Kdr)~RpxVBi1JBIg6wuLp;C-b9f0o&$skW$ceMY<uI&R0@esdk28llD(#gwC!d zJ3{h@)VR=Z!()g=%)_+cUD-g|b0c<Mv%g$zu4d<_3kj`Ibe5Xp<S|TIE2^+N!h{2H zo^XRxJcRXmtbLC4h^Y<Fw@1`8dfwoyri9Si8si}DwF((N014m5Qpp*;#eF=M!x*0y z&%`uFO5pIqO!Arzzz_Iwa&-nFc1ndhg|MU9Q|<eBei7V~(})p;fb%Z)VFcjaYjV&L zj1$N|i<+c`({nF{UkS6E!_-8FRCf3h`y2~eS<suy*-_V49fMr%&!zGu4hE}ha?J=b zFm$~ApR(pKw($4CiFDdXtI6-yRmCC*88rc+&o+|JxuK)`v(qid{0L>-5;9@{vxjyG z*UDq}zWEhpHgn36?h8^oRI`MW*gT1*i~N?O*=1O<P|mgu!-xqkHC2bSlabfA5xRkD zw@IGlVMot&J3giikvy+ei7mD95lCRW-VwyJbGf-g*!yItARj+)1b<}C36!b9AP<+o z_Ih8w&InGH?ygvmznxxyXkZ0CWv%ErG!CSflZMMe76ge@=FJ!i>Ri6%c(K`RJ%lh# zLo8ZE_z6J|W51D;u;{$u8n{@nnma=BCTnecx?P-080SihwdDhc|1~;Yg^46~^mYyD zgJB4vq>ffA8K7pb{@LQ9^X?%CK-bGAYI&!rBeAHnwe7R8Jr8Dwo7MRiTP9{%Qd*8+ z;%{J)6#T(Pifrd<Tc)sm*Kh%(s`O5P4OS@ol)iR6J5Rx0S|OXa4?U^G8V05a??rm( zA_vF|QSbC*c7L_DJ2rt)d$y8`l(NTSOykWtmqn+@9<NjIjV|Fi?M_l&ao!5j1vii8 zM>b>j$uk^>#Gjpra5jg>tN8{t+#{9#(kRAfQ1G_8*K<ox8DsYsG{)@S8cE^s_OIv1 z%lQUI4)=W_V1q^p#0YmbDWU>n2FdQ>vmLEh*EVXcbAt)PurMhNW*bqG&4I}YFc3SM z!@xWC0j+=o;AP{b6B<EwbE?fBuD7yC)2FSheA&B}br2yf4oUqpOy?MO)W)kY072tz zv60oAJdf!bBMQgVc*79R2p44vjj)6%K@=<qXO}m|6#OANB-Nx9KukyTB*9CNGN%g; zIbRsxNaY>AU!y<E_^A;5YE8J2*IfrT2{img+25lLEN;<~cLJKO)<V$m54tKH>=WA1 zdOH--w0LwKb}N)@$$SJ~40F_2duMd9=cKXW+0~WMl*M|nIlJW%g^vg?N_g!b&uOt) zkz1{pMX({<jdX7fcmfEx@85@T$4THA9K^(JuM1l2-G?si(Z?8W4U$rWa1UD&ZUEqd z<GN1^^z;-jUx-b|#|!9TizsdU*;WA0w!5;qQbS5EJZTER3Y_O#E2%tPEP+b}i%B<X zHTxGcyma^!2p%^BYO|?3yj(9<SZA;)ou9tPRCH2M#pnq31V;z@i8V6SH`2pq)C7_! zpyI&q3~Pn*vhf7NT(;CchBV`VNMif&PmbpBs<{+tv^2`GJCG^u)y>s|V$j2tlNS<5 zhY5Ccx;Dr)f{nK8{ERjLeq>e!eV+$?UtLShkgYg2kAA#4I$N!1I8@9(g@Bl^+hAlY zMZ<b!%P!$6&8^<LF3kjzHDHJa?+Kr9b^@Rc?;V*!I|Lafcwu?i6)9~*7Net;9kj{O z`D}Y^&){%|O&>zuAL6-XapS_l>|8u_xB{(`T!K+VNW=4$m?srp+)Rs4JaY&)5Vycm zm;ANSwnMzv^K-o1#A`1FSmBPg%iFAJq^}Ne8?4)~2e2ZRnqvytQ*M^7>Pd&qpPa9c zus`_Ky87T4a_9*01L5Q)m_a(2QR4*Wa}j=@xF_w_bl8UQ)c7946`O@Epjd;!l{hmT zH6nk5*^#)Ox^r{{v2fZ_H~2NDcmTd%oG)TJcjgz^q4jKIBR)*x6ObR|AMUaN5oRJ( z&jqeem7wJu%Ngf5VRaer4Rky`ihxuBoN(JJjUBuiED@KD=nGf8P?kHU-WpJthv7cQ z6#}oqw6oU|;4=2DL(#b=`?T}uS4$?e04dvg$FQA4WVeuz;qk(um)bWd+Z|JG2-kiI zaDOa>t^C31YIPe}>8Zx5dauE;5Hwuu7)C^xNHY?fG*INA>Sn|k`0Hc=0cd$Vq~(U6 zn%Eq$b2)L2Sk=TfJc4UiX@_uNO{^gW#i?M9%S-X(1!jY%j|5NQAD%+!=JD+O0>%e^ zhQ3s6N$cI(SkAT}fB*wIR)%gw2D)=`asoV6(zLvWGJzA->!!=q`T}dL0p;o$kCtOK zy<Rc$1qx}XVMme@-1j4Z1aZhF))B%pM;ucOdb&Wc>qfviUBJW>YNATAo4_6j3n5jx z)^mEYULbg6V|j-8!n82RYg-4irG*HRST$2<Ep^L83AwFDSNo1=!0|SX7HEYz!@$kd z^t>v%hTRK+G}ss}N?b#((il-BD~|zOA3SDwVLhqmB%H?`ZHBzF>K!+u6;2ynfO2<W z#a%ddjDQH@ycoe`wtO;d1SlFg;`S#J{F3Pl;BjqZGn@@;2|$B#4IDeOLJEUuNS@_F z#nS>BiRFMPac(<HH}%g~T~COfpB~URAg)af_e85T0gN5g_gvQo4F;fl<NkYv3(`Nm zg!0>*SsTi!(p^y8fdv~QWEwz22)3_`6b=<3YId;oQWIDKtS8I)`o0ZST^7iJXKSI~ zKYg&{yN9r7QE+#F5!b*1aG&?#KyFJU$-6N|7Jwxk0DXiOHydk&O8t0Htq>(_!wX<l zmh?e0p{fJ6M8rc8S}kF30}Q)&20W&}rFmn3r!Seik1=BY3x|eMT6@}Bs2jYFM-K4} z7{DN{BgX#?6Al=T=qz~%b|)QxoanQJz7^Ha4SCo34=pD_Udw0(3(Lx+5}$po2=tPq zo_PBJs6GG=Nuh^#-}T7eUI9c*U}M-LUNozz6mTg!&1g_f4yMVR=7<ApO+g~Rx3c^# z^J>^KNIp%C=@G<q;-^Q7zZlg$JfAJGgq>B(+eXM;F%Os++wodcdM7@T<X9PqBLa(( z1G`ww@N}`Z0WV2TdnSdB<8)^V4wdYaM%|1lc>GvXvdA>{yP%_V7xBzo=ODS_`{GY9 zUzf{U6i3AbX-x;tY+@f<|5U2qgD?a?;>*Q%B=Hh|v?G5U@Ld5=PG{@m`7&%LXge;& z1RibtME}s(p4Y|V7E;gHCmaVkLa}%rhOfzpO64QjRU8@Fx@GUhdnv)IdyAWd6>+YK zfC38%8G&ObJKSciC52RX<PfLADryFS(FGF(KU?EkEbPO^7YHP4I+%j4lXx_ekcuqt zZ?+H@6X8Z;S+ibR-C?q~Z0ymA60$lg1FRUZ5w@i)L1Mo$PB@qf??FVcG7{oS?AP{$ z_=zT;Yz8P$;w2c0308bq+fif0@Dzb_^P_Cbaf#lxWELVysiqFOBCt5Z{S4h%%+Qb$ zqYWG#URgkhntldA`|!5}TrpT|P-kg|*NEkE$Wqk%4*&$r*Vg*u3s{l%Xc}s+=&7R& zr^j>HCG@houLyl-+C8Y;vCf4BL)b^G6U1Y3LblZkw8Tj!&&Y$4A9y@-kf1RXDATJ! zKH5p2z#yQ}MvK7PgX=1B(RVslfR@&GfMx<usJ-_wYzp`lcwhWF#A<Q-V=xhUG%r_) zK&E0lcAYhJUJ@W9YRRiJP#dL?NiINuhIAs_dF*gki#JAqh7}@hw24tVTl(o*4Nq3* z_jY8UJt|^igkd>=jyXI^5M>zQ>i$O2{wogqOsoz6oEzbh$Nu*gbbAQ#VdR8N!a<KQ zjwKs*sHbdtY68S-wwKzZdLg}0^fCBefNbz~@sA?vB=ExC8D;k5U?rCBUmj+%!ti9a zJ@1+#`R2}MO)wBBAjygd5P0civ(z4GhU+W@M_P3*tAk-Bdxm;3L}=}7bHc|9qG)E) z(H0tssy8QBDVtDoV=nnFvyW!S$adp{x{0ivsSOZ;#APPO^A(`{zNMEvhtMCxO9+(b zT&#Eww1XuB2fgFRr|06;>7Y+3X`85-Or})kCrIS$SZWpXH;&Rc9Hr*s7HLi(c#$Jq zi~Ws*@tSaI9E1?KEQQK2;>ap=hMIx*Oh4Q#<YOTC#5|zRn~O@q!wmL?AKy5ki9#O0 zjlBdjIuzehcSEEKQ4Y2wj$xNZGdDS|E$Aay1+f@1$}}iO^2=dI(L`5%PQlr0fO^9G zofS5jFA>p~gifO{cWr`HA%Hbpw21jM(%jrz!nlDXd3s1gaGa;JYRQSk65-@0%NgH6 zLDk^OIg(1UYsdQ$*@W}yNx<5`ySh5dmlc=`@eC*=k_S@UuER7n1;JO6h&+TjhgmLQ z41#eadm@{;xp{xdJk;=etqyG}?06UP)Efpc;zsH5*NA_2$DLMmh}@*qB(h+e%*Lrv zm>{8eK-+q=&76$YRTwG1LLAG%o~~d)ZvZ)B$&9!*Dk=E<a0Rnj_6dF{&{pHsiv0FL ziu1)O{A@fZm01{mg&2~OTX{dl@d%eyJ{G6}(#(8LlD&nzZm`(!Zf!G|n^Tf}MurIz zA2~1!@)5%YoN+NT?;soc2(H+X6SRtDtGP{>ta@&o%kti=I7ED<VrnGp%Bo1g>CMO_ zm-IM+fbq?f4RY6!D$0Mx<9D{J(8hYkkoN!?9w1pvP;6>Oo@^kpmPk$StWbs9Od<;& zugdVOwMa3G0#L*d6}KNDn_J73goHb%1ZzV*0(dw>S%dB5-Z6uFXAiK}$7{j`{y9k? zl*nMS<#r4Ykwe;5M9JwTYCE}FVS{AQOqg2!WG(NV4Bk80Npy)ihhpl;3CwgCgO#zM z;aQ$SP|GSrw$vdYg|6gJvV~Yj0PuXCNfn|iQ7LS4B<*!p-qVR35-)T~Fh!~<^Bb@d zVb7*P8c_J)1ZieTwRFUNah0K3Xu|eWkry^-iiZZCm9t=fEg+vpgx9Zu+A~HuhJSpB zczb6)h_oQ!5Gg)pPB?m3QKDihl!O(K)1U`6@qnCA?_(3(chm_2Dg5*lYIq=kTqBuh z5mKe-=nQP4^w{0%ngJk9QtTBnyfdZ0B#CsMfw+Q~DDoC2*R#w3iLFTolvy;Ef>;kK zm{wH1xOMFi;Anff$t+KdKhiL(a*9G@O|mK{P8^T_DHfAeSjz>h)^~vL9y0m*3Vd|4 z@~6rhwGPEf8}%R=g>3*%!z%+o9wGfw9<6<%2n@v~p${%s=MXu($3OHHSDipUP=Mlh zB=^I;bb5{kUZ+w=5W*9xzXW_9rhe}pv-QqFINe1^4ya3)%ySm@Q#g_{EGOxIa<8Pa zfcRGd`9&TnY?Djr=P0*og8{{I5pa3ze2Odwv07jx3zai?Ps!d5qmR0)GwvNJXR`tZ z7~)koU%Q6a;!Se&!O+?r+^}9fi8z`P%f_(rkST^iAX7#0phAmY;}m+@_|fNYou;+# zBA0d$zkP#@<VUfRcp?1<N#TS+EJOSAC-LF~B8<lAxE9K~!as}%v$P0byto6x`JKt8 z3F<X%2s5QZBmBZB8f_yx$uyeq9<L`=;?s6|D_KAE(R9ANy9#_b$hYF7Op~X69i1TI zh~xo7iy5*2XB6!W`b@EKg~R}*n1(>lQJZrnv-2z-Ojo|Qlb*Ok8T%;snxooFwnod1 z7$YaFPC1=0gDObGDlR>~s6foO#&GvB38axNk7y#<+#4gciueI$h!0L!RM1JV7}8AS zVwmO0;rBD1#k}0^o<{u%W0aaj%DEmRs-wKMHMb#)DDVoR#V2r=um<>+yV(ce+j+@u zylo^)yBTK5&Toguf^M#%|M`YULoc=<a7=O(m!beD<_sv7CSw8=jYXrDK<*MDlOn8> zN38q{V@!_W9#-Kv!hQ740QV^mI_yOt<rRzU001oXbH;@{-U&6Eu}zK9>awMq4jhQT ziaZ4Sj1jC4w(zTK*K&Ndx`v0uwRC~QV6!@*2EzuHiF#Ei>~P(9IX|TljXT=PCaYnC zLs|)&enN<-(6~7mXyP-EnMMUM>du=7_UJ732Zwh18UbQ%224?>A*2s9#jdBOw_Fv5 z(z|9dJkoShVxe>oAw6X_KdSm%FhFEHCQT@NuR;6tbI>Lbp|vxIVxi$Lr=M-&Px!w< zQwnO7YvcOPLs)6IDnl>X2zaEk@{JT=*vv!cCib1%7Y4ka95@EEqr1u~hF>7PN=6Q# zJ+lBXCdFC0C^Um?QXK%rPfr(8!$6e+L$n?U40K_A#mHHeW-7Kph#dh-j9lMt2vGK~ zud8D0Tq~DXQ}Wx_SH8q8)nFC_tve6yZ_nYvwlU}NCUcEj*M=w@Zr4gtjVT>%ITPqK z^N!(0-Iv+EnavIL(k<v3tHzGiiAM2?ND7ufh>LPj!dv3L#Y+RqXxxe@4S3M_VMCV1 zDoEtvMysdg+Qs%B@VTfeK1b?WhXaAEg5vC+8L_S?<&JDxz$kW{Ajw6Rh*l`#)_GKF zh-sEhav8&MX=ah?)hp_X>~OUxJ;1zC@fuR=;Dk;9li!eMXY;$tC7vL_on?>?zgoSG z=}`+g3k~HGv^#a3b9eZt1$NwBIwc}xK_i2i3Lqjae7r$wlJW&dCnz8$kvCXTBm)OE zc{r%BL;gzVg~_6Abznl-N6mFnJ6U<{h$#zinZ7BZV3Q%DdhtpJpwnCv|3u|`taAm_ zp)&vn-^nPQ(B}^okjVz#`u-(oyB)MQOToG0lUlngcM>p3mSv-=(LQaB^p0WFBP|m5 zPOf1YwG~gw+)5=#Fzv$&XCjl+vXJ1ycgMvXf_zV!{lT({n#3g`H)tQkGTEgKCQUS_ zd8R}d_G_oc-pYDfrEIn&%>Ep*2*rnTs^aCkz!qZ`9(dy>oD^&kjfxVY7(YO}Hcsi4 zDW=g3!P2}a)ernWL`b@3-1r;@k4Z&~j;nyn@wW6Mn|PxJHd=z7MOwy35?EykyAqqq z6GCI9bv&>MW2+g#)2H;Bdd?{y^3}!GgSqx(lv|lwODlxj(x^h5+@GDpd=|-GBQP(D z3te8eUBODkQ_Y_lKMQFxMI!MjiVYD&D>l%Mf!4$_z&A+Thb(8_aUa5)|8BH#2nPse z7vFGZyJ6q3jW|F`7TYSL#qv_DnIU5nBIKo=EeEAB-)-!kjM*%*M)wXG`l!EP(uDU@ zRNsjodOdp!3?CtCx%`(1+fym;Sb;H4@1O+(LaCrBWZE>VW%>?KFt=0`*;Hw;hU?xf zE^))kz@H<@uVdj=U;}D79FXoUy5Lb2y@`<nFIu6#P79V6d|w5?eaju=b|I42D}g|O zDv|szRZ%4iHGDR>uVu5Vm7XdPH5Thh{Dkoudgo}dK61~QL*yq(!q{AZA0=DKybkli z+DTjnSIv~c>(eR$a7f=`bpF7YOu){&3z3@;RmKI$0KpmLsk06vkFrc;v0Sb=6_SvK z*xl(^Te=_$Flo*=RF4EjM0J%{V|0ufRtYKKN-Ec{b+pYKL^ho;d8joXEH)C}98j)A z<oQmk%wlHFQ4+w#YzAybP9@h6TAlKX1U|GKISS90C3%3Un@5PcLoru|P=~(nY_MV| zSc_uFKT`d?`6l493`fVz!jZtD*@!l^%HYRrnE#rJOIG%71`wL5+()=YODu{~I+>`+ znho_jXuxxiKtF%&FkAvSv2+;?9o};pO25T7o8^q}>CF+U9xa^47g0D&K)apsRahcc zCXGbh7Z4TVE9GZNnhhNyDsj#QI1Q>HLjRsi+%<5y3m6=kOVCR8_Y=Ks7hn#bf)wMt z7Z-ro#A}*U_%aSA$LI&h=;r)%mBjFfX;C|tbFy8^;zzErO_2XwV`*<-T;li~Zv;?| zY%qqSRcs<tzxk%WQHdghQw?e6%tVV-+W9j*>T`5~Q6mN$jm=#qyI|rbXTDphp%tHy zqMaKaV;&QyGw2U5l$BPHTbN&!(2LZZ3us#oVT1j_$K`?@9eM(r>^(7TA)0P#gd&WW zeIt_cA_No$(av~S#UrJE9ZA!>uS6^kT~;K}S{dKtYfy-bTIa_*A(CM-X4HMc*x??A zu#V;HNP(M>u$DQgwXc%iI~Ck%5JZG4GSZe0?=E^(POKhM@<)^CMzd=JD<N-@hRq#_ zWX4!+nj_@)VxJ5sv~Hn|v?tM|iTkX?VbvoUc&JdS9JfZ+M2nh#RS=2EFM8U95G$#! z`O1>qzxl%|5UkBJ^X7O3<V@C>H7;tA7-lVoo!4I?v&m_($CQ;E`i2rGAa>kt3k9)a zCUt1jqk%z%D+Ux+DBD(3S@ENDB)Ra3%Jm}(;6!NiH*zPU!Y|iy1W~I{#st1XWF{r5 z7`#}$+^ZUMfc#M>w@7k`q>B<A^7x%(gj8IFo)<@*C5rUyc+Y5aj<JxM%)E7ya;El= zSyQN3Ofaq)#ff$x*63N|iUjGguzF_*m{Z4T1|lyMx50lyNFxjdM<sJz61A>XxV<+> z{F^dBy$UK5?yXQd+B<D&_im;17-3c$e;F){Rux%988q9q1tQyOcnbfD;IRaL=DG#s z;JqHLfuHlwH`N^{JD2GO$KF{i=?P{TLO50TQ5>X5jHnhUOcu3@Y!hn9b~|mvJj3V` z!?9^>I|7?ZZdAc6<H8|y4_T{aZti$W)ASE;WvQQ{tCfaEc!g<h#>3Hwhbxk12V*RF zn|^}HAYGEQNBNa5Zfudo@X;9p?K`<%=|APdPG;Bl0tGKnTypk=fh}*o*)&~PC!b^c zMB!ERsxICNZUHo|;8EMGPZU+Cb5kW%a|w#%RXIsDS+)-El>2WBgr4k}J-8IO8J*L{ z<JGuI1O@v5Uy}o6_x10=PeS0CzXzrPY0~9);KjJW(Y*-5rjTi@X0d4EC()9y(gGpy zFeW`qV=dJ>nT&{}PXeBe$D9>|V389MfO6TOIxc3akg~|OtYj8$cjRv56!^^%yxP(B zBC--pF|cfBOJOBJEfweHmbJo)sg%S2PAA=66U@Rv7h$w};$tRYXkzRidXvflW{n=H zh_r`%@w3M0Ty`yJq8R;#`r$6Jro6J*OKC*bEL-e{zr@THnPH9w1ycZ$mg(b5!*0oR zrsP71T$n<Lk$GdR5fe;yHY&Gs2n)mX7Cl1hA%>-nH0;jg!v<k|@pS1#r$XMypcJtX znd@SX5IFJ!y)1!(#zTacR~w1em&israoJ7b>!uUJWX4)ZhdbF6p)*8O@S?;kYC2<$ zYS6j?+KnZcZMDKvy*Vh(8T+k7{>WC#J_Y6>TV><zGNKZG9_fRgcl3p|?NRHwVBr13 z8PZ#0xV=jxRg8|0J+L6?BI%Kp*O_XHSgT)SSEh~3?1SQ!{}4?Mg8qF?o!rsQ>PZP3 z{d%6UttionfOO!vS~F(HU7<T5WfBri_+gseC#ZPuB)S*0oI50kxAK3zkj9vk9V?^& zXivhMEu+A_gz!G~POM<x6b11bPZz<6MlQDaA%tDLB|Wha87(M>o%<;6V)KSq?m9L= zE<jZw?DqxfO_=OrPC>uFXk7OwvOB1qv%oT+cT=)SZUwry!u><@(3!)XFp-eXNqrzK z8e5z3Q(H>NKe%MWOAJ|?)os<)pPhD%@||plwBum`xJp;`Dag0sLL~D?>Uml^fo3Jt zFm*dqh{_HD@4T#Tc?Nt7FW5M<r^o?sb7pfy>GT-%?i%P@DzA1yp9Zw3av1FmCHX}6 zByA^pC$l<Dn{Qd4>k_{Cgjl?l!5HBrNYjQ+*fPZTW`a__0!X@){pKD&`qH*2N^`=H zB?%IxmuNP1zP)knw5`k<QX1P%{$jF@NwVb^1(w_*R&xUS{IZS|MB8~K{2`Zrc}be! zqDPvPsxeh~F?09mb;9@QJaA|KJMD21;C93uNlRYaG%<>T3?=S#;>;hW_T>5(2W}t6 z3pgjVLL>r*H?bvPE@&>K<muLV!Y#NWN|Nn8wyT~aq-j?woY*7jLiB``2~-RI&^O0j z#>>@vUaOrnCy`K7YZEPeH|$<uRfq&mT#STCg}Xzdcq<MUr*1k-Us!s0JV)||?$blP z4^v>LY0#u=mP*KV!!6%u2nw-ZEvfH~m>?e*DNkzoeQ2TYJkTofAre552<eIjqwn!6 zKp^;@4V~<GC1}8bW{;#!a*WNJ`ID~3k(E3_64l1-!_Zv@Mv1s~A1&<HSHzsSq%9SW z{D|2DTTAk3r@AZXjk(rb*J#5tTyoIO@LKGX;?F;}4bjlHtxF=*4=(^ce`H3>bZ^4x z2%<ol^8I?`z(J+v@2%*>Zm-(XpjNSl%Df+<@<HB(cc`S(??*>lFNR$E>aGFrYNo!k zGnFfZ_y@EkinlTJAjJW+9g~lU4mS_+yrM^qwhv0frlPktvX^W?(K#Zg!Yt7cQyKtt zGAV^#XzhxeEa4B&qtZn^8&J^L)o$iB!|$;FE-K+YJll$P)6Nb|&QM~tI<37J0=n91 zCmu@3VrPfVW0@*LxP(ya#IMwRFgqJ_CX#tQG%ga?Jxu{7?3I#yTMKQGQYt(ekPpI0 zX-zlWn<{^F7al5!wtx%y;=x^Jk!CDa2^4n0^VU9E!-J$(Y>BYrzor~)p)YA5siS#^ zda6-}yNiK1l?A@FGxW<t!%PaXHqD-WB-1eDVYOOKj1ZRB1dG3~vSm6?NPB>FZ_vAh z{$w3PeA&MZ*(Wz(&7CS1Of)?S(FL4`oHzlW2b2+BMv`=Rl3|_3!1p)vC*kBoH4N(- zx#M-<Qj-6VmX}rVXB1Sf71XFCjJPlw#o2EgyJ-#wO-se;8KoQB=mP6U_R#>+d9nLW z0uj+FXFGCGs~n|@_RS>7{e*Gf+3(!TM%;8NKrA@I*x3h*)ARODT1l}5n>-3YRUD63 zd<>@~dgn-+G;|fDaX`DM>91us#~OACv`#CRY3UJr@=(&5H^P(%RZ&7MWGXGg6X!JT zGt(CO)NSW08*`06IN)Kr?d`U(NPr-?!t%NmHW{D|F!!-JTtU;>FKv(T$1%L9g<hXg zcgt)y=-_Z~>&sFqSF$~dI7Dwi+x}xrth=-9cZ*jhO4(uOL^zOC#EfWOIJ*#*y`HI! zP$pGp(Aj?tfo<b1q6-zJO}5MINoBPWgGeD#py(V`SytmYL$yZf&Ln|{>R^<1F`TN* zkgU0rbQHwaR02S_qr%AqoDwvs1g)6VrU6SqQ&&2@(-5=N(kJFB2vRZ8DTLwjp1tRA z+A(Rn+AW*2<e6q}B3?_IFXRDAZ=xlq3H+Akr|F?a3f%xL68H?agu1s-r@=qMGFI+d zv0EOQ-E!k#qoYwQYfII3&eDWz{yAWh?CEJj#wkMtEtto#B#xE}S%-@zw|WO3O!kKQ zwXIpsyO4Rq=+NR)wyTlo5+n`pBZz)-fy78>sYc!BX12A638N)vk%q%<?pPzoENX!+ zbyjPns*AFrQ2KBx*qU2%oWk1vk$aL>ypujKq;=g4qMI_YV=2av&6!QdkP<sA8_RgF zfX2><F?VT_)CCz987l>s8&lwlXuj#yRaBSV8f0y^s_&wH8>c4h)CNJROWcSK0z+v6 z2StvD_^@eNrD5jpQuL;!Vq^fqQzmkvz({$`gv#(Z4Z4*Q#V{KXVJ9Q|(M2My9w33U z4Ob^d7zHC{H}qo#`G%csO$eih1Yo)kcnsS_vtm3yfv&6EtqV8$AHbW!pVKyrC_;!P z1QF3!Nv;di>jsoy7;mAd?hcMndP<EDW@8GvDXJE-Z4=~`lJg}tv@FhJ>l?@pd%|JL z3M3w(=ZH@ai{mXy)kr8_3<a#+UAI_Nt+2?y5aj{NPngWg!(cF8xfaFM(k2JYZKw2B zNuR{K&%BkZ7PcRA$@D1^P1b3ETkS8!!nbIkVW#G|g5o8?4pF~M<>G=<n$63670?AU zY0?P64Y$<KR)zL<x=F`DP*N@$Rp^UMt=F?Pm(m;&%ws#czJ7QYXb*>xI$O?hRWh9` zm)4e-AT2%>lX6S(2;xM{h$NFL)x{Lv2L#mJ@D@5&bVX}*$1_M}d!9k<9=4ORW7T?> zbZ#g$`*&xmJz}+}%@s|=E0AfW5IBuCXj-JU+({VKa3{4fRX5iWy1Ki=lFeGsAaEX_ zFOC?ezQrHyhRa{6!y{~nfyAnS^JDk}8+Jj$z}UsSe0gg~>iEeY#fU(;AF#_2en46v zYI5)o=b{0vLAp%^8?+X*lc|&{EkNFAEGK$G6>Fuob~H+(<i^N`ux>6M7a80_W2cfP zDQScXbFpp-bfY(HyZB%<!|`Vo&j6fRtljWaNC^x#uCo_p3b13gJalA2f#TM@cV8!> zxWQi=--Ve8`yab-OaPU;Krj{gqx|m*Iaz3<-L<4c+WVm=q(6d#`$uOG!&f*Y4PR0R zYf6*Z1-o8X%}8`O3NA}Q9JZd~+9s!9%BE{wxV-B780NVp^Xgb)r?moUI&_W+l(E$1 z%%b1oG(-~P2s&97*mTZZ`N$m077s!5skm!csLDU~fC8DrfqHNNCCuAG$>6B3O}Pv! znX0X*giQ47t-5s}NnnVY;2epl#ffOP!kA(eIm9tdq%$LWj<Xq=oZj0p*1|uitgvot znRb<wxmJ__fOmd`$g0p;<{x4T0fKV?n9ZsEVF~I<w`sRY0PreQReg%lL0S{kqM;8_ zq7!9(1%r=)J6MT(kR^2-O$Pvm(VGRwwCZ}|g<+X@Wr*z~^f|0m)&!{s@Wg2WW`@-i zji`|aWEVCLEC}L<FLA<}NUno~rr%iOZM6`QrPOkctJf7XaKyw|i5m?J@kBR<v$dIK z1=qfbkzncD<%1S*$$Zurw;hWn(kwn&A-Dz3C`p1dYkH0$=O=L1HUJSYjw*I_J03Ry zE}E^sJ+TJTmUKnSK;*pasMVNoE2>`^$FbsUB6AM&1fz-IVPw8cXh*t24MvJAduHEh zeILxbNPO_>IqE}4r~|P;vsYTnfLx;+?T56O1A>a2`fwqk52Z$PXRy1@|B4k>r`$T+ z)2K8FUAs_sw#)!S*E5R|`$6?xR)7V1Fq$+$4cir4^>^fB>-?N>ADbwB0pxNdIr#{< zG}-<Go)yt~cIF>e;--n5!XRHGVq|E|a99x69aK_lf^p2I2*%J8GM%E<h(}}*w=oF} zS)0UfHvMeUAS1cE#X0<@cVSkQCq9K9Wycn7>fTzhA|)DM;o<0Y4S!2HcBT(4<3M-0 zYy`wcHA;~+5LOm|1`eL3cub+DM9G1|+1j8No>nOW%yzb7t@2cu9zudRuLB`E4iTMR z@ZVGUU-C~h4R(kogRtbM$LrA4sA}OCbx;+1v5y4U%A621oHLksV0>`t7MsD2FmFLK zjwK*+qSr;?q}mM~u3W`yD;=_w>P0g@o?XyT0^Wf9$lK#(p%EH_+J|3-^Gm_;)vj9< zPOHukAu}M~kTWTRnCUhw6pZ+uDJ|;)B_Dwn!xZIm=DuKWm-wZ#<CY+eV#oed=uFW( zi5Z(zowX!!iC{i5jhZPY4g*cE3uG;}ruNw)>WXz+otw&1$i&ePDP-baG=O-L%UikD z@-l$lb|%qS!<Ds@!j8z|QrmOO@z;4_&@@KTa<B&xt5mFXLV_`MjgkI?14y|*7KYYK zG?gUQ&Cv*?ijtxa6_OoX8iXZVtsek>>#F%&TZ?1#NavBY%HOZ|q2$-sr5)3g70R`O z_Cvpmxlne~S+OdSA5oO%=n5K_m{BGEJdRc&wxu;tFSVbb&~f9MtgiLU;SBRpEhU^` z<%pAb|HEBAkYOv{=Lnh(fN_<d3P&8tM?0mVqpYBeLYnGyKJ}GL<rWA}kuWxMLuVtY zEhCHa@}TU{eW<f?xj*gNt8fb=(^(Du>R_-88caSC^W9ej_8?;&2p@G`0nu_ak31kP z3f2P+2wVi(wy#ETX}2Ob(_oISPAt+{il~;@#rb01sVQN)qB9^~)@mEyh^s7Zz&SrQ z_?!|iK%}-In`Rw&vTP;5edul0nI?)@qKl7rIL96xAFUWI`%r@0GQqJzI8VoXW^*SV zk$u2?P*|oP`5{#^t&(?x7|RxTNe4sBb%7mC{uv`NJWgUy^(Wu=?zd0(_P~JM`=bOQ z<#EMKh=o$3b$enBPcfgaP|M}WFwg`JgrLD&Mr#tELg!<&S{e~|Vp4`VM2HjS1rX4` z<w<VeijLUeUf`}!A;|TVDv85cy){jWYVCd#M?ec=UcO&vyoNkOYw7M=uYI0Q>|na5 z(8%C;N)YQa_hG}=T$ekW2gDI^nY0v2Ul~xA)oD>Nf!RO-i+iRJ!Wer*KUI$g*BO7H zx29$YMChU=w=551N~q<N02QK9UAr)d6{b5X<}c4zkr}Q_C%2Ejm1TugDj5(Mh2m80 zxB6^9542b&8Ad*aYa6e$dxC;~rzcU@m9gGIe#e54vJdKuMc%}DL=Y;wM`#~w6t?MN zw0ye;=vM`Q1jS*OeL_%YV|QFMbibf=geq(b8H>y?3QL_Y!em?#<MsuZfGJ>QA}A;2 zkV737;ek{3uBtpUTT<y*anPX~bzU@7Yga4cTq*gknX!eJitatpmaeGI?t7$bQ^6jE z%`AAWK)N<5(zFQk(z_A<Kt#<8R2)`C#@AX0TeX!yazy+5sxTKajGNwTrv0)M0}Gv~ zL0j5l;@kz#IPPp!48*o&_Oc5gyKU$v-LqHsNk-_abszbWZIMey0xzsE?*!2uR4B(> zYRFY;Gn#|?RHyXZ!gp+TyLF;c%Z}-+uy+|kE8A;!$LPo@+OF7MGBKr!(^~C<RZ}F@ zTq>cV^R#rkGg{H8**>sY7?n>cV1Zgc=a=MQqjh>%P%WAaGh9M5FAo*Qqy5yyEX0kV z9o_6$Py1|HR+9T9KL<My>3|@VnPFoX7tqJ+PATNV=y<V~^|J%1HBrQH6T-D|;*ub% z!6{12IuAzfF2_xwW;7ztsH8tIIYF*s<i|y#s`;u@CK)7)Yzox6NShtmTj!a4#GLW0 zShc+Lcn-x(1yCg(#3i<YE7hKSYN3SAr;52|7F|=LUM3N=Qao*`W+d;$D{-<6>Wy%m zmxJJGNIhph<`VTo7;PmN(XxK+5Q>C)3|<?L<5Y-3^7`hyqjY|<R4UxTR^ePt>2)7o zv_L?}vyYT2NDdN9nqSEx8PS8g7N`H*Bv^?QM#<F}-IBt}vw7P;L0&k|bHKt>;Sr5! z!~i~Zx4!i;JN#4R9%LC(eR3BDiVt7v<OQy9#v_%do!qcIFN9iSNTE9%J_aRnf|so` zSn{0RrE{Y~*G8Ppme<*K6KTTT^P*bH3Kq5qJ#yzXqm=T6U~q~Q5M^P0epTJM$17_} zh`-_tcJz1<+@!(OEhx})0f}tLbn9?lz7)mwJOOxT&FM(d>d*)qgMOcfT#|HUOSOy! zMt^BANSqKWx(w0cw2wP9Ue#<;7bM1|=O?0WD+%!VPUSnV$ZfBAlX*uNWvjZWI{jc@ zjdhkQJ-DXHjDMhQq)ZlLb#Cn!yoB&{m|^yYYB;Z)P~Z;0RgvTbAkx*=4H<AEll($# z_KuH~T7ptT=}vXcR_$|v#~PXb2qG!{2WnivVOQym9;5L<y*<5)+Hc^q$Ol^V9%8y} zT^$%uxLWq_MNk;5T4qjB{|LietBDvKQ}zLA<AHdv7|isX#t=In;*TkNBe?9N@<I|1 z1A8lt$!fx`jO`*^Q;t=qr98qmffSDu9>;ePIZ1Lb6r4DkZIEJ$zwh6JdC%^_95QSg zoZqvvFFeDg>I#i`*m+fDUbyB`y*4;G#A)ymKwX^A3h@KY%AW-{G+O7-IC-v!g$+P- z^x_2P7y|4a^FS1ouH(N~P1!c`w9&V;vtv1)M7p&86$Io}LZyKaO<U)cP6!D$jE<-k z@>wCTR)|K4%|f{DG^(*IfnbR%Z;=ogW}Qh)aDsM5jHYxzdBk%TxOV6(o{sz_#B!E6 zz*CXjfItEwMl`{_qf7sI<lG$BjTlXatvrY{tVic4OJU(L9FeY<5RfVI=j0r8f*Y}m z;DNh5gPw*)YkM>E$;?HkYO*Ea=~?K>3IVNox6r=TVYc5DtANpj)5Nm6jyQX7MdwjH zADT9voFo9Wubh#d63lVg4$XL$)q*$q$7A5#?4sDYBe8ieYbf2}*069lN6r^zw+ES- zy;GRILzePt&GcXWVR-03cqN(Bcxv0NDJ{J&DW@VCLP-m`3~clgR1^4-SnvSo;u1an zoKh0{yh;`zxRL#}fIme>a?l+_I;F|M4AB^*N*Q^Gut_MAVv-KgBK@hZ-<X5*JaI;s zy|9)J%o3|S0@+g~1}h!Xi2CGp&|}>nF4i4(9T!=O02wScGK1tsX$R$a#*5|I0%<0= z!7Qnzw0#ukNu+>oAXJRHQAj~dM<_~ixv^PdbOt5GHEW$bUJRVlGO%$Qrf7U~6uD0J z^n!>RFlxHGL<^39JFL+~MX(xiRJlltuS|QrqVTG}Lg<%WA!uMC&(FDGlG8W_K(fn< z_E<S)^j*3flS~p(Oj_%aX<+lul3%?P9cqGtt1Z41S0!?S8@lQ`cQFYf9Zt~l8hK}4 z5h|zr10i;FlIJgo_JS$f>OU7QpF7&j;HU8FFdHUBGCWEoXS<jvYD>pO$yyUK0jT}- zU~B+(C@Hn=U-~Rr#l#f{XakQBSN5&*7V8u)45i%Cl11n<h2*#!3bJx4Hx*V?7DUd* z#M^3W9U70V2*COnL6D_?Xa9ig(iSq?JBx%J3GxxLo>W2W49~3h3LQ5>qQ6hY&E8(( zEF7t{T|)}df+(9CequQmc8FR*)S<N(Ip;fOQl1nK@>5W<`}sI4wvh{Id$C_9UQre2 z?e<-2IXR^2*#8)grJ#(&aP6wYm!=y4v*ly~iv0<wW`U|iPAnp@_E?~Hn7740)cP?S zUZoN2RVVdQ{Wj90RQ_byk~d=rElGAIlK;#ji_T(Y2VaMMTj8uJ3cIiYKBWec1f__X zvLkt;NqHGN#T>7hAh$7+2V3MBG*lhU961INaBjkvW&{LuS56FZc|(-|ljW>5xc^Y8 zqd|m(W+rOrMN=rcX2Vw_iIs#Obg<D6lP2@cx&Q_wkk4}~&z8|s73L#9gPjJkfngk- z(fZaH1VzQ(As148Uz|GTbqbFIbvDs`QkKGm_+%lsuFeVcw>l9sIB(;)EYLiJqfyos z3PS*MonzOj0&VS>ZSW16As{~wkyk<vR*|(p#`MOkqa)yD_?M^Wr=(roLW7exMes<0 zVG^Cp3LtphQ)`n$3D<UmQ28950_bB8r%ZB)051qbfjOv6oB^=YXRI9D!&-KXI;2@X z)02Q}gw#`Vn5I+t6G->;hBW4gIn5?naf=P21n})OX;l$3MdW75mGaJn=a#UrwuzhC zz(B>I3_003?6{7wyP_Z%?r;P3#977JSIDFYD;X*N+9X-oZZv@`h@EEF<?Xtz!S)gW z%5KJip2ao+f2w@MPO?)j?HFRuF?i?B9Dw~+h~SbyyK4~+G_(s#`GgZ5Fmw7UCt3r0 zVQSprm_P{Zp5AQiK8wJRX(e3%K_3)}7<Y9*xI+X}ai6QGP_E)PcSqK-Yg&pstK+l< z!}|uLD&n!i9cAy34(TP~GT<+-ulzvMT$>lM``Z2<(EjcYG78^X%pQFV!ktepW>Q}G za@mOC=Athm@QG~|GZtW;TuIw*-N;97eDKuic5&{^0So^L0=1EF5`ZMkUzhXPPh!)e zI25MMDw$T1E$6l0SS?FPoPP<ZmVGKlnT<0BSm7F|RJwj+R@kL?RY!Yq7y;;sB4S!q zuCRi}_NvvTpEJdYiO7{#fV8u#VfL^F437#5fbHp~;5lYx4TsPSxlVUB$B(%=NNM+E z*KyCKu2MTe>&T@hCsaR%Ta>wvu?lg_ebPk6)iNH#9(!F13pPNDs{6<RgdpdOQa3|v zKuF4XyJ>=%%Wbh3tPX!3ZY!U%BUy#!O*3T@S7eD}?T3|+EcUQY2Qm0uxk5-ki(c>D zIo+tV+Q&X7k=6KQ7NRaoCywaf;4mqWXYN7LQ1_+bKHrr28H&YFYL>7U-V4!793;-J z^Rb<HS!^o~KhUmFIpS9>w+xYLHCna#jyT@N#tq^!CCUfh;M2R~X{Q3UAfb$V259cx zhIvk${6;Mhk?(pP+O7D(x00025vpiqqFjMO%eIM-D#9H6!NTrby&~asRnjQWDe^Kh zJ;}Ln#`sLNLH|2poLWrQIw}l2nLa$`#GL7wp+{Xb$Low5t`1fQiySf~x`q^KU@>VR z=@KVO-sXgtvpKY5*`!4#O36-WmeAk>3;~(K`;Zk(XxFWtdyuTN_EX$tj)u*TutW*@ zkse#lUEmCfduOlG?7idZ%w1EQz3l>2xq>1LvEniy{WmxPbs6W#1e0JLZyq3$=EM^M z027WmVpp<*4a;1zzZ~RN9=ho2AA?=Q!Ceep+=EtI+bO&r9k0UuiTNgYx!d3k(!8AQ zcmuR@&}wE<cnUzqIUpbPm%oO+%Ee4kkMgf0#NV|qu?YkY=#EnycjQd*nw4_u#zG$S z6afQRi(X0NeRV@AdrvU6;r=Lo=}Q`6GDRXh8ntVX&99Y+VG}B_H+-Ycm~N7JEMHVn z)E0Ts!+30k#W!!T%Z(oKsHE!2R7YE4M`vR>D)hOX=}PAHfO+U1BJhh%G%4&bV46fG zzow+G<)Me#2-OPPM&@c)i?t@!*Co}oQsuUM1f~M=JWvvt9p%V)Q|#9cM<=zEK<y1N zhol)ZVr38HO0hGUSv)md(0VlTtio#H35*R<b(KXR(;*(Gh)&YrrlN>7v%&1@VFjm3 zGg6e<@1EYABTv6X``?MA_BVPy938x--)VK3orL$Z_DCx6Lb>eF2aZj!e!;mmrD!na z8jL`4UE>L91Z7wNyO&9TaZ5J^(m96+w&N*_`(Y{-b48D9RIAx7Ou&Mv*BY?%<0#ue zHz3v+g_<5U?Q-O+bodUksH8<#$NrE)xqt$eL!#k4XW(%7wc_VGl4D^oQsWF<l{q2C zn1L5Bv-b^B${}vJ_F<f`@`2x^AOQKh-L%q!*kuua5s$>0irGtVD70Uk&&3xI(ugcK zk7Zl5`MoYt-c-vG>z+t#W3u*+VJOf9z_zQ!`!iq#=6!H+nw}5CD**hQ@|9Eh(JJXa zeDvmm-iDPO!kW*nuIL_5kcfE&`Ot;2Yu%t>rAgArsfIxI7ig)P1~)a6u*CYdG14k6 z-LQ!}xwj@TG%H0?Xo%O^lPT%QDG6h{Fci*zLW9c>Ph&rJD1bW-SsFU4m!Oc0q~Kf| zZg{zz%DGiaW+F#dF}^5moFR{L<ZX4+lj_)N6fd7k&CB;oJsb#FTA$UZ-SoQcP9`&l zbY#b1Pw@{$)r(Y;W~|G<OGgEHki)h5Ib=m;=}8>@tj$HXrzVXo!qZPkV!dln63#Y) zoQYH&?9%;Joz+zra=<5J(Mr(30oX1Q6Y(gsTPzdu@>Yzxcf><3N5{7dPtNoJpNG3t z0#Bis!o590oNC7c$;UFx$5}N-PDAEU0TSW^Ueg&?ydvwHSmJ8qGTaFe9-bO$MMyEj z8Zdv7!_4xRaKd}Ll@HN<^j1Mp%qPzf-KHdSOmwGmvnp*aZcOAJA;u`q48uAW5~=i3 zMLfh8W1O%RvdOLjVC*lzwnrnR?t%z(QC_spPRb^6h*t_<t|YdfQWR|^z!MHBhK>u$ zZ)j$OGs{6q<Y0vX!#hZo2ME9jl{TXs)DY!P?8SFV^oV<LU+oXvx5NP@1af>Tr47<- z&5IK*P!bP0Bqm^>i6#f15vEpoX(Yej=i|*wh{l~?NVgB;NVdv&GyIh4L4+YeEZO2j zNDL_w4+H9tKei6`%;GOTA8%tnvJRalowtq(w|oZR%^~^>@!rm1)Olx)%vR?xQy*_f zeUSPKa1N>8G=rzK<#NpCjj&LmXH<M6vJ2!+q;Usp3-{<WPFte#TUQRm4uo=Nb`1Lh z=lwX>FxLDgNHtt&HRr?FEMfaMR7%8(`+~X})q9-)9cbQZtBE{5kBkns*=7UgJAf6C z<$MgsPI@TjX^Z3_>|<nUb0cnuAR$frG#5{v=p^i1j=oB~ys;F#cF8;w?K_Gm^El$B z*^2%$E@virno}GZiOL~GsGnD#fvKrP5gNou7cUQkArr+b)*mt*tZM)!p*4xwS$B{M z4eD18;b?=_=Q4BZVC4<XEp9j@HcpQSTQ+yFv1GERbTwLv7F4qsN3G&x`*BLQ2z=K% zN}t7C=HoX*$4!v*a^1Aoiww%UZ+M^X(LLSRf1r9jX65$LuLF{(P41|<%6=>8kso0@ zjXbCvi7OOUHWFQY+msQM<zS@K25ifMIgAR#a^#lfT1KzhT{v6u`i`(qVKPC=78K@g zSE=B<+<D#JyT;Ia7kT<Hk~fws&SQFVzJj+bU%#qK@+;Ah#D5uI!SF{1fm;OQb5u83 z!DI!tixRr-6nd#TO)$icH{H>V3}`#UQp3XDK=EPh4eEv#)Z0UuFNr;6vPlrl$ZfK) z<#7O7)lgC)7@&OE56`xEa!4W+;*=|yR#rJaakN6Y#7?XW_t&~JdDC7aiq=6x_MgK_ z#L{AKF1a^6zNN1kNRU}2u(c$Gpj%~GX_X{<G`3e@9C)MW70YXbM_C0sZwS&+ak$f9 ztFnk<-{m4s(B&mGOyg}e!7!t%xp;K!XoYuZsC5i;^3aF}LpOFoP8?_c!e*wNw`03w zptW^Qg=xpsEBvUk&LXr53nSra#gRi##WQP!+<0rtZ_@@%&_9q7SZWr7o^XOK=(~VD z`U2gy`1?F#xSXRMG~(zpEz1yiH-^z#SOXeHH_a)*@Gh#%uy1&CeU(LL)?R{EG3S=S zlhos+&OnR_x<Yz55|=@ocyxBR^lDjz#tRyi(W(?HO|z<|r>P01D0;zZX0kYME=Oo- z9ibFbE;q#6Z^`eB#7$XPgV`~Ss)np{9Ny^W+$5}kk8+axVE;vX-zpoI;EK}-zz~WY zCRMlTETud9e;uQjx=X8bgY@>%(HgC8F*=MX5%F#o_3jz1O<-euoae5>_;RNHc#eV# znO6lG*Q)^jCA}pdhu=P!ZR8@u%<Q@`De%f276eIoQRNi@0O)eT10x{a&UoClio-rI z3AJi664MCbhg>veZ67E(9M$SJFYF%l&$wnS9TWP9eV(;_iWZbbX_XnE394?l4a1cu zh<J1E-Y{Yox5NNooO^4X#uFnN!I!P;x&Rx%yAar5A0N^MXTNEaDpNGj{Mk7o6dWLc zgv(2M4JIobTfr`5t2H3_o(z{rH1le7h5~3ou&Ko>7ebI8<UAvie0)PYSb~VvJjc<7 zAqqrGjj3b}1n)wPXsn!)mesXA>M3f3D2>i+dVothnk1EndCkyG1Fbr_w&G7vr`*t3 z{v_IiD(9s8f@mTUa*i#3vUpvruycT&yE$l;8i`jr$CrJ-@i@KahWdb{xM#n~ljx2y zaF4VKGBR}wm={E%fV8PhtB3P!0BkbN>`bQzLnt_LqN5;^!*$cxT|z-5G0Lc6OHU+! zb(<xIDTQj_p!9N@p1=skJ<$cFs*(uFabsdkr&Krx$O18PLpJ!Wlmk)$4Yzid&YV<T z$d&*NSYXFH+E|CYX*WI;l_wqL(v)sB;MuwlZ>&a90<#&7z*#pl?k4;yk~SS3L5!3| zz>;50Ub(m1J8Dw|)i->Bz>wfvoMWhPXJ;L5@WwI*+vz#dM8sk?Q8E+-@Kc>K73{6k zhdXa-g)I<#4LU$Tlexm!zG^er{OlR^!vd<xNMeRvnA1afH-?MibHQRTlPj1d{^jPA zK_XA3w?vc?oj%ndFOtIzTEf+Qkd2hW0ja?x8#WDx5w!-;fg(fCxv{w-kR{Wp(}K?7 zHRl{>I)&Rgg`R{!k6awUjHGGpVRdrjZqZwFI)dG=9Ada(>^kNXTAX}@)O9>n%HtGy zo0Mfq#Z~=jLtI!Z7_UA=Ahh@?x>xW48A524JEXEq&^BtG;G(f(jn$GS6`{sZ8o|nv zEqzvW+;BW^juWqtv+c(Sdu*$hblm`>uham@8H=d5hdvCyFCP+tQp+j$fXZQl%R%Wh z0^VfN<k)@C8l+jo@}o;Hl#%0KTorIsqmcoUC~Pte-=NA&)>P5(WE)L{b3oysp#6c6 zo62>u{3g1_)LT+V->LbA9-LE-%3qAC!+W`Mk>NNxLB*cHMs?_gO7EheJL0j&Qy}qc zVhSnI96-H0`D0x`W}iUbQA5oduXb!#<dhcsogC0K_!8d!V-V4o&{HV+0Vv=E7nGf% zBY=4jUCWR>7P$d2!tpiYZTNF{MKW=#G%@LtRN5dBi>;(nw^weRjx@SV*UP?~oP5l~ z3Hb6A9-)nPUuU<{4rK~3dcXh&u(jug60pYbx0X;no?Ua#*@-=h)DhOe*5S9_4X{L< zWF#_cwKgtcH9CG?IOb8nk|gCwx8%ZCGT&x(Mz-@5u&@OXBa|#1++DfMpYY5+5|5BT z<Tr$itez3wtw^W4N>OcPXuxLRV%&y#<nP4v{jxDr(j<Q^vHbEWOl;uZ@)B^C23EKV zXy!wjH3?6RCr0Dt=t-c`p<BkDjvSb?oa9i90p`l1{-$v{APmKRyB%C7uEgHuM<32W zy=A2pYc_5U=a$KW`^&*$A)I1nFpeJBk4zj|k4$L=C!jMmoY@@lx3Dk7AOLI7CIAaE z!W}1&(~O`ismpFkn4XEuAreSZ=wDD>B6O%=Z_18L*gW)M#1)6Z9)?0>pZYjcl9>tI zEu2D#ioniD)2yn~Yf|D(GR>;KCzDDnf}1Vx9Wv&R<D!7ganumr7=h24XexA0?WKp- zvy?WSMRcikzDQFGQ<dz6Oi@=%lEuONNlJk1lWNMeL025q!3Krh7>-eqJ8+HQy-AyO zg?Fw8dJ~!GL>d4=nQ+E=>AU^137pOp(&@j^CP2K7D=ms~9=eUID!5tg*YMc=J&FY$ zL<bxPDS9`cra8oDNEUc_9i$j6XG*$gcBgTY#^{zF3Q(zut=>7QN2P0$4AMZ?k0lzM zqv*pp#GZ3)I$exMK+F{s$V=;l8@fp~S@HJZ%VF55*h3mmb2tOA`WR4<C@br<+rA{9 z;Ox^}$8=8ciZP;J$PvT8(ozWiL1BpiS_!yGR6=u1kaXnO$lyp(wXgF~xi|P*L*2oY zEDDRjhBOg!3K7ojBwpmT1tM<{w4gB+m~uz>l8~seMEhr3oy>g$^c`0;acFD9x-X0- zWJ^0YJWR+8A~@yF>z!mK!Vkl{aYc>Q1KfmiJub&d^x7RsQ)b){_OgnF(TX|lBaDk! zG^!W7Q9Iz**?TKTTyan<_9)=OPhDzzrgyi;r-%^4u!9k%y|<OwO5EV-rc~&iZEENA z5$z`d?QCW&`oSSfv5=J|r=jT{?#+W<rrk)Al0>Kdi0`Nap5ZJGnZLGsN`*9J90bgc zh)O=mY!Y=k!jlHbm&J$G2tbCbE7Pa!Wc*=#KEuXxr4Mk}(c&o}(q0eW_Z<9WNl^-I z`7RF?aROxANzxTzo0TQNZ;+3MYYYF^wT@f@ZIc3=c~Z8WjiF*pn2GdOc=0S+>+DYQ zJCIK7FGj-kjr^CR+Gt1%1<hSi1<h3tcZ`Kq6xot(NIK09olwY-YB=n5vExiV2Qg<# ziEXAHeZK(~n*KOJ8k`s{XfR2*2o9^09`NC3di3UjB7ul_N2rxhk!H(i*%nb+zpJu_ zN*Dj|z*V4Fj%vt#n?HGk!(^#H<Cg)`8u0!#g1%_m&%f{_Jt0rUq(!a)B6j$b^(%Zg z+vbuD+D)iz*peJNYR@ESE?jheV~eY_PW_lEItd^vz<O6w@6PP%Vzs%1vd}{+o^KSq zfY$@IUPs08rA+!<;704Uu<%|@NCp&gCg5;HB)u*tC{)lE*o}?S%Hl8~9<AmKRn}UN z84P5UN-{?+O&cSdhbver6`#94>Ul_#f|I2HhVAPa>%X-%e&?{&04<3=by2*-VqR_W zvM7DZYRQ`{h+PkrXBPF)IuwKoMT;(VvaAw*w+j&*e@nyCooAwU_Q(f9i#if??xW|+ zEo|(q7~Kbrk%BBWCv+leKd5m~3dGRPy$yFM@F*}Hk^*me;|+-M1Xb%RAEcWrOmZSA zvjqZ<jo{K4(E>fr)^YTlA^0K$TUAny(4(6@ii)KIU?AuzTAAYOjC2Mao({kK0?8nH zQ}Ui?1=8a21h^*Hwti1g0{l^SiUMlFaW1>Mv<cdW9zotNlFK-r5j_R#DBv3SL1HnE zs0f!xTwaB}hdMg6v%anJhImaf$83uxQj{SA(ik;|;JH<$+A~6i@iB!X2N@Gbt{(>7 zyw{mpRa#HPKT^E;sN1wa8Y5C59yL|-XiX9Hs<u}iz?9u@BTs%OCu|59(?r2~8x%wW zN-+h+%WtnJk5>AI#6r=9!FEBQOlFaGg>WgF>1)vjt9!dAKc!WWp2(sosyO(~6f|I| zVY6%<qdxa4gdK5dX@Ja=CQ-zs(8(q;Rz2WR2~aX|iUT)2-oWrXP6SHV;&>5KpRK)< zN>bKTp8L-OG;nd6Q&tGxI`%HXpOi6?0hw~ZuV!2TWWH|%Yg!Brb*mdE8}!Tw_CrD- zukl?LfoabaBC3mxbzugTx*DFu7I+d8voDb<EM+QoU_TiA##y6Gtcxh@h}S+GUaIIQ z??&?Pnljwn8{1ca{#ybVIgz4Lf_AC{p(BBWa?Q<aTwQP3j6N6$YTKPK2Pi()$#O~# z#3NeCdr6;_R@e45-8Il6#iz3;$t5&`;kZMgNo)t_MIAY;8*XiewMw*pr1}>wvd2o; zPAQ<$@}YCo`q>d>?G3y|F}QP8qF5LH0}n|Xxv-_fQn{o~z6{wn%7T>}(vU-I!42V? zOm1$FY6m~K_jpB1vbWqmy!$QBN1yUxlq3YAVx$qB4_T>g_cR_Hxx^7_?6|*3=wo&Y zA3ZJi-ZmpUg6^HzxGDwL4~R@*s6KiK!SA>t<V?yWx`W<0wt~R9vWuqoXXo4KAawxY z6e+A63+&C~$WFF#z@v&nF-CA%By+$Wgvn*gZOpXc;4DgHv=&(*Wuh~mR!og@&-2&e zny^74zt?J=LPlhMtrqBot@ZL^hEz?`(VZp_?Vd01bLuruxv{cDd9onSov{Bhf#SN) zIAt?lYn-CED~3s|1eJ^^Mbuf21!wSGVW~&h2PMir)XW~Y*{6_YQ%BVjlDjQ1t1g?C zcOB(Ji^-}hhlLe@KaA>g38)AljMyYnV%~JyGR#|owB~RTO_Q%6a%@UF(-v%xE9{!~ z=9<}y$<}lfsuA?6hXveH735UwUxoc{eOi@#7x8(MO==yiI!KSpz{aU^1@Lb_ZZOkX zG(FUJcJ{TTue2r_;@bP#EO?6Gh}6YuwEDDa86(CjF^A+SG*InY&Jln@QS2J#J;$lg zXb8A{^c6x&99Ov%5!mX3i5uPGgIx_S&lX3Onjc2{a%4cFg_bm-5RH@+P-8eQLo0QO z=txMch*>kC=h~f<G^FwTIL@LUp(hCVv01^^lBJJL2#gL8)kbn<hHO&%Wiz#GjF)sC z#wSiRwGi><S;JHU6{eGOqBD%(I5(Mxlgg3!X@d(kjz!(T6`@bb=6wAWi-=-0i8### z`$pwYmP`5{FyEsy<wkAl#kM6eH{6E)dZ@Fp($pi<LK0o%Rw9R@oc~vmwQq<Vu?%mu z%X8Tm0<lLYuKAe}9fEhO{i>vAkZWiz90<z8cQx9OX7G<u`&>A}YoK6uQ)Li)=dH*I zH0Nq&cAnD}hS&ivLxw@>?1fAzjPOi37*AY+*52%*j$5suDhDLaTqE=lbMK5e2URso zu$k9mp7RjLpcYdgMGt!kg2cKH3VE6jtcnCHfbQmG&XC-brVQFER)Jx`+y`LT%?4=! z64{RuR-dm?)N3=NTREdW$pL|(VP*+|wNo1d=AK7WsW=}d+l?k?AuNQ$<zWw9cINDr zq7sM6pod!MO)n9s49}_efE5gAH;(_#xuv}aSlB*t^!c-laa;7gBR1Sc{VXownkNAs z@#xzjz#LwCbNFUTizG&ngT|+O(I?1{vy?79ar}kIC&t=MM9sBwkBGXD3i|Sc19t3z zN)1V&gkzC8=h>rmfaF+|PeQmD$BG$Ou1Mk>dP-H`GGOs;XW3z$P*9lQX<U`VB2+$C zjcJPWb<m4aSr&0o@%!p73!zac(}b;ab!we51;7j{vmL%hZ{=o07L?UH+nzYuW@S8q z1NFokOc0gVc2lkTl!KV3`^3e)BT&ll_}F4gc2FCS862)2bX8<-WQy|yKFaCmNhV^E zCQTsQ7v{KP3>k@RVJgw;CG?E2nz+2a3>h-&E{EAS!{*ij<ibhE5?hrR-yz#hvF?~` zC1_635&;A8*8O6C;_0k;grs*ue^`Q65eCAnr=lCz0Y$@l0Xtg*<2QFsX?Gto;D!c9 z%p!*vYKWjEXmV-O9@(JHH^5{2b}}53*m)&Q`g&lQapXq^CINyu**az*V<aAp-Ez0) z`p2rh#QtUm11bbjk`rYRs&ZVL;mu3A7bXGM?j(9njsX|4-Tp0e9nVhCWJW52$1L_V z8y*_kbC8P#@S!!A^R6inctRxjU|2E43HxJty)uvx2zjWpT)*O3hhw@@f(?8W9Dvt6 zE)(jsqQ-RuCMP*&+c;aC)CRO+Vu|5q;pBEM8H4dM2g7oRJb9ZgV>o{9m+E}9f;4+) zqSiRJ+kSE-GDg{utSag<u{EYd*bs+kq|}VAau+OFCPZ~x0RL{IkR6GIHq8dxlSJ5* zr(YkYF5s`Rg=4vIK=D%U7_n?n-Lzj)4&l6YAK{SUssiqG>ajYMdV_bCZeKbSx7Ob# z?%Hm)o2+!`mQXw3c6zs&7!B#H#uMhxP_EKi&1sUcS4=s=UL~DTv{CPtG6WrQ68(#9 zR3_1#;0T9Xa}($?<uRzkOC%=YnW#~t^?*b=up57XQNmQanqrTMqu#>DX^Gl7i}BYr z%CODg1V{&sQ0`1Ss%`r2*02O>ov9C%?Vz<|o0KCsVqmo+2wsevY-Z8|H8>$9DUw2~ z_JX7PM<%UD--1ey=oQr+jmpiW^UOt0`iW4uEjSN@hepA}5#h_U=u2E5I1?w}qA|jl zQLPmS0?itL9CouV5fALTeG^#F8N{*Fxdhc}A0;pR_VFdp6Ot@4a}RkYE}m`O72{6& zy#xr$MUr$1$0<Ag1ul(julAk|;B0ZMfvZj$Neo(*rZSbMeZF00y~@a46*|u=wp9@W z_D_7$4M)zU>_D?49x<$~*AnZEX^$Z=e!7IE*QK49UpMVBydxHeYtNjT9itC&T+i=< z>|_YH3y(?&Z0D)61dW$4Sgf&PyE>DH&3nStERw7e@G#$1>V$WIELsRO?g%}|aa!xO z&z(o_!u5UIuvPRA=F^i25Z30jK{eRnJ8em0QLACquC&&@D!Cl8wHF3gr3A_LtRjd( zBY3W~tzk-2Oge+`#|y<)b%MKT9$o_|c0q%!f^#z+5^QkXZo~-DI9%KnVhc$n=#M5; zFAJuG_h2jRChT<AOQ(pAv^UgRz0Fjke2=-xSjq@147k3#`*EkHACsX?fEBEyk{KID zxk5bIDr5)-khv7z!3u4gLK={a$4I+$lcm%oRi;G<$VP4DHPE;#Y8wrxfW?^<-EuoH z9vMVq+JC!=q{+;KL$PVo-zbGdH9@_tTRT=@!WO34z(v?5M^f^EK~eZB1lI;jgSJXC z1@vxP3I~<Tm#8--Lp!PVE4r_(RxD9CtvbzRC+(~BMbt?+Ja3__$3{SWA3;(qGyl>* zyWl_NT%S@Fcyw^;(Mh*Q`t-$*R7k4%C8%dSLtUZh5vR|n0Gsz{5z=xhZDiQI!e_@u zGR|;=QXY8Oag>HMNm2vv?xsA9+1JJzU6>|bg`$O~W;y5CDoqaX>%^FF|EV)Lr<yyi zwufU6HTV$kxOAy%mM)+RwJR!~T+H(H3cPs=ry5|YPqk@CqacZq=FgMHI8LU>@H1cP z&dfkhcX6p$+Vq5rc(zMnq$5R`Gt-JDXdinSh=5CJjdq+Wn@(F8(zUlb#10@okAw79 z7B40CX^d<IhvqHW9-Ut(Ck)u65!SnCNrB1rT?Od`Jv0IgsF{`T?yKYh^{-}lW$%zA zMsa~Hmvr&(agCKZ4HG0s!BWOQOn9p}pe4F?I4Tr#nc$e1FjHzf3=SsXE{od}hu?!L zB9$tM?uMv)(p2uozN7n|c<hMG0H~&Om7j1S^b70+=e?j2h_D|xKR^OF<zh%zF1dw9 zozLGDQ#lsh9lebA6(5?r1<Zt@kg!ea_yIx$l7z^Imot`fN-J7KEkY_R@8FVmQ(DRo zFxYAMUfd=RY_n9jXtzJ2H?bwnk->v79$p&~V-m0kf3bqG7TRY=XQX!@^V;s(z<nsa zibU$^2DySBnK=Yfk^V=aHc~x0{*lhyZk&<le##d?rdlaW0L%dGc0c9Z9mXp!VVpr5 zRU6qWNhBrR9&o*8IE4p_<_n^M&@Int0w&dk8io)PpBa<U(W12i(S%@=afUEGWN_S4 zBpwT!gkVCBUT6cK*V!mVoUVdC>o{Nr54K5R^J0Q(00bu~6hS*kQR3R7Bnl=Ym%Ftj z_)C&+PD-S3)0wM8HMgg(g2;iMpn(9M5noY_Nr<ZX!NaP2Ce{ZuYh&KZ0oeI1dMS=! zLH(c<p6seIvBJvE$~In=!_@SLNuv%b#Cfs}JlQ2vV7NN>Eea3fvBT_M9QFgY1@Zb2 zkF91sDXDOUjS7}VHuE%hrBrL4rfa>)b_<k$hUN28Fpu9kpc!LRsZN@C04~>1v(0MX z_?qC})J%q%SZ)ZF&3i3!sm&w|N9<T6@y`mP^7q7ZN7RD)b6Dn<sT1TFM5cI<0wb<Z zY2zexV-)4+=%jEiDX(JxM-kFZnbA?Qn{<+>9dc_><j1=UHHPnMqaAfiyMUuj>ZI4w zKp}CqH1b-q3bh`1kR%1-v+nuJbqH;<X_a_CFF;);Fo@OF{5cXUZz7A1U141=^{*P` zQFv!>JbmPmJ^l~iC;~sD(@2)dl|z3e*8tX4Is=?Q%GG6KSHP;yLC>{^IjQ~Sc2>t) zqqNGwaIGSbD@VUeroN0I=IgLaNGIc$8D$=$Dk`orsUfvkxF^6^9!R<4@W_s!E1YVk zSy|C@lx#FDD7-+`k$2<GIp#-Wd9dD67*@w^T8xJsVp5+X-dw5?6^Zfh+VTO83+X!K zX~i@HYdicQp@OEWBig!Brj5HKZd5E>CH7$W<cS)z3%7-^8Cj(AKP)DS#MFuJy%jgw zsVOPWVkhDhS0hs;QO34`)|x8cY>4PTUKQ>tIyQ<15SAa<QrY#py9`v-N7?VlXcUh0 zfJZgT8n><?k|4VAZjYv#aZ#YRm9LaDmg_XjA=G?<51K-FsgzJr-2((g80ZDD(z${= zz!7qka0dF3U95&MYBMp}Ea-ZSHkgjRAtq+YDhAs}V?vlu4m?M09&UB0(Y<z4D4}06 zEP74^fwAi@`U7v{RIs9Vtw_tjvzg_>Jd3VYx(N$x_rTCnpgI{wY&rYWEMxlzXU`-G z?^na>2wbG%ELc1|GA)i<4RvIVl^_qC0KtecV(9NpH{m-LC}OZR#tP?~DjZNP^D9_= zJVeI>G7POe%DC|K<tp4eiQF+;vb!uk++1uWI_skD37~LbHkGtWu&%T~KpJcq2)zrq zw1X2szwtu|hy#>E>rf&Gk4|v9M`e<AFbbHt>dnJxWI}I4Za`r;=P}PRmf1y`BYhOl zv&x<rFq5#e`*p5O82>n|eifY-9Xevy!1_$upyzgYdSDMWX?{fE#x)O<#7(wvCl{kH zoju;bFY{8KLvzI%wdg9Wa?S^bYHEU&NPSKV{SgYmb7nk@RvEVsr^Qw};sPrWN?;t0 z(K~&nk3a+dKzJ1xZ1z$Oqpi1lQYp8CGu3%Taz$`2>O2P~6v^mOmbIZPgMdjQ+zQxZ zSA9G=&Y8q$n1wKmKI4#->JG@r27D}LZ(v18lwPG<`abUlP#XK2o!d@?knkyzKW!*6 zXmp3MGw!DSj1xm{Fx770qX^B%gVT0vA7xU^=Qyeet)~ED7`Bj+GU3@JOiby}ZxM1? zTV}OU^qpI-Jup(#*4mzlc@PayXbPp<sVze?@|yc<k>*`QUa;6JRXy^haK0jM4Id4y z1rQ2xDuziv0piBej4IuN=2UPt4!VMY#E~rBjeZzI_p`zd4vlqn4QEcaGh*xoh_QX! zhT4q#nYz)u9;G|tJFkE;%|d0wV6){&D^t@$2x6bIr2~RW4q@=+buiJKY4fnMe*F^U zV&!QVDJ@c>fG~+HnJ&kE1;2}9aX5j|<)oyD)zuOsj{4mx3rPqUIykVje0617#B&>% zpt-t;(*pWE$Hh&{oR*6iOB}J>DnOq@88L^DGSke5f<=S_wH%b_krQtj)?BK%TOQqB z@$)DHficin;(h2Z!lTlQDEUVNRi^{_Igs>tRc1>Hb7Jay3HDhzRcj`;2uJ;S@U~VL z@Wg>5>Y>TijoXbi*g>e$3@!o|BS>S?goJ$QEPMf9xMb5v9v0KtTl(sCW2+Q)#^Fq+ zgecj@j<brQwsLBpb!UsL69Ne(tXyW?dT@F608SCCHnJPKzC?O@?R!q$YDDJ{S;0+_ z*)vmoy;;DD<*HS*1&%J|s4@)U5!-f$6Tsx-EJWSjZl0MT4>L_{^Bq)p@tEJ}BY7}G zs}5k{Rvt-jIFexwrBXIm53d7vAya{k$hY$PwK})jgB$`=)(XogeI{GM-u?C|oK;}A zw0|lu-D@BVcr-h{aO604Kdk?l24Feh^AUrU9zeqs!HmcQl<z7F9<BzFWGco9nxdgg zXZ5xb`d8?doHkQ=(epeu6US6YhUr1^+B{@P>;yO|&Sh&GJUJ5cWuu2&Ki(WrUBrAm zyeWs~&g~?>Mp8(>>CRQrNu7n*^_yH?k0ppuktQ^~i{s4EKGn)CPH47gqlQl$0dDEB zNnBs2+$QH)-<=XO<TE>m@$e@RMK!ou6sWT026YW0Jpm`Ww^kJZu5IjGN^|q0ENMa~ z8{}VB80#c6S31<YYx=ZCg_QcpqHmcq0K-^vuZha$s9<MDLZbg)WqgW=QQ&hQ31$#? zBsg0|4z|D|KXd?{l#qcdm6etGNh%>K9+jA83)>@MBFL#=3r}XXj8I{x-KxZ?S{dj@ z<-8Q}rOih_9{cjHw2wX_{=&mMNcU6L3GkpP{JYxApjG5AO9zCrmK@}f`pEbw!hl^W zHmc39DGYqXmr%^q*`-{8GF+W|5pCwfxWSfqwpN?TJnBrvwXR>(zv77zVAMcMo4%D{ z2S$Hb+Y#6_RDB;qWN_1}W1mw&Y-Wy0#XFVEl9_PA0FeNG)8JZSgNA~N`AZTZPCWuA z;*}xVQUBnM7IfXjpe^&RDiTT`Gc1TGHOM`>bk9=}`VhVw5+&OrJ#6wvLE&@g=7hc) zGD<y-{gtJK)L0npTpbn~+CHd5$4M49^bgA|O;QQB(A$+i;v`1&E#u=m`GQk=ClR+U zv1m8A70o5aIE*DQul1pRk|K%$-8OqhrBfmFiIhW9h6%in_&|5nqDs<o6axeHS?iqn z5|)ldl5&$G{>zXo?3ZIhzsS6Tlbb84B#weUX-1`!cB6!nJ4!C+=<3>-b7Bm`JkT}T zTt?`%K}balI6nkIS6gCLiCZ~{38J_n#&|}HuTXhOE5c;jl9+X+!f?5oF*_x9*9&3G zz<FHM|H5^+IJmMBqb;i?@<KWzoX}>sEP(TPanVJD&=b&6ZsiM9Lad9121)4W{&uH< zVOl#u8BtpZJ`Nqp%)v1+lFDFkiCHihECLN=gvOGHCU)ovpx@2IOqOXm3vr+pE3iP? zwlkeI6V4E08x76G8R;0IWrS@2z;K{ZGMc%Eh7Z1kc`qXArkb)WQ{cbF34j>_X%nO~ z<k?5Hk8vdPx;xQu5#wB>+C9Tmov)ObfK6~x4hL37DGxXu=Q4@dzb20?o5o~AA4WYL zXGs&3#pgT8;%-G1qs-NByuJimy*@bZm)WIQH9vWL0wtrB4|S1Uife;3MRO##BxiVj z-50BN1w_XFI*37oS$tw%1<Mz?m(q)M{GDZI%V2J9DCEYgl1SI9OIBrG#MB_(<7}1T z7J0t>Y7RiYl7?oFbFfkSlbg$i;3%J4XR;=mvJ$$cGd>uWj)rmm!Op>QSRZ3@@o5sD z9wL~BPSiHbI8IH%Nm1JJgej1k(v{wb6C>k-3zB7qc>h|-zi4NHM{#Pf`;anF>*Pv$ zjA8rE1$%n!t{J?-O@Vy{mMEwY>33SkqVs$ASd%rURP0^AwC>#lcGnk16GV)h8lu#k zTO|!mAhC7jX_K=Dr=U6VF(UAIG0La#?|2f-8I1Ip$Ff!^vYyn<%57G@D%sbS4M5nm zR)fHo(uZ{*Gg22?;I(uCGcu&vlFV;xJPU52=x+%*KT&xKL4q+S1e>*>5je_$L|LQf z#;`@KU_3i-2KkbVN77#ShxR%!3hNt`>7ecg=iG?iGiy2S=ejpS6DJuRBcV(ZI5qQa zhs_3PiflM^-EJTAmH<==mx|EZ_9(u=!$rpqF-xMQmIWy%N81hD*M<Z_1dUHiGej9J znl}#qga^gtNC?Fnjuxo~qH|IVL(R?~xPHUQUacUXAra&iW{F4Cnnv_8&q*v@bRE?X zVYhP#POGUQN}8Sb9=Z)-?2U{xGa07cDnJa6;AHV6r-eVt&JO7B7Ui4cAltK|0r`&V z^EP3#9LsZ-Z$?XKP$N}Pe^<)pYGpm5787z6C=G7dHL0}+`%1!DSb-qv4Q)V_jc?Vc zOVW0LFv#A3(4RXYzRJIicG?;MMy0@+$)u{=3XdUFX;fzwI$wqT7}!filir9V*4FWP z7^zy;%J}a}oT^SDBn3l;3?A!n3igC@&PhlX*R^;KO%TwTQaVMLyU;kfDEcjeR+vTr zpFNbLAojmFp&uZrKLFMRpLuY7gZCL}IM1o1ivXg-hILRJLF0^~>R7~FehqmFYZ-%Z zcekcJpl>sfQVf&0p9O=I=~s3}qx<WQHakWVow#_S7^-t^;hRbBdJH%vN1;IKRNISo zV1}oLeeNviuv}z?bES1cY{4%KT$l{P?%LEiVcP~F1sm06!ZDGo^Yn$I+4@w@(xi%z zfK$1c9iym~pmEc&Q(>&&urFH;6Pz-6<n_W9?s&TiV8P{4vKwC67JbE-ipiM1IzVgc zGOta^1Iv&@t9TYT3*yTibEoRPSMgPy9uU#!A>+SXXzUKhnwax2K=fG9`SR|HGBM9f zZ1u*gO=8=oPzAggqMuoSHgp6Y#de`X%e`1mh&{9e<Fo0SIt9{W@#IJD($<+@U76e7 z`$##eDhizo3`yNi&JSsiy3p`%d~CJ4BNrtcHh%TANUcsSoj5@&bu}Yov%2j7OdFVR zax}C!tvgZxQ`r#hPC+lt)w^au7$=~^m}s|K<fX7RV=cpQ1+=_Eqjv>rg`as3!|y72 zHw0J2z`kuCr&1ONrtMs?7&A{x(E}@z%6!^eyxq}~ZRiQ_!W{1GVb0MBRVYzbXwbpS zbi!-*4K$7Tr-$s2idniYY6qLT*-5s{v<(-01b7NT_clD0qezy+r@R5LinQbS3_zlY zWQzPx;#lOE(IEm(LLL%6W*RUYI<LHa8E4w`j%f2AsS7F{E#$#<88(LjB`KK?msIAy z9#doL!*IgTVSRCR2BqGaPfARwt%-toMhxOf;xsUw8Y#8Y)&(SGr2-S25V;Uy=ygWo zdYob*C!9U%jLuh2p3>u|B|{nx#V!Rg#vd#dI<kDT;)w%V-Ejcqof%bH9k%j$ryF<K zujjCq!qL+}N?!X&&9gX8Rxx6O(hrTSwW^Z{u+u1)t8rgub<v>HV;zqmej#sVa)i^) zT<aFx=Zv;=2ki(H>#?-{>Rpjnj#jskM{7;}&ZY>+51rCq0oEcDQX4Ua$wcW#iI5LS zx2l0|ib(+i)p_&g04vB;<4(y5q^z2FYPDuC<#Bvzgp~r5$;#EttBp+!D%TR%lhA8= zs0(g0?u3l!I=Zl>n2;N@L}wMsM%@}YTVmxFDoG9O>(P@w)~AI)q9M(tdn9>jEk=;Z zCOOm@PjYZd#Np|Aiw&6K%FenRmRLNrxpI}L7RL7%;aH)}?+T$uUU{FQUx(iH?y;LJ zCWv3upCIPNGpZwsSV{XlswBbn=cr*p!#AG06}ikn*9DS|mo(Rufg;PG-jUeA(*7@V zfLk7$*1jwXqESb%k`;(;D(}MjswysPHEk<BQc<R8;J01dW$>fZVs9x@tKK0Kj=N@T z84GxWQm`sV37M4nClUX;qaL#A%g9|*Sz#>zaHca3f01E)FhlJV;_b(dZlZ5F(yQj; zL3Coin&b2+3EtGXtV>y)E^+2&P{Ra45A@k1VVKsErZ15qA(K8B3L*ww1#wwe9yo>t z;S7bC?+_;?LK937d=)pk+l+cbR9St~GE_X#_nF6;dy41!r)s8XZUs{v<~dhn4i#iU zBOPFESw?(^DVKnv^E@}GfGxSwFL3n4Tp0@~wqV4#tJHGqO8PAloz7W6p!M>eSk-#* zic*jkM4}oB)g%w4T+{W+B3&HQ3yjs-J@oOEPDik=81o%(Dt~vZ0Ts;UY-sa$F&`~L z>sVR~L~_2%h+U63I*i(l6*5QU2KYF;p{5*9SsLC%(=w}d_=f?Nm^X$4r%&DYkbXd? zRb0ulKQCuU;XM}a8nzW8jgUklUKDw;NK&m)x$X&?nR{=e+C@`1onszdZI`l1TFE^v zc>xv33e-!QOw5P}l1_0Lq)b_`*qoq@t|Nn|%B`?cDBj13I*4n;I5!GUf$7Aa;-Z~G zw@w!cjAH)*T<&>Do66c1wFZ_~m$MTyHK%A6#m2BSny>}6jYh;o9=Jh=b&2QM2WNC< zA`(l4F_DmJBuZyTY<}Q)$)p(psfe;>h%8#U&}NJ(&2M_OLd-H4S036Z{ks^|#g?nj z1*&~FL;UHgSY4;{rImq^&;DXc*GC=}|G6`-Ak;*#Vn;FET?Zjd*=p81l@(rK#O^7e zktxKFoxTT>$vhh32;GNn6D=+zveDP>ePa<-S^nq+8A%iLy(Q7KiUZI97d|?0j7|~V zHQCvgcUP?F%b4UDOu2sr&=04bL!ERfAy!P3)w1r9^SE4Pe`t*$1rtt>u~X~BQnjXN z#>w-Kq}biF1BC=3K|M@8$qQSms7e~qu8K!PAsd@1q=5~nSBhAC2k3#h!eS<FR~SRJ zq~&ohYq5H$<aW|!abDA`$yoGoMSAY6TwrB6u2^Ywg@WEdTN(_ZKBg*yg2rR4oXidT zSvgei<$}vuQRdXyCiW7Kc`&^3R*S_3nJr%T2s5p^+MsAMdy)X9QE91O=Xlx;X>lIg z0ndVGnwm@m92=-q8jDht;8SM8dh7=4I;mtyV3hk+!)TNxE5EnwJr-~${apQYT;AA| zWqQhbEg*T=U-y~wQf5ix5h4?K4Qy9U&qAnLM?CM6T#9R&_xdXI=f0tvPz711ereE} zrdSn`=9SB>gA9D|ItNh%W~{?=*T$9*l&#Ycwl~4DVNKddD9EdgpQ_cR8TWFl5^V2k z1QYfV$3gFHhX^g7G9trc;EbAa(()c=AeCJnNaBZfhjVm3cOOhMS7;~b2UJ{8a&}}A zC<hv7cytw3o2*Xn1$?0G@j`tM@KW9yxID}bS~Iqa5!7*K*@|<3E8tGaMbKL0gvOqr z%mOi2JPqw7SdMT>cqKK&sawxLt?7J$AS-_Bu>BALNtNPsGcsUwm!3J%Pr#4|{2a~U z5*GWdo_7;1HtHc$vW@Mgz==!@Zzi@(d^l!nZY?vK4qXatjH#7+@Yoh*2;_cmE5K{F z@henOh%&1Ot7s!?BgXN_NTpF&?Yu-m6%TvgIkXWHd~<~49gQ|h3u^UK@oVf{^@}B5 z0g}T?^slfc(4bHTcFbL|&~r5ZvgE8{WAS7Vuv4xDI%QP2<PsFbr2#TRYOfBPDvnea z-8OV)=I}yoC&jqR+ua}}MN2DOkf29|2*{H}V!Py+7EidH>S2pSIBPj=iAM~id#PkC zG=>m*k)!VXdH)2K0vaBKbtk2rb-}|pk<-gPWk&Q>bQ3(r{AOM12FJC}-c6uq2v49+ zz1Q~dsCuBC&MJ~RjCDZqM;@`>U5aqk!)XZ>zwrrsEkemH(d5v(@#0eSk<zUCB@6^K zy+Jn-5PLCYv1Nu*BkRX<c#IGfEPKk8h#<qqT`_RyLxz;^v0G&|fpfo&4okYEnD{v) zl&KND*_p9Tt&(XT<mLI5%*rmR4!wnREsFKHzIHskxB7*<<w*bhnUUnn=Hcf(e8DsC zk6OFS2QPf(5zyEdUZ4nI!Na<Mao%|)fd{yM=s14$@C}d+x)OSyc^$mu4G&+s_v(i) zdF1fnmp=Tf4`1-oM_&5qOGmGHc=v~2z!MUV*|3iH^-ezS;p<-OOZv||@`(>$_Oh3K z>c7MP^q-gRT|GQ}-6vi4R5AYV`=3vK&Gs{1ytg;`_>X<fSL5@s{Xh4Lr+#d2@9CHP z?k_%p@1OpQpYs`C%<n(?6R*6F&u{sQKlzV-9>4#`pa1or_Evm<{3pNo1%Csdix2$= z&;OtC`HFY{*z?|i&-eaw-*xgdKEL1_KjI_b`r^I4w><HVH~a{`zvtvL-}xGRe%v4b zrhm=%eeY{7e)a*rKRft{4?F*{y{EtJw_f#~zx3k0$3N%WHh<!u;`bMQ<xjni&pW>N z4}alXas6NTl+XCt{}$gr?aRO6)nA3rr+?vRKK8HU^X2b&%d7u3{{GmPeDFKA`2OC1 z{$oD=X?(vL|J%1e#P_$aKlDu;|KlGu|B-8aj^6*%{_?ls?{EK#|K(fx{jm@Kp|5@; zKELk&dEamP5_~@KfgkZjC-~g^jt@Tk>=)y@FaC`GVvEnm-tXoA=8%8?nZNu?Uykp; z@st1T$*;ioH~-eZ`==N`Ui=;J;<*0huWtX~`|$VI|M;i<6RzK<f7Ca8@ZZ7bYft~- zU*~(C`2KHr{TiQpKmPLPeJ|$y7heA8FTMT6dwak5=<j~XoACR`edY(f``hsOSqFdO z{tx1F@G-yj?3?kq_vUvUd^SE`|BY|_vA@ahul=CGU%}_IFaMr*z52!Y{$)S=qo2Xg z_y71ed>a4$>A(Hj6~2GKfAZNs{I&f3SAO+Xufp%o-kE&#tMU2t^b@{Y`~9n*`Ux68 zF8QPX?;$Af?hk=j@4glPAuZqiQT!u``9Q?{6ZqwIpY-1U|HyOi<mC$t^F6=w&%WzT z82+>0@-<(`<=lJX*DgK<-=BDV@B6h3Fa1A1N5AjA?%5;!{p&vegWmMb`1|9}|LspE zx$V8^fBSBd;|Gks;7dtPd!PJKf9Hkx``+W9^dqn1_YeO0tG~(b|MFw+AM^X?{=nPT z`2O}6p8ebx;q%QO{MxT4neRP5`mQ5<f8x<!d<Dts@%Q_K`RC&EMSt>37hj3r-~NH0 z_$!zA-1~u7{L;tb_r0gT_KQD{pFjK0zk}QH?5BO^dwm8zU;mLG_E~)2J3r(_AIkSV z`;Pa#b%xKqr+@NoFURNQ$A0<Oa9;2E%^!TtllXl6?>_RwZ+J02Kjt_8HMjTmZ~F1x z2EXHtqet-lMT<Z6e?5cG$KLPX{WRyl_iG<|%5nVOKmYU}<ow<_c-#Bk#pe^h`d_}~ zZTS87KW6$q{{)|3@S#8VJtV6ae*DkB`X})D^ndV{Z+#m+Kjf+V-}8<5e8KPhzyJHM zzj*KISHAMW<uCL1H~#W({ylvD-bZ}jr+ha)-}&p$`)_^_pYQ&GU-==CCx`S${@))$ zJ|9q#&(niH@>A;zpMCv5Ce=TF_kBNy%kji}ef^uh5X0Sj-hcG+b9{g7TR!ZR-1@!K z5Bfq%p}iM;!Jp;zAM)^r9{*B&e|)`s-Dl$S+4b-K_GjbswV(Qq?<cps`PW|eg-_sf z@Ap3WNs{GW@3a5YKaS5Qe(Iy&vc&Jd_<z6GzvcZO{yXpbKlu6d#i#B|nmqQkU;ejn z-6uctmRG$2zdyGBXa9iD|BWAc^@sco{J!^>{><O~ukiiZ5BSkfd<j1Ho_OzX|9klT ziEsR-cR$4US3JJ_$6ticKQVv!mM_8YzxY)zdi&qP_sgI9M?by7=icLQ`k(mwyMOvW z`<^ey_qRX)b3c&MYVYIcKll0g{n_6?dg`V4+?y^w;p6%F#Jj$UWcY&5dE&=8uV??o z@+<!$J|Dk+>35O*f8#&@{?EF?_q~7lC%^8m^7HYReGsL`7rgV4{#WDk>3{G!pG)cc zzdrU|Z+rcVA*G-Hna}6*p8ncT{UDO*-qXLf;CP<>i?5izlHcF=&;0fW;Pc}?_uqWp z_Q&@2{)gW=eTHQG>`R~j!CKF~|Nc8p@b{M=yz;BKZr}TxAMw+C|4;qQSAW=7;rBPc z<L7>u>-F@@re8?5+k59Xzw0aT`&(b~9dCac-yi$)Uv|ppKK8~R|MJhm=d<tks!#YU z`1{^pcrgAHeBXQTuX@EB`2FCq|B?H&_r~A(`=5aC&;HzZe)xZl@9#eSt=GL4pYQzd zKJ@!u!N2c){X0GZpYM4v{e6z-@pu2sH-FZPAx8GU?k{oQU-7c<`TNh{_p^U>`c}?k z@A7j$`L+1{_g?j@zrgi*?0x>#GcU*Im;CzAe&TEJ`?J6Qr(ZxZ@Sd;P8){#kzCI>h zz5ShE{5EQxXXlT6^yB!P{ML8A^;Iw4+xu&;n!IF#&u4%6Z@>Rb@VU45?PBX+zx$ED z{Nwok`j@}*XSh#)`t4tKK(_t)pMTGvqW1W@-?{giFT&^Eo4@~Gd>#J&`d6Pm^VRtL zYcKl$m^-hiD6**EA2UWUf{5BG2qG#N02QesDM6tD2?Ao1bCjf{A|r@2h>C*P0wR(P zC?>j81V%v-6XsS77zQz8zI)fKyVk6AALg66_u=yLbMN!76ZZ*SUDZlEB!HhjY4Lm~ zut(<7mL#C0^5c9LAR&(P>nTE}$zI)10k+g;gD^~QI5BS|_HX~rT?aY6jMDB9unBuU z5SN|)S=SYCHx#;(_wnHOwi;K4dO4L<sz@R~V7MPrePz-k3c_jAuUFT=b}AgDj^zwi z>g#U+PO>N&cnFxk`BVhPHQ|M%>;MWL`;5f?keFRBjsvptegm;zcmrh8U4f+Ee+xl& zT>riwSlId0x>^kB!uIPK=vSs+L;%@>hITKsn`heUBmdRkKP*?1|F&G!{io%Mo#QIc zEff9M6Dpy4>KU)^WA{Zd#VHtmc~djncLBA<<CD-YDjMsQM?o9<?(M>EBB9@Z<N@Uu zSc&(5UvR?Wu{GEoJH!UK`qPdFuX=-@FtaXV7?9~>f^b@>oSEqt34Uhz@k$-A1=6jQ z71)xXC;7=h(x);Tr?>jK;{n`Z?dsY4-k{5S(|;@jNK9L#0)W~>d%60N4(G%=&^JBw zcfdp0t@X8sM3A2N;o{8#VBJxNqnHoB{p%Cl-H2$`&rqP?q)ny=1;ey<)c~9(+R^9B zv0fdi)!jZo(VX;i-0m#1v>4mZrw#-L1DmGpd7lXB`8E<m9A*-gw(N5ww3jk(;`)>K zrWL0HqpQTYLb_xw+vNi6==RS-z38q_m;lIpES-(Z03p9rjDWN}U4~(n-}dV!_LuON zslsNkiOeCLeqG3Cig^t;&4|A(=EvK){CpHpuzb=UH_)pWd^^VVch|Kkns}Y}^KyCc z6X9_N$BU`RmkI+v{VH)Q>VDiB_6qx(iBR5wHc!gFHW*0y?hu@=%nR90Td?`OLsUGl zL+^M5P77v4NvR`HP&DhWZGhaqjMp>e+Ua&c-e>7rK9Ck4-DD?*{PwGURszz~Z(Ozq zKVg|<>}S$Apb(eE;53Iw9AD<L(tAv=(<;9q02}`06$|}JNPJ`=*a9c*em6)D9&dXC z+YuMN<U<bH>E#KYz`~1a%Y<T(AFKO-%L=cvQQ8aas=*dxaoR7dJM}pSD6+lahwW0A z=x)H}gLIsX!2YI<HLqaYmZyi_z~yFb{>J<r6!^8Ct;YNp-=29q7yP8R)B8Bsf}5JV zQozoa^I*e(k_EN5%7B6kYUwymYZ8i_P5_zirsO*mA)DtE4ua)|$(MP4z8?IteSDvD z!1IZ^x+TE6uPx<VedJ2E;e3)=vsEACw(f9A32wK1$LRPNAiYKO#Tt36<-P}yNcdm( zBl+b{_CPk<=pM%BnHz_v6#&VX?JEM1vPnbrko08JeL#k72*6XOq&Ij8vP(B+Wh#(p zOq`7KL~>qoIUQ}b@p>5r`ma+vahdEY@u}ka;oGUhT)fCCHDSN~yT5-}-v0Vz`qyFp zKYrFEyT@ag+4m0Fh}G{Hb^mxaR<p=r^k$%N*$HI~7uM~aG1ts4Ru7xSKefw->$c$6 zAvmqrY@5%16bwV#bHa7cn+FMIU=v}>1MH5vS(RE%VAEHZYq|henoQu~u>W{-@JTt4 zHgRp_mYta~NB4kz!!Ww526V;_5w;k1O!lZT;XuLXoqkv!$(h`(q3HK~PuEd|e7rWU zwGwPzS7IM_GZR^K48wq!H|N?wI-xInsDoYiX5fb&_$({)%F6UE*msTnWpGy#IqrIb z_1WJo{C)vwU37m`9guW-uEBQdyhxe77i{Aw(`nqPfOUS4?JVCqelCW;X!Q7_*e=4l z3`z&mWlO!$rcE}@=62a%MKj#d?l>5SVNW~ETpkQ0%!d*@Wr~youSJ6`S>EZ+)tl0? z#C(XI+yW2OeINeL6G?7eb^tOjJMLk>klh)x^?-EQ`G+`;jPwm1TsGOr1Vvn)33)Wa z71G(r$95@bR}AaX0WwGHvoKwtG{Pzc(wX@wfwsVZ_xBIsDf748mkj!MS^uS`w*)Q; zgK7#}Rt~{cn0@MBhr>?vXWT(QVYfPB^~tyV!yAE;X04D=@T*J^%tJlnC4bimC>l6b zmVtD_K2GAo_|jmnK=6~rhlZ<wziw2mA`S<=XhsVTm#|mnuU(w1spPuZb8#`Q!gTAY zQq;-G#C*qLB>R=_;xr*^D>Hm4prorP4>{t(4uABUc6MRdI?1T%?*h`t#oV-_XGNBz zp-wbz@owasSNohGoi~5YzDQtxv^&F1!^@L``RE@XYQvz8C+_6JA~^Uq4l}QN%HTLi zS4dkrDhJ3kcB*6e2zOd23n87TAJ@Xwp9p^2WB#P`W8GRzcds?s$oYku7*>pT-RdB; zNvVq?*kq4}2zLcK$VCd<mw)eoMHc$IlU8Cr>=d(O+^$P6Dcnkdf3Wky4M_2oTgE`9 z`1lxe(DCjpb;0ms!V<=KV>(ea!`+G0#Z+MajAqLpTsRN6)U*IUtJ^P)`AZ%|m~ri8 z>NkdfP5SK2ZGaOre}5E#?xtcv-BGZact#-!NMC)v40i$gOW0*x4#>M>kI~MrOX|vp z-6Z?K{>IDTXFq3$;Q0T#w5#SEkgPaUwgbrK7_}nD_>EZB1o~%#&Rn<wcCf11^D<zc z=})b*K$5jn^o#-~Pp;mN_P_f3hw1YFU^VZPinE34vG#3YT=#7oX86~pWBLSFDZ<G3 zU>Newe>KDD!qzDS;vt3{lB&ZnV?M4Ohv7$>hTbWofKFGeFbtV(HqItM85!S&$AN=2 z!?!j9`QH3*2Z8M2^q!{_MDrRs>sw%t_~T?`1&~Zh59e0tx9_-Vz<ir|AOQUWpWQee z*<07uc2O{FA1}vRg3V+ddVs@8F7suOOnr?VPCG)YDaTWQ-*@(5IP?BUUg3g%w!?1* z@}=He0aCd%$_UcoUgcEO6XWFQ1n>)#Ryn$(tuHy7j7*sE*DWC{*XtD8!hMO{G~95m zu>|zAjqxe054jQQU=L(omdD|0Pr4$1apjPk{apTP{K2@~&@-l~VOX;jTMC4z8@#DD z56A>grnqH-oV<(U!2~TxIRKQH)-Dc1-9@#%*#8}Ds-Fl*PY#RPj(+xB9M>-=R>xsD ztFOtP#7#fX1JyVlybR$xP8Sp`-hz5&j43WVOlQixQefYb@7!l&Nck6k{}7&Pe_PFI zDK1~hYyJo*j;Hw8B>_l;_wtOzQ2R|M;&IkWB0Ma)P3zC^uek8pQ@3_E_@!Q&nPW9c z-Ij(msB{0WkKGj9{q$lmkTE|Uis8t+p|m^`D15YaauEgkw1zNDXAjXIVu6y<46`60 ziKoX}Aoa(`A$j7xp4k-G4~@>^aIu?SeUAnPkL$k1<-c!YlP&5DJ)fijWy-F5=TV^N zJM_*1vIqJTx%&Gw-1dTW@-SOxEl}#^vl3hlq)OD6RD+JZIy`ngko9<<jjLo|Tjj7Q zAc<TZh^roZd*qoIO!tc39gcp1zGMp0c-$W<;AfPhp5rP>v^}4$1~RqJ!*CU5u3B;R zCi&%`7GnB+`3?$cv2)fEWPi!amEx`QPfaXHXQ=t7(#`7bVOTIjM7Kk!m&GGHcMl7p zg!50e&M2CI`e`<Mti`hH!(Mw822--@n=S`^&qcpys*Eiac5TQy2hBukrR)RaBc9x@ zP`F8hM*V`aqIk^z%GnfdTDG-qTq*7}l6}*-O#uF$=C1>B)h5F3W?ih0?EbVg)T`}C z+lu)os?W8BeAm~#S2luj!wi-vIiX(V=+m|0lFTui223fVw$6`ON!yfyp1It*!9zU% zl=q{CRj!!dj#Qo(>KXM4JJjECO5G3n_<y>TUWEF0bDji=PxTc4oWs)>%dHP{e4}TL zdQo_lm3XD>)Lg^LBGA7v2*}{3OV}sM7}|C0`)@c*p6Z#o{Y4&*Gx=41dL`!n_1L%) z(5vwd$lz{3_%9yWKt1k0Fj||0`T;UWxoNxX+IKT?Oy9dPYjsXS`@ZD4QAbekZQdR# zew-xDI0zEOQq#}O2{TO-+t}>*rfu&g*6o*CG~jA8=yy#&tAWcW8<0D97wE5kinz;& zlckHAe4gctD=S9mg~Bbie#wQSKSv#a{&Btay91X8;vH-5C_X=I-_g$1n?bKzl`U&Q zRaR6~v~010^!%r<ow1#a>4zbH6f7s#hqniT-C<x?mJGIH>0;v~AYrbII|rYY)vfbx zV*E4pC51jn%`8KV^A3J*JL-)C1B;@8jALvp&Qn3Dk|oBms5tWgE@$+kIz^mcOnZJi z?&3U8ooRgVlXnJZ7J)5jdNCg78-39<H3IxhoaGACk$X;ua6RA|>Xuo8pS)Whse$&Z zw`;?}W^&ATVZGT^3w^o6ys~t!9r$_Ap5Nf~1J*0Ag!FVz>mhr=uRg9Vv=D5`1)b1P zAiF?UgmJ^0n4;haBmp0z3^1MlQa=vZ^K|vv{owC^6|z4EY`Qsh5SCY0>GRzmNPhQw zZvgTJ*-s3kAbr@ZH#m>`&ZWNPwgIYOj4{~c=vzNk(2?6o1~x!mB|i`QM?x|`;Hil9 zx>1e&Ll4?`#2wgQI^hxa2T30jq5|Yy7ANDn5zN@n&848-*~bZ%OCOyWhV|)B9%F|2 zkz|i0n!paF$06B3p53K6dnhR9)m<lCHzjoeDY$K?)B8tYeTm)DUTv^{R%dQ%2X3{q zecb_miR9d&e4zig;I2yW6W5zdr-3belti#zPC><^i-9Wpq$`hrpPw~OD;CJOT=0tr z|J6q+S42R$ufq#)-q3|ATU{Ytq+{Nj2UPo*^D-a&<k**!dSJ`+bmzJN1<4OQE#Xw9 zG;7KF4oH7`$@3M~C%gA|PXkb@a);J_NUsg(n^XtsYO;>@xZa2s_q>XRbl&nQ`znB6 zg<I<|o$<;n@Bmxp_2P@ZK&H;sl!AGny3>1<K&;Rba=&LOH!sF%s5yy46;?gDqvT1o zhA7O<crb~A^cYpSv0|7u_Fnq5_^I}`S<)BvVBS?&8TmM)URCnNm0G&ZZHfCpQ)*kc z_QO}x)x@?oH%&I?d*Stpmqtf}ZFA@g<t}asQJ9mp*??*ZQTuiNaTultpG~4E$iM6T zC?l*-X;fes=09#?vLoi73s1_UevR^|5X@ihSrwnMv6*;5t0)}nGo?p54s4spPbxjc zQ15dO;MOQGv-;XJ%>PIBCOODYb?6;k7c5^(KHLxWKEsVX@cM>A#)=A6Xn8;5_CnOh zY}+;#)*+ks2{D1FKM@%zfPN!<;|E4!0>btS7j(CE_v@ypx>L=N{?u*Kf6-g53UL)n zw6}HNS~s1`U%pcx>mPaZ?mTE`|M_L@?o?K{_Lcr`(&B$OZcO;wafAI&A0F8=QfqM= zCX&y#7@ndMRXaTVGw<)WW7v?@#XW{VW=7)$3^%r9=%$T8*l#y3#Ppb<hKgVZ2d&s2 zg7!GOPwJRHbFz#9*pjvfQekK_eFivP;fwhlgTc>i@L7Y)6K{8Y4TdixYq}J}Re1e& z8h5&8KXJFt7(u5UA9Q4x+gv<NGG6wcwm>qp)DfqRaQnklu0H%vOSXfKSw2RD%b@7h zGT%s`jKjEuG$6UNiktS5PxcUwU~8MI6@j09%aip3CYP*xod^Elo~|d@PLl3KDFI_< z4$T#!t-!b(0tzpMyJY}1z4Q~xC|EYWUMt@ZB<1(a<^Y*#+Xisg=Y|>Af-P{?oQ(CX zNZ)I6L=1=HyEi>kfP%tZWrg4;9mVyIV3W1gpRs(pe!v}jOplo#5e4b~PyNzxT$mR> zRNcTYGmzEw0Kz@+3NP@py>lL00S#w<D98W`{gl^n+f|^_3vbZrsGr<{^(CTx>e-M^ zyCqY&4401nbu1YuGp}WS0#MTH-Jb*Lo^<cCqhO1IUN?7xJ%6!>#z8UMxC=_PWPwb@ zBwh!k-`!=k13&Eb#eIn01SI7Ls<B+jnV?wQj#{(&=HfWEWyg)h<&+gX{^A2{kW70Y z1%8t6>5BcPuH&c@4R(65a>7QSy~ao<JQmUYU*fQxk}al7oq$9puW&n1J*ip)ukX4W zlZE=iS<lDkQsD2BkHYPQS@v+40Q`jPvcT!fj30Ukw^PyO`*JQoHf&n(M(~q0^TT1e zCCv0;N4WY6c2C_0e!;hmt})=3y1H@#w~ggYki&Awhe0V=5Bd+@IL>C)R%5%Ez}F@i z*L0sxBgPf)=CoJ_NPf4La&Zs;{Gb2*!*rkfx9KhepDtjv3|@Y26|8cmIehIv3hZwg z$BnRfVb*mWSe+(X-Mnjuwz_oQGF%O}zKrt(TID@%PN86Reh{#89j0r0uQh_zF=eaQ zVs3c0IY(Q9E<@WO1W#vkXr<;H)VJ*E;`G<vPw+<l!8`G|I}`KOYfZ7dXZdTRF#mMt z=nM+#Gbx`n#dPUGf4HE&WXvr*U6AHcO{llg5F6WrzT)bc`i*G!jXlNf?s8hU6G3mY zfA1h=3iLlBSSv`MSo-K!0dOg+`Ku1ncbIFXV)=}w--!tDucuj;ooIhFnuVtVyXLn3 z3lO%}Pmd340lVkjoL=y=O%Db+by0-8{n|VLck`Zyvpxm`v#a0j{zFXYm-9SAE`q)K z-K#VlPI~%FS=96NH#%$p_WAl$d4k{Y;P}iFKt_~s1*b3n+4YZ*KchT+)A)0sQ|XlM z+k)wNUF&^-A0Kr;yh#xzNyW>n6YPAYH@7g%WT@X_B>%y?);&PMAHu9ivCREdyuPi= z#qZ@!Z|okKDDL7rkB_{+h*GKDpBFRAodUa8B|nPt*4R5*X$3`{iyfSpk`X4Ja>?)Z zh41lH-fg~Sr?0uVcK?}()4!d>QKO%EST<@>`Bz-WUHqvdrV_uO+dCkfg8a7zcBP5^ zRchB2vwD=Jv%vt1?^c-qq{xskN`3Fer?V85QUA4jekkf!t9GYgzO#*}`Bd?l)I^Uz z)}Y=YsyQ6>O>!&eisJ@<sk|-Yje3QX>dBb@;WH+NsMi<sEKonVyC4Gfi%W*gM*Uoy zm9EsYC3)JCu2|G-yey@;`UG!VOF?;2NznmVe+T!$i^bU!r5;BvaibE{J8dRj4950$ zZa85sHd$_4KG@4n4DHySHPlZWxW#&G58Rvq{n+7tK@=_b@>1X01nQEX%46GO>%^r~ z=YAYJ$z2Tns&Vy2jQB*;p_nH>t;H7)Jz8FrxmrBPrS$TOV_vA&D_CkT&W)%F9-d_= zK9l<1>SNq03i7uZ*JLPua^vlK)fe{SMD@;R?PL?`b4E%#V14X^Zrh0sH;w+&<&81c zC;X2%6Y<hd(Hds!L&cM%W<B__QiHn9ciz5jfI0=`Qzz1Fu)VAE?cAtYf7)+}s#}D% z{#;LM9N%5(%^NZQ4FklMSPywOv6dM6b&^`eI^_KRdPA|Z!KRdNdKR4ikFz1zKfSFB zS9ALLiAzv-oTND*1H&uyvHdk;H~L|G8J_Sq7lVH1mb+X&)p0Sa#0Hy22Z*;DVmsC+ zzSF^UFP%a0IKDULgd}r%1z!U!H^AZNN->OY#jXj#RN8y_WzOT~VfuHg%3vyd()CKu zOTJ>Lzx7lz2OOt&>CHCO-iu+H4ZX?~)TejrtX2O-zd&_u2-O@msdIQwFviidgylY1 z566J30`Y1u4a?{2*I{|DN5wi~d%_=us-rG_y=n*-&kucOV|_3Eu9=Pfz4-iof9iw$ zrOO?z^3?s~0T+fxTT@CV<7)PGY^HJ*P56rqyu^X`jt3ZiH4_82J1A@rp9r|;E1m5k zhV+Ls4FiG0WmlIcQ6{;HQa<;BQ8#5*qCl)<lIwrW!-j(ECqAvR6hrz3e|R9nBscl= z=?8gGPOf0vNoUOeL1<(MUU!#ONkH3U&nQd0-ompx8}fhrYx49Xn1591pIrWLKi%5_ zI?W}b=ezN`u{RcmB9mk&2Pp5eiuJ)%%;#tG6&)<c-M7;Num9fuIvm@zr*k78+j~!N zrC;|%K=k6R!*e;ktIsk_@BDr|3a>kGqbLb&-$SkIkkhVYdV=nAdNz*{gB?{d)(v@n zvwt+^f3(DX6J-pJG0xI>{h0^+bx^NA#|CM{VE$*bG_@#2lib168$D6~!A~`UtB2(3 zRP5K(7<WUsZruEL??Ul<rZ3zX{hBiz){CLKnjTLTFrR1Tj`m`>{_35J5y0fkg5`TL zzrGJs!%^RSqCN>3=H?TD9GK(hiTc{<Vc}5U;j-DT*{B~-lD$TZ{m}l<7j=2Ml^Zet zA*o}nuwB!%*13tXJ|>-9JPh1oNnv_G=5qt259{KA*IyngOXKxKKQ@-D|J`-*6kN~D zGz&o8pLO%YkT0c--BI@>?`#m($K=<{p2qFaA;}~6=wiO#WktGZD=vMm3+6q|v%*G@ z<(5v*)BT6zgVf)S56l4ARR8rKQxjm8MZslJ`T=W^PiK6=Z5AF7nzjRN$??qHXp5#v z;W2_fnR_o2{eqJpwUL)*$>M2@u>GC5EyDAPq7d|}j6aRrFQb-~9*%b4wYzxpl983W z_}6J`zHTh~N$oH0Q%Q%_>HtU=Wv}f$3^X=o6K_De(5Yh7F0gq)qnDlo68TL(s?pZ7 z?c9$xIk*qEX*QRzl@wI#)X;)N^lR^AUBM<g&n>z7r;cpJ)11ipkBO&&qwXjEk^lw0 zYr|`S!k^!Vy`n&``21oW*i7pK-$THE_xBIOCI7eKlKk)Ia;k2@Jt;!`!b<RY20G)? zCGNR~V<CGu?wnu2u`FmGQ0DlJ?gLQG4lSQ@Y~J9?<uTaY2JM5}@UT8%Q2ICQj)q;| zLb3Z<;nG{DDR?UI#ge%ffbIQdL$O)184We%z?(Y8edmEs3+H5J0vVA)!!bDR6gb%} zzC=MeL*-ZafnW9QjBf14h+%yP>VXsfOc$q!NuR!M?c)~|34Swu;f!wZFHv#6RS7(r zR4Fb3db>^KV>vU62M)>s%C9+`yOV-j%lle)3xN?g8ya$ePX<rV!0@S?JwUqzm>;!; zjQ}c6@wY96bhQPmC*$=!8fEGjK7y?G^N@63U%NAqT#CJ;K|xrI%Xe`Ds+9ZnW529E zx?*=C*vyeH)%w7?S*B0$I;Kr!u?yIQ-O`00s%|ZDeuk&a@*YPcycfoPs*Kizbp1Z} zI4ob(_E~Nvkko~64@*}S(4N>|<tk(AT!Btkt->}#dU8jkLOM_-DN-6gtPrVM?m3N{ zB`;4EgHCGi*e@7{Pu6)X+yd#H2O~_1fKeY$P4own;1AtiKw)@eIo3<Y_-x}^V4K3o z6Jfygn<J#+F<rCvyA{|(JN{}Uuwh1bGM3-BKj;8XNA~6=Uq0CMk3yjZ+CJ?L=3tZH zeV6@!Hx^VECV+nw^@#gQj0n?bJcaoBk-Pj%GU^o7AI*Vu_^Qzu>yh_o+Ujgz(`|!K z@xZDx8r(g?=Z6QU=TXotZl<3sfrK~v;3gn%<e+js=(erjpt=ognW2Ym(0;XRf9ZB0 zllk<VD)@=<kk$k^<+u38DeZ+%4P%xy$>K6mcsq3}e5|#3C~G^W1awiS-aqmLKdHN8 zn2Gvtn+NX#egBa?eC)@CYI(!(Q^Z3p?H83`>&%myf#VEc&5qLn-M8?Str?I$(rKU? z#+yvc@r!7$cfT8x1U8x0sJRF!@^K4=>ux0e`j`u(+xE5NrvqMg(P$hum18zncYxiu z<(53oKO*XoqyX6^OZ`(JooTaviTzD{L>l4fzxiq*UPr$<EMt#;qA>>hiOFI|?grAf zmKv7eXO3;r#rdlJanMd7kiX$jb_ft|7ff11L9-o}nc@6k+!xl;VDnDyf9VbsTx?z$ z10<IZq}wCOvb%9&=&w~~?=Y?yjZ3T7fGrw1xXKGC)SrJ1$B_>Dl7sV_kd5;;0~-!* zPb#LMzHjzUibB7#Sdxci#wPxPxL^*?4a9i?e{?Ok9wkQNy>z%+o6*X#jA`TiyhHVW z@k{AH?Oy-)4@!{peeQWsZBp16l@DZD`?DC<q*eUE6#atp=l7s}_q1{{kpBHJUIhNC z6Pae)fi(4gCpNe5zQqE(=RiWg8B~EyhLkMAFz01g-9HR$kh#={VInbk8ji~nWBT=q z2iRoza)ralx#L#j@WIO+u4zEtYlkBkM$F-7_SWcMk~2Mv0+RjQty{uotXhKUDz?Kg zU;f93aL9%FhwnM!2V^Q@%=00g2!hUX%R;VL2mRzpfu;xeMa~g#v3xc_P1X%;)_K4l z3=dKs_8=SlaMSoK_aWVs?}*bW|DD|$9CosO&S@h|ugMifBkQV5ak(PX*B$Z(GGfUF zT;?0}pG?UDGPc!9j^JnYYBX`05#y#aJCNTW%LgGX!?UnICIwy{QUW&9%J;*5klIxm zeI7`%P1lRTPmjC21D7dL!`rLa|NK2IY1m%Ezy8z${E`<<Z*~Dm>oz6qKXUr5hCPtZ zdhAdG<mC)Kg!N^v)o}OXXyyDzxLixrmh3zMe)dY!I{Z*Cd{?g849t1aO<w{Eo>d*h ze*7UV-`+|=dTz+KHlW1ESR1D|AzHpZXe$kWirWtJGU@~7L++lt5)6JKSsGJ@RNv^# zjqk!Mx3N9JJu{q-fKH%bas-!k;gXdxSRTn2N-je>@eeL7K>J_){ln>D;@?gWegEm! zFgd}m;C8)^&XOFcret8vY1~y<%?UcV%CmC@@wn;3kKKWG@JrjH*v%wn>G2v0_(2@9 z32gPmPL*_^;M^ej93WHkJH!VV<(Rn@rw=pD@ar`S(%bAt<0{-~tUE~zl<ORjd>kk# ztJ;gJWu9}7BR4(Uy6zvKApbA&=W#fq&W+E*X=zwwu>*&x?(obU9FCUj@|WttW?oOw zybrdF$E4}yaBt_gtV0Nf+uNIkA}nXK$;r>9z<w3|d~P`V*Gl2^7scnv1yE2AAEP)7 zr{Ktv><A!j`FRcY3t#VwUbq<Q!3ONM0h?)g@zW64@p;#Mcc8>ziP#m>XAE$_=|Wqd z72z}?-g}2|;bai>WhL0!7q`TvKsqyUi7ieS#`#{79Z=G)y5_I`eYi6Pbj;xR2kXG! zp&L3q4t1L<oN*UtTU8EVJxNVVs57!_>0<0>nJ^yr>R5W}>>mvH7q_^t7h-zJ7_A^k zCsvAU7bEM+j$nJ4Ld8V}K!LyGc5E+i@Vyx9Uq)%)4=Z4w`13ccCp`F9>VdRL>NXRD z?pkQIKiKN00<U5FnCnx1*#U|7mCa$`CqjWTjz4qtG~BCzesfT~n1+7c?bQxo6E`VM z7a)7`M+S}?VQu2E-$aq?EpZwNb~L<=M}La$T#OHL#__`%pd|V8j8L@6QVlGZK4*Ij z<BIUD9_*sPJ{q$ampih^Kw&l5QuR$yT>BZ{C8%cu{Z0k|=@GVPFi!ao=Xj-qj+K#j z<oYN3{4DHeB2=ux=_zsT>BI75oV)E)QFr#|`>mLMlIbu8KT#X=%pb|~mBsNA$}F_P z@naRoI$(eD@Ai7`1oGxc*I{{N!|JEp_2l9*ERU(HYrti%Z~p9;m_IKla6865OZ86W z+6|Zd&;R~mdH8?u9F<E+GIuB_GdlEFvnSla!vwi|LMs}1t*OWhoAE|&XD*Q0(=ryj zgNeP?vklYjw@*S_eN&1xE*rdk%Y6zdXui~w&^<tQ<@Jmt@G}n|4BiVQU7CM=;n_ZE zSq9iF5gF_SUBk~FWf73RP%6Q0Vy?fOnhqp<3uO!+_UzaLIBdc$U3shrd-_2<4l7;g z9?yl(qwJG^b#I@%9{7p#_%HgPXJ=_thokO+X$yvBYrC0!G5E=_GspKKmtC+BBEM%^ z76I8z9e12&_QUtrp8}F&bi)$x6MeQh3T+}=Gz-X@H+7*-ls;<=hJ%c}u`ZYI{gG!J z#UQiDE#|_#&O@;rY+=HdV%#msKw&nQ&*yM&U)1e9*zEyiQpKr>n10G+Z6h%M^Y}l` zV0zD;pV+TV!lJxn@TY4RP{+XT^YM9q64(<nMEnZ;63OR56<{w8y;gSvnBTFKbOHrd z^$plQ(XWMa`+?+M$sQaZ5<1;zGm!ouuZYVGTUr{7{r~U&{vkZ2|F(N|{I@qSnL7gm zp?dT@>-s9FUSHa`51C-AD|pPsuoLb&>W~MllWLF0*-9D;{1buwv-0){DOhE{%uW`7 ztx~-*Js9kU;}c%tbtKpNB#o4t%sqxT=JE5*XCS?AN`4!x@=TS{Y;pwB)yl*hF)Zqm z=567^OfvdHDCl^bvwg6<zJtt+cpwq)a>d~mygPX*7RU;|_TuX4yR2|iE7&$xfm)}* zHq86@AO~#q(6jGL!R|T|Ip`p;fAJ9s3=>1^ejWFMA|ig1>(<BMpY-Sbo?6ry?|g*A z7q!PZtP&`asI9}WU>{4H;$hGKi@hI)BePeok^-B)F>(7Eu<1uswGG%rZb-r)u*puf z0k|3+3@pA`3HHZ51ui%})g)J|ak@nrJ&MW(N_a|pqJjMd5?Kgeh729C;4%eaHEYt$ zeDF`Bdv0P_-`~meJq#?&SL)migqn_D0PH)JavS@Z-PUd6NP*idvlqjhjvQZz{lj=& zcJM?$InqZX)1FMlaU<Og>I=bUAJtsoR&BcO<8Vw550&F|gsqYW9WV0HJ6sl+R-<oL zKyr4I2JWiNB$a9p<SqY2)<Ec~_!XG0=ogH7x<yeNcY8LE@yNvV@eej~%SKP)0Y2LQ z>hB-IfB4_RzwSTXsUtt!4{@sq(N*A@`@?)O1Jx%5^szv!Zs9GVKhk#UHg3~wy#6PK z6ZxomvIG42ir@QpfW6Qx?)(X0o6?t14C|hWnmvU;m=D}D{zRZzR6xOEE4ThFuF6DV z`_@^=Tww!tFT2T1(G$5{;W|z$QogGc^C58|pK%o-$z|=?Kz`Ud$6Y{nXkT0!Fn)|E z>NXtKR9m}>9)m5l-Fp*+IS~~+cg8Rt7hylU0m%3>OdhZ}KK1k!3Jz)E61fcEYNN{E zW#H$JG|H?44kSaPP%radCJ@7qnWH)v`-Sjs@2sMr+*wM&7<P<e>yt2`x&rSO?mF!6 z!3|t^f%Jd=_YcE6>2Jf!``>r={^z-)QbumjA*eQqpB5B`%qm$G2h}CI(t~i-;a$8n zXA5$*$zPjdOsE5{Cg#Q8%Q}JNR__YzHon<3*Dhcw8GEq?*tFPj9S%G3k8a7Kp!u;y zBe-b)(?u6o3!;|w9m^3WwvWW&<~=mHz=hA#O_y+ZBu;Ky_EFG{EjPHQL&>KV3vt#8 zBl51}>dX6lVHmE;Wd7tyr_jGYhI_Sy6xmF&1-~R;epNB*4xf&ULUv8EZ~?+c+g&(Z z!5xQ9xpoJ|HU?q3v(8Z*W;R6cH_l%2aJ`>0@*_VN!;QB~a~-ym1P-|73}jN4)hB|V zb}ql=jdq>RW<6lvkOlX!-h%vK8mEEa-p)lBwvs|S|DE9HHMu$BAxEIIZZ)pXyt%dB zTfq;{%Y-jRu0Cos78zeygZ(3O|7`yzAoDodYdMfe|0u=qmONM&g5gI;j#}x9e&11! zBJhjYYIRqz1$%Pyv7Um$!v@E}&%4C$$LRvkU&P{W#8`_<BY>hb_bF(ruV~%k2kf)k z{AN1^`7M~|w;6276w6y{f%F@hIiBEevpd2)){`H*>y113wf9=KV?D|n->V*kbiuO( zYbUUI@>hm#0-GIqWP$<Mk`JGrAZ3nh9#8~?SI54pA}bQSR4KTAOzPx9Aj1YdFb4{L zY%@agq@A8&|1dq$_x*q*K|aGq4Ef#=*5UZ`r$)R=273P(WQFZ!XEsG=0Vj+P(<%iT z*KZq+<KGdo#N-GCt4;Hjy-8sAQv<or3c^EQMjQfr!dCyg<&Z9DkNda|Y{oFhBM5Bf zx62quv|oNZJ`X6Ya1=EI$r$0GqmWJ}w0UDYjt#VEs0Z7Az|tREfOY1@LN1OJ60?HA zX8L>{VmXW+k9$0YJ`#7Y2K=mZHysUr)=tyg4Jdac<@OWs7y2EV_84r@d@1e)V0dWJ z498u&-n<OkrM^yCH5>Tv{{CS(nfkZoBzVYQS^NKf$lX|D)Y6c=(Sz`YE!<h<6Q~a1 z{ce5ErPtcw7YJ-!%K#h(`e-m8S98%~wgsnGpZMUsRA9&1rseTKl5#E61*o_-{cQoz zuY7;?1qyUWKG>##T|T8+>nLzqz^hpzV3+C;y98kCg4*dg{hQ>bovWarc$a81^s`&O z6>S4HT%wPq0|hz;aslYCmWuWUitdD_V7QP=S;N)<8H4!KffU%^RdP65Io3}DThj9^ z50{6S=rP6E-HiA`3Qi+>&&?zZ8=^My9ZpA{!DV+0Ln1huH->`Yj@I?rjdq)6&UCOB zFPv}$r;#wjzb^#HuG71~2NIE%{zPDdbji0|G2{zx{9*p1jJvFI!LM#!e;mVw9Z{5l zVJ8!@-6#^sFi-NaUG&Mpwm407g51Yp|4MXp%eZ#lvf{4?zp-_rK_ZYh(tR81B|CG1 z!hwOTlt~E%;Zv1+0(Wo0_v{SJ=f>o@f5I-z6Gd(EeG0bPxLka-Nn&J?I`;+Wt?ufF zVE?y8RA*qm-f3Fgi=>&AXERTM&FdcRiTykNNo;W=1@?RIfq2N`)eH9GGR(~To@WFk zKa4J{MCvz=(#G^g(~&s6XrEWRIh}6P9xh$Fe494-^B3za!fk+*ujw%ZJN=jOrDUMs zanNoYFXr^@`Ky5B%z95VG4$7~>F{+r*fNO%>^DJ0))`l@nYT}`(_oW9bMLM|$~ZbK zLI1V7^O!F&T{ck*{f>iFv0TPFbi5wejOEVfSZ{%p+Id{Adyhnv7DGC%Yx2z#Y+_%) zZUxraT{?pOEOWVnyHnTy?n+oP*ttKBl$BAS+upV)6l`+e`UAEnc&61wOy|W!GPvET zcdcm;Lwm^4ByOC)@7Q8afnGS8dsC6gPBgIto1UCDXDv|SNBR1lpf_CU@S_TB;g;=p zaD0jSpFeP!Gu9m0wH;WuB4G)}F>PWx$qVS7knFOP0=<ojO$gZVV9^6CPedo~#bur@ zn`i5eJpE!Y&Q~4fghE`7*-PCgF|HusGg5%UUDiKv9?(^{UkK2jx;AJQ1<UD`k=Jn^ zlNrONW51LB!uU*J--+d;5`gsBVdH&(jO`c~?006$GkYr_u|H#NjO^2Q!hT@O$9TD+ z-K8;PGT4mv$6BmkPx&5U9{3r3b|J<aeAQPNjXXRt5XYN*Im4|x?0kV&1lVlETkZ=d zM#HKA+XeRqZ+TOI>9cnTfTZD}w=VbvC$A2}`Mla?=a&88XA8c*!|^7hdN8glk~2~K z477994bacboFj*E78NR`irW}{fBaAG*kU}R2d^hpT5(IjPmW8E^+o=xzkgVs$Np`3 zR{u|TVrkWBiJ@3clGdJzJRUg~!-lBG#$dD9V!k*AY?{vD4z)X-<u8|G`iMocS0P=P z`&Ayom=PUU7=Il6@-0rc4g>pSl3jNJ-EBJ8Vfc}Jt4p;Mu#xJ*=`qTDiW-JZM|7<z zx9rIt3c@fWYN3YNNMbL+=_MI-p`4o@{heJAU<X??zQeGS7}ib2FklC4{^bND<V7tP zep24zrJ&>a+!fdYnY^!8ahfoWIngFao%vC-#i(a%$AC?LI3l!1-pb7P0KfX08i5Vi z<YB^{QDD=DL^4`H_=Y$YbxhlxZez52;|zv_O^;<ND6}2s4IKeCUq9A;1KRYLhurc` zwe%~2&F2sC<gVX+Yo0aO0{WAjG3bfbny(7Ty4=aQEeQW~{*B9taB!p%hJBq+dJTq? zsQvXHD}i;D^B>@}ARV_9cSAZe>)Q=Xzu!aV;I!{BTxk&t)PCq|jniF9@&5f1ATfQS z+e|^fnb!}3ZHcKg*^zY+{6|7t>~TEx|9I)217x+6t=0iy0lTD%^gnK{LxDg4>rR}e zq%AFRIa2U7ZZxptPLMuVAKs56*<jNqZAE@!sOJW~yVwp%)slH`KwjWA5snYLCCkKt zg6nxJ9;JcJm<Mrhpla8#E@9y3M;`r%@yNSLWL$th>dT*(f)4h%lPzG&wB{M@1baeY z+90g=kKNlwRs)6V_1r_ep!^qq|1iD&&sXl4p+1(l>vilsyKp-Ny<iu$I}U8tD{C&- zZM&9PDuPW0cMmrLGMnrJlR?KExpohSv#+?v7sHu-xVxFtE8lZhM%_V{!r>JiUbedm zD0L`*aW#cuK42LRBY96>p9$<(yxq_X{N&rwt_i@7GP7!&cFA*uS5AN}aj*@;)tr~` zl8@cq5mI-;75pUC`{o3&Y4+@`ePD|W_#P=>^W;)$xYLsN@hEJE=-c6Q0jS$O=!!Y; z{`R9@H!1im!5BJT1>4w|_x2D_=&h52!=V0gXfdvaGQL&rIDI9>Z{M7v;0u+Rnok?R zCT~7@YXaHU=$Du-%uI>*017f?&szffiUcoPDVRM6Z_T_2HZyE$Hin71=AzM<@39%~ z3sZq|=i^5Nix(|g<R~1RMtORA&Y05}Dc-aD&dRMF!D6s`mG8=c|M{~&<xD}(ZmPd& zfcn!5q7zZSf5uY0{%E@zocd6IZV#vT@KoURTAFPJ=+}%;h^5-w+7jOud!T;ehaw+w zTU(pL!V+oBzxC`WPOo122bcfyp})EM4EQxa2K7U(*ZN~VV;(7vMSWS)NKQ}qot*wq z>$`H)jXjr8LID*YE!zrQWW8_h8t^Yl?)=8>UT)19`p^#YVW0}y^pihbJt?qd&C787 zd4J?raN|2n))M=X{d0Xf4OF+dW`pA*RS>Wz7j#VdsEaW`Uc8Ce1^l9mGh|i+nU}`v zaaj{pE5+gX#;iS>-URnV-d=tsjpHB)9xuf4(%!n_JT5D|RKIGh2ai6rb`J&PY<=@i z2H5t`PN+u#1#|sJc?0R|6FqP_qKD3_F#?jqys0>S*_tuiawyQ3Ua=Md)gyGSgn~af zROp1KaM~fK$qsmnY#VnIsJ@q<(n7)b56c|b4D9*C;wLT_YJsb?F&+e=Tkl}~)IE;B zECTZEzw!4{P@c8!VQjbH{P9sZ&$M;U>)`SvaXT){#Z~3~136&x?9VL~f{qN{JR%)P zoBi~U1b=Y+gA2HPvAe@;*8<7HcAFR=-Fx~Mwwvy``UG{N{eRZuGD}CSxr6x&mI~T9 zy?ee5UQb7id%TkZ-VaGdJ#X;x5vXSdYpQX2fB#TUU#9jRmpw*N+AR~vJL@wG>n+$- z+Kv6gUb@kY{mI_6v|a-w|57ig9+Y_)uH#KT`|}_L%d&Q%7S@Yw%j#c<Y%8&z59#FL zL;XoezmYQ8NUO&mydhn1ZPc(WK(ce;5q0D!lWG-AuZ^o$Lavp$>jQq~`=be4z!vn) zo|lGp!Wiy{4#<6xh5`7Q4ZZc+KvJIjjgRTecd4a-EwEpa&*{Un4siYP<jD+9FFhqi z8y<_XnHP9A4PFko`6K>Drnm}T;R-6A4Uf^hT9uIDO4-=lEE>t0L4NEi(++b^-<y?z zdW8suZDM$-VEECG%TaHAK9$oODAsZMcC#K%AD~v9jxQf5Hs^1}{5KnGtASp1ShFMQ zZH6sMMLjcCIL`v=OOBl#BScCq`yfF7*JnzpK+-p9i7MJtRp)!4O{W*V)CIB&ZFY#k zpLcag5vG56tF#Gha_meSjwf^5irAyg-#z~)>fcxP)q^c`Tjq9wg8p%h)W_qK$m{mP zoj?_XEec4%C1X{bHzcdesEUGoMoNixgH0!!s%IkorWB@QdhhlXI4@aQoBMbyVJmBM zalH*DPI}E?^X{vB!u%K~S$OCb^v6d|#_?hO#EWgve=6F!0c?rhVx|#n(xKWs7bptM zHOT=#{qEh1%|K>rzvC((**Mey=Qs0$DC0b1W^2yp_OH`xmU@8A?i>6S=Y!<>-e9yz z@S_6MlegavV*JpxCn6Hjf9|-RJCH4zdLH$Hm=&>{o?LA$r6AwP)fRc+r!6|+;eWh- zm_Da}@~Vl`8@nfQ`tme!4aP~~$vNc^4>zYg<|klWlyo>bW4vTDO&idb+?Z*I`FQ*9 zIf(inSGVtnIH=h57G7C}^y{DWIKADbDo)?kcm(5=Fv22y`NYO1%gu@ceT74yHC|T{ z;bnsPD^23_(LVL{<l=vN<!;vBPV>T{kU5M`N5yUKW(vK22n9B~@$(n7)dwx%{A|Le zR<v31<Ey~F+sBG<I>JrClZik^EAKrnNBJ|>DtiI7J3P#AIQqWpcH(lFQ2lVzK_ID7 z+i;nJVfj*Sx)t7GX_Fi`Iu8C}Dvu6U0%I12eZb{j%4sb3<Ag+VO<^nO84CwD%s1cX zyeXCwEcmp*4aj8I^ehLSF>q8)q9A{n;d&SjBv@hOB%t^2S10_yZydX(D+JSr72i+< zKWptCK%>px8L$z^T>o_jmp@U;{nC|SOL`OI*MrTjQJas`XYizP{S1)ENcZCOVh0Ut zj0fBMSCXC^*b286t;BlwWjxTq=^|70B`p95U(CB;{zUiTbv>~2FD{#o>B5a`7rKI< zzW=OfHN1hEBzrY1Qw(p28ajB1Q15G<?FMgn{>sxD&cLwxo^eQy0(;Emm}lr8Ge2NT z5WWF=;bJH^4mW13a)38Zhn3D1yJC0@?PawwJ#xyNr4;BRBH?)#$lp0(a|DzR`!c-( z${lpz^2m*#SCxI8<AHCIW((q1!JDM-wnS|3gY@nwqpjR>Nw52J4(8u%>FR-DD;WXD z4|u~=nCr;L`ZzS1ac`u$4Y-BvBEf#A^~7*}Wc5KqsK4s>gmxEv1GZ=IkCm9uvllOd zv3}i8Ef-;ZScBz?VuhB3z>*)SP#<4cpEHqI--gR?&9Izf@j<qb9#VKz(I54sc9;Ry z&ra_LhX0Owo68|s|6#2oS3)`c%Z8rY4DH^w=*_nR)C;stEiwNeW)_K%|D`JtBQq$- z_mSym73`l&CumnJXINEN3hH@>EG@8qT(%qvgZA!NdbBE%EANY;6$Q_>chnbXQgW#- zU0W@q@LBi##<l2oEEzu?Ub-|1Rq1FHz;!m&sjys`<iayw=HB9-O+^Reyh8Dr{MXNy z+lk?kch_OY3&nza$Ft5~>urSTzh;uX3g-XoU9~RtQRnK1A9EI<Uix0<T70&C;>-0G z_{@F8*lHTuQPEXD!5p69U#A<Dnu^czZ?<(Th4h!tysCoX+56TN@|#nkeqQbE3S7HF z6kqtkv;GSWtE8fVxgWir?MD3@Yj}1)S23qz|IBD8Conc(ML71u5QA?Uz`rh3!z&5( zhWiH<f!^oKtck(kKm6{lO+4tQ#VKsX;~5Qi@mw+gr!D&;Q171JlTRtZiv^kI@m&&) zsncVzzof4z4@^M4+d9e(kJn|%FaKJ0*T=ZQan_TWaGMslx2?V|BU}x~)s4+MCHB;w z-MiPT*eC-#JSGZ6+&B$e+6ek>q8KU@bi0SnxGI9{W_W1T7D2vSW`_kN!!g;^wP0Qn z>LuqC%i!33#^0+e9&{?BpAAR>o!=STs#Lgs(8Mo0HiExx(VWqEEF$cE-3$tj%kNrD z)<Jq^#^VJpkl(|G(40Wf`Da}G#0USP-7++8WAGRCTo;b<ZA}sVjf)mtHyOU$1lwaA zy=nxMySi9y9fff*N~2Z-;?^cSp*IxAvFuf?16(iIm>7WX2;SUvhp!LZaotG@-(jp+ zRMMr5x)Ftyk$Bz2MO8~F=)aRUH<*Hdg#Pf!_-6RcE`QrKK%3HQUsj-Q$K~yQxUGB& z{X+-zqK@<nPM}v+Gc#O?`jzkGEl@A}B48%wFYh3iiFW1aJGj2q4G3A_DXy&8oMI!5 z??jrqPjPXBe1}999t#G$!Z*5lE9!l(oW*sm>yBe@D5Tqr6J%+Fe}_Xw+6L5(@YZ<Q z2!5N1S>sW^!z*kM=;7xmS3X96{RpYYji57SCwDjg!>8p%f8V}_!*9&Vu9v737*g;9 zA(`i2twEN2g3}nVC7;q4zbwvtpL2eRcmX)9E9SuugM7TOY(_BraLD>&Cx`Ba9|8#* z)dgSafxqkXoJe5abXQjfekf$V{D?8W;`L$Iq&G-8;SY~kzWWqR0fTGhJ>dsTdfsi5 zbq8IIp4CGS$`~@5JZ2<?`mTCD{;-Mn75y!!?<i~YLj7UpnIGh58Fy$xAeR5}TPObT z$SHbXoHhmJE&H_F3d_@TOV`Kp7dt82WBo?!pU{FI2sxGRQceGVr|*{}J+p4Jqu>Wo zbk+XQ#`-5L8fhZdyyNb;uSSEqW+eNpTXvrK+_337CSh9Qf2)t_JEt!UaG`+1s?T^~ z{)gSZ>xm2ek7OAvGsk`zns{5EN|e^^FZ5D|uo8%xd&4li=&&a46<p&JQL7*<>tBBP z90Y!Lhk=3>OpBXOW;i=iAhS682)B7cAAyJYVf#5bQo|a<SbA!-C-`{_%|bSa?QLwv zJ}jd^|Lap(OFrsXYpgpC;r!>&I2{J`ONH(ma9Mip{Cs~d{P0A^<dacQ54uymV@;iy zh*VOSWlG@swet(E?}Xz|=B-xIQP4{#xwKvf%6k;fIS4d=8XkzJQ=wGL_c#is?Z|{y zSFqz|o0_7Zr<@op0KfL6M;$3Z_LG++8T>CMILEeQdeEnFxO{&bYPc~U(kn;gA8P}$ zr>335X-)dB>>LL+5x%^Fr(t5w8ZQGodHQFWNMI8st_cPT!aN@0@rD$4kHxzSgr9x_ zmu(^}NXO}$oE`Td4ARqsT=||rHowLc`;#%OS<W4Qx+aqxw3~YzY{hWAi`!d<_0sWH z{p(jC=Zz211e>h;Aa8{foDIQKK2g0lj{>`I&T6p}FiJA_BpxH_S?_8rfkaBSdj=AI zMH-K7@Z}J1B2ebJ)fgKJ_^7-O>p`v`dZvl|xB6ePd=tEm_%Fh8$=z?SmjPv-`+Bbj zKhxj2PzBSguienX^naz-IXvEC72MrXiE#Z=5xJSVX#Peg(qkc2)HOWrgM)`SV{g}^ z1=Sp9tb*^Z4v$|RBPVvFn#|uA_W!9T2K|o(KNnLGuF75GuB;I&N4Qe?ErxjAHJdMn z;t9*&*=`qPid`Y0B%q!GBtP2d3+bZjy}J%Te&3fWeaORn?L;=AaQ&0mrMHo;Y3EGh zs0qstZ=97pj#_?r{KvU)lewsC%g@XK3o!q$<D6EB-!zSp^?Q|s<wx8w;PScspxc>p zRsOt5%EAM$d$VL=7^ZJC(lw%>oYj3t+^IKB=2cJNJ1dJfP5Fswwpb5R;~j$fym@wE z$e`sX9g!BV#~X^fw(M;PoEnPd50GtGiS2&<Ez<_`6?{zeM|zfTH^=_#+tIQL%X!!J z(--q+*Wcu${_^!mE7XZ@I=NDipWMd>^FV)Qui$_P`$v1-^g1eIuiS*7-`n82!N13O z$KiD+`X+9~^!IiJzG%zj=owLW;QHo!F_^z7Io$#J!918Bwnz;1weMISi}jvTY-mlX zL_C>ilo1N!aCq*rdItI>BJPU1IgW3Uy0jbiWBl^pZrDCK->GV-Uy&sUN4=lT+*GV* z{G@IH1@*aIJ2M>h)TigJ7%yI57pP)?*e$xv7sp;c;ohShMFoG|Ic!nkBFZ@F7&Fz` zMGQaw^xpOsSI)J$S%#>W{Yb1(uXt!hG$naI&%*iZT-4{wjB@4l)n`4ioNF!TO|X5A z{nA0=#%GSr1ySh~bNPf$OVU8pYqtmZisei7vhrR9qkggld<P2sBNSSiWB)`|X-A;{ z<Tc$nINs`|!_~2W$l&x)jNc|-xu3eIcQQ0Veg2Qh(O6!OOr0(C_vgv#OMG#>>c(FV z!hYzwe`pr=YtXx$MyR_Yo#zkz(o}9c+Md&&4tArUJPQv!FD!S<Xs0zeZpQhiY%v}V zH~9+iy4G8pEU_PAr}YiMdI=81t%rVhJ*akh7R2H5()x|w=vTYgnuNL>vUoMN>%+2( zd@SF-L?Z%O`Ra`&j;GWGb0^dn`i3WC{`sZL*J3=^O|#+Ro-SGvi0xSU6Yc_G{A4Pd za{Y7pHWxoq!>uf_{$nkRgHT7Ctn@^B#^$fWa^4!WnBsg?j+_-rVSV~Exb<RR=1*m8 z@1sedJuqIZM{Zv%Mtyss9@fMB?)5;_Ydk68)&s8rLyS0mVk9@dhw7Hl5O-4(R}K%w z`d4M_NfY-qnQ#3<jm7pJ^^4np^^l3W9f<Ytd$z-#Ywx3zPF#QL26<us9Nso+752B+ zpvT<2po0gm=JGouSxEiE@onbcj&Fj$?mob`_ych{CF366!MhC1t@^*-37zD~-F;A3 zDDUI^2CY|+WKZpC(92Z5&`bvM9_thC(}Ud0D%>VzSgmh(_kms1d>OY7BB@=4%Q){# zwwXT#c2n;z6ZD@SHImzY#@Uvsqb)b_iyV@T_`?o#qG>u&-axYZ*-6~K$m$DQ@OZ&$ z*1mH=JLit=N+daUNQQzBy<10eKiETl)C8J=U(k`FWsQ6;eF*C(_~TIsx6O%~w!4D9 z$2eg|D)@<%*1nZsca(3O69g2@i!}%UGQ4}Qb%BIGI2La<CC8**F94g(9v_J1FU~qS zBne1gaW&fv<mCpWa@($G-Yoo-Oxt<<;!|*aU8(UzARD7M*9`5Z0q)p7B6*@Ejf`4R z&H!ax#Sz@6L@&t$tS8-FoMH)ncB{>EFCa7E68w%i=#BHjGr+F8`gorRY}UQ$3?3JF zZbw3Vfik;hOiTfi0l%(pK(g)*tB^JEGqL~K1A0k(F>LP*Z(7y?>21B-JA2G4g#lcD z_Qi@_Kv(WFc}OmhbdcHZp!<>DZXN>+J1qRo0C~#-W^>~p*)t9G9VTjvwgKszi)69f z(AtY@azRIKzZ|**Y_`$P8gH7ZrhU)pgk#$?YvY=;aBPc=+M~4-{G*+(l-B^0e(&=< z25jzVOvwONEDjxW7WD7-%8oe+`qSf;(}m!7KU6p*2mAqJ9;qgSzui=PU=!+9|Fm`o z`i?Z}5uu$I9K4<aS|~a!0=<n!+p{8IeT{7@_7|Dt$K8Y-6IZngkAK?DC6VdCpn8p& z`+=Va?IsoIU#C71k9j0`|C)moJRo>}W<K7Gl_3_NFy5U;T)l+xBuW33j>jU&{Dqsj z{yApyOhmzx2R{bh#r|Mky@6i>fMI|6RX+l#K2qagD3JcJx7Y?ql1~XZee@3H9bmJ? zd1E&Lqoxhw-nHZ}{gNI+!TeCDACB{i<bOC{g!Z82HJM=32TT0$(_jbV^?-}-X{x;o zA)UA1{rCp(lk&V$jAM0Un_A4DUjI9Xi`T8S-grExM{e_Whjh`bngtjqL`L`M4zNkb zV{@zrJy)g13n;un1;%0evHq863$Bfp#=CY9AxbNd_YL0Tyd#5mZ&9S6KgK9E34r9s zx|4=L#(2>qtQVnEMsxY4d4=NfQ*iV}6wX_b+3U^DsC(w|5&M%hZEL}KK_nw$Hv;QA zwi)BR6DbMkwG>oOJO3yb-x=psBEY62A6(|<AH8^DGT2?eukf-#&y4LKiytFI<=YMI zf#g$GCLW(jVdy4Y2Z*Te%N}5t!qb8+VlbzV^uu!P{R|Ru9bjLW4h;hmCH`=3eEViI zI38rEFZ^;5Fstm^QlN2f$F@N5vrM1F0&I4;)m3hOH4N;)c~0*Qv5$pxGWLpt2Cy}K z*-q>q(ZkWt@EA=@7Ff&_17jL8^T5uxxV>X5*lcM*7}iHyvu2ztkf%Q@+z}`$Yx-#k zB=l?tTMCw~8SSsR@wpZ7a0b|=3-s);o`RE^7ciZXs(!K@NP=5i_X8zsS8zY{#e5DE zVm#L=SH$}P`_mh(aXt{y?^W(d2z5ds7GAy2?FW~xHzDf;4sVcbl=36a&-7OR)7^ye zf7_0P|LM^dA{58qurT(|x&J4PC%m#Y7VN$+^3B-Ib>U%ew*f_TRwy?-Tlb#BX(IW! z<>y`sx=-am19qdp@D_IyMP|O@d+at**HrFFMKb*w_aq{3ouHxy^fEUZO)%{24-87f z>7`<GV@d*$mo?nf19*ddJpL>NAKE@GEIJ1y{oX;j@F+%d&e*hbkbd^qfD1=~idr+X zwu4{k^!MuL6k*H`QRPp-uRfgR-sVZ4HjaDZ(RFm|g-Y<>AN_jfE+FwPU4mi9T>tP_ z{qBw)j^V}oYPuQ2jofc*TLw0r@70XchsaC!BmjBNhO3joPYOpr;igv)^^RNq0xKCE zNT-LM_+^9Xq-~xCP}HS*Js<piKQ&cxnG&fUlf(lVtxwIk4KVpnuHrI7oD$0qQ1F%E z9<8s(!KSr`R^YTtU+fx&)2M?=9Ejmd=IJY0B3BRX$9`{EB$|3i4EYw!DR%>=7tg() z1Aa!sP8<cKpU&3d>Z3mSQZ(4)!SkqS(1|?O+v74t_P;G(4P;E8g`l3*oHNW8{qOs^ zpZTQK9x@oVbnCixI6cS!Lq{b2dm8thJ6-y=(HZ>?Ziz)ejRAY#T%ce!+?&^l@$u45 zUGE4me0r=xF|ck@b1(J->8d}2)3#OW=CnevC5p?doWOQZ4@kp)UflRnn*nwxUQiMM zN$-J~s8i7}xR3oL^S@a8^01n^_Wy+_gv=2+hR7V6o>cpk6fz`DsGLS=AW0+HX;2v| z%?T$Vvtur$bIfJTocWkTk|`PceeV5y|9G$O^}8<L=Y8M)@bS6Veb&C$z3z3dwfEXX zP2XX(Ot4zh;y@gD#ha$RY2Z7CWCRLnCGAJc1jxl(%Nl}!${`Ieg?hrH*HfC5?*7%4 z+NIDP-co3<hK^w@xG3cH^>FBm^PX1==_5{WU<!D{4E61CC>Jk_ahM1EXMcZKP5$Q( z0rE3Ck6#5dgX=vsC|uaoMe729dNC^1yU1L<+Gpe@W>6bKvl2gJ);530Gau(Y-3qMD zdpkS@Sbng7NCnDSdG)Aba6Y`#_;kW0er<dpmtCD;xEff!?B3eNK#i?0{-SPyV>Z&P z!SD3d3I?(-CbJ?Tw;LGn6~hz5YR#UdK&j!ExLhhPd96y_%j~oN=c0_Y(*WWg7j1ax ziEUi1{eY4m$;EjjpJdRO0M1vr45V(a{`RgR4_Nka>$-C&Pn$nMc$U-qPP4-~$XP#2 z|E-XV&F3V?gWr>S<efsA@GY_)dlNi5*Ran4pzGFAzpH?D6H?yRa?~zG>JD)BsGFSZ zmuL>hNj3PXCKF>IALVj&JqOM{YgrMhMzZ;YQ=hm}_~_YKl#{)R{rfG2;iVk<+nCA~ z#-GW4w(4Y*H&9&Vwlf~`nl{e{7J~D2Qz|ID8n&FCvKw;jDw>hLLiOAvvY+p?sy+Et zG~$ADDmZ(fbBJaaY3Z4}n;~ZkX$hrKzA1ZC2$lb%zdwZMf8i;pmzhIpaU?4&NGXE3 zipBeGtRyq}Jtt;@vp@@PWOv**m{~?{l;qkBqDfj^Rr7?5%FV5EX?9|jXDx*A2sOOF z5u6tfVpLDso2^?n1Fs}Zd~}wB-lAb@g8R3HEj<Le<VscuWl5g<;7M+ce}7c}F!1`` z1A$L~5p`E4?*Y0mJ$dR42feepr;mfD1uq&xvxcmUbt3x|U(FU5pj_nTpGnz6RFKm^ zVa6YO^MtY=TbMtJ>{0l9_n~s#*t56yqg?dr$1loGOr>E6b&q(^n6_EO`&M43_VTtZ zJ4XQ}DUQa;9OSK6|Dr6#@}Ia+nDASU6jOa{#JOJ63IEaGAG-Iy@D!BpSQ{E8mNQF* z8}VdG^_N$|Y#Hmpt%W?v`D4osK+%|;f4|IA+^#cqr^+4g44NcV)*MVN0zU7u)prkv zxmg-*L%-*59_>hr5LVrZTLR2<NuN*dVh0EAwFN4*j|o{M?afHMJrVK_KQ0O3l>2yJ z7<kC93T^5(iLz>fKe!@q#ypy2^macS9tGsJH1%zPOrymoKRG6!CWSF&K~|ACe*ok= zq_NW}OUc5{Pp0}r=bN8+0-3b;u{m;Njn!_?C`&L&*HW6)HHJRwP2E@RYBItH$gB>1 z6zW&`X+fh~JRt1$GRVb&8-53XGygri&A}t~Z&anx&8k1Wb|SR<xobJFviqKV`d!kq zfR+?K;=Eyl0w9+?xqLJg$cDdiB|pG3Gc|w_Uo;<6mRH~oFq-uE58d*efz@>f7E!zS z<InfI0`YLErr@U<1FNy%60KJQ$-iv5YD-_BY;@0ki#h0DI=xe9ugtAAwR6vju}0;P z*R&XOjl%x-n+(5WkpDcFU$+a$w4O|!L@3kUO?Fpje6w7~LH}XiBFdU}g(ep$j4Mrk zElvjVe*93Oz2&}Pl+F0EJF_C7$E2+nk=@L1v|%*KS00`wA#POYLX)y2;hZnE-*430 zzcPSH=Wk!6ELp9WC86P6<GO7u#fkKu*4I#AH_5vWltz^EPF`LL&Yz#gCIA&%Zamrn zY<OsBDYSo-M+2p)q%M}X%CMNcKKJzO(;SZLGf6EM@{?5?s?PyM_iRhH11BY3q&q-Z zu&A!XE)M4w)-e{;UaZ}p4upJ=g;@(3P!yzSw-~6{-DBh|lxujIuOYjouWx%(oan7; zQAGau?9`N;30~52{grHR*79e{GR#^{*9TZU<(NdTZN2$((i<^AQUv+L`r4DHpl_^W zKbF#8aG0%E8ewX`k;RaU*_v_>AhXODo(*PZ{qh!~TrYm-QmS8h*zb89@SpwtA)ToI z%ejd7-+E+Ry!L~02+S(6KB=;WLwDVe*+p(<m4{wUBa|e}q3-76dkCMf2=2V$81c}) zSDa{e5zpS`M%h&S<HAu2E2fOSLSe@mPGwOz1n#ZcSPb_ap8NhqH3zvOr~gND?7=z| zc0WmanSSGGaUkJM*QNudH~!iQcZu(h+Wz$pc#U5+opZ2ymoKX_@wCtFV)?*_9De70 zttB}7o%H@N{XXc>^HUYTK)Wd?w&M5sZANJ+@U&3ps$*?xpdY+W$C&h$Ew32I03|K5 zV=0X=i*EfWy)dz#JEaN!>-mE@9Pm=|p#|XLsO)2;r!dhTp8z=<JipQ%9FITgSrXdi zOz<YT>7HsI;`^-z_yR>fOZre6X4?JNQ5djcuDP2yAQN$Z0&W&}0g4A0<c0!8fi{0X z(w)3*C;7Kw(~h+5q^GFpP3>n6)7GydTo)2!0~9A6pF;h{tZ&+qeNv5q6Y@E5w)ZB& z82M?{fpEfq^!JBw``=e$^>HJ^rx}+jQcr!+y|ndWMYnx6PyFv)sG$29F7^3fKIutR zH}^@`D+nzUPFCPPiSku-p7NotS4~UZ3%N#J*X7;EM&f>oyq)`It(3c_o@$=`BUxUe zouF~XeG%O!(R@{Z9qGG{`@Eg2n_04{Rhd8OyNz3xMEV|EhY9+Mj9xw@A3a{2F7LK) zzSF@42f3}mW>b!L)Wdxgm*&hW%ay~tz^+Gffc+!R2J)o;ZAtfebiBJumWES(w>F(K zrQ>F(6uNS-`{&$<XzuhEUGHyMf06vz+#6mT^hSJMzfz9+hk1-L<WSFoohN-b_(9XB zMIaa5ZJ&|<w^3YqS=rkjquofqpm_=}M}0Tnb?whtSy?R`7Z}8a-V;AQ*Kr<KH9A`} z(cF@IUtfRyduk}@ms=vs-YE+H@_FQ3`G5ffDmq1maVA01)G-<o&Zv69{H~D`<q_fG z;TZ+0q^~|Ma*f=+MBAVb?jM1EyFP=La2pM)59~==LG|ybZEHhzPL}#QlHIGjA5SKK zS`W~kkNZ=6+jyO~<lu+Crt_D}7n`-qS^L?I?rW*DH=4oqOL#k5+<hMDI~}s~BtM*X zn&!%(+xM7RPA2{L$IE8JpLZnzvI6q|oxIn_sekhi%~^)}U%nf<aU1+zSk~M(SMHi> z8S_yqgpRvFeL}PxeycM!3F2V?#!;)SslN(->bjG>v9>sXj=yxT(^R@IW=9*hVDjU3 z8@pxHKl(?Ebvf7*m#zqwliwqsn3MjOeQ(87&w_)htH?ifHu))X*x$!#<pS#WUN34V zQ$3wxlES%mcZ#Y%TlAv#I^DNVKs$5avYrRX{|B5CW>I@BFL`W&eeS<ztja=M{jxAq zT}H<}WO-JX`d$5Z?R*aU*UsQdAnI9@*w&0g*dPC@bdv)=$8NWv<0gli&Zc%<QlI3< zAsz?Rw3#T^Lq)Gff2x3e+AFX0q2JxREuBjGRX-bMlAVK7E=80757&P2p!)kv>pqj} z)jo96mC)ok?vsQ3`R3RDRKL1~<|1l$>ysCZNnTLc#ZhkF?#=|8KGQg0`@;ESIEn{b zUnh!()7rn|$)1k^BPLRNRc~wvp?+Poe7g_f@?pj^N&YD<Wg3l_zuE<alU;{h^oDY% zzoeq?67u_plMb^84eXjNNv{6WVmk3&3p+ZIf6~_tbtik8ngW(`sP~R)_aLgT-TjGl zpOo*qg7{#vXI##<Skk|HNN!L1m%_ulkR9?z3V%8J%dU41E%Hz4kTZ6IKexRPB>%{= zumKNwVTf2le*Aq)SMdLZewwse5@&J2ls1z@2AwE9^E1X@^`ta8B{^aP2aFv)Ihyc8 z&Vn(JSL;UxQTkPAt=Z)XIbV2gia$8ZEPJ#PoF$n$6DpeZ{b;o-mM^y7hZ{BSk4zf( z40^SBUX`?3J2N8M{0+*hd;3-$1~2K=r&R$^q@Hp!3s~D%&6bX@w2X)rR-3P)!f18F zSKr+$ML9o2Ym*=O-+6C7^~0xU6&AuY%PwrtC%?6+yBba7V?dnsa_U!AaW@<C!|qA5 zJ%GK8I~dX`lkwXUy~v;H>5iVH-*}mGAieWCsy0;5KJ8`J#AD0)k0E_FaF!X#6EBWm z%;ESyjK?k@J7$=+7(?hk{I3wmKMkC2Ppc}%x2bU_{We|ABdGoXug5ImP+xK1&?)jf z{0(39vVySR)oa2PSD8{-F&~P)o0`q3p98yI7RqgYUis6N*#G?Ph<w*q$=PV8q^hS@ z7|fS$yfA(-H6ubxRhZrRUf*dp<xiF4Hb3B-!B=UPl{_D176GnYqLC`h!c8-;xIxYo zuEoOYX^D$)#f)F+{Tqu%mKJhJxZ<nucyVDaxOmxPeiiATHTW(iZ1Kg^9k@>8foe6- zXKN-qD#!j;{g0+Cz}j{``;Gxw=EHU?fx)$*7xIA`55{#Y=3oc6<#Z^xV%A^9v<T&| zwGvJWnL>XGg&$KCv}g~x*rTx(oiyn=$&b>>B6C<XXe>B;9VNUa5ohZ=y@-poU(W+J z6%-sS!|Lrp;$YPh@F)ijVI%2M$GCZW<<J{@@+B>z#Z7PbhXeWO`<XPH#_qn}kHWd+ zY`5oYfw=?Or)<bspl&NVX;8!-9EOt|@z%rdXWs&<-9K{m9B}FR6Cqf&v8ivkZrgww zwc1Nwao7k@^R%LNtXcBOidJz!DwmwGs$`${F0!QIB6%8ZK&v|Tq<`d2aH;e}=?Wlg z9;rsdEBNyld+JB=z`L(z6Ylow7D459db+uU&;24yAs4OOHHC(|n$@@!Jmkt*JrDbV zi|wrgh5AGm86@W~yw;RJUVSTT0{y<hYbS0~g&hY@9Fl;uqKs?8s?yl#(R#?yF~jvC zmqfJR#Dh0Yl-;D&9AE0U%Ni&(93-ZC@oo!K@)w));j1g;|LpG%tG7P?db05MD--xh z_l13OrNgk>!am3Yt-nX5VsI-mL(y%6lH|Nms<6Mls1Lsoa_{bU_2YqhMv@HSngQE9 z&W+4H9QJe+<PC{Q3K}kwLrxDVT<r$$w5G6Bnw9Ji0>>v;woV4OJH22zEvos|0WB;^ zenEY!C3s+CTwW}=IDW<?J8(GY@9f&)MN}ZTILhq?4L3!~1aE&TpFj4%XrPqakxbo! zc<w>RQ%EK{`hfFYCc8~1d^3FgTwvvhOQ}nNVlMU(g#$V~eK4?k$erHQE^*af2YYb- z&G%bQz{)jiZblP7>Ai9W%JEu1T^g?9&8<$7J&L3H<;2;oxPjE2W1p=aAcXsJ)53A! zc!zd#1j?)DTby14l#J(9yn(uss2SuR)pY*>>EQgTkDbXctZsKTUCZ#hYW9T6`M|g7 zx`gpvc3S|Ab2UYD5+PbUxKe2EVY|y#kSjJkI_m;?+JM}@9KhAue-OfIqF#D4g*9I@ z&u%i14OnLE13k4d#`!{fTi-4k3wiJd)p69HTA43p6xPa}qJ`ezc)ep=s^8_Mp$&zt zZeVc+$(66Q+ELd3)_g^Hs86wSw6_O1`)w6N{Ufs4SxV_q?BOtcCQyxkxM(9z%8KHq zm7hkr*llpUAIWQu{WuCVc5l9dau%%b+@Ink{`Z753?IJtogPJn9Q@{Xp_5SmY)#zG zf_BC<m63mpT@y;ZfSQLAFOh$Xng%<O|M@RkGp7QXbEwn{a%t7b9Wg*z|GUS@Kj#k> z^gqZk*~d;s`ba~}Jt5OD3CCM!6!qaghtJa4e4KF-a^uI9a@fT$yH<DRf*fBBeO)%> z6y)-~{I<=IpIkZj_+1YDHLN~qHRO^9uV+)duz|)WErE^q^tw<QvXk7r=udLz^WSqg z^jF(iD?Nei`spmPN71;g&01jXz@DRp>EXM_MSXBKN4+T)dg716-%#ALK*K^kLP^?M zN62%d<&EpWrKQ95eM!!~>8*yGt>`jR7#~fZ_gWLzn;9Miy!0#NKmqCbPGveI=Py}> z0hRBES)>tOoH>T#i(lqr84Z*Th<*?yM}K<w2@f+dX`p#B<k+G};~^JsI=q5Tiuq+@ zzeGa5r_xa`2V7xR&}<LRq=$zJ_TU=yeVqI&JW9{gc#+P&uNwyp?!v|_1}X};mkZ?3 zX9eMgglQKKkRQd#xpm|>@uAugq*vKEZbKZn!u9omML;Bi4|EcZcXuZ)A=Js)OYtgA zu|Bd6T#_>RJNcP^lQ56s4HJxTF@rzl`8gb%-@PJ&;t4mtPPi`QyW7)*r<d3+i&MmL zgio*=FBiD_P~iI}pZ?S8MD<_Nt=+%(J~KY2Zx+mEbxr0peAwcu9@K1h?T#*`6Sir; z0l7!{X=s=rXN{)INN%n<gWM9VJx4DEh>c%W!99-$<x*IQChZCo!jpB=5xOOM%~EnN zzvICtAsx1=wh_Y2#eJhI$(;??x==kktXk&*Cz}6t;1&n>?#X;c!>oF!sz*7{<=g$c zG%U6bc@$6x6hB-)@C}C-mP9Uadkn4--qm^^;T&7X<3QOF>kcP?Hyd|b?gIX^zdy`& z?f=F78voX%F}BZBEf!{pH^voiBJ=W>Sfvwo;NnX;$P@0EP&4^4R-TDK<%71C2Z8aQ zUzSy&oE@|5DY!XmO2G#3;34PhsJXIoqhC@WUwbf~MxiJ-y&wbSY?!|rjb6TSmYob- zqHiv|qgPzYEGg?sUq0#&_w(8b`#O?4Mc1O<Z=|erdQ{a@=qnti45E5-om)If12Th7 zC&{kBg1!^Cpj>qKVsI9CL)RDex!^2CqkAs6)V=xdOG*3A44_$CZ-xJ*Oe#-XYPFD1 z?D%LN<osqcccJ~m!rGb=Z}VeSB#=dipQ2IBYoA|&?8(@>6HOFmOytyIK2S0@X)P^| z_;33rgaOl5IW%UGT>bZQ!A`sGL7w1zR${A_K#}&AAhMh975o>4Vf82DJj#~*^IM6F z!I{i^B4r&mPW1@Y#~Ku4dlCMlzdwY}f8%AGE(<qu=$6BMp3!KOb@|~ejM8?kzIlR+ zAC)x|7cZ$Y3IH<SLjk4e&ZfEfmFGE(lJUc`DCrk07%`kihisbTatgPGH6#1Q1KGN9 zy$+(hF}UVFmGgXCuazV(f8#e0a{fuF-*S>quCbuw@v5WhD6D>V8rp}tz2>Cx8*-oC z*UerOrb!hWy5#{S0~-Ina_Bkt@_G*4(LYrs1*q0{Ni<F7vgY5zXsvD6@M3)`P+6M# zd?ApXs*1eJ;p1~1N)KHGuWar5tO!{C?c<6)K#i=OOJu<KR;j|b#_>#K4uyfSQ_(jX zUi=xkCuJGl!+y07<YHZe&`2PE$!nV%knsljtAJ`wcVl@f4`?}#CK3K!yPZx{9ypLu zyZ8XT)mFrtuBTCZD_y=#6vF9c$3qq<*9<$eCY#DtUd*N8D!p#Lf)*Q0L%nDp<jbbD z`1>2$(^M^EN&a$O4}Hk}zO1&-2XA;2e?E%jia`sGL9U<XwtO?V_}r7V>wtc(moCW$ zHbk5i?qbN*uNg~|IzG>Mk>Wvm(RvmQGd^d8vnx1n^ssCVxb#}gQ92&~;BD_fAX8IG zKY()Yn$WspaO1E28BMAUAH5TH0!!Sck0E<SH})k6cH$@h_rE`c|9{~or=k4{Y+)wT zYxJY;;*+YMdJ{jmsl^Q9rfd&|4d3SU12>@kMRD?Opd!cJ@fON!9ZL-mj=amh{0cd6 zMUsCxO&aA_bNg=qUNYJ=U_FqXtCj4BzTW1i-ByE_{D^o=b9#P)w;z=^3}M%)yX+UG z)KK@a!sc=Xhu(fW!EhPsZ8g`id>iE5ca;b)1NHVznoI7yzW(Lb4AQp_dQbs<zqp6# zDWtD!;6Zjit;qUB!)4Ej3SliWrN_ualzo{*5quW<E(czFQFpyItga{oRzz*RwHK&4 z_U3x3S6o{<#u12*RAy0lGM%=kDa*zbZMaFZL2%nyZD1d^w;UH!nADEra77q;iZ?z~ zui}ee0u4julX44tl)rhGxx5G{wrPLQ0T}Fmr%y1DvB>lDfFipqIiYe42fLqUY2bGE zCcPzpDPL^WqcxXacUj>Q$nnL78Ij;j>(`(uz=+L{%o0ehR(6_(iALo^;oeVn;D#^x z6<_B(Cxr9b$q^3Vo=KbB6QM7jtMg+nc*Bo+%>Z!4??YGYfUJ5<KrrMur}ibeU$;?j zy@7VETpcMLY3!R=NnCPufx9P=*?n-KG$}fDWeC}?u2qpmX+Ydk>J<o7d|Kx-A9Cr) z-Y<l;9dD-T4|!TIUlX!h^O>QNW)Zzvr{D2FiFVbT1(36O;W1`})lpi)?0#+O*%>Go zXWpMM1@hVx1^e=W$}tf=NniP+?FF)f@pDEl1!tjZIi$}oIPsjyMSI7$AvvG^-i+)~ zbl&9~0>nv_A*BQ6K75crP||(fr+5zgnK5;GnLze@ZkZ*O|D(S@4B!9#(M;66;%(GG zIvi8k+mi6og5ZHTEbq8@mk!~#Ubjtv_<qn13zTc*ZeHp|+)B~73Mi@Qa)`QT&)0Lk zsk?Jm>opKp$g=Y`aR{2O=@nbR1CKmka|hh-X+K#yhRfz>PeZmszTQ`*Ck=xR1B$O* z0gj#ZO<0WB-Ttud2=t}y4kjxg_g&w=XEIRKJ$~CdpytyTeK!I#eMZ$~0@KtkT)oMm zn`(FNB0JvZyYX4TekI0Tq`>oS`^l~Xr2*-#)D7&&)z?(cG-sW52a3DCdN!7LW$5&8 zSkgyqyWVC42i|kY^@Sw2PFPLDOtJW)1`P|vg(1B>fYm3vT_Arb7bHE(;^40pKRQzP zD&*FV2f@Y9H`Y@aC{i=D=(yFhd)QGHuPs~XC&LBTshzmr4s=0uQ=$Dzx&S-X>IYq* zy}OW0^}+?x*=O(H45JII<DMs*a-`oJHjDJN4>ZQoh1PL4vxeYzfnTkb(1qATQ?y6= z(uLK$ajm8hYM(Eg!FBH3IenF;g&g}rJ?qxbpbN3%GXkx+q@*O)t<zB27#c7mb{yH0 z8aZVa$txXty2!CnW$TLz>7>6&e;VmA4ar2>sIp3|97)GFJA7J?<XuAiBjgY9*FsH4 zu>Z$!A1*mbjn7;$pL_K1p<BE5<MD}W|G~Gzz0r<Kmtr<2$m{FtL$>XjEXM}d%NfVa zP|wPh<6UAoJR#YA>631_75>(x>}Af>j@jx>;ex(XM?v4WyQ3cLP}%s~JdAFMKlo#+ zm_E_%>(*^B`RDeK4$&OiyZE}wOxh4TBzray8*e{;SG^CR2bqtw*P-b}VLL5Yu&3_k z5P!1k=!A@!Xz$B5=Sqe5G~K=O!wLPzq(=`>8fH;P4;ql(;VIt}(ayZPV#!eWeaX-r z?Sf$E?81!K!K5$yUE#+iyq&rFR-7;VI>J8;w_;;M<A(K5Cpq>*UT?weH`u`J(masI z#@H(DO($b%17qwcOBbql-;A39v;orUgUp`vPt0gsN*f@vO4kNr!z$vFddNCqgUT*$ z9_-!T##O|VzT5qYcBtps$h9xMNdHCDHVNsg#F?gI<0Es<WL0{QeY;sIZvVhWkh0Lm zUw-;a$pW*8SZow-5;ePpV8cxN@QOV}u>YE5=p%YSJ*_FX-$88DJ+bJsAQ<+{tg0Ne zl=Lr_+zgYOwTl~h_`y-MZ|_}xz7+LOy3o_>Jn0`A>K}s*8h^E^-=(C##M?Fk8#C^E zHk4GO{`BPap$ky|&!qMj3dld!RXJ|3-)pJg-}b+W&-Maea>f1ndf2IUqA73*{L?== zbC;Ah3<tf@q4H<3+X5ER#*^_Pe^=OV_+s;*Vs5Bws;})QeTq}f*9Vu=#?R42M^pI2 zSZ2S-k@$9p1Y@q-zVm%1oTPrRxnGzto%&xsdyL@E7q2Yc<-um{R`t5ul7m0ruKndf zzyF>;J%u(hT|V2`qu=x9)%*C6KWDYClb~NyH^;}#kf)vc67Q;SNgIaI-!6w@gRSSP zvnN6kFQ2p$qSED0qq<!g<zftb*Bwm0u$bDPHGS4>+Te;?ygrJ{{^=>t@U?;dp|)$% z(`chB{9%&?HtdX|A1_Vf4nFT0_)*1!Ht3pGI7m4ERvTT?a%?Gn9yTBKp^Z<~Qke#a z4Y1}9R~)!CI&wwv*>SW1xP10Pcg{BEK+9(;&K$;zdGqmUv|*+kebNg4u-T%u#a_-& z*Q#CItF}TN25Dq{#QtEBC{fLk{L|-Jhn4bv32(>rdt?AVJlLQe;6)pEr+eyIlKm&9 zt1RO*ZGZP)*eizg4-C@Z%Bh+J6--*a5dB!oJ?@z(k2x@Cau9CGk2&!CQv@~+HEk=# z4ICJSc$oSuYg#Vpk5HQvfOzYi-Ve)<U!IdSm*RG!U!GM(<2hfsMR5mDw_i3KZaBCh zT4ls()gC^O{&^C`UH$kg`dr4m5p9RI>o5PfT4(h01~KBKXn>>HJUQ}}r}U{C2mg;+ z@kL*bW`_FrA0jubUZAyZt||G+C2QC~^25Yq&uru({9y3%E~LNj)r~Q7_<gmbQiqPW z)@tPh(yxi=IGFq#RXxX&^4-P<ehzZP*TS{O#>>%<Pf|+ea2G~rtN6C%<kbsW^?kXa zE&Q-Ux_@k_9O*Q0Plt8X|M%rq^W;Zjr?0Tt5G4P0cD?beY5ruN?y)s?H11ADDEvr& zkLuP$(qBFKyB+B}FF6xP`k`xXd&{xGU#0zMI>p}(>8@`T{i(sR_s!d*W2!!>%W;E2 z@o=AS74XmGtM5c|vVV<i<e#3n`_FGHVv6tSYmgpAcYoQ$0Hy8ZJgvUOdTL@isnbw~ z7}ByrL+zZfA;Kh{FKJc8-xWWLfV@V0cOB^~wyWpR@9~!0ZS>kIagEPo;pB)9toK1V z+hgzDA1Jx&=;TLwVRwa>>)Mr@r_%C(y{YR+t3;elhAs#4J^F^uhMbq?%$x`mYZQkr zhP-KvVI&=of4Qf877*Vt<7ic`XnnhbaGaB~g?FASaze_&pkKYVZ#W_U%($lq5IahJ zV}a5`H&T=2paYK`iUdluJxr;7-JKF)$3(KbQ-{TnGxqBY{T@%uPb52-XwFkJpwiJa zBAj@q<Zgcvo>@3@E(dJ5bZIXzZG6uDHIOG|eC(PAY`Al`FCi}KSa}13^?r3sAg*CM zvyfxVaF*xXa^Q~py{py(%cDo9m7`pF*-NqmsIb@~JVL@s79M<wa-;kAc3p#9Qs2(! zDA2e?;_f5B9c%1vR08){-_<(+{AYiESgro&x8#e9Gy2oX0bcv{b~6VYcP)mxfgNp} zNtdd`sk)(bvLwA_;~WKah$?)(lgd9ItGmp>4U^it3%}ox`h}V;+NX1unpsn$^Y?yW zQOXorg|bOwZyzK5j9>|!oHhLZo=IVx)S)<)R=<i9do^kGcvN9?`52JJPrmnn!)kb} zY}8@!KCH=@+<&$7oV)_KL;0|lx<RQrq+1ve%St;sxnt8?Y>DQ8J?*Br03+sc7AZhw zRquWj#%$U+Cm%Ub@lYe3_-3O}(qprI6*GaAkuK&G7QE9X$z;MEZ*)9?SRXH-lOq0e zr(KJH%<Jw23FMLpcMlqNb}gS>2?i<?az@7j8Gq}s;Fs(roDH~mS>2Ra4&pk`1-^ux z%S(h|SMw-jC^){pD7+qvY5By^s!N=Fbml6^B@<paQP?u$NtzRYcAf7Ip>V{rMPKM7 zR1y0&p8Uz@wcSf$ES`QVh0=j?feu~|1JrlCM|Lw_^M;TXS~X9hVI;n~;IQBi*B8C$ zc#6vvp28}gs{TL!{UJQH|K&=u^nYG8@4eubun(ZvZA4}@+%_=4;gNo<gl{}b{CCz+ zx$?bZ6QRba2~ngcDLzT=V6RliFNeNp*tL*QaMNe;{xoVuw@imp81M~h&0dhRwENK+ z;LJv4Bz2EC#i%?G@^d|!3Nxs@HoE^%D({zl$%#<n^h?km^4*2pleT49#cJrS8EO6| z5y;f7*m&U604eROVH|`uX8{$iv2-bt&zp1pBVAHj^Yq9v>UP=AVfW~`8uOwzQ@1d+ zr@}cj+pc}h4&2xO<pJuZT9dAweSrKl8}F%*iz-^{xd7R+E9q1}lXVx31dhvUQ<=u$ z)cHh@Zj}ARrMe<JAp4-xI2h&3G$XNqaL?oy)<EB3$>-Lh-0ktMR0<c_lWu(}Jej&i zobZywtSb#dnA(Mu7~=WZwHJS7E<t(PWR-cV=wzYcg$aczYt`zaA5JnHyTAOtl762% z`0_OH7qy>Pd!jsT=u7PTqP+iUZ+mdbr`yTl^c?QJfydHFpM5tOkNSpr%lDJLTXT#a zr4#RbL(3nW-_tZako0cOp0}A$8D2OX=qAzE$wGO^@t6_{U-3$dTf%TDK591$91iW= zj!@|?){)~Jb@_x3^MPqMZhoL^Hmt-fQ4?IEwQG#fZoWuP{lfahoTTA@Q^R%S3h0H# zjF}P&WK){(vR2r$-r%AzYp}j)bkfTrUdiSGrHNDZCqb|FXV=~yK$g-dqhYPM8qr;d zpK`yCf<N88cTb1BI#j_HLhoX*MkgM~|In{DgPd`hqsI`|9_~ZKlZmIEqLXRA?;omz zp{E!tNgN5}RpQqe05z5!y1E$1YWCa|hU=ucb)&!~x6Mi&<k+ZqWwV~rredSIjU`ZH z&H39QC})1{e=Z~BO=I7neu<MwKDEPONX}2hEB@+hvuJ$X-IDD~yx|h}x7@8`BlML& z%5F&T`?|kYR}1#{K341wF4Ob!_ayyb-P<%B;2Q+iV<4}Lx@bZDq_;M#!I|XaUa1PR z@TZL(1`<!ZwkC`)vp$6SQ9SLH|4bn3wrDlIBvbso%az&SE`FkMLVxTyVwpnaXSR*< z1}dEAMBB?T%YHDNp9$pKrn=igE-CaD%>m+CR~H&@OcHj@2&i_X<Y+A9CCPfiODV-= z`A5)C{Pp@-Z)n;OKb^VqI?C<7ca<Sd^^~eU(@)|iX*HWs^ADmt><OQi4%F&krIHI| z_gif`0sXWGIzj7kms;0f4dsQX@A9IsHx##91M;lU|9o~)M~MtvdT{!?8t6ATr^=*q zJO|jS-0Cd$@1Hz!kd>nR-Z6Le4ZxaFy{6N+V~0Bk*G^g3HG9en4fSs#rvt_9#1m*d zXsoHZPJWR1oj&Icj!%=^^8x3-4bq^v84>2?MytJHPyWiHX@P$m>r4HIuX3+CM*Yl0 zpSnWM6q+)1k}EQtPQdSYC;aGZguKbw59a?ood<kL^Pp;n*AI7~YST*B4Iv&9+1818 zNnE7B_2ff@!#gP~=AvBVs9Hom%K4aUa~Fdv?skZw@DnH9apHmOi*7e+riyfnaVZBY z=XU&b1gJ>4J&4LDwOJW;8pxJ};G7roua0-ZfjgQfcckuRF2%W)9B@SV^eN!tkJE1! z6QAGqn;oGGi>5G<J>K~JF!9s@v0mWJ_|k|8K-~|&S{^2@pI1%pXX7Gvk(~Gap_2*} zhlh@g;~+PE-Z2<l;lK1gbsKxB?oPvjo&4!bY52TttrvyCI?v&4YQYuGdtYV%D@+qo z9zgy;y5jD3a6T@&Q!=pW<1m%oKt*n&E`>RZC|FB=DCu0*p6pcAuGk^i+2C=X(u{1< zr(-f8-(yIRbtq?(tbbCuqC@t;ec+PqyWR@^4Sc?WhNYsF^zWB6PM^?mGU<=lo8kaD z|75LbBk|lX8oJ<Y=yq>PSBldqH)t5d>^Ho96y?&AIs0fD;J?lAA-iSc<9#UHWG3u1 z4R3yk<F-VgUiz$8sT_E(4}YhNv_o4Lg0m4LTT-}-8&5}JIO3+zSa*s8jYxyOQgG&w z_tGC6_e*q00!r^x?Wg`0H#`5Zr*ba0r{MRs+k>J=Poqr7oaCAk8HOK5#l_;KKt)X1 zd8(h4w`fb#xZ-T{<YXYzGMVf~@*UBSs2>zJ!j}XRpF56G`e)0oW=3<cD|RTCK{)5x zC}FrI>3fhLlx-((A^Z8dLR&{5bI-8whF-(Vk0TO*tn7iQV5jKD4Dtgz);`jjFv)*J zKSD!g7fOGM4Wcc=G?L9Zh)FK9sfq;h@i{sUC}-}ezs-SsfXjtdKuK=Ee4gZP7Akgf zpr}h*vWIP}HWJ#CJ6?4ZINxv8_cf%Kt(lNb=(FgwE9osuos&;^q<GeH!hj?GE_5wW zS=c%V)A`7wyUt{i{!|tr$W2ZM(`z<vZLv3BMALc40=&x`(|ps-zRO8}d;QN%q+e0t zwUFe6_kz+1FCM=?iS(yuSk0yS7v^PnK;C^p$-QvWS4`|eFGF<n`Q#zIyfDH4p#|x? z1kAD)%GD!^fc(huo<ctdeD<OEWiDUu7XtZJ=Q>dTvY*#I{R!ELxmg^l-LYdI^(PY< zcGVG3v?C74Prchgm<Ih$30ph3ILU{Q{OqS)<X6^U-o}db#r_9^!Ihro5+ROsv{y?= z&NMm*(@Nu@hXJIotsh7IkNZn)rhqGKFQ`!3Wj_NYLj3TJ9dn60c=Zwdp1X6*EYeq8 z@Lfl^wdRe`p5Li*AwKzn;sWR^-dNP<(C<>tuAn$z%DJ|ifc&t;DG7w9%(@HhM$!NM z?+@wxe?OCgip91kn=l*j(M3PUlNl@d#_=3b7IXCz%_6+mv|PaQdFO!2o#(I6FjZ>n z8qFsCCZ}axfRdAARDyt$jG}i^S`ZKY<VWeJ*vaw8V~&Zd`@G(86SL*m3q#*1Ab+{7 zW^Fd`sORUMYk-=?87b7<cDrV5ItTq5U0(h=2_78ti>EZ{a#U^-1MGc<%Si(I<u(?Q zTP50_w`3f)HtLu6C<Yf5A1U_(Hq`GQ6%DL(&UF{MSNwjzH(dfW<6?twDQ!)6HN15N zmk=xC?}g#_?4Iu(x~F^l=O>=Q94=+QbB>&fON`r+H%>W>OPMF#v-rIzPxKjRNZra` zjy>!Ld6nVO#Awtv!s?l(Kk#tyq1)^6ySSqhu^PrD&HA4+g|wAiywM+g$+zu=i(tp> z)KLm2!a_T@)f{dCpF3?FPoF5r*>>Iq_9zZD&!qBF6RFl>;9Z%GvokQVS8ZAn2fgAg zmUbNORX;O&0EKO}@_Ou2lrOmHcs+#l6%X}i$&-_6>cgij#_%)Px_)*#j(_N$YjPy) zJ2Y7%(-K<@?m-^s!f|QN&0>C(1GWwVAJ<qVl76=YE*JI8c1$Xbh5n7Rolgf~t6=;% zxt%lgtXd8!nkawtaO3&tV0#SrTN<N^$j;dw9-8s+--xBhyLsS};=zPlnjUg&wRD~4 zI2_!t^tVwaoejL#dA}5Td-S@F&j%WoeZ@tA<RsJ4LH6-L%^BCTL$KA7VR$`Y73$9# za%J%<vfJMJ4c#k0c+T?dS=hq48}nvQG%o#}IhGi10SrubyCa;PL^`#wfSzyb{$pqh zqwac_F$+;|V5+JM4|_hWJ!0kw{dl`Wy^>*%zoOUXSoE*&uE;&p2<=_IXrrAfpL*_^ z1G_yvt0p*LtElJOU)sUcUeU4QP<bx4U<@2va`|~}pC7MZjAxQMX4bR_CO?gSw0a_k zt)91A=EaadBN|4hQ@vTkZwJAisli{(tfBuXu&rtc^vfz*kDW-DNRPL_;6U}Wq2){E zh{Iv8>)p}r44wA+UR1B_xZPaRKh}D%HSDwcT=$jUF`Ko@cMMOLe22%}G>1L2ZC3V} zhjvcBam_md_HS9ca=i<9dR#z?zo36{=Q`9o%S&x{I*v0nZ&9yFsBddvt!NhfV&&i7 z%mV%E_PPZx^e^A_(+aZxR{wc{9L9<I_AB#X@2??|IBSGGGk;`GL4RCZJ8`l%>c6GP z)m$M*f4%8e8A|QVP;s3>TWH&!yqG8lo*6JokG8nB<PTnm>A=7C{=0P)Ka(FsQ@nlp zJokt<{9~^6ZcR9Fp`AxR3B4oQ^_P7l-fdl97ps<se%obHzEXmBOBZO3jS;4~lXE&o z$)Oi7Zs|+taz8#?(C@09OLl4I%?%~}tb>n*G(X+_uf?QyVZ$$PdI$EsM<WtRe}r{1 z={sh##|t^=7wjKP`G;+Hz)K4MTIUsAG+WTW?oz!<&<`54nf!V=epM3p+<1E6H+9Nu z{GyD&6x2H=yHTBvQ`GK|BnG^5tJ>x?xufIRC&BTBsNdBp=sQhQioqMSg7FS-o4F?s z&%ryochot5Er9;UfOxkvsQ;J$=r;#Z?mVFP#ysfb!(Z=mpy#(LCoY!kYJ2Tk7`5lh z=Z*Gc&&k3UtMJZl<7OitU$XyY)3pWo{rNVbIEw{dOV?UR$Gdb^eL4E!!u5`I*|4Wi zX~gAl(%)M7Xff%fS8a19#9O@YnfC?QTv}3r>3&y!Qc(qQRkwVaCz$5SD+0f>z4T9a zeRltsi$x9p*4u%Ui7o1B+QFSLFND=h&aaq7;C#kYYg(=8-D013Dt~8Y7XW;fQLI)( z+`GvBEQjf8f6Nw|PG07jdmaRO1_qmK0Zy&(T@Vkn2@yS_X@#ko$<A|_^!Jb1ydHd5 z=2YRDr)L+7u3N$NeO4H(0w%>pb^d$S-jSpY&Z0v2A;5^eVhx%Gcv;m6S|uoawj0`l zi(EA|glUu&F-vf1OFKr>*G(_)zhAjOV&HRca9N@0f%$|%FXmD?(=a%{l0!Y8Qk^ON z^SN{JC?&!{rzui6zD`w`7vxE==C@h`Im;c|SU`H)H~*&9OoM~ooehvnz7)3(0xANR z_w<BZqH{}ASjF&dzY2bP*{gz<1ESpdJ>~-&l(#z5bSbG_5#$7%<mwc(7|0~c7sL{3 zd4&qA)gc)MmU1k*i>C`u=!%XO46z3f{&M+(59#rzYlT&km$!K+<T5*}MZS=W2R{Bl z$I%d-Iqm^@+U&7ssC`Vic#*J*YGIottgbgSJ)?^z;@@L$lRe7oIl@+kxYhWFlS!YM za1;lM-HJ<gr1#5Ur!$aO4ctwuILQpJLPwz8<bY2U7wGv}w5sD5s7xfgS&#buX5i9~ z*A|c;CHsGrxB!*U2L*XUjvkLnCTz@hw}o8%^{G4cqut=B4Z`ZptX4vHO9yzYUIxTd z32`ep;NFHV!Yc66%LQaV`}xRTOepCr?hU=_mlrA{flOH=tXBCcMn9<E*|^|E)NTcL zcLMo^xvI1^=TPm^Z<mDME9M_v29C=mN!<z84pE;C)JWDg5c;vc`3L#C@>1R9bRa$* z<xOXcQk%9(vEX)=A9E;Ov9oGB2u1e;=Fl?k=4{P2yGX89zxyhO`fl|<yb=8ADGR5A zz#WNQy9nri{#PF9{iDA>tX8}IOL|cJTj>G))t<Ule24|E2C~@qjTCOI;9&)IC*P+y zC64&7mw(@<^=N162Jlhg3*)OeG<T!x8cGj8*EzQ@1uA!M9Iyi@^KNxC704oD@B}j4 z$PWA#y8V!bi38-4OV(A1kTdm$ArwA*yCgMt!mmCj3nAZ8w|<!vT(o?VMHEoU=9Ex) zD3+)^@C8@A>|a5{h@bn`zX)>8wz>PM8x(uq_n_{V%%A5)`qj%%ET=TUju|OUATL<> z>iQL+QNiG$Hy{@^cIp@nt~}bpI~x2<uc~2We?x;_*eYPjn6@hAkUMFMixlAehc{Cx zjDuTs_(b;WRYkw$fvm(h+J|sqzVO})(L?(Ww0h^O^B#FZ&M&yzKLaRk4*N)Hj$LnK z>q0o-p;5dXcK?Xh64Ha(i*G~0nPlb7AS$o2;>j<pdXt|9;j-x+DZIqtV}mzQxqi?5 z(S+?j20K%^)UW#(;-SZK#)B*Va=x*f_$UMGP2kdy0dFY%@g5thg9(>-zfUCm;#x~w z?0#^Fl!HI2RUfPeXVp8q3vqDmgCqHa8Q-&~{^PruoS6YV=3o|0{ujS%KE0aCw`qC^ z{TXp~cNTH&Q8u1H#TApY4$$K#OGhLDSxRubML@;<!S+IbO83;Sgq|pB!pjh#!oJ#= z(o*ovMKccpl|`0osDH&9o+eTKii;gjP<$y}x4okH;JeL8vE^Xyii?Y10RzvxIjw|T zcE7i*5GaiqJajKmZ_4n1qd-yeOIPv-TiE{477o}?d+vImqH>ur#j|)*r?0tG9-$r} z^pE=K1|dxt+Rxt#IX|a^15FRuQ?i^+dTPPlg!CC+{9b4u`?gq2<^SmK55xWc^T-{> zZ=A5GU`=Pu=~$+2!pSHT`OOiyc#xw|{?l$3P|V+0LT<%d-R=lsX?Wd4=-w8^JIzSG zYMPNSJ5*&KCAs4HoRC>0SKN9**^XI;?l1-N@1G2(*+J^1@hE`GL%tdfBwX6@gayi( z;?z|^f7z~V>K4Ud@BSV@zV6;bcc56iU=01fDaHD2gdF<fRbi`1zS!5EW>=|~?BN2S zQvLa836)Ri;A;*X*RGEVbtlU`-$ua!)=h9v2ESr3O@9k8aHxTBU*M!Uim%jd%Hz+4 zlRu_7rV<BzF&9<lLGqQ~liYxIe#J7O{-xVu9Krd??FKnPkFoX%G#nKj$IT;sX{oh@ z^pk!sU9kknUS1t7q_L5gpL;_O7w>{epKlSBGzaAkrW?x_0~OJevIBtZ{>h!xF1CMV zhTvz(je}_%e&&7En8LYs#t4mApm<Y)>mndO>01a58;uFyrF1fluWH|-FqKA2G$`El zUVODq0P<t{Z6rUji2?QVfR#?KaeotdQu57G4nM2^5|jd7V|{ry4d?1dJK70pz@(xt z51y8H6;FFXpJg7T_Otm;Y&r)!R_Cp71F~W4T`=LM*H0)8f=8X&vY0p<zj+Q&d}O?e zI~K*?dwrWDT+{mUDUwcx9E~jMY~{Fu?;Br|Li&~52_YRch2ExD;BJ5Hd@Kr!?-M_E zrwJbpDY~R5hQ9jra&ZFb>+Twn2F|_=?r#QtYYoqM`Vfx6lBv8chm)Eq4tw7rz7<{% zb%f!^8(AjecgoPl?D_bep6ORrUvS-e^-D3}iVKrm$e(F<mn!`s#~q{gGl7zk4XNIc zS1*rjy#T!0ZTtElaQ5YK4fU_`#JIjfduyC4GEgo$ymO%h^5=R%UbG6RW~~aB0r}KH zWfOpU9UCt!;qd#kz^>G8@z0i<DQ-+}UeX|cD^m+MlfM-grVpgmhT@TZZ<<c%sU{j9 ztW`-*l8c6&E|^Q)!tjw5m3!<i<w(BRW<WT}+4wSHTzY*pC3_@;opbDfieXIH(o%SS z&82uydKf+y+9Po(qqya_+|j3auAcI}LBhe_)i;WHAUoT9!w4wWTJlJUC$XiLV9&b! z5!TR`u2_0Zu*cP_vj#Y;u<{Z?f5&zIvT)+TnKvk&S;K1&@|&IM+w0U1{41AQ$^(2< zlhrh!%+P$l9}w$-6dLdBhJP^G+0gARBYl~Um2e-d`0-zKcEUe>k&tnm^nINIu0cP^ z`Bp$Sxcv3!`v-wKT~m$r0OQRP`cQnMrk#s~e22GDBR=ppz!OIVE^AmJpjGNR0pDtr z2wOW@mhA7R>HJG|@m8;L{A0@!GW{#?pH-RmnYy+uY}@_IGE4byJ$t8ooA+cL>{E+# z&p*H+Jkr<Qr}54t?ObSl1t*U)ECQG1{PtS~6#M<wrhW?DvTjZp%A0JiY>NkHwiiT) zseHmJP0E|PGKZTa_p8vEN2^wquW9}VIpoE(>a{eD;RBjw5^%LXAs$qJnz^pma-iO$ z+O{hp4|Y`>z7^cAzr=biP@KI(i+-=Hj!Y-MW6wA*vS;e6($GT4U22Z^I0P<Tuc{Ia z&SoTN<%3t+cneR;m)!O`7zepV&MTix$Ti;G>rM5Et+rXwaaqvimfHw}J(T@{3ahp* z-arl40~5$TrsdF{#;4*$zh)X=?Aor&3UIwsvZ@2bFL!H6%Zwd=jgLG540|v+y9OxD zP1{inRJeUS`HDlnE4HgR1#VZpre-JbpZ)z|8vf5;6d@Y5^3-~m$;2IxZUahO=glRz zh>k4?3IrNAI`9d^2lw3(3asr>!l;{AWwQr$1FKnELft3HUweKQ<hqw8Uk(O}hd6ek zX_ZAb%=aN|`0AAo<U6IN(KIPN;veY%o|M$aL`Xw>2MCws)9P-{4MRC@V7o!}Dr~<Q z$ARn3$TOo=8hf$HvOnRfp+6`M{2o^SVJ~sz@qN_&l3nc{W&xW5jJ}b*dT|>5^MT@R zW4$Sz_>E5PDC3~7w=#jcv-;7a<n55J`CRnyF!0yLunp8rn#Mo2rU73z=vtO=(3f^m z-3Ko1vHSK0DtDUtj?(bh>*bRv+?o1?brhbbd_CI=_2)Y^Qd$t{=-6iidpy1~gZx}D zHnlCKV`;+n6!MGmcx?dL&2o>o$pHo*kE|<#+%F*KayXR-#9Ful#lK7jx^sA@>Y&N| zO6XnM^)&G%<ZHHd{(cMT=|I4OBNz^R``%qvP<ZMd4<DTezP3xR=V!t7F4})M2h2P$ zP)&(?Hhr3^MS1~SJ#Xy?^4<Dckeyo(U4JbFG6(;ylpY#5?OQY)#OGDYaC{~<DgQbd zh%GK3U7%!W*ehZ9DRy352Ck^wW}PZWxJ%FJF9p|JX!|Y%oPAHtnhGu|)6Jp&XALz) z$HCK91U(=>DO{bedjKmBKYBpvg8$IkHVF97{{Aprdi_iGl&So)s{cp(?Ga%$gg1<v z<Zv+a?BelD$;@wqTT&Q{I<-!sY$$d#y6yw4E=l-Fs~o0M)|<jjIj_U21n`O!=g1vE zzHOVW6iy|nBYwvNrC$s9SRmfe{wb3~xz{>Na)U-2*P%456bo$hX%(k1yqh}{DC_C9 zGm3-0V#+fCZR)pCnDY;MRMGI|T^fZOc+yV$#rT1%UkrSgP5Nr)H@1Q^Gb2-LaQ;YT zNg`08YMi+csCQcBlrK;}BhQP56RT<bk`B(ac1<<}N+R#rIs<v@$G)b-zt}vrr}B!L zD}xDT!#-1XWDdPrb_Q0q&l|oR@`m+2uTuTNKbD@Q@XvOd*hIsQ?;d`x8eCU(<MNH* zn;Jj*QCMtpp3t6#-95Sgbh1Mc66r_n!+V7uZshR04y7Gv)ua2|T)3tuYF#ue1U%UM zlnJ$8%qoMaUl?x~zL*2}im?>~zDwF=0P*-1LAJz2DSOO;qOlK#kseO74SnT+Cn4J{ zfD*fn!VBT~wH?amfQt<(9yk)->2uYe^muni@-OT2<jx2Vczj6N7;wD~k;g)ahkrJw zVa$Agwx?ARQ{?X@JNcqm1%<ucZPUIhP%fVF@UpO)TIOxg5Ay2T!CEVU*!f*VX@Ldx zK0t9L>QXu`1z7oZh5^NstW)1n6c>E`#vn>}OqnAkf3wKfxSJe@JK{HJ0l3}el(MDZ z%DDNfoPcR-d+&|{;;}*lGeYzC=ZrYSq1wj}OMvXbCnpP_(%^I^*_9TjE~DQw&5W`< zDsQ&f<^V1Wn{=GgG&|AnG1XT+En=k?@fRmL&E$Y$3ls8#a>jn)`&~_~zPJ%j(i++# z40_UUHt#%v;*ouK3hBfy?9+O1#Wq8c8}u~B|8}MHA5jr2yeL6(-KmuXTzn~BxTpbx zI#GYKcHzErfihF=4&f;0XSm)cfAcT82TlfxZG#_qLyj9fld^#dCTmIkAXAlJrZ`DD zzr~Q+VK?l;Od4<e+!bl_fGn;4Ljeano!jl9_Ss$9X%q^SWj|j*{+ABz&8VIdd(*a? zfy`9(mz=}Tau@wh0<RGFQlarCp7JuR5UA+w@K)$osQ%yo{*d1O3$IwdVl<E3AQoxm zZ{~nctj@)PGufVjwuEf-wiv?tpCQW$S5yzB<|}*8la_IC*XxHf!+`9!WpA2Qm`Lju zHJ9m4Os3z7hBdb#_dK3)x!rBxh~y&SyB^hd&bP}1Z|<*E_>e=kjC$2(3wY(uX+}qY z%CZr{`&jmLFj%}7d`;fK10}!~#UoX2aJa(y<3hm>a7Ek@?{J`9`rc>jf#Rq6N>7yY z0oU^vf-^mpr`h1l=1i(TP#ojpLft9u{Gf+m_vZZQ>7?h#<qIdt?D29MHY_vX%mmV} z`dLqQ>Ruf?SO%^ruI*35kdLi!71Eu!^VwD4Y-mM43XgyG_lNHN&u1_GtxJ3AUiPki zV+IGar}fOC=8FQ#T2quNtq!(Igq-!;JBCIj>!iQY87O}CAU1?}cKM?5#77$k`*HYv zOZ_8Ggwq!;rO~MDwq+=}AviF2eJ=4HIj?CH6&R0sMco-c<mX>oAQyAviztimd$rXw zfT26mt8yU!S>Q5)e(y3{y($ryyE@|J2FN7|Pc?+_`SN;f4!#~(&0h3-iSkjk+uu-k zH1yh}k8T&=p8C8^E%f!g=Dne?<gN9?sXL@sKgX_xoF(e7vZnI7ox<m1YVL)Oivw45 z|2RR&;x{ZFMMB@WqcdLW2)``9T1&I9sI|)UmB73)Lk3Z|^M}T5od?dVgxw;4@>UY7 z2p|g(FPcl`7b?XRZju_8>&f6OVZjm)!c#`Q-3fPINlF4rTnvWz%E9~Z>+DA8p6(_L zFCFU?s-GG6^A^HUvERfL`r_4{1B9@YK3hk#D(l^4Jo%lCs5_$%<mWh)1OcU;uhf%V zRQpYxW*63QUQMWfaM?T?a6Up7ZBKHC!NOM@bq{^EqW<R(_1Bon0VkC4zCgw5Yl~@E zh|eEB>PcJ~Bn*cJ`7(8Cuf)3hCU+q3&`KVNa^|r60JWbD`Fz+Ca<*mW8$EDEpL-V6 z9*KU*Jvy%V?ya@+A!m9~CpmD|=jlBYaK2r~_hhHu`%^iTUP|omzNU7FJAL#Zd)WFZ z+k|#K|7bw==+&pxQosCUuA?b_N~X$=)9QdX>9U{tksZ!HEAU=_y|%@*n@RerZMWl^ zP|*j^RN9~&_2p<{I`qcQZv3$w<?9F9*l$JoN#8pY>9a=d@;mwl00&o%b>0B|yvTEI z6t{L$Pu^Vty{x~)-J^gyNk3faMgxP=Q$0!kF7oN5Bn}(cw_Lv(LSG!5DD#EfF|OY` z7u2`wXk)4|2m1z|QqcJ0-;P|rg7_)z<%5AdYdKh$&1&jK+LL_xeKk9h_q~}EFUR;g z(xUAFa8d2E;Q8Qba-G5~Aaiqmkq6`h)c278*qaOQDGvEZ&bNg6uee^HLh^Iz+IEDS z;uZ_55m|3%UE(*tNol-_wO(_19QrLUNt5hjCR<JXfPB?R;lm2d_N}lAMae(-`$KyB z|2>VsYA@2I6UFRNr-e%;`xeoqW2{2`6(r{qAC(Gx=Gg6oiv9!0{K~^+cn2rkw9$JE zbyrQ(qKj$Z`sH;!DO@z(>5d?`Fgt!rFyvQ4@{5pGnDSts%_Of0^yCU4uhvbwMQK4a ztiRiAAk(d~TMN7r64-w;FgRWI5orf6t+F}0kHb0J)Rs2Oz%@3euJ-}rb6#1CfjySB zSe;G$ckb$LK+W|-pKsv6TiSIOW>sB>@!y4Z>~jo3`G}gkqmqFQHg=1>N#8%D|0d{* zMZG4h0Jp1|Bs@&Q#BY-)g4el}v`++zoR3(Ooe{~)4Kg^Sfp_<2F9O$ycIhj0^XEIK z*5TamsrHt*gD6k?q0^nx!<MT4la2tzTGPTPz3>r>X8#4m7YQ~^;21kyId)_bxL*sS zqvX#AT==40kW18$3-@g)O$Jy}7_zi)Czb$DtL1xb0s1ZO-9yknrrU83>HnO4jlxN3 zujTFq#G6EVh61Or4bRO2^2aR)n*%33zSb=G=j^T<G<?_*mxC6BlDBUuT`F&0`}?l{ zdc)8;q~EOKNbS}29sM{CT>L_1q+pNum2h%U9rf;DEad!zn@e1v$F_VsvkJ(MRV#5L z{_a?#B_V%(ksh#Qab#IEu;H9@Z^3UTRfRY8i%L%gP=DYlgtgOwY~Zqm;n35x)r^Y* zN|Ga0W<joOy)2oICssS8F2v2U<=07H@^gw4;*5X0uH%y{(3kcsETuGh_2IAY=YU(T zT1Dpr(@u;%MRBBHZe>)Dsd3v=QV#V_oOy%#vwB>?<#j-pHL<o7?`)xB5FJ-?d76^^ zrabnB(ex~e51ud=az&kOZUK<x?(abT$!FP~r8I0euYJ~b@S1O0!Zirq`|37ozwD9K zK-kF?pN&Qafb(r#Vr_t{8`|7`&9StkWj@EAqC9kgSThgULUa9Y1<*7tOnW!5o6bX( zRG|3olPhaEtjWzEI;;nm`kk+!epgKL{SX8>Gw|;wOp6h>htjLl@gk|7OCX=Bu`{3i z>DTe+I}hkr@0(UlaiciAGjj$|&*=1HXCMpK%J$~)nkGIuH4B(lKh1k4<a2tS9wi0h z%_*Lg_VhHSu5ku3<=ENer*(H`86=XtX51^9Zp0_E_BeodyJ0PSRj+zNO+6i#DZ(S| zAQyM+cEtc(PdvT{{a$SGu*EFMS-0K$=_x4j=<R)J9HJwp>O*d~Wm*W0W8O)5-J0|! zwXh?*lr{&nsNLcz#y(UptJWMG4Y}#fO<yv=6_)Scc!Kjstw-7cMXCdpl*UE<m77S; zg8RqNcw(b}C65L2KPuWd0vilF_{DM1YtH}s1`LD!!>mY;O*u>BIVso7nU*1n(!ROG z#b-WF3If)YItlqwq-}2D1dcyDnUIIxpF1w#^oZSre7NOpbHa^|C4&5E=goh*U+6!7 z)K=^>bGgucxr%TheHPTX>yVj#&W%$zVBqlA)GT}=;lXsEH1MMKUJS31o7ca*0UkW~ z{mBeDrh{t+<H~`GdlNHCfDNukXKVy2J@d+G7@3}V;8w&T>{FY+B!cr>+h!~QvYJlA zC=A4V@;!(LS2j+jZhBDtBkCBqqT^~C3O~h<?`{-+EXDcQR>I@Qa#sR%gKl;&l_N@~ zO4IKFr?xU|+zFJ;(<vZ3q!+Jmc*)`S7uoG%@U*&7-)OiiwnhuDS5wG4Eu*j(nH_N1 z1mwSqWi-vOj|Zocp0tc^;{+e7xqCh2{JN<33y4p*^l>KCpY<w?a8N<vLc%@UZwu3n z$v!IzJHAGzkiwF2_xcLMkY6c_;J}-%CYlo}{JRMCJE(SzA-!du7e@mX_dkRrQaRIf z<OrLtB~#e**X~Utecr>}BbjiEt%;BZwj2*5zu{Jgh-}j9Ic392l>f88Kdc^f|K-vi zuPRjC=0I=CX7~<{m(aW`^XJg0;N5$K+5A7+zB8(drD?a#iYP%8%v*wD0u)44WQHU` zK(a*HNCv@8kRYH#6fps!1O>K$iik>55tThEB4*41(G4a{hzN@LR?S)8TIa`I>wM>Z z@8!?KQ&rPb(_LNNGt<)>anN#guGh}f(_93cR-fL<RTE=+Jr|B+N({DOb+#l?1~Gs- z{%g2LbG+?)xo?(U6b+5s4|4d_ZO>J(C-U~Pc=K{cqy5?mz#GG?S04q8{^@i-4X`Eg z*+K5IyCvoKQDATSY!+6R!a()nWWc%)ZB`5*|Lv-ie36RT?bjAl)>BVT1h-1B7EsD& zvL0dGJ|bnatQQr|Zd8`?p+pC@4WcaN%L5|Ce9_6^kxP!l_G7Ilf_JVPUIzNj*AJx# zK;9bNbS6w>mSul$hhQ5X|9Q$w8<4x22JG_@g|wbHTWFbp<;RTL<0XRgEt%AzDl*S1 zm0i*7jP@rZq+G$DtlzwOmQb$Qz{P`7K`wji;YU8|O&?8irA`JPPI@pZg`&Zssn#zW zz+Pg{sW7Tn_vV(Ei5a58*D-o~mRXB#evZ5PB9EaIQm1K~dxoR_(LneJ1InFIx+R@j zU-<f^%bSB>r~6=YlRw(eC>WSRrFy0{Sgzkgb;cCd+=y9#*L6PhK>(IJ#Th<gN%d41 z7aK(3@t=qOw4>%p8vgW*3!o$~XgJ>TGlcrCwEg4EO{%54A`^hOOf69pKz+kHsdpF$ zY_t4vSCBI+h9*O|XD&tzpld<SnCbnx4|4msx0`Z7K3=8qDhu*Ahm39N0j-_Slwo?3 z4!Fwq1BCvj=t#kF;mYG|QD0=+6AYYm%(qwyobi{hGX#|Go0q>D5FVyH#%{-)d|HO~ zbg<D$OW?G3&>U~%ow_n87r&W6VLZY*{o6}HFSL6mat1C~H6|wjI6w4W1$SPh1<SNR z-aUB3Xs-LxCr06D(lTJl5G+q;Q(jIC+Ruwy>H)}l2JSQf`I(>ylT1L8w(QDokhdfs zzKN6mz2~GaZv}2|+3B7RTyQ?^GuETHt?4n>Uw9tlG5ye2Upe7*HFxvsF>RGq58j3S zh7G9g(ubR$T@t5X3c!xg3P&9fS*JAaX6Ve1FjL^PW}^&O-t+w1)@Z*tQ1q95&%jk^ zZ_&><<IU3(rK?83g%@uQ=GyN^_bnTc&l>xuoR4-)<20`P^8+8RLHmGXHI5<>hc<U| z<($!et_hqT+fj$B?Hx9+x5>l}bsTg{)F>#&c-V`zfD(RH%m%>njUTeDp<JduSr~vk zOYz1w&i=~>RkR;#^eqJKi^OkYP`_zev@QB^cfP+imcP#MZ3JA8gF`Ge80{aN8;bU` zCW*ZqQGY8*MhiDKTG`k$A68Z8UB2VI88`e{`?&&Fc^d_{=?QQnpv^mfMB}RLQpx43 zDA;)DXyq&1s3`C94U;s^KJ20i+8?Q%77MGeyRxKH133HrDb9HO#pbEeu*zHWvjqI2 z1gu_nHIntDV70k+f_W0G2AA13dsYapCO61B;)(V{7|2hss(sF}s7$oqGi!z?tddW2 z2^z2#SCL=)Ja99fuUP6ScOKI)qKy7HOJ5DfkBjQ<*XoI&K5mS?<$(56Eed_nUfEGm z1<tFOrFVtfk>6S4y${xp`}ZdXoV~Gx8`>A0$n`}3P34BjQ{J!xi?i1_kr*~;`e6A3 zXCsX&g%~FerykeaV7YfENgE(qq^>j&{abrjjeo(TirXMc@!{8n5Fcdljw?2J9ajz} zO-FwNFWuzMbL|4(L<D}7&0VGmtKT(i)<j0ZYJHh$RpojppHPtMh4p{?rdcr6-PIN1 zabc7Q{o8)Yl;Y~=%{JtwiyW;HN5z)JV|~wz{1%Vrk!_l!iS4nV&n*PY51+x0!i}li zpB4|p_A_qD@y4%8-ro~&zjQHernD({-al2LSbogOfFu!|zsKi+9kxfJ!J#me#~(`d zLEOibn{)o8Pd0<E1AKkgeDOp3u=F)9_*F~A$w70m-5c!hM#9$s*IKk*IitSIyy*E~ zzi2RPF#4(ZLQmX)@+*n04NAZt4{mFKb~`;eP5e)@!~cFYIc#Wl0=t=T<PU<gK)PY; z#xQI)dRZ|J?_|Vovp_)FqGwzLa^~P%OnYpNwK1kKy4U^@rZ?vLh$y@%X?b(F6U$*A z+k0X<gmL{0&i<H>zm&O~7I!)gIqz^#L^{~<6mC{U0jJ-!%F=+OTfa6N<du7j_u(v3 zdXN8lPH$h>6#;VQu*(-r$9yRV__zT5>F~mIr4Z0bX9H(=;r^HgNukS4FGPvV8R!pV zb#fxdb5q{nwBbpN>hFUhxEWcs<Igt0hhN_Y=Kxl$Xiz!=dU2fmd2Y5w&pU<bvvNw{ z3!HwiC+q+Es9<Q<15AI?ab>^pI*9zS?|k4)TebQn;Mr>z$p`^ePf^;D@F{gs^||14 zpyyL#dX51WuHW+48%n3O57EEKT>0`tAkVnueL4tG^=8>QyuR#BQ+^)+J^NzV3oSrW zJl-AsTa%*Ct=4Bhe2`iTcGEk49Kq|~TECG(6hvOC3<iW}n_b5!7?yI6t=S5kCu_(3 z*6ixl+jgU!WX_W;d}yE%axoo;BjP=z;TZ*+ruI-BEx>>7ohOk8xXe+jtpae3ipl`I z4mbVmt}H-U<$Vo@%j5^Hxf#HP=bNRlori0G8ISE}?O?WSFX)+Hf20-w!h*tuA%F{K zlukSdNbetb4E<jt|85leA@|iK3H@SLQct+{jc(N~0={KTM|?2_hs&MNw*g+}cDo?~ zutmMWD-`7^*JdgLCnP5Z+p#k9Krr4t@MG<!Vtne{ypwwfIO{!fmJqnOYGMcu`%q)K zE+8jEpB)MY<aaGKKzm^>U#bAO^vCnp@`2a=T<gvC3o`Z~)>FoY!h0#8=f`?}-v~Tf zQNaw`sU=+7sswoV9Ve}IfY1PGHXs)|%uGrGBnC<!?E&dD^C=j2<b&`SPM63%dv9aJ zfA#kdvw8Wy&E~EDvlsPc#yeclO=*weS>PV8XJ#69M{+QH1~w19YGd$fK=xIR8aAVN z?Z)#y6zKVevX~~sQ2_#+y)dDh+?E4pX8b04!1*;gld+oFC$R>OfP}x}HrI?|w^<e_ z52~5$it@xQ(<ULh1})bCJT<XgvI>xyQRsh*!Y@t7r(?Lh=XFG%0!+AP$LWo}nb++E zE<O;m*Ao!7bInVp0Gd8+UJsmo);l5yIQ#gbs~>PW^yigTfXw-VVk<ycr!sIW=*2c) z{d|!(S~)lYmOVc_E*em%Tvg-)D1JHe23J0B@uei>Z0RQC!Y|Kj<0uF_{r>29Kw@ws zQw`*ejf!RH7b!ashT+VgH01|Z&drik2ITyVo;XMs&|Ug`4FTDN*}v_;4whZlYylL0 zQ})GrCUjUH9>+A@=;QnuZ7;_24p)Bi*Zxl8@gY0Gp3HB!i0#K4cX~+xV9SH~*SNGW z(8FFExbT{J3(85xX=*nG<t4YlHe`@KN)(TwAN?jfwqn?`Ct|JfIB}M-{CW!PNzZUI zz|I$6%2Cf4dFa{$@{?=gSAjg^dHNC@HrQ*U=7s|&tZZ^7a3*&R++6_9E%6NlY`zq+ z0?XsAD|%#&a*@efoJ~UjoJav|S)>w;@x@TFC%gbNR;jsS`|)2^-dTxqpUT(}K=RPX z3G35ovbq!J&*KH$Zxah@ml$JxckbDnirrCowZ$$K?ZwaL;`Ndv&(Bx_^7OiX;PqPP zX_&;Ke*32<h9Hmr)tX<7Jn)t5Ip6~Sl`-3an@W71QwSJhS3Pt;pm6(V?gv}CpY)Lf zz{%=kr<*8vRJiQ)YmCnqY98E%QVr1>$I5^+hyC+*pq+GC$y~4_s=XI6Eir@cXRHM- z99MdAGhoI*?;Bj&sFAYe>iuxrU%$r5(~{th*Pfc>19r?Jy~$jhHH;hMjC}Zoncj#I zMUSx@STPZFPz2mX`A-yZ_UmA$G(hqDZ%jJs@20--Li|^M|B!xW{Vn~7|9Y5^VH`ah zPeX71DvxN-Q!xi*YRdX)5%8O;Uv&^+2b5WW%$%e>7$%Ht1-HQrnSEkEhJ#>S!a7Vp z;zzgcU|0%1E6Sjp6n0A3Qh>ZW*;oxkTjq%y$Z5T#tB%0g;JxY&z{Nc|9@c=&3<D+X zrbMT?3bQzgw6w#p%D8DWL;!N8^+et>;OtAqNGsHTY5nL2a`ygNSv)V{YnRFaCpPa2 z79%=qjYnjvA2n;BzO#9*B_Mss_Jl9$EoQF9uo53>kmdZ<QQ9Ddb}6G~&ql00vXkou z3s#L^g?#bFK=hY>aZm~UXN=nJuK*;+J9|}8-(vo70rLAAhFZuY8r4<+GR=qhG{_mF zzIJ`o2fpc<BD!TP|9#y32vL2fV(RKJ3#w$xlpdx2B+5PFmXTFqs3_u=u^?@~rAV#5 zv+C{21)^p0?t;bB9f7CK)1ZB+t!nE#H_Q1`^_|80`0Kr}9Aa|7UL^0{>+jKGEGpTe zHl=1|fJnVQZGm%DAhk?>X|nFOby)uJ%spYEH^p0cN9q=#y+uxxA^J11e3U@s9wD=Q z`3zgMziVa^hUIV0?e&54w_UOw?Lev5cLt2Vrh(-TOZV~-Ic_j`e5`W4XoG><*`p4r z)VC3#6{Eh}P^yiO7N>4<5dq(#vD+T}leLr7IV}1-ro2T#zeyz5@mBnK$bG6Z>d7Yg zFBPI`Hx4XH9(PDYO$%9LoxchETRv**#B|Z_qVA{brLsgq=?9TFyGp3Uv(IB~|HO!( z{Bwolt+9M9^3e|Luht3YB%uAcE$vxQ&e7A(HTl%%F&%7cg(b!Rm|G{=riZ*&eT$K( zJxNO9*b@`cfSGgqy2cAczmn^kidV-`i{r=3H@;a6{yT3|t#uWZ)=sQEv)EcRHLiQe z5f>v0?B!ifN8)+i7fJb32i2eVoVn~rF$Q~g4X&6VG7VT*skPl4%h&#O%M8oE5#}C> z_V+C1g3<m!{gMF6I<2PbWSb@0_qUf~Toaz=Mo)+jp7>)GrauziC$Ise*R>k0#+&QI zE`vNNxT$}gZ>0iu(9>=Zv>WiseL13C*JL|DrrgZH0qo1?P^>xLjL$p#Ne=Z)$0<xJ zJa%&s_E&OelZidr&nkostf1WcMVD-;vhwouPe<S3Bk`jnTod4UpILn?aAyvOT>E|4 zPic3RpFy}Au<_c5@^CXbOM4Etkr?StcW?#!1+y=jL{lJNbDCSH#42ui7Jv^Iv@V@< zMtjGcXmdROdnQ}J+1oe80*(z-i9^5Wg6xex$S+QLif^9BhOZrfTMy)^TsabjpML9@ zy<9Kyhkq0o55}9l^e~qt+)Zm}rYv&FKc6)4rqE#zY%KtMvR|kJ;!^K4KHO~9Sak*Q z@n-Nvva_2A?!hGwnK|N3=;5Wxet2`ZSVv%m$FF<#(**5r9DT=a-H;bIeGvNb*>;&O z9Opak>rXcdKEa>4%$NIl!+Wbwa=SRE>#SXZ$D5UF8sT|P)jQL8TtL=ncR*)9ojEb6 z-z7M?9QF0ma}BY)toiDzk>`1om_R+mCuqnwQm_m&b6r~n+y%__O8$Bd<nx0srr@yk zsw>(t8?a&0RA(GcTg+D9PNiV<y=U5i!;`hI{=O)XSH3TH!(qB?LP5U(u(Ml{o94B= z^U!z=cCtmQFW-ihCi2k@MGrtfywC5h5bdsJB;s%#-y>I$3@H4*|L7*b=tw7>Yzp$O zcgB0n=M1xNA5JSt?vGVQAn$hGk!OaSc1XtIT)JN3A{WmaI$SJ(Gi7@RxB&7lHwEEr zTU_cN!sSKrx1mlVIIPp87xOI9lQ1&{J#C++iFcJuYtt+ok65$7H%oyN{&Qorlm32b zLIfa5OBlri<b8J-fWs<1|L0^YK&HZN6qnD%#!I>Jr}Lp3_7m92M|mtDt@T-RE!eT6 z44e&+k6XQD9_n|z{mln#Q45?A3VM=u;QT^B#!P9VJ0QO=e+^Enh`j?m+yi;PYFsSJ zi*K9wpnTQuS=hfwZtcs-C?A`k?TK79)C$KT66UpHCCKTWUzTBcr2o@TJ&?1>9`Epa z`D^Jncy~j3Ok25fc*6|FqTRJG17uM?T+>q%dF{tV%E-Tri^S`PEod@+{nNt=g}+S) znE$yO=`O)%P(;Jmi+dwhYLd($5%W3u!>nwM?~PAD+_<_u2u|Phs{Hd#z=tmUpl~?7 zSij*%G~%YC*BL;;w}zOtfMnqd^FRu2PNk#QWdf)3Jt$j1(`S1^H-WtSh_N{i=S&MD zG6p2^zmwJg3bXb{;P6f-UTJU!Y*q_UN&vaS%zgK`W*<_fjI-+QCT%$kQ*wFB*Bzj5 z>E5sihjl^48|ZFOy9G6|G01P2oxp0Pe+_-_3n-kp;SV+o9eg<q&&x=jl*O>i2v{=4 zACSJfwH4*8x*#8$Rb$Jn>l;xo2nzNBPWop*!s}psnrbLOUd4k|$jPmSWUiT`ciLi@ zlX+8S%tF-U^IcKTJAU&q)GPZzx_ueQ>9WiKyuKIH1~+d(y9C}eOatWjo?>sn=Dhp| z>j3!$dN$}k)4&hH@_Wmc-bw+ak43D*^YA576R`Vh_%~Mg05W68-^1%+rbTMgfZZ#G zmTnb6RE`e0itQ!5|Mi0>$onaUmMp-GE1nKmZyDhqU2*np9j{)v1yHO*>>UAlZ5DU1 z-R%u?H=;c;zq3LHQQD2$pr}9W-36>~X81@s3{U3$?HmfQ$x-JM#sgcRbaXLra;@8X z9-#MAYvTlzKcO>mSP*afW{B5e|K!;_ybgX+ha>vWH@TgJ=ND%Tv_m=bdsr#f7kS?n zfc2`=prqjsTo`JsXbSH%&a9awSq%?+Zb-=NxDR^1az)1;(0BbA%r=1BHgk&WS>U^_ z&k4;2BzbcbSAbnv!O!&F!0ERecG>|a4P&OP1EjsxGd3fhJZridFnM8`QXSyP+0y50 zMG#jbv|BN~R#txM-v~$w<zMO{9;iRaUDsae@w+HUpAv?Kn?c@v_4LM2Kz6+7q&wQx zTKIDD^nRq5D%jQCuzOVvnEbV&AP4jsHIt&^0kcCiC2|4D9UH$LfZYl8+((nCGTGek z^qmjbeWVQJ{P(Bj0|2l4%&@Eh^zVFbh~-r7cy|)pz4FJ0VWkv2JgU64xEMISCqW+D zmG?5QJQ<MRm}87_!hY3=$8sK>ks$3b44vNp>q;ANg~}zmg@9z5sSd`g(;nT!Lf~mf zx?He-|GU3`xO@5UcO4c#Q10NeI6LD!*NlZ{xP=jfCs#fN+^4;(%`k1z#m7ytS;Uh@ zw(#*X*bxoUpeJQnmyk15sb?^7K7R;jM}|#U;{bB@{Ne3(fbi{kwlSiU>Et<JNBn21 z;Cb0GUrPK?UiU^(8+iR;%RsDlQtnrL3ApN+8LKcnQ=bL7qny|5-**hKJNf!zY;Kot z5B>H5k`u*pjTGo*)8rz7j}Nenssz;Nt=eA#D4c7xVmlyN6nGQE1r|>J+)Bau%TEsr zMm|kty&-Vkki(Ve4-=;H5l1gx`KuRr-LL^)P%P+4Qht&da>rsr?8bs2uPiaVX+eA{ zrVC;-NW}v6q^l<!ICDi)3DXAqxO@Q@Mg?u$w}vF*j1i_WdfS45+;QY}RSej(C7VOA zJhrvyGY$)Uoh&Ubo!?(K2GcU5s=3x35N?;a-G|wq2HeU3cG~;-*iG2g64ji&_b5B` z`~JiVi4$m_y*l_I1;fg3lXmO|zK`V+P62LhTDIs6V2j+@KU}?I(f{kee+bY2ei<`K zi~Ttt+~zM;Y)%AZ6x`MQ!Cf-i2Y%ZfE_ZT45B5U7O#dY(K<O3t9~3~A8&w)9%~CLG zO!~|%P|%3mXI={W_w{z$FkGzUKkrHgWW0s%u-cjj`V}Wp(EAO%xHqxPPseB2Y-Ino zkE;O1UN-`0K%SgyZ4}sbkB;eyL;a%Qc|73ctmdngh>BmQNdvMkhd*2hdZu8<5%hET zEUD-Okc+L;e{iE<?{_bhx2)Z^&<C*F!qW)533Ke;66{WVPc;T7U)F}ile^x)TOS;O zlZ*qx5oky6U7`+1zD*g5qmiV`%Qsn|PuqL055u3-wRdsHJ@n)@vawdJV_iYcl&;OP z0;KPD3lYh41&hVV_qSwV{Z;lp$nvAWfBK}lC1AHbuX`QH)8PedE^rMsBR>o;>D3YP zcpe?6fgw11V8UN-^*|inl!NE#_9==+|H-(RkF!v|@sTfgKI5b^9G%JfR=uf!xxMZ$ z^P#;eP0ei1fW7pc&?DUSls~U50)6z#E6=gL1mj<R-2})g@x8Gdkb$AIv?wU|N8g8e z$ajsOjrB@zwQ=?Zc}v&OY8yayerpbPZ#hLvlWgF^(fknflN8*2gL3Hz<18Fy732fz zu-=JV_YMO<UajICuKv~>N*YeVdEBBE3<23iH}!Fp6W4WdyAU&dMa5pgiPNKQ3&6_o z5AEn5yOlKJ`AJ(!AC_mmk~Xx4>+GJHW7P_y#o5Yy_72E9?+i)E0915{DBTa3szqKM z1oXa9yE7f^btjyZ<m^xA>fc5C*D_u>T5U)UoQ3gRdir=7)>G-eF-O6kIBmZAbq@s# zohF+pqaArFUWC`xQa=;6c?7<0+f}Y!$^+jRgI%tJg(}vImMAi67wTu<*@<z_B&)tM z2RZ3*t8fM6uNd~j8hQS}0p5W9-m4~U1{4k)EgK{P{h`cp*pC?3>*^REPV<%f1c0P@ z!jGjCgrgVV7W*6RrmbiXTqt|-Gu9ikpuZ8*B)!z;1I{ww$F(zO1M-h`cyR4}E#L+h zKWpzs`k=m|<%X&VP^sQh6F9GXT>LsfI=&Zv!UD>BAe*-eIIA@L7sj!$_8|A8*$nmd z4))`J_xBI!ZTjDC7I^UN5S}f}`kvd#-3;iO4}$H+wqJM}^Pxci+jGSxHt4~#g5!F# zI~oA{%$QpZVDJ26y9c;QqPy0wqrxU_GE_2j!EML}M6F&8_xODYN>ey2a?s{>IA}jR z9=jKLA_+g$uxR^*539F%fxS(cJMD`v)Q6^fEW+c48MfL1I^0_`ib8vxMCS-Bf7Cmk zBW`#0vhXXnAj5F6oi!Zy_RyzbBN4pUO;own3;r|P588y`c4cWp+!s;txBZre4B&nt zX<6F{Ykc8t)gNR4FYbT*E`Gx`%i_*%7u<$SX6dk1qWIgdQ}$Z<!;9{(8%JNYh3);) z7A-nr4KL1Z&YfJzb(aFe#eU#-Xl?aq7qFk9Py?HJKW&?Qp=6>RJhQkHr54X^cW2Vq zyBzDM$)na5w@W*uzur^yz4YdD?~awYK%q5PP6wV59MF_iUxEJrC|_?2_RQ#)x7MNm z-TH3EBI=E%XxG;W3SPk1#EbRunZ`JxH6NcfsGF`e#AgpyqbB;}vj)ASajFzN`>1Aa z*rNXDyIv3QM|0SOLJi!;@AJK^2YRrL;=vioQE1;(-=Ylq=T{Be0^k{mh4-zke6+vw zX4YcB&*$Q=xj;G2Rv8ohL?Hi=ea#(EDN#E-0Q5DNeKs1w`Fx$8x0%2*jwes+&I&kt z71&_`>=VNaY{0+qtp(fyfbqlgTIPeiX3>Jp)_~#bbZ^_hdFRX+Jb_#AG4xe{E!fLE z$~lS)0ltlz>FfyS(bODq=?=!F)U}D2jvAtl|HkgCkrDG1(*rF%=&C88(SrdN8v(mp z^}M(^EB5ZAz;5`yVMn>{c=)Fq4m)s1?22iC9Q<-&Dq?J1$~cfS2i`c@0LC5L#Sa5H zdnjMt7&xh}rKJJ=?GC)~1Qhn}7U}~s&gac>SWA|+{T&UQRA2oy7Pw%-TYq0bjq!bM zZh*|(>3?wY|CRr}WFMr(mtEa4mB2G(c09&mxb)T+BTNHU!<HuO2L57R)e9_dWm@RF zqaxB$5g=aM3A}f7@xx3J$=%(%JoY`<XBAc(lz@C-retU;Aki-!fa&JpUc*?<pZk;h zHNdWN!yRW&z=y3hKLY^yPnIPc0SaVG4`Ds1YQKx&rUSM5mUz6=pF`Y^CH#+~bOX>c zz9a+djd2K<um;rHZFM985Z=w~$KjVWO>X0cOU1}0ybcC-vcvRCHy^IXdSIHaOkW6g z%!Sx|M?j)+<`bqr(z5X3RN(B5@q|l%&w{6UfV}15Mu)X%=XW#H5RjRXu8#hb^K2BR ze`fuT;at1O&N;di^uj8UJlEc8>|`zA<jvSVceIziBG2`|@{85zKY42QIUewpvd)Ek z(6cKxn&WW6*JtWb&Q8DaDi(3z*m7?`(r+AwX*btS`L9=uqtC12`Iyh$<DF4&Fns{V z1G&;M%mHw{j?2m_xCz~BQMB?V1;<%#J-!FzjmPEVYXEaeQ|S@Fh=K`gG630a?m>Ql z8V8l01X55=%a~{hL?<mDF7L59Uo}8a7M|WT59H#<RSVI-u4$i=ccI;hNZVz=*~cl| zS44b=eKWD$=>bt4PAISO9fsxe+P8i20E8t2O|vM#izyN~-J|Eu4)y_La#roo2NZ55 z+`fCJI;Sq<aLat3yNDYWbF#B>I3f?UGjUqQzKWB?>57mVwF`$oWw&MCTYwYy8g+~h zcDF=fG|1V=<<{JFsnqUV2%Mgz-h}m4H!Qe)BXAN>sDbTHte*#I1Jd6mqx=Ec#Z{CI zpg7OHPe4KWx)*v9fs;c=o^tk$RjfbA+3P=N1Ot)-r!r=Oo*peZF#zz%g{N_eC=aON zJ}V%?7uz&JP7X|$3<PA{8#~OAlk?-b&vKaLW3}A4XEeWxLugVx)GG|D;4pW3+CM#e z`|p>L!$K||R&Pf8mi&Ey?63Wm;ozq5nnp+zy1UVD-%dblm5w`+$mu+0JK)jTIU_L* zWIQU@-wIf}|GR%SAR{x@`UC~XN7Z#>c!I^p6qH{Omz#i`*XD680ytf5;f2j97Mg9z z06Cc>@tPnCohF3=3Qy%p`+{Dmb^otNveB<+Vi@s{j(WcyP@p6g91MC`eg6x)-NWg; zvNYuLM?T@guJVE{hL3fR>C+@Y=IZ+PMW`pG4T>nweZOHF%3rl#=IrTMdNAlUByU<` zvv+TGsprZk`5!S%=A0e6_aNBwbrbs&08QN<R2~9(nyg?gcB{rHrB9ncPS|$IP(b0l zJnmC@p+{^db`M5}Ww1Q9A+{XDFhfRZ(R$$U1%6dA@DR;`Of7Kf1+mAlzUUd<*)f2V z-m11%fxgpygh~={c<0jl2KaCFh&zqIOE)-VRRSuEGkQ}GnD}GruSb9(gUOC+z<>Am z53{$Sf1kbmNBip#o<aO!3bKFDZb31+7qZ594RC(VpK1&vhI;M35#{NjPxV1glCvh1 z!05#uzC7U;^1<3>ZlIS|YU?`<Sdn33e*&=ksj9IMkeIgjXHrnk$T=_VfQ#$jCS(B@ zO8pp-3S48Nm*g%$cp>kEy_QURnSj+<$z;5K2)wr<Lx}|x43jj%=BPWH{0ceKdraZ~ zd39?3tvWMCz*BtKTzB9CMVyp+zm1uLFVd>gBneK+R0TSdxyk9w=fCt|)#a3<>jqM= zM{Y(lyjXE?2v!~Qd@JC^;@Pu(z`o?+Qn=TIa;!QoETayWZ_=-+ErrSTjhm<JODOmR z^NhlhM6e&SPf=nuPAcuf9IU}^mT$tD^>CaZ{jqSD2<+Rv8cWf>^Fq@$kV_nxs_F)N z4o@0zs~mqxXYADnok@W0T1Er6!rr|)gVynqp`5oR*Weo@D9=pgofFvaZN1upKi_Gt zRlV64%8Tud+M9@z-#qcRm0<sU*1K=fX#eQtUMD<nsJoAYsG_{w{poUCC7WF1p~2ww zo>(Zz0lwO9;vRgVxa<8Z?t1ThjL;E)JUxG7h!@C5HH3F&QBa)|M{K}PRxf{lW0MK; zd*tz6Kd2wOs;2_H&agGNT@pcl@^GePF4`|Xms<k%7hPJGrUN>phm6MheD3aZEDU&j z;3YTw1)z%8rM&$VjBOq++>fLio%c{H0?y|pJa7QJlmVJz%qsr#RM)^Ku=hf`2TtAx zd~!y0)lC?zKlq&H-3EIiJ>md<8rvx4eh{;)#@cC3Y2eq}e&4P0fj4-a%fRF0?r7=Y z`7&NsZT1FxcTLfK%+gv5Puyd`-rW7TF=nMH@1AEtR-h+u81Mzlw_Yak2<`m+6peAR z5jbgxWEP;p1eeL!eRNBtrvNffc~jJzDJXxA=|{{;T5_E{>@FRjswQUx{&siWauASQ z`Qp!qNqnTk%*rf$!I<o}zziqbM@r>vVRHT1_1cIaz^f~xM&M+JoF4<fGY6CHtMi>V z0a_G4_`L${HMZ8<qrJ^9SXB*MefKQc#Z4||$^Y>JUXy$xWDVMXY4$h8n+EIJIt>xr zOw>JfGNxd!_2#hB7%ad0(R*ts&w2m3cVTExS?)JO`@tVCX#l@A?D#}uz=|(<-0rDF z<A|LZmM_t@ANS6;I2dhUihG~S<Ohw%<GKeYET+)j^+Gs&^-<knqUQ-;Z?x_%pOt`L zgZLP|vxcuLCU&gZ%!l)DP4rccqFPUU2+vPg2w#zSU$pXeqQLG*s2~)-HW?XdW(vn? z8x5Un1NI8W>reV}_6mMl_;pJ9yrvac{)w|sR^eA0j^)le_?5}<Z4xG6&(9v2VTyV` z_KnKFw!it=ZSL3*KIN&9TFHxZ6iHsF)ZY0)m-~9;Vt_ovP2k1qL=9?_r^1R`ix-0Y z|F1pNn`Y?>XZ%WL$TT%|_<CpQwK_E${CcL&ewT*GG)wuH>eHVMA+29zj-6bJ_582w z;cFYsfQJ;+Q{Ur<Ka#P0tNqvAp?uky@7sMq{>$gX76H_6{B>rAJ=XKJa+yVxS=QZm zCC%8*v6BjhSwTB0-!^^Y4Enjk%$!Bgo?q5YT)b4YTA^vO#rjq7bx!-mAX{tXAL?68 zMDSJ3*Dv4Zi4;=x7u=a^L|xF>`tg++h5CXmB_8mVO?QV7_I3vz++%Jcg0FO*jp>~T z?IFxp+30}wYrk|X76C3fo^MKZ>)uS7@o@;XT47o5gaf7`RzouB#*i+(d6G#VSOq<h z4^2>dNJGDMic;BqnS$Nm&KJpF6v37cEAy<epGRLhR)8qkouQZs$R9n^NdQ<ivg-30 z3ho)*o7Q5#RtuQC2yeoqCY=+O0Xi0@KgD5G*cfZN4sdnetXt(2JhEl3GMI-pWc%nw z0CpG16!;>}Yx}hUQ0(v4i<ADwEBh{GP%x=ExA7R}o&MvF@0}5sjUF5eD7KTI>jlVe z@$$jRw)Dm^PjE6%hn3%O1=QI5QEY|sJ^lC5j@ihHya8F&X$Hn97cGj4q`*EsW{M+V zu7Ce=ocv1%?R&u4Yc@|qzt~#!{mW1;C6^NqcI3sX<Z*zxNuzwZ^Bz;%g8njLGx<J% ztYfW|8KAV(h_VQ<W6rov!<#sb<v|LZKd&zs^Fhz#7G1@gH#Vz$5SC9qy^p29PPJ&K z6gSH|y+2_l+D-JH?gVo05d(t-faE~4%|ygccPGpPRDJehHP$0@`dFL_;>74Bm>-&L z)1|qaT8o081A&vYuTvxdRaZ#T+-zJnUOW^yJNeU9FSO6Hj>nr4q7*oH2BNgO4Sp-E zDiuYeKkVe`gBBtd$bR+(ee>-T@EZVdKDB$5S|V6@Gk&JqR2a|Ro{NZzhH>TI)V&s( zXx|dp7zB0-)A;hfumGogb;x3Az`(A(E8JiK&e#nfMufxsr0=8lC3mnVy)CZO5S2pi zj|2I=&`Y*9P~L)*W8?fG&q(}9+K|H8-|NHnqHlgt!glIZQ%OgC`Grf6m#rNfGA}!i zh2zFrEtwOFeyMNPrP0r&J7&qD{qEdbc(X?GRvx9nk9*gfUg?9K#6cCUL@a-y?;a<V zf0o*Y=ZSbXejv`GNuJjwb2!giU&_Lof^mJR+S{3U-rwclMj^hQ=>+|WFd^-)RM9@; zPm_e`A7;b<{Z|^ywfeK66r7Hp$o-NONp*|GZbA~P4=4fZ6h__K0mx&@!*Ev2F7dy# zk^(*Zba@hRa$(BFm57ETf-wx!n(Fr8u*kGnj=<*TKX7C*YzaNDX$x@jO)JzExKKs! z1$HCJR+Ia956wPM*@4}MmdieC2XY8p7cImYePvjUch8d@DZnkpjG2-Qoc7GGwgarZ zabXXJF`=InM<QO+R@bD!&h*J429Wo5eghuQ-ywH105}tQwZ;Juet*^s(+;uSu}mMa zK5h<%DLr;<9QsA>=R92j$o!(Fd!n9vo#KY_F5^WjfHU=912~?P?8zN(e=3y+de+{N zXNmSqt4#=^@cw0Mke8jAvIN7PNXtH30&=EU<tX}@dte<;037yp9G-ysn~sAq4bdY; z#NqW@zgqnU)0=Selyg`wmB+UiW4D6c&xd6Lr{kQ;@%jaV3u;);;!m^uFkMO8f8T`V z4nExSI2{n)xh7%#vd;U)2LoC;ycoQh!g4LYM*-4r)d~73p1G>p5s>hbmSiA5e7!eJ z1TP|oQ1YRGPLhK^VLgktsVu?t<-Ll;6af-ls=0{*xsBFa28M+kvG%tUAa6U~{;32o zLo;n$3ZPTw>W)M}!sM^dq2Rn`6D-kRa!YhJ8IaK)FnA8)ko^;}9-llEJuO6i{W(6C zA2vH`ST*qGOUAu>;5;3?zzO?+KRj@h#W=E$sbcZ~NwX;RECnCwybRia=g*pQF*6^q zglD0f1^CdlsW1VMC@mbRM*(Jh`Ha)WNfL{EP){88PE!N^lU`MX!x-DY=s2bsjo~VP z-85?qSK1AFM)Cb0^jm!9gA(e62S*&hdLb5X!ZZQH7F_VfVU0u_eRqliw=$JsWesEl z*AC?)ARnHy;#4^xbI*I}Zoq%{_Ydh|%-`?s{<A&*qj~*D!CjK8_+uMj#;>xI*o@+* zY4KQ{Mi*Qign&Zp_XoJNQPyCV33k4Qwhxa1!tY<qg>WFk1yf&Rm~1fY*T?1(MqX{_ zvf{5x>tWW<h+XDCz6ti-L)LA;F!MJR(Dwm9yYK#0gZ8qO+?(I%MaOrE!S3gu@VKWS zUtP;4?gG4j<!TfQ^1bx_6D;tEbC1^T1SBgDFF-wgedllVBh5K63A>Ne_?&MCD7cPi z$``PCc@v_%eE`XmKZ@wT#^+&E&_C<8z2X2sdR@zJ>`rXfwsB5?jP>0Y*xW>8%+?JQ zG~;N21BP|lrR_JcJF;GalJy{`N4kc)0}|hm<@nG5HX;*Xd8GY*nhv12b7YYj=;4+x zZU>;`sZzadAg3Q)H*f(C+ZSJ03Ecm{_pe(4drc=wVLf+;o%X}?X2p)N*a@g1GqMc* zgA#`60+&t<lM4gRw!3#M1zu^C_%#VITk*R!)&udIQ{o1ku<PI(a6skO&yxVH!&10C z{OO^BUnnm#f5YS2=}DFh=Rd6#y9wmN*2nifMDXO+baFY)W-4V$vN641SyZ8&4P3)L z>ar&wVNS_o+}!7%K8W$6Jv6W<4Y1|O$LpA0)b>}tL;v@>$(f-XA|aA%j~vr#OwYn8 zQjUB;|IyA}d%!Mt%RO!zL;Bjx2CNr&Mx2AQ5b=Qn{@%dFBYt?>14>^=8C(FEoVv8O z5cEX4>N<8uc#1M{9q^WY#SIu|%$&>TDM0bYCa3KnSIAPlfb~yCOz<*7WGp{mT5e(E z{@P0*x@Iiu`F*zYFrK%2q;%GTp50a&w-)%n`}>FPJK=BLSNuQA8vorHB(_Pd2Dik! zPh}N=n~e9uFW8)HZu;Df=<dPGcXJR2J<m%4eRs}wnPT8eCq4|m1zfzf${-nd|A}0^ z!+@r4$$ggr+2y7gSRTFY;-Vzr!uICBHUrk&JTn|PtgkA@W+YM~?#JlayT5N|fjwKP zxD>;-a;fvhW2pBW+U$e!#Zteb0K45juPFs2<^9~vwQ}F#QB`2qBvU-14N#~$A$u>N zLT{P=A;7IYYZY$*dcSBp_?Cj}+gIPlU00D5x4ev4zyE3sdUE>eq+mqjDMgsRNY2D) zE-X^UB-sIHm)Tfx;kW&sw-f5aU)s3>GGn$$W0(?FdbvIzb4{qkb>9x20%y0lXdKor z?0@1PigrgYpV)}luNAir<n)W04h%Dm!n{9t{Y3x9UPr+0I#2w$TX<vhG<Q2<M$T96 zxa}`{OhI2cxp3?*l*c;4LntU;wz!rLa>i?ANH!qs<0xZ+`t&wEE=@ev9_oksqsb#Y z0eP;%2j~xTWS`>>v|~r=aov3W9bIqK&%3)W7m#gGh96)9J7!I;J0SbS<Q^9ftm!3e z7iPk;KX|=D`Si3(v|Igp>KZ^&aa>IqurjJ^XB7oJV@Ds_lbPWb=&yLyx)3afo*q_& z@gT1Jp&9{t)_+7~9w4Fn_M3y8dEqq&(~;nyUi3adGOctmrcve;vq?Y!mKC%e1cd43 zsd<QX^|9E!HPrf}jsTL%E7Dm1<o+N%j2|ZPT2DD35ie8mMXbE8I0y9X9{LK`zT&mN zR-xUf`3nM2U;aBH9FbUAa@T9O@HNH{yCr~I?n|B+Zse}_=FBKemrUlOr(8RdMP+#N z#jF}G!fwwy_*@s$0(?xHqJ(%YR2Ace?R@0H#Sf7-atBUy-abZs%c^<P3(<~t)n9-p z7n_H1L2q2Y82c5}ICUG|$S_+Y=0u=g+*2Kd^32{j=m*&v^OHsly03=uMfyh^U4vZe za7i#A|HB@c-5{sUNDtQ!e-0Ui*ULWkYT(Lmj2n;Xl`b22A|3J4+i4Y`C&{tXvHjQy z<q?0CyWL6%oDAPC;D!^S0k<nEZ@_tnLufDQ#zcZ%Zcnl|_ER!!3->C4WJGHRA^%r@ z|Bw!){+13~2L7v(|Hqg;7=)uBd(Xou8jyGGogfO_6!gV#`(W{mRJdo8Y_i&H9A#nB z|I!(7aQO1P9Iz**{UgkP^ZApPV|eB|F4VwkW_ukD`2)hu`3D@0$cA}GlmY3(a+keO zpMSC^9=Pz;hbilT)AxtUqa1eXx|Ra7pNssgJEuUO+irRY!@t=&;q?i$v;U#u2l#WW zX+5T45?4DS53u0EJNp9^=wrt(!Dgac2cG7_biJHLDstb!dPaamdcd<#usd<+Hie@V zTOYT>3^<=Gqf!Bfx0WnV1LSFqtm3+_MyXdm_+yc*eXbVldC8Hp+yGnBzMM@4yIBE? z7w!kN)~Iog1iO1%x@0;4r#pVmy8zF8)lcuf+XD8c3c3RHGv<(*@Fd{L!nflwjLMG< zl4+!16|d&CYAByMx2@gtGVqF$Wr@gZW}hC?2zDZaMDr3rBAe@f67=1-^xZiB&!)yQ zAfI$+tHeG){?7g1F}?9O2)IwZVOmob2wae8!tHv?NX`E12g2se`DhJtp~uItZ4{K- z^nUPPv#T?zRv>3i=;zr1vPxOBKk8SH>9qu8t!|WK_n{AmbI*L>qt4MWz{P6~`m+F; z=XJjl0Le6Jk~;;}eeT|4As{i1f#013{8xYf5dN}%3xEE9G?_0O8g&J$l}Y|ozZu<} zWW0+D|JN$Nv03SJP4Kg+pchP>i`BR^*Fo(n$XVOU?lXXlFnm=K$afE}9(4;adDG!# zSe}MU)Dq0DM&loh#;hQ`J!3mp4R7aIMuVI`?NJJj9vWxAt-<PIncYpR0ULj9m|KSW z`ibz1wBXNs?HWD6Npp`T*HKWNCq76Q0P^!yR3KcLg}QwbcY#}-G-qFcUB=_)i5O;g zVuGGM0l9^7w+^1C`_Zg|y@1T2qdRdFXG_8y(LY9W<5BER3Pw+!<LIvu>1wqJ^)b$4 zya1&)T^^hOa@Kp1D~`&HQ_sy1K+`K*i*|yXcYc%ddf@b=Qg{Ud{vI*Cl>^A=-}g@h zJvlROlqSlDYx|%*>3Cj-qXRkIEv6A=wkTyFPW*5P>zTAZwX~-IE1z0sA{w=-WA>&+ zcE52Hr>hoBsRd*`TlCRSV*TxfI-;(i2CtJSPZ@#TlW6SUKL}Ct&06$_zP5#1c1w;- zn2ha6a`R~}eMQ_c3;@n<y1UF5?NYO3cL8c7E#H|%0YB-=JsTiB+ceUFGh&rTn64Op zUH?iz)>~=}+SAmZrU=mUpU)l^4xB_)AA+<2`5{Aq^~+RFXvB2L@HW1|dT)t1L!n>9 zcZQxF1;>{KNTEH`e#kQlkXO!Y@&@GJa`(k}%6?)n{Uqwko%e46PNpR<i3DVBIDgv> zsN;XV1JfLt`bg^tAn#P<fEEglKc#&I{eyiETOpmm>?3GeHDHU&{O+A7|5tzi&>jCD zyok2{v5(6Vo%2TH<&$^q*SKm+`lAqm?q+Y=%!So5mlxPwq-~9}jsjYLj%dcqCCO2P zF#L7*3^G3jNDdTyC<7FG4B@h+FizjP5O~Xvv>6vDsE*0W`q(W5yPPiN1Cq!?1_uDe z;=AtHJWRXEks83b^Px^1fYz?H`sIL?!!GC@2NblfIP{i+a!e=o;(7kv-#=8R%-^z` z#($LE$jEI!H-LMLxv?iAc_iD-&0*fQ^kXk$a{VK)*D((kr(&;URfki10P~-%sK&up z{H}BYMpeY{(eLqI$2WZGYn;<m_l=D^2KvTxmW}Pe=T)c;zXsUpcBZ!&@XV#IvsjIC zcNW_L=L_ziOnpzm`Fkw>`lKsJ?d&$t6E^j50Lu4#hu^aT6uy2Ji>OR0P~OpA*LV@o zT6T&>t_UX!LoyQqJO9}2#%9aV>RWmeurlT6K#XSpqKw-m6!^U(dgfN(%$dZZHGtiP zZR#n=%@(fau2<815B7RGXYw1&#^jZ36v~Oe?%X*PxaE}{yate&y+@WooN-Xo2asRz zN)fXFaeF=pvncsNKe7fC9=yf*O*<t!XQEs@zkLYUF-ymAo29ZpyLLyRTyFOVOF;e( zS8nk+EoXHr1MG;KwFujTy<r!Z2}sM9^mFzO8P)5+o=*K3?g>bwujyk}CcO6*D*@RB zw_Cy~IM01wIe(P*zx|~GoV+jHHw{sjspg}czPDTjP~f&(39}k0N`)Wi1?*MG)dUnL zhuy<0&Xg_{aP?q4)z=AlrB0MgF$H!B^4F4qlMjEYxjC;&&L+%0WZ9ML>4=^u7UOx! z>c>7jNr8K&HXfzGnJP8z(Z|CFH|c$7rxaU}26}pv1(SxneeT&J;LL?p-0u<*>ibG8 zmt9~p0^6B5m2<1th*fAFcO4-5-~atX_fq*=_v#+XEqZSGl(HP(LpZe0(WK!0gYB4f zO?dC&@a*PJeRRkA;asdnQn?I%ehKs~@1JuYVDFbWybF&@pG^gWec#gV(@9{*bR<@C z&3M{90pF**U0~u9i{<UwYgh#3&q`S`Aqwq3)For}G99At*xck;{8&Gb-+OmNFA&dD z9+!phX&eG&oqa^`0e7P0VPy*Jh8($sVN7nU33NmIt77=EVl3aO8s7ssyczp^1sp$X zc!*CR+N*eQ?+3|V4>@D>XUNabxZa{XQEhKA1;^z_$_BvkeQgs8aB!th-H}6N!aWGC z<9NI7(-zJ?us>)A+M9P|tQHYi5RzDpUzRf8jemxtKU1=&x`F-rv(7ac=+E(+HC+8j zu!Dnu53#($eNie*oFkEe<*WCo;JTQ%!6w0Yp7OVAe#YZ*TNZ7!gY%44-f}6Lf^zpH zeGNjqTd#%dXUe10HoBvIpj$1sTiyPhC#J!1W0lA7v0IQYC$2jqMl^(>{aY2aeVpCB zcZ0H^TnBTh^FDC=+g1||41aPlP1X-_jc(C0l)tQl53j+_;pLMEtWSr)>=(G$y<zSj z9nQ`)%mB-0o%W6d&W!*4*c|23nK=o_E2wvdfDbRV!FxFvq$S#$IKR5wPU!<@obz0* zKo7GyTW)l7R*qi;oImxAx+~hd{J1CpgvGT}Ir~SOW~@Ma-qG0AAg5oYNMbjp_2a#l z1F|-L^m33BakR%UK>n)*AzXY^KDrbNoK79?vI^`}O;f$F8|&n^riFo=+z20J2$)+j z-i_;)iY>x2;2H9pcVN9p^iOLm02GsjHCUg_0htg_;OyXTH!Ke(VZm4)^E0`DyZ$dT zY81duC-<;t93aVkB%=h_5~6et{U903@8*l3yxPa9eBke2k4Z=Zob>G5enfI&Lazy6 z%Z<m|<B_*Wa;q4{7Gbi!z$>SY3&7(h&6H8)?0vL)97J&b#1$9R(Y~K~Aq0Lz@x{~w zz*SF0GZ3f5O@R(!0Lk?B?K1$GfjN8?&=ZY<W|UiuTz3Pnf6eo*cN;-oDkXTG0!W9O zO~dwxm_Otp*FRxjPP{JD@7cey00q?rjhMD<i?@g|p6LfFD?Q--zyGndV``9_4|vxO zcEbi-5!S&y;ka!IVOL?j(eF>~&(DEAr0(R$eSjJZBs#hJ<hxJa1omWo6-9%5@2-mx zNq|CQZSKou*ug|y5qL}W&>}aq6U02<4V+nelaKY{?c4k&8L;-uYwpn^$sLtx4P0Zy zhe2*&M@mcCJ%}N8zcF6sD~_Fy*Wohv8+ROZoMhZ1TH{plJI=nl>N;K@3G41z2H5=Y zM<KRL%iF02-1%93BP(357Fc-C2G?cjq&-T(^=Kwr?<nE=t-~AUC4xQaZwYpR^={h- z70k7Tk2o$(81W?lKZbZZMy>*nZ}frvbuNWoGtkT{LHnex9tQ2EwfTFaUA(<c5SFj- z(R>@MN83L8!9D!=V&2usXSSgIm9oYdIDXQmZNvoa$DMt&7|tJhEzLOt?MpmG^U$B! zO|D$r<cC>>qd!VEF1BcwKkv3Pmfv}>F^#kDlhsB03rkOK!}1>%-nT~i^ZaCW5!gSr zX>;c6erE(C24&g?fP9`3GcFbFHLmp0xc+Xsv$+@IMORBt#HO7aveD0#uVoy7M@KQs zQ!tNdMZ$UoxZbMnbZxZjd-Nj!@xV%6J_Y5EJbX9^>h;V!o_iL&Pj`5EtBOW{``?e# z2l=FfQ)iDA?RX9qyi${z3=8~*I?PA;Ch_6XnD6ejbxp<dnwU?U@=x#4NB?bj6#i$A zmg491F<1@Oj;Gy4fb$oQ*aw_{d7=4E;OyW@bGdZ0ZPdO*;MNO0=3-h9n0UlDfgN$p zF|GtuaJEi33G$!1jhk>%&&KUPhT)!}&pW&a^lZOWQweawZZFCN&QyIEm;jvKP-Vak z8(lL(0)e-@nvXv>NBFm;zSn{sIlJ&SmM={`Z_WXE*}WIvN&(5uf(@AVj5f46HUsLc z@&0-rQ2f5Pu#<upTAkgGih*a%xvYN?kfG_1IO}6ip3T9uNkS^!V^DrD`pRLz7RL>n z^FYp+FW`n%B3{g58s|@|Q{=i~<;ON-;ACwsQvi0XR*!ugaAwigo*3ZdNjkSIhyD{g zAPo7b7jL;)W+nfxbR-$tzYdQ}lApW-_5BunPZ9Wi(7}T1MylT?cman;yp327q+e5Q zH6Wuge-PF;sn=hD>6`ac3Wi1Cw~t-thAH-L50*m&7nbq>1$zURaOYKi+|JeK`1SSs z!JhWGHLM)%>@9Ng04w_%?1g}(BY!$_^{m!IaJJ9pXISn-JHgqXYY-Va1Jf$`HY`69 zQG1duUO%x;{e;8Jzx(@#+1~$kKa78OhR)^OCWdTfy+k&;S1%<D6#*KRN^sT6FN(nI z56=?HxXV#!70m}bCb&j}%Qkw7HLj?)=$>f>azaP5k;s{Q=0S)e`An`H##?NG`cG@} zf>GX={>mRY{O}WoCCrZ1u{_~<)%lx%D|~Br!*C>V>xY>FN*|thcPHqHeS8#l2NL&X zJ2#rdXKLF3XP%$-3_+Z?WjD^&#931daI_%yldp6B>(H{?&6szuH`i@<EWHql_H+7@ ze88S%^>%Ty5|Xgb43DEDL$MqpVYAgAage_x=jSke3P&?WJmi2U+Vx+&pMfZ-U%mnD zCN2-h`qP-CAjO51j`|O-er}319nkKfd--ledNXB?_WT99PJnFOfu-E}dzcto<d=4- z$D&@y8x>Ch@}&RTZ{fwRM=Md@6kf&EZv<^`h<x|C8#qeB0Rt8z@-4ive(6HlVoVp^ zYSv)|B9QkUiO@oQmx-zoVurE|SC4N^_oe{n89lX30y~|d7G7N1a4GVkfk$UG9NmZZ zEw|ot^*Tu>JOVhgAlD@o^c8!Z)iLcb>&1h;fs3WwFLURMo;<DyIMMrA#Kp_-Nw<~* z=Nn{ca-)xQhLSWKN4m7LIR7=~dSLsLYxZ>lMEmR9&)$)S3RygkTu8Xd#m{ybo*y2^ z_*!sx0si0Ta^3pnj2etL;gGZQwgNI=2TMfZarKXU12}uR{W}of$6m(zX40d%<t_Zd zfpy7f=jmt6byK-{i(=7^cyrstkyppLPj1-8#hMu3%pg}CFVu(15iXvp-L~=Zc<D&L zY?LcXu<6LXp4`NEV4E!`qd$c1Ntl6nKEa!tC9@&hx$`92CT^xc{(jxCHJ}&Y>Mg_J zgtRPm!n9@`baHVn>gkYBF3$3&9Z3MrJoTNw8T8%9{$!Q`7yI?3Bmj33R;HZ-gde$| z!d-9G1D_+H7y3Np!)%RQ(W;cc3z+6tTe~0diC0eSc|fwQIp`?`IF58QApWbre@IvV z{fiyJ(MS;&9>ic1h8u4cKZWaNw;W$2g8Ti$oW#i4=~u2Gr=@NtbCWcFhg%77qWAW` zDFxlrPI3?f$UoAmz7<f~<*M6yz_JSoDLEpr(^w^251hzI)H(pC*T#;(YLcF>J`BTI zy2p5N3?LafR%RU{JMkxX9<^USSUyR+{K*P1ZAj*m2-NqDsxSd$KZqxwUqozJoCkU) z+gO<k*WVSoK_FKcci|<LOIwazk36C(@eejH5#Nu-a^l|4yNu0z!{eet8gRyMq1HA) zo#f*SF<t4?Vy!Yj()~&jyYs%soek&ViF#<E^Z8Pcv!5Lo9s&&U&*fzU%AUKZe-w~J zYdwELLDWjNUTOh8<h5S!K|s}M%g3NzYvVV?JV3thp3U0<;Sq6HDg|gX>zF<uukDEu zc3)O3A<LyN6a9;;f!_^o%-kgcI}&D40G!d%S%cS=CT`(wPHDc|0X&|aAw25}sB11C zv>Ws!+T=(zaN#W-9)@#idl+{UWA!rWD9$c;#&**%4LtNc5XYrcrgd66*ptcpdzh}J zwVjm004qLUm&fa5eo2~b1T3rbPQiL7WXy_Ph+0+mjX@rF-pVWsP~kw*QLIniga8Sy z`=aUp`tKjYf8yUJpVCAADs|JHuW(NSi-vo2(H){tv=~lLm?SUmgJ4E~&n?t5h5K6& z`?I*sBnf|({$8|yJghwf?8R3VT`@dpMTg^<h1gFMK4LWsbGEpq0Fn%HYzv|w;xLXz zytDE*xN6Y)tY!(EVecJxpg^A+>M$4W$ZnAa^6uwLx$KttNX-M~8Y0FY<;&%+86zqz z(~N*;5ak_5`njy9aOTe@e6~Oh8BK$HLSVv03}41tae)h>lawp>tx$2^fDAl7?%wWn zyhm8lEt3YvsaWrh+m7;jYxM<SPjp5GYM@?X#&oV52xGh4pxn122KF+Aseh88$y&6Z z5;X$Di0`~em%C2!izaULmUiNr-FnBUk4d2SA2v=Q6Yaex9K~iQv(MVNBd>helZrgU z&<*R4|8C$COb6ooW(HxX?}~vP?ZJOTv+MxmlH>jIMS!Y5xF092>~;vj@DoS{z1@L! zhh{HW1$us@3Aeuqse5sj8)X@b#TbTk{22x89>O2R?oohbZcH7GDDE!Yj7W?wjurt( z7Ab82>{oQ#h4+Fj#jn3@1D<ieXp8{RdU%jw4#<g{`Orkfx|<@LH3(aS4*CEW-mu%2 z3!Il&@hJ$99l;EV13A%H^eYIlQS*E<$mxhSnGoRYrkq`CffMN`o27xnmfNG(BUh|@ zig89JC~wBJ^Drt>0ppLaSYwFS#ZQy7M1NR$^DW81VRdaQj*_q|<SMS+mTnKmv_P|= zWgAiN`S>muZ=)v*XtX0UlNprTtmEoK`l8}=^o!|kyT!%L)(LJHhkGA<xl)Pt@86GZ z27Bw&cOkWaLlq7<i9qhVXHaStpy1*3@!T*W=kU}5^dwhz&M}blrIt4w2F}*3SLWjQ zrNRPd;LPBm@XH8bH*_G}O#_NM4r~QvM|!y9tdpKETY>e(G+$T~0CM`}*>{*`NTA#G zNRY#?5TC^KNZ8CD$eGykq78`S1`duueTV!x?mR7telf^Ni76L98@w#`0B`wi+m--m zU2^&a)-UtYdlT0Vvk%E&y%PSO*W9q-lpO5?oISVQ8r!S;ar&kdKw>g3hbzA%@GO^> zZ_hni336tQ-!rT)=Gv`gcEEXmVti776Z^iCHh}(fceY}^kuNvZu-+=G-o`$q2r<)F zn%W3l+H>xodO-T9Wf7N_$<GFEHpTz-9ES&@=Vf{j>{+FYPr3MG=FcApoV+mEx*GNW z>hB-Y=j6YoPr+a5lezKzI+s2ps{h*bZ~e<7949{;_3+$E{pUD@b&4SGUPhAQ!7aL{ z`>p}FDR7Ba2tzk3Q-vkKbGNrd;plJeFZ<W7QUN{pv*3|dC)w|J6ZCwaDS4PyDl>QQ zZU*dJXWPv+W8ItwA;5?0$(}t8Z-%-S&8>d`+$p(mz#hOg(-dz$r=XTu`L2_|dwpi_ z%m!rkK9I(6>Kv15gMNWP=_UFrmOjOWmDReEwG{Ykd`j6Ikgpq&f!%5A{u5EyjBN7k zMlV33eE!7&(6g>8M>BwTkD2xb!;@bz=0zZ&(E9P?^?;;w_-P(u#?_&iuE@LVvEInf z3@TCw%otf6h~Xw)*<Qkho1=}5A?h`roWbU0Bvdp6h#5+|4FMZ<d$%*77uMOX;&4Lv zM6TI~pVQ6<J$*${z@>qQ-V&G&(_}stVEPkI$-b8k$PA%obHfOIaTkWac>ew1WI&>D zFVqFl=)vst?O><#Vgz?nDK4{I>;jyeF}Pm{dfvi2+$u-m%CA3NLC!SMbrk9!d|##n z$X+!%j();2EN=B1-E7)`X_1$&6@%TqTRr^%rbQRsF*T`x-A+GEIe$%Oy}~q1Kb362 z?j`+7ax2<NYh-kK0<RUv%0&Yg&r90J)gSxo7y8SbIk6wRH?QJ%3;NScIRtG0G#d5? zew7sb@IBFub|ix@xdV`XwbCD-iSPyvT8rIRz&md14QTXy#5`_Rx$Atc59mqDvOVYr zb1gU$<4hbsp}+};+1<N7djB8R-ZLtSW&8gwGiJ<~$BY@pfQfDt1r>EbkfbmmNdzPc zC}JZ~1stM)sH1`+CNQERG>Ql+iW&2m5yQcZ`TyO0o@cH5;<uLf+<STRVei^gU18U* z>YnK;Opos`<erqif*EustuLlUi%$UqX<wR@GsEQ6p8Uu$GpsOe4q5zbAni^2>>P`e zzPd@iX-BW(<|alA3nzB#Gk*YG@02oNBJ!C&-)U?V?8hpu<7Nia?%Ky6mw~zNz4c>} z-l?=vI@Loki|=WP&s7!5`;vd=^su}{;S(>FKB^>tR-JT8CcBP)W2EVB_qlvy$V~6T zr-iT+#*Xrn6Ki_pS%H{Wt*M__i!x0G(V$oDXz0bFdg)htf>l<v<UgL(uji9J-?McF z*-h^Ep8Orpa{U^CG1JG5qv_GG?WYY&$l}w}x;~UHYw<G*dZGRDq6o-}%hjdSzp{c| zo&BLd9F_Sk0kSZ<IS%@QJvQZ(@^?5}NPezJ<qaIbDw7T`sQwFWuK%XEArs~_4g~SD zM(M}StXK11_K@=$KhF0C#XcXLsDC^9Ij&BEte#xlgZv1G+PB*Vy>PekKE(m`>dLjA zXy@gFr`GL5``@rBl4YX3EtUr~afBVtw>+3ivU&8UWhD1}>E(j{Gwgmi>j*gO>-ma9 z_`G}3gcNEgY<#gJ75dt5!Sv1sURlf8A|PK2i<!TU(|$)50kxU>=-1rSg-8C5m#l=o z63VR|oF~c2+57bW5T5@BXP)N`H*G8x*K8|=GuhX&KIBH)MkbXMj=a4)bt3oT-(7Yh zd115AjJ+VMxiRtzhp?Ug={e0(*_+<IIt02b9@PB^sA|~c>=sa{ZLOlu;b(EPNfv9` z7E$xa4L%x%f%kg6N;$y6ZdTq$a>JO|AJVHNIEHrRMd<lswTIH6_m3U(CIhTrf#bA5 z?JtQc(%1QX-5Aaxf9*$|jzSiPCDqYt^ZMt_FH*svs=>+Rj<U_R7(TG;)^W>xBKvXM ziukeT+quOk-iUL)K9Kp;D;uO~Vc-Nm$*%C69@U3sPSU@Fbfqxsa}MM+KPR+30TxxQ z)DuDPCOg*d2LIXLAG){mzs$a}|LiyF;<U;L4%L7EdDSLRXKq#-8hzrRvbzBw-#NvE zx>s!3RtN|2jn++?ZOW~lhi8H+yUcSmy4`lQn?cc9SUcEG8l@2qI8z$tV)5l@Q0dkq zArh2FXsClholir@Q~oMboAz5d(6?1@TFfz->jQz96#07&(lxbmcwbQPdF$axjBMA4 zx<U0dvz-TIZ0;E40#-$Cze@SDKE6ph#L;J(`Ej6E;EVuJHf_%gX;kulj($iNwBLEf zkX@jDM~Vi#ur50gc53y6rZmg2Q<j^i5`{OtohZGLi2=E#x=nQ`)t?PrILHnZoXu^i z-0HaD_ku_-Xi6s&=#=liN$prQIpY+ykLZ(cOHoMc((M<?uoF6u{=$Jc5#ohE^y(+( z((hk{t)q|XLa)|7w%!Hw{xZll3RDTVb?iZ*|7x2vp!Rc5b!IVEG2es{7$!pF7skJj zL5}&W!Kn?zF_{geS<~a|*+keYS6rXB8hV|6UN&^SQlIVpl0Yq^4<97AztybAe8_A) z8}|gyCZ2f=)F^}eHNe|jh1Hh+_xu=g<BboeOsNESPvws7Ap6F4(xcOO$0@66xap)m zmg-YIbF>Aulf3-4ztmnG%O;wUy@_5W4ZB|#XJA7H)MxAMOwvaWt39zD<d=ReHwD=R zgCsqoqNJM>2Y)j>y^YG5mfh^DBN+6>r#uen!rcl-ACQe{AZr9Yi+S}m4Af3;sY`yX zOx<|O5|pWXnK~9DyUXoPl7EVEPj(o9IAymfwTtGE1(Kf%`o^2gIBNd`k5@o1T+JW9 z5wfu9Wg`X1)a}<xewN?DgjPXSHXk!+7BRW@u3klU=6iQ6h0HemE}uwL>HLm{Ugv!8 zuTsC8-BC=1zN(4NpiQ8lTIcKx%1*h)lb>N1NoT5Oa@83bg^#L;#!Kir`O6gzN}w0J zMqZ%tR5xv|DiQo=e}4!kE&e5(sQ;twUBqwQ8iZ1@1W(P~D6TMUTn2>=K53&b4g2aV zKEtW`b@Ii;HJ~@&u{jMBrnY{%i$nE}9T3PaFH@^y2pH=USVi{CkxiuT=ux)#61fS} zx!j?g0}4vrsTs0=IJC?Mw}du5unP>jY27k~*maJyCnn1=`$Em9E*lv|!{%dV{RCp- zgBHo;#vOB-_Emz{pDh2d4`ij@;#Cgrd;Hxx%I}tS{3Xi&n);#RA#lY!zVA+uT^o@V zMO56e+{S?+Uk!~(o*0$s2I^!!Y(@F<i4ChIx6m-YY)R>gW}kL&;O1Uh9-vD5^j}mD z@%YbP8K5wy>nAtp1?#q*XcojC!j9x#%wtXUJd!uhE?Z8r--mc|Bi2=R+k*Iy{{G+& z|AWn?zE+>0(ZMo3|9-Pb_WAHi$U@}Czc;q_i?E@n%GQ4ED^*i#+FN}N>AOE1hy$58 ze#1gapXaGTqewPsZZf%p5T9L3ld$l#$&VnAAN|rH73mm<1-&4%(vhY!A&V0~>Bm48 z92%YSCB1QM#tdTZk5)A5S>E=3G@I~7A)BSyA%0<=Iq4sEb+dw=oxkwZkGSCaIDz!y zf(45pt9xDa^Cf-Y)-)H=hhQUQ;>_Xt<j#WVWw({m<>t$%+hsvx-_fuT_N=}m(N*Ik zMI*Ipz<9~s>IT2KO!kGLA858sosqJr08GQ-=Xsz~IQWFzgS|H&w2Ff~A}*1l16xyT zSO}Rd(v_Zwi)SmFN$#lg(7+bys!PjUkAhh(rgp9d`SNa~pTqv1XNflH3(T56qkPm) zhepycVB^?)YA>;>;qE;oXW1p4;85;}gZYt=`Gk23rS|W;X#&ar?C%fmrtvS)NBGa8 z&#szE0mVfW{w7g7)1TZ;bbja^2)*prLZfiV!s7<g)`^B~SJ+WAv-rn#_b9zd@9o); zyYz`%TLPNij!&dX&uUQqu^h1fk{^LbK=%CO%Uc|p*SBURt#!S$oVml`nwDl-g<zU| z!5E4f>~ZaCJEAyCklbg2gS8dO{r&w0fK{a@Jl1fK+qP-%42tmqb&({qlfyiT3!kOa zVnFlffHhH|mHle}<YMR*Bc3cL*7eAj+?72)5sY+Zp1WWc(f!;Aa!b*q?K(fQYtcn! zOZtEG_lNH4@h{!Q|9?+@c>SkExQ0;h`vwu;{G67^?0(Z7i%@R$LXtaoJ_V`|*xQAp zSV2!a8YEEhINj<XDlU_`bp&PaBVB3kQ<RJ@4*>bS9fDSH&@b<GNeX^rn;(~&7mp-m zlzy_@l)9Tg!mXlF$M2o_ibii;sewf_WH#2o!k#F2MFv4HtZjbX31m4<PjrS}h<Kst zg*&qIZ5!vW$DLa4>FFMt(j8gEoH)9(%a7<qn?PAubS(LW`tVa%clRUvd9MrTL3yWm zGhYY#TyEeGdJt~W)3|mi(sNJP_tB^8Hg`L3L-q%B+Di6YE|^Jo?zv}vuO_><<$fXb zx%=<m&!X~JxMVH^RTf&Wsr-VC=My=HeAvfRwB{6&qW=DV>u}=*cBCIuDgEYpn4zIN zxjCzwpQ{TBET(|mOmoG?B^2#cvXmhd9ohb@WfXOXMMPUr)M4H;zSHEy>t&3WqUp9o z`*E=2uWU0J0}8JqM<5D`<K`~AOT$AnUa@>VXr6St;YFmsjasmiqR7$JXQWA8knfAy zh_z|YT@S9&<SG8XcWn)*XB<DO2=;pMvtCoQ@i_X0qgg_D9&*AI%+z_(6xU(0dtcmL zL2m=)zzh!Mm}&fkWO>uOJIIcod3=)-*!aLoYg~uPd`>wygF5%yzR2WIpYUhTXMmay zicRIvvzD9p(y$n&z4CA|^uia@&SM~FejopfqM&@<mJw7QH+jGVy0=h#O?8L^HO^h? zokZ#8GAq*8^lVhWi$l2_C(NZ`t>I_uz8GXtkz1(!RCVhc`jebKYu;22WLn4jfwCpt zcKDHeu;q7Ym|ii+r}FS$%$j?GZ0qO4Mnsl3p)V+VulV~=zWBqwWG~u;^DCg&;o_R6 zLuTGAM+-8(5dA$LtjjoljK&M=vjjhi1<iZckK&O3_(WZ)JXeOd_}hQlUtd7_*cCs$ zKvwc9#tNHg-EKd!krj3b)|gpxAO!ws7U&e~Oa3^#6!$*Z94o%_g09%f*lTXxt5MiI zD{EoQiuE}9c(XXfD7|=3?wy4zw=V^A?`50q<gm$CXCwCo#esq2PqaN{4ExO4{FPYP zKMOH#PMg+YW3{Q$7f<76Chg>0Tn1hZvhy!4Jh@Wu!Z|svSGIoCk3?+Nb*|abyv?W& zUt;)03ZqKfb4QW?m2+3JGEnXbvyAyw$UnCAGv7$qH!I)kmjQdBtGcr<$h3n~cVSbn z*fEZOc_H8Ds{5y{p_i`;o=>xHs_L%iK~OyG(qBpT5nt=dIrxpQ#x3$^zIE-sC{Xy7 z(8w41G^YiXsgRlT6+fw6`RAup&<j=z+S95+7!~d1Na_FR?+@X&<G+Mk@jrV-gSAT_ zH?23*J)lH!RSDq_sQ9X(6Jn*Xo42>xhV-2da#FLo9r&D#^h=MHc29x6?s&vo3P+~; zYq@P;)%`v%soHrd>$_01Fjb$4fnfELUL~}KudZl5orbltf4E`^^uj*h!$dy%$yXcb znd;?Vly8t$=z7XeSv9DPK3B8Hf=TWj6*<`uvY24n#Sdis3tP%R&*4Hg4R^+gON}Ax zt)F2)`G~34r3ZRU`{nV5lwLJYE<n#TTCbC)iJv#Sn?tW?8dzotJzr<x>;qX-QQg!5 z#5NThT%czQ6=P<Sta+`DuCMW_#}TQ1PreKYge+uC7&r<PGZ$W=Y3szKs_s&GPj#Q; z1$}jB-dHMM`T4PjrQ!InE^sQ@@AnEuIR&nJs9q61$L}58j;15u_1k`npic4r*;4*C zG8-D6S&rMzyny}W<SNhWWEcGYP8JxKIK<@)=(0W1x*XKbyZ4UtIJHeq;m<qpxkIER z2Tk+faNT}2UntBlHUB&evarYWgS5KP`4$%ky-B7^K{WK$%kE1X`sR7;31193CVI;E z2(a#c!U+m{cn0F^3i*MZxqmujow{|AZs47a$0ih>g*|hmJ+o9te5$RWXL<Jf<DfTT z5tnG#<`}CSB!6i2`iMXDvRI50=vDKpRh}Tz%kM({IA->N?KB<bcupQgewfVV7Hoxn zVY!K76XY0!%l)M=z!seFgFY`cvScgtd6t7XYQO%j_p4G#f9k_As+Z#Rd$|I7bZFc# z$m*c_y;QEcP`jRFA9MIg5&4O-Y5P1H=6NC8^~n#hd^;=$s~)iKG~D@;CCkZAa^8Ql zPvel>zhe>kfsha^Z4N}A`1k+*Fx>z92e(4gq?W6xnC$J~5Tau8-zPzTo7JD1O|Vgz zM^o{u>!maN#G$8rRzb#1lja7p-~7sQ4W(<Qm(Xg8-PrQb5qd$h?^6Y_Ft&__Gn)0d zjHtAkZV2*|%2R_uvB7jb`aJva%$|l_OwrdpJ4ha*cDRakmUQv?2FU)^?B#JV=+gO{ zCqQq#;A_;)ih*bU{`y(=X7WA`_MJSWO<1t0RLjOlulo}7emmI>daykf%>8xtMH$HK z?fR%Wl<)3}rh6bap5%4A02~&av`PfEy|vHn0t;H-f1J(1J}-WJHe~kPz6%W-{?+=^ z6b@BJsSh_IU6p?3!{6=(n}7T758eAeIKi3sIkX1Ft8zc%m<Z}<g-VM#rq%N=iYki0 z(J|qmaO`qZsaqBIq!-VWlbSrFYN{KwGN)>aAxpF4Ikdv+eg=`CFt^xd2J}p2SQrJl z>QrCp#Xa_D%0MX!m<1PGL(c|<kDd)Go+a;;Mwgg)SBlb)_ubxt&&xMQb*E7z_~aSU zD7?9SyxlgiZqdwsKF}jtow0>19@>#1HT#-wODXEHom0x3K}Fd(Yd4V3$Z)kIJ)0^} z^iV}a7|>`G6eThf(zk7`ExC6|#zU%y=>B~yeGXd(t|a#pM=I~F12uy$+^1+FG>II( z7U?ll2U=3}W(KbFouFs#N`Z!pqJC$VE2#6a<6KFf`r47)SJf~yayh8};yG>s$W;4l zIoRdtML(u|bxeIGIfFu3Sj-COS(80M4xp^MO>628_WP7GxoLsZ;8&764f3d&f^?<e z)LaQ_4mxB`!%~cI+1~<WOAC&M6(hd^ht5nRxqarNagcSKS4*>I&4xxNDN3?F+H>`Z zIc0NccGR@Y^rL#$rTdmG24!pducLlc{kUvG!x*8qoXR6~wW!zznt$JY@EB-!)2mY& z_^Npi(<>a_!<#yI8V$>X(bkD6AX7SJ(y&%2T~?9b@SR(h^Pni4d{5<;Zx2}`^?xU8 zy*`lf6YyIDkj}JiUsJz{Yjkf*^|-QN5%n{_IQq0XsCs+L-V|h2=LaqZYrY<Dr!0o7 z<2tfA$gj%1PwiP_82cApN8NJHC<VxjSM3Qvy78E-4&>+jFV|<(Ph!8UTVxlZzONAj z@*m#6B!5;#_nmhG@_=<3owtGBE#=aVj!m0&pKt@|X2V*WlizB+O$}WF>O>{QJmQ!b z$~81S4S8;$yUAuS*5%Q+%eblW=<Iin!d1&Iy=Ia>vb0xb<OeLwKzlN=(0-{Fhw^-> z+Cly%T5tCbfnHpDvFl7q*Jva62ZKC2b`y}!TU-BqB(UYYKpNNHo>>~ykBZ~Ln-+op z?C%fZr~kh$Ch<Zd{pro(aane~{?s^PK}_ffy6iVIg;ZS6_@VlwcWoA`OM0u53o{|- z?fqI$-6bSGb)o93CTLD0H+So4YesITZsQZ@g|)s#Cyn9jp&#3-8#@5mb3?O6!Qj@& zLL(ZUa&CP80^D<)Kg?NKll@r-Yw8B8&XyhM9{WywyI+pDx6ps_QhI}cMQU)tcu_8; zqVULE*xwk}u>KIPm(?`Sj9vgtNnYNSHlVE6*wkzX@+qwCIDxKfQC-+p4|eMAH@;rQ z;wrD`ePtff^?ux&M$IkjuQMkS_qaw8sjn*_Kkc0EO?o!K`%)eUH)-P>MZ-jC|G|gK z#}DW-(-->6@4}NT$ljZphdY9@81<l=&_C$;@!>wmPSaYLMu68XHn+?MExUbAz6de{ zyM5Fy?8oS4$sF3Tq;#PqdlVIqhpepJIXZ^Y`4zF|kj2nXj@}>>OQoYKqe6PkPeQsl zyv>4zph1rTemO|Ls$Vi`Bjmgb&sMpDtZrrOO0d;%#Y?F^#^YYMf?eZ;b(IwM3$miF z`Gah__U(bhk3Srez$wQ&$L#=x%a7z!;r3f??WHG&Bfi*u*+O>TollP}235CYF5t)4 zmd(hW#SXVeSVI=)z4Dp^$|}dslY@%l&@;9mj(SU?`nI}t_RCtx>ZzJe6rKdOs&q8; ze7D;zO(5s3ZgS8AWV~S($*q1H93g*Ek9PU64E(XR<52qiM&0-=Dh~F4Y0jpwhpi58 zQ8=o(A~RkNz3_0;Rb$9?2W+G_QoOClhB`venp8}b5$iw6D4g&c3Zv&i7P_o$OX00< z<e|aT&jqjJKGXETUOPW-1{voGNV`9%UQEf9rl&S7o%=#BlNT3IKk;{5PSEwl<rPy6 z$es<FHjj9wzE)~4Z>^0V@C-~QFVJ2HdzRIDEknAnFY}%h&as)=XQXSb`|JC1`kb+m zM+?|#mWMZS1-*A4H**8o-6IbNQ#vbkGz7J#?haT53Ln#t8zEgYr|DfMuwcw5dmm6R zbGo4mvics$Da9y`*)8AgplD<`fx>6~$eE#m=;ztWa4qt8mDard^FgLrrrv|}sQZIn zq=I5}^&zs?@aB63VjRu3t8mPRop#3C2pSKp;H<P!@U*#?SJ5=1o}ZBF4*t6Ic2F|q zKPsUu<u8^k?`#A;ej7cU%Gv78J}&{XxKXDO`E#Aol{_bK;rS*yQAk&;&uw5zCz$*D zba%ta;)|Yiog0Uf#;-JMGS8+scEm*6F$qx|P9UF_^4W{S$>b-_yjdjKH`u7aN#`eK z9Io-fN#-?AT5v8zk6~GkWPj$Evj;Jt%wPsi7~kHqosoi0Hh*un#EVWUU);bzvVT1^ zVhQDY>6p$CO8=G;VNVP##EzZFKdPH?Fnzvl*~aOlZ@%_ZFqQA?X=iK6esz)=m1oR{ z6j!>Qb8EBtbdvg)rcW)&&boH=A`U0DPo8+(iR`=C+SnCeo!;fmtM@TfU)}RZ!ssOT zO{c2e>E!gC8`I1v{mcv;iHnooTc7T_m_E0&nNte#za`#ZkWDAE4__A)i0f{i9+H~~ zyR$X1<H9-E&8m1{T8wgdF6kYKljHY4QYI#oeQTjvFtuk(i`sZ9=abzjQvNqOb(PAs zrLCTlLpQ`-+TsCw|B|fY5tQFr2O}Sn^%vY)P5FK*=ciFQzr|l#N%^<y)jx&W{gBrq zFS7fb-8hKKBRjBaJ$?S0a;z<tf5eHjclt=bGPbm`2=?3Ooh&Ir`_?#)-dIHXuN8NS zpk8mY`0AfdN%`;3R1_Y*zGnkBWI?(fTgeUU9%?8d%eD<4OJPX%rX+@jotSIqOyS2p zptupOCYZwh8x60*fa2>V7$%F(CG=52{#bf8c`H~T4%&n1hNZoho%RQrc>NlMf8H%; zeg=nWv{_3tXUOXBV{#}wivBmZn1WRWzfWV>A>MsEOw%q7A$-k4R&Rd!W;!VLnX-?D z4-4K^H=X!y@LB~?{W5{V9>2f$sxZ*)W%fB5Ui{vy36zh}GdDt7ZKS0X_Cz|%n$scy zdbO}-7ul<C9j!A4bDlP|kgoqq5G^5>{~jL}4LjDRaMdozDjlEt4Um&}53~#hFIok- zMu0+H)7?^iE@^XakVQ?6l@(xAS?fOMIi%|=j0S<C$%SiFPeE(1v^SJ|_UNb7KEkW_ z2M>ac#~n^T3_Jd&zS~$(9oAZEcQ!HW)M%uetzRW=Mx(ymCgd9AcCyc_t6|sae&f9q zzGh7CIwu2^*B$({1{9ah8891UQNt>eKyUX6htfENx9#I*Q@hD}h0V93^uA?Ej##7? zXu4x!7WPd*J_pvAM{tmb{xqfV%}3o*Na;qGHqV4Cw8(8{LFwv>m$9H`)2Jb9p=Y~4 zlun`ajk>1NUBZ;SP?|Ps_`iC@fV}y_0fij8p(4+FKV;4Bd_W>7>$X)%?JFD{prUC@ zerW0#BKxaCNq(cV%gQ&F(vytaQTf=Jt7oXa+0(v1C4V@&W`iHe{#7YEIq>Sy*_n{V zzMfSG|F~(dUu+7!keeSALh`qpCaa13qWz1ZXN@&pke`a{*Jt>E%8yRxslAxl@Zr>N z;?r|-sXVHnI&aA6(ATv5VEg?N<-}n_eU%&-(`=v_$j?1^XgTTY3_~c)vzSRM<4CS= z>PG&?Y=%t_CcdpJQh<l8LNoIy-SBX~tB}Rop;>gdQmA;ldpD>tb^OV_pqRY<9<`&; zqvDwZ2YX>|To`0FxlvXS$vI!t9Vz`H`{GFYeJitFNFVoexjT^;?Wvs^-$bNUJUhO_ ze-_CL60S-9GV75pO^-s`-M%X+UD#Z`ooL=*GR28J<Kcbci&2ep<8^XDvDfK*0sLow ze^|{o`<K<c{6G5QRCu+}%N51N-g)_}h-Fa|%~4FD=#4W~Po)@WLg7u=*y^$?s5JHb zK;4f3*5VM-@ngTiG`z*LGe<^&PEVQyZU!|k_3KVyp>e^jJyd<f$q$trTC@7(X6lw+ z_iv0n3XW1Rj=HlvBWyr2$kq%Bk(!y!xD(GI->CS7<QBroGpEUYw7S<Gp<&AIoay8b zUhg}rmb#yrM9x%mXzsnyWyc`zKa~?w2pWa%t1bb(E6qJ6w}I)u|M!RH*Zh~}XaCug zd5zS~dMGyH<wFw`6UVP?q|v7C+wZ0i^xo6rq!+u?k(w<+Dg9uJdt;zS99!uD3ig-t zmO!s6G7NDhea9_Y+MuW|TkHWn-*t@>5zqW41cI_ibKOYrsoLvk0tfw#E<WL)csu(g zb+ckr*&Uh<uq}F!<gM*X#?h>%dV39v3}o|kr6LAot>o(7(95p7o|WuXQ&viDQSUQ- z9Q2Cr7k81pa)r~pb)cfQy{#iybqez~(y>AL964m6bwR!p<oe7Y?M<MEm9l*{^sF|0 z?F3MC7~9ATdOV>|pr|GKR&1A|mq8QLX-MZM#w~~ih0pCH=9AqT&z|IlET^{fZs^(h z-YY4(c%L~oi^^H9sA!)K@^^pSq<nd9cq!EzH&ti;u6NFwMI54PBe$W;LH^|?)d~>D zAtc$sPNC;_Vl~Mz--d64%&x8OMD4E#>iLq|i9N7fLN`bJW#=h2V2#I+%oNi93amO0 zSzxpJ(sgu0<FpTgsU>#(Pl9>Q{Js#-%5l2%<2?3b*oiz2?RUO)^EJ>LKU(~2Eok_p zi8;BytoF>&U@%P-UZ#TV>5IRPamZ)dV@7tZKIZO8028}adG7%EiVoALzIz`&D9;C( zR`b7Zb7&XUR|`tNc(h&S5pd2%hqh&))rS||vcZ4$_lM!~-@k9l>T0z!I1G>JFE>*+ z3+jOj7f>;SUbz^8nl>(H!%5#~vM)s|*8h|Pnn%<+v}pEK4s%T9*|8K&TudLOokM!l z!rNhoK(%jo18LIH3>InlJ=LAApzEkii`UceU|qhRqD6pa>3AHDh34#VbPz=!ouf~J zYLLFMsn**okh?FBpHI=Jg}m=TiaP#X8lJ5Hqn2N+*ax!ZgWS?Nctd4Fl?d7D!N>PB z$*2$S%#VY<zIe#Vqu^7|+6&U~^p1$mg50X}hD-E$t+Z25$=$^eD}^+yH~kt)la_aB zv$cL8Gv4u?qOtJX!_}VTJ9pgsLe}Xv%8fq9+g{%t0GT;?`u8M?Zg(ggm}nlDOYW@h z^m0GBq2cql!)W*f4b|uo2Ih6KEenF42@8gkEQD=UCPS{e@n%Idn0cyQdn%_+&)0{j z{nElheoNtljqP9yJ)2+odl~e?hQQV2hU&IgTS)Ed`LMS+WJR|l0~|mm+Ml88sJyl% ztpJ5b6ZBk2{$M#?;2<ZQe7gn|H@9%Iho157(k3?ir+6g|SN6N9wBab<!}B4PlSQXX z+tl;PMln)*@%aXHU3F--AC*r~UrJdDvbQ74BtI(q*(m_&!a3iY3rYWv{{G<peg9>) zQ2hPqO*nJqE^hVd?+1QXg1zHh-jb~Dboz{>&u;NIpT0W)nYZiX>V)Dk&5ah>pqMrA z8%3|e(=l}G&a@Y$gi>^q*-!PP=%AK0o#6xGlNUR3z<l{KYsi{M+;5sgR!qt|>reXI z`<$jj@2%{8FA`+Dcc1m7uj*Ub1JoRwVQdYm4FA%g>j=6BpDci^_anw1!wYMkIUfoj z3p;zfT8ev}TQbAp2cVaC3;aRuA=@<!x59|Nd(74;VaH;2J=+VJxn$j)4VtQs+9yF@ z?U?S90t)>$)=>GB(ccr-g3NgFNs3ZJh2c+fpY2V1JR^6ge(o`w>{)ctgPEXr&5<;U zmN>-dbqwiM&mJ`(+715F5_+NiU&Xe>$qzS3-Mw?rdOygxK$Bsx6LQL}$xSNecAY}y ztjfA0{S;mnv>?oh>{RKG#zBt*%gaNE%M{Bg%3~M*gwN<dx2vhG$gTLQ%`@hM?50Kg zl|-FI2{i0kAH$L4?)N5Jx2Iv~=03IwwU40iFgFI-`JyGh9Oymh=~j}9y`2|9P8@8t zoWfSx*)D%Yg6fS^o4A9lz`UttF({jsAq`K*J=5nx?tgd0oFJr&9`#B)P+0vV%>ZOC z4R%En`ENSZj%?J;DOR9*r`@P&AfMD;I%PrA|M3aQZ+32)A>}K~m%Eewl|*eCcE&fC z87}5ft(A*%R)OMWgX5MUvohRigLM3co0o>y_}D7=15>`vdu9eIK6h;B1&XWV`;h-! z+tERKeV+~TtR?@^>YA&Qh&`=Z9VwWaMURTq$#1q7V{O#a{u$+$W>t1w0Ww+GYY*77 zp7Lcp<e2YajZH~k@Ff2cjhm=V4Lcxx;nQ+Gsr`$N>Z>5Dwl&nG{ytJ?BfV+j6lXh0 z@@E^*YvdO}cg^Y-gP6v54FSdVN6$EO2!95hMi_&#=I*_zomiUzGY5jg_jg50NLJ@w z3qZOuuDZw>vO3TF;3Tl7!C(_B=v51+I!o<)GyAL5u8h+*BKxar-cUdAEw0}g3He0W zqrBCi>{oGL1L%XMI=jSyta9r5Jm@ulZSAMP_<H)RrSV4SUpWm8B5L`W*HgIEtd2gH z1`3A^6=yhnAL<x6XAkxQ>ooR9zirT;y*GClF>m#|@6;}Q`uBE{-|_1wlOM7xKFz5e zzgcUAhI8=$4qd93fb3<0<rLzk2vr}VV&jXjVz6$x8?|%IYRB!=&o#sORBw=X_Z>t1 z&CZ|w-L)93x|Nqm>^E-#_4~JoE}bI4nh6(eSAarYMkuv!wZ`KglK-o}l+ZNGmQI<z z5c+Ta?E}KVnt-iZ6tDP?C3h|HsDEtdK^dN8$8I(uzZc&2A7~E!jjmh+9dN+JyVD)< z6@t6>kQTGSZktu^zGSbuIFRClP{K!;<Ew$Qkymch9(^`fxPZmr`K*8?a_Gmuj*8qx z_S^lkD_~!3e2Hy=-u&UJ2K!()%6j0VJ&+ewe>hBMa0cAF)6aqIg`kU$uxHac%`${t z?eKmrjX3-~YVXEv-H5NZFPi{;?wWgc3ncsCK~x@WGt;6e^7$0E?V|@cpvePe0PKc* z*zsHXYQ!R?Yc2UVw)oW|dzQPlE#*^tfZZ>`UR^1}M%*vLuMmeQ=H2?!qp|;M`-2}T z8>Q^6cN9o`y&^)NF4J^qu_UvCzu)6+wLc&FdZFK~a4P<lgJ<+Wt($wQ{K0Z-`I=Nv zb5Dvgn?tQarg@XTO6SNo8ph)G{=O6r<?idPDXb`(X>?o%vY`#*$c^}yHx0cZ%WW4) zKg#VZ+g6$n{pW2O1C`MGM{O8!1mpvLg&u)kA$+?^i(ZrD*7FcH)we9F2R?%=`YsBi z;c(dWuk8_d54gJ3ZRHEt%QiSqj)J~!RNQL1&gh{9;RrX_VR2f2cccf|92rIW)9*%Z zqVltiRniyJLhmLE$?XOAIM;cgcxLQbqH<e5JsQ?^hNhOW9PBH#C(H!v<b(XzLw{pR zpYUUlwUaO5s5P|9+U;YBrTlK^{h+#s#xk15#k98PXxiZUYw^^s!XCdeIS1}ldP~E} zt$&6zT+ExzbCC2QTc(5Z*f&;WFQ4-;k;<X@^o2dGl32~NzgB`mQ0f#H=!I4r>YRzC z9kOSD?6dMfdy>z&HY7dE^EeQ|fjTL(jX?FWqtaHJS~Dgtqx@7;mTZ-#yBfWFQW#bZ zi>7{MJ+AM!KziQwoXZ}Nb=uaNZiLKqIuBX|SyYvdvI2#yrh}-Tgyy{hDeQUAlRu<< zggp&^l3xhT&G(sdxNh@?kDWl~5cAxhxOn;|x~}5=rKOHwP|YOs*kaVzpmDY>WbeO( zAyT<#ee2{5nO)wxi`qv~e!QU%xYx)hE~6NJIpvP7BN*fVb(8?Tklyl8Jjfy{T1xF( zlYgo&WNlM7?*!N}{qMnpKz_pB1gRVaF1@2Ad$R&#&?9N$9P*p;ZdR>|Nx$;SdYT^P zGxwKo0@;iO8+L+%Y>y}TP0W(k0hF(-`*@Fi9B4T_mBO&%U_)U8$g1~uBY$Bo?@FY0 zWzn$>VAnrv-;C{`YX9W7!AKX4OFG1po#Lk=lK8qr@FD6qKSR@EmCby6Y9Ds$+f=FD z=U2=Mh8`W7=S__8uCOMycz&M;g@my?DU8;ySXfJb&l;t-A-}<{P1>}4U>#Q9CBIaE ztTm!#1$)1AG5KL#!pFAMk4!)14%M3#^lIS;3Mo3%9YB7k`}D=c{(UA$*L53kVlLUK zU)T>NvK3JrQT(9)5%EMgJzeF=;j)2~?Wum^S<cjw_+gV>81zhDY34_q)qCkw=$WgJ z2KfVD7oD;ORBmk;AYK36#S9uxOgykn@}pjkCFF;AI<obU6vy%|ZIS4IG+m;b_eqJ2 zKcP={*F8%9^nUICVC%xRw$hPqWBS=yZlh`({q*-6RKer2s5|&x_h-_qEE`hkK;c5Y zpzoVCVA;p<2h%w;*Dc#RX*NqLFf)a|Dzm%vBUvHZ?TsB|w(Vo=LfGk*99yy+ROno* z3Lt&#qY8J(;tjX#C6L8-Co^dnst$g<vjwZJA$n~_o<_R*(&^LGjC#(^Qlwe1*zCkk z$W`gXd&GfO&I9RLg}9=fgZ6F6ijMlq2z=hBOLO!49EOpJsr055bGb2;${lgRZ3{jx zu5ewPauu@Xpz^PDy<S$`?e4+;dP3(#xzN|u9?g)3Ptm9vnjUJpb@M{Lh{q!=XTZ+e z?^~iY3~kyynoRnsGmg?KOIY{vG_8X4^5dfg$o%p}2IS7NZ@Y~{A<Il0HcG=}U86_S zp=aJ1i@l)F*&kfK2^0>0xJ<(oZW(6_isRahTn8$K&+}bPG)pke0wdRac~QcleIAD; zD<R_%bpo}Iy8FvZnPA=4x?xmaF??e%+J`md*WNq9!7Tk`<0!~XrMH0e?C7mc)Shf@ zVpR;Wps<$u#arI4oUY62OdItjvT;9GLeDf_pPd7l@9y9t4JS(axBvcdbNWAcpIEg! zmqxAX^6E!BP+XyO+t*0QqDh+rQZsqa`Kp4<MAx+KNLTcZG9i85du|cAKNC86_ag2| ze&Gl`(>h@*HP7Ln(V5T-+ow;Lx;L<*lhmBXeOp;VU#$#(POAZyS2Hn#SYJ3f4tnnf z>XPG-k9o8xzXtY?{g|`^T(MO1=?RcO^<l^z4#i3D>w6kxnP<*W^wFAljoXKGS;@#} zG`!T!M@$Q*^rS^&4-$Wz%a*!t`Q7cykgivA-IVfGx2ZIuZc(IrzM)x%89F?rVanF+ zG9h;qE@hq$FUD-~X0Ycj$jWDDeW{z}1=*9N?vB~_UUCD?z@SBBf4OLH6w=w~P-(Ry zaC$vy7_-w88`E`}kUB{!mt6f*T6K8b&ZpIgaB!fs`48LsxYKyp3-!M_SF&fO?H3Re zO}>YbzEi{fz94^Zty+LyJUC2x0aJA}UV3E9)TPnVaA<X~Q6TKpgMaMY04io*-9p2k zzZD|=KEW;Rn41!MrtbK2E%Z!0-`s-i%=c?VLsoUl(4u^V4!XsQL0PNV$<&WbKBfT; zJKQ}UQh+-CBf3-niN`P3(C38RU6uxeI?<J-6h4^zcFJxJREKFegW|ph-zhw)SI>N# zK<VY58aadV60aW8Y^qgoAq#daFQ_l&uX<;+MjAG0fB8`u<n1H#y~zG{lfTaZV<$}d zLuSmgqYEgz_M}MwhkAH*lfF=sxmzwHf7m-@ne?M)78IQ*&DP$p8=1mROkI1Q`h$P< z>z5zwa0c5FGmu|9#$W0mVTma9ud%(Q1N6e(-;=2PEM>_$8Yiqi-FX2A7O8(*5_y|b z)K5%PoGkUTXU2FhlFe+SamBW8wUqjGvu!EW53fpgjUcLZ`YVdT+)Is}NM5mHRw&6% z^67G7?52*A|FCt|)KAP?oFQEY|Ni@be+VZn|K(Yi=6|-aZ{eo3TdBAmvddJU*j2TP z?sf6i#@s@pcAKdeIB=IMFNK#jAu|$5zwfUfNuZd;rA3get}aa`)}L@Y#DR}AHwz@| zzg|Li%wbHG<aRqdq)P6t_2y7M*|EL`Hk7{kor$!V>gMJpb<^z?zEmEaS(Ak{N@qip zwi6Y1J{NK5wUv9*q;hN?zhBD#bKXxXhxj_RQ6Smryc(SiI|QUn)+9G53i1PmG+!mv zSM2Q9p2{l(_gY2a6enFy$s#KItUAR(f2?|xR1TM!^&zC6d8>60aloFx9|;Sg2U1~& zrzRg2q<`Swjoci^IealCYE)08@(8Owm(Va^a(jcl9H=-pfa=8_ZyO}J%aF4>r22JJ zY@yj$Jh-BzbX~raw&ccncJB_;=K^|OliCgc{`-G_aG(FdD=({_EwV?ktp;?sn1o`g zODZ}`qg5Om*9&@~XIwlrk8FMNHKNAN=rI&U3hWoVV)hW8*l8^~2l=qW_q7!q2FVy^ zMbTJOuhmsgF!^i^?T)~Y3ySM>4U3v>4qfwaK;Lq<2KNH?J={#2?t~oSK1-9LC42tx zz&Pl~wAxce?pil5CXYt5cx{790Qs|7c$E}%yME3F#N9;-jn1r0r6IXdK})?>%OMLV z)4$MY<=?MpvlL{SCOw-F2b^m*4Xkl(_=uwM-T|W$q+#(h^!pOXVuO5XZz}O&%3H~; zH7m_^i}AU_To#CQm4@vF$&EDT-FAd5^o*E0pVB9mDJOshFXBq6+smE@G(CcJd2aE8 z&5-5o+jgYuC@NZXr*c-`xV>`)SUO~eCAss$x9<)e;}C>@+$~H7>$^@A$<6pKU3cvO zvu1l$QoHgWnoV7VbXCTI>=ej~sR0U_#l;twOFck#wcqrypkkb2;(U@@$u@TdwS)~0 zG+fI2S5+mF-gJ}eJmTes#U`Mz_^*hGNM~;}YH0QmPqombVJJ*m`OXyS?7AkliHE$^ z<)=T%N88mKk!*i=8Z9cTmYepX*{4U=2Zu~bcfT$DY(y5&X?i&H;)`MFv{=;2&mBnR zX#9G3!(1$CC*5*9S3>r4e(8~4#k`n--$~(-uWpmwR9}_iV%IHT)$wy9$xkM-HNSE= zT-WLZmrjgzzq1U?s~5ldB3&q0agf@z{>{Se+rX*rE05$NosE%)l0S;P*M2`x*5~p& z7qDv8p~D+N<t)|sbg+}d5oIZdc1~UR?I2`t?cfsX2U+Z`L)7oW0zrC%QvT!HfsL?Z zE8ZO+MO--eUI5ZXt!iVcuR67^Q1TNb{ri7^81Dc5_vri!*8Kn#w`xma8nN7U2UT0G zbs#zoR4k6OC3j_~J{(v@>|mIb#epi59Y;yl?-z3%vZ`|1BPse0mv_k^{lbe<)SeZR zHj|o<4g65H2YSJ3>e4()pLfNL+^DX+!EEVzy#0L|4tYC<8qzRi=F4VE<rzAzj=GJH zEbP7ldg1zMQF6m`2ir=`d$|8SY0)B!_NQ=wopALj+Vbzl1V@q_-_C%Bu@JcXxDO~> zR<woOhZQdEyo=JW-<pyGS*2GSwHdN-Dly%a^f8woOa}R}2`#B!?BZA-$=)d^Z8u~- zB{Z6b8CyT~J%tta(c~b75mj7yyY(dVw$gj={Jgi32T2}azIJIb)OzDw6eRELcW69h zl~Lgm>GMKYzgdu(D)0LoVypc!YuNGE8!Vvu@?q)AoWWLI&C5zjZWh_81nE@^xld)F z)v|%_3ZU<0q4y~ptm{@+69I~;z0^BEHsIph?HuISkK67Ah5I=jL!sAyeS8_UZ`I-^ z`O%;-FEJz*>CC%-u2im%R`bK5XG@A=7J&Tn!YmrrqFuk`RGyq-qx=d=FCY4p`c<~I zzTzH-dx9<Bn^1X-Jyx{I2V)wQj;3M7mM;#7BhKx&@&pI^&bdZ*Or7H5L9%z*P-(Gt zueQ%jlK;`)ABM~S@kJuLx#kf)x$wRfSx=M?pIJjsG&I-5s)@>8t0Y$K_ms{zUBV4X zL9wdZ6ep1T<ped)r07z^>Amy;RRMGAY)F5tY>z9*Pq}_6fx~aNl>4WoK$dg0CJvx5 z{P|@LjG6LGT!nPyogpz6q+fhe+UMD0*}#!B9E7nQ=S)Jn;zYG{Yu)(Bo%`hG!r=u| zh9X_vH(&>q$NblMPg-mUd{!7mF@Erlxt3sEpFHW7U&mW}3q@O%=Y{KJFW)2E6aw;2 z7j8L0pIr0OpJpNN+x2&;9`z4)XD<fDxBeBO9M)24aql7_vv3=Ia!<ae>m5_D{#DjT zKctIBUB`_9h2DN4RBti*kO`6fI<7Yb6b_AeZUP3Cap!3mtLII#;K95JgGbYFDSUXs zmBN6`?=7R%hq#-sr1H0Ck<yRebUNiNn-4k9AVm?u!9LT*n1-?P(v#0peS39GFo(YV z>b-m_sHhulGZpCz7hd|b6XbPbPFg@OzcVOn31oTGPo?Dk<6Au#un&5xHv94@pL(%W zkIJFB;)IO)h27usV+3ST&s{oxeAwW)z<B88lP9dj?58+#Z+7S{eDRiB&F)`=-QkST z_F>?U+vi@O9ffY|tFC52&N)(RhvBc<wR`)VtN47y`jrccpqJk|=I#idx6$9Y7Ej>$ z{K;2OL(bY6Z4n4Ie`aD)&f)8MyD+yB$m$XA2GZg+B&vt|dT_*hw|3MH5$*8nMbK^B zKJ!f+@|`wXPGMNA<1bo)nRmiB37}K6*F(aQuG-`M#)u9txhS@uNr$C;7^)tEFIZ|b zhTGcUu#~x#pEYK{PUq8MeFY9BS-8UI8AtX{2W1$71z$Uf6F7Wf^mWQ86C8@NE8A9j z;>i!%*U5m=8-JWK9QnShs+jN2#Xl+MM;@Rrh-w{L&oRTHC>`o_e=VkR>@wFf$Mr_o zt`z6cVJXM&c3+6=IjLIf1(E%5J3CY4(_HWRMjzNK^md1uA-{sc*xLh&zh%wy4nAmy z^pa@vt&v>2cJ0EKT(zU?FFvemi}%{6;hhTa;+98c{kBBSFa9)h>+7daXX85IZROE{ zWIs*+;6hwK-nVq15#FaS8#v~y6YN#DmV{Xpue!SHdF)j;vLAW%-AcUYUvB5G?+W`i zCFM3V@IHMy*YDmm%K!4Vr}AR$RqM=qec6Wg2-)?+K;#S!`?uauxEkfNwB9ly2k+Yt z^HH7OTpT{UX5{9ptC9bpW%uTIQahD=Zn>b?(9kgE!9fn!>-X}*21n#?t@E^pGxsv7 zXofssD(cT_4qY8wjB#@ATe1{K>^u|{mX(ps8apFhrqR!X{9JJ97BT>O7Mrz=gRFdF z7e;Z4HSMx}GRY|o7b!r^{bdW6fa2k+jr2kO@u7p>#aOkCuQ5y~`Rj#t3djXTOLwdU zLu6HcG#$9ry-TO~ARK!&&5c8T{chHh|FM+Aho*x{t<M*npl2F(7vCeU3aNufj70rS z_=Z!Q!B(&Jey`$mb#-n0zS2GlT`flavWEU*d#-dgJ};QLo6Uqi-EG^RMc99!>F2SD zVQ7!8lhTGM$Uek>ferfiQ17%z8b5-g*Mhl_uiZ7ozULX~VQ)5;?cs2pixnZEkiTw7 zu}-Gz=W$nP9O&dEn)yJ#JHm2PBH8b2^?X0s?>BF}3i;@EwK*P(>+ZaLX?z0gz2Bep z@F`BGzXR{|k*>>C9w|b3FOIo=qKM@FiVMUK9hyI(d4nYkJo%?D5?lYvy}tK<cCX+4 z$={E%^89@%tg%L^YH2l;Gv<PdgRB#3BF(ms`>hE=x>8Xcw;znTc78_%rTe;AOJTn1 zW2-{wS!v-kvKI{2g%j078@JpDyQ;mngnUYWKE!beWWoAS>pdWA6_5}?dUkoyH&k0O zs^3nU?$nLvO{TER?j0EIKz5>!k|+6+(X338Q$I;3hst}jFW3QnomqwAFzE|gw{j;t zhq}-`#0Tw~Qhuy{)HrgBF?T$~a!@g2-_`4&`mxzhs*hq=+6jbVJbKuadl^*c7S>X} ziXnIOX*DVA?b2>9WTyN&n%rEdw0L_GvQYl1{b|T--+7%Cpr{@bx0(1?Pw5pnw&2la zY1k;8%eRxAtR`Ps45lXRS`G^N8GB+#pWP-Rj`+Cej{r)q89eb2$yO(>9wm8P|2q`6 z1>aMz1kw+*m_}}ey9IB3qAa6@)L-%yMH@+$UvE1Xc7nKiKTS7+;>+$Fkkws2v!2q~ zgoG_5n_Mx?gDeD3=pdDglK$<#KdhFz{fm35|NosbTinKIrjFQ2UM@^rd`{y14rhtj z>3E}*-u`PU$+1nPp9{Rsaeu!J#VvGtb~F{m)fg!5BDYC1F`r4SiBk3P16f4h(-hVG z`;{yX1}{GHdO^+f<D!^HZr~kkH7pQR$`sFCK~@#JmfXtHd$jRZ$i`+NHql^a+q@5H z&?`Hv8ZNnU^q#TOYNW^aE9g#P?TqHvj=|2v;mxDnV5{OD9gATfa&q4$x}GAobqiX2 z+iemQ&oH|uWPfdW1Ns5Gwi;dlN3R+6f!uE3hZEAqrPV8USkmX*LavN1<#4R<!4j=q zkR!SW8%S&$dTuM^G1ghq(Rx9qFV|DOS;YzI$HYNjU%pSo=hQ*E-LMM4_8<%T$AF5* zY0}Yh>SZa?5qV6e*O%HSD=}#ZMX{>FsSe~WUe;}Q(Xf&))4ClBsunhup2RYp2HOH4 z_Yc=7qjq3TZLiIPthcXI;Yv_6nv}Z;WOljsx|Gg-wIz4tA3VO{4LZFlmbN`n&#1jl zc6p0(RMF6DwrCfm0O9vLHbEb;tp8Ukhf8kN(_F}LON`xeAgh~o>qKEk{+G>rCyMUh z<6C5L8Mik*o7?y><d<4Iy5?Zk{kEZPRW6s29=&(erd;R`YMeI+LsU-h<7u@VJhge` zFK@EXZmzM7!z?N`dFqEK-PXCNLJ3|-bvm^eyWpI>eIsQHnBU8Iud?{Bw`QmA*KUS= z!L;MklpJ=+QS6@Nh*^Ksk_Y<p$^OyLHYu<x>-MYDs^Y$VZ7kbgUXSY(temuA3wG`4 z6??!x3ZI*lbT@M{7l^-u<D-k$#$;;0=n{_ftC#ZUCv&>GMrnG{Nyz8zl_9GPIPC88 z!L^Q7PfYXvhA`AWXL7@I>Ti`pT%QQ&`Otx3)+C>gOAsKdMyG9b#A?dI;`H5_`1#81 zLlYmxgEtN}+B*~a%F~Hup;(>e9`_p?1o_AW)hJugqEq)gI#P73!)QC{D9)X|rrJYx zUhzTC6|1M*uO=6llYQ{h2ph7u*msHp_qo_=36$O@;J`rGPnUUp4W|6xj+tsoG>>o9 zALU}g4rx`~s)tySLh{h^o6aaVzwy`|d&olc+#JcT=In336ngwFNJZgZaihC*8Vt@c zxNZZzydd7g8+ulh6iexXP4gfR=n)|5sk|1S17{lGx<hQVAJNgO7Tf2l%~8H}?rCTr z=>3Pyc9QI`zo-4eQ+{3<;wIUzb98}S?($C2lSt0>GxH>V?9tvuvd2bE(*Do6LtP}U zXmvuL+To|w2}#Cgb^7>xuDf%{pT4*LKf|F)3Uh4k%>*g@RW&^<u|PLXBER~yK2=kB z&V%HJk5X(<eYf!uGims-BlpT}shEX1N9CX@)h(HZonlXEtrhrfkxTXs4q@<%+foV( z-kmg~Xm&sBaVT;N9-TURkM+9(S!h3HCWQkn_0-rWxGDK{Ief1)JB~JL9tgcgy5&1` zlepB-vbYMes@Fk%8kQ_V^q9}#dfRtz4JVn^-W&y4=(l>A7qQFpHAI#-)`Q$x-fKes zx?;TlYnr#8!lve)Y-=f>dBe2H4YbReZ=!OtkYA%|G02boS+@+V|GL*Uh(mq}2^#Y# zeg4|kR6oIfZd)FD9AVl~MtU(ZhZax5#<3IKpy#`eoJ{S)9v<pC0TeS+rdW`CFHd(g zv2gSX`kXk&E`+89aig&orSq*<cQgeRhw?X2xD?%fr<j4|jboHl&U@#gHb|?2=+{fU zAbaG-Hl=#;Lmxk-Ff4dBx=;GF`jxfRZZWTvOYcDT_B*Xj?R0p}r;$|8!buA>?sK@# z&$qeK;`d18FgwVy7uh2Mkk0y7ei;mz1(-~y@X3lU1<aD<ZJQ0iw4wW3<`v_|6|1g1 zJrDUt<?KbYT2L&#-6s>&4DQ=H9drvZ-9`N?guSVxeDg-ey077|xLfKwel^I~&KoGT z_ZFYq{zw<Y!@n;C<;lA=_kwdgG%p;5UFp&t>(W4tmS-EQp!dF6-Iu0gxmjrz<tu3G zwOs}3SafS247#QLo<!3rlbJ7~n|Rjq^Bk&&uzdX@D!&lke=n7b)zppB0qfomxkl3u zOJ3D(F|ohffr+3P`Ffun(pmk4t2F(HR;$NFK~}uGFvS@%Yn-~6?D^w`<9$Hk$Tw3X zQ1-Y(#rk3_qVzlW*a(?*cdFJWCO6Wf{uGaxR)i3{MPIQ1nX7FxYf$y-gCm6-Z&l#t z5Rmo9@5G=NFMaCn2l5W`6_Q^mhg}{8S@A+kTJ5rC9(Sbv(fexdkIxCOwVG3SX2GRv zOo<N`ZH^~>T2Yu8DC>IQEQJ|$zJ8WBD85gaMeWDGZPJ1KPRJ;HO6{l8do-EGyQXVO z^>XN0!&no#uFPTIsWl+4oHB{RXI|7YZU=|yz1f1lsDJp0B@?LJg2HY2TIhL=3ziY2 zkC-{to#^K|l>ET_;?N-aoa!id#0PrTdia^GplE!)A>}V#^V&w`{AYiESRMWke)qt_ z4p1{InqP4b2Ze95yVCGt(Z5&E1cinlmQXi}2dB-TSrq%yT=#`sFz4ZDa$A1S;l_?& z`HbF)kziH(tPdGT7g`<IB0WkJ+%`Bt*6FfnKDnuwI%gcsB6U+$Bc$%V^V?%7(iuPE zz&PmfG-$XSa@~yM4XD{ztmR1^$jW0iGuMG^Mv%!0=!M72lg&uxT_2IVt1`XXQMU_$ zcEyxGyO_Grfzls@ySWf;^0KAo$C0N?AS-p7g;F=FTW;Ix3F@dqH^o3N^bWsB`O3yN zd+Q866MGM%bd8DHKb4?xXJMrwsCqr>KoEy~Mw*pSxp<}Sd0P-qn;fV;6dm3DH-JLg z*LieZMajLZR8FBNez{Z+E&J@=&|`B(X%Ep>C)1|xgdAhKz=(!Zx5<V}6JW2>b$OBu zR<$ZUM{dZ}M$#s8TC=mRQhD?qPc)=@i{H4DJnRImA@KoVPT+$sE1_p;=_xA6ic{Ic zsQ=WSAuise|6tf=G04Z|E{g|cPGefTl3vxYa1{soyfme7ql)aP^a7p6<lI{Wy)bE1 z9F<SD`OEnL5RR|c4=iZ1Wg&%Q{@PJ53IhdR!w36txSsZUSMn>?+2Z0rA}i}Y3F-1d zg=v(J;2nKx0_deXy%+hD+YHt3O&l%SDoYd~elG4$;e?M1ilKht_pfe2VQ=!i#~rA@ zbmpj~?S%LR9p@TB7Q;fcm7pTJaya#$_tW{`J;0jPBi2g(RM<IT4rJDAu&*xcgp%Kt z>%k%Nt5b7GezdE#9?~x^+o_ieszU=mQ@zxWDj4~xiOTNnOYq2}3g;5&&6|6drh!K7 zA1CMIuBPGiT+K(2Ywh|J(CV9M>^x%wdI$BsnE(pw4*w;E;T}_a>T{T`oj;i-fkHv1 zmkp?Hn5sMpyK?)O$2&m9>>C%Te=^4X?sXjbKdf%*RRuf#L|-E}@SpwtA)Ng8o44UM zt{J%>3vVmE(aAUYzLT0=*!j9JlJs_6y3dEKbn2KQ%~~u}`js0$Vnz>IJt$lTKZ-&+ z8~tXt3)x5RI}kzoJ6qDF#VyOcM&!LV|GkgEFyBrS$ZpQhcydqiXx`gcq|2VJ$#o=s zTA1t@cyZdJ(qfW7-YJ=d^tr!1)YN?*O{b-z+n8oe&sfQwZ7rtP!0ywqb*ih-*YT?= z4uU;ep9wn(9?j_GQwa{=)_CP15Z`qE4h9vf<{u+TzEfto0OX$y?<~zy<{t~ZIp~Xv zL&j0MaP^ZfC?37M-iMep<#q(=<LtIjKD=J~vTYoO^~ymv!bty*{{GOt|APa5joxh) zP&~n=i3@d$P?LO0iYDxBttAy#d@kK<F|*6Mq%U~cKaHB#FmLF|64+NI_4FohX6;&C zDk5$gWO5jK-o;G%l~UtoO>SI({();dy&K@<pUwlegEGI9UXNhMGRl_h2h~5{jZ21J zFi@EW5eH8@MEC0Km*Zgzq+^w_Z6lE-^<GW--h1O{k&O+<Go@D$_Fua0Li!cUpScm0 z7e8+!);%-Y2|K|$cd|3dsTor#`eXMq8!4JDKfB8S>C7$Q<4)4&6$j>$Jnov6EAf3* zLuaC}Xm5X{3+pB)OZf<TkrLU^^Zt}x{bIBu(wP>&KME9ET{y6g<fLo<H0v^r#mO{_ zvZ|Yl+(|C(q;w$pxXm|DkZ&?n+J1xYntn^F2d@YsJ>En%T0ykB`9`-GQP$!G?PQD> zbd7YSe%o|CZ82n~)%&*!6sF}&-oqikq#ZY$NZ+u5{bI<>%dgpJQ0!^zER~ag=qj~S zwb2`jf^5%=-?PX*#aKF_Qy!5Lngm&B_$tkk($lUj-a_fe_cx-b$_jc6u>$k-gevMk zH{HPf)US1Oww1<#I99}FA8}9bW|S_o&k2kqIe7i8K*(&|%f>Sx3!ggG%SmQ$8&bVk z?ChKIB;VMrZ4PSg*<7CvJzw6sxj&_6AL5OO?C?xK;*Bk?+c@O2T^>fWwb(=V;3`nD zFL5#Xk8pXXz2wI_H%D)Wo`sHivl)7pd_pw><a_RJMfnJkmGjn<{;8MmU=CDudbFHm zE9K@?k_R>$L4GI%-}vZ7@|+9nA}D=dX2l$0{p?mWYhab%a0{iMX!wro3q}}vXM-x= z&12S(KIzLXsUE7hT4~&{>X%ae*3_g@xKeK^+!_ZvS%({WlK<~)Tu8GGleHWtT~G5# zvXl=$QTRYN!8$f~Y$<(^&Rl6$_SRjlNiwtFtssgC84kqXU&1{=^{kfWG#+pev9{FS z89xl9@{8|=hLOI0-R~6G3FTi7&}{zC{{9fI{`=om2#)>d_@bEVn;kt8LE(B^U#Xjf z{hMewu&B)sl2B}3cfN5TsA_!u<vLI(PXFS8blK;NufiZRVc_3CV)U5TdIR*lf7V=S zI7EE8KA!9k3Hs9T4D0gDn(S4TX-Tld=3nweN)H+^)ROcm+Y8N!StkUkyBfC`I)-H7 zgkcZ|{`h>A++Aq4#A^}A`^M@~_X|z;O21iWPKQ6!;sm?+O_j?3`LL!GhHbSyB>VGD zH@(STGclFAgO3a?qI%)TG-+|mwit#}w~3Ft4Df<pmAg)$`iZK0UDkra9d4H=$W%Fv zB=>mh%uB=T>YLK>9Ng1;PYVhgoVT_9CCK8$q=(d=b!pookAOPAJ>DHB>h4LTS@{1k z_nl!;G|RtZ79%PuVp;_i6M_W6zzjh$NEQ%f$r+anl7<`vT%sT%3jzY70xBXRI|@cn zF#@lOYsQQTG2g0rznl-}dEUS0{O^4(AN*O>wbj+t)zv+zo4HXEg7tHBWL`v*K+mry zF^$qoZ(PUlq|Q~=WFh}vQ6Q$7l;gv54gm7Tu8F|3!MUkE3Wr&p<F^Z^fL3ixKYbMF zuwpti7x1F=I`+JRmyx~t4}eZNu;XndV5ax|#m`7s^xf`}M!Qs<HhPu-QlSOyXb;a^ zesLBc^Q5m77te%zutXsV*!tR$?FTKX@{Pc!ER)CKY%ThH&OHf`;jfy`P6KuGZn5cE zsbn_B0bQ7CY=xNoRLmbS-SMhF3BQl5Tg3X2vDkodMTC8p#<(I18lK7^{YQWQFkJr4 zxduc+-3h$q1>a`<eQWGq#ou3y5O@B<Y(NxVyT+CmZ72hz-sl8kcT${=l{ib()p$K( zhYQa}V=tJ^udIF*5BwC(APsa2^;ziy(sdPb>KNv{?rrRQ3-FS6GiEg^!sdbpph&L6 z#vIBEN<VR$0j<9qTs;LSb=>SmDIoFu{+oLwK^(dGaq|VBof`MHV0ILQL^Pm&j_e|@ zJV0>`zd&z5BIr;{ISKgj&>-6#O(Ui(NBa7`kv4$Lh%58MK#m_4{AxQOp?7*1+QURG z9frf5S@!-ewvQ{fD`W@Yy~AAwg@887W95-1<YQ&`lOXr>Ne|A}{l;?*aTrnwD=*-% zNodiP;seqL?{8cONYouY#V3LO(%0_<C@}XAwM6>tG+i?&r#0m}b^{U~OA@}Izo{$i zJN2CJTF-WZ+`x-&2RA^|)W>0%7Sg-kK0OMw(VCuyL_oOMoVEw}Lq|>RFue8F71v?d zGF$FE4}@|%F(39YAkouMGj{mW->>HYpP09<1npu<TcfOzb_p=ScGI_-4r(IZp)d#A z1NTTOW01ahOOws+Y3bqCa7TApN?`#P?({|t8XuxTjyrs=FZ&^AO-73Y(4qx_6BFQ0 z?>K8I6}+>3d#QSn4`5>GW{YspSJx@F#{$Z0UcP@52=a_*G@P&rdb4L;R0cV#;YJyF z2YK8Z33mGy`4RQ7`4sRgW)!3S-0F<Jc#yAAe|Hh>CI*(TlLO?aMocV2de!MC`iPuy zk1<V|CS@(k1*Cp;e_`W**E)d1P*vVyD#p*i!)4>y{^gwcio;ScdBaRh=iHG|k*J?> z4EBx$TF-jbRg~u}RZ7O;O^8&CQ-M!uKOP<p$oZ>BlC_7p=ZF0->oVyb#w+zWQ^5+5 z;OWxEAV*yGevLHK{b1H+<Yykafa5^kYr;G1XHn*Zm0aZWW2&*A2regl10dy_X15uh zfJ`9j2ZP|rOxgLPH*(<#Otto&`e@*Hu2^s+1fJ9kb3OJe7tr>Jr=B-FfjN3PIWh>! zElUoBroeM^w@a~K72x-7A)PbQNYLlKCe01dsBn#&1>mjFpx0TT_vCNoLH2q3sqt1` z_`7E(^^!pEtb4mxqyAZr8B<+>|7_&|yq5t_e$4moPlDeS?rzK9iu#|)Zb=1worAlB zjnR%x->xXa6PcWkMVkUa-hS-Z5oy3zJb0^rJL-3no1G5jN2dk-iUqv+uBF@p``t`Q zjy+do%KmWd_hsQ{UF?xRebcYK;1?!iR8b1@p9DnXlZl$ZtZzYx&GRhrgv(`P^quWc zKKS*yERd(eldAFkm1Sce!tP5TS0h?=pMB!+&8d-n0<o^4j(ze_^X(Oz4>LrM*b^*& z-p~1`ClUYVHMX<6&o01jghTFh5IdJ>FBAgStv*ixQq%N1D5MJ|o1_8HPJcD5lmvS1 zs<#P%bg2dG*a9y<-(o^40O{WB@yfubT5CS&0X=YL<8K}yMa^ig2dvv{bT|(1_IH&* zhJ-cd;{H?RK$nNC3XTU9IV;EFA`bG(foR0WQ2RB2rcsyJTPK8Z%BQ8sUpkCh1;`}F z|NV)DUM~A~Aw6tp587jzVWWq`q$@#xx;xOE__5P4tVL~Odt3ntj&6%TAZ=Dyv<1-A zc-i?l6765NR1J`-yc}f&{2`_+AMGOQ?){Yx$W(FOV7jWB7afMfy>p(HJ^qe*<~BwL z_yZbC{$9jeW^i(WX86u6Zh)3cHVg<r&ZDfeCje-U*0NtXtVI13ywnU~_~f}QKA2Q| z*E*8IFpEhxb3r_%v^)XA)M#sFwiW!I$?}7}4?$k(_aG1DNfQ$d2=`ucQD!KHweE3W z9?Gk&ayKJuYA(P3d?AipxNzZO?+8}^49{N}&Wi=xV3h{1F&+NWvIAZ>8kyap=m=q$ zr={5*LxzQhCVvjJg!&cT7nxC@UyLs%%0l^hIr7o)0Q2q4umxJMCGhR04ht?k0A1+x zYdO9aQ+V{#7ISiMVd2k*-&L^Q_Jmwd(Dx;$aGD+1Pd%HuG6vcWJ5e}#BF>8)vH(6& zc;Q|MASZU+I-Gv#pI4V}0$f%ya^5i^@JFQT+yvTkxsG89V8H2zqc6d&rLJ|$hhbW+ zn%4gmr-i1YpLNmi#BQsMa*%5;3A#=L_J0^KNd~^1#Juhd!2UK(Er<g;#eb%I1JFeM z)%n*zZo)!cdM|ur5WQo@zGmQ`4~ia!tFSoo!!?LodWq`jT$HD`jyc%{^3;ms_(dgC zF6tTCg8cgN?6mf1i`|SQpt;incCgbqF>@8tM5p93GyL7}9sQV=iN4WML4YEi81_SK z!R54(T;zup748-Sdf1*Y1ngQe@>n+VTclgv02$+I2V=l0Cw*ItbE0eYA{<wQt8`Wn zAl*6?;)DE@kw<Xa<98+*<FrsEMZCdrOH?_Vgd*zS@51SY+HTt#fV87{7V6Q{V?N^a zXPL1uDj)ewq>Kq-q1(;?;B&)X^s?<qh~Qv9>PeKnz;Vtj)9bVZB*e|wFER<q`#riO z=qC+zSpCr<ifnt6JhqsUwKX-r?;TZ#a_FfcZA^ouKBcxo*lIdKvo#R;7snjk2KqR| zlx}0X9aTqH;W7z!5|9do_^vrS&_`jq6f21LMfn8RHC!mCh6g^-1^#fs?$?PZPo?+c z@}OpM(j8M0;#^5XJ`CmM3eTWj%%PLpv;b{J$zH<vAgU6)aJnPbE|c{FzF=~28BV{% z=Fb{QfV`r=B-{Zlt!_PI{lmX-fsJdI`9kzFwJGCa6w3d!x7r0X-?GdK^G3~C%H<&P z%jYSV0Zq)3-HrYhJfE~U75Mb8IkU5X)|=tA4E<a;Zp73`K)45|6O34Sie}sSEn_i` z58~IF9JHSoc!R=mV;amG<BxpN^H$VDcw8zo02Gk!VQI)8$k>B%L##QUAp^)PdwUGy z19onRVdKs2+68BzyXK#xb5PFVUQjF`{p7=a1(YKW2K~KJ1!3}toqjxuJ8*eOa2c9K zPEtK0^&$HL`x;x`lHq@EljwC)7QW?Zfqx8%#9Xgd_)l`Kd9JMCFvI`;kr4H&W`--; zOaID5yXk`7X&9FrjS(K`H!gKq6XSs4Wt6b}^YPjTH=qe_#~Zf))P`bN{zE%g^oPgk zp%m<Y&bh&bD98M&ycCM^)XS?l-bB-$rO`msQ{}Il0CH9*HTeKv{7x9<4>VIN?z9zX z7<j2T-na(EGB^+MZe5??11KJ6Hx2U@ZF_$)J3b4Nii&_H#$+sJNVwN_?WY=E<yNfX zs2l*iaN?35#tYX<$_3}6hS-UI2LXwJs72>UNEf%CvKxR*_`N5k6!<}oif<|adHq#q zQvqqeIo&pZaCdOTPQ+O|9@vn8UuTJL2OM~Lg1|gU44S>Q1vJf`2nVPGO-!E^2Q)R> zJ`C*?wSBl_g{Z9c$R3cJ`@_HvkaOa}WotnA+Gj83XJ*;(Rmc~gJ8C2LgWlXHe&`pG zxl<+1x5U)`BsM;cUj3>iA>On;!j{Qkr`nmNd4Q1;^;1~?!!a<Zm-cUR(g1|hAm?Dd zB51i+=P~|P>d$4zZHkryn^!iMjk6}fAD_+}vvPt<WFF9hp^g$)_+6Nq?~-u9uIblm zFfVgb;<8zL&OBbe6y%7y#S^jsxyN3NaDj61Quyiu&{EO)Hu~N4lcEwEHz|)VtOYqj zuw9HDpYluIOoei0{`pCMz^D8tk!=5Of3wjH<u-IDC?Yb=I^Kw!6dLO%X8+b;<&H(J zSOPRNX8PZL9Fr|$h~-qS3D!^KXw6awq@xEmD*}oxOGG%4fae74M{69<qAJWUu=o4C zDB#mi%%*ArP0x@iQUT<4J+j64qpy@alZW!5`MdO3y9vdG=pSnO%2+Iyw|f5x`-#sv z=NAIV>}Ix`0{*&eTTlW>h&<)mN$_KWt~wuR-u|oeD*;XRW}K-8tb6gP7yDK6spBRr zhr~8mLxTRBIT!HqPxNx^z9c{<;*>P@uRyUtE&}k+{{G>v!s35<|By3=RV^CcGJ}Nw z9h?2t7;(nd3!Aapi;m~C+3L+V>mo89zHE2gQ2mMFOZdkAKsOT|)~)CsLiObveWc-F z9b6{D3vW}{?lu0%#o<pG_SIsT5T+rEaF`RC&(7mC3WuVm$3ZzEn*TBwQ4qd*8X%@E z8p{VJgtOgTGU-MlmNUKVm(K)MaNQT_f^$vEAV*(_@j<u28zdt%fG^USeF}#g+<ezv zgUAs-ri103cc!r2e<AN+I?}Z2+24?snTe|^Lr|`34ZIZ%NHP8Dh)X=1JOF9kLdHV~ z=y%?p4Ns+Aw(PJTI$e^Ea`%kC$3i)A;_3lsl%F=?JL{kCEu%3V5UPPLY?yN`B(R(q zE60BOM;w|v3H?E5d0F@)zh7XBcJfuXjm<{7>t}Q(&_vxPeb&#Fm%~}VHb19rk?-;G zI@_LhTTM5Vrx#XOBJy6|W92Vh-suUn=t{HIZj?{rjdlWL6wY4^KslrG^=v&%n<}Oe zT4wc493~>)2MK$CCf1rg#x%&$)l6pl!{wPv0nh~ZT(7Cn?{1^?PjgGeAA1Y29mKfG z&^1CC{Tl@zKZTI`*)saWF6jw*cN=}vDp!#xKf1$Gc*1u}Yff?i%BR^)QA7ErO!E*d z_np0OJ=x@YSEb<HYVw5dU4?J+oQ0rfgyUcCP%pi@4K^6j&n`D>eBnX@zae(66Y76s zGG`&m`v>+oqI`T;zYE&w>UC%%EAKhgiIopcaAf5VcC8>4CbZqS(HREz-0i>iAqbXN zR;S;=Mjku%J-3tk6^Q!!`Y(i&$9(UG3`DF)`GDx+9jyFw3h#c@%<Fljf%45AbP&o% z?|&D=%C9=H7UdiEP7474s8nwA&BtZj0*cZlH(U!m{bXbu3C)c^ZJSOu`rdUb`uhoR z=i%~AU^h1@u?ORc&e&e;4M<$iJ+uYe;~@L@K}Ck6gL8p44NBWv0Vt^XeQ_t2Kkm!L z>FiUIa(x{j*L_Uxalj^v<=iGH7l^K?6$4FeS9Zs=L;U!*zzeYMfqDEMD5nz+R%K(k zmEzA5p!2Jih1>vIrqiwR6rhMdKM~8vtSWNv2K4E^c3nimMh^F;7~!(NYkHdnme-9s zb}kN(ZZvQ*2V~BA-YSD~>g|#gHXVk}Ux;=S+N}}_fOR=KWr-k1IDb#VWdrlzy#V8q zj)-`K`lvV3iOE27{ZH%F04;eVYI-H|hfRH+0GN=ycJ_HFCr01CaujGn>$g-q(pw(z zlYwSt3M|ncB5eC6CE#;(pHvlNdEFM*aO8_jd9H9`!lMU+(RFarf~41#;v75yU}qtG zb)_@&=ghUH1$e^2ln~hN4h{fVdz;UL6A5<Dp6rxL!g&CPj>ZP#Ndx|iytlxK0-w~v z4g})~3+>Ae2BG}7VXp$=gn>o6<14B_f0fSGtSrz!UiHIUXJOFwr*|TiV)5jGv5H#d zcml&*w>0=lrEDj>?zRU`W|)$>M<)?aFkr-&Y=YkzwzeyV;fV;>jW`7;zu{z}H|XEE z)G)~oXbpuEuCXYecz3Zb*xg;)0?TzcsbE^s(kPVMBvC>kZF&8tBeXltDRNy5oV3s^ zp)z$9=zk@zqRM0CC96YFe%FQ!O%n91E`F8<_GoNLQHvM$ml@v7*YtqjrG5W$m_|G1 zT6V<Y2@$g9=eJ_LA6mGX@cU8sCExpie|D>9Iaq^#B;1Sl1)_g)8+Y>%ht9t=6XqC) z$Pcc{CmSxEuQ-yDfc`vLF^K+DYB72jB+NBdu?^PS26oJp-e(aZTvGI!*epyTeH;AU zQ>Gk%b}S6Nnw13YPI6u-S1DBKb-C&7(u98exYnE&_PXR4zy0>2wc*m@qI7LhEAZti zXLqtEIaI{lZOt)O=~W$Jj`jyFPc#$$!*u$;@U;o=mn=K17mjblu%!FX_v31kDE0n= zX`4Pfif;ilBV*34wwS*bUE2?|zI%NB6(}E=6auen!!XUSlsgI7m1%2y7;ydcv0rf5 zFg22!Q2uqEvvNBLvqfaeXPgG~0@l?ax}=`)s0D24Zi0_?;Ml09R1X}^>$?wKyaP0G zDd1)<pyecwlY0T@(vnjf0r@g>>hl5n$R(k7NLV%5k1W3oG-LC0L;>I;(Q@f}z&)80 zmk0p61f_v_fb`-gHRUAeUGxmDazQ>aeHah;x7K{VUj!&V<B}bwC1&YeMNdFDt=2|| z1a#T4SrbvVpllW5uH9$X19GPSm58fX;>Cfvy2vL|a!H_hhfj@1zYuSWJ4_K5mhayI z<=m#g44mE>Pi-|vK=^o;w1jeM0srs)kmj76f%2k91@UY;9j+H>1T^6mxs6@b5-Vt& z4hgqmp~&Z)^7{}CNKIZoIScsQn8zfhZ6?$83%lxs?`qH<I$M^#$<KZ8R4x?gu6KIU zC{J8@D2w$o(Ra2v0{+?GKZNIh|Aq*Edbt~$m2GBzKMdy5kE_yC!Ay_t+#t-Rxtnb@ zFgyg8FAqcdSmu~w3_m*NMBoh)nrXdX4OTC-o!nT-(}43%$Vg#!mP>gun_XQfyZn8w z)zJHke2`~EXZ2kHO>L&iV7FE+NZJzuD6m}5ZX?E-E0ct)FnF!GHV))O8`9m-oz#Js zikgVM<Tnb-y*leRLU~inr3$o*;7IEo09wFY9*1F{BAs~<!%p_YS+ih3e*Ew&0f3_7 z9~mxya2H^W8wvWzS(00UHl4eB3>WzWM^@PZpYubu!U51!xxpQOPjDU&<N-Y(pFJ1= zn4n?16vJTbneY4J0qa!u#p7zM$~v|fXDh<zNfz2e`w!b32=v}Nr`Lj?U>uZQ#@VVT zYU>B&%kOyQ!s?ftdoqWFa`^}E(Jw=$x6UJ<b71Z_Y=_9Rb_S*m?KZzNtY0rG3WX%J z(`e^0KJZ5czc()f%#JJBifHrL#SrZ<^)%TXNP^y>?YD85GB;hXxBzYMTQ@clP|~Dv zbqXL;vDMcHP~PgE7Hf~qc5XJ%f~is;{Dgpc_vE(#%DxB`pdVo?Q}!zd)vcpt6F`p8 z<LQ+EpT75bzYd^JV-x!*y8rgGBN13Gnd7nm`0A%$xu6}yhxvZ3z}Gui{}kgbWWjTt z{eV8@%h)ubx4L=(`jyar6*3i(I=hZbf*rNh?dWG>X_6eK3&y_L!2*yr%ZSheKBchS zcN><|50AtHpJRCWu_Mr`aT|+bf#&L-Y)J!}8&Rlk4%kHo^q_y?F2yVKBh$l8#dh+m zK7YpF%kJE88-K?eH-A5-1$yJ&Ofx`&Yjy?wOr%5(A;15OO$o}66_v#80%X1(J&)~X zp4}V4#xqSk4g;Ea6mgIJz4nLWYC_PbHZ%e4qvosk+5<99Tg<%y;f2oa=vO+(=o<DH zr{7OI2vA(GS}_UA1v4)VdI1ujep;9SAKov&jpK-r50}8-Gm?3+PJnf@mKU@A)1w+F zjdG2T#j&3WDx2Mlo#?k)YYTLjwR$*?HvwU8i+X6Om#K~@_eX#Kkgopw14Q6;?GE<h zjPT|7gPBCneS-=#^W@OaI5gWVg5Bbtwz_d57U(+Fky<!Q5!@Ylr%<j#aquEwoxl5= zX5gpD7q~S64(va5yA+UKrnnZ}3w!Xdk3jrkJfV^VwEX=V(}cig+2kxh+Wp(tNG!Jt zoR8r_a7S@iJ1!~;s*xYYFJZe|Zs~P*q>YBXa0R3WCrO<^nx7<v^%7zWHs2>fukO!r zIJ-ApI-$b=()FX~vwA?}fByFm?jQRv_Xg<y==>H!<C-x>H^*3N4`w0SrKOq8c8|@& zoPkdW^y=6wW~0=jkL9LCnUTP^nPWT-M>n16c_|oZ=G0-;Xh33M@|a{mx_Zyia6r?x z74Fz<gBOt(t|C8G!xgiQjCtpJbPti#u(%G8BO-MQ0hyZPTXzBuP!GmowyIk*F(Vsj zUX<|^9L=!J)JyCZinC%d%GF&>TyO;F#y!jI+krN{+_4R3x4J_=j^zLn+!1nZP)>PG zj!Xrba@TOV0N69<lFKDPdbU~dA;6R%uTjSUiC-@)9+A-OUekbDr2pveAG&M8zjPP% zpMB|^GF!HQHOsrV1<d69R+xm%<-W_?h5W9+=E(>EnS0_#(g8)s-W*`vCLiJ0KtgkQ zyS91*Uwm}$9psDGiF)<|QU#{tVvx3vQm+IgE*7o5LIS^OscaC?xrH)urvNW*q}QOk zI3fJnU4UH^-x;v>Y(BoRorJ*^EXvpo`~`2U<uI8(x__w+<!#RH;bAw@`4L)wC!vU? zMkMG-FH?2{zG&h80aHL`-^A~j#OZMDQw~s0Us#{Xj@o{4#r;4tovT*v!15>V6xvH& z{C*WjH~skBFHCY&vsfh$5I+9VC?Ek(Hm0%J<?CQ2hNEnE`y_0)V8YJPsE2A?b{pFV zuQ#8`hjNb2rfV1`0*g6CaY)~9SsMy8PkNUw4am4Om!iLjlnV|gpq!iXY$Apk9rLUe zvlai9xGwrb^m@Q71&}(>`WfpZ<X=3mB>`K$7_r0aldYU5@OeKempqiyTv-{0a)0#q z58?K2&T8#S>foRm+<cwic*EK6f$UVQmfI9F8Ijg_$-W<B8gA2r4?eCOPjmr!$&>Y- zd_dLsuqJc|^Tp2y-AOenXkr-1QrkY@(pfOZd?JQp;$m%h(F{hJ@e*U~7S4;!kI_An zH*YLN_qC7dnI8<;_2q3hgd3fqRd)l!O>&>L8Lpv%?9P=lfVrEZcUJ)>Xci2ho-Kw> z@vlgNlVcHkNCfnpAdO$3kG}EU;qn>4Gl$l$ISc%V^JD9a0l9W-wv>?YXr^fN)nh;t zYiG=J0OawrFQC4LX&Jk!p*)~{ej5!)bRA(Iu=77JIGql3U0HxR7vyLYCdL%#uv5{~ zvHK`F9=kz5(I<NrCjpNYFUxmBxx9U0A<p5{p))ehfK3CZYB8Krz7KqK0tCkiTwz~~ z3vDxAB;$S?y79`Zao>xnU;P6};J>nq$M<<^vg<6SkswcdjJ1G$JXGiPuHGuV;6LYW zfbV7!_9<yOc77(}oMjKD!X2$;sa7#IC{J8Spis}wncw43FYo(xXZU^CQstj<u%F1B zBd-j%!vn+Hch>2mA0S;$$wa>9`3-sSyX2U`I}xN>rq<mNKZ>wE1MgW`sK45UzZLb% zQfCrKu=|0{Wq;I9jC6B{_9W+9!6AN7@Aw5j6TyD3?vSsJs9$tM!v*bGQhIbJ?C0^g zzW<y(wnr(gZ8OR*pD3F_R@KxPtT@Q-J4A0y(?)*Ilck9$f97`!?vwI3@xgjW)U({{ zKs@?q<U1aFX=l3YMG(r5*sT+f_7qeW`=R`(`4QeE;Bf;VKDKvXaXF?Hh>RWP==T%f zhWVr1u*{RD*v?fe9h9+MYZ~IiLEdV1NR2!z?+15=d}LP*x9SZQo=*BexsA6#KU|4S z3nRB_g>U6f(-LZhPgGoEz^>VdrsM7?e^xHZUs%?X6x03*hdI$@3SS&w_LjBVosd9& zl)9`qU2zfe=`&Y-geti+9Ub;2p&nt-ZF}UuIUxu~`L#2m6G)ZZZh1wc30N<)Pa=ez zeR(Ihp^`5IJtL;ts{o%4aaK$skCe8^&DOKQdS}(jv381wq=ySZUNGxOuFz~-o9?(} z;h>+|l5{r*^*imjn@&Qze2oA8CP!4K?TGpXFShL#${u<6Dq}+|%76da!O9OWFG+)X z*3UkujB!qcnNQ6`dCAPqVyyQ_f|Co%uk*iENczYg@qV{ENC@)Ze|%=;<zvi(p*=IN zk5=A=@{`~Dqdcv0!k#8`yG=h`NeV^3hIBPspnie<_$bs*_zo7Kf99VPGe!M3qjd8~ z_<fmt%_g+J^X4>7lqVK1QbPU1#BL{}{#CCZ;&ey5eI?>i|FJdF(WqbNiUJ$|f>W*j zVCSkw^Gt(;$seS)S=Sn%9^WwS7_`4*`<Eag$lviTFhhQq+xS@UOX;bgF+SwPr^@T4 z?AN1y>nR$kD8D?>D;(ow#kxU#lqasV83{rEhZ{tYuqEk3)xKV~Uj!$UrAg2;*2c;c z+cE3ib}JIZsnZk}<Kg7j*m#srS=PT9`EPzY1f#si$qrV&@Nm`^<P#;$F2bkEL9@e_ z=8@qOpJol)#Kz(NrT$T%r#oxK$Yiv~$m4=GYtMcce-h*s>(}o_KWNtr-9QgB_IGYJ z%D?|cB(d^d;|x)rwtN^V>@nTe?mXKG{W-?;tUK_j<tDXx*xvCA!lThY&&o?|QJ(4C z6@&I`RD6rY{(9Fe#Qvfir3@0$|4P#hcaXsU_~MWbiS`rxm+EGqU`jdD3U*4hx?RC} zhmbcr+PV}1`t9tStswujx3%g2!9Fs6=DRkbS=6^j`G9oBU1j!8QQ~BHa02}3o%(Tr zf^#X+I9p4OrB&EPqOAY?>nQ(AyBk(r^v{FsdMBZL)y&cS20;4>+NX*D>5W54L4XAD zbbS^H;BkAF2GAaVRn<fRt#^WcimNtW_^_n`fXvBPI@^E`7oq1DB2N3vo;O1Y+R99z zoYVXHh&S-5SA`mufJ~-s7aK0L$r*OI$yHWt2RWB{S0rgDr#Ut@6wp*}^WH6hf)$gk zaQMMQXIuc~)U(EC{y>ZRTISgRa@}<;&>nj5<vr}){&@=>69C~sY#UB@^p3gvFuifI z#Kp`31(Z)Pdx^jL&H@t3>(=(OpPa(k)N@guxD`5x{^LH`RE4WR!Ge?f!ht4QJGWW_ z3f$U9M<Gpl3`hSn9do)-o>Gv0js9oqo<G3wBR(9eM0uWpeM=qErVZw|NdUjA#yIWk z!qy<i0V#t(>C=ElbK1o!07bL+zQ_h723l0=NLcOuJfDPg)27nXxQroINhIN_kdyGc zDiG;E`um5~*nfX#k9!i%swL5;zN8>P`ld@gx`B`&mx}|<JO7*ggpL_{Yk}R7{;i2B z!E(#b>li5Sium~+S0kzQcC~oz#w^m@Qwpf`)?Wb^(foi91Rs#-g6V~X^3k5b=#HRs zMI*5r#)gXNp`7X6sl#y9GxlAgK6?kOQ~nt)ZYjPcdxy^8#mN$Mr*4Px?<_!@@>`7o zfGG}po~8g2659DSB)Hi&JQVex9L9IWdKc%LiKPMR{YpEJ@>Q8%Qm}hqCkCd0gq)D= zt%>2c<6}rJ$|q^9b;$=5d}<BBu;I0BeCkDlJe4`*12hq&cXkQjkM&v}IJ`J#^^+rz zFR<MB6`voUK9yh$e1frLzfdLS%yUwMa)H(=0Xw|fXNm`4x#sN54fwl9&u1wj3U*YY zeZ-WGHdQDOTlD4cZ_mOtVkw}hraSA+0Zl)t{Rjgjh`?UVrd(d$EgX)L_D46Po%AR| z5BpD4y1@}=sXC%|gC8JFpRG7N=-}tGO@S7F;s1mkCL5wGV}VcDZCZ%+b_Iyo!!-$R z?O+JV5h-U3(SD*Uk$42{<IFi<6OHmUzOqJ$GoM_<bOZZAd0~IjpHn?H1HY^M<8~iF zYPF-43n2Ycy%g&wl;@3SXG7|GB&G#o#={Jp)##@db?66Cd?>jC<pQSZDImXHG|~=f zf%Jn+q?eDG<pe0dYN8q!_zX>or(-#jDe*!b#0K)3Fnz#RCO#Q}w4_uT`a$)=b3p{i zja8MAVdYz5=BNS9Q68bp+C`k5uM2X-Jk@R^Kx*i=9L{>QroWg5Vx`?ET(wht@$XnK zyx$js_H$Lj4r6`14P&e^9*BEZ{cC|1gt%_q49LmiTVng+ljDU!KvOz%m*H&fky=0A zA5btq=p3d6QQNU%2cS7cnuF*+xaZTs-t$mUOU1NFj4_QO0r^jAPNARqdio>59{S*4 z_sA1izWlq)9TM7E6n9Age5btdye8o5O?_p467a8r1GD#{95HUb9LNz|?U-yt^4vU( z2LVrw-Hep0-!L;4<@N*)sbl#c{ry9_`tNTo(JCP;BGKF!p9ummkG?o~6w?RcvA&57 zM>;o*MmoJG2*aD=&$)uLt>m^k>L3a98pFR2G;p#m3r0Cn^gB!^#1rAyL_q%2mpc0Z z%MPd0Z6xUbI=w9zu&ZwIm1Dps>fD{m5l5QcL_6daJ~rn;xvIeoVHMDH1bH6oqsNlD zp};3bdS{d&{_<aed>uVeH3MkqHtSk1AniWiydE%2(>=NsklOQl<O>p_`P24{BUoO0 zdMw)2r7Cm22KZEIU1|!@#ONnuT@kyCj^-mu>~3b;1ONS>|NTRE{cn6Ye#jWJn%>I9 zqen=H?#gfZm}Tk7<L@D#sWzIr8?bJ3#WP$K5OsG(V7d4(g_+o0wEW&QbTc6ya7h;M zqQUT^djNUC8#8#w@8S;NY~=S$``bAZ=t<!UFe+e&U7rouU3>SfOQ-=H_;NBh29Qu% z&7O8Nr%xiSmW2A-ROYdZ3*Y;SY<9ny*qsA%`~bfTJ5c^|o&SE2OAG$EuL0@RTbJQz zr}wQ(2?W0U9iM`|fV`V;CSaIwHf%_KNkXz4KB?k9$mxCEVSNzr*8Wg;9Hom_z87x? ztp7O6zYdV-8~Tjl(WEkoy9ZG4(zpw=7SA$T8~s9UmbjV)sHc`$#)dWg<bVG658X@s z%bi1F!ry9d>6n5U1id}m8}X9TOC6}5h<TLe3`pCqevQeIXunpYhDiLGeE-2IgBbn9 zH&44uBsBA_kp?C+(PpE!xM+|)%sku$D4&>qBMeaBa7F<~74h1%{5T16Pls;eWXr4_ z#eSOR5m5CTla*+%e)o2i>+q3x203^@G+`m)q(HHa$nTUZX4|nwG)o(3YQ9Z^2J-)Z zlTQg%!cTq_6{|OUz&8bjg^TZLB5nBes|kK<@bOLDBIIirti<6!{8#e0<L1B95A1)d z|9@K^+Veiw2eTVN6=^15`z$`j$l<3%Nn&a$*iI|46$bdtM#!wGD(HtV6(2EP2)*~U z*&u)PVf$)V<c}+XU3)-2VwDPRVcL7V^)HM6M1E#wRx;|BHG71c&mEnA^sWo)iQ91B z6ZLPLzk=2OAp9z;|BvM1U3~i8U^2d3miYQ=4&HK_wB~@J5cuIQTk(@Tt9zfE@r=d1 z!v48dNE3^!b5VZO;GANV-{&^Y80BA7gtDL1%}MCQP3rQFM>wE--pS2#>+pB?C+%z? z;dd`OXPTqD#EzXPXZ1<)H0r<o{Cc}3w3A=(Gd`7s_8O#CV|o+}_N$v>`_o3&YXiTo zNX;1+Pn?#Qoyh-l`NCB@K@V{#oWk@aQ0Hth10-b2Ef)d5?wX*T)z4EPk<TyqGkL)i zlLe8Whj_Nu(--wdM9elpT;g?09Z>%Lg9TQAoU*xlwE+KA-ma>v6K7?H?0CnJ5T@5N ze6|8jxtj1p0lUJJ6wrR!wnAVIi>`aizI65gt;f5hf0u-MQ|3+O0<9`vHwqVDrq%1# zp&vv`SBB#!ZHop?rU>xX=CR*mr9$Xm^$^E3*gpw(U}FgAzvL=wRnS9B+%2u}$MSHC zRl)N`C<*=E;W7$8nX9oMT|$B$tDEOG=%N0^T)%0k-^hDN5$q!NL};30oSi+oYzxX+ zNXf8ye*K0=Dk!J7D9{k~9myD>@c%%b8L5@H7wWsc%6~={#PM2D=yzQFQ}SvN!Dyep zb3CqsIP(&QV|@OpJa<I&glcH-%N14E_k;d#V?r*vq5kT{p20Y7c8-$SjPy~r;ds&V zr}9H{?T0!+zuiQ8-A>TgthZPL=TYt$=kabNv`69Cet)3FR~${mdIXQhj4nlcx^-v) z$a}?U>EZlE%{pUa4e~E%_`*1ccJ4A#UJpocjdI;FKNP=s=ZgI@A@d-{JyEGknS*>y zeXhi6r1$l$Tl~lJ5C{AzX8srttAn58C!4pAXl~RI0zF0`W`h2Acef@{Z}Y+!_UzdI ztMX9K$FLviXwUcA8xo=Zyt3n(*zdGlmxLAiVcWr=O=!>kDL2^p{ma6~=x?~napaKz zM^+zX^P3$#UJGdYy7W6-4-f)REei4fGkI>h#|2!D5`xTt0<f2ul4oOsH22sMj7O&T z3cHOKp=c|m3-q7LGm_(0<NApZ`JLWH0>0N8kNJ)0ulC1r#i+UJIUt?9MUSoTkL2MI z$bwGEL=qm&ybXI7&pw)3Qm=)NYWiB{8)LlwEV5gVadhrk?K*s9@<;OUXhrf|c>z9Z zTC<p6fcj^C9I1(V#+Ml4x1BZLJ}50k{abI;t6{lFUVS$!@29?7c+r2(_KXM;<>UFg zOj-H1<I*VqCb&mcIC@Ln%cY}s0g97~QG8N1>c*-~pBD%tZ!Fnr9<WBpS-8;P#62mY zrIpqC?eS}1>v@~wOm9BQZwg($o8*pfB$_qE$N~KlrJAQRgv_Z^ua?gmMuL3Rso#o1 z^2Ic%_vRJk%_UJXk5`uoO~1(m8ICstEPqNgx(eYNQ^_Yo>>10imPs251)nZ8@di(l zFLU0WU)ys6<&R#rv_<*0wZnaI)7=0u%1H=EEl-v|W+wbzzKI{l+eI3deCmz<D_L0b zNq>oKpblcrCqEn9RCnrwtWDV7`xa4}|MZgU)PK2L;EsTx`18MGCO?}Y%x-PlJHHIV zsxE%A1zx1fe_AD51jsB~d9DPIw=U)y@=beWT^r$Qs^|H=%a=)b8Fk(OgVSiPb9U2d z!1RgEZ*Y44oZ_S}0PJzC%fo5<;GwJ&jX;ar_O3|+<eZn&X6t#cN@W49H?S-v5|DcC z?*f-0wEa8(jEg|~3^^X+1OA#F9aT)i?{v=j;zjiePYYYTI;HKlXVwG%q~D6TI-qT) zE-FR+zp5$rfE)35tD0~*TYFA(IxZubn>!bEk#N=e^?X7B(DDI$*|!{H_0vBIf!0>t zDus6TsE=WASpXk@@H|LZR@_<Ckc#xU8`_x8>Wc10<1&Y?v6A!ygxy;A&c$*<F$<O* zunS$bJ3EYBCg*EFIk(WM-VKoPcstP_kiIT)%n=aoa_+%pA9eY0I2X(PE@;~VE#Fx< z4*kQ`sA`A@K4EZ>#Ay(2@+n&*PIfOc20p(@O(h6uy>++UlYpkL?cB+x*I_$8;#DzR zPyH|hJ~zzKBL?Z9881+er16wFDZnSxH~C?Dq5F;w;&KdbgM3B%Ik(4-X47-^o26){ zNba|*Cm=ONZ1YYi=h?Z;3jrG5@R7!}&MVLHK|ZDOIS|tm?H0V!2+Bp0`C@5+f{EMW zod5~qXdwENiD>V`Wg6#`T7@^zUBtQ8AV6-^)_2%nf|TY_=r8!VVl=jo5NkY+{Z1b_ z!=3~|6;wS%J(O*9dNRr_xi02}a>V+KG(cvRK{Lj$$SPwS_9Jt9=O!%YD-I5?CQ)Ck z)@H;%`um5g_y7K08pnQa4c<&4y2PV#*zk&<9mxhd<(Ymv4g-6s<CpR3n=}2|`dmQ4 zoj`(}jZ9T6n@DgUpQpVG_~KKZEks<QCm)Z)kZC`%A{|im?U&zY0Ey$y=}$;_VxXS< zr2**sbGlYs28@Yo)olkHGEQZO6R~K|<2aCmlNCl|y-c(HP3)!~t-DtFfP&<_q8)(r zeY;8wYr@3w2#rWxR?b4?Ps~~(gz`GCd>lUXR>MTJ1Ku7UvcqzvL{R`DA;?;U{FC;c z*i9V6WwQ!_&viR%jqN8~eb`46DEUAC`-krOH(%Vrz&;Lltl{sKI^0=@eBxh#G%Ysy zEK75wGg*9gybAbCc**j1Fp~+8YPAD1O?UmZGY;@(>8){?4f&rNSMLMtibzr7A-?*q z&_==-F4B5+5zs?kh&N5>6E0UOI-p#$xNe{cu<6Vk_C$C_q554d$cb{k+D8Kl#@E~8 z&6d9W{Ywr5(v|jyw*jB{6(U**^ku)B<|2|18HpX#?*n?|tPv6zPPRX~O@tsPw)sYQ z3(#S+ul>#kr0>6pNCp1j4XF$aC;HKpP;<m%2?kz({lfQ_n0;u%^Pme}oSaMRRRr2J z!;KRNlhkK#JHi#@crGa$BT(+3);-*3#!GD*Y{3hYePgE#W`kR!H=E_cB6ZotdEJ^I zr#eCISP~#p>|+x_!lLoem8aggh-}?HDHP;a7%flE2mbBE8d=OrqML_i`+z+EzS=oH z=<$+Xt?dqcBJsOhEYdB*a&h)xu1M%%b{qK0vCRZ|y7!q+FzOxF;Ia+nM>Xul_g17d z&TPl9qNVjR@FJIZ*#BJ*kg-&fjKg-O`qLXo;EQJ6jR8B9T?;rEzKq^V_To{braYGo zTZ_Jf*xs7uZIexf>*}Z7osi-p+}Cr-As=4zdfjd^o>SpUhBz&mX2~-ZO027wdt@9y zf?Qxkr!%>)C-q9AmA^2pxQBSzWKPykTX(22UkCV?Mjjp&fbxE#7ZKz@yUFW&;qO}c zqdn<EsvbhQ2OqAVGglRANR`L_aGWIE^xCDYrYl~kq&K4M;mtI1Jt)eVHd3e;uT<AY z`wQ(`pYB)QyGY3Un*Tubaxr<%#7BjD#*Zx5km9eaHYK5+X=xRdP*bX0!_2`%xO@9o z+1CTwDDUuLoD2!=SS-6)TX=&rQBTRiiQEJX9(TDA+NGlW&I0Oxviq_I4ed^?7%_dd z5ZHGw=|z%DLY(Hd6uM$Ni&HnuMSrAs$xuS5pA&q?7whHhxz0uT**nuhu%0U$`hw6O z3NckS!iH(<6jGlp#&Yq)`*@^mh?C>lvkpibemowE@)M_Khoih%g_jN5bH{Y89oiFU zn3yO8`L6}Xwh8N}6`h_gmxA^L`=4_pIUsoW*nM@BZ_jf;`7Pf}Y4msbxS7r<KdPxY zk(JL;<YK+L58KyZJ)Wl3+pvGG$-$!o;1@<6GiB{RP6l9mqL0Fto99ek+uUt%fp)%@ zn49fP!tW?W+pTDCyePl}`nhPszSvZV1B;6$rF@jr+jGbp?YDkA&m8TUwjwANu-&|y zZ-V}6><jfo{nmkvAsA17D@X^7qn#?xy|8{+WrrPX{LJ~TkNQuGb|#_z!%<hVg&^1M z_skpRrLWy^7cL3;@NR}n4z_o~y{)do-qb6a8^igie_nr34BB~*AB=IBTDpIB6t<hV zA!`Nguo+ak6O8uEpiMoHp1(eKC+V~##C}?Jun>N?;_3X2sQ=@z^c<|$u(Njw+IQ+w z5|2D*QX4T#i4XX?-TLN4Y{v}i;BCTZThwc2_d5xjB0E;iEZBg2i(M9G!q%r-UW9!L zz;@N25|c*zy{Bn83*{%?5T!l*t&Z|aGdE!W9F?n9B){cvU(~wV2JLYgu|W#k@jB%u zCG0p6De<*wi;y$%26ewyoVCaGlRr7ylfP%rD|@il>ACQs3dZmIIj2GZFO}qKN1}g> z1_HK2Ke(*TTp0`PuH97Ty$AcVex#>4wlA>xqaXH{!@g5HvA-H7PS-^Jh6XnR(LWDH z?N7$`wC4NTp#59MS308p$;07W45;tv{x3=>AF=gkAjaEiUZo4RXNlTFRn)&uOq0zg zlh6DW%*r1>qk{fdsV(uw{_4~J9gKMMx0;p^?9%U>Wl4hmBN|6`K))8}2R2wiy*Zbr zukpe59B%m<Ndmv%^#f%R$MJdi@)zADc4jT&fzO@;>cCz+)6f9xf93Rn<-2QsQTV5e zk^lb12jXm$t0kn_gq7N>N+JC+>pj=}eJ#w~4Ff*?R3Embg0wuc^Z`y2#A5ljtsr+X zU|u+`I_NL%d@k~d<@<GjW_EsPz~2eF_J+A2pLu=W1Ca6D>tThoQ-i4{A`#q*dWeIJ ztpgwxSUB1OP(Cik30GI=)@QFg3N$}zPApCXL{t7Sv_t$=*n&bpuJhftC_q|x^DQoF zdE3*|ak?iyDNJ5Q0t%k$IRes`E(~}ft#`-{r)?u=%BmFh;Za`LXNb!P;?~=!Xdxg| zm~{lOJ&BB{02FVF9mK`8yo&jgB*0(Y+VZ%%V(RB7vhAWf!ttIJk?_`hG4SamyN#xR zR81}s2zX2GY+fFe)0?bP*>;Ma$|Ik#TflBs%NRd6$p!j};mmzdpD3teyU9(E=blN% zeIjU|$r=ZGpu9GEHWwGws&V^I;>CoWOWTD8u>Zx3NgkJg?)m!tO$o?5o6B772KhMk zmYf>kA3kF4ix+vc-Q}x00OQq8Z^Cxb%AAAfXL@qb+X_Hxm{*4@AV)WFp$(uYq5e?3 z5EhHmU!<}2hUQ(u{v-I8yz`)3zBl(t0?>6FcSl?f&@0#5(OCZ0DOL{3iHVN`Fb;^O z%oYBKg~La)?dQLoYz{Q%-cnWcTdqDWj~5Zlk4YK0>>=b<&j<!u^g&M_?XCN~eVZ%t z6UP~v0Ds_h#Wsu+#+Y-!2Kij)kIsO!&xo@Iz^CGVl&uD&xfPz_z&CPUbqw`!0yPXA zfhLaGTRQ?xl?9G51Ee)3n=AxAE%h!F>$#N>n41WEk;SWFPCyewOBKwpyfH8t+sXOb zseu=5gnlX){nB#4i-UfmJ?GEWL%zc|_B2y^bZ9=t2UE62MIZUs8?WQ)z4c?;W?b$O zcP`H5AkG=vkp_JFb<PtjM3sr}Nk9?RosX;kT91m481Ev(OG>Q&mC9iHg0LQO_%C)@ zJA8gM+DT_m-mx3_jVoM!l>*HKr#7<oZ&5o_0DM|;D*IZwpx5}yc9egx<7X)F!>q4A z%S3s5-!@-BqIs!*2Ji`naT*Rty*mi^RDk}YzkgW$|1W$Xync&%1DMAtCMwGjFFuRJ zVK!FonGQBz-lgNjen9@=MIE@>Bs}fMR*-;txxsv-hfZIa1sGN{YZ0auMr5Fi!<^XU zU!+5VdwyFx9|vTFH;OL<3e5aFB4Bt)-v55>49JPk(=ccS^hi_`;jpB>+>>}r!gtB{ z`n~S}&Dox3eHi6DCs*SzkuA?%%m8)~TW?ka5_QAIqx~*6PhwGyxFsmYu;9<n2`ffi z)+1dC<;0`Wg#m~uzwU$rf5MZ?EAgtLuD$&zh5?<`qv!{8msRqQ9H6P8hkNnboH#F; zjdBC+1uvq3rmF)vRzUM4w;W9XS}^YHR~q?ZXIEp`LHspgIMK5={(bGwqzsKh`b+)> zJ1k#$Q5{$J^r`eGE=Uun*;7|Ve~lP{?IY4=^|%9{y4knM56eA9JXQo`wAJk}%pm_% zg(0>#|NVYbeaWFnA^7Lig$-yoGjpyW8ED4+QA`LR>~}T682EIA?R~V1NDtfOkL69f zy4b6on9&b;SiZH=$Op^+=<grG^S|F+ig1l|z|lWYeOG~PzWY<}EnqJ3_@F~DntP&l zp9LV}D^YEWe9NG&d<ZZ9{O2;KfM!kyr9pOwy+9WyU>Mxey^~W3G-s=K$!0|Du}>k~ z=>_sufpt(m;5yBr0{HR;!r7HT%Oq6u%YoM0pbIbI!$qUpJ=?1!Z2c^{QF8<6u3!1m zh^x*|XzC`R+p-6SX96wNRp^exWgsNs=oP>rVZnP0ztuN-lIwteuHTY+3uyW4ZX<BG z-HHig@0c?`t%nMLe!Xy<6(7*jVdWtleix^!eLqQpy_RC{*n1<_Q&pmXPj}x@#BMY) zN*;#w5*Pa%q!Fu^%*3oc;BjzeI+W8hr^#yo5)Nm+WB9naJ}E-|wD)io3}<5AVOz|K zVNY{r@u8g2tQR4le&KNwv-MsNW-c3^73J)$K*Ijv)*z7MG(BE{`UI(=`!M_|#v>2g zNy9fsYY<=0hKoheU%Q?T7J^>Msu?&tQOk4PO#tb4zRyv=z_UTzhJ^A`cWHA#V&c(t zs9&_>c48=$Q~tVb$ftw6e&g^lH7~g01T-^#B*6~5r4xBr50~NX#db5%tEZa+%^BIH zi^D-Z-|RKsv!V73QwRbivVS}`05l3NRxTldzuNPc9w1S${uSDle!o`^<*3HAz2U%T z9_h`+{;ylR!U<{9Naq@7pcy;$G?XhClh=Xui+6QArvZruX-h2U>{MiM_NDdpT+NXG z-ugAln{Kg+Mtf-aStDGL&q&PL0Z899bU|dyIJbNOFBTtuTrY%OODYELoPs#@u=DY~ z4Sb?xLpa8zi*GginqI>9#or5o=A4fHnFZM7w^#w=;n+`G_Mj5Di!;F!%hw%mjzc`5 zABBD=!X(dAB((G6lmqOnIP>xw7vwYVy)eBI3!dv?TA=6u_=V$zm@+n*2go(8d5itd z{G7Ec5fBpUQ5wr*4=zW&bjh{f)=2-+-#?_2asTq9K=hw|K}zf&7X#)Bx{RCC068&N zh9#nzGUXrrfu?g@8__*X<gVcub_5l6tPHR<n?Lsolv6Tq>?47;A87t~9I*EIwi!nO z!*qwVaM;3^<dd*_MSWs$dLigbUmp+wG~8E^_X3*A*ghA3PY6G))<z_z4`4Wb<NUV6 z^u+&V)`#V^?2qNTfbt7{C*$nH@!NHt{T;ok2;I8=u@rY73H2m!iXDL-sQ%%O_2`{+ zO2A<a`wB?oBA)KK>HU%<nBR))QttsBoObaJhSMs|rQxRm=U$k15@+Eqvr+GA0SP4y zwf!W}56k57qJtZI&IZGR<D@kg!<p-SKs6tb{>Xd}00ax<<w=ki%le6Fl@Ju#uf}rj z0v|RkEa&b-JE=2O;{pNy?C&4KVadOQgDGq_mYR}cLvB!q!yiwz?LMgsd#h^u41SY` zTWd9GcIpbahw7C4#5MSQ>TuM1MK}~P<@l-AFgz@BeXUh8$hU`#T+c&!S=Zw{lwa^d zCQR6O|Grh?0Uz9J)mkFg77vwdO6}PJM@y$n+^fmHUt{(`)mNATe|OJmp}eyABrg7b zxpaUp%IT`YEg?YP#SN}-%h9SkKQaLKdJQ!!)<JuhPtr5Pa%I)F0NA_r(e>?x+4$UT zn#fTdZrxd}IoTSDd%ya1%7tUQa(Aw?K)u1I7X+i-k9odvSl_y|I}T|7+mK!_IMnj~ z{f$>V(B9qGWHf{z*PK$Hi26NOC*+}gQHG>FE3XXOVu0Mcn%;Q!cW!YqLZB}PEsa1s z+I(!Zus&l|Yu5ga*1<Yh?$Gfy0e`n<-CbkUcQ9#;BMEL8BYNeJ{t7t|tc&g4x}nt) z_0ApI&&77UjX1a(?9r~hG2Q^_f%YO-Y}e}o9{apM?bqO1{QaX(k6p0-Q?pM_NB#9l z%jM8tZSiG#B+#>`$yuTO%`X*nSi48?WzerD7B9i}YS)gCFaf>oBMj`4giyb}Kx`uk z@^VEF{IK3%>2?8F?~@5q=J@-iJgsfm4;K#n3}Nk8dAX7GkMsdg*6!So0a&k@gMlj2 z5|aWQQ9kUG!y3e&(`IbP_GZmL<&OU8-q)Fc_P*LO(iHVs*T$QZ(5x)0wp=0neq?@? zIreY&UYJ!te)m121hnT?(@X~#E~3IaX67)Qh*&3fu_yQ?ls5u;KxX4r9u6l)mrWz| zrQv;;E*Qn+*;r1*Qi^(jf>tdt<cm^+D||?l-*!L?Xj<PPACYK3<~ADml)!9@6VOyt zP^c0hWAW?nYfQ?}UmMDaV|=N3h}Wd<FNJbixU>Nmv!+6sU3l$C)#eRt1B6ek8F%2* zFRtA;0eWD4RSl*O=E0RfHvMgweNr80+UE0d3J|_Ncx_4o=8WxD06MJi_!^vDMBA^- z4+g%V&s(4awD``B6M=vP-^LGTS)x*C&e~C6S*Z%NIL~Vnrg3KYK&>*+1KRb`xOyN& z4OzH|rbjpPHzB(3ZN~PA3y!w2X@0oe6E)=b=x)b)h_EjPaY&zE;bsUpkiP2~+Am($ z;)Sytov5~ZIr4L#4Y2+ndUqrgX!`taTN6OasQZK_pg2#=7%zfMcl89A0fzl`V|Xm^ ziIm_NO!u_f=4IF~>m73C(*Orzs@#)-f9s0ISbyZ7zOJ1GG$C%i8q>PGZI2GNpOf^C zK)*A;CqiBU|4d30Xab+A?Ts)6Bt*RXXfIqtx3K&P%@?uV;yqzn$-uAc|8WfcJutF! z1Fjkgj#txWK&s>B4NE}r5h}45CmjC8vuKCum|_j=*MTQiUaa4_H)1dz>7`Ds8v(_& z&QRVY$eWsO!s)=Yr0?-oK*plOVH@xX@z2UmfXvA0EAV%saHn!RK=B^?Ip}|Zx}*sG zK9HLm97clt*vp~pH1WoN%m$!2AI{j|wREkCWqTf=e1&@oPIpw%_N~r<q7yfFl#uYd z=H{0;EsCq%dA$R$>KlBl4}AIQj%&F<(}U)+Y<!NKt%Y_Iizbe7L;i;B`XE5=2kE$6 z5>{LJJui7ci)KZ*veT{o&oo>m(zyj*o`AR96y<6FXOHKGo+E*OVBedyfYv@E7M%h< zqf_O>#)(eIZ+D<gmuLv00cp>A!<`@}e)nYlcA&W<FOyg=5xeRFE<1=vWu0jMVpr|s zM}S@tqB<x9TCnm(l`SCWzRn8t3#}McdI*qs*>T7LurPB$ZyS`~)!MeY7qEF!Uuzj) zkg`#86QEScn8s%${QmV+@4Z07U&&ISVIb{X$>M`y$}GRBa3)Lh&U|Md4JJk``lnl^ z|7J^jsOhmfyw<8nu4rzB*K6HA%|FwM^gERkt*~`^XXIA)>~TVJS0h`^qcR704R*xj z;^-`REmm>I<W?J?Kl<&94aL`DkL+9)j^>@8(_@b)N)Hb~`QM`H`LNY`x|~uE(u(c+ zXSV>|x!txZ9_0+R4P6oG4p}>R?f2Q3=kJ|`TsJqJ16M|Y-dT^-g<d4kZlc%U;0Y*k ztEg0;1f+QIB{P(VNvL_F{=NZI-0J<K+>fLnwC7Cd5qp%gxVZqsk05%rcYxfFx*4~# z@bzMyU6V%uCh3I-(P$6HP|1VUtE(->;^qoxA@G?P&9(UY@v+NOHnR3tIB$b`3uD`N zTA*A(d;MC}4_|xOpglwDnp{}<Qd<qQBXo53My&5wbO{b8LTAw)3q-h^sfK*fV*}jE zUQx<vku}mOb^%7Hf7p~nGt|E#`x*{AsG#$cx|`c*^~SBSs9(wD+$N!in_J)X)r+y7 zk~{-8ObyRAvhC%PD=~fbT~Db?fZx+_Y?m~kM}woJ3-IM5htFq+`$PB5n0{bq3Ca)n zef~CQGm*Bnl9~&&$A?3^(JvEX(r(d!u!wZN#l}NUFoq?a(mQhzAg>|S71L*@+Y<Jh zF3J4-(oo<t{^v&+0-s2!unGixr<69G54iW<ku5lEkNTIX?gMNqe>d+i33^7pl)^Ls zTPo<z0_@A`4#srITb*fz>8h)J>wEOego9CsQvivR)kW+up*FHlRNu*KdgXu|{aQ0v z4$75=<;0>L6Qm=xQUN(Lzsqk1gqvOxIGo;HG^#8Bq*Uw|D*#`~c7J#nVA9g64OxJk zc(oEMK!Uq}DcDPg^5^W{F9iFEa}>5msj#O$8p_)a6LTv8nHTZL*!FU5uG&KR!bYxV zGVpokXW1Ph<nNwYi0$b2k9d!1Sfr{SjB*6M;=LNkQSI|n(GDi{wvRgSmDD5-`vcu? z_S_KVi8-g@#1Om0m+k_4QWko1YoT2A1($;tgKkR-d1-(<ZWL44E)g%}Gp2KbSrBGU z!Zo35D7#yiXr#kgys9bKI+FGU{PcRHXeXeEQJ7~52p{E{tCNrh{mZx60CJX>?ll5b zI@x&_{rPFqYqxkI*gMH>q9M?b=kDqNiq~J!HUp$BDn_q_a-~A~7W8Mo>+M%jK+}%{ zj1>UkTRnEUp&i6I5((uZnb<mG;CqxsOS%Ky|E6Ot#xqm;sCpA1oHVZE0NAp8Oh720 zs9i@9$AyQc^dw&r-Y+r;C<p?iM@+Y7^*>zFfYWAV&hI9Se<i<q**KsDY5BhBfBM$J zTt7gfKT`1%=9P(c!$Y8aDEyEK+S!$I?KO@Q>a&d=JN`z#GFL)*--aD4fG=>{B~}hc zAiNJdk#_-TQ^(IXX!l&XiUmhNuKdI4DaQbfOg;|A0@6|@ObX!E9FNWABvjicl@S9d z(MnjO-}x&~@5lk<z1-vI0XU6V`lSzcB>7q&aQr&Z5{U|AAw2q+XDt2d4A8e4n`a0B zt7hmPOafdaBfs(n$d4+TA9)+--0%@GMGzkiiNE?zlR$UAx?T!=fwh{MHy}?pBcB0& zmDi`8sX%k(6!&EV&X{k|eE`toK-rl}K>2;`>uJD0`}>EhL5Y948f5;n*9heu9rH+7 zB@JF}#$my^uPlSBfi6RWJvoK=9`b1&lneBCu{+4a5hw>SY?&FsmN+fYHCk!*AWs-r zC1Y3#q?AYDG{g+VK4Zh*^w`r<kc*stIt$!R@Xa}Jbp_o#D|9&yM=nQuPcYC-e{8W6 zATjVM=Nj<Eo-V1Z1A2XKu|+=M)wzn9`vLDwy(rZTm}35Pc`hJqkRp*u!fL5IsT#wA zxs;}41?bU!`Xjc3e_=^GI~}D2evt#3C`jOCk#Mh((;|W0&Dq9zg2T3HSBbzMP+n}- z91K5VfA1!CxQi6KtAW-#?|uaJ3pD*+V_55TH{U?Js6xv&H$bBA#F;QeB3BOmz-=|E zz;K|me2$rszz*w>#xy{jIO~~&XzzCgS2IL`;@J|wo^u)u+DlpUC`Z7nmZ#?K0TkbJ zx!noM1?r17*#S-1{AkGn<jp)4hr^Ysw0(xdi~HXCF!o2&^p$y0fb#3?e>wsZ{3S&K zzyz%ZX$)IV<dd=Nuz#_EgMJq*^8cBPa<_Pua}dAsR$c~Fm+3aHg7QA8_3WqIdwrEZ zlmQ(vp)RTsFd!+A%LgQ^2Z!oOSj0w+9S3QY=<lB9gsVU1sr?kBbMNn*3d4tx&DdPa z`ll<)A(n)A8+&gP4pY^Xu?t{2AOv?md2Io--uZaVZYbw095~OWl{!hCIG~w|MOKMW zPNf;>8vu$PK9Mm4H1%$`!1fc|H%dl`|A)0dkE^MB-@x&eF-k%d!a+z#BBHc+b18M2 z)8sVIoJP&<=8|YuNga_gL@H$JoQOimRAvVubLPz7``&(kJg?X9kLUY*-rxSYy6$!D zwbyX3Vehl{TAVuy7(Z7}scJKXlco26F9ptD{C-F@plIO|WjnCbS7j<4fRmq2x|n`? zu-$hof5Eht*VxVV-Hf<~*M}!a@51yeymU=Re}O~d_m@zhU%w>|n?F5J#coI>8yd-O z)=6KK9*6<E=-U8%?F9P&^!G2#{eS<Vo=*IzkJSS^I}EO&z&%fYszNS#M}80%uR>YI zc+7VFgNK9AJ<bQ2^wtBCI~QMeQs72zlFrz>b-Wiv9s;bbHaoo!P;Xt?NNkQ>>-`pE zbrSLMYIJj2GFHPDkTDWi8-ktdNCL5&VK(oOSOh3?ERtfInP}8HPu8yYl17|Zbi$2- zc;+(YYJj2+$r$83qsTLCFPhu+Fa@}pd37=y-`IKwGvIW_n>p-tdyZRb09TZ-VZX#C zJn3B(z&VrG1_lWsf6fJ#n9eTimW7yq-Y{u@XvR#&gqwbiz{%Xch^rKgl8uK0D}WCR zdwQq=@bG<d3G>~V@X!XkGx4$IYn>_3=gxew2{>7>$;2MG-;9Oqr_Ri-W&QrZ``0Nr zc>{Jmnzt(ukZ=T|Xd$Fmaal5^r~ioZrU0;uECWm<0lRkW7GihES+m{^n?Ip@*=^X! zo^3oDxWIP$F>LPjKAY1}M@}!*qkuCzC!`nxk}mIgINKxDk3z8g9l3E+_<;SV+uvZf zA?nfTb_Eo?>?~xvJI>dbG~i^<kJpQZ&|D-mliUG29t@g}%|Wan_+bT@dq><7L4i&* z$N=LLXS``#2X?+h)d1@0%nxo{<V`2%Vttakx=KD2Ac?tv{y_@YYj^_hI(lvU20%u< z#3>GtGkM%dZ^Rv`d$5_pw6l>fgxlDruHpeQ;^*CKz%GiqJJ}jIv6}D={fQSs?qPRK z>O*6zFuXD41U5TnbaquNV5jzmj9lbH+E261^=#=<^h?4M4k`gV`Fv#!rk}H>X*WJ> z`XzQJW?^`cHv2M%IV$hV`kQ3Tm`1SEI-4EPZ)lU_wb(6ad)ltp4k(VP^v3imf09^( z?T9dDn~@WL>3j=7-b4%bRN~wxd`tHJeKAd}zqWe(#&RX}A-hc!z=fH1m|yy1lq7aj zyjSar69JhaJF8p}S2Q_b_Xm43hvZX`?$APW>{iuyQ#pv@Z=>7TZs*Z+XJ6m~SIgh% zUod|j&iav|(P$Ik^l!spE(PY4fpd!xYwj;th4`QT{-wJb|Bu;8|Nm`v@@&#)HajHn z4O=aut9IDDneH=UtTx)s^k)blS*AL6J>bt){fkw|Y0IxiDR9@piK3l=3CgE?u$xk9 zs~B4e*mZQKR65}0>%TSX0pY-%9o2yA?%oo1Q7~v&Xtf^%{%aUd;SgX+W5s){Zst{@ zFE$@yl60vRkbb$aO$2s&Sf~Kqi}zEYiEc+8*xNcIuF}o4r2vWDEq6q5aSV17yx=Bl z2D#!(TW!RqkO3ZIuKFq^2!{>xCPyLfimt=%iBTJ5gUyB%znE@{;dFBMEab#^%vj_i zvNs0#_eJb^V|46+U7^S`QrUGmr2Oa@3*_Pg1-ALn?(ne=a2J(P=w75}O@<5du0<a> zh`hrbc|^IT&1M)bwp)VTCmFxUcO!5{W0V)VwFtHXXZf!sGH$4g`$$-%?W3?8BN20L zlflltp<9jlAcAeXve908ZM!jWW}Az;2ij>foeZ#xqDJ>(`G`d0w>tnAuX?3ohZyDO zjqWRIT)Y>%86wb)!{#D-`TbH3aQdj8FP0zIM=ylcO}lv60qi9j`Tn_p@U_Uv{SZzs zekd#e-m8+5j&_bo@K)A;1S`*KqRw+J#|w1u5!^7g+25O6i73vW(3b>u(UL0^c6)@j zu`>l!>-?mb4dG<d%hug!uP}E@13r+ek&O8khutoZ0hE1}GY^N;pw+IC0zh(h?bb^a z+~#TG_Z`5!KR@+p1YCOXZ5jIGlFScRw*t!M#cofdKp!<~#!lcQ?({<RgRV*YpKSzW z<Rjv--nm*AKVbL8TaciS-4hw`ZP|h0;}e%*JoLP^3)$vBJ75A1JEFd6<~D##o!^sG zK=?XCB9#IZJY|2;C6n*&#(LtW*Z1aQcyU=T=J&t-{Y&?t{EzN|`Fm~-QSg`xw_z41 zA76&I>sYEGg}2Y|nm8R%>DUN$#7@Iw69JiT7qT&%m&2zOl>z43wVgzFzP7W|v>L+| z3mdTaQ=8{z!Mb1JN(<~o+qOUEVfHvD6?XXmmkw|aKz9(ui{nGUUSHZ)fuo||qxn}a zQ-smBEe&l1F4;6?JX9;AbZjZz3i=5<(hJ%ET^`ifX8|tpUykc|x*W4tWj_L5A$xio zx*M-o;#wXB)$raz0@BI2-gB~R06Qry?#l)H@9gepSWUHsN$gJnjI1Yo@&@TKa5lpH zH2*rd6y21Osl3F1UG~`NZfxG1Yk?y$e~ye*8df7?*1~?$$elRc*B!!1ra}f*+sX68 zhE!m<<jY~$oNeRf2TH-tgiku}0LV4Hb;pZct?U0x7gwvEU)dQZgvH+`m7~|d>R5xP z*8M>kp7GVi8&{jo3{yA7Rj4xcQL7PC9bq>-SiSpL6rv%V6InI#C=-UORR?Q7VX^$- zc|##wKgRTrF2vX7Rkbl2S7FaFx3q@q4RS`k4n%!y`3YQw9mE_R9gg=KbLp5K>f5Y! zb5OrkdYuETvaNbn7Z8HsJ!bdTqn>Q6j-c@V4GR~ezW2tdWL(v3Fvi3k)6-o`_!z$V zw4NL4f7-Tgfcv&>&Z;$pRkvBDGWwC2-{vK&oiYDq^auyk%k0@`i>roj?HpLgu1c28 zkYM$v%H!EpzQUU6$Yb<w+p*>I;It3w75wJ<Vf-?)h8rOEJ~m3mcwN=uKnlpO>#@_m zs2{$FX7vN(7*7iBuY4fH9?LQE80@MDI4Jwl3P_jhn4xn*gb<HO#zP-m4Lp6dl`{p4 zqz%S?T!8sBmku`(a^WA7+b>%VjXR;&ZR7!~jbGn09c_f^;mSb$LUG%#ULJz0iw|!Q zmLUI4kK_wMZ*9IR7|O}x#{Q2{kgtN_anoZk-u)44Jy4$}eX9WTKi{vyO9=N}m2=Vz z_2HUnaj4(0p*I%O?V(n@0?TFGsF%U)s^-Nz#zVRdri@#_!~8l(>8;0jHiwLK#$5>) z969F#yEVE;Pj%i1yHK9I(;`}hyD@sb|By(*Zifag5%Bc@1n>?9!>*Pmm$n$K$6Yc* zb39`3dJX9X6z+zochehpmFzLl_KU_{3=?Lq!d)W|9<ujgAU?~BlHUuF4|A^e7Z&z( z#XgCP0pxxuunxvu6R%BvWdyre26h>r5C|t<xPEzxc?<A;Q|`g1p3$RDncH4(6vA$t zT90RhA%;vVU?|uX@%-+M-mpuj<I-3o0pw$cb$NgTTqosVq?L>Dt=eLdhPvC^W;^0; zjtkyTamHO2FYYX~!~B+o4>1&em~=rZF;jtpT^wyL&0B-{eXT?DaJR_A9fx(WeAYP6 z+z9D-dQ&QL8kWyJ&ygXRU%3UXF{syW?(k;IM|&O*%jbjHmN?AU$gAfyG2Ve^8@w?8 zdpx^?Q14>%*o%sR?J-@)t`Gvg*m80e>dv~2;llO$>;3&4g;5aR9;Qff-SqcdyS0{r zaxQx|cQvLzdlUTh3Hr3?DXy3gSz)goULR&M(Hi3qiiBmdaDVIAVNOsV^_?T(w*t^z zIB~=rcb`-}IyVK=FWvSi1od*Retwh!6zqrrMd0q=3g8?Zh;P;bLo3Yhrqp?fSbmqf z0^qBGgFDJ4wtHZ?>V63e!uwQbUh)Rr!5Oy(@$kA^<K_pVuJf9$9r{V#@FH`}S6&b7 zt_#Ig_jX$pmd7AzYd<Wfip-HT)~n6vH+;N)@+(^pte01lf~EtnFtSP9g!k8QS6nH~ z{4gn|=d&{G>RI^t)`V2l-=!P_ad*;@%JxerxbFH%wIx`8H-C(sg6R*JTEWM3HuNmi z!E}CJ7|6%_en<kpxV}c(D{cE`^y7dD8MCo`oF+*pQ^`Lhb%rf<!+b2cmFP#oeNt?0 z#tJ7~|MqL|>3rm?4M%a=^w?dTE}YgHpF3}GJf_F&2OWs%4|EJt6<+aJA-sFmfKAV) z*<*3nQoXN6YSh;;_l9Q|Q@BfQ;PUqq|Lv!*N&gtG#Y13lfj>#fZuTdP631OEG8gRe z6)=n}&sdDIxXIdOOEZycY+xToMITfz6v40~IyB@d4~8T7McfQ$Eus$}x2FJ#FMM0Y z2c##TEe)U`va_;pV*m-S?=(Rq9me<+KtHt;<FHQ0%FE#JC=kqMKhY&8-AZ(T6YB#_ zYY~|R{Wxo9)VsfUAunF(<BRw-OvxP4e)J=63?KOL4QE$`6q%SI+D%;%iQ(t^la>Of zzl3J{1J5n{Jh&84^yZu$&bkQqml6)kqGd7~IE&(#9K09~NY~GUorhrfJ{WMY9FXWM zuVrUZ98>5nfJZ;w&OXhYx1Q?;oO5$KdtH~NZ*UTZdmgnjMPwA_2T=eXf7atHjw#RK zI)a_w)ma?|C@bT|z69Z4+K?X#oRn+{NJpK{1A_=a-m60`9$>E@(VK}+C$FW{*wZ^X z#pY{*fs;6Qt3uGx9$WNt5LI}~F+KH5B>S;kgQkq<>;_bGIm~{|K^*I+V}64w=HA>5 zD0=fC!3L1{YdB~Eio*{z;6u4c|BIs)+D#_MX92JM?RE&KJDm7vc^;z0$s#YrGxk?; zR?S2vO0N)t-epefX5a?L4Q3Z3cPKf$9K#FBIt`I8S>iPhI6bwr#~6?(H$`AQiQjDP z3q)>NTYxV&$k9vePad3Bo!2<~CNp)Hu0(s+jcw>xyp4@vI89=nrK{6uS8KmBN(i_s zd_P;>4a4W^BUgCAML!cOE$hbknaHMAYe38GXT~;zy{*AMO$4a^>7aNQpe*gIC5CC9 zkABn43&3kst)$Q{>C<vL15m+m^uP{4hRSzm)3xCJ!d(=U&&*I&%y+K-1@>E7ZqN;O zS#$ILhyM8VDZVHF_b17zv)7@Xq(-!1I>_4}k2D~hQ{{LVr;C^4R`u-x6zkQT^#>#; zx9zN>07opg!D%fMZYMAS<mg>7i-PdWK~fiRx-71`t>+0yTqej=LpVLQpUWVwn6=*p zQ1tmj(<TVt^iFmX>iahz_!SLEu1DrJK)7s!>fJQt60^4~LbUaM!#)M?Qfb9@)!*xP zm-SDtmwWSo^P3Me!E~C)rc5_SKNG)J-G=4C^->vG0O8_cGPSIpS#|~ML;QgE2=(M* zp&aImU);dK^f3DMH#Y#*7y9rR3^(bFM!)2=I2mLiH@Fdu{mFm(`<JJe|NUJok^8dP zACeh#ee)BxxYfc^He)hHXJs-01@E#mHv%%HFBmLl`i-*nIttvQb;+4zz%H93<FUHQ z$xN>eXg9xOg4q|_F1+oB*eP2Y0(R1oHGT%7RVRl+B-z&|LO6$i<{=gfbK_;T5un(i zfwK+5pXE)+JpjDk(8WFtI5U2JH`{C<SruWoMrK{uWCtkN8w#@<NQe66Z3(Em#rH}T zLU?ZT26Qih$tiYYC$Wnnduk?6IkYGP?6;=Rd0GfM`kIv;rjr;PIFkf+^8L#QKST#H zqXt+z)imHRgg51+rCtEenXfz?^^Eyo_A7`Xx*hB<geF3{=_z1`-9l|_K}W`?4aIKN z?~tu0x-Suo2-1b{`j}WH);&8Ocg_dywJ5HC2cZ5LW1qvIV>m(0L5QU7^j@$N>96mw zTM}f{PQY>!O@(dU0dHCLKgWDBCw%S%LpTY#53@1AtTz|f>&GuTy&8Bud3rV*@RqGi z6}}@>m~Q?G>y3yGf7AeEPT!Y^1*GFN?wCS2{UcV&15mVi`wAQm=@qg^tN;nD%ffVs z9<Kb2!;PT%SuR`tM@r<-?KzY5a@lgsda1b*^{>kVqd`X`s*Kq36x;8W2fO&}+3i?= zoXk<LQO_InYrhBT`a7oMQ=q8I=>X=FanpBq0wm|H{=OWN@U6jagYMLC^g%ljFByqA zXJ$6WCx{#q?F9&LDQ<AVE(kR2$^_mumAb(;SApemyq@gR8-e)~blzL)4af)_1X!MA zh}=UC@|F4pRuu64quAkq6dadh@7E|Y)WC3RemLqyBy}%#-y|TcOdB|n{FTJ=v{x$Y zkdGo)mjTjFQ&KT~y9(}n698wte#v3|5R*|`auM&0k%&W7x+aC)8<8BRIztFL-pfID zh%>)`@&Y8H@sHMHc<T5~Z2HpoO~!okhb0|925$p8B{)`M;BG;sLG^%sN)B}|Kwqe{ zMH0e^Qpd`U3P5uETT&*Vpv_OM9FV&_kc-_D)9O)mkb>KswjP1QIp5l8X9VDX`}>#f zYWhEBZ)(Hf7>W?Z_KSOex4=3~JB@{Mc(lTk-XA0J$OT$ob_OJe@OwJOBG=S+lf@$( zk`%H{aUE&EviKC#?=$HMhJAo!;%p+&jYOxWt3;u$$bW|)?gQjI=&?1fBYjy^k%anp z*W&z8&-*xlqk(A6i?@8#zuHmb14l!IY_grk!}u=<OY(4EBOM+1BzgJf!Cr%EHiDks zEy@YQc($tMuVwc=+W*M}*PY&!Pzl0#-cPTYi1+P_*0KdXBl_9Sqaa=<$L(AS^6|$? z#9(@6xHStX(AQK=*Z}&U!<4^e3qjxRy2T6Q$>l!r#`wEuO!Q>qKemOz^nGgkWeV{f zx-RDt4(nHMhI|>b4&zxXF_cSzzBp@-J8qp2@N7{u>YEH5j8MOLT{Iu{SA))ZqW;h3 zI2#Jm8G9JE8i4daRUBOex{xE{>3C$vp|KK!LZKWsMqG->#QcQE%K72~s`mvN4v<cG z+3BB*3!I)E9%lpj6f8K_!-s`V%g2gu@L@sIGtTN$TwK_+f9Ns;2q#QgTL><MGU%*E zw<S&AE-Ar6txYBAcl}`@R%2z{O$%I*wc_$~>;|A6m}{cmF<PG;y`^iGFU0$Qo`1#w z7FacE_<8EVLaB-KS3>?lE>@iuhSwh|=&*qJnW*K}E|`wDqttC6p4(c76}YJ9B_8oY zy<pRnMBt@@Jww)lo_0#T?hCqO#U~`gF#a8?QTkvf<BZa=IT9wW3jKv+P%7&VoF62g zjOpYYH<)9N;mZ4yO#szKlsykbPOOYkPjv6AY60i>8U9!coGx6}$#xfvM*MKFtF4JG z^#>$m(Xw#FsLTh$Kvy17wFA2a@y!B8ACUB4Hr56#;SQaI%@Y<>HDbBaZ9ksa11~pI z;lv}q5jkWDhA)xKK|87W$^H}w)6Ouq8>t&oX#;jq-1yyEpwFB$|B4SFnJ2#$^I<;c z*clv-$g@@DonU9I56*;HC;d}pQ-ct?8GrT0*gY7Ye}=;VGFQv^*i8}ftohN1yaQv@ z0c~w26ydN!_K*fe;LIFXMVuu%Zs5(_2AsU)eyYcC`^BR%9{YViMHztp-;^Bk07cTv zBCs6G%PtkN-B#D)!AB_2|6FW>-B<XlT=iVQpqWRd3jsC5sJY32w@hncD+|yuuOtd7 zz<6yh?7nl=r%s6nq*c!}bHPrh>5j)?O>@O6_O}B<r*QV6y>^rnmhV8$8PiRGsacD8 zm>#-gnlsJ<`CUhCF&-kidpw_l+i*vm@C4p}K4d3e$8i~Sf~`-FC8tgC@NB_J6K|mi zPJuphqfh4^910z5xlB0AG{39Z*^vT1#~0Qu1J~O(U=D{&JL@c)9Vdjtqks4gRKcOy zOYhrW4?|w^@>dodew~%_JSQ0rsdl!W>J^OfM^KD9;wbB7X`nwMeKj!z^%so?9N=(l zr9N@B2OetOQuK-k_PU9$P9z~u7d|RPeTn(05Y)~6>TLk&7^`hEC>rCtZyW+!WhkUy zmHucdJfkL^VilZ40Uy;TbOF9eZ~q8y;gyd+9f#I!K>dWy7mmQSk5=orpicMQw0ue- z-F@NisiDF}(mMr({3O)#M%D2#{WImxdV>9o+VORtTHHaXxj>Bqx_`R#OktY4f>E97 zx0aP~!Tt$fS|GjMp=$^K+slFfeIb${Z>?w(R4X~jxqxmg63O34MD*O^iS7v}uiVPO zaBG(Uba%<~yk%v8a#C#*1%Q0B;q%d*MJHC+q*I_{HoQl-A{$49<Ls|?z0w#Qbqfzn z?K=Qizma2!Zd>2p#5n{=yz;`%QP7IEZJLg5&dk2N1)Gi7B+5P(xVWHM6a<|6#Ee1r zBlWMR7J;2PaJVxRxL~=wN(dmo-OU<jnY>CH7!DxW&WHE2voS_e0v{fUQ_%+O2Kwio z%)r?x<64y$0$fcb{*1_ZL3)z_2{YCSXOF~VLO36|^nvs)0dR5d>ou6Z%=6lPm|u7( zmt)hz%TLDgAUf+!u({-pZa#tOP%B=P5ry^_atj?Ph_<O?ZUP_?DAi!P=^6L*0|0r0 zlJ7(Ta_@e6!N>6Wn-MdGfCo0sT!mb1qXIT>=4NHTBcQ^^Khc;@Zf(epR6s$-=O(n1 zmp#Og0wl$?d4QsvHTpO`xTO+goPze#_3S=oqB|u|O@+V}20pScA;^UeW8jS2^I9C{ zBn`KjV*WY#ya9JWzl58Gm><#A(fVvT=H@=$0GwGIV2a)Agw{xh-GIY3_kP(=!Q{?x z)P^d+@WV4^?FT#jR+o<D!b={SkOmLuiM7Ww+n`&O=YLyw2J8>`!rP62Uj+HrjsP+q zEp1{7Mwcm}ay7t@<Zdh81vs+v>yRqI{v$53>45d)^9L|pq<OW(F$%DF@M6qYe{BBY zt-#e@Y`)3*Q%q`MByi^Sy@@;sC-jIRcs--;ZMPm!r2RxC8N&N_jGB{*;fI2}X+Uwu zO#$oojOQfQFKlY3xqw}Em|i!=Pn?r?x*}?4q^bfkA!b^U5KefvZ(uh~q+2b55dYKP zzsx45{9`uBAIipj>xA!exIIZ8-Kvh*^6t}oxGggz%#R(VzSw`XK)Z|4WqGg@AHjeZ zpkG4p3v?6SVU-2w229(wCR;$-y*Ub-zgY9}ZBIav_Wc$34yHtJ;@Er&?6HNuzQ9dN z%-Tx;>&?3)(at!9{ayuJtZp(s3-H|bVKWbbUA*h|U@Ybl#Rn#x1GGP}dx;3_T<_1P z!+>iQXPp-VCh>=hx&Xu5@9u+9DEGwty9Q4|_bEqt;Tb?>pBa;KAe?-ha~jjjE3J^n z>Js%?%*NTzE!j$KtY*&6S@V+snITCMdVoanrvRIe?3kySSS@t=SjHRh^7j=vyCA&3 z@8v^`hZ%Kh4Eyk9x=#nwC0M4($NMoSPK<N~yccFY8_NrhO1Z)|Q|9x`yYP;cnQ@vI zf%-=cYgu=zkRG%Qbjlfa9q6v9Wl_F+fzvaO-gN^smnhD>02t*x_u(E0S5j}eite>5 zPIcZfK(E8~3wHu4^tR2wa+Hj7_1Z&0InPrLz;yP%wq-xIlug!pzZE#+yz>usr@SLE z@3CCC;`jOJo}90<G%+7syZ)_SfL%AF2UxchY?<H-;XL(t_8qYJQ}<b{Uvb=>6|9@* z=DtyaaJsqo4f=sVr}0KHa8d64leU0@tnlO*u!}sBIdOo*K6s24*f~1G9$~W*#AayY z{Y8-m!yN$OAjG2WfMkeE33i8~(T3~X00qHbS?uh8qVq58PUz8{df2>(df>bO3ef4> z8;nnMHfxp@hFk5=!ETy%KD7wDC!(;tPa2Tx6Se3d;F0Q&J@puVef`OBK=GuX3y}-r zm(FU0aK@QzVe5Cz&VXFte%VT{=)Xk3EOJ29Z~vG9$V6I}7C|^MKh`%No)T6*|5aWA zT&((|A{?I%loD45z|+RdIgY*T25qqmb|u2o&C7U$;9$TNe&I(`Ku<US-Vg}8`Hs)N zeF=BTzWJgyej{8Lpns)31)oZ^zFF9Ve&p`xLs@vgn!p)WsM~G25_c&ce`2727lx1O z$`gRzZsLFuwol^J6oBoIeE;zd{e(zL5jDJ?v=nm@WuF#QA>Iz;hX68MmjbL`(dd*E zyboi3WD~Xr(!alj^#|FCI;>yfNE|VK?yxP9Xy+ErD=q{(bC7uOfWu?9KE>ilff=Hs zaIP_+pkx($@S`B{n^Zal`eT`&0x+C;?&!>-=H*bZ6OPbPANhfur@VoS^Nc9={#`FU zr=eXGbub2X^z^U@M8&VYYY?ppim^S5Qg2*L2BZTIU&8iAcvDodJ;3LD<J`ec>h>t1 zzmxQBMgGXoD9%SeVN%zhV)Z08qY}fr+7Fjt`1dZDr2+n@zklhTwf@mP%Sy2^Glt`h z;dbH+=@q7c%z_ia%i*@XQb|Q4K*qe(VJ;w1TsdDAkvG{im;(Lx2lI>onM*USt^~W| z?7UZ4thCR@Nos&(U8v(CMCOhCDhg&l5yOVq0gB{~om~xf#(U*@4j|{Dl(jA(sf?e; zB6G?G)5AaYUdA3!)S)%>uYTE!*{C-jG=oOHP_>jr-c8sC4dM&Sxj7s4(Vt4N8Xd1S zUPHIw{Q4592S_qsyjq6Hb1uN*7e(t$N4FGhZ@Z1*^-8aIuLb11Pr39r-iKG00*87~ z!Qn~sYV95$K#`pP3pV~4KdjwRZ<+m#)rV&rVmT1nOU4Uy<qk6*p<9U}*Gb{9Gk;?G zc&z?h`4m-aK%y$6%3eqR{rX3?@PrTMmz-(3zXZZHHE;J|c``vOeqcE=v+Q}So*1q) z1D$Le{{(hl<l_V(hVyf&(e{93&9?C>h=D2YI*9a)9&~5fj5h`NuuI0AySEtZnp~G| zKR`M!c!Mb*X*b%)HXr&_KDw*yqL)dmzNl(DtFIq=%oFwV79TZ7y^wdF)vK9{B0)bO zvitk)&dBH?*1wwk`5#!GL}j_<L_k_^NQohGHN&P26zHQed$8MQd}aoszGRz>fd_`G zjQ%kRkUssUg4HvkWwsRPRxVA%X@jUFw*l=U@0T1bZ^ryjKh`7peO?az1D>*b{D6-d zv7@{kP|fqiPfT~BqDoZ(VANpeAJKq}XWUUgc!|^~n-qQxUcPjlJ>qs5UhatU^aeJ; z%a+y$J}crOe9z;F3rgVS&$Q*^;OU}0q<mDO-a&XtlWEX@Z!hYC`eDN<IM(&LyySX# z3AOZ0z`jU$nWY2g@_9peh^o$tjqsAmGj2|I6vQ`JHciI^U*7C2l2*g(l>3x@0e_yi ztZ_hnx937uAAWgADC(KEC7xhk^x(c4AJP}KBihRva3D;)-xBq4q}2+pzc=b`d>Fn& z$~IkTFZ{mC^6h)6Wy018uW5^XN7Om3-J*l{%@~+u3@@dcwd}OqAze@Gc){+lNp^wr zkx34?S$5*>U|R^+tdj!2hL>t7m;X3ZaDC;=mrnSSZFKt`OVq!LTf~LUxNk)#^czEZ zG?S*+;7hqDi~@X=&sS`ZU$<Es({q$rV+)&S_wG&93Bk?28xPLVM*YO?e~h49+w;WF zc@Xd0Pwm6AFg+o|qr6dXou?8atRJe?l5LJJ<H|2b!3J3-CMKCTB>2?!@^VMI+gIr2 z={c`qlA$m+H<vH}XQ+_J<B12-Kj;h|JowM13Juhg&llq<3k#X5@WYXye=^89#X=Yt z8*9B8b|6Yf_#I`~=_X80N_wk4PFFa6`t;Tj5e`&nXlPeshA!$iHr!YxY&o@Z?t&N| z>U$5pbrwSWzn(5yE>u!dqGumS!uV_E<%Xi(O*p|^IAX+z?8Pptg`f}S4tB-(9g?2w zp<bcHCYdTPS5y@#V)~!7ybf6<)YCgXBOcZvgZ@5$tOM#}FU-;vu1FbgRNS9TCGPAD z@4TNR3|*a?dSaomFe)m_&4&|3UBC8ByX;}4aA5M&9TKBB!aiyHl2YiOe;bvq*(!jS zsGv`U&G+B;_x~I_Dwx_`eW~;<obL<z>36;_K)vRtQ^C~0WN#hiRc^u#jWLhky;+Fy zH>>~A78VyT-5Tm`D*T!A!R7H#4OTxv#Sirr7b6{5{d>^@R$q5m4fXP~ZB0-w{e9_T zVfe6l<qSW8s(rtBtlHQJOi$h%fvIrn)Ts~oQ=Eh?Ejq64x(3vq_bETk;A>9MyR@81 z1bw5Xt8*mkrQR=Jh5E^1lh>eLe|dyH>TmiO>tXqX>W)tZ{mjK39igBfVmHIlK$w|f zoL?s$BJ6whYVh9p5tQ|l;i^l!DB<YQqccxW>!i%gUddD)J1+G1k6u%xoDS(v9XEDT zqwsK3<lW$(#lo${#gFF2B;b7m-W1nUTZ<>eS?>@CuV4G%Ol^r0ww;+N?ffJgt`9B_ z;iU){ET{|YE(-wrnI|z0{!lO19$3qAp*)IX-`D60wY9YynjWn}-D&gSm2jPZ{h05I zQSWI`6o~q%i@WVmpM9`XANA_*X6XU9KXhMO7xmn5>t({nj~-nWMkP{CP9sgX|4tK{ znVHo;QMG~eH#3`Zq9I=wQ~R9lAs?R-`qd5D^qhJX0P$OQy!q&c@iPH`EYUvngQOV+ zJS#uKTo~a0;{LEBn&>CI{Y$bT{*7%z?NTxRGO78_sMk0mxeDX&J?G;fbaIMGZ(C=@ z>JO@AQ41CruBwNxPIp(&n)RW{6Vvl7@qPlvZ(8B1kNTx;8P>wtK`FeNL#EWHk$$&U zl}*QTew}7xD!g3LE9DYJQQgr)YfX;IQ;^?S1G|;Nra9kq45w)ctA2mjm1nyK>Y+GD zoNFi)zcy{5@BG$LN&oJ!;n_qipC6iOD}>{Y=<+|7T2r7`f*;pWZ~o$xp0Koj)c366 zdcw2ZKQB$~4O#uOm#n@&s@;at|Mk>#!U#RoZ!$itE962#6C_`?puRQ4l-1Xb&=&HP z657q-x4)NOuQ@{<!i2qLlG@XQRtSF`n_{<emm{@t$M19VYT#UO@2_T)hp3`H<L<9e z)HB^z!_YqyR0?!aKVC7{g*wYk^jv6XB;4Ha<LQ@$8bXoDPnptLKGco#w_1%Kx}$!x z_hu8}w=k^$sXwbIIsIQbO3UV={;XEi3e<1zD)B~r(o#<kR)4C@O!)KI6pi=r<AZYg z_sMBzC!yZ((0wh`yDeQ3L{Z0ech3uC(_^7$WFV~Z>R4Mj&xO)D$ovd$(5EzAcdMO> zVB>EIgrC?!KTLf&oY%d2h>V$*De9*kZgfMvwt1ro>iI)zv@!kztA7Qe{@Iy=SkzlI zYZ(h|)q5><KloAG9ca~hA3HYw6i*G|^JmYVotf+@<Z|o9M!Lz=>sPNV>kh0EdU^G3 zlQ#?!e);^lbmS^W;mDCALl;|^3B$s|KEAS#5bhB0TJvI}S-ro7sW3k;Z~e0leW9G3 zT&JwD2?f~OTdFBM-_g;bY`uiK)zedRW%XELe0=<`^VPpwjE#(1-d~)BJpP=@_ZD6F zn{evWznxS3zaQ1dWWG9x(<@kt9EH;qB1f-fpZd)12yvPwdP`uh7_fW(KI;lNVENOk z9J$wi`_mZyJoep2;9Z3^AF2TzvsRNDz>YI1Qkwx4f)#({0usmhd+RB{wU!6)DV?@> z^d<$cKB;X;KH#liA%~LyO{_f3wgAG{$)-&dOzTH0x+Vh;xH*4$5r!WhGz_0Y#ox1` zn}m-67k!_z4UmMc-zTDAdS!fQB2KGRT8~_<1Z0$r*cC0rm*a`)Av(RomV*wS!$PMa z&mDK$5V+udSF|VM;9)SW0sE1oDrn~(-R0JVeBc&!2RLuTV~xGQW%sy#X$EvO8UIxT zD0rBqw+HaQ{r$_+(dd6X9d!-*Z&CkM^DpE&VzI*E1TwW0;PofxGJrGF*Jxt#i&Q%l z;{o|A71@s#V;0Mws{&30`7Y-v$nHF=G%S9v>pZ6hz}BfJRv!ZFA6)(ttKGyq)8g+# z<>cR3Z9MamB^WQqYWc1d3aW?KZd?gSDkpg3LuLJtoG1A5g7j(U)dC7yzg2_*G7q}5 z^eA{Im*f$%4^WL(>I(tn<W>g-0g~MGBCKZGAgjs;c>f>G>xqDwtM8A;`|zI5s>G*2 zPRka7zYz4Ii;fAviOy;@_G#^qB)mL;>+kckaF$Ol`a4q)E?&Z$?twbaQ|%nI57cSQ z04@lBYmQG5B;m3bHVfV|&3a6Squ0IjX+p5iuiSwzNd#I>>_&%N>5b~>)^cxEE@Cs_ zH%9tJ0eU*mX+8?bXeYHkqF}*5)D%N>lg{+Wyi<U8-5TCq25i{W&+G?OQ!vV5-H(&C zJc)w)oT6lO!A_LZ#tZ>2`n<?vJ@Tj@S)%|MJMSnH<RafPe2U>sO#8ca$;NI=CE&~z zefBFuPP|G#4|RLf^fVC3rhz~RhXo2Nf`N+`OpXWwq$m7}jzIgBw|_rA6_57y0Xw0q zgRnUf|G`x;fP$Bk!m)fyYWfQWfTTZp+GNE4^!G3BFY^!g7yO^xp9pJSv&my-YnDOs zn8IK3=|X7L-`W$fH{jIxyOsg+sM0EI2E=NObUp=c@nh{?bld(y1NNjYSRHYH6NGma zx2?k7htVEug~dRIta!o)U2Tx5NjqSsaLiC_URFc=ryl_f|1t0on@eAu<VWn~n8Wey zz7)LcU2|d=ri**ky?ZmDV0N$-y4CBmcQ)++%vImDADap1chAdo2(R~j<CX}#V|Byg zt-za2GG|~fCsGJ#MR)G9Xm$#Rp9lg=er)ff2rq1L-$XIk<8F*OjlEQ;enVy*;Js-P ztFalIT#G2(1<0HIvk03vt$W+g8IW9Vt@Z=#-!piAECuNu`{`jAApMCdW19(`teOm5 zG=pmJLwlEG03uVjn1gnX{jYsng%D5G&U^ZZ%sqC0$&QoKNjM5HE4-iBf{vaxM=}bK zEV*p%0?6#5+pt-~c(Heh5crh$ob|}p6!;hc7tzuu?13}+m#x+TC*wqKDMUC$L<8+c z$qneXyc@3PL%>dJ^st9kFy(VUqubNXg(s~5VZYn=SU<e2!xy4I2)MeT=m+$v2|aOu zjKSiPEU=5rKA49B|8IZ);?DnjZ%&#%FTrF9927nkLNdj3l{6zUnd0f))__b+Ni+7c zWQpo$>_zB}tKHB&)It*BKq#;)T)(ddTy#4=23tKH6z73%My@fEfq+bvOQItMy>?Ii zxN1PA;=Vstk6#!`sDyCAAJY-&E@aR0OW120EI+_*lPPPS@5F$vqkobXx@lC#Z{2jj z{&M4(60nnD58u;(M|>)H+ri#+JFV&p;D^*-Ckp{v3d$ehpgM`U*4#}&y1esvY%gQE zXf!lS(a-A%Wmh4bxiN7XR=X&q`eZohPKE_9zY0j!dK+MGNk2FHodeC_+pDV5cF<Yx z$y<;ID6poC&@D!0?Q*;fI^NK;(P(Fue$d2buS_R$aTH;8d_98hM#8JCOo5YayR4iL z2Vb$o^5ig2USjzx52&rlK>N-E0p7q#-^T_;<oP8P>}VCD|MwT+y(ZdN&b-XExRVU5 zzx_F5Jm@&5<)ShDJVlq2ser`jMgo>6t&m!Q<tI8d`4?MX*E|2NE7K=zw?TeCc)>t8 ze1m&`CS1pR#B;~`;2bTq!ETFD$^06C;kHgmX240IcuoRxo(r`ac>QS}J9;-Iyt#i9 zc**Fi&v!$(=-7E#>?X*I{=0~@g?qtH<p1gKU)+E4KYDZ7zrV!&r|3cal{{r9GaWA; z7Er*A>w?jJ1)3cbQvgNZx)#KPU9d1L)dx7^Rn~0^NL;=QLpLMWGdCMRIDLxW>ITSv zJoG5KP5CtK>&bwsc^b{wE7IL(CSM0$Z}Qa<dwso?E0y;FepATkLAS~r$YeKO(q1-g z66;30q~@IgJ#SRs^9%@AN-<dvZb&$VS0iy0X!-1V6npFL5A%Lv@6H%#9=%0D{Ocag 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 ZZ6H1DExj!E$-my7vbVg%<Qd5p{|osuhd}@U diff --git a/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy b/demo/nlp_ext_cache/bhadresh-savani__distilbert-base-uncased-emotion/d7a88ad33d547dd8231f8b1dd0d037aa.proj.npy deleted file mode 100644 index cc1eac929e4d598935cf6f6161706c7475546b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4128 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 CnI0Gb diff --git a/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl b/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.pkl deleted file mode 100644 index f48efec27526f4e5abf0a3ba57b5c4b2e6a3205a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 772326 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%0DvvW+lvzKgH|BsVl6 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 XIw1r|)lSI@gHRDbs-w6xsZ<XD_uV&{ diff --git a/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy b/demo/nlp_ext_cache/lvwerra__distilbert-imdb/717d02a26c2b6b25b7d9583195fd207a.proj.npy deleted file mode 100644 index 8411c01fae4d2c931d06af467c41ce2dfcf8db48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 928 zcmbWr|5MX-9Ki7*6vPR6n^+Av2Pk9X+XUr~F+Q)?J8)bgAbN9r32}`fmrHQ-%r67I z_|^r2*GQ%Y%NR0h1yj^K2;Of?1Jn5CBA~)35iGAX4fH6pf1&4Z&xbi(m9{C{jklL~ zP+XuhY7Jt!NUSJaD~=F}3yTeV44Pf}#fAdif3DQ%jk>39EYg(do_^RGnJhvSBZ&}| zivDNbSv3MOwr>>HoQh0rZ+;gCa&Kdo;wx^XArBuu34nyC?JPd+2DQytvGkL6;2)8) z3x!2A^L{va``KK$GSiAZasH?rf0=OKy2Dtsf@B{Sg2%9wZRtKpADj`8U(;>y&Grpo zk#>RRP8HT#?ZDqDW83~V)5gC#xWV6?U|Tl{cW>&UNt?|YZe69rOohAs<oJ6qu$4Wn zv|=b6UXA_?8vA4Lmuzn=N?t+u7tg_KpI(5m#Xb17_o2ka**`#(<jKmWRdlCQ-8=dI z0lfB)KR$S75SBM;xO=DC@bbl<uwwKQ)LcR0)gu7Q6E)*K?x!<$7n)pwP?6x^rb<p? zMv0ItO}GZtgKC!A)=&LfGMV;VA5AIuCGX|W!?7AWm)Bs%15E-_blnca<yxHk^3C4! zt#;hCFb(VlA$iLy3!;PTQU3fYtPZ$`zS=~{xuIq2_2cxN4F>E=S<<JAk&qTyJbd>G z66?Y;IPVjKX;Kbp?@R2cvyS@fJGmTrJw$Cx#_}{7EOakYXHF8`G26j?f7FZ<%?s3( zv6c>!+td*%p`k~Xv5N3qntE7;UXNsW{ni{Pf0zTm`~p_*K0&R;m7ljL0?}?+$CXxF zQ80p}^Ry3y%<xGH&%wR1%8Z+<EPWq8myB#60NqVjvHSxExSiBsh+`hktRIK{)l#hU ztB0_abyzt+g+9BF!3aNy4Kx_(>(&sao7U634o`Nuax=Xd?7@suCB0wph->qmf;(Xn z*4<P_SG$L^y($xxELGz+(;?24yn=y9LSHPK#Is!%i1Y}AS)a3T++oA2IyV%@30P{8 zka|bCF}0_X@{Y%Xv-bqveVoo(rn;#hGKj3s_yV?O3dxUKO^`9YgzRW`LBh|3DK53s ztmUCFZ!_aZAzoyz*afk1L9FxPJ{r4!LOzyMi)F)GU^M<i%&pC1WN3)~nYalnPq%ab E0uZ8(!~g&Q From 307dd4e96d7399833fbdaf3488fdb3bafef4c239 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 15 Jul 2026 15:48:22 +0200 Subject: [PATCH 067/144] refactor(webapp): route NlpWebApp reads through NlpView, drop raw explainer handle --- shapash/webapp/nlp_app.py | 51 +++++++++---------- shapash/webapp/nlp_view.py | 5 ++ .../unit_tests/webapp/test_nlp_whatif_app.py | 20 ++++++++ 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 63453798..6973bbca 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -144,7 +144,6 @@ class NlpWebApp: def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: if explainer.contributions is None: raise RuntimeError("NlpExplainer.compile() must be called before launching the webapp.") - self.explainer = explainer if scatter_xy is not None: scatter_xy = np.asarray(scatter_xy) n = len(explainer.texts) @@ -193,26 +192,26 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: # ------------------------------------------------------------------ def _build_layout(self) -> None: - contrib = self.explainer.contributions - label_names = contrib.label_names or [str(i) for i in range(self._n_classes())] + contrib = self._view.contributions + label_names = contrib.label_names or [str(i) for i in range(self._view.n_classes)] # Predicted-label name → class index, shared by the confusion matrix and by the sync # callback that resets the Sentence Highlight class picker to the predicted class. self._label_to_idx = {name: i for i, name in enumerate(label_names)} - n = len(self.explainer.texts) + n = self._view.n_samples # ── Table records — always include _orig_idx for scatter filtering ── records: dict[str, list] = { "_orig_idx": list(range(n)), - "text": self.explainer.texts.tolist(), - "prediction": (self.explainer.y_pred.tolist() if self.explainer.y_pred is not None else [""] * n), + "text": self._view.texts.tolist(), + "prediction": (self._view.y_pred.tolist() if self._view.y_pred is not None else [""] * n), } - if getattr(self.explainer, "y_true", None) is not None: - records["ground_truth"] = self.explainer.y_true.tolist() + if self._view.y_true is not None: + records["ground_truth"] = self._view.y_true.tolist() # Single "Probability" column — confidence of the predicted class. # max(axis=1) works for both binary and multiclass since the predicted # label is always the argmax, regardless of how columns are named. - y_prob: pd.DataFrame | None = getattr(self.explainer, "y_prob", None) + y_prob: pd.DataFrame | None = self._view.y_prob if y_prob is not None: records["probability"] = y_prob.max(axis=1).tolist() @@ -257,7 +256,7 @@ def _build_layout(self) -> None: scatter_col_content = None if self._scatter_xy is not None: color_options = [{"label": "Prediction", "value": "prediction"}] - if getattr(self.explainer, "y_true", None) is not None: + if self._view.y_true is not None: color_options.append({"label": "Ground Truth", "value": "ground_truth"}) color_options.append({"label": "Word contribution", "value": "word_contribution"}) @@ -325,8 +324,8 @@ def _build_layout(self) -> None: error_analysis_body = None if has_gt: idx_of = self._label_to_idx - true_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_true.tolist()]) - pred_arr = np.array([idx_of.get(str(v), -1) for v in self.explainer.y_pred.tolist()]) + true_arr = np.array([idx_of.get(str(v), -1) for v in self._view.y_true.tolist()]) + pred_arr = np.array([idx_of.get(str(v), -1) for v in self._view.y_pred.tolist()]) k = len(label_names) cm = np.zeros((k, k), dtype=int) for t, p in zip(true_arr, pred_arr, strict=True): @@ -788,7 +787,7 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: # ------------------------------------------------------------------ def _register_callbacks(self) -> None: - contrib = self.explainer.contributions + contrib = self._view.contributions full_records = self._full_table_records has_gt = self._has_gt @@ -1213,14 +1212,10 @@ def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> # Helpers # ------------------------------------------------------------------ - def _n_classes(self) -> int: - sample = self.explainer.contributions.values[0] - return sample.shape[1] if sample.ndim == 2 else 1 - def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: """Per-sample sum of SHAP contributions for all tokens matching *word*.""" - contrib = self.explainer.contributions - n = len(self.explainer.texts) + contrib = self._view.contributions + n = self._view.n_samples result = np.zeros(n) word_lower = word.lower() for i in range(n): @@ -1239,8 +1234,8 @@ def _error_mask(self) -> np.ndarray | None: ``None`` when either is unavailable. Matches the string comparison used by the "Model Errors" table filter so both stay consistent. """ - y_true = getattr(self.explainer, "y_true", None) - y_pred = self.explainer.y_pred + y_true = self._view.y_true + y_pred = self._view.y_pred if y_true is None or y_pred is None: return None return np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) @@ -1281,9 +1276,9 @@ def _build_scatter_fig( set, misclassified points are emphasized (larger, opaque) and the rest are shadowed (small, faint) without altering their color. """ - n = len(self.explainer.texts) - contrib = self.explainer.contributions - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self.explainer.texts] + n = self._view.n_samples + contrib = self._view.contributions + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self._view.texts] xy = self._scatter_xy error_mask = self._error_mask() if errors_only else None @@ -1353,10 +1348,10 @@ def _build_scatter_fig( ) return fig - if color_by == "ground_truth" and getattr(self.explainer, "y_true", None) is not None: - labels = [str(label) for label in self.explainer.y_true.tolist()] - elif self.explainer.y_pred is not None: - labels = [str(label) for label in self.explainer.y_pred.tolist()] + if color_by == "ground_truth" and self._view.y_true is not None: + labels = [str(label) for label in self._view.y_true.tolist()] + elif self._view.y_pred is not None: + labels = [str(label) for label in self._view.y_pred.tolist()] else: labels = [""] * n diff --git a/shapash/webapp/nlp_view.py b/shapash/webapp/nlp_view.py index 75922086..0a414aa4 100644 --- a/shapash/webapp/nlp_view.py +++ b/shapash/webapp/nlp_view.py @@ -64,3 +64,8 @@ def n_classes(self) -> int: """Number of classes inferred from the compiled contributions.""" sample = self.contributions.values[0] return sample.shape[1] if np.ndim(sample) == 2 else 1 + + @property + def n_samples(self) -> int: + """Number of samples in the compiled batch.""" + return len(self.texts) if self.texts is not None else 0 diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 07621c23..e10d5ec6 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -13,6 +13,7 @@ from shapash.backend.nlp_backend import NlpContributions from shapash.compute.generators.base import Counterfactual, IntField, TokenListField from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.nlp_view import NlpView LABEL_NAMES = ["neg", "pos"] @@ -245,5 +246,24 @@ def test_tab_toggle_callbacks_registered(self): self.assertIn("lower-right-tabs-body-waterfall.style", outputs) +class TestReadContractSeam(unittest.TestCase): + """The app shell reads compiled data only through ``NlpView``; it keeps no raw explainer handle. + + ``FakeEngine`` exposes the view/engine surface but has no ``explainer`` attribute, so a mounted + app proves the read path is served by the contract alone. These asserts lock the seam so a future + edit cannot silently reintroduce a ``self.explainer`` bypass (see nlp_app engine/view split). + """ + + def test_app_holds_no_raw_explainer_handle(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + self.assertFalse(hasattr(app, "explainer")) + + def test_app_exposes_view_and_engine_roles(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine) + self.assertIsInstance(app._view, NlpView) # read contract + self.assertIs(app._engine, engine) # live-action contract + + if __name__ == "__main__": unittest.main() From 2c69312749d231d033970ea424030bdd032a81b6 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 15 Jul 2026 16:32:35 +0200 Subject: [PATCH 068/144] refactor(webapp): extract Sentence Highlight + Waterfall as WebappComponents Move the local-class-selector/sentence-highlight and waterfall-threshold/ waterfall-graph panels out of NlpWebApp into SentenceHighlightComponent and WaterfallComponent, following the DataEditor/Counterfactual pattern (first slice of the planned full componentization). Adds NlpView.label_to_idx and re-gates whatif-apply-store creation on editor/counterfactual presence, since self._components now also holds the always-on core panels. --- shapash/webapp/nlp_app.py | 181 ++++-------------- shapash/webapp/nlp_components/__init__.py | 4 + .../nlp_components/sentence_highlight.py | 92 +++++++++ shapash/webapp/nlp_components/waterfall.py | 78 ++++++++ shapash/webapp/nlp_view.py | 6 + .../unit_tests/webapp/test_nlp_whatif_app.py | 13 +- 6 files changed, 229 insertions(+), 145 deletions(-) create mode 100644 shapash/webapp/nlp_components/sentence_highlight.py create mode 100644 shapash/webapp/nlp_components/waterfall.py diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 6973bbca..c80f1e20 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -1,10 +1,10 @@ """Minimal Dash webapp for NLP text classification explanations. -Prototype bridge toward Phase 5b (composable WebappComponents). The three -panels here (global word importance, dataset table, local token contributions) -map directly onto the three ``WebappComponent`` instances that Phase 5b would -register for a text modality. All tabular-only SmartApp panels (violin, cluster, -scatter prediction picking, confusion matrix) are absent rather than disabled. +Prototype bridge toward Phase 5b (composable WebappComponents). Panels are being extracted into +``WebappComponent``s one at a time (see ``shapash/webapp/nlp_components/``); global word importance, +the dataset table, scatter, and error analysis are still built inline here pending their own +extraction. All tabular-only SmartApp panels (violin, cluster, scatter prediction picking) beyond +what NLP needs are absent rather than disabled. """ from __future__ import annotations @@ -21,14 +21,13 @@ from dash.exceptions import PreventUpdate from shapash.plots.plot_confusion_matrix import plot_confusion_matrix -from shapash.plots.plot_sentence_highlight import plot_sentence_highlight -from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, + SentenceHighlightComponent, + WaterfallComponent, pack_datapoint, - unpack_datapoint, ) from shapash.webapp.nlp_view import NlpView @@ -153,14 +152,10 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: # What-if Lab: read-only view + live engine (the explainer itself when it is live). # Components self-disable via their `requires` when the engine lacks a capability - # (e.g. an explainer restored from a snapshot holds no model). + # (e.g. an explainer restored from a snapshot holds no model). `self._components` itself is + # assembled later in `_build_layout` (it needs `_full_table_records`, not ready yet here). self._view = NlpView(explainer) self._engine = explainer - self._components = [ - comp - for comp in (DataEditorComponent(), CounterfactualComponent()) - if type(comp).is_available(self._view, self._engine) - ] self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) self.app.title = "Shapash — NLP Explainer" @@ -194,9 +189,9 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: def _build_layout(self) -> None: contrib = self._view.contributions label_names = contrib.label_names or [str(i) for i in range(self._view.n_classes)] - # Predicted-label name → class index, shared by the confusion matrix and by the sync - # callback that resets the Sentence Highlight class picker to the predicted class. - self._label_to_idx = {name: i for i, name in enumerate(label_names)} + # Predicted-label name → class index, shared by the confusion matrix and (via + # NlpView.label_to_idx) by SentenceHighlightComponent's predicted-class sync callback. + self._label_to_idx = self._view.label_to_idx n = self._view.n_samples # ── Table records — always include _orig_idx for scatter filtering ── @@ -541,71 +536,6 @@ def _build_layout(self) -> None: style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) - # ── Sentence highlight body (lower-right default tab) ───────── - # Local class picker lives here (and drives the sibling Waterfall tab too, since both - # render the same selected sentence): it defaults to the predicted class of the initially - # selected row and is reset to the newly-selected text's prediction by a sync callback — - # see sync_local_class_to_prediction — so switching sentences always starts on "why did the - # model predict this", while still letting the user override it for the current sentence. - default_local_class = self._label_to_idx.get(self._full_table_records[0].get("prediction"), 0) - highlight_body = html.Div( - [ - # One inline phrase instead of a title + a same-info dropdown: the class name would - # otherwise appear twice (once written out, once as the dropdown's selected value). - html.Div( - [ - html.Span("Token Contributions for", className="fw-bold small me-2"), - dcc.Dropdown( - id="local-class-selector", - options=[{"label": name, "value": i} for i, name in enumerate(label_names)], - value=default_local_class, - clearable=False, - style={"width": "180px"}, - ), - ], - className="d-flex align-items-center mb-2", - ), - # Min-height + vertical centring so short samples still give the - # panel presence instead of collapsing to a thin strip. - html.Div( - id="sentence-highlight", - style={ - "minHeight": "150px", - "display": "flex", - "flexDirection": "column", - "justifyContent": "center", - }, - ), - ], - style={"height": "100%"}, - ) - - # ── Waterfall body (lower-right second tab) ─────────────────── - waterfall_body = html.Div( - [ - html.Div( - [ - html.Label( - "Group tokens below (% of max contribution)", - className="small fw-bold mb-1 d-block", - ), - dcc.Slider( - id="waterfall-threshold", - min=0, - max=50, - step=1, - value=10, - marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, - tooltip={"placement": "bottom", "always_visible": False}, - ), - ], - className="mb-2", - ), - dcc.Graph(id="waterfall-graph", config={"displayModeBar": False}), - ], - style={"height": "100%"}, - ) - # ── Selection bar: persistent, always-visible filter state + clears ── # Lives above the left tabs so a scatter/word selection stays visible whichever # left tab (table or embeddings) is active. Clear buttons are consolidated here @@ -664,6 +594,24 @@ def _build_layout(self) -> None: # ── Assemble the three panels as tab groups (all bodies stay mounted) ── self._tab_groups = {} + # Local class picker default: the predicted class of the initially selected row. It is reset + # to the newly-selected text's prediction by a sync callback owned by + # SentenceHighlightComponent — see sync_local_class_to_prediction — so switching sentences + # always starts on "why did the model predict this", while still letting the user override it + # for the current sentence. + default_local_class = self._label_to_idx.get(self._full_table_records[0].get("prediction"), 0) + self._components = [ + comp + for comp in ( + SentenceHighlightComponent(default_local_class), + WaterfallComponent(), + DataEditorComponent(), + CounterfactualComponent(), + ) + if type(comp).is_available(self._view, self._engine) + ] + highlight_comp = next(c for c in self._components if isinstance(c, SentenceHighlightComponent)) + waterfall_comp = next(c for c in self._components if isinstance(c, WaterfallComponent)) editor_comp = next((c for c in self._components if isinstance(c, DataEditorComponent)), None) cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) @@ -682,8 +630,8 @@ def _build_layout(self) -> None: upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) lower_right_tabs: list = [ - ("highlight", "Sentence", highlight_body), - ("waterfall", "Waterfall", waterfall_body), + ("highlight", "Sentence", highlight_comp.layout(self._view, self._engine)), + ("waterfall", "Waterfall", waterfall_comp.layout(self._view, self._engine)), ] left_column = html.Div( @@ -707,7 +655,9 @@ def _build_layout(self) -> None: # Selected confusion-matrix cell: {"pred": idx, "true": idx, "indices": [...]} or None. dcc.Store(id="error-cell", data=None), ] - if self._components: + # Only the What-if Lab (editor + counterfactual) reads/writes the apply store; the always-on + # core panels (highlight, waterfall) never do, so their presence alone shouldn't create it. + if editor_comp is not None or cf_comp is not None: stores.append(dcc.Store(id=_APPLY_STORE, data=None)) self.app.layout = dbc.Container( @@ -865,62 +815,9 @@ def set_current_from_row(selected_rows): label=selected_rows[0].get("prediction"), ) - # ── Local class picker: reset to the newly-selected text's predicted class ─────── - # Fires only on current-datapoint changes (row click, editor Predict, counterfactual - # Apply) — not on manual dropdown edits — so a user's in-place class override survives - # until they actually switch sentences. - @self.app.callback( - Output("local-class-selector", "value"), - Input(_CURRENT_STORE, "data"), - ) - def sync_local_class_to_prediction(datapoint): - if not datapoint or datapoint.get("label") is None: - raise PreventUpdate - label_idx = self._label_to_idx.get(str(datapoint["label"])) - if label_idx is None: - raise PreventUpdate - return label_idx - - # ── Sentence highlight ─────────────────────────────────────────── - @self.app.callback( - Output("sentence-highlight", "children"), - [ - Input(_CURRENT_STORE, "data"), - Input("local-class-selector", "value"), - ], - ) - def update_sentence_highlight(datapoint, label_idx): - if not datapoint or label_idx is None: - raise PreventUpdate - label_idx = int(label_idx) - tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) - return plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) - - # ── Waterfall chart ────────────────────────────────────────────── - # Shares the local class picker with Sentence Highlight — both render the same selected - # sentence, so they should always agree on which class's contributions are shown. - @self.app.callback( - Output("waterfall-graph", "figure"), - [ - Input(_CURRENT_STORE, "data"), - Input("local-class-selector", "value"), - Input("waterfall-threshold", "value"), - ], - ) - def update_waterfall(datapoint, label_idx, threshold_pct): - if not datapoint or label_idx is None: - raise PreventUpdate - label_idx = int(label_idx) - tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) - label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) - min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 - return plot_waterfall( - tokens=tokens, - values=vals, - base_value=base_value, - min_pct=min_pct, - title=f"Token contributions — {label_name}", - ) + # Local class picker sync + sentence highlight + waterfall are registered by + # SentenceHighlightComponent / WaterfallComponent (see the component loop at the end of this + # method) — they only need the current-datapoint store, already shared via `stores["current"]`. # ── Word-bar click / clear → table word filter ─────────────────── # Resetting the graph's clickData to None after each event lets the SAME bar be clicked @@ -1195,7 +1092,7 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): # (The selection-summary readout is written by filter_table, which knows the row count.) - # ── What-if Lab component callbacks (self-contained, capability-gated) ── + # ── Registered components (always-on core panels + capability-gated What-if Lab) ── stores = {"apply": _APPLY_STORE, "current": _CURRENT_STORE} for comp in self._components: comp.register_callbacks(self.app, self._view, self._engine, stores) diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index c4262997..60106f12 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -21,12 +21,16 @@ pack_datapoint, unpack_datapoint, ) +from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent +from shapash.webapp.nlp_components.waterfall import WaterfallComponent __all__ = [ "WebappComponent", "available_capabilities", "DataEditorComponent", "CounterfactualComponent", + "SentenceHighlightComponent", + "WaterfallComponent", "pack_datapoint", "unpack_datapoint", "datapoint_from_contributions", diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py new file mode 100644 index 00000000..7af57be9 --- /dev/null +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -0,0 +1,92 @@ +"""Sentence-highlight component: inline token-contribution rendering for the current datapoint.""" + +from __future__ import annotations + +from dash import Input, Output, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_sentence_highlight import plot_sentence_highlight +from shapash.webapp.nlp_components.base import WebappComponent +from shapash.webapp.nlp_components.datapoint import unpack_datapoint + + +class SentenceHighlightComponent(WebappComponent): + """Inline sentence highlight for the current datapoint, with its own class picker. + + Owns the ``local-class-selector`` dropdown, which the sibling :class:`WaterfallComponent` also + reads (by literal id) since both render the same selected sentence and must agree on the class. + """ + + id = "sentence-highlight-panel" + name = "Sentence" + scope = "local" + + def __init__(self, default_class_idx: int = 0) -> None: + self._default_class_idx = default_class_idx + + def layout(self, view, engine=None) -> html.Div: + """Return the class picker + sentence-highlight placeholder div.""" + label_names = view.label_names or [str(i) for i in range(view.n_classes)] + return html.Div( + [ + # One inline phrase instead of a title + a same-info dropdown: the class name would + # otherwise appear twice (once written out, once as the dropdown's selected value). + html.Div( + [ + html.Span("Token Contributions for", className="fw-bold small me-2"), + dcc.Dropdown( + id="local-class-selector", + options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + value=self._default_class_idx, + clearable=False, + style={"width": "180px"}, + ), + ], + className="d-flex align-items-center mb-2", + ), + # Min-height + vertical centring so short samples still give the + # panel presence instead of collapsing to a thin strip. + html.Div( + id="sentence-highlight", + style={ + "minHeight": "150px", + "display": "flex", + "flexDirection": "column", + "justifyContent": "center", + }, + ), + ], + style={"height": "100%"}, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Wire the predicted-class sync and the sentence-highlight render.""" + current_store = stores["current"] + + # Fires only on current-datapoint changes (row click, editor Predict, counterfactual Apply) + # — not on manual dropdown edits — so a user's in-place class override survives until they + # actually switch sentences. + @app.callback( + Output("local-class-selector", "value"), + Input(current_store, "data"), + ) + def sync_local_class_to_prediction(datapoint): + if not datapoint or datapoint.get("label") is None: + raise PreventUpdate + label_idx = view.label_to_idx.get(str(datapoint["label"])) + if label_idx is None: + raise PreventUpdate + return label_idx + + @app.callback( + Output("sentence-highlight", "children"), + [ + Input(current_store, "data"), + Input("local-class-selector", "value"), + ], + ) + def update_sentence_highlight(datapoint, label_idx): + if not datapoint or label_idx is None: + raise PreventUpdate + tokens, vals, base_value, _ = unpack_datapoint(datapoint, int(label_idx)) + return plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) diff --git a/shapash/webapp/nlp_components/waterfall.py b/shapash/webapp/nlp_components/waterfall.py new file mode 100644 index 00000000..8d838642 --- /dev/null +++ b/shapash/webapp/nlp_components/waterfall.py @@ -0,0 +1,78 @@ +"""Waterfall component: token contributions grouped into a threshold-gated "other" bar.""" + +from __future__ import annotations + +from dash import Input, Output, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_waterfall import plot_waterfall +from shapash.webapp.nlp_components.base import WebappComponent +from shapash.webapp.nlp_components.datapoint import unpack_datapoint + + +class WaterfallComponent(WebappComponent): + """Waterfall chart for the current datapoint, sharing the Sentence panel's class picker. + + Reads ``local-class-selector`` (owned by :class:`~shapash.webapp.nlp_components.sentence_highlight.SentenceHighlightComponent`) + by literal id — the same cross-component reference style already used by + :class:`~shapash.webapp.nlp_components.data_editor.DataEditorComponent` for ``dataset-table``. + """ + + id = "waterfall-panel" + name = "Waterfall" + scope = "local" + + def layout(self, view, engine=None) -> html.Div: + """Return the grouping-threshold slider + waterfall graph.""" + return html.Div( + [ + html.Div( + [ + html.Label( + "Group tokens below (% of max contribution)", + className="small fw-bold mb-1 d-block", + ), + dcc.Slider( + id="waterfall-threshold", + min=0, + max=50, + step=1, + value=10, + marks={0: "0%", 10: "10%", 25: "25%", 50: "50%"}, + tooltip={"placement": "bottom", "always_visible": False}, + ), + ], + className="mb-2", + ), + dcc.Graph(id="waterfall-graph", config={"displayModeBar": False}), + ], + style={"height": "100%"}, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Wire the waterfall figure to the current datapoint, class picker, and threshold.""" + current_store = stores["current"] + contrib = view.contributions + + @app.callback( + Output("waterfall-graph", "figure"), + [ + Input(current_store, "data"), + Input("local-class-selector", "value"), + Input("waterfall-threshold", "value"), + ], + ) + def update_waterfall(datapoint, label_idx, threshold_pct): + if not datapoint or label_idx is None: + raise PreventUpdate + label_idx = int(label_idx) + tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) + label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) + min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 + return plot_waterfall( + tokens=tokens, + values=vals, + base_value=base_value, + min_pct=min_pct, + title=f"Token contributions — {label_name}", + ) diff --git a/shapash/webapp/nlp_view.py b/shapash/webapp/nlp_view.py index 0a414aa4..c42d7517 100644 --- a/shapash/webapp/nlp_view.py +++ b/shapash/webapp/nlp_view.py @@ -59,6 +59,12 @@ def has_ground_truth(self) -> bool: """Whether ground-truth labels are available.""" return self.y_true is not None + @property + def label_to_idx(self) -> dict[str, int]: + """Class name to output-column index, in ``label_names`` order.""" + names = self.label_names or [str(i) for i in range(self.n_classes)] + return {name: i for i, name in enumerate(names)} + @property def n_classes(self) -> int: """Number of classes inferred from the compiled contributions.""" diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index e10d5ec6..95d477b6 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -13,6 +13,7 @@ from shapash.backend.nlp_backend import NlpContributions from shapash.compute.generators.base import Counterfactual, IntField, TokenListField from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.nlp_components import CounterfactualComponent, DataEditorComponent from shapash.webapp.nlp_view import NlpView LABEL_NAMES = ["neg", "pos"] @@ -104,12 +105,18 @@ def _ids(self, engine): _collect_ids(app.app.layout, found) return app, found + @staticmethod + def _whatif_components(app): + # app._components also holds the always-on core panels (Sentence Highlight, Waterfall); + # scope these assertions to the capability-gated What-if Lab ones only. + return [c for c in app._components if isinstance(c, (DataEditorComponent, CounterfactualComponent))] + def test_full_capabilities_mount_both(self): app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) self.assertIn("data-editor-input", ids) self.assertIn("data-editor-predict-btn", ids) self.assertIn("counterfactual-generate-btn", ids) - self.assertEqual(len(app._components), 2) + self.assertEqual(len(self._whatif_components(app)), 2) def test_counterfactual_config_controls_in_initial_layout(self): # The generate callback's State references these ids, so they must exist in the initial @@ -136,14 +143,14 @@ def test_edit_only_mounts_editor_not_counterfactual(self): app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) self.assertIn("data-editor-input", ids) self.assertNotIn("counterfactual-generate-btn", ids) - self.assertEqual(len(app._components), 1) + self.assertEqual(len(self._whatif_components(app)), 1) def test_no_capabilities_hides_whatif_lab(self): app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) self.assertNotIn("data-editor-input", ids) self.assertNotIn("counterfactual-generate-btn", ids) self.assertNotIn("whatif-apply-store", ids) - self.assertEqual(app._components, []) + self.assertEqual(self._whatif_components(app), []) def test_callbacks_registered_when_mounted(self): app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) From a817f5cb5eb68faaeee633a0aacf0d4dd4a94606 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 17 Jul 2026 10:43:47 +0200 Subject: [PATCH 069/144] feat: add similar examples the new feature allows the user to see training samples that are similar in representation space to the selected sample on a new tab in the upper right panel --- shapash/compute/retrieval/__init__.py | 14 ++ shapash/compute/retrieval/similar_examples.py | 181 ++++++++++++++++++ shapash/explainer/interactive.py | 9 + shapash/explainer/nlp_explainer.py | 96 +++++++++- shapash/webapp/nlp_app.py | 15 +- shapash/webapp/nlp_components/__init__.py | 2 + shapash/webapp/nlp_components/base.py | 3 + .../webapp/nlp_components/similar_examples.py | 157 +++++++++++++++ .../retrieval/test_similar_examples.py | 136 +++++++++++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 129 ++++++++++++- 10 files changed, 731 insertions(+), 11 deletions(-) create mode 100644 shapash/compute/retrieval/__init__.py create mode 100644 shapash/compute/retrieval/similar_examples.py create mode 100644 shapash/webapp/nlp_components/similar_examples.py create mode 100644 tests/unit_tests/compute/retrieval/test_similar_examples.py diff --git a/shapash/compute/retrieval/__init__.py b/shapash/compute/retrieval/__init__.py new file mode 100644 index 00000000..433f032a --- /dev/null +++ b/shapash/compute/retrieval/__init__.py @@ -0,0 +1,14 @@ +"""Reference-example retrieval: find the corpus examples most like a prediction's input. + +Where ``compute/generators/`` perturbs an input (counterfactuals), this package looks *outward* to a +reference corpus and answers "which examples most resemble this one?". The first method is +:class:`~shapash.compute.retrieval.similar_examples.SimilarExampleRetriever` — cosine similarity +between a query and a corpus in a chosen layer's activation space (a lightweight, in-house take on +Captum's ``SimilarityInfluence``, keyed to the capability-based model layer). +""" + +from __future__ import annotations + +from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever + +__all__ = ["Neighbor", "SimilarExampleRetriever"] diff --git a/shapash/compute/retrieval/similar_examples.py b/shapash/compute/retrieval/similar_examples.py new file mode 100644 index 00000000..0b8db088 --- /dev/null +++ b/shapash/compute/retrieval/similar_examples.py @@ -0,0 +1,181 @@ +"""Similar-example retrieval: nearest reference examples in a layer's activation space. + +For a prediction, this retrieves the reference-corpus examples whose internal representation is most +similar to the query's — the examples the model treats most alike. It is the in-house counterpart of +Captum's ``SimilarityInfluence``, reimplemented over the capability-based model layer +(:class:`~shapash.model.base.SupportsActivations`) so it needs no per-model wiring and carries none of +Captum 0.9.0's single-layer / short-final-batch quirks. The cost splits cleanly: + +* **Bank** — one activation vector per reference example, computed once and cached to a single ``.npy`` + matrix (keyed by the corpus hash, layer, and model id). This is the expensive, amortizable part. +* **Query** — one activation vector for the query text, then cosine similarity against the bank and a + top-k selection. Milliseconds, so it runs live per selection in the webapp. + +This is a *representation-similarity* (nearest-neighbour) method, **not** a leave-one-out / influence- +function measure: it surfaces the examples most alike in decision space, a cheap and faithful proxy for +"what shaped this prediction", not a causal retraining attribution. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + +from shapash.model.base import SupportsActivations, has_capabilities + + +@dataclass(frozen=True) +class Neighbor: + """One retrieved reference example for a query. + + Attributes + ---------- + index : int + Position of the example within the reference corpus. + score : float + Cosine similarity to the query in the chosen layer's activation space (higher = more similar). + text : str + The reference example's text. + label : str or None + The reference example's label, when the corpus was built with labels. + """ + + index: int + score: float + text: str + label: str | None = None + + +def _hash_corpus(texts: list[str], layer: str, model_id: str) -> str: + """Stable cache key over the corpus texts + layer + model id (order-sensitive).""" + h = hashlib.md5(usedforsecurity=False) + h.update(f"{model_id}\0{layer}\0".encode()) + for t in texts: + h.update(t.encode()) + h.update(b"\0") + return h.hexdigest() + + +class SimilarExampleRetriever: + """Retrieve the reference examples most similar to a query in a model layer's activation space. + + Parameters + ---------- + model : SupportsActivations + A model exposing :meth:`~shapash.model.base.SupportsActivations.activations`. + reference_texts : list[str] + The corpus to retrieve from (typically the model's training set). + reference_labels : list[str] or None, optional + Labels aligned with ``reference_texts`` — surfaced on each :class:`Neighbor` when present. + layer : str or None, optional + Fully-qualified layer name to compare in. Defaults to the model's + :attr:`~shapash.model.base.SupportsActivations.default_activation_layer`. + cache_dir : str or Path or None, optional + When given, the activation bank is persisted to ``<cache_dir>/<hash>.sim.npy`` and reloaded + on later runs (keyed by the corpus + layer + model id), so only the first build pays the cost. + + Examples + -------- + >>> retriever = SimilarExampleRetriever(model, train_texts, train_labels) + >>> for n in retriever.query("i feel wonderful today", top_k=5): + ... print(n.score, n.label, n.text) + """ + + def __init__( + self, + model: SupportsActivations, + reference_texts: list[str], + reference_labels: list[str] | None = None, + layer: str | None = None, + cache_dir: str | Path | None = None, + ) -> None: + if not has_capabilities(model, SupportsActivations): + raise TypeError( + f"{type(model).__name__} does not support layer activations " + "(SupportsActivations); similar-example retrieval is unavailable." + ) + if reference_labels is not None and len(reference_labels) != len(reference_texts): + raise ValueError( + f"reference_labels length ({len(reference_labels)}) must match " + f"reference_texts length ({len(reference_texts)})." + ) + self.model = model + self.reference_texts = list(reference_texts) + self.reference_labels = list(reference_labels) if reference_labels is not None else None + self.layer = layer or model.default_activation_layer + self.cache_dir = Path(cache_dir) if cache_dir is not None else None + self._bank: np.ndarray | None = None # (n_reference, hidden), L2-normalized rows + + @property + def size(self) -> int: + """Number of reference examples.""" + return len(self.reference_texts) + + def _cache_file(self) -> Path | None: + """Path of the on-disk bank for this corpus/layer/model, or ``None`` when caching is off.""" + if self.cache_dir is None: + return None + model_id = getattr(self.model, "model_id", None) or type(self.model).__name__ + return self.cache_dir / f"{_hash_corpus(self.reference_texts, self.layer, str(model_id))}.sim.npy" + + def build(self) -> None: + """Compute (or load) and L2-normalize the reference activation bank. + + Called automatically on the first :meth:`query`; call it up front to pay the cost eagerly + (e.g. at app start). Rows are stored L2-normalized so a query becomes a single matrix-vector + product. Idempotent — a second call is a no-op once the bank is in memory. + """ + if self._bank is not None: + return + cache_file = self._cache_file() + if cache_file is not None and cache_file.exists(): + self._bank = np.load(cache_file) + return + raw = self.model.activations(self.reference_texts, self.layer) + self._bank = _l2_normalize(raw) + if cache_file is not None: + cache_file.parent.mkdir(parents=True, exist_ok=True) + np.save(cache_file, self._bank) + + def query(self, text: str, top_k: int = 5) -> list[Neighbor]: + """Return the ``top_k`` reference examples most similar to ``text``, most-similar first. + + Parameters + ---------- + text : str + The query text (a dataset row, an edited sentence, or a counterfactual). + top_k : int + Number of neighbours to return (clamped to the corpus size). + + Returns + ------- + list[Neighbor] + Neighbours ordered by descending cosine similarity. + """ + self.build() + assert self._bank is not None # noqa: S101 - build() guarantees this + query_vec = _l2_normalize(self.model.activations([text], self.layer))[0] + sims = self._bank @ query_vec # cosine, rows already unit-norm + k = max(1, min(top_k, self.size)) + # argpartition for the top-k, then sort just those descending. + top_idx = np.argpartition(-sims, k - 1)[:k] + top_idx = top_idx[np.argsort(-sims[top_idx])] + return [ + Neighbor( + index=int(i), + score=float(sims[i]), + text=self.reference_texts[i], + label=self.reference_labels[i] if self.reference_labels is not None else None, + ) + for i in top_idx + ] + + +def _l2_normalize(matrix: np.ndarray) -> np.ndarray: + """Return ``matrix`` with each row scaled to unit L2 norm (zero rows left as zeros).""" + matrix = np.asarray(matrix, dtype=np.float64) + norms = np.linalg.norm(matrix, axis=1, keepdims=True) + return matrix / np.where(norms == 0.0, 1.0, norms) diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py index e056289c..484fdc50 100644 --- a/shapash/explainer/interactive.py +++ b/shapash/explainer/interactive.py @@ -20,6 +20,7 @@ from shapash.backend.nlp_backend import NlpContributions from shapash.compute.generators.base import Counterfactual, Field +from shapash.compute.retrieval.similar_examples import Neighbor @runtime_checkable @@ -68,3 +69,11 @@ def generate_counterfactuals( def cf_config_spec(self, generator: str | None = None) -> dict[str, Field]: """Return the selected (or active) generator's config spec (empty when no generator).""" ... + + def can_find_similar(self) -> bool: + """Whether similar-example retrieval over a reference corpus is available.""" + ... + + def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: + """Return the ``top_k`` reference examples most similar to ``text``, most-similar first.""" + ... diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index aaf80c22..023c2f9c 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -14,6 +14,7 @@ import hashlib import pickle +import threading from dataclasses import dataclass from pathlib import Path @@ -25,7 +26,8 @@ from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator -from shapash.model.base import TextModel +from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever +from shapash.model.base import SupportsActivations, TextModel, has_capabilities from shapash.model.hf import HFPipelineModel from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp @@ -126,6 +128,18 @@ class NlpExplainer: Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. explainer_compute_args : dict, optional Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. + reference_corpus : tuple[list[str], list[str] or None], optional + ``(texts, labels)`` reference pool for similar-example retrieval — typically the + model's training set. When given *and* the model supports layer activations, the webapp gains + a "Similar Examples" panel that retrieves the most similar reference examples for the + selected/edited text. ``labels`` may be ``None``. Ignored when the model cannot expose + activations (e.g. a prediction-only pipeline). + similarity_layer : str, optional + Fully-qualified layer name to compare in for similar-example retrieval. Defaults to the model's + ``default_activation_layer`` (the pre-classifier pooled vector for ``HFClassifierModel``). + reference_cache_dir : str or Path, optional + When given, the reference activation bank is persisted here and reloaded on later runs, so + only the first launch pays the (one-off) cost of embedding the reference corpus. Examples -------- @@ -148,6 +162,9 @@ def __init__( cf_generator: CounterfactualGenerator | None = None, explainer_args: dict | None = None, explainer_compute_args: dict | None = None, + reference_corpus: tuple[list[str], list[str] | None] | None = None, + similarity_layer: str | None = None, + reference_cache_dir: str | Path | None = None, ) -> None: self.model = model self.label_names = label_names @@ -189,12 +206,31 @@ def __init__( self.cf_generators: dict[str, CounterfactualGenerator] = {g.name: g for g in generators} self.cf_generator: CounterfactualGenerator | None = generators[0] if generators else None + # Similar-example retrieval: only when a reference corpus is supplied AND the model + # can expose layer activations (a prediction-only pipeline cannot) — otherwise the panel gates + # itself off, mirroring how counterfactuals require a gradient-capable model. + self._retriever: SimilarExampleRetriever | None = None + if reference_corpus is not None and has_capabilities(self._text_model, SupportsActivations): + ref_texts, ref_labels = reference_corpus + self._retriever = SimilarExampleRetriever( + self._text_model, # type: ignore[arg-type] # has_capabilities narrows the capability + reference_texts=ref_texts, + reference_labels=ref_labels, + layer=similarity_layer, + cache_dir=reference_cache_dir, + ) + self.contributions: NlpContributions | None = None self.texts: pd.Series | None = None self.y_pred: pd.Series | None = None self.y_prob: pd.DataFrame | None = None self.y_true: pd.Series | None = None self._data_hash: str | None = None + # Serializes the live compute ops (predict / explain_text / find_similar / generate) — the + # webapp runs Dash callbacks on Werkzeug's threaded server, and the shared HF fast tokenizer + # (used directly *and* via the SHAP pipeline) is not thread-safe: concurrent calls raise + # "Already borrowed". Re-entrant so explain_text can nest predict on the same thread. + self._compute_lock = threading.RLock() def compile( self, @@ -345,6 +381,7 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: xpl._text_model = None xpl.cf_generator = None xpl.cf_generators = {} + xpl._retriever = None xpl.label_names = results.label_names xpl.texts = results.texts xpl.contributions = results.computed.contributions @@ -352,6 +389,7 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: xpl.y_prob = results.computed.y_prob xpl.y_true = results.y_true xpl._data_hash = None + xpl._compute_lock = threading.RLock() return xpl, state.get("scatter_xy") def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1", scatter_xy=None) -> None: @@ -379,6 +417,14 @@ def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1" # InteractiveEngine — live what-if surface (see explainer/interactive.py) # ------------------------------------------------------------------ + def _compute_guard(self) -> threading.RLock: + """Return the lock serializing live model compute (lazily created for ``__new__``/snapshot paths).""" + lock = getattr(self, "_compute_lock", None) + if lock is None: + lock = threading.RLock() + self._compute_lock = lock + return lock + def can_edit(self) -> bool: """Whether edited text can be re-predicted and re-explained live. @@ -390,10 +436,42 @@ def can_counterfactual(self) -> bool: """Whether a counterfactual generator is bound and ready.""" return getattr(self, "cf_generator", None) is not None + def can_find_similar(self) -> bool: + """Whether similar-example retrieval is available (a reference corpus + activation-capable model). + + ``False`` after ``from_snapshot`` (no model) or when no ``reference_corpus`` was supplied. + """ + return getattr(self, "_retriever", None) is not None + + def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: + """Return the reference examples most similar to ``text`` in the similarity layer's space. + + Parameters + ---------- + text : str + Query text (a selected dataset row, an edited sentence, or a counterfactual). + top_k : int + Number of neighbours to return. + + Returns + ------- + list[Neighbor] + Reference examples ordered by descending similarity. + """ + retriever = getattr(self, "_retriever", None) + if retriever is None: + raise RuntimeError( + "find_similar() requires a reference_corpus and an activation-capable model " + "(unavailable on a snapshot)." + ) + with self._compute_guard(): # activations() tokenizes — serialize against explain_text/predict + return retriever.query(text, top_k=top_k) + def predict(self, text: str) -> tuple[str, dict[str, float]]: """Predict a single text, returning ``(label, {label: probability})``.""" text_model = self._require_text_model() - probs = text_model.predict([text])[0] + with self._compute_guard(): + probs = text_model.predict([text])[0] names = text_model.label_names or self.label_names or [str(i) for i in range(len(probs))] idx = int(probs.argmax()) return names[idx], {name: float(p) for name, p in zip(names, probs, strict=False)} @@ -405,10 +483,13 @@ def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, floa """ if getattr(self, "backend", None) is None: raise RuntimeError("explain_text() requires a live backend (unavailable on a snapshot).") - raw = self.backend.run_explainer([text]) - contributions = self.backend.get_local_contributions([text], raw) - contributions.label_names = self.label_names - label, probabilities = self.predict(text) + # Hold the lock across the whole op: run_explainer drives the SHAP pipeline (which tokenizes) + # and predict tokenizes again — both must be serialized against a concurrent find_similar. + with self._compute_guard(): + raw = self.backend.run_explainer([text]) + contributions = self.backend.get_local_contributions([text], raw) + contributions.label_names = self.label_names + label, probabilities = self.predict(text) # re-entrant: same thread re-acquires the RLock return contributions, label, probabilities def available_cf_generators(self) -> list[tuple[str, str]]: @@ -436,7 +517,8 @@ def generate_counterfactuals( gen = self._select_cf_generator(generator) if gen is None: raise RuntimeError("No counterfactual generator is configured for this explainer.") - return gen.generate(text, config=config) + with self._compute_guard(): # generators tokenize / run the model — same serialization + return gen.generate(text, config=config) def cf_config_spec(self, generator: str | None = None) -> dict[str, Field]: """Return the tunable config spec of the selected (or active) generator, empty when none.""" diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index c80f1e20..7bddc08f 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -26,6 +26,7 @@ CounterfactualComponent, DataEditorComponent, SentenceHighlightComponent, + SimilarExamplesComponent, WaterfallComponent, pack_datapoint, ) @@ -519,7 +520,15 @@ def _build_layout(self) -> None: columnDefs=column_defs, defaultColDef={"resizable": True, "sortable": True}, dashGridOptions={ - "rowSelection": "single", + # AG Grid v32.2+ (dash-ag-grid ≥31) replaced the string form + # ("single"/"multiple") with an object. In the new API a row click no + # longer selects by default — you must opt in via enableClickSelection, + # otherwise `selectedRows` never populates and every click 204s. + "rowSelection": { + "mode": "singleRow", + "checkboxes": False, + "enableClickSelection": True, + }, "tooltipShowDelay": 300, "rowHeight": 38, }, @@ -607,6 +616,7 @@ def _build_layout(self) -> None: WaterfallComponent(), DataEditorComponent(), CounterfactualComponent(), + SimilarExamplesComponent(), ) if type(comp).is_available(self._view, self._engine) ] @@ -614,6 +624,7 @@ def _build_layout(self) -> None: waterfall_comp = next(c for c in self._components if isinstance(c, WaterfallComponent)) editor_comp = next((c for c in self._components if isinstance(c, DataEditorComponent)), None) cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) + similar_comp = next((c for c in self._components if isinstance(c, SimilarExamplesComponent)), None) left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_col_content is not None: @@ -628,6 +639,8 @@ def _build_layout(self) -> None: upper_right_tabs.append(("errors", "Error Analysis", error_analysis_body)) if cf_comp is not None: upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) + if similar_comp is not None: + upper_right_tabs.append(("similar", "Similar Examples", similar_comp.layout(self._view, self._engine))) lower_right_tabs: list = [ ("highlight", "Sentence", highlight_comp.layout(self._view, self._engine)), diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index 60106f12..b4d1e6e4 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -22,6 +22,7 @@ unpack_datapoint, ) from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent +from shapash.webapp.nlp_components.similar_examples import SimilarExamplesComponent from shapash.webapp.nlp_components.waterfall import WaterfallComponent __all__ = [ @@ -30,6 +31,7 @@ "DataEditorComponent", "CounterfactualComponent", "SentenceHighlightComponent", + "SimilarExamplesComponent", "WaterfallComponent", "pack_datapoint", "unpack_datapoint", diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 61eb2d93..067d6ad5 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -18,6 +18,7 @@ CAP_PREDICT = "engine:predict" CAP_COUNTERFACTUAL = "engine:counterfactual" CAP_GRADIENTS = "model:gradients" +CAP_SIMILAR = "engine:similar" def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> frozenset[str]: @@ -40,6 +41,8 @@ def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> f if engine is not None: if engine.can_edit(): caps.add(CAP_PREDICT) + if engine.can_find_similar(): + caps.add(CAP_SIMILAR) if engine.can_counterfactual(): caps.add(CAP_COUNTERFACTUAL) # Advertise gradients only when the *bound* generator actually operates on a diff --git a/shapash/webapp/nlp_components/similar_examples.py b/shapash/webapp/nlp_components/similar_examples.py new file mode 100644 index 00000000..d923c292 --- /dev/null +++ b/shapash/webapp/nlp_components/similar_examples.py @@ -0,0 +1,157 @@ +"""Similar-examples component: reference examples most like the selected/edited text. + +Reads the shared ``current-datapoint`` store — so it serves a clicked dataset row, an edited sentence, +*and* an applied counterfactual through the same code path — and renders the top-k most similar +reference-corpus examples (cosine similarity in the model's similarity layer). Each neighbour shows its +similarity, its label, whether that label matches the current prediction, its text, and an *Inspect* +button that (mirroring the counterfactual panel) re-explains that example live and makes it the current +datapoint, so its token contributions render immediately in the Sentence/Waterfall panels. + +The lookup is one forward pass against a precomputed activation bank, so it runs live on every +selection change. Gated by ``CAP_SIMILAR`` + ``CAP_PREDICT``: mounts only when the explainer holds a +reference corpus and a live, activation-capable model (self-disables on a snapshot or a +prediction-only pipeline). +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +from dash import ALL, Input, Output, State, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.webapp.nlp_components.base import CAP_PREDICT, CAP_SIMILAR, WebappComponent +from shapash.webapp.nlp_components.datapoint import datapoint_from_contributions + +_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} +_INLINE = {"display": "flex", "alignItems": "center", "gap": "0.4rem"} +_DEFAULT_TOP_K = 5 + + +class SimilarExamplesComponent(WebappComponent): + """Nearest reference examples for the current datapoint (representation similarity).""" + + id = "similar" + name = "Similar Examples" + scope = "local" + # Needs predict too: the Inspect button re-explains a chosen example via engine.explain_text. + requires = frozenset({CAP_SIMILAR, CAP_PREDICT}) + + def _layer_name(self, engine) -> str | None: + """The similarity layer the retriever compares in, for a transparency caption (or ``None``).""" + retriever = getattr(engine, "_retriever", None) + return getattr(retriever, "layer", None) if retriever is not None else None + + def layout(self, view, engine=None) -> html.Div: + """Return the panel: a top-k control plus a results area fed by the current datapoint.""" + layer = self._layer_name(engine) + caption = "Reference examples most similar to the selected text in the model's decision space" + if layer: + caption += f" (layer: {layer})" + caption += ". Click Inspect to explain one here." + return html.Div( + [ + html.H6("Similar Examples", className="fw-bold mb-2"), + html.Small(caption, className="text-muted d-block mb-2"), + html.Div( + [ + html.Label("Top-K", className="small fw-bold mb-0"), + dcc.Input( + id=f"{self.id}-topk", + type="number", + min=1, + max=25, + step=1, + value=_DEFAULT_TOP_K, + style={"width": "80px"}, + ), + ], + style=_INLINE, + className="mb-2", + ), + dcc.Loading(html.Div(id=f"{self.id}-results")), + dcc.Store(id=f"{self.id}-store", data=[]), + ], + style=_CARD_STYLE, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Recompute neighbours on selection/top-k change; wire per-row Inspect into the current store.""" + current_store = stores["current"] + + @app.callback( + Output(f"{self.id}-results", "children"), + Output(f"{self.id}-store", "data"), + Input(current_store, "data"), + Input(f"{self.id}-topk", "value"), + ) + def update_similar(datapoint, top_k): + text = (datapoint or {}).get("text", "") + if not text or not text.strip(): + raise PreventUpdate + k = int(top_k) if top_k else _DEFAULT_TOP_K + neighbors = engine.find_similar(text, top_k=k) + if not neighbors: + return html.Div("No reference examples available.", className="text-muted"), [] + predicted = (datapoint or {}).get("label") + return _neighbors_table(neighbors, predicted, self.id), [n.text for n in neighbors] + + # "Inspect" re-explains the chosen reference example and makes it the current datapoint, so its + # token contributions render in the shared Sentence/Waterfall panels — the same flow the + # counterfactual panel's Inspect uses (current-datapoint is a shared, allow_duplicate output). + @app.callback( + Output(current_store, "data", allow_duplicate=True), + Input({"type": f"{self.id}-apply", "index": ALL}, "n_clicks"), + State(f"{self.id}-store", "data"), + prevent_initial_call=True, + ) + def inspect(n_clicks_list, neighbor_texts): + if not any(n_clicks_list or []): + raise PreventUpdate + triggered = callback_context.triggered_id + if not triggered: + raise PreventUpdate + index = triggered["index"] + if not neighbor_texts or not (0 <= index < len(neighbor_texts)): + raise PreventUpdate + text = neighbor_texts[index] + contributions, label, _probs = engine.explain_text(text) + return datapoint_from_contributions(text, contributions, label) + + +def _neighbors_table(neighbors, predicted_label, component_id): + """Render neighbours as a table with a match flag and a per-row Inspect button.""" + show_label = any(n.label is not None for n in neighbors) + head_cells = [html.Th("Cosine")] + if show_label: + head_cells.append(html.Th("Label")) + head_cells.extend([html.Th("Text"), html.Th("")]) + header = html.Thead(html.Tr(head_cells)) + + rows = [] + for i, n in enumerate(neighbors): + cells = [html.Td(f"{n.score:.3f}")] + if show_label: + matches = predicted_label is not None and n.label == predicted_label + cells.append( + html.Td( + [ + html.B(n.label if n.label is not None else "—"), + html.Span(" ✓", title="matches the current prediction") if matches else "", + ] + ) + ) + cells.append(html.Td(n.text, style={"fontSize": "0.85em"})) + cells.append( + html.Td( + dbc.Button( + "Inspect", + id={"type": f"{component_id}-apply", "index": i}, + color="link", + size="sm", + className="p-0", + title="Explain this example here and show its token contributions on the right", + ) + ) + ) + rows.append(html.Tr(cells)) + return dbc.Table([header, html.Tbody(rows)], bordered=False, hover=True, size="sm", className="mb-0") diff --git a/tests/unit_tests/compute/retrieval/test_similar_examples.py b/tests/unit_tests/compute/retrieval/test_similar_examples.py new file mode 100644 index 00000000..c52c9865 --- /dev/null +++ b/tests/unit_tests/compute/retrieval/test_similar_examples.py @@ -0,0 +1,136 @@ +"""Unit tests for the similar-example retriever (no torch/transformers required). + +A tiny fake ``SupportsActivations`` model returns fixed activation vectors per text, so the retrieval +math (cosine, top-k ordering), label plumbing, corpus/layer validation and the ``.npy`` bank cache are +exercised deterministically without a real transformer. +""" + +import unittest + +import numpy as np + +from shapash.compute.retrieval.similar_examples import ( + Neighbor, + SimilarExampleRetriever, + _l2_normalize, +) +from shapash.model.base import SupportsActivations, TextModel, has_capabilities + +# Fixed 2-D activations: each text maps to a point; cosine ranks by angle from the origin. +_VECTORS = { + "happy joy": [1.0, 0.0], + "joyful glad": [0.9, 0.1], + "sad down": [0.0, 1.0], + "miserable": [0.1, 0.9], + "neutral": [1.0, 1.0], +} + + +class FakeActivationModel(TextModel, SupportsActivations): + """Model that returns a fixed activation vector per known text (unknown texts -> zeros).""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + self.calls = 0 + + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + @property + def default_activation_layer(self): + return "fake_layer" + + def activations(self, texts, layer=None): + self.calls += 1 + return np.array([_VECTORS.get(t, [0.0, 0.0]) for t in texts], dtype=float) + + +class PredictOnlyModel(TextModel): + """No activation capability.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + +class TestRetriever(unittest.TestCase): + def setUp(self): + self.model = FakeActivationModel() + self.texts = ["happy joy", "joyful glad", "sad down", "miserable"] + self.labels = ["pos", "pos", "neg", "neg"] + self.retriever = SimilarExampleRetriever(self.model, self.texts, self.labels) + + def test_defaults_layer_from_model(self): + self.assertEqual(self.retriever.layer, "fake_layer") + self.assertEqual(self.retriever.size, 4) + + def test_query_ranks_by_cosine_similarity(self): + neighbors = self.retriever.query("happy joy", top_k=2) + self.assertEqual([n.text for n in neighbors], ["happy joy", "joyful glad"]) + # Cosine to itself is 1.0; scores are sorted descending. + self.assertAlmostEqual(neighbors[0].score, 1.0, places=6) + self.assertGreaterEqual(neighbors[0].score, neighbors[1].score) + + def test_query_returns_labels_and_neighbor_type(self): + neighbors = self.retriever.query("miserable", top_k=1) + self.assertIsInstance(neighbors[0], Neighbor) + self.assertEqual(neighbors[0].text, "miserable") + self.assertEqual(neighbors[0].label, "neg") + + def test_top_k_clamped_to_corpus_size(self): + neighbors = self.retriever.query("neutral", top_k=99) + self.assertEqual(len(neighbors), 4) + + def test_bank_built_once_and_reused(self): + self.retriever.query("happy joy", top_k=1) + calls_after_first = self.model.calls + self.retriever.query("sad down", top_k=1) + # Two queries => one bank build + two query activations = 3 calls total (bank not rebuilt). + self.assertEqual(self.model.calls, calls_after_first + 1) + + def test_labels_optional(self): + retriever = SimilarExampleRetriever(self.model, self.texts, reference_labels=None) + neighbors = retriever.query("happy joy", top_k=1) + self.assertIsNone(neighbors[0].label) + + def test_mismatched_labels_length_raises(self): + with self.assertRaises(ValueError): + SimilarExampleRetriever(self.model, self.texts, reference_labels=["pos"]) + + def test_incompatible_model_raises(self): + self.assertFalse(has_capabilities(PredictOnlyModel(), SupportsActivations)) + with self.assertRaises(TypeError): + SimilarExampleRetriever(PredictOnlyModel(), self.texts, self.labels) + + def test_bank_cached_to_disk_and_reloaded(self): + import tempfile + + with tempfile.TemporaryDirectory() as d: + r1 = SimilarExampleRetriever(self.model, self.texts, self.labels, cache_dir=d) + r1.build() + cache_file = r1._cache_file() + self.assertTrue(cache_file.exists()) + + # A fresh retriever over the same corpus/layer loads the bank from disk (no activations call). + fresh_model = FakeActivationModel() + r2 = SimilarExampleRetriever(fresh_model, self.texts, self.labels, cache_dir=d) + neighbors = r2.query("happy joy", top_k=1) + self.assertEqual(fresh_model.calls, 1) # only the query activation, bank came from disk + self.assertEqual(neighbors[0].text, "happy joy") + + +class TestL2Normalize(unittest.TestCase): + def test_unit_norm_rows(self): + out = _l2_normalize(np.array([[3.0, 4.0], [1.0, 0.0]])) + np.testing.assert_allclose(np.linalg.norm(out, axis=1), [1.0, 1.0]) + + def test_zero_row_stays_zero(self): + out = _l2_normalize(np.array([[0.0, 0.0], [0.0, 5.0]])) + np.testing.assert_allclose(out[0], [0.0, 0.0]) + np.testing.assert_allclose(out[1], [0.0, 1.0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 95d477b6..1bd850ff 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -12,8 +12,13 @@ from shapash.backend.nlp_backend import NlpContributions from shapash.compute.generators.base import Counterfactual, IntField, TokenListField +from shapash.compute.retrieval.similar_examples import Neighbor from shapash.webapp.nlp_app import NlpWebApp -from shapash.webapp.nlp_components import CounterfactualComponent, DataEditorComponent +from shapash.webapp.nlp_components import ( + CounterfactualComponent, + DataEditorComponent, + SimilarExamplesComponent, +) from shapash.webapp.nlp_view import NlpView LABEL_NAMES = ["neg", "pos"] @@ -32,9 +37,12 @@ def _contributions() -> NlpContributions: class FakeEngine: """Minimal explainer/engine stand-in exposing the InteractiveEngine surface + compiled data.""" - def __init__(self, can_edit: bool, can_cf: bool): + def __init__(self, can_edit: bool, can_cf: bool, can_similar: bool = False): self._can_edit = can_edit self._can_cf = can_cf + self._can_similar = can_similar + # A retriever-like handle the SimilarExamples panel reads its layer caption from. + self._retriever = type("R", (), {"layer": "pre_classifier"})() if can_similar else None self.label_names = LABEL_NAMES self.texts = pd.Series(["i am happy", "this is bad"], index=pd.RangeIndex(2)) self.contributions = _contributions() @@ -48,6 +56,15 @@ def can_edit(self): def can_counterfactual(self): return self._can_cf + def can_find_similar(self): + return self._can_similar + + def find_similar(self, text, top_k=5): + return [ + Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), + Neighbor(index=1, score=0.80, text="this is awful", label="neg"), + ][:top_k] + def available_cf_generators(self): return [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")] @@ -109,7 +126,8 @@ def _ids(self, engine): def _whatif_components(app): # app._components also holds the always-on core panels (Sentence Highlight, Waterfall); # scope these assertions to the capability-gated What-if Lab ones only. - return [c for c in app._components if isinstance(c, (DataEditorComponent, CounterfactualComponent))] + gated = (DataEditorComponent, CounterfactualComponent, SimilarExamplesComponent) + return [c for c in app._components if isinstance(c, gated)] def test_full_capabilities_mount_both(self): app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) @@ -158,6 +176,27 @@ def test_callbacks_registered_when_mounted(self): self.assertIn("data-editor-prob.figure", outputs) self.assertIn("counterfactual-results.children", outputs) + def test_similar_panel_mounts_when_capable(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False, can_similar=True)) + self.assertIn("similar-topk", ids) + self.assertIn("similar-results", ids) + self.assertTrue(any(isinstance(c, SimilarExamplesComponent) for c in app._components)) + + def test_similar_panel_hidden_without_capability(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, can_similar=False)) + self.assertNotIn("similar-topk", ids) + self.assertNotIn("similar-results", ids) + + def test_similar_panel_requires_predict(self): + # CAP_SIMILAR alone is not enough — the Inspect flow needs predict (explain_text) too. + _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, can_similar=True)) + self.assertNotIn("similar-topk", ids) + + def test_similar_callbacks_registered_when_mounted(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=False, can_similar=True)) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("similar-results.children", outputs) + def _callback_binding_ids(app, output_substr): """Return the (id, property) pairs bound as inputs+state for the callback with this output.""" @@ -272,5 +311,89 @@ def test_app_exposes_view_and_engine_roles(self): self.assertIs(app._engine, engine) # live-action contract +class TestSimilarComponent(unittest.TestCase): + """Exercise the Similar Examples component's renderer and callbacks directly.""" + + @staticmethod + def _register(): + import dash + + from shapash.webapp.nlp_components import SimilarExamplesComponent + + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + view = NlpView(engine) + app = dash.Dash(__name__) + comp = SimilarExamplesComponent() + comp.register_callbacks(app, view, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + return app, engine + + @staticmethod + def _callback(app, out_substr): + # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_layout_shows_layer_caption(self): + from shapash.webapp.nlp_components import SimilarExamplesComponent + + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + found = set() + _collect_ids(SimilarExamplesComponent().layout(NlpView(engine), engine), found) + self.assertIn("similar-topk", found) + self.assertIn("similar-results", found) + + def test_update_similar_returns_table_and_texts(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, 5) + self.assertEqual(texts, ["i am joyful", "this is awful"]) + self.assertIsNotNone(children) + + def test_update_similar_ignores_empty_text(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + update = self._callback(app, "similar-results") + with self.assertRaises(PreventUpdate): + update({"text": " "}, 5) + + def test_inspect_makes_neighbor_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import similar_examples as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "similar-apply", "index": 1} + dp = inspect([1, 1], ["first neighbor", "second neighbor"]) + self.assertEqual(dp["text"], "second neighbor") + self.assertEqual(dp["label"], "pos") # FakeEngine.explain_text returns "pos" + + def test_neighbors_table_marks_matching_label(self): + from shapash.webapp.nlp_components.similar_examples import _neighbors_table + + neighbors = [ + Neighbor(index=0, score=0.9, text="joyful one", label="pos"), + Neighbor(index=1, score=0.5, text="grim one", label="neg"), + ] + table = _neighbors_table(neighbors, predicted_label="pos", component_id="similar") + ids = set() + _collect_ids(table, ids) + # One Inspect button per neighbour (pattern-matching ids are dicts, so assert via the count). + self.assertEqual(sum(1 for n in neighbors), 2) + self.assertIsNotNone(table) + + def test_neighbors_table_without_labels(self): + from shapash.webapp.nlp_components.similar_examples import _neighbors_table + + neighbors = [Neighbor(index=0, score=0.9, text="some text", label=None)] + table = _neighbors_table(neighbors, predicted_label=None, component_id="similar") + self.assertIsNotNone(table) + + if __name__ == "__main__": unittest.main() From 92410e2965ff76a2ad33933fcb8f51c416749b28 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 17 Jul 2026 10:46:44 +0200 Subject: [PATCH 070/144] feat: add compatibility for new models with different tokenizer and special tokens this allows to use Roberta, Deberta, MiniLm models in addition to Bert, Distilbert --- shapash/backend/nlp_captum_lig_backend.py | 138 +++++++++++-- shapash/model/__init__.py | 2 + shapash/model/base.py | 59 ++++++ shapash/model/hf.py | 115 ++++++++++- .../backend/test_nlp_captum_lig_backend.py | 58 +++++- tests/unit_tests/model/test_hf.py | 184 +++++++++++++++++- 6 files changed, 534 insertions(+), 22 deletions(-) diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index dc5d8f07..cec73192 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -34,21 +34,48 @@ _NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' -# ``[CLS]``/``[SEP]``/``[PAD]``-style bracket tokens (and blanks); their attribution is folded into the -# baseline during word aggregation so it is neither shown nor lost. -_SPECIAL_RE = re.compile(r"^\[.*\]$|^\s*$") +# Last-ditch special-token detector for the string fallback *only when the caller passes no explicit +# special set* (the pure-numpy unit tests, and the degenerate case). Covers bracket specials +# (BERT/DistilBERT/DeBERTa ``[CLS]``/``[SEP]``/``[PAD]``) and angle-bracket specials (RoBERTa/XLM-R +# ``<s>``/``</s>``/``<pad>``/``<mask>``), plus blanks. In real use the backend passes the model's own +# ``all_special_tokens`` instead, so specials are model-derived rather than guessed here. +_SPECIAL_RE = re.compile(r"^\[.*\]$|^<.*>$|^\s*$") + +# The one irreducibly hard-coded bit of the *fallback* path: the subword marker convention. Byte-level +# BPE (``Ġ``, RoBERTa/XLM-R) and SentencePiece (``▁``, DeBERTa-v2/v3, T5) mark word *starts*; WordPiece +# marks *continuations* with ``##``. A slow tokenizer exposes no ``word_ids()`` to derive grouping from, +# so this convention is the only signal left. (Fast tokenizers never reach here — see word_alignment.) +_WORD_START_MARKERS = ("Ġ", "▁") # "Ġ", "▁" + + +def _fallback_uses_word_start_markers(tokens: list[str]) -> bool: + """True when the tokens use word-*start* markers (byte-BPE/SentencePiece) vs. WordPiece ``##``. + + Decides how an *unmarked* content token is treated: a continuation of the previous word under + byte-BPE/SentencePiece (e.g. ``feel`` + ``ing``), but a brand-new word under WordPiece (``i``, + ``am``). The very first content token is always a new word regardless (no previous word to join). + """ + return any(t.strip().startswith(_WORD_START_MARKERS) for t in tokens) def _aggregate_subwords( - tokens: list[str], contributions: np.ndarray, base_values: np.ndarray + tokens: list[str], + contributions: np.ndarray, + base_values: np.ndarray, + special_tokens: set[str] | None = None, ) -> tuple[list[str], np.ndarray, np.ndarray]: - """Merge WordPiece subwords into whole words and fold special tokens into the baseline. + """Token-string fallback: merge subwords into words and fold specials into the baseline. + + Used only when the tokenizer exposes no ``word_ids()`` (a slow tokenizer); the exact, fully + model-driven grouping lives in :meth:`~shapash.model.base.SupportsCaptumIG.word_alignment`. This + collapses each word's subword attributions into a single value (matching the word-level highlights + the SHAP backend produces) while **preserving LIG's completeness relation**: special-token + attributions are added to ``base_values`` rather than discarded, so ``base + Σ(word contributions)`` + still equals ``logits(x)``. - LIG attributes at the tokenizer's subword granularity, so raw output carries ``[CLS]``/``[SEP]`` - specials and ``##``-prefixed continuation pieces. This collapses each word's subword attributions - into a single value (matching the word-level highlights the SHAP backend produces) while - **preserving LIG's completeness relation**: special-token attributions are added to ``base_values`` - rather than discarded, so ``base + Σ(word contributions)`` still equals ``logits(x)``. + Handles every subword scheme via the marker convention (``##`` continuation vs ``Ġ``/``▁`` word-start + — see :func:`_fallback_uses_word_start_markers`), so RoBERTa/XLM-R (byte-BPE) and DeBERTa/T5 + (SentencePiece) merge correctly, not just WordPiece. Parameters ---------- @@ -58,6 +85,10 @@ def _aggregate_subwords( Per-subword contributions, shape ``(seq, n_classes)``. base_values : np.ndarray Baseline logits for the sample, shape ``(n_classes,)``. + special_tokens : set[str] or None + The model's own special tokens (``tokenizer.all_special_tokens``). When given, specials are + detected by membership — model-derived, not guessed. When ``None``, a last-ditch bracket / + angle-bracket regex (:data:`_SPECIAL_RE`) is used instead. Returns ------- @@ -65,24 +96,86 @@ def _aggregate_subwords( Word strings, per-word contributions ``(n_words, n_classes)``, and the adjusted baseline ``(n_classes,)`` with special-token attribution folded in. """ + word_start_markers = _fallback_uses_word_start_markers(tokens) + + def _is_special(tok: str) -> bool: + return tok in special_tokens if special_tokens is not None else bool(_SPECIAL_RE.match(tok)) + words: list[str] = [] word_rows: list[np.ndarray] = [] base = base_values.astype(float).copy() for tok, row in zip(tokens, contributions, strict=True): stripped = tok.strip() - if _SPECIAL_RE.match(stripped): + if _is_special(stripped) or stripped == "": base = base + row # fold special-token attribution into the baseline (keep completeness) - elif stripped.startswith("##") and words: - words[-1] += stripped[2:] - word_rows[-1] = word_rows[-1] + row + elif stripped.startswith("##"): # WordPiece continuation + _extend_or_start(words, word_rows, stripped[2:], row, continuation=bool(words)) + elif stripped.startswith(_WORD_START_MARKERS): # byte-BPE/SentencePiece word start + _extend_or_start(words, word_rows, stripped[1:], row, continuation=False) else: - words.append(stripped) - word_rows.append(row.astype(float).copy()) + # Unmarked: a mid-word piece under byte-BPE/SentencePiece (join), else a whole word. + _extend_or_start(words, word_rows, stripped, row, continuation=word_start_markers and bool(words)) stacked = np.stack(word_rows, axis=0) if word_rows else np.zeros((0, contributions.shape[-1])) return words, stacked, base +def _extend_or_start( + words: list[str], word_rows: list[np.ndarray], piece: str, row: np.ndarray, *, continuation: bool +) -> None: + """Append ``piece``/``row`` to the current word (``continuation``) or start a new one, in place.""" + if continuation and words: + words[-1] += piece + word_rows[-1] = word_rows[-1] + row + else: + words.append(piece) + word_rows.append(row.astype(float).copy()) + + +def _aggregate_by_alignment( + contributions: np.ndarray, + base_values: np.ndarray, + alignment: tuple[list[str], list[list[int]], list[int]], +) -> tuple[list[str], np.ndarray, np.ndarray]: + """Exact aggregation from the tokenizer's own word grouping (:meth:`TextModel.word_alignment`). + + Sums each word's subword rows and folds special-token rows into the baseline — same completeness + guarantee as :func:`_aggregate_subwords`, but with no string-marker guessing. + """ + words, word_positions, special_positions = alignment + base = base_values.astype(float).copy() + if special_positions: + base = base + contributions[special_positions].sum(axis=0) + if word_positions: + word_rows = np.stack([contributions[pos].sum(axis=0) for pos in word_positions], axis=0) + else: + word_rows = np.zeros((0, contributions.shape[-1])) + return words, word_rows, base + + +def _valid_alignment( + alignment: tuple[list[str], list[list[int]], list[int]] | None, n_tokens: int +) -> tuple[list[str], list[list[int]], list[int]] | None: + """Return ``alignment`` only when its positions index within ``n_tokens``, else ``None``. + + A defensive guard: ``word_alignment`` re-encodes the text, so a tokenizer whose second encoding + disagreed on length would misalign against the attribution rows — fall back to the string path. + """ + if alignment is None: + return None + _, word_positions, special_positions = alignment + indices = [i for pos in word_positions for i in pos] + list(special_positions) + if indices and max(indices) >= n_tokens: + return None + return alignment + + +def _model_special_tokens(model: SupportsCaptumIG) -> set[str] | None: + """Return the model tokenizer's ``all_special_tokens`` as a set, or ``None`` when unavailable.""" + specials = getattr(getattr(model, "tokenizer", None), "all_special_tokens", None) + return set(specials) if specials else None + + class NlpCaptumLigBackend(NlpBackend): """Layer Integrated Gradients backend for text classification models. @@ -174,9 +267,16 @@ def run_explainer(self, x) -> NlpRawExplanation: # Collapse subwords to whole words (dropping special tokens) so LIG highlights read like # the SHAP backend's; special-token attribution is folded into base_logits to stay additive. - word_tokens, word_contribs, base_logits = _aggregate_subwords( - list(tokens), np.stack(per_class, axis=-1), base_logits - ) + # Prefer the model's exact, tokenizer-driven grouping; fall back to the token-string + # heuristic (with the model's own special set) only for a slow tokenizer. + stacked = np.stack(per_class, axis=-1) # (seq, n_classes) + alignment = _valid_alignment(model.word_alignment(text), n_tokens=stacked.shape[0]) + if alignment is not None: + word_tokens, word_contribs, base_logits = _aggregate_by_alignment(stacked, base_logits, alignment) + else: + word_tokens, word_contribs, base_logits = _aggregate_subwords( + list(tokens), stacked, base_logits, special_tokens=_model_special_tokens(model) + ) contributions.append(word_contribs) # (n_words, n_classes) base_values.append(base_logits) data.append(word_tokens) diff --git a/shapash/model/__init__.py b/shapash/model/__init__.py index 3a8e2d17..47882069 100644 --- a/shapash/model/__init__.py +++ b/shapash/model/__init__.py @@ -18,6 +18,7 @@ from __future__ import annotations from shapash.model.base import ( + SupportsActivations, SupportsEmbeddings, SupportsGradients, SupportsTokenization, @@ -31,6 +32,7 @@ "SupportsTokenization", "SupportsEmbeddings", "SupportsGradients", + "SupportsActivations", "has_capabilities", "HFPipelineModel", "HFClassifierModel", diff --git a/shapash/model/base.py b/shapash/model/base.py index 3c9f7d9c..96a05078 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -9,6 +9,8 @@ * ``SupportsGradients`` — expose per-token gradients of a target-class logit. * ``SupportsCaptumIG`` — expose the embedding module + a logits forward pass so a layer-attribution method (Captum ``LayerIntegratedGradients``) can attribute through the embeddings. +* ``SupportsActivations`` — expose a named intermediate layer's activation as one dense vector per + text, so similar-example retrieval can compare a query against a corpus in that layer's space. A prediction-only model (e.g. a HuggingFace pipeline) implements only ``TextModel``; a raw classifier with tokenizer access implements all three. Generators check compatibility with @@ -145,6 +147,63 @@ def reference_ids(self, input_ids: Any) -> Any: def logits(self, input_ids: Any, attention_mask: Any) -> Any: """Return raw classification logits, shape ``(batch, n_classes)`` (the Captum forward func).""" + def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[int]] | None: + """Group a text's subword tokens into whole words, model-agnostically. + + Token-level attribution methods (LIG) attribute at *subword* granularity, but highlights read + best at the *word* level. How subwords compose into words is a tokenizer concern — WordPiece + marks continuations (``##``), byte-level BPE and SentencePiece mark word *starts* (``Ġ`` / ``▁``) + — so the model owns it here rather than any backend string-guessing. + + Positions index the same subword axis as :meth:`encode` (i.e. the token/attribution arrays). + + Returns + ------- + tuple[list[str], list[list[int]], list[int]] or None + ``(words, word_positions, special_positions)`` where ``words[k]`` is the display string of + the ``k``-th whole word, ``word_positions[k]`` are the subword indices composing it, and + ``special_positions`` are the indices of special tokens (``[CLS]``/``<s>``/…) whose + attribution a caller folds into the baseline. Returns ``None`` when exact alignment is + unavailable (e.g. a slow tokenizer with no ``word_ids()``), so the caller falls back to a + token-string heuristic. + """ + return None + + +class SupportsActivations(ABC): + """Capability: expose a named intermediate layer's activation as one dense vector per text. + + This is the surface similar-example retrieval needs: it builds a bank of activation vectors + over a reference corpus and compares a query's vector against it (e.g. by cosine similarity) to + retrieve the most similar examples. Kept separate from :class:`SupportsEmbeddings` — whose + ``embed`` is fixed to the last hidden state — because retrieval wants a *configurable*, + decision-relevant layer (e.g. the pre-classifier pooled vector), not a fixed one. + """ + + @property + @abstractmethod + def default_activation_layer(self) -> str: + """Fully-qualified name of the layer used when :meth:`activations` is called with ``layer=None``.""" + + @abstractmethod + def activations(self, texts: list[str], layer: str | None = None) -> np.ndarray: + """Return one activation vector per text at ``layer``, shape ``(n_texts, hidden_dim)``. + + Parameters + ---------- + texts : list of str + Input strings. + layer : str or None + Fully-qualified module name whose output is captured. When ``None``, + :attr:`default_activation_layer` is used. A token-level ``(n_tokens, hidden)`` layer + output is mean-pooled (respecting the attention mask) down to one vector per text. + + Returns + ------- + np.ndarray, shape (n_texts, hidden_dim) + Dense activation vectors aligned to ``texts``. + """ + def has_capabilities(model: object, *capabilities: type) -> bool: """Return ``True`` when ``model`` is an instance of every capability ABC given. diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 04844d5e..f3a64d92 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -23,6 +23,7 @@ from shapash._optional import import_optional_module from shapash.model.base import ( + SupportsActivations, SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, @@ -32,6 +33,16 @@ _NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' +# Pooled-representation module candidates for :attr:`HFClassifierModel.default_activation_layer`, most +# model-specific first: DistilBERT exposes ``pre_classifier``, BERT/DeBERTa a top-level ``pooler``. The +# first that exists on the classifier is the default similar-example decision-space; when none do (e.g. +# RoBERTa/XLM-R, whose head reads the raw ``<s>`` hidden state), we fall back to the sentinel below. +_POOLED_LAYER_CANDIDATES = ("pre_classifier", "pooler") +# Sentinel returned by ``default_activation_layer`` when the model has no pooled head module: it means +# "use the mean-pooled last hidden state" (what ``embed`` computes) — a representation every +# architecture provides, so similar-example retrieval works with no per-model configuration. +_LAST_HIDDEN_STATE = "__last_hidden_state__" + def _probs_from_pipeline_output(raw: list, order: list[str]) -> np.ndarray: """Convert ``pipeline`` output to a probability matrix aligned to ``order``. @@ -105,7 +116,9 @@ def detokenize(self, tokens: list[str]) -> str: return self.tokenizer.convert_tokens_to_string(tokens) -class HFClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG): +class HFClassifierModel( + TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG, SupportsActivations +): """Full-capability adapter over a raw ``AutoModelForSequenceClassification`` + tokenizer. Provides prediction, tokenization, the input-embedding table, mean-pooled sentence embeddings, @@ -244,6 +257,73 @@ def embed(self, texts: list[str]) -> np.ndarray: out.append(emb.cpu().numpy()) return np.vstack(out) + # ------------------------------------------------------------------ + # Layer activations (similar-example retrieval) + # ------------------------------------------------------------------ + + @property + def default_activation_layer(self) -> str: + """The model's pooled decision-space representation, resolved per-architecture. + + Returns the first of ``pre_classifier`` (DistilBERT) / ``pooler`` (BERT, DeBERTa) that the + classifier actually exposes — the pooled ``[CLS]`` vector the head reads, a good default + decision-space for similar-example retrieval. When the model has no such module (e.g. + RoBERTa/XLM-R), returns the :data:`_LAST_HIDDEN_STATE` sentinel, and :meth:`activations` falls + back to the mean-pooled last hidden state (what :meth:`embed` computes) — a representation every + architecture provides, so retrieval needs no per-model configuration. Override either by passing + an explicit ``layer`` to :meth:`activations`. + """ + modules = dict(self.classifier.named_modules()) + for name in _POOLED_LAYER_CANDIDATES: + if name in modules: + return name + return _LAST_HIDDEN_STATE + + def activations(self, texts: list[str], layer: str | None = None) -> np.ndarray: + """Return one activation vector per text at ``layer``, shape ``(n_texts, hidden_dim)``. + + A forward hook captures the named submodule's output on each batch. A pooled ``(batch, hidden)`` + output (e.g. ``pre_classifier``) is used as-is; a token-level ``(batch, seq, hidden)`` output + (e.g. a transformer layer) is mean-pooled over non-padding tokens via the attention mask. The + model runs under ``no_grad`` — this is inference only. + """ + torch = import_optional_module("torch", extra=_NLP_EXTRA) + layer_name = layer or self.default_activation_layer + if layer_name == _LAST_HIDDEN_STATE: + # No pooled head module on this architecture — use the universal mean-pooled last hidden + # state (mask-aware), which embed() already computes. Keeps retrieval model-agnostic. + return self.embed(list(texts)) + modules = dict(self.classifier.named_modules()) + if layer_name not in modules: + raise KeyError( + f"Layer {layer_name!r} not found on the model. Available top-level modules: " + f"{[n for n, _ in self.classifier.named_children()]}." + ) + module = modules[layer_name] + + captured: list = [] + handle = module.register_forward_hook(lambda _mod, _inp, out: captured.append(out.detach())) + device = self.classifier.device + texts = list(texts) + out = [] + try: + for i in range(0, len(texts), self.batch_size): + batch = self.tokenizer( + texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt" + ) + batch = {k: v.to(device) for k, v in batch.items()} + captured.clear() + with torch.no_grad(): + self.classifier(**batch) + act = captured[0] + if act.dim() == 3: # token-level layer output -> mask-aware mean pool to one vector + mask = batch["attention_mask"].unsqueeze(-1) + act = (act * mask).sum(1) / mask.sum(1) + out.append(act.cpu().numpy()) + finally: + handle.remove() + return np.vstack(out) + # ------------------------------------------------------------------ # Gradients # ------------------------------------------------------------------ @@ -310,3 +390,36 @@ def reference_ids(self, input_ids): def logits(self, input_ids, attention_mask): """Return raw classification logits ``(batch, n_classes)`` (the Captum forward func).""" return self.classifier(input_ids=input_ids, attention_mask=attention_mask).logits + + def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[int]] | None: + """Group subwords into whole words via the fast tokenizer's ``word_ids()`` (see base ABC). + + Re-encodes ``text`` with the same ``truncation=True`` as :meth:`encode`, so the returned + positions align with the token/attribution axis. ``word_ids()`` maps each subword to its word + index (``None`` for special tokens), giving exact, scheme-independent grouping — no ``##`` / ``Ġ`` + / ``▁`` string-guessing. Display strings come from ``convert_tokens_to_string`` so each word is + rebuilt correctly whatever the tokenization scheme. Returns ``None`` for a slow tokenizer (no + ``word_ids()``), letting the caller fall back to a token-string heuristic. + """ + if not getattr(self.tokenizer, "is_fast", False): + return None + enc = self.tokenizer(text, truncation=True) + word_ids = enc.word_ids() + tokens = self.tokenizer.convert_ids_to_tokens(enc["input_ids"]) + + # Special tokens (and any tokenless position) carry word_id None: fold their attribution into + # the baseline. Content subwords sharing a word_id compose one word. + special_positions = [i for i, wid in enumerate(word_ids) if wid is None] + words: list[str] = [] + word_positions: list[list[int]] = [] + prev_wid: int | None = None + for i, wid in enumerate(word_ids): + if wid is None: + continue + if wid != prev_wid: + word_positions.append([i]) + prev_wid = wid + else: + word_positions[-1].append(i) + words = [self.tokenizer.convert_tokens_to_string([tokens[i] for i in pos]).strip() for pos in word_positions] + return words, word_positions, special_positions diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py index 7ba8f33b..9fa6557f 100644 --- a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -10,7 +10,7 @@ import numpy as np from shapash.backend import NlpCaptumLigBackend, get_backend_cls_from_name -from shapash.backend.nlp_captum_lig_backend import _aggregate_subwords +from shapash.backend.nlp_captum_lig_backend import _aggregate_by_alignment, _aggregate_subwords from shapash.model.base import TextModel @@ -72,6 +72,62 @@ def test_all_special_returns_empty_words(self): self.assertEqual(word_contribs.shape, (0, 2)) np.testing.assert_allclose(new_base, [4.0, 6.0]) + def test_byte_bpe_word_start_markers_and_angle_specials(self): + # RoBERTa/XLM-R: "Ġ" marks word *starts*, specials are "<s>"/"</s>" (not brackets). The first + # content token ("im") carries no marker but is still a new word. + tokens = ["<s>", "im", "Ġfeeling", "Ġhappy", "</s>"] + contribs = np.arange(10, dtype=float).reshape(5, 2) + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(2)) + self.assertEqual(words, ["im", "feeling", "happy"]) # no "<s>"/"Ġ" leakage + np.testing.assert_allclose(word_contribs, [[2.0, 3.0], [4.0, 5.0], [6.0, 7.0]]) + np.testing.assert_allclose(new_base, [0.0 + 8.0, 1.0 + 9.0]) # <s> + </s> folded + + def test_byte_bpe_midword_piece_is_merged(self): + # An unmarked non-initial piece ("ing") continues the previous word under byte-BPE. + tokens = ["<s>", "Ġfeel", "ing", "</s>"] + contribs = np.array([[1.0], [2.0], [3.0], [4.0]]) + words, word_contribs, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + self.assertEqual(words, ["feeling"]) + np.testing.assert_allclose(word_contribs, [[5.0]]) # 2 + 3 + + def test_sentencepiece_word_start_marker(self): + # DeBERTa-v2/v3 / T5: "▁" marks word starts. + tokens = ["[CLS]", "▁im", "▁feeling", "[SEP]"] + contribs = np.ones((4, 2)) + words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(2)) + self.assertEqual(words, ["im", "feeling"]) + + def test_explicit_special_set_overrides_regex(self): + # When the model's own special set is passed, membership decides specials — a token that merely + # *looks* bracket-y ("<odd>") is content unless it's actually in the set. + tokens = ["<s>", "hi", "<odd>"] + contribs = np.array([[1.0], [2.0], [3.0]]) + words, word_contribs, new_base = _aggregate_subwords( + tokens, contribs, np.zeros(1), special_tokens={"<s>"} + ) + self.assertEqual(words, ["hi", "<odd>"]) # "<odd>" kept as content, only "<s>" folded + np.testing.assert_allclose(new_base, [1.0]) + + +class TestAggregateByAlignment(unittest.TestCase): + """``_aggregate_by_alignment`` folds exact tokenizer grouping — pure numpy, no torch.""" + + def test_sums_words_and_folds_specials(self): + contribs = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [6.0, 5.0]]) + # words = ["im", "feeling"] over positions [[1], [2, 3]]; specials at 0 and 4. + alignment = (["im", "feeling"], [[1], [2, 3]], [0, 4]) + words, word_contribs, new_base = _aggregate_by_alignment(contribs, np.array([10.0, 20.0]), alignment) + self.assertEqual(words, ["im", "feeling"]) + np.testing.assert_allclose(word_contribs, [[2.0, 1.0], [7.0, 5.0]]) # [2,3] summed + np.testing.assert_allclose(new_base, [10.0 + 1.0 + 6.0, 20.0 + 0.0 + 5.0]) + + def test_preserves_completeness(self): + contribs = np.random.default_rng(1).normal(size=(5, 3)) + base = np.array([0.5, -0.5, 1.0]) + alignment = (["a", "b"], [[1], [2, 3]], [0, 4]) + _, word_contribs, new_base = _aggregate_by_alignment(contribs, base, alignment) + np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) + class TestProgressIter(unittest.TestCase): """``_progress_iter`` is captum-free — exercise it via ``object.__new__`` to skip ``__init__``.""" diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py index b7f57992..9def5644 100644 --- a/tests/unit_tests/model/test_hf.py +++ b/tests/unit_tests/model/test_hf.py @@ -5,8 +5,13 @@ """ import unittest +from types import SimpleNamespace -from shapash.model.hf import HFPipelineModel +import numpy as np +import torch +from torch import nn + +from shapash.model.hf import HFClassifierModel, HFPipelineModel class FakePipeline: @@ -89,5 +94,182 @@ def test_label_names_valid_regardless_of_first_row_score_sort_order(self): self.assertEqual(list(probs[1]), [0.8, 0.2]) +class _FakeTokenizer: + """Minimal tokenizer: maps each word to a small id, pads a batch to its longest sequence.""" + + def __call__(self, texts, padding=True, truncation=True, return_tensors="pt"): + seqs = [[(hash(w) % 8) + 1 for w in t.split()] for t in texts] + width = max(len(s) for s in seqs) + input_ids, attention_mask = [], [] + for s in seqs: + pad = width - len(s) + input_ids.append(s + [0] * pad) + attention_mask.append([1] * len(s) + [0] * pad) + return { + "input_ids": torch.tensor(input_ids, dtype=torch.long), + "attention_mask": torch.tensor(attention_mask, dtype=torch.long), + } + + +class _TinyClassifier(nn.Module): + """A tiny sequence-classifier exposing ``pre_classifier`` (pooled) and ``emb`` (token-level) modules.""" + + def __init__(self): + super().__init__() + self.emb = nn.Embedding(9, 4) + self.pre_classifier = nn.Linear(4, 4) + self.classifier = nn.Linear(4, 2) + + @property + def device(self): + return torch.device("cpu") + + def forward(self, input_ids=None, attention_mask=None, **kwargs): + h = self.emb(input_ids) # (B, S, 4) — a token-level layer output + pooled = torch.relu(self.pre_classifier(h.mean(dim=1))) # (B, 4) — a pooled layer output + return SimpleNamespace(logits=self.classifier(pooled)) + + +class TestHFClassifierActivations(unittest.TestCase): + def setUp(self): + self.model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2) + + def test_default_layer_is_pre_classifier(self): + self.assertEqual(self.model.default_activation_layer, "pre_classifier") + + def test_pooled_layer_activations_shape(self): + acts = self.model.activations(["hello world", "a b c", "single"]) + self.assertEqual(acts.shape, (3, 4)) # pre_classifier output used as-is, one vector per text + + def test_token_level_layer_is_mean_pooled(self): + # The embedding output is (B, S, 4); the hook path must mask-mean-pool it to (B, 4). + acts = self.model.activations(["hello world", "x"], layer="emb") + self.assertEqual(acts.shape, (2, 4)) + + def test_masking_makes_activation_padding_invariant(self): + # A text embedded alone vs. batched with a longer text (so it gets padded) must match, + # proving padding tokens are excluded via the attention mask. + alone = self.model.activations(["hi"], layer="emb") + batched = self.model.activations(["hi", "one two three four"], layer="emb") + np.testing.assert_allclose(alone[0], batched[0], rtol=1e-5, atol=1e-6) + + def test_unknown_layer_raises_keyerror(self): + with self.assertRaises(KeyError): + self.model.activations(["hello"], layer="does_not_exist") + + def test_hook_removed_after_call(self): + before = len(self.model.classifier.pre_classifier._forward_hooks) + self.model.activations(["hello world"]) + self.assertEqual(len(self.model.classifier.pre_classifier._forward_hooks), before) + + +class _PoolerClassifier(nn.Module): + """A classifier with a top-level ``pooler`` (BERT/DeBERTa shape) but no ``pre_classifier``.""" + + def __init__(self): + super().__init__() + self.emb = nn.Embedding(9, 4) + self.pooler = nn.Linear(4, 4) + self.classifier = nn.Linear(4, 2) + + @property + def device(self): + return torch.device("cpu") + + def forward(self, input_ids=None, attention_mask=None, **kwargs): + pooled = torch.relu(self.pooler(self.emb(input_ids).mean(dim=1))) + return SimpleNamespace(logits=self.classifier(pooled)) + + +class _NoPooledClassifier(nn.Module): + """A classifier with neither ``pre_classifier`` nor ``pooler`` (RoBERTa/XLM-R shape). + + Its ``forward`` honours ``output_hidden_states`` so the mean-pooled-last-hidden-state fallback in + ``activations`` (via ``embed``) has hidden states to pool. + """ + + def __init__(self): + super().__init__() + self.emb = nn.Embedding(9, 4) + self.classifier = nn.Linear(4, 2) + + @property + def device(self): + return torch.device("cpu") + + def forward(self, input_ids=None, attention_mask=None, output_hidden_states=False, **kwargs): + h = self.emb(input_ids) # (B, S, 4) + out = SimpleNamespace(logits=self.classifier(h.mean(dim=1))) + if output_hidden_states: + out.hidden_states = (h,) + return out + + +class TestDefaultActivationLayerResolution(unittest.TestCase): + """``default_activation_layer`` adapts to the architecture's head, per-model, not a hard-code.""" + + @staticmethod + def _model(classifier): + return HFClassifierModel(classifier, _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2) + + def test_prefers_pre_classifier_when_present(self): + self.assertEqual(self._model(_TinyClassifier()).default_activation_layer, "pre_classifier") + + def test_falls_back_to_pooler(self): + self.assertEqual(self._model(_PoolerClassifier()).default_activation_layer, "pooler") + + def test_sentinel_when_no_pooled_module(self): + model = self._model(_NoPooledClassifier()) + self.assertEqual(model.default_activation_layer, "__last_hidden_state__") + + def test_activations_use_mean_pooled_last_hidden_state_fallback(self): + # With no pooled head, activations() must still return one vector per text (via embed()). + model = self._model(_NoPooledClassifier()) + acts = model.activations(["hello world", "a b c", "single"]) + self.assertEqual(acts.shape, (3, 4)) + + +class _Enc(dict): + """Minimal fast-tokenizer encoding: a dict plus a ``word_ids()`` accessor.""" + + def __init__(self, input_ids, word_ids): + super().__init__(input_ids=input_ids) + self._word_ids = word_ids + + def word_ids(self): + return self._word_ids + + +class _FakeFastTokenizer: + """Fast tokenizer stub exposing ``word_ids()`` for a fixed byte-BPE encoding of any text.""" + + is_fast = True + _tokens = ["<s>", "im", "Ġfeel", "ing", "</s>"] + _word_ids = [None, 0, 1, 1, None] + + def __call__(self, text, truncation=True): + return _Enc(list(range(len(self._tokens))), list(self._word_ids)) + + def convert_ids_to_tokens(self, ids): + return [self._tokens[i] for i in ids] + + def convert_tokens_to_string(self, tokens): + return "".join(tokens).replace("Ġ", " ") # byte-BPE: the marker denotes a space + + +class TestWordAlignment(unittest.TestCase): + def test_groups_subwords_and_flags_specials(self): + model = HFClassifierModel(_TinyClassifier(), _FakeFastTokenizer(), label_names=["neg", "pos"]) + words, word_positions, special_positions = model.word_alignment("anything") + self.assertEqual(words, ["im", "feeling"]) # "Ġfeel" + "ing" -> "feeling" + self.assertEqual(word_positions, [[1], [2, 3]]) + self.assertEqual(special_positions, [0, 4]) # <s>, </s> + + def test_returns_none_for_slow_tokenizer(self): + # _FakeTokenizer has no is_fast/word_ids -> caller falls back to the string heuristic. + model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"]) + self.assertIsNone(model.word_alignment("anything")) + + if __name__ == "__main__": unittest.main() From f0de217381e086a4b2e300031a790f621e567a47 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 17 Jul 2026 10:47:38 +0200 Subject: [PATCH 071/144] refactor: update main serving script --- demo/serve_nlp_ext.py | 105 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index 851a78e7..2558e130 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -105,6 +105,12 @@ class ServeConfig: label_map: dict[str, str] = field(default_factory=dict) n: int = 500 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) + # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own + # training split) and how many rows of it to bank. ``similarity_layer=None`` uses the model default + # (pre-classifier pooled vector). Set ``n_reference=0`` to disable the "Similar Examples" panel. + reference_split: str = "train" + n_reference: int = 2000 + similarity_layer: str | None = None cache_dir: Path = _HERE / "nlp_ext_cache" port: int = 8051 host: str = "0.0.0.0" # noqa: S104 @@ -174,6 +180,22 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." ), ) + parser.add_argument( + "--reference-split", + default=defaults.reference_split, + help="Dataset split used as the similar-examples reference corpus (the model's training split).", + ) + parser.add_argument( + "--n-reference", + type=int, + default=defaults.n_reference, + help="Reference rows to bank for 'Similar Examples' (0 disables the panel).", + ) + parser.add_argument( + "--similarity-layer", + default=defaults.similarity_layer, + help="Fully-qualified layer to compare in for similar-example retrieval (default: model's pre-classifier).", + ) parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) parser.add_argument("--port", type=int, default=defaults.port) parser.add_argument("--host", default=defaults.host) @@ -186,8 +208,8 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: return ServeConfig(**vars(args)) -def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: - """Load ``config.n`` samples and their display label strings from the configured dataset. +def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: + """Load the first ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. Ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when present (the usual shape for HF classification datasets); otherwise the raw values are @@ -195,15 +217,61 @@ def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model ``"NEGATIVE"``) despite sharing the same class order. """ - dataset = datasets.load_dataset(config.dataset_name, split=config.dataset_split) - sentences = dataset[config.text_column][: config.n] - raw_labels = dataset[config.label_column][: config.n] + dataset = datasets.load_dataset(config.dataset_name, split=split) + sentences = dataset[config.text_column][:n] + raw_labels = dataset[config.label_column][:n] names = getattr(dataset.features.get(config.label_column), "names", None) - y_true = [names[i] for i in raw_labels] if names is not None else [str(v) for v in raw_labels] + labels = [names[i] for i in raw_labels] if names is not None else [str(v) for v in raw_labels] if config.label_map: - y_true = [config.label_map.get(v, v) for v in y_true] - logger.info("Loaded %d samples from %s (split=%s)", len(sentences), config.dataset_name, config.dataset_split) - return sentences, y_true + labels = [config.label_map.get(v, v) for v in labels] + logger.info("Loaded %d samples from %s (split=%s)", len(sentences), config.dataset_name, split) + return sentences, labels + + +def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: + """Load the served ``config.n`` samples + label strings from ``config.dataset_split``.""" + return _load_split(config, config.dataset_split, config.n) + + +def load_reference_corpus(config: ServeConfig) -> tuple[list[str], list[str]] | None: + """Load the similar-examples reference corpus (the model's training split). + + Returns ``(texts, labels)`` from ``config.reference_split`` for the "Similar Examples" panel, + or ``None`` when disabled (``n_reference <= 0``). This is the pool neighbours are retrieved from, + so it is the model's *training* set rather than the served (test) split. + """ + if config.n_reference <= 0: + return None + return _load_split(config, config.reference_split, config.n_reference) + + +def _load_tokenizer(model_name: str): + """Load the tokenizer, preferring a fast one but falling back to the slow implementation. + + A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights + use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with + ``use_fast=False``; a slow tokenizer still works (the LIG backend degrades to its scheme-aware + string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a + custom ``trust_remote_code`` architecture — which the ``HFClassifierModel`` adapter does not support; + surface that as a clear error rather than a deep tokenizer stack trace. + """ + try: + return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=True) + except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint + logger.warning( + "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", + model_name, + type(fast_err).__name__, + ) + try: + return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False) + except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message + raise RuntimeError( + f"Could not load a tokenizer for {model_name!r} in fast or slow mode. This is usually a " + "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " + "which the HFClassifierModel adapter does not support. Use a standard " + "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." + ) from slow_err def load_model(config: ServeConfig) -> HFClassifierModel: @@ -220,7 +288,7 @@ def load_model(config: ServeConfig) -> HFClassifierModel: device = "cpu" logger.info("No CUDA device found — using CPU") - tokenizer = transformers.AutoTokenizer.from_pretrained(config.model_name, use_fast=True) + tokenizer = _load_tokenizer(config.model_name) classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) logger.info("Loaded model %s on device: %s", config.model_name, next(classifier.parameters()).device) return HFClassifierModel(classifier, tokenizer) @@ -283,12 +351,21 @@ def main() -> None: model = load_model(config) + # Similar-examples reference corpus (the training split) + a cache dir for its activation bank, + # keyed by corpus/layer/model so it is embedded once and reloaded on later runs. + reference_corpus = load_reference_corpus(config) + similar_cache_dir = dataset_dir / "similar" + # No cf_generator is passed: NlpExplainer auto-discovers every built-in compatible with the model # (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's method dropdown. + # reference_corpus enables the "Similar Examples" panel (skipped when --n-reference 0). xpl = NlpExplainer( model, label_names=model.label_names, backend=build_backend(config, model), + reference_corpus=reference_corpus, + similarity_layer=config.similarity_layer, + reference_cache_dir=similar_cache_dir, ) # Both steps load from cache if one exists for these exact texts, otherwise they compute and write # it. So the first run is slow; later runs only pay the model load. @@ -304,12 +381,18 @@ def main() -> None: xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) projected = load_or_project(sentences, model, dataset_dir) + if xpl.can_find_similar(): + # Build (and cache) the reference activation bank now so the first in-app lookup is instant. + logger.info("Warming similar-examples bank over %d reference texts…", len(reference_corpus[0])) + xpl.find_similar(sentences[0], top_k=1) + logger.info( - "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s", + "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s", config.attribution, ",".join(name for name, _ in xpl.available_cf_generators()) or "none", xpl.can_edit(), xpl.can_counterfactual(), + xpl.can_find_similar(), ) logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) From 35174f8b61e592cce877cf29a33aec383a330d43 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 17 Jul 2026 10:51:19 +0200 Subject: [PATCH 072/144] build: add sentencepiece in nlp dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c7a72f1d..07758d20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ nlp = [ "transformers>=5.12.1", "datasets>=5.0.0", "sentence-transformers>=5.6.0", + "sentencepiece>=0.2.2", "captum>=0.9.0", "pacmap>=0.9.1", "dash-ag-grid>=35.2.0" From 63a47edfb04735fd6b1ae86bcd69615fb59a9f4e Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:35:58 +0200 Subject: [PATCH 073/144] test: fix test whatif with new changes --- tests/integration_tests/test_nlp_whatif.py | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py index 0d295a8f..ac944cf5 100644 --- a/tests/integration_tests/test_nlp_whatif.py +++ b/tests/integration_tests/test_nlp_whatif.py @@ -11,7 +11,7 @@ transformers = pytest.importorskip("transformers") pytest.importorskip("torch") -from shapash.compute.generators import HotFlipGenerator # noqa: E402 +from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator, TokenListField # noqa: E402 from shapash.explainer.interactive import InteractiveEngine # noqa: E402 from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 from shapash.model import HFClassifierModel, HFPipelineModel # noqa: E402 @@ -131,13 +131,17 @@ def test_explainer_interactive_engine_with_classifier(hf): assert all(cf.new_label != cf.orig_label for cf in cfs) -def test_explainer_pipeline_is_editable_but_no_counterfactuals(hf): +def test_explainer_pipeline_gets_ablation_flip_but_not_hotflip(hf): + """A predict-only pipeline can't run gradient-based HotFlip, but AblationFlip (forward-pass-only, + needs only tokenization) auto-binds as the fallback — so it still gets a What-if Lab. + """ _, _, pipe = hf xpl = NlpExplainer(pipe, label_names=LABELS) # predict-only pipeline assert xpl.can_edit() - assert not xpl.can_counterfactual() - assert xpl.cf_generator is None - assert xpl.cf_config_spec() == {} + assert xpl.can_counterfactual() + assert isinstance(xpl.cf_generator, AblationFlipGenerator) + assert [name for name, _ in xpl.available_cf_generators()] == ["ablation_flip"] + assert set(xpl.cf_config_spec()) == {"num_examples", "max_ablations", "tokens_to_ignore"} def _find_callback_key(app, output_substr): @@ -191,17 +195,30 @@ def test_whatif_app_callbacks_end_to_end(hf): assert payload["current-datapoint"]["data"]["tokens"] # Generate: current datapoint text -> counterfactual results table + store of new texts. + # The classifier model binds both HotFlip and AblationFlip (see test_explainer_interactive_engine_ + # with_classifier), so the counterfactual panel renders a method selector plus one config-control + # group per generator, each namespaced "counterfactual-cfg-{generator}-{field}". The callback's + # State list mirrors that layout exactly (selector, then every generator's fields in order) — build + # it from the live config specs rather than hardcoding ids, so it keeps matching the panel's layout. + gen_names = [name for name, _ in xpl.available_cf_generators()] + assert "hotflip" in gen_names + overrides = {"hotflip": {"num_examples": 3, "max_flips": 2, "tokens_to_ignore": ""}} + state = [ + {"id": "current-datapoint", "property": "data", "value": {"text": "i am so happy today"}}, + {"id": "counterfactual-generator", "property": "value", "value": "hotflip"}, + ] + for gen in gen_names: + for field_name, field in xpl.cf_config_spec(gen).items(): + default = ",".join(field.default) if isinstance(field, TokenListField) else field.default + value = overrides.get(gen, {}).get(field_name, default) + state.append({"id": f"counterfactual-cfg-{gen}-{field_name}", "property": "value", "value": value}) + gen_key = _find_callback_key(app, "counterfactual-results") r = _post_callback( app, gen_key, inputs=[{"id": "counterfactual-generate-btn", "property": "n_clicks", "value": 1}], - state=[ - {"id": "current-datapoint", "property": "data", "value": {"text": "i am so happy today"}}, - {"id": "counterfactual-cfg-num_examples", "property": "value", "value": 3}, - {"id": "counterfactual-cfg-max_flips", "property": "value", "value": 2}, - {"id": "counterfactual-cfg-tokens_to_ignore", "property": "value", "value": ""}, - ], + state=state, ) assert r.status_code == 200 payload = r.get_json()["response"] From 250b97b619c4ab2c4efd1e26773508d987895100 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:39:36 +0200 Subject: [PATCH 074/144] feat: upgrade TextModel protocol --- shapash/model/base.py | 134 +++++++++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 34 deletions(-) diff --git a/shapash/model/base.py b/shapash/model/base.py index 96a05078..65c76099 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -5,12 +5,13 @@ component can require exactly what it needs and no more: * ``SupportsTokenization`` — split text into tokens and rebuild text from tokens. -* ``SupportsEmbeddings`` — expose the input-embedding table and mean-pooled sentence embeddings. +* ``SupportsEmbeddings`` — expose the input-embedding table and one dense vector per text, in a + caller-selectable *representation space* (see :meth:`SupportsEmbeddings.embed`). This single + capability backs both the 2-D projection scatter and similar-example retrieval, which agree only + as far as they both go through it — see the caveat on :class:`SupportsEmbeddings`. * ``SupportsGradients`` — expose per-token gradients of a target-class logit. * ``SupportsCaptumIG`` — expose the embedding module + a logits forward pass so a layer-attribution method (Captum ``LayerIntegratedGradients``) can attribute through the embeddings. -* ``SupportsActivations`` — expose a named intermediate layer's activation as one dense vector per - text, so similar-example retrieval can compare a query against a corpus in that layer's space. A prediction-only model (e.g. a HuggingFace pipeline) implements only ``TextModel``; a raw classifier with tokenizer access implements all three. Generators check compatibility with @@ -21,7 +22,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any +from typing import Any, Protocol, runtime_checkable import numpy as np @@ -62,6 +63,28 @@ def n_classes(self) -> int | None: """Number of classes, or ``None`` when unknown before the first prediction.""" return len(self.label_names) if self.label_names is not None else None + @property + def shap_masker(self) -> Any: + """Masker SHAP should use, or ``None`` to let SHAP infer one from :attr:`shap_callable`. + + SHAP's text explainer needs a scoring function *and* a way to segment/mask the text. When + ``shap_callable`` is a ``transformers`` pipeline SHAP infers a ``Text`` masker from it, so the + default ``None`` is correct. Adapters whose ``shap_callable`` is a plain callable (no tokenizer + for SHAP to find) must override this and return an explicit ``shap.maskers.Text``. + """ + return None + + @property + def model_id(self) -> str: + """Stable identity for this adapter *and its configuration*, for cache keys. + + Anything that changes the vectors or scores a downstream cache stores must appear here — + otherwise two differently-configured models collide on the same cache entry and silently reload + each other's results. The default is the class name alone, which is only safe for an adapter with + no configuration; :class:`~shapash.model.encoder.EncoderClassifierModel` overrides it. + """ + return type(self).__name__ + class SupportsTokenization(ABC): """Capability: split text into tokens and rebuild text from tokens.""" @@ -76,15 +99,65 @@ def detokenize(self, tokens: list[str]) -> str: class SupportsEmbeddings(ABC): - """Capability: expose the input-embedding table and mean-pooled sentence embeddings.""" + """Capability: expose the input-embedding table and one dense vector per text. + + :meth:`embed` is the single entry point for *"give me a vector per text in some representation + space"* — the space is a parameter, not a separate capability. Everything downstream that compares + or projects texts (the 2-D projection scatter, similar-example retrieval) is expected to go + through it, so that changing the model's space moves them together. + + That agreement is a **convention, not a guarantee**: a caller who computes its own coordinates and + hands them to the webapp (``run_app(scatter_xy=...)``) bypasses this entirely, and nothing detects + it. Use :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection`, which derives the + scatter from this method, unless you specifically want a space of your own choosing. + """ @abstractmethod def get_embedding_table(self) -> tuple[list[str], np.ndarray]: """Return ``(vocab, matrix)`` where ``matrix`` has shape ``(vocab_size, hidden_dim)``.""" + def resolve_space(self, space: str | None = None) -> str: + """Return the name of the space :meth:`embed` will actually use for ``space``. + + Everything that caches embeddings must key on the *effective* space, not on the argument it was + given — otherwise ``None`` (meaning "the adapter's default") collides with whatever that default + currently is, and a cache entry from one space is silently reloaded for another. This is the one + place that resolution happens; callers must not reimplement it. + + Parameters + ---------- + space : str or None, optional + The space a caller asked for, or ``None`` for the adapter's default. + + Returns + ------- + str + A concrete space name, never ``None``. The base implementation has no notion of a default + space and reports ``"default"``; adapters with a configurable one (e.g. + :class:`~shapash.model.encoder.EncoderClassifierModel`) override this. + """ + return space if space is not None else "default" + @abstractmethod - def embed(self, texts: list[str]) -> np.ndarray: - """Return one dense vector per text, shape ``(n_texts, hidden_dim)``.""" + def embed(self, texts: list[str], space: str | None = None) -> np.ndarray: + """Return one dense vector per text, shape ``(n_texts, hidden_dim)``. + + Parameters + ---------- + texts : list of str + Input strings. + space : str or None, optional + Which representation to return. When ``None`` (default), the adapter's configured default + space is used. Adapters are free to define their own space names; the shared + :class:`~shapash.model.encoder.EncoderClassifierModel` vocabulary is ``"decision"`` (input + to the final classification linear), ``"pooled"`` (the pooled last hidden state), or any + named submodule of the model, whose token-level output is pooled to one vector per text. + + Returns + ------- + np.ndarray, shape (n_texts, hidden_dim) + Dense vectors aligned to ``texts``. The dimension depends on ``space``. + """ class SupportsGradients(ABC): @@ -170,39 +243,32 @@ def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[in return None -class SupportsActivations(ABC): - """Capability: expose a named intermediate layer's activation as one dense vector per text. +@runtime_checkable +class EmbeddingSource(Protocol): + """Structural type for the surface an embedding *cache* consumes. - This is the surface similar-example retrieval needs: it builds a bank of activation vectors - over a reference corpus and compares a query's vector against it (e.g. by cosine similarity) to - retrieve the most similar examples. Kept separate from :class:`SupportsEmbeddings` — whose - ``embed`` is fixed to the last hidden state — because retrieval wants a *configurable*, - decision-relevant layer (e.g. the pre-classifier pooled vector), not a fixed one. + Caching embeddings needs strictly more than :class:`SupportsEmbeddings`: besides ``embed`` and + ``resolve_space`` it needs ``model_id``, which lives on :class:`TextModel`. Neither ABC alone + describes that, and merging them would make ``model_id`` a capability concern or ``embed`` a + base-model one — both wrong. This ``Protocol`` names the intersection without moving anything. + + It is a *typing* aid, not a gate. Components still admit a model by capability + (``has_capabilities(model, SupportsEmbeddings)``); every adapter that passes that check is a + ``TextModel`` too, so it satisfies this protocol by construction. """ @property - @abstractmethod - def default_activation_layer(self) -> str: - """Fully-qualified name of the layer used when :meth:`activations` is called with ``layer=None``.""" + def model_id(self) -> str: + """Stable identity of the model and its configuration (see :attr:`TextModel.model_id`).""" + ... - @abstractmethod - def activations(self, texts: list[str], layer: str | None = None) -> np.ndarray: - """Return one activation vector per text at ``layer``, shape ``(n_texts, hidden_dim)``. - - Parameters - ---------- - texts : list of str - Input strings. - layer : str or None - Fully-qualified module name whose output is captured. When ``None``, - :attr:`default_activation_layer` is used. A token-level ``(n_tokens, hidden)`` layer - output is mean-pooled (respecting the attention mask) down to one vector per text. + def resolve_space(self, space: str | None = None) -> str: + """Name of the space :meth:`embed` will actually use (see :meth:`SupportsEmbeddings.resolve_space`).""" + ... - Returns - ------- - np.ndarray, shape (n_texts, hidden_dim) - Dense activation vectors aligned to ``texts``. - """ + def embed(self, texts: list[str], space: str | None = None) -> np.ndarray: + """One dense vector per text (see :meth:`SupportsEmbeddings.embed`).""" + ... def has_capabilities(model: object, *capabilities: type) -> bool: From a097f984d73255e416c2d22cfaf00044bb49c115 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:44:09 +0200 Subject: [PATCH 075/144] feat(model): extend to encoder models with classifier heads this includes SentenceTransformersModel and TorchClassifierModel --- shapash/model/__init__.py | 41 ++- shapash/model/encoder.py | 611 ++++++++++++++++++++++++++++++++++ shapash/model/hf.py | 337 +++---------------- shapash/model/torch_models.py | 444 ++++++++++++++++++++++++ 4 files changed, 1131 insertions(+), 302 deletions(-) create mode 100644 shapash/model/encoder.py create mode 100644 shapash/model/torch_models.py diff --git a/shapash/model/__init__.py b/shapash/model/__init__.py index 47882069..82e176d8 100644 --- a/shapash/model/__init__.py +++ b/shapash/model/__init__.py @@ -11,29 +11,62 @@ ``SupportsGradients``) instead of LIT's runtime ``JsonDict``/``LitType`` specs. This matches the typed-dataclass style already used elsewhere in shapash (e.g. ``NlpContributions``). -Currently ships text adapters for HuggingFace models. ``SentenceTransformerModel`` and -``TorchModel`` are documented extension points (see ``hf.py`` roadmap notes). +All full-capability adapters share one spine, :class:`~shapash.model.encoder.EncoderClassifierModel` +(encoder + pooling + classification head), and differ only in how the backbone is assembled: +``HFClassifierModel`` (the classifier is the backbone), ``SentenceTransformerModel`` and +``TorchClassifierModel`` (a body + pooling + head fused into a backbone). """ from __future__ import annotations +from typing import TYPE_CHECKING + from shapash.model.base import ( - SupportsActivations, + SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel, has_capabilities, ) +from shapash.model.encoder import EncoderClassifierModel from shapash.model.hf import HFClassifierModel, HFPipelineModel +if TYPE_CHECKING: # import for type checkers only — the runtime path is the lazy __getattr__ below + from shapash.model.torch_models import SentenceTransformerModel, TorchClassifierModel + +# Adapters imported on demand. ``shapash.model`` is loaded by a plain ``import shapash``, but +# ``torch_models`` needs ``torch`` at *module* level (it subclasses ``nn.Module`` to fuse body+head into +# a backbone), and torch is an optional dependency. Deferring the import keeps core installs working and +# means users who never touch these adapters don't pay to load them. ``__getattr__`` (PEP 562) makes +# ``from shapash.model import TorchClassifierModel`` work exactly as before. +_LAZY_ADAPTERS = {"SentenceTransformerModel", "TorchClassifierModel"} + + +def __getattr__(name: str): + """Resolve the torch-only adapters on first access (see :data:`_LAZY_ADAPTERS`).""" + if name in _LAZY_ADAPTERS: + from shapash.model import torch_models # noqa: PLC0415 + + return getattr(torch_models, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + """Include the lazily-resolved adapters in ``dir()`` and tab-completion.""" + return sorted(__all__) + + __all__ = [ "TextModel", "SupportsTokenization", "SupportsEmbeddings", "SupportsGradients", - "SupportsActivations", + "SupportsCaptumIG", "has_capabilities", "HFPipelineModel", "HFClassifierModel", + "EncoderClassifierModel", + "SentenceTransformerModel", + "TorchClassifierModel", ] diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py new file mode 100644 index 00000000..d01e3562 --- /dev/null +++ b/shapash/model/encoder.py @@ -0,0 +1,611 @@ +"""General encoder-classifier adapter — the full-capability text-model spine. + +Every text model in the *full-capability* regime shares one structure:: + + text -> [tokenizer] -> input_ids -> [embedding layer] -> inputs_embeds + -> [ body -> pool -> head ] -> logits + +`EncoderClassifierModel` implements **all** capability mixins (tokenization, embeddings, gradients and +the Captum attribution surface) once, in terms of a single *backbone* object that honours a small +HuggingFace-model-like contract. Concrete models are then thin **presets** that build such a backbone: + +* :class:`~shapash.model.hf.HFClassifierModel` — the backbone *is* a + ``AutoModelForSequenceClassification`` (pooling + head are internal to it). +* :class:`SentenceTransformerModel` / :class:`TorchClassifierModel` — wrap a separate encoder body, + a pooling mode, and a classification head into a backbone via :class:`_EncoderHeadBackbone`. + +The backbone contract (what every method here relies on): + +* ``backbone(input_ids=?, inputs_embeds=?, attention_mask=?, output_hidden_states=?, **extra)`` returns + an object with ``.logits``; with ``output_hidden_states=True`` it also exposes ``.hidden_states`` whose + last element is the token-level ``(batch, seq, hidden)`` last hidden state. +* ``backbone.get_input_embeddings()`` returns the word-embedding ``nn.Module`` (with a ``.weight``). +* ``backbone.device`` is the device its parameters live on. +* ``backbone.named_modules()`` / ``backbone.zero_grad()`` — it is an ``nn.Module``. + +Because the whole engine gates on *declared capabilities* (``has_capabilities``), never on a concrete +class, adding a new preset needs no change to any backend, generator, or webapp component. +""" + +from __future__ import annotations + +import logging +import warnings +from typing import Any + +import numpy as np + +from shapash._optional import import_optional_module +from shapash.model.base import ( + SupportsCaptumIG, + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, +) + +logger = logging.getLogger(__name__) + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' + +# ``embedding_space`` keywords selecting the representation :meth:`EncoderClassifierModel.embed` returns +# (any other value is treated as a named backbone submodule): +# "decision" — the input to the final classification ``nn.Linear`` (the features the model linearly +# separates into logits). Reaches *into* the head, so a frozen-encoder + trained-head +# model still projects in a class-discriminative space; ``pre_classifier``-like for HF. +# "pooled" — the pooled last hidden state of the encoder (mask-aware :attr:`pool`), the historical +# behaviour. Best when the encoder itself was fine-tuned end-to-end for the task. +_DECISION_SPACE = "decision" +_POOLED_SPACE = "pooled" +_KEYWORD_SPACES = (_DECISION_SPACE, _POOLED_SPACE) + +# Supported string pooling modes for turning a ``(batch, seq, hidden)`` token-level output into one +# vector per text. A callable ``pool`` may be passed instead (see ``EncoderClassifierModel``). +_POOL_MODES = ("mean", "cls", "max") + +# Attribute names under which a backbone exposes its classification head, tried in order. ``head`` is +# what ``build_encoder_head_backbone`` names it; ``classifier`` is the universal HuggingFace +# sequence-classification convention (BERT, DistilBERT, RoBERTa, XLM-R, DeBERTa-v2), with ``score`` used +# by a few newer checkpoints. See ``EncoderClassifierModel._head_module``. +_HEAD_ATTRS = ("head", "classifier", "score") + + +def _pool_hidden(hidden: Any, attention_mask: Any, pool: Any) -> Any: + """Reduce a token-level ``(batch, seq, hidden)`` tensor to ``(batch, hidden)``, mask-aware. + + Parameters + ---------- + hidden : torch.Tensor + Token-level output, shape ``(batch, seq, hidden)``. + attention_mask : torch.Tensor + Shape ``(batch, seq)``; padding positions (0) are excluded from ``mean``/``max``. + pool : {"mean", "cls", "max"} or callable + Pooling strategy. ``"mean"`` mask-averages over tokens (the default, matching the historical + ``HFClassifierModel`` behaviour), ``"cls"`` takes position 0, ``"max"`` mask-maxes. A callable + is invoked as ``pool(hidden, attention_mask)`` and must return ``(batch, hidden)``. + + Returns + ------- + torch.Tensor + Pooled representation, shape ``(batch, hidden)``. + """ + if callable(pool): + return pool(hidden, attention_mask) + if pool == "cls": + return hidden[:, 0] + mask = attention_mask.unsqueeze(-1) + if pool == "max": + return hidden.masked_fill(mask == 0, float("-inf")).max(dim=1).values + # "mean" — mask-aware average over non-padding tokens. + return (hidden * mask).sum(1) / mask.sum(1) + + +class EncoderClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG): + """Full-capability text adapter over any *encoder + classification head* backbone. + + Implements prediction, tokenization, the input-embedding table, sentence embeddings in any + representation space, per-token gradients and the Captum ``LayerIntegratedGradients`` surface — + everything gradient-based counterfactual generators, LIG, and similar-example retrieval require — + in terms of the backbone contract documented in this module. Concrete model classes subclass this + as thin presets (see :class:`~shapash.model.hf.HFClassifierModel`, + :class:`SentenceTransformerModel`, :class:`TorchClassifierModel`). + + Parameters + ---------- + backbone : nn.Module + An object honouring the HuggingFace-model-like backbone contract (see module docstring): it + maps ``input_ids``/``inputs_embeds`` (+ ``attention_mask``) to ``.logits`` and, on request, + ``.hidden_states``; and exposes ``get_input_embeddings()`` and ``.device``. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer matching the backbone. A *fast* tokenizer additionally enables exact + subword-to-word grouping via :meth:`word_alignment`. + label_names : list[str] or None + Class names in class-index order. Presets typically resolve this from the model config. + batch_size : int, optional + Batch size for ``predict``, ``embed`` and the SHAP pipeline. Default 32. + device : int or str or torch.device or None, optional + Device for the SHAP pipeline. When ``None``, inherits the backbone's device. + pool : {"mean", "cls", "max"} or callable, optional + How a token-level layer output is reduced to one vector per text in :meth:`embed`. Default + ``"mean"`` (the historical behaviour). + normalize : bool, optional + Whether the pooled sentence vector is L2-normalized before the classification head (and in + :meth:`embed`). Default ``False``. Sentence-transformer pipelines that end in a ``Normalize`` + module train their head on unit-length embeddings; setting this reproduces that step so + ``predict`` stays faithful and :meth:`embed` lives in the head's true decision space. Presets + that own the pooling→head path (:class:`~shapash.model.torch_models.TorchClassifierModel` and + subclasses) also apply it *inside* the backbone; it is a no-op for + :class:`~shapash.model.hf.HFClassifierModel` (its head does its own pooling). + max_length : int or None, optional + Truncation length applied to every tokenizer call. When ``None`` (default) the tokenizer's own + ``model_max_length`` applies. Set this when the model is served at a shorter length than its + tokenizer allows, so long texts are not scored on more context than the model was trained with. + :class:`~shapash.model.torch_models.SentenceTransformerModel` resolves it from the + sentence-transformers ``max_seq_length``. + embedding_space : str, optional + Default representation :meth:`embed` returns — the space used for the 2-D projection scatter and + similar-example retrieval. Default ``"decision"``: the input to the final classification linear + (the model's decision space), which stays class-discriminative even when the encoder is frozen + and only a head was trained. ``"pooled"`` returns the mask-pooled last hidden state + (:attr:`pool`) — the historical behaviour, best when the encoder was fine-tuned end-to-end. Any + other value must name a backbone submodule, whose output is used. Validated eagerly: an unknown + name raises here rather than deep inside the first :meth:`embed` call. + + Raises + ------ + ValueError + If ``pool`` is not a supported mode or callable, or ``embedding_space`` is neither a keyword + space nor the name of an existing backbone submodule. + """ + + def __init__( + self, + backbone, + tokenizer, + label_names: list[str] | None = None, + batch_size: int = 32, + device: int | str | object | None = None, + *, + pool: Any = "mean", + normalize: bool = False, + embedding_space: str = _DECISION_SPACE, + max_length: int | None = None, + ) -> None: + if not callable(pool) and pool not in _POOL_MODES: + raise ValueError(f"pool must be one of {_POOL_MODES} or a callable; got {pool!r}.") + super().__init__(label_names=label_names) + self.backbone = backbone + self.tokenizer = tokenizer + self.batch_size = batch_size + self.device = device + self.pool = pool + self.normalize = bool(normalize) + self.max_length = max_length + self._pipeline = None # lazily built for SHAP + self._decision_linear: Any = None # lazily resolved final classification linear + self._decision_resolved = False + self.embedding_space = embedding_space # validated by the property setter + + @property + def embedding_space(self) -> str: + """Default representation space :meth:`embed` returns when called without one. + + Assignable after construction — a caller switches the whole app between a semantic and a + decision view this way, without reloading the model — and validated on assignment, so a bad + name raises at the point of the mistake rather than on the next forward pass. + """ + return self._embedding_space + + @embedding_space.setter + def embedding_space(self, space: str) -> None: + self._validate_space(space) + self._embedding_space = space + + def resolve_space(self, space: str | None = None) -> str: + """Return the concrete space name :meth:`embed` will use — :attr:`embedding_space` for ``None``. + + Validates an explicit ``space`` so cache-key derivation and :meth:`embed` reject the same names. + See :meth:`~shapash.model.base.SupportsEmbeddings.resolve_space`. + """ + if space is None: + return self.embedding_space + self._validate_space(space) + return space + + def _validate_space(self, space: str) -> None: + """Raise if ``space`` is neither a keyword space nor an existing backbone submodule name. + + Checked eagerly at construction (and on every explicit ``space=`` passed to :meth:`embed`) so a + typo surfaces immediately with the available names, instead of as a ``KeyError`` from the first + forward pass — which, in the webapp, happens at start-up after the model has already loaded. + """ + if space in _KEYWORD_SPACES: + return + modules = dict(self.backbone.named_modules()) + if space not in modules: + top_level = [name for name, _ in self.backbone.named_children()] + raise ValueError( + f"embedding space {space!r} is neither a keyword space {_KEYWORD_SPACES} nor a submodule " + f"of the backbone. Available top-level modules: {top_level}." + ) + + @property + def model_id(self) -> str: + """Stable identity for this adapter *and every setting that changes its vectors/scores*. + + Downstream caches (the similar-example bank, the projection cache) key on this. It therefore + includes the backbone's checkpoint name **and** ``pool`` / ``normalize`` — two models that differ + only in pooling produce different embeddings, and keying on the class name alone would make them + silently share a cache entry. + """ + config = getattr(self.backbone, "config", None) + checkpoint = getattr(config, "_name_or_path", None) or type(self.backbone).__name__ + pool = self.pool if isinstance(self.pool, str) else getattr(self.pool, "__name__", "callable") + return f"{type(self).__name__}:{checkpoint}:pool={pool}:norm={int(self.normalize)}" + + # ------------------------------------------------------------------ + # Batched forward (shared by predict / embed) + # ------------------------------------------------------------------ + + def _tokenize(self, texts: str | list[str], **overrides: Any): + """Tokenize with this model's truncation settings — the single place they are applied. + + Every tokenizer call in this class goes through here so they truncate *identically*. That is not + just tidiness: :meth:`word_alignment` returns positions into the same subword axis as + :meth:`encode`, so if the two truncated differently the attribution highlights would silently + misalign on any text long enough to be cut. + """ + kwargs: dict[str, Any] = {"truncation": True} + if self.max_length is not None: + kwargs["max_length"] = self.max_length + kwargs.update(overrides) + return self.tokenizer(texts, **kwargs) + + def _encode_one(self, text: str): + """Return ``(input_ids, attention_mask)`` batch-size-1 tensors on the backbone device.""" + enc = self._tokenize(text, return_tensors="pt") + device = self.backbone.device + return enc["input_ids"].to(device), enc["attention_mask"].to(device) + + def _batches(self, texts: list[str], **forward_kwargs: Any): + """Yield ``(batch, output)`` per chunk of ``texts``, tokenized, on-device and under ``no_grad``. + + The single place the tokenize -> to(device) -> forward loop lives; ``predict`` and every + :meth:`embed` space differ only in what they pull out of each ``(batch, output)`` pair. + ``forward_kwargs`` are passed through to the backbone (e.g. ``output_hidden_states=True``). + """ + torch = import_optional_module("torch", extra=_NLP_EXTRA) + device = self.backbone.device + for i in range(0, len(texts), self.batch_size): + batch = self._tokenize(texts[i : i + self.batch_size], padding=True, return_tensors="pt") + batch = {k: v.to(device) for k, v in batch.items()} + with torch.no_grad(): + output = self.backbone(**batch, **forward_kwargs) + yield batch, output + + def _pool_batch(self, tensor: Any, batch: dict) -> Any: + """Reduce a captured tensor to ``(batch, hidden)``, pooling only if it is still token-level.""" + if tensor.dim() == 3: # token-level (batch, seq, hidden) -> one vector per text + return _pool_hidden(tensor, batch["attention_mask"], self.pool) + return tensor + + # ------------------------------------------------------------------ + # Prediction + # ------------------------------------------------------------------ + + def predict(self, texts: list[str]) -> np.ndarray: + """Return ``(n_texts, n_classes)`` softmax probabilities in class-index order.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + out = [torch.softmax(output.logits, dim=-1).cpu().numpy() for _, output in self._batches(list(texts))] + return np.vstack(out) + + def _resolve_pipeline_device(self): + """Pick the pipeline device, degrading an unavailable-GPU request to CPU with a warning.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + if self.device is None: + # Inherit whatever device the backbone already lives on (CPU-safe by construction). + return self.backbone.device + wants_cuda = (isinstance(self.device, str) and self.device.startswith("cuda")) or ( + isinstance(self.device, int) and self.device >= 0 + ) + if wants_cuda and not torch.cuda.is_available(): + warnings.warn( + f"Requested device {self.device!r} but no CUDA GPU is available; " + "falling back to CPU for the SHAP pipeline.", + stacklevel=2, + ) + return torch.device("cpu") + return self.device + + @property + def shap_callable(self): + """A ``text-classification`` pipeline built from the backbone, for SHAP's TextMasker. + + Requires the backbone to be a ``transformers`` model ``pipeline`` accepts. Presets whose + backbone is not a ``PreTrainedModel`` (e.g. an external-head wrapper) override this. + """ + if self._pipeline is None: + transformers = import_optional_module("transformers", extra=_NLP_EXTRA) + self._pipeline = transformers.pipeline( + "text-classification", + model=self.backbone, + tokenizer=self.tokenizer, + top_k=None, + device=self._resolve_pipeline_device(), + batch_size=self.batch_size, + truncation=True, + ) + return self._pipeline + + # ------------------------------------------------------------------ + # Tokenization + # ------------------------------------------------------------------ + + def tokenize(self, text: str) -> list[str]: + """Return the tokenizer's tokens for ``text``.""" + return self.tokenizer.tokenize(text) + + def detokenize(self, tokens: list[str]) -> str: + """Rebuild a display string from tokens (merges word-pieces).""" + return self.tokenizer.convert_tokens_to_string(tokens) + + # ------------------------------------------------------------------ + # Embeddings + # ------------------------------------------------------------------ + + def get_embedding_table(self) -> tuple[list[str], np.ndarray]: + """Return ``(vocab, matrix)`` from the backbone's input-embedding layer.""" + weight = self.backbone.get_input_embeddings().weight + matrix = weight.detach().cpu().numpy() + vocab = self.tokenizer.convert_ids_to_tokens(range(matrix.shape[0])) + return vocab, matrix + + def embed(self, texts: list[str], space: str | None = None) -> np.ndarray: + """Return one representation vector per text, shape ``(n_texts, hidden_dim)``. + + This is the single entry point for every representation space: the model's ``"decision"`` space + (input to the final classification linear), the ``"pooled"`` last hidden state, or any named + backbone submodule whose output is captured. The scatter projection and similar-example + retrieval both go through it, so they always compare texts in the same space. + + Parameters + ---------- + texts : list of str + Input strings. + space : str or None, optional + Representation to return. When ``None`` (default), :attr:`embedding_space` is used. An + explicit value is validated the same way (unknown names raise :class:`ValueError`). + + Returns + ------- + np.ndarray, shape (n_texts, hidden_dim) + Dense vectors aligned to ``texts``. The dimension depends on ``space``. + """ + texts = list(texts) + space = self.resolve_space(space) + if space == _POOLED_SPACE: + return self._pooled_embed(texts) + if space == _DECISION_SPACE: + return self._decision_embed(texts) + # A named backbone submodule: capture its output on the forward pass. + module = dict(self.backbone.named_modules())[space] + return self._hooked_embed(texts, module, pre=False) + + def _pooled_embed(self, texts: list[str]) -> np.ndarray: + """Return the mask-pooled last hidden state, shape ``(n_texts, hidden_dim)``. + + The token-level last hidden state is reduced with the configured :attr:`pool` (mask-aware for + ``"mean"``/``"max"``), so padding tokens never contribute. When :attr:`normalize` is set the + pooled vector is L2-normalized — matching a sentence-transformer ``Normalize`` module, so the + embedding lives in the same space the classification head reads. + """ + torch = import_optional_module("torch", extra=_NLP_EXTRA) + out = [] + for batch, output in self._batches(texts, output_hidden_states=True): + hidden = getattr(output, "hidden_states", None) + if not hidden: + # Part of the backbone contract (see the module docstring). Say so plainly — the raw + # AttributeError names only ``hidden_states`` and not what the caller has to fix. + raise AttributeError( + f"{type(self.backbone).__name__} returned no `hidden_states` for " + f"output_hidden_states=True, so the {_POOLED_SPACE!r} embedding space is " + f"unavailable. Use embedding_space={_DECISION_SPACE!r} or a named submodule, or " + "make the backbone honour output_hidden_states." + ) + emb = _pool_hidden(hidden[-1], batch["attention_mask"], self.pool) + if self.normalize: + emb = torch.nn.functional.normalize(emb, p=2, dim=-1) + out.append(emb.cpu().numpy()) + return np.vstack(out) + + def _hooked_embed(self, texts: list[str], module: Any, pre: bool) -> np.ndarray: + """Return one vector per text captured at ``module``, shape ``(n_texts, hidden_dim)``. + + With ``pre=False`` a forward hook captures the module's *output*; with ``pre=True`` a forward + pre-hook captures its first *input* (how the decision space is read off the final linear). A + pooled ``(batch, hidden)`` capture is used as-is; a token-level ``(batch, seq, hidden)`` one is + reduced with the configured :attr:`pool`. The backbone runs under ``no_grad`` — inference only. + """ + captured: list = [] + if pre: + handle = module.register_forward_pre_hook(lambda _mod, args: captured.append(args[0].detach())) + else: + handle = module.register_forward_hook(lambda _mod, _inp, out: captured.append(out.detach())) + out = [] + try: + for batch, _ in self._batches(texts): + # The hook fires during the forward pass above; take the first capture of this batch and + # clear so a module invoked more than once cannot leak captures across batches. + vec = self._pool_batch(captured[0], batch) + out.append(vec.cpu().numpy()) + captured.clear() + finally: + handle.remove() + return np.vstack(out) + + def _head_module(self): + """Return the classification head module, or ``None`` when it cannot be located. + + The head is found by attribute name (:data:`_HEAD_ATTRS`), which covers every preset without any + of them needing to declare their layout: the fused backbone built by + :func:`~shapash.model.torch_models.build_encoder_head_backbone` names it ``head``, and HuggingFace + sequence-classification models name it ``classifier`` (a few newer ones ``score``). A preset whose + head is named otherwise overrides this method. + """ + for attr in _HEAD_ATTRS: + module = getattr(self.backbone, attr, None) + if module is not None: + return module + return None + + def _resolve_decision_linear(self): + """Return the final classification ``nn.Linear`` (its input is the decision space), or ``None``. + + Located structurally: find the classification head (:meth:`_head_module`), then take the last + ``nn.Linear`` inside it. That last linear is the one producing logits whether the head is a bare + ``nn.Linear`` (BERT, DistilBERT, DeBERTa-v2) or a small compound module (``RobertaClassificationHead`` + = ``dense`` -> ``dropout`` -> ``out_proj``). + + Scoping the search to the head is what makes it reliable. Scanning the *whole* backbone for a + linear whose ``out_features`` matched the class count — as this once did — searched dozens of + candidates in registration rather than execution order, so it could silently select the wrong + layer and return a plausible embedding from the wrong space. Inside a 1-3 linear head the two + orders agree and there is nothing else to collide with. It also drops the dependency on + ``label_names``, which is a display concern and should never have decided which representation a + caller gets. + + Returns ``None`` when no head can be located, in which case :meth:`_decision_embed` warns and + falls back to the pooled space. + """ + if self._decision_resolved: + return self._decision_linear + self._decision_resolved = True + torch = import_optional_module("torch", extra=_NLP_EXTRA) + head = self._head_module() + if head is None: + logger.debug("Decision space: no head module found among attributes %s.", (_HEAD_ATTRS,)) + return self._decision_linear + linears = [module for module in head.modules() if isinstance(module, torch.nn.Linear)] + self._decision_linear = linears[-1] if linears else None + logger.debug( + "Decision space: head %s exposes %d linear(s); resolved=%s", + type(head).__name__, + len(linears), + self._decision_linear is not None, + ) + return self._decision_linear + + def _decision_embed(self, texts: list[str]) -> np.ndarray: + """Return the input to the final classification linear, shape ``(n_texts, n_features)``. + + A forward pre-hook captures the vector the final linear reads (the model's decision space). When + no final linear can be resolved (no head found under :data:`_HEAD_ATTRS`, or a head containing no + ``nn.Linear``), falls back to :meth:`_pooled_embed` — and warns, because the caller asked for the + decision space and is getting a different one, which would otherwise be invisible. + """ + linear = self._resolve_decision_linear() + if linear is None: + warnings.warn( + f"{type(self).__name__}: could not locate a final classification nn.Linear — no head " + f"found under {_HEAD_ATTRS} on {type(self.backbone).__name__}, or it contains no linear. " + f"Embedding space {_DECISION_SPACE!r} falls back to {_POOLED_SPACE!r} (the pooled last " + f"hidden state). Pass embedding_space={_POOLED_SPACE!r} to silence this, a submodule name " + "to pick the representation explicitly, or override _head_module() on your adapter.", + UserWarning, + stacklevel=3, + ) + logger.info("Decision space unavailable — falling back to the pooled last hidden state.") + return self._pooled_embed(texts) + return self._hooked_embed(texts, linear, pre=True) + + # ------------------------------------------------------------------ + # Gradients + # ------------------------------------------------------------------ + + def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.ndarray]: + """Return ``(tokens, grads)`` of the target-class logit w.r.t. input embeddings. + + Special tokens (``[CLS]``/``[SEP]``/padding) are dropped so the returned tokens and gradient + rows are aligned to substitutable content tokens only. + """ + input_ids, attention_mask = self._encode_one(text) + embed_layer = self.backbone.get_input_embeddings() + inputs_embeds = embed_layer(input_ids) + inputs_embeds.requires_grad_(True) + inputs_embeds.retain_grad() + + self.backbone.zero_grad() + logits = self.backbone(inputs_embeds=inputs_embeds, attention_mask=attention_mask).logits + logits[0, target_class].backward() + + grads = inputs_embeds.grad[0].detach().cpu().numpy() # (seq, hidden) + ids = input_ids[0].tolist() + tokens = self.tokenizer.convert_ids_to_tokens(ids) + special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) + + keep = [i for i, is_special in enumerate(special_mask) if not is_special] + kept_tokens = [tokens[i] for i in keep] + kept_grads = grads[keep] + return kept_tokens, kept_grads + + # ------------------------------------------------------------------ + # Captum layer-attribution surface + # ------------------------------------------------------------------ + + @property + def embedding_layer(self): + """Return the word-embedding ``nn.Module`` (what ``LayerIntegratedGradients`` attributes through).""" + return self.backbone.get_input_embeddings() + + def encode(self, text: str): + """Return ``(input_ids, attention_mask, tokens)`` — batch-size-1 tensors + aligned token strings.""" + input_ids, attention_mask = self._encode_one(text) + tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0].tolist()) + return input_ids, attention_mask, tokens + + def reference_ids(self, input_ids): + """Return baseline ids: content tokens replaced by the pad/mask reference, special tokens kept.""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + ids = input_ids[0].tolist() + special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) + ref_id = self.tokenizer.pad_token_id + if ref_id is None: + ref_id = self.tokenizer.mask_token_id or self.tokenizer.unk_token_id or 0 + ref = [tid if is_special else ref_id for tid, is_special in zip(ids, special_mask, strict=True)] + return torch.tensor([ref], device=input_ids.device) + + def logits(self, input_ids, attention_mask): + """Return raw classification logits ``(batch, n_classes)`` (the Captum forward func).""" + return self.backbone(input_ids=input_ids, attention_mask=attention_mask).logits + + def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[int]] | None: + """Group subwords into whole words via the fast tokenizer's ``word_ids()`` (see base ABC). + + Re-encodes ``text`` with the same ``truncation=True`` as :meth:`encode`, so the returned + positions align with the token/attribution axis. ``word_ids()`` maps each subword to its word + index (``None`` for special tokens), giving exact, scheme-independent grouping — no ``##`` / ``Ġ`` + / ``▁`` string-guessing. Display strings come from ``convert_tokens_to_string`` so each word is + rebuilt correctly whatever the tokenization scheme. Returns ``None`` for a slow tokenizer (no + ``word_ids()``), letting the caller fall back to a token-string heuristic. + """ + if not getattr(self.tokenizer, "is_fast", False): + return None + enc = self._tokenize(text) + word_ids = enc.word_ids() + tokens = self.tokenizer.convert_ids_to_tokens(enc["input_ids"]) + + # Special tokens (and any tokenless position) carry word_id None: fold their attribution into + # the baseline. Content subwords sharing a word_id compose one word. + special_positions = [i for i, wid in enumerate(word_ids) if wid is None] + word_positions: list[list[int]] = [] + prev_wid: int | None = None + for i, wid in enumerate(word_ids): + if wid is None: + continue + if wid != prev_wid: + word_positions.append([i]) + prev_wid = wid + else: + word_positions[-1].append(i) + words = [self.tokenizer.convert_tokens_to_string([tokens[i] for i in pos]).strip() for pos in word_positions] + return words, word_positions, special_positions diff --git a/shapash/model/hf.py b/shapash/model/hf.py index f3a64d92..13bae230 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -6,42 +6,29 @@ (plus tokenization). This is the backward-compatible default: everything the existing prototype passes to ``NlpExplainer`` is a pipeline, so it is wrapped here transparently. * :class:`HFClassifierModel` — wraps a raw ``AutoModelForSequenceClassification`` + tokenizer and - implements **all** capabilities (tokenization, embeddings, gradients), which gradient-based - counterfactual methods such as HotFlip need. - -Roadmap (documented extension points, not implemented): ``SentenceTransformerModel`` (encoder + -classifier head) and ``TorchModel`` (hand-rolled ``nn.Module``) would subclass ``TextModel`` and the -relevant capability mixins the same way — generators and the webapp need no changes because they -depend on capabilities, not concrete classes. + implements **all** capabilities, which gradient-based counterfactual methods (HotFlip) and the + Captum LIG backend need. It is a thin preset over + :class:`~shapash.model.encoder.EncoderClassifierModel`: the classifier itself is the backbone + (pooling and head are internal to it), so no fusing is required. + +For models whose encoder body and classification head are *separate* modules (sentence-transformers, +hand-rolled PyTorch classifiers), see :class:`~shapash.model.torch_models.SentenceTransformerModel` and +:class:`~shapash.model.torch_models.TorchClassifierModel` — they share the same capability spine via a +fused backbone. Generators and the webapp need no changes because everything depends on *capabilities*, +not concrete classes. """ from __future__ import annotations -import warnings +from typing import Any import numpy as np -from shapash._optional import import_optional_module from shapash.model.base import ( - SupportsActivations, - SupportsCaptumIG, - SupportsEmbeddings, - SupportsGradients, SupportsTokenization, TextModel, ) - -_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' - -# Pooled-representation module candidates for :attr:`HFClassifierModel.default_activation_layer`, most -# model-specific first: DistilBERT exposes ``pre_classifier``, BERT/DeBERTa a top-level ``pooler``. The -# first that exists on the classifier is the default similar-example decision-space; when none do (e.g. -# RoBERTa/XLM-R, whose head reads the raw ``<s>`` hidden state), we fall back to the sentinel below. -_POOLED_LAYER_CANDIDATES = ("pre_classifier", "pooler") -# Sentinel returned by ``default_activation_layer`` when the model has no pooled head module: it means -# "use the mean-pooled last hidden state" (what ``embed`` computes) — a representation every -# architecture provides, so similar-example retrieval works with no per-model configuration. -_LAST_HIDDEN_STATE = "__last_hidden_state__" +from shapash.model.encoder import _DECISION_SPACE, EncoderClassifierModel def _probs_from_pipeline_output(raw: list, order: list[str]) -> np.ndarray: @@ -116,14 +103,14 @@ def detokenize(self, tokens: list[str]) -> str: return self.tokenizer.convert_tokens_to_string(tokens) -class HFClassifierModel( - TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG, SupportsActivations -): +class HFClassifierModel(EncoderClassifierModel): """Full-capability adapter over a raw ``AutoModelForSequenceClassification`` + tokenizer. - Provides prediction, tokenization, the input-embedding table, mean-pooled sentence embeddings, - per-token gradients, and the Captum layer-attribution surface — everything gradient-based - counterfactual generators and ``LayerIntegratedGradients`` require. + A thin preset over :class:`~shapash.model.encoder.EncoderClassifierModel`: the classifier *is* the + backbone (it maps ids/embeds to ``.logits`` with pooling and head internal), so this class only + resolves ``label_names`` from the model config and delegates the entire capability surface — + prediction, tokenization, the input-embedding table, sentence embeddings in any representation + space, per-token gradients and the Captum layer-attribution surface. Parameters ---------- @@ -141,6 +128,14 @@ class HFClassifierModel( the pipeline inherits the device the classifier already lives on, so CPU-only environments work with no configuration. An explicit CUDA request is silently downgraded to CPU (with a warning) when no GPU is available. + embedding_space : str, optional + Representation :meth:`embed` returns (see :class:`~shapash.model.encoder.EncoderClassifierModel`). + Default ``"decision"`` (input to the final classification linear); ``"pooled"`` restores the + historical mask-pooled last hidden state. + pool : {"mean", "cls", "max"} or callable, optional + How a token-level output is reduced to one vector per text in :meth:`embed`. Default ``"mean"``. + Note this affects the *analysis* representation only — an ``AutoModelForSequenceClassification`` + does its own pooling internally, so :meth:`predict` is unaffected. """ def __init__( @@ -150,276 +145,22 @@ def __init__( label_names: list[str] | None = None, batch_size: int = 32, device: int | str | object | None = None, + *, + embedding_space: str = _DECISION_SPACE, + pool: Any = "mean", ) -> None: if label_names is None: id2label = getattr(getattr(classifier, "config", None), "id2label", None) if id2label: label_names = [id2label[i] for i in sorted(id2label)] - super().__init__(label_names=label_names) - self.classifier = classifier - self.tokenizer = tokenizer - self.batch_size = batch_size - self.device = device - self._pipeline = None # lazily built for SHAP - - # ------------------------------------------------------------------ - # Prediction - # ------------------------------------------------------------------ - - def predict(self, texts: list[str]) -> np.ndarray: - """Return ``(n_texts, n_classes)`` softmax probabilities in class-index order.""" - torch = import_optional_module("torch", extra=_NLP_EXTRA) - device = self.classifier.device - texts = list(texts) - out = [] - for i in range(0, len(texts), self.batch_size): - batch = self.tokenizer(texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt") - batch = {k: v.to(device) for k, v in batch.items()} - with torch.no_grad(): - logits = self.classifier(**batch).logits - probs = torch.softmax(logits, dim=-1) - out.append(probs.cpu().numpy()) - return np.vstack(out) - - def _resolve_pipeline_device(self): - """Pick the pipeline device, degrading an unavailable-GPU request to CPU with a warning.""" - torch = import_optional_module("torch", extra=_NLP_EXTRA) - if self.device is None: - # Inherit whatever device the classifier already lives on (CPU-safe by construction). - return self.classifier.device - wants_cuda = (isinstance(self.device, str) and self.device.startswith("cuda")) or ( - isinstance(self.device, int) and self.device >= 0 + super().__init__( + classifier, + tokenizer, + label_names=label_names, + batch_size=batch_size, + device=device, + pool=pool, + embedding_space=embedding_space, ) - if wants_cuda and not torch.cuda.is_available(): - warnings.warn( - f"Requested device {self.device!r} but no CUDA GPU is available; " - "falling back to CPU for the SHAP pipeline.", - stacklevel=2, - ) - return torch.device("cpu") - return self.device - - @property - def shap_callable(self): - """A ``text-classification`` pipeline built from the classifier, for SHAP's TextMasker.""" - if self._pipeline is None: - transformers = import_optional_module("transformers", extra=_NLP_EXTRA) - self._pipeline = transformers.pipeline( - "text-classification", - model=self.classifier, - tokenizer=self.tokenizer, - top_k=None, - device=self._resolve_pipeline_device(), - batch_size=self.batch_size, - truncation=True, - ) - return self._pipeline - - # ------------------------------------------------------------------ - # Tokenization - # ------------------------------------------------------------------ - - def tokenize(self, text: str) -> list[str]: - """Return the classifier tokenizer's tokens for ``text``.""" - return self.tokenizer.tokenize(text) - - def detokenize(self, tokens: list[str]) -> str: - """Rebuild a display string from tokens (merges ``##`` word-pieces).""" - return self.tokenizer.convert_tokens_to_string(tokens) - - # ------------------------------------------------------------------ - # Embeddings - # ------------------------------------------------------------------ - - def get_embedding_table(self) -> tuple[list[str], np.ndarray]: - """Return ``(vocab, matrix)`` from the model's input-embedding layer.""" - weight = self.classifier.get_input_embeddings().weight - matrix = weight.detach().cpu().numpy() - vocab = self.tokenizer.convert_ids_to_tokens(range(matrix.shape[0])) - return vocab, matrix - - def embed(self, texts: list[str]) -> np.ndarray: - """Return mean-pooled last-layer hidden states, shape ``(n_texts, hidden_dim)``. - - Padding tokens are excluded from the mean via the attention mask. - """ - torch = import_optional_module("torch", extra=_NLP_EXTRA) - device = self.classifier.device - texts = list(texts) - out = [] - for i in range(0, len(texts), self.batch_size): - batch = self.tokenizer(texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt") - batch = {k: v.to(device) for k, v in batch.items()} - with torch.no_grad(): - hidden = self.classifier(**batch, output_hidden_states=True).hidden_states[-1] - mask = batch["attention_mask"].unsqueeze(-1) - emb = (hidden * mask).sum(1) / mask.sum(1) - out.append(emb.cpu().numpy()) - return np.vstack(out) - - # ------------------------------------------------------------------ - # Layer activations (similar-example retrieval) - # ------------------------------------------------------------------ - - @property - def default_activation_layer(self) -> str: - """The model's pooled decision-space representation, resolved per-architecture. - - Returns the first of ``pre_classifier`` (DistilBERT) / ``pooler`` (BERT, DeBERTa) that the - classifier actually exposes — the pooled ``[CLS]`` vector the head reads, a good default - decision-space for similar-example retrieval. When the model has no such module (e.g. - RoBERTa/XLM-R), returns the :data:`_LAST_HIDDEN_STATE` sentinel, and :meth:`activations` falls - back to the mean-pooled last hidden state (what :meth:`embed` computes) — a representation every - architecture provides, so retrieval needs no per-model configuration. Override either by passing - an explicit ``layer`` to :meth:`activations`. - """ - modules = dict(self.classifier.named_modules()) - for name in _POOLED_LAYER_CANDIDATES: - if name in modules: - return name - return _LAST_HIDDEN_STATE - - def activations(self, texts: list[str], layer: str | None = None) -> np.ndarray: - """Return one activation vector per text at ``layer``, shape ``(n_texts, hidden_dim)``. - - A forward hook captures the named submodule's output on each batch. A pooled ``(batch, hidden)`` - output (e.g. ``pre_classifier``) is used as-is; a token-level ``(batch, seq, hidden)`` output - (e.g. a transformer layer) is mean-pooled over non-padding tokens via the attention mask. The - model runs under ``no_grad`` — this is inference only. - """ - torch = import_optional_module("torch", extra=_NLP_EXTRA) - layer_name = layer or self.default_activation_layer - if layer_name == _LAST_HIDDEN_STATE: - # No pooled head module on this architecture — use the universal mean-pooled last hidden - # state (mask-aware), which embed() already computes. Keeps retrieval model-agnostic. - return self.embed(list(texts)) - modules = dict(self.classifier.named_modules()) - if layer_name not in modules: - raise KeyError( - f"Layer {layer_name!r} not found on the model. Available top-level modules: " - f"{[n for n, _ in self.classifier.named_children()]}." - ) - module = modules[layer_name] - - captured: list = [] - handle = module.register_forward_hook(lambda _mod, _inp, out: captured.append(out.detach())) - device = self.classifier.device - texts = list(texts) - out = [] - try: - for i in range(0, len(texts), self.batch_size): - batch = self.tokenizer( - texts[i : i + self.batch_size], padding=True, truncation=True, return_tensors="pt" - ) - batch = {k: v.to(device) for k, v in batch.items()} - captured.clear() - with torch.no_grad(): - self.classifier(**batch) - act = captured[0] - if act.dim() == 3: # token-level layer output -> mask-aware mean pool to one vector - mask = batch["attention_mask"].unsqueeze(-1) - act = (act * mask).sum(1) / mask.sum(1) - out.append(act.cpu().numpy()) - finally: - handle.remove() - return np.vstack(out) - - # ------------------------------------------------------------------ - # Gradients - # ------------------------------------------------------------------ - - def token_gradients(self, text: str, target_class: int) -> tuple[list[str], np.ndarray]: - """Return ``(tokens, grads)`` of the target-class logit w.r.t. input embeddings. - - Special tokens (``[CLS]``/``[SEP]``/padding) are dropped so the returned tokens and gradient - rows are aligned to substitutable content tokens only. - """ - device = self.classifier.device # torch is already present (classifier is a torch model) - enc = self.tokenizer(text, truncation=True, return_tensors="pt") - input_ids = enc["input_ids"].to(device) - attention_mask = enc["attention_mask"].to(device) - - embed_layer = self.classifier.get_input_embeddings() - inputs_embeds = embed_layer(input_ids) - inputs_embeds.requires_grad_(True) - inputs_embeds.retain_grad() - - self.classifier.zero_grad() - logits = self.classifier(inputs_embeds=inputs_embeds, attention_mask=attention_mask).logits - logits[0, target_class].backward() - - grads = inputs_embeds.grad[0].detach().cpu().numpy() # (seq, hidden) - ids = input_ids[0].tolist() - tokens = self.tokenizer.convert_ids_to_tokens(ids) - special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) - - keep = [i for i, is_special in enumerate(special_mask) if not is_special] - kept_tokens = [tokens[i] for i in keep] - kept_grads = grads[keep] - return kept_tokens, kept_grads - - # ------------------------------------------------------------------ - # Captum layer-attribution surface - # ------------------------------------------------------------------ - - @property - def embedding_layer(self): - """Return the word-embedding ``nn.Module`` (what ``LayerIntegratedGradients`` attributes through).""" - return self.classifier.get_input_embeddings() - - def encode(self, text: str): - """Return ``(input_ids, attention_mask, tokens)`` — batch-size-1 tensors + aligned token strings.""" - device = self.classifier.device - enc = self.tokenizer(text, truncation=True, return_tensors="pt") - input_ids = enc["input_ids"].to(device) - attention_mask = enc["attention_mask"].to(device) - tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0].tolist()) - return input_ids, attention_mask, tokens - - def reference_ids(self, input_ids): - """Return baseline ids: content tokens replaced by the pad/mask reference, special tokens kept.""" - torch = import_optional_module("torch", extra=_NLP_EXTRA) - ids = input_ids[0].tolist() - special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) - ref_id = self.tokenizer.pad_token_id - if ref_id is None: - ref_id = self.tokenizer.mask_token_id or self.tokenizer.unk_token_id or 0 - ref = [tid if is_special else ref_id for tid, is_special in zip(ids, special_mask, strict=True)] - return torch.tensor([ref], device=input_ids.device) - - def logits(self, input_ids, attention_mask): - """Return raw classification logits ``(batch, n_classes)`` (the Captum forward func).""" - return self.classifier(input_ids=input_ids, attention_mask=attention_mask).logits - - def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[int]] | None: - """Group subwords into whole words via the fast tokenizer's ``word_ids()`` (see base ABC). - - Re-encodes ``text`` with the same ``truncation=True`` as :meth:`encode`, so the returned - positions align with the token/attribution axis. ``word_ids()`` maps each subword to its word - index (``None`` for special tokens), giving exact, scheme-independent grouping — no ``##`` / ``Ġ`` - / ``▁`` string-guessing. Display strings come from ``convert_tokens_to_string`` so each word is - rebuilt correctly whatever the tokenization scheme. Returns ``None`` for a slow tokenizer (no - ``word_ids()``), letting the caller fall back to a token-string heuristic. - """ - if not getattr(self.tokenizer, "is_fast", False): - return None - enc = self.tokenizer(text, truncation=True) - word_ids = enc.word_ids() - tokens = self.tokenizer.convert_ids_to_tokens(enc["input_ids"]) - - # Special tokens (and any tokenless position) carry word_id None: fold their attribution into - # the baseline. Content subwords sharing a word_id compose one word. - special_positions = [i for i, wid in enumerate(word_ids) if wid is None] - words: list[str] = [] - word_positions: list[list[int]] = [] - prev_wid: int | None = None - for i, wid in enumerate(word_ids): - if wid is None: - continue - if wid != prev_wid: - word_positions.append([i]) - prev_wid = wid - else: - word_positions[-1].append(i) - words = [self.tokenizer.convert_tokens_to_string([tokens[i] for i in pos]).strip() for pos in word_positions] - return words, word_positions, special_positions + # ``classifier`` is the backbone; keep the historical alias for introspection and tests. + self.classifier = classifier diff --git a/shapash/model/torch_models.py b/shapash/model/torch_models.py new file mode 100644 index 00000000..977adb2b --- /dev/null +++ b/shapash/model/torch_models.py @@ -0,0 +1,444 @@ +"""External-head presets: wrap a separate encoder body + pooling + classification head. + +:class:`~shapash.model.encoder.EncoderClassifierModel` needs a *backbone* that maps input ids/embeds to +``.logits`` with pooling and the head baked in. A raw ``AutoModelForSequenceClassification`` already is +such a backbone (see :class:`~shapash.model.hf.HFClassifierModel`). Sentence-transformer classifiers and +hand-rolled PyTorch classifiers instead keep the encoder body, the pooling, and the head as *separate* +pieces. :func:`build_encoder_head_backbone` fuses those pieces into one backbone honouring the contract, +so both get the full capability surface with no bespoke per-model code. + +* :class:`TorchClassifierModel` — the general, framework-agnostic preset: ``(body, head, tokenizer)``. +* :class:`SentenceTransformerModel` — extracts ``body``/``tokenizer``/pooling-mode from a + ``sentence_transformers`` model, then defers to the same machinery with a caller-supplied head. +""" + +from __future__ import annotations + +import hashlib +import logging +import warnings +from types import SimpleNamespace +from typing import Any + +# This module is imported lazily by ``shapash.model`` (see its ``__getattr__``) precisely so that torch +# can be imported here at module level: ``_EncoderHeadBackbone`` subclasses ``nn.Module``, which cannot +# be done inside a lazy helper without minting a new class per call. Core installs never load this file. +import torch + +from shapash._optional import import_optional_module +from shapash.model.encoder import _DECISION_SPACE, EncoderClassifierModel, _pool_hidden + +logger = logging.getLogger(__name__) + + +def _extract_hidden(body_output: Any) -> Any: + """Return the token-level ``(batch, seq, hidden)`` tensor from an encoder body's output. + + Handles the common return shapes: a HuggingFace output with ``.last_hidden_state``, a bare tensor, + or a tuple/list whose first element is the hidden state. + """ + hidden = getattr(body_output, "last_hidden_state", None) + if hidden is not None: + return hidden + if isinstance(body_output, (tuple, list)): + return body_output[0] + return body_output + + +class _EncoderHeadBackbone(torch.nn.Module): + """``body -> pool -> [normalize] -> head`` fused into one HF-model-like backbone. + + Built by :func:`build_encoder_head_backbone`; see that function for the contract it honours. + """ + + def __init__(self, body, head, pool, normalize): + super().__init__() + self.body = body + self.head = head + self.pool = pool + self.normalize = normalize + + def forward( + self, + input_ids=None, + inputs_embeds=None, + attention_mask=None, + output_hidden_states=False, + **_kwargs, + ): + # Only pass the id/embed argument the caller actually supplied; some bodies reject a None + # inputs_embeds alongside input_ids. token_type_ids etc. are intentionally not threaded + # through — the historical HF gradient/logits paths did not either. + if inputs_embeds is not None: + body_out = self.body(inputs_embeds=inputs_embeds, attention_mask=attention_mask) + else: + body_out = self.body(input_ids=input_ids, attention_mask=attention_mask) + hidden = _extract_hidden(body_out) + if attention_mask is None: + attention_mask = torch.ones(hidden.shape[:2], device=hidden.device) + pooled = _pool_hidden(hidden, attention_mask, self.pool) + if self.normalize: + pooled = torch.nn.functional.normalize(pooled, p=2, dim=-1) + logits = self.head(pooled) + out = SimpleNamespace(logits=logits) + if output_hidden_states: + out.hidden_states = (hidden,) + return out + + def get_input_embeddings(self): + """Return the body's word-embedding module (what LIG attributes through).""" + return self.body.get_input_embeddings() + + @property + def config(self): + """Expose the body's HF config, so backbone-contract consumers see the real checkpoint. + + Without this the fused backbone looks anonymous and everything that identifies a model by + ``config._name_or_path`` (notably ``TextModel.model_id``, which keys downstream caches) + would read the same value for every external-head model. + """ + return getattr(self.body, "config", None) + + @property + def device(self): + """Device the fused module's parameters live on.""" + return next(self.parameters()).device + + +def build_encoder_head_backbone(body, head, pool: Any, normalize: bool = False): + """Fuse an encoder ``body``, a ``pool`` strategy, and a classification ``head`` into one backbone. + + The returned ``nn.Module`` honours the backbone contract + (:mod:`shapash.model.encoder`): it accepts ``input_ids``/``inputs_embeds`` (+ ``attention_mask``), + runs ``body -> pool -> [normalize] -> head`` to produce ``.logits``, exposes ``.hidden_states`` on + request, and forwards ``get_input_embeddings`` / ``.device`` to the body. Pooling (and the optional + L2 normalization) happen *inside* the differentiated path, so Captum ``LayerIntegratedGradients`` + attributes embeddings -> logits through them. + + Parameters + ---------- + body : nn.Module + Encoder returning a token-level last hidden state (e.g. a HF ``AutoModel``). Must accept + ``inputs_embeds`` for the gradient/Captum paths and expose ``get_input_embeddings()``. + head : nn.Module + Maps the pooled ``(batch, hidden)`` vector to ``(batch, n_classes)`` logits. + pool : {"mean", "cls", "max"} or callable + Pooling strategy applied between body and head; see :func:`~shapash.model.encoder._pool_hidden`. + normalize : bool, optional + L2-normalize the pooled vector before the head. Default ``False``. Reproduces a + sentence-transformer ``Normalize`` module so a head trained on unit-length embeddings receives + them. L2 normalization is differentiable, so gradients/LIG still flow embeddings -> logits. + + Returns + ------- + _EncoderHeadBackbone + A backbone instance ready to pass to :class:`~shapash.model.encoder.EncoderClassifierModel`. + """ + return _EncoderHeadBackbone(body, head, pool, normalize) + + +class TorchClassifierModel(EncoderClassifierModel): + """Full-capability adapter over a hand-rolled ``(encoder body, classification head)`` pair. + + Use this for any PyTorch text classifier whose encoder and head are separate modules (i.e. the head + is not baked into a single ``AutoModelForSequenceClassification``). The body must expose a + token-embedding layer to attribute through (``get_input_embeddings()``) and accept ``inputs_embeds``; + given that, every capability — gradients, Captum LIG, similar-example retrieval — is available. + + Parameters + ---------- + body : nn.Module + Encoder returning a token-level last hidden state; accepts ``inputs_embeds`` and exposes + ``get_input_embeddings()``. + head : nn.Module + Maps the pooled ``(batch, hidden)`` vector to ``(batch, n_classes)`` logits. + tokenizer : transformers.PreTrainedTokenizer + Tokenizer matching ``body``. A fast tokenizer enables exact word alignment. + label_names : list[str] or None + Class names in class-index order. + pool : {"mean", "cls", "max"} or callable, optional + Pooling between body and head. Default ``"mean"``. + normalize : bool, optional + L2-normalize the pooled vector before the head (and in :meth:`embed`). Default ``False``. Set + this when the head was trained on unit-length sentence embeddings (a sentence-transformer + ``Normalize`` step); :class:`SentenceTransformerModel` detects and sets it automatically. + embedding_space : str, optional + Representation :meth:`embed` returns (see :class:`~shapash.model.encoder.EncoderClassifierModel`). + Default ``"decision"`` (input to the final classification linear) — for an external head this + reaches into it, so the projection stays class-discriminative even with a frozen encoder. + batch_size : int, optional + Batch size. Default 32. + device : int or str or torch.device or None, optional + Device for the SHAP pipeline; inherits the body's device when ``None``. + max_length : int or None, optional + Truncation length for every tokenizer call. When ``None`` (default) the tokenizer's own + ``model_max_length`` applies — **which is often longer than the length the body is actually + served at**. Loading ``sentence-transformers/all-MiniLM-L6-v2`` via ``AutoTokenizer`` reports 512, + while the same model loaded through ``SentenceTransformer`` truncates at 256; on a 400-token text + the resulting sentence embeddings differ by ~0.02 per component on unit-length vectors, and the + head then scores an input it was never trained on. Pass the length the body is served at (here + ``max_length=256``) whenever the tokenizer's limit does not already reflect it. + + Notes + ----- + The SHAP backend cannot build a ``transformers`` pipeline here (the fused backbone is not a + ``PreTrainedModel``), so :attr:`shap_callable` is the plain :meth:`predict` function and + :attr:`shap_masker` supplies an explicit ``shap.maskers.Text`` over the tokenizer — the two pieces + SHAP's text explainer needs. The Captum LIG backend works unchanged. + """ + + def __init__( + self, + body, + head, + tokenizer, + label_names: list[str] | None = None, + *, + pool: Any = "mean", + normalize: bool = False, + embedding_space: str = _DECISION_SPACE, + batch_size: int = 32, + device: int | str | object | None = None, + max_length: int | None = None, + ) -> None: + backbone = build_encoder_head_backbone(body, head, pool, normalize=normalize) + super().__init__( + backbone, + tokenizer, + label_names=label_names, + batch_size=batch_size, + device=device, + pool=pool, + normalize=normalize, + embedding_space=embedding_space, + max_length=max_length, + ) + self.body = body + self.head = head + self._head_fingerprint: str | None = None + + @property + def model_id(self) -> str: + """Identity including a fingerprint of the **head's weights**, on top of the base identity. + + The base :attr:`~shapash.model.base.TextModel.model_id` identifies the body checkpoint, pooling + and normalization — but here the head is a separate, independently-trained module, and two + models sharing a frozen body (the common sentence-transformers case) differ *only* in it. + Without the head in the key they would silently share a cached embedding/neighbour bank. Heads + are small, so the fingerprint hashes their actual parameter bytes rather than just the + architecture; it is computed once and memoized. + """ + if self._head_fingerprint is None: + self._head_fingerprint = self._fingerprint_head() + return f"{super().model_id}:head={self._head_fingerprint}" + + def _fingerprint_head(self) -> str: + """Return a short stable hash of the head's parameter values (architecture + weights).""" + digest = hashlib.sha256() + try: + for name, tensor in sorted(self.head.state_dict().items()): + digest.update(name.encode()) + digest.update(tensor.detach().cpu().numpy().tobytes()) + except (AttributeError, RuntimeError, TypeError): + # A head without a usable state_dict: fall back to its architecture repr. Weaker (two + # identically-shaped heads collide) but never wrong-by-crash, and still separates shapes. + logger.warning("Could not hash head weights for the cache key; falling back to its repr().") + digest.update(repr(self.head).encode()) + return digest.hexdigest()[:16] + + @property + def shap_callable(self): + """The plain scoring callable SHAP's text explainer wraps (``predict``). + + Unlike :class:`~shapash.model.hf.HFClassifierModel` (whose backbone is a ``PreTrainedModel`` that + SHAP wraps as a pipeline, auto-inferring a masker), an external-head model exposes only + ``predict``; the companion :attr:`shap_masker` provides the text masker SHAP would otherwise + infer from a pipeline. + """ + return self.predict + + @property + def shap_masker(self): + """A ``shap.maskers.Text`` over the tokenizer, so SHAP can segment/mask a plain callable. + + Overrides :attr:`~shapash.model.base.TextModel.shap_masker` (which returns ``None``, letting + SHAP infer a masker from a pipeline) because :attr:`shap_callable` here is a bare function with + no tokenizer for SHAP to find. ``NlpExplainer`` forwards it to ``NlpShapBackend``. + """ + shap = import_optional_module("shap") + return shap.maskers.Text(self.tokenizer) + + +# Sentence-transformers pooling modes this module can reproduce, mapped onto ``_pool_hidden`` strategies. +# Modern ST (>=2.x) exposes a single ``pooling_mode`` string on its ``Pooling`` module; older releases +# set one of several ``pooling_mode_*_tokens`` booleans instead. Both are handled below. +_ST_MODE_MAP = {"cls": "cls", "max": "max", "mean": "mean"} +_ST_POOLING_FLAGS = ( + ("pooling_mode_cls_token", "cls"), + ("pooling_mode_max_tokens", "max"), + ("pooling_mode_mean_tokens", "mean"), +) + + +def _sentence_transformer_pool(st_model) -> str: + """Read the pooling mode from a ``sentence_transformers`` model's ``Pooling`` module. + + Returns one of ``"cls"``/``"max"``/``"mean"``. Raises when the model uses an exotic ST pooling mode + (``mean_sqrt_len_tokens``, ``weightedmean``, ``lasttoken``) that :func:`~shapash.model.encoder._pool_hidden` + does not reproduce — silently mean-pooling there would diverge from how the head was trained, so the + caller must pass an explicit ``pool`` instead. + """ + for module in st_model: + mode = getattr(module, "pooling_mode", None) + if isinstance(mode, str): + if mode in _ST_MODE_MAP: + return _ST_MODE_MAP[mode] + raise ValueError( + f"sentence-transformers pooling mode {mode!r} is not reproduced by " + f"SentenceTransformerModel (supported: {sorted(_ST_MODE_MAP)}). Pass an explicit " + "`pool=` (mode name or callable) matching how the classification head was trained." + ) + for flag, mapped in _ST_POOLING_FLAGS: # legacy ST boolean flags + if getattr(module, flag, False): + return mapped + return "mean" + + +def _sentence_transformer_max_length(st_model) -> int | None: + """Return the sentence-transformers model's ``max_seq_length``, or ``None`` when unset. + + Loading *through* ``SentenceTransformer`` already syncs this onto the tokenizer's + ``model_max_length`` (both 256 for ``all-MiniLM-L6-v2``), so for this preset ``truncation=True`` + alone would agree with ``st_model.encode`` anyway — reading it here is belt-and-braces. + + It is worth carrying for two reasons. The sync is an ST implementation detail, not a documented + guarantee; and it makes the served length explicit and inspectable as ``model.max_length``, matching + :class:`TorchClassifierModel`, where the same value must be passed by hand because a raw + ``AutoTokenizer`` for the *same checkpoint* reports 512 rather than 256. + """ + max_length = getattr(st_model, "max_seq_length", None) + if max_length is None: + first = st_model[0] + max_length = getattr(first, "max_seq_length", None) + return int(max_length) if max_length else None + + +def _sentence_transformer_normalize(st_model) -> bool: + """Return whether the sentence-transformers model ends its forward in an L2 ``Normalize`` module. + + Many ST embedding models (e.g. ``all-MiniLM-L6-v2``) append a ``Normalize`` module after pooling, so + ``st_model.encode`` returns unit-length vectors and a downstream head is trained on them. When present + we reproduce it (see :func:`build_encoder_head_backbone`) so ``predict`` stays faithful. Post-pooling + modules other than ``Normalize`` (e.g. a ``Dense`` projection) are *not* reproduced — warn so the user + can pass an explicit head/pooling that accounts for them rather than get silently divergent scores. + """ + normalizes = False + for idx, module in enumerate(st_model): + name = type(module).__name__ + if name == "Normalize": + normalizes = True + elif idx >= 1 and name not in ("Pooling", "Normalize"): + # Anything after the transformer body that isn't pooling/normalize changes the sentence + # vector in a way this adapter does not replicate. + warnings.warn( + f"sentence-transformers module {name!r} after pooling is not reproduced by " + "SentenceTransformerModel; embeddings/predictions may diverge from `st_model.encode`. " + "Pass a TorchClassifierModel with an explicit body/head if this module matters.", + stacklevel=3, + ) + return normalizes + + +class SentenceTransformerModel(TorchClassifierModel): + """Full-capability adapter for a ``sentence_transformers`` encoder + a classification ``head``. + + Extracts the underlying HuggingFace transformer body, its tokenizer, and the configured pooling + mode from ``st_model``, then reuses :class:`TorchClassifierModel`'s ``body -> pool -> head`` machinery. + The head is supplied by the caller (sentence-transformers keeps the classifier separate from the + embedding model). + + Parameters + ---------- + st_model : sentence_transformers.SentenceTransformer + A sentence-transformers model whose first module wraps a HF transformer (``.auto_model``) and + exposes a ``.tokenizer``, and which contains a ``Pooling`` module. + head : nn.Module + Maps the pooled ``(batch, hidden)`` sentence embedding to ``(batch, n_classes)`` logits. + label_names : list[str] or None + Class names in class-index order. + pool : {"mean", "cls", "max"} or callable or None, optional + Override the pooling mode. When ``None`` (default), read from ``st_model``'s ``Pooling`` module. + normalize : bool or None, optional + Whether to L2-normalize the sentence embedding before the head. When ``None`` (default), + detected from ``st_model`` — set to ``True`` if it ends in a ``Normalize`` module (e.g. + ``all-MiniLM-L6-v2``), so a head trained on ``st_model.encode`` output receives unit-length + vectors. Pass ``True``/``False`` to override. + embedding_space : str, optional + Representation :meth:`embed` returns (see :class:`~shapash.model.encoder.EncoderClassifierModel`). + Default ``"decision"`` (input to the final classification linear). Because the encoder is + typically frozen here, this is what keeps the projection scatter class-discriminative; pass + ``"pooled"`` to project the raw sentence embedding instead. + batch_size : int, optional + Batch size. Default 32. + device : int or str or torch.device or None, optional + Device for the SHAP pipeline; inherits the body's device when ``None``. + max_length : int or None, optional + Truncation length. When ``None`` (default), read from the model's ``max_seq_length`` so the + length this adapter uses is explicit rather than implicit in the tokenizer. Sentence-transformers + normally syncs the two already, so this rarely changes behaviour; pass a value to override. + """ + + def __init__( + self, + st_model, + head, + label_names: list[str] | None = None, + *, + pool: Any = None, + normalize: bool | None = None, + embedding_space: str = _DECISION_SPACE, + batch_size: int = 32, + device: int | str | object | None = None, + max_length: int | None = None, + ) -> None: + body = self._extract_body(st_model) + tokenizer = getattr(st_model, "tokenizer", None) or getattr(st_model[0], "tokenizer", None) + if tokenizer is None: + raise ValueError( + "Could not find a tokenizer on the sentence-transformers model. Pass a " + "TorchClassifierModel with an explicit tokenizer instead." + ) + resolved_pool = pool if pool is not None else _sentence_transformer_pool(st_model) + resolved_normalize = normalize if normalize is not None else _sentence_transformer_normalize(st_model) + resolved_max_length = max_length if max_length is not None else _sentence_transformer_max_length(st_model) + super().__init__( + body, + head, + tokenizer, + label_names=label_names, + pool=resolved_pool, + normalize=resolved_normalize, + embedding_space=embedding_space, + batch_size=batch_size, + device=device, + max_length=resolved_max_length, + ) + self.st_model = st_model + + @staticmethod + def _extract_body(st_model): + """Return the underlying HF transformer module (``auto_model``) from a ST model's first module.""" + try: + first = st_model[0] + except (TypeError, KeyError, IndexError) as exc: + raise ValueError( + "sentence-transformers model is not indexable as expected; cannot locate its " + "transformer body. Use TorchClassifierModel with an explicit body instead." + ) from exc + body = getattr(first, "auto_model", None) + if body is None: + raise ValueError( + "The sentence-transformers model's first module exposes no `.auto_model` HF transformer; " + "cannot attribute through its embeddings. Use TorchClassifierModel with an explicit body." + ) + return body From 59a64ab552cb9b6ac44a6c83b69a3d4053be5f0f Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:45:59 +0200 Subject: [PATCH 076/144] feat(explainer): add embedding store and compute projection method --- shapash/compute/__init__.py | 11 +- shapash/compute/embedding_store.py | 141 ++++++++++++++++++ shapash/compute/retrieval/__init__.py | 2 +- shapash/compute/retrieval/similar_examples.py | 106 +++++++------ shapash/explainer/nlp_explainer.py | 139 +++++++++++++---- .../webapp/nlp_components/similar_examples.py | 4 +- 6 files changed, 314 insertions(+), 89 deletions(-) create mode 100644 shapash/compute/embedding_store.py diff --git a/shapash/compute/__init__.py b/shapash/compute/__init__.py index 28b39c4a..a7b6e0b0 100644 --- a/shapash/compute/__init__.py +++ b/shapash/compute/__init__.py @@ -1,8 +1,9 @@ """Compute layer — heavy computation behind modality-agnostic interfaces. -Seeded by the NLP prototype ahead of the master refactoring. Today it holds only the -``generators`` sub-package (counterfactual / what-if generation). The master plan's -``analyses/{local,global,diagnostics}`` interpreters will move here in later phases; -``generators`` is a *new* sibling axis for generative components (LIT's ``Generator``, -as opposed to explanation ``Interpreter``s). +Seeded by the NLP prototype ahead of the master refactoring. Today it holds the ``generators`` +sub-package (counterfactual / what-if generation), the ``retrieval`` sub-package (similar-example +lookup), and :mod:`~shapash.compute.embedding_store` — the shared cache both retrieval and the 2-D +scatter draw their vectors from. The master plan's ``analyses/{local,global,diagnostics}`` +interpreters will move here in later phases; ``generators`` is a *new* sibling axis for generative +components (LIT's ``Generator``, as opposed to explanation ``Interpreter``s). """ diff --git a/shapash/compute/embedding_store.py b/shapash/compute/embedding_store.py new file mode 100644 index 00000000..7e5fd76f --- /dev/null +++ b/shapash/compute/embedding_store.py @@ -0,0 +1,141 @@ +"""Cached embeddings for a fixed corpus in a model's current representation space. + +Embedding a corpus is the expensive, repeated step behind more than one feature: similar-example +retrieval needs one vector per reference text, and the 2-D scatter needs one vector per compiled +text. Both are a pure function of *(model identity, effective space, corpus)*, so both belong behind +one cache with one key — otherwise each caller invents its own filename and they drift, which is how +a bank built in the ``"decision"`` space ends up reloaded for a scatter drawn in ``"pooled"``. + +:class:`EmbeddingStore` is that one place. It owns the key and the on-disk layout; callers ask for +:meth:`~EmbeddingStore.vectors` and, when they derive something further from them (a 2-D projection), +park it next to the embeddings under the same key via :meth:`~EmbeddingStore.cached_array`. +""" + +from __future__ import annotations + +import hashlib +import logging +from collections.abc import Callable, Sequence +from pathlib import Path + +import numpy as np + +from shapash.model.base import EmbeddingSource + +logger = logging.getLogger(__name__) + + +def _hash_corpus(texts: Sequence[str], key: str) -> str: + """Stable digest over a model/space key plus the corpus texts (order-sensitive).""" + h = hashlib.md5(usedforsecurity=False) + h.update(f"{key}\0".encode()) + for text in texts: + h.update(text.encode()) + h.update(b"\0") + return h.hexdigest() + + +class EmbeddingStore: + """One vector per text, computed once and cached to disk. + + Parameters + ---------- + model : EmbeddingSource + A model exposing ``model_id``, ``resolve_space`` and ``embed``. + texts : sequence of str + The corpus to embed. Fixed for the lifetime of the store — the cache key is derived from it. + cache_dir : str or Path or None, optional + Where cached ``.npy`` files live. When ``None`` the store still memoizes in memory but writes + nothing, so a fresh process recomputes. + + Notes + ----- + The key is ``model_id | resolve_space() | corpus-hash``. ``model_id`` is the model's own + declaration of what makes it distinct (checkpoint, pooling, normalization, head weights) and + ``resolve_space()`` is the single place that knows which space :meth:`embed` will *actually* use, + so ``None`` (meaning "the model's default") can never collide with the default it stands for. + + Examples + -------- + >>> store = EmbeddingStore(model, train_texts, cache_dir="cache/") + >>> store.vectors().shape + (5000, 384) + """ + + def __init__( + self, + model: EmbeddingSource, + texts: Sequence[str], + cache_dir: str | Path | None = None, + ) -> None: + self.model = model + self.texts = list(texts) + self.cache_dir = Path(cache_dir) if cache_dir is not None else None + self._arrays: dict[str, np.ndarray] = {} + + @property + def space_key(self) -> str: + """Readable ``model_id|space`` half of the key — what gets logged when a lookup misses.""" + return f"{self.model.model_id}|{self.model.resolve_space()}" + + @property + def key(self) -> str: + """The cache key: model identity, effective space, and corpus digest.""" + return _hash_corpus(self.texts, self.space_key) + + def path(self, tag: str) -> Path | None: + """On-disk location of the ``tag`` artifact, or ``None`` when caching is off.""" + if self.cache_dir is None: + return None + return self.cache_dir / f"{self.key}.{tag}.npy" + + def vectors(self) -> np.ndarray: + """Return ``(n_texts, hidden_dim)`` embeddings in the model's current space.""" + return self.cached_array("emb", lambda: np.asarray(self.model.embed(self.texts))) + + def cached_array(self, tag: str, compute: Callable[[], np.ndarray]) -> np.ndarray: + """Return the ``tag`` array for this corpus, loading it or computing and storing it. + + Parameters + ---------- + tag : str + Names the artifact within this store's key (``"emb"`` for the embeddings themselves, + e.g. ``"pca-1f3c.proj"`` for a projection derived from them). Anything that changes the + array's contents but not the key must be reflected here. + compute : callable + Produces the array on a cache miss. Called at most once per process. + + Returns + ------- + np.ndarray + The cached array. Memoized in memory, so repeat calls are free. + """ + if tag in self._arrays: + return self._arrays[tag] + cache_file = self.path(tag) + if cache_file is not None and cache_file.exists(): + logger.info("Embedding store hit (%s) — loading %s", tag, cache_file) + self._arrays[tag] = np.load(cache_file) + return self._arrays[tag] + + logger.info("Embedding store miss (%s) — computing over %d texts (%s)", tag, len(self.texts), self.space_key) + array = compute() + self._arrays[tag] = array + if cache_file is not None: + cache_file.parent.mkdir(parents=True, exist_ok=True) + np.save(cache_file, array) + logger.info("Embedding store cached (%s) to %s", tag, cache_file) + return array + + def clear(self) -> None: + """Drop every cached artifact for this corpus — in memory and, when caching, on disk. + + Removes *all* tags under this key, not just the embeddings, so a caller forcing a recompute + cannot leave a projection behind that was derived from vectors no longer on disk. + """ + self._arrays.clear() + if self.cache_dir is None: + return + for stale in self.cache_dir.glob(f"{self.key}.*.npy"): + stale.unlink(missing_ok=True) + logger.info("Embedding store dropped %s", stale) diff --git a/shapash/compute/retrieval/__init__.py b/shapash/compute/retrieval/__init__.py index 433f032a..744ea67d 100644 --- a/shapash/compute/retrieval/__init__.py +++ b/shapash/compute/retrieval/__init__.py @@ -3,7 +3,7 @@ Where ``compute/generators/`` perturbs an input (counterfactuals), this package looks *outward* to a reference corpus and answers "which examples most resemble this one?". The first method is :class:`~shapash.compute.retrieval.similar_examples.SimilarExampleRetriever` — cosine similarity -between a query and a corpus in a chosen layer's activation space (a lightweight, in-house take on +between a query and a corpus in the model's representation space (a lightweight, in-house take on Captum's ``SimilarityInfluence``, keyed to the capability-based model layer). """ diff --git a/shapash/compute/retrieval/similar_examples.py b/shapash/compute/retrieval/similar_examples.py index 0b8db088..fec079fc 100644 --- a/shapash/compute/retrieval/similar_examples.py +++ b/shapash/compute/retrieval/similar_examples.py @@ -1,15 +1,16 @@ -"""Similar-example retrieval: nearest reference examples in a layer's activation space. +"""Similar-example retrieval: nearest reference examples in a model's representation space. For a prediction, this retrieves the reference-corpus examples whose internal representation is most similar to the query's — the examples the model treats most alike. It is the in-house counterpart of Captum's ``SimilarityInfluence``, reimplemented over the capability-based model layer -(:class:`~shapash.model.base.SupportsActivations`) so it needs no per-model wiring and carries none of +(:class:`~shapash.model.base.SupportsEmbeddings`) so it needs no per-model wiring and carries none of Captum 0.9.0's single-layer / short-final-batch quirks. The cost splits cleanly: -* **Bank** — one activation vector per reference example, computed once and cached to a single ``.npy`` - matrix (keyed by the corpus hash, layer, and model id). This is the expensive, amortizable part. -* **Query** — one activation vector for the query text, then cosine similarity against the bank and a - top-k selection. Milliseconds, so it runs live per selection in the webapp. +* **Bank** — one vector per reference example, computed once and cached by + :class:`~shapash.compute.embedding_store.EmbeddingStore` (keyed by the corpus hash, space, and model + id). This is the expensive, amortizable part, and the same cache the 2-D scatter draws from. +* **Query** — one vector for the query text, then cosine similarity against the bank and a top-k + selection. Milliseconds, so it runs live per selection in the webapp. This is a *representation-similarity* (nearest-neighbour) method, **not** a leave-one-out / influence- function measure: it surfaces the examples most alike in decision space, a cheap and faithful proxy for @@ -18,13 +19,16 @@ from __future__ import annotations -import hashlib +import logging from dataclasses import dataclass from pathlib import Path import numpy as np -from shapash.model.base import SupportsActivations, has_capabilities +from shapash.compute.embedding_store import EmbeddingStore +from shapash.model.base import EmbeddingSource, SupportsEmbeddings, has_capabilities + +logger = logging.getLogger(__name__) @dataclass(frozen=True) @@ -36,7 +40,7 @@ class Neighbor: index : int Position of the example within the reference corpus. score : float - Cosine similarity to the query in the chosen layer's activation space (higher = more similar). + Cosine similarity to the query in the model's representation space (higher = more similar). text : str The reference example's text. label : str or None @@ -49,33 +53,37 @@ class Neighbor: label: str | None = None -def _hash_corpus(texts: list[str], layer: str, model_id: str) -> str: - """Stable cache key over the corpus texts + layer + model id (order-sensitive).""" - h = hashlib.md5(usedforsecurity=False) - h.update(f"{model_id}\0{layer}\0".encode()) - for t in texts: - h.update(t.encode()) - h.update(b"\0") - return h.hexdigest() - - class SimilarExampleRetriever: - """Retrieve the reference examples most similar to a query in a model layer's activation space. + """Retrieve the reference examples most similar to a query in the model's representation space. Parameters ---------- - model : SupportsActivations - A model exposing :meth:`~shapash.model.base.SupportsActivations.activations`. + model : EmbeddingSource + A model exposing :meth:`~shapash.model.base.SupportsEmbeddings.embed` plus the identity and + space resolution the embedding cache keys on (see + :class:`~shapash.model.base.EmbeddingSource`). Admission is still checked by *capability* + (``SupportsEmbeddings``) below. reference_texts : list[str] The corpus to retrieve from (typically the model's training set). reference_labels : list[str] or None, optional Labels aligned with ``reference_texts`` — surfaced on each :class:`Neighbor` when present. - layer : str or None, optional - Fully-qualified layer name to compare in. Defaults to the model's - :attr:`~shapash.model.base.SupportsActivations.default_activation_layer`. cache_dir : str or Path or None, optional - When given, the activation bank is persisted to ``<cache_dir>/<hash>.sim.npy`` and reloaded - on later runs (keyed by the corpus + layer + model id), so only the first build pays the cost. + When given, the reference embeddings are persisted here by + :class:`~shapash.compute.embedding_store.EmbeddingStore` and reloaded on later runs, so only + the first build pays the cost. + + Notes + ----- + The comparison space is deliberately **not** a parameter here: it is whatever the model's + :attr:`~shapash.model.encoder.EncoderClassifierModel.embedding_space` currently is. Letting a + caller override it per-retriever allowed the scatter and the neighbours to sit in different spaces + — you would select a cluster in one space and rank neighbours in another, with nothing saying so. + Change the model's space to move both together. + + This matches the 2-D scatter *when the scatter is built by* + :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection`, which reads the same + space through the same :class:`~shapash.compute.embedding_store.EmbeddingStore`. Coordinates passed + in by hand via ``run_app(scatter_xy=...)`` are outside that guarantee. Examples -------- @@ -86,16 +94,15 @@ class SimilarExampleRetriever: def __init__( self, - model: SupportsActivations, + model: EmbeddingSource, reference_texts: list[str], reference_labels: list[str] | None = None, - layer: str | None = None, cache_dir: str | Path | None = None, ) -> None: - if not has_capabilities(model, SupportsActivations): + if not has_capabilities(model, SupportsEmbeddings): raise TypeError( - f"{type(model).__name__} does not support layer activations " - "(SupportsActivations); similar-example retrieval is unavailable." + f"{type(model).__name__} does not support embeddings (SupportsEmbeddings); " + "similar-example retrieval is unavailable." ) if reference_labels is not None and len(reference_labels) != len(reference_texts): raise ValueError( @@ -105,8 +112,7 @@ def __init__( self.model = model self.reference_texts = list(reference_texts) self.reference_labels = list(reference_labels) if reference_labels is not None else None - self.layer = layer or model.default_activation_layer - self.cache_dir = Path(cache_dir) if cache_dir is not None else None + self.store = EmbeddingStore(model, self.reference_texts, cache_dir=cache_dir) self._bank: np.ndarray | None = None # (n_reference, hidden), L2-normalized rows @property @@ -114,31 +120,23 @@ def size(self) -> int: """Number of reference examples.""" return len(self.reference_texts) - def _cache_file(self) -> Path | None: - """Path of the on-disk bank for this corpus/layer/model, or ``None`` when caching is off.""" - if self.cache_dir is None: - return None - model_id = getattr(self.model, "model_id", None) or type(self.model).__name__ - return self.cache_dir / f"{_hash_corpus(self.reference_texts, self.layer, str(model_id))}.sim.npy" - def build(self) -> None: - """Compute (or load) and L2-normalize the reference activation bank. + """Compute (or load) and L2-normalize the reference embedding bank. Called automatically on the first :meth:`query`; call it up front to pay the cost eagerly - (e.g. at app start). Rows are stored L2-normalized so a query becomes a single matrix-vector - product. Idempotent — a second call is a no-op once the bank is in memory. + (e.g. at app start). The raw vectors come from the shared + :class:`~shapash.compute.embedding_store.EmbeddingStore`; normalization is applied here rather + than cached, since it is a single cheap pass and keeps the stored artifact the plain embeddings + that other consumers (e.g. the scatter projection) can reuse. Rows end up unit-norm so a query + is a single matrix-vector product. Idempotent. """ if self._bank is not None: return - cache_file = self._cache_file() - if cache_file is not None and cache_file.exists(): - self._bank = np.load(cache_file) - return - raw = self.model.activations(self.reference_texts, self.layer) - self._bank = _l2_normalize(raw) - if cache_file is not None: - cache_file.parent.mkdir(parents=True, exist_ok=True) - np.save(cache_file, self._bank) + self._bank = _l2_normalize(self.store.vectors()) + + def _encode(self, texts: list[str]) -> np.ndarray: + """Embed ``texts`` in the model's configured space — the same one the scatter projects.""" + return self.model.embed(texts) def query(self, text: str, top_k: int = 5) -> list[Neighbor]: """Return the ``top_k`` reference examples most similar to ``text``, most-similar first. @@ -157,7 +155,7 @@ def query(self, text: str, top_k: int = 5) -> list[Neighbor]: """ self.build() assert self._bank is not None # noqa: S101 - build() guarantees this - query_vec = _l2_normalize(self.model.activations([text], self.layer))[0] + query_vec = _l2_normalize(self._encode([text]))[0] sims = self._bank @ query_vec # cosine, rows already unit-norm k = max(1, min(top_k, self.size)) # argpartition for the top-k, then sort just those descending. diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 023c2f9c..17d1a03c 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -18,16 +18,19 @@ from dataclasses import dataclass from pathlib import Path +import numpy as np import pandas as pd from plotly import graph_objs as go +from sklearn.decomposition import PCA from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.embedding_store import EmbeddingStore from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever -from shapash.model.base import SupportsActivations, TextModel, has_capabilities +from shapash.model.base import SupportsEmbeddings, TextModel, has_capabilities from shapash.model.hf import HFPipelineModel from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp @@ -54,6 +57,22 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.pkl" +def _reducer_tag(reducer: object) -> str: + """Name a reducer for a cache tag: its class plus a digest of its parameters when it exposes them. + + Two runs of the same reducer class with different settings produce different coordinates, so the + class name alone would silently reload the wrong scatter. ``get_params()`` (sklearn's convention, + which ``pacmap`` also follows) makes the settings part of the tag. A reducer without it is keyed by + class name only — documented on :meth:`NlpExplainer.compute_projection` as needing ``recompute``. + """ + name = type(reducer).__name__.lower() + get_params = getattr(reducer, "get_params", None) + if get_params is None: + return name + digest = hashlib.md5(repr(sorted(get_params().items())).encode(), usedforsecurity=False).hexdigest() + return f"{name}-{digest[:8]}" + + @dataclass class InferenceResults: """Model + explainer output for a batch — a pure function of the input texts. @@ -130,15 +149,15 @@ class NlpExplainer: Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. reference_corpus : tuple[list[str], list[str] or None], optional ``(texts, labels)`` reference pool for similar-example retrieval — typically the - model's training set. When given *and* the model supports layer activations, the webapp gains - a "Similar Examples" panel that retrieves the most similar reference examples for the - selected/edited text. ``labels`` may be ``None``. Ignored when the model cannot expose - activations (e.g. a prediction-only pipeline). - similarity_layer : str, optional - Fully-qualified layer name to compare in for similar-example retrieval. Defaults to the model's - ``default_activation_layer`` (the pre-classifier pooled vector for ``HFClassifierModel``). + model's training set. When given *and* the model supports embeddings, the webapp gains a + "Similar Examples" panel that retrieves the most similar reference examples for the + selected/edited text. ``labels`` may be ``None``. Ignored when the model cannot embed + (e.g. a prediction-only pipeline). + Neighbours are compared in the model's own ``embedding_space``. To compare in a different + space, set the model's ``embedding_space`` — that moves the neighbours and any scatter built + by :meth:`compute_projection` together, since both read the space through the same store. reference_cache_dir : str or Path, optional - When given, the reference activation bank is persisted here and reloaded on later runs, so + When given, the reference embedding bank is persisted here and reloaded on later runs, so only the first launch pays the (one-off) cost of embedding the reference corpus. Examples @@ -163,7 +182,6 @@ def __init__( explainer_args: dict | None = None, explainer_compute_args: dict | None = None, reference_corpus: tuple[list[str], list[str] | None] | None = None, - similarity_layer: str | None = None, reference_cache_dir: str | Path | None = None, ) -> None: self.model = model @@ -181,11 +199,15 @@ def __init__( else: self._text_model = None - # The SHAP/LIME backend consumes a plain callable; a TextModel exposes it via shap_callable. + # The SHAP/LIME backend consumes a plain callable; a TextModel exposes it via shap_callable and + # its companion masker via shap_masker (``None`` for a pipeline-backed model, which SHAP can + # infer a Text masker from; an explicit one when shap_callable is a bare scoring function). shap_model = self._text_model.shap_callable if self._text_model is not None else model + shap_masker = self._text_model.shap_masker if self._text_model is not None else None self.backend: NlpBackend = backend or NlpShapBackend( model=shap_model, label_names=self.label_names, + masker=shap_masker, explainer_args=explainer_args or {}, explainer_compute_args=explainer_compute_args or {}, ) @@ -206,17 +228,16 @@ def __init__( self.cf_generators: dict[str, CounterfactualGenerator] = {g.name: g for g in generators} self.cf_generator: CounterfactualGenerator | None = generators[0] if generators else None - # Similar-example retrieval: only when a reference corpus is supplied AND the model - # can expose layer activations (a prediction-only pipeline cannot) — otherwise the panel gates - # itself off, mirroring how counterfactuals require a gradient-capable model. + # Similar-example retrieval: only when a reference corpus is supplied AND the model can embed + # (a prediction-only pipeline cannot) — otherwise the panel gates itself off, mirroring how + # counterfactuals require a gradient-capable model. self._retriever: SimilarExampleRetriever | None = None - if reference_corpus is not None and has_capabilities(self._text_model, SupportsActivations): + if reference_corpus is not None and has_capabilities(self._text_model, SupportsEmbeddings): ref_texts, ref_labels = reference_corpus self._retriever = SimilarExampleRetriever( self._text_model, # type: ignore[arg-type] # has_capabilities narrows the capability reference_texts=ref_texts, reference_labels=ref_labels, - layer=similarity_layer, cache_dir=reference_cache_dir, ) @@ -332,6 +353,65 @@ def text_plot(self, pos: int = 0, label_idx: int = 0, max_tokens: int | None = N return plot_token_highlight(tokens=tokens, values=values, title=title, max_tokens=max_tokens) + def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, recompute: bool = False): + """Return a 2-D projection of the compiled texts, ready to pass to :meth:`run_app`. + + The library owns the parts that must stay consistent — *which* space the texts are embedded in + and how that is cached — while the caller injects the dimensionality reducer, which is a + modelling choice shapash has no business picking for you. Embedding goes through the model's + current ``embedding_space``, so the scatter and the similar-example neighbours are guaranteed + to sit in the same space; they even share the cached vectors. + + Parameters + ---------- + reducer : object, optional + Anything with ``fit_transform(X) -> (n_samples, 2)`` — ``sklearn`` PCA/TSNE, ``pacmap``, + ``umap``. Defaults to :class:`sklearn.decomposition.PCA` with two components (sklearn is + already a core dependency, so the default costs no extra install). + cache_dir : str or Path, optional + When given, both the embeddings and the projected coordinates are persisted here and + reloaded on later runs, so only the first call pays the cost. The key covers the model, the + effective space, the texts, and the reducer's class + parameters. + recompute : bool, optional + Drop this text set's cached artifacts first, forcing a fresh embed + fit. + + Returns + ------- + np.ndarray, shape (n_samples, 2) + Coordinates aligned with the compiled texts. + + Notes + ----- + The reducer's contribution to the cache key is its class name plus a digest of ``get_params()`` + when it exposes one (sklearn and pacmap both do). A reducer with neither is keyed by class name + alone, so re-tuning such a reducer needs ``recompute=True`` to take effect. + + Examples + -------- + >>> xpl.compile(texts) + >>> xy = xpl.compute_projection(reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/") + >>> xpl.run_app(scatter_xy=xy) + """ + if self.texts is None: + raise RuntimeError("Call compile() before compute_projection().") + text_model = self._require_text_model() + if not has_capabilities(text_model, SupportsEmbeddings): + raise TypeError( + f"{type(text_model).__name__} does not support embeddings (SupportsEmbeddings); " + "pass pre-computed coordinates to run_app(scatter_xy=...) instead." + ) + if reducer is None: + reducer = PCA(n_components=2) + + store = EmbeddingStore(text_model, self.texts.tolist(), cache_dir=cache_dir) + if recompute: + store.clear() + with self._compute_guard(): # embed() tokenizes — serialize against the live compute ops + return store.cached_array( + f"{_reducer_tag(reducer)}.proj", + lambda: np.asarray(reducer.fit_transform(store.vectors())), + ) + def save_snapshot(self, path: str | Path, scatter_xy=None) -> None: """Persist compiled results (no model or backend) for offline serving. @@ -402,12 +482,18 @@ def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1" debug : bool Enable Dash debug mode (hot reload, error overlay). scatter_xy : np.ndarray, optional - Pre-computed 2-D projection of the text samples, shape - ``(n_samples, 2)``. When provided, a scatter panel appears in the - webapp that can be used to lasso/box-select a subset of samples, - filtering both the dataset table and the global word importance - plot to that subset. Compute with any projection method - (PaCMAP, UMAP, PCA, t-SNE …) and pass the result here. + Pre-computed 2-D projection of the text samples, shape ``(n_samples, 2)``. When provided, a + scatter panel appears in the webapp that can be used to lasso/box-select a subset of + samples, filtering both the dataset table and the global word importance plot to that + subset. + + Prefer :meth:`compute_projection`, whose output goes here and which guarantees the scatter + is drawn in the same space the similar-example neighbours are ranked in. This parameter is + an **unverified escape hatch**: coordinates from any source are accepted as-is, so a + projection built from a different space (or a different model entirely) renders happily + next to neighbours computed in another — selecting a cluster then means something other + than it appears to. Use it when you want coordinates the library cannot produce, and + accept that keeping them consistent is yours to manage. """ if self.contributions is None: raise RuntimeError("Call compile() before run_app().") @@ -437,14 +523,14 @@ def can_counterfactual(self) -> bool: return getattr(self, "cf_generator", None) is not None def can_find_similar(self) -> bool: - """Whether similar-example retrieval is available (a reference corpus + activation-capable model). + """Whether similar-example retrieval is available (a reference corpus + embedding-capable model). ``False`` after ``from_snapshot`` (no model) or when no ``reference_corpus`` was supplied. """ return getattr(self, "_retriever", None) is not None def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: - """Return the reference examples most similar to ``text`` in the similarity layer's space. + """Return the reference examples most similar to ``text`` in the model's embedding space. Parameters ---------- @@ -461,10 +547,9 @@ def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: retriever = getattr(self, "_retriever", None) if retriever is None: raise RuntimeError( - "find_similar() requires a reference_corpus and an activation-capable model " - "(unavailable on a snapshot)." + "find_similar() requires a reference_corpus and an embedding-capable model (unavailable on a snapshot)." ) - with self._compute_guard(): # activations() tokenizes — serialize against explain_text/predict + with self._compute_guard(): # embed() tokenizes — serialize against explain_text/predict return retriever.query(text, top_k=top_k) def predict(self, text: str) -> tuple[str, dict[str, float]]: diff --git a/shapash/webapp/nlp_components/similar_examples.py b/shapash/webapp/nlp_components/similar_examples.py index d923c292..73b19cc2 100644 --- a/shapash/webapp/nlp_components/similar_examples.py +++ b/shapash/webapp/nlp_components/similar_examples.py @@ -7,9 +7,9 @@ button that (mirroring the counterfactual panel) re-explains that example live and makes it the current datapoint, so its token contributions render immediately in the Sentence/Waterfall panels. -The lookup is one forward pass against a precomputed activation bank, so it runs live on every +The lookup is one forward pass against a precomputed embedding bank, so it runs live on every selection change. Gated by ``CAP_SIMILAR`` + ``CAP_PREDICT``: mounts only when the explainer holds a -reference corpus and a live, activation-capable model (self-disables on a snapshot or a +reference corpus and a live, embedding-capable model (self-disables on a snapshot or a prediction-only pipeline). """ From 038dbffbfb3ca239b2db76a34ae698904693883e Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:46:45 +0200 Subject: [PATCH 077/144] chore: add masker in shap backend --- shapash/backend/nlp_shap_backend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 3cfa8222..840b7374 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -124,7 +124,10 @@ def __init__( elif self.explainer_args: self.explainer = shap.Explainer(**self.explainer_args) else: - self.explainer = shap.Explainer(model) + # ``masker=None`` lets SHAP auto-infer a Text masker from a transformers pipeline (the + # HFClassifierModel/pipeline path); an explicit masker is required when ``model`` is a plain + # scoring callable (external-head models expose one via ``TextModel.shap_masker``). + self.explainer = shap.Explainer(model, masker=self.masker) def run_explainer(self, x) -> NlpRawExplanation: """Run the SHAP text explainer and return all explanation components. From 9000fbbf5f4ad197ab971796bb44c9a93b89b10b Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:47:11 +0200 Subject: [PATCH 078/144] test: add tests covering new functionalities --- .../integration_tests/test_torch_st_models.py | 225 ++++++++++ .../retrieval/test_similar_examples.py | 117 +++-- .../compute/test_embedding_store.py | 142 +++++++ tests/unit_tests/model/test_hf.py | 148 +++++-- tests/unit_tests/model/test_torch_models.py | 400 ++++++++++++++++++ tests/unit_tests/nlp/test_nlp_prototype.py | 116 +++++ 6 files changed, 1084 insertions(+), 64 deletions(-) create mode 100644 tests/integration_tests/test_torch_st_models.py create mode 100644 tests/unit_tests/compute/test_embedding_store.py create mode 100644 tests/unit_tests/model/test_torch_models.py diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py new file mode 100644 index 00000000..5a87f14a --- /dev/null +++ b/tests/integration_tests/test_torch_st_models.py @@ -0,0 +1,225 @@ +"""Integration tests for the external-head presets against real encoders. + +Covers the two adapters whose backbone is a fused ``body -> pool -> head`` rather than a single +``AutoModelForSequenceClassification``: + +* :class:`SentenceTransformerModel` over a real ``sentence_transformers`` model + a Linear head. +* :class:`TorchClassifierModel` over a raw HuggingFace ``AutoModel`` body + a Linear head. + +The heads are randomly initialised — predictions are meaningless, but every *capability* (predict, +embed, gradients, activations, similar-example retrieval) and both attribution backends (SHAP via the +explicit Text masker, and Captum LIG) must run and return the right shapes. Skipped when +``transformers`` / ``torch`` / ``sentence_transformers`` / ``captum`` are unavailable. +""" + +import numpy as np +import pytest + +transformers = pytest.importorskip("transformers") +torch = pytest.importorskip("torch") +st = pytest.importorskip("sentence_transformers") +pytest.importorskip("captum") + +from torch import nn # noqa: E402 + +from shapash.backend import NlpCaptumLigBackend # noqa: E402 +from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 +from shapash.model import ( # noqa: E402 + SentenceTransformerModel, + SupportsCaptumIG, + SupportsEmbeddings, + SupportsGradients, + TorchClassifierModel, + has_capabilities, +) + +ST_MODEL = "sentence-transformers/all-MiniLM-L6-v2" +HF_BODY = "bhadresh-savani/distilbert-base-uncased-emotion" +LABELS = ["neg", "pos", "neutral"] +TEXTS = ["i am so happy today", "this is terrible and sad", "an ordinary grey afternoon"] + + +@pytest.fixture(scope="module") +def st_model(): + """A SentenceTransformerModel: cached MiniLM sentence-transformer + a random 3-class head.""" + try: + sentence_model = st.SentenceTransformer(ST_MODEL, device="cpu") + except Exception as exc: # network / cache miss + pytest.skip(f"sentence-transformers model unavailable: {exc}") + hidden = sentence_model.get_sentence_embedding_dimension() + head = nn.Linear(hidden, len(LABELS)) + return SentenceTransformerModel(sentence_model, head, label_names=LABELS) + + +@pytest.fixture(scope="module") +def torch_model(): + """A TorchClassifierModel: a raw HF encoder body + a random 3-class head + fast tokenizer.""" + try: + tokenizer = transformers.AutoTokenizer.from_pretrained(HF_BODY, use_fast=True) + body = transformers.AutoModel.from_pretrained(HF_BODY) + except Exception as exc: # network / cache miss + pytest.skip(f"HF body unavailable: {exc}") + body.eval() + head = nn.Linear(body.config.hidden_size, len(LABELS)) + return TorchClassifierModel(body, head, tokenizer, label_names=LABELS, pool="mean") + + +# -------------------------------------------------------------------------------------------------- +# SentenceTransformerModel +# -------------------------------------------------------------------------------------------------- + + +def test_st_extracts_body_tokenizer_and_pool(st_model): + """Extraction pulls the HF transformer body, its tokenizer, and the configured pooling mode.""" + assert isinstance(st_model.body, transformers.PreTrainedModel) + assert st_model.tokenizer is not None + assert st_model.pool == "mean" # all-MiniLM-L6-v2 pools by mean + assert st_model.normalize is True # all-MiniLM-L6-v2 ends in a Normalize module + + +def test_st_embed_reproduces_encode(st_model): + """With Normalize detected, embed() must match the sentence-transformer's own encode().""" + ours = st_model.embed(TEXTS) + reference = st_model.st_model.encode(TEXTS, convert_to_numpy=True) + np.testing.assert_allclose(ours, reference, atol=1e-5) + np.testing.assert_allclose(np.linalg.norm(ours, axis=1), np.ones(len(TEXTS)), atol=1e-5) + + +def test_st_adopts_max_seq_length(st_model): + """The ST model's own truncation length is adopted, so the served length is explicit.""" + assert st_model.max_length == st_model.st_model.max_seq_length + + +def test_st_embed_reproduces_encode_on_long_text(st_model): + """Agreement with encode() must also hold past the truncation boundary, not just for short texts. + + The short-text check above passes regardless of how truncation is configured, so on its own it says + nothing about long inputs. This exercises a text well past ``max_seq_length``. + """ + long_text = " ".join(TEXTS * 40) # comfortably past the 256-token limit + assert len(st_model.tokenizer.tokenize(long_text)) > st_model.max_length + ours = st_model.embed([long_text]) + reference = st_model.st_model.encode([long_text], convert_to_numpy=True) + np.testing.assert_allclose(ours, reference, atol=1e-5) + + +def test_st_declares_full_capability_surface(st_model): + assert has_capabilities( + st_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG + ) + + +def test_st_predict_is_normalised(st_model): + probs = st_model.predict(TEXTS) + assert probs.shape == (len(TEXTS), len(LABELS)) + np.testing.assert_allclose(probs.sum(axis=1), np.ones(len(TEXTS)), atol=1e-5) + + +def test_st_embed_spaces_shapes(st_model): + hidden = st_model.backbone.get_input_embeddings().weight.shape[1] + # Default "decision" space: the input to the head's final linear. + assert st_model.embed(TEXTS).shape[0] == len(TEXTS) + # "pooled" is the universal space every architecture provides: the pooled last hidden state. + assert st_model.embed(TEXTS, "pooled").shape == (len(TEXTS), hidden) + + +def test_st_token_gradients_align(st_model): + tokens, grads = st_model.token_gradients(TEXTS[0], target_class=1) + assert grads.shape[0] == len(tokens) + assert grads.shape[1] == st_model.backbone.get_input_embeddings().weight.shape[1] + + +def test_st_shap_backend_runs(st_model): + """SHAP works via the explicit Text masker (no transformers pipeline needed).""" + xpl = NlpExplainer(st_model, label_names=LABELS) + xpl.compile(TEXTS[:2]) + words = xpl.contributions.token_strings[0] + assert len(words) > 0 + # word-level highlights must not leak special tokens or subword markers. + assert not any(w.startswith(("##", "Ġ", "▁")) or w in ("[CLS]", "[SEP]") for w in words) + + +def test_st_lig_backend_runs(st_model): + backend = NlpCaptumLigBackend(st_model, label_names=LABELS) + raw = backend.run_explainer(TEXTS[:2]) + assert len(raw.contributions) == 2 + for contrib in raw.contributions: + assert contrib.shape[1] == len(LABELS) + + +# -------------------------------------------------------------------------------------------------- +# TorchClassifierModel +# -------------------------------------------------------------------------------------------------- + + +def test_torch_declares_full_capability_surface(torch_model): + assert has_capabilities( + torch_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG + ) + + +def test_torch_predict_and_embed(torch_model): + probs = torch_model.predict(TEXTS) + assert probs.shape == (len(TEXTS), len(LABELS)) + np.testing.assert_allclose(probs.sum(axis=1), np.ones(len(TEXTS)), atol=1e-5) + assert torch_model.embed(TEXTS).shape == (len(TEXTS), torch_model.body.config.hidden_size) + + +def test_torch_max_length_controls_long_text_truncation(): + """``max_length`` is what keeps a hand-wired body+tokenizer faithful to how the body is served. + + A raw ``AutoTokenizer`` for ``all-MiniLM-L6-v2`` reports ``model_max_length=512``, but the model is + served at 256 (that is what ``SentenceTransformer`` truncates at). Pairing the two by hand — the + natural ``TorchClassifierModel`` wiring — therefore feeds the body twice the context it expects on + long inputs, and the sentence embedding drifts from the reference. This pins both directions. + """ + try: + tokenizer = transformers.AutoTokenizer.from_pretrained(ST_MODEL, use_fast=True) + body = transformers.AutoModel.from_pretrained(ST_MODEL) + reference = st.SentenceTransformer(ST_MODEL, device="cpu") + except Exception as exc: # network / cache miss + pytest.skip(f"model unavailable: {exc}") + body.eval() + + long_text = " ".join(TEXTS * 40) + assert len(tokenizer.tokenize(long_text)) > reference.max_seq_length + assert tokenizer.model_max_length > reference.max_seq_length # 512 vs 256: the trap + + head = nn.Linear(body.config.hidden_size, len(LABELS)) + kwargs = {"label_names": LABELS, "normalize": True} + naive = TorchClassifierModel(body, head, tokenizer, **kwargs) + fixed = TorchClassifierModel(body, head, tokenizer, max_length=reference.max_seq_length, **kwargs) + target = reference.encode([long_text], convert_to_numpy=True) + + # Without max_length the embedding drifts; with it, it matches the reference exactly. + assert np.abs(naive.embed([long_text], "pooled") - target).max() > 1e-3 + np.testing.assert_allclose(fixed.embed([long_text], "pooled"), target, atol=1e-5) + + +def test_torch_word_alignment_with_fast_tokenizer(torch_model): + """A fast tokenizer yields exact subword->word grouping (used by the LIG highlight path).""" + alignment = torch_model.word_alignment(TEXTS[0]) + assert alignment is not None + words, positions, specials = alignment + assert len(words) == len(positions) + assert len(specials) >= 2 # [CLS] / [SEP] + + +def test_torch_shap_and_lig_backends_run(torch_model): + xpl = NlpExplainer(torch_model, label_names=LABELS) + xpl.compile(TEXTS[:2]) + assert len(xpl.contributions.token_strings[0]) > 0 + + backend = NlpCaptumLigBackend(torch_model, label_names=LABELS) + raw = backend.run_explainer(TEXTS[:2]) + assert len(raw.contributions) == 2 + + +def test_torch_pool_mode_changes_embedding(torch_model): + """The pool knob is honoured: cls-pooling differs from mean-pooling on the same text.""" + mean_emb = torch_model.embed([TEXTS[0]]) + cls_model = TorchClassifierModel( + torch_model.body, torch_model.head, torch_model.tokenizer, label_names=LABELS, pool="cls" + ) + cls_emb = cls_model.embed([TEXTS[0]]) + assert not np.allclose(mean_emb, cls_emb) diff --git a/tests/unit_tests/compute/retrieval/test_similar_examples.py b/tests/unit_tests/compute/retrieval/test_similar_examples.py index c52c9865..8b861730 100644 --- a/tests/unit_tests/compute/retrieval/test_similar_examples.py +++ b/tests/unit_tests/compute/retrieval/test_similar_examples.py @@ -1,8 +1,10 @@ """Unit tests for the similar-example retriever (no torch/transformers required). -A tiny fake ``SupportsActivations`` model returns fixed activation vectors per text, so the retrieval -math (cosine, top-k ordering), label plumbing, corpus/layer validation and the ``.npy`` bank cache are -exercised deterministically without a real transformer. +A tiny fake model returns fixed vectors per text, so the retrieval math (cosine, top-k ordering), label +plumbing, corpus validation and the ``.npy`` bank cache are exercised deterministically without a real +transformer. Retrieval always goes through ``embed`` in the *model's* configured space — the retriever +has no space of its own — and that space takes part in the cache key of the +:class:`~shapash.compute.embedding_store.EmbeddingStore` the bank is drawn from. """ import unittest @@ -14,9 +16,13 @@ SimilarExampleRetriever, _l2_normalize, ) -from shapash.model.base import SupportsActivations, TextModel, has_capabilities +from shapash.model.base import ( + SupportsEmbeddings, + TextModel, + has_capabilities, +) -# Fixed 2-D activations: each text maps to a point; cosine ranks by angle from the origin. +# Fixed 2-D vectors: each text maps to a point; cosine ranks by angle from the origin. _VECTORS = { "happy joy": [1.0, 0.0], "joyful glad": [0.9, 0.1], @@ -26,27 +32,47 @@ } -class FakeActivationModel(TextModel, SupportsActivations): - """Model that returns a fixed activation vector per known text (unknown texts -> zeros).""" +def _vecs(texts): + return np.array([_VECTORS.get(t, [0.0, 0.0]) for t in texts], dtype=float) - def __init__(self): + +class FakeEmbeddingModel(TextModel, SupportsEmbeddings): + """Model returning a fixed vector per known text from ``embed``. + + ``calls`` counts encode calls (so the bank-reuse/cache tests can assert the bank is not rebuilt) and + ``spaces`` records the ``space`` argument each call received. ``embedding_space``, ``resolve_space`` + and ``model_id`` mirror a real adapter's surface so the retriever's cache key behaves as it does in + production. + """ + + def __init__(self, model_id="FakeEmbeddingModel:v1", embedding_space="decision"): super().__init__(label_names=["neg", "pos"]) self.calls = 0 + self.spaces = [] + self._model_id = model_id + self.embedding_space = embedding_space - def predict(self, texts): - return np.tile([0.5, 0.5], (len(texts), 1)) + def resolve_space(self, space=None): + return space if space is not None else self.embedding_space @property - def default_activation_layer(self): - return "fake_layer" + def model_id(self): + return self._model_id + + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) - def activations(self, texts, layer=None): + def embed(self, texts, space=None): self.calls += 1 - return np.array([_VECTORS.get(t, [0.0, 0.0]) for t in texts], dtype=float) + self.spaces.append(space) + return _vecs(texts) + + def get_embedding_table(self): + return ["<pad>"], np.zeros((1, 2), dtype=float) # unused by retrieval; satisfies the ABC class PredictOnlyModel(TextModel): - """No activation capability.""" + """No embedding capability.""" def __init__(self): super().__init__(label_names=["neg", "pos"]) @@ -57,14 +83,27 @@ def predict(self, texts): class TestRetriever(unittest.TestCase): def setUp(self): - self.model = FakeActivationModel() + self.model = FakeEmbeddingModel() self.texts = ["happy joy", "joyful glad", "sad down", "miserable"] self.labels = ["pos", "pos", "neg", "neg"] self.retriever = SimilarExampleRetriever(self.model, self.texts, self.labels) - def test_defaults_layer_from_model(self): - self.assertEqual(self.retriever.layer, "fake_layer") + def test_embeds_in_the_models_space_without_overriding_it(self): + # The retriever never passes a space of its own: embed() is called bare, so whatever the model + # is configured with (the same space the scatter projects) is what neighbours are ranked in. + # The cache key still names that space, resolved via the model. self.assertEqual(self.retriever.size, 4) + self.retriever.query("happy joy", top_k=1) + self.assertEqual(set(self.model.spaces), {None}) + self.assertIn("decision", self.retriever.store.space_key) + + def test_cache_key_follows_the_models_space(self): + # Moving the model to another space moves the retriever with it — one setting drives both. + model = FakeEmbeddingModel(embedding_space="pooled") + retriever = SimilarExampleRetriever(model, self.texts, self.labels) + retriever.query("happy joy", top_k=1) + self.assertEqual(set(model.spaces), {None}) + self.assertIn("pooled", retriever.store.space_key) def test_query_ranks_by_cosine_similarity(self): neighbors = self.retriever.query("happy joy", top_k=2) @@ -87,7 +126,7 @@ def test_bank_built_once_and_reused(self): self.retriever.query("happy joy", top_k=1) calls_after_first = self.model.calls self.retriever.query("sad down", top_k=1) - # Two queries => one bank build + two query activations = 3 calls total (bank not rebuilt). + # Two queries => one bank build + two query embeds = 3 calls total (bank not rebuilt). self.assertEqual(self.model.calls, calls_after_first + 1) def test_labels_optional(self): @@ -100,9 +139,10 @@ def test_mismatched_labels_length_raises(self): SimilarExampleRetriever(self.model, self.texts, reference_labels=["pos"]) def test_incompatible_model_raises(self): - self.assertFalse(has_capabilities(PredictOnlyModel(), SupportsActivations)) - with self.assertRaises(TypeError): - SimilarExampleRetriever(PredictOnlyModel(), self.texts, self.labels) + model = PredictOnlyModel() + self.assertFalse(has_capabilities(model, SupportsEmbeddings)) + with self.assertRaises(TypeError): # retrieval needs embed() + SimilarExampleRetriever(model, self.texts, self.labels) def test_bank_cached_to_disk_and_reloaded(self): import tempfile @@ -110,16 +150,41 @@ def test_bank_cached_to_disk_and_reloaded(self): with tempfile.TemporaryDirectory() as d: r1 = SimilarExampleRetriever(self.model, self.texts, self.labels, cache_dir=d) r1.build() - cache_file = r1._cache_file() + cache_file = r1.store.path("emb") self.assertTrue(cache_file.exists()) - # A fresh retriever over the same corpus/layer loads the bank from disk (no activations call). - fresh_model = FakeActivationModel() + # A fresh retriever over the same corpus/space/model loads the bank from disk. + fresh_model = FakeEmbeddingModel() r2 = SimilarExampleRetriever(fresh_model, self.texts, self.labels, cache_dir=d) neighbors = r2.query("happy joy", top_k=1) - self.assertEqual(fresh_model.calls, 1) # only the query activation, bank came from disk + self.assertEqual(fresh_model.calls, 1) # only the query embed, bank came from disk self.assertEqual(neighbors[0].text, "happy joy") + def test_different_models_do_not_share_cache(self): + """Two models over the same corpus must not collide — the bug when the key was the class name.""" + import tempfile + + with tempfile.TemporaryDirectory() as d: + a = FakeEmbeddingModel(model_id="checkpoint-a") + b = FakeEmbeddingModel(model_id="checkpoint-b") + r_a = SimilarExampleRetriever(a, self.texts, self.labels, cache_dir=d) + r_b = SimilarExampleRetriever(b, self.texts, self.labels, cache_dir=d) + self.assertNotEqual(r_a.store.key, r_b.store.key) + r_a.build() + r_b.build() + self.assertGreater(b.calls, 0) # b built its own bank rather than loading a's + + def test_different_spaces_do_not_share_cache(self): + """Same model, different representation space => different bank.""" + import tempfile + + with tempfile.TemporaryDirectory() as d: + m_dec = FakeEmbeddingModel(embedding_space="decision") + m_pool = FakeEmbeddingModel(embedding_space="pooled") + r_dec = SimilarExampleRetriever(m_dec, self.texts, self.labels, cache_dir=d) + r_pool = SimilarExampleRetriever(m_pool, self.texts, self.labels, cache_dir=d) + self.assertNotEqual(r_dec.store.key, r_pool.store.key) + class TestL2Normalize(unittest.TestCase): def test_unit_norm_rows(self): diff --git a/tests/unit_tests/compute/test_embedding_store.py b/tests/unit_tests/compute/test_embedding_store.py new file mode 100644 index 00000000..e02d8592 --- /dev/null +++ b/tests/unit_tests/compute/test_embedding_store.py @@ -0,0 +1,142 @@ +"""Unit tests for the shared embedding cache (no torch/transformers required). + +The store's whole job is *not recomputing*, and doing so under a key that cannot collide across +models, spaces or corpora. A counting fake model makes both directly observable: an assertion on +``model.calls`` says whether a cache was hit, and comparing keys says whether two configurations +would have shared a file. +""" + +import tempfile +import unittest +from pathlib import Path + +import numpy as np + +from shapash.compute.embedding_store import EmbeddingStore +from shapash.model.base import EmbeddingSource + +TEXTS = ["alpha", "beta", "gamma"] + + +class FakeModel: + """Minimal ``EmbeddingSource``: deterministic vectors, counted calls, configurable identity/space.""" + + def __init__(self, model_id="fake:v1", space="decision"): + self._model_id = model_id + self.space = space + self.calls = 0 + self.requested_spaces = [] + + @property + def model_id(self): + return self._model_id + + def resolve_space(self, space=None): + return space if space is not None else self.space + + def embed(self, texts, space=None): + self.calls += 1 + self.requested_spaces.append(space) + return np.array([[float(len(t)), 1.0] for t in texts]) + + +class TestProtocolConformance(unittest.TestCase): + def test_fake_satisfies_embedding_source(self): + """The protocol describes a real structural surface, not just documentation.""" + self.assertIsInstance(FakeModel(), EmbeddingSource) + + +class TestVectors(unittest.TestCase): + def test_embeds_once_and_memoizes(self): + model = FakeModel() + store = EmbeddingStore(model, TEXTS) + first = store.vectors() + np.testing.assert_allclose(store.vectors(), first) + self.assertEqual(model.calls, 1) + + def test_embeds_in_the_models_default_space(self): + """The store never picks a space of its own: it calls embed() bare and keys on what that means.""" + model = FakeModel(space="pooled") + store = EmbeddingStore(model, TEXTS) + store.vectors() + self.assertEqual(model.requested_spaces, [None]) + self.assertIn("pooled", store.space_key) + + def test_without_cache_dir_nothing_is_written(self): + with tempfile.TemporaryDirectory() as d: + store = EmbeddingStore(FakeModel(), TEXTS) + store.vectors() + self.assertIsNone(store.path("emb")) + self.assertEqual(list(Path(d).iterdir()), []) + + +class TestDiskCache(unittest.TestCase): + def test_second_store_loads_from_disk(self): + with tempfile.TemporaryDirectory() as d: + EmbeddingStore(FakeModel(), TEXTS, cache_dir=d).vectors() + fresh = FakeModel() + vectors = EmbeddingStore(fresh, TEXTS, cache_dir=d).vectors() + self.assertEqual(fresh.calls, 0) # served entirely from disk + np.testing.assert_allclose(vectors, [[5.0, 1.0], [4.0, 1.0], [5.0, 1.0]]) + + def test_derived_arrays_cache_beside_the_embeddings(self): + """A projection is cached under its own tag but the same key, so both travel together.""" + with tempfile.TemporaryDirectory() as d: + store = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) + fits = [] + + def project(): + fits.append(1) + return store.vectors()[:, :1] + + store.cached_array("pca.proj", project) + fresh = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) + fresh.cached_array("pca.proj", project) + self.assertEqual(len(fits), 1) # the reducer did not re-fit + self.assertTrue(store.path("pca.proj").exists()) + self.assertTrue(store.path("emb").exists()) + + def test_clear_drops_every_tag_under_the_key(self): + """Half-clearing would leave a projection derived from vectors that no longer exist.""" + with tempfile.TemporaryDirectory() as d: + store = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) + store.vectors() + store.cached_array("pca.proj", lambda: np.zeros((3, 2))) + store.clear() + self.assertFalse(store.path("emb").exists()) + self.assertFalse(store.path("pca.proj").exists()) + + model = FakeModel() + EmbeddingStore(model, TEXTS, cache_dir=d).vectors() + self.assertEqual(model.calls, 1) # really recomputed, not silently reloaded + + +class TestKeySeparation(unittest.TestCase): + """Each ingredient of the key must, on its own, produce a different cache entry.""" + + def test_different_models_do_not_collide(self): + a = EmbeddingStore(FakeModel(model_id="checkpoint-a"), TEXTS) + b = EmbeddingStore(FakeModel(model_id="checkpoint-b"), TEXTS) + self.assertNotEqual(a.key, b.key) + + def test_different_spaces_do_not_collide(self): + a = EmbeddingStore(FakeModel(space="decision"), TEXTS) + b = EmbeddingStore(FakeModel(space="pooled"), TEXTS) + self.assertNotEqual(a.key, b.key) + + def test_different_corpora_do_not_collide(self): + model = FakeModel() + self.assertNotEqual(EmbeddingStore(model, TEXTS).key, EmbeddingStore(model, [*TEXTS, "delta"]).key) + + def test_corpus_order_matters(self): + """Rows are positional — a reordered corpus is a different bank, not the same one.""" + model = FakeModel() + self.assertNotEqual(EmbeddingStore(model, TEXTS).key, EmbeddingStore(model, list(reversed(TEXTS))).key) + + def test_same_configuration_collides_on_purpose(self): + model = FakeModel() + self.assertEqual(EmbeddingStore(model, TEXTS).key, EmbeddingStore(FakeModel(), list(TEXTS)).key) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py index 9def5644..027f4186 100644 --- a/tests/unit_tests/model/test_hf.py +++ b/tests/unit_tests/model/test_hf.py @@ -8,6 +8,7 @@ from types import SimpleNamespace import numpy as np +import pytest import torch from torch import nn @@ -124,44 +125,60 @@ def __init__(self): def device(self): return torch.device("cpu") - def forward(self, input_ids=None, attention_mask=None, **kwargs): + def forward(self, input_ids=None, attention_mask=None, output_hidden_states=False, **kwargs): h = self.emb(input_ids) # (B, S, 4) — a token-level layer output pooled = torch.relu(self.pre_classifier(h.mean(dim=1))) # (B, 4) — a pooled layer output - return SimpleNamespace(logits=self.classifier(pooled)) + out = SimpleNamespace(logits=self.classifier(pooled)) + if output_hidden_states: + out.hidden_states = (h,) + return out -class TestHFClassifierActivations(unittest.TestCase): +class TestHFClassifierEmbedSpaces(unittest.TestCase): + """``embed`` is the single entry point for every representation space.""" + def setUp(self): self.model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2) - def test_default_layer_is_pre_classifier(self): - self.assertEqual(self.model.default_activation_layer, "pre_classifier") + def test_decision_is_the_default_space(self): + self.assertEqual(self.model.embedding_space, "decision") + # Decision space = input to the final Linear(4, 2), i.e. 4 features, one vector per text. + self.assertEqual(self.model.embed(["hello world", "a b c", "single"]).shape, (3, 4)) - def test_pooled_layer_activations_shape(self): - acts = self.model.activations(["hello world", "a b c", "single"]) - self.assertEqual(acts.shape, (3, 4)) # pre_classifier output used as-is, one vector per text + def test_pooled_space(self): + self.assertEqual(self.model.embed(["hello world", "x"], "pooled").shape, (2, 4)) - def test_token_level_layer_is_mean_pooled(self): + def test_named_submodule_space_is_pooled_when_token_level(self): # The embedding output is (B, S, 4); the hook path must mask-mean-pool it to (B, 4). - acts = self.model.activations(["hello world", "x"], layer="emb") - self.assertEqual(acts.shape, (2, 4)) + self.assertEqual(self.model.embed(["hello world", "x"], "emb").shape, (2, 4)) - def test_masking_makes_activation_padding_invariant(self): + def test_masking_makes_embedding_padding_invariant(self): # A text embedded alone vs. batched with a longer text (so it gets padded) must match, # proving padding tokens are excluded via the attention mask. - alone = self.model.activations(["hi"], layer="emb") - batched = self.model.activations(["hi", "one two three four"], layer="emb") + alone = self.model.embed(["hi"], "emb") + batched = self.model.embed(["hi", "one two three four"], "emb") np.testing.assert_allclose(alone[0], batched[0], rtol=1e-5, atol=1e-6) - def test_unknown_layer_raises_keyerror(self): - with self.assertRaises(KeyError): - self.model.activations(["hello"], layer="does_not_exist") + def test_unknown_space_raises_valueerror_eagerly(self): + # At construction: a typo must not wait for the first forward pass to surface. + with self.assertRaises(ValueError): + HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"], embedding_space="nope") + # And on an explicit per-call space. + with self.assertRaises(ValueError): + self.model.embed(["hello"], "does_not_exist") def test_hook_removed_after_call(self): before = len(self.model.classifier.pre_classifier._forward_hooks) - self.model.activations(["hello world"]) + self.model.embed(["hello world"], "pre_classifier") self.assertEqual(len(self.model.classifier.pre_classifier._forward_hooks), before) + def test_model_id_tracks_configuration(self): + # Two models differing only in pooling must not share a cache key. + other = HFClassifierModel( + _TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2, pool="cls" + ) + self.assertNotEqual(self.model.model_id, other.model_id) + class _PoolerClassifier(nn.Module): """A classifier with a top-level ``pooler`` (BERT/DeBERTa shape) but no ``pre_classifier``.""" @@ -176,9 +193,13 @@ def __init__(self): def device(self): return torch.device("cpu") - def forward(self, input_ids=None, attention_mask=None, **kwargs): - pooled = torch.relu(self.pooler(self.emb(input_ids).mean(dim=1))) - return SimpleNamespace(logits=self.classifier(pooled)) + def forward(self, input_ids=None, attention_mask=None, output_hidden_states=False, **kwargs): + h = self.emb(input_ids) # (B, S, 4) + pooled = torch.relu(self.pooler(h.mean(dim=1))) + out = SimpleNamespace(logits=self.classifier(pooled)) + if output_hidden_states: + out.hidden_states = (h,) + return out class _NoPooledClassifier(nn.Module): @@ -205,28 +226,79 @@ def forward(self, input_ids=None, attention_mask=None, output_hidden_states=Fals return out -class TestDefaultActivationLayerResolution(unittest.TestCase): - """``default_activation_layer`` adapts to the architecture's head, per-model, not a hard-code.""" +class TestEmbedSpacesAcrossArchitectures(unittest.TestCase): + """Both keyword spaces work on every head shape — no per-architecture configuration.""" @staticmethod - def _model(classifier): - return HFClassifierModel(classifier, _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2) - - def test_prefers_pre_classifier_when_present(self): - self.assertEqual(self._model(_TinyClassifier()).default_activation_layer, "pre_classifier") + def _model(classifier, **kwargs): + return HFClassifierModel(classifier, _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2, **kwargs) + + def test_decision_space_on_pre_classifier_head(self): + self.assertEqual(self._model(_TinyClassifier()).embed(["hello world", "x"]).shape, (2, 4)) + + def test_decision_space_on_pooler_head(self): + self.assertEqual(self._model(_PoolerClassifier()).embed(["hello world", "x"]).shape, (2, 4)) + + def test_decision_space_on_headless_architecture(self): + # No pre_classifier/pooler (RoBERTa/XLM-R shape): the final Linear is still resolvable. + self.assertEqual(self._model(_NoPooledClassifier()).embed(["hello world", "a b c"]).shape, (2, 4)) + + def test_pooled_space_on_every_architecture(self): + for classifier in (_TinyClassifier(), _PoolerClassifier(), _NoPooledClassifier()): + with self.subTest(classifier=type(classifier).__name__): + self.assertEqual(self._model(classifier).embed(["hello world", "x"], "pooled").shape, (2, 4)) + + def test_decision_space_does_not_require_label_names(self): + # The final Linear is located structurally (the head module -> its last Linear), so label_names — + # a display concern — must not decide which representation a caller gets. + model = HFClassifierModel(_NoPooledClassifier(), _FakeTokenizer(), label_names=None, batch_size=2) + self.assertIs(model._resolve_decision_linear(), model.classifier.classifier) + self.assertEqual(model.embed(["hello world", "a b c"]).shape, (2, 4)) + + def test_decision_space_falls_back_audibly_when_no_head_is_found(self): + # A backbone naming its head outside the known conventions leaves nothing to read a decision + # space off; the caller asked for "decision" and gets "pooled", so this must be audible. + model = HFClassifierModel(_OddlyNamedHeadClassifier(), _FakeTokenizer(), batch_size=2) + self.assertIsNone(model._resolve_decision_linear()) + # pytest.warns, not unittest's assertWarns: the latter walks sys.modules reading + # __warningregistry__, which blows up on any lazily-imported module (transformers) that a + # sibling test suite may have loaded into the same session. + with pytest.warns(UserWarning): + self.assertEqual(model.embed(["hello world", "a b c"]).shape, (2, 4)) + + def test_odd_head_name_is_recoverable_by_overriding_head_module(self): + # The documented escape hatch for an unconventional layout: two lines on the adapter, and the + # decision space works again — no change to the base class. + class _Adapter(HFClassifierModel): + def _head_module(self): + return self.backbone.decision_layer + + model = _Adapter(_OddlyNamedHeadClassifier(), _FakeTokenizer(), batch_size=2) + self.assertIs(model._resolve_decision_linear(), model.backbone.decision_layer) + + +class _OddlyNamedHeadClassifier(nn.Module): + """A classifier whose head is named outside the ``head``/``classifier``/``score`` conventions. + + Exercises the structural resolution's miss path: no head can be located, so the decision space must + degrade audibly to pooled rather than silently returning the wrong representation. + """ - def test_falls_back_to_pooler(self): - self.assertEqual(self._model(_PoolerClassifier()).default_activation_layer, "pooler") + def __init__(self): + super().__init__() + self.emb = nn.Embedding(9, 4) + self.decision_layer = nn.Linear(4, 2) - def test_sentinel_when_no_pooled_module(self): - model = self._model(_NoPooledClassifier()) - self.assertEqual(model.default_activation_layer, "__last_hidden_state__") + @property + def device(self): + return torch.device("cpu") - def test_activations_use_mean_pooled_last_hidden_state_fallback(self): - # With no pooled head, activations() must still return one vector per text (via embed()). - model = self._model(_NoPooledClassifier()) - acts = model.activations(["hello world", "a b c", "single"]) - self.assertEqual(acts.shape, (3, 4)) + def forward(self, input_ids=None, attention_mask=None, output_hidden_states=False, **kwargs): + h = self.emb(input_ids) + out = SimpleNamespace(logits=self.decision_layer(h.mean(dim=1))) + if output_hidden_states: + out.hidden_states = (h,) + return out class _Enc(dict): diff --git a/tests/unit_tests/model/test_torch_models.py b/tests/unit_tests/model/test_torch_models.py new file mode 100644 index 00000000..87372133 --- /dev/null +++ b/tests/unit_tests/model/test_torch_models.py @@ -0,0 +1,400 @@ +"""Unit tests for the external-head presets and the shared encoder spine. + +These exercise ``EncoderClassifierModel`` via ``TorchClassifierModel`` (a body + pool + head fused into +a backbone) with tiny torch modules — no ``transformers``/``sentence_transformers`` needed. They lock +in that the fused backbone honours the backbone contract and that every capability comes out of the +shared spine unchanged. +""" + +import unittest +from types import SimpleNamespace + +import numpy as np +import pytest +import torch +from torch import nn + +from shapash.model import ( + SupportsCaptumIG, + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TorchClassifierModel, + has_capabilities, +) +from shapash.model.encoder import EncoderClassifierModel, _pool_hidden +from shapash.model.torch_models import build_encoder_head_backbone + + +class _FakeTokenizer: + """Minimal tokenizer: word -> small id, batch-padded; single-text path returns a batch of one. + + Honours ``max_length`` (and records the calls) so tests can assert the model threads its truncation + settings through *every* tokenization path rather than only the batched one. + """ + + is_fast = False + model_max_length = 512 + + def __init__(self): + self.calls: list[dict] = [] + + def __call__(self, texts, padding=True, truncation=True, return_tensors="pt", max_length=None): + self.calls.append({"truncation": truncation, "max_length": max_length}) + single = isinstance(texts, str) + batch = [texts] if single else list(texts) + seqs = [[(hash(w) % 8) + 1 for w in t.split()] for t in batch] + if truncation and max_length is not None: + seqs = [s[:max_length] for s in seqs] + width = max(len(s) for s in seqs) + input_ids, attention_mask = [], [] + for s in seqs: + pad = width - len(s) + input_ids.append(s + [0] * pad) + attention_mask.append([1] * len(s) + [0] * pad) + return { + "input_ids": torch.tensor(input_ids, dtype=torch.long), + "attention_mask": torch.tensor(attention_mask, dtype=torch.long), + } + + def convert_ids_to_tokens(self, ids): + return [f"t{i}" for i in ids] + + def get_special_tokens_mask(self, ids, already_has_special_tokens=True): + return [0] * len(ids) # this fake has no special tokens + + +class _Body(nn.Module): + """A HF-``AutoModel``-like encoder: returns ``.last_hidden_state`` and accepts ``inputs_embeds``.""" + + def __init__(self, vocab=9, hidden=4): + super().__init__() + self.emb = nn.Embedding(vocab, hidden) + + def get_input_embeddings(self): + return self.emb + + def forward(self, input_ids=None, inputs_embeds=None, attention_mask=None, **kwargs): + hidden = self.emb(input_ids) if inputs_embeds is None else inputs_embeds + return SimpleNamespace(last_hidden_state=hidden) + + +def _model(pool="mean"): + return TorchClassifierModel(_Body(), nn.Linear(4, 2), _FakeTokenizer(), label_names=["neg", "pos"], pool=pool) + + +class TestPoolHidden(unittest.TestCase): + """``_pool_hidden`` is pure torch — mask-aware mean/max, cls, and callable.""" + + def setUp(self): + # Two texts, seq len 3, hidden 2; second position of row 2 is padding. + self.hidden = torch.tensor( + [[[1.0, 1.0], [3.0, 3.0], [5.0, 5.0]], [[2.0, 0.0], [4.0, 8.0], [0.0, 0.0]]] + ) + self.mask = torch.tensor([[1, 1, 1], [1, 1, 0]]) + + def test_mean_is_mask_aware(self): + pooled = _pool_hidden(self.hidden, self.mask, "mean") + np.testing.assert_allclose(pooled.numpy(), [[3.0, 3.0], [3.0, 4.0]]) # row2 ignores padding + + def test_cls_takes_first_position(self): + pooled = _pool_hidden(self.hidden, self.mask, "cls") + np.testing.assert_allclose(pooled.numpy(), [[1.0, 1.0], [2.0, 0.0]]) + + def test_max_ignores_padding(self): + pooled = _pool_hidden(self.hidden, self.mask, "max") + np.testing.assert_allclose(pooled.numpy(), [[5.0, 5.0], [4.0, 8.0]]) # padded row2 pos excluded + + def test_callable_pool_is_used(self): + pooled = _pool_hidden(self.hidden, self.mask, lambda h, m: h.sum(dim=1)) + np.testing.assert_allclose(pooled.numpy(), [[9.0, 9.0], [6.0, 8.0]]) + + +class TestFusedBackbone(unittest.TestCase): + """``build_encoder_head_backbone`` honours the backbone contract used by EncoderClassifierModel.""" + + def setUp(self): + self.backbone = build_encoder_head_backbone(_Body(), nn.Linear(4, 2), "mean") + + def test_logits_shape_from_ids(self): + ids = torch.tensor([[1, 2, 3]]) + mask = torch.ones(1, 3, dtype=torch.long) + self.assertEqual(self.backbone(input_ids=ids, attention_mask=mask).logits.shape, (1, 2)) + + def test_hidden_states_exposed_on_request(self): + ids = torch.tensor([[1, 2, 3]]) + out = self.backbone(input_ids=ids, attention_mask=torch.ones(1, 3, dtype=torch.long), output_hidden_states=True) + self.assertEqual(out.hidden_states[-1].shape, (1, 3, 4)) + + def test_inputs_embeds_path_matches_ids_path(self): + ids = torch.tensor([[1, 2, 3]]) + mask = torch.ones(1, 3, dtype=torch.long) + via_ids = self.backbone(input_ids=ids, attention_mask=mask).logits + embeds = self.backbone.get_input_embeddings()(ids) + via_embeds = self.backbone(inputs_embeds=embeds, attention_mask=mask).logits + np.testing.assert_allclose(via_ids.detach().numpy(), via_embeds.detach().numpy(), rtol=1e-6) + + def test_get_input_embeddings_is_body_embedding(self): + self.assertIsInstance(self.backbone.get_input_embeddings(), nn.Embedding) + + +class TestTorchClassifierModelCapabilities(unittest.TestCase): + def test_declares_full_capability_surface(self): + model = _model() + self.assertTrue( + has_capabilities( + model, + SupportsTokenization, + SupportsEmbeddings, + SupportsGradients, + SupportsCaptumIG, + ) + ) + + def test_predict_shape_and_normalisation(self): + probs = _model().predict(["hello world", "a b c"]) + self.assertEqual(probs.shape, (2, 2)) + np.testing.assert_allclose(probs.sum(axis=1), [1.0, 1.0], rtol=1e-6) + + def test_embed_shape(self): + self.assertEqual(_model().embed(["hello world", "x"]).shape, (2, 4)) + + def test_pooled_space_shape(self): + # The fused backbone exposes hidden_states, so the universal "pooled" space works here too. + self.assertEqual(_model().embed(["hello world", "a b c"], "pooled").shape, (2, 4)) + + def test_model_id_includes_head_weights(self): + # Same body + pooling, different head weights => different cache identity. Keying on the class + # name (or on the body alone) would make these two silently share a cached bank. + a, b = _model(), _model() + with torch.no_grad(): + b.head.weight.add_(1.0) + self.assertNotEqual(a.model_id, b.model_id) + + def test_token_gradients_return_aligned_rows(self): + tokens, grads = _model().token_gradients("hello world", target_class=1) + self.assertEqual(len(tokens), grads.shape[0]) + self.assertEqual(grads.shape[1], 4) # hidden dim + + def test_shap_callable_is_predict_for_external_head(self): + model = _model() + # No transformers pipeline (backbone isn't a PreTrainedModel): SHAP wraps the plain scorer. + # (The companion shap_masker over a real HF tokenizer is covered by the integration tests.) + self.assertEqual(model.shap_callable, model.predict) + + +class TestNormalize(unittest.TestCase): + """``normalize=True`` L2-normalizes the pooled vector in embed *and* before the head.""" + + def test_embed_rows_are_unit_norm(self): + model = TorchClassifierModel( + _Body(), nn.Linear(4, 2), _FakeTokenizer(), label_names=["neg", "pos"], pool="mean", normalize=True + ) + emb = model.embed(["hello world", "a b c d"]) + np.testing.assert_allclose(np.linalg.norm(emb, axis=1), [1.0, 1.0], rtol=1e-5) + + def test_normalize_changes_predictions(self): + # Same body/head/tokens, only the normalize step differs -> the head sees different inputs. + body, head, tok = _Body(), nn.Linear(4, 2), _FakeTokenizer() + plain = TorchClassifierModel(body, head, tok, label_names=["neg", "pos"], normalize=False) + normed = TorchClassifierModel(body, head, tok, label_names=["neg", "pos"], normalize=True) + self.assertFalse(np.allclose(plain.predict(["hello world"]), normed.predict(["hello world"]))) + + def test_default_is_no_normalize(self): + model = _model() + self.assertFalse(model.normalize) + self.assertFalse(model.backbone.normalize) + + +class TestEmbeddingSpace(unittest.TestCase): + """``embed`` returns the configured representation; the default reaches into the head.""" + + @staticmethod + def _deep_head_model(**kwargs): + # Two-layer head: pooled(4) -> Linear(4,3) -> relu -> Linear(3,2)=logits. The decision space is + # the 3-dim input to the final Linear; the pooled space is 4-dim, so the two are distinguishable. + head = nn.Sequential(nn.Linear(4, 3), nn.ReLU(), nn.Linear(3, 2)) + return TorchClassifierModel(_Body(), head, _FakeTokenizer(), label_names=["neg", "pos"], **kwargs) + + def test_default_space_is_decision(self): + self.assertEqual(self._deep_head_model().embedding_space, "decision") + + def test_decision_reaches_into_head(self): + # Decision space = input to the final classification Linear (3-dim), not the pooled encoder (4). + emb = self._deep_head_model(embedding_space="decision").embed(["hello world", "a b c"]) + self.assertEqual(emb.shape, (2, 3)) + + def test_pooled_space_is_encoder_output(self): + emb = self._deep_head_model(embedding_space="pooled").embed(["hello world", "a b c"]) + self.assertEqual(emb.shape, (2, 4)) # the encoder hidden size, head untouched + + def test_resolves_final_classification_linear(self): + # The last Linear *inside the head* is the one producing logits. + model = self._deep_head_model() + linear = model._resolve_decision_linear() + self.assertIsInstance(linear, nn.Linear) + self.assertIs(linear, model.head[-1]) + + def test_decision_space_does_not_require_label_names(self): + # Resolution is structural (head -> last Linear), so label_names — a display concern — must not + # decide which representation a caller gets. Without it the decision space still works. + head = nn.Sequential(nn.Linear(4, 3), nn.ReLU(), nn.Linear(3, 2)) + model = TorchClassifierModel(_Body(), head, _FakeTokenizer(), label_names=None) + self.assertIs(model._resolve_decision_linear(), head[-1]) + self.assertEqual(model.embed(["hello world"]).shape, (1, 3)) # decision, not pooled(4) + + def test_head_with_no_linear_falls_back_to_pooled_audibly(self): + # A head containing no nn.Linear leaves nothing to read a decision space off. The caller asked + # for "decision" and gets "pooled", so the degradation must be audible rather than silent. + model = TorchClassifierModel(_Body(), nn.Identity(), _FakeTokenizer(), label_names=["neg", "pos"]) + self.assertIsNone(model._resolve_decision_linear()) + # pytest.warns, not unittest's assertWarns: the latter walks sys.modules reading + # __warningregistry__, which blows up on any lazily-imported module (transformers) that a + # sibling test suite may have loaded into the same session. + with pytest.warns(UserWarning): + self.assertEqual(model.embed(["hello world"]).shape, (1, 4)) # pooled encoder hidden size + + def test_resolution_is_memoized(self): + model = self._deep_head_model() + self.assertIs(model._resolve_decision_linear(), model._resolve_decision_linear()) + + +class _FastEnc(dict): + """A fast-tokenizer encoding: a plain ids mapping plus the ``word_ids()`` accessor.""" + + def __init__(self, input_ids, word_ids): + super().__init__(input_ids=input_ids) + self._word_ids = word_ids + + def word_ids(self): + return self._word_ids + + +class _FastFakeTokenizer(_FakeTokenizer): + """Fast variant of :class:`_FakeTokenizer`: one word per token, with ``word_ids()``. + + Needed because ``word_alignment`` returns ``None`` outright for a slow tokenizer, so the slow fake + can never exercise its truncation. + """ + + is_fast = True + + def __call__(self, texts, padding=True, truncation=True, return_tensors=None, max_length=None): + self.calls.append({"truncation": truncation, "max_length": max_length}) + words = texts.split() if isinstance(texts, str) else list(texts) + if truncation and max_length is not None: + words = words[:max_length] + return _FastEnc(list(range(len(words))), list(range(len(words)))) + + def convert_ids_to_tokens(self, ids): + return [f"t{i}" for i in ids] + + def convert_tokens_to_string(self, tokens): + return " ".join(tokens) + + +class TestMaxLength(unittest.TestCase): + """``max_length`` reaches every tokenization path, so long texts truncate consistently.""" + + @staticmethod + def _model(max_length=None, tokenizer=None): + return TorchClassifierModel( + _Body(), + nn.Linear(4, 2), + tokenizer or _FakeTokenizer(), + label_names=["neg", "pos"], + max_length=max_length, + ) + + def test_defaults_to_none_so_the_tokenizer_decides(self): + model = self._model() + model.predict(["hello world"]) + self.assertIsNone(model.tokenizer.calls[-1]["max_length"]) + + def test_batched_path_truncates(self): + model = self._model(max_length=3) + long_text = " ".join(f"w{i}" for i in range(20)) + model.predict([long_text]) + self.assertEqual(model.tokenizer.calls[-1]["max_length"], 3) + + def test_every_tokenization_path_uses_it(self): + # encode/token_gradients/word_alignment must agree with the batched path: word_alignment returns + # positions into the same subword axis as encode, so a mismatch misaligns highlights silently. + long_text = " ".join(f"w{i}" for i in range(20)) + paths = { + "predict": lambda m: m.predict([long_text]), + "encode": lambda m: m.encode(long_text), + "token_gradients": lambda m: m.token_gradients(long_text, target_class=0), + "word_alignment": lambda m: m.word_alignment(long_text), + } + for name, run in paths.items(): + with self.subTest(path=name): + # word_alignment short-circuits on a slow tokenizer, so give it a fast one. + tokenizer = _FastFakeTokenizer() if name == "word_alignment" else _FakeTokenizer() + model = self._model(max_length=3, tokenizer=tokenizer) + run(model) + self.assertTrue(model.tokenizer.calls, f"{name} performed no tokenization") + for call in model.tokenizer.calls: + self.assertEqual(call["max_length"], 3) + self.assertTrue(call["truncation"]) + + def test_word_alignment_positions_stay_within_the_truncated_axis(self): + # The concrete failure the shared tokenization helper prevents: word positions indexing past the + # end of the (truncated) token axis that encode/attribution produce. + model = self._model(max_length=3, tokenizer=_FastFakeTokenizer()) + words, word_positions, _ = model.word_alignment(" ".join(f"w{i}" for i in range(20))) + self.assertEqual(len(words), 3) + self.assertTrue(all(p < 3 for pos in word_positions for p in pos)) + + def test_encode_and_gradients_agree_on_length(self): + model = self._model(max_length=4) + long_text = " ".join(f"w{i}" for i in range(20)) + input_ids, _, tokens = model.encode(long_text) + grad_tokens, grads = model.token_gradients(long_text, target_class=0) + self.assertEqual(input_ids.shape[1], 4) + self.assertEqual(len(tokens), len(grad_tokens)) + self.assertEqual(grads.shape[0], len(grad_tokens)) + + +class TestResolveSpace(unittest.TestCase): + """``resolve_space`` is the single source of truth for "which space will ``embed`` use".""" + + def setUp(self): + head = nn.Sequential(nn.Linear(4, 3), nn.ReLU(), nn.Linear(3, 2)) + self.model = TorchClassifierModel(_Body(), head, _FakeTokenizer(), label_names=["neg", "pos"]) + + def test_none_resolves_to_the_configured_default(self): + self.assertEqual(self.model.resolve_space(), "decision") + self.model.embedding_space = "pooled" + self.assertEqual(self.model.resolve_space(), "pooled") + + def test_explicit_space_is_returned_and_validated(self): + self.assertEqual(self.model.resolve_space("pooled"), "pooled") + with self.assertRaises(ValueError): + self.model.resolve_space("no_such_space") + + def test_assigning_an_unknown_space_raises_at_assignment(self): + # The setter validates, so a typo surfaces here rather than inside the next forward pass. + with self.assertRaises(ValueError): + self.model.embedding_space = "no_such_space" + self.assertEqual(self.model.embedding_space, "decision") # unchanged by the failed assignment + + def test_assignment_changes_what_embed_returns(self): + # Switching the space post-construction moves every consumer (scatter + retrieval) at once. + self.assertEqual(self.model.embed(["hello world"]).shape, (1, 3)) # decision + self.model.embedding_space = "pooled" + self.assertEqual(self.model.embed(["hello world"]).shape, (1, 4)) # encoder hidden size + + +class TestEncoderModelValidation(unittest.TestCase): + def test_rejects_unknown_pool_mode(self): + with self.assertRaises(ValueError): + EncoderClassifierModel(_Body(), _FakeTokenizer(), pool="median") + + def test_rejects_unknown_embedding_space_at_construction(self): + with self.assertRaises(ValueError): + EncoderClassifierModel(_Body(), _FakeTokenizer(), embedding_space="no_such_space") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 69722a3b..32b02a06 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -7,6 +7,7 @@ runs in CI without transformers/datasets. """ +import tempfile import unittest from unittest.mock import patch @@ -443,6 +444,121 @@ def test_y_true_is_none_by_default(self): self.assertIsNone(xpl.y_true) +class _ProjectableModel(TextModel, SupportsEmbeddings): + """Embeds each text to a deterministic 3-D point; counts calls so cache hits are observable.""" + + def __init__(self, space="decision"): + super().__init__(label_names=LABEL_NAMES[:2]) + self.space = space + self.calls = 0 + + def resolve_space(self, space=None): + return space if space is not None else self.space + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def get_embedding_table(self): + return (["a"], np.zeros((1, 3))) + + def embed(self, texts, space=None): + self.calls += 1 + return np.array([[float(len(t)), float(t.count("a")), 1.0] for t in texts]) + + @property + def shap_callable(self): + return self.predict + + +class _CountingReducer: + """A reducer with sklearn's ``get_params`` surface, so its settings reach the cache tag.""" + + def __init__(self, scale=1.0): + self.scale = scale + self.fits = 0 + + def get_params(self, deep=True): + return {"scale": self.scale} + + def fit_transform(self, x): + self.fits += 1 + return np.asarray(x)[:, :2] * self.scale + + +class TestComputeProjection(unittest.TestCase): + """The library owns the space + the caching; the caller injects only the reducer.""" + + def setUp(self): + self.model = _ProjectableModel() + self.xpl = NlpExplainer(self.model, backend=object()) + self.xpl.texts = pd.Series(["alpha", "beta banana", "gamma"]) + + def test_returns_two_columns_aligned_with_the_texts(self): + xy = self.xpl.compute_projection() + self.assertEqual(xy.shape, (3, 2)) + + def test_defaults_to_pca_without_any_extra_dependency(self): + """The default reducer must be something a core install already has — sklearn's PCA.""" + xy = self.xpl.compute_projection() + self.assertEqual(xy.shape, (3, 2)) + self.assertEqual(self.model.calls, 1) + + def test_injected_reducer_is_used(self): + reducer = _CountingReducer(scale=2.0) + xy = self.xpl.compute_projection(reducer=reducer) + self.assertEqual(reducer.fits, 1) + np.testing.assert_allclose(xy[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled + + def test_raises_before_compile(self): + xpl = NlpExplainer(_ProjectableModel(), backend=object()) + with self.assertRaises(RuntimeError): + xpl.compute_projection() + + def test_raises_for_a_model_that_cannot_embed(self): + """A prediction-only model gets a clear error pointing at the escape hatch, not an AttributeError.""" + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) + xpl.texts = pd.Series(["alpha", "beta"]) + with self.assertRaises(TypeError): + xpl.compute_projection() + + def test_cached_across_instances(self): + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(reducer=reducer, cache_dir=d) + + fresh_model = _ProjectableModel() + fresh = NlpExplainer(fresh_model, backend=object()) + fresh.texts = self.xpl.texts + fresh.compute_projection(reducer=reducer, cache_dir=d) + self.assertEqual(fresh_model.calls, 0) # neither embedded + self.assertEqual(reducer.fits, 1) # nor re-fitted + + def test_reducer_settings_take_part_in_the_key(self): + """Re-tuning a reducer must not silently reload the previous scatter.""" + with tempfile.TemporaryDirectory() as d: + a = self.xpl.compute_projection(reducer=_CountingReducer(scale=1.0), cache_dir=d) + b = self.xpl.compute_projection(reducer=_CountingReducer(scale=3.0), cache_dir=d) + np.testing.assert_allclose(b, a * 3.0) + + def test_model_space_takes_part_in_the_key(self): + """Moving the model's space must re-project, not reload the other space's coordinates.""" + with tempfile.TemporaryDirectory() as d: + self.xpl.compute_projection(reducer=_CountingReducer(), cache_dir=d) + self.model.space = "pooled" + self.model.calls = 0 + self.xpl.compute_projection(reducer=_CountingReducer(), cache_dir=d) + self.assertEqual(self.model.calls, 1) # re-embedded under the new space + + def test_recompute_forces_a_fresh_fit(self): + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(reducer=reducer, cache_dir=d) + self.model.calls = 0 + self.xpl.compute_projection(reducer=reducer, cache_dir=d, recompute=True) + self.assertEqual(self.model.calls, 1) + self.assertEqual(reducer.fits, 2) + + # --------------------------------------------------------------------------- # NlpExplainer — counterfactual generator discovery / selection (captum-free) # --------------------------------------------------------------------------- From e66f7f2a953af805d188f1127a04729064f62596 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 20 Jul 2026 16:48:07 +0200 Subject: [PATCH 079/144] refactor: update main serving script --- demo/serve_nlp_ext.py | 210 ++++++++++++++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 47 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index 2558e130..c9dc184e 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -9,8 +9,9 @@ model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): * **SHAP contributions + predictions** — via the built-in ``compile(cache_dir=...)``. -* **The 2-D PaCMAP projection** — via a small ``<hash>.proj.npy`` written here (the - library deliberately does not own a projection method). +* **The text embeddings and the 2-D projection** — via ``compute_projection(cache_dir=...)``. The + library owns the embedding + caching (so the scatter and the similar-example neighbours are always + in the same space); this script only supplies the reducer, PaCMAP. The caching is automatic: each run **loads** the cache if one exists for these exact texts, otherwise it **computes and writes** it. So the first run is slow; every run after @@ -46,9 +47,47 @@ **method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: -``<model>/<dataset>__<split>/`` holds the (backend-independent) ``<hash>.proj.npy`` projection, and a -per-backend ``<...>/nlp_shap/`` or ``<...>/nlp_captum_lig/`` subdirectory holds that method's -``<hash>.pkl`` contributions. +``<model>/<dataset>__<split>/`` holds the (backend-independent) embedding-store artifacts — the +vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or +``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.pkl`` contributions. + +Serving a *custom* model (external-head checkpoints) +---------------------------------------------------- +Any standard ``AutoModelForSequenceClassification`` checkpoint loads automatically from ``--model-name`` +alone (``load_model`` builds an ``HFClassifierModel``; label names come from ``config.id2label``). That +covers every certified encoder architecture — BERT, DistilBERT, RoBERTa, XLM-R, DeBERTa-v1, and a MiniLM +packaged as a sequence classifier. + +A model whose classification **head is custom** cannot be introspected from its name — its layer +structure, weight-key convention, and input contract are non-standard (a sentence-transformer body + a +separate MLP head, a SetFit head, a hand-rolled ``nn.Module``, …). These are served through the +``MODEL_BUILDERS`` registry: a ``{--model-name: builder}`` map consulted *before* the generic path. To add +one: + +1. Write a builder ``def build_x(config: ServeConfig, device: str) -> TextModel:`` that assembles the + right adapter and returns it. Put **all** model-specific code inside it — the core ``load_model`` never + changes. Pick the adapter by shape: + + * **sentence-transformer body + head** → :class:`~shapash.model.SentenceTransformerModel` (pass the + ``sentence_transformers`` model + a *logits* head ``nn.Module``; drop any final ``softmax`` the head + applies — ``predict`` softmaxes and the LIG backend needs logit space; set ``pool=`` to the body's + pooling mode, usually ``"mean"``). + * **raw encoder body + head** (head not fused into an ``AutoModelForSequenceClassification``) → + :class:`~shapash.model.TorchClassifierModel` (pass ``body, head, tokenizer``; ``pool="cls"`` for a + CLS-pooled head, ``"mean"`` otherwise). + * **standard classifier that merely needs ``trust_remote_code``** → just call + ``HFClassifierModel(AutoModelForSequenceClassification.from_pretrained(name, trust_remote_code=True), + tokenizer)``. + +2. Register it: ``MODEL_BUILDERS["org/your-model"] = build_x``. Nothing else changes — caching, the + What-if Lab, both attribution backends, counterfactuals, and similar-examples all work through the + capability interface regardless of adapter. + +``_build_shhossain_sentiment_model`` (registered for +``shhossain/all-MiniLM-L6-v2-sentiment-classifier``) is the worked example: a MiniLM ST body + a trained +``fc1 -> relu -> fc2 -> relu -> out`` MLP head, ``trust_remote_code`` with no HF tokenizer, so the generic +path can't touch it — but ``SentenceTransformerModel`` serves it faithfully (it reproduces the source +model's predictions exactly). Usage ----- @@ -59,8 +98,11 @@ --dataset-name sst2 --dataset-split validation --text-column sentence python demo/serve_nlp_ext.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' + # A registered custom (external-head) model — served via MODEL_BUILDERS, no other flags needed: + python demo/serve_nlp_ext.py --model-name shhossain/all-MiniLM-L6-v2-sentiment-classifier -First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). +First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). A ``SentenceTransformerModel`` +custom builder additionally needs the ``sentence-transformers`` package. """ from __future__ import annotations @@ -68,21 +110,22 @@ import argparse import json import logging +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path import datasets -import numpy as np import pacmap import torch import transformers +from torch import nn from shapash.backend import NlpCaptumLigBackend from shapash.explainer.nlp_explainer import ( NlpExplainer, _hash_texts, # same keying as the compile cache ) -from shapash.model import HFClassifierModel +from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel _HERE = Path(__file__).parent @@ -106,11 +149,13 @@ class ServeConfig: n: int = 500 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own - # training split) and how many rows of it to bank. ``similarity_layer=None`` uses the model default - # (pre-classifier pooled vector). Set ``n_reference=0`` to disable the "Similar Examples" panel. + # training split) and how many rows of it to bank. Set ``n_reference=0`` to disable the "Similar + # Examples" panel. reference_split: str = "train" n_reference: int = 2000 - similarity_layer: str | None = None + # Representation space the scatter projects *and* neighbours are ranked in — one setting for both, + # so they can never disagree. ``None`` keeps whatever the model was built with ("decision"). + embedding_space: str | None = None cache_dir: Path = _HERE / "nlp_ext_cache" port: int = 8051 host: str = "0.0.0.0" # noqa: S104 @@ -192,9 +237,13 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: help="Reference rows to bank for 'Similar Examples' (0 disables the panel).", ) parser.add_argument( - "--similarity-layer", - default=defaults.similarity_layer, - help="Fully-qualified layer to compare in for similar-example retrieval (default: model's pre-classifier).", + "--embedding-space", + default=defaults.embedding_space, + help=( + "Representation space for the 2-D scatter and similar-example retrieval: 'decision' " + "(input to the final classification linear, class-discriminative), 'pooled' (the pooled " + "last hidden state, semantic), or any backbone submodule name. Default: the model's own." + ), ) parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) parser.add_argument("--port", type=int, default=defaults.port) @@ -274,12 +323,84 @@ def _load_tokenizer(model_name: str): ) from slow_err -def load_model(config: ServeConfig) -> HFClassifierModel: - """Load the tokenizer + classifier on the best available device. +def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: + """The generic default: any standard ``AutoModelForSequenceClassification`` checkpoint. + + Covers every certified encoder architecture (BERT/DistilBERT/RoBERTa/XLM-R/DeBERTa-v1, and MiniLM + packaged as a sequence classifier). Label names come from the model's own ``config.id2label``, so a + new ``--model-name`` brings its own classes with no extra flags. + """ + tokenizer = _load_tokenizer(config.model_name) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) + logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) + return HFClassifierModel(classifier, tokenizer) + + +# Reconcile the shhossain checkpoint's own class label spelling with the dair-ai/emotion dataset it is +# demoed against ("sad" -> "sadness"); every other class name already matches. +_SHHOSSAIN_LABEL_ALIASES = {"sad": "sadness"} + + +# ── Custom-model builders ────────────────────────────────────────────────────────────────────────── +# Standard checkpoints load generically above. A model whose classification *head* is custom (its layer +# structure / weight keys / IO contract are not a standard, so they can't be introspected from a name +# alone) needs a per-model builder. Register one function per such checkpoint below; the generic path is +# never touched. Each builder takes (config, device) and returns any TextModel adapter. + + +def _build_shhossain_sentiment_model(config: ServeConfig, device: str) -> SentenceTransformerModel: + """Example builder — ``shhossain/all-MiniLM-L6-v2-sentiment-classifier`` (ST body + MLP head). - Label names aren't passed explicitly: ``HFClassifierModel`` reads them from the model's - own ``config.id2label`` when none are given, so a new ``--model-name`` brings its own - classes automatically. + This checkpoint is a custom ``trust_remote_code`` model: a standard ``all-MiniLM-L6-v2`` body plus a + trained ``fc1 -> relu -> fc2 -> relu -> out -> softmax`` MLP head, and it ships no HF tokenizer — so the + generic HF path cannot load it. Everything model-specific (the head's weight-key convention, dropping + its final softmax to recover logits, mean pooling) is contained *here*, not in the core loader. + """ + huggingface_hub = __import__("huggingface_hub") + sentence_transformers = __import__("sentence_transformers") + safetensors_torch = __import__("safetensors.torch", fromlist=["load_file"]) + + with open(huggingface_hub.hf_hub_download(config.model_name, "config.json")) as fh: + cfg = json.load(fh) + class_map = cfg["class_map"] + labels = [class_map[str(i)] for i in range(len(class_map))] + # This checkpoint's class_map spells one emotion as "sad" while its natural demo dataset + # (dair-ai/emotion) names the same class "sadness"; align them so predicted-vs-ground-truth + # comparisons in the webapp line up. (Same intent as the --label-map flag, but done here since + # the mismatch is intrinsic to this model's own labels, not a dataset choice.) + labels = [_SHHOSSAIN_LABEL_ALIASES.get(label, label) for label in labels] + + st_model = sentence_transformers.SentenceTransformer(cfg["embedding_model"], device=device) + weights = safetensors_torch.load_file(huggingface_hub.hf_hub_download(config.model_name, "model.safetensors")) + # Rebuild the trained MLP as a *logits* head (sizes from weight shapes; the checkpoint's final softmax + # is dropped — SentenceTransformerModel.predict softmaxes and the LIG backend needs logit space). + head = nn.Sequential( + nn.Linear(weights["fc1.weight"].shape[1], weights["fc1.weight"].shape[0]), + nn.ReLU(), + nn.Linear(weights["fc2.weight"].shape[1], weights["fc2.weight"].shape[0]), + nn.ReLU(), + nn.Linear(weights["out.weight"].shape[1], weights["out.weight"].shape[0]), + ) + for idx, name in ((0, "fc1"), (2, "fc2"), (4, "out")): + head[idx].load_state_dict({"weight": weights[f"{name}.weight"], "bias": weights[f"{name}.bias"]}) + return SentenceTransformerModel(st_model, head.to(device).eval(), label_names=labels, pool="mean") + + +# ``--model-name`` -> a custom builder for a checkpoint whose head can't be introspected from its name. +# Anything not listed takes the generic ``_load_hf_classifier`` path. To serve a new custom model, add a +# ``build(config, device) -> TextModel`` function above and one entry here — the core loader is untouched. +# See the module docstring's "Serving a custom model" section for the per-adapter recipe. +MODEL_BUILDERS: dict[str, Callable[[ServeConfig, str], TextModel]] = { + "shhossain/all-MiniLM-L6-v2-sentiment-classifier": _build_shhossain_sentiment_model, +} + + +def load_model(config: ServeConfig) -> TextModel: + """Load the model on the best available device: a registered custom builder, else the generic HF path. + + Standard ``AutoModelForSequenceClassification`` checkpoints of any certified architecture load with no + configuration; a checkpoint whose head is custom (so it can't be introspected from its name) is built + by its entry in :data:`MODEL_BUILDERS`. """ if torch.cuda.is_available(): device = "cuda" @@ -288,13 +409,11 @@ def load_model(config: ServeConfig) -> HFClassifierModel: device = "cpu" logger.info("No CUDA device found — using CPU") - tokenizer = _load_tokenizer(config.model_name) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) - logger.info("Loaded model %s on device: %s", config.model_name, next(classifier.parameters()).device) - return HFClassifierModel(classifier, tokenizer) + builder = MODEL_BUILDERS.get(config.model_name) + return builder(config, device) if builder is not None else _load_hf_classifier(config, device) -def build_backend(config: ServeConfig, model: HFClassifierModel) -> NlpCaptumLigBackend | None: +def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend | None: """Return the attribution backend selected by ``--attribution``. Returns ``None`` for ``"shap"`` so ``NlpExplainer`` builds its default ``NlpShapBackend`` (which @@ -306,25 +425,16 @@ def build_backend(config: ServeConfig, model: HFClassifierModel) -> NlpCaptumLig return None -def load_or_project(sentences, model, cache_dir: Path, n_components: int = 2) -> np.ndarray: - """Return a 2-D PaCMAP projection, reading from / writing to ``cache_dir``. +def build_reducer() -> pacmap.PaCMAP: + """The dimensionality reducer for the scatter — a *demo* choice, not a library one. - Cached alongside the ``compile`` results under the same text hash so a single - ``cache_dir`` holds everything the app needs after the first run. + ``NlpExplainer.compute_projection`` owns everything that must stay consistent (which space the + texts are embedded in, and the caching of both the vectors and the coordinates) and takes the + reducer as an argument, because which manifold method suits your data is a modelling decision. + PaCMAP is a good default for text clusters; swap in UMAP, t-SNE, or drop the argument entirely for + the built-in PCA. """ - cache_file = cache_dir / f"{_hash_texts(list(sentences))}.proj.npy" - if cache_file.exists(): - logger.info("Projection cache hit — loading %s", cache_file) - return np.load(cache_file) - - logger.info("Projection cache miss — computing PaCMAP projection (embedding %d texts)", len(sentences)) - projector = pacmap.PaCMAP(n_components=n_components, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) - projected = projector.fit_transform(model.embed(sentences), init="pca") - - cache_dir.mkdir(parents=True, exist_ok=True) - np.save(cache_file, projected) - logger.info("Projection computed and cached to %s", cache_file) - return projected + return pacmap.PaCMAP(n_components=2, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) def main() -> None: @@ -347,12 +457,19 @@ def main() -> None: # selected backend's contributions are dropped; other backends' caches are left intact. logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) (compile_dir / f"{text_hash}.pkl").unlink(missing_ok=True) - (dataset_dir / f"{text_hash}.proj.npy").unlink(missing_ok=True) + # The embeddings + projection are dropped by compute_projection(recompute=True) below, which + # knows their key; only the contributions pickle is this script's to manage. model = load_model(config) - - # Similar-examples reference corpus (the training split) + a cache dir for its activation bank, - # keyed by corpus/layer/model so it is embedded once and reloaded on later runs. + if config.embedding_space is not None: + # One setting drives both the scatter projection and similar-example retrieval. Assigning it + # here (rather than threading it through every MODEL_BUILDERS entry) is safe because the + # setter validates the name against the backbone — a typo raises now, not mid-forward-pass. + model.embedding_space = config.embedding_space + logger.info("Embedding space: %s (scatter projection + similar examples)", model.resolve_space()) + + # Similar-examples reference corpus (the training split) + a cache dir for its embedding bank, + # keyed by corpus/space/model so it is embedded once and reloaded on later runs. reference_corpus = load_reference_corpus(config) similar_cache_dir = dataset_dir / "similar" @@ -364,7 +481,6 @@ def main() -> None: label_names=model.label_names, backend=build_backend(config, model), reference_corpus=reference_corpus, - similarity_layer=config.similarity_layer, reference_cache_dir=similar_cache_dir, ) # Both steps load from cache if one exists for these exact texts, otherwise they compute and write @@ -379,7 +495,7 @@ def main() -> None: len(sentences), ) xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) - projected = load_or_project(sentences, model, dataset_dir) + projected = xpl.compute_projection(reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute) if xpl.can_find_similar(): # Build (and cache) the reference activation bank now so the first in-app lookup is instant. From d99980dbc1a51bf79fda6947d1fb8fed6d0a6c18 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 10:00:10 +0200 Subject: [PATCH 080/144] fix(model): raise a named error when a hooked embedding space never runs --- shapash/model/encoder.py | 19 ++++++++++++++++--- tests/unit_tests/model/test_torch_models.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index d01e3562..ffe0b947 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -389,7 +389,7 @@ def embed(self, texts: list[str], space: str | None = None) -> np.ndarray: return self._decision_embed(texts) # A named backbone submodule: capture its output on the forward pass. module = dict(self.backbone.named_modules())[space] - return self._hooked_embed(texts, module, pre=False) + return self._hooked_embed(texts, module, pre=False, space=space) def _pooled_embed(self, texts: list[str]) -> np.ndarray: """Return the mask-pooled last hidden state, shape ``(n_texts, hidden_dim)``. @@ -418,13 +418,18 @@ def _pooled_embed(self, texts: list[str]) -> np.ndarray: out.append(emb.cpu().numpy()) return np.vstack(out) - def _hooked_embed(self, texts: list[str], module: Any, pre: bool) -> np.ndarray: + def _hooked_embed(self, texts: list[str], module: Any, pre: bool, space: str) -> np.ndarray: """Return one vector per text captured at ``module``, shape ``(n_texts, hidden_dim)``. With ``pre=False`` a forward hook captures the module's *output*; with ``pre=True`` a forward pre-hook captures its first *input* (how the decision space is read off the final linear). A pooled ``(batch, hidden)`` capture is used as-is; a token-level ``(batch, seq, hidden)`` one is reduced with the configured :attr:`pool`. The backbone runs under ``no_grad`` — inference only. + + ``space`` names the requested space for error reporting only. It is needed because a module can + exist on the backbone without lying on the path a forward pass takes — :meth:`_validate_space` + checks a submodule name *exists*, not that it *runs* — and the resulting empty capture must say + which space is unreachable rather than surface as a bare ``IndexError``. """ captured: list = [] if pre: @@ -436,6 +441,14 @@ def _hooked_embed(self, texts: list[str], module: Any, pre: bool) -> np.ndarray: for batch, _ in self._batches(texts): # The hook fires during the forward pass above; take the first capture of this batch and # clear so a module invoked more than once cannot leak captures across batches. + if not captured: + raise RuntimeError( + f"embedding space {space!r} resolved to a {type(module).__name__} that did not " + f"run during {type(self.backbone).__name__}'s forward pass, so no representation " + "was captured. The module exists on the backbone but is not on the path this " + f"input takes. Use a keyword space {_KEYWORD_SPACES}, or name a submodule the " + "forward pass actually calls." + ) vec = self._pool_batch(captured[0], batch) out.append(vec.cpu().numpy()) captured.clear() @@ -516,7 +529,7 @@ def _decision_embed(self, texts: list[str]) -> np.ndarray: ) logger.info("Decision space unavailable — falling back to the pooled last hidden state.") return self._pooled_embed(texts) - return self._hooked_embed(texts, linear, pre=True) + return self._hooked_embed(texts, linear, pre=True, space=_DECISION_SPACE) # ------------------------------------------------------------------ # Gradients diff --git a/tests/unit_tests/model/test_torch_models.py b/tests/unit_tests/model/test_torch_models.py index 87372133..ce109e9d 100644 --- a/tests/unit_tests/model/test_torch_models.py +++ b/tests/unit_tests/model/test_torch_models.py @@ -254,6 +254,26 @@ def test_head_with_no_linear_falls_back_to_pooled_audibly(self): with pytest.warns(UserWarning): self.assertEqual(model.embed(["hello world"]).shape, (1, 4)) # pooled encoder hidden size + def test_unreachable_submodule_space_raises_a_named_error(self): + # Space validation only checks that a submodule *name exists* — a module can exist on the + # backbone and still never run (a dead branch, or one this input routes around). The hook then + # captures nothing, and the failure has to name the unreachable space rather than surface as a + # bare IndexError off the empty capture list. + class _BodyWithDeadBranch(_Body): + def __init__(self): + super().__init__() + self.dead = nn.Linear(4, 4) # registered, but _Body.forward never calls it + + model = TorchClassifierModel( + _BodyWithDeadBranch(), + nn.Linear(4, 2), + _FakeTokenizer(), + label_names=["neg", "pos"], + embedding_space="body.dead", # accepted at construction: the name does exist + ) + with self.assertRaisesRegex(RuntimeError, "body.dead"): + model.embed(["hello world"]) + def test_resolution_is_memoized(self): model = self._deep_head_model() self.assertIs(model._resolve_decision_linear(), model._resolve_decision_linear()) From 46c67d94cfe7448a686a635272fd22a0e5876b20 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 11:24:26 +0200 Subject: [PATCH 081/144] fix(model): pin fused encoder backbones to inference mode --- shapash/model/encoder.py | 8 ++++++++ shapash/model/torch_models.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index ffe0b947..d2a67d0a 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -174,6 +174,14 @@ def __init__( if not callable(pool) and pool not in _POOL_MODES: raise ValueError(f"pool must be one of {_POOL_MODES} or a callable; got {pool!r}.") super().__init__(label_names=label_names) + # Pin the backbone to inference mode. Every method here is a forward (or a backward *through* a + # forward) for analysis — none trains — so a live Dropout/BatchNorm can only randomise results, + # and it does so silently. ``from_pretrained`` happens to return eval-mode models, but a + # hand-assembled backbone (see ``build_encoder_head_backbone``) does not; relying on the caller + # makes correctness a convention. ``eval()`` recurses, so this covers body and head at once. + eval_mode = getattr(backbone, "eval", None) + if callable(eval_mode): + eval_mode() self.backbone = backbone self.tokenizer = tokenizer self.batch_size = batch_size diff --git a/shapash/model/torch_models.py b/shapash/model/torch_models.py index 977adb2b..6d165387 100644 --- a/shapash/model/torch_models.py +++ b/shapash/model/torch_models.py @@ -132,9 +132,21 @@ def build_encoder_head_backbone(body, head, pool: Any, normalize: bool = False): Returns ------- _EncoderHeadBackbone - A backbone instance ready to pass to :class:`~shapash.model.encoder.EncoderClassifierModel`. + A backbone instance in **eval mode**, ready to pass to + :class:`~shapash.model.encoder.EncoderClassifierModel`. + + Notes + ----- + The returned module is switched to eval mode here. Assigning a submodule to an ``nn.Module`` does + not change that submodule's ``training`` flag, so a head the caller built fresh (``nn.Sequential`` + with a ``Dropout``/``BatchNorm``) would otherwise stay in *training* mode and randomise every + forward pass — silently, since nothing raises: predictions, contributions, embeddings and + counterfactual flips would all differ run to run. ``eval()`` recurses into body and head, so one + call pins the whole path. These adapters are inference wrappers; there is no path that wants + training mode. """ - return _EncoderHeadBackbone(body, head, pool, normalize) + backbone = _EncoderHeadBackbone(body, head, pool, normalize) + return backbone.eval() class TorchClassifierModel(EncoderClassifierModel): From af6405d2a8741260c0e4523108c9376119054b45 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 11:26:54 +0200 Subject: [PATCH 082/144] fix(generators): detect the tokenizer's word-start marker --- shapash/compute/generators/ablation_flip.py | 11 ++- shapash/compute/generators/cf_utils.py | 32 ++++-- shapash/compute/generators/hotflip.py | 40 ++++++-- shapash/model/base.py | 102 +++++++++++++++++++- 4 files changed, 166 insertions(+), 19 deletions(-) diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py index 5db67f71..30fa22aa 100644 --- a/shapash/compute/generators/ablation_flip.py +++ b/shapash/compute/generators/ablation_flip.py @@ -31,7 +31,7 @@ IntField, TokenListField, ) -from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token +from shapash.compute.generators.cf_utils import display_form, is_prediction_flip from shapash.model.base import SupportsTokenization, TextModel, has_capabilities if TYPE_CHECKING: @@ -78,7 +78,14 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None tokens = model.tokenize(text) - content_positions = [i for i, t in enumerate(tokens) if is_word_token(t) and t.lower() not in ignore] + # Removable positions are decided by the *model's* tokenization scheme (``is_substitutable``), + # not by a string rule here: under SentencePiece/byte-BPE every content token carries a + # ``▁``/``Ġ`` word-start marker, which a bare ``isalpha`` check rejects wholesale. ``ignore`` + # holds user-typed words, so it is matched against each token's display form for the same + # reason — ``"great"`` must match the token ``"▁great"``. + content_positions = [ + i for i, t in enumerate(tokens) if model.is_substitutable(t) and display_form(model, t) not in ignore + ] if not content_positions: return [] diff --git a/shapash/compute/generators/cf_utils.py b/shapash/compute/generators/cf_utils.py index 94901c74..c2f693b8 100644 --- a/shapash/compute/generators/cf_utils.py +++ b/shapash/compute/generators/cf_utils.py @@ -2,6 +2,11 @@ Small, pure helpers shared by generators: detecting a prediction flip and measuring how far a counterfactual moved the original class probability. + +Token word-hood used to live here too, as a bare ``token.isalpha()``. It now lives in the model layer +(:func:`~shapash.model.base.is_word_token` and the scheme-aware +:meth:`~shapash.model.base.SupportsTokenization.is_substitutable`), because the answer depends on the +tokenization scheme and only the model knows which one it uses. """ from __future__ import annotations @@ -39,12 +44,25 @@ def prediction_difference(orig_probs: np.ndarray, cf_probs: np.ndarray, class_id return float(orig_probs[class_idx] - cf_probs[class_idx]) -def is_word_token(token: str) -> bool: - """True for plausible standalone word tokens. +def display_form(model, token: str) -> str: + """Return the user-facing spelling of ``token`` — markers stripped, lowercased. + + Generators match tokens against user-typed word lists (``tokens_to_ignore``, typed into the + webapp), which hold plain words — while a token may carry a ``▁``/``Ġ`` word-start marker. Comparing + the two directly means ``"great"`` never matches the token ``"▁great"``, so the ignore list silently + does nothing on SentencePiece and byte-BPE models. Round-tripping through the model's own + ``detokenize`` yields the display spelling for any tokenization scheme, with no marker table here. - Rejects sub-word continuations (``##ing``), special/placeholder tokens (``[CLS]``), punctuation - and numerics — substituting or removing any of these yields malformed text once detokenized. - ``str.isalpha`` handles all of these in one check (``##ing``/``[CLS]``/``1b`` are not alphabetic). - Shared by the token-substitution (HotFlip) and token-removal (ablation) generators. + Parameters + ---------- + model : SupportsTokenization + The model whose tokenizer produced ``token``. + token : str + A single token string. + + Returns + ------- + str + The token as a user would write it. """ - return token.isalpha() + return model.detokenize([token]).strip().lower() diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index 73d1d828..c59a1a27 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -32,15 +32,21 @@ IntField, TokenListField, ) -from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token -from shapash.model.base import SupportsEmbeddings, SupportsGradients, TextModel, has_capabilities +from shapash.compute.generators.cf_utils import display_form, is_prediction_flip +from shapash.model.base import ( + SupportsEmbeddings, + SupportsGradients, + SupportsTokenization, + TextModel, + has_capabilities, +) _MAX_FLIPPABLE_TOKENS = 10 _CANDIDATES_PER_POSITION = 50 class _GradientModel(Protocol): - """The exact capability surface HotFlip uses (predict + gradients + embeddings + detokenize).""" + """The exact capability surface HotFlip uses (predict + gradients + embeddings + tokenization).""" label_names: list[str] | None @@ -52,6 +58,8 @@ def get_embedding_table(self) -> tuple[list[str], np.ndarray]: ... def detokenize(self, tokens: list[str]) -> str: ... + def is_substitutable(self, token: str) -> bool: ... + class HotFlipGenerator(CounterfactualGenerator): """Gradient-based token-substitution counterfactuals (LIT HotFlip).""" @@ -69,8 +77,14 @@ def config_spec(self) -> dict[str, Field]: @classmethod def is_compatible(cls, model: TextModel) -> bool: - """Compatible only with models that expose gradients *and* an embedding table.""" - return has_capabilities(model, SupportsGradients, SupportsEmbeddings) + """Compatible only with models exposing gradients, an embedding table *and* tokenization. + + ``SupportsTokenization`` is required because rebuilding a flipped sentence goes through + ``detokenize``, and choosing which vocab entries are usable replacements goes through + ``is_substitutable`` — both live on that capability. It was previously used without being + declared, which happened to work only because every gradient-capable adapter also tokenizes. + """ + return has_capabilities(model, SupportsGradients, SupportsEmbeddings, SupportsTokenization) def generate(self, text: str, target_label: str | None = None, config: dict | None = None) -> list[Counterfactual]: """Generate minimal HotFlip counterfactuals for ``text`` (see module docstring).""" @@ -91,20 +105,28 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No if not tokens: return [] - # Rank content tokens by gradient L2 norm; keep the top-K flippable positions. + # Rank content tokens by gradient L2 norm; keep the top-K flippable positions. Positions are + # filtered by word-hood as well as candidates: substituting a whole word into a *mid-word* + # position rebuilds malformed text, which is what the module docstring already promises to + # avoid — it was previously enforced on candidates only. grad_l2 = np.sum(grads * grads, axis=-1) ranked = np.argsort(-grad_l2).tolist() - flippable = [p for p in ranked if tokens[p].strip().lower() not in ignore][:_MAX_FLIPPABLE_TOKENS] + flippable = [ + p for p in ranked if model.is_substitutable(tokens[p]) and display_form(model, tokens[p]) not in ignore + ][:_MAX_FLIPPABLE_TOKENS] vocab, matrix = model.get_embedding_table() replacement: dict[int, str] = {} for pos in flippable: - # Shortlist the top-K word candidates by the first-order estimate (most-negative first)... + # Shortlist the top-K word candidates by the first-order estimate (most-negative first). + # Candidate word-hood is judged by the model (``is_substitutable``) because the vocab + # carries the tokenizer's own markers — under SentencePiece/byte-BPE every usable word is + # ``▁word``/``Ġword``, which a bare ``isalpha`` check would reject, emptying every shortlist. scores = matrix @ grads[pos] shortlist: list[str] = [] for cand in np.argsort(scores): cand_tok = vocab[int(cand)] - if cand_tok != tokens[pos] and is_word_token(cand_tok): + if cand_tok != tokens[pos] and model.is_substitutable(cand_tok): shortlist.append(cand_tok) if len(shortlist) >= _CANDIDATES_PER_POSITION: break diff --git a/shapash/model/base.py b/shapash/model/base.py index 65c76099..e10cad48 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -63,6 +63,18 @@ def n_classes(self) -> int | None: """Number of classes, or ``None`` when unknown before the first prediction.""" return len(self.label_names) if self.label_names is not None else None + @property + def shap_callable(self) -> Any: + """Scoring callable SHAP's text explainer wraps. Defaults to :meth:`predict`. + + Part of the base contract rather than an undeclared convention: an adapter that overrides + nothing still works with the SHAP backend, and :attr:`shap_masker` (which documents itself in + terms of this property) can no longer refer to something that may not exist. Adapters wrapping + an object SHAP understands natively — a ``transformers`` pipeline, from which it infers a + ``Text`` masker — override this to return that object instead. + """ + return self.predict + @property def shap_masker(self) -> Any: """Masker SHAP should use, or ``None`` to let SHAP infer one from :attr:`shap_callable`. @@ -86,8 +98,35 @@ def model_id(self) -> str: return type(self).__name__ +# Word-*start* markers: byte-level BPE (``Ġ``, RoBERTa/XLM-R/GPT-2) and SentencePiece (``▁``, +# DeBERTa-v2/v3, T5, most multilingual checkpoints) prefix the token that *begins* a word. WordPiece +# (BERT/DistilBERT) uses the opposite convention — it marks *continuations* with ``##`` and leaves word +# starts bare. See :meth:`SupportsTokenization.word_start_marker`. +WORD_START_MARKERS = ("Ġ", "▁") # "Ġ", "▁" + +# Two ordinary lowercase words, tokenized once to detect the convention above. Deliberately trivial: +# any tokenizer segments this into whole words, so a marker appears if and only if the scheme uses one. +_MARKER_PROBE_TEXT = "hello world" + +_UNRESOLVED = object() # sentinel distinguishing "not probed yet" from a probed ``None`` + + +def is_word_token(token: str) -> bool: + """True for a plausible standalone word token under a **marker-free** tokenizer. + + Rejects WordPiece continuations (``##ing``), special/placeholder tokens (``[CLS]``), punctuation and + numerics — substituting or removing any of these yields malformed text once detokenized. + ``str.isalpha`` handles all of them in one check. + + This is the *default* rule only. It is wrong for a tokenizer that marks word starts, where every + content token carries a ``Ġ``/``▁`` prefix; use :meth:`SupportsTokenization.is_substitutable`, which + dispatches on the model's own tokenization scheme, rather than calling this directly. + """ + return token.isalpha() + + class SupportsTokenization(ABC): - """Capability: split text into tokens and rebuild text from tokens.""" + """Capability: split text into tokens, rebuild text from tokens, and judge token word-hood.""" @abstractmethod def tokenize(self, text: str) -> list[str]: @@ -97,6 +136,67 @@ def tokenize(self, text: str) -> list[str]: def detokenize(self, tokens: list[str]) -> str: """Rebuild a display string from a list of tokens.""" + def word_start_marker(self) -> str | None: + """Return this tokenizer's word-*start* marker, or ``None`` when it marks continuations. + + Probed once from :meth:`tokenize` (not from a tokenizer class name or a hard-coded model list), + so a scheme this code has never seen is classified by what it actually emits, and the result is + memoized on the instance. + + Returns + ------- + str or None + ``"Ġ"`` (byte-level BPE) or ``"▁"`` (SentencePiece) when word starts are marked; ``None`` + for WordPiece-style continuation marking or a plain whitespace tokenizer. + """ + marker = getattr(self, "_word_start_marker_cache", _UNRESOLVED) + if marker is _UNRESOLVED: + marker = self._probe_word_start_marker() + self._word_start_marker_cache = marker + return marker + + def _probe_word_start_marker(self) -> str | None: + """Tokenize :data:`_MARKER_PROBE_TEXT` and report the marker its tokens carry, if any.""" + try: + probe = self.tokenize(_MARKER_PROBE_TEXT) + except Exception: # noqa: BLE001 - a tokenizer that cannot run the probe simply has no marker + return None + for token in probe: + for marker in WORD_START_MARKERS: + if token.startswith(marker): + return marker + return None + + def is_substitutable(self, token: str) -> bool: + """Whether ``token`` is a standalone word that can be swapped or removed cleanly. + + Counterfactual generators use this to decide which positions they may perturb and which vocab + entries are usable replacements. Word-hood is a property of the *tokenization scheme*, so it is + answered here rather than by string-guessing in ``compute/``: + + * **Continuation-marking** tokenizers (WordPiece, or a plain whitespace split) leave word starts + bare, so a bare alphabetic token is a word and ``##ing`` is not — :func:`is_word_token`. + * **Word-start-marking** tokenizers (byte-BPE ``Ġ``, SentencePiece ``▁``) invert that: only a + *marked* token begins a word, and a bare one is a mid-word piece. Judging these with + :func:`is_word_token` rejects every content token of a SentencePiece model (``"▁good"`` is not + alphabetic) and accepts every mid-word BPE fragment — leaving both generators silently unable + to find any candidate at all. + + Parameters + ---------- + token : str + A token string from this model's tokenizer or vocabulary. + + Returns + ------- + bool + ``True`` when the token is a whole word this tokenizer can round-trip on its own. + """ + marker = self.word_start_marker() + if marker is None: + return is_word_token(token) + return token.startswith(marker) and is_word_token(token[len(marker) :]) + class SupportsEmbeddings(ABC): """Capability: expose the input-embedding table and one dense vector per text. From a554f8bd70d8f0701f3ce9945604f2ed3f551aaa Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 11:27:56 +0200 Subject: [PATCH 083/144] fix(explainer): include model and backend in the compile cache key --- demo/serve_nlp_ext.py | 42 +++++----- shapash/compute/embedding_store.py | 16 +++- shapash/explainer/nlp_explainer.py | 120 +++++++++++++++++++++-------- 3 files changed, 121 insertions(+), 57 deletions(-) diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index c9dc184e..11fe58fe 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -25,10 +25,10 @@ Model and dataset are configurable (see ``ServeConfig``): swap ``--model-name`` for any HF sequence-classification checkpoint (its label names are read from the model's own config) and ``--dataset-name`` for any HF ``datasets`` classification dataset (point ``--text-column`` / -``--label-column`` at the right fields if they aren't ``text`` / ``label``). The on-disk cache is -keyed by a hash of the input texts only (not the model), so each ``--model-name`` gets its own -subdirectory under ``--cache-dir`` — otherwise switching models with the same dataset/``--n`` -would silently reload another model's cached contributions/embeddings. +``--label-column`` at the right fields if they aren't ``text`` / ``label``). Cache keys cover the model +and the attribution backend as well as the texts, so switching ``--model-name`` can never reload another +model's contributions; the per-model/per-dataset subdirectories under ``--cache-dir`` below are for +human legibility (and easy pruning), not for correctness. Dataset and model label strings aren't guaranteed to match even when their class order does (e.g. a dataset's ``ClassLabel`` names ``"neg"``/``"pos"`` vs. a model's ``config.id2label`` values @@ -121,10 +121,7 @@ from torch import nn from shapash.backend import NlpCaptumLigBackend -from shapash.explainer.nlp_explainer import ( - NlpExplainer, - _hash_texts, # same keying as the compile cache -) +from shapash.explainer.nlp_explainer import NlpExplainer from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel _HERE = Path(__file__).parent @@ -175,11 +172,11 @@ def _dataset_slug(config: ServeConfig) -> str: def _dataset_cache_dir(config: ServeConfig) -> Path: """Return ``cache_dir/<model>/<dataset>__<split>`` — the level the projection is cached at. - The compile/projection caches are keyed by a hash of the input texts only (see ``_hash_texts``), - so different models or datasets would otherwise collide on the same cache file; the ``<model>`` and - ``<dataset>`` path levels keep them apart. The PaCMAP projection embeds the texts with - ``model.embed`` and never touches the attribution backend, so every ``--attribution`` choice shares - one projection cache at *this* level (below it, contributions split per backend). + The compile and projection caches both key on the model (and, for contributions, the backend), so + these path levels are organisational rather than load-bearing: they keep a ``--cache-dir`` readable + and prunable per model/dataset. The PaCMAP projection embeds the texts with ``model.embed`` and + never touches the attribution backend, so every ``--attribution`` choice shares one projection cache + at *this* level (below it, contributions split per backend). """ return config.cache_dir / config.model_name.replace("/", "__") / _dataset_slug(config) @@ -451,15 +448,6 @@ def main() -> None: dataset_dir = _dataset_cache_dir(config) # projection lives here (shared across attribution backends) compile_dir = _compile_cache_dir(config) # contributions live here (one subdir per attribution backend) - text_hash = _hash_texts(list(sentences)) - if config.recompute: - # Drop this text set's cached artifacts so the steps below recompute + overwrite. Only the - # selected backend's contributions are dropped; other backends' caches are left intact. - logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) - (compile_dir / f"{text_hash}.pkl").unlink(missing_ok=True) - # The embeddings + projection are dropped by compute_projection(recompute=True) below, which - # knows their key; only the contributions pickle is this script's to manage. - model = load_model(config) if config.embedding_space is not None: # One setting drives both the scatter projection and similar-example retrieval. Assigning it @@ -483,9 +471,17 @@ def main() -> None: reference_corpus=reference_corpus, reference_cache_dir=similar_cache_dir, ) + if config.recompute: + # Drop this text set's cached contributions so the steps below recompute + overwrite. The + # explainer owns the key (it covers the model and backend), so it also owns the deletion — + # only *this* model+backend's entry goes, other backends' caches are left intact. The + # embeddings + projection are dropped by compute_projection(recompute=True) below. + logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) + xpl.clear_cache(sentences, compile_dir) + # Both steps load from cache if one exists for these exact texts, otherwise they compute and write # it. So the first run is slow; later runs only pay the model load. - compile_cache = compile_dir / f"{text_hash}.pkl" + compile_cache = xpl.cache_path(sentences, compile_dir) if compile_cache.exists(): logger.info("Contributions cache hit (%s) — loading %s", config.attribution, compile_cache) else: diff --git a/shapash/compute/embedding_store.py b/shapash/compute/embedding_store.py index 7e5fd76f..8a9d705b 100644 --- a/shapash/compute/embedding_store.py +++ b/shapash/compute/embedding_store.py @@ -25,8 +25,18 @@ logger = logging.getLogger(__name__) -def _hash_corpus(texts: Sequence[str], key: str) -> str: - """Stable digest over a model/space key plus the corpus texts (order-sensitive).""" +def hash_corpus(texts: Sequence[str], key: str) -> str: + """Stable digest over an identity ``key`` plus the corpus texts (order-sensitive). + + The ``\\0`` separator after every text is what makes this collision-safe: without it + ``["ab", "c"]`` and ``["a", "bc"]`` hash identically, so two different corpora would share a cache + entry. ``key`` carries whatever else changes the cached artifact (model identity, representation + space, explanation backend) and is separated the same way. + + Shared by every disk cache in the NLP path — the embedding/projection artifacts here and the + contribution pickle in :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compile` — so all of + them key on the same, collision-safe rule. + """ h = hashlib.md5(usedforsecurity=False) h.update(f"{key}\0".encode()) for text in texts: @@ -81,7 +91,7 @@ def space_key(self) -> str: @property def key(self) -> str: """The cache key: model identity, effective space, and corpus digest.""" - return _hash_corpus(self.texts, self.space_key) + return hash_corpus(self.texts, self.space_key) def path(self, tag: str) -> Path | None: """On-disk location of the ``tag`` artifact, or ``None`` when caching is off.""" diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 17d1a03c..225228ab 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -25,7 +25,7 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend -from shapash.compute.embedding_store import EmbeddingStore +from shapash.compute.embedding_store import EmbeddingStore, hash_corpus from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator @@ -46,13 +46,6 @@ def _looks_like_pipeline(model: object) -> bool: return callable(model) and hasattr(model, "tokenizer") -def _hash_texts(text_list: list[str]) -> str: - h = hashlib.md5(usedforsecurity=False) - for t in text_list: - h.update(t.encode()) - return h.hexdigest() - - def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.pkl" @@ -75,12 +68,13 @@ class name only — documented on :meth:`NlpExplainer.compute_projection` as nee @dataclass class InferenceResults: - """Model + explainer output for a batch — a pure function of the input texts. + """Model + explainer output for a batch — a pure function of *(texts, model, backend)*. This is exactly the payload the disk cache persists: recomputing it is the - expensive step ``compile`` memoizes. It deliberately excludes anything that - is *not* a function of the input-text hash — ground truth, class names, the - 2-D projection — so a hash-keyed cache file can never carry a stale ``y_true``. + expensive step ``compile`` memoizes, and all three of its inputs are in the + cache key (see :meth:`NlpExplainer._compute_key`). It deliberately excludes + anything that is *not* determined by those — ground truth, class names, the + 2-D projection — so a keyed cache file can never carry a stale ``y_true``. Attributes ---------- @@ -199,18 +193,21 @@ def __init__( else: self._text_model = None - # The SHAP/LIME backend consumes a plain callable; a TextModel exposes it via shap_callable and - # its companion masker via shap_masker (``None`` for a pipeline-backed model, which SHAP can - # infer a Text masker from; an explicit one when shap_callable is a bare scoring function). - shap_model = self._text_model.shap_callable if self._text_model is not None else model - shap_masker = self._text_model.shap_masker if self._text_model is not None else None - self.backend: NlpBackend = backend or NlpShapBackend( - model=shap_model, - label_names=self.label_names, - masker=shap_masker, - explainer_args=explainer_args or {}, - explainer_compute_args=explainer_compute_args or {}, - ) + # Default backend only when the caller brought none. Building it reads the model's SHAP surface + # (``shap_callable`` + its companion ``shap_masker``: ``None`` for a pipeline-backed model, which + # SHAP can infer a Text masker from; explicit when the callable is a bare scoring function) — + # which must not happen when an explicit backend makes those values unused, or an adapter that + # never intends to be explained by SHAP could not be used at all. + if backend is not None: + self.backend: NlpBackend = backend + else: + self.backend = NlpShapBackend( + model=self._text_model.shap_callable if self._text_model is not None else model, + label_names=self.label_names, + masker=self._text_model.shap_masker if self._text_model is not None else None, + explainer_args=explainer_args or {}, + explainer_compute_args=explainer_compute_args or {}, + ) # Counterfactual generators: explicit > auto-discovered > none. An explicit ``cf_generator`` # is used verbatim (no surprise additions). Otherwise every built-in compatible with the model @@ -261,11 +258,12 @@ def compile( ) -> None: """Compute token-level contributions and model predictions. - Results are cached in memory keyed by a hash of the input texts, so - re-calling ``compile`` with the same data on the same instance skips the - expensive explainer run. Passing only ``y_true`` (same ``x``) is always - a lightweight metadata update. For persistence across kernel restarts, - pass a ``cache_dir`` path to enable an opt-in disk cache. + Results are cached in memory keyed by the input texts *together with the model and backend + that score them* (see :meth:`_compute_key`), so re-calling ``compile`` with the same data on + the same instance skips the expensive explainer run — while changing the model, the backend or + ``label_names`` correctly recomputes. Passing only ``y_true`` (same ``x``) is always a + lightweight metadata update. For persistence across kernel restarts, pass a ``cache_dir`` + path to enable an opt-in disk cache. Parameters ---------- @@ -278,11 +276,13 @@ def compile( cache_dir : str or Path, optional If provided, contributions and predictions are also persisted to ``<cache_dir>/<hash>.pkl`` and reloaded on subsequent calls — even - after a kernel restart. Disabled by default. + after a kernel restart. Disabled by default. One directory can be + shared across models and backends: the hash identifies them, so + entries cannot collide. """ texts = x if isinstance(x, pd.Series) else pd.Series(x) text_list = texts.tolist() - new_hash = _hash_texts(text_list) + new_hash = self._compute_key(text_list) if new_hash != self._data_hash: self.texts = texts @@ -320,6 +320,37 @@ def compile( else pd.Series(y_true, index=self.texts.index, name="ground_truth") ) + def cache_path(self, x: list[str] | pd.Series, cache_dir: str | Path) -> Path: + """Return the file :meth:`compile` would read/write for ``x`` under ``cache_dir``. + + Lets a caller report a cache hit or drop a stale entry without reconstructing the key, which + depends on the bound model and backend and is the explainer's to own (see :meth:`_compute_key`). + + Parameters + ---------- + x : list[str] or pd.Series + The same texts that would be passed to :meth:`compile`. + cache_dir : str or Path + The same directory that would be passed to :meth:`compile`. + + Returns + ------- + Path + The cache file's location. It need not exist. + """ + text_list = x.tolist() if isinstance(x, pd.Series) else list(x) + return _cache_file(self._compute_key(text_list), Path(cache_dir)) + + def clear_cache(self, x: list[str] | pd.Series, cache_dir: str | Path) -> None: + """Drop the cached :meth:`compile` result for ``x``, in memory and on disk. + + The counterpart of :meth:`~shapash.compute.embedding_store.EmbeddingStore.clear` for + contributions: use it to force a fresh explainer run. Only *this* model+backend's entry is + removed — another backend's cache for the same texts is left intact. + """ + self.cache_path(x, cache_dir).unlink(missing_ok=True) + self._data_hash = None + def text_plot(self, pos: int = 0, label_idx: int = 0, max_tokens: int | None = None) -> go.Figure: """Plot token-level contributions for one sample and one class. @@ -623,6 +654,33 @@ def _select_cf_generator(self, generator: str | None) -> CounterfactualGenerator # Private # ------------------------------------------------------------------ + def _compute_key(self, text_list: list[str]) -> str: + """Return the cache key for explaining ``text_list`` with *this* model and backend. + + :class:`InferenceResults` is a function of the texts **and** of everything that scores them, so + all of it belongs in the key: the model's own identity declaration + (:attr:`~shapash.model.base.TextModel.model_id` — checkpoint, pooling, normalization, head + weights), the backend's registered ``name``, its explainer settings, and ``label_names`` (which + fixes the column order of ``y_prob``). + + Keying on the texts alone — as this once did — means swapping the backend (SHAP -> LIG), + the model, or the label order and pointing at the same ``cache_dir`` silently reloads the + previous run's contributions. It also defeats the in-memory guard in :meth:`compile`, so even + without a ``cache_dir`` a re-``compile`` after changing the backend was a no-op. Callers + currently work around this by hand-partitioning ``cache_dir`` per model and per backend; with + the identity in the key that is no longer necessary or possible to get wrong. + """ + text_model = getattr(self, "_text_model", None) + model_id = text_model.model_id if text_model is not None else type(self.model).__name__ + backend = getattr(self, "backend", None) + backend_id = "none" + if backend is not None: + # Sorted repr, so two logically identical configs written in a different order agree. + args = sorted(getattr(backend, "explainer_args", {}).items()) + compute_args = sorted(getattr(backend, "explainer_compute_args", {}).items()) + backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}" + return hash_corpus(text_list, f"{model_id}|{backend_id}|{self.label_names!r}") + def _require_text_model(self) -> TextModel: text_model = getattr(self, "_text_model", None) if text_model is None: From 5bef295314806fa02c582bbe489073dcdeac4993 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 11:28:19 +0200 Subject: [PATCH 084/144] test: add tests to cover recent fixes --- .../compute/generators/test_ablation_flip.py | 55 +++++- .../compute/generators/test_hotflip.py | 78 ++++++++ tests/unit_tests/model/test_model_base.py | 89 +++++++++ tests/unit_tests/model/test_torch_models.py | 64 ++++++ tests/unit_tests/nlp/test_nlp_prototype.py | 184 ++++++++++++++++++ 5 files changed, 468 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/compute/generators/test_ablation_flip.py b/tests/unit_tests/compute/generators/test_ablation_flip.py index 3d6388e6..a13dcaf3 100644 --- a/tests/unit_tests/compute/generators/test_ablation_flip.py +++ b/tests/unit_tests/compute/generators/test_ablation_flip.py @@ -11,8 +11,8 @@ import numpy as np from shapash.compute.generators import AblationFlipGenerator, Counterfactual -from shapash.compute.generators.cf_utils import is_prediction_flip, is_word_token -from shapash.model.base import SupportsTokenization, TextModel +from shapash.compute.generators.cf_utils import is_prediction_flip +from shapash.model.base import SupportsTokenization, TextModel, is_word_token # Per-token [neg_logit, pos_logit] contributions; logits = sum over content tokens. _EMB = { @@ -144,5 +144,56 @@ def test_no_content_tokens_returns_empty(self): self.assertEqual(cfs, []) +class SentencePieceModel(BagOfWordsModel): + """The same classifier, tokenized SentencePiece-style: ``▁`` marks every word *start*. + + Used by DeBERTa-v2/v3, XLM-R, T5 and most multilingual checkpoints. + """ + + def tokenize(self, text): + return [f"▁{w}" for w in text.split()] + + def detokenize(self, tokens): + return " ".join(t.removeprefix("▁") for t in tokens) + + +class TestAblationFlipOnMarkedTokenizer(unittest.TestCase): + """Regression: a word-start-marking tokenizer used to yield zero removal candidates. + + Content tokens are ``"▁great"``, which is not alphabetic, so the old bare ``isalpha`` word test + rejected all of them — ``content_positions`` came back empty and ``generate`` returned ``[]`` for + *every* input. Nothing raised; the webapp just reported "No counterfactual found". + """ + + def setUp(self): + self.gen = AblationFlipGenerator(SentencePieceModel()) + self.gen._ablation_scores = _exact_ablation_scores.__get__(self.gen, AblationFlipGenerator) + + def test_marked_tokens_are_removal_candidates(self): + model = self.gen.model + tokens = model.tokenize("this is great") + self.assertEqual(tokens, ["▁this", "▁is", "▁great"]) + self.assertTrue(all(model.is_substitutable(t) for t in tokens)) + + def test_generates_flip(self): + cfs = self.gen.generate("this is great", config={"num_examples": 3, "max_ablations": 1}) + self.assertTrue(cfs, "no counterfactual on a SentencePiece tokenizer — the C2 regression") + cf = cfs[0] + self.assertEqual((cf.orig_label, cf.new_label), ("pos", "neg")) + self.assertIn("▁great", [old for _, old, _ in cf.substitutions]) + # The rebuilt text is detokenized cleanly (no stray markers) and the flip really holds. + self.assertNotIn("▁", cf.new_text) + self.assertTrue( + is_prediction_flip(self.gen.model.predict([cf.original_text])[0], self.gen.model.predict([cf.new_text])[0]) + ) + + def test_tokens_to_ignore_matches_the_bare_word_a_user_types(self): + # ``tokens_to_ignore`` is typed into the webapp, so it holds plain words. Comparing them + # against raw tokens means "great" never matches "▁great" and the ignore list silently does + # nothing; the comparison goes through the token's display form instead. + cfs = self.gen.generate("this is great", config={"tokens_to_ignore": ["great"], "max_ablations": 1}) + self.assertEqual(cfs, []) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/compute/generators/test_hotflip.py b/tests/unit_tests/compute/generators/test_hotflip.py index 2ec8a3c3..3a02c0a2 100644 --- a/tests/unit_tests/compute/generators/test_hotflip.py +++ b/tests/unit_tests/compute/generators/test_hotflip.py @@ -139,5 +139,83 @@ def test_no_flip_returns_empty(self): self.assertEqual(cfs, []) +class SentencePieceLinearModel(LinearFakeModel): + """The same linear classifier, tokenized SentencePiece-style: ``▁`` marks every word *start*. + + Both the tokens *and the vocabulary* carry the marker, as they do for a real DeBERTa-v3 / XLM-R + tokenizer — which is what makes HotFlip's candidate shortlists empty under the old word test. + """ + + def tokenize(self, text): + return [f"▁{w}" for w in text.split()] + + def detokenize(self, tokens): + return " ".join(t.removeprefix("▁") for t in tokens) + + def get_embedding_table(self): + return [f"▁{t}" for t in _VOCAB], self._matrix + + def token_gradients(self, text, target_class): + tokens, grads = super().token_gradients(text, target_class) + return [f"▁{t}" for t in tokens], grads + + +class TestHotFlipOnMarkedTokenizer(unittest.TestCase): + """Regression: a word-start-marking tokenizer used to produce no candidates at all. + + Every vocabulary entry is ``"▁word"``, which is not alphabetic, so the old bare ``isalpha`` test + rejected all of them: every shortlist came back empty, ``replacement`` stayed empty, and + ``generate`` returned ``[]`` for every input on every SentencePiece model — silently. + """ + + def setUp(self): + self.model = SentencePieceLinearModel() + self.gen = HotFlipGenerator(self.model) + + def test_still_compatible(self): + self.assertTrue(HotFlipGenerator.is_compatible(self.model)) + + def test_marked_vocab_entries_are_candidates(self): + vocab, _ = self.model.get_embedding_table() + self.assertTrue(all(self.model.is_substitutable(t) for t in vocab)) + + def test_generates_flip(self): + cfs = self.gen.generate("this is great", config={"num_examples": 3, "max_flips": 1}) + self.assertTrue(cfs, "no counterfactual on a SentencePiece tokenizer — the C2 regression") + cf = cfs[0] + self.assertEqual((cf.orig_label, cf.new_label), ("pos", "neg")) + self.assertTrue(all(new in {"▁terrible", "▁awful"} for _, _, new in cf.substitutions)) + # The rebuilt text detokenizes cleanly, and the reported flip holds under the model. + self.assertNotIn("▁", cf.new_text) + self.assertTrue( + is_prediction_flip(self.model.predict([cf.original_text])[0], self.model.predict([cf.new_text])[0]) + ) + + def test_tokens_to_ignore_matches_the_bare_word_a_user_types(self): + cfs = self.gen.generate( + "this is great", config={"tokens_to_ignore": ["this", "is", "great"], "max_flips": 2} + ) + self.assertEqual(cfs, []) + + +class TestHotFlipRequiresTokenization(unittest.TestCase): + def test_incompatible_without_tokenization(self): + # HotFlip calls detokenize/is_substitutable, so the capability must be declared, not assumed. + class GradientsOnly(TextModel, SupportsEmbeddings, SupportsGradients): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + def get_embedding_table(self): + return ["a"], np.zeros((1, 2)) + + def embed(self, texts): + return np.zeros((len(texts), 2)) + + def token_gradients(self, text, target_class): + return ["a"], np.zeros((1, 2)) + + self.assertFalse(HotFlipGenerator.is_compatible(GradientsOnly())) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/model/test_model_base.py b/tests/unit_tests/model/test_model_base.py index f4057ccb..9a265144 100644 --- a/tests/unit_tests/model/test_model_base.py +++ b/tests/unit_tests/model/test_model_base.py @@ -14,6 +14,7 @@ SupportsTokenization, TextModel, has_capabilities, + is_word_token, ) @@ -80,5 +81,93 @@ def test_n_classes_none_without_labels(self): self.assertIsNone(m.n_classes) +class _SchemeModel(TextModel, SupportsTokenization): + """Tokenizes by whitespace, then applies a subword-marker convention to each word. + + Stands in for the three families the real adapters meet: WordPiece-style continuation marking + (no prefix), byte-level BPE (``Ġ``) and SentencePiece (``▁``). + """ + + def __init__(self, marker=""): + super().__init__(label_names=["neg", "pos"]) + self.marker = marker + self.tokenize_calls = 0 + + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + def tokenize(self, text): + self.tokenize_calls += 1 + return [f"{self.marker}{w}" for w in text.split()] + + def detokenize(self, tokens): + return " ".join(t.removeprefix(self.marker) for t in tokens) + + +class TestWordStartMarker(unittest.TestCase): + """The marker is probed from what the tokenizer emits, not from a class name or model list.""" + + def test_detects_no_marker_for_continuation_scheme(self): + self.assertIsNone(_SchemeModel().word_start_marker()) + + def test_detects_byte_bpe_marker(self): + self.assertEqual(_SchemeModel(marker="Ġ").word_start_marker(), "Ġ") + + def test_detects_sentencepiece_marker(self): + self.assertEqual(_SchemeModel(marker="▁").word_start_marker(), "▁") + + def test_probe_runs_once_and_is_memoized(self): + model = _SchemeModel(marker="▁") + for _ in range(5): + model.word_start_marker() + self.assertEqual(model.tokenize_calls, 1) + + def test_unprobeable_tokenizer_degrades_to_no_marker(self): + # A tokenizer that cannot run the probe must not break word-hood entirely. + class _Broken(_SchemeModel): + def tokenize(self, text): + raise RuntimeError("tokenizer unavailable") + + model = _Broken() + self.assertIsNone(model.word_start_marker()) + self.assertTrue(model.is_substitutable("happy")) + + +class TestIsSubstitutable(unittest.TestCase): + """Word-hood must follow the model's own scheme — a single ``isalpha`` rule is wrong for two of three. + + ``"▁good"`` is not alphabetic and ``"Ġ"`` (U+0120) *is*, so a bare ``isalpha`` check rejects every + SentencePiece content token and accepts every mid-word byte-BPE fragment. + """ + + def test_continuation_scheme_accepts_bare_words(self): + m = _SchemeModel() + self.assertTrue(m.is_substitutable("happy")) + self.assertFalse(m.is_substitutable("##ing")) # WordPiece continuation + self.assertFalse(m.is_substitutable("[CLS]")) + self.assertFalse(m.is_substitutable("1b")) + self.assertFalse(m.is_substitutable("!")) + + def test_sentencepiece_scheme_accepts_marked_words(self): + m = _SchemeModel(marker="▁") + self.assertTrue(m.is_substitutable("▁good")) # the regression: was False for every token + self.assertFalse(m.is_substitutable("good")) # bare -> mid-word piece under this scheme + self.assertFalse(m.is_substitutable("▁")) # marker alone is not a word + self.assertFalse(m.is_substitutable("▁123")) + self.assertFalse(m.is_substitutable("<s>")) + + def test_byte_bpe_scheme_accepts_marked_words(self): + m = _SchemeModel(marker="Ġ") + self.assertTrue(m.is_substitutable("Ġgood")) + self.assertFalse(m.is_substitutable("good")) # the other regression: was True (mid-word piece) + self.assertFalse(m.is_substitutable("Ġ")) + self.assertFalse(m.is_substitutable("Ċ")) + + def test_is_word_token_is_the_marker_free_default(self): + self.assertTrue(is_word_token("happy")) + self.assertFalse(is_word_token("##ing")) + self.assertFalse(is_word_token("▁good")) # why it cannot be used alone + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/model/test_torch_models.py b/tests/unit_tests/model/test_torch_models.py index ce109e9d..29ef3d90 100644 --- a/tests/unit_tests/model/test_torch_models.py +++ b/tests/unit_tests/model/test_torch_models.py @@ -416,5 +416,69 @@ def test_rejects_unknown_embedding_space_at_construction(self): EncoderClassifierModel(_Body(), _FakeTokenizer(), embedding_space="no_such_space") +def _dropout_head(p=0.5): + """A head that is *not* a no-op in training mode, so eval/train is observable in the output.""" + return nn.Sequential(nn.Linear(4, 4), nn.Dropout(p), nn.Linear(4, 2)) + + +class TestInferenceMode(unittest.TestCase): + """Adapters must pin their backbone to eval mode — dropout/batchnorm can only corrupt analysis. + + Assigning a submodule to an ``nn.Module`` does not touch that submodule's ``training`` flag, so a + head the caller built fresh stays in *training* mode and silently randomises every forward pass: + predictions, contributions, embeddings and counterfactual flips all differ run to run, with nothing + raising. ``from_pretrained`` returns eval-mode models, which made this look fine on the HF path only. + """ + + def test_fused_backbone_is_eval_mode(self): + backbone = build_encoder_head_backbone(_Body(), _dropout_head(), "mean") + self.assertFalse(backbone.training) + self.assertFalse(backbone.head.training) # eval() recurses into head... + self.assertFalse(backbone.body.training) # ...and body + + def test_fused_backbone_forward_is_deterministic(self): + backbone = build_encoder_head_backbone(_Body(), _dropout_head(), "mean") + ids = torch.tensor([[1, 2, 3]]) + mask = torch.ones(1, 3, dtype=torch.long) + first = backbone(input_ids=ids, attention_mask=mask).logits.detach().numpy() + for _ in range(4): + repeat = backbone(input_ids=ids, attention_mask=mask).logits.detach().numpy() + np.testing.assert_allclose(repeat, first, rtol=1e-7) + + def test_torch_classifier_predict_is_deterministic_with_dropout_head(self): + # The end-to-end symptom: identical text, different probabilities on every call. + model = TorchClassifierModel(_Body(), _dropout_head(), _FakeTokenizer(), label_names=["neg", "pos"]) + first = model.predict(["hello world"]) + for _ in range(4): + np.testing.assert_allclose(model.predict(["hello world"]), first, rtol=1e-7) + + def test_embed_is_deterministic_with_dropout_head(self): + # Non-determinism here would also destabilise the scatter and similar-example neighbours. + model = TorchClassifierModel(_Body(), _dropout_head(), _FakeTokenizer(), label_names=["neg", "pos"]) + first = model.embed(["hello world"]) + np.testing.assert_allclose(model.embed(["hello world"]), first, rtol=1e-7) + + def test_encoder_model_evals_any_backbone(self): + # The guard lives in EncoderClassifierModel too, so it covers a hand-passed backbone that did + # not come from build_encoder_head_backbone. + backbone = _Body() + backbone.train() + EncoderClassifierModel(backbone, _FakeTokenizer(), embedding_space="pooled") + self.assertFalse(backbone.training) + + def test_backbone_without_eval_is_tolerated(self): + # The guard is best-effort: a non-nn.Module backbone honouring the contract must still build. + class _PlainBackbone: + device = torch.device("cpu") + + def named_modules(self): + return [] + + def get_input_embeddings(self): + return nn.Embedding(9, 4) + + EncoderClassifierModel(_PlainBackbone(), _FakeTokenizer(), embedding_space="pooled") + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 32b02a06..59b56a89 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -1100,5 +1100,189 @@ def test_compile_caching_skips_rerun(self): self.assertEqual(xpl._data_hash, hash_after_first) +# --------------------------------------------------------------------------- +# compile() cache key +# --------------------------------------------------------------------------- + + +class _KeyModel(TextModel): + """Prediction-only model with a caller-chosen ``model_id``, so keys can be varied deliberately.""" + + def __init__(self, ident="model-a"): + super().__init__(label_names=LABEL_NAMES) + self._ident = ident + + @property + def model_id(self): + return self._ident + + def predict(self, texts): + probs = np.full((len(texts), N_CLASSES), 1.0 / N_CLASSES) + return probs + + +class _MarkerBackend(NlpBackend): + """Returns a constant contribution equal to ``marker``, and counts its explainer runs. + + The constant makes it visible *whose* result a cache served: a value of 1.0 can only have come + from the backend built with ``marker=1.0``. + """ + + name = "marker_backend" + + def __init__(self, marker=1.0, **kwargs): + super().__init__(model=None, label_names=LABEL_NAMES, **kwargs) + self.marker = marker + self.calls = 0 + + def run_explainer(self, x): + self.calls += 1 + texts = list(x) + return NlpRawExplanation( + contributions=[np.full((2, N_CLASSES), self.marker) for _ in texts], + base_values=np.zeros((len(texts), N_CLASSES)), + data=[["a", "b"] for _ in texts], + ) + + +class _OtherMarkerBackend(_MarkerBackend): + """Same behaviour under a different registered ``name`` — a distinct attribution method.""" + + name = "other_marker_backend" + + +def _explainer(model=None, backend=None, label_names=LABEL_NAMES): + return NlpExplainer( + model or _KeyModel(), + label_names=label_names, + backend=backend or _MarkerBackend(), + ) + + +class TestCompileCacheKey(unittest.TestCase): + """``compile`` results depend on *(texts, model, backend)* — so all three must be in the key. + + Keying on the texts alone means swapping the model or the attribution backend and pointing at the + same ``cache_dir`` silently reloads the previous run's contributions, and even without a + ``cache_dir`` the in-memory guard turns a re-``compile`` into a no-op. + """ + + def test_identical_inputs_still_hit_the_in_memory_cache(self): + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.compile(_SAMPLE_TEXTS) + xpl.compile(_SAMPLE_TEXTS) + self.assertEqual(backend.calls, 1, "memoization must survive the richer key") + + def test_swapping_the_backend_recomputes(self): + xpl = _explainer(backend=_MarkerBackend(marker=1.0)) + xpl.compile(_SAMPLE_TEXTS) + replacement = _OtherMarkerBackend(marker=2.0) + xpl.backend = replacement + xpl.compile(_SAMPLE_TEXTS) + self.assertEqual(replacement.calls, 1, "in-memory guard served a stale, other-backend result") + np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + + def test_backend_compute_args_are_part_of_the_key(self): + xpl = _explainer(backend=_MarkerBackend(explainer_compute_args={"n_steps": 50})) + first = xpl._compute_key(_SAMPLE_TEXTS) + xpl.backend = _MarkerBackend(explainer_compute_args={"n_steps": 200}) + self.assertNotEqual(first, xpl._compute_key(_SAMPLE_TEXTS)) + + def test_compute_args_key_is_order_insensitive(self): + # Same settings written in a different order are the same configuration. + a = _explainer(backend=_MarkerBackend(explainer_compute_args={"x": 1, "y": 2})) + b = _explainer(backend=_MarkerBackend(explainer_compute_args={"y": 2, "x": 1})) + self.assertEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_model_identity_is_part_of_the_key(self): + a = _explainer(model=_KeyModel("model-a")) + b = _explainer(model=_KeyModel("model-b")) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_label_names_are_part_of_the_key(self): + # label_names fixes the column order of y_prob, so it changes the cached payload. + a = _explainer(label_names=LABEL_NAMES) + b = _explainer(label_names=list(reversed(LABEL_NAMES))) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_key_is_collision_safe_across_text_boundaries(self): + # Without a separator between texts these two corpora hash identically. + xpl = _explainer() + self.assertNotEqual(xpl._compute_key(["ab", "c"]), xpl._compute_key(["a", "bc"])) + + def test_key_is_order_sensitive(self): + xpl = _explainer() + self.assertNotEqual(xpl._compute_key(["a", "b"]), xpl._compute_key(["b", "a"])) + + +class TestCompileDiskCacheIsolation(unittest.TestCase): + """The disk cache is the dangerous case: a stale entry survives the process that wrote it.""" + + def test_two_backends_share_a_cache_dir_without_collision(self): + with tempfile.TemporaryDirectory() as cache_dir: + first = _MarkerBackend(marker=1.0) + _explainer(backend=first).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + + second = _OtherMarkerBackend(marker=2.0) + xpl = _explainer(backend=second) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(second.calls, 1, "loaded the other backend's cached contributions") + np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + + def test_two_models_share_a_cache_dir_without_collision(self): + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(model=_KeyModel("model-a"), backend=_MarkerBackend(marker=1.0)).compile( + _SAMPLE_TEXTS, cache_dir=cache_dir + ) + backend_b = _MarkerBackend(marker=2.0) + xpl = _explainer(model=_KeyModel("model-b"), backend=backend_b) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend_b.calls, 1, "loaded the other model's cached contributions") + np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + + def test_same_model_and_backend_reload_from_disk(self): + # The cache must still *work* — a fresh instance skips the expensive run. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=3.0)).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + reloaded_backend = _MarkerBackend(marker=3.0) + xpl = _explainer(backend=reloaded_backend) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertEqual(reloaded_backend.calls, 0, "disk cache did not hit for identical inputs") + np.testing.assert_allclose(xpl.contributions.values[0], 3.0) + + def test_cache_path_points_at_the_file_compile_writes(self): + with tempfile.TemporaryDirectory() as cache_dir: + xpl = _explainer() + path = xpl.cache_path(_SAMPLE_TEXTS, cache_dir) + self.assertFalse(path.exists()) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertTrue(path.exists(), "cache_path disagrees with where compile() wrote") + + def test_clear_cache_forces_a_recompute(self): + with tempfile.TemporaryDirectory() as cache_dir: + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + xpl.clear_cache(_SAMPLE_TEXTS, cache_dir) + self.assertFalse(xpl.cache_path(_SAMPLE_TEXTS, cache_dir).exists()) + xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertEqual(backend.calls, 2, "clear_cache must defeat the in-memory guard too") + + def test_clear_cache_leaves_other_backends_entries_intact(self): + with tempfile.TemporaryDirectory() as cache_dir: + keeper = _MarkerBackend(marker=1.0) + _explainer(backend=keeper).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + keeper_path = _explainer(backend=_MarkerBackend(marker=1.0)).cache_path(_SAMPLE_TEXTS, cache_dir) + + other = _explainer(backend=_OtherMarkerBackend(marker=2.0)) + other.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + other.clear_cache(_SAMPLE_TEXTS, cache_dir) + + self.assertTrue(keeper_path.exists(), "clearing one backend dropped another's cache") + + if __name__ == "__main__": unittest.main() From 09811ae4b078aadc6e6b7b3f0ae4eadf12f3a755 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 11:46:44 +0200 Subject: [PATCH 085/144] perf(generators): batch counterfactual search, drop captum from ablation --- demo/serve_nlp_ext.py | 2 +- shapash/compute/generators/ablation_flip.py | 102 ++++------ shapash/compute/generators/base.py | 150 +++++++++++++- shapash/compute/generators/hotflip.py | 56 ++---- .../integration_tests/test_captum_backends.py | 6 +- .../compute/generators/test_ablation_flip.py | 50 +++++ .../compute/generators/test_hotflip.py | 26 +++ .../compute/generators/test_search_minimal.py | 188 ++++++++++++++++++ 8 files changed, 470 insertions(+), 110 deletions(-) create mode 100644 tests/unit_tests/compute/generators/test_search_minimal.py diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py index 11fe58fe..c6eb51c3 100644 --- a/demo/serve_nlp_ext.py +++ b/demo/serve_nlp_ext.py @@ -43,7 +43,7 @@ flip between them freely without ``--recompute``. Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token -substitution) and ``ablation`` (Captum ``FeatureAblation`` token removal) — and are switched from a +substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a **method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py index 30fa22aa..dd55c7a5 100644 --- a/shapash/compute/generators/ablation_flip.py +++ b/shapash/compute/generators/ablation_flip.py @@ -4,26 +4,24 @@ HotFlip *substitutes* tokens using embedding gradients, AblationFlip *removes* tokens it scores as most supportive of the current prediction, until the prediction flips: -1. Score each content token with Captum ``FeatureAblation``: starting from the full text, ablate one - token at a time (to an empty baseline) and measure the drop in the original class's probability. - Positive score = the token supports the original class (removing it hurts that class most). -2. Greedily remove the highest-scoring tokens in combinations of increasing size (1..``max_ablations``), - rebuild the text, re-predict, and keep those that flip the prediction — enforcing **minimality** - (never return a removal set that is a superset of an already-successful one). - -It needs only forward passes (``predict``) plus tokenization, so — unlike HotFlip — it also works on -prediction-only models such as ``HFPipelineModel``. The Captum scoring step (:meth:`_ablation_scores`) -is isolated from the pure greedy-flip loop so the flip/minimality logic is testable without Captum. +1. Score each content token leave-one-out (:meth:`_ablation_scores`): starting from the full text, + drop one token at a time and measure the drop in the original class's probability. Positive score + = the token supports the original class (removing it hurts that class most). +2. Hand the highest-scoring positions to the shared minimal search + (:meth:`~shapash.compute.generators.base.CounterfactualGenerator.search_minimal`), expressing a + removal as an *empty* replacement, and keep the minimal removal sets that flip the prediction. + +It needs only forward passes (``predict``) plus tokenization — no gradients, no torch, no captum — so +unlike HotFlip it also works on prediction-only models such as ``HFPipelineModel``. The scoring step is +kept separate from the search so each is testable on its own. """ from __future__ import annotations -from itertools import combinations from typing import TYPE_CHECKING import numpy as np -from shapash._optional import import_optional_module from shapash.compute.generators.base import ( Counterfactual, CounterfactualGenerator, @@ -31,13 +29,12 @@ IntField, TokenListField, ) -from shapash.compute.generators.cf_utils import display_form, is_prediction_flip +from shapash.compute.generators.cf_utils import display_form from shapash.model.base import SupportsTokenization, TextModel, has_capabilities if TYPE_CHECKING: from collections.abc import Sequence -_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' _MAX_ABLATABLE_TOKENS = 10 @@ -74,7 +71,6 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No orig_probs = model.predict([text])[0] orig_class = int(np.argmax(orig_probs)) - orig_label = label_names[orig_class] if orig_class < len(label_names) else str(orig_class) target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None tokens = model.tokenize(text) @@ -92,62 +88,36 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No scores = self._ablation_scores(tokens, content_positions, orig_class) # Keep the most supportive positions (largest drop when removed) as removal candidates. ranked = [content_positions[j] for j in np.argsort(-scores)][:_MAX_ABLATABLE_TOKENS] - - results: list[Counterfactual] = [] - successful: list[frozenset[int]] = [] - for size in range(1, max_ablations + 1): - for combo in combinations(ranked, size): - combo_set = frozenset(combo) - if any(prev <= combo_set for prev in successful): - continue # minimality: skip supersets of a smaller success - new_tokens = [tok for i, tok in enumerate(tokens) if i not in combo_set] - new_text = model.detokenize(new_tokens) - cf_probs = model.predict([new_text])[0] - if is_prediction_flip(orig_probs, cf_probs, target_class): - cf_class = int(np.argmax(cf_probs)) - results.append( - Counterfactual( - original_text=text, - new_text=new_text, - tokens=list(tokens), - flipped_positions=list(combo), - substitutions=[(pos, tokens[pos], "") for pos in combo], - orig_label=orig_label, - new_label=label_names[cf_class] if cf_class < len(label_names) else str(cf_class), - orig_prob=float(orig_probs[orig_class]), - new_prob=float(cf_probs[cf_class]), - prob_delta=float(orig_probs[orig_class] - cf_probs[orig_class]), - ) - ) - successful.append(combo_set) - if len(results) >= num_examples: - return results - return results + return self.search_minimal( + text, + tokens, + ranked, + lambda _position: "", # an ablation is a substitution by nothing + max_size=max_ablations, + num_examples=num_examples, + orig_probs=orig_probs, + target_class=target_class, + ) def _ablation_scores(self, tokens: list[str], content_positions: Sequence[int], orig_class: int) -> np.ndarray: """Score each content token by the drop in ``orig_class`` probability when it is removed. - Uses Captum ``FeatureAblation`` over a token-presence mask: the input is an all-ones mask (every - content token present); ablating a feature to the zero baseline drops that token from the - rebuilt text. Returns one score per entry of ``content_positions`` (aligned by order). - """ - torch = import_optional_module("torch", extra=_NLP_EXTRA) - captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) + Leave-one-out over the content positions: score the full text once, then once per position with + that token dropped, and take the difference. Returns one score per entry of + ``content_positions`` (aligned by order); a positive score means the token supports + ``orig_class``. + This is what Captum's ``FeatureAblation`` computes over a token-presence mask, done directly + instead. The single ``predict`` call lets the model batch the perturbations on its own device, + where ``FeatureAblation`` issued one forward pass per token — the dominant cost of a + counterfactual run on CPU. It also drops torch and captum from this path entirely, so + AblationFlip now needs nothing beyond the model's own ``predict``. + """ model = self.model if not isinstance(model, SupportsTokenization): # is_compatible guarantees this; guard direct use raise TypeError("AblationFlipGenerator requires a model implementing SupportsTokenization.") - pos_index = {p: j for j, p in enumerate(content_positions)} - - def forward(mask): # mask: (batch, n_content) float tensor of 0/1 - active = mask.detach().cpu().numpy() > 0.5 - texts = [ - model.detokenize([tok for i, tok in enumerate(tokens) if i not in pos_index or row[pos_index[i]]]) - for row in active - ] - return torch.tensor(model.predict(texts), dtype=torch.float32) - - ablator = captum_attr.FeatureAblation(forward) - inputs = torch.ones((1, len(content_positions)), dtype=torch.float32) - attributions = ablator.attribute(inputs, baselines=0.0, target=orig_class) - return attributions.squeeze(0).detach().cpu().numpy() + + texts = [model.detokenize(list(tokens))] + texts += [model.detokenize([t for i, t in enumerate(tokens) if i != p]) for p in content_positions] + probs = model.predict(texts)[:, orig_class] + return probs[0] - probs[1:] diff --git a/shapash/compute/generators/base.py b/shapash/compute/generators/base.py index ef7fff74..794fb2a7 100644 --- a/shapash/compute/generators/base.py +++ b/shapash/compute/generators/base.py @@ -4,15 +4,29 @@ the shapash ``NlpBackend`` ABC style. The tiny :class:`Field` spec types let a generator declare its tunable parameters once; the webapp renders controls from that declaration automatically, so adding a new knob or a new generator needs no UI changes. + +The ABC also owns the **minimal-perturbation search** (:meth:`CounterfactualGenerator.search_minimal`) +that every generator ends in: try perturbation sets of increasing size, rebuild the text, re-predict, +keep the flips, never return a superset of a smaller success. Concrete generators differ only in +*which* positions they nominate and *what* they put there, so they supply a ranked candidate list and +a per-position replacement, and the search is written once — batched. """ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Callable, Sequence from dataclasses import dataclass, field +from itertools import combinations from typing import Any -from shapash.model.base import TextModel +import numpy as np + +from shapash.compute.generators.cf_utils import is_prediction_flip +from shapash.model.base import SupportsTokenization, TextModel + +#: Candidate texts scored per ``predict`` call when the model declares no batch size of its own. +DEFAULT_PREDICT_BATCH_SIZE = 32 @dataclass(frozen=True) @@ -127,3 +141,137 @@ def resolve_config(self, config: dict | None) -> dict: if config: resolved.update({k: v for k, v in config.items() if k in resolved}) return resolved + + @property + def predict_batch_size(self) -> int: + """Number of candidate texts to score per :meth:`~shapash.model.base.TextModel.predict` call. + + Read from the model, which is the only layer that knows its device and how much it can hold in + one forward pass: a CPU adapter configured with a small ``batch_size`` keeps its small batches, + a GPU one keeps its large ones, and neither is second-guessed here. Models that batch + internally and expose nothing (e.g. a pipeline adapter) get + :data:`DEFAULT_PREDICT_BATCH_SIZE`. + """ + return max(1, int(getattr(self.model, "batch_size", DEFAULT_PREDICT_BATCH_SIZE))) + + def search_minimal( + self, + text: str, + tokens: Sequence[str], + candidates: Sequence[int], + replacement: Callable[[int], str], + *, + max_size: int, + num_examples: int, + orig_probs: np.ndarray, + target_class: int | None = None, + ) -> list[Counterfactual]: + """Search for minimal perturbation sets over ``candidates`` that flip the prediction. + + Tries every combination of 1..``max_size`` candidate positions, rebuilds the text by applying + ``replacement`` at each chosen position, re-predicts, and keeps the combinations that flip. + A combination that is a superset of an already-successful one is never evaluated, so results + are minimal and ordered by increasing perturbation size. + + Candidates within one size are scored in **batches**, not one text per ``predict`` call. This + is exact, not an approximation: two distinct combinations of the same size can never be + supersets of one another, so nothing inside a size level can prune anything else inside it — + only smaller sizes can, and those are already resolved when the level starts. Batches are + capped at :attr:`predict_batch_size` rather than run per level, so ``num_examples`` still + stops the search promptly instead of after a whole level has been scored. + + Parameters + ---------- + text : str + The original input text (recorded on each returned counterfactual). + tokens : Sequence[str] + Tokenization of ``text``; positions in ``candidates`` index into it. + candidates : Sequence[int] + Perturbable positions, best first. The caller is responsible for ranking and for + capping how many are offered — the combination count grows steeply with this length. + replacement : Callable[[int], str] + The token to substitute at a given position. Returning ``""`` removes the token, which + is how removal-based generators express an ablation. + max_size : int + Largest number of positions to perturb simultaneously. + num_examples : int + Stop once this many counterfactuals have been found. + orig_probs : np.ndarray, shape (n_classes,) + Class probabilities of ``text``, used as the flip reference. + target_class : int or None + When given, only a flip *to* that class counts as success. + + Returns + ------- + list[Counterfactual] + Minimal successful counterfactuals, ordered by increasing perturbation size. + """ + model = self.model + if not isinstance(model, SupportsTokenization): # rebuilding perturbed text needs detokenize + raise TypeError(f"{type(self).__name__} requires a model implementing SupportsTokenization.") + label_names = model.label_names or [] + orig_class = int(np.argmax(orig_probs)) + orig_label = _label_at(label_names, orig_class) + token_list = list(tokens) + batch_size = self.predict_batch_size + + results: list[Counterfactual] = [] + successful: list[frozenset[int]] = [] + for size in range(1, max_size + 1): + # Prune supersets of smaller successes up front — same-size combinations cannot prune + # each other, so this is the complete pruning available at this level. + combos = [c for c in combinations(candidates, size) if not any(prev <= set(c) for prev in successful)] + for start in range(0, len(combos), batch_size): + chunk = combos[start : start + batch_size] + built = [_apply_replacements(token_list, combo, replacement) for combo in chunk] + texts = [model.detokenize(new_tokens) for new_tokens, _ in built] + cf_probs = model.predict(texts) + for combo, new_text, (_, subs), probs in zip(chunk, texts, built, cf_probs, strict=True): + if not is_prediction_flip(orig_probs, probs, target_class): + continue + cf_class = int(np.argmax(probs)) + results.append( + Counterfactual( + original_text=text, + new_text=new_text, + tokens=token_list, + flipped_positions=list(combo), + substitutions=subs, + orig_label=orig_label, + new_label=_label_at(label_names, cf_class), + orig_prob=float(orig_probs[orig_class]), + new_prob=float(probs[cf_class]), + prob_delta=float(orig_probs[orig_class] - probs[orig_class]), + ) + ) + successful.append(frozenset(combo)) + if len(results) >= num_examples: + return results + return results + + +def _label_at(label_names: list[str], index: int) -> str: + """Return ``label_names[index]``, falling back to the bare index for an unnamed class.""" + return label_names[index] if index < len(label_names) else str(index) + + +def _apply_replacements( + tokens: list[str], combo: Sequence[int], replacement: Callable[[int], str] +) -> tuple[list[str], list[tuple[int, str, str]]]: + """Apply ``replacement`` at every position of ``combo``, returning ``(new_tokens, substitutions)``. + + An empty replacement drops the token instead of substituting it, which is what makes one search + serve both substitution-based and removal-based generators. + """ + chosen = set(combo) + new_tokens: list[str] = [] + subs: list[tuple[int, str, str]] = [] + for i, token in enumerate(tokens): + if i not in chosen: + new_tokens.append(token) + continue + new_token = replacement(i) + subs.append((i, token, new_token)) + if new_token: + new_tokens.append(new_token) + return new_tokens, subs diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index c59a1a27..3ca643b8 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -10,9 +10,10 @@ estimate alone is an unreliable proxy — its single best pick often does not flip at all — so model verification is what surfaces genuine substitutions. Sub-word (``##``) and non-word tokens are excluded so rebuilt text stays well-formed. -3. Try flip combinations of increasing size (1..``max_flips``), rebuild the text, re-predict, and - keep those that flip the prediction — enforcing **minimality** (never return a combination that - is a superset of an already-successful one). +3. Hand the chosen substitutions to the shared minimal search + (:meth:`~shapash.compute.generators.base.CounterfactualGenerator.search_minimal`), which tries flip + combinations of increasing size (1..``max_flips``) in batched ``predict`` calls and keeps the + minimal ones that flip. Requires a model exposing :class:`~shapash.model.base.SupportsGradients` and :class:`~shapash.model.base.SupportsEmbeddings`. @@ -20,7 +21,6 @@ from __future__ import annotations -from itertools import combinations from typing import Protocol, cast import numpy as np @@ -32,7 +32,7 @@ IntField, TokenListField, ) -from shapash.compute.generators.cf_utils import display_form, is_prediction_flip +from shapash.compute.generators.cf_utils import display_form from shapash.model.base import ( SupportsEmbeddings, SupportsGradients, @@ -98,7 +98,6 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No orig_probs = model.predict([text])[0] orig_class = int(np.argmax(orig_probs)) - orig_label = label_names[orig_class] if orig_class < len(label_names) else str(orig_class) target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None tokens, grads = model.token_gradients(text, orig_class) @@ -139,38 +138,13 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No replacement[pos] = shortlist[int(np.argmin(cand_orig_probs))] flippable = [p for p in flippable if p in replacement] - - results: list[Counterfactual] = [] - successful: list[frozenset[int]] = [] - for size in range(1, max_flips + 1): - for combo in combinations(flippable, size): - combo_set = frozenset(combo) - if any(prev <= combo_set for prev in successful): - continue # minimality: skip supersets of a smaller success - new_tokens = list(tokens) - subs: list[tuple[int, str, str]] = [] - for pos in combo: - subs.append((pos, tokens[pos], replacement[pos])) - new_tokens[pos] = replacement[pos] - new_text = model.detokenize(new_tokens) - cf_probs = model.predict([new_text])[0] - if is_prediction_flip(orig_probs, cf_probs, target_class): - cf_class = int(np.argmax(cf_probs)) - results.append( - Counterfactual( - original_text=text, - new_text=new_text, - tokens=list(tokens), - flipped_positions=list(combo), - substitutions=subs, - orig_label=orig_label, - new_label=label_names[cf_class] if cf_class < len(label_names) else str(cf_class), - orig_prob=float(orig_probs[orig_class]), - new_prob=float(cf_probs[cf_class]), - prob_delta=float(orig_probs[orig_class] - cf_probs[orig_class]), - ) - ) - successful.append(combo_set) - if len(results) >= num_examples: - return results - return results + return self.search_minimal( + text, + tokens, + flippable, + replacement.__getitem__, + max_size=max_flips, + num_examples=num_examples, + orig_probs=orig_probs, + target_class=target_class, + ) diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py index 3264da6f..42ae3529 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_captum_backends.py @@ -1,9 +1,13 @@ """Integration tests for the Captum-backed NLP features against a real transformer. Exercises the LayerIntegratedGradients attribution backend (:class:`NlpCaptumLigBackend`) and the -FeatureAblation counterfactual generator (:class:`AblationFlipGenerator`) end to end on the emotion +token-removal counterfactual generator (:class:`AblationFlipGenerator`) end to end on the emotion distilbert model used by the demos. Skipped automatically when ``transformers`` / ``torch`` / ``captum`` are not installed (or the model is unavailable). + +``AblationFlipGenerator`` no longer needs captum (it scores leave-one-out through the model's own +``predict``), so its tests are gated more tightly here than they strictly need to be — they share this +module's real-transformer fixture. """ import numpy as np diff --git a/tests/unit_tests/compute/generators/test_ablation_flip.py b/tests/unit_tests/compute/generators/test_ablation_flip.py index a13dcaf3..71b12608 100644 --- a/tests/unit_tests/compute/generators/test_ablation_flip.py +++ b/tests/unit_tests/compute/generators/test_ablation_flip.py @@ -157,6 +157,56 @@ def detokenize(self, tokens): return " ".join(t.removeprefix("▁") for t in tokens) +class CountingBagOfWordsModel(BagOfWordsModel): + """Bag-of-words model recording each ``predict`` call, for asserting how the search batches.""" + + batch_size = 32 + + def __init__(self): + super().__init__() + self.calls: list[list[str]] = [] + + def predict(self, texts): + texts = list(texts) + self.calls.append(texts) + return super().predict(texts) + + +class TestAblationScores(unittest.TestCase): + """The scorer is plain leave-one-out now, so it is testable directly — captum is not involved.""" + + def setUp(self): + self.model = CountingBagOfWordsModel() + self.gen = AblationFlipGenerator(self.model) + + def test_matches_exact_leave_one_out(self): + tokens = self.model.tokenize("this is great good") + positions = list(range(len(tokens))) + expected = _exact_ablation_scores(self.gen, tokens, positions, orig_class=1) + np.testing.assert_allclose(self.gen._ablation_scores(tokens, positions, orig_class=1), expected) + + def test_supportive_tokens_score_higher(self): + tokens = self.model.tokenize("this is great") + scores = self.gen._ablation_scores(tokens, [0, 1, 2], orig_class=1) + # Removing "great" costs the positive class far more than removing "this". + self.assertGreater(scores[2], scores[0]) + + def test_scores_every_token_in_one_predict_call(self): + tokens = self.model.tokenize("this is great good") + self.gen._ablation_scores(tokens, [0, 1, 2, 3], orig_class=1) + # One call carrying the baseline plus one perturbation per token, not one call per token. + self.assertEqual(len(self.model.calls), 1) + self.assertEqual(len(self.model.calls[0]), 5) + + def test_generate_works_without_stubbing_the_scorer(self): + # End-to-end on a plain predict-only model: the whole generator now runs on numpy alone. + cfs = self.gen.generate("this is great", config={"num_examples": 3, "max_ablations": 2}) + self.assertTrue(cfs) + self.assertEqual(cfs[0].new_label, "neg") + # Scoring (1 call) plus one call per combination *chunk* — a handful, not one call per combo. + self.assertLessEqual(len(self.model.calls), 4) + + class TestAblationFlipOnMarkedTokenizer(unittest.TestCase): """Regression: a word-start-marking tokenizer used to yield zero removal candidates. diff --git a/tests/unit_tests/compute/generators/test_hotflip.py b/tests/unit_tests/compute/generators/test_hotflip.py index 3a02c0a2..8f38f529 100644 --- a/tests/unit_tests/compute/generators/test_hotflip.py +++ b/tests/unit_tests/compute/generators/test_hotflip.py @@ -139,6 +139,32 @@ def test_no_flip_returns_empty(self): self.assertEqual(cfs, []) +class CountingLinearModel(LinearFakeModel): + """Linear fake model recording each ``predict`` call, for asserting how the search batches.""" + + def __init__(self): + super().__init__() + self.calls: list[list[str]] = [] + + def predict(self, texts): + texts = list(texts) + self.calls.append(texts) + return super().predict(texts) + + +class TestHotFlipBatching(unittest.TestCase): + def test_combination_search_is_batched(self): + model = CountingLinearModel() + gen = HotFlipGenerator(model) + # Five flippable positions and max_flips=2 means 5 + C(5,2) = 20 candidate combinations. + cfs = gen.generate("this is great good ok", config={"num_examples": 20, "max_flips": 2}) + self.assertTrue(cfs) + # Budget: one call for the original, one shortlist re-scoring per position, and one per + # combination *size level* — not one per combination, which is what dominated the cost. + self.assertLessEqual(len(model.calls), 1 + 5 + 2) + self.assertGreater(max(len(c) for c in model.calls), 1) + + class SentencePieceLinearModel(LinearFakeModel): """The same linear classifier, tokenized SentencePiece-style: ``▁`` marks every word *start*. diff --git a/tests/unit_tests/compute/generators/test_search_minimal.py b/tests/unit_tests/compute/generators/test_search_minimal.py new file mode 100644 index 00000000..0db536cc --- /dev/null +++ b/tests/unit_tests/compute/generators/test_search_minimal.py @@ -0,0 +1,188 @@ +"""Unit tests for the shared minimal-perturbation search on ``CounterfactualGenerator``. + +Both concrete generators end in the same combination search, so it is tested once here, against a +scripted model that records every ``predict`` call. That record is what lets us assert the two +properties the search claims beyond "it finds flips": it **batches** (one call per chunk, not one per +candidate) and it **prunes** (a superset of a success is never even scored). +""" + +import unittest + +import numpy as np + +from shapash.compute.generators.base import ( + DEFAULT_PREDICT_BATCH_SIZE, + CounterfactualGenerator, +) +from shapash.model.base import SupportsTokenization, TextModel + +_REMOVE = lambda _pos: "" # noqa: E731 - the removal replacement, named for readability + + +class ScriptedModel(TextModel, SupportsTokenization): + """Predicts ``pos`` for everything except the texts the test declares as flipping. + + Records each ``predict`` call as one entry of :attr:`calls`, so tests can assert both how many + calls the search made and exactly which texts were scored. + """ + + def __init__(self, flipping=(), batch_size=None): + super().__init__(label_names=["neg", "pos"]) + self.flipping = set(flipping) + self.calls: list[list[str]] = [] + if batch_size is not None: + self.batch_size = batch_size + + def predict(self, texts): + texts = list(texts) + self.calls.append(texts) + return np.array([[0.9, 0.1] if t in self.flipping else [0.1, 0.9] for t in texts]) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + @property + def scored_texts(self): + """Every text passed to ``predict``, flattened across calls.""" + return [t for call in self.calls for t in call] + + +class SearchGenerator(CounterfactualGenerator): + """Minimal concrete generator — exists only to reach the ABC's shared search.""" + + name = "search_only" + + def config_spec(self): + return {} + + @classmethod + def is_compatible(cls, model): + return True + + def generate(self, text, target_label=None, config=None): + return [] + + +_TEXT = "a b c d e f" +_TOKENS = _TEXT.split() +_POSITIONS = list(range(len(_TOKENS))) +_POS_PROBS = np.array([0.1, 0.9]) + + +def _search(gen, **kwargs): + """Run the shared search over the whole of ``_TEXT`` with removal as the perturbation.""" + params = { + "max_size": 2, + "num_examples": 5, + "orig_probs": _POS_PROBS, + **kwargs, + } + return gen.search_minimal(_TEXT, _TOKENS, _POSITIONS, params.pop("replacement", _REMOVE), **params) + + +class TestPredictBatchSize(unittest.TestCase): + def test_taken_from_the_model(self): + # The model owns device knowledge, so a CPU adapter's small batch is respected as-is. + self.assertEqual(SearchGenerator(ScriptedModel(batch_size=8)).predict_batch_size, 8) + + def test_default_when_the_model_declares_none(self): + self.assertEqual(SearchGenerator(ScriptedModel()).predict_batch_size, DEFAULT_PREDICT_BATCH_SIZE) + + def test_never_below_one(self): + # A zero would make the chunk loop score nothing at all; floor it instead of looping forever. + self.assertEqual(SearchGenerator(ScriptedModel(batch_size=0)).predict_batch_size, 1) + + +class TestSearchMinimalResults(unittest.TestCase): + def test_finds_the_declared_flip(self): + gen = SearchGenerator(ScriptedModel(flipping={"b c d e f"})) + cfs = _search(gen) + self.assertEqual(len(cfs), 1) + cf = cfs[0] + self.assertEqual(cf.original_text, _TEXT) + self.assertEqual(cf.new_text, "b c d e f") + self.assertEqual(cf.flipped_positions, [0]) + self.assertEqual(cf.substitutions, [(0, "a", "")]) + self.assertEqual((cf.orig_label, cf.new_label), ("pos", "neg")) + + def test_substitution_replaces_instead_of_removing(self): + gen = SearchGenerator(ScriptedModel(flipping={"a b X d e f"})) + cfs = _search(gen, replacement=lambda _pos: "X") + self.assertEqual(len(cfs), 1) + self.assertEqual(cfs[0].new_text, "a b X d e f") + self.assertEqual(cfs[0].substitutions, [(2, "c", "X")]) + + def test_results_ordered_by_increasing_size(self): + # "b c d e f" (size 1) and "a b c d" (size 2) both flip; the smaller must come first. + gen = SearchGenerator(ScriptedModel(flipping={"b c d e f", "a b c d"})) + cfs = _search(gen) + self.assertEqual([len(cf.flipped_positions) for cf in cfs], [1, 2]) + + def test_no_flip_returns_empty(self): + self.assertEqual(_search(SearchGenerator(ScriptedModel())), []) + + def test_target_class_must_match(self): + gen = SearchGenerator(ScriptedModel(flipping={"b c d e f"})) + # Flipping towards "pos" (class 1) is impossible: the text is already pos. + self.assertEqual(_search(gen, target_class=1), []) + self.assertEqual(len(_search(gen, target_class=0)), 1) + + def test_requires_a_tokenizable_model(self): + class PredictOnly(TextModel): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + # Rebuilding a perturbed text goes through detokenize; fail loudly rather than AttributeError. + with self.assertRaises(TypeError): + _search(SearchGenerator(PredictOnly())) + + +class TestSearchMinimalBatching(unittest.TestCase): + """The batching is an efficiency claim, so it is asserted on the recorded ``predict`` calls.""" + + def test_one_call_per_chunk_not_per_candidate(self): + gen = SearchGenerator(ScriptedModel(batch_size=DEFAULT_PREDICT_BATCH_SIZE)) + _search(gen, max_size=1) + # Six candidates at size 1, one batch: one call carrying all six texts. + self.assertEqual(len(gen.model.calls), 1) + self.assertEqual(len(gen.model.calls[0]), 6) + + def test_chunks_respect_the_batch_size(self): + gen = SearchGenerator(ScriptedModel(batch_size=4)) + _search(gen, max_size=1) + self.assertEqual([len(c) for c in gen.model.calls], [4, 2]) + + def test_successful_supersets_are_never_scored(self): + gen = SearchGenerator(ScriptedModel(flipping={"b c d e f"}, batch_size=DEFAULT_PREDICT_BATCH_SIZE)) + _search(gen) + # Removing "a" flips, so no size-2 removal that also drops "a" may be evaluated: every text + # scored after the size-1 batch still contains "a". Pruning is what keeps the search tractable, + # and batching a whole size level must not weaken it. + level_two = gen.model.calls[1] + self.assertTrue(all("a" in t.split() for t in level_two)) + self.assertEqual(len(level_two), 10) # C(6,2) = 15, minus the 5 containing position 0 + + def test_num_examples_stops_within_a_level(self): + # Every single-token removal flips, so the cap is reachable on the first chunk; the search + # must not score the remaining candidates of that level just because it batches. + every_removal = {" ".join(_TOKENS[:i] + _TOKENS[i + 1 :]) for i in _POSITIONS} + gen = SearchGenerator(ScriptedModel(flipping=every_removal, batch_size=2)) + cfs = _search(gen, num_examples=1) + self.assertEqual(len(cfs), 1) + self.assertEqual(len(gen.model.calls), 1) + self.assertEqual(len(gen.model.scored_texts), 2) + + def test_batching_does_not_change_the_outcome(self): + # Exactness check: batch of 1 (the old per-combination behaviour) against a full-level batch. + flipping = {"b c d e f", "a b c d", "a c d e f"} + one_at_a_time = _search(SearchGenerator(ScriptedModel(flipping=flipping, batch_size=1))) + batched = _search(SearchGenerator(ScriptedModel(flipping=flipping, batch_size=64))) + self.assertEqual(one_at_a_time, batched) + self.assertTrue(batched) + + +if __name__ == "__main__": + unittest.main() From 20b2bb0eb2ad73e0a3e333c8efe7ea82d85c4658 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 14:31:55 +0200 Subject: [PATCH 086/144] chore(demo): consolidate NLP demo into a single serve_nlp.py flow The old snapshot-only server (serve_nlp.py, test_webapp_nlp.py, explainer_snapshot.pkl, Dockerfile) is dropped in favor of the more capable live-model flow, previously serve_nlp_ext.py: it supports configurable models/datasets, dual attribution backends, and custom model builders that the old script never gained. - Rename serve_nlp_ext.py -> serve_nlp.py, Dockerfile.nlp_ext -> Dockerfile, demo/nlp_ext_cache/ -> demo/nlp_cache/ - Collapse docker-compose.yml to one shapash-nlp service, unify on port 8050 - Update README.md to document only the remaining flow --- demo/Dockerfile | 46 +++- demo/Dockerfile.nlp_ext | 52 ---- demo/README.md | 130 +++------ demo/docker-compose.yml | 12 +- demo/explainer_snapshot.pkl | Bin 156540 -> 0 bytes demo/serve_nlp.py | 511 ++++++++++++++++++++++++++++++++++- demo/serve_nlp_ext.py | 514 ------------------------------------ demo/test_webapp_nlp.py | 129 --------- 8 files changed, 571 insertions(+), 823 deletions(-) delete mode 100644 demo/Dockerfile.nlp_ext delete mode 100644 demo/explainer_snapshot.pkl delete mode 100644 demo/serve_nlp_ext.py delete mode 100644 demo/test_webapp_nlp.py diff --git a/demo/Dockerfile b/demo/Dockerfile index 412d1339..28f7e72e 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -1,7 +1,13 @@ -# Build from the repo root: -# docker build -f demo/Dockerfile -t shapash-nlp . +# NLP Explainer image (CPU-only, live model — the What-if Lab needs it in memory). +# +# This is a convenience wrapper around `demo/serve_nlp.py`, the actual serving script — +# run that once to generate `demo/nlp_cache/` before building. The expensive part — SHAP + +# the projection — is computed ONCE outside Docker and copied in as a cache (see demo/README.md); +# the build never runs SHAP. The model + dataset are downloaded in a build layer (fast, ~270 MB, +# cached like any pip layer), so runtime is fully offline: no downloads, no SHAP, no GPU at run time. # -# The image contains the snapshot baked in, so colleagues only need the image: +# Build from the repo root (fails fast if nlp_cache is missing — do Step 1 first): +# docker build -f demo/Dockerfile -t shapash-nlp . # docker run --rm -p 8050:8050 shapash-nlp # Then open http://localhost:8050 @@ -9,19 +15,37 @@ FROM python:3.11-slim WORKDIR /app -# Install shapash base dependencies (dash, plotly, pandas, numpy, …) -# then add dash-ag-grid which is in the [nlp] optional group but needed by NlpWebApp. -# Heavy ML deps (torch, transformers, datasets, pacmap) are intentionally omitted. +# CPU-only torch first (from PyTorch's CPU index — smaller, no CUDA), so the [nlp] extra +# sees torch already satisfied and doesn't pull the CUDA build from PyPI. +RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu "torch>=2.12.1" + COPY pyproject.toml README.md MANIFEST.in ./ COPY shapash/ ./shapash/ -# setuptools-scm can't find .git in the build context; any placeholder works since -# the running app doesn't use the installed package version at runtime. +# setuptools-scm can't find .git in the build context; any placeholder works since the +# running app doesn't use the installed package version at runtime. ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=0.0.0.dev -RUN pip install --no-cache-dir "." "dash-ag-grid>=35.2.0" "pyarrow>=17.0.0" +RUN pip install --no-cache-dir ".[nlp]" "pyarrow>=17.0.0" + +# Download the model + dataset into the image (a plain download, not SHAP compute). +ENV HF_HOME=/app/hf_cache +RUN python -c "\ +from transformers import AutoTokenizer, AutoModelForSequenceClassification; \ +import datasets; \ +m = 'bhadresh-savani/distilbert-base-uncased-emotion'; \ +AutoTokenizer.from_pretrained(m, use_fast=True); \ +AutoModelForSequenceClassification.from_pretrained(m); \ +datasets.load_dataset('dair-ai/emotion', split='test')" -# Serve script + pre-computed snapshot (no model required at runtime) COPY demo/serve_nlp.py ./serve_nlp.py -COPY demo/explainer_snapshot.pkl ./explainer_snapshot.pkl +# Pre-computed SHAP + projection cache (built by running serve_nlp.py once — see README). +COPY demo/nlp_cache/ ./nlp_cache/ + +# Serve fully offline: model from the baked HF cache, SHAP from nlp_cache, no network. +# PYTHONUNBUFFERED streams logs; TOKENIZERS_PARALLELISM avoids a fork deadlock. +ENV HF_HUB_OFFLINE=1 \ + TRANSFORMERS_OFFLINE=1 \ + TOKENIZERS_PARALLELISM=false \ + PYTHONUNBUFFERED=1 EXPOSE 8050 diff --git a/demo/Dockerfile.nlp_ext b/demo/Dockerfile.nlp_ext deleted file mode 100644 index 70c9fa7a..00000000 --- a/demo/Dockerfile.nlp_ext +++ /dev/null @@ -1,52 +0,0 @@ -# NLP Explainer Extended image (CPU-only). Unlike demo/Dockerfile (model-free snapshot), -# this one carries a live model because the What-if Lab needs it. -# -# The expensive part — SHAP + the projection — is computed ONCE outside Docker and copied -# in as a cache (see demo/README.md); the build never runs SHAP. The model + dataset are -# downloaded in a build layer (fast, ~270 MB, cached like any pip layer), so runtime is -# fully offline: no downloads, no SHAP, no GPU at run time. -# -# Build from the repo root (fails fast if nlp_ext_cache is missing — do Step 1 first): -# docker build -f demo/Dockerfile.nlp_ext -t shapash-nlp-ext . -# docker run --rm -p 8051:8051 shapash-nlp-ext -# Then open http://localhost:8051 - -FROM python:3.11-slim - -WORKDIR /app - -# CPU-only torch first (from PyTorch's CPU index — smaller, no CUDA), so the [nlp] extra -# sees torch already satisfied and doesn't pull the CUDA build from PyPI. -RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu "torch>=2.12.1" - -COPY pyproject.toml README.md MANIFEST.in ./ -COPY shapash/ ./shapash/ -# setuptools-scm can't find .git in the build context; any placeholder works since the -# running app doesn't use the installed package version at runtime. -ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SHAPASH=0.0.0.dev -RUN pip install --no-cache-dir ".[nlp]" "pyarrow>=17.0.0" - -# Download the model + dataset into the image (a plain download, not SHAP compute). -ENV HF_HOME=/app/hf_cache -RUN python -c "\ -from transformers import AutoTokenizer, AutoModelForSequenceClassification; \ -import datasets; \ -m = 'bhadresh-savani/distilbert-base-uncased-emotion'; \ -AutoTokenizer.from_pretrained(m, use_fast=True); \ -AutoModelForSequenceClassification.from_pretrained(m); \ -datasets.load_dataset('dair-ai/emotion', split='test')" - -COPY demo/serve_nlp_ext.py ./serve_nlp_ext.py -# Pre-computed SHAP + projection cache (built by running serve_nlp_ext.py once — see README). -COPY demo/nlp_ext_cache/ ./nlp_ext_cache/ - -# Serve fully offline: model from the baked HF cache, SHAP from nlp_ext_cache, no network. -# PYTHONUNBUFFERED streams logs; TOKENIZERS_PARALLELISM avoids a fork deadlock. -ENV HF_HUB_OFFLINE=1 \ - TRANSFORMERS_OFFLINE=1 \ - TOKENIZERS_PARALLELISM=false \ - PYTHONUNBUFFERED=1 - -EXPOSE 8051 - -CMD ["python", "serve_nlp_ext.py"] diff --git a/demo/README.md b/demo/README.md index d490d501..7f65de5e 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,97 +1,35 @@ # Shapash NLP Explainer — Demo -Interactive webapp that explains a DistilBERT emotion classifier at the token level using SHAP values. - -The base app ([below](#nlp-explainer)) is served read-only from a snapshot. An optional -[**Extended**](#nlp-explainer-extended) variant adds a live-model What-if Lab on top. +Interactive webapp that explains a DistilBERT emotion classifier at the token level using SHAP +values, with a live **What-if Lab**: edit a sample's text for a live prediction + token-contribution +highlight, and get auto-generated minimal token flips (counterfactuals) that change the prediction. + +**Webapp layout**, three panels: +- Left: control panel with **Dataset** (Dash AG Grid to explore the dataset and click on texts), + **Embeddings** (scatter plot, used to lasso/box select, highlight word importance), and **Data Editor** +- Upper Right: **Word Importance** (global explainability) tab and **Counterfactuals** tab. +- Lower Right: **Local contributions** divided as sentence highlights tab and waterfall plot tab. -| Variant | Port | Live model? | Image | What it adds | -|---|---|---|---|---| -| **NLP Explainer** | 8050 | No — served from a snapshot | Lightweight (no torch/GPU) | Explore precomputed SHAP explanations | -| **NLP Explainer Extended** | 8051 | Yes — model in memory (CPU) | Heavier; model + caches baked in | The above **plus** live editing, re-prediction, and counterfactual suggestions | +**`demo/serve_nlp.py` is the serving script** — everything below runs it with its defaults (the +emotion model/dataset). `Dockerfile`/`docker-compose.yml` are just a convenience wrapper around it: +they never run SHAP themselves, they only copy in a cache built by running the script once (Step 1 +below). Model, dataset, split, columns, sample count, and a ground-truth label-renaming map are all +configurable via `ServeConfig` in that file — see its module docstring for +`--model-name`/`--dataset-name`/`--dataset-split`/`--text-column`/`--label-column`/`--label-map`/`--n` +and worked examples (e.g. swapping in an IMDB sentiment model/dataset). `Dockerfile` itself stays +hardcoded to the emotion example on purpose (it's meant to run out of the box for anyone cloning the +repo) — its build-time warm-up (`RUN python -c "..."`) downloads exactly that model/dataset/split, so +it always matches `ServeConfig`'s defaults. Serving a different model/dataset combo in Docker means +editing that warm-up line to match, then redoing Step 1 below with the same flags before rebuilding. --- -# NLP Explainer - ## Prerequisites -- [Docker](https://docs.docker.com/get-docker/) installed and running -- The snapshot file `demo/explainer_snapshot.pkl` (see Step 1 below) - ---- - -## Step 1 — Generate the snapshot (one-time, requires GPU) - -This step runs the model and computes SHAP values for 100 samples. It only needs to be done once; the result is saved as a self-contained file. - -```bash -# From the repo root -pip install "shapash[nlp]" -python demo/test_webapp_nlp.py -``` - -This writes `demo/explainer_snapshot.pkl`. The file bundles texts, predictions, SHAP contributions, and the 2-D scatter projection — everything the webapp needs. - -> **Already have the snapshot?** Skip to Step 2. - ---- - -## Step 2 — Build and run the app - -Run from the **repo root** (the build context needs access to the `shapash/` package): - -```bash -docker compose -f demo/docker-compose.yml up --build shapash-nlp -``` - -The image installs only the lightweight serving dependencies (Dash, Plotly, pandas …). No model, no GPU, no heavy ML libraries. Drop `--build` on subsequent runs if nothing has changed. - -Then open **http://localhost:8050** in your browser. - -Press `Ctrl+C` to stop. +- [Docker](https://docs.docker.com/get-docker/) installed and running (for the Docker path), or the + `[nlp]` extra installed locally (`pip install "shapash[nlp]"`) ---- - -## Webapp overview - -| Panel | Description | -|---|---| -| **Global word importance** | Mean SHAP contribution per word for the selected class. Use the top-K slider, sign filter, and word exclusion list to explore. | -| **Sample scatter** | 2D PaCMAP projection of text embeddings. Draw a box, lasso or click to filter the table and word importance to a subset. | -| **Dataset table** | All samples with predicted label, ground truth, and predicted probability. Click a row to inspect it. Click a bar in the word importance chart to filter to samples containing that word. | -| **Local contributions** | Token-level SHAP highlight for the selected sample. Toggle to a waterfall chart for a ranked view. | - ---- - -# NLP Explainer Extended - -Adds a **Data Editor** tab: edit a sample's text for a live prediction + token-contribution highlight, and a **Counterfactuals** tab to -get auto-generated minimal token flips (counterfactuals) that change the prediction. - -This needs the live model in memory, so it can't use the snapshot — but SHAP, the projection, and the -model/dataset downloads are all cached, so only the first run pays for it. Runs on **CPU**. - -**New webapp layout** with three panels: -- Left: control panel with **Dataset** (Dash AG Grid to explore the dataset and click on texts), **Embeddings** (Scatter plot, used to lasso/box select, hihglight word importance), and **Data Editor** -- Upper Right: **Word Importance** (Global explainability) tab and **Counterfactuals** tab. -- Lower Right: **Local contributions** divided as sentence highlights tab and waterfall plot tab. - -Same idea as the snapshot: compute the heavy stuff (SHAP) **once, up front**, then Docker just loads -it. The model + dataset are downloaded by the Docker build itself, so you don't stage them. - -**`demo/serve_nlp_ext.py` is the serving script**, and everything below runs it with its defaults -(the emotion model/dataset). Model, dataset, split, columns, sample count, and a ground-truth -label-renaming map are all configurable via `ServeConfig` in that file — see its module docstring -for `--model-name`/`--dataset-name`/`--dataset-split`/`--text-column`/`--label-column`/`--label-map`/ -`--n` and worked examples (e.g. swapping in an IMDB sentiment model/dataset). `Dockerfile.nlp_ext` -itself stays hardcoded to the emotion example on purpose (it's meant to run out of the box for -anyone cloning the repo) — its build-time warm-up (`RUN python -c "..."`) downloads exactly that -model/dataset/split, so it always matches `ServeConfig`'s defaults. Serving a different model/dataset -combo in Docker means editing that warm-up line to match, then redoing Step 1 below with the same -flags before rebuilding. - -### Step 1 — Pre-compute the SHAP cache (one-time, GPU optional) +## Step 1 — Pre-compute the SHAP cache (one-time, GPU optional) Needs the `[nlp]` extra (torch, transformers, datasets, pacmap). Run the script once — it computes SHAP + the projection (a few minutes on CPU, faster on GPU), writes the cache, then starts serving. Once it @@ -99,40 +37,40 @@ prints the local URL, the cache is on disk — press `Ctrl+C`: ```bash pip install "shapash[nlp]" -python demo/serve_nlp_ext.py # Ctrl+C once it starts serving +python demo/serve_nlp.py # Ctrl+C once it starts serving ``` -This writes `demo/nlp_ext_cache/` — SHAP contributions, predictions, and the 2-D projection, keyed by a +This writes `demo/nlp_cache/` — SHAP contributions, predictions, and the 2-D projection, keyed by a hash of the input texts. The Docker build copies it in, so this expensive step never runs in Docker. -> **Already have `demo/nlp_ext_cache/`?** Skip to Step 2. +> **Already have `demo/nlp_cache/`?** Skip to Step 2. -### Step 2 — Build and run +## Step 2 — Build and run Run from the **repo root**: ```bash -docker compose -f demo/docker-compose.yml up --build shapash-nlp-ext +docker compose -f demo/docker-compose.yml up --build shapash-nlp ``` The build downloads the model + dataset into the image and copies in the SHAP cache; at **run** time it -serves fully offline — no downloads, no SHAP, no GPU. (The build fails fast if `demo/nlp_ext_cache/` is +serves fully offline — no downloads, no SHAP, no GPU. (The build fails fast if `demo/nlp_cache/` is missing — do Step 1 first.) -Then open **http://localhost:8051** in your browser. Press `Ctrl+C` to stop. +Then open **http://localhost:8050** in your browser. Press `Ctrl+C` to stop. ## Run without Docker -Just run the same script — it serves on http://127.0.0.1:8051 and reuses the `demo/nlp_ext_cache/` +Just run the same script — it serves on http://127.0.0.1:8050 and reuses the `demo/nlp_cache/` from Step 1 (recomputing only if the input texts change, or if you pass `--recompute`): ```bash -python demo/serve_nlp_ext.py # ServeConfig defaults (500 samples, dair-ai/emotion test split) -python demo/serve_nlp_ext.py --n 1000 # more samples (recomputes + re-caches) +python demo/serve_nlp.py # ServeConfig defaults (500 samples, dair-ai/emotion test split) +python demo/serve_nlp.py --n 1000 # more samples (recomputes + re-caches) ``` It uses the GPU if present, otherwise CPU. -> **Iterating on the webapp code?** Don't restart the process — run `serve_nlp_ext.py`'s body in a REPL +> **Iterating on the webapp code?** Don't restart the process — run `serve_nlp.py`'s body in a REPL > once, then re-call `xpl.run_app(...)` after reloading the changed webapp modules. The model stays > resident, so you never pay the reload. diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index b3e68e2b..0d551aa9 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,4 +1,6 @@ services: + # NLP Explainer (CPU, live model). Loads pre-computed caches — run serve_nlp.py + # once first to build them (see demo/README.md). shapash-nlp: build: context: .. @@ -6,13 +8,3 @@ services: image: shapash-nlp ports: - "8050:8050" - - # NLP Explainer Extended (CPU, live model). Heavier image; loads pre-computed caches - # (run serve_nlp_ext.py once first to build them — see demo/README.md). - shapash-nlp-ext: - build: - context: .. - dockerfile: demo/Dockerfile.nlp_ext - image: shapash-nlp-ext - ports: - - "8051:8051" diff --git a/demo/explainer_snapshot.pkl b/demo/explainer_snapshot.pkl deleted file mode 100644 index 2d81091ab4489d02532af17b331ccdf271fae544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156540 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 diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 4df32e27..878fab4a 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -1,24 +1,513 @@ -"""Serve the NLP explainer webapp from a pre-computed snapshot. +"""Serve the NLP explainer **with the What-if Lab** (live model, cached compute). + +The What-if Lab needs a live model in memory — editing a sample, re-predicting, and generating +counterfactuals all call the real model, so a model-free snapshot (``from_snapshot``) makes the +lab self-disable. There is therefore no way to serve what-if without loading the model. + +What *can* be avoided on every restart is recomputing the expensive stuff around the +model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): + +* **SHAP contributions + predictions** — via the built-in ``compile(cache_dir=...)``. +* **The text embeddings and the 2-D projection** — via ``compute_projection(cache_dir=...)``. The + library owns the embedding + caching (so the scatter and the similar-example neighbours are always + in the same space); this script only supplies the reducer, PaCMAP. + +The caching is automatic: each run **loads** the cache if one exists for these exact +texts, otherwise it **computes and writes** it. So the first run is slow; every run after +pays only the model load (a few seconds on CPU). Pass ``--recompute`` to force a fresh +compute (e.g. after changing the explainer). Iterating on the webapp itself? Don't +restart at all — keep this in a REPL and re-call ``run_app`` after reloading the webapp +modules; the model stays resident. + +The model runs on CPU when no GPU is present, so this serves fine on a plain box. + +Model and dataset are configurable (see ``ServeConfig``): swap ``--model-name`` for any HF +sequence-classification checkpoint (its label names are read from the model's own config) and +``--dataset-name`` for any HF ``datasets`` classification dataset (point ``--text-column`` / +``--label-column`` at the right fields if they aren't ``text`` / ``label``). Cache keys cover the model +and the attribution backend as well as the texts, so switching ``--model-name`` can never reload another +model's contributions; the per-model/per-dataset subdirectories under ``--cache-dir`` below are for +human legibility (and easy pruning), not for correctness. + +Dataset and model label strings aren't guaranteed to match even when their class order does (e.g. +a dataset's ``ClassLabel`` names ``"neg"``/``"pos"`` vs. a model's ``config.id2label`` values +``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the +model's spelling so predicted vs. ground-truth comparisons in the webapp line up. + +The sentence-highlight attribution method is selectable (a Captum-backed alternative to the default, +see the ``[nlp]`` extra): + +* ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` + (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can + flip between them freely without ``--recompute``. + +Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token +substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a +**method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. + +The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: +``<model>/<dataset>__<split>/`` holds the (backend-independent) embedding-store artifacts — the +vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or +``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.pkl`` contributions. + +Serving a *custom* model (external-head checkpoints) +---------------------------------------------------- +Any standard ``AutoModelForSequenceClassification`` checkpoint loads automatically from ``--model-name`` +alone (``load_model`` builds an ``HFClassifierModel``; label names come from ``config.id2label``). That +covers every certified encoder architecture — BERT, DistilBERT, RoBERTa, XLM-R, DeBERTa-v1, and a MiniLM +packaged as a sequence classifier. + +A model whose classification **head is custom** cannot be introspected from its name — its layer +structure, weight-key convention, and input contract are non-standard (a sentence-transformer body + a +separate MLP head, a SetFit head, a hand-rolled ``nn.Module``, …). These are served through the +``MODEL_BUILDERS`` registry: a ``{--model-name: builder}`` map consulted *before* the generic path. To add +one: + +1. Write a builder ``def build_x(config: ServeConfig, device: str) -> TextModel:`` that assembles the + right adapter and returns it. Put **all** model-specific code inside it — the core ``load_model`` never + changes. Pick the adapter by shape: + + * **sentence-transformer body + head** → :class:`~shapash.model.SentenceTransformerModel` (pass the + ``sentence_transformers`` model + a *logits* head ``nn.Module``; drop any final ``softmax`` the head + applies — ``predict`` softmaxes and the LIG backend needs logit space; set ``pool=`` to the body's + pooling mode, usually ``"mean"``). + * **raw encoder body + head** (head not fused into an ``AutoModelForSequenceClassification``) → + :class:`~shapash.model.TorchClassifierModel` (pass ``body, head, tokenizer``; ``pool="cls"`` for a + CLS-pooled head, ``"mean"`` otherwise). + * **standard classifier that merely needs ``trust_remote_code``** → just call + ``HFClassifierModel(AutoModelForSequenceClassification.from_pretrained(name, trust_remote_code=True), + tokenizer)``. + +2. Register it: ``MODEL_BUILDERS["org/your-model"] = build_x``. Nothing else changes — caching, the + What-if Lab, both attribution backends, counterfactuals, and similar-examples all work through the + capability interface regardless of adapter. + +``_build_shhossain_sentiment_model`` (registered for +``shhossain/all-MiniLM-L6-v2-sentiment-classifier``) is the worked example: a MiniLM ST body + a trained +``fc1 -> relu -> fc2 -> relu -> out`` MLP head, ``trust_remote_code`` with no HF tokenizer, so the generic +path can't touch it — but ``SentenceTransformerModel`` serves it faithfully (it reproduces the source +model's predictions exactly). Usage ----- - python serve_nlp.py [path/to/explainer_snapshot.pkl] + python demo/serve_nlp.py [--n 100] [--cache-dir demo/nlp_cache] [--port 8050] + python demo/serve_nlp.py --recompute # ignore the cache and recompute + python demo/serve_nlp.py --attribution lig # Captum LayerIntegratedGradients highlights + python demo/serve_nlp.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ + --dataset-name sst2 --dataset-split validation --text-column sentence + python demo/serve_nlp.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ + --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' + # A registered custom (external-head) model — served via MODEL_BUILDERS, no other flags needed: + python demo/serve_nlp.py --model-name shhossain/all-MiniLM-L6-v2-sentiment-classifier -The snapshot is produced by running ``demo/test_webapp_nlp.py``, which calls -``NlpExplainer.save_snapshot()`` after compile. No model, dataset, or GPU is -required at serve time. +First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). A ``SentenceTransformerModel`` +custom builder additionally needs the ``sentence-transformers`` package. """ from __future__ import annotations -import sys +import argparse +import json +import logging +from collections.abc import Callable +from dataclasses import dataclass, field from pathlib import Path +import datasets +import pacmap +import torch +import transformers +from torch import nn + +from shapash.backend import NlpCaptumLigBackend from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel + +_HERE = Path(__file__).parent + +logger = logging.getLogger("serve_nlp") + + +@dataclass +class ServeConfig: + """Everything needed to load data + model and serve the What-if webapp. + + A single object (rather than argparse's ``Namespace``) so the loading helpers below + stay reusable from a REPL too — see the module docstring on iterating without restarting. + """ + + model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" + dataset_name: str = "dair-ai/emotion" + dataset_split: str = "test" + text_column: str = "text" + label_column: str = "label" + label_map: dict[str, str] = field(default_factory=dict) + n: int = 500 + attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) + # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own + # training split) and how many rows of it to bank. Set ``n_reference=0`` to disable the "Similar + # Examples" panel. + reference_split: str = "train" + n_reference: int = 2000 + # Representation space the scatter projects *and* neighbours are ranked in — one setting for both, + # so they can never disagree. ``None`` keeps whatever the model was built with ("decision"). + embedding_space: str | None = None + cache_dir: Path = _HERE / "nlp_cache" + port: int = 8050 + host: str = "0.0.0.0" # noqa: S104 + recompute: bool = False + + +# Short ``--attribution`` choice → the attribution backend's registered ``.name`` (used as the cache +# subdirectory *and* to pick the backend in ``build_backend``). +_ATTRIBUTION_BACKENDS = {"shap": "nlp_shap", "lig": "nlp_captum_lig"} + + +def _dataset_slug(config: ServeConfig) -> str: + """Filesystem-safe ``<dataset>__<split>`` tag for the cache hierarchy.""" + return f"{config.dataset_name}__{config.dataset_split}".replace("/", "__") + + +def _dataset_cache_dir(config: ServeConfig) -> Path: + """Return ``cache_dir/<model>/<dataset>__<split>`` — the level the projection is cached at. + + The compile and projection caches both key on the model (and, for contributions, the backend), so + these path levels are organisational rather than load-bearing: they keep a ``--cache-dir`` readable + and prunable per model/dataset. The PaCMAP projection embeds the texts with ``model.embed`` and + never touches the attribution backend, so every ``--attribution`` choice shares one projection cache + at *this* level (below it, contributions split per backend). + """ + return config.cache_dir / config.model_name.replace("/", "__") / _dataset_slug(config) + + +def _compile_cache_dir(config: ServeConfig) -> Path: + """Return ``<dataset-dir>/<backend>`` — the level contributions are cached at. + + Contributions *do* depend on the attribution backend, so SHAP and LIG get separate subdirectories. + Switching ``--attribution`` then reads/writes a different ``<hash>.pkl`` instead of silently + reusing the other method's cached highlights — no ``--recompute`` needed to swap methods. + """ + return _dataset_cache_dir(config) / _ATTRIBUTION_BACKENDS[config.attribution] + + +def parse_args(argv: list[str] | None = None) -> ServeConfig: + """Parse CLI args into a ``ServeConfig``, defaulting every flag from the dataclass.""" + defaults = ServeConfig() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--model-name", default=defaults.model_name, help="HF model id (label names are read from its config)." + ) + parser.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") + parser.add_argument("--dataset-split", default=defaults.dataset_split) + parser.add_argument("--text-column", default=defaults.text_column, help="Column holding the raw text.") + parser.add_argument("--label-column", default=defaults.label_column, help="Column holding the ground-truth label.") + parser.add_argument( + "--label-map", + type=json.loads, + default=defaults.label_map, + help=( + "JSON object renaming dataset label strings to match the model's own label names, e.g. " + '\'{"neg": "NEGATIVE", "pos": "POSITIVE"}\'. Needed when the dataset and model disagree on ' + "label spelling/casing even though their class order matches." + ), + ) + parser.add_argument("--n", type=int, default=defaults.n, help="Number of samples to load.") + parser.add_argument( + "--attribution", + choices=list(_ATTRIBUTION_BACKENDS), + default=defaults.attribution, + help=( + "Sentence-highlight attribution method: 'shap' (KernelSHAP, the default) or 'lig' " + "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." + ), + ) + parser.add_argument( + "--reference-split", + default=defaults.reference_split, + help="Dataset split used as the similar-examples reference corpus (the model's training split).", + ) + parser.add_argument( + "--n-reference", + type=int, + default=defaults.n_reference, + help="Reference rows to bank for 'Similar Examples' (0 disables the panel).", + ) + parser.add_argument( + "--embedding-space", + default=defaults.embedding_space, + help=( + "Representation space for the 2-D scatter and similar-example retrieval: 'decision' " + "(input to the final classification linear, class-discriminative), 'pooled' (the pooled " + "last hidden state, semantic), or any backbone submodule name. Default: the model's own." + ), + ) + parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) + parser.add_argument("--port", type=int, default=defaults.port) + parser.add_argument("--host", default=defaults.host) + parser.add_argument( + "--recompute", + action="store_true", + help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", + ) + args = parser.parse_args(argv) + return ServeConfig(**vars(args)) + + +def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: + """Load the first ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. + + Ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when + present (the usual shape for HF classification datasets); otherwise the raw values are + stringified as-is. ``config.label_map`` then renames them to match the model's label + names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model + ``"NEGATIVE"``) despite sharing the same class order. + """ + dataset = datasets.load_dataset(config.dataset_name, split=split) + sentences = dataset[config.text_column][:n] + raw_labels = dataset[config.label_column][:n] + names = getattr(dataset.features.get(config.label_column), "names", None) + labels = [names[i] for i in raw_labels] if names is not None else [str(v) for v in raw_labels] + if config.label_map: + labels = [config.label_map.get(v, v) for v in labels] + logger.info("Loaded %d samples from %s (split=%s)", len(sentences), config.dataset_name, split) + return sentences, labels + + +def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: + """Load the served ``config.n`` samples + label strings from ``config.dataset_split``.""" + return _load_split(config, config.dataset_split, config.n) + + +def load_reference_corpus(config: ServeConfig) -> tuple[list[str], list[str]] | None: + """Load the similar-examples reference corpus (the model's training split). + + Returns ``(texts, labels)`` from ``config.reference_split`` for the "Similar Examples" panel, + or ``None`` when disabled (``n_reference <= 0``). This is the pool neighbours are retrieved from, + so it is the model's *training* set rather than the served (test) split. + """ + if config.n_reference <= 0: + return None + return _load_split(config, config.reference_split, config.n_reference) + + +def _load_tokenizer(model_name: str): + """Load the tokenizer, preferring a fast one but falling back to the slow implementation. + + A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights + use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with + ``use_fast=False``; a slow tokenizer still works (the LIG backend degrades to its scheme-aware + string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a + custom ``trust_remote_code`` architecture — which the ``HFClassifierModel`` adapter does not support; + surface that as a clear error rather than a deep tokenizer stack trace. + """ + try: + return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=True) + except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint + logger.warning( + "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", + model_name, + type(fast_err).__name__, + ) + try: + return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False) + except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message + raise RuntimeError( + f"Could not load a tokenizer for {model_name!r} in fast or slow mode. This is usually a " + "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " + "which the HFClassifierModel adapter does not support. Use a standard " + "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." + ) from slow_err + + +def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: + """The generic default: any standard ``AutoModelForSequenceClassification`` checkpoint. + + Covers every certified encoder architecture (BERT/DistilBERT/RoBERTa/XLM-R/DeBERTa-v1, and MiniLM + packaged as a sequence classifier). Label names come from the model's own ``config.id2label``, so a + new ``--model-name`` brings its own classes with no extra flags. + """ + tokenizer = _load_tokenizer(config.model_name) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) + logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) + return HFClassifierModel(classifier, tokenizer) + + +# Reconcile the shhossain checkpoint's own class label spelling with the dair-ai/emotion dataset it is +# demoed against ("sad" -> "sadness"); every other class name already matches. +_SHHOSSAIN_LABEL_ALIASES = {"sad": "sadness"} + + +# ── Custom-model builders ────────────────────────────────────────────────────────────────────────── +# Standard checkpoints load generically above. A model whose classification *head* is custom (its layer +# structure / weight keys / IO contract are not a standard, so they can't be introspected from a name +# alone) needs a per-model builder. Register one function per such checkpoint below; the generic path is +# never touched. Each builder takes (config, device) and returns any TextModel adapter. + + +def _build_shhossain_sentiment_model(config: ServeConfig, device: str) -> SentenceTransformerModel: + """Example builder — ``shhossain/all-MiniLM-L6-v2-sentiment-classifier`` (ST body + MLP head). + + This checkpoint is a custom ``trust_remote_code`` model: a standard ``all-MiniLM-L6-v2`` body plus a + trained ``fc1 -> relu -> fc2 -> relu -> out -> softmax`` MLP head, and it ships no HF tokenizer — so the + generic HF path cannot load it. Everything model-specific (the head's weight-key convention, dropping + its final softmax to recover logits, mean pooling) is contained *here*, not in the core loader. + """ + huggingface_hub = __import__("huggingface_hub") + sentence_transformers = __import__("sentence_transformers") + safetensors_torch = __import__("safetensors.torch", fromlist=["load_file"]) + + with open(huggingface_hub.hf_hub_download(config.model_name, "config.json")) as fh: + cfg = json.load(fh) + class_map = cfg["class_map"] + labels = [class_map[str(i)] for i in range(len(class_map))] + # This checkpoint's class_map spells one emotion as "sad" while its natural demo dataset + # (dair-ai/emotion) names the same class "sadness"; align them so predicted-vs-ground-truth + # comparisons in the webapp line up. (Same intent as the --label-map flag, but done here since + # the mismatch is intrinsic to this model's own labels, not a dataset choice.) + labels = [_SHHOSSAIN_LABEL_ALIASES.get(label, label) for label in labels] + + st_model = sentence_transformers.SentenceTransformer(cfg["embedding_model"], device=device) + weights = safetensors_torch.load_file(huggingface_hub.hf_hub_download(config.model_name, "model.safetensors")) + # Rebuild the trained MLP as a *logits* head (sizes from weight shapes; the checkpoint's final softmax + # is dropped — SentenceTransformerModel.predict softmaxes and the LIG backend needs logit space). + head = nn.Sequential( + nn.Linear(weights["fc1.weight"].shape[1], weights["fc1.weight"].shape[0]), + nn.ReLU(), + nn.Linear(weights["fc2.weight"].shape[1], weights["fc2.weight"].shape[0]), + nn.ReLU(), + nn.Linear(weights["out.weight"].shape[1], weights["out.weight"].shape[0]), + ) + for idx, name in ((0, "fc1"), (2, "fc2"), (4, "out")): + head[idx].load_state_dict({"weight": weights[f"{name}.weight"], "bias": weights[f"{name}.bias"]}) + return SentenceTransformerModel(st_model, head.to(device).eval(), label_names=labels, pool="mean") + + +# ``--model-name`` -> a custom builder for a checkpoint whose head can't be introspected from its name. +# Anything not listed takes the generic ``_load_hf_classifier`` path. To serve a new custom model, add a +# ``build(config, device) -> TextModel`` function above and one entry here — the core loader is untouched. +# See the module docstring's "Serving a custom model" section for the per-adapter recipe. +MODEL_BUILDERS: dict[str, Callable[[ServeConfig, str], TextModel]] = { + "shhossain/all-MiniLM-L6-v2-sentiment-classifier": _build_shhossain_sentiment_model, +} + + +def load_model(config: ServeConfig) -> TextModel: + """Load the model on the best available device: a registered custom builder, else the generic HF path. + + Standard ``AutoModelForSequenceClassification`` checkpoints of any certified architecture load with no + configuration; a checkpoint whose head is custom (so it can't be introspected from its name) is built + by its entry in :data:`MODEL_BUILDERS`. + """ + if torch.cuda.is_available(): + device = "cuda" + logger.info("CUDA available — using GPU: %s", torch.cuda.get_device_name(0)) + else: + device = "cpu" + logger.info("No CUDA device found — using CPU") + + builder = MODEL_BUILDERS.get(config.model_name) + return builder(config, device) if builder is not None else _load_hf_classifier(config, device) + + +def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend | None: + """Return the attribution backend selected by ``--attribution``. + + Returns ``None`` for ``"shap"`` so ``NlpExplainer`` builds its default ``NlpShapBackend`` (which + needs the model's ``shap_callable`` wiring); ``"lig"`` returns an explicit ``NlpCaptumLigBackend``. + """ + if config.attribution == "lig": + # LIG runs one integration per class per sample — show a progress bar over the batch. + return NlpCaptumLigBackend(model, label_names=model.label_names, show_progress=True) + return None + + +def build_reducer() -> pacmap.PaCMAP: + """The dimensionality reducer for the scatter — a *demo* choice, not a library one. + + ``NlpExplainer.compute_projection`` owns everything that must stay consistent (which space the + texts are embedded in, and the caching of both the vectors and the coordinates) and takes the + reducer as an argument, because which manifold method suits your data is a modelling decision. + PaCMAP is a good default for text clusters; swap in UMAP, t-SNE, or drop the argument entirely for + the built-in PCA. + """ + return pacmap.PaCMAP(n_components=2, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) + + +def main() -> None: + """Load config + data + model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" + config = parse_args() + + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") + # Quiet noisy third-party INFO logs (HF Hub freshness checks, download chatter) so the + # device/cache messages below stand out. + for noisy in ("httpx", "httpcore", "urllib3", "filelock", "datasets"): + logging.getLogger(noisy).setLevel(logging.WARNING) + + sentences, y_true = load_data(config) + dataset_dir = _dataset_cache_dir(config) # projection lives here (shared across attribution backends) + compile_dir = _compile_cache_dir(config) # contributions live here (one subdir per attribution backend) + + model = load_model(config) + if config.embedding_space is not None: + # One setting drives both the scatter projection and similar-example retrieval. Assigning it + # here (rather than threading it through every MODEL_BUILDERS entry) is safe because the + # setter validates the name against the backbone — a typo raises now, not mid-forward-pass. + model.embedding_space = config.embedding_space + logger.info("Embedding space: %s (scatter projection + similar examples)", model.resolve_space()) + + # Similar-examples reference corpus (the training split) + a cache dir for its embedding bank, + # keyed by corpus/space/model so it is embedded once and reloaded on later runs. + reference_corpus = load_reference_corpus(config) + similar_cache_dir = dataset_dir / "similar" + + # No cf_generator is passed: NlpExplainer auto-discovers every built-in compatible with the model + # (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's method dropdown. + # reference_corpus enables the "Similar Examples" panel (skipped when --n-reference 0). + xpl = NlpExplainer( + model, + label_names=model.label_names, + backend=build_backend(config, model), + reference_corpus=reference_corpus, + reference_cache_dir=similar_cache_dir, + ) + if config.recompute: + # Drop this text set's cached contributions so the steps below recompute + overwrite. The + # explainer owns the key (it covers the model and backend), so it also owns the deletion — + # only *this* model+backend's entry goes, other backends' caches are left intact. The + # embeddings + projection are dropped by compute_projection(recompute=True) below. + logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) + xpl.clear_cache(sentences, compile_dir) + + # Both steps load from cache if one exists for these exact texts, otherwise they compute and write + # it. So the first run is slow; later runs only pay the model load. + compile_cache = xpl.cache_path(sentences, compile_dir) + if compile_cache.exists(): + logger.info("Contributions cache hit (%s) — loading %s", config.attribution, compile_cache) + else: + logger.info( + "Contributions cache miss (%s) — computing for %d texts (this is the slow part)", + config.attribution, + len(sentences), + ) + xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) + projected = xpl.compute_projection(reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute) + + if xpl.can_find_similar(): + # Build (and cache) the reference activation bank now so the first in-app lookup is instant. + logger.info("Warming similar-examples bank over %d reference texts…", len(reference_corpus[0])) + xpl.find_similar(sentences[0], top_k=1) + + logger.info( + "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s", + config.attribution, + ",".join(name for name, _ in xpl.available_cf_generators()) or "none", + xpl.can_edit(), + xpl.can_counterfactual(), + xpl.can_find_similar(), + ) + logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) + xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) -snapshot = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent / "explainer_snapshot.pkl" -if not snapshot.exists(): - raise FileNotFoundError(f"Snapshot not found: {snapshot}\nRun demo/test_webapp_nlp.py first to generate it.") -xpl, scatter_xy = NlpExplainer.from_snapshot(snapshot) -xpl.run_app(port=8050, debug=False, host="0.0.0.0", scatter_xy=scatter_xy) # noqa: S104 +if __name__ == "__main__": + main() diff --git a/demo/serve_nlp_ext.py b/demo/serve_nlp_ext.py deleted file mode 100644 index c6eb51c3..00000000 --- a/demo/serve_nlp_ext.py +++ /dev/null @@ -1,514 +0,0 @@ -"""Serve the NLP explainer **with the What-if Lab** (live model, cached compute). - -Unlike ``serve_nlp.py``, the What-if Lab needs a live model in memory — editing a -sample, re-predicting, and generating counterfactuals all call the real model, so a -model-free snapshot (``from_snapshot``) makes the lab self-disable. There is therefore -no way to serve what-if without loading the model. - -What *can* be avoided on every restart is recomputing the expensive stuff around the -model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): - -* **SHAP contributions + predictions** — via the built-in ``compile(cache_dir=...)``. -* **The text embeddings and the 2-D projection** — via ``compute_projection(cache_dir=...)``. The - library owns the embedding + caching (so the scatter and the similar-example neighbours are always - in the same space); this script only supplies the reducer, PaCMAP. - -The caching is automatic: each run **loads** the cache if one exists for these exact -texts, otherwise it **computes and writes** it. So the first run is slow; every run after -pays only the model load (a few seconds on CPU). Pass ``--recompute`` to force a fresh -compute (e.g. after changing the explainer). Iterating on the webapp itself? Don't -restart at all — keep this in a REPL and re-call ``run_app`` after reloading the webapp -modules; the model stays resident. - -The model runs on CPU when no GPU is present, so this serves fine on a plain box. - -Model and dataset are configurable (see ``ServeConfig``): swap ``--model-name`` for any HF -sequence-classification checkpoint (its label names are read from the model's own config) and -``--dataset-name`` for any HF ``datasets`` classification dataset (point ``--text-column`` / -``--label-column`` at the right fields if they aren't ``text`` / ``label``). Cache keys cover the model -and the attribution backend as well as the texts, so switching ``--model-name`` can never reload another -model's contributions; the per-model/per-dataset subdirectories under ``--cache-dir`` below are for -human legibility (and easy pruning), not for correctness. - -Dataset and model label strings aren't guaranteed to match even when their class order does (e.g. -a dataset's ``ClassLabel`` names ``"neg"``/``"pos"`` vs. a model's ``config.id2label`` values -``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the -model's spelling so predicted vs. ground-truth comparisons in the webapp line up. - -The sentence-highlight attribution method is selectable (a Captum-backed alternative to the default, -see the ``[nlp]`` extra): - -* ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` - (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can - flip between them freely without ``--recompute``. - -Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token -substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a -**method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. - -The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: -``<model>/<dataset>__<split>/`` holds the (backend-independent) embedding-store artifacts — the -vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or -``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.pkl`` contributions. - -Serving a *custom* model (external-head checkpoints) ----------------------------------------------------- -Any standard ``AutoModelForSequenceClassification`` checkpoint loads automatically from ``--model-name`` -alone (``load_model`` builds an ``HFClassifierModel``; label names come from ``config.id2label``). That -covers every certified encoder architecture — BERT, DistilBERT, RoBERTa, XLM-R, DeBERTa-v1, and a MiniLM -packaged as a sequence classifier. - -A model whose classification **head is custom** cannot be introspected from its name — its layer -structure, weight-key convention, and input contract are non-standard (a sentence-transformer body + a -separate MLP head, a SetFit head, a hand-rolled ``nn.Module``, …). These are served through the -``MODEL_BUILDERS`` registry: a ``{--model-name: builder}`` map consulted *before* the generic path. To add -one: - -1. Write a builder ``def build_x(config: ServeConfig, device: str) -> TextModel:`` that assembles the - right adapter and returns it. Put **all** model-specific code inside it — the core ``load_model`` never - changes. Pick the adapter by shape: - - * **sentence-transformer body + head** → :class:`~shapash.model.SentenceTransformerModel` (pass the - ``sentence_transformers`` model + a *logits* head ``nn.Module``; drop any final ``softmax`` the head - applies — ``predict`` softmaxes and the LIG backend needs logit space; set ``pool=`` to the body's - pooling mode, usually ``"mean"``). - * **raw encoder body + head** (head not fused into an ``AutoModelForSequenceClassification``) → - :class:`~shapash.model.TorchClassifierModel` (pass ``body, head, tokenizer``; ``pool="cls"`` for a - CLS-pooled head, ``"mean"`` otherwise). - * **standard classifier that merely needs ``trust_remote_code``** → just call - ``HFClassifierModel(AutoModelForSequenceClassification.from_pretrained(name, trust_remote_code=True), - tokenizer)``. - -2. Register it: ``MODEL_BUILDERS["org/your-model"] = build_x``. Nothing else changes — caching, the - What-if Lab, both attribution backends, counterfactuals, and similar-examples all work through the - capability interface regardless of adapter. - -``_build_shhossain_sentiment_model`` (registered for -``shhossain/all-MiniLM-L6-v2-sentiment-classifier``) is the worked example: a MiniLM ST body + a trained -``fc1 -> relu -> fc2 -> relu -> out`` MLP head, ``trust_remote_code`` with no HF tokenizer, so the generic -path can't touch it — but ``SentenceTransformerModel`` serves it faithfully (it reproduces the source -model's predictions exactly). - -Usage ------ - python demo/serve_nlp_ext.py [--n 100] [--cache-dir demo/nlp_ext_cache] [--port 8051] - python demo/serve_nlp_ext.py --recompute # ignore the cache and recompute - python demo/serve_nlp_ext.py --attribution lig # Captum LayerIntegratedGradients highlights - python demo/serve_nlp_ext.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ - --dataset-name sst2 --dataset-split validation --text-column sentence - python demo/serve_nlp_ext.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ - --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' - # A registered custom (external-head) model — served via MODEL_BUILDERS, no other flags needed: - python demo/serve_nlp_ext.py --model-name shhossain/all-MiniLM-L6-v2-sentiment-classifier - -First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). A ``SentenceTransformerModel`` -custom builder additionally needs the ``sentence-transformers`` package. -""" - -from __future__ import annotations - -import argparse -import json -import logging -from collections.abc import Callable -from dataclasses import dataclass, field -from pathlib import Path - -import datasets -import pacmap -import torch -import transformers -from torch import nn - -from shapash.backend import NlpCaptumLigBackend -from shapash.explainer.nlp_explainer import NlpExplainer -from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel - -_HERE = Path(__file__).parent - -logger = logging.getLogger("serve_nlp_ext") - - -@dataclass -class ServeConfig: - """Everything needed to load data + model and serve the What-if webapp. - - A single object (rather than argparse's ``Namespace``) so the loading helpers below - stay reusable from a REPL too — see the module docstring on iterating without restarting. - """ - - model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" - dataset_name: str = "dair-ai/emotion" - dataset_split: str = "test" - text_column: str = "text" - label_column: str = "label" - label_map: dict[str, str] = field(default_factory=dict) - n: int = 500 - attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) - # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own - # training split) and how many rows of it to bank. Set ``n_reference=0`` to disable the "Similar - # Examples" panel. - reference_split: str = "train" - n_reference: int = 2000 - # Representation space the scatter projects *and* neighbours are ranked in — one setting for both, - # so they can never disagree. ``None`` keeps whatever the model was built with ("decision"). - embedding_space: str | None = None - cache_dir: Path = _HERE / "nlp_ext_cache" - port: int = 8051 - host: str = "0.0.0.0" # noqa: S104 - recompute: bool = False - - -# Short ``--attribution`` choice → the attribution backend's registered ``.name`` (used as the cache -# subdirectory *and* to pick the backend in ``build_backend``). -_ATTRIBUTION_BACKENDS = {"shap": "nlp_shap", "lig": "nlp_captum_lig"} - - -def _dataset_slug(config: ServeConfig) -> str: - """Filesystem-safe ``<dataset>__<split>`` tag for the cache hierarchy.""" - return f"{config.dataset_name}__{config.dataset_split}".replace("/", "__") - - -def _dataset_cache_dir(config: ServeConfig) -> Path: - """Return ``cache_dir/<model>/<dataset>__<split>`` — the level the projection is cached at. - - The compile and projection caches both key on the model (and, for contributions, the backend), so - these path levels are organisational rather than load-bearing: they keep a ``--cache-dir`` readable - and prunable per model/dataset. The PaCMAP projection embeds the texts with ``model.embed`` and - never touches the attribution backend, so every ``--attribution`` choice shares one projection cache - at *this* level (below it, contributions split per backend). - """ - return config.cache_dir / config.model_name.replace("/", "__") / _dataset_slug(config) - - -def _compile_cache_dir(config: ServeConfig) -> Path: - """Return ``<dataset-dir>/<backend>`` — the level contributions are cached at. - - Contributions *do* depend on the attribution backend, so SHAP and LIG get separate subdirectories. - Switching ``--attribution`` then reads/writes a different ``<hash>.pkl`` instead of silently - reusing the other method's cached highlights — no ``--recompute`` needed to swap methods. - """ - return _dataset_cache_dir(config) / _ATTRIBUTION_BACKENDS[config.attribution] - - -def parse_args(argv: list[str] | None = None) -> ServeConfig: - """Parse CLI args into a ``ServeConfig``, defaulting every flag from the dataclass.""" - defaults = ServeConfig() - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--model-name", default=defaults.model_name, help="HF model id (label names are read from its config)." - ) - parser.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") - parser.add_argument("--dataset-split", default=defaults.dataset_split) - parser.add_argument("--text-column", default=defaults.text_column, help="Column holding the raw text.") - parser.add_argument("--label-column", default=defaults.label_column, help="Column holding the ground-truth label.") - parser.add_argument( - "--label-map", - type=json.loads, - default=defaults.label_map, - help=( - "JSON object renaming dataset label strings to match the model's own label names, e.g. " - '\'{"neg": "NEGATIVE", "pos": "POSITIVE"}\'. Needed when the dataset and model disagree on ' - "label spelling/casing even though their class order matches." - ), - ) - parser.add_argument("--n", type=int, default=defaults.n, help="Number of samples to load.") - parser.add_argument( - "--attribution", - choices=list(_ATTRIBUTION_BACKENDS), - default=defaults.attribution, - help=( - "Sentence-highlight attribution method: 'shap' (KernelSHAP, the default) or 'lig' " - "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." - ), - ) - parser.add_argument( - "--reference-split", - default=defaults.reference_split, - help="Dataset split used as the similar-examples reference corpus (the model's training split).", - ) - parser.add_argument( - "--n-reference", - type=int, - default=defaults.n_reference, - help="Reference rows to bank for 'Similar Examples' (0 disables the panel).", - ) - parser.add_argument( - "--embedding-space", - default=defaults.embedding_space, - help=( - "Representation space for the 2-D scatter and similar-example retrieval: 'decision' " - "(input to the final classification linear, class-discriminative), 'pooled' (the pooled " - "last hidden state, semantic), or any backbone submodule name. Default: the model's own." - ), - ) - parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) - parser.add_argument("--port", type=int, default=defaults.port) - parser.add_argument("--host", default=defaults.host) - parser.add_argument( - "--recompute", - action="store_true", - help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", - ) - args = parser.parse_args(argv) - return ServeConfig(**vars(args)) - - -def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: - """Load the first ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. - - Ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when - present (the usual shape for HF classification datasets); otherwise the raw values are - stringified as-is. ``config.label_map`` then renames them to match the model's label - names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model - ``"NEGATIVE"``) despite sharing the same class order. - """ - dataset = datasets.load_dataset(config.dataset_name, split=split) - sentences = dataset[config.text_column][:n] - raw_labels = dataset[config.label_column][:n] - names = getattr(dataset.features.get(config.label_column), "names", None) - labels = [names[i] for i in raw_labels] if names is not None else [str(v) for v in raw_labels] - if config.label_map: - labels = [config.label_map.get(v, v) for v in labels] - logger.info("Loaded %d samples from %s (split=%s)", len(sentences), config.dataset_name, split) - return sentences, labels - - -def load_data(config: ServeConfig) -> tuple[list[str], list[str]]: - """Load the served ``config.n`` samples + label strings from ``config.dataset_split``.""" - return _load_split(config, config.dataset_split, config.n) - - -def load_reference_corpus(config: ServeConfig) -> tuple[list[str], list[str]] | None: - """Load the similar-examples reference corpus (the model's training split). - - Returns ``(texts, labels)`` from ``config.reference_split`` for the "Similar Examples" panel, - or ``None`` when disabled (``n_reference <= 0``). This is the pool neighbours are retrieved from, - so it is the model's *training* set rather than the served (test) split. - """ - if config.n_reference <= 0: - return None - return _load_split(config, config.reference_split, config.n_reference) - - -def _load_tokenizer(model_name: str): - """Load the tokenizer, preferring a fast one but falling back to the slow implementation. - - A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights - use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with - ``use_fast=False``; a slow tokenizer still works (the LIG backend degrades to its scheme-aware - string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a - custom ``trust_remote_code`` architecture — which the ``HFClassifierModel`` adapter does not support; - surface that as a clear error rather than a deep tokenizer stack trace. - """ - try: - return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=True) - except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint - logger.warning( - "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", - model_name, - type(fast_err).__name__, - ) - try: - return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False) - except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message - raise RuntimeError( - f"Could not load a tokenizer for {model_name!r} in fast or slow mode. This is usually a " - "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " - "which the HFClassifierModel adapter does not support. Use a standard " - "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." - ) from slow_err - - -def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: - """The generic default: any standard ``AutoModelForSequenceClassification`` checkpoint. - - Covers every certified encoder architecture (BERT/DistilBERT/RoBERTa/XLM-R/DeBERTa-v1, and MiniLM - packaged as a sequence classifier). Label names come from the model's own ``config.id2label``, so a - new ``--model-name`` brings its own classes with no extra flags. - """ - tokenizer = _load_tokenizer(config.model_name) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) - logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) - return HFClassifierModel(classifier, tokenizer) - - -# Reconcile the shhossain checkpoint's own class label spelling with the dair-ai/emotion dataset it is -# demoed against ("sad" -> "sadness"); every other class name already matches. -_SHHOSSAIN_LABEL_ALIASES = {"sad": "sadness"} - - -# ── Custom-model builders ────────────────────────────────────────────────────────────────────────── -# Standard checkpoints load generically above. A model whose classification *head* is custom (its layer -# structure / weight keys / IO contract are not a standard, so they can't be introspected from a name -# alone) needs a per-model builder. Register one function per such checkpoint below; the generic path is -# never touched. Each builder takes (config, device) and returns any TextModel adapter. - - -def _build_shhossain_sentiment_model(config: ServeConfig, device: str) -> SentenceTransformerModel: - """Example builder — ``shhossain/all-MiniLM-L6-v2-sentiment-classifier`` (ST body + MLP head). - - This checkpoint is a custom ``trust_remote_code`` model: a standard ``all-MiniLM-L6-v2`` body plus a - trained ``fc1 -> relu -> fc2 -> relu -> out -> softmax`` MLP head, and it ships no HF tokenizer — so the - generic HF path cannot load it. Everything model-specific (the head's weight-key convention, dropping - its final softmax to recover logits, mean pooling) is contained *here*, not in the core loader. - """ - huggingface_hub = __import__("huggingface_hub") - sentence_transformers = __import__("sentence_transformers") - safetensors_torch = __import__("safetensors.torch", fromlist=["load_file"]) - - with open(huggingface_hub.hf_hub_download(config.model_name, "config.json")) as fh: - cfg = json.load(fh) - class_map = cfg["class_map"] - labels = [class_map[str(i)] for i in range(len(class_map))] - # This checkpoint's class_map spells one emotion as "sad" while its natural demo dataset - # (dair-ai/emotion) names the same class "sadness"; align them so predicted-vs-ground-truth - # comparisons in the webapp line up. (Same intent as the --label-map flag, but done here since - # the mismatch is intrinsic to this model's own labels, not a dataset choice.) - labels = [_SHHOSSAIN_LABEL_ALIASES.get(label, label) for label in labels] - - st_model = sentence_transformers.SentenceTransformer(cfg["embedding_model"], device=device) - weights = safetensors_torch.load_file(huggingface_hub.hf_hub_download(config.model_name, "model.safetensors")) - # Rebuild the trained MLP as a *logits* head (sizes from weight shapes; the checkpoint's final softmax - # is dropped — SentenceTransformerModel.predict softmaxes and the LIG backend needs logit space). - head = nn.Sequential( - nn.Linear(weights["fc1.weight"].shape[1], weights["fc1.weight"].shape[0]), - nn.ReLU(), - nn.Linear(weights["fc2.weight"].shape[1], weights["fc2.weight"].shape[0]), - nn.ReLU(), - nn.Linear(weights["out.weight"].shape[1], weights["out.weight"].shape[0]), - ) - for idx, name in ((0, "fc1"), (2, "fc2"), (4, "out")): - head[idx].load_state_dict({"weight": weights[f"{name}.weight"], "bias": weights[f"{name}.bias"]}) - return SentenceTransformerModel(st_model, head.to(device).eval(), label_names=labels, pool="mean") - - -# ``--model-name`` -> a custom builder for a checkpoint whose head can't be introspected from its name. -# Anything not listed takes the generic ``_load_hf_classifier`` path. To serve a new custom model, add a -# ``build(config, device) -> TextModel`` function above and one entry here — the core loader is untouched. -# See the module docstring's "Serving a custom model" section for the per-adapter recipe. -MODEL_BUILDERS: dict[str, Callable[[ServeConfig, str], TextModel]] = { - "shhossain/all-MiniLM-L6-v2-sentiment-classifier": _build_shhossain_sentiment_model, -} - - -def load_model(config: ServeConfig) -> TextModel: - """Load the model on the best available device: a registered custom builder, else the generic HF path. - - Standard ``AutoModelForSequenceClassification`` checkpoints of any certified architecture load with no - configuration; a checkpoint whose head is custom (so it can't be introspected from its name) is built - by its entry in :data:`MODEL_BUILDERS`. - """ - if torch.cuda.is_available(): - device = "cuda" - logger.info("CUDA available — using GPU: %s", torch.cuda.get_device_name(0)) - else: - device = "cpu" - logger.info("No CUDA device found — using CPU") - - builder = MODEL_BUILDERS.get(config.model_name) - return builder(config, device) if builder is not None else _load_hf_classifier(config, device) - - -def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend | None: - """Return the attribution backend selected by ``--attribution``. - - Returns ``None`` for ``"shap"`` so ``NlpExplainer`` builds its default ``NlpShapBackend`` (which - needs the model's ``shap_callable`` wiring); ``"lig"`` returns an explicit ``NlpCaptumLigBackend``. - """ - if config.attribution == "lig": - # LIG runs one integration per class per sample — show a progress bar over the batch. - return NlpCaptumLigBackend(model, label_names=model.label_names, show_progress=True) - return None - - -def build_reducer() -> pacmap.PaCMAP: - """The dimensionality reducer for the scatter — a *demo* choice, not a library one. - - ``NlpExplainer.compute_projection`` owns everything that must stay consistent (which space the - texts are embedded in, and the caching of both the vectors and the coordinates) and takes the - reducer as an argument, because which manifold method suits your data is a modelling decision. - PaCMAP is a good default for text clusters; swap in UMAP, t-SNE, or drop the argument entirely for - the built-in PCA. - """ - return pacmap.PaCMAP(n_components=2, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) - - -def main() -> None: - """Load config + data + model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" - config = parse_args() - - logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") - # Quiet noisy third-party INFO logs (HF Hub freshness checks, download chatter) so the - # device/cache messages below stand out. - for noisy in ("httpx", "httpcore", "urllib3", "filelock", "datasets"): - logging.getLogger(noisy).setLevel(logging.WARNING) - - sentences, y_true = load_data(config) - dataset_dir = _dataset_cache_dir(config) # projection lives here (shared across attribution backends) - compile_dir = _compile_cache_dir(config) # contributions live here (one subdir per attribution backend) - - model = load_model(config) - if config.embedding_space is not None: - # One setting drives both the scatter projection and similar-example retrieval. Assigning it - # here (rather than threading it through every MODEL_BUILDERS entry) is safe because the - # setter validates the name against the backbone — a typo raises now, not mid-forward-pass. - model.embedding_space = config.embedding_space - logger.info("Embedding space: %s (scatter projection + similar examples)", model.resolve_space()) - - # Similar-examples reference corpus (the training split) + a cache dir for its embedding bank, - # keyed by corpus/space/model so it is embedded once and reloaded on later runs. - reference_corpus = load_reference_corpus(config) - similar_cache_dir = dataset_dir / "similar" - - # No cf_generator is passed: NlpExplainer auto-discovers every built-in compatible with the model - # (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's method dropdown. - # reference_corpus enables the "Similar Examples" panel (skipped when --n-reference 0). - xpl = NlpExplainer( - model, - label_names=model.label_names, - backend=build_backend(config, model), - reference_corpus=reference_corpus, - reference_cache_dir=similar_cache_dir, - ) - if config.recompute: - # Drop this text set's cached contributions so the steps below recompute + overwrite. The - # explainer owns the key (it covers the model and backend), so it also owns the deletion — - # only *this* model+backend's entry goes, other backends' caches are left intact. The - # embeddings + projection are dropped by compute_projection(recompute=True) below. - logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) - xpl.clear_cache(sentences, compile_dir) - - # Both steps load from cache if one exists for these exact texts, otherwise they compute and write - # it. So the first run is slow; later runs only pay the model load. - compile_cache = xpl.cache_path(sentences, compile_dir) - if compile_cache.exists(): - logger.info("Contributions cache hit (%s) — loading %s", config.attribution, compile_cache) - else: - logger.info( - "Contributions cache miss (%s) — computing for %d texts (this is the slow part)", - config.attribution, - len(sentences), - ) - xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) - projected = xpl.compute_projection(reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute) - - if xpl.can_find_similar(): - # Build (and cache) the reference activation bank now so the first in-app lookup is instant. - logger.info("Warming similar-examples bank over %d reference texts…", len(reference_corpus[0])) - xpl.find_similar(sentences[0], top_k=1) - - logger.info( - "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s", - config.attribution, - ",".join(name for name, _ in xpl.available_cf_generators()) or "none", - xpl.can_edit(), - xpl.can_counterfactual(), - xpl.can_find_similar(), - ) - logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) - xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) - - -if __name__ == "__main__": - main() diff --git a/demo/test_webapp_nlp.py b/demo/test_webapp_nlp.py deleted file mode 100644 index bd28977f..00000000 --- a/demo/test_webapp_nlp.py +++ /dev/null @@ -1,129 +0,0 @@ -from pathlib import Path - -import datasets -import numpy as np -import pacmap -import pandas as pd -import torch -import transformers - -from shapash.explainer.nlp_explainer import NlpExplainer - -_HERE = Path(__file__).parent - - -def project_embeddings(embeddings, n_components=2): - """Project high-dimensional embeddings to a lower-dimensional space using PaCMAP. - - Parameters - ---------- - embeddings : np.ndarray, shape (n_samples, hidden_dim) - Dense vector representations of the input texts. - n_components : int, optional - Number of output dimensions. Default is 2 (for 2-D scatter plots). - - Returns - ------- - np.ndarray, shape (n_samples, n_components) - Low-dimensional projection initialised with PCA. - """ - projector = pacmap.PaCMAP( - n_components=n_components, - n_neighbors=5, - MN_ratio=0.5, - FP_ratio=2.0, - # random_state=1 - ) - - return projector.fit_transform(embeddings, init="pca") - - -def get_embeddings(texts, classifier, tokenizer, batch_size=32): - """Extract mean-pooled last-layer hidden states from a HuggingFace classifier. - - Runs inference in batches without gradient computation. Tokens are pooled - using the attention mask so padding tokens do not contribute to the mean. - - Parameters - ---------- - texts : list of str - Raw input texts to encode. - classifier : transformers.PreTrainedModel - A HuggingFace model that accepts ``output_hidden_states=True``. - The model's ``device`` attribute is used to move batches automatically. - tokenizer : transformers.PreTrainedTokenizer - Tokenizer matching the classifier; applied with padding and truncation. - batch_size : int, optional - Number of texts processed per forward pass. Default is 32. - - Returns - ------- - np.ndarray, shape (n_samples, hidden_dim) - One embedding vector per input text. - """ - device = classifier.device - - all_embeddings = [] - for i in range(0, len(texts), batch_size): - batch = tokenizer(texts[i : i + batch_size], padding=True, truncation=True, return_tensors="pt") - batch = {k: v.to(device) for k, v in batch.items()} # fix 1: move to GPU - with torch.no_grad(): - out = classifier(**batch, output_hidden_states=True) # fix 2: request hidden states - mask = batch["attention_mask"].unsqueeze(-1) - hidden = out.hidden_states[-1] # last layer, shape [batch, seq_len, hidden_dim] - emb = (hidden * mask).sum(1) / mask.sum(1) - all_embeddings.append(emb.cpu().numpy()) - return np.vstack(all_embeddings) - - -if __name__ == "__main__": - N = 100 - - # load the emotion dataset - dataset = datasets.load_dataset("dair-ai/emotion", split="train") - data = pd.DataFrame({"text": dataset["text"], "emotion": dataset["label"]}) - - sentences = dataset["text"][:N] - labels = dataset["label"][:N] - - # load the model and tokenizer - tokenizer = transformers.AutoTokenizer.from_pretrained( - "bhadresh-savani/distilbert-base-uncased-emotion", - # "nateraw/bert-base-uncased-emotion", - use_fast=True, - ) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained( - "bhadresh-savani/distilbert-base-uncased-emotion", - # "nateraw/bert-base-uncased-emotion" - ).cuda() - - # build a pipeline object to do predictions - pred = transformers.pipeline( - "text-classification", - model=classifier, - tokenizer=tokenizer, - batch_size=4, - top_k=None, - ) - - embeddings = get_embeddings(sentences, classifier, tokenizer) - projected = project_embeddings(embeddings, n_components=2) - - label_names = ["sadness", "joy", "love", "anger", "fear", "surprise"] - - # from shapash.backend.nlp_lime_backend import NlpLimeBackend - # lime_backend = NlpLimeBackend( - # pred, - # label_names=label_names, - # explainer_compute_args={ - # "num_samples": 300, # default is 5000 — this is the main lever - # "num_features": 10, # words explained per sample per label - # } - # ) - # xpl = NlpExplainer(pred, label_names=label_names, backend=lime_backend) - - xpl = NlpExplainer(pred, label_names=label_names) - - xpl.compile(sentences, y_true=[label_names[i] for i in labels]) - xpl.save_snapshot(_HERE / "explainer_snapshot.pkl", scatter_xy=projected) - xpl.run_app(port=8050, debug=False, scatter_xy=projected) From c45184009fc8f2d7704ca9e8d946c48e1b6d1ae7 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 17:52:02 +0200 Subject: [PATCH 087/144] build: add protobuf dependency needed for deberta models --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 07758d20..01997bc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ nlp = [ "datasets>=5.0.0", "sentence-transformers>=5.6.0", "sentencepiece>=0.2.2", + "protobuf>=7.35.1", "captum>=0.9.0", "pacmap>=0.9.1", "dash-ag-grid>=35.2.0" From 540d453fa1182a5e87d7d9de293f09880721b17b Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 21 Jul 2026 17:54:24 +0200 Subject: [PATCH 088/144] fix(demo): add --lig-batch-size to avoid CUDA OOM on LIG attribution --- demo/serve_nlp.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 878fab4a..eaeaa56a 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -40,6 +40,9 @@ * ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can flip between them freely without ``--recompute``. +* ``--lig-batch-size`` — only used by ``lig``: Captum's ``internal_batch_size``, chunking each sample's + 50-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or + ``1``) if you hit a CUDA out-of-memory error, especially on memory-hungry architectures like DeBERTa. Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a @@ -144,6 +147,11 @@ class ServeConfig: label_map: dict[str, str] = field(default_factory=dict) n: int = 500 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) + # Captum's LayerIntegratedGradients expands one sample into ``n_steps`` (50) scaled copies and runs + # them through the model as a single batch unless told otherwise — on a memory-hungry architecture + # (e.g. DeBERTa-v2/v3's disentangled attention) that batch of 50 can blow past a small GPU's memory. + # ``internal_batch_size`` makes Captum chunk that batch instead; lower it further if you still OOM. + lig_batch_size: int = 8 # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own # training split) and how many rows of it to bank. Set ``n_reference=0`` to disable the "Similar # Examples" panel. @@ -221,6 +229,16 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: "(Captum LayerIntegratedGradients). Cached separately, so switching needs no --recompute." ), ) + parser.add_argument( + "--lig-batch-size", + type=int, + default=defaults.lig_batch_size, + help=( + "Captum internal_batch_size for --attribution lig: chunks each sample's n_steps=50 " + "integration batch instead of running it through the model in one shot. Lower this " + "(e.g. 2 or 1) if LIG hits a CUDA out-of-memory error." + ), + ) parser.add_argument( "--reference-split", default=defaults.reference_split, @@ -417,7 +435,14 @@ def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend """ if config.attribution == "lig": # LIG runs one integration per class per sample — show a progress bar over the batch. - return NlpCaptumLigBackend(model, label_names=model.label_names, show_progress=True) + # internal_batch_size chunks each sample's n_steps=50 scaled-copies batch so it doesn't OOM + # memory-hungry architectures (e.g. DeBERTa) on a small GPU — see ServeConfig.lig_batch_size. + return NlpCaptumLigBackend( + model, + label_names=model.label_names, + explainer_compute_args={"internal_batch_size": config.lig_batch_size}, + show_progress=True, + ) return None From 7e3f407341aab042d3715d27511996ff14c1ac92 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 23 Jul 2026 17:22:17 +0200 Subject: [PATCH 089/144] fix(model): guard against a truncation no-op aborting the CUDA context --- demo/serve_nlp.py | 264 ++++++++++++++++-- shapash/model/encoder.py | 84 +++++- shapash/model/hf.py | 10 + .../model/test_encoder_length_guard.py | 149 ++++++++++ 4 files changed, 490 insertions(+), 17 deletions(-) create mode 100644 tests/unit_tests/model/test_encoder_length_guard.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index eaeaa56a..83760099 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -32,7 +32,9 @@ Dataset and model label strings aren't guaranteed to match even when their class order does (e.g. a dataset's ``ClassLabel`` names ``"neg"``/``"pos"`` vs. a model's ``config.id2label`` values ``"NEGATIVE"``/``"POSITIVE"``) — pass ``--label-map`` to rename ground-truth labels onto the -model's spelling so predicted vs. ground-truth comparisons in the webapp line up. +model's spelling so predicted vs. ground-truth comparisons in the webapp line up. See "Serving a custom +dataset" below for label spaces ``--label-map`` can't fix (bucketing, dropping rows), and ``--seed`` for +how rows are sampled. The sentence-highlight attribution method is selectable (a Captum-backed alternative to the default, see the ``[nlp]`` extra): @@ -44,6 +46,9 @@ 50-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or ``1``) if you hit a CUDA out-of-memory error, especially on memory-hungry architectures like DeBERTa. +``lig`` is also the method most sensitive to *truncation* actually being configured — see +``_hf_classifier_model`` for why every HF classifier here resolves an explicit ``max_length``. + Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a **method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. @@ -80,6 +85,14 @@ * **standard classifier that merely needs ``trust_remote_code``** → just call ``HFClassifierModel(AutoModelForSequenceClassification.from_pretrained(name, trust_remote_code=True), tokenizer)``. + * **standard classifier whose config's labels can't be trusted** (missing ``id2label``, or a + ``label2id`` that turns out backwards from the model's real behaviour) → same call as the generic + path, but pass ``label_names=[...]`` explicitly instead of leaving it to be read from + ``config.id2label``. Verify the ordering against a real labeled sample (dozens+, not a handful) — + ``almanach/camembertv2-base-cls`` was first "fixed" from a 10-example hand check that (wrongly) + looked decisive; a proper sanity check (``demo/check_camembertv2_amazon_fr.py``) later showed + *neither* ordering is trustworthy on that checkpoint at all. A handful of examples can't tell a + genuinely swapped label from a model that just doesn't work. 2. Register it: ``MODEL_BUILDERS["org/your-model"] = build_x``. Nothing else changes — caching, the What-if Lab, both attribution backends, counterfactuals, and similar-examples all work through the @@ -89,7 +102,42 @@ ``shhossain/all-MiniLM-L6-v2-sentiment-classifier``) is the worked example: a MiniLM ST body + a trained ``fc1 -> relu -> fc2 -> relu -> out`` MLP head, ``trust_remote_code`` with no HF tokenizer, so the generic path can't touch it — but ``SentenceTransformerModel`` serves it faithfully (it reproduces the source -model's predictions exactly). +model's predictions exactly). ``_build_camembertv2_cls_model`` (registered for +``almanach/camembertv2-base-cls``) is the worked example of the label-names-can't-be-trusted case: an +architecturally plain ``RobertaForSequenceClassification`` whose checkpoint config omits ``id2label``. +**Its config's labels can't be trusted, but neither can the checkpoint's own predictions** — see the +builder's docstring and ``demo/check_camembertv2_amazon_fr.py`` for a sanity check showing neither label +ordering beats a trivial majority-class baseline on gold in-domain data. It stays registered only as a +worked example of this builder pattern, not as a model whose predictions should be trusted. + +Serving a *custom* dataset (label logic that isn't a 1:1 rename) +------------------------------------------------------------------ +Any standard HF classification dataset loads automatically from ``--dataset-name`` alone: point +``--text-column``/``--label-column`` at the right fields if they aren't ``text``/``label``, and (rows are +shuffled with ``--seed`` before ``--n``/``--n-reference`` are taken, so this is representative even when +the dataset's on-disk order groups rows by label). Ground-truth strings come from the dataset's own +``ClassLabel`` names when present, else the raw values stringified; ``--label-map`` renames them 1:1 onto +the model's own spelling when the two disagree on casing/wording despite sharing the same class order +(e.g. dataset ``"neg"`` vs. model ``"NEGATIVE"``). + +A dataset whose ground truth needs **more** than a rename — bucketing several raw values into one class, +dropping rows with no mapping, or any other dataset-specific logic — is served through the +``DATASET_LOADERS`` registry: a ``{--dataset-name: loader}`` map consulted *before* the generic path +(the exact ``MODEL_BUILDERS`` pattern above, applied to data instead of the model). To add one: + +1. Write a loader ``def load_x(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]`` + that returns ``(texts, label_strings)``. Put **all** dataset-specific code inside it, including which + raw columns it reads — a bucketing loader's logic is tied to that dataset's exact label semantics, so + there is no generic ``--text-column``/``--label-column`` to thread through; the core ``_load_split`` + never changes. +2. Register it: ``DATASET_LOADERS["org/your-dataset"] = load_x``. Nothing else changes — caching, the + webapp, and the reference corpus all call ``_load_split`` regardless of which path served it. + +``_load_amazon_reviews_multi_binary`` (registered for ``mteb/amazon_reviews_multi``) is the worked +example: its ``label`` is a 5-star rating minus one (0-4), not the binary sentiment space a binary +classifier like ``almanach/camembertv2-base-cls`` predicts into, so it buckets rating > 3 -> ``"positive"``, +rating < 3 -> ``"negative"``, and drops the neutral 3-star (a ground-truth convention only — see +``demo/check_camembertv2_amazon_fr.py`` for whether this specific model's predictions are trustworthy). Usage ----- @@ -98,6 +146,10 @@ python demo/serve_nlp.py --attribution lig # Captum LayerIntegratedGradients highlights python demo/serve_nlp.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ --dataset-name sst2 --dataset-split validation --text-column sentence + # A registered custom model against a registered dataset loader (bucketed 5-star -> binary + # sentiment ground truth; --text-column/--label-column don't apply here, see DATASET_LOADERS): + python demo/serve_nlp.py --model-name almanach/camembertv2-base-cls \\ + --dataset-name mteb/amazon_reviews_multi --dataset-config fr --dataset-split test python demo/serve_nlp.py --model-name lvwerra/distilbert-imdb --dataset-name stanfordnlp/imdb \\ --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' # A registered custom (external-head) model — served via MODEL_BUILDERS, no other flags needed: @@ -141,11 +193,16 @@ class ServeConfig: model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" dataset_name: str = "dair-ai/emotion" + dataset_config: str | None = None # HF dataset config/subset (e.g. a language); None = dataset's default dataset_split: str = "test" text_column: str = "text" label_column: str = "label" label_map: dict[str, str] = field(default_factory=dict) n: int = 500 + # Rows are shuffled with this seed before taking --n / --n-reference (see _load_split) — several + # HF dataset repos (e.g. mteb/amazon_reviews_multi) store rows grouped by label, so an unshuffled + # "first n" slice can be entirely one class. + seed: int = 0 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) # Captum's LayerIntegratedGradients expands one sample into ``n_steps`` (50) scaled copies and runs # them through the model as a single batch unless told otherwise — on a memory-hungry architecture @@ -172,8 +229,14 @@ class ServeConfig: def _dataset_slug(config: ServeConfig) -> str: - """Filesystem-safe ``<dataset>__<split>`` tag for the cache hierarchy.""" - return f"{config.dataset_name}__{config.dataset_split}".replace("/", "__") + """Filesystem-safe ``<dataset>__[<config>__]<split>`` tag for the cache hierarchy. + + The config segment is only included when ``--dataset-config`` is actually set, so existing cache + directories built before that flag existed (``<dataset>__<split>``, no config with no HF dataset + config) still resolve to the same path. + """ + parts = [config.dataset_name, *([config.dataset_config] if config.dataset_config else []), config.dataset_split] + return "__".join(parts).replace("/", "__") def _dataset_cache_dir(config: ServeConfig) -> Path: @@ -206,6 +269,12 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: "--model-name", default=defaults.model_name, help="HF model id (label names are read from its config)." ) parser.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") + parser.add_argument( + "--dataset-config", + default=defaults.dataset_config, + help="HF dataset config/subset (e.g. a language) — needed when the dataset has more than one, " + "such as mteb/amazon_reviews_multi (pass 'fr', 'en', ...).", + ) parser.add_argument("--dataset-split", default=defaults.dataset_split) parser.add_argument("--text-column", default=defaults.text_column, help="Column holding the raw text.") parser.add_argument("--label-column", default=defaults.label_column, help="Column holding the ground-truth label.") @@ -220,6 +289,13 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: ), ) parser.add_argument("--n", type=int, default=defaults.n, help="Number of samples to load.") + parser.add_argument( + "--seed", + type=int, + default=defaults.seed, + help="Shuffle seed applied before taking --n / --n-reference rows, so sampling is representative " + "rather than whatever the dataset's on-disk row order puts first.", + ) parser.add_argument( "--attribution", choices=list(_ATTRIBUTION_BACKENDS), @@ -271,16 +347,91 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: return ServeConfig(**vars(args)) -def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: - """Load the first ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. +def _load_hf_dataset(name: str, dataset_config: str | None, split: str) -> datasets.Dataset: + """Load one split of a HF dataset, falling back to raw per-split JSONL for legacy-script repos. - Ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when - present (the usual shape for HF classification datasets); otherwise the raw values are - stringified as-is. ``config.label_map`` then renames them to match the model's label - names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model - ``"NEGATIVE"``) despite sharing the same class order. + Some older dataset repos (e.g. ``mteb/amazon_reviews_multi``) still ship a ``<name>.py`` loading + script, which ``datasets>=4.0`` refuses to execute (``RuntimeError: Dataset scripts are no longer + supported``). Their raw ``<config>/<split>.jsonl`` files are usually still present in the repo, so on + exactly that error this retries against them directly via the generic ``json`` builder, bypassing the + broken script. Any other failure (missing dataset, bad split name, ...) is not masked. """ - dataset = datasets.load_dataset(config.dataset_name, split=split) + try: + return datasets.load_dataset(name, dataset_config, split=split) + except RuntimeError as exc: + if "Dataset scripts are no longer supported" not in str(exc) or dataset_config is None: + raise + logger.warning("%s ships a legacy loading script; falling back to raw %s/%s.jsonl", name, dataset_config, split) + return datasets.load_dataset( + "json", data_files=f"hf://datasets/{name}/{dataset_config}/{split}.jsonl", split="train" + ) + + +def _load_amazon_reviews_multi_binary(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: + """``mteb/amazon_reviews_multi`` — bucket its 5-star ``label`` (0-4) onto binary sentiment. + + The raw ``label`` is a star rating minus one (0 = 1-star, ..., 4 = 5-star) — not the binary + positive/negative space a binary sentiment model (e.g. ``almanach/camembertv2-base-cls``) predicts + into, and not something ``--label-map`` can fix: that flag only renames labels 1:1, it can't collapse + five classes into two or drop one. Its ``label_text`` column doesn't help either — it's just + ``str(label)`` ("0".."4"), not a description. + + Standard star-rating bucketing: rating > 3 -> ``"positive"``, rating < 3 -> ``"negative"``; the + 3-star (rating == 3) review is neutral, has no binary ground truth, and is dropped — so this can + return fewer than ``n`` rows. This is a *ground-truth* convention only — it says nothing about + whether ``almanach/camembertv2-base-cls`` (or any other model served against this data) predicts + accurately; see ``demo/check_camembertv2_amazon_fr.py`` for that question answered against a + cleaner, in-domain gold set. + + Also fixes the "all one label" symptom on this dataset specifically: the raw file is sorted by + ``label`` in blocks of 1000 identical values, so an unshuffled ``[:n]`` slice for any ``n`` <= 1000 + is entirely 1-star. Shuffling with ``config.seed`` before selecting fixes that regardless of ``n``. + """ + dataset = _load_hf_dataset(config.dataset_name, config.dataset_config, split) + dataset = dataset.shuffle(seed=config.seed).select(range(min(n, len(dataset)))) + texts, labels = [], [] + for row in dataset: + rating = row["label"] + 1 + if rating == 3: + continue + texts.append(row["text"]) + labels.append("positive" if rating > 3 else "negative") + return texts, labels + + +# ``--dataset-name`` -> a custom loader for a dataset whose ground-truth labels need dataset-specific +# logic (bucketing, dropping rows, ...) that the generic ``--label-column``/``--label-map`` path can't +# express. Anything not listed takes the generic path in ``_load_split``. Mirrors ``MODEL_BUILDERS``: put +# all dataset-specific code inside the loader and register it here — ``_load_split`` itself never changes. +DATASET_LOADERS: dict[str, Callable[[ServeConfig, str, int], tuple[list[str], list[str]]]] = { + "mteb/amazon_reviews_multi": _load_amazon_reviews_multi_binary, +} + + +def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: + """Load ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. + + A dataset registered in :data:`DATASET_LOADERS` is delegated to its loader entirely (see there for + why ``mteb/amazon_reviews_multi`` needs one). Otherwise, the generic path applies: rows are shuffled + with ``config.seed`` before taking the first ``n`` — so samples are representative even when the + dataset's on-disk order groups rows by label — and ground-truth label strings come from the + dataset's own ``ClassLabel`` feature names when present (the usual shape for HF classification + datasets), otherwise the raw values are stringified as-is. ``config.label_map`` then renames them to + match the model's label names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. + model ``"NEGATIVE"``) despite sharing the same class order. + """ + loader = DATASET_LOADERS.get(config.dataset_name) + if loader is not None: + sentences, labels = loader(config, split, n) + logger.info( + "Loaded %d samples from %s (split=%s) via its registered dataset loader", + len(sentences), + config.dataset_name, + split, + ) + return sentences, labels + + dataset = _load_hf_dataset(config.dataset_name, config.dataset_config, split).shuffle(seed=config.seed) sentences = dataset[config.text_column][:n] raw_labels = dataset[config.label_column][:n] names = getattr(dataset.features.get(config.label_column), "names", None) @@ -337,6 +488,53 @@ def _load_tokenizer(model_name: str): ) from slow_err +# transformers' sentinel for "tokenizer_config.json never set model_max_length" — effectively means +# "no limit", not a real length. Passing it straight through makes truncation=True a no-op. +_UNSET_TOKENIZER_MAX_LENGTH = 100_000 + + +def _safe_max_length(tokenizer, default: int = 512) -> int | None: + """The tokenizer's own truncation length, unless it's the "unset" sentinel — then ``default``. + + A checkpoint whose tokenizer config never set ``model_max_length`` reports ``~1e30`` instead, so + ``truncation=True`` with no explicit ``max_length`` silently does nothing and a long text tokenizes + **unbounded**. That is not merely slow or memory-hungry: once a sequence is longer than the model's + ``max_position_embeddings``, its position ids index past the position/token-type buffers, and the + resulting out-of-bounds gather aborts the CUDA context outright (``device-side assert triggered``). + The abort is asynchronous, so it surfaces at whatever unrelated line next synchronises — see + ``_hf_classifier_model``, which is why every HF classifier here is built through one place. + """ + model_max_length = getattr(tokenizer, "model_max_length", None) + if model_max_length is None or model_max_length > _UNSET_TOKENIZER_MAX_LENGTH: + return default + return model_max_length + + +def _hf_classifier_model(config: ServeConfig, device: str, **kwargs) -> HFClassifierModel: + """Build an ``HFClassifierModel`` with truncation always resolved — the single construction point. + + Every path that produces an ``HFClassifierModel`` (the generic loader *and* every custom builder in + :data:`MODEL_BUILDERS`) goes through here, so ``max_length`` can never be left unset by an author who + only meant to override something else, such as ``label_names``. That is not hypothetical: this helper + exists because a custom builder once constructed the model directly and inherited + ``max_length=None``, which silently disabled truncation for a tokenizer reporting the "unset" + sentinel and made any input longer than the model's position buffer abort the CUDA context. + + ``kwargs`` are forwarded to :class:`~shapash.model.HFClassifierModel` (e.g. ``label_names=``). + """ + tokenizer = _load_tokenizer(config.model_name) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) + logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) + max_length = _safe_max_length(tokenizer) + if max_length != tokenizer.model_max_length: + logger.info( + "%s's tokenizer reports no configured model_max_length — truncating at %d tokens instead.", + config.model_name, + max_length, + ) + return HFClassifierModel(classifier, tokenizer, max_length=max_length, **kwargs) + + def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: """The generic default: any standard ``AutoModelForSequenceClassification`` checkpoint. @@ -344,10 +542,7 @@ def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: packaged as a sequence classifier). Label names come from the model's own ``config.id2label``, so a new ``--model-name`` brings its own classes with no extra flags. """ - tokenizer = _load_tokenizer(config.model_name) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) - logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) - return HFClassifierModel(classifier, tokenizer) + return _hf_classifier_model(config, device) # Reconcile the shhossain checkpoint's own class label spelling with the dair-ai/emotion dataset it is @@ -400,12 +595,49 @@ def _build_shhossain_sentiment_model(config: ServeConfig, device: str) -> Senten return SentenceTransformerModel(st_model, head.to(device).eval(), label_names=labels, pool="mean") +def _build_camembertv2_cls_model(config: ServeConfig, device: str) -> HFClassifierModel: + """``almanach/camembertv2-base-cls`` — standard checkpoint, but don't trust its predictions either. + + Architecturally this is a plain ``RobertaForSequenceClassification`` (CamemBERT v2) that would load + fine through the generic path — the config problem is only ``label_names``. Its ``config.json`` ships + ``label2id`` but no ``id2label``; when only one of the pair is present, transformers regenerates the + missing side (and *overwrites* the other) via a ``num_labels`` setter, so ``config.id2label`` resolves + to meaningless ``{0: "LABEL_0", 1: "LABEL_1"}`` at load time — the generic ``_load_hf_classifier`` + path (which reads ``label_names`` from exactly that) would show those in the webapp. + + Passing ``label_names`` explicitly only fixes the *display* problem, not the model. A first attempt + at picking the right order used a 10-example hand-written probe (documented order 2/10, flipped + 8/10) and shipped the flip — that was wrong to trust: ``demo/check_camembertv2_amazon_fr.py`` scores + both orderings against 388 gold-labeled reviews from this checkpoint's own FLUE-CLS fine-tuning + domain and finds **neither beats the trivial majority-class baseline**, and the model is confidently + wrong about as often as confidently right (mean top-class probability ~0.79 regardless of + correctness). The 10-example probe was noise, not signal — a small hand check can't tell a genuinely + swapped label from a model that just doesn't work. ``label_names`` below keeps the checkpoint's own + documented convention (no evidence favours the flip over it); treat every prediction from this model + as illustrative, not accurate — this builder exists as a worked example of the + label-names-can't-be-trusted registration pattern, not as a working sentiment classifier. + + Only ``label_names`` is overridden — everything else, truncation included, comes from + :func:`_hf_classifier_model`. This checkpoint's tokenizer reports no ``model_max_length`` and its + ``max_position_embeddings`` is 1025, so building the model directly (as this once did) left + truncation disabled and let any review over 1024 tokens abort the CUDA context. + """ + logger.warning( + "%s: this checkpoint's predictions are near chance-level and confidently wrong on gold in-domain " + "data (neither label ordering beats the majority-class baseline) — see " + "demo/check_camembertv2_amazon_fr.py. Treat its output in this demo as illustrative only.", + config.model_name, + ) + return _hf_classifier_model(config, device, label_names=["negative", "positive"]) + + # ``--model-name`` -> a custom builder for a checkpoint whose head can't be introspected from its name. # Anything not listed takes the generic ``_load_hf_classifier`` path. To serve a new custom model, add a # ``build(config, device) -> TextModel`` function above and one entry here — the core loader is untouched. # See the module docstring's "Serving a custom model" section for the per-adapter recipe. MODEL_BUILDERS: dict[str, Callable[[ServeConfig, str], TextModel]] = { "shhossain/all-MiniLM-L6-v2-sentiment-classifier": _build_shhossain_sentiment_model, + "almanach/camembertv2-base-cls": _build_camembertv2_cls_model, } diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index d2a67d0a..04a10382 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -70,6 +70,41 @@ _HEAD_ATTRS = ("head", "classifier", "score") +def _encoded_length(encoding: Any) -> int | None: + """Return the sequence length of a tokenizer ``encoding``, or ``None`` if it cannot be read. + + Handles every shape :meth:`EncoderClassifierModel._tokenize` can produce: a ``return_tensors="pt"`` + ``(batch, seq)`` tensor, a batched list of id lists, and a single un-tensorised list of ids. + """ + ids = encoding["input_ids"] + shape = getattr(ids, "shape", None) + if shape is not None: + return int(shape[-1]) + if not len(ids): + return 0 + if isinstance(ids[0], (list, tuple)): + return max(len(row) for row in ids) + return len(ids) + + +def _position_capacity(backbone: Any) -> int | None: + """Return how many *absolute* position ids ``backbone`` can index, or ``None`` when not knowable. + + ``None`` means "do not check" and is returned generously — this feeds a hard error, so a missed + check (status quo) is far cheaper than a wrong one. Two cases yield ``None``: + + * no ``config.max_position_embeddings`` — a rotary/ALiBi model has no such table; + * ``config.position_biased_input`` is ``False`` — DeBERTa v1/v2/v3 add no absolute position + embeddings at all, so their ``max_position_embeddings`` is not an indexing bound and comparing + against it would reject sequences the model handles fine. + """ + config = getattr(backbone, "config", None) + if config is None or getattr(config, "position_biased_input", True) is False: + return None + capacity = getattr(config, "max_position_embeddings", None) + return capacity if isinstance(capacity, int) and not isinstance(capacity, bool) and capacity > 0 else None + + def _pool_hidden(hidden: Any, attention_mask: Any, pool: Any) -> Any: """Reduce a token-level ``(batch, seq, hidden)`` tensor to ``(batch, hidden)``, mask-aware. @@ -262,12 +297,59 @@ def _tokenize(self, texts: str | list[str], **overrides: Any): just tidiness: :meth:`word_alignment` returns positions into the same subword axis as :meth:`encode`, so if the two truncated differently the attribution highlights would silently misalign on any text long enough to be cut. + + Also the single place the result is checked against what the backbone can actually index — see + :meth:`_check_encoded_length`. """ kwargs: dict[str, Any] = {"truncation": True} if self.max_length is not None: kwargs["max_length"] = self.max_length kwargs.update(overrides) - return self.tokenizer(texts, **kwargs) + encoding = self.tokenizer(texts, **kwargs) + self._check_encoded_length(encoding) + return encoding + + def _check_encoded_length(self, encoding: Any) -> None: + """Raise if ``encoding`` is longer than the backbone can index absolute positions for. + + This is a *diagnostic*, not a limiter: it never truncates and never fires for input the model + could have handled. It exists because the failure it pre-empts is close to undebuggable. A + sequence longer than ``max_position_embeddings`` makes the model's own embedding layer gather + position ids past the end of its buffer, which trips a device-side assert; on CUDA that abort is + *asynchronous*, so it surfaces at whatever unrelated line next synchronises — with a traceback + pointing at innocent code, often thousands of samples into a batch job. Failing here instead + names the real cause and the fix. + + The usual trigger is a checkpoint whose tokenizer config never set ``model_max_length``: + ``transformers`` then reports a ~1e30 sentinel, ``truncation=True`` becomes a silent no-op, and a + long text tokenizes unbounded. Passing an explicit ``max_length`` is the fix. + + Deliberately conservative — it compares against ``max_position_embeddings`` itself, so it cannot + reject a sequence the model would have accepted. The cost is that it under-catches RoBERTa-style + models by a couple of tokens: those offset position ids by ``pad_token_id + 1`` (which is why + roberta-base pairs a 514-wide buffer with a real 512-token limit), so a sequence in that narrow + band still reaches the original CUDA abort. Catching most cases with no false positives is worth + more here than catching all of them at the risk of rejecting valid input. + + Raises + ------ + ValueError + If the encoded sequence exceeds the backbone's absolute-position capacity. + """ + capacity = _position_capacity(self.backbone) + if capacity is None: + return + length = _encoded_length(encoding) + if length is None or length <= capacity: + return + raise ValueError( + f"Text tokenized to {length} tokens, but {type(self.backbone).__name__} can only index " + f"{capacity} absolute positions — running it would abort the CUDA context with a " + f"device-side assert far from this call. Pass an explicit max_length (<= {capacity}) to " + f"{type(self).__name__}. This usually means the tokenizer reports no model_max_length " + f"(currently {getattr(self.tokenizer, 'model_max_length', None)!r}), which silently makes " + "truncation=True a no-op." + ) def _encode_one(self, text: str): """Return ``(input_ids, attention_mask)`` batch-size-1 tensors on the backbone device.""" diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 13bae230..4203f2ee 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -136,6 +136,14 @@ class HFClassifierModel(EncoderClassifierModel): How a token-level output is reduced to one vector per text in :meth:`embed`. Default ``"mean"``. Note this affects the *analysis* representation only — an ``AutoModelForSequenceClassification`` does its own pooling internally, so :meth:`predict` is unaffected. + max_length : int or None, optional + Truncation length applied to every tokenizer call — see + :class:`~shapash.model.encoder.EncoderClassifierModel`. When ``None`` (default) the tokenizer's + own ``model_max_length`` applies; note some checkpoints ship a tokenizer whose config never sets + this, so ``transformers`` reports its "unset" sentinel (~1e30) instead of a real limit — with + ``max_length=None`` that makes truncation a no-op, so a gradient-based attribution method (e.g. + ``NlpCaptumLigBackend``) can exhaust GPU memory on a long enough text. Pass an explicit + ``max_length`` for such checkpoints. """ def __init__( @@ -148,6 +156,7 @@ def __init__( *, embedding_space: str = _DECISION_SPACE, pool: Any = "mean", + max_length: int | None = None, ) -> None: if label_names is None: id2label = getattr(getattr(classifier, "config", None), "id2label", None) @@ -161,6 +170,7 @@ def __init__( device=device, pool=pool, embedding_space=embedding_space, + max_length=max_length, ) # ``classifier`` is the backbone; keep the historical alias for introspection and tests. self.classifier = classifier diff --git a/tests/unit_tests/model/test_encoder_length_guard.py b/tests/unit_tests/model/test_encoder_length_guard.py new file mode 100644 index 00000000..43ff49ea --- /dev/null +++ b/tests/unit_tests/model/test_encoder_length_guard.py @@ -0,0 +1,149 @@ +"""Unit tests for the encoded-length diagnostic in ``EncoderClassifierModel``. + +The guard pre-empts an out-of-bounds position-id gather (which on CUDA surfaces as an asynchronous +device-side assert, far from its cause). It is pure Python — no torch, transformers or GPU — so it is +exercised here with fake backbones/tokenizers via ``object.__new__``, skipping the real ``__init__``. + +The central property under test is that it **never false-positives**: it must not reject a sequence +the model would have accepted, since it raises rather than truncating. +""" + +import unittest +from types import SimpleNamespace + +from shapash.model.encoder import EncoderClassifierModel, _encoded_length, _position_capacity + + +class FakeTokenizer: + """Returns a canned encoding; records the kwargs it was called with.""" + + def __init__(self, encoding, model_max_length=512): + self.encoding = encoding + self.model_max_length = model_max_length + self.calls = [] + + def __call__(self, texts, **kwargs): + self.calls.append(kwargs) + return self.encoding + + +def _backbone(max_position_embeddings=512, **config_extra): + config = SimpleNamespace(max_position_embeddings=max_position_embeddings, **config_extra) + return SimpleNamespace(config=config) + + +def _model(backbone, tokenizer, max_length=None): + """Build the adapter without running ``__init__`` (which needs a real torch backbone).""" + model = object.__new__(EncoderClassifierModel) + model.backbone = backbone + model.tokenizer = tokenizer + model.max_length = max_length + return model + + +class TestEncodedLength(unittest.TestCase): + """``_encoded_length`` must read every shape ``_tokenize`` can return.""" + + def test_reads_tensor_like_shape(self): + self.assertEqual(_encoded_length({"input_ids": SimpleNamespace(shape=(1, 1092))}), 1092) + + def test_reads_batched_tensor_shape(self): + self.assertEqual(_encoded_length({"input_ids": SimpleNamespace(shape=(8, 256))}), 256) + + def test_reads_single_id_list(self): + self.assertEqual(_encoded_length({"input_ids": [5, 6, 7]}), 3) + + def test_reads_batched_id_lists_as_the_longest_row(self): + self.assertEqual(_encoded_length({"input_ids": [[1, 2], [1, 2, 3, 4]]}), 4) + + def test_empty_encoding_is_zero(self): + self.assertEqual(_encoded_length({"input_ids": []}), 0) + + +class TestPositionCapacity(unittest.TestCase): + """``_position_capacity`` returns ``None`` (meaning "do not check") whenever it is unsure.""" + + def test_reads_max_position_embeddings(self): + self.assertEqual(_position_capacity(_backbone(1025)), 1025) + + def test_none_when_config_absent(self): + self.assertIsNone(_position_capacity(SimpleNamespace())) + + def test_none_when_max_position_embeddings_absent(self): + # A rotary/ALiBi model has no absolute position table. + self.assertIsNone(_position_capacity(SimpleNamespace(config=SimpleNamespace()))) + + def test_none_for_deberta_style_relative_positions(self): + # DeBERTa v1/v2/v3 ship position_biased_input=False and add no absolute position embeddings, + # so their max_position_embeddings is not an indexing bound — checking it would false-positive. + self.assertIsNone(_position_capacity(_backbone(512, position_biased_input=False))) + + def test_applies_when_position_biased_input_is_true(self): + self.assertEqual(_position_capacity(_backbone(1025, position_biased_input=True)), 1025) + + def test_none_for_nonsense_values(self): + for bad in (0, -5, None, "512", True): + self.assertIsNone(_position_capacity(_backbone(bad)), f"capacity={bad!r}") + + +class TestTokenizeLengthGuard(unittest.TestCase): + def test_raises_when_sequence_exceeds_capacity(self): + # The camembertv2 case: 1092 tokens against a 1025-wide position buffer. + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 1092))}, model_max_length=10**30) + model = _model(_backbone(1025), tokenizer) + with self.assertRaises(ValueError) as ctx: + model._tokenize("a very long review") + message = str(ctx.exception) + self.assertIn("1092", message) + self.assertIn("1025", message) + self.assertIn("max_length", message) # tells the caller what to do + + def test_allows_sequence_exactly_at_capacity(self): + # Boundary: capacity positions are indices 0..capacity-1 for an arange scheme, so a sequence of + # exactly `capacity` tokens fits. Rejecting it would be a false positive. + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 512))}) + model = _model(_backbone(512), tokenizer) + model._tokenize("text") # must not raise + + def test_allows_short_sequence(self): + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 7))}) + model = _model(_backbone(512), tokenizer) + model._tokenize("text") + + def test_does_not_fire_for_deberta_style_model(self): + # Relative-position model: a longer-than-mpe sequence must pass through untouched. + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 900))}) + model = _model(_backbone(512, position_biased_input=False), tokenizer) + model._tokenize("text") + + def test_does_not_fire_when_capacity_unknown(self): + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 9999))}) + model = _model(SimpleNamespace(config=SimpleNamespace()), tokenizer) + model._tokenize("text") + + def test_explicit_max_length_is_still_forwarded_to_the_tokenizer(self): + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 512))}) + model = _model(_backbone(1025), tokenizer, max_length=512) + model._tokenize("text") + self.assertEqual(tokenizer.calls[-1]["max_length"], 512) + self.assertTrue(tokenizer.calls[-1]["truncation"]) + + def test_no_max_length_sends_truncation_only(self): + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 8))}) + model = _model(_backbone(512), tokenizer) + model._tokenize("text") + self.assertNotIn("max_length", tokenizer.calls[-1]) + + def test_message_surfaces_the_sentinel_model_max_length(self): + # The actionable root cause: a tokenizer reporting the ~1e30 "unset" sentinel makes + # truncation=True a silent no-op, which is how an unbounded sequence gets here at all. + sentinel = 1000000000000000019884624838656 + tokenizer = FakeTokenizer({"input_ids": SimpleNamespace(shape=(1, 2000))}, model_max_length=sentinel) + model = _model(_backbone(1025), tokenizer) + with self.assertRaises(ValueError) as ctx: + model._tokenize("text") + self.assertIn("model_max_length", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() From f8081b60f3f8cb14e1f786ebcb7e347d55ebce5c Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 27 Jul 2026 10:51:39 +0200 Subject: [PATCH 090/144] feat(model): serve local checkpoints via HFClassifierModel.from_pretrained Add a from_pretrained classmethod that loads a hub id or a local save_pretrained directory, resolves the tokenizer (base-model fallback when the dir ships none), and auto-picks a safe truncation length. serve_nlp.py now delegates local model/dataset serving to it. --- demo/README.md | 8 +- demo/serve_nlp.py | 380 ++++++++++++++---- shapash/model/hf.py | 220 +++++++++- .../model/test_hf_from_pretrained.py | 268 ++++++++++++ 4 files changed, 787 insertions(+), 89 deletions(-) create mode 100644 tests/unit_tests/model/test_hf_from_pretrained.py diff --git a/demo/README.md b/demo/README.md index 7f65de5e..0e3698e8 100644 --- a/demo/README.md +++ b/demo/README.md @@ -16,7 +16,13 @@ they never run SHAP themselves, they only copy in a cache built by running the s below). Model, dataset, split, columns, sample count, and a ground-truth label-renaming map are all configurable via `ServeConfig` in that file — see its module docstring for `--model-name`/`--dataset-name`/`--dataset-split`/`--text-column`/`--label-column`/`--label-map`/`--n` -and worked examples (e.g. swapping in an IMDB sentiment model/dataset). `Dockerfile` itself stays +and worked examples (e.g. swapping in an IMDB sentiment model/dataset). **Nothing has to come from the +HuggingFace hub**: `--model-name /path/to/checkpoint` serves a local `save_pretrained` directory (its +tokenizer is resolved from the checkpoint, falling back to the base model in its `config.json`; pass +`--tokenizer-name` for a customised one, and `--label-names` when the config has no class names), and +`--dataset-path` serves local `<split>.parquet` / `<split>.pkl` dataframes instead of `--dataset-name`. +A private model + private data gets the full webapp — highlights, What-if Lab, counterfactuals, +similar examples — like any hub model. `Dockerfile` itself stays hardcoded to the emotion example on purpose (it's meant to run out of the box for anyone cloning the repo) — its build-time warm-up (`RUN python -c "..."`) downloads exactly that model/dataset/split, so it always matches `ServeConfig`'s defaults. Serving a different model/dataset combo in Docker means diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 83760099..44052e41 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -46,8 +46,9 @@ 50-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or ``1``) if you hit a CUDA out-of-memory error, especially on memory-hungry architectures like DeBERTa. -``lig`` is also the method most sensitive to *truncation* actually being configured — see -``_hf_classifier_model`` for why every HF classifier here resolves an explicit ``max_length``. +``lig`` is also the method most sensitive to *truncation* actually being configured — every HF +classifier here is built through ``HFClassifierModel.from_pretrained(..., max_length="auto")``, which +resolves a safe length even when the checkpoint's tokenizer reports none. Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a @@ -58,12 +59,46 @@ vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or ``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.pkl`` contributions. +Serving a *private* model and dataset (both local, nothing on the hub) +---------------------------------------------------------------------- +Neither ``--model-name`` nor the data has to come from the HuggingFace hub, and a local run gets the +**full** capability surface — sentence highlights (both methods), the What-if Lab, both counterfactual +generators, similar examples — exactly like a hub model, because everything is served through the same +``HFClassifierModel`` adapter: + +* ``--model-name /path/to/checkpoint`` — a directory saved with ``model.save_pretrained(...)``. Weights + and ``config.json`` are enough; the *tokenizer* need not be there. When the directory ships no + tokenizer files, ``HFClassifierModel.from_pretrained`` falls back to the base model recorded in the + checkpoint's own ``config.json`` (``_name_or_path``) — right for any fine-tune that kept its base + tokenizer, which is the usual case. Pass ``--tokenizer-name`` (an id or a path) for a **customised** + tokenizer: nothing can detect that automatically. +* ``--label-names NEGATIVE,POSITIVE`` — class names in output-column order, when the checkpoint's config + carries none. ``trainer.model.save_pretrained(...)`` records ``id2label`` only if training set it, so a + fine-tune often reports ``LABEL_0``/``LABEL_1``; this replaces them without needing a custom builder. + It is applied in ``_hf_classifier_model`` — the one place every *HF classifier* is built — so it also + overrides the hard-coded names of a ``MODEL_BUILDERS`` builder that goes through there + (``_build_camembertv2_cls_model``). A builder assembling some other adapter directly + (``_build_shhossain_sentiment_model``, a ``SentenceTransformerModel``) owns its label names outright + and this flag does not reach it. Get the *order* right — verify against real labeled rows, not a + handful (see ``--label-map`` below and the camembert cautionary tale further down). +* ``--dataset-path /path/to/dir`` — local dataframes instead of ``--dataset-name`` (the two are mutually + exclusive; the CLI rejects both). A **directory** holds one file per split, ``<split>.parquet`` or + ``<split>.pkl``, so ``--dataset-split``/``--reference-split`` pick files exactly as they pick hub + splits. A **single file** works too, but only with ``--n-reference 0``: one file cannot also be the + reference corpus, and silently retrieving "similar examples" from the served rows would be a lie. + ``--text-column``/``--label-column`` name the columns; ``--label-map`` reconciles the dataframe's label + spelling with the model's, as for hub datasets. (``.pkl`` is read with ``pandas.read_pickle``, which + executes arbitrary code on load — point it only at files you produced yourself.) + +Caching works the same way: a local model or dataset is keyed in the cache hierarchy by its directory +name plus a digest of its absolute path, so two checkpoints both named ``output/`` never share a cache. + Serving a *custom* model (external-head checkpoints) ---------------------------------------------------- Any standard ``AutoModelForSequenceClassification`` checkpoint loads automatically from ``--model-name`` -alone (``load_model`` builds an ``HFClassifierModel``; label names come from ``config.id2label``). That -covers every certified encoder architecture — BERT, DistilBERT, RoBERTa, XLM-R, DeBERTa-v1, and a MiniLM -packaged as a sequence classifier. +alone — hub id or local path (``load_model`` builds an ``HFClassifierModel``; label names come from +``config.id2label``). That covers every certified encoder architecture — BERT, DistilBERT, RoBERTa, +XLM-R, DeBERTa-v1, and a MiniLM packaged as a sequence classifier. A model whose classification **head is custom** cannot be introspected from its name — its layer structure, weight-key convention, and input contract are non-standard (a sentence-transformer body + a @@ -154,6 +189,11 @@ --label-column label --label-map '{"neg": "NEGATIVE", "pos": "POSITIVE"}' # A registered custom (external-head) model — served via MODEL_BUILDERS, no other flags needed: python demo/serve_nlp.py --model-name shhossain/all-MiniLM-L6-v2-sentiment-classifier + # A private local checkpoint against local parquet dataframes (tokenizer resolved from the + # checkpoint's base model; --label-map reconciles the dataframe's labels with the model's): + python demo/serve_nlp.py --model-name demo/private_model \\ + --dataset-path data/nlp_test_parquet --dataset-split test --reference-split train \\ + --label-map '{"negative": "NEGATIVE", "positive": "POSITIVE"}' First run needs transformers, datasets and pacmap (the ``[nlp]`` extra). A ``SentenceTransformerModel`` custom builder additionally needs the ``sentence-transformers`` package. @@ -162,6 +202,7 @@ from __future__ import annotations import argparse +import hashlib import json import logging from collections.abc import Callable @@ -170,8 +211,8 @@ import datasets import pacmap +import pandas as pd import torch -import transformers from torch import nn from shapash.backend import NlpCaptumLigBackend @@ -192,7 +233,18 @@ class ServeConfig: """ model_name: str = "bhadresh-savani/distilbert-base-uncased-emotion" + # Tokenizer source, when it isn't the checkpoint itself: a local ``save_pretrained`` artifact often + # ships weights + config only. ``None`` resolves it — see _resolve_tokenizer_source. + tokenizer_name: str | None = None + # Class names in output-column order, overriding the model config's ``id2label``. Needed for a + # fine-tune saved without label names (its config reports "LABEL_0"/"LABEL_1"). + label_names: list[str] | None = None dataset_name: str = "dair-ai/emotion" + # Local dataframes instead of the HF hub: a directory of ``<split>.parquet`` / ``<split>.pkl``, or a + # single file (see _local_split_file). Mutually exclusive with ``dataset_name`` — the CLI rejects + # both, and every branch between the two goes through ``is_local_dataset`` so no code path can + # consult the unused one. + dataset_path: Path | None = None dataset_config: str | None = None # HF dataset config/subset (e.g. a language); None = dataset's default dataset_split: str = "test" text_column: str = "text" @@ -222,19 +274,80 @@ class ServeConfig: host: str = "0.0.0.0" # noqa: S104 recompute: bool = False + def __post_init__(self) -> None: + """Normalise ``dataset_path`` once, so every consumer sees the same path. + + ``~`` is expanded by the *shell*, so a path only arrives here unexpanded when a caller built the + config directly (the REPL workflow this dataclass exists for) — and a literal ``~/...`` then + reads as a nonexistent *relative* directory, which silently defeats the ``is_file()`` tests that + decide whether a single file can also serve as the reference corpus. Doing it here means no + consumer has to remember to. + """ + if self.dataset_path is not None: + self.dataset_path = self.dataset_path.expanduser() + + @property + def model_path(self) -> Path | None: + """The local checkpoint directory ``model_name`` points at, or ``None`` when it is a hub id. + + The single answer to "is this model local, and where?": the cache slug, the tokenizer fallback + and ``from_pretrained`` all ask *here* instead of each re-deriving it. They used to, and + disagreed — two expanded ``~`` and the loader did not, so a ``~/checkpoints/...`` model resolved + its tokenizer and its cache directory correctly and then failed to load. + """ + path = Path(self.model_name).expanduser() + return path if path.is_dir() else None + + @property + def is_local_dataset(self) -> bool: + """True when rows come from local dataframes (``--dataset-path``) rather than the HF hub. + + ``dataset_name`` and ``dataset_path`` are mutually exclusive (the CLI rejects both), so exactly + one of them is meaningful on any given run. Every branch between the two tests *this* rather + than comparing fields, so a stale ``dataset_name`` default cannot leak into a local run — not + into its cache path (:func:`_dataset_slug`) and not into its loader (:func:`_load_split`). + """ + return self.dataset_path is not None + # Short ``--attribution`` choice → the attribution backend's registered ``.name`` (used as the cache # subdirectory *and* to pick the backend in ``build_backend``). _ATTRIBUTION_BACKENDS = {"shap": "nlp_shap", "lig": "nlp_captum_lig"} +def _path_slug(path: Path) -> str: + """Filesystem-safe cache tag for a local path: its name plus a digest of its absolute location. + + The name alone would collide across runs (every training run named ``output/`` or ``best_model/`` + would share one cache directory); the absolute path alone is unreadable as a directory name. The + digest disambiguates, the name keeps ``--cache-dir`` browsable. + """ + resolved = path.expanduser().resolve() + digest = hashlib.sha256(str(resolved).encode()).hexdigest()[:8] + return f"{resolved.name}__{digest}" + + +def _model_slug(config: ServeConfig) -> str: + """Filesystem-safe tag for ``--model-name`` — an HF id or a local checkpoint directory. + + HF ids keep the historical ``org__model`` spelling, so caches built before local checkpoints were + supported still resolve to the same directory; a local path goes through :func:`_path_slug`. + """ + if config.model_path is not None: + return _path_slug(config.model_path) + return config.model_name.replace("/", "__") + + def _dataset_slug(config: ServeConfig) -> str: """Filesystem-safe ``<dataset>__[<config>__]<split>`` tag for the cache hierarchy. The config segment is only included when ``--dataset-config`` is actually set, so existing cache directories built before that flag existed (``<dataset>__<split>``, no config with no HF dataset - config) still resolve to the same path. + config) still resolve to the same path. A local ``--dataset-path`` is tagged by + :func:`_path_slug` (an absolute path is neither readable nor unique as a directory name). """ + if config.is_local_dataset: + return f"{_path_slug(config.dataset_path)}__{config.dataset_split}" parts = [config.dataset_name, *([config.dataset_config] if config.dataset_config else []), config.dataset_split] return "__".join(parts).replace("/", "__") @@ -248,7 +361,7 @@ def _dataset_cache_dir(config: ServeConfig) -> Path: never touches the attribution backend, so every ``--attribution`` choice shares one projection cache at *this* level (below it, contributions split per backend). """ - return config.cache_dir / config.model_name.replace("/", "__") / _dataset_slug(config) + return config.cache_dir / _model_slug(config) / _dataset_slug(config) def _compile_cache_dir(config: ServeConfig) -> Path: @@ -266,9 +379,36 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: defaults = ServeConfig() parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - "--model-name", default=defaults.model_name, help="HF model id (label names are read from its config)." + "--model-name", + default=defaults.model_name, + help="HF model id, or a path to a local checkpoint directory saved with save_pretrained " + "(label names are read from its config unless --label-names is given).", + ) + parser.add_argument( + "--tokenizer-name", + default=defaults.tokenizer_name, + help="HF id or path of the tokenizer, when it isn't in the checkpoint itself. Default: resolved " + "from the checkpoint, falling back to the base model recorded in its config.", + ) + parser.add_argument( + "--label-names", + default=None, + help="Comma-separated class names in output-column order, overriding the model config's " + "id2label — e.g. 'NEGATIVE,POSITIVE'. Needed for a fine-tune saved without label names " + "(its config reports LABEL_0/LABEL_1).", + ) + # One dataset source per run: a stale --dataset-name alongside --dataset-path would otherwise decide + # the cache slug and the DATASET_LOADERS lookup for rows it did not provide. + source = parser.add_mutually_exclusive_group() + source.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") + source.add_argument( + "--dataset-path", + type=Path, + default=defaults.dataset_path, + help="Local dataframes instead of the HF hub: a directory holding <split>.parquet or " + "<split>.pkl (so --dataset-split / --reference-split select the file), or a single file " + "(which then requires --n-reference 0). Reads --text-column / --label-column.", ) - parser.add_argument("--dataset-name", default=defaults.dataset_name, help="HF datasets id.") parser.add_argument( "--dataset-config", default=defaults.dataset_config, @@ -344,7 +484,31 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: help="Ignore any cached results and recompute SHAP + projection (overwrites the cache).", ) args = parser.parse_args(argv) - return ServeConfig(**vars(args)) + # --label-names is a flat string on the CLI (one flag, no quoting rules to remember) but a list + # everywhere else, since that is what the model adapters take. + args.label_names = [name.strip() for name in args.label_names.split(",")] if args.label_names else None + # Validate the *config*, not the raw namespace: ``__post_init__`` has normalised ``dataset_path`` by + # then, so the ``is_file()`` test below sees the same path the loaders will (an unexpanded ``~/...`` + # is a nonexistent relative directory, and would slip through this check). + config = ServeConfig(**vars(args)) + if config.is_local_dataset and config.dataset_config is not None: + # --dataset-config only means anything to the hub loader; accepting it here would look like it + # selected something. (It isn't in the mutually exclusive group above: that pairs the two + # *sources*, and grouping three flags would reject the legitimate --dataset-name/--dataset-config + # combination too.) + parser.error( + "--dataset-config applies to --dataset-name (a hub dataset) and has no meaning for --dataset-path." + ) + if config.is_local_dataset and config.dataset_path.is_file() and config.n_reference > 0: + # A single file can only serve one split; silently reusing it as the reference corpus would + # retrieve "similar examples" from the served rows themselves. Make the caller choose. The same + # invariant is re-checked in load_reference_corpus, which a REPL-built config does reach. + parser.error( + f"--dataset-path {config.dataset_path} is a single file, so there is no --reference-split " + f"{config.reference_split!r} to load: pass --n-reference 0 to disable the Similar Examples " + "panel, or point --dataset-path at a directory holding one file per split." + ) + return config def _load_hf_dataset(name: str, dataset_config: str | None, split: str) -> datasets.Dataset: @@ -367,6 +531,78 @@ def _load_hf_dataset(name: str, dataset_config: str | None, split: str) -> datas ) +# Local dataframe formats ``--dataset-path`` recognises, in the order a ``<split>.<ext>`` file is looked +# up. ``read_pickle`` unpickles, which executes arbitrary code — only ever point ``--dataset-path`` at +# files you produced yourself (the same trust assumption as loading your own model weights). +_DATAFRAME_READERS: dict[str, Callable[[Path], pd.DataFrame]] = { + ".parquet": pd.read_parquet, + ".pkl": pd.read_pickle, # noqa: S301 + ".pickle": pd.read_pickle, # noqa: S301 +} + + +def _local_split_file(dataset_path: Path, split: str) -> Path: + """Resolve ``--dataset-path`` + a split name to one dataframe file. + + A *file* is that split's data as given (the CLI only allows this when the reference corpus is + disabled, since one file cannot also be another split). A *directory* is searched for + ``<split>.<ext>`` over :data:`_DATAFRAME_READERS`, so ``--dataset-split``/``--reference-split`` + select files the same way they select HF splits. Where a split exists in more than one format, that + dict's order decides (parquet first). + + ``dataset_path`` is expected already normalised — ``ServeConfig.__post_init__`` expands ``~`` so + every caller tests the same path. + """ + path = dataset_path + if path.is_file(): + return path + if not path.is_dir(): + # Distinguish "the dataset root is wrong" from "this split is missing" — the message below + # would otherwise blame the split name for a mistyped --dataset-path. + raise FileNotFoundError(f"--dataset-path {path} is neither a file nor a directory.") + for suffix in _DATAFRAME_READERS: + candidate = path / f"{split}{suffix}" + if candidate.is_file(): + return candidate + raise FileNotFoundError( + f"No '{split}' dataframe in {path}: expected one of " + f"{', '.join(f'{split}{suffix}' for suffix in _DATAFRAME_READERS)}." + ) + + +def _load_local_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: + """Load ``n`` ``(texts, label_strings)`` rows of ``split`` from a local dataframe. + + The dataframe counterpart of the generic HF path in :func:`_load_split`, and deliberately identical + downstream of the read: rows are shuffled with ``config.seed`` before the first ``n`` are taken, + labels are stringified, and ``config.label_map`` renames them onto the model's spelling. Rows with a + missing or blank text or label are dropped first — a real-world dataframe has them, an HF + ``ClassLabel`` column cannot. + """ + file = _local_split_file(config.dataset_path, split) + frame = _DATAFRAME_READERS[file.suffix](file) + for column in (config.text_column, config.label_column): + if column not in frame.columns: + raise KeyError( + f"Column {column!r} not in {file} (columns: {list(frame.columns)}). " + "Point --text-column / --label-column at the right fields." + ) + frame = frame[[config.text_column, config.label_column]].dropna() + # Blank-but-present is as unusable as missing, and a stray "" label would otherwise show up in the + # webapp as a ground-truth class of its own. dropna() above only catches NaN/None. + for column in (config.text_column, config.label_column): + frame = frame[frame[column].astype(str).str.strip().astype(bool)] + if frame.empty: + raise ValueError(f"{file} has no usable rows in columns {config.text_column!r}/{config.label_column!r}.") + frame = frame.sample(frac=1.0, random_state=config.seed).head(n) + sentences = [str(text) for text in frame[config.text_column]] + labels = [str(label) for label in frame[config.label_column]] + if config.label_map: + labels = [config.label_map.get(label, label) for label in labels] + logger.info("Loaded %d samples from %s (split=%s)", len(sentences), file, split) + return sentences, labels + + def _load_amazon_reviews_multi_binary(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: """``mteb/amazon_reviews_multi`` — bucket its 5-star ``label`` (0-4) onto binary sentiment. @@ -411,8 +647,11 @@ def _load_amazon_reviews_multi_binary(config: ServeConfig, split: str, n: int) - def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], list[str]]: """Load ``n`` ``(texts, label_strings)`` of ``split`` from the configured dataset. - A dataset registered in :data:`DATASET_LOADERS` is delegated to its loader entirely (see there for - why ``mteb/amazon_reviews_multi`` needs one). Otherwise, the generic path applies: rows are shuffled + Three sources, checked in this order. ``--dataset-path`` (local dataframes) goes to + :func:`_load_local_split` — first, so a leftover ``--dataset-name`` default can never route local + rows through a hub loader. Otherwise a hub dataset registered in :data:`DATASET_LOADERS` is + delegated to its loader entirely (see there for why ``mteb/amazon_reviews_multi`` needs one). + Otherwise, the generic hub path applies: rows are shuffled with ``config.seed`` before taking the first ``n`` — so samples are representative even when the dataset's on-disk order groups rows by label — and ground-truth label strings come from the dataset's own ``ClassLabel`` feature names when present (the usual shape for HF classification @@ -420,6 +659,9 @@ def _load_split(config: ServeConfig, split: str, n: int) -> tuple[list[str], lis match the model's label names when the two disagree on spelling/casing (e.g. dataset ``"neg"`` vs. model ``"NEGATIVE"``) despite sharing the same class order. """ + if config.is_local_dataset: + return _load_local_split(config, split, n) + loader = DATASET_LOADERS.get(config.dataset_name) if loader is not None: sentences, labels = loader(config, split, n) @@ -453,94 +695,58 @@ def load_reference_corpus(config: ServeConfig) -> tuple[list[str], list[str]] | Returns ``(texts, labels)`` from ``config.reference_split`` for the "Similar Examples" panel, or ``None`` when disabled (``n_reference <= 0``). This is the pool neighbours are retrieved from, so it is the model's *training* set rather than the served (test) split. + + A single-file ``--dataset-path`` cannot supply it: :func:`_local_split_file` returns that one file + for *any* split name, so the bank would be built from the served rows themselves and every "similar + example" would be a lookalike of the corpus it came from. The CLI rejects that combination up front + with a friendlier message, but the invariant is enforced here as well — a ``ServeConfig`` built + directly (the REPL workflow this module is designed for) never goes through ``parse_args``. """ if config.n_reference <= 0: return None - return _load_split(config, config.reference_split, config.n_reference) - - -def _load_tokenizer(model_name: str): - """Load the tokenizer, preferring a fast one but falling back to the slow implementation. - - A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights - use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with - ``use_fast=False``; a slow tokenizer still works (the LIG backend degrades to its scheme-aware - string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a - custom ``trust_remote_code`` architecture — which the ``HFClassifierModel`` adapter does not support; - surface that as a clear error rather than a deep tokenizer stack trace. - """ - try: - return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=True) - except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint - logger.warning( - "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", - model_name, - type(fast_err).__name__, + if config.is_local_dataset and config.dataset_path.is_file(): + raise ValueError( + f"dataset_path {config.dataset_path} is a single file, so it cannot also provide the " + f"reference_split {config.reference_split!r}. Set n_reference=0, or point dataset_path at a " + "directory holding one file per split." ) - try: - return transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False) - except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message - raise RuntimeError( - f"Could not load a tokenizer for {model_name!r} in fast or slow mode. This is usually a " - "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " - "which the HFClassifierModel adapter does not support. Use a standard " - "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." - ) from slow_err - - -# transformers' sentinel for "tokenizer_config.json never set model_max_length" — effectively means -# "no limit", not a real length. Passing it straight through makes truncation=True a no-op. -_UNSET_TOKENIZER_MAX_LENGTH = 100_000 - - -def _safe_max_length(tokenizer, default: int = 512) -> int | None: - """The tokenizer's own truncation length, unless it's the "unset" sentinel — then ``default``. - - A checkpoint whose tokenizer config never set ``model_max_length`` reports ``~1e30`` instead, so - ``truncation=True`` with no explicit ``max_length`` silently does nothing and a long text tokenizes - **unbounded**. That is not merely slow or memory-hungry: once a sequence is longer than the model's - ``max_position_embeddings``, its position ids index past the position/token-type buffers, and the - resulting out-of-bounds gather aborts the CUDA context outright (``device-side assert triggered``). - The abort is asynchronous, so it surfaces at whatever unrelated line next synchronises — see - ``_hf_classifier_model``, which is why every HF classifier here is built through one place. - """ - model_max_length = getattr(tokenizer, "model_max_length", None) - if model_max_length is None or model_max_length > _UNSET_TOKENIZER_MAX_LENGTH: - return default - return model_max_length + return _load_split(config, config.reference_split, config.n_reference) def _hf_classifier_model(config: ServeConfig, device: str, **kwargs) -> HFClassifierModel: - """Build an ``HFClassifierModel`` with truncation always resolved — the single construction point. - - Every path that produces an ``HFClassifierModel`` (the generic loader *and* every custom builder in - :data:`MODEL_BUILDERS`) goes through here, so ``max_length`` can never be left unset by an author who - only meant to override something else, such as ``label_names``. That is not hypothetical: this helper - exists because a custom builder once constructed the model directly and inherited - ``max_length=None``, which silently disabled truncation for a tokenizer reporting the "unset" - sentinel and made any input longer than the model's position buffer abort the CUDA context. - - ``kwargs`` are forwarded to :class:`~shapash.model.HFClassifierModel` (e.g. ``label_names=``). + """Adapt this demo's ``ServeConfig`` onto :meth:`HFClassifierModel.from_pretrained`. + + A thin translation layer: the library classmethod owns everything that used to live here — resolving + a tokenizer (including the base-model fallback for a ``save_pretrained`` directory that shipped none), + defeating the ``model_max_length`` no-op trap (``max_length="auto"``), and the ``label_names`` arity + check. All this demo adds is its own conventions: ``--model-name`` may be a local path or a hub id, + ``--tokenizer-name`` maps to the ``tokenizer=`` override, and ``--label-names`` beats a builder's own + hard-coded ``label_names`` (passing the flag is a deliberate request to override). + + Every path that produces an ``HFClassifierModel`` — the generic loader *and* every custom builder in + :data:`MODEL_BUILDERS` — goes through here, so no builder can bypass ``max_length`` resolution by + constructing the adapter directly. Other ``kwargs`` are forwarded untouched. """ - tokenizer = _load_tokenizer(config.model_name) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(config.model_name).to(device) - logger.info("Loaded %s as HFClassifierModel on device: %s", config.model_name, next(classifier.parameters()).device) - max_length = _safe_max_length(tokenizer) - if max_length != tokenizer.model_max_length: - logger.info( - "%s's tokenizer reports no configured model_max_length — truncating at %d tokens instead.", - config.model_name, - max_length, - ) - return HFClassifierModel(classifier, tokenizer, max_length=max_length, **kwargs) + label_names = config.label_names or kwargs.pop("label_names", None) + source = str(config.model_path) if config.model_path is not None else config.model_name + return HFClassifierModel.from_pretrained( + source, + tokenizer=config.tokenizer_name, + label_names=label_names, + device=device, + **kwargs, + ) def _load_hf_classifier(config: ServeConfig, device: str) -> HFClassifierModel: """The generic default: any standard ``AutoModelForSequenceClassification`` checkpoint. Covers every certified encoder architecture (BERT/DistilBERT/RoBERTa/XLM-R/DeBERTa-v1, and MiniLM - packaged as a sequence classifier). Label names come from the model's own ``config.id2label``, so a - new ``--model-name`` brings its own classes with no extra flags. + packaged as a sequence classifier), whether ``--model-name`` is a hub id or a local checkpoint + directory. Label names come from the model's own ``config.id2label``, so a new ``--model-name`` + brings its own classes with no extra flags — unless ``--label-names`` overrides them (applied in + :func:`_hf_classifier_model`, so builders honour it too), which a fine-tune saved without label + names needs (its config reports ``LABEL_0``/``LABEL_1``). """ return _hf_classifier_model(config, device) diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 4203f2ee..332cf533 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -20,15 +20,148 @@ from __future__ import annotations +import json +import logging +from pathlib import Path from typing import Any import numpy as np +from shapash._optional import import_optional_module from shapash.model.base import ( SupportsTokenization, TextModel, ) -from shapash.model.encoder import _DECISION_SPACE, EncoderClassifierModel +from shapash.model.encoder import _DECISION_SPACE, EncoderClassifierModel, _position_capacity + +logger = logging.getLogger(__name__) + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' + +# Files whose presence in a local checkpoint directory means it ships its own tokenizer. A directory +# saved with ``model.save_pretrained(...)`` holds weights + ``config.json`` but *none* of these — the +# tokenizer is saved separately (often archived elsewhere entirely), which is what makes the base-model +# fallback in :func:`_resolve_tokenizer_source` necessary. +_TOKENIZER_FILES = ("tokenizer_config.json", "tokenizer.json", "vocab.txt") + +# ``transformers`` reports this "unset" sentinel (~1e30, stored as a huge int) for a tokenizer whose +# config never set ``model_max_length``. It means "no configured limit", not a real length: passing it +# straight to ``truncation=True`` makes truncation a silent no-op. Any ``model_max_length`` above this +# is treated as unset. See :func:`_resolve_max_length`. +_UNSET_TOKENIZER_MAX_LENGTH = 100_000 + +# Last-resort truncation length when a checkpoint reports no usable ``model_max_length`` *and* its +# backbone exposes no absolute-position capacity to fall back on (a rotary/ALiBi model). A conservative +# encoder default that every standard architecture can index. +_FALLBACK_MAX_LENGTH = 512 + + +def _resolve_tokenizer_source(name_or_path: str, tokenizer_name: str | None = None) -> str: + """Decide *where* the tokenizer is loaded from — an explicit override, the checkpoint, or its base. + + ``tokenizer_name`` wins when given. Otherwise the checkpoint at ``name_or_path`` is used, except in + the one case it cannot be: a local directory saved with ``model.save_pretrained(...)`` holds weights + and ``config.json`` but no tokenizer files. Since a fine-tune keeps its base model's tokenizer unless + it was deliberately customised, the base id recorded in the checkpoint's own ``config.json`` + (``_name_or_path``, transformers' record of what it was fine-tuned from) is the right fallback — it + needs no extra argument and no access to wherever the tokenizer artifacts were archived. + + A *custom* tokenizer therefore always needs an explicit ``tokenizer_name``: nothing here can detect + that the base tokenizer is the wrong one, so pass it whenever the vocabulary was extended. + + Parameters + ---------- + name_or_path : str + A HuggingFace hub id or a local checkpoint directory. + tokenizer_name : str or None, optional + Explicit tokenizer id/path; short-circuits everything below when given. + + Returns + ------- + str + The id or path to load the tokenizer from. + + Raises + ------ + RuntimeError + If ``name_or_path`` is a local directory that ships no tokenizer files and whose ``config.json`` + records no base model to fall back on. + """ + if tokenizer_name: + return tokenizer_name + + path = Path(name_or_path).expanduser() + if not path.is_dir(): + return str(name_or_path) # a hub id: the repo always ships its own tokenizer files + if any((path / name).is_file() for name in _TOKENIZER_FILES): + return str(path) + + config_file = path / "config.json" + # JSON is UTF-8 by specification; read_text()'s locale default would fail on a config carrying + # non-ASCII label names (e.g. accented id2label values) under a non-UTF-8 locale. + base_model = ( + json.loads(config_file.read_text(encoding="utf-8")).get("_name_or_path") if config_file.is_file() else None + ) + if not base_model: + raise RuntimeError( + f"{path} ships no tokenizer files, and its config.json records no base model to fall back on. " + "Pass tokenizer= with the HF id (or local path) of the tokenizer this checkpoint was trained " + "with." + ) + logger.info( + "%s ships no tokenizer — falling back to its base model %r (from config.json's _name_or_path). " + "Pass tokenizer= if this checkpoint uses a customised tokenizer.", + path, + base_model, + ) + return base_model + + +def _load_tokenizer(source: str, transformers: Any) -> Any: + """Load the tokenizer, preferring a fast one but falling back to the slow implementation. + + A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights + use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with + ``use_fast=False``; a slow tokenizer still works (gradient attribution degrades to a scheme-aware + string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a + custom ``trust_remote_code`` architecture — which this adapter does not support; surface that as a + clear error rather than a deep tokenizer stack trace. + """ + try: + return transformers.AutoTokenizer.from_pretrained(source, use_fast=True) + except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint + logger.warning( + "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", + source, + type(fast_err).__name__, + ) + try: + return transformers.AutoTokenizer.from_pretrained(source, use_fast=False) + except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message + raise RuntimeError( + f"Could not load a tokenizer for {source!r} in fast or slow mode. This is usually a " + "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " + "which HFClassifierModel does not support. Use a standard " + "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." + ) from slow_err + + +def _resolve_max_length(tokenizer: Any, backbone: Any) -> int | None: + """Resolve the truncation length for a checkpoint, defeating the ``model_max_length`` no-op trap. + + A checkpoint whose tokenizer config never set ``model_max_length`` reports the ``~1e30`` sentinel, so + ``truncation=True`` with no explicit ``max_length`` silently does nothing and a long text tokenizes + unbounded — which is not merely slow: once a sequence outruns the backbone's position buffer the + out-of-bounds gather aborts the CUDA context (see + :meth:`~shapash.model.encoder.EncoderClassifierModel._check_encoded_length`). When the tokenizer + reports a real limit we trust it; otherwise we fall back to the backbone's *own* absolute-position + capacity (the exactly-correct bound), and only to :data:`_FALLBACK_MAX_LENGTH` when even that is + unknowable (a rotary/ALiBi backbone). + """ + model_max = getattr(tokenizer, "model_max_length", None) + if isinstance(model_max, int) and 0 < model_max <= _UNSET_TOKENIZER_MAX_LENGTH: + return model_max + return _position_capacity(backbone) or _FALLBACK_MAX_LENGTH def _probs_from_pipeline_output(raw: list, order: list[str]) -> np.ndarray: @@ -162,6 +295,16 @@ def __init__( id2label = getattr(getattr(classifier, "config", None), "id2label", None) if id2label: label_names = [id2label[i] for i in sorted(id2label)] + else: + # Only an *explicit* list can be wrong: names read from ``config.id2label`` above always + # match ``num_labels`` by construction. A wrong-length list would otherwise misalign every + # class name, or fail much later inside a webapp — check it at the point of the mistake. + num_labels = getattr(getattr(classifier, "config", None), "num_labels", None) + if isinstance(num_labels, int) and len(label_names) != num_labels: + raise ValueError( + f"{len(label_names)} label name(s) {label_names!r} for a {num_labels}-class model. " + "Pass exactly one label name per output column, in class-index order." + ) super().__init__( classifier, tokenizer, @@ -174,3 +317,78 @@ def __init__( ) # ``classifier`` is the backbone; keep the historical alias for introspection and tests. self.classifier = classifier + + @classmethod + def from_pretrained( + cls, + name_or_path: str, + *, + tokenizer: Any = None, + label_names: list[str] | None = None, + device: int | str | object | None = None, + max_length: int | str | None = "auto", + **model_kwargs: Any, + ) -> HFClassifierModel: + """Build a full-capability adapter straight from a checkpoint id or a local directory. + + The one-call entry point for serving a classifier: it loads the weights, resolves a matching + tokenizer, and picks a safe truncation length — the three steps that are easy to get subtly + wrong (a checkpoint saved without its tokenizer; a tokenizer reporting no ``model_max_length``, + which silently disables truncation and can abort the CUDA context on a long input). Both a + HuggingFace hub id (``"distilbert-base-uncased-finetuned-sst-2-english"``) and a local + ``save_pretrained`` directory work with no extra arguments. + + Parameters + ---------- + name_or_path : str + A HuggingFace hub id, or a path to a local checkpoint directory. + tokenizer : str or transformers.PreTrainedTokenizerBase or None, optional + An already-loaded tokenizer (used as-is), or an id/path to load one from. When ``None`` + (default) the tokenizer is resolved from the checkpoint, falling back to the base model + recorded in its ``config.json`` for a directory that ships no tokenizer files — see + :func:`_resolve_tokenizer_source`. Pass this explicitly for a customised tokenizer. + label_names : list[str] or None, optional + Class names in class-index order. When ``None`` (default) they are read from the model's + ``config.id2label``; pass this to override a fine-tune saved without label names (its config + reports ``LABEL_0``/``LABEL_1``). Validated against the model's ``num_labels``. + device : int or str or torch.device or None, optional + Device to move the classifier onto (e.g. ``0``, ``"cuda"``, ``"cpu"``). When ``None`` + (default) the checkpoint loads wherever ``transformers`` places it (CPU), and the adapter + inherits that device — so CPU-only environments need no configuration. + max_length : int, "auto", or None, optional + Truncation length. ``"auto"`` (default) resolves the tokenizer's own ``model_max_length``, + substituting the backbone's true position capacity when the tokenizer reports the "unset" + sentinel (see :func:`_resolve_max_length`). ``None`` trusts the tokenizer verbatim (the + length guard in :class:`~shapash.model.encoder.EncoderClassifierModel` still applies). An int + forces an explicit length. + **model_kwargs + Forwarded to the constructor (e.g. ``batch_size``, ``embedding_space``, ``pool``). + + Returns + ------- + HFClassifierModel + The constructed adapter, with the classifier on ``device`` and truncation resolved. + + Raises + ------ + ValueError + If ``max_length`` is not an int, ``None``, or ``"auto"`` (or ``label_names`` has the wrong + length — see :meth:`__init__`). + """ + transformers = import_optional_module("transformers", extra=_NLP_EXTRA) + if tokenizer is None or isinstance(tokenizer, str): + tokenizer = _load_tokenizer(_resolve_tokenizer_source(name_or_path, tokenizer), transformers) + + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(str(name_or_path)) + if device is not None: + classifier = classifier.to(device) + logger.info("Loaded %s as HFClassifierModel on device: %s", name_or_path, next(classifier.parameters()).device) + + if max_length == "auto": + resolved_max_length = _resolve_max_length(tokenizer, classifier) + elif max_length is None or isinstance(max_length, int): + resolved_max_length = max_length + else: + raise ValueError(f"max_length must be an int, None, or 'auto'; got {max_length!r}.") + + return cls(classifier, tokenizer, label_names=label_names, max_length=resolved_max_length, **model_kwargs) diff --git a/tests/unit_tests/model/test_hf_from_pretrained.py b/tests/unit_tests/model/test_hf_from_pretrained.py new file mode 100644 index 00000000..a0498b19 --- /dev/null +++ b/tests/unit_tests/model/test_hf_from_pretrained.py @@ -0,0 +1,268 @@ +"""Unit tests for ``HFClassifierModel.from_pretrained`` and its loader helpers. + +These cover the checkpoint-loading logic that used to live in ``demo/serve_nlp.py``: tokenizer-source +resolution (including the base-model fallback for a directory saved without its tokenizer), the +``model_max_length`` no-op trap, the fast/slow tokenizer fallback, the label-name arity check, and the +end-to-end classmethod. ``transformers`` is faked throughout so the tests stay fast and need no network, +no real weights, and no GPU. +""" + +import json +from types import SimpleNamespace + +import pytest +import torch +from torch import nn + +from shapash.model import hf +from shapash.model.hf import ( + _FALLBACK_MAX_LENGTH, + _UNSET_TOKENIZER_MAX_LENGTH, + HFClassifierModel, + _load_tokenizer, + _resolve_max_length, + _resolve_tokenizer_source, +) + + +# ── _resolve_tokenizer_source ─────────────────────────────────────────────────────────────────────── + + +def test_explicit_tokenizer_name_wins_over_everything(tmp_path): + (tmp_path / "config.json").write_text(json.dumps({"_name_or_path": "base-model"})) + assert _resolve_tokenizer_source(str(tmp_path), tokenizer_name="my/tokenizer") == "my/tokenizer" + + +def test_hub_id_is_passed_through_unchanged(): + # Not a local directory → assumed a hub repo, which always ships its own tokenizer. + assert _resolve_tokenizer_source("distilbert-base-uncased") == "distilbert-base-uncased" + + +def test_local_dir_with_tokenizer_files_loads_from_the_dir(tmp_path): + (tmp_path / "tokenizer.json").write_text("{}") + assert _resolve_tokenizer_source(str(tmp_path)) == str(tmp_path) + + +def test_local_dir_without_tokenizer_falls_back_to_config_base_model(tmp_path): + # A save_pretrained() dir: weights + config.json but no tokenizer files. + (tmp_path / "config.json").write_text(json.dumps({"_name_or_path": "the-base-model"})) + assert _resolve_tokenizer_source(str(tmp_path)) == "the-base-model" + + +def test_local_dir_with_no_tokenizer_and_no_base_model_raises(tmp_path): + (tmp_path / "config.json").write_text(json.dumps({"num_labels": 2})) # no _name_or_path + with pytest.raises(RuntimeError, match="no base model to fall back on"): + _resolve_tokenizer_source(str(tmp_path)) + + +def test_local_dir_with_no_files_at_all_raises(tmp_path): + with pytest.raises(RuntimeError, match="ships no tokenizer files"): + _resolve_tokenizer_source(str(tmp_path)) + + +# ── _resolve_max_length ───────────────────────────────────────────────────────────────────────────── + + +def _backbone(max_position_embeddings=None, position_biased_input=True): + return SimpleNamespace( + config=SimpleNamespace( + max_position_embeddings=max_position_embeddings, + position_biased_input=position_biased_input, + ) + ) + + +def test_real_model_max_length_is_trusted(): + tok = SimpleNamespace(model_max_length=256) + assert _resolve_max_length(tok, _backbone(max_position_embeddings=512)) == 256 + + +def test_unset_sentinel_falls_back_to_backbone_capacity(): + tok = SimpleNamespace(model_max_length=int(1e30)) # transformers' "no limit" sentinel + assert tok.model_max_length > _UNSET_TOKENIZER_MAX_LENGTH + assert _resolve_max_length(tok, _backbone(max_position_embeddings=512)) == 512 + + +def test_missing_model_max_length_falls_back_to_capacity(): + tok = SimpleNamespace(model_max_length=None) + assert _resolve_max_length(tok, _backbone(max_position_embeddings=1024)) == 1024 + + +def test_sentinel_with_no_position_capacity_uses_hardcoded_default(): + tok = SimpleNamespace(model_max_length=int(1e30)) + # A rotary/ALiBi backbone exposes no absolute-position table → capacity unknowable. + assert _resolve_max_length(tok, _backbone(max_position_embeddings=None)) == _FALLBACK_MAX_LENGTH + + +# ── _load_tokenizer (fast → slow fallback) ──────────────────────────────────────────────────────────── + + +class _FakeAutoTokenizer: + """Records calls; can be told to fail the fast path, the slow path, or both.""" + + def __init__(self, fail_fast=False, fail_slow=False): + self.fail_fast = fail_fast + self.fail_slow = fail_slow + self.calls = [] + + def from_pretrained(self, source, use_fast): + self.calls.append(use_fast) + if use_fast and self.fail_fast: + raise OSError("no tokenizer.json") + if not use_fast and self.fail_slow: + raise OSError("no slow tokenizer either") + return SimpleNamespace(kind="fast" if use_fast else "slow") + + +def test_load_tokenizer_prefers_fast(): + auto = _FakeAutoTokenizer() + tok = _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto)) + assert tok.kind == "fast" + assert auto.calls == [True] + + +def test_load_tokenizer_falls_back_to_slow(): + auto = _FakeAutoTokenizer(fail_fast=True) + tok = _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto)) + assert tok.kind == "slow" + assert auto.calls == [True, False] + + +def test_load_tokenizer_both_fail_raises_actionable_error(): + auto = _FakeAutoTokenizer(fail_fast=True, fail_slow=True) + with pytest.raises(RuntimeError, match="does not support"): + _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto)) + + +# ── label_names arity check in __init__ ─────────────────────────────────────────────────────────────── + + +class _FakeTokenizer: + def __call__(self, texts, **kwargs): + seqs = [[(hash(w) % 8) + 1 for w in t.split()] for t in ([texts] if isinstance(texts, str) else texts)] + width = max(len(s) for s in seqs) + return { + "input_ids": torch.tensor([s + [0] * (width - len(s)) for s in seqs], dtype=torch.long), + "attention_mask": torch.tensor([[1] * len(s) + [0] * (width - len(s)) for s in seqs], dtype=torch.long), + } + + +class _TinyClassifier(nn.Module): + """A tiny 2-class sequence classifier carrying a realistic ``config``.""" + + def __init__(self, num_labels=2, id2label=None): + super().__init__() + self.emb = nn.Embedding(9, 4) + self.classifier = nn.Linear(4, num_labels) + self.config = SimpleNamespace( + num_labels=num_labels, + id2label=id2label, + max_position_embeddings=512, + position_biased_input=True, + _name_or_path="tiny-base", + ) + + @property + def device(self): + return torch.device("cpu") + + def forward(self, input_ids=None, attention_mask=None, output_hidden_states=False, **kwargs): + h = self.emb(input_ids) + out = SimpleNamespace(logits=self.classifier(h.mean(dim=1))) + if output_hidden_states: + out.hidden_states = (h,) + return out + + +def test_explicit_label_names_wrong_length_raises(): + with pytest.raises(ValueError, match="for a 2-class model"): + HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "neutral", "pos"]) + + +def test_explicit_label_names_right_length_is_accepted(): + model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"]) + assert model.label_names == ["neg", "pos"] + + +def test_label_names_read_from_config_never_trip_the_arity_check(): + clf = _TinyClassifier(id2label={0: "neg", 1: "pos"}) + model = HFClassifierModel(clf, _FakeTokenizer()) # label_names=None → read from config + assert model.label_names == ["neg", "pos"] + + +# ── from_pretrained (end-to-end, faked transformers) ────────────────────────────────────────────────── + + +class _FakeTransformers: + """A stand-in ``transformers`` module: hands back a fake tokenizer and a real tiny nn.Module.""" + + def __init__(self, tokenizer, classifier): + self._tokenizer = tokenizer + self._classifier = classifier + self.model_loaded_from = None + self.AutoTokenizer = SimpleNamespace(from_pretrained=lambda source, use_fast: tokenizer) + self.AutoModelForSequenceClassification = SimpleNamespace(from_pretrained=self._load_model) + + def _load_model(self, source): + self.model_loaded_from = source + return self._classifier + + +@pytest.fixture +def fake_transformers(monkeypatch): + """Patch ``import_optional_module`` so ``from_pretrained`` gets our fake transformers module.""" + tokenizer = _FakeTokenizer() + tokenizer.model_max_length = int(1e30) # unset sentinel → exercises the capacity fallback + classifier = _TinyClassifier(id2label={0: "neg", 1: "pos"}) + fake = _FakeTransformers(tokenizer, classifier) + monkeypatch.setattr(hf, "import_optional_module", lambda name, extra="": fake) + return fake + + +def test_from_pretrained_builds_a_working_adapter(fake_transformers): + model = HFClassifierModel.from_pretrained("some/checkpoint") + + assert isinstance(model, HFClassifierModel) + assert model.classifier is fake_transformers._classifier + assert model.label_names == ["neg", "pos"] # read from the model config + assert fake_transformers.model_loaded_from == "some/checkpoint" + probs = model.predict(["a good movie", "a bad one"]) + assert probs.shape == (2, 2) + + +def test_from_pretrained_auto_resolves_the_unset_max_length_sentinel(fake_transformers): + # The fake tokenizer reports the ~1e30 sentinel; "auto" must substitute the backbone capacity (512). + model = HFClassifierModel.from_pretrained("some/checkpoint", max_length="auto") + assert model.max_length == 512 + + +def test_from_pretrained_none_max_length_is_passed_through(fake_transformers): + model = HFClassifierModel.from_pretrained("some/checkpoint", max_length=None) + assert model.max_length is None + + +def test_from_pretrained_explicit_int_max_length_wins(fake_transformers): + model = HFClassifierModel.from_pretrained("some/checkpoint", max_length=128) + assert model.max_length == 128 + + +def test_from_pretrained_rejects_a_bad_max_length(fake_transformers): + with pytest.raises(ValueError, match="max_length must be"): + HFClassifierModel.from_pretrained("some/checkpoint", max_length="longish") + + +def test_from_pretrained_label_names_override_is_arity_checked(fake_transformers): + with pytest.raises(ValueError, match="for a 2-class model"): + HFClassifierModel.from_pretrained("some/checkpoint", label_names=["only-one"]) + + +def test_from_pretrained_forwards_model_kwargs(fake_transformers): + model = HFClassifierModel.from_pretrained("some/checkpoint", batch_size=7) + assert model.batch_size == 7 + + +def test_from_pretrained_moves_classifier_to_requested_device(fake_transformers, monkeypatch): + moved_to = [] + monkeypatch.setattr(fake_transformers._classifier, "to", lambda device: (moved_to.append(device) or fake_transformers._classifier)) + HFClassifierModel.from_pretrained("some/checkpoint", device="cpu") + assert moved_to == ["cpu"] From 835527f1e61daa75e721efe8b8743b3f93039398 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 18 Aug 2026 17:18:53 +0200 Subject: [PATCH 091/144] fix(types): resolve mypy errors --- shapash/explainer/nlp_explainer.py | 13 +++-- shapash/model/base.py | 9 ++-- shapash/webapp/nlp_app.py | 52 ++++++++++++------- .../nlp_components/sentence_highlight.py | 3 +- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 225228ab..4fd901fe 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -315,9 +315,7 @@ def compile( if y_true is not None: self.y_true = ( - y_true - if isinstance(y_true, pd.Series) - else pd.Series(y_true, index=self.texts.index, name="ground_truth") + y_true if isinstance(y_true, pd.Series) else pd.Series(y_true, index=texts.index, name="ground_truth") ) def cache_path(self, x: list[str] | pd.Series, cache_dir: str | Path) -> Path: @@ -434,7 +432,11 @@ def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, if reducer is None: reducer = PCA(n_components=2) - store = EmbeddingStore(text_model, self.texts.tolist(), cache_dir=cache_dir) + store = EmbeddingStore( + text_model, # type: ignore[arg-type] # has_capabilities narrows the capability + self.texts.tolist(), + cache_dir=cache_dir, + ) if recompute: store.clear() with self._compute_guard(): # embed() tokenizes — serialize against the live compute ops @@ -488,7 +490,7 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: results: TextResults = state["results"] xpl = cls.__new__(cls) xpl.model = None - xpl.backend = None + xpl.backend = None # type: ignore[assignment] # snapshot instance never runs live compute xpl._text_model = None xpl.cf_generator = None xpl.cf_generators = {} @@ -698,6 +700,7 @@ def _predict(self, text_list: list[str]) -> pd.DataFrame: Handles both ``return_all_scores=True`` (list of lists of dicts) and single-prediction (list of dicts) pipeline output formats. """ + assert self.texts is not None # noqa: S101 - compile() sets this before calling _predict text_model = getattr(self, "_text_model", None) if text_model is not None: probs = text_model.predict(text_list) diff --git a/shapash/model/base.py b/shapash/model/base.py index e10cad48..403462b4 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -22,7 +22,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Protocol, runtime_checkable +from typing import Any, Protocol, cast, runtime_checkable import numpy as np @@ -149,11 +149,12 @@ def word_start_marker(self) -> str | None: ``"Ġ"`` (byte-level BPE) or ``"▁"`` (SentencePiece) when word starts are marked; ``None`` for WordPiece-style continuation marking or a plain whitespace tokenizer. """ - marker = getattr(self, "_word_start_marker_cache", _UNRESOLVED) - if marker is _UNRESOLVED: + cached = getattr(self, "_word_start_marker_cache", _UNRESOLVED) + if cached is _UNRESOLVED: marker = self._probe_word_start_marker() self._word_start_marker_cache = marker - return marker + return marker + return cast("str | None", cached) def _probe_word_start_marker(self) -> str | None: """Tokenize :data:`_MARKER_PROBE_TEXT` and report the marker its tokens carry, if any.""" diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 7bddc08f..1e80bc6a 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -196,9 +196,11 @@ def _build_layout(self) -> None: n = self._view.n_samples # ── Table records — always include _orig_idx for scatter filtering ── + texts = self._view.texts + assert texts is not None # noqa: S101 - the webapp requires an already-compiled explainer records: dict[str, list] = { "_orig_idx": list(range(n)), - "text": self._view.texts.tolist(), + "text": texts.tolist(), "prediction": (self._view.y_pred.tolist() if self._view.y_pred is not None else [""] * n), } if self._view.y_true is not None: @@ -247,11 +249,12 @@ def _build_layout(self) -> None: if tok.strip() and not _SPECIAL_RE.match(tok.strip()) } ) + word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in all_words] # ── Scatter panel content (only when scatter_xy is provided) ── scatter_col_content = None if self._scatter_xy is not None: - color_options = [{"label": "Prediction", "value": "prediction"}] + color_options: list[dcc.Dropdown.Options] = [{"label": "Prediction", "value": "prediction"}] if self._view.y_true is not None: color_options.append({"label": "Ground Truth", "value": "ground_truth"}) color_options.append({"label": "Word contribution", "value": "word_contribution"}) @@ -278,7 +281,7 @@ def _build_layout(self) -> None: dbc.Col( dcc.Dropdown( id="scatter-word-select", - options=[{"label": w, "value": w} for w in all_words], + options=word_options, value=[], multi=True, clearable=True, @@ -319,9 +322,16 @@ def _build_layout(self) -> None: # per-cell word charts below (words toward the predicted vs. the true class). error_analysis_body = None if has_gt: + normalize_options: list[dcc.RadioItems.Options] = [ + {"label": " Counts", "value": "count"}, + {"label": " Recall", "value": "recall"}, + ] idx_of = self._label_to_idx - true_arr = np.array([idx_of.get(str(v), -1) for v in self._view.y_true.tolist()]) - pred_arr = np.array([idx_of.get(str(v), -1) for v in self._view.y_pred.tolist()]) + y_true, y_pred = self._view.y_true, self._view.y_pred + # has_gt confirms y_true was compiled; y_pred always accompanies it. + assert y_true is not None and y_pred is not None # noqa: S101 + true_arr = np.array([idx_of.get(str(v), -1) for v in y_true.tolist()]) + pred_arr = np.array([idx_of.get(str(v), -1) for v in y_pred.tolist()]) k = len(label_names) cm = np.zeros((k, k), dtype=int) for t, p in zip(true_arr, pred_arr, strict=True): @@ -346,10 +356,7 @@ def _build_layout(self) -> None: dbc.Col( dcc.RadioItems( id="cm-normalize", - options=[ - {"label": " Counts", "value": "count"}, - {"label": " Recall", "value": "recall"}, - ], + options=normalize_options, value="count", inline=True, inputStyle={"marginRight": "4px"}, @@ -418,6 +425,14 @@ def _build_layout(self) -> None: # independent from the local class picker in the Sentence Highlight panel below.) # The tab label already names the panel, so no H6; a tight controls row + a graph that # flex-grows to fill the panel means the whole chart is visible without scrolling. + global_class_options: list[dcc.Dropdown.Options] = [ + {"label": name, "value": i} for i, name in enumerate(label_names) + ] + sign_filter_options: list[dcc.RadioItems.Options] = [ + {"label": " All", "value": "all"}, + {"label": " Positive", "value": "positive"}, + {"label": " Negative", "value": "negative"}, + ] word_importance_panel = html.Div( [ dbc.Row( @@ -427,7 +442,7 @@ def _build_layout(self) -> None: html.Label("Class", className="fw-bold small mb-0"), dcc.Dropdown( id="global-class-selector", - options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + options=global_class_options, value=0, clearable=False, ), @@ -454,11 +469,7 @@ def _build_layout(self) -> None: html.Label("Contributions", className="fw-bold small mb-0"), dcc.RadioItems( id="sign-filter", - options=[ - {"label": " All", "value": "all"}, - {"label": " Positive", "value": "positive"}, - {"label": " Negative", "value": "negative"}, - ], + options=sign_filter_options, value="all", inline=True, inputStyle={"marginRight": "4px"}, @@ -473,7 +484,7 @@ def _build_layout(self) -> None: html.Label("Exclude words", className="fw-bold small mb-0"), dcc.Dropdown( id="word-filter", - options=[{"label": w, "value": w} for w in all_words], + options=word_options, value=[], multi=True, placeholder="Exclude…", @@ -601,7 +612,7 @@ def _build_layout(self) -> None: ) # ── Assemble the three panels as tab groups (all bodies stay mounted) ── - self._tab_groups = {} + self._tab_groups: dict[str, list[str]] = {} # Local class picker default: the predicted class of the initially selected row. It is reset # to the newly-selected text's prediction by a sync callback owned by @@ -1188,12 +1199,15 @@ def _build_scatter_fig( """ n = self._view.n_samples contrib = self._view.contributions - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in self._view.texts] + view_texts = self._view.texts + assert view_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in view_texts] xy = self._scatter_xy + assert xy is not None # noqa: S101 - only registered/called when scatter_xy was provided error_mask = self._error_mask() if errors_only else None if color_by == "word_contribution" and words: - contributions = sum(self._word_contributions(w, label_idx) for w in words) + contributions = np.sum([self._word_contributions(w, label_idx) for w in words], axis=0) max_abs = float(np.abs(contributions).max()) or 1.0 present_mask = np.where(contributions != 0.0)[0] absent_mask = np.where(contributions == 0.0)[0] diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py index 7af57be9..3133dd6b 100644 --- a/shapash/webapp/nlp_components/sentence_highlight.py +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -27,6 +27,7 @@ def __init__(self, default_class_idx: int = 0) -> None: def layout(self, view, engine=None) -> html.Div: """Return the class picker + sentence-highlight placeholder div.""" label_names = view.label_names or [str(i) for i in range(view.n_classes)] + class_options: list[dcc.Dropdown.Options] = [{"label": name, "value": i} for i, name in enumerate(label_names)] return html.Div( [ # One inline phrase instead of a title + a same-info dropdown: the class name would @@ -36,7 +37,7 @@ def layout(self, view, engine=None) -> html.Div: html.Span("Token Contributions for", className="fw-bold small me-2"), dcc.Dropdown( id="local-class-selector", - options=[{"label": name, "value": i} for i, name in enumerate(label_names)], + options=class_options, value=self._default_class_idx, clearable=False, style={"width": "180px"}, From dbaaa8f4c43934f799522d5460f310221f7f2e09 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 19 Aug 2026 17:13:04 +0200 Subject: [PATCH 092/144] fix(nlp): segment text contributions at word boundaries, not whitespace two consequences on corpus-level ranking, both fixed here: 1. punctuation is filtered in word_importance, it stays attributed and additive in local explanations. 2. aggregation now folds case when model tokenizer does, probed via SupportsTokenization.folds_case(), thus on an uncased model AWFUL = awful, on a cased model they stay distinct. Furthermore, contractions now split at apostrophe which is the tokenizer's own boundary. Captum LIG remains affected by the same bug on sentencepiece metaspace tokenizers (not adressed here) --- shapash/backend/nlp_backend.py | 93 ++++++++++- shapash/backend/nlp_shap_backend.py | 122 +++++++++++--- shapash/explainer/nlp_explainer.py | 19 ++- shapash/model/base.py | 36 +++++ shapash/webapp/nlp_app.py | 22 ++- tests/unit_tests/backend/test_nlp_backend.py | 35 ++++ .../backend/test_nlp_shap_backend.py | 131 ++++++++++++++- tests/unit_tests/model/test_model_base.py | 40 +++++ tests/unit_tests/nlp/test_nlp_prototype.py | 151 ++++++++++++++++-- 9 files changed, 595 insertions(+), 54 deletions(-) create mode 100644 tests/unit_tests/backend/test_nlp_backend.py diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index 40d90888..ddf238cb 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -25,6 +25,18 @@ from shapash.backend.backend import Backend +# A unit made entirely of punctuation/symbol characters — no letters, digits or underscore. Word +# segmentation deliberately emits these as their own units (the model reads them, and "!!!" or "?" +# genuinely moves a sentiment prediction), so they stay visible in a local highlight. They are noise +# in a *corpus-level* ranking, though, which is why ``word_importance`` filters them by default. +_PUNCTUATION_RE = re.compile(r"^[^\w\s]+$") + + +def is_punctuation(word: str) -> bool: + """True when ``word`` consists solely of punctuation or symbol characters.""" + stripped = word.strip() + return bool(stripped) and bool(_PUNCTUATION_RE.match(stripped)) + @dataclass class NlpRawExplanation: @@ -71,6 +83,11 @@ class NlpContributions: Human-readable class names; set by the caller after construction. index : pd.Index or None Row index from the source ``pd.Series``; set by the caller after construction. + folds_case : bool or None + Whether the model's tokenizer normalises case away, from + :meth:`~shapash.model.base.SupportsTokenization.folds_case`; set by the caller after + construction. Decides the default unit grouping in :meth:`word_importance` — see + :meth:`resolve_lowercase`. ``None`` when the model exposes no tokenizer to ask. """ token_strings: list[list[str]] @@ -78,25 +95,59 @@ class NlpContributions: base_values: np.ndarray label_names: list[str] | None = field(default=None) index: pd.Index | None = field(default=None) + folds_case: bool | None = field(default=None) _SPECIAL_RE: re.Pattern = re.compile(r"^\[.*\]$|^##|^\s*$") def __len__(self) -> int: return len(self.token_strings) + def resolve_lowercase(self, lowercase: bool | None = None) -> bool: + """Decide whether word units should be case-folded, deferring to the model by default. + + One source of truth for the question, so a caller that has to normalise units the same + way (the webapp's exclusion vocabulary) cannot drift from :meth:`word_importance`. + + Parameters + ---------- + lowercase : bool, optional + Explicit caller override. ``None`` (default) derives the answer from ``folds_case``. + + Returns + ------- + bool + ``True`` when ``AWFUL`` and ``awful`` should aggregate into one unit. + + Notes + ----- + With no override, an *uncased* model folds and a *cased* model does not: on an uncased + tokenizer both spellings are the same input ids, so keeping them apart splits one word + into variants the model provably cannot distinguish; on a cased one they are different + inputs whose attributions differ for real reasons, and merging would hide that. + + When ``folds_case`` is ``None`` — no tokenizer to probe, e.g. a bare LIME + ``classifier_fn`` — this falls back to folding, which keeps a corpus-level ranking from + fragmenting on a question that cannot be answered here. + """ + if lowercase is not None: + return lowercase + return True if self.folds_case is None else self.folds_case + def word_importance( self, label_idx: int, n_top: int = 20, filter_special: bool = True, + filter_punctuation: bool = True, + lowercase: bool | None = None, filter_sign: str = "all", exclude_words: set[str] | None = None, sample_indices: list[int] | None = None, ) -> pd.Series: """Aggregate token contributions by word across all samples for one class. - For each unique (stripped) token string that appears in the batch, - computes the mean contribution across all its occurrences. + For each unique (stripped, by default lowercased) token string that appears in + the batch, computes the mean contribution across all its occurrences. Parameters ---------- @@ -107,12 +158,29 @@ def word_importance( filter_special : bool If ``True``, skip empty strings, ``[CLS]``/``[SEP]``-style bracket tokens, and ``##subword`` wordpiece prefixes. + filter_punctuation : bool + If ``True`` (default), skip units made entirely of punctuation (``.``, ``,``, ``!!!``). + Word segmentation emits punctuation as its own unit so it stays visible and additive in + a *local* explanation; in a corpus-level ranking it is almost always noise, so it is + hidden here unless explicitly asked for. + lowercase : bool, optional + Case-fold each unit before aggregating, so ``AWFUL``, ``Awful`` and ``awful`` + form one row. ``None`` (default) derives this from the model's own tokenizer + via :meth:`resolve_lowercase` — folding on an uncased model, keeping case on a + cased one — so pass a bool only to override that. + Folding also makes the ranking comparable across backends: SHAP labels units by + slicing the *source text* (keeping its casing) while Captum/LIG labels them from + tokenizer output (already normalised), so on an uncased model the two would + otherwise report different vocabularies for one corpus. + The per-instance sentence highlight always keeps the original casing, whatever + this is set to. filter_sign : {"all", "positive", "negative"} If ``"positive"``, keep only words with positive mean contribution. If ``"negative"``, keep only words with negative mean contribution. exclude_words : set[str], optional Additional set of word strings to exclude (e.g. user-selected - stopwords or corpus words to hide). + stopwords or corpus words to hide). Matched against the same + normalised key, so under ``lowercase`` the exclusion is case-insensitive. sample_indices : list[int], optional Positional indices of the samples to include in the aggregation. When ``None`` (default) all samples are used. @@ -122,7 +190,13 @@ def word_importance( pd.Series Word → mean contribution, sorted by absolute value descending. """ - _exclude: set[str] = exclude_words or set() + + fold = self.resolve_lowercase(lowercase) + + def _key(token: str) -> str: + return token.lower() if fold else token + + _exclude: set[str] = {_key(w) for w in exclude_words} if exclude_words else set() word_contribs: dict[str, list[float]] = {} iter_data = ( ((self.token_strings[i], self.values[i]) for i in sample_indices) @@ -135,11 +209,14 @@ def word_importance( tok_clean = tok.strip() if filter_special and self._SPECIAL_RE.match(tok_clean): continue - if tok_clean in _exclude: + if filter_punctuation and is_punctuation(tok_clean): + continue + key = _key(tok_clean) + if key in _exclude: continue - if tok_clean not in word_contribs: - word_contribs[tok_clean] = [] - word_contribs[tok_clean].append(float(val)) + if key not in word_contribs: + word_contribs[key] = [] + word_contribs[key].append(float(val)) importance = pd.Series({w: float(np.mean(vs)) for w, vs in word_contribs.items()}) importance = importance.reindex(importance.abs().sort_values(ascending=False).index) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 840b7374..7c3d1b9d 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -5,43 +5,115 @@ ``get_local_contributions``, common ``__init__`` skeleton) lives in ``NlpBackend`` (see ``nlp_backend.py``). -SHAP's ``Text`` masker (``shap.maskers.Text.token_segments``) reconstructs each token as the slice -of the original string running up to the start of the next token, rather than the tokenizer's raw -WordPiece strings. A subword glued directly onto the previous piece (e.g. ``"up"`` + ``"dating "`` -for "updating") therefore carries no trailing whitespace, while the piece that actually ends a word -carries the whitespace that follows it in the source sentence. ``_aggregate_subwords`` uses that -signal to merge subwords back into whole words — matching the word-level highlights -``nlp_captum_lig_backend`` produces — and folds special ([CLS]/[SEP]-equivalent blank) tokens' -attribution into the baseline so ``base + Σ(word contributions)`` keeps SHAP's additive guarantee. +``shap.maskers.Text.token_segments`` emits *segments* of the source string rather than the +tokenizer's raw subword strings, and it does so in three different regimes: the offset-mapping path +slices each token up to the start of the next (so a segment carries the *trailing* gap text), the +slow-tokenizer fallback prepends a *leading* space to each token instead, and ``SimpleTokenizer`` +splits on a regex. ``_aggregate_subwords`` merges those segments back into whole words — matching +the word-level highlights ``nlp_captum_lig_backend`` produces — and folds special-token attribution +into the baseline so ``base + Σ(word contributions)`` keeps SHAP's additive guarantee. + +Word boundaries come from :func:`_merges`: flush on source-text whitespace *or* on a word/non-word +transition. A whitespace-only rule (what this module used previously) is wrong in two ways — it +glues punctuation onto its neighbours whenever the source has no space around it (``"superb!!!"``, +``"enjoy.Overall,I"``), and under the leading-space fallback regime it never fires at all, so an +entire sample collapses into a single "word". """ from __future__ import annotations import re +import unicodedata import numpy as np import shap from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation -# ``[CLS]``/``[SEP]``-style bracket tokens (and blanks) SHAP's ``Text`` masker reports as empty -# strings; their attribution is folded into the baseline during word aggregation. -_SPECIAL_RE = re.compile(r"^\[.*\]$|^\s*$") +# SHAP's masker reports special tokens as blank segments on its offset-mapping path; their +# attribution is folded into the baseline during word aggregation. +_BLANK_RE = re.compile(r"^\s*$") + +# Last-ditch special-token detector, used *only* when the caller cannot supply the tokenizer's own +# special set (a bare scoring callable, or SHAP's tokenizer-less ``SimpleTokenizer``). It is +# deliberately not applied otherwise: ``[...]`` also matches literal source text such as +# ``[LAUGHTER]``, which would then be silently folded into the baseline instead of shown as a word. +_BRACKET_SPECIAL_RE = re.compile(r"^\[.*\]$") + +# Scripts written without inter-word spaces. A character-class boundary test sees such a sentence as +# one uninterrupted run of word characters, so :func:`_merges` would collapse it into a single +# "word"; breaking between these characters keeps units at the character level instead — the only +# tokenizer-free option, and what ``BertPreTokenizer`` does anyway (it inserts whitespace around +# CJK). Hangul is deliberately absent: Korean *is* space-segmented, so its subword pieces must merge +# like any other alphabetic script's. +_UNSEGMENTED_SCRIPTS = ("CJK", "HIRAGANA", "KATAKANA", "THAI", "LAO", "KHMER", "MYANMAR") + + +def _is_special(segment: str, special_tokens: frozenset[str] | None) -> bool: + """True when ``segment`` is a special token whose attribution belongs in the baseline.""" + stripped = segment.strip() + if _BLANK_RE.match(stripped): + return True + if special_tokens is not None: + return stripped in special_tokens + return bool(_BRACKET_SPECIAL_RE.match(stripped)) + + +def _is_unsegmented_script(char: str) -> bool: + """True when ``char`` belongs to a script written without spaces between words.""" + return unicodedata.name(char, "").startswith(_UNSEGMENTED_SCRIPTS) + + +def _merges(buffer: str, following: str) -> bool: + """Whether the segment ``following`` continues the word currently held in ``buffer``. + + Merge only when ``buffer`` ends in a word character and ``following`` starts with one — i.e. + flush on whitespace *or* on a word/non-word transition, so ``"superb"`` + ``"!"`` splits while + ``"up"`` + ``"dating"`` merges. It reads only the segment strings, so it works for a bare + callable with a custom masker as well as for a HuggingFace pipeline, and it holds across all + three ``token_segments`` regimes. + """ + if not buffer or not following: + return False + if buffer != buffer.rstrip(): # source-text whitespace — a hard word boundary + return False + last, first = buffer[-1], following[0] + if not (last.isalnum() or last == "_") or not (first.isalnum() or first == "_"): + return False + return not (_is_unsegmented_script(last) or _is_unsegmented_script(first)) + + +def _masker_special_tokens(explainer) -> frozenset[str] | None: + """The masker tokenizer's ``all_special_tokens``, or ``None`` when no tokenizer is reachable.""" + tokenizer = getattr(getattr(explainer, "masker", None), "tokenizer", None) + specials = getattr(tokenizer, "all_special_tokens", None) + return frozenset(specials) if specials else None def _aggregate_subwords( - tokens: list[str], contributions: np.ndarray, base_values: np.ndarray + tokens: list[str], + contributions: np.ndarray, + base_values: np.ndarray, + special_tokens: frozenset[str] | None = None, ) -> tuple[list[str], np.ndarray, np.ndarray]: - """Merge SHAP's subword fragments into whole words and fold specials into the baseline. + """Merge SHAP's segments into whole words and fold specials into the baseline. + + Word boundaries come from :func:`_merges`, so punctuation becomes its own unit while genuine + subword pieces still merge. Special-token attribution is added to ``base_values`` rather than + discarded, so ``base + Σ(word contributions)`` still equals the model output. Parameters ---------- tokens : list[str] - Token strings for one sample (length ``seq``), as produced by SHAP's text masker. + Segment strings for one sample (length ``seq``), as produced by SHAP's text masker. contributions : np.ndarray - Per-token contributions, shape ``(seq, n_classes)``. + Per-segment contributions, shape ``(seq, n_classes)``. base_values : np.ndarray Baseline SHAP values for the sample, shape ``(n_classes,)``. + special_tokens : frozenset[str] or None + The masker tokenizer's ``all_special_tokens``. When given, non-blank specials are detected + by membership — model-derived, not guessed. When ``None`` (a bare scoring callable, or a + tokenizer-less masker), the bracket regex :data:`_BRACKET_SPECIAL_RE` stands in. Returns ------- @@ -62,14 +134,17 @@ def _flush() -> None: word_rows.append(buffer_row) buffer_text, buffer_row = "", None - for tok, row in zip(tokens, contributions, strict=True): - if _SPECIAL_RE.match(tok.strip()): + for i, (tok, row) in enumerate(zip(tokens, contributions, strict=True)): + if _is_special(tok, special_tokens): _flush() base = base + row continue buffer_text += tok buffer_row = row.astype(float).copy() if buffer_row is None else buffer_row + row - if tok != tok.rstrip(): # trailing whitespace in the source text -> word boundary + # A special token never continues a word (it is blank, or bracketed), so the raw next + # segment is lookahead enough — no need to skip over specials to find the next content one. + following = tokens[i + 1] if i + 1 < len(tokens) else "" + if not _merges(buffer_text, following): _flush() _flush() @@ -129,6 +204,10 @@ def __init__( # scoring callable (external-head models expose one via ``TextModel.shap_masker``). self.explainer = shap.Explainer(model, masker=self.masker) + # Resolved once: the masker's tokenizer is what decides which segments are special. ``None`` + # when no tokenizer is reachable (bare callable / ``SimpleTokenizer``) — see ``_is_special``. + self._special_tokens = _masker_special_tokens(self.explainer) + def run_explainer(self, x) -> NlpRawExplanation: """Run the SHAP text explainer and return all explanation components. @@ -155,7 +234,12 @@ def run_explainer(self, x) -> NlpRawExplanation: for tokens, values, base in zip( shap_explanation.data, shap_explanation.values, shap_explanation.base_values, strict=True ): - words, word_contribs, word_base = _aggregate_subwords(list(tokens), np.asarray(values), np.asarray(base)) + words, word_contribs, word_base = _aggregate_subwords( + list(tokens), + np.asarray(values), + np.asarray(base), + special_tokens=getattr(self, "_special_tokens", None), + ) data.append(words) contributions.append(word_contribs) base_values.append(word_base) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 4fd901fe..0d6482ce 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -17,6 +17,7 @@ import threading from dataclasses import dataclass from pathlib import Path +from typing import cast import numpy as np import pandas as pd @@ -30,7 +31,7 @@ from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever -from shapash.model.base import SupportsEmbeddings, TextModel, has_capabilities +from shapash.model.base import SupportsEmbeddings, SupportsTokenization, TextModel, has_capabilities from shapash.model.hf import HFPipelineModel from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp @@ -311,6 +312,10 @@ def compile( self.contributions.label_names = self.label_names self.contributions.index = self.texts.index + # Set here rather than in the backend so a cache written before this existed still gets + # it (the branch above may have unpickled ``computed``), and so every backend inherits + # one answer — the question is about the *model*, not about how contributions were computed. + self.contributions.folds_case = self._folds_case() self._data_hash = new_hash if y_true is not None: @@ -689,6 +694,18 @@ def _require_text_model(self) -> TextModel: raise RuntimeError("Live prediction requires a TextModel-backed explainer (unavailable on a snapshot).") return text_model + def _folds_case(self) -> bool | None: + """Whether this model's tokenizer normalises case away, or ``None`` when it cannot be asked. + + ``None`` for a model with no tokenization capability (a bare LIME ``classifier_fn``, or a + prediction-only adapter) — the honest answer, which + :meth:`~shapash.backend.nlp_backend.NlpContributions.resolve_lowercase` turns into a default. + """ + text_model = getattr(self, "_text_model", None) + if text_model is None or not has_capabilities(text_model, SupportsTokenization): + return None + return cast("SupportsTokenization", text_model).folds_case() + def _predict(self, text_list: list[str]) -> pd.DataFrame: """Run the pipeline and return a unified DataFrame of predictions and probabilities. diff --git a/shapash/model/base.py b/shapash/model/base.py index 403462b4..590f6289 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -168,6 +168,42 @@ def _probe_word_start_marker(self) -> str | None: return marker return None + def folds_case(self) -> bool: + """Whether this tokenizer normalises case away before the model ever sees the text. + + Probed once from :meth:`tokenize` (not from ``do_lower_case``, a normalizer class, or a + hard-coded model list) and memoized on the instance, for the same reason as + :meth:`word_start_marker`: the question is what the tokenizer *does*, and every family + records it somewhere different — ``BertNormalizer(lowercase=True)``, a ``Lowercase`` step + inside a normalizer ``Sequence``, or a legacy ``do_lower_case`` flag. + + Callers use this to decide whether ``AWFUL`` and ``awful`` are one unit or two. On an + uncased model they encode to the *same input ids*, so the model cannot tell them apart and + any difference in their attributions comes from context alone — treating them as separate + units splits one word into rare variants. On a cased model they are genuinely different + inputs and must stay apart. + + Returns + ------- + bool + ``True`` when upper- and lower-case text tokenize identically. + """ + cached = getattr(self, "_folds_case_cache", _UNRESOLVED) + if cached is _UNRESOLVED: + folds = self._probe_folds_case() + self._folds_case_cache = folds + return folds + return cast("bool", cached) + + def _probe_folds_case(self) -> bool: + """Tokenize :data:`_MARKER_PROBE_TEXT` in both cases and report whether they agree.""" + try: + lowered = self.tokenize(_MARKER_PROBE_TEXT) + uppered = self.tokenize(_MARKER_PROBE_TEXT.upper()) + except Exception: # noqa: BLE001 - a tokenizer that cannot run the probe is treated as cased + return False + return lowered == uppered + def is_substitutable(self, token: str) -> bool: """Whether ``token`` is a standalone word that can be swapped or removed cleanly. diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 1e80bc6a..bca8ed7c 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -20,6 +20,7 @@ from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate +from shapash.backend.nlp_backend import is_punctuation from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_word_importance import plot_word_importance from shapash.webapp.nlp_components import ( @@ -241,12 +242,17 @@ def _build_layout(self) -> None: ) # ── Corpus word list for the exclusion multi-select ──────────── + # Normalised exactly as ``word_importance`` keys its units, via the same resolver, so an + # entry here always matches a bar in the chart. On an uncased model that folds + # AWFUL/Awful/awful into one entry; on a cased model it leaves all three, because there + # they are genuinely different inputs. + _fold_words = contrib.resolve_lowercase() all_words = sorted( { - tok.strip() + tok.strip().lower() if _fold_words else tok.strip() for sample_tokens in contrib.token_strings for tok in sample_tokens - if tok.strip() and not _SPECIAL_RE.match(tok.strip()) + if tok.strip() and not _SPECIAL_RE.match(tok.strip()) and not is_punctuation(tok) } ) word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in all_words] @@ -794,7 +800,13 @@ def _compose_indices(selected_indices, error_cell, errors_only=False): ], ) def update_global_importance( - label_idx, topk, sign_filter, exclude_words_list, selected_indices, error_cell, errors_only + label_idx, + topk, + sign_filter, + exclude_words_list, + selected_indices, + error_cell, + errors_only, ): if label_idx is None: raise PreventUpdate @@ -803,6 +815,10 @@ def update_global_importance( label_idx=int(label_idx), n_top=int(topk or 20), filter_sign=sign_filter or "all", + # Punctuation is its own unit since word segmentation splits on word/non-word + # boundaries; corpus-wide its mean contribution averages to ~0, so it is noise here. + # Per-instance punctuation contributions stay visible in the Sentence Highlight panel. + filter_punctuation=True, exclude_words=set(exclude_words_list or []) or None, sample_indices=effective_indices, ) diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py new file mode 100644 index 00000000..a34c3335 --- /dev/null +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -0,0 +1,35 @@ +"""Unit tests for the shared NLP backend layer — the punctuation helper. + +``NlpContributions.word_importance`` filtering is covered in ``tests/unit_tests/nlp``; this file +covers :func:`~shapash.backend.nlp_backend.is_punctuation` itself, which word segmentation now +makes load-bearing (punctuation is emitted as its own unit rather than glued to a neighbour). +""" + +import unittest + +from shapash.backend.nlp_backend import is_punctuation + + +class TestIsPunctuation(unittest.TestCase): + def test_pure_punctuation_units(self): + for unit in [".", ",", "!", "!!!", "?!", "--", "\u2026", "(", ")", "\u201c", "/"]: + with self.subTest(unit=unit): + self.assertTrue(is_punctuation(unit)) + + def test_words_are_not_punctuation(self): + for unit in ["great", "don't", "state-of-the-art", "10/10", "u.s.", "_", "3"]: + with self.subTest(unit=unit): + self.assertFalse(is_punctuation(unit)) + + def test_blank_is_not_punctuation(self): + # Blanks are the special-token concern, handled by ``filter_special`` — not this helper. + for unit in ["", " ", "\n"]: + with self.subTest(unit=unit): + self.assertFalse(is_punctuation(unit)) + + def test_surrounding_whitespace_is_ignored(self): + self.assertTrue(is_punctuation(" ! ")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index 27a417bf..3df9112b 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -21,15 +21,13 @@ def test_backend_name_attribute(self): class TestAggregateSubwords(unittest.TestCase): - """``_aggregate_subwords`` merges SHAP's whitespace-delimited subword fragments into words.""" + """``_aggregate_subwords`` merges SHAP's segments into words at word/non-word boundaries.""" def test_merges_subwords_and_drops_specials(self): # SHAP's Text masker: a subword glued to the previous piece carries no trailing space; # the piece that ends the word does (see shap.maskers.Text.token_segments). tokens = ["", "i ", "am ", "hap", "py ", ""] - contribs = np.array( - [[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float - ) + contribs = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float) base = np.array([10.0, 20.0]) words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) @@ -40,16 +38,133 @@ def test_merges_subwords_and_drops_specials(self): # Leading/trailing blank (CLS/SEP-equivalent) attribution folded into the baseline. np.testing.assert_allclose(new_base, [10.0 + 1.0 + 6.0, 20.0 + 0.0 + 5.0]) - def test_merges_punctuation_glued_contraction(self): - # "don't" is tokenized as "don" + "'" + "t " — none carry inter-token whitespace. + def test_splits_contraction_at_the_apostrophe(self): + # "don't" is segmented as "don" + "'" + "t " — none carry inter-token whitespace, so the + # old whitespace-only rule glued them. The word/non-word rule splits at the apostrophe, + # matching the units the tokenizer's own pre-tokenizer produces (and hence the LIG backend). tokens = ["", "i ", "don", "'", "t ", "feel ", "sad", ""] contribs = np.arange(8 * 2, dtype=float).reshape(8, 2) base = np.zeros(2) words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) - self.assertEqual(words, ["i", "don't", "feel", "sad"]) - np.testing.assert_allclose(word_contribs[1], contribs[2] + contribs[3] + contribs[4]) + self.assertEqual(words, ["i", "don", "'", "t", "feel", "sad"]) + np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) + + def test_punctuation_is_not_glued_to_neighbouring_words(self): + # Regression: real ``shap.maskers.Text`` output for + # "The acting was superb!!! I really did enjoy.Overall,I recommend it." — the old rule + # produced "superb!!!" and "enjoy.Overall,I" because only whitespace ended a word. + tokens = [ + "", + "The ", + "acting ", + "was ", + "superb", + "!", + "!", + "! ", + "I ", + "really ", + "did ", + "enjoy", + ".", + "Overall", + ",", + "I ", + "recommend ", + "it", + ".", + "", + ] + contribs = np.arange(len(tokens), dtype=float).reshape(-1, 1) + + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(1)) + + self.assertEqual( + words, + [ + "The", + "acting", + "was", + "superb", + "!", + "!", + "!", + "I", + "really", + "did", + "enjoy", + ".", + "Overall", + ",", + "I", + "recommend", + "it", + ".", + ], + ) + np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), contribs.sum(axis=0)) + + def test_leading_space_regime_does_not_collapse_the_sample(self): + # SHAP's slow-tokenizer fallback prepends a *leading* space to each token instead of + # carrying trailing gap text (shap/maskers/_text.py). Under the old trailing-whitespace + # rule nothing ever flushed, so a whole sample became one "word". + tokens = ["", "The", " acting", " was", " superb", "!", "!", "!", " I", " enjoy", ""] + contribs = np.arange(len(tokens), dtype=float).reshape(-1, 1) + + words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + + self.assertEqual(words, ["The", "acting", "was", "superb", "!", "!", "!", "I", "enjoy"]) + + def test_subwords_merge_in_both_segment_regimes(self): + for tokens in (["up", "dating "], [" up", "dating"]): + with self.subTest(tokens=tokens): + contribs = np.array([[1.0], [2.0]]) + words, word_contribs, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + self.assertEqual(words, ["updating"]) + np.testing.assert_allclose(word_contribs, [[3.0]]) + + def test_unsegmented_script_does_not_collapse_into_one_word(self): + # CJK has no inter-word spaces, so a character-class rule would see one long word run. + tokens = ["\u6211", "\u559c", "\u6b22"] + contribs = np.arange(3, dtype=float).reshape(-1, 1) + words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + self.assertEqual(words, ["\u6211", "\u559c", "\u6b22"]) + + def test_korean_subwords_still_merge(self): + # Korean *is* space-segmented, so its subword pieces must join like any other script's — + # Hangul is deliberately excluded from the unsegmented-script list. + tokens = ["\uc601\ud654", "\ub294 ", "\uc88b", "\uc558\ub2e4"] + contribs = np.arange(4, dtype=float).reshape(-1, 1) + words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + self.assertEqual(words, ["\uc601\ud654\ub294", "\uc88b\uc558\ub2e4"]) + + def test_bracket_text_folds_into_baseline_only_without_a_special_set(self): + # With the tokenizer's own special set, literal "[LAUGHTER]" in the source is a real word. + tokens = ["ha ", "[LAUGHTER] ", "good "] + contribs = np.array([[1.0], [2.0], [3.0]]) + + words, _, new_base = _aggregate_subwords( + tokens, contribs, np.zeros(1), special_tokens=frozenset({"[CLS]", "[SEP]"}) + ) + self.assertEqual(words, ["ha", "[LAUGHTER]", "good"]) + np.testing.assert_allclose(new_base, [0.0]) + + # Without one (bare callable), the bracket regex still guards against leaking [CLS]/[SEP]. + words, _, new_base = _aggregate_subwords(["[CLS] ", "good ", "[SEP] "], contribs, np.zeros(1)) + self.assertEqual(words, ["good"]) + np.testing.assert_allclose(new_base, [4.0]) + + def test_declared_special_token_folds_into_baseline(self): + tokens = ["<s> ", "good ", "</s> "] + contribs = np.array([[1.0], [2.0], [3.0]]) + words, word_contribs, new_base = _aggregate_subwords( + tokens, contribs, np.zeros(1), special_tokens=frozenset({"<s>", "</s>"}) + ) + self.assertEqual(words, ["good"]) + np.testing.assert_allclose(word_contribs, [[2.0]]) + np.testing.assert_allclose(new_base, [4.0]) def test_preserves_completeness(self): tokens = ["", "great ", "mov", "ie ", ""] diff --git a/tests/unit_tests/model/test_model_base.py b/tests/unit_tests/model/test_model_base.py index 9a265144..f6bd6870 100644 --- a/tests/unit_tests/model/test_model_base.py +++ b/tests/unit_tests/model/test_model_base.py @@ -133,6 +133,46 @@ def tokenize(self, text): self.assertTrue(model.is_substitutable("happy")) +class _CasingModel(_SchemeModel): + """Whitespace tokenizer that optionally case-folds, as an uncased checkpoint's normalizer does.""" + + def __init__(self, lowercase=False): + super().__init__() + self.lowercase = lowercase + + def tokenize(self, text): + return super().tokenize(text.lower() if self.lowercase else text) + + +class TestFoldsCase(unittest.TestCase): + """Casing is probed from what the tokenizer emits — no real family records it the same way. + + RoBERTa and CamemBERTv2 both report ``normalizer=None`` while a BERT-family uncased checkpoint + hides it in ``BertNormalizer(lowercase=True)``, so reading attributes would misclassify them. + """ + + def test_detects_uncased_tokenizer(self): + self.assertTrue(_CasingModel(lowercase=True).folds_case()) + + def test_detects_cased_tokenizer(self): + self.assertFalse(_CasingModel(lowercase=False).folds_case()) + + def test_probe_runs_once_and_is_memoized(self): + model = _CasingModel(lowercase=True) + for _ in range(5): + model.folds_case() + # Two calls for the single probe (lower + upper), then memoized. + self.assertEqual(model.tokenize_calls, 2) + + def test_unprobeable_tokenizer_is_treated_as_cased(self): + # Guessing "uncased" would merge units of a model we know nothing about; keep them apart. + class _Broken(_CasingModel): + def tokenize(self, text): + raise RuntimeError("tokenizer unavailable") + + self.assertFalse(_Broken().folds_case()) + + class TestIsSubstitutable(unittest.TestCase): """Word-hood must follow the model's own scheme — a single ``isalpha`` rule is wrong for two of three. diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 59b56a89..2ecc24b6 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -45,10 +45,7 @@ def _make_contributions() -> NlpContributions: ["", "this", "is", "terrible", "and", "sad", ""], ["", "what", "a", "wonderful", "day", ""], ] - values = [ - rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) - for t in token_strings - ] + values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) return NlpContributions( token_strings=token_strings, @@ -109,6 +106,118 @@ def test_word_importance_keeps_special_when_disabled(self): # Empty strings (BOS/EOS) should now be present self.assertIn("", imp.index) + def test_word_importance_hides_punctuation_by_default(self): + contrib = NlpContributions( + token_strings=[["great", "!", "!", "movie", ".", ","]], + values=[np.array([[3.0], [9.0], [9.0], [2.0], [8.0], [7.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["great", "movie"]) + + def test_word_importance_keeps_punctuation_when_asked(self): + contrib = NlpContributions( + token_strings=[["great", "!", "!"]], + values=[np.array([[3.0], [9.0], [9.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20, filter_punctuation=False) + self.assertIn("!", imp.index) + # Punctuation kept means it can outrank real words, which is why it is hidden by default. + self.assertEqual(imp.index[0], "!") + + def test_word_importance_keeps_words_containing_punctuation(self): + # Only *pure* punctuation units are dropped — a hyphenated or apostrophised word stays. + contrib = NlpContributions( + token_strings=[["state-of-the-art", "don't", "-"]], + values=[np.array([[3.0], [2.0], [9.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["don't", "state-of-the-art"]) + + def test_resolve_lowercase_follows_the_model(self): + contrib = _make_contributions() + for folds_case, expected in ((True, True), (False, False), (None, True)): + with self.subTest(folds_case=folds_case): + contrib.folds_case = folds_case + self.assertEqual(contrib.resolve_lowercase(), expected) + + def test_resolve_lowercase_explicit_argument_wins(self): + contrib = _make_contributions() + contrib.folds_case = True + self.assertFalse(contrib.resolve_lowercase(False)) + contrib.folds_case = False + self.assertTrue(contrib.resolve_lowercase(True)) + + def test_word_importance_keeps_case_on_a_cased_model(self): + # A cased tokenizer encodes AWFUL and awful to *different* ids, so they are different + # inputs with genuinely different attributions — merging them would hide that. + contrib = NlpContributions( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + folds_case=False, + ) + imp = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) + + def test_word_importance_merges_case_on_an_uncased_model(self): + # An uncased tokenizer maps both spellings to one input id — the model cannot tell them + # apart, so they must not appear as two rows. + contrib = NlpContributions( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + folds_case=True, + ) + imp = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(list(imp.index), ["awful"]) + self.assertAlmostEqual(imp["awful"], -6.0) + + def test_word_importance_merges_case_variants_by_default(self): + # One row per word, averaged over every casing — not three rows of one occurrence each. + contrib = NlpContributions( + token_strings=[["AWFUL", "Awful", "awful", "good"]], + values=[np.array([[-9.0], [-3.0], [-3.0], [1.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["awful", "good"]) + self.assertAlmostEqual(imp["awful"], -5.0) + + def test_word_importance_case_fragmentation_lets_rare_variants_outrank(self): + # The reason lowercasing is the default: a single capitalised occurrence keeps its own + # extreme value instead of being averaged into the 3 common ones, and tops the ranking. + contrib = NlpContributions( + token_strings=[["TERRIBLE", *["terrible"] * 9, "dull"]], + values=[np.array([[-9.0], *[[-0.1]] * 9, [-2.0]])], + base_values=np.zeros((1, 1)), + ) + cased = contrib.word_importance(label_idx=0, n_top=20, lowercase=False) + self.assertEqual(cased.index[0], "TERRIBLE") + folded = contrib.word_importance(label_idx=0, n_top=20) + self.assertEqual(folded.index[0], "dull") + + def test_word_importance_keeps_case_when_disabled(self): + contrib = NlpContributions( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20, lowercase=False) + self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) + + def test_word_importance_exclusion_is_case_insensitive_when_folding(self): + # The webapp's dropdown offers lowercase entries; excluding one must drop every casing. + contrib = NlpContributions( + token_strings=[["AWFUL", "awful", "good"]], + values=[np.array([[-9.0], [-3.0], [1.0]])], + base_values=np.zeros((1, 1)), + ) + imp = contrib.word_importance(label_idx=0, n_top=20, exclude_words={"AwFuL"}) + self.assertEqual(list(imp.index), ["good"]) + def test_word_importance_respects_n_top(self): imp = self.contrib.word_importance(label_idx=1, n_top=3) self.assertLessEqual(len(imp), 3) @@ -146,9 +255,7 @@ def test_word_importance_exclude_words(self): if len(imp_full) == 0: return word_to_exclude = imp_full.index[0] - imp_filtered = self.contrib.word_importance( - label_idx=1, filter_special=True, exclude_words={word_to_exclude} - ) + imp_filtered = self.contrib.word_importance(label_idx=1, filter_special=True, exclude_words={word_to_exclude}) self.assertNotIn(word_to_exclude, imp_filtered.index) def test_word_importance_exclude_words_empty_set(self): @@ -407,6 +514,24 @@ def test_text_plot_returns_figure(self): fig = self.xpl.text_plot(pos=0, label_idx=1) self.assertIsInstance(fig, go.Figure) + def test_folds_case_is_none_without_a_tokenizing_model(self): + # A bare classifier_fn has no tokenizer to ask; None is the honest answer, not a guess. + self.assertIsNone(self.xpl._folds_case()) + + def test_folds_case_reads_the_model_capability(self): + class _Uncased(TextModel, SupportsTokenization): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + def tokenize(self, text): + return text.lower().split() + + def detokenize(self, tokens): + return " ".join(tokens) + + self.xpl._text_model = _Uncased(label_names=["neg", "pos"]) + self.assertTrue(self.xpl._folds_case()) + def test_text_plot_all_samples(self): for pos in range(3): fig = self.xpl.text_plot(pos=pos, label_idx=0) @@ -656,7 +781,9 @@ def test_unknown_generator_raises(self): def test_no_generators_without_text_model(self): # A plain callable is neither a TextModel nor a pipeline → no generators, empty selector. - xpl = NlpExplainer(lambda texts: np.tile([0.5, 0.5], (len(texts), 1)), label_names=["neg", "pos"], backend=object()) + xpl = NlpExplainer( + lambda texts: np.tile([0.5, 0.5], (len(texts), 1)), label_names=["neg", "pos"], backend=object() + ) self.assertEqual(xpl.available_cf_generators(), []) self.assertEqual(xpl.cf_config_spec(), {}) self.assertIsNone(xpl.cf_generator) @@ -823,7 +950,6 @@ def test_cell_from_click_none_on_empty_or_unknown(self): self.assertIsNone(_cell_from_click({"points": []}, name_to_idx)) self.assertIsNone(_cell_from_click({"points": [{"x": "??", "y": "??"}]}, name_to_idx)) - # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -913,7 +1039,6 @@ def _make_lime_backend() -> NlpLimeBackend: class TestNlpLimeBackend(unittest.TestCase): - def setUp(self): self.backend = _make_lime_backend() @@ -970,10 +1095,7 @@ def array_model(texts): def test_classifier_fn_converts_hf_pipeline_format(self): # HuggingFace pipeline with return_all_scores=True returns list[list[dict]]. def hf_model(texts): - return [ - [{"label": name, "score": 1.0 / N_CLASSES} for name in LABEL_NAMES] - for _ in texts - ] + return [[{"label": name, "score": 1.0 / N_CLASSES} for name in LABEL_NAMES] for _ in texts] backend = NlpLimeBackend(hf_model, label_names=LABEL_NAMES) result = backend._classifier_fn(["hello", "world"]) @@ -1052,7 +1174,6 @@ def test_get_local_contributions_subset(self): class TestNlpExplainerWithLimeBackend(unittest.TestCase): - def _make_explainer_lime(self, compiled: bool = True) -> NlpExplainer: """NlpExplainer backed by NlpLimeBackend, with synthetic contributions.""" xpl = _make_explainer(compiled=compiled) From b6ee040709d57293ffc98b6ed2a55c77214b22d2 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 20 Aug 2026 18:07:17 +0200 Subject: [PATCH 093/144] feat(diagnostics): detect label noise by confident learning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Label Noise" panel to the NLP webapp, answering the question error analysis leaves open: whether the label is wrong rather than the model. New `shapash/compute/diagnostics/` axis, peer to `generators/` and `retrieval/`: - `label_noise.py` — confident learning (Northcutt et al., JAIR 2021) over the probabilities and ground truth `compile()` already holds. Pure numpy, no model access, no new dependency. Returns a ranked list of probably- mislabelled samples plus an estimated class-to-class noise matrix. - `label_probe.py` — a model-independent second opinion. Confident learning cannot tell a wrong *label* from a confidently-wrong *model*; both look like "the model disagrees with the label". A TF-IDF + logistic-regression classifier fit on the reference corpus reports how much probability it puts on the label a flagged row already carries. High means the corpus backs the label and the model is the likelier culprit. An earlier revision corroborated flagged rows with their nearest neighbours instead. Measured over 500 rows, that column matched the model's prediction 98.6% of the time and backed the *wrong* prediction on 87% of its errors: retrieval ranks in the model's decision space, so anything drawn from it restates the prediction by construction. Representation neighbours remain in the Similar Examples panel, which follows the current datapoint. `CAP_LABELS` is the first *data* capability — resolved from the view, not the engine, so the panel also mounts on a snapshot with no live model. The probe gates separately on `can_probe_labels()`, since it needs only labelled text and works for a prediction-only pipeline that cannot embed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- demo/serve_nlp.py | 5 +- shapash/compute/diagnostics/__init__.py | 34 ++ shapash/compute/diagnostics/label_noise.py | 372 ++++++++++++++++++ shapash/compute/diagnostics/label_probe.py | 183 +++++++++ shapash/explainer/interactive.py | 15 + shapash/explainer/nlp_explainer.py | 144 ++++++- shapash/plots/plot_noise_matrix.py | 94 +++++ shapash/webapp/nlp_app.py | 7 + shapash/webapp/nlp_components/__init__.py | 2 + shapash/webapp/nlp_components/base.py | 9 +- shapash/webapp/nlp_components/label_noise.py | 273 +++++++++++++ tests/integration_tests/test_nlp_whatif.py | 74 ++++ .../compute/diagnostics/test_label_noise.py | 296 ++++++++++++++ .../compute/diagnostics/test_label_probe.py | 141 +++++++ tests/unit_tests/nlp/test_nlp_prototype.py | 205 ++++++++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 314 ++++++++++++++- 16 files changed, 2164 insertions(+), 4 deletions(-) create mode 100644 shapash/compute/diagnostics/__init__.py create mode 100644 shapash/compute/diagnostics/label_noise.py create mode 100644 shapash/compute/diagnostics/label_probe.py create mode 100644 shapash/plots/plot_noise_matrix.py create mode 100644 shapash/webapp/nlp_components/label_noise.py create mode 100644 tests/unit_tests/compute/diagnostics/test_label_noise.py create mode 100644 tests/unit_tests/compute/diagnostics/test_label_probe.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 44052e41..acb0fd94 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -961,12 +961,15 @@ def main() -> None: xpl.find_similar(sentences[0], top_k=1) logger.info( - "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s", + "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s" + " | can_detect_label_noise=%s | can_probe_labels=%s", config.attribution, ",".join(name for name, _ in xpl.available_cf_generators()) or "none", xpl.can_edit(), xpl.can_counterfactual(), xpl.can_find_similar(), + xpl.can_detect_label_noise(), + xpl.can_probe_labels(), ) logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) diff --git a/shapash/compute/diagnostics/__init__.py b/shapash/compute/diagnostics/__init__.py new file mode 100644 index 00000000..8d3166e7 --- /dev/null +++ b/shapash/compute/diagnostics/__init__.py @@ -0,0 +1,34 @@ +"""Dataset-quality diagnostics — what is wrong with the *data*, not with the model. + +Where ``compute/generators/`` perturbs an input and ``compute/retrieval/`` looks outward to a +reference corpus, this package audits the labelled corpus itself. The first method is +:func:`~shapash.compute.diagnostics.label_noise.detect_label_issues` — confident learning +(Northcutt et al., JAIR 2021) over the predicted probabilities an explainer has already computed — +paired with :class:`~shapash.compute.diagnostics.label_probe.LabelProbe`, the model-independent +second opinion that says whether a flagged row is a label error or just a model error. + +This is a peer axis to ``generators`` and ``retrieval``, seeded by the NLP prototype. The master +plan reserves ``compute/analyses/{local,global,diagnostics}`` for the interpreter hierarchy and +assigns the design of that namespace to the tabular modality; when it lands, this package is +expected to move under it unchanged. Sitting here in the meantime keeps NLP from originating a +namespace it does not own. +""" + +from __future__ import annotations + +from shapash.compute.diagnostics.label_noise import ( + LabelIssue, + LabelNoiseReport, + detect_label_issues, + has_usable_probabilities, +) +from shapash.compute.diagnostics.label_probe import LabelProbe, ProbeVerdict + +__all__ = [ + "LabelIssue", + "LabelNoiseReport", + "LabelProbe", + "ProbeVerdict", + "detect_label_issues", + "has_usable_probabilities", +] diff --git a/shapash/compute/diagnostics/label_noise.py b/shapash/compute/diagnostics/label_noise.py new file mode 100644 index 00000000..9a488434 --- /dev/null +++ b/shapash/compute/diagnostics/label_noise.py @@ -0,0 +1,372 @@ +"""Label-noise detection by confident learning. + +Error analysis usually bottoms out in "the label is wrong". Confident learning (Northcutt, Jiang & +Chuang, *Confident Learning: Estimating Uncertainty in Dataset Labels*, JAIR 2021 — the algorithm +behind ``cleanlab``) turns a matrix of predicted probabilities plus the given labels into two +things: a **ranked list of probably-mislabelled samples**, and an **estimated noise matrix** saying +which class pairs are contaminated and by how much. + +It needs nothing an explainer has not already computed: the ``(n_samples, n_classes)`` probability +matrix and the ground-truth labels. No retraining, no cross-validation, no model access — which is +also why this module is pure numpy and holds no reference to a model or a backend. + +Implemented in-house rather than by depending on ``cleanlab``: the core is the ~150 lines below, and +a new dependency would have to keep its ``scikit-learn`` pin inside this project's narrow +``>=1.8,<1.9`` window for a routine the project can own outright. (Same call as the retrieval +package's, which reimplemented Captum's ``SimilarityInfluence``.) + +Precondition +------------ +Confident learning's guarantees assume the probabilities are **out-of-sample** for the labelled +data — produced by a model that did not train on these rows (a held-out split, or cross-validated +predictions). Applied to a model's own training data, self-confidence is inflated by memorisation +and the method systematically *under*-reports noise. Nothing here can detect that, so it is the +caller's precondition to satisfy; :func:`detect_label_issues` will happily run either way. +""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from dataclasses import dataclass, field + +import numpy as np +import pandas as pd + +from shapash.compute.diagnostics.label_probe import ProbeVerdict + +logger = logging.getLogger(__name__) + +#: Ranking methods accepted by ``detect_label_issues(score=...)``. +SCORE_METHODS = ("self_confidence", "normalized_margin") + +# Row sums are checked only loosely: a caller may legitimately pass calibrated or temperature-scaled +# scores that do not sum to exactly one. +_PROB_SUM_TOLERANCE = 1e-3 + +# Slack on the "clears its class threshold" test. A threshold is the *mean* of the very values it is +# compared against, so when a class's samples share a self-confidence the mean lands a few ulps off +# it and an exact ``>=`` rejects every one of them — the class then abstains wholesale and vanishes +# from the confident joint. The slack is far below any meaningful probability difference. +_THRESHOLD_EPSILON = 1e-9 + + +@dataclass(frozen=True) +class LabelIssue: + """One sample whose given label confident learning believes is wrong. + + Attributes + ---------- + index : int + Position of the sample within the analysed corpus. + text : str + The sample's text. + given_label : str + The label the dataset carries. + suggested_label : str + The label the probabilities point to instead. + given_prob : float + Predicted probability of ``given_label``. + suggested_prob : float + Predicted probability of ``suggested_label``. + score : float + Label-quality score, **lower means more likely mislabelled**. Its meaning depends on the + ``score`` method requested (see :func:`detect_label_issues`). + probe : ProbeVerdict or None + A model-independent second opinion on ``given_label``, attached by a caller holding a + :class:`~shapash.compute.diagnostics.label_probe.LabelProbe`; ``None`` when none was + available. Reported beside ``score``, never folded into it — and it is the only field here + that does not derive from the audited model, which is exactly what makes it able to + distinguish a label error from a confidently-wrong model. See + :mod:`shapash.compute.diagnostics.label_probe`. + """ + + index: int + text: str + given_label: str + suggested_label: str + given_prob: float + suggested_prob: float + score: float + probe: ProbeVerdict | None = field(default=None) + + +@dataclass(frozen=True) +class LabelNoiseReport: + """The outcome of a confident-learning pass over a labelled corpus. + + Attributes + ---------- + issues : list of LabelIssue + Flagged samples ordered worst-first, truncated to the requested ``top_n``. + noise_matrix : numpy.ndarray + Calibrated joint distribution of shape ``(n_classes, n_classes)`` summing to 1, where + ``noise_matrix[i, j]`` estimates the fraction of the whole corpus that is *labelled* ``i`` + but truly ``j``. The diagonal is the correctly-labelled mass; the off-diagonal is the noise. + thresholds : numpy.ndarray + Per-class confidence thresholds of shape ``(n_classes,)`` — the average self-confidence of + the samples carrying each label. ``inf`` for a class with no labelled samples. + label_names : list of str + Class names, ordered to match the probability columns and both matrix axes. + n_samples : int + Size of the analysed corpus. + n_issues : int + Total number of flagged samples **before** ``top_n`` truncation. + """ + + issues: list[LabelIssue] + noise_matrix: np.ndarray + thresholds: np.ndarray + label_names: list[str] + n_samples: int + n_issues: int + + @property + def noise_rate(self) -> float: + """Estimated fraction of the corpus that is mislabelled (the off-diagonal mass).""" + return float(self.noise_matrix.sum() - np.trace(self.noise_matrix)) + + +def has_usable_probabilities(y_prob: pd.DataFrame | None) -> bool: + """Whether a probability frame can support confident learning. + + The single predicate behind both the explainer's ``can_detect_label_noise()`` and the webapp's + ``data:labels`` capability, so the two can never drift apart. + + Parameters + ---------- + y_prob : pandas.DataFrame or None + A compiled explainer's per-class probabilities. + + Returns + ------- + bool + ``True`` when the frame holds one column per class. ``False`` for ``None`` and for the + single-column frame the raw-pipeline prediction path produces, which carries only the + *winning* class's confidence — the losing classes' probabilities are exactly what confident + learning needs, so there is nothing to work with. + """ + if y_prob is None: + return False + shape = getattr(y_prob, "shape", None) + return bool(shape is not None and len(shape) == 2 and shape[1] >= 2) + + +def detect_label_issues( + probs: np.ndarray, + labels: Sequence[str], + texts: Sequence[str], + label_names: list[str], + *, + top_n: int = 50, + score: str = "self_confidence", +) -> LabelNoiseReport: + """Rank probably-mislabelled samples and estimate the corpus's noise matrix. + + Runs the four steps of confident learning: per-class confidence thresholds, the confident joint, + calibration to a joint distribution, then per-cell pruning by estimated noise rate. + + Parameters + ---------- + probs : numpy.ndarray + Predicted probabilities of shape ``(n_samples, n_classes)``, columns ordered like + ``label_names``. Must be **out-of-sample** for the labelled data — see the module docstring. + labels : sequence of str + The given (possibly noisy) label of each sample. Every value must appear in ``label_names``. + texts : sequence of str + The samples' texts, aligned with ``labels``; carried onto each :class:`LabelIssue`. + label_names : list of str + Class names in probability-column order. + top_n : int, optional + Maximum number of issues to return. The full count is still reported as + :attr:`LabelNoiseReport.n_issues`. + score : {"self_confidence", "normalized_margin"}, optional + How to rank the flagged samples. ``self_confidence`` is the predicted probability of the + given label. ``normalized_margin`` subtracts the best competing class's probability, so it + punishes a confident *alternative* rather than mere uncertainty. Lower is worse either way. + + Returns + ------- + LabelNoiseReport + + Raises + ------ + ValueError + If ``probs`` is not 2-D, if there are fewer than two classes, if ``labels``/``texts``/ + ``label_names`` lengths disagree with ``probs``, if a label is absent from ``label_names``, + or if ``score`` is not a recognised method. + + Notes + ----- + A sample is only ever flagged toward a class the model scores *above* the given label + (``margin > 0``). Confident learning's per-cell counts are estimates and can exceed the number + of samples that actually favour the alternative; without this guard the tail of a cell would + "suggest" a class the model itself ranks lower than the label already on the row. + + Examples + -------- + >>> report = detect_label_issues(probs, y_true, texts, ["joy", "surprise"], top_n=10) + >>> for issue in report.issues: + ... print(issue.score, issue.given_label, "->", issue.suggested_label, issue.text) + """ + probs = np.asarray(probs, dtype=float) + if probs.ndim != 2: + raise ValueError(f"probs must be 2-D (n_samples, n_classes); got shape {probs.shape}.") + n_samples, n_classes = probs.shape + if n_classes < 2: + raise ValueError(f"Label-noise detection needs at least 2 classes; got {n_classes}.") + if len(label_names) != n_classes: + raise ValueError(f"label_names has {len(label_names)} entries but probs has {n_classes} columns.") + if len(labels) != n_samples: + raise ValueError(f"labels has {len(labels)} entries but probs has {n_samples} rows.") + if len(texts) != n_samples: + raise ValueError(f"texts has {len(texts)} entries but probs has {n_samples} rows.") + if score not in SCORE_METHODS: + raise ValueError(f"Unknown score method {score!r}; expected one of {list(SCORE_METHODS)}.") + + name_to_idx = {name: i for i, name in enumerate(label_names)} + unknown = sorted({str(v) for v in labels} - set(name_to_idx)) + if unknown: + raise ValueError(f"Labels absent from label_names: {unknown}. Known classes: {label_names}.") + given = np.array([name_to_idx[str(v)] for v in labels], dtype=int) + + if not np.allclose(probs.sum(axis=1), 1.0, atol=_PROB_SUM_TOLERANCE): + logger.warning( + "Probability rows do not sum to 1; confident learning's thresholds assume a proper " + "distribution per sample, so results may be distorted." + ) + + thresholds = _class_thresholds(probs, given, n_classes) + joint = _calibrate(_confident_joint(probs, given, thresholds), given, n_classes) + scores = _quality_scores(probs, given, score) + suggested = _prune_by_noise_rate(probs, given, joint, n_classes) + + # Worst first; the index breaks ties so the ordering is deterministic across runs. + ranked = sorted(suggested, key=lambda i: (scores[i], i)) + issues = [ + LabelIssue( + index=int(i), + text=str(texts[i]), + given_label=label_names[given[i]], + suggested_label=label_names[suggested[i]], + given_prob=float(probs[i, given[i]]), + suggested_prob=float(probs[i, suggested[i]]), + score=float(scores[i]), + ) + for i in ranked[: max(0, top_n)] + ] + return LabelNoiseReport( + issues=issues, + noise_matrix=joint, + thresholds=thresholds, + label_names=list(label_names), + n_samples=n_samples, + n_issues=len(ranked), + ) + + +def _class_thresholds(probs: np.ndarray, given: np.ndarray, n_classes: int) -> np.ndarray: + """Average self-confidence per class: ``t_j = mean{P[i, j] : given_i = j}``. + + A class carrying no labels gets ``inf``, so nothing is ever confidently assigned to it. Its + threshold is unestimable, and substituting an arbitrary one would fabricate suggestions toward a + class the corpus gives no evidence about. + """ + thresholds = np.full(n_classes, np.inf) + absent = [] + for j in range(n_classes): + rows = given == j + if rows.any(): + thresholds[j] = probs[rows, j].mean() + else: + absent.append(j) + if absent: + logger.warning( + "Classes %s carry no labelled samples; their confidence thresholds cannot be estimated " + "and they will never be suggested as a corrected label.", + absent, + ) + return thresholds + + +def _confident_joint(probs: np.ndarray, given: np.ndarray, thresholds: np.ndarray) -> np.ndarray: + """Count ``(given, confidently-predicted)`` pairs — the confident joint ``C``. + + A sample votes only for classes it clears the threshold for; among those it votes for its + argmax. A sample clearing no threshold abstains rather than being forced onto its argmax, which + is what keeps ``C`` a *confident* count instead of a plain confusion matrix. + """ + n_classes = thresholds.shape[0] + above = probs >= thresholds - _THRESHOLD_EPSILON # broadcast over rows; an inf threshold is never cleared + has_candidate = above.any(axis=1) + best = np.where(above, probs, -np.inf).argmax(axis=1) + + counts = np.zeros((n_classes, n_classes), dtype=int) + np.add.at(counts, (given[has_candidate], best[has_candidate]), 1) + return counts + + +def _calibrate(counts: np.ndarray, given: np.ndarray, n_classes: int) -> np.ndarray: + """Scale the confident joint to the observed class counts, then to a joint summing to 1. + + Abstentions mean each row of ``counts`` under-counts its class. Rescaling every row to the + number of samples actually carrying that label restores the marginal, so the result can be read + as "fraction of the corpus labelled *i* but truly *j*". + """ + counts = counts.astype(float) + row_totals = counts.sum(axis=1, keepdims=True) + class_counts = np.bincount(given, minlength=n_classes).astype(float).reshape(-1, 1) + scaled = np.divide(counts, row_totals, out=np.zeros_like(counts), where=row_totals != 0) * class_counts + total = scaled.sum() + return scaled / total if total > 0 else scaled + + +def _quality_scores(probs: np.ndarray, given: np.ndarray, method: str) -> np.ndarray: + """Per-sample label-quality score, lower meaning more likely mislabelled.""" + rows = np.arange(probs.shape[0]) + self_confidence = probs[rows, given] + if method == "self_confidence": + return self_confidence + competing = probs.copy() + competing[rows, given] = -np.inf + return self_confidence - competing.max(axis=1) + + +def _prune_by_noise_rate(probs: np.ndarray, given: np.ndarray, joint: np.ndarray, n_classes: int) -> dict[int, int]: + """Select the flagged samples cell by cell — confident learning's ``prune_by_noise_rate``. + + Cell ``(i, j)`` of the joint estimates how *many* samples are labelled ``i`` but truly ``j``; + this picks exactly that many, taking the class-``i`` samples with the largest + ``P[:, j] - P[:, i]`` margin. Ranking within the cell rather than globally is what stops a + single high-noise class pair from crowding out every other one. + + Returns + ------- + dict[int, int] + Sample index to suggested class index. A sample selected by several cells keeps the cell + with the largest margin. + """ + n_samples = probs.shape[0] + best: dict[int, tuple[float, int]] = {} + for i in range(n_classes): + rows = np.flatnonzero(given == i) + if rows.size == 0: + continue + for j in range(n_classes): + if i == j: + continue + n_errors = int(round(joint[i, j] * n_samples)) + if n_errors <= 0: + continue + margins = probs[rows, j] - probs[rows, i] + # Only samples the model actually prefers j for — see the Notes in detect_label_issues. + candidates = np.flatnonzero(margins > 0) + if candidates.size == 0: + continue + order = candidates[np.argsort(-margins[candidates])][:n_errors] + for pos in order: + sample, margin = int(rows[pos]), float(margins[pos]) + current = best.get(sample) + if current is None or margin > current[0]: + best[sample] = (margin, j) + return {sample: suggested for sample, (_margin, suggested) in best.items()} diff --git a/shapash/compute/diagnostics/label_probe.py b/shapash/compute/diagnostics/label_probe.py new file mode 100644 index 00000000..996ee0c7 --- /dev/null +++ b/shapash/compute/diagnostics/label_probe.py @@ -0,0 +1,183 @@ +"""An independent second opinion on a label, from a model that is not the one under audit. + +Confident learning flags a sample when the audited model confidently disagrees with its label. It +cannot say *which* of the two is wrong: a genuine label error and a confidently-wrong model produce +the same evidence. On a strong classifier the second case dominates — the emotion demo's distilbert +is 90.6% accurate against an estimated 3% label noise, so most of what it disagrees with is its own +mistake, not the corpus's. + +Breaking the tie needs a signal that does not come from the audited model. This is that signal: a +cheap bag-of-words classifier (TF-IDF + multinomial logistic regression) fit on a *labelled reference +corpus*, then asked one question per flagged row — **what probability does it put on the label the +row already carries?** High means the reference corpus backs the label and the audited model is +probably just wrong; low means both models agree the label is off. + +Why a separate classifier rather than nearest neighbours in the model's representation space: that +space is the one feeding the classifier head, so its neighbours restate the prediction (measured at +98.6% agreement on the emotion demo) and back the *wrong* prediction on 87% of the model's errors. +Anything derived from the audited model corroborates it by construction. See implementation-log C.27. + +Why bag-of-words rather than sentence embeddings: measured on the emotion demo, TF-IDF + logistic +regression scores 0.658 accuracy and agrees with the audited model 66.6% of the time, against 0.636 +and 64.2% for ``all-MiniLM-L6-v2`` + logistic regression — better on both axes, with no dependency +beyond the scikit-learn already in core. A weak-but-independent probe beats a strong-but-correlated +one for this job, and the probe never has to be right on its own: it only has to be wrong for +*different reasons*. + +Limitations +----------- +The probe is weak in absolute terms (0.658 against the audited model's 0.906) and is **not** a +detector. Run as one it estimates a ~29% noise rate on a corpus whose real noise is a few percent. +Its output is only meaningful as a per-row second opinion on rows confident learning already flagged, +and never as a ranking, a threshold, or a noise-rate estimate. +""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from dataclasses import dataclass + +import numpy as np +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import Pipeline, make_pipeline + +logger = logging.getLogger(__name__) + +#: Minimum reference examples per class for the probe to be worth fitting. Below this the estimate is +#: noise, and a confident-looking second opinion drawn from three examples is worse than none. +MIN_PER_CLASS = 5 + + +@dataclass(frozen=True) +class ProbeVerdict: + """The independent probe's read on one row's given label. + + Attributes + ---------- + given_prob : float + Probability the probe assigns to the label the row already carries. **High is the + interesting case**: the reference corpus backs the label, so the audited model's confident + disagreement is more likely its own error than a label error. + top_label : str + The class the probe itself would pick. + backs_given : bool + Whether ``top_label`` is the given label — a threshold-free reading of "the probe sides with + the corpus against the audited model". + """ + + given_prob: float + top_label: str + backs_given: bool + + +class LabelProbe: + """A model-independent second opinion on given labels, fit on a labelled reference corpus. + + Parameters + ---------- + reference_texts : sequence of str + Texts of the reference corpus — typically the audited model's training split. + reference_labels : sequence of str + Labels aligned with ``reference_texts``. + + Raises + ------ + ValueError + If the two sequences differ in length, or the corpus holds fewer than two classes. + + Notes + ----- + Fitting is lazy and happens once (:meth:`fit` is idempotent); on 2000 short texts it costs about + ten seconds, which is why callers should hold one instance rather than rebuilding per query. + + The probe must not be fit on the rows it will judge — it would then be scoring its own training + data and would back every label. Fitting on a *reference* corpus keeps its verdicts out-of-sample + by construction, which is also why there is no "fit on the compiled batch" path: cross-validating + a 500-row batch measured 0.460 accuracy, barely above the 0.330 majority-class rate. + + Examples + -------- + >>> probe = LabelProbe(train_texts, train_labels) + >>> probe.verdicts(["im feeling rather angsty and listless"], ["sadness"])[0] + ProbeVerdict(given_prob=0.795, top_label='sadness', backs_given=True) + """ + + def __init__(self, reference_texts: Sequence[str], reference_labels: Sequence[str]) -> None: + if len(reference_texts) != len(reference_labels): + raise ValueError( + f"reference_labels length ({len(reference_labels)}) must match " + f"reference_texts length ({len(reference_texts)})." + ) + self.reference_texts = [str(t) for t in reference_texts] + self.reference_labels = [str(v) for v in reference_labels] + classes, counts = np.unique(self.reference_labels, return_counts=True) + if classes.size < 2: + raise ValueError(f"LabelProbe needs at least 2 classes in the reference corpus; got {classes.size}.") + thin = [str(c) for c, n in zip(classes, counts, strict=True) if n < MIN_PER_CLASS] + if thin: + logger.warning( + "Reference classes %s have fewer than %d examples; the probe's verdicts for them are unreliable.", + thin, + MIN_PER_CLASS, + ) + self._pipeline: Pipeline | None = None + + @property + def classes(self) -> list[str]: + """Classes the probe can report on — those present in the reference corpus.""" + return sorted(set(self.reference_labels)) + + def fit(self) -> None: + """Fit the TF-IDF + logistic-regression pipeline over the reference corpus. Idempotent.""" + if self._pipeline is not None: + return + # Word unigrams+bigrams, sublinear tf: a standard strong text baseline. Deliberately plain — + # every knob added here is a knob that could quietly re-correlate the probe with the model. + pipeline = make_pipeline( + TfidfVectorizer(sublinear_tf=True, ngram_range=(1, 2), min_df=1), + LogisticRegression(max_iter=1000, C=10.0), + ) + pipeline.fit(self.reference_texts, self.reference_labels) + self._pipeline = pipeline + + def verdicts(self, texts: Sequence[str], given_labels: Sequence[str]) -> list[ProbeVerdict]: + """Return the probe's read on each ``(text, given_label)`` pair. + + Parameters + ---------- + texts : sequence of str + The texts to judge. + given_labels : sequence of str + The label each text currently carries, aligned with ``texts``. A label the reference + corpus never shows gets ``given_prob`` 0.0 — the probe cannot speak for a class it has + not seen, and 0.0 correctly reads as "no corpus support". + + Returns + ------- + list[ProbeVerdict] + One verdict per input row, in order. + + Raises + ------ + ValueError + If ``texts`` and ``given_labels`` differ in length. + """ + if len(texts) != len(given_labels): + raise ValueError(f"given_labels length ({len(given_labels)}) must match texts length ({len(texts)}).") + if not texts: + return [] + self.fit() + assert self._pipeline is not None # noqa: S101 - fit() guarantees this + probs = self._pipeline.predict_proba(list(texts)) + classes = [str(c) for c in self._pipeline.classes_] + position = {name: i for i, name in enumerate(classes)} + return [ + ProbeVerdict( + given_prob=float(row[position[label]]) if label in position else 0.0, + top_label=classes[int(row.argmax())], + backs_given=classes[int(row.argmax())] == label, + ) + for row, label in ((probs[i], str(given_labels[i])) for i in range(len(texts))) + ] diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py index 484fdc50..261fdc70 100644 --- a/shapash/explainer/interactive.py +++ b/shapash/explainer/interactive.py @@ -19,6 +19,7 @@ from typing import Protocol, runtime_checkable from shapash.backend.nlp_backend import NlpContributions +from shapash.compute.diagnostics.label_noise import LabelNoiseReport from shapash.compute.generators.base import Counterfactual, Field from shapash.compute.retrieval.similar_examples import Neighbor @@ -77,3 +78,17 @@ def can_find_similar(self) -> bool: def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: """Return the ``top_k`` reference examples most similar to ``text``, most-similar first.""" ... + + def can_detect_label_noise(self) -> bool: + """Whether the compiled batch carries the ground truth + per-class probabilities this needs.""" + ... + + def can_probe_labels(self) -> bool: + """Whether a labelled reference corpus is bound to second-guess flagged labels.""" + ... + + def detect_label_noise( + self, top_n: int = 50, score: str = "self_confidence", probe: bool = True + ) -> LabelNoiseReport: + """Rank probably-mislabelled samples in the compiled batch by confident learning.""" + ... diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 0d6482ce..1c0b1252 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -15,7 +15,7 @@ import hashlib import pickle import threading -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path from typing import cast @@ -26,6 +26,12 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.diagnostics.label_noise import ( + LabelNoiseReport, + detect_label_issues, + has_usable_probabilities, +) +from shapash.compute.diagnostics.label_probe import LabelProbe from shapash.compute.embedding_store import EmbeddingStore, hash_corpus from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field @@ -151,6 +157,9 @@ class NlpExplainer: Neighbours are compared in the model's own ``embedding_space``. To compare in a different space, set the model's ``embedding_space`` — that moves the neighbours and any scatter built by :meth:`compute_projection` together, since both read the space through the same store. + When ``labels`` is given, this corpus additionally trains the label-noise panel's + independent probe (:meth:`can_probe_labels`), which needs no model and so is available even + when retrieval is not. reference_cache_dir : str or Path, optional When given, the reference embedding bank is persisted here and reloaded on later runs, so only the first launch pays the (one-off) cost of embedding the reference corpus. @@ -238,6 +247,15 @@ def __init__( reference_labels=ref_labels, cache_dir=reference_cache_dir, ) + # Kept as plain data beside the retriever, not inside it: the label probe needs only labelled + # text and no model at all, so it must stay available for a prediction-only pipeline that + # cannot embed — exactly the case the retriever above declines to build for. + self._reference_corpus: tuple[list[str], list[str]] | None = ( + (list(reference_corpus[0]), list(reference_corpus[1])) + if reference_corpus is not None and reference_corpus[1] is not None + else None + ) + self._label_probe: LabelProbe | None = None # fit lazily on first use, then reused self.contributions: NlpContributions | None = None self.texts: pd.Series | None = None @@ -500,6 +518,10 @@ def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: xpl.cf_generator = None xpl.cf_generators = {} xpl._retriever = None + # The snapshot carries results, not the corpus they were compared against, so the label + # probe cannot be refit — label-noise detection still runs, just without the second opinion. + xpl._reference_corpus = None + xpl._label_probe = None xpl.label_names = results.label_names xpl.texts = results.texts xpl.contributions = results.computed.contributions @@ -590,6 +612,126 @@ def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: with self._compute_guard(): # embed() tokenizes — serialize against explain_text/predict return retriever.query(text, top_k=top_k) + def can_detect_label_noise(self) -> bool: + """Whether label-noise detection can run on the compiled batch. + + Needs ground truth plus one probability column per class — nothing else. In particular it + needs **no model**, so unlike the other capability flags this stays ``True`` after + :meth:`from_snapshot`; only the optional neighbour corroboration in + :meth:`detect_label_noise` requires a live embedding-capable model. + + ``False`` when ``compile()`` got no ``y_true``, and when the bound model is a raw pipeline + that reports only the winning class's confidence (a single ``probability`` column). + """ + return getattr(self, "y_true", None) is not None and has_usable_probabilities(getattr(self, "y_prob", None)) + + def can_probe_labels(self) -> bool: + """Whether the model-independent label probe can run (needs a *labelled* reference corpus). + + The probe is fit on the reference corpus, so unlike :meth:`can_find_similar` it needs no + model — a prediction-only pipeline still gets it. It is ``False`` after + :meth:`from_snapshot`, which carries results but not the corpus they were compared against. + """ + return getattr(self, "_reference_corpus", None) is not None + + def detect_label_noise( + self, top_n: int = 50, score: str = "self_confidence", probe: bool = True + ) -> LabelNoiseReport: + """Rank the compiled samples whose ground-truth label is probably wrong. + + Runs confident learning (see :mod:`shapash.compute.diagnostics.label_noise`) over the + probabilities and labels already held from :meth:`compile`, then attaches a + model-independent second opinion to each flagged sample. + + Parameters + ---------- + top_n : int + Maximum number of issues to return, worst first. + score : {"self_confidence", "normalized_margin"} + Ranking method for the flagged samples. + probe : bool, optional + Attach a :class:`~shapash.compute.diagnostics.label_probe.ProbeVerdict` to each returned + issue — a second opinion from a bag-of-words classifier fit on the reference corpus, + which owes nothing to the audited model. Silently skipped when + :meth:`can_probe_labels` is ``False``. The probe judges only the issues actually + returned, so cost scales with ``top_n``, not the corpus. + + Returns + ------- + LabelNoiseReport + The ranked issues plus the estimated class-to-class noise matrix. + + Raises + ------ + RuntimeError + If the batch carries no ground truth or no per-class probabilities. + + Notes + ----- + Confident learning assumes the probabilities are **out-of-sample** — compiled on data the + model did not train on. On a model's own training split it will under-report. See the module + docstring of :mod:`shapash.compute.diagnostics.label_noise`. + + Confident learning cannot tell a wrong *label* from a confidently wrong *model* — both look + like "the model disagrees with the label". The probe verdict is what separates them, and it + is reported alongside the ranking rather than folded into it: a high + ``issue.probe.given_prob`` means the reference corpus backs the label, so the row is + probably the model's mistake, not the corpus's. Only the probe can say this, because it is + the one signal here that does not come from the audited model. + + Examples + -------- + >>> xpl.compile(texts, y_true=labels) + >>> report = xpl.detect_label_noise(top_n=20) + >>> report.noise_rate + 0.062 + >>> report.issues[0].probe.backs_given # corpus sides with the label -> suspect the model + True + """ + if not self.can_detect_label_noise(): + raise RuntimeError( + "detect_label_noise() needs ground-truth labels (pass y_true to compile()) and " + "per-class probabilities (a model reporting every class's score, not just the " + "predicted one)." + ) + cache_key = (getattr(self, "_data_hash", None), top_n, score, probe) + cached = getattr(self, "_label_noise_cache", None) + if cached is not None and cached[0] == cache_key: + return cast("LabelNoiseReport", cached[1]) + + y_prob = cast("pd.DataFrame", self.y_prob) + y_true = cast("pd.Series", self.y_true) + texts = cast("pd.Series", self.texts) + report = detect_label_issues( + y_prob.to_numpy(dtype=float), + [str(v) for v in y_true.tolist()], + [str(t) for t in texts.tolist()], + [str(c) for c in y_prob.columns], + top_n=top_n, + score=score, + ) + if probe and report.issues and self.can_probe_labels(): + report = replace(report, issues=self._attach_probe(report.issues)) + + self._label_noise_cache = (cache_key, report) + return report + + def _attach_probe(self, issues: list) -> list: + """Return ``issues`` with each carrying the independent probe's verdict on its given label. + + The probe is fit once and kept: it costs seconds over a few thousand reference texts, and a + panel re-detecting on every control change would otherwise pay that each time. No compute + guard is taken — this is scikit-learn over plain strings, touching neither the shared + tokenizer nor the model. + """ + probe = getattr(self, "_label_probe", None) + if probe is None: + ref_texts, ref_labels = cast("tuple[list[str], list[str]]", self._reference_corpus) + probe = LabelProbe(ref_texts, ref_labels) + self._label_probe = probe + verdicts = probe.verdicts([iss.text for iss in issues], [iss.given_label for iss in issues]) + return [replace(issue, probe=verdict) for issue, verdict in zip(issues, verdicts, strict=True)] + def predict(self, text: str) -> tuple[str, dict[str, float]]: """Predict a single text, returning ``(label, {label: probability})``.""" text_model = self._require_text_model() diff --git a/shapash/plots/plot_noise_matrix.py b/shapash/plots/plot_noise_matrix.py new file mode 100644 index 00000000..06adebe2 --- /dev/null +++ b/shapash/plots/plot_noise_matrix.py @@ -0,0 +1,94 @@ +"""Estimated label-noise matrix heatmap. + +Sibling of :mod:`~shapash.plots.plot_confusion_matrix`, and deliberately *not* a mode of it. The two +render the same shape but answer different questions and need different axes, hover text and number +formatting: + +* a confusion matrix is **true vs. predicted**, in counts — how the model behaves; +* a noise matrix is **given vs. estimated-true**, as a fraction of the whole corpus — how the labels + behave. + +Folding the second into the first would mean an axis reading "Predicted class" over a column that +means "estimated true class", which is exactly the sort of quietly-wrong label the panel exists to +find. +""" + +from __future__ import annotations + +import numpy as np +from plotly import graph_objs as go + +_COLORSCALE = "Blues" + + +def plot_noise_matrix( + noise_matrix: np.ndarray, + labels: list[str], + mask_diagonal: bool = True, + title: str = "Estimated label noise", + width: int | None = None, + height: int | None = None, +) -> go.Figure: + """Heatmap of an estimated label-noise joint distribution. + + Parameters + ---------- + noise_matrix : numpy.ndarray + Joint distribution of shape ``(n_classes, n_classes)`` summing to 1, where + ``noise_matrix[i, j]`` is the estimated fraction of the corpus labelled ``i`` but truly + ``j`` — :attr:`~shapash.compute.diagnostics.label_noise.LabelNoiseReport.noise_matrix`. + labels : list of str + Class display names, ordered to match the rows/columns. + mask_diagonal : bool, optional + Blank the diagonal (default). The correctly-labelled mass is normally well over 90% of the + joint, so leaving it in flattens every off-diagonal cell to the same near-white shade and + hides the only part of the matrix anyone reads it for. The diagonal is not information the + caller loses — it is ``1 - noise_rate``. + title : str + Figure title. + width : int, optional + Figure width in pixels. Defaults to a size scaled to the number of classes. + height : int, optional + Figure height in pixels. Defaults to a size scaled to the number of classes. + + Returns + ------- + plotly.graph_objs.Figure + """ + joint = np.asarray(noise_matrix, dtype=float).copy() + n = joint.shape[0] + if mask_diagonal: + # NaN rather than 0 so the cells render blank instead of as a "genuinely zero noise" colour. + np.fill_diagonal(joint, np.nan) + + text = np.array([[("" if np.isnan(v) else f"{v:.1%}") for v in row] for row in joint]) + + fig = go.Figure( + go.Heatmap( + z=joint, + x=labels, + y=labels, + text=text, + texttemplate="%{text}", + colorscale=_COLORSCALE, + colorbar=dict(title="% of corpus", tickformat=".1%"), + hovertemplate=("labelled: %{y}<br>probably: %{x}<br>%{z:.2%} of corpus<extra></extra>"), + ) + ) + + size = max(360, 90 * n + 120) + top_margin = 80 if title else 40 + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title="Estimated true class", + yaxis_title="Given label", + width=width or size, + height=height or size, + plot_bgcolor="white", + # First class at the top so the (masked) diagonal reads top-left to bottom-right, matching + # the confusion matrix next door. + yaxis=dict(autorange="reversed", automargin=True), + xaxis=dict(automargin=True, side="top"), + margin=dict(l=20, r=20, t=top_margin, b=20), + ) + return fig diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index bca8ed7c..357ee7be 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -26,6 +26,7 @@ from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, + LabelNoiseComponent, SentenceHighlightComponent, SimilarExamplesComponent, WaterfallComponent, @@ -634,6 +635,7 @@ def _build_layout(self) -> None: DataEditorComponent(), CounterfactualComponent(), SimilarExamplesComponent(), + LabelNoiseComponent(), ) if type(comp).is_available(self._view, self._engine) ] @@ -642,6 +644,7 @@ def _build_layout(self) -> None: editor_comp = next((c for c in self._components if isinstance(c, DataEditorComponent)), None) cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) similar_comp = next((c for c in self._components if isinstance(c, SimilarExamplesComponent)), None) + noise_comp = next((c for c in self._components if isinstance(c, LabelNoiseComponent)), None) left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_col_content is not None: @@ -654,6 +657,10 @@ def _build_layout(self) -> None: upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] if error_analysis_body is not None: upper_right_tabs.append(("errors", "Error Analysis", error_analysis_body)) + # Next to Error Analysis by design: same ground-truth prerequisite, and it answers the + # question that panel leaves open — whether the *label*, not the model, is what is wrong. + if noise_comp is not None: + upper_right_tabs.append(("label-noise", "Label Noise", noise_comp.layout(self._view, self._engine))) if cf_comp is not None: upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) if similar_comp is not None: diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index b4d1e6e4..03983d4d 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -21,6 +21,7 @@ pack_datapoint, unpack_datapoint, ) +from shapash.webapp.nlp_components.label_noise import LabelNoiseComponent from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent from shapash.webapp.nlp_components.similar_examples import SimilarExamplesComponent from shapash.webapp.nlp_components.waterfall import WaterfallComponent @@ -30,6 +31,7 @@ "available_capabilities", "DataEditorComponent", "CounterfactualComponent", + "LabelNoiseComponent", "SentenceHighlightComponent", "SimilarExamplesComponent", "WaterfallComponent", diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 067d6ad5..6d7a64c9 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -10,6 +10,7 @@ from abc import ABC, abstractmethod +from shapash.compute.diagnostics.label_noise import has_usable_probabilities from shapash.explainer.interactive import InteractiveEngine from shapash.model.base import SupportsGradients, has_capabilities from shapash.webapp.nlp_view import NlpView @@ -19,6 +20,7 @@ CAP_COUNTERFACTUAL = "engine:counterfactual" CAP_GRADIENTS = "model:gradients" CAP_SIMILAR = "engine:similar" +CAP_LABELS = "data:labels" def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> frozenset[str]: @@ -27,7 +29,8 @@ def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> f Parameters ---------- view : NlpView - Read-only view (data capabilities could be added here later). + Read-only view, supplying the *data* capabilities — what the compiled batch contains, + independent of whether a live model is still attached. engine : InteractiveEngine or None Live engine, or ``None`` for a snapshot (no live capabilities). @@ -38,6 +41,10 @@ def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> f "model:gradients"}``). """ caps: set[str] = set() + # Data capabilities are read from the compiled batch, so they survive a snapshot — they sit + # outside the engine guard below on purpose. + if view.y_true is not None and has_usable_probabilities(view.y_prob): + caps.add(CAP_LABELS) if engine is not None: if engine.can_edit(): caps.add(CAP_PREDICT) diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py new file mode 100644 index 00000000..9445a3e6 --- /dev/null +++ b/shapash/webapp/nlp_components/label_noise.py @@ -0,0 +1,273 @@ +"""Label-noise panel: which ground-truth labels are probably wrong, and where the noise concentrates. + +Error analysis stops at "the model got this one wrong"; this panel asks the next question — whether +the *label* is wrong instead. It runs confident learning +(:mod:`shapash.compute.diagnostics.label_noise`) over the probabilities and ground truth already +held from ``compile()``, and shows two views of the answer: an estimated noise matrix for the +corpus-level read (which class pairs are contaminated) and a ranked table for row-level triage. + +Confident learning alone cannot answer the question the panel is named for. It flags rows where the +model confidently disagrees with the label, and a genuine label error looks exactly like a +confidently-wrong model. On the emotion demo every flagged row is also a model error, by +construction — so read on its own, the ranking silently invites you to "fix" labels the model simply +got wrong. + +The **Corpus check** column is what separates the two. It is a bag-of-words classifier fit on the +reference corpus (:mod:`shapash.compute.diagnostics.label_probe`) — the one signal in this panel that +does not come from the audited model — asked how much probability it puts on the label the row +already carries. High, and the corpus backs the label: suspect the model. Low, and both models agree +the label is off. + +That column replaced an earlier one showing the labels of the row's nearest neighbours. Retrieval +ranks in the model's decision space, so those neighbours restated the prediction 98.6% of the time +and backed the *wrong* prediction on 87% of model errors — it read as confirmation while carrying no +information. Representation neighbours are still one click away and honestly framed in the Similar +Examples panel, which follows the current datapoint. See implementation-log C.27. + +*Inspect* makes a flagged row the current datapoint so its token contributions render in the shared +Sentence/Waterfall panels — the words that pushed the model away from the given label. Unlike the +counterfactual and similar-example panels, the sample is already compiled, so this costs no +re-explanation. + +Gated by ``CAP_LABELS`` alone: the method needs no model, so the panel also works on a snapshot. The +Corpus check column self-disables when no labelled reference corpus is bound. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +from dash import ALL, Input, Output, State, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_noise_matrix import plot_noise_matrix +from shapash.webapp.nlp_components.base import CAP_LABELS, WebappComponent +from shapash.webapp.nlp_components.datapoint import pack_datapoint + +_CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} +_INLINE = {"display": "flex", "alignItems": "center", "gap": "0.4rem"} + +_DEFAULT_TOP_N = 50 +_DEFAULT_SCORE = "self_confidence" +_SCORE_OPTIONS: list[dcc.Dropdown.Options] = [ + {"label": "Self-confidence", "value": _DEFAULT_SCORE}, + {"label": "Normalized margin", "value": "normalized_margin"}, +] + + +class LabelNoiseComponent(WebappComponent): + """Ranked probably-mislabelled samples plus an estimated noise matrix (confident learning).""" + + id = "label-noise" + name = "Label Noise" + scope = "global" + # Data-only: no engine capability, so this also mounts on a snapshot explainer. + requires = frozenset({CAP_LABELS}) + + def layout(self, view, engine=None) -> html.Div: + """Return the panel: detection controls over an initially empty results area.""" + can_probe = engine is not None and engine.can_probe_labels() + caption = ( + "Confident learning over the model's probabilities and the ground-truth labels. " + "Assumes those probabilities are out-of-sample — on a model's own training split " + "it under-reports." + ) + caption += ( + " Every flagged row is one the model disagrees with, so check the Corpus column: a high " + "score there means an independent classifier backs the existing label, and the model is " + "the more likely culprit." + if can_probe + else " No labelled reference corpus is bound, so flagged rows carry no independent " + "cross-check — a row may be a label error or simply a model error." + ) + return html.Div( + [ + html.H6("Label-Noise Detection", className="fw-bold mb-2"), + html.Small(caption, className="text-muted d-block mb-2"), + html.Div( + [ + html.Div( + [ + html.Label("Top-N", className="small fw-bold mb-0"), + dcc.Input( + id=f"{self.id}-top-n", + type="number", + min=1, + max=500, + step=1, + value=_DEFAULT_TOP_N, + style={"width": "80px"}, + ), + ], + style=_INLINE, + ), + html.Div( + [ + html.Label("Rank by", className="small fw-bold mb-0"), + dcc.Dropdown( + id=f"{self.id}-score", + options=_SCORE_OPTIONS, + value=_DEFAULT_SCORE, + clearable=False, + style={"width": "190px"}, + ), + ], + style=_INLINE, + ), + dbc.Button("Detect", id=f"{self.id}-detect-btn", color="secondary", size="sm"), + ], + className="d-flex flex-wrap align-items-center gap-3 mb-2", + ), + dcc.Loading(html.Div(id=f"{self.id}-results")), + # Positional indices of the displayed rows, so an Inspect click resolves to a sample. + dcc.Store(id=f"{self.id}-store", data=[]), + ], + style=_CARD_STYLE, + ) + + def register_callbacks(self, app, view, engine, stores) -> None: + """Run detection on demand; wire each row's Inspect into the shared current datapoint.""" + current_store = stores["current"] + + @app.callback( + Output(f"{self.id}-results", "children"), + Output(f"{self.id}-store", "data"), + Input(f"{self.id}-detect-btn", "n_clicks"), + State(f"{self.id}-top-n", "value"), + State(f"{self.id}-score", "value"), + ) + def detect(n_clicks, top_n, score): + if not n_clicks: + raise PreventUpdate + report = engine.detect_label_noise( + top_n=int(top_n) if top_n else _DEFAULT_TOP_N, + score=score or _DEFAULT_SCORE, + ) + if not report.issues: + return html.Div( + "No label issues detected — every label is consistent with the model's confident predictions.", + className="text-muted", + ), [] + return _report_view(report, self.id), [issue.index for issue in report.issues] + + # Inspect: the sample is already in the compiled batch, so it packs straight from the stored + # contributions — no explain_text round-trip, unlike the counterfactual/similar panels whose + # texts are novel. Mirrors the app shell's set_current_from_row. + @app.callback( + Output(current_store, "data", allow_duplicate=True), + Input({"type": f"{self.id}-apply", "index": ALL}, "n_clicks"), + State(f"{self.id}-store", "data"), + prevent_initial_call=True, + ) + def inspect(n_clicks_list, sample_indices): + if not any(n_clicks_list or []): + raise PreventUpdate + triggered = callback_context.triggered_id + if not triggered: + raise PreventUpdate + row = triggered["index"] + if not sample_indices or not (0 <= row < len(sample_indices)): + raise PreventUpdate + pos = int(sample_indices[row]) + contributions = view.contributions + texts = view.texts + if contributions is None or texts is None or not (0 <= pos < len(texts)): + raise PreventUpdate + base_values = contributions.base_values + y_pred = view.y_pred + return pack_datapoint( + text=str(texts.iloc[pos]), + orig_idx=pos, + tokens=contributions.token_strings[pos], + values=contributions.values[pos], + base_values=(base_values[pos] if base_values is not None else None), + label=(str(y_pred.iloc[pos]) if y_pred is not None else None), + ) + + +def _report_view(report, component_id): + """Assemble the summary line, the noise-matrix heatmap and the ranked issue table.""" + rate = report.noise_rate + summary = ( + f"Estimated {rate:.1%} of {report.n_samples} labels are wrong " + f"({report.n_issues} flagged, showing {len(report.issues)})." + ) + fig = plot_noise_matrix(report.noise_matrix, report.label_names, title="") + # Let the container height drive size — this panel is only half a column tall. + fig.layout.width = None + fig.layout.height = None + return html.Div( + [ + html.Div(summary, className="small fw-bold mb-1"), + html.Small( + "Rows are the given label, columns the class the model points to instead. " + "The diagonal (correctly labelled) is blanked so the off-diagonal noise stays visible. " + "⚠ in the Corpus column marks rows an independent classifier says are model errors, " + "not label errors.", + className="text-muted d-block mb-2", + ), + dcc.Graph(figure=fig, config={"displayModeBar": False}, style={"height": "320px"}), + _issues_table(report.issues, component_id), + ] + ) + + +def _issues_table(issues, component_id): + """Render the ranked issues, with the Corpus column only when the probe produced verdicts.""" + show_probe = any(issue.probe is not None for issue in issues) + head_cells = [html.Th("Score"), html.Th("Given"), html.Th("Probably")] + if show_probe: + head_cells.append(html.Th("Corpus", title="Independent classifier's probability for the given label")) + head_cells.extend([html.Th("Text"), html.Th("")]) + header = html.Thead(html.Tr(head_cells)) + + rows = [] + for i, issue in enumerate(issues): + cells = [ + html.Td(f"{issue.score:.3f}"), + html.Td([html.B(issue.given_label), html.Span(f" {issue.given_prob:.2f}", className="text-muted")]), + html.Td([html.B(issue.suggested_label), html.Span(f" {issue.suggested_prob:.2f}", className="text-muted")]), + ] + if show_probe: + cells.append(html.Td(_probe_summary(issue), style={"fontSize": "0.85em"})) + cells.append(html.Td(issue.text, style={"fontSize": "0.85em"})) + cells.append( + html.Td( + dbc.Button( + "Inspect", + id={"type": f"{component_id}-apply", "index": i}, + color="link", + size="sm", + className="p-0", + title="Show this sample's token contributions on the right", + ) + ) + ) + rows.append(html.Tr(cells)) + return dbc.Table([header, html.Tbody(rows)], bordered=False, hover=True, size="sm", className="mb-0") + + +def _probe_summary(issue): + """The independent probe's probability for the *given* label, flagged when it backs it. + + ``⚠ 0.83`` means a classifier that never saw the audited model puts 0.83 on the label already + on the row — the corpus is with the label, so the model is the likelier culprit and this is not + a row to relabel. ``0.16 → love`` means the probe rejects the given label too, which is the + corroborated label error worth acting on. + """ + probe = issue.probe + if probe is None: + return "—" + if probe.backs_given: + return html.Span( + f"⚠ {probe.given_prob:.2f}", + className="text-warning fw-bold", + title=( + f"An independent classifier backs '{issue.given_label}' — the model is more likely " + "wrong here than the label." + ), + ) + return html.Span( + f"{probe.given_prob:.2f} → {probe.top_label}", + className="text-muted", + title=f"The independent classifier also rejects '{issue.given_label}'; it would pick '{probe.top_label}'.", + ) diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py index ac944cf5..2d54e7c5 100644 --- a/tests/integration_tests/test_nlp_whatif.py +++ b/tests/integration_tests/test_nlp_whatif.py @@ -224,3 +224,77 @@ def test_whatif_app_callbacks_end_to_end(hf): payload = r.get_json()["response"] assert payload["counterfactual-results"]["children"] is not None assert isinstance(payload["counterfactual-store"]["data"], list) + + +def test_label_noise_detection_with_real_probabilities(hf): + """Confident learning end to end on real model outputs, with neighbour corroboration. + + The corpus deliberately mislabels two obviously-emotional sentences, so a working pipeline has to + surface them; the assertions stay on structure and on the planted rows rather than on an exact + count, since the estimated per-cell counts depend on the model's real calibration. + """ + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + + texts = [ + "i am so happy today", + "i feel terrified and alone", + "i am furious about this", + "what a wonderful and joyful day", + "i am scared of the dark", + "this fills me with rage", + ] + truth = ["joy", "fear", "anger", "joy", "fear", "anger"] + # Plant two label errors: rows 0 and 2 keep their text but get someone else's label. + noisy = list(truth) + noisy[0], noisy[2] = "anger", "joy" + + reference = (texts, truth) + xpl = NlpExplainer(model, label_names=LABELS, reference_corpus=reference) + xpl.compile(texts, y_true=noisy) + + assert xpl.can_detect_label_noise() + assert xpl.can_probe_labels() + report = xpl.detect_label_noise(top_n=5) + + assert report.n_samples == len(texts) + assert report.label_names == LABELS + assert report.noise_matrix.shape == (len(LABELS), len(LABELS)) + np.testing.assert_allclose(report.noise_matrix.sum(), 1.0, atol=1e-6) + assert 0.0 <= report.noise_rate <= 1.0 + assert report.thresholds.shape == (len(LABELS),) + + flagged = {issue.index for issue in report.issues} + assert {0, 2} <= flagged, f"planted mislabels missed; flagged {flagged}" + for issue in report.issues: + assert issue.given_label in LABELS + assert issue.suggested_label in LABELS + assert issue.suggested_label != issue.given_label + assert issue.text == texts[issue.index] + assert issue.probe is not None + assert issue.probe.top_label in LABELS + assert 0.0 <= issue.probe.given_prob <= 1.0 + + # The probe is fit on the reference corpus, which here carries the *clean* labels — so on the two + # planted errors it should reject the (wrong) label the batch gave them. Note this reference + # corpus deliberately reuses the batch texts to keep the assertion deterministic; in real use it + # must be a separate corpus, or the probe would be scoring its own training rows. + planted = {issue.index: issue for issue in report.issues if issue.index in {0, 2}} + for index, issue in planted.items(): + assert not issue.probe.backs_given, f"row {index}: probe defended a planted wrong label" + assert issue.probe.top_label == truth[index] + + # Without a reference corpus there is no second opinion, and detection still runs. + bare = NlpExplainer(model, label_names=LABELS) + bare.compile(texts, y_true=noisy) + assert not bare.can_probe_labels() + assert all(issue.probe is None for issue in bare.detect_label_noise(top_n=5).issues) + + +def test_label_noise_unavailable_without_ground_truth(hf): + classifier, tokenizer, _ = hf + xpl = NlpExplainer(HFClassifierModel(classifier, tokenizer, label_names=LABELS), label_names=LABELS) + xpl.compile(["i am so happy today", "i feel terrified and alone"]) + assert not xpl.can_detect_label_noise() + with pytest.raises(RuntimeError, match="ground-truth labels"): + xpl.detect_label_noise() diff --git a/tests/unit_tests/compute/diagnostics/test_label_noise.py b/tests/unit_tests/compute/diagnostics/test_label_noise.py new file mode 100644 index 00000000..97073e92 --- /dev/null +++ b/tests/unit_tests/compute/diagnostics/test_label_noise.py @@ -0,0 +1,296 @@ +"""Unit tests for confident-learning label-noise detection (pure numpy, no model). + +Probabilities are constructed by hand so every expected count is derivable on paper: the tests assert +the algorithm's *intermediate* quantities (thresholds, confident joint, calibrated joint) as well as +the ranked output, since a plausible-looking issue list can hide a wrong noise matrix and vice versa. +""" + +import logging +import unittest + +import numpy as np +import pandas as pd + +from shapash.compute.diagnostics.label_noise import ( + SCORE_METHODS, + LabelNoiseReport, + _calibrate, + _class_thresholds, + _confident_joint, + _prune_by_noise_rate, + _quality_scores, + detect_label_issues, + has_usable_probabilities, +) + +_NAMES = ["joy", "surprise", "anger"] + + +def _confident_probs(true_classes, n_classes=3, peak=0.90): + """One row per entry: ``peak`` on the true class, the rest spread evenly.""" + rest = (1.0 - peak) / (n_classes - 1) + probs = np.full((len(true_classes), n_classes), rest) + probs[np.arange(len(true_classes)), true_classes] = peak + return probs + + +def _planted_corpus(n=300, n_flips=15, seed=0): + """A confident model on ``n`` samples, with ``n_flips`` joy samples mislabelled as surprise.""" + rng = np.random.default_rng(seed) + true = rng.integers(0, 3, n) + probs = _confident_probs(true) + given = true.copy() + flipped = np.flatnonzero(true == 0)[:n_flips] + given[flipped] = 1 + labels = [_NAMES[i] for i in given] + texts = [f"text-{i}" for i in range(n)] + return probs, labels, texts, set(flipped.tolist()) + + +class TestHasUsableProbabilities(unittest.TestCase): + def test_none_is_unusable(self): + self.assertFalse(has_usable_probabilities(None)) + + def test_legacy_single_probability_column_is_unusable(self): + # The raw-pipeline path reports only the winning class's confidence; the losing classes' + # probabilities are exactly what confident learning needs. + self.assertFalse(has_usable_probabilities(pd.DataFrame({"probability": [0.9, 0.8]}))) + + def test_per_class_columns_are_usable(self): + self.assertTrue(has_usable_probabilities(pd.DataFrame({"joy": [0.9, 0.2], "surprise": [0.1, 0.8]}))) + + +class TestThresholds(unittest.TestCase): + def test_threshold_is_the_mean_self_confidence_of_each_class(self): + probs = np.array([[0.8, 0.2], [0.6, 0.4], [0.3, 0.7]]) + given = np.array([0, 0, 1]) + thresholds = _class_thresholds(probs, given, 2) + np.testing.assert_allclose(thresholds, [(0.8 + 0.6) / 2, 0.7]) + + def test_class_without_labels_gets_an_infinite_threshold_and_warns(self): + probs = np.array([[0.5, 0.3, 0.2], [0.4, 0.4, 0.2]]) + given = np.array([0, 0]) + with self.assertLogs("shapash.compute.diagnostics.label_noise", level=logging.WARNING) as logs: + thresholds = _class_thresholds(probs, given, 3) + self.assertEqual(thresholds[1], np.inf) + self.assertEqual(thresholds[2], np.inf) + self.assertIn("[1, 2]", "".join(logs.output)) + + def test_an_unlabelled_class_is_never_suggested(self): + # Class 2 has no labelled sample, yet the model scores it highest on both rows. Without the + # inf threshold it would be "suggested" on evidence the corpus says nothing about. + probs = np.array([[0.2, 0.1, 0.7], [0.15, 0.05, 0.80]]) + labels = ["joy", "joy"] + report = detect_label_issues(probs, labels, ["a", "b"], _NAMES) + self.assertEqual([i.suggested_label for i in report.issues], []) + + +class TestConfidentJoint(unittest.TestCase): + def test_counts_given_against_confidently_predicted(self): + # t = [0.5, 0.5]. Row 0: only class 0 clears -> C[0,0]. Row 1: only class 1 clears, given 0 + # -> C[0,1]. Row 2: only class 1 clears, given 1 -> C[1,1]. + probs = np.array([[0.9, 0.1], [0.2, 0.8], [0.3, 0.7]]) + given = np.array([0, 0, 1]) + counts = _confident_joint(probs, given, np.array([0.5, 0.5])) + np.testing.assert_array_equal(counts, [[1, 1], [0, 1]]) + + def test_a_sample_clearing_no_threshold_abstains(self): + # This is what separates the confident joint from a plain confusion matrix: the uncertain row + # is dropped rather than forced onto its argmax. + probs = np.array([[0.55, 0.45]]) + counts = _confident_joint(probs, np.array([0]), np.array([0.9, 0.9])) + np.testing.assert_array_equal(counts, np.zeros((2, 2), dtype=int)) + + def test_identical_self_confidences_still_clear_their_threshold(self): + # A threshold is the mean of the very values compared against it, so exact `>=` loses to + # float rounding when a class's samples share a confidence — the class would abstain entirely. + true = np.array([0, 0, 1, 1, 2, 2]) + probs = _confident_probs(true) + probs = probs / probs.sum(axis=1, keepdims=True) + thresholds = _class_thresholds(probs, true, 3) + counts = _confident_joint(probs, true, thresholds) + np.testing.assert_array_equal(np.diag(counts), [2, 2, 2]) + + +class TestCalibrate(unittest.TestCase): + def test_rows_are_rescaled_to_the_observed_class_counts_and_sum_to_one(self): + # Class 0 has 10 labelled samples but only 5 confident votes; calibration restores the + # marginal so the joint reads as a fraction of the corpus. + counts = np.array([[4, 1], [0, 5]]) + given = np.array([0] * 10 + [1] * 10) + joint = _calibrate(counts, given, 2) + self.assertAlmostEqual(joint.sum(), 1.0) + np.testing.assert_allclose(joint.sum(axis=1), [0.5, 0.5]) + self.assertAlmostEqual(joint[0, 1], (1 / 5) * 10 / 20) + + def test_all_zero_counts_stay_zero_instead_of_dividing_by_zero(self): + joint = _calibrate(np.zeros((2, 2), dtype=int), np.array([0, 1]), 2) + np.testing.assert_array_equal(joint, np.zeros((2, 2))) + + +class TestQualityScores(unittest.TestCase): + def test_self_confidence_is_the_given_labels_probability(self): + probs = np.array([[0.7, 0.3], [0.1, 0.9]]) + np.testing.assert_allclose(_quality_scores(probs, np.array([0, 0]), "self_confidence"), [0.7, 0.1]) + + def test_normalized_margin_subtracts_the_best_competitor(self): + probs = np.array([[0.7, 0.3], [0.1, 0.9]]) + np.testing.assert_allclose(_quality_scores(probs, np.array([0, 0]), "normalized_margin"), [0.4, -0.8]) + + def test_the_two_methods_can_disagree_on_ordering(self): + # Row 0 is merely uncertain; row 1 has a confident alternative. Self-confidence prefers row 1 + # as the worse label, the margin agrees more strongly — the ranking is not interchangeable. + probs = np.array([[0.34, 0.33, 0.33], [0.30, 0.65, 0.05]]) + given = np.array([0, 0]) + conf = _quality_scores(probs, given, "self_confidence") + margin = _quality_scores(probs, given, "normalized_margin") + self.assertLess(conf[1], conf[0]) + self.assertLess(margin[1], margin[0]) + self.assertGreater(margin[0], 0.0) # nothing beats the given label + self.assertLess(margin[1], 0.0) # something does + + def test_every_documented_method_is_accepted(self): + probs = np.array([[0.7, 0.3], [0.1, 0.9]]) + for method in SCORE_METHODS: + self.assertEqual(_quality_scores(probs, np.array([0, 0]), method).shape, (2,)) + + +class TestPruneByNoiseRate(unittest.TestCase): + def test_never_suggests_a_class_the_model_ranks_below_the_given_label(self): + # The joint asks for 2 errors in cell (0, 1), but only one sample actually favours class 1. + # Taking the second would "suggest" a class the model scores lower than the label already on + # the row. + probs = np.array([[0.2, 0.8], [0.9, 0.1]]) + joint = np.array([[0.0, 1.0], [0.0, 0.0]]) + selected = _prune_by_noise_rate(probs, np.array([0, 0]), joint, 2) + self.assertEqual(selected, {0: 1}) + + def test_a_cell_with_no_positive_margin_selects_nothing(self): + # The joint asks for an error in cell (0, 1), but the model prefers the given label on every + # class-0 sample, so there is no defensible row to flag. + probs = np.array([[0.9, 0.1], [0.8, 0.2]]) + joint = np.array([[0.0, 1.0], [0.0, 0.0]]) + self.assertEqual(_prune_by_noise_rate(probs, np.array([0, 0]), joint, 2), {}) + + def test_a_sample_claimed_by_two_cells_keeps_the_larger_margin(self): + probs = np.array([[0.1, 0.3, 0.6]]) + joint = np.array([[0.0, 1.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + selected = _prune_by_noise_rate(probs, np.array([0]), joint, 3) + self.assertEqual(selected, {0: 2}) # margin 0.5 beats 0.2 + + +class TestDetectLabelIssues(unittest.TestCase): + def test_recovers_planted_mislabels_exactly(self): + probs, labels, texts, planted = _planted_corpus() + report = detect_label_issues(probs, labels, texts, _NAMES, top_n=100) + self.assertEqual(report.n_issues, len(planted)) + self.assertEqual({i.index for i in report.issues}, planted) + for issue in report.issues: + self.assertEqual(issue.given_label, "surprise") + self.assertEqual(issue.suggested_label, "joy") + self.assertGreater(issue.suggested_prob, issue.given_prob) + + def test_estimates_the_noise_matrix_and_rate(self): + probs, labels, texts, planted = _planted_corpus(n=300, n_flips=15) + report = detect_label_issues(probs, labels, texts, _NAMES, top_n=100) + self.assertAlmostEqual(report.noise_matrix.sum(), 1.0) + self.assertAlmostEqual(report.noise_rate, len(planted) / 300, places=6) + # The contamination is surprise-labelled rows that are really joy, and nothing else. + self.assertAlmostEqual(report.noise_matrix[1, 0], len(planted) / 300, places=6) + off_diagonal = report.noise_matrix.copy() + np.fill_diagonal(off_diagonal, 0.0) + off_diagonal[1, 0] = 0.0 + np.testing.assert_allclose(off_diagonal, 0.0, atol=1e-12) + + def test_a_clean_corpus_yields_no_issues_and_a_diagonal_matrix(self): + rng = np.random.default_rng(3) + true = rng.integers(0, 3, 120) + probs = _confident_probs(true) + report = detect_label_issues(probs, [_NAMES[i] for i in true], [f"t{i}" for i in range(120)], _NAMES) + self.assertEqual(report.issues, []) + self.assertEqual(report.n_issues, 0) + self.assertAlmostEqual(report.noise_rate, 0.0) + np.testing.assert_allclose(report.noise_matrix.sum(axis=1), np.bincount(true) / 120) + + def test_issues_are_ranked_worst_first(self): + probs, labels, texts, _ = _planted_corpus() + report = detect_label_issues(probs, labels, texts, _NAMES, top_n=100, score="normalized_margin") + scores = [i.score for i in report.issues] + self.assertEqual(scores, sorted(scores)) + + def test_top_n_truncates_the_list_but_not_the_count(self): + probs, labels, texts, planted = _planted_corpus() + report = detect_label_issues(probs, labels, texts, _NAMES, top_n=4) + self.assertEqual(len(report.issues), 4) + self.assertEqual(report.n_issues, len(planted)) + # The kept ones are the worst-scoring of the full set. + full = detect_label_issues(probs, labels, texts, _NAMES, top_n=100) + self.assertEqual([i.index for i in report.issues], [i.index for i in full.issues[:4]]) + + def test_top_n_zero_returns_no_issues_but_still_reports_the_count(self): + probs, labels, texts, planted = _planted_corpus() + report = detect_label_issues(probs, labels, texts, _NAMES, top_n=0) + self.assertEqual(report.issues, []) + self.assertEqual(report.n_issues, len(planted)) + + def test_issues_carry_their_text_and_position(self): + probs, labels, texts, _ = _planted_corpus() + issue = detect_label_issues(probs, labels, texts, _NAMES, top_n=1).issues[0] + self.assertEqual(issue.text, texts[issue.index]) + self.assertIsNone(issue.probe) # the second opinion is the caller's, not the algorithm's + + def test_report_is_a_frozen_dataclass_with_the_documented_fields(self): + probs, labels, texts, _ = _planted_corpus(n=30, n_flips=2) + report = detect_label_issues(probs, labels, texts, _NAMES) + self.assertIsInstance(report, LabelNoiseReport) + self.assertEqual(report.label_names, _NAMES) + self.assertEqual(report.n_samples, 30) + self.assertEqual(report.thresholds.shape, (3,)) + with self.assertRaises(Exception): + report.issues = [] # type: ignore[misc] + + def test_warns_when_probability_rows_do_not_sum_to_one(self): + probs = np.array([[0.2, 0.2], [0.9, 0.05]]) + with self.assertLogs("shapash.compute.diagnostics.label_noise", level=logging.WARNING) as logs: + detect_label_issues(probs, ["joy", "joy"], ["a", "b"], ["joy", "surprise"]) + self.assertIn("do not sum to 1", "".join(logs.output)) + + +class TestValidation(unittest.TestCase): + def setUp(self): + self.probs = np.array([[0.9, 0.1], [0.2, 0.8]]) + self.labels = ["joy", "surprise"] + self.texts = ["a", "b"] + self.names = ["joy", "surprise"] + + def test_rejects_non_2d_probabilities(self): + with self.assertRaisesRegex(ValueError, "must be 2-D"): + detect_label_issues(np.array([0.9, 0.1]), self.labels, self.texts, self.names) + + def test_rejects_fewer_than_two_classes(self): + with self.assertRaisesRegex(ValueError, "at least 2 classes"): + detect_label_issues(np.array([[1.0], [1.0]]), self.labels, self.texts, ["joy"]) + + def test_rejects_label_names_of_the_wrong_length(self): + with self.assertRaisesRegex(ValueError, "label_names has 3 entries"): + detect_label_issues(self.probs, self.labels, self.texts, _NAMES) + + def test_rejects_mismatched_labels_length(self): + with self.assertRaisesRegex(ValueError, "labels has 1 entries"): + detect_label_issues(self.probs, ["joy"], self.texts, self.names) + + def test_rejects_mismatched_texts_length(self): + with self.assertRaisesRegex(ValueError, "texts has 1 entries"): + detect_label_issues(self.probs, self.labels, ["a"], self.names) + + def test_rejects_a_label_absent_from_label_names(self): + with self.assertRaisesRegex(ValueError, r"\['anger'\]"): + detect_label_issues(self.probs, ["joy", "anger"], self.texts, self.names) + + def test_rejects_an_unknown_score_method(self): + with self.assertRaisesRegex(ValueError, "Unknown score method"): + detect_label_issues(self.probs, self.labels, self.texts, self.names, score="entropy") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/compute/diagnostics/test_label_probe.py b/tests/unit_tests/compute/diagnostics/test_label_probe.py new file mode 100644 index 00000000..4967bd92 --- /dev/null +++ b/tests/unit_tests/compute/diagnostics/test_label_probe.py @@ -0,0 +1,141 @@ +"""Unit tests for the model-independent label probe.""" + +import logging +import unittest + +from shapash.compute.diagnostics.label_probe import MIN_PER_CLASS, LabelProbe, ProbeVerdict + +# A corpus with two lexically separable classes, so a bag-of-words probe is reliable on it. +POSITIVE = [ + "this film is wonderful and moving", + "a wonderful delightful movie", + "delightful and heartwarming story", + "i loved this wonderful picture", + "heartwarming and delightful throughout", + "a moving and wonderful tale", +] +NEGATIVE = [ + "this film is dreadful and boring", + "a dreadful tedious movie", + "tedious and boring story", + "i hated this dreadful picture", + "boring and tedious throughout", + "a tedious and dreadful tale", +] +TEXTS = POSITIVE + NEGATIVE +LABELS = ["pos"] * len(POSITIVE) + ["neg"] * len(NEGATIVE) + + +class TestConstruction(unittest.TestCase): + """Input validation and corpus reporting.""" + + def test_mismatched_lengths_raise(self): + with self.assertRaises(ValueError) as ctx: + LabelProbe(TEXTS, LABELS[:-1]) + self.assertIn("must match", str(ctx.exception)) + + def test_single_class_corpus_raises(self): + with self.assertRaises(ValueError) as ctx: + LabelProbe(POSITIVE, ["pos"] * len(POSITIVE)) + self.assertIn("at least 2 classes", str(ctx.exception)) + + def test_classes_reports_the_reference_labels(self): + self.assertEqual(LabelProbe(TEXTS, LABELS).classes, ["neg", "pos"]) + + def test_thin_class_warns_but_still_builds(self): + texts = [*POSITIVE, "a dreadful tedious movie"] + labels = ["pos"] * len(POSITIVE) + ["neg"] + with self.assertLogs("shapash.compute.diagnostics.label_probe", level=logging.WARNING) as logs: + probe = LabelProbe(texts, labels) + self.assertIn("neg", logs.output[0]) + self.assertIn(str(MIN_PER_CLASS), logs.output[0]) + self.assertEqual(probe.classes, ["neg", "pos"]) + + def test_values_are_coerced_to_str(self): + probe = LabelProbe(TEXTS, [1] * 6 + [0] * 6) + self.assertEqual(probe.classes, ["0", "1"]) + + +class TestVerdicts(unittest.TestCase): + """The per-row second opinion.""" + + def setUp(self): + self.probe = LabelProbe(TEXTS, LABELS) + + def test_backs_a_label_the_corpus_supports(self): + [verdict] = self.probe.verdicts(["a wonderful and heartwarming film"], ["pos"]) + self.assertIsInstance(verdict, ProbeVerdict) + self.assertTrue(verdict.backs_given) + self.assertEqual(verdict.top_label, "pos") + self.assertGreater(verdict.given_prob, 0.5) + + def test_rejects_a_label_the_corpus_contradicts(self): + # The text is plainly negative but carries the positive label: the probe should side against it. + [verdict] = self.probe.verdicts(["a dreadful boring tedious film"], ["pos"]) + self.assertFalse(verdict.backs_given) + self.assertEqual(verdict.top_label, "neg") + self.assertLess(verdict.given_prob, 0.5) + + def test_given_prob_is_the_probability_of_the_given_label_not_the_top_one(self): + text = "a dreadful boring tedious film" + [as_pos] = self.probe.verdicts([text], ["pos"]) + [as_neg] = self.probe.verdicts([text], ["neg"]) + # Same text, same model: only which column is read changes, and the two must sum to 1. + self.assertAlmostEqual(as_pos.given_prob + as_neg.given_prob, 1.0, places=6) + self.assertEqual(as_pos.top_label, as_neg.top_label) + + def test_label_absent_from_the_reference_corpus_scores_zero(self): + [verdict] = self.probe.verdicts(["a wonderful film"], ["surprise"]) + self.assertEqual(verdict.given_prob, 0.0) + self.assertFalse(verdict.backs_given) + self.assertIn(verdict.top_label, {"pos", "neg"}) + + def test_batch_preserves_input_order(self): + verdicts = self.probe.verdicts( + ["a wonderful heartwarming film", "a dreadful tedious film"], ["pos", "neg"] + ) + self.assertEqual([v.top_label for v in verdicts], ["pos", "neg"]) + self.assertTrue(all(v.backs_given for v in verdicts)) + + def test_empty_input_returns_empty_without_fitting(self): + probe = LabelProbe(TEXTS, LABELS) + self.assertEqual(probe.verdicts([], []), []) + self.assertIsNone(probe._pipeline) + + def test_mismatched_batch_lengths_raise(self): + with self.assertRaises(ValueError) as ctx: + self.probe.verdicts(["a", "b"], ["pos"]) + self.assertIn("must match", str(ctx.exception)) + + def test_verdict_is_frozen(self): + [verdict] = self.probe.verdicts(["a wonderful film"], ["pos"]) + with self.assertRaises(Exception): + verdict.given_prob = 0.0 # type: ignore[misc] + + +class TestFit(unittest.TestCase): + """Fitting is lazy and happens once.""" + + def test_fit_is_idempotent_and_reuses_the_pipeline(self): + probe = LabelProbe(TEXTS, LABELS) + self.assertIsNone(probe._pipeline) + probe.fit() + first = probe._pipeline + probe.fit() + self.assertIs(probe._pipeline, first) + + def test_verdicts_fits_on_demand(self): + probe = LabelProbe(TEXTS, LABELS) + probe.verdicts(["a wonderful film"], ["pos"]) + self.assertIsNotNone(probe._pipeline) + + def test_probe_is_independent_of_any_model(self): + # The whole point: nothing about the audited model reaches the probe. Constructing and + # scoring must work with no model, backend or explainer in sight. + probe = LabelProbe(TEXTS, LABELS) + [verdict] = probe.verdicts(["wonderful delightful"], ["pos"]) + self.assertTrue(verdict.backs_given) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 2ecc24b6..679d2f00 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -20,9 +20,11 @@ from shapash.backend.nlp_lime_backend import NlpLimeBackend from shapash.backend.nlp_shap_backend import NlpShapBackend from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator +from shapash.compute.retrieval import Neighbor from shapash.explainer.nlp_explainer import NlpExplainer from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.plots.plot_noise_matrix import plot_noise_matrix from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall @@ -458,6 +460,45 @@ def test_orientation_horizontal(self): # --------------------------------------------------------------------------- +class TestPlotNoiseMatrix(unittest.TestCase): + """The noise matrix is given-vs-true and mostly diagonal, so it needs its own axes and masking.""" + + def setUp(self): + # Typical shape: ~94% correctly labelled, one contaminated pair (A labelled, really B). + self.joint = np.array([[0.50, 0.05, 0.0], [0.01, 0.30, 0.0], [0.0, 0.0, 0.14]]) + self.labels = ["A", "B", "C"] + + def test_returns_heatmap_figure(self): + self.assertIsInstance(plot_noise_matrix(self.joint, self.labels), go.Figure) + + def test_diagonal_is_masked_by_default(self): + # Left in, the ~90% diagonal flattens every off-diagonal cell to the same near-white shade. + z = plot_noise_matrix(self.joint, self.labels).data[0].z + self.assertTrue(np.all(np.isnan(np.diag(z)))) + self.assertAlmostEqual(z[0][1], 0.05) + + def test_masked_cells_render_no_text(self): + text = plot_noise_matrix(self.joint, self.labels).data[0].text + self.assertEqual(text[0][0], "") + self.assertEqual(text[0][1], "5.0%") + + def test_diagonal_can_be_kept(self): + z = plot_noise_matrix(self.joint, self.labels, mask_diagonal=False).data[0].z + np.testing.assert_allclose(np.diag(z), [0.50, 0.30, 0.14]) + + def test_does_not_mutate_the_callers_matrix(self): + original = self.joint.copy() + plot_noise_matrix(self.joint, self.labels) + np.testing.assert_array_equal(self.joint, original) + + def test_axes_name_the_label_semantics_not_the_prediction(self): + # The whole reason this is not a plot_confusion_matrix mode: the columns are the estimated + # *true* class, not the model's prediction. + layout = plot_noise_matrix(self.joint, self.labels).layout + self.assertEqual(layout.yaxis.title.text, "Given label") + self.assertEqual(layout.xaxis.title.text, "Estimated true class") + + class TestPlotConfusionMatrix(unittest.TestCase): def setUp(self): # 3-class matrix: rows = true, cols = predicted. Row 2 (index 2) is empty. @@ -1405,5 +1446,169 @@ def test_clear_cache_leaves_other_backends_entries_intact(self): self.assertTrue(keeper_path.exists(), "clearing one backend dropped another's cache") +class TestDetectLabelNoise(unittest.TestCase): + """The explainer's confident-learning surface, on the __new__-built stub (no model, no backend).""" + + def _with_labels(self, y_true=None, y_prob=None): + """A compiled stub carrying ground truth and per-class probabilities. + + Sample 1 is labelled ``joy`` while the model confidently says ``sadness`` — the planted + error. Both classes carry at least one label, without which ``sadness`` would have no + estimable threshold and could never be suggested (covered separately in the compute tests). + """ + xpl = _make_explainer() + confident = ["joy", "sadness", "sadness"] + probs = np.full((3, N_CLASSES), 0.02) + for row, name in enumerate(confident): + probs[row, LABEL_NAMES.index(name)] = 0.90 + probs = probs / probs.sum(axis=1, keepdims=True) + xpl.y_pred = pd.Series(confident, index=pd.RangeIndex(3), name="prediction") + xpl.y_prob = pd.DataFrame(probs, index=pd.RangeIndex(3), columns=LABEL_NAMES) if y_prob is None else y_prob + xpl.y_true = ( + pd.Series(["joy", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") + if y_true is None + else y_true + ) + return xpl + + # ── capability flag ──────────────────────────────────────────────── + def test_available_with_ground_truth_and_per_class_probabilities(self): + self.assertTrue(self._with_labels().can_detect_label_noise()) + + def test_unavailable_without_ground_truth(self): + xpl = self._with_labels() + xpl.y_true = None + self.assertFalse(xpl.can_detect_label_noise()) + + def test_unavailable_with_only_the_winning_class_probability(self): + # The raw-pipeline path emits a single "probability" column; the losing classes' scores are + # precisely what confident learning needs. + legacy = pd.DataFrame({"probability": [0.9, 0.9, 0.9]}, index=pd.RangeIndex(3)) + self.assertFalse(self._with_labels(y_prob=legacy).can_detect_label_noise()) + + def test_unavailable_on_a_stub_that_never_set_y_prob(self): + # The __new__ path sets neither attribute; getattr must not raise. + self.assertFalse(_make_explainer().can_detect_label_noise()) + + def test_available_without_a_model(self): + # Unlike the other capability flags this needs no live model, so it survives from_snapshot(). + xpl = self._with_labels() + self.assertIsNone(xpl.model) + self.assertTrue(xpl.can_detect_label_noise()) + + # ── detection ────────────────────────────────────────────────────── + def test_raises_when_the_prerequisites_are_missing(self): + with self.assertRaisesRegex(RuntimeError, "ground-truth labels"): + _make_explainer().detect_label_noise() + + def test_flags_the_planted_mislabel(self): + report = self._with_labels().detect_label_noise() + self.assertEqual([i.index for i in report.issues], [1]) + issue = report.issues[0] + self.assertEqual(issue.given_label, "joy") + self.assertEqual(issue.suggested_label, "sadness") + self.assertEqual(issue.text, "this is terrible and sad") + + def test_label_names_come_from_the_probability_columns(self): + report = self._with_labels().detect_label_noise() + self.assertEqual(report.label_names, LABEL_NAMES) + self.assertEqual(report.noise_matrix.shape, (N_CLASSES, N_CLASSES)) + self.assertEqual(report.n_samples, 3) + + def test_respects_top_n_and_score(self): + report = self._with_labels().detect_label_noise(top_n=0, score="normalized_margin") + self.assertEqual(report.issues, []) + self.assertEqual(report.n_issues, 1) + + # ── memoisation ──────────────────────────────────────────────────── + def test_repeats_are_served_from_the_memo(self): + xpl = self._with_labels() + first = xpl.detect_label_noise(top_n=5) + self.assertIs(xpl.detect_label_noise(top_n=5), first) + + def test_different_arguments_recompute(self): + xpl = self._with_labels() + self.assertIsNot(xpl.detect_label_noise(top_n=5), xpl.detect_label_noise(top_n=4)) + + # ── independent probe ────────────────────────────────────────────── + def _probe_corpus(self): + """A reference corpus separable by words the audited fixture's texts also use.""" + texts = [ + "this is wonderful and joyful", + "wonderful joyful and bright", + "a joyful wonderful day", + "bright and wonderful joy", + "this is terrible and sad", + "terrible sad and bleak", + "a sad terrible day", + "bleak and terrible sadness", + ] + labels = ["joy"] * 4 + ["sadness"] * 4 + return texts, labels + + def test_no_probe_when_no_reference_corpus_is_bound(self): + xpl = self._with_labels() + self.assertFalse(xpl.can_probe_labels()) + self.assertIsNone(xpl.detect_label_noise().issues[0].probe) + + def test_probe_verdict_is_attached_when_a_labelled_corpus_is_bound(self): + xpl = self._with_labels() + xpl._reference_corpus = self._probe_corpus() + self.assertTrue(xpl.can_probe_labels()) + issue = xpl.detect_label_noise().issues[0] + self.assertIsNotNone(issue.probe) + self.assertIn(issue.probe.top_label, {"joy", "sadness"}) + self.assertEqual(issue.probe.backs_given, issue.probe.top_label == issue.given_label) + + def test_probe_corroborates_a_genuine_label_error(self): + # The flagged row is "this is terrible and sad" carrying the label "joy". The reference + # corpus puts that vocabulary firmly in "sadness", so the probe rejects the given label too + # — the two-signals-agree case, which is the one worth relabelling. + xpl = self._with_labels() + xpl._reference_corpus = self._probe_corpus() + issue = xpl.detect_label_noise().issues[0] + self.assertEqual((issue.given_label, issue.text), ("joy", "this is terrible and sad")) + self.assertFalse(issue.probe.backs_given) + self.assertEqual(issue.probe.top_label, "sadness") + self.assertLess(issue.probe.given_prob, 0.5) + + def test_probe_backs_the_label_when_the_corpus_sides_with_it(self): + # The mirror case, and the reason the column exists: same flagged row, but a corpus that + # calls this vocabulary "joy". The probe now defends the label, marking the row as the + # audited model's error rather than the corpus's. + xpl = self._with_labels() + texts, _ = self._probe_corpus() + xpl._reference_corpus = (texts, ["sadness"] * 4 + ["joy"] * 4) + issue = xpl.detect_label_noise().issues[0] + self.assertEqual(issue.given_label, "joy") + self.assertTrue(issue.probe.backs_given) + self.assertGreater(issue.probe.given_prob, 0.5) + + def test_probe_is_skipped_when_not_requested(self): + xpl = self._with_labels() + xpl._reference_corpus = self._probe_corpus() + self.assertIsNone(xpl.detect_label_noise(probe=False).issues[0].probe) + + def test_probe_is_fit_once_and_reused_across_calls(self): + xpl = self._with_labels() + xpl._reference_corpus = self._probe_corpus() + xpl.detect_label_noise(top_n=5) + first = xpl._label_probe + self.assertIsNotNone(first) + xpl.detect_label_noise(top_n=4) # different args -> recompute, but the probe is kept + self.assertIs(xpl._label_probe, first) + + def test_probe_needs_no_model_or_retriever(self): + # The point of fitting on plain text: a prediction-only pipeline that cannot embed (so + # can_find_similar() is False) still gets the second opinion. + xpl = self._with_labels() + xpl._reference_corpus = self._probe_corpus() + xpl._retriever = None + xpl._text_model = None + self.assertFalse(xpl.can_find_similar()) + self.assertTrue(xpl.can_probe_labels()) + self.assertIsNotNone(xpl.detect_label_noise().issues[0].probe) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 1bd850ff..36f7b3b3 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -6,23 +6,35 @@ """ import unittest +from dataclasses import replace import numpy as np import pandas as pd from shapash.backend.nlp_backend import NlpContributions +from shapash.compute.diagnostics.label_noise import LabelIssue, detect_label_issues +from shapash.compute.diagnostics.label_probe import LabelProbe, ProbeVerdict from shapash.compute.generators.base import Counterfactual, IntField, TokenListField from shapash.compute.retrieval.similar_examples import Neighbor from shapash.webapp.nlp_app import NlpWebApp from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, + LabelNoiseComponent, SimilarExamplesComponent, ) from shapash.webapp.nlp_view import NlpView LABEL_NAMES = ["neg", "pos"] +# A tiny lexically separable corpus for the independent label probe. "bad"/"awful" sit in "neg", +# so a row labelled "neg" gets backed and a row labelled "pos" gets rejected. +_PROBE_CORPUS = ( + ["i am happy", "happy and glad", "so glad today", "a happy glad day", + "this is bad", "bad and awful", "so awful today", "a bad awful day"], + ["pos"] * 4 + ["neg"] * 4, +) + def _contributions() -> NlpContributions: token_strings = [["i", "am", "happy"], ["this", "is", "bad"]] @@ -37,7 +49,15 @@ def _contributions() -> NlpContributions: class FakeEngine: """Minimal explainer/engine stand-in exposing the InteractiveEngine surface + compiled data.""" - def __init__(self, can_edit: bool, can_cf: bool, can_similar: bool = False): + def __init__( + self, + can_edit: bool, + can_cf: bool, + can_similar: bool = False, + has_labels: bool = False, + probe_corpus: tuple[list[str], list[str]] | None = None, + ): + self.probe_corpus = probe_corpus self._can_edit = can_edit self._can_cf = can_cf self._can_similar = can_similar @@ -49,6 +69,43 @@ def __init__(self, can_edit: bool, can_cf: bool, can_similar: bool = False): self.y_pred = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="prediction") self.y_prob = pd.DataFrame({"neg": [0.2, 0.8], "pos": [0.8, 0.2]}, index=pd.RangeIndex(2)) self.y_true = None + if has_labels: + # A batch with exactly one planted label error. With two samples and two classes the + # arrangement is forced: both classes must carry a label (or the unlabelled one has no + # estimable threshold and can never be suggested), so sample 0 is labelled "neg" while + # the model confidently says "pos", and sample 1 is labelled "pos" and agrees. + self.y_prob = pd.DataFrame({"neg": [0.1, 0.1], "pos": [0.9, 0.9]}, index=pd.RangeIndex(2)) + self.y_pred = pd.Series(["pos", "pos"], index=pd.RangeIndex(2), name="prediction") + self.y_true = pd.Series(["neg", "pos"], index=pd.RangeIndex(2), name="ground_truth") + # Ground truth is off by default so the existing layout tests, which pin the exact tab + # groups, keep seeing neither the Error Analysis nor the Label Noise tab. + self.detect_calls = [] + + def can_detect_label_noise(self): + return self.y_true is not None + + def can_probe_labels(self): + return self.probe_corpus is not None + + def detect_label_noise(self, top_n=50, score="self_confidence", probe=True): + self.detect_calls.append({"top_n": top_n, "score": score, "probe": probe}) + report = detect_label_issues( + self.y_prob.to_numpy(dtype=float), + [str(v) for v in self.y_true.tolist()], + [str(t) for t in self.texts.tolist()], + list(self.y_prob.columns), + top_n=top_n, + score=score, + ) + if probe and self.can_probe_labels() and report.issues: + verdicts = LabelProbe(*self.probe_corpus).verdicts( + [i.text for i in report.issues], [i.given_label for i in report.issues] + ) + report = replace( + report, + issues=[replace(i, probe=v) for i, v in zip(report.issues, verdicts, strict=True)], + ) + return report def can_edit(self): return self._can_edit @@ -395,5 +452,260 @@ def test_neighbors_table_without_labels(self): self.assertIsNotNone(table) +class TestLabelNoiseMounting(unittest.TestCase): + """``CAP_LABELS`` is a *data* capability: it depends on the compiled batch, not on a live model.""" + + def _ids(self, engine): + app = NlpWebApp(engine) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + def test_mounts_with_ground_truth_and_per_class_probabilities(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, has_labels=True)) + self.assertIn("label-noise-detect-btn", ids) + self.assertIn("label-noise-results", ids) + self.assertIn("label-noise", app._tab_groups["upper-right-tabs"]) + self.assertTrue(any(isinstance(c, LabelNoiseComponent) for c in app._components)) + + def test_hidden_without_ground_truth(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertNotIn("label-noise-detect-btn", ids) + self.assertNotIn("label-noise", app._tab_groups["upper-right-tabs"]) + + def test_hidden_when_only_the_winning_probability_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) + engine.y_prob = pd.DataFrame({"probability": [0.8, 0.8]}, index=pd.RangeIndex(2)) + _, ids = self._ids(engine) + self.assertNotIn("label-noise-detect-btn", ids) + + def test_mounts_without_any_live_capability(self): + # The snapshot case: no model, so no editor/counterfactual/similar panel — but the labels and + # probabilities are still in the compiled batch, so this panel stands on its own. + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, has_labels=True)) + self.assertIn("label-noise-detect-btn", ids) + self.assertNotIn("data-editor-input", ids) + + def test_caption_warns_when_no_independent_cross_check_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True) + layout = LabelNoiseComponent().layout(NlpView(engine), engine) + caption = layout.children[1].children + self.assertIn("no independent cross-check", caption) + + def test_caption_explains_the_corpus_column_when_the_probe_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, probe_corpus=_PROBE_CORPUS) + layout = LabelNoiseComponent().layout(NlpView(engine), engine) + caption = layout.children[1].children + self.assertIn("Corpus column", caption) + + def test_callbacks_registered_when_mounted(self): + app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True, has_labels=True)) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("label-noise-results.children", outputs) + + +class TestLabelNoiseComponent(unittest.TestCase): + """Drive the panel's renderer and callbacks directly, without the app shell.""" + + @staticmethod + def _register(**kwargs): + import dash + + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, **kwargs) + view = NlpView(engine) + app = dash.Dash(__name__) + comp = LabelNoiseComponent() + comp.register_callbacks(app, view, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + return app, engine + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_detect_returns_a_view_and_the_flagged_indices(self): + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + children, indices = detect(1, 50, "self_confidence") + self.assertEqual(indices, [0]) # sample 0 is labelled neg but confidently predicted pos + self.assertIsNotNone(children) + + def test_detect_forwards_the_controls_to_the_engine(self): + app, engine = self._register() + detect = self._callback(app, "label-noise-results") + detect(1, 7, "normalized_margin") + self.assertEqual(engine.detect_calls[-1], {"top_n": 7, "score": "normalized_margin", "probe": True}) + + def test_blank_controls_fall_back_to_defaults(self): + app, engine = self._register() + detect = self._callback(app, "label-noise-results") + detect(1, None, None) + self.assertEqual(engine.detect_calls[-1], {"top_n": 50, "score": "self_confidence", "probe": True}) + + def test_detect_renders_the_corpus_column_when_a_probe_corpus_is_bound(self): + app, _ = self._register(probe_corpus=_PROBE_CORPUS) + detect = self._callback(app, "label-noise-results") + children, _ = detect(1, 50, "self_confidence") + table = children.children[-1] + headers = [th.children for th in table.children[0].children.children] + self.assertEqual(headers, ["Score", "Given", "Probably", "Corpus", "Text", ""]) + # The flagged row is "i am happy" labelled "neg"; the corpus puts that vocabulary in "pos", + # so the probe rejects the label too rather than defending it. + cell = table.children[1].children[0].children[3].children + self.assertEqual(cell.children, "0.14 → pos") + + def test_detect_omits_the_corpus_column_without_a_probe_corpus(self): + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + children, _ = detect(1, 50, "self_confidence") + table = children.children[-1] + headers = [th.children for th in table.children[0].children.children] + self.assertNotIn("Corpus", headers) + + def test_detect_does_nothing_before_the_button_is_clicked(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + with self.assertRaises(PreventUpdate): + detect(None, 50, "self_confidence") + + def test_a_clean_corpus_renders_a_message_and_no_rows(self): + app, engine = self._register() + # Both classes labelled and both labels agreeing with the model, so there is no noise to find. + engine.y_prob = pd.DataFrame({"neg": [0.1, 0.9], "pos": [0.9, 0.1]}, index=pd.RangeIndex(2)) + engine.y_true = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="ground_truth") + detect = self._callback(app, "label-noise-results") + children, indices = detect(1, 50, "self_confidence") + self.assertEqual(indices, []) + self.assertIn("No label issues detected", children.children) + + def test_inspect_makes_the_flagged_sample_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 0} + dp = inspect([1], [1]) # row 0 of the table points at compiled sample 1 + self.assertEqual(dp["text"], "this is bad") + self.assertEqual(dp["orig_idx"], 1) + self.assertEqual(dp["label"], "pos") # the *prediction*, matching the dataset-row path + self.assertEqual(len(dp["tokens"]), 3) + + def test_inspect_ignores_a_render_with_no_clicks(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with self.assertRaises(PreventUpdate): + inspect([0], [1]) + + def test_inspect_ignores_a_missing_trigger(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = None + with self.assertRaises(PreventUpdate): + inspect([1], [1]) + + def test_inspect_ignores_an_out_of_range_row(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 5} + with self.assertRaises(PreventUpdate): + inspect([1], [1]) + + def test_inspect_ignores_a_stale_index_beyond_the_corpus(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 0} + with self.assertRaises(PreventUpdate): + inspect([1], [99]) + + +class TestLabelNoiseTable(unittest.TestCase): + """The neighbour column is evidence *beside* the score, and must not appear when absent.""" + + @staticmethod + def _issue(**kwargs): + base = dict( + index=0, + text="i am happy", + given_label="neg", + suggested_label="pos", + given_prob=0.2, + suggested_prob=0.8, + score=0.2, + ) + return LabelIssue(**{**base, **kwargs}) + + def test_corpus_column_omitted_when_no_probe_ran(self): + from shapash.webapp.nlp_components.label_noise import _issues_table + + table = _issues_table([self._issue()], "label-noise") + headers = [th.children for th in table.children[0].children.children] + self.assertNotIn("Corpus", headers) + self.assertEqual(headers, ["Score", "Given", "Probably", "Text", ""]) + + def test_corpus_column_present_when_any_row_has_a_verdict(self): + from shapash.webapp.nlp_components.label_noise import _issues_table + + verdict = ProbeVerdict(given_prob=0.8, top_label="neg", backs_given=True) + issues = [self._issue(probe=verdict), self._issue(index=1)] + table = _issues_table(issues, "label-noise") + headers = [th.children for th in table.children[0].children.children] + self.assertIn("Corpus", headers) + + def test_probe_summary_warns_when_the_corpus_backs_the_given_label(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + # The row the panel exists to catch: confident learning flagged it, but an independent + # classifier defends the label — so it is the model that is wrong, not the corpus. + verdict = ProbeVerdict(given_prob=0.83, top_label="neg", backs_given=True) + summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) + self.assertEqual(summary.children, "⚠ 0.83") + self.assertIn("text-warning", summary.className) + self.assertIn("more likely", summary.title) + + def test_probe_summary_reports_a_corroborated_label_error_plainly(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + verdict = ProbeVerdict(given_prob=0.16, top_label="pos", backs_given=False) + summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) + self.assertEqual(summary.children, "0.16 → pos") + self.assertIn("text-muted", summary.className) + + def test_rows_without_a_verdict_render_a_dash(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + self.assertEqual(_probe_summary(self._issue()), "—") + + if __name__ == "__main__": unittest.main() From a29938cf1d2b2bae8cbc290de5974ee9ea2fd7d2 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 20 Aug 2026 18:07:32 +0200 Subject: [PATCH 094/144] feat(retrieval): add batched query_many with exclude_self MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the batch counterpart of `query` to `SimilarExampleRetriever`: one `embed` call (chunked internally by the model) plus a single matrix product against the cached bank, rather than one forward pass per text. `exclude_self` drops reference rows whose text is *exactly equal* to the query, for callers whose queries may themselves be in the reference corpus. Matching on text equality rather than a similarity cutoff keeps genuine near-duplicates in the results. Purely additive — `query` now delegates its top-k selection to the shared `_top_k` helper and behaves as before. No shipped caller uses `query_many` yet; it is kept as a general batch API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- shapash/compute/retrieval/similar_examples.py | 56 ++++++++++++++++ .../retrieval/test_similar_examples.py | 64 +++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/shapash/compute/retrieval/similar_examples.py b/shapash/compute/retrieval/similar_examples.py index fec079fc..b37667a1 100644 --- a/shapash/compute/retrieval/similar_examples.py +++ b/shapash/compute/retrieval/similar_examples.py @@ -114,6 +114,7 @@ def __init__( self.reference_labels = list(reference_labels) if reference_labels is not None else None self.store = EmbeddingStore(model, self.reference_texts, cache_dir=cache_dir) self._bank: np.ndarray | None = None # (n_reference, hidden), L2-normalized rows + self._positions: dict[str, list[int]] | None = None # text -> reference rows, for exclude_self @property def size(self) -> int: @@ -157,6 +158,59 @@ def query(self, text: str, top_k: int = 5) -> list[Neighbor]: assert self._bank is not None # noqa: S101 - build() guarantees this query_vec = _l2_normalize(self._encode([text]))[0] sims = self._bank @ query_vec # cosine, rows already unit-norm + return self._top_k(sims, top_k) + + def query_many(self, texts: list[str], top_k: int = 5, exclude_self: bool = False) -> list[list[Neighbor]]: + """Return the ``top_k`` most similar reference examples for **each** of ``texts``. + + The batch counterpart of :meth:`query`, for callers ranking a whole set at once (e.g. + corroborating a list of suspected label errors). One :meth:`~shapash.model.base.SupportsEmbeddings.embed` + call — which the model chunks internally — plus a single matrix product against the bank, + rather than one forward pass per text. + + Parameters + ---------- + texts : list of str + The query texts. + top_k : int + Number of neighbours per query (clamped to the corpus size). + exclude_self : bool, optional + Drop reference examples whose text is *exactly equal* to the query. Set this when the + queries may themselves be in the reference corpus, where every text would otherwise + retrieve itself as its own nearest neighbour and crowd out a real one. Matching on text + equality rather than a similarity cutoff keeps genuine near-duplicates in the results. + + Returns + ------- + list[list[Neighbor]] + One neighbour list per query, in the order of ``texts``, each ordered by descending + cosine similarity. + """ + if not texts: + return [] + self.build() + assert self._bank is not None # noqa: S101 - build() guarantees this + query_vecs = _l2_normalize(self._encode(list(texts))) # (n_queries, hidden) + sims = query_vecs @ self._bank.T # (n_queries, n_reference) + if exclude_self: + positions = self._text_positions() + for row, text in enumerate(texts): + hits = positions.get(text) + if hits: + sims[row, hits] = -np.inf + return [self._top_k(sims[row], top_k) for row in range(sims.shape[0])] + + def _text_positions(self) -> dict[str, list[int]]: + """Reference text to the rows holding it, built once (supports ``exclude_self``).""" + if self._positions is None: + positions: dict[str, list[int]] = {} + for i, text in enumerate(self.reference_texts): + positions.setdefault(text, []).append(i) + self._positions = positions + return self._positions + + def _top_k(self, sims: np.ndarray, top_k: int) -> list[Neighbor]: + """Turn one row of similarities into its ``top_k`` :class:`Neighbor` objects, best first.""" k = max(1, min(top_k, self.size)) # argpartition for the top-k, then sort just those descending. top_idx = np.argpartition(-sims, k - 1)[:k] @@ -169,6 +223,8 @@ def query(self, text: str, top_k: int = 5) -> list[Neighbor]: label=self.reference_labels[i] if self.reference_labels is not None else None, ) for i in top_idx + # An excluded row is -inf; it only surfaces when the corpus has nothing else to offer. + if np.isfinite(sims[i]) ] diff --git a/tests/unit_tests/compute/retrieval/test_similar_examples.py b/tests/unit_tests/compute/retrieval/test_similar_examples.py index 8b861730..693fdf63 100644 --- a/tests/unit_tests/compute/retrieval/test_similar_examples.py +++ b/tests/unit_tests/compute/retrieval/test_similar_examples.py @@ -186,6 +186,70 @@ def test_different_spaces_do_not_share_cache(self): self.assertNotEqual(r_dec.store.key, r_pool.store.key) +class TestQueryMany(unittest.TestCase): + """The batch path must agree with the single-text one, and pay one embed call for the lot.""" + + def setUp(self): + self.model = FakeEmbeddingModel() + self.texts = ["happy joy", "joyful glad", "sad down", "miserable"] + self.labels = ["pos", "pos", "neg", "neg"] + self.retriever = SimilarExampleRetriever(self.model, self.texts, self.labels) + + def test_matches_query_row_for_row(self): + queries = ["happy joy", "miserable"] + batched = self.retriever.query_many(queries, top_k=3) + one_at_a_time = [self.retriever.query(q, top_k=3) for q in queries] + self.assertEqual(batched, one_at_a_time) + + def test_embeds_the_whole_batch_in_one_call(self): + self.retriever.build() # pay the bank up front so only query encoding is counted + before = self.model.calls + self.retriever.query_many(["happy joy", "miserable", "neutral"], top_k=2) + self.assertEqual(self.model.calls - before, 1) + + def test_empty_input_returns_empty_without_embedding(self): + before = self.model.calls + self.assertEqual(self.retriever.query_many([], top_k=3), []) + self.assertEqual(self.model.calls, before) + + def test_single_text_batch(self): + [neighbors] = self.retriever.query_many(["happy joy"], top_k=2) + self.assertEqual([n.text for n in neighbors], ["happy joy", "joyful glad"]) + + def test_top_k_is_clamped_to_the_corpus_size(self): + [neighbors] = self.retriever.query_many(["happy joy"], top_k=99) + self.assertEqual(len(neighbors), self.retriever.size) + + def test_exclude_self_drops_the_query_from_its_own_results(self): + # The case that matters: the queries are themselves corpus members, so without this every + # row's nearest neighbour is itself. + [neighbors] = self.retriever.query_many(["happy joy"], top_k=2, exclude_self=True) + self.assertEqual([n.text for n in neighbors], ["joyful glad", "miserable"]) + + def test_exclude_self_removes_every_copy_of_a_duplicated_text(self): + retriever = SimilarExampleRetriever( + FakeEmbeddingModel(), ["happy joy", "happy joy", "sad down"], ["pos", "pos", "neg"] + ) + [neighbors] = retriever.query_many(["happy joy"], top_k=3, exclude_self=True) + self.assertEqual([n.text for n in neighbors], ["sad down"]) + + def test_exclude_self_leaves_texts_that_merely_look_alike(self): + # Matching on text equality, not a similarity cutoff: "joyful glad" is a near-duplicate in + # embedding space and must survive. + [neighbors] = self.retriever.query_many(["happy joy"], top_k=1, exclude_self=True) + self.assertEqual(neighbors[0].text, "joyful glad") + + def test_labels_ride_along_as_in_the_single_query_path(self): + [neighbors] = self.retriever.query_many(["sad down"], top_k=2) + self.assertTrue(all(isinstance(n, Neighbor) for n in neighbors)) + self.assertEqual([n.label for n in neighbors], ["neg", "neg"]) + + def test_unlabelled_corpus_yields_none_labels(self): + retriever = SimilarExampleRetriever(FakeEmbeddingModel(), self.texts) + [neighbors] = retriever.query_many(["happy joy"], top_k=2) + self.assertEqual([n.label for n in neighbors], [None, None]) + + class TestL2Normalize(unittest.TestCase): def test_unit_norm_rows(self): out = _l2_normalize(np.array([[3.0, 4.0], [1.0, 0.0]])) From 23e651ae1318992252d5ace87dd33eec5509ab0d Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 27 Aug 2026 11:51:17 +0200 Subject: [PATCH 095/144] fix: add batch size to HF pipeline --- shapash/backend/nlp_shap_backend.py | 17 +++++++ .../backend/test_nlp_shap_backend.py | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 7c3d1b9d..6354bdae 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -177,6 +177,16 @@ class NlpShapBackend(NlpBackend): keys are forwarded as its constructor arguments). explainer_compute_args : dict, optional Keyword arguments forwarded to the explainer call (``__call__``). + batch_size : int or None, optional + Batch size applied to ``model`` when it is a ``transformers`` pipeline that does not + already have one. Default 64. Pass ``None`` to leave the pipeline untouched. + + A pipeline built without ``batch_size`` scores a list of strings *one at a time*, so an + explanation degenerates into ``max_evals`` (default 500) sequential single-sample forward + passes. Batching lets them pad into one pass — ~1.9x on distilbert-imdb/GPU — for a + numerically identical explanation (max|Δ| 1.1e-07), since the explainer's tree traversal + is untouched. Raise it alongside the explainer's own ``batch_size``, which caps how many + masked variants arrive per call. """ name = "nlp_shap" @@ -189,10 +199,17 @@ def __init__( masker=None, explainer_args: dict | None = None, explainer_compute_args: dict | None = None, + batch_size: int | None = 64, ) -> None: super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.masker = masker + # ``_batch_size`` is what ``transformers.Pipeline.__call__`` reads (``None`` means 1) and + # there is no public setter, so a release that renames it makes this a silent no-op rather + # than an error. A pipeline the caller already configured is never overridden. + if batch_size is not None and getattr(model, "_batch_size", "absent") in (None, 1): + model._batch_size = batch_size + if "explainer" in self.explainer_args: shap_params = {k: v for k, v in self.explainer_args.items() if k != "explainer"} self.explainer = self.explainer_args["explainer"](**shap_params) diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index 3df9112b..9801905b 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -225,5 +225,55 @@ def test_aggregates_subwords_end_to_end(self): np.testing.assert_allclose(raw.base_values, [[0.1 + 1.0 + 4.0, 0.2 + 0.0 + 3.0]]) +class FakePipeline: + """Stands in for a ``transformers`` pipeline: callable, with the private batch-size slot.""" + + def __init__(self, batch_size=None): + self._batch_size = batch_size + self.tokenizer = object() + + def __call__(self, texts, **kwargs): + return np.zeros((len(texts), 2)) + + +class TestPipelineBatchSize(unittest.TestCase): + """A pipeline arriving with no batch size scores masked variants one at a time — see __init__.""" + + def _backend(self, model, **kwargs): + return NlpShapBackend(model=model, explainer_args={"explainer": FakeShapExplainer}, **kwargs) + + def test_sets_batch_size_on_unconfigured_pipeline(self): + pipe = FakePipeline() + self._backend(pipe) + self.assertEqual(pipe._batch_size, 64) + + def test_treats_batch_size_of_one_as_unconfigured(self): + # transformers resolves an unset batch size to 1, so 1 carries no intent. + pipe = FakePipeline(batch_size=1) + self._backend(pipe) + self.assertEqual(pipe._batch_size, 64) + + def test_never_overrides_a_configured_pipeline(self): + pipe = FakePipeline(batch_size=8) + self._backend(pipe) + self.assertEqual(pipe._batch_size, 8) + + def test_explicit_batch_size_is_honoured(self): + pipe = FakePipeline() + self._backend(pipe, batch_size=256) + self.assertEqual(pipe._batch_size, 256) + + def test_none_leaves_the_pipeline_untouched(self): + pipe = FakePipeline() + self._backend(pipe, batch_size=None) + self.assertIsNone(pipe._batch_size) + + def test_bare_callable_is_unaffected(self): + # A LIME-style scoring function has no _batch_size slot; it must not gain one. + model = lambda texts: np.zeros((len(texts), 2)) # noqa: E731 + self._backend(model) + self.assertFalse(hasattr(model, "_batch_size")) + + if __name__ == "__main__": unittest.main() From 4042e17a5109cbd44f472fdca9e65754d87c5d56 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 2 Sep 2026 16:05:53 +0200 Subject: [PATCH 096/144] refactor: replace compile with fit/explain minor side changes: extraction of error analysis component, add threshold filtering to similar examples --- demo/serve_nlp.py | 109 ++- shapash/backend/__init__.py | 2 +- shapash/backend/backend.py | 11 +- shapash/backend/nlp_backend.py | 281 ++----- shapash/backend/nlp_captum_lig_backend.py | 28 +- shapash/backend/nlp_lime_backend.py | 33 +- shapash/backend/nlp_shap_backend.py | 51 +- shapash/compute/embedding_store.py | 46 +- shapash/compute/retrieval/similar_examples.py | 67 +- shapash/explainer/interactive.py | 17 +- shapash/explainer/nlp_explainer.py | 643 ++++++++------- shapash/explainer/nlp_explanation.py | 761 ++++++++++++++++++ shapash/explainer/nlp_plotter.py | 289 +++++++ shapash/plots/plot_word_importance.py | 4 +- shapash/webapp/nlp_app.py | 380 +++------ shapash/webapp/nlp_components/__init__.py | 4 +- shapash/webapp/nlp_components/base.py | 39 +- .../webapp/nlp_components/counterfactual.py | 4 +- shapash/webapp/nlp_components/data_editor.py | 4 +- shapash/webapp/nlp_components/datapoint.py | 4 +- .../webapp/nlp_components/error_analysis.py | 248 ++++++ shapash/webapp/nlp_components/label_noise.py | 37 +- .../nlp_components/sentence_highlight.py | 8 +- .../webapp/nlp_components/similar_examples.py | 149 +++- shapash/webapp/nlp_components/waterfall.py | 7 +- shapash/webapp/nlp_view.py | 77 -- 26 files changed, 2264 insertions(+), 1039 deletions(-) create mode 100644 shapash/explainer/nlp_explanation.py create mode 100644 shapash/explainer/nlp_plotter.py create mode 100644 shapash/webapp/nlp_components/error_analysis.py delete mode 100644 shapash/webapp/nlp_view.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index acb0fd94..93cc0ac9 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -1,23 +1,52 @@ """Serve the NLP explainer **with the What-if Lab** (live model, cached compute). +The shape of the script follows the ``fit`` / ``explain`` split: + +1. ``NlpExplainer(model, backend=...)`` — bind the model. No data, no results. +2. ``xpl.fit(reference_texts, y=reference_labels)`` — learn *reference state*: the similar-examples + corpus and the label probe. Optional; skipped entirely with ``--n-reference 0``. This embeds the + reference corpus (``precompute=True``), so the first run is slower here and the webapp's + "Similar Examples" panel is instant; ``--cache-dir`` makes later runs reload instead. +3. ``explanation = xpl.explain(sentences, y=y_true, cache_dir=...)`` — the expensive step. Returns an + immutable ``NlpExplanation``; the explainer keeps a memo, not a result. +4. Everything downstream takes that artifact as an *argument* — ``compute_projection(explanation, + ...)``, ``can_detect_label_noise(explanation)``, ``run_app(explanation, ...)``. + The What-if Lab needs a live model in memory — editing a sample, re-predicting, and generating -counterfactuals all call the real model, so a model-free snapshot (``from_snapshot``) makes the -lab self-disable. There is therefore no way to serve what-if without loading the model. +counterfactuals all call the real model. That is why ``run_app`` gets both the artifact (what to +render) and ``self`` (how to compute live); serving an artifact alone makes the lab self-disable. +There is therefore no way to serve what-if without loading the model. What *can* be avoided on every restart is recomputing the expensive stuff around the model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): -* **SHAP contributions + predictions** — via the built-in ``compile(cache_dir=...)``. -* **The text embeddings and the 2-D projection** — via ``compute_projection(cache_dir=...)``. The - library owns the embedding + caching (so the scatter and the similar-example neighbours are always - in the same space); this script only supplies the reducer, PaCMAP. +* **Contributions + predictions** — via ``explain(..., cache_dir=...)``. +* **The text embeddings and the 2-D projection** — via ``compute_projection(explanation, cache_dir=...)``. + The library owns the embedding + caching (so the scatter and the similar-example neighbours are + always in the same space); this script only supplies the reducer, PaCMAP. The caching is automatic: each run **loads** the cache if one exists for these exact texts, otherwise it **computes and writes** it. So the first run is slow; every run after pays only the model load (a few seconds on CPU). Pass ``--recompute`` to force a fresh compute (e.g. after changing the explainer). Iterating on the webapp itself? Don't -restart at all — keep this in a REPL and re-call ``run_app`` after reloading the webapp -modules; the model stays resident. +restart at all — keep this in a REPL and re-call ``run_app(explanation, ...)`` after reloading the +webapp modules; the model and the artifact both stay resident. + +**The cache file is a snapshot.** ``explain``'s cache entries are ``NlpExplanation.save`` output — a +zip of plain-text ``meta.json`` plus parquet, no pickle — so any ``<hash>.xpl`` under ``--cache-dir`` +can be reopened with **no model and no backend**:: + + from shapash.explainer.nlp_explanation import NlpExplanation + from shapash.webapp.nlp_app import NlpWebApp + + explanation, scatter = NlpExplanation.load("…/nlp_shap/<hash>.xpl") + NlpWebApp(explanation, engine=None, scatter_xy=scatter).run(port=8050) + +One caveat, and it is deliberate rather than an oversight: **the cached artifact carries no ground +truth.** The cache key covers ``(texts, model, backend)``, and ``y`` is not a function of that key — +it belongs to whoever called ``explain``. Serving a cache file directly therefore self-disables the +confusion matrix and the label-noise panel. For a shareable snapshot *with* labels, save the object +``explain`` returned: ``explanation.save(path, scatter_xy=projected)``. The model runs on CPU when no GPU is present, so this serves fine on a plain box. @@ -57,7 +86,7 @@ The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: ``<model>/<dataset>__<split>/`` holds the (backend-independent) embedding-store artifacts — the vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or -``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.pkl`` contributions. +``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.xpl`` contributions. Serving a *private* model and dataset (both local, nothing on the hub) ---------------------------------------------------------------------- @@ -355,7 +384,7 @@ def _dataset_slug(config: ServeConfig) -> str: def _dataset_cache_dir(config: ServeConfig) -> Path: """Return ``cache_dir/<model>/<dataset>__<split>`` — the level the projection is cached at. - The compile and projection caches both key on the model (and, for contributions, the backend), so + The explain and projection caches both key on the model (and, for contributions, the backend), so these path levels are organisational rather than load-bearing: they keep a ``--cache-dir`` readable and prunable per model/dataset. The PaCMAP projection embeds the texts with ``model.embed`` and never touches the attribution backend, so every ``--attribution`` choice shares one projection cache @@ -364,11 +393,11 @@ def _dataset_cache_dir(config: ServeConfig) -> Path: return config.cache_dir / _model_slug(config) / _dataset_slug(config) -def _compile_cache_dir(config: ServeConfig) -> Path: +def _explain_cache_dir(config: ServeConfig) -> Path: """Return ``<dataset-dir>/<backend>`` — the level contributions are cached at. Contributions *do* depend on the attribution backend, so SHAP and LIG get separate subdirectories. - Switching ``--attribution`` then reads/writes a different ``<hash>.pkl`` instead of silently + Switching ``--attribution`` then reads/writes a different ``<hash>.xpl`` instead of silently reusing the other method's cached highlights — no ``--recompute`` needed to swap methods. """ return _dataset_cache_dir(config) / _ATTRIBUTION_BACKENDS[config.attribution] @@ -897,7 +926,7 @@ def build_reducer() -> pacmap.PaCMAP: def main() -> None: - """Load config + data + model, (re)use the SHAP + projection cache, and serve the What-if webapp.""" + """Load config + data + model, fit, explain (reusing the cache), and serve the artifact.""" config = parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") @@ -908,7 +937,7 @@ def main() -> None: sentences, y_true = load_data(config) dataset_dir = _dataset_cache_dir(config) # projection lives here (shared across attribution backends) - compile_dir = _compile_cache_dir(config) # contributions live here (one subdir per attribution backend) + explain_dir = _explain_cache_dir(config) # contributions live here (one subdir per attribution backend) model = load_model(config) if config.embedding_space is not None: @@ -923,42 +952,58 @@ def main() -> None: reference_corpus = load_reference_corpus(config) similar_cache_dir = dataset_dir / "similar" - # No cf_generator is passed: NlpExplainer auto-discovers every built-in compatible with the model - # (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's method dropdown. - # reference_corpus enables the "Similar Examples" panel (skipped when --n-reference 0). + # Step 1 — bind the model. No data and no results: the constructor takes what is true for every + # batch. No cf_generator is passed, so NlpExplainer auto-discovers every built-in compatible with + # the model (HotFlip + AblationFlip on an HF classifier) and offers them from the What-if Lab's + # method dropdown. xpl = NlpExplainer( model, label_names=model.label_names, backend=build_backend(config, model), - reference_corpus=reference_corpus, - reference_cache_dir=similar_cache_dir, ) + + # Step 2 — fit() learns reference state, which for text is the corpus. Two panels come from it, + # with different requirements: "Similar Examples" needs an embedding-capable model, while the + # label-noise probe needs only the *labels* and so works even behind a prediction-only pipeline. + # Passing y is what enables the second. Both are skipped when --n-reference 0 leaves this None. + # This is where the reference corpus gets embedded (one forward pass per example, cached under + # similar_cache_dir) — deliberately here rather than inside the first callback that opens the + # panel. Pass precompute=False to trade a fast start for a slow first click. + reference_texts, reference_labels = reference_corpus if reference_corpus is not None else (None, None) + xpl.fit(reference_texts, y=reference_labels, cache_dir=similar_cache_dir) + if config.recompute: # Drop this text set's cached contributions so the steps below recompute + overwrite. The # explainer owns the key (it covers the model and backend), so it also owns the deletion — # only *this* model+backend's entry goes, other backends' caches are left intact. The # embeddings + projection are dropped by compute_projection(recompute=True) below. logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) - xpl.clear_cache(sentences, compile_dir) + xpl.clear_cache(sentences, explain_dir) # Both steps load from cache if one exists for these exact texts, otherwise they compute and write # it. So the first run is slow; later runs only pay the model load. - compile_cache = xpl.cache_path(sentences, compile_dir) - if compile_cache.exists(): - logger.info("Contributions cache hit (%s) — loading %s", config.attribution, compile_cache) + explain_cache = xpl.cache_path(sentences, explain_dir) + if explain_cache.exists(): + logger.info("Contributions cache hit (%s) — loading %s", config.attribution, explain_cache) else: logger.info( "Contributions cache miss (%s) — computing for %d texts (this is the slow part)", config.attribution, len(sentences), ) - xpl.compile(sentences, y_true=y_true, cache_dir=compile_dir) - projected = xpl.compute_projection(reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute) + # Step 3 — the expensive call, and the only one that produces results. The returned artifact is + # immutable and holds no model, no backend and no explainer handle; ground truth rides on it + # (never on xpl), which is why a cache hit still gets *this* run's y_true attached. + explanation = xpl.explain(sentences, y=y_true, cache_dir=explain_dir) + + # Step 4 — everything downstream reads the artifact as an argument. + projected = xpl.compute_projection( + explanation, reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute + ) - if xpl.can_find_similar(): - # Build (and cache) the reference activation bank now so the first in-app lookup is instant. - logger.info("Warming similar-examples bank over %d reference texts…", len(reference_corpus[0])) - xpl.find_similar(sentences[0], top_k=1) + # (No bank warm-up here: fit(precompute=True) above already embedded the reference corpus, under + # the same condition this used to test — can_find_similar() is "a retriever was built", which is + # exactly what fit's precompute branch keys on.) logger.info( "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s" @@ -968,11 +1013,13 @@ def main() -> None: xpl.can_edit(), xpl.can_counterfactual(), xpl.can_find_similar(), - xpl.can_detect_label_noise(), + # Takes the artifact: label-noise detection needs ground truth + per-class probabilities, and + # both live on the explanation rather than on the explainer. + xpl.can_detect_label_noise(explanation), xpl.can_probe_labels(), ) logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) - xpl.run_app(port=config.port, debug=False, host=config.host, scatter_xy=projected) + xpl.run_app(explanation, port=config.port, debug=False, host=config.host, scatter_xy=projected) if __name__ == "__main__": diff --git a/shapash/backend/__init__.py b/shapash/backend/__init__.py index 64f54224..b93a5f3d 100644 --- a/shapash/backend/__init__.py +++ b/shapash/backend/__init__.py @@ -4,7 +4,7 @@ from .backend import Backend from .base_backend import BaseBackend from .lime_backend import LimeBackend -from .nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation +from .nlp_backend import NlpBackend, NlpContributions from .nlp_captum_lig_backend import NlpCaptumLigBackend from .nlp_lime_backend import NlpLimeBackend from .nlp_shap_backend import NlpShapBackend diff --git a/shapash/backend/backend.py b/shapash/backend/backend.py index 849870b6..c9245931 100644 --- a/shapash/backend/backend.py +++ b/shapash/backend/backend.py @@ -7,12 +7,11 @@ Return-type contract (migration target) ---------------------------------------- -Text backends already return typed structures (``NlpRawExplanation`` from -``run_explainer``, ``NlpContributions`` from ``get_local_contributions``). -Tabular backends still return ``dict`` / ``pd.DataFrame`` pending migration to -the same typed contract. ``Backend`` therefore types both methods loosely -(``Any``); tighten these annotations once the tabular path has been migrated to -the typed structures. +Text backends already return a typed structure (``NlpContributions``, from both +``run_explainer`` and ``get_local_contributions``). Tabular backends still return +``dict`` / ``pd.DataFrame`` pending migration to the same typed contract. +``Backend`` therefore types both methods loosely (``Any``); tighten these +annotations once the tabular path has been migrated to the typed structures. """ from __future__ import annotations diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index ddf238cb..ddf2915d 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -1,13 +1,13 @@ """NLP backend infrastructure — shared base for text explainability backends. -Two dataclasses form the typed pipeline between raw explainer output and the -final explanation object: - -* ``NlpRawExplanation`` — returned by every concrete ``run_explainer``. - Carries ``contributions``, ``base_values``, and ``data`` as typed fields - instead of an untyped dict. -* ``NlpContributions`` — returned by ``get_local_contributions`` to callers. - Adds word-importance aggregation and optional metadata (label names, index). +``NlpContributions`` is the single typed payload every concrete ``run_explainer`` +returns: per-sample token strings, contribution values, and baseline predictions — +no batch metadata (predictions, ground truth, class names) attached. It carries no +behavior either; word-importance aggregation and persistence live on +:class:`~shapash.explainer.nlp_explanation.NlpExplanation`, the batch artifact that +wraps this data together with everything else ``NlpExplainer.explain()`` knows. +The same slim type is also what :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain_text` +returns for a single live (uncommitted) text — a one-sample batch. ``NlpBackend`` is the abstract base class that owns the common ``__init__`` skeleton and the ``get_local_contributions`` implementation. Concrete @@ -18,17 +18,19 @@ from __future__ import annotations import re -from dataclasses import dataclass, field +from dataclasses import dataclass +from typing import ClassVar, Literal import numpy as np -import pandas as pd from shapash.backend.backend import Backend +from shapash.model.base import has_capabilities # A unit made entirely of punctuation/symbol characters — no letters, digits or underscore. Word # segmentation deliberately emits these as their own units (the model reads them, and "!!!" or "?" # genuinely moves a sentiment prediction), so they stay visible in a local highlight. They are noise -# in a *corpus-level* ranking, though, which is why ``word_importance`` filters them by default. +# in a *corpus-level* ranking, though, which is why ``NlpExplanation.word_importance`` filters them +# by default. _PUNCTUATION_RE = re.compile(r"^[^\w\s]+$") @@ -38,203 +40,60 @@ def is_punctuation(word: str) -> bool: return bool(stripped) and bool(_PUNCTUATION_RE.match(stripped)) -@dataclass -class NlpRawExplanation: - """Typed intermediate output of ``NlpBackend.run_explainer``. - - Every concrete backend returns this instead of a plain dict so that the - three-field contract is enforced structurally rather than by convention. - ``get_local_contributions`` consumes it and produces ``NlpContributions``. - - Attributes - ---------- - contributions : list[np.ndarray] - Per-token contribution values, one array per sample. - Shape per sample: ``(n_tokens_i, n_classes)`` or ``(n_tokens_i,)``. - base_values : np.ndarray - Prediction baseline for each sample — SHAP base values or LIME - intercepts depending on the backend. Shape ``(n_samples, n_classes)`` - or ``(n_samples,)``. - data : list[list[str]] - Token or word strings per sample (variable length). - """ - - contributions: list[np.ndarray] - base_values: np.ndarray - data: list[list[str]] - - @dataclass class NlpContributions: - """Token-level contributions for a batch of text samples. + """Raw per-sample token contributions — a backend's output, no batch metadata attached. Attributes ---------- token_strings : list[list[str]] - Tokenized representation of each sample (variable length per sample). + Word (or LIME-vocabulary) strings per sample, variable length. values : list[np.ndarray] Per-token contribution values, one array per sample. Each array has shape ``(n_tokens_i, n_classes)`` for multi-class models or ``(n_tokens_i,)`` for binary/regression. - base_values : np.ndarray + base_values : np.ndarray or None Baseline prediction for each sample, shape ``(n_samples, n_classes)`` - or ``(n_samples,)``. - label_names : list[str] or None - Human-readable class names; set by the caller after construction. - index : pd.Index or None - Row index from the source ``pd.Series``; set by the caller after construction. - folds_case : bool or None - Whether the model's tokenizer normalises case away, from - :meth:`~shapash.model.base.SupportsTokenization.folds_case`; set by the caller after - construction. Decides the default unit grouping in :meth:`word_importance` — see - :meth:`resolve_lowercase`. ``None`` when the model exposes no tokenizer to ask. + or ``(n_samples,)``. ``None`` when the backend has no reference at all + (see :attr:`NlpBackend.reference_kind`). """ token_strings: list[list[str]] values: list[np.ndarray] - base_values: np.ndarray - label_names: list[str] | None = field(default=None) - index: pd.Index | None = field(default=None) - folds_case: bool | None = field(default=None) - - _SPECIAL_RE: re.Pattern = re.compile(r"^\[.*\]$|^##|^\s*$") - - def __len__(self) -> int: - return len(self.token_strings) - - def resolve_lowercase(self, lowercase: bool | None = None) -> bool: - """Decide whether word units should be case-folded, deferring to the model by default. - - One source of truth for the question, so a caller that has to normalise units the same - way (the webapp's exclusion vocabulary) cannot drift from :meth:`word_importance`. - - Parameters - ---------- - lowercase : bool, optional - Explicit caller override. ``None`` (default) derives the answer from ``folds_case``. - - Returns - ------- - bool - ``True`` when ``AWFUL`` and ``awful`` should aggregate into one unit. - - Notes - ----- - With no override, an *uncased* model folds and a *cased* model does not: on an uncased - tokenizer both spellings are the same input ids, so keeping them apart splits one word - into variants the model provably cannot distinguish; on a cased one they are different - inputs whose attributions differ for real reasons, and merging would hide that. - - When ``folds_case`` is ``None`` — no tokenizer to probe, e.g. a bare LIME - ``classifier_fn`` — this falls back to folding, which keeps a corpus-level ranking from - fragmenting on a question that cannot be answered here. - """ - if lowercase is not None: - return lowercase - return True if self.folds_case is None else self.folds_case - - def word_importance( - self, - label_idx: int, - n_top: int = 20, - filter_special: bool = True, - filter_punctuation: bool = True, - lowercase: bool | None = None, - filter_sign: str = "all", - exclude_words: set[str] | None = None, - sample_indices: list[int] | None = None, - ) -> pd.Series: - """Aggregate token contributions by word across all samples for one class. - - For each unique (stripped, by default lowercased) token string that appears in - the batch, computes the mean contribution across all its occurrences. - - Parameters - ---------- - label_idx : int - Index of the class to compute importance for. - n_top : int - Maximum number of words to return, sorted by ``|mean contribution|``. - filter_special : bool - If ``True``, skip empty strings, ``[CLS]``/``[SEP]``-style bracket - tokens, and ``##subword`` wordpiece prefixes. - filter_punctuation : bool - If ``True`` (default), skip units made entirely of punctuation (``.``, ``,``, ``!!!``). - Word segmentation emits punctuation as its own unit so it stays visible and additive in - a *local* explanation; in a corpus-level ranking it is almost always noise, so it is - hidden here unless explicitly asked for. - lowercase : bool, optional - Case-fold each unit before aggregating, so ``AWFUL``, ``Awful`` and ``awful`` - form one row. ``None`` (default) derives this from the model's own tokenizer - via :meth:`resolve_lowercase` — folding on an uncased model, keeping case on a - cased one — so pass a bool only to override that. - Folding also makes the ranking comparable across backends: SHAP labels units by - slicing the *source text* (keeping its casing) while Captum/LIG labels them from - tokenizer output (already normalised), so on an uncased model the two would - otherwise report different vocabularies for one corpus. - The per-instance sentence highlight always keeps the original casing, whatever - this is set to. - filter_sign : {"all", "positive", "negative"} - If ``"positive"``, keep only words with positive mean contribution. - If ``"negative"``, keep only words with negative mean contribution. - exclude_words : set[str], optional - Additional set of word strings to exclude (e.g. user-selected - stopwords or corpus words to hide). Matched against the same - normalised key, so under ``lowercase`` the exclusion is case-insensitive. - sample_indices : list[int], optional - Positional indices of the samples to include in the aggregation. - When ``None`` (default) all samples are used. - - Returns - ------- - pd.Series - Word → mean contribution, sorted by absolute value descending. - """ - - fold = self.resolve_lowercase(lowercase) - - def _key(token: str) -> str: - return token.lower() if fold else token - - _exclude: set[str] = {_key(w) for w in exclude_words} if exclude_words else set() - word_contribs: dict[str, list[float]] = {} - iter_data = ( - ((self.token_strings[i], self.values[i]) for i in sample_indices) - if sample_indices is not None - else zip(self.token_strings, self.values, strict=True) - ) - for tokens, vals in iter_data: - vals_1d: np.ndarray = vals[:, label_idx] if vals.ndim == 2 else vals - for tok, val in zip(tokens, vals_1d, strict=True): - tok_clean = tok.strip() - if filter_special and self._SPECIAL_RE.match(tok_clean): - continue - if filter_punctuation and is_punctuation(tok_clean): - continue - key = _key(tok_clean) - if key in _exclude: - continue - if key not in word_contribs: - word_contribs[key] = [] - word_contribs[key].append(float(val)) - - importance = pd.Series({w: float(np.mean(vs)) for w, vs in word_contribs.items()}) - importance = importance.reindex(importance.abs().sort_values(ascending=False).index) - - if filter_sign == "positive": - importance = importance[importance > 0] - elif filter_sign == "negative": - importance = importance[importance < 0] - - return importance.head(n_top) + base_values: np.ndarray | None class NlpBackend(Backend): """Abstract base class for NLP explainability backends. - Owns the ``__init__`` skeleton shared by all text backends and the - ``get_local_contributions`` implementation. Concrete subclasses must - implement ``run_explainer`` and return an ``NlpRawExplanation``. + Owns the ``__init__`` skeleton shared by all text backends (including the + ``requires_model_capabilities`` check) and the ``get_local_contributions`` + implementation. Concrete subclasses must implement ``run_explainer`` and + return an ``NlpContributions``, and must declare the three class + attributes below. + + Class Attributes + ----------------- + reference_kind : {"distribution", "statistics", "point", "none"} + What kind of reference, if any, this method's numbers are measured against — + not every method has a baseline, so this is a four-valued question, not a + boolean. ``"distribution"``: rows the model is compared against (a genuine + baseline, e.g. SHAP kernel/permutation on tabular data). ``"statistics"``: + summaries used to perturb/discretize, never subtracted (e.g. LIME tabular's + discretizer). ``"point"``: one constructed input built by the model/tokenizer, + not learned from data (e.g. Captum LIG's pad/mask reference ids). ``"none"``: + no reference exists, or none is learned from data (e.g. a masker that infers + itself, or a pointwise gradient method). + is_additive : bool + Whether this method's contributions satisfy an efficiency/completeness axiom — + i.e. sum to a well-defined total (the prediction gap for Shapley-family methods, + ``logits(x) - logits(baseline)`` for Integrated Gradients). Licenses feature + grouping and waterfall/force-style charts; ``False`` for LIME, whose local + surrogate coefficients carry no such guarantee. + requires_model_capabilities : tuple[type, ...] + Capability ABCs (from :mod:`shapash.model.base`) the bound model must satisfy, + checked once here so a new backend declares its needs instead of failing at + first call. Empty when the backend only needs a plain scoring callable. Parameters ---------- @@ -249,8 +108,17 @@ class NlpBackend(Backend): Keyword arguments forwarded to the underlying explainer constructor. explainer_compute_args : dict, optional Keyword arguments forwarded to the explainer call / ``explain_instance``. + + Raises + ------ + TypeError + If ``model`` does not satisfy every capability in ``requires_model_capabilities``. """ + reference_kind: ClassVar[Literal["distribution", "statistics", "point", "none"]] + is_additive: ClassVar[bool] + requires_model_capabilities: ClassVar[tuple[type, ...]] = () + def __init__( self, model, @@ -259,24 +127,33 @@ def __init__( explainer_args: dict | None = None, explainer_compute_args: dict | None = None, ) -> None: + required = type(self).requires_model_capabilities + if required and not has_capabilities(model, *required): + names = ", ".join(c.__name__ for c in required) + raise TypeError( + f"{type(self).__name__} requires a model implementing {names}; " + "got a model without the required attribution surface." + ) self.model = model self._classes = label_names or [] self.explainer_args = explainer_args or {} self.explainer_compute_args = explainer_compute_args or {} def get_local_contributions( - self, x, explain_data: NlpRawExplanation, subset: list[int] | None = None + self, x, explain_data: NlpContributions, subset: list[int] | None = None ) -> NlpContributions: - """Convert a ``NlpRawExplanation`` to ``NlpContributions``. + """Optionally select a subset of ``run_explainer``'s output. + TODO: dead-code, not used by any current NLP backend. Keep for now to avoid breaking the interface. Parameters ---------- x : list[str] or pd.Series Text samples (not used here; kept for interface compatibility). - explain_data : NlpRawExplanation - Typed output of ``run_explainer``. + explain_data : NlpContributions + The output of ``run_explainer``. subset : list[int], optional - Positional indices to select a subset of samples. + Positional indices to select a subset of samples. Returns ``explain_data`` unchanged + when ``None`` (the default). Returns ------- @@ -284,13 +161,11 @@ def get_local_contributions( Token strings, contribution values, and baseline predictions for each sample. """ - token_strings: list[list[str]] = [list(s) for s in explain_data.data] - values: list[np.ndarray] = list(explain_data.contributions) - base_values: np.ndarray = np.asarray(explain_data.base_values) - - if subset is not None: - token_strings = [token_strings[i] for i in subset] - values = [values[i] for i in subset] - base_values = base_values[subset] - - return NlpContributions(token_strings=token_strings, values=values, base_values=base_values) + if subset is None: + return explain_data + base_values = explain_data.base_values + return NlpContributions( + token_strings=[explain_data.token_strings[i] for i in subset], + values=[explain_data.values[i] for i in subset], + base_values=None if base_values is None else base_values[subset], + ) diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index cec73192..d5ea8a8d 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -29,8 +29,8 @@ import numpy as np from shapash._optional import import_optional_module -from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation -from shapash.model.base import SupportsCaptumIG, has_capabilities +from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.model.base import SupportsCaptumIG _NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' @@ -205,6 +205,15 @@ class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` default """ name = "nlp_captum_lig" + # A constructed point from the tokenizer (model.reference_ids() -> pad/mask ids), + # not learned from data — see A9 in docs/architecture/refactoring-plan.md. + reference_kind = "point" + # Integrated Gradients satisfies the completeness axiom (Sundararajan, Taly & Yan, + # 2017, "Axiomatic Attribution for Deep Networks"): attributions sum exactly to + # logits(x) - logits(baseline). + is_additive = True + # Checked once by NlpBackend.__init__ instead of here — see its docstring. + requires_model_capabilities = (SupportsCaptumIG,) def __init__( self, @@ -215,17 +224,12 @@ def __init__( explainer_compute_args: dict | None = None, show_progress: bool = False, ) -> None: - if not has_capabilities(model, SupportsCaptumIG): - raise TypeError( - "NlpCaptumLigBackend requires a model implementing SupportsCaptumIG " - "(e.g. HFClassifierModel); got a model without the Captum attribution surface." - ) super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.show_progress = show_progress captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) self.explainer = captum_attr.LayerIntegratedGradients(model.logits, model.embedding_layer) - def run_explainer(self, x) -> NlpRawExplanation: + def run_explainer(self, x) -> NlpContributions: """Attribute each text with LIG, one pass per class, and return per-token contributions. Parameters @@ -235,7 +239,7 @@ def run_explainer(self, x) -> NlpRawExplanation: Returns ------- - NlpRawExplanation + NlpContributions Ragged list of ``(n_tokens_i, n_classes)`` value arrays, per-sample baseline logits, and token strings per sample. """ @@ -281,10 +285,10 @@ def run_explainer(self, x) -> NlpRawExplanation: base_values.append(base_logits) data.append(word_tokens) - return NlpRawExplanation( - contributions=contributions, + return NlpContributions( + token_strings=data, + values=contributions, base_values=np.stack(base_values, axis=0), - data=data, ) def _progress_iter(self, items: list[str]) -> Iterable[str]: diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index 5cd66d24..ab013246 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -1,8 +1,8 @@ """NLP LIME backend — word-level LIME contributions for text classification models. -``NlpLimeBackend`` wraps ``LimeTextExplainer`` and implements ``run_explainer``. -All shared infrastructure (``NlpContributions`` dataclass, -``get_local_contributions``, common ``__init__`` skeleton) lives in +``NlpLimeBackend`` wraps ``LimeTextExplainer`` and implements ``run_explainer``, +returning an ``NlpContributions``. All shared infrastructure +(``get_local_contributions``, common ``__init__`` skeleton) lives in ``NlpBackend`` (see ``nlp_backend.py``). LIME works at word level (bag-of-words by default) rather than at the subword @@ -23,7 +23,7 @@ except ImportError: _lime_available = False -from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation +from shapash.backend.nlp_backend import NlpBackend, NlpContributions class NlpLimeBackend(NlpBackend): @@ -81,6 +81,19 @@ class NlpLimeBackend(NlpBackend): """ name = "nlp_lime" + # Unlike tabular LIME (``LimeTabularExplainer(training_data=...)``, which needs a + # background corpus to build its discretizer/feature statistics), LimeTextExplainer + # takes no training-data-like constructor argument at all (see __init__ below) — + # explain_instance perturbs the instance text itself (word removal / mask_string + # substitution). There is nothing backend-specific for ``fit`` to learn here. + reference_kind = "none" + # LIME fits a locally-weighted linear surrogate (Ribeiro, Singh & Guestrin, 2016, + # "Why Should I Trust You?") whose objective is local fidelity, not exact + # reconstruction — there is no efficiency/completeness axiom forcing the weights to + # sum to f(x) - f(baseline). Matches tabular LimeBackend.support_groups=False, for + # the same algorithmic reason (not because tabular already decided it). + is_additive = False + requires_model_capabilities = () # a plain scoring callable is enough def __init__( self, @@ -136,12 +149,12 @@ def _classifier_fn(self, texts: list[str]) -> np.ndarray: return matrix return np.array(result, dtype=np.float64) - def run_explainer(self, x) -> NlpRawExplanation: + def run_explainer(self, x) -> NlpContributions: """Run LimeTextExplainer on each sample and normalise output. Converts LIME's sparse per-label ``{label_id: [(word_id, weight)]}`` representation into a dense ``(n_words, n_classes)`` array so that the - returned ``NlpRawExplanation`` matches the same field shapes as + returned ``NlpContributions`` matches the same field shapes as ``NlpShapBackend``. Parameters @@ -151,7 +164,7 @@ def run_explainer(self, x) -> NlpRawExplanation: Returns ------- - NlpRawExplanation + NlpContributions Dense weight arrays, LIME intercepts as base values, and unique vocabulary words per sample. """ @@ -185,8 +198,8 @@ def run_explainer(self, x) -> NlpRawExplanation: base_values_list.append([exp.intercept.get(i, 0.0) for i in range(effective_n_classes)]) data.append(vocab) - return NlpRawExplanation( - contributions=contributions, + return NlpContributions( + token_strings=data, + values=contributions, base_values=np.array(base_values_list), - data=data, ) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 6354bdae..eaef6762 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -1,8 +1,8 @@ """NLP SHAP backend — token-level SHAP contributions for text classification models. ``NlpShapBackend`` wraps ``shap.Explainer`` for text inputs and implements -``run_explainer``. All shared infrastructure (``NlpContributions`` dataclass, -``get_local_contributions``, common ``__init__`` skeleton) lives in +``run_explainer``, returning an ``NlpContributions``. All shared infrastructure +(``get_local_contributions``, common ``__init__`` skeleton) lives in ``NlpBackend`` (see ``nlp_backend.py``). ``shap.maskers.Text.token_segments`` emits *segments* of the source string rather than the @@ -28,7 +28,7 @@ import numpy as np import shap -from shapash.backend.nlp_backend import NlpBackend, NlpRawExplanation +from shapash.backend.nlp_backend import NlpBackend, NlpContributions # SHAP's masker reports special tokens as blank segments on its offset-mapping path; their # attribution is folded into the baseline during word aggregation. @@ -176,20 +176,41 @@ class NlpShapBackend(NlpBackend): explainer class (the ``"explainer"`` key selects the class; all other keys are forwarded as its constructor arguments). explainer_compute_args : dict, optional - Keyword arguments forwarded to the explainer call (``__call__``). + Keyword arguments forwarded to the explainer call (``__call__``). Two matter for text: + + ``max_evals`` (SHAP default 500) is the accuracy knob, and that default is far from + converged: any imdb review over ~40 words exhausts it and lands 8-45% (max|Δ| / max|value|) + from the fully-traversed explanation, which costs 3k-23k evals. + + ``batch_size`` (default: the ``Text`` masker's 5) caps how many masked variants reach + ``model`` per call, and is *not* free. The Owen loop drains a whole batch from its + best-first queue before pushing any children, so a larger batch expands different nodes + under the same ``max_evals`` and deterministically shifts the values, within the error band + above. Worth ~1.6x on short texts; 0.98x and 2.2x peak memory on imdb-length ones, where a + 512-token forward pass already saturates the GPU. batch_size : int or None, optional Batch size applied to ``model`` when it is a ``transformers`` pipeline that does not - already have one. Default 64. Pass ``None`` to leave the pipeline untouched. + already have one. Default 64. Pass ``None`` to leave the pipeline untouched. Distinct from + the explainer's ``batch_size`` above: this one only regroups the strings SHAP has already + chosen, so it never changes the explanation. A pipeline built without ``batch_size`` scores a list of strings *one at a time*, so an explanation degenerates into ``max_evals`` (default 500) sequential single-sample forward passes. Batching lets them pad into one pass — ~1.9x on distilbert-imdb/GPU — for a numerically identical explanation (max|Δ| 1.1e-07), since the explainer's tree traversal - is untouched. Raise it alongside the explainer's own ``batch_size``, which caps how many - masked variants arrive per call. + is untouched. """ name = "nlp_shap" + # No reference is learned from data: the ``Text`` masker infers its own masking + # scheme from the pipeline/tokenizer (``masker=None`` below), so ``fit`` has + # nothing backend-specific to learn here. + reference_kind = "none" + # Shapley values satisfy the efficiency axiom by construction, and stay additive + # even under the Partition/Owen path SHAP silently takes for text (see A8 in + # docs/architecture/refactoring-plan.md: Owen values satisfy efficiency too). + is_additive = True + requires_model_capabilities = () # a plain scoring callable is enough def __init__( self, @@ -207,6 +228,12 @@ def __init__( # ``_batch_size`` is what ``transformers.Pipeline.__call__`` reads (``None`` means 1) and # there is no public setter, so a release that renames it makes this a silent no-op rather # than an error. A pipeline the caller already configured is never overridden. + # + # transformers logs "using the pipelines sequentially on GPU ... please use a dataset" during + # every GPU explanation. It is a false positive: the check is only ``call_count > 10 and + # device == cuda`` and never inspects the input. We already pass lists, which transformers + # wraps in a ``PipelineDataset`` and feeds to the same ``DataLoader`` a real Dataset would + # get (measured: 1.009x, i.e. noise), and SHAP would flatten any dataset back to a list anyway. if batch_size is not None and getattr(model, "_batch_size", "absent") in (None, 1): model._batch_size = batch_size @@ -225,7 +252,7 @@ def __init__( # when no tokenizer is reachable (bare callable / ``SimpleTokenizer``) — see ``_is_special``. self._special_tokens = _masker_special_tokens(self.explainer) - def run_explainer(self, x) -> NlpRawExplanation: + def run_explainer(self, x) -> NlpContributions: """Run the SHAP text explainer and return all explanation components. Subword tokens are merged into whole words via ``_aggregate_subwords`` before being @@ -239,7 +266,7 @@ def run_explainer(self, x) -> NlpRawExplanation: Returns ------- - NlpRawExplanation + NlpContributions Ragged list of value arrays, baseline predictions, and token strings per sample. """ @@ -261,8 +288,8 @@ def run_explainer(self, x) -> NlpRawExplanation: contributions.append(word_contribs) base_values.append(word_base) - return NlpRawExplanation( - contributions=contributions, + return NlpContributions( + token_strings=data, + values=contributions, base_values=np.stack(base_values, axis=0), - data=data, ) diff --git a/shapash/compute/embedding_store.py b/shapash/compute/embedding_store.py index 8a9d705b..2c4207ed 100644 --- a/shapash/compute/embedding_store.py +++ b/shapash/compute/embedding_store.py @@ -28,20 +28,20 @@ def hash_corpus(texts: Sequence[str], key: str) -> str: """Stable digest over an identity ``key`` plus the corpus texts (order-sensitive). - The ``\\0`` separator after every text is what makes this collision-safe: without it - ``["ab", "c"]`` and ``["a", "bc"]`` hash identically, so two different corpora would share a cache - entry. ``key`` carries whatever else changes the cached artifact (model identity, representation - space, explanation backend) and is separated the same way. + Each string is framed with its own byte length before hashing, so no separator choice can + let two different inputs collide — a literal ``"\\0"`` inside a text can no longer make + ``["a\\0b"]`` and ``["a", "b"]`` hash identically. ``key`` carries whatever else changes the + cached artifact (model identity, representation space, explanation backend). Shared by every disk cache in the NLP path — the embedding/projection artifacts here and the - contribution pickle in :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compile` — so all of + contribution cache in :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain` — so all of them key on the same, collision-safe rule. """ h = hashlib.md5(usedforsecurity=False) - h.update(f"{key}\0".encode()) - for text in texts: - h.update(text.encode()) - h.update(b"\0") + for s in (key, *texts): + data = s.encode() + h.update(len(data).to_bytes(8, "big")) + h.update(data) return h.hexdigest() @@ -81,7 +81,10 @@ def __init__( self.model = model self.texts = list(texts) self.cache_dir = Path(cache_dir) if cache_dir is not None else None - self._arrays: dict[str, np.ndarray] = {} + # Keyed by (key, tag), not tag alone: ``key`` folds in the model's *current* space, which is + # assignable at runtime (EncoderClassifierModel.embedding_space). A tag-only memo would answer + # from the old space after such a switch and never consult the correctly-keyed file on disk. + self._arrays: dict[tuple[str, str], np.ndarray] = {} @property def space_key(self) -> str: @@ -118,19 +121,22 @@ def cached_array(self, tag: str, compute: Callable[[], np.ndarray]) -> np.ndarra Returns ------- np.ndarray - The cached array. Memoized in memory, so repeat calls are free. + The cached array. Memoized in memory per ``(key, tag)``, so repeat calls are free while + the model's space is unchanged, and a space switch re-reads (or recomputes) rather than + returning the previous space's array. """ - if tag in self._arrays: - return self._arrays[tag] + memo = (self.key, tag) + if memo in self._arrays: + return self._arrays[memo] cache_file = self.path(tag) if cache_file is not None and cache_file.exists(): logger.info("Embedding store hit (%s) — loading %s", tag, cache_file) - self._arrays[tag] = np.load(cache_file) - return self._arrays[tag] + self._arrays[memo] = np.load(cache_file) + return self._arrays[memo] logger.info("Embedding store miss (%s) — computing over %d texts (%s)", tag, len(self.texts), self.space_key) array = compute() - self._arrays[tag] = array + self._arrays[memo] = array if cache_file is not None: cache_file.parent.mkdir(parents=True, exist_ok=True) np.save(cache_file, array) @@ -138,10 +144,14 @@ def cached_array(self, tag: str, compute: Callable[[], np.ndarray]) -> np.ndarra return array def clear(self) -> None: - """Drop every cached artifact for this corpus — in memory and, when caching, on disk. + """Drop this corpus's cached artifacts — every in-memory one, and on disk those under this key. Removes *all* tags under this key, not just the embeddings, so a caller forcing a recompute - cannot leave a projection behind that was derived from vectors no longer on disk. + cannot leave a projection behind that was derived from vectors no longer on disk. The disk + sweep is scoped to the key, and therefore to the model's **current** space: files written for + a space the model has since moved off are not touched. They are already unreachable by + lookup, and re-selecting that space finds them again — which is the useful behaviour, not an + oversight. In memory the sweep is unconditional (every space). """ self._arrays.clear() if self.cache_dir is None: diff --git a/shapash/compute/retrieval/similar_examples.py b/shapash/compute/retrieval/similar_examples.py index b37667a1..217f8abd 100644 --- a/shapash/compute/retrieval/similar_examples.py +++ b/shapash/compute/retrieval/similar_examples.py @@ -114,6 +114,7 @@ def __init__( self.reference_labels = list(reference_labels) if reference_labels is not None else None self.store = EmbeddingStore(model, self.reference_texts, cache_dir=cache_dir) self._bank: np.ndarray | None = None # (n_reference, hidden), L2-normalized rows + self._bank_space: str | None = None # store.space_key the bank was built under — see build() self._positions: dict[str, list[int]] | None = None # text -> reference rows, for exclude_self @property @@ -129,11 +130,22 @@ def build(self) -> None: :class:`~shapash.compute.embedding_store.EmbeddingStore`; normalization is applied here rather than cached, since it is a single cheap pass and keeps the stored artifact the plain embeddings that other consumers (e.g. the scatter projection) can reuse. Rows end up unit-norm so a query - is a single matrix-vector product. Idempotent. + is a single matrix-vector product. + + Idempotent *while the model stays in one space*. The space is assignable at runtime + (:attr:`~shapash.model.encoder.EncoderClassifierModel.embedding_space`), and a bank held over + such a switch would be compared against queries encoded in the new space — a cosine between + two unrelated spaces, which returns a plausible-looking number rather than failing. So the + bank records the space it was built under and is rebuilt when that moves. The comparison is + on ``space_key`` (two string reads) rather than the store's full key (an MD5 over the whole + corpus), which is sound here because the corpus is fixed at construction: only the space can + change under a given retriever. """ - if self._bank is not None: + space = self.store.space_key + if self._bank is not None and self._bank_space == space: return self._bank = _l2_normalize(self.store.vectors()) + self._bank_space = space def _encode(self, texts: list[str]) -> np.ndarray: """Embed ``texts`` in the model's configured space — the same one the scatter projects.""" @@ -160,6 +172,38 @@ def query(self, text: str, top_k: int = 5) -> list[Neighbor]: sims = self._bank @ query_vec # cosine, rows already unit-norm return self._top_k(sims, top_k) + def query_threshold(self, text: str, threshold: float, limit: int | None = None) -> tuple[list[Neighbor], int]: + """Return every reference example scoring above ``threshold``, most-similar first. + + Unlike :meth:`query`, the candidate set is not a fixed size: any reference example whose + cosine similarity to ``text`` exceeds ``threshold`` qualifies, however many that is. This is + the counterpart the webapp's threshold-filter mode needs — :meth:`query`'s ``top_k`` is an + ``argpartition`` over a fixed count and cannot tell you "how many clear 0.95", only "the k + best, whatever their score". + + Parameters + ---------- + text : str + The query text. + threshold : float + Minimum cosine similarity a reference example must *exceed* (strict) to qualify. + limit : int or None, optional + Cap on how many matches to return, most-similar first. ``None`` returns all of them — + callers rendering a live list should pass a display cap, since a low threshold can match + most of the corpus. + + Returns + ------- + tuple[list[Neighbor], int] + The matching neighbours (capped to ``limit``) and the total number that cleared + ``threshold`` before capping, so a caller can report e.g. "showing 50 of 138". + """ + self.build() + assert self._bank is not None # noqa: S101 - build() guarantees this + query_vec = _l2_normalize(self._encode([text]))[0] + sims = self._bank @ query_vec # cosine, rows already unit-norm + return self._above_threshold(sims, threshold, limit) + def query_many(self, texts: list[str], top_k: int = 5, exclude_self: bool = False) -> list[list[Neighbor]]: """Return the ``top_k`` most similar reference examples for **each** of ``texts``. @@ -215,6 +259,21 @@ def _top_k(self, sims: np.ndarray, top_k: int) -> list[Neighbor]: # argpartition for the top-k, then sort just those descending. top_idx = np.argpartition(-sims, k - 1)[:k] top_idx = top_idx[np.argsort(-sims[top_idx])] + # An excluded row is -inf; it only surfaces when the corpus has nothing else to offer. + top_idx = top_idx[np.isfinite(sims[top_idx])] + return self._neighbors_from_indices(top_idx, sims) + + def _above_threshold(self, sims: np.ndarray, threshold: float, limit: int | None) -> tuple[list[Neighbor], int]: + """Every finite row exceeding ``threshold``, descending, plus the pre-cap match count.""" + idx = np.flatnonzero(np.isfinite(sims) & (sims > threshold)) + idx = idx[np.argsort(-sims[idx])] + total = len(idx) + if limit is not None: + idx = idx[:limit] + return self._neighbors_from_indices(idx, sims), total + + def _neighbors_from_indices(self, idx: np.ndarray, sims: np.ndarray) -> list[Neighbor]: + """Build :class:`Neighbor` objects for ``idx``, in the order given.""" return [ Neighbor( index=int(i), @@ -222,9 +281,7 @@ def _top_k(self, sims: np.ndarray, top_k: int) -> list[Neighbor]: text=self.reference_texts[i], label=self.reference_labels[i] if self.reference_labels is not None else None, ) - for i in top_idx - # An excluded row is -inf; it only surfaces when the corpus has nothing else to offer. - if np.isfinite(sims[i]) + for i in idx ] diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py index 261fdc70..15524bf9 100644 --- a/shapash/explainer/interactive.py +++ b/shapash/explainer/interactive.py @@ -22,6 +22,7 @@ from shapash.compute.diagnostics.label_noise import LabelNoiseReport from shapash.compute.generators.base import Counterfactual, Field from shapash.compute.retrieval.similar_examples import Neighbor +from shapash.explainer.nlp_explanation import NlpExplanation @runtime_checkable @@ -79,8 +80,16 @@ def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: """Return the ``top_k`` reference examples most similar to ``text``, most-similar first.""" ... - def can_detect_label_noise(self) -> bool: - """Whether the compiled batch carries the ground truth + per-class probabilities this needs.""" + def find_similar_threshold(self, text: str, threshold: float = 0.95, limit: int = 50) -> tuple[list[Neighbor], int]: + """Return reference examples scoring above ``threshold`` (not a fixed count), capped at ``limit``. + + Returns ``(neighbors, total)`` where ``total`` is the count that cleared ``threshold`` before + capping to ``limit``. + """ + ... + + def can_detect_label_noise(self, explanation: NlpExplanation) -> bool: + """Whether ``explanation`` carries the ground truth + per-class probabilities this needs.""" ... def can_probe_labels(self) -> bool: @@ -88,7 +97,7 @@ def can_probe_labels(self) -> bool: ... def detect_label_noise( - self, top_n: int = 50, score: str = "self_confidence", probe: bool = True + self, explanation: NlpExplanation, top_n: int = 50, score: str = "self_confidence", probe: bool = True ) -> LabelNoiseReport: - """Rank probably-mislabelled samples in the compiled batch by confident learning.""" + """Rank probably-mislabelled samples in ``explanation`` by confident learning.""" ... diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 1c0b1252..295bc988 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -6,22 +6,24 @@ will call ``SmartExplainer.compile(x=TextDataset(texts))`` and this class will be removed. -For now, ``NlpExplainer`` provides a minimal compile → plot → webapp workflow -for text without touching the tabular path in ``SmartExplainer.compile()``. +``NlpExplainer`` follows the ``fit``/``explain`` seam (Amendment A5 in +``docs/architecture/refactoring-plan.md``): ``fit(X_reference, y=None)`` learns +reference state (the similar-example / label-noise-probe corpus) and +``explain(X, y=None)`` runs the backend and returns an immutable +:class:`~shapash.explainer.nlp_explanation.NlpExplanation` — the explainer +itself keeps no compiled batch. """ from __future__ import annotations import hashlib -import pickle import threading -from dataclasses import dataclass, replace +from dataclasses import replace from pathlib import Path from typing import cast import numpy as np import pandas as pd -from plotly import graph_objs as go from sklearn.decomposition import PCA from shapash.backend.nlp_backend import NlpBackend, NlpContributions @@ -37,9 +39,9 @@ from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever +from shapash.explainer.nlp_explanation import NlpExplanation from shapash.model.base import SupportsEmbeddings, SupportsTokenization, TextModel, has_capabilities from shapash.model.hf import HFPipelineModel -from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.webapp.nlp_app import NlpWebApp # Built-in counterfactual generators, in preference order: HotFlip (gradient-based, richer @@ -54,7 +56,7 @@ def _looks_like_pipeline(model: object) -> bool: def _cache_file(data_hash: str, cache_dir: Path) -> Path: - return cache_dir / f"{data_hash}.pkl" + return cache_dir / f"{data_hash}.xpl" def _reducer_tag(reducer: object) -> str: @@ -73,60 +75,6 @@ class name only — documented on :meth:`NlpExplainer.compute_projection` as nee return f"{name}-{digest[:8]}" -@dataclass -class InferenceResults: - """Model + explainer output for a batch — a pure function of *(texts, model, backend)*. - - This is exactly the payload the disk cache persists: recomputing it is the - expensive step ``compile`` memoizes, and all three of its inputs are in the - cache key (see :meth:`NlpExplainer._compute_key`). It deliberately excludes - anything that is *not* determined by those — ground truth, class names, the - 2-D projection — so a keyed cache file can never carry a stale ``y_true``. - - Attributes - ---------- - contributions : NlpContributions - Token-level contributions for every sample in the batch. - y_pred : pd.Series - Argmax label per sample. - y_prob : pd.DataFrame or None - Per-class probabilities, one column per class (or a single confidence - column), aligned to the sample index. - """ - - contributions: NlpContributions - y_pred: pd.Series - y_prob: pd.DataFrame | None = None - - -@dataclass -class TextResults: - """Portable results bundle for offline webapp serving. - - Wraps the computed :class:`InferenceResults` with the dataset context needed - to render without a live model — the source texts, optional ground truth, and - class names. This is the object :meth:`NlpExplainer.save_snapshot` serializes; - the scatter projection travels in the snapshot envelope alongside it, not here, - because it is a webapp view artifact rather than an explanation result. - - Attributes - ---------- - texts : pd.Series - The source text samples. - computed : InferenceResults - Contributions and predictions — the memoizable model/explainer output. - y_true : pd.Series or None - Ground-truth labels, when supplied to ``compile``. - label_names : list[str] or None - Human-readable class names in model-output order. - """ - - texts: pd.Series - computed: InferenceResults - y_true: pd.Series | None = None - label_names: list[str] | None = None - - class NlpExplainer: """Minimal explainer for text classification — token-level contributions. @@ -148,29 +96,15 @@ class NlpExplainer: Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. explainer_compute_args : dict, optional Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. - reference_corpus : tuple[list[str], list[str] or None], optional - ``(texts, labels)`` reference pool for similar-example retrieval — typically the - model's training set. When given *and* the model supports embeddings, the webapp gains a - "Similar Examples" panel that retrieves the most similar reference examples for the - selected/edited text. ``labels`` may be ``None``. Ignored when the model cannot embed - (e.g. a prediction-only pipeline). - Neighbours are compared in the model's own ``embedding_space``. To compare in a different - space, set the model's ``embedding_space`` — that moves the neighbours and any scatter built - by :meth:`compute_projection` together, since both read the space through the same store. - When ``labels`` is given, this corpus additionally trains the label-noise panel's - independent probe (:meth:`can_probe_labels`), which needs no model and so is available even - when retrieval is not. - reference_cache_dir : str or Path, optional - When given, the reference embedding bank is persisted here and reloaded on later runs, so - only the first launch pays the (one-off) cost of embedding the reference corpus. Examples -------- >>> import transformers >>> pipe = transformers.pipeline("text-classification", model="...", return_all_scores=True) >>> xpl = NlpExplainer(pipe, label_names=["sadness", "joy", "love", "anger", "fear", "surprise"]) - >>> xpl.compile(texts[:100]) - >>> xpl.run_app(port=8050) + >>> xpl.fit(reference_texts, y=reference_labels) # optional — see fit() + >>> explanation = xpl.explain(texts) + >>> xpl.run_app(explanation, port=8050) >>> from shapash.backend.nlp_lime_backend import NlpLimeBackend >>> lime_backend = NlpLimeBackend(classifier_fn, label_names=[...], explainer_compute_args={"num_features": 15}) @@ -185,8 +119,6 @@ def __init__( cf_generator: CounterfactualGenerator | None = None, explainer_args: dict | None = None, explainer_compute_args: dict | None = None, - reference_corpus: tuple[list[str], list[str] | None] | None = None, - reference_cache_dir: str | Path | None = None, ) -> None: self.model = model self.label_names = label_names @@ -235,114 +167,232 @@ def __init__( self.cf_generators: dict[str, CounterfactualGenerator] = {g.name: g for g in generators} self.cf_generator: CounterfactualGenerator | None = generators[0] if generators else None - # Similar-example retrieval: only when a reference corpus is supplied AND the model can embed - # (a prediction-only pipeline cannot) — otherwise the panel gates itself off, mirroring how - # counterfactuals require a gradient-capable model. + # Reference state (similar-example retrieval / label-noise probe corpus) is *fit-time*, not + # constructor-time — see fit(). Not initialized here at all: every reader already goes through + # getattr(self, "_retriever"/"reference_"/"_label_probe", None) (see can_find_similar, + # can_probe_labels, _attach_probe), which is also what keeps the object.__new__ bypass some + # tests use working without ever calling __init__ or fit(). + + # Private memoization for explain() — (hash, NlpExplanation), never assigned to public state + # otherwise (the compiled batch is only ever returned, never stored on self beyond this key). + # Excludes anything not determined by (texts, model, backend) — ground truth, class names — + # so a keyed cache entry can never carry a stale y (explain() overrides texts/y_true on read). + self._computed_cache: tuple[str, NlpExplanation] | None = None + + # Serializes the live compute ops (predict / explain_text / find_similar / generate) — the + # webapp runs Dash callbacks on Werkzeug's threaded server, and the shared HF fast tokenizer + # (used directly *and* via the SHAP pipeline) is not thread-safe: concurrent calls raise + # "Already borrowed". Re-entrant so explain_text can nest predict on the same thread. + self._compute_lock = threading.RLock() + + def fit( + self, + X_reference: list[str] | pd.Series | None = None, + y: list[str] | pd.Series | None = None, + cache_dir: str | Path | None = None, + precompute: bool = True, + ) -> NlpExplainer: + """Learn reference state: the similar-example / label-noise-probe corpus. + + None of the three built-in backends (SHAP, LIME, Captum LIG) have fit-time reference needs + of their own — see ``NlpBackend.reference_kind`` on each (``"none"``/``"point"``: a masker + that infers itself, or a point constructed by the tokenizer, neither learned from data). So + calling ``fit`` is optional; :meth:`explain` works standalone, exactly as ``compile`` did. + What *is* fit-time reference state for text — the two pieces the plan calls out (see the + refactoring plan's Context 4a) — is the corpus this method learns: + + Parameters + ---------- + X_reference : list[str] or pd.Series, optional + Reference pool for similar-example retrieval — typically the model's training set. + When given *and* the model supports embeddings, the webapp gains a "Similar Examples" + panel that retrieves the most similar reference examples for the selected/edited text. + Ignored when the model cannot embed (e.g. a prediction-only pipeline). + Neighbours are compared in the model's own ``embedding_space``. To compare in a + different space, set the model's ``embedding_space`` — that moves the neighbours and + any scatter built by :meth:`compute_projection` together, since both read the space + through the same store. + y : list[str] or pd.Series, optional + Labels for ``X_reference``. When given, this corpus additionally trains the label-noise + panel's independent probe (:meth:`can_probe_labels`), which needs no model and so is + available even when retrieval is not. + cache_dir : str or Path, optional + When given, the reference embedding bank is persisted here and reloaded on later runs, + so only the first call pays the (one-off) cost of embedding the reference corpus. + precompute : bool, optional + Embed the reference corpus here rather than on first use (default). The bank is the + expensive, amortizable half of retrieval — one forward pass per reference example — and + paying it in ``fit`` is what makes ``fit`` actually *fit*: the cost, and any failure + (a corpus the model cannot encode, an unwritable ``cache_dir``), surface at the call + that asked for them instead of inside the first webapp callback that happens to touch + the panel. Set ``False`` to keep the old lazy behaviour — worth it for a large corpus + behind a feature you may never open. No effect when no retriever was built. + + Returns + ------- + NlpExplainer + ``self``, following the scikit-learn ``fit`` convention. + + Notes + ----- + Sets ``self.reference_`` (the ``(texts, labels)`` pair, or ``None``) and ``self.classes_`` + (label names — from the model when known, else derived from ``y``) with the sklearn + trailing-underscore convention for fitted state. Does not subclass ``BaseEstimator``: text + models here are HuggingFace pipelines / torch models, not sklearn estimators, so there is no + ``Pipeline``/skrub interop need to justify full conformance (``get_params``/``clone``) the + way there is for the tabular explainer — see the refactoring plan's Phase 1. + """ + ref_texts = list(X_reference) if X_reference is not None else None + ref_labels = list(y) if y is not None else None + if ref_texts is not None and ref_labels is not None and len(ref_texts) != len(ref_labels): + raise ValueError(f"y length ({len(ref_labels)}) must match X_reference length ({len(ref_texts)}).") + self._retriever: SimilarExampleRetriever | None = None - if reference_corpus is not None and has_capabilities(self._text_model, SupportsEmbeddings): - ref_texts, ref_labels = reference_corpus + if ref_texts is not None and has_capabilities(self._text_model, SupportsEmbeddings): self._retriever = SimilarExampleRetriever( self._text_model, # type: ignore[arg-type] # has_capabilities narrows the capability reference_texts=ref_texts, reference_labels=ref_labels, - cache_dir=reference_cache_dir, + cache_dir=cache_dir, ) - # Kept as plain data beside the retriever, not inside it: the label probe needs only labelled - # text and no model at all, so it must stay available for a prediction-only pipeline that - # cannot embed — exactly the case the retriever above declines to build for. - self._reference_corpus: tuple[list[str], list[str]] | None = ( - (list(reference_corpus[0]), list(reference_corpus[1])) - if reference_corpus is not None and reference_corpus[1] is not None - else None - ) self._label_probe: LabelProbe | None = None # fit lazily on first use, then reused - self.contributions: NlpContributions | None = None - self.texts: pd.Series | None = None - self.y_pred: pd.Series | None = None - self.y_prob: pd.DataFrame | None = None - self.y_true: pd.Series | None = None - self._data_hash: str | None = None - # Serializes the live compute ops (predict / explain_text / find_similar / generate) — the - # webapp runs Dash callbacks on Werkzeug's threaded server, and the shared HF fast tokenizer - # (used directly *and* via the SHAP pipeline) is not thread-safe: concurrent calls raise - # "Already borrowed". Re-entrant so explain_text can nest predict on the same thread. - self._compute_lock = threading.RLock() + # Kept as plain data, not inside the retriever: the label probe (can_probe_labels, + # _attach_probe) needs only labelled text and no model at all, so it must stay available for + # a prediction-only pipeline that cannot embed — exactly the case the retriever above + # declines to build for. + self.reference_ = (ref_texts, ref_labels) if ref_texts is not None else None + self.classes_ = self._resolve_classes(ref_labels) + + # Deliberately last: building the bank runs the model over the whole corpus and may raise + # (OOM, an unencodable corpus, an unwritable cache_dir). Assigning reference_/classes_ first + # means such a failure costs only retrieval — the label probe, which needs no model at all, + # is already usable on the half-fitted object. + if precompute and self._retriever is not None: + self._retriever.build() + return self + + def _resolve_classes(self, y: list[str] | None) -> list[str] | None: + """Label names known from the model/constructor, else derived from ``y``.""" + text_model = getattr(self, "_text_model", None) + if text_model is not None and text_model.label_names is not None: + return list(text_model.label_names) + if self.label_names is not None: + return list(self.label_names) + return sorted(set(y)) if y else None - def compile( + def explain( self, - x: list[str] | pd.Series, - y_true: list | pd.Series | None = None, + X: list[str] | pd.Series, + y: list | pd.Series | None = None, cache_dir: str | Path | None = None, - ) -> None: - """Compute token-level contributions and model predictions. - - Results are cached in memory keyed by the input texts *together with the model and backend - that score them* (see :meth:`_compute_key`), so re-calling ``compile`` with the same data on - the same instance skips the expensive explainer run — while changing the model, the backend or - ``label_names`` correctly recomputes. Passing only ``y_true`` (same ``x``) is always a - lightweight metadata update. For persistence across kernel restarts, pass a ``cache_dir`` - path to enable an opt-in disk cache. + ) -> NlpExplanation: + """Compute token-level contributions and model predictions, and return them as an artifact. + + Results are memoized in memory, keyed by the input texts *together with the model and + backend that score them* (see :meth:`_compute_key`), so calling ``explain`` again with the + same data on the same instance skips the expensive explainer run — while changing the + model, the backend or ``label_names`` correctly recomputes. The explainer keeps nothing + else: every call returns a fresh + :class:`~shapash.explainer.nlp_explanation.NlpExplanation`; ``y`` (ground truth) travels + only on that returned artifact, never on ``self``. "Fresh" means a new object, not a deep + copy — it is derived from the memoized artifact via + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.relabelled` and shares its + contribution arrays with it. Those arrays are sealed read-only by + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.__post_init__`, so the sharing is + safe: an in-place edit raises rather than quietly rewriting the memo. Derive variants with + ``dataclasses.replace``. For persistence across kernel restarts + (or across processes), pass a ``cache_dir`` path to enable an opt-in disk cache — it is + keyed by hash automatically rather than by an explicit path, but on disk it is exactly a + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.save` file, so it stays one format + whether you let ``explain`` manage it or call ``save``/``load`` yourself. Parameters ---------- - x : list[str] or pd.Series + X : list[str] or pd.Series Text samples to explain. - y_true : list or pd.Series, optional - Ground-truth labels for each sample. When provided, the webapp will - show a "Ground Truth" column alongside "Prediction" in the dataset - table, making it easy to spot misclassifications. + y : list or pd.Series, optional + Ground-truth labels for each sample, carried on the returned artifact + (``NlpExplanation.y_true``). When provided, the webapp will show a "Ground Truth" + column alongside "Prediction" in the dataset table, making it easy to spot + misclassifications. cache_dir : str or Path, optional - If provided, contributions and predictions are also persisted to - ``<cache_dir>/<hash>.pkl`` and reloaded on subsequent calls — even - after a kernel restart. Disabled by default. One directory can be - shared across models and backends: the hash identifies them, so - entries cannot collide. + If provided, the computed explanation is also persisted to + ``<cache_dir>/<hash>.xpl`` (an :class:`~shapash.explainer.nlp_explanation.NlpExplanation` + file) and reloaded on subsequent calls — even after a kernel restart. Disabled by + default. One directory can be shared across models and backends: the hash identifies + them, so entries cannot collide. + + Returns + ------- + NlpExplanation + The computed contributions, predictions and ground truth for this batch. """ - texts = x if isinstance(x, pd.Series) else pd.Series(x) + texts = X if isinstance(X, pd.Series) else pd.Series(X) text_list = texts.tolist() new_hash = self._compute_key(text_list) - if new_hash != self._data_hash: - self.texts = texts + # ``computed`` is an NlpExplanation because that is the one on-disk format, but only its + # ``_COMPUTED_FIELDS`` half is meaningful here — its own texts/index/y_true belong to + # whoever computed it first. See ``relabelled`` at the return below. + cached = getattr(self, "_computed_cache", None) + if cached is not None and cached[0] == new_hash: + computed = cached[1] + else: cache_path = _cache_file(new_hash, Path(cache_dir)) if cache_dir is not None else None if cache_path is not None and cache_path.exists(): - with cache_path.open("rb") as f: - computed: InferenceResults = pickle.load(f) # noqa: S301 + computed, _ = NlpExplanation.load(cache_path) else: - explain_data = self.backend.run_explainer(text_list) - contributions = self.backend.get_local_contributions(text_list, explain_data) - pred_df = self._predict(text_list) - computed = InferenceResults( - contributions=contributions, - y_pred=pred_df["prediction"], - y_prob=pred_df.drop(columns=["prediction"]), + contributions = self.backend.run_explainer(text_list) + pred_df = self._predict(text_list, texts.index) + y_pred, y_prob = pred_df["prediction"], pred_df.drop(columns=["prediction"]) + backend_cls = type(self.backend) + computed = NlpExplanation( + texts=texts, + token_strings=contributions.token_strings, + values=contributions.values, + base_values=contributions.base_values, + y_pred=y_pred, + y_prob=y_prob, + y_true=None, + label_names=self.label_names, + folds_case=self._folds_case(), + backend_name=backend_cls.name, + is_additive=backend_cls.is_additive, + reference_kind=backend_cls.reference_kind, ) if cache_path is not None: cache_path.parent.mkdir(parents=True, exist_ok=True) - with cache_path.open("wb") as f: - pickle.dump(computed, f) - - self.contributions = computed.contributions - self.y_pred = computed.y_pred - self.y_prob = computed.y_prob - - self.contributions.label_names = self.label_names - self.contributions.index = self.texts.index - # Set here rather than in the backend so a cache written before this existed still gets - # it (the branch above may have unpickled ``computed``), and so every backend inherits - # one answer — the question is about the *model*, not about how contributions were computed. - self.contributions.folds_case = self._folds_case() - self._data_hash = new_hash - - if y_true is not None: - self.y_true = ( - y_true if isinstance(y_true, pd.Series) else pd.Series(y_true, index=texts.index, name="ground_truth") - ) + computed.save(cache_path) + + self._computed_cache = (new_hash, computed) + + if y is None: + y_series = None + elif isinstance(y, pd.Series): + # Aligned to the texts, or refused. Silently reindexing a labelled Series onto + # texts.index would pair rows positionally under the appearance of index alignment, + # which is exactly how ground truth ends up attached to the wrong sample. + if not y.index.equals(texts.index): + raise ValueError( + "y is a Series indexed differently from X, so its labels cannot be matched to " + f"the texts: X.index starts {list(texts.index[:3])!r} while y.index starts " + f"{list(y.index[:3])!r} (lengths {len(texts.index)} and {len(y.index)}). " + "Reindex y onto X.index, or pass it as a plain list to be taken in order." + ) + y_series = y + else: + y_series = pd.Series(y, index=texts.index, name="ground_truth") + + # ``computed`` is whatever the cache key covers, so on a hit it still carries the index and + # the ground truth of whichever call populated it. ``relabelled`` swaps in this caller's: + # NlpExplanation owns the split between the fields a content key determines and the fields + # it cannot, so that list stays next to the fields rather than being restated here. + return computed.relabelled(texts, y_true=y_series) def cache_path(self, x: list[str] | pd.Series, cache_dir: str | Path) -> Path: - """Return the file :meth:`compile` would read/write for ``x`` under ``cache_dir``. + """Return the file :meth:`explain` would read/write for ``x`` under ``cache_dir``. Lets a caller report a cache hit or drop a stale entry without reconstructing the key, which depends on the bound model and backend and is the explainer's to own (see :meth:`_compute_key`). @@ -350,63 +400,37 @@ def cache_path(self, x: list[str] | pd.Series, cache_dir: str | Path) -> Path: Parameters ---------- x : list[str] or pd.Series - The same texts that would be passed to :meth:`compile`. + The same texts that would be passed to :meth:`explain`. cache_dir : str or Path - The same directory that would be passed to :meth:`compile`. + The same directory that would be passed to :meth:`explain`. Returns ------- Path - The cache file's location. It need not exist. + The cache file's location (an :class:`~shapash.explainer.nlp_explanation.NlpExplanation` + file). It need not exist. """ text_list = x.tolist() if isinstance(x, pd.Series) else list(x) return _cache_file(self._compute_key(text_list), Path(cache_dir)) def clear_cache(self, x: list[str] | pd.Series, cache_dir: str | Path) -> None: - """Drop the cached :meth:`compile` result for ``x``, in memory and on disk. + """Drop the cached :meth:`explain` result for ``x``, in memory and on disk. The counterpart of :meth:`~shapash.compute.embedding_store.EmbeddingStore.clear` for contributions: use it to force a fresh explainer run. Only *this* model+backend's entry is removed — another backend's cache for the same texts is left intact. """ self.cache_path(x, cache_dir).unlink(missing_ok=True) - self._data_hash = None - - def text_plot(self, pos: int = 0, label_idx: int = 0, max_tokens: int | None = None) -> go.Figure: - """Plot token-level contributions for one sample and one class. - - Parameters - ---------- - pos : int - Positional index of the sample within the compiled batch. - label_idx : int - Index of the class to display (matches ``label_names`` order). - max_tokens : int, optional - If set, show only the top-``max_tokens`` tokens by absolute - contribution magnitude (in their original sentence order). - - Returns - ------- - go.Figure - """ - if self.contributions is None: - raise RuntimeError("Call compile() before text_plot().") + self._computed_cache = None - tokens = self.contributions.token_strings[pos] - values = self.contributions.values[pos] - - if values.ndim == 2: - values = values[:, label_idx] - - label_name = ( - self.label_names[label_idx] if self.label_names is not None and label_idx < len(self.label_names) else None - ) - title = f"Token contributions — {label_name}" if label_name else "Token contributions" - - return plot_token_highlight(tokens=tokens, values=values, title=title, max_tokens=max_tokens) - - def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, recompute: bool = False): - """Return a 2-D projection of the compiled texts, ready to pass to :meth:`run_app`. + def compute_projection( + self, + explanation: NlpExplanation, + reducer=None, + cache_dir: str | Path | None = None, + recompute: bool = False, + ): + """Return a 2-D projection of ``explanation``'s texts, ready to pass to :meth:`run_app`. The library owns the parts that must stay consistent — *which* space the texts are embedded in and how that is cached — while the caller injects the dimensionality reducer, which is a @@ -416,6 +440,8 @@ def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, Parameters ---------- + explanation : NlpExplanation + The result of :meth:`explain` for the batch to project. reducer : object, optional Anything with ``fit_transform(X) -> (n_samples, 2)`` — ``sklearn`` PCA/TSNE, ``pacmap``, ``umap``. Defaults to :class:`sklearn.decomposition.PCA` with two components (sklearn is @@ -440,12 +466,10 @@ def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, Examples -------- - >>> xpl.compile(texts) - >>> xy = xpl.compute_projection(reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/") - >>> xpl.run_app(scatter_xy=xy) + >>> explanation = xpl.explain(texts) + >>> xy = xpl.compute_projection(explanation, reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/") + >>> xpl.run_app(explanation, scatter_xy=xy) """ - if self.texts is None: - raise RuntimeError("Call compile() before compute_projection().") text_model = self._require_text_model() if not has_capabilities(text_model, SupportsEmbeddings): raise TypeError( @@ -457,7 +481,7 @@ def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, store = EmbeddingStore( text_model, # type: ignore[arg-type] # has_capabilities narrows the capability - self.texts.tolist(), + explanation.texts.tolist(), cache_dir=cache_dir, ) if recompute: @@ -468,75 +492,29 @@ def compute_projection(self, reducer=None, cache_dir: str | Path | None = None, lambda: np.asarray(reducer.fit_transform(store.vectors())), ) - def save_snapshot(self, path: str | Path, scatter_xy=None) -> None: - """Persist compiled results (no model or backend) for offline serving. - - The saved file contains only the data required by ``NlpWebApp`` — - texts, contributions, predictions and the optional 2-D projection. - It can be loaded back with :meth:`from_snapshot` without installing - torch, transformers, or any other heavy inference dependency. - - Parameters - ---------- - path : str or Path - Destination ``.pkl`` file. - scatter_xy : np.ndarray, optional - Pre-computed 2-D projection to bundle with the snapshot (same - array you would pass to ``run_app``). - """ - if self.contributions is None: - raise RuntimeError("Call compile() before save_snapshot().") - results = TextResults( - texts=self.texts, - computed=InferenceResults(contributions=self.contributions, y_pred=self.y_pred, y_prob=self.y_prob), - y_true=self.y_true, - label_names=self.label_names, - ) - # Snapshot envelope: the portable results bundle plus the scatter projection, - # which is a webapp view artifact and so rides alongside TextResults, not within it. - with Path(path).open("wb") as f: - pickle.dump({"results": results, "scatter_xy": scatter_xy}, f) - - @classmethod - def from_snapshot(cls, path: str | Path) -> tuple[NlpExplainer, object]: - """Restore a snapshot saved by :meth:`save_snapshot`. + def run_app( + self, + explanation: NlpExplanation, + port: int = 8050, + debug: bool = False, + host: str = "127.0.0.1", + scatter_xy=None, + ) -> None: + """Launch the NLP explanation webapp for ``explanation``. - Returns - ------- - explainer : NlpExplainer - Ready-to-serve instance (``model`` and ``backend`` are ``None``). - scatter_xy : np.ndarray or None - The projection array bundled at save time, or ``None``. - """ - with Path(path).open("rb") as f: - state = pickle.load(f) # noqa: S301 - results: TextResults = state["results"] - xpl = cls.__new__(cls) - xpl.model = None - xpl.backend = None # type: ignore[assignment] # snapshot instance never runs live compute - xpl._text_model = None - xpl.cf_generator = None - xpl.cf_generators = {} - xpl._retriever = None - # The snapshot carries results, not the corpus they were compared against, so the label - # probe cannot be refit — label-noise detection still runs, just without the second opinion. - xpl._reference_corpus = None - xpl._label_probe = None - xpl.label_names = results.label_names - xpl.texts = results.texts - xpl.contributions = results.computed.contributions - xpl.y_pred = results.computed.y_pred - xpl.y_prob = results.computed.y_prob - xpl.y_true = results.y_true - xpl._data_hash = None - xpl._compute_lock = threading.RLock() - return xpl, state.get("scatter_xy") - - def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1", scatter_xy=None) -> None: - """Launch the NLP explanation webapp. + The webapp reads ``explanation`` directly — it never writes to it, so the artifact it renders + stays the one that was computed — and reaches back to ``self`` (as an :class:`~shapash.explainer.interactive.InteractiveEngine`) + only for live what-if actions (re-predicting edited text, generating counterfactuals, + similar-example retrieval, label-noise detection) — a snapshot loaded via + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.load` has no such engine and serves + this same method with ``engine=None`` passed to :class:`~shapash.webapp.nlp_app.NlpWebApp` + directly, self-disabling those panels. Parameters ---------- + explanation : NlpExplanation + The result of :meth:`explain` (or a loaded one — see + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.load`) to serve. port : int Port for the Dash development server. debug : bool @@ -555,9 +533,7 @@ def run_app(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1" than it appears to. Use it when you want coordinates the library cannot produce, and accept that keeping them consistent is yours to manage. """ - if self.contributions is None: - raise RuntimeError("Call compile() before run_app().") - NlpWebApp(self, scatter_xy=scatter_xy).run(port=port, debug=debug, host=host) + NlpWebApp(explanation, engine=self, scatter_xy=scatter_xy).run(port=port, debug=debug, host=host) # ------------------------------------------------------------------ # InteractiveEngine — live what-if surface (see explainer/interactive.py) @@ -574,7 +550,11 @@ def _compute_guard(self) -> threading.RLock: def can_edit(self) -> bool: """Whether edited text can be re-predicted and re-explained live. - Requires a live model adapter and backend (both are ``None`` after ``from_snapshot``). + Requires a capability-aware model adapter (``False`` for a bare LIME ``classifier_fn`` with + no ``TextModel`` wrapper). An :class:`~shapash.explainer.nlp_explanation.NlpExplanation` + loaded via :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.load` carries no engine + at all — :class:`~shapash.webapp.nlp_app.NlpWebApp` is served with ``engine=None`` in that + case, and this method is never reached. """ return getattr(self, "_text_model", None) is not None and getattr(self, "backend", None) is not None @@ -585,7 +565,7 @@ def can_counterfactual(self) -> bool: def can_find_similar(self) -> bool: """Whether similar-example retrieval is available (a reference corpus + embedding-capable model). - ``False`` after ``from_snapshot`` (no model) or when no ``reference_corpus`` was supplied. + ``False`` when :meth:`fit` was never called with an ``X_reference``, or the model cannot embed. """ return getattr(self, "_retriever", None) is not None @@ -612,39 +592,76 @@ def find_similar(self, text: str, top_k: int = 5) -> list[Neighbor]: with self._compute_guard(): # embed() tokenizes — serialize against explain_text/predict return retriever.query(text, top_k=top_k) - def can_detect_label_noise(self) -> bool: - """Whether label-noise detection can run on the compiled batch. + def find_similar_threshold(self, text: str, threshold: float = 0.95, limit: int = 50) -> tuple[list[Neighbor], int]: + """Return reference examples scoring above ``threshold``, in the model's embedding space. + + Companion to :meth:`find_similar` for the webapp's threshold-filter mode: rather than a fixed + count, every reference example that exceeds ``threshold`` qualifies. ``limit`` caps how many + are actually returned (a low threshold can otherwise match most of the corpus); the second + return value is the *total* count that cleared the threshold, so a caller can report e.g. + "showing 50 of 138". + + Parameters + ---------- + text : str + Query text (a selected dataset row, an edited sentence, or a counterfactual). + threshold : float + Minimum cosine similarity a reference example must exceed to qualify. + limit : int + Maximum neighbours to return, most-similar first. + + Returns + ------- + tuple[list[Neighbor], int] + Matching neighbours (capped to ``limit``) and the total number that cleared ``threshold``. + """ + retriever = getattr(self, "_retriever", None) + if retriever is None: + raise RuntimeError( + "find_similar_threshold() requires a reference_corpus and an embedding-capable model " + "(unavailable on a snapshot)." + ) + with self._compute_guard(): # embed() tokenizes — serialize against explain_text/predict + return retriever.query_threshold(text, threshold=threshold, limit=limit) + + def can_detect_label_noise(self, explanation: NlpExplanation) -> bool: + """Whether label-noise detection can run on ``explanation``. Needs ground truth plus one probability column per class — nothing else. In particular it - needs **no model**, so unlike the other capability flags this stays ``True`` after - :meth:`from_snapshot`; only the optional neighbour corroboration in - :meth:`detect_label_noise` requires a live embedding-capable model. + needs **no model**, so unlike the other capability flags this stays usable on an + :class:`~shapash.explainer.nlp_explanation.NlpExplanation` loaded from disk (no live model); + only the optional neighbour corroboration in :meth:`detect_label_noise` requires a live + embedding-capable model. - ``False`` when ``compile()`` got no ``y_true``, and when the bound model is a raw pipeline - that reports only the winning class's confidence (a single ``probability`` column). + ``False`` when ``explanation`` carries no ``y_true``, and when the bound model is a raw + pipeline that reports only the winning class's confidence (a single ``probability`` column). """ - return getattr(self, "y_true", None) is not None and has_usable_probabilities(getattr(self, "y_prob", None)) + return explanation.y_true is not None and has_usable_probabilities(explanation.y_prob) def can_probe_labels(self) -> bool: """Whether the model-independent label probe can run (needs a *labelled* reference corpus). The probe is fit on the reference corpus, so unlike :meth:`can_find_similar` it needs no - model — a prediction-only pipeline still gets it. It is ``False`` after - :meth:`from_snapshot`, which carries results but not the corpus they were compared against. + model — a prediction-only pipeline still gets it. It is ``False`` before :meth:`fit` is + called with a labelled ``X_reference``. """ - return getattr(self, "_reference_corpus", None) is not None + reference = getattr(self, "reference_", None) + return reference is not None and reference[1] is not None def detect_label_noise( - self, top_n: int = 50, score: str = "self_confidence", probe: bool = True + self, explanation: NlpExplanation, top_n: int = 50, score: str = "self_confidence", probe: bool = True ) -> LabelNoiseReport: - """Rank the compiled samples whose ground-truth label is probably wrong. + """Rank ``explanation``'s samples whose ground-truth label is probably wrong. Runs confident learning (see :mod:`shapash.compute.diagnostics.label_noise`) over the - probabilities and labels already held from :meth:`compile`, then attaches a - model-independent second opinion to each flagged sample. + probabilities and labels carried on ``explanation``, then attaches a model-independent + second opinion to each flagged sample. Parameters ---------- + explanation : NlpExplanation + The result of :meth:`explain` for the batch to audit — must carry ``y_true`` and + per-class ``y_prob`` (see :meth:`can_detect_label_noise`). top_n : int Maximum number of issues to return, worst first. score : {"self_confidence", "normalized_margin"} @@ -681,27 +698,30 @@ def detect_label_noise( Examples -------- - >>> xpl.compile(texts, y_true=labels) - >>> report = xpl.detect_label_noise(top_n=20) + >>> explanation = xpl.explain(texts, y=labels) + >>> report = xpl.detect_label_noise(explanation, top_n=20) >>> report.noise_rate 0.062 >>> report.issues[0].probe.backs_given # corpus sides with the label -> suspect the model True """ - if not self.can_detect_label_noise(): + if not self.can_detect_label_noise(explanation): raise RuntimeError( - "detect_label_noise() needs ground-truth labels (pass y_true to compile()) and " + "detect_label_noise() needs ground-truth labels (pass y= to explain()) and " "per-class probabilities (a model reporting every class's score, not just the " "predicted one)." ) - cache_key = (getattr(self, "_data_hash", None), top_n, score, probe) + # id() is safe here because the cache only needs to survive while the caller keeps holding + # this exact explanation (the webapp's typical pattern — NlpWebApp holds one for its lifetime); + # a garbage-collected explanation can't collide because nothing still holds its id then. + cache_key = (id(explanation), top_n, score, probe) cached = getattr(self, "_label_noise_cache", None) if cached is not None and cached[0] == cache_key: return cast("LabelNoiseReport", cached[1]) - y_prob = cast("pd.DataFrame", self.y_prob) - y_true = cast("pd.Series", self.y_true) - texts = cast("pd.Series", self.texts) + y_prob = cast("pd.DataFrame", explanation.y_prob) + y_true = cast("pd.Series", explanation.y_true) + texts = explanation.texts report = detect_label_issues( y_prob.to_numpy(dtype=float), [str(v) for v in y_true.tolist()], @@ -726,7 +746,7 @@ def _attach_probe(self, issues: list) -> list: """ probe = getattr(self, "_label_probe", None) if probe is None: - ref_texts, ref_labels = cast("tuple[list[str], list[str]]", self._reference_corpus) + ref_texts, ref_labels = cast("tuple[list[str], list[str]]", self.reference_) probe = LabelProbe(ref_texts, ref_labels) self._label_probe = probe verdicts = probe.verdicts([iss.text for iss in issues], [iss.given_label for iss in issues]) @@ -751,9 +771,7 @@ def explain_text(self, text: str) -> tuple[NlpContributions, str, dict[str, floa # Hold the lock across the whole op: run_explainer drives the SHAP pipeline (which tokenizes) # and predict tokenizes again — both must be serialized against a concurrent find_similar. with self._compute_guard(): - raw = self.backend.run_explainer([text]) - contributions = self.backend.get_local_contributions([text], raw) - contributions.label_names = self.label_names + contributions = self.backend.run_explainer([text]) label, probabilities = self.predict(text) # re-entrant: same thread re-acquires the RLock return contributions, label, probabilities @@ -806,16 +824,16 @@ def _select_cf_generator(self, generator: str | None) -> CounterfactualGenerator def _compute_key(self, text_list: list[str]) -> str: """Return the cache key for explaining ``text_list`` with *this* model and backend. - :class:`InferenceResults` is a function of the texts **and** of everything that scores them, so - all of it belongs in the key: the model's own identity declaration + The cached explanation is a function of the texts **and** of everything that scores them, + so all of it belongs in the key: the model's own identity declaration (:attr:`~shapash.model.base.TextModel.model_id` — checkpoint, pooling, normalization, head weights), the backend's registered ``name``, its explainer settings, and ``label_names`` (which fixes the column order of ``y_prob``). Keying on the texts alone — as this once did — means swapping the backend (SHAP -> LIG), the model, or the label order and pointing at the same ``cache_dir`` silently reloads the - previous run's contributions. It also defeats the in-memory guard in :meth:`compile`, so even - without a ``cache_dir`` a re-``compile`` after changing the backend was a no-op. Callers + previous run's contributions. It also defeats the in-memory guard in :meth:`explain`, so even + without a ``cache_dir`` a re-``explain`` after changing the backend was a no-op. Callers currently work around this by hand-partitioning ``cache_dir`` per model and per backend; with the identity in the key that is no longer necessary or possible to get wrong. """ @@ -848,7 +866,7 @@ def _folds_case(self) -> bool | None: return None return cast("SupportsTokenization", text_model).folds_case() - def _predict(self, text_list: list[str]) -> pd.DataFrame: + def _predict(self, text_list: list[str], index: pd.Index) -> pd.DataFrame: """Run the pipeline and return a unified DataFrame of predictions and probabilities. The first column is always ``"prediction"`` (the argmax label). @@ -859,14 +877,13 @@ def _predict(self, text_list: list[str]) -> pd.DataFrame: Handles both ``return_all_scores=True`` (list of lists of dicts) and single-prediction (list of dicts) pipeline output formats. """ - assert self.texts is not None # noqa: S101 - compile() sets this before calling _predict text_model = getattr(self, "_text_model", None) if text_model is not None: probs = text_model.predict(text_list) names = text_model.label_names or self.label_names or [str(i) for i in range(probs.shape[1])] - result = pd.DataFrame(probs, index=self.texts.index, columns=list(names)) + result = pd.DataFrame(probs, index=index, columns=list(names)) labels = [names[int(i)] for i in probs.argmax(axis=1)] - result.insert(0, "prediction", pd.Series(labels, index=self.texts.index)) + result.insert(0, "prediction", pd.Series(labels, index=index)) return result raw = self.model(text_list) @@ -876,12 +893,12 @@ def _predict(self, text_list: list[str]) -> pd.DataFrame: if self.label_names and len(self.label_names) == len(col_labels): col_labels = self.label_names probs = [[d["score"] for d in preds] for preds in raw] - result = pd.DataFrame(probs, index=self.texts.index, columns=col_labels) + result = pd.DataFrame(probs, index=index, columns=col_labels) else: labels = [p["label"] for p in raw] result = pd.DataFrame( {"probability": [p["score"] for p in raw]}, - index=self.texts.index, + index=index, ) - result.insert(0, "prediction", pd.Series(labels, index=self.texts.index)) + result.insert(0, "prediction", pd.Series(labels, index=index)) return result diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py new file mode 100644 index 00000000..93e0b907 --- /dev/null +++ b/shapash/explainer/nlp_explanation.py @@ -0,0 +1,761 @@ +"""``NlpExplanation`` — the immutable artifact returned by ``NlpExplainer.explain()``. + +The one result object for a batch: token-level contributions together with predictions, +ground truth and provenance, plus the word-importance aggregation behavior that acts on +them. There is deliberately no separate "contributions" sub-object nested inside it — +see the class docstring's Design note. + +Persistence is a zip container (stdlib ``zipfile``) holding a plain-text ``meta.json`` +(readable without any Python/shapash install) plus long/tidy parquet tables for the +numeric payload — no pickle anywhere. ``pyarrow`` is already a core shapash dependency, +so this costs nothing new. See :meth:`NlpExplanation.save`/:meth:`NlpExplanation.load`. +""" + +from __future__ import annotations + +import io +import json +import re +import zipfile +from dataclasses import dataclass, fields, replace +from datetime import UTC, datetime +from pathlib import Path +from typing import TYPE_CHECKING, ClassVar, Literal + +import numpy as np +import pandas as pd + +from shapash.__version__ import __version__ as _shapash_version +from shapash.backend.nlp_backend import is_punctuation + +if TYPE_CHECKING: + from shapash.explainer.nlp_plotter import NlpPlotter + +# Stamped into meta.json on save and checked on load, so an incompatible file fails loudly instead of +# parsing into wrong numbers — a layout change (wide instead of tidy contributions, a different +# class_idx encoding) often still reads cleanly and yields a plausible, wrong explanation. The risk is +# live rather than hypothetical: explain() writes these files into cache_dir and its key covers +# texts/model/backend but *not* the shapash version, so an upgrade leaves old files the new reader +# will hit. It also cannot be added retroactively — a file written without a version is forever +# ambiguous — which is why it sits here at v1 with nothing yet to reject. Limit: it versions the +# *layout* only; numbers whose meaning changes under an identical schema are invisible to it (the +# shapash_version recorded alongside it is the breadcrumb for that, and nothing checks it). The tuple +# is what lets a future reader accept v1 as well as its own — the migration code does not exist yet. +_FORMAT_VERSION = 1 +_SUPPORTED_FORMAT_VERSIONS = (1,) + +_PROB_PREFIX = "y_prob__" + + +def select_label_column(values: np.ndarray, label_idx: int) -> np.ndarray: + """Pick one class's column out of a per-token contribution array. + + Multiclass backends return ``values`` shaped ``(n_tokens, n_classes)``; binary and + regression backends already return the 1-D case. Every render path — per-instance + plots, corpus-level word importance, the webapp's live datapoint — needs this same + rule applied consistently, so it lives here once rather than as a repeated inline + ``if values.ndim == 2`` check. + """ + return values[:, label_idx] if values.ndim == 2 else values + + +@dataclass(frozen=True, eq=False, slots=True) +class NlpExplanation: + """Immutable result of :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain`. + + A pure function of *(texts, model, backend)* — model-free and backend-free by + construction, so it can be saved, shared and reloaded without the model that + produced it (:meth:`save`/:meth:`load`). + + Immutability is enforced, not merely intended, because rendering code and the webapp are + written against the promise that what they draw is what was computed: + + - ``frozen=True`` — rebinding a field raises ``FrozenInstanceError``. This matters beyond + tidiness: :attr:`is_additive` gates + :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.waterfall`, so a writable flag would be a + correctness guard a caller could switch off by assignment. + - The contribution arrays are sealed read-only in :meth:`__post_init__`, so the artifact is + immutable in depth and not only at its surface. + - ``slots=True`` — the instance has no ``__dict__``, so no attribute that is not a declared + field can be attached to it at all. ``frozen`` already refuses ordinary assignment, but it is + enforced through ``__setattr__`` and so is bypassable with ``object.__setattr__``; without a + ``__dict__`` that bypass has nowhere to write an undeclared name. This is what makes "no + display state on the artifact" a property of the class rather than a review rule: a webapp + callback cannot stash ``_selected_row`` here even by accident. It is *not* here for memory — + one instance exists per batch and it points at megabytes of arrays. The cost to know about: + ``functools.cached_property`` needs a ``__dict__`` and so cannot be used on this class. + - ``eq=False`` — value equality is meaningless here (comparing the ``pd.Series`` fields raises + "truth value is ambiguous"), so the object compares and hashes by identity rather than + offering an ``__eq__`` that only ever raises. Use :meth:`save`/:meth:`load` to compare runs. + + The boundary worth knowing: the pandas fields (``texts``, ``y_pred``, ``y_prob``, ``y_true``) + are held by reference and *not* sealed — pandas has no equivalent write lock — so in-place + edits through them are still possible. Derive a modified artifact with + ``dataclasses.replace(explanation, ...)``, which is the supported way to vary any field. + + Design note: token-level data (``token_strings``/``values``/``base_values``/``folds_case``) + lives here as flat fields rather than behind a separate ``NlpContributions`` sub-object. + An earlier version nested one, which produced two mutable copies of ``label_names`` (one + here, one on the sub-object) that could disagree, plus a redundant ``index`` field that was + always exactly ``texts.index``. There is no batch-level reuse that justifies that split — the + only other place per-sample contributions travel is + :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain_text`'s single-text return + (:class:`~shapash.backend.nlp_backend.NlpContributions`, the backend's own raw 3-field + output), which every caller immediately unpacks to bare arrays and never wraps in this class. + + Attributes + ---------- + texts : pd.Series + The explained text samples. + token_strings : list[list[str]] + Tokenized (word-level) representation of each sample, variable length. + values : list[np.ndarray] + Per-token contribution values, one array per sample, shape ``(n_tokens_i, n_classes)`` + for multi-class models or ``(n_tokens_i,)`` for binary/regression. + base_values : np.ndarray or None + Baseline prediction for each sample, shape ``(n_samples, n_classes)`` or + ``(n_samples,)``. ``None`` when the backend has no reference at all (see + :attr:`~shapash.backend.nlp_backend.NlpBackend.reference_kind`). + y_pred : pd.Series + Argmax label per sample. + y_prob : pd.DataFrame or None + Per-class probabilities, one column per class (or a single confidence + column), aligned to ``texts``. + y_true : pd.Series or None + Ground-truth labels, when supplied to ``explain()``. + label_names : list[str] or None + Human-readable class names in model-output order. + folds_case : bool or None + Whether the model's tokenizer normalises case away, from + :meth:`~shapash.model.base.SupportsTokenization.folds_case`. Decides the default unit + grouping in :meth:`word_importance` — see :meth:`resolve_lowercase`. ``None`` when the + model exposes no tokenizer to ask. + backend_name : str + ``type(backend).name`` — which explanation method produced this artifact. + is_additive : bool + Whether the contributions sum to a well-defined total — read off the backend + at ``explain()`` time. Licenses feature grouping / waterfall-style charts; + ``False`` means summing the numbers means nothing (e.g. LIME). + reference_kind : {"distribution", "statistics", "point", "none"} + What kind of reference, if any, the contributions are measured against — read + off the backend at ``explain()`` time. See ``NlpBackend.reference_kind``. + """ + + texts: pd.Series + token_strings: list[list[str]] + values: list[np.ndarray] + base_values: np.ndarray | None + y_pred: pd.Series + y_prob: pd.DataFrame | None + y_true: pd.Series | None + label_names: list[str] | None + folds_case: bool | None + backend_name: str + is_additive: bool + reference_kind: Literal["distribution", "statistics", "point", "none"] + + # ClassVar, not a field: it is a shared constant, not per-explanation data, so it stays out + # of ``fields()`` — and therefore out of ``__init__``, ``replace()`` and ``save()``. + _SPECIAL_RE: ClassVar[re.Pattern] = re.compile(r"^\[.*\]$|^##|^\s*$") + + def __post_init__(self) -> None: + """Seal the numeric payload, and check the fields that must agree actually do. + + ``frozen=True`` stops attributes being rebound, but a frozen dataclass still hands out + live numpy arrays: ``explanation.values[0][0, 0] = 999`` would sail through. That is not + hypothetical here — :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain` + memoizes its result and returns a ``dataclasses.replace`` of it, which is a *shallow* + copy, so one in-place edit would silently corrupt the cache and every artifact later + served from it. Clearing the write flag closes that at zero copy cost: the mutation + raises where it happens instead of surfacing as wrong numbers somewhere else. + + To derive a modified artifact, build new arrays and pass them through + ``dataclasses.replace(explanation, values=...)`` rather than writing in place. + + The shared-index check exists because ``replace`` is now the mandated way to vary a frozen + artifact, and ``replace`` is *silent about the fields it does not touch*. Updating + ``texts`` without also updating ``y_pred``/``y_prob``/``y_true`` leaves an artifact whose + halves are indexed differently — which does not raise anywhere: pandas alignment quietly + yields all-NaN, and :meth:`confusion_matrix` zips positionally and reports plausible, + wrong counts. Validating here rather than at each call site protects every ``replace`` + call, including ones not yet written. + """ + for array in self.values: + array.setflags(write=False) + if self.base_values is not None: + self.base_values.setflags(write=False) + self._check_shared_index() + + def _check_shared_index(self) -> None: + """Every index-bearing field must be indexed like :attr:`texts`. + + Raises + ------ + ValueError + If ``y_pred``, ``y_prob`` or ``y_true`` carries a different index from ``texts``. + """ + index = self.texts.index + for name, labelled in (("y_pred", self.y_pred), ("y_prob", self.y_prob), ("y_true", self.y_true)): + if labelled is None or labelled.index.equals(index): + continue + raise ValueError( + f"{name} is indexed differently from texts, so this explanation cannot be aligned " + f"with itself: texts.index starts {list(index[:3])!r} while {name}.index starts " + f"{list(labelled.index[:3])!r} (lengths {len(index)} and {len(labelled.index)}). " + f"Pass {name} aligned to texts.index, or as a plain list to be indexed positionally." + ) + + # Which half of the artifact each field belongs to. Everything outside ``_CALLER_FIELDS`` is a + # pure function of *(texts, model, backend)* — i.e. exactly what a content-addressed cache key + # can cover — which is what makes memoizing on text content alone sound. The two sets are + # checked against ``fields()`` at import time (bottom of this module), so a field added later + # has to be classified rather than silently carried over from a cached run. + _CALLER_FIELDS: ClassVar[frozenset[str]] = frozenset({"texts", "y_pred", "y_prob", "y_true"}) + _COMPUTED_FIELDS: ClassVar[frozenset[str]] = frozenset( + { + "token_strings", + "values", + "base_values", + "label_names", + "folds_case", + "backend_name", + "is_additive", + "reference_kind", + } + ) + + def relabelled(self, texts: pd.Series, y_true: pd.Series | None = None) -> NlpExplanation: + """This artifact's computed payload, carried onto another caller's index and ground truth. + + Contributions are positional, so an artifact computed for one caller is valid for any other + caller passing the same texts in the same order, whatever index they carry. That is what + lets :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain` memoize on text content + alone: on a cache hit it re-labels the stored result through here instead of re-running the + backend, and attaches *this* call's ground truth rather than the one the cached run happened + to be given. + + The field list lives here rather than at that call site because this class owns it — adding + a field means classifying it a few lines above, not remembering to thread it through another + module. + + Parameters + ---------- + texts : pd.Series + The caller's texts. Expected to hold the same values in the same order as + :attr:`texts`; only the index may differ. + y_true : pd.Series or None + Ground truth for this call, aligned to ``texts.index``. + + Returns + ------- + NlpExplanation + A new artifact sharing this one's contribution arrays, which are read-only (see + :meth:`__post_init__`), so the sharing cannot corrupt either side. + + Raises + ------ + ValueError + If :attr:`_CALLER_FIELDS` names a field this method does not produce a value for, or + vice versa — either way the two have drifted apart and the result would be wrong. + """ + caller_values = { + "texts": texts, + "y_pred": self.y_pred.set_axis(texts.index), + "y_prob": None if self.y_prob is None else self.y_prob.set_axis(texts.index), + "y_true": y_true, + } + # ``replace`` is silent about every field it is not handed, so what it is handed is checked + # against the declared partition instead of trusted. The import-time guard below only + # establishes that each field has been *classified*; without this, classifying a new field + # as caller-owned and then forgetting to produce it here would hand every later caller the + # value belonging to whichever call happened to populate the cache — which is precisely the + # failure this method exists to prevent. Together the two checks make the split total. + if caller_values.keys() != self._CALLER_FIELDS: + missing = sorted(self._CALLER_FIELDS - caller_values.keys()) + unexpected = sorted(caller_values.keys() - self._CALLER_FIELDS) + raise ValueError( + f"relabelled() and _CALLER_FIELDS disagree about which fields belong to the caller: " + f"declared but not produced here {missing!r} (they would silently inherit the cached " + f"run's values), produced here but not declared {unexpected!r}." + ) + return replace(self, **caller_values) + + def __len__(self) -> int: + return len(self.token_strings) + + @property + def n_samples(self) -> int: + """Number of samples in the batch.""" + return len(self.texts) + + @property + def n_classes(self) -> int: + """Number of model output columns the contributions carry.""" + return _n_classes(self.values, self.base_values, self.label_names) + + @property + def has_ground_truth(self) -> bool: + """Whether ground-truth labels were supplied to ``explain()``.""" + return self.y_true is not None + + @property + def label_to_idx(self) -> dict[str, int]: + """Class name to output-column index, in :attr:`label_names` order. + + Falls back to stringified column indices when the model exposes no class names, so + callers always get a usable mapping. + """ + names = self.label_names or [str(i) for i in range(self.n_classes)] + return {name: i for i, name in enumerate(names)} + + @property + def plot(self) -> NlpPlotter: + """Rendering helpers over this explanation — ``explanation.plot.waterfall(row=0)``. + + A plain accessor holding nothing but a reference back here: it reads the artifact and + returns figures, and never writes to it, so display choices stay arguments rather than + state. Built fresh on each access, which keeps it out of :meth:`save`'s payload and out + of ``dataclasses.fields``. + + Returns + ------- + NlpPlotter + + Examples + -------- + >>> explanation, _ = NlpExplanation.load("run.zip") # no model, no backend + >>> explanation.plot.waterfall(row=0, label_idx=1).show() + """ + # Imported here, not at module scope: this module is the persistence boundary and stays + # free of plotly/dash so that loading a saved artifact costs no rendering imports. + from shapash.explainer.nlp_plotter import NlpPlotter # noqa: PLC0415 + + return NlpPlotter(self) + + def resolve_lowercase(self, lowercase: bool | None = None) -> bool: + """Decide whether word units should be case-folded, deferring to the model by default. + + One source of truth for the question, so a caller that has to normalise units the same + way (the webapp's exclusion vocabulary) cannot drift from :meth:`word_importance`. + + Parameters + ---------- + lowercase : bool, optional + Explicit caller override. ``None`` (default) derives the answer from ``folds_case``. + + Returns + ------- + bool + ``True`` when ``AWFUL`` and ``awful`` should aggregate into one unit. + + Notes + ----- + With no override, an *uncased* model folds and a *cased* model does not: on an uncased + tokenizer both spellings are the same input ids, so keeping them apart splits one word + into variants the model provably cannot distinguish; on a cased one they are different + inputs whose attributions differ for real reasons, and merging would hide that. + + When ``folds_case`` is ``None`` — no tokenizer to probe, e.g. a bare LIME + ``classifier_fn`` — this falls back to folding, which keeps a corpus-level ranking from + fragmenting on a question that cannot be answered here. + """ + if lowercase is not None: + return lowercase + return True if self.folds_case is None else self.folds_case + + def word_importance( + self, + label_idx: int, + n_top: int = 20, + filter_special: bool = True, + filter_punctuation: bool = True, + lowercase: bool | None = None, + filter_sign: str = "all", + exclude_words: set[str] | None = None, + sample_indices: list[int] | None = None, + ) -> pd.Series: + """Aggregate token contributions by word across all samples for one class. + + For each unique (stripped, by default lowercased) token string that appears in + the batch, computes the mean contribution across all its occurrences. + + Parameters + ---------- + label_idx : int + Index of the class to compute importance for. + n_top : int + Maximum number of words to return, sorted by ``|mean contribution|``. + filter_special : bool + If ``True``, skip empty strings, ``[CLS]``/``[SEP]``-style bracket + tokens, and ``##subword`` wordpiece prefixes. + filter_punctuation : bool + If ``True`` (default), skip units made entirely of punctuation (``.``, ``,``, ``!!!``). + Word segmentation emits punctuation as its own unit so it stays visible and additive in + a *local* explanation; in a corpus-level ranking it is almost always noise, so it is + hidden here unless explicitly asked for. + lowercase : bool, optional + Case-fold each unit before aggregating, so ``AWFUL``, ``Awful`` and ``awful`` + form one row. ``None`` (default) derives this from the model's own tokenizer + via :meth:`resolve_lowercase` — folding on an uncased model, keeping case on a + cased one — so pass a bool only to override that. + Folding also makes the ranking comparable across backends: SHAP labels units by + slicing the *source text* (keeping its casing) while Captum/LIG labels them from + tokenizer output (already normalised), so on an uncased model the two would + otherwise report different vocabularies for one corpus. + The per-instance sentence highlight always keeps the original casing, whatever + this is set to. + filter_sign : {"all", "positive", "negative"} + If ``"positive"``, keep only words with positive mean contribution. + If ``"negative"``, keep only words with negative mean contribution. + exclude_words : set[str], optional + Additional set of word strings to exclude (e.g. user-selected + stopwords or corpus words to hide). Matched against the same + normalised key, so under ``lowercase`` the exclusion is case-insensitive. + sample_indices : list[int], optional + Positional indices of the samples to include in the aggregation. + When ``None`` (default) all samples are used. + + Returns + ------- + pd.Series + Word → mean contribution, sorted by absolute value descending. + """ + + fold = self.resolve_lowercase(lowercase) + + def _key(token: str) -> str: + return token.lower() if fold else token + + _exclude: set[str] = {_key(w) for w in exclude_words} if exclude_words else set() + word_contribs: dict[str, list[float]] = {} + iter_data = ( + ((self.token_strings[i], self.values[i]) for i in sample_indices) + if sample_indices is not None + else zip(self.token_strings, self.values, strict=True) + ) + for tokens, vals in iter_data: + vals_1d: np.ndarray = select_label_column(vals, label_idx) + for tok, val in zip(tokens, vals_1d, strict=True): + tok_clean = tok.strip() + if filter_special and self._SPECIAL_RE.match(tok_clean): + continue + if filter_punctuation and is_punctuation(tok_clean): + continue + key = _key(tok_clean) + if key in _exclude: + continue + if key not in word_contribs: + word_contribs[key] = [] + word_contribs[key].append(float(val)) + + importance = pd.Series({w: float(np.mean(vs)) for w, vs in word_contribs.items()}) + importance = importance.reindex(importance.abs().sort_values(ascending=False).index) + + if filter_sign == "positive": + importance = importance[importance > 0] + elif filter_sign == "negative": + importance = importance[importance < 0] + + return importance.head(n_top) + + def confusion_matrix(self) -> np.ndarray: + """Counts of ground truth against predictions, in :attr:`label_to_idx` order. + + Returns + ------- + numpy.ndarray + Square integer array of shape ``(n_classes, n_classes)`` where ``cm[true, pred]`` + counts the samples whose true class is ``true`` and predicted class is ``pred`` — + the scikit-learn orientation, and the layout + :func:`~shapash.plots.plot_confusion_matrix.plot_confusion_matrix` expects. + + Raises + ------ + ValueError + If no ground truth was supplied to ``explain()``. + + Notes + ----- + Labels are matched by their string form through :attr:`label_to_idx`; a sample whose + true or predicted label is not among the known classes is skipped rather than raising, + so an unexpected label costs one row of the matrix and not the whole plot. + """ + if self.y_true is None: + raise ValueError( + "A confusion matrix needs ground truth, but this explanation has no y_true. " + "Pass y_true to explain() to enable it." + ) + + idx_of = self.label_to_idx + k = len(idx_of) + cm = np.zeros((k, k), dtype=int) + for true_label, pred_label in zip(self.y_true.tolist(), self.y_pred.tolist(), strict=True): + t, p = idx_of.get(str(true_label), -1), idx_of.get(str(pred_label), -1) + if t >= 0 and p >= 0: + cm[t, p] += 1 + return cm + + def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: + """Persist this explanation (no model or backend required to reload). + + Writes a single zip file containing a plain-text ``meta.json`` (readable + without shapash) plus long/tidy parquet tables for contributions, base + values, samples (texts/predictions/ground truth) and, optionally, a 2-D + scatter projection. + + Parameters + ---------- + path : str or Path + Destination file. Any extension is accepted; ``.shxpl`` is a reasonable + convention but not enforced. + scatter_xy : np.ndarray, optional + Pre-computed 2-D projection to bundle alongside the explanation, shape + ``(n_samples, 2)`` (the same array :meth:`~NlpExplainer.compute_projection` + returns). + """ + contrib_df, base_df, values_ndim = _contributions_to_frames(self.token_strings, self.values, self.base_values) + samples_df = _samples_to_frame(self.texts, self.y_pred, self.y_prob, self.y_true) + + meta = { + "format_version": _FORMAT_VERSION, + "shapash_version": _shapash_version, + "created_at": datetime.now(UTC).isoformat(), + "backend_name": self.backend_name, + "is_additive": self.is_additive, + "reference_kind": self.reference_kind, + "label_names": self.label_names, + "folds_case": self.folds_case, + # Authoritative counts, not re-derived from the tidy tables on load: a sample with + # zero tokens has no row in contrib_df at all, so inferring n_samples from the table + # would silently drop it (and n_classes has nothing to read from an all-empty batch). + "n_samples": len(self.texts), + "n_classes": _n_classes(self.values, self.base_values, self.label_names), + "values_ndim": values_ndim, + "has_base_values": base_df is not None, + "has_ground_truth": self.y_true is not None, + "has_scatter": scatter_xy is not None, + } + + with zipfile.ZipFile(Path(path), "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("meta.json", json.dumps(meta, indent=2, ensure_ascii=False)) + _write_parquet(zf, "contributions.parquet", contrib_df) + if base_df is not None: + _write_parquet(zf, "base_values.parquet", base_df) + _write_parquet(zf, "samples.parquet", samples_df) + if scatter_xy is not None: + scatter_arr = np.asarray(scatter_xy) + scatter_df = pd.DataFrame( + {"sample_idx": np.arange(len(scatter_arr)), "x": scatter_arr[:, 0], "y": scatter_arr[:, 1]} + ) + _write_parquet(zf, "scatter.parquet", scatter_df) + + @classmethod + def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: + """Restore an explanation saved by :meth:`save`. + + Parameters + ---------- + path : str or Path + File written by :meth:`save`. + + Returns + ------- + explanation : NlpExplanation + The restored explanation. + scatter_xy : np.ndarray or None + The projection array bundled at save time, or ``None``. + + Raises + ------ + ValueError + If the file's ``format_version`` is not one this shapash version reads. + """ + with zipfile.ZipFile(Path(path), "r") as zf: + meta = json.loads(zf.read("meta.json")) + version = meta.get("format_version") + if version not in _SUPPORTED_FORMAT_VERSIONS: + raise ValueError( + f"Unsupported NlpExplanation format_version={version!r}; this shapash version " + f"({_shapash_version}) reads version(s) {_SUPPORTED_FORMAT_VERSIONS}. " + "Re-save this explanation with a compatible shapash version." + ) + contrib_df = _read_parquet(zf, "contributions.parquet") + base_df = _read_parquet(zf, "base_values.parquet") if meta.get("has_base_values", True) else None + samples_df = _read_parquet(zf, "samples.parquet") + scatter_xy = None + if meta.get("has_scatter"): + scatter_df = _read_parquet(zf, "scatter.parquet") + scatter_xy = scatter_df[["x", "y"]].to_numpy() + + token_strings, values, base_values = _frames_to_contributions( + contrib_df, base_df, meta["values_ndim"], meta["n_samples"], meta["n_classes"] + ) + texts, y_pred, y_true, y_prob = _frame_to_samples(samples_df) + + explanation = cls( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=y_pred, + y_prob=y_prob, + y_true=y_true, + label_names=meta.get("label_names"), + folds_case=meta.get("folds_case"), + backend_name=meta["backend_name"], + is_additive=meta["is_additive"], + reference_kind=meta["reference_kind"], + ) + return explanation, scatter_xy + + +def _write_parquet(zf: zipfile.ZipFile, name: str, df: pd.DataFrame) -> None: + buf = io.BytesIO() + df.to_parquet(buf, engine="pyarrow", index=False) + zf.writestr(name, buf.getvalue()) + + +def _read_parquet(zf: zipfile.ZipFile, name: str) -> pd.DataFrame: + return pd.read_parquet(io.BytesIO(zf.read(name)), engine="pyarrow") + + +def _contributions_to_frames( + token_strings: list[list[str]], values: list[np.ndarray], base_values: np.ndarray | None +) -> tuple[pd.DataFrame, pd.DataFrame | None, int]: + """Ragged per-sample arrays -> long/tidy ``(contributions_df, base_values_df, values_ndim)``. + + ``values_ndim`` records whether the original per-sample arrays were 1-D + (binary/regression) or 2-D (multi-class), so :func:`_frames_to_contributions` can + restore the exact original shape rather than always returning 2-D arrays. + """ + values_ndim = 2 if values and values[0].ndim == 2 else 1 + + contrib_rows: list[tuple[int, int, str, int, float]] = [] + for sample_idx, (tokens, vals) in enumerate(zip(token_strings, values, strict=True)): + vals_2d = vals if vals.ndim == 2 else vals[:, None] + for token_idx, token in enumerate(tokens): + for class_idx in range(vals_2d.shape[1]): + contrib_rows.append((sample_idx, token_idx, token, class_idx, float(vals_2d[token_idx, class_idx]))) + contrib_df = pd.DataFrame(contrib_rows, columns=["sample_idx", "token_idx", "token", "class_idx", "value"]) + + if base_values is None: + return contrib_df, None, values_ndim + + base_2d = base_values if base_values.ndim == 2 else base_values[:, None] + base_rows = [ + (sample_idx, class_idx, float(base_2d[sample_idx, class_idx])) + for sample_idx in range(base_2d.shape[0]) + for class_idx in range(base_2d.shape[1]) + ] + base_df = pd.DataFrame(base_rows, columns=["sample_idx", "class_idx", "base_value"]) + return contrib_df, base_df, values_ndim + + +def _n_classes(values: list[np.ndarray], base_values: np.ndarray | None, label_names: list[str] | None) -> int: + """Number of classes for a batch, robust to an all-empty-tokens / no-baseline batch. + + Needed as ``meta.json`` state rather than re-derived from the tidy tables on load: a + sample with zero tokens leaves no row in ``contributions.parquet``, so if *every* + sample in the batch happened to be empty *and* there is no baseline either, the + tables alone carry no evidence of ``n_classes`` at all. + """ + if base_values is not None: + return base_values.shape[1] if base_values.ndim == 2 else 1 + for vals in values: + if vals.shape[0] > 0: + return vals.shape[1] if vals.ndim == 2 else 1 + return len(label_names) if label_names else 1 + + +def _frames_to_contributions( + contrib_df: pd.DataFrame, + base_df: pd.DataFrame | None, + values_ndim: int, + n_samples: int, + n_classes: int, +) -> tuple[list[list[str]], list[np.ndarray], np.ndarray | None]: + """Inverse of :func:`_contributions_to_frames`. + + ``n_samples``/``n_classes`` come from ``meta.json`` (see :func:`_n_classes`) rather + than being re-derived from the tables, which cannot see a trailing all-empty sample. + """ + groups = dict(tuple(contrib_df.groupby("sample_idx"))) if len(contrib_df) else {} + token_strings: list[list[str]] = [] + values: list[np.ndarray] = [] + for sample_idx in range(n_samples): + group = groups.get(sample_idx) + if group is None or group.empty: + token_strings.append([]) + values.append(np.zeros((0, n_classes) if values_ndim == 2 else (0,), dtype=float)) + continue + tokens = group.drop_duplicates("token_idx").sort_values("token_idx")["token"].tolist() + arr = np.zeros((len(tokens), n_classes), dtype=float) + arr[group["token_idx"].to_numpy(), group["class_idx"].to_numpy()] = group["value"].to_numpy() + token_strings.append(tokens) + values.append(arr if values_ndim == 2 else arr[:, 0]) + + if base_df is None: + return token_strings, values, None + + base_values = np.zeros((n_samples, n_classes), dtype=float) + if len(base_df): + base_values[base_df["sample_idx"].to_numpy(), base_df["class_idx"].to_numpy()] = base_df[ + "base_value" + ].to_numpy() + if values_ndim == 1: + base_values = base_values[:, 0] + return token_strings, values, base_values + + +def _samples_to_frame( + texts: pd.Series, y_pred: pd.Series, y_prob: pd.DataFrame | None, y_true: pd.Series | None +) -> pd.DataFrame: + df = pd.DataFrame( + { + "sample_idx": np.arange(len(texts)), + "orig_index": list(texts.index), + "text": texts.to_numpy(), + "y_pred": y_pred.to_numpy(), + } + ) + if y_true is not None: + df["y_true"] = y_true.to_numpy() + if y_prob is not None: + for col in y_prob.columns: + df[f"{_PROB_PREFIX}{col}"] = y_prob[col].to_numpy() + return df + + +def _frame_to_samples( + df: pd.DataFrame, +) -> tuple[pd.Series, pd.Series, pd.Series | None, pd.DataFrame | None]: + index = pd.Index(df["orig_index"].tolist()) + texts = pd.Series(df["text"].tolist(), index=index, name="text") + y_pred = pd.Series(df["y_pred"].tolist(), index=index, name="prediction") + y_true = pd.Series(df["y_true"].tolist(), index=index, name="ground_truth") if "y_true" in df.columns else None + + prob_cols = [c for c in df.columns if c.startswith(_PROB_PREFIX)] + y_prob = None + if prob_cols: + y_prob = pd.DataFrame( + {c[len(_PROB_PREFIX) :]: df[c].tolist() for c in prob_cols}, + index=index, + ) + return texts, y_pred, y_true, y_prob + + +# Checked once, at import, rather than on every ``relabelled`` call: an unclassified field is a +# coding error, not a runtime condition, and this way it surfaces as an ImportError on ``import +# shapash`` instead of waiting for a cache hit to expose it. This is what makes ``relabelled``'s +# ``replace`` safe — ``replace`` is silent about fields it is not handed, so the guarantee that +# every field has been *considered* has to come from somewhere, and it comes from here. +_unclassified = {f.name for f in fields(NlpExplanation)} - ( + NlpExplanation._CALLER_FIELDS | NlpExplanation._COMPUTED_FIELDS +) +if _unclassified: + raise ValueError( + f"NlpExplanation field(s) {sorted(_unclassified)!r} are classified as neither caller-owned " + f"nor computed. Add each one to NlpExplanation._CALLER_FIELDS if its value comes from the " + f"caller of explain(), or to _COMPUTED_FIELDS if it is determined by (texts, model, " + f"backend) and may therefore be served from cache." + ) diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py new file mode 100644 index 00000000..a60eae14 --- /dev/null +++ b/shapash/explainer/nlp_plotter.py @@ -0,0 +1,289 @@ +"""``NlpPlotter`` — rendering helpers bound to one :class:`NlpExplanation`. + +Reached as ``explanation.plot``. It holds a reference to the artifact and nothing else: every +call slices that artifact, hands the slice to a pure function in :mod:`shapash.plots`, and +returns the figure. No display state is stored anywhere — pass different arguments and you get +a different figure from the *same* untouched explanation. + +Why a separate object rather than methods on :class:`NlpExplanation` itself: the explanation is +the persistence boundary (``save``/``load`` with a ``format_version``), so keeping rendering off +it keeps the dataclass a dataclass. It also mirrors the tabular side, where ``SmartExplainer`` +exposes :class:`~shapash.explainer.smart_plotter.SmartPlotter` as ``.plot``. + +The webapp deliberately does *not* go through here: its panels render ``dcc.Store`` datapoints +via :func:`~shapash.webapp.nlp_components.datapoint.unpack_datapoint`, because a what-if +datapoint has no row in the artifact at all. This accessor serves notebook use, including a +snapshot reloaded with :meth:`NlpExplanation.load`, which has no model and no backend. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np +from dash import html +from plotly import graph_objs as go + +from shapash.explainer.nlp_explanation import select_label_column +from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.plots.plot_sentence_highlight import plot_sentence_highlight +from shapash.plots.plot_token_highlight import plot_token_highlight +from shapash.plots.plot_waterfall import plot_waterfall +from shapash.plots.plot_word_importance import plot_word_importance + +if TYPE_CHECKING: + from shapash.explainer.nlp_explanation import NlpExplanation + + +class NlpPlotter: + """Plots over one :class:`~shapash.explainer.nlp_explanation.NlpExplanation`. + + Not instantiated directly — reach it as ``explanation.plot``. + + Parameters + ---------- + explanation : NlpExplanation + The artifact to render. Held by reference and never written to. + + Examples + -------- + >>> explanation, _ = NlpExplanation.load("run.zip") # no model needed + >>> explanation.plot.waterfall(row=0, label_idx=1).show() + """ + + def __init__(self, explanation: NlpExplanation) -> None: + self._exp = explanation + + def __repr__(self) -> str: + exp = self._exp + return f"<NlpPlotter over {len(exp)} sample(s), backend={exp.backend_name!r}>" + + # ── internals ─────────────────────────────────────────────────────────────────────── + def _check_label_idx(self, label_idx: int) -> int: + """Fail with the class list rather than a bare numpy ``IndexError``.""" + n_classes = self._exp.n_classes + if not -n_classes <= label_idx < n_classes: + names = self._exp.label_names + known = f" Classes: {names}." if names else "" + raise IndexError(f"label_idx={label_idx} is out of range for {n_classes} output column(s).{known}") + return label_idx % n_classes + + def _label_name(self, label_idx: int) -> str | None: + names = self._exp.label_names + return names[label_idx] if names is not None and label_idx < len(names) else None + + def _slice(self, row: int, label_idx: int) -> tuple[list[str], np.ndarray, float | None]: + """One sample, one class — the shape every per-instance plot function takes. + + Returns ``(tokens, 1-D values, base_value)``. ``base_value`` is ``None`` when the + backend had no reference at all (``reference_kind == "none"``). + """ + exp = self._exp + if not -len(exp) <= row < len(exp): + raise IndexError(f"row={row} is out of range for {len(exp)} sample(s).") + + tokens = exp.token_strings[row] + values = select_label_column(exp.values[row], label_idx) + + base = exp.base_values + base_value: float | None = None + if base is not None: + base_value = float(base[row, label_idx]) if base.ndim == 2 else float(base[row]) + + return list(tokens), np.asarray(values, dtype=float), base_value + + # ── per-instance plots ────────────────────────────────────────────────────────────── + def tokens( + self, + row: int = 0, + label_idx: int = 0, + max_tokens: int | None = None, + title: str | None = None, + width: int = 900, + height: int | None = None, + ) -> go.Figure: + """Bar chart of one sample's token contributions for one class. + + Parameters + ---------- + row : int + Positional index of the sample (``0`` is the first row, negatives count from the + end). This is a position, not a label from ``texts.index``. + label_idx : int + Index of the class to display, in ``label_names`` order. + max_tokens : int, optional + Keep only the ``max_tokens`` highest-magnitude tokens, in sentence order. + title : str, optional + Overrides the default, which names the class when the model exposes class names. + width, height : int, optional + Figure size in pixels. + + Returns + ------- + plotly.graph_objs.Figure + """ + label_idx = self._check_label_idx(label_idx) + toks, values, _ = self._slice(row, label_idx) + name = self._label_name(label_idx) + if title is None: + title = f"Token contributions — {name}" if name else "Token contributions" + return plot_token_highlight( + tokens=toks, values=values, title=title, max_tokens=max_tokens, width=width, height=height + ) + + def waterfall( + self, + row: int = 0, + label_idx: int = 0, + min_pct: float = 0.10, + filter_special: bool = True, + title: str | None = None, + width: int | None = None, + ) -> go.Figure: + """Waterfall decomposing one prediction from the baseline into token contributions. + + Parameters + ---------- + row : int + Positional index of the sample (see :meth:`tokens`). + label_idx : int + Index of the class to display, in ``label_names`` order. + min_pct : float + Fraction of the largest absolute contribution below which tokens collapse into a + single "other" bar. Range [0, 1]. + filter_special : bool + Drop ``[CLS]``/``[SEP]``-style and ``##subword`` tokens. + title : str, optional + Overrides the default, which names the class when the model exposes class names. + width : int, optional + Figure width in pixels. + + Returns + ------- + plotly.graph_objs.Figure + + Raises + ------ + ValueError + If the producing backend is not additive. A waterfall's running total only means + something when the contributions sum to the prediction, which is exactly what + :attr:`~shapash.explainer.nlp_explanation.NlpExplanation.is_additive` records — so + this refuses rather than drawing a chart whose arithmetic is meaningless. + """ + exp = self._exp + if not exp.is_additive: + raise ValueError( + f"A waterfall decomposes a prediction into contributions that sum to it, but the " + f"{exp.backend_name!r} backend is not additive, so the running total would be " + f"meaningless. Use .plot.tokens() or .plot.sentence() instead." + ) + label_idx = self._check_label_idx(label_idx) + toks, values, base_value = self._slice(row, label_idx) + name = self._label_name(label_idx) + if title is None: + title = f"Token contributions — {name}" if name else "Token contributions" + return plot_waterfall( + tokens=toks, + values=values, + base_value=base_value, + min_pct=min_pct, + filter_special=filter_special, + title=title, + width=width, + ) + + def sentence(self, row: int = 0, label_idx: int = 0) -> html.Div: + """One sample rendered inline, each token background-shaded by its contribution. + + Parameters + ---------- + row : int + Positional index of the sample (see :meth:`tokens`). + label_idx : int + Index of the class to display, in ``label_names`` order. + + Returns + ------- + dash.html.Div + A Dash component. In a notebook, display it inside a + ``jupyter_dash``/``dash`` app, or use :meth:`tokens` for a standalone figure. + """ + label_idx = self._check_label_idx(label_idx) + toks, values, base_value = self._slice(row, label_idx) + return plot_sentence_highlight(tokens=toks, values=values, base_value=base_value) + + # ── batch-level plots ─────────────────────────────────────────────────────────────── + def words( + self, + label_idx: int = 0, + n_top: int = 20, + title: str | None = None, + width: int = 900, + height: int | None = None, + **kwargs: Any, + ) -> go.Figure: + """Corpus-level word importance for one class. + + Parameters + ---------- + label_idx : int + Index of the class to aggregate, in ``label_names`` order. + n_top : int + Number of words to show, ranked by ``|mean contribution|``. + title : str, optional + Overrides the default, which names the class when the model exposes class names. + width, height : int, optional + Figure size in pixels. + **kwargs + Forwarded to + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_importance` — + ``filter_special``, ``filter_punctuation``, ``lowercase``, ``filter_sign``, + ``exclude_words``, ``sample_indices``. + + Returns + ------- + plotly.graph_objs.Figure + """ + label_idx = self._check_label_idx(label_idx) + word_imp = self._exp.word_importance(label_idx=label_idx, n_top=n_top, **kwargs) + name = self._label_name(label_idx) + if title is None: + title = f"Word importance — {name}" if name else "Word importance" + return plot_word_importance(word_imp, title=title, width=width, height=height) + + def confusion( + self, + normalize: str | None = None, + title: str = "Confusion matrix", + width: int | None = None, + height: int | None = None, + ) -> go.Figure: + """Confusion matrix of predictions against ground truth. + + Parameters + ---------- + normalize : {None, "true"}, optional + ``"true"`` divides each row by its sum so cells show recall. ``None`` shows counts. + title : str + Figure title. + width, height : int, optional + Figure size in pixels. + + Returns + ------- + plotly.graph_objs.Figure + + Raises + ------ + ValueError + If no ground truth was supplied to ``explain()``. + """ + exp = self._exp + if not exp.has_ground_truth: + raise ValueError( + "A confusion matrix needs ground truth, but this explanation has no y_true. " + "Pass y_true to explain() to enable it." + ) + cm = exp.confusion_matrix() + labels = list(exp.label_to_idx) + return plot_confusion_matrix(cm, labels, normalize=normalize, title=title, width=width, height=height) diff --git a/shapash/plots/plot_word_importance.py b/shapash/plots/plot_word_importance.py index 54b953e7..f855da65 100644 --- a/shapash/plots/plot_word_importance.py +++ b/shapash/plots/plot_word_importance.py @@ -24,7 +24,7 @@ def plot_word_importance( ---------- word_imp : pd.Series Word → mean SHAP contribution, already sorted by ``|value|`` descending - (as returned by ``NlpContributions.word_importance``). + (as returned by ``NlpExplanation.word_importance``). title : str Figure title. width : int @@ -38,7 +38,7 @@ def plot_word_importance( Examples -------- - >>> imp = contributions.word_importance(label_idx=1) + >>> imp = explanation.word_importance(label_idx=1) >>> fig = plot_word_importance(imp, title="Word importance — joy") >>> fig.show() """ diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 357ee7be..e352aa20 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -2,9 +2,9 @@ Prototype bridge toward Phase 5b (composable WebappComponents). Panels are being extracted into ``WebappComponent``s one at a time (see ``shapash/webapp/nlp_components/``); global word importance, -the dataset table, scatter, and error analysis are still built inline here pending their own -extraction. All tabular-only SmartApp panels (violin, cluster, scatter prediction picking) beyond -what NLP needs are absent rather than disabled. +the dataset table, and scatter are still built inline here pending their own extraction. All +tabular-only SmartApp panels (violin, cluster, scatter prediction picking) beyond what NLP needs are +absent rather than disabled. """ from __future__ import annotations @@ -21,18 +21,19 @@ from dash.exceptions import PreventUpdate from shapash.backend.nlp_backend import is_punctuation -from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.explainer.interactive import InteractiveEngine +from shapash.explainer.nlp_explanation import NlpExplanation, select_label_column from shapash.plots.plot_word_importance import plot_word_importance from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, + ErrorAnalysisComponent, LabelNoiseComponent, SentenceHighlightComponent, SimilarExamplesComponent, WaterfallComponent, pack_datapoint, ) -from shapash.webapp.nlp_view import NlpView _APPLY_STORE = "whatif-apply-store" _CURRENT_STORE = "current-datapoint" @@ -77,37 +78,6 @@ def _compose_selection( return combined -def _cell_from_click(click_data: dict | None, name_to_idx: dict[str, int]) -> tuple[int, int] | None: - """Resolve a confusion-matrix cell click to ``(pred_idx, true_idx)``. - - Heatmap ``clickData`` does not reliably include ``customdata`` the way scatter traces do, so - prefer it when present but fall back to mapping the predicted (``x``) and true (``y``) axis label - names back to their class indices. Returns ``None`` for an empty or unrecognised click. - """ - if not click_data or not click_data.get("points"): - return None - point = click_data["points"][0] - custom = point.get("customdata") - if custom is not None and len(custom) == 2: - return int(custom[0]), int(custom[1]) - if point.get("x") in name_to_idx and point.get("y") in name_to_idx: - return name_to_idx[point["x"]], name_to_idx[point["y"]] - return None - - -def _empty_word_fig(message: str) -> go.Figure: - """Placeholder figure with a centred hint, for the per-cell word charts before a cell is picked.""" - fig = go.Figure() - fig.add_annotation(text=message, showarrow=False, font=dict(color="#888888"), xref="paper", yref="paper") - fig.update_layout( - xaxis=dict(visible=False), - yaxis=dict(visible=False), - plot_bgcolor="white", - margin=dict(l=20, r=20, t=20, b=20), - ) - return fig - - class NlpWebApp: """Minimal Dash webapp driven by an ``NlpExplainer``. @@ -134,8 +104,15 @@ class NlpWebApp: Parameters ---------- - explainer : NlpExplainer - A compiled ``NlpExplainer`` instance (``compile()`` must have been called). + explanation : NlpExplanation + The result of ``NlpExplainer.explain()`` (or a loaded one — see + ``NlpExplanation.load()``) to serve. + engine : InteractiveEngine, optional + Live engine for what-if actions (re-predicting edited text, generating + counterfactuals, similar-example retrieval, label-noise probing) — normally the + ``NlpExplainer`` that produced ``explanation``. ``None`` for a snapshot loaded via + ``NlpExplanation.load()``, which carries no model: components self-disable via + their ``requires`` when the engine is absent or lacks a capability. scatter_xy : np.ndarray, optional Pre-computed 2-D projection, shape ``(n_samples, 2)``. When provided, a scatter panel is added to the layout. Compute with PaCMAP, UMAP, @@ -143,22 +120,27 @@ class NlpWebApp: the projection itself to avoid heavy optional dependencies. """ - def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: - if explainer.contributions is None: - raise RuntimeError("NlpExplainer.compile() must be called before launching the webapp.") + def __init__( + self, + explanation: NlpExplanation, + engine: InteractiveEngine | None = None, + scatter_xy: np.ndarray | None = None, + ) -> None: if scatter_xy is not None: scatter_xy = np.asarray(scatter_xy) - n = len(explainer.texts) + n = len(explanation.texts) if scatter_xy.shape != (n, 2): raise ValueError(f"scatter_xy must have shape ({n}, 2), got {scatter_xy.shape}") self._scatter_xy: np.ndarray | None = scatter_xy - # What-if Lab: read-only view + live engine (the explainer itself when it is live). - # Components self-disable via their `requires` when the engine lacks a capability - # (e.g. an explainer restored from a snapshot holds no model). `self._components` itself is - # assembled later in `_build_layout` (it needs `_full_table_records`, not ready yet here). - self._view = NlpView(explainer) - self._engine = explainer + # What-if Lab: the immutable artifact + a separate, possibly-absent live engine. The + # artifact is only ever read here — display state lives in the `dcc.Store`s declared in + # `_build_layout`, never on the explanation. + # Components self-disable via their `requires` when the engine is None or lacks a capability + # (e.g. a loaded snapshot has no model at all). `self._components` itself is assembled later + # in `_build_layout` (it needs `_full_table_records`, not ready yet here). + self._explanation = explanation + self._engine = engine self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) self.app.title = "Shapash — NLP Explainer" @@ -190,28 +172,28 @@ def __init__(self, explainer, scatter_xy: np.ndarray | None = None) -> None: # ------------------------------------------------------------------ def _build_layout(self) -> None: - contrib = self._view.contributions - label_names = contrib.label_names or [str(i) for i in range(self._view.n_classes)] + explanation = self._explanation + label_names = explanation.label_names or [str(i) for i in range(self._explanation.n_classes)] # Predicted-label name → class index, shared by the confusion matrix and (via - # NlpView.label_to_idx) by SentenceHighlightComponent's predicted-class sync callback. - self._label_to_idx = self._view.label_to_idx - n = self._view.n_samples + # NlpExplanation.label_to_idx) by SentenceHighlightComponent's predicted-class sync callback. + self._label_to_idx = self._explanation.label_to_idx + n = self._explanation.n_samples # ── Table records — always include _orig_idx for scatter filtering ── - texts = self._view.texts + texts = self._explanation.texts assert texts is not None # noqa: S101 - the webapp requires an already-compiled explainer records: dict[str, list] = { "_orig_idx": list(range(n)), "text": texts.tolist(), - "prediction": (self._view.y_pred.tolist() if self._view.y_pred is not None else [""] * n), + "prediction": (self._explanation.y_pred.tolist() if self._explanation.y_pred is not None else [""] * n), } - if self._view.y_true is not None: - records["ground_truth"] = self._view.y_true.tolist() + if self._explanation.y_true is not None: + records["ground_truth"] = self._explanation.y_true.tolist() # Single "Probability" column — confidence of the predicted class. # max(axis=1) works for both binary and multiclass since the predicted # label is always the argmax, regardless of how columns are named. - y_prob: pd.DataFrame | None = self._view.y_prob + y_prob: pd.DataFrame | None = self._explanation.y_prob if y_prob is not None: records["probability"] = y_prob.max(axis=1).tolist() @@ -247,11 +229,11 @@ def _build_layout(self) -> None: # entry here always matches a bar in the chart. On an uncased model that folds # AWFUL/Awful/awful into one entry; on a cased model it leaves all three, because there # they are genuinely different inputs. - _fold_words = contrib.resolve_lowercase() + _fold_words = explanation.resolve_lowercase() all_words = sorted( { tok.strip().lower() if _fold_words else tok.strip() - for sample_tokens in contrib.token_strings + for sample_tokens in explanation.token_strings for tok in sample_tokens if tok.strip() and not _SPECIAL_RE.match(tok.strip()) and not is_punctuation(tok) } @@ -262,7 +244,7 @@ def _build_layout(self) -> None: scatter_col_content = None if self._scatter_xy is not None: color_options: list[dcc.Dropdown.Options] = [{"label": "Prediction", "value": "prediction"}] - if self._view.y_true is not None: + if self._explanation.y_true is not None: color_options.append({"label": "Ground Truth", "value": "ground_truth"}) color_options.append({"label": "Word contribution", "value": "word_contribution"}) @@ -322,111 +304,6 @@ def _build_layout(self) -> None: style={"display": "flex", "flexDirection": "column", "height": "100%"}, ) - # ── Error Analysis panel (confusion matrix + per-cell word importance) ── - # Only meaningful with ground truth. The matrix is a global selection driver like the - # scatter: clicking a cell writes ``error-cell`` (see callbacks), which composes with any - # scatter selection to filter the table and the main Word Importance tab, and drives the two - # per-cell word charts below (words toward the predicted vs. the true class). - error_analysis_body = None - if has_gt: - normalize_options: list[dcc.RadioItems.Options] = [ - {"label": " Counts", "value": "count"}, - {"label": " Recall", "value": "recall"}, - ] - idx_of = self._label_to_idx - y_true, y_pred = self._view.y_true, self._view.y_pred - # has_gt confirms y_true was compiled; y_pred always accompanies it. - assert y_true is not None and y_pred is not None # noqa: S101 - true_arr = np.array([idx_of.get(str(v), -1) for v in y_true.tolist()]) - pred_arr = np.array([idx_of.get(str(v), -1) for v in y_pred.tolist()]) - k = len(label_names) - cm = np.zeros((k, k), dtype=int) - for t, p in zip(true_arr, pred_arr, strict=True): - if t >= 0 and p >= 0: - cm[t, p] += 1 - self._cm = cm - self._cm_true_idx = true_arr - self._cm_pred_idx = pred_arr - - error_analysis_body = html.Div( - [ - dbc.Row( - [ - dbc.Col( - html.H6( - "Confusion Matrix — click a cell to inspect that error group", - className="fw-bold mb-0", - ), - width="auto", - className="align-self-center", - ), - dbc.Col( - dcc.RadioItems( - id="cm-normalize", - options=normalize_options, - value="count", - inline=True, - inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "12px"}, - ), - width="auto", - className="align-self-center", - ), - dbc.Col( - dbc.Button( - "× clear cell", - id="error-cell-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0", - style={"fontSize": "0.8em"}, - ), - width="auto", - className="align-self-center ms-auto", - ), - ], - className="align-items-center mb-2", - style={"flex": "0 0 auto"}, - ), - dcc.Graph( - id="confusion-matrix-graph", - figure=plot_confusion_matrix(cm, label_names, title="", width=None, height=None), - config={"displayModeBar": False, "responsive": True}, - style={"height": "360px", "flex": "0 0 auto"}, - ), - html.Small( - id="error-cell-caption", - children="Click a cell to see the words behind those errors.", - className="text-muted d-block my-2", - style={"flex": "0 0 auto"}, - ), - dbc.Row( - [ - dbc.Col( - dcc.Graph( - id="error-pred-importance", - config={"displayModeBar": False, "responsive": True}, - style={"height": "320px"}, - ), - width=6, - ), - dbc.Col( - dcc.Graph( - id="error-true-importance", - config={"displayModeBar": False, "responsive": True}, - style={"height": "320px"}, - ), - width=6, - ), - ], - className="g-2", - style={"flex": "0 0 auto"}, - ), - ], - style={"height": "100%", "display": "flex", "flexDirection": "column", "overflowY": "auto"}, - ) - # ── Global Word Importance panel — controls live inside it ── # (Top-K / Sign / Exclude / Class only affect this panel, so they are co-located here — # independent from the local class picker in the Sentence Highlight panel below.) @@ -583,6 +460,18 @@ def _build_layout(self) -> None: style={"display": "none", "fontSize": "0.8em"}, ) ) + if has_gt: + selection_children.append( + dbc.Button( + "× clear cell", + id="error-cell-clear-btn", + n_clicks=0, + color="link", + size="sm", + className="text-muted p-0 me-3", + style={"display": "none", "fontSize": "0.8em"}, + ) + ) selection_children.append( dbc.Button( "× clear word filter", @@ -636,8 +525,9 @@ def _build_layout(self) -> None: CounterfactualComponent(), SimilarExamplesComponent(), LabelNoiseComponent(), + ErrorAnalysisComponent(), ) - if type(comp).is_available(self._view, self._engine) + if type(comp).is_available(self._explanation, self._engine) ] highlight_comp = next(c for c in self._components if isinstance(c, SentenceHighlightComponent)) waterfall_comp = next(c for c in self._components if isinstance(c, WaterfallComponent)) @@ -645,30 +535,37 @@ def _build_layout(self) -> None: cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) similar_comp = next((c for c in self._components if isinstance(c, SimilarExamplesComponent)), None) noise_comp = next((c for c in self._components if isinstance(c, LabelNoiseComponent)), None) + error_analysis_comp = next((c for c in self._components if isinstance(c, ErrorAnalysisComponent)), None) left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_col_content is not None: left_tabs.append(("scatter", "Embeddings", scatter_col_content)) if editor_comp is not None: - left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._view, self._engine))) + left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._explanation, self._engine))) # Error Analysis sits beside Word Importance: it *is* an aggregated word-importance view # (per confusion-matrix cell), so it belongs with the other global "why" panels on the right. upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] - if error_analysis_body is not None: - upper_right_tabs.append(("errors", "Error Analysis", error_analysis_body)) + if error_analysis_comp is not None: + upper_right_tabs.append( + ("errors", "Error Analysis", error_analysis_comp.layout(self._explanation, self._engine)) + ) # Next to Error Analysis by design: same ground-truth prerequisite, and it answers the # question that panel leaves open — whether the *label*, not the model, is what is wrong. if noise_comp is not None: - upper_right_tabs.append(("label-noise", "Label Noise", noise_comp.layout(self._view, self._engine))) + upper_right_tabs.append(("label-noise", "Label Noise", noise_comp.layout(self._explanation, self._engine))) if cf_comp is not None: - upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._view, self._engine))) + upper_right_tabs.append( + ("counterfactual", "Counterfactuals", cf_comp.layout(self._explanation, self._engine)) + ) if similar_comp is not None: - upper_right_tabs.append(("similar", "Similar Examples", similar_comp.layout(self._view, self._engine))) + upper_right_tabs.append( + ("similar", "Similar Examples", similar_comp.layout(self._explanation, self._engine)) + ) lower_right_tabs: list = [ - ("highlight", "Sentence", highlight_comp.layout(self._view, self._engine)), - ("waterfall", "Waterfall", waterfall_comp.layout(self._view, self._engine)), + ("highlight", "Sentence", highlight_comp.layout(self._explanation, self._engine)), + ("waterfall", "Waterfall", waterfall_comp.layout(self._explanation, self._engine)), ] left_column = html.Div( @@ -774,7 +671,7 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: # ------------------------------------------------------------------ def _register_callbacks(self) -> None: - contrib = self._view.contributions + explanation = self._explanation full_records = self._full_table_records has_gt = self._has_gt @@ -818,7 +715,7 @@ def update_global_importance( if label_idx is None: raise PreventUpdate effective_indices = _compose_indices(selected_indices, error_cell, bool(errors_only)) - word_imp = contrib.word_importance( + word_imp = explanation.word_importance( label_idx=int(label_idx), n_top=int(topk or 20), filter_sign=sign_filter or "all", @@ -851,13 +748,13 @@ def set_current_from_row(selected_rows): if not selected_rows: raise PreventUpdate pos = int(selected_rows[0]["_orig_idx"]) - base_values = contrib.base_values + base_values = explanation.base_values base = base_values[pos] if base_values is not None else None return pack_datapoint( text=selected_rows[0].get("text", ""), orig_idx=pos, - tokens=contrib.token_strings[pos], - values=contrib.values[pos], + tokens=explanation.token_strings[pos], + values=explanation.values[pos], base_values=base, label=selected_rows[0].get("prediction"), ) @@ -936,7 +833,7 @@ def filter_table(selected_indices, word_filter, errors_only, error_cell): total = len(full_records) parts = [] if error_cell: - names = contrib.label_names or [] + names = explanation.label_names or [] pred_name = names[error_cell["pred"]] if error_cell["pred"] < len(names) else str(error_cell["pred"]) true_name = names[error_cell["true"]] if error_cell["true"] < len(names) else str(error_cell["true"]) parts.append(f"predicted {pred_name} · true {true_name}") @@ -1047,84 +944,6 @@ def toggle_clear_button(selected_indices): hidden = {"display": "none", "fontSize": "0.8em"} return visible if selected_indices else hidden - # ── Error Analysis callbacks (registered only with ground truth) ── - if has_gt: - names = contrib.label_names or [str(i) for i in range(self._cm.shape[0])] - name_to_idx = {name: i for i, name in enumerate(names)} - cm = self._cm - pred_idx_arr = self._cm_pred_idx - true_idx_arr = self._cm_true_idx - - @self.app.callback( - Output("confusion-matrix-graph", "figure"), - Input("cm-normalize", "value"), - ) - def update_confusion_matrix(normalize): - fig = plot_confusion_matrix( - cm, - names, - normalize="true" if normalize == "recall" else None, - title="", # the tab header already labels this panel - ) - # Let the container height drive size (this panel is only half-column tall). - fig.layout.width = None - fig.layout.height = None - return fig - - # Cell click / clear → the shared error-cell selection. A single owner (dispatching on the - # trigger) keeps this the only writer, so no allow_duplicate coordination is needed. - @self.app.callback( - Output("error-cell", "data"), - Output("confusion-matrix-graph", "clickData"), - Input("confusion-matrix-graph", "clickData"), - Input("error-cell-clear-btn", "n_clicks"), - prevent_initial_call=True, - ) - def set_error_cell(click_data, _clear_clicks): - trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" - if "error-cell-clear-btn" in trigger: - return None, None - cell = _cell_from_click(click_data, name_to_idx) - if cell is None: - raise PreventUpdate - pred_i, true_i = cell - indices = np.where((pred_idx_arr == pred_i) & (true_idx_arr == true_i))[0].tolist() - # Reset clickData so re-clicking the *same* cell registers as a change and re-fires. - return {"pred": pred_i, "true": true_i, "indices": indices}, None - - # Per-cell word importance: words driving the (wrong) predicted class vs. the true class. - @self.app.callback( - Output("error-pred-importance", "figure"), - Output("error-true-importance", "figure"), - Output("error-cell-caption", "children"), - Input("error-cell", "data"), - ) - def update_error_word_charts(error_cell): - if not error_cell: - empty = _empty_word_fig("Click a confusion-matrix cell to see the words behind those errors.") - return empty, empty, "Click a cell to see the words behind those errors." - pred_i, true_i, indices = error_cell["pred"], error_cell["true"], error_cell["indices"] - pred_name, true_name = names[pred_i], names[true_i] - if not indices: - empty = _empty_word_fig("No samples for this (predicted, true) pair.") - return empty, empty, f"predicted {pred_name} · true {true_name}: 0 samples" - wi_pred = contrib.word_importance(label_idx=pred_i, n_top=15, sample_indices=indices) - wi_true = contrib.word_importance(label_idx=true_i, n_top=15, sample_indices=indices) - fig_pred = plot_word_importance( - wi_pred, title=f"Words toward predicted: {pred_name}", width=None, height=None - ) - fig_true = plot_word_importance( - wi_true, title=f"Words toward true: {true_name}", width=None, height=None - ) - fig_pred.layout.height = None - fig_true.layout.height = None - caption = f"predicted {pred_name} · true {true_name}: {len(indices)} sample(s)" - if pred_i == true_i: - caption += " — correct predictions (diagonal)" - elif len(indices) < 5: - caption += " — few samples; interpret with caution" - return fig_pred, fig_true, caption - # ── Tab visibility: toggle display of always-mounted bodies ────── # Bodies stay in the DOM (see _tabbed_card); only their `display` flips so the # cross-panel callbacks above keep firing for panels on inactive tabs. @@ -1140,9 +959,14 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): # (The selection-summary readout is written by filter_table, which knows the row count.) # ── Registered components (always-on core panels + capability-gated What-if Lab) ── - stores = {"apply": _APPLY_STORE, "current": _CURRENT_STORE} + stores = { + "apply": _APPLY_STORE, + "current": _CURRENT_STORE, + "error_cell": "error-cell", + "error_cell_clear": "error-cell-clear-btn", + } for comp in self._components: - comp.register_callbacks(self.app, self._view, self._engine, stores) + comp.register_callbacks(self.app, self._explanation, self._engine, stores) # ------------------------------------------------------------------ # Public @@ -1158,15 +982,13 @@ def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: """Per-sample sum of SHAP contributions for all tokens matching *word*.""" - contrib = self._view.contributions - n = self._view.n_samples + explanation = self._explanation + n = self._explanation.n_samples result = np.zeros(n) word_lower = word.lower() for i in range(n): - tokens = contrib.token_strings[i] - vals = contrib.values[i] - if vals.ndim == 2: - vals = vals[:, label_idx] + tokens = explanation.token_strings[i] + vals = select_label_column(explanation.values[i], label_idx) for j, tok in enumerate(tokens): if tok.strip().lower() == word_lower: result[i] += vals[j] @@ -1178,8 +1000,8 @@ def _error_mask(self) -> np.ndarray | None: ``None`` when either is unavailable. Matches the string comparison used by the "Model Errors" table filter so both stay consistent. """ - y_true = self._view.y_true - y_pred = self._view.y_pred + y_true = self._explanation.y_true + y_pred = self._explanation.y_pred if y_true is None or y_pred is None: return None return np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) @@ -1220,11 +1042,11 @@ def _build_scatter_fig( set, misclassified points are emphasized (larger, opaque) and the rest are shadowed (small, faint) without altering their color. """ - n = self._view.n_samples - contrib = self._view.contributions - view_texts = self._view.texts - assert view_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in view_texts] + n = self._explanation.n_samples + explanation = self._explanation + exp_texts = self._explanation.texts + assert exp_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in exp_texts] xy = self._scatter_xy assert xy is not None # noqa: S101 - only registered/called when scatter_xy was provided error_mask = self._error_mask() if errors_only else None @@ -1295,14 +1117,14 @@ def _build_scatter_fig( ) return fig - if color_by == "ground_truth" and self._view.y_true is not None: - labels = [str(label) for label in self._view.y_true.tolist()] - elif self._view.y_pred is not None: - labels = [str(label) for label in self._view.y_pred.tolist()] + if color_by == "ground_truth" and self._explanation.y_true is not None: + labels = [str(label) for label in self._explanation.y_true.tolist()] + elif self._explanation.y_pred is not None: + labels = [str(label) for label in self._explanation.y_pred.tolist()] else: labels = [""] * n - label_names = contrib.label_names or sorted(set(labels)) + label_names = explanation.label_names or sorted(set(labels)) fig = go.Figure() for i, name in enumerate(label_names): diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index 03983d4d..f0d739c6 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -2,7 +2,7 @@ Prototype of the master plan's Phase-5b ``WebappComponent`` contract, applied first to the interactive what-if panels (data editor + counterfactuals). Each component owns its layout and -callbacks and declares its ``requires`` capabilities, self-disabling when the bound view/engine +callbacks and declares its ``requires`` capabilities, self-disabling when the bound explanation/engine cannot satisfy them. Notably this **extends** the Phase-5b ``requires`` idea from *data* capabilities (e.g. @@ -21,6 +21,7 @@ pack_datapoint, unpack_datapoint, ) +from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent from shapash.webapp.nlp_components.label_noise import LabelNoiseComponent from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent from shapash.webapp.nlp_components.similar_examples import SimilarExamplesComponent @@ -31,6 +32,7 @@ "available_capabilities", "DataEditorComponent", "CounterfactualComponent", + "ErrorAnalysisComponent", "LabelNoiseComponent", "SentenceHighlightComponent", "SimilarExamplesComponent", diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 6d7a64c9..5b785c0e 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -1,9 +1,13 @@ """``WebappComponent`` contract + capability resolution for what-if panels. A component declares the capabilities it needs via ``requires``; :func:`available_capabilities` -computes what the bound view + engine actually provide, and :meth:`WebappComponent.is_available` +computes what the bound explanation + engine actually provide, and :meth:`WebappComponent.is_available` gates mounting on ``requires <= available``. This is the mechanism that makes the What-if Lab appear only when the explainer holds a live (and, for counterfactuals, gradient-capable) model. + +Components read the immutable :class:`~shapash.explainer.nlp_explanation.NlpExplanation` directly and +never write to it: every display choice lives in a Dash ``dcc.Store`` or a callback argument, so the +artifact a component renders is the same one that was saved. """ from __future__ import annotations @@ -12,8 +16,8 @@ from shapash.compute.diagnostics.label_noise import has_usable_probabilities from shapash.explainer.interactive import InteractiveEngine +from shapash.explainer.nlp_explanation import NlpExplanation from shapash.model.base import SupportsGradients, has_capabilities -from shapash.webapp.nlp_view import NlpView # Capability tokens components may require. CAP_PREDICT = "engine:predict" @@ -21,16 +25,17 @@ CAP_GRADIENTS = "model:gradients" CAP_SIMILAR = "engine:similar" CAP_LABELS = "data:labels" +CAP_GROUND_TRUTH = "data:ground_truth" -def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> frozenset[str]: - """Return the capability tokens the given view + engine satisfy. +def available_capabilities(explanation: NlpExplanation, engine: InteractiveEngine | None) -> frozenset[str]: + """Return the capability tokens the given explanation + engine satisfy. Parameters ---------- - view : NlpView - Read-only view, supplying the *data* capabilities — what the compiled batch contains, - independent of whether a live model is still attached. + explanation : NlpExplanation + The immutable artifact, supplying the *data* capabilities — what the compiled batch + contains, independent of whether a live model is still attached. engine : InteractiveEngine or None Live engine, or ``None`` for a snapshot (no live capabilities). @@ -43,8 +48,10 @@ def available_capabilities(view: NlpView, engine: InteractiveEngine | None) -> f caps: set[str] = set() # Data capabilities are read from the compiled batch, so they survive a snapshot — they sit # outside the engine guard below on purpose. - if view.y_true is not None and has_usable_probabilities(view.y_prob): - caps.add(CAP_LABELS) + if explanation.y_true is not None: + caps.add(CAP_GROUND_TRUTH) + if has_usable_probabilities(explanation.y_prob): + caps.add(CAP_LABELS) if engine is not None: if engine.can_edit(): caps.add(CAP_PREDICT) @@ -74,12 +81,12 @@ class WebappComponent(ABC): requires: frozenset[str] = frozenset() @classmethod - def is_available(cls, view: NlpView, engine: InteractiveEngine | None) -> bool: - """Whether the component's ``requires`` are satisfied by the view + engine.""" - return cls.requires <= available_capabilities(view, engine) + def is_available(cls, explanation: NlpExplanation, engine: InteractiveEngine | None) -> bool: + """Whether the component's ``requires`` are satisfied by the explanation + engine.""" + return cls.requires <= available_capabilities(explanation, engine) @abstractmethod - def layout(self, view: NlpView, engine: InteractiveEngine | None = None): + def layout(self, explanation: NlpExplanation, engine: InteractiveEngine | None = None): """Return the Dash layout for this component. ``engine`` is provided for components whose initial UI depends on live capabilities — e.g. the @@ -88,15 +95,15 @@ def layout(self, view: NlpView, engine: InteractiveEngine | None = None): """ @abstractmethod - def register_callbacks(self, app, view: NlpView, engine: InteractiveEngine, stores: dict) -> None: + def register_callbacks(self, app, explanation: NlpExplanation, engine: InteractiveEngine, stores: dict) -> None: """Register this component's Dash callbacks. Parameters ---------- app : dash.Dash The Dash application. - view : NlpView - Read-only data accessor. + explanation : NlpExplanation + The immutable artifact to read (never written to). engine : InteractiveEngine Live engine for prediction / counterfactual generation. stores : dict diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index 78df196c..9799c6c0 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -77,7 +77,7 @@ def _config_controls(self, engine, generator: str) -> list: ) return items - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the counterfactual card with a method selector and per-generator config controls. The selector and *every* generator's controls are built here (not injected by a callback) so @@ -132,7 +132,7 @@ def layout(self, view, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, view, engine, stores) -> None: + def register_callbacks(self, app, explanation, engine, stores) -> None: """Wire the method selector, Generate, and per-row Apply (→ shared editor store).""" apply_store = stores["apply"] current_store = stores["current"] diff --git a/shapash/webapp/nlp_components/data_editor.py b/shapash/webapp/nlp_components/data_editor.py index 9e30ce30..0e02b77f 100644 --- a/shapash/webapp/nlp_components/data_editor.py +++ b/shapash/webapp/nlp_components/data_editor.py @@ -51,7 +51,7 @@ class DataEditorComponent(WebappComponent): scope = "local" requires = frozenset({CAP_PREDICT}) - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the editor card: textarea, Predict button, class-probability bar. Token contributions for the edited text are shown by the shared lower-right panel (it reads the @@ -82,7 +82,7 @@ def layout(self, view, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, view, engine, stores) -> None: + def register_callbacks(self, app, explanation, engine, stores) -> None: """Wire prefill (row/apply → textarea) and Predict (textarea → prediction + current datapoint).""" apply_store = stores["apply"] current_store = stores["current"] diff --git a/shapash/webapp/nlp_components/datapoint.py b/shapash/webapp/nlp_components/datapoint.py index 46147e62..246c86c0 100644 --- a/shapash/webapp/nlp_components/datapoint.py +++ b/shapash/webapp/nlp_components/datapoint.py @@ -14,6 +14,8 @@ import numpy as np +from shapash.explainer.nlp_explanation import select_label_column + def pack_datapoint( *, @@ -114,7 +116,7 @@ def unpack_datapoint(dp: dict, label_idx: int) -> tuple[list[str], np.ndarray, f """ tokens = dp["tokens"] values = np.asarray(dp["values"], dtype=float) - vals = values[:, label_idx] if values.ndim == 2 else values + vals = select_label_column(values, label_idx) base_list = dp.get("base_values") base_value: float | None = None if base_list: diff --git a/shapash/webapp/nlp_components/error_analysis.py b/shapash/webapp/nlp_components/error_analysis.py new file mode 100644 index 00000000..c65975d0 --- /dev/null +++ b/shapash/webapp/nlp_components/error_analysis.py @@ -0,0 +1,248 @@ +"""Error Analysis panel: confusion matrix + per-(predicted, true) word importance. + +Clicking a confusion-matrix cell is a *global* selection driver, on a par with the embeddings-scatter +selection: it writes the shared ``error-cell`` store (``stores["error_cell"]``), which the app shell +composes with the scatter selection and the errors-only switch (see ``nlp_app._compose_selection``) to +filter the dataset table and the global Word Importance panel, and drives the two per-cell word charts +below the matrix in this panel. + +The "clear cell" button lives outside this component's own layout — in the app shell's persistent +selection bar, alongside the other filter clears — so its id is handed in via +``stores["error_cell_clear"]`` rather than created here. + +Gated on ``CAP_GROUND_TRUTH`` alone (not ``CAP_LABELS``): a confusion matrix only needs true/predicted +labels, not usable probabilities, so it must not be denied to a model with unusable/absent probabilities +the way Label Noise (confident learning) rightly is. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +import numpy as np +import plotly.graph_objs as go +from dash import Input, Output, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.plots.plot_word_importance import plot_word_importance +from shapash.webapp.nlp_components.base import CAP_GROUND_TRUTH, WebappComponent + +_NORMALIZE_OPTIONS: list[dcc.RadioItems.Options] = [ + {"label": " Counts", "value": "count"}, + {"label": " Recall", "value": "recall"}, +] + + +def _cell_from_click(click_data: dict | None, name_to_idx: dict[str, int]) -> tuple[int, int] | None: + """Resolve a confusion-matrix cell click to ``(pred_idx, true_idx)``. + + Heatmap ``clickData`` does not reliably include ``customdata`` the way scatter traces do, so + prefer it when present but fall back to mapping the predicted (``x``) and true (``y``) axis label + names back to their class indices. Returns ``None`` for an empty or unrecognised click. + """ + if not click_data or not click_data.get("points"): + return None + point = click_data["points"][0] + custom = point.get("customdata") + if custom is not None and len(custom) == 2: + return int(custom[0]), int(custom[1]) + if point.get("x") in name_to_idx and point.get("y") in name_to_idx: + return name_to_idx[point["x"]], name_to_idx[point["y"]] + return None + + +def _empty_word_fig(message: str) -> go.Figure: + """Placeholder figure with a centred hint, for the per-cell word charts before a cell is picked.""" + fig = go.Figure() + fig.add_annotation(text=message, showarrow=False, font=dict(color="#888888"), xref="paper", yref="paper") + fig.update_layout( + xaxis=dict(visible=False), + yaxis=dict(visible=False), + plot_bgcolor="white", + margin=dict(l=20, r=20, t=20, b=20), + ) + return fig + + +class ErrorAnalysisComponent(WebappComponent): + """Confusion matrix + per-cell word importance, for auditing where the model is wrong.""" + + id = "error-analysis" + name = "Error Analysis" + scope = "global" + requires = frozenset({CAP_GROUND_TRUTH}) + + def __init__(self) -> None: + # Populated by layout(); register_callbacks() runs after layout() has built the tab (see + # NlpWebApp._build_layout / _register_callbacks), so these are always set by then. + self._cm: np.ndarray | None = None + self._cm_true_idx: np.ndarray | None = None + self._cm_pred_idx: np.ndarray | None = None + + def layout(self, explanation, engine=None) -> html.Div: + """Build the matrix + per-cell word-importance panel, caching the matrix for callbacks.""" + label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] + idx_of = explanation.label_to_idx + y_true, y_pred = explanation.y_true, explanation.y_pred + assert y_true is not None and y_pred is not None # noqa: S101 - gated by CAP_GROUND_TRUTH + # The per-sample class indices stay local: the cell-click handler needs them to recover + # *which* samples sit in a clicked cell. The matrix itself is a plain derivation of the + # artifact, so it is read from there rather than recomputed here. + self._cm = explanation.confusion_matrix() + self._cm_true_idx = np.array([idx_of.get(str(v), -1) for v in y_true.tolist()]) + self._cm_pred_idx = np.array([idx_of.get(str(v), -1) for v in y_pred.tolist()]) + + return html.Div( + [ + dbc.Row( + [ + dbc.Col( + html.H6( + "Confusion Matrix — click a cell to inspect that error group", + className="fw-bold mb-0", + ), + width="auto", + className="align-self-center", + ), + dbc.Col( + dcc.RadioItems( + id="cm-normalize", + options=_NORMALIZE_OPTIONS, + value="count", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "12px"}, + ), + width="auto", + className="align-self-center ms-auto", + ), + ], + className="align-items-center mb-2", + style={"flex": "0 0 auto"}, + ), + dcc.Graph( + id="confusion-matrix-graph", + figure=plot_confusion_matrix(self._cm, label_names, title="", width=None, height=None), + config={"displayModeBar": False, "responsive": True}, + style={"height": "360px", "flex": "0 0 auto"}, + ), + html.Small( + id="error-cell-caption", + children="Click a cell to see the words behind those errors.", + className="text-muted d-block my-2", + style={"flex": "0 0 auto"}, + ), + dbc.Row( + [ + dbc.Col( + dcc.Graph( + id="error-pred-importance", + config={"displayModeBar": False, "responsive": True}, + style={"height": "320px"}, + ), + width=6, + ), + dbc.Col( + dcc.Graph( + id="error-true-importance", + config={"displayModeBar": False, "responsive": True}, + style={"height": "320px"}, + ), + width=6, + ), + ], + className="g-2", + style={"flex": "0 0 auto"}, + ), + ], + style={"height": "100%", "display": "flex", "flexDirection": "column", "overflowY": "auto"}, + ) + + def register_callbacks(self, app, explanation, engine, stores) -> None: + """Wire the normalize toggle, cell click/clear, and the two per-cell word charts.""" + error_cell_store = stores["error_cell"] + clear_btn_id = stores["error_cell_clear"] + + cm = self._cm + pred_idx_arr = self._cm_pred_idx + true_idx_arr = self._cm_true_idx + assert cm is not None and pred_idx_arr is not None and true_idx_arr is not None # noqa: S101 - layout() runs first + label_names = explanation.label_names or [str(i) for i in range(cm.shape[0])] + name_to_idx = {name: i for i, name in enumerate(label_names)} + + @app.callback( + Output("confusion-matrix-graph", "figure"), + Input("cm-normalize", "value"), + ) + def update_confusion_matrix(normalize): + fig = plot_confusion_matrix( + cm, + label_names, + normalize="true" if normalize == "recall" else None, + title="", # the tab header already labels this panel + ) + # Let the container height drive size (this panel is only half-column tall). + fig.layout.width = None + fig.layout.height = None + return fig + + # Cell click / clear → the shared error-cell selection. A single owner (dispatching on the + # trigger) keeps this the only writer, so no allow_duplicate coordination is needed. + @app.callback( + Output(error_cell_store, "data"), + Output("confusion-matrix-graph", "clickData"), + Input("confusion-matrix-graph", "clickData"), + Input(clear_btn_id, "n_clicks"), + prevent_initial_call=True, + ) + def set_error_cell(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if clear_btn_id in trigger: + return None, None + cell = _cell_from_click(click_data, name_to_idx) + if cell is None: + raise PreventUpdate + pred_i, true_i = cell + indices = np.where((pred_idx_arr == pred_i) & (true_idx_arr == true_i))[0].tolist() + # Reset clickData so re-clicking the *same* cell registers as a change and re-fires. + return {"pred": pred_i, "true": true_i, "indices": indices}, None + + @app.callback( + Output(clear_btn_id, "style"), + Input(error_cell_store, "data"), + ) + def toggle_error_cell_clear_button(error_cell): + visible = {"display": "inline", "fontSize": "0.8em"} + hidden = {"display": "none", "fontSize": "0.8em"} + return visible if error_cell else hidden + + # Per-cell word importance: words driving the (wrong) predicted class vs. the true class. + @app.callback( + Output("error-pred-importance", "figure"), + Output("error-true-importance", "figure"), + Output("error-cell-caption", "children"), + Input(error_cell_store, "data"), + ) + def update_error_word_charts(error_cell): + if not error_cell: + empty = _empty_word_fig("Click a confusion-matrix cell to see the words behind those errors.") + return empty, empty, "Click a cell to see the words behind those errors." + pred_i, true_i, indices = error_cell["pred"], error_cell["true"], error_cell["indices"] + pred_name, true_name = label_names[pred_i], label_names[true_i] + if not indices: + empty = _empty_word_fig("No samples for this (predicted, true) pair.") + return empty, empty, f"predicted {pred_name} · true {true_name}: 0 samples" + wi_pred = explanation.word_importance(label_idx=pred_i, n_top=15, sample_indices=indices) + wi_true = explanation.word_importance(label_idx=true_i, n_top=15, sample_indices=indices) + fig_pred = plot_word_importance( + wi_pred, title=f"Words toward predicted: {pred_name}", width=None, height=None + ) + fig_true = plot_word_importance(wi_true, title=f"Words toward true: {true_name}", width=None, height=None) + fig_pred.layout.height = None + fig_true.layout.height = None + caption = f"predicted {pred_name} · true {true_name}: {len(indices)} sample(s)" + if pred_i == true_i: + caption += " — correct predictions (diagonal)" + elif len(indices) < 5: + caption += " — few samples; interpret with caution" + return fig_pred, fig_true, caption diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py index 9445a3e6..15ae46c1 100644 --- a/shapash/webapp/nlp_components/label_noise.py +++ b/shapash/webapp/nlp_components/label_noise.py @@ -29,8 +29,16 @@ counterfactual and similar-example panels, the sample is already compiled, so this costs no re-explanation. -Gated by ``CAP_LABELS`` alone: the method needs no model, so the panel also works on a snapshot. The -Corpus check column self-disables when no labelled reference corpus is bound. +Gated by ``CAP_LABELS`` alone: the underlying method needs no model, so in principle this panel +should also work on a loaded snapshot (no live engine). **Temporarily not the case**: since the +``fit``/``explain`` split moved ``detect_label_noise`` onto the live ``NlpExplainer`` (it also +attaches the optional label-probe corroboration, which is fit-time engine state), a snapshot loaded +via ``NlpExplanation.load()`` has no engine to call it on. The Detect button therefore shows a +message instead of running when ``engine`` is ``None`` — see :meth:`LabelNoiseComponent.register_callbacks`. +Restoring snapshot support needs ``detect_label_noise``'s core computation split out as a pure +function over the artifact (data-only), with the probe corroboration staying engine-only; deferred +to a later retrofit. +The Corpus check column self-disables when no labelled reference corpus is bound. """ from __future__ import annotations @@ -63,7 +71,7 @@ class LabelNoiseComponent(WebappComponent): # Data-only: no engine capability, so this also mounts on a snapshot explainer. requires = frozenset({CAP_LABELS}) - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the panel: detection controls over an initially empty results area.""" can_probe = engine is not None and engine.can_probe_labels() caption = ( @@ -124,7 +132,7 @@ def layout(self, view, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, view, engine, stores) -> None: + def register_callbacks(self, app, explanation, engine, stores) -> None: """Run detection on demand; wire each row's Inspect into the shared current datapoint.""" current_store = stores["current"] @@ -138,7 +146,15 @@ def register_callbacks(self, app, view, engine, stores) -> None: def detect(n_clicks, top_n, score): if not n_clicks: raise PreventUpdate + if engine is None: + # See the module docstring: detect_label_noise lives on the live engine for now, + # so a loaded snapshot (no engine) cannot run it yet. + return html.Div( + "Label-noise detection needs a live explainer — unavailable on a loaded snapshot.", + className="text-muted", + ), [] report = engine.detect_label_noise( + explanation, top_n=int(top_n) if top_n else _DEFAULT_TOP_N, score=score or _DEFAULT_SCORE, ) @@ -168,17 +184,16 @@ def inspect(n_clicks_list, sample_indices): if not sample_indices or not (0 <= row < len(sample_indices)): raise PreventUpdate pos = int(sample_indices[row]) - contributions = view.contributions - texts = view.texts - if contributions is None or texts is None or not (0 <= pos < len(texts)): + texts = explanation.texts + if texts is None or not (0 <= pos < len(texts)): raise PreventUpdate - base_values = contributions.base_values - y_pred = view.y_pred + base_values = explanation.base_values + y_pred = explanation.y_pred return pack_datapoint( text=str(texts.iloc[pos]), orig_idx=pos, - tokens=contributions.token_strings[pos], - values=contributions.values[pos], + tokens=explanation.token_strings[pos], + values=explanation.values[pos], base_values=(base_values[pos] if base_values is not None else None), label=(str(y_pred.iloc[pos]) if y_pred is not None else None), ) diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py index 3133dd6b..2853e846 100644 --- a/shapash/webapp/nlp_components/sentence_highlight.py +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -24,9 +24,9 @@ class SentenceHighlightComponent(WebappComponent): def __init__(self, default_class_idx: int = 0) -> None: self._default_class_idx = default_class_idx - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the class picker + sentence-highlight placeholder div.""" - label_names = view.label_names or [str(i) for i in range(view.n_classes)] + label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] class_options: list[dcc.Dropdown.Options] = [{"label": name, "value": i} for i, name in enumerate(label_names)] return html.Div( [ @@ -60,7 +60,7 @@ def layout(self, view, engine=None) -> html.Div: style={"height": "100%"}, ) - def register_callbacks(self, app, view, engine, stores) -> None: + def register_callbacks(self, app, explanation, engine, stores) -> None: """Wire the predicted-class sync and the sentence-highlight render.""" current_store = stores["current"] @@ -74,7 +74,7 @@ def register_callbacks(self, app, view, engine, stores) -> None: def sync_local_class_to_prediction(datapoint): if not datapoint or datapoint.get("label") is None: raise PreventUpdate - label_idx = view.label_to_idx.get(str(datapoint["label"])) + label_idx = explanation.label_to_idx.get(str(datapoint["label"])) if label_idx is None: raise PreventUpdate return label_idx diff --git a/shapash/webapp/nlp_components/similar_examples.py b/shapash/webapp/nlp_components/similar_examples.py index 73b19cc2..3093a3cf 100644 --- a/shapash/webapp/nlp_components/similar_examples.py +++ b/shapash/webapp/nlp_components/similar_examples.py @@ -1,11 +1,19 @@ """Similar-examples component: reference examples most like the selected/edited text. Reads the shared ``current-datapoint`` store — so it serves a clicked dataset row, an edited sentence, -*and* an applied counterfactual through the same code path — and renders the top-k most similar -reference-corpus examples (cosine similarity in the model's similarity layer). Each neighbour shows its -similarity, its label, whether that label matches the current prediction, its text, and an *Inspect* -button that (mirroring the counterfactual panel) re-explains that example live and makes it the current -datapoint, so its token contributions render immediately in the Sentence/Waterfall panels. +*and* an applied counterfactual through the same code path — and renders similar reference-corpus +examples (cosine similarity in the model's similarity layer). Two mutually exclusive retrieval modes, +picked with a radio toggle whose two options each carry their own numeric field inline (Top-K's count, +Threshold's cutoff — the inactive one fades out via its ``disabled`` prop): **Top-K** (a fixed neighbour +count, any score) and **Threshold** (every reference above a cutoff, ranked descending and capped at +``_THRESHOLD_DISPLAY_LIMIT`` for display). The modes are kept exclusive rather than combined, since +"top-k above a threshold" is ambiguous about which bound wins and can silently under-report how many +references really clear the threshold. Each neighbour shows its similarity, its label, whether that +label matches the current prediction, its text, and an *Inspect* button that (mirroring the +counterfactual panel) re-explains that example live and makes it the current datapoint, so its token +contributions render immediately in the Sentence/Waterfall panels. Between the filter controls and the +table, a summary line reports what fraction of the *displayed* neighbours share the current prediction's +label. The lookup is one forward pass against a precomputed embedding bank, so it runs live on every selection change. Gated by ``CAP_SIMILAR`` + ``CAP_PREDICT``: mounts only when the explainer holds a @@ -23,8 +31,10 @@ from shapash.webapp.nlp_components.datapoint import datapoint_from_contributions _CARD_STYLE = {"border": "1px solid #dee2e6", "borderRadius": "4px", "padding": "12px", "height": "100%"} -_INLINE = {"display": "flex", "alignItems": "center", "gap": "0.4rem"} _DEFAULT_TOP_K = 5 +_DEFAULT_THRESHOLD = 0.95 +# A low threshold can match most of the corpus; cap what a live, per-keystroke panel renders. +_THRESHOLD_DISPLAY_LIMIT = 100 class SimilarExamplesComponent(WebappComponent): @@ -41,31 +51,66 @@ def _layer_name(self, engine) -> str | None: retriever = getattr(engine, "_retriever", None) return getattr(retriever, "layer", None) if retriever is not None else None - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the panel: a top-k control plus a results area fed by the current datapoint.""" layer = self._layer_name(engine) - caption = "Reference examples most similar to the selected text in the model's decision space" + caption = "Train examples most similar to the selected text in the model's decision space" if layer: caption += f" (layer: {layer})" caption += ". Click Inspect to explain one here." - return html.Div( - [ - html.H6("Similar Examples", className="fw-bold mb-2"), - html.Small(caption, className="text-muted d-block mb-2"), - html.Div( + mode_options: list[dcc.RadioItems.Options] = [ + { + "label": html.Span( [ - html.Label("Top-K", className="small fw-bold mb-0"), + "Top-K ", dcc.Input( id=f"{self.id}-topk", type="number", min=1, - max=25, + max=50, step=1, value=_DEFAULT_TOP_K, - style={"width": "80px"}, + style={"width": "70px"}, ), ], - style=_INLINE, + className="d-inline-flex align-items-center gap-1", + ), + "value": "topk", + }, + { + "label": html.Span( + [ + "Threshold ", + dcc.Input( + id=f"{self.id}-threshold", + type="number", + min=0, + max=1, + step=0.001, + value=_DEFAULT_THRESHOLD, + disabled=True, + style={"width": "70px"}, + ), + ], + className="d-inline-flex align-items-center gap-1", + ), + "value": "threshold", + }, + ] + return html.Div( + [ + html.H6("Similar Examples", className="fw-bold mb-2"), + html.Small(caption, className="text-muted d-block mb-2"), + # Each radio option's label carries its own numeric input, so the button and the + # field it governs sit on one line; a nested <input> inside a <label> still gets + # its own clicks (typing in it does not also toggle the radio), so this is safe. + dcc.RadioItems( + id=f"{self.id}-mode", + options=mode_options, + value="topk", + inline=True, + inputStyle={"marginRight": "6px"}, + labelStyle={"marginRight": "20px", "display": "inline-flex", "alignItems": "center"}, className="mb-2", ), dcc.Loading(html.Div(id=f"{self.id}-results")), @@ -74,26 +119,45 @@ def layout(self, view, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, view, engine, stores) -> None: - """Recompute neighbours on selection/top-k change; wire per-row Inspect into the current store.""" + def register_callbacks(self, app, explanation, engine, stores) -> None: + """Recompute neighbours on selection/mode/filter change; wire per-row Inspect into the current store.""" current_store = stores["current"] @app.callback( Output(f"{self.id}-results", "children"), Output(f"{self.id}-store", "data"), Input(current_store, "data"), + Input(f"{self.id}-mode", "value"), Input(f"{self.id}-topk", "value"), + Input(f"{self.id}-threshold", "value"), ) - def update_similar(datapoint, top_k): + def update_similar(datapoint, mode, top_k, threshold): text = (datapoint or {}).get("text", "") if not text or not text.strip(): raise PreventUpdate - k = int(top_k) if top_k else _DEFAULT_TOP_K - neighbors = engine.find_similar(text, top_k=k) - if not neighbors: - return html.Div("No reference examples available.", className="text-muted"), [] predicted = (datapoint or {}).get("label") - return _neighbors_table(neighbors, predicted, self.id), [n.text for n in neighbors] + if mode == "threshold": + t = float(threshold) if threshold is not None else _DEFAULT_THRESHOLD + neighbors, total = engine.find_similar_threshold(text, threshold=t, limit=_THRESHOLD_DISPLAY_LIMIT) + if not neighbors: + return html.Div("No train examples above the threshold.", className="text-muted"), [] + shown_of = total if total > len(neighbors) else None + else: + k = int(top_k) if top_k else _DEFAULT_TOP_K + neighbors = engine.find_similar(text, top_k=k) + if not neighbors: + return html.Div("No train examples available.", className="text-muted"), [] + shown_of = None + return _render_results(neighbors, predicted, self.id, shown_of=shown_of), [n.text for n in neighbors] + + @app.callback( + Output(f"{self.id}-topk", "disabled"), + Output(f"{self.id}-threshold", "disabled"), + Input(f"{self.id}-mode", "value"), + ) + def toggle_mode_inputs(mode): + is_threshold = mode == "threshold" + return is_threshold, not is_threshold # "Inspect" re-explains the chosen reference example and makes it the current datapoint, so its # token contributions render in the shared Sentence/Waterfall panels — the same flow the @@ -118,6 +182,41 @@ def inspect(n_clicks_list, neighbor_texts): return datapoint_from_contributions(text, contributions, label) +def _render_results(neighbors, predicted_label, component_id, shown_of=None): + """Precede the neighbours table with an optional cap note and a same-label match-rate summary. + + Both sit between the filter controls and the table, since they summarize *what was filtered* + rather than a per-row detail. ``shown_of`` is the total count that cleared the threshold before + capping to the displayed ``neighbors`` (``None`` in top-k mode, where the count is fixed by + construction). + """ + children = [] + if shown_of is not None: + children.append( + html.Small( + f"Showing {len(neighbors)} of {shown_of} train examples above the threshold.", + className="text-muted d-block mb-1", + ) + ) + match_rate = _match_rate_caption(neighbors, predicted_label) + if match_rate: + children.append(html.Div(match_rate, className="text-muted d-block mb-2")) + children.append(_neighbors_table(neighbors, predicted_label, component_id)) + return html.Div(children) + + +def _match_rate_caption(neighbors, predicted_label) -> str | None: + """``m/n (p%) share the predicted label`` over the labelled, currently displayed neighbours.""" + if predicted_label is None: + return None + labelled = [n for n in neighbors if n.label is not None] + if not labelled: + return None + matches = sum(1 for n in labelled if n.label == predicted_label) + pct = 100.0 * matches / len(labelled) + return f'{matches}/{len(labelled)} ({pct:.0f}%) share the predicted label "{predicted_label}".' + + def _neighbors_table(neighbors, predicted_label, component_id): """Render neighbours as a table with a match flag and a per-row Inspect button.""" show_label = any(n.label is not None for n in neighbors) diff --git a/shapash/webapp/nlp_components/waterfall.py b/shapash/webapp/nlp_components/waterfall.py index 8d838642..56d2e4d5 100644 --- a/shapash/webapp/nlp_components/waterfall.py +++ b/shapash/webapp/nlp_components/waterfall.py @@ -22,7 +22,7 @@ class WaterfallComponent(WebappComponent): name = "Waterfall" scope = "local" - def layout(self, view, engine=None) -> html.Div: + def layout(self, explanation, engine=None) -> html.Div: """Return the grouping-threshold slider + waterfall graph.""" return html.Div( [ @@ -49,10 +49,9 @@ def layout(self, view, engine=None) -> html.Div: style={"height": "100%"}, ) - def register_callbacks(self, app, view, engine, stores) -> None: + def register_callbacks(self, app, explanation, engine, stores) -> None: """Wire the waterfall figure to the current datapoint, class picker, and threshold.""" current_store = stores["current"] - contrib = view.contributions @app.callback( Output("waterfall-graph", "figure"), @@ -67,7 +66,7 @@ def update_waterfall(datapoint, label_idx, threshold_pct): raise PreventUpdate label_idx = int(label_idx) tokens, vals, base_value, _ = unpack_datapoint(datapoint, label_idx) - label_name = (contrib.label_names or [])[label_idx] if contrib.label_names else str(label_idx) + label_name = (explanation.label_names or [])[label_idx] if explanation.label_names else str(label_idx) min_pct = (threshold_pct if threshold_pct is not None else 10) / 100.0 return plot_waterfall( tokens=tokens, diff --git a/shapash/webapp/nlp_view.py b/shapash/webapp/nlp_view.py deleted file mode 100644 index c42d7517..00000000 --- a/shapash/webapp/nlp_view.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Read-only view over a compiled ``NlpExplainer`` for webapp components. - -Prototype of the master plan's ``ExplainerView`` (Phase 5) for the text modality: webapp components -read compiled explanation data **only** through this contract, never by reaching into explainer -internals. It is deliberately read-only — live actions (re-predicting edited text, generating -counterfactuals) live on the separate :class:`~shapash.explainer.interactive.InteractiveEngine`. -""" - -from __future__ import annotations - -import numpy as np -import pandas as pd - - -class NlpView: - """Read-only accessor over a compiled ``NlpExplainer``. - - Parameters - ---------- - explainer : NlpExplainer - A compiled explainer (``compile()`` already called). - """ - - def __init__(self, explainer) -> None: - self._explainer = explainer - - @property - def texts(self) -> pd.Series | None: - """The compiled input texts.""" - return self._explainer.texts - - @property - def contributions(self): - """The compiled ``NlpContributions``.""" - return self._explainer.contributions - - @property - def y_pred(self) -> pd.Series | None: - """Predicted labels for the compiled batch.""" - return self._explainer.y_pred - - @property - def y_prob(self) -> pd.DataFrame | None: - """Per-class probabilities for the compiled batch, if available.""" - return getattr(self._explainer, "y_prob", None) - - @property - def y_true(self) -> pd.Series | None: - """Ground-truth labels, if provided at compile time.""" - return getattr(self._explainer, "y_true", None) - - @property - def label_names(self) -> list[str] | None: - """Class names in output-column order.""" - return self._explainer.label_names or (self.contributions.label_names if self.contributions else None) - - @property - def has_ground_truth(self) -> bool: - """Whether ground-truth labels are available.""" - return self.y_true is not None - - @property - def label_to_idx(self) -> dict[str, int]: - """Class name to output-column index, in ``label_names`` order.""" - names = self.label_names or [str(i) for i in range(self.n_classes)] - return {name: i for i, name in enumerate(names)} - - @property - def n_classes(self) -> int: - """Number of classes inferred from the compiled contributions.""" - sample = self.contributions.values[0] - return sample.shape[1] if np.ndim(sample) == 2 else 1 - - @property - def n_samples(self) -> int: - """Number of samples in the compiled batch.""" - return len(self.texts) if self.texts is not None else 0 From 6de2ba4eedd7f3413462f86af5dec419c50edfe3 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 2 Sep 2026 16:09:12 +0200 Subject: [PATCH 097/144] test: update tests after refactoring --- .../integration_tests/test_captum_backends.py | 6 +- .../integration_tests/test_torch_st_models.py | 6 +- tests/unit_tests/backend/test_nlp_backend.py | 66 +- .../backend/test_nlp_captum_lig_backend.py | 7 +- .../backend/test_nlp_shap_backend.py | 4 +- .../retrieval/test_similar_examples.py | 53 ++ .../compute/test_embedding_store.py | 36 + tests/unit_tests/nlp/test_nlp_explanation.py | 346 ++++++++++ tests/unit_tests/nlp/test_nlp_plotter.py | 212 ++++++ tests/unit_tests/nlp/test_nlp_prototype.py | 638 +++++++++++------- .../unit_tests/webapp/test_nlp_whatif_app.py | 186 ++++- 11 files changed, 1275 insertions(+), 285 deletions(-) create mode 100644 tests/unit_tests/nlp/test_nlp_explanation.py create mode 100644 tests/unit_tests/nlp/test_nlp_plotter.py diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py index 42ae3529..7950914b 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_captum_backends.py @@ -56,9 +56,9 @@ def test_lig_backend_contributions(model): backend = NlpCaptumLigBackend(model, label_names=LABELS) raw = backend.run_explainer(["i am so happy today", "i feel terrified"]) - assert len(raw.contributions) == 2 + assert len(raw.values) == 2 assert raw.base_values.shape == (2, len(LABELS)) - for values, tokens in zip(raw.contributions, raw.data): + for values, tokens in zip(raw.values, raw.token_strings): assert values.shape == (len(tokens), len(LABELS)) assert np.isfinite(values).all() # Subwords are merged to whole words and specials dropped — no [CLS]/[SEP]/## leak into highlights. @@ -67,7 +67,7 @@ def test_lig_backend_contributions(model): # LIG is a completion method: base + sum(attributions) ≈ logits(x) for each class. input_ids, attention_mask, _ = model.encode("i am so happy today") logits_x = model.logits(input_ids, attention_mask)[0].detach().cpu().numpy() - recon = raw.base_values[0] + raw.contributions[0].sum(axis=0) + recon = raw.base_values[0] + raw.values[0].sum(axis=0) np.testing.assert_allclose(recon, logits_x, atol=0.2) diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py index 5a87f14a..42ae295e 100644 --- a/tests/integration_tests/test_torch_st_models.py +++ b/tests/integration_tests/test_torch_st_models.py @@ -142,8 +142,8 @@ def test_st_shap_backend_runs(st_model): def test_st_lig_backend_runs(st_model): backend = NlpCaptumLigBackend(st_model, label_names=LABELS) raw = backend.run_explainer(TEXTS[:2]) - assert len(raw.contributions) == 2 - for contrib in raw.contributions: + assert len(raw.values) == 2 + for contrib in raw.values: assert contrib.shape[1] == len(LABELS) @@ -212,7 +212,7 @@ def test_torch_shap_and_lig_backends_run(torch_model): backend = NlpCaptumLigBackend(torch_model, label_names=LABELS) raw = backend.run_explainer(TEXTS[:2]) - assert len(raw.contributions) == 2 + assert len(raw.values) == 2 def test_torch_pool_mode_changes_embedding(torch_model): diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py index a34c3335..962a00a9 100644 --- a/tests/unit_tests/backend/test_nlp_backend.py +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -1,13 +1,18 @@ """Unit tests for the shared NLP backend layer — the punctuation helper. -``NlpContributions.word_importance`` filtering is covered in ``tests/unit_tests/nlp``; this file +``NlpExplanation.word_importance`` filtering is covered in ``tests/unit_tests/nlp``; this file covers :func:`~shapash.backend.nlp_backend.is_punctuation` itself, which word segmentation now makes load-bearing (punctuation is emitted as its own unit rather than glued to a neighbour). """ import unittest -from shapash.backend.nlp_backend import is_punctuation +import numpy as np + +from shapash.backend.nlp_backend import NlpBackend, is_punctuation +from shapash.backend.nlp_lime_backend import NlpLimeBackend +from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.model.base import SupportsEmbeddings, TextModel class TestIsPunctuation(unittest.TestCase): @@ -31,5 +36,62 @@ def test_surrounding_whitespace_is_ignored(self): self.assertTrue(is_punctuation(" ! ")) +class TestBackendContractAttributes(unittest.TestCase): + """``reference_kind`` / ``is_additive`` / `requires_model_capabilities`` (A9 / A11).""" + + def test_nlp_shap_backend(self): + self.assertEqual(NlpShapBackend.reference_kind, "none") + self.assertTrue(NlpShapBackend.is_additive) + self.assertEqual(NlpShapBackend.requires_model_capabilities, ()) + + def test_nlp_lime_backend(self): + self.assertEqual(NlpLimeBackend.reference_kind, "none") + self.assertFalse(NlpLimeBackend.is_additive) + self.assertEqual(NlpLimeBackend.requires_model_capabilities, ()) + + +class _PredictOnlyModel(TextModel): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + +class _MinimalBackend(NlpBackend): + """Minimal concrete backend for testing ``NlpBackend.__init__``'s capability guard.""" + + name = "test_minimal_backend" + reference_kind = "none" + is_additive = True + requires_model_capabilities: tuple[type, ...] = () + + def run_explainer(self, x): + raise NotImplementedError + + def get_local_contributions(self, x, explain_data, subset=None): + raise NotImplementedError + + +class _RequiresEmbeddings(_MinimalBackend): + """Same as ``_MinimalBackend``, but declares a capability requirement.""" + + name = "test_requires_embeddings" + requires_model_capabilities = (SupportsEmbeddings,) + + +class TestRequiresModelCapabilitiesGuard(unittest.TestCase): + """``NlpBackend.__init__`` centralizes the capability check (A11 point 2).""" + + def test_raises_when_model_lacks_required_capability(self): + with self.assertRaises(TypeError): + _RequiresEmbeddings(_PredictOnlyModel()) + + def test_no_check_when_no_capabilities_required(self): + # The base default is () — a backend that declares no capability requirement + # (like NlpShapBackend/NlpLimeBackend) accepts any callable, including a bare + # model with no capability mixins at all. + self.assertEqual(NlpBackend.requires_model_capabilities, ()) + backend = _MinimalBackend(_PredictOnlyModel()) + self.assertIsInstance(backend, _MinimalBackend) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py index 9fa6557f..001414c6 100644 --- a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -11,7 +11,7 @@ from shapash.backend import NlpCaptumLigBackend, get_backend_cls_from_name from shapash.backend.nlp_captum_lig_backend import _aggregate_by_alignment, _aggregate_subwords -from shapash.model.base import TextModel +from shapash.model.base import SupportsCaptumIG, TextModel class PredictOnlyModel(TextModel): @@ -35,6 +35,11 @@ def test_requires_captum_capability(self): with self.assertRaises(TypeError): NlpCaptumLigBackend(PredictOnlyModel(), label_names=["neg", "pos"]) + def test_backend_contract_attributes(self): + self.assertEqual(NlpCaptumLigBackend.reference_kind, "point") + self.assertTrue(NlpCaptumLigBackend.is_additive) + self.assertEqual(NlpCaptumLigBackend.requires_model_capabilities, (SupportsCaptumIG,)) + class TestAggregateSubwords(unittest.TestCase): """``_aggregate_subwords`` is pure numpy — no torch/captum needed.""" diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index 9801905b..8737d3e0 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -219,8 +219,8 @@ def test_aggregates_subwords_end_to_end(self): raw = backend.run_explainer(["updating"]) - self.assertEqual(raw.data, [["updating"]]) - np.testing.assert_allclose(raw.contributions[0], [[5.0, 3.0]]) + self.assertEqual(raw.token_strings, [["updating"]]) + np.testing.assert_allclose(raw.values[0], [[5.0, 3.0]]) # CLS ([1,0]) + SEP ([4,3]) folded into the base. np.testing.assert_allclose(raw.base_values, [[0.1 + 1.0 + 4.0, 0.2 + 0.0 + 3.0]]) diff --git a/tests/unit_tests/compute/retrieval/test_similar_examples.py b/tests/unit_tests/compute/retrieval/test_similar_examples.py index 693fdf63..24b02d18 100644 --- a/tests/unit_tests/compute/retrieval/test_similar_examples.py +++ b/tests/unit_tests/compute/retrieval/test_similar_examples.py @@ -105,6 +105,21 @@ def test_cache_key_follows_the_models_space(self): self.assertEqual(set(model.spaces), {None}) self.assertIn("pooled", retriever.store.space_key) + def test_a_space_change_rebuilds_the_bank(self): + """A bank held across a live space switch would be cosine-compared against queries from the + new space — two unrelated spaces, yielding a plausible number rather than an error.""" + self.retriever.query("happy joy", top_k=1) + self.model.calls = 0 + self.model.embedding_space = "pooled" + self.retriever.query("happy joy", top_k=1) + self.assertEqual(self.model.calls, 2) # bank rebuilt in the new space, plus the query + + def test_the_bank_is_built_once_while_the_space_holds(self): + """Guard on the fix above: the bank is the expensive, amortizable half and must stay so.""" + for _ in range(3): + self.retriever.query("happy joy", top_k=1) + self.assertEqual(self.model.calls, 4) # one bank + three queries + def test_query_ranks_by_cosine_similarity(self): neighbors = self.retriever.query("happy joy", top_k=2) self.assertEqual([n.text for n in neighbors], ["happy joy", "joyful glad"]) @@ -122,6 +137,44 @@ def test_top_k_clamped_to_corpus_size(self): neighbors = self.retriever.query("neutral", top_k=99) self.assertEqual(len(neighbors), 4) + +class TestQueryThreshold(unittest.TestCase): + """The threshold path: not a fixed count, ranked descending, with an uncapped total.""" + + def setUp(self): + self.model = FakeEmbeddingModel() + self.texts = ["happy joy", "joyful glad", "sad down", "miserable"] + self.labels = ["pos", "pos", "neg", "neg"] + self.retriever = SimilarExampleRetriever(self.model, self.texts, self.labels) + + def test_returns_only_matches_above_threshold_descending(self): + # cosine("happy joy", "happy joy") = 1.0, cosine("happy joy", "joyful glad") ~= 0.994, + # the other two are near-orthogonal — well below any threshold used here. + neighbors, total = self.retriever.query_threshold("happy joy", threshold=0.99) + self.assertEqual([n.text for n in neighbors], ["happy joy", "joyful glad"]) + self.assertEqual(total, 2) + self.assertGreaterEqual(neighbors[0].score, neighbors[1].score) + + def test_threshold_is_strict_exclusive(self): + # A threshold equal to the best possible score (self-similarity = 1.0) admits nothing. + neighbors, total = self.retriever.query_threshold("happy joy", threshold=1.0) + self.assertEqual(neighbors, []) + self.assertEqual(total, 0) + + def test_limit_caps_returned_neighbors_but_not_the_total(self): + neighbors, total = self.retriever.query_threshold("happy joy", threshold=0.99, limit=1) + self.assertEqual([n.text for n in neighbors], ["happy joy"]) + self.assertEqual(total, 2) # still 2 cleared the threshold, only 1 is returned + + def test_no_limit_returns_every_match(self): + neighbors, total = self.retriever.query_threshold("happy joy", threshold=-1.0, limit=None) + self.assertEqual(len(neighbors), self.retriever.size) + self.assertEqual(total, self.retriever.size) + + def test_labels_ride_along(self): + neighbors, _total = self.retriever.query_threshold("happy joy", threshold=0.99) + self.assertEqual([n.label for n in neighbors], ["pos", "pos"]) + def test_bank_built_once_and_reused(self): self.retriever.query("happy joy", top_k=1) calls_after_first = self.model.calls diff --git a/tests/unit_tests/compute/test_embedding_store.py b/tests/unit_tests/compute/test_embedding_store.py index e02d8592..45656932 100644 --- a/tests/unit_tests/compute/test_embedding_store.py +++ b/tests/unit_tests/compute/test_embedding_store.py @@ -54,6 +54,42 @@ def test_embeds_once_and_memoizes(self): np.testing.assert_allclose(store.vectors(), first) self.assertEqual(model.calls, 1) + def test_a_space_change_invalidates_the_in_memory_memo(self): + """The memo is keyed by ``key``, which folds in the space — not by tag alone. + + A tag-only memo answered from the previous space after a live ``embedding_space`` switch and + never even consulted the (correctly keyed) file on disk. + """ + model = FakeModel() + store = EmbeddingStore(model, TEXTS) + store.vectors() + model.space = "pooled" + store.vectors() + self.assertEqual(model.calls, 2) # re-embedded rather than replaying the decision-space memo + + def test_the_memo_still_holds_within_one_space(self): + """Guard on the fix above: invalidating on a space change must not disable memoization.""" + model = FakeModel() + store = EmbeddingStore(model, TEXTS) + for _ in range(3): + store.vectors() + self.assertEqual(model.calls, 1) + + def test_a_space_change_reloads_from_disk_rather_than_recomputing(self): + """Once both spaces are on disk, switching back and forth costs no forward pass.""" + with tempfile.TemporaryDirectory() as d: + model = FakeModel() + store = EmbeddingStore(model, TEXTS, cache_dir=d) + store.vectors() + model.space = "pooled" + store.vectors() + fresh = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) + fresh.model.space = "pooled" + fresh.vectors() + fresh.model.space = "decision" + fresh.vectors() + self.assertEqual(fresh.model.calls, 0) # both spaces served from their own files + def test_embeds_in_the_models_default_space(self): """The store never picks a space of its own: it calls embed() bare and keys on what that means.""" model = FakeModel(space="pooled") diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/nlp/test_nlp_explanation.py new file mode 100644 index 00000000..7b3f868c --- /dev/null +++ b/tests/unit_tests/nlp/test_nlp_explanation.py @@ -0,0 +1,346 @@ +"""Unit tests for ``NlpExplanation`` — the ``explain()`` return value and its persistence. + +Covers the round-trip (save -> load) across the shape matrix that matters: 1-D (binary/ +regression) vs 2-D (multi-class) contribution arrays, with/without a baseline, with/without +ground truth and probabilities, with/without a bundled scatter projection, and a sample with +zero tokens (the edge case that broke a naive "infer counts from the tidy tables" design — +see ``_n_classes``/``_frames_to_contributions`` in the module under test). +""" + +import json +import tempfile +import unittest +import unittest.mock +import zipfile +import dataclasses +from dataclasses import replace +from pathlib import Path + +import numpy as np +import pandas as pd + +from shapash.explainer.nlp_explanation import NlpExplanation + + +def _make_explanation(values_ndim: int, with_base: bool, with_true: bool, with_prob: bool) -> NlpExplanation: + texts = pd.Series(["hello world", "i am happy today", "ok"], index=[10, 11, 12]) + label_names = ["neg", "pos"] if values_ndim == 2 else None + + if values_ndim == 2: + values = [ + np.array([[1.0, -1.0], [2.0, -2.0]]), + np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]), + np.zeros((0, 2)), # the third sample has zero tokens + ] + base_values = np.array([[0.1, 0.9], [0.2, 0.8], [0.3, 0.7]]) if with_base else None + else: + values = [np.array([1.0, 2.0]), np.array([0.1, 0.3, 0.5]), np.zeros((0,))] + base_values = np.array([0.1, 0.2, 0.3]) if with_base else None + + y_pred = pd.Series(["pos", "neg", "pos"], index=texts.index, name="prediction") + y_prob = ( + pd.DataFrame({"neg": [0.1, 0.8, 0.3], "pos": [0.9, 0.2, 0.7]}, index=texts.index) if with_prob else None + ) + y_true = pd.Series(["pos", "pos", "pos"], index=texts.index, name="ground_truth") if with_true else None + + return NlpExplanation( + texts=texts, + token_strings=[["hello", "world"], ["i", "am", "happy"], []], + values=values, + base_values=base_values, + y_pred=y_pred, + y_prob=y_prob, + y_true=y_true, + label_names=label_names, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + ) + + +class TestNlpExplanationRoundTrip(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.tmp_path = Path(self._tmp.name) + + def _round_trip(self, expl: NlpExplanation, scatter_xy=None) -> tuple[NlpExplanation, np.ndarray | None]: + path = self.tmp_path / "explanation.zip" + expl.save(path, scatter_xy=scatter_xy) + return NlpExplanation.load(path) + + def _assert_round_trips(self, expl: NlpExplanation, scatter_xy=None): + loaded, loaded_scatter = self._round_trip(expl, scatter_xy=scatter_xy) + + for original, restored in zip(expl.values, loaded.values, strict=True): + np.testing.assert_allclose(original, restored) + self.assertEqual(expl.token_strings, loaded.token_strings) + + if expl.base_values is None: + self.assertIsNone(loaded.base_values) + else: + np.testing.assert_allclose(expl.base_values, loaded.base_values) + + pd.testing.assert_series_equal(expl.texts, loaded.texts, check_names=False) + pd.testing.assert_series_equal(expl.y_pred, loaded.y_pred, check_names=False) + + if expl.y_true is None: + self.assertIsNone(loaded.y_true) + else: + pd.testing.assert_series_equal(expl.y_true, loaded.y_true, check_names=False) + + if expl.y_prob is None: + self.assertIsNone(loaded.y_prob) + else: + pd.testing.assert_frame_equal(expl.y_prob, loaded.y_prob) + + self.assertEqual(loaded.backend_name, expl.backend_name) + self.assertEqual(loaded.is_additive, expl.is_additive) + self.assertEqual(loaded.reference_kind, expl.reference_kind) + self.assertEqual(loaded.label_names, expl.label_names) + self.assertEqual(loaded.folds_case, expl.folds_case) + + if scatter_xy is None: + self.assertIsNone(loaded_scatter) + else: + np.testing.assert_allclose(loaded_scatter, scatter_xy) + + def test_multiclass_with_base_ground_truth_probabilities_and_scatter(self): + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + scatter = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]) + self._assert_round_trips(expl, scatter_xy=scatter) + + def test_binary_without_ground_truth_or_probabilities(self): + expl = _make_explanation(values_ndim=1, with_base=True, with_true=False, with_prob=False) + self._assert_round_trips(expl) + + def test_multiclass_without_base_values(self): + expl = _make_explanation(values_ndim=2, with_base=False, with_true=True, with_prob=True) + self._assert_round_trips(expl) + + def test_binary_without_base_values_ground_truth_or_probabilities(self): + expl = _make_explanation(values_ndim=1, with_base=False, with_true=False, with_prob=False) + self._assert_round_trips(expl) + + def test_meta_json_is_plain_text_readable_without_shapash(self): + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + path = self.tmp_path / "explanation.zip" + expl.save(path) + with zipfile.ZipFile(path) as zf: + meta = json.loads(zf.read("meta.json")) + self.assertEqual(meta["backend_name"], "nlp_shap") + self.assertTrue(meta["is_additive"]) + self.assertEqual(meta["reference_kind"], "none") + self.assertEqual(meta["label_names"], ["neg", "pos"]) + self.assertEqual(meta["n_samples"], 3) + self.assertIn("shapash_version", meta) + self.assertIn("created_at", meta) + + def test_unsupported_format_version_raises_actionable_error(self): + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + path = self.tmp_path / "explanation.zip" + expl.save(path) + + with zipfile.ZipFile(path) as zin: + meta = json.loads(zin.read("meta.json")) + meta["format_version"] = 999 + members = {item.filename: zin.read(item.filename) for item in zin.infolist()} + members["meta.json"] = json.dumps(meta).encode() + + bad_path = self.tmp_path / "bad_version.zip" + with zipfile.ZipFile(bad_path, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + + with self.assertRaises(ValueError) as ctx: + NlpExplanation.load(bad_path) + self.assertIn("format_version", str(ctx.exception)) + self.assertIn("999", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() + + +class TestNlpExplanationDescriptors(unittest.TestCase): + """Batch descriptors the webapp reads straight off the artifact. + + These four used to live on a separate ``NlpView`` wrapper that held no state of its own — + every display choice already lives in the webapp's ``dcc.Store``s, so the wrapper only + forwarded attribute reads and the app routed around it to reach the explanation anyway. + They are derivations of the data, so they belong with the data. + """ + + def test_counts_and_ground_truth_flag(self): + explanation = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + self.assertEqual(explanation.n_samples, 3) + self.assertEqual(explanation.n_classes, 2) + self.assertTrue(explanation.has_ground_truth) + + def test_has_ground_truth_is_false_without_y_true(self): + explanation = _make_explanation(values_ndim=2, with_base=True, with_true=False, with_prob=True) + self.assertFalse(explanation.has_ground_truth) + + def test_n_classes_is_one_for_binary_1d_values(self): + explanation = _make_explanation(values_ndim=1, with_base=True, with_true=True, with_prob=True) + self.assertEqual(explanation.n_classes, 1) + + def test_label_to_idx_follows_label_names_order(self): + explanation = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + self.assertEqual(explanation.label_to_idx, {"neg": 0, "pos": 1}) + + def test_label_to_idx_falls_back_to_column_indices_without_names(self): + explanation = replace( + _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True), label_names=None + ) + self.assertEqual(explanation.label_to_idx, {"0": 0, "1": 1}) + + def test_n_classes_survives_an_all_empty_batch(self): + """The old wrapper read ``values[0].shape[1]``, which raised here; ``_n_classes`` does not.""" + explanation = replace( + _make_explanation(values_ndim=2, with_base=False, with_true=False, with_prob=False), + token_strings=[[], [], []], + values=[np.zeros((0, 2)) for _ in range(3)], + ) + self.assertEqual(explanation.n_classes, 2) # recovered from label_names + + +class TestFieldPartition(unittest.TestCase): + """The caller/computed split that makes memoizing on text content alone sound.""" + + def test_every_field_is_classified(self): + # Mirrors the import-time guard at the bottom of nlp_explanation.py, so the reason for + # the failure is legible from the test suite and not only from a broken import. + names = {f.name for f in dataclasses.fields(NlpExplanation)} + classified = NlpExplanation._CALLER_FIELDS | NlpExplanation._COMPUTED_FIELDS + self.assertEqual(names, classified) + + def test_the_two_halves_do_not_overlap(self): + self.assertEqual(NlpExplanation._CALLER_FIELDS & NlpExplanation._COMPUTED_FIELDS, frozenset()) + + def test_relabelled_refuses_a_caller_field_it_does_not_produce(self): + # The import-time guard only establishes that a field has been *classified*. This is the + # second half: a field declared caller-owned but not produced in relabelled() would be + # inherited from the cached run, which is the exact bug the method exists to prevent. + exp = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + with unittest.mock.patch.object( + NlpExplanation, "_CALLER_FIELDS", NlpExplanation._CALLER_FIELDS | {"label_names"} + ): + with self.assertRaises(ValueError) as ctx: + exp.relabelled(exp.texts.set_axis(["a", "b", "c"])) + self.assertIn("label_names", str(ctx.exception)) + self.assertIn("not produced here", str(ctx.exception)) + + def test_relabelled_moves_every_caller_field_and_no_computed_one(self): + exp = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + texts = exp.texts.set_axis(["a", "b", "c"]) + y_true = pd.Series(["neg", "pos", "neg"], index=["a", "b", "c"]) + + out = exp.relabelled(texts, y_true=y_true) + + for name in NlpExplanation._CALLER_FIELDS: + field = getattr(out, name) + if field is not None: + self.assertTrue(field.index.equals(texts.index), f"{name} kept the old index") + self.assertIs(out.y_true, y_true) + for name in NlpExplanation._COMPUTED_FIELDS: + self.assertIs(getattr(out, name), getattr(exp, name), f"{name} was not carried through") + + +class TestImmutability(unittest.TestCase): + """The artifact is immutable in depth, and ``replace`` is the supported way to vary it. + + These are not style assertions. The whole display-state design rests on "what the webapp and + the plotter render is what ``explain()`` computed", and two of the fields below are read as + correctness guards (:attr:`is_additive` gates the waterfall, ``y_true`` gates the confusion + matrix) — a guard a caller can switch off by assignment is not a guard. + """ + + def setUp(self): + self.explanation = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + + def test_rebinding_a_field_is_refused(self): + for field_name, value in ( + ("is_additive", False), # the waterfall guard + ("backend_name", "something-else"), + ("label_names", None), + ("y_true", None), # the confusion-matrix guard + ): + with self.subTest(field=field_name), self.assertRaises(dataclasses.FrozenInstanceError): + setattr(self.explanation, field_name, value) + + def test_contribution_arrays_are_sealed_against_in_place_writes(self): + with self.assertRaises(ValueError): + self.explanation.values[0][0, 0] = 999.0 + with self.assertRaises(ValueError): + self.explanation.base_values[0, 0] = 999.0 + + def test_replace_is_the_supported_way_to_vary_a_field(self): + derived = replace(self.explanation, label_names=["a", "b"]) + self.assertEqual(derived.label_names, ["a", "b"]) + self.assertEqual(self.explanation.label_names, ["neg", "pos"]) # original untouched + # replace() re-runs __post_init__, so the derived artifact is sealed too. + with self.assertRaises(ValueError): + derived.values[0][0, 0] = 1.0 + + def test_the_regex_constant_is_not_a_field(self): + """A ClassVar, so it stays out of ``__init__``, ``replace()`` and the saved payload.""" + self.assertNotIn("_SPECIAL_RE", {f.name for f in dataclasses.fields(NlpExplanation)}) + + def test_identity_equality_replaces_an__eq__that_could_only_raise(self): + other = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + self.assertEqual(self.explanation, self.explanation) + self.assertNotEqual(self.explanation, other) + self.assertEqual(len({self.explanation, other}), 2) # hashable again, by identity + + +class TestSharedIndexInvariant(unittest.TestCase): + """``texts``, ``y_pred``, ``y_prob`` and ``y_true`` must carry one index. + + Enforced in ``__post_init__`` rather than at the call sites because ``replace`` — the mandated + way to vary a frozen artifact — says nothing about the fields it is not handed. An artifact + whose halves are indexed differently raises nowhere on its own: pandas alignment yields + all-NaN and :meth:`confusion_matrix` zips positionally, reporting plausible wrong counts. + """ + + def setUp(self): + self.explanation = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + + def test_replacing_texts_alone_is_refused(self): + """The shape of the real bug: patch one index-bearing field, leave the rest stale.""" + shifted = pd.Series(self.explanation.texts.to_numpy(), index=[77, 88, 99]) + with self.assertRaises(ValueError) as ctx: + replace(self.explanation, texts=shifted) + self.assertIn("y_pred", str(ctx.exception)) + + def test_each_labelled_field_is_checked(self): + for field_name in ("y_pred", "y_prob", "y_true"): + original = getattr(self.explanation, field_name) + with self.subTest(field=field_name), self.assertRaises(ValueError) as ctx: + replace(self.explanation, **{field_name: original.set_axis([77, 88, 99])}) + self.assertIn(field_name, str(ctx.exception)) + + def test_replacing_every_index_bearing_field_together_is_allowed(self): + index = pd.Index([77, 88, 99]) + derived = replace( + self.explanation, + texts=self.explanation.texts.set_axis(index), + y_pred=self.explanation.y_pred.set_axis(index), + y_prob=self.explanation.y_prob.set_axis(index), + y_true=self.explanation.y_true.set_axis(index), + ) + self.assertTrue(derived.y_pred.index.equals(index)) + + def test_absent_optional_fields_are_skipped_not_faulted(self): + bare = _make_explanation(values_ndim=2, with_base=False, with_true=False, with_prob=False) + self.assertIsNone(bare.y_true) + self.assertIsNone(bare.y_prob) + + def test_a_saved_artifact_reloads_with_one_index(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "run.zip" + self.explanation.save(path) + restored, _ = NlpExplanation.load(path) + self.assertTrue(restored.y_pred.index.equals(restored.texts.index)) + self.assertTrue(restored.y_true.index.equals(restored.texts.index)) diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py new file mode 100644 index 00000000..ebe3aa9b --- /dev/null +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -0,0 +1,212 @@ +"""Unit tests for ``NlpPlotter`` — the ``explanation.plot`` accessor. + +The accessor exists so a *reloaded* explanation can be plotted: ``NlpExplanation.load()`` +deliberately needs no model and no backend, but plotting used to require an ``NlpExplainer``. +These tests therefore check three things: the figures come out, the artifact is never written +to, and the two guards (non-additive backend, missing ground truth) refuse rather than draw +something meaningless. +""" + +import copy +import unittest +from dataclasses import replace + +import numpy as np +import pandas as pd +from dash import html +from plotly import graph_objs as go + +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.explainer.nlp_plotter import NlpPlotter + + +def _make_explanation( + ndim: int = 2, is_additive: bool = True, with_true: bool = True, with_base: bool = True +) -> NlpExplanation: + texts = pd.Series(["hello world", "i am happy today", "ok"], index=[10, 11, 12]) + if ndim == 2: + values = [ + np.array([[1.0, -1.0], [2.0, -2.0]]), + np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]), + np.array([[0.7, -0.7]]), + ] + base_values = np.array([[0.1, 0.9], [0.2, 0.8], [0.3, 0.7]]) if with_base else None + label_names = ["neg", "pos"] + else: + values = [np.array([1.0, 2.0]), np.array([0.1, 0.3, 0.5]), np.array([0.7])] + base_values = np.array([0.1, 0.2, 0.3]) if with_base else None + label_names = None + + return NlpExplanation( + texts=texts, + token_strings=[["hello", "world"], ["i", "am", "happy"], ["ok"]], + values=values, + base_values=base_values, + y_pred=pd.Series(["pos", "neg", "pos"], index=texts.index), + y_prob=None, + y_true=pd.Series(["pos", "pos", "pos"], index=texts.index) if with_true else None, + label_names=label_names, + folds_case=True, + backend_name="nlp_shap" if is_additive else "nlp_lime", + is_additive=is_additive, + reference_kind="distribution" if with_base else "none", + ) + + +class TestPlotAccessor(unittest.TestCase): + def test_plot_returns_a_plotter_bound_to_this_explanation(self): + explanation = _make_explanation() + self.assertIsInstance(explanation.plot, NlpPlotter) + self.assertIs(explanation.plot._exp, explanation) + + def test_plot_is_not_a_dataclass_field(self): + """It must stay out of ``fields()`` or ``save()``/``load()`` would try to persist it.""" + import dataclasses + + names = {f.name for f in dataclasses.fields(_make_explanation())} + self.assertNotIn("plot", names) + + def test_plot_is_built_fresh_and_holds_no_state(self): + explanation = _make_explanation() + self.assertIsNot(explanation.plot, explanation.plot) + + def test_repr_names_the_batch_and_backend(self): + self.assertIn("nlp_shap", repr(_make_explanation().plot)) + + +class TestPerInstancePlots(unittest.TestCase): + def setUp(self): + self.explanation = _make_explanation() + + def test_tokens_returns_a_figure_for_every_row_and_class(self): + for row in range(len(self.explanation)): + for label_idx in range(self.explanation.n_classes): + self.assertIsInstance(self.explanation.plot.tokens(row=row, label_idx=label_idx), go.Figure) + + def test_tokens_titles_the_class_when_names_are_known(self): + self.assertIn("pos", self.explanation.plot.tokens(row=0, label_idx=1).layout.title.text) + + def test_tokens_falls_back_to_a_generic_title_without_class_names(self): + explanation = _make_explanation(ndim=1) + self.assertEqual(explanation.plot.tokens(row=0).layout.title.text, "Token contributions") + + def test_tokens_honours_max_tokens(self): + fig = self.explanation.plot.tokens(row=1, label_idx=0, max_tokens=2) + self.assertLessEqual(len(fig.data[0].x), 2) + + def test_waterfall_returns_a_figure(self): + self.assertIsInstance(self.explanation.plot.waterfall(row=0, label_idx=1), go.Figure) + + def test_sentence_returns_a_dash_component(self): + self.assertIsInstance(self.explanation.plot.sentence(row=0, label_idx=1), html.Div) + + def test_negative_row_counts_from_the_end(self): + last = self.explanation.plot.tokens(row=-1, label_idx=0) + self.assertEqual(list(last.data[0].y), ["ok"]) + + def test_row_is_positional_not_a_texts_index_label(self): + """``texts.index`` starts at 10; ``row=0`` must still mean the first sample.""" + self.assertEqual(list(self.explanation.texts.index)[0], 10) + fig = self.explanation.plot.tokens(row=0, label_idx=0) + self.assertEqual(set(fig.data[0].y), {"hello", "world"}) + + def test_out_of_range_row_raises_with_the_batch_size(self): + with self.assertRaises(IndexError) as ctx: + self.explanation.plot.tokens(row=99) + self.assertIn("3 sample(s)", str(ctx.exception)) + + def test_out_of_range_label_idx_raises_listing_the_classes(self): + with self.assertRaises(IndexError) as ctx: + self.explanation.plot.tokens(row=0, label_idx=7) + self.assertIn("neg", str(ctx.exception)) + + def test_binary_1d_values_need_no_class_slicing(self): + explanation = _make_explanation(ndim=1) + self.assertIsInstance(explanation.plot.waterfall(row=1), go.Figure) + + def test_plots_work_without_base_values(self): + """``reference_kind == "none"`` means no baseline bar, not a crash.""" + explanation = _make_explanation(with_base=False) + self.assertIsInstance(explanation.plot.waterfall(row=0, label_idx=0), go.Figure) + self.assertIsInstance(explanation.plot.sentence(row=0, label_idx=0), html.Div) + + +class TestBatchPlots(unittest.TestCase): + def test_words_returns_a_figure_titled_with_the_class(self): + fig = _make_explanation().plot.words(label_idx=1, n_top=5) + self.assertIsInstance(fig, go.Figure) + self.assertIn("pos", fig.layout.title.text) + + def test_words_forwards_kwargs_to_word_importance(self): + explanation = _make_explanation() + fig = explanation.plot.words(label_idx=0, n_top=10, exclude_words={"hello"}) + self.assertNotIn("hello", set(fig.data[0].y)) + + def test_confusion_returns_a_figure(self): + self.assertIsInstance(_make_explanation().plot.confusion(), go.Figure) + + def test_confusion_refuses_without_ground_truth(self): + explanation = _make_explanation(with_true=False) + with self.assertRaises(ValueError) as ctx: + explanation.plot.confusion() + self.assertIn("y_true", str(ctx.exception)) + + +class TestGuards(unittest.TestCase): + def test_waterfall_refuses_on_a_non_additive_backend(self): + """LIME contributions do not sum to the prediction, so the running total is nonsense.""" + explanation = _make_explanation(is_additive=False) + with self.assertRaises(ValueError) as ctx: + explanation.plot.waterfall(row=0, label_idx=0) + message = str(ctx.exception) + self.assertIn("nlp_lime", message) + self.assertIn("tokens", message) # points at the plot that is still valid + + def test_the_other_plots_stay_available_on_a_non_additive_backend(self): + explanation = _make_explanation(is_additive=False) + self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) + self.assertIsInstance(explanation.plot.words(label_idx=0), go.Figure) + + +class TestArtifactIsNeverWritten(unittest.TestCase): + def test_rendering_leaves_the_explanation_untouched(self): + explanation = _make_explanation() + before = copy.deepcopy(explanation) + + explanation.plot.tokens(row=0, label_idx=1, max_tokens=1) + explanation.plot.waterfall(row=1, label_idx=0) + explanation.plot.sentence(row=0, label_idx=0) + explanation.plot.words(label_idx=1, n_top=3) + explanation.plot.confusion(normalize="true") + + self.assertEqual(explanation.backend_name, before.backend_name) + self.assertEqual(explanation.label_names, before.label_names) + self.assertEqual(explanation.token_strings, before.token_strings) + for original, current in zip(before.values, explanation.values, strict=True): + np.testing.assert_array_equal(original, current) + np.testing.assert_array_equal(before.base_values, explanation.base_values) + + +class TestConfusionMatrixData(unittest.TestCase): + def test_counts_are_true_by_predicted(self): + explanation = _make_explanation() + # y_true = [pos, pos, pos], y_pred = [pos, neg, pos]; idx: neg=0, pos=1 + cm = explanation.confusion_matrix() + self.assertEqual(cm.shape, (2, 2)) + self.assertEqual(cm[1, 1], 2) # true pos, predicted pos + self.assertEqual(cm[1, 0], 1) # true pos, predicted neg + self.assertEqual(cm[0].sum(), 0) # no true-neg samples + + def test_unknown_labels_are_skipped_not_raised(self): + base = _make_explanation() + explanation = replace(base, y_pred=pd.Series(["pos", "surprise", "pos"], index=base.texts.index)) + cm = explanation.confusion_matrix() + self.assertEqual(cm.sum(), 2) # the unmatched row is dropped, the rest still counted + + def test_raises_without_ground_truth(self): + with self.assertRaises(ValueError): + _make_explanation(with_true=False).confusion_matrix() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 679d2f00..9e6b12cc 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -9,6 +9,7 @@ import tempfile import unittest +from dataclasses import replace from unittest.mock import patch import numpy as np @@ -16,12 +17,13 @@ import plotly.graph_objs as go from dash import html -from shapash.backend.nlp_backend import NlpBackend, NlpContributions, NlpRawExplanation +from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_lime_backend import NlpLimeBackend from shapash.backend.nlp_shap_backend import NlpShapBackend from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.compute.retrieval import Neighbor from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.explainer.nlp_explanation import NlpExplanation from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_noise_matrix import plot_noise_matrix @@ -29,7 +31,8 @@ from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance -from shapash.webapp.nlp_app import NlpWebApp, _cell_from_click, _compose_selection +from shapash.webapp.nlp_app import NlpWebApp, _compose_selection +from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] N_CLASSES = len(LABEL_NAMES) @@ -39,8 +42,8 @@ # --------------------------------------------------------------------------- -def _make_contributions() -> NlpContributions: - """Synthetic NlpContributions for 3 samples, 6 classes.""" +def _make_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: + """Synthetic per-token contribution data for 3 samples, 6 classes.""" rng = np.random.default_rng(42) token_strings = [ ["", "i", "feel", "so", "happy", "today", ""], @@ -49,224 +52,267 @@ def _make_contributions() -> NlpContributions: ] values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) - return NlpContributions( + return token_strings, values, base_values + + +def _make_texts() -> pd.Series: + return pd.Series( + ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], + index=pd.RangeIndex(3), + ) + + +def _make_explanation( + texts: pd.Series | None = None, + y_pred: pd.Series | None = None, + y_prob: pd.DataFrame | None = None, + y_true: pd.Series | None = None, + label_names: list[str] | None = LABEL_NAMES, + backend_name: str = "nlp_shap", + is_additive: bool = True, + reference_kind: str = "none", +) -> NlpExplanation: + """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" + texts = _make_texts() if texts is None else texts + token_strings, values, base_values = _make_token_data() + return NlpExplanation( + texts=texts, token_strings=token_strings, values=values, base_values=base_values, - label_names=LABEL_NAMES, - index=pd.RangeIndex(3), + y_pred=(pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred), + y_prob=y_prob, + y_true=y_true, + label_names=label_names, + folds_case=None, + backend_name=backend_name, + is_additive=is_additive, + reference_kind=reference_kind, ) -def _make_explainer(compiled: bool = True) -> NlpExplainer: - """NlpExplainer with synthetic data, bypassing __init__ to avoid shap.Explainer(None). +def _minimal_explanation( + token_strings: list[list[str]], + values: list[np.ndarray], + base_values: np.ndarray | None, + folds_case: bool | None = None, +) -> NlpExplanation: + """Bare ``NlpExplanation`` for word-importance/case-folding tests — only the token data matters.""" + n = len(token_strings) + texts = pd.Series([""] * n, index=pd.RangeIndex(n)) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=pd.Series([""] * n, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=folds_case, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + ) + - ``compiled=True`` simulates a post-compile() instance with contributions set. - ``compiled=False`` simulates a pre-compile() instance (contributions=None). +def _make_explainer() -> NlpExplainer: + """Bare ``NlpExplainer`` (engine role only), bypassing ``__init__`` to avoid ``shap.Explainer(None)``. + + Per ``fit``/``explain``, the explainer never holds compiled results — tests that need a computed + batch build an :class:`NlpExplanation` directly with :func:`_make_explanation` instead. """ xpl = object.__new__(NlpExplainer) xpl.model = None xpl.label_names = LABEL_NAMES xpl.backend = None - xpl.contributions = None - xpl.texts = None - xpl.y_pred = None - xpl.y_true = None - if compiled: - xpl.texts = pd.Series( - ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], - index=pd.RangeIndex(3), - ) - xpl.contributions = _make_contributions() - xpl.y_pred = pd.Series(["joy", "sadness", "joy"], index=pd.RangeIndex(3), name="prediction") return xpl # --------------------------------------------------------------------------- -# NlpContributions +# NlpExplanation.word_importance / resolve_lowercase # --------------------------------------------------------------------------- -class TestNlpContributions(unittest.TestCase): +class TestWordImportance(unittest.TestCase): def setUp(self): - self.contrib = _make_contributions() + token_strings, values, base_values = _make_token_data() + self.explanation = _minimal_explanation(token_strings, values, base_values) def test_len(self): - self.assertEqual(len(self.contrib), 3) + self.assertEqual(len(self.explanation), 3) def test_word_importance_returns_series(self): - imp = self.contrib.word_importance(label_idx=1) + imp = self.explanation.word_importance(label_idx=1) self.assertIsInstance(imp, pd.Series) def test_word_importance_filters_special_tokens(self): - imp = self.contrib.word_importance(label_idx=0, filter_special=True) + imp = self.explanation.word_importance(label_idx=0, filter_special=True) self.assertNotIn("", imp.index) self.assertNotIn(" ", imp.index) def test_word_importance_keeps_special_when_disabled(self): - imp = self.contrib.word_importance(label_idx=0, filter_special=False) + imp = self.explanation.word_importance(label_idx=0, filter_special=False) # Empty strings (BOS/EOS) should now be present self.assertIn("", imp.index) def test_word_importance_hides_punctuation_by_default(self): - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["great", "!", "!", "movie", ".", ","]], values=[np.array([[3.0], [9.0], [9.0], [2.0], [8.0], [7.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20) + imp = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(sorted(imp.index), ["great", "movie"]) def test_word_importance_keeps_punctuation_when_asked(self): - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["great", "!", "!"]], values=[np.array([[3.0], [9.0], [9.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20, filter_punctuation=False) + imp = explanation.word_importance(label_idx=0, n_top=20, filter_punctuation=False) self.assertIn("!", imp.index) # Punctuation kept means it can outrank real words, which is why it is hidden by default. self.assertEqual(imp.index[0], "!") def test_word_importance_keeps_words_containing_punctuation(self): # Only *pure* punctuation units are dropped — a hyphenated or apostrophised word stays. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["state-of-the-art", "don't", "-"]], values=[np.array([[3.0], [2.0], [9.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20) + imp = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(sorted(imp.index), ["don't", "state-of-the-art"]) def test_resolve_lowercase_follows_the_model(self): - contrib = _make_contributions() for folds_case, expected in ((True, True), (False, False), (None, True)): with self.subTest(folds_case=folds_case): - contrib.folds_case = folds_case - self.assertEqual(contrib.resolve_lowercase(), expected) + explanation = replace(self.explanation, folds_case=folds_case) + self.assertEqual(explanation.resolve_lowercase(), expected) def test_resolve_lowercase_explicit_argument_wins(self): - contrib = _make_contributions() - contrib.folds_case = True - self.assertFalse(contrib.resolve_lowercase(False)) - contrib.folds_case = False - self.assertTrue(contrib.resolve_lowercase(True)) + self.assertFalse(replace(self.explanation, folds_case=True).resolve_lowercase(False)) + self.assertTrue(replace(self.explanation, folds_case=False).resolve_lowercase(True)) def test_word_importance_keeps_case_on_a_cased_model(self): # A cased tokenizer encodes AWFUL and awful to *different* ids, so they are different # inputs with genuinely different attributions — merging them would hide that. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["AWFUL", "awful"]], values=[np.array([[-9.0], [-3.0]])], base_values=np.zeros((1, 1)), folds_case=False, ) - imp = contrib.word_importance(label_idx=0, n_top=20) + imp = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) def test_word_importance_merges_case_on_an_uncased_model(self): # An uncased tokenizer maps both spellings to one input id — the model cannot tell them # apart, so they must not appear as two rows. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["AWFUL", "awful"]], values=[np.array([[-9.0], [-3.0]])], base_values=np.zeros((1, 1)), folds_case=True, ) - imp = contrib.word_importance(label_idx=0, n_top=20) + imp = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(list(imp.index), ["awful"]) self.assertAlmostEqual(imp["awful"], -6.0) def test_word_importance_merges_case_variants_by_default(self): # One row per word, averaged over every casing — not three rows of one occurrence each. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["AWFUL", "Awful", "awful", "good"]], values=[np.array([[-9.0], [-3.0], [-3.0], [1.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20) + imp = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(sorted(imp.index), ["awful", "good"]) self.assertAlmostEqual(imp["awful"], -5.0) def test_word_importance_case_fragmentation_lets_rare_variants_outrank(self): # The reason lowercasing is the default: a single capitalised occurrence keeps its own # extreme value instead of being averaged into the 3 common ones, and tops the ranking. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["TERRIBLE", *["terrible"] * 9, "dull"]], values=[np.array([[-9.0], *[[-0.1]] * 9, [-2.0]])], base_values=np.zeros((1, 1)), ) - cased = contrib.word_importance(label_idx=0, n_top=20, lowercase=False) + cased = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) self.assertEqual(cased.index[0], "TERRIBLE") - folded = contrib.word_importance(label_idx=0, n_top=20) + folded = explanation.word_importance(label_idx=0, n_top=20) self.assertEqual(folded.index[0], "dull") def test_word_importance_keeps_case_when_disabled(self): - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["AWFUL", "awful"]], values=[np.array([[-9.0], [-3.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20, lowercase=False) + imp = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) def test_word_importance_exclusion_is_case_insensitive_when_folding(self): # The webapp's dropdown offers lowercase entries; excluding one must drop every casing. - contrib = NlpContributions( + explanation = _minimal_explanation( token_strings=[["AWFUL", "awful", "good"]], values=[np.array([[-9.0], [-3.0], [1.0]])], base_values=np.zeros((1, 1)), ) - imp = contrib.word_importance(label_idx=0, n_top=20, exclude_words={"AwFuL"}) + imp = explanation.word_importance(label_idx=0, n_top=20, exclude_words={"AwFuL"}) self.assertEqual(list(imp.index), ["good"]) def test_word_importance_respects_n_top(self): - imp = self.contrib.word_importance(label_idx=1, n_top=3) + imp = self.explanation.word_importance(label_idx=1, n_top=3) self.assertLessEqual(len(imp), 3) def test_word_importance_sorted_by_absolute_value(self): - imp = self.contrib.word_importance(label_idx=2, n_top=20) + imp = self.explanation.word_importance(label_idx=2, n_top=20) abs_vals = imp.abs().tolist() self.assertEqual(abs_vals, sorted(abs_vals, reverse=True)) def test_word_importance_aggregates_repeated_words(self): # "feel" only appears once; check it has a single contribution value - imp = self.contrib.word_importance(label_idx=1, n_top=20, filter_special=True) + imp = self.explanation.word_importance(label_idx=1, n_top=20, filter_special=True) self.assertIn("feel", imp.index) # Should be a scalar (mean of one occurrence) self.assertIsInstance(imp["feel"], float) def test_word_importance_all_labels(self): for idx in range(N_CLASSES): - imp = self.contrib.word_importance(label_idx=idx) + imp = self.explanation.word_importance(label_idx=idx) self.assertIsInstance(imp, pd.Series) self.assertGreater(len(imp), 0) def test_word_importance_filter_sign_positive(self): - imp = self.contrib.word_importance(label_idx=0, filter_sign="positive") + imp = self.explanation.word_importance(label_idx=0, filter_sign="positive") if len(imp) > 0: self.assertTrue((imp > 0).all(), "positive filter should return only positive values") def test_word_importance_filter_sign_negative(self): - imp = self.contrib.word_importance(label_idx=0, filter_sign="negative") + imp = self.explanation.word_importance(label_idx=0, filter_sign="negative") if len(imp) > 0: self.assertTrue((imp < 0).all(), "negative filter should return only negative values") def test_word_importance_exclude_words(self): - imp_full = self.contrib.word_importance(label_idx=1, filter_special=True) + imp_full = self.explanation.word_importance(label_idx=1, filter_special=True) if len(imp_full) == 0: return word_to_exclude = imp_full.index[0] - imp_filtered = self.contrib.word_importance(label_idx=1, filter_special=True, exclude_words={word_to_exclude}) + imp_filtered = self.explanation.word_importance( + label_idx=1, filter_special=True, exclude_words={word_to_exclude} + ) self.assertNotIn(word_to_exclude, imp_filtered.index) def test_word_importance_exclude_words_empty_set(self): - imp_no_exclude = self.contrib.word_importance(label_idx=1, exclude_words=set()) - imp_none_exclude = self.contrib.word_importance(label_idx=1, exclude_words=None) + imp_no_exclude = self.explanation.word_importance(label_idx=1, exclude_words=set()) + imp_none_exclude = self.explanation.word_importance(label_idx=1, exclude_words=None) pd.testing.assert_series_equal(imp_no_exclude, imp_none_exclude) def test_word_importance_sample_indices(self): - imp = self.contrib.word_importance(label_idx=0, sample_indices=[0], n_top=50) + imp = self.explanation.word_importance(label_idx=0, sample_indices=[0], n_top=50) self.assertGreater(len(imp), 0) # "terrible" only exists in sample 1 — must not appear in the sample-0 subset self.assertNotIn("terrible", imp.index) @@ -550,9 +596,10 @@ def test_custom_title(self): class TestNlpExplainer(unittest.TestCase): def setUp(self): self.xpl = _make_explainer() + self.explanation = _make_explanation() - def test_text_plot_returns_figure(self): - fig = self.xpl.text_plot(pos=0, label_idx=1) + def test_plot_tokens_returns_figure(self): + fig = self.explanation.plot.tokens(row=0, label_idx=1) self.assertIsInstance(fig, go.Figure) def test_folds_case_is_none_without_a_tokenizing_model(self): @@ -573,41 +620,30 @@ def detokenize(self, tokens): self.xpl._text_model = _Uncased(label_names=["neg", "pos"]) self.assertTrue(self.xpl._folds_case()) - def test_text_plot_all_samples(self): - for pos in range(3): - fig = self.xpl.text_plot(pos=pos, label_idx=0) + def test_plot_tokens_all_samples(self): + for row in range(3): + fig = self.explanation.plot.tokens(row=row, label_idx=0) self.assertIsInstance(fig, go.Figure) - def test_text_plot_all_labels(self): + def test_plot_tokens_all_labels(self): for label_idx in range(N_CLASSES): - fig = self.xpl.text_plot(pos=0, label_idx=label_idx) + fig = self.explanation.plot.tokens(row=0, label_idx=label_idx) self.assertIsInstance(fig, go.Figure) - def test_text_plot_max_tokens(self): - fig = self.xpl.text_plot(pos=0, label_idx=1, max_tokens=3) + def test_plot_tokens_max_tokens(self): + fig = self.explanation.plot.tokens(row=0, label_idx=1, max_tokens=3) self.assertIsInstance(fig, go.Figure) self.assertLessEqual(len(fig.data[0].x), 3) - def test_text_plot_raises_before_compile(self): - xpl = _make_explainer(compiled=False) - with self.assertRaises(RuntimeError): - xpl.text_plot(pos=0) - - def test_run_app_raises_before_compile(self): - xpl = _make_explainer(compiled=False) - with self.assertRaises(RuntimeError): - xpl.run_app() - def test_y_pred_stored(self): - self.assertIsNotNone(self.xpl.y_pred) - self.assertEqual(len(self.xpl.y_pred), 3) + self.assertIsNotNone(self.explanation.y_pred) + self.assertEqual(len(self.explanation.y_pred), 3) def test_label_names_propagated(self): - self.assertEqual(self.xpl.contributions.label_names, LABEL_NAMES) + self.assertEqual(self.explanation.label_names, LABEL_NAMES) def test_y_true_is_none_by_default(self): - xpl = _make_explainer(compiled=True) - self.assertIsNone(xpl.y_true) + self.assertIsNone(self.explanation.y_true) class _ProjectableModel(TextModel, SupportsEmbeddings): @@ -651,80 +687,151 @@ def fit_transform(self, x): return np.asarray(x)[:, :2] * self.scale +def _projection_explanation(texts: pd.Series) -> NlpExplanation: + """A minimal ``NlpExplanation`` carrying only ``texts`` — all ``compute_projection`` needs.""" + n = len(texts) + return NlpExplanation( + texts=texts, + token_strings=[[] for _ in range(n)], + values=[np.zeros((0, 2)) for _ in range(n)], + base_values=np.zeros((n, 2)), + y_pred=pd.Series(["pos"] * n, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=None, + backend_name="test", + is_additive=True, + reference_kind="none", + ) + + class TestComputeProjection(unittest.TestCase): """The library owns the space + the caching; the caller injects only the reducer.""" def setUp(self): self.model = _ProjectableModel() self.xpl = NlpExplainer(self.model, backend=object()) - self.xpl.texts = pd.Series(["alpha", "beta banana", "gamma"]) + self.explanation = _projection_explanation(pd.Series(["alpha", "beta banana", "gamma"])) def test_returns_two_columns_aligned_with_the_texts(self): - xy = self.xpl.compute_projection() + xy = self.xpl.compute_projection(self.explanation) self.assertEqual(xy.shape, (3, 2)) def test_defaults_to_pca_without_any_extra_dependency(self): """The default reducer must be something a core install already has — sklearn's PCA.""" - xy = self.xpl.compute_projection() + xy = self.xpl.compute_projection(self.explanation) self.assertEqual(xy.shape, (3, 2)) self.assertEqual(self.model.calls, 1) def test_injected_reducer_is_used(self): reducer = _CountingReducer(scale=2.0) - xy = self.xpl.compute_projection(reducer=reducer) + xy = self.xpl.compute_projection(self.explanation, reducer=reducer) self.assertEqual(reducer.fits, 1) np.testing.assert_allclose(xy[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled - def test_raises_before_compile(self): - xpl = NlpExplainer(_ProjectableModel(), backend=object()) - with self.assertRaises(RuntimeError): - xpl.compute_projection() - def test_raises_for_a_model_that_cannot_embed(self): """A prediction-only model gets a clear error pointing at the escape hatch, not an AttributeError.""" xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) - xpl.texts = pd.Series(["alpha", "beta"]) + explanation = _projection_explanation(pd.Series(["alpha", "beta"])) with self.assertRaises(TypeError): - xpl.compute_projection() + xpl.compute_projection(explanation) def test_cached_across_instances(self): with tempfile.TemporaryDirectory() as d: reducer = _CountingReducer() - self.xpl.compute_projection(reducer=reducer, cache_dir=d) + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) fresh_model = _ProjectableModel() fresh = NlpExplainer(fresh_model, backend=object()) - fresh.texts = self.xpl.texts - fresh.compute_projection(reducer=reducer, cache_dir=d) + fresh.compute_projection(self.explanation, reducer=reducer, cache_dir=d) self.assertEqual(fresh_model.calls, 0) # neither embedded self.assertEqual(reducer.fits, 1) # nor re-fitted def test_reducer_settings_take_part_in_the_key(self): """Re-tuning a reducer must not silently reload the previous scatter.""" with tempfile.TemporaryDirectory() as d: - a = self.xpl.compute_projection(reducer=_CountingReducer(scale=1.0), cache_dir=d) - b = self.xpl.compute_projection(reducer=_CountingReducer(scale=3.0), cache_dir=d) + a = self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(scale=1.0), cache_dir=d) + b = self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(scale=3.0), cache_dir=d) np.testing.assert_allclose(b, a * 3.0) def test_model_space_takes_part_in_the_key(self): """Moving the model's space must re-project, not reload the other space's coordinates.""" with tempfile.TemporaryDirectory() as d: - self.xpl.compute_projection(reducer=_CountingReducer(), cache_dir=d) + self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) self.model.space = "pooled" self.model.calls = 0 - self.xpl.compute_projection(reducer=_CountingReducer(), cache_dir=d) + self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) self.assertEqual(self.model.calls, 1) # re-embedded under the new space def test_recompute_forces_a_fresh_fit(self): with tempfile.TemporaryDirectory() as d: reducer = _CountingReducer() - self.xpl.compute_projection(reducer=reducer, cache_dir=d) + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) self.model.calls = 0 - self.xpl.compute_projection(reducer=reducer, cache_dir=d, recompute=True) + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, recompute=True) self.assertEqual(self.model.calls, 1) self.assertEqual(reducer.fits, 2) +# --------------------------------------------------------------------------- +# NlpExplainer.fit — reference state, and when its cost is paid +# --------------------------------------------------------------------------- + + +class _UnembeddableModel(_ProjectableModel): + """Embeds nothing: stands in for a corpus the model chokes on (OOM, bad encoding).""" + + def embed(self, texts, space=None): + raise RuntimeError("cannot embed this corpus") + + +class TestFitPrecompute(unittest.TestCase): + CORPUS = ["a happy line", "a sad line", "another happy one"] + LABELS = ["joy", "sadness", "joy"] + + def test_precompute_embeds_the_bank_at_fit(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) + self.assertEqual(model.calls, 1) # the corpus, embedded once, inside fit + xpl.find_similar("a happy line") + self.assertEqual(model.calls, 2) # the query only — the bank was already there + + def test_precompute_false_defers_to_first_query(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS, precompute=False) + self.assertEqual(model.calls, 0) + xpl.find_similar("a happy line") + self.assertEqual(model.calls, 2) # bank + query, both charged to the first click + + def test_find_similar_threshold_filters_by_score_and_reports_total(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) + neighbors, total = xpl.find_similar_threshold("a happy line", threshold=-1.0, limit=1) + self.assertLessEqual(len(neighbors), 1) + self.assertGreaterEqual(total, len(neighbors)) + + def test_find_similar_threshold_requires_a_retriever(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) + with self.assertRaises(RuntimeError): + xpl.find_similar_threshold("a happy line") + + def test_precompute_is_a_noop_when_no_retriever_was_built(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) + self.assertFalse(xpl.can_find_similar()) # model cannot embed + self.assertTrue(xpl.can_probe_labels()) # ... but the model-free probe still fit + + def test_a_bank_failure_still_leaves_the_model_free_probe_usable(self): + xpl = NlpExplainer(_UnembeddableModel(), backend=object()) + with self.assertRaises(RuntimeError): + xpl.fit(self.CORPUS, y=self.LABELS) + # reference_/classes_ are assigned before the bank is built, so the half-fitted object keeps + # the feature that never needed the model. + self.assertEqual(xpl.reference_, (self.CORPUS, self.LABELS)) + self.assertEqual(xpl.classes_, LABEL_NAMES[:2]) # from the model, not derived from y + self.assertTrue(xpl.can_probe_labels()) + + # --------------------------------------------------------------------------- # NlpExplainer — counterfactual generator discovery / selection (captum-free) # --------------------------------------------------------------------------- @@ -838,7 +945,8 @@ def test_no_generators_without_text_model(self): class TestNlpWebApp(unittest.TestCase): def setUp(self): self.xpl = _make_explainer() - self.webapp = NlpWebApp(self.xpl) + self.explanation = _make_explanation() + self.webapp = NlpWebApp(self.explanation, engine=self.xpl) def test_layout_built(self): self.assertIsNotNone(self.webapp.app.layout) @@ -866,11 +974,6 @@ def test_dataset_table_populated(self): self.assertIn("text", table.rowData[0]) self.assertIn("prediction", table.rowData[0]) - def test_raises_without_compile(self): - xpl = _make_explainer(compiled=False) - with self.assertRaises(RuntimeError): - NlpWebApp(xpl) - def test_graph_ids_present(self): ids = self._collect_ids(self.webapp.app.layout) self.assertIn("global-importance-graph", ids) @@ -902,9 +1005,9 @@ def test_dataset_table_no_ground_truth_by_default(self): self.assertNotIn("ground_truth", col_fields) def test_dataset_table_with_y_true(self): - xpl = _make_explainer(compiled=True) - xpl.y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") - webapp = NlpWebApp(xpl) + y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") + explanation = _make_explanation(y_true=y_true) + webapp = NlpWebApp(explanation, engine=self.xpl) table = self._find_component(webapp.app.layout, "dataset-table") col_fields = [c["field"] for c in table.columnDefs] self.assertIn("ground_truth", col_fields) @@ -920,25 +1023,23 @@ def test_scatter_absent_when_no_xy(self): self.assertNotIn("color-by", ids) def test_scatter_present_when_xy_given(self): - xpl = _make_explainer(compiled=True) xy = np.zeros((3, 2)) - webapp = NlpWebApp(xpl, scatter_xy=xy) + webapp = NlpWebApp(self.explanation, engine=self.xpl, scatter_xy=xy) ids = self._collect_ids(webapp.app.layout) self.assertIn("scatter-plot", ids) self.assertIn("color-by", ids) def test_scatter_wrong_shape_raises(self): - xpl = _make_explainer(compiled=True) with self.assertRaises(ValueError): - NlpWebApp(xpl, scatter_xy=np.zeros((5, 2))) # 5 rows but only 3 samples + NlpWebApp(self.explanation, engine=self.xpl, scatter_xy=np.zeros((5, 2))) # 5 rows but only 3 samples # ── Error Analysis tab (confusion matrix) ───────────────────────── def _make_webapp_with_gt(self) -> NlpWebApp: # y_pred is ["joy", "sadness", "joy"]; make sample 0 a sadness→joy error, others correct. - xpl = _make_explainer(compiled=True) - xpl.y_true = pd.Series(["sadness", "sadness", "joy"], index=pd.RangeIndex(3), name="ground_truth") - return NlpWebApp(xpl) + y_true = pd.Series(["sadness", "sadness", "joy"], index=pd.RangeIndex(3), name="ground_truth") + explanation = _make_explanation(y_true=y_true) + return NlpWebApp(explanation, engine=self.xpl) def test_error_analysis_absent_without_ground_truth(self): ids = self._collect_ids(self.webapp.app.layout) @@ -954,9 +1055,13 @@ def test_error_analysis_present_with_ground_truth(self): for cid in ("confusion-matrix-graph", "error-pred-importance", "error-true-importance", "cm-normalize"): self.assertIn(cid, ids) + @staticmethod + def _error_analysis_comp(webapp: NlpWebApp) -> ErrorAnalysisComponent: + return next(c for c in webapp._components if isinstance(c, ErrorAnalysisComponent)) + def test_confusion_matrix_counts(self): webapp = self._make_webapp_with_gt() - cm = webapp._cm + cm = self._error_analysis_comp(webapp)._cm # LABEL_NAMES index: sadness=0, joy=1. Rows=true, cols=pred. self.assertEqual(cm[0, 1], 1) # true sadness predicted joy (the error) self.assertEqual(cm[0, 0], 1) # true sadness predicted sadness @@ -965,8 +1070,9 @@ def test_confusion_matrix_counts(self): def test_confusion_matrix_index_arrays(self): webapp = self._make_webapp_with_gt() - self.assertEqual(webapp._cm_true_idx.tolist(), [0, 0, 1]) # sadness, sadness, joy - self.assertEqual(webapp._cm_pred_idx.tolist(), [1, 0, 1]) # joy, sadness, joy + comp = self._error_analysis_comp(webapp) + self.assertEqual(comp._cm_true_idx.tolist(), [0, 0, 1]) # sadness, sadness, joy + self.assertEqual(comp._cm_pred_idx.tolist(), [1, 0, 1]) # joy, sadness, joy def test_confusion_matrix_figure_customdata_orientation(self): webapp = self._make_webapp_with_gt() @@ -1148,17 +1254,17 @@ def hf_model(texts): # --- run_explainer --- - def test_run_explainer_returns_raw_explanation(self): + def test_run_explainer_returns_nlp_contributions(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertIsInstance(raw, NlpRawExplanation) + self.assertIsInstance(raw, NlpContributions) def test_run_explainer_contributions_count(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertEqual(len(raw.contributions), len(_SAMPLE_TEXTS)) + self.assertEqual(len(raw.values), len(_SAMPLE_TEXTS)) def test_run_explainer_contributions_shape(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) - for arr in raw.contributions: + for arr in raw.values: self.assertEqual(arr.ndim, 2) self.assertEqual(arr.shape[1], N_CLASSES) @@ -1168,15 +1274,15 @@ def test_run_explainer_base_values_shape(self): def test_run_explainer_data_is_word_list(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertEqual(len(raw.data), len(_SAMPLE_TEXTS)) - for word_list in raw.data: + self.assertEqual(len(raw.token_strings), len(_SAMPLE_TEXTS)) + for word_list in raw.token_strings: self.assertIsInstance(word_list, list) self.assertTrue(all(isinstance(w, str) for w in word_list)) def test_run_explainer_sparse_weights(self): # LIME fills at most num_features non-zero weights per label column. raw = self.backend.run_explainer(_SAMPLE_TEXTS[:1]) - matrix = raw.contributions[0] + matrix = raw.values[0] for col in range(N_CLASSES): self.assertLessEqual( np.count_nonzero(matrix[:, col]), @@ -1193,12 +1299,12 @@ def test_get_local_contributions_returns_nlp_contributions(self): def test_get_local_contributions_token_strings_match_data(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) - self.assertEqual(contrib.token_strings, raw.data) + self.assertEqual(contrib.token_strings, raw.token_strings) def test_get_local_contributions_values_match_contributions(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) - for got, expected in zip(contrib.values, raw.contributions): + for got, expected in zip(contrib.values, raw.values): np.testing.assert_array_equal(got, expected) def test_get_local_contributions_subset(self): @@ -1215,9 +1321,9 @@ def test_get_local_contributions_subset(self): class TestNlpExplainerWithLimeBackend(unittest.TestCase): - def _make_explainer_lime(self, compiled: bool = True) -> NlpExplainer: - """NlpExplainer backed by NlpLimeBackend, with synthetic contributions.""" - xpl = _make_explainer(compiled=compiled) + def _make_explainer_lime(self) -> NlpExplainer: + """NlpExplainer backed by NlpLimeBackend.""" + xpl = _make_explainer() xpl.backend = _make_lime_backend() return xpl @@ -1225,17 +1331,14 @@ def test_backend_is_lime_instance(self): xpl = self._make_explainer_lime() self.assertIsInstance(xpl.backend, NlpLimeBackend) - def test_text_plot_works_with_lime_backend(self): - xpl = self._make_explainer_lime(compiled=True) - fig = xpl.text_plot(pos=0, label_idx=0) - self.assertIsInstance(fig, go.Figure) - - def test_text_plot_raises_before_compile(self): - xpl = self._make_explainer_lime(compiled=False) - with self.assertRaises(RuntimeError): - xpl.text_plot(pos=0) + def test_plot_tokens_works_for_a_lime_explanation(self): + # LIME is non-additive, so .plot.waterfall refuses; .plot.tokens stays available. + explanation = replace(_make_explanation(), backend_name="nlp_lime", is_additive=False) + self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) + with self.assertRaises(ValueError): + explanation.plot.waterfall(row=0, label_idx=0) - def test_compile_sets_contributions(self): + def test_explain_sets_contributions(self): backend = _make_lime_backend() xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) fake_pred_df = pd.DataFrame( @@ -1243,23 +1346,21 @@ def test_compile_sets_contributions(self): index=pd.RangeIndex(len(_SAMPLE_TEXTS)), ) with patch.object(xpl, "_predict", return_value=fake_pred_df): - xpl.compile(_SAMPLE_TEXTS) - self.assertIsInstance(xpl.contributions, NlpContributions) - self.assertEqual(len(xpl.contributions), len(_SAMPLE_TEXTS)) - self.assertEqual(xpl.contributions.label_names, LABEL_NAMES) + explanation = xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(len(explanation), len(_SAMPLE_TEXTS)) + self.assertEqual(explanation.label_names, LABEL_NAMES) - def test_compile_caching_skips_rerun(self): + def test_explain_caching_skips_rerun(self): backend = _make_lime_backend() xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) fake_pred_df = pd.DataFrame( {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, index=pd.RangeIndex(len(_SAMPLE_TEXTS)), ) - with patch.object(xpl, "_predict", return_value=fake_pred_df): - xpl.compile(_SAMPLE_TEXTS) - hash_after_first = xpl._data_hash - xpl.compile(_SAMPLE_TEXTS) # same data — must hit in-memory cache - self.assertEqual(xpl._data_hash, hash_after_first) + with patch.object(xpl, "_predict", return_value=fake_pred_df) as mocked_predict: + xpl.explain(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) # same data — must hit in-memory cache + self.assertEqual(mocked_predict.call_count, 1, "memoization must skip the second _predict call") # --------------------------------------------------------------------------- @@ -1291,6 +1392,8 @@ class _MarkerBackend(NlpBackend): """ name = "marker_backend" + reference_kind = "none" + is_additive = True def __init__(self, marker=1.0, **kwargs): super().__init__(model=None, label_names=LABEL_NAMES, **kwargs) @@ -1300,10 +1403,10 @@ def __init__(self, marker=1.0, **kwargs): def run_explainer(self, x): self.calls += 1 texts = list(x) - return NlpRawExplanation( - contributions=[np.full((2, N_CLASSES), self.marker) for _ in texts], + return NlpContributions( + token_strings=[["a", "b"] for _ in texts], + values=[np.full((2, N_CLASSES), self.marker) for _ in texts], base_values=np.zeros((len(texts), N_CLASSES)), - data=[["a", "b"] for _ in texts], ) @@ -1321,29 +1424,84 @@ def _explainer(model=None, backend=None, label_names=LABEL_NAMES): ) -class TestCompileCacheKey(unittest.TestCase): - """``compile`` results depend on *(texts, model, backend)* — so all three must be in the key. +class TestExplainCacheKey(unittest.TestCase): + """``explain`` results depend on *(texts, model, backend)* — so all three must be in the key. Keying on the texts alone means swapping the model or the attribution backend and pointing at the same ``cache_dir`` silently reloads the previous run's contributions, and even without a - ``cache_dir`` the in-memory guard turns a re-``compile`` into a no-op. + ``cache_dir`` the in-memory guard turns a re-``explain`` into a no-op. """ def test_identical_inputs_still_hit_the_in_memory_cache(self): backend = _MarkerBackend() xpl = _explainer(backend=backend) - xpl.compile(_SAMPLE_TEXTS) - xpl.compile(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) self.assertEqual(backend.calls, 1, "memoization must survive the richer key") + def test_the_memo_cannot_be_poisoned_through_a_returned_artifact(self): + """``explain`` returns a ``replace()`` of the memoized artifact — a *shallow* copy. + + The two artifacts therefore share their contribution arrays, so before the arrays were + sealed an in-place edit of the first return value silently rewrote the cache, and every + later ``explain`` of the same texts served the corrupted numbers as a fresh result. The + seal turns that into an error at the point of the edit. + """ + backend = _MarkerBackend(marker=1.0) + xpl = _explainer(backend=backend) + first = xpl.explain(_SAMPLE_TEXTS) + + with self.assertRaises(ValueError): + first.values[0][0] = -42.0 + + second = xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(backend.calls, 1, "still a memo hit — the seal must not defeat caching") + np.testing.assert_allclose(second.values[0], 1.0) + + def test_a_memo_hit_is_relabelled_onto_the_caller_index(self): + """The memo is keyed by text *content*, so the same texts from a differently-indexed + source is a hit — and the cached predictions still carry the first caller's index. + ``replace`` says nothing about the fields it is not handed, so every index-bearing field + has to be re-labelled, not just ``texts``. + """ + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.explain(pd.Series(_SAMPLE_TEXTS)) # first call: default RangeIndex + + index = pd.Index([77 + i for i in range(len(_SAMPLE_TEXTS))]) + second = xpl.explain(pd.Series(_SAMPLE_TEXTS, index=index)) + + self.assertEqual(backend.calls, 1, "re-labelling must not cost a recompute") + for field_name in ("texts", "y_pred", "y_prob"): + with self.subTest(field=field_name): + self.assertTrue(getattr(second, field_name).index.equals(index)) + + def test_a_y_series_that_does_not_match_x_is_refused(self): + """Reindexing it silently would pair labels positionally while looking like alignment.""" + xpl = _explainer(backend=_MarkerBackend()) + texts = pd.Series(_SAMPLE_TEXTS, index=[10 + i for i in range(len(_SAMPLE_TEXTS))]) + misaligned = pd.Series(["joy"] * len(_SAMPLE_TEXTS), index=range(len(_SAMPLE_TEXTS))) + with self.assertRaises(ValueError) as ctx: + xpl.explain(texts, y=misaligned) + self.assertIn("indexed differently", str(ctx.exception)) + + def test_a_y_series_aligned_to_x_is_kept_and_a_list_is_indexed_positionally(self): + xpl = _explainer(backend=_MarkerBackend()) + index = pd.Index([10 + i for i in range(len(_SAMPLE_TEXTS))]) + texts = pd.Series(_SAMPLE_TEXTS, index=index) + labels = ["joy"] * len(_SAMPLE_TEXTS) + for tag, y in (("series", pd.Series(labels, index=index)), ("list", labels)): + with self.subTest(y=tag): + self.assertTrue(xpl.explain(texts, y=y).y_true.index.equals(index)) + def test_swapping_the_backend_recomputes(self): xpl = _explainer(backend=_MarkerBackend(marker=1.0)) - xpl.compile(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) replacement = _OtherMarkerBackend(marker=2.0) xpl.backend = replacement - xpl.compile(_SAMPLE_TEXTS) + explanation = xpl.explain(_SAMPLE_TEXTS) self.assertEqual(replacement.calls, 1, "in-memory guard served a stale, other-backend result") - np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + np.testing.assert_allclose(explanation.values[0], 2.0) def test_backend_compute_args_are_part_of_the_key(self): xpl = _explainer(backend=_MarkerBackend(explainer_compute_args={"n_steps": 50})) @@ -1378,131 +1536,128 @@ def test_key_is_order_sensitive(self): self.assertNotEqual(xpl._compute_key(["a", "b"]), xpl._compute_key(["b", "a"])) -class TestCompileDiskCacheIsolation(unittest.TestCase): +class TestExplainDiskCacheIsolation(unittest.TestCase): """The disk cache is the dangerous case: a stale entry survives the process that wrote it.""" def test_two_backends_share_a_cache_dir_without_collision(self): with tempfile.TemporaryDirectory() as cache_dir: first = _MarkerBackend(marker=1.0) - _explainer(backend=first).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + _explainer(backend=first).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) second = _OtherMarkerBackend(marker=2.0) xpl = _explainer(backend=second) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) self.assertEqual(second.calls, 1, "loaded the other backend's cached contributions") - np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + np.testing.assert_allclose(explanation.values[0], 2.0) def test_two_models_share_a_cache_dir_without_collision(self): with tempfile.TemporaryDirectory() as cache_dir: - _explainer(model=_KeyModel("model-a"), backend=_MarkerBackend(marker=1.0)).compile( + _explainer(model=_KeyModel("model-a"), backend=_MarkerBackend(marker=1.0)).explain( _SAMPLE_TEXTS, cache_dir=cache_dir ) backend_b = _MarkerBackend(marker=2.0) xpl = _explainer(model=_KeyModel("model-b"), backend=backend_b) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) self.assertEqual(backend_b.calls, 1, "loaded the other model's cached contributions") - np.testing.assert_allclose(xpl.contributions.values[0], 2.0) + np.testing.assert_allclose(explanation.values[0], 2.0) def test_same_model_and_backend_reload_from_disk(self): # The cache must still *work* — a fresh instance skips the expensive run. with tempfile.TemporaryDirectory() as cache_dir: - _explainer(backend=_MarkerBackend(marker=3.0)).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + _explainer(backend=_MarkerBackend(marker=3.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) reloaded_backend = _MarkerBackend(marker=3.0) xpl = _explainer(backend=reloaded_backend) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) self.assertEqual(reloaded_backend.calls, 0, "disk cache did not hit for identical inputs") - np.testing.assert_allclose(xpl.contributions.values[0], 3.0) + np.testing.assert_allclose(explanation.values[0], 3.0) - def test_cache_path_points_at_the_file_compile_writes(self): + def test_cache_path_points_at_the_file_explain_writes(self): with tempfile.TemporaryDirectory() as cache_dir: xpl = _explainer() path = xpl.cache_path(_SAMPLE_TEXTS, cache_dir) self.assertFalse(path.exists()) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) - self.assertTrue(path.exists(), "cache_path disagrees with where compile() wrote") + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertTrue(path.exists(), "cache_path disagrees with where explain() wrote") def test_clear_cache_forces_a_recompute(self): with tempfile.TemporaryDirectory() as cache_dir: backend = _MarkerBackend() xpl = _explainer(backend=backend) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) xpl.clear_cache(_SAMPLE_TEXTS, cache_dir) self.assertFalse(xpl.cache_path(_SAMPLE_TEXTS, cache_dir).exists()) - xpl.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) self.assertEqual(backend.calls, 2, "clear_cache must defeat the in-memory guard too") def test_clear_cache_leaves_other_backends_entries_intact(self): with tempfile.TemporaryDirectory() as cache_dir: keeper = _MarkerBackend(marker=1.0) - _explainer(backend=keeper).compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + _explainer(backend=keeper).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) keeper_path = _explainer(backend=_MarkerBackend(marker=1.0)).cache_path(_SAMPLE_TEXTS, cache_dir) other = _explainer(backend=_OtherMarkerBackend(marker=2.0)) - other.compile(_SAMPLE_TEXTS, cache_dir=cache_dir) + other.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) other.clear_cache(_SAMPLE_TEXTS, cache_dir) self.assertTrue(keeper_path.exists(), "clearing one backend dropped another's cache") class TestDetectLabelNoise(unittest.TestCase): - """The explainer's confident-learning surface, on the __new__-built stub (no model, no backend).""" + """The explainer's confident-learning surface, over an ``NlpExplanation`` (no model, no backend).""" - def _with_labels(self, y_true=None, y_prob=None): - """A compiled stub carrying ground truth and per-class probabilities. + def _with_labels(self, y_true=None, y_prob=None) -> NlpExplanation: + """A batch carrying ground truth and per-class probabilities. Sample 1 is labelled ``joy`` while the model confidently says ``sadness`` — the planted error. Both classes carry at least one label, without which ``sadness`` would have no estimable threshold and could never be suggested (covered separately in the compute tests). """ - xpl = _make_explainer() confident = ["joy", "sadness", "sadness"] probs = np.full((3, N_CLASSES), 0.02) for row, name in enumerate(confident): probs[row, LABEL_NAMES.index(name)] = 0.90 probs = probs / probs.sum(axis=1, keepdims=True) - xpl.y_pred = pd.Series(confident, index=pd.RangeIndex(3), name="prediction") - xpl.y_prob = pd.DataFrame(probs, index=pd.RangeIndex(3), columns=LABEL_NAMES) if y_prob is None else y_prob - xpl.y_true = ( + y_pred = pd.Series(confident, index=pd.RangeIndex(3), name="prediction") + resolved_y_prob = pd.DataFrame(probs, index=pd.RangeIndex(3), columns=LABEL_NAMES) if y_prob is None else y_prob + resolved_y_true = ( pd.Series(["joy", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") if y_true is None else y_true ) - return xpl + return _make_explanation(y_pred=y_pred, y_prob=resolved_y_prob, y_true=resolved_y_true) # ── capability flag ──────────────────────────────────────────────── def test_available_with_ground_truth_and_per_class_probabilities(self): - self.assertTrue(self._with_labels().can_detect_label_noise()) + self.assertTrue(_make_explainer().can_detect_label_noise(self._with_labels())) def test_unavailable_without_ground_truth(self): - xpl = self._with_labels() - xpl.y_true = None - self.assertFalse(xpl.can_detect_label_noise()) + explanation = replace(self._with_labels(), y_true=None) + self.assertFalse(_make_explainer().can_detect_label_noise(explanation)) def test_unavailable_with_only_the_winning_class_probability(self): # The raw-pipeline path emits a single "probability" column; the losing classes' scores are # precisely what confident learning needs. legacy = pd.DataFrame({"probability": [0.9, 0.9, 0.9]}, index=pd.RangeIndex(3)) - self.assertFalse(self._with_labels(y_prob=legacy).can_detect_label_noise()) + self.assertFalse(_make_explainer().can_detect_label_noise(self._with_labels(y_prob=legacy))) - def test_unavailable_on_a_stub_that_never_set_y_prob(self): - # The __new__ path sets neither attribute; getattr must not raise. - self.assertFalse(_make_explainer().can_detect_label_noise()) + def test_unavailable_on_an_explanation_with_no_probabilities(self): + self.assertFalse(_make_explainer().can_detect_label_noise(_make_explanation())) def test_available_without_a_model(self): - # Unlike the other capability flags this needs no live model, so it survives from_snapshot(). - xpl = self._with_labels() + # Unlike the other capability flags this needs no live model. + xpl = _make_explainer() self.assertIsNone(xpl.model) - self.assertTrue(xpl.can_detect_label_noise()) + self.assertTrue(xpl.can_detect_label_noise(self._with_labels())) # ── detection ────────────────────────────────────────────────────── def test_raises_when_the_prerequisites_are_missing(self): with self.assertRaisesRegex(RuntimeError, "ground-truth labels"): - _make_explainer().detect_label_noise() + _make_explainer().detect_label_noise(_make_explanation()) def test_flags_the_planted_mislabel(self): - report = self._with_labels().detect_label_noise() + report = _make_explainer().detect_label_noise(self._with_labels()) self.assertEqual([i.index for i in report.issues], [1]) issue = report.issues[0] self.assertEqual(issue.given_label, "joy") @@ -1510,25 +1665,29 @@ def test_flags_the_planted_mislabel(self): self.assertEqual(issue.text, "this is terrible and sad") def test_label_names_come_from_the_probability_columns(self): - report = self._with_labels().detect_label_noise() + report = _make_explainer().detect_label_noise(self._with_labels()) self.assertEqual(report.label_names, LABEL_NAMES) self.assertEqual(report.noise_matrix.shape, (N_CLASSES, N_CLASSES)) self.assertEqual(report.n_samples, 3) def test_respects_top_n_and_score(self): - report = self._with_labels().detect_label_noise(top_n=0, score="normalized_margin") + report = _make_explainer().detect_label_noise(self._with_labels(), top_n=0, score="normalized_margin") self.assertEqual(report.issues, []) self.assertEqual(report.n_issues, 1) # ── memoisation ──────────────────────────────────────────────────── def test_repeats_are_served_from_the_memo(self): - xpl = self._with_labels() - first = xpl.detect_label_noise(top_n=5) - self.assertIs(xpl.detect_label_noise(top_n=5), first) + xpl = _make_explainer() + explanation = self._with_labels() + first = xpl.detect_label_noise(explanation, top_n=5) + self.assertIs(xpl.detect_label_noise(explanation, top_n=5), first) def test_different_arguments_recompute(self): - xpl = self._with_labels() - self.assertIsNot(xpl.detect_label_noise(top_n=5), xpl.detect_label_noise(top_n=4)) + xpl = _make_explainer() + explanation = self._with_labels() + self.assertIsNot( + xpl.detect_label_noise(explanation, top_n=5), xpl.detect_label_noise(explanation, top_n=4) + ) # ── independent probe ────────────────────────────────────────────── def _probe_corpus(self): @@ -1547,15 +1706,15 @@ def _probe_corpus(self): return texts, labels def test_no_probe_when_no_reference_corpus_is_bound(self): - xpl = self._with_labels() + xpl = _make_explainer() self.assertFalse(xpl.can_probe_labels()) - self.assertIsNone(xpl.detect_label_noise().issues[0].probe) + self.assertIsNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) def test_probe_verdict_is_attached_when_a_labelled_corpus_is_bound(self): - xpl = self._with_labels() - xpl._reference_corpus = self._probe_corpus() + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() self.assertTrue(xpl.can_probe_labels()) - issue = xpl.detect_label_noise().issues[0] + issue = xpl.detect_label_noise(self._with_labels()).issues[0] self.assertIsNotNone(issue.probe) self.assertIn(issue.probe.top_label, {"joy", "sadness"}) self.assertEqual(issue.probe.backs_given, issue.probe.top_label == issue.given_label) @@ -1564,9 +1723,9 @@ def test_probe_corroborates_a_genuine_label_error(self): # The flagged row is "this is terrible and sad" carrying the label "joy". The reference # corpus puts that vocabulary firmly in "sadness", so the probe rejects the given label too # — the two-signals-agree case, which is the one worth relabelling. - xpl = self._with_labels() - xpl._reference_corpus = self._probe_corpus() - issue = xpl.detect_label_noise().issues[0] + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + issue = xpl.detect_label_noise(self._with_labels()).issues[0] self.assertEqual((issue.given_label, issue.text), ("joy", "this is terrible and sad")) self.assertFalse(issue.probe.backs_given) self.assertEqual(issue.probe.top_label, "sadness") @@ -1576,38 +1735,39 @@ def test_probe_backs_the_label_when_the_corpus_sides_with_it(self): # The mirror case, and the reason the column exists: same flagged row, but a corpus that # calls this vocabulary "joy". The probe now defends the label, marking the row as the # audited model's error rather than the corpus's. - xpl = self._with_labels() + xpl = _make_explainer() texts, _ = self._probe_corpus() - xpl._reference_corpus = (texts, ["sadness"] * 4 + ["joy"] * 4) - issue = xpl.detect_label_noise().issues[0] + xpl.reference_ = (texts, ["sadness"] * 4 + ["joy"] * 4) + issue = xpl.detect_label_noise(self._with_labels()).issues[0] self.assertEqual(issue.given_label, "joy") self.assertTrue(issue.probe.backs_given) self.assertGreater(issue.probe.given_prob, 0.5) def test_probe_is_skipped_when_not_requested(self): - xpl = self._with_labels() - xpl._reference_corpus = self._probe_corpus() - self.assertIsNone(xpl.detect_label_noise(probe=False).issues[0].probe) + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + self.assertIsNone(xpl.detect_label_noise(self._with_labels(), probe=False).issues[0].probe) def test_probe_is_fit_once_and_reused_across_calls(self): - xpl = self._with_labels() - xpl._reference_corpus = self._probe_corpus() - xpl.detect_label_noise(top_n=5) + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + explanation = self._with_labels() + xpl.detect_label_noise(explanation, top_n=5) first = xpl._label_probe self.assertIsNotNone(first) - xpl.detect_label_noise(top_n=4) # different args -> recompute, but the probe is kept + xpl.detect_label_noise(explanation, top_n=4) # different args -> recompute, but the probe is kept self.assertIs(xpl._label_probe, first) def test_probe_needs_no_model_or_retriever(self): # The point of fitting on plain text: a prediction-only pipeline that cannot embed (so # can_find_similar() is False) still gets the second opinion. - xpl = self._with_labels() - xpl._reference_corpus = self._probe_corpus() + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() xpl._retriever = None xpl._text_model = None self.assertFalse(xpl.can_find_similar()) self.assertTrue(xpl.can_probe_labels()) - self.assertIsNotNone(xpl.detect_label_noise().issues[0].probe) + self.assertIsNotNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) if __name__ == "__main__": diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 36f7b3b3..89b2832f 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -16,6 +16,7 @@ from shapash.compute.diagnostics.label_probe import LabelProbe, ProbeVerdict from shapash.compute.generators.base import Counterfactual, IntField, TokenListField from shapash.compute.retrieval.similar_examples import Neighbor +from shapash.explainer.nlp_explanation import NlpExplanation from shapash.webapp.nlp_app import NlpWebApp from shapash.webapp.nlp_components import ( CounterfactualComponent, @@ -23,7 +24,6 @@ LabelNoiseComponent, SimilarExamplesComponent, ) -from shapash.webapp.nlp_view import NlpView LABEL_NAMES = ["neg", "pos"] @@ -40,10 +40,7 @@ def _contributions() -> NlpContributions: token_strings = [["i", "am", "happy"], ["this", "is", "bad"]] values = [np.random.randn(3, 2), np.random.randn(3, 2)] base_values = np.zeros((2, 2)) - c = NlpContributions(token_strings=token_strings, values=values, base_values=base_values) - c.label_names = LABEL_NAMES - c.index = pd.RangeIndex(2) - return c + return NlpContributions(token_strings=token_strings, values=values, base_values=base_values) class FakeEngine: @@ -81,13 +78,34 @@ def __init__( # groups, keep seeing neither the Error Analysis nor the Label Noise tab. self.detect_calls = [] - def can_detect_label_noise(self): + def to_explanation(self) -> NlpExplanation: + """The ``NlpExplanation`` a real ``explain()`` call would have produced for this batch. + + ``NlpWebApp`` holds an explanation, not the engine — this is what tests pass as the first + argument, while ``self`` (the ``FakeEngine``) is passed separately as ``engine=``. + """ + return NlpExplanation( + texts=self.texts, + token_strings=self.contributions.token_strings, + values=self.contributions.values, + base_values=self.contributions.base_values, + y_pred=self.y_pred, + y_prob=self.y_prob, + y_true=self.y_true, + label_names=self.label_names, + folds_case=None, + backend_name="fake", + is_additive=True, + reference_kind="none", + ) + + def can_detect_label_noise(self, explanation=None): return self.y_true is not None def can_probe_labels(self): return self.probe_corpus is not None - def detect_label_noise(self, top_n=50, score="self_confidence", probe=True): + def detect_label_noise(self, explanation, top_n=50, score="self_confidence", probe=True): self.detect_calls.append({"top_n": top_n, "score": score, "probe": probe}) report = detect_label_issues( self.y_prob.to_numpy(dtype=float), @@ -122,6 +140,15 @@ def find_similar(self, text, top_k=5): Neighbor(index=1, score=0.80, text="this is awful", label="neg"), ][:top_k] + def find_similar_threshold(self, text, threshold=0.95, limit=50): + all_neighbors = [ + Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), + Neighbor(index=1, score=0.96, text="so glad today", label="pos"), + Neighbor(index=2, score=0.80, text="this is awful", label="neg"), + ] + matches = [n for n in all_neighbors if n.score > threshold] + return matches[:limit], len(matches) + def available_cf_generators(self): return [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")] @@ -158,10 +185,24 @@ def generate_counterfactuals(self, text, config=None, generator=None): def _collect_ids(node, found): - """Recursively collect all string component ids in a Dash layout tree.""" + """Recursively collect all string component ids in a Dash layout tree. + + Also descends into RadioItems/Checklist ``options[].label`` — components nested there (e.g. a + numeric input inline with its radio button) aren't under ``.children``, but Dash still renders + and wires them, since ``label`` is documented to accept a component. + """ cid = getattr(node, "id", None) if isinstance(cid, str): found.add(cid) + options = getattr(node, "options", None) + if isinstance(options, (list, tuple)): + for opt in options: + label = opt.get("label") if isinstance(opt, dict) else None + if isinstance(label, (list, tuple)): + for item in label: + _collect_ids(item, found) + elif label is not None: + _collect_ids(label, found) children = getattr(node, "children", None) if children is None: return @@ -174,7 +215,7 @@ def _collect_ids(node, found): class TestWhatIfMounting(unittest.TestCase): def _ids(self, engine): - app = NlpWebApp(engine) + app = NlpWebApp(engine.to_explanation(), engine=engine) found = set() _collect_ids(app.app.layout, found) return app, found @@ -210,7 +251,8 @@ def test_counterfactual_method_selector_present_with_multiple_generators(self): self.assertIn("counterfactual-cfg-group-ablation_flip", ids) def test_selector_toggle_callback_registered_with_multiple_generators(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("counterfactual-cfg-group-hotflip.style", outputs) @@ -228,7 +270,8 @@ def test_no_capabilities_hides_whatif_lab(self): self.assertEqual(self._whatif_components(app), []) def test_callbacks_registered_when_mounted(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("data-editor-prob.figure", outputs) self.assertIn("counterfactual-results.children", outputs) @@ -250,7 +293,8 @@ def test_similar_panel_requires_predict(self): self.assertNotIn("similar-topk", ids) def test_similar_callbacks_registered_when_mounted(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=False, can_similar=True)) + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("similar-results.children", outputs) @@ -269,7 +313,7 @@ class TestThreePanelLayout(unittest.TestCase): """The LIT-style three-panel shell: tab groups, mounted bodies, and the current-datapoint store.""" def _ids(self, engine, **kwargs): - app = NlpWebApp(engine, **kwargs) + app = NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) found = set() _collect_ids(app.app.layout, found) return app, found @@ -327,7 +371,8 @@ def test_current_datapoint_store_present(self): self.assertIn("current-datapoint", ids) def test_detail_panels_read_current_datapoint(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) # Highlight and waterfall render off the shared primary-selection store... self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "sentence-highlight.children")) self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "waterfall-graph.figure")) @@ -335,7 +380,8 @@ def test_detail_panels_read_current_datapoint(self): self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "counterfactual-results.children")) def test_current_datapoint_written_by_row_and_editor(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) # The table-selection writer keys off the selected row. self.assertIn(("dataset-table", "selectedRows"), _callback_binding_ids(app, "current-datapoint.data")) # The editor's Predict also writes it (allow_duplicate) — its combined key carries the prob figure. @@ -343,28 +389,31 @@ def test_current_datapoint_written_by_row_and_editor(self): self.assertIn("current-datapoint.data", editor_key) def test_tab_toggle_callbacks_registered(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine, scatter_xy=np.zeros((2, 2))) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("left-tabs-body-table.style", outputs) self.assertIn("lower-right-tabs-body-waterfall.style", outputs) class TestReadContractSeam(unittest.TestCase): - """The app shell reads compiled data only through ``NlpView``; it keeps no raw explainer handle. + """The app shell reads compiled data only from the ``NlpExplanation``; no raw explainer handle. - ``FakeEngine`` exposes the view/engine surface but has no ``explainer`` attribute, so a mounted - app proves the read path is served by the contract alone. These asserts lock the seam so a future - edit cannot silently reintroduce a ``self.explainer`` bypass (see nlp_app engine/view split). + ``FakeEngine`` has no ``explainer`` attribute, so a mounted app proves the read path is served by + the artifact alone. These asserts lock the seam so a future edit cannot silently reintroduce a + ``self.explainer`` bypass, nor let display state accumulate back onto the artifact. """ def test_app_holds_no_raw_explainer_handle(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True)) + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) self.assertFalse(hasattr(app, "explainer")) - def test_app_exposes_view_and_engine_roles(self): + def test_app_exposes_explanation_and_engine_roles(self): engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine) - self.assertIsInstance(app._view, NlpView) # read contract + explanation = engine.to_explanation() + app = NlpWebApp(explanation, engine=engine) + self.assertIs(app._explanation, explanation) # read contract: the artifact itself self.assertIs(app._engine, engine) # live-action contract @@ -378,10 +427,10 @@ def _register(): from shapash.webapp.nlp_components import SimilarExamplesComponent engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) - view = NlpView(engine) + explanation = engine.to_explanation() app = dash.Dash(__name__) comp = SimilarExamplesComponent() - comp.register_callbacks(app, view, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + comp.register_callbacks(app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) return app, engine @staticmethod @@ -398,14 +447,16 @@ def test_layout_shows_layer_caption(self): engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) found = set() - _collect_ids(SimilarExamplesComponent().layout(NlpView(engine), engine), found) + _collect_ids(SimilarExamplesComponent().layout(engine.to_explanation(), engine), found) self.assertIn("similar-topk", found) + self.assertIn("similar-threshold", found) + self.assertIn("similar-mode", found) self.assertIn("similar-results", found) def test_update_similar_returns_table_and_texts(self): app, _ = self._register() update = self._callback(app, "similar-results") - children, texts = update({"text": "i am happy", "label": "pos"}, 5) + children, texts = update({"text": "i am happy", "label": "pos"}, "topk", 5, 0.95) self.assertEqual(texts, ["i am joyful", "this is awful"]) self.assertIsNotNone(children) @@ -415,7 +466,32 @@ def test_update_similar_ignores_empty_text(self): app, _ = self._register() update = self._callback(app, "similar-results") with self.assertRaises(PreventUpdate): - update({"text": " "}, 5) + update({"text": " "}, "topk", 5, 0.95) + + def test_update_similar_threshold_mode_filters_and_reports_total(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.90) + # Only the two neighbours scoring above 0.90 clear the threshold (see FakeEngine.find_similar_threshold). + self.assertEqual(texts, ["i am joyful", "so glad today"]) + self.assertIsNotNone(children) + + def test_update_similar_threshold_mode_empty_above_cutoff(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.999) + self.assertEqual(texts, []) + self.assertIsNotNone(children) + + def test_toggle_mode_inputs_disables_the_inactive_control(self): + app, _ = self._register() + toggle = self._callback(app, "similar-topk.disabled") + topk_disabled, threshold_disabled = toggle("threshold") + self.assertTrue(topk_disabled) + self.assertFalse(threshold_disabled) + topk_disabled, threshold_disabled = toggle("topk") + self.assertFalse(topk_disabled) + self.assertTrue(threshold_disabled) def test_inspect_makes_neighbor_the_current_datapoint(self): from unittest import mock @@ -451,12 +527,51 @@ def test_neighbors_table_without_labels(self): table = _neighbors_table(neighbors, predicted_label=None, component_id="similar") self.assertIsNotNone(table) + def test_match_rate_caption_reports_share_of_predicted_label(self): + from shapash.webapp.nlp_components.similar_examples import _match_rate_caption + + neighbors = [ + Neighbor(index=0, score=0.9, text="a", label="pos"), + Neighbor(index=1, score=0.8, text="b", label="pos"), + Neighbor(index=2, score=0.7, text="c", label="neg"), + Neighbor(index=3, score=0.6, text="d", label="neg"), + ] + caption = _match_rate_caption(neighbors, predicted_label="pos") + self.assertIn("2/4", caption) + self.assertIn("50%", caption) + + def test_match_rate_caption_none_without_a_prediction_or_labels(self): + from shapash.webapp.nlp_components.similar_examples import _match_rate_caption + + neighbors = [Neighbor(index=0, score=0.9, text="a", label="pos")] + self.assertIsNone(_match_rate_caption(neighbors, predicted_label=None)) + unlabelled = [Neighbor(index=0, score=0.9, text="a", label=None)] + self.assertIsNone(_match_rate_caption(unlabelled, predicted_label="pos")) + + def test_render_results_includes_cap_note_only_when_capped(self): + from shapash.webapp.nlp_components.similar_examples import _render_results + + neighbors = [Neighbor(index=0, score=0.99, text="a", label="pos")] + capped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=5) + uncapped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=None) + + def _flat_text(node): + children = getattr(node, "children", None) + if isinstance(children, str): + return children + if isinstance(children, list): + return "".join(_flat_text(c) for c in children if c is not None) + return _flat_text(children) if children is not None else "" + + self.assertIn("Showing 1 of 5", _flat_text(capped)) + self.assertNotIn("Showing", _flat_text(uncapped)) + class TestLabelNoiseMounting(unittest.TestCase): """``CAP_LABELS`` is a *data* capability: it depends on the compiled batch, not on a live model.""" def _ids(self, engine): - app = NlpWebApp(engine) + app = NlpWebApp(engine.to_explanation(), engine=engine) found = set() _collect_ids(app.app.layout, found) return app, found @@ -488,18 +603,19 @@ def test_mounts_without_any_live_capability(self): def test_caption_warns_when_no_independent_cross_check_is_available(self): engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True) - layout = LabelNoiseComponent().layout(NlpView(engine), engine) + layout = LabelNoiseComponent().layout(engine.to_explanation(), engine) caption = layout.children[1].children self.assertIn("no independent cross-check", caption) def test_caption_explains_the_corpus_column_when_the_probe_is_available(self): engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, probe_corpus=_PROBE_CORPUS) - layout = LabelNoiseComponent().layout(NlpView(engine), engine) + layout = LabelNoiseComponent().layout(engine.to_explanation(), engine) caption = layout.children[1].children self.assertIn("Corpus column", caption) def test_callbacks_registered_when_mounted(self): - app = NlpWebApp(FakeEngine(can_edit=True, can_cf=True, has_labels=True)) + engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("label-noise-results.children", outputs) @@ -512,10 +628,10 @@ def _register(**kwargs): import dash engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, **kwargs) - view = NlpView(engine) + explanation = engine.to_explanation() app = dash.Dash(__name__) comp = LabelNoiseComponent() - comp.register_callbacks(app, view, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + comp.register_callbacks(app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) return app, engine @staticmethod From 86cbb73d15defdffaff065489fd1b2faf2a60132 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 3 Sep 2026 14:35:03 +0200 Subject: [PATCH 098/144] feat(webapp): mount the NLP webapp under a URL prefix --- .dockerignore | 47 +++++++++++++++++++ demo/Dockerfile | 2 +- demo/serve_nlp.py | 27 ++++++++++- shapash/explainer/nlp_explainer.py | 10 +++- shapash/webapp/nlp_app.py | 40 +++++++++++++++- tests/unit_tests/nlp/test_nlp_prototype.py | 22 +++++++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 42 +++++++++++++++++ 7 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..22e14f9f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Build context for demo/Dockerfile (context = repo root — see demo/README.md). +# +# The image only needs pyproject.toml, README.md, MANIFEST.in, shapash/, demo/serve_nlp.py +# and demo/nlp_cache/. Everything below is either huge or irrelevant to the build; without +# these excludes the context is several GB (.venv alone is multi-GB), which is minutes of +# upload on a remote Docker host. + +# Virtualenvs and VCS +.git +.gitignore +.venv +venv + +# Build / packaging / coverage artifacts +dist/ +build/ +htmlcov/ +.coverage +*.egg-info/ + +# Not used by the image +docs/ +tutorial/ +tests/ +data/ +dev/ +catboost_info/ +demo/private_model/ + +# Caches +**/__pycache__/ +*.py[cod] +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.ipynb_checkpoints/ + +# Editor / agent config +.idea/ +.vscode/ +.claude/ + +# Optional: ship only the model cache actually being served (saves ~50 MB, and keeps +# unrelated models' contributions out of the image). Uncomment both lines together — +# the second re-includes the one directory the default ServeConfig needs. +# demo/nlp_cache/*/ +# !demo/nlp_cache/bhadresh-savani__distilbert-base-uncased-emotion/ diff --git a/demo/Dockerfile b/demo/Dockerfile index 28f7e72e..bc83e287 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -49,4 +49,4 @@ ENV HF_HUB_OFFLINE=1 \ EXPOSE 8050 -CMD ["python", "serve_nlp.py"] +CMD ["python", "serve_nlp.py", "--n", "1000", "--n-reference", "5000"] diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 93cc0ac9..2da6839d 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -206,6 +206,8 @@ Usage ----- python demo/serve_nlp.py [--n 100] [--cache-dir demo/nlp_cache] [--port 8050] + # Behind a reverse proxy that routes a subpath (not a whole hostname) to this process: + python demo/serve_nlp.py --url-base-path /shapash-nlp-explainer/ python demo/serve_nlp.py --recompute # ignore the cache and recompute python demo/serve_nlp.py --attribution lig # Captum LayerIntegratedGradients highlights python demo/serve_nlp.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ @@ -301,6 +303,10 @@ class ServeConfig: cache_dir: Path = _HERE / "nlp_cache" port: int = 8050 host: str = "0.0.0.0" # noqa: S104 + # Mount point when a reverse proxy routes a subpath here rather than a whole hostname (e.g. + # "/shapash-nlp-explainer/"). Dash rewrites its own asset and callback URLs to match, so this + # has to equal the proxied path. None serves at the server root. + url_base_path: str | None = None recompute: bool = False def __post_init__(self) -> None: @@ -507,6 +513,13 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: parser.add_argument("--cache-dir", type=Path, default=defaults.cache_dir) parser.add_argument("--port", type=int, default=defaults.port) parser.add_argument("--host", default=defaults.host) + parser.add_argument( + "--url-base-path", + default=defaults.url_base_path, + help="Serve the app under this URL prefix instead of '/', for a reverse proxy that routes a " + "subpath to this process (e.g. '/shapash-nlp-explainer/'). Must match the proxied path; " + "missing slashes are added.", + ) parser.add_argument( "--recompute", action="store_true", @@ -1018,8 +1031,18 @@ def main() -> None: xpl.can_detect_label_noise(explanation), xpl.can_probe_labels(), ) - logger.info("Serving on http://%s:%d (Ctrl+C to stop)", config.host, config.port) - xpl.run_app(explanation, port=config.port, debug=False, host=config.host, scatter_xy=projected) + # Log the path the app actually mounts on, normalised the way NlpWebApp will normalise it, so a + # subpath run shows the URL to open rather than a root that would 404. + base_path = f"/{config.url_base_path.strip().strip('/')}/" if config.url_base_path else "/" + logger.info("Serving on http://%s:%d%s (Ctrl+C to stop)", config.host, config.port, base_path) + xpl.run_app( + explanation, + port=config.port, + debug=False, + host=config.host, + scatter_xy=projected, + url_base_pathname=config.url_base_path, + ) if __name__ == "__main__": diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 295bc988..efae1d19 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -499,6 +499,7 @@ def run_app( debug: bool = False, host: str = "127.0.0.1", scatter_xy=None, + url_base_pathname: str | None = None, ) -> None: """Launch the NLP explanation webapp for ``explanation``. @@ -532,8 +533,15 @@ def run_app( next to neighbours computed in another — selecting a cluster then means something other than it appears to. Use it when you want coordinates the library cannot produce, and accept that keeping them consistent is yours to manage. + url_base_pathname : str, optional + Mount the app under a URL prefix instead of the server root — for a reverse proxy that + routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Must match the + proxied path exactly; see :class:`~shapash.webapp.nlp_app.NlpWebApp`. ``None`` serves + at ``/``. """ - NlpWebApp(explanation, engine=self, scatter_xy=scatter_xy).run(port=port, debug=debug, host=host) + NlpWebApp(explanation, engine=self, scatter_xy=scatter_xy, url_base_pathname=url_base_pathname).run( + port=port, debug=debug, host=host + ) # ------------------------------------------------------------------ # InteractiveEngine — live what-if surface (see explainer/interactive.py) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index e352aa20..50752b01 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -78,6 +78,30 @@ def _compose_selection( return combined +def _normalize_base_pathname(path: str | None) -> str | None: + """Return *path* spelled the one way Dash accepts as ``url_base_pathname``. + + Dash rejects a prefix that does not both start and end with ``/``, and a mount point copied + from an nginx ``location`` or typed on a CLI routinely arrives missing one. ``/x/`` is the only + valid spelling of the same intent, so the slashes are added rather than raised over. + + Parameters + ---------- + path : str or None + The requested mount point, in any slash spelling. + + Returns + ------- + str or None + ``None`` when the app should serve at the server root (``None``, ``""`` or ``"/"``), + otherwise the prefix with both slashes. + """ + if path is None: + return None + stripped = path.strip().strip("/") + return f"/{stripped}/" if stripped else None + + class NlpWebApp: """Minimal Dash webapp driven by an ``NlpExplainer``. @@ -118,6 +142,15 @@ class NlpWebApp: a scatter panel is added to the layout. Compute with PaCMAP, UMAP, t-SNE, PCA, etc. and pass the result here — Shapash does not perform the projection itself to avoid heavy optional dependencies. + url_base_pathname : str, optional + Mount the app under a URL prefix instead of the server root, for serving behind a reverse + proxy that routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Dash + rewrites its own routes *and* every asset/callback URL it emits, so the prefix must match + the proxied path exactly. ``None`` (the default) serves at ``/``. + + Missing slashes are added — ``"shapash-nlp-explainer"`` and ``"/shapash-nlp-explainer"`` + both become ``"/shapash-nlp-explainer/"`` — since that is the only spelling Dash accepts + and a prefix copied from an nginx ``location`` easily arrives without one. """ def __init__( @@ -125,6 +158,7 @@ def __init__( explanation: NlpExplanation, engine: InteractiveEngine | None = None, scatter_xy: np.ndarray | None = None, + url_base_pathname: str | None = None, ) -> None: if scatter_xy is not None: scatter_xy = np.asarray(scatter_xy) @@ -142,7 +176,11 @@ def __init__( self._explanation = explanation self._engine = engine - self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + self.app = dash.Dash( + __name__, + external_stylesheets=[dbc.themes.BOOTSTRAP], + url_base_pathname=_normalize_base_pathname(url_base_pathname), + ) self.app.title = "Shapash — NLP Explainer" # Pin the document to the viewport so only the inner panels scroll, never the page. The # 100vh shell needs html/body at full height with no default margin; overflow:hidden is the diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 9e6b12cc..6f5bf956 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -1770,5 +1770,27 @@ def test_probe_needs_no_model_or_retriever(self): self.assertIsNotNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) +class TestRunAppMountPath(unittest.TestCase): + """``run_app`` forwards the reverse-proxy mount point to the webapp it builds.""" + + def test_url_base_pathname_reaches_the_webapp(self): + xpl = object.__new__(NlpExplainer) + explanation = object() + with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: + xpl.run_app(explanation, url_base_pathname="/shapash-nlp-explainer/") + web_app.assert_called_once_with( + explanation, + engine=xpl, + scatter_xy=None, + url_base_pathname="/shapash-nlp-explainer/", + ) + + def test_defaults_to_no_prefix(self): + xpl = object.__new__(NlpExplainer) + with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: + xpl.run_app(object()) + self.assertIsNone(web_app.call_args.kwargs["url_base_pathname"]) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 89b2832f..56a476f0 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -823,5 +823,47 @@ def test_rows_without_a_verdict_render_a_dash(self): self.assertEqual(_probe_summary(self._issue()), "—") +class TestSubpathMounting(unittest.TestCase): + """``url_base_pathname`` — serving behind a reverse proxy that routes a subpath, not a host.""" + + def _app(self, **kwargs): + engine = FakeEngine(can_edit=True, can_cf=True) + return NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) + + def test_serves_at_the_root_by_default(self): + config = self._app().app.config + self.assertEqual(config.routes_pathname_prefix, "/") + self.assertEqual(config.requests_pathname_prefix, "/") + + def test_mounts_under_the_given_prefix(self): + config = self._app(url_base_pathname="/shapash-nlp-explainer/").app.config + # Both prefixes matter: routes_* is where Dash listens, requests_* is what it writes into + # the asset and callback URLs the browser then fetches. + self.assertEqual(config.routes_pathname_prefix, "/shapash-nlp-explainer/") + self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") + + def test_missing_slashes_are_added(self): + for given in ("shapash-nlp-explainer", "/shapash-nlp-explainer", "shapash-nlp-explainer/"): + with self.subTest(given=given): + config = self._app(url_base_pathname=given).app.config + self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") + + def test_the_served_routes_and_asset_urls_carry_the_prefix(self): + # The config values above are only half the contract: Dash must also *answer* on the prefix + # and write it into the script tags, or the page loads blank behind the proxy. + client = self._app(url_base_pathname="/shapash-nlp-explainer/").app.server.test_client() + self.assertEqual(client.get("/shapash-nlp-explainer/").status_code, 200) + self.assertEqual(client.get("/").status_code, 404) + body = client.get("/shapash-nlp-explainer/").get_data(as_text=True) + self.assertIn("/shapash-nlp-explainer/_dash-component-suites", body) + self.assertNotIn('"/_dash-', body) + + def test_empty_prefixes_serve_at_the_root(self): + for given in ("", "/", " "): + with self.subTest(given=given): + config = self._app(url_base_pathname=given).app.config + self.assertEqual(config.requests_pathname_prefix, "/") + + if __name__ == "__main__": unittest.main() From 7e884942f2840803f6f91ad26e3b93964c09f2ab Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 4 Sep 2026 16:40:43 +0200 Subject: [PATCH 099/144] feat(app): add word profile tab and more filtering on word importance include changes on explanation output space (probability/logits) to tackle deeper in the future --- shapash/backend/nlp_backend.py | 10 + shapash/backend/nlp_captum_lig_backend.py | 9 +- shapash/backend/nlp_lime_backend.py | 4 + shapash/backend/nlp_shap_backend.py | 5 + shapash/explainer/nlp_explainer.py | 1 + shapash/explainer/nlp_explanation.py | 439 +++++++++++- shapash/explainer/nlp_plotter.py | 139 +++- shapash/plots/plot_word_importance.py | 180 ++++- shapash/plots/plot_word_profile.py | 107 +++ shapash/webapp/assets/style.css | 24 + shapash/webapp/nlp_app.py | 367 +++++++--- shapash/webapp/nlp_components/__init__.py | 11 +- shapash/webapp/nlp_components/base.py | 40 ++ .../webapp/nlp_components/error_analysis.py | 42 +- shapash/webapp/nlp_components/word_profile.py | 435 ++++++++++++ .../integration_tests/test_torch_st_models.py | 8 +- tests/unit_tests/backend/test_nlp_backend.py | 4 +- .../backend/test_nlp_captum_lig_backend.py | 9 +- tests/unit_tests/nlp/test_nlp_explanation.py | 30 + tests/unit_tests/nlp/test_nlp_plotter.py | 13 +- tests/unit_tests/nlp/test_nlp_prototype.py | 639 +++++++++++++++++- .../unit_tests/webapp/test_nlp_whatif_app.py | 602 ++++++++++++++++- 22 files changed, 2914 insertions(+), 204 deletions(-) create mode 100644 shapash/plots/plot_word_profile.py create mode 100644 shapash/webapp/nlp_components/word_profile.py diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index ddf2915d..a63db8f8 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -90,6 +90,15 @@ class NlpBackend(Backend): ``logits(x) - logits(baseline)`` for Integrated Gradients). Licenses feature grouping and waterfall/force-style charts; ``False`` for LIME, whose local surrogate coefficients carry no such guarantee. + output_space : {"probability", "logit"} + Which model output the contributions explain — read off the backend at + ``explain()`` time and recorded on :class:`~shapash.explainer.nlp_explanation.NlpExplanation` + so a saved artifact says which. ``"probability"``: the explained quantity is a + (softmax) probability, which forces per-token cross-class cancellation — see + ``docs/architecture/explanation-space.md``. ``"logit"``: the explained quantity is + the model's raw pre-softmax output, which does not cancel. Not an affine rescaling + of one another; a caller comparing two backends is comparing numbers on different + scales unless both report the same space. requires_model_capabilities : tuple[type, ...] Capability ABCs (from :mod:`shapash.model.base`) the bound model must satisfy, checked once here so a new backend declares its needs instead of failing at @@ -117,6 +126,7 @@ class NlpBackend(Backend): reference_kind: ClassVar[Literal["distribution", "statistics", "point", "none"]] is_additive: ClassVar[bool] + output_space: ClassVar[Literal["probability", "logit"]] requires_model_capabilities: ClassVar[tuple[type, ...]] = () def __init__( diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index d5ea8a8d..214d2b0e 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -13,8 +13,10 @@ LIG is a *completion* method: for each class ``c`` the token attributions sum to ``logits(x)[c] - logits(baseline)[c]``. We therefore report ``base_values[c] = logits(baseline)[c]`` -so the additive ``base + Σ = total`` summary in ``plot_sentence_highlight`` stays consistent (values -live in logit space, like the additive SHAP output). +so the additive ``base + Σ = total`` summary in ``plot_sentence_highlight`` stays consistent. These +values live in raw logit space — **not** the same space ``NlpShapBackend`` reports: SHAP explains +the pipeline's softmax probability output, not ``model.logits``, so the two backends' numbers are on +different scales and are not directly comparable (see ``docs/architecture/explanation-space.md``). Unlike the SHAP/LIME backends (which wrap a plain text callable), this backend needs the embedding module and a logits forward pass, so it consumes a :class:`~shapash.model.base.TextModel` that @@ -212,6 +214,9 @@ class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` default # 2017, "Axiomatic Attribution for Deep Networks"): attributions sum exactly to # logits(x) - logits(baseline). is_additive = True + # Attributes model.logits directly (see module docstring) — raw pre-softmax output, not the + # probability nlp_shap explains. + output_space = "logit" # Checked once by NlpBackend.__init__ instead of here — see its docstring. requires_model_capabilities = (SupportsCaptumIG,) diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index ab013246..8b3767aa 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -93,6 +93,10 @@ class NlpLimeBackend(NlpBackend): # sum to f(x) - f(baseline). Matches tabular LimeBackend.support_groups=False, for # the same algorithmic reason (not because tabular already decided it). is_additive = False + # ``model`` returns class probabilities (see its docstring above) and the surrogate is fit + # against that output directly, so this is a probability-space explanation like nlp_shap's — + # not the raw-logit space nlp_captum_lig reports. + output_space = "probability" requires_model_capabilities = () # a plain scoring callable is enough def __init__( diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index eaef6762..f4121560 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -210,6 +210,11 @@ class NlpShapBackend(NlpBackend): # even under the Partition/Owen path SHAP silently takes for text (see A8 in # docs/architecture/refactoring-plan.md: Owen values satisfy efficiency too). is_additive = True + # ``shap_callable`` resolves to a ``text-classification`` pipeline (softmax output), not + # ``model.logits`` — see docs/architecture/explanation-space.md §1/§2. This is why per-token + # attributions cancel across classes: the explained quantity sums to 1 for every masked + # variant, which is a constant-payoff game whose Shapley values are all zero. + output_space = "probability" requires_model_capabilities = () # a plain scoring callable is enough def __init__( diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index efae1d19..14273421 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -361,6 +361,7 @@ def explain( backend_name=backend_cls.name, is_additive=backend_cls.is_additive, reference_kind=backend_cls.reference_kind, + output_space=backend_cls.output_space, ) if cache_path is not None: cache_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index 93e0b907..26a32f8c 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -20,7 +20,7 @@ from dataclasses import dataclass, fields, replace from datetime import UTC, datetime from pathlib import Path -from typing import TYPE_CHECKING, ClassVar, Literal +from typing import TYPE_CHECKING, Any, ClassVar, Literal import numpy as np import pandas as pd @@ -59,6 +59,132 @@ def select_label_column(values: np.ndarray, label_idx: int) -> np.ndarray: return values[:, label_idx] if values.ndim == 2 else values +# How a word's occurrences collapse into one number per class. Keys are the wire values used by the +# webapp control and by ``NlpPlotter.word``; the values are (label, uses_absolute, reducer) — the +# label is UI text, kept beside the definition so a new operation is one entry here rather than an +# entry here plus a hard-coded option list in the app. +WORD_AGGREGATIONS: dict[str, tuple[str, bool, str]] = { + "mean": ("Mean", False, "mean"), + "sum": ("Sum", False, "sum"), + "mean_abs": ("Mean |·|", True, "mean"), + "sum_abs": ("Sum |·|", True, "sum"), +} + + +def aggregate_word_contributions(occurrences: pd.DataFrame, agg: str = "mean") -> pd.Series: + """Collapse one word's occurrences into a single number per class. + + The four operations answer different questions and are not interchangeable: + + - ``"mean"`` — how the word pushes *on average* wherever it appears. Comparable across words + of wildly different frequency; this is what :meth:`NlpExplanation.word_importance` uses. + - ``"sum"`` — the word's total pull on the corpus. Frequency counts: a mild word appearing 200 + times outranks a strong one appearing twice, which is the right ranking for "what is moving + this dataset" and the wrong one for "what does this word mean to the model". + + Worth knowing before reaching for it: **a sum tells you nothing a mean does not, for a single + word.** Every occurrence contributes to every class, so the occurrence count is identical + across classes and ``sum_c == N * mean_c`` for all ``c`` — the per-class profile under + ``"sum"`` is the ``"mean"`` profile rescaled by a constant, with the same shape and the same + ranking. Frequency only carries information when *different* words with *different* ``N`` are + compared, which is :meth:`NlpExplanation.word_importance`'s ``rank_by="sum"``, not this + function's per-word use. The webapp's single-word panel therefore offers only the two mean + forms; the sums stay here because this is a general aggregator and a caller comparing across + words has a legitimate use for them. + - ``"mean_abs"`` / ``"sum_abs"`` — *magnitude* regardless of direction. A word that pushes hard + toward the class in half its occurrences and hard away in the other half averages to ~0 under + ``"mean"`` while being anything but unimportant; the absolute forms surface exactly that word. + Their cost is that the sign is gone, so they say "this word matters here" and never "which + way" — read them against the signed pair rather than instead of it. + + Parameters + ---------- + occurrences : pd.DataFrame + Output of :meth:`NlpExplanation.word_occurrences`. + agg : {"mean", "sum", "mean_abs", "sum_abs"} + Which operation to apply. See :data:`WORD_AGGREGATIONS`. + + Returns + ------- + pd.Series + ``class_idx`` → aggregated contribution, ordered by class index. Empty when the word has + no occurrences. + + Raises + ------ + ValueError + If ``agg`` is not one of :data:`WORD_AGGREGATIONS`. + """ + if agg not in WORD_AGGREGATIONS: + raise ValueError(f"agg={agg!r} is not one of {sorted(WORD_AGGREGATIONS)}.") + _, use_abs, reducer = WORD_AGGREGATIONS[agg] + if occurrences.empty: + return pd.Series(dtype="float64", name=agg, index=pd.Index([], dtype="int64", name="class_idx")) + + values = occurrences["contribution"].abs() if use_abs else occurrences["contribution"] + out = values.groupby(occurrences["class_idx"]).agg(reducer).sort_index() + out.name = agg + out.index.name = "class_idx" + return out + + +def rank_word_samples( + occurrences: pd.DataFrame, + class_idx: int, + order: str = "most", + n_top: int = 10, +) -> pd.DataFrame: + """Rank the samples containing a word by how much it contributed in each of them. + + Occurrences are summed *within* a sample first, so a text using the word three times is ranked + on the word's total pull in that text rather than appearing three times in the list. + + Parameters + ---------- + occurrences : pd.DataFrame + Output of :meth:`NlpExplanation.word_occurrences`. + class_idx : int + Which class's contribution to rank on. + order : {"most", "least", "strongest"} + ``"most"`` ranks by the most positive contribution, ``"least"`` by the most negative, and + ``"strongest"`` by the largest magnitude in either direction. + n_top : int + How many samples to return. + + Returns + ------- + pd.DataFrame + Columns ``sample`` (positional index), ``contribution`` (summed within the sample) and + ``n_occurrences``, already ordered and truncated to ``n_top``. + + Raises + ------ + ValueError + If ``order`` is not one of the three accepted values. + """ + if order not in {"most", "least", "strongest"}: + raise ValueError(f"order={order!r} must be one of 'most', 'least', 'strongest'.") + columns = ["sample", "contribution", "n_occurrences"] + if occurrences.empty: + return pd.DataFrame(columns=columns).astype( + {"sample": "int64", "contribution": "float64", "n_occurrences": "int64"} + ) + + per_class = occurrences[occurrences["class_idx"] == int(class_idx)] + if per_class.empty: + return pd.DataFrame(columns=columns).astype( + {"sample": "int64", "contribution": "float64", "n_occurrences": "int64"} + ) + + grouped = per_class.groupby("sample")["contribution"].agg(contribution="sum", n_occurrences="size") + ranked = grouped.reset_index() + if order == "strongest": + ranked = ranked.reindex(ranked["contribution"].abs().sort_values(ascending=False).index) + else: + ranked = ranked.sort_values("contribution", ascending=(order == "least")) + return ranked.head(n_top).reset_index(drop=True) + + @dataclass(frozen=True, eq=False, slots=True) class NlpExplanation: """Immutable result of :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain`. @@ -139,6 +265,12 @@ class NlpExplanation: reference_kind : {"distribution", "statistics", "point", "none"} What kind of reference, if any, the contributions are measured against — read off the backend at ``explain()`` time. See ``NlpBackend.reference_kind``. + output_space : {"probability", "logit"} + Which model output the contributions explain — read off the backend at + ``explain()`` time. See ``NlpBackend.output_space``. Two backends reporting + different spaces are not directly comparable: notably, only ``"probability"`` + forces the per-token cross-class cancellation that :meth:`word_importance`'s + ``label_idx=None`` collapse relies on (see its Notes section). """ texts: pd.Series @@ -153,6 +285,7 @@ class NlpExplanation: backend_name: str is_additive: bool reference_kind: Literal["distribution", "statistics", "point", "none"] + output_space: Literal["probability", "logit"] # ClassVar, not a field: it is a shared constant, not per-explanation data, so it stays out # of ``fields()`` — and therefore out of ``__init__``, ``replace()`` and ``save()``. @@ -221,6 +354,7 @@ def _check_shared_index(self) -> None: "backend_name", "is_additive", "reference_kind", + "output_space", } ) @@ -365,7 +499,7 @@ def resolve_lowercase(self, lowercase: bool | None = None) -> bool: def word_importance( self, - label_idx: int, + label_idx: int | None, n_top: int = 20, filter_special: bool = True, filter_punctuation: bool = True, @@ -373,16 +507,23 @@ def word_importance( filter_sign: str = "all", exclude_words: set[str] | None = None, sample_indices: list[int] | None = None, + rank_by: str = "mean", + min_occurrences: int = 1, ) -> pd.Series: """Aggregate token contributions by word across all samples for one class. For each unique (stripped, by default lowercased) token string that appears in - the batch, computes the mean contribution across all its occurrences. + the batch, computes the mean (or total) contribution across all its occurrences. + + ``rank_by`` chooses the statistic; the ranking is always by its *absolute* value while + the returned numbers keep their sign, so a caller's positive/negative filter and a + renderer's sign colouring keep working unchanged in either mode. Parameters ---------- - label_idx : int - Index of the class to compute importance for. + label_idx : int or None + Index of the class to compute importance for. ``None`` aggregates *across* classes: + see the "Notes" section, which also explains why the aggregate is a magnitude. n_top : int Maximum number of words to return, sorted by ``|mean contribution|``. filter_special : bool @@ -414,28 +555,90 @@ def word_importance( sample_indices : list[int], optional Positional indices of the samples to include in the aggregation. When ``None`` (default) all samples are used. + rank_by : {"mean", "sum"} + Which statistic to compute and rank on. + + ``"mean"`` is the average pull wherever the word appears — "what does this word mean + to the model", comparable across words of different frequency. ``"sum"`` is the + word's total pull on the corpus, so frequency counts: a mild word appearing 674 times + outranks a strong one appearing five times. The two answer different questions and + routinely disagree — on a 1000-sample emotion corpus their top-20 lists overlap by + about 13 of 20 entries — so neither is a substitute for the other. + min_occurrences : int + Drop words occurring fewer than this many times *within the aggregated samples*. + + Not cosmetic: a mean over one observation is not a mean, and on real corpora most of + the vocabulary is hapax (58% of 3185 words on the emotion demo), so an unfiltered + ``"mean"`` ranking is dominated by single high-variance attributions rather than by + stable model behaviour. Counted after ``sample_indices`` scoping, so a threshold means + the same thing on a selection as on the whole batch — which also means a small + selection plus a high threshold legitimately returns nothing. Returns ------- pd.Series - Word → mean contribution, sorted by absolute value descending. + Word → signed contribution (mean or total, per ``rank_by``), sorted by absolute value + descending. ``.name`` carries the ``rank_by`` that produced it, suffixed with + ``"_across_classes"`` when ``label_idx`` is ``None``. + + Raises + ------ + ValueError + If ``rank_by`` is neither ``"mean"`` nor ``"sum"``. + + Notes + ----- + **Aggregating across classes** (``label_idx=None``). The per-class statistics are computed + exactly as above, then collapsed to ``max_c |stat_c(word)|`` — the strongest pull the word + exerts on *any* single class. + + The collapse has to discard sign, because a signed average across classes is identically + zero: an explainer of a normalised multiclass output distributes each token's attribution + so that the classes cancel (measured on the 1000-sample emotion demo, the largest per-token + sum across its six classes is 1.3e-7). A "mean over classes" chart would be a chart of + zeros. + + ``max`` rather than ``mean`` of the magnitudes, for two reasons. It keeps the number + comparable with the single-class view — a word reading ``0.909`` here reads ``+0.909`` + under the class that drives it, rather than ``0.303`` under an averaging that divides by + six. And under the same cancellation, ``mean_c`` is just ``max_c`` scaled by roughly + ``2 / n_classes`` whenever one class dominates, so it carries no information the max does + not (identical top-20 on the emotion demo) while being harder to read. + + Every returned value is therefore ``>= 0``, and ``filter_sign="negative"`` legitimately + returns nothing. Which class the magnitude came from is not reported here: reach it by + selecting the word in :meth:`word_occurrences` / + :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.word_profile`, which shows the full + per-class profile. """ + if rank_by not in ("mean", "sum"): + raise ValueError(f"rank_by={rank_by!r} must be 'mean' or 'sum'.") fold = self.resolve_lowercase(lowercase) def _key(token: str) -> str: return token.lower() if fold else token + across_classes = label_idx is None _exclude: set[str] = {_key(w) for w in exclude_words} if exclude_words else set() - word_contribs: dict[str, list[float]] = {} + # One float per occurrence for a single class; one row of per-class floats when collapsing + # across classes, since the collapse has to happen after the per-class statistic. + word_contribs: dict[str, list[Any]] = {} iter_data = ( ((self.token_strings[i], self.values[i]) for i in sample_indices) if sample_indices is not None else zip(self.token_strings, self.values, strict=True) ) for tokens, vals in iter_data: - vals_1d: np.ndarray = select_label_column(vals, label_idx) - for tok, val in zip(tokens, vals_1d, strict=True): + per_token: np.ndarray + if label_idx is None: + arr = np.asarray(vals, dtype=float) + # A single-output backend already gives one column; give it an explicit class axis + # so the collapse below is the same code either way. + per_token = arr if arr.ndim == 2 else arr[:, None] + else: + per_token = select_label_column(vals, label_idx) + for tok, val in zip(tokens, per_token, strict=True): tok_clean = tok.strip() if filter_special and self._SPECIAL_RE.match(tok_clean): continue @@ -446,9 +649,18 @@ def _key(token: str) -> str: continue if key not in word_contribs: word_contribs[key] = [] - word_contribs[key].append(float(val)) - - importance = pd.Series({w: float(np.mean(vs)) for w, vs in word_contribs.items()}) + word_contribs[key].append(val if across_classes else float(val)) + + reduce = np.mean if rank_by == "mean" else np.sum + kept = {w: vs for w, vs in word_contribs.items() if len(vs) >= min_occurrences} + if across_classes: + # Per-class statistic first, then the magnitude of its strongest class — see Notes. + stat = {w: float(np.abs(reduce(np.stack(vs), axis=0)).max()) for w, vs in kept.items()} + name = f"{rank_by}_across_classes" + else: + stat = {w: float(reduce(vs)) for w, vs in kept.items()} + name = rank_by + importance = pd.Series(stat, dtype="float64", name=name) importance = importance.reindex(importance.abs().sort_values(ascending=False).index) if filter_sign == "positive": @@ -458,6 +670,204 @@ def _key(token: str) -> str: return importance.head(n_top) + def word_counts( + self, + lowercase: bool | None = None, + filter_special: bool = True, + filter_punctuation: bool = True, + sample_indices: list[int] | None = None, + ) -> pd.DataFrame: + """How often each word unit occurs, and in how many samples. + + The frequency half of every word-level view: it orders the word picker, labels its + options, and is the count :meth:`word_importance`'s ``min_occurrences`` threshold is + applied to. Keyed exactly as :meth:`word_importance` aggregates, so a count shown beside + a word is the count that word's aggregate was computed over. + + Parameters + ---------- + lowercase : bool, optional + Case-fold the units. ``None`` (default) defers to :meth:`resolve_lowercase`. + filter_special : bool + Drop empty strings, ``[CLS]``/``[SEP]``-style bracket tokens and ``##subword`` prefixes. + filter_punctuation : bool + Drop units made entirely of punctuation. + sample_indices : list[int], optional + Count within these samples only. ``None`` (default) counts the whole batch. + + Returns + ------- + pd.DataFrame + Indexed by word (index name ``"word"``), with columns ``n_occurrences`` and + ``n_samples``, sorted by ``n_occurrences`` descending then alphabetically. + + Notes + ----- + The two columns differ only for words used more than once in a single text, which is why + both are reported: ``n_occurrences`` is the aggregation's denominator, while ``n_samples`` + counts *independent* observations and is the more honest measure of how much evidence sits + behind a mean. They track each other closely in practice (883 vs 859 words clear a + threshold of 3 on the emotion demo), so the picker uses occurrences. + """ + fold = self.resolve_lowercase(lowercase) + positions = range(len(self.token_strings)) if sample_indices is None else sample_indices + + occurrences: dict[str, int] = {} + samples: dict[str, int] = {} + for i in positions: + seen: set[str] = set() + for tok in self.token_strings[i]: + tok_clean = tok.strip() + if not tok_clean: + continue + if filter_special and self._SPECIAL_RE.match(tok_clean): + continue + if filter_punctuation and is_punctuation(tok_clean): + continue + key = tok_clean.lower() if fold else tok_clean + occurrences[key] = occurrences.get(key, 0) + 1 + seen.add(key) + for key in seen: + samples[key] = samples.get(key, 0) + 1 + + frame = pd.DataFrame( + { + "n_occurrences": pd.Series(occurrences, dtype="int64"), + "n_samples": pd.Series(samples, dtype="int64"), + } + ) + frame.index.name = "word" + if frame.empty: + return frame + # Alphabetical within a frequency tier, so the order is total and stable rather than + # dict-insertion order among the (many) words sharing a count. + return frame.sort_values(["n_occurrences", "word"], ascending=[False, True]) + + def vocabulary( + self, + lowercase: bool | None = None, + filter_special: bool = True, + filter_punctuation: bool = True, + sample_indices: list[int] | None = None, + ) -> list[str]: + """The batch's unique word units, keyed exactly as :meth:`word_importance` aggregates them. + + One source of truth for "which words can be talked about": every UI that lets a user *pick* + a word (the exclusion multi-select, the scatter's word colouring, the single-word profile) + must offer the same strings :meth:`word_importance` and :meth:`word_occurrences` match on, + or a pick silently finds nothing. + + Parameters + ---------- + lowercase : bool, optional + Case-fold the units. ``None`` (default) defers to :meth:`resolve_lowercase`. + filter_special : bool + Drop empty strings, ``[CLS]``/``[SEP]``-style bracket tokens and ``##subword`` prefixes. + filter_punctuation : bool + Drop units made entirely of punctuation. + sample_indices : list[int], optional + Restrict to these samples. ``None`` (default) uses the whole batch. + + Returns + ------- + list[str] + Alphabetically sorted unique units. For frequency order, or for the counts + themselves, use :meth:`word_counts` — this is its index. + """ + counts = self.word_counts( + lowercase=lowercase, + filter_special=filter_special, + filter_punctuation=filter_punctuation, + sample_indices=sample_indices, + ) + return sorted(counts.index) + + def word_occurrences( + self, + word: str, + lowercase: bool | None = None, + sample_indices: list[int] | None = None, + ) -> pd.DataFrame: + """Every occurrence of one word in the batch, with its contribution to every class. + + The raw material behind the single-word profile: :meth:`word_importance` ranks the whole + vocabulary but collapses each word to one number for one class, which answers "which words + matter" and not "what does *this* word do". This returns the un-aggregated occurrences so a + caller can pick the aggregation (see :func:`aggregate_word_contributions`) and drill from the + aggregate back to the samples it came from (see :func:`rank_word_samples`) without + re-scanning the batch. + + Parameters + ---------- + word : str + The word to look for. Matched against the same normalised key + :meth:`word_importance` aggregates on, so a label read off a word-importance bar always + finds its occurrences here. + lowercase : bool, optional + Case-fold both the query and the tokens before matching. ``None`` (default) derives the + answer from the model's own tokenizer via :meth:`resolve_lowercase`. + sample_indices : list[int], optional + Positional indices of the samples to search. ``None`` (default) searches all of them. + + Returns + ------- + pd.DataFrame + Tidy, one row per (occurrence, class), with columns: + + ``sample`` + Positional index of the sample the occurrence is in. + ``token_pos`` + Index of the token within that sample. + ``token`` + The token as it appears in the text (original casing, stripped), so a folded + match still shows which spelling occurred. + ``class_idx`` + Output-column index, in :attr:`label_names` order. + ``contribution`` + That token's contribution to that class. + + Empty (but correctly typed and columned) when the word never occurs. + + Examples + -------- + >>> occ = explanation.word_occurrences("terrible") + >>> aggregate_word_contributions(occ, agg="mean") + class_idx + 0 -0.184 + 1 0.211 + Name: mean, dtype: float64 + """ + fold = self.resolve_lowercase(lowercase) + target = word.strip().lower() if fold else word.strip() + n_classes = self.n_classes + positions = range(len(self.token_strings)) if sample_indices is None else sample_indices + + records: list[tuple[int, int, str, int, float]] = [] + for i in positions: + tokens = self.token_strings[i] + vals = self.values[i] + for pos, tok in enumerate(tokens): + tok_clean = tok.strip() + if (tok_clean.lower() if fold else tok_clean) != target: + continue + row_vals = vals[pos] if vals.ndim == 2 else [vals[pos]] + for class_idx in range(n_classes): + records.append((int(i), pos, tok_clean, class_idx, float(row_vals[class_idx]))) + + frame = pd.DataFrame(records, columns=["sample", "token_pos", "token", "class_idx", "contribution"]) + # Build the dtypes explicitly: an empty frame from an empty record list comes back all-object, + # which would make a downstream ``.abs()`` or a numeric groupby raise instead of returning an + # empty result — and "this word never occurs" is a normal state in the webapp, not an error. + return frame.astype( + { + "sample": "int64", + "token_pos": "int64", + "token": "object", + "class_idx": "int64", + "contribution": "float64", + } + ) + def confusion_matrix(self) -> np.ndarray: """Counts of ground truth against predictions, in :attr:`label_to_idx` order. @@ -523,6 +933,7 @@ def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: "backend_name": self.backend_name, "is_additive": self.is_additive, "reference_kind": self.reference_kind, + "output_space": self.output_space, "label_names": self.label_names, "folds_case": self.folds_case, # Authoritative counts, not re-derived from the tidy tables on load: a sample with @@ -605,6 +1016,10 @@ def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: backend_name=meta["backend_name"], is_additive=meta["is_additive"], reference_kind=meta["reference_kind"], + # Absent on a file saved before this field existed. "nlp_captum_lig" has always + # explained raw logits, everything else probabilities. + output_space=meta.get("output_space") + or ("logit" if meta["backend_name"] == "nlp_captum_lig" else "probability"), ) return explanation, scatter_xy diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index a60eae14..ceb7020d 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -24,16 +24,26 @@ from dash import html from plotly import graph_objs as go -from shapash.explainer.nlp_explanation import select_label_column +from shapash.explainer.nlp_explanation import ( + WORD_AGGREGATIONS, + aggregate_word_contributions, + select_label_column, +) from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall -from shapash.plots.plot_word_importance import plot_word_importance +from shapash.plots.plot_word_importance import plot_word_importance, word_importance_axis_title +from shapash.plots.plot_word_profile import plot_word_profile if TYPE_CHECKING: from shapash.explainer.nlp_explanation import NlpExplanation +# The arguments that decide how word units are *keyed*, and so how many times each one occurs. +# Shared by word_importance and word_counts; forwarding exactly these keeps a hover count equal to +# the count its bar's aggregate was computed over. +_COUNT_KWARGS = ("lowercase", "filter_special", "filter_punctuation", "sample_indices") + class NlpPlotter: """Plots over one :class:`~shapash.explainer.nlp_explanation.NlpExplanation`. @@ -213,21 +223,25 @@ def sentence(self, row: int = 0, label_idx: int = 0) -> html.Div: return plot_sentence_highlight(tokens=toks, values=values, base_value=base_value) # ── batch-level plots ─────────────────────────────────────────────────────────────── - def words( + def word_importance( self, - label_idx: int = 0, + label_idx: int | None = 0, n_top: int = 20, title: str | None = None, width: int = 900, height: int | None = None, **kwargs: Any, ) -> go.Figure: - """Corpus-level word importance for one class. + """Corpus-level word importance for one class, or across all of them. Parameters ---------- - label_idx : int - Index of the class to aggregate, in ``label_names`` order. + label_idx : int or None + Index of the class to aggregate, in ``label_names`` order. ``None`` collapses across + classes into ``max_c |statistic|`` — "how hard does this word push the model at all", + regardless of where it pushes. The bars are then magnitudes, so they are all one + colour and ``filter_sign`` has nothing to select on; the per-class breakdown of a word + found this way is :meth:`word_profile`. n_top : int Number of words to show, ranked by ``|mean contribution|``. title : str, optional @@ -238,18 +252,119 @@ def words( Forwarded to :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_importance` — ``filter_special``, ``filter_punctuation``, ``lowercase``, ``filter_sign``, - ``exclude_words``, ``sample_indices``. + ``exclude_words``, ``sample_indices``, ``rank_by``, ``min_occurrences``. Returns ------- plotly.graph_objs.Figure + + Notes + ----- + The default ranking is by ``|mean|`` with no frequency floor, which on a real corpus is + dominated by words seen once. Pass ``min_occurrences=3`` (or ``rank_by="sum"``, which + weights by frequency inherently) for a ranking that reflects stable model behaviour. """ - label_idx = self._check_label_idx(label_idx) + across_classes = label_idx is None + if label_idx is not None: + label_idx = self._check_label_idx(label_idx) word_imp = self._exp.word_importance(label_idx=label_idx, n_top=n_top, **kwargs) - name = self._label_name(label_idx) + name = self._label_name(label_idx) if label_idx is not None else None + if title is None: + if across_classes: + title = "Word importance — all classes" + else: + title = f"Word importance — {name}" if name else "Word importance" + # The axis names whichever statistic word_importance actually returned (it stamps its + # rank_by onto the Series name), so a total is never read as an average — and, across + # classes, so a magnitude is never read as a signed contribution. + x_title = word_importance_axis_title(str(word_imp.name)) + # Hover counts, computed under the same filters/scope as the ranking so a bar's count is + # the one its aggregate was taken over. Only the keying arguments are forwarded — the rest + # (filter_sign, exclude_words, rank_by...) select and order words, they do not change how + # many times a word occurs. + count_kwargs = {k: v for k, v in kwargs.items() if k in _COUNT_KWARGS} + counts = self._exp.word_counts(**count_kwargs) + return plot_word_importance( + word_imp, + title=title, + x_title=x_title, + width=width, + height=height, + counts=counts["n_occurrences"], + ) + + def word_profile( + self, + word: str, + agg: str = "mean", + lowercase: bool | None = None, + sample_indices: list[int] | None = None, + title: str | None = None, + width: int | None = 640, + height: int | None = None, + ) -> go.Figure: + """Profile of one word: its aggregated contribution to every class at once. + + The complement of :meth:`word_importance`, which fixes a class and ranks the vocabulary. + Here the word is fixed and every class is shown, which is the view that answers "what does + this word mean to the model" — including the multi-class case where a word pulls toward + one class and away from another. + + Parameters + ---------- + word : str + The word to profile, matched the way :meth:`word_importance` keys its units. + agg : {"mean", "sum", "mean_abs", "sum_abs"} + How the word's occurrences collapse into one number per class. See + :func:`~shapash.explainer.nlp_explanation.aggregate_word_contributions` for what each + one does and does not tell you. Mean aggregations also draw the standard deviation + across occurrences as error bars. + lowercase : bool, optional + Case-fold the match. ``None`` (default) defers to the model's tokenizer. + sample_indices : list[int], optional + Restrict the aggregation to these samples (e.g. only the misclassified ones). + title : str, optional + Overrides the default, which names the word and the aggregation. + width, height : int, optional + Figure size in pixels. + + Returns + ------- + plotly.graph_objs.Figure + + Raises + ------ + ValueError + If the word does not occur in the batch (or in ``sample_indices``) — an empty chart + here is indistinguishable from a word with zero contribution, so it says so instead. + """ + occurrences = self._exp.word_occurrences(word, lowercase=lowercase, sample_indices=sample_indices) + if occurrences.empty: + scope = " in the selected samples" if sample_indices is not None else "" + raise ValueError(f"{word!r} does not occur{scope} in this explanation.") + stats = aggregate_word_contributions(occurrences, agg=agg) + agg_label = WORD_AGGREGATIONS[agg][0] + + spread = None + if WORD_AGGREGATIONS[agg][2] == "mean": + values = occurrences["contribution"].abs() if WORD_AGGREGATIONS[agg][1] else occurrences["contribution"] + # ddof=0: this is the spread of the occurrences actually observed, not an estimate of a + # population — and it keeps a single-occurrence word at 0 rather than NaN. + spread = values.groupby(occurrences["class_idx"]).std(ddof=0).sort_index() + + n_occ = len(occurrences) // max(self._exp.n_classes, 1) + n_samples = int(occurrences["sample"].nunique()) if title is None: - title = f"Word importance — {name}" if name else "Word importance" - return plot_word_importance(word_imp, title=title, width=width, height=height) + title = f"{word!r} — {agg_label} over {n_occ} occurrence(s) in {n_samples} sample(s)" + return plot_word_profile( + stats, + label_names=self._exp.label_names, + spread=spread, + x_title=f"{agg_label} contribution", + title=title, + width=width, + height=height, + ) def confusion( self, diff --git a/shapash/plots/plot_word_importance.py b/shapash/plots/plot_word_importance.py index f855da65..bfd2c62f 100644 --- a/shapash/plots/plot_word_importance.py +++ b/shapash/plots/plot_word_importance.py @@ -2,35 +2,126 @@ from __future__ import annotations +from collections.abc import Mapping + import pandas as pd from plotly import graph_objs as go _COLOR_POSITIVE = "#1f77b4" _COLOR_NEGATIVE = "#d62728" +# Vertical room per word. The y axis is categorical and every category gets a tick (dtick=1 +# below), so this is what decides whether those ticks have room to render at all: squeeze 50 words +# into a 400px panel and each row gets 8px, at which point plotly drops labels rather than overlap +# them and the chart silently stops naming its own bars. Callers that want the chart to fill a +# fixed panel should scroll it, not override the height. +_ROW_HEIGHT = 30 +_CHROME_HEIGHT = 120 +# Room the title band itself takes out of _CHROME_HEIGHT — reclaimed (from both the auto-height +# budget and the top margin) when a caller has nowhere to put a title and skips it. +_TITLE_HEIGHT = 40 +_MIN_HEIGHT = 400 +# Headroom for the value labels, as a fraction of the largest bar — "outside" text is drawn past +# the bar end and would otherwise be clipped at the plot edge on the longest bar. +_VALUE_LABEL_PAD = 0.18 + + +# X-axis wording per statistic, keyed by the ``.name`` ``NlpExplanation.word_importance`` stamps on +# its result. A mean, a total and a cross-class magnitude are three different quantities drawn as +# identical-looking bars, and the axis is the only place a reader can tell which one they have. +_AXIS_TITLES = { + "mean": "Mean SHAP contribution", + "sum": "Total SHAP contribution", + "mean_across_classes": "Largest |mean SHAP| across classes", + "sum_across_classes": "Largest |total SHAP| across classes", +} + + +def word_importance_axis_title(name: str | None) -> str: + """X-axis label for a ``word_importance`` result, from the ``.name`` it carries. + + Shared by every caller so the wording cannot drift between the library plot and the webapp + panel drawing the same numbers. Falls back to the neutral "SHAP contribution" for a Series + that did not come from ``word_importance``. + """ + return _AXIS_TITLES.get(name or "", "SHAP contribution") + + +def empty_word_figure(message: str) -> go.Figure: + """Blank axes carrying a centred hint, for when a word chart has nothing to draw. + + An empty bar chart and a chart of all-zero bars look alike, and both look like a bug. Every + word-level panel has a legitimate empty state — no confusion-matrix cell picked yet, a word + absent from the selection, a frequency threshold that excludes the whole vocabulary — so the + reason is drawn on the figure rather than left to the reader. + + Parameters + ---------- + message : str + Why there is nothing to show. + + Returns + ------- + go.Figure + """ + fig = go.Figure() + fig.add_annotation(text=message, showarrow=False, font=dict(color="#888888"), xref="paper", yref="paper") + fig.update_layout( + xaxis=dict(visible=False), + yaxis=dict(visible=False), + plot_bgcolor="white", + margin=dict(l=20, r=20, t=20, b=20), + ) + return fig + def plot_word_importance( word_imp: pd.Series, - title: str = "Word importance", + title: str | None = "Word importance", + x_title: str = "Mean SHAP contribution", width: int = 900, height: int | None = None, + show_values: bool = True, + counts: pd.Series | Mapping[str, int] | None = None, ) -> go.Figure: - """Horizontal bar chart of mean token-level SHAP contributions per word. + """Horizontal bar chart of aggregated token-level SHAP contributions per word. - Words are displayed sorted by absolute mean contribution (highest at top). - Positive mean contributions are shown in blue, negative in red. + Words are displayed in the order given (highest at top). Positive contributions are shown + in blue, negative in red — which holds whichever statistic produced them, since + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_importance` ranks by absolute + value but returns signed numbers. Parameters ---------- word_imp : pd.Series - Word → mean SHAP contribution, already sorted by ``|value|`` descending + Word → aggregated SHAP contribution, already sorted by ``|value|`` descending (as returned by ``NlpExplanation.word_importance``). - title : str - Figure title. + title : str, optional + Figure title. Pass ``None`` or ``""`` to omit it and reclaim the title band's vertical + space — worth it when a caller already names the chart elsewhere (a tab label, a panel + header) and would otherwise be paying for the same words twice. + x_title : str + X-axis label. Name the statistic actually plotted — a mean and a total are different + quantities on the same-looking chart, and the axis is where a reader checks which. width : int Figure width in pixels. height : int, optional - Figure height in pixels. Defaults to ``max(400, 30 * n_words + 120)``. + Figure height in pixels. Defaults to ``max(400, 30 * n_words + chrome)``, where ``chrome`` + is ``120`` with a title or ``80`` without one — enough room for every word label to + render. Override it only for a chart that is scrolled rather than squeezed; forcing a + short height on a long ranking makes plotly drop the labels. + show_values : bool + Print each contribution at the end of its bar. Keeps the magnitude readable without + tracing back to the axis, at the cost of horizontal room — pass ``False`` in a narrow + panel. + counts : pd.Series or Mapping[str, int], optional + Word → number of occurrences the aggregate was computed over, added to each bar's hover. + It is the missing half of the ranking: a mean of ``+0.90`` over one occurrence and the + same mean over two hundred are indistinguishable on the bars themselves. Pass the + ``n_occurrences`` column of + :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_counts` computed with the + *same* filters and sample scope as the ranking. A mapping that does not cover every + plotted word is ignored outright rather than shown partially. Returns ------- @@ -42,9 +133,18 @@ def plot_word_importance( >>> fig = plot_word_importance(imp, title="Word importance — joy") >>> fig.show() """ - words = list(word_imp.index) - values = list(word_imp.values) + words = [str(w) for w in word_imp.index] + values = [float(v) for v in word_imp.to_numpy()] colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + labels = [f"{v:+.3f}" for v in values] if show_values else None + occurrences = _aligned_counts(words, counts) + + # Name the statistic in the hover too: the axis title is the only thing distinguishing a mean + # from a total, and it is off-screen once the chart is long enough to scroll. + hover = f"<b>%{{y}}</b><br>{x_title}: %{{x:.4f}}" + if occurrences is not None: + hover += "<br>Occurrences: %{customdata[0]:,}" + hover += "<extra></extra>" # Plotly renders y-axis bottom-to-top; reverse so highest-importance word appears at top. fig = go.Figure( @@ -53,24 +153,72 @@ def plot_word_importance( y=words[::-1], orientation="h", marker_color=colors[::-1], - hovertemplate="%{y}: %{x:.4f}<extra></extra>", + text=labels[::-1] if labels is not None else None, + textposition="outside" if show_values else "none", + textfont=dict(size=10, color="#666666"), + # Outside labels sit past the bar end, which is beyond the axis range for the longest + # bar; without this they are cut off rather than drawn. + cliponaxis=False, + customdata=[[n] for n in occurrences][::-1] if occurrences is not None else None, + hovertemplate=hover, ) ) + chrome = _CHROME_HEIGHT if title else _CHROME_HEIGHT - _TITLE_HEIGHT fig.update_layout( - title=dict(text=title, x=0.5), - xaxis_title="Mean SHAP contribution", + title=dict(text=title, x=0.5) if title else None, + xaxis_title=x_title, width=width, - height=height or max(400, 30 * len(words) + 120), + height=height or max(_MIN_HEIGHT, _ROW_HEIGHT * len(words) + chrome), plot_bgcolor="white", xaxis=dict( zeroline=True, zerolinecolor="#333333", zerolinewidth=1, gridcolor="#eeeeee", + range=_padded_range(values) if show_values else None, ), - yaxis=dict(automargin=True), - margin=dict(l=20, r=20, t=60, b=40), + yaxis=dict( + automargin=True, + # One tick per word, always. On a categorical axis plotly thins ticks when they do not + # fit, so a long ranking loses labels for the very bars a reader is trying to identify + # — and it does it silently. Forcing dtick makes a cramped chart look cramped instead. + dtick=1, + tickfont=dict(size=12, color="#222222"), + ), + # Slightly thicker bars than plotly's default, so a word label lines up against a bar + # rather than against whitespace. + bargap=0.25, + margin=dict(l=20, r=20, t=60 if title else 20, b=40), ) return fig + + +def _aligned_counts(words: list[str], counts: pd.Series | Mapping[str, int] | None) -> list[int] | None: + """Occurrence counts in ``words`` order, or ``None`` when there is nothing trustworthy to show. + + An incomplete mapping is rejected as a whole rather than filled with zeros: a bar labelled + "0 occurrences" that in fact has forty is worse than a bar with no count at all, and the only + way the mapping can be incomplete is a caller computing it under different filters — a + mismatch that would misreport *every* count, not just the missing ones. + """ + if counts is None: + return None + lookup = counts.to_dict() if isinstance(counts, pd.Series) else dict(counts) + aligned = [lookup.get(w) for w in words] + if any(n is None for n in aligned): + return None + return [int(n) for n in aligned] # type: ignore[arg-type] + + +def _padded_range(values: list[float]) -> list[float] | None: + """X range with headroom for the outside value labels, or ``None`` to let plotly autoscale.""" + if not values: + return None + lo, hi = min(0.0, *values), max(0.0, *values) + span = max(abs(lo), abs(hi)) + if span == 0: + return None + pad = _VALUE_LABEL_PAD * span + return [lo - pad, hi + pad] diff --git a/shapash/plots/plot_word_profile.py b/shapash/plots/plot_word_profile.py new file mode 100644 index 00000000..f11e5163 --- /dev/null +++ b/shapash/plots/plot_word_profile.py @@ -0,0 +1,107 @@ +"""Per-class profile of a single word's aggregated contributions.""" + +from __future__ import annotations + +import pandas as pd +from plotly import graph_objs as go + +_COLOR_POSITIVE = "#1f77b4" +_COLOR_NEGATIVE = "#d62728" + + +def plot_word_profile( + stats: pd.Series, + label_names: list[str] | None = None, + spread: pd.Series | None = None, + x_title: str = "Aggregated contribution", + title: str = "Word profile", + width: int | None = 640, + height: int | None = None, +) -> go.Figure: + """Horizontal bar chart of one word's aggregated contribution to each class. + + The transpose of :func:`~shapash.plots.plot_word_importance.plot_word_importance`: that one + fixes a class and ranks words, this one fixes a word and ranks nothing — it shows every class + at once, so a word that pushes toward one class and away from another reads as a single shape + instead of requiring the reader to flip a class selector back and forth. + + Parameters + ---------- + stats : pd.Series + ``class_idx`` → aggregated contribution, as returned by + :func:`~shapash.explainer.nlp_explanation.aggregate_word_contributions`. + label_names : list[str], optional + Class names in model-output order, used for the y-axis. Falls back to the class indices. + spread : pd.Series, optional + Same index as ``stats``, drawn as symmetric error bars — the standard deviation across + occurrences, when the aggregation is a mean. Omit for sums, where a spread across + occurrences says nothing about the total. + x_title : str + X-axis label. The caller names the aggregation here (e.g. ``"Mean contribution"``), since + the bars mean something different for each one. + title : str + Figure title. + width, height : int, optional + Figure size in pixels. ``height`` defaults to a size that fits the number of classes. + + Returns + ------- + go.Figure + + Examples + -------- + >>> occ = explanation.word_occurrences("terrible") + >>> stats = aggregate_word_contributions(occ, agg="mean") + >>> plot_word_profile(stats, label_names=explanation.label_names).show() + """ + class_idx = list(stats.index) + names = [label_names[i] if label_names is not None and i < len(label_names) else str(i) for i in class_idx] + values = [float(v) for v in stats.to_numpy()] + colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + + errors: list[float] | None = None + if spread is not None and len(spread): + # Reindexed on the bars actually drawn, so a mismatched spread cannot silently shift the + # error bars onto the wrong classes. + aligned = spread.reindex(stats.index).fillna(0.0) + errors = [float(v) for v in aligned.to_numpy()] + + # Plotly renders the y-axis bottom-to-top; reverse so class 0 sits at the top, matching the + # order every class selector in the app lists them in. + fig = go.Figure( + go.Bar( + x=values[::-1], + y=names[::-1], + orientation="h", + marker_color=colors[::-1], + # The class index travels on the bar so a click handler resolves it directly. Matching + # the y-axis label back to a class would be ambiguous the moment two classes share a + # display name, and reversed here alongside every other per-bar array. + customdata=[[int(i)] for i in class_idx][::-1], + error_x=( + dict(type="data", array=errors[::-1], visible=True, color="#666666", thickness=1) + if errors is not None + else None + ), + hovertemplate="%{y}: %{x:.4f}<extra></extra>", + ) + ) + + fig.update_layout( + title=dict(text=title, x=0.5), + xaxis_title=x_title, + width=width, + height=height or max(220, 46 * len(names) + 120), + plot_bgcolor="white", + xaxis=dict( + zeroline=True, + zerolinecolor="#333333", + zerolinewidth=1, + gridcolor="#eeeeee", + ), + yaxis=dict(automargin=True), + margin=dict(l=20, r=20, t=60, b=40), + bargap=0.45, + ) + + return fig diff --git a/shapash/webapp/assets/style.css b/shapash/webapp/assets/style.css index 0d148be9..fc414257 100644 --- a/shapash/webapp/assets/style.css +++ b/shapash/webapp/assets/style.css @@ -249,3 +249,27 @@ button#id_card, .btn-warning { align-items: center; padding-top: 0.2vw !important; } + +/* NUMBER INPUT SIZING */ +/* Bootstrap's .form-control-sm sets a fixed rem font-size/height that never moves with viewport + width, while the Class/Rank-by dropdowns right next to these two (.Select-control, above) are + deliberately sized in vw so the row's typography scales together as the page is resized — and + zooming the browser changes the effective viewport width in CSS px, which is exactly what drives + vw. Without this, Min-occur/Top-K stay pinned at a fixed size while their neighbors grow or + shrink, so at some zoom levels they look comparatively "off" — too big or too small next to the + dropdowns. Matching the same formula keeps every control in this row scaling in lockstep. + min-height (and the fixed-px padding it's built from) must be overridden too — Bootstrap derives + it from `.form-control-sm`'s own em/rem box model, which stays bigger than 1.4vw's own height + below ~2650px viewport width and silently clamps it, so the box height ends up following that + different, non-proportional formula instead of actually tracking 1.4vw like its neighbor. */ +#min-occurrences, #topk-input { + /* +2px: the dropdown control's height is content-box (its 1px+1px border sits outside the + 1.4vw), while this is border-box (border included inside it) — without the offset the two + land 2px apart at every width, a small but constant mismatch worth closing exactly. */ + height: calc(1.4vw + 2px) !important; + min-height: calc(1.4vw + 2px) !important; + line-height: 1.4vw !important; + font-size: calc(0.3rem + 0.5vw) !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 50752b01..8b2fea92 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -9,8 +9,6 @@ from __future__ import annotations -import re - import dash import dash_ag_grid as dag import dash_bootstrap_components as dbc @@ -20,10 +18,13 @@ from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate -from shapash.backend.nlp_backend import is_punctuation from shapash.explainer.interactive import InteractiveEngine from shapash.explainer.nlp_explanation import NlpExplanation, select_label_column -from shapash.plots.plot_word_importance import plot_word_importance +from shapash.plots.plot_word_importance import ( + empty_word_figure, + plot_word_importance, + word_importance_axis_title, +) from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, @@ -32,13 +33,42 @@ SentenceHighlightComponent, SimilarExamplesComponent, WaterfallComponent, + WordProfileComponent, + compose_selection, pack_datapoint, ) _APPLY_STORE = "whatif-apply-store" _CURRENT_STORE = "current-datapoint" -_SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") +# The frequency floor the global word ranking starts at. 1 (no filter) is the status quo and is +# actively misleading: on the emotion demo 58% of the vocabulary occurs exactly once, so an +# unfiltered |mean| ranking is a list of single attributions. 2 is the principled minimum — a mean +# over one observation is not a mean — and is the least surprising default; 3 is where the chart +# visibly settles into repeated, stable words. +_DEFAULT_MIN_OCCURRENCES = 2 + +# Bounds for the Top-K box. The browser enforces these on the spinner arrows but not on typed +# input, so the callback clamps rather than trusting them. +_MIN_TOPK, _MAX_TOPK, _DEFAULT_TOPK = 1, 50, 20 +# Idle delay (seconds) before a number box commits its value. ``debounce=True`` would commit only on +# Enter or blur, which leaves the spinner arrows apparently dead: clicking one keeps focus in the +# box, so nothing reaches the server until the user clicks away. A numeric debounce commits after a +# pause instead, so arrows respond while a typed "20" still does not recompute at "2". +_INPUT_DEBOUNCE_S = 0.4 + +# Class-dropdown value meaning "collapse the ranking across every class" — a string so it can never +# collide with a real (integer) class index. +_ALL_CLASSES = "all" + +# Shared by the layout and the callback that greys the sign options out under "All classes", so the +# two cannot drift apart. +_SIGN_FILTER_OPTIONS: list[dcc.RadioItems.Options] = [ + {"label": " All", "value": "all"}, + {"label": " Positive", "value": "positive"}, + {"label": " Negative", "value": "negative"}, +] + _HIDDEN = {"display": "none"} # The visible tab body is a flex-column item that fills the bodies container (which itself fills the # card). This unbroken flex chain is what lets a body's inner content use height:100% / flex:1 (e.g. the @@ -58,26 +88,6 @@ ] -def _compose_selection( - selected_indices: list[int] | None, - cell_indices: list[int] | None, - error_positions: set[int] | None, -) -> list[int] | None: - """Intersect the app's active sample filters into one index list. - - Each argument is an independent filter that may be inactive (``None``): the scatter box/lasso - selection, the confusion-matrix cell, and — when the errors-only switch is on — the set of - misclassified positions. Active filters intersect; returns ``None`` when none are active. - """ - combined = selected_indices - if cell_indices is not None: - cell_set = set(cell_indices) - combined = list(cell_indices) if combined is None else [i for i in combined if i in cell_set] - if error_positions is not None: - combined = sorted(error_positions) if combined is None else [i for i in combined if i in error_positions] - return combined - - def _normalize_base_pathname(path: str | None) -> str | None: """Return *path* spelled the one way Dash accepts as ``url_base_pathname``. @@ -267,15 +277,7 @@ def _build_layout(self) -> None: # entry here always matches a bar in the chart. On an uncased model that folds # AWFUL/Awful/awful into one entry; on a cased model it leaves all three, because there # they are genuinely different inputs. - _fold_words = explanation.resolve_lowercase() - all_words = sorted( - { - tok.strip().lower() if _fold_words else tok.strip() - for sample_tokens in explanation.token_strings - for tok in sample_tokens - if tok.strip() and not _SPECIAL_RE.match(tok.strip()) and not is_punctuation(tok) - } - ) + all_words = explanation.vocabulary() word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in all_words] # ── Scatter panel content (only when scatter_xy is provided) ── @@ -347,82 +349,166 @@ def _build_layout(self) -> None: # independent from the local class picker in the Sentence Highlight panel below.) # The tab label already names the panel, so no H6; a tight controls row + a graph that # flex-grows to fill the panel means the whole chart is visible without scrolling. - global_class_options: list[dcc.Dropdown.Options] = [ - {"label": name, "value": i} for i, name in enumerate(label_names) - ] - sign_filter_options: list[dcc.RadioItems.Options] = [ - {"label": " All", "value": "all"}, - {"label": " Positive", "value": "positive"}, - {"label": " Negative", "value": "negative"}, + # "All classes" first: it is the overview a reader without a class in mind wants, and it is + # the only entry whose value is not an index (see _ALL_CLASSES). + global_class_options: list[dcc.Dropdown.Options] = [{"label": "All classes", "value": _ALL_CLASSES}] + global_class_options += [{"label": name, "value": i} for i, name in enumerate(label_names)] + # Ordering only: both statistics are signed and are ranked on |value|, so the sign filter + # and the bars' red/blue colouring mean the same thing in either mode. + rank_by_options: list[dcc.Dropdown.Options] = [ + {"label": "Mean |·|", "value": "mean"}, + {"label": "Total |·|", "value": "sum"}, ] word_importance_panel = html.Div( [ - dbc.Row( + # One flex row split into two clusters — "what to rank" (left) and "what to keep" + # (right) — rather than six controls in an unbroken run: grouping reads faster, and + # a Bootstrap 50/50 column split was tried first but broke (the left cluster's own + # natural width exceeds half the panel at ordinary sizes, so the fixed-width column + # either overlapped the right cluster or wrapped mid-cluster). space-between instead + # pushes the right cluster as far right as the two clusters' actual widths allow — + # around the midpoint in practice, and never overlapping or force-wrapping either + # cluster — with flex-wrap kept as a fallback for genuinely narrow panels. + html.Div( [ - dbc.Col( + html.Div( [ - html.Label("Class", className="fw-bold small mb-0"), - dcc.Dropdown( - id="global-class-selector", - options=global_class_options, - value=0, - clearable=False, + html.Div( + [ + html.Label("Class", className="fw-bold small mb-0"), + dcc.Dropdown( + id="global-class-selector", + options=global_class_options, + value=0, + clearable=False, + style={"width": "140px"}, + ), + ] ), - ], - width=3, - ), - dbc.Col( - [ - html.Label("Top-K words", className="fw-bold small mb-0"), - dcc.Slider( - id="topk-slider", - min=1, - max=50, - step=1, - value=20, - marks={1: "1", 10: "10", 20: "20", 30: "30", 50: "50"}, - tooltip={"placement": "bottom", "always_visible": False}, + html.Div( + [ + html.Label( + "Rank by", + className="fw-bold small mb-0", + title=( + "Mean: the average pull wherever the word " + "appears. Total: its whole pull on the " + "corpus, so frequent words outrank rare " + "strong ones. The two disagree often." + ), + ), + dcc.Dropdown( + id="rank-by", + options=rank_by_options, + value="mean", + clearable=False, + style={"width": "95px"}, + ), + ] ), - ], - width=5, - ), - dbc.Col( - [ - html.Label("Contributions", className="fw-bold small mb-0"), - dcc.RadioItems( - id="sign-filter", - options=sign_filter_options, - value="all", - inline=True, - inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "12px"}, + html.Div( + [ + html.Label( + "Min occur.", + className="fw-bold small mb-0", + title=( + "Hide words seen fewer than this many times " + "in the current selection. A mean over one " + "occurrence is not a mean, and most of a " + "corpus is words seen once." + ), + ), + dcc.Input( + id="min-occurrences", + type="number", + min=1, + step=1, + value=_DEFAULT_MIN_OCCURRENCES, + debounce=_INPUT_DEBOUNCE_S, + className="form-control form-control-sm", + style={"width": "80px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Top-K words", + className="fw-bold small mb-0", + title=f"How many words to chart, {_MIN_TOPK}–{_MAX_TOPK}", + ), + dcc.Input( + id="topk-input", + type="number", + min=_MIN_TOPK, + max=_MAX_TOPK, + step=1, + value=_DEFAULT_TOPK, + debounce=_INPUT_DEBOUNCE_S, + className="form-control form-control-sm", + style={"width": "90px"}, + ), + ] ), ], - width="auto", - className="align-self-start", + className="d-flex align-items-start flex-wrap gap-2", ), - dbc.Col( + html.Div( [ - html.Label("Exclude words", className="fw-bold small mb-0"), - dcc.Dropdown( - id="word-filter", - options=word_options, - value=[], - multi=True, - placeholder="Exclude…", - style={"fontSize": "0.85em"}, + html.Div( + [ + html.Label("Contributions", className="fw-bold small mb-0"), + dcc.RadioItems( + id="sign-filter", + options=_SIGN_FILTER_OPTIONS, + value="all", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "12px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Exclude words", + className="fw-bold small mb-0", + style={"whiteSpace": "nowrap"}, + ), + dcc.Dropdown( + id="word-filter", + options=word_options, + value=[], + multi=True, + placeholder="Exclude…", + style={"fontSize": "0.85em"}, + ), + ], + style={"flex": "1 1 auto", "minWidth": "130px"}, ), ], - width=True, + className="d-flex align-items-start flex-wrap gap-2", ), ], - className="align-items-start g-2 mb-1", - style={"flex": "0 0 auto"}, + className="d-flex align-items-start flex-wrap mb-1", + style={"flex": "0 0 auto", "justifyContent": "space-between", "gap": "0.5rem"}, ), - dcc.Graph( - id="global-importance-graph", - config={"displayModeBar": False, "responsive": True}, - style={"flex": "1 1 auto", "minHeight": "0"}, + # The graph keeps the height plot_word_importance computed for it (30px per word) + # and this wrapper scrolls. Letting the chart flex to the panel instead — which is + # what it used to do — gives 50 words 8px each, at which point plotly drops the + # word labels and the bars become unidentifiable. + # "responsive" must stay off here: it makes plotly track the *container's* current + # size instead of the figure's own layout.height, which silently defeats the fixed + # per-row height above and squeezes every word back into whatever space the flex + # panel happens to have — the exact illegible-labels bug this height is built to + # avoid. Width still adapts once, at mount, from the container. + html.Div( + dcc.Graph( + id="global-importance-graph", + config={"displayModeBar": False}, + style={"width": "100%"}, + ), + style={"flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"}, ), ], style={"height": "100%", "display": "flex", "flexDirection": "column"}, @@ -564,6 +650,7 @@ def _build_layout(self) -> None: SimilarExamplesComponent(), LabelNoiseComponent(), ErrorAnalysisComponent(), + WordProfileComponent(), ) if type(comp).is_available(self._explanation, self._engine) ] @@ -574,6 +661,7 @@ def _build_layout(self) -> None: similar_comp = next((c for c in self._components if isinstance(c, SimilarExamplesComponent)), None) noise_comp = next((c for c in self._components if isinstance(c, LabelNoiseComponent)), None) error_analysis_comp = next((c for c in self._components if isinstance(c, ErrorAnalysisComponent)), None) + word_profile_comp = next((c for c in self._components if isinstance(c, WordProfileComponent)), None) left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_col_content is not None: @@ -584,6 +672,13 @@ def _build_layout(self) -> None: # Error Analysis sits beside Word Importance: it *is* an aggregated word-importance view # (per confusion-matrix cell), so it belongs with the other global "why" panels on the right. upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] + # Immediately after Word Importance, because it is the same question asked the other way + # round (one word across all classes, instead of one class across the top words) and the two + # are read together — a bar clicked there arrives preselected here. + if word_profile_comp is not None: + upper_right_tabs.append( + ("word-profile", "Word Profile", word_profile_comp.layout(self._explanation, self._engine)) + ) if error_analysis_comp is not None: upper_right_tabs.append( ("errors", "Error Analysis", error_analysis_comp.layout(self._explanation, self._engine)) @@ -726,16 +821,18 @@ def _compose_indices(selected_indices, error_cell, errors_only=False): mask = self._error_mask() if mask is not None: error_positions = set(np.where(mask)[0].tolist()) - return _compose_selection(selected_indices, cell_indices, error_positions) + return compose_selection(selected_indices, cell_indices, error_positions) # ── Global word importance ─────────────────────────────────────── @self.app.callback( Output("global-importance-graph", "figure"), [ Input("global-class-selector", "value"), - Input("topk-slider", "value"), + Input("topk-input", "value"), Input("sign-filter", "value"), Input("word-filter", "value"), + Input("rank-by", "value"), + Input("min-occurrences", "value"), Input("scatter-selected-indices", "data"), Input("error-cell", "data"), Input("errors-only-switch", "value"), @@ -746,35 +843,90 @@ def update_global_importance( topk, sign_filter, exclude_words_list, + rank_by, + min_occurrences, selected_indices, error_cell, errors_only, ): if label_idx is None: raise PreventUpdate + across_classes = label_idx == _ALL_CLASSES + rank_by = rank_by or "mean" + # Typed input bypasses the box's own min/max, and a cleared box arrives as None. + n_top = max(_MIN_TOPK, min(_MAX_TOPK, int(topk))) if topk is not None else _DEFAULT_TOPK + # A cleared number input arrives as None; 1 is "no floor", which is what an empty box + # should mean rather than reverting to the default the user just deleted. + floor = max(1, int(min_occurrences)) if min_occurrences else 1 effective_indices = _compose_indices(selected_indices, error_cell, bool(errors_only)) + # Across classes the bars are magnitudes (max over classes of |statistic|), so a sign + # filter has nothing left to select on: it is greyed out in that mode and forced to + # "all" here as well, so a value left over from a single-class view cannot silently + # empty the chart. + sign = "all" if across_classes else (sign_filter or "all") word_imp = explanation.word_importance( - label_idx=int(label_idx), - n_top=int(topk or 20), - filter_sign=sign_filter or "all", + label_idx=None if across_classes else int(label_idx), + n_top=n_top, + filter_sign=sign, # Punctuation is its own unit since word segmentation splits on word/non-word # boundaries; corpus-wide its mean contribution averages to ~0, so it is noise here. # Per-instance punctuation contributions stay visible in the Sentence Highlight panel. filter_punctuation=True, exclude_words=set(exclude_words_list or []) or None, sample_indices=effective_indices, + rank_by=rank_by, + min_occurrences=floor, ) - # No class name here — the Class dropdown right above the chart already shows it. - suffix = f" ({len(effective_indices)} samples)" if effective_indices is not None else "" + n_scope = len(effective_indices) if effective_indices is not None else len(explanation) + if word_imp.empty: + # Name the filter that actually emptied it, since the fix differs: the sign filter + # (checked first — it is applied last), then the frequency floor, then everything + # else (an exclusion list or an empty selection). + if sign in ("positive", "negative"): + reason = f"No {sign} word passes these filters." + elif floor > 1: + reason = f"No word occurs at least {floor} time(s) in these {n_scope} sample(s)." + else: + reason = f"No word passes these filters in these {n_scope} sample(s)." + return empty_word_figure(reason) + + # Same filters and same sample scope as the ranking above, so the count on a bar's + # hover is the count that bar's aggregate was computed over — and the count the + # min-occurrences floor was applied to. + counts = explanation.word_counts(filter_punctuation=True, sample_indices=effective_indices) fig = plot_word_importance( word_imp, - title=f"Word importance{suffix}", + # No title: the tab is already labelled "Word Importance" and the class/floor are + # both visible in the filter row right above the chart — a repeated title band was + # just eating vertical space this panel needs for word rows (see the graph wrapper + # below, which scrolls when it runs out). + title=None, + # A mean, a total and a cross-class magnitude are different quantities on an + # identical-looking chart, so the axis has to say which one is drawn. Derived from + # what word_importance stamped on its result rather than re-deduced here. + x_title=word_importance_axis_title(str(word_imp.name)), width=None, height=None, + counts=counts["n_occurrences"], ) - fig.layout.height = None # let the CSS container height take over + # Height deliberately left as plot_word_importance computed it — see the graph's + # wrapper in the layout. return fig + # Under "All classes" the ranking is a magnitude, so Positive/Negative would silently + # return nothing. Grey them out and pull the selection back to "All" rather than leaving a + # live control that cannot do anything. + @self.app.callback( + Output("sign-filter", "options"), + Output("sign-filter", "value"), + Input("global-class-selector", "value"), + ) + def gate_sign_filter(label_idx): + if label_idx != _ALL_CLASSES: + return _SIGN_FILTER_OPTIONS, dash.no_update + greyed = [{**opt, "disabled": opt["value"] != "all"} for opt in _SIGN_FILTER_OPTIONS] + return greyed, "all" + # ── Primary selection: selected table row → current-datapoint ──── # The editor's Predict callback also writes this store (see DataEditorComponent), # so every per-instance panel reads one source regardless of where it came from. @@ -905,7 +1057,9 @@ def update_scatter_color(color_by, words, label_idx, errors_only): return self._build_scatter_fig( color_by or "prediction", words=words or [], - label_idx=int(label_idx or 0), + # "All classes" is not a column to colour by; the scatter falls back to the + # first class rather than crashing on int("all"). + label_idx=label_idx if isinstance(label_idx, int) else 0, errors_only=bool(errors_only), ) @@ -1002,6 +1156,11 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): "current": _CURRENT_STORE, "error_cell": "error-cell", "error_cell_clear": "error-cell-clear-btn", + # The three shell-owned filters a global panel has to honour, plus the clicked-word + # store, handed over by id so a component never hard-codes the shell's ids. + "selection": "scatter-selected-indices", + "errors_only": "errors-only-switch", + "word_click": "word-click-filter", } for comp in self._components: comp.register_callbacks(self.app, self._explanation, self._engine, stores) diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index f0d739c6..9bddf723 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -13,7 +13,12 @@ from __future__ import annotations -from shapash.webapp.nlp_components.base import WebappComponent, available_capabilities +from shapash.webapp.nlp_components.base import ( + WebappComponent, + available_capabilities, + compose_selection, + error_positions, +) from shapash.webapp.nlp_components.counterfactual import CounterfactualComponent from shapash.webapp.nlp_components.data_editor import DataEditorComponent from shapash.webapp.nlp_components.datapoint import ( @@ -26,10 +31,13 @@ from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent from shapash.webapp.nlp_components.similar_examples import SimilarExamplesComponent from shapash.webapp.nlp_components.waterfall import WaterfallComponent +from shapash.webapp.nlp_components.word_profile import WordProfileComponent __all__ = [ "WebappComponent", "available_capabilities", + "compose_selection", + "error_positions", "DataEditorComponent", "CounterfactualComponent", "ErrorAnalysisComponent", @@ -37,6 +45,7 @@ "SentenceHighlightComponent", "SimilarExamplesComponent", "WaterfallComponent", + "WordProfileComponent", "pack_datapoint", "unpack_datapoint", "datapoint_from_contributions", diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 5b785c0e..9f893aea 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -14,6 +14,8 @@ from abc import ABC, abstractmethod +import numpy as np + from shapash.compute.diagnostics.label_noise import has_usable_probabilities from shapash.explainer.interactive import InteractiveEngine from shapash.explainer.nlp_explanation import NlpExplanation @@ -67,6 +69,44 @@ def available_capabilities(explanation: NlpExplanation, engine: InteractiveEngin return frozenset(caps) +def error_positions(explanation: NlpExplanation) -> set[int] | None: + """Positional indices of the samples the model got wrong, or ``None`` without ground truth. + + Compared as strings, the same way the dataset table's "Model Errors" filter does, so every + panel that scopes itself to errors scopes to exactly the same rows. + """ + y_true, y_pred = explanation.y_true, explanation.y_pred + if y_true is None or y_pred is None: + return None + mask = np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) + return set(np.where(mask)[0].tolist()) + + +def compose_selection( + selected_indices: list[int] | None, + cell_indices: list[int] | None, + errors: set[int] | None, +) -> list[int] | None: + """Intersect the app's active sample filters into one index list. + + Each argument is an independent filter that may be inactive (``None``): the scatter box/lasso + selection, the confusion-matrix cell, and — when the errors-only switch is on — the set of + misclassified positions. Active filters intersect; returns ``None`` when none are active. + + Lives here rather than in the app shell because every panel that honours the global selection + (the word-importance chart in the shell, the single-word profile component) has to compose it + identically — a panel with its own precedence rules would silently show a different subset than + the table beside it. + """ + combined = selected_indices + if cell_indices is not None: + cell_set = set(cell_indices) + combined = list(cell_indices) if combined is None else [i for i in combined if i in cell_set] + if errors is not None: + combined = sorted(errors) if combined is None else [i for i in combined if i in errors] + return combined + + class WebappComponent(ABC): """Base class for a self-contained, registrable webapp panel. diff --git a/shapash/webapp/nlp_components/error_analysis.py b/shapash/webapp/nlp_components/error_analysis.py index c65975d0..994590cb 100644 --- a/shapash/webapp/nlp_components/error_analysis.py +++ b/shapash/webapp/nlp_components/error_analysis.py @@ -19,12 +19,11 @@ import dash_bootstrap_components as dbc import numpy as np -import plotly.graph_objs as go from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate from shapash.plots.plot_confusion_matrix import plot_confusion_matrix -from shapash.plots.plot_word_importance import plot_word_importance +from shapash.plots.plot_word_importance import empty_word_figure, plot_word_importance from shapash.webapp.nlp_components.base import CAP_GROUND_TRUTH, WebappComponent _NORMALIZE_OPTIONS: list[dcc.RadioItems.Options] = [ @@ -51,19 +50,6 @@ def _cell_from_click(click_data: dict | None, name_to_idx: dict[str, int]) -> tu return None -def _empty_word_fig(message: str) -> go.Figure: - """Placeholder figure with a centred hint, for the per-cell word charts before a cell is picked.""" - fig = go.Figure() - fig.add_annotation(text=message, showarrow=False, font=dict(color="#888888"), xref="paper", yref="paper") - fig.update_layout( - xaxis=dict(visible=False), - yaxis=dict(visible=False), - plot_bgcolor="white", - margin=dict(l=20, r=20, t=20, b=20), - ) - return fig - - class ErrorAnalysisComponent(WebappComponent): """Confusion matrix + per-cell word importance, for auditing where the model is wrong.""" @@ -225,19 +211,37 @@ def toggle_error_cell_clear_button(error_cell): ) def update_error_word_charts(error_cell): if not error_cell: - empty = _empty_word_fig("Click a confusion-matrix cell to see the words behind those errors.") + empty = empty_word_figure("Click a confusion-matrix cell to see the words behind those errors.") return empty, empty, "Click a cell to see the words behind those errors." pred_i, true_i, indices = error_cell["pred"], error_cell["true"], error_cell["indices"] pred_name, true_name = label_names[pred_i], label_names[true_i] if not indices: - empty = _empty_word_fig("No samples for this (predicted, true) pair.") + empty = empty_word_figure("No samples for this (predicted, true) pair.") return empty, empty, f"predicted {pred_name} · true {true_name}: 0 samples" wi_pred = explanation.word_importance(label_idx=pred_i, n_top=15, sample_indices=indices) wi_true = explanation.word_importance(label_idx=true_i, n_top=15, sample_indices=indices) + # Counts within the clicked cell, not the corpus — the number that matters here is how + # much evidence sits behind a word *in this error group*, which is often very little. + counts = explanation.word_counts(sample_indices=indices)["n_occurrences"] + # show_values=False: these two sit side by side in half a column each, where outside + # value labels would cost horizontal room the word labels need more. The magnitudes + # move to the hover instead, alongside the counts. fig_pred = plot_word_importance( - wi_pred, title=f"Words toward predicted: {pred_name}", width=None, height=None + wi_pred, + title=f"Words toward predicted: {pred_name}", + width=None, + height=None, + show_values=False, + counts=counts, + ) + fig_true = plot_word_importance( + wi_true, + title=f"Words toward true: {true_name}", + width=None, + height=None, + show_values=False, + counts=counts, ) - fig_true = plot_word_importance(wi_true, title=f"Words toward true: {true_name}", width=None, height=None) fig_pred.layout.height = None fig_true.layout.height = None caption = f"predicted {pred_name} · true {true_name}: {len(indices)} sample(s)" diff --git a/shapash/webapp/nlp_components/word_profile.py b/shapash/webapp/nlp_components/word_profile.py new file mode 100644 index 00000000..14d30178 --- /dev/null +++ b/shapash/webapp/nlp_components/word_profile.py @@ -0,0 +1,435 @@ +"""Word Profile panel: one chosen word, aggregated across the corpus, for every class at once. + +The global Word Importance chart answers "which words matter for this class" — it fixes a class, +ranks the vocabulary by mean contribution, and shows the top K. Three questions it cannot answer +fall out of that shape, and this panel exists for them: + +* **Any word, not just the top K.** A word you have a hypothesis about ("does *not* actually flip + anything?") is usually nowhere near the top of a ranking, so it is simply not on the chart. +* **All classes at once.** In multi-class, a word's meaning to the model *is* the shape across + classes — pulls toward ``anger``, pushes away from ``joy``. Reading that off the top-K chart + means flipping the class selector k times and remembering k pictures. +* **Which aggregation.** The top-K chart is hard-wired to the mean, which is the right default and + the wrong statistic for two common cases: a frequent mild word that moves the corpus more than + any single strong one (use ``Sum``), and a word that pushes hard in *both* directions and + therefore averages to ~0 while being highly influential (use ``Mean |·|`` and compare against the + signed mean — a large gap between them *is* the context-dependence signal). + +Then it drills back down: the ranked sample table shows where the aggregate came from, and each +row's *Inspect* makes that sample the current datapoint, so the Sentence/Waterfall panels show the +word in its actual context. This is the loop the aggregate is only useful as an entry point to — +"this word averages +0.3 toward anger" is a claim you should be able to spot-check in one click. + +The panel honours the app's global sample selection (scatter box/lasso, confusion-matrix cell, +Model Errors switch) through the same :func:`~shapash.webapp.nlp_components.base.compose_selection` +the shell's own word-importance chart uses, which is what makes "how does this word behave *on the +errors*" a two-click question. + +Data-only: ``requires`` is empty, so it mounts on a loaded snapshot with no live model. The Inspect +button costs no re-explanation — every listed sample is already in the compiled batch. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +from dash import ALL, Input, Output, State, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.explainer.nlp_explanation import ( + WORD_AGGREGATIONS, + aggregate_word_contributions, + rank_word_samples, +) +from shapash.plots.plot_word_importance import empty_word_figure +from shapash.plots.plot_word_profile import plot_word_profile +from shapash.webapp.nlp_components.base import WebappComponent, compose_selection, error_positions +from shapash.webapp.nlp_components.datapoint import pack_datapoint + +_CARD_STYLE = {"height": "100%", "display": "flex", "flexDirection": "column", "overflowY": "auto"} +_INLINE = {"display": "flex", "alignItems": "center", "gap": "0.4rem"} + +# Only the two mean forms. A sum over one word is that word's mean rescaled by its occurrence +# count — identical across classes, so identical bar shape — and carries information only when +# words of differing frequency are compared, which is the Word Importance ranking's job (its +# "Rank by: Total" mode). See aggregate_word_contributions for the derivation. +_OFFERED_AGGS = ("mean", "mean_abs") +_DEFAULT_AGG = "mean" +_SORT_OPTIONS: list[dcc.RadioItems.Options] = [ + {"label": " A→Z", "value": "alpha"}, + {"label": " Frequency", "value": "frequency"}, +] +_DEFAULT_ORDER = "strongest" +_DEFAULT_LIMIT = 10 +_TEXT_PREVIEW_CHARS = 140 + +_ORDER_OPTIONS: list[dcc.Dropdown.Options] = [ + {"label": "Strongest (either way)", "value": "strongest"}, + {"label": "Most positive", "value": "most"}, + {"label": "Most negative", "value": "least"}, +] +_LIMIT_OPTIONS: list[dcc.Dropdown.Options] = [{"label": str(n), "value": n} for n in (5, 10, 20, 50)] + + +def _truncate(text: str) -> str: + return text if len(text) <= _TEXT_PREVIEW_CHARS else text[: _TEXT_PREVIEW_CHARS - 1] + "…" + + +def _samples_table(ranked, explanation, component_id: str, show_truth: bool): + """Ranked samples with their contribution, occurrence count, prediction and an Inspect button.""" + head_cells = [html.Th("Contribution"), html.Th("×", title="Occurrences of the word in this sample")] + head_cells.append(html.Th("Predicted")) + if show_truth: + head_cells.append(html.Th("True")) + head_cells.extend([html.Th("Text"), html.Th("")]) + + y_pred, y_true = explanation.y_pred, explanation.y_true + rows = [] + for i, record in enumerate(ranked.itertuples(index=False)): + pos = int(record.sample) + value = float(record.contribution) + cells = [ + html.Td( + f"{value:+.4f}", + className="fw-bold " + ("text-primary" if value >= 0 else "text-danger"), + ), + html.Td(str(int(record.n_occurrences)), className="text-muted"), + html.Td(str(y_pred.iloc[pos]) if y_pred is not None else "—", style={"fontSize": "0.85em"}), + ] + if show_truth: + cells.append(html.Td(str(y_true.iloc[pos]), style={"fontSize": "0.85em"})) + cells.append(html.Td(_truncate(str(explanation.texts.iloc[pos])), style={"fontSize": "0.85em"})) + cells.append( + html.Td( + dbc.Button( + "Inspect", + id={"type": f"{component_id}-inspect", "index": i}, + color="link", + size="sm", + className="p-0", + title="Show this sample's token contributions on the right", + ) + ) + ) + rows.append(html.Tr(cells)) + return dbc.Table( + [html.Thead(html.Tr(head_cells)), html.Tbody(rows)], + bordered=False, + hover=True, + size="sm", + className="mb-0", + ) + + +class WordProfileComponent(WebappComponent): + """Per-class aggregated contributions of one selected word, with drill-down to its samples.""" + + id = "word-profile" + name = "Word Profile" + scope = "global" + # Data-only: reads the artifact and nothing else, so it also mounts on a loaded snapshot. + requires = frozenset() + + def __init__(self) -> None: + # Both option orderings, built once in layout() and swapped by a callback: the counts cost + # a full pass over the corpus, and the sort toggle must not pay it on every click. + self._options: dict[str, list] = {"alpha": [], "frequency": []} + + def layout(self, explanation, engine=None) -> html.Div: + """Build the panel, seeded with the corpus's single most important word so it opens full.""" + counts = explanation.word_counts() + # The count rides in the option *label* while the value stays the bare word, so it is + # visible in either sort order (frequency is what tells you whether an aggregate is worth + # trusting) and every existing consumer of the value is unaffected. + by_frequency = [{"label": f"{w} ({n})", "value": w} for w, n in counts["n_occurrences"].items()] + self._options = { + "frequency": by_frequency, + "alpha": sorted(by_frequency, key=lambda o: o["value"]), + } + word_options = self._options["alpha"] + # Seed with the top word for class 0 rather than an empty panel: the first thing a user needs + # is an example of what this view *is*, and "the single most important word" is the least + # arbitrary pick available without asking them. The same frequency floor the Word Importance + # panel defaults to, so the seed is never a word seen once. + top = explanation.word_importance(label_idx=0, n_top=1, min_occurrences=2) + default_word = str(top.index[0]) if len(top) else (word_options[0]["value"] if word_options else None) + + label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] + class_options: list[dcc.Dropdown.Options] = [{"label": n, "value": i} for i, n in enumerate(label_names)] + agg_options: list[dcc.Dropdown.Options] = [ + {"label": WORD_AGGREGATIONS[key][0], "value": key} for key in _OFFERED_AGGS + ] + # With one output column there is nothing to rank *by* — the ranking class is forced to 0 and + # the control is hidden rather than shown as a single-entry dropdown. + single_class = explanation.n_classes == 1 + + return html.Div( + [ + html.Small( + "Pick any word in the corpus to see how it contributes to every class at once. " + "Compare the signed mean against Mean |·|: a large gap means the word cuts both " + "ways depending on context. Click a bar to rank the samples below by that class.", + className="text-muted d-block mb-2", + ), + html.Div( + [ + html.Div( + [ + html.Label("Word", className="small fw-bold mb-0"), + dcc.Dropdown( + id=f"{self.id}-select", + options=word_options, + value=default_word, + clearable=False, + placeholder="Search a word…", + style={"minWidth": "220px", "fontSize": "0.9em"}, + ), + ], + style={**_INLINE, "flex": "1 1 240px"}, + ), + html.Div( + dcc.RadioItems( + id=f"{self.id}-sort", + options=_SORT_OPTIONS, + value="alpha", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "10px"}, + className="small", + ), + title="Order the word list alphabetically or by how often each word occurs", + ), + html.Div( + [ + html.Label("Aggregate", className="small fw-bold mb-0"), + dcc.Dropdown( + id=f"{self.id}-agg", + options=agg_options, + value=_DEFAULT_AGG, + clearable=False, + style={"minWidth": "130px", "fontSize": "0.9em"}, + ), + ], + style=_INLINE, + ), + ], + className="d-flex flex-wrap align-items-center gap-3 mb-2", + style={"flex": "0 0 auto"}, + ), + html.Small(id=f"{self.id}-caption", className="text-muted d-block mb-1"), + dcc.Graph( + id=f"{self.id}-graph", + config={"displayModeBar": False, "responsive": True}, + style={"height": "280px", "flex": "0 0 auto"}, + ), + html.Hr(className="my-2"), + html.Div( + [ + html.Span("Samples where it contributed", className="small fw-bold"), + html.Div( + [ + html.Label("to", className="small text-muted mb-0"), + dcc.Dropdown( + id=f"{self.id}-class", + options=class_options, + value=0, + clearable=False, + style={"minWidth": "130px", "fontSize": "0.9em"}, + ), + ], + style={**_INLINE, "display": "none" if single_class else "flex"}, + ), + dcc.Dropdown( + id=f"{self.id}-order", + options=_ORDER_OPTIONS, + value=_DEFAULT_ORDER, + clearable=False, + style={"minWidth": "180px", "fontSize": "0.9em"}, + ), + html.Div( + [ + html.Label("Show", className="small text-muted mb-0"), + dcc.Dropdown( + id=f"{self.id}-limit", + options=_LIMIT_OPTIONS, + value=_DEFAULT_LIMIT, + clearable=False, + style={"minWidth": "80px", "fontSize": "0.9em"}, + ), + ], + style=_INLINE, + ), + ], + className="d-flex flex-wrap align-items-center gap-3 mb-2", + style={"flex": "0 0 auto"}, + ), + html.Div(id=f"{self.id}-results"), + # Positional indices of the displayed rows, so an Inspect click resolves to a sample. + dcc.Store(id=f"{self.id}-store", data=[]), + ], + style=_CARD_STYLE, + ) + + def register_callbacks(self, app, explanation, engine, stores) -> None: + """Wire the profile (word × aggregation × global selection) and each row's Inspect.""" + current_store = stores["current"] + selection_store = stores["selection"] + error_cell_store = stores["error_cell"] + errors_only_switch = stores["errors_only"] + word_click_store = stores["word_click"] + label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] + show_truth = explanation.y_true is not None + + @app.callback( + Output(f"{self.id}-graph", "figure"), + Output(f"{self.id}-caption", "children"), + Output(f"{self.id}-results", "children"), + Output(f"{self.id}-store", "data"), + Input(f"{self.id}-select", "value"), + Input(f"{self.id}-agg", "value"), + Input(f"{self.id}-class", "value"), + Input(f"{self.id}-order", "value"), + Input(f"{self.id}-limit", "value"), + Input(selection_store, "data"), + Input(error_cell_store, "data"), + Input(errors_only_switch, "value"), + ) + def update_profile(word, agg, class_idx, order, limit, selected_indices, error_cell, errors_only): + if not word: + return empty_word_figure("Pick a word to profile."), "", None, [] + + cell_indices = error_cell.get("indices") if error_cell else None + errors = error_positions(explanation) if errors_only else None + indices = compose_selection(selected_indices, cell_indices, errors) + scope = f" · scoped to {len(indices)} selected sample(s)" if indices is not None else "" + + occurrences = explanation.word_occurrences(word, sample_indices=indices) + if occurrences.empty: + where = "in the selected samples" if indices is not None else "in this corpus" + return ( + empty_word_figure(f"'{word}' does not occur {where}."), + f"'{word}' — no occurrences{scope}", + None, + [], + ) + + agg = agg or _DEFAULT_AGG + agg_label, use_abs, reducer = WORD_AGGREGATIONS[agg] + stats = aggregate_word_contributions(occurrences, agg=agg) + + spread = None + if reducer == "mean": + values = occurrences["contribution"].abs() if use_abs else occurrences["contribution"] + # ddof=0: the spread of the occurrences actually seen, not an estimate of a population + # — and it renders as 0 rather than NaN when a word occurs once. + spread = values.groupby(occurrences["class_idx"]).std(ddof=0).sort_index() + + fig = plot_word_profile( + stats, + label_names=explanation.label_names, + spread=spread, + x_title=f"{agg_label} contribution", + title="", # the caption below carries the word and the counts + width=None, + height=None, + ) + fig.layout.height = None # let the CSS container height take over + + n_occ = len(occurrences) // max(explanation.n_classes, 1) + n_samples = int(occurrences["sample"].nunique()) + total = len(indices) if indices is not None else len(explanation) + share = f" ({n_samples / total:.0%})" if total else "" + caption = ( + f"'{word}' — {agg_label} over {n_occ} occurrence(s) in {n_samples} of {total} sample(s){share}{scope}" + ) + if reducer == "mean": + caption += ". Error bars: ± std across occurrences." + + ranked = rank_word_samples( + occurrences, + class_idx=int(class_idx or 0), + order=order or _DEFAULT_ORDER, + n_top=int(limit or _DEFAULT_LIMIT), + ) + table = html.Div( + [ + html.Small( + f"Contribution to {label_names[int(class_idx or 0)]}, summed within each sample.", + className="text-muted d-block mb-1", + ), + _samples_table(ranked, explanation, self.id, show_truth), + ] + ) + return fig, caption, table, [int(s) for s in ranked["sample"]] + + @app.callback( + Output(f"{self.id}-select", "options"), + Input(f"{self.id}-sort", "value"), + ) + def reorder_word_list(sort): + # Options only — the selected value is untouched, so reordering never moves the chart. + return self._options.get(sort or "alpha", self._options["alpha"]) + + # Clicking a class's bar ranks the samples below by that class. The two halves of the panel + # otherwise drift apart: you read "pulls hard toward anger" off the top and then have to + # find anger again in a separate dropdown before the table below is about the same thing. + @app.callback( + Output(f"{self.id}-class", "value"), + Output(f"{self.id}-graph", "clickData"), + Input(f"{self.id}-graph", "clickData"), + prevent_initial_call=True, + ) + def rank_by_clicked_class(click_data): + if not click_data or not click_data.get("points"): + raise PreventUpdate + custom = click_data["points"][0].get("customdata") + if not custom: + raise PreventUpdate + # Reset clickData so re-clicking the *same* bar counts as a change and re-fires; plotly + # does not re-emit an unchanged value. + return int(custom[0]), None + + # A word bar clicked in the Word Importance panel writes the shell's word-click store; follow + # it so switching to this tab lands on the word the user just clicked. Only ever a *default* + # — the dropdown stays freely editable, and this never fires on load (which would overwrite + # the seeded top word with an empty filter). + @app.callback( + Output(f"{self.id}-select", "value"), + Input(word_click_store, "data"), + prevent_initial_call=True, + ) + def follow_clicked_word(word_filter): + # The store holds a bare string without a scatter and a list of words with one (the + # scatter's multi-select is the source of truth there); take the last word either way. + words = word_filter if isinstance(word_filter, list) else ([word_filter] if word_filter else []) + if not words: + raise PreventUpdate + return str(words[-1]) + + # Inspect: the sample is already in the compiled batch, so it packs straight from the stored + # contributions — no explain_text round-trip. Mirrors the app shell's set_current_from_row. + @app.callback( + Output(current_store, "data", allow_duplicate=True), + Input({"type": f"{self.id}-inspect", "index": ALL}, "n_clicks"), + State(f"{self.id}-store", "data"), + prevent_initial_call=True, + ) + def inspect(n_clicks_list, sample_indices): + if not any(n_clicks_list or []): + raise PreventUpdate + triggered = callback_context.triggered_id + if not triggered: + raise PreventUpdate + row = triggered["index"] + if not sample_indices or not (0 <= row < len(sample_indices)): + raise PreventUpdate + pos = int(sample_indices[row]) + base_values = explanation.base_values + y_pred = explanation.y_pred + return pack_datapoint( + text=str(explanation.texts.iloc[pos]), + orig_idx=pos, + tokens=explanation.token_strings[pos], + values=explanation.values[pos], + base_values=(base_values[pos] if base_values is not None else None), + label=(str(y_pred.iloc[pos]) if y_pred is not None else None), + ) diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py index 42ae295e..6b401a82 100644 --- a/tests/integration_tests/test_torch_st_models.py +++ b/tests/integration_tests/test_torch_st_models.py @@ -104,9 +104,7 @@ def test_st_embed_reproduces_encode_on_long_text(st_model): def test_st_declares_full_capability_surface(st_model): - assert has_capabilities( - st_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG - ) + assert has_capabilities(st_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG) def test_st_predict_is_normalised(st_model): @@ -153,9 +151,7 @@ def test_st_lig_backend_runs(st_model): def test_torch_declares_full_capability_surface(torch_model): - assert has_capabilities( - torch_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG - ) + assert has_capabilities(torch_model, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG) def test_torch_predict_and_embed(torch_model): diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py index 962a00a9..a684ec39 100644 --- a/tests/unit_tests/backend/test_nlp_backend.py +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -37,16 +37,18 @@ def test_surrounding_whitespace_is_ignored(self): class TestBackendContractAttributes(unittest.TestCase): - """``reference_kind`` / ``is_additive`` / `requires_model_capabilities`` (A9 / A11).""" + """``reference_kind`` / ``is_additive`` / ``output_space`` / `requires_model_capabilities`` (A9 / A11).""" def test_nlp_shap_backend(self): self.assertEqual(NlpShapBackend.reference_kind, "none") self.assertTrue(NlpShapBackend.is_additive) + self.assertEqual(NlpShapBackend.output_space, "probability") self.assertEqual(NlpShapBackend.requires_model_capabilities, ()) def test_nlp_lime_backend(self): self.assertEqual(NlpLimeBackend.reference_kind, "none") self.assertFalse(NlpLimeBackend.is_additive) + self.assertEqual(NlpLimeBackend.output_space, "probability") self.assertEqual(NlpLimeBackend.requires_model_capabilities, ()) diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py index 001414c6..df39c66c 100644 --- a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -38,6 +38,7 @@ def test_requires_captum_capability(self): def test_backend_contract_attributes(self): self.assertEqual(NlpCaptumLigBackend.reference_kind, "point") self.assertTrue(NlpCaptumLigBackend.is_additive) + self.assertEqual(NlpCaptumLigBackend.output_space, "logit") self.assertEqual(NlpCaptumLigBackend.requires_model_capabilities, (SupportsCaptumIG,)) @@ -46,9 +47,7 @@ class TestAggregateSubwords(unittest.TestCase): def test_merges_subwords_and_drops_specials(self): tokens = ["[CLS]", "i", "am", "hap", "##py", "[SEP]"] - contribs = np.array( - [[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float - ) + contribs = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float) base = np.array([10.0, 20.0]) words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) @@ -107,9 +106,7 @@ def test_explicit_special_set_overrides_regex(self): # *looks* bracket-y ("<odd>") is content unless it's actually in the set. tokens = ["<s>", "hi", "<odd>"] contribs = np.array([[1.0], [2.0], [3.0]]) - words, word_contribs, new_base = _aggregate_subwords( - tokens, contribs, np.zeros(1), special_tokens={"<s>"} - ) + words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(1), special_tokens={"<s>"}) self.assertEqual(words, ["hi", "<odd>"]) # "<odd>" kept as content, only "<s>" folded np.testing.assert_allclose(new_base, [1.0]) diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/nlp/test_nlp_explanation.py index 7b3f868c..c9c77a60 100644 --- a/tests/unit_tests/nlp/test_nlp_explanation.py +++ b/tests/unit_tests/nlp/test_nlp_explanation.py @@ -56,6 +56,7 @@ def _make_explanation(values_ndim: int, with_base: bool, with_true: bool, with_p backend_name="nlp_shap", is_additive=True, reference_kind="none", + output_space="probability", ) @@ -98,6 +99,7 @@ def _assert_round_trips(self, expl: NlpExplanation, scatter_xy=None): self.assertEqual(loaded.backend_name, expl.backend_name) self.assertEqual(loaded.is_additive, expl.is_additive) self.assertEqual(loaded.reference_kind, expl.reference_kind) + self.assertEqual(loaded.output_space, expl.output_space) self.assertEqual(loaded.label_names, expl.label_names) self.assertEqual(loaded.folds_case, expl.folds_case) @@ -132,6 +134,7 @@ def test_meta_json_is_plain_text_readable_without_shapash(self): self.assertEqual(meta["backend_name"], "nlp_shap") self.assertTrue(meta["is_additive"]) self.assertEqual(meta["reference_kind"], "none") + self.assertEqual(meta["output_space"], "probability") self.assertEqual(meta["label_names"], ["neg", "pos"]) self.assertEqual(meta["n_samples"], 3) self.assertIn("shapash_version", meta) @@ -158,6 +161,33 @@ def test_unsupported_format_version_raises_actionable_error(self): self.assertIn("format_version", str(ctx.exception)) self.assertIn("999", str(ctx.exception)) + def _resave_without_output_space(self, expl: NlpExplanation) -> Path: + """A file as an earlier shapash (pre-``output_space``) would have written it.""" + path = self.tmp_path / "legacy.zip" + expl.save(path) + with zipfile.ZipFile(path) as zin: + meta = json.loads(zin.read("meta.json")) + del meta["output_space"] + members = {item.filename: zin.read(item.filename) for item in zin.infolist()} + members["meta.json"] = json.dumps(meta).encode() + legacy_path = self.tmp_path / "legacy_no_output_space.zip" + with zipfile.ZipFile(legacy_path, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + return legacy_path + + def test_legacy_file_without_output_space_defaults_by_backend(self): + # nlp_captum_lig has always explained raw logits, everything else probabilities — a single + # global default would mislabel one of the two. See docs/architecture/explanation-space.md §5.3. + shap_expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + lig_expl = replace(shap_expl, backend_name="nlp_captum_lig") + + loaded_shap, _ = NlpExplanation.load(self._resave_without_output_space(shap_expl)) + loaded_lig, _ = NlpExplanation.load(self._resave_without_output_space(lig_expl)) + + self.assertEqual(loaded_shap.output_space, "probability") + self.assertEqual(loaded_lig.output_space, "logit") + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py index ebe3aa9b..1fb4944c 100644 --- a/tests/unit_tests/nlp/test_nlp_plotter.py +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -50,6 +50,7 @@ def _make_explanation( backend_name="nlp_shap" if is_additive else "nlp_lime", is_additive=is_additive, reference_kind="distribution" if with_base else "none", + output_space="probability", ) @@ -132,14 +133,14 @@ def test_plots_work_without_base_values(self): class TestBatchPlots(unittest.TestCase): - def test_words_returns_a_figure_titled_with_the_class(self): - fig = _make_explanation().plot.words(label_idx=1, n_top=5) + def test_word_importance_returns_a_figure_titled_with_the_class(self): + fig = _make_explanation().plot.word_importance(label_idx=1, n_top=5) self.assertIsInstance(fig, go.Figure) self.assertIn("pos", fig.layout.title.text) - def test_words_forwards_kwargs_to_word_importance(self): + def test_word_importance_forwards_kwargs(self): explanation = _make_explanation() - fig = explanation.plot.words(label_idx=0, n_top=10, exclude_words={"hello"}) + fig = explanation.plot.word_importance(label_idx=0, n_top=10, exclude_words={"hello"}) self.assertNotIn("hello", set(fig.data[0].y)) def test_confusion_returns_a_figure(self): @@ -165,7 +166,7 @@ def test_waterfall_refuses_on_a_non_additive_backend(self): def test_the_other_plots_stay_available_on_a_non_additive_backend(self): explanation = _make_explanation(is_additive=False) self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) - self.assertIsInstance(explanation.plot.words(label_idx=0), go.Figure) + self.assertIsInstance(explanation.plot.word_importance(label_idx=0), go.Figure) class TestArtifactIsNeverWritten(unittest.TestCase): @@ -176,7 +177,7 @@ def test_rendering_leaves_the_explanation_untouched(self): explanation.plot.tokens(row=0, label_idx=1, max_tokens=1) explanation.plot.waterfall(row=1, label_idx=0) explanation.plot.sentence(row=0, label_idx=0) - explanation.plot.words(label_idx=1, n_top=3) + explanation.plot.word_importance(label_idx=1, n_top=3) explanation.plot.confusion(normalize="true") self.assertEqual(explanation.backend_name, before.backend_name) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 6f5bf956..ade71b3d 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -23,7 +23,12 @@ from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.compute.retrieval import Neighbor from shapash.explainer.nlp_explainer import NlpExplainer -from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.explainer.nlp_explanation import ( + WORD_AGGREGATIONS, + NlpExplanation, + aggregate_word_contributions, + rank_word_samples, +) from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_noise_matrix import plot_noise_matrix @@ -31,7 +36,10 @@ from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance -from shapash.webapp.nlp_app import NlpWebApp, _compose_selection +from shapash.plots.plot_word_profile import plot_word_profile +from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.nlp_components import compose_selection as _compose_selection +from shapash.webapp.nlp_components import error_positions from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] @@ -71,6 +79,7 @@ def _make_explanation( backend_name: str = "nlp_shap", is_additive: bool = True, reference_kind: str = "none", + output_space: str = "probability", ) -> NlpExplanation: """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" texts = _make_texts() if texts is None else texts @@ -80,7 +89,9 @@ def _make_explanation( token_strings=token_strings, values=values, base_values=base_values, - y_pred=(pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred), + y_pred=( + pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred + ), y_prob=y_prob, y_true=y_true, label_names=label_names, @@ -88,6 +99,7 @@ def _make_explanation( backend_name=backend_name, is_additive=is_additive, reference_kind=reference_kind, + output_space=output_space, ) @@ -113,6 +125,7 @@ def _minimal_explanation( backend_name="nlp_shap", is_additive=True, reference_kind="none", + output_space="probability", ) @@ -703,6 +716,7 @@ def _projection_explanation(texts: pd.Series) -> NlpExplanation: backend_name="test", is_additive=True, reference_kind="none", + output_space="probability", ) @@ -954,12 +968,19 @@ def test_layout_built(self): def test_class_selector_options(self): # Class selector is now two independent dropdowns: "local-class-selector" (Sentence # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). - for dropdown_id in ("local-class-selector", "global-class-selector"): - dropdown = self._find_component(self.webapp.app.layout, dropdown_id) - self.assertIsNotNone(dropdown, f"{dropdown_id} dropdown not found in layout") - self.assertEqual(len(dropdown.options), N_CLASSES) - labels = [opt["label"] for opt in dropdown.options] - self.assertEqual(labels, LABEL_NAMES) + # Only the global one offers the cross-class overview — a single sample's highlight has no + # such aggregate. + local = self._find_component(self.webapp.app.layout, "local-class-selector") + self.assertIsNotNone(local, "local-class-selector dropdown not found in layout") + self.assertEqual([opt["label"] for opt in local.options], LABEL_NAMES) + + glob = self._find_component(self.webapp.app.layout, "global-class-selector") + self.assertIsNotNone(glob, "global-class-selector dropdown not found in layout") + self.assertEqual(len(glob.options), N_CLASSES + 1) + self.assertEqual([opt["label"] for opt in glob.options], ["All classes", *LABEL_NAMES]) + # Its value must never collide with a real class index. + self.assertEqual(glob.options[0]["value"], "all") + self.assertEqual([opt["value"] for opt in glob.options[1:]], list(range(N_CLASSES))) def test_local_class_selector_defaults_to_predicted_class(self): # Defaults to the predicted class of the initially selected row (row 0). @@ -985,7 +1006,7 @@ def test_graph_ids_present(self): def test_control_ids_present(self): ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("topk-slider", ids) + self.assertIn("topk-input", ids) self.assertIn("sign-filter", ids) self.assertIn("word-filter", ids) @@ -1160,6 +1181,599 @@ def test_empty_intersection_is_empty_list(self): self.assertEqual(_compose_selection([1, 2], None, {8, 9}), []) +class TestErrorPositions(unittest.TestCase): + """The shared definition of "a model error" every errors-scoped panel filters on.""" + + def test_none_without_ground_truth(self): + self.assertIsNone(error_positions(_make_explanation())) + + def test_compares_as_strings(self): + texts = _make_texts() + explanation = _make_explanation(y_true=pd.Series(["joy", "joy", "joy"], index=texts.index)) + # y_pred is ["joy", "sadness", "joy"], so only the middle row disagrees. + self.assertEqual(error_positions(explanation), {1}) + + +# --------------------------------------------------------------------------- +# Single-word profile: word_occurrences / vocabulary / aggregators / plot / plotter +# --------------------------------------------------------------------------- + + +def _profile_explanation(folds_case=True): + """Three samples, two classes, with 'happy' appearing in three of them (twice in one).""" + token_strings = [ + ["[CLS]", "so", "happy", "today", "!"], + ["Happy", "and", "happy", "again"], + ["not", "happy", "at", "all"], + ["nothing", "here"], + ] + values = [ + np.array([[0.0, 0.0], [0.1, -0.1], [0.4, -0.4], [0.05, -0.05], [0.0, 0.0]]), + np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1], [0.0, 0.0]]), + np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), + np.array([[0.0, 0.0], [0.0, 0.0]]), + ] + texts = pd.Series(["so happy today !", "Happy and happy again", "not happy at all", "nothing here"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=None, + y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), + label_names=["pos", "neg"], + folds_case=folds_case, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +class TestVocabulary(unittest.TestCase): + def test_filters_special_and_punctuation_and_folds(self): + vocab = _profile_explanation().vocabulary() + self.assertNotIn("[CLS]", vocab) + self.assertNotIn("!", vocab) + # Folded: "Happy" and "happy" collapse to one entry. + self.assertIn("happy", vocab) + self.assertNotIn("Happy", vocab) + + def test_keeps_case_when_model_does_not_fold(self): + vocab = _profile_explanation(folds_case=False).vocabulary() + self.assertIn("Happy", vocab) + self.assertIn("happy", vocab) + + def test_sorted_and_unique(self): + vocab = _profile_explanation().vocabulary() + self.assertEqual(vocab, sorted(set(vocab))) + + def test_offers_exactly_what_word_importance_ranks(self): + # The contract the pickers rely on: every unit offered has occurrences to show. + explanation = _profile_explanation() + for word in explanation.vocabulary(): + self.assertFalse(explanation.word_occurrences(word).empty, word) + + def test_can_keep_punctuation_and_special(self): + vocab = _profile_explanation().vocabulary(filter_special=False, filter_punctuation=False) + self.assertIn("[cls]", vocab) + self.assertIn("!", vocab) + + +class TestWordImportanceRanking(unittest.TestCase): + """``rank_by`` + ``min_occurrences``: ordering criteria that never touch the reported sign.""" + + def setUp(self): + # "rare" appears once with a large pull; "common" appears four times with a small one. + # Under |mean| rare wins; under |sum| common wins (4 x 0.3 = 1.2 > 0.9). + token_strings = [ + ["rare", "common"], + ["common"], + ["common", "common"], + ["mild"], + ] + values = [ + np.array([[0.9], [0.3]]), + np.array([[0.3]]), + np.array([[0.3], [0.3]]), + np.array([[-0.5]]), + ] + self.explanation = _minimal_explanation(token_strings, values, None) + + def test_mean_is_the_default_and_unchanged(self): + imp = self.explanation.word_importance(label_idx=0) + self.assertEqual(imp.index[0], "rare") + self.assertAlmostEqual(imp["common"], 0.3) + + def test_sum_reranks_by_total_mass(self): + imp = self.explanation.word_importance(label_idx=0, rank_by="sum") + self.assertEqual(imp.index[0], "common") + self.assertAlmostEqual(imp["common"], 1.2) + + def test_series_name_records_the_statistic(self): + self.assertEqual(self.explanation.word_importance(label_idx=0).name, "mean") + self.assertEqual(self.explanation.word_importance(label_idx=0, rank_by="sum").name, "sum") + + def test_ranking_is_absolute_but_values_stay_signed(self): + # "mild" is negative and must survive ranking, keeping its sign for the sign filter and + # for the renderer's red/blue colouring. + for rank_by in ("mean", "sum"): + imp = self.explanation.word_importance(label_idx=0, rank_by=rank_by) + self.assertLess(imp["mild"], 0) + + def test_sign_filter_works_in_both_modes(self): + for rank_by in ("mean", "sum"): + pos = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="positive") + neg = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="negative") + self.assertNotIn("mild", pos.index) + self.assertEqual(list(neg.index), ["mild"]) + + def test_min_occurrences_drops_rare_words(self): + imp = self.explanation.word_importance(label_idx=0, min_occurrences=2) + self.assertEqual(list(imp.index), ["common"]) + + def test_min_occurrences_of_one_is_no_filter(self): + self.assertEqual( + set(self.explanation.word_importance(label_idx=0, min_occurrences=1).index), + {"rare", "common", "mild"}, + ) + + def test_min_occurrences_counts_within_the_selection(self): + # "common" occurs 4x across the batch but only once in sample 1, so a floor of 2 must + # exclude it there — otherwise a threshold would mean something different per scope. + imp = self.explanation.word_importance(label_idx=0, sample_indices=[1], min_occurrences=2) + self.assertTrue(imp.empty) + self.assertEqual( + list(self.explanation.word_importance(label_idx=0, sample_indices=[2], min_occurrences=2).index), + ["common"], + ) + + def test_impossible_floor_returns_an_empty_series_not_an_error(self): + imp = self.explanation.word_importance(label_idx=0, min_occurrences=999) + self.assertTrue(imp.empty) + self.assertEqual(imp.dtype, np.float64) + + def test_unknown_rank_by_raises(self): + with self.assertRaisesRegex(ValueError, "must be 'mean' or 'sum'"): + self.explanation.word_importance(label_idx=0, rank_by="median") + + def test_plotter_labels_the_axis_for_the_statistic(self): + self.assertEqual( + self.explanation.plot.word_importance(label_idx=0, rank_by="sum").layout.xaxis.title.text, + "Total SHAP contribution", + ) + self.assertEqual( + self.explanation.plot.word_importance(label_idx=0).layout.xaxis.title.text, + "Mean SHAP contribution", + ) + + def test_plotter_hover_counts_match_the_scope_it_ranked(self): + # The plotter forwards only the keying arguments to word_counts, so a count on a bar must + # be the count over the *same* samples the aggregate was taken over. + fig = self.explanation.plot.word_importance(label_idx=0, sample_indices=[2]) + counts = self.explanation.word_counts(sample_indices=[2])["n_occurrences"] + drawn = list(fig.data[0].y) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [int(counts[w]) for w in drawn]) + + +class TestWordImportanceAcrossClasses(unittest.TestCase): + """``label_idx=None``: one ranking spanning every class, on magnitudes.""" + + @staticmethod + def _three_class(): + # "alpha" is decisive for class c only; "beta" is mild and spread. Three classes so max + # and mean over classes are actually distinguishable (with two they coincide). + texts = pd.Series(["alpha beta", "alpha beta"]) + rows = np.array([[-0.3, -0.3, 0.6], [0.1, -0.2, 0.1]]) + return NlpExplanation( + texts=texts, + token_strings=[["alpha", "beta"], ["alpha", "beta"]], + values=[rows, rows], + base_values=None, + y_pred=pd.Series(["a", "a"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=["a", "b", "c"], + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def test_value_is_the_strongest_classs_magnitude(self): + # Not the mean over classes, which would read 0.4 and 0.133 — smaller than any real + # contribution and matching nothing the single-class views show. + self.assertEqual(self._three_class().word_importance(label_idx=None).to_dict(), {"alpha": 0.6, "beta": 0.2}) + + def test_it_matches_what_the_driving_class_shows(self): + exp = self._three_class() + self.assertEqual(exp.word_importance(label_idx=None)["alpha"], abs(exp.word_importance(label_idx=2)["alpha"])) + + def test_signed_averaging_across_classes_would_be_zero(self): + # The reason the collapse discards sign: an explainer of a normalised output cancels + # across classes, so a signed cross-class mean is a chart of zeros. + exp = self._three_class() + per_class = np.array([exp.word_importance(label_idx=i)["alpha"] for i in range(3)]) + self.assertAlmostEqual(float(per_class.mean()), 0.0) + self.assertGreater(exp.word_importance(label_idx=None)["alpha"], 0.5) + + def test_single_output_model_collapses_to_the_absolute_value(self): + # A 1-D contribution array has no class axis to reduce over; it still must not raise. + texts = pd.Series(["alpha beta"]) + exp = NlpExplanation( + texts=texts, + token_strings=[["alpha", "beta"]], + values=[np.array([0.5, -0.2])], + base_values=None, + y_pred=pd.Series(["a"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + self.assertEqual(exp.word_importance(label_idx=None).to_dict(), {"alpha": 0.5, "beta": 0.2}) + + def test_every_value_is_a_magnitude(self): + self.assertTrue((_profile_explanation().word_importance(label_idx=None) >= 0).all()) + + def test_name_records_the_statistic(self): + exp = _profile_explanation() + self.assertEqual(exp.word_importance(label_idx=None).name, "mean_across_classes") + self.assertEqual(exp.word_importance(label_idx=None, rank_by="sum").name, "sum_across_classes") + + def test_a_negative_sign_filter_has_nothing_to_select(self): + self.assertTrue(_profile_explanation().word_importance(label_idx=None, filter_sign="negative").empty) + + def test_the_frequency_floor_still_applies(self): + exp = _profile_explanation() + self.assertIn("happy", exp.word_importance(label_idx=None, min_occurrences=4).index) + self.assertNotIn("today", exp.word_importance(label_idx=None, min_occurrences=2).index) + + def test_plotter_titles_and_labels_the_cross_class_view(self): + fig = _profile_explanation().plot.word_importance(label_idx=None) + self.assertEqual(fig.layout.title.text, "Word importance — all classes") + self.assertEqual(fig.layout.xaxis.title.text, "Largest |mean SHAP| across classes") + + +class TestWordImportanceReadability(unittest.TestCase): + """The chart must name every bar it draws, whatever K is.""" + + @staticmethod + def _imp(n): + return pd.Series( + [(-1) ** i * (n - i) / n for i in range(n)], + index=[f"word{i}" for i in range(n)], + ) + + def test_every_word_gets_a_tick(self): + # dtick=1 on a categorical axis is what stops plotly thinning labels on a long ranking. + fig = plot_word_importance(self._imp(50)) + self.assertEqual(fig.layout.yaxis.dtick, 1) + self.assertEqual(len(set(fig.data[0].y)), 50) + + def test_height_grows_with_the_word_count(self): + heights = [plot_word_importance(self._imp(n)).layout.height for n in (5, 20, 50)] + self.assertEqual(heights, sorted(heights)) + # Every word keeps room to render, rather than the chart being squeezed to a fixed panel. + for n, h in zip((5, 20, 50), heights): + self.assertGreaterEqual(h / n, 24) + + def test_explicit_height_still_wins(self): + self.assertEqual(plot_word_importance(self._imp(50), height=300).layout.height, 300) + + def test_value_labels_are_signed_and_on_by_default(self): + fig = plot_word_importance(pd.Series([0.9, -0.5], index=["a", "b"])) + self.assertEqual(list(fig.data[0].text), ["-0.500", "+0.900"]) # reversed for drawing + self.assertEqual(fig.data[0].textposition, "outside") + # Outside text sits past the bar end and would be cut off at the plot edge without this. + self.assertFalse(fig.data[0].cliponaxis) + + def test_value_labels_get_headroom(self): + fig = plot_word_importance(pd.Series([1.0, -0.5], index=["a", "b"])) + lo, hi = fig.layout.xaxis.range + self.assertLess(lo, -0.5) + self.assertGreater(hi, 1.0) + + def test_value_labels_can_be_turned_off(self): + fig = plot_word_importance(pd.Series([0.9], index=["a"]), show_values=False) + self.assertIsNone(fig.data[0].text) + # No outside text means no need to reserve headroom; plotly autoscales. + self.assertIsNone(fig.layout.xaxis.range) + + def test_degenerate_inputs_autoscale_rather_than_collapse(self): + # An all-zero (or empty) series has no span to pad, and a [0, 0] range would be invalid. + self.assertIsNone(plot_word_importance(pd.Series(dtype=float)).layout.xaxis.range) + self.assertIsNone(plot_word_importance(pd.Series([0.0, 0.0], index=["a", "b"])).layout.xaxis.range) + + def test_tick_font_is_set(self): + self.assertEqual(plot_word_importance(self._imp(3)).layout.yaxis.tickfont.size, 12) + + +class TestWordImportanceHover(unittest.TestCase): + """The hover names the statistic and, when supplied, how many occurrences it aggregated.""" + + imp = pd.Series([0.9, -0.5], index=["a", "b"]) + + def test_hover_names_the_statistic_on_the_axis(self): + # The axis title scrolls off a long chart; the hover is where the reader can still check + # whether they are looking at a mean or a total. + fig = plot_word_importance(self.imp, x_title="Total SHAP contribution") + self.assertIn("Total SHAP contribution: %{x:.4f}", fig.data[0].hovertemplate) + + def test_counts_are_reversed_with_the_bars(self): + fig = plot_word_importance(self.imp, counts={"a": 12, "b": 3}) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) + self.assertIn("Occurrences:", fig.data[0].hovertemplate) + + def test_counts_accept_a_series(self): + fig = plot_word_importance(self.imp, counts=pd.Series({"a": 12, "b": 3})) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) + + def test_without_counts_the_hover_is_unchanged(self): + fig = plot_word_importance(self.imp) + self.assertIsNone(fig.data[0].customdata) + self.assertNotIn("Occurrences", fig.data[0].hovertemplate) + + def test_a_partial_mapping_is_dropped_rather_than_shown_as_zero(self): + # A missing word can only mean the caller counted under different filters, which would + # misreport every bar — not just the uncovered one. + fig = plot_word_importance(self.imp, counts={"a": 12}) + self.assertIsNone(fig.data[0].customdata) + self.assertNotIn("Occurrences", fig.data[0].hovertemplate) + + +class TestWordCounts(unittest.TestCase): + """The frequency table behind the word picker's order, labels and threshold.""" + + def setUp(self): + self.explanation = _profile_explanation() + + def test_occurrences_and_samples_differ_for_a_repeated_word(self): + counts = self.explanation.word_counts() + # "happy" occurs 4x (twice in one sample) across 3 samples. + self.assertEqual(counts.loc["happy", "n_occurrences"], 4) + self.assertEqual(counts.loc["happy", "n_samples"], 3) + + def test_sorted_by_frequency_then_alphabetically(self): + counts = self.explanation.word_counts() + self.assertEqual(counts.index[0], "happy") + ties = counts[counts["n_occurrences"] == 1].index.tolist() + self.assertEqual(ties, sorted(ties)) + + def test_applies_the_same_filters_as_word_importance(self): + counts = self.explanation.word_counts() + self.assertNotIn("[CLS]", counts.index) + self.assertNotIn("!", counts.index) + self.assertNotIn("Happy", counts.index) # folded into "happy" + + def test_index_is_exactly_the_vocabulary(self): + self.assertEqual(sorted(self.explanation.word_counts().index), self.explanation.vocabulary()) + + def test_scoped_to_sample_indices(self): + counts = self.explanation.word_counts(sample_indices=[1]) + self.assertEqual(counts.loc["happy", "n_occurrences"], 2) + self.assertEqual(counts.loc["happy", "n_samples"], 1) + + def test_counts_are_the_denominator_word_importance_filters_on(self): + # The contract the min-occurrence control depends on: the count shown beside a word is the + # count its aggregate was computed over. + counts = self.explanation.word_counts() + for word, n in counts["n_occurrences"].items(): + kept = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n)) + self.assertIn(word, kept.index, word) + dropped = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n) + 1) + self.assertNotIn(word, dropped.index, word) + + def test_empty_batch_returns_an_empty_frame(self): + empty = _minimal_explanation([[]], [np.zeros((0, 2))], None) + self.assertTrue(empty.word_counts().empty) + self.assertEqual(empty.vocabulary(), []) + + +class TestWordOccurrences(unittest.TestCase): + def setUp(self): + self.explanation = _profile_explanation() + + def test_one_row_per_occurrence_and_class(self): + occ = self.explanation.word_occurrences("happy") + # 4 occurrences (one in sample 0, two in sample 1, one in sample 2) x 2 classes. + self.assertEqual(len(occ), 8) + self.assertEqual(list(occ.columns), ["sample", "token_pos", "token", "class_idx", "contribution"]) + + def test_case_folding_follows_the_model(self): + self.assertEqual(len(self.explanation.word_occurrences("HAPPY")), 8) + cased = _profile_explanation(folds_case=False) + self.assertEqual(len(cased.word_occurrences("happy")), 6) + self.assertEqual(len(cased.word_occurrences("Happy")), 2) + + def test_explicit_lowercase_overrides(self): + cased = _profile_explanation(folds_case=False) + self.assertEqual(len(cased.word_occurrences("happy", lowercase=True)), 8) + + def test_token_keeps_original_casing(self): + occ = self.explanation.word_occurrences("happy") + self.assertIn("Happy", set(occ["token"])) + + def test_sample_indices_scope(self): + occ = self.explanation.word_occurrences("happy", sample_indices=[2]) + self.assertEqual(set(occ["sample"]), {2}) + self.assertEqual(len(occ), 2) + + def test_missing_word_returns_typed_empty_frame(self): + occ = self.explanation.word_occurrences("absent") + self.assertTrue(occ.empty) + self.assertEqual(occ["contribution"].dtype, np.float64) + # An empty frame must still support the numeric work the callers do on it. + self.assertTrue(aggregate_word_contributions(occ, "mean_abs").empty) + self.assertTrue(rank_word_samples(occ, class_idx=0).empty) + + def test_binary_1d_values(self): + explanation = _minimal_explanation( + [["good", "day"], ["good"]], + [np.array([0.5, 0.1]), np.array([-0.3])], + None, + ) + occ = explanation.word_occurrences("good") + self.assertEqual(set(occ["class_idx"]), {0}) + self.assertEqual(sorted(occ["contribution"].round(3)), [-0.3, 0.5]) + + +class TestAggregateWordContributions(unittest.TestCase): + def setUp(self): + self.occ = _profile_explanation().word_occurrences("happy") + + def test_mean_is_signed(self): + stats = aggregate_word_contributions(self.occ, "mean") + # class 0: (0.4 + 0.2 + 0.1 - 0.6) / 4 + self.assertAlmostEqual(stats[0], 0.025) + self.assertAlmostEqual(stats[1], -0.025) + + def test_sum_scales_with_frequency(self): + stats = aggregate_word_contributions(self.occ, "sum") + self.assertAlmostEqual(stats[0], 0.1) + + def test_abs_forms_expose_the_two_way_word(self): + signed = aggregate_word_contributions(self.occ, "mean") + magnitude = aggregate_word_contributions(self.occ, "mean_abs") + # The whole point of the abs forms: a word averaging to ~0 is not a weak word. + self.assertLess(abs(signed[0]), 0.05) + self.assertAlmostEqual(magnitude[0], 0.325) + self.assertAlmostEqual(aggregate_word_contributions(self.occ, "sum_abs")[0], 1.3) + + def test_index_is_every_class_in_order(self): + self.assertEqual(list(aggregate_word_contributions(self.occ, "sum").index), [0, 1]) + + def test_unknown_agg_raises(self): + with self.assertRaisesRegex(ValueError, "not one of"): + aggregate_word_contributions(self.occ, "median") + + def test_all_documented_ops_run(self): + for agg in WORD_AGGREGATIONS: + self.assertEqual(len(aggregate_word_contributions(self.occ, agg)), 2) + + +class TestRankWordSamples(unittest.TestCase): + def setUp(self): + self.occ = _profile_explanation().word_occurrences("happy") + + def test_occurrences_are_summed_within_a_sample(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="most") + row = ranked[ranked["sample"] == 1].iloc[0] + self.assertAlmostEqual(row["contribution"], 0.3) # 0.2 + 0.1 + self.assertEqual(row["n_occurrences"], 2) + # One row per sample, never one per occurrence. + self.assertEqual(len(ranked), ranked["sample"].nunique()) + + def test_most_ranks_positive_first(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="most") + self.assertEqual(ranked["sample"].tolist(), [0, 1, 2]) + + def test_least_ranks_negative_first(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="least") + self.assertEqual(ranked["sample"].iloc[0], 2) + + def test_strongest_ignores_sign(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="strongest") + self.assertEqual(ranked["sample"].iloc[0], 2) # |-0.6| is the largest + + def test_class_idx_selects_the_column(self): + pos = rank_word_samples(self.occ, class_idx=0, order="most")["contribution"].tolist() + neg = rank_word_samples(self.occ, class_idx=1, order="most")["contribution"].tolist() + self.assertNotEqual(pos, neg) + + def test_n_top_truncates(self): + self.assertEqual(len(rank_word_samples(self.occ, class_idx=0, n_top=1)), 1) + + def test_unknown_order_raises(self): + with self.assertRaisesRegex(ValueError, "must be one of"): + rank_word_samples(self.occ, class_idx=0, order="alphabetical") + + def test_absent_class_returns_typed_empty(self): + ranked = rank_word_samples(self.occ, class_idx=99) + self.assertTrue(ranked.empty) + self.assertEqual(list(ranked.columns), ["sample", "contribution", "n_occurrences"]) + + +class TestPlotWordProfile(unittest.TestCase): + def setUp(self): + self.stats = pd.Series([0.2, -0.3], index=pd.Index([0, 1], name="class_idx")) + + def test_returns_figure_with_one_bar_per_class(self): + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data[0].x), 2) + + def test_class_zero_is_drawn_at_the_top(self): + # Plotly draws the y-axis bottom-to-top, so the first class must be last in the array. + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + self.assertEqual(fig.data[0].y[-1], "pos") + + def test_sign_drives_colour(self): + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + # Reversed alongside the values, so the last colour belongs to the positive class 0. + self.assertEqual(fig.data[0].marker.color[-1], "#1f77b4") + self.assertEqual(fig.data[0].marker.color[0], "#d62728") + + def test_falls_back_to_class_indices_without_names(self): + fig = plot_word_profile(self.stats) + self.assertEqual(set(fig.data[0].y), {"0", "1"}) + + def test_spread_is_reversed_with_the_bars(self): + spread = pd.Series([0.1, 0.9], index=pd.Index([0, 1], name="class_idx")) + fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) + # Class 1's spread must sit on class 1's bar, which is drawn first. + self.assertAlmostEqual(fig.data[0].error_x.array[0], 0.9) + + def test_misaligned_spread_does_not_shift_error_bars(self): + # A spread indexed on classes the stats do not carry must not silently slide onto them. + spread = pd.Series([0.1], index=pd.Index([5], name="class_idx")) + fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) + self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) + + def test_no_error_bars_without_spread(self): + fig = plot_word_profile(self.stats) + self.assertIsNone(fig.data[0].error_x.array) + + +class TestPlotterWordProfile(unittest.TestCase): + def setUp(self): + self.explanation = _profile_explanation() + + def test_returns_figure_titled_with_counts(self): + fig = self.explanation.plot.word_profile("happy") + self.assertIsInstance(fig, go.Figure) + self.assertIn("4 occurrence(s) in 3 sample(s)", fig.layout.title.text) + + def test_mean_draws_error_bars_and_sum_does_not(self): + self.assertIsNotNone(self.explanation.plot.word_profile("happy", agg="mean").data[0].error_x.array) + self.assertIsNone(self.explanation.plot.word_profile("happy", agg="sum").data[0].error_x.array) + + def test_single_occurrence_spread_is_zero_not_nan(self): + fig = self.explanation.plot.word_profile("today", agg="mean") + self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) + + def test_sample_indices_scope(self): + fig = self.explanation.plot.word_profile("happy", sample_indices=[1]) + self.assertIn("2 occurrence(s) in 1 sample(s)", fig.layout.title.text) + + def test_title_override(self): + self.assertEqual(self.explanation.plot.word_profile("happy", title="X").layout.title.text, "X") + + def test_absent_word_raises_rather_than_drawing_an_empty_chart(self): + with self.assertRaisesRegex(ValueError, "does not occur"): + self.explanation.plot.word_profile("absent") + + def test_absent_in_scope_names_the_scope(self): + with self.assertRaisesRegex(ValueError, "in the selected samples"): + self.explanation.plot.word_profile("happy", sample_indices=[3]) + + # --------------------------------------------------------------------------- # NlpLimeBackend # --------------------------------------------------------------------------- @@ -1394,6 +2008,7 @@ class _MarkerBackend(NlpBackend): name = "marker_backend" reference_kind = "none" is_additive = True + output_space = "probability" def __init__(self, marker=1.0, **kwargs): super().__init__(model=None, label_names=LABEL_NAMES, **kwargs) @@ -1685,9 +2300,7 @@ def test_repeats_are_served_from_the_memo(self): def test_different_arguments_recompute(self): xpl = _make_explainer() explanation = self._with_labels() - self.assertIsNot( - xpl.detect_label_noise(explanation, top_n=5), xpl.detect_label_noise(explanation, top_n=4) - ) + self.assertIsNot(xpl.detect_label_noise(explanation, top_n=5), xpl.detect_label_noise(explanation, top_n=4)) # ── independent probe ────────────────────────────────────────────── def _probe_corpus(self): diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 56a476f0..7b6601db 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -30,8 +30,16 @@ # A tiny lexically separable corpus for the independent label probe. "bad"/"awful" sit in "neg", # so a row labelled "neg" gets backed and a row labelled "pos" gets rejected. _PROBE_CORPUS = ( - ["i am happy", "happy and glad", "so glad today", "a happy glad day", - "this is bad", "bad and awful", "so awful today", "a bad awful day"], + [ + "i am happy", + "happy and glad", + "so glad today", + "a happy glad day", + "this is bad", + "bad and awful", + "so awful today", + "a bad awful day", + ], ["pos"] * 4 + ["neg"] * 4, ) @@ -97,6 +105,7 @@ def to_explanation(self) -> NlpExplanation: backend_name="fake", is_additive=True, reference_kind="none", + output_space="probability", ) def can_detect_label_noise(self, explanation=None): @@ -324,7 +333,8 @@ def test_full_tab_groups_when_all_panels_available(self): app._tab_groups, { "left-tabs": ["table", "scatter", "editor"], - "upper-right-tabs": ["importance", "counterfactual"], + # Word Profile is data-only, so it mounts unconditionally beside Word Importance. + "upper-right-tabs": ["importance", "word-profile", "counterfactual"], "lower-right-tabs": ["highlight", "waterfall"], }, ) @@ -343,7 +353,7 @@ def test_full_tab_groups_when_all_panels_available(self): def test_tabs_degrade_without_scatter_or_whatif(self): app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) self.assertEqual(app._tab_groups["left-tabs"], ["table"]) - self.assertEqual(app._tab_groups["upper-right-tabs"], ["importance"]) + self.assertEqual(app._tab_groups["upper-right-tabs"], ["importance", "word-profile"]) self.assertEqual(app._tab_groups["lower-right-tabs"], ["highlight", "waterfall"]) self.assertNotIn("left-tabs-body-scatter", ids) self.assertNotIn("left-tabs-body-editor", ids) @@ -430,7 +440,9 @@ def _register(): explanation = engine.to_explanation() app = dash.Dash(__name__) comp = SimilarExamplesComponent() - comp.register_callbacks(app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + comp.register_callbacks( + app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"} + ) return app, engine @staticmethod @@ -631,7 +643,9 @@ def _register(**kwargs): explanation = engine.to_explanation() app = dash.Dash(__name__) comp = LabelNoiseComponent() - comp.register_callbacks(app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"}) + comp.register_callbacks( + app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"} + ) return app, engine @staticmethod @@ -867,3 +881,579 @@ def test_empty_prefixes_serve_at_the_root(self): if __name__ == "__main__": unittest.main() + + +class TestWordProfileComponent(unittest.TestCase): + """The single-word profile panel: word × aggregation × global selection, then drill-down.""" + + STORES = { + "current": "current-datapoint", + "selection": "scatter-selected-indices", + "error_cell": "error-cell", + "errors_only": "errors-only-switch", + "word_click": "word-click-filter", + } + + @staticmethod + def _explanation(): + """Four samples, two classes. 'happy' pulls +0.4/+0.3 in two, -0.6 in a third.""" + token_strings = [ + ["so", "happy", "today"], + ["happy", "and", "happy"], + ["not", "happy", "at", "all"], + ["nothing", "here"], + ] + values = [ + np.array([[0.1, -0.1], [0.4, -0.4], [0.05, -0.05]]), + np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1]]), + np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), + np.array([[0.0, 0.0], [0.0, 0.0]]), + ] + texts = pd.Series(["so happy today", "happy and happy", "not happy at all", "nothing here"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), + y_prob=None, + # Sample 1 is the only model error, so errors-only scopes to exactly that row. + y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def _app(self): + import dash + + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + app = dash.Dash(__name__) + comp = WordProfileComponent() + comp.register_callbacks(app, explanation, None, self.STORES) + return app, explanation + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + # ── mounting / layout ─────────────────────────────────────────────── + def test_mounts_without_an_engine(self): + from shapash.webapp.nlp_components import WordProfileComponent + + # Data-only panel: it must survive a loaded snapshot with no live model. + self.assertTrue(WordProfileComponent.is_available(self._explanation(), None)) + + def test_layout_declares_every_id_its_callbacks_bind(self): + from shapash.webapp.nlp_components import WordProfileComponent + + found = set() + _collect_ids(WordProfileComponent().layout(self._explanation(), None), found) + for suffix in ("select", "agg", "class", "order", "limit", "graph", "caption", "results", "store"): + self.assertIn(f"word-profile-{suffix}", found) + + def test_word_dropdown_is_seeded_with_the_top_word(self): + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + layout = WordProfileComponent().layout(explanation, None) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + # Not empty on open, and the seed is a real corpus word. + self.assertIn(found["word-profile-select"].value, explanation.vocabulary()) + + def test_class_picker_hidden_for_a_single_output_column(self): + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + binary = replace( + explanation, + values=[v[:, 0] for v in explanation.values], + base_values=np.zeros(4), + label_names=["score"], + ) + layout = WordProfileComponent().layout(binary, None) + found = set() + _collect_ids(layout, found) + # Still present (callbacks bind it), just not shown. + self.assertIn("word-profile-class", found) + + # ── the profile callback ──────────────────────────────────────────── + def test_profile_reports_counts_and_one_bar_per_class(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "strongest", 10, None, None, False + ) + self.assertEqual(len(fig.data[0].x), 2) + self.assertIn("4 occurrence(s) in 3 of 4 sample(s)", caption) + self.assertIsNotNone(table) + self.assertEqual(store, [2, 0, 1]) # |-0.6| > 0.4 > 0.3 + + def test_mean_reports_its_error_bars_in_the_caption(self): + app, _ = self._app() + _, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 10, None, None, False) + self.assertIn("std across occurrences", caption) + + def test_sum_has_no_error_bars(self): + app, _ = self._app() + fig, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "sum", 0, "most", 10, None, None, False) + self.assertIsNone(fig.data[0].error_x.array) + self.assertNotIn("std", caption) + + def test_absolute_aggregation_surfaces_the_two_way_word(self): + app, _ = self._app() + graph = self._callback(app, "word-profile-graph") + signed = graph("happy", "mean", 0, "most", 10, None, None, False)[0].data[0].x + magnitude = graph("happy", "mean_abs", 0, "most", 10, None, None, False)[0].data[0].x + # The signed mean nearly cancels; the magnitude does not. This gap is the panel's point. + self.assertLess(abs(signed[-1]), 0.05) + self.assertGreater(magnitude[-1], 0.3) + + def test_order_reorders_the_drill_down_only(self): + app, _ = self._app() + graph = self._callback(app, "word-profile-graph") + self.assertEqual(graph("happy", "mean", 0, "most", 10, None, None, False)[3], [0, 1, 2]) + self.assertEqual(graph("happy", "mean", 0, "least", 10, None, None, False)[3][0], 2) + + def test_limit_truncates_the_drill_down(self): + app, _ = self._app() + store = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 1, None, None, False)[3] + self.assertEqual(len(store), 1) + + def test_scatter_selection_scopes_the_aggregate(self): + app, _ = self._app() + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, [0, 3], None, False + ) + self.assertIn("scoped to 2 selected sample(s)", caption) + self.assertEqual(store, [0]) + + def test_errors_only_scopes_the_aggregate(self): + app, _ = self._app() + # Sample 1 is the only misclassified row, and it does contain the word. + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, None, None, True + ) + self.assertIn("scoped to 1 selected sample(s)", caption) + self.assertEqual(store, [1]) + + def test_confusion_cell_scopes_the_aggregate(self): + app, _ = self._app() + _, caption, _, _ = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, None, {"pred": 1, "true": 0, "indices": [2]}, False + ) + self.assertIn("scoped to 1 selected sample(s)", caption) + + def test_word_absent_from_the_scope_says_so(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, [3], None, False + ) + self.assertIn("no occurrences", caption) + self.assertIn("does not occur", fig.layout.annotations[0].text) + self.assertIsNone(table) + self.assertEqual(store, []) + + def test_no_word_selected_shows_a_prompt(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + None, "mean", 0, "most", 10, None, None, False + ) + self.assertIn("Pick a word", fig.layout.annotations[0].text) + self.assertEqual((caption, table, store), ("", None, [])) + + def test_falls_back_to_defaults_on_cleared_controls(self): + app, _ = self._app() + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", None, None, None, None, None, None, None + ) + self.assertIn("Mean", caption) + self.assertEqual(len(store), 3) + + # ── cross-panel wiring ────────────────────────────────────────────── + def test_follows_a_word_bar_clicked_in_the_importance_panel(self): + app, _ = self._app() + follow = self._callback(app, "word-profile-select.value") + self.assertEqual(follow("today"), "today") + # With a scatter the store holds the multi-select list; the newest word wins. + self.assertEqual(follow(["today", "happy"]), "happy") + + def test_cleared_word_filter_leaves_the_selection_alone(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + follow = self._callback(app, "word-profile-select.value") + for cleared in (None, []): + with self.assertRaises(PreventUpdate): + follow(cleared) + + def test_inspect_packs_the_sample_into_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_profile as mod + + app, _ = self._app() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "word-profile-inspect", "index": 1} + # The store holds the displayed rows' positions, so row 1 is sample 0. + datapoint = inspect([None, 1], [2, 0]) + self.assertEqual(datapoint["orig_idx"], 0) + self.assertEqual(datapoint["text"], "so happy today") + self.assertEqual(datapoint["label"], "pos") + + def test_inspect_ignores_clicks_it_cannot_resolve(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import word_profile as mod + + app, _ = self._app() + inspect = self._callback(app, "current-datapoint") + with self.assertRaises(PreventUpdate): + inspect([None, None], [0, 1]) # no button actually clicked + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = None + with self.assertRaises(PreventUpdate): + inspect([1], [0]) + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "word-profile-inspect", "index": 5} + with self.assertRaises(PreventUpdate): + inspect([1], [0]) # index past the end of the displayed rows + + +class TestGlobalWordImportancePanel(unittest.TestCase): + """The shell's Word Importance chart: rank-by, frequency floor, and its empty states.""" + + @staticmethod + def _explanation(): + # "rare" pulls hard once; "common" pulls mildly four times. The two rank-by modes must + # disagree, which is the whole reason the control exists. + token_strings = [["rare", "common"], ["common"], ["common", "common"], ["mild"]] + values = [ + np.array([[0.9, -0.9], [0.3, -0.3]]), + np.array([[0.3, -0.3]]), + np.array([[0.3, -0.3], [0.3, -0.3]]), + np.array([[-0.5, 0.5]]), + ] + texts = pd.Series(["rare common", "common", "common common", "mild"]) + explanation = NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["neg"] * 4, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + return explanation + + @classmethod + def _app(cls): + app = NlpWebApp(cls._explanation(), engine=None) + for key, spec in app.app.callback_map.items(): + if "global-importance-graph.figure" in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError("global-importance-graph") + + @staticmethod + def _words(fig): + # Bars are drawn bottom-to-top, so reverse back into rank order. + return list(fig.data[0].y)[::-1] + + def setUp(self): + self.graph = self._app() + + def _call(self, rank_by="mean", floor=1, sign="all", topk=10, indices=None, cell=None, errors=False, label=0): + return self.graph(label, topk, sign, [], rank_by, floor, indices, cell, errors) + + @staticmethod + def _values(fig): + return list(fig.data[0].x)[::-1] + + def test_mean_and_sum_produce_different_rankings(self): + self.assertEqual(self._words(self._call(rank_by="mean"))[0], "rare") + self.assertEqual(self._words(self._call(rank_by="sum"))[0], "common") + + def test_axis_names_the_statistic_drawn(self): + self.assertEqual(self._call(rank_by="mean").layout.xaxis.title.text, "Mean SHAP contribution") + self.assertEqual(self._call(rank_by="sum").layout.xaxis.title.text, "Total SHAP contribution") + + def test_frequency_floor_removes_rare_words(self): + self.assertIn("rare", self._words(self._call(floor=1))) + self.assertNotIn("rare", self._words(self._call(floor=2))) + + def test_panel_has_no_title(self): + # The tab is already labelled "Word Importance" and the floor/class are visible in the + # filter row above the chart, so the figure itself carries no title band — reclaiming that + # vertical space for word rows instead. + self.assertIsNone(self._call(floor=2).layout.title.text) + self.assertIsNone(self._call(floor=1).layout.title.text) + + def test_sign_filter_still_works_in_both_modes(self): + for rank_by in ("mean", "sum"): + self.assertEqual(self._words(self._call(rank_by=rank_by, sign="negative")), ["mild"]) + self.assertNotIn("mild", self._words(self._call(rank_by=rank_by, sign="positive"))) + + def test_default_floor_is_two(self): + from shapash.webapp.nlp_app import _DEFAULT_MIN_OCCURRENCES + + # A mean over a single observation is not a mean; the panel must not open on one. + self.assertEqual(_DEFAULT_MIN_OCCURRENCES, 2) + + def test_cleared_floor_input_means_no_filter(self): + # An emptied number box arrives as None and must not silently restore the default. + self.assertIn("rare", self._words(self._call(floor=None))) + + def test_floor_below_one_is_clamped(self): + self.assertIn("rare", self._words(self._call(floor=0))) + + def test_impossible_floor_explains_itself(self): + fig = self._call(floor=99) + self.assertEqual(len(fig.data), 0) + self.assertIn("at least 99 time(s) in these 4 sample(s)", fig.layout.annotations[0].text) + + def test_empty_sign_filter_explains_itself_differently(self): + # Only "mild" is negative; excluding it by scope leaves the sign filter with nothing, and + # the message must point at the filter rather than at the frequency floor. + fig = self._call(sign="negative", indices=[0, 1]) + self.assertEqual(len(fig.data), 0) + self.assertIn("negative", fig.layout.annotations[0].text) + + def test_floor_counts_within_the_selection(self): + # "common" occurs 4x overall but once in sample 1, so a floor of 2 must exclude it there. + fig = self._call(floor=2, indices=[1]) + self.assertEqual(len(fig.data), 0) + self.assertIn("in these 1 sample(s)", fig.layout.annotations[0].text) + + def test_hover_carries_the_occurrence_count(self): + fig = self._call(floor=1) + drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) + self.assertEqual(drawn, {"rare": 1, "common": 4, "mild": 1}) + self.assertIn("Occurrences:", fig.data[0].hovertemplate) + + def test_hover_counts_follow_the_selection(self): + # A count that ignored the scope would contradict the floor applied right beside it: + # "common" occurs 4x in the corpus but twice in samples 0-1. + fig = self._call(floor=1, indices=[0, 1]) + drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) + self.assertEqual(drawn["common"], 2) + + def test_all_classes_ranks_on_the_strongest_class_magnitude(self): + # "mild" pulls -0.5 on class 0 and +0.5 on class 1: across classes it is a magnitude of + # 0.5, not the -0.5 the single-class view shows. + fig = self._call(label="all") + drawn = dict(zip(self._words(fig), self._values(fig))) + self.assertEqual(drawn, {"rare": 0.9, "mild": 0.5, "common": 0.3}) + self.assertEqual(dict(zip(self._words(self._call()), self._values(self._call())))["mild"], -0.5) + + def test_all_classes_names_its_statistic_on_the_axis(self): + self.assertEqual(self._call(label="all").layout.xaxis.title.text, "Largest |mean SHAP| across classes") + self.assertEqual( + self._call(label="all", rank_by="sum").layout.xaxis.title.text, + "Largest |total SHAP| across classes", + ) + + def test_all_classes_ignores_a_stale_sign_filter(self): + # Every value is a magnitude, so honouring "negative" would blank the chart on a control + # the user cannot even see is active. + self.assertEqual(self._words(self._call(label="all", sign="negative")), ["rare", "mild", "common"]) + + def test_all_classes_still_honours_the_frequency_floor(self): + self.assertNotIn("rare", self._words(self._call(label="all", floor=2))) + + def test_sign_filter_is_greyed_out_under_all_classes(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = None + for key, spec in app.app.callback_map.items(): + if "sign-filter.options" in key: + fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) + self.assertIsNotNone(fn, "sign-filter gating callback not registered") + options, value = fn("all") + self.assertEqual([o["value"] for o in options if o.get("disabled")], ["positive", "negative"]) + self.assertEqual(value, "all") + options, _ = fn(0) + self.assertFalse(any(o.get("disabled") for o in options)) + + def test_scatter_colouring_survives_all_classes(self): + # The scatter reads the same dropdown, where int("all") would raise. + app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + fn = None + for key, spec in app.app.callback_map.items(): + if "scatter-plot.figure" in key: + fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) + self.assertIsNotNone(fn, "scatter callback not registered") + self.assertIsNotNone(fn("word_contribution", ["common"], "all", False)) + + def test_number_boxes_commit_without_needing_a_blur(self): + # debounce=True commits only on Enter or blur, which leaves the spinner arrows looking + # dead: clicking one keeps focus in the box, so the chart never updates. A numeric + # (seconds) debounce commits after a pause instead. + layout = NlpWebApp(self._explanation(), engine=None).app.layout + for box_id in ("min-occurrences", "topk-input"): + debounce = layout[box_id].debounce + self.assertNotIsInstance(debounce, bool, f"{box_id} commits only on blur") + self.assertGreater(debounce, 0) + + def test_topk_input_replaces_the_slider(self): + found = set() + _collect_ids(NlpWebApp(self._explanation(), engine=None).app.layout, found) + self.assertIn("topk-input", found) + self.assertNotIn("topk-slider", found) + + def test_topk_controls_the_bar_count(self): + self.assertEqual(len(self._words(self._call(topk=2))), 2) + + def test_typed_topk_is_clamped_to_the_boxs_range(self): + from shapash.webapp.nlp_app import _MAX_TOPK, _MIN_TOPK + + # The browser enforces min/max on the spinner but not on typed input. + self.assertEqual(len(self._words(self._call(topk=999))), 3) # only 3 words exist + self.assertEqual(len(self._words(self._call(topk=0))), _MIN_TOPK) + self.assertEqual(len(self._words(self._call(topk=-5))), _MIN_TOPK) + self.assertLessEqual(_MAX_TOPK, 50) + + def test_cleared_topk_box_falls_back_to_the_default(self): + from shapash.webapp.nlp_app import _DEFAULT_TOPK + + self.assertEqual(_DEFAULT_TOPK, 20) + # Only None (an emptied box) restores the default — a typed 0 clamps instead. + self.assertEqual(len(self._words(self._call(topk=None))), 3) # all 3 words, under the cap + + def test_chart_keeps_its_computed_height_so_labels_survive(self): + # The panel scrolls; squeezing 50 words into it is what made plotly drop word labels. + fig = self._call(topk=50) + self.assertIsNotNone(fig.layout.height) + + def test_missing_class_prevents_update(self): + from dash.exceptions import PreventUpdate + + with self.assertRaises(PreventUpdate): + self.graph(None, 10, "all", [], "mean", 1, None, None, False) + + +class TestWordProfileControls(unittest.TestCase): + """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" + + def _layout(self): + from shapash.webapp.nlp_components import WordProfileComponent + + comp = WordProfileComponent() + layout = comp.layout(TestWordProfileComponent._explanation(), None) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + return comp, found + + def _app(self): + import dash + + from shapash.webapp.nlp_components import WordProfileComponent + + comp = WordProfileComponent() + explanation = TestWordProfileComponent._explanation() + comp.layout(explanation, None) # builds the option lists the sort callback serves + app = dash.Dash(__name__) + comp.register_callbacks(app, explanation, None, TestWordProfileComponent.STORES) + return app + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_sum_aggregations_are_not_offered(self): + # Per word, a sum is the mean rescaled by the occurrence count — same bars, no information. + _, found = self._layout() + self.assertEqual([o["value"] for o in found["word-profile-agg"].options], ["mean", "mean_abs"]) + + def test_options_carry_the_occurrence_count(self): + _, found = self._layout() + labels = {o["value"]: o["label"] for o in found["word-profile-select"].options} + self.assertEqual(labels["happy"], "happy (4)") + + def test_option_values_stay_bare_words(self): + # The label carries the count; the value must not, or every consumer of it breaks. + _, found = self._layout() + self.assertIn("happy", [o["value"] for o in found["word-profile-select"].options]) + + def test_sort_toggle_reorders_without_changing_values(self): + app = self._app() + reorder = self._callback(app, "word-profile-select.options") + alpha = [o["value"] for o in reorder("alpha")] + freq = [o["value"] for o in reorder("frequency")] + self.assertEqual(alpha, sorted(alpha)) + self.assertEqual(freq[0], "happy") # the most frequent word + self.assertEqual(set(alpha), set(freq)) + + def test_unknown_sort_falls_back_to_alphabetical(self): + app = self._app() + reorder = self._callback(app, "word-profile-select.options") + self.assertEqual(reorder(None), reorder("alpha")) + + def test_bar_click_sets_the_drill_down_class(self): + app = self._app() + click = self._callback(app, "word-profile-class.value") + value, reset = click({"points": [{"customdata": [1], "y": "pos"}]}) + self.assertEqual(value, 1) + # clickData is reset so clicking the same bar twice re-fires. + self.assertIsNone(reset) + + def test_bar_click_reads_the_index_not_the_label(self): + app = self._app() + click = self._callback(app, "word-profile-class.value") + # Duplicate display names must not be able to mis-resolve the class. + self.assertEqual(click({"points": [{"customdata": [0], "y": "neg"}]})[0], 0) + + def test_graph_carries_the_class_index_on_every_bar(self): + app = self._app() + fig = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 5, None, None, False)[0] + self.assertEqual([c[0] for c in fig.data[0].customdata], [1, 0]) # reversed for drawing + + def test_empty_clicks_are_ignored(self): + from dash.exceptions import PreventUpdate + + app = self._app() + click = self._callback(app, "word-profile-class.value") + for bad in (None, {}, {"points": []}, {"points": [{}]}): + with self.assertRaises(PreventUpdate): + click(bad) From 2be411a04d196644445f1169316235f426420222 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Tue, 8 Sep 2026 15:34:27 +0200 Subject: [PATCH 100/144] refactor(webapp): rewrite nlp app layout - split build_layout into focused builder methods - extract scatter and word importance components - add customizable coloring theming (NlpTheme) - allow to magnify tabs - filtering corrections - add info button - change style of header + logo --- demo/serve_nlp.py | 2 + shapash/explainer/nlp_explainer.py | 31 +- shapash/explainer/nlp_explanation.py | 22 + shapash/model/encoder.py | 30 +- shapash/plots/plot_sentence_highlight.py | 35 +- shapash/plots/plot_token_highlight.py | 14 +- shapash/plots/plot_waterfall.py | 15 +- shapash/plots/plot_word_importance.py | 18 +- shapash/plots/plot_word_profile.py | 12 +- shapash/style/colors.json | 4 + shapash/style/style_utils.py | 50 + shapash/webapp/assets/style.css | 16 + shapash/webapp/nlp_app.py | 1251 ++++++----------- shapash/webapp/nlp_components/__init__.py | 6 + shapash/webapp/nlp_components/base.py | 31 +- .../webapp/nlp_components/counterfactual.py | 2 +- shapash/webapp/nlp_components/data_editor.py | 2 +- .../webapp/nlp_components/error_analysis.py | 8 +- shapash/webapp/nlp_components/label_noise.py | 2 +- shapash/webapp/nlp_components/scatter.py | 385 +++++ .../nlp_components/sentence_highlight.py | 12 +- shapash/webapp/nlp_components/waterfall.py | 6 + .../webapp/nlp_components/word_importance.py | 396 ++++++ shapash/webapp/nlp_components/word_profile.py | 6 +- tests/unit_tests/nlp/test_nlp_prototype.py | 8 +- tests/unit_tests/style/test_style_utils.py | 29 + .../unit_tests/webapp/test_nlp_whatif_app.py | 236 +++- 27 files changed, 1732 insertions(+), 897 deletions(-) create mode 100644 shapash/webapp/nlp_components/scatter.py create mode 100644 shapash/webapp/nlp_components/word_importance.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 2da6839d..2c8ac379 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -1007,6 +1007,7 @@ def main() -> None: # Step 3 — the expensive call, and the only one that produces results. The returned artifact is # immutable and holds no model, no backend and no explainer handle; ground truth rides on it # (never on xpl), which is why a cache hit still gets *this* run's y_true attached. + dataset_label = str(config.dataset_path) if config.is_local_dataset else config.dataset_name explanation = xpl.explain(sentences, y=y_true, cache_dir=explain_dir) # Step 4 — everything downstream reads the artifact as an argument. @@ -1042,6 +1043,7 @@ def main() -> None: host=config.host, scatter_xy=projected, url_base_pathname=config.url_base_path, + info={"Dataset": dataset_label}, ) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 14273421..7feef441 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -362,6 +362,8 @@ def explain( is_additive=backend_cls.is_additive, reference_kind=backend_cls.reference_kind, output_space=backend_cls.output_space, + model_id=getattr(self._text_model, "checkpoint", None), + architecture=getattr(self._text_model, "architecture", None), ) if cache_path is not None: cache_path.parent.mkdir(parents=True, exist_ok=True) @@ -501,6 +503,9 @@ def run_app( host: str = "127.0.0.1", scatter_xy=None, url_base_pathname: str | None = None, + palette_name: str = "default", + colors_dict: dict[str, str] | None = None, + info: dict[str, str] | None = None, ) -> None: """Launch the NLP explanation webapp for ``explanation``. @@ -539,10 +544,30 @@ def run_app( routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Must match the proxied path exactly; see :class:`~shapash.webapp.nlp_app.NlpWebApp`. ``None`` serves at ``/``. + palette_name, colors_dict + Theme the webapp's header band — see :class:`~shapash.webapp.nlp_app.NlpWebApp`. + info : dict[str, str], optional + Extra facts shown in the header's "ⓘ" popover, layered on top of what this call already + knows: ``explanation`` carries the model checkpoint/architecture + (:attr:`~shapash.explainer.nlp_explanation.NlpExplanation.model_id`/``architecture`` — + see :meth:`explain`) and sample/class counts, and — when :meth:`fit` was called with a + reference corpus — this adds "Train samples" from it. Pass *info* for anything neither + source can know — e.g. ``{"Dataset": "dair-ai/emotion"}`` for a caller-supplied dataset + label — or to override a value from either. """ - NlpWebApp(explanation, engine=self, scatter_xy=scatter_xy, url_base_pathname=url_base_pathname).run( - port=port, debug=debug, host=host - ) + # fit() is the one place that knows the reference corpus, so its size is added here rather + # than living on `explanation` (which is model-free and never sees the training set at all). + reference = getattr(self, "reference_", None) + engine_info = {"Train samples": str(len(reference[0]))} if reference is not None else {} + NlpWebApp( + explanation, + engine=self, + scatter_xy=scatter_xy, + url_base_pathname=url_base_pathname, + palette_name=palette_name, + colors_dict=colors_dict, + info={**engine_info, **(info or {})}, + ).run(port=port, debug=debug, host=host) # ------------------------------------------------------------------ # InteractiveEngine — live what-if surface (see explainer/interactive.py) diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index 26a32f8c..6c0421cd 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -256,6 +256,15 @@ class NlpExplanation: :meth:`~shapash.model.base.SupportsTokenization.folds_case`. Decides the default unit grouping in :meth:`word_importance` — see :meth:`resolve_lowercase`. ``None`` when the model exposes no tokenizer to ask. + model_id : str or None + The model's checkpoint identity (e.g. an HF hub id or local path) at ``explain()`` time — + read off ``TextModel.checkpoint`` (see :attr:`~shapash.model.encoder.EncoderClassifierModel.checkpoint`) + when the bound model exposes one. Purely descriptive: a plain string, never a live model + handle, so it does not compromise this class being model-free. ``None`` for a model with no + such identity (e.g. a bare LIME ``classifier_fn``). + architecture : str or None + Best-effort architecture tag (e.g. ``"distilbert"``), read off ``TextModel.architecture`` + when available. ``None`` on the same models that leave :attr:`model_id` ``None``. backend_name : str ``type(backend).name`` — which explanation method produced this artifact. is_additive : bool @@ -286,6 +295,10 @@ class NlpExplanation: is_additive: bool reference_kind: Literal["distribution", "statistics", "point", "none"] output_space: Literal["probability", "logit"] + # Purely descriptive metadata (see the docstrings above) — defaulted so every existing + # NlpExplanation(...) call site (library and test) keeps working unchanged. + model_id: str | None = None + architecture: str | None = None # ClassVar, not a field: it is a shared constant, not per-explanation data, so it stays out # of ``fields()`` — and therefore out of ``__init__``, ``replace()`` and ``save()``. @@ -355,6 +368,8 @@ def _check_shared_index(self) -> None: "is_additive", "reference_kind", "output_space", + "model_id", + "architecture", } ) @@ -936,6 +951,8 @@ def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: "output_space": self.output_space, "label_names": self.label_names, "folds_case": self.folds_case, + "model_id": self.model_id, + "architecture": self.architecture, # Authoritative counts, not re-derived from the tidy tables on load: a sample with # zero tokens has no row in contrib_df at all, so inferring n_samples from the table # would silently drop it (and n_classes has nothing to read from an all-empty batch). @@ -1020,6 +1037,11 @@ def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: # explained raw logits, everything else probabilities. output_space=meta.get("output_space") or ("logit" if meta["backend_name"] == "nlp_captum_lig" else "probability"), + # Absent (None) on a file saved before these fields existed, or when the model that + # produced it exposed no such identity — either way a webapp reading it just omits + # the fact rather than showing a stale placeholder. + model_id=meta.get("model_id"), + architecture=meta.get("architecture"), ) return explanation, scatter_xy diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index 04a10382..e0818650 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -272,19 +272,39 @@ def _validate_space(self, space: str) -> None: f"of the backbone. Available top-level modules: {top_level}." ) + @property + def checkpoint(self) -> str: + """Human-readable checkpoint identity — the backbone's ``config._name_or_path``. + + Falls back to the backbone's class name for a backbone with no HF-style config (e.g. a + hand-rolled ``nn.Module`` body). Display-oriented: for a *cache* key see :attr:`model_id`, + which adds the settings that change the vectors this checkpoint alone does not determine. + """ + config = getattr(self.backbone, "config", None) + return getattr(config, "_name_or_path", None) or type(self.backbone).__name__ + + @property + def architecture(self) -> str: + """Best-effort architecture tag (e.g. ``"distilbert"``), from the backbone's HF config. + + Reads ``config.model_type``, transformers' own architecture tag; falls back to the + backbone's class name when the backbone carries no such config (same fallback as + :attr:`checkpoint`, for the same reason). + """ + config = getattr(self.backbone, "config", None) + return getattr(config, "model_type", None) or type(self.backbone).__name__ + @property def model_id(self) -> str: """Stable identity for this adapter *and every setting that changes its vectors/scores*. Downstream caches (the similar-example bank, the projection cache) key on this. It therefore - includes the backbone's checkpoint name **and** ``pool`` / ``normalize`` — two models that differ - only in pooling produce different embeddings, and keying on the class name alone would make them + includes :attr:`checkpoint` **and** ``pool`` / ``normalize`` — two models that differ only in + pooling produce different embeddings, and keying on the checkpoint alone would make them silently share a cache entry. """ - config = getattr(self.backbone, "config", None) - checkpoint = getattr(config, "_name_or_path", None) or type(self.backbone).__name__ pool = self.pool if isinstance(self.pool, str) else getattr(self.pool, "__name__", "callable") - return f"{type(self).__name__}:{checkpoint}:pool={pool}:norm={int(self.normalize)}" + return f"{type(self).__name__}:{self.checkpoint}:pool={pool}:norm={int(self.normalize)}" # ------------------------------------------------------------------ # Batched forward (shared by predict / embed) diff --git a/shapash/plots/plot_sentence_highlight.py b/shapash/plots/plot_sentence_highlight.py index 077acea6..91f1f2ff 100644 --- a/shapash/plots/plot_sentence_highlight.py +++ b/shapash/plots/plot_sentence_highlight.py @@ -2,8 +2,8 @@ Renders a sentence as a sequence of coloured ``html.Span`` elements where each token's background intensity is proportional to its SHAP contribution magnitude. -Positive contributions are blue (#1f77b4), negative are red (#d62728), matching -``plot_word_importance`` and ``plot_waterfall``. +Positive/negative contributions use ``NlpTheme.xpl_positive``/``xpl_negative`` (the same colors +``plot_word_importance`` and ``plot_waterfall`` use), interpolated from white by magnitude. """ from __future__ import annotations @@ -13,17 +13,24 @@ import numpy as np from dash import html +from shapash.style.style_utils import DEFAULT_NLP_THEME + _SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") -_POS_RGB = (31, 119, 180) # #1f77b4 -_NEG_RGB = (214, 39, 40) # #d62728 -def _shap_color(val: float, max_abs: float) -> str: +def _parse_rgb(color: str) -> tuple[int, int, int]: + """Parse a ``rgb(r, g, b)``/``rgba(r, g, b, a)`` string (the ``colors.json`` format) to ints.""" + nums = color.replace("rgba", "").replace("rgb", "").replace("(", "").replace(")", "").split(",") + r, g, b = (int(float(n)) for n in nums[:3]) + return r, g, b + + +def _shap_color(val: float, max_abs: float, pos_rgb: tuple[int, int, int], neg_rgb: tuple[int, int, int]) -> str: """Interpolate from white toward the sign-appropriate colour.""" if max_abs == 0 or val == 0: return "transparent" t = min(abs(val) / max_abs, 1.0) - r_base, g_base, b_base = _POS_RGB if val >= 0 else _NEG_RGB + r_base, g_base, b_base = pos_rgb if val >= 0 else neg_rgb r = int(255 + t * (r_base - 255)) g = int(255 + t * (g_base - 255)) b = int(255 + t * (b_base - 255)) @@ -35,6 +42,8 @@ def plot_sentence_highlight( values: np.ndarray, base_value: float | None = None, title: str = "Sentence Highlight", + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> html.Div: """Inline sentence with background-coloured spans proportional to SHAP contribution. @@ -55,6 +64,12 @@ def plot_sentence_highlight( title : str Unused (callers render their own ``html.H6`` header); kept for API symmetry with the other plot functions. + color_positive, color_negative : str + Span background for a non-negative / negative contribution, interpolated from white by + magnitude. Must be a ``rgb(r, g, b)``/``rgba(r, g, b, a)`` string (the ``colors.json`` + format). Default to the ``"default"`` palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` + — see :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to + theme every NLP chart at once instead of overriding this one call. Returns ------- @@ -69,6 +84,8 @@ def plot_sentence_highlight( if max_abs == 0: max_abs = 1.0 + pos_rgb, neg_rgb = _parse_rgb(color_positive), _parse_rgb(color_negative) + _span_base = { "padding": "3px 5px", "borderRadius": "3px", @@ -94,7 +111,7 @@ def plot_sentence_highlight( spans.append( html.Span( tok + " ", - style={**_span_base, "backgroundColor": _shap_color(float(val), max_abs)}, + style={**_span_base, "backgroundColor": _shap_color(float(val), max_abs, pos_rgb, neg_rgb)}, title=tooltip, ) ) @@ -118,9 +135,9 @@ def plot_sentence_highlight( legend = html.Div( [ - html.Span("■ ", style={"color": "#1f77b4"}), + html.Span("■ ", style={"color": color_positive}), html.Span("positive ", style={"fontSize": "0.78em", "color": "#555"}), - html.Span("■ ", style={"color": "#d62728"}), + html.Span("■ ", style={"color": color_negative}), html.Span("negative", style={"fontSize": "0.78em", "color": "#555"}), ], style={"marginBottom": "6px"}, diff --git a/shapash/plots/plot_token_highlight.py b/shapash/plots/plot_token_highlight.py index c22e960c..44bf89eb 100644 --- a/shapash/plots/plot_token_highlight.py +++ b/shapash/plots/plot_token_highlight.py @@ -5,8 +5,7 @@ import numpy as np from plotly import graph_objs as go -_COLOR_POSITIVE = "#1f77b4" -_COLOR_NEGATIVE = "#d62728" +from shapash.style.style_utils import DEFAULT_NLP_THEME def plot_token_highlight( @@ -16,10 +15,12 @@ def plot_token_highlight( max_tokens: int | None = None, width: int = 900, height: int | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> go.Figure: """Horizontal bar chart of token-level SHAP contributions for one sample. - Positive contributions are shown in blue, negative in red. + Positive contributions are shown in *color_positive*, negative in *color_negative*. Tokens are displayed in sentence order (top to bottom). When ``max_tokens`` is set, only the highest-magnitude tokens are kept but their relative order in the sentence is preserved. @@ -39,6 +40,11 @@ def plot_token_highlight( Figure width in pixels. height : int, optional Figure height in pixels. Defaults to ``max(400, 30 * n_tokens + 120)``. + color_positive, color_negative : str + Bar colors for a non-negative / negative contribution. Default to the ``"default"`` + palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in ``shapash/style/colors.json`` — see + :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to theme + every NLP chart at once instead of overriding this one call. Returns ------- @@ -58,7 +64,7 @@ def plot_token_highlight( tokens = [tokens[i] for i in keep] values = values[keep] - colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + colors = [color_positive if v >= 0 else color_negative for v in values] # Plotly renders y-axis bottom-to-top; reverse so sentence reads top-to-bottom. fig = go.Figure( diff --git a/shapash/plots/plot_waterfall.py b/shapash/plots/plot_waterfall.py index 521fc47e..b9586367 100644 --- a/shapash/plots/plot_waterfall.py +++ b/shapash/plots/plot_waterfall.py @@ -14,6 +14,8 @@ import numpy as np from plotly import graph_objs as go +from shapash.style.style_utils import DEFAULT_NLP_THEME + _SPECIAL_RE = re.compile(r"^\[.*\]$|^##|^\s*$") @@ -25,11 +27,13 @@ def plot_waterfall( filter_special: bool = True, title: str = "Token contributions", width: int | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> go.Figure: """Horizontal waterfall chart decomposing a single prediction into token SHAP contributions. Each token appears as a horizontal bar segment. Positive contributions - extend right (blue), negative extend left (red). Connectors show the + extend right (*color_positive*), negative extend left (*color_negative*). Connectors show the cumulative path from the baseline to the final prediction. Tokens with ``|value| < min_pct * max_abs`` are aggregated into a single @@ -54,6 +58,11 @@ def plot_waterfall( Figure title. width : int, optional Figure width in pixels. Defaults to Plotly responsive behaviour. + color_positive, color_negative : str + Bar colors for a non-negative / negative contribution. Default to the ``"default"`` + palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in ``shapash/style/colors.json`` — see + :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to theme + every NLP chart at once instead of overriding this one call. Returns ------- @@ -138,8 +147,8 @@ def plot_waterfall( measure=measures, text=texts, textposition="outside", - increasing=dict(marker=dict(color="#1f77b4")), - decreasing=dict(marker=dict(color="#d62728")), + increasing=dict(marker=dict(color=color_positive)), + decreasing=dict(marker=dict(color=color_negative)), totals=dict(marker=dict(color="#7f7f7f")), connector=dict(line=dict(color="#dddddd", width=1, dash="dot")), cliponaxis=False, diff --git a/shapash/plots/plot_word_importance.py b/shapash/plots/plot_word_importance.py index bfd2c62f..d146d1e7 100644 --- a/shapash/plots/plot_word_importance.py +++ b/shapash/plots/plot_word_importance.py @@ -7,8 +7,7 @@ import pandas as pd from plotly import graph_objs as go -_COLOR_POSITIVE = "#1f77b4" -_COLOR_NEGATIVE = "#d62728" +from shapash.style.style_utils import DEFAULT_NLP_THEME # Vertical room per word. The y axis is categorical and every category gets a tick (dtick=1 # below), so this is what decides whether those ticks have room to render at all: squeeze 50 words @@ -83,13 +82,15 @@ def plot_word_importance( height: int | None = None, show_values: bool = True, counts: pd.Series | Mapping[str, int] | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> go.Figure: """Horizontal bar chart of aggregated token-level SHAP contributions per word. Words are displayed in the order given (highest at top). Positive contributions are shown - in blue, negative in red — which holds whichever statistic produced them, since - :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_importance` ranks by absolute - value but returns signed numbers. + in *color_positive*, negative in *color_negative* — which holds whichever statistic produced + them, since :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_importance` ranks by + absolute value but returns signed numbers. Parameters ---------- @@ -122,6 +123,11 @@ def plot_word_importance( :meth:`~shapash.explainer.nlp_explanation.NlpExplanation.word_counts` computed with the *same* filters and sample scope as the ranking. A mapping that does not cover every plotted word is ignored outright rather than shown partially. + color_positive, color_negative : str + Bar colors for a non-negative / negative contribution. Default to the ``"default"`` + palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in ``shapash/style/colors.json`` — see + :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to theme + every NLP chart at once instead of overriding this one call. Returns ------- @@ -135,7 +141,7 @@ def plot_word_importance( """ words = [str(w) for w in word_imp.index] values = [float(v) for v in word_imp.to_numpy()] - colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + colors = [color_positive if v >= 0 else color_negative for v in values] labels = [f"{v:+.3f}" for v in values] if show_values else None occurrences = _aligned_counts(words, counts) diff --git a/shapash/plots/plot_word_profile.py b/shapash/plots/plot_word_profile.py index f11e5163..1958d92d 100644 --- a/shapash/plots/plot_word_profile.py +++ b/shapash/plots/plot_word_profile.py @@ -5,8 +5,7 @@ import pandas as pd from plotly import graph_objs as go -_COLOR_POSITIVE = "#1f77b4" -_COLOR_NEGATIVE = "#d62728" +from shapash.style.style_utils import DEFAULT_NLP_THEME def plot_word_profile( @@ -17,6 +16,8 @@ def plot_word_profile( title: str = "Word profile", width: int | None = 640, height: int | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> go.Figure: """Horizontal bar chart of one word's aggregated contribution to each class. @@ -43,6 +44,11 @@ def plot_word_profile( Figure title. width, height : int, optional Figure size in pixels. ``height`` defaults to a size that fits the number of classes. + color_positive, color_negative : str + Bar colors for a non-negative / negative contribution. Default to the ``"default"`` + palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in ``shapash/style/colors.json`` — see + :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to theme + every NLP chart at once instead of overriding this one call. Returns ------- @@ -57,7 +63,7 @@ def plot_word_profile( class_idx = list(stats.index) names = [label_names[i] if label_names is not None and i < len(label_names) else str(i) for i in class_idx] values = [float(v) for v in stats.to_numpy()] - colors = [_COLOR_POSITIVE if v >= 0 else _COLOR_NEGATIVE for v in values] + colors = [color_positive if v >= 0 else color_negative for v in values] errors: list[float] | None = None if spread is not None and len(spread): diff --git a/shapash/style/colors.json b/shapash/style/colors.json index c21f9668..9b80ca07 100644 --- a/shapash/style/colors.json +++ b/shapash/style/colors.json @@ -85,6 +85,8 @@ "0": "rgba(52, 55, 54, 0.8)", "1": "rgba(52, 55, 54, 0.8)" }, + "nlp_xpl_negative": "rgb(214, 39, 40)", + "nlp_xpl_positive": "rgb(31, 119, 180)", "prediction_plot": { "0": "rgba(0, 70, 92, 0.9)", "1": "rgba(168, 84, 0, 0.9)" @@ -207,6 +209,8 @@ "0": "rgba(52, 55, 54, 0.8)", "1": "rgba(52, 55, 54, 0.8)" }, + "nlp_xpl_negative": "rgb(214, 39, 40)", + "nlp_xpl_positive": "rgb(31, 119, 180)", "prediction_plot": { "0": "rgba(255, 77, 7, 0.9)", "1": "rgba(52, 55, 54, 0.9)" diff --git a/shapash/style/style_utils.py b/shapash/style/style_utils.py index 91cc1435..de163b1b 100644 --- a/shapash/style/style_utils.py +++ b/shapash/style/style_utils.py @@ -4,6 +4,7 @@ import json import os +from dataclasses import dataclass import numpy as np @@ -182,6 +183,55 @@ def get_palette(palette_name): return select_palette(colors_loading(), palette_name) +@dataclass(frozen=True) +class NlpTheme: + """Resolved colors the NLP webapp and its plots draw from — a palette with overrides merged in. + + ``xpl_positive``/``xpl_negative`` name the concept "explanation pushes the prediction + up/down" rather than any one backend's algorithm (``nlp_shap`` and ``nlp_captum_lig`` both + produce signed per-token contributions, drawn with these two colors) — see + ``docs/architecture/explanation-space.md``. + """ + + header_bkg: str + header_accent: str + xpl_positive: str + xpl_negative: str + + +def resolve_nlp_theme(palette_name: str = "default", colors_dict: dict[str, str] | None = None) -> NlpTheme: + """Resolve the NLP webapp's theme: *palette_name* from ``colors.json``, *colors_dict* merged on top. + + The single place every NLP color customization funnels through — :class:`~shapash.webapp.nlp_app.NlpWebApp` + resolves it once from its ``palette_name``/``colors_dict`` constructor arguments; every plot + function in :mod:`shapash.plots` that draws a signed contribution defaults its own color + arguments from :data:`DEFAULT_NLP_THEME` below, so a bare call (a doctest, a notebook via + ``NlpExplanation.plot``) still renders in the same default colors without going through the + webapp at all. + + Parameters + ---------- + palette_name : str + Name of a palette in ``colors.json``. + colors_dict : dict[str, str], optional + Per-key overrides merged on top of the selected palette, same key names as ``colors.json``. + + Returns + ------- + NlpTheme + """ + palette = {**select_palette(colors_loading(), palette_name), **(colors_dict or {})} + return NlpTheme( + header_bkg=palette["webapp_bkg"], + header_accent=palette["webapp_title"], + xpl_positive=palette["nlp_xpl_positive"], + xpl_negative=palette["nlp_xpl_negative"], + ) + + +DEFAULT_NLP_THEME = resolve_nlp_theme() + + def get_pyplot_color(colors): """ Returns the color(s) of the color_name key in the palette in matplotlib format. diff --git a/shapash/webapp/assets/style.css b/shapash/webapp/assets/style.css index fc414257..b13a545f 100644 --- a/shapash/webapp/assets/style.css +++ b/shapash/webapp/assets/style.css @@ -144,6 +144,22 @@ h4 { z-index: 998; } +/* NLP app: magnifies a `_tabbed_card` panel to the full viewport (see nlp_app.py's + panel-card/panel-expand-btn clientside callback). The card's own flex layout fills whatever + box it is given, so no per-panel sizing is needed here beyond taking over the viewport. */ +.fullscreen-overlay { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: auto !important; + height: auto !important; + margin: 0 !important; + z-index: 2000 !important; + background-color: white; +} + /* PLOTLY MODEBAR */ .modebar { position: absolute; diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 8b2fea92..d8da1f70 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -1,10 +1,10 @@ """Minimal Dash webapp for NLP text classification explanations. Prototype bridge toward Phase 5b (composable WebappComponents). Panels are being extracted into -``WebappComponent``s one at a time (see ``shapash/webapp/nlp_components/``); global word importance, -the dataset table, and scatter are still built inline here pending their own extraction. All -tabular-only SmartApp panels (violin, cluster, scatter prediction picking) beyond what NLP needs are -absent rather than disabled. +``WebappComponent``s one at a time (see ``shapash/webapp/nlp_components/``); the dataset table and +the persistent selection bar are the only pieces still built inline — both are mandatory shell UI, +not optional panels, so there is nothing left to extract them into. All tabular-only SmartApp panels +(violin, cluster, scatter prediction picking) beyond what NLP needs are absent rather than disabled. """ from __future__ import annotations @@ -14,78 +14,55 @@ import dash_bootstrap_components as dbc import numpy as np import pandas as pd -import plotly.graph_objs as go -from dash import Input, Output, callback_context, dcc, html +from dash import MATCH, Input, Output, dcc, html from dash.exceptions import PreventUpdate from shapash.explainer.interactive import InteractiveEngine -from shapash.explainer.nlp_explanation import NlpExplanation, select_label_column -from shapash.plots.plot_word_importance import ( - empty_word_figure, - plot_word_importance, - word_importance_axis_title, -) +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.style.style_utils import NlpTheme, resolve_nlp_theme from shapash.webapp.nlp_components import ( CounterfactualComponent, DataEditorComponent, ErrorAnalysisComponent, LabelNoiseComponent, + ScatterComponent, SentenceHighlightComponent, SimilarExamplesComponent, WaterfallComponent, + WebappComponent, + WordImportanceComponent, WordProfileComponent, compose_selection, + error_positions, pack_datapoint, ) _APPLY_STORE = "whatif-apply-store" _CURRENT_STORE = "current-datapoint" -# The frequency floor the global word ranking starts at. 1 (no filter) is the status quo and is -# actively misleading: on the emotion demo 58% of the vocabulary occurs exactly once, so an -# unfiltered |mean| ranking is a list of single attributions. 2 is the principled minimum — a mean -# over one observation is not a mean — and is the least surprising default; 3 is where the chart -# visibly settles into repeated, stable words. -_DEFAULT_MIN_OCCURRENCES = 2 - -# Bounds for the Top-K box. The browser enforces these on the spinner arrows but not on typed -# input, so the callback clamps rather than trusting them. -_MIN_TOPK, _MAX_TOPK, _DEFAULT_TOPK = 1, 50, 20 -# Idle delay (seconds) before a number box commits its value. ``debounce=True`` would commit only on -# Enter or blur, which leaves the spinner arrows apparently dead: clicking one keeps focus in the -# box, so nothing reaches the server until the user clicks away. A numeric debounce commits after a -# pause instead, so arrows respond while a typed "20" still does not recompute at "2". -_INPUT_DEBOUNCE_S = 0.4 - -# Class-dropdown value meaning "collapse the ranking across every class" — a string so it can never -# collide with a real (integer) class index. -_ALL_CLASSES = "all" - -# Shared by the layout and the callback that greys the sign options out under "All classes", so the -# two cannot drift apart. -_SIGN_FILTER_OPTIONS: list[dcc.RadioItems.Options] = [ - {"label": " All", "value": "all"}, - {"label": " Positive", "value": "positive"}, - {"label": " Negative", "value": "negative"}, -] - _HIDDEN = {"display": "none"} # The visible tab body is a flex-column item that fills the bodies container (which itself fills the # card). This unbroken flex chain is what lets a body's inner content use height:100% / flex:1 (e.g. the # dataset grid) to fill the panel; a plain `display:block` here would collapse to content height. -_VISIBLE = {"display": "flex", "flexDirection": "column", "flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"} -_PALETTE = [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", -] +# overflowX is pinned to "hidden" rather than left at its "visible" default: per the CSS spec, an axis +# left "visible" next to a sibling axis that isn't computes to "auto" instead — so without this, any +# child a stray pixel wider than the panel (a border, a scrollbar's own width) silently grows a +# horizontal scrollbar here, even though nothing in this layout is meant to scroll sideways. +_VISIBLE = { + "display": "flex", + "flexDirection": "column", + "flex": "1 1 auto", + "minHeight": "0", + "overflowY": "auto", + "overflowX": "hidden", +} + +# Flex/border idioms repeated verbatim across the header, selection bar, tab columns and +# `_tabbed_card` — named here so a layout tweak to one of them doesn't have to be hunted down +# across every dict literal that copied it. +_FLEX_FILL = {"flex": "1 1 auto", "minHeight": "0"} +_FLEX_FIXED = {"flex": "0 0 auto"} +_PANEL_BORDER = {"border": "1px solid #dee2e6", "borderRadius": "4px"} def _normalize_base_pathname(path: str | None) -> str | None: @@ -112,6 +89,25 @@ def _normalize_base_pathname(path: str | None) -> str | None: return f"/{stripped}/" if stripped else None +def _clear_button(button_id: str, label: str, *, margin: str = "me-3") -> dbc.Button: + """A small "x clear ..." link button, hidden until its own callback reveals it. + + Shared by every selection-bar clear control (scatter / error-cell / word-filter / grid-filter + — see `_build_selection_bar`) — same look and same hidden-until-active behaviour, differing + only in id, label, and whether a right-margin is needed before the next control. + """ + className = f"text-muted p-0 {margin}".rstrip() + return dbc.Button( + label, + id=button_id, + n_clicks=0, + color="link", + size="sm", + className=className, + style={"display": "none", "fontSize": "0.8em"}, + ) + + class NlpWebApp: """Minimal Dash webapp driven by an ``NlpExplainer``. @@ -161,6 +157,26 @@ class NlpWebApp: Missing slashes are added — ``"shapash-nlp-explainer"`` and ``"/shapash-nlp-explainer"`` both become ``"/shapash-nlp-explainer/"`` — since that is the only spelling Dash accepts and a prefix copied from an nginx ``location`` easily arrives without one. + palette_name : str, optional + Name of a palette in ``shapash/style/colors.json`` (the same file the tabular + ``SmartExplainer`` webapp draws its theme from — see ``smart_app.py``). Selects the + header's band color (``webapp_bkg``) and accent color (``webapp_title``). Defaults to + ``"default"``, the same palette ``SmartExplainer`` falls back to. + colors_dict : dict[str, str], optional + Per-key overrides merged on top of *palette_name*, same shape and key names as + ``colors.json`` and as ``SmartExplainer``'s own ``colors_dict`` (see + ``smart_explainer.py``'s ``define_style``). Only ``webapp_bkg`` (header band) and + ``webapp_title`` (accent color) are read today; other keys are accepted but unused. + info : dict[str, str], optional + Extra ``label -> value`` facts shown in the header's "ⓘ" popover, layered on top of what + ``explanation`` already carries (``model_id``, ``architecture`` — each omitted when the + value is ``None`` — plus ``n_samples`` and ``n_classes``/``backend_name``, + always shown). A key already filled in from ``explanation`` is overridden by *info*; any + other key is appended after — except ``"Train samples"``, which is placed right after "Test + samples" since the two are naturally read together. ``NlpExplainer.run_app`` uses this key to + add it (only known post-``fit()``, so it can never live on the model-free ``explanation``) — + see there for the full picture when going through it rather than constructing ``NlpWebApp`` + directly. """ def __init__( @@ -169,6 +185,9 @@ def __init__( engine: InteractiveEngine | None = None, scatter_xy: np.ndarray | None = None, url_base_pathname: str | None = None, + palette_name: str = "default", + colors_dict: dict[str, str] | None = None, + info: dict[str, str] | None = None, ) -> None: if scatter_xy is not None: scatter_xy = np.asarray(scatter_xy) @@ -186,12 +205,32 @@ def __init__( self._explanation = explanation self._engine = engine + # See the `info` docstring above: everything `explanation` can say for itself, in reading + # order, with *info* (from the caller, or NlpExplainer.run_app's "Train samples") layered on + # top — overriding a key already here, or appended after for anything new. + self._info: dict[str, str] = {} + if explanation.model_id: + self._info["Model"] = explanation.model_id + if explanation.architecture: + self._info["Model architecture"] = explanation.architecture + self._info["Test samples"] = str(explanation.n_samples) + if info and "Train samples" in info: + # Reserve the slot here, right after "Test samples", so the update() below fills an + # existing key in place instead of appending it at the end like any other extra fact. + self._info["Train samples"] = info["Train samples"] + self._info["Number of classes"] = str(explanation.n_classes) + self._info["Explainer backend"] = explanation.backend_name + self._info.update(info or {}) + self.app = dash.Dash( __name__, external_stylesheets=[dbc.themes.BOOTSTRAP], url_base_pathname=_normalize_base_pathname(url_base_pathname), ) - self.app.title = "Shapash — NLP Explainer" + self.app.title = "Shapash - NLP Explainer" + + self._theme: NlpTheme = resolve_nlp_theme(palette_name, colors_dict) + # Pin the document to the viewport so only the inner panels scroll, never the page. The # 100vh shell needs html/body at full height with no default margin; overflow:hidden is the # guarantee against any residual sub-pixel overflow producing a page scrollbar. @@ -220,14 +259,81 @@ def __init__( # ------------------------------------------------------------------ def _build_layout(self) -> None: - explanation = self._explanation - label_names = explanation.label_names or [str(i) for i in range(self._explanation.n_classes)] + """Orchestrate the shell: table → columns → selection bar → components/tabs → container. + + Each step below is a focused builder (see the methods that follow) — this method's job is + only the assembly order and the handful of values threaded between them. + """ # Predicted-label name → class index, shared by the confusion matrix and (via # NlpExplanation.label_to_idx) by SentenceHighlightComponent's predicted-class sync callback. self._label_to_idx = self._explanation.label_to_idx - n = self._explanation.n_samples - # ── Table records — always include _orig_idx for scatter filtering ── + has_prob = self._build_table_records() + column_defs = self._build_column_defs(has_prob) + # Field → header-name lookup for the grid-filter badge in `_register_callbacks` (AG Grid's + # own column filters report field names via filterModel, but the badge should read like the UI). + self._column_header_by_field = {c["field"]: c["headerName"] for c in column_defs} + text_samples_body = self._build_text_samples_body(column_defs) + + selection_bar = self._build_selection_bar() + + # ── Assemble the three panels as tab groups (all bodies stay mounted) ── + self._tab_groups: dict[str, list[str]] = {} + components = self._build_components() + left_tabs, upper_right_tabs, lower_right_tabs = self._build_tabs(components, text_samples_body) + + left_column = html.Div( + [selection_bar, self._tabbed_card("left-tabs", left_tabs)], + style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, + ) + right_column = html.Div( + [ + self._tabbed_card("upper-right-tabs", upper_right_tabs), + self._tabbed_card("lower-right-tabs", lower_right_tabs), + ], + style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, + ) + + self.app.layout = dbc.Container( + [ + # The Class selector lives with the panel it drives: Word Importance (global) and + # Sentence Highlight (local, see highlight_body) each get their own, so switching + # one no longer silently reinterprets the other. + self._build_header(), + # ── Three-panel body: left (data) | right (global over local) ── + dbc.Row( + [ + dbc.Col(left_column, width=5, style={"height": "100%"}), + dbc.Col(right_column, width=7, style={"height": "100%"}), + ], + # gx-3 = horizontal gutter only. A vertical gutter (g-3) adds a 1rem net excess + # that pushes the 100vh shell past the viewport and makes the whole page scroll. + className="gx-3", + style=_FLEX_FILL, + ), + *self._build_stores(), + ], + fluid=True, + # Full-viewport shell: panels scroll internally, the page itself never scrolls. The 4px + # paddingBottom is breathing room below the last panel — box-sizing is border-box + # (Bootstrap's reboot), so it comes out of the 100vh instead of pushing the page taller. + style={ + "height": "100vh", + "display": "flex", + "flexDirection": "column", + "overflow": "hidden", + "paddingBottom": "4px", + }, + ) + + def _build_table_records(self) -> bool: + """Build the dataset table's row records. + + Caches ``self._full_table_records`` (scatter-driven re-filtering in callbacks reads it) and + ``self._has_gt``. Returns whether a "probability" column was included, the one piece the + caller still needs to build the matching column defs. + """ + n = self._explanation.n_samples texts = self._explanation.texts assert texts is not None # noqa: S101 - the webapp requires an already-compiled explainer records: dict[str, list] = { @@ -246,18 +352,17 @@ def _build_layout(self) -> None: records["probability"] = y_prob.max(axis=1).tolist() table_df = pd.DataFrame(records) - - # Full records cached for scatter-driven re-filtering in callbacks self._full_table_records: list[dict] = table_df.to_dict("records") + self._has_gt = "ground_truth" in table_df.columns + return y_prob is not None - has_gt = "ground_truth" in table_df.columns - self._has_gt = has_gt - has_prob = y_prob is not None + def _build_column_defs(self, has_prob: bool) -> list[dict]: + """AG Grid column defs for the dataset table — ground truth/probability columns are optional.""" column_defs: list[dict] = [ {"field": "text", "headerName": "Text", "flex": 3, "tooltipField": "text", "filter": "agTextColumnFilter"}, {"field": "prediction", "headerName": "Prediction", "flex": 1, "filter": "agTextColumnFilter"}, ] - if has_gt: + if self._has_gt: column_defs.append( {"field": "ground_truth", "headerName": "Ground Truth", "flex": 1, "filter": "agTextColumnFilter"} ) @@ -271,251 +376,11 @@ def _build_layout(self) -> None: "valueFormatter": {"function": "params.value != null ? params.value.toFixed(3) : ''"}, } ) + return column_defs - # ── Corpus word list for the exclusion multi-select ──────────── - # Normalised exactly as ``word_importance`` keys its units, via the same resolver, so an - # entry here always matches a bar in the chart. On an uncased model that folds - # AWFUL/Awful/awful into one entry; on a cased model it leaves all three, because there - # they are genuinely different inputs. - all_words = explanation.vocabulary() - word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in all_words] - - # ── Scatter panel content (only when scatter_xy is provided) ── - scatter_col_content = None - if self._scatter_xy is not None: - color_options: list[dcc.Dropdown.Options] = [{"label": "Prediction", "value": "prediction"}] - if self._explanation.y_true is not None: - color_options.append({"label": "Ground Truth", "value": "ground_truth"}) - color_options.append({"label": "Word contribution", "value": "word_contribution"}) - - scatter_col_content = html.Div( - [ - dbc.Row( - [ - dbc.Col( - html.H6("Sample Space", className="fw-bold mb-0"), - width="auto", - className="align-self-center", - ), - dbc.Col( - dcc.Dropdown( - id="color-by", - options=color_options, - value="prediction", - clearable=False, - style={"width": "160px", "fontSize": "0.9em"}, - ), - width="auto", - ), - dbc.Col( - dcc.Dropdown( - id="scatter-word-select", - options=word_options, - value=[], - multi=True, - clearable=True, - placeholder="Select words…", - style={"display": "none", "minWidth": "180px", "fontSize": "0.9em"}, - ), - width="auto", - className="align-self-center", - ), - ], - className="align-items-center mb-2", - ), - html.Small( - "Box/lasso or click to filter. Click a word bar to color by its SHAP contribution.", - className="text-muted d-block mb-2", - ), - dcc.Graph( - id="scatter-plot", - figure=self._build_scatter_fig("prediction"), - config={ - "displayModeBar": True, - "modeBarButtonsToRemove": ["autoScale2d", "resetScale2d"], - "responsive": True, - }, - # Grow to fill the card's remaining height so the scatter - # matches the taller word-importance panel beside it. - style={"flex": "1 1 auto", "minHeight": "340px"}, - ), - ], - # Flex column so the graph above can stretch to the panel height. - style={"display": "flex", "flexDirection": "column", "height": "100%"}, - ) - - # ── Global Word Importance panel — controls live inside it ── - # (Top-K / Sign / Exclude / Class only affect this panel, so they are co-located here — - # independent from the local class picker in the Sentence Highlight panel below.) - # The tab label already names the panel, so no H6; a tight controls row + a graph that - # flex-grows to fill the panel means the whole chart is visible without scrolling. - # "All classes" first: it is the overview a reader without a class in mind wants, and it is - # the only entry whose value is not an index (see _ALL_CLASSES). - global_class_options: list[dcc.Dropdown.Options] = [{"label": "All classes", "value": _ALL_CLASSES}] - global_class_options += [{"label": name, "value": i} for i, name in enumerate(label_names)] - # Ordering only: both statistics are signed and are ranked on |value|, so the sign filter - # and the bars' red/blue colouring mean the same thing in either mode. - rank_by_options: list[dcc.Dropdown.Options] = [ - {"label": "Mean |·|", "value": "mean"}, - {"label": "Total |·|", "value": "sum"}, - ] - word_importance_panel = html.Div( - [ - # One flex row split into two clusters — "what to rank" (left) and "what to keep" - # (right) — rather than six controls in an unbroken run: grouping reads faster, and - # a Bootstrap 50/50 column split was tried first but broke (the left cluster's own - # natural width exceeds half the panel at ordinary sizes, so the fixed-width column - # either overlapped the right cluster or wrapped mid-cluster). space-between instead - # pushes the right cluster as far right as the two clusters' actual widths allow — - # around the midpoint in practice, and never overlapping or force-wrapping either - # cluster — with flex-wrap kept as a fallback for genuinely narrow panels. - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Label("Class", className="fw-bold small mb-0"), - dcc.Dropdown( - id="global-class-selector", - options=global_class_options, - value=0, - clearable=False, - style={"width": "140px"}, - ), - ] - ), - html.Div( - [ - html.Label( - "Rank by", - className="fw-bold small mb-0", - title=( - "Mean: the average pull wherever the word " - "appears. Total: its whole pull on the " - "corpus, so frequent words outrank rare " - "strong ones. The two disagree often." - ), - ), - dcc.Dropdown( - id="rank-by", - options=rank_by_options, - value="mean", - clearable=False, - style={"width": "95px"}, - ), - ] - ), - html.Div( - [ - html.Label( - "Min occur.", - className="fw-bold small mb-0", - title=( - "Hide words seen fewer than this many times " - "in the current selection. A mean over one " - "occurrence is not a mean, and most of a " - "corpus is words seen once." - ), - ), - dcc.Input( - id="min-occurrences", - type="number", - min=1, - step=1, - value=_DEFAULT_MIN_OCCURRENCES, - debounce=_INPUT_DEBOUNCE_S, - className="form-control form-control-sm", - style={"width": "80px"}, - ), - ] - ), - html.Div( - [ - html.Label( - "Top-K words", - className="fw-bold small mb-0", - title=f"How many words to chart, {_MIN_TOPK}–{_MAX_TOPK}", - ), - dcc.Input( - id="topk-input", - type="number", - min=_MIN_TOPK, - max=_MAX_TOPK, - step=1, - value=_DEFAULT_TOPK, - debounce=_INPUT_DEBOUNCE_S, - className="form-control form-control-sm", - style={"width": "90px"}, - ), - ] - ), - ], - className="d-flex align-items-start flex-wrap gap-2", - ), - html.Div( - [ - html.Div( - [ - html.Label("Contributions", className="fw-bold small mb-0"), - dcc.RadioItems( - id="sign-filter", - options=_SIGN_FILTER_OPTIONS, - value="all", - inline=True, - inputStyle={"marginRight": "4px"}, - labelStyle={"marginRight": "12px"}, - ), - ] - ), - html.Div( - [ - html.Label( - "Exclude words", - className="fw-bold small mb-0", - style={"whiteSpace": "nowrap"}, - ), - dcc.Dropdown( - id="word-filter", - options=word_options, - value=[], - multi=True, - placeholder="Exclude…", - style={"fontSize": "0.85em"}, - ), - ], - style={"flex": "1 1 auto", "minWidth": "130px"}, - ), - ], - className="d-flex align-items-start flex-wrap gap-2", - ), - ], - className="d-flex align-items-start flex-wrap mb-1", - style={"flex": "0 0 auto", "justifyContent": "space-between", "gap": "0.5rem"}, - ), - # The graph keeps the height plot_word_importance computed for it (30px per word) - # and this wrapper scrolls. Letting the chart flex to the panel instead — which is - # what it used to do — gives 50 words 8px each, at which point plotly drops the - # word labels and the bars become unidentifiable. - # "responsive" must stay off here: it makes plotly track the *container's* current - # size instead of the figure's own layout.height, which silently defeats the fixed - # per-row height above and squeezes every word back into whatever space the flex - # panel happens to have — the exact illegible-labels bug this height is built to - # avoid. Width still adapts once, at mount, from the container. - html.Div( - dcc.Graph( - id="global-importance-graph", - config={"displayModeBar": False}, - style={"width": "100%"}, - ), - style={"flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"}, - ), - ], - style={"height": "100%", "display": "flex", "flexDirection": "column"}, - ) - - # ── Dataset table body (left-panel default tab) ─────────────── - text_samples_body = html.Div( + def _build_text_samples_body(self, column_defs: list[dict]) -> html.Div: + """Dataset table body (left-panel default tab): title row + the AG Grid itself.""" + return html.Div( [ dbc.Row( [ @@ -530,7 +395,7 @@ def _build_layout(self) -> None: ), ], className="align-items-center mb-2", - style={"flex": "0 0 auto"}, + style=_FLEX_FIXED, ), # Grid grows to fill the tab body (now that the editor lives on its own tab). dag.AgGrid( @@ -552,61 +417,36 @@ def _build_layout(self) -> None: "rowHeight": 38, }, selectedRows=[self._full_table_records[0]], - style={ - "flex": "1 1 auto", - "minHeight": "0", - "--ag-font-size": "15px", - "--ag-header-font-size": "14px", - }, + style={**_FLEX_FILL, "--ag-font-size": "15px", "--ag-header-font-size": "14px"}, className="ag-theme-alpine", ), ], style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) - # ── Selection bar: persistent, always-visible filter state + clears ── - # Lives above the left tabs so a scatter/word selection stays visible whichever - # left tab (table or embeddings) is active. Clear buttons are consolidated here - # from the individual panels they used to live in. + def _build_selection_bar(self) -> html.Div: + """Persistent, always-visible filter state + clear buttons. + + Lives above the left tabs so a scatter/word selection stays visible whichever left tab + (table or embeddings) is active. Clear buttons are consolidated here from the individual + panels they used to live in. + """ selection_children: list = [ html.Span("Showing:", className="text-muted small me-2"), + # AG Grid's own column filters (the funnel icon in each header) act on the grid + # client-side and don't flow through filter_table's inputs the way the other filters + # do — filter_table folds them into this same summary via a filterModel/virtualRowData + # Input pair, so a grid-level filter is described here too, not just inside the grid. html.Span("all samples", id="selection-summary", className="small fw-bold me-3"), ] - if scatter_col_content is not None: - selection_children.append( - dbc.Button( - "× clear selection", - id="scatter-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0 me-3", - style={"display": "none", "fontSize": "0.8em"}, - ) - ) - if has_gt: - selection_children.append( - dbc.Button( - "× clear cell", - id="error-cell-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0 me-3", - style={"display": "none", "fontSize": "0.8em"}, - ) - ) - selection_children.append( - dbc.Button( - "× clear word filter", - id="word-filter-clear-btn", - n_clicks=0, - color="link", - size="sm", - className="text-muted p-0", - style={"display": "none", "fontSize": "0.8em"}, - ) - ) + if self._scatter_xy is not None: + selection_children.append(_clear_button("scatter-clear-btn", "× clear selection")) + if self._has_gt: + selection_children.append(_clear_button("error-cell-clear-btn", "× clear cell")) + selection_children.append(_clear_button("word-filter-clear-btn", "× clear word filter")) + # No trailing margin: it is followed by the errors-only switch, pushed to the far right + # by that switch's own `ms-auto`, not by spacing here. + selection_children.append(_clear_button("grid-filter-clear-btn", "× clear grid filter", margin="")) # Right-aligned (ms-auto) and here — rather than inside the Dataset tab body — so it stays # usable from the Embeddings tab too, where it now also drives point highlighting. selection_children.append( @@ -615,63 +455,82 @@ def _build_layout(self) -> None: label="Model Errors", value=False, className="small mb-0 ms-auto", - style={} if has_gt else {"display": "none"}, + style={} if self._has_gt else _HIDDEN, ) ) - selection_bar = html.Div( + return html.Div( selection_children, style={ - "border": "1px solid #dee2e6", - "borderRadius": "4px", + **_PANEL_BORDER, "padding": "6px 12px", "display": "flex", "alignItems": "center", "flexWrap": "wrap", - "flex": "0 0 auto", + **_FLEX_FIXED, }, ) - # ── Assemble the three panels as tab groups (all bodies stay mounted) ── - self._tab_groups: dict[str, list[str]] = {} + def _build_components(self) -> list[WebappComponent]: + """Instantiate every capability-gated component, plus the scatter panel if data was given. + Assigns ``self._components`` — read again by ``_register_callbacks`` once the layout exists + — and returns the same list for ``_build_tabs`` to map onto tab groups. + """ # Local class picker default: the predicted class of the initially selected row. It is reset # to the newly-selected text's prediction by a sync callback owned by # SentenceHighlightComponent — see sync_local_class_to_prediction — so switching sentences # always starts on "why did the model predict this", while still letting the user override it # for the current sentence. - default_local_class = self._label_to_idx.get(self._full_table_records[0].get("prediction"), 0) - self._components = [ + predicted_label = self._full_table_records[0].get("prediction") + default_local_class = self._label_to_idx.get(predicted_label, 0) if isinstance(predicted_label, str) else 0 + components: list[WebappComponent] = [ comp for comp in ( - SentenceHighlightComponent(default_local_class), - WaterfallComponent(), + SentenceHighlightComponent(default_local_class, theme=self._theme), + WaterfallComponent(theme=self._theme), DataEditorComponent(), CounterfactualComponent(), SimilarExamplesComponent(), LabelNoiseComponent(), - ErrorAnalysisComponent(), - WordProfileComponent(), + ErrorAnalysisComponent(theme=self._theme), + WordProfileComponent(theme=self._theme), + WordImportanceComponent(theme=self._theme), ) if type(comp).is_available(self._explanation, self._engine) ] - highlight_comp = next(c for c in self._components if isinstance(c, SentenceHighlightComponent)) - waterfall_comp = next(c for c in self._components if isinstance(c, WaterfallComponent)) - editor_comp = next((c for c in self._components if isinstance(c, DataEditorComponent)), None) - cf_comp = next((c for c in self._components if isinstance(c, CounterfactualComponent)), None) - similar_comp = next((c for c in self._components if isinstance(c, SimilarExamplesComponent)), None) - noise_comp = next((c for c in self._components if isinstance(c, LabelNoiseComponent)), None) - error_analysis_comp = next((c for c in self._components if isinstance(c, ErrorAnalysisComponent)), None) - word_profile_comp = next((c for c in self._components if isinstance(c, WordProfileComponent)), None) + # Mounting is decided by the caller-supplied array, not by `is_available` — a pre-computed + # projection is arbitrary data handed to NlpWebApp, not an explanation/engine capability. + # Word Importance is always mounted at this point (it has no `requires`), so the scatter's + # word-contribution color mode can always be offered. + if self._scatter_xy is not None: + components.append(ScatterComponent(self._scatter_xy, offer_word_contribution=True)) + self._components = components + return components + + def _build_tabs(self, components: list[WebappComponent], text_samples_body: html.Div) -> tuple[list, list, list]: + """Map *components* onto the shell's three tab groups: left, upper-right, lower-right.""" + highlight_comp = next(c for c in components if isinstance(c, SentenceHighlightComponent)) + waterfall_comp = next(c for c in components if isinstance(c, WaterfallComponent)) + word_importance_comp = next(c for c in components if isinstance(c, WordImportanceComponent)) + editor_comp = next((c for c in components if isinstance(c, DataEditorComponent)), None) + cf_comp = next((c for c in components if isinstance(c, CounterfactualComponent)), None) + similar_comp = next((c for c in components if isinstance(c, SimilarExamplesComponent)), None) + noise_comp = next((c for c in components if isinstance(c, LabelNoiseComponent)), None) + error_analysis_comp = next((c for c in components if isinstance(c, ErrorAnalysisComponent)), None) + word_profile_comp = next((c for c in components if isinstance(c, WordProfileComponent)), None) + scatter_comp = next((c for c in components if isinstance(c, ScatterComponent)), None) left_tabs: list = [("table", "Dataset", text_samples_body)] - if scatter_col_content is not None: - left_tabs.append(("scatter", "Embeddings", scatter_col_content)) + if scatter_comp is not None: + left_tabs.append(("scatter", "Embeddings", scatter_comp.layout(self._explanation, self._engine))) if editor_comp is not None: left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._explanation, self._engine))) # Error Analysis sits beside Word Importance: it *is* an aggregated word-importance view # (per confusion-matrix cell), so it belongs with the other global "why" panels on the right. - upper_right_tabs: list = [("importance", "Word Importance", word_importance_panel)] + upper_right_tabs: list = [ + ("importance", "Word Importance", word_importance_comp.layout(self._explanation, self._engine)) + ] # Immediately after Word Importance, because it is the same question asked the other way # round (one word across all classes, instead of one class across the top words) and the two # are read together — a bar clicked there arrives preselected here. @@ -700,19 +559,10 @@ def _build_layout(self) -> None: ("highlight", "Sentence", highlight_comp.layout(self._explanation, self._engine)), ("waterfall", "Waterfall", waterfall_comp.layout(self._explanation, self._engine)), ] + return left_tabs, upper_right_tabs, lower_right_tabs - left_column = html.Div( - [selection_bar, self._tabbed_card("left-tabs", left_tabs)], - style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, - ) - right_column = html.Div( - [ - self._tabbed_card("upper-right-tabs", upper_right_tabs), - self._tabbed_card("lower-right-tabs", lower_right_tabs), - ], - style={"display": "flex", "flexDirection": "column", "gap": "8px", "height": "100%"}, - ) - + def _build_stores(self) -> list[dcc.Store]: + """Shared ``dcc.Store``s the shell and its components read/write cross-panel state through.""" stores: list = [ # current-datapoint is the app's primary selection: the one text every # per-instance panel (highlight, waterfall, counterfactuals) reads from. @@ -721,40 +571,84 @@ def _build_layout(self) -> None: dcc.Store(id="word-click-filter", data=None), # Selected confusion-matrix cell: {"pred": idx, "true": idx, "indices": [...]} or None. dcc.Store(id="error-cell", data=None), + # Word Importance's class selector, published so Scatter's word-contribution color mode + # can read it without referencing Word Importance's DOM id directly (see + # nlp_components/scatter.py). + dcc.Store(id="active-class-store", data=0), ] # Only the What-if Lab (editor + counterfactual) reads/writes the apply store; the always-on # core panels (highlight, waterfall) never do, so their presence alone shouldn't create it. - if editor_comp is not None or cf_comp is not None: + needs_apply_store = any(isinstance(c, (DataEditorComponent, CounterfactualComponent)) for c in self._components) + if needs_apply_store: stores.append(dcc.Store(id=_APPLY_STORE, data=None)) + return stores - self.app.layout = dbc.Container( + def _build_header(self) -> html.Div: + """Top band: clickable logo + title (links to the repo) and the run-info popover.""" + return html.Div( [ - # ── Header — the Class selector now lives with the panel it drives: Word - # Importance (global) and Sentence Highlight (local, see highlight_body) each - # get their own, so switching one no longer silently reinterprets the other. ── - dbc.Row( - [ - dbc.Col(html.H4("Shapash — NLP Explainer", className="mb-0"), width=12), - ], - className="py-2 align-items-center", - style={"flex": "0 0 auto"}, + html.A( + dbc.Row( + [ + dbc.Col( + html.Img( + # "fond fonce" — the dark-background mark, matching + # the gray band it sits on here. + src=self.app.get_asset_url("shapash-fond-fonce.png"), + style={"height": "32px"}, + ), + width="auto", + ), + dbc.Col( + html.H4( + "Shapash - NLP Explainer", + className="mb-0", + style={"color": self._theme.header_accent}, + ) + ), + ], + align="center", + className="g-2", + ), + href="https://github.com/MAIF/shapash", + target="_blank", + style={"textDecoration": "none", "display": "block"}, ), - # ── Three-panel body: left (data) | right (global over local) ── - dbc.Row( + html.Div( [ - dbc.Col(left_column, width=5, style={"height": "100%"}), - dbc.Col(right_column, width=7, style={"height": "100%"}), + html.Span( + html.I("info", className="material-icons"), + id="app-info-btn", + title="Run info", + style={"cursor": "pointer", "color": self._theme.header_accent, "fontSize": "22px"}, + ), + dbc.Popover( + dbc.PopoverBody( + [ + html.Div( + [html.Span(f"{label}: ", className="fw-bold"), html.Span(value)], + className="small", + ) + for label, value in self._info.items() + ] + ), + target="app-info-btn", + trigger="click", + placement="bottom-end", + ), ], - # gx-3 = horizontal gutter only. A vertical gutter (g-3) adds a 1rem net excess - # that pushes the 100vh shell past the viewport and makes the whole page scroll. - className="gx-3", - style={"flex": "1 1 auto", "minHeight": "0"}, + style={**_FLEX_FIXED, "alignSelf": "center"}, ), - *stores, ], - fluid=True, - # Full-viewport shell: panels scroll internally, the page itself never scrolls. - style={"height": "100vh", "display": "flex", "flexDirection": "column", "overflow": "hidden"}, + style={ + "display": "flex", + "alignItems": "center", + "justifyContent": "space-between", + "margin": "0 0 6px 0", + "padding": "12px 16px", + "backgroundColor": self._theme.header_bkg, + **_FLEX_FIXED, + }, ) def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: @@ -774,7 +668,23 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: ] return html.Div( [ - dbc.Tabs(headers, id=tabs_id, active_tab=active), + html.Div( + [ + dbc.Tabs(headers, id=tabs_id, active_tab=active, style={"flex": "1 1 auto", "minWidth": "0"}), + # Magnifies whichever tab is active — see the `panel-card`/`panel-expand-btn` + # clientside callback in `_register_callbacks`. A CSS class toggle (not the + # browser Fullscreen API) so it works the same for every panel regardless of + # what the active tab's own layout does internally. + html.Span( + html.I("fullscreen", className="material-icons"), + id={"type": "panel-expand-btn", "id": tabs_id}, + n_clicks=0, + title="Expand panel", + style={"cursor": "pointer", "flex": "0 0 auto", "padding": "0 2px", "color": "#6c757d"}, + ), + ], + style={"display": "flex", "alignItems": "center", "gap": "6px"}, + ), # Flex column so the single visible body (the others are display:none) fills the height. html.Div( bodies, @@ -787,6 +697,7 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: }, ), ], + id={"type": "panel-card", "id": tabs_id}, style={ "border": "1px solid #dee2e6", "borderRadius": "4px", @@ -796,6 +707,7 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: "overflow": "hidden", "flex": "1 1 0", "minHeight": "0", + "backgroundColor": "white", }, ) @@ -807,6 +719,7 @@ def _register_callbacks(self) -> None: explanation = self._explanation full_records = self._full_table_records has_gt = self._has_gt + column_header_by_field = self._column_header_by_field def _compose_indices(selected_indices, error_cell, errors_only=False): """Combine the scatter selection, the confusion-cell selection, and the errors toggle. @@ -816,116 +729,8 @@ def _compose_indices(selected_indices, error_cell, errors_only=False): is set. Returns a list of original sample indices, or ``None`` when nothing is active. """ cell_indices = error_cell.get("indices") if error_cell else None - error_positions: set[int] | None = None - if errors_only and has_gt: - mask = self._error_mask() - if mask is not None: - error_positions = set(np.where(mask)[0].tolist()) - return compose_selection(selected_indices, cell_indices, error_positions) - - # ── Global word importance ─────────────────────────────────────── - @self.app.callback( - Output("global-importance-graph", "figure"), - [ - Input("global-class-selector", "value"), - Input("topk-input", "value"), - Input("sign-filter", "value"), - Input("word-filter", "value"), - Input("rank-by", "value"), - Input("min-occurrences", "value"), - Input("scatter-selected-indices", "data"), - Input("error-cell", "data"), - Input("errors-only-switch", "value"), - ], - ) - def update_global_importance( - label_idx, - topk, - sign_filter, - exclude_words_list, - rank_by, - min_occurrences, - selected_indices, - error_cell, - errors_only, - ): - if label_idx is None: - raise PreventUpdate - across_classes = label_idx == _ALL_CLASSES - rank_by = rank_by or "mean" - # Typed input bypasses the box's own min/max, and a cleared box arrives as None. - n_top = max(_MIN_TOPK, min(_MAX_TOPK, int(topk))) if topk is not None else _DEFAULT_TOPK - # A cleared number input arrives as None; 1 is "no floor", which is what an empty box - # should mean rather than reverting to the default the user just deleted. - floor = max(1, int(min_occurrences)) if min_occurrences else 1 - effective_indices = _compose_indices(selected_indices, error_cell, bool(errors_only)) - # Across classes the bars are magnitudes (max over classes of |statistic|), so a sign - # filter has nothing left to select on: it is greyed out in that mode and forced to - # "all" here as well, so a value left over from a single-class view cannot silently - # empty the chart. - sign = "all" if across_classes else (sign_filter or "all") - word_imp = explanation.word_importance( - label_idx=None if across_classes else int(label_idx), - n_top=n_top, - filter_sign=sign, - # Punctuation is its own unit since word segmentation splits on word/non-word - # boundaries; corpus-wide its mean contribution averages to ~0, so it is noise here. - # Per-instance punctuation contributions stay visible in the Sentence Highlight panel. - filter_punctuation=True, - exclude_words=set(exclude_words_list or []) or None, - sample_indices=effective_indices, - rank_by=rank_by, - min_occurrences=floor, - ) - n_scope = len(effective_indices) if effective_indices is not None else len(explanation) - if word_imp.empty: - # Name the filter that actually emptied it, since the fix differs: the sign filter - # (checked first — it is applied last), then the frequency floor, then everything - # else (an exclusion list or an empty selection). - if sign in ("positive", "negative"): - reason = f"No {sign} word passes these filters." - elif floor > 1: - reason = f"No word occurs at least {floor} time(s) in these {n_scope} sample(s)." - else: - reason = f"No word passes these filters in these {n_scope} sample(s)." - return empty_word_figure(reason) - - # Same filters and same sample scope as the ranking above, so the count on a bar's - # hover is the count that bar's aggregate was computed over — and the count the - # min-occurrences floor was applied to. - counts = explanation.word_counts(filter_punctuation=True, sample_indices=effective_indices) - fig = plot_word_importance( - word_imp, - # No title: the tab is already labelled "Word Importance" and the class/floor are - # both visible in the filter row right above the chart — a repeated title band was - # just eating vertical space this panel needs for word rows (see the graph wrapper - # below, which scrolls when it runs out). - title=None, - # A mean, a total and a cross-class magnitude are different quantities on an - # identical-looking chart, so the axis has to say which one is drawn. Derived from - # what word_importance stamped on its result rather than re-deduced here. - x_title=word_importance_axis_title(str(word_imp.name)), - width=None, - height=None, - counts=counts["n_occurrences"], - ) - # Height deliberately left as plot_word_importance computed it — see the graph's - # wrapper in the layout. - return fig - - # Under "All classes" the ranking is a magnitude, so Positive/Negative would silently - # return nothing. Grey them out and pull the selection back to "All" rather than leaving a - # live control that cannot do anything. - @self.app.callback( - Output("sign-filter", "options"), - Output("sign-filter", "value"), - Input("global-class-selector", "value"), - ) - def gate_sign_filter(label_idx): - if label_idx != _ALL_CLASSES: - return _SIGN_FILTER_OPTIONS, dash.no_update - greyed = [{**opt, "disabled": opt["value"] != "all"} for opt in _SIGN_FILTER_OPTIONS] - return greyed, "all" + errors = error_positions(explanation) if (errors_only and has_gt) else None + return compose_selection(selected_indices, cell_indices, errors) # ── Primary selection: selected table row → current-datapoint ──── # The editor's Predict callback also writes this store (see DataEditorComponent), @@ -953,37 +758,10 @@ def set_current_from_row(selected_rows): # SentenceHighlightComponent / WaterfallComponent (see the component loop at the end of this # method) — they only need the current-datapoint store, already shared via `stores["current"]`. - # ── Word-bar click / clear → table word filter ─────────────────── - # Resetting the graph's clickData to None after each event lets the SAME bar be clicked - # again (Plotly does not re-fire clickData when the value is unchanged). Without a scatter - # the bar click drives word-click-filter directly; WITH a scatter the filter is derived - # from scatter-word-select instead (see the scatter block), so editing/removing the word - # there also clears this filter. - if self._scatter_xy is None: - - @self.app.callback( - Output("word-click-filter", "data"), - Output("global-importance-graph", "clickData"), - Input("global-importance-graph", "clickData"), - Input("word-filter-clear-btn", "n_clicks"), - prevent_initial_call=True, - ) - def update_word_click_filter(click_data, _clear_clicks): - trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" - if "word-filter-clear-btn" in trigger: - return None, None - if not click_data or not click_data.get("points"): - raise PreventUpdate - return [click_data["points"][0]["y"]], None - - @self.app.callback( - Output("word-filter-clear-btn", "style"), - Input("word-click-filter", "data"), - ) - def toggle_word_clear_button(word_filter): - if word_filter: - return {"display": "inline", "fontSize": "0.8em"} - return {"display": "none", "fontSize": "0.8em"} + # Word-bar click / clear → table word filter, and the coloring/selection scatter callbacks, + # are registered by WordImportanceComponent / ScatterComponent respectively (see the + # component loop at the end of this method) — both read/write the shared + # `stores["word_click"]`/`stores["selection"]` stores, already wired above. # ── Unified table filter (scatter + word-bar click + errors-only) ── # Also writes the selection-summary readout: it already knows the resulting row count, so the @@ -1000,9 +778,11 @@ def toggle_word_clear_button(word_filter): Input("word-click-filter", "data"), Input("errors-only-switch", "value"), Input("error-cell", "data"), + Input("dataset-table", "filterModel"), + Input("dataset-table", "virtualRowData"), ], ) - def filter_table(selected_indices, word_filter, errors_only, error_cell): + def filter_table(selected_indices, word_filter, errors_only, error_cell, filter_model, virtual_row_data): # word_filter may be a list of words (multi-select in the scatter) or None. words = word_filter if isinstance(word_filter, list) else ([word_filter] if word_filter else []) effective_indices = _compose_indices(selected_indices, error_cell) @@ -1012,13 +792,21 @@ def filter_table(selected_indices, word_filter, errors_only, error_cell): idx_set = set(effective_indices) recs = [r for r in full_records if r["_orig_idx"] in idx_set] or full_records if errors_only and has_gt: - misclassified = [r for r in recs if str(r.get("prediction", "")) != str(r.get("ground_truth", ""))] - recs = misclassified or recs + # True intersection — must not silently drop this filter when it empties the + # selection, or the summary below (which still says "model errors only") would + # describe a filter that was not actually applied. + recs = [r for r in recs if str(r.get("prediction", "")) != str(r.get("ground_truth", ""))] if words: - lowers = [w.lower() for w in words] - # Rows containing ANY of the selected words (matches the scatter's word colouring). - filtered = [r for r in recs if any(w in r["text"].lower() for w in lowers)] - recs = filtered or recs + lowers = {w.lower() for w in words} + # Rows containing ANY of the selected words as an exact token — matched against + # explanation.token_strings, the same tokenization word_importance()/vocabulary() + # rank on, so a bar labeled "happy" cannot pull in "unhappy"/"happier" via a raw + # substring match on the sentence text. + # True intersection, same reasoning as errors_only above — e.g. a word clicked in + # Word Importance followed by an Error Analysis cell that contains none of that + # word's rows must show 0 rows, not silently ignore the word and claim it in the + # summary anyway. + recs = [r for r in recs if lowers & {t.lower() for t in explanation.token_strings[r["_orig_idx"]]}] total = len(full_records) parts = [] @@ -1033,108 +821,40 @@ def filter_table(selected_indices, word_filter, errors_only, error_cell): parts.append("containing " + ", ".join(f'"{w}"' for w in words)) if errors_only and has_gt: parts.append("model errors only") + + # AG Grid's own column filters (the funnel icon in each header) run entirely + # client-side and never pass through the filtering above — fold them into the same + # count here rather than reporting a second, disconnected number, so "Showing:" always + # names the row count actually on screen. virtualRowData is the grid's rowData after + # those inline filters are applied, so its length is that final on-screen count. + grid_filtered = bool(filter_model) + shown = len(virtual_row_data) if (grid_filtered and virtual_row_data is not None) else len(recs) + if grid_filtered: + names = ", ".join(column_header_by_field.get(f, f) for f in filter_model) + parts.append(f"grid filter: {names}") + if parts: - summary = " · ".join(parts) + f" ({len(recs)} of {total})" + summary = " · ".join(parts) + f" ({shown} of {total})" title = "Text Samples — filtered" else: summary = f"all {total} samples" title = "Text Samples — click a row to inspect" - return recs, [recs[0]], title, summary - - # ── Scatter-specific callbacks (registered only when xy provided) ── - if self._scatter_xy is not None: - - @self.app.callback( - Output("scatter-plot", "figure"), - [ - Input("color-by", "value"), - Input("scatter-word-select", "value"), - Input("global-class-selector", "value"), - Input("errors-only-switch", "value"), - ], - ) - def update_scatter_color(color_by, words, label_idx, errors_only): - return self._build_scatter_fig( - color_by or "prediction", - words=words or [], - # "All classes" is not a column to colour by; the scatter falls back to the - # first class rather than crashing on int("all"). - label_idx=label_idx if isinstance(label_idx, int) else 0, - errors_only=bool(errors_only), - ) - - @self.app.callback( - Output("color-by", "value"), - Input("scatter-word-select", "value"), - ) - def sync_color_by(words): - if not words: - raise PreventUpdate - return "word_contribution" + return recs, ([recs[0]] if recs else []), title, summary - # Bar click / clear → scatter word selection (the single source of truth for the word - # filter when a scatter exists). clickData is reset so the same bar can be re-clicked. - @self.app.callback( - Output("scatter-word-select", "value"), - Output("global-importance-graph", "clickData"), - Input("global-importance-graph", "clickData"), - Input("word-filter-clear-btn", "n_clicks"), - prevent_initial_call=True, - ) - def set_scatter_words(click_data, _clear_clicks): - trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" - if "word-filter-clear-btn" in trigger: - return [], None - if not click_data or not click_data.get("points"): - raise PreventUpdate - return [click_data["points"][0]["y"]], None - - # Table word filter follows the scatter selection: removing the word in the dropdown (or - # hitting clear) unfilters the table and hides the clear button. - @self.app.callback( - Output("word-click-filter", "data"), - Input("scatter-word-select", "value"), - ) - def word_filter_from_scatter(words): - return words or None - - @self.app.callback( - Output("scatter-word-select", "style"), - Input("color-by", "value"), - ) - def toggle_word_select(color_by): - base = {"minWidth": "180px", "fontSize": "0.9em"} - return base if color_by == "word_contribution" else {**base, "display": "none"} - - @self.app.callback( - Output("scatter-selected-indices", "data"), - Input("scatter-plot", "selectedData"), - Input("scatter-plot", "clickData"), - Input("scatter-clear-btn", "n_clicks"), - ) - def update_scatter_selection(selected_data, click_data, _clear_clicks): - trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" - if "scatter-clear-btn" in trigger: - return None - if "clickData" in trigger: - if not click_data or not click_data.get("points"): - return None - return [int(click_data["points"][0]["customdata"][0])] - # An empty selectedData here is almost always plotly re-emitting on a figure recolor - # (color-by / word / errors-only toggle), NOT a user deselect — ignore it so the box - # survives. Genuine clears go through the clear button or a point click above. - if not selected_data or not selected_data.get("points"): - raise PreventUpdate - return [int(pt["customdata"][0]) for pt in selected_data["points"]] + @self.app.callback( + Output("grid-filter-clear-btn", "style"), + Input("dataset-table", "filterModel"), + ) + def toggle_grid_filter_clear_btn(filter_model): + return {"fontSize": "0.8em"} if filter_model else {"display": "none"} - @self.app.callback( - Output("scatter-clear-btn", "style"), - Input("scatter-selected-indices", "data"), - ) - def toggle_clear_button(selected_indices): - visible = {"display": "inline", "fontSize": "0.8em"} - hidden = {"display": "none", "fontSize": "0.8em"} - return visible if selected_indices else hidden + @self.app.callback( + Output("dataset-table", "filterModel"), + Input("grid-filter-clear-btn", "n_clicks"), + prevent_initial_call=True, + ) + def clear_grid_filter(_n_clicks): + return {} # ── Tab visibility: toggle display of always-mounted bodies ────── # Bodies stay in the DOM (see _tabbed_card); only their `display` flips so the @@ -1150,17 +870,45 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): # (The selection-summary readout is written by filter_table, which knows the row count.) + # ── Panel magnify: one clientside toggle for every `_tabbed_card` ───────────────── + # A single MATCH callback handles all three panels (Dataset/Embeddings, the global + # Word Importance group, Sentence/Waterfall) — no per-graph wiring needed, since it just + # toggles a CSS class on the whole card (see `.fullscreen-overlay` in style.css) rather + # than resizing any one plot: the card's own flex layout does the rest. Plotly figures + # inside don't auto-track a CSS-only resize, so a manual `Plotly.Plots.resize` nudges + # them once the browser has reflowed the class change. + self.app.clientside_callback( + """ + function(n_clicks) { + if (!n_clicks) { return window.dash_clientside.no_update; } + const expand = (n_clicks % 2 === 1); + setTimeout(function() { + document.querySelectorAll('.js-plotly-plot').forEach(function(gd) { + if (window.Plotly) { window.Plotly.Plots.resize(gd); } + }); + }, 60); + return expand ? 'fullscreen-overlay' : ''; + } + """, + Output({"type": "panel-card", "id": MATCH}, "className"), + Input({"type": "panel-expand-btn", "id": MATCH}, "n_clicks"), + ) + # ── Registered components (always-on core panels + capability-gated What-if Lab) ── stores = { "apply": _APPLY_STORE, "current": _CURRENT_STORE, "error_cell": "error-cell", "error_cell_clear": "error-cell-clear-btn", - # The three shell-owned filters a global panel has to honour, plus the clicked-word - # store, handed over by id so a component never hard-codes the shell's ids. + # The shell-owned filters a global panel has to honour, plus the clicked-word store and + # its clear button and the published active class, handed over by id so a component + # never hard-codes the shell's ids. "selection": "scatter-selected-indices", + "selection_clear": "scatter-clear-btn", "errors_only": "errors-only-switch", "word_click": "word-click-filter", + "word_click_clear": "word-filter-clear-btn", + "active_class": "active-class-store", } for comp in self._components: comp.register_callbacks(self.app, self._explanation, self._engine, stores) @@ -1172,188 +920,3 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> None: """Launch the Dash development server.""" self.app.run(port=port, debug=debug, host=host) - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _word_contributions(self, word: str, label_idx: int) -> np.ndarray: - """Per-sample sum of SHAP contributions for all tokens matching *word*.""" - explanation = self._explanation - n = self._explanation.n_samples - result = np.zeros(n) - word_lower = word.lower() - for i in range(n): - tokens = explanation.token_strings[i] - vals = select_label_column(explanation.values[i], label_idx) - for j, tok in enumerate(tokens): - if tok.strip().lower() == word_lower: - result[i] += vals[j] - return result - - def _error_mask(self) -> np.ndarray | None: - """Boolean array, ``True`` where the prediction disagrees with the ground truth. - - ``None`` when either is unavailable. Matches the string comparison used by the - "Model Errors" table filter so both stay consistent. - """ - y_true = self._explanation.y_true - y_pred = self._explanation.y_pred - if y_true is None or y_pred is None: - return None - return np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) - - @staticmethod - def _emphasize_errors( - idx_arr: np.ndarray, - base_opacity: float, - base_size: float, - error_mask: np.ndarray | None, - ) -> tuple[list[float] | float, list[float] | float]: - """Per-point opacity/size that pops model errors and shadows everything else. - - Keeps the caller's coloring untouched — only opacity and marker size change — - so points stay grouped/colored however the "Color by" dropdown already draws them. - Returns scalars (the unmodified base values) when ``error_mask`` is unavailable. - """ - if error_mask is None or len(idx_arr) == 0: - return base_opacity, base_size - is_error = error_mask[idx_arr] - opacity = np.where(is_error, max(base_opacity, 0.9), 0.12).tolist() - size = np.where(is_error, base_size + 3, max(base_size - 2, 3)).tolist() - return opacity, size - - def _build_scatter_fig( - self, - color_by: str, - words: list[str] | None = None, - label_idx: int = 0, - errors_only: bool = False, - ) -> go.Figure: - """2-D scatter coloured by prediction, ground-truth label, or word SHAP contribution. - - One trace per class for label-based coloring so the legend works correctly - and Plotly's box/lasso select dims unselected points across all traces. - Word-contribution mode uses a single diverging-colorscale trace. - ``customdata`` always stores the original sample index. When ``errors_only`` is - set, misclassified points are emphasized (larger, opaque) and the rest are - shadowed (small, faint) without altering their color. - """ - n = self._explanation.n_samples - explanation = self._explanation - exp_texts = self._explanation.texts - assert exp_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in exp_texts] - xy = self._scatter_xy - assert xy is not None # noqa: S101 - only registered/called when scatter_xy was provided - error_mask = self._error_mask() if errors_only else None - - if color_by == "word_contribution" and words: - contributions = np.sum([self._word_contributions(w, label_idx) for w in words], axis=0) - max_abs = float(np.abs(contributions).max()) or 1.0 - present_mask = np.where(contributions != 0.0)[0] - absent_mask = np.where(contributions == 0.0)[0] - colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" - - fig = go.Figure() - # Both layers are ALWAYS added (even when a mask is empty) so the trace structure stays - # constant across word additions/removals. With a stable `uirevision`, a changing WebGL - # (Scattergl) trace count leaves ghost/"shadow" points from the previous render — keeping - # exactly two traces avoids that. - absent_opacity, absent_size = self._emphasize_errors(absent_mask, 0.35, 5, error_mask) - present_opacity, present_size = self._emphasize_errors(present_mask, 0.9, 9, error_mask) - - # Gray context layer — absent points (no selected word contributes to them). - fig.add_trace( - go.Scattergl( - x=xy[absent_mask, 0].tolist(), - y=xy[absent_mask, 1].tolist(), - mode="markers", - marker=dict(color="#b0b0b0", size=absent_size, opacity=absent_opacity), - customdata=absent_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in absent_mask], - hovertemplate="%{text}<extra>absent</extra>", - showlegend=False, - ) - ) - # Colored overlay — samples where at least one selected word contributes. - fig.add_trace( - go.Scattergl( - x=xy[present_mask, 0].tolist(), - y=xy[present_mask, 1].tolist(), - mode="markers", - marker=dict( - color=contributions[present_mask].tolist(), - colorscale="RdBu", - cmin=-max_abs, - cmax=max_abs, - size=present_size, - opacity=present_opacity, - colorbar=dict( - title=dict(text=colorbar_title, side="right"), - thickness=12, - tickformat=".2f", - ), - ), - customdata=present_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in present_mask], - hovertemplate="<b>SHAP: %{marker.color:.3f}</b><br>%{text}<extra></extra>", - showlegend=False, - ) - ) - fig.update_layout( - dragmode="select", - uirevision="scatter", - xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - plot_bgcolor="#f9f9f9", - paper_bgcolor="white", - margin=dict(l=10, r=10, t=10, b=10), - autosize=True, - showlegend=False, - ) - return fig - - if color_by == "ground_truth" and self._explanation.y_true is not None: - labels = [str(label) for label in self._explanation.y_true.tolist()] - elif self._explanation.y_pred is not None: - labels = [str(label) for label in self._explanation.y_pred.tolist()] - else: - labels = [""] * n - - label_names = explanation.label_names or sorted(set(labels)) - - fig = go.Figure() - for i, name in enumerate(label_names): - mask = [j for j, lbl in enumerate(labels) if lbl == name] - if not mask: - continue - mask_arr = np.array(mask) - opacity, size = self._emphasize_errors(mask_arr, 0.75, 7, error_mask) - fig.add_trace( - go.Scattergl( - x=xy[mask_arr, 0].tolist(), - y=xy[mask_arr, 1].tolist(), - mode="markers", - marker=dict(color=_PALETTE[i % len(_PALETTE)], size=size, opacity=opacity), - customdata=mask_arr.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in mask], - hovertemplate=f"<b>{name}</b><br>%{{text}}<extra></extra>", - name=name, - ) - ) - - fig.update_layout( - dragmode="select", - uirevision="scatter", - xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - plot_bgcolor="#f9f9f9", - paper_bgcolor="white", - margin=dict(l=10, r=10, t=10, b=10), - # No fixed height — the responsive Graph stretches it to fill the card. - autosize=True, - legend=dict(itemsizing="constant", orientation="v", title_text=""), - showlegend=True, - ) - return fig diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index 9bddf723..d6839446 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -17,6 +17,7 @@ WebappComponent, available_capabilities, compose_selection, + error_mask, error_positions, ) from shapash.webapp.nlp_components.counterfactual import CounterfactualComponent @@ -28,23 +29,28 @@ ) from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent from shapash.webapp.nlp_components.label_noise import LabelNoiseComponent +from shapash.webapp.nlp_components.scatter import ScatterComponent from shapash.webapp.nlp_components.sentence_highlight import SentenceHighlightComponent from shapash.webapp.nlp_components.similar_examples import SimilarExamplesComponent from shapash.webapp.nlp_components.waterfall import WaterfallComponent +from shapash.webapp.nlp_components.word_importance import WordImportanceComponent from shapash.webapp.nlp_components.word_profile import WordProfileComponent __all__ = [ "WebappComponent", "available_capabilities", "compose_selection", + "error_mask", "error_positions", "DataEditorComponent", "CounterfactualComponent", "ErrorAnalysisComponent", "LabelNoiseComponent", + "ScatterComponent", "SentenceHighlightComponent", "SimilarExamplesComponent", "WaterfallComponent", + "WordImportanceComponent", "WordProfileComponent", "pack_datapoint", "unpack_datapoint", diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 9f893aea..7cfa42f2 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -69,16 +69,28 @@ def available_capabilities(explanation: NlpExplanation, engine: InteractiveEngin return frozenset(caps) -def error_positions(explanation: NlpExplanation) -> set[int] | None: - """Positional indices of the samples the model got wrong, or ``None`` without ground truth. +def error_mask(explanation: NlpExplanation) -> np.ndarray | None: + """Boolean array, ``True`` where the prediction disagrees with the ground truth. - Compared as strings, the same way the dataset table's "Model Errors" filter does, so every - panel that scopes itself to errors scopes to exactly the same rows. + ``None`` when either is unavailable. Compared as strings, the same way the dataset table's + "Model Errors" filter does, so every panel that scopes itself to errors scopes to exactly the + same rows. :func:`error_positions` is this same comparison, shaped as a set of positions instead + of a boolean array — use whichever shape the caller needs, they must never drift apart. """ y_true, y_pred = explanation.y_true, explanation.y_pred if y_true is None or y_pred is None: return None - mask = np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) + return np.asarray(y_true).astype(str) != np.asarray(y_pred).astype(str) + + +def error_positions(explanation: NlpExplanation) -> set[int] | None: + """Positional indices of the samples the model got wrong, or ``None`` without ground truth. + + See :func:`error_mask` for the boolean-array shape of the same comparison. + """ + mask = error_mask(explanation) + if mask is None: + return None return set(np.where(mask)[0].tolist()) @@ -135,7 +147,9 @@ def layout(self, explanation: NlpExplanation, engine: InteractiveEngine | None = """ @abstractmethod - def register_callbacks(self, app, explanation: NlpExplanation, engine: InteractiveEngine, stores: dict) -> None: + def register_callbacks( + self, app, explanation: NlpExplanation, engine: InteractiveEngine | None, stores: dict + ) -> None: """Register this component's Dash callbacks. Parameters @@ -144,8 +158,9 @@ def register_callbacks(self, app, explanation: NlpExplanation, engine: Interacti The Dash application. explanation : NlpExplanation The immutable artifact to read (never written to). - engine : InteractiveEngine - Live engine for prediction / counterfactual generation. + engine : InteractiveEngine or None + Live engine for prediction / counterfactual generation, or ``None`` for a snapshot with + no live model — only reached for a component whose ``requires`` needs no engine capability. stores : dict Shared ``dcc.Store`` ids the What-if Lab wires between components (e.g. ``{"apply": "whatif-apply-store"}``). diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index 9799c6c0..8697bebc 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -125,7 +125,7 @@ def layout(self, explanation, engine=None) -> html.Div: className="text-muted d-block mb-2", ), controls_row, - dbc.Button("Generate", id=f"{self.id}-generate-btn", color="secondary", size="sm", className="mb-2"), + dbc.Button("Generate", id=f"{self.id}-generate-btn", color="warning", size="sm", className="mb-2"), dcc.Loading(html.Div(id=f"{self.id}-results")), dcc.Store(id=f"{self.id}-store", data=[]), ], diff --git a/shapash/webapp/nlp_components/data_editor.py b/shapash/webapp/nlp_components/data_editor.py index 0e02b77f..39fb6253 100644 --- a/shapash/webapp/nlp_components/data_editor.py +++ b/shapash/webapp/nlp_components/data_editor.py @@ -68,7 +68,7 @@ def layout(self, explanation, engine=None) -> html.Div: id=f"{self.id}-input", style={"width": "100%", "height": "90px", "fontSize": "0.95em"}, ), - dbc.Button("Predict", id=f"{self.id}-predict-btn", color="primary", size="sm", className="mt-2"), + dbc.Button("Predict", id=f"{self.id}-predict-btn", color="warning", size="sm", className="mt-2"), # Hidden until the first Predict, so an empty plot area is not shown on tab open. html.Div( [ diff --git a/shapash/webapp/nlp_components/error_analysis.py b/shapash/webapp/nlp_components/error_analysis.py index 994590cb..d4f6ee3a 100644 --- a/shapash/webapp/nlp_components/error_analysis.py +++ b/shapash/webapp/nlp_components/error_analysis.py @@ -24,6 +24,7 @@ from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_word_importance import empty_word_figure, plot_word_importance +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme from shapash.webapp.nlp_components.base import CAP_GROUND_TRUTH, WebappComponent _NORMALIZE_OPTIONS: list[dcc.RadioItems.Options] = [ @@ -58,7 +59,8 @@ class ErrorAnalysisComponent(WebappComponent): scope = "global" requires = frozenset({CAP_GROUND_TRUTH}) - def __init__(self) -> None: + def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: + self._theme = theme # Populated by layout(); register_callbacks() runs after layout() has built the tab (see # NlpWebApp._build_layout / _register_callbacks), so these are always set by then. self._cm: np.ndarray | None = None @@ -233,6 +235,8 @@ def update_error_word_charts(error_cell): height=None, show_values=False, counts=counts, + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, ) fig_true = plot_word_importance( wi_true, @@ -240,6 +244,8 @@ def update_error_word_charts(error_cell): width=None, height=None, show_values=False, + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, counts=counts, ) fig_pred.layout.height = None diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py index 15ae46c1..60548a1c 100644 --- a/shapash/webapp/nlp_components/label_noise.py +++ b/shapash/webapp/nlp_components/label_noise.py @@ -121,7 +121,7 @@ def layout(self, explanation, engine=None) -> html.Div: ], style=_INLINE, ), - dbc.Button("Detect", id=f"{self.id}-detect-btn", color="secondary", size="sm"), + dbc.Button("Detect", id=f"{self.id}-detect-btn", color="warning", size="sm"), ], className="d-flex flex-wrap align-items-center gap-3 mb-2", ), diff --git a/shapash/webapp/nlp_components/scatter.py b/shapash/webapp/nlp_components/scatter.py new file mode 100644 index 00000000..08afe927 --- /dev/null +++ b/shapash/webapp/nlp_components/scatter.py @@ -0,0 +1,385 @@ +"""Scatter (Embeddings) panel: 2-D projection of the text embeddings, box/lasso-selectable. + +Mounting is decided by the caller-supplied ``scatter_xy`` array, not by ``requires`` — a pre-computed +projection is arbitrary data handed to ``NlpWebApp``, not an explanation/engine capability, so +``NlpWebApp`` only ever constructs this component when it has one (see ``ScatterComponent.__init__``). + +The "Word contribution" color mode is a **structural, not capability, dependency** on +:class:`~shapash.webapp.nlp_components.word_importance.WordImportanceComponent`: coloring by a word's +contribution needs a class index, and that control lives on the Word Importance panel. Rather than +reading that panel's DOM id directly (cross-component data flows only through ``stores`` — see +``nlp_components/base.py``), this component reads the shared ``stores["active_class"]`` store that +Word Importance publishes to, and the shell tells this component at construction time +(``offer_word_contribution``) whether to even offer the mode — omitted rather than left silently +non-functional when Word Importance isn't mounted. + +The word list (``scatter-word-select``) syncs bidirectionally with the shared ``stores["word_click"]`` +store: a Word Importance bar click updates it here too, and editing the dropdown here updates the +shared store back — same end-user behavior as before extraction, just no longer dependent on which of +the two panels happens to be mounted or the order callbacks fire in. +""" + +from __future__ import annotations + +import dash_bootstrap_components as dbc +import numpy as np +import plotly.graph_objs as go +from dash import Input, Output, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.explainer.nlp_explanation import select_label_column +from shapash.webapp.nlp_components.base import WebappComponent, error_mask + +_PALETTE = [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +] + + +class ScatterComponent(WebappComponent): + """2-D embeddings scatter, coloured by prediction, ground truth, or word SHAP contribution.""" + + id = "scatter" + name = "Embeddings" + scope = "global" + requires = frozenset() + + def __init__(self, scatter_xy: np.ndarray, offer_word_contribution: bool) -> None: + self._scatter_xy = scatter_xy + self._offer_word_contribution = offer_word_contribution + + def layout(self, explanation, engine=None) -> html.Div: + """Build the color-by/word-select controls and the scatter graph itself.""" + word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in explanation.vocabulary()] + color_options: list[dcc.Dropdown.Options] = [{"label": "Prediction", "value": "prediction"}] + if explanation.y_true is not None: + color_options.append({"label": "Ground Truth", "value": "ground_truth"}) + if self._offer_word_contribution: + color_options.append({"label": "Word contribution", "value": "word_contribution"}) + + return html.Div( + [ + dbc.Row( + [ + dbc.Col( + html.H6("Sample Space", className="fw-bold mb-0"), + width="auto", + className="align-self-center", + ), + dbc.Col( + dcc.Dropdown( + id="color-by", + options=color_options, + value="prediction", + clearable=False, + style={"width": "160px", "fontSize": "0.9em"}, + ), + width="auto", + ), + dbc.Col( + dcc.Dropdown( + id="scatter-word-select", + options=word_options, + value=[], + multi=True, + clearable=True, + placeholder="Select words…", + style={"display": "none", "minWidth": "180px", "fontSize": "0.9em"}, + ), + width="auto", + className="align-self-center", + ), + ], + className="align-items-center mb-2", + ), + html.Small( + "Box/lasso or click to filter. Click a word bar to color by its SHAP contribution.", + className="text-muted d-block mb-2", + ), + dcc.Graph( + id="scatter-plot", + figure=self._build_scatter_fig(explanation, "prediction"), + config={ + "displayModeBar": True, + "modeBarButtonsToRemove": ["autoScale2d", "resetScale2d"], + "responsive": True, + }, + # Grow to fill the card's remaining height so the scatter + # matches the taller word-importance panel beside it. + style={"flex": "1 1 auto", "minHeight": "340px"}, + ), + ], + # Flex column so the graph above can stretch to the panel height. + style={"display": "flex", "flexDirection": "column", "height": "100%"}, + ) + + def register_callbacks(self, app, explanation, engine, stores) -> None: + """Wire coloring, selection, and the bidirectional word-list sync with the shared store.""" + selection_store = stores["selection"] + selection_clear_btn = stores["selection_clear"] + errors_only_switch = stores["errors_only"] + word_click_store = stores["word_click"] + active_class_store = stores["active_class"] + + @app.callback( + Output("scatter-plot", "figure"), + [ + Input("color-by", "value"), + Input("scatter-word-select", "value"), + Input(active_class_store, "data"), + Input(errors_only_switch, "value"), + ], + ) + def update_scatter_color(color_by, words, label_idx, errors_only): + return self._build_scatter_fig( + explanation, + color_by or "prediction", + words=words or [], + label_idx=label_idx if isinstance(label_idx, int) else 0, + errors_only=bool(errors_only), + ) + + @app.callback( + Output("color-by", "value"), + Input("scatter-word-select", "value"), + ) + def sync_color_by(words): + if not words: + raise PreventUpdate + return "word_contribution" + + # Bidirectional sync with the shared word-click store: a Word Importance bar click lands + # here too, and editing/clearing the dropdown here updates the shared store back — neither + # panel "wins", both stay in sync regardless of which one changed first. + @app.callback( + Output("scatter-word-select", "value"), + Input(word_click_store, "data"), + ) + def follow_word_click(word_filter): + words = word_filter if isinstance(word_filter, list) else ([word_filter] if word_filter else []) + return words + + @app.callback( + Output(word_click_store, "data", allow_duplicate=True), + Input("scatter-word-select", "value"), + prevent_initial_call=True, + ) + def word_filter_from_scatter(words): + return words or None + + @app.callback( + Output("scatter-word-select", "style"), + Input("color-by", "value"), + ) + def toggle_word_select(color_by): + base = {"minWidth": "180px", "fontSize": "0.9em"} + return base if color_by == "word_contribution" else {**base, "display": "none"} + + @app.callback( + Output(selection_store, "data"), + Input("scatter-plot", "selectedData"), + Input("scatter-plot", "clickData"), + Input(selection_clear_btn, "n_clicks"), + ) + def update_scatter_selection(selected_data, click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if selection_clear_btn in trigger: + return None + if "clickData" in trigger: + if not click_data or not click_data.get("points"): + return None + return [int(click_data["points"][0]["customdata"][0])] + # An empty selectedData here is almost always plotly re-emitting on a figure recolor + # (color-by / word / errors-only toggle), NOT a user deselect — ignore it so the box + # survives. Genuine clears go through the clear button or a point click above. + if not selected_data or not selected_data.get("points"): + raise PreventUpdate + return [int(pt["customdata"][0]) for pt in selected_data["points"]] + + @app.callback( + Output(selection_clear_btn, "style"), + Input(selection_store, "data"), + ) + def toggle_clear_button(selected_indices): + visible = {"display": "inline", "fontSize": "0.8em"} + hidden = {"display": "none", "fontSize": "0.8em"} + return visible if selected_indices else hidden + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _word_contributions(self, explanation, word: str, label_idx: int) -> np.ndarray: + """Per-sample sum of SHAP contributions for all tokens matching *word*.""" + n = explanation.n_samples + result = np.zeros(n) + word_lower = word.lower() + for i in range(n): + tokens = explanation.token_strings[i] + vals = select_label_column(explanation.values[i], label_idx) + for j, tok in enumerate(tokens): + if tok.strip().lower() == word_lower: + result[i] += vals[j] + return result + + @staticmethod + def _emphasize_errors( + idx_arr: np.ndarray, + base_opacity: float, + base_size: float, + error_mask: np.ndarray | None, + ) -> tuple[list[float] | float, list[float] | float]: + """Per-point opacity/size that pops model errors and shadows everything else. + + Keeps the caller's coloring untouched — only opacity and marker size change — + so points stay grouped/colored however the "Color by" dropdown already draws them. + Returns scalars (the unmodified base values) when ``error_mask`` is unavailable. + """ + if error_mask is None or len(idx_arr) == 0: + return base_opacity, base_size + is_error = error_mask[idx_arr] + opacity = np.where(is_error, max(base_opacity, 0.9), 0.12).tolist() + size = np.where(is_error, base_size + 3, max(base_size - 2, 3)).tolist() + return opacity, size + + def _build_scatter_fig( + self, + explanation, + color_by: str, + words: list[str] | None = None, + label_idx: int = 0, + errors_only: bool = False, + ) -> go.Figure: + """2-D scatter coloured by prediction, ground-truth label, or word SHAP contribution. + + One trace per class for label-based coloring so the legend works correctly + and Plotly's box/lasso select dims unselected points across all traces. + Word-contribution mode uses a single diverging-colorscale trace. + ``customdata`` always stores the original sample index. When ``errors_only`` is + set, misclassified points are emphasized (larger, opaque) and the rest are + shadowed (small, faint) without altering their color. + """ + n = explanation.n_samples + exp_texts = explanation.texts + assert exp_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer + texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in exp_texts] + xy = self._scatter_xy + err_mask = error_mask(explanation) if errors_only else None + + if color_by == "word_contribution" and words: + contributions = np.sum([self._word_contributions(explanation, w, label_idx) for w in words], axis=0) + max_abs = float(np.abs(contributions).max()) or 1.0 + present_mask = np.where(contributions != 0.0)[0] + absent_mask = np.where(contributions == 0.0)[0] + colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" + + fig = go.Figure() + # Both layers are ALWAYS added (even when a mask is empty) so the trace structure stays + # constant across word additions/removals. With a stable `uirevision`, a changing WebGL + # (Scattergl) trace count leaves ghost/"shadow" points from the previous render — keeping + # exactly two traces avoids that. + absent_opacity, absent_size = self._emphasize_errors(absent_mask, 0.35, 5, err_mask) + present_opacity, present_size = self._emphasize_errors(present_mask, 0.9, 9, err_mask) + + # Gray context layer — absent points (no selected word contributes to them). + fig.add_trace( + go.Scattergl( + x=xy[absent_mask, 0].tolist(), + y=xy[absent_mask, 1].tolist(), + mode="markers", + marker=dict(color="#b0b0b0", size=absent_size, opacity=absent_opacity), + customdata=absent_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in absent_mask], + hovertemplate="%{text}<extra>absent</extra>", + showlegend=False, + ) + ) + # Colored overlay — samples where at least one selected word contributes. + fig.add_trace( + go.Scattergl( + x=xy[present_mask, 0].tolist(), + y=xy[present_mask, 1].tolist(), + mode="markers", + marker=dict( + color=contributions[present_mask].tolist(), + colorscale="RdBu", + cmin=-max_abs, + cmax=max_abs, + size=present_size, + opacity=present_opacity, + colorbar=dict( + title=dict(text=colorbar_title, side="right"), + thickness=12, + tickformat=".2f", + ), + ), + customdata=present_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in present_mask], + hovertemplate="<b>SHAP: %{marker.color:.3f}</b><br>%{text}<extra></extra>", + showlegend=False, + ) + ) + fig.update_layout( + dragmode="select", + uirevision="scatter", + xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + plot_bgcolor="#f9f9f9", + paper_bgcolor="white", + margin=dict(l=10, r=10, t=10, b=10), + autosize=True, + showlegend=False, + ) + return fig + + if color_by == "ground_truth" and explanation.y_true is not None: + labels = [str(label) for label in explanation.y_true.tolist()] + elif explanation.y_pred is not None: + labels = [str(label) for label in explanation.y_pred.tolist()] + else: + labels = [""] * n + + label_names = explanation.label_names or sorted(set(labels)) + + fig = go.Figure() + for i, name in enumerate(label_names): + mask = [j for j, lbl in enumerate(labels) if lbl == name] + if not mask: + continue + mask_arr = np.array(mask) + opacity, size = self._emphasize_errors(mask_arr, 0.75, 7, err_mask) + fig.add_trace( + go.Scattergl( + x=xy[mask_arr, 0].tolist(), + y=xy[mask_arr, 1].tolist(), + mode="markers", + marker=dict(color=_PALETTE[i % len(_PALETTE)], size=size, opacity=opacity), + customdata=mask_arr.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in mask], + hovertemplate=f"<b>{name}</b><br>%{{text}}<extra></extra>", + name=name, + ) + ) + + fig.update_layout( + dragmode="select", + uirevision="scatter", + xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + plot_bgcolor="#f9f9f9", + paper_bgcolor="white", + margin=dict(l=10, r=10, t=10, b=10), + # No fixed height — the responsive Graph stretches it to fill the card. + autosize=True, + legend=dict(itemsizing="constant", orientation="v", title_text=""), + showlegend=True, + ) + return fig diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py index 2853e846..c62958d5 100644 --- a/shapash/webapp/nlp_components/sentence_highlight.py +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -6,6 +6,7 @@ from dash.exceptions import PreventUpdate from shapash.plots.plot_sentence_highlight import plot_sentence_highlight +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme from shapash.webapp.nlp_components.base import WebappComponent from shapash.webapp.nlp_components.datapoint import unpack_datapoint @@ -21,8 +22,9 @@ class SentenceHighlightComponent(WebappComponent): name = "Sentence" scope = "local" - def __init__(self, default_class_idx: int = 0) -> None: + def __init__(self, default_class_idx: int = 0, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._default_class_idx = default_class_idx + self._theme = theme def layout(self, explanation, engine=None) -> html.Div: """Return the class picker + sentence-highlight placeholder div.""" @@ -90,4 +92,10 @@ def update_sentence_highlight(datapoint, label_idx): if not datapoint or label_idx is None: raise PreventUpdate tokens, vals, base_value, _ = unpack_datapoint(datapoint, int(label_idx)) - return plot_sentence_highlight(tokens=tokens, values=vals, base_value=base_value) + return plot_sentence_highlight( + tokens=tokens, + values=vals, + base_value=base_value, + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, + ) diff --git a/shapash/webapp/nlp_components/waterfall.py b/shapash/webapp/nlp_components/waterfall.py index 56d2e4d5..08ace9f0 100644 --- a/shapash/webapp/nlp_components/waterfall.py +++ b/shapash/webapp/nlp_components/waterfall.py @@ -6,6 +6,7 @@ from dash.exceptions import PreventUpdate from shapash.plots.plot_waterfall import plot_waterfall +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme from shapash.webapp.nlp_components.base import WebappComponent from shapash.webapp.nlp_components.datapoint import unpack_datapoint @@ -22,6 +23,9 @@ class WaterfallComponent(WebappComponent): name = "Waterfall" scope = "local" + def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: + self._theme = theme + def layout(self, explanation, engine=None) -> html.Div: """Return the grouping-threshold slider + waterfall graph.""" return html.Div( @@ -74,4 +78,6 @@ def update_waterfall(datapoint, label_idx, threshold_pct): base_value=base_value, min_pct=min_pct, title=f"Token contributions — {label_name}", + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, ) diff --git a/shapash/webapp/nlp_components/word_importance.py b/shapash/webapp/nlp_components/word_importance.py new file mode 100644 index 00000000..f1279179 --- /dev/null +++ b/shapash/webapp/nlp_components/word_importance.py @@ -0,0 +1,396 @@ +"""Word Importance panel: mean/total SHAP contribution per unique word, for one class or all of them. + +The corpus-level counterpart to the Sentence Highlight panel — "which words matter" instead of "why +this one prediction". Honours the app's global sample selection (scatter box/lasso, confusion-matrix +cell, Model Errors switch) through the shared ``stores["selection"]``/``stores["error_cell"]``/ +``stores["errors_only"]`` stores, exactly as :class:`~shapash.webapp.nlp_components.word_profile.WordProfileComponent` +does — both call the same :func:`~shapash.webapp.nlp_components.base.compose_selection`. + +A clicked bar is this panel's one cross-panel *output*: it always writes the shell's shared +``stores["word_click"]`` store, regardless of whether an embeddings scatter is also mounted — the +scatter, when present, is what reads/mirrors that store rather than the other way around (see +:mod:`~shapash.webapp.nlp_components.scatter`). This panel also publishes its class selector's value +to ``stores["active_class"]``, which is how the scatter's "Word contribution" color mode gets a class +index without reading this panel's DOM id directly. + +Data-only: ``requires`` is empty, so it mounts on a loaded snapshot with no live model, the same as +``WordProfileComponent``. +""" + +from __future__ import annotations + +import dash +from dash import Input, Output, callback_context, dcc, html +from dash.exceptions import PreventUpdate + +from shapash.plots.plot_word_importance import empty_word_figure, plot_word_importance, word_importance_axis_title +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme +from shapash.webapp.nlp_components.base import WebappComponent, compose_selection, error_positions + +# The frequency floor the global word ranking starts at. 1 (no filter) is the status quo and is +# actively misleading: on the emotion demo 58% of the vocabulary occurs exactly once, so an +# unfiltered |mean| ranking is a list of single attributions. 2 is the principled minimum — a mean +# over one observation is not a mean — and is the least surprising default; 3 is where the chart +# visibly settles into repeated, stable words. +_DEFAULT_MIN_OCCURRENCES = 2 + +# Bounds for the Top-K box. The browser enforces these on the spinner arrows but not on typed +# input, so the callback clamps rather than trusting them. +_MIN_TOPK, _MAX_TOPK, _DEFAULT_TOPK = 1, 50, 20 +# Idle delay (seconds) before a number box commits its value. ``debounce=True`` would commit only on +# Enter or blur, which leaves the spinner arrows apparently dead: clicking one keeps focus in the +# box, so nothing reaches the server until the user clicks away. A numeric debounce commits after a +# pause instead, so arrows respond while a typed "20" still does not recompute at "2". +_INPUT_DEBOUNCE_S = 0.4 + +# Class-dropdown value meaning "collapse the ranking across every class" — a string so it can never +# collide with a real (integer) class index. +_ALL_CLASSES = "all" + +# Shared by the layout and the callback that greys the sign options out under "All classes", so the +# two cannot drift apart. +_SIGN_FILTER_OPTIONS: list[dcc.RadioItems.Options] = [ + {"label": " All", "value": "all"}, + {"label": " Positive", "value": "positive"}, + {"label": " Negative", "value": "negative"}, +] + + +class WordImportanceComponent(WebappComponent): + """Global word-importance ranking, with class/top-K/sign/exclude controls.""" + + id = "word-importance" + name = "Word Importance" + scope = "global" + requires = frozenset() + + def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: + self._theme = theme + + def layout(self, explanation, engine=None) -> html.Div: + """Build the controls + graph. DOM ids are the pre-extraction literals (no ``self.id`` prefix) + so existing layout tests keep matching — these ids were never shared with any other panel. + """ + label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] + word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in explanation.vocabulary()] + + # "All classes" first: it is the overview a reader without a class in mind wants, and it is + # the only entry whose value is not an index (see _ALL_CLASSES). + global_class_options: list[dcc.Dropdown.Options] = [{"label": "All classes", "value": _ALL_CLASSES}] + global_class_options += [{"label": name, "value": i} for i, name in enumerate(label_names)] + # Ordering only: both statistics are signed and are ranked on |value|, so the sign filter + # and the bars' red/blue colouring mean the same thing in either mode. + rank_by_options: list[dcc.Dropdown.Options] = [ + {"label": "Mean |·|", "value": "mean"}, + {"label": "Total |·|", "value": "sum"}, + ] + + return html.Div( + [ + # One flex row split into two clusters — "what to rank" (left) and "what to keep" + # (right) — rather than six controls in an unbroken run: grouping reads faster, and + # a Bootstrap 50/50 column split was tried first but broke (the left cluster's own + # natural width exceeds half the panel at ordinary sizes, so the fixed-width column + # either overlapped the right cluster or wrapped mid-cluster). space-between instead + # pushes the right cluster as far right as the two clusters' actual widths allow — + # around the midpoint in practice, and never overlapping or force-wrapping either + # cluster — with flex-wrap kept as a fallback for genuinely narrow panels. + html.Div( + [ + html.Div( + [ + html.Div( + [ + html.Label("Class", className="fw-bold small mb-0"), + dcc.Dropdown( + id="global-class-selector", + options=global_class_options, + value=0, + clearable=False, + style={"width": "140px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Rank by", + className="fw-bold small mb-0", + title=( + "Mean: the average pull wherever the word " + "appears. Total: its whole pull on the " + "corpus, so frequent words outrank rare " + "strong ones. The two disagree often." + ), + ), + dcc.Dropdown( + id="rank-by", + options=rank_by_options, + value="mean", + clearable=False, + style={"width": "95px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Min occur.", + className="fw-bold small mb-0", + title=( + "Hide words seen fewer than this many times " + "in the current selection. A mean over one " + "occurrence is not a mean, and most of a " + "corpus is words seen once." + ), + ), + dcc.Input( + id="min-occurrences", + type="number", + min=1, + step=1, + value=_DEFAULT_MIN_OCCURRENCES, + debounce=_INPUT_DEBOUNCE_S, + className="form-control form-control-sm", + style={"width": "80px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Top-K words", + className="fw-bold small mb-0", + title=f"How many words to chart, {_MIN_TOPK}–{_MAX_TOPK}", + ), + dcc.Input( + id="topk-input", + type="number", + min=_MIN_TOPK, + max=_MAX_TOPK, + step=1, + value=_DEFAULT_TOPK, + debounce=_INPUT_DEBOUNCE_S, + className="form-control form-control-sm", + style={"width": "90px"}, + ), + ] + ), + ], + className="d-flex align-items-start flex-wrap gap-2", + ), + html.Div( + [ + html.Div( + [ + html.Label("Contributions", className="fw-bold small mb-0"), + dcc.RadioItems( + id="sign-filter", + options=_SIGN_FILTER_OPTIONS, + value="all", + inline=True, + inputStyle={"marginRight": "4px"}, + labelStyle={"marginRight": "12px"}, + ), + ] + ), + html.Div( + [ + html.Label( + "Exclude words", + className="fw-bold small mb-0", + style={"whiteSpace": "nowrap"}, + ), + dcc.Dropdown( + id="word-filter", + options=word_options, + value=[], + multi=True, + placeholder="Exclude…", + style={"fontSize": "0.85em"}, + ), + ], + style={"flex": "1 1 auto", "minWidth": "130px"}, + ), + ], + className="d-flex align-items-start flex-wrap gap-2", + ), + ], + className="d-flex align-items-start flex-wrap mb-1", + style={"flex": "0 0 auto", "justifyContent": "space-between", "gap": "0.5rem"}, + ), + # The graph keeps the height plot_word_importance computed for it (30px per word) + # and this wrapper scrolls. Letting the chart flex to the panel instead — which is + # what it used to do — gives 50 words 8px each, at which point plotly drops the + # word labels and the bars become unidentifiable. + # "responsive" must stay off here: it makes plotly track the *container's* current + # size instead of the figure's own layout.height, which silently defeats the fixed + # per-row height above and squeezes every word back into whatever space the flex + # panel happens to have — the exact illegible-labels bug this height is built to + # avoid. Width still adapts once, at mount, from the container. + html.Div( + dcc.Graph( + id="global-importance-graph", + config={"displayModeBar": False}, + style={"width": "100%"}, + ), + style={"flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"}, + ), + ], + style={"height": "100%", "display": "flex", "flexDirection": "column"}, + ) + + def register_callbacks(self, app, explanation, engine, stores) -> None: + """Wire the ranking, the sign-filter gate, and the bar-click → shared word/class stores.""" + selection_store = stores["selection"] + error_cell_store = stores["error_cell"] + errors_only_switch = stores["errors_only"] + word_click_store = stores["word_click"] + word_click_clear_btn = stores["word_click_clear"] + active_class_store = stores["active_class"] + + @app.callback( + Output("global-importance-graph", "figure"), + [ + Input("global-class-selector", "value"), + Input("topk-input", "value"), + Input("sign-filter", "value"), + Input("word-filter", "value"), + Input("rank-by", "value"), + Input("min-occurrences", "value"), + Input(selection_store, "data"), + Input(error_cell_store, "data"), + Input(errors_only_switch, "value"), + ], + ) + def update_global_importance( + label_idx, + topk, + sign_filter, + exclude_words_list, + rank_by, + min_occurrences, + selected_indices, + error_cell, + errors_only, + ): + if label_idx is None: + raise PreventUpdate + across_classes = label_idx == _ALL_CLASSES + rank_by = rank_by or "mean" + # Typed input bypasses the box's own min/max, and a cleared box arrives as None. + n_top = max(_MIN_TOPK, min(_MAX_TOPK, int(topk))) if topk is not None else _DEFAULT_TOPK + # A cleared number input arrives as None; 1 is "no floor", which is what an empty box + # should mean rather than reverting to the default the user just deleted. + floor = max(1, int(min_occurrences)) if min_occurrences else 1 + cell_indices = error_cell.get("indices") if error_cell else None + errors = error_positions(explanation) if (errors_only and error_positions(explanation)) else None + effective_indices = compose_selection(selected_indices, cell_indices, errors) + # Across classes the bars are magnitudes (max over classes of |statistic|), so a sign + # filter has nothing left to select on: it is greyed out in that mode and forced to + # "all" here as well, so a value left over from a single-class view cannot silently + # empty the chart. + sign = "all" if across_classes else (sign_filter or "all") + word_imp = explanation.word_importance( + label_idx=None if across_classes else int(label_idx), + n_top=n_top, + filter_sign=sign, + # Punctuation is its own unit since word segmentation splits on word/non-word + # boundaries; corpus-wide its mean contribution averages to ~0, so it is noise here. + # Per-instance punctuation contributions stay visible in the Sentence Highlight panel. + filter_punctuation=True, + exclude_words=set(exclude_words_list or []) or None, + sample_indices=effective_indices, + rank_by=rank_by, + min_occurrences=floor, + ) + n_scope = len(effective_indices) if effective_indices is not None else len(explanation) + if word_imp.empty: + # Name the filter that actually emptied it, since the fix differs: the sign filter + # (checked first — it is applied last), then the frequency floor, then everything + # else (an exclusion list or an empty selection). + if sign in ("positive", "negative"): + reason = f"No {sign} word passes these filters." + elif floor > 1: + reason = f"No word occurs at least {floor} time(s) in these {n_scope} sample(s)." + else: + reason = f"No word passes these filters in these {n_scope} sample(s)." + return empty_word_figure(reason) + + # Same filters and same sample scope as the ranking above, so the count on a bar's + # hover is the count that bar's aggregate was computed over — and the count the + # min-occurrences floor was applied to. + counts = explanation.word_counts(filter_punctuation=True, sample_indices=effective_indices) + fig = plot_word_importance( + word_imp, + # No title: the tab is already labelled "Word Importance" and the class/floor are + # both visible in the filter row right above the chart — a repeated title band was + # just eating vertical space this panel needs for word rows (see the graph wrapper + # below, which scrolls when it runs out). + title=None, + # A mean, a total and a cross-class magnitude are different quantities on an + # identical-looking chart, so the axis has to say which one is drawn. Derived from + # what word_importance stamped on its result rather than re-deduced here. + x_title=word_importance_axis_title(str(word_imp.name)), + width=None, + height=None, + counts=counts["n_occurrences"], + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, + ) + # Height deliberately left as plot_word_importance computed it — see the graph's + # wrapper in the layout. + return fig + + # Under "All classes" the ranking is a magnitude, so Positive/Negative would silently + # return nothing. Grey them out and pull the selection back to "All" rather than leaving a + # live control that cannot do anything. + @app.callback( + Output("sign-filter", "options"), + Output("sign-filter", "value"), + Input("global-class-selector", "value"), + ) + def gate_sign_filter(label_idx): + if label_idx != _ALL_CLASSES: + return _SIGN_FILTER_OPTIONS, dash.no_update + greyed = [{**opt, "disabled": opt["value"] != "all"} for opt in _SIGN_FILTER_OPTIONS] + return greyed, "all" + + # A word bar clicked here is the one cross-panel *output* of this component: it always + # writes the shared word-click store, whether or not an embeddings scatter is mounted — the + # scatter (when present) mirrors this store rather than being the thing this panel branches + # on. Resetting clickData lets the SAME bar be re-clicked (plotly does not re-fire an + # unchanged clickData). + @app.callback( + Output(word_click_store, "data"), + Output("global-importance-graph", "clickData"), + Input("global-importance-graph", "clickData"), + Input(word_click_clear_btn, "n_clicks"), + prevent_initial_call=True, + ) + def update_word_click_filter(click_data, _clear_clicks): + trigger = callback_context.triggered[0]["prop_id"] if callback_context.triggered else "" + if word_click_clear_btn in trigger: + return None, None + if not click_data or not click_data.get("points"): + raise PreventUpdate + return [click_data["points"][0]["y"]], None + + @app.callback( + Output(word_click_clear_btn, "style"), + Input(word_click_store, "data"), + ) + def toggle_word_clear_button(word_filter): + if word_filter: + return {"display": "inline", "fontSize": "0.8em"} + return {"display": "none", "fontSize": "0.8em"} + + # Publishes the active class so ScatterComponent's word-contribution mode can read it + # without referencing this component's DOM id directly (cross-component data only flows + # through `stores`). + @app.callback( + Output(active_class_store, "data"), + Input("global-class-selector", "value"), + ) + def publish_active_class(label_idx): + return label_idx if isinstance(label_idx, int) else 0 diff --git a/shapash/webapp/nlp_components/word_profile.py b/shapash/webapp/nlp_components/word_profile.py index 14d30178..580e8841 100644 --- a/shapash/webapp/nlp_components/word_profile.py +++ b/shapash/webapp/nlp_components/word_profile.py @@ -42,6 +42,7 @@ ) from shapash.plots.plot_word_importance import empty_word_figure from shapash.plots.plot_word_profile import plot_word_profile +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme from shapash.webapp.nlp_components.base import WebappComponent, compose_selection, error_positions from shapash.webapp.nlp_components.datapoint import pack_datapoint @@ -129,7 +130,8 @@ class WordProfileComponent(WebappComponent): # Data-only: reads the artifact and nothing else, so it also mounts on a loaded snapshot. requires = frozenset() - def __init__(self) -> None: + def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: + self._theme = theme # Both option orderings, built once in layout() and swapped by a callback: the counts cost # a full pass over the corpus, and the sort toggle must not pay it on every click. self._options: dict[str, list] = {"alpha": [], "frequency": []} @@ -331,6 +333,8 @@ def update_profile(word, agg, class_idx, order, limit, selected_indices, error_c title="", # the caption below carries the word and the counts width=None, height=None, + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, ) fig.layout.height = None # let the CSS container height take over diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index ade71b3d..fe683a1d 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -37,6 +37,7 @@ from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance from shapash.plots.plot_word_profile import plot_word_profile +from shapash.style.style_utils import DEFAULT_NLP_THEME from shapash.webapp.nlp_app import NlpWebApp from shapash.webapp.nlp_components import compose_selection as _compose_selection from shapash.webapp.nlp_components import error_positions @@ -1717,8 +1718,8 @@ def test_class_zero_is_drawn_at_the_top(self): def test_sign_drives_colour(self): fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) # Reversed alongside the values, so the last colour belongs to the positive class 0. - self.assertEqual(fig.data[0].marker.color[-1], "#1f77b4") - self.assertEqual(fig.data[0].marker.color[0], "#d62728") + self.assertEqual(fig.data[0].marker.color[-1], DEFAULT_NLP_THEME.xpl_positive) + self.assertEqual(fig.data[0].marker.color[0], DEFAULT_NLP_THEME.xpl_negative) def test_falls_back_to_class_indices_without_names(self): fig = plot_word_profile(self.stats) @@ -2396,6 +2397,9 @@ def test_url_base_pathname_reaches_the_webapp(self): engine=xpl, scatter_xy=None, url_base_pathname="/shapash-nlp-explainer/", + palette_name="default", + colors_dict=None, + info={}, ) def test_defaults_to_no_prefix(self): diff --git a/tests/unit_tests/style/test_style_utils.py b/tests/unit_tests/style/test_style_utils.py index 157ee565..fe50bf86 100644 --- a/tests/unit_tests/style/test_style_utils.py +++ b/tests/unit_tests/style/test_style_utils.py @@ -9,6 +9,7 @@ convert_str_color_to_plt_format, define_style, get_pyplot_color, + resolve_nlp_theme, select_palette, ) @@ -78,3 +79,31 @@ def test_get_pyplot_color(self): col1 = colors_loading()[palette_name]["title_color"] color = get_pyplot_color(col1) assert isinstance(color, list) + + def test_palette_key_parity(self): + """Every palette must define the same keys. + + A palette missing a key that another one has surfaces as a bare ``KeyError`` wherever that + key is first read (e.g. deep in a plot call), not at load time. Cheaper to catch here. + """ + available_palettes = colors_loading() + key_sets = {name: set(palette.keys()) for name, palette in available_palettes.items()} + reference_name, reference_keys = next(iter(key_sets.items())) + for name, keys in key_sets.items(): + assert keys == reference_keys, ( + f"Palette '{name}' differs from '{reference_name}': " + f"missing {reference_keys - keys}, extra {keys - reference_keys}" + ) + + def test_resolve_nlp_theme(self): + """Default theme resolves from the 'default' palette; colors_dict overrides win.""" + theme = resolve_nlp_theme() + default_palette = select_palette(colors_loading(), "default") + assert theme.header_bkg == default_palette["webapp_bkg"] + assert theme.header_accent == default_palette["webapp_title"] + assert theme.xpl_positive == default_palette["nlp_xpl_positive"] + assert theme.xpl_negative == default_palette["nlp_xpl_negative"] + + overridden = resolve_nlp_theme(colors_dict={"nlp_xpl_positive": "rgb(1, 2, 3)"}) + assert overridden.xpl_positive == "rgb(1, 2, 3)" + assert overridden.xpl_negative == default_palette["nlp_xpl_negative"] diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index 7b6601db..bfe854fe 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -1223,7 +1223,7 @@ def test_sign_filter_still_works_in_both_modes(self): self.assertNotIn("mild", self._words(self._call(rank_by=rank_by, sign="positive"))) def test_default_floor_is_two(self): - from shapash.webapp.nlp_app import _DEFAULT_MIN_OCCURRENCES + from shapash.webapp.nlp_components.word_importance import _DEFAULT_MIN_OCCURRENCES # A mean over a single observation is not a mean; the panel must not open on one. self.assertEqual(_DEFAULT_MIN_OCCURRENCES, 2) @@ -1332,7 +1332,7 @@ def test_topk_controls_the_bar_count(self): self.assertEqual(len(self._words(self._call(topk=2))), 2) def test_typed_topk_is_clamped_to_the_boxs_range(self): - from shapash.webapp.nlp_app import _MAX_TOPK, _MIN_TOPK + from shapash.webapp.nlp_components.word_importance import _MAX_TOPK, _MIN_TOPK # The browser enforces min/max on the spinner but not on typed input. self.assertEqual(len(self._words(self._call(topk=999))), 3) # only 3 words exist @@ -1341,7 +1341,7 @@ def test_typed_topk_is_clamped_to_the_boxs_range(self): self.assertLessEqual(_MAX_TOPK, 50) def test_cleared_topk_box_falls_back_to_the_default(self): - from shapash.webapp.nlp_app import _DEFAULT_TOPK + from shapash.webapp.nlp_components.word_importance import _DEFAULT_TOPK self.assertEqual(_DEFAULT_TOPK, 20) # Only None (an emptied box) restores the default — a typed 0 clamps instead. @@ -1359,6 +1359,236 @@ def test_missing_class_prevents_update(self): self.graph(None, 10, "all", [], "mean", 1, None, None, False) +class TestWordImportanceScatterSync(unittest.TestCase): + """Word Importance and Scatter communicate only through the shared stores (Phase A extraction). + + A bar click always writes ``word-click-filter`` directly now, whether or not a scatter is + mounted (no more branching on the scatter's presence), and the scatter's own word dropdown + mirrors that store bidirectionally instead of being the "source of truth" only when it exists. + """ + + @staticmethod + def _explanation(): + return TestGlobalWordImportancePanel._explanation() + + def test_bar_click_registered_without_a_scatter(self): + app = NlpWebApp(self._explanation(), engine=None) + pairs = _callback_binding_ids(app, "global-importance-graph.clickData") + self.assertIn(("global-importance-graph", "clickData"), pairs) + self.assertIn(("word-filter-clear-btn", "n_clicks"), pairs) + + def test_bar_click_registered_with_a_scatter_too(self): + # Before Phase A, a mounted scatter changed which store a bar click targeted; now Word + # Importance always registers this callback unconditionally, regardless of scatter presence. + app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + pairs = _callback_binding_ids(app, "global-importance-graph.clickData") + self.assertIn(("global-importance-graph", "clickData"), pairs) + + def test_scatter_word_select_mirrors_the_shared_store(self): + app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + pairs = _callback_binding_ids(app, "scatter-word-select.value") + self.assertIn(("word-click-filter", "data"), pairs) + + def test_class_selector_publishes_to_the_active_class_store(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "active-class-store.data") + self.assertEqual(fn(1), 1) + # "All classes" (the string sentinel) is not itself a class index — falls back to 0. + self.assertEqual(fn("all"), 0) + + +class TestTableFilterIntersection(unittest.TestCase): + """The dataset table's active filters (error cell, word click, errors-only) must intersect for + real. A filter that empties the combination has to actually empty the table, not be silently + dropped while the summary still claims it is applied — see the reported bug where clicking a + Word Importance bar and then an Error Analysis cell with no matching rows kept the word filter + in the summary text while showing every row from the cell, unfiltered. + """ + + @staticmethod + def _explanation(): + texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def test_word_filter_after_error_cell_narrows_instead_of_being_dropped(self): + # "happy" only occurs in row 0 ("i am happy"); the clicked cell (pred=pos, true=neg) holds + # only row 1 ("so glad"). The intersection is empty and must stay empty. + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + error_cell = {"pred": 1, "true": 0, "indices": [1]} + rows, selected, _title, summary = fn(None, ["happy"], False, error_cell, None, None) + self.assertEqual(rows, []) + self.assertEqual(selected, []) + self.assertIn('containing "happy"', summary) + self.assertIn("(0 of 4)", summary) + + def test_word_filter_matching_the_cell_still_narrows(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + error_cell = {"pred": 0, "true": 1, "indices": [3]} # row 3: "very sad" + rows, selected, _title, summary = fn(None, ["sad"], False, error_cell, None, None) + self.assertEqual([r["text"] for r in rows], ["very sad"]) + self.assertEqual(selected, rows) + self.assertIn("(1 of 4)", summary) + + def test_errors_only_narrows_a_word_filter_too(self): + # "happy" only occurs in row 0, a correct prediction — errors_only must not silently drop + # the word filter to avoid an empty table. + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + rows, _selected, _title, summary = fn(None, ["happy"], True, None, None, None) + self.assertEqual(rows, []) + self.assertIn("model errors only", summary) + self.assertIn('containing "happy"', summary) + + def test_word_filter_matches_the_exact_token_not_a_substring(self): + # "unhappy" contains "happy" as a substring but is a different token — a bar labeled + # "happy" must not pull it in, matching the exact-token semantics word_importance()/the + # scatter's word-contribution coloring already use. + texts = pd.Series(["i am happy", "so unhappy today"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + explanation = NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((2, 2)), + y_pred=pd.Series(["pos", "neg"], name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + app = NlpWebApp(explanation, engine=None) + fn = _callback(app, "dataset-table.rowData") + rows, _selected, _title, _summary = fn(None, ["happy"], False, None, None, None) + self.assertEqual([r["text"] for r in rows], ["i am happy"]) + + def test_grid_filter_narrows_the_shown_count_without_touching_rowData(self): + # AG Grid's own column filter runs client-side on top of whatever rowData filter_table + # hands it — it must not change rowData/selectedRows, only the reported count, so the + # grid's filtered view and the "Showing:" summary agree without a second, disconnected + # counter (the earlier bug this mirrors: a filter description without a matching count). + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + filter_model = {"text": {"filterType": "text", "type": "contains", "filter": "happy"}} + virtual_row_data = [{"text": "i am happy"}] + rows, _selected, _title, summary = fn(None, None, False, None, filter_model, virtual_row_data) + self.assertEqual(len(rows), 4) # rowData is untouched — the grid does its own filtering + self.assertIn("grid filter: Text", summary) + self.assertIn("(1 of 4)", summary) + + +def _callback(app, out_substr): + for key, spec in app.app.callback_map.items(): + if out_substr in key: + return getattr(spec["callback"], "__wrapped__", spec["callback"]) + raise KeyError(out_substr) + + +class TestGridFilterClearButton(unittest.TestCase): + """AG Grid's own column filters run client-side, outside filter_table's inputs — the clear + button is the only thing that lets a user drop one without reaching into the grid's own header + UI, so its show/hide and reset behavior need their own coverage. The filter's *description* is + covered by filter_table itself (see TestTableFilterIntersection), since it's folded into the + same "Showing:" summary rather than a separate badge. + """ + + @staticmethod + def _explanation(): + texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def test_no_filter_model_hides_the_clear_button(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "grid-filter-clear-btn.style") + self.assertEqual(fn(None)["display"], "none") + + def test_active_filter_model_shows_the_clear_button(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "grid-filter-clear-btn.style") + filter_model = {"prediction": {"filterType": "text", "type": "equals", "filter": "pos"}} + self.assertNotEqual(fn(filter_model).get("display"), "none") + + def test_clear_button_resets_the_grid_filter_model(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.filterModel") + self.assertEqual(fn(1), {}) + + +class TestScatterComponentWordContributionOption(unittest.TestCase): + """``offer_word_contribution`` gates the "Word contribution" color mode, not the whole panel.""" + + @staticmethod + def _explanation(): + return TestGlobalWordImportancePanel._explanation() + + def _color_options(self, offer_word_contribution: bool): + from shapash.webapp.nlp_components import ScatterComponent + + comp = ScatterComponent(np.zeros((4, 2)), offer_word_contribution=offer_word_contribution) + layout = comp.layout(self._explanation(), None) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + return [opt["value"] for opt in found["color-by"].options] + + def test_offered_when_word_importance_is_mounted(self): + self.assertIn("word_contribution", self._color_options(offer_word_contribution=True)) + + def test_omitted_without_word_importance(self): + # Scatter still works standalone (Prediction/Ground-Truth) — it just cannot source a class + # index for word-contribution coloring without Word Importance's control. + options = self._color_options(offer_word_contribution=False) + self.assertNotIn("word_contribution", options) + self.assertIn("prediction", options) + + class TestWordProfileControls(unittest.TestCase): """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" From c2bd8c578a001eabb56b9f52acf7b920f6845077 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 9 Sep 2026 15:59:09 +0200 Subject: [PATCH 101/144] refactor: change representation method and documentation --- shapash/compute/generators/base.py | 22 ++++++++++++++++++- shapash/compute/generators/cf_utils.py | 10 +++++++++ shapash/explainer/nlp_explainer.py | 9 ++++++++ .../webapp/nlp_components/counterfactual.py | 3 ++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/shapash/compute/generators/base.py b/shapash/compute/generators/base.py index 794fb2a7..c4027617 100644 --- a/shapash/compute/generators/base.py +++ b/shapash/compute/generators/base.py @@ -22,7 +22,7 @@ import numpy as np -from shapash.compute.generators.cf_utils import is_prediction_flip +from shapash.compute.generators.cf_utils import format_edit, is_prediction_flip from shapash.model.base import SupportsTokenization, TextModel #: Candidate texts scored per ``predict`` call when the model declares no batch size of its own. @@ -55,6 +55,12 @@ class Counterfactual: Probability of ``new_label`` on the counterfactual text. prob_delta : float Drop in the original label's probability (``orig_prob`` minus its probability on the CF). + + Examples + -------- + >>> import dataclasses, pandas as pd + >>> res = xpl.generate_counterfactuals(text, generator="ablation_flip") + >>> pd.DataFrame([dataclasses.asdict(cf) for cf in res]) """ original_text: str @@ -68,6 +74,20 @@ class Counterfactual: new_prob: float prob_delta: float + def __repr__(self) -> str: + """Concise, edit-first summary — the shape that matters when scanning results in a notebook. + + The default dataclass repr dumps every field (``tokens`` included), burying the one thing + worth reading at a glance under noise, and leaves a removal's edit (``old_token, ""``) to be + inferred from an empty string rather than stated. :func:`~.cf_utils.format_edit` spells out a + substitution (``old→new``) and a removal (``−old``) the same way here and in the webapp's + results table, so the two read consistently wherever a counterfactual is shown. + """ + edits = ", ".join(format_edit(old, new) for _, old, new in self.substitutions) or "no edits" + return ( + f'Counterfactual("{self.original_text}" → "{self.new_text}", {self.orig_label}→{self.new_label}, {edits})' + ) + @dataclass(frozen=True) class Field: diff --git a/shapash/compute/generators/cf_utils.py b/shapash/compute/generators/cf_utils.py index c2f693b8..88fbab74 100644 --- a/shapash/compute/generators/cf_utils.py +++ b/shapash/compute/generators/cf_utils.py @@ -44,6 +44,16 @@ def prediction_difference(orig_probs: np.ndarray, cf_probs: np.ndarray, class_id return float(orig_probs[class_idx] - cf_probs[class_idx]) +def format_edit(old_token: str, new_token: str) -> str: + """Render one token edit for display: a substitution as ``old→new``, a removal as ``−old``. + + An empty ``new_token`` means the position was dropped rather than replaced (how removal-based + generators such as AblationFlip express themselves in ``Counterfactual.substitutions``) — spelled + out here so a removal doesn't read as "replaced with nothing" but as what it is, a drop. + """ + return f"{old_token}→{new_token}" if new_token else f"−{old_token}" + + def display_form(model, token: str) -> str: """Return the user-facing spelling of ``token`` — markers stripped, lowercased. diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 7feef441..ac5514ca 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -92,6 +92,15 @@ class NlpExplainer: Pre-built backend instance. When provided, ``explainer_args`` and ``explainer_compute_args`` are ignored — configure the backend directly before passing it in. Defaults to ``NlpShapBackend``. + cf_generator : CounterfactualGenerator, optional + Counterfactual generator driving the webapp's "What-if Lab" panel and + :meth:`generate_counterfactuals` — finds the smallest text edit that flips the model's + prediction. When given, used verbatim as the sole (active) generator — no auto-discovery. + When ``None`` (default), every built-in generator compatible with the model — checked via + capabilities, e.g. gradient-based ``HotFlipGenerator`` needs ``SupportsGradients``, while + forward-pass-only ``AblationFlipGenerator`` works even on a prediction-only pipeline — is + auto-registered (see :meth:`available_cf_generators`), so the webapp can switch between + them live; the first one becomes the default/active generator (:attr:`cf_generator`). explainer_args : dict, optional Forwarded to ``NlpShapBackend.__init__`` when no ``backend`` is given. explainer_compute_args : dict, optional diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index 8697bebc..348d6ed5 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -15,6 +15,7 @@ from dash.exceptions import PreventUpdate from shapash.compute.generators.base import Field, IntField, TokenListField +from shapash.compute.generators.cf_utils import format_edit from shapash.webapp.nlp_components.base import CAP_COUNTERFACTUAL, CAP_PREDICT, WebappComponent from shapash.webapp.nlp_components.datapoint import datapoint_from_contributions @@ -215,7 +216,7 @@ def _results_table(cfs, component_id: str): ) rows = [] for i, cf in enumerate(cfs): - subs = ", ".join(f"{old}→{new}" for _, old, new in cf.substitutions) + subs = ", ".join(format_edit(old, new) for _, old, new in cf.substitutions) rows.append( html.Tr( [ From 153dfc93675286d6567d5cd2e07d0e091b3013ba Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 9 Sep 2026 16:00:40 +0200 Subject: [PATCH 102/144] refactor: add kill app --- shapash/explainer/nlp_explainer.py | 13 +++- shapash/webapp/nlp_app.py | 35 ++++++++++- shapash/webapp/utils/launch.py | 64 ++++++++++++++++++++ tests/unit_tests/webapp/utils/test_launch.py | 29 +++++++++ 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 shapash/webapp/utils/launch.py create mode 100644 tests/unit_tests/webapp/utils/test_launch.py diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index ac5514ca..344af868 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -43,6 +43,7 @@ from shapash.model.base import SupportsEmbeddings, SupportsTokenization, TextModel, has_capabilities from shapash.model.hf import HFPipelineModel from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.utils.launch import RunningApp # Built-in counterfactual generators, in preference order: HotFlip (gradient-based, richer # substitutions) first, AblationFlip (forward-pass-only removal) as the broader fallback. Every entry @@ -515,7 +516,7 @@ def run_app( palette_name: str = "default", colors_dict: dict[str, str] | None = None, info: dict[str, str] | None = None, - ) -> None: + ) -> RunningApp | None: """Launch the NLP explanation webapp for ``explanation``. The webapp reads ``explanation`` directly — it never writes to it, so the artifact it renders @@ -563,12 +564,20 @@ def run_app( reference corpus — this adds "Train samples" from it. Pass *info* for anything neither source can know — e.g. ``{"Dataset": "dair-ai/emotion"}`` for a caller-supplied dataset label — or to override a value from either. + + Returns + ------- + RunningApp or None + When ``debug`` is ``False`` (the default), the app runs on a background thread and + this call returns immediately with a handle to it — call ``.kill()`` on it (or use it + as a context manager) to stop the app. Returns ``None`` when ``debug=True``, since + Dash's own dev server blocks in that mode instead — see :meth:`~shapash.webapp.nlp_app.NlpWebApp.run`. """ # fit() is the one place that knows the reference corpus, so its size is added here rather # than living on `explanation` (which is model-free and never sees the training set at all). reference = getattr(self, "reference_", None) engine_info = {"Train samples": str(len(reference[0]))} if reference is not None else {} - NlpWebApp( + return NlpWebApp( explanation, engine=self, scatter_xy=scatter_xy, diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index d8da1f70..5fd2259d 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -9,6 +9,8 @@ from __future__ import annotations +import logging + import dash import dash_ag_grid as dag import dash_bootstrap_components as dbc @@ -36,6 +38,7 @@ error_positions, pack_datapoint, ) +from shapash.webapp.utils.launch import RunningApp, run_in_background _APPLY_STORE = "whatif-apply-store" _CURRENT_STORE = "current-datapoint" @@ -917,6 +920,32 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): # Public # ------------------------------------------------------------------ - def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> None: - """Launch the Dash development server.""" - self.app.run(port=port, debug=debug, host=host) + def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> RunningApp | None: + """Launch the Dash server. + + Parameters + ---------- + port : int + Port for the server. + debug : bool + Enable Dash debug mode (hot reload, error overlay). Debug mode uses Dash's own + blocking dev server and reloader, so it cannot be handed back as a killable handle — + stop it by interrupting the kernel/process. Defaults to ``False``. + host : str + Host to bind the server to. + + Returns + ------- + RunningApp or None + When ``debug`` is ``False`` (the default), the server runs on a background thread and + this call returns immediately with a handle to it — call ``.kill()`` on it (or use it + as a context manager) to stop the server. Returns ``None`` when ``debug=True``, since + that path blocks instead. + """ + if debug: + self.app.run(port=port, debug=debug, host=host) + return None + running_app = run_in_background(self.app.server, host, port) + logging.info(f"Your Shapash application run on {running_app.url}") + logging.info("Use the method .kill() to stop your app.") + return running_app diff --git a/shapash/webapp/utils/launch.py b/shapash/webapp/utils/launch.py new file mode 100644 index 00000000..0ed098fd --- /dev/null +++ b/shapash/webapp/utils/launch.py @@ -0,0 +1,64 @@ +"""Serve a WSGI app in a background thread, stoppable from the caller. + +Mirrors how Dash's own Jupyter integration launches apps (``dash._jupyter.JupyterDash.run_app``): +a daemon thread running ``server.serve_forever()``, stopped via ``server.shutdown()`` — the +cooperative shutdown built into ``socketserver.BaseServer``. No thread-killing trickery is needed +since ``serve_forever``/``shutdown`` are already a clean, designed-for-this pair. +""" + +from __future__ import annotations + +import threading + +from werkzeug.serving import BaseWSGIServer, make_server + + +class RunningApp: + """A WSGI app running on a background thread; call ``.kill()`` to stop it.""" + + def __init__(self, server: BaseWSGIServer, thread: threading.Thread, url: str): + self._server = server + self._thread = thread + self.url = url + + def kill(self) -> None: + """Stop the server and wait for its thread to exit.""" + self._server.shutdown() + self._thread.join() + + def is_alive(self) -> bool: + """Whether the server thread is still running.""" + return self._thread.is_alive() + + def __enter__(self) -> RunningApp: + return self + + def __exit__(self, *exc_info) -> None: + self.kill() + + def __repr__(self) -> str: + return f"<RunningApp url={self.url!r} running={self.is_alive()}>" + + +def run_in_background(wsgi_app, host: str, port: int) -> RunningApp: + """Serve ``wsgi_app`` on a background daemon thread and return a killable handle. + + Parameters + ---------- + wsgi_app : WSGI application + The application to serve (e.g. a Flask app, or ``dash.Dash(...).server``). + host : str + Host to bind the server to. + port : int + Port to bind the server to. + + Returns + ------- + RunningApp + Already-started server handle; call ``.kill()`` on it (or use it as a context manager) + to stop the server. + """ + server = make_server(host, port, wsgi_app) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + return RunningApp(server, thread, url=f"http://{host}:{server.server_port}/") diff --git a/tests/unit_tests/webapp/utils/test_launch.py b/tests/unit_tests/webapp/utils/test_launch.py new file mode 100644 index 00000000..417e210e --- /dev/null +++ b/tests/unit_tests/webapp/utils/test_launch.py @@ -0,0 +1,29 @@ +"""Unit tests for ``run_in_background`` (serve a WSGI app on a killable background thread).""" + +import unittest +import urllib.request + +from shapash.webapp.utils.launch import RunningApp, run_in_background + + +def _wsgi_app(environ, start_response): + start_response("200 OK", [("Content-Type", "text/plain")]) + return [b"ok"] + + +class TestRunInBackground(unittest.TestCase): + def test_serves_and_returns_killable_handle(self): + app = run_in_background(_wsgi_app, "127.0.0.1", 0) + try: + self.assertIsInstance(app, RunningApp) + self.assertTrue(app.is_alive()) + with urllib.request.urlopen(app.url, timeout=5) as resp: + self.assertEqual(resp.read(), b"ok") + finally: + app.kill() + self.assertFalse(app.is_alive()) + + def test_context_manager_kills_on_exit(self): + with run_in_background(_wsgi_app, "127.0.0.1", 0) as app: + self.assertTrue(app.is_alive()) + self.assertFalse(app.is_alive()) From 2ee4ee4d114d5614174b37794c4fc568d05c261e Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 9 Sep 2026 16:01:49 +0200 Subject: [PATCH 103/144] refactor: modify sentence plot and confusion matrix --- shapash/explainer/nlp_plotter.py | 20 +++- shapash/plots/plot_confusion_matrix.py | 10 +- shapash/webapp/utils/dash_to_html.py | 99 +++++++++++++++++++ tests/unit_tests/nlp/test_nlp_plotter.py | 7 ++ .../webapp/utils/test_dash_to_html.py | 71 +++++++++++++ 5 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 shapash/webapp/utils/dash_to_html.py create mode 100644 tests/unit_tests/webapp/utils/test_dash_to_html.py diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index ceb7020d..0123f46f 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -35,6 +35,7 @@ from shapash.plots.plot_waterfall import plot_waterfall from shapash.plots.plot_word_importance import plot_word_importance, word_importance_axis_title from shapash.plots.plot_word_profile import plot_word_profile +from shapash.webapp.utils.dash_to_html import DashHtmlPreview if TYPE_CHECKING: from shapash.explainer.nlp_explanation import NlpExplanation @@ -202,7 +203,7 @@ def waterfall( width=width, ) - def sentence(self, row: int = 0, label_idx: int = 0) -> html.Div: + def sentence(self, row: int = 0, label_idx: int = 0, notebook: bool = False) -> html.Div | DashHtmlPreview: """One sample rendered inline, each token background-shaded by its contribution. Parameters @@ -211,16 +212,25 @@ def sentence(self, row: int = 0, label_idx: int = 0) -> html.Div: Positional index of the sample (see :meth:`tokens`). label_idx : int Index of the class to display, in ``label_names`` order. + notebook : bool + ``False`` (default) returns the raw Dash component — directly usable as ``children`` + in your own Dash layout, the same way :meth:`tokens`/:meth:`waterfall` return a + ``go.Figure`` directly usable in ``dcc.Graph(figure=...)``. ``True`` wraps it in a + :class:`~shapash.webapp.utils.dash_to_html.DashHtmlPreview`, so leaving the call as + the last expression in a notebook cell (or ``display()``-ing it) renders it as static + HTML — a Dash component's own ``str()``/``repr()`` is Python source, not markup, so + ``IPython.display.HTML(...)`` on the raw component would just show that source as text. Returns ------- - dash.html.Div - A Dash component. In a notebook, display it inside a - ``jupyter_dash``/``dash`` app, or use :meth:`tokens` for a standalone figure. + dash.html.Div or DashHtmlPreview + A Dash component (``notebook=False``), or that same component wrapped for notebook + display (``notebook=True`` — the original is still reachable as ``.component``). """ label_idx = self._check_label_idx(label_idx) toks, values, base_value = self._slice(row, label_idx) - return plot_sentence_highlight(tokens=toks, values=values, base_value=base_value) + div = plot_sentence_highlight(tokens=toks, values=values, base_value=base_value) + return DashHtmlPreview(div) if notebook else div # ── batch-level plots ─────────────────────────────────────────────────────────────── def word_importance( diff --git a/shapash/plots/plot_confusion_matrix.py b/shapash/plots/plot_confusion_matrix.py index abedd1fc..815fdeac 100644 --- a/shapash/plots/plot_confusion_matrix.py +++ b/shapash/plots/plot_confusion_matrix.py @@ -82,11 +82,13 @@ def plot_confusion_matrix( ) size = max(360, 90 * n + 120) - # The x-axis sits on top, so keep room for its labels even without a title. - top_margin = 80 if title else 40 + # The x-axis sits on top, so keep room for its labels even without a title. With a title, + # pin it above that axis title (rather than at Plotly's default position, which drifts down + # into the same band) and give the axis title a standoff so the two never share a line. + top_margin = 115 if title else 40 fig.update_layout( - title=dict(text=title, x=0.5), - xaxis_title="Predicted class", + title=dict(text=title, x=0.5, y=0.98, yanchor="top"), + xaxis_title=dict(text="Predicted class", standoff=25), yaxis_title="True class", width=width or size, height=height or size, diff --git a/shapash/webapp/utils/dash_to_html.py b/shapash/webapp/utils/dash_to_html.py new file mode 100644 index 00000000..e89ee811 --- /dev/null +++ b/shapash/webapp/utils/dash_to_html.py @@ -0,0 +1,99 @@ +"""Render a Dash component tree to static HTML — for viewing Dash markup outside a running app. + +A Dash component's ``str()``/``repr()`` is Python source (``Div([Span(...)])``), not markup: Dash +never turns components into HTML in Python — the JS renderer builds the DOM straight from that +Python tree over the wire. ``IPython.display.HTML(component)`` therefore just shows that repr as +literal text. :func:`dash_component_to_html` walks the same tree and emits actual markup instead, +and :class:`DashHtmlPreview` wraps it with a ``_repr_html_`` so Jupyter renders it automatically. +""" + +from __future__ import annotations + +import re +from html import escape +from typing import Any + +_CAMEL_TO_KEBAB = re.compile(r"(?<!^)(?=[A-Z])") + + +def _style_to_css(style: dict) -> str: + return ";".join(f"{_CAMEL_TO_KEBAB.sub('-', k).lower()}:{v}" for k, v in style.items()) + + +def dash_component_to_html(component: Any) -> str: + """Recursively render a Dash ``html.*`` component tree to an HTML string. + + Handles the plain attributes shapash's own components use: ``style`` (a dict), ``title``, + ``className``. It does not handle interactive components (``dcc.Graph``, ``dcc.Dropdown``, + callbacks...) — those have no meaning outside a running Dash app. + + Parameters + ---------- + component : Any + A Dash ``html.*`` component, a list of them, or a plain string/number (as found in + ``children``). + + Returns + ------- + str + HTML markup equivalent to what Dash's JS renderer would mount for this tree. + """ + if component is None: + return "" + if isinstance(component, (list, tuple)): + return "".join(dash_component_to_html(c) for c in component) + if isinstance(component, (str, int, float)): + return escape(str(component)) + + tag = type(component).__name__.lower() # Div -> div, Span -> span, ... + attrs = [] + style = getattr(component, "style", None) + if style: + attrs.append(f'style="{_style_to_css(style)}"') + title = getattr(component, "title", None) + if title: + attrs.append(f'title="{escape(str(title))}"') + class_name = getattr(component, "className", None) + if class_name: + attrs.append(f'class="{escape(str(class_name))}"') + attr_str = "" if not attrs else " " + " ".join(attrs) + + inner = dash_component_to_html(getattr(component, "children", None)) + return f"<{tag}{attr_str}>{inner}</{tag}>" + + +class DashHtmlPreview: + """Wraps a Dash component so Jupyter renders it as static HTML. + + ``explanation.plot.sentence(0)`` returns one of these: leave it as the last expression in a + notebook cell (or ``display()`` it) and Jupyter calls ``_repr_html_`` automatically. The + underlying Dash component — e.g. to embed in your own app's ``layout`` — is available as + ``.component``. + + Panel components like ``plot_sentence_highlight`` are block-level ``<div>``s with no width of + their own, because inside the webapp they should fill their panel. Outside one — e.g. a + notebook's full output width — that same div stretches edge to edge regardless of how short + the text is, leaving a wide slab of background colour next to it. This wraps the markup in an + ``inline-block`` container so it hugs its content's width instead, capped at ``max_width`` so a + genuinely long one still wraps within the cell rather than overflowing it. + + These components also never set their own default text colour on their light backgrounds — + they rely on the *ambient* page text colour, which is safe inside the webapp because its + Bootstrap page always supplies a light background with dark default text, but not in a + notebook: a dark IDE theme (e.g. VS Code's default dark Jupyter theme) supplies a light-grey + default text colour meant for a dark background, which is nearly invisible against these pale + spans. This wrapper pins an explicit light background and dark text colour so the component + renders the same regardless of the surrounding theme. + """ + + def __init__(self, component: Any, max_width: str = "100%") -> None: + self.component = component + self.max_width = max_width + + def _repr_html_(self) -> str: + inner = dash_component_to_html(self.component) + style = f"display:inline-block;max-width:{self.max_width};background-color:#ffffff;color:#212529" + return f'<div style="{style}">{inner}</div>' + + def __repr__(self) -> str: + return repr(self.component) diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py index 1fb4944c..b7479b25 100644 --- a/tests/unit_tests/nlp/test_nlp_plotter.py +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -18,6 +18,7 @@ from shapash.explainer.nlp_explanation import NlpExplanation from shapash.explainer.nlp_plotter import NlpPlotter +from shapash.webapp.utils.dash_to_html import DashHtmlPreview def _make_explanation( @@ -101,6 +102,12 @@ def test_waterfall_returns_a_figure(self): def test_sentence_returns_a_dash_component(self): self.assertIsInstance(self.explanation.plot.sentence(row=0, label_idx=1), html.Div) + def test_sentence_notebook_true_returns_a_previewable_wrapper(self): + preview = self.explanation.plot.sentence(row=0, label_idx=1, notebook=True) + self.assertIsInstance(preview, DashHtmlPreview) + self.assertIsInstance(preview.component, html.Div) + self.assertIn("<div", preview._repr_html_()) + def test_negative_row_counts_from_the_end(self): last = self.explanation.plot.tokens(row=-1, label_idx=0) self.assertEqual(list(last.data[0].y), ["ok"]) diff --git a/tests/unit_tests/webapp/utils/test_dash_to_html.py b/tests/unit_tests/webapp/utils/test_dash_to_html.py new file mode 100644 index 00000000..7c933061 --- /dev/null +++ b/tests/unit_tests/webapp/utils/test_dash_to_html.py @@ -0,0 +1,71 @@ +import unittest + +from dash import html + +from shapash.webapp.utils.dash_to_html import DashHtmlPreview, dash_component_to_html + + +class TestDashComponentToHtml(unittest.TestCase): + def test_renders_tag_and_text_child(self): + self.assertEqual(dash_component_to_html(html.Div("hello")), "<div>hello</div>") + + def test_renders_nested_children_in_order(self): + component = html.Div([html.Span("a"), html.Span("b")]) + self.assertEqual(dash_component_to_html(component), "<div><span>a</span><span>b</span></div>") + + def test_renders_style_dict_as_kebab_case_css(self): + component = html.Span("x", style={"backgroundColor": "rgb(1,2,3)", "fontSize": "1em"}) + result = dash_component_to_html(component) + self.assertIn('style="background-color:rgb(1,2,3);font-size:1em"', result) + + def test_renders_title_and_class_name(self): + component = html.Span("x", title="tip", className="fw-bold") + result = dash_component_to_html(component) + self.assertIn('title="tip"', result) + self.assertIn('class="fw-bold"', result) + + def test_escapes_text_content_and_attribute_values(self): + component = html.Span("<script>", title='say "hi"') + result = dash_component_to_html(component) + self.assertNotIn("<script>", result) + self.assertIn("&lt;script&gt;", result) + self.assertIn("&quot;hi&quot;", result) + + def test_none_children_renders_as_empty(self): + self.assertEqual(dash_component_to_html(html.Div()), "<div></div>") + + +class TestDashHtmlPreview(unittest.TestCase): + def test_repr_html_wraps_the_component_markup_in_a_shrink_to_fit_container(self): + component = html.Div([html.Span("hi")]) + preview = DashHtmlPreview(component) + rendered = preview._repr_html_() + self.assertIn(dash_component_to_html(component), rendered) + self.assertIn("display:inline-block", rendered) + self.assertIn("max-width:100%", rendered) + + def test_repr_html_pins_an_explicit_light_background_and_dark_text(self): + """Ambient-inherited text colour is invisible against these panels in a dark IDE theme.""" + preview = DashHtmlPreview(html.Div("hi")) + rendered = preview._repr_html_() + self.assertIn("background-color:#ffffff", rendered) + self.assertIn("color:#212529", rendered) + + def test_max_width_is_customizable(self): + component = html.Div("hi") + preview = DashHtmlPreview(component, max_width="500px") + self.assertIn("max-width:500px", preview._repr_html_()) + + def test_component_attribute_exposes_the_underlying_dash_component(self): + component = html.Div("hi") + preview = DashHtmlPreview(component) + self.assertIs(preview.component, component) + + def test_repr_stays_the_dash_component_repr(self): + component = html.Div("hi") + preview = DashHtmlPreview(component) + self.assertEqual(repr(preview), repr(component)) + + +if __name__ == "__main__": + unittest.main() From 9c9edc7c4165ba471da6d2217e54108fef58aed6 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 9 Sep 2026 16:02:44 +0200 Subject: [PATCH 104/144] test: add nlp webapp run test --- tests/unit_tests/nlp/test_nlp_prototype.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index fe683a1d..063ed311 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -42,6 +42,7 @@ from shapash.webapp.nlp_components import compose_selection as _compose_selection from shapash.webapp.nlp_components import error_positions from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click +from shapash.webapp.utils.launch import RunningApp LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] N_CLASSES = len(LABEL_NAMES) @@ -1153,6 +1154,39 @@ def _collect_ids(self, node) -> set: return ids +class TestNlpWebAppRun(unittest.TestCase): + """``NlpWebApp.run`` / ``NlpExplainer.run_app`` — killable background server.""" + + def setUp(self): + self.xpl = _make_explainer() + self.explanation = _make_explanation() + + def test_run_returns_killable_app_by_default(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl) + app = webapp.run(port=0) + try: + self.assertIsInstance(app, RunningApp) + self.assertTrue(app.is_alive()) + finally: + app.kill() + self.assertFalse(app.is_alive()) + + def test_run_app_returns_killable_app(self): + app = self.xpl.run_app(self.explanation, port=0) + try: + self.assertIsInstance(app, RunningApp) + self.assertTrue(app.is_alive()) + finally: + app.kill() + + def test_debug_mode_blocks_and_returns_none(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl) + with patch.object(webapp.app, "run") as mock_run: + result = webapp.run(port=0, debug=True) + mock_run.assert_called_once_with(port=0, debug=True, host="127.0.0.1") + self.assertIsNone(result) + + class TestComposeSelection(unittest.TestCase): """The three sample filters (scatter box, confusion cell, errors-only) must intersect.""" From 8e081d520f6394c03c02414bc4d92af94b32c213 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 9 Sep 2026 17:34:03 +0200 Subject: [PATCH 105/144] refactor: extract scatter plot Move ScatterComponent's inline go.Figure construction into a pure plot_scatter() function, matching the split every other NLP panel already follows (plot_word_importance, plot_word_profile, etc.), and expose it as NlpExplanation.plot.scatter() for notebook/script use without the Dash app. Also adds word_contributions_by_sample() to reuse word_occurrences() instead of a hand-rolled token loop, and fixes NlpWebApp's stale scatter_xy docstring pointing at NlpExplainer.compute_projection(). --- shapash/explainer/nlp_explainer.py | 44 ++-- shapash/explainer/nlp_explanation.py | 37 +++ shapash/explainer/nlp_plotter.py | 81 ++++++ shapash/plots/plot_scatter.py | 235 ++++++++++++++++++ shapash/webapp/nlp_app.py | 10 +- shapash/webapp/nlp_components/scatter.py | 177 ++----------- tests/unit_tests/nlp/test_nlp_explanation.py | 4 +- tests/unit_tests/nlp/test_nlp_plotter.py | 48 ++++ tests/unit_tests/nlp/test_nlp_prototype.py | 105 +++++++- .../unit_tests/webapp/test_nlp_whatif_app.py | 39 +++ 10 files changed, 602 insertions(+), 178 deletions(-) create mode 100644 shapash/plots/plot_scatter.py diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 344af868..872f3362 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -20,7 +20,7 @@ import threading from dataclasses import replace from pathlib import Path -from typing import cast +from typing import Any, cast import numpy as np import pandas as pd @@ -60,19 +60,25 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.xpl" -def _reducer_tag(reducer: object) -> str: - """Name a reducer for a cache tag: its class plus a digest of its parameters when it exposes them. +def _reducer_tag(reducer: object, fit_transform_kwargs: dict[str, Any] | None = None) -> str: + """Name a reducer for a cache tag: its class plus a digest of its settings. Two runs of the same reducer class with different settings produce different coordinates, so the - class name alone would silently reload the wrong scatter. ``get_params()`` (sklearn's convention, - which ``pacmap`` also follows) makes the settings part of the tag. A reducer without it is keyed by - class name only — documented on :meth:`NlpExplainer.compute_projection` as needing ``recompute``. + class name alone would silently reload the wrong scatter. "Settings" covers both the reducer's own + constructor parameters — read via ``get_params()`` (sklearn's convention, which ``pacmap`` also + follows) — and any ``fit_transform_kwargs`` a caller passed at call time (e.g. PaCMAP's + ``init="pca"``), since those affect the output identically but live outside ``get_params()``. A + reducer with no ``get_params()`` and no ``fit_transform_kwargs`` is keyed by class name only — + documented on :meth:`NlpExplainer.compute_projection` as needing ``recompute``. """ name = type(reducer).__name__.lower() get_params = getattr(reducer, "get_params", None) - if get_params is None: + if get_params is None and not fit_transform_kwargs: return name - digest = hashlib.md5(repr(sorted(get_params().items())).encode(), usedforsecurity=False).hexdigest() + params = dict(get_params()) if get_params is not None else {} + if fit_transform_kwargs: + params["__fit_transform_kwargs__"] = sorted(fit_transform_kwargs.items()) + digest = hashlib.md5(repr(sorted(params.items())).encode(), usedforsecurity=False).hexdigest() return f"{name}-{digest[:8]}" @@ -442,6 +448,7 @@ def compute_projection( reducer=None, cache_dir: str | Path | None = None, recompute: bool = False, + **fit_transform_kwargs: Any, ): """Return a 2-D projection of ``explanation``'s texts, ready to pass to :meth:`run_app`. @@ -462,9 +469,15 @@ def compute_projection( cache_dir : str or Path, optional When given, both the embeddings and the projected coordinates are persisted here and reloaded on later runs, so only the first call pays the cost. The key covers the model, the - effective space, the texts, and the reducer's class + parameters. + effective space, the texts, and the reducer's class + parameters (including + ``fit_transform_kwargs`` below). recompute : bool, optional Drop this text set's cached artifacts first, forcing a fresh embed + fit. + **fit_transform_kwargs + Forwarded to ``reducer.fit_transform(vectors, **fit_transform_kwargs)`` — for arguments a + reducer only accepts at call time rather than at construction, e.g. PaCMAP's + ``init="pca"``. Part of the cache key (see Notes), so changing them busts the cache the + same way changing the reducer's own constructor parameters does. Returns ------- @@ -474,13 +487,16 @@ def compute_projection( Notes ----- The reducer's contribution to the cache key is its class name plus a digest of ``get_params()`` - when it exposes one (sklearn and pacmap both do). A reducer with neither is keyed by class name - alone, so re-tuning such a reducer needs ``recompute=True`` to take effect. + (when it exposes one — sklearn and pacmap both do) and of ``fit_transform_kwargs``. A reducer + with neither is keyed by class name alone, so re-tuning such a reducer needs ``recompute=True`` + to take effect. Examples -------- >>> explanation = xpl.explain(texts) - >>> xy = xpl.compute_projection(explanation, reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/") + >>> xy = xpl.compute_projection( + ... explanation, reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/", init="pca" + ... ) >>> xpl.run_app(explanation, scatter_xy=xy) """ text_model = self._require_text_model() @@ -501,8 +517,8 @@ def compute_projection( store.clear() with self._compute_guard(): # embed() tokenizes — serialize against the live compute ops return store.cached_array( - f"{_reducer_tag(reducer)}.proj", - lambda: np.asarray(reducer.fit_transform(store.vectors())), + f"{_reducer_tag(reducer, fit_transform_kwargs)}.proj", + lambda: np.asarray(reducer.fit_transform(store.vectors(), **fit_transform_kwargs)), ) def run_app( diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index 6c0421cd..2fdef621 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -17,6 +17,7 @@ import json import re import zipfile +from collections.abc import Sequence from dataclasses import dataclass, fields, replace from datetime import UTC, datetime from pathlib import Path @@ -128,6 +129,42 @@ def aggregate_word_contributions(occurrences: pd.DataFrame, agg: str = "mean") - return out +def word_contributions_by_sample(explanation: NlpExplanation, words: Sequence[str], label_idx: int) -> np.ndarray: + """Per-sample sum of one class's contribution from every occurrence of any of ``words``. + + The per-sample counterpart of :func:`aggregate_word_contributions`: that one collapses a + word's occurrences into one number per class, this collapses one class's occurrences into one + number per sample — the shape the scatter panel colors points by + (:func:`~shapash.plots.plot_scatter.plot_scatter`'s ``contributions``). Goes through + :meth:`NlpExplanation.word_occurrences` for each word, so the match honors the same + tokenizer-derived case-folding as :meth:`NlpExplanation.word_importance`/:meth:`word_profile`. + + Parameters + ---------- + explanation : NlpExplanation + The artifact to read occurrences from. + words : Sequence[str] + Words to sum together — a sample contributing via more than one of them is counted once + per matching word, same as summing each word's own per-sample series. + label_idx : int + Class whose contribution to keep. + + Returns + ------- + np.ndarray, shape (n_samples,) + ``0.0`` for a sample none of ``words`` occur in. + """ + result = np.zeros(explanation.n_samples) + for word in words: + occurrences = explanation.word_occurrences(word) + if occurrences.empty: + continue + matched = occurrences[occurrences["class_idx"] == label_idx] + by_sample = matched.groupby("sample")["contribution"].sum() + result[by_sample.index.to_numpy()] += by_sample.to_numpy() + return result + + def rank_word_samples( occurrences: pd.DataFrame, class_idx: int, diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index 0123f46f..f7dd2cd5 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -28,8 +28,10 @@ WORD_AGGREGATIONS, aggregate_word_contributions, select_label_column, + word_contributions_by_sample, ) from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.plots.plot_scatter import plot_scatter from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall @@ -412,3 +414,82 @@ def confusion( cm = exp.confusion_matrix() labels = list(exp.label_to_idx) return plot_confusion_matrix(cm, labels, normalize=normalize, title=title, width=width, height=height) + + def scatter( + self, + xy: np.ndarray, + color_by: str = "prediction", + words: list[str] | None = None, + label_idx: int = 0, + errors_only: bool = False, + use_webgl: bool = False, + ) -> go.Figure: + """2-D scatter of the batch (e.g. an embedding projection), colored by class or by a word. + + ``xy`` is not something the artifact carries — see :meth:`NlpExplainer.compute_projection` + for how to produce it (defaults to PCA on the model's embeddings, no extra install since + sklearn is a core dependency) — every other argument here comes straight from ``self._exp``, + the same split :class:`~shapash.webapp.nlp_components.scatter.ScatterComponent` uses to + build the identical figure inside the What-if Lab. + + Parameters + ---------- + xy : np.ndarray, shape (n_samples, 2) + 2-D coordinates aligned with the explanation's samples. + color_by : {"prediction", "ground_truth", "word_contribution"} + ``"ground_truth"`` falls back to predictions when ``y_true`` is unavailable; + ``"word_contribution"`` falls back to predictions when ``words`` is empty. + words : list[str], optional + With ``color_by="word_contribution"``, sum these words' contributions to + ``label_idx`` per sample and color by that (diverging scale; gray where none occur). + label_idx : int + Class whose contribution to color by, in ``label_names`` order. Only used with + ``color_by="word_contribution"``. + errors_only : bool + Emphasize misclassified points (larger, opaque) and shadow the rest, without changing + their color. Needs ``y_true``; ignored otherwise. + use_webgl : bool + Draw with WebGL (``Scattergl``) rather than plain SVG (``Scatter``). Defaults to + ``False`` here since some notebook front-ends (e.g. VSCode's notebook renderer over + certain remote/SSH or sandboxed setups) can't get a WebGL context; pass ``True`` for + smoother interaction on large batches when your renderer supports it — the Dash What-if + Lab's identical figure keeps WebGL on by default. + + Returns + ------- + plotly.graph_objs.Figure + + Examples + -------- + >>> xy = xpl.compute_projection(explanation, cache_dir="cache/") + >>> explanation.plot.scatter(xy, color_by="word_contribution", words=["terrible"]).show() + """ + exp = self._exp + err_mask = None + if errors_only and exp.y_true is not None and exp.y_pred is not None: + err_mask = np.asarray(exp.y_true).astype(str) != np.asarray(exp.y_pred).astype(str) + + if color_by == "word_contribution" and words: + label_idx = self._check_label_idx(label_idx) + contributions = word_contributions_by_sample(exp, words, label_idx) + colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" + return plot_scatter( + xy, + exp.texts, + contributions=contributions, + colorbar_title=colorbar_title, + error_mask=err_mask, + use_webgl=use_webgl, + ) + + if color_by == "ground_truth" and exp.y_true is not None: + labels = [str(label) for label in exp.y_true.tolist()] + elif exp.y_pred is not None: + labels = [str(label) for label in exp.y_pred.tolist()] + else: + labels = [""] * exp.n_samples + + label_names = exp.label_names or sorted(set(labels)) + return plot_scatter( + xy, exp.texts, labels=labels, label_names=label_names, error_mask=err_mask, use_webgl=use_webgl + ) diff --git a/shapash/plots/plot_scatter.py b/shapash/plots/plot_scatter.py new file mode 100644 index 00000000..4030ac9c --- /dev/null +++ b/shapash/plots/plot_scatter.py @@ -0,0 +1,235 @@ +"""2-D scatter of samples (e.g. embeddings projected to a plane), for the NLP explanation. + +Coloring has two independent modes that do not share a colorbar/legend, hence the two branches +below rather than one code path parameterized by color: *categorical* groups points into one +trace per class (predicted or ground truth) so Plotly's legend and box/lasso selection work +correctly across every trace, while *word contribution* is a single diverging-colorscale overlay +plus a gray "absent" layer, since the quantity being colored (a signed SHAP sum) has nothing to do +with class membership. +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np +import plotly.graph_objs as go + +# Qualitative palette for the categorical mode — one color per class, cycling if there are more +# classes than colors. Independent of the signed positive/negative theme in +# ``shapash.style.style_utils`` (``DEFAULT_NLP_THEME.xpl_positive``/``xpl_negative``), which colors a +# *contribution*, not a *class*. +DEFAULT_CATEGORICAL_PALETTE: tuple[str, ...] = ( + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +) + +_LAYOUT_COMMON = dict( + dragmode="select", + uirevision="scatter", + xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), + plot_bgcolor="#f9f9f9", + paper_bgcolor="white", + margin=dict(l=10, r=10, t=10, b=10), + autosize=True, +) + + +def _emphasize_errors( + idx_arr: np.ndarray, + base_opacity: float, + base_size: float, + error_mask: np.ndarray | None, +) -> tuple[list[float] | float, list[float] | float]: + """Per-point opacity/size that pops model errors and shadows everything else. + + Keeps the caller's coloring untouched — only opacity and marker size change — so points stay + grouped/colored however the categorical/word-contribution branch already draws them. Returns + scalars (the unmodified base values) when ``error_mask`` is unavailable. + """ + if error_mask is None or len(idx_arr) == 0: + return base_opacity, base_size + is_error = error_mask[idx_arr] + opacity = np.where(is_error, max(base_opacity, 0.9), 0.12).tolist() + size = np.where(is_error, base_size + 3, max(base_size - 2, 3)).tolist() + return opacity, size + + +def _short_texts(texts: Sequence[str], max_len: int = 120) -> list[str]: + return [(t[:max_len] + "…") if len(t) > max_len else t for t in texts] + + +def plot_scatter( + xy: np.ndarray, + texts: Sequence[str], + labels: Sequence[str] | None = None, + label_names: Sequence[str] | None = None, + contributions: np.ndarray | None = None, + colorbar_title: str | None = None, + error_mask: np.ndarray | None = None, + palette: Sequence[str] = DEFAULT_CATEGORICAL_PALETTE, + use_webgl: bool = True, +) -> go.Figure: + """2-D scatter, colored either by class or by a word's SHAP contribution. + + Exactly one of ``contributions`` or ``labels`` decides the mode. ``customdata`` on every trace + always carries the point's original position in ``xy``/``texts``, so a caller wiring + ``clickData``/``selectedData`` back to sample indices does not need to know which mode drew + the figure. + + Parameters + ---------- + xy : np.ndarray, shape (n_samples, 2) + 2-D coordinates (e.g. a UMAP/PaCMAP/PCA projection of embeddings). + texts : Sequence[str] + One text per sample, shown (truncated to 120 chars) in the hover. + labels : Sequence[str], optional + Categorical mode: one class label per sample (e.g. predictions or ground truth). + Mutually exclusive with ``contributions``. + label_names : Sequence[str], optional + Categorical mode: the categories to draw, in legend order — not necessarily + ``set(labels)``, so a caller can pass a fixed class ordering (and colors stay stable) even + when a class has zero points in this batch. Required when ``labels`` is given. + contributions : np.ndarray, shape (n_samples,), optional + Word-contribution mode: signed SHAP sum per sample. Points with ``0`` are drawn as a gray + "absent" context layer; the rest as a diverging-colorscale overlay. Mutually exclusive + with ``labels``. + colorbar_title : str, optional + Word-contribution mode: label for the colorbar (e.g. the word(s) being colored by). + error_mask : np.ndarray of bool, shape (n_samples,), optional + ``True`` where the model got the sample wrong. When given, error points are drawn larger + and opaque while the rest are shrunk and faded — coloring is untouched either way. + palette : Sequence[str] + Categorical mode: colors assigned to ``label_names`` in order, cycling if there are more + classes than colors. Defaults to :data:`DEFAULT_CATEGORICAL_PALETTE`. + use_webgl : bool + Draw with ``Scattergl`` (default) for smooth interaction up to several thousand points, as + the Dash webapp needs. Some notebook front-ends (e.g. VSCode's notebook renderer over + certain remote/SSH or sandboxed setups) can't get a WebGL context and raise "WebGL is not + supported by your browser" — pass ``False`` there to fall back to the plain SVG ``Scatter``. + + Returns + ------- + go.Figure + + Raises + ------ + ValueError + If neither or both of ``contributions``/``labels`` are given, or ``labels`` is given + without ``label_names``. + """ + if (contributions is None) == (labels is None): + raise ValueError("Pass exactly one of `contributions` (word mode) or `labels` (categorical mode).") + + texts_short = _short_texts(texts) + scatter_cls = go.Scattergl if use_webgl else go.Scatter + + if contributions is not None: + return _plot_word_contribution(xy, texts_short, contributions, colorbar_title, error_mask, scatter_cls) + if labels is None or label_names is None: + raise ValueError("`label_names` is required alongside `labels`.") + return _plot_categorical(xy, texts_short, list(labels), list(label_names), error_mask, palette, scatter_cls) + + +def _plot_word_contribution( + xy: np.ndarray, + texts_short: list[str], + contributions: np.ndarray, + colorbar_title: str | None, + error_mask: np.ndarray | None, + scatter_cls: type[go.Scatter] | type[go.Scattergl], +) -> go.Figure: + max_abs = float(np.abs(contributions).max()) or 1.0 + present_mask = np.where(contributions != 0.0)[0] + absent_mask = np.where(contributions == 0.0)[0] + + fig = go.Figure() + # Both layers are ALWAYS added (even when a mask is empty) so the trace structure stays constant + # across word additions/removals. With a stable `uirevision`, a changing trace count leaves + # ghost/"shadow" points from the previous render — keeping exactly two traces avoids that. + absent_opacity, absent_size = _emphasize_errors(absent_mask, 0.35, 5, error_mask) + present_opacity, present_size = _emphasize_errors(present_mask, 0.9, 9, error_mask) + + fig.add_trace( + scatter_cls( + x=xy[absent_mask, 0].tolist(), + y=xy[absent_mask, 1].tolist(), + mode="markers", + marker=dict(color="#b0b0b0", size=absent_size, opacity=absent_opacity), + customdata=absent_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in absent_mask], + hovertemplate="%{text}<extra>absent</extra>", + showlegend=False, + ) + ) + fig.add_trace( + scatter_cls( + x=xy[present_mask, 0].tolist(), + y=xy[present_mask, 1].tolist(), + mode="markers", + marker=dict( + color=contributions[present_mask].tolist(), + colorscale="RdBu", + cmin=-max_abs, + cmax=max_abs, + size=present_size, + opacity=present_opacity, + colorbar=dict( + title=dict(text=colorbar_title, side="right"), + thickness=12, + tickformat=".2f", + ), + ), + customdata=present_mask.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in present_mask], + hovertemplate="<b>SHAP: %{marker.color:.3f}</b><br>%{text}<extra></extra>", + showlegend=False, + ) + ) + fig.update_layout(**_LAYOUT_COMMON, showlegend=False) + return fig + + +def _plot_categorical( + xy: np.ndarray, + texts_short: list[str], + labels: list[str], + label_names: list[str], + error_mask: np.ndarray | None, + palette: Sequence[str], + scatter_cls: type[go.Scatter] | type[go.Scattergl], +) -> go.Figure: + fig = go.Figure() + for i, name in enumerate(label_names): + mask = [j for j, lbl in enumerate(labels) if lbl == name] + if not mask: + continue + mask_arr = np.array(mask) + opacity, size = _emphasize_errors(mask_arr, 0.75, 7, error_mask) + fig.add_trace( + scatter_cls( + x=xy[mask_arr, 0].tolist(), + y=xy[mask_arr, 1].tolist(), + mode="markers", + marker=dict(color=palette[i % len(palette)], size=size, opacity=opacity), + customdata=mask_arr.reshape(-1, 1).tolist(), + text=[texts_short[j] for j in mask], + hovertemplate=f"<b>{name}</b><br>%{{text}}<extra></extra>", + name=name, + ) + ) + fig.update_layout( + **_LAYOUT_COMMON, + legend=dict(itemsizing="constant", orientation="v", title_text=""), + showlegend=True, + ) + return fig diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 5fd2259d..dad3cb00 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -147,10 +147,12 @@ class NlpWebApp: ``NlpExplanation.load()``, which carries no model: components self-disable via their ``requires`` when the engine is absent or lacks a capability. scatter_xy : np.ndarray, optional - Pre-computed 2-D projection, shape ``(n_samples, 2)``. When provided, - a scatter panel is added to the layout. Compute with PaCMAP, UMAP, - t-SNE, PCA, etc. and pass the result here — Shapash does not perform - the projection itself to avoid heavy optional dependencies. + Pre-computed 2-D projection, shape ``(n_samples, 2)``. When provided, a scatter panel is + added to the layout. ``NlpWebApp`` itself never computes this — it has no model to embed + with (see ``engine`` above) — so pass it in from whatever produced it: PaCMAP, UMAP, + t-SNE, PCA, etc. Prefer :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection` + (defaults to PCA — no extra install, sklearn is already a core dependency) over rolling + your own, so the scatter and the similar-example neighbours share one embedding space. url_base_pathname : str, optional Mount the app under a URL prefix instead of the server root, for serving behind a reverse proxy that routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Dash diff --git a/shapash/webapp/nlp_components/scatter.py b/shapash/webapp/nlp_components/scatter.py index 08afe927..e5d09838 100644 --- a/shapash/webapp/nlp_components/scatter.py +++ b/shapash/webapp/nlp_components/scatter.py @@ -27,22 +27,10 @@ from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate -from shapash.explainer.nlp_explanation import select_label_column +from shapash.explainer.nlp_explanation import word_contributions_by_sample +from shapash.plots.plot_scatter import plot_scatter from shapash.webapp.nlp_components.base import WebappComponent, error_mask -_PALETTE = [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", -] - class ScatterComponent(WebappComponent): """2-D embeddings scatter, coloured by prediction, ground truth, or word SHAP contribution.""" @@ -217,39 +205,6 @@ def toggle_clear_button(selected_indices): # Helpers # ------------------------------------------------------------------ - def _word_contributions(self, explanation, word: str, label_idx: int) -> np.ndarray: - """Per-sample sum of SHAP contributions for all tokens matching *word*.""" - n = explanation.n_samples - result = np.zeros(n) - word_lower = word.lower() - for i in range(n): - tokens = explanation.token_strings[i] - vals = select_label_column(explanation.values[i], label_idx) - for j, tok in enumerate(tokens): - if tok.strip().lower() == word_lower: - result[i] += vals[j] - return result - - @staticmethod - def _emphasize_errors( - idx_arr: np.ndarray, - base_opacity: float, - base_size: float, - error_mask: np.ndarray | None, - ) -> tuple[list[float] | float, list[float] | float]: - """Per-point opacity/size that pops model errors and shadows everything else. - - Keeps the caller's coloring untouched — only opacity and marker size change — - so points stay grouped/colored however the "Color by" dropdown already draws them. - Returns scalars (the unmodified base values) when ``error_mask`` is unavailable. - """ - if error_mask is None or len(idx_arr) == 0: - return base_opacity, base_size - is_error = error_mask[idx_arr] - opacity = np.where(is_error, max(base_opacity, 0.9), 0.12).tolist() - size = np.where(is_error, base_size + 3, max(base_size - 2, 3)).tolist() - return opacity, size - def _build_scatter_fig( self, explanation, @@ -258,128 +213,38 @@ def _build_scatter_fig( label_idx: int = 0, errors_only: bool = False, ) -> go.Figure: - """2-D scatter coloured by prediction, ground-truth label, or word SHAP contribution. + """Slice ``explanation`` for the requested coloring and hand it to :func:`plot_scatter`. - One trace per class for label-based coloring so the legend works correctly - and Plotly's box/lasso select dims unselected points across all traces. - Word-contribution mode uses a single diverging-colorscale trace. - ``customdata`` always stores the original sample index. When ``errors_only`` is - set, misclassified points are emphasized (larger, opaque) and the rest are - shadowed (small, faint) without altering their color. + ``color_by``/``words``/``label_idx`` decide *which* arrays to slice (a webapp-control + concern); the actual figure construction is the same pure function notebook/script callers + get via ``explanation.plot.scatter`` — see + :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.scatter`. """ - n = explanation.n_samples - exp_texts = explanation.texts - assert exp_texts is not None # noqa: S101 - the webapp requires an already-compiled explainer - texts_short = [(t[:120] + "…") if len(t) > 120 else t for t in exp_texts] - xy = self._scatter_xy err_mask = error_mask(explanation) if errors_only else None if color_by == "word_contribution" and words: - contributions = np.sum([self._word_contributions(explanation, w, label_idx) for w in words], axis=0) - max_abs = float(np.abs(contributions).max()) or 1.0 - present_mask = np.where(contributions != 0.0)[0] - absent_mask = np.where(contributions == 0.0)[0] + contributions = word_contributions_by_sample(explanation, words, label_idx) colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" - - fig = go.Figure() - # Both layers are ALWAYS added (even when a mask is empty) so the trace structure stays - # constant across word additions/removals. With a stable `uirevision`, a changing WebGL - # (Scattergl) trace count leaves ghost/"shadow" points from the previous render — keeping - # exactly two traces avoids that. - absent_opacity, absent_size = self._emphasize_errors(absent_mask, 0.35, 5, err_mask) - present_opacity, present_size = self._emphasize_errors(present_mask, 0.9, 9, err_mask) - - # Gray context layer — absent points (no selected word contributes to them). - fig.add_trace( - go.Scattergl( - x=xy[absent_mask, 0].tolist(), - y=xy[absent_mask, 1].tolist(), - mode="markers", - marker=dict(color="#b0b0b0", size=absent_size, opacity=absent_opacity), - customdata=absent_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in absent_mask], - hovertemplate="%{text}<extra>absent</extra>", - showlegend=False, - ) + return plot_scatter( + self._scatter_xy, + explanation.texts, + contributions=contributions, + colorbar_title=colorbar_title, + error_mask=err_mask, ) - # Colored overlay — samples where at least one selected word contributes. - fig.add_trace( - go.Scattergl( - x=xy[present_mask, 0].tolist(), - y=xy[present_mask, 1].tolist(), - mode="markers", - marker=dict( - color=contributions[present_mask].tolist(), - colorscale="RdBu", - cmin=-max_abs, - cmax=max_abs, - size=present_size, - opacity=present_opacity, - colorbar=dict( - title=dict(text=colorbar_title, side="right"), - thickness=12, - tickformat=".2f", - ), - ), - customdata=present_mask.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in present_mask], - hovertemplate="<b>SHAP: %{marker.color:.3f}</b><br>%{text}<extra></extra>", - showlegend=False, - ) - ) - fig.update_layout( - dragmode="select", - uirevision="scatter", - xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - plot_bgcolor="#f9f9f9", - paper_bgcolor="white", - margin=dict(l=10, r=10, t=10, b=10), - autosize=True, - showlegend=False, - ) - return fig if color_by == "ground_truth" and explanation.y_true is not None: labels = [str(label) for label in explanation.y_true.tolist()] elif explanation.y_pred is not None: labels = [str(label) for label in explanation.y_pred.tolist()] else: - labels = [""] * n + labels = [""] * explanation.n_samples label_names = explanation.label_names or sorted(set(labels)) - - fig = go.Figure() - for i, name in enumerate(label_names): - mask = [j for j, lbl in enumerate(labels) if lbl == name] - if not mask: - continue - mask_arr = np.array(mask) - opacity, size = self._emphasize_errors(mask_arr, 0.75, 7, err_mask) - fig.add_trace( - go.Scattergl( - x=xy[mask_arr, 0].tolist(), - y=xy[mask_arr, 1].tolist(), - mode="markers", - marker=dict(color=_PALETTE[i % len(_PALETTE)], size=size, opacity=opacity), - customdata=mask_arr.reshape(-1, 1).tolist(), - text=[texts_short[j] for j in mask], - hovertemplate=f"<b>{name}</b><br>%{{text}}<extra></extra>", - name=name, - ) - ) - - fig.update_layout( - dragmode="select", - uirevision="scatter", - xaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - yaxis=dict(showticklabels=False, showgrid=True, gridcolor="#e5e5e5", zeroline=False, title=""), - plot_bgcolor="#f9f9f9", - paper_bgcolor="white", - margin=dict(l=10, r=10, t=10, b=10), - # No fixed height — the responsive Graph stretches it to fill the card. - autosize=True, - legend=dict(itemsizing="constant", orientation="v", title_text=""), - showlegend=True, + return plot_scatter( + self._scatter_xy, + explanation.texts, + labels=labels, + label_names=label_names, + error_mask=err_mask, ) - return fig diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/nlp/test_nlp_explanation.py index c9c77a60..5ade461a 100644 --- a/tests/unit_tests/nlp/test_nlp_explanation.py +++ b/tests/unit_tests/nlp/test_nlp_explanation.py @@ -38,9 +38,7 @@ def _make_explanation(values_ndim: int, with_base: bool, with_true: bool, with_p base_values = np.array([0.1, 0.2, 0.3]) if with_base else None y_pred = pd.Series(["pos", "neg", "pos"], index=texts.index, name="prediction") - y_prob = ( - pd.DataFrame({"neg": [0.1, 0.8, 0.3], "pos": [0.9, 0.2, 0.7]}, index=texts.index) if with_prob else None - ) + y_prob = pd.DataFrame({"neg": [0.1, 0.8, 0.3], "pos": [0.9, 0.2, 0.7]}, index=texts.index) if with_prob else None y_true = pd.Series(["pos", "pos", "pos"], index=texts.index, name="ground_truth") if with_true else None return NlpExplanation( diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py index b7479b25..476a621c 100644 --- a/tests/unit_tests/nlp/test_nlp_plotter.py +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -160,6 +160,54 @@ def test_confusion_refuses_without_ground_truth(self): self.assertIn("y_true", str(ctx.exception)) +class TestScatterPlot(unittest.TestCase): + def setUp(self): + self.xy = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]) + + def test_default_colors_by_prediction(self): + explanation = _make_explanation() # y_pred = [pos, neg, pos] + fig = explanation.plot.scatter(self.xy) + self.assertIsInstance(fig, go.Figure) + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos", "neg"}) + + def test_ground_truth_falls_back_to_prediction_without_y_true(self): + explanation = _make_explanation(with_true=False) # y_pred = [pos, neg, pos] + fig = explanation.plot.scatter(self.xy, color_by="ground_truth") + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos", "neg"}) + + def test_word_contribution_colors_by_the_matching_samples(self): + explanation = _make_explanation() # sample 1 tokens: ["i", "am", "happy"] + fig = explanation.plot.scatter(self.xy, color_by="word_contribution", words=["happy"], label_idx=1) + self.assertEqual(len(fig.data), 2) # absent layer + present layer + absent_trace, present_trace = fig.data + self.assertEqual([row[0] for row in present_trace.customdata], [1]) + self.assertEqual([row[0] for row in absent_trace.customdata], [0, 2]) + + def test_word_contribution_without_words_falls_back_to_prediction(self): + explanation = _make_explanation() + fig = explanation.plot.scatter(self.xy, color_by="word_contribution", words=[]) + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos", "neg"}) + + def test_out_of_range_label_idx_raises_with_words(self): + explanation = _make_explanation() + with self.assertRaises(IndexError): + explanation.plot.scatter(self.xy, color_by="word_contribution", words=["happy"], label_idx=5) + + def test_errors_only_runs_without_ground_truth(self): + explanation = _make_explanation(with_true=False) + fig = explanation.plot.scatter(self.xy, errors_only=True) + self.assertIsInstance(fig, go.Figure) + + def test_ground_truth_and_errors_only_with_real_ground_truth(self): + explanation = _make_explanation() # y_true = [pos, pos, pos], y_pred = [pos, neg, pos] + fig = explanation.plot.scatter(self.xy, color_by="ground_truth", errors_only=True) + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos"}) + + class TestGuards(unittest.TestCase): def test_waterfall_refuses_on_a_non_additive_backend(self): """LIME contributions do not sum to the prediction, so the running total is nonsense.""" diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 063ed311..9efb02c4 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -28,10 +28,12 @@ NlpExplanation, aggregate_word_contributions, rank_word_samples, + word_contributions_by_sample, ) from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_noise_matrix import plot_noise_matrix +from shapash.plots.plot_scatter import plot_scatter from shapash.plots.plot_sentence_highlight import plot_sentence_highlight from shapash.plots.plot_token_highlight import plot_token_highlight from shapash.plots.plot_waterfall import plot_waterfall @@ -693,12 +695,14 @@ class _CountingReducer: def __init__(self, scale=1.0): self.scale = scale self.fits = 0 + self.last_kwargs = None def get_params(self, deep=True): return {"scale": self.scale} - def fit_transform(self, x): + def fit_transform(self, x, **kwargs): self.fits += 1 + self.last_kwargs = kwargs return np.asarray(x)[:, :2] * self.scale @@ -789,6 +793,26 @@ def test_recompute_forces_a_fresh_fit(self): self.assertEqual(self.model.calls, 1) self.assertEqual(reducer.fits, 2) + def test_fit_transform_kwargs_are_forwarded(self): + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, init="pca", verbose=False) + self.assertEqual(reducer.last_kwargs, {"init": "pca", "verbose": False}) + + def test_fit_transform_kwargs_take_part_in_the_key(self): + """Changing a call-time argument (e.g. PaCMAP's ``init=``) must not silently reload.""" + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="b") + self.assertEqual(reducer.fits, 2) + + def test_same_fit_transform_kwargs_reuse_the_cache(self): + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") + self.assertEqual(reducer.fits, 1) + # --------------------------------------------------------------------------- # NlpExplainer.fit — reference state, and when its cost is paid @@ -1735,6 +1759,85 @@ def test_absent_class_returns_typed_empty(self): self.assertEqual(list(ranked.columns), ["sample", "contribution", "n_occurrences"]) +class TestWordContributionsBySample(unittest.TestCase): + def setUp(self): + self.explanation = _profile_explanation() + + def test_sums_one_words_occurrences_per_sample(self): + # class 0: sample0 has one "happy" (0.4), sample1 has two ("Happy" 0.2 + "happy" 0.1 = 0.3), + # sample2 has one (-0.6), sample3 has none. + result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + np.testing.assert_allclose(result, [0.4, 0.3, -0.6, 0.0]) + + def test_sums_across_words_within_a_sample(self): + # "today" only occurs in sample0 (0.05, class 0), added on top of "happy"'s own 0.4. + result = word_contributions_by_sample(self.explanation, ["happy", "today"], label_idx=0) + np.testing.assert_allclose(result, [0.45, 0.3, -0.6, 0.0]) + + def test_label_idx_selects_the_class(self): + class0 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + class1 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=1) + np.testing.assert_allclose(class1, -class0) + + def test_absent_word_returns_all_zeros(self): + result = word_contributions_by_sample(self.explanation, ["nonexistent"], label_idx=0) + np.testing.assert_allclose(result, np.zeros(len(self.explanation))) + + def test_result_length_matches_the_batch(self): + result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + self.assertEqual(len(result), len(self.explanation)) + + +class TestPlotScatter(unittest.TestCase): + def setUp(self): + self.xy = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]]) + self.texts = ["so happy today !", "Happy and happy again", "not happy at all", "nothing here"] + + def test_categorical_mode_draws_one_trace_per_label_name(self): + fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "neg", "neg"], label_names=["pos", "neg"]) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data), 2) + self.assertEqual(fig.data[0].name, "pos") + self.assertEqual([row[0] for row in fig.data[0].customdata], [0, 1]) + + def test_categorical_mode_skips_empty_categories(self): + fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "pos", "pos"], label_names=["pos", "neg"]) + self.assertEqual(len(fig.data), 1) + + def test_word_contribution_mode_splits_present_and_absent(self): + fig = plot_scatter(self.xy, self.texts, contributions=np.array([0.4, 0.0, -0.6, 0.0])) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data), 2) + absent_trace, present_trace = fig.data + self.assertEqual([row[0] for row in absent_trace.customdata], [1, 3]) + self.assertEqual([row[0] for row in present_trace.customdata], [0, 2]) + + def test_error_mask_shrinks_correct_points_and_grows_wrong_ones(self): + contributions = np.array([0.4, 0.0, -0.6, 0.0]) + error_mask = np.array([True, False, False, False]) + fig = plot_scatter(self.xy, self.texts, contributions=contributions, error_mask=error_mask) + _, present_trace = fig.data + # present_mask is [0, 2]; sample 0 is the error and should be emphasized over sample 2. + sizes = present_trace.marker.size + self.assertGreater(sizes[0], sizes[1]) + + def test_requires_exactly_one_of_contributions_or_labels(self): + with self.assertRaises(ValueError): + plot_scatter(self.xy, self.texts) + with self.assertRaises(ValueError): + plot_scatter( + self.xy, + self.texts, + labels=["pos"] * 4, + label_names=["pos"], + contributions=np.zeros(4), + ) + + def test_labels_without_label_names_raises(self): + with self.assertRaises(ValueError): + plot_scatter(self.xy, self.texts, labels=["pos"] * 4) + + class TestPlotWordProfile(unittest.TestCase): def setUp(self): self.stats = pd.Series([0.2, -0.3], index=pd.Index([0, 1], name="class_idx")) diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index bfe854fe..ed4d3614 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -1589,6 +1589,45 @@ def test_omitted_without_word_importance(self): self.assertIn("prediction", options) +class TestBuildScatterFig(unittest.TestCase): + """``_build_scatter_fig``'s own dispatch — the part left after extracting the drawing itself + into :func:`~shapash.plots.plot_scatter.plot_scatter`.""" + + @staticmethod + def _explanation(with_true: bool, with_pred: bool): + texts = pd.Series(["rare common", "common"]) + return NlpExplanation( + texts=texts, + token_strings=[["rare", "common"], ["common"]], + values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], + base_values=None, + y_pred=pd.Series(["neg", "pos"], index=texts.index) if with_pred else None, + y_prob=None, + y_true=pd.Series(["pos", "pos"], index=texts.index) if with_true else None, + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def _component(self): + from shapash.webapp.nlp_components import ScatterComponent + + return ScatterComponent(np.zeros((2, 2)), offer_word_contribution=True) + + def test_ground_truth_colors_by_y_true(self): + fig = self._component()._build_scatter_fig(self._explanation(True, True), "ground_truth") + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos"}) # both samples are "pos" in y_true + + def test_without_prediction_or_ground_truth_draws_a_single_unlabeled_group(self): + fig = self._component()._build_scatter_fig(self._explanation(False, False), "prediction") + self.assertEqual(len(fig.data), 1) + self.assertEqual(len(fig.data[0].x), 2) + + class TestWordProfileControls(unittest.TestCase): """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" From d18762197bb7a41428654f1f8a67425aa4e7f4db Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 10 Sep 2026 11:52:38 +0200 Subject: [PATCH 106/144] docs: add notebook for overview of the nlp features --- tutorial/nlp/overview.ipynb | 605 ++++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 tutorial/nlp/overview.ipynb diff --git a/tutorial/nlp/overview.ipynb b/tutorial/nlp/overview.ipynb new file mode 100644 index 00000000..8df052f5 --- /dev/null +++ b/tutorial/nlp/overview.ipynb @@ -0,0 +1,605 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "893fd947", + "metadata": {}, + "source": [ + "# NLP Explainer - Overview" + ] + }, + { + "cell_type": "markdown", + "id": "a6e1cf71", + "metadata": {}, + "source": [ + "## Set up" + ] + }, + { + "cell_type": "markdown", + "id": "901f2890", + "metadata": {}, + "source": [ + "First, make sure to install all required dependencies:\n", + "```\n", + "uv sync --extra nlp\n", + "```\n", + "or\n", + "```\n", + "pip install '.[nlp]'" + ] + }, + { + "cell_type": "markdown", + "id": "93a0f246", + "metadata": {}, + "source": [ + "We will work with the model [bhadresh-savani/distilbert-base-uncased-emotion](https://huggingface.co/bhadresh-savani/distilbert-base-uncased-emotion) from Hugging Face.\n", + "\n", + "This is a sentiment analysis distilbert classifier trained on [dair-ai/emotion dataset](https://huggingface.co/datasets/dair-ai/emotion)\n", + "\n", + "We start by loading it with our wrapper `HFClassifierModel`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15576fc2", + "metadata": {}, + "outputs": [], + "source": [ + "from shapash.model.hf import HFClassifierModel\n", + "\n", + "model = HFClassifierModel.from_pretrained(\n", + " \"bhadresh-savani/distilbert-base-uncased-emotion\",\n", + " label_names=[\"sadness\", \"joy\", \"love\", \"anger\", \"fear\", \"surprise\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2b0d6d52", + "metadata": {}, + "source": [ + "We test its predict method on some selected texts:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "880f09dd", + "metadata": {}, + "outputs": [], + "source": [ + "texts = [\n", + " \"i am so happy today\",\n", + " \"i feel terrified\",\n", + " \"when i was a child i was completely mad of my teacher who repeatedly turned my interventions to derision\"\n", + " ]\n", + "preds = model.predict(texts)\n", + "preds" + ] + }, + { + "cell_type": "markdown", + "id": "9d883590", + "metadata": {}, + "source": [ + "## The NlpExplainer" + ] + }, + { + "cell_type": "markdown", + "id": "6a0421b8", + "metadata": {}, + "source": [ + "The main object of Shapash NLP is the `NlpExplainer`. It takes as input the model and the label names.\n", + "\n", + "The default explainer backend is Shap, other options include LIME and Layer Integrated Gradients. We will explore these other possibilities in a dedicated notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "109093f1", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from shapash.explainer.nlp_explainer import NlpExplainer\n", + "\n", + "xpl = NlpExplainer(model, label_names=[\"sadness\", \"joy\", \"love\", \"anger\", \"fear\", \"surprise\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "141457c5", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.backend" + ] + }, + { + "cell_type": "markdown", + "id": "589d7708", + "metadata": {}, + "source": [ + "## Fit method" + ] + }, + { + "cell_type": "markdown", + "id": "18be4cba", + "metadata": {}, + "source": [ + "We start fitting the explainer with some texts of the train corpus. We will use them to find similar examples for a selected text to explain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc060138", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.fit(texts, y=[\"joy\", \"fear\", \"anger\"])" + ] + }, + { + "cell_type": "markdown", + "id": "b8a6324d", + "metadata": {}, + "source": [ + "Looking for similar samples in the train corpus is easy. Under the hood, embeddings are computed: by default these are the inputs of the linear classification head of the model, but you can choose other layers at model initialization with the `embedding_space` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f2b60ff", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.find_similar(\"that scares me\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9df4107", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.find_similar_threshold(\"that scares me\", threshold=0.9)" + ] + }, + { + "cell_type": "markdown", + "id": "b9678120", + "metadata": {}, + "source": [ + "## Explain method" + ] + }, + { + "cell_type": "markdown", + "id": "bf6c7c20", + "metadata": {}, + "source": [ + "This is the main method of our explainer. We compute the explanations (with the selected backend, in this case Shap) for the input texts for all the classes of the model, including the predicted and the true class (given explicitly as `y`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca47352e", + "metadata": {}, + "outputs": [], + "source": [ + "explanations = xpl.explain(texts, y=[\"joy\", \"fear\", \"anger\"])" + ] + }, + { + "cell_type": "markdown", + "id": "359561e9", + "metadata": {}, + "source": [ + "The output is an immutable dataclass `NlpExplanation`, which can be saved and loaded independently from the explainer." + ] + }, + { + "cell_type": "markdown", + "id": "c067abcd", + "metadata": {}, + "source": [ + "You can inspect the contribution values and the base values for each token" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55a454ef", + "metadata": {}, + "outputs": [], + "source": [ + "# shap base values\n", + "explanations.base_values\n", + "\n", + "# raw contributions for each token of the input texts: shape (n_samples, n_tokens, n_classes)\n", + "explanations.values\n", + "\n", + "# token strings associated with the contribution values\n", + "explanations.token_strings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af5ceb97", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.texts" + ] + }, + { + "cell_type": "markdown", + "id": "00a687b7", + "metadata": {}, + "source": [ + "There are several useful methods for explanations inspection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95d2ca09", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.confusion_matrix()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da794525", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.word_importance(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f033e02", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.word_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e9adf75", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.word_occurrences(\"happy\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ebe96bb", + "metadata": {}, + "source": [ + "Saving and loading explanations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "863765cf", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.save(my_save_path)\n", + "\n", + "from shapash.explainer.nlp_explanation import NlpExplanation\n", + "explanations = NlpExplanation.load(my_save_path)" + ] + }, + { + "cell_type": "markdown", + "id": "bdacdb5a", + "metadata": {}, + "source": [ + "## Plots" + ] + }, + { + "cell_type": "markdown", + "id": "55824127", + "metadata": {}, + "source": [ + "We included several plotting functions for the explanations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3600a940", + "metadata": {}, + "outputs": [], + "source": [ + "# bar chart of the contributions of each token, \n", + "# the first input is the row index of the input text, the second input is the label index\n", + "explanations.plot.tokens(0, label_idx=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83bfd563", + "metadata": {}, + "outputs": [], + "source": [ + "# waterfall plot of the contributions of each token\n", + "explanations.plot.waterfall(1, label_idx=4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e29e7e9", + "metadata": {}, + "outputs": [], + "source": [ + "# sentence highlighting of the contributions of each token\n", + "explanations.plot.sentence(2, label_idx=3, notebook=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afcb78cc", + "metadata": {}, + "outputs": [], + "source": [ + "# global word importance for the entire set of explanations\n", + "# the first input is the label index\n", + "explanations.plot.word_importance(1, n_top=5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8aa7ec3", + "metadata": {}, + "outputs": [], + "source": [ + "# word profile for a specific word\n", + "# includes the aggregated contributions of the word for each label\n", + "explanations.plot.word_profile(\"terrified\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13f44f2c", + "metadata": {}, + "outputs": [], + "source": [ + "# confusion matrix of the predictions\n", + "explanations.plot.confusion()" + ] + }, + { + "cell_type": "markdown", + "id": "d348c5be", + "metadata": {}, + "source": [ + "## Embedding projection and scatterplot" + ] + }, + { + "cell_type": "markdown", + "id": "81939e89", + "metadata": {}, + "source": [ + "We can project the embeddings of the explained texts and display them on a scatter plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7932548d", + "metadata": {}, + "outputs": [], + "source": [ + "# Native projection with PCA (default)\n", + "scatter_xy = xpl.compute_projection(explanations)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28893975", + "metadata": {}, + "outputs": [], + "source": [ + "explanations.plot.scatter(scatter_xy, use_webgl=False)" + ] + }, + { + "cell_type": "markdown", + "id": "4d2887f7", + "metadata": {}, + "source": [ + "You can use your own favorite projection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db4b07aa", + "metadata": {}, + "outputs": [], + "source": [ + "# Custom projection (PaCMAP, UMAP, TSNE, etc.) with optional parameters\n", + "import pacmap\n", + "reducer = pacmap.PaCMAP(n_components=2, n_neighbors=1, MN_ratio=0.5, FP_ratio=2.0)\n", + "\n", + "xpl.compute_projection(explanations, reducer=reducer, init=\"pca\")" + ] + }, + { + "cell_type": "markdown", + "id": "90f28601", + "metadata": {}, + "source": [ + "## Counterfactuals" + ] + }, + { + "cell_type": "markdown", + "id": "174e7585", + "metadata": {}, + "source": [ + "How does the model behave by dropping some words or substituting them with others? This is the main aim of the conterfactuals explanations: \n", + "- `AblationFlipGenerator` drops a subset of words and returns the sentences whose predicted label changed\n", + "- `HotFlipGenerator` substitutes a subset of words and returns the sentences whose predicted label changed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f228b21f", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.available_cf_generators()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fed2c2a", + "metadata": {}, + "outputs": [], + "source": [ + "res = xpl.generate_counterfactuals(\"I am happy today\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77e5a48f", + "metadata": {}, + "outputs": [], + "source": [ + "import dataclasses\n", + "import pandas as pd\n", + "\n", + "pd.DataFrame([dataclasses.asdict(cf) for cf in res])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64fcf86c", + "metadata": {}, + "outputs": [], + "source": [ + "xpl.generate_counterfactuals(\n", + " \"I am happy today\", \n", + " generator=\"ablation_flip\",\n", + " config={\"max_ablations\": 3}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5d72c821", + "metadata": {}, + "source": [ + "## Webapp" + ] + }, + { + "cell_type": "markdown", + "id": "6528debe", + "metadata": {}, + "source": [ + "Finally you can run the webapp, which puts all the features together and interact with" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "917f3095", + "metadata": {}, + "outputs": [], + "source": [ + "app = xpl.run_app(explanations)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6feee1b0", + "metadata": {}, + "outputs": [], + "source": [ + "# stop the app\n", + "app.kill()" + ] + }, + { + "cell_type": "markdown", + "id": "2f96e12b", + "metadata": {}, + "source": [ + "The app has features which depend directly only from the explanations object, thus you can run it without the explainer.\n", + "Similar examples, Counterfactuals and Data Editor need the model and the explainer loaded. In this case, if you want to intereact with these features you can pass an explainer in the `engine` parameter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7ba1c10", + "metadata": {}, + "outputs": [], + "source": [ + "from shapash.webapp.nlp_app import NlpWebApp\n", + "NlpWebApp(explanations, engine=xpl).run()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "shapash (3.13.13.final.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 743438b878728531f7759b422e1fa4b7c99b4160 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 10 Sep 2026 16:25:17 +0200 Subject: [PATCH 107/144] feat(model): add trust_remote_code through HFClassifierModel.from_pretrained MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom-architecture checkpoints (for ex GTE-v1.5, Jina, Nomic) could not be loaded: from_pretrained never passed trust_remote_code and forwarded no kwargs to the loaders, so AutoModel refused with "Please pass the argument trust_remote_code=True". - Add `trust_remote_code: bool = False`, applied to both the tokenizer and the model load since a custom checkpoint normally ships both. It is forwarded explicitly (False included) rather than left unset: unset, transformers prompts interactively for confirmation, which would hang a webapp or batch job on a prompt nobody sees. - Add `load_kwargs` for the remaining loader arguments (revision, dtype, token, cache_dir), kept separate from **model_kwargs — which still configures the adapter — so no argument is ambiguous about which destination it reaches. - _load_tokenizer forwards **tokenizer_kwargs to both the fast and slow attempts, and its dead-end error no longer declares the checkpoint unsupported: it suggests trust_remote_code=True (noting this executes code from the checkpoint), or, when custom code was already allowed, points at the path, network and missing sentencepiece/protobuf instead. --- shapash/model/hf.py | 71 ++++++++++++++---- .../model/test_hf_from_pretrained.py | 73 +++++++++++++++++-- 2 files changed, 126 insertions(+), 18 deletions(-) diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 332cf533..0aec19f5 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -117,18 +117,27 @@ def _resolve_tokenizer_source(name_or_path: str, tokenizer_name: str | None = No return base_model -def _load_tokenizer(source: str, transformers: Any) -> Any: +def _load_tokenizer(source: str, transformers: Any, **tokenizer_kwargs: Any) -> Any: """Load the tokenizer, preferring a fast one but falling back to the slow implementation. A fast tokenizer is best — it unlocks the exact ``word_ids()`` word-alignment the LIG highlights use — but some checkpoints ship no ``tokenizer.json``, so the fast load raises. We then retry with ``use_fast=False``; a slow tokenizer still works (gradient attribution degrades to a scheme-aware - string merge). When *both* fail, the checkpoint has no standard HF tokenizer at all — typically a - custom ``trust_remote_code`` architecture — which this adapter does not support; surface that as a - clear error rather than a deep tokenizer stack trace. + string merge). When *both* fail the checkpoint has no tokenizer this call can build, and the error + says what to do about it rather than dumping a deep tokenizer stack trace. + + Parameters + ---------- + source : str + Hub id or local path to load the tokenizer from. + transformers : module + The imported ``transformers`` module (injected so this stays unit-testable). + **tokenizer_kwargs + Forwarded to both attempts — notably ``trust_remote_code``, which a checkpoint shipping its own + tokenizer implementation needs. ``use_fast`` is set here and must not be passed. """ try: - return transformers.AutoTokenizer.from_pretrained(source, use_fast=True) + return transformers.AutoTokenizer.from_pretrained(source, use_fast=True, **tokenizer_kwargs) except Exception as fast_err: # noqa: BLE001 — retry slow, then re-raise with a usable hint logger.warning( "Fast tokenizer unavailable for %s (%s) — retrying with use_fast=False.", @@ -136,13 +145,24 @@ def _load_tokenizer(source: str, transformers: Any) -> Any: type(fast_err).__name__, ) try: - return transformers.AutoTokenizer.from_pretrained(source, use_fast=False) - except Exception as slow_err: # noqa: BLE001 — no standard tokenizer; give an actionable message + return transformers.AutoTokenizer.from_pretrained(source, use_fast=False, **tokenizer_kwargs) + except Exception as slow_err: # noqa: BLE001 — no loadable tokenizer; give an actionable message + # The hint depends on what was already tried: suggesting trust_remote_code=True to someone + # who just passed it is noise, and would read as "this checkpoint is unsupported" when the + # real problem is something else (a bad path, a missing sentencepiece/protobuf, no network). + hint = ( + "If this checkpoint ships a custom tokenizer implementation, retry with " + "trust_remote_code=True (this executes code from the checkpoint — only do so for a " + "source you trust)." + if not tokenizer_kwargs.get("trust_remote_code") + else "Custom code was already allowed, so this is not a trust_remote_code problem — " + "check the path/hub id, network access, and that any extra tokenizer dependency " + "(e.g. sentencepiece, protobuf) is installed." + ) raise RuntimeError( - f"Could not load a tokenizer for {source!r} in fast or slow mode. This is usually a " - "checkpoint with a non-standard or custom (trust_remote_code) tokenizer/architecture, " - "which HFClassifierModel does not support. Use a standard " - "AutoModelForSequenceClassification checkpoint that ships a normal HF tokenizer." + f"Could not load a tokenizer for {source!r} in fast or slow mode. {hint} You can also " + "pass an already-loaded tokenizer via tokenizer=, or build HFClassifierModel(classifier, " + "tokenizer) directly." ) from slow_err @@ -327,6 +347,8 @@ def from_pretrained( label_names: list[str] | None = None, device: int | str | object | None = None, max_length: int | str | None = "auto", + trust_remote_code: bool = False, + load_kwargs: dict[str, Any] | None = None, **model_kwargs: Any, ) -> HFClassifierModel: """Build a full-capability adapter straight from a checkpoint id or a local directory. @@ -361,6 +383,26 @@ def from_pretrained( sentinel (see :func:`_resolve_max_length`). ``None`` trusts the tokenizer verbatim (the length guard in :class:`~shapash.model.encoder.EncoderClassifierModel` still applies). An int forces an explicit length. + trust_remote_code : bool, optional + Allow the checkpoint to define its own model/tokenizer classes (``AutoModel``'s flag of the + same name), which is what a custom architecture such as ``Alibaba-NLP/gte-base-en-v1.5``, + Jina or Nomic needs. Default ``False``. Applied to the tokenizer *and* the model, since a + checkpoint that ships one usually ships both. + + **This executes Python code downloaded from the checkpoint**, so pass it only for a source + you trust. It is always forwarded explicitly (``False`` included) rather than left unset: + unset, ``transformers`` asks for confirmation on an interactive terminal, which would hang + a webapp or a batch job on a prompt nobody sees. + + A loaded custom architecture is not automatically *fully* capable — the capability surface + still needs a backbone that accepts ``inputs_embeds`` and exposes ``get_input_embeddings()`` + (see :class:`~shapash.model.encoder.EncoderClassifierModel`). Prediction and SHAP work + regardless; gradients and Captum LIG depend on the checkpoint's own implementation. + load_kwargs : dict, optional + Extra keyword arguments forwarded to ``AutoModelForSequenceClassification.from_pretrained`` + and to the tokenizer load (e.g. ``revision``, ``dtype``, ``token``, ``cache_dir``). Kept + separate from ``**model_kwargs`` below, which configures *this adapter* — one dict per + destination, so no argument is ambiguous about which of the two it reaches. **model_kwargs Forwarded to the constructor (e.g. ``batch_size``, ``embedding_space``, ``pool``). @@ -376,10 +418,13 @@ def from_pretrained( length — see :meth:`__init__`). """ transformers = import_optional_module("transformers", extra=_NLP_EXTRA) + loader_kwargs = {"trust_remote_code": trust_remote_code, **(load_kwargs or {})} if tokenizer is None or isinstance(tokenizer, str): - tokenizer = _load_tokenizer(_resolve_tokenizer_source(name_or_path, tokenizer), transformers) + tokenizer = _load_tokenizer( + _resolve_tokenizer_source(name_or_path, tokenizer), transformers, **loader_kwargs + ) - classifier = transformers.AutoModelForSequenceClassification.from_pretrained(str(name_or_path)) + classifier = transformers.AutoModelForSequenceClassification.from_pretrained(str(name_or_path), **loader_kwargs) if device is not None: classifier = classifier.to(device) logger.info("Loaded %s as HFClassifierModel on device: %s", name_or_path, next(classifier.parameters()).device) diff --git a/tests/unit_tests/model/test_hf_from_pretrained.py b/tests/unit_tests/model/test_hf_from_pretrained.py index a0498b19..741ca9aa 100644 --- a/tests/unit_tests/model/test_hf_from_pretrained.py +++ b/tests/unit_tests/model/test_hf_from_pretrained.py @@ -104,9 +104,11 @@ def __init__(self, fail_fast=False, fail_slow=False): self.fail_fast = fail_fast self.fail_slow = fail_slow self.calls = [] + self.kwargs = [] - def from_pretrained(self, source, use_fast): + def from_pretrained(self, source, use_fast, **kwargs): self.calls.append(use_fast) + self.kwargs.append(kwargs) if use_fast and self.fail_fast: raise OSError("no tokenizer.json") if not use_fast and self.fail_slow: @@ -128,12 +130,28 @@ def test_load_tokenizer_falls_back_to_slow(): assert auto.calls == [True, False] -def test_load_tokenizer_both_fail_raises_actionable_error(): +def test_load_tokenizer_both_fail_suggests_trust_remote_code(): auto = _FakeAutoTokenizer(fail_fast=True, fail_slow=True) - with pytest.raises(RuntimeError, match="does not support"): + with pytest.raises(RuntimeError, match="retry with trust_remote_code=True"): _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto)) +def test_load_tokenizer_both_fail_with_custom_code_allowed_points_elsewhere(): + # Suggesting trust_remote_code=True to someone who already passed it would misdiagnose the failure. + auto = _FakeAutoTokenizer(fail_fast=True, fail_slow=True) + with pytest.raises(RuntimeError, match="not a trust_remote_code problem"): + _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto), trust_remote_code=True) + + +def test_load_tokenizer_forwards_kwargs_to_both_attempts(): + auto = _FakeAutoTokenizer(fail_fast=True) + _load_tokenizer("some-model", SimpleNamespace(AutoTokenizer=auto), trust_remote_code=True, revision="v2") + assert auto.kwargs == [ + {"trust_remote_code": True, "revision": "v2"}, + {"trust_remote_code": True, "revision": "v2"}, + ] + + # ── label_names arity check in __init__ ─────────────────────────────────────────────────────────────── @@ -200,11 +218,18 @@ def __init__(self, tokenizer, classifier): self._tokenizer = tokenizer self._classifier = classifier self.model_loaded_from = None - self.AutoTokenizer = SimpleNamespace(from_pretrained=lambda source, use_fast: tokenizer) + self.model_kwargs = None + self.tokenizer_kwargs = None + self.AutoTokenizer = SimpleNamespace(from_pretrained=self._load_tokenizer) self.AutoModelForSequenceClassification = SimpleNamespace(from_pretrained=self._load_model) - def _load_model(self, source): + def _load_tokenizer(self, source, use_fast, **kwargs): + self.tokenizer_kwargs = kwargs + return self._tokenizer + + def _load_model(self, source, **kwargs): self.model_loaded_from = source + self.model_kwargs = kwargs return self._classifier @@ -259,6 +284,44 @@ def test_from_pretrained_label_names_override_is_arity_checked(fake_transformers def test_from_pretrained_forwards_model_kwargs(fake_transformers): model = HFClassifierModel.from_pretrained("some/checkpoint", batch_size=7) assert model.batch_size == 7 + # ``**model_kwargs`` configures the adapter — it must never leak into the checkpoint loaders. + assert "batch_size" not in fake_transformers.model_kwargs + assert "batch_size" not in fake_transformers.tokenizer_kwargs + + +def test_from_pretrained_passes_trust_remote_code_to_both_loaders(fake_transformers): + # A custom architecture (GTE-v1.5, Jina, Nomic) ships both a model and a tokenizer implementation, + # so the flag has to reach both loads or the pair fails halfway. + HFClassifierModel.from_pretrained("custom/checkpoint", trust_remote_code=True) + assert fake_transformers.model_kwargs["trust_remote_code"] is True + assert fake_transformers.tokenizer_kwargs["trust_remote_code"] is True + + +def test_from_pretrained_sends_trust_remote_code_false_explicitly(fake_transformers): + # Explicit False, not omitted: left unset, transformers prompts for confirmation on an interactive + # terminal, which would hang a webapp or batch job on a prompt nobody sees. + HFClassifierModel.from_pretrained("some/checkpoint") + assert fake_transformers.model_kwargs["trust_remote_code"] is False + assert fake_transformers.tokenizer_kwargs["trust_remote_code"] is False + + +def test_from_pretrained_forwards_load_kwargs_to_both_loaders(fake_transformers): + HFClassifierModel.from_pretrained("some/checkpoint", load_kwargs={"revision": "abc123"}) + assert fake_transformers.model_kwargs["revision"] == "abc123" + assert fake_transformers.tokenizer_kwargs["revision"] == "abc123" + + +def test_from_pretrained_load_kwargs_can_override_trust_remote_code(fake_transformers): + HFClassifierModel.from_pretrained("some/checkpoint", load_kwargs={"trust_remote_code": True}) + assert fake_transformers.model_kwargs["trust_remote_code"] is True + + +def test_from_pretrained_skips_the_tokenizer_load_when_one_is_supplied(fake_transformers): + # An already-loaded tokenizer is used verbatim, so nothing is forwarded to a tokenizer loader. + tokenizer = _FakeTokenizer() + HFClassifierModel.from_pretrained("some/checkpoint", tokenizer=tokenizer, trust_remote_code=True) + assert fake_transformers.tokenizer_kwargs is None + assert fake_transformers.model_kwargs["trust_remote_code"] is True def test_from_pretrained_moves_classifier_to_requested_device(fake_transformers, monkeypatch): From f0cd1a4fe8e64407374e122f777005dad0a21fa1 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 10 Sep 2026 17:24:06 +0200 Subject: [PATCH 108/144] fix(model): decide word substitutability by position, not by token alone A per-token predicate cannot see that "gr" heads ["gr", "##ou", "##chy"] (flipping it rebuilt "superouchy"), nor that byte-BPE leaves a text's first token unmarked (making the opening word unperturbable on the RoBERTa/GPT-2 family). Add `continues_word` / `is_substitutable_at` to SupportsTokenization and use them for position selection in HotFlip and AblationFlip; `is_substitutable` stays correct for vocab entries offered as replacements. --- shapash/compute/generators/ablation_flip.py | 10 ++- shapash/compute/generators/cf_utils.py | 4 +- shapash/compute/generators/hotflip.py | 25 +++++-- shapash/model/base.py | 75 +++++++++++++++++++ .../compute/generators/test_ablation_flip.py | 57 ++++++++++++++ tests/unit_tests/model/test_model_base.py | 71 ++++++++++++++++++ 6 files changed, 232 insertions(+), 10 deletions(-) diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py index dd55c7a5..767d6c0d 100644 --- a/shapash/compute/generators/ablation_flip.py +++ b/shapash/compute/generators/ablation_flip.py @@ -74,13 +74,19 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No target_class = label_names.index(target_label) if (target_label and target_label in label_names) else None tokens = model.tokenize(text) - # Removable positions are decided by the *model's* tokenization scheme (``is_substitutable``), + # Removable positions are decided by the *model's* tokenization scheme (``is_substitutable_at``), # not by a string rule here: under SentencePiece/byte-BPE every content token carries a # ``▁``/``Ġ`` word-start marker, which a bare ``isalpha`` check rejects wholesale. ``ignore`` # holds user-typed words, so it is matched against each token's display form for the same # reason — ``"great"`` must match the token ``"▁great"``. + # + # The *position* form matters here as much as in HotFlip: dropping the head of a multi-piece + # word strands its continuations, turning ``["gr", "##ou", "##chy"]`` into ``"ouchy"`` rather + # than removing ``"grouchy"``. content_positions = [ - i for i, t in enumerate(tokens) if model.is_substitutable(t) and display_form(model, t) not in ignore + i + for i, t in enumerate(tokens) + if model.is_substitutable_at(tokens, i) and display_form(model, t) not in ignore ] if not content_positions: return [] diff --git a/shapash/compute/generators/cf_utils.py b/shapash/compute/generators/cf_utils.py index 88fbab74..56432d8c 100644 --- a/shapash/compute/generators/cf_utils.py +++ b/shapash/compute/generators/cf_utils.py @@ -6,7 +6,9 @@ Token word-hood used to live here too, as a bare ``token.isalpha()``. It now lives in the model layer (:func:`~shapash.model.base.is_word_token` and the scheme-aware :meth:`~shapash.model.base.SupportsTokenization.is_substitutable`), because the answer depends on the -tokenization scheme and only the model knows which one it uses. +tokenization scheme and only the model knows which one it uses. Generators choosing a *position* to +perturb want :meth:`~shapash.model.base.SupportsTokenization.is_substitutable_at` instead, which also +rejects the head of a multi-piece word — a question no per-token predicate can answer. """ from __future__ import annotations diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index 3ca643b8..76eeaf4d 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -3,13 +3,14 @@ HotFlip finds a *minimal* set of token substitutions that changes the model's prediction, using a single backward pass to estimate each token's impact: -1. Rank content tokens by the L2 norm of their input-embedding gradient (largest first). +1. Rank content tokens by the L2 norm of their input-embedding gradient (largest first), keeping only + positions holding a **complete word** (:meth:`~shapash.model.base.SupportsTokenization.is_substitutable_at`). 2. For each position, shortlist the top-K **word** candidates by the first-order estimate (``embedding_matrix @ token_gradient``, most-negative first), then **re-score that shortlist against the model** and keep the one that most reduces the original-class probability. The linear estimate alone is an unreliable proxy — its single best pick often does not flip at all — so - model verification is what surfaces genuine substitutions. Sub-word (``##``) and non-word tokens - are excluded so rebuilt text stays well-formed. + model verification is what surfaces genuine substitutions. Sub-word continuations, the *heads* of + multi-piece words, and non-word tokens are all excluded so rebuilt text stays well-formed. 3. Hand the chosen substitutions to the shared minimal search (:meth:`~shapash.compute.generators.base.CounterfactualGenerator.search_minimal`), which tries flip combinations of increasing size (1..``max_flips``) in batched ``predict`` calls and keeps the @@ -21,6 +22,7 @@ from __future__ import annotations +from collections.abc import Sequence from typing import Protocol, cast import numpy as np @@ -60,6 +62,8 @@ def detokenize(self, tokens: list[str]) -> str: ... def is_substitutable(self, token: str) -> bool: ... + def is_substitutable_at(self, tokens: Sequence[str], index: int) -> bool: ... + class HotFlipGenerator(CounterfactualGenerator): """Gradient-based token-substitution counterfactuals (LIT HotFlip).""" @@ -80,9 +84,11 @@ def is_compatible(cls, model: TextModel) -> bool: """Compatible only with models exposing gradients, an embedding table *and* tokenization. ``SupportsTokenization`` is required because rebuilding a flipped sentence goes through - ``detokenize``, and choosing which vocab entries are usable replacements goes through - ``is_substitutable`` — both live on that capability. It was previously used without being - declared, which happened to work only because every gradient-capable adapter also tokenizes. + ``detokenize``, choosing which vocab entries are usable replacements goes through + ``is_substitutable``, and choosing which *positions* may be flipped goes through + ``is_substitutable_at`` — all three live on that capability. It was previously used without + being declared, which happened to work only because every gradient-capable adapter also + tokenizes. """ return has_capabilities(model, SupportsGradients, SupportsEmbeddings, SupportsTokenization) @@ -108,10 +114,15 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No # filtered by word-hood as well as candidates: substituting a whole word into a *mid-word* # position rebuilds malformed text, which is what the module docstring already promises to # avoid — it was previously enforced on candidates only. + # + # ``is_substitutable_at`` (not ``is_substitutable``) because the *head* of a multi-piece word is + # itself a bare word token: ``"grouchy"`` tokenizes to ``["gr", "##ou", "##chy"]``, and flipping + # position ``"gr"`` rebuilt ``"superouchy"``. Only the position form can see the continuation + # that follows. grad_l2 = np.sum(grads * grads, axis=-1) ranked = np.argsort(-grad_l2).tolist() flippable = [ - p for p in ranked if model.is_substitutable(tokens[p]) and display_form(model, tokens[p]) not in ignore + p for p in ranked if model.is_substitutable_at(tokens, p) and display_form(model, tokens[p]) not in ignore ][:_MAX_FLIPPABLE_TOKENS] vocab, matrix = model.get_embedding_table() diff --git a/shapash/model/base.py b/shapash/model/base.py index 590f6289..1da564b2 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -22,6 +22,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Sequence from typing import Any, Protocol, cast, runtime_checkable import numpy as np @@ -104,6 +105,12 @@ def model_id(self) -> str: # starts bare. See :meth:`SupportsTokenization.word_start_marker`. WORD_START_MARKERS = ("Ġ", "▁") # "Ġ", "▁" +# Word-*continuation* markers, the opposite convention: WordPiece prefixes every piece *after* the +# first with ``##``. Needed to tell a whole word from the *head* of a multi-piece one — ``"gr"`` in +# ``["gr", "##ou", "##chy"]`` is bare and alphabetic, so it passes every token-level word test while +# being unsubstitutable in place. See :meth:`SupportsTokenization.is_substitutable_at`. +CONTINUATION_MARKERS = ("##",) + # Two ordinary lowercase words, tokenized once to detect the convention above. Deliberately trivial: # any tokenizer segments this into whole words, so a marker appears if and only if the scheme uses one. _MARKER_PROBE_TEXT = "hello world" @@ -234,6 +241,74 @@ def is_substitutable(self, token: str) -> bool: return is_word_token(token) return token.startswith(marker) and is_word_token(token[len(marker) :]) + def continues_word(self, token: str) -> bool: + """Whether ``token`` continues the word begun by the token before it. + + The mirror image of :meth:`is_substitutable`, and answered from the same probed scheme: + + * **Continuation-marking** tokenizers say so explicitly — ``##ou`` continues, ``and`` does not. + * **Word-start-marking** tokenizers say so by omission: an *unmarked* piece continues the + previous word. Punctuation is unmarked too (``"▁rude"``, ``"."``) but starts nothing, so the + alphabetic test excludes it — without that, no word before a comma or full stop would ever be + perturbable. + + Only meaningful for a token that *follows* another; the first token of a text continues nothing + regardless of what this returns. + + Parameters + ---------- + token : str + A token string from this model's tokenizer. + + Returns + ------- + bool + ``True`` when the token is a mid-word piece rather than the start of a new word. + """ + marker = self.word_start_marker() + if marker is None: + return token.startswith(CONTINUATION_MARKERS) + return not token.startswith(marker) and is_word_token(token) + + def is_substitutable_at(self, tokens: Sequence[str], index: int) -> bool: + """Whether ``tokens[index]`` is a whole word that can be swapped or dropped *in place*. + + The position-level form of :meth:`is_substitutable`, and the one counterfactual generators want + when choosing which positions to perturb. Word-hood cannot be decided from a token alone: a + multi-piece word's *head* is bare and alphabetic, so ``"gr"`` in ``["gr", "##ou", "##chy"]`` + passes :meth:`is_substitutable` while a substitution there rebuilds ``"superouchy"`` — malformed + text of exactly the kind both generators document themselves as avoiding. Deciding it needs the + *next* token, which a per-token predicate structurally cannot see. + + Position also settles the *opening* word, which the token-level rule gets wrong in the other + direction: byte-BPE marks a word start with ``Ġ`` but leaves the very first token of a text + bare, emitting ``["the", "Ġwaiter", ...]``. :meth:`is_substitutable` reads that bare ``"the"`` + as a mid-word piece, so the first word of *every* input was silently unperturbable on the whole + RoBERTa/GPT-2 family. At index 0 there is no preceding word to continue, so word-hood there is + the marker-free rule. + + :meth:`is_substitutable` remains the right check for a bare vocabulary entry being considered as + a *replacement*, where there is no surrounding sequence to consult. + + Parameters + ---------- + tokens : Sequence[str] + The full tokenization of the text. + index : int + Position into ``tokens`` to test. + + Returns + ------- + bool + ``True`` when the position holds a complete word, so replacing or removing it leaves the + rest of the text well-formed. + """ + # The index-0 clause only ever *adds* the opening token; it is redundant (not wrong) under a + # continuation-marking scheme, where ``is_substitutable`` is already the marker-free rule. + if not (self.is_substitutable(tokens[index]) or (index == 0 and is_word_token(tokens[index]))): + return False + return index + 1 >= len(tokens) or not self.continues_word(tokens[index + 1]) + class SupportsEmbeddings(ABC): """Capability: expose the input-embedding table and one dense vector per text. diff --git a/tests/unit_tests/compute/generators/test_ablation_flip.py b/tests/unit_tests/compute/generators/test_ablation_flip.py index 71b12608..8cea7a59 100644 --- a/tests/unit_tests/compute/generators/test_ablation_flip.py +++ b/tests/unit_tests/compute/generators/test_ablation_flip.py @@ -51,6 +51,32 @@ def detokenize(self, tokens): return " ".join(tokens) +class SubwordModel(BagOfWordsModel): + """WordPiece-style fake: some words split into pieces, so a word *head* is bare but not a word. + + ``BagOfWordsModel`` splits on whitespace, which can never reproduce the multi-piece case that made + both generators emit malformed text (``"grouchy"`` → ``["gr", "##ou", "##chy"]``, flipped to + ``"superouchy"``). + """ + + _PIECES = {"terrible": ["terr", "##ible"], "grouchy": ["gr", "##ou", "##chy"]} + + def tokenize(self, text): + pieces = [] + for word in text.split(): + pieces.extend(self._PIECES.get(word, [word])) + return pieces + + def detokenize(self, tokens): + out = "" + for token in tokens: + if token.startswith("##"): + out += token[2:] + else: + out = f"{out} {token}" if out else token + return out + + def _exact_ablation_scores(self, tokens, content_positions, orig_class): """Captum-free stand-in for ``_ablation_scores``: exact drop in ``orig_class`` per removed token.""" base = self.model.predict([self.model.detokenize(list(tokens))])[0][orig_class] @@ -245,5 +271,36 @@ def test_tokens_to_ignore_matches_the_bare_word_a_user_types(self): self.assertEqual(cfs, []) +class TestMultiPieceWordsAreNotStranded(unittest.TestCase): + """Dropping the *head* of a multi-piece word strands its continuations into a fragment. + + ``"terrible"`` tokenizes to ``["terr", "##ible"]``. Removing ``"terr"`` leaves ``"ible"`` — a word + the model never saw, which reads to the search as a large probability drop and so gets selected + eagerly. The position must be excluded, even at the cost of finding no counterfactual at all. + """ + + def setUp(self): + self.gen = AblationFlipGenerator(SubwordModel()) + self.gen._ablation_scores = _exact_ablation_scores.__get__(self.gen, AblationFlipGenerator) + + def test_word_head_is_not_an_ablation_candidate(self): + model = self.gen.model + tokens = model.tokenize("this is terrible") + self.assertEqual(tokens, ["this", "is", "terr", "##ible"]) + self.assertTrue(model.is_substitutable(tokens[2])) # token-level accepts the bare head + self.assertFalse(model.is_substitutable_at(tokens, 2)) # the position does not + + def test_never_returns_a_stranded_fragment(self): + # Pre-fix, "terr" was removable and this returned "this is ible but good" as a valid flip. + cfs = self.gen.generate("this is terrible but good", config={"num_examples": 5, "max_ablations": 2}) + for cf in cfs: + self.assertNotIn("ible", cf.new_text) + self.assertNotIn("ouchy", cf.new_text) + + def test_prefers_no_counterfactual_over_a_malformed_one(self): + # The only flip-enabling removal is the multi-piece word, which cannot be removed piecewise. + self.assertEqual(self.gen.generate("this is terrible but good", config={"max_ablations": 1}), []) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/model/test_model_base.py b/tests/unit_tests/model/test_model_base.py index f6bd6870..31ed482d 100644 --- a/tests/unit_tests/model/test_model_base.py +++ b/tests/unit_tests/model/test_model_base.py @@ -209,5 +209,76 @@ def test_is_word_token_is_the_marker_free_default(self): self.assertFalse(is_word_token("▁good")) # why it cannot be used alone +class TestIsSubstitutableAt(unittest.TestCase): + """A multi-piece word's *head* is a word token but not a substitutable *position*. + + ``"grouchy"`` tokenizes to ``["gr", "##ou", "##chy"]``; ``"gr"`` is bare and alphabetic, so every + token-level test accepts it, and substituting there rebuilt ``"superouchy"``. Only a check that can + see the following token rejects it. + """ + + def test_continuation_scheme_rejects_multi_piece_head(self): + m = _SchemeModel() + tokens = ["i", "am", "gr", "##ou", "##chy", "today"] + self.assertTrue(m.is_substitutable(tokens[2])) # token-level says yes... + self.assertFalse(m.is_substitutable_at(tokens, 2)) # ...position-level says no + self.assertEqual([i for i in range(len(tokens)) if m.is_substitutable_at(tokens, i)], [0, 1, 5]) + + def test_marked_scheme_rejects_multi_piece_head(self): + m = _SchemeModel(marker="▁") + tokens = ["▁i", "▁am", "▁grou", "chy", "▁today"] + self.assertTrue(m.is_substitutable(tokens[2])) + self.assertFalse(m.is_substitutable_at(tokens, 2)) + self.assertEqual([i for i in range(len(tokens)) if m.is_substitutable_at(tokens, i)], [0, 1, 4]) + + def test_marked_scheme_keeps_word_before_punctuation(self): + """Punctuation is unmarked but starts no word — the word before a comma stays perturbable. + + Treating every unmarked token as a continuation would silently make the last word of every + sentence, and any word before a comma, unsubstitutable. + """ + m = _SchemeModel(marker="Ġ") + tokens = ["Ġrude", ",", "Ġawful", "."] + self.assertTrue(m.is_substitutable_at(tokens, 0)) + self.assertTrue(m.is_substitutable_at(tokens, 2)) + + def test_final_position_has_no_successor(self): + m = _SchemeModel() + self.assertTrue(m.is_substitutable_at(["hello", "world"], 1)) + + def test_marked_scheme_accepts_the_unmarked_opening_word(self): + """byte-BPE leaves the first token of a text bare, so the marker test misreads it. + + ``"the waiter was rude"`` tokenizes to ``["the", "Ġwaiter", ...]``; judging ``"the"`` by the + marker made the opening word of every input unperturbable on the whole RoBERTa/GPT-2 family. + """ + m = _SchemeModel(marker="Ġ") + tokens = ["the", "Ġwaiter", "Ġwas", "Ġrude"] + self.assertFalse(m.is_substitutable(tokens[0])) # token-level still reads it as mid-word + self.assertTrue(m.is_substitutable_at(tokens, 0)) # position 0 knows better + + def test_opening_word_still_rejected_when_multi_piece(self): + """The index-0 clause relaxes the *marker* rule, not the whole-word rule. + + ``"rude waiters"`` tokenizes to ``["r", "ude", "Ġwait", "ers"]`` — ``"r"`` opens the text but + still only heads a multi-piece word, so substituting there would rebuild ``"Xude"``. + """ + m = _SchemeModel(marker="Ġ") + self.assertFalse(m.is_substitutable_at(["r", "ude", "Ġwait", "ers"], 0)) + + def test_unmarked_token_is_only_forgiven_at_index_zero(self): + m = _SchemeModel(marker="Ġ") + # Same bare token, mid-text: still a mid-word piece, still rejected. + self.assertFalse(m.is_substitutable_at(["Ġthe", "the", "Ġrude"], 1)) + + def test_continues_word_follows_the_scheme(self): + self.assertTrue(_SchemeModel().continues_word("##ou")) + self.assertFalse(_SchemeModel().continues_word("and")) + # Marked schemes invert it: bare = mid-word, marked = new word, punctuation = neither. + self.assertTrue(_SchemeModel(marker="▁").continues_word("chy")) + self.assertFalse(_SchemeModel(marker="▁").continues_word("▁and")) + self.assertFalse(_SchemeModel(marker="▁").continues_word(",")) + + if __name__ == "__main__": unittest.main() From 9e769973956c737de7b5874fd37b41dd5334a834 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 10 Sep 2026 17:34:18 +0200 Subject: [PATCH 109/144] refactor: add class attributes and erase constants in cf methods --- shapash/compute/generators/ablation_flip.py | 4 +-- shapash/compute/generators/base.py | 8 ++++++ shapash/compute/generators/hotflip.py | 13 +++++---- .../compute/generators/test_ablation_flip.py | 27 +++++++++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/shapash/compute/generators/ablation_flip.py b/shapash/compute/generators/ablation_flip.py index 767d6c0d..8eb7ace4 100644 --- a/shapash/compute/generators/ablation_flip.py +++ b/shapash/compute/generators/ablation_flip.py @@ -35,8 +35,6 @@ if TYPE_CHECKING: from collections.abc import Sequence -_MAX_ABLATABLE_TOKENS = 10 - class AblationFlipGenerator(CounterfactualGenerator): """Perturbation-based token-removal counterfactuals (Captum ``FeatureAblation``).""" @@ -93,7 +91,7 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No scores = self._ablation_scores(tokens, content_positions, orig_class) # Keep the most supportive positions (largest drop when removed) as removal candidates. - ranked = [content_positions[j] for j in np.argsort(-scores)][:_MAX_ABLATABLE_TOKENS] + ranked = [content_positions[j] for j in np.argsort(-scores)][: self.max_candidate_positions] return self.search_minimal( text, tokens, diff --git a/shapash/compute/generators/base.py b/shapash/compute/generators/base.py index c4027617..5d52d698 100644 --- a/shapash/compute/generators/base.py +++ b/shapash/compute/generators/base.py @@ -123,6 +123,14 @@ class CounterfactualGenerator(ABC): name: str = "counterfactual" #: Human-readable label for the webapp method selector (falls back to a title-cased ``name``). display_name: str = "Counterfactual" + #: How many ranked positions a generator may offer :meth:`search_minimal`. + #: + #: Deliberately **not** a :meth:`config_spec` field. ``max_flips`` already is one, and the two + #: multiply: the search scores ``sum(C(n, s) for s in 1..max_flips)`` combinations, so 10 positions + #: at ``max_flips=5`` is 637 candidate texts while 30 positions is 174,436 — an unbounded wait in a + #: blocking webapp callback, chosen by the person least placed to predict it. Overridable per + #: subclass or per instance (``generator.max_candidate_positions = 20``) for deliberate experiments. + max_candidate_positions: int = 10 def __init__(self, model: TextModel) -> None: self.model = model diff --git a/shapash/compute/generators/hotflip.py b/shapash/compute/generators/hotflip.py index 76eeaf4d..6d6ca579 100644 --- a/shapash/compute/generators/hotflip.py +++ b/shapash/compute/generators/hotflip.py @@ -43,9 +43,6 @@ has_capabilities, ) -_MAX_FLIPPABLE_TOKENS = 10 -_CANDIDATES_PER_POSITION = 50 - class _GradientModel(Protocol): """The exact capability surface HotFlip uses (predict + gradients + embeddings + tokenization).""" @@ -70,6 +67,12 @@ class HotFlipGenerator(CounterfactualGenerator): name = "hotflip" display_name = "HotFlip" + #: Vocabulary entries shortlisted per position by the linear estimate, before the model re-scores + #: them. Not a :meth:`config_spec` field: it is the dominant cost of a run (``positions x this`` + #: forward passes, far more than the search itself) and raising it changes neither the flip rate, + #: which saturates well below the default, nor the plausibility of the winning word — it only + #: reshuffles which candidate wins. Overridable per instance for deliberate experiments. + candidates_per_position: int = 50 def config_spec(self) -> dict[str, Field]: """Expose ``num_examples``, ``max_flips`` and ``tokens_to_ignore`` as tunable knobs.""" @@ -123,7 +126,7 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No ranked = np.argsort(-grad_l2).tolist() flippable = [ p for p in ranked if model.is_substitutable_at(tokens, p) and display_form(model, tokens[p]) not in ignore - ][:_MAX_FLIPPABLE_TOKENS] + ][: self.max_candidate_positions] vocab, matrix = model.get_embedding_table() replacement: dict[int, str] = {} @@ -138,7 +141,7 @@ def generate(self, text: str, target_label: str | None = None, config: dict | No cand_tok = vocab[int(cand)] if cand_tok != tokens[pos] and model.is_substitutable(cand_tok): shortlist.append(cand_tok) - if len(shortlist) >= _CANDIDATES_PER_POSITION: + if len(shortlist) >= self.candidates_per_position: break if not shortlist: continue diff --git a/tests/unit_tests/compute/generators/test_ablation_flip.py b/tests/unit_tests/compute/generators/test_ablation_flip.py index 8cea7a59..bba17315 100644 --- a/tests/unit_tests/compute/generators/test_ablation_flip.py +++ b/tests/unit_tests/compute/generators/test_ablation_flip.py @@ -271,6 +271,33 @@ def test_tokens_to_ignore_matches_the_bare_word_a_user_types(self): self.assertEqual(cfs, []) +class TestCandidatePositionCap(unittest.TestCase): + """The cap is one named attribute on the ABC, overridable without patching module globals. + + It was two module constants (``_MAX_ABLATABLE_TOKENS`` / ``_MAX_FLIPPABLE_TOKENS``) holding the + same value for the same purpose in two files, reachable only by monkeypatching. + """ + + def test_default_is_shared_by_both_generators(self): + from shapash.compute.generators import HotFlipGenerator + + self.assertEqual(AblationFlipGenerator.max_candidate_positions, 10) + self.assertEqual(HotFlipGenerator.max_candidate_positions, 10) + + def test_instance_override_limits_the_positions_searched(self): + gen = _make_generator() + gen.max_candidate_positions = 1 + # Only the single best removal is offered, so a flip needing any other token cannot be found. + cfs = gen.generate("this is great and good", config={"num_examples": 5, "max_ablations": 2}) + for cf in cfs: + self.assertEqual(len(cf.flipped_positions), 1) + + def test_override_does_not_leak_to_other_instances(self): + gen = _make_generator() + gen.max_candidate_positions = 2 + self.assertEqual(_make_generator().max_candidate_positions, 10) + + class TestMultiPieceWordsAreNotStranded(unittest.TestCase): """Dropping the *head* of a multi-piece word strands its continuations into a fragment. From 2c5180117028ad12fcf768a74035db27abd6fe44 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 11 Sep 2026 15:07:36 +0200 Subject: [PATCH 110/144] doc(notebook): fix save and load, change projection --- tutorial/nlp/overview.ipynb | 86 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/tutorial/nlp/overview.ipynb b/tutorial/nlp/overview.ipynb index 8df052f5..6c090952 100644 --- a/tutorial/nlp/overview.ipynb +++ b/tutorial/nlp/overview.ipynb @@ -119,7 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "xpl.backend" + "xpl.backend.name" ] }, { @@ -225,24 +225,17 @@ "metadata": {}, "outputs": [], "source": [ + "# texts associated with the contribution values\n", + "explanations.texts\n", + "\n", + "# token strings associated with the contribution values\n", + "explanations.token_strings\n", + "\n", "# shap base values\n", "explanations.base_values\n", "\n", "# raw contributions for each token of the input texts: shape (n_samples, n_tokens, n_classes)\n", - "explanations.values\n", - "\n", - "# token strings associated with the contribution values\n", - "explanations.token_strings" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af5ceb97", - "metadata": {}, - "outputs": [], - "source": [ - "explanations.texts" + "explanations.values" ] }, { @@ -293,27 +286,6 @@ "explanations.word_occurrences(\"happy\")" ] }, - { - "cell_type": "markdown", - "id": "0ebe96bb", - "metadata": {}, - "source": [ - "Saving and loading explanations:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "863765cf", - "metadata": {}, - "outputs": [], - "source": [ - "explanations.save(my_save_path)\n", - "\n", - "from shapash.explainer.nlp_explanation import NlpExplanation\n", - "explanations = NlpExplanation.load(my_save_path)" - ] - }, { "cell_type": "markdown", "id": "bdacdb5a", @@ -451,11 +423,43 @@ "metadata": {}, "outputs": [], "source": [ - "# Custom projection (PaCMAP, UMAP, TSNE, etc.) with optional parameters\n", - "import pacmap\n", - "reducer = pacmap.PaCMAP(n_components=2, n_neighbors=1, MN_ratio=0.5, FP_ratio=2.0)\n", + "# Custom projection (TSNE, PaCMAP, UMAP, etc.) with optional parameters\n", + "from sklearn.manifold import TSNE\n", + "reducer = TSNE(n_components=2, perplexity=2, random_state=42)\n", + "\n", + "xpl.compute_projection(explanations, reducer=reducer)" + ] + }, + { + "cell_type": "markdown", + "id": "48578f32", + "metadata": {}, + "source": [ + "## Saving and loading explanations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5ca7fd2", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "my_save_path = Path(\"explanations.xpl\")\n", "\n", - "xpl.compute_projection(explanations, reducer=reducer, init=\"pca\")" + "explanations.save(my_save_path, scatter_xy=scatter_xy)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93a84be9", + "metadata": {}, + "outputs": [], + "source": [ + "from shapash.explainer.nlp_explanation import NlpExplanation\n", + "explanations, scatter_xy = NlpExplanation.load(my_save_path)" ] }, { @@ -546,7 +550,7 @@ "metadata": {}, "outputs": [], "source": [ - "app = xpl.run_app(explanations)" + "app = xpl.run_app(explanations, scatter_xy=scatter_xy)" ] }, { From 45c6328f4f29f225f3b2a5e785adef8a336e7a32 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 14 Sep 2026 14:08:32 +0200 Subject: [PATCH 111/144] refactor(nlp): split projection and embeddings into a dedicated Embedding object - NlpWebApp(explanation, engine, projection) builds a private AppContext and checks the projection once via projection_coords - Move hash_corpus to compute/hashing.py and drop the re-export from embedding_store --- demo/serve_nlp.py | 49 +++-- shapash/compute/embedding_store.py | 134 +++++------- shapash/compute/embeddings.py | 199 ++++++++++++++++++ shapash/compute/hashing.py | 30 +++ shapash/compute/retrieval/similar_examples.py | 2 +- shapash/explainer/nlp_explainer.py | 187 ++++++++-------- shapash/explainer/nlp_explanation.py | 97 ++++----- shapash/explainer/nlp_plotter.py | 27 ++- shapash/model/base.py | 2 +- shapash/webapp/nlp_app.py | 94 ++++----- shapash/webapp/nlp_components/base.py | 69 +++--- .../webapp/nlp_components/counterfactual.py | 6 +- shapash/webapp/nlp_components/data_editor.py | 5 +- .../webapp/nlp_components/error_analysis.py | 6 +- shapash/webapp/nlp_components/label_noise.py | 6 +- shapash/webapp/nlp_components/scatter.py | 34 +-- .../nlp_components/sentence_highlight.py | 6 +- .../webapp/nlp_components/similar_examples.py | 6 +- shapash/webapp/nlp_components/waterfall.py | 5 +- .../webapp/nlp_components/word_importance.py | 6 +- shapash/webapp/nlp_components/word_profile.py | 6 +- .../retrieval/test_similar_examples.py | 2 +- .../compute/test_embedding_store.py | 60 ++++-- tests/unit_tests/compute/test_embeddings.py | 167 +++++++++++++++ tests/unit_tests/nlp/test_nlp_explanation.py | 95 +++++---- tests/unit_tests/nlp/test_nlp_plotter.py | 32 +++ tests/unit_tests/nlp/test_nlp_prototype.py | 167 ++++++++++----- .../unit_tests/webapp/test_nlp_whatif_app.py | 61 +++--- tutorial/nlp/overview.ipynb | 95 ++++++--- 29 files changed, 1131 insertions(+), 524 deletions(-) create mode 100644 shapash/compute/embeddings.py create mode 100644 shapash/compute/hashing.py create mode 100644 tests/unit_tests/compute/test_embeddings.py diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 2c8ac379..5da1fbe9 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -21,9 +21,11 @@ model. This script caches both to ``--cache-dir`` (keyed by a hash of the input texts): * **Contributions + predictions** — via ``explain(..., cache_dir=...)``. -* **The text embeddings and the 2-D projection** — via ``compute_projection(explanation, cache_dir=...)``. - The library owns the embedding + caching (so the scatter and the similar-example neighbours are - always in the same space); this script only supplies the reducer, PaCMAP. +* **The text embeddings** — via ``compute_embeddings(explanation, cache_dir=...)``. The library owns + the embedding + caching (so the scatter and the similar-example neighbours are always in the same + space). +* **The 2-D projection** — reduced with PaCMAP and saved explicitly with ``Embedding.save``, since a + stochastic reducer would otherwise give a different layout on every restart. The caching is automatic: each run **loads** the cache if one exists for these exact texts, otherwise it **computes and writes** it. So the first run is slow; every run after @@ -39,14 +41,22 @@ from shapash.explainer.nlp_explanation import NlpExplanation from shapash.webapp.nlp_app import NlpWebApp - explanation, scatter = NlpExplanation.load("…/nlp_shap/<hash>.xpl") - NlpWebApp(explanation, engine=None, scatter_xy=scatter).run(port=8050) + explanation = NlpExplanation.load("…/nlp_shap/<hash>.xpl") + NlpWebApp(explanation, engine=None).run(port=8050) + +The scatter is a separate file, because a projection is a fact about the *embeddings* rather than +about the explanation — different key, replaceable without any of the contributions changing:: + + from shapash.compute.embeddings import Embedding + + projection = Embedding.load("…/projection.<space>.npz") + NlpWebApp(explanation, engine=None, projection=projection).run(port=8050) One caveat, and it is deliberate rather than an oversight: **the cached artifact carries no ground truth.** The cache key covers ``(texts, model, backend)``, and ``y`` is not a function of that key — it belongs to whoever called ``explain``. Serving a cache file directly therefore self-disables the confusion matrix and the label-noise panel. For a shareable snapshot *with* labels, save the object -``explain`` returned: ``explanation.save(path, scatter_xy=projected)``. +``explain`` returned: ``explanation.save(path)`` (and, alongside it, ``projection.save(...)``). The model runs on CPU when no GPU is present, so this serves fine on a plain box. @@ -84,8 +94,8 @@ **method dropdown in the webapp**, so there is no CLI flag for them. They run live and are not cached. The on-disk cache mirrors these dependencies as a hierarchy under ``--cache-dir``: -``<model>/<dataset>__<split>/`` holds the (backend-independent) embedding-store artifacts — the -vectors and the projection derived from them — and a per-backend ``<...>/nlp_shap/`` or +``<model>/<dataset>__<split>/`` holds the (backend-independent) embeddings and the projection derived +from them — and a per-backend ``<...>/nlp_shap/`` or ``<...>/nlp_captum_lig/`` subdirectory holds that method's ``<hash>.xpl`` contributions. Serving a *private* model and dataset (both local, nothing on the hub) @@ -247,6 +257,7 @@ from torch import nn from shapash.backend import NlpCaptumLigBackend +from shapash.compute.embeddings import Embedding from shapash.explainer.nlp_explainer import NlpExplainer from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel @@ -929,9 +940,8 @@ def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend def build_reducer() -> pacmap.PaCMAP: """The dimensionality reducer for the scatter — a *demo* choice, not a library one. - ``NlpExplainer.compute_projection`` owns everything that must stay consistent (which space the - texts are embedded in, and the caching of both the vectors and the coordinates) and takes the - reducer as an argument, because which manifold method suits your data is a modelling decision. + ``NlpExplainer.compute_embeddings`` owns everything that must stay consistent (which space the + texts are embedded in, and the caching of the vectors); the reducer is applied on top, because which manifold method suits your data is a modelling decision. PaCMAP is a good default for text clusters; swap in UMAP, t-SNE, or drop the argument entirely for the built-in PCA. """ @@ -989,7 +999,7 @@ def main() -> None: # Drop this text set's cached contributions so the steps below recompute + overwrite. The # explainer owns the key (it covers the model and backend), so it also owns the deletion — # only *this* model+backend's entry goes, other backends' caches are left intact. The - # embeddings + projection are dropped by compute_projection(recompute=True) below. + # embeddings are dropped by compute_embeddings(recompute=True) below, the projection is re-fitted. logger.info("--recompute: dropping cached %s contributions + projection for these texts", config.attribution) xpl.clear_cache(sentences, explain_dir) @@ -1011,9 +1021,16 @@ def main() -> None: explanation = xpl.explain(sentences, y=y_true, cache_dir=explain_dir) # Step 4 — everything downstream reads the artifact as an argument. - projected = xpl.compute_projection( - explanation, reducer=build_reducer(), cache_dir=dataset_dir, recompute=config.recompute - ) + # Saved per space, so switching --embedding-space never reloads a layout drawn in another one, and + # refitted when the texts changed (e.g. a different --n-samples): corpus_id is what tells. + projection_file = dataset_dir / f"projection.{model.resolve_space()}.npz" + projection = Embedding.load(projection_file) if projection_file.exists() and not config.recompute else None + if projection is not None and projection.corpus_id == explanation.corpus_id: + logger.info("Projection loaded from %s", projection_file) + else: + embeddings = xpl.compute_embeddings(explanation, cache_dir=dataset_dir, recompute=config.recompute) + projection = embeddings.project(build_reducer()) + projection.save(projection_file) # (No bank warm-up here: fit(precompute=True) above already embedded the reference corpus, under # the same condition this used to test — can_find_similar() is "a retriever was built", which is @@ -1041,7 +1058,7 @@ def main() -> None: port=config.port, debug=False, host=config.host, - scatter_xy=projected, + projection=projection, url_base_pathname=config.url_base_path, info={"Dataset": dataset_label}, ) diff --git a/shapash/compute/embedding_store.py b/shapash/compute/embedding_store.py index 2c4207ed..d1b15b3b 100644 --- a/shapash/compute/embedding_store.py +++ b/shapash/compute/embedding_store.py @@ -6,43 +6,26 @@ one cache with one key — otherwise each caller invents its own filename and they drift, which is how a bank built in the ``"decision"`` space ends up reloaded for a scatter drawn in ``"pooled"``. -:class:`EmbeddingStore` is that one place. It owns the key and the on-disk layout; callers ask for -:meth:`~EmbeddingStore.vectors` and, when they derive something further from them (a 2-D projection), -park it next to the embeddings under the same key via :meth:`~EmbeddingStore.cached_array`. +Entries are :class:`~shapash.compute.embeddings.Embedding` files, so a cache entry can be copied out +and loaded with :meth:`Embedding.load`. Projections are not cached here: reducing is cheap next to +embedding, and a layout worth keeping is saved explicitly with :meth:`Embedding.save`. """ from __future__ import annotations -import hashlib import logging -from collections.abc import Callable, Sequence +from collections.abc import Sequence from pathlib import Path import numpy as np +from shapash.compute.embeddings import Embedding +from shapash.compute.hashing import hash_corpus from shapash.model.base import EmbeddingSource -logger = logging.getLogger(__name__) - - -def hash_corpus(texts: Sequence[str], key: str) -> str: - """Stable digest over an identity ``key`` plus the corpus texts (order-sensitive). - - Each string is framed with its own byte length before hashing, so no separator choice can - let two different inputs collide — a literal ``"\\0"`` inside a text can no longer make - ``["a\\0b"]`` and ``["a", "b"]`` hash identically. ``key`` carries whatever else changes the - cached artifact (model identity, representation space, explanation backend). +__all__ = ["EmbeddingStore"] - Shared by every disk cache in the NLP path — the embedding/projection artifacts here and the - contribution cache in :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain` — so all of - them key on the same, collision-safe rule. - """ - h = hashlib.md5(usedforsecurity=False) - for s in (key, *texts): - data = s.encode() - h.update(len(data).to_bytes(8, "big")) - h.update(data) - return h.hexdigest() +logger = logging.getLogger(__name__) class EmbeddingStore: @@ -55,7 +38,7 @@ class EmbeddingStore: texts : sequence of str The corpus to embed. Fixed for the lifetime of the store — the cache key is derived from it. cache_dir : str or Path or None, optional - Where cached ``.npy`` files live. When ``None`` the store still memoizes in memory but writes + Where cached ``.npz`` files live. When ``None`` the store still memoizes in memory but writes nothing, so a fresh process recomputes. Notes @@ -81,10 +64,10 @@ def __init__( self.model = model self.texts = list(texts) self.cache_dir = Path(cache_dir) if cache_dir is not None else None - # Keyed by (key, tag), not tag alone: ``key`` folds in the model's *current* space, which is - # assignable at runtime (EncoderClassifierModel.embedding_space). A tag-only memo would answer - # from the old space after such a switch and never consult the correctly-keyed file on disk. - self._arrays: dict[tuple[str, str], np.ndarray] = {} + # Keyed by the full key, not a single slot: the key folds in the model's *current* space, + # which is assignable at runtime (EncoderClassifierModel.embedding_space). A single slot would + # answer from the old space after such a switch. + self._memo: dict[str, Embedding] = {} @property def space_key(self) -> str: @@ -96,66 +79,49 @@ def key(self) -> str: """The cache key: model identity, effective space, and corpus digest.""" return hash_corpus(self.texts, self.space_key) - def path(self, tag: str) -> Path | None: - """On-disk location of the ``tag`` artifact, or ``None`` when caching is off.""" + @property + def path(self) -> Path | None: + """On-disk location of the cached embeddings, or ``None`` when caching is off.""" if self.cache_dir is None: return None - return self.cache_dir / f"{self.key}.{tag}.npy" + return self.cache_dir / f"{self.key}.emb.npz" def vectors(self) -> np.ndarray: """Return ``(n_texts, hidden_dim)`` embeddings in the model's current space.""" - return self.cached_array("emb", lambda: np.asarray(self.model.embed(self.texts))) - - def cached_array(self, tag: str, compute: Callable[[], np.ndarray]) -> np.ndarray: - """Return the ``tag`` array for this corpus, loading it or computing and storing it. - - Parameters - ---------- - tag : str - Names the artifact within this store's key (``"emb"`` for the embeddings themselves, - e.g. ``"pca-1f3c.proj"`` for a projection derived from them). Anything that changes the - array's contents but not the key must be reflected here. - compute : callable - Produces the array on a cache miss. Called at most once per process. - - Returns - ------- - np.ndarray - The cached array. Memoized in memory per ``(key, tag)``, so repeat calls are free while - the model's space is unchanged, and a space switch re-reads (or recomputes) rather than - returning the previous space's array. - """ - memo = (self.key, tag) - if memo in self._arrays: - return self._arrays[memo] - cache_file = self.path(tag) + return self.embedding().vectors + + def embedding(self) -> Embedding: + """Return this corpus's embeddings, loading them from cache or computing and caching them.""" + key = self.key + if key in self._memo: + return self._memo[key] + + cache_file = self.path if cache_file is not None and cache_file.exists(): - logger.info("Embedding store hit (%s) — loading %s", tag, cache_file) - self._arrays[memo] = np.load(cache_file) - return self._arrays[memo] - - logger.info("Embedding store miss (%s) — computing over %d texts (%s)", tag, len(self.texts), self.space_key) - array = compute() - self._arrays[memo] = array - if cache_file is not None: - cache_file.parent.mkdir(parents=True, exist_ok=True) - np.save(cache_file, array) - logger.info("Embedding store cached (%s) to %s", tag, cache_file) - return array + logger.info("Embedding store hit — loading %s", cache_file) + embedding = Embedding.load(cache_file) + else: + logger.info("Embedding store miss — computing over %d texts (%s)", len(self.texts), self.space_key) + embedding = Embedding( + vectors=np.asarray(self.model.embed(self.texts)), + model_id=self.model.model_id, + space=str(self.model.resolve_space()), + corpus_id=hash_corpus(self.texts), + ) + if cache_file is not None: + embedding.save(cache_file) + logger.info("Embedding store cached to %s", cache_file) + self._memo[key] = embedding + return embedding def clear(self) -> None: - """Drop this corpus's cached artifacts — every in-memory one, and on disk those under this key. - - Removes *all* tags under this key, not just the embeddings, so a caller forcing a recompute - cannot leave a projection behind that was derived from vectors no longer on disk. The disk - sweep is scoped to the key, and therefore to the model's **current** space: files written for - a space the model has since moved off are not touched. They are already unreachable by - lookup, and re-selecting that space finds them again — which is the useful behaviour, not an - oversight. In memory the sweep is unconditional (every space). + """Drop the cached embeddings — every in-memory entry, and the on-disk file for the current space. + + Files written for a space the model has since moved off are left alone: they are unreachable + by lookup, and re-selecting that space finds them again. """ - self._arrays.clear() - if self.cache_dir is None: - return - for stale in self.cache_dir.glob(f"{self.key}.*.npy"): - stale.unlink(missing_ok=True) - logger.info("Embedding store dropped %s", stale) + self._memo.clear() + cache_file = self.path + if cache_file is not None and cache_file.exists(): + cache_file.unlink() + logger.info("Embedding store dropped %s", cache_file) diff --git a/shapash/compute/embeddings.py b/shapash/compute/embeddings.py new file mode 100644 index 00000000..79c53d06 --- /dev/null +++ b/shapash/compute/embeddings.py @@ -0,0 +1,199 @@ +"""``Embedding`` — one vector per text, with the provenance needed to pair it back to its texts. + +The companion artifact to :class:`~shapash.explainer.nlp_explanation.NlpExplanation`. The same class +holds raw model output (``reducer_tag=None``) and a 2-D projection of it (``reducer_tag`` names the +reducer): same shape ``(n, k)``, same provenance, same file. It is saved separately from the +explanation because it has a different key — *(texts, model, space)* rather than +*(texts, model, backend)* — and can be replaced without any contribution changing. + +Persistence is a single ``.npz`` (the vectors plus a JSON ``meta`` entry), read back with +``allow_pickle=False``. + +Lives under ``shapash/compute`` because :class:`~shapash.compute.embedding_store.EmbeddingStore` +returns it, and nothing in ``shapash/compute`` may depend on ``shapash/explainer``. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +from sklearn.decomposition import PCA + +from shapash.__version__ import __version__ as _shapash_version + + +@dataclass(frozen=True, eq=False, slots=True) +class Embedding: + """Vectors over a corpus, carrying the model, space and corpus they came from. + + Parameters + ---------- + vectors : np.ndarray + ``(n_samples, n_components)``. Sealed read-only on construction, like the arrays of + ``NlpExplanation``: the store hands the same instance to every caller. + model_id : str + The model's own identity string (see :attr:`~shapash.model.base.EmbeddingSource.model_id`). + space : str + The resolved representation space the vectors were read from (e.g. ``"pooled"``). + corpus_id : str + Digest of the texts alone (:func:`~shapash.compute.hashing.hash_corpus`). Row ``i`` is text + ``i``, so this is what pairs the embedding with an explanation of the same texts. + reducer_tag : str or None, optional + ``None`` for raw model output; otherwise the name of the reducer that produced these vectors. + + Examples + -------- + >>> emb = xpl.compute_embeddings(explanation) # doctest: +SKIP + >>> proj = emb.project() # PCA(2) by default # doctest: +SKIP + >>> proj.save("amazon.proj.npz") # doctest: +SKIP + """ + + vectors: np.ndarray + model_id: str + space: str + corpus_id: str + reducer_tag: str | None = None + + def __post_init__(self) -> None: + vectors = np.asarray(self.vectors) + if vectors.ndim != 2: + raise ValueError(f"vectors must be 2-D (n_samples, n_components), got shape {vectors.shape}") + vectors.setflags(write=False) + object.__setattr__(self, "vectors", vectors) + + @property + def n_samples(self) -> int: + """Number of texts — rows of :attr:`vectors`.""" + return int(self.vectors.shape[0]) + + @property + def n_components(self) -> int: + """Dimensionality — columns of :attr:`vectors`. 2 for a plottable projection.""" + return int(self.vectors.shape[1]) + + def project(self, reducer: Any = None, **fit_transform_kwargs: Any) -> Embedding: + """Reduce these vectors with *reducer*, returning another :class:`Embedding`. + + Model-free and cheap compared with embedding: it only reads :attr:`vectors`. Most reducers + other than PCA are stochastic, so :meth:`save` a layout you want to keep. + + Parameters + ---------- + reducer : object, optional + Anything with ``fit_transform`` (sklearn's ``PCA``/``TSNE``, ``pacmap.PaCMAP``, + ``umap.UMAP``). Defaults to ``PCA(n_components=2)``. + **fit_transform_kwargs + Passed through to ``reducer.fit_transform`` (e.g. PaCMAP's ``init="pca"``). + + Returns + ------- + Embedding + Same model, space and corpus; reduced vectors, ``reducer_tag`` set to the reducer's + class name. + + Raises + ------ + ValueError + If the reducer does not return one row per text. + """ + if reducer is None: + reducer = PCA(n_components=2) + coords = np.asarray(reducer.fit_transform(self.vectors, **fit_transform_kwargs)) + if coords.ndim != 2 or coords.shape[0] != self.n_samples: + raise ValueError( + f"a reduction must keep one row per text: got shape {coords.shape} from {self.n_samples} samples." + ) + return Embedding( + vectors=coords, + model_id=self.model_id, + space=self.space, + corpus_id=self.corpus_id, + reducer_tag=type(reducer).__name__.lower(), + ) + + def save(self, path: str | Path) -> None: + """Write this embedding to a single ``.npz`` file, exactly at *path* (no suffix appended).""" + meta = { + "shapash_version": _shapash_version, + "model_id": self.model_id, + "space": self.space, + "corpus_id": self.corpus_id, + "reducer_tag": self.reducer_tag, + } + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + # Through an open handle: np.savez appends ".npz" to a path given by name that lacks it. + with path.open("wb") as fh: + np.savez(fh, vectors=self.vectors, meta=json.dumps(meta, ensure_ascii=False)) + + @classmethod + def load(cls, path: str | Path) -> Embedding: + """Read an embedding written by :meth:`save` (or cached by ``EmbeddingStore``).""" + with np.load(Path(path), allow_pickle=False) as archive: + meta = json.loads(str(archive["meta"].item())) + vectors = archive["vectors"] + return cls( + vectors=vectors, + model_id=meta["model_id"], + space=meta["space"], + corpus_id=meta["corpus_id"], + reducer_tag=meta.get("reducer_tag"), + ) + + def __repr__(self) -> str: + # model_id and corpus_id are identity strings for cache lookups (a long fingerprint, an + # md5 digest) — useful to compare, not to read. reducer_tag says whether this is a + # plottable 2-D layout or raw model output, which is what you'd actually check by eye; + # space is shown only for raw vectors, since a projection's reducer already implies one. + if self.reducer_tag: + return ( + f"Embedding(n_samples={self.n_samples}, n_components={self.n_components}, reducer={self.reducer_tag!r})" + ) + return f"Embedding(n_samples={self.n_samples}, n_components={self.n_components}, space={self.space!r})" + + +def projection_coords(projection: Embedding | np.ndarray, explanation: Any) -> np.ndarray: + """Check *projection* can be drawn for *explanation* and return its ``(n_samples, 2)`` coordinates. + + The single check shared by ``explanation.plot.scatter`` and the webapp. An :class:`Embedding` must + come from the same texts (``corpus_id``) — otherwise every point is labelled from the wrong + sample. A bare array is accepted on its shape alone, as the escape hatch for coordinates produced + outside shapash. + + Parameters + ---------- + projection : Embedding or np.ndarray + The 2-D projection. + explanation : NlpExplanation + The explanation it is drawn with (anything with ``n_samples`` and ``corpus_id``). + + Returns + ------- + np.ndarray + + Raises + ------ + ValueError + If the projection is over different texts or is not ``(n_samples, 2)``. + """ + if isinstance(projection, Embedding): + if projection.corpus_id != explanation.corpus_id: + raise ValueError( + "projection was built from different texts than the explanation " + f"(projection.corpus_id={projection.corpus_id[:12]}…, " + f"explanation.corpus_id={explanation.corpus_id[:12]}…)." + ) + coords = projection.vectors + else: + coords = np.asarray(projection) + expected = (explanation.n_samples, 2) + if coords.shape != expected: + raise ValueError( + f"projection must have shape {expected}, got {coords.shape}. " + "Reduce an embedding to 2-D first: embedding.project(reducer)." + ) + return coords diff --git a/shapash/compute/hashing.py b/shapash/compute/hashing.py new file mode 100644 index 00000000..f190c97d --- /dev/null +++ b/shapash/compute/hashing.py @@ -0,0 +1,30 @@ +"""Corpus digests shared by every NLP cache key and by ``corpus_id``. + +A dependency-free leaf module, so the persistence layer (``NlpExplanation``) can compute a corpus id +without importing the model or embedding layers. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Sequence + + +def hash_corpus(texts: Sequence[str], key: str = "") -> str: + """Stable digest over an identity ``key`` plus the corpus texts (order-sensitive). + + Each string is framed with its own byte length before hashing, so no separator choice can + let two different inputs collide — a literal ``"\\0"`` inside a text can no longer make + ``["a\\0b"]`` and ``["a", "b"]`` hash identically. ``key`` carries whatever else changes the + cached artifact (model identity, representation space, explanation backend). + + With the default empty ``key`` the digest depends on the texts alone: that is + :attr:`~shapash.explainer.nlp_explanation.NlpExplanation.corpus_id`, the value that pairs an + embedding or projection with the explanation of the same texts. + """ + h = hashlib.md5(usedforsecurity=False) + for s in (key, *texts): + data = s.encode() + h.update(len(data).to_bytes(8, "big")) + h.update(data) + return h.hexdigest() diff --git a/shapash/compute/retrieval/similar_examples.py b/shapash/compute/retrieval/similar_examples.py index 217f8abd..2ab17834 100644 --- a/shapash/compute/retrieval/similar_examples.py +++ b/shapash/compute/retrieval/similar_examples.py @@ -83,7 +83,7 @@ class SimilarExampleRetriever: This matches the 2-D scatter *when the scatter is built by* :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection`, which reads the same space through the same :class:`~shapash.compute.embedding_store.EmbeddingStore`. Coordinates passed - in by hand via ``run_app(scatter_xy=...)`` are outside that guarantee. + in by hand via ``run_app(projection=...)`` are outside that guarantee. Examples -------- diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 872f3362..bca8d418 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -16,15 +16,15 @@ from __future__ import annotations -import hashlib +import logging import threading +import zipfile from dataclasses import replace from pathlib import Path from typing import Any, cast import numpy as np import pandas as pd -from sklearn.decomposition import PCA from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend @@ -34,10 +34,12 @@ has_usable_probabilities, ) from shapash.compute.diagnostics.label_probe import LabelProbe -from shapash.compute.embedding_store import EmbeddingStore, hash_corpus +from shapash.compute.embedding_store import EmbeddingStore +from shapash.compute.embeddings import Embedding from shapash.compute.generators.ablation_flip import AblationFlipGenerator from shapash.compute.generators.base import Counterfactual, CounterfactualGenerator, Field from shapash.compute.generators.hotflip import HotFlipGenerator +from shapash.compute.hashing import hash_corpus from shapash.compute.retrieval.similar_examples import Neighbor, SimilarExampleRetriever from shapash.explainer.nlp_explanation import NlpExplanation from shapash.model.base import SupportsEmbeddings, SupportsTokenization, TextModel, has_capabilities @@ -45,6 +47,8 @@ from shapash.webapp.nlp_app import NlpWebApp from shapash.webapp.utils.launch import RunningApp +logger = logging.getLogger(__name__) + # Built-in counterfactual generators, in preference order: HotFlip (gradient-based, richer # substitutions) first, AblationFlip (forward-pass-only removal) as the broader fallback. Every entry # compatible with the bound model is offered in the webapp's method selector. @@ -60,26 +64,24 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.xpl" -def _reducer_tag(reducer: object, fit_transform_kwargs: dict[str, Any] | None = None) -> str: - """Name a reducer for a cache tag: its class plus a digest of its settings. +def _load_cached(cache_path: Path | None) -> NlpExplanation | None: + """Read a cached explanation, or return ``None`` when the entry cannot be read. + + An unreadable entry is a **miss, not a failure**: the cache key covers texts/model/backend but + not the shapash version, so a layout change leaves entries the new reader cannot parse, and a + cache exists so the caller need not care whether the answer was on disk. + :meth:`NlpExplanation.load` called directly still raises. - Two runs of the same reducer class with different settings produce different coordinates, so the - class name alone would silently reload the wrong scatter. "Settings" covers both the reducer's own - constructor parameters — read via ``get_params()`` (sklearn's convention, which ``pacmap`` also - follows) — and any ``fit_transform_kwargs`` a caller passed at call time (e.g. PaCMAP's - ``init="pca"``), since those affect the output identically but live outside ``get_params()``. A - reducer with no ``get_params()`` and no ``fit_transform_kwargs`` is keyed by class name only — - documented on :meth:`NlpExplainer.compute_projection` as needing ``recompute``. + Scoped to the errors a stale or damaged entry produces (a missing meta key or parquet member, + a truncated zip) rather than bare ``Exception``, so a genuine reader bug still surfaces. """ - name = type(reducer).__name__.lower() - get_params = getattr(reducer, "get_params", None) - if get_params is None and not fit_transform_kwargs: - return name - params = dict(get_params()) if get_params is not None else {} - if fit_transform_kwargs: - params["__fit_transform_kwargs__"] = sorted(fit_transform_kwargs.items()) - digest = hashlib.md5(repr(sorted(params.items())).encode(), usedforsecurity=False).hexdigest() - return f"{name}-{digest[:8]}" + if cache_path is None or not cache_path.exists(): + return None + try: + return NlpExplanation.load(cache_path) + except (KeyError, ValueError, OSError, zipfile.BadZipFile) as exc: + logger.warning("Ignoring unreadable explanation cache entry %s (%s) — recomputing.", cache_path, exc) + return None class NlpExplainer: @@ -356,10 +358,9 @@ def explain( computed = cached[1] else: cache_path = _cache_file(new_hash, Path(cache_dir)) if cache_dir is not None else None + computed = _load_cached(cache_path) - if cache_path is not None and cache_path.exists(): - computed, _ = NlpExplanation.load(cache_path) - else: + if computed is None: contributions = self.backend.run_explainer(text_list) pred_df = self._predict(text_list, texts.index) y_pred, y_prob = pred_df["prediction"], pred_df.drop(columns=["prediction"]) @@ -442,72 +443,51 @@ def clear_cache(self, x: list[str] | pd.Series, cache_dir: str | Path) -> None: self.cache_path(x, cache_dir).unlink(missing_ok=True) self._computed_cache = None - def compute_projection( + def compute_embeddings( self, explanation: NlpExplanation, - reducer=None, cache_dir: str | Path | None = None, recompute: bool = False, - **fit_transform_kwargs: Any, - ): - """Return a 2-D projection of ``explanation``'s texts, ready to pass to :meth:`run_app`. + ) -> Embedding: + """Embed ``explanation``'s texts with the bound model, cached. - The library owns the parts that must stay consistent — *which* space the texts are embedded in - and how that is cached — while the caller injects the dimensionality reducer, which is a - modelling choice shapash has no business picking for you. Embedding goes through the model's - current ``embedding_space``, so the scatter and the similar-example neighbours are guaranteed - to sit in the same space; they even share the cached vectors. + The expensive half of the scatter: it needs the live model and a pass over every text. The + result is model-free and can be reduced any number of times with :meth:`Embedding.project`. Parameters ---------- explanation : NlpExplanation - The result of :meth:`explain` for the batch to project. - reducer : object, optional - Anything with ``fit_transform(X) -> (n_samples, 2)`` — ``sklearn`` PCA/TSNE, ``pacmap``, - ``umap``. Defaults to :class:`sklearn.decomposition.PCA` with two components (sklearn is - already a core dependency, so the default costs no extra install). + The result of :meth:`explain` for the batch to embed. cache_dir : str or Path, optional - When given, both the embeddings and the projected coordinates are persisted here and - reloaded on later runs, so only the first call pays the cost. The key covers the model, the - effective space, the texts, and the reducer's class + parameters (including - ``fit_transform_kwargs`` below). + When given, the vectors are persisted here and reloaded on later runs. The key covers + the model, the effective space and the texts — the same key + :meth:`find_similar`'s reference bank uses, so the scatter and the neighbours share one + embedding space and, when the corpora coincide, one file. recompute : bool, optional - Drop this text set's cached artifacts first, forcing a fresh embed + fit. - **fit_transform_kwargs - Forwarded to ``reducer.fit_transform(vectors, **fit_transform_kwargs)`` — for arguments a - reducer only accepts at call time rather than at construction, e.g. PaCMAP's - ``init="pca"``. Part of the cache key (see Notes), so changing them busts the cache the - same way changing the reducer's own constructor parameters does. + Drop this text set's cached embeddings first, forcing a fresh embed. Returns ------- - np.ndarray, shape (n_samples, 2) - Coordinates aligned with the compiled texts. + Embedding + ``(n_samples, hidden_dim)`` vectors carrying the model, space and corpus they came + from. Its ``corpus_id`` equals ``explanation.corpus_id``. - Notes - ----- - The reducer's contribution to the cache key is its class name plus a digest of ``get_params()`` - (when it exposes one — sklearn and pacmap both do) and of ``fit_transform_kwargs``. A reducer - with neither is keyed by class name alone, so re-tuning such a reducer needs ``recompute=True`` - to take effect. + Raises + ------ + TypeError + If the bound model cannot produce embeddings (no ``SupportsEmbeddings``). Examples -------- - >>> explanation = xpl.explain(texts) - >>> xy = xpl.compute_projection( - ... explanation, reducer=pacmap.PaCMAP(n_components=2), cache_dir="cache/", init="pca" - ... ) - >>> xpl.run_app(explanation, scatter_xy=xy) + >>> emb = xpl.compute_embeddings(explanation, cache_dir="cache/") # doctest: +SKIP + >>> emb.project(pacmap.PaCMAP(n_components=2), init="pca") # doctest: +SKIP """ text_model = self._require_text_model() if not has_capabilities(text_model, SupportsEmbeddings): raise TypeError( f"{type(text_model).__name__} does not support embeddings (SupportsEmbeddings); " - "pass pre-computed coordinates to run_app(scatter_xy=...) instead." + "pass pre-computed coordinates to run_app(projection=...) instead." ) - if reducer is None: - reducer = PCA(n_components=2) - store = EmbeddingStore( text_model, # type: ignore[arg-type] # has_capabilities narrows the capability explanation.texts.tolist(), @@ -516,10 +496,51 @@ def compute_projection( if recompute: store.clear() with self._compute_guard(): # embed() tokenizes — serialize against the live compute ops - return store.cached_array( - f"{_reducer_tag(reducer, fit_transform_kwargs)}.proj", - lambda: np.asarray(reducer.fit_transform(store.vectors(), **fit_transform_kwargs)), - ) + return store.embedding() + + def compute_projection( + self, + explanation: NlpExplanation, + reducer=None, + cache_dir: str | Path | None = None, + recompute: bool = False, + **fit_transform_kwargs: Any, + ) -> Embedding: + """Return a 2-D projection of ``explanation``'s texts, ready to pass to :meth:`run_app`. + + Shorthand for ``compute_embeddings(explanation, cache_dir, recompute).project(reducer)``. The + embeddings go through the model's current ``embedding_space``, so the scatter and the + similar-example neighbours sit in the same space. Only the embeddings are cached: reducing is + cheap, and most reducers are stochastic, so :meth:`Embedding.save` a layout you want to keep. + + Parameters + ---------- + explanation : NlpExplanation + The result of :meth:`explain` for the batch to project. + reducer : object, optional + Anything with ``fit_transform(X) -> (n_samples, 2)`` — ``sklearn`` PCA/TSNE, ``pacmap``, + ``umap``. Defaults to ``PCA(n_components=2)``. + cache_dir : str or Path, optional + Where the embeddings are cached (see :meth:`compute_embeddings`). + recompute : bool, optional + Drop the cached embeddings first. + **fit_transform_kwargs + Forwarded to ``reducer.fit_transform`` — e.g. PaCMAP's ``init="pca"``. + + Returns + ------- + Embedding + ``(n_samples, 2)`` coordinates aligned with the explanation's texts. + + Examples + -------- + >>> explanation = xpl.explain(texts) + >>> projection = xpl.compute_projection(explanation, reducer=pacmap.PaCMAP(n_components=2), init="pca") + >>> xpl.run_app(explanation, projection=projection) + """ + # The reducer runs outside the compute lock (taken inside compute_embeddings): it is pure CPU + # over vectors already in hand and must not block live what-if predictions. + return self.compute_embeddings(explanation, cache_dir, recompute).project(reducer, **fit_transform_kwargs) def run_app( self, @@ -527,7 +548,7 @@ def run_app( port: int = 8050, debug: bool = False, host: str = "127.0.0.1", - scatter_xy=None, + projection: Embedding | np.ndarray | None = None, url_base_pathname: str | None = None, palette_name: str = "default", colors_dict: dict[str, str] | None = None, @@ -552,19 +573,15 @@ def run_app( Port for the Dash development server. debug : bool Enable Dash debug mode (hot reload, error overlay). - scatter_xy : np.ndarray, optional - Pre-computed 2-D projection of the text samples, shape ``(n_samples, 2)``. When provided, a - scatter panel appears in the webapp that can be used to lasso/box-select a subset of - samples, filtering both the dataset table and the global word importance plot to that - subset. - - Prefer :meth:`compute_projection`, whose output goes here and which guarantees the scatter - is drawn in the same space the similar-example neighbours are ranked in. This parameter is - an **unverified escape hatch**: coordinates from any source are accepted as-is, so a - projection built from a different space (or a different model entirely) renders happily - next to neighbours computed in another — selecting a cluster then means something other - than it appears to. Use it when you want coordinates the library cannot produce, and - accept that keeping them consistent is yours to manage. + projection : Embedding, optional + Pre-computed 2-D projection of the text samples, normally from + :meth:`compute_projection` or :meth:`Embedding.project`. When provided, a scatter panel + appears in the webapp that can be used to lasso/box-select a subset of samples, + filtering both the dataset table and the global word importance plot to that subset. + + An :class:`~shapash.compute.embeddings.Embedding` built from different texts is refused. + A bare ``(n_samples, 2)`` array is accepted on its shape alone, for coordinates produced + outside shapash. url_base_pathname : str, optional Mount the app under a URL prefix instead of the server root — for a reverse proxy that routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Must match the @@ -596,7 +613,7 @@ def run_app( return NlpWebApp( explanation, engine=self, - scatter_xy=scatter_xy, + projection=projection, url_base_pathname=url_base_pathname, palette_name=palette_name, colors_dict=colors_dict, diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index 2fdef621..a8e27d01 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -28,23 +28,11 @@ from shapash.__version__ import __version__ as _shapash_version from shapash.backend.nlp_backend import is_punctuation +from shapash.compute.hashing import hash_corpus if TYPE_CHECKING: from shapash.explainer.nlp_plotter import NlpPlotter -# Stamped into meta.json on save and checked on load, so an incompatible file fails loudly instead of -# parsing into wrong numbers — a layout change (wide instead of tidy contributions, a different -# class_idx encoding) often still reads cleanly and yields a plausible, wrong explanation. The risk is -# live rather than hypothetical: explain() writes these files into cache_dir and its key covers -# texts/model/backend but *not* the shapash version, so an upgrade leaves old files the new reader -# will hit. It also cannot be added retroactively — a file written without a version is forever -# ambiguous — which is why it sits here at v1 with nothing yet to reject. Limit: it versions the -# *layout* only; numbers whose meaning changes under an identical schema are invisible to it (the -# shapash_version recorded alongside it is the breadcrumb for that, and nothing checks it). The tuple -# is what lets a future reader accept v1 as well as its own — the migration code does not exist yet. -_FORMAT_VERSION = 1 -_SUPPORTED_FORMAT_VERSIONS = (1,) - _PROB_PREFIX = "y_prob__" @@ -222,7 +210,7 @@ def rank_word_samples( return ranked.head(n_top).reset_index(drop=True) -@dataclass(frozen=True, eq=False, slots=True) +@dataclass(frozen=True, eq=False, slots=True, repr=False) class NlpExplanation: """Immutable result of :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.explain`. @@ -250,6 +238,10 @@ class NlpExplanation: - ``eq=False`` — value equality is meaningless here (comparing the ``pd.Series`` fields raises "truth value is ambiguous"), so the object compares and hashes by identity rather than offering an ``__eq__`` that only ever raises. Use :meth:`save`/:meth:`load` to compare runs. + - ``repr=False`` — the generated dataclass repr prints every field's ``repr()`` in full, which + for ``texts``/``token_strings``/``values`` means the entire batch: a notebook cell that just + names the variable would dump megabytes of text and per-token arrays. :meth:`__repr__` below + prints a one-line summary instead; the full payload stays one attribute access away. The boundary worth knowing: the pandas fields (``texts``, ``y_pred``, ``y_prob``, ``y_true``) are held by reference and *not* sealed — pandas has no equivalent write lock — so in-place @@ -469,6 +461,12 @@ def relabelled(self, texts: pd.Series, y_true: pd.Series | None = None) -> NlpEx def __len__(self) -> int: return len(self.token_strings) + def __repr__(self) -> str: + return ( + f"NlpExplanation(n_samples={self.n_samples}, n_classes={self.n_classes}, " + f"backend={self.backend_name!r}, has_ground_truth={self.has_ground_truth})" + ) + @property def n_samples(self) -> int: """Number of samples in the batch.""" @@ -479,6 +477,22 @@ def n_classes(self) -> int: """Number of model output columns the contributions carry.""" return _n_classes(self.values, self.base_values, self.label_names) + @property + def corpus_id(self) -> str: + """Digest of :attr:`texts` alone — the dataset's identity, independent of model or backend. + + Equal to the ``corpus_id`` of any :class:`~shapash.compute.embeddings.Embedding` of the same + texts, which is how a projection is checked against this explanation. A property rather than + a field, since it is a pure function of ``texts``; it hashes every text, so call it at + boundaries rather than inside a render loop. + + Returns + ------- + str + 32-character hex digest. + """ + return hash_corpus(self.texts.tolist()) + @property def has_ground_truth(self) -> bool: """Whether ground-truth labels were supplied to ``explain()``.""" @@ -509,7 +523,7 @@ def plot(self) -> NlpPlotter: Examples -------- - >>> explanation, _ = NlpExplanation.load("run.zip") # no model, no backend + >>> explanation = NlpExplanation.load("run.xpl") # no model, no backend >>> explanation.plot.waterfall(row=0, label_idx=1).show() """ # Imported here, not at module scope: this module is the persistence boundary and stays @@ -957,29 +971,30 @@ def confusion_matrix(self) -> np.ndarray: cm[t, p] += 1 return cm - def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: + def save(self, path: str | Path) -> None: """Persist this explanation (no model or backend required to reload). Writes a single zip file containing a plain-text ``meta.json`` (readable without shapash) plus long/tidy parquet tables for contributions, base - values, samples (texts/predictions/ground truth) and, optionally, a 2-D - scatter projection. + values and samples (texts/predictions/ground truth). + + A 2-D projection is not bundled: save it separately with + :meth:`~shapash.compute.embeddings.Embedding.save`. Parameters ---------- path : str or Path - Destination file. Any extension is accepted; ``.shxpl`` is a reasonable + Destination file. Any extension is accepted; ``.xpl`` is a reasonable convention but not enforced. - scatter_xy : np.ndarray, optional - Pre-computed 2-D projection to bundle alongside the explanation, shape - ``(n_samples, 2)`` (the same array :meth:`~NlpExplainer.compute_projection` - returns). + + See Also + -------- + load : Restore a file written here. """ contrib_df, base_df, values_ndim = _contributions_to_frames(self.token_strings, self.values, self.base_values) samples_df = _samples_to_frame(self.texts, self.y_pred, self.y_prob, self.y_true) meta = { - "format_version": _FORMAT_VERSION, "shapash_version": _shapash_version, "created_at": datetime.now(UTC).isoformat(), "backend_name": self.backend_name, @@ -998,7 +1013,6 @@ def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: "values_ndim": values_ndim, "has_base_values": base_df is not None, "has_ground_truth": self.y_true is not None, - "has_scatter": scatter_xy is not None, } with zipfile.ZipFile(Path(path), "w", compression=zipfile.ZIP_DEFLATED) as zf: @@ -1007,15 +1021,9 @@ def save(self, path: str | Path, scatter_xy: np.ndarray | None = None) -> None: if base_df is not None: _write_parquet(zf, "base_values.parquet", base_df) _write_parquet(zf, "samples.parquet", samples_df) - if scatter_xy is not None: - scatter_arr = np.asarray(scatter_xy) - scatter_df = pd.DataFrame( - {"sample_idx": np.arange(len(scatter_arr)), "x": scatter_arr[:, 0], "y": scatter_arr[:, 1]} - ) - _write_parquet(zf, "scatter.parquet", scatter_df) @classmethod - def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: + def load(cls, path: str | Path) -> NlpExplanation: """Restore an explanation saved by :meth:`save`. Parameters @@ -1025,39 +1033,21 @@ def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: Returns ------- - explanation : NlpExplanation - The restored explanation. - scatter_xy : np.ndarray or None - The projection array bundled at save time, or ``None``. - - Raises - ------ - ValueError - If the file's ``format_version`` is not one this shapash version reads. + NlpExplanation + The restored artifact, model-free and backend-free. """ with zipfile.ZipFile(Path(path), "r") as zf: meta = json.loads(zf.read("meta.json")) - version = meta.get("format_version") - if version not in _SUPPORTED_FORMAT_VERSIONS: - raise ValueError( - f"Unsupported NlpExplanation format_version={version!r}; this shapash version " - f"({_shapash_version}) reads version(s) {_SUPPORTED_FORMAT_VERSIONS}. " - "Re-save this explanation with a compatible shapash version." - ) contrib_df = _read_parquet(zf, "contributions.parquet") base_df = _read_parquet(zf, "base_values.parquet") if meta.get("has_base_values", True) else None samples_df = _read_parquet(zf, "samples.parquet") - scatter_xy = None - if meta.get("has_scatter"): - scatter_df = _read_parquet(zf, "scatter.parquet") - scatter_xy = scatter_df[["x", "y"]].to_numpy() token_strings, values, base_values = _frames_to_contributions( contrib_df, base_df, meta["values_ndim"], meta["n_samples"], meta["n_classes"] ) texts, y_pred, y_true, y_prob = _frame_to_samples(samples_df) - explanation = cls( + return cls( texts=texts, token_strings=token_strings, values=values, @@ -1080,7 +1070,6 @@ def load(cls, path: str | Path) -> tuple[NlpExplanation, np.ndarray | None]: model_id=meta.get("model_id"), architecture=meta.get("architecture"), ) - return explanation, scatter_xy def _write_parquet(zf: zipfile.ZipFile, name: str, df: pd.DataFrame) -> None: diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index f7dd2cd5..78c0a3c0 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -6,7 +6,7 @@ a different figure from the *same* untouched explanation. Why a separate object rather than methods on :class:`NlpExplanation` itself: the explanation is -the persistence boundary (``save``/``load`` with a ``format_version``), so keeping rendering off +the persistence boundary (``save``/``load``), so keeping rendering off it keeps the dataclass a dataclass. It also mirrors the tabular side, where ``SmartExplainer`` exposes :class:`~shapash.explainer.smart_plotter.SmartPlotter` as ``.plot``. @@ -24,6 +24,7 @@ from dash import html from plotly import graph_objs as go +from shapash.compute.embeddings import Embedding, projection_coords from shapash.explainer.nlp_explanation import ( WORD_AGGREGATIONS, aggregate_word_contributions, @@ -60,7 +61,7 @@ class NlpPlotter: Examples -------- - >>> explanation, _ = NlpExplanation.load("run.zip") # no model needed + >>> explanation = NlpExplanation.load("run.xpl") # no model needed >>> explanation.plot.waterfall(row=0, label_idx=1).show() """ @@ -417,7 +418,7 @@ def confusion( def scatter( self, - xy: np.ndarray, + projection: Embedding | np.ndarray, color_by: str = "prediction", words: list[str] | None = None, label_idx: int = 0, @@ -426,16 +427,19 @@ def scatter( ) -> go.Figure: """2-D scatter of the batch (e.g. an embedding projection), colored by class or by a word. - ``xy`` is not something the artifact carries — see :meth:`NlpExplainer.compute_projection` - for how to produce it (defaults to PCA on the model's embeddings, no extra install since - sklearn is a core dependency) — every other argument here comes straight from ``self._exp``, - the same split :class:`~shapash.webapp.nlp_components.scatter.ScatterComponent` uses to + The projection is not something the artifact carries — see + :meth:`NlpExplainer.compute_projection` for how to produce it (defaults to PCA on the + model's embeddings, no extra install since sklearn is a core dependency). The projection + supplies only the coordinates; colours, legend, hover text and error mask all come from + ``self._exp`` — the same split :class:`~shapash.webapp.nlp_components.scatter.ScatterComponent` uses to build the identical figure inside the What-if Lab. Parameters ---------- - xy : np.ndarray, shape (n_samples, 2) - 2-D coordinates aligned with the explanation's samples. + projection : Embedding or np.ndarray, shape (n_samples, 2) + 2-D coordinates aligned with the explanation's samples, checked by + :func:`~shapash.compute.embeddings.projection_coords`: an ``Embedding`` of different texts + is refused, a bare array is accepted on its shape alone. color_by : {"prediction", "ground_truth", "word_contribution"} ``"ground_truth"`` falls back to predictions when ``y_true`` is unavailable; ``"word_contribution"`` falls back to predictions when ``words`` is empty. @@ -461,10 +465,11 @@ def scatter( Examples -------- - >>> xy = xpl.compute_projection(explanation, cache_dir="cache/") - >>> explanation.plot.scatter(xy, color_by="word_contribution", words=["terrible"]).show() + >>> projection = xpl.compute_projection(explanation, cache_dir="cache/") + >>> explanation.plot.scatter(projection, color_by="word_contribution", words=["terrible"]).show() """ exp = self._exp + xy = projection_coords(projection, exp) err_mask = None if errors_only and exp.y_true is not None and exp.y_pred is not None: err_mask = np.asarray(exp.y_true).astype(str) != np.asarray(exp.y_pred).astype(str) diff --git a/shapash/model/base.py b/shapash/model/base.py index 1da564b2..2924f3ea 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -319,7 +319,7 @@ class SupportsEmbeddings(ABC): through it, so that changing the model's space moves them together. That agreement is a **convention, not a guarantee**: a caller who computes its own coordinates and - hands them to the webapp (``run_app(scatter_xy=...)``) bypasses this entirely, and nothing detects + hands them to the webapp (``run_app(projection=...)``) bypasses this entirely, and nothing detects it. Use :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection`, which derives the scatter from this method, unless you specifically want a space of your own choosing. """ diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index dad3cb00..b7399c9b 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -19,6 +19,7 @@ from dash import MATCH, Input, Output, dcc, html from dash.exceptions import PreventUpdate +from shapash.compute.embeddings import Embedding, projection_coords from shapash.explainer.interactive import InteractiveEngine from shapash.explainer.nlp_explanation import NlpExplanation from shapash.style.style_utils import NlpTheme, resolve_nlp_theme @@ -38,6 +39,7 @@ error_positions, pack_datapoint, ) +from shapash.webapp.nlp_components.base import AppContext from shapash.webapp.utils.launch import RunningApp, run_in_background _APPLY_STORE = "whatif-apply-store" @@ -138,21 +140,22 @@ class NlpWebApp: Parameters ---------- explanation : NlpExplanation - The result of ``NlpExplainer.explain()`` (or a loaded one — see - ``NlpExplanation.load()``) to serve. + The result of ``NlpExplainer.explain()`` (or a loaded one — see ``NlpExplanation.load()``) + to serve. engine : InteractiveEngine, optional Live engine for what-if actions (re-predicting edited text, generating counterfactuals, similar-example retrieval, label-noise probing) — normally the - ``NlpExplainer`` that produced ``explanation``. ``None`` for a snapshot loaded via + ``NlpExplainer`` that produced the explanation. ``None`` for a snapshot loaded via ``NlpExplanation.load()``, which carries no model: components self-disable via their ``requires`` when the engine is absent or lacks a capability. - scatter_xy : np.ndarray, optional - Pre-computed 2-D projection, shape ``(n_samples, 2)``. When provided, a scatter panel is - added to the layout. ``NlpWebApp`` itself never computes this — it has no model to embed - with (see ``engine`` above) — so pass it in from whatever produced it: PaCMAP, UMAP, - t-SNE, PCA, etc. Prefer :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection` - (defaults to PCA — no extra install, sklearn is already a core dependency) over rolling - your own, so the scatter and the similar-example neighbours share one embedding space. + projection : Embedding or np.ndarray, optional + Pre-computed 2-D projection. When provided, a scatter panel is added to the layout. + ``NlpWebApp`` itself never computes this — it has no model to embed with (see *engine* + above) — so pass it in from whatever produced it: PaCMAP, UMAP, t-SNE, PCA, etc. Prefer + :meth:`~shapash.explainer.nlp_explainer.NlpExplainer.compute_projection` (defaults to PCA — + no extra install, sklearn is already a core dependency) over rolling your own, so the + scatter and the similar-example neighbours share one embedding space. An ``Embedding`` of + different texts is refused; a bare ``(n_samples, 2)`` array is accepted on its shape alone. url_base_pathname : str, optional Mount the app under a URL prefix instead of the server root, for serving behind a reverse proxy that routes a subpath (e.g. ``"/shapash-nlp-explainer/"``) to this process. Dash @@ -188,25 +191,21 @@ def __init__( self, explanation: NlpExplanation, engine: InteractiveEngine | None = None, - scatter_xy: np.ndarray | None = None, + projection: Embedding | np.ndarray | None = None, url_base_pathname: str | None = None, palette_name: str = "default", colors_dict: dict[str, str] | None = None, info: dict[str, str] | None = None, ) -> None: - if scatter_xy is not None: - scatter_xy = np.asarray(scatter_xy) - n = len(explanation.texts) - if scatter_xy.shape != (n, 2): - raise ValueError(f"scatter_xy must have shape ({n}, 2), got {scatter_xy.shape}") - self._scatter_xy: np.ndarray | None = scatter_xy - - # What-if Lab: the immutable artifact + a separate, possibly-absent live engine. The - # artifact is only ever read here — display state lives in the `dcc.Store`s declared in - # `_build_layout`, never on the explanation. - # Components self-disable via their `requires` when the engine is None or lacks a capability - # (e.g. a loaded snapshot has no model at all). `self._components` itself is assembled later - # in `_build_layout` (it needs `_full_table_records`, not ready yet here). + # Every input is checked here, once, so a mismatched deployment fails at construction rather + # than as a wrong picture. Components then read the validated context and self-disable via + # their `requires` (e.g. a loaded snapshot has no engine). Display state lives in the + # `dcc.Store`s declared in `_build_layout`, never on the explanation. + self._ctx = AppContext( + explanation=explanation, + engine=engine, + coords=projection_coords(projection, explanation) if projection is not None else None, + ) self._explanation = explanation self._engine = engine @@ -444,7 +443,7 @@ def _build_selection_bar(self) -> html.Div: # Input pair, so a grid-level filter is described here too, not just inside the grid. html.Span("all samples", id="selection-summary", className="small fw-bold me-3"), ] - if self._scatter_xy is not None: + if self._ctx.coords is not None: selection_children.append(_clear_button("scatter-clear-btn", "× clear selection")) if self._has_gt: selection_children.append(_clear_button("error-cell-clear-btn", "× clear cell")) @@ -501,14 +500,13 @@ def _build_components(self) -> list[WebappComponent]: WordProfileComponent(theme=self._theme), WordImportanceComponent(theme=self._theme), ) - if type(comp).is_available(self._explanation, self._engine) + if type(comp).is_available(self._ctx) ] - # Mounting is decided by the caller-supplied array, not by `is_available` — a pre-computed - # projection is arbitrary data handed to NlpWebApp, not an explanation/engine capability. - # Word Importance is always mounted at this point (it has no `requires`), so the scatter's - # word-contribution color mode can always be offered. - if self._scatter_xy is not None: - components.append(ScatterComponent(self._scatter_xy, offer_word_contribution=True)) + # The scatter is mounted by `requires` like everything else (CAP_PROJECTION). It is appended + # rather than listed above only because its `offer_word_contribution` argument depends on Word Importance being mounted — + # which it always is at this point, since that panel has no `requires`. + if ScatterComponent.is_available(self._ctx): + components.append(ScatterComponent(offer_word_contribution=True)) self._components = components return components @@ -527,42 +525,32 @@ def _build_tabs(self, components: list[WebappComponent], text_samples_body: html left_tabs: list = [("table", "Dataset", text_samples_body)] if scatter_comp is not None: - left_tabs.append(("scatter", "Embeddings", scatter_comp.layout(self._explanation, self._engine))) + left_tabs.append(("scatter", "Embeddings", scatter_comp.layout(self._ctx))) if editor_comp is not None: - left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._explanation, self._engine))) + left_tabs.append(("editor", "Data Editor", editor_comp.layout(self._ctx))) # Error Analysis sits beside Word Importance: it *is* an aggregated word-importance view # (per confusion-matrix cell), so it belongs with the other global "why" panels on the right. - upper_right_tabs: list = [ - ("importance", "Word Importance", word_importance_comp.layout(self._explanation, self._engine)) - ] + upper_right_tabs: list = [("importance", "Word Importance", word_importance_comp.layout(self._ctx))] # Immediately after Word Importance, because it is the same question asked the other way # round (one word across all classes, instead of one class across the top words) and the two # are read together — a bar clicked there arrives preselected here. if word_profile_comp is not None: - upper_right_tabs.append( - ("word-profile", "Word Profile", word_profile_comp.layout(self._explanation, self._engine)) - ) + upper_right_tabs.append(("word-profile", "Word Profile", word_profile_comp.layout(self._ctx))) if error_analysis_comp is not None: - upper_right_tabs.append( - ("errors", "Error Analysis", error_analysis_comp.layout(self._explanation, self._engine)) - ) + upper_right_tabs.append(("errors", "Error Analysis", error_analysis_comp.layout(self._ctx))) # Next to Error Analysis by design: same ground-truth prerequisite, and it answers the # question that panel leaves open — whether the *label*, not the model, is what is wrong. if noise_comp is not None: - upper_right_tabs.append(("label-noise", "Label Noise", noise_comp.layout(self._explanation, self._engine))) + upper_right_tabs.append(("label-noise", "Label Noise", noise_comp.layout(self._ctx))) if cf_comp is not None: - upper_right_tabs.append( - ("counterfactual", "Counterfactuals", cf_comp.layout(self._explanation, self._engine)) - ) + upper_right_tabs.append(("counterfactual", "Counterfactuals", cf_comp.layout(self._ctx))) if similar_comp is not None: - upper_right_tabs.append( - ("similar", "Similar Examples", similar_comp.layout(self._explanation, self._engine)) - ) + upper_right_tabs.append(("similar", "Similar Examples", similar_comp.layout(self._ctx))) lower_right_tabs: list = [ - ("highlight", "Sentence", highlight_comp.layout(self._explanation, self._engine)), - ("waterfall", "Waterfall", waterfall_comp.layout(self._explanation, self._engine)), + ("highlight", "Sentence", highlight_comp.layout(self._ctx)), + ("waterfall", "Waterfall", waterfall_comp.layout(self._ctx)), ] return left_tabs, upper_right_tabs, lower_right_tabs @@ -916,7 +904,7 @@ def _toggle_tab_bodies(active, _tab_ids=tab_ids): "active_class": "active-class-store", } for comp in self._components: - comp.register_callbacks(self.app, self._explanation, self._engine, stores) + comp.register_callbacks(self.app, self._ctx, stores) # ------------------------------------------------------------------ # Public diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 7cfa42f2..589236df 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -1,9 +1,9 @@ """``WebappComponent`` contract + capability resolution for what-if panels. A component declares the capabilities it needs via ``requires``; :func:`available_capabilities` -computes what the bound explanation + engine actually provide, and :meth:`WebappComponent.is_available` -gates mounting on ``requires <= available``. This is the mechanism that makes the What-if Lab -appear only when the explainer holds a live (and, for counterfactuals, gradient-capable) model. +computes what the app's :class:`AppContext` actually provides, and :meth:`WebappComponent.is_available` +gates mounting on ``requires <= available``. This is the mechanism that makes the What-if Lab appear +only when the explainer holds a live (and, for counterfactuals, gradient-capable) model. Components read the immutable :class:`~shapash.explainer.nlp_explanation.NlpExplanation` directly and never write to it: every display choice lives in a Dash ``dcc.Store`` or a callback argument, so the @@ -13,6 +13,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass import numpy as np @@ -28,18 +29,37 @@ CAP_SIMILAR = "engine:similar" CAP_LABELS = "data:labels" CAP_GROUND_TRUTH = "data:ground_truth" +CAP_PROJECTION = "data:projection" -def available_capabilities(explanation: NlpExplanation, engine: InteractiveEngine | None) -> frozenset[str]: - """Return the capability tokens the given explanation + engine satisfy. +@dataclass(frozen=True) +class AppContext: + """What every panel reads: the explanation, the live engine, and the scatter coordinates. + + Built and validated once by :class:`~shapash.webapp.nlp_app.NlpWebApp`; not a user-facing type. Parameters ---------- explanation : NlpExplanation - The immutable artifact, supplying the *data* capabilities — what the compiled batch - contains, independent of whether a live model is still attached. + The immutable artifact every panel reads. engine : InteractiveEngine or None - Live engine, or ``None`` for a snapshot (no live capabilities). + Live engine for what-if actions, or ``None`` for a snapshot. + coords : np.ndarray or None + ``(n_samples, 2)`` scatter coordinates, already checked against *explanation*. + """ + + explanation: NlpExplanation + engine: InteractiveEngine | None = None + coords: np.ndarray | None = None + + +def available_capabilities(ctx: AppContext) -> frozenset[str]: + """Return the capability tokens *ctx* satisfies. + + Parameters + ---------- + ctx : AppContext + The explanation, engine and coordinates this app was built from. Returns ------- @@ -47,9 +67,12 @@ def available_capabilities(explanation: NlpExplanation, engine: InteractiveEngin Satisfied capability tokens (e.g. ``{"engine:predict", "engine:counterfactual", "model:gradients"}``). """ + explanation, engine = ctx.explanation, ctx.engine caps: set[str] = set() - # Data capabilities are read from the compiled batch, so they survive a snapshot — they sit - # outside the engine guard below on purpose. + # Data capabilities are read from the compiled batch and the coordinates beside it, so they + # survive a snapshot — they sit outside the engine guard below on purpose. + if ctx.coords is not None: + caps.add(CAP_PROJECTION) if explanation.y_true is not None: caps.add(CAP_GROUND_TRUTH) if has_usable_probabilities(explanation.y_prob): @@ -133,34 +156,30 @@ class WebappComponent(ABC): requires: frozenset[str] = frozenset() @classmethod - def is_available(cls, explanation: NlpExplanation, engine: InteractiveEngine | None) -> bool: - """Whether the component's ``requires`` are satisfied by the explanation + engine.""" - return cls.requires <= available_capabilities(explanation, engine) + def is_available(cls, ctx: AppContext) -> bool: + """Whether the component's ``requires`` are satisfied by *ctx*.""" + return cls.requires <= available_capabilities(ctx) @abstractmethod - def layout(self, explanation: NlpExplanation, engine: InteractiveEngine | None = None): + def layout(self, ctx: AppContext): """Return the Dash layout for this component. - ``engine`` is provided for components whose initial UI depends on live capabilities — e.g. the - counterfactual panel renders its config controls from the generator's spec, so they must exist - in the initial layout rather than be injected by a later callback. + The whole context is passed because a component's initial UI may depend on more than the + explanation — the counterfactual panel renders its config controls from the live generator's + spec, and the scatter panel needs the coordinates. """ @abstractmethod - def register_callbacks( - self, app, explanation: NlpExplanation, engine: InteractiveEngine | None, stores: dict - ) -> None: + def register_callbacks(self, app, ctx: AppContext, stores: dict) -> None: """Register this component's Dash callbacks. Parameters ---------- app : dash.Dash The Dash application. - explanation : NlpExplanation - The immutable artifact to read (never written to). - engine : InteractiveEngine or None - Live engine for prediction / counterfactual generation, or ``None`` for a snapshot with - no live model — only reached for a component whose ``requires`` needs no engine capability. + ctx : AppContext + The explanation, engine and coordinates to read (never written to). Display state lives + in the ``dcc.Store``s. stores : dict Shared ``dcc.Store`` ids the What-if Lab wires between components (e.g. ``{"apply": "whatif-apply-store"}``). diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index 348d6ed5..c6c541b6 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -78,7 +78,7 @@ def _config_controls(self, engine, generator: str) -> list: ) return items - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the counterfactual card with a method selector and per-generator config controls. The selector and *every* generator's controls are built here (not injected by a callback) so @@ -86,6 +86,7 @@ def layout(self, explanation, engine=None) -> html.Div: reference not-yet-created objects. Only the active generator's control group is visible; the selector callback toggles the rest. The selector is hidden when a single generator is bound. """ + engine = ctx.engine generators = self._generators(engine) default_gen = generators[0][0] if generators else None @@ -133,8 +134,9 @@ def layout(self, explanation, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the method selector, Generate, and per-row Apply (→ shared editor store).""" + engine = ctx.engine apply_store = stores["apply"] current_store = stores["current"] diff --git a/shapash/webapp/nlp_components/data_editor.py b/shapash/webapp/nlp_components/data_editor.py index 39fb6253..ebdfeb36 100644 --- a/shapash/webapp/nlp_components/data_editor.py +++ b/shapash/webapp/nlp_components/data_editor.py @@ -51,7 +51,7 @@ class DataEditorComponent(WebappComponent): scope = "local" requires = frozenset({CAP_PREDICT}) - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the editor card: textarea, Predict button, class-probability bar. Token contributions for the edited text are shown by the shared lower-right panel (it reads the @@ -82,8 +82,9 @@ def layout(self, explanation, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire prefill (row/apply → textarea) and Predict (textarea → prediction + current datapoint).""" + engine = ctx.engine apply_store = stores["apply"] current_store = stores["current"] diff --git a/shapash/webapp/nlp_components/error_analysis.py b/shapash/webapp/nlp_components/error_analysis.py index d4f6ee3a..7abfe2fc 100644 --- a/shapash/webapp/nlp_components/error_analysis.py +++ b/shapash/webapp/nlp_components/error_analysis.py @@ -67,8 +67,9 @@ def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._cm_true_idx: np.ndarray | None = None self._cm_pred_idx: np.ndarray | None = None - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Build the matrix + per-cell word-importance panel, caching the matrix for callbacks.""" + explanation = ctx.explanation label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] idx_of = explanation.label_to_idx y_true, y_pred = explanation.y_true, explanation.y_pred @@ -146,8 +147,9 @@ def layout(self, explanation, engine=None) -> html.Div: style={"height": "100%", "display": "flex", "flexDirection": "column", "overflowY": "auto"}, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the normalize toggle, cell click/clear, and the two per-cell word charts.""" + explanation = ctx.explanation error_cell_store = stores["error_cell"] clear_btn_id = stores["error_cell_clear"] diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py index 60548a1c..35f6f7ef 100644 --- a/shapash/webapp/nlp_components/label_noise.py +++ b/shapash/webapp/nlp_components/label_noise.py @@ -71,8 +71,9 @@ class LabelNoiseComponent(WebappComponent): # Data-only: no engine capability, so this also mounts on a snapshot explainer. requires = frozenset({CAP_LABELS}) - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the panel: detection controls over an initially empty results area.""" + engine = ctx.engine can_probe = engine is not None and engine.can_probe_labels() caption = ( "Confident learning over the model's probabilities and the ground-truth labels. " @@ -132,8 +133,9 @@ def layout(self, explanation, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Run detection on demand; wire each row's Inspect into the shared current datapoint.""" + explanation, engine = ctx.explanation, ctx.engine current_store = stores["current"] @app.callback( diff --git a/shapash/webapp/nlp_components/scatter.py b/shapash/webapp/nlp_components/scatter.py index e5d09838..c4340f44 100644 --- a/shapash/webapp/nlp_components/scatter.py +++ b/shapash/webapp/nlp_components/scatter.py @@ -1,8 +1,7 @@ """Scatter (Embeddings) panel: 2-D projection of the text embeddings, box/lasso-selectable. -Mounting is decided by the caller-supplied ``scatter_xy`` array, not by ``requires`` — a pre-computed -projection is arbitrary data handed to ``NlpWebApp``, not an explanation/engine capability, so -``NlpWebApp`` only ever constructs this component when it has one (see ``ScatterComponent.__init__``). +Mounted like every other panel: ``requires = {CAP_PROJECTION}``, satisfied when the app was given a +projection (``AppContext.coords``). The "Word contribution" color mode is a **structural, not capability, dependency** on :class:`~shapash.webapp.nlp_components.word_importance.WordImportanceComponent`: coloring by a word's @@ -22,14 +21,13 @@ from __future__ import annotations import dash_bootstrap_components as dbc -import numpy as np import plotly.graph_objs as go from dash import Input, Output, callback_context, dcc, html from dash.exceptions import PreventUpdate from shapash.explainer.nlp_explanation import word_contributions_by_sample from shapash.plots.plot_scatter import plot_scatter -from shapash.webapp.nlp_components.base import WebappComponent, error_mask +from shapash.webapp.nlp_components.base import CAP_PROJECTION, WebappComponent, error_mask class ScatterComponent(WebappComponent): @@ -38,14 +36,14 @@ class ScatterComponent(WebappComponent): id = "scatter" name = "Embeddings" scope = "global" - requires = frozenset() + requires = frozenset({CAP_PROJECTION}) - def __init__(self, scatter_xy: np.ndarray, offer_word_contribution: bool) -> None: - self._scatter_xy = scatter_xy + def __init__(self, offer_word_contribution: bool) -> None: self._offer_word_contribution = offer_word_contribution - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Build the color-by/word-select controls and the scatter graph itself.""" + explanation = ctx.explanation word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in explanation.vocabulary()] color_options: list[dcc.Dropdown.Options] = [{"label": "Prediction", "value": "prediction"}] if explanation.y_true is not None: @@ -94,7 +92,7 @@ def layout(self, explanation, engine=None) -> html.Div: ), dcc.Graph( id="scatter-plot", - figure=self._build_scatter_fig(explanation, "prediction"), + figure=self._build_scatter_fig(ctx, "prediction"), config={ "displayModeBar": True, "modeBarButtonsToRemove": ["autoScale2d", "resetScale2d"], @@ -109,7 +107,7 @@ def layout(self, explanation, engine=None) -> html.Div: style={"display": "flex", "flexDirection": "column", "height": "100%"}, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire coloring, selection, and the bidirectional word-list sync with the shared store.""" selection_store = stores["selection"] selection_clear_btn = stores["selection_clear"] @@ -128,7 +126,7 @@ def register_callbacks(self, app, explanation, engine, stores) -> None: ) def update_scatter_color(color_by, words, label_idx, errors_only): return self._build_scatter_fig( - explanation, + ctx, color_by or "prediction", words=words or [], label_idx=label_idx if isinstance(label_idx, int) else 0, @@ -207,26 +205,30 @@ def toggle_clear_button(selected_indices): def _build_scatter_fig( self, - explanation, + ctx, color_by: str, words: list[str] | None = None, label_idx: int = 0, errors_only: bool = False, ) -> go.Figure: - """Slice ``explanation`` for the requested coloring and hand it to :func:`plot_scatter`. + """Slice *ctx* for the requested coloring and hand it to :func:`plot_scatter`. + + Coordinates come from ``ctx.coords``; every visual encoding comes from ``ctx.explanation``. ``color_by``/``words``/``label_idx`` decide *which* arrays to slice (a webapp-control concern); the actual figure construction is the same pure function notebook/script callers get via ``explanation.plot.scatter`` — see :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.scatter`. """ + explanation = ctx.explanation + coords = ctx.coords err_mask = error_mask(explanation) if errors_only else None if color_by == "word_contribution" and words: contributions = word_contributions_by_sample(explanation, words, label_idx) colorbar_title = " + ".join(f'"{w}"' for w in words) if len(words) <= 3 else f"{len(words)} words" return plot_scatter( - self._scatter_xy, + coords, explanation.texts, contributions=contributions, colorbar_title=colorbar_title, @@ -242,7 +244,7 @@ def _build_scatter_fig( label_names = explanation.label_names or sorted(set(labels)) return plot_scatter( - self._scatter_xy, + coords, explanation.texts, labels=labels, label_names=label_names, diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py index c62958d5..d9292618 100644 --- a/shapash/webapp/nlp_components/sentence_highlight.py +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -26,8 +26,9 @@ def __init__(self, default_class_idx: int = 0, theme: NlpTheme = DEFAULT_NLP_THE self._default_class_idx = default_class_idx self._theme = theme - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the class picker + sentence-highlight placeholder div.""" + explanation = ctx.explanation label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] class_options: list[dcc.Dropdown.Options] = [{"label": name, "value": i} for i, name in enumerate(label_names)] return html.Div( @@ -62,8 +63,9 @@ def layout(self, explanation, engine=None) -> html.Div: style={"height": "100%"}, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the predicted-class sync and the sentence-highlight render.""" + explanation = ctx.explanation current_store = stores["current"] # Fires only on current-datapoint changes (row click, editor Predict, counterfactual Apply) diff --git a/shapash/webapp/nlp_components/similar_examples.py b/shapash/webapp/nlp_components/similar_examples.py index 3093a3cf..3bc4f3dd 100644 --- a/shapash/webapp/nlp_components/similar_examples.py +++ b/shapash/webapp/nlp_components/similar_examples.py @@ -51,8 +51,9 @@ def _layer_name(self, engine) -> str | None: retriever = getattr(engine, "_retriever", None) return getattr(retriever, "layer", None) if retriever is not None else None - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the panel: a top-k control plus a results area fed by the current datapoint.""" + engine = ctx.engine layer = self._layer_name(engine) caption = "Train examples most similar to the selected text in the model's decision space" if layer: @@ -119,8 +120,9 @@ def layout(self, explanation, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Recompute neighbours on selection/mode/filter change; wire per-row Inspect into the current store.""" + engine = ctx.engine current_store = stores["current"] @app.callback( diff --git a/shapash/webapp/nlp_components/waterfall.py b/shapash/webapp/nlp_components/waterfall.py index 08ace9f0..d87c7aee 100644 --- a/shapash/webapp/nlp_components/waterfall.py +++ b/shapash/webapp/nlp_components/waterfall.py @@ -26,7 +26,7 @@ class WaterfallComponent(WebappComponent): def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._theme = theme - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Return the grouping-threshold slider + waterfall graph.""" return html.Div( [ @@ -53,8 +53,9 @@ def layout(self, explanation, engine=None) -> html.Div: style={"height": "100%"}, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the waterfall figure to the current datapoint, class picker, and threshold.""" + explanation = ctx.explanation current_store = stores["current"] @app.callback( diff --git a/shapash/webapp/nlp_components/word_importance.py b/shapash/webapp/nlp_components/word_importance.py index f1279179..40f8807e 100644 --- a/shapash/webapp/nlp_components/word_importance.py +++ b/shapash/webapp/nlp_components/word_importance.py @@ -67,10 +67,11 @@ class WordImportanceComponent(WebappComponent): def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._theme = theme - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Build the controls + graph. DOM ids are the pre-extraction literals (no ``self.id`` prefix) so existing layout tests keep matching — these ids were never shared with any other panel. """ + explanation = ctx.explanation label_names = explanation.label_names or [str(i) for i in range(explanation.n_classes)] word_options: list[dcc.Dropdown.Options] = [{"label": w, "value": w} for w in explanation.vocabulary()] @@ -240,8 +241,9 @@ def layout(self, explanation, engine=None) -> html.Div: style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the ranking, the sign-filter gate, and the bar-click → shared word/class stores.""" + explanation = ctx.explanation selection_store = stores["selection"] error_cell_store = stores["error_cell"] errors_only_switch = stores["errors_only"] diff --git a/shapash/webapp/nlp_components/word_profile.py b/shapash/webapp/nlp_components/word_profile.py index 580e8841..2ef3d557 100644 --- a/shapash/webapp/nlp_components/word_profile.py +++ b/shapash/webapp/nlp_components/word_profile.py @@ -136,8 +136,9 @@ def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: # a full pass over the corpus, and the sort toggle must not pay it on every click. self._options: dict[str, list] = {"alpha": [], "frequency": []} - def layout(self, explanation, engine=None) -> html.Div: + def layout(self, ctx) -> html.Div: """Build the panel, seeded with the corpus's single most important word so it opens full.""" + explanation = ctx.explanation counts = explanation.word_counts() # The count rides in the option *label* while the value stays the bare word, so it is # visible in either sort order (frequency is what tells you whether an aggregate is worth @@ -271,8 +272,9 @@ def layout(self, explanation, engine=None) -> html.Div: style=_CARD_STYLE, ) - def register_callbacks(self, app, explanation, engine, stores) -> None: + def register_callbacks(self, app, ctx, stores) -> None: """Wire the profile (word × aggregation × global selection) and each row's Inspect.""" + explanation = ctx.explanation current_store = stores["current"] selection_store = stores["selection"] error_cell_store = stores["error_cell"] diff --git a/tests/unit_tests/compute/retrieval/test_similar_examples.py b/tests/unit_tests/compute/retrieval/test_similar_examples.py index 24b02d18..1c1ddb11 100644 --- a/tests/unit_tests/compute/retrieval/test_similar_examples.py +++ b/tests/unit_tests/compute/retrieval/test_similar_examples.py @@ -203,7 +203,7 @@ def test_bank_cached_to_disk_and_reloaded(self): with tempfile.TemporaryDirectory() as d: r1 = SimilarExampleRetriever(self.model, self.texts, self.labels, cache_dir=d) r1.build() - cache_file = r1.store.path("emb") + cache_file = r1.store.path self.assertTrue(cache_file.exists()) # A fresh retriever over the same corpus/space/model loads the bank from disk. diff --git a/tests/unit_tests/compute/test_embedding_store.py b/tests/unit_tests/compute/test_embedding_store.py index 45656932..5090b1f0 100644 --- a/tests/unit_tests/compute/test_embedding_store.py +++ b/tests/unit_tests/compute/test_embedding_store.py @@ -13,6 +13,8 @@ import numpy as np from shapash.compute.embedding_store import EmbeddingStore +from shapash.compute.embeddings import Embedding +from shapash.compute.hashing import hash_corpus from shapash.model.base import EmbeddingSource TEXTS = ["alpha", "beta", "gamma"] @@ -102,7 +104,7 @@ def test_without_cache_dir_nothing_is_written(self): with tempfile.TemporaryDirectory() as d: store = EmbeddingStore(FakeModel(), TEXTS) store.vectors() - self.assertIsNone(store.path("emb")) + self.assertIsNone(store.path) self.assertEqual(list(Path(d).iterdir()), []) @@ -115,37 +117,23 @@ def test_second_store_loads_from_disk(self): self.assertEqual(fresh.calls, 0) # served entirely from disk np.testing.assert_allclose(vectors, [[5.0, 1.0], [4.0, 1.0], [5.0, 1.0]]) - def test_derived_arrays_cache_beside_the_embeddings(self): - """A projection is cached under its own tag but the same key, so both travel together.""" - with tempfile.TemporaryDirectory() as d: - store = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) - fits = [] - - def project(): - fits.append(1) - return store.vectors()[:, :1] - - store.cached_array("pca.proj", project) - fresh = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) - fresh.cached_array("pca.proj", project) - self.assertEqual(len(fits), 1) # the reducer did not re-fit - self.assertTrue(store.path("pca.proj").exists()) - self.assertTrue(store.path("emb").exists()) - - def test_clear_drops_every_tag_under_the_key(self): - """Half-clearing would leave a projection derived from vectors that no longer exist.""" + def test_clear_drops_the_cached_file(self): with tempfile.TemporaryDirectory() as d: store = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) store.vectors() - store.cached_array("pca.proj", lambda: np.zeros((3, 2))) store.clear() - self.assertFalse(store.path("emb").exists()) - self.assertFalse(store.path("pca.proj").exists()) + self.assertFalse(store.path.exists()) model = FakeModel() EmbeddingStore(model, TEXTS, cache_dir=d).vectors() self.assertEqual(model.calls, 1) # really recomputed, not silently reloaded + def test_clear_without_a_cache_dir_is_a_no_op(self): + store = EmbeddingStore(FakeModel(), TEXTS) + store.embedding() + store.clear() + self.assertEqual(store.embedding().n_samples, len(TEXTS)) + class TestKeySeparation(unittest.TestCase): """Each ingredient of the key must, on its own, produce a different cache entry.""" @@ -174,5 +162,31 @@ def test_same_configuration_collides_on_purpose(self): self.assertEqual(EmbeddingStore(model, TEXTS).key, EmbeddingStore(FakeModel(), list(TEXTS)).key) +class TestEmbeddingArtifact(unittest.TestCase): + """The store returns provenance, not a bare array.""" + + def test_embedding_carries_the_identity_the_store_keyed_on(self): + model = FakeModel(model_id="fake:v2", space="pooled") + embedding = EmbeddingStore(model, TEXTS).embedding() + + self.assertEqual(embedding.model_id, "fake:v2") + self.assertEqual(embedding.space, "pooled") + self.assertEqual(embedding.corpus_id, hash_corpus(TEXTS)) + self.assertIsNone(embedding.reducer_tag, "raw model output is not a reduction") + np.testing.assert_allclose(embedding.vectors, model.embed(TEXTS)) + + def test_entries_are_written_as_loadable_embedding_files(self): + # One format, two locations: a cache entry is exactly what Embedding.save writes, so it can + # be copied out and shared without any conversion step. + with tempfile.TemporaryDirectory() as d: + store = EmbeddingStore(FakeModel(), TEXTS, cache_dir=d) + store.embedding() + straight_from_disk = Embedding.load(store.path) + + self.assertEqual(straight_from_disk.corpus_id, hash_corpus(TEXTS)) + self.assertEqual(straight_from_disk.model_id, "fake:v1") + np.testing.assert_allclose(straight_from_disk.vectors, store.vectors()) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/compute/test_embeddings.py b/tests/unit_tests/compute/test_embeddings.py new file mode 100644 index 00000000..35a45487 --- /dev/null +++ b/tests/unit_tests/compute/test_embeddings.py @@ -0,0 +1,167 @@ +"""Unit tests for ``Embedding`` (the vectors artifact and its ``.npz`` persistence) and for +``projection_coords``, the one check that pairs a projection with an explanation.""" + +import json +import tempfile +import unittest +import zipfile +from pathlib import Path +from types import SimpleNamespace + +import numpy as np + +from shapash.compute.embeddings import Embedding, projection_coords + + +class _Scaler: + """Deterministic stand-in for a reducer: keeps two columns, scaled.""" + + def __init__(self, scale=1.0): + self.scale = scale + + def fit_transform(self, vectors, **kwargs): + self.kwargs = kwargs + return np.asarray(vectors)[:, :2] * self.scale + + +def _embedding(n=4, d=6, **overrides) -> Embedding: + fields = { + "vectors": np.arange(n * d, dtype="float32").reshape(n, d), + "model_id": "fake:v1", + "space": "decision", + "corpus_id": "c0ffee", + } + fields.update(overrides) + return Embedding(**fields) + + +class TestConstruction(unittest.TestCase): + def test_reports_its_shape(self): + emb = _embedding(n=4, d=6) + self.assertEqual((emb.n_samples, emb.n_components), (4, 6)) + + def test_vectors_are_sealed_against_in_place_writes(self): + emb = _embedding() + with self.assertRaises(ValueError): + emb.vectors[0, 0] = 999.0 + + def test_repr_says_what_it_is_at_a_glance(self): + self.assertIn("n_samples=4, n_components=6, space='decision'", repr(_embedding())) + self.assertIn("reducer='_scaler'", repr(_embedding().project(_Scaler()))) + + def test_repr_omits_identity_strings(self): + # model_id and corpus_id are cache-lookup fingerprints, not something to read at a glance. + repr_str = repr(_embedding()) + self.assertNotIn("fake:v1", repr_str) + self.assertNotIn("c0ffee", repr_str) + + def test_a_1d_array_is_refused(self): + with self.assertRaises(ValueError) as ctx: + Embedding(np.zeros(5), model_id="m", space="s", corpus_id="c") + self.assertIn("2-D", str(ctx.exception)) + + +class TestProjection(unittest.TestCase): + def test_project_inherits_provenance_and_records_the_reducer(self): + emb = _embedding() + projected = emb.project(_Scaler(scale=2.0)) + + self.assertEqual( + (projected.corpus_id, projected.model_id, projected.space), (emb.corpus_id, emb.model_id, emb.space) + ) + self.assertEqual(projected.reducer_tag, "_scaler") + np.testing.assert_allclose(projected.vectors, emb.vectors[:, :2] * 2.0) + + def test_default_reducer_is_pca_to_two_components(self): + projected = _embedding(n=5, d=8).project() + self.assertEqual((projected.n_components, projected.reducer_tag), (2, "pca")) + + def test_fit_transform_kwargs_are_forwarded(self): + reducer = _Scaler() + _embedding().project(reducer, init="pca") + self.assertEqual(reducer.kwargs, {"init": "pca"}) + + def test_a_reducer_that_drops_rows_is_refused(self): + # Row i must stay the projection of text i, or every point is labelled from the wrong sample. + class _DropsRows: + def fit_transform(self, vectors, **kwargs): + return np.asarray(vectors)[:-1, :2] + + with self.assertRaises(ValueError) as ctx: + _embedding().project(_DropsRows()) + self.assertIn("one row per text", str(ctx.exception)) + + +class TestPersistence(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.tmp_path = Path(self._tmp.name) + + def test_round_trip_preserves_vectors_and_provenance(self): + emb = _embedding().project(_Scaler(scale=3.0)) + path = self.tmp_path / "run.emb" + emb.save(path) + loaded = Embedding.load(path) + + np.testing.assert_allclose(loaded.vectors, emb.vectors) + self.assertEqual( + (loaded.model_id, loaded.space, loaded.corpus_id, loaded.reducer_tag), + (emb.model_id, emb.space, emb.corpus_id, emb.reducer_tag), + ) + + def test_raw_embeddings_round_trip_with_a_null_reducer_tag(self): + path = self.tmp_path / "raw.emb" + _embedding().save(path) + self.assertIsNone(Embedding.load(path).reducer_tag) + + def test_the_path_is_written_verbatim(self): + # np.savez appends ".npz" to a path given by name that lacks it. + path = self.tmp_path / "run.emb" + _embedding().save(path) + self.assertEqual([p.name for p in self.tmp_path.iterdir()], ["run.emb"]) + + def test_the_container_is_a_zip_of_npy_members_with_json_meta(self): + path = self.tmp_path / "run.emb" + _embedding().save(path) + with zipfile.ZipFile(path) as zf: + self.assertEqual(sorted(zf.namelist()), ["meta.npy", "vectors.npy"]) + with np.load(path, allow_pickle=False) as archive: + meta = json.loads(str(archive["meta"].item())) + self.assertEqual(meta["corpus_id"], "c0ffee") + + def test_load_refuses_pickled_payloads(self): + path = self.tmp_path / "pickled.emb" + with path.open("wb") as fh: + np.savez(fh, vectors=np.array([{"not": "an array"}], dtype=object), meta="{}") + with self.assertRaises(ValueError): + Embedding.load(path) + + +class TestProjectionCoords(unittest.TestCase): + explanation = SimpleNamespace(n_samples=4, corpus_id="c0ffee") + + def test_an_embedding_of_the_same_texts_returns_its_vectors(self): + projection = _embedding().project(_Scaler()) + np.testing.assert_array_equal(projection_coords(projection, self.explanation), projection.vectors) + + def test_an_embedding_of_different_texts_is_refused(self): + projection = _embedding(corpus_id="other").project(_Scaler()) + with self.assertRaises(ValueError) as ctx: + projection_coords(projection, self.explanation) + self.assertIn("different texts", str(ctx.exception)) + + def test_an_unreduced_embedding_is_refused(self): + with self.assertRaises(ValueError) as ctx: + projection_coords(_embedding(), self.explanation) + self.assertIn("project", str(ctx.exception)) + + def test_a_bare_array_is_accepted_on_shape_alone(self): + coords = projection_coords(np.zeros((4, 2)), self.explanation) + self.assertEqual(coords.shape, (4, 2)) + with self.assertRaises(ValueError): + projection_coords(np.zeros((3, 2)), self.explanation) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/nlp/test_nlp_explanation.py index 5ade461a..76770365 100644 --- a/tests/unit_tests/nlp/test_nlp_explanation.py +++ b/tests/unit_tests/nlp/test_nlp_explanation.py @@ -2,7 +2,7 @@ Covers the round-trip (save -> load) across the shape matrix that matters: 1-D (binary/ regression) vs 2-D (multi-class) contribution arrays, with/without a baseline, with/without -ground truth and probabilities, with/without a bundled scatter projection, and a sample with +ground truth and probabilities, and a sample with zero tokens (the edge case that broke a naive "infer counts from the tidy tables" design — see ``_n_classes``/``_frames_to_contributions`` in the module under test). """ @@ -64,13 +64,13 @@ def setUp(self): self.addCleanup(self._tmp.cleanup) self.tmp_path = Path(self._tmp.name) - def _round_trip(self, expl: NlpExplanation, scatter_xy=None) -> tuple[NlpExplanation, np.ndarray | None]: + def _round_trip(self, expl: NlpExplanation) -> NlpExplanation: path = self.tmp_path / "explanation.zip" - expl.save(path, scatter_xy=scatter_xy) + expl.save(path) return NlpExplanation.load(path) - def _assert_round_trips(self, expl: NlpExplanation, scatter_xy=None): - loaded, loaded_scatter = self._round_trip(expl, scatter_xy=scatter_xy) + def _assert_round_trips(self, expl: NlpExplanation): + loaded = self._round_trip(expl) for original, restored in zip(expl.values, loaded.values, strict=True): np.testing.assert_allclose(original, restored) @@ -101,15 +101,12 @@ def _assert_round_trips(self, expl: NlpExplanation, scatter_xy=None): self.assertEqual(loaded.label_names, expl.label_names) self.assertEqual(loaded.folds_case, expl.folds_case) - if scatter_xy is None: - self.assertIsNone(loaded_scatter) - else: - np.testing.assert_allclose(loaded_scatter, scatter_xy) + # Survives the round trip because the texts do: it is derived, never stored-and-restored. + self.assertEqual(loaded.corpus_id, expl.corpus_id) - def test_multiclass_with_base_ground_truth_probabilities_and_scatter(self): + def test_multiclass_with_base_ground_truth_and_probabilities(self): expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) - scatter = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]) - self._assert_round_trips(expl, scatter_xy=scatter) + self._assert_round_trips(expl) def test_binary_without_ground_truth_or_probabilities(self): expl = _make_explanation(values_ndim=1, with_base=True, with_true=False, with_prob=False) @@ -138,27 +135,6 @@ def test_meta_json_is_plain_text_readable_without_shapash(self): self.assertIn("shapash_version", meta) self.assertIn("created_at", meta) - def test_unsupported_format_version_raises_actionable_error(self): - expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) - path = self.tmp_path / "explanation.zip" - expl.save(path) - - with zipfile.ZipFile(path) as zin: - meta = json.loads(zin.read("meta.json")) - meta["format_version"] = 999 - members = {item.filename: zin.read(item.filename) for item in zin.infolist()} - members["meta.json"] = json.dumps(meta).encode() - - bad_path = self.tmp_path / "bad_version.zip" - with zipfile.ZipFile(bad_path, "w") as zout: - for name, data in members.items(): - zout.writestr(name, data) - - with self.assertRaises(ValueError) as ctx: - NlpExplanation.load(bad_path) - self.assertIn("format_version", str(ctx.exception)) - self.assertIn("999", str(ctx.exception)) - def _resave_without_output_space(self, expl: NlpExplanation) -> Path: """A file as an earlier shapash (pre-``output_space``) would have written it.""" path = self.tmp_path / "legacy.zip" @@ -180,13 +156,47 @@ def test_legacy_file_without_output_space_defaults_by_backend(self): shap_expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) lig_expl = replace(shap_expl, backend_name="nlp_captum_lig") - loaded_shap, _ = NlpExplanation.load(self._resave_without_output_space(shap_expl)) - loaded_lig, _ = NlpExplanation.load(self._resave_without_output_space(lig_expl)) + loaded_shap = NlpExplanation.load(self._resave_without_output_space(shap_expl)) + loaded_lig = NlpExplanation.load(self._resave_without_output_space(lig_expl)) self.assertEqual(loaded_shap.output_space, "probability") self.assertEqual(loaded_lig.output_space, "logit") +class TestCorpusIdentity(unittest.TestCase): + """``corpus_id`` — the texts-only digest that pairs an explanation with its embeddings. + + It has to depend on the texts and on *nothing else*, because that is what makes two artifacts + over one dataset recognisable as such: a SHAP and a Captum explanation of the same batch, an + ``Embedding`` of it, a projection of that embedding. + """ + + def test_same_texts_agree_across_backend_and_model(self): + shap_expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + lig_expl = replace(shap_expl, backend_name="nlp_captum_lig", output_space="logit", model_id="other/model") + self.assertEqual(shap_expl.corpus_id, lig_expl.corpus_id) + + def test_different_texts_disagree(self): + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + other = replace(expl, texts=pd.Series(["hello world", "i am happy today", "KO"], index=expl.texts.index)) + self.assertNotEqual(expl.corpus_id, other.corpus_id) + + def test_order_is_part_of_the_identity(self): + # Row i of a projection means "the projection of texts[i]", so a reordered corpus is a + # different corpus even though it holds the same strings. + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + shuffled = replace(expl, texts=pd.Series(expl.texts.tolist()[::-1], index=expl.texts.index)) + self.assertNotEqual(expl.corpus_id, shuffled.corpus_id) + + def test_reindexing_does_not_change_it(self): + # relabelled() rebinds texts to the caller's index without touching the strings; an id that + # moved here would break the pairing for every explanation served out of the explain cache. + expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + reindexed = pd.Series(expl.texts.tolist(), index=[0, 1, 2]) + derived = expl.relabelled(texts=reindexed, y_true=None) + self.assertEqual(derived.corpus_id, expl.corpus_id) + + if __name__ == "__main__": unittest.main() @@ -233,6 +243,19 @@ def test_n_classes_survives_an_all_empty_batch(self): ) self.assertEqual(explanation.n_classes, 2) # recovered from label_names + def test_repr_is_a_one_line_summary_not_the_dataclass_default(self): + # The generated dataclass repr would print every text, token and contribution array in + # full — a notebook cell that just names the variable must not dump the whole batch. + explanation = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) + repr_str = repr(explanation) + self.assertEqual( + repr_str, "NlpExplanation(n_samples=3, n_classes=2, backend='nlp_shap', has_ground_truth=True)" + ) + + def test_repr_reports_has_ground_truth_false_when_absent(self): + explanation = _make_explanation(values_ndim=2, with_base=True, with_true=False, with_prob=True) + self.assertIn("has_ground_truth=False", repr(explanation)) + class TestFieldPartition(unittest.TestCase): """The caller/computed split that makes memoizing on text content alone sound.""" @@ -369,6 +392,6 @@ def test_a_saved_artifact_reloads_with_one_index(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "run.zip" self.explanation.save(path) - restored, _ = NlpExplanation.load(path) + restored = NlpExplanation.load(path) self.assertTrue(restored.y_pred.index.equals(restored.texts.index)) self.assertTrue(restored.y_true.index.equals(restored.texts.index)) diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py index 476a621c..09277a6f 100644 --- a/tests/unit_tests/nlp/test_nlp_plotter.py +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -16,6 +16,7 @@ from dash import html from plotly import graph_objs as go +from shapash.compute.embeddings import Embedding from shapash.explainer.nlp_explanation import NlpExplanation from shapash.explainer.nlp_plotter import NlpPlotter from shapash.webapp.utils.dash_to_html import DashHtmlPreview @@ -208,6 +209,37 @@ def test_ground_truth_and_errors_only_with_real_ground_truth(self): self.assertEqual(names, {"pos"}) +class TestScatterProjectionArgument(unittest.TestCase): + """Wiring only — the check itself is tested in ``test_embeddings.TestProjectionCoords``.""" + + @staticmethod + def _projection(explanation, **overrides): + fields = { + "vectors": np.zeros((explanation.n_samples, 2)), + "model_id": "fake:v1", + "space": "decision", + "corpus_id": explanation.corpus_id, + "reducer_tag": "pca-1234", + } + fields.update(overrides) + return Embedding(**fields) + + def test_a_typed_projection_is_accepted(self): + explanation = _make_explanation() + fig = explanation.plot.scatter(self._projection(explanation)) + self.assertIsInstance(fig, go.Figure) + + def test_a_projection_from_another_corpus_is_refused(self): + explanation = _make_explanation() + with self.assertRaises(ValueError) as ctx: + explanation.plot.scatter(self._projection(explanation, corpus_id="a-different-corpus")) + self.assertIn("different texts", str(ctx.exception)) + + def test_a_bare_array_is_still_accepted_on_its_shape(self): + explanation = _make_explanation() + self.assertIsInstance(explanation.plot.scatter(np.zeros((3, 2))), go.Figure) + + class TestGuards(unittest.TestCase): def test_waterfall_refuses_on_a_non_additive_backend(self): """LIME contributions do not sum to the prediction, so the running total is nonsense.""" diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 9efb02c4..1c1d2972 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -7,8 +7,10 @@ runs in CI without transformers/datasets. """ +import json import tempfile import unittest +import zipfile from dataclasses import replace from unittest.mock import patch @@ -20,6 +22,7 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_lime_backend import NlpLimeBackend from shapash.backend.nlp_shap_backend import NlpShapBackend +from shapash.compute.embeddings import Embedding from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator from shapash.compute.retrieval import Neighbor from shapash.explainer.nlp_explainer import NlpExplainer @@ -690,16 +693,13 @@ def shap_callable(self): class _CountingReducer: - """A reducer with sklearn's ``get_params`` surface, so its settings reach the cache tag.""" + """A deterministic reducer that counts its fits and records its call-time kwargs.""" def __init__(self, scale=1.0): self.scale = scale self.fits = 0 self.last_kwargs = None - def get_params(self, deep=True): - return {"scale": self.scale} - def fit_transform(self, x, **kwargs): self.fits += 1 self.last_kwargs = kwargs @@ -727,7 +727,7 @@ def _projection_explanation(texts: pd.Series) -> NlpExplanation: class TestComputeProjection(unittest.TestCase): - """The library owns the space + the caching; the caller injects only the reducer.""" + """The library owns the space + the embedding cache; the caller injects only the reducer.""" def setUp(self): self.model = _ProjectableModel() @@ -735,20 +735,42 @@ def setUp(self): self.explanation = _projection_explanation(pd.Series(["alpha", "beta banana", "gamma"])) def test_returns_two_columns_aligned_with_the_texts(self): - xy = self.xpl.compute_projection(self.explanation) - self.assertEqual(xy.shape, (3, 2)) + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.vectors.shape, (3, 2)) + self.assertEqual(projection.n_samples, self.explanation.n_samples) def test_defaults_to_pca_without_any_extra_dependency(self): """The default reducer must be something a core install already has — sklearn's PCA.""" - xy = self.xpl.compute_projection(self.explanation) - self.assertEqual(xy.shape, (3, 2)) + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.n_components, 2) self.assertEqual(self.model.calls, 1) def test_injected_reducer_is_used(self): reducer = _CountingReducer(scale=2.0) - xy = self.xpl.compute_projection(self.explanation, reducer=reducer) + projection = self.xpl.compute_projection(self.explanation, reducer=reducer) self.assertEqual(reducer.fits, 1) - np.testing.assert_allclose(xy[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled + np.testing.assert_allclose(projection.vectors[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled + + def test_projection_carries_the_provenance_needed_to_pair_it_back(self): + # The point of returning an Embedding rather than a bare array: the coordinates say which + # corpus, model and space they belong to, so a mismatch can be caught instead of drawn. + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.corpus_id, self.explanation.corpus_id) + self.assertEqual(projection.model_id, self.model.model_id) + self.assertEqual(projection.space, self.model.resolve_space()) + self.assertEqual(projection.reducer_tag, "pca") + + def test_embeddings_are_raw_and_reusable_for_several_projections(self): + # The flow the split exists for: embed once (needs the model), reduce many times (does not). + embedding = self.xpl.compute_embeddings(self.explanation) + self.assertEqual(self.model.calls, 1) + self.assertIsNone(embedding.reducer_tag) + self.assertEqual(embedding.corpus_id, self.explanation.corpus_id) + + first = embedding.project(_CountingReducer(scale=1.0)) + second = embedding.project(_CountingReducer(scale=3.0)) + self.assertEqual(self.model.calls, 1, "reducing must not re-embed") + np.testing.assert_allclose(second.vectors, first.vectors * 3.0) def test_raises_for_a_model_that_cannot_embed(self): """A prediction-only model gets a clear error pointing at the escape hatch, not an AttributeError.""" @@ -757,7 +779,8 @@ def test_raises_for_a_model_that_cannot_embed(self): with self.assertRaises(TypeError): xpl.compute_projection(explanation) - def test_cached_across_instances(self): + def test_embeddings_cached_across_instances_while_the_reducer_reruns(self): + # Only the expensive half is cached; a layout worth keeping is saved explicitly. with tempfile.TemporaryDirectory() as d: reducer = _CountingReducer() self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) @@ -765,18 +788,11 @@ def test_cached_across_instances(self): fresh_model = _ProjectableModel() fresh = NlpExplainer(fresh_model, backend=object()) fresh.compute_projection(self.explanation, reducer=reducer, cache_dir=d) - self.assertEqual(fresh_model.calls, 0) # neither embedded - self.assertEqual(reducer.fits, 1) # nor re-fitted - - def test_reducer_settings_take_part_in_the_key(self): - """Re-tuning a reducer must not silently reload the previous scatter.""" - with tempfile.TemporaryDirectory() as d: - a = self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(scale=1.0), cache_dir=d) - b = self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(scale=3.0), cache_dir=d) - np.testing.assert_allclose(b, a * 3.0) + self.assertEqual(fresh_model.calls, 0) + self.assertEqual(reducer.fits, 2) def test_model_space_takes_part_in_the_key(self): - """Moving the model's space must re-project, not reload the other space's coordinates.""" + """Moving the model's space must re-embed, not reload the other space's vectors.""" with tempfile.TemporaryDirectory() as d: self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) self.model.space = "pooled" @@ -798,22 +814,6 @@ def test_fit_transform_kwargs_are_forwarded(self): self.xpl.compute_projection(self.explanation, reducer=reducer, init="pca", verbose=False) self.assertEqual(reducer.last_kwargs, {"init": "pca", "verbose": False}) - def test_fit_transform_kwargs_take_part_in_the_key(self): - """Changing a call-time argument (e.g. PaCMAP's ``init=``) must not silently reload.""" - with tempfile.TemporaryDirectory() as d: - reducer = _CountingReducer() - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="b") - self.assertEqual(reducer.fits, 2) - - def test_same_fit_transform_kwargs_reuse_the_cache(self): - with tempfile.TemporaryDirectory() as d: - reducer = _CountingReducer() - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, init="a") - self.assertEqual(reducer.fits, 1) - - # --------------------------------------------------------------------------- # NlpExplainer.fit — reference state, and when its cost is paid # --------------------------------------------------------------------------- @@ -1069,16 +1069,25 @@ def test_scatter_absent_when_no_xy(self): self.assertNotIn("scatter-plot", ids) self.assertNotIn("color-by", ids) - def test_scatter_present_when_xy_given(self): - xy = np.zeros((3, 2)) - webapp = NlpWebApp(self.explanation, engine=self.xpl, scatter_xy=xy) + def test_scatter_present_when_a_projection_is_given(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((3, 2))) ids = self._collect_ids(webapp.app.layout) self.assertIn("scatter-plot", ids) self.assertIn("color-by", ids) + def test_scatter_mounts_from_a_matching_embedding(self): + projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id=self.explanation.corpus_id, reducer_tag="pca") + ids = self._collect_ids(NlpWebApp(self.explanation, engine=self.xpl, projection=projection).app.layout) + self.assertIn("scatter-plot", ids) + + def test_an_embedding_of_other_texts_is_refused_at_construction(self): + projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id="other-texts", reducer_tag="pca") + with self.assertRaises(ValueError): + NlpWebApp(self.explanation, engine=self.xpl, projection=projection) + def test_scatter_wrong_shape_raises(self): with self.assertRaises(ValueError): - NlpWebApp(self.explanation, engine=self.xpl, scatter_xy=np.zeros((5, 2))) # 5 rows but only 3 samples + NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((5, 2))) # 5 rows but only 3 samples # ── Error Analysis tab (confusion matrix) ───────────────────────── @@ -2289,6 +2298,23 @@ def test_key_is_order_sensitive(self): self.assertNotEqual(xpl._compute_key(["a", "b"]), xpl._compute_key(["b", "a"])) +def _read_meta(path) -> dict: + with zipfile.ZipFile(path) as zf: + return json.loads(zf.read("meta.json")) + + +def _drop_meta_key(path, key) -> None: + """Rewrite a saved ``.xpl`` in place without one ``meta.json`` key — how an older layout is faked.""" + with zipfile.ZipFile(path) as zin: + members = {item.filename: zin.read(item.filename) for item in zin.infolist()} + meta = json.loads(members["meta.json"]) + del meta[key] + members["meta.json"] = json.dumps(meta).encode() + with zipfile.ZipFile(path, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + + class TestExplainDiskCacheIsolation(unittest.TestCase): """The disk cache is the dangerous case: a stale entry survives the process that wrote it.""" @@ -2334,6 +2360,43 @@ def test_cache_path_points_at_the_file_explain_writes(self): xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) self.assertTrue(path.exists(), "cache_path disagrees with where explain() wrote") + def test_stale_layout_is_a_miss_not_a_crash(self): + # The cache key covers texts/model/backend but not the shapash version, so a layout change + # leaves entries the new reader cannot parse. The entry must be recomputed and overwritten. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=4.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + _drop_meta_key(path, "values_ndim") + + backend = _MarkerBackend(marker=4.0) + explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend.calls, 1, "a rejected cache entry must fall through to a recompute") + np.testing.assert_allclose(explanation.values[0], 4.0) + self.assertIn("values_ndim", _read_meta(path), "stale entry was not replaced") + + def test_corrupt_cache_entry_is_a_miss_not_a_crash(self): + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=5.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + path.write_bytes(b"not a zip file") + + backend = _MarkerBackend(marker=5.0) + explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend.calls, 1) + np.testing.assert_allclose(explanation.values[0], 5.0) + + def test_explicit_load_still_raises_on_a_stale_file(self): + # The miss-not-crash rule is scoped to the cache. A caller who names a file gets an error + # rather than a silent substitution. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer().explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + _drop_meta_key(path, "values_ndim") + with self.assertRaises(KeyError): + NlpExplanation.load(path) + def test_clear_cache_forces_a_recompute(self): with tempfile.TemporaryDirectory() as cache_dir: backend = _MarkerBackend() @@ -2529,14 +2592,18 @@ def test_url_base_pathname_reaches_the_webapp(self): explanation = object() with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: xpl.run_app(explanation, url_base_pathname="/shapash-nlp-explainer/") - web_app.assert_called_once_with( - explanation, - engine=xpl, - scatter_xy=None, - url_base_pathname="/shapash-nlp-explainer/", - palette_name="default", - colors_dict=None, - info={}, + + self.assertIs(web_app.call_args.args[0], explanation) + self.assertEqual( + web_app.call_args.kwargs, + { + "engine": xpl, + "projection": None, + "url_base_pathname": "/shapash-nlp-explainer/", + "palette_name": "default", + "colors_dict": None, + "info": {}, + }, ) def test_defaults_to_no_prefix(self): diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index ed4d3614..a516616c 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -24,9 +24,16 @@ LabelNoiseComponent, SimilarExamplesComponent, ) +from shapash.webapp.nlp_components.base import AppContext LABEL_NAMES = ["neg", "pos"] + +def _ctx(explanation, engine=None, coords=None) -> AppContext: + """The context ``NlpWebApp`` builds, for calling a component directly.""" + return AppContext(explanation, engine=engine, coords=coords) + + # A tiny lexically separable corpus for the independent label probe. "bad"/"awful" sit in "neg", # so a row labelled "neg" gets backed and a row labelled "pos" gets rejected. _PROBE_CORPUS = ( @@ -328,7 +335,7 @@ def _ids(self, engine, **kwargs): return app, found def test_full_tab_groups_when_all_panels_available(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) self.assertEqual( app._tab_groups, { @@ -366,7 +373,7 @@ def test_counterfactual_tab_only_with_editor(self): self.assertNotIn("counterfactual", app._tab_groups["upper-right-tabs"]) def test_selection_bar_present(self): - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), scatter_xy=np.zeros((2, 2))) + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) self.assertIn("selection-summary", ids) self.assertIn("word-filter-clear-btn", ids) self.assertIn("scatter-clear-btn", ids) @@ -400,7 +407,7 @@ def test_current_datapoint_written_by_row_and_editor(self): def test_tab_toggle_callbacks_registered(self): engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine, scatter_xy=np.zeros((2, 2))) + app = NlpWebApp(engine.to_explanation(), engine=engine, projection=np.zeros((2, 2))) outputs = " ".join(app.app.callback_map.keys()) self.assertIn("left-tabs-body-table.style", outputs) self.assertIn("lower-right-tabs-body-waterfall.style", outputs) @@ -441,7 +448,7 @@ def _register(): app = dash.Dash(__name__) comp = SimilarExamplesComponent() comp.register_callbacks( - app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"} + app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} ) return app, engine @@ -459,7 +466,7 @@ def test_layout_shows_layer_caption(self): engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) found = set() - _collect_ids(SimilarExamplesComponent().layout(engine.to_explanation(), engine), found) + _collect_ids(SimilarExamplesComponent().layout(_ctx(engine.to_explanation(), engine)), found) self.assertIn("similar-topk", found) self.assertIn("similar-threshold", found) self.assertIn("similar-mode", found) @@ -615,13 +622,13 @@ def test_mounts_without_any_live_capability(self): def test_caption_warns_when_no_independent_cross_check_is_available(self): engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True) - layout = LabelNoiseComponent().layout(engine.to_explanation(), engine) + layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) caption = layout.children[1].children self.assertIn("no independent cross-check", caption) def test_caption_explains_the_corpus_column_when_the_probe_is_available(self): engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, probe_corpus=_PROBE_CORPUS) - layout = LabelNoiseComponent().layout(engine.to_explanation(), engine) + layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) caption = layout.children[1].children self.assertIn("Corpus column", caption) @@ -644,7 +651,7 @@ def _register(**kwargs): app = dash.Dash(__name__) comp = LabelNoiseComponent() comp.register_callbacks( - app, explanation, engine, {"apply": "whatif-apply-store", "current": "current-datapoint"} + app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} ) return app, engine @@ -935,7 +942,7 @@ def _app(self): explanation = self._explanation() app = dash.Dash(__name__) comp = WordProfileComponent() - comp.register_callbacks(app, explanation, None, self.STORES) + comp.register_callbacks(app, _ctx(explanation, None), self.STORES) return app, explanation @staticmethod @@ -951,13 +958,13 @@ def test_mounts_without_an_engine(self): from shapash.webapp.nlp_components import WordProfileComponent # Data-only panel: it must survive a loaded snapshot with no live model. - self.assertTrue(WordProfileComponent.is_available(self._explanation(), None)) + self.assertTrue(WordProfileComponent.is_available(_ctx(self._explanation(), None))) def test_layout_declares_every_id_its_callbacks_bind(self): from shapash.webapp.nlp_components import WordProfileComponent found = set() - _collect_ids(WordProfileComponent().layout(self._explanation(), None), found) + _collect_ids(WordProfileComponent().layout(_ctx(self._explanation(), None)), found) for suffix in ("select", "agg", "class", "order", "limit", "graph", "caption", "results", "store"): self.assertIn(f"word-profile-{suffix}", found) @@ -965,7 +972,7 @@ def test_word_dropdown_is_seeded_with_the_top_word(self): from shapash.webapp.nlp_components import WordProfileComponent explanation = self._explanation() - layout = WordProfileComponent().layout(explanation, None) + layout = WordProfileComponent().layout(_ctx(explanation, None)) found = {} def walk(node): @@ -991,7 +998,7 @@ def test_class_picker_hidden_for_a_single_output_column(self): base_values=np.zeros(4), label_names=["score"], ) - layout = WordProfileComponent().layout(binary, None) + layout = WordProfileComponent().layout(_ctx(binary, None)) found = set() _collect_ids(layout, found) # Still present (callbacks bind it), just not shown. @@ -1304,7 +1311,7 @@ def test_sign_filter_is_greyed_out_under_all_classes(self): def test_scatter_colouring_survives_all_classes(self): # The scatter reads the same dropdown, where int("all") would raise. - app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) fn = None for key, spec in app.app.callback_map.items(): if "scatter-plot.figure" in key: @@ -1380,12 +1387,12 @@ def test_bar_click_registered_without_a_scatter(self): def test_bar_click_registered_with_a_scatter_too(self): # Before Phase A, a mounted scatter changed which store a bar click targeted; now Word # Importance always registers this callback unconditionally, regardless of scatter presence. - app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) pairs = _callback_binding_ids(app, "global-importance-graph.clickData") self.assertIn(("global-importance-graph", "clickData"), pairs) def test_scatter_word_select_mirrors_the_shared_store(self): - app = NlpWebApp(self._explanation(), engine=None, scatter_xy=np.zeros((4, 2))) + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) pairs = _callback_binding_ids(app, "scatter-word-select.value") self.assertIn(("word-click-filter", "data"), pairs) @@ -1562,8 +1569,9 @@ def _explanation(): def _color_options(self, offer_word_contribution: bool): from shapash.webapp.nlp_components import ScatterComponent - comp = ScatterComponent(np.zeros((4, 2)), offer_word_contribution=offer_word_contribution) - layout = comp.layout(self._explanation(), None) + comp = ScatterComponent(offer_word_contribution=offer_word_contribution) + explanation = self._explanation() + layout = comp.layout(_ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2)))) found = {} def walk(node): @@ -1615,15 +1623,20 @@ def _explanation(with_true: bool, with_pred: bool): def _component(self): from shapash.webapp.nlp_components import ScatterComponent - return ScatterComponent(np.zeros((2, 2)), offer_word_contribution=True) + return ScatterComponent(offer_word_contribution=True) + + def _ctx_for(self, explanation): + # The figure takes its two coordinate columns from the projection and every visual encoding + # — colours, legend, hover, error mask — from the explanation, so both have to be bound. + return _ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2))) def test_ground_truth_colors_by_y_true(self): - fig = self._component()._build_scatter_fig(self._explanation(True, True), "ground_truth") + fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(True, True)), "ground_truth") names = {trace.name for trace in fig.data} self.assertEqual(names, {"pos"}) # both samples are "pos" in y_true def test_without_prediction_or_ground_truth_draws_a_single_unlabeled_group(self): - fig = self._component()._build_scatter_fig(self._explanation(False, False), "prediction") + fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(False, False)), "prediction") self.assertEqual(len(fig.data), 1) self.assertEqual(len(fig.data[0].x), 2) @@ -1635,7 +1648,7 @@ def _layout(self): from shapash.webapp.nlp_components import WordProfileComponent comp = WordProfileComponent() - layout = comp.layout(TestWordProfileComponent._explanation(), None) + layout = comp.layout(_ctx(TestWordProfileComponent._explanation(), None)) found = {} def walk(node): @@ -1657,9 +1670,9 @@ def _app(self): comp = WordProfileComponent() explanation = TestWordProfileComponent._explanation() - comp.layout(explanation, None) # builds the option lists the sort callback serves + comp.layout(_ctx(explanation, None)) # builds the option lists the sort callback serves app = dash.Dash(__name__) - comp.register_callbacks(app, explanation, None, TestWordProfileComponent.STORES) + comp.register_callbacks(app, _ctx(explanation, None), TestWordProfileComponent.STORES) return app @staticmethod diff --git a/tutorial/nlp/overview.ipynb b/tutorial/nlp/overview.ipynb index 6c090952..3276d3d4 100644 --- a/tutorial/nlp/overview.ipynb +++ b/tutorial/nlp/overview.ipynb @@ -199,7 +199,8 @@ "metadata": {}, "outputs": [], "source": [ - "explanations = xpl.explain(texts, y=[\"joy\", \"fear\", \"anger\"])" + "explanations = xpl.explain(texts, y=[\"joy\", \"fear\", \"anger\"])\n", + "explanations" ] }, { @@ -371,6 +372,39 @@ "explanations.plot.confusion()" ] }, + { + "cell_type": "markdown", + "id": "48578f32", + "metadata": {}, + "source": [ + "## Saving and loading explanations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5ca7fd2", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "expl_path = Path(\"explanations.xpl\")\n", + "\n", + "explanations.save(expl_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93a84be9", + "metadata": {}, + "outputs": [], + "source": [ + "from shapash.explainer.nlp_explanation import NlpExplanation\n", + "explanations = NlpExplanation.load(expl_path)" + ] + }, { "cell_type": "markdown", "id": "d348c5be", @@ -395,7 +429,18 @@ "outputs": [], "source": [ "# Native projection with PCA (default)\n", - "scatter_xy = xpl.compute_projection(explanations)" + "projection = xpl.compute_projection(explanations)\n", + "projection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4f6c152", + "metadata": {}, + "outputs": [], + "source": [ + "projection.vectors" ] }, { @@ -405,7 +450,7 @@ "metadata": {}, "outputs": [], "source": [ - "explanations.plot.scatter(scatter_xy, use_webgl=False)" + "explanations.plot.scatter(projection, use_webgl=False)" ] }, { @@ -413,7 +458,9 @@ "id": "4d2887f7", "metadata": {}, "source": [ - "You can use your own favorite projection" + "You can use your own favorite projection.\n", + "\n", + "`compute_projection` is a convenience wrapper over two steps that are worth separating when you want to compare projections: **embedding** needs the model, possibly a GPU pass; **reducing** needs only the vectors, and takes seconds. Embed once, then try as many reducers as you like — including in a later session where the model was never loaded at all." ] }, { @@ -423,43 +470,39 @@ "metadata": {}, "outputs": [], "source": [ - "# Custom projection (TSNE, PaCMAP, UMAP, etc.) with optional parameters\n", - "from sklearn.manifold import TSNE\n", - "reducer = TSNE(n_components=2, perplexity=2, random_state=42)\n", - "\n", - "xpl.compute_projection(explanations, reducer=reducer)" - ] - }, - { - "cell_type": "markdown", - "id": "48578f32", - "metadata": {}, - "source": [ - "## Saving and loading explanations" + "embeddings = xpl.compute_embeddings(explanations)\n", + "embeddings" ] }, { "cell_type": "code", "execution_count": null, - "id": "d5ca7fd2", + "id": "f0fbecb6", "metadata": {}, "outputs": [], "source": [ - "from pathlib import Path\n", - "my_save_path = Path(\"explanations.xpl\")\n", + "from sklearn.manifold import TSNE\n", "\n", - "explanations.save(my_save_path, scatter_xy=scatter_xy)" + "projection = embeddings.project(TSNE(n_components=2, perplexity=2, random_state=42))\n", + "explanations.plot.scatter(projection, use_webgl=False)" ] }, { "cell_type": "code", "execution_count": null, - "id": "93a84be9", + "id": "66df2cfe", "metadata": {}, "outputs": [], "source": [ - "from shapash.explainer.nlp_explanation import NlpExplanation\n", - "explanations, scatter_xy = NlpExplanation.load(my_save_path)" + "# You can save and reload the embeddings/projections to avoid recomputing it. This is especially useful for large datasets.\n", + "# The file carries the texts and the model it was embedded with, so it can be paired back later.\n", + "from pathlib import Path\n", + "proj_path = Path(\"projection.emb\")\n", + "\n", + "projection.save(proj_path)\n", + "\n", + "from shapash.compute.embeddings import Embedding\n", + "Embedding.load(proj_path)" ] }, { @@ -550,7 +593,7 @@ "metadata": {}, "outputs": [], "source": [ - "app = xpl.run_app(explanations, scatter_xy=scatter_xy)" + "app = xpl.run_app(explanations, projection=projection)" ] }, { @@ -581,7 +624,7 @@ "outputs": [], "source": [ "from shapash.webapp.nlp_app import NlpWebApp\n", - "NlpWebApp(explanations, engine=xpl).run()" + "app = NlpWebApp(explanations, engine=xpl, projection=projection).run()" ] } ], From 62ed1615bc42925f4bfcbe8a5608933d93ef3287 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 14 Sep 2026 15:50:00 +0200 Subject: [PATCH 112/144] doc(nlp): add technical readme for the nlp part --- docs/wiki/NLP-Explainability.md | 391 ++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 docs/wiki/NLP-Explainability.md diff --git a/docs/wiki/NLP-Explainability.md b/docs/wiki/NLP-Explainability.md new file mode 100644 index 00000000..d2f3c042 --- /dev/null +++ b/docs/wiki/NLP-Explainability.md @@ -0,0 +1,391 @@ +# Shapash NLP Explainer + +A tool to inspect the behavior of a text classifier, not just attribution +(which words drove a prediction) but also error analysis: where the model gets things wrong, whether a +misprediction is a model error or a bad label, and what minimal edit flips +a prediction (counterfactuals). + +> **Status: prototype, not yet unified with SmartExplainer** This is a standalone text-modality stack +> (`NlpExplainer` / `NlpExplanation` / `NlpWebApp`) built *alongside* `SmartExplainer`, not through it — +> it deliberately bypasses `SmartExplainer.compile()` rather than adding a `modality="text"` branch that +> would have to be written and then deleted once tabular and text explainers are unified behind one API. + +## Contents +- [Installation](#installation) +- [Why a separate stack](#why-a-separate-stack) +- [Architecture at a glance](#architecture-at-a-glance) +- [Quickstart](#quickstart) +- [`NlpExplainer` — the entry point](#nlpexplainer--the-entry-point) +- [`NlpExplanation` — the frozen artifact](#nlpexplanation--the-frozen-artifact) +- [The `TextModel` layer](#the-textmodel-layer) +- [Model & framework support](#model--framework-support) +- [Backends: SHAP, LIME and Captum LIG](#backends-shap-lime-and-captum-lig) +- [Counterfactual generators](#counterfactual-generators) +- [Diagnostics: label noise & label probe](#diagnostics-label-noise--label-probe) +- [The webapp](#the-webapp) +- [Where to go next](#where-to-go-next) +- [Known limitations](#known-limitations) + +## Installation + +The `shap` backend is a core dependency already. Everything else (HF models, torch, captum, the +counterfactual/diagnostics stack) is behind the `nlp` extra: + +```bash +uv sync --extra nlp +``` +or +```bash +pip install ".[nlp]" +``` + +which pulls in `torch`, `transformers`, `datasets`, `sentence-transformers`, `sentencepiece`, +`protobuf`, `captum`, `pacmap`, `dash-ag-grid`. + +The `nlp_lime` backend needs `lime`, which is its own extra rather than bundled into `nlp` (it's also +usable with tabular data, via `LimeBackend`). + +## Why a separate stack + +Rather than growing `compile()` to understand text, the NLP path was built as a **bridge prototype** +that: + +- explains a black-box text classifier (HF pipeline, HF checkpoint, or hand-rolled PyTorch model) with + SHAP or Captum Integrated Gradients — **classification only** (binary or multiclass), +- ships its own Dash webapp instead of extending the ~3000-line tabular `SmartApp`, +- and is explicitly modeled as a prototype of the target architecture — a capability-based + `Model` / `Interpreter` / `Generator` split similar to Google PAIR's + [LIT](https://pair-code.github.io/lit/) — so the pieces (`TextModel` capabilities, `Backend`, + `CounterfactualGenerator`, `InteractiveEngine`, capability-gated `WebappComponent`s) are meant to + generalize to tabular data later rather than being thrown away. + +## Architecture at a glance + +```mermaid +flowchart LR + Model["TextModel<br/>HF pipeline / checkpoint / custom"] --> Explainer["NlpExplainer"] + + Explainer -->|"fit(X_reference) — optional"| Bank[("EmbeddingStore<br/>similarity bank")] + Explainer -->|"explain(X, y)"| Explanation["NlpExplanation<br/>frozen artifact"] + Explainer -->|"compute_embeddings /<br/>compute_projection"| Embedding["Embedding<br/>vectors or 2-D projection"] + + Explanation -->|"save / load"| XplFile[(".xpl file")] + Embedding -->|"save / load"| EmbFile[(".npz file")] + Bank -.shares cache.- Embedding + + Explainer -->|"run_app(explanation, projection)<br/>engine = self"| WebApp["NlpWebApp"] + Explanation --> WebApp + Embedding -.->|"projection"| WebApp + + WebApp --> Ctx["AppContext<br/>explanation + engine + coords"] + Ctx --> Panels["capability-gated WebappComponents<br/>scatter · waterfall · what-if lab · ..."] +``` + +A loaded snapshot (`NlpExplanation.load()`) skips `NlpExplainer` entirely and reaches `NlpWebApp` +with `engine=None` — the diagram's "fit"/"compute_*" edges and the similarity bank are then absent, +and capability gating is what makes the What-if Lab panels self-disable rather than error. + +The same picture as method signatures: + +``` +NlpExplainer.fit(X_reference, y) # optional: enables similar-example retrieval + label probe +NlpExplainer.explain(X, y) -> NlpExplanation # memoized on (texts, model identity, backend+config); + # model-free, backend-free, frozen, save()/load()-able +NlpExplainer.run_app(explanation) -> NlpWebApp # Dash app; What-if Lab panels only if engine is live +``` + +...and as file paths: + +| Path | What's there | +|---|---| +| `shapash/model/` | `TextModel` ABC + capability mixins (`SupportsTokenization`, `SupportsEmbeddings`, `SupportsGradients`, `SupportsCaptumIG`) + HF / torch adapters | +| `shapash/backend/` | `nlp_shap`, `nlp_lime`, `nlp_captum_lig` — self-register by `.name` | +| `shapash/compute/generators/` | `CounterfactualGenerator` ABC (`HotFlip`, `AblationFlip`) — shared minimal-perturbation search | +| `shapash/compute/diagnostics/` | `label_noise.py` (confident learning), `label_probe.py` (independent label-vs-model-error probe) | +| `shapash/compute/embeddings.py`, `embedding_store.py` | `Embedding` (raw vectors or a 2-D projection) + its on-disk cache, keyed by (model, space, corpus) | +| `shapash/compute/retrieval/` | Nearest-neighbour bank over the same cached embeddings — powers `find_similar` / `find_similar_threshold` | +| `shapash/explainer/nlp_explainer.py` | `NlpExplainer` — orchestrates the above, implements `InteractiveEngine` | +| `shapash/explainer/nlp_explanation.py` | `NlpExplanation` — the returned artifact, + `.plot` | +| `shapash/explainer/interactive.py` | `InteractiveEngine` — the "live compute" protocol | +| `shapash/webapp/nlp_app.py` + `nlp_components/` | `NlpWebApp` — capability-gated Dash panels | + +A model only needs to satisfy the capabilities a given feature requires — e.g. `AblationFlipGenerator` +works on prediction-only models (`SupportsTokenization` only), while `HotFlipGenerator` and the Captum +LIG backend need gradient/embedding access (`SupportsGradients`, `SupportsEmbeddings`, +`SupportsCaptumIG`). `TextModel.has_capabilities(model, *caps)` is the single mechanism everything +(generators, backends, webapp panels) uses to check this — no isinstance-on-concrete-class checks. + +## Quickstart + +The same flow, worked through end to end, lives in `tutorial/nlp/overview.ipynb` (notebook +walkthrough) and `demo/serve_nlp.py` (a runnable script serving the webapp — see +`demo/nlp_cache/` for a pre-computed example run). + +```python +from shapash.model import HFClassifierModel +from shapash.explainer.nlp_explainer import NlpExplainer + +model = HFClassifierModel.from_pretrained( + "bhadresh-savani/distilbert-base-uncased-emotion", + trust_remote_code=False, # never leave unset — unset makes transformers prompt interactively +) + +xpl = NlpExplainer(model) # backend defaults to NlpShapBackend +xpl = xpl.fit( + X_reference=ref_texts, y=ref_labels +) # optional — enables similar-example retrieval +explanation = xpl.explain(texts, y=labels, cache_dir="demo/nlp_cache") + +explanation.plot.word_importance() # corpus-level view +explanation.save("emotion_run.xpl") # zip of meta.json + parquet, no pickle + +xpl.run_app(explanation, port=8050) # live app, What-if Lab enabled (engine=xpl bound) +``` + +A snapshot reloaded later has no bound engine: + +```python +from shapash.explainer.nlp_explanation import NlpExplanation + +explanation = NlpExplanation.load("emotion_run.xpl") +# run_app(explanation) still works, but What-if Lab panels self-disable — no engine to call back into +``` + +The scatter travels separately, as its own `Embedding` file — a projection is a fact about the +embeddings, not about the explanation, and the two have different keys: + +```python +from shapash.compute.embeddings import Embedding + +embedding = Embedding.load("emotion.emb") # raw vectors — needs no model to reduce +projection = embedding.project( + TSNE(n_components=2) +) # try as many as you like, seconds each +NlpWebApp(explanation, projection=projection).run() +``` + +## `NlpExplainer` — the entry point + +`shapash/explainer/nlp_explainer.py` + +```python +NlpExplainer( + model, + label_names=None, + backend=None, + cf_generator=None, + explainer_args=None, + explainer_compute_args=None, +) +``` + +- `model` — a HF `pipeline`, a `TextModel` instance, or any callable. A HF pipeline is auto-wrapped as + `HFPipelineModel`; anything else that isn't a `TextModel` disables the model-layer-dependent features. +- `backend` — defaults to `NlpShapBackend` built from the model's `shap_callable`/`shap_masker`. +- `cf_generator` — explicit generator, or every built-in generator compatible with the model + (`HotFlipGenerator` preferred over `AblationFlipGenerator`) is auto-registered. + +**`fit(X_reference=None, y=None, cache_dir=None, precompute=True)`** — sklearn-style; optional. Builds a +similar-example retriever if `X_reference` is given and the model supports embeddings. + +**`explain(X, y=None, cache_dir=None) -> NlpExplanation`** — the memoization core. The cache key is +`hash(texts, model_id, backend_name+config)` — same texts *and* same model identity *and* same backend +class+config hits the cache; anything else busts it. `cache_dir` persists results to +`<cache_dir>/<hash>.xpl`. Every call returns a fresh `NlpExplanation` (shares read-only arrays with the +cache, never mutated in place). + +**`compute_embeddings(explanation, cache_dir=None, recompute=False)`** — embeds the batch with the +bound model (needs the model, the GPU and the compute lock) and returns an `Embedding`. The result is +model-free, so it can be saved and reduced later in a process that never loads the model. + +**`compute_projection(explanation, reducer=None, cache_dir=None, ...)`** — thin wrapper over +`compute_embeddings(...).project(reducer)`, returning a 2-D `Embedding`. Only the embeddings are +cached; projections are cheap to recompute, and a layout worth keeping is saved with +`Embedding.save` (most reducers are stochastic). + +**`run_app(explanation, port=8050, debug=False, host="127.0.0.1", projection=None, ...)`** — launches +`NlpWebApp(explanation, engine=self, projection=projection, ...)`. + +**Live/interactive methods** (implement the `InteractiveEngine` protocol — see below): +`predict`, `explain_text`, `find_similar` / `find_similar_threshold`, `generate_counterfactuals`, +`detect_label_noise`, plus `can_edit()` / `can_counterfactual()` / `can_find_similar()` / +`can_probe_labels()` capability flags the webapp reads to decide what to mount. + +## `NlpExplanation` — the frozen artifact + +`shapash/explainer/nlp_explanation.py` — `@dataclass(frozen=True, eq=False, slots=True)`. + +Holds only plain data: `texts`, `token_strings`, `values` (per-sample contributions), `base_values`, +`y_pred`/`y_prob`/`y_true`, `label_names`, `backend_name`, `is_additive`, `reference_kind`, +`output_space` (`"probability"` or `"logit"` — see below), `model_id`, `architecture`. **No model or +backend handle** — those are captured as plain strings/booleans at construction time, so the artifact +never needs the model to be reloaded. Arrays are set read-only (`setflags(write=False)`) on top of +`frozen=True`, so in-place mutation is blocked too, not just rebinding. + +- `save(path)` / `NlpExplanation.load(path)` — a zip of `meta.json` + parquet tables + (`contributions.parquet`, `base_values.parquet`, `samples.parquet`). No pickle, no format version + (prototype: an unreadable `explain` cache entry is simply recomputed). The projection is saved + separately as an `Embedding` (`shapash/compute/embeddings.py`). +- `corpus_id` — digest of the texts alone, so every artifact over one dataset agrees on it whatever + model, backend or reducer produced it. This is what pairs a projection back to an explanation, and + a property rather than a field so it cannot desync from `texts`. +- `.plot` — lazily builds an `NlpPlotter` (plotly/dash imports stay out of the persistence module). + Methods: `tokens`, `waterfall` (raises if `is_additive=False`), `sentence`, `word_importance`, + `word_profile`, `confusion`, `scatter`. + +## The `TextModel` layer + +`shapash/model/` — `base.py` defines the ABC + capability mixins consumed everywhere else: + +| Mixin | Gives you | Needed by | +|---|---|---| +| `TextModel` | `predict()`, `model_id`, `shap_callable`/`shap_masker` | everything | +| `SupportsTokenization` | `tokenize`/`detokenize`, case-folding | AblationFlip, tokenization-aware webapp panels | +| `SupportsEmbeddings` | `get_embedding_table()`, `embed()` | HotFlip, similar-example retrieval | +| `SupportsGradients` | `token_gradients()` | HotFlip | +| `SupportsCaptumIG` | embedding layer / encode / word alignment | `nlp_captum_lig` backend | + +Adapters (`hf.py`, `encoder.py`, `torch_models.py`): +- `HFPipelineModel` — thin, prediction-only wrapper over a `transformers.pipeline`. +- `HFClassifierModel.from_pretrained(name_or_path, *, tokenizer=None, label_names=None, device=None, + max_length="auto", trust_remote_code=False, load_kwargs=None, **model_kwargs)` — full-capability + adapter over `AutoModelForSequenceClassification`. `trust_remote_code` is forwarded to both the + tokenizer and model loaders (needed for checkpoints with custom code, e.g. some embedding-model + families); default `False`, and always passed explicitly so it never falls back to `transformers`' + interactive prompt, which would hang a webapp/batch job. +- `TorchClassifierModel` / `SentenceTransformerModel` (`torch_models.py`) — for encoder+head models + where the two are separate modules (hand-rolled PyTorch classifiers, sentence-transformers). + +## Model & framework support + +What the adapters above actually cover today, per architecture and per framework. + +**Architectures** (columns = capabilities from the mixin table above) + +| Family | Predict + SHAP/LIME | LIG + HotFlip | Embeddings | +|---|:-:|:-:|:-:| +| BERT, DistilBERT, RoBERTa, DeBERTa v1–v3 | ✅ | ✅ | ✅ | +| XLM-RoBERTa, CamemBERT v2, ALBERT | ✅ | ✅ | ✅ | +| PyTorch encoder + head | ✅ | ✅ | ✅ | +| sentence-transformers + head | ✅ | ✅ | ✅ | +| Encoder-decoder (T5, BART, mT5, FLAN-T5) | ✅ | ❌ | ❌ | +| Decoder-only / LLMs | ❌ | ❌ | ❌ | +| Long-context (Longformer, BigBird, ModernBERT) | ❌ | ❌ | ❌ | +| Cross-encoders / pairs, multi-label, regression heads | ❌ | ❌ | ❌ | + +**Frameworks** (which adapter, if any, applies) + +| Framework | Adapter | Status | What you get | +|---|---|:-:|---| +| PyTorch + `transformers` | `HFClassifierModel` | ✅ | Full capability surface — the reference path | +| `sentence-transformers` + torch head | `SentenceTransformerModel` | ✅ | Full | +| `transformers` pipeline | `HFPipelineModel` | ✅ | Predict-only: SHAP/LIME + ablation counterfactuals | +| Plain callable `f(texts) -> probs`, scikit-learn, ONNX/Optimum, TensorFlow/Keras/Flax/JAX, remote/API classifiers | — | ❌ | Blocked behind one seam (`NlpExplainer` leaves `_text_model=None` for a non-pipeline callable) | +| spaCy, fastText, gensim | — | ❌ | No route | + +## Backends: SHAP, LIME and Captum LIG + +Self-register by a `.name` class attribute, looked up via `get_backend_cls_from_name` — no manual +registry. + +- **`nlp_shap`** (`NlpShapBackend`, default) — wraps `shap.Explainer` over the softmax + text-classification pipeline. `output_space = "probability"`, `reference_kind = "none"`, + `is_additive = True`. Core dependency — no extra install needed. +- **`nlp_lime`** (`NlpLimeBackend`) — wraps `LimeTextExplainer`, fitting a locally-weighted linear + surrogate per sample. Word-level (bag-of-words by default), so `token_strings` is each sample's + unique vocabulary words rather than subword tokens. `output_space = "probability"`, + `reference_kind = "none"`, `is_additive = False` (a surrogate optimizes local fidelity, not exact + reconstruction, so there's no guarantee the weights sum to `f(x) - f(baseline)` — this disables + `.plot.waterfall`, which requires `is_additive = True`). Needs the `lime` extra, installed + separately from `nlp` (see [Installation](#installation)). +- **`nlp_captum_lig`** (`NlpCaptumLigBackend`) — Captum `LayerIntegratedGradients` through the embedding + layer, target-class **raw logit**. `output_space = "logit"`, `reference_kind = "point"` (a + tokenizer-built pad/mask baseline), requires `SupportsCaptumIG`. + +⚠️ **These are not directly comparable with each other.** SHAP and LIME both explain post-softmax +probability but via different mechanisms (exact game-theoretic attribution vs. a local linear +surrogate); LIG explains pre-softmax logit — a different space, where cross-class contributions don't +cancel the same way. This is tracked on the artifact (`output_space` field) but there is currently no +UI indicator or unit-conversion between spaces. + +## Counterfactual generators + +`shapash/compute/generators/` — `CounterfactualGenerator` ABC owns a shared +**minimal-perturbation search** (`search_minimal`): try perturbation sets of increasing size, keep the +smallest one that flips the prediction. + +- **`HotFlipGenerator`** — port of Google PAIR LIT's HotFlip. Ranks tokens by embedding-gradient norm, + shortlists candidate substitutions by a first-order linear estimate, then re-scores the shortlist + against the real model (the linear estimate alone isn't reliable) before handing choices to the + shared search. Needs `SupportsGradients + SupportsEmbeddings + SupportsTokenization`. +- **`AblationFlipGenerator`** — leave-one-out token removal instead of substitution; scores each token + by the probability drop when removed. Needs only `SupportsTokenization`, so it also works on + prediction-only models (e.g. a bare HF pipeline) that can't do HotFlip. + +## Diagnostics: label noise & label probe + +`shapash/compute/diagnostics/` + +- **`label_noise.py`** — in-house **confident learning** (Northcutt, Jiang & Chuang, JAIR 2021; the + algorithm behind `cleanlab`, reimplemented rather than adding a dependency). `detect_label_issues(...) + -> LabelNoiseReport`. Requires **out-of-sample** probabilities — measured on training data it + systematically under-reports. +- **`label_probe.py`** — `LabelProbe`: a small `TfidfVectorizer + LogisticRegression` fit on a labelled + reference corpus, deliberately uncorrelated with the audited model. Confident learning alone can't + tell "the model is wrong" from "the label is wrong" — both look like "the model confidently disagrees + with the label." The probe asks a second, independent classifier whether it backs the given label + (`verdicts(...)`), which tells the two apart. A nearest-neighbour-in-model-space alternative was + rejected during development for being circular (98.6% agreement with the audited model's own + predictions). + +## The webapp + +`NlpWebApp(explanation, engine=None, projection=None, ...)`, launched via `.run(...)` or (normally) +through `NlpExplainer.run_app(...)`. The constructor validates its inputs once (a projection over +different texts is refused, see `projection_coords`) and hands panels a private `AppContext` +(explanation, engine, coordinates). Layout: global Word Importance / Scatter panels → a full dataset +table → detail-on-demand local view (sentence highlight / waterfall) → an optional **What-if Lab** +(editable text, counterfactual generation, similar examples, label-noise ranking). + +Panels in `shapash/webapp/nlp_components/`: + +| Component | Shows | +|---|---| +| `word_importance.py` | corpus-level mean/total contribution per word, per class or aggregated | +| `scatter.py` | 2-D embedding projection, selectable, optional contribution-coloring | +| `word_profile.py` | one word's contribution across every class + ranked examples | +| `error_analysis.py` | confusion matrix + per-cell word importance, drives an error filter | +| `sentence_highlight.py` | inline per-token highlight for the current datapoint | +| `waterfall.py` | waterfall chart of the current datapoint's token contributions | +| `data_editor.py` | edit text, re-predict + re-explain live via the engine | +| `counterfactual.py` | generate what-if token flips, apply into the editor | +| `similar_examples.py` | reference-corpus neighbours (top-k or threshold) | +| `label_noise.py` | confident-learning ranking + the independent probe's verdict column | + +**Capability gating**: each `WebappComponent` declares a `requires` set of capability tokens +(`engine:predict`, `engine:counterfactual`, `model:gradients`, `engine:similar`, `data:labels`, +`data:ground_truth`, ...). `available_capabilities(explanation, engine)` computes what's actually +satisfied — data-derived capabilities survive a reloaded snapshot, `engine:*` ones require a live +`engine`. This is why the What-if Lab disappears entirely on a `NlpExplanation.load()`-only snapshot +(`engine=None`), and why e.g. `AblationFlipGenerator`-only setups don't advertise `model:gradients`. + +## Where to go next + +- Notebook walkthrough: `tutorial/nlp/overview.ipynb` +- Demo server: `demo/serve_nlp.py` (see `demo/nlp_cache/` for a pre-computed example run on + model `bhadresh-savani/distilbert-base-uncased-emotion` over dataset `dair-ai/emotion`) + +## Known limitations + +- **Classification only** (binary or multiclass, softmax output). `predict` softmaxes + unconditionally, so sigmoid (multi-label) or regression heads are silently mis-normalized rather than + refused — not just unsupported, but wrong if you point one at it. Sentence-pair / cross-encoder inputs + aren't supported either (one string per sample). See the *Cross-encoders / pairs, multi-label, + regression heads* row in [Model & framework support](#model--framework-support). +- `NlpExplainer` bypasses `SmartExplainer.compile()` entirely — there is no unified tabular+text entry + point yet. +- SHAP and Captum-LIG attributions are in different spaces (probability vs. logit) with no in-UI + indicator or conversion — see [Backends](#backends-shap-lime-and-captum-lig) above. +- T5-style encoder-decoder models and some sentence-transformer checkpoints don't fit the current + encoder adapter's assumptions (need `inputs_embeds` support); prediction + SHAP highlighting work, + gradient-based features (HotFlip, LIG) do not, for those architectures. \ No newline at end of file From ae8637c2e53486cce30bb9aabd14aedcded5f0e4 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 15:01:26 +0200 Subject: [PATCH 113/144] fix(webapp): serve the NLP app through Dash's own run() Replace the custom background-thread launcher with dash.Dash.run(): scripts block again (request logs, clean Ctrl+C), notebooks run in the background and get a RunningApp handle whose .kill() stops the server Dash registered. --- shapash/explainer/nlp_explainer.py | 7 +-- shapash/webapp/nlp_app.py | 26 +++----- shapash/webapp/utils/launch.py | 59 ++++++------------ tests/unit_tests/nlp/test_nlp_prototype.py | 57 +++++++++-------- tests/unit_tests/webapp/utils/test_launch.py | 65 ++++++++++++++++---- 5 files changed, 113 insertions(+), 101 deletions(-) diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index bca8d418..b1b37491 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -601,10 +601,9 @@ def run_app( Returns ------- RunningApp or None - When ``debug`` is ``False`` (the default), the app runs on a background thread and - this call returns immediately with a handle to it — call ``.kill()`` on it (or use it - as a context manager) to stop the app. Returns ``None`` when ``debug=True``, since - Dash's own dev server blocks in that mode instead — see :meth:`~shapash.webapp.nlp_app.NlpWebApp.run`. + In a notebook, the app is served in the background and this returns immediately with a + handle — call ``.kill()`` on it to stop the app. Anywhere else, this blocks until the + server is stopped (Ctrl+C) and returns ``None`` — see :meth:`~shapash.webapp.nlp_app.NlpWebApp.run`. """ # fit() is the one place that knows the reference corpus, so its size is added here rather # than living on `explanation` (which is model-free and never sees the training set at all). diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index b7399c9b..e2f3b209 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -9,14 +9,12 @@ from __future__ import annotations -import logging - import dash import dash_ag_grid as dag import dash_bootstrap_components as dbc import numpy as np import pandas as pd -from dash import MATCH, Input, Output, dcc, html +from dash import MATCH, Input, Output, dcc, html, jupyter_dash from dash.exceptions import PreventUpdate from shapash.compute.embeddings import Embedding, projection_coords @@ -40,7 +38,7 @@ pack_datapoint, ) from shapash.webapp.nlp_components.base import AppContext -from shapash.webapp.utils.launch import RunningApp, run_in_background +from shapash.webapp.utils.launch import RunningApp _APPLY_STORE = "whatif-apply-store" _CURRENT_STORE = "current-datapoint" @@ -918,24 +916,16 @@ def run(self, port: int = 8050, debug: bool = False, host: str = "127.0.0.1") -> port : int Port for the server. debug : bool - Enable Dash debug mode (hot reload, error overlay). Debug mode uses Dash's own - blocking dev server and reloader, so it cannot be handed back as a killable handle — - stop it by interrupting the kernel/process. Defaults to ``False``. + Enable Dash debug mode (hot reload, error overlay). Defaults to ``False``. host : str Host to bind the server to. Returns ------- RunningApp or None - When ``debug`` is ``False`` (the default), the server runs on a background thread and - this call returns immediately with a handle to it — call ``.kill()`` on it (or use it - as a context manager) to stop the server. Returns ``None`` when ``debug=True``, since - that path blocks instead. + In a notebook, Dash serves the app in the background and this returns immediately with + a handle — call ``.kill()`` on it (or use it as a context manager) to stop the server. + Anywhere else, this blocks until the server is stopped (Ctrl+C) and returns ``None``. """ - if debug: - self.app.run(port=port, debug=debug, host=host) - return None - running_app = run_in_background(self.app.server, host, port) - logging.info(f"Your Shapash application run on {running_app.url}") - logging.info("Use the method .kill() to stop your app.") - return running_app + self.app.run(port=port, debug=debug, host=host) + return RunningApp(host, port) if jupyter_dash.active else None diff --git a/shapash/webapp/utils/launch.py b/shapash/webapp/utils/launch.py index 0ed098fd..1fa8c055 100644 --- a/shapash/webapp/utils/launch.py +++ b/shapash/webapp/utils/launch.py @@ -1,34 +1,35 @@ -"""Serve a WSGI app in a background thread, stoppable from the caller. +"""Stop handle for a Dash app served in the background by Dash's own Jupyter integration. -Mirrors how Dash's own Jupyter integration launches apps (``dash._jupyter.JupyterDash.run_app``): -a daemon thread running ``server.serve_forever()``, stopped via ``server.shutdown()`` — the -cooperative shutdown built into ``socketserver.BaseServer``. No thread-killing trickery is needed -since ``serve_forever``/``shutdown`` are already a clean, designed-for-this pair. +``dash.Dash.run`` already picks the right serving strategy: in a script it blocks on Flask's server +(clean Ctrl+C, request logs); in Jupyter it serves from a background thread and returns at once. The +one thing it doesn't give a notebook user is a way to stop that background server, so this wraps the +server Dash registered for ``(host, port)``. Only ``jupyter_dash._servers`` is private here — Dash's +own port-reuse logic relies on it, and ``tests/unit_tests/webapp/utils/test_launch.py`` pins it. """ from __future__ import annotations -import threading - -from werkzeug.serving import BaseWSGIServer, make_server +from dash import jupyter_dash class RunningApp: - """A WSGI app running on a background thread; call ``.kill()`` to stop it.""" + """A Dash app Dash is serving in the background of a notebook; call ``.kill()`` to stop it.""" - def __init__(self, server: BaseWSGIServer, thread: threading.Thread, url: str): - self._server = server - self._thread = thread - self.url = url + def __init__(self, host: str, port: int): + self.url = f"http://{host}:{port}/" + self._key = (host, port) + self._server = jupyter_dash._servers[self._key] def kill(self) -> None: - """Stop the server and wait for its thread to exit.""" + """Stop the server; a no-op if it is already stopped.""" self._server.shutdown() - self._thread.join() + # Re-running an app on the same port replaces the registry entry — never evict a newer server. + if jupyter_dash._servers.get(self._key) is self._server: + del jupyter_dash._servers[self._key] def is_alive(self) -> bool: - """Whether the server thread is still running.""" - return self._thread.is_alive() + """Whether this server is still the one Dash is serving on its port.""" + return jupyter_dash._servers.get(self._key) is self._server def __enter__(self) -> RunningApp: return self @@ -38,27 +39,3 @@ def __exit__(self, *exc_info) -> None: def __repr__(self) -> str: return f"<RunningApp url={self.url!r} running={self.is_alive()}>" - - -def run_in_background(wsgi_app, host: str, port: int) -> RunningApp: - """Serve ``wsgi_app`` on a background daemon thread and return a killable handle. - - Parameters - ---------- - wsgi_app : WSGI application - The application to serve (e.g. a Flask app, or ``dash.Dash(...).server``). - host : str - Host to bind the server to. - port : int - Port to bind the server to. - - Returns - ------- - RunningApp - Already-started server handle; call ``.kill()`` on it (or use it as a context manager) - to stop the server. - """ - server = make_server(host, port, wsgi_app) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - return RunningApp(server, thread, url=f"http://{host}:{server.server_port}/") diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 1c1d2972..6fafd328 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -12,12 +12,12 @@ import unittest import zipfile from dataclasses import replace -from unittest.mock import patch +from unittest.mock import MagicMock, PropertyMock, patch import numpy as np import pandas as pd import plotly.graph_objs as go -from dash import html +from dash import html, jupyter_dash from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_lime_backend import NlpLimeBackend @@ -1188,37 +1188,44 @@ def _collect_ids(self, node) -> set: class TestNlpWebAppRun(unittest.TestCase): - """``NlpWebApp.run`` / ``NlpExplainer.run_app`` — killable background server.""" + """``NlpWebApp.run`` / ``NlpExplainer.run_app`` — delegate to ``dash.Dash.run``; a stop handle in notebooks.""" def setUp(self): self.xpl = _make_explainer() self.explanation = _make_explanation() - def test_run_returns_killable_app_by_default(self): - webapp = NlpWebApp(self.explanation, engine=self.xpl) - app = webapp.run(port=0) - try: - self.assertIsInstance(app, RunningApp) - self.assertTrue(app.is_alive()) - finally: - app.kill() - self.assertFalse(app.is_alive()) - - def test_run_app_returns_killable_app(self): - app = self.xpl.run_app(self.explanation, port=0) - try: - self.assertIsInstance(app, RunningApp) - self.assertTrue(app.is_alive()) - finally: - app.kill() - - def test_debug_mode_blocks_and_returns_none(self): + @staticmethod + def _jupyter_active(active): + return patch.object(type(jupyter_dash), "active", new_callable=PropertyMock, return_value=active) + + def test_outside_notebook_delegates_to_dash_and_returns_none(self): webapp = NlpWebApp(self.explanation, engine=self.xpl) - with patch.object(webapp.app, "run") as mock_run: - result = webapp.run(port=0, debug=True) - mock_run.assert_called_once_with(port=0, debug=True, host="127.0.0.1") + with self._jupyter_active(False), patch.object(webapp.app, "run") as mock_run: + result = webapp.run(port=8123, debug=True) + mock_run.assert_called_once_with(port=8123, debug=True, host="127.0.0.1") self.assertIsNone(result) + def test_in_notebook_returns_handle_on_dash_registered_server(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl) + server = MagicMock() + + def register(port, debug, host): + jupyter_dash._servers[(host, port)] = server + + with self._jupyter_active(True), patch.object(webapp.app, "run", side_effect=register): + app = webapp.run(port=8123) + self.assertIsInstance(app, RunningApp) + app.kill() + server.shutdown.assert_called_once_with() + self.assertNotIn(("127.0.0.1", 8123), jupyter_dash._servers) + + def test_run_app_forwards_the_handle(self): + sentinel = object() + with patch.object(NlpWebApp, "run", return_value=sentinel) as mock_run: + result = self.xpl.run_app(self.explanation, port=8123) + mock_run.assert_called_once_with(port=8123, debug=False, host="127.0.0.1") + self.assertIs(result, sentinel) + class TestComposeSelection(unittest.TestCase): """The three sample filters (scatter box, confusion cell, errors-only) must intersect.""" diff --git a/tests/unit_tests/webapp/utils/test_launch.py b/tests/unit_tests/webapp/utils/test_launch.py index 417e210e..fb10f13a 100644 --- a/tests/unit_tests/webapp/utils/test_launch.py +++ b/tests/unit_tests/webapp/utils/test_launch.py @@ -1,9 +1,14 @@ -"""Unit tests for ``run_in_background`` (serve a WSGI app on a killable background thread).""" +"""Unit tests for ``RunningApp`` — the stop handle over a server Dash's Jupyter integration registered.""" +import threading import unittest +import urllib.error import urllib.request -from shapash.webapp.utils.launch import RunningApp, run_in_background +from dash import jupyter_dash +from werkzeug.serving import make_server + +from shapash.webapp.utils.launch import RunningApp def _wsgi_app(environ, start_response): @@ -11,19 +16,53 @@ def _wsgi_app(environ, start_response): return [b"ok"] -class TestRunInBackground(unittest.TestCase): - def test_serves_and_returns_killable_handle(self): - app = run_in_background(_wsgi_app, "127.0.0.1", 0) - try: - self.assertIsInstance(app, RunningApp) - self.assertTrue(app.is_alive()) - with urllib.request.urlopen(app.url, timeout=5) as resp: - self.assertEqual(resp.read(), b"ok") - finally: - app.kill() +def _serve_like_jupyter_dash(host="127.0.0.1"): + """Start and register a server the way ``dash._jupyter.JupyterDash.run_app`` does.""" + server = make_server(host, 0, _wsgi_app, threaded=True) + threading.Thread(target=server.serve_forever, daemon=True).start() + port = server.server_port + jupyter_dash._servers[(host, port)] = server + return host, port, server + + +class TestRunningApp(unittest.TestCase): + def test_dash_still_exposes_server_registry(self): + # RunningApp's only dependency on Dash internals — fail loudly if an upgrade renames it. + self.assertIsInstance(jupyter_dash._servers, dict) + + def test_kill_stops_server_and_unregisters_it(self): + host, port, _ = _serve_like_jupyter_dash() + app = RunningApp(host, port) + with urllib.request.urlopen(app.url, timeout=5) as resp: + self.assertEqual(resp.read(), b"ok") + self.assertTrue(app.is_alive()) + + app.kill() + + self.assertFalse(app.is_alive()) + self.assertNotIn((host, port), jupyter_dash._servers) + with self.assertRaises(urllib.error.URLError): + urllib.request.urlopen(app.url, timeout=1) + + def test_kill_twice_is_a_no_op(self): + app = RunningApp(*_serve_like_jupyter_dash()[:2]) + app.kill() + app.kill() self.assertFalse(app.is_alive()) def test_context_manager_kills_on_exit(self): - with run_in_background(_wsgi_app, "127.0.0.1", 0) as app: + with RunningApp(*_serve_like_jupyter_dash()[:2]) as app: self.assertTrue(app.is_alive()) self.assertFalse(app.is_alive()) + + def test_kill_does_not_evict_a_newer_server_on_the_same_port(self): + host, port, _ = _serve_like_jupyter_dash() + old = RunningApp(host, port) + newer = make_server(host, 0, _wsgi_app) + jupyter_dash._servers[(host, port)] = newer # what re-running the app on that port does + try: + old.kill() + self.assertIs(jupyter_dash._servers[(host, port)], newer) + finally: + del jupyter_dash._servers[(host, port)] + newer.server_close() From 37c97aa3657b63468ca950fc479aae19e4fe4439 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 15:08:52 +0200 Subject: [PATCH 114/144] refactor(webapp): align nlp webapp styling with smart_app.py - Tab labels: text color matches smart_app.py, bold only while active - Bar charts (word importance, token highlight, word profile, waterfall): thin marker outline like smart_app.py's Local Explanation chart; fixed negative-bar color to preserve smart_app.py's original alpha (was rendering darker than intended) - Sentence highlight: token text switches to white on dark backgrounds (luminance-based), fixing unreadable black-on-dark-blue negative cells - Confusion matrix: switched from theme-coupled coloring back to Plotly's built-in "Blues" (a themed/custom scale either buried errors under the yellow palette or banded visibly) - Word Profile: contribution values colored by the app's positive/negative palette instead of Bootstrap red/blue; "Inspect" buttons across all word/counterfactual/label-noise/similar-examples panels now black/bold instead of link-blue or theme-accent - Embeddings scatter: word-contribution mode now diverges through the theme's positive/negative colors (white at zero) instead of a generic RdBu scale - "Model Errors" switch: on-fill themed via CSS var, label left default - Word Importance: clicking a bar fades every other bar to emphasize the selection; fixed a double-click bug where Plotly's default axis-reset behavior discarded the chart's padded range and rescaled the bars; double-click now clears the word-click filter via a clientside listener bound to Plotly's own event (fixing an initial wrong-DOM-node bug) --- shapash/plots/plot_confusion_matrix.py | 14 +++++- shapash/plots/plot_scatter.py | 21 ++++++++- shapash/plots/plot_sentence_highlight.py | 15 ++++--- shapash/plots/plot_token_highlight.py | 2 +- shapash/plots/plot_waterfall.py | 6 +-- shapash/plots/plot_word_importance.py | 22 +++++++++- shapash/plots/plot_word_profile.py | 2 +- shapash/style/colors.json | 8 ++-- shapash/webapp/assets/style.css | 7 +++ shapash/webapp/nlp_app.py | 18 ++++++-- .../webapp/nlp_components/counterfactual.py | 1 + shapash/webapp/nlp_components/label_noise.py | 1 + shapash/webapp/nlp_components/scatter.py | 6 ++- .../webapp/nlp_components/similar_examples.py | 1 + .../webapp/nlp_components/word_importance.py | 43 +++++++++++++++++++ shapash/webapp/nlp_components/word_profile.py | 8 ++-- .../unit_tests/webapp/test_nlp_whatif_app.py | 8 ++-- 17 files changed, 153 insertions(+), 30 deletions(-) diff --git a/shapash/plots/plot_confusion_matrix.py b/shapash/plots/plot_confusion_matrix.py index 815fdeac..8988acf4 100644 --- a/shapash/plots/plot_confusion_matrix.py +++ b/shapash/plots/plot_confusion_matrix.py @@ -5,7 +5,12 @@ import numpy as np from plotly import graph_objs as go -_COLORSCALE = "Blues" +# Plotly's own continuous "Blues" scale, independent of the app's active NLP palette: the +# discrete, hand-picked stops in colors.json's "blues"/"default" palettes band visibly on a +# heatmap, and under the yellow "default" palette low off-diagonal counts (the errors this +# matrix exists to surface) rendered as near-invisible pale yellow. Plotly's built-in scale reads +# cleanly regardless of which palette the rest of the app is themed with. +_DEFAULT_COLORSCALE = "Blues" def plot_confusion_matrix( @@ -15,6 +20,7 @@ def plot_confusion_matrix( title: str = "Confusion matrix", width: int | None = None, height: int | None = None, + colorscale: list[str] | str = _DEFAULT_COLORSCALE, ) -> go.Figure: """Heatmap of a confusion matrix with click-identifiable cells. @@ -40,6 +46,10 @@ def plot_confusion_matrix( Figure width in pixels. Defaults to a size scaled to the number of classes. height : int, optional Figure height in pixels. Defaults to a size scaled to the number of classes. + colorscale : list of str or str + Plotly heatmap colorscale. Defaults to Plotly's built-in ``"Blues"``, independent of the + app's active NLP palette — the errors this matrix exists to surface need to stay readable + whichever palette the rest of the app is themed with. Returns ------- @@ -75,7 +85,7 @@ def plot_confusion_matrix( customdata=customdata, text=text, texttemplate="%{text}", - colorscale=_COLORSCALE, + colorscale=colorscale, colorbar=dict(title=colorbar_title), hovertemplate=("true: %{y}<br>predicted: %{x}<br>" + hover_val + "<extra></extra>"), ) diff --git a/shapash/plots/plot_scatter.py b/shapash/plots/plot_scatter.py index 4030ac9c..6b6f918e 100644 --- a/shapash/plots/plot_scatter.py +++ b/shapash/plots/plot_scatter.py @@ -15,6 +15,8 @@ import numpy as np import plotly.graph_objs as go +from shapash.style.style_utils import DEFAULT_NLP_THEME + # Qualitative palette for the categorical mode — one color per class, cycling if there are more # classes than colors. Independent of the signed positive/negative theme in # ``shapash.style.style_utils`` (``DEFAULT_NLP_THEME.xpl_positive``/``xpl_negative``), which colors a @@ -78,6 +80,8 @@ def plot_scatter( error_mask: np.ndarray | None = None, palette: Sequence[str] = DEFAULT_CATEGORICAL_PALETTE, use_webgl: bool = True, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, ) -> go.Figure: """2-D scatter, colored either by class or by a word's SHAP contribution. @@ -116,6 +120,12 @@ def plot_scatter( the Dash webapp needs. Some notebook front-ends (e.g. VSCode's notebook renderer over certain remote/SSH or sandboxed setups) can't get a WebGL context and raise "WebGL is not supported by your browser" — pass ``False`` there to fall back to the plain SVG ``Scatter``. + color_positive, color_negative : str + Word-contribution mode: the two ends of the diverging colorscale (white at zero). Default + to the ``"default"`` palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in + ``shapash/style/colors.json`` — see :class:`~shapash.webapp.nlp_app.NlpWebApp`'s + ``palette_name``/``colors_dict`` to theme every NLP chart at once instead of overriding + this one call. Returns ------- @@ -134,7 +144,9 @@ def plot_scatter( scatter_cls = go.Scattergl if use_webgl else go.Scatter if contributions is not None: - return _plot_word_contribution(xy, texts_short, contributions, colorbar_title, error_mask, scatter_cls) + return _plot_word_contribution( + xy, texts_short, contributions, colorbar_title, error_mask, scatter_cls, color_positive, color_negative + ) if labels is None or label_names is None: raise ValueError("`label_names` is required alongside `labels`.") return _plot_categorical(xy, texts_short, list(labels), list(label_names), error_mask, palette, scatter_cls) @@ -147,8 +159,13 @@ def _plot_word_contribution( colorbar_title: str | None, error_mask: np.ndarray | None, scatter_cls: type[go.Scatter] | type[go.Scattergl], + color_positive: str, + color_negative: str, ) -> go.Figure: max_abs = float(np.abs(contributions).max()) or 1.0 + # Diverging colorscale white-out-from-zero, matching every other signed-contribution chart in + # the app (Word Importance, Sentence Highlight, Waterfall) instead of a generic red/blue. + diverging_colorscale = [[0.0, color_negative], [0.5, "rgb(255, 255, 255)"], [1.0, color_positive]] present_mask = np.where(contributions != 0.0)[0] absent_mask = np.where(contributions == 0.0)[0] @@ -178,7 +195,7 @@ def _plot_word_contribution( mode="markers", marker=dict( color=contributions[present_mask].tolist(), - colorscale="RdBu", + colorscale=diverging_colorscale, cmin=-max_abs, cmax=max_abs, size=present_size, diff --git a/shapash/plots/plot_sentence_highlight.py b/shapash/plots/plot_sentence_highlight.py index 91f1f2ff..b8990a44 100644 --- a/shapash/plots/plot_sentence_highlight.py +++ b/shapash/plots/plot_sentence_highlight.py @@ -25,16 +25,21 @@ def _parse_rgb(color: str) -> tuple[int, int, int]: return r, g, b -def _shap_color(val: float, max_abs: float, pos_rgb: tuple[int, int, int], neg_rgb: tuple[int, int, int]) -> str: - """Interpolate from white toward the sign-appropriate colour.""" +def _shap_style( + val: float, max_abs: float, pos_rgb: tuple[int, int, int], neg_rgb: tuple[int, int, int] +) -> dict[str, str]: + """Background interpolated from white toward the sign-appropriate colour, with a text colour + picked for contrast against it (the negative blue gets dark enough, at high magnitude, that + black text stops being readable — see the module docstring).""" if max_abs == 0 or val == 0: - return "transparent" + return {"backgroundColor": "transparent"} t = min(abs(val) / max_abs, 1.0) r_base, g_base, b_base = pos_rgb if val >= 0 else neg_rgb r = int(255 + t * (r_base - 255)) g = int(255 + t * (g_base - 255)) b = int(255 + t * (b_base - 255)) - return f"rgb({r},{g},{b})" + luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 + return {"backgroundColor": f"rgb({r},{g},{b})", "color": "#111111" if luminance > 0.6 else "#ffffff"} def plot_sentence_highlight( @@ -111,7 +116,7 @@ def plot_sentence_highlight( spans.append( html.Span( tok + " ", - style={**_span_base, "backgroundColor": _shap_color(float(val), max_abs, pos_rgb, neg_rgb)}, + style={**_span_base, **_shap_style(float(val), max_abs, pos_rgb, neg_rgb)}, title=tooltip, ) ) diff --git a/shapash/plots/plot_token_highlight.py b/shapash/plots/plot_token_highlight.py index 44bf89eb..8b93f397 100644 --- a/shapash/plots/plot_token_highlight.py +++ b/shapash/plots/plot_token_highlight.py @@ -72,7 +72,7 @@ def plot_token_highlight( x=values[::-1], y=tokens[::-1], orientation="h", - marker_color=colors[::-1], + marker=dict(color=colors[::-1], line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5)), hovertemplate="%{y}: %{x:.4f}<extra></extra>", ) ) diff --git a/shapash/plots/plot_waterfall.py b/shapash/plots/plot_waterfall.py index b9586367..bb7236e7 100644 --- a/shapash/plots/plot_waterfall.py +++ b/shapash/plots/plot_waterfall.py @@ -147,9 +147,9 @@ def plot_waterfall( measure=measures, text=texts, textposition="outside", - increasing=dict(marker=dict(color=color_positive)), - decreasing=dict(marker=dict(color=color_negative)), - totals=dict(marker=dict(color="#7f7f7f")), + increasing=dict(marker=dict(color=color_positive, line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5))), + decreasing=dict(marker=dict(color=color_negative, line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5))), + totals=dict(marker=dict(color="#7f7f7f", line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5))), connector=dict(line=dict(color="#dddddd", width=1, dash="dot")), cliponaxis=False, ) diff --git a/shapash/plots/plot_word_importance.py b/shapash/plots/plot_word_importance.py index d146d1e7..749148fc 100644 --- a/shapash/plots/plot_word_importance.py +++ b/shapash/plots/plot_word_importance.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Mapping, Sequence import pandas as pd from plotly import graph_objs as go @@ -84,6 +84,7 @@ def plot_word_importance( counts: pd.Series | Mapping[str, int] | None = None, color_positive: str = DEFAULT_NLP_THEME.xpl_positive, color_negative: str = DEFAULT_NLP_THEME.xpl_negative, + highlighted_words: Sequence[str] | None = None, ) -> go.Figure: """Horizontal bar chart of aggregated token-level SHAP contributions per word. @@ -128,6 +129,10 @@ def plot_word_importance( palette's ``nlp_xpl_positive``/``nlp_xpl_negative`` in ``shapash/style/colors.json`` — see :class:`~shapash.webapp.nlp_app.NlpWebApp`'s ``palette_name``/``colors_dict`` to theme every NLP chart at once instead of overriding this one call. + highlighted_words : Sequence[str], optional + Words to keep at full opacity, fading every other bar — emphasizes a selection (e.g. the + word a bar click just filtered on) without changing any bar's color. ``None`` or empty + leaves every bar at full opacity. Returns ------- @@ -144,6 +149,8 @@ def plot_word_importance( colors = [color_positive if v >= 0 else color_negative for v in values] labels = [f"{v:+.3f}" for v in values] if show_values else None occurrences = _aligned_counts(words, counts) + highlight_set = set(highlighted_words) if highlighted_words else None + opacity = [1.0 if w in highlight_set else 0.3 for w in words] if highlight_set else 1.0 # Name the statistic in the hover too: the axis title is the only thing distinguishing a mean # from a total, and it is off-screen once the chart is long enough to scroll. @@ -158,7 +165,11 @@ def plot_word_importance( x=values[::-1], y=words[::-1], orientation="h", - marker_color=colors[::-1], + marker=dict( + color=colors[::-1], + opacity=opacity[::-1] if isinstance(opacity, list) else opacity, + line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5), + ), text=labels[::-1] if labels is not None else None, textposition="outside" if show_values else "none", textfont=dict(size=10, color="#666666"), @@ -183,6 +194,12 @@ def plot_word_importance( zerolinewidth=1, gridcolor="#eeeeee", range=_padded_range(values) if show_values else None, + # Plotly's default double-click ("reset+autosize") recomputes autorange from the data + # whenever the axis is still at its as-drawn range — which discards the value-label + # padding above and re-stretches every bar to the fresh, unpadded extent. There is no + # zoom/pan use for a chart whose range is already fully computed, so disable both axes' + # interactivity outright rather than fight the reset behavior. + fixedrange=True, ), yaxis=dict( automargin=True, @@ -191,6 +208,7 @@ def plot_word_importance( # — and it does it silently. Forcing dtick makes a cramped chart look cramped instead. dtick=1, tickfont=dict(size=12, color="#222222"), + fixedrange=True, ), # Slightly thicker bars than plotly's default, so a word label lines up against a bar # rather than against whitespace. diff --git a/shapash/plots/plot_word_profile.py b/shapash/plots/plot_word_profile.py index 1958d92d..b7e3be1d 100644 --- a/shapash/plots/plot_word_profile.py +++ b/shapash/plots/plot_word_profile.py @@ -79,7 +79,7 @@ def plot_word_profile( x=values[::-1], y=names[::-1], orientation="h", - marker_color=colors[::-1], + marker=dict(color=colors[::-1], line=dict(color="rgba(50, 50, 50, 0.6)", width=0.5)), # The class index travels on the bar so a click handler resolves it directly. Matching # the y-axis label back to a class would be ambiguous the moment two classes share a # display name, and reversed here alongside every other per-bar array. diff --git a/shapash/style/colors.json b/shapash/style/colors.json index 9b80ca07..cfb4e230 100644 --- a/shapash/style/colors.json +++ b/shapash/style/colors.json @@ -85,8 +85,8 @@ "0": "rgba(52, 55, 54, 0.8)", "1": "rgba(52, 55, 54, 0.8)" }, - "nlp_xpl_negative": "rgb(214, 39, 40)", - "nlp_xpl_positive": "rgb(31, 119, 180)", + "nlp_xpl_negative": "rgba(235, 149, 72, 0.9)", + "nlp_xpl_positive": "rgba(0, 154, 203, 1)", "prediction_plot": { "0": "rgba(0, 70, 92, 0.9)", "1": "rgba(168, 84, 0, 0.9)" @@ -209,8 +209,8 @@ "0": "rgba(52, 55, 54, 0.8)", "1": "rgba(52, 55, 54, 0.8)" }, - "nlp_xpl_negative": "rgb(214, 39, 40)", - "nlp_xpl_positive": "rgb(31, 119, 180)", + "nlp_xpl_negative": "rgba(74, 99, 138, 0.7)", + "nlp_xpl_positive": "rgba(244, 192, 0, 1.0)", "prediction_plot": { "0": "rgba(255, 77, 7, 0.9)", "1": "rgba(52, 55, 54, 0.9)" diff --git a/shapash/webapp/assets/style.css b/shapash/webapp/assets/style.css index b13a545f..eb7fa585 100644 --- a/shapash/webapp/assets/style.css +++ b/shapash/webapp/assets/style.css @@ -214,6 +214,13 @@ h4 { margin-bottom: 0; } +/* NLP app: "Model Errors" switch fill, themed via the --errors-switch-color custom property + set inline on the switch (see nlp_app.py._build_selection_bar). */ +#errors-only-switch:checked { + background-color: var(--errors-switch-color, #f4c000); + border-color: var(--errors-switch-color, #f4c000); +} + .Select-placeholder, .Select__placeholder { line-height: 1.4vw !important; font-size: calc(0.3rem + 0.5vw) !important; diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index e2f3b209..d6a9a666 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -457,7 +457,10 @@ def _build_selection_bar(self) -> html.Div: label="Model Errors", value=False, className="small mb-0 ms-auto", - style={} if self._has_gt else _HIDDEN, + # The "on" fill (--errors-switch-color, read by style.css) takes the theme's accent + # color — the same color smart_app.py's own switches use. The label keeps its + # default text color; only the switch itself is themed. + style={"--errors-switch-color": self._theme.header_accent} if self._has_gt else _HIDDEN, ) ) return html.Div( @@ -504,7 +507,7 @@ def _build_components(self) -> list[WebappComponent]: # rather than listed above only because its `offer_word_contribution` argument depends on Word Importance being mounted — # which it always is at this point, since that panel has no `requires`. if ScatterComponent.is_available(self._ctx): - components.append(ScatterComponent(offer_word_contribution=True)) + components.append(ScatterComponent(offer_word_contribution=True, theme=self._theme)) self._components = components return components @@ -652,7 +655,16 @@ def _tabbed_card(self, tabs_id: str, tabs: list) -> html.Div: """ active = tabs[0][0] self._tab_groups[tabs_id] = [tid for tid, _, _ in tabs] - headers = [dbc.Tab(label=label, tab_id=tid) for tid, label, _ in tabs] + # Tab label text color matches smart_app.py's tabs; bold only while selected. + headers = [ + dbc.Tab( + label=label, + tab_id=tid, + label_style={"color": "black"}, + active_label_style={"color": "black", "fontWeight": "bold"}, + ) + for tid, label, _ in tabs + ] bodies = [ html.Div(body, id=f"{tabs_id}-body-{tid}", style=(_VISIBLE if tid == active else _HIDDEN)) for tid, _, body in tabs diff --git a/shapash/webapp/nlp_components/counterfactual.py b/shapash/webapp/nlp_components/counterfactual.py index c6c541b6..ba683e5d 100644 --- a/shapash/webapp/nlp_components/counterfactual.py +++ b/shapash/webapp/nlp_components/counterfactual.py @@ -233,6 +233,7 @@ def _results_table(cfs, component_id: str): color="link", size="sm", className="p-0", + style={"color": "#111111", "fontWeight": "600"}, title="Load into the editor and show its token contributions on the right", ) ), diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py index 35f6f7ef..b625461a 100644 --- a/shapash/webapp/nlp_components/label_noise.py +++ b/shapash/webapp/nlp_components/label_noise.py @@ -255,6 +255,7 @@ def _issues_table(issues, component_id): color="link", size="sm", className="p-0", + style={"color": "#111111", "fontWeight": "600"}, title="Show this sample's token contributions on the right", ) ) diff --git a/shapash/webapp/nlp_components/scatter.py b/shapash/webapp/nlp_components/scatter.py index c4340f44..a41a3d9e 100644 --- a/shapash/webapp/nlp_components/scatter.py +++ b/shapash/webapp/nlp_components/scatter.py @@ -27,6 +27,7 @@ from shapash.explainer.nlp_explanation import word_contributions_by_sample from shapash.plots.plot_scatter import plot_scatter +from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme from shapash.webapp.nlp_components.base import CAP_PROJECTION, WebappComponent, error_mask @@ -38,8 +39,9 @@ class ScatterComponent(WebappComponent): scope = "global" requires = frozenset({CAP_PROJECTION}) - def __init__(self, offer_word_contribution: bool) -> None: + def __init__(self, offer_word_contribution: bool, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._offer_word_contribution = offer_word_contribution + self._theme = theme def layout(self, ctx) -> html.Div: """Build the color-by/word-select controls and the scatter graph itself.""" @@ -233,6 +235,8 @@ def _build_scatter_fig( contributions=contributions, colorbar_title=colorbar_title, error_mask=err_mask, + color_positive=self._theme.xpl_positive, + color_negative=self._theme.xpl_negative, ) if color_by == "ground_truth" and explanation.y_true is not None: diff --git a/shapash/webapp/nlp_components/similar_examples.py b/shapash/webapp/nlp_components/similar_examples.py index 3bc4f3dd..bc5dea2e 100644 --- a/shapash/webapp/nlp_components/similar_examples.py +++ b/shapash/webapp/nlp_components/similar_examples.py @@ -250,6 +250,7 @@ def _neighbors_table(neighbors, predicted_label, component_id): color="link", size="sm", className="p-0", + style={"color": "#111111", "fontWeight": "600"}, title="Explain this example here and show its token contributions on the right", ) ) diff --git a/shapash/webapp/nlp_components/word_importance.py b/shapash/webapp/nlp_components/word_importance.py index 40f8807e..6d282562 100644 --- a/shapash/webapp/nlp_components/word_importance.py +++ b/shapash/webapp/nlp_components/word_importance.py @@ -237,6 +237,9 @@ def layout(self, ctx) -> html.Div: ), style={"flex": "1 1 auto", "minHeight": "0", "overflowY": "auto"}, ), + # Dummy Output for the clientside double-click listener below — nothing reads its + # data, it exists only because a clientside_callback needs some Output to target. + dcc.Store(id="wi-dblclick-bind"), ], style={"height": "100%", "display": "flex", "flexDirection": "column"}, ) @@ -251,6 +254,43 @@ def register_callbacks(self, app, ctx, stores) -> None: word_click_clear_btn = stores["word_click_clear"] active_class_store = stores["active_class"] + # Double-clicking the chart (a bar or blank space) clears the word-click filter, mirroring + # the clear button. Plotly's own click handling already swallows the *single*-click event + # that would otherwise precede a double-click, so this never races the bar-click handler + # below. Done via a raw `plotly_doubleclick` listener + `set_props` rather than the + # `relayoutData` prop: with both axes `fixedrange` (see plot_word_importance), Plotly's + # relayout on double-click carries no changed attributes, and Dash dedupes an unchanged + # prop value — a second double-click would otherwise silently stop firing anything. + app.clientside_callback( + f""" + function() {{ + function bind() {{ + // getElementById returns Dash's own wrapper div; Plotly attaches its event + // emitter (the `.on(...)` method used below) to the inner `.js-plotly-plot` + // node instead, the same one the panel-expand fullscreen callback resizes — + // and only once its own (async) first render has actually completed, which + // this callback's own trigger does not wait for, hence the retry below. + var wrapper = document.getElementById('global-importance-graph'); + var gd = wrapper ? wrapper.querySelector('.js-plotly-plot') : null; + if (!gd) {{ + setTimeout(bind, 100); + return; + }} + if (!gd._wiDblclickBound) {{ + gd._wiDblclickBound = true; + gd.on('plotly_doubleclick', function() {{ + window.dash_clientside.set_props('{word_click_store}', {{data: null}}); + }}); + }} + }} + bind(); + return window.dash_clientside.no_update; + }} + """, + Output("wi-dblclick-bind", "data"), + Input("global-importance-graph", "id"), + ) + @app.callback( Output("global-importance-graph", "figure"), [ @@ -263,6 +303,7 @@ def register_callbacks(self, app, ctx, stores) -> None: Input(selection_store, "data"), Input(error_cell_store, "data"), Input(errors_only_switch, "value"), + Input(word_click_store, "data"), ], ) def update_global_importance( @@ -275,6 +316,7 @@ def update_global_importance( selected_indices, error_cell, errors_only, + highlighted_words, ): if label_idx is None: raise PreventUpdate @@ -339,6 +381,7 @@ def update_global_importance( counts=counts["n_occurrences"], color_positive=self._theme.xpl_positive, color_negative=self._theme.xpl_negative, + highlighted_words=highlighted_words, ) # Height deliberately left as plot_word_importance computed it — see the graph's # wrapper in the layout. diff --git a/shapash/webapp/nlp_components/word_profile.py b/shapash/webapp/nlp_components/word_profile.py index 2ef3d557..99e14cdb 100644 --- a/shapash/webapp/nlp_components/word_profile.py +++ b/shapash/webapp/nlp_components/word_profile.py @@ -75,7 +75,7 @@ def _truncate(text: str) -> str: return text if len(text) <= _TEXT_PREVIEW_CHARS else text[: _TEXT_PREVIEW_CHARS - 1] + "…" -def _samples_table(ranked, explanation, component_id: str, show_truth: bool): +def _samples_table(ranked, explanation, component_id: str, show_truth: bool, theme: NlpTheme): """Ranked samples with their contribution, occurrence count, prediction and an Inspect button.""" head_cells = [html.Th("Contribution"), html.Th("×", title="Occurrences of the word in this sample")] head_cells.append(html.Th("Predicted")) @@ -91,7 +91,8 @@ def _samples_table(ranked, explanation, component_id: str, show_truth: bool): cells = [ html.Td( f"{value:+.4f}", - className="fw-bold " + ("text-primary" if value >= 0 else "text-danger"), + className="fw-bold", + style={"color": theme.xpl_positive if value >= 0 else theme.xpl_negative}, ), html.Td(str(int(record.n_occurrences)), className="text-muted"), html.Td(str(y_pred.iloc[pos]) if y_pred is not None else "—", style={"fontSize": "0.85em"}), @@ -107,6 +108,7 @@ def _samples_table(ranked, explanation, component_id: str, show_truth: bool): color="link", size="sm", className="p-0", + style={"color": "#111111", "fontWeight": "600"}, title="Show this sample's token contributions on the right", ) ) @@ -362,7 +364,7 @@ def update_profile(word, agg, class_idx, order, limit, selected_indices, error_c f"Contribution to {label_names[int(class_idx or 0)]}, summed within each sample.", className="text-muted d-block mb-1", ), - _samples_table(ranked, explanation, self.id, show_truth), + _samples_table(ranked, explanation, self.id, show_truth, self._theme), ] ) return fig, caption, table, [int(s) for s in ranked["sample"]] diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index a516616c..e7e58cc8 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -1198,8 +1198,10 @@ def _words(fig): def setUp(self): self.graph = self._app() - def _call(self, rank_by="mean", floor=1, sign="all", topk=10, indices=None, cell=None, errors=False, label=0): - return self.graph(label, topk, sign, [], rank_by, floor, indices, cell, errors) + def _call( + self, rank_by="mean", floor=1, sign="all", topk=10, indices=None, cell=None, errors=False, label=0, words=None + ): + return self.graph(label, topk, sign, [], rank_by, floor, indices, cell, errors, words) @staticmethod def _values(fig): @@ -1363,7 +1365,7 @@ def test_missing_class_prevents_update(self): from dash.exceptions import PreventUpdate with self.assertRaises(PreventUpdate): - self.graph(None, 10, "all", [], "mean", 1, None, None, False) + self.graph(None, 10, "all", [], "mean", 1, None, None, False, None) class TestWordImportanceScatterSync(unittest.TestCase): From 5bcf282d82ff0ffd41f675449a448db00c774e7c Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 16:28:05 +0200 Subject: [PATCH 115/144] fix(tests): update NLP integration tests to fit/explain API, pin string storage for inverse_transform - test_captum_backends.py, test_nlp_whatif.py, test_torch_st_models.py: replace the removed NlpExplainer.compile() with fit()/explain(), thread the returned NlpExplanation through can_detect_label_noise()/detect_label_noise()/NlpWebApp, and fix two assertions (NlpContributions has no __len__ or label_names) that the old API had been masking. - test_integration_inverse_tranform.py: pin pandas string_storage to "python" for the test, since pyarrow being importable (now pulled in transitively by the NLP extras) flips pandas' "auto" string storage default and breaks exact dtype comparison against the pyarrow-free titanic fixture, though the values themselves were always correct. --- .../integration_tests/test_captum_backends.py | 10 +++---- .../test_integration_inverse_tranform.py | 9 ++++++ tests/integration_tests/test_nlp_whatif.py | 30 +++++++++---------- .../integration_tests/test_torch_st_models.py | 8 ++--- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py index 7950914b..6af57583 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_captum_backends.py @@ -18,7 +18,6 @@ pytest.importorskip("captum") from shapash.backend import NlpCaptumLigBackend # noqa: E402 -from shapash.backend.nlp_backend import NlpContributions # noqa: E402 from shapash.compute.generators import AblationFlipGenerator # noqa: E402 from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 from shapash.model import HFClassifierModel # noqa: E402 @@ -72,13 +71,12 @@ def test_lig_backend_contributions(model): def test_lig_backend_through_explainer_and_word_importance(model): - """The LIG backend drives NlpExplainer.compile and the shared word-importance aggregation.""" + """The LIG backend drives NlpExplainer.explain and the shared word-importance aggregation.""" xpl = NlpExplainer(model, label_names=LABELS, backend=NlpCaptumLigBackend(model, label_names=LABELS)) - xpl.compile(["i am so happy today", "i feel terrified and alone"]) - assert isinstance(xpl.contributions, NlpContributions) - assert len(xpl.contributions) == 2 + explanation = xpl.explain(["i am so happy today", "i feel terrified and alone"]) + assert len(explanation) == 2 joy_idx = LABELS.index("joy") - word_imp = xpl.contributions.word_importance(joy_idx, n_top=5) + word_imp = explanation.word_importance(joy_idx, n_top=5) assert len(word_imp) > 0 # some words survive special-token filtering diff --git a/tests/integration_tests/test_integration_inverse_tranform.py b/tests/integration_tests/test_integration_inverse_tranform.py index 1eef8d5d..184ad6cd 100644 --- a/tests/integration_tests/test_integration_inverse_tranform.py +++ b/tests/integration_tests/test_integration_inverse_tranform.py @@ -28,6 +28,15 @@ def setUp(self): unittest : [type] [description] """ + # The "python"-storage titanic fixture below was intentionally built pyarrow-free (see git + # history on the .pkl). With pyarrow installed, pandas' default "auto" string storage infers + # pyarrow-backed strings for any newly-built column (e.g. category_encoders' inverse-transform + # output), which then fails assert_frame_equal's exact-dtype check against the fixture even + # though the values match. Pin "python" storage for the duration of this test only. + previous_storage = pd.options.mode.string_storage + pd.options.mode.string_storage = "python" + self.addCleanup(lambda: setattr(pd.options.mode, "string_storage", previous_storage)) + data_path = dirname(dirname(abspath(__file__))) if int(pd.__version__.split(".")[0]) >= 3: self.ds_titanic_clean = pd.read_pickle(join(data_path, "data", "clean_titanic_pandas_3.pkl")) diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py index 2d54e7c5..91bde1bf 100644 --- a/tests/integration_tests/test_nlp_whatif.py +++ b/tests/integration_tests/test_nlp_whatif.py @@ -121,11 +121,12 @@ def test_explainer_interactive_engine_with_classifier(hf): assert label in LABELS assert abs(sum(probs.values()) - 1.0) < 1e-4 - xpl.compile(["i am so happy today", "i feel terrified and alone"], y_true=["joy", "fear"]) + explanation = xpl.explain(["i am so happy today", "i feel terrified and alone"], y=["joy", "fear"]) + assert list(explanation.y_true) == ["joy", "fear"] contribs, elabel, eprobs = xpl.explain_text("i am furious about this") - assert len(contribs) == 1 + assert len(contribs.token_strings) == 1 assert elabel in LABELS - assert contribs.label_names == LABELS + assert set(eprobs.keys()) == set(LABELS) cfs = xpl.generate_counterfactuals("i am so happy today", config={"num_examples": 2, "max_flips": 2}) assert all(cf.new_label != cf.orig_label for cf in cfs) @@ -174,8 +175,8 @@ def test_whatif_app_callbacks_end_to_end(hf): classifier, tokenizer, _ = hf model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) xpl = NlpExplainer(model, label_names=LABELS) - xpl.compile(["i am so happy today", "i feel terrified and alone"], y_true=["joy", "fear"]) - webapp = NlpWebApp(xpl) + explanation = xpl.explain(["i am so happy today", "i feel terrified and alone"], y=["joy", "fear"]) + webapp = NlpWebApp(explanation, engine=xpl) app = webapp.app # Predict: edited text -> probability figure + current-datapoint (drives the shared highlight). @@ -249,13 +250,12 @@ def test_label_noise_detection_with_real_probabilities(hf): noisy = list(truth) noisy[0], noisy[2] = "anger", "joy" - reference = (texts, truth) - xpl = NlpExplainer(model, label_names=LABELS, reference_corpus=reference) - xpl.compile(texts, y_true=noisy) + xpl = NlpExplainer(model, label_names=LABELS).fit(X_reference=texts, y=truth) + explanation = xpl.explain(texts, y=noisy) - assert xpl.can_detect_label_noise() + assert xpl.can_detect_label_noise(explanation) assert xpl.can_probe_labels() - report = xpl.detect_label_noise(top_n=5) + report = xpl.detect_label_noise(explanation, top_n=5) assert report.n_samples == len(texts) assert report.label_names == LABELS @@ -286,15 +286,15 @@ def test_label_noise_detection_with_real_probabilities(hf): # Without a reference corpus there is no second opinion, and detection still runs. bare = NlpExplainer(model, label_names=LABELS) - bare.compile(texts, y_true=noisy) + bare_explanation = bare.explain(texts, y=noisy) assert not bare.can_probe_labels() - assert all(issue.probe is None for issue in bare.detect_label_noise(top_n=5).issues) + assert all(issue.probe is None for issue in bare.detect_label_noise(bare_explanation, top_n=5).issues) def test_label_noise_unavailable_without_ground_truth(hf): classifier, tokenizer, _ = hf xpl = NlpExplainer(HFClassifierModel(classifier, tokenizer, label_names=LABELS), label_names=LABELS) - xpl.compile(["i am so happy today", "i feel terrified and alone"]) - assert not xpl.can_detect_label_noise() + explanation = xpl.explain(["i am so happy today", "i feel terrified and alone"]) + assert not xpl.can_detect_label_noise(explanation) with pytest.raises(RuntimeError, match="ground-truth labels"): - xpl.detect_label_noise() + xpl.detect_label_noise(explanation) diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py index 6b401a82..5061c993 100644 --- a/tests/integration_tests/test_torch_st_models.py +++ b/tests/integration_tests/test_torch_st_models.py @@ -130,8 +130,8 @@ def test_st_token_gradients_align(st_model): def test_st_shap_backend_runs(st_model): """SHAP works via the explicit Text masker (no transformers pipeline needed).""" xpl = NlpExplainer(st_model, label_names=LABELS) - xpl.compile(TEXTS[:2]) - words = xpl.contributions.token_strings[0] + explanation = xpl.explain(TEXTS[:2]) + words = explanation.token_strings[0] assert len(words) > 0 # word-level highlights must not leak special tokens or subword markers. assert not any(w.startswith(("##", "Ġ", "▁")) or w in ("[CLS]", "[SEP]") for w in words) @@ -203,8 +203,8 @@ def test_torch_word_alignment_with_fast_tokenizer(torch_model): def test_torch_shap_and_lig_backends_run(torch_model): xpl = NlpExplainer(torch_model, label_names=LABELS) - xpl.compile(TEXTS[:2]) - assert len(xpl.contributions.token_strings[0]) > 0 + explanation = xpl.explain(TEXTS[:2]) + assert len(explanation.token_strings[0]) > 0 backend = NlpCaptumLigBackend(torch_model, label_names=LABELS) raw = backend.run_explainer(TEXTS[:2]) From 09ecc16cb0f1529c0573928f3edaf75208d55be2 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 16:59:52 +0200 Subject: [PATCH 116/144] test: add test coverage for nlp webapp components --- tests/unit_tests/nlp/test_nlp_prototype.py | 135 +++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 471 ++++++++++++++++++ 2 files changed, 606 insertions(+) diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py index 6fafd328..9ef58827 100644 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ b/tests/unit_tests/nlp/test_nlp_prototype.py @@ -18,6 +18,7 @@ import pandas as pd import plotly.graph_objs as go from dash import html, jupyter_dash +from dash.exceptions import PreventUpdate from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_lime_backend import NlpLimeBackend @@ -46,6 +47,8 @@ from shapash.webapp.nlp_app import NlpWebApp from shapash.webapp.nlp_components import compose_selection as _compose_selection from shapash.webapp.nlp_components import error_positions +from shapash.webapp.nlp_components.datapoint import pack_datapoint, unpack_datapoint +from shapash.webapp.nlp_components import error_analysis as error_analysis_module from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click from shapash.webapp.utils.launch import RunningApp @@ -1153,6 +1156,93 @@ def test_cell_from_click_none_on_empty_or_unknown(self): self.assertIsNone(_cell_from_click({"points": []}, name_to_idx)) self.assertIsNone(_cell_from_click({"points": [{"x": "??", "y": "??"}]}, name_to_idx)) + @staticmethod + def _callback(webapp, out_substr): + # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. + for key, spec in webapp.app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_update_confusion_matrix_normalizes_on_recall(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "confusion-matrix-graph.figure") + fig = update("recall") + # The container drives sizing for this half-column panel, not the figure itself. + self.assertIsNone(fig.layout.width) + self.assertIsNone(fig.layout.height) + + def test_set_error_cell_resolves_click_to_cell_and_clears_clickdata(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + click = {"points": [{"x": "joy", "y": "sadness"}]} + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] + data, click_out = set_cell(click, None) + self.assertEqual(data, {"pred": 1, "true": 0, "indices": [0]}) + self.assertIsNone(click_out) + + def test_set_error_cell_prevents_update_on_unresolved_click(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + click = {"points": [{"x": "??", "y": "??"}]} + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] + with self.assertRaises(PreventUpdate): + set_cell(click, None) + + def test_set_error_cell_clear_button_resets_store(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "error-cell-clear-btn.n_clicks"}] + data, click_out = set_cell(None, 1) + self.assertIsNone(data) + self.assertIsNone(click_out) + + def test_toggle_error_cell_clear_button_visibility(self): + webapp = self._make_webapp_with_gt() + toggle = self._callback(webapp, "error-cell-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle({"pred": 1, "true": 0, "indices": [0]})["display"], "inline") + + def test_update_error_word_charts_empty_without_a_selected_cell(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + fig_pred, fig_true, caption = update(None) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("Click a cell", caption) + + def test_update_error_word_charts_empty_when_cell_has_no_samples(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + fig_pred, fig_true, caption = update({"pred": 0, "true": 1, "indices": []}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIn("0 samples", caption) + + def test_update_error_word_charts_off_diagonal_cell_flags_few_samples(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + # pred=joy(1), true=sadness(0): the single planted error, row 0. + fig_pred, fig_true, caption = update({"pred": 1, "true": 0, "indices": [0]}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("predicted joy", caption) + self.assertIn("true sadness", caption) + self.assertIn("1 sample", caption) + self.assertIn("few samples", caption) + + def test_update_error_word_charts_diagonal_cell_marks_correct_predictions(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + # pred=joy(1), true=joy(1): a correct prediction, row 2. + fig_pred, fig_true, caption = update({"pred": 1, "true": 1, "indices": [2]}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("correct predictions (diagonal)", caption) + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -1269,6 +1359,51 @@ def test_compares_as_strings(self): self.assertEqual(error_positions(explanation), {1}) +class TestUnpackDatapoint(unittest.TestCase): + """``unpack_datapoint`` slices a ``pack_datapoint`` payload down to one class for rendering.""" + + def test_multiclass_slices_the_requested_column(self): + dp = pack_datapoint( + text="i am happy", + orig_idx=2, + tokens=["i", "am", "happy"], + values=np.array([[0.1, -0.1], [0.2, -0.2], [0.3, -0.3]]), + base_values=np.array([0.5, -0.5]), + label="joy", + ) + tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) + self.assertEqual(tokens, ["i", "am", "happy"]) + np.testing.assert_allclose(vals, [-0.1, -0.2, -0.3]) + self.assertEqual(base_value, -0.5) + self.assertEqual(label, "joy") + + def test_binary_1d_values_ignore_label_idx(self): + dp = pack_datapoint( + text="not great", + orig_idx=None, + tokens=["not", "great"], + values=np.array([0.2, -0.4]), + base_values=None, + ) + tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) + np.testing.assert_allclose(vals, [0.2, -0.4]) + self.assertIsNone(base_value) + self.assertIsNone(label) + + def test_single_scalar_base_value_is_shared_across_classes(self): + dp = pack_datapoint( + text="ok", + orig_idx=0, + tokens=["ok"], + values=np.array([[0.1, 0.2, 0.3]]), + base_values=np.array([0.7]), # one shared scalar, not one per class + ) + # label_idx=2 slices fine into values (3 classes) but has no matching entry in base_values. + _, vals, base_value, _ = unpack_datapoint(dp, label_idx=2) + np.testing.assert_allclose(vals, [0.3]) + self.assertEqual(base_value, 0.7) + + # --------------------------------------------------------------------------- # Single-word profile: word_occurrences / vocabulary / aggregators / plot / plotter # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py index e7e58cc8..fa6b8c99 100644 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ b/tests/unit_tests/webapp/test_nlp_whatif_app.py @@ -25,6 +25,7 @@ SimilarExamplesComponent, ) from shapash.webapp.nlp_components.base import AppContext +from shapash.webapp.nlp_components.datapoint import pack_datapoint LABEL_NAMES = ["neg", "pos"] @@ -1367,6 +1368,69 @@ def test_missing_class_prevents_update(self): with self.assertRaises(PreventUpdate): self.graph(None, 10, "all", [], "mean", 1, None, None, False, None) + def test_generic_empty_explains_itself_when_neither_sign_nor_floor_is_the_cause(self): + # Excluding every word in the corpus empties the ranking without any sign filter or floor + # active — the fallback message names neither, unlike the two more specific reasons above. + fig = self.graph(0, 10, "all", ["rare", "common", "mild"], "mean", 1, None, None, False, None) + self.assertEqual(len(fig.data), 0) + self.assertIn("No word passes these filters in these 4 sample(s)", fig.layout.annotations[0].text) + + +class TestWordImportanceClickCallbacks(unittest.TestCase): + """Word-click store sync: a bar click or the clear button, and the clear button's own visibility.""" + + @staticmethod + def _app(): + return NlpWebApp(TestGlobalWordImportancePanel._explanation(), engine=None) + + def test_bar_click_publishes_the_word_and_resets_clickdata(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + click_data = {"points": [{"y": "common"}]} + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] + words, reset = update(click_data, None) + self.assertEqual(words, ["common"]) + self.assertIsNone(reset) + + def test_clear_button_resets_the_store(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "word-filter-clear-btn.n_clicks"}] + words, reset = update(None, 1) + self.assertIsNone(words) + self.assertIsNone(reset) + + def test_empty_click_prevents_update(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] + with self.assertRaises(PreventUpdate): + update({"points": []}, None) + with self.assertRaises(PreventUpdate): + update(None, None) + + def test_toggle_clear_button_visibility(self): + app = self._app() + toggle = _callback(app, "word-filter-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle(["common"])["display"], "inline") + class TestWordImportanceScatterSync(unittest.TestCase): """Word Importance and Scatter communicate only through the shared stores (Phase A extraction). @@ -1643,6 +1707,413 @@ def test_without_prediction_or_ground_truth_draws_a_single_unlabeled_group(self) self.assertEqual(len(fig.data[0].x), 2) +class TestScatterComponentCallbacks(unittest.TestCase): + """The scatter panel's own callbacks: color-mode/word-list sync and box/lasso/click selection.""" + + @staticmethod + def _explanation(): + texts = pd.Series(["rare common", "common"]) + return NlpExplanation( + texts=texts, + token_strings=[["rare", "common"], ["common"]], + values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], + base_values=None, + y_pred=pd.Series(["neg", "pos"], index=texts.index, name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "pos"], index=texts.index, name="ground_truth"), + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def _app(self): + return NlpWebApp(self._explanation(), engine=None, projection=np.zeros((2, 2))) + + def test_ground_truth_option_offered_when_available(self): + app = self._app() + # Walk the layout for the color-by dropdown directly, mirroring TestScatterComponentWordContributionOption. + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(app.app.layout) + dropdown = found["color-by"] + self.assertIn("ground_truth", [opt["value"] for opt in dropdown.options]) + + def test_sync_color_by_switches_to_word_contribution_when_words_selected(self): + app = self._app() + sync = _callback(app, "color-by.value") + self.assertEqual(sync(["common"]), "word_contribution") + + def test_sync_color_by_prevents_update_when_words_cleared(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "color-by.value") + with self.assertRaises(PreventUpdate): + sync([]) + with self.assertRaises(PreventUpdate): + sync(None) + + def test_follow_word_click_mirrors_a_single_word_as_a_list(self): + app = self._app() + follow = _callback(app, "scatter-word-select.value") + self.assertEqual(follow("common"), ["common"]) + + def test_follow_word_click_passes_through_a_list_and_clears_on_falsy(self): + app = self._app() + follow = _callback(app, "scatter-word-select.value") + self.assertEqual(follow(["common", "rare"]), ["common", "rare"]) + self.assertEqual(follow(None), []) + + def test_word_filter_from_scatter_publishes_to_the_shared_store(self): + app = self._app() + publish = _callback(app, "word-click-filter.data@") + self.assertEqual(publish(["common"]), ["common"]) + self.assertIsNone(publish([])) + self.assertIsNone(publish(None)) + + def test_toggle_word_select_visibility_follows_color_mode(self): + app = self._app() + toggle = _callback(app, "scatter-word-select.style") + self.assertNotEqual(toggle("word_contribution").get("display"), "none") + self.assertEqual(toggle("prediction")["display"], "none") + + def test_update_scatter_selection_clear_button_resets(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-clear-btn.n_clicks"}] + self.assertIsNone(update(None, None, 1)) + + def test_update_scatter_selection_from_a_click(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + click_data = {"points": [{"customdata": [1]}]} + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.clickData"}] + self.assertEqual(update(None, click_data, None), [1]) + + def test_update_scatter_selection_ignores_an_empty_click(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.clickData"}] + self.assertIsNone(update(None, {"points": []}, None)) + self.assertIsNone(update(None, None, None)) + + def test_update_scatter_selection_ignores_an_empty_box_select_as_a_recolor_echo(self): + from dash.exceptions import PreventUpdate + + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] + with self.assertRaises(PreventUpdate): + update({"points": []}, None, None) + + def test_update_scatter_selection_from_a_box_select(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + selected_data = {"points": [{"customdata": [0]}, {"customdata": [1]}]} + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] + self.assertEqual(update(selected_data, None, None), [0, 1]) + + def test_toggle_clear_button_visibility(self): + app = self._app() + toggle = _callback(app, "scatter-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle([0, 1])["display"], "inline") + + +def _make_local_panel_explanation(): + """Two samples, two classes — enough for the sentence-highlight / waterfall panels.""" + texts = pd.Series(["i am happy", "so sad"]) + return NlpExplanation( + texts=texts, + token_strings=[["i", "am", "happy"], ["so", "sad"]], + values=[np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), np.array([[-0.3, 0.3], [-0.5, 0.5]])], + base_values=np.array([[0.0, 0.0], [0.0, 0.0]]), + y_pred=pd.Series(["pos", "neg"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +def _make_local_panel_datapoint(label="pos"): + return pack_datapoint( + text="i am happy", + orig_idx=0, + tokens=["i", "am", "happy"], + values=np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), + base_values=np.array([0.0, 0.0]), + label=label, + ) + + +class TestSentenceHighlightComponentCallbacks(unittest.TestCase): + """The Sentence panel's own callbacks: class-picker sync to the prediction, and the render.""" + + def _app(self): + return NlpWebApp(_make_local_panel_explanation(), engine=None) + + def test_sync_local_class_prevents_update_without_a_datapoint(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "local-class-selector.value") + with self.assertRaises(PreventUpdate): + sync(None) + with self.assertRaises(PreventUpdate): + sync({"text": "i am happy", "label": None}) + + def test_sync_local_class_prevents_update_for_an_unknown_label(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "local-class-selector.value") + with self.assertRaises(PreventUpdate): + sync(_make_local_panel_datapoint(label="not-a-real-class")) + + def test_sync_local_class_resolves_the_predicted_label_to_its_index(self): + app = self._app() + sync = _callback(app, "local-class-selector.value") + self.assertEqual(sync(_make_local_panel_datapoint(label="pos")), LABEL_NAMES.index("pos")) + + def test_update_sentence_highlight_prevents_update_without_datapoint_or_class(self): + from dash.exceptions import PreventUpdate + + app = self._app() + update = _callback(app, "sentence-highlight.children") + with self.assertRaises(PreventUpdate): + update(None, 0) + with self.assertRaises(PreventUpdate): + update(_make_local_panel_datapoint(), None) + + def test_update_sentence_highlight_renders_for_a_valid_datapoint(self): + from dash import html + + app = self._app() + update = _callback(app, "sentence-highlight.children") + result = update(_make_local_panel_datapoint(), 1) + self.assertIsInstance(result, html.Div) + + +class TestWaterfallComponentCallbacks(unittest.TestCase): + """The Waterfall panel's own callback: current datapoint + class + threshold -> figure.""" + + def _app(self): + return NlpWebApp(_make_local_panel_explanation(), engine=None) + + def test_update_waterfall_prevents_update_without_datapoint_or_class(self): + from dash.exceptions import PreventUpdate + + app = self._app() + update = _callback(app, "waterfall-graph.figure") + with self.assertRaises(PreventUpdate): + update(None, 0, 10) + with self.assertRaises(PreventUpdate): + update(_make_local_panel_datapoint(), None, 10) + + def test_update_waterfall_renders_and_titles_by_class(self): + import plotly.graph_objs as go + + app = self._app() + update = _callback(app, "waterfall-graph.figure") + fig = update(_make_local_panel_datapoint(), 1, 10) + self.assertIsInstance(fig, go.Figure) + self.assertIn(LABEL_NAMES[1], fig.layout.title.text) + + def test_update_waterfall_defaults_the_threshold_when_missing(self): + app = self._app() + update = _callback(app, "waterfall-graph.figure") + # None threshold (e.g. before the slider ever fires) must not raise — falls back to 10%. + fig = update(_make_local_panel_datapoint(), 0, None) + self.assertIsNotNone(fig) + + +class TestCounterfactualComponentCallbacks(unittest.TestCase): + """The generate/apply wiring: method-group toggling, the Generate click, and per-row Apply.""" + + def _app(self, engine=None): + engine = engine or FakeEngine(can_edit=True, can_cf=True) + return NlpWebApp(engine.to_explanation(), engine=engine), engine + + def test_toggle_controls_shows_only_the_selected_generators_group(self): + app, _ = self._app() + toggle = _callback(app, "counterfactual-cfg-group-hotflip.style") + hotflip_style, ablation_style = toggle("hotflip") + self.assertNotEqual(hotflip_style.get("display"), "none") + self.assertEqual(ablation_style.get("display"), "none") + + # generate(n_clicks, datapoint, selected_gen, *config_values) — 6 states: (num_examples, + # max_flips/max_ablations, tokens_to_ignore) for each of FakeEngine's two generators, in order. + _CONFIG_VALUES = (5, 3, "", 5, 3, "") + + def test_generate_prevents_update_without_a_click(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + generate = _callback(app, "counterfactual-results.children") + with self.assertRaises(PreventUpdate): + generate(None, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + + def test_generate_prevents_update_without_text(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + generate = _callback(app, "counterfactual-results.children") + with self.assertRaises(PreventUpdate): + generate(1, None, "hotflip", *self._CONFIG_VALUES) + with self.assertRaises(PreventUpdate): + generate(1, {"text": " "}, "hotflip", *self._CONFIG_VALUES) + + def test_generate_returns_a_results_table_on_success(self): + app, engine = self._app() + generate = _callback(app, "counterfactual-results.children") + children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + self.assertIsNotNone(children) + self.assertEqual(texts, [cf.new_text for cf in engine.generate_counterfactuals("i am happy")]) + + def test_generate_reports_when_no_counterfactual_is_found(self): + class _NoResultsEngine(FakeEngine): + def generate_counterfactuals(self, text, config=None, generator=None): + return [] + + app, _ = self._app(_NoResultsEngine(can_edit=True, can_cf=True)) + generate = _callback(app, "counterfactual-results.children") + children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + self.assertIn("No counterfactual found", children.children) + self.assertEqual(texts, []) + + def test_apply_prevents_update_without_any_click(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with self.assertRaises(PreventUpdate): + apply_fn([None], ["a new text"]) + + def test_apply_prevents_update_on_out_of_range_index(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import counterfactual as counterfactual_module + + app, _ = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with mock.patch.object(counterfactual_module, "callback_context") as cc: + cc.triggered_id = {"type": "counterfactual-apply", "index": 5} + with self.assertRaises(PreventUpdate): + apply_fn([1], ["only one text"]) + + def test_apply_publishes_the_chosen_text_and_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import counterfactual as counterfactual_module + + app, engine = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with mock.patch.object(counterfactual_module, "callback_context") as cc: + cc.triggered_id = {"type": "counterfactual-apply", "index": 0} + text, datapoint = apply_fn([1], ["a new counterfactual text"]) + self.assertEqual(text, "a new counterfactual text") + self.assertEqual(datapoint["text"], "a new counterfactual text") + self.assertEqual(datapoint["label"], "pos") # FakeEngine.explain_text always returns "pos" + + +class TestDataEditorComponentCallbacks(unittest.TestCase): + """The editor's own callbacks: prefill from a row/applied counterfactual, and Predict.""" + + def _app(self): + engine = FakeEngine(can_edit=True, can_cf=False) + return NlpWebApp(engine.to_explanation(), engine=engine) + + def test_prefill_prevents_update_without_a_row_or_an_applied_text(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] + with self.assertRaises(PreventUpdate): + prefill(None, None) + + def test_prefill_from_a_selected_row(self): + from unittest import mock + + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] + self.assertEqual(prefill([{"text": "a selected row"}], None), "a selected row") + + def test_prefill_from_an_applied_counterfactual(self): + from unittest import mock + + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "whatif-apply-store.data"}] + self.assertEqual(prefill(None, "an applied counterfactual"), "an applied counterfactual") + + def test_predict_prevents_update_without_a_click_or_text(self): + from dash.exceptions import PreventUpdate + + app = self._app() + predict = _callback(app, "data-editor-prob.figure") + with self.assertRaises(PreventUpdate): + predict(None, "some text") + with self.assertRaises(PreventUpdate): + predict(1, "") + with self.assertRaises(PreventUpdate): + predict(1, " ") + + class TestWordProfileControls(unittest.TestCase): """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" From 2f170b47892c095a303215eeeec57e63ea987718 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 17:45:40 +0200 Subject: [PATCH 117/144] test(webapp): split NLP webapp test coverage by component test_nlp_whatif_app.py had grown into a 2200-line file covering every NLP webapp component at once. Split it to mirror shapash/webapp/nlp_components/ module-for-module under tests/unit_tests/webapp/nlp_components/, with the NlpWebApp shell/mounting tests moved to their own test_nlp_app.py. Shared fixtures (FakeEngine, AppContext helper, _collect_ids) consolidated into nlp_components/_shared.py. Pure code movement, no test behavior changes; full suite pass count verified unchanged before/after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- .../webapp/nlp_components/__init__.py | 0 .../webapp/nlp_components/_shared.py | 265 ++ .../webapp/nlp_components/test_base.py | 108 + .../nlp_components/test_counterfactual.py | 97 + .../webapp/nlp_components/test_data_editor.py | 62 + .../webapp/nlp_components/test_datapoint.py | 52 + .../webapp/nlp_components/test_label_noise.py | 216 ++ .../webapp/nlp_components/test_scatter.py | 269 ++ .../nlp_components/test_sentence_highlight.py | 60 + .../nlp_components/test_similar_examples.py | 159 ++ .../webapp/nlp_components/test_waterfall.py | 45 + .../nlp_components/test_word_importance.py | 331 +++ .../nlp_components/test_word_profile.py | 370 +++ tests/unit_tests/webapp/test_nlp_app.py | 910 +++++++ .../unit_tests/webapp/test_nlp_whatif_app.py | 2214 ----------------- 15 files changed, 2944 insertions(+), 2214 deletions(-) create mode 100644 tests/unit_tests/webapp/nlp_components/__init__.py create mode 100644 tests/unit_tests/webapp/nlp_components/_shared.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_base.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_counterfactual.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_data_editor.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_datapoint.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_label_noise.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_scatter.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_sentence_highlight.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_similar_examples.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_waterfall.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_word_importance.py create mode 100644 tests/unit_tests/webapp/nlp_components/test_word_profile.py create mode 100644 tests/unit_tests/webapp/test_nlp_app.py delete mode 100644 tests/unit_tests/webapp/test_nlp_whatif_app.py diff --git a/tests/unit_tests/webapp/nlp_components/__init__.py b/tests/unit_tests/webapp/nlp_components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/webapp/nlp_components/_shared.py b/tests/unit_tests/webapp/nlp_components/_shared.py new file mode 100644 index 00000000..30765190 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/_shared.py @@ -0,0 +1,265 @@ +"""Fixtures and helpers shared by 2+ of the ``tests/unit_tests/webapp/nlp_components`` test modules.""" + +from dataclasses import replace + +import numpy as np +import pandas as pd + +from shapash.backend.nlp_backend import NlpContributions +from shapash.compute.diagnostics.label_noise import detect_label_issues +from shapash.compute.diagnostics.label_probe import LabelProbe +from shapash.compute.generators.base import Counterfactual, IntField, TokenListField +from shapash.compute.retrieval.similar_examples import Neighbor +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.webapp.nlp_components.base import AppContext +from shapash.webapp.nlp_components.datapoint import pack_datapoint + +LABEL_NAMES = ["neg", "pos"] + + +def _ctx(explanation, engine=None, coords=None) -> AppContext: + """The context ``NlpWebApp`` builds, for calling a component directly.""" + return AppContext(explanation, engine=engine, coords=coords) + + +# A tiny lexically separable corpus for the independent label probe. "bad"/"awful" sit in "neg", +# so a row labelled "neg" gets backed and a row labelled "pos" gets rejected. +_PROBE_CORPUS = ( + [ + "i am happy", + "happy and glad", + "so glad today", + "a happy glad day", + "this is bad", + "bad and awful", + "so awful today", + "a bad awful day", + ], + ["pos"] * 4 + ["neg"] * 4, +) + + +def _contributions() -> NlpContributions: + token_strings = [["i", "am", "happy"], ["this", "is", "bad"]] + values = [np.random.randn(3, 2), np.random.randn(3, 2)] + base_values = np.zeros((2, 2)) + return NlpContributions(token_strings=token_strings, values=values, base_values=base_values) + + +class FakeEngine: + """Minimal explainer/engine stand-in exposing the InteractiveEngine surface + compiled data.""" + + def __init__( + self, + can_edit: bool, + can_cf: bool, + can_similar: bool = False, + has_labels: bool = False, + probe_corpus: tuple[list[str], list[str]] | None = None, + ): + self.probe_corpus = probe_corpus + self._can_edit = can_edit + self._can_cf = can_cf + self._can_similar = can_similar + # A retriever-like handle the SimilarExamples panel reads its layer caption from. + self._retriever = type("R", (), {"layer": "pre_classifier"})() if can_similar else None + self.label_names = LABEL_NAMES + self.texts = pd.Series(["i am happy", "this is bad"], index=pd.RangeIndex(2)) + self.contributions = _contributions() + self.y_pred = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="prediction") + self.y_prob = pd.DataFrame({"neg": [0.2, 0.8], "pos": [0.8, 0.2]}, index=pd.RangeIndex(2)) + self.y_true = None + if has_labels: + # A batch with exactly one planted label error. With two samples and two classes the + # arrangement is forced: both classes must carry a label (or the unlabelled one has no + # estimable threshold and can never be suggested), so sample 0 is labelled "neg" while + # the model confidently says "pos", and sample 1 is labelled "pos" and agrees. + self.y_prob = pd.DataFrame({"neg": [0.1, 0.1], "pos": [0.9, 0.9]}, index=pd.RangeIndex(2)) + self.y_pred = pd.Series(["pos", "pos"], index=pd.RangeIndex(2), name="prediction") + self.y_true = pd.Series(["neg", "pos"], index=pd.RangeIndex(2), name="ground_truth") + # Ground truth is off by default so the existing layout tests, which pin the exact tab + # groups, keep seeing neither the Error Analysis nor the Label Noise tab. + self.detect_calls = [] + + def to_explanation(self) -> NlpExplanation: + """The ``NlpExplanation`` a real ``explain()`` call would have produced for this batch. + + ``NlpWebApp`` holds an explanation, not the engine — this is what tests pass as the first + argument, while ``self`` (the ``FakeEngine``) is passed separately as ``engine=``. + """ + return NlpExplanation( + texts=self.texts, + token_strings=self.contributions.token_strings, + values=self.contributions.values, + base_values=self.contributions.base_values, + y_pred=self.y_pred, + y_prob=self.y_prob, + y_true=self.y_true, + label_names=self.label_names, + folds_case=None, + backend_name="fake", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def can_detect_label_noise(self, explanation=None): + return self.y_true is not None + + def can_probe_labels(self): + return self.probe_corpus is not None + + def detect_label_noise(self, explanation, top_n=50, score="self_confidence", probe=True): + self.detect_calls.append({"top_n": top_n, "score": score, "probe": probe}) + report = detect_label_issues( + self.y_prob.to_numpy(dtype=float), + [str(v) for v in self.y_true.tolist()], + [str(t) for t in self.texts.tolist()], + list(self.y_prob.columns), + top_n=top_n, + score=score, + ) + if probe and self.can_probe_labels() and report.issues: + verdicts = LabelProbe(*self.probe_corpus).verdicts( + [i.text for i in report.issues], [i.given_label for i in report.issues] + ) + report = replace( + report, + issues=[replace(i, probe=v) for i, v in zip(report.issues, verdicts, strict=True)], + ) + return report + + def can_edit(self): + return self._can_edit + + def can_counterfactual(self): + return self._can_cf + + def can_find_similar(self): + return self._can_similar + + def find_similar(self, text, top_k=5): + return [ + Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), + Neighbor(index=1, score=0.80, text="this is awful", label="neg"), + ][:top_k] + + def find_similar_threshold(self, text, threshold=0.95, limit=50): + all_neighbors = [ + Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), + Neighbor(index=1, score=0.96, text="so glad today", label="pos"), + Neighbor(index=2, score=0.80, text="this is awful", label="neg"), + ] + matches = [n for n in all_neighbors if n.score > threshold] + return matches[:limit], len(matches) + + def available_cf_generators(self): + return [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")] + + def cf_config_spec(self, generator=None): + max_field = "max_ablations" if generator == "ablation_flip" else "max_flips" + return { + "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), + max_field: IntField(label="Max token edits", default=3, minimum=1, maximum=5), + "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), + } + + def predict(self, text): + return "pos", {"neg": 0.3, "pos": 0.7} + + def explain_text(self, text): + c = _contributions() + return c, "pos", {"neg": 0.3, "pos": 0.7} + + def generate_counterfactuals(self, text, config=None, generator=None): + return [ + Counterfactual( + original_text=text, + new_text=text.replace("happy", "bad"), + tokens=["i", "am", "happy"], + flipped_positions=[2], + substitutions=[(2, "happy", "bad")], + orig_label="pos", + new_label="neg", + orig_prob=0.8, + new_prob=0.7, + prob_delta=0.5, + ) + ] + + +def _collect_ids(node, found): + """Recursively collect all string component ids in a Dash layout tree. + + Also descends into RadioItems/Checklist ``options[].label`` — components nested there (e.g. a + numeric input inline with its radio button) aren't under ``.children``, but Dash still renders + and wires them, since ``label`` is documented to accept a component. + """ + cid = getattr(node, "id", None) + if isinstance(cid, str): + found.add(cid) + options = getattr(node, "options", None) + if isinstance(options, (list, tuple)): + for opt in options: + label = opt.get("label") if isinstance(opt, dict) else None + if isinstance(label, (list, tuple)): + for item in label: + _collect_ids(item, found) + elif label is not None: + _collect_ids(label, found) + children = getattr(node, "children", None) + if children is None: + return + if isinstance(children, (list, tuple)): + for ch in children: + _collect_ids(ch, found) + else: + _collect_ids(children, found) + + +def _callback_binding_ids(app, output_substr): + """Return the (id, property) pairs bound as inputs+state for the callback with this output.""" + for key, spec in app.app.callback_map.items(): + if output_substr in key: + pairs = [(i["id"], i["property"]) for i in spec["inputs"]] + pairs += [(s["id"], s["property"]) for s in spec.get("state", [])] + return pairs + raise KeyError(output_substr) + + +def _callback(app, out_substr): + for key, spec in app.app.callback_map.items(): + if out_substr in key: + return getattr(spec["callback"], "__wrapped__", spec["callback"]) + raise KeyError(out_substr) + + +def _make_local_panel_explanation(): + """Two samples, two classes — enough for the sentence-highlight / waterfall panels.""" + texts = pd.Series(["i am happy", "so sad"]) + return NlpExplanation( + texts=texts, + token_strings=[["i", "am", "happy"], ["so", "sad"]], + values=[np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), np.array([[-0.3, 0.3], [-0.5, 0.5]])], + base_values=np.array([[0.0, 0.0], [0.0, 0.0]]), + y_pred=pd.Series(["pos", "neg"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +def _make_local_panel_datapoint(label="pos"): + return pack_datapoint( + text="i am happy", + orig_idx=0, + tokens=["i", "am", "happy"], + values=np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), + base_values=np.array([0.0, 0.0]), + label=label, + ) diff --git a/tests/unit_tests/webapp/nlp_components/test_base.py b/tests/unit_tests/webapp/nlp_components/test_base.py new file mode 100644 index 00000000..62f13b0f --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_base.py @@ -0,0 +1,108 @@ +"""Unit tests for shapash.webapp.nlp_components.base: compose_selection and error_positions.""" + +import unittest + +import numpy as np +import pandas as pd + +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.webapp.nlp_components import compose_selection as _compose_selection +from shapash.webapp.nlp_components import error_positions + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + + +def _make_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: + """Synthetic per-token contribution data for 3 samples, 6 classes.""" + rng = np.random.default_rng(42) + token_strings = [ + ["", "i", "feel", "so", "happy", "today", ""], + ["", "this", "is", "terrible", "and", "sad", ""], + ["", "what", "a", "wonderful", "day", ""], + ] + values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] + base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) + return token_strings, values, base_values + + +def _make_texts() -> pd.Series: + return pd.Series( + ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], + index=pd.RangeIndex(3), + ) + + +def _make_explanation( + texts: pd.Series | None = None, + y_pred: pd.Series | None = None, + y_prob: pd.DataFrame | None = None, + y_true: pd.Series | None = None, + label_names: list[str] | None = LABEL_NAMES, + backend_name: str = "nlp_shap", + is_additive: bool = True, + reference_kind: str = "none", + output_space: str = "probability", +) -> NlpExplanation: + """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" + texts = _make_texts() if texts is None else texts + token_strings, values, base_values = _make_token_data() + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=( + pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred + ), + y_prob=y_prob, + y_true=y_true, + label_names=label_names, + folds_case=None, + backend_name=backend_name, + is_additive=is_additive, + reference_kind=reference_kind, + output_space=output_space, + ) + + +class TestComposeSelection(unittest.TestCase): + """The three sample filters (scatter box, confusion cell, errors-only) must intersect.""" + + def test_nothing_active_returns_none(self): + self.assertIsNone(_compose_selection(None, None, None)) + + def test_scatter_only(self): + self.assertEqual(_compose_selection([1, 2, 3], None, None), [1, 2, 3]) + + def test_cell_only(self): + self.assertEqual(_compose_selection(None, [4, 5], None), [4, 5]) + + def test_errors_only(self): + self.assertEqual(_compose_selection(None, None, {2, 7}), [2, 7]) + + def test_scatter_intersect_cell(self): + self.assertEqual(_compose_selection([1, 2, 3], [2, 3, 4], None), [2, 3]) + + def test_scatter_intersect_errors(self): + # This is the box-then-errors case: errors *within* the selected points. + self.assertEqual(_compose_selection([1, 2, 3, 4], None, {2, 4, 9}), [2, 4]) + + def test_all_three_intersect(self): + self.assertEqual(_compose_selection([1, 2, 3, 4], [2, 3, 4], {3, 4}), [3, 4]) + + def test_empty_intersection_is_empty_list(self): + self.assertEqual(_compose_selection([1, 2], None, {8, 9}), []) + + +class TestErrorPositions(unittest.TestCase): + """The shared definition of "a model error" every errors-scoped panel filters on.""" + + def test_none_without_ground_truth(self): + self.assertIsNone(error_positions(_make_explanation())) + + def test_compares_as_strings(self): + texts = _make_texts() + explanation = _make_explanation(y_true=pd.Series(["joy", "joy", "joy"], index=texts.index)) + # y_pred is ["joy", "sadness", "joy"], so only the middle row disagrees. + self.assertEqual(error_positions(explanation), {1}) diff --git a/tests/unit_tests/webapp/nlp_components/test_counterfactual.py b/tests/unit_tests/webapp/nlp_components/test_counterfactual.py new file mode 100644 index 00000000..c40a2bdc --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_counterfactual.py @@ -0,0 +1,97 @@ +"""Unit tests for shapash.webapp.nlp_components.counterfactual: CounterfactualComponent.""" + +import unittest + +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import FakeEngine, _callback + + +class TestCounterfactualComponentCallbacks(unittest.TestCase): + """The generate/apply wiring: method-group toggling, the Generate click, and per-row Apply.""" + + def _app(self, engine=None): + engine = engine or FakeEngine(can_edit=True, can_cf=True) + return NlpWebApp(engine.to_explanation(), engine=engine), engine + + def test_toggle_controls_shows_only_the_selected_generators_group(self): + app, _ = self._app() + toggle = _callback(app, "counterfactual-cfg-group-hotflip.style") + hotflip_style, ablation_style = toggle("hotflip") + self.assertNotEqual(hotflip_style.get("display"), "none") + self.assertEqual(ablation_style.get("display"), "none") + + # generate(n_clicks, datapoint, selected_gen, *config_values) — 6 states: (num_examples, + # max_flips/max_ablations, tokens_to_ignore) for each of FakeEngine's two generators, in order. + _CONFIG_VALUES = (5, 3, "", 5, 3, "") + + def test_generate_prevents_update_without_a_click(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + generate = _callback(app, "counterfactual-results.children") + with self.assertRaises(PreventUpdate): + generate(None, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + + def test_generate_prevents_update_without_text(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + generate = _callback(app, "counterfactual-results.children") + with self.assertRaises(PreventUpdate): + generate(1, None, "hotflip", *self._CONFIG_VALUES) + with self.assertRaises(PreventUpdate): + generate(1, {"text": " "}, "hotflip", *self._CONFIG_VALUES) + + def test_generate_returns_a_results_table_on_success(self): + app, engine = self._app() + generate = _callback(app, "counterfactual-results.children") + children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + self.assertIsNotNone(children) + self.assertEqual(texts, [cf.new_text for cf in engine.generate_counterfactuals("i am happy")]) + + def test_generate_reports_when_no_counterfactual_is_found(self): + class _NoResultsEngine(FakeEngine): + def generate_counterfactuals(self, text, config=None, generator=None): + return [] + + app, _ = self._app(_NoResultsEngine(can_edit=True, can_cf=True)) + generate = _callback(app, "counterfactual-results.children") + children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) + self.assertIn("No counterfactual found", children.children) + self.assertEqual(texts, []) + + def test_apply_prevents_update_without_any_click(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with self.assertRaises(PreventUpdate): + apply_fn([None], ["a new text"]) + + def test_apply_prevents_update_on_out_of_range_index(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import counterfactual as counterfactual_module + + app, _ = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with mock.patch.object(counterfactual_module, "callback_context") as cc: + cc.triggered_id = {"type": "counterfactual-apply", "index": 5} + with self.assertRaises(PreventUpdate): + apply_fn([1], ["only one text"]) + + def test_apply_publishes_the_chosen_text_and_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import counterfactual as counterfactual_module + + app, engine = self._app() + apply_fn = _callback(app, "whatif-apply-store.data") + with mock.patch.object(counterfactual_module, "callback_context") as cc: + cc.triggered_id = {"type": "counterfactual-apply", "index": 0} + text, datapoint = apply_fn([1], ["a new counterfactual text"]) + self.assertEqual(text, "a new counterfactual text") + self.assertEqual(datapoint["text"], "a new counterfactual text") + self.assertEqual(datapoint["label"], "pos") # FakeEngine.explain_text always returns "pos" diff --git a/tests/unit_tests/webapp/nlp_components/test_data_editor.py b/tests/unit_tests/webapp/nlp_components/test_data_editor.py new file mode 100644 index 00000000..8972324e --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_data_editor.py @@ -0,0 +1,62 @@ +"""Unit tests for shapash.webapp.nlp_components.data_editor: DataEditorComponent.""" + +import unittest + +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import FakeEngine, _callback + + +class TestDataEditorComponentCallbacks(unittest.TestCase): + """The editor's own callbacks: prefill from a row/applied counterfactual, and Predict.""" + + def _app(self): + engine = FakeEngine(can_edit=True, can_cf=False) + return NlpWebApp(engine.to_explanation(), engine=engine) + + def test_prefill_prevents_update_without_a_row_or_an_applied_text(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] + with self.assertRaises(PreventUpdate): + prefill(None, None) + + def test_prefill_from_a_selected_row(self): + from unittest import mock + + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] + self.assertEqual(prefill([{"text": "a selected row"}], None), "a selected row") + + def test_prefill_from_an_applied_counterfactual(self): + from unittest import mock + + from shapash.webapp.nlp_components import data_editor as data_editor_module + + app = self._app() + prefill = _callback(app, "data-editor-input.value") + with mock.patch.object(data_editor_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "whatif-apply-store.data"}] + self.assertEqual(prefill(None, "an applied counterfactual"), "an applied counterfactual") + + def test_predict_prevents_update_without_a_click_or_text(self): + from dash.exceptions import PreventUpdate + + app = self._app() + predict = _callback(app, "data-editor-prob.figure") + with self.assertRaises(PreventUpdate): + predict(None, "some text") + with self.assertRaises(PreventUpdate): + predict(1, "") + with self.assertRaises(PreventUpdate): + predict(1, " ") diff --git a/tests/unit_tests/webapp/nlp_components/test_datapoint.py b/tests/unit_tests/webapp/nlp_components/test_datapoint.py new file mode 100644 index 00000000..c6932b5f --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_datapoint.py @@ -0,0 +1,52 @@ +"""Unit tests for shapash.webapp.nlp_components.datapoint: unpack_datapoint.""" + +import unittest + +import numpy as np + +from shapash.webapp.nlp_components.datapoint import pack_datapoint, unpack_datapoint + + +class TestUnpackDatapoint(unittest.TestCase): + """``unpack_datapoint`` slices a ``pack_datapoint`` payload down to one class for rendering.""" + + def test_multiclass_slices_the_requested_column(self): + dp = pack_datapoint( + text="i am happy", + orig_idx=2, + tokens=["i", "am", "happy"], + values=np.array([[0.1, -0.1], [0.2, -0.2], [0.3, -0.3]]), + base_values=np.array([0.5, -0.5]), + label="joy", + ) + tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) + self.assertEqual(tokens, ["i", "am", "happy"]) + np.testing.assert_allclose(vals, [-0.1, -0.2, -0.3]) + self.assertEqual(base_value, -0.5) + self.assertEqual(label, "joy") + + def test_binary_1d_values_ignore_label_idx(self): + dp = pack_datapoint( + text="not great", + orig_idx=None, + tokens=["not", "great"], + values=np.array([0.2, -0.4]), + base_values=None, + ) + tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) + np.testing.assert_allclose(vals, [0.2, -0.4]) + self.assertIsNone(base_value) + self.assertIsNone(label) + + def test_single_scalar_base_value_is_shared_across_classes(self): + dp = pack_datapoint( + text="ok", + orig_idx=0, + tokens=["ok"], + values=np.array([[0.1, 0.2, 0.3]]), + base_values=np.array([0.7]), # one shared scalar, not one per class + ) + # label_idx=2 slices fine into values (3 classes) but has no matching entry in base_values. + _, vals, base_value, _ = unpack_datapoint(dp, label_idx=2) + np.testing.assert_allclose(vals, [0.3]) + self.assertEqual(base_value, 0.7) diff --git a/tests/unit_tests/webapp/nlp_components/test_label_noise.py b/tests/unit_tests/webapp/nlp_components/test_label_noise.py new file mode 100644 index 00000000..432e1737 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_label_noise.py @@ -0,0 +1,216 @@ +"""Unit tests for shapash.webapp.nlp_components.label_noise: LabelNoiseComponent.""" + +import unittest + +import pandas as pd + +from shapash.compute.diagnostics.label_noise import LabelIssue +from shapash.compute.diagnostics.label_probe import ProbeVerdict +from shapash.webapp.nlp_components import LabelNoiseComponent + +from tests.unit_tests.webapp.nlp_components._shared import _PROBE_CORPUS, FakeEngine, _ctx + + +class TestLabelNoiseComponent(unittest.TestCase): + """Drive the panel's renderer and callbacks directly, without the app shell.""" + + @staticmethod + def _register(**kwargs): + import dash + + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, **kwargs) + explanation = engine.to_explanation() + app = dash.Dash(__name__) + comp = LabelNoiseComponent() + comp.register_callbacks( + app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} + ) + return app, engine + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_detect_returns_a_view_and_the_flagged_indices(self): + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + children, indices = detect(1, 50, "self_confidence") + self.assertEqual(indices, [0]) # sample 0 is labelled neg but confidently predicted pos + self.assertIsNotNone(children) + + def test_detect_forwards_the_controls_to_the_engine(self): + app, engine = self._register() + detect = self._callback(app, "label-noise-results") + detect(1, 7, "normalized_margin") + self.assertEqual(engine.detect_calls[-1], {"top_n": 7, "score": "normalized_margin", "probe": True}) + + def test_blank_controls_fall_back_to_defaults(self): + app, engine = self._register() + detect = self._callback(app, "label-noise-results") + detect(1, None, None) + self.assertEqual(engine.detect_calls[-1], {"top_n": 50, "score": "self_confidence", "probe": True}) + + def test_detect_renders_the_corpus_column_when_a_probe_corpus_is_bound(self): + app, _ = self._register(probe_corpus=_PROBE_CORPUS) + detect = self._callback(app, "label-noise-results") + children, _ = detect(1, 50, "self_confidence") + table = children.children[-1] + headers = [th.children for th in table.children[0].children.children] + self.assertEqual(headers, ["Score", "Given", "Probably", "Corpus", "Text", ""]) + # The flagged row is "i am happy" labelled "neg"; the corpus puts that vocabulary in "pos", + # so the probe rejects the label too rather than defending it. + cell = table.children[1].children[0].children[3].children + self.assertEqual(cell.children, "0.14 → pos") + + def test_detect_omits_the_corpus_column_without_a_probe_corpus(self): + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + children, _ = detect(1, 50, "self_confidence") + table = children.children[-1] + headers = [th.children for th in table.children[0].children.children] + self.assertNotIn("Corpus", headers) + + def test_detect_does_nothing_before_the_button_is_clicked(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + detect = self._callback(app, "label-noise-results") + with self.assertRaises(PreventUpdate): + detect(None, 50, "self_confidence") + + def test_a_clean_corpus_renders_a_message_and_no_rows(self): + app, engine = self._register() + # Both classes labelled and both labels agreeing with the model, so there is no noise to find. + engine.y_prob = pd.DataFrame({"neg": [0.1, 0.9], "pos": [0.9, 0.1]}, index=pd.RangeIndex(2)) + engine.y_true = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="ground_truth") + detect = self._callback(app, "label-noise-results") + children, indices = detect(1, 50, "self_confidence") + self.assertEqual(indices, []) + self.assertIn("No label issues detected", children.children) + + def test_inspect_makes_the_flagged_sample_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 0} + dp = inspect([1], [1]) # row 0 of the table points at compiled sample 1 + self.assertEqual(dp["text"], "this is bad") + self.assertEqual(dp["orig_idx"], 1) + self.assertEqual(dp["label"], "pos") # the *prediction*, matching the dataset-row path + self.assertEqual(len(dp["tokens"]), 3) + + def test_inspect_ignores_a_render_with_no_clicks(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with self.assertRaises(PreventUpdate): + inspect([0], [1]) + + def test_inspect_ignores_a_missing_trigger(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = None + with self.assertRaises(PreventUpdate): + inspect([1], [1]) + + def test_inspect_ignores_an_out_of_range_row(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 5} + with self.assertRaises(PreventUpdate): + inspect([1], [1]) + + def test_inspect_ignores_a_stale_index_beyond_the_corpus(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import label_noise as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "label-noise-apply", "index": 0} + with self.assertRaises(PreventUpdate): + inspect([1], [99]) + + +class TestLabelNoiseTable(unittest.TestCase): + """The neighbour column is evidence *beside* the score, and must not appear when absent.""" + + @staticmethod + def _issue(**kwargs): + base = dict( + index=0, + text="i am happy", + given_label="neg", + suggested_label="pos", + given_prob=0.2, + suggested_prob=0.8, + score=0.2, + ) + return LabelIssue(**{**base, **kwargs}) + + def test_corpus_column_omitted_when_no_probe_ran(self): + from shapash.webapp.nlp_components.label_noise import _issues_table + + table = _issues_table([self._issue()], "label-noise") + headers = [th.children for th in table.children[0].children.children] + self.assertNotIn("Corpus", headers) + self.assertEqual(headers, ["Score", "Given", "Probably", "Text", ""]) + + def test_corpus_column_present_when_any_row_has_a_verdict(self): + from shapash.webapp.nlp_components.label_noise import _issues_table + + verdict = ProbeVerdict(given_prob=0.8, top_label="neg", backs_given=True) + issues = [self._issue(probe=verdict), self._issue(index=1)] + table = _issues_table(issues, "label-noise") + headers = [th.children for th in table.children[0].children.children] + self.assertIn("Corpus", headers) + + def test_probe_summary_warns_when_the_corpus_backs_the_given_label(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + # The row the panel exists to catch: confident learning flagged it, but an independent + # classifier defends the label — so it is the model that is wrong, not the corpus. + verdict = ProbeVerdict(given_prob=0.83, top_label="neg", backs_given=True) + summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) + self.assertEqual(summary.children, "⚠ 0.83") + self.assertIn("text-warning", summary.className) + self.assertIn("more likely", summary.title) + + def test_probe_summary_reports_a_corroborated_label_error_plainly(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + verdict = ProbeVerdict(given_prob=0.16, top_label="pos", backs_given=False) + summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) + self.assertEqual(summary.children, "0.16 → pos") + self.assertIn("text-muted", summary.className) + + def test_rows_without_a_verdict_render_a_dash(self): + from shapash.webapp.nlp_components.label_noise import _probe_summary + + self.assertEqual(_probe_summary(self._issue()), "—") diff --git a/tests/unit_tests/webapp/nlp_components/test_scatter.py b/tests/unit_tests/webapp/nlp_components/test_scatter.py new file mode 100644 index 00000000..82f57f81 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_scatter.py @@ -0,0 +1,269 @@ +"""Unit tests for shapash.webapp.nlp_components.scatter: ScatterComponent.""" + +import unittest + +import numpy as np +import pandas as pd + +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import LABEL_NAMES, _callback, _ctx + + +class TestScatterComponentWordContributionOption(unittest.TestCase): + """``offer_word_contribution`` gates the "Word contribution" color mode, not the whole panel.""" + + @staticmethod + def _explanation(): + # Same fixture as TestGlobalWordImportancePanel (tests/unit_tests/webapp/nlp_components/ + # test_word_importance.py): "rare" pulls hard once, "common" mildly four times. Duplicated + # here rather than imported across test modules, matching this repo's no-cross-file-test- + # helper-sharing convention (sharing goes through _shared.py only). + token_strings = [["rare", "common"], ["common"], ["common", "common"], ["mild"]] + values = [ + np.array([[0.9, -0.9], [0.3, -0.3]]), + np.array([[0.3, -0.3]]), + np.array([[0.3, -0.3], [0.3, -0.3]]), + np.array([[-0.5, 0.5]]), + ] + texts = pd.Series(["rare common", "common", "common common", "mild"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["neg"] * 4, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def _color_options(self, offer_word_contribution: bool): + from shapash.webapp.nlp_components import ScatterComponent + + comp = ScatterComponent(offer_word_contribution=offer_word_contribution) + explanation = self._explanation() + layout = comp.layout(_ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2)))) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + return [opt["value"] for opt in found["color-by"].options] + + def test_offered_when_word_importance_is_mounted(self): + self.assertIn("word_contribution", self._color_options(offer_word_contribution=True)) + + def test_omitted_without_word_importance(self): + # Scatter still works standalone (Prediction/Ground-Truth) — it just cannot source a class + # index for word-contribution coloring without Word Importance's control. + options = self._color_options(offer_word_contribution=False) + self.assertNotIn("word_contribution", options) + self.assertIn("prediction", options) + + +class TestBuildScatterFig(unittest.TestCase): + """``_build_scatter_fig``'s own dispatch — the part left after extracting the drawing itself + into :func:`~shapash.plots.plot_scatter.plot_scatter`.""" + + @staticmethod + def _explanation(with_true: bool, with_pred: bool): + texts = pd.Series(["rare common", "common"]) + return NlpExplanation( + texts=texts, + token_strings=[["rare", "common"], ["common"]], + values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], + base_values=None, + y_pred=pd.Series(["neg", "pos"], index=texts.index) if with_pred else None, + y_prob=None, + y_true=pd.Series(["pos", "pos"], index=texts.index) if with_true else None, + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def _component(self): + from shapash.webapp.nlp_components import ScatterComponent + + return ScatterComponent(offer_word_contribution=True) + + def _ctx_for(self, explanation): + # The figure takes its two coordinate columns from the projection and every visual encoding + # — colours, legend, hover, error mask — from the explanation, so both have to be bound. + return _ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2))) + + def test_ground_truth_colors_by_y_true(self): + fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(True, True)), "ground_truth") + names = {trace.name for trace in fig.data} + self.assertEqual(names, {"pos"}) # both samples are "pos" in y_true + + def test_without_prediction_or_ground_truth_draws_a_single_unlabeled_group(self): + fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(False, False)), "prediction") + self.assertEqual(len(fig.data), 1) + self.assertEqual(len(fig.data[0].x), 2) + + +class TestScatterComponentCallbacks(unittest.TestCase): + """The scatter panel's own callbacks: color-mode/word-list sync and box/lasso/click selection.""" + + @staticmethod + def _explanation(): + texts = pd.Series(["rare common", "common"]) + return NlpExplanation( + texts=texts, + token_strings=[["rare", "common"], ["common"]], + values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], + base_values=None, + y_pred=pd.Series(["neg", "pos"], index=texts.index, name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "pos"], index=texts.index, name="ground_truth"), + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def _app(self): + return NlpWebApp(self._explanation(), engine=None, projection=np.zeros((2, 2))) + + def test_ground_truth_option_offered_when_available(self): + app = self._app() + # Walk the layout for the color-by dropdown directly, mirroring TestScatterComponentWordContributionOption. + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(app.app.layout) + dropdown = found["color-by"] + self.assertIn("ground_truth", [opt["value"] for opt in dropdown.options]) + + def test_sync_color_by_switches_to_word_contribution_when_words_selected(self): + app = self._app() + sync = _callback(app, "color-by.value") + self.assertEqual(sync(["common"]), "word_contribution") + + def test_sync_color_by_prevents_update_when_words_cleared(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "color-by.value") + with self.assertRaises(PreventUpdate): + sync([]) + with self.assertRaises(PreventUpdate): + sync(None) + + def test_follow_word_click_mirrors_a_single_word_as_a_list(self): + app = self._app() + follow = _callback(app, "scatter-word-select.value") + self.assertEqual(follow("common"), ["common"]) + + def test_follow_word_click_passes_through_a_list_and_clears_on_falsy(self): + app = self._app() + follow = _callback(app, "scatter-word-select.value") + self.assertEqual(follow(["common", "rare"]), ["common", "rare"]) + self.assertEqual(follow(None), []) + + def test_word_filter_from_scatter_publishes_to_the_shared_store(self): + app = self._app() + publish = _callback(app, "word-click-filter.data@") + self.assertEqual(publish(["common"]), ["common"]) + self.assertIsNone(publish([])) + self.assertIsNone(publish(None)) + + def test_toggle_word_select_visibility_follows_color_mode(self): + app = self._app() + toggle = _callback(app, "scatter-word-select.style") + self.assertNotEqual(toggle("word_contribution").get("display"), "none") + self.assertEqual(toggle("prediction")["display"], "none") + + def test_update_scatter_selection_clear_button_resets(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-clear-btn.n_clicks"}] + self.assertIsNone(update(None, None, 1)) + + def test_update_scatter_selection_from_a_click(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + click_data = {"points": [{"customdata": [1]}]} + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.clickData"}] + self.assertEqual(update(None, click_data, None), [1]) + + def test_update_scatter_selection_ignores_an_empty_click(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.clickData"}] + self.assertIsNone(update(None, {"points": []}, None)) + self.assertIsNone(update(None, None, None)) + + def test_update_scatter_selection_ignores_an_empty_box_select_as_a_recolor_echo(self): + from dash.exceptions import PreventUpdate + + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] + with self.assertRaises(PreventUpdate): + update({"points": []}, None, None) + + def test_update_scatter_selection_from_a_box_select(self): + from unittest import mock + + from shapash.webapp.nlp_components import scatter as scatter_module + + app = self._app() + update = _callback(app, "scatter-selected-indices.data") + selected_data = {"points": [{"customdata": [0]}, {"customdata": [1]}]} + with mock.patch.object(scatter_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] + self.assertEqual(update(selected_data, None, None), [0, 1]) + + def test_toggle_clear_button_visibility(self): + app = self._app() + toggle = _callback(app, "scatter-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle([0, 1])["display"], "inline") diff --git a/tests/unit_tests/webapp/nlp_components/test_sentence_highlight.py b/tests/unit_tests/webapp/nlp_components/test_sentence_highlight.py new file mode 100644 index 00000000..5d5df1ff --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_sentence_highlight.py @@ -0,0 +1,60 @@ +"""Unit tests for shapash.webapp.nlp_components.sentence_highlight: SentenceHighlightComponent.""" + +import unittest + +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import ( + LABEL_NAMES, + _callback, + _make_local_panel_datapoint, + _make_local_panel_explanation, +) + + +class TestSentenceHighlightComponentCallbacks(unittest.TestCase): + """The Sentence panel's own callbacks: class-picker sync to the prediction, and the render.""" + + def _app(self): + return NlpWebApp(_make_local_panel_explanation(), engine=None) + + def test_sync_local_class_prevents_update_without_a_datapoint(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "local-class-selector.value") + with self.assertRaises(PreventUpdate): + sync(None) + with self.assertRaises(PreventUpdate): + sync({"text": "i am happy", "label": None}) + + def test_sync_local_class_prevents_update_for_an_unknown_label(self): + from dash.exceptions import PreventUpdate + + app = self._app() + sync = _callback(app, "local-class-selector.value") + with self.assertRaises(PreventUpdate): + sync(_make_local_panel_datapoint(label="not-a-real-class")) + + def test_sync_local_class_resolves_the_predicted_label_to_its_index(self): + app = self._app() + sync = _callback(app, "local-class-selector.value") + self.assertEqual(sync(_make_local_panel_datapoint(label="pos")), LABEL_NAMES.index("pos")) + + def test_update_sentence_highlight_prevents_update_without_datapoint_or_class(self): + from dash.exceptions import PreventUpdate + + app = self._app() + update = _callback(app, "sentence-highlight.children") + with self.assertRaises(PreventUpdate): + update(None, 0) + with self.assertRaises(PreventUpdate): + update(_make_local_panel_datapoint(), None) + + def test_update_sentence_highlight_renders_for_a_valid_datapoint(self): + from dash import html + + app = self._app() + update = _callback(app, "sentence-highlight.children") + result = update(_make_local_panel_datapoint(), 1) + self.assertIsInstance(result, html.Div) diff --git a/tests/unit_tests/webapp/nlp_components/test_similar_examples.py b/tests/unit_tests/webapp/nlp_components/test_similar_examples.py new file mode 100644 index 00000000..3112f316 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_similar_examples.py @@ -0,0 +1,159 @@ +"""Unit tests for shapash.webapp.nlp_components.similar_examples: SimilarExamplesComponent.""" + +import unittest + +from shapash.compute.retrieval.similar_examples import Neighbor + +from tests.unit_tests.webapp.nlp_components._shared import FakeEngine, _collect_ids, _ctx + + +class TestSimilarComponent(unittest.TestCase): + """Exercise the Similar Examples component's renderer and callbacks directly.""" + + @staticmethod + def _register(): + import dash + + from shapash.webapp.nlp_components import SimilarExamplesComponent + + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + explanation = engine.to_explanation() + app = dash.Dash(__name__) + comp = SimilarExamplesComponent() + comp.register_callbacks( + app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} + ) + return app, engine + + @staticmethod + def _callback(app, out_substr): + # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_layout_shows_layer_caption(self): + from shapash.webapp.nlp_components import SimilarExamplesComponent + + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + found = set() + _collect_ids(SimilarExamplesComponent().layout(_ctx(engine.to_explanation(), engine)), found) + self.assertIn("similar-topk", found) + self.assertIn("similar-threshold", found) + self.assertIn("similar-mode", found) + self.assertIn("similar-results", found) + + def test_update_similar_returns_table_and_texts(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, "topk", 5, 0.95) + self.assertEqual(texts, ["i am joyful", "this is awful"]) + self.assertIsNotNone(children) + + def test_update_similar_ignores_empty_text(self): + from dash.exceptions import PreventUpdate + + app, _ = self._register() + update = self._callback(app, "similar-results") + with self.assertRaises(PreventUpdate): + update({"text": " "}, "topk", 5, 0.95) + + def test_update_similar_threshold_mode_filters_and_reports_total(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.90) + # Only the two neighbours scoring above 0.90 clear the threshold (see FakeEngine.find_similar_threshold). + self.assertEqual(texts, ["i am joyful", "so glad today"]) + self.assertIsNotNone(children) + + def test_update_similar_threshold_mode_empty_above_cutoff(self): + app, _ = self._register() + update = self._callback(app, "similar-results") + children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.999) + self.assertEqual(texts, []) + self.assertIsNotNone(children) + + def test_toggle_mode_inputs_disables_the_inactive_control(self): + app, _ = self._register() + toggle = self._callback(app, "similar-topk.disabled") + topk_disabled, threshold_disabled = toggle("threshold") + self.assertTrue(topk_disabled) + self.assertFalse(threshold_disabled) + topk_disabled, threshold_disabled = toggle("topk") + self.assertFalse(topk_disabled) + self.assertTrue(threshold_disabled) + + def test_inspect_makes_neighbor_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import similar_examples as mod + + app, _ = self._register() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "similar-apply", "index": 1} + dp = inspect([1, 1], ["first neighbor", "second neighbor"]) + self.assertEqual(dp["text"], "second neighbor") + self.assertEqual(dp["label"], "pos") # FakeEngine.explain_text returns "pos" + + def test_neighbors_table_marks_matching_label(self): + from shapash.webapp.nlp_components.similar_examples import _neighbors_table + + neighbors = [ + Neighbor(index=0, score=0.9, text="joyful one", label="pos"), + Neighbor(index=1, score=0.5, text="grim one", label="neg"), + ] + table = _neighbors_table(neighbors, predicted_label="pos", component_id="similar") + ids = set() + _collect_ids(table, ids) + # One Inspect button per neighbour (pattern-matching ids are dicts, so assert via the count). + self.assertEqual(sum(1 for n in neighbors), 2) + self.assertIsNotNone(table) + + def test_neighbors_table_without_labels(self): + from shapash.webapp.nlp_components.similar_examples import _neighbors_table + + neighbors = [Neighbor(index=0, score=0.9, text="some text", label=None)] + table = _neighbors_table(neighbors, predicted_label=None, component_id="similar") + self.assertIsNotNone(table) + + def test_match_rate_caption_reports_share_of_predicted_label(self): + from shapash.webapp.nlp_components.similar_examples import _match_rate_caption + + neighbors = [ + Neighbor(index=0, score=0.9, text="a", label="pos"), + Neighbor(index=1, score=0.8, text="b", label="pos"), + Neighbor(index=2, score=0.7, text="c", label="neg"), + Neighbor(index=3, score=0.6, text="d", label="neg"), + ] + caption = _match_rate_caption(neighbors, predicted_label="pos") + self.assertIn("2/4", caption) + self.assertIn("50%", caption) + + def test_match_rate_caption_none_without_a_prediction_or_labels(self): + from shapash.webapp.nlp_components.similar_examples import _match_rate_caption + + neighbors = [Neighbor(index=0, score=0.9, text="a", label="pos")] + self.assertIsNone(_match_rate_caption(neighbors, predicted_label=None)) + unlabelled = [Neighbor(index=0, score=0.9, text="a", label=None)] + self.assertIsNone(_match_rate_caption(unlabelled, predicted_label="pos")) + + def test_render_results_includes_cap_note_only_when_capped(self): + from shapash.webapp.nlp_components.similar_examples import _render_results + + neighbors = [Neighbor(index=0, score=0.99, text="a", label="pos")] + capped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=5) + uncapped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=None) + + def _flat_text(node): + children = getattr(node, "children", None) + if isinstance(children, str): + return children + if isinstance(children, list): + return "".join(_flat_text(c) for c in children if c is not None) + return _flat_text(children) if children is not None else "" + + self.assertIn("Showing 1 of 5", _flat_text(capped)) + self.assertNotIn("Showing", _flat_text(uncapped)) diff --git a/tests/unit_tests/webapp/nlp_components/test_waterfall.py b/tests/unit_tests/webapp/nlp_components/test_waterfall.py new file mode 100644 index 00000000..14e57e05 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_waterfall.py @@ -0,0 +1,45 @@ +"""Unit tests for shapash.webapp.nlp_components.waterfall: WaterfallComponent.""" + +import unittest + +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import ( + LABEL_NAMES, + _callback, + _make_local_panel_datapoint, + _make_local_panel_explanation, +) + + +class TestWaterfallComponentCallbacks(unittest.TestCase): + """The Waterfall panel's own callback: current datapoint + class + threshold -> figure.""" + + def _app(self): + return NlpWebApp(_make_local_panel_explanation(), engine=None) + + def test_update_waterfall_prevents_update_without_datapoint_or_class(self): + from dash.exceptions import PreventUpdate + + app = self._app() + update = _callback(app, "waterfall-graph.figure") + with self.assertRaises(PreventUpdate): + update(None, 0, 10) + with self.assertRaises(PreventUpdate): + update(_make_local_panel_datapoint(), None, 10) + + def test_update_waterfall_renders_and_titles_by_class(self): + import plotly.graph_objs as go + + app = self._app() + update = _callback(app, "waterfall-graph.figure") + fig = update(_make_local_panel_datapoint(), 1, 10) + self.assertIsInstance(fig, go.Figure) + self.assertIn(LABEL_NAMES[1], fig.layout.title.text) + + def test_update_waterfall_defaults_the_threshold_when_missing(self): + app = self._app() + update = _callback(app, "waterfall-graph.figure") + # None threshold (e.g. before the slider ever fires) must not raise — falls back to 10%. + fig = update(_make_local_panel_datapoint(), 0, None) + self.assertIsNotNone(fig) diff --git a/tests/unit_tests/webapp/nlp_components/test_word_importance.py b/tests/unit_tests/webapp/nlp_components/test_word_importance.py new file mode 100644 index 00000000..627a3379 --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_word_importance.py @@ -0,0 +1,331 @@ +"""Unit tests for shapash.webapp.nlp_components.word_importance: the global Word Importance panel.""" + +import unittest + +import numpy as np +import pandas as pd + +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.webapp.nlp_app import NlpWebApp + +from tests.unit_tests.webapp.nlp_components._shared import LABEL_NAMES, _callback, _callback_binding_ids, _collect_ids + + +class TestGlobalWordImportancePanel(unittest.TestCase): + """The shell's Word Importance chart: rank-by, frequency floor, and its empty states.""" + + @staticmethod + def _explanation(): + # "rare" pulls hard once; "common" pulls mildly four times. The two rank-by modes must + # disagree, which is the whole reason the control exists. + token_strings = [["rare", "common"], ["common"], ["common", "common"], ["mild"]] + values = [ + np.array([[0.9, -0.9], [0.3, -0.3]]), + np.array([[0.3, -0.3]]), + np.array([[0.3, -0.3], [0.3, -0.3]]), + np.array([[-0.5, 0.5]]), + ] + texts = pd.Series(["rare common", "common", "common common", "mild"]) + explanation = NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["neg"] * 4, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + return explanation + + @classmethod + def _app(cls): + app = NlpWebApp(cls._explanation(), engine=None) + for key, spec in app.app.callback_map.items(): + if "global-importance-graph.figure" in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError("global-importance-graph") + + @staticmethod + def _words(fig): + # Bars are drawn bottom-to-top, so reverse back into rank order. + return list(fig.data[0].y)[::-1] + + def setUp(self): + self.graph = self._app() + + def _call( + self, rank_by="mean", floor=1, sign="all", topk=10, indices=None, cell=None, errors=False, label=0, words=None + ): + return self.graph(label, topk, sign, [], rank_by, floor, indices, cell, errors, words) + + @staticmethod + def _values(fig): + return list(fig.data[0].x)[::-1] + + def test_mean_and_sum_produce_different_rankings(self): + self.assertEqual(self._words(self._call(rank_by="mean"))[0], "rare") + self.assertEqual(self._words(self._call(rank_by="sum"))[0], "common") + + def test_axis_names_the_statistic_drawn(self): + self.assertEqual(self._call(rank_by="mean").layout.xaxis.title.text, "Mean SHAP contribution") + self.assertEqual(self._call(rank_by="sum").layout.xaxis.title.text, "Total SHAP contribution") + + def test_frequency_floor_removes_rare_words(self): + self.assertIn("rare", self._words(self._call(floor=1))) + self.assertNotIn("rare", self._words(self._call(floor=2))) + + def test_panel_has_no_title(self): + # The tab is already labelled "Word Importance" and the floor/class are visible in the + # filter row above the chart, so the figure itself carries no title band — reclaiming that + # vertical space for word rows instead. + self.assertIsNone(self._call(floor=2).layout.title.text) + self.assertIsNone(self._call(floor=1).layout.title.text) + + def test_sign_filter_still_works_in_both_modes(self): + for rank_by in ("mean", "sum"): + self.assertEqual(self._words(self._call(rank_by=rank_by, sign="negative")), ["mild"]) + self.assertNotIn("mild", self._words(self._call(rank_by=rank_by, sign="positive"))) + + def test_default_floor_is_two(self): + from shapash.webapp.nlp_components.word_importance import _DEFAULT_MIN_OCCURRENCES + + # A mean over a single observation is not a mean; the panel must not open on one. + self.assertEqual(_DEFAULT_MIN_OCCURRENCES, 2) + + def test_cleared_floor_input_means_no_filter(self): + # An emptied number box arrives as None and must not silently restore the default. + self.assertIn("rare", self._words(self._call(floor=None))) + + def test_floor_below_one_is_clamped(self): + self.assertIn("rare", self._words(self._call(floor=0))) + + def test_impossible_floor_explains_itself(self): + fig = self._call(floor=99) + self.assertEqual(len(fig.data), 0) + self.assertIn("at least 99 time(s) in these 4 sample(s)", fig.layout.annotations[0].text) + + def test_empty_sign_filter_explains_itself_differently(self): + # Only "mild" is negative; excluding it by scope leaves the sign filter with nothing, and + # the message must point at the filter rather than at the frequency floor. + fig = self._call(sign="negative", indices=[0, 1]) + self.assertEqual(len(fig.data), 0) + self.assertIn("negative", fig.layout.annotations[0].text) + + def test_floor_counts_within_the_selection(self): + # "common" occurs 4x overall but once in sample 1, so a floor of 2 must exclude it there. + fig = self._call(floor=2, indices=[1]) + self.assertEqual(len(fig.data), 0) + self.assertIn("in these 1 sample(s)", fig.layout.annotations[0].text) + + def test_hover_carries_the_occurrence_count(self): + fig = self._call(floor=1) + drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) + self.assertEqual(drawn, {"rare": 1, "common": 4, "mild": 1}) + self.assertIn("Occurrences:", fig.data[0].hovertemplate) + + def test_hover_counts_follow_the_selection(self): + # A count that ignored the scope would contradict the floor applied right beside it: + # "common" occurs 4x in the corpus but twice in samples 0-1. + fig = self._call(floor=1, indices=[0, 1]) + drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) + self.assertEqual(drawn["common"], 2) + + def test_all_classes_ranks_on_the_strongest_class_magnitude(self): + # "mild" pulls -0.5 on class 0 and +0.5 on class 1: across classes it is a magnitude of + # 0.5, not the -0.5 the single-class view shows. + fig = self._call(label="all") + drawn = dict(zip(self._words(fig), self._values(fig))) + self.assertEqual(drawn, {"rare": 0.9, "mild": 0.5, "common": 0.3}) + self.assertEqual(dict(zip(self._words(self._call()), self._values(self._call())))["mild"], -0.5) + + def test_all_classes_names_its_statistic_on_the_axis(self): + self.assertEqual(self._call(label="all").layout.xaxis.title.text, "Largest |mean SHAP| across classes") + self.assertEqual( + self._call(label="all", rank_by="sum").layout.xaxis.title.text, + "Largest |total SHAP| across classes", + ) + + def test_all_classes_ignores_a_stale_sign_filter(self): + # Every value is a magnitude, so honouring "negative" would blank the chart on a control + # the user cannot even see is active. + self.assertEqual(self._words(self._call(label="all", sign="negative")), ["rare", "mild", "common"]) + + def test_all_classes_still_honours_the_frequency_floor(self): + self.assertNotIn("rare", self._words(self._call(label="all", floor=2))) + + def test_sign_filter_is_greyed_out_under_all_classes(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = None + for key, spec in app.app.callback_map.items(): + if "sign-filter.options" in key: + fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) + self.assertIsNotNone(fn, "sign-filter gating callback not registered") + options, value = fn("all") + self.assertEqual([o["value"] for o in options if o.get("disabled")], ["positive", "negative"]) + self.assertEqual(value, "all") + options, _ = fn(0) + self.assertFalse(any(o.get("disabled") for o in options)) + + def test_scatter_colouring_survives_all_classes(self): + # The scatter reads the same dropdown, where int("all") would raise. + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) + fn = None + for key, spec in app.app.callback_map.items(): + if "scatter-plot.figure" in key: + fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) + self.assertIsNotNone(fn, "scatter callback not registered") + self.assertIsNotNone(fn("word_contribution", ["common"], "all", False)) + + def test_number_boxes_commit_without_needing_a_blur(self): + # debounce=True commits only on Enter or blur, which leaves the spinner arrows looking + # dead: clicking one keeps focus in the box, so the chart never updates. A numeric + # (seconds) debounce commits after a pause instead. + layout = NlpWebApp(self._explanation(), engine=None).app.layout + for box_id in ("min-occurrences", "topk-input"): + debounce = layout[box_id].debounce + self.assertNotIsInstance(debounce, bool, f"{box_id} commits only on blur") + self.assertGreater(debounce, 0) + + def test_topk_input_replaces_the_slider(self): + found = set() + _collect_ids(NlpWebApp(self._explanation(), engine=None).app.layout, found) + self.assertIn("topk-input", found) + self.assertNotIn("topk-slider", found) + + def test_topk_controls_the_bar_count(self): + self.assertEqual(len(self._words(self._call(topk=2))), 2) + + def test_typed_topk_is_clamped_to_the_boxs_range(self): + from shapash.webapp.nlp_components.word_importance import _MAX_TOPK, _MIN_TOPK + + # The browser enforces min/max on the spinner but not on typed input. + self.assertEqual(len(self._words(self._call(topk=999))), 3) # only 3 words exist + self.assertEqual(len(self._words(self._call(topk=0))), _MIN_TOPK) + self.assertEqual(len(self._words(self._call(topk=-5))), _MIN_TOPK) + self.assertLessEqual(_MAX_TOPK, 50) + + def test_cleared_topk_box_falls_back_to_the_default(self): + from shapash.webapp.nlp_components.word_importance import _DEFAULT_TOPK + + self.assertEqual(_DEFAULT_TOPK, 20) + # Only None (an emptied box) restores the default — a typed 0 clamps instead. + self.assertEqual(len(self._words(self._call(topk=None))), 3) # all 3 words, under the cap + + def test_chart_keeps_its_computed_height_so_labels_survive(self): + # The panel scrolls; squeezing 50 words into it is what made plotly drop word labels. + fig = self._call(topk=50) + self.assertIsNotNone(fig.layout.height) + + def test_missing_class_prevents_update(self): + from dash.exceptions import PreventUpdate + + with self.assertRaises(PreventUpdate): + self.graph(None, 10, "all", [], "mean", 1, None, None, False, None) + + def test_generic_empty_explains_itself_when_neither_sign_nor_floor_is_the_cause(self): + # Excluding every word in the corpus empties the ranking without any sign filter or floor + # active — the fallback message names neither, unlike the two more specific reasons above. + fig = self.graph(0, 10, "all", ["rare", "common", "mild"], "mean", 1, None, None, False, None) + self.assertEqual(len(fig.data), 0) + self.assertIn("No word passes these filters in these 4 sample(s)", fig.layout.annotations[0].text) + + +class TestWordImportanceClickCallbacks(unittest.TestCase): + """Word-click store sync: a bar click or the clear button, and the clear button's own visibility.""" + + @staticmethod + def _app(): + return NlpWebApp(TestGlobalWordImportancePanel._explanation(), engine=None) + + def test_bar_click_publishes_the_word_and_resets_clickdata(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + click_data = {"points": [{"y": "common"}]} + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] + words, reset = update(click_data, None) + self.assertEqual(words, ["common"]) + self.assertIsNone(reset) + + def test_clear_button_resets_the_store(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "word-filter-clear-btn.n_clicks"}] + words, reset = update(None, 1) + self.assertIsNone(words) + self.assertIsNone(reset) + + def test_empty_click_prevents_update(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + from shapash.webapp.nlp_components import word_importance as word_importance_module + + app = self._app() + update = _callback(app, "data...global-importance-graph.clickData") + with mock.patch.object(word_importance_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] + with self.assertRaises(PreventUpdate): + update({"points": []}, None) + with self.assertRaises(PreventUpdate): + update(None, None) + + def test_toggle_clear_button_visibility(self): + app = self._app() + toggle = _callback(app, "word-filter-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle(["common"])["display"], "inline") + + +class TestWordImportanceScatterSync(unittest.TestCase): + """Word Importance and Scatter communicate only through the shared stores (Phase A extraction). + + A bar click always writes ``word-click-filter`` directly now, whether or not a scatter is + mounted (no more branching on the scatter's presence), and the scatter's own word dropdown + mirrors that store bidirectionally instead of being the "source of truth" only when it exists. + """ + + @staticmethod + def _explanation(): + return TestGlobalWordImportancePanel._explanation() + + def test_bar_click_registered_without_a_scatter(self): + app = NlpWebApp(self._explanation(), engine=None) + pairs = _callback_binding_ids(app, "global-importance-graph.clickData") + self.assertIn(("global-importance-graph", "clickData"), pairs) + self.assertIn(("word-filter-clear-btn", "n_clicks"), pairs) + + def test_bar_click_registered_with_a_scatter_too(self): + # Before Phase A, a mounted scatter changed which store a bar click targeted; now Word + # Importance always registers this callback unconditionally, regardless of scatter presence. + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) + pairs = _callback_binding_ids(app, "global-importance-graph.clickData") + self.assertIn(("global-importance-graph", "clickData"), pairs) + + def test_scatter_word_select_mirrors_the_shared_store(self): + app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) + pairs = _callback_binding_ids(app, "scatter-word-select.value") + self.assertIn(("word-click-filter", "data"), pairs) + + def test_class_selector_publishes_to_the_active_class_store(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "active-class-store.data") + self.assertEqual(fn(1), 1) + # "All classes" (the string sentinel) is not itself a class index — falls back to 0. + self.assertEqual(fn("all"), 0) diff --git a/tests/unit_tests/webapp/nlp_components/test_word_profile.py b/tests/unit_tests/webapp/nlp_components/test_word_profile.py new file mode 100644 index 00000000..ec4c797d --- /dev/null +++ b/tests/unit_tests/webapp/nlp_components/test_word_profile.py @@ -0,0 +1,370 @@ +"""Unit tests for shapash.webapp.nlp_components.word_profile: WordProfileComponent.""" + +import unittest +from dataclasses import replace + +import numpy as np +import pandas as pd + +from shapash.explainer.nlp_explanation import NlpExplanation + +from tests.unit_tests.webapp.nlp_components._shared import LABEL_NAMES, _collect_ids, _ctx + + +class TestWordProfileComponent(unittest.TestCase): + """The single-word profile panel: word × aggregation × global selection, then drill-down.""" + + STORES = { + "current": "current-datapoint", + "selection": "scatter-selected-indices", + "error_cell": "error-cell", + "errors_only": "errors-only-switch", + "word_click": "word-click-filter", + } + + @staticmethod + def _explanation(): + """Four samples, two classes. 'happy' pulls +0.4/+0.3 in two, -0.6 in a third.""" + token_strings = [ + ["so", "happy", "today"], + ["happy", "and", "happy"], + ["not", "happy", "at", "all"], + ["nothing", "here"], + ] + values = [ + np.array([[0.1, -0.1], [0.4, -0.4], [0.05, -0.05]]), + np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1]]), + np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), + np.array([[0.0, 0.0], [0.0, 0.0]]), + ] + texts = pd.Series(["so happy today", "happy and happy", "not happy at all", "nothing here"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), + y_prob=None, + # Sample 1 is the only model error, so errors-only scopes to exactly that row. + y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), + label_names=LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def _app(self): + import dash + + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + app = dash.Dash(__name__) + comp = WordProfileComponent() + comp.register_callbacks(app, _ctx(explanation, None), self.STORES) + return app, explanation + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + # ── mounting / layout ─────────────────────────────────────────────── + def test_mounts_without_an_engine(self): + from shapash.webapp.nlp_components import WordProfileComponent + + # Data-only panel: it must survive a loaded snapshot with no live model. + self.assertTrue(WordProfileComponent.is_available(_ctx(self._explanation(), None))) + + def test_layout_declares_every_id_its_callbacks_bind(self): + from shapash.webapp.nlp_components import WordProfileComponent + + found = set() + _collect_ids(WordProfileComponent().layout(_ctx(self._explanation(), None)), found) + for suffix in ("select", "agg", "class", "order", "limit", "graph", "caption", "results", "store"): + self.assertIn(f"word-profile-{suffix}", found) + + def test_word_dropdown_is_seeded_with_the_top_word(self): + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + layout = WordProfileComponent().layout(_ctx(explanation, None)) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + # Not empty on open, and the seed is a real corpus word. + self.assertIn(found["word-profile-select"].value, explanation.vocabulary()) + + def test_class_picker_hidden_for_a_single_output_column(self): + from shapash.webapp.nlp_components import WordProfileComponent + + explanation = self._explanation() + binary = replace( + explanation, + values=[v[:, 0] for v in explanation.values], + base_values=np.zeros(4), + label_names=["score"], + ) + layout = WordProfileComponent().layout(_ctx(binary, None)) + found = set() + _collect_ids(layout, found) + # Still present (callbacks bind it), just not shown. + self.assertIn("word-profile-class", found) + + # ── the profile callback ──────────────────────────────────────────── + def test_profile_reports_counts_and_one_bar_per_class(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "strongest", 10, None, None, False + ) + self.assertEqual(len(fig.data[0].x), 2) + self.assertIn("4 occurrence(s) in 3 of 4 sample(s)", caption) + self.assertIsNotNone(table) + self.assertEqual(store, [2, 0, 1]) # |-0.6| > 0.4 > 0.3 + + def test_mean_reports_its_error_bars_in_the_caption(self): + app, _ = self._app() + _, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 10, None, None, False) + self.assertIn("std across occurrences", caption) + + def test_sum_has_no_error_bars(self): + app, _ = self._app() + fig, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "sum", 0, "most", 10, None, None, False) + self.assertIsNone(fig.data[0].error_x.array) + self.assertNotIn("std", caption) + + def test_absolute_aggregation_surfaces_the_two_way_word(self): + app, _ = self._app() + graph = self._callback(app, "word-profile-graph") + signed = graph("happy", "mean", 0, "most", 10, None, None, False)[0].data[0].x + magnitude = graph("happy", "mean_abs", 0, "most", 10, None, None, False)[0].data[0].x + # The signed mean nearly cancels; the magnitude does not. This gap is the panel's point. + self.assertLess(abs(signed[-1]), 0.05) + self.assertGreater(magnitude[-1], 0.3) + + def test_order_reorders_the_drill_down_only(self): + app, _ = self._app() + graph = self._callback(app, "word-profile-graph") + self.assertEqual(graph("happy", "mean", 0, "most", 10, None, None, False)[3], [0, 1, 2]) + self.assertEqual(graph("happy", "mean", 0, "least", 10, None, None, False)[3][0], 2) + + def test_limit_truncates_the_drill_down(self): + app, _ = self._app() + store = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 1, None, None, False)[3] + self.assertEqual(len(store), 1) + + def test_scatter_selection_scopes_the_aggregate(self): + app, _ = self._app() + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, [0, 3], None, False + ) + self.assertIn("scoped to 2 selected sample(s)", caption) + self.assertEqual(store, [0]) + + def test_errors_only_scopes_the_aggregate(self): + app, _ = self._app() + # Sample 1 is the only misclassified row, and it does contain the word. + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, None, None, True + ) + self.assertIn("scoped to 1 selected sample(s)", caption) + self.assertEqual(store, [1]) + + def test_confusion_cell_scopes_the_aggregate(self): + app, _ = self._app() + _, caption, _, _ = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, None, {"pred": 1, "true": 0, "indices": [2]}, False + ) + self.assertIn("scoped to 1 selected sample(s)", caption) + + def test_word_absent_from_the_scope_says_so(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + "happy", "mean", 0, "most", 10, [3], None, False + ) + self.assertIn("no occurrences", caption) + self.assertIn("does not occur", fig.layout.annotations[0].text) + self.assertIsNone(table) + self.assertEqual(store, []) + + def test_no_word_selected_shows_a_prompt(self): + app, _ = self._app() + fig, caption, table, store = self._callback(app, "word-profile-graph")( + None, "mean", 0, "most", 10, None, None, False + ) + self.assertIn("Pick a word", fig.layout.annotations[0].text) + self.assertEqual((caption, table, store), ("", None, [])) + + def test_falls_back_to_defaults_on_cleared_controls(self): + app, _ = self._app() + _, caption, _, store = self._callback(app, "word-profile-graph")( + "happy", None, None, None, None, None, None, None + ) + self.assertIn("Mean", caption) + self.assertEqual(len(store), 3) + + # ── cross-panel wiring ────────────────────────────────────────────── + def test_follows_a_word_bar_clicked_in_the_importance_panel(self): + app, _ = self._app() + follow = self._callback(app, "word-profile-select.value") + self.assertEqual(follow("today"), "today") + # With a scatter the store holds the multi-select list; the newest word wins. + self.assertEqual(follow(["today", "happy"]), "happy") + + def test_cleared_word_filter_leaves_the_selection_alone(self): + from dash.exceptions import PreventUpdate + + app, _ = self._app() + follow = self._callback(app, "word-profile-select.value") + for cleared in (None, []): + with self.assertRaises(PreventUpdate): + follow(cleared) + + def test_inspect_packs_the_sample_into_the_current_datapoint(self): + from unittest import mock + + from shapash.webapp.nlp_components import word_profile as mod + + app, _ = self._app() + inspect = self._callback(app, "current-datapoint") + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "word-profile-inspect", "index": 1} + # The store holds the displayed rows' positions, so row 1 is sample 0. + datapoint = inspect([None, 1], [2, 0]) + self.assertEqual(datapoint["orig_idx"], 0) + self.assertEqual(datapoint["text"], "so happy today") + self.assertEqual(datapoint["label"], "pos") + + def test_inspect_ignores_clicks_it_cannot_resolve(self): + from unittest import mock + + from dash.exceptions import PreventUpdate + + from shapash.webapp.nlp_components import word_profile as mod + + app, _ = self._app() + inspect = self._callback(app, "current-datapoint") + with self.assertRaises(PreventUpdate): + inspect([None, None], [0, 1]) # no button actually clicked + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = None + with self.assertRaises(PreventUpdate): + inspect([1], [0]) + with mock.patch.object(mod, "callback_context") as cc: + cc.triggered_id = {"type": "word-profile-inspect", "index": 5} + with self.assertRaises(PreventUpdate): + inspect([1], [0]) # index past the end of the displayed rows + + +class TestWordProfileControls(unittest.TestCase): + """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" + + def _layout(self): + from shapash.webapp.nlp_components import WordProfileComponent + + comp = WordProfileComponent() + layout = comp.layout(_ctx(TestWordProfileComponent._explanation(), None)) + found = {} + + def walk(node): + cid = getattr(node, "id", None) + if isinstance(cid, str): + found[cid] = node + children = getattr(node, "children", None) + for ch in children if isinstance(children, (list, tuple)) else [children]: + if ch is not None and not isinstance(ch, str): + walk(ch) + + walk(layout) + return comp, found + + def _app(self): + import dash + + from shapash.webapp.nlp_components import WordProfileComponent + + comp = WordProfileComponent() + explanation = TestWordProfileComponent._explanation() + comp.layout(_ctx(explanation, None)) # builds the option lists the sort callback serves + app = dash.Dash(__name__) + comp.register_callbacks(app, _ctx(explanation, None), TestWordProfileComponent.STORES) + return app + + @staticmethod + def _callback(app, out_substr): + for key, spec in app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_sum_aggregations_are_not_offered(self): + # Per word, a sum is the mean rescaled by the occurrence count — same bars, no information. + _, found = self._layout() + self.assertEqual([o["value"] for o in found["word-profile-agg"].options], ["mean", "mean_abs"]) + + def test_options_carry_the_occurrence_count(self): + _, found = self._layout() + labels = {o["value"]: o["label"] for o in found["word-profile-select"].options} + self.assertEqual(labels["happy"], "happy (4)") + + def test_option_values_stay_bare_words(self): + # The label carries the count; the value must not, or every consumer of it breaks. + _, found = self._layout() + self.assertIn("happy", [o["value"] for o in found["word-profile-select"].options]) + + def test_sort_toggle_reorders_without_changing_values(self): + app = self._app() + reorder = self._callback(app, "word-profile-select.options") + alpha = [o["value"] for o in reorder("alpha")] + freq = [o["value"] for o in reorder("frequency")] + self.assertEqual(alpha, sorted(alpha)) + self.assertEqual(freq[0], "happy") # the most frequent word + self.assertEqual(set(alpha), set(freq)) + + def test_unknown_sort_falls_back_to_alphabetical(self): + app = self._app() + reorder = self._callback(app, "word-profile-select.options") + self.assertEqual(reorder(None), reorder("alpha")) + + def test_bar_click_sets_the_drill_down_class(self): + app = self._app() + click = self._callback(app, "word-profile-class.value") + value, reset = click({"points": [{"customdata": [1], "y": "pos"}]}) + self.assertEqual(value, 1) + # clickData is reset so clicking the same bar twice re-fires. + self.assertIsNone(reset) + + def test_bar_click_reads_the_index_not_the_label(self): + app = self._app() + click = self._callback(app, "word-profile-class.value") + # Duplicate display names must not be able to mis-resolve the class. + self.assertEqual(click({"points": [{"customdata": [0], "y": "neg"}]})[0], 0) + + def test_graph_carries_the_class_index_on_every_bar(self): + app = self._app() + fig = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 5, None, None, False)[0] + self.assertEqual([c[0] for c in fig.data[0].customdata], [1, 0]) # reversed for drawing + + def test_empty_clicks_are_ignored(self): + from dash.exceptions import PreventUpdate + + app = self._app() + click = self._callback(app, "word-profile-class.value") + for bad in (None, {}, {"points": []}, {"points": [{}]}): + with self.assertRaises(PreventUpdate): + click(bad) diff --git a/tests/unit_tests/webapp/test_nlp_app.py b/tests/unit_tests/webapp/test_nlp_app.py new file mode 100644 index 00000000..72b35c47 --- /dev/null +++ b/tests/unit_tests/webapp/test_nlp_app.py @@ -0,0 +1,910 @@ +"""Unit tests for ``shapash.webapp.nlp_app``: the app shell — three-panel layout, capability-gated +What-if Lab mounting, subpath mounting, table/grid filtering, and construction/run wiring. +""" + +import unittest +from unittest.mock import MagicMock, PropertyMock, patch + +import numpy as np +import pandas as pd +import plotly.graph_objs as go +from dash import jupyter_dash +from dash.exceptions import PreventUpdate + +from shapash.compute.embeddings import Embedding +from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.webapp.nlp_app import NlpWebApp +from shapash.webapp.nlp_components import ( + CounterfactualComponent, + DataEditorComponent, + LabelNoiseComponent, + SimilarExamplesComponent, +) +from shapash.webapp.nlp_components import error_analysis as error_analysis_module +from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click +from shapash.webapp.utils.launch import RunningApp + +from tests.unit_tests.webapp.nlp_components._shared import ( + FakeEngine, + _PROBE_CORPUS, + _callback, + _callback_binding_ids, + _collect_ids, + _ctx, +) +from tests.unit_tests.webapp.nlp_components._shared import LABEL_NAMES as _WHATIF_LABEL_NAMES + +# The 6-class label set used by the TestNlpWebApp / TestNlpWebAppRun fixtures below (moved here +# from tests/unit_tests/nlp/test_nlp_prototype.py). This intentionally shadows, for this module +# only, the 2-class ``LABEL_NAMES`` used by the What-if Lab classes further down (imported above +# as ``_WHATIF_LABEL_NAMES``) — the two source files each defined their own module-level +# ``LABEL_NAMES`` with different values, which collide once both land in one file. See the +# migration report for details. +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + + +def _make_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: + """Synthetic per-token contribution data for 3 samples, 6 classes.""" + rng = np.random.default_rng(42) + token_strings = [ + ["", "i", "feel", "so", "happy", "today", ""], + ["", "this", "is", "terrible", "and", "sad", ""], + ["", "what", "a", "wonderful", "day", ""], + ] + values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] + base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) + return token_strings, values, base_values + + +def _make_texts() -> pd.Series: + return pd.Series( + ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], + index=pd.RangeIndex(3), + ) + + +def _make_explanation( + texts: pd.Series | None = None, + y_pred: pd.Series | None = None, + y_prob: pd.DataFrame | None = None, + y_true: pd.Series | None = None, + label_names: list[str] | None = LABEL_NAMES, + backend_name: str = "nlp_shap", + is_additive: bool = True, + reference_kind: str = "none", + output_space: str = "probability", +) -> NlpExplanation: + """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" + texts = _make_texts() if texts is None else texts + token_strings, values, base_values = _make_token_data() + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=( + pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred + ), + y_prob=y_prob, + y_true=y_true, + label_names=label_names, + folds_case=None, + backend_name=backend_name, + is_additive=is_additive, + reference_kind=reference_kind, + output_space=output_space, + ) + + +def _make_explainer() -> NlpExplainer: + """Bare ``NlpExplainer`` (engine role only), bypassing ``__init__`` to avoid ``shap.Explainer(None)``. + + Per ``fit``/``explain``, the explainer never holds compiled results — tests that need a computed + batch build an :class:`NlpExplanation` directly with :func:`_make_explanation` instead. + """ + xpl = object.__new__(NlpExplainer) + xpl.model = None + xpl.label_names = LABEL_NAMES + xpl.backend = None + return xpl + + +class TestWhatIfMounting(unittest.TestCase): + def _ids(self, engine): + app = NlpWebApp(engine.to_explanation(), engine=engine) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + @staticmethod + def _whatif_components(app): + # app._components also holds the always-on core panels (Sentence Highlight, Waterfall); + # scope these assertions to the capability-gated What-if Lab ones only. + gated = (DataEditorComponent, CounterfactualComponent, SimilarExamplesComponent) + return [c for c in app._components if isinstance(c, gated)] + + def test_full_capabilities_mount_both(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("data-editor-input", ids) + self.assertIn("data-editor-predict-btn", ids) + self.assertIn("counterfactual-generate-btn", ids) + self.assertEqual(len(self._whatif_components(app)), 2) + + def test_counterfactual_config_controls_in_initial_layout(self): + # The generate callback's State references these ids, so they must exist in the initial + # layout (rendered per generator from cf_config_spec), not be injected by a later callback. + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + for name in ("num_examples", "max_flips", "tokens_to_ignore"): + self.assertIn(f"counterfactual-cfg-hotflip-{name}", ids) + for name in ("num_examples", "max_ablations", "tokens_to_ignore"): + self.assertIn(f"counterfactual-cfg-ablation_flip-{name}", ids) + + def test_counterfactual_method_selector_present_with_multiple_generators(self): + # A method selector and one visibility-toggled control group per generator are in the layout. + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("counterfactual-generator", ids) + self.assertIn("counterfactual-cfg-group-hotflip", ids) + self.assertIn("counterfactual-cfg-group-ablation_flip", ids) + + def test_selector_toggle_callback_registered_with_multiple_generators(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("counterfactual-cfg-group-hotflip.style", outputs) + + def test_edit_only_mounts_editor_not_counterfactual(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) + self.assertIn("data-editor-input", ids) + self.assertNotIn("counterfactual-generate-btn", ids) + self.assertEqual(len(self._whatif_components(app)), 1) + + def test_no_capabilities_hides_whatif_lab(self): + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertNotIn("data-editor-input", ids) + self.assertNotIn("counterfactual-generate-btn", ids) + self.assertNotIn("whatif-apply-store", ids) + self.assertEqual(self._whatif_components(app), []) + + def test_callbacks_registered_when_mounted(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("data-editor-prob.figure", outputs) + self.assertIn("counterfactual-results.children", outputs) + + def test_similar_panel_mounts_when_capable(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False, can_similar=True)) + self.assertIn("similar-topk", ids) + self.assertIn("similar-results", ids) + self.assertTrue(any(isinstance(c, SimilarExamplesComponent) for c in app._components)) + + def test_similar_panel_hidden_without_capability(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, can_similar=False)) + self.assertNotIn("similar-topk", ids) + self.assertNotIn("similar-results", ids) + + def test_similar_panel_requires_predict(self): + # CAP_SIMILAR alone is not enough — the Inspect flow needs predict (explain_text) too. + _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, can_similar=True)) + self.assertNotIn("similar-topk", ids) + + def test_similar_callbacks_registered_when_mounted(self): + engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("similar-results.children", outputs) + + +class TestThreePanelLayout(unittest.TestCase): + """The LIT-style three-panel shell: tab groups, mounted bodies, and the current-datapoint store.""" + + def _ids(self, engine, **kwargs): + app = NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + def test_full_tab_groups_when_all_panels_available(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) + self.assertEqual( + app._tab_groups, + { + "left-tabs": ["table", "scatter", "editor"], + # Word Profile is data-only, so it mounts unconditionally beside Word Importance. + "upper-right-tabs": ["importance", "word-profile", "counterfactual"], + "lower-right-tabs": ["highlight", "waterfall"], + }, + ) + # Every tab body is mounted in the DOM (visibility is toggled, not the mount). + for body_id in ( + "left-tabs-body-table", + "left-tabs-body-scatter", + "left-tabs-body-editor", + "upper-right-tabs-body-importance", + "upper-right-tabs-body-counterfactual", + "lower-right-tabs-body-highlight", + "lower-right-tabs-body-waterfall", + ): + self.assertIn(body_id, ids) + + def test_tabs_degrade_without_scatter_or_whatif(self): + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertEqual(app._tab_groups["left-tabs"], ["table"]) + self.assertEqual(app._tab_groups["upper-right-tabs"], ["importance", "word-profile"]) + self.assertEqual(app._tab_groups["lower-right-tabs"], ["highlight", "waterfall"]) + self.assertNotIn("left-tabs-body-scatter", ids) + self.assertNotIn("left-tabs-body-editor", ids) + self.assertNotIn("upper-right-tabs-body-counterfactual", ids) + + def test_counterfactual_tab_only_with_editor(self): + # Editor without CF: editor tab present on the left, no counterfactual tab on the right. + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) + self.assertIn("editor", app._tab_groups["left-tabs"]) + self.assertNotIn("counterfactual", app._tab_groups["upper-right-tabs"]) + + def test_selection_bar_present(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) + self.assertIn("selection-summary", ids) + self.assertIn("word-filter-clear-btn", ids) + self.assertIn("scatter-clear-btn", ids) + + def test_scatter_clear_absent_without_scatter(self): + _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertIn("word-filter-clear-btn", ids) + self.assertNotIn("scatter-clear-btn", ids) + + def test_current_datapoint_store_present(self): + _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) + self.assertIn("current-datapoint", ids) + + def test_detail_panels_read_current_datapoint(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + # Highlight and waterfall render off the shared primary-selection store... + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "sentence-highlight.children")) + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "waterfall-graph.figure")) + # ...and counterfactuals generate from it too (so a selected row works, not only editor text). + self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "counterfactual-results.children")) + + def test_current_datapoint_written_by_row_and_editor(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + # The table-selection writer keys off the selected row. + self.assertIn(("dataset-table", "selectedRows"), _callback_binding_ids(app, "current-datapoint.data")) + # The editor's Predict also writes it (allow_duplicate) — its combined key carries the prob figure. + editor_key = next(k for k in app.app.callback_map if "data-editor-prob.figure" in k) + self.assertIn("current-datapoint.data", editor_key) + + def test_tab_toggle_callbacks_registered(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine, projection=np.zeros((2, 2))) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("left-tabs-body-table.style", outputs) + self.assertIn("lower-right-tabs-body-waterfall.style", outputs) + + +class TestReadContractSeam(unittest.TestCase): + """The app shell reads compiled data only from the ``NlpExplanation``; no raw explainer handle. + + ``FakeEngine`` has no ``explainer`` attribute, so a mounted app proves the read path is served by + the artifact alone. These asserts lock the seam so a future edit cannot silently reintroduce a + ``self.explainer`` bypass, nor let display state accumulate back onto the artifact. + """ + + def test_app_holds_no_raw_explainer_handle(self): + engine = FakeEngine(can_edit=True, can_cf=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + self.assertFalse(hasattr(app, "explainer")) + + def test_app_exposes_explanation_and_engine_roles(self): + engine = FakeEngine(can_edit=True, can_cf=True) + explanation = engine.to_explanation() + app = NlpWebApp(explanation, engine=engine) + self.assertIs(app._explanation, explanation) # read contract: the artifact itself + self.assertIs(app._engine, engine) # live-action contract + + +class TestLabelNoiseMounting(unittest.TestCase): + """``CAP_LABELS`` is a *data* capability: it depends on the compiled batch, not on a live model.""" + + def _ids(self, engine): + app = NlpWebApp(engine.to_explanation(), engine=engine) + found = set() + _collect_ids(app.app.layout, found) + return app, found + + def test_mounts_with_ground_truth_and_per_class_probabilities(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, has_labels=True)) + self.assertIn("label-noise-detect-btn", ids) + self.assertIn("label-noise-results", ids) + self.assertIn("label-noise", app._tab_groups["upper-right-tabs"]) + self.assertTrue(any(isinstance(c, LabelNoiseComponent) for c in app._components)) + + def test_hidden_without_ground_truth(self): + app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) + self.assertNotIn("label-noise-detect-btn", ids) + self.assertNotIn("label-noise", app._tab_groups["upper-right-tabs"]) + + def test_hidden_when_only_the_winning_probability_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) + engine.y_prob = pd.DataFrame({"probability": [0.8, 0.8]}, index=pd.RangeIndex(2)) + _, ids = self._ids(engine) + self.assertNotIn("label-noise-detect-btn", ids) + + def test_mounts_without_any_live_capability(self): + # The snapshot case: no model, so no editor/counterfactual/similar panel — but the labels and + # probabilities are still in the compiled batch, so this panel stands on its own. + app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, has_labels=True)) + self.assertIn("label-noise-detect-btn", ids) + self.assertNotIn("data-editor-input", ids) + + def test_caption_warns_when_no_independent_cross_check_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True) + layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) + caption = layout.children[1].children + self.assertIn("no independent cross-check", caption) + + def test_caption_explains_the_corpus_column_when_the_probe_is_available(self): + engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, probe_corpus=_PROBE_CORPUS) + layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) + caption = layout.children[1].children + self.assertIn("Corpus column", caption) + + def test_callbacks_registered_when_mounted(self): + engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) + app = NlpWebApp(engine.to_explanation(), engine=engine) + outputs = " ".join(app.app.callback_map.keys()) + self.assertIn("label-noise-results.children", outputs) + + +class TestSubpathMounting(unittest.TestCase): + """``url_base_pathname`` — serving behind a reverse proxy that routes a subpath, not a host.""" + + def _app(self, **kwargs): + engine = FakeEngine(can_edit=True, can_cf=True) + return NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) + + def test_serves_at_the_root_by_default(self): + config = self._app().app.config + self.assertEqual(config.routes_pathname_prefix, "/") + self.assertEqual(config.requests_pathname_prefix, "/") + + def test_mounts_under_the_given_prefix(self): + config = self._app(url_base_pathname="/shapash-nlp-explainer/").app.config + # Both prefixes matter: routes_* is where Dash listens, requests_* is what it writes into + # the asset and callback URLs the browser then fetches. + self.assertEqual(config.routes_pathname_prefix, "/shapash-nlp-explainer/") + self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") + + def test_missing_slashes_are_added(self): + for given in ("shapash-nlp-explainer", "/shapash-nlp-explainer", "shapash-nlp-explainer/"): + with self.subTest(given=given): + config = self._app(url_base_pathname=given).app.config + self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") + + def test_the_served_routes_and_asset_urls_carry_the_prefix(self): + # The config values above are only half the contract: Dash must also *answer* on the prefix + # and write it into the script tags, or the page loads blank behind the proxy. + client = self._app(url_base_pathname="/shapash-nlp-explainer/").app.server.test_client() + self.assertEqual(client.get("/shapash-nlp-explainer/").status_code, 200) + self.assertEqual(client.get("/").status_code, 404) + body = client.get("/shapash-nlp-explainer/").get_data(as_text=True) + self.assertIn("/shapash-nlp-explainer/_dash-component-suites", body) + self.assertNotIn('"/_dash-', body) + + def test_empty_prefixes_serve_at_the_root(self): + for given in ("", "/", " "): + with self.subTest(given=given): + config = self._app(url_base_pathname=given).app.config + self.assertEqual(config.requests_pathname_prefix, "/") + + +class TestTableFilterIntersection(unittest.TestCase): + """The dataset table's active filters (error cell, word click, errors-only) must intersect for + real. A filter that empties the combination has to actually empty the table, not be silently + dropped while the summary still claims it is applied — see the reported bug where clicking a + Word Importance bar and then an Error Analysis cell with no matching rows kept the word filter + in the summary text while showing every row from the cell, unfiltered. + """ + + @staticmethod + def _explanation(): + texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), + label_names=_WHATIF_LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def test_word_filter_after_error_cell_narrows_instead_of_being_dropped(self): + # "happy" only occurs in row 0 ("i am happy"); the clicked cell (pred=pos, true=neg) holds + # only row 1 ("so glad"). The intersection is empty and must stay empty. + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + error_cell = {"pred": 1, "true": 0, "indices": [1]} + rows, selected, _title, summary = fn(None, ["happy"], False, error_cell, None, None) + self.assertEqual(rows, []) + self.assertEqual(selected, []) + self.assertIn('containing "happy"', summary) + self.assertIn("(0 of 4)", summary) + + def test_word_filter_matching_the_cell_still_narrows(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + error_cell = {"pred": 0, "true": 1, "indices": [3]} # row 3: "very sad" + rows, selected, _title, summary = fn(None, ["sad"], False, error_cell, None, None) + self.assertEqual([r["text"] for r in rows], ["very sad"]) + self.assertEqual(selected, rows) + self.assertIn("(1 of 4)", summary) + + def test_errors_only_narrows_a_word_filter_too(self): + # "happy" only occurs in row 0, a correct prediction — errors_only must not silently drop + # the word filter to avoid an empty table. + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + rows, _selected, _title, summary = fn(None, ["happy"], True, None, None, None) + self.assertEqual(rows, []) + self.assertIn("model errors only", summary) + self.assertIn('containing "happy"', summary) + + def test_word_filter_matches_the_exact_token_not_a_substring(self): + # "unhappy" contains "happy" as a substring but is a different token — a bar labeled + # "happy" must not pull it in, matching the exact-token semantics word_importance()/the + # scatter's word-contribution coloring already use. + texts = pd.Series(["i am happy", "so unhappy today"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + explanation = NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((2, 2)), + y_pred=pd.Series(["pos", "neg"], name="prediction"), + y_prob=None, + y_true=None, + label_names=_WHATIF_LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + app = NlpWebApp(explanation, engine=None) + fn = _callback(app, "dataset-table.rowData") + rows, _selected, _title, _summary = fn(None, ["happy"], False, None, None, None) + self.assertEqual([r["text"] for r in rows], ["i am happy"]) + + def test_grid_filter_narrows_the_shown_count_without_touching_rowData(self): + # AG Grid's own column filter runs client-side on top of whatever rowData filter_table + # hands it — it must not change rowData/selectedRows, only the reported count, so the + # grid's filtered view and the "Showing:" summary agree without a second, disconnected + # counter (the earlier bug this mirrors: a filter description without a matching count). + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.rowData") + filter_model = {"text": {"filterType": "text", "type": "contains", "filter": "happy"}} + virtual_row_data = [{"text": "i am happy"}] + rows, _selected, _title, summary = fn(None, None, False, None, filter_model, virtual_row_data) + self.assertEqual(len(rows), 4) # rowData is untouched — the grid does its own filtering + self.assertIn("grid filter: Text", summary) + self.assertIn("(1 of 4)", summary) + + +class TestGridFilterClearButton(unittest.TestCase): + """AG Grid's own column filters run client-side, outside filter_table's inputs — the clear + button is the only thing that lets a user drop one without reaching into the grid's own header + UI, so its show/hide and reset behavior need their own coverage. The filter's *description* is + covered by filter_table itself (see TestTableFilterIntersection), since it's folded into the + same "Showing:" summary rather than a separate badge. + """ + + @staticmethod + def _explanation(): + texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) + token_strings = [t.split() for t in texts] + values = [np.random.randn(len(toks), 2) for toks in token_strings] + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=np.zeros((4, 2)), + y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), + label_names=_WHATIF_LABEL_NAMES, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="point", + output_space="probability", + ) + + def test_no_filter_model_hides_the_clear_button(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "grid-filter-clear-btn.style") + self.assertEqual(fn(None)["display"], "none") + + def test_active_filter_model_shows_the_clear_button(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "grid-filter-clear-btn.style") + filter_model = {"prediction": {"filterType": "text", "type": "equals", "filter": "pos"}} + self.assertNotEqual(fn(filter_model).get("display"), "none") + + def test_clear_button_resets_the_grid_filter_model(self): + app = NlpWebApp(self._explanation(), engine=None) + fn = _callback(app, "dataset-table.filterModel") + self.assertEqual(fn(1), {}) + + +class TestNlpWebApp(unittest.TestCase): + def setUp(self): + self.xpl = _make_explainer() + self.explanation = _make_explanation() + self.webapp = NlpWebApp(self.explanation, engine=self.xpl) + + def test_layout_built(self): + self.assertIsNotNone(self.webapp.app.layout) + + def test_class_selector_options(self): + # Class selector is now two independent dropdowns: "local-class-selector" (Sentence + # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). + # Only the global one offers the cross-class overview — a single sample's highlight has no + # such aggregate. + local = self._find_component(self.webapp.app.layout, "local-class-selector") + self.assertIsNotNone(local, "local-class-selector dropdown not found in layout") + self.assertEqual([opt["label"] for opt in local.options], LABEL_NAMES) + + glob = self._find_component(self.webapp.app.layout, "global-class-selector") + self.assertIsNotNone(glob, "global-class-selector dropdown not found in layout") + self.assertEqual(len(glob.options), N_CLASSES + 1) + self.assertEqual([opt["label"] for opt in glob.options], ["All classes", *LABEL_NAMES]) + # Its value must never collide with a real class index. + self.assertEqual(glob.options[0]["value"], "all") + self.assertEqual([opt["value"] for opt in glob.options[1:]], list(range(N_CLASSES))) + + def test_local_class_selector_defaults_to_predicted_class(self): + # Defaults to the predicted class of the initially selected row (row 0). + dropdown = self._find_component(self.webapp.app.layout, "local-class-selector") + row0_prediction = self.webapp._full_table_records[0]["prediction"] + self.assertEqual(dropdown.value, LABEL_NAMES.index(row0_prediction)) + + def test_dataset_table_populated(self): + table = self._find_component(self.webapp.app.layout, "dataset-table") + self.assertIsNotNone(table, "dataset-table not found in layout") + self.assertEqual(len(table.rowData), 3) + self.assertIn("text", table.rowData[0]) + self.assertIn("prediction", table.rowData[0]) + + def test_graph_ids_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("global-importance-graph", ids) + self.assertIn("dataset-table", ids) + self.assertIn("local-class-selector", ids) + self.assertIn("global-class-selector", ids) + # token bar chart removed; sentence-highlight replaced it + self.assertNotIn("local-contributions-graph", ids) + + def test_control_ids_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("topk-input", ids) + self.assertIn("sign-filter", ids) + self.assertIn("word-filter", ids) + + def test_sentence_highlight_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("sentence-highlight", ids) + + def test_waterfall_controls_present(self): + ids = self._collect_ids(self.webapp.app.layout) + # Waterfall is now a tab (no show/hide switch); the threshold slider + graph live in it. + self.assertIn("waterfall-threshold", ids) + self.assertIn("waterfall-graph", ids) + + def test_dataset_table_no_ground_truth_by_default(self): + table = self._find_component(self.webapp.app.layout, "dataset-table") + col_fields = [c["field"] for c in table.columnDefs] + self.assertNotIn("ground_truth", col_fields) + + def test_dataset_table_with_y_true(self): + y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") + explanation = _make_explanation(y_true=y_true) + webapp = NlpWebApp(explanation, engine=self.xpl) + table = self._find_component(webapp.app.layout, "dataset-table") + col_fields = [c["field"] for c in table.columnDefs] + self.assertIn("ground_truth", col_fields) + self.assertEqual(table.rowData[0]["ground_truth"], "sadness") + + def test_scatter_store_always_present(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertIn("scatter-selected-indices", ids) + + def test_scatter_absent_when_no_xy(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertNotIn("scatter-plot", ids) + self.assertNotIn("color-by", ids) + + def test_scatter_present_when_a_projection_is_given(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((3, 2))) + ids = self._collect_ids(webapp.app.layout) + self.assertIn("scatter-plot", ids) + self.assertIn("color-by", ids) + + def test_scatter_mounts_from_a_matching_embedding(self): + projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id=self.explanation.corpus_id, reducer_tag="pca") + ids = self._collect_ids(NlpWebApp(self.explanation, engine=self.xpl, projection=projection).app.layout) + self.assertIn("scatter-plot", ids) + + def test_an_embedding_of_other_texts_is_refused_at_construction(self): + projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id="other-texts", reducer_tag="pca") + with self.assertRaises(ValueError): + NlpWebApp(self.explanation, engine=self.xpl, projection=projection) + + def test_scatter_wrong_shape_raises(self): + with self.assertRaises(ValueError): + NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((5, 2))) # 5 rows but only 3 samples + + # ── Error Analysis tab (confusion matrix) ───────────────────────── + + def _make_webapp_with_gt(self) -> NlpWebApp: + # y_pred is ["joy", "sadness", "joy"]; make sample 0 a sadness→joy error, others correct. + y_true = pd.Series(["sadness", "sadness", "joy"], index=pd.RangeIndex(3), name="ground_truth") + explanation = _make_explanation(y_true=y_true) + return NlpWebApp(explanation, engine=self.xpl) + + def test_error_analysis_absent_without_ground_truth(self): + ids = self._collect_ids(self.webapp.app.layout) + self.assertNotIn("confusion-matrix-graph", ids) + self.assertNotIn("error-pred-importance", ids) + + def test_error_cell_store_always_present(self): + # The store is created unconditionally so cross-panel callbacks can read it even without gt. + self.assertIn("error-cell", self._collect_ids(self.webapp.app.layout)) + + def test_error_analysis_present_with_ground_truth(self): + ids = self._collect_ids(self._make_webapp_with_gt().app.layout) + for cid in ("confusion-matrix-graph", "error-pred-importance", "error-true-importance", "cm-normalize"): + self.assertIn(cid, ids) + + @staticmethod + def _error_analysis_comp(webapp: NlpWebApp) -> ErrorAnalysisComponent: + return next(c for c in webapp._components if isinstance(c, ErrorAnalysisComponent)) + + def test_confusion_matrix_counts(self): + webapp = self._make_webapp_with_gt() + cm = self._error_analysis_comp(webapp)._cm + # LABEL_NAMES index: sadness=0, joy=1. Rows=true, cols=pred. + self.assertEqual(cm[0, 1], 1) # true sadness predicted joy (the error) + self.assertEqual(cm[0, 0], 1) # true sadness predicted sadness + self.assertEqual(cm[1, 1], 1) # true joy predicted joy + self.assertEqual(cm.sum(), 3) + + def test_confusion_matrix_index_arrays(self): + webapp = self._make_webapp_with_gt() + comp = self._error_analysis_comp(webapp) + self.assertEqual(comp._cm_true_idx.tolist(), [0, 0, 1]) # sadness, sadness, joy + self.assertEqual(comp._cm_pred_idx.tolist(), [1, 0, 1]) # joy, sadness, joy + + def test_confusion_matrix_figure_customdata_orientation(self): + webapp = self._make_webapp_with_gt() + graph = self._find_component(webapp.app.layout, "confusion-matrix-graph") + cd = np.asarray(graph.figure.data[0].customdata) + self.assertEqual(list(cd[0, 1]), [1, 0]) # cell (true=0, pred=1) → [pred_idx, true_idx] + + def test_cell_from_click_uses_label_names_when_no_customdata(self): + # Heatmap clicks may omit customdata; the x (pred) / y (true) labels must still resolve. + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + click = {"points": [{"x": "joy", "y": "sadness"}]} + self.assertEqual(_cell_from_click(click, name_to_idx), (1, 0)) + + def test_cell_from_click_prefers_customdata(self): + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + click = {"points": [{"x": "joy", "y": "sadness", "customdata": [2, 3]}]} + self.assertEqual(_cell_from_click(click, name_to_idx), (2, 3)) + + def test_cell_from_click_none_on_empty_or_unknown(self): + name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} + self.assertIsNone(_cell_from_click(None, name_to_idx)) + self.assertIsNone(_cell_from_click({"points": []}, name_to_idx)) + self.assertIsNone(_cell_from_click({"points": [{"x": "??", "y": "??"}]}, name_to_idx)) + + @staticmethod + def _callback(webapp, out_substr): + # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. + for key, spec in webapp.app.callback_map.items(): + if out_substr in key: + fn = spec["callback"] + return getattr(fn, "__wrapped__", fn) + raise KeyError(out_substr) + + def test_update_confusion_matrix_normalizes_on_recall(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "confusion-matrix-graph.figure") + fig = update("recall") + # The container drives sizing for this half-column panel, not the figure itself. + self.assertIsNone(fig.layout.width) + self.assertIsNone(fig.layout.height) + + def test_set_error_cell_resolves_click_to_cell_and_clears_clickdata(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + click = {"points": [{"x": "joy", "y": "sadness"}]} + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] + data, click_out = set_cell(click, None) + self.assertEqual(data, {"pred": 1, "true": 0, "indices": [0]}) + self.assertIsNone(click_out) + + def test_set_error_cell_prevents_update_on_unresolved_click(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + click = {"points": [{"x": "??", "y": "??"}]} + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] + with self.assertRaises(PreventUpdate): + set_cell(click, None) + + def test_set_error_cell_clear_button_resets_store(self): + webapp = self._make_webapp_with_gt() + set_cell = self._callback(webapp, "error-cell.data") + with patch.object(error_analysis_module, "callback_context") as cc: + cc.triggered = [{"prop_id": "error-cell-clear-btn.n_clicks"}] + data, click_out = set_cell(None, 1) + self.assertIsNone(data) + self.assertIsNone(click_out) + + def test_toggle_error_cell_clear_button_visibility(self): + webapp = self._make_webapp_with_gt() + toggle = self._callback(webapp, "error-cell-clear-btn.style") + self.assertEqual(toggle(None)["display"], "none") + self.assertEqual(toggle({"pred": 1, "true": 0, "indices": [0]})["display"], "inline") + + def test_update_error_word_charts_empty_without_a_selected_cell(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + fig_pred, fig_true, caption = update(None) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("Click a cell", caption) + + def test_update_error_word_charts_empty_when_cell_has_no_samples(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + fig_pred, fig_true, caption = update({"pred": 0, "true": 1, "indices": []}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIn("0 samples", caption) + + def test_update_error_word_charts_off_diagonal_cell_flags_few_samples(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + # pred=joy(1), true=sadness(0): the single planted error, row 0. + fig_pred, fig_true, caption = update({"pred": 1, "true": 0, "indices": [0]}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("predicted joy", caption) + self.assertIn("true sadness", caption) + self.assertIn("1 sample", caption) + self.assertIn("few samples", caption) + + def test_update_error_word_charts_diagonal_cell_marks_correct_predictions(self): + webapp = self._make_webapp_with_gt() + update = self._callback(webapp, "error-cell-caption.children") + # pred=joy(1), true=joy(1): a correct prediction, row 2. + fig_pred, fig_true, caption = update({"pred": 1, "true": 1, "indices": [2]}) + self.assertIsInstance(fig_pred, go.Figure) + self.assertIsInstance(fig_true, go.Figure) + self.assertIn("correct predictions (diagonal)", caption) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _find_component(self, node, component_id): + """Depth-first search for a Dash component by id.""" + if hasattr(node, "id") and node.id == component_id: + return node + children = getattr(node, "children", None) + if children is None: + return None + if not isinstance(children, list): + children = [children] + for child in children: + result = self._find_component(child, component_id) + if result is not None: + return result + return None + + def _collect_ids(self, node) -> set: + """Collect all component ids in the layout tree.""" + ids = set() + if hasattr(node, "id") and isinstance(node.id, str): + ids.add(node.id) + children = getattr(node, "children", None) + if children is None: + return ids + if not isinstance(children, list): + children = [children] + for child in children: + ids |= self._collect_ids(child) + return ids + + +class TestNlpWebAppRun(unittest.TestCase): + """``NlpWebApp.run`` / ``NlpExplainer.run_app`` — delegate to ``dash.Dash.run``; a stop handle in notebooks.""" + + def setUp(self): + self.xpl = _make_explainer() + self.explanation = _make_explanation() + + @staticmethod + def _jupyter_active(active): + return patch.object(type(jupyter_dash), "active", new_callable=PropertyMock, return_value=active) + + def test_outside_notebook_delegates_to_dash_and_returns_none(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl) + with self._jupyter_active(False), patch.object(webapp.app, "run") as mock_run: + result = webapp.run(port=8123, debug=True) + mock_run.assert_called_once_with(port=8123, debug=True, host="127.0.0.1") + self.assertIsNone(result) + + def test_in_notebook_returns_handle_on_dash_registered_server(self): + webapp = NlpWebApp(self.explanation, engine=self.xpl) + server = MagicMock() + + def register(port, debug, host): + jupyter_dash._servers[(host, port)] = server + + with self._jupyter_active(True), patch.object(webapp.app, "run", side_effect=register): + app = webapp.run(port=8123) + self.assertIsInstance(app, RunningApp) + app.kill() + server.shutdown.assert_called_once_with() + self.assertNotIn(("127.0.0.1", 8123), jupyter_dash._servers) + + def test_run_app_forwards_the_handle(self): + sentinel = object() + with patch.object(NlpWebApp, "run", return_value=sentinel) as mock_run: + result = self.xpl.run_app(self.explanation, port=8123) + mock_run.assert_called_once_with(port=8123, debug=False, host="127.0.0.1") + self.assertIs(result, sentinel) + + +class TestRunAppMountPath(unittest.TestCase): + """``run_app`` forwards the reverse-proxy mount point to the webapp it builds.""" + + def test_url_base_pathname_reaches_the_webapp(self): + xpl = object.__new__(NlpExplainer) + explanation = object() + with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: + xpl.run_app(explanation, url_base_pathname="/shapash-nlp-explainer/") + + self.assertIs(web_app.call_args.args[0], explanation) + self.assertEqual( + web_app.call_args.kwargs, + { + "engine": xpl, + "projection": None, + "url_base_pathname": "/shapash-nlp-explainer/", + "palette_name": "default", + "colors_dict": None, + "info": {}, + }, + ) + + def test_defaults_to_no_prefix(self): + xpl = object.__new__(NlpExplainer) + with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: + xpl.run_app(object()) + self.assertIsNone(web_app.call_args.kwargs["url_base_pathname"]) diff --git a/tests/unit_tests/webapp/test_nlp_whatif_app.py b/tests/unit_tests/webapp/test_nlp_whatif_app.py deleted file mode 100644 index fa6b8c99..00000000 --- a/tests/unit_tests/webapp/test_nlp_whatif_app.py +++ /dev/null @@ -1,2214 +0,0 @@ -"""Unit tests for the What-if Lab wiring in ``NlpWebApp`` (no real model, no server). - -A fake explainer/engine with synthetic contributions drives layout construction and capability -gating, verifying that the data-editor and counterfactual components mount only when the engine -reports the matching capabilities. -""" - -import unittest -from dataclasses import replace - -import numpy as np -import pandas as pd - -from shapash.backend.nlp_backend import NlpContributions -from shapash.compute.diagnostics.label_noise import LabelIssue, detect_label_issues -from shapash.compute.diagnostics.label_probe import LabelProbe, ProbeVerdict -from shapash.compute.generators.base import Counterfactual, IntField, TokenListField -from shapash.compute.retrieval.similar_examples import Neighbor -from shapash.explainer.nlp_explanation import NlpExplanation -from shapash.webapp.nlp_app import NlpWebApp -from shapash.webapp.nlp_components import ( - CounterfactualComponent, - DataEditorComponent, - LabelNoiseComponent, - SimilarExamplesComponent, -) -from shapash.webapp.nlp_components.base import AppContext -from shapash.webapp.nlp_components.datapoint import pack_datapoint - -LABEL_NAMES = ["neg", "pos"] - - -def _ctx(explanation, engine=None, coords=None) -> AppContext: - """The context ``NlpWebApp`` builds, for calling a component directly.""" - return AppContext(explanation, engine=engine, coords=coords) - - -# A tiny lexically separable corpus for the independent label probe. "bad"/"awful" sit in "neg", -# so a row labelled "neg" gets backed and a row labelled "pos" gets rejected. -_PROBE_CORPUS = ( - [ - "i am happy", - "happy and glad", - "so glad today", - "a happy glad day", - "this is bad", - "bad and awful", - "so awful today", - "a bad awful day", - ], - ["pos"] * 4 + ["neg"] * 4, -) - - -def _contributions() -> NlpContributions: - token_strings = [["i", "am", "happy"], ["this", "is", "bad"]] - values = [np.random.randn(3, 2), np.random.randn(3, 2)] - base_values = np.zeros((2, 2)) - return NlpContributions(token_strings=token_strings, values=values, base_values=base_values) - - -class FakeEngine: - """Minimal explainer/engine stand-in exposing the InteractiveEngine surface + compiled data.""" - - def __init__( - self, - can_edit: bool, - can_cf: bool, - can_similar: bool = False, - has_labels: bool = False, - probe_corpus: tuple[list[str], list[str]] | None = None, - ): - self.probe_corpus = probe_corpus - self._can_edit = can_edit - self._can_cf = can_cf - self._can_similar = can_similar - # A retriever-like handle the SimilarExamples panel reads its layer caption from. - self._retriever = type("R", (), {"layer": "pre_classifier"})() if can_similar else None - self.label_names = LABEL_NAMES - self.texts = pd.Series(["i am happy", "this is bad"], index=pd.RangeIndex(2)) - self.contributions = _contributions() - self.y_pred = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="prediction") - self.y_prob = pd.DataFrame({"neg": [0.2, 0.8], "pos": [0.8, 0.2]}, index=pd.RangeIndex(2)) - self.y_true = None - if has_labels: - # A batch with exactly one planted label error. With two samples and two classes the - # arrangement is forced: both classes must carry a label (or the unlabelled one has no - # estimable threshold and can never be suggested), so sample 0 is labelled "neg" while - # the model confidently says "pos", and sample 1 is labelled "pos" and agrees. - self.y_prob = pd.DataFrame({"neg": [0.1, 0.1], "pos": [0.9, 0.9]}, index=pd.RangeIndex(2)) - self.y_pred = pd.Series(["pos", "pos"], index=pd.RangeIndex(2), name="prediction") - self.y_true = pd.Series(["neg", "pos"], index=pd.RangeIndex(2), name="ground_truth") - # Ground truth is off by default so the existing layout tests, which pin the exact tab - # groups, keep seeing neither the Error Analysis nor the Label Noise tab. - self.detect_calls = [] - - def to_explanation(self) -> NlpExplanation: - """The ``NlpExplanation`` a real ``explain()`` call would have produced for this batch. - - ``NlpWebApp`` holds an explanation, not the engine — this is what tests pass as the first - argument, while ``self`` (the ``FakeEngine``) is passed separately as ``engine=``. - """ - return NlpExplanation( - texts=self.texts, - token_strings=self.contributions.token_strings, - values=self.contributions.values, - base_values=self.contributions.base_values, - y_pred=self.y_pred, - y_prob=self.y_prob, - y_true=self.y_true, - label_names=self.label_names, - folds_case=None, - backend_name="fake", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - def can_detect_label_noise(self, explanation=None): - return self.y_true is not None - - def can_probe_labels(self): - return self.probe_corpus is not None - - def detect_label_noise(self, explanation, top_n=50, score="self_confidence", probe=True): - self.detect_calls.append({"top_n": top_n, "score": score, "probe": probe}) - report = detect_label_issues( - self.y_prob.to_numpy(dtype=float), - [str(v) for v in self.y_true.tolist()], - [str(t) for t in self.texts.tolist()], - list(self.y_prob.columns), - top_n=top_n, - score=score, - ) - if probe and self.can_probe_labels() and report.issues: - verdicts = LabelProbe(*self.probe_corpus).verdicts( - [i.text for i in report.issues], [i.given_label for i in report.issues] - ) - report = replace( - report, - issues=[replace(i, probe=v) for i, v in zip(report.issues, verdicts, strict=True)], - ) - return report - - def can_edit(self): - return self._can_edit - - def can_counterfactual(self): - return self._can_cf - - def can_find_similar(self): - return self._can_similar - - def find_similar(self, text, top_k=5): - return [ - Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), - Neighbor(index=1, score=0.80, text="this is awful", label="neg"), - ][:top_k] - - def find_similar_threshold(self, text, threshold=0.95, limit=50): - all_neighbors = [ - Neighbor(index=0, score=0.99, text="i am joyful", label="pos"), - Neighbor(index=1, score=0.96, text="so glad today", label="pos"), - Neighbor(index=2, score=0.80, text="this is awful", label="neg"), - ] - matches = [n for n in all_neighbors if n.score > threshold] - return matches[:limit], len(matches) - - def available_cf_generators(self): - return [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")] - - def cf_config_spec(self, generator=None): - max_field = "max_ablations" if generator == "ablation_flip" else "max_flips" - return { - "num_examples": IntField(label="Max counterfactuals", default=5, minimum=1, maximum=20), - max_field: IntField(label="Max token edits", default=3, minimum=1, maximum=5), - "tokens_to_ignore": TokenListField(label="Tokens to ignore", default=[]), - } - - def predict(self, text): - return "pos", {"neg": 0.3, "pos": 0.7} - - def explain_text(self, text): - c = _contributions() - return c, "pos", {"neg": 0.3, "pos": 0.7} - - def generate_counterfactuals(self, text, config=None, generator=None): - return [ - Counterfactual( - original_text=text, - new_text=text.replace("happy", "bad"), - tokens=["i", "am", "happy"], - flipped_positions=[2], - substitutions=[(2, "happy", "bad")], - orig_label="pos", - new_label="neg", - orig_prob=0.8, - new_prob=0.7, - prob_delta=0.5, - ) - ] - - -def _collect_ids(node, found): - """Recursively collect all string component ids in a Dash layout tree. - - Also descends into RadioItems/Checklist ``options[].label`` — components nested there (e.g. a - numeric input inline with its radio button) aren't under ``.children``, but Dash still renders - and wires them, since ``label`` is documented to accept a component. - """ - cid = getattr(node, "id", None) - if isinstance(cid, str): - found.add(cid) - options = getattr(node, "options", None) - if isinstance(options, (list, tuple)): - for opt in options: - label = opt.get("label") if isinstance(opt, dict) else None - if isinstance(label, (list, tuple)): - for item in label: - _collect_ids(item, found) - elif label is not None: - _collect_ids(label, found) - children = getattr(node, "children", None) - if children is None: - return - if isinstance(children, (list, tuple)): - for ch in children: - _collect_ids(ch, found) - else: - _collect_ids(children, found) - - -class TestWhatIfMounting(unittest.TestCase): - def _ids(self, engine): - app = NlpWebApp(engine.to_explanation(), engine=engine) - found = set() - _collect_ids(app.app.layout, found) - return app, found - - @staticmethod - def _whatif_components(app): - # app._components also holds the always-on core panels (Sentence Highlight, Waterfall); - # scope these assertions to the capability-gated What-if Lab ones only. - gated = (DataEditorComponent, CounterfactualComponent, SimilarExamplesComponent) - return [c for c in app._components if isinstance(c, gated)] - - def test_full_capabilities_mount_both(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) - self.assertIn("data-editor-input", ids) - self.assertIn("data-editor-predict-btn", ids) - self.assertIn("counterfactual-generate-btn", ids) - self.assertEqual(len(self._whatif_components(app)), 2) - - def test_counterfactual_config_controls_in_initial_layout(self): - # The generate callback's State references these ids, so they must exist in the initial - # layout (rendered per generator from cf_config_spec), not be injected by a later callback. - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) - for name in ("num_examples", "max_flips", "tokens_to_ignore"): - self.assertIn(f"counterfactual-cfg-hotflip-{name}", ids) - for name in ("num_examples", "max_ablations", "tokens_to_ignore"): - self.assertIn(f"counterfactual-cfg-ablation_flip-{name}", ids) - - def test_counterfactual_method_selector_present_with_multiple_generators(self): - # A method selector and one visibility-toggled control group per generator are in the layout. - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) - self.assertIn("counterfactual-generator", ids) - self.assertIn("counterfactual-cfg-group-hotflip", ids) - self.assertIn("counterfactual-cfg-group-ablation_flip", ids) - - def test_selector_toggle_callback_registered_with_multiple_generators(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - outputs = " ".join(app.app.callback_map.keys()) - self.assertIn("counterfactual-cfg-group-hotflip.style", outputs) - - def test_edit_only_mounts_editor_not_counterfactual(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) - self.assertIn("data-editor-input", ids) - self.assertNotIn("counterfactual-generate-btn", ids) - self.assertEqual(len(self._whatif_components(app)), 1) - - def test_no_capabilities_hides_whatif_lab(self): - app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) - self.assertNotIn("data-editor-input", ids) - self.assertNotIn("counterfactual-generate-btn", ids) - self.assertNotIn("whatif-apply-store", ids) - self.assertEqual(self._whatif_components(app), []) - - def test_callbacks_registered_when_mounted(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - outputs = " ".join(app.app.callback_map.keys()) - self.assertIn("data-editor-prob.figure", outputs) - self.assertIn("counterfactual-results.children", outputs) - - def test_similar_panel_mounts_when_capable(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False, can_similar=True)) - self.assertIn("similar-topk", ids) - self.assertIn("similar-results", ids) - self.assertTrue(any(isinstance(c, SimilarExamplesComponent) for c in app._components)) - - def test_similar_panel_hidden_without_capability(self): - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, can_similar=False)) - self.assertNotIn("similar-topk", ids) - self.assertNotIn("similar-results", ids) - - def test_similar_panel_requires_predict(self): - # CAP_SIMILAR alone is not enough — the Inspect flow needs predict (explain_text) too. - _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, can_similar=True)) - self.assertNotIn("similar-topk", ids) - - def test_similar_callbacks_registered_when_mounted(self): - engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - outputs = " ".join(app.app.callback_map.keys()) - self.assertIn("similar-results.children", outputs) - - -def _callback_binding_ids(app, output_substr): - """Return the (id, property) pairs bound as inputs+state for the callback with this output.""" - for key, spec in app.app.callback_map.items(): - if output_substr in key: - pairs = [(i["id"], i["property"]) for i in spec["inputs"]] - pairs += [(s["id"], s["property"]) for s in spec.get("state", [])] - return pairs - raise KeyError(output_substr) - - -class TestThreePanelLayout(unittest.TestCase): - """The LIT-style three-panel shell: tab groups, mounted bodies, and the current-datapoint store.""" - - def _ids(self, engine, **kwargs): - app = NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) - found = set() - _collect_ids(app.app.layout, found) - return app, found - - def test_full_tab_groups_when_all_panels_available(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) - self.assertEqual( - app._tab_groups, - { - "left-tabs": ["table", "scatter", "editor"], - # Word Profile is data-only, so it mounts unconditionally beside Word Importance. - "upper-right-tabs": ["importance", "word-profile", "counterfactual"], - "lower-right-tabs": ["highlight", "waterfall"], - }, - ) - # Every tab body is mounted in the DOM (visibility is toggled, not the mount). - for body_id in ( - "left-tabs-body-table", - "left-tabs-body-scatter", - "left-tabs-body-editor", - "upper-right-tabs-body-importance", - "upper-right-tabs-body-counterfactual", - "lower-right-tabs-body-highlight", - "lower-right-tabs-body-waterfall", - ): - self.assertIn(body_id, ids) - - def test_tabs_degrade_without_scatter_or_whatif(self): - app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) - self.assertEqual(app._tab_groups["left-tabs"], ["table"]) - self.assertEqual(app._tab_groups["upper-right-tabs"], ["importance", "word-profile"]) - self.assertEqual(app._tab_groups["lower-right-tabs"], ["highlight", "waterfall"]) - self.assertNotIn("left-tabs-body-scatter", ids) - self.assertNotIn("left-tabs-body-editor", ids) - self.assertNotIn("upper-right-tabs-body-counterfactual", ids) - - def test_counterfactual_tab_only_with_editor(self): - # Editor without CF: editor tab present on the left, no counterfactual tab on the right. - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) - self.assertIn("editor", app._tab_groups["left-tabs"]) - self.assertNotIn("counterfactual", app._tab_groups["upper-right-tabs"]) - - def test_selection_bar_present(self): - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True), projection=np.zeros((2, 2))) - self.assertIn("selection-summary", ids) - self.assertIn("word-filter-clear-btn", ids) - self.assertIn("scatter-clear-btn", ids) - - def test_scatter_clear_absent_without_scatter(self): - _, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) - self.assertIn("word-filter-clear-btn", ids) - self.assertNotIn("scatter-clear-btn", ids) - - def test_current_datapoint_store_present(self): - _, ids = self._ids(FakeEngine(can_edit=False, can_cf=False)) - self.assertIn("current-datapoint", ids) - - def test_detail_panels_read_current_datapoint(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - # Highlight and waterfall render off the shared primary-selection store... - self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "sentence-highlight.children")) - self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "waterfall-graph.figure")) - # ...and counterfactuals generate from it too (so a selected row works, not only editor text). - self.assertIn(("current-datapoint", "data"), _callback_binding_ids(app, "counterfactual-results.children")) - - def test_current_datapoint_written_by_row_and_editor(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - # The table-selection writer keys off the selected row. - self.assertIn(("dataset-table", "selectedRows"), _callback_binding_ids(app, "current-datapoint.data")) - # The editor's Predict also writes it (allow_duplicate) — its combined key carries the prob figure. - editor_key = next(k for k in app.app.callback_map if "data-editor-prob.figure" in k) - self.assertIn("current-datapoint.data", editor_key) - - def test_tab_toggle_callbacks_registered(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine, projection=np.zeros((2, 2))) - outputs = " ".join(app.app.callback_map.keys()) - self.assertIn("left-tabs-body-table.style", outputs) - self.assertIn("lower-right-tabs-body-waterfall.style", outputs) - - -class TestReadContractSeam(unittest.TestCase): - """The app shell reads compiled data only from the ``NlpExplanation``; no raw explainer handle. - - ``FakeEngine`` has no ``explainer`` attribute, so a mounted app proves the read path is served by - the artifact alone. These asserts lock the seam so a future edit cannot silently reintroduce a - ``self.explainer`` bypass, nor let display state accumulate back onto the artifact. - """ - - def test_app_holds_no_raw_explainer_handle(self): - engine = FakeEngine(can_edit=True, can_cf=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - self.assertFalse(hasattr(app, "explainer")) - - def test_app_exposes_explanation_and_engine_roles(self): - engine = FakeEngine(can_edit=True, can_cf=True) - explanation = engine.to_explanation() - app = NlpWebApp(explanation, engine=engine) - self.assertIs(app._explanation, explanation) # read contract: the artifact itself - self.assertIs(app._engine, engine) # live-action contract - - -class TestSimilarComponent(unittest.TestCase): - """Exercise the Similar Examples component's renderer and callbacks directly.""" - - @staticmethod - def _register(): - import dash - - from shapash.webapp.nlp_components import SimilarExamplesComponent - - engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) - explanation = engine.to_explanation() - app = dash.Dash(__name__) - comp = SimilarExamplesComponent() - comp.register_callbacks( - app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} - ) - return app, engine - - @staticmethod - def _callback(app, out_substr): - # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. - for key, spec in app.callback_map.items(): - if out_substr in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError(out_substr) - - def test_layout_shows_layer_caption(self): - from shapash.webapp.nlp_components import SimilarExamplesComponent - - engine = FakeEngine(can_edit=True, can_cf=False, can_similar=True) - found = set() - _collect_ids(SimilarExamplesComponent().layout(_ctx(engine.to_explanation(), engine)), found) - self.assertIn("similar-topk", found) - self.assertIn("similar-threshold", found) - self.assertIn("similar-mode", found) - self.assertIn("similar-results", found) - - def test_update_similar_returns_table_and_texts(self): - app, _ = self._register() - update = self._callback(app, "similar-results") - children, texts = update({"text": "i am happy", "label": "pos"}, "topk", 5, 0.95) - self.assertEqual(texts, ["i am joyful", "this is awful"]) - self.assertIsNotNone(children) - - def test_update_similar_ignores_empty_text(self): - from dash.exceptions import PreventUpdate - - app, _ = self._register() - update = self._callback(app, "similar-results") - with self.assertRaises(PreventUpdate): - update({"text": " "}, "topk", 5, 0.95) - - def test_update_similar_threshold_mode_filters_and_reports_total(self): - app, _ = self._register() - update = self._callback(app, "similar-results") - children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.90) - # Only the two neighbours scoring above 0.90 clear the threshold (see FakeEngine.find_similar_threshold). - self.assertEqual(texts, ["i am joyful", "so glad today"]) - self.assertIsNotNone(children) - - def test_update_similar_threshold_mode_empty_above_cutoff(self): - app, _ = self._register() - update = self._callback(app, "similar-results") - children, texts = update({"text": "i am happy", "label": "pos"}, "threshold", 5, 0.999) - self.assertEqual(texts, []) - self.assertIsNotNone(children) - - def test_toggle_mode_inputs_disables_the_inactive_control(self): - app, _ = self._register() - toggle = self._callback(app, "similar-topk.disabled") - topk_disabled, threshold_disabled = toggle("threshold") - self.assertTrue(topk_disabled) - self.assertFalse(threshold_disabled) - topk_disabled, threshold_disabled = toggle("topk") - self.assertFalse(topk_disabled) - self.assertTrue(threshold_disabled) - - def test_inspect_makes_neighbor_the_current_datapoint(self): - from unittest import mock - - from shapash.webapp.nlp_components import similar_examples as mod - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "similar-apply", "index": 1} - dp = inspect([1, 1], ["first neighbor", "second neighbor"]) - self.assertEqual(dp["text"], "second neighbor") - self.assertEqual(dp["label"], "pos") # FakeEngine.explain_text returns "pos" - - def test_neighbors_table_marks_matching_label(self): - from shapash.webapp.nlp_components.similar_examples import _neighbors_table - - neighbors = [ - Neighbor(index=0, score=0.9, text="joyful one", label="pos"), - Neighbor(index=1, score=0.5, text="grim one", label="neg"), - ] - table = _neighbors_table(neighbors, predicted_label="pos", component_id="similar") - ids = set() - _collect_ids(table, ids) - # One Inspect button per neighbour (pattern-matching ids are dicts, so assert via the count). - self.assertEqual(sum(1 for n in neighbors), 2) - self.assertIsNotNone(table) - - def test_neighbors_table_without_labels(self): - from shapash.webapp.nlp_components.similar_examples import _neighbors_table - - neighbors = [Neighbor(index=0, score=0.9, text="some text", label=None)] - table = _neighbors_table(neighbors, predicted_label=None, component_id="similar") - self.assertIsNotNone(table) - - def test_match_rate_caption_reports_share_of_predicted_label(self): - from shapash.webapp.nlp_components.similar_examples import _match_rate_caption - - neighbors = [ - Neighbor(index=0, score=0.9, text="a", label="pos"), - Neighbor(index=1, score=0.8, text="b", label="pos"), - Neighbor(index=2, score=0.7, text="c", label="neg"), - Neighbor(index=3, score=0.6, text="d", label="neg"), - ] - caption = _match_rate_caption(neighbors, predicted_label="pos") - self.assertIn("2/4", caption) - self.assertIn("50%", caption) - - def test_match_rate_caption_none_without_a_prediction_or_labels(self): - from shapash.webapp.nlp_components.similar_examples import _match_rate_caption - - neighbors = [Neighbor(index=0, score=0.9, text="a", label="pos")] - self.assertIsNone(_match_rate_caption(neighbors, predicted_label=None)) - unlabelled = [Neighbor(index=0, score=0.9, text="a", label=None)] - self.assertIsNone(_match_rate_caption(unlabelled, predicted_label="pos")) - - def test_render_results_includes_cap_note_only_when_capped(self): - from shapash.webapp.nlp_components.similar_examples import _render_results - - neighbors = [Neighbor(index=0, score=0.99, text="a", label="pos")] - capped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=5) - uncapped = _render_results(neighbors, predicted_label="pos", component_id="similar", shown_of=None) - - def _flat_text(node): - children = getattr(node, "children", None) - if isinstance(children, str): - return children - if isinstance(children, list): - return "".join(_flat_text(c) for c in children if c is not None) - return _flat_text(children) if children is not None else "" - - self.assertIn("Showing 1 of 5", _flat_text(capped)) - self.assertNotIn("Showing", _flat_text(uncapped)) - - -class TestLabelNoiseMounting(unittest.TestCase): - """``CAP_LABELS`` is a *data* capability: it depends on the compiled batch, not on a live model.""" - - def _ids(self, engine): - app = NlpWebApp(engine.to_explanation(), engine=engine) - found = set() - _collect_ids(app.app.layout, found) - return app, found - - def test_mounts_with_ground_truth_and_per_class_probabilities(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True, has_labels=True)) - self.assertIn("label-noise-detect-btn", ids) - self.assertIn("label-noise-results", ids) - self.assertIn("label-noise", app._tab_groups["upper-right-tabs"]) - self.assertTrue(any(isinstance(c, LabelNoiseComponent) for c in app._components)) - - def test_hidden_without_ground_truth(self): - app, ids = self._ids(FakeEngine(can_edit=True, can_cf=True)) - self.assertNotIn("label-noise-detect-btn", ids) - self.assertNotIn("label-noise", app._tab_groups["upper-right-tabs"]) - - def test_hidden_when_only_the_winning_probability_is_available(self): - engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) - engine.y_prob = pd.DataFrame({"probability": [0.8, 0.8]}, index=pd.RangeIndex(2)) - _, ids = self._ids(engine) - self.assertNotIn("label-noise-detect-btn", ids) - - def test_mounts_without_any_live_capability(self): - # The snapshot case: no model, so no editor/counterfactual/similar panel — but the labels and - # probabilities are still in the compiled batch, so this panel stands on its own. - app, ids = self._ids(FakeEngine(can_edit=False, can_cf=False, has_labels=True)) - self.assertIn("label-noise-detect-btn", ids) - self.assertNotIn("data-editor-input", ids) - - def test_caption_warns_when_no_independent_cross_check_is_available(self): - engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True) - layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) - caption = layout.children[1].children - self.assertIn("no independent cross-check", caption) - - def test_caption_explains_the_corpus_column_when_the_probe_is_available(self): - engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, probe_corpus=_PROBE_CORPUS) - layout = LabelNoiseComponent().layout(_ctx(engine.to_explanation(), engine)) - caption = layout.children[1].children - self.assertIn("Corpus column", caption) - - def test_callbacks_registered_when_mounted(self): - engine = FakeEngine(can_edit=True, can_cf=True, has_labels=True) - app = NlpWebApp(engine.to_explanation(), engine=engine) - outputs = " ".join(app.app.callback_map.keys()) - self.assertIn("label-noise-results.children", outputs) - - -class TestLabelNoiseComponent(unittest.TestCase): - """Drive the panel's renderer and callbacks directly, without the app shell.""" - - @staticmethod - def _register(**kwargs): - import dash - - engine = FakeEngine(can_edit=True, can_cf=False, has_labels=True, **kwargs) - explanation = engine.to_explanation() - app = dash.Dash(__name__) - comp = LabelNoiseComponent() - comp.register_callbacks( - app, _ctx(explanation, engine), {"apply": "whatif-apply-store", "current": "current-datapoint"} - ) - return app, engine - - @staticmethod - def _callback(app, out_substr): - for key, spec in app.callback_map.items(): - if out_substr in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError(out_substr) - - def test_detect_returns_a_view_and_the_flagged_indices(self): - app, _ = self._register() - detect = self._callback(app, "label-noise-results") - children, indices = detect(1, 50, "self_confidence") - self.assertEqual(indices, [0]) # sample 0 is labelled neg but confidently predicted pos - self.assertIsNotNone(children) - - def test_detect_forwards_the_controls_to_the_engine(self): - app, engine = self._register() - detect = self._callback(app, "label-noise-results") - detect(1, 7, "normalized_margin") - self.assertEqual(engine.detect_calls[-1], {"top_n": 7, "score": "normalized_margin", "probe": True}) - - def test_blank_controls_fall_back_to_defaults(self): - app, engine = self._register() - detect = self._callback(app, "label-noise-results") - detect(1, None, None) - self.assertEqual(engine.detect_calls[-1], {"top_n": 50, "score": "self_confidence", "probe": True}) - - def test_detect_renders_the_corpus_column_when_a_probe_corpus_is_bound(self): - app, _ = self._register(probe_corpus=_PROBE_CORPUS) - detect = self._callback(app, "label-noise-results") - children, _ = detect(1, 50, "self_confidence") - table = children.children[-1] - headers = [th.children for th in table.children[0].children.children] - self.assertEqual(headers, ["Score", "Given", "Probably", "Corpus", "Text", ""]) - # The flagged row is "i am happy" labelled "neg"; the corpus puts that vocabulary in "pos", - # so the probe rejects the label too rather than defending it. - cell = table.children[1].children[0].children[3].children - self.assertEqual(cell.children, "0.14 → pos") - - def test_detect_omits_the_corpus_column_without_a_probe_corpus(self): - app, _ = self._register() - detect = self._callback(app, "label-noise-results") - children, _ = detect(1, 50, "self_confidence") - table = children.children[-1] - headers = [th.children for th in table.children[0].children.children] - self.assertNotIn("Corpus", headers) - - def test_detect_does_nothing_before_the_button_is_clicked(self): - from dash.exceptions import PreventUpdate - - app, _ = self._register() - detect = self._callback(app, "label-noise-results") - with self.assertRaises(PreventUpdate): - detect(None, 50, "self_confidence") - - def test_a_clean_corpus_renders_a_message_and_no_rows(self): - app, engine = self._register() - # Both classes labelled and both labels agreeing with the model, so there is no noise to find. - engine.y_prob = pd.DataFrame({"neg": [0.1, 0.9], "pos": [0.9, 0.1]}, index=pd.RangeIndex(2)) - engine.y_true = pd.Series(["pos", "neg"], index=pd.RangeIndex(2), name="ground_truth") - detect = self._callback(app, "label-noise-results") - children, indices = detect(1, 50, "self_confidence") - self.assertEqual(indices, []) - self.assertIn("No label issues detected", children.children) - - def test_inspect_makes_the_flagged_sample_the_current_datapoint(self): - from unittest import mock - - from shapash.webapp.nlp_components import label_noise as mod - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "label-noise-apply", "index": 0} - dp = inspect([1], [1]) # row 0 of the table points at compiled sample 1 - self.assertEqual(dp["text"], "this is bad") - self.assertEqual(dp["orig_idx"], 1) - self.assertEqual(dp["label"], "pos") # the *prediction*, matching the dataset-row path - self.assertEqual(len(dp["tokens"]), 3) - - def test_inspect_ignores_a_render_with_no_clicks(self): - from dash.exceptions import PreventUpdate - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with self.assertRaises(PreventUpdate): - inspect([0], [1]) - - def test_inspect_ignores_a_missing_trigger(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - - from shapash.webapp.nlp_components import label_noise as mod - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = None - with self.assertRaises(PreventUpdate): - inspect([1], [1]) - - def test_inspect_ignores_an_out_of_range_row(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - - from shapash.webapp.nlp_components import label_noise as mod - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "label-noise-apply", "index": 5} - with self.assertRaises(PreventUpdate): - inspect([1], [1]) - - def test_inspect_ignores_a_stale_index_beyond_the_corpus(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - - from shapash.webapp.nlp_components import label_noise as mod - - app, _ = self._register() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "label-noise-apply", "index": 0} - with self.assertRaises(PreventUpdate): - inspect([1], [99]) - - -class TestLabelNoiseTable(unittest.TestCase): - """The neighbour column is evidence *beside* the score, and must not appear when absent.""" - - @staticmethod - def _issue(**kwargs): - base = dict( - index=0, - text="i am happy", - given_label="neg", - suggested_label="pos", - given_prob=0.2, - suggested_prob=0.8, - score=0.2, - ) - return LabelIssue(**{**base, **kwargs}) - - def test_corpus_column_omitted_when_no_probe_ran(self): - from shapash.webapp.nlp_components.label_noise import _issues_table - - table = _issues_table([self._issue()], "label-noise") - headers = [th.children for th in table.children[0].children.children] - self.assertNotIn("Corpus", headers) - self.assertEqual(headers, ["Score", "Given", "Probably", "Text", ""]) - - def test_corpus_column_present_when_any_row_has_a_verdict(self): - from shapash.webapp.nlp_components.label_noise import _issues_table - - verdict = ProbeVerdict(given_prob=0.8, top_label="neg", backs_given=True) - issues = [self._issue(probe=verdict), self._issue(index=1)] - table = _issues_table(issues, "label-noise") - headers = [th.children for th in table.children[0].children.children] - self.assertIn("Corpus", headers) - - def test_probe_summary_warns_when_the_corpus_backs_the_given_label(self): - from shapash.webapp.nlp_components.label_noise import _probe_summary - - # The row the panel exists to catch: confident learning flagged it, but an independent - # classifier defends the label — so it is the model that is wrong, not the corpus. - verdict = ProbeVerdict(given_prob=0.83, top_label="neg", backs_given=True) - summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) - self.assertEqual(summary.children, "⚠ 0.83") - self.assertIn("text-warning", summary.className) - self.assertIn("more likely", summary.title) - - def test_probe_summary_reports_a_corroborated_label_error_plainly(self): - from shapash.webapp.nlp_components.label_noise import _probe_summary - - verdict = ProbeVerdict(given_prob=0.16, top_label="pos", backs_given=False) - summary = _probe_summary(self._issue(given_label="neg", probe=verdict)) - self.assertEqual(summary.children, "0.16 → pos") - self.assertIn("text-muted", summary.className) - - def test_rows_without_a_verdict_render_a_dash(self): - from shapash.webapp.nlp_components.label_noise import _probe_summary - - self.assertEqual(_probe_summary(self._issue()), "—") - - -class TestSubpathMounting(unittest.TestCase): - """``url_base_pathname`` — serving behind a reverse proxy that routes a subpath, not a host.""" - - def _app(self, **kwargs): - engine = FakeEngine(can_edit=True, can_cf=True) - return NlpWebApp(engine.to_explanation(), engine=engine, **kwargs) - - def test_serves_at_the_root_by_default(self): - config = self._app().app.config - self.assertEqual(config.routes_pathname_prefix, "/") - self.assertEqual(config.requests_pathname_prefix, "/") - - def test_mounts_under_the_given_prefix(self): - config = self._app(url_base_pathname="/shapash-nlp-explainer/").app.config - # Both prefixes matter: routes_* is where Dash listens, requests_* is what it writes into - # the asset and callback URLs the browser then fetches. - self.assertEqual(config.routes_pathname_prefix, "/shapash-nlp-explainer/") - self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") - - def test_missing_slashes_are_added(self): - for given in ("shapash-nlp-explainer", "/shapash-nlp-explainer", "shapash-nlp-explainer/"): - with self.subTest(given=given): - config = self._app(url_base_pathname=given).app.config - self.assertEqual(config.requests_pathname_prefix, "/shapash-nlp-explainer/") - - def test_the_served_routes_and_asset_urls_carry_the_prefix(self): - # The config values above are only half the contract: Dash must also *answer* on the prefix - # and write it into the script tags, or the page loads blank behind the proxy. - client = self._app(url_base_pathname="/shapash-nlp-explainer/").app.server.test_client() - self.assertEqual(client.get("/shapash-nlp-explainer/").status_code, 200) - self.assertEqual(client.get("/").status_code, 404) - body = client.get("/shapash-nlp-explainer/").get_data(as_text=True) - self.assertIn("/shapash-nlp-explainer/_dash-component-suites", body) - self.assertNotIn('"/_dash-', body) - - def test_empty_prefixes_serve_at_the_root(self): - for given in ("", "/", " "): - with self.subTest(given=given): - config = self._app(url_base_pathname=given).app.config - self.assertEqual(config.requests_pathname_prefix, "/") - - -if __name__ == "__main__": - unittest.main() - - -class TestWordProfileComponent(unittest.TestCase): - """The single-word profile panel: word × aggregation × global selection, then drill-down.""" - - STORES = { - "current": "current-datapoint", - "selection": "scatter-selected-indices", - "error_cell": "error-cell", - "errors_only": "errors-only-switch", - "word_click": "word-click-filter", - } - - @staticmethod - def _explanation(): - """Four samples, two classes. 'happy' pulls +0.4/+0.3 in two, -0.6 in a third.""" - token_strings = [ - ["so", "happy", "today"], - ["happy", "and", "happy"], - ["not", "happy", "at", "all"], - ["nothing", "here"], - ] - values = [ - np.array([[0.1, -0.1], [0.4, -0.4], [0.05, -0.05]]), - np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1]]), - np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), - np.array([[0.0, 0.0], [0.0, 0.0]]), - ] - texts = pd.Series(["so happy today", "happy and happy", "not happy at all", "nothing here"]) - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=np.zeros((4, 2)), - y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), - y_prob=None, - # Sample 1 is the only model error, so errors-only scopes to exactly that row. - y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="point", - output_space="probability", - ) - - def _app(self): - import dash - - from shapash.webapp.nlp_components import WordProfileComponent - - explanation = self._explanation() - app = dash.Dash(__name__) - comp = WordProfileComponent() - comp.register_callbacks(app, _ctx(explanation, None), self.STORES) - return app, explanation - - @staticmethod - def _callback(app, out_substr): - for key, spec in app.callback_map.items(): - if out_substr in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError(out_substr) - - # ── mounting / layout ─────────────────────────────────────────────── - def test_mounts_without_an_engine(self): - from shapash.webapp.nlp_components import WordProfileComponent - - # Data-only panel: it must survive a loaded snapshot with no live model. - self.assertTrue(WordProfileComponent.is_available(_ctx(self._explanation(), None))) - - def test_layout_declares_every_id_its_callbacks_bind(self): - from shapash.webapp.nlp_components import WordProfileComponent - - found = set() - _collect_ids(WordProfileComponent().layout(_ctx(self._explanation(), None)), found) - for suffix in ("select", "agg", "class", "order", "limit", "graph", "caption", "results", "store"): - self.assertIn(f"word-profile-{suffix}", found) - - def test_word_dropdown_is_seeded_with_the_top_word(self): - from shapash.webapp.nlp_components import WordProfileComponent - - explanation = self._explanation() - layout = WordProfileComponent().layout(_ctx(explanation, None)) - found = {} - - def walk(node): - cid = getattr(node, "id", None) - if isinstance(cid, str): - found[cid] = node - children = getattr(node, "children", None) - for ch in children if isinstance(children, (list, tuple)) else [children]: - if ch is not None and not isinstance(ch, str): - walk(ch) - - walk(layout) - # Not empty on open, and the seed is a real corpus word. - self.assertIn(found["word-profile-select"].value, explanation.vocabulary()) - - def test_class_picker_hidden_for_a_single_output_column(self): - from shapash.webapp.nlp_components import WordProfileComponent - - explanation = self._explanation() - binary = replace( - explanation, - values=[v[:, 0] for v in explanation.values], - base_values=np.zeros(4), - label_names=["score"], - ) - layout = WordProfileComponent().layout(_ctx(binary, None)) - found = set() - _collect_ids(layout, found) - # Still present (callbacks bind it), just not shown. - self.assertIn("word-profile-class", found) - - # ── the profile callback ──────────────────────────────────────────── - def test_profile_reports_counts_and_one_bar_per_class(self): - app, _ = self._app() - fig, caption, table, store = self._callback(app, "word-profile-graph")( - "happy", "mean", 0, "strongest", 10, None, None, False - ) - self.assertEqual(len(fig.data[0].x), 2) - self.assertIn("4 occurrence(s) in 3 of 4 sample(s)", caption) - self.assertIsNotNone(table) - self.assertEqual(store, [2, 0, 1]) # |-0.6| > 0.4 > 0.3 - - def test_mean_reports_its_error_bars_in_the_caption(self): - app, _ = self._app() - _, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 10, None, None, False) - self.assertIn("std across occurrences", caption) - - def test_sum_has_no_error_bars(self): - app, _ = self._app() - fig, caption, _, _ = self._callback(app, "word-profile-graph")("happy", "sum", 0, "most", 10, None, None, False) - self.assertIsNone(fig.data[0].error_x.array) - self.assertNotIn("std", caption) - - def test_absolute_aggregation_surfaces_the_two_way_word(self): - app, _ = self._app() - graph = self._callback(app, "word-profile-graph") - signed = graph("happy", "mean", 0, "most", 10, None, None, False)[0].data[0].x - magnitude = graph("happy", "mean_abs", 0, "most", 10, None, None, False)[0].data[0].x - # The signed mean nearly cancels; the magnitude does not. This gap is the panel's point. - self.assertLess(abs(signed[-1]), 0.05) - self.assertGreater(magnitude[-1], 0.3) - - def test_order_reorders_the_drill_down_only(self): - app, _ = self._app() - graph = self._callback(app, "word-profile-graph") - self.assertEqual(graph("happy", "mean", 0, "most", 10, None, None, False)[3], [0, 1, 2]) - self.assertEqual(graph("happy", "mean", 0, "least", 10, None, None, False)[3][0], 2) - - def test_limit_truncates_the_drill_down(self): - app, _ = self._app() - store = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 1, None, None, False)[3] - self.assertEqual(len(store), 1) - - def test_scatter_selection_scopes_the_aggregate(self): - app, _ = self._app() - _, caption, _, store = self._callback(app, "word-profile-graph")( - "happy", "mean", 0, "most", 10, [0, 3], None, False - ) - self.assertIn("scoped to 2 selected sample(s)", caption) - self.assertEqual(store, [0]) - - def test_errors_only_scopes_the_aggregate(self): - app, _ = self._app() - # Sample 1 is the only misclassified row, and it does contain the word. - _, caption, _, store = self._callback(app, "word-profile-graph")( - "happy", "mean", 0, "most", 10, None, None, True - ) - self.assertIn("scoped to 1 selected sample(s)", caption) - self.assertEqual(store, [1]) - - def test_confusion_cell_scopes_the_aggregate(self): - app, _ = self._app() - _, caption, _, _ = self._callback(app, "word-profile-graph")( - "happy", "mean", 0, "most", 10, None, {"pred": 1, "true": 0, "indices": [2]}, False - ) - self.assertIn("scoped to 1 selected sample(s)", caption) - - def test_word_absent_from_the_scope_says_so(self): - app, _ = self._app() - fig, caption, table, store = self._callback(app, "word-profile-graph")( - "happy", "mean", 0, "most", 10, [3], None, False - ) - self.assertIn("no occurrences", caption) - self.assertIn("does not occur", fig.layout.annotations[0].text) - self.assertIsNone(table) - self.assertEqual(store, []) - - def test_no_word_selected_shows_a_prompt(self): - app, _ = self._app() - fig, caption, table, store = self._callback(app, "word-profile-graph")( - None, "mean", 0, "most", 10, None, None, False - ) - self.assertIn("Pick a word", fig.layout.annotations[0].text) - self.assertEqual((caption, table, store), ("", None, [])) - - def test_falls_back_to_defaults_on_cleared_controls(self): - app, _ = self._app() - _, caption, _, store = self._callback(app, "word-profile-graph")( - "happy", None, None, None, None, None, None, None - ) - self.assertIn("Mean", caption) - self.assertEqual(len(store), 3) - - # ── cross-panel wiring ────────────────────────────────────────────── - def test_follows_a_word_bar_clicked_in_the_importance_panel(self): - app, _ = self._app() - follow = self._callback(app, "word-profile-select.value") - self.assertEqual(follow("today"), "today") - # With a scatter the store holds the multi-select list; the newest word wins. - self.assertEqual(follow(["today", "happy"]), "happy") - - def test_cleared_word_filter_leaves_the_selection_alone(self): - from dash.exceptions import PreventUpdate - - app, _ = self._app() - follow = self._callback(app, "word-profile-select.value") - for cleared in (None, []): - with self.assertRaises(PreventUpdate): - follow(cleared) - - def test_inspect_packs_the_sample_into_the_current_datapoint(self): - from unittest import mock - - from shapash.webapp.nlp_components import word_profile as mod - - app, _ = self._app() - inspect = self._callback(app, "current-datapoint") - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "word-profile-inspect", "index": 1} - # The store holds the displayed rows' positions, so row 1 is sample 0. - datapoint = inspect([None, 1], [2, 0]) - self.assertEqual(datapoint["orig_idx"], 0) - self.assertEqual(datapoint["text"], "so happy today") - self.assertEqual(datapoint["label"], "pos") - - def test_inspect_ignores_clicks_it_cannot_resolve(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - - from shapash.webapp.nlp_components import word_profile as mod - - app, _ = self._app() - inspect = self._callback(app, "current-datapoint") - with self.assertRaises(PreventUpdate): - inspect([None, None], [0, 1]) # no button actually clicked - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = None - with self.assertRaises(PreventUpdate): - inspect([1], [0]) - with mock.patch.object(mod, "callback_context") as cc: - cc.triggered_id = {"type": "word-profile-inspect", "index": 5} - with self.assertRaises(PreventUpdate): - inspect([1], [0]) # index past the end of the displayed rows - - -class TestGlobalWordImportancePanel(unittest.TestCase): - """The shell's Word Importance chart: rank-by, frequency floor, and its empty states.""" - - @staticmethod - def _explanation(): - # "rare" pulls hard once; "common" pulls mildly four times. The two rank-by modes must - # disagree, which is the whole reason the control exists. - token_strings = [["rare", "common"], ["common"], ["common", "common"], ["mild"]] - values = [ - np.array([[0.9, -0.9], [0.3, -0.3]]), - np.array([[0.3, -0.3]]), - np.array([[0.3, -0.3], [0.3, -0.3]]), - np.array([[-0.5, 0.5]]), - ] - texts = pd.Series(["rare common", "common", "common common", "mild"]) - explanation = NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=np.zeros((4, 2)), - y_pred=pd.Series(["neg"] * 4, index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="point", - output_space="probability", - ) - return explanation - - @classmethod - def _app(cls): - app = NlpWebApp(cls._explanation(), engine=None) - for key, spec in app.app.callback_map.items(): - if "global-importance-graph.figure" in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError("global-importance-graph") - - @staticmethod - def _words(fig): - # Bars are drawn bottom-to-top, so reverse back into rank order. - return list(fig.data[0].y)[::-1] - - def setUp(self): - self.graph = self._app() - - def _call( - self, rank_by="mean", floor=1, sign="all", topk=10, indices=None, cell=None, errors=False, label=0, words=None - ): - return self.graph(label, topk, sign, [], rank_by, floor, indices, cell, errors, words) - - @staticmethod - def _values(fig): - return list(fig.data[0].x)[::-1] - - def test_mean_and_sum_produce_different_rankings(self): - self.assertEqual(self._words(self._call(rank_by="mean"))[0], "rare") - self.assertEqual(self._words(self._call(rank_by="sum"))[0], "common") - - def test_axis_names_the_statistic_drawn(self): - self.assertEqual(self._call(rank_by="mean").layout.xaxis.title.text, "Mean SHAP contribution") - self.assertEqual(self._call(rank_by="sum").layout.xaxis.title.text, "Total SHAP contribution") - - def test_frequency_floor_removes_rare_words(self): - self.assertIn("rare", self._words(self._call(floor=1))) - self.assertNotIn("rare", self._words(self._call(floor=2))) - - def test_panel_has_no_title(self): - # The tab is already labelled "Word Importance" and the floor/class are visible in the - # filter row above the chart, so the figure itself carries no title band — reclaiming that - # vertical space for word rows instead. - self.assertIsNone(self._call(floor=2).layout.title.text) - self.assertIsNone(self._call(floor=1).layout.title.text) - - def test_sign_filter_still_works_in_both_modes(self): - for rank_by in ("mean", "sum"): - self.assertEqual(self._words(self._call(rank_by=rank_by, sign="negative")), ["mild"]) - self.assertNotIn("mild", self._words(self._call(rank_by=rank_by, sign="positive"))) - - def test_default_floor_is_two(self): - from shapash.webapp.nlp_components.word_importance import _DEFAULT_MIN_OCCURRENCES - - # A mean over a single observation is not a mean; the panel must not open on one. - self.assertEqual(_DEFAULT_MIN_OCCURRENCES, 2) - - def test_cleared_floor_input_means_no_filter(self): - # An emptied number box arrives as None and must not silently restore the default. - self.assertIn("rare", self._words(self._call(floor=None))) - - def test_floor_below_one_is_clamped(self): - self.assertIn("rare", self._words(self._call(floor=0))) - - def test_impossible_floor_explains_itself(self): - fig = self._call(floor=99) - self.assertEqual(len(fig.data), 0) - self.assertIn("at least 99 time(s) in these 4 sample(s)", fig.layout.annotations[0].text) - - def test_empty_sign_filter_explains_itself_differently(self): - # Only "mild" is negative; excluding it by scope leaves the sign filter with nothing, and - # the message must point at the filter rather than at the frequency floor. - fig = self._call(sign="negative", indices=[0, 1]) - self.assertEqual(len(fig.data), 0) - self.assertIn("negative", fig.layout.annotations[0].text) - - def test_floor_counts_within_the_selection(self): - # "common" occurs 4x overall but once in sample 1, so a floor of 2 must exclude it there. - fig = self._call(floor=2, indices=[1]) - self.assertEqual(len(fig.data), 0) - self.assertIn("in these 1 sample(s)", fig.layout.annotations[0].text) - - def test_hover_carries_the_occurrence_count(self): - fig = self._call(floor=1) - drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) - self.assertEqual(drawn, {"rare": 1, "common": 4, "mild": 1}) - self.assertIn("Occurrences:", fig.data[0].hovertemplate) - - def test_hover_counts_follow_the_selection(self): - # A count that ignored the scope would contradict the floor applied right beside it: - # "common" occurs 4x in the corpus but twice in samples 0-1. - fig = self._call(floor=1, indices=[0, 1]) - drawn = dict(zip(self._words(fig), [int(c[0]) for c in fig.data[0].customdata][::-1])) - self.assertEqual(drawn["common"], 2) - - def test_all_classes_ranks_on_the_strongest_class_magnitude(self): - # "mild" pulls -0.5 on class 0 and +0.5 on class 1: across classes it is a magnitude of - # 0.5, not the -0.5 the single-class view shows. - fig = self._call(label="all") - drawn = dict(zip(self._words(fig), self._values(fig))) - self.assertEqual(drawn, {"rare": 0.9, "mild": 0.5, "common": 0.3}) - self.assertEqual(dict(zip(self._words(self._call()), self._values(self._call())))["mild"], -0.5) - - def test_all_classes_names_its_statistic_on_the_axis(self): - self.assertEqual(self._call(label="all").layout.xaxis.title.text, "Largest |mean SHAP| across classes") - self.assertEqual( - self._call(label="all", rank_by="sum").layout.xaxis.title.text, - "Largest |total SHAP| across classes", - ) - - def test_all_classes_ignores_a_stale_sign_filter(self): - # Every value is a magnitude, so honouring "negative" would blank the chart on a control - # the user cannot even see is active. - self.assertEqual(self._words(self._call(label="all", sign="negative")), ["rare", "mild", "common"]) - - def test_all_classes_still_honours_the_frequency_floor(self): - self.assertNotIn("rare", self._words(self._call(label="all", floor=2))) - - def test_sign_filter_is_greyed_out_under_all_classes(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = None - for key, spec in app.app.callback_map.items(): - if "sign-filter.options" in key: - fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) - self.assertIsNotNone(fn, "sign-filter gating callback not registered") - options, value = fn("all") - self.assertEqual([o["value"] for o in options if o.get("disabled")], ["positive", "negative"]) - self.assertEqual(value, "all") - options, _ = fn(0) - self.assertFalse(any(o.get("disabled") for o in options)) - - def test_scatter_colouring_survives_all_classes(self): - # The scatter reads the same dropdown, where int("all") would raise. - app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) - fn = None - for key, spec in app.app.callback_map.items(): - if "scatter-plot.figure" in key: - fn = getattr(spec["callback"], "__wrapped__", spec["callback"]) - self.assertIsNotNone(fn, "scatter callback not registered") - self.assertIsNotNone(fn("word_contribution", ["common"], "all", False)) - - def test_number_boxes_commit_without_needing_a_blur(self): - # debounce=True commits only on Enter or blur, which leaves the spinner arrows looking - # dead: clicking one keeps focus in the box, so the chart never updates. A numeric - # (seconds) debounce commits after a pause instead. - layout = NlpWebApp(self._explanation(), engine=None).app.layout - for box_id in ("min-occurrences", "topk-input"): - debounce = layout[box_id].debounce - self.assertNotIsInstance(debounce, bool, f"{box_id} commits only on blur") - self.assertGreater(debounce, 0) - - def test_topk_input_replaces_the_slider(self): - found = set() - _collect_ids(NlpWebApp(self._explanation(), engine=None).app.layout, found) - self.assertIn("topk-input", found) - self.assertNotIn("topk-slider", found) - - def test_topk_controls_the_bar_count(self): - self.assertEqual(len(self._words(self._call(topk=2))), 2) - - def test_typed_topk_is_clamped_to_the_boxs_range(self): - from shapash.webapp.nlp_components.word_importance import _MAX_TOPK, _MIN_TOPK - - # The browser enforces min/max on the spinner but not on typed input. - self.assertEqual(len(self._words(self._call(topk=999))), 3) # only 3 words exist - self.assertEqual(len(self._words(self._call(topk=0))), _MIN_TOPK) - self.assertEqual(len(self._words(self._call(topk=-5))), _MIN_TOPK) - self.assertLessEqual(_MAX_TOPK, 50) - - def test_cleared_topk_box_falls_back_to_the_default(self): - from shapash.webapp.nlp_components.word_importance import _DEFAULT_TOPK - - self.assertEqual(_DEFAULT_TOPK, 20) - # Only None (an emptied box) restores the default — a typed 0 clamps instead. - self.assertEqual(len(self._words(self._call(topk=None))), 3) # all 3 words, under the cap - - def test_chart_keeps_its_computed_height_so_labels_survive(self): - # The panel scrolls; squeezing 50 words into it is what made plotly drop word labels. - fig = self._call(topk=50) - self.assertIsNotNone(fig.layout.height) - - def test_missing_class_prevents_update(self): - from dash.exceptions import PreventUpdate - - with self.assertRaises(PreventUpdate): - self.graph(None, 10, "all", [], "mean", 1, None, None, False, None) - - def test_generic_empty_explains_itself_when_neither_sign_nor_floor_is_the_cause(self): - # Excluding every word in the corpus empties the ranking without any sign filter or floor - # active — the fallback message names neither, unlike the two more specific reasons above. - fig = self.graph(0, 10, "all", ["rare", "common", "mild"], "mean", 1, None, None, False, None) - self.assertEqual(len(fig.data), 0) - self.assertIn("No word passes these filters in these 4 sample(s)", fig.layout.annotations[0].text) - - -class TestWordImportanceClickCallbacks(unittest.TestCase): - """Word-click store sync: a bar click or the clear button, and the clear button's own visibility.""" - - @staticmethod - def _app(): - return NlpWebApp(TestGlobalWordImportancePanel._explanation(), engine=None) - - def test_bar_click_publishes_the_word_and_resets_clickdata(self): - from unittest import mock - - from shapash.webapp.nlp_components import word_importance as word_importance_module - - app = self._app() - update = _callback(app, "data...global-importance-graph.clickData") - click_data = {"points": [{"y": "common"}]} - with mock.patch.object(word_importance_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] - words, reset = update(click_data, None) - self.assertEqual(words, ["common"]) - self.assertIsNone(reset) - - def test_clear_button_resets_the_store(self): - from unittest import mock - - from shapash.webapp.nlp_components import word_importance as word_importance_module - - app = self._app() - update = _callback(app, "data...global-importance-graph.clickData") - with mock.patch.object(word_importance_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "word-filter-clear-btn.n_clicks"}] - words, reset = update(None, 1) - self.assertIsNone(words) - self.assertIsNone(reset) - - def test_empty_click_prevents_update(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - from shapash.webapp.nlp_components import word_importance as word_importance_module - - app = self._app() - update = _callback(app, "data...global-importance-graph.clickData") - with mock.patch.object(word_importance_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "global-importance-graph.clickData"}] - with self.assertRaises(PreventUpdate): - update({"points": []}, None) - with self.assertRaises(PreventUpdate): - update(None, None) - - def test_toggle_clear_button_visibility(self): - app = self._app() - toggle = _callback(app, "word-filter-clear-btn.style") - self.assertEqual(toggle(None)["display"], "none") - self.assertEqual(toggle(["common"])["display"], "inline") - - -class TestWordImportanceScatterSync(unittest.TestCase): - """Word Importance and Scatter communicate only through the shared stores (Phase A extraction). - - A bar click always writes ``word-click-filter`` directly now, whether or not a scatter is - mounted (no more branching on the scatter's presence), and the scatter's own word dropdown - mirrors that store bidirectionally instead of being the "source of truth" only when it exists. - """ - - @staticmethod - def _explanation(): - return TestGlobalWordImportancePanel._explanation() - - def test_bar_click_registered_without_a_scatter(self): - app = NlpWebApp(self._explanation(), engine=None) - pairs = _callback_binding_ids(app, "global-importance-graph.clickData") - self.assertIn(("global-importance-graph", "clickData"), pairs) - self.assertIn(("word-filter-clear-btn", "n_clicks"), pairs) - - def test_bar_click_registered_with_a_scatter_too(self): - # Before Phase A, a mounted scatter changed which store a bar click targeted; now Word - # Importance always registers this callback unconditionally, regardless of scatter presence. - app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) - pairs = _callback_binding_ids(app, "global-importance-graph.clickData") - self.assertIn(("global-importance-graph", "clickData"), pairs) - - def test_scatter_word_select_mirrors_the_shared_store(self): - app = NlpWebApp(self._explanation(), engine=None, projection=np.zeros((4, 2))) - pairs = _callback_binding_ids(app, "scatter-word-select.value") - self.assertIn(("word-click-filter", "data"), pairs) - - def test_class_selector_publishes_to_the_active_class_store(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "active-class-store.data") - self.assertEqual(fn(1), 1) - # "All classes" (the string sentinel) is not itself a class index — falls back to 0. - self.assertEqual(fn("all"), 0) - - -class TestTableFilterIntersection(unittest.TestCase): - """The dataset table's active filters (error cell, word click, errors-only) must intersect for - real. A filter that empties the combination has to actually empty the table, not be silently - dropped while the summary still claims it is applied — see the reported bug where clicking a - Word Importance bar and then an Error Analysis cell with no matching rows kept the word filter - in the summary text while showing every row from the cell, unfiltered. - """ - - @staticmethod - def _explanation(): - texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) - token_strings = [t.split() for t in texts] - values = [np.random.randn(len(toks), 2) for toks in token_strings] - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=np.zeros((4, 2)), - y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), - y_prob=None, - y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="point", - output_space="probability", - ) - - def test_word_filter_after_error_cell_narrows_instead_of_being_dropped(self): - # "happy" only occurs in row 0 ("i am happy"); the clicked cell (pred=pos, true=neg) holds - # only row 1 ("so glad"). The intersection is empty and must stay empty. - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "dataset-table.rowData") - error_cell = {"pred": 1, "true": 0, "indices": [1]} - rows, selected, _title, summary = fn(None, ["happy"], False, error_cell, None, None) - self.assertEqual(rows, []) - self.assertEqual(selected, []) - self.assertIn('containing "happy"', summary) - self.assertIn("(0 of 4)", summary) - - def test_word_filter_matching_the_cell_still_narrows(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "dataset-table.rowData") - error_cell = {"pred": 0, "true": 1, "indices": [3]} # row 3: "very sad" - rows, selected, _title, summary = fn(None, ["sad"], False, error_cell, None, None) - self.assertEqual([r["text"] for r in rows], ["very sad"]) - self.assertEqual(selected, rows) - self.assertIn("(1 of 4)", summary) - - def test_errors_only_narrows_a_word_filter_too(self): - # "happy" only occurs in row 0, a correct prediction — errors_only must not silently drop - # the word filter to avoid an empty table. - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "dataset-table.rowData") - rows, _selected, _title, summary = fn(None, ["happy"], True, None, None, None) - self.assertEqual(rows, []) - self.assertIn("model errors only", summary) - self.assertIn('containing "happy"', summary) - - def test_word_filter_matches_the_exact_token_not_a_substring(self): - # "unhappy" contains "happy" as a substring but is a different token — a bar labeled - # "happy" must not pull it in, matching the exact-token semantics word_importance()/the - # scatter's word-contribution coloring already use. - texts = pd.Series(["i am happy", "so unhappy today"]) - token_strings = [t.split() for t in texts] - values = [np.random.randn(len(toks), 2) for toks in token_strings] - explanation = NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=np.zeros((2, 2)), - y_pred=pd.Series(["pos", "neg"], name="prediction"), - y_prob=None, - y_true=None, - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="point", - output_space="probability", - ) - app = NlpWebApp(explanation, engine=None) - fn = _callback(app, "dataset-table.rowData") - rows, _selected, _title, _summary = fn(None, ["happy"], False, None, None, None) - self.assertEqual([r["text"] for r in rows], ["i am happy"]) - - def test_grid_filter_narrows_the_shown_count_without_touching_rowData(self): - # AG Grid's own column filter runs client-side on top of whatever rowData filter_table - # hands it — it must not change rowData/selectedRows, only the reported count, so the - # grid's filtered view and the "Showing:" summary agree without a second, disconnected - # counter (the earlier bug this mirrors: a filter description without a matching count). - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "dataset-table.rowData") - filter_model = {"text": {"filterType": "text", "type": "contains", "filter": "happy"}} - virtual_row_data = [{"text": "i am happy"}] - rows, _selected, _title, summary = fn(None, None, False, None, filter_model, virtual_row_data) - self.assertEqual(len(rows), 4) # rowData is untouched — the grid does its own filtering - self.assertIn("grid filter: Text", summary) - self.assertIn("(1 of 4)", summary) - - -def _callback(app, out_substr): - for key, spec in app.app.callback_map.items(): - if out_substr in key: - return getattr(spec["callback"], "__wrapped__", spec["callback"]) - raise KeyError(out_substr) - - -class TestGridFilterClearButton(unittest.TestCase): - """AG Grid's own column filters run client-side, outside filter_table's inputs — the clear - button is the only thing that lets a user drop one without reaching into the grid's own header - UI, so its show/hide and reset behavior need their own coverage. The filter's *description* is - covered by filter_table itself (see TestTableFilterIntersection), since it's folded into the - same "Showing:" summary rather than a separate badge. - """ - - @staticmethod - def _explanation(): - texts = pd.Series(["i am happy", "so glad", "this is bad", "very sad"]) - token_strings = [t.split() for t in texts] - values = [np.random.randn(len(toks), 2) for toks in token_strings] - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=np.zeros((4, 2)), - y_pred=pd.Series(["pos", "pos", "neg", "neg"], name="prediction"), - y_prob=None, - y_true=pd.Series(["pos", "neg", "neg", "pos"], name="ground_truth"), - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="point", - output_space="probability", - ) - - def test_no_filter_model_hides_the_clear_button(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "grid-filter-clear-btn.style") - self.assertEqual(fn(None)["display"], "none") - - def test_active_filter_model_shows_the_clear_button(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "grid-filter-clear-btn.style") - filter_model = {"prediction": {"filterType": "text", "type": "equals", "filter": "pos"}} - self.assertNotEqual(fn(filter_model).get("display"), "none") - - def test_clear_button_resets_the_grid_filter_model(self): - app = NlpWebApp(self._explanation(), engine=None) - fn = _callback(app, "dataset-table.filterModel") - self.assertEqual(fn(1), {}) - - -class TestScatterComponentWordContributionOption(unittest.TestCase): - """``offer_word_contribution`` gates the "Word contribution" color mode, not the whole panel.""" - - @staticmethod - def _explanation(): - return TestGlobalWordImportancePanel._explanation() - - def _color_options(self, offer_word_contribution: bool): - from shapash.webapp.nlp_components import ScatterComponent - - comp = ScatterComponent(offer_word_contribution=offer_word_contribution) - explanation = self._explanation() - layout = comp.layout(_ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2)))) - found = {} - - def walk(node): - cid = getattr(node, "id", None) - if isinstance(cid, str): - found[cid] = node - children = getattr(node, "children", None) - for ch in children if isinstance(children, (list, tuple)) else [children]: - if ch is not None and not isinstance(ch, str): - walk(ch) - - walk(layout) - return [opt["value"] for opt in found["color-by"].options] - - def test_offered_when_word_importance_is_mounted(self): - self.assertIn("word_contribution", self._color_options(offer_word_contribution=True)) - - def test_omitted_without_word_importance(self): - # Scatter still works standalone (Prediction/Ground-Truth) — it just cannot source a class - # index for word-contribution coloring without Word Importance's control. - options = self._color_options(offer_word_contribution=False) - self.assertNotIn("word_contribution", options) - self.assertIn("prediction", options) - - -class TestBuildScatterFig(unittest.TestCase): - """``_build_scatter_fig``'s own dispatch — the part left after extracting the drawing itself - into :func:`~shapash.plots.plot_scatter.plot_scatter`.""" - - @staticmethod - def _explanation(with_true: bool, with_pred: bool): - texts = pd.Series(["rare common", "common"]) - return NlpExplanation( - texts=texts, - token_strings=[["rare", "common"], ["common"]], - values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], - base_values=None, - y_pred=pd.Series(["neg", "pos"], index=texts.index) if with_pred else None, - y_prob=None, - y_true=pd.Series(["pos", "pos"], index=texts.index) if with_true else None, - label_names=None, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - def _component(self): - from shapash.webapp.nlp_components import ScatterComponent - - return ScatterComponent(offer_word_contribution=True) - - def _ctx_for(self, explanation): - # The figure takes its two coordinate columns from the projection and every visual encoding - # — colours, legend, hover, error mask — from the explanation, so both have to be bound. - return _ctx(explanation, None, coords=np.zeros((explanation.n_samples, 2))) - - def test_ground_truth_colors_by_y_true(self): - fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(True, True)), "ground_truth") - names = {trace.name for trace in fig.data} - self.assertEqual(names, {"pos"}) # both samples are "pos" in y_true - - def test_without_prediction_or_ground_truth_draws_a_single_unlabeled_group(self): - fig = self._component()._build_scatter_fig(self._ctx_for(self._explanation(False, False)), "prediction") - self.assertEqual(len(fig.data), 1) - self.assertEqual(len(fig.data[0].x), 2) - - -class TestScatterComponentCallbacks(unittest.TestCase): - """The scatter panel's own callbacks: color-mode/word-list sync and box/lasso/click selection.""" - - @staticmethod - def _explanation(): - texts = pd.Series(["rare common", "common"]) - return NlpExplanation( - texts=texts, - token_strings=[["rare", "common"], ["common"]], - values=[np.array([[0.9, -0.9], [0.3, -0.3]]), np.array([[0.3, -0.3]])], - base_values=None, - y_pred=pd.Series(["neg", "pos"], index=texts.index, name="prediction"), - y_prob=None, - y_true=pd.Series(["pos", "pos"], index=texts.index, name="ground_truth"), - label_names=None, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - def _app(self): - return NlpWebApp(self._explanation(), engine=None, projection=np.zeros((2, 2))) - - def test_ground_truth_option_offered_when_available(self): - app = self._app() - # Walk the layout for the color-by dropdown directly, mirroring TestScatterComponentWordContributionOption. - found = {} - - def walk(node): - cid = getattr(node, "id", None) - if isinstance(cid, str): - found[cid] = node - children = getattr(node, "children", None) - for ch in children if isinstance(children, (list, tuple)) else [children]: - if ch is not None and not isinstance(ch, str): - walk(ch) - - walk(app.app.layout) - dropdown = found["color-by"] - self.assertIn("ground_truth", [opt["value"] for opt in dropdown.options]) - - def test_sync_color_by_switches_to_word_contribution_when_words_selected(self): - app = self._app() - sync = _callback(app, "color-by.value") - self.assertEqual(sync(["common"]), "word_contribution") - - def test_sync_color_by_prevents_update_when_words_cleared(self): - from dash.exceptions import PreventUpdate - - app = self._app() - sync = _callback(app, "color-by.value") - with self.assertRaises(PreventUpdate): - sync([]) - with self.assertRaises(PreventUpdate): - sync(None) - - def test_follow_word_click_mirrors_a_single_word_as_a_list(self): - app = self._app() - follow = _callback(app, "scatter-word-select.value") - self.assertEqual(follow("common"), ["common"]) - - def test_follow_word_click_passes_through_a_list_and_clears_on_falsy(self): - app = self._app() - follow = _callback(app, "scatter-word-select.value") - self.assertEqual(follow(["common", "rare"]), ["common", "rare"]) - self.assertEqual(follow(None), []) - - def test_word_filter_from_scatter_publishes_to_the_shared_store(self): - app = self._app() - publish = _callback(app, "word-click-filter.data@") - self.assertEqual(publish(["common"]), ["common"]) - self.assertIsNone(publish([])) - self.assertIsNone(publish(None)) - - def test_toggle_word_select_visibility_follows_color_mode(self): - app = self._app() - toggle = _callback(app, "scatter-word-select.style") - self.assertNotEqual(toggle("word_contribution").get("display"), "none") - self.assertEqual(toggle("prediction")["display"], "none") - - def test_update_scatter_selection_clear_button_resets(self): - from unittest import mock - - from shapash.webapp.nlp_components import scatter as scatter_module - - app = self._app() - update = _callback(app, "scatter-selected-indices.data") - with mock.patch.object(scatter_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "scatter-clear-btn.n_clicks"}] - self.assertIsNone(update(None, None, 1)) - - def test_update_scatter_selection_from_a_click(self): - from unittest import mock - - from shapash.webapp.nlp_components import scatter as scatter_module - - app = self._app() - update = _callback(app, "scatter-selected-indices.data") - click_data = {"points": [{"customdata": [1]}]} - with mock.patch.object(scatter_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "scatter-plot.clickData"}] - self.assertEqual(update(None, click_data, None), [1]) - - def test_update_scatter_selection_ignores_an_empty_click(self): - from unittest import mock - - from shapash.webapp.nlp_components import scatter as scatter_module - - app = self._app() - update = _callback(app, "scatter-selected-indices.data") - with mock.patch.object(scatter_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "scatter-plot.clickData"}] - self.assertIsNone(update(None, {"points": []}, None)) - self.assertIsNone(update(None, None, None)) - - def test_update_scatter_selection_ignores_an_empty_box_select_as_a_recolor_echo(self): - from dash.exceptions import PreventUpdate - - from unittest import mock - - from shapash.webapp.nlp_components import scatter as scatter_module - - app = self._app() - update = _callback(app, "scatter-selected-indices.data") - with mock.patch.object(scatter_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] - with self.assertRaises(PreventUpdate): - update({"points": []}, None, None) - - def test_update_scatter_selection_from_a_box_select(self): - from unittest import mock - - from shapash.webapp.nlp_components import scatter as scatter_module - - app = self._app() - update = _callback(app, "scatter-selected-indices.data") - selected_data = {"points": [{"customdata": [0]}, {"customdata": [1]}]} - with mock.patch.object(scatter_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "scatter-plot.selectedData"}] - self.assertEqual(update(selected_data, None, None), [0, 1]) - - def test_toggle_clear_button_visibility(self): - app = self._app() - toggle = _callback(app, "scatter-clear-btn.style") - self.assertEqual(toggle(None)["display"], "none") - self.assertEqual(toggle([0, 1])["display"], "inline") - - -def _make_local_panel_explanation(): - """Two samples, two classes — enough for the sentence-highlight / waterfall panels.""" - texts = pd.Series(["i am happy", "so sad"]) - return NlpExplanation( - texts=texts, - token_strings=[["i", "am", "happy"], ["so", "sad"]], - values=[np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), np.array([[-0.3, 0.3], [-0.5, 0.5]])], - base_values=np.array([[0.0, 0.0], [0.0, 0.0]]), - y_pred=pd.Series(["pos", "neg"], index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=LABEL_NAMES, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - -def _make_local_panel_datapoint(label="pos"): - return pack_datapoint( - text="i am happy", - orig_idx=0, - tokens=["i", "am", "happy"], - values=np.array([[0.1, -0.1], [0.2, -0.2], [0.4, -0.4]]), - base_values=np.array([0.0, 0.0]), - label=label, - ) - - -class TestSentenceHighlightComponentCallbacks(unittest.TestCase): - """The Sentence panel's own callbacks: class-picker sync to the prediction, and the render.""" - - def _app(self): - return NlpWebApp(_make_local_panel_explanation(), engine=None) - - def test_sync_local_class_prevents_update_without_a_datapoint(self): - from dash.exceptions import PreventUpdate - - app = self._app() - sync = _callback(app, "local-class-selector.value") - with self.assertRaises(PreventUpdate): - sync(None) - with self.assertRaises(PreventUpdate): - sync({"text": "i am happy", "label": None}) - - def test_sync_local_class_prevents_update_for_an_unknown_label(self): - from dash.exceptions import PreventUpdate - - app = self._app() - sync = _callback(app, "local-class-selector.value") - with self.assertRaises(PreventUpdate): - sync(_make_local_panel_datapoint(label="not-a-real-class")) - - def test_sync_local_class_resolves_the_predicted_label_to_its_index(self): - app = self._app() - sync = _callback(app, "local-class-selector.value") - self.assertEqual(sync(_make_local_panel_datapoint(label="pos")), LABEL_NAMES.index("pos")) - - def test_update_sentence_highlight_prevents_update_without_datapoint_or_class(self): - from dash.exceptions import PreventUpdate - - app = self._app() - update = _callback(app, "sentence-highlight.children") - with self.assertRaises(PreventUpdate): - update(None, 0) - with self.assertRaises(PreventUpdate): - update(_make_local_panel_datapoint(), None) - - def test_update_sentence_highlight_renders_for_a_valid_datapoint(self): - from dash import html - - app = self._app() - update = _callback(app, "sentence-highlight.children") - result = update(_make_local_panel_datapoint(), 1) - self.assertIsInstance(result, html.Div) - - -class TestWaterfallComponentCallbacks(unittest.TestCase): - """The Waterfall panel's own callback: current datapoint + class + threshold -> figure.""" - - def _app(self): - return NlpWebApp(_make_local_panel_explanation(), engine=None) - - def test_update_waterfall_prevents_update_without_datapoint_or_class(self): - from dash.exceptions import PreventUpdate - - app = self._app() - update = _callback(app, "waterfall-graph.figure") - with self.assertRaises(PreventUpdate): - update(None, 0, 10) - with self.assertRaises(PreventUpdate): - update(_make_local_panel_datapoint(), None, 10) - - def test_update_waterfall_renders_and_titles_by_class(self): - import plotly.graph_objs as go - - app = self._app() - update = _callback(app, "waterfall-graph.figure") - fig = update(_make_local_panel_datapoint(), 1, 10) - self.assertIsInstance(fig, go.Figure) - self.assertIn(LABEL_NAMES[1], fig.layout.title.text) - - def test_update_waterfall_defaults_the_threshold_when_missing(self): - app = self._app() - update = _callback(app, "waterfall-graph.figure") - # None threshold (e.g. before the slider ever fires) must not raise — falls back to 10%. - fig = update(_make_local_panel_datapoint(), 0, None) - self.assertIsNotNone(fig) - - -class TestCounterfactualComponentCallbacks(unittest.TestCase): - """The generate/apply wiring: method-group toggling, the Generate click, and per-row Apply.""" - - def _app(self, engine=None): - engine = engine or FakeEngine(can_edit=True, can_cf=True) - return NlpWebApp(engine.to_explanation(), engine=engine), engine - - def test_toggle_controls_shows_only_the_selected_generators_group(self): - app, _ = self._app() - toggle = _callback(app, "counterfactual-cfg-group-hotflip.style") - hotflip_style, ablation_style = toggle("hotflip") - self.assertNotEqual(hotflip_style.get("display"), "none") - self.assertEqual(ablation_style.get("display"), "none") - - # generate(n_clicks, datapoint, selected_gen, *config_values) — 6 states: (num_examples, - # max_flips/max_ablations, tokens_to_ignore) for each of FakeEngine's two generators, in order. - _CONFIG_VALUES = (5, 3, "", 5, 3, "") - - def test_generate_prevents_update_without_a_click(self): - from dash.exceptions import PreventUpdate - - app, _ = self._app() - generate = _callback(app, "counterfactual-results.children") - with self.assertRaises(PreventUpdate): - generate(None, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) - - def test_generate_prevents_update_without_text(self): - from dash.exceptions import PreventUpdate - - app, _ = self._app() - generate = _callback(app, "counterfactual-results.children") - with self.assertRaises(PreventUpdate): - generate(1, None, "hotflip", *self._CONFIG_VALUES) - with self.assertRaises(PreventUpdate): - generate(1, {"text": " "}, "hotflip", *self._CONFIG_VALUES) - - def test_generate_returns_a_results_table_on_success(self): - app, engine = self._app() - generate = _callback(app, "counterfactual-results.children") - children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) - self.assertIsNotNone(children) - self.assertEqual(texts, [cf.new_text for cf in engine.generate_counterfactuals("i am happy")]) - - def test_generate_reports_when_no_counterfactual_is_found(self): - class _NoResultsEngine(FakeEngine): - def generate_counterfactuals(self, text, config=None, generator=None): - return [] - - app, _ = self._app(_NoResultsEngine(can_edit=True, can_cf=True)) - generate = _callback(app, "counterfactual-results.children") - children, texts = generate(1, {"text": "i am happy"}, "hotflip", *self._CONFIG_VALUES) - self.assertIn("No counterfactual found", children.children) - self.assertEqual(texts, []) - - def test_apply_prevents_update_without_any_click(self): - from dash.exceptions import PreventUpdate - - app, _ = self._app() - apply_fn = _callback(app, "whatif-apply-store.data") - with self.assertRaises(PreventUpdate): - apply_fn([None], ["a new text"]) - - def test_apply_prevents_update_on_out_of_range_index(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - from shapash.webapp.nlp_components import counterfactual as counterfactual_module - - app, _ = self._app() - apply_fn = _callback(app, "whatif-apply-store.data") - with mock.patch.object(counterfactual_module, "callback_context") as cc: - cc.triggered_id = {"type": "counterfactual-apply", "index": 5} - with self.assertRaises(PreventUpdate): - apply_fn([1], ["only one text"]) - - def test_apply_publishes_the_chosen_text_and_datapoint(self): - from unittest import mock - - from shapash.webapp.nlp_components import counterfactual as counterfactual_module - - app, engine = self._app() - apply_fn = _callback(app, "whatif-apply-store.data") - with mock.patch.object(counterfactual_module, "callback_context") as cc: - cc.triggered_id = {"type": "counterfactual-apply", "index": 0} - text, datapoint = apply_fn([1], ["a new counterfactual text"]) - self.assertEqual(text, "a new counterfactual text") - self.assertEqual(datapoint["text"], "a new counterfactual text") - self.assertEqual(datapoint["label"], "pos") # FakeEngine.explain_text always returns "pos" - - -class TestDataEditorComponentCallbacks(unittest.TestCase): - """The editor's own callbacks: prefill from a row/applied counterfactual, and Predict.""" - - def _app(self): - engine = FakeEngine(can_edit=True, can_cf=False) - return NlpWebApp(engine.to_explanation(), engine=engine) - - def test_prefill_prevents_update_without_a_row_or_an_applied_text(self): - from unittest import mock - - from dash.exceptions import PreventUpdate - from shapash.webapp.nlp_components import data_editor as data_editor_module - - app = self._app() - prefill = _callback(app, "data-editor-input.value") - with mock.patch.object(data_editor_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] - with self.assertRaises(PreventUpdate): - prefill(None, None) - - def test_prefill_from_a_selected_row(self): - from unittest import mock - - from shapash.webapp.nlp_components import data_editor as data_editor_module - - app = self._app() - prefill = _callback(app, "data-editor-input.value") - with mock.patch.object(data_editor_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "dataset-table.selectedRows"}] - self.assertEqual(prefill([{"text": "a selected row"}], None), "a selected row") - - def test_prefill_from_an_applied_counterfactual(self): - from unittest import mock - - from shapash.webapp.nlp_components import data_editor as data_editor_module - - app = self._app() - prefill = _callback(app, "data-editor-input.value") - with mock.patch.object(data_editor_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "whatif-apply-store.data"}] - self.assertEqual(prefill(None, "an applied counterfactual"), "an applied counterfactual") - - def test_predict_prevents_update_without_a_click_or_text(self): - from dash.exceptions import PreventUpdate - - app = self._app() - predict = _callback(app, "data-editor-prob.figure") - with self.assertRaises(PreventUpdate): - predict(None, "some text") - with self.assertRaises(PreventUpdate): - predict(1, "") - with self.assertRaises(PreventUpdate): - predict(1, " ") - - -class TestWordProfileControls(unittest.TestCase): - """The picker's sort/labels, the restricted aggregation list, and bar-click class ranking.""" - - def _layout(self): - from shapash.webapp.nlp_components import WordProfileComponent - - comp = WordProfileComponent() - layout = comp.layout(_ctx(TestWordProfileComponent._explanation(), None)) - found = {} - - def walk(node): - cid = getattr(node, "id", None) - if isinstance(cid, str): - found[cid] = node - children = getattr(node, "children", None) - for ch in children if isinstance(children, (list, tuple)) else [children]: - if ch is not None and not isinstance(ch, str): - walk(ch) - - walk(layout) - return comp, found - - def _app(self): - import dash - - from shapash.webapp.nlp_components import WordProfileComponent - - comp = WordProfileComponent() - explanation = TestWordProfileComponent._explanation() - comp.layout(_ctx(explanation, None)) # builds the option lists the sort callback serves - app = dash.Dash(__name__) - comp.register_callbacks(app, _ctx(explanation, None), TestWordProfileComponent.STORES) - return app - - @staticmethod - def _callback(app, out_substr): - for key, spec in app.callback_map.items(): - if out_substr in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError(out_substr) - - def test_sum_aggregations_are_not_offered(self): - # Per word, a sum is the mean rescaled by the occurrence count — same bars, no information. - _, found = self._layout() - self.assertEqual([o["value"] for o in found["word-profile-agg"].options], ["mean", "mean_abs"]) - - def test_options_carry_the_occurrence_count(self): - _, found = self._layout() - labels = {o["value"]: o["label"] for o in found["word-profile-select"].options} - self.assertEqual(labels["happy"], "happy (4)") - - def test_option_values_stay_bare_words(self): - # The label carries the count; the value must not, or every consumer of it breaks. - _, found = self._layout() - self.assertIn("happy", [o["value"] for o in found["word-profile-select"].options]) - - def test_sort_toggle_reorders_without_changing_values(self): - app = self._app() - reorder = self._callback(app, "word-profile-select.options") - alpha = [o["value"] for o in reorder("alpha")] - freq = [o["value"] for o in reorder("frequency")] - self.assertEqual(alpha, sorted(alpha)) - self.assertEqual(freq[0], "happy") # the most frequent word - self.assertEqual(set(alpha), set(freq)) - - def test_unknown_sort_falls_back_to_alphabetical(self): - app = self._app() - reorder = self._callback(app, "word-profile-select.options") - self.assertEqual(reorder(None), reorder("alpha")) - - def test_bar_click_sets_the_drill_down_class(self): - app = self._app() - click = self._callback(app, "word-profile-class.value") - value, reset = click({"points": [{"customdata": [1], "y": "pos"}]}) - self.assertEqual(value, 1) - # clickData is reset so clicking the same bar twice re-fires. - self.assertIsNone(reset) - - def test_bar_click_reads_the_index_not_the_label(self): - app = self._app() - click = self._callback(app, "word-profile-class.value") - # Duplicate display names must not be able to mis-resolve the class. - self.assertEqual(click({"points": [{"customdata": [0], "y": "neg"}]})[0], 0) - - def test_graph_carries_the_class_index_on_every_bar(self): - app = self._app() - fig = self._callback(app, "word-profile-graph")("happy", "mean", 0, "most", 5, None, None, False)[0] - self.assertEqual([c[0] for c in fig.data[0].customdata], [1, 0]) # reversed for drawing - - def test_empty_clicks_are_ignored(self): - from dash.exceptions import PreventUpdate - - app = self._app() - click = self._callback(app, "word-profile-class.value") - for bad in (None, {}, {"points": []}, {"points": [{}]}): - with self.assertRaises(PreventUpdate): - click(bad) From 0e77a56c40880232bd7b972a164388a7d608f9c4 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 17:45:52 +0200 Subject: [PATCH 118/144] test(nlp): split explainer/backend/compute coverage out of test_nlp_prototype test_nlp_prototype.py mixed explainer, backend, and compute-layer tests in one file. Move NlpExplainer/caching/label-noise coverage to tests/unit_tests/explainer/test_nlp_explainer.py, NlpLimeBackend to tests/unit_tests/backend/test_nlp_lime_backend.py, and embedding-projection coverage into the existing tests/unit_tests/compute/test_embeddings.py. Word-importance/vocabulary coverage on NlpExplanation folds into the existing tests/unit_tests/nlp/test_nlp_explanation.py and test_nlp_plotter.py. Renamed a few incoming fixtures (e.g. _make_word_explanation) to avoid colliding with same-named, differently-shaped fixtures already in those files. Pure code movement; full suite pass count verified unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- .../backend/test_nlp_lime_backend.py | 165 ++++ tests/unit_tests/compute/test_embeddings.py | 234 ++++++ .../explainer/test_nlp_explainer.py | 790 ++++++++++++++++++ tests/unit_tests/nlp/test_nlp_explanation.py | 777 ++++++++++++++++- tests/unit_tests/nlp/test_nlp_plotter.py | 65 ++ 5 files changed, 2030 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/backend/test_nlp_lime_backend.py create mode 100644 tests/unit_tests/explainer/test_nlp_explainer.py diff --git a/tests/unit_tests/backend/test_nlp_lime_backend.py b/tests/unit_tests/backend/test_nlp_lime_backend.py new file mode 100644 index 00000000..0b507da5 --- /dev/null +++ b/tests/unit_tests/backend/test_nlp_lime_backend.py @@ -0,0 +1,165 @@ +"""Unit tests for ``NlpLimeBackend`` — LIME-based word attribution for NLP models.""" + +import unittest + +import numpy as np + +from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.backend.nlp_lime_backend import NlpLimeBackend + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + +# Keep num_samples tiny so LIME tests run fast in CI. +_LIME_COMPUTE_ARGS = {"num_samples": 50, "num_features": 5} +_SAMPLE_TEXTS = ["i feel so happy today", "this is terrible and sad"] + + +def _fake_classifier(texts: list[str]) -> np.ndarray: + """Deterministic fake classifier returning (n_texts, N_CLASSES) probabilities.""" + rng = np.random.default_rng(0) + probs = rng.random((len(texts), N_CLASSES)).astype(np.float32) + probs /= probs.sum(axis=1, keepdims=True) + return probs + + +def _make_lime_backend() -> NlpLimeBackend: + return NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + + +class TestNlpLimeBackend(unittest.TestCase): + def setUp(self): + self.backend = _make_lime_backend() + + # --- init / config --- + + def test_name(self): + self.assertEqual(self.backend.name, "nlp_lime") + + def test_inherits_nlp_backend(self): + self.assertIsInstance(self.backend, NlpBackend) + + def test_label_names_stored(self): + self.assertEqual(self.backend._classes, LABEL_NAMES) + + def test_mask_string_stored(self): + backend = NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + mask_string="[MASK]", + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + self.assertEqual(backend.mask_string, "[MASK]") + + def test_explainer_args_forwarded(self): + backend = NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + explainer_args={"bow": False}, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + self.assertFalse(backend.explainer.bow) + + # --- _classifier_fn --- + + def test_classifier_fn_converts_list_to_array(self): + def list_model(texts): + return [[0.5] * N_CLASSES for _ in texts] + + backend = NlpLimeBackend(list_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["hello"]) + self.assertIsInstance(result, np.ndarray) + self.assertEqual(result.shape, (1, N_CLASSES)) + + def test_classifier_fn_passes_through_array(self): + arr = np.ones((2, N_CLASSES), dtype=np.float32) + + def array_model(texts): + return arr + + backend = NlpLimeBackend(array_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["a", "b"]) + self.assertIs(result, arr) + + def test_classifier_fn_converts_hf_pipeline_format(self): + # HuggingFace pipeline with return_all_scores=True returns list[list[dict]]. + def hf_model(texts): + return [[{"label": name, "score": 1.0 / N_CLASSES} for name in LABEL_NAMES] for _ in texts] + + backend = NlpLimeBackend(hf_model, label_names=LABEL_NAMES) + result = backend._classifier_fn(["hello", "world"]) + self.assertIsInstance(result, np.ndarray) + self.assertEqual(result.shape, (2, N_CLASSES)) + self.assertEqual(result.dtype, np.float64) + # Each score should be 1/N_CLASSES + np.testing.assert_allclose(result, 1.0 / N_CLASSES, atol=1e-6) + + # --- run_explainer --- + + def test_run_explainer_returns_nlp_contributions(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertIsInstance(raw, NlpContributions) + + def test_run_explainer_contributions_count(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(len(raw.values), len(_SAMPLE_TEXTS)) + + def test_run_explainer_contributions_shape(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + for arr in raw.values: + self.assertEqual(arr.ndim, 2) + self.assertEqual(arr.shape[1], N_CLASSES) + + def test_run_explainer_base_values_shape(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(raw.base_values.shape, (len(_SAMPLE_TEXTS), N_CLASSES)) + + def test_run_explainer_data_is_word_list(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertEqual(len(raw.token_strings), len(_SAMPLE_TEXTS)) + for word_list in raw.token_strings: + self.assertIsInstance(word_list, list) + self.assertTrue(all(isinstance(w, str) for w in word_list)) + + def test_run_explainer_sparse_weights(self): + # LIME fills at most num_features non-zero weights per label column. + raw = self.backend.run_explainer(_SAMPLE_TEXTS[:1]) + matrix = raw.values[0] + for col in range(N_CLASSES): + self.assertLessEqual( + np.count_nonzero(matrix[:, col]), + _LIME_COMPUTE_ARGS["num_features"], + ) + + # --- get_local_contributions --- + + def test_get_local_contributions_returns_nlp_contributions(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + self.assertIsInstance(contrib, NlpContributions) + + def test_get_local_contributions_token_strings_match_data(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + self.assertEqual(contrib.token_strings, raw.token_strings) + + def test_get_local_contributions_values_match_contributions(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) + for got, expected in zip(contrib.values, raw.values): + np.testing.assert_array_equal(got, expected) + + def test_get_local_contributions_subset(self): + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw, subset=[0]) + self.assertEqual(len(contrib.token_strings), 1) + self.assertEqual(len(contrib.values), 1) + self.assertEqual(contrib.base_values.shape[0], 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/compute/test_embeddings.py b/tests/unit_tests/compute/test_embeddings.py index 35a45487..dcaef747 100644 --- a/tests/unit_tests/compute/test_embeddings.py +++ b/tests/unit_tests/compute/test_embeddings.py @@ -9,8 +9,12 @@ from types import SimpleNamespace import numpy as np +import pandas as pd from shapash.compute.embeddings import Embedding, projection_coords +from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.model.base import SupportsEmbeddings, SupportsTokenization, TextModel class _Scaler: @@ -163,5 +167,235 @@ def test_a_bare_array_is_accepted_on_shape_alone(self): projection_coords(np.zeros((3, 2)), self.explanation) +# --------------------------------------------------------------------------- +# NlpExplainer.compute_projection / compute_embeddings / fit — reducer wiring +# and embedding precompute at fit time +# --------------------------------------------------------------------------- + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] + + +class _ProjectableModel(TextModel, SupportsEmbeddings): + """Embeds each text to a deterministic 3-D point; counts calls so cache hits are observable.""" + + def __init__(self, space="decision"): + super().__init__(label_names=LABEL_NAMES[:2]) + self.space = space + self.calls = 0 + + def resolve_space(self, space=None): + return space if space is not None else self.space + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def get_embedding_table(self): + return (["a"], np.zeros((1, 3))) + + def embed(self, texts, space=None): + self.calls += 1 + return np.array([[float(len(t)), float(t.count("a")), 1.0] for t in texts]) + + @property + def shap_callable(self): + return self.predict + + +class _CountingReducer: + """A deterministic reducer that counts its fits and records its call-time kwargs.""" + + def __init__(self, scale=1.0): + self.scale = scale + self.fits = 0 + self.last_kwargs = None + + def fit_transform(self, x, **kwargs): + self.fits += 1 + self.last_kwargs = kwargs + return np.asarray(x)[:, :2] * self.scale + + +class _TokenizeOnlyModel(TextModel, SupportsTokenization): + """Tokenizable but gradient-free — only AblationFlip is compatible.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + @property + def shap_callable(self): + return self.predict + + +def _projection_explanation(texts: pd.Series) -> NlpExplanation: + """A minimal ``NlpExplanation`` carrying only ``texts`` — all ``compute_projection`` needs.""" + n = len(texts) + return NlpExplanation( + texts=texts, + token_strings=[[] for _ in range(n)], + values=[np.zeros((0, 2)) for _ in range(n)], + base_values=np.zeros((n, 2)), + y_pred=pd.Series(["pos"] * n, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=None, + backend_name="test", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +class TestComputeProjection(unittest.TestCase): + """The library owns the space + the embedding cache; the caller injects only the reducer.""" + + def setUp(self): + self.model = _ProjectableModel() + self.xpl = NlpExplainer(self.model, backend=object()) + self.explanation = _projection_explanation(pd.Series(["alpha", "beta banana", "gamma"])) + + def test_returns_two_columns_aligned_with_the_texts(self): + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.vectors.shape, (3, 2)) + self.assertEqual(projection.n_samples, self.explanation.n_samples) + + def test_defaults_to_pca_without_any_extra_dependency(self): + """The default reducer must be something a core install already has — sklearn's PCA.""" + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.n_components, 2) + self.assertEqual(self.model.calls, 1) + + def test_injected_reducer_is_used(self): + reducer = _CountingReducer(scale=2.0) + projection = self.xpl.compute_projection(self.explanation, reducer=reducer) + self.assertEqual(reducer.fits, 1) + np.testing.assert_allclose(projection.vectors[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled + + def test_projection_carries_the_provenance_needed_to_pair_it_back(self): + # The point of returning an Embedding rather than a bare array: the coordinates say which + # corpus, model and space they belong to, so a mismatch can be caught instead of drawn. + projection = self.xpl.compute_projection(self.explanation) + self.assertEqual(projection.corpus_id, self.explanation.corpus_id) + self.assertEqual(projection.model_id, self.model.model_id) + self.assertEqual(projection.space, self.model.resolve_space()) + self.assertEqual(projection.reducer_tag, "pca") + + def test_embeddings_are_raw_and_reusable_for_several_projections(self): + # The flow the split exists for: embed once (needs the model), reduce many times (does not). + embedding = self.xpl.compute_embeddings(self.explanation) + self.assertEqual(self.model.calls, 1) + self.assertIsNone(embedding.reducer_tag) + self.assertEqual(embedding.corpus_id, self.explanation.corpus_id) + + first = embedding.project(_CountingReducer(scale=1.0)) + second = embedding.project(_CountingReducer(scale=3.0)) + self.assertEqual(self.model.calls, 1, "reducing must not re-embed") + np.testing.assert_allclose(second.vectors, first.vectors * 3.0) + + def test_raises_for_a_model_that_cannot_embed(self): + """A prediction-only model gets a clear error pointing at the escape hatch, not an AttributeError.""" + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) + explanation = _projection_explanation(pd.Series(["alpha", "beta"])) + with self.assertRaises(TypeError): + xpl.compute_projection(explanation) + + def test_embeddings_cached_across_instances_while_the_reducer_reruns(self): + # Only the expensive half is cached; a layout worth keeping is saved explicitly. + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) + + fresh_model = _ProjectableModel() + fresh = NlpExplainer(fresh_model, backend=object()) + fresh.compute_projection(self.explanation, reducer=reducer, cache_dir=d) + self.assertEqual(fresh_model.calls, 0) + self.assertEqual(reducer.fits, 2) + + def test_model_space_takes_part_in_the_key(self): + """Moving the model's space must re-embed, not reload the other space's vectors.""" + with tempfile.TemporaryDirectory() as d: + self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) + self.model.space = "pooled" + self.model.calls = 0 + self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) + self.assertEqual(self.model.calls, 1) # re-embedded under the new space + + def test_recompute_forces_a_fresh_fit(self): + with tempfile.TemporaryDirectory() as d: + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) + self.model.calls = 0 + self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, recompute=True) + self.assertEqual(self.model.calls, 1) + self.assertEqual(reducer.fits, 2) + + def test_fit_transform_kwargs_are_forwarded(self): + reducer = _CountingReducer() + self.xpl.compute_projection(self.explanation, reducer=reducer, init="pca", verbose=False) + self.assertEqual(reducer.last_kwargs, {"init": "pca", "verbose": False}) + + +class _UnembeddableModel(_ProjectableModel): + """Embeds nothing: stands in for a corpus the model chokes on (OOM, bad encoding).""" + + def embed(self, texts, space=None): + raise RuntimeError("cannot embed this corpus") + + +class TestFitPrecompute(unittest.TestCase): + CORPUS = ["a happy line", "a sad line", "another happy one"] + LABELS = ["joy", "sadness", "joy"] + + def test_precompute_embeds_the_bank_at_fit(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) + self.assertEqual(model.calls, 1) # the corpus, embedded once, inside fit + xpl.find_similar("a happy line") + self.assertEqual(model.calls, 2) # the query only — the bank was already there + + def test_precompute_false_defers_to_first_query(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS, precompute=False) + self.assertEqual(model.calls, 0) + xpl.find_similar("a happy line") + self.assertEqual(model.calls, 2) # bank + query, both charged to the first click + + def test_find_similar_threshold_filters_by_score_and_reports_total(self): + model = _ProjectableModel() + xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) + neighbors, total = xpl.find_similar_threshold("a happy line", threshold=-1.0, limit=1) + self.assertLessEqual(len(neighbors), 1) + self.assertGreaterEqual(total, len(neighbors)) + + def test_find_similar_threshold_requires_a_retriever(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) + with self.assertRaises(RuntimeError): + xpl.find_similar_threshold("a happy line") + + def test_precompute_is_a_noop_when_no_retriever_was_built(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) + self.assertFalse(xpl.can_find_similar()) # model cannot embed + self.assertTrue(xpl.can_probe_labels()) # ... but the model-free probe still fit + + def test_a_bank_failure_still_leaves_the_model_free_probe_usable(self): + xpl = NlpExplainer(_UnembeddableModel(), backend=object()) + with self.assertRaises(RuntimeError): + xpl.fit(self.CORPUS, y=self.LABELS) + # reference_/classes_ are assigned before the bank is built, so the half-fitted object keeps + # the feature that never needed the model. + self.assertEqual(xpl.reference_, (self.CORPUS, self.LABELS)) + self.assertEqual(xpl.classes_, LABEL_NAMES[:2]) # from the model, not derived from y + self.assertTrue(xpl.can_probe_labels()) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/explainer/test_nlp_explainer.py b/tests/unit_tests/explainer/test_nlp_explainer.py new file mode 100644 index 00000000..403fde7b --- /dev/null +++ b/tests/unit_tests/explainer/test_nlp_explainer.py @@ -0,0 +1,790 @@ +"""Unit tests for ``NlpExplainer`` — engine wiring, generator discovery, ``explain()`` caching +(in-memory and on-disk), and label-noise detection. A real NLP model is not required — synthetic +data is used throughout so the suite runs in CI without transformers/datasets. +""" + +import json +import tempfile +import unittest +import zipfile +from dataclasses import replace +from unittest.mock import patch + +import numpy as np +import pandas as pd +import plotly.graph_objs as go + +from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.backend.nlp_lime_backend import NlpLimeBackend +from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator +from shapash.explainer.nlp_explainer import NlpExplainer +from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + + +def _make_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: + """Synthetic per-token contribution data for 3 samples, 6 classes.""" + rng = np.random.default_rng(42) + token_strings = [ + ["", "i", "feel", "so", "happy", "today", ""], + ["", "this", "is", "terrible", "and", "sad", ""], + ["", "what", "a", "wonderful", "day", ""], + ] + values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] + base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) + return token_strings, values, base_values + + +def _make_texts() -> pd.Series: + return pd.Series( + ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], + index=pd.RangeIndex(3), + ) + + +def _make_explanation( + texts: pd.Series | None = None, + y_pred: pd.Series | None = None, + y_prob: pd.DataFrame | None = None, + y_true: pd.Series | None = None, + label_names: list[str] | None = LABEL_NAMES, + backend_name: str = "nlp_shap", + is_additive: bool = True, + reference_kind: str = "none", + output_space: str = "probability", +) -> NlpExplanation: + """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" + texts = _make_texts() if texts is None else texts + token_strings, values, base_values = _make_token_data() + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=( + pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred + ), + y_prob=y_prob, + y_true=y_true, + label_names=label_names, + folds_case=None, + backend_name=backend_name, + is_additive=is_additive, + reference_kind=reference_kind, + output_space=output_space, + ) + + +def _make_explainer() -> NlpExplainer: + """Bare ``NlpExplainer`` (engine role only), bypassing ``__init__`` to avoid ``shap.Explainer(None)``. + + Per ``fit``/``explain``, the explainer never holds compiled results — tests that need a computed + batch build an :class:`NlpExplanation` directly with :func:`_make_explanation` instead. + """ + xpl = object.__new__(NlpExplainer) + xpl.model = None + xpl.label_names = LABEL_NAMES + xpl.backend = None + return xpl + + +# --------------------------------------------------------------------------- +# NlpExplainer (no real model) +# --------------------------------------------------------------------------- + + +class TestNlpExplainer(unittest.TestCase): + def setUp(self): + self.xpl = _make_explainer() + self.explanation = _make_explanation() + + def test_plot_tokens_returns_figure(self): + fig = self.explanation.plot.tokens(row=0, label_idx=1) + self.assertIsInstance(fig, go.Figure) + + def test_folds_case_is_none_without_a_tokenizing_model(self): + # A bare classifier_fn has no tokenizer to ask; None is the honest answer, not a guess. + self.assertIsNone(self.xpl._folds_case()) + + def test_folds_case_reads_the_model_capability(self): + class _Uncased(TextModel, SupportsTokenization): + def predict(self, texts): + return np.tile([0.5, 0.5], (len(texts), 1)) + + def tokenize(self, text): + return text.lower().split() + + def detokenize(self, tokens): + return " ".join(tokens) + + self.xpl._text_model = _Uncased(label_names=["neg", "pos"]) + self.assertTrue(self.xpl._folds_case()) + + def test_plot_tokens_all_samples(self): + for row in range(3): + fig = self.explanation.plot.tokens(row=row, label_idx=0) + self.assertIsInstance(fig, go.Figure) + + def test_plot_tokens_all_labels(self): + for label_idx in range(N_CLASSES): + fig = self.explanation.plot.tokens(row=0, label_idx=label_idx) + self.assertIsInstance(fig, go.Figure) + + def test_plot_tokens_max_tokens(self): + fig = self.explanation.plot.tokens(row=0, label_idx=1, max_tokens=3) + self.assertIsInstance(fig, go.Figure) + self.assertLessEqual(len(fig.data[0].x), 3) + + def test_y_pred_stored(self): + self.assertIsNotNone(self.explanation.y_pred) + self.assertEqual(len(self.explanation.y_pred), 3) + + def test_label_names_propagated(self): + self.assertEqual(self.explanation.label_names, LABEL_NAMES) + + def test_y_true_is_none_by_default(self): + self.assertIsNone(self.explanation.y_true) + + +# --------------------------------------------------------------------------- +# NlpExplainer — counterfactual generator discovery / selection (captum-free) +# --------------------------------------------------------------------------- + + +class _FullCapModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): + """A model exposing every capability — both HotFlip and AblationFlip are compatible.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + def get_embedding_table(self): + return (["a"], np.zeros((1, 2))) + + def embed(self, texts): + return np.zeros((len(texts), 2)) + + def token_gradients(self, text, target_class): + toks = text.split() + return toks, np.zeros((len(toks), 2)) + + @property + def shap_callable(self): + return self.predict + + +class _TokenizeOnlyModel(TextModel, SupportsTokenization): + """Tokenizable but gradient-free — only AblationFlip is compatible.""" + + def __init__(self): + super().__init__(label_names=["neg", "pos"]) + + def predict(self, texts): + return np.tile([0.4, 0.6], (len(texts), 1)) + + def tokenize(self, text): + return text.split() + + def detokenize(self, tokens): + return " ".join(tokens) + + @property + def shap_callable(self): + return self.predict + + +class TestNlpExplainerGenerators(unittest.TestCase): + """Generator auto-discovery drives the webapp's method selector (no captum needed here).""" + + def test_full_capability_model_offers_both_methods(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + self.assertEqual( + xpl.available_cf_generators(), + [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")], + ) + # The preferred (first-discovered) generator stays the active default. + self.assertIsInstance(xpl.cf_generator, HotFlipGenerator) + self.assertEqual(set(xpl.cf_generators), {"hotflip", "ablation_flip"}) + + def test_tokenize_only_model_offers_ablation_only(self): + xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) + self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) + self.assertIsInstance(xpl.cf_generator, AblationFlipGenerator) + + def test_explicit_generator_used_verbatim_no_extras(self): + model = _FullCapModel() + gen = AblationFlipGenerator(model) + xpl = NlpExplainer(model, backend=object(), cf_generator=gen) + # An explicit choice is not augmented with the other compatible built-ins. + self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) + self.assertIs(xpl.cf_generator, gen) + + def test_cf_config_spec_selected_by_generator(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + self.assertIn("max_flips", xpl.cf_config_spec("hotflip")) + self.assertIn("max_ablations", xpl.cf_config_spec("ablation_flip")) + # No argument → the active generator's spec. + self.assertIn("max_flips", xpl.cf_config_spec()) + + def test_unknown_generator_raises(self): + xpl = NlpExplainer(_FullCapModel(), backend=object()) + with self.assertRaises(KeyError): + xpl.cf_config_spec("does_not_exist") + with self.assertRaises(KeyError): + xpl.generate_counterfactuals("hi there", generator="does_not_exist") + + def test_no_generators_without_text_model(self): + # A plain callable is neither a TextModel nor a pipeline → no generators, empty selector. + xpl = NlpExplainer( + lambda texts: np.tile([0.5, 0.5], (len(texts), 1)), label_names=["neg", "pos"], backend=object() + ) + self.assertEqual(xpl.available_cf_generators(), []) + self.assertEqual(xpl.cf_config_spec(), {}) + self.assertIsNone(xpl.cf_generator) + + +# --------------------------------------------------------------------------- +# NlpExplainer with NlpLimeBackend +# --------------------------------------------------------------------------- + +# Keep num_samples tiny so LIME tests run fast in CI. +_LIME_COMPUTE_ARGS = {"num_samples": 50, "num_features": 5} +_SAMPLE_TEXTS = ["i feel so happy today", "this is terrible and sad"] + + +def _fake_classifier(texts: list[str]) -> np.ndarray: + """Deterministic fake classifier returning (n_texts, N_CLASSES) probabilities.""" + rng = np.random.default_rng(0) + probs = rng.random((len(texts), N_CLASSES)).astype(np.float32) + probs /= probs.sum(axis=1, keepdims=True) + return probs + + +def _make_lime_backend() -> NlpLimeBackend: + return NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + + +class TestNlpExplainerWithLimeBackend(unittest.TestCase): + def _make_explainer_lime(self) -> NlpExplainer: + """NlpExplainer backed by NlpLimeBackend.""" + xpl = _make_explainer() + xpl.backend = _make_lime_backend() + return xpl + + def test_backend_is_lime_instance(self): + xpl = self._make_explainer_lime() + self.assertIsInstance(xpl.backend, NlpLimeBackend) + + def test_plot_tokens_works_for_a_lime_explanation(self): + # LIME is non-additive, so .plot.waterfall refuses; .plot.tokens stays available. + explanation = replace(_make_explanation(), backend_name="nlp_lime", is_additive=False) + self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) + with self.assertRaises(ValueError): + explanation.plot.waterfall(row=0, label_idx=0) + + def test_explain_sets_contributions(self): + backend = _make_lime_backend() + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) + fake_pred_df = pd.DataFrame( + {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, + index=pd.RangeIndex(len(_SAMPLE_TEXTS)), + ) + with patch.object(xpl, "_predict", return_value=fake_pred_df): + explanation = xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(len(explanation), len(_SAMPLE_TEXTS)) + self.assertEqual(explanation.label_names, LABEL_NAMES) + + def test_explain_caching_skips_rerun(self): + backend = _make_lime_backend() + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) + fake_pred_df = pd.DataFrame( + {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, + index=pd.RangeIndex(len(_SAMPLE_TEXTS)), + ) + with patch.object(xpl, "_predict", return_value=fake_pred_df) as mocked_predict: + xpl.explain(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) # same data — must hit in-memory cache + self.assertEqual(mocked_predict.call_count, 1, "memoization must skip the second _predict call") + + +# --------------------------------------------------------------------------- +# compile() cache key +# --------------------------------------------------------------------------- + + +class _KeyModel(TextModel): + """Prediction-only model with a caller-chosen ``model_id``, so keys can be varied deliberately.""" + + def __init__(self, ident="model-a"): + super().__init__(label_names=LABEL_NAMES) + self._ident = ident + + @property + def model_id(self): + return self._ident + + def predict(self, texts): + probs = np.full((len(texts), N_CLASSES), 1.0 / N_CLASSES) + return probs + + +class _MarkerBackend(NlpBackend): + """Returns a constant contribution equal to ``marker``, and counts its explainer runs. + + The constant makes it visible *whose* result a cache served: a value of 1.0 can only have come + from the backend built with ``marker=1.0``. + """ + + name = "marker_backend" + reference_kind = "none" + is_additive = True + output_space = "probability" + + def __init__(self, marker=1.0, **kwargs): + super().__init__(model=None, label_names=LABEL_NAMES, **kwargs) + self.marker = marker + self.calls = 0 + + def run_explainer(self, x): + self.calls += 1 + texts = list(x) + return NlpContributions( + token_strings=[["a", "b"] for _ in texts], + values=[np.full((2, N_CLASSES), self.marker) for _ in texts], + base_values=np.zeros((len(texts), N_CLASSES)), + ) + + +class _OtherMarkerBackend(_MarkerBackend): + """Same behaviour under a different registered ``name`` — a distinct attribution method.""" + + name = "other_marker_backend" + + +def _explainer(model=None, backend=None, label_names=LABEL_NAMES): + return NlpExplainer( + model or _KeyModel(), + label_names=label_names, + backend=backend or _MarkerBackend(), + ) + + +class TestExplainCacheKey(unittest.TestCase): + """``explain`` results depend on *(texts, model, backend)* — so all three must be in the key. + + Keying on the texts alone means swapping the model or the attribution backend and pointing at the + same ``cache_dir`` silently reloads the previous run's contributions, and even without a + ``cache_dir`` the in-memory guard turns a re-``explain`` into a no-op. + """ + + def test_identical_inputs_still_hit_the_in_memory_cache(self): + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.explain(_SAMPLE_TEXTS) + xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(backend.calls, 1, "memoization must survive the richer key") + + def test_the_memo_cannot_be_poisoned_through_a_returned_artifact(self): + """``explain`` returns a ``replace()`` of the memoized artifact — a *shallow* copy. + + The two artifacts therefore share their contribution arrays, so before the arrays were + sealed an in-place edit of the first return value silently rewrote the cache, and every + later ``explain`` of the same texts served the corrupted numbers as a fresh result. The + seal turns that into an error at the point of the edit. + """ + backend = _MarkerBackend(marker=1.0) + xpl = _explainer(backend=backend) + first = xpl.explain(_SAMPLE_TEXTS) + + with self.assertRaises(ValueError): + first.values[0][0] = -42.0 + + second = xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(backend.calls, 1, "still a memo hit — the seal must not defeat caching") + np.testing.assert_allclose(second.values[0], 1.0) + + def test_a_memo_hit_is_relabelled_onto_the_caller_index(self): + """The memo is keyed by text *content*, so the same texts from a differently-indexed + source is a hit — and the cached predictions still carry the first caller's index. + ``replace`` says nothing about the fields it is not handed, so every index-bearing field + has to be re-labelled, not just ``texts``. + """ + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.explain(pd.Series(_SAMPLE_TEXTS)) # first call: default RangeIndex + + index = pd.Index([77 + i for i in range(len(_SAMPLE_TEXTS))]) + second = xpl.explain(pd.Series(_SAMPLE_TEXTS, index=index)) + + self.assertEqual(backend.calls, 1, "re-labelling must not cost a recompute") + for field_name in ("texts", "y_pred", "y_prob"): + with self.subTest(field=field_name): + self.assertTrue(getattr(second, field_name).index.equals(index)) + + def test_a_y_series_that_does_not_match_x_is_refused(self): + """Reindexing it silently would pair labels positionally while looking like alignment.""" + xpl = _explainer(backend=_MarkerBackend()) + texts = pd.Series(_SAMPLE_TEXTS, index=[10 + i for i in range(len(_SAMPLE_TEXTS))]) + misaligned = pd.Series(["joy"] * len(_SAMPLE_TEXTS), index=range(len(_SAMPLE_TEXTS))) + with self.assertRaises(ValueError) as ctx: + xpl.explain(texts, y=misaligned) + self.assertIn("indexed differently", str(ctx.exception)) + + def test_a_y_series_aligned_to_x_is_kept_and_a_list_is_indexed_positionally(self): + xpl = _explainer(backend=_MarkerBackend()) + index = pd.Index([10 + i for i in range(len(_SAMPLE_TEXTS))]) + texts = pd.Series(_SAMPLE_TEXTS, index=index) + labels = ["joy"] * len(_SAMPLE_TEXTS) + for tag, y in (("series", pd.Series(labels, index=index)), ("list", labels)): + with self.subTest(y=tag): + self.assertTrue(xpl.explain(texts, y=y).y_true.index.equals(index)) + + def test_swapping_the_backend_recomputes(self): + xpl = _explainer(backend=_MarkerBackend(marker=1.0)) + xpl.explain(_SAMPLE_TEXTS) + replacement = _OtherMarkerBackend(marker=2.0) + xpl.backend = replacement + explanation = xpl.explain(_SAMPLE_TEXTS) + self.assertEqual(replacement.calls, 1, "in-memory guard served a stale, other-backend result") + np.testing.assert_allclose(explanation.values[0], 2.0) + + def test_backend_compute_args_are_part_of_the_key(self): + xpl = _explainer(backend=_MarkerBackend(explainer_compute_args={"n_steps": 50})) + first = xpl._compute_key(_SAMPLE_TEXTS) + xpl.backend = _MarkerBackend(explainer_compute_args={"n_steps": 200}) + self.assertNotEqual(first, xpl._compute_key(_SAMPLE_TEXTS)) + + def test_compute_args_key_is_order_insensitive(self): + # Same settings written in a different order are the same configuration. + a = _explainer(backend=_MarkerBackend(explainer_compute_args={"x": 1, "y": 2})) + b = _explainer(backend=_MarkerBackend(explainer_compute_args={"y": 2, "x": 1})) + self.assertEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_model_identity_is_part_of_the_key(self): + a = _explainer(model=_KeyModel("model-a")) + b = _explainer(model=_KeyModel("model-b")) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_label_names_are_part_of_the_key(self): + # label_names fixes the column order of y_prob, so it changes the cached payload. + a = _explainer(label_names=LABEL_NAMES) + b = _explainer(label_names=list(reversed(LABEL_NAMES))) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_key_is_collision_safe_across_text_boundaries(self): + # Without a separator between texts these two corpora hash identically. + xpl = _explainer() + self.assertNotEqual(xpl._compute_key(["ab", "c"]), xpl._compute_key(["a", "bc"])) + + def test_key_is_order_sensitive(self): + xpl = _explainer() + self.assertNotEqual(xpl._compute_key(["a", "b"]), xpl._compute_key(["b", "a"])) + + +def _read_meta(path) -> dict: + with zipfile.ZipFile(path) as zf: + return json.loads(zf.read("meta.json")) + + +def _drop_meta_key(path, key) -> None: + """Rewrite a saved ``.xpl`` in place without one ``meta.json`` key — how an older layout is faked.""" + with zipfile.ZipFile(path) as zin: + members = {item.filename: zin.read(item.filename) for item in zin.infolist()} + meta = json.loads(members["meta.json"]) + del meta[key] + members["meta.json"] = json.dumps(meta).encode() + with zipfile.ZipFile(path, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + + +class TestExplainDiskCacheIsolation(unittest.TestCase): + """The disk cache is the dangerous case: a stale entry survives the process that wrote it.""" + + def test_two_backends_share_a_cache_dir_without_collision(self): + with tempfile.TemporaryDirectory() as cache_dir: + first = _MarkerBackend(marker=1.0) + _explainer(backend=first).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + second = _OtherMarkerBackend(marker=2.0) + xpl = _explainer(backend=second) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(second.calls, 1, "loaded the other backend's cached contributions") + np.testing.assert_allclose(explanation.values[0], 2.0) + + def test_two_models_share_a_cache_dir_without_collision(self): + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(model=_KeyModel("model-a"), backend=_MarkerBackend(marker=1.0)).explain( + _SAMPLE_TEXTS, cache_dir=cache_dir + ) + backend_b = _MarkerBackend(marker=2.0) + xpl = _explainer(model=_KeyModel("model-b"), backend=backend_b) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend_b.calls, 1, "loaded the other model's cached contributions") + np.testing.assert_allclose(explanation.values[0], 2.0) + + def test_same_model_and_backend_reload_from_disk(self): + # The cache must still *work* — a fresh instance skips the expensive run. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=3.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + reloaded_backend = _MarkerBackend(marker=3.0) + xpl = _explainer(backend=reloaded_backend) + explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertEqual(reloaded_backend.calls, 0, "disk cache did not hit for identical inputs") + np.testing.assert_allclose(explanation.values[0], 3.0) + + def test_cache_path_points_at_the_file_explain_writes(self): + with tempfile.TemporaryDirectory() as cache_dir: + xpl = _explainer() + path = xpl.cache_path(_SAMPLE_TEXTS, cache_dir) + self.assertFalse(path.exists()) + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertTrue(path.exists(), "cache_path disagrees with where explain() wrote") + + def test_stale_layout_is_a_miss_not_a_crash(self): + # The cache key covers texts/model/backend but not the shapash version, so a layout change + # leaves entries the new reader cannot parse. The entry must be recomputed and overwritten. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=4.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + _drop_meta_key(path, "values_ndim") + + backend = _MarkerBackend(marker=4.0) + explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend.calls, 1, "a rejected cache entry must fall through to a recompute") + np.testing.assert_allclose(explanation.values[0], 4.0) + self.assertIn("values_ndim", _read_meta(path), "stale entry was not replaced") + + def test_corrupt_cache_entry_is_a_miss_not_a_crash(self): + with tempfile.TemporaryDirectory() as cache_dir: + _explainer(backend=_MarkerBackend(marker=5.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + path.write_bytes(b"not a zip file") + + backend = _MarkerBackend(marker=5.0) + explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + + self.assertEqual(backend.calls, 1) + np.testing.assert_allclose(explanation.values[0], 5.0) + + def test_explicit_load_still_raises_on_a_stale_file(self): + # The miss-not-crash rule is scoped to the cache. A caller who names a file gets an error + # rather than a silent substitution. + with tempfile.TemporaryDirectory() as cache_dir: + _explainer().explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) + _drop_meta_key(path, "values_ndim") + with self.assertRaises(KeyError): + NlpExplanation.load(path) + + def test_clear_cache_forces_a_recompute(self): + with tempfile.TemporaryDirectory() as cache_dir: + backend = _MarkerBackend() + xpl = _explainer(backend=backend) + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + xpl.clear_cache(_SAMPLE_TEXTS, cache_dir) + self.assertFalse(xpl.cache_path(_SAMPLE_TEXTS, cache_dir).exists()) + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + self.assertEqual(backend.calls, 2, "clear_cache must defeat the in-memory guard too") + + def test_clear_cache_leaves_other_backends_entries_intact(self): + with tempfile.TemporaryDirectory() as cache_dir: + keeper = _MarkerBackend(marker=1.0) + _explainer(backend=keeper).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + keeper_path = _explainer(backend=_MarkerBackend(marker=1.0)).cache_path(_SAMPLE_TEXTS, cache_dir) + + other = _explainer(backend=_OtherMarkerBackend(marker=2.0)) + other.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + other.clear_cache(_SAMPLE_TEXTS, cache_dir) + + self.assertTrue(keeper_path.exists(), "clearing one backend dropped another's cache") + + +class TestDetectLabelNoise(unittest.TestCase): + """The explainer's confident-learning surface, over an ``NlpExplanation`` (no model, no backend).""" + + def _with_labels(self, y_true=None, y_prob=None) -> NlpExplanation: + """A batch carrying ground truth and per-class probabilities. + + Sample 1 is labelled ``joy`` while the model confidently says ``sadness`` — the planted + error. Both classes carry at least one label, without which ``sadness`` would have no + estimable threshold and could never be suggested (covered separately in the compute tests). + """ + confident = ["joy", "sadness", "sadness"] + probs = np.full((3, N_CLASSES), 0.02) + for row, name in enumerate(confident): + probs[row, LABEL_NAMES.index(name)] = 0.90 + probs = probs / probs.sum(axis=1, keepdims=True) + y_pred = pd.Series(confident, index=pd.RangeIndex(3), name="prediction") + resolved_y_prob = pd.DataFrame(probs, index=pd.RangeIndex(3), columns=LABEL_NAMES) if y_prob is None else y_prob + resolved_y_true = ( + pd.Series(["joy", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") + if y_true is None + else y_true + ) + return _make_explanation(y_pred=y_pred, y_prob=resolved_y_prob, y_true=resolved_y_true) + + # ── capability flag ──────────────────────────────────────────────── + def test_available_with_ground_truth_and_per_class_probabilities(self): + self.assertTrue(_make_explainer().can_detect_label_noise(self._with_labels())) + + def test_unavailable_without_ground_truth(self): + explanation = replace(self._with_labels(), y_true=None) + self.assertFalse(_make_explainer().can_detect_label_noise(explanation)) + + def test_unavailable_with_only_the_winning_class_probability(self): + # The raw-pipeline path emits a single "probability" column; the losing classes' scores are + # precisely what confident learning needs. + legacy = pd.DataFrame({"probability": [0.9, 0.9, 0.9]}, index=pd.RangeIndex(3)) + self.assertFalse(_make_explainer().can_detect_label_noise(self._with_labels(y_prob=legacy))) + + def test_unavailable_on_an_explanation_with_no_probabilities(self): + self.assertFalse(_make_explainer().can_detect_label_noise(_make_explanation())) + + def test_available_without_a_model(self): + # Unlike the other capability flags this needs no live model. + xpl = _make_explainer() + self.assertIsNone(xpl.model) + self.assertTrue(xpl.can_detect_label_noise(self._with_labels())) + + # ── detection ────────────────────────────────────────────────────── + def test_raises_when_the_prerequisites_are_missing(self): + with self.assertRaisesRegex(RuntimeError, "ground-truth labels"): + _make_explainer().detect_label_noise(_make_explanation()) + + def test_flags_the_planted_mislabel(self): + report = _make_explainer().detect_label_noise(self._with_labels()) + self.assertEqual([i.index for i in report.issues], [1]) + issue = report.issues[0] + self.assertEqual(issue.given_label, "joy") + self.assertEqual(issue.suggested_label, "sadness") + self.assertEqual(issue.text, "this is terrible and sad") + + def test_label_names_come_from_the_probability_columns(self): + report = _make_explainer().detect_label_noise(self._with_labels()) + self.assertEqual(report.label_names, LABEL_NAMES) + self.assertEqual(report.noise_matrix.shape, (N_CLASSES, N_CLASSES)) + self.assertEqual(report.n_samples, 3) + + def test_respects_top_n_and_score(self): + report = _make_explainer().detect_label_noise(self._with_labels(), top_n=0, score="normalized_margin") + self.assertEqual(report.issues, []) + self.assertEqual(report.n_issues, 1) + + # ── memoisation ──────────────────────────────────────────────────── + def test_repeats_are_served_from_the_memo(self): + xpl = _make_explainer() + explanation = self._with_labels() + first = xpl.detect_label_noise(explanation, top_n=5) + self.assertIs(xpl.detect_label_noise(explanation, top_n=5), first) + + def test_different_arguments_recompute(self): + xpl = _make_explainer() + explanation = self._with_labels() + self.assertIsNot(xpl.detect_label_noise(explanation, top_n=5), xpl.detect_label_noise(explanation, top_n=4)) + + # ── independent probe ────────────────────────────────────────────── + def _probe_corpus(self): + """A reference corpus separable by words the audited fixture's texts also use.""" + texts = [ + "this is wonderful and joyful", + "wonderful joyful and bright", + "a joyful wonderful day", + "bright and wonderful joy", + "this is terrible and sad", + "terrible sad and bleak", + "a sad terrible day", + "bleak and terrible sadness", + ] + labels = ["joy"] * 4 + ["sadness"] * 4 + return texts, labels + + def test_no_probe_when_no_reference_corpus_is_bound(self): + xpl = _make_explainer() + self.assertFalse(xpl.can_probe_labels()) + self.assertIsNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) + + def test_probe_verdict_is_attached_when_a_labelled_corpus_is_bound(self): + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + self.assertTrue(xpl.can_probe_labels()) + issue = xpl.detect_label_noise(self._with_labels()).issues[0] + self.assertIsNotNone(issue.probe) + self.assertIn(issue.probe.top_label, {"joy", "sadness"}) + self.assertEqual(issue.probe.backs_given, issue.probe.top_label == issue.given_label) + + def test_probe_corroborates_a_genuine_label_error(self): + # The flagged row is "this is terrible and sad" carrying the label "joy". The reference + # corpus puts that vocabulary firmly in "sadness", so the probe rejects the given label too + # — the two-signals-agree case, which is the one worth relabelling. + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + issue = xpl.detect_label_noise(self._with_labels()).issues[0] + self.assertEqual((issue.given_label, issue.text), ("joy", "this is terrible and sad")) + self.assertFalse(issue.probe.backs_given) + self.assertEqual(issue.probe.top_label, "sadness") + self.assertLess(issue.probe.given_prob, 0.5) + + def test_probe_backs_the_label_when_the_corpus_sides_with_it(self): + # The mirror case, and the reason the column exists: same flagged row, but a corpus that + # calls this vocabulary "joy". The probe now defends the label, marking the row as the + # audited model's error rather than the corpus's. + xpl = _make_explainer() + texts, _ = self._probe_corpus() + xpl.reference_ = (texts, ["sadness"] * 4 + ["joy"] * 4) + issue = xpl.detect_label_noise(self._with_labels()).issues[0] + self.assertEqual(issue.given_label, "joy") + self.assertTrue(issue.probe.backs_given) + self.assertGreater(issue.probe.given_prob, 0.5) + + def test_probe_is_skipped_when_not_requested(self): + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + self.assertIsNone(xpl.detect_label_noise(self._with_labels(), probe=False).issues[0].probe) + + def test_probe_is_fit_once_and_reused_across_calls(self): + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + explanation = self._with_labels() + xpl.detect_label_noise(explanation, top_n=5) + first = xpl._label_probe + self.assertIsNotNone(first) + xpl.detect_label_noise(explanation, top_n=4) # different args -> recompute, but the probe is kept + self.assertIs(xpl._label_probe, first) + + def test_probe_needs_no_model_or_retriever(self): + # The point of fitting on plain text: a prediction-only pipeline that cannot embed (so + # can_find_similar() is False) still gets the second opinion. + xpl = _make_explainer() + xpl.reference_ = self._probe_corpus() + xpl._retriever = None + xpl._text_model = None + self.assertFalse(xpl.can_find_similar()) + self.assertTrue(xpl.can_probe_labels()) + self.assertIsNotNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/nlp/test_nlp_explanation.py index 76770365..fa19d38a 100644 --- a/tests/unit_tests/nlp/test_nlp_explanation.py +++ b/tests/unit_tests/nlp/test_nlp_explanation.py @@ -19,7 +19,14 @@ import numpy as np import pandas as pd -from shapash.explainer.nlp_explanation import NlpExplanation +from shapash.explainer.nlp_explanation import ( + WORD_AGGREGATIONS, + NlpExplanation, + aggregate_word_contributions, + rank_word_samples, + word_contributions_by_sample, +) +from shapash.plots.plot_word_importance import plot_word_importance def _make_explanation(values_ndim: int, with_base: bool, with_true: bool, with_prob: bool) -> NlpExplanation: @@ -395,3 +402,771 @@ def test_a_saved_artifact_reloads_with_one_index(self): restored = NlpExplanation.load(path) self.assertTrue(restored.y_pred.index.equals(restored.texts.index)) self.assertTrue(restored.y_true.index.equals(restored.texts.index)) + + +# --------------------------------------------------------------------------- +# NlpExplanation.word_importance / resolve_lowercase / vocabulary / word_counts +# / word_occurrences / aggregate_word_contributions / rank_word_samples / +# word_contributions_by_sample +# --------------------------------------------------------------------------- + +LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] +N_CLASSES = len(LABEL_NAMES) + + +def _make_word_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: + """Synthetic per-token contribution data for 3 samples, 6 classes.""" + rng = np.random.default_rng(42) + token_strings = [ + ["", "i", "feel", "so", "happy", "today", ""], + ["", "this", "is", "terrible", "and", "sad", ""], + ["", "what", "a", "wonderful", "day", ""], + ] + values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] + base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) + return token_strings, values, base_values + + +def _minimal_word_explanation( + token_strings: list[list[str]], + values: list[np.ndarray], + base_values: np.ndarray | None, + folds_case: bool | None = None, +) -> NlpExplanation: + """Bare ``NlpExplanation`` for word-importance/case-folding tests — only the token data matters.""" + n = len(token_strings) + texts = pd.Series([""] * n, index=pd.RangeIndex(n)) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=base_values, + y_pred=pd.Series([""] * n, index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=folds_case, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +class TestWordImportance(unittest.TestCase): + def setUp(self): + token_strings, values, base_values = _make_word_token_data() + self.explanation = _minimal_word_explanation(token_strings, values, base_values) + + def test_len(self): + self.assertEqual(len(self.explanation), 3) + + def test_word_importance_returns_series(self): + imp = self.explanation.word_importance(label_idx=1) + self.assertIsInstance(imp, pd.Series) + + def test_word_importance_filters_special_tokens(self): + imp = self.explanation.word_importance(label_idx=0, filter_special=True) + self.assertNotIn("", imp.index) + self.assertNotIn(" ", imp.index) + + def test_word_importance_keeps_special_when_disabled(self): + imp = self.explanation.word_importance(label_idx=0, filter_special=False) + # Empty strings (BOS/EOS) should now be present + self.assertIn("", imp.index) + + def test_word_importance_hides_punctuation_by_default(self): + explanation = _minimal_word_explanation( + token_strings=[["great", "!", "!", "movie", ".", ","]], + values=[np.array([[3.0], [9.0], [9.0], [2.0], [8.0], [7.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["great", "movie"]) + + def test_word_importance_keeps_punctuation_when_asked(self): + explanation = _minimal_word_explanation( + token_strings=[["great", "!", "!"]], + values=[np.array([[3.0], [9.0], [9.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20, filter_punctuation=False) + self.assertIn("!", imp.index) + # Punctuation kept means it can outrank real words, which is why it is hidden by default. + self.assertEqual(imp.index[0], "!") + + def test_word_importance_keeps_words_containing_punctuation(self): + # Only *pure* punctuation units are dropped — a hyphenated or apostrophised word stays. + explanation = _minimal_word_explanation( + token_strings=[["state-of-the-art", "don't", "-"]], + values=[np.array([[3.0], [2.0], [9.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["don't", "state-of-the-art"]) + + def test_resolve_lowercase_follows_the_model(self): + for folds_case, expected in ((True, True), (False, False), (None, True)): + with self.subTest(folds_case=folds_case): + explanation = replace(self.explanation, folds_case=folds_case) + self.assertEqual(explanation.resolve_lowercase(), expected) + + def test_resolve_lowercase_explicit_argument_wins(self): + self.assertFalse(replace(self.explanation, folds_case=True).resolve_lowercase(False)) + self.assertTrue(replace(self.explanation, folds_case=False).resolve_lowercase(True)) + + def test_word_importance_keeps_case_on_a_cased_model(self): + # A cased tokenizer encodes AWFUL and awful to *different* ids, so they are different + # inputs with genuinely different attributions — merging them would hide that. + explanation = _minimal_word_explanation( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + folds_case=False, + ) + imp = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) + + def test_word_importance_merges_case_on_an_uncased_model(self): + # An uncased tokenizer maps both spellings to one input id — the model cannot tell them + # apart, so they must not appear as two rows. + explanation = _minimal_word_explanation( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + folds_case=True, + ) + imp = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(list(imp.index), ["awful"]) + self.assertAlmostEqual(imp["awful"], -6.0) + + def test_word_importance_merges_case_variants_by_default(self): + # One row per word, averaged over every casing — not three rows of one occurrence each. + explanation = _minimal_word_explanation( + token_strings=[["AWFUL", "Awful", "awful", "good"]], + values=[np.array([[-9.0], [-3.0], [-3.0], [1.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(sorted(imp.index), ["awful", "good"]) + self.assertAlmostEqual(imp["awful"], -5.0) + + def test_word_importance_case_fragmentation_lets_rare_variants_outrank(self): + # The reason lowercasing is the default: a single capitalised occurrence keeps its own + # extreme value instead of being averaged into the 3 common ones, and tops the ranking. + explanation = _minimal_word_explanation( + token_strings=[["TERRIBLE", *["terrible"] * 9, "dull"]], + values=[np.array([[-9.0], *[[-0.1]] * 9, [-2.0]])], + base_values=np.zeros((1, 1)), + ) + cased = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) + self.assertEqual(cased.index[0], "TERRIBLE") + folded = explanation.word_importance(label_idx=0, n_top=20) + self.assertEqual(folded.index[0], "dull") + + def test_word_importance_keeps_case_when_disabled(self): + explanation = _minimal_word_explanation( + token_strings=[["AWFUL", "awful"]], + values=[np.array([[-9.0], [-3.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) + self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) + + def test_word_importance_exclusion_is_case_insensitive_when_folding(self): + # The webapp's dropdown offers lowercase entries; excluding one must drop every casing. + explanation = _minimal_word_explanation( + token_strings=[["AWFUL", "awful", "good"]], + values=[np.array([[-9.0], [-3.0], [1.0]])], + base_values=np.zeros((1, 1)), + ) + imp = explanation.word_importance(label_idx=0, n_top=20, exclude_words={"AwFuL"}) + self.assertEqual(list(imp.index), ["good"]) + + def test_word_importance_respects_n_top(self): + imp = self.explanation.word_importance(label_idx=1, n_top=3) + self.assertLessEqual(len(imp), 3) + + def test_word_importance_sorted_by_absolute_value(self): + imp = self.explanation.word_importance(label_idx=2, n_top=20) + abs_vals = imp.abs().tolist() + self.assertEqual(abs_vals, sorted(abs_vals, reverse=True)) + + def test_word_importance_aggregates_repeated_words(self): + # "feel" only appears once; check it has a single contribution value + imp = self.explanation.word_importance(label_idx=1, n_top=20, filter_special=True) + self.assertIn("feel", imp.index) + # Should be a scalar (mean of one occurrence) + self.assertIsInstance(imp["feel"], float) + + def test_word_importance_all_labels(self): + for idx in range(N_CLASSES): + imp = self.explanation.word_importance(label_idx=idx) + self.assertIsInstance(imp, pd.Series) + self.assertGreater(len(imp), 0) + + def test_word_importance_filter_sign_positive(self): + imp = self.explanation.word_importance(label_idx=0, filter_sign="positive") + if len(imp) > 0: + self.assertTrue((imp > 0).all(), "positive filter should return only positive values") + + def test_word_importance_filter_sign_negative(self): + imp = self.explanation.word_importance(label_idx=0, filter_sign="negative") + if len(imp) > 0: + self.assertTrue((imp < 0).all(), "negative filter should return only negative values") + + def test_word_importance_exclude_words(self): + imp_full = self.explanation.word_importance(label_idx=1, filter_special=True) + if len(imp_full) == 0: + return + word_to_exclude = imp_full.index[0] + imp_filtered = self.explanation.word_importance( + label_idx=1, filter_special=True, exclude_words={word_to_exclude} + ) + self.assertNotIn(word_to_exclude, imp_filtered.index) + + def test_word_importance_exclude_words_empty_set(self): + imp_no_exclude = self.explanation.word_importance(label_idx=1, exclude_words=set()) + imp_none_exclude = self.explanation.word_importance(label_idx=1, exclude_words=None) + pd.testing.assert_series_equal(imp_no_exclude, imp_none_exclude) + + def test_word_importance_sample_indices(self): + imp = self.explanation.word_importance(label_idx=0, sample_indices=[0], n_top=50) + self.assertGreater(len(imp), 0) + # "terrible" only exists in sample 1 — must not appear in the sample-0 subset + self.assertNotIn("terrible", imp.index) + + +# --------------------------------------------------------------------------- +# Single-word profile: word_occurrences / vocabulary / aggregators +# --------------------------------------------------------------------------- + + +def _word_profile_explanation(folds_case=True): + """Three samples, two classes, with 'happy' appearing in three of them (twice in one).""" + token_strings = [ + ["[CLS]", "so", "happy", "today", "!"], + ["Happy", "and", "happy", "again"], + ["not", "happy", "at", "all"], + ["nothing", "here"], + ] + values = [ + np.array([[0.0, 0.0], [0.1, -0.1], [0.4, -0.4], [0.05, -0.05], [0.0, 0.0]]), + np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1], [0.0, 0.0]]), + np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), + np.array([[0.0, 0.0], [0.0, 0.0]]), + ] + texts = pd.Series(["so happy today !", "Happy and happy again", "not happy at all", "nothing here"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=None, + y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), + label_names=["pos", "neg"], + folds_case=folds_case, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +class TestVocabulary(unittest.TestCase): + def test_filters_special_and_punctuation_and_folds(self): + vocab = _word_profile_explanation().vocabulary() + self.assertNotIn("[CLS]", vocab) + self.assertNotIn("!", vocab) + # Folded: "Happy" and "happy" collapse to one entry. + self.assertIn("happy", vocab) + self.assertNotIn("Happy", vocab) + + def test_keeps_case_when_model_does_not_fold(self): + vocab = _word_profile_explanation(folds_case=False).vocabulary() + self.assertIn("Happy", vocab) + self.assertIn("happy", vocab) + + def test_sorted_and_unique(self): + vocab = _word_profile_explanation().vocabulary() + self.assertEqual(vocab, sorted(set(vocab))) + + def test_offers_exactly_what_word_importance_ranks(self): + # The contract the pickers rely on: every unit offered has occurrences to show. + explanation = _word_profile_explanation() + for word in explanation.vocabulary(): + self.assertFalse(explanation.word_occurrences(word).empty, word) + + def test_can_keep_punctuation_and_special(self): + vocab = _word_profile_explanation().vocabulary(filter_special=False, filter_punctuation=False) + self.assertIn("[cls]", vocab) + self.assertIn("!", vocab) + + +class TestWordImportanceRanking(unittest.TestCase): + """``rank_by`` + ``min_occurrences``: ordering criteria that never touch the reported sign.""" + + def setUp(self): + # "rare" appears once with a large pull; "common" appears four times with a small one. + # Under |mean| rare wins; under |sum| common wins (4 x 0.3 = 1.2 > 0.9). + token_strings = [ + ["rare", "common"], + ["common"], + ["common", "common"], + ["mild"], + ] + values = [ + np.array([[0.9], [0.3]]), + np.array([[0.3]]), + np.array([[0.3], [0.3]]), + np.array([[-0.5]]), + ] + self.explanation = _minimal_word_explanation(token_strings, values, None) + + def test_mean_is_the_default_and_unchanged(self): + imp = self.explanation.word_importance(label_idx=0) + self.assertEqual(imp.index[0], "rare") + self.assertAlmostEqual(imp["common"], 0.3) + + def test_sum_reranks_by_total_mass(self): + imp = self.explanation.word_importance(label_idx=0, rank_by="sum") + self.assertEqual(imp.index[0], "common") + self.assertAlmostEqual(imp["common"], 1.2) + + def test_series_name_records_the_statistic(self): + self.assertEqual(self.explanation.word_importance(label_idx=0).name, "mean") + self.assertEqual(self.explanation.word_importance(label_idx=0, rank_by="sum").name, "sum") + + def test_ranking_is_absolute_but_values_stay_signed(self): + # "mild" is negative and must survive ranking, keeping its sign for the sign filter and + # for the renderer's red/blue colouring. + for rank_by in ("mean", "sum"): + imp = self.explanation.word_importance(label_idx=0, rank_by=rank_by) + self.assertLess(imp["mild"], 0) + + def test_sign_filter_works_in_both_modes(self): + for rank_by in ("mean", "sum"): + pos = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="positive") + neg = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="negative") + self.assertNotIn("mild", pos.index) + self.assertEqual(list(neg.index), ["mild"]) + + def test_min_occurrences_drops_rare_words(self): + imp = self.explanation.word_importance(label_idx=0, min_occurrences=2) + self.assertEqual(list(imp.index), ["common"]) + + def test_min_occurrences_of_one_is_no_filter(self): + self.assertEqual( + set(self.explanation.word_importance(label_idx=0, min_occurrences=1).index), + {"rare", "common", "mild"}, + ) + + def test_min_occurrences_counts_within_the_selection(self): + # "common" occurs 4x across the batch but only once in sample 1, so a floor of 2 must + # exclude it there — otherwise a threshold would mean something different per scope. + imp = self.explanation.word_importance(label_idx=0, sample_indices=[1], min_occurrences=2) + self.assertTrue(imp.empty) + self.assertEqual( + list(self.explanation.word_importance(label_idx=0, sample_indices=[2], min_occurrences=2).index), + ["common"], + ) + + def test_impossible_floor_returns_an_empty_series_not_an_error(self): + imp = self.explanation.word_importance(label_idx=0, min_occurrences=999) + self.assertTrue(imp.empty) + self.assertEqual(imp.dtype, np.float64) + + def test_unknown_rank_by_raises(self): + with self.assertRaisesRegex(ValueError, "must be 'mean' or 'sum'"): + self.explanation.word_importance(label_idx=0, rank_by="median") + + def test_plotter_labels_the_axis_for_the_statistic(self): + self.assertEqual( + self.explanation.plot.word_importance(label_idx=0, rank_by="sum").layout.xaxis.title.text, + "Total SHAP contribution", + ) + self.assertEqual( + self.explanation.plot.word_importance(label_idx=0).layout.xaxis.title.text, + "Mean SHAP contribution", + ) + + def test_plotter_hover_counts_match_the_scope_it_ranked(self): + # The plotter forwards only the keying arguments to word_counts, so a count on a bar must + # be the count over the *same* samples the aggregate was taken over. + fig = self.explanation.plot.word_importance(label_idx=0, sample_indices=[2]) + counts = self.explanation.word_counts(sample_indices=[2])["n_occurrences"] + drawn = list(fig.data[0].y) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [int(counts[w]) for w in drawn]) + + +class TestWordImportanceAcrossClasses(unittest.TestCase): + """``label_idx=None``: one ranking spanning every class, on magnitudes.""" + + @staticmethod + def _three_class(): + # "alpha" is decisive for class c only; "beta" is mild and spread. Three classes so max + # and mean over classes are actually distinguishable (with two they coincide). + texts = pd.Series(["alpha beta", "alpha beta"]) + rows = np.array([[-0.3, -0.3, 0.6], [0.1, -0.2, 0.1]]) + return NlpExplanation( + texts=texts, + token_strings=[["alpha", "beta"], ["alpha", "beta"]], + values=[rows, rows], + base_values=None, + y_pred=pd.Series(["a", "a"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=["a", "b", "c"], + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + def test_value_is_the_strongest_classs_magnitude(self): + # Not the mean over classes, which would read 0.4 and 0.133 — smaller than any real + # contribution and matching nothing the single-class views show. + self.assertEqual(self._three_class().word_importance(label_idx=None).to_dict(), {"alpha": 0.6, "beta": 0.2}) + + def test_it_matches_what_the_driving_class_shows(self): + exp = self._three_class() + self.assertEqual(exp.word_importance(label_idx=None)["alpha"], abs(exp.word_importance(label_idx=2)["alpha"])) + + def test_signed_averaging_across_classes_would_be_zero(self): + # The reason the collapse discards sign: an explainer of a normalised output cancels + # across classes, so a signed cross-class mean is a chart of zeros. + exp = self._three_class() + per_class = np.array([exp.word_importance(label_idx=i)["alpha"] for i in range(3)]) + self.assertAlmostEqual(float(per_class.mean()), 0.0) + self.assertGreater(exp.word_importance(label_idx=None)["alpha"], 0.5) + + def test_single_output_model_collapses_to_the_absolute_value(self): + # A 1-D contribution array has no class axis to reduce over; it still must not raise. + texts = pd.Series(["alpha beta"]) + exp = NlpExplanation( + texts=texts, + token_strings=[["alpha", "beta"]], + values=[np.array([0.5, -0.2])], + base_values=None, + y_pred=pd.Series(["a"], index=texts.index, name="prediction"), + y_prob=None, + y_true=None, + label_names=None, + folds_case=True, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + self.assertEqual(exp.word_importance(label_idx=None).to_dict(), {"alpha": 0.5, "beta": 0.2}) + + def test_every_value_is_a_magnitude(self): + self.assertTrue((_word_profile_explanation().word_importance(label_idx=None) >= 0).all()) + + def test_name_records_the_statistic(self): + exp = _word_profile_explanation() + self.assertEqual(exp.word_importance(label_idx=None).name, "mean_across_classes") + self.assertEqual(exp.word_importance(label_idx=None, rank_by="sum").name, "sum_across_classes") + + def test_a_negative_sign_filter_has_nothing_to_select(self): + self.assertTrue(_word_profile_explanation().word_importance(label_idx=None, filter_sign="negative").empty) + + def test_the_frequency_floor_still_applies(self): + exp = _word_profile_explanation() + self.assertIn("happy", exp.word_importance(label_idx=None, min_occurrences=4).index) + self.assertNotIn("today", exp.word_importance(label_idx=None, min_occurrences=2).index) + + def test_plotter_titles_and_labels_the_cross_class_view(self): + fig = _word_profile_explanation().plot.word_importance(label_idx=None) + self.assertEqual(fig.layout.title.text, "Word importance — all classes") + self.assertEqual(fig.layout.xaxis.title.text, "Largest |mean SHAP| across classes") + + +class TestWordImportanceReadability(unittest.TestCase): + """The chart must name every bar it draws, whatever K is.""" + + @staticmethod + def _imp(n): + return pd.Series( + [(-1) ** i * (n - i) / n for i in range(n)], + index=[f"word{i}" for i in range(n)], + ) + + def test_every_word_gets_a_tick(self): + # dtick=1 on a categorical axis is what stops plotly thinning labels on a long ranking. + fig = plot_word_importance(self._imp(50)) + self.assertEqual(fig.layout.yaxis.dtick, 1) + self.assertEqual(len(set(fig.data[0].y)), 50) + + def test_height_grows_with_the_word_count(self): + heights = [plot_word_importance(self._imp(n)).layout.height for n in (5, 20, 50)] + self.assertEqual(heights, sorted(heights)) + # Every word keeps room to render, rather than the chart being squeezed to a fixed panel. + for n, h in zip((5, 20, 50), heights): + self.assertGreaterEqual(h / n, 24) + + def test_explicit_height_still_wins(self): + self.assertEqual(plot_word_importance(self._imp(50), height=300).layout.height, 300) + + def test_value_labels_are_signed_and_on_by_default(self): + fig = plot_word_importance(pd.Series([0.9, -0.5], index=["a", "b"])) + self.assertEqual(list(fig.data[0].text), ["-0.500", "+0.900"]) # reversed for drawing + self.assertEqual(fig.data[0].textposition, "outside") + # Outside text sits past the bar end and would be cut off at the plot edge without this. + self.assertFalse(fig.data[0].cliponaxis) + + def test_value_labels_get_headroom(self): + fig = plot_word_importance(pd.Series([1.0, -0.5], index=["a", "b"])) + lo, hi = fig.layout.xaxis.range + self.assertLess(lo, -0.5) + self.assertGreater(hi, 1.0) + + def test_value_labels_can_be_turned_off(self): + fig = plot_word_importance(pd.Series([0.9], index=["a"]), show_values=False) + self.assertIsNone(fig.data[0].text) + # No outside text means no need to reserve headroom; plotly autoscales. + self.assertIsNone(fig.layout.xaxis.range) + + def test_degenerate_inputs_autoscale_rather_than_collapse(self): + # An all-zero (or empty) series has no span to pad, and a [0, 0] range would be invalid. + self.assertIsNone(plot_word_importance(pd.Series(dtype=float)).layout.xaxis.range) + self.assertIsNone(plot_word_importance(pd.Series([0.0, 0.0], index=["a", "b"])).layout.xaxis.range) + + def test_tick_font_is_set(self): + self.assertEqual(plot_word_importance(self._imp(3)).layout.yaxis.tickfont.size, 12) + + +class TestWordImportanceHover(unittest.TestCase): + """The hover names the statistic and, when supplied, how many occurrences it aggregated.""" + + imp = pd.Series([0.9, -0.5], index=["a", "b"]) + + def test_hover_names_the_statistic_on_the_axis(self): + # The axis title scrolls off a long chart; the hover is where the reader can still check + # whether they are looking at a mean or a total. + fig = plot_word_importance(self.imp, x_title="Total SHAP contribution") + self.assertIn("Total SHAP contribution: %{x:.4f}", fig.data[0].hovertemplate) + + def test_counts_are_reversed_with_the_bars(self): + fig = plot_word_importance(self.imp, counts={"a": 12, "b": 3}) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) + self.assertIn("Occurrences:", fig.data[0].hovertemplate) + + def test_counts_accept_a_series(self): + fig = plot_word_importance(self.imp, counts=pd.Series({"a": 12, "b": 3})) + self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) + + def test_without_counts_the_hover_is_unchanged(self): + fig = plot_word_importance(self.imp) + self.assertIsNone(fig.data[0].customdata) + self.assertNotIn("Occurrences", fig.data[0].hovertemplate) + + def test_a_partial_mapping_is_dropped_rather_than_shown_as_zero(self): + # A missing word can only mean the caller counted under different filters, which would + # misreport every bar — not just the uncovered one. + fig = plot_word_importance(self.imp, counts={"a": 12}) + self.assertIsNone(fig.data[0].customdata) + self.assertNotIn("Occurrences", fig.data[0].hovertemplate) + + +class TestWordCounts(unittest.TestCase): + """The frequency table behind the word picker's order, labels and threshold.""" + + def setUp(self): + self.explanation = _word_profile_explanation() + + def test_occurrences_and_samples_differ_for_a_repeated_word(self): + counts = self.explanation.word_counts() + # "happy" occurs 4x (twice in one sample) across 3 samples. + self.assertEqual(counts.loc["happy", "n_occurrences"], 4) + self.assertEqual(counts.loc["happy", "n_samples"], 3) + + def test_sorted_by_frequency_then_alphabetically(self): + counts = self.explanation.word_counts() + self.assertEqual(counts.index[0], "happy") + ties = counts[counts["n_occurrences"] == 1].index.tolist() + self.assertEqual(ties, sorted(ties)) + + def test_applies_the_same_filters_as_word_importance(self): + counts = self.explanation.word_counts() + self.assertNotIn("[CLS]", counts.index) + self.assertNotIn("!", counts.index) + self.assertNotIn("Happy", counts.index) # folded into "happy" + + def test_index_is_exactly_the_vocabulary(self): + self.assertEqual(sorted(self.explanation.word_counts().index), self.explanation.vocabulary()) + + def test_scoped_to_sample_indices(self): + counts = self.explanation.word_counts(sample_indices=[1]) + self.assertEqual(counts.loc["happy", "n_occurrences"], 2) + self.assertEqual(counts.loc["happy", "n_samples"], 1) + + def test_counts_are_the_denominator_word_importance_filters_on(self): + # The contract the min-occurrence control depends on: the count shown beside a word is the + # count its aggregate was computed over. + counts = self.explanation.word_counts() + for word, n in counts["n_occurrences"].items(): + kept = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n)) + self.assertIn(word, kept.index, word) + dropped = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n) + 1) + self.assertNotIn(word, dropped.index, word) + + def test_empty_batch_returns_an_empty_frame(self): + empty = _minimal_word_explanation([[]], [np.zeros((0, 2))], None) + self.assertTrue(empty.word_counts().empty) + self.assertEqual(empty.vocabulary(), []) + + +class TestWordOccurrences(unittest.TestCase): + def setUp(self): + self.explanation = _word_profile_explanation() + + def test_one_row_per_occurrence_and_class(self): + occ = self.explanation.word_occurrences("happy") + # 4 occurrences (one in sample 0, two in sample 1, one in sample 2) x 2 classes. + self.assertEqual(len(occ), 8) + self.assertEqual(list(occ.columns), ["sample", "token_pos", "token", "class_idx", "contribution"]) + + def test_case_folding_follows_the_model(self): + self.assertEqual(len(self.explanation.word_occurrences("HAPPY")), 8) + cased = _word_profile_explanation(folds_case=False) + self.assertEqual(len(cased.word_occurrences("happy")), 6) + self.assertEqual(len(cased.word_occurrences("Happy")), 2) + + def test_explicit_lowercase_overrides(self): + cased = _word_profile_explanation(folds_case=False) + self.assertEqual(len(cased.word_occurrences("happy", lowercase=True)), 8) + + def test_token_keeps_original_casing(self): + occ = self.explanation.word_occurrences("happy") + self.assertIn("Happy", set(occ["token"])) + + def test_sample_indices_scope(self): + occ = self.explanation.word_occurrences("happy", sample_indices=[2]) + self.assertEqual(set(occ["sample"]), {2}) + self.assertEqual(len(occ), 2) + + def test_missing_word_returns_typed_empty_frame(self): + occ = self.explanation.word_occurrences("absent") + self.assertTrue(occ.empty) + self.assertEqual(occ["contribution"].dtype, np.float64) + # An empty frame must still support the numeric work the callers do on it. + self.assertTrue(aggregate_word_contributions(occ, "mean_abs").empty) + self.assertTrue(rank_word_samples(occ, class_idx=0).empty) + + def test_binary_1d_values(self): + explanation = _minimal_word_explanation( + [["good", "day"], ["good"]], + [np.array([0.5, 0.1]), np.array([-0.3])], + None, + ) + occ = explanation.word_occurrences("good") + self.assertEqual(set(occ["class_idx"]), {0}) + self.assertEqual(sorted(occ["contribution"].round(3)), [-0.3, 0.5]) + + +class TestAggregateWordContributions(unittest.TestCase): + def setUp(self): + self.occ = _word_profile_explanation().word_occurrences("happy") + + def test_mean_is_signed(self): + stats = aggregate_word_contributions(self.occ, "mean") + # class 0: (0.4 + 0.2 + 0.1 - 0.6) / 4 + self.assertAlmostEqual(stats[0], 0.025) + self.assertAlmostEqual(stats[1], -0.025) + + def test_sum_scales_with_frequency(self): + stats = aggregate_word_contributions(self.occ, "sum") + self.assertAlmostEqual(stats[0], 0.1) + + def test_abs_forms_expose_the_two_way_word(self): + signed = aggregate_word_contributions(self.occ, "mean") + magnitude = aggregate_word_contributions(self.occ, "mean_abs") + # The whole point of the abs forms: a word averaging to ~0 is not a weak word. + self.assertLess(abs(signed[0]), 0.05) + self.assertAlmostEqual(magnitude[0], 0.325) + self.assertAlmostEqual(aggregate_word_contributions(self.occ, "sum_abs")[0], 1.3) + + def test_index_is_every_class_in_order(self): + self.assertEqual(list(aggregate_word_contributions(self.occ, "sum").index), [0, 1]) + + def test_unknown_agg_raises(self): + with self.assertRaisesRegex(ValueError, "not one of"): + aggregate_word_contributions(self.occ, "median") + + def test_all_documented_ops_run(self): + for agg in WORD_AGGREGATIONS: + self.assertEqual(len(aggregate_word_contributions(self.occ, agg)), 2) + + +class TestRankWordSamples(unittest.TestCase): + def setUp(self): + self.occ = _word_profile_explanation().word_occurrences("happy") + + def test_occurrences_are_summed_within_a_sample(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="most") + row = ranked[ranked["sample"] == 1].iloc[0] + self.assertAlmostEqual(row["contribution"], 0.3) # 0.2 + 0.1 + self.assertEqual(row["n_occurrences"], 2) + # One row per sample, never one per occurrence. + self.assertEqual(len(ranked), ranked["sample"].nunique()) + + def test_most_ranks_positive_first(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="most") + self.assertEqual(ranked["sample"].tolist(), [0, 1, 2]) + + def test_least_ranks_negative_first(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="least") + self.assertEqual(ranked["sample"].iloc[0], 2) + + def test_strongest_ignores_sign(self): + ranked = rank_word_samples(self.occ, class_idx=0, order="strongest") + self.assertEqual(ranked["sample"].iloc[0], 2) # |-0.6| is the largest + + def test_class_idx_selects_the_column(self): + pos = rank_word_samples(self.occ, class_idx=0, order="most")["contribution"].tolist() + neg = rank_word_samples(self.occ, class_idx=1, order="most")["contribution"].tolist() + self.assertNotEqual(pos, neg) + + def test_n_top_truncates(self): + self.assertEqual(len(rank_word_samples(self.occ, class_idx=0, n_top=1)), 1) + + def test_unknown_order_raises(self): + with self.assertRaisesRegex(ValueError, "must be one of"): + rank_word_samples(self.occ, class_idx=0, order="alphabetical") + + def test_absent_class_returns_typed_empty(self): + ranked = rank_word_samples(self.occ, class_idx=99) + self.assertTrue(ranked.empty) + self.assertEqual(list(ranked.columns), ["sample", "contribution", "n_occurrences"]) + + +class TestWordContributionsBySample(unittest.TestCase): + def setUp(self): + self.explanation = _word_profile_explanation() + + def test_sums_one_words_occurrences_per_sample(self): + # class 0: sample0 has one "happy" (0.4), sample1 has two ("Happy" 0.2 + "happy" 0.1 = 0.3), + # sample2 has one (-0.6), sample3 has none. + result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + np.testing.assert_allclose(result, [0.4, 0.3, -0.6, 0.0]) + + def test_sums_across_words_within_a_sample(self): + # "today" only occurs in sample0 (0.05, class 0), added on top of "happy"'s own 0.4. + result = word_contributions_by_sample(self.explanation, ["happy", "today"], label_idx=0) + np.testing.assert_allclose(result, [0.45, 0.3, -0.6, 0.0]) + + def test_label_idx_selects_the_class(self): + class0 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + class1 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=1) + np.testing.assert_allclose(class1, -class0) + + def test_absent_word_returns_all_zeros(self): + result = word_contributions_by_sample(self.explanation, ["nonexistent"], label_idx=0) + np.testing.assert_allclose(result, np.zeros(len(self.explanation))) + + def test_result_length_matches_the_batch(self): + result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) + self.assertEqual(len(result), len(self.explanation)) diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/nlp/test_nlp_plotter.py index 09277a6f..65cb60d5 100644 --- a/tests/unit_tests/nlp/test_nlp_plotter.py +++ b/tests/unit_tests/nlp/test_nlp_plotter.py @@ -296,5 +296,70 @@ def test_raises_without_ground_truth(self): _make_explanation(with_true=False).confusion_matrix() +def _word_profile_explanation(folds_case=True): + """Three samples, two classes, with 'happy' appearing in three of them (twice in one).""" + token_strings = [ + ["[CLS]", "so", "happy", "today", "!"], + ["Happy", "and", "happy", "again"], + ["not", "happy", "at", "all"], + ["nothing", "here"], + ] + values = [ + np.array([[0.0, 0.0], [0.1, -0.1], [0.4, -0.4], [0.05, -0.05], [0.0, 0.0]]), + np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1], [0.0, 0.0]]), + np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), + np.array([[0.0, 0.0], [0.0, 0.0]]), + ] + texts = pd.Series(["so happy today !", "Happy and happy again", "not happy at all", "nothing here"]) + return NlpExplanation( + texts=texts, + token_strings=token_strings, + values=values, + base_values=None, + y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), + y_prob=None, + y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), + label_names=["pos", "neg"], + folds_case=folds_case, + backend_name="nlp_shap", + is_additive=True, + reference_kind="none", + output_space="probability", + ) + + +class TestPlotterWordProfile(unittest.TestCase): + def setUp(self): + self.explanation = _word_profile_explanation() + + def test_returns_figure_titled_with_counts(self): + fig = self.explanation.plot.word_profile("happy") + self.assertIsInstance(fig, go.Figure) + self.assertIn("4 occurrence(s) in 3 sample(s)", fig.layout.title.text) + + def test_mean_draws_error_bars_and_sum_does_not(self): + self.assertIsNotNone(self.explanation.plot.word_profile("happy", agg="mean").data[0].error_x.array) + self.assertIsNone(self.explanation.plot.word_profile("happy", agg="sum").data[0].error_x.array) + + def test_single_occurrence_spread_is_zero_not_nan(self): + fig = self.explanation.plot.word_profile("today", agg="mean") + self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) + + def test_sample_indices_scope(self): + fig = self.explanation.plot.word_profile("happy", sample_indices=[1]) + self.assertIn("2 occurrence(s) in 1 sample(s)", fig.layout.title.text) + + def test_title_override(self): + self.assertEqual(self.explanation.plot.word_profile("happy", title="X").layout.title.text, "X") + + def test_absent_word_raises_rather_than_drawing_an_empty_chart(self): + with self.assertRaisesRegex(ValueError, "does not occur"): + self.explanation.plot.word_profile("absent") + + def test_absent_in_scope_names_the_scope(self): + with self.assertRaisesRegex(ValueError, "in the selected samples"): + self.explanation.plot.word_profile("happy", sample_indices=[3]) + + if __name__ == "__main__": unittest.main() From 7a9f1d067f269551751990a507014a3d2ce0c290 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 16 Sep 2026 17:46:03 +0200 Subject: [PATCH 119/144] test(plots): split plot-function coverage into tests/unit_tests/plots The last remaining content of test_nlp_prototype.py was 8 self-contained classes testing shapash/plots/plot_*.py functions directly. Split each into its own file under a new tests/unit_tests/plots/ directory, mirroring the shapash/ package convention used elsewhere (compute/, explainer/, backend/, webapp/). test_nlp_prototype.py is now fully drained and removed. Pure code movement, no shared fixtures between these classes; full suite pass count verified unchanged (1491 passed, same as pre-split baseline). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- tests/unit_tests/nlp/test_nlp_prototype.py | 2759 ----------------- .../plots/test_plot_confusion_matrix.py | 51 + .../plots/test_plot_noise_matrix.py | 47 + tests/unit_tests/plots/test_plot_scatter.py | 58 + .../plots/test_plot_sentence_highlight.py | 36 + .../plots/test_plot_token_highlight.py | 46 + tests/unit_tests/plots/test_plot_waterfall.py | 77 + .../plots/test_plot_word_importance.py | 36 + .../plots/test_plot_word_profile.py | 50 + 9 files changed, 401 insertions(+), 2759 deletions(-) delete mode 100644 tests/unit_tests/nlp/test_nlp_prototype.py create mode 100644 tests/unit_tests/plots/test_plot_confusion_matrix.py create mode 100644 tests/unit_tests/plots/test_plot_noise_matrix.py create mode 100644 tests/unit_tests/plots/test_plot_scatter.py create mode 100644 tests/unit_tests/plots/test_plot_sentence_highlight.py create mode 100644 tests/unit_tests/plots/test_plot_token_highlight.py create mode 100644 tests/unit_tests/plots/test_plot_waterfall.py create mode 100644 tests/unit_tests/plots/test_plot_word_importance.py create mode 100644 tests/unit_tests/plots/test_plot_word_profile.py diff --git a/tests/unit_tests/nlp/test_nlp_prototype.py b/tests/unit_tests/nlp/test_nlp_prototype.py deleted file mode 100644 index 9ef58827..00000000 --- a/tests/unit_tests/nlp/test_nlp_prototype.py +++ /dev/null @@ -1,2759 +0,0 @@ -"""Integration tests for the NLP explanation prototype. - -Covers the full wiring between NlpContributions (backend), word_importance -aggregation, plot_token_highlight / plot_word_importance (plots), NlpExplainer -(explainer), and NlpWebApp (webapp layout construction). A real NLP model is -not required — synthetic NlpContributions data is used throughout so the suite -runs in CI without transformers/datasets. -""" - -import json -import tempfile -import unittest -import zipfile -from dataclasses import replace -from unittest.mock import MagicMock, PropertyMock, patch - -import numpy as np -import pandas as pd -import plotly.graph_objs as go -from dash import html, jupyter_dash -from dash.exceptions import PreventUpdate - -from shapash.backend.nlp_backend import NlpBackend, NlpContributions -from shapash.backend.nlp_lime_backend import NlpLimeBackend -from shapash.backend.nlp_shap_backend import NlpShapBackend -from shapash.compute.embeddings import Embedding -from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator -from shapash.compute.retrieval import Neighbor -from shapash.explainer.nlp_explainer import NlpExplainer -from shapash.explainer.nlp_explanation import ( - WORD_AGGREGATIONS, - NlpExplanation, - aggregate_word_contributions, - rank_word_samples, - word_contributions_by_sample, -) -from shapash.model.base import SupportsEmbeddings, SupportsGradients, SupportsTokenization, TextModel -from shapash.plots.plot_confusion_matrix import plot_confusion_matrix -from shapash.plots.plot_noise_matrix import plot_noise_matrix -from shapash.plots.plot_scatter import plot_scatter -from shapash.plots.plot_sentence_highlight import plot_sentence_highlight -from shapash.plots.plot_token_highlight import plot_token_highlight -from shapash.plots.plot_waterfall import plot_waterfall -from shapash.plots.plot_word_importance import plot_word_importance -from shapash.plots.plot_word_profile import plot_word_profile -from shapash.style.style_utils import DEFAULT_NLP_THEME -from shapash.webapp.nlp_app import NlpWebApp -from shapash.webapp.nlp_components import compose_selection as _compose_selection -from shapash.webapp.nlp_components import error_positions -from shapash.webapp.nlp_components.datapoint import pack_datapoint, unpack_datapoint -from shapash.webapp.nlp_components import error_analysis as error_analysis_module -from shapash.webapp.nlp_components.error_analysis import ErrorAnalysisComponent, _cell_from_click -from shapash.webapp.utils.launch import RunningApp - -LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] -N_CLASSES = len(LABEL_NAMES) - -# --------------------------------------------------------------------------- -# Shared fixtures -# --------------------------------------------------------------------------- - - -def _make_token_data() -> tuple[list[list[str]], list[np.ndarray], np.ndarray]: - """Synthetic per-token contribution data for 3 samples, 6 classes.""" - rng = np.random.default_rng(42) - token_strings = [ - ["", "i", "feel", "so", "happy", "today", ""], - ["", "this", "is", "terrible", "and", "sad", ""], - ["", "what", "a", "wonderful", "day", ""], - ] - values = [rng.uniform(-0.4, 0.4, size=(len(t), N_CLASSES)).astype(np.float32) for t in token_strings] - base_values = rng.uniform(-0.1, 0.1, size=(3, N_CLASSES)).astype(np.float32) - return token_strings, values, base_values - - -def _make_texts() -> pd.Series: - return pd.Series( - ["i feel so happy today", "this is terrible and sad", "what a wonderful day"], - index=pd.RangeIndex(3), - ) - - -def _make_explanation( - texts: pd.Series | None = None, - y_pred: pd.Series | None = None, - y_prob: pd.DataFrame | None = None, - y_true: pd.Series | None = None, - label_names: list[str] | None = LABEL_NAMES, - backend_name: str = "nlp_shap", - is_additive: bool = True, - reference_kind: str = "none", - output_space: str = "probability", -) -> NlpExplanation: - """Synthetic ``NlpExplanation`` for 3 samples, 6 classes — the ``explain()`` return value.""" - texts = _make_texts() if texts is None else texts - token_strings, values, base_values = _make_token_data() - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=base_values, - y_pred=( - pd.Series(["joy", "sadness", "joy"], index=texts.index, name="prediction") if y_pred is None else y_pred - ), - y_prob=y_prob, - y_true=y_true, - label_names=label_names, - folds_case=None, - backend_name=backend_name, - is_additive=is_additive, - reference_kind=reference_kind, - output_space=output_space, - ) - - -def _minimal_explanation( - token_strings: list[list[str]], - values: list[np.ndarray], - base_values: np.ndarray | None, - folds_case: bool | None = None, -) -> NlpExplanation: - """Bare ``NlpExplanation`` for word-importance/case-folding tests — only the token data matters.""" - n = len(token_strings) - texts = pd.Series([""] * n, index=pd.RangeIndex(n)) - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=base_values, - y_pred=pd.Series([""] * n, index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=None, - folds_case=folds_case, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - -def _make_explainer() -> NlpExplainer: - """Bare ``NlpExplainer`` (engine role only), bypassing ``__init__`` to avoid ``shap.Explainer(None)``. - - Per ``fit``/``explain``, the explainer never holds compiled results — tests that need a computed - batch build an :class:`NlpExplanation` directly with :func:`_make_explanation` instead. - """ - xpl = object.__new__(NlpExplainer) - xpl.model = None - xpl.label_names = LABEL_NAMES - xpl.backend = None - return xpl - - -# --------------------------------------------------------------------------- -# NlpExplanation.word_importance / resolve_lowercase -# --------------------------------------------------------------------------- - - -class TestWordImportance(unittest.TestCase): - def setUp(self): - token_strings, values, base_values = _make_token_data() - self.explanation = _minimal_explanation(token_strings, values, base_values) - - def test_len(self): - self.assertEqual(len(self.explanation), 3) - - def test_word_importance_returns_series(self): - imp = self.explanation.word_importance(label_idx=1) - self.assertIsInstance(imp, pd.Series) - - def test_word_importance_filters_special_tokens(self): - imp = self.explanation.word_importance(label_idx=0, filter_special=True) - self.assertNotIn("", imp.index) - self.assertNotIn(" ", imp.index) - - def test_word_importance_keeps_special_when_disabled(self): - imp = self.explanation.word_importance(label_idx=0, filter_special=False) - # Empty strings (BOS/EOS) should now be present - self.assertIn("", imp.index) - - def test_word_importance_hides_punctuation_by_default(self): - explanation = _minimal_explanation( - token_strings=[["great", "!", "!", "movie", ".", ","]], - values=[np.array([[3.0], [9.0], [9.0], [2.0], [8.0], [7.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(sorted(imp.index), ["great", "movie"]) - - def test_word_importance_keeps_punctuation_when_asked(self): - explanation = _minimal_explanation( - token_strings=[["great", "!", "!"]], - values=[np.array([[3.0], [9.0], [9.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20, filter_punctuation=False) - self.assertIn("!", imp.index) - # Punctuation kept means it can outrank real words, which is why it is hidden by default. - self.assertEqual(imp.index[0], "!") - - def test_word_importance_keeps_words_containing_punctuation(self): - # Only *pure* punctuation units are dropped — a hyphenated or apostrophised word stays. - explanation = _minimal_explanation( - token_strings=[["state-of-the-art", "don't", "-"]], - values=[np.array([[3.0], [2.0], [9.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(sorted(imp.index), ["don't", "state-of-the-art"]) - - def test_resolve_lowercase_follows_the_model(self): - for folds_case, expected in ((True, True), (False, False), (None, True)): - with self.subTest(folds_case=folds_case): - explanation = replace(self.explanation, folds_case=folds_case) - self.assertEqual(explanation.resolve_lowercase(), expected) - - def test_resolve_lowercase_explicit_argument_wins(self): - self.assertFalse(replace(self.explanation, folds_case=True).resolve_lowercase(False)) - self.assertTrue(replace(self.explanation, folds_case=False).resolve_lowercase(True)) - - def test_word_importance_keeps_case_on_a_cased_model(self): - # A cased tokenizer encodes AWFUL and awful to *different* ids, so they are different - # inputs with genuinely different attributions — merging them would hide that. - explanation = _minimal_explanation( - token_strings=[["AWFUL", "awful"]], - values=[np.array([[-9.0], [-3.0]])], - base_values=np.zeros((1, 1)), - folds_case=False, - ) - imp = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) - - def test_word_importance_merges_case_on_an_uncased_model(self): - # An uncased tokenizer maps both spellings to one input id — the model cannot tell them - # apart, so they must not appear as two rows. - explanation = _minimal_explanation( - token_strings=[["AWFUL", "awful"]], - values=[np.array([[-9.0], [-3.0]])], - base_values=np.zeros((1, 1)), - folds_case=True, - ) - imp = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(list(imp.index), ["awful"]) - self.assertAlmostEqual(imp["awful"], -6.0) - - def test_word_importance_merges_case_variants_by_default(self): - # One row per word, averaged over every casing — not three rows of one occurrence each. - explanation = _minimal_explanation( - token_strings=[["AWFUL", "Awful", "awful", "good"]], - values=[np.array([[-9.0], [-3.0], [-3.0], [1.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(sorted(imp.index), ["awful", "good"]) - self.assertAlmostEqual(imp["awful"], -5.0) - - def test_word_importance_case_fragmentation_lets_rare_variants_outrank(self): - # The reason lowercasing is the default: a single capitalised occurrence keeps its own - # extreme value instead of being averaged into the 3 common ones, and tops the ranking. - explanation = _minimal_explanation( - token_strings=[["TERRIBLE", *["terrible"] * 9, "dull"]], - values=[np.array([[-9.0], *[[-0.1]] * 9, [-2.0]])], - base_values=np.zeros((1, 1)), - ) - cased = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) - self.assertEqual(cased.index[0], "TERRIBLE") - folded = explanation.word_importance(label_idx=0, n_top=20) - self.assertEqual(folded.index[0], "dull") - - def test_word_importance_keeps_case_when_disabled(self): - explanation = _minimal_explanation( - token_strings=[["AWFUL", "awful"]], - values=[np.array([[-9.0], [-3.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20, lowercase=False) - self.assertEqual(sorted(imp.index), ["AWFUL", "awful"]) - - def test_word_importance_exclusion_is_case_insensitive_when_folding(self): - # The webapp's dropdown offers lowercase entries; excluding one must drop every casing. - explanation = _minimal_explanation( - token_strings=[["AWFUL", "awful", "good"]], - values=[np.array([[-9.0], [-3.0], [1.0]])], - base_values=np.zeros((1, 1)), - ) - imp = explanation.word_importance(label_idx=0, n_top=20, exclude_words={"AwFuL"}) - self.assertEqual(list(imp.index), ["good"]) - - def test_word_importance_respects_n_top(self): - imp = self.explanation.word_importance(label_idx=1, n_top=3) - self.assertLessEqual(len(imp), 3) - - def test_word_importance_sorted_by_absolute_value(self): - imp = self.explanation.word_importance(label_idx=2, n_top=20) - abs_vals = imp.abs().tolist() - self.assertEqual(abs_vals, sorted(abs_vals, reverse=True)) - - def test_word_importance_aggregates_repeated_words(self): - # "feel" only appears once; check it has a single contribution value - imp = self.explanation.word_importance(label_idx=1, n_top=20, filter_special=True) - self.assertIn("feel", imp.index) - # Should be a scalar (mean of one occurrence) - self.assertIsInstance(imp["feel"], float) - - def test_word_importance_all_labels(self): - for idx in range(N_CLASSES): - imp = self.explanation.word_importance(label_idx=idx) - self.assertIsInstance(imp, pd.Series) - self.assertGreater(len(imp), 0) - - def test_word_importance_filter_sign_positive(self): - imp = self.explanation.word_importance(label_idx=0, filter_sign="positive") - if len(imp) > 0: - self.assertTrue((imp > 0).all(), "positive filter should return only positive values") - - def test_word_importance_filter_sign_negative(self): - imp = self.explanation.word_importance(label_idx=0, filter_sign="negative") - if len(imp) > 0: - self.assertTrue((imp < 0).all(), "negative filter should return only negative values") - - def test_word_importance_exclude_words(self): - imp_full = self.explanation.word_importance(label_idx=1, filter_special=True) - if len(imp_full) == 0: - return - word_to_exclude = imp_full.index[0] - imp_filtered = self.explanation.word_importance( - label_idx=1, filter_special=True, exclude_words={word_to_exclude} - ) - self.assertNotIn(word_to_exclude, imp_filtered.index) - - def test_word_importance_exclude_words_empty_set(self): - imp_no_exclude = self.explanation.word_importance(label_idx=1, exclude_words=set()) - imp_none_exclude = self.explanation.word_importance(label_idx=1, exclude_words=None) - pd.testing.assert_series_equal(imp_no_exclude, imp_none_exclude) - - def test_word_importance_sample_indices(self): - imp = self.explanation.word_importance(label_idx=0, sample_indices=[0], n_top=50) - self.assertGreater(len(imp), 0) - # "terrible" only exists in sample 1 — must not appear in the sample-0 subset - self.assertNotIn("terrible", imp.index) - - -# --------------------------------------------------------------------------- -# plot_sentence_highlight -# --------------------------------------------------------------------------- - - -class TestPlotSentenceHighlight(unittest.TestCase): - def setUp(self): - self.tokens = ["[CLS]", "i", "feel", "happy", "[SEP]"] - self.values = np.array([0.01, 0.05, 0.30, -0.20, 0.01]) - - def test_returns_html_div(self): - result = plot_sentence_highlight(self.tokens, self.values) - self.assertIsInstance(result, html.Div) - - def test_has_children(self): - result = plot_sentence_highlight(self.tokens, self.values) - self.assertIsNotNone(result.children) - # legend + spans div + summary - self.assertGreaterEqual(len(result.children), 3) - - def test_raises_on_2d_values(self): - with self.assertRaises(ValueError): - plot_sentence_highlight(self.tokens, np.zeros((5, 3))) - - def test_with_base_value_does_not_raise(self): - result = plot_sentence_highlight(self.tokens, self.values, base_value=0.15) - self.assertIsInstance(result, html.Div) - - def test_empty_tokens_does_not_raise(self): - result = plot_sentence_highlight([], np.array([])) - self.assertIsInstance(result, html.Div) - - -# --------------------------------------------------------------------------- -# plot_waterfall -# --------------------------------------------------------------------------- - - -class TestPlotWaterfall(unittest.TestCase): - def setUp(self): - self.tokens = ["[CLS]", "i", "feel", "so", "happy", "today", "[SEP]"] - self.values = np.array([0.01, 0.08, 0.35, 0.02, -0.20, 0.05, 0.01]) - - def test_returns_figure(self): - fig = plot_waterfall(self.tokens, self.values) - self.assertIsInstance(fig, go.Figure) - - def test_has_waterfall_trace(self): - fig = plot_waterfall(self.tokens, self.values) - self.assertEqual(len(fig.data), 1) - self.assertIsInstance(fig.data[0], go.Waterfall) - - def test_filters_special_tokens_by_default(self): - fig = plot_waterfall(self.tokens, self.values) - y_labels = list(fig.data[0].y) - for label in y_labels: - self.assertNotIn("[CLS]", label) - self.assertNotIn("[SEP]", label) - - def test_keeps_special_tokens_when_disabled(self): - # min_pct=0 disables grouping so every token gets its own bar - fig = plot_waterfall(self.tokens, self.values, filter_special=False, min_pct=0.0) - y_labels = list(fig.data[0].y) - self.assertTrue(any("[CLS]" in lbl for lbl in y_labels)) - - def test_total_bar_present(self): - fig = plot_waterfall(self.tokens, self.values) - measures = list(fig.data[0].measure) - self.assertIn("total", measures) - - def test_grouping_reduces_bar_count(self): - # With min_pct=0.0 (no grouping), every non-special token is its own bar - fig_no_group = plot_waterfall(self.tokens, self.values, min_pct=0.0) - # With min_pct=0.5, small tokens are lumped - fig_grouped = plot_waterfall(self.tokens, self.values, min_pct=0.5) - self.assertLessEqual(len(fig_grouped.data[0].y), len(fig_no_group.data[0].y)) - - def test_other_bar_present_when_grouping_active(self): - fig = plot_waterfall(self.tokens, self.values, min_pct=0.5) - y_labels = list(fig.data[0].y) - self.assertTrue(any("other" in lbl for lbl in y_labels)) - - def test_no_other_bar_when_min_pct_zero(self): - fig = plot_waterfall(self.tokens, self.values, min_pct=0.0) - y_labels = list(fig.data[0].y) - self.assertFalse(any("other" in lbl for lbl in y_labels)) - - def test_base_value_creates_absolute_bar(self): - fig = plot_waterfall(self.tokens, self.values, base_value=0.20) - measures = list(fig.data[0].measure) - self.assertEqual(measures[0], "absolute") - y_labels = list(fig.data[0].y) - self.assertEqual(y_labels[0], "Base") - - def test_empty_tokens_returns_figure(self): - fig = plot_waterfall([], np.array([])) - self.assertIsInstance(fig, go.Figure) - - def test_all_special_tokens_returns_figure(self): - fig = plot_waterfall(["[CLS]", "[SEP]"], np.array([0.01, -0.01])) - self.assertIsInstance(fig, go.Figure) - - def test_custom_title(self): - fig = plot_waterfall(self.tokens, self.values, title="joy waterfall") - self.assertIn("joy waterfall", fig.layout.title.text) - - -# --------------------------------------------------------------------------- -# plot_token_highlight -# --------------------------------------------------------------------------- - - -class TestPlotTokenHighlight(unittest.TestCase): - def setUp(self): - self.tokens = ["i", "feel", "happy", "today"] - self.values = np.array([0.1, 0.4, -0.2, 0.05]) - - def test_returns_figure(self): - fig = plot_token_highlight(self.tokens, self.values) - self.assertIsInstance(fig, go.Figure) - - def test_has_one_bar_trace(self): - fig = plot_token_highlight(self.tokens, self.values) - self.assertEqual(len(fig.data), 1) - self.assertIsInstance(fig.data[0], go.Bar) - - def test_max_tokens_limits_bars(self): - fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) - self.assertLessEqual(len(fig.data[0].x), 2) - - def test_max_tokens_preserves_sentence_order(self): - # top-2 by |value| are "feel" (0.4) and "happy" (-0.2) - # sentence order: "feel" before "happy" - fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) - # y-axis is reversed for display (highest importance at top → reversed list) - displayed_labels = list(fig.data[0].y) - feel_pos = displayed_labels.index("feel") - happy_pos = displayed_labels.index("happy") - # In reversed display: "feel" appears after "happy" (lower index = further down) - self.assertNotEqual(feel_pos, happy_pos) - - def test_custom_title(self): - fig = plot_token_highlight(self.tokens, self.values, title="Test title") - self.assertIn("Test title", fig.layout.title.text) - - def test_orientation_horizontal(self): - fig = plot_token_highlight(self.tokens, self.values) - self.assertEqual(fig.data[0].orientation, "h") - - -# --------------------------------------------------------------------------- -# plot_word_importance -# --------------------------------------------------------------------------- - - -class TestPlotWordImportance(unittest.TestCase): - def setUp(self): - self.word_imp = pd.Series( - {"happy": 0.35, "terrible": -0.28, "wonderful": 0.20, "feel": -0.10}, - ) - - def test_returns_figure(self): - fig = plot_word_importance(self.word_imp) - self.assertIsInstance(fig, go.Figure) - - def test_has_one_bar_trace(self): - fig = plot_word_importance(self.word_imp) - self.assertEqual(len(fig.data), 1) - self.assertIsInstance(fig.data[0], go.Bar) - - def test_all_words_rendered(self): - fig = plot_word_importance(self.word_imp) - self.assertEqual(len(fig.data[0].x), len(self.word_imp)) - - def test_custom_title(self): - fig = plot_word_importance(self.word_imp, title="Joy importance") - self.assertIn("Joy importance", fig.layout.title.text) - - def test_orientation_horizontal(self): - fig = plot_word_importance(self.word_imp) - self.assertEqual(fig.data[0].orientation, "h") - - -# --------------------------------------------------------------------------- -# plot_confusion_matrix -# --------------------------------------------------------------------------- - - -class TestPlotNoiseMatrix(unittest.TestCase): - """The noise matrix is given-vs-true and mostly diagonal, so it needs its own axes and masking.""" - - def setUp(self): - # Typical shape: ~94% correctly labelled, one contaminated pair (A labelled, really B). - self.joint = np.array([[0.50, 0.05, 0.0], [0.01, 0.30, 0.0], [0.0, 0.0, 0.14]]) - self.labels = ["A", "B", "C"] - - def test_returns_heatmap_figure(self): - self.assertIsInstance(plot_noise_matrix(self.joint, self.labels), go.Figure) - - def test_diagonal_is_masked_by_default(self): - # Left in, the ~90% diagonal flattens every off-diagonal cell to the same near-white shade. - z = plot_noise_matrix(self.joint, self.labels).data[0].z - self.assertTrue(np.all(np.isnan(np.diag(z)))) - self.assertAlmostEqual(z[0][1], 0.05) - - def test_masked_cells_render_no_text(self): - text = plot_noise_matrix(self.joint, self.labels).data[0].text - self.assertEqual(text[0][0], "") - self.assertEqual(text[0][1], "5.0%") - - def test_diagonal_can_be_kept(self): - z = plot_noise_matrix(self.joint, self.labels, mask_diagonal=False).data[0].z - np.testing.assert_allclose(np.diag(z), [0.50, 0.30, 0.14]) - - def test_does_not_mutate_the_callers_matrix(self): - original = self.joint.copy() - plot_noise_matrix(self.joint, self.labels) - np.testing.assert_array_equal(self.joint, original) - - def test_axes_name_the_label_semantics_not_the_prediction(self): - # The whole reason this is not a plot_confusion_matrix mode: the columns are the estimated - # *true* class, not the model's prediction. - layout = plot_noise_matrix(self.joint, self.labels).layout - self.assertEqual(layout.yaxis.title.text, "Given label") - self.assertEqual(layout.xaxis.title.text, "Estimated true class") - - -class TestPlotConfusionMatrix(unittest.TestCase): - def setUp(self): - # 3-class matrix: rows = true, cols = predicted. Row 2 (index 2) is empty. - self.cm = np.array([[5, 2, 0], [1, 4, 0], [0, 0, 0]]) - self.labels = ["A", "B", "C"] - - def test_returns_heatmap_figure(self): - fig = plot_confusion_matrix(self.cm, self.labels) - self.assertIsInstance(fig, go.Figure) - self.assertIsInstance(fig.data[0], go.Heatmap) - - def test_axes_are_labelled_true_and_predicted(self): - fig = plot_confusion_matrix(self.cm, self.labels) - self.assertEqual(list(fig.data[0].x), self.labels) - self.assertEqual(list(fig.data[0].y), self.labels) - - def test_customdata_encodes_pred_then_true(self): - # customdata[true][pred] must be [pred_idx, true_idx] for the click handler. - fig = plot_confusion_matrix(self.cm, self.labels) - cd = np.asarray(fig.data[0].customdata) - self.assertEqual(list(cd[0, 1]), [1, 0]) # true=0, pred=1 - self.assertEqual(list(cd[1, 2]), [2, 1]) # true=1, pred=2 - - def test_counts_shown_as_text(self): - fig = plot_confusion_matrix(self.cm, self.labels) - self.assertEqual(fig.data[0].text[0][0], "5") - - def test_normalize_true_is_row_recall(self): - fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") - z = np.asarray(fig.data[0].z) - np.testing.assert_allclose(z[0], [5 / 7, 2 / 7, 0.0]) - - def test_normalize_true_handles_empty_row_without_nan(self): - fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") - z = np.asarray(fig.data[0].z) - self.assertFalse(np.isnan(z).any()) - np.testing.assert_array_equal(z[2], [0.0, 0.0, 0.0]) - - def test_custom_title(self): - fig = plot_confusion_matrix(self.cm, self.labels, title="Errors") - self.assertIn("Errors", fig.layout.title.text) - - -# --------------------------------------------------------------------------- -# NlpExplainer (no real model) -# --------------------------------------------------------------------------- - - -class TestNlpExplainer(unittest.TestCase): - def setUp(self): - self.xpl = _make_explainer() - self.explanation = _make_explanation() - - def test_plot_tokens_returns_figure(self): - fig = self.explanation.plot.tokens(row=0, label_idx=1) - self.assertIsInstance(fig, go.Figure) - - def test_folds_case_is_none_without_a_tokenizing_model(self): - # A bare classifier_fn has no tokenizer to ask; None is the honest answer, not a guess. - self.assertIsNone(self.xpl._folds_case()) - - def test_folds_case_reads_the_model_capability(self): - class _Uncased(TextModel, SupportsTokenization): - def predict(self, texts): - return np.tile([0.5, 0.5], (len(texts), 1)) - - def tokenize(self, text): - return text.lower().split() - - def detokenize(self, tokens): - return " ".join(tokens) - - self.xpl._text_model = _Uncased(label_names=["neg", "pos"]) - self.assertTrue(self.xpl._folds_case()) - - def test_plot_tokens_all_samples(self): - for row in range(3): - fig = self.explanation.plot.tokens(row=row, label_idx=0) - self.assertIsInstance(fig, go.Figure) - - def test_plot_tokens_all_labels(self): - for label_idx in range(N_CLASSES): - fig = self.explanation.plot.tokens(row=0, label_idx=label_idx) - self.assertIsInstance(fig, go.Figure) - - def test_plot_tokens_max_tokens(self): - fig = self.explanation.plot.tokens(row=0, label_idx=1, max_tokens=3) - self.assertIsInstance(fig, go.Figure) - self.assertLessEqual(len(fig.data[0].x), 3) - - def test_y_pred_stored(self): - self.assertIsNotNone(self.explanation.y_pred) - self.assertEqual(len(self.explanation.y_pred), 3) - - def test_label_names_propagated(self): - self.assertEqual(self.explanation.label_names, LABEL_NAMES) - - def test_y_true_is_none_by_default(self): - self.assertIsNone(self.explanation.y_true) - - -class _ProjectableModel(TextModel, SupportsEmbeddings): - """Embeds each text to a deterministic 3-D point; counts calls so cache hits are observable.""" - - def __init__(self, space="decision"): - super().__init__(label_names=LABEL_NAMES[:2]) - self.space = space - self.calls = 0 - - def resolve_space(self, space=None): - return space if space is not None else self.space - - def predict(self, texts): - return np.tile([0.4, 0.6], (len(texts), 1)) - - def get_embedding_table(self): - return (["a"], np.zeros((1, 3))) - - def embed(self, texts, space=None): - self.calls += 1 - return np.array([[float(len(t)), float(t.count("a")), 1.0] for t in texts]) - - @property - def shap_callable(self): - return self.predict - - -class _CountingReducer: - """A deterministic reducer that counts its fits and records its call-time kwargs.""" - - def __init__(self, scale=1.0): - self.scale = scale - self.fits = 0 - self.last_kwargs = None - - def fit_transform(self, x, **kwargs): - self.fits += 1 - self.last_kwargs = kwargs - return np.asarray(x)[:, :2] * self.scale - - -def _projection_explanation(texts: pd.Series) -> NlpExplanation: - """A minimal ``NlpExplanation`` carrying only ``texts`` — all ``compute_projection`` needs.""" - n = len(texts) - return NlpExplanation( - texts=texts, - token_strings=[[] for _ in range(n)], - values=[np.zeros((0, 2)) for _ in range(n)], - base_values=np.zeros((n, 2)), - y_pred=pd.Series(["pos"] * n, index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=None, - folds_case=None, - backend_name="test", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - -class TestComputeProjection(unittest.TestCase): - """The library owns the space + the embedding cache; the caller injects only the reducer.""" - - def setUp(self): - self.model = _ProjectableModel() - self.xpl = NlpExplainer(self.model, backend=object()) - self.explanation = _projection_explanation(pd.Series(["alpha", "beta banana", "gamma"])) - - def test_returns_two_columns_aligned_with_the_texts(self): - projection = self.xpl.compute_projection(self.explanation) - self.assertEqual(projection.vectors.shape, (3, 2)) - self.assertEqual(projection.n_samples, self.explanation.n_samples) - - def test_defaults_to_pca_without_any_extra_dependency(self): - """The default reducer must be something a core install already has — sklearn's PCA.""" - projection = self.xpl.compute_projection(self.explanation) - self.assertEqual(projection.n_components, 2) - self.assertEqual(self.model.calls, 1) - - def test_injected_reducer_is_used(self): - reducer = _CountingReducer(scale=2.0) - projection = self.xpl.compute_projection(self.explanation, reducer=reducer) - self.assertEqual(reducer.fits, 1) - np.testing.assert_allclose(projection.vectors[0], [10.0, 4.0]) # "alpha": len 5, 2 a's, doubled - - def test_projection_carries_the_provenance_needed_to_pair_it_back(self): - # The point of returning an Embedding rather than a bare array: the coordinates say which - # corpus, model and space they belong to, so a mismatch can be caught instead of drawn. - projection = self.xpl.compute_projection(self.explanation) - self.assertEqual(projection.corpus_id, self.explanation.corpus_id) - self.assertEqual(projection.model_id, self.model.model_id) - self.assertEqual(projection.space, self.model.resolve_space()) - self.assertEqual(projection.reducer_tag, "pca") - - def test_embeddings_are_raw_and_reusable_for_several_projections(self): - # The flow the split exists for: embed once (needs the model), reduce many times (does not). - embedding = self.xpl.compute_embeddings(self.explanation) - self.assertEqual(self.model.calls, 1) - self.assertIsNone(embedding.reducer_tag) - self.assertEqual(embedding.corpus_id, self.explanation.corpus_id) - - first = embedding.project(_CountingReducer(scale=1.0)) - second = embedding.project(_CountingReducer(scale=3.0)) - self.assertEqual(self.model.calls, 1, "reducing must not re-embed") - np.testing.assert_allclose(second.vectors, first.vectors * 3.0) - - def test_raises_for_a_model_that_cannot_embed(self): - """A prediction-only model gets a clear error pointing at the escape hatch, not an AttributeError.""" - xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) - explanation = _projection_explanation(pd.Series(["alpha", "beta"])) - with self.assertRaises(TypeError): - xpl.compute_projection(explanation) - - def test_embeddings_cached_across_instances_while_the_reducer_reruns(self): - # Only the expensive half is cached; a layout worth keeping is saved explicitly. - with tempfile.TemporaryDirectory() as d: - reducer = _CountingReducer() - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) - - fresh_model = _ProjectableModel() - fresh = NlpExplainer(fresh_model, backend=object()) - fresh.compute_projection(self.explanation, reducer=reducer, cache_dir=d) - self.assertEqual(fresh_model.calls, 0) - self.assertEqual(reducer.fits, 2) - - def test_model_space_takes_part_in_the_key(self): - """Moving the model's space must re-embed, not reload the other space's vectors.""" - with tempfile.TemporaryDirectory() as d: - self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) - self.model.space = "pooled" - self.model.calls = 0 - self.xpl.compute_projection(self.explanation, reducer=_CountingReducer(), cache_dir=d) - self.assertEqual(self.model.calls, 1) # re-embedded under the new space - - def test_recompute_forces_a_fresh_fit(self): - with tempfile.TemporaryDirectory() as d: - reducer = _CountingReducer() - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d) - self.model.calls = 0 - self.xpl.compute_projection(self.explanation, reducer=reducer, cache_dir=d, recompute=True) - self.assertEqual(self.model.calls, 1) - self.assertEqual(reducer.fits, 2) - - def test_fit_transform_kwargs_are_forwarded(self): - reducer = _CountingReducer() - self.xpl.compute_projection(self.explanation, reducer=reducer, init="pca", verbose=False) - self.assertEqual(reducer.last_kwargs, {"init": "pca", "verbose": False}) - -# --------------------------------------------------------------------------- -# NlpExplainer.fit — reference state, and when its cost is paid -# --------------------------------------------------------------------------- - - -class _UnembeddableModel(_ProjectableModel): - """Embeds nothing: stands in for a corpus the model chokes on (OOM, bad encoding).""" - - def embed(self, texts, space=None): - raise RuntimeError("cannot embed this corpus") - - -class TestFitPrecompute(unittest.TestCase): - CORPUS = ["a happy line", "a sad line", "another happy one"] - LABELS = ["joy", "sadness", "joy"] - - def test_precompute_embeds_the_bank_at_fit(self): - model = _ProjectableModel() - xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) - self.assertEqual(model.calls, 1) # the corpus, embedded once, inside fit - xpl.find_similar("a happy line") - self.assertEqual(model.calls, 2) # the query only — the bank was already there - - def test_precompute_false_defers_to_first_query(self): - model = _ProjectableModel() - xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS, precompute=False) - self.assertEqual(model.calls, 0) - xpl.find_similar("a happy line") - self.assertEqual(model.calls, 2) # bank + query, both charged to the first click - - def test_find_similar_threshold_filters_by_score_and_reports_total(self): - model = _ProjectableModel() - xpl = NlpExplainer(model, backend=object()).fit(self.CORPUS, y=self.LABELS) - neighbors, total = xpl.find_similar_threshold("a happy line", threshold=-1.0, limit=1) - self.assertLessEqual(len(neighbors), 1) - self.assertGreaterEqual(total, len(neighbors)) - - def test_find_similar_threshold_requires_a_retriever(self): - xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) - with self.assertRaises(RuntimeError): - xpl.find_similar_threshold("a happy line") - - def test_precompute_is_a_noop_when_no_retriever_was_built(self): - xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()).fit(self.CORPUS, y=self.LABELS) - self.assertFalse(xpl.can_find_similar()) # model cannot embed - self.assertTrue(xpl.can_probe_labels()) # ... but the model-free probe still fit - - def test_a_bank_failure_still_leaves_the_model_free_probe_usable(self): - xpl = NlpExplainer(_UnembeddableModel(), backend=object()) - with self.assertRaises(RuntimeError): - xpl.fit(self.CORPUS, y=self.LABELS) - # reference_/classes_ are assigned before the bank is built, so the half-fitted object keeps - # the feature that never needed the model. - self.assertEqual(xpl.reference_, (self.CORPUS, self.LABELS)) - self.assertEqual(xpl.classes_, LABEL_NAMES[:2]) # from the model, not derived from y - self.assertTrue(xpl.can_probe_labels()) - - -# --------------------------------------------------------------------------- -# NlpExplainer — counterfactual generator discovery / selection (captum-free) -# --------------------------------------------------------------------------- - - -class _FullCapModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients): - """A model exposing every capability — both HotFlip and AblationFlip are compatible.""" - - def __init__(self): - super().__init__(label_names=["neg", "pos"]) - - def predict(self, texts): - return np.tile([0.4, 0.6], (len(texts), 1)) - - def tokenize(self, text): - return text.split() - - def detokenize(self, tokens): - return " ".join(tokens) - - def get_embedding_table(self): - return (["a"], np.zeros((1, 2))) - - def embed(self, texts): - return np.zeros((len(texts), 2)) - - def token_gradients(self, text, target_class): - toks = text.split() - return toks, np.zeros((len(toks), 2)) - - @property - def shap_callable(self): - return self.predict - - -class _TokenizeOnlyModel(TextModel, SupportsTokenization): - """Tokenizable but gradient-free — only AblationFlip is compatible.""" - - def __init__(self): - super().__init__(label_names=["neg", "pos"]) - - def predict(self, texts): - return np.tile([0.4, 0.6], (len(texts), 1)) - - def tokenize(self, text): - return text.split() - - def detokenize(self, tokens): - return " ".join(tokens) - - @property - def shap_callable(self): - return self.predict - - -class TestNlpExplainerGenerators(unittest.TestCase): - """Generator auto-discovery drives the webapp's method selector (no captum needed here).""" - - def test_full_capability_model_offers_both_methods(self): - xpl = NlpExplainer(_FullCapModel(), backend=object()) - self.assertEqual( - xpl.available_cf_generators(), - [("hotflip", "HotFlip"), ("ablation_flip", "Ablation")], - ) - # The preferred (first-discovered) generator stays the active default. - self.assertIsInstance(xpl.cf_generator, HotFlipGenerator) - self.assertEqual(set(xpl.cf_generators), {"hotflip", "ablation_flip"}) - - def test_tokenize_only_model_offers_ablation_only(self): - xpl = NlpExplainer(_TokenizeOnlyModel(), backend=object()) - self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) - self.assertIsInstance(xpl.cf_generator, AblationFlipGenerator) - - def test_explicit_generator_used_verbatim_no_extras(self): - model = _FullCapModel() - gen = AblationFlipGenerator(model) - xpl = NlpExplainer(model, backend=object(), cf_generator=gen) - # An explicit choice is not augmented with the other compatible built-ins. - self.assertEqual(xpl.available_cf_generators(), [("ablation_flip", "Ablation")]) - self.assertIs(xpl.cf_generator, gen) - - def test_cf_config_spec_selected_by_generator(self): - xpl = NlpExplainer(_FullCapModel(), backend=object()) - self.assertIn("max_flips", xpl.cf_config_spec("hotflip")) - self.assertIn("max_ablations", xpl.cf_config_spec("ablation_flip")) - # No argument → the active generator's spec. - self.assertIn("max_flips", xpl.cf_config_spec()) - - def test_unknown_generator_raises(self): - xpl = NlpExplainer(_FullCapModel(), backend=object()) - with self.assertRaises(KeyError): - xpl.cf_config_spec("does_not_exist") - with self.assertRaises(KeyError): - xpl.generate_counterfactuals("hi there", generator="does_not_exist") - - def test_no_generators_without_text_model(self): - # A plain callable is neither a TextModel nor a pipeline → no generators, empty selector. - xpl = NlpExplainer( - lambda texts: np.tile([0.5, 0.5], (len(texts), 1)), label_names=["neg", "pos"], backend=object() - ) - self.assertEqual(xpl.available_cf_generators(), []) - self.assertEqual(xpl.cf_config_spec(), {}) - self.assertIsNone(xpl.cf_generator) - - -# --------------------------------------------------------------------------- -# NlpWebApp layout (no server launch) -# --------------------------------------------------------------------------- - - -class TestNlpWebApp(unittest.TestCase): - def setUp(self): - self.xpl = _make_explainer() - self.explanation = _make_explanation() - self.webapp = NlpWebApp(self.explanation, engine=self.xpl) - - def test_layout_built(self): - self.assertIsNotNone(self.webapp.app.layout) - - def test_class_selector_options(self): - # Class selector is now two independent dropdowns: "local-class-selector" (Sentence - # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). - # Only the global one offers the cross-class overview — a single sample's highlight has no - # such aggregate. - local = self._find_component(self.webapp.app.layout, "local-class-selector") - self.assertIsNotNone(local, "local-class-selector dropdown not found in layout") - self.assertEqual([opt["label"] for opt in local.options], LABEL_NAMES) - - glob = self._find_component(self.webapp.app.layout, "global-class-selector") - self.assertIsNotNone(glob, "global-class-selector dropdown not found in layout") - self.assertEqual(len(glob.options), N_CLASSES + 1) - self.assertEqual([opt["label"] for opt in glob.options], ["All classes", *LABEL_NAMES]) - # Its value must never collide with a real class index. - self.assertEqual(glob.options[0]["value"], "all") - self.assertEqual([opt["value"] for opt in glob.options[1:]], list(range(N_CLASSES))) - - def test_local_class_selector_defaults_to_predicted_class(self): - # Defaults to the predicted class of the initially selected row (row 0). - dropdown = self._find_component(self.webapp.app.layout, "local-class-selector") - row0_prediction = self.webapp._full_table_records[0]["prediction"] - self.assertEqual(dropdown.value, LABEL_NAMES.index(row0_prediction)) - - def test_dataset_table_populated(self): - table = self._find_component(self.webapp.app.layout, "dataset-table") - self.assertIsNotNone(table, "dataset-table not found in layout") - self.assertEqual(len(table.rowData), 3) - self.assertIn("text", table.rowData[0]) - self.assertIn("prediction", table.rowData[0]) - - def test_graph_ids_present(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("global-importance-graph", ids) - self.assertIn("dataset-table", ids) - self.assertIn("local-class-selector", ids) - self.assertIn("global-class-selector", ids) - # token bar chart removed; sentence-highlight replaced it - self.assertNotIn("local-contributions-graph", ids) - - def test_control_ids_present(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("topk-input", ids) - self.assertIn("sign-filter", ids) - self.assertIn("word-filter", ids) - - def test_sentence_highlight_present(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("sentence-highlight", ids) - - def test_waterfall_controls_present(self): - ids = self._collect_ids(self.webapp.app.layout) - # Waterfall is now a tab (no show/hide switch); the threshold slider + graph live in it. - self.assertIn("waterfall-threshold", ids) - self.assertIn("waterfall-graph", ids) - - def test_dataset_table_no_ground_truth_by_default(self): - table = self._find_component(self.webapp.app.layout, "dataset-table") - col_fields = [c["field"] for c in table.columnDefs] - self.assertNotIn("ground_truth", col_fields) - - def test_dataset_table_with_y_true(self): - y_true = pd.Series(["sadness", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") - explanation = _make_explanation(y_true=y_true) - webapp = NlpWebApp(explanation, engine=self.xpl) - table = self._find_component(webapp.app.layout, "dataset-table") - col_fields = [c["field"] for c in table.columnDefs] - self.assertIn("ground_truth", col_fields) - self.assertEqual(table.rowData[0]["ground_truth"], "sadness") - - def test_scatter_store_always_present(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertIn("scatter-selected-indices", ids) - - def test_scatter_absent_when_no_xy(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertNotIn("scatter-plot", ids) - self.assertNotIn("color-by", ids) - - def test_scatter_present_when_a_projection_is_given(self): - webapp = NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((3, 2))) - ids = self._collect_ids(webapp.app.layout) - self.assertIn("scatter-plot", ids) - self.assertIn("color-by", ids) - - def test_scatter_mounts_from_a_matching_embedding(self): - projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id=self.explanation.corpus_id, reducer_tag="pca") - ids = self._collect_ids(NlpWebApp(self.explanation, engine=self.xpl, projection=projection).app.layout) - self.assertIn("scatter-plot", ids) - - def test_an_embedding_of_other_texts_is_refused_at_construction(self): - projection = Embedding(np.zeros((3, 2)), "m", "s", corpus_id="other-texts", reducer_tag="pca") - with self.assertRaises(ValueError): - NlpWebApp(self.explanation, engine=self.xpl, projection=projection) - - def test_scatter_wrong_shape_raises(self): - with self.assertRaises(ValueError): - NlpWebApp(self.explanation, engine=self.xpl, projection=np.zeros((5, 2))) # 5 rows but only 3 samples - - # ── Error Analysis tab (confusion matrix) ───────────────────────── - - def _make_webapp_with_gt(self) -> NlpWebApp: - # y_pred is ["joy", "sadness", "joy"]; make sample 0 a sadness→joy error, others correct. - y_true = pd.Series(["sadness", "sadness", "joy"], index=pd.RangeIndex(3), name="ground_truth") - explanation = _make_explanation(y_true=y_true) - return NlpWebApp(explanation, engine=self.xpl) - - def test_error_analysis_absent_without_ground_truth(self): - ids = self._collect_ids(self.webapp.app.layout) - self.assertNotIn("confusion-matrix-graph", ids) - self.assertNotIn("error-pred-importance", ids) - - def test_error_cell_store_always_present(self): - # The store is created unconditionally so cross-panel callbacks can read it even without gt. - self.assertIn("error-cell", self._collect_ids(self.webapp.app.layout)) - - def test_error_analysis_present_with_ground_truth(self): - ids = self._collect_ids(self._make_webapp_with_gt().app.layout) - for cid in ("confusion-matrix-graph", "error-pred-importance", "error-true-importance", "cm-normalize"): - self.assertIn(cid, ids) - - @staticmethod - def _error_analysis_comp(webapp: NlpWebApp) -> ErrorAnalysisComponent: - return next(c for c in webapp._components if isinstance(c, ErrorAnalysisComponent)) - - def test_confusion_matrix_counts(self): - webapp = self._make_webapp_with_gt() - cm = self._error_analysis_comp(webapp)._cm - # LABEL_NAMES index: sadness=0, joy=1. Rows=true, cols=pred. - self.assertEqual(cm[0, 1], 1) # true sadness predicted joy (the error) - self.assertEqual(cm[0, 0], 1) # true sadness predicted sadness - self.assertEqual(cm[1, 1], 1) # true joy predicted joy - self.assertEqual(cm.sum(), 3) - - def test_confusion_matrix_index_arrays(self): - webapp = self._make_webapp_with_gt() - comp = self._error_analysis_comp(webapp) - self.assertEqual(comp._cm_true_idx.tolist(), [0, 0, 1]) # sadness, sadness, joy - self.assertEqual(comp._cm_pred_idx.tolist(), [1, 0, 1]) # joy, sadness, joy - - def test_confusion_matrix_figure_customdata_orientation(self): - webapp = self._make_webapp_with_gt() - graph = self._find_component(webapp.app.layout, "confusion-matrix-graph") - cd = np.asarray(graph.figure.data[0].customdata) - self.assertEqual(list(cd[0, 1]), [1, 0]) # cell (true=0, pred=1) → [pred_idx, true_idx] - - def test_cell_from_click_uses_label_names_when_no_customdata(self): - # Heatmap clicks may omit customdata; the x (pred) / y (true) labels must still resolve. - name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} - click = {"points": [{"x": "joy", "y": "sadness"}]} - self.assertEqual(_cell_from_click(click, name_to_idx), (1, 0)) - - def test_cell_from_click_prefers_customdata(self): - name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} - click = {"points": [{"x": "joy", "y": "sadness", "customdata": [2, 3]}]} - self.assertEqual(_cell_from_click(click, name_to_idx), (2, 3)) - - def test_cell_from_click_none_on_empty_or_unknown(self): - name_to_idx = {name: i for i, name in enumerate(LABEL_NAMES)} - self.assertIsNone(_cell_from_click(None, name_to_idx)) - self.assertIsNone(_cell_from_click({"points": []}, name_to_idx)) - self.assertIsNone(_cell_from_click({"points": [{"x": "??", "y": "??"}]}, name_to_idx)) - - @staticmethod - def _callback(webapp, out_substr): - # callback_map stores Dash's context-wrapping shim; the raw user function is under __wrapped__. - for key, spec in webapp.app.callback_map.items(): - if out_substr in key: - fn = spec["callback"] - return getattr(fn, "__wrapped__", fn) - raise KeyError(out_substr) - - def test_update_confusion_matrix_normalizes_on_recall(self): - webapp = self._make_webapp_with_gt() - update = self._callback(webapp, "confusion-matrix-graph.figure") - fig = update("recall") - # The container drives sizing for this half-column panel, not the figure itself. - self.assertIsNone(fig.layout.width) - self.assertIsNone(fig.layout.height) - - def test_set_error_cell_resolves_click_to_cell_and_clears_clickdata(self): - webapp = self._make_webapp_with_gt() - set_cell = self._callback(webapp, "error-cell.data") - click = {"points": [{"x": "joy", "y": "sadness"}]} - with patch.object(error_analysis_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] - data, click_out = set_cell(click, None) - self.assertEqual(data, {"pred": 1, "true": 0, "indices": [0]}) - self.assertIsNone(click_out) - - def test_set_error_cell_prevents_update_on_unresolved_click(self): - webapp = self._make_webapp_with_gt() - set_cell = self._callback(webapp, "error-cell.data") - click = {"points": [{"x": "??", "y": "??"}]} - with patch.object(error_analysis_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "confusion-matrix-graph.clickData"}] - with self.assertRaises(PreventUpdate): - set_cell(click, None) - - def test_set_error_cell_clear_button_resets_store(self): - webapp = self._make_webapp_with_gt() - set_cell = self._callback(webapp, "error-cell.data") - with patch.object(error_analysis_module, "callback_context") as cc: - cc.triggered = [{"prop_id": "error-cell-clear-btn.n_clicks"}] - data, click_out = set_cell(None, 1) - self.assertIsNone(data) - self.assertIsNone(click_out) - - def test_toggle_error_cell_clear_button_visibility(self): - webapp = self._make_webapp_with_gt() - toggle = self._callback(webapp, "error-cell-clear-btn.style") - self.assertEqual(toggle(None)["display"], "none") - self.assertEqual(toggle({"pred": 1, "true": 0, "indices": [0]})["display"], "inline") - - def test_update_error_word_charts_empty_without_a_selected_cell(self): - webapp = self._make_webapp_with_gt() - update = self._callback(webapp, "error-cell-caption.children") - fig_pred, fig_true, caption = update(None) - self.assertIsInstance(fig_pred, go.Figure) - self.assertIsInstance(fig_true, go.Figure) - self.assertIn("Click a cell", caption) - - def test_update_error_word_charts_empty_when_cell_has_no_samples(self): - webapp = self._make_webapp_with_gt() - update = self._callback(webapp, "error-cell-caption.children") - fig_pred, fig_true, caption = update({"pred": 0, "true": 1, "indices": []}) - self.assertIsInstance(fig_pred, go.Figure) - self.assertIn("0 samples", caption) - - def test_update_error_word_charts_off_diagonal_cell_flags_few_samples(self): - webapp = self._make_webapp_with_gt() - update = self._callback(webapp, "error-cell-caption.children") - # pred=joy(1), true=sadness(0): the single planted error, row 0. - fig_pred, fig_true, caption = update({"pred": 1, "true": 0, "indices": [0]}) - self.assertIsInstance(fig_pred, go.Figure) - self.assertIsInstance(fig_true, go.Figure) - self.assertIn("predicted joy", caption) - self.assertIn("true sadness", caption) - self.assertIn("1 sample", caption) - self.assertIn("few samples", caption) - - def test_update_error_word_charts_diagonal_cell_marks_correct_predictions(self): - webapp = self._make_webapp_with_gt() - update = self._callback(webapp, "error-cell-caption.children") - # pred=joy(1), true=joy(1): a correct prediction, row 2. - fig_pred, fig_true, caption = update({"pred": 1, "true": 1, "indices": [2]}) - self.assertIsInstance(fig_pred, go.Figure) - self.assertIsInstance(fig_true, go.Figure) - self.assertIn("correct predictions (diagonal)", caption) - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _find_component(self, node, component_id): - """Depth-first search for a Dash component by id.""" - if hasattr(node, "id") and node.id == component_id: - return node - children = getattr(node, "children", None) - if children is None: - return None - if not isinstance(children, list): - children = [children] - for child in children: - result = self._find_component(child, component_id) - if result is not None: - return result - return None - - def _collect_ids(self, node) -> set: - """Collect all component ids in the layout tree.""" - ids = set() - if hasattr(node, "id") and isinstance(node.id, str): - ids.add(node.id) - children = getattr(node, "children", None) - if children is None: - return ids - if not isinstance(children, list): - children = [children] - for child in children: - ids |= self._collect_ids(child) - return ids - - -class TestNlpWebAppRun(unittest.TestCase): - """``NlpWebApp.run`` / ``NlpExplainer.run_app`` — delegate to ``dash.Dash.run``; a stop handle in notebooks.""" - - def setUp(self): - self.xpl = _make_explainer() - self.explanation = _make_explanation() - - @staticmethod - def _jupyter_active(active): - return patch.object(type(jupyter_dash), "active", new_callable=PropertyMock, return_value=active) - - def test_outside_notebook_delegates_to_dash_and_returns_none(self): - webapp = NlpWebApp(self.explanation, engine=self.xpl) - with self._jupyter_active(False), patch.object(webapp.app, "run") as mock_run: - result = webapp.run(port=8123, debug=True) - mock_run.assert_called_once_with(port=8123, debug=True, host="127.0.0.1") - self.assertIsNone(result) - - def test_in_notebook_returns_handle_on_dash_registered_server(self): - webapp = NlpWebApp(self.explanation, engine=self.xpl) - server = MagicMock() - - def register(port, debug, host): - jupyter_dash._servers[(host, port)] = server - - with self._jupyter_active(True), patch.object(webapp.app, "run", side_effect=register): - app = webapp.run(port=8123) - self.assertIsInstance(app, RunningApp) - app.kill() - server.shutdown.assert_called_once_with() - self.assertNotIn(("127.0.0.1", 8123), jupyter_dash._servers) - - def test_run_app_forwards_the_handle(self): - sentinel = object() - with patch.object(NlpWebApp, "run", return_value=sentinel) as mock_run: - result = self.xpl.run_app(self.explanation, port=8123) - mock_run.assert_called_once_with(port=8123, debug=False, host="127.0.0.1") - self.assertIs(result, sentinel) - - -class TestComposeSelection(unittest.TestCase): - """The three sample filters (scatter box, confusion cell, errors-only) must intersect.""" - - def test_nothing_active_returns_none(self): - self.assertIsNone(_compose_selection(None, None, None)) - - def test_scatter_only(self): - self.assertEqual(_compose_selection([1, 2, 3], None, None), [1, 2, 3]) - - def test_cell_only(self): - self.assertEqual(_compose_selection(None, [4, 5], None), [4, 5]) - - def test_errors_only(self): - self.assertEqual(_compose_selection(None, None, {2, 7}), [2, 7]) - - def test_scatter_intersect_cell(self): - self.assertEqual(_compose_selection([1, 2, 3], [2, 3, 4], None), [2, 3]) - - def test_scatter_intersect_errors(self): - # This is the box-then-errors case: errors *within* the selected points. - self.assertEqual(_compose_selection([1, 2, 3, 4], None, {2, 4, 9}), [2, 4]) - - def test_all_three_intersect(self): - self.assertEqual(_compose_selection([1, 2, 3, 4], [2, 3, 4], {3, 4}), [3, 4]) - - def test_empty_intersection_is_empty_list(self): - self.assertEqual(_compose_selection([1, 2], None, {8, 9}), []) - - -class TestErrorPositions(unittest.TestCase): - """The shared definition of "a model error" every errors-scoped panel filters on.""" - - def test_none_without_ground_truth(self): - self.assertIsNone(error_positions(_make_explanation())) - - def test_compares_as_strings(self): - texts = _make_texts() - explanation = _make_explanation(y_true=pd.Series(["joy", "joy", "joy"], index=texts.index)) - # y_pred is ["joy", "sadness", "joy"], so only the middle row disagrees. - self.assertEqual(error_positions(explanation), {1}) - - -class TestUnpackDatapoint(unittest.TestCase): - """``unpack_datapoint`` slices a ``pack_datapoint`` payload down to one class for rendering.""" - - def test_multiclass_slices_the_requested_column(self): - dp = pack_datapoint( - text="i am happy", - orig_idx=2, - tokens=["i", "am", "happy"], - values=np.array([[0.1, -0.1], [0.2, -0.2], [0.3, -0.3]]), - base_values=np.array([0.5, -0.5]), - label="joy", - ) - tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) - self.assertEqual(tokens, ["i", "am", "happy"]) - np.testing.assert_allclose(vals, [-0.1, -0.2, -0.3]) - self.assertEqual(base_value, -0.5) - self.assertEqual(label, "joy") - - def test_binary_1d_values_ignore_label_idx(self): - dp = pack_datapoint( - text="not great", - orig_idx=None, - tokens=["not", "great"], - values=np.array([0.2, -0.4]), - base_values=None, - ) - tokens, vals, base_value, label = unpack_datapoint(dp, label_idx=1) - np.testing.assert_allclose(vals, [0.2, -0.4]) - self.assertIsNone(base_value) - self.assertIsNone(label) - - def test_single_scalar_base_value_is_shared_across_classes(self): - dp = pack_datapoint( - text="ok", - orig_idx=0, - tokens=["ok"], - values=np.array([[0.1, 0.2, 0.3]]), - base_values=np.array([0.7]), # one shared scalar, not one per class - ) - # label_idx=2 slices fine into values (3 classes) but has no matching entry in base_values. - _, vals, base_value, _ = unpack_datapoint(dp, label_idx=2) - np.testing.assert_allclose(vals, [0.3]) - self.assertEqual(base_value, 0.7) - - -# --------------------------------------------------------------------------- -# Single-word profile: word_occurrences / vocabulary / aggregators / plot / plotter -# --------------------------------------------------------------------------- - - -def _profile_explanation(folds_case=True): - """Three samples, two classes, with 'happy' appearing in three of them (twice in one).""" - token_strings = [ - ["[CLS]", "so", "happy", "today", "!"], - ["Happy", "and", "happy", "again"], - ["not", "happy", "at", "all"], - ["nothing", "here"], - ] - values = [ - np.array([[0.0, 0.0], [0.1, -0.1], [0.4, -0.4], [0.05, -0.05], [0.0, 0.0]]), - np.array([[0.2, -0.2], [0.0, 0.0], [0.1, -0.1], [0.0, 0.0]]), - np.array([[-0.1, 0.1], [-0.6, 0.6], [0.0, 0.0], [0.0, 0.0]]), - np.array([[0.0, 0.0], [0.0, 0.0]]), - ] - texts = pd.Series(["so happy today !", "Happy and happy again", "not happy at all", "nothing here"]) - return NlpExplanation( - texts=texts, - token_strings=token_strings, - values=values, - base_values=None, - y_pred=pd.Series(["pos", "pos", "neg", "neg"], index=texts.index, name="prediction"), - y_prob=None, - y_true=pd.Series(["pos", "neg", "neg", "neg"], index=texts.index, name="ground_truth"), - label_names=["pos", "neg"], - folds_case=folds_case, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - -class TestVocabulary(unittest.TestCase): - def test_filters_special_and_punctuation_and_folds(self): - vocab = _profile_explanation().vocabulary() - self.assertNotIn("[CLS]", vocab) - self.assertNotIn("!", vocab) - # Folded: "Happy" and "happy" collapse to one entry. - self.assertIn("happy", vocab) - self.assertNotIn("Happy", vocab) - - def test_keeps_case_when_model_does_not_fold(self): - vocab = _profile_explanation(folds_case=False).vocabulary() - self.assertIn("Happy", vocab) - self.assertIn("happy", vocab) - - def test_sorted_and_unique(self): - vocab = _profile_explanation().vocabulary() - self.assertEqual(vocab, sorted(set(vocab))) - - def test_offers_exactly_what_word_importance_ranks(self): - # The contract the pickers rely on: every unit offered has occurrences to show. - explanation = _profile_explanation() - for word in explanation.vocabulary(): - self.assertFalse(explanation.word_occurrences(word).empty, word) - - def test_can_keep_punctuation_and_special(self): - vocab = _profile_explanation().vocabulary(filter_special=False, filter_punctuation=False) - self.assertIn("[cls]", vocab) - self.assertIn("!", vocab) - - -class TestWordImportanceRanking(unittest.TestCase): - """``rank_by`` + ``min_occurrences``: ordering criteria that never touch the reported sign.""" - - def setUp(self): - # "rare" appears once with a large pull; "common" appears four times with a small one. - # Under |mean| rare wins; under |sum| common wins (4 x 0.3 = 1.2 > 0.9). - token_strings = [ - ["rare", "common"], - ["common"], - ["common", "common"], - ["mild"], - ] - values = [ - np.array([[0.9], [0.3]]), - np.array([[0.3]]), - np.array([[0.3], [0.3]]), - np.array([[-0.5]]), - ] - self.explanation = _minimal_explanation(token_strings, values, None) - - def test_mean_is_the_default_and_unchanged(self): - imp = self.explanation.word_importance(label_idx=0) - self.assertEqual(imp.index[0], "rare") - self.assertAlmostEqual(imp["common"], 0.3) - - def test_sum_reranks_by_total_mass(self): - imp = self.explanation.word_importance(label_idx=0, rank_by="sum") - self.assertEqual(imp.index[0], "common") - self.assertAlmostEqual(imp["common"], 1.2) - - def test_series_name_records_the_statistic(self): - self.assertEqual(self.explanation.word_importance(label_idx=0).name, "mean") - self.assertEqual(self.explanation.word_importance(label_idx=0, rank_by="sum").name, "sum") - - def test_ranking_is_absolute_but_values_stay_signed(self): - # "mild" is negative and must survive ranking, keeping its sign for the sign filter and - # for the renderer's red/blue colouring. - for rank_by in ("mean", "sum"): - imp = self.explanation.word_importance(label_idx=0, rank_by=rank_by) - self.assertLess(imp["mild"], 0) - - def test_sign_filter_works_in_both_modes(self): - for rank_by in ("mean", "sum"): - pos = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="positive") - neg = self.explanation.word_importance(label_idx=0, rank_by=rank_by, filter_sign="negative") - self.assertNotIn("mild", pos.index) - self.assertEqual(list(neg.index), ["mild"]) - - def test_min_occurrences_drops_rare_words(self): - imp = self.explanation.word_importance(label_idx=0, min_occurrences=2) - self.assertEqual(list(imp.index), ["common"]) - - def test_min_occurrences_of_one_is_no_filter(self): - self.assertEqual( - set(self.explanation.word_importance(label_idx=0, min_occurrences=1).index), - {"rare", "common", "mild"}, - ) - - def test_min_occurrences_counts_within_the_selection(self): - # "common" occurs 4x across the batch but only once in sample 1, so a floor of 2 must - # exclude it there — otherwise a threshold would mean something different per scope. - imp = self.explanation.word_importance(label_idx=0, sample_indices=[1], min_occurrences=2) - self.assertTrue(imp.empty) - self.assertEqual( - list(self.explanation.word_importance(label_idx=0, sample_indices=[2], min_occurrences=2).index), - ["common"], - ) - - def test_impossible_floor_returns_an_empty_series_not_an_error(self): - imp = self.explanation.word_importance(label_idx=0, min_occurrences=999) - self.assertTrue(imp.empty) - self.assertEqual(imp.dtype, np.float64) - - def test_unknown_rank_by_raises(self): - with self.assertRaisesRegex(ValueError, "must be 'mean' or 'sum'"): - self.explanation.word_importance(label_idx=0, rank_by="median") - - def test_plotter_labels_the_axis_for_the_statistic(self): - self.assertEqual( - self.explanation.plot.word_importance(label_idx=0, rank_by="sum").layout.xaxis.title.text, - "Total SHAP contribution", - ) - self.assertEqual( - self.explanation.plot.word_importance(label_idx=0).layout.xaxis.title.text, - "Mean SHAP contribution", - ) - - def test_plotter_hover_counts_match_the_scope_it_ranked(self): - # The plotter forwards only the keying arguments to word_counts, so a count on a bar must - # be the count over the *same* samples the aggregate was taken over. - fig = self.explanation.plot.word_importance(label_idx=0, sample_indices=[2]) - counts = self.explanation.word_counts(sample_indices=[2])["n_occurrences"] - drawn = list(fig.data[0].y) - self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [int(counts[w]) for w in drawn]) - - -class TestWordImportanceAcrossClasses(unittest.TestCase): - """``label_idx=None``: one ranking spanning every class, on magnitudes.""" - - @staticmethod - def _three_class(): - # "alpha" is decisive for class c only; "beta" is mild and spread. Three classes so max - # and mean over classes are actually distinguishable (with two they coincide). - texts = pd.Series(["alpha beta", "alpha beta"]) - rows = np.array([[-0.3, -0.3, 0.6], [0.1, -0.2, 0.1]]) - return NlpExplanation( - texts=texts, - token_strings=[["alpha", "beta"], ["alpha", "beta"]], - values=[rows, rows], - base_values=None, - y_pred=pd.Series(["a", "a"], index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=["a", "b", "c"], - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - - def test_value_is_the_strongest_classs_magnitude(self): - # Not the mean over classes, which would read 0.4 and 0.133 — smaller than any real - # contribution and matching nothing the single-class views show. - self.assertEqual(self._three_class().word_importance(label_idx=None).to_dict(), {"alpha": 0.6, "beta": 0.2}) - - def test_it_matches_what_the_driving_class_shows(self): - exp = self._three_class() - self.assertEqual(exp.word_importance(label_idx=None)["alpha"], abs(exp.word_importance(label_idx=2)["alpha"])) - - def test_signed_averaging_across_classes_would_be_zero(self): - # The reason the collapse discards sign: an explainer of a normalised output cancels - # across classes, so a signed cross-class mean is a chart of zeros. - exp = self._three_class() - per_class = np.array([exp.word_importance(label_idx=i)["alpha"] for i in range(3)]) - self.assertAlmostEqual(float(per_class.mean()), 0.0) - self.assertGreater(exp.word_importance(label_idx=None)["alpha"], 0.5) - - def test_single_output_model_collapses_to_the_absolute_value(self): - # A 1-D contribution array has no class axis to reduce over; it still must not raise. - texts = pd.Series(["alpha beta"]) - exp = NlpExplanation( - texts=texts, - token_strings=[["alpha", "beta"]], - values=[np.array([0.5, -0.2])], - base_values=None, - y_pred=pd.Series(["a"], index=texts.index, name="prediction"), - y_prob=None, - y_true=None, - label_names=None, - folds_case=True, - backend_name="nlp_shap", - is_additive=True, - reference_kind="none", - output_space="probability", - ) - self.assertEqual(exp.word_importance(label_idx=None).to_dict(), {"alpha": 0.5, "beta": 0.2}) - - def test_every_value_is_a_magnitude(self): - self.assertTrue((_profile_explanation().word_importance(label_idx=None) >= 0).all()) - - def test_name_records_the_statistic(self): - exp = _profile_explanation() - self.assertEqual(exp.word_importance(label_idx=None).name, "mean_across_classes") - self.assertEqual(exp.word_importance(label_idx=None, rank_by="sum").name, "sum_across_classes") - - def test_a_negative_sign_filter_has_nothing_to_select(self): - self.assertTrue(_profile_explanation().word_importance(label_idx=None, filter_sign="negative").empty) - - def test_the_frequency_floor_still_applies(self): - exp = _profile_explanation() - self.assertIn("happy", exp.word_importance(label_idx=None, min_occurrences=4).index) - self.assertNotIn("today", exp.word_importance(label_idx=None, min_occurrences=2).index) - - def test_plotter_titles_and_labels_the_cross_class_view(self): - fig = _profile_explanation().plot.word_importance(label_idx=None) - self.assertEqual(fig.layout.title.text, "Word importance — all classes") - self.assertEqual(fig.layout.xaxis.title.text, "Largest |mean SHAP| across classes") - - -class TestWordImportanceReadability(unittest.TestCase): - """The chart must name every bar it draws, whatever K is.""" - - @staticmethod - def _imp(n): - return pd.Series( - [(-1) ** i * (n - i) / n for i in range(n)], - index=[f"word{i}" for i in range(n)], - ) - - def test_every_word_gets_a_tick(self): - # dtick=1 on a categorical axis is what stops plotly thinning labels on a long ranking. - fig = plot_word_importance(self._imp(50)) - self.assertEqual(fig.layout.yaxis.dtick, 1) - self.assertEqual(len(set(fig.data[0].y)), 50) - - def test_height_grows_with_the_word_count(self): - heights = [plot_word_importance(self._imp(n)).layout.height for n in (5, 20, 50)] - self.assertEqual(heights, sorted(heights)) - # Every word keeps room to render, rather than the chart being squeezed to a fixed panel. - for n, h in zip((5, 20, 50), heights): - self.assertGreaterEqual(h / n, 24) - - def test_explicit_height_still_wins(self): - self.assertEqual(plot_word_importance(self._imp(50), height=300).layout.height, 300) - - def test_value_labels_are_signed_and_on_by_default(self): - fig = plot_word_importance(pd.Series([0.9, -0.5], index=["a", "b"])) - self.assertEqual(list(fig.data[0].text), ["-0.500", "+0.900"]) # reversed for drawing - self.assertEqual(fig.data[0].textposition, "outside") - # Outside text sits past the bar end and would be cut off at the plot edge without this. - self.assertFalse(fig.data[0].cliponaxis) - - def test_value_labels_get_headroom(self): - fig = plot_word_importance(pd.Series([1.0, -0.5], index=["a", "b"])) - lo, hi = fig.layout.xaxis.range - self.assertLess(lo, -0.5) - self.assertGreater(hi, 1.0) - - def test_value_labels_can_be_turned_off(self): - fig = plot_word_importance(pd.Series([0.9], index=["a"]), show_values=False) - self.assertIsNone(fig.data[0].text) - # No outside text means no need to reserve headroom; plotly autoscales. - self.assertIsNone(fig.layout.xaxis.range) - - def test_degenerate_inputs_autoscale_rather_than_collapse(self): - # An all-zero (or empty) series has no span to pad, and a [0, 0] range would be invalid. - self.assertIsNone(plot_word_importance(pd.Series(dtype=float)).layout.xaxis.range) - self.assertIsNone(plot_word_importance(pd.Series([0.0, 0.0], index=["a", "b"])).layout.xaxis.range) - - def test_tick_font_is_set(self): - self.assertEqual(plot_word_importance(self._imp(3)).layout.yaxis.tickfont.size, 12) - - -class TestWordImportanceHover(unittest.TestCase): - """The hover names the statistic and, when supplied, how many occurrences it aggregated.""" - - imp = pd.Series([0.9, -0.5], index=["a", "b"]) - - def test_hover_names_the_statistic_on_the_axis(self): - # The axis title scrolls off a long chart; the hover is where the reader can still check - # whether they are looking at a mean or a total. - fig = plot_word_importance(self.imp, x_title="Total SHAP contribution") - self.assertIn("Total SHAP contribution: %{x:.4f}", fig.data[0].hovertemplate) - - def test_counts_are_reversed_with_the_bars(self): - fig = plot_word_importance(self.imp, counts={"a": 12, "b": 3}) - self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) - self.assertIn("Occurrences:", fig.data[0].hovertemplate) - - def test_counts_accept_a_series(self): - fig = plot_word_importance(self.imp, counts=pd.Series({"a": 12, "b": 3})) - self.assertEqual([int(c[0]) for c in fig.data[0].customdata], [3, 12]) - - def test_without_counts_the_hover_is_unchanged(self): - fig = plot_word_importance(self.imp) - self.assertIsNone(fig.data[0].customdata) - self.assertNotIn("Occurrences", fig.data[0].hovertemplate) - - def test_a_partial_mapping_is_dropped_rather_than_shown_as_zero(self): - # A missing word can only mean the caller counted under different filters, which would - # misreport every bar — not just the uncovered one. - fig = plot_word_importance(self.imp, counts={"a": 12}) - self.assertIsNone(fig.data[0].customdata) - self.assertNotIn("Occurrences", fig.data[0].hovertemplate) - - -class TestWordCounts(unittest.TestCase): - """The frequency table behind the word picker's order, labels and threshold.""" - - def setUp(self): - self.explanation = _profile_explanation() - - def test_occurrences_and_samples_differ_for_a_repeated_word(self): - counts = self.explanation.word_counts() - # "happy" occurs 4x (twice in one sample) across 3 samples. - self.assertEqual(counts.loc["happy", "n_occurrences"], 4) - self.assertEqual(counts.loc["happy", "n_samples"], 3) - - def test_sorted_by_frequency_then_alphabetically(self): - counts = self.explanation.word_counts() - self.assertEqual(counts.index[0], "happy") - ties = counts[counts["n_occurrences"] == 1].index.tolist() - self.assertEqual(ties, sorted(ties)) - - def test_applies_the_same_filters_as_word_importance(self): - counts = self.explanation.word_counts() - self.assertNotIn("[CLS]", counts.index) - self.assertNotIn("!", counts.index) - self.assertNotIn("Happy", counts.index) # folded into "happy" - - def test_index_is_exactly_the_vocabulary(self): - self.assertEqual(sorted(self.explanation.word_counts().index), self.explanation.vocabulary()) - - def test_scoped_to_sample_indices(self): - counts = self.explanation.word_counts(sample_indices=[1]) - self.assertEqual(counts.loc["happy", "n_occurrences"], 2) - self.assertEqual(counts.loc["happy", "n_samples"], 1) - - def test_counts_are_the_denominator_word_importance_filters_on(self): - # The contract the min-occurrence control depends on: the count shown beside a word is the - # count its aggregate was computed over. - counts = self.explanation.word_counts() - for word, n in counts["n_occurrences"].items(): - kept = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n)) - self.assertIn(word, kept.index, word) - dropped = self.explanation.word_importance(label_idx=0, n_top=999, min_occurrences=int(n) + 1) - self.assertNotIn(word, dropped.index, word) - - def test_empty_batch_returns_an_empty_frame(self): - empty = _minimal_explanation([[]], [np.zeros((0, 2))], None) - self.assertTrue(empty.word_counts().empty) - self.assertEqual(empty.vocabulary(), []) - - -class TestWordOccurrences(unittest.TestCase): - def setUp(self): - self.explanation = _profile_explanation() - - def test_one_row_per_occurrence_and_class(self): - occ = self.explanation.word_occurrences("happy") - # 4 occurrences (one in sample 0, two in sample 1, one in sample 2) x 2 classes. - self.assertEqual(len(occ), 8) - self.assertEqual(list(occ.columns), ["sample", "token_pos", "token", "class_idx", "contribution"]) - - def test_case_folding_follows_the_model(self): - self.assertEqual(len(self.explanation.word_occurrences("HAPPY")), 8) - cased = _profile_explanation(folds_case=False) - self.assertEqual(len(cased.word_occurrences("happy")), 6) - self.assertEqual(len(cased.word_occurrences("Happy")), 2) - - def test_explicit_lowercase_overrides(self): - cased = _profile_explanation(folds_case=False) - self.assertEqual(len(cased.word_occurrences("happy", lowercase=True)), 8) - - def test_token_keeps_original_casing(self): - occ = self.explanation.word_occurrences("happy") - self.assertIn("Happy", set(occ["token"])) - - def test_sample_indices_scope(self): - occ = self.explanation.word_occurrences("happy", sample_indices=[2]) - self.assertEqual(set(occ["sample"]), {2}) - self.assertEqual(len(occ), 2) - - def test_missing_word_returns_typed_empty_frame(self): - occ = self.explanation.word_occurrences("absent") - self.assertTrue(occ.empty) - self.assertEqual(occ["contribution"].dtype, np.float64) - # An empty frame must still support the numeric work the callers do on it. - self.assertTrue(aggregate_word_contributions(occ, "mean_abs").empty) - self.assertTrue(rank_word_samples(occ, class_idx=0).empty) - - def test_binary_1d_values(self): - explanation = _minimal_explanation( - [["good", "day"], ["good"]], - [np.array([0.5, 0.1]), np.array([-0.3])], - None, - ) - occ = explanation.word_occurrences("good") - self.assertEqual(set(occ["class_idx"]), {0}) - self.assertEqual(sorted(occ["contribution"].round(3)), [-0.3, 0.5]) - - -class TestAggregateWordContributions(unittest.TestCase): - def setUp(self): - self.occ = _profile_explanation().word_occurrences("happy") - - def test_mean_is_signed(self): - stats = aggregate_word_contributions(self.occ, "mean") - # class 0: (0.4 + 0.2 + 0.1 - 0.6) / 4 - self.assertAlmostEqual(stats[0], 0.025) - self.assertAlmostEqual(stats[1], -0.025) - - def test_sum_scales_with_frequency(self): - stats = aggregate_word_contributions(self.occ, "sum") - self.assertAlmostEqual(stats[0], 0.1) - - def test_abs_forms_expose_the_two_way_word(self): - signed = aggregate_word_contributions(self.occ, "mean") - magnitude = aggregate_word_contributions(self.occ, "mean_abs") - # The whole point of the abs forms: a word averaging to ~0 is not a weak word. - self.assertLess(abs(signed[0]), 0.05) - self.assertAlmostEqual(magnitude[0], 0.325) - self.assertAlmostEqual(aggregate_word_contributions(self.occ, "sum_abs")[0], 1.3) - - def test_index_is_every_class_in_order(self): - self.assertEqual(list(aggregate_word_contributions(self.occ, "sum").index), [0, 1]) - - def test_unknown_agg_raises(self): - with self.assertRaisesRegex(ValueError, "not one of"): - aggregate_word_contributions(self.occ, "median") - - def test_all_documented_ops_run(self): - for agg in WORD_AGGREGATIONS: - self.assertEqual(len(aggregate_word_contributions(self.occ, agg)), 2) - - -class TestRankWordSamples(unittest.TestCase): - def setUp(self): - self.occ = _profile_explanation().word_occurrences("happy") - - def test_occurrences_are_summed_within_a_sample(self): - ranked = rank_word_samples(self.occ, class_idx=0, order="most") - row = ranked[ranked["sample"] == 1].iloc[0] - self.assertAlmostEqual(row["contribution"], 0.3) # 0.2 + 0.1 - self.assertEqual(row["n_occurrences"], 2) - # One row per sample, never one per occurrence. - self.assertEqual(len(ranked), ranked["sample"].nunique()) - - def test_most_ranks_positive_first(self): - ranked = rank_word_samples(self.occ, class_idx=0, order="most") - self.assertEqual(ranked["sample"].tolist(), [0, 1, 2]) - - def test_least_ranks_negative_first(self): - ranked = rank_word_samples(self.occ, class_idx=0, order="least") - self.assertEqual(ranked["sample"].iloc[0], 2) - - def test_strongest_ignores_sign(self): - ranked = rank_word_samples(self.occ, class_idx=0, order="strongest") - self.assertEqual(ranked["sample"].iloc[0], 2) # |-0.6| is the largest - - def test_class_idx_selects_the_column(self): - pos = rank_word_samples(self.occ, class_idx=0, order="most")["contribution"].tolist() - neg = rank_word_samples(self.occ, class_idx=1, order="most")["contribution"].tolist() - self.assertNotEqual(pos, neg) - - def test_n_top_truncates(self): - self.assertEqual(len(rank_word_samples(self.occ, class_idx=0, n_top=1)), 1) - - def test_unknown_order_raises(self): - with self.assertRaisesRegex(ValueError, "must be one of"): - rank_word_samples(self.occ, class_idx=0, order="alphabetical") - - def test_absent_class_returns_typed_empty(self): - ranked = rank_word_samples(self.occ, class_idx=99) - self.assertTrue(ranked.empty) - self.assertEqual(list(ranked.columns), ["sample", "contribution", "n_occurrences"]) - - -class TestWordContributionsBySample(unittest.TestCase): - def setUp(self): - self.explanation = _profile_explanation() - - def test_sums_one_words_occurrences_per_sample(self): - # class 0: sample0 has one "happy" (0.4), sample1 has two ("Happy" 0.2 + "happy" 0.1 = 0.3), - # sample2 has one (-0.6), sample3 has none. - result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) - np.testing.assert_allclose(result, [0.4, 0.3, -0.6, 0.0]) - - def test_sums_across_words_within_a_sample(self): - # "today" only occurs in sample0 (0.05, class 0), added on top of "happy"'s own 0.4. - result = word_contributions_by_sample(self.explanation, ["happy", "today"], label_idx=0) - np.testing.assert_allclose(result, [0.45, 0.3, -0.6, 0.0]) - - def test_label_idx_selects_the_class(self): - class0 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) - class1 = word_contributions_by_sample(self.explanation, ["happy"], label_idx=1) - np.testing.assert_allclose(class1, -class0) - - def test_absent_word_returns_all_zeros(self): - result = word_contributions_by_sample(self.explanation, ["nonexistent"], label_idx=0) - np.testing.assert_allclose(result, np.zeros(len(self.explanation))) - - def test_result_length_matches_the_batch(self): - result = word_contributions_by_sample(self.explanation, ["happy"], label_idx=0) - self.assertEqual(len(result), len(self.explanation)) - - -class TestPlotScatter(unittest.TestCase): - def setUp(self): - self.xy = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]]) - self.texts = ["so happy today !", "Happy and happy again", "not happy at all", "nothing here"] - - def test_categorical_mode_draws_one_trace_per_label_name(self): - fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "neg", "neg"], label_names=["pos", "neg"]) - self.assertIsInstance(fig, go.Figure) - self.assertEqual(len(fig.data), 2) - self.assertEqual(fig.data[0].name, "pos") - self.assertEqual([row[0] for row in fig.data[0].customdata], [0, 1]) - - def test_categorical_mode_skips_empty_categories(self): - fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "pos", "pos"], label_names=["pos", "neg"]) - self.assertEqual(len(fig.data), 1) - - def test_word_contribution_mode_splits_present_and_absent(self): - fig = plot_scatter(self.xy, self.texts, contributions=np.array([0.4, 0.0, -0.6, 0.0])) - self.assertIsInstance(fig, go.Figure) - self.assertEqual(len(fig.data), 2) - absent_trace, present_trace = fig.data - self.assertEqual([row[0] for row in absent_trace.customdata], [1, 3]) - self.assertEqual([row[0] for row in present_trace.customdata], [0, 2]) - - def test_error_mask_shrinks_correct_points_and_grows_wrong_ones(self): - contributions = np.array([0.4, 0.0, -0.6, 0.0]) - error_mask = np.array([True, False, False, False]) - fig = plot_scatter(self.xy, self.texts, contributions=contributions, error_mask=error_mask) - _, present_trace = fig.data - # present_mask is [0, 2]; sample 0 is the error and should be emphasized over sample 2. - sizes = present_trace.marker.size - self.assertGreater(sizes[0], sizes[1]) - - def test_requires_exactly_one_of_contributions_or_labels(self): - with self.assertRaises(ValueError): - plot_scatter(self.xy, self.texts) - with self.assertRaises(ValueError): - plot_scatter( - self.xy, - self.texts, - labels=["pos"] * 4, - label_names=["pos"], - contributions=np.zeros(4), - ) - - def test_labels_without_label_names_raises(self): - with self.assertRaises(ValueError): - plot_scatter(self.xy, self.texts, labels=["pos"] * 4) - - -class TestPlotWordProfile(unittest.TestCase): - def setUp(self): - self.stats = pd.Series([0.2, -0.3], index=pd.Index([0, 1], name="class_idx")) - - def test_returns_figure_with_one_bar_per_class(self): - fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) - self.assertIsInstance(fig, go.Figure) - self.assertEqual(len(fig.data[0].x), 2) - - def test_class_zero_is_drawn_at_the_top(self): - # Plotly draws the y-axis bottom-to-top, so the first class must be last in the array. - fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) - self.assertEqual(fig.data[0].y[-1], "pos") - - def test_sign_drives_colour(self): - fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) - # Reversed alongside the values, so the last colour belongs to the positive class 0. - self.assertEqual(fig.data[0].marker.color[-1], DEFAULT_NLP_THEME.xpl_positive) - self.assertEqual(fig.data[0].marker.color[0], DEFAULT_NLP_THEME.xpl_negative) - - def test_falls_back_to_class_indices_without_names(self): - fig = plot_word_profile(self.stats) - self.assertEqual(set(fig.data[0].y), {"0", "1"}) - - def test_spread_is_reversed_with_the_bars(self): - spread = pd.Series([0.1, 0.9], index=pd.Index([0, 1], name="class_idx")) - fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) - # Class 1's spread must sit on class 1's bar, which is drawn first. - self.assertAlmostEqual(fig.data[0].error_x.array[0], 0.9) - - def test_misaligned_spread_does_not_shift_error_bars(self): - # A spread indexed on classes the stats do not carry must not silently slide onto them. - spread = pd.Series([0.1], index=pd.Index([5], name="class_idx")) - fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) - self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) - - def test_no_error_bars_without_spread(self): - fig = plot_word_profile(self.stats) - self.assertIsNone(fig.data[0].error_x.array) - - -class TestPlotterWordProfile(unittest.TestCase): - def setUp(self): - self.explanation = _profile_explanation() - - def test_returns_figure_titled_with_counts(self): - fig = self.explanation.plot.word_profile("happy") - self.assertIsInstance(fig, go.Figure) - self.assertIn("4 occurrence(s) in 3 sample(s)", fig.layout.title.text) - - def test_mean_draws_error_bars_and_sum_does_not(self): - self.assertIsNotNone(self.explanation.plot.word_profile("happy", agg="mean").data[0].error_x.array) - self.assertIsNone(self.explanation.plot.word_profile("happy", agg="sum").data[0].error_x.array) - - def test_single_occurrence_spread_is_zero_not_nan(self): - fig = self.explanation.plot.word_profile("today", agg="mean") - self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) - - def test_sample_indices_scope(self): - fig = self.explanation.plot.word_profile("happy", sample_indices=[1]) - self.assertIn("2 occurrence(s) in 1 sample(s)", fig.layout.title.text) - - def test_title_override(self): - self.assertEqual(self.explanation.plot.word_profile("happy", title="X").layout.title.text, "X") - - def test_absent_word_raises_rather_than_drawing_an_empty_chart(self): - with self.assertRaisesRegex(ValueError, "does not occur"): - self.explanation.plot.word_profile("absent") - - def test_absent_in_scope_names_the_scope(self): - with self.assertRaisesRegex(ValueError, "in the selected samples"): - self.explanation.plot.word_profile("happy", sample_indices=[3]) - - -# --------------------------------------------------------------------------- -# NlpLimeBackend -# --------------------------------------------------------------------------- - -# Keep num_samples tiny so LIME tests run fast in CI. -_LIME_COMPUTE_ARGS = {"num_samples": 50, "num_features": 5} -_SAMPLE_TEXTS = ["i feel so happy today", "this is terrible and sad"] - - -def _fake_classifier(texts: list[str]) -> np.ndarray: - """Deterministic fake classifier returning (n_texts, N_CLASSES) probabilities.""" - rng = np.random.default_rng(0) - probs = rng.random((len(texts), N_CLASSES)).astype(np.float32) - probs /= probs.sum(axis=1, keepdims=True) - return probs - - -def _make_lime_backend() -> NlpLimeBackend: - return NlpLimeBackend( - _fake_classifier, - label_names=LABEL_NAMES, - explainer_compute_args=_LIME_COMPUTE_ARGS, - ) - - -class TestNlpLimeBackend(unittest.TestCase): - def setUp(self): - self.backend = _make_lime_backend() - - # --- init / config --- - - def test_name(self): - self.assertEqual(self.backend.name, "nlp_lime") - - def test_inherits_nlp_backend(self): - self.assertIsInstance(self.backend, NlpBackend) - - def test_label_names_stored(self): - self.assertEqual(self.backend._classes, LABEL_NAMES) - - def test_mask_string_stored(self): - backend = NlpLimeBackend( - _fake_classifier, - label_names=LABEL_NAMES, - mask_string="[MASK]", - explainer_compute_args=_LIME_COMPUTE_ARGS, - ) - self.assertEqual(backend.mask_string, "[MASK]") - - def test_explainer_args_forwarded(self): - backend = NlpLimeBackend( - _fake_classifier, - label_names=LABEL_NAMES, - explainer_args={"bow": False}, - explainer_compute_args=_LIME_COMPUTE_ARGS, - ) - self.assertFalse(backend.explainer.bow) - - # --- _classifier_fn --- - - def test_classifier_fn_converts_list_to_array(self): - def list_model(texts): - return [[0.5] * N_CLASSES for _ in texts] - - backend = NlpLimeBackend(list_model, label_names=LABEL_NAMES) - result = backend._classifier_fn(["hello"]) - self.assertIsInstance(result, np.ndarray) - self.assertEqual(result.shape, (1, N_CLASSES)) - - def test_classifier_fn_passes_through_array(self): - arr = np.ones((2, N_CLASSES), dtype=np.float32) - - def array_model(texts): - return arr - - backend = NlpLimeBackend(array_model, label_names=LABEL_NAMES) - result = backend._classifier_fn(["a", "b"]) - self.assertIs(result, arr) - - def test_classifier_fn_converts_hf_pipeline_format(self): - # HuggingFace pipeline with return_all_scores=True returns list[list[dict]]. - def hf_model(texts): - return [[{"label": name, "score": 1.0 / N_CLASSES} for name in LABEL_NAMES] for _ in texts] - - backend = NlpLimeBackend(hf_model, label_names=LABEL_NAMES) - result = backend._classifier_fn(["hello", "world"]) - self.assertIsInstance(result, np.ndarray) - self.assertEqual(result.shape, (2, N_CLASSES)) - self.assertEqual(result.dtype, np.float64) - # Each score should be 1/N_CLASSES - np.testing.assert_allclose(result, 1.0 / N_CLASSES, atol=1e-6) - - # --- run_explainer --- - - def test_run_explainer_returns_nlp_contributions(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertIsInstance(raw, NlpContributions) - - def test_run_explainer_contributions_count(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertEqual(len(raw.values), len(_SAMPLE_TEXTS)) - - def test_run_explainer_contributions_shape(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - for arr in raw.values: - self.assertEqual(arr.ndim, 2) - self.assertEqual(arr.shape[1], N_CLASSES) - - def test_run_explainer_base_values_shape(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertEqual(raw.base_values.shape, (len(_SAMPLE_TEXTS), N_CLASSES)) - - def test_run_explainer_data_is_word_list(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - self.assertEqual(len(raw.token_strings), len(_SAMPLE_TEXTS)) - for word_list in raw.token_strings: - self.assertIsInstance(word_list, list) - self.assertTrue(all(isinstance(w, str) for w in word_list)) - - def test_run_explainer_sparse_weights(self): - # LIME fills at most num_features non-zero weights per label column. - raw = self.backend.run_explainer(_SAMPLE_TEXTS[:1]) - matrix = raw.values[0] - for col in range(N_CLASSES): - self.assertLessEqual( - np.count_nonzero(matrix[:, col]), - _LIME_COMPUTE_ARGS["num_features"], - ) - - # --- get_local_contributions --- - - def test_get_local_contributions_returns_nlp_contributions(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) - self.assertIsInstance(contrib, NlpContributions) - - def test_get_local_contributions_token_strings_match_data(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) - self.assertEqual(contrib.token_strings, raw.token_strings) - - def test_get_local_contributions_values_match_contributions(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw) - for got, expected in zip(contrib.values, raw.values): - np.testing.assert_array_equal(got, expected) - - def test_get_local_contributions_subset(self): - raw = self.backend.run_explainer(_SAMPLE_TEXTS) - contrib = self.backend.get_local_contributions(_SAMPLE_TEXTS, raw, subset=[0]) - self.assertEqual(len(contrib.token_strings), 1) - self.assertEqual(len(contrib.values), 1) - self.assertEqual(contrib.base_values.shape[0], 1) - - -# --------------------------------------------------------------------------- -# NlpExplainer with NlpLimeBackend -# --------------------------------------------------------------------------- - - -class TestNlpExplainerWithLimeBackend(unittest.TestCase): - def _make_explainer_lime(self) -> NlpExplainer: - """NlpExplainer backed by NlpLimeBackend.""" - xpl = _make_explainer() - xpl.backend = _make_lime_backend() - return xpl - - def test_backend_is_lime_instance(self): - xpl = self._make_explainer_lime() - self.assertIsInstance(xpl.backend, NlpLimeBackend) - - def test_plot_tokens_works_for_a_lime_explanation(self): - # LIME is non-additive, so .plot.waterfall refuses; .plot.tokens stays available. - explanation = replace(_make_explanation(), backend_name="nlp_lime", is_additive=False) - self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) - with self.assertRaises(ValueError): - explanation.plot.waterfall(row=0, label_idx=0) - - def test_explain_sets_contributions(self): - backend = _make_lime_backend() - xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) - fake_pred_df = pd.DataFrame( - {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, - index=pd.RangeIndex(len(_SAMPLE_TEXTS)), - ) - with patch.object(xpl, "_predict", return_value=fake_pred_df): - explanation = xpl.explain(_SAMPLE_TEXTS) - self.assertEqual(len(explanation), len(_SAMPLE_TEXTS)) - self.assertEqual(explanation.label_names, LABEL_NAMES) - - def test_explain_caching_skips_rerun(self): - backend = _make_lime_backend() - xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=backend) - fake_pred_df = pd.DataFrame( - {"prediction": ["joy"] * len(_SAMPLE_TEXTS)}, - index=pd.RangeIndex(len(_SAMPLE_TEXTS)), - ) - with patch.object(xpl, "_predict", return_value=fake_pred_df) as mocked_predict: - xpl.explain(_SAMPLE_TEXTS) - xpl.explain(_SAMPLE_TEXTS) # same data — must hit in-memory cache - self.assertEqual(mocked_predict.call_count, 1, "memoization must skip the second _predict call") - - -# --------------------------------------------------------------------------- -# compile() cache key -# --------------------------------------------------------------------------- - - -class _KeyModel(TextModel): - """Prediction-only model with a caller-chosen ``model_id``, so keys can be varied deliberately.""" - - def __init__(self, ident="model-a"): - super().__init__(label_names=LABEL_NAMES) - self._ident = ident - - @property - def model_id(self): - return self._ident - - def predict(self, texts): - probs = np.full((len(texts), N_CLASSES), 1.0 / N_CLASSES) - return probs - - -class _MarkerBackend(NlpBackend): - """Returns a constant contribution equal to ``marker``, and counts its explainer runs. - - The constant makes it visible *whose* result a cache served: a value of 1.0 can only have come - from the backend built with ``marker=1.0``. - """ - - name = "marker_backend" - reference_kind = "none" - is_additive = True - output_space = "probability" - - def __init__(self, marker=1.0, **kwargs): - super().__init__(model=None, label_names=LABEL_NAMES, **kwargs) - self.marker = marker - self.calls = 0 - - def run_explainer(self, x): - self.calls += 1 - texts = list(x) - return NlpContributions( - token_strings=[["a", "b"] for _ in texts], - values=[np.full((2, N_CLASSES), self.marker) for _ in texts], - base_values=np.zeros((len(texts), N_CLASSES)), - ) - - -class _OtherMarkerBackend(_MarkerBackend): - """Same behaviour under a different registered ``name`` — a distinct attribution method.""" - - name = "other_marker_backend" - - -def _explainer(model=None, backend=None, label_names=LABEL_NAMES): - return NlpExplainer( - model or _KeyModel(), - label_names=label_names, - backend=backend or _MarkerBackend(), - ) - - -class TestExplainCacheKey(unittest.TestCase): - """``explain`` results depend on *(texts, model, backend)* — so all three must be in the key. - - Keying on the texts alone means swapping the model or the attribution backend and pointing at the - same ``cache_dir`` silently reloads the previous run's contributions, and even without a - ``cache_dir`` the in-memory guard turns a re-``explain`` into a no-op. - """ - - def test_identical_inputs_still_hit_the_in_memory_cache(self): - backend = _MarkerBackend() - xpl = _explainer(backend=backend) - xpl.explain(_SAMPLE_TEXTS) - xpl.explain(_SAMPLE_TEXTS) - self.assertEqual(backend.calls, 1, "memoization must survive the richer key") - - def test_the_memo_cannot_be_poisoned_through_a_returned_artifact(self): - """``explain`` returns a ``replace()`` of the memoized artifact — a *shallow* copy. - - The two artifacts therefore share their contribution arrays, so before the arrays were - sealed an in-place edit of the first return value silently rewrote the cache, and every - later ``explain`` of the same texts served the corrupted numbers as a fresh result. The - seal turns that into an error at the point of the edit. - """ - backend = _MarkerBackend(marker=1.0) - xpl = _explainer(backend=backend) - first = xpl.explain(_SAMPLE_TEXTS) - - with self.assertRaises(ValueError): - first.values[0][0] = -42.0 - - second = xpl.explain(_SAMPLE_TEXTS) - self.assertEqual(backend.calls, 1, "still a memo hit — the seal must not defeat caching") - np.testing.assert_allclose(second.values[0], 1.0) - - def test_a_memo_hit_is_relabelled_onto_the_caller_index(self): - """The memo is keyed by text *content*, so the same texts from a differently-indexed - source is a hit — and the cached predictions still carry the first caller's index. - ``replace`` says nothing about the fields it is not handed, so every index-bearing field - has to be re-labelled, not just ``texts``. - """ - backend = _MarkerBackend() - xpl = _explainer(backend=backend) - xpl.explain(pd.Series(_SAMPLE_TEXTS)) # first call: default RangeIndex - - index = pd.Index([77 + i for i in range(len(_SAMPLE_TEXTS))]) - second = xpl.explain(pd.Series(_SAMPLE_TEXTS, index=index)) - - self.assertEqual(backend.calls, 1, "re-labelling must not cost a recompute") - for field_name in ("texts", "y_pred", "y_prob"): - with self.subTest(field=field_name): - self.assertTrue(getattr(second, field_name).index.equals(index)) - - def test_a_y_series_that_does_not_match_x_is_refused(self): - """Reindexing it silently would pair labels positionally while looking like alignment.""" - xpl = _explainer(backend=_MarkerBackend()) - texts = pd.Series(_SAMPLE_TEXTS, index=[10 + i for i in range(len(_SAMPLE_TEXTS))]) - misaligned = pd.Series(["joy"] * len(_SAMPLE_TEXTS), index=range(len(_SAMPLE_TEXTS))) - with self.assertRaises(ValueError) as ctx: - xpl.explain(texts, y=misaligned) - self.assertIn("indexed differently", str(ctx.exception)) - - def test_a_y_series_aligned_to_x_is_kept_and_a_list_is_indexed_positionally(self): - xpl = _explainer(backend=_MarkerBackend()) - index = pd.Index([10 + i for i in range(len(_SAMPLE_TEXTS))]) - texts = pd.Series(_SAMPLE_TEXTS, index=index) - labels = ["joy"] * len(_SAMPLE_TEXTS) - for tag, y in (("series", pd.Series(labels, index=index)), ("list", labels)): - with self.subTest(y=tag): - self.assertTrue(xpl.explain(texts, y=y).y_true.index.equals(index)) - - def test_swapping_the_backend_recomputes(self): - xpl = _explainer(backend=_MarkerBackend(marker=1.0)) - xpl.explain(_SAMPLE_TEXTS) - replacement = _OtherMarkerBackend(marker=2.0) - xpl.backend = replacement - explanation = xpl.explain(_SAMPLE_TEXTS) - self.assertEqual(replacement.calls, 1, "in-memory guard served a stale, other-backend result") - np.testing.assert_allclose(explanation.values[0], 2.0) - - def test_backend_compute_args_are_part_of_the_key(self): - xpl = _explainer(backend=_MarkerBackend(explainer_compute_args={"n_steps": 50})) - first = xpl._compute_key(_SAMPLE_TEXTS) - xpl.backend = _MarkerBackend(explainer_compute_args={"n_steps": 200}) - self.assertNotEqual(first, xpl._compute_key(_SAMPLE_TEXTS)) - - def test_compute_args_key_is_order_insensitive(self): - # Same settings written in a different order are the same configuration. - a = _explainer(backend=_MarkerBackend(explainer_compute_args={"x": 1, "y": 2})) - b = _explainer(backend=_MarkerBackend(explainer_compute_args={"y": 2, "x": 1})) - self.assertEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) - - def test_model_identity_is_part_of_the_key(self): - a = _explainer(model=_KeyModel("model-a")) - b = _explainer(model=_KeyModel("model-b")) - self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) - - def test_label_names_are_part_of_the_key(self): - # label_names fixes the column order of y_prob, so it changes the cached payload. - a = _explainer(label_names=LABEL_NAMES) - b = _explainer(label_names=list(reversed(LABEL_NAMES))) - self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) - - def test_key_is_collision_safe_across_text_boundaries(self): - # Without a separator between texts these two corpora hash identically. - xpl = _explainer() - self.assertNotEqual(xpl._compute_key(["ab", "c"]), xpl._compute_key(["a", "bc"])) - - def test_key_is_order_sensitive(self): - xpl = _explainer() - self.assertNotEqual(xpl._compute_key(["a", "b"]), xpl._compute_key(["b", "a"])) - - -def _read_meta(path) -> dict: - with zipfile.ZipFile(path) as zf: - return json.loads(zf.read("meta.json")) - - -def _drop_meta_key(path, key) -> None: - """Rewrite a saved ``.xpl`` in place without one ``meta.json`` key — how an older layout is faked.""" - with zipfile.ZipFile(path) as zin: - members = {item.filename: zin.read(item.filename) for item in zin.infolist()} - meta = json.loads(members["meta.json"]) - del meta[key] - members["meta.json"] = json.dumps(meta).encode() - with zipfile.ZipFile(path, "w") as zout: - for name, data in members.items(): - zout.writestr(name, data) - - -class TestExplainDiskCacheIsolation(unittest.TestCase): - """The disk cache is the dangerous case: a stale entry survives the process that wrote it.""" - - def test_two_backends_share_a_cache_dir_without_collision(self): - with tempfile.TemporaryDirectory() as cache_dir: - first = _MarkerBackend(marker=1.0) - _explainer(backend=first).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - - second = _OtherMarkerBackend(marker=2.0) - xpl = _explainer(backend=second) - explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - - self.assertEqual(second.calls, 1, "loaded the other backend's cached contributions") - np.testing.assert_allclose(explanation.values[0], 2.0) - - def test_two_models_share_a_cache_dir_without_collision(self): - with tempfile.TemporaryDirectory() as cache_dir: - _explainer(model=_KeyModel("model-a"), backend=_MarkerBackend(marker=1.0)).explain( - _SAMPLE_TEXTS, cache_dir=cache_dir - ) - backend_b = _MarkerBackend(marker=2.0) - xpl = _explainer(model=_KeyModel("model-b"), backend=backend_b) - explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - - self.assertEqual(backend_b.calls, 1, "loaded the other model's cached contributions") - np.testing.assert_allclose(explanation.values[0], 2.0) - - def test_same_model_and_backend_reload_from_disk(self): - # The cache must still *work* — a fresh instance skips the expensive run. - with tempfile.TemporaryDirectory() as cache_dir: - _explainer(backend=_MarkerBackend(marker=3.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - reloaded_backend = _MarkerBackend(marker=3.0) - xpl = _explainer(backend=reloaded_backend) - explanation = xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - self.assertEqual(reloaded_backend.calls, 0, "disk cache did not hit for identical inputs") - np.testing.assert_allclose(explanation.values[0], 3.0) - - def test_cache_path_points_at_the_file_explain_writes(self): - with tempfile.TemporaryDirectory() as cache_dir: - xpl = _explainer() - path = xpl.cache_path(_SAMPLE_TEXTS, cache_dir) - self.assertFalse(path.exists()) - xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - self.assertTrue(path.exists(), "cache_path disagrees with where explain() wrote") - - def test_stale_layout_is_a_miss_not_a_crash(self): - # The cache key covers texts/model/backend but not the shapash version, so a layout change - # leaves entries the new reader cannot parse. The entry must be recomputed and overwritten. - with tempfile.TemporaryDirectory() as cache_dir: - _explainer(backend=_MarkerBackend(marker=4.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) - _drop_meta_key(path, "values_ndim") - - backend = _MarkerBackend(marker=4.0) - explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - - self.assertEqual(backend.calls, 1, "a rejected cache entry must fall through to a recompute") - np.testing.assert_allclose(explanation.values[0], 4.0) - self.assertIn("values_ndim", _read_meta(path), "stale entry was not replaced") - - def test_corrupt_cache_entry_is_a_miss_not_a_crash(self): - with tempfile.TemporaryDirectory() as cache_dir: - _explainer(backend=_MarkerBackend(marker=5.0)).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) - path.write_bytes(b"not a zip file") - - backend = _MarkerBackend(marker=5.0) - explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - - self.assertEqual(backend.calls, 1) - np.testing.assert_allclose(explanation.values[0], 5.0) - - def test_explicit_load_still_raises_on_a_stale_file(self): - # The miss-not-crash rule is scoped to the cache. A caller who names a file gets an error - # rather than a silent substitution. - with tempfile.TemporaryDirectory() as cache_dir: - _explainer().explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - path = _explainer().cache_path(_SAMPLE_TEXTS, cache_dir) - _drop_meta_key(path, "values_ndim") - with self.assertRaises(KeyError): - NlpExplanation.load(path) - - def test_clear_cache_forces_a_recompute(self): - with tempfile.TemporaryDirectory() as cache_dir: - backend = _MarkerBackend() - xpl = _explainer(backend=backend) - xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - xpl.clear_cache(_SAMPLE_TEXTS, cache_dir) - self.assertFalse(xpl.cache_path(_SAMPLE_TEXTS, cache_dir).exists()) - xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - self.assertEqual(backend.calls, 2, "clear_cache must defeat the in-memory guard too") - - def test_clear_cache_leaves_other_backends_entries_intact(self): - with tempfile.TemporaryDirectory() as cache_dir: - keeper = _MarkerBackend(marker=1.0) - _explainer(backend=keeper).explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - keeper_path = _explainer(backend=_MarkerBackend(marker=1.0)).cache_path(_SAMPLE_TEXTS, cache_dir) - - other = _explainer(backend=_OtherMarkerBackend(marker=2.0)) - other.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) - other.clear_cache(_SAMPLE_TEXTS, cache_dir) - - self.assertTrue(keeper_path.exists(), "clearing one backend dropped another's cache") - - -class TestDetectLabelNoise(unittest.TestCase): - """The explainer's confident-learning surface, over an ``NlpExplanation`` (no model, no backend).""" - - def _with_labels(self, y_true=None, y_prob=None) -> NlpExplanation: - """A batch carrying ground truth and per-class probabilities. - - Sample 1 is labelled ``joy`` while the model confidently says ``sadness`` — the planted - error. Both classes carry at least one label, without which ``sadness`` would have no - estimable threshold and could never be suggested (covered separately in the compute tests). - """ - confident = ["joy", "sadness", "sadness"] - probs = np.full((3, N_CLASSES), 0.02) - for row, name in enumerate(confident): - probs[row, LABEL_NAMES.index(name)] = 0.90 - probs = probs / probs.sum(axis=1, keepdims=True) - y_pred = pd.Series(confident, index=pd.RangeIndex(3), name="prediction") - resolved_y_prob = pd.DataFrame(probs, index=pd.RangeIndex(3), columns=LABEL_NAMES) if y_prob is None else y_prob - resolved_y_true = ( - pd.Series(["joy", "joy", "sadness"], index=pd.RangeIndex(3), name="ground_truth") - if y_true is None - else y_true - ) - return _make_explanation(y_pred=y_pred, y_prob=resolved_y_prob, y_true=resolved_y_true) - - # ── capability flag ──────────────────────────────────────────────── - def test_available_with_ground_truth_and_per_class_probabilities(self): - self.assertTrue(_make_explainer().can_detect_label_noise(self._with_labels())) - - def test_unavailable_without_ground_truth(self): - explanation = replace(self._with_labels(), y_true=None) - self.assertFalse(_make_explainer().can_detect_label_noise(explanation)) - - def test_unavailable_with_only_the_winning_class_probability(self): - # The raw-pipeline path emits a single "probability" column; the losing classes' scores are - # precisely what confident learning needs. - legacy = pd.DataFrame({"probability": [0.9, 0.9, 0.9]}, index=pd.RangeIndex(3)) - self.assertFalse(_make_explainer().can_detect_label_noise(self._with_labels(y_prob=legacy))) - - def test_unavailable_on_an_explanation_with_no_probabilities(self): - self.assertFalse(_make_explainer().can_detect_label_noise(_make_explanation())) - - def test_available_without_a_model(self): - # Unlike the other capability flags this needs no live model. - xpl = _make_explainer() - self.assertIsNone(xpl.model) - self.assertTrue(xpl.can_detect_label_noise(self._with_labels())) - - # ── detection ────────────────────────────────────────────────────── - def test_raises_when_the_prerequisites_are_missing(self): - with self.assertRaisesRegex(RuntimeError, "ground-truth labels"): - _make_explainer().detect_label_noise(_make_explanation()) - - def test_flags_the_planted_mislabel(self): - report = _make_explainer().detect_label_noise(self._with_labels()) - self.assertEqual([i.index for i in report.issues], [1]) - issue = report.issues[0] - self.assertEqual(issue.given_label, "joy") - self.assertEqual(issue.suggested_label, "sadness") - self.assertEqual(issue.text, "this is terrible and sad") - - def test_label_names_come_from_the_probability_columns(self): - report = _make_explainer().detect_label_noise(self._with_labels()) - self.assertEqual(report.label_names, LABEL_NAMES) - self.assertEqual(report.noise_matrix.shape, (N_CLASSES, N_CLASSES)) - self.assertEqual(report.n_samples, 3) - - def test_respects_top_n_and_score(self): - report = _make_explainer().detect_label_noise(self._with_labels(), top_n=0, score="normalized_margin") - self.assertEqual(report.issues, []) - self.assertEqual(report.n_issues, 1) - - # ── memoisation ──────────────────────────────────────────────────── - def test_repeats_are_served_from_the_memo(self): - xpl = _make_explainer() - explanation = self._with_labels() - first = xpl.detect_label_noise(explanation, top_n=5) - self.assertIs(xpl.detect_label_noise(explanation, top_n=5), first) - - def test_different_arguments_recompute(self): - xpl = _make_explainer() - explanation = self._with_labels() - self.assertIsNot(xpl.detect_label_noise(explanation, top_n=5), xpl.detect_label_noise(explanation, top_n=4)) - - # ── independent probe ────────────────────────────────────────────── - def _probe_corpus(self): - """A reference corpus separable by words the audited fixture's texts also use.""" - texts = [ - "this is wonderful and joyful", - "wonderful joyful and bright", - "a joyful wonderful day", - "bright and wonderful joy", - "this is terrible and sad", - "terrible sad and bleak", - "a sad terrible day", - "bleak and terrible sadness", - ] - labels = ["joy"] * 4 + ["sadness"] * 4 - return texts, labels - - def test_no_probe_when_no_reference_corpus_is_bound(self): - xpl = _make_explainer() - self.assertFalse(xpl.can_probe_labels()) - self.assertIsNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) - - def test_probe_verdict_is_attached_when_a_labelled_corpus_is_bound(self): - xpl = _make_explainer() - xpl.reference_ = self._probe_corpus() - self.assertTrue(xpl.can_probe_labels()) - issue = xpl.detect_label_noise(self._with_labels()).issues[0] - self.assertIsNotNone(issue.probe) - self.assertIn(issue.probe.top_label, {"joy", "sadness"}) - self.assertEqual(issue.probe.backs_given, issue.probe.top_label == issue.given_label) - - def test_probe_corroborates_a_genuine_label_error(self): - # The flagged row is "this is terrible and sad" carrying the label "joy". The reference - # corpus puts that vocabulary firmly in "sadness", so the probe rejects the given label too - # — the two-signals-agree case, which is the one worth relabelling. - xpl = _make_explainer() - xpl.reference_ = self._probe_corpus() - issue = xpl.detect_label_noise(self._with_labels()).issues[0] - self.assertEqual((issue.given_label, issue.text), ("joy", "this is terrible and sad")) - self.assertFalse(issue.probe.backs_given) - self.assertEqual(issue.probe.top_label, "sadness") - self.assertLess(issue.probe.given_prob, 0.5) - - def test_probe_backs_the_label_when_the_corpus_sides_with_it(self): - # The mirror case, and the reason the column exists: same flagged row, but a corpus that - # calls this vocabulary "joy". The probe now defends the label, marking the row as the - # audited model's error rather than the corpus's. - xpl = _make_explainer() - texts, _ = self._probe_corpus() - xpl.reference_ = (texts, ["sadness"] * 4 + ["joy"] * 4) - issue = xpl.detect_label_noise(self._with_labels()).issues[0] - self.assertEqual(issue.given_label, "joy") - self.assertTrue(issue.probe.backs_given) - self.assertGreater(issue.probe.given_prob, 0.5) - - def test_probe_is_skipped_when_not_requested(self): - xpl = _make_explainer() - xpl.reference_ = self._probe_corpus() - self.assertIsNone(xpl.detect_label_noise(self._with_labels(), probe=False).issues[0].probe) - - def test_probe_is_fit_once_and_reused_across_calls(self): - xpl = _make_explainer() - xpl.reference_ = self._probe_corpus() - explanation = self._with_labels() - xpl.detect_label_noise(explanation, top_n=5) - first = xpl._label_probe - self.assertIsNotNone(first) - xpl.detect_label_noise(explanation, top_n=4) # different args -> recompute, but the probe is kept - self.assertIs(xpl._label_probe, first) - - def test_probe_needs_no_model_or_retriever(self): - # The point of fitting on plain text: a prediction-only pipeline that cannot embed (so - # can_find_similar() is False) still gets the second opinion. - xpl = _make_explainer() - xpl.reference_ = self._probe_corpus() - xpl._retriever = None - xpl._text_model = None - self.assertFalse(xpl.can_find_similar()) - self.assertTrue(xpl.can_probe_labels()) - self.assertIsNotNone(xpl.detect_label_noise(self._with_labels()).issues[0].probe) - - -class TestRunAppMountPath(unittest.TestCase): - """``run_app`` forwards the reverse-proxy mount point to the webapp it builds.""" - - def test_url_base_pathname_reaches_the_webapp(self): - xpl = object.__new__(NlpExplainer) - explanation = object() - with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: - xpl.run_app(explanation, url_base_pathname="/shapash-nlp-explainer/") - - self.assertIs(web_app.call_args.args[0], explanation) - self.assertEqual( - web_app.call_args.kwargs, - { - "engine": xpl, - "projection": None, - "url_base_pathname": "/shapash-nlp-explainer/", - "palette_name": "default", - "colors_dict": None, - "info": {}, - }, - ) - - def test_defaults_to_no_prefix(self): - xpl = object.__new__(NlpExplainer) - with patch("shapash.explainer.nlp_explainer.NlpWebApp") as web_app: - xpl.run_app(object()) - self.assertIsNone(web_app.call_args.kwargs["url_base_pathname"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit_tests/plots/test_plot_confusion_matrix.py b/tests/unit_tests/plots/test_plot_confusion_matrix.py new file mode 100644 index 00000000..319ac199 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_confusion_matrix.py @@ -0,0 +1,51 @@ +"""Unit tests for the plot_confusion_matrix function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +import plotly.graph_objs as go + +from shapash.plots.plot_confusion_matrix import plot_confusion_matrix + + +class TestPlotConfusionMatrix(unittest.TestCase): + def setUp(self): + # 3-class matrix: rows = true, cols = predicted. Row 2 (index 2) is empty. + self.cm = np.array([[5, 2, 0], [1, 4, 0], [0, 0, 0]]) + self.labels = ["A", "B", "C"] + + def test_returns_heatmap_figure(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertIsInstance(fig, go.Figure) + self.assertIsInstance(fig.data[0], go.Heatmap) + + def test_axes_are_labelled_true_and_predicted(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertEqual(list(fig.data[0].x), self.labels) + self.assertEqual(list(fig.data[0].y), self.labels) + + def test_customdata_encodes_pred_then_true(self): + # customdata[true][pred] must be [pred_idx, true_idx] for the click handler. + fig = plot_confusion_matrix(self.cm, self.labels) + cd = np.asarray(fig.data[0].customdata) + self.assertEqual(list(cd[0, 1]), [1, 0]) # true=0, pred=1 + self.assertEqual(list(cd[1, 2]), [2, 1]) # true=1, pred=2 + + def test_counts_shown_as_text(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertEqual(fig.data[0].text[0][0], "5") + + def test_normalize_true_is_row_recall(self): + fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") + z = np.asarray(fig.data[0].z) + np.testing.assert_allclose(z[0], [5 / 7, 2 / 7, 0.0]) + + def test_normalize_true_handles_empty_row_without_nan(self): + fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") + z = np.asarray(fig.data[0].z) + self.assertFalse(np.isnan(z).any()) + np.testing.assert_array_equal(z[2], [0.0, 0.0, 0.0]) + + def test_custom_title(self): + fig = plot_confusion_matrix(self.cm, self.labels, title="Errors") + self.assertIn("Errors", fig.layout.title.text) diff --git a/tests/unit_tests/plots/test_plot_noise_matrix.py b/tests/unit_tests/plots/test_plot_noise_matrix.py new file mode 100644 index 00000000..9e5925e0 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_noise_matrix.py @@ -0,0 +1,47 @@ +"""Unit tests for the plot_noise_matrix function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +import plotly.graph_objs as go + +from shapash.plots.plot_noise_matrix import plot_noise_matrix + + +class TestPlotNoiseMatrix(unittest.TestCase): + """The noise matrix is given-vs-true and mostly diagonal, so it needs its own axes and masking.""" + + def setUp(self): + # Typical shape: ~94% correctly labelled, one contaminated pair (A labelled, really B). + self.joint = np.array([[0.50, 0.05, 0.0], [0.01, 0.30, 0.0], [0.0, 0.0, 0.14]]) + self.labels = ["A", "B", "C"] + + def test_returns_heatmap_figure(self): + self.assertIsInstance(plot_noise_matrix(self.joint, self.labels), go.Figure) + + def test_diagonal_is_masked_by_default(self): + # Left in, the ~90% diagonal flattens every off-diagonal cell to the same near-white shade. + z = plot_noise_matrix(self.joint, self.labels).data[0].z + self.assertTrue(np.all(np.isnan(np.diag(z)))) + self.assertAlmostEqual(z[0][1], 0.05) + + def test_masked_cells_render_no_text(self): + text = plot_noise_matrix(self.joint, self.labels).data[0].text + self.assertEqual(text[0][0], "") + self.assertEqual(text[0][1], "5.0%") + + def test_diagonal_can_be_kept(self): + z = plot_noise_matrix(self.joint, self.labels, mask_diagonal=False).data[0].z + np.testing.assert_allclose(np.diag(z), [0.50, 0.30, 0.14]) + + def test_does_not_mutate_the_callers_matrix(self): + original = self.joint.copy() + plot_noise_matrix(self.joint, self.labels) + np.testing.assert_array_equal(self.joint, original) + + def test_axes_name_the_label_semantics_not_the_prediction(self): + # The whole reason this is not a plot_confusion_matrix mode: the columns are the estimated + # *true* class, not the model's prediction. + layout = plot_noise_matrix(self.joint, self.labels).layout + self.assertEqual(layout.yaxis.title.text, "Given label") + self.assertEqual(layout.xaxis.title.text, "Estimated true class") diff --git a/tests/unit_tests/plots/test_plot_scatter.py b/tests/unit_tests/plots/test_plot_scatter.py new file mode 100644 index 00000000..981d9d67 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_scatter.py @@ -0,0 +1,58 @@ +"""Unit tests for the plot_scatter function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +import plotly.graph_objs as go + +from shapash.plots.plot_scatter import plot_scatter + + +class TestPlotScatter(unittest.TestCase): + def setUp(self): + self.xy = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]]) + self.texts = ["so happy today !", "Happy and happy again", "not happy at all", "nothing here"] + + def test_categorical_mode_draws_one_trace_per_label_name(self): + fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "neg", "neg"], label_names=["pos", "neg"]) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data), 2) + self.assertEqual(fig.data[0].name, "pos") + self.assertEqual([row[0] for row in fig.data[0].customdata], [0, 1]) + + def test_categorical_mode_skips_empty_categories(self): + fig = plot_scatter(self.xy, self.texts, labels=["pos", "pos", "pos", "pos"], label_names=["pos", "neg"]) + self.assertEqual(len(fig.data), 1) + + def test_word_contribution_mode_splits_present_and_absent(self): + fig = plot_scatter(self.xy, self.texts, contributions=np.array([0.4, 0.0, -0.6, 0.0])) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data), 2) + absent_trace, present_trace = fig.data + self.assertEqual([row[0] for row in absent_trace.customdata], [1, 3]) + self.assertEqual([row[0] for row in present_trace.customdata], [0, 2]) + + def test_error_mask_shrinks_correct_points_and_grows_wrong_ones(self): + contributions = np.array([0.4, 0.0, -0.6, 0.0]) + error_mask = np.array([True, False, False, False]) + fig = plot_scatter(self.xy, self.texts, contributions=contributions, error_mask=error_mask) + _, present_trace = fig.data + # present_mask is [0, 2]; sample 0 is the error and should be emphasized over sample 2. + sizes = present_trace.marker.size + self.assertGreater(sizes[0], sizes[1]) + + def test_requires_exactly_one_of_contributions_or_labels(self): + with self.assertRaises(ValueError): + plot_scatter(self.xy, self.texts) + with self.assertRaises(ValueError): + plot_scatter( + self.xy, + self.texts, + labels=["pos"] * 4, + label_names=["pos"], + contributions=np.zeros(4), + ) + + def test_labels_without_label_names_raises(self): + with self.assertRaises(ValueError): + plot_scatter(self.xy, self.texts, labels=["pos"] * 4) diff --git a/tests/unit_tests/plots/test_plot_sentence_highlight.py b/tests/unit_tests/plots/test_plot_sentence_highlight.py new file mode 100644 index 00000000..eb7b980c --- /dev/null +++ b/tests/unit_tests/plots/test_plot_sentence_highlight.py @@ -0,0 +1,36 @@ +"""Unit tests for the plot_sentence_highlight function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +from dash import html + +from shapash.plots.plot_sentence_highlight import plot_sentence_highlight + + +class TestPlotSentenceHighlight(unittest.TestCase): + def setUp(self): + self.tokens = ["[CLS]", "i", "feel", "happy", "[SEP]"] + self.values = np.array([0.01, 0.05, 0.30, -0.20, 0.01]) + + def test_returns_html_div(self): + result = plot_sentence_highlight(self.tokens, self.values) + self.assertIsInstance(result, html.Div) + + def test_has_children(self): + result = plot_sentence_highlight(self.tokens, self.values) + self.assertIsNotNone(result.children) + # legend + spans div + summary + self.assertGreaterEqual(len(result.children), 3) + + def test_raises_on_2d_values(self): + with self.assertRaises(ValueError): + plot_sentence_highlight(self.tokens, np.zeros((5, 3))) + + def test_with_base_value_does_not_raise(self): + result = plot_sentence_highlight(self.tokens, self.values, base_value=0.15) + self.assertIsInstance(result, html.Div) + + def test_empty_tokens_does_not_raise(self): + result = plot_sentence_highlight([], np.array([])) + self.assertIsInstance(result, html.Div) diff --git a/tests/unit_tests/plots/test_plot_token_highlight.py b/tests/unit_tests/plots/test_plot_token_highlight.py new file mode 100644 index 00000000..8e222794 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_token_highlight.py @@ -0,0 +1,46 @@ +"""Unit tests for the plot_token_highlight function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +import plotly.graph_objs as go + +from shapash.plots.plot_token_highlight import plot_token_highlight + + +class TestPlotTokenHighlight(unittest.TestCase): + def setUp(self): + self.tokens = ["i", "feel", "happy", "today"] + self.values = np.array([0.1, 0.4, -0.2, 0.05]) + + def test_returns_figure(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertIsInstance(fig, go.Figure) + + def test_has_one_bar_trace(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Bar) + + def test_max_tokens_limits_bars(self): + fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) + self.assertLessEqual(len(fig.data[0].x), 2) + + def test_max_tokens_preserves_sentence_order(self): + # top-2 by |value| are "feel" (0.4) and "happy" (-0.2) + # sentence order: "feel" before "happy" + fig = plot_token_highlight(self.tokens, self.values, max_tokens=2) + # y-axis is reversed for display (highest importance at top → reversed list) + displayed_labels = list(fig.data[0].y) + feel_pos = displayed_labels.index("feel") + happy_pos = displayed_labels.index("happy") + # In reversed display: "feel" appears after "happy" (lower index = further down) + self.assertNotEqual(feel_pos, happy_pos) + + def test_custom_title(self): + fig = plot_token_highlight(self.tokens, self.values, title="Test title") + self.assertIn("Test title", fig.layout.title.text) + + def test_orientation_horizontal(self): + fig = plot_token_highlight(self.tokens, self.values) + self.assertEqual(fig.data[0].orientation, "h") diff --git a/tests/unit_tests/plots/test_plot_waterfall.py b/tests/unit_tests/plots/test_plot_waterfall.py new file mode 100644 index 00000000..1f49ba19 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_waterfall.py @@ -0,0 +1,77 @@ +"""Unit tests for the plot_waterfall function — pure, no explainer or model required.""" + +import unittest + +import numpy as np +import plotly.graph_objs as go + +from shapash.plots.plot_waterfall import plot_waterfall + + +class TestPlotWaterfall(unittest.TestCase): + def setUp(self): + self.tokens = ["[CLS]", "i", "feel", "so", "happy", "today", "[SEP]"] + self.values = np.array([0.01, 0.08, 0.35, 0.02, -0.20, 0.05, 0.01]) + + def test_returns_figure(self): + fig = plot_waterfall(self.tokens, self.values) + self.assertIsInstance(fig, go.Figure) + + def test_has_waterfall_trace(self): + fig = plot_waterfall(self.tokens, self.values) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Waterfall) + + def test_filters_special_tokens_by_default(self): + fig = plot_waterfall(self.tokens, self.values) + y_labels = list(fig.data[0].y) + for label in y_labels: + self.assertNotIn("[CLS]", label) + self.assertNotIn("[SEP]", label) + + def test_keeps_special_tokens_when_disabled(self): + # min_pct=0 disables grouping so every token gets its own bar + fig = plot_waterfall(self.tokens, self.values, filter_special=False, min_pct=0.0) + y_labels = list(fig.data[0].y) + self.assertTrue(any("[CLS]" in lbl for lbl in y_labels)) + + def test_total_bar_present(self): + fig = plot_waterfall(self.tokens, self.values) + measures = list(fig.data[0].measure) + self.assertIn("total", measures) + + def test_grouping_reduces_bar_count(self): + # With min_pct=0.0 (no grouping), every non-special token is its own bar + fig_no_group = plot_waterfall(self.tokens, self.values, min_pct=0.0) + # With min_pct=0.5, small tokens are lumped + fig_grouped = plot_waterfall(self.tokens, self.values, min_pct=0.5) + self.assertLessEqual(len(fig_grouped.data[0].y), len(fig_no_group.data[0].y)) + + def test_other_bar_present_when_grouping_active(self): + fig = plot_waterfall(self.tokens, self.values, min_pct=0.5) + y_labels = list(fig.data[0].y) + self.assertTrue(any("other" in lbl for lbl in y_labels)) + + def test_no_other_bar_when_min_pct_zero(self): + fig = plot_waterfall(self.tokens, self.values, min_pct=0.0) + y_labels = list(fig.data[0].y) + self.assertFalse(any("other" in lbl for lbl in y_labels)) + + def test_base_value_creates_absolute_bar(self): + fig = plot_waterfall(self.tokens, self.values, base_value=0.20) + measures = list(fig.data[0].measure) + self.assertEqual(measures[0], "absolute") + y_labels = list(fig.data[0].y) + self.assertEqual(y_labels[0], "Base") + + def test_empty_tokens_returns_figure(self): + fig = plot_waterfall([], np.array([])) + self.assertIsInstance(fig, go.Figure) + + def test_all_special_tokens_returns_figure(self): + fig = plot_waterfall(["[CLS]", "[SEP]"], np.array([0.01, -0.01])) + self.assertIsInstance(fig, go.Figure) + + def test_custom_title(self): + fig = plot_waterfall(self.tokens, self.values, title="joy waterfall") + self.assertIn("joy waterfall", fig.layout.title.text) diff --git a/tests/unit_tests/plots/test_plot_word_importance.py b/tests/unit_tests/plots/test_plot_word_importance.py new file mode 100644 index 00000000..a48861a1 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_word_importance.py @@ -0,0 +1,36 @@ +"""Unit tests for the plot_word_importance function — pure, no explainer or model required.""" + +import unittest + +import pandas as pd +import plotly.graph_objs as go + +from shapash.plots.plot_word_importance import plot_word_importance + + +class TestPlotWordImportance(unittest.TestCase): + def setUp(self): + self.word_imp = pd.Series( + {"happy": 0.35, "terrible": -0.28, "wonderful": 0.20, "feel": -0.10}, + ) + + def test_returns_figure(self): + fig = plot_word_importance(self.word_imp) + self.assertIsInstance(fig, go.Figure) + + def test_has_one_bar_trace(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(len(fig.data), 1) + self.assertIsInstance(fig.data[0], go.Bar) + + def test_all_words_rendered(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(len(fig.data[0].x), len(self.word_imp)) + + def test_custom_title(self): + fig = plot_word_importance(self.word_imp, title="Joy importance") + self.assertIn("Joy importance", fig.layout.title.text) + + def test_orientation_horizontal(self): + fig = plot_word_importance(self.word_imp) + self.assertEqual(fig.data[0].orientation, "h") diff --git a/tests/unit_tests/plots/test_plot_word_profile.py b/tests/unit_tests/plots/test_plot_word_profile.py new file mode 100644 index 00000000..11c44106 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_word_profile.py @@ -0,0 +1,50 @@ +"""Unit tests for the plot_word_profile function — pure, no explainer or model required.""" + +import unittest + +import pandas as pd +import plotly.graph_objs as go + +from shapash.plots.plot_word_profile import plot_word_profile +from shapash.style.style_utils import DEFAULT_NLP_THEME + + +class TestPlotWordProfile(unittest.TestCase): + def setUp(self): + self.stats = pd.Series([0.2, -0.3], index=pd.Index([0, 1], name="class_idx")) + + def test_returns_figure_with_one_bar_per_class(self): + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + self.assertIsInstance(fig, go.Figure) + self.assertEqual(len(fig.data[0].x), 2) + + def test_class_zero_is_drawn_at_the_top(self): + # Plotly draws the y-axis bottom-to-top, so the first class must be last in the array. + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + self.assertEqual(fig.data[0].y[-1], "pos") + + def test_sign_drives_colour(self): + fig = plot_word_profile(self.stats, label_names=["pos", "neg"]) + # Reversed alongside the values, so the last colour belongs to the positive class 0. + self.assertEqual(fig.data[0].marker.color[-1], DEFAULT_NLP_THEME.xpl_positive) + self.assertEqual(fig.data[0].marker.color[0], DEFAULT_NLP_THEME.xpl_negative) + + def test_falls_back_to_class_indices_without_names(self): + fig = plot_word_profile(self.stats) + self.assertEqual(set(fig.data[0].y), {"0", "1"}) + + def test_spread_is_reversed_with_the_bars(self): + spread = pd.Series([0.1, 0.9], index=pd.Index([0, 1], name="class_idx")) + fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) + # Class 1's spread must sit on class 1's bar, which is drawn first. + self.assertAlmostEqual(fig.data[0].error_x.array[0], 0.9) + + def test_misaligned_spread_does_not_shift_error_bars(self): + # A spread indexed on classes the stats do not carry must not silently slide onto them. + spread = pd.Series([0.1], index=pd.Index([5], name="class_idx")) + fig = plot_word_profile(self.stats, label_names=["pos", "neg"], spread=spread) + self.assertEqual(list(fig.data[0].error_x.array), [0.0, 0.0]) + + def test_no_error_bars_without_spread(self): + fig = plot_word_profile(self.stats) + self.assertIsNone(fig.data[0].error_x.array) From 3bb0af850836191eece032f20c36f663f77cf02d Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 17 Sep 2026 15:45:39 +0200 Subject: [PATCH 120/144] refactor: move tests --- tests/unit_tests/backend/test_nlp_backend.py | 2 +- .../{nlp => explainer}/test_nlp_explanation.py | 0 .../unit_tests/{nlp => explainer}/test_nlp_plotter.py | 0 tests/unit_tests/webapp/test_nlp_app.py | 11 +++++------ 4 files changed, 6 insertions(+), 7 deletions(-) rename tests/unit_tests/{nlp => explainer}/test_nlp_explanation.py (100%) rename tests/unit_tests/{nlp => explainer}/test_nlp_plotter.py (100%) diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py index a684ec39..e0975bdc 100644 --- a/tests/unit_tests/backend/test_nlp_backend.py +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -1,6 +1,6 @@ """Unit tests for the shared NLP backend layer — the punctuation helper. -``NlpExplanation.word_importance`` filtering is covered in ``tests/unit_tests/nlp``; this file +``NlpExplanation.word_importance`` filtering is covered in ``tests/unit_tests/explainer``; this file covers :func:`~shapash.backend.nlp_backend.is_punctuation` itself, which word segmentation now makes load-bearing (punctuation is emitted as its own unit rather than glued to a neighbour). """ diff --git a/tests/unit_tests/nlp/test_nlp_explanation.py b/tests/unit_tests/explainer/test_nlp_explanation.py similarity index 100% rename from tests/unit_tests/nlp/test_nlp_explanation.py rename to tests/unit_tests/explainer/test_nlp_explanation.py diff --git a/tests/unit_tests/nlp/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py similarity index 100% rename from tests/unit_tests/nlp/test_nlp_plotter.py rename to tests/unit_tests/explainer/test_nlp_plotter.py diff --git a/tests/unit_tests/webapp/test_nlp_app.py b/tests/unit_tests/webapp/test_nlp_app.py index 72b35c47..d6b1416e 100644 --- a/tests/unit_tests/webapp/test_nlp_app.py +++ b/tests/unit_tests/webapp/test_nlp_app.py @@ -35,12 +35,11 @@ ) from tests.unit_tests.webapp.nlp_components._shared import LABEL_NAMES as _WHATIF_LABEL_NAMES -# The 6-class label set used by the TestNlpWebApp / TestNlpWebAppRun fixtures below (moved here -# from tests/unit_tests/nlp/test_nlp_prototype.py). This intentionally shadows, for this module -# only, the 2-class ``LABEL_NAMES`` used by the What-if Lab classes further down (imported above -# as ``_WHATIF_LABEL_NAMES``) — the two source files each defined their own module-level -# ``LABEL_NAMES`` with different values, which collide once both land in one file. See the -# migration report for details. +# The 6-class label set used by the TestNlpWebApp / TestNlpWebAppRun fixtures below. This +# intentionally shadows, for this module only, the 2-class ``LABEL_NAMES`` used by the What-if Lab +# classes further down (imported above as ``_WHATIF_LABEL_NAMES``) — the two source files each +# defined their own module-level ``LABEL_NAMES`` with different values, which collide once both land +# in one file. See the migration report for details. LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] N_CLASSES = len(LABEL_NAMES) From 00ebfcb79874ef27d1dc3cdca2aff91ca0d1292d Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Fri, 18 Sep 2026 14:34:29 +0200 Subject: [PATCH 121/144] refactor: change confusion matrix color palette --- shapash/plots/plot_confusion_matrix.py | 127 ++++++++++++++---- shapash/style/style_utils.py | 4 + .../webapp/nlp_components/error_analysis.py | 11 +- .../plots/test_plot_confusion_matrix.py | 119 ++++++++++++++-- 4 files changed, 223 insertions(+), 38 deletions(-) diff --git a/shapash/plots/plot_confusion_matrix.py b/shapash/plots/plot_confusion_matrix.py index 8988acf4..0b8567e9 100644 --- a/shapash/plots/plot_confusion_matrix.py +++ b/shapash/plots/plot_confusion_matrix.py @@ -2,15 +2,37 @@ from __future__ import annotations +from collections.abc import Sequence + import numpy as np from plotly import graph_objs as go -# Plotly's own continuous "Blues" scale, independent of the app's active NLP palette: the -# discrete, hand-picked stops in colors.json's "blues"/"default" palettes band visibly on a -# heatmap, and under the yellow "default" palette low off-diagonal counts (the errors this -# matrix exists to surface) rendered as near-invisible pale yellow. Plotly's built-in scale reads -# cleanly regardless of which palette the rest of the app is themed with. -_DEFAULT_COLORSCALE = "Blues" +from shapash.style.style_utils import DEFAULT_NLP_THEME + +# Faint neutral ramp for the diagonal. The correct predictions are context here, not the subject: +# they keep a readable count but stay out of the colour competition, so the whole chromatic range +# is free for the off-diagonal cells this matrix exists to surface (and to have clicked). +_DIAGONAL_COLORSCALE = [[0.0, "rgb(244, 245, 246)"], [1.0, "rgb(214, 217, 220)"]] + +# How hard to ease the pale end of the error colourscale (see _emphasis_colorscale). 2.0 puts a +# cell at ~30% of the worst confusion pair already in the orange band. +_EMPHASIS_GAMMA = 2.0 + + +def _emphasis_colorscale(colors: Sequence[str], gamma: float = _EMPHASIS_GAMMA) -> list[list]: + """Place *colors* on a ``pos = (i / (n - 1)) ** gamma`` curve instead of evenly. + + Error counts are heavy-tailed — one confusion pair usually dwarfs the rest — so an evenly + spaced scale spends most of its range on values that never occur and renders every small + cell in the palest, near-invisible stop. Squeezing the pale stops towards zero means a cell + holding a handful of errors is already visibly coloured, while the worst pair still reads as + the darkest. + """ + stops = list(colors) + if len(stops) == 1: + return [[0.0, stops[0]], [1.0, stops[0]]] + last = len(stops) - 1 + return [[(i / last) ** gamma, color] for i, color in enumerate(stops)] def plot_confusion_matrix( @@ -20,7 +42,8 @@ def plot_confusion_matrix( title: str = "Confusion matrix", width: int | None = None, height: int | None = None, - colorscale: list[str] | str = _DEFAULT_COLORSCALE, + colorscale: Sequence[str] | str | None = None, + emphasize_off_diagonal: bool = True, ) -> go.Figure: """Heatmap of a confusion matrix with click-identifiable cells. @@ -46,10 +69,18 @@ def plot_confusion_matrix( Figure width in pixels. Defaults to a size scaled to the number of classes. height : int, optional Figure height in pixels. Defaults to a size scaled to the number of classes. - colorscale : list of str or str - Plotly heatmap colorscale. Defaults to Plotly's built-in ``"Blues"``, independent of the - app's active NLP palette — the errors this matrix exists to surface need to stay readable - whichever palette the rest of the app is themed with. + colorscale : sequence of str or str, optional + Plotly heatmap colorscale for the error cells. Defaults to the active palette's + ``confusion_matrix_colorscale`` (yellow → orange → red under the default palette), + the same family the Feature Contribution plot draws from. + emphasize_off_diagonal : bool, optional + Colour the errors on their own scale (default). The diagonal normally holds most of the + mass, so on a shared scale it takes the whole colour range and flattens every error cell + to the palest shade — the opposite of what this matrix is read for. With this on, the + diagonal is drawn in faint grey and the off-diagonal cells are scaled to the *worst + confusion pair*, so darker always means "more errors, more worth clicking". The counts + themselves are unchanged; only the colour mapping is. Set ``False`` for a classic + single-scale matrix where the diagonal is coloured too. Returns ------- @@ -57,6 +88,8 @@ def plot_confusion_matrix( """ counts = np.asarray(cm, dtype=float) n = counts.shape[0] + if colorscale is None: + colorscale = DEFAULT_NLP_THEME.confusion_scale if normalize == "true": row_sums = counts.sum(axis=1, keepdims=True) @@ -65,11 +98,13 @@ def plot_confusion_matrix( text = np.array([[f"{v:.0%}" for v in row] for row in z]) hover_val = "%{z:.1%}" colorbar_title = "recall" + colorbar_tickformat = ".0%" # match the cell text, which is a percentage too else: z = counts text = np.array([[f"{int(v)}" for v in row] for row in counts]) hover_val = "%{z:.0f}" colorbar_title = "count" + colorbar_tickformat = None # customdata[true][pred] = [pred_idx, true_idx] — the order a click handler reads. customdata = np.empty((n, n, 2), dtype=int) @@ -77,20 +112,65 @@ def plot_confusion_matrix( for j in range(n): customdata[i, j] = (j, i) - fig = go.Figure( - go.Heatmap( - z=z, - x=labels, - y=labels, - customdata=customdata, - text=text, - texttemplate="%{text}", - colorscale=colorscale, - colorbar=dict(title=colorbar_title), - hovertemplate=("true: %{y}<br>predicted: %{x}<br>" + hover_val + "<extra></extra>"), - ) + hovertemplate = "true: %{y}<br>predicted: %{x}<br>" + hover_val + "<extra></extra>" + common = dict( + x=labels, + y=labels, + customdata=customdata, + texttemplate="%{text}", + hovertemplate=hovertemplate, + # Hairline gaps turn the matrix into distinct tiles, which reads as "clickable". + xgap=2, + ygap=2, ) + fig = go.Figure() + if emphasize_off_diagonal and n > 1: + diagonal_mask = np.eye(n, dtype=bool) + # NaN renders transparent, so the two traces interleave into one matrix with no overlap. + diag_z = np.where(diagonal_mask, z, np.nan) + off_z = np.where(diagonal_mask, np.nan, z) + off_max = np.nanmax(off_z) + if not np.isfinite(off_max) or off_max <= 0: + off_max = 1.0 # a matrix with no errors at all: avoid a degenerate zmin == zmax scale + fig.add_trace( + go.Heatmap( + z=diag_z, + text=np.where(diagonal_mask, text, ""), + name="correct", + colorscale=_DIAGONAL_COLORSCALE, + showscale=False, + textfont=dict(color="rgb(110, 113, 116)"), + hoverongaps=False, + **common, + ) + ) + fig.add_trace( + go.Heatmap( + z=off_z, + text=np.where(diagonal_mask, "", text), + name="errors", + colorscale=_emphasis_colorscale(colorscale) if not isinstance(colorscale, str) else colorscale, + zmin=0, + zmax=off_max, + colorbar=dict(title=f"errors<br>({colorbar_title})", tickformat=colorbar_tickformat), + textfont=dict(color="rgb(50, 50, 50)"), + hoverongaps=False, + **common, + ) + ) + else: + fig.add_trace( + go.Heatmap( + z=z, + text=text, + name="cells", + colorscale=list(colorscale) if not isinstance(colorscale, str) else colorscale, + colorbar=dict(title=colorbar_title, tickformat=colorbar_tickformat), + **common, + ) + ) + size = max(360, 90 * n + 120) # The x-axis sits on top, so keep room for its labels even without a title. With a title, # pin it above that axis title (rather than at Plotly's default position, which drifts down @@ -103,6 +183,7 @@ def plot_confusion_matrix( width=width or size, height=height or size, plot_bgcolor="white", + showlegend=False, # Put the first true class at the top so the diagonal reads top-left to bottom-right. yaxis=dict(autorange="reversed", automargin=True), xaxis=dict(automargin=True, side="top"), diff --git a/shapash/style/style_utils.py b/shapash/style/style_utils.py index de163b1b..81979beb 100644 --- a/shapash/style/style_utils.py +++ b/shapash/style/style_utils.py @@ -197,6 +197,7 @@ class NlpTheme: header_accent: str xpl_positive: str xpl_negative: str + confusion_scale: tuple[str, ...] def resolve_nlp_theme(palette_name: str = "default", colors_dict: dict[str, str] | None = None) -> NlpTheme: @@ -226,6 +227,9 @@ def resolve_nlp_theme(palette_name: str = "default", colors_dict: dict[str, str] header_accent=palette["webapp_title"], xpl_positive=palette["nlp_xpl_positive"], xpl_negative=palette["nlp_xpl_negative"], + # A tuple, not the palette's list: NlpTheme is a frozen dataclass, so a mutable field + # would make it unhashable. + confusion_scale=tuple(palette["confusion_matrix_colorscale"]), ) diff --git a/shapash/webapp/nlp_components/error_analysis.py b/shapash/webapp/nlp_components/error_analysis.py index 7abfe2fc..cba3b053 100644 --- a/shapash/webapp/nlp_components/error_analysis.py +++ b/shapash/webapp/nlp_components/error_analysis.py @@ -111,7 +111,14 @@ def layout(self, ctx) -> html.Div: ), dcc.Graph( id="confusion-matrix-graph", - figure=plot_confusion_matrix(self._cm, label_names, title="", width=None, height=None), + figure=plot_confusion_matrix( + self._cm, + label_names, + title="", + width=None, + height=None, + colorscale=self._theme.confusion_scale, + ), config={"displayModeBar": False, "responsive": True}, style={"height": "360px", "flex": "0 0 auto"}, ), @@ -159,6 +166,7 @@ def register_callbacks(self, app, ctx, stores) -> None: assert cm is not None and pred_idx_arr is not None and true_idx_arr is not None # noqa: S101 - layout() runs first label_names = explanation.label_names or [str(i) for i in range(cm.shape[0])] name_to_idx = {name: i for i, name in enumerate(label_names)} + confusion_scale = self._theme.confusion_scale @app.callback( Output("confusion-matrix-graph", "figure"), @@ -170,6 +178,7 @@ def update_confusion_matrix(normalize): label_names, normalize="true" if normalize == "recall" else None, title="", # the tab header already labels this panel + colorscale=confusion_scale, ) # Let the container height drive size (this panel is only half-column tall). fig.layout.width = None diff --git a/tests/unit_tests/plots/test_plot_confusion_matrix.py b/tests/unit_tests/plots/test_plot_confusion_matrix.py index 319ac199..beadd1a9 100644 --- a/tests/unit_tests/plots/test_plot_confusion_matrix.py +++ b/tests/unit_tests/plots/test_plot_confusion_matrix.py @@ -5,7 +5,12 @@ import numpy as np import plotly.graph_objs as go -from shapash.plots.plot_confusion_matrix import plot_confusion_matrix +from shapash.plots.plot_confusion_matrix import _emphasis_colorscale, plot_confusion_matrix + + +def _trace(fig, name): + """The one trace called *name* — traces are named rather than positional on purpose.""" + return next(t for t in fig.data if t.name == name) class TestPlotConfusionMatrix(unittest.TestCase): @@ -17,35 +22,121 @@ def setUp(self): def test_returns_heatmap_figure(self): fig = plot_confusion_matrix(self.cm, self.labels) self.assertIsInstance(fig, go.Figure) - self.assertIsInstance(fig.data[0], go.Heatmap) + self.assertTrue(all(isinstance(t, go.Heatmap) for t in fig.data)) def test_axes_are_labelled_true_and_predicted(self): fig = plot_confusion_matrix(self.cm, self.labels) - self.assertEqual(list(fig.data[0].x), self.labels) - self.assertEqual(list(fig.data[0].y), self.labels) + for trace in fig.data: + self.assertEqual(list(trace.x), self.labels) + self.assertEqual(list(trace.y), self.labels) def test_customdata_encodes_pred_then_true(self): - # customdata[true][pred] must be [pred_idx, true_idx] for the click handler. + # customdata[true][pred] must be [pred_idx, true_idx] for the click handler — on *every* + # trace, since a click can land on either the diagonal or an error cell. fig = plot_confusion_matrix(self.cm, self.labels) - cd = np.asarray(fig.data[0].customdata) - self.assertEqual(list(cd[0, 1]), [1, 0]) # true=0, pred=1 - self.assertEqual(list(cd[1, 2]), [2, 1]) # true=1, pred=2 + for trace in fig.data: + cd = np.asarray(trace.customdata) + self.assertEqual(list(cd[0, 1]), [1, 0]) # true=0, pred=1 + self.assertEqual(list(cd[1, 2]), [2, 1]) # true=1, pred=2 def test_counts_shown_as_text(self): fig = plot_confusion_matrix(self.cm, self.labels) - self.assertEqual(fig.data[0].text[0][0], "5") + self.assertEqual(_trace(fig, "correct").text[0][0], "5") # diagonal + self.assertEqual(_trace(fig, "errors").text[0][1], "2") # off-diagonal + # Each cell's number is written by exactly one trace, never both. + self.assertEqual(_trace(fig, "errors").text[0][0], "") + self.assertEqual(_trace(fig, "correct").text[0][1], "") def test_normalize_true_is_row_recall(self): fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") - z = np.asarray(fig.data[0].z) - np.testing.assert_allclose(z[0], [5 / 7, 2 / 7, 0.0]) + diag, errors = _trace(fig, "correct"), _trace(fig, "errors") + self.assertAlmostEqual(np.asarray(diag.z)[0, 0], 5 / 7) + np.testing.assert_allclose(np.asarray(errors.z)[0, 1:], [2 / 7, 0.0]) def test_normalize_true_handles_empty_row_without_nan(self): + # Row 2 has no samples: its cells must be 0, not NaN-from-division. Only the diagonal + # masking may introduce NaNs, so check each trace on the half it owns. fig = plot_confusion_matrix(self.cm, self.labels, normalize="true") - z = np.asarray(fig.data[0].z) - self.assertFalse(np.isnan(z).any()) - np.testing.assert_array_equal(z[2], [0.0, 0.0, 0.0]) + np.testing.assert_array_equal(np.asarray(_trace(fig, "errors").z)[2, :2], [0.0, 0.0]) + self.assertEqual(np.asarray(_trace(fig, "correct").z)[2, 2], 0.0) def test_custom_title(self): fig = plot_confusion_matrix(self.cm, self.labels, title="Errors") self.assertIn("Errors", fig.layout.title.text) + + +class TestOffDiagonalEmphasis(unittest.TestCase): + """The errors get their own colour range so the diagonal cannot flatten them out.""" + + def setUp(self): + # A realistic shape: the diagonal dwarfs every error cell. + self.cm = np.array([[500, 3, 0], [12, 480, 1], [0, 2, 450]]) + self.labels = ["A", "B", "C"] + + def test_diagonal_and_errors_are_separate_traces(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertEqual([t.name for t in fig.data], ["correct", "errors"]) + + def test_diagonal_is_masked_out_of_the_error_trace(self): + z = np.asarray(_trace(plot_confusion_matrix(self.cm, self.labels), "errors").z) + self.assertTrue(np.isnan(np.diagonal(z)).all()) + np.testing.assert_array_equal(z[0, 1:], [3.0, 0.0]) + + def test_error_scale_tops_out_at_the_worst_confusion_pair(self): + # zmax = 12 (the B→A cell), not 500 — that is what keeps small errors visible. + errors = _trace(plot_confusion_matrix(self.cm, self.labels), "errors") + self.assertEqual(errors.zmin, 0) + self.assertEqual(errors.zmax, 12.0) + + def test_only_the_error_trace_carries_the_colorbar(self): + fig = plot_confusion_matrix(self.cm, self.labels) + self.assertFalse(_trace(fig, "correct").showscale) + self.assertIn("errors", _trace(fig, "errors").colorbar.title.text) + + def test_error_colorscale_defaults_to_the_palette_yellow_orange(self): + errors = _trace(plot_confusion_matrix(self.cm, self.labels), "errors") + stops = [color for _, color in errors.colorscale] + self.assertEqual(stops[0], "rgb(255, 255, 220)") # palest yellow + self.assertEqual(stops[-1], "rgb(255, 77, 7)") # deepest orange-red + + def test_error_colorscale_eases_the_pale_end(self): + # Positions must be monotonic, span [0, 1], and be squeezed below the even spacing so a + # small error cell already lands past the near-white stops. + errors = _trace(plot_confusion_matrix(self.cm, self.labels), "errors") + positions = [pos for pos, _ in errors.colorscale] + self.assertEqual((positions[0], positions[-1]), (0.0, 1.0)) + self.assertTrue(all(b > a for a, b in zip(positions, positions[1:]))) + midpoint = positions[len(positions) // 2] + self.assertLess(midpoint, 0.5) + + def test_custom_colorscale_is_honoured(self): + fig = plot_confusion_matrix(self.cm, self.labels, colorscale=["rgb(0,0,0)", "rgb(255,255,255)"]) + self.assertEqual([color for _, color in _trace(fig, "errors").colorscale], ["rgb(0,0,0)", "rgb(255,255,255)"]) + + def test_named_plotly_colorscale_is_passed_through(self): + fig = plot_confusion_matrix(self.cm, self.labels, colorscale="Blues") + self.assertTrue(len(_trace(fig, "errors").colorscale) > 1) + + def test_matrix_without_errors_keeps_a_valid_scale(self): + perfect = np.diag([10, 20, 30]) + errors = _trace(plot_confusion_matrix(perfect, self.labels), "errors") + self.assertGreater(errors.zmax, errors.zmin) + + def test_single_class_matrix_falls_back_to_one_trace(self): + fig = plot_confusion_matrix(np.array([[7]]), ["A"]) + self.assertEqual(len(fig.data), 1) + self.assertEqual(np.asarray(fig.data[0].z)[0, 0], 7.0) + + def test_emphasis_can_be_switched_off_for_a_classic_matrix(self): + fig = plot_confusion_matrix(self.cm, self.labels, emphasize_off_diagonal=False) + self.assertEqual(len(fig.data), 1) + z = np.asarray(fig.data[0].z) + self.assertFalse(np.isnan(z).any()) + self.assertEqual(fig.data[0].text[0][0], "500") + self.assertIn("count", fig.data[0].colorbar.title.text) + + +class TestEmphasisColorscale(unittest.TestCase): + def test_single_colour_scale_is_expanded_to_a_valid_two_stop_scale(self): + # Plotly rejects a one-stop colorscale, so a degenerate palette must still come back usable. + self.assertEqual(_emphasis_colorscale(["rgb(1,2,3)"]), [[0.0, "rgb(1,2,3)"], [1.0, "rgb(1,2,3)"]]) From a929c19bbfd7f3b4af06a69248133ca325d80481 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 21 Sep 2026 10:19:30 +0200 Subject: [PATCH 122/144] docs(nlp): strip refactoring-plan references from docstrings --- shapash/backend/nlp_backend.py | 9 ++++----- shapash/backend/nlp_captum_lig_backend.py | 4 ++-- shapash/backend/nlp_shap_backend.py | 9 ++++----- shapash/compute/__init__.py | 11 +++++------ shapash/compute/diagnostics/label_probe.py | 2 +- shapash/explainer/interactive.py | 8 ++++---- shapash/explainer/nlp_explainer.py | 14 +++----------- shapash/style/style_utils.py | 3 +-- shapash/webapp/nlp_app.py | 10 +++++----- shapash/webapp/nlp_components/__init__.py | 14 ++++++-------- shapash/webapp/nlp_components/label_noise.py | 2 +- 11 files changed, 36 insertions(+), 50 deletions(-) diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index a63db8f8..6ffded06 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -94,11 +94,10 @@ class NlpBackend(Backend): Which model output the contributions explain — read off the backend at ``explain()`` time and recorded on :class:`~shapash.explainer.nlp_explanation.NlpExplanation` so a saved artifact says which. ``"probability"``: the explained quantity is a - (softmax) probability, which forces per-token cross-class cancellation — see - ``docs/architecture/explanation-space.md``. ``"logit"``: the explained quantity is - the model's raw pre-softmax output, which does not cancel. Not an affine rescaling - of one another; a caller comparing two backends is comparing numbers on different - scales unless both report the same space. + (softmax) probability, which forces per-token cross-class cancellation. ``"logit"``: the + explained quantity is the model's raw pre-softmax output, which does not cancel. Not an + affine rescaling of one another; a caller comparing two backends is comparing numbers on + different scales unless both report the same space. requires_model_capabilities : tuple[type, ...] Capability ABCs (from :mod:`shapash.model.base`) the bound model must satisfy, checked once here so a new backend declares its needs instead of failing at diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index 214d2b0e..cb938c33 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -16,7 +16,7 @@ so the additive ``base + Σ = total`` summary in ``plot_sentence_highlight`` stays consistent. These values live in raw logit space — **not** the same space ``NlpShapBackend`` reports: SHAP explains the pipeline's softmax probability output, not ``model.logits``, so the two backends' numbers are on -different scales and are not directly comparable (see ``docs/architecture/explanation-space.md``). +different scales and are not directly comparable. Unlike the SHAP/LIME backends (which wrap a plain text callable), this backend needs the embedding module and a logits forward pass, so it consumes a :class:`~shapash.model.base.TextModel` that @@ -208,7 +208,7 @@ class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` default name = "nlp_captum_lig" # A constructed point from the tokenizer (model.reference_ids() -> pad/mask ids), - # not learned from data — see A9 in docs/architecture/refactoring-plan.md. + # not learned from data. reference_kind = "point" # Integrated Gradients satisfies the completeness axiom (Sundararajan, Taly & Yan, # 2017, "Axiomatic Attribution for Deep Networks"): attributions sum exactly to diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index f4121560..0600ff4e 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -207,13 +207,12 @@ class NlpShapBackend(NlpBackend): # nothing backend-specific to learn here. reference_kind = "none" # Shapley values satisfy the efficiency axiom by construction, and stay additive - # even under the Partition/Owen path SHAP silently takes for text (see A8 in - # docs/architecture/refactoring-plan.md: Owen values satisfy efficiency too). + # even under the Partition/Owen path SHAP silently takes for text. Owen values satisfy efficiency too. is_additive = True # ``shap_callable`` resolves to a ``text-classification`` pipeline (softmax output), not - # ``model.logits`` — see docs/architecture/explanation-space.md §1/§2. This is why per-token - # attributions cancel across classes: the explained quantity sums to 1 for every masked - # variant, which is a constant-payoff game whose Shapley values are all zero. + # ``model.logits``. This is why per-token attributions cancel across classes: the explained + # quantity sums to 1 for every masked variant, which is a constant-payoff game whose Shapley + # values are all zero. output_space = "probability" requires_model_capabilities = () # a plain scoring callable is enough diff --git a/shapash/compute/__init__.py b/shapash/compute/__init__.py index a7b6e0b0..32d1f405 100644 --- a/shapash/compute/__init__.py +++ b/shapash/compute/__init__.py @@ -1,9 +1,8 @@ """Compute layer — heavy computation behind modality-agnostic interfaces. -Seeded by the NLP prototype ahead of the master refactoring. Today it holds the ``generators`` -sub-package (counterfactual / what-if generation), the ``retrieval`` sub-package (similar-example -lookup), and :mod:`~shapash.compute.embedding_store` — the shared cache both retrieval and the 2-D -scatter draw their vectors from. The master plan's ``analyses/{local,global,diagnostics}`` -interpreters will move here in later phases; ``generators`` is a *new* sibling axis for generative -components (LIT's ``Generator``, as opposed to explanation ``Interpreter``s). +Seeded by the NLP prototype. Today it holds the ``generators`` sub-package (counterfactual / what-if +generation), the ``retrieval`` sub-package (similar-example lookup), and +:mod:`~shapash.compute.embedding_store` — the shared cache both retrieval and the 2-D scatter draw +their vectors from. ``generators`` is a *new* sibling axis for generative components (LIT's +``Generator``, as opposed to explanation ``Interpreter``s). """ diff --git a/shapash/compute/diagnostics/label_probe.py b/shapash/compute/diagnostics/label_probe.py index 996ee0c7..cc18c9f3 100644 --- a/shapash/compute/diagnostics/label_probe.py +++ b/shapash/compute/diagnostics/label_probe.py @@ -15,7 +15,7 @@ Why a separate classifier rather than nearest neighbours in the model's representation space: that space is the one feeding the classifier head, so its neighbours restate the prediction (measured at 98.6% agreement on the emotion demo) and back the *wrong* prediction on 87% of the model's errors. -Anything derived from the audited model corroborates it by construction. See implementation-log C.27. +Anything derived from the audited model corroborates it by construction. Why bag-of-words rather than sentence embeddings: measured on the emotion demo, TF-IDF + logistic regression scores 0.658 accuracy and agrees with the audited model 66.6% of the time, against 0.636 diff --git a/shapash/explainer/interactive.py b/shapash/explainer/interactive.py index 15524bf9..c27ecca2 100644 --- a/shapash/explainer/interactive.py +++ b/shapash/explainer/interactive.py @@ -1,9 +1,9 @@ """Interactive (live) compute contract for what-if tooling. -The master refactoring's ``ExplainerView`` is a **read-only** DTO over a *compiled* result — it -deliberately cannot run the model on new inputs. Interactive what-if (re-predicting text the user -edits, generating counterfactuals on the fly) needs the opposite: live access to the model, -explanation backend and generator. +A compiled explanation result is a **read-only** view over past output — it deliberately cannot run +the model on new inputs. Interactive what-if (re-predicting text the user edits, generating +counterfactuals on the fly) needs the opposite: live access to the model, explanation backend and +generator. ``InteractiveEngine`` is that live counterpart, kept separate from the read-only view (mirroring LIT keeping ``Model`` separate from data). Webapp components that mutate/regenerate inputs *require* an diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index b1b37491..015dc5b7 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -1,13 +1,6 @@ """Prototype NLP explainer for text classification models. -This module is a **bridge prototype** toward Phase 6 of the refactoring plan, -where token-level NLP explanations will be integrated into ``SmartExplainer`` -via ``TextDataset`` and ``ExplanationSession``. Once those are in place, users -will call ``SmartExplainer.compile(x=TextDataset(texts))`` and this class will -be removed. - -``NlpExplainer`` follows the ``fit``/``explain`` seam (Amendment A5 in -``docs/architecture/refactoring-plan.md``): ``fit(X_reference, y=None)`` learns +``NlpExplainer`` follows a ``fit``/``explain`` seam: ``fit(X_reference, y=None)`` learns reference state (the similar-example / label-noise-probe corpus) and ``explain(X, y=None)`` runs the backend and returns an immutable :class:`~shapash.explainer.nlp_explanation.NlpExplanation` — the explainer @@ -216,8 +209,7 @@ def fit( of their own — see ``NlpBackend.reference_kind`` on each (``"none"``/``"point"``: a masker that infers itself, or a point constructed by the tokenizer, neither learned from data). So calling ``fit`` is optional; :meth:`explain` works standalone, exactly as ``compile`` did. - What *is* fit-time reference state for text — the two pieces the plan calls out (see the - refactoring plan's Context 4a) — is the corpus this method learns: + What *is* fit-time reference state for text is the corpus this method learns: Parameters ---------- @@ -258,7 +250,7 @@ def fit( trailing-underscore convention for fitted state. Does not subclass ``BaseEstimator``: text models here are HuggingFace pipelines / torch models, not sklearn estimators, so there is no ``Pipeline``/skrub interop need to justify full conformance (``get_params``/``clone``) the - way there is for the tabular explainer — see the refactoring plan's Phase 1. + way there is for the tabular explainer. """ ref_texts = list(X_reference) if X_reference is not None else None ref_labels = list(y) if y is not None else None diff --git a/shapash/style/style_utils.py b/shapash/style/style_utils.py index 81979beb..229adeac 100644 --- a/shapash/style/style_utils.py +++ b/shapash/style/style_utils.py @@ -189,8 +189,7 @@ class NlpTheme: ``xpl_positive``/``xpl_negative`` name the concept "explanation pushes the prediction up/down" rather than any one backend's algorithm (``nlp_shap`` and ``nlp_captum_lig`` both - produce signed per-token contributions, drawn with these two colors) — see - ``docs/architecture/explanation-space.md``. + produce signed per-token contributions, drawn with these two colors). """ header_bkg: str diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index d6a9a666..b43d1630 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -1,10 +1,10 @@ """Minimal Dash webapp for NLP text classification explanations. -Prototype bridge toward Phase 5b (composable WebappComponents). Panels are being extracted into -``WebappComponent``s one at a time (see ``shapash/webapp/nlp_components/``); the dataset table and -the persistent selection bar are the only pieces still built inline — both are mandatory shell UI, -not optional panels, so there is nothing left to extract them into. All tabular-only SmartApp panels -(violin, cluster, scatter prediction picking) beyond what NLP needs are absent rather than disabled. +Panels are being extracted into ``WebappComponent``s one at a time (see +``shapash/webapp/nlp_components/``); the dataset table and the persistent selection bar are the only +pieces still built inline — both are mandatory shell UI, not optional panels, so there is nothing +left to extract them into. All tabular-only SmartApp panels (violin, cluster, scatter prediction +picking) beyond what NLP needs are absent rather than disabled. """ from __future__ import annotations diff --git a/shapash/webapp/nlp_components/__init__.py b/shapash/webapp/nlp_components/__init__.py index d6839446..55cf1319 100644 --- a/shapash/webapp/nlp_components/__init__.py +++ b/shapash/webapp/nlp_components/__init__.py @@ -1,14 +1,12 @@ """Composable webapp components for the NLP what-if tools. -Prototype of the master plan's Phase-5b ``WebappComponent`` contract, applied first to the -interactive what-if panels (data editor + counterfactuals). Each component owns its layout and -callbacks and declares its ``requires`` capabilities, self-disabling when the bound explanation/engine -cannot satisfy them. +A ``WebappComponent`` contract, applied first to the interactive what-if panels (data editor + +counterfactuals). Each component owns its layout and callbacks and declares its ``requires`` +capabilities, self-disabling when the bound explanation/engine cannot satisfy them. -Notably this **extends** the Phase-5b ``requires`` idea from *data* capabilities (e.g. -``proba_values``) to *engine/model* capabilities (``engine:predict``, ``engine:counterfactual``), -so a panel can require a live, gradient-capable model — the refinement to fold back into the master -contract. +Notably this **extends** the ``requires`` idea from *data* capabilities (e.g. ``proba_values``) to +*engine/model* capabilities (``engine:predict``, ``engine:counterfactual``), so a panel can require a +live, gradient-capable model. """ from __future__ import annotations diff --git a/shapash/webapp/nlp_components/label_noise.py b/shapash/webapp/nlp_components/label_noise.py index b625461a..2e6fa5e3 100644 --- a/shapash/webapp/nlp_components/label_noise.py +++ b/shapash/webapp/nlp_components/label_noise.py @@ -22,7 +22,7 @@ ranks in the model's decision space, so those neighbours restated the prediction 98.6% of the time and backed the *wrong* prediction on 87% of model errors — it read as confirmation while carrying no information. Representation neighbours are still one click away and honestly framed in the Similar -Examples panel, which follows the current datapoint. See implementation-log C.27. +Examples panel, which follows the current datapoint. *Inspect* makes a flagged row the current datapoint so its token contributions render in the shared Sentence/Waterfall panels — the words that pushed the model away from the given label. Unlike the From d6d7b212fe0f2fbf2895abf2cec77680ec4b7856 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 21 Sep 2026 13:45:01 +0200 Subject: [PATCH 123/144] test(nlp): switch to tiny model in test torch --- tests/integration_tests/test_torch_st_models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py index 5061c993..505dbf3d 100644 --- a/tests/integration_tests/test_torch_st_models.py +++ b/tests/integration_tests/test_torch_st_models.py @@ -34,7 +34,11 @@ ) ST_MODEL = "sentence-transformers/all-MiniLM-L6-v2" -HF_BODY = "bhadresh-savani/distilbert-base-uncased-emotion" +# Tiny random-weight DistilBert: only shapes/capabilities are asserted below (the head is random +# too), so there's no need for the ~250MB trained checkpoint. ST_MODEL stays full-size because +# several tests below pin behaviour specific to its actual config (mean pooling, normalize=True, +# the 512 vs 256 tokenizer/max_seq_length mismatch). +HF_BODY = "hf-internal-testing/tiny-random-DistilBertModel" LABELS = ["neg", "pos", "neutral"] TEXTS = ["i am so happy today", "this is terrible and sad", "an ordinary grey afternoon"] @@ -53,7 +57,7 @@ def st_model(): @pytest.fixture(scope="module") def torch_model(): - """A TorchClassifierModel: a raw HF encoder body + a random 3-class head + fast tokenizer.""" + """A TorchClassifierModel: a tiny random-weight HF encoder body + a random 3-class head + fast tokenizer.""" try: tokenizer = transformers.AutoTokenizer.from_pretrained(HF_BODY, use_fast=True) body = transformers.AutoModel.from_pretrained(HF_BODY) From 7237f9b716aa336ac124a5ac3d039006f055028c Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 21 Sep 2026 16:22:49 +0200 Subject: [PATCH 124/144] chore: make NLP an opt-in extra across packaging, tests and CI Packaging: - Contributor tooling moves from extras to PEP 735 dependency groups: dev, test, doc and dev-all (which includes the nlp extra). The `all` extra is removed. - `uv sync` installs dev + test by default. NLP work uses `uv sync --extra nlp`. - dash-ag-grid moves from the nlp extra to core dependencies. - pyarrow is restored as a core dependency. It had only been pulled in by `datasets`. - tox uses the `test` dependency group. Tests: - The NLP test modules skip cleanly when torch or transformers is missing (pytest.importorskip). - They carry a new `nlp` marker. CI: - The test matrix (3.11-3.14) installs CPU-only torch with the nlp extra and runs the full suite. - Only 3.12 measures coverage, uploads it to Codecov and builds the package. - Hugging Face models are cached between runs. - pandas_compat runs without NLP and doubles as the classical-install check. It fails if torch is present. Docs: CONTRIBUTING and the NLP wiki page describe the new install commands. --- .github/workflows/main.yml | 54 ++++++++++++++----- CONTRIBUTING.md | 16 ++++-- docs/wiki/NLP-Explainability.md | 4 +- pyproject.toml | 29 +++++++--- .../integration_tests/test_captum_backends.py | 1 + tests/integration_tests/test_nlp_whatif.py | 1 + .../integration_tests/test_torch_st_models.py | 1 + tests/unit_tests/model/test_hf.py | 9 ++-- .../model/test_hf_from_pretrained.py | 11 ++-- tests/unit_tests/model/test_torch_models.py | 13 +++-- 10 files changed, 102 insertions(+), 37 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 51a0f714..59067790 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: - name: Install dev dependencies run: | python -m pip install --upgrade pip - python -m pip install '.[dev]' + python -m pip install . --group dev - name: Check linting run: make lint - name: Check formatting @@ -41,7 +41,7 @@ jobs: run: make typecheck test: - name: Build & Test + name: Test (Python ${{ matrix.python-version }}) needs: code_quality runs-on: ubuntu-latest timeout-minutes: 60 @@ -49,6 +49,10 @@ jobs: fail-fast: false matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] + include: + # One job measures coverage and uploads it: the others run the same suite. + - python-version: "3.12" + coverage: true steps: - uses: actions/checkout@v6 with: @@ -59,24 +63,45 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: pyproject.toml + # Full suite, NLP included. CPU-only torch first: the PyPI Linux wheel drags in several GB of + # CUDA libraries the tests never use. The `nlp` extra's `torch>=` bound is then already + # satisfied and pip keeps it. - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install '.[all]' - - name: Build python package - run: | - python -m pip install --upgrade build - python -m build + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + python -m pip install '.[nlp]' --group test + # NLP test modules skip (not fail) when their imports are missing, so fail loudly here instead. + - name: Check NLP stack is importable + run: python -c "import torch, transformers, sentence_transformers, captum; print(torch.__version__)" + # Models the NLP tests download from the Hugging Face Hub: reuse them instead of hitting the Hub + # from every job. The key follows the tests, so a newly referenced model gets saved too. + - name: Cache Hugging Face models + uses: actions/cache@v6 + with: + path: ~/.cache/huggingface + key: hf-models-${{ hashFiles('tests/**/*.py') }} + restore-keys: hf-models- + - name: Test + if: ${{ !matrix.coverage }} + run: make test - name: Test and coverage + if: ${{ matrix.coverage }} run: make coverage-report - name: Upload coverage to Codecov - if: matrix.python-version == '3.13' + if: ${{ matrix.coverage }} uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml fail_ci_if_error: false disable_search: true + # Pure-Python package: building it once is enough. + - name: Build python package + if: ${{ matrix.coverage }} + run: | + python -m pip install --upgrade build + python -m build pandas_compat: name: Pandas Compat (${{ matrix.pandas }}) @@ -86,7 +111,8 @@ jobs: strategy: fail-fast: false matrix: - pandas: ["2.2.2", "2.3.*", "3.*"] + # Lower bound + last 2.x. Latest pandas 3.x is already what the `test` jobs resolve to. + pandas: ["2.2.2", "2.3.*"] steps: - uses: actions/checkout@v6 with: @@ -97,11 +123,13 @@ jobs: python-version: "3.12" cache: pip cache-dependency-path: pyproject.toml + # Classical-install jobs: deliberately without the `nlp` extra. - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install '.[all]' - python -m pip install "pandas==${{ matrix.pandas }}" + python -m pip install . --group test "pandas==${{ matrix.pandas }}" python -c "import pandas; print(pandas.__version__)" - - name: Test and coverage - run: make coverage + - name: Check NLP stack is absent + run: python -c "import importlib.util as u; assert u.find_spec('torch') is None, 'torch installed'" + - name: Test + run: make test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e6f6cc5..e35f4086 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,16 +45,24 @@ git pull upstream develop Python `>=3.11, <3.15` is required. -After creating a virtual environment, run: +We recommend `uv`. A plain sync installs shapash with the `dev` and `test` dependency groups: ``` -pip install -e ".[dev,test]" +uv sync ``` -Alternatively, we recommend using `uv`: +Alternatively, with pip (>= 25.1), after creating a virtual environment, run: ``` -uv sync --extra dev --extra test +pip install -e . --group dev --group test ``` +This covers classical (tabular) shapash. The NLP modules and their tests need the heavier `nlp` +extra (torch, transformers, ...); without it, those tests are skipped. To work on NLP: +``` +uv sync --extra nlp # or: pip install -e ".[nlp]" --group dev --group test +``` +A later plain `uv sync` removes the NLP packages again (it syncs exactly). `uv run` and +`uv sync --inexact` leave them in place. + See `pyproject.toml` for other dependency groups. ## Set up pre-commit hooks diff --git a/docs/wiki/NLP-Explainability.md b/docs/wiki/NLP-Explainability.md index d2f3c042..d040e851 100644 --- a/docs/wiki/NLP-Explainability.md +++ b/docs/wiki/NLP-Explainability.md @@ -32,7 +32,7 @@ The `shap` backend is a core dependency already. Everything else (HF models, tor counterfactual/diagnostics stack) is behind the `nlp` extra: ```bash -uv sync --extra nlp +uv sync --extra nlp # contributors: dev + test tooling plus the nlp extra ``` or ```bash @@ -40,7 +40,7 @@ pip install ".[nlp]" ``` which pulls in `torch`, `transformers`, `datasets`, `sentence-transformers`, `sentencepiece`, -`protobuf`, `captum`, `pacmap`, `dash-ag-grid`. +`protobuf`, `captum`, `pacmap`. The `nlp_lime` backend needs `lime`, which is its own extra rather than bundled into `nlp` (it's also usable with tabular data, via `LimeBackend`). diff --git a/pyproject.toml b/pyproject.toml index 3570f931..049497bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ ] dependencies = [ "category_encoders>=2.6.0", + "dash-ag-grid>=35.2.0", "dash-bootstrap-components>=2.0.0", "dash-daq>=0.6.0", "dash>=3.0.0,<4.0.0", @@ -43,6 +44,7 @@ dependencies = [ "pandas>=2.2.2,!=3.0.4,<4.0.0", "phik>=0.12.4", "plotly>=5.0.0,<8.0.0", + "pyarrow>=17.0.0", "scikit-learn>=1.8.0,<1.10.0", "scipy>=1.13.0", "shap>=0.46.0", @@ -64,7 +66,6 @@ nlp = [ "protobuf>=7.35.1", "captum>=0.9.0", "pacmap>=0.9.1", - "dash-ag-grid>=35.2.0" ] xgboost = ["xgboost>=2.1.0"] @@ -72,8 +73,17 @@ catboost = ["catboost>=1.2.8"] lightgbm = ["lightgbm>=4.4.0"] lime = ["lime>=0.2.0.0"] +[project.urls] +Homepage = "https://github.com/MAIF/shapash" + +[dependency-groups] +# Developer-only (not accessible via pip) +# For uv users: `uv sync` installs `dev` + `test` by default (see [tool.uv]) +# For pip >= 25.1: `pip install . --group <name>` dev = ["pre-commit", "mypy", "ruff"] +# Classical (tabular) test deps only. +# NLP tests skip cleanly without the `nlp` extra; add it with `uv sync --extra nlp`. test = ["pytest", "pytest-cov", "shapash[xgboost,catboost,lightgbm,lime,report]"] doc = [ @@ -87,11 +97,15 @@ doc = [ "sphinxcontrib-qthelp==1.0.3", "sphinxcontrib-serializinghtml==1.1.5", ] +dev-all = [ + {include-group = "dev"}, + {include-group = "test"}, + {include-group = "doc"}, + "shapash[nlp]", +] -all = ["shapash[dev, test, doc]"] - -[project.urls] -Homepage = "https://github.com/MAIF/shapash" +[tool.uv] +default-groups = ["dev", "test"] [tool.setuptools] package-dir = {"" = "."} @@ -107,6 +121,9 @@ where = ["."] [tool.pytest.ini_options] pythonpath = ["."] testpaths = ["tests"] +markers = [ + "nlp: needs the `nlp` extra (torch, transformers, captum, ...); skipped when it is not installed", +] [tool.coverage.report] show_missing = true @@ -118,7 +135,7 @@ legacy_tox_ini = """ envlist = py311, py312, py313, py314 [testenv] -extras = test +dependency_groups = test commands = pytest {posargs} """ diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py index 6af57583..1f9b6e91 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_captum_backends.py @@ -16,6 +16,7 @@ transformers = pytest.importorskip("transformers") pytest.importorskip("torch") pytest.importorskip("captum") +pytestmark = pytest.mark.nlp from shapash.backend import NlpCaptumLigBackend # noqa: E402 from shapash.compute.generators import AblationFlipGenerator # noqa: E402 diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py index 91bde1bf..ea526ff1 100644 --- a/tests/integration_tests/test_nlp_whatif.py +++ b/tests/integration_tests/test_nlp_whatif.py @@ -10,6 +10,7 @@ transformers = pytest.importorskip("transformers") pytest.importorskip("torch") +pytestmark = pytest.mark.nlp from shapash.compute.generators import AblationFlipGenerator, HotFlipGenerator, TokenListField # noqa: E402 from shapash.explainer.interactive import InteractiveEngine # noqa: E402 diff --git a/tests/integration_tests/test_torch_st_models.py b/tests/integration_tests/test_torch_st_models.py index 505dbf3d..ef604835 100644 --- a/tests/integration_tests/test_torch_st_models.py +++ b/tests/integration_tests/test_torch_st_models.py @@ -19,6 +19,7 @@ torch = pytest.importorskip("torch") st = pytest.importorskip("sentence_transformers") pytest.importorskip("captum") +pytestmark = pytest.mark.nlp from torch import nn # noqa: E402 diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py index 027f4186..a2a7abb1 100644 --- a/tests/unit_tests/model/test_hf.py +++ b/tests/unit_tests/model/test_hf.py @@ -9,10 +9,13 @@ import numpy as np import pytest -import torch -from torch import nn -from shapash.model.hf import HFClassifierModel, HFPipelineModel +torch = pytest.importorskip("torch") +pytestmark = pytest.mark.nlp + +from torch import nn # noqa: E402 + +from shapash.model.hf import HFClassifierModel, HFPipelineModel # noqa: E402 class FakePipeline: diff --git a/tests/unit_tests/model/test_hf_from_pretrained.py b/tests/unit_tests/model/test_hf_from_pretrained.py index 741ca9aa..d3283870 100644 --- a/tests/unit_tests/model/test_hf_from_pretrained.py +++ b/tests/unit_tests/model/test_hf_from_pretrained.py @@ -11,11 +11,14 @@ from types import SimpleNamespace import pytest -import torch -from torch import nn -from shapash.model import hf -from shapash.model.hf import ( +torch = pytest.importorskip("torch") +pytestmark = pytest.mark.nlp + +from torch import nn # noqa: E402 + +from shapash.model import hf # noqa: E402 +from shapash.model.hf import ( # noqa: E402 _FALLBACK_MAX_LENGTH, _UNSET_TOKENIZER_MAX_LENGTH, HFClassifierModel, diff --git a/tests/unit_tests/model/test_torch_models.py b/tests/unit_tests/model/test_torch_models.py index 29ef3d90..8faa2a20 100644 --- a/tests/unit_tests/model/test_torch_models.py +++ b/tests/unit_tests/model/test_torch_models.py @@ -11,10 +11,13 @@ import numpy as np import pytest -import torch -from torch import nn -from shapash.model import ( +torch = pytest.importorskip("torch") +pytestmark = pytest.mark.nlp + +from torch import nn # noqa: E402 + +from shapash.model import ( # noqa: E402 SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, @@ -22,8 +25,8 @@ TorchClassifierModel, has_capabilities, ) -from shapash.model.encoder import EncoderClassifierModel, _pool_hidden -from shapash.model.torch_models import build_encoder_head_backbone +from shapash.model.encoder import EncoderClassifierModel, _pool_hidden # noqa: E402 +from shapash.model.torch_models import build_encoder_head_backbone # noqa: E402 class _FakeTokenizer: From 3c99cd5fadff884f7da0c30f786e9bd82beaee13 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 21 Sep 2026 16:44:10 +0200 Subject: [PATCH 125/144] fix: fix confusion matrix due to plotly upgrade --- shapash/plots/plot_confusion_matrix.py | 10 +++++-- .../plots/test_plot_confusion_matrix.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/shapash/plots/plot_confusion_matrix.py b/shapash/plots/plot_confusion_matrix.py index 0b8567e9..b40c8518 100644 --- a/shapash/plots/plot_confusion_matrix.py +++ b/shapash/plots/plot_confusion_matrix.py @@ -107,10 +107,16 @@ def plot_confusion_matrix( colorbar_tickformat = None # customdata[true][pred] = [pred_idx, true_idx] — the order a click handler reads. - customdata = np.empty((n, n, 2), dtype=int) + # Plain nested Python lists, not a numpy array: Plotly>=6 serializes an ndarray customdata + # as a binary blob ({"dtype", "bdata", "shape"}) instead of a per-point JSON list, so a real + # click in the browser loses the cell indices entirely (see plot_contribution.py for the + # same fix and tests/integration_tests/test_webapp_click_updates_local_explanation.py for + # the incident this class of bug caused). + customdata_arr = np.empty((n, n, 2), dtype=int) for i in range(n): for j in range(n): - customdata[i, j] = (j, i) + customdata_arr[i, j] = (j, i) + customdata = customdata_arr.tolist() hovertemplate = "true: %{y}<br>predicted: %{x}<br>" + hover_val + "<extra></extra>" common = dict( diff --git a/tests/unit_tests/plots/test_plot_confusion_matrix.py b/tests/unit_tests/plots/test_plot_confusion_matrix.py index beadd1a9..4856abc1 100644 --- a/tests/unit_tests/plots/test_plot_confusion_matrix.py +++ b/tests/unit_tests/plots/test_plot_confusion_matrix.py @@ -1,9 +1,11 @@ """Unit tests for the plot_confusion_matrix function — pure, no explainer or model required.""" +import json import unittest import numpy as np import plotly.graph_objs as go +import plotly.io as pio from shapash.plots.plot_confusion_matrix import _emphasis_colorscale, plot_confusion_matrix @@ -64,6 +66,31 @@ def test_custom_title(self): fig = plot_confusion_matrix(self.cm, self.labels, title="Errors") self.assertIn("Errors", fig.layout.title.text) + def test_customdata_is_json_list_not_binary_blob(self): + # Regression: customdata built from a numpy ndarray is serialized by Plotly>=6 into a + # binary blob ({"dtype", "bdata", "shape"}) instead of a plain per-point JSON list, so + # `clickData.points[0].customdata` in the browser comes back as a dict and + # `_cell_from_click`'s `custom[0]` raises KeyError: 0. See plot_contribution.py's + # equivalent test for the same incident class. + fig = plot_confusion_matrix(self.cm, self.labels) + traces = json.loads(pio.to_json(fig))["data"] + found_customdata = False + for trace in traces: + customdata = trace.get("customdata") + if customdata is None: + continue + found_customdata = True + self.assertIsInstance( + customdata, + list, + msg=( + f"customdata was serialized as {type(customdata)} instead of a plain " + "list. Plotly.js cannot recover a per-point value from a binary-encoded " + "array, so clicking a cell loses its (pred, true) indices in the browser." + ), + ) + self.assertTrue(found_customdata, "No trace with customdata found in the figure") + class TestOffDiagonalEmphasis(unittest.TestCase): """The errors get their own colour range so the diagonal cannot flatten them out.""" From b5571919aeff1f09b5af9301f1c9f0ccb3fb4986 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 21 Sep 2026 16:57:45 +0200 Subject: [PATCH 126/144] refactor(plots): sort significant tokens ascending by abs value in waterfall --- shapash/plots/plot_waterfall.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/shapash/plots/plot_waterfall.py b/shapash/plots/plot_waterfall.py index bb7236e7..ed122b57 100644 --- a/shapash/plots/plot_waterfall.py +++ b/shapash/plots/plot_waterfall.py @@ -2,8 +2,8 @@ Tokens whose absolute contribution falls below a configurable threshold are collapsed into a single "other (N tokens)" bar so the chart stays readable even -for long sequences. Tokens are sorted by absolute contribution descending -(standard SHAP convention), giving the most important tokens top billing. +for long sequences. Tokens are ordered so the biggest absolute contribution +renders at the top of the chart, decreasing downward (standard SHAP convention). """ from __future__ import annotations @@ -102,8 +102,19 @@ def plot_waterfall( significant = [(t, v) for t, v in zip(all_toks, all_vals_f, strict=False) if abs(v) >= threshold] small = [(t, v) for t, v in zip(all_toks, all_vals_f, strict=False) if abs(v) < threshold] - # Sort significant by |value| descending (SHAP convention) - significant.sort(key=lambda p: abs(p[1]), reverse=True) + # "other" is a single aggregate bar standing in for all below-threshold tokens, so it + # must be ranked alongside the significant ones by its own |value| rather than pinned + # next to the total. + bars = list(significant) + if small: + n_small = len(small) + sum_small = sum(v for _, v in small) + bars.append((f"other ({n_small} token{'s' if n_small != 1 else ''})", sum_small)) + + # Sort ascending by |value|. Plotly's horizontal waterfall renders the first y-entry + # at the *bottom* and the last at the *top*, so ascending order here puts the biggest + # contribution at the top of the chart, decreasing downward. + bars.sort(key=lambda p: abs(p[1])) # ── Build waterfall arrays ────────────────────────────────────────── y_labels: list[str] = [] @@ -115,18 +126,11 @@ def plot_waterfall( x_vals.append(float(base_value)) measures.append("absolute") - for tok, val in significant: - y_labels.append(tok) + for label, val in bars: + y_labels.append(label) x_vals.append(val) measures.append("relative") - if small: - n_small = len(small) - sum_small = sum(v for _, v in small) - y_labels.append(f"other ({n_small} token{'s' if n_small != 1 else ''})") - x_vals.append(sum_small) - measures.append("relative") - running_total = (float(base_value) if base_value is not None else 0.0) + sum(all_vals_f) y_labels.append("Total") x_vals.append(0) # value is ignored for measure="total"; Plotly computes it From 2d6528b9c35917b66f7f606248986a39f0bb84dd Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 24 Sep 2026 16:18:20 +0200 Subject: [PATCH 127/144] docs: update readme with nlp preview --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index d017bf4e..3444c4ce 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Shapash is suitable for Regression, Binary Classification and Multiclass problem | Version | New Feature | Description | Tutorial | |:-------------:|:-------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------:|:--------:| +| Preview | NLP explainability | Explain text classifiers at the token level, with a webapp, a What-if Lab (counterfactuals) and label-noise detection. Opt-in: `pip install shapash[nlp]` | [notebook](https://github.com/MAIF/shapash/blob/feature/nlp-refacto/tutorial/nlp/overview.ipynb) | | 2.3.x | Additional dataset columns <br> [New demo](https://shapash-demo.ossbymaif.fr/) <br> [Article](https://pub.towardsai.net/shapash-2-3-0-comprehensive-model-interpretation-40b50157c2fb) | In Webapp: Target and error columns added to dataset and possibility to add features outside the model for more filtering options | [<img src="https://raw.githubusercontent.com/MAIF/shapash/master/docs/_static/add_column_icon.png" width="50" title="add_column">](https://github.com/MAIF/shapash/blob/master/tutorial/generate_webapp/tuto-webapp01-additional-data.ipynb) | 2.3.x | Identity card <br> [New demo](https://shapash-demo.ossbymaif.fr/) <br> [Article](https://pub.towardsai.net/shapash-2-3-0-comprehensive-model-interpretation-40b50157c2fb) | In Webapp: New identity card to summarize the information of the selected sample | [<img src="https://raw.githubusercontent.com/MAIF/shapash/master/docs/_static/identity_card.png" width="50" title="identity">](https://github.com/MAIF/shapash/blob/master/tutorial/generate_webapp/tuto-webapp01-additional-data.ipynb) | 2.2.x | Picking samples <br> [Article](https://www.kdnuggets.com/2022/11/picking-examples-understand-machine-learning-model.html) | New tab in the webapp for picking samples. The graph represents the "True Values Vs Predicted Values" | [<img src="https://raw.githubusercontent.com/MAIF/shapash/master/docs/_static/picking.png" width="50" title="picking">](https://github.com/MAIF/shapash/blob/master/tutorial/plots_and_charts/tuto-plot06-prediction_plot.ipynb) @@ -144,6 +145,41 @@ Shapash can use category-encoders object, sklearn ColumnTransformer or simply fe - Category_encoder: *OneHotEncoder*, *OrdinalEncoder*, *BaseNEncoder*, *BinaryEncoder*, *TargetEncoder* - Sklearn ColumnTransformer: *OneHotEncoder*, *OrdinalEncoder*, *StandardScaler*, *QuantileTransformer*, *PowerTransformer* +## 🔤 Text / NLP Explainability (preview) + +Shapash also makes **text classifiers** explainable: see which words drove a prediction, why the model chose one label over another, and how the prediction changes when the text does. +The NLP stack (`NlpExplainer`, `NlpExplanation`, `NlpWebApp`) is a standalone, opt-in module that lives alongside `SmartExplainer`: +the tabular API is unchanged and the default install pulls no NLP dependency. + +- **Token-level explanations** for a text classifier: which words pushed the prediction towards which label. Choose between SHAP, LIME and Captum (Integrated Gradients) backends +- **Local and global views**: sentence highlights and waterfall plots for one text; word importance, word profiles and a confusion matrix with per-error word analysis for the whole corpus +- **What-if Lab**: edit a text and see the live prediction, and get auto-generated minimal word changes (counterfactuals) that flip it +- **Dataset exploration**: embeddings scatter plot, similar examples, and label-noise detection that tells label errors from model errors +- **Portable results**: `explain()` returns a model-free artifact that can be saved and reloaded on its own (no pickle), and explored in a dedicated Dash webapp + +Works with encoder-based single-label classifiers (BERT, DistilBERT, RoBERTa, DeBERTa, CamemBERT, XLM-R...) from Hugging Face, +sentence-transformers or plain PyTorch. + +```python +from shapash.model.hf import HFClassifierModel +from shapash.explainer.nlp_explainer import NlpExplainer + +labels = ["sadness", "joy", "love", "anger", "fear", "surprise"] +model = HFClassifierModel.from_pretrained( + "bhadresh-savani/distilbert-base-uncased-emotion", label_names=labels +) + +xpl = NlpExplainer(model, label_names=labels) +explanations = xpl.explain(["i am so happy today", "i feel terrified"]) + +explanations.plot.word_importance() # corpus-level word importance +xpl.run_app(explanations) # interactive webapp +``` + +Install it with `pip install "shapash[nlp]"`. Learn more in the [overview notebook](https://github.com/MAIF/shapash/blob/feature/nlp-refacto/tutorial/nlp/overview.ipynb), +the [technical documentation](https://github.com/MAIF/shapash/blob/feature/nlp-refacto/docs/wiki/NLP-Explainability.md) +and the [demo app](https://github.com/MAIF/shapash/blob/feature/nlp-refacto/demo/README.md). + ## 🛠 Installation Shapash is intended to work with Python versions 3.11 to 3.14. Installation can be done with pip: @@ -158,6 +194,11 @@ You can install these using the following command : pip install shapash[report] ``` +To explain text models, install the NLP extra (torch, transformers, captum...): +```bash +pip install shapash[nlp] +``` + If you encounter **compatibility issues** you may check the corresponding section in the Shapash documentation [here](https://shapash.readthedocs.io/en/latest/installation-instructions/index.html). ## 🕐 Quickstart From 1d6c73f157ba8b66ed8da3bee7c746aea14e9c8b Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 24 Sep 2026 16:57:20 +0200 Subject: [PATCH 128/144] chore: align precommit blackendocs with ruff (120 line length) --- .pre-commit-config.yaml | 1 + README.md | 4 +--- docs/wiki/NLP-Explainability.md | 8 ++------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ab596c5..4ac5dd1d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,7 @@ repos: rev: 1.20.0 hooks: - id: blacken-docs + args: [--line-length=120] additional_dependencies: [black==26.5.1] - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.3.0 diff --git a/README.md b/README.md index 3444c4ce..e1530038 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,7 @@ from shapash.model.hf import HFClassifierModel from shapash.explainer.nlp_explainer import NlpExplainer labels = ["sadness", "joy", "love", "anger", "fear", "surprise"] -model = HFClassifierModel.from_pretrained( - "bhadresh-savani/distilbert-base-uncased-emotion", label_names=labels -) +model = HFClassifierModel.from_pretrained("bhadresh-savani/distilbert-base-uncased-emotion", label_names=labels) xpl = NlpExplainer(model, label_names=labels) explanations = xpl.explain(["i am so happy today", "i feel terrified"]) diff --git a/docs/wiki/NLP-Explainability.md b/docs/wiki/NLP-Explainability.md index d040e851..8be41ee3 100644 --- a/docs/wiki/NLP-Explainability.md +++ b/docs/wiki/NLP-Explainability.md @@ -131,9 +131,7 @@ model = HFClassifierModel.from_pretrained( ) xpl = NlpExplainer(model) # backend defaults to NlpShapBackend -xpl = xpl.fit( - X_reference=ref_texts, y=ref_labels -) # optional — enables similar-example retrieval +xpl = xpl.fit(X_reference=ref_texts, y=ref_labels) # optional — enables similar-example retrieval explanation = xpl.explain(texts, y=labels, cache_dir="demo/nlp_cache") explanation.plot.word_importance() # corpus-level view @@ -158,9 +156,7 @@ embeddings, not about the explanation, and the two have different keys: from shapash.compute.embeddings import Embedding embedding = Embedding.load("emotion.emb") # raw vectors — needs no model to reduce -projection = embedding.project( - TSNE(n_components=2) -) # try as many as you like, seconds each +projection = embedding.project(TSNE(n_components=2)) # try as many as you like, seconds each NlpWebApp(explanation, projection=projection).run() ``` From 311e076b5e912a4059492ac7cc3c4ad06621e774 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 30 Sep 2026 16:52:09 +0200 Subject: [PATCH 129/144] feat(nlp): record explanation space and baseline token, add logit-space SHAP - NlpExplanation carries output_space, is_signed and baseline_token; all three are saved to meta.json and output_space/baseline_token enter the cache key - NlpShapBackend(output_space="logit") explains raw logits via the new SupportsLogits capability - LIG uses [MASK] as its baseline token (like SHAP) and defaults to n_steps=100 - serve_nlp.py gains --output-space; the webapp info box shows the space --- demo/serve_nlp.py | 45 +++++++--- shapash/backend/nlp_backend.py | 27 ++++-- shapash/backend/nlp_captum_lig_backend.py | 13 ++- shapash/backend/nlp_lime_backend.py | 4 + shapash/backend/nlp_shap_backend.py | 83 ++++++++++++++++-- shapash/explainer/nlp_explainer.py | 13 ++- shapash/explainer/nlp_explanation.py | 19 +++++ shapash/model/__init__.py | 2 + shapash/model/base.py | 25 ++++++ shapash/model/encoder.py | 39 +++++++-- shapash/webapp/nlp_app.py | 3 +- .../integration_tests/test_captum_backends.py | 54 +++++++++++- tests/unit_tests/backend/test_nlp_backend.py | 7 ++ .../backend/test_nlp_lime_backend.py | 16 ++++ .../backend/test_nlp_shap_backend.py | 85 +++++++++++++++++++ .../explainer/test_nlp_explainer.py | 21 +++++ .../explainer/test_nlp_explanation.py | 39 +++++++++ tests/unit_tests/model/test_hf.py | 53 ++++++++++++ tests/unit_tests/webapp/test_nlp_app.py | 8 ++ 19 files changed, 520 insertions(+), 36 deletions(-) diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 5da1fbe9..61cf9b4e 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -81,8 +81,12 @@ * ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can flip between them freely without ``--recompute``. +* ``--output-space {probability,logit}`` — the space the contributions live in. ``shap`` explains the + softmax probabilities by default and can explain the raw logits instead; ``lig`` is always ``logit`` + (passing ``probability`` with it is rejected). The space is part of the cache key, so switching needs + no ``--recompute`` either. The run-info popover shows which one is displayed. * ``--lig-batch-size`` — only used by ``lig``: Captum's ``internal_batch_size``, chunking each sample's - 50-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or + 100-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or ``1``) if you hit a CUDA out-of-memory error, especially on memory-hungry architectures like DeBERTa. ``lig`` is also the method most sensitive to *truncation* actually being configured — every HF @@ -220,6 +224,7 @@ python demo/serve_nlp.py --url-base-path /shapash-nlp-explainer/ python demo/serve_nlp.py --recompute # ignore the cache and recompute python demo/serve_nlp.py --attribution lig # Captum LayerIntegratedGradients highlights + python demo/serve_nlp.py --output-space logit # SHAP on raw logits (same space as LIG) python demo/serve_nlp.py --model-name distilbert-base-uncased-finetuned-sst-2-english \\ --dataset-name sst2 --dataset-split validation --text-column sentence # A registered custom model against a registered dataset loader (bucketed 5-star -> binary @@ -256,7 +261,7 @@ import torch from torch import nn -from shapash.backend import NlpCaptumLigBackend +from shapash.backend import NlpBackend, NlpCaptumLigBackend, NlpShapBackend from shapash.compute.embeddings import Embedding from shapash.explainer.nlp_explainer import NlpExplainer from shapash.model import HFClassifierModel, SentenceTransformerModel, TextModel @@ -298,11 +303,14 @@ class ServeConfig: # "first n" slice can be entirely one class. seed: int = 0 attribution: str = "shap" # sentence-highlight method: "shap" | "lig" (Captum LayerIntegratedGradients) - # Captum's LayerIntegratedGradients expands one sample into ``n_steps`` (50) scaled copies and runs + # Captum's LayerIntegratedGradients expands one sample into ``n_steps`` (100) scaled copies and runs # them through the model as a single batch unless told otherwise — on a memory-hungry architecture - # (e.g. DeBERTa-v2/v3's disentangled attention) that batch of 50 can blow past a small GPU's memory. + # (e.g. DeBERTa-v2/v3's disentangled attention) that batch of 100 can blow past a small GPU's memory. # ``internal_batch_size`` makes Captum chunk that batch instead; lower it further if you still OOM. lig_batch_size: int = 8 + # Explanation space: "probability" | "logit". ``None`` keeps the backend's own — probability for + # SHAP, logit for LIG (its only space). + output_space: str | None = None # Similar-examples reference corpus: the split neighbours are retrieved from (the model's own # training split) and how many rows of it to bank. Set ``n_reference=0`` to disable the "Similar # Examples" panel. @@ -496,11 +504,20 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: type=int, default=defaults.lig_batch_size, help=( - "Captum internal_batch_size for --attribution lig: chunks each sample's n_steps=50 " + "Captum internal_batch_size for --attribution lig: chunks each sample's n_steps=100 " "integration batch instead of running it through the model in one shot. Lower this " "(e.g. 2 or 1) if LIG hits a CUDA out-of-memory error." ), ) + parser.add_argument( + "--output-space", + choices=["probability", "logit"], + default=defaults.output_space, + help=( + "Space the contributions are explained in. Default: the backend's own (probability for " + "shap, logit for lig). lig only supports logit. Part of the cache key, so no --recompute." + ), + ) parser.add_argument( "--reference-split", default=defaults.reference_split, @@ -544,6 +561,8 @@ def parse_args(argv: list[str] | None = None) -> ServeConfig: # then, so the ``is_file()`` test below sees the same path the loaders will (an unexpanded ``~/...`` # is a nonexistent relative directory, and would slip through this check). config = ServeConfig(**vars(args)) + if config.attribution == "lig" and config.output_space == "probability": + parser.error("--attribution lig attributes raw logits only; drop --output-space or pass 'logit'.") if config.is_local_dataset and config.dataset_config is not None: # --dataset-config only means anything to the hub loader; accepting it here would look like it # selected something. (It isn't in the mutually exclusive group above: that pairs the two @@ -918,15 +937,16 @@ def load_model(config: ServeConfig) -> TextModel: return builder(config, device) if builder is not None else _load_hf_classifier(config, device) -def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend | None: - """Return the attribution backend selected by ``--attribution``. +def build_backend(config: ServeConfig, model: TextModel) -> NlpBackend | None: + """Return the attribution backend selected by ``--attribution`` and ``--output-space``. - Returns ``None`` for ``"shap"`` so ``NlpExplainer`` builds its default ``NlpShapBackend`` (which - needs the model's ``shap_callable`` wiring); ``"lig"`` returns an explicit ``NlpCaptumLigBackend``. + Returns ``None`` for probability-space ``"shap"`` so ``NlpExplainer`` builds its default + ``NlpShapBackend``; logit-space ``"shap"`` returns an explicit ``NlpShapBackend(output_space="logit")`` + (needs a model implementing ``SupportsLogits``), and ``"lig"`` an ``NlpCaptumLigBackend``. """ if config.attribution == "lig": # LIG runs one integration per class per sample — show a progress bar over the batch. - # internal_batch_size chunks each sample's n_steps=50 scaled-copies batch so it doesn't OOM + # internal_batch_size chunks each sample's n_steps=100 scaled-copies batch so it doesn't OOM # memory-hungry architectures (e.g. DeBERTa) on a small GPU — see ServeConfig.lig_batch_size. return NlpCaptumLigBackend( model, @@ -934,6 +954,8 @@ def build_backend(config: ServeConfig, model: TextModel) -> NlpCaptumLigBackend explainer_compute_args={"internal_batch_size": config.lig_batch_size}, show_progress=True, ) + if config.output_space == "logit": + return NlpShapBackend(model, label_names=model.label_names, output_space="logit") return None @@ -1037,9 +1059,10 @@ def main() -> None: # exactly what fit's precompute branch keys on.) logger.info( - "attribution=%s | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s" + "attribution=%s (%s) | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s" " | can_detect_label_noise=%s | can_probe_labels=%s", config.attribution, + xpl.backend.output_space, ",".join(name for name, _ in xpl.available_cf_generators()) or "none", xpl.can_edit(), xpl.can_counterfactual(), diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index 6ffded06..bb94cd9c 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -69,8 +69,8 @@ class NlpBackend(Backend): Owns the ``__init__`` skeleton shared by all text backends (including the ``requires_model_capabilities`` check) and the ``get_local_contributions`` implementation. Concrete subclasses must implement ``run_explainer`` and - return an ``NlpContributions``, and must declare the three class - attributes below. + return an ``NlpContributions``, and must declare ``reference_kind``, + ``is_additive`` and ``output_space`` below. Class Attributes ----------------- @@ -91,13 +91,27 @@ class NlpBackend(Backend): grouping and waterfall/force-style charts; ``False`` for LIME, whose local surrogate coefficients carry no such guarantee. output_space : {"probability", "logit"} - Which model output the contributions explain — read off the backend at + Which model output the contributions explain — read off the backend *instance* at ``explain()`` time and recorded on :class:`~shapash.explainer.nlp_explanation.NlpExplanation` so a saved artifact says which. ``"probability"``: the explained quantity is a (softmax) probability, which forces per-token cross-class cancellation. ``"logit"``: the explained quantity is the model's raw pre-softmax output, which does not cancel. Not an affine rescaling of one another; a caller comparing two backends is comparing numbers on - different scales unless both report the same space. + different scales unless both report the same space. The class attribute is the default; a + backend whose space is a constructor choice (``NlpShapBackend``) overrides it per instance, + which is why it also enters ``NlpExplainer``'s cache key. + is_signed : bool + Whether a contribution's sign carries meaning (pushes toward / away from the class). ``True`` + for every attribution method so far; ``False`` for a magnitude-only method such as unsigned + saliency, whose values the diverging colour scales and "pushes toward" wording would misread. + Defaults to ``True``. + baseline_token : str or None + Instance attribute: the token string a perturbation/path method substitutes for "word absent" + (``"[MASK]"``, ``"[PAD]"``, ``"..."``), or ``None`` when the method has no single one (LIME + removes words) or it is unknown. The baseline input — every content token replaced by it — is + what ``base_values`` is the model's output on, for SHAP (the fully masked text) and LIG (the + integration start) alike. Two backends measured against different baselines answer different + questions even in the same ``output_space``, so it is recorded and keyed too. requires_model_capabilities : tuple[type, ...] Capability ABCs (from :mod:`shapash.model.base`) the bound model must satisfy, checked once here so a new backend declares its needs instead of failing at @@ -125,7 +139,10 @@ class NlpBackend(Backend): reference_kind: ClassVar[Literal["distribution", "statistics", "point", "none"]] is_additive: ClassVar[bool] - output_space: ClassVar[Literal["probability", "logit"]] + # Not a ClassVar: the class value is the default, an instance may override it (see above). + output_space: Literal["probability", "logit"] + is_signed: ClassVar[bool] = True + baseline_token: str | None = None requires_model_capabilities: ClassVar[tuple[type, ...]] = () def __init__( diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index cb938c33..bbb73b64 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -194,7 +194,12 @@ class NlpCaptumLigBackend(NlpBackend): Unused; accepted for interface parity with the other NLP backends. explainer_compute_args : dict, optional Keyword arguments forwarded to ``LayerIntegratedGradients.attribute`` for every sample and - class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` defaults to 50. + class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` defaults to 100: the + ``[MASK]`` reference (see :attr:`baseline_token`) makes the integrand less smooth than ``[PAD]`` + did: on 100 DistilBERT-emotion texts (predicted class) the worst completeness error was 0.84 + logits at 50 steps and 0.31 at 100 (``[PAD]``: 0.11 / 0.06). The step count barely moves the + word *ranking* — only how exactly the values sum to the logit gap — + ``docs/architecture/explanation-space.md`` §10.4 and §10.7. show_progress : bool, default False When True, wrap the per-sample attribution loop in a ``tqdm`` progress bar (LIG runs one integration per class per sample, so a batch is slow). Best-effort: if ``tqdm`` is not @@ -207,7 +212,7 @@ class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` default """ name = "nlp_captum_lig" - # A constructed point from the tokenizer (model.reference_ids() -> pad/mask ids), + # A constructed point from the tokenizer (model.reference_ids() -> mask/pad ids), # not learned from data. reference_kind = "point" # Integrated Gradients satisfies the completeness axiom (Sundararajan, Taly & Yan, @@ -231,6 +236,8 @@ def __init__( ) -> None: super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.show_progress = show_progress + # Same token SHAP's Text masker substitutes (the tokenizer's mask token) whenever one exists. + self.baseline_token = model.baseline_token captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) self.explainer = captum_attr.LayerIntegratedGradients(model.logits, model.embedding_layer) @@ -249,7 +256,7 @@ def run_explainer(self, x) -> NlpContributions: token strings per sample. """ model: SupportsCaptumIG = self.model - attribute_args = {"n_steps": 50, **self.explainer_compute_args} + attribute_args = {"n_steps": 100, **self.explainer_compute_args} contributions: list[np.ndarray] = [] base_values: list[np.ndarray] = [] diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index 8b3767aa..1b77c0d4 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -123,6 +123,10 @@ def __init__( mask_string=self.mask_string, **self.explainer_args, ) + # ``bow=True`` (LIME's default) removes words outright — no substitute token at all. + if not getattr(self.explainer, "bow", True): + # ``mask_string=None`` makes LIME fall back to its own "UNKWORDZ". + self.baseline_token = getattr(self.explainer, "mask_string", None) or "UNKWORDZ" def _classifier_fn(self, texts: list[str]) -> np.ndarray: """Wrap self.model to guarantee a float numpy array of shape (n, n_classes). diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 0600ff4e..fb297f1c 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -24,11 +24,13 @@ import re import unicodedata +from typing import Literal, cast import numpy as np import shap from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.model.base import SupportsLogits, TextModel, has_capabilities # SHAP's masker reports special tokens as blank segments on its offset-mapping path; their # attribution is folded into the baseline during word aggregation. @@ -90,6 +92,38 @@ def _masker_special_tokens(explainer) -> frozenset[str] | None: return frozenset(specials) if specials else None +def _resolve_text_model( + model: TextModel, masker, output_space: Literal["probability", "logit"] +) -> tuple[object, object]: + """Pick the scoring callable and masker a ``TextModel`` offers for ``output_space``. + + Probability space is the model's own SHAP surface (``shap_callable`` + ``shap_masker``: a + pipeline SHAP infers a ``Text`` masker from, or a bare ``predict`` with an explicit one). Logit + space wraps :meth:`~shapash.model.base.SupportsLogits.predict_logits`, a bare function, so it + always needs an explicit masker — built over the model's tokenizer, which is exactly the masker + SHAP would infer from the pipeline, so the two spaces mask identically. + """ + if output_space == "probability": + return model.shap_callable, masker if masker is not None else model.shap_masker + if not has_capabilities(model, SupportsLogits): + raise TypeError( + f"output_space='logit' needs a model implementing SupportsLogits; {type(model).__name__} can " + "only return probabilities. Use output_space='probability', or an adapter with logit access " + "(e.g. HFClassifierModel)." + ) + if masker is None: + masker = model.shap_masker + if masker is None: + tokenizer = getattr(model, "tokenizer", None) + if tokenizer is None: + raise TypeError( + f"output_space='logit' needs a tokenizer to mask text with, and {type(model).__name__} " + "exposes none; pass masker= explicitly." + ) + masker = shap.maskers.Text(tokenizer) + return cast(SupportsLogits, model).predict_logits, masker + + def _aggregate_subwords( tokens: list[str], contributions: np.ndarray, @@ -160,9 +194,11 @@ class NlpShapBackend(NlpBackend): Parameters ---------- - model : callable - A text pipeline callable accepted by ``shap.Explainer`` (e.g. a - ``transformers.pipeline`` with ``return_all_scores=True``). + model : TextModel or callable + A :class:`~shapash.model.base.TextModel`, whose scoring surface is chosen by + ``output_space``; or a text callable accepted by ``shap.Explainer`` (e.g. a + ``transformers.pipeline`` with ``return_all_scores=True``), which is taken to return + probabilities. preprocessing : None Unused; accepted for interface compatibility with ``BaseBackend``. label_names : list[str] or None @@ -199,6 +235,20 @@ class NlpShapBackend(NlpBackend): passes. Batching lets them pad into one pass — ~1.9x on distilbert-imdb/GPU — for a numerically identical explanation (max|Δ| 1.1e-07), since the explainer's tree traversal is untouched. + output_space : {"probability", "logit"}, default "probability" + Which model output to explain. ``"logit"`` explains the raw pre-softmax scores, needs a + ``TextModel`` implementing :class:`~shapash.model.base.SupportsLogits`, and is incompatible + with ``explainer_args`` (which bring their own model). Same runtime and masking; the + contributions stop cancelling across classes and stop saturating near 0/1, sum exactly to + ``logits(x) - base``, and share their scale with ``nlp_captum_lig`` + (``docs/architecture/explanation-space.md`` §10). + + Raises + ------ + TypeError + If ``output_space="logit"`` and ``model`` cannot return logits. + ValueError + If ``output_space`` is not a known space, or ``"logit"`` is combined with ``explainer_args``. """ name = "nlp_shap" @@ -209,11 +259,11 @@ class NlpShapBackend(NlpBackend): # Shapley values satisfy the efficiency axiom by construction, and stay additive # even under the Partition/Owen path SHAP silently takes for text. Owen values satisfy efficiency too. is_additive = True - # ``shap_callable`` resolves to a ``text-classification`` pipeline (softmax output), not - # ``model.logits``. This is why per-token attributions cancel across classes: the explained + # The default: ``shap_callable`` resolves to a ``text-classification`` pipeline (softmax output), + # not ``model.logits``. This is why per-token attributions cancel across classes: the explained # quantity sums to 1 for every masked variant, which is a constant-payoff game whose Shapley - # values are all zero. - output_space = "probability" + # values are all zero. ``output_space="logit"`` overrides it per instance. + output_space: Literal["probability", "logit"] = "probability" requires_model_capabilities = () # a plain scoring callable is enough def __init__( @@ -225,9 +275,25 @@ def __init__( explainer_args: dict | None = None, explainer_compute_args: dict | None = None, batch_size: int | None = 64, + output_space: Literal["probability", "logit"] = "probability", ) -> None: + if output_space not in ("probability", "logit"): + raise ValueError(f"output_space must be 'probability' or 'logit', got {output_space!r}.") + if output_space == "logit" and explainer_args: + raise ValueError( + "output_space='logit' cannot be combined with explainer_args: those build the explainer " + "around their own model, whose output space this backend cannot check." + ) + if isinstance(model, TextModel): + model, masker = _resolve_text_model(model, masker, output_space) + elif output_space == "logit": + raise TypeError( + "output_space='logit' needs a TextModel implementing SupportsLogits; a bare callable's " + "output space cannot be checked." + ) super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.masker = masker + self.output_space = output_space # ``_batch_size`` is what ``transformers.Pipeline.__call__`` reads (``None`` means 1) and # there is no public setter, so a release that renames it makes this a silent no-op rather @@ -255,6 +321,9 @@ def __init__( # Resolved once: the masker's tokenizer is what decides which segments are special. ``None`` # when no tokenizer is reachable (bare callable / ``SimpleTokenizer``) — see ``_is_special``. self._special_tokens = _masker_special_tokens(self.explainer) + # What the masker substitutes for a hidden token: the tokenizer's mask token, or "..." when + # it has none (SHAP's own fallback). ``None`` for a custom masker exposing no such attribute. + self.baseline_token = getattr(getattr(self.explainer, "masker", None), "mask_token", None) def run_explainer(self, x) -> NlpContributions: """Run the SHAP text explainer and return all explanation components. diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 015dc5b7..f058518a 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -370,7 +370,10 @@ def explain( backend_name=backend_cls.name, is_additive=backend_cls.is_additive, reference_kind=backend_cls.reference_kind, - output_space=backend_cls.output_space, + # Off the instance, not the class: a backend may choose its space per instance. + output_space=self.backend.output_space, + is_signed=backend_cls.is_signed, + baseline_token=getattr(self.backend, "baseline_token", None), model_id=getattr(self._text_model, "checkpoint", None), architecture=getattr(self._text_model, "architecture", None), ) @@ -903,7 +906,9 @@ def _compute_key(self, text_list: list[str]) -> str: The cached explanation is a function of the texts **and** of everything that scores them, so all of it belongs in the key: the model's own identity declaration (:attr:`~shapash.model.base.TextModel.model_id` — checkpoint, pooling, normalization, head - weights), the backend's registered ``name``, its explainer settings, and ``label_names`` (which + weights), the backend's registered ``name``, its explainer settings, the explanation space and + baseline token it reports (both constructor choices, not class constants — a probability-space + and a logit-space ``nlp_shap`` share every other part of the key), and ``label_names`` (which fixes the column order of ``y_prob``). Keying on the texts alone — as this once did — means swapping the backend (SHAP -> LIG), @@ -921,7 +926,9 @@ def _compute_key(self, text_list: list[str]) -> str: # Sorted repr, so two logically identical configs written in a different order agree. args = sorted(getattr(backend, "explainer_args", {}).items()) compute_args = sorted(getattr(backend, "explainer_compute_args", {}).items()) - backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}" + space = getattr(backend, "output_space", None) + baseline = getattr(backend, "baseline_token", None) + backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}:{space}:{baseline!r}" return hash_corpus(text_list, f"{model_id}|{backend_id}|{self.label_names!r}") def _require_text_model(self) -> TextModel: diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index a8e27d01..75420e74 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -309,6 +309,15 @@ class NlpExplanation: different spaces are not directly comparable: notably, only ``"probability"`` forces the per-token cross-class cancellation that :meth:`word_importance`'s ``label_idx=None`` collapse relies on (see its Notes section). + is_signed : bool + Whether a contribution's sign carries meaning — read off the backend at ``explain()`` + time (see ``NlpBackend.is_signed``). ``False`` for a magnitude-only method, whose values + must not be read as pushing toward or away from a class. ``True`` on files saved before + this field existed: every backend up to then was signed. + baseline_token : str or None + The token the backend substituted for an absent word (``"[MASK]"``, ``"[PAD]"``, ``"..."``) + — read off the backend instance at ``explain()`` time. ``None`` when the method has no + single substitute (LIME removing words) or when the file predates this field. """ texts: pd.Series @@ -328,6 +337,8 @@ class NlpExplanation: # NlpExplanation(...) call site (library and test) keeps working unchanged. model_id: str | None = None architecture: str | None = None + is_signed: bool = True + baseline_token: str | None = None # ClassVar, not a field: it is a shared constant, not per-explanation data, so it stays out # of ``fields()`` — and therefore out of ``__init__``, ``replace()`` and ``save()``. @@ -397,6 +408,8 @@ def _check_shared_index(self) -> None: "is_additive", "reference_kind", "output_space", + "is_signed", + "baseline_token", "model_id", "architecture", } @@ -1001,6 +1014,8 @@ def save(self, path: str | Path) -> None: "is_additive": self.is_additive, "reference_kind": self.reference_kind, "output_space": self.output_space, + "is_signed": self.is_signed, + "baseline_token": self.baseline_token, "label_names": self.label_names, "folds_case": self.folds_case, "model_id": self.model_id, @@ -1064,6 +1079,10 @@ def load(cls, path: str | Path) -> NlpExplanation: # explained raw logits, everything else probabilities. output_space=meta.get("output_space") or ("logit" if meta["backend_name"] == "nlp_captum_lig" else "probability"), + # Absent on a file saved before these fields existed: every backend was signed then, and + # the baseline token went unrecorded (None reads as "unknown"). + is_signed=meta.get("is_signed", True), + baseline_token=meta.get("baseline_token"), # Absent (None) on a file saved before these fields existed, or when the model that # produced it exposed no such identity — either way a webapp reading it just omits # the fact rather than showing a stale placeholder. diff --git a/shapash/model/__init__.py b/shapash/model/__init__.py index 82e176d8..97e3398d 100644 --- a/shapash/model/__init__.py +++ b/shapash/model/__init__.py @@ -25,6 +25,7 @@ SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, + SupportsLogits, SupportsTokenization, TextModel, has_capabilities, @@ -63,6 +64,7 @@ def __dir__() -> list[str]: "SupportsEmbeddings", "SupportsGradients", "SupportsCaptumIG", + "SupportsLogits", "has_capabilities", "HFPipelineModel", "HFClassifierModel", diff --git a/shapash/model/base.py b/shapash/model/base.py index 2924f3ea..4f4e6c28 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -12,6 +12,8 @@ * ``SupportsGradients`` — expose per-token gradients of a target-class logit. * ``SupportsCaptumIG`` — expose the embedding module + a logits forward pass so a layer-attribution method (Captum ``LayerIntegratedGradients``) can attribute through the embeddings. +* ``SupportsLogits`` — score a list of strings in raw pre-softmax logit space, so a perturbation + method (SHAP) can explain logits instead of probabilities. A prediction-only model (e.g. a HuggingFace pipeline) implements only ``TextModel``; a raw classifier with tokenizer access implements all three. Generators check compatibility with @@ -428,6 +430,15 @@ def reference_ids(self, input_ids: Any) -> Any: baseline is a well-formed empty-content sequence. """ + @property + def baseline_token(self) -> str | None: + """The token string :meth:`reference_ids` substitutes for content tokens, or ``None`` if unknown. + + Recorded on the explanation artifact so two backends' baselines can be compared: a method + measured against ``[MASK]`` and one measured against ``[PAD]`` answer different questions. + """ + return None + @abstractmethod def logits(self, input_ids: Any, attention_mask: Any) -> Any: """Return raw classification logits, shape ``(batch, n_classes)`` (the Captum forward func).""" @@ -455,6 +466,20 @@ def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[in return None +class SupportsLogits(ABC): + """Capability: score raw text in pre-softmax logit space. + + The text-level counterpart of :meth:`SupportsCaptumIG.logits` (which takes tensors): what a + perturbation explainer wraps when it should explain logits rather than the probabilities + :meth:`TextModel.predict` returns. Probability-space attributions cancel exactly across classes + and saturate near 0/1; logit-space ones do neither (``docs/architecture/explanation-space.md``). + """ + + @abstractmethod + def predict_logits(self, texts: list[str]) -> np.ndarray: + """Return ``(n_texts, n_classes)`` raw logits, columns in the same order as :meth:`TextModel.predict`.""" + + @runtime_checkable class EmbeddingSource(Protocol): """Structural type for the surface an embedding *cache* consumes. diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index e0818650..70aaab23 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -40,6 +40,7 @@ SupportsCaptumIG, SupportsEmbeddings, SupportsGradients, + SupportsLogits, SupportsTokenization, TextModel, ) @@ -135,7 +136,9 @@ def _pool_hidden(hidden: Any, attention_mask: Any, pool: Any) -> Any: return (hidden * mask).sum(1) / mask.sum(1) -class EncoderClassifierModel(TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG): +class EncoderClassifierModel( + TextModel, SupportsTokenization, SupportsEmbeddings, SupportsGradients, SupportsCaptumIG, SupportsLogits +): """Full-capability text adapter over any *encoder + classification head* backbone. Implements prediction, tokenization, the input-embedding table, sentence embeddings in any @@ -409,6 +412,11 @@ def predict(self, texts: list[str]) -> np.ndarray: out = [torch.softmax(output.logits, dim=-1).cpu().numpy() for _, output in self._batches(list(texts))] return np.vstack(out) + def predict_logits(self, texts: list[str]) -> np.ndarray: + """Return ``(n_texts, n_classes)`` raw pre-softmax logits, columns in :meth:`predict` order.""" + out = [output.logits.float().cpu().numpy() for _, output in self._batches(list(texts))] + return np.vstack(out) + def _resolve_pipeline_device(self): """Pick the pipeline device, degrading an unavailable-GPU request to CPU with a warning.""" torch = import_optional_module("torch", extra=_NLP_EXTRA) @@ -686,14 +694,35 @@ def encode(self, text: str): tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0].tolist()) return input_ids, attention_mask, tokens + def _baseline_token_id(self) -> int: + """The id substituted for content tokens in :meth:`reference_ids`: mask, else pad, else unk, else 0. + + ``mask`` comes first because it is what SHAP's ``Text`` masker substitutes (it takes the + tokenizer's ``mask_token``), so LIG and SHAP measure against the same "word absent" input. + Aligning on it raised SHAP<->LIG rank agreement from 0.46 to 0.57 on DistilBERT + (``docs/architecture/explanation-space.md`` §10.4). Explicit ``None`` checks, not ``or``: id + ``0`` is a legitimate token id (BERT's ``[PAD]``, RoBERTa's ``<s>``). + """ + for candidate in ( + self.tokenizer.mask_token_id, + self.tokenizer.pad_token_id, + self.tokenizer.unk_token_id, + ): + if candidate is not None: + return int(candidate) + return 0 + + @property + def baseline_token(self) -> str | None: + """The token string :meth:`reference_ids` substitutes (e.g. ``"[MASK]"``).""" + return self.tokenizer.convert_ids_to_tokens(self._baseline_token_id()) + def reference_ids(self, input_ids): - """Return baseline ids: content tokens replaced by the pad/mask reference, special tokens kept.""" + """Return baseline ids: content tokens replaced by :attr:`baseline_token`, special tokens kept.""" torch = import_optional_module("torch", extra=_NLP_EXTRA) ids = input_ids[0].tolist() special_mask = self.tokenizer.get_special_tokens_mask(ids, already_has_special_tokens=True) - ref_id = self.tokenizer.pad_token_id - if ref_id is None: - ref_id = self.tokenizer.mask_token_id or self.tokenizer.unk_token_id or 0 + ref_id = self._baseline_token_id() ref = [tid if is_special else ref_id for tid, is_special in zip(ids, special_mask, strict=True)] return torch.tensor([ref], device=input_ids.device) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index b43d1630..078a95ad 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -176,7 +176,7 @@ class NlpWebApp: info : dict[str, str], optional Extra ``label -> value`` facts shown in the header's "ⓘ" popover, layered on top of what ``explanation`` already carries (``model_id``, ``architecture`` — each omitted when the - value is ``None`` — plus ``n_samples`` and ``n_classes``/``backend_name``, + value is ``None`` — plus ``n_samples`` and ``n_classes``/``backend_name``/``output_space``, always shown). A key already filled in from ``explanation`` is overridden by *info*; any other key is appended after — except ``"Train samples"``, which is placed right after "Test samples" since the two are naturally read together. ``NlpExplainer.run_app`` uses this key to @@ -222,6 +222,7 @@ def __init__( self._info["Train samples"] = info["Train samples"] self._info["Number of classes"] = str(explanation.n_classes) self._info["Explainer backend"] = explanation.backend_name + self._info["Explanation space"] = "logit" if explanation.output_space == "logit" else "probability" self._info.update(info or {}) self.app = dash.Dash( diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_captum_backends.py index 1f9b6e91..568b84b6 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_captum_backends.py @@ -18,7 +18,7 @@ pytest.importorskip("captum") pytestmark = pytest.mark.nlp -from shapash.backend import NlpCaptumLigBackend # noqa: E402 +from shapash.backend import NlpCaptumLigBackend, NlpShapBackend # noqa: E402 from shapash.compute.generators import AblationFlipGenerator # noqa: E402 from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 from shapash.model import HFClassifierModel # noqa: E402 @@ -109,3 +109,55 @@ def test_ablation_flip_auto_bound_for_pipeline(model): xpl = NlpExplainer(pm, label_names=LABELS) assert xpl.can_counterfactual() assert isinstance(xpl.cf_generator, AblationFlipGenerator) + + +# --------------------------------------------------------------------------- +# Explanation space: raw-logit SHAP and the shared [MASK] baseline +# (docs/architecture/explanation-space.md §8, phases 1b and 3) +# --------------------------------------------------------------------------- + +_SPACE_TEXTS = ["i am so happy today", "i feel terrified and alone"] +_SHAP_FAST = {"max_evals": 60} + + +def test_shap_logit_space_is_exactly_additive_against_predict_logits(model): + backend = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) + raw = backend.run_explainer(_SPACE_TEXTS) + logits = model.predict_logits(_SPACE_TEXTS) + for i in range(len(_SPACE_TEXTS)): + np.testing.assert_allclose(raw.base_values[i] + raw.values[i].sum(axis=0), logits[i], atol=1e-4) + + +def test_shap_logit_space_does_not_cancel_across_classes(model): + # Probability-space contributions sum to ~0 over classes for every word (the payoff sums to 1 for + # every coalition); logit-space ones do not — the reason the space exists. + probability = NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) + logit = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) + prob_sum = np.abs(probability.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() + logit_sum = np.abs(logit.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() + assert prob_sum < 1e-5 + assert logit_sum > 1e-2 + + +def test_shap_and_lig_share_the_mask_reference(model): + shap_backend = NlpShapBackend(model, label_names=LABELS, output_space="logit") + lig_backend = NlpCaptumLigBackend(model, label_names=LABELS) + assert shap_backend.baseline_token == lig_backend.baseline_token == model.tokenizer.mask_token + input_ids, _, _ = model.encode("i am so happy today") + ref = model.reference_ids(input_ids)[0, 1:-1] + assert (ref == model.tokenizer.mask_token_id).all() + + +def test_explainer_records_logit_space_and_keys_it_apart(model, tmp_path): + probability = NlpExplainer( + model, label_names=LABELS, backend=NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) + ) + logit = NlpExplainer( + model, + label_names=LABELS, + backend=NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST), + ) + assert probability._compute_key(_SPACE_TEXTS) != logit._compute_key(_SPACE_TEXTS) + explanation = logit.explain(_SPACE_TEXTS, cache_dir=tmp_path) + assert (explanation.output_space, explanation.baseline_token) == ("logit", "[MASK]") + assert probability.explain(_SPACE_TEXTS, cache_dir=tmp_path).output_space == "probability" diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py index e0975bdc..e3f044e3 100644 --- a/tests/unit_tests/backend/test_nlp_backend.py +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -51,6 +51,13 @@ def test_nlp_lime_backend(self): self.assertEqual(NlpLimeBackend.output_space, "probability") self.assertEqual(NlpLimeBackend.requires_model_capabilities, ()) + def test_every_backend_so_far_is_signed_with_no_class_level_baseline(self): + # ``baseline_token`` is an instance fact (it depends on the model's tokenizer / masker). + for backend_cls in (NlpShapBackend, NlpLimeBackend): + with self.subTest(backend=backend_cls.name): + self.assertTrue(backend_cls.is_signed) + self.assertIsNone(backend_cls.baseline_token) + class _PredictOnlyModel(TextModel): def predict(self, texts): diff --git a/tests/unit_tests/backend/test_nlp_lime_backend.py b/tests/unit_tests/backend/test_nlp_lime_backend.py index 0b507da5..f34e3e8d 100644 --- a/tests/unit_tests/backend/test_nlp_lime_backend.py +++ b/tests/unit_tests/backend/test_nlp_lime_backend.py @@ -64,6 +64,22 @@ def test_explainer_args_forwarded(self): ) self.assertFalse(backend.explainer.bow) + def test_baseline_token_is_none_when_words_are_removed(self): + # bow=True (LIME's default) drops words outright: there is no substitute token. + self.assertIsNone(self.backend.baseline_token) + + def test_baseline_token_is_the_mask_string_when_words_are_replaced(self): + for mask_string, expected in ((None, "UNKWORDZ"), ("[MASK]", "[MASK]")): + with self.subTest(mask_string=mask_string): + backend = NlpLimeBackend( + _fake_classifier, + label_names=LABEL_NAMES, + mask_string=mask_string, + explainer_args={"bow": False}, + explainer_compute_args=_LIME_COMPUTE_ARGS, + ) + self.assertEqual(backend.baseline_token, expected) + # --- _classifier_fn --- def test_classifier_fn_converts_list_to_array(self): diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index 8737d3e0..a6d290e7 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -5,11 +5,13 @@ """ import unittest +from unittest.mock import patch import numpy as np from shapash.backend import NlpShapBackend, get_backend_cls_from_name from shapash.backend.nlp_shap_backend import _aggregate_subwords +from shapash.model.base import SupportsLogits, TextModel class TestNlpShapBackend(unittest.TestCase): @@ -277,3 +279,86 @@ def test_bare_callable_is_unaffected(self): if __name__ == "__main__": unittest.main() + + +class _ProbabilityOnlyModel(TextModel): + """A ``TextModel`` with no logit access — ``predict`` only.""" + + def predict(self, texts): + return np.full((len(texts), 2), 0.5) + + +class _LogitModel(_ProbabilityOnlyModel, SupportsLogits): + """A ``TextModel`` that can also score in logit space.""" + + def __init__(self, tokenizer=None): + super().__init__(label_names=["neg", "pos"]) + self.tokenizer = tokenizer + + def predict_logits(self, texts): + return np.zeros((len(texts), 2)) + + +class TestOutputSpace(unittest.TestCase): + """``output_space`` is a constructor choice, read back off the instance (explanation-space.md §8, phase 3).""" + + def test_unknown_space_is_refused(self): + with self.assertRaises(ValueError): + NlpShapBackend(_LogitModel(), output_space="log-odds") + + def test_logit_with_explainer_args_is_refused(self): + # explainer_args bring their own model, whose output space cannot be checked. + with self.assertRaises(ValueError): + NlpShapBackend(_LogitModel(), output_space="logit", explainer_args={"explainer": FakeShapExplainer}) + + def test_logit_with_a_bare_callable_is_refused(self): + with self.assertRaises(TypeError): + NlpShapBackend(lambda texts: np.zeros((len(texts), 2)), output_space="logit") + + def test_logit_with_a_probability_only_model_is_refused(self): + with self.assertRaises(TypeError) as ctx: + NlpShapBackend(_ProbabilityOnlyModel(), output_space="logit") + self.assertIn("SupportsLogits", str(ctx.exception)) + + def test_logit_without_tokenizer_or_masker_is_refused(self): + with self.assertRaises(TypeError) as ctx: + NlpShapBackend(_LogitModel(tokenizer=None), output_space="logit") + self.assertIn("masker=", str(ctx.exception)) + + @patch("shapash.backend.nlp_shap_backend.shap.Explainer") + def test_logit_wraps_predict_logits_with_an_explicit_masker(self, explainer_cls): + model, masker = _LogitModel(), object() + backend = NlpShapBackend(model, output_space="logit", masker=masker) + explainer_cls.assert_called_once_with(model.predict_logits, masker=masker) + self.assertEqual(backend.output_space, "logit") + # The class default is untouched — another instance still explains probabilities. + self.assertEqual(NlpShapBackend.output_space, "probability") + + @patch("shapash.backend.nlp_shap_backend.shap.maskers.Text") + @patch("shapash.backend.nlp_shap_backend.shap.Explainer") + def test_logit_builds_a_text_masker_over_the_model_tokenizer(self, explainer_cls, text_masker_cls): + tokenizer = object() + model = _LogitModel(tokenizer=tokenizer) + NlpShapBackend(model, output_space="logit") + text_masker_cls.assert_called_once_with(tokenizer) + explainer_cls.assert_called_once_with(model.predict_logits, masker=text_masker_cls.return_value) + + @patch("shapash.backend.nlp_shap_backend.shap.Explainer") + def test_probability_with_a_text_model_uses_its_shap_surface(self, explainer_cls): + model = _LogitModel() + backend = NlpShapBackend(model) + explainer_cls.assert_called_once_with(model.shap_callable, masker=model.shap_masker) + self.assertEqual(backend.output_space, "probability") + + @patch("shapash.backend.nlp_shap_backend.shap.Explainer") + def test_baseline_token_is_the_masker_mask_token(self, explainer_cls): + explainer_cls.return_value.masker.mask_token = "[MASK]" + backend = NlpShapBackend(_LogitModel(), output_space="logit", masker=object()) + self.assertEqual(backend.baseline_token, "[MASK]") + + def test_baseline_token_is_none_without_a_masker(self): + backend = NlpShapBackend( + model=lambda texts: np.zeros((len(texts), 2)), + explainer_args={"explainer": FakeShapExplainer}, + ) + self.assertIsNone(backend.baseline_token) diff --git a/tests/unit_tests/explainer/test_nlp_explainer.py b/tests/unit_tests/explainer/test_nlp_explainer.py index 403fde7b..96c7df73 100644 --- a/tests/unit_tests/explainer/test_nlp_explainer.py +++ b/tests/unit_tests/explainer/test_nlp_explainer.py @@ -490,6 +490,27 @@ def test_label_names_are_part_of_the_key(self): b = _explainer(label_names=list(reversed(LABEL_NAMES))) self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + def test_output_space_is_part_of_the_key(self): + # A per-instance choice (NlpShapBackend(output_space=...)), not visible in name or args. + probability, logit = _MarkerBackend(), _MarkerBackend() + logit.output_space = "logit" + a, b = _explainer(backend=probability), _explainer(backend=logit) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_baseline_token_is_part_of_the_key(self): + mask, pad = _MarkerBackend(), _MarkerBackend() + mask.baseline_token, pad.baseline_token = "[MASK]", "[PAD]" + a, b = _explainer(backend=mask), _explainer(backend=pad) + self.assertNotEqual(a._compute_key(_SAMPLE_TEXTS), b._compute_key(_SAMPLE_TEXTS)) + + def test_explain_records_the_backend_instance_semantics(self): + backend = _MarkerBackend() + backend.output_space, backend.baseline_token = "logit", "[MASK]" + explanation = _explainer(backend=backend).explain(_SAMPLE_TEXTS) + self.assertEqual(explanation.output_space, "logit") + self.assertEqual(explanation.baseline_token, "[MASK]") + self.assertTrue(explanation.is_signed) + def test_key_is_collision_safe_across_text_boundaries(self): # Without a separator between texts these two corpora hash identically. xpl = _explainer() diff --git a/tests/unit_tests/explainer/test_nlp_explanation.py b/tests/unit_tests/explainer/test_nlp_explanation.py index fa19d38a..b7023dc9 100644 --- a/tests/unit_tests/explainer/test_nlp_explanation.py +++ b/tests/unit_tests/explainer/test_nlp_explanation.py @@ -105,6 +105,8 @@ def _assert_round_trips(self, expl: NlpExplanation): self.assertEqual(loaded.is_additive, expl.is_additive) self.assertEqual(loaded.reference_kind, expl.reference_kind) self.assertEqual(loaded.output_space, expl.output_space) + self.assertEqual(loaded.is_signed, expl.is_signed) + self.assertEqual(loaded.baseline_token, expl.baseline_token) self.assertEqual(loaded.label_names, expl.label_names) self.assertEqual(loaded.folds_case, expl.folds_case) @@ -169,6 +171,43 @@ def test_legacy_file_without_output_space_defaults_by_backend(self): self.assertEqual(loaded_shap.output_space, "probability") self.assertEqual(loaded_lig.output_space, "logit") + def test_semantics_fields_round_trip(self): + expl = replace( + _make_explanation(values_ndim=2, with_base=True, with_true=False, with_prob=False), + output_space="logit", + is_signed=False, + baseline_token="[MASK]", + ) + path = self.tmp_path / "semantics.zip" + expl.save(path) + with zipfile.ZipFile(path) as zf: + meta = json.loads(zf.read("meta.json")) + self.assertEqual((meta["is_signed"], meta["baseline_token"]), (False, "[MASK]")) + loaded = NlpExplanation.load(path) + self.assertEqual((loaded.output_space, loaded.is_signed, loaded.baseline_token), ("logit", False, "[MASK]")) + + def test_legacy_file_without_semantics_fields_reads_as_signed_with_unknown_baseline(self): + expl = replace( + _make_explanation(values_ndim=2, with_base=True, with_true=False, with_prob=False), + is_signed=False, + baseline_token="[PAD]", + ) + path = self.tmp_path / "pre_semantics.zip" + expl.save(path) + with zipfile.ZipFile(path) as zin: + meta = json.loads(zin.read("meta.json")) + del meta["is_signed"], meta["baseline_token"] + members = {item.filename: zin.read(item.filename) for item in zin.infolist()} + members["meta.json"] = json.dumps(meta).encode() + legacy_path = self.tmp_path / "pre_semantics_legacy.zip" + with zipfile.ZipFile(legacy_path, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + + loaded = NlpExplanation.load(legacy_path) + self.assertTrue(loaded.is_signed) + self.assertIsNone(loaded.baseline_token) + class TestCorpusIdentity(unittest.TestCase): """``corpus_id`` — the texts-only digest that pairs an explanation with its embeddings. diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py index a2a7abb1..8cd73add 100644 --- a/tests/unit_tests/model/test_hf.py +++ b/tests/unit_tests/model/test_hf.py @@ -15,6 +15,7 @@ from torch import nn # noqa: E402 +from shapash.model.base import SupportsLogits, has_capabilities # noqa: E402 from shapash.model.hf import HFClassifierModel, HFPipelineModel # noqa: E402 @@ -348,3 +349,55 @@ def test_returns_none_for_slow_tokenizer(self): if __name__ == "__main__": unittest.main() + + +class _ReferenceTokenizer: + """Tokenizer stub exposing only what ``reference_ids`` / ``baseline_token`` read.""" + + _VOCAB = {0: "[PAD]", 1: "[CLS]", 2: "[SEP]", 3: "[MASK]", 4: "[UNK]"} + + def __init__(self, mask=3, pad=0, unk=4): + self.mask_token_id, self.pad_token_id, self.unk_token_id = mask, pad, unk + + def convert_ids_to_tokens(self, ids): + return self._VOCAB.get(ids, f"w{ids}") if isinstance(ids, int) else [self._VOCAB.get(i, f"w{i}") for i in ids] + + def get_special_tokens_mask(self, ids, already_has_special_tokens=True): + return [int(i in (1, 2)) for i in ids] + + +class TestBaselineToken(unittest.TestCase): + """The LIG baseline is SHAP's masker token (``[MASK]``) whenever the tokenizer has one.""" + + def _model(self, **tokenizer_ids): + return HFClassifierModel(_TinyClassifier(), _ReferenceTokenizer(**tokenizer_ids), label_names=["neg", "pos"]) + + def test_mask_is_preferred_over_pad(self): + model = self._model() + self.assertEqual(model.baseline_token, "[MASK]") + ref = model.reference_ids(torch.tensor([[1, 7, 8, 2]])) + self.assertEqual(ref.tolist(), [[1, 3, 3, 2]]) # specials kept, content -> [MASK] + + def test_falls_back_to_pad_then_unk(self): + self.assertEqual(self._model(mask=None).baseline_token, "[PAD]") + self.assertEqual(self._model(mask=None, pad=None).baseline_token, "[UNK]") + self.assertEqual(self._model(mask=None, pad=None, unk=None).baseline_token, "[PAD]") # id 0 + + def test_id_zero_is_a_valid_reference(self): + # ``mask_token_id or ...`` would skip a legitimate id 0. + model = self._model(mask=None, pad=0) + self.assertEqual(model.reference_ids(torch.tensor([[1, 7, 2]])).tolist(), [[1, 0, 2]]) + + +class TestPredictLogits(unittest.TestCase): + def test_softmax_of_logits_is_predict(self): + model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"], batch_size=2) + texts = ["good movie", "bad", "fine film overall"] + logits = model.predict_logits(texts) + self.assertEqual(logits.shape, (3, 2)) + softmax = np.exp(logits) / np.exp(logits).sum(axis=1, keepdims=True) + np.testing.assert_allclose(softmax, model.predict(texts), rtol=1e-5) + + def test_classifier_model_advertises_the_capability(self): + model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"]) + self.assertTrue(has_capabilities(model, SupportsLogits)) diff --git a/tests/unit_tests/webapp/test_nlp_app.py b/tests/unit_tests/webapp/test_nlp_app.py index d6b1416e..e583020c 100644 --- a/tests/unit_tests/webapp/test_nlp_app.py +++ b/tests/unit_tests/webapp/test_nlp_app.py @@ -557,6 +557,14 @@ def setUp(self): def test_layout_built(self): self.assertIsNotNone(self.webapp.app.layout) + def test_info_reports_explanation_space(self): + # Right after the backend, so a reader knows whether base/contributions are probabilities or logits. + keys = list(self.webapp._info) + self.assertEqual(keys[keys.index("Explainer backend") + 1], "Explanation space") + self.assertEqual(self.webapp._info["Explanation space"], "probability") + logit_app = NlpWebApp(_make_explanation(output_space="logit"), engine=self.xpl) + self.assertEqual(logit_app._info["Explanation space"], "logit") + def test_class_selector_options(self): # Class selector is now two independent dropdowns: "local-class-selector" (Sentence # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). From 7c40ab9fe39b330adc05e2a2ec8d2fe201cbd422 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 30 Sep 2026 16:57:10 +0200 Subject: [PATCH 130/144] chore: reorder info box of the webapp --- shapash/webapp/nlp_app.py | 11 +++++++---- tests/unit_tests/webapp/test_nlp_app.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 078a95ad..8d8192d0 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -178,8 +178,8 @@ class NlpWebApp: ``explanation`` already carries (``model_id``, ``architecture`` — each omitted when the value is ``None`` — plus ``n_samples`` and ``n_classes``/``backend_name``/``output_space``, always shown). A key already filled in from ``explanation`` is overridden by *info*; any - other key is appended after — except ``"Train samples"``, which is placed right after "Test - samples" since the two are naturally read together. ``NlpExplainer.run_app`` uses this key to + other key is appended after — except ``"Dataset"``, placed right after the model rows, and + ``"Train samples"``, placed right after "Test samples" since the two are naturally read together. ``NlpExplainer.run_app`` uses this key to add it (only known post-``fit()``, so it can never live on the model-free ``explanation``) — see there for the full picture when going through it rather than constructing ``NlpWebApp`` directly. @@ -215,10 +215,13 @@ def __init__( self._info["Model"] = explanation.model_id if explanation.architecture: self._info["Model architecture"] = explanation.architecture + # "Dataset" and "Train samples" come only from *info*. Reserve their slots here (right after + # the model and after "Test samples"), so the update() below fills an existing key in place + # instead of appending it at the end like any other extra fact. + if info and "Dataset" in info: + self._info["Dataset"] = info["Dataset"] self._info["Test samples"] = str(explanation.n_samples) if info and "Train samples" in info: - # Reserve the slot here, right after "Test samples", so the update() below fills an - # existing key in place instead of appending it at the end like any other extra fact. self._info["Train samples"] = info["Train samples"] self._info["Number of classes"] = str(explanation.n_classes) self._info["Explainer backend"] = explanation.backend_name diff --git a/tests/unit_tests/webapp/test_nlp_app.py b/tests/unit_tests/webapp/test_nlp_app.py index e583020c..d28cf704 100644 --- a/tests/unit_tests/webapp/test_nlp_app.py +++ b/tests/unit_tests/webapp/test_nlp_app.py @@ -565,6 +565,18 @@ def test_info_reports_explanation_space(self): logit_app = NlpWebApp(_make_explanation(output_space="logit"), engine=self.xpl) self.assertEqual(logit_app._info["Explanation space"], "logit") + def test_info_places_dataset_and_train_samples_in_reserved_slots(self): + app = NlpWebApp( + _make_explanation(), + engine=self.xpl, + info={"Extra": "x", "Train samples": "10", "Dataset": "dair-ai/emotion"}, + ) + keys = list(app._info) + self.assertEqual(keys.index("Dataset"), keys.index("Test samples") - 1) + self.assertEqual(keys.index("Train samples"), keys.index("Test samples") + 1) + self.assertLess(keys.index("Dataset"), keys.index("Explainer backend")) + self.assertEqual(keys[-1], "Extra") + def test_class_selector_options(self): # Class selector is now two independent dropdowns: "local-class-selector" (Sentence # Highlight / Waterfall) and "global-class-selector" (Word Importance / Embeddings). From 78f6c7f28b328f013b3ade2aa3a8c9d0e320a853 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 30 Sep 2026 18:12:35 +0200 Subject: [PATCH 131/144] fix(nlp): make the LIME backend a first-class citizen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NlpLimeBackend accepts a TextModel, like the SHAP and LIG backends: it scores through `predict` and takes `label_names` from the model unless you pass your own. - NlpLimeBackend without `label_names` now asks the model for its output width and explains every class. Before, LIME explained class 1 only and the contributions were silently zero. - NlpExplainer._predict accepts a bare scoring callable that returns an (n_texts, n_classes) array (e.g. sklearn's predict_proba), with clear errors for a non-2-D array or mismatched label_names. - explain(cache_dir=...) refuses a bare callable: it has no identity, so two different callables would share cache entries. HFPipelineModel.model_id now includes the pipeline's checkpoint for the same reason. - The webapp honours `is_additive`: the Waterfall tab is not mounted and the "Base + Σ = Total" summary is dropped (webapp and `.plot.sentence`) for non-additive explanations such as LIME. - Rewrite the NlpLimeBackend and NlpExplainer docstrings with runnable examples (TextModel, sklearn predict_proba, HF pipeline). Tests: merge test_captum_backends.py into test_nlp_backends.py, with one section per backend (SHAP, LIG, LIME) plus the cross-backend checks. Only the LIME tests are gated on the `lime` extra. Move the AblationFlip flip test to test_nlp_whatif.py and drop the pipeline auto-bind test, which that file already covered. Add unit tests for the fixes above. --- shapash/backend/nlp_lime_backend.py | 85 +++++-- shapash/explainer/nlp_explainer.py | 50 +++- shapash/explainer/nlp_plotter.py | 5 +- shapash/model/hf.py | 6 + shapash/webapp/nlp_app.py | 9 +- shapash/webapp/nlp_components/base.py | 5 + .../nlp_components/sentence_highlight.py | 3 +- shapash/webapp/nlp_components/waterfall.py | 4 +- ...aptum_backends.py => test_nlp_backends.py} | 224 ++++++++++++------ tests/integration_tests/test_nlp_whatif.py | 25 +- .../backend/test_nlp_lime_backend.py | 35 +++ .../explainer/test_nlp_explainer.py | 32 +++ .../unit_tests/explainer/test_nlp_plotter.py | 5 + tests/unit_tests/model/test_hf.py | 20 ++ tests/unit_tests/webapp/test_nlp_app.py | 8 + 15 files changed, 398 insertions(+), 118 deletions(-) rename tests/integration_tests/{test_captum_backends.py => test_nlp_backends.py} (51%) diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index 1b77c0d4..aa29d41c 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -24,6 +24,7 @@ _lime_available = False from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.model.base import TextModel class NlpLimeBackend(NlpBackend): @@ -34,15 +35,21 @@ class NlpLimeBackend(NlpBackend): Parameters ---------- - model : callable - A function ``f(texts: list[str]) -> np.ndarray`` of shape - ``(n_texts, n_classes)`` returning class probabilities. - HuggingFace pipelines require a thin wrapper — see the example below. + model : TextModel or callable + A ``TextModel`` adapter (scored through its ``predict``, like the other NLP + backends), or a scoring function ``f(texts: list[str]) -> np.ndarray`` of shape + ``(n_texts, n_classes)`` returning class probabilities, columns in the order of + ``label_names`` (e.g. an sklearn pipeline's ``predict_proba``). A HuggingFace + pipeline built with ``top_k=None`` (or ``return_all_scores=True``) is also + accepted as-is, but then ``label_names`` is required so the scores can be + matched to columns by label. preprocessing : None Unused; accepted for interface compatibility with ``BaseBackend``. label_names : list[str] or None - Class names in the same order as the model output columns. - Forwarded to ``LimeTextExplainer`` as ``class_names``. + Class names in the same order as the model output columns. Defaults to + ``model.label_names`` for a ``TextModel``. Forwarded to ``LimeTextExplainer`` + as ``class_names``. When unknown, every output column is still explained, + named by position. mask_string : str or None Token used to replace masked words when ``bow=False``. Mirrors the ``masker`` parameter of ``NlpShapBackend``. Defaults to @@ -62,22 +69,40 @@ class NlpLimeBackend(NlpBackend): If neither ``labels`` nor ``top_labels`` is provided, ``labels`` is automatically set to ``range(len(label_names))``. - Example - ------- - >>> import numpy as np - >>> from transformers import pipeline - >>> pipe = pipeline( - ... "text-classification", - ... model="distilbert-base-uncased-finetuned-sst-2-english", - ... return_all_scores=True, - ... ) - >>> def classifier_fn(texts): - ... return np.array([[s["score"] for s in row] for row in pipe(texts)]) + Examples + -------- + With a ``TextModel`` adapter — the same object ``NlpShapBackend`` and + ``NlpCaptumLigBackend`` take; ``label_names`` comes from the model: + + >>> from shapash.model.hf import HFClassifierModel + >>> model = HFClassifierModel.from_pretrained("bhadresh-savani/distilbert-base-uncased-emotion") + >>> backend = NlpLimeBackend(model, explainer_args={"random_state": 0}) + + With a scikit-learn text classifier, whose ``predict_proba`` already maps a list of + strings to an ``(n_texts, n_classes)`` array: + + >>> from sklearn.feature_extraction.text import TfidfVectorizer + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.pipeline import make_pipeline + >>> clf = make_pipeline(TfidfVectorizer(), LogisticRegression()).fit(train_texts, train_labels) >>> backend = NlpLimeBackend( - ... classifier_fn, - ... label_names=["NEGATIVE", "POSITIVE"], + ... clf.predict_proba, + ... label_names=list(clf.classes_), ... explainer_compute_args={"num_features": 15, "num_samples": 3000}, ... ) + + With a HuggingFace pipeline, which must return every class score: + + >>> from transformers import pipeline + >>> pipe = pipeline("text-classification", model="...", top_k=None) + >>> backend = NlpLimeBackend(pipe, label_names=["NEGATIVE", "POSITIVE"]) + + Then hand the backend to ``NlpExplainer`` along with the same model (the ``TextModel`` itself + when you have one — that keeps the What-if Lab available): + + >>> from shapash.explainer.nlp_explainer import NlpExplainer + >>> xpl = NlpExplainer(model, backend=backend) + >>> explanation = xpl.explain(texts) """ name = "nlp_lime" @@ -111,6 +136,12 @@ def __init__( if not _lime_available: raise ImportError("lime is required for NlpLimeBackend — pip install lime") + # LIME only needs probabilities, so a TextModel is scored through ``predict`` — no capability + # beyond the base contract, which is why ``requires_model_capabilities`` stays empty. + if isinstance(model, TextModel): + if label_names is None: + label_names = model.label_names + model = model.predict super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.mask_string = mask_string @@ -177,10 +208,12 @@ def run_explainer(self, x) -> NlpContributions: vocabulary words per sample. """ texts = list(x) - n_classes = len(self._classes) + # Without label names, ask the model for its output width: LIME's own default (``labels=(1,)``) + # would explain class 1 only and leave every other column silently zero. + n_classes = len(self._classes) or (self._classifier_fn(texts[:1]).shape[1] if texts else 0) compute_args = dict(self.explainer_compute_args) - if "labels" not in compute_args and "top_labels" not in compute_args and n_classes: + if "labels" not in compute_args and "top_labels" not in compute_args: compute_args["labels"] = list(range(n_classes)) contributions: list[np.ndarray] = [] @@ -193,17 +226,17 @@ def run_explainer(self, x) -> NlpContributions: indexed_string = exp.domain_mapper.indexed_string vocab: list[str] = list(indexed_string.inverse_vocab) n_words = len(vocab) - effective_n_classes = n_classes or len(exp.local_exp) - # Dense weight matrix — same shape contract as NlpShapBackend values. - weight_matrix = np.zeros((n_words, effective_n_classes), dtype=float) - for label_idx in range(effective_n_classes): + # Dense weight matrix — same shape contract as NlpShapBackend values. Classes LIME did not + # explain (outside ``top_labels``) stay zero. + weight_matrix = np.zeros((n_words, n_classes), dtype=float) + for label_idx in range(n_classes): if label_idx in exp.local_exp: for word_id, weight in exp.local_exp[label_idx]: weight_matrix[word_id, label_idx] = weight contributions.append(weight_matrix) - base_values_list.append([exp.intercept.get(i, 0.0) for i in range(effective_n_classes)]) + base_values_list.append([exp.intercept.get(i, 0.0) for i in range(n_classes)]) data.append(vocab) return NlpContributions( diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index f058518a..02cdfebb 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -85,8 +85,11 @@ class NlpExplainer: Parameters ---------- - model : callable - Text pipeline or callable accepted by the chosen backend. + model : TextModel or callable + A ``TextModel`` adapter, a ``transformers`` text-classification pipeline, or a bare scoring + callable ``f(texts: list[str]) -> np.ndarray`` of shape ``(n_texts, n_classes)`` holding class + probabilities (then pass ``label_names`` in the same column order). A bare callable cannot + drive the What-if Lab or similar-examples panels, which need a ``TextModel``. label_names : list[str], optional Class names in the same order as the model output columns. Used in plot titles and the webapp class selector. @@ -117,9 +120,21 @@ class NlpExplainer: >>> explanation = xpl.explain(texts) >>> xpl.run_app(explanation, port=8050) + A bare scoring function works too — any callable mapping ``list[str]`` to an + ``(n_texts, n_classes)`` probability array — here an sklearn text classifier explained with LIME. + The same callable goes to both the backend and the explainer, and ``label_names`` must follow the + array's column order: + + >>> from sklearn.feature_extraction.text import TfidfVectorizer + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.pipeline import make_pipeline >>> from shapash.backend.nlp_lime_backend import NlpLimeBackend - >>> lime_backend = NlpLimeBackend(classifier_fn, label_names=[...], explainer_compute_args={"num_features": 15}) - >>> xpl = NlpExplainer(classifier_fn, label_names=[...], backend=lime_backend) + >>> clf = make_pipeline(TfidfVectorizer(), LogisticRegression()).fit(train_texts, train_labels) + >>> classifier_fn = clf.predict_proba + >>> label_names = list(clf.classes_) + >>> lime_backend = NlpLimeBackend(classifier_fn, label_names=label_names) + >>> xpl = NlpExplainer(classifier_fn, label_names=label_names, backend=lime_backend) + >>> explanation = xpl.explain(texts) """ def __init__( @@ -331,13 +346,22 @@ def explain( ``<cache_dir>/<hash>.xpl`` (an :class:`~shapash.explainer.nlp_explanation.NlpExplanation` file) and reloaded on subsequent calls — even after a kernel restart. Disabled by default. One directory can be shared across models and backends: the hash identifies - them, so entries cannot collide. + them, so entries cannot collide. Needs a model with an identity — a ``TextModel`` or a + pipeline; a bare scoring callable raises ``ValueError``. Returns ------- NlpExplanation The computed contributions, predictions and ground truth for this batch. """ + if cache_dir is not None and getattr(self, "_text_model", None) is None: + # The disk key needs the model's identity, and a bare callable has none: every function + # would hash alike, so a second model pointed at the same cache_dir reloads the first's results. + raise ValueError( + "cache_dir needs a model with a stable identity, and a bare callable has none — two " + "different callables would share cache entries. Wrap the model in a TextModel adapter " + "(its model_id enters the key), or call explain() without cache_dir." + ) texts = X if isinstance(X, pd.Series) else pd.Series(X) text_list = texts.tolist() new_hash = self._compute_key(text_list) @@ -957,8 +981,9 @@ def _predict(self, text_list: list[str], index: pd.Index) -> pd.DataFrame: pipeline returns all scores (``return_all_scores=True``), or a single ``"probability"`` column (the winning class confidence) otherwise. - Handles both ``return_all_scores=True`` (list of lists of dicts) and - single-prediction (list of dicts) pipeline output formats. + Handles a ``TextModel``, a probability ``np.ndarray`` from a bare scoring callable + (``label_names`` names the columns), and both ``return_all_scores=True`` (list of lists of + dicts) and single-prediction (list of dicts) pipeline output formats. """ text_model = getattr(self, "_text_model", None) if text_model is not None: @@ -970,6 +995,17 @@ def _predict(self, text_list: list[str], index: pd.Index) -> pd.DataFrame: return result raw = self.model(text_list) + if isinstance(raw, np.ndarray): + # A bare scoring callable (e.g. sklearn's ``predict_proba``): a probability matrix, with + # no label information of its own — the columns are named by ``label_names``. + if raw.ndim != 2: + raise ValueError(f"The model must return an (n_texts, n_classes) array, got shape {raw.shape}.") + names = list(self.label_names) if self.label_names else [str(i) for i in range(raw.shape[1])] + if len(names) != raw.shape[1]: + raise ValueError(f"label_names has {len(names)} entries but the model returned {raw.shape[1]} columns.") + result = pd.DataFrame(raw, index=index, columns=names) + result.insert(0, "prediction", pd.Series([names[i] for i in raw.argmax(axis=1)], index=index)) + return result if raw and isinstance(raw[0], list): labels = [max(preds, key=lambda p: p["score"])["label"] for preds in raw] col_labels = [d["label"] for d in raw[0]] diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index 78c0a3c0..1be274ec 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -232,7 +232,10 @@ def sentence(self, row: int = 0, label_idx: int = 0, notebook: bool = False) -> """ label_idx = self._check_label_idx(label_idx) toks, values, base_value = self._slice(row, label_idx) - div = plot_sentence_highlight(tokens=toks, values=values, base_value=base_value) + # The "base + Σ = total" summary is only true of additive contributions (see ``waterfall``). + div = plot_sentence_highlight( + tokens=toks, values=values, base_value=base_value if self._exp.is_additive else None + ) return DashHtmlPreview(div) if notebook else div # ── batch-level plots ─────────────────────────────────────────────────────────────── diff --git a/shapash/model/hf.py b/shapash/model/hf.py index 0aec19f5..456845ab 100644 --- a/shapash/model/hf.py +++ b/shapash/model/hf.py @@ -224,6 +224,12 @@ def shap_callable(self): """Callable SHAP's text explainer can wrap directly (the pipeline itself).""" return self.pipeline + @property + def model_id(self) -> str: + """The class name plus the pipeline's checkpoint, so two pipelines never share a cache entry.""" + checkpoint = getattr(getattr(self.pipeline, "model", None), "name_or_path", None) + return f"{type(self).__name__}:{checkpoint}" if checkpoint else type(self).__name__ + def predict(self, texts: list[str]) -> np.ndarray: """Return ``(n_texts, n_classes)`` probabilities via the pipeline.""" raw = self.pipeline(list(texts), top_k=None) diff --git a/shapash/webapp/nlp_app.py b/shapash/webapp/nlp_app.py index 8d8192d0..62aef0a9 100644 --- a/shapash/webapp/nlp_app.py +++ b/shapash/webapp/nlp_app.py @@ -518,7 +518,7 @@ def _build_components(self) -> list[WebappComponent]: def _build_tabs(self, components: list[WebappComponent], text_samples_body: html.Div) -> tuple[list, list, list]: """Map *components* onto the shell's three tab groups: left, upper-right, lower-right.""" highlight_comp = next(c for c in components if isinstance(c, SentenceHighlightComponent)) - waterfall_comp = next(c for c in components if isinstance(c, WaterfallComponent)) + waterfall_comp = next((c for c in components if isinstance(c, WaterfallComponent)), None) word_importance_comp = next(c for c in components if isinstance(c, WordImportanceComponent)) editor_comp = next((c for c in components if isinstance(c, DataEditorComponent)), None) cf_comp = next((c for c in components if isinstance(c, CounterfactualComponent)), None) @@ -553,10 +553,9 @@ def _build_tabs(self, components: list[WebappComponent], text_samples_body: html if similar_comp is not None: upper_right_tabs.append(("similar", "Similar Examples", similar_comp.layout(self._ctx))) - lower_right_tabs: list = [ - ("highlight", "Sentence", highlight_comp.layout(self._ctx)), - ("waterfall", "Waterfall", waterfall_comp.layout(self._ctx)), - ] + lower_right_tabs: list = [("highlight", "Sentence", highlight_comp.layout(self._ctx))] + if waterfall_comp is not None: + lower_right_tabs.append(("waterfall", "Waterfall", waterfall_comp.layout(self._ctx))) return left_tabs, upper_right_tabs, lower_right_tabs def _build_stores(self) -> list[dcc.Store]: diff --git a/shapash/webapp/nlp_components/base.py b/shapash/webapp/nlp_components/base.py index 589236df..68855892 100644 --- a/shapash/webapp/nlp_components/base.py +++ b/shapash/webapp/nlp_components/base.py @@ -30,6 +30,9 @@ CAP_LABELS = "data:labels" CAP_GROUND_TRUTH = "data:ground_truth" CAP_PROJECTION = "data:projection" +# The contributions sum to a well-defined total (``NlpExplanation.is_additive``) — what a waterfall's +# running sum and a "base + Σ = total" summary assert. Absent for LIME's surrogate weights. +CAP_ADDITIVE = "data:additive" @dataclass(frozen=True) @@ -73,6 +76,8 @@ def available_capabilities(ctx: AppContext) -> frozenset[str]: # survive a snapshot — they sit outside the engine guard below on purpose. if ctx.coords is not None: caps.add(CAP_PROJECTION) + if explanation.is_additive: + caps.add(CAP_ADDITIVE) if explanation.y_true is not None: caps.add(CAP_GROUND_TRUTH) if has_usable_probabilities(explanation.y_prob): diff --git a/shapash/webapp/nlp_components/sentence_highlight.py b/shapash/webapp/nlp_components/sentence_highlight.py index d9292618..b1eb217a 100644 --- a/shapash/webapp/nlp_components/sentence_highlight.py +++ b/shapash/webapp/nlp_components/sentence_highlight.py @@ -97,7 +97,8 @@ def update_sentence_highlight(datapoint, label_idx): return plot_sentence_highlight( tokens=tokens, values=vals, - base_value=base_value, + # "base + Σ = total" is only true of additive contributions; LIME's intercept + weights is not a prediction. + base_value=base_value if explanation.is_additive else None, color_positive=self._theme.xpl_positive, color_negative=self._theme.xpl_negative, ) diff --git a/shapash/webapp/nlp_components/waterfall.py b/shapash/webapp/nlp_components/waterfall.py index d87c7aee..c8c06dc4 100644 --- a/shapash/webapp/nlp_components/waterfall.py +++ b/shapash/webapp/nlp_components/waterfall.py @@ -7,7 +7,7 @@ from shapash.plots.plot_waterfall import plot_waterfall from shapash.style.style_utils import DEFAULT_NLP_THEME, NlpTheme -from shapash.webapp.nlp_components.base import WebappComponent +from shapash.webapp.nlp_components.base import CAP_ADDITIVE, WebappComponent from shapash.webapp.nlp_components.datapoint import unpack_datapoint @@ -22,6 +22,8 @@ class WaterfallComponent(WebappComponent): id = "waterfall-panel" name = "Waterfall" scope = "local" + # A running sum is only meaningful for additive contributions — same gate as ``.plot.waterfall``. + requires = frozenset({CAP_ADDITIVE}) def __init__(self, theme: NlpTheme = DEFAULT_NLP_THEME) -> None: self._theme = theme diff --git a/tests/integration_tests/test_captum_backends.py b/tests/integration_tests/test_nlp_backends.py similarity index 51% rename from tests/integration_tests/test_captum_backends.py rename to tests/integration_tests/test_nlp_backends.py index 568b84b6..4bedd6b0 100644 --- a/tests/integration_tests/test_captum_backends.py +++ b/tests/integration_tests/test_nlp_backends.py @@ -1,15 +1,14 @@ -"""Integration tests for the Captum-backed NLP features against a real transformer. +"""Integration tests for the NLP attribution backends against a real transformer. -Exercises the LayerIntegratedGradients attribution backend (:class:`NlpCaptumLigBackend`) and the -token-removal counterfactual generator (:class:`AblationFlipGenerator`) end to end on the emotion -distilbert model used by the demos. Skipped automatically when ``transformers`` / ``torch`` / ``captum`` -are not installed (or the model is unavailable). - -``AblationFlipGenerator`` no longer needs captum (it scores leave-one-out through the model's own -``predict``), so its tests are gated more tightly here than they strictly need to be — they share this -module's real-transformer fixture. +One section per backend — SHAP (:class:`NlpShapBackend`), Layer Integrated Gradients +(:class:`NlpCaptumLigBackend`) and LIME (:class:`NlpLimeBackend`) — plus the cross-backend checks, all +on the emotion distilbert model used by the demos. Skipped automatically when the ``nlp`` extra +(``transformers`` / ``torch`` / ``captum``) is not installed or the model is unavailable; the LIME +section additionally needs the separate ``lime`` extra. """ +import importlib.util + import numpy as np import pytest @@ -18,14 +17,18 @@ pytest.importorskip("captum") pytestmark = pytest.mark.nlp -from shapash.backend import NlpCaptumLigBackend, NlpShapBackend # noqa: E402 -from shapash.compute.generators import AblationFlipGenerator # noqa: E402 +from shapash.backend import NlpCaptumLigBackend, NlpLimeBackend, NlpShapBackend # noqa: E402 from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 +from shapash.explainer.nlp_explanation import NlpExplanation # noqa: E402 from shapash.model import HFClassifierModel # noqa: E402 +from shapash.webapp.nlp_app import NlpWebApp # noqa: E402 MODEL_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" LABELS = ["sadness", "joy", "love", "anger", "fear", "surprise"] +# ``lime`` is its own extra rather than part of ``nlp``, so only its section is gated on it. +requires_lime = pytest.mark.skipif(importlib.util.find_spec("lime") is None, reason="needs the lime extra") + @pytest.fixture(scope="module") def model(): @@ -37,6 +40,53 @@ def model(): return HFClassifierModel(classifier, tokenizer, label_names=LABELS) +# --------------------------------------------------------------------------- +# SHAP: raw-logit explanation space +# --------------------------------------------------------------------------- + +_SPACE_TEXTS = ["i am so happy today", "i feel terrified and alone"] +_SHAP_FAST = {"max_evals": 60} + + +def test_shap_logit_space_is_exactly_additive_against_predict_logits(model): + backend = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) + raw = backend.run_explainer(_SPACE_TEXTS) + logits = model.predict_logits(_SPACE_TEXTS) + for i in range(len(_SPACE_TEXTS)): + np.testing.assert_allclose(raw.base_values[i] + raw.values[i].sum(axis=0), logits[i], atol=1e-4) + + +def test_shap_logit_space_does_not_cancel_across_classes(model): + # Probability-space contributions sum to ~0 over classes for every word (the payoff sums to 1 for + # every coalition); logit-space ones do not — the reason the space exists. + probability = NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) + logit = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) + prob_sum = np.abs(probability.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() + logit_sum = np.abs(logit.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() + assert prob_sum < 1e-5 + assert logit_sum > 1e-2 + + +def test_explainer_records_logit_space_and_keys_it_apart(model, tmp_path): + probability = NlpExplainer( + model, label_names=LABELS, backend=NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) + ) + logit = NlpExplainer( + model, + label_names=LABELS, + backend=NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST), + ) + assert probability._compute_key(_SPACE_TEXTS) != logit._compute_key(_SPACE_TEXTS) + explanation = logit.explain(_SPACE_TEXTS, cache_dir=tmp_path) + assert (explanation.output_space, explanation.baseline_token) == ("logit", "[MASK]") + assert probability.explain(_SPACE_TEXTS, cache_dir=tmp_path).output_space == "probability" + + +# --------------------------------------------------------------------------- +# Captum LIG +# --------------------------------------------------------------------------- + + def test_captum_ig_surface(model): """The classifier adapter exposes the LayerIntegratedGradients surface with sane shapes.""" input_ids, attention_mask, tokens = model.encode("i am so happy today") @@ -81,62 +131,101 @@ def test_lig_backend_through_explainer_and_word_importance(model): assert len(word_imp) > 0 # some words survive special-token filtering -def test_ablation_flip_finds_flip(model): - """FeatureAblation-scored token removal flips a confidently classified sample, minimally.""" - gen = AblationFlipGenerator(model) - cfs = gen.generate("i am so happy today", config={"num_examples": 3, "max_ablations": 3}) - assert cfs, "AblationFlip found no counterfactual on a confident sample" - for cf in cfs: - assert cf.new_label != cf.orig_label - assert cf.new_text != cf.original_text - assert all(new == "" for _, _, new in cf.substitutions) # removals record an empty replacement - # Removed tokens really are gone and the reported flip holds under the model. - assert model.predict([cf.new_text])[0].argmax() != model.predict([cf.original_text])[0].argmax() - # minimality: no returned removal set is a strict superset of another - sets = [frozenset(cf.flipped_positions) for cf in cfs] - for i, a in enumerate(sets): - for j, b in enumerate(sets): - if i != j: - assert not (b < a) - - -def test_ablation_flip_auto_bound_for_pipeline(model): - """A prediction-only pipeline model gets AblationFlip auto-bound (HotFlip cannot apply).""" - pipe = transformers.pipeline("text-classification", model=model.classifier, tokenizer=model.tokenizer, top_k=None) - from shapash.model import HFPipelineModel - - pm = HFPipelineModel(pipe, label_names=LABELS) - xpl = NlpExplainer(pm, label_names=LABELS) - assert xpl.can_counterfactual() - assert isinstance(xpl.cf_generator, AblationFlipGenerator) - - # --------------------------------------------------------------------------- -# Explanation space: raw-logit SHAP and the shared [MASK] baseline -# (docs/architecture/explanation-space.md §8, phases 1b and 3) +# LIME # --------------------------------------------------------------------------- -_SPACE_TEXTS = ["i am so happy today", "i feel terrified and alone"] -_SHAP_FAST = {"max_evals": 60} +_LIME_TEXTS = ["i am so happy and excited today", "i feel sad and lonely tonight"] +# Seeded so runs are reproducible; enough samples for a stable top word on these short texts. +_LIME_ARGS = {"random_state": 0} +_LIME_COMPUTE = {"num_samples": 300} -def test_shap_logit_space_is_exactly_additive_against_predict_logits(model): - backend = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) - raw = backend.run_explainer(_SPACE_TEXTS) - logits = model.predict_logits(_SPACE_TEXTS) - for i in range(len(_SPACE_TEXTS)): - np.testing.assert_allclose(raw.base_values[i] + raw.values[i].sum(axis=0), logits[i], atol=1e-4) +def _lime_backend(scorer, **kwargs): + return NlpLimeBackend(scorer, explainer_args=_LIME_ARGS, explainer_compute_args=_LIME_COMPUTE, **kwargs) -def test_shap_logit_space_does_not_cancel_across_classes(model): - # Probability-space contributions sum to ~0 over classes for every word (the payoff sums to 1 for - # every coalition); logit-space ones do not — the reason the space exists. - probability = NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) - logit = NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST) - prob_sum = np.abs(probability.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() - logit_sum = np.abs(logit.run_explainer(_SPACE_TEXTS[:1]).values[0].sum(axis=1)).max() - assert prob_sum < 1e-5 - assert logit_sum > 1e-2 +@requires_lime +def test_lime_text_model_input_explains_every_class_with_the_model_s_labels(model): + backend = _lime_backend(model) # a TextModel, no label_names: both come from the model + assert backend._classes == LABELS + + raw = backend.run_explainer(_LIME_TEXTS) + + assert raw.base_values.shape == (len(_LIME_TEXTS), len(LABELS)) + for text, tokens, values in zip(_LIME_TEXTS, raw.token_strings, raw.values, strict=True): + assert values.shape == (len(tokens), len(LABELS)) + assert np.isfinite(values).all() + assert set(tokens) <= set(text.split()) # LIME explains the text's own words + assert all(np.any(values[:, col]) for col in range(len(LABELS))) # no silently-empty class + + +@requires_lime +def test_lime_attributes_the_obvious_word(model): + """A sanity check on the numbers themselves, not just their shapes.""" + raw = _lime_backend(model).run_explainer(_LIME_TEXTS[:1]) + joy = raw.values[0][:, LABELS.index("joy")] + assert raw.token_strings[0][int(joy.argmax())] in {"happy", "excited"} + + +@requires_lime +def test_lime_seeded_runs_are_reproducible(model): + first = _lime_backend(model).run_explainer(_LIME_TEXTS[:1]).values[0] + second = _lime_backend(model).run_explainer(_LIME_TEXTS[:1]).values[0] + np.testing.assert_array_equal(first, second) + + +@requires_lime +def test_lime_pipeline_input_matches_the_text_model_input(model): + """The ``list[list[dict]]`` pipeline adapter must reorder scores by label, not by position.""" + pipe = transformers.pipeline( + "text-classification", model=model.classifier, tokenizer=model.tokenizer, top_k=None, device=-1 + ) + via_pipeline = _lime_backend(pipe, label_names=LABELS).run_explainer(_LIME_TEXTS[:1]) + via_model = _lime_backend(model).run_explainer(_LIME_TEXTS[:1]) + assert via_pipeline.token_strings == via_model.token_strings + np.testing.assert_allclose(via_pipeline.values[0], via_model.values[0], atol=1e-4) + + +@requires_lime +def test_lime_explainer_artifact_and_round_trip(model, tmp_path): + xpl = NlpExplainer(model, backend=_lime_backend(model)) + explanation = xpl.explain(_LIME_TEXTS, y=["joy", "sadness"]) + + assert explanation.backend_name == "nlp_lime" + assert explanation.is_additive is False + assert explanation.output_space == "probability" + assert explanation.baseline_token is None # bow=True removes words, substitutes nothing + assert list(explanation.y_pred) == [LABELS[i] for i in model.predict(_LIME_TEXTS).argmax(axis=1)] + + path = tmp_path / "lime.xpl" + explanation.save(path) + reloaded = NlpExplanation.load(path) + assert reloaded.backend_name == "nlp_lime" and reloaded.is_additive is False + for before, after in zip(explanation.values, reloaded.values, strict=True): + np.testing.assert_allclose(before, after) + + +@requires_lime +def test_lime_live_explain_text(model): + xpl = NlpExplainer(model, backend=_lime_backend(model)) + contributions, label, probabilities = xpl.explain_text("i am thrilled about this") + assert label in LABELS + assert set(probabilities) == set(LABELS) + assert contributions.values[0].shape == (len(contributions.token_strings[0]), len(LABELS)) + + +@requires_lime +def test_webapp_hides_the_waterfall_for_lime(model): + xpl = NlpExplainer(model, backend=_lime_backend(model)) + app = NlpWebApp(xpl.explain(_LIME_TEXTS), engine=xpl) + assert app._tab_groups["lower-right-tabs"] == ["highlight"] + assert "editor" in app._tab_groups["left-tabs"] # the What-if Lab still mounts with a live TextModel + + +# --------------------------------------------------------------------------- +# Cross-backend +# --------------------------------------------------------------------------- def test_shap_and_lig_share_the_mask_reference(model): @@ -146,18 +235,3 @@ def test_shap_and_lig_share_the_mask_reference(model): input_ids, _, _ = model.encode("i am so happy today") ref = model.reference_ids(input_ids)[0, 1:-1] assert (ref == model.tokenizer.mask_token_id).all() - - -def test_explainer_records_logit_space_and_keys_it_apart(model, tmp_path): - probability = NlpExplainer( - model, label_names=LABELS, backend=NlpShapBackend(model, label_names=LABELS, explainer_compute_args=_SHAP_FAST) - ) - logit = NlpExplainer( - model, - label_names=LABELS, - backend=NlpShapBackend(model, label_names=LABELS, output_space="logit", explainer_compute_args=_SHAP_FAST), - ) - assert probability._compute_key(_SPACE_TEXTS) != logit._compute_key(_SPACE_TEXTS) - explanation = logit.explain(_SPACE_TEXTS, cache_dir=tmp_path) - assert (explanation.output_space, explanation.baseline_token) == ("logit", "[MASK]") - assert probability.explain(_SPACE_TEXTS, cache_dir=tmp_path).output_space == "probability" diff --git a/tests/integration_tests/test_nlp_whatif.py b/tests/integration_tests/test_nlp_whatif.py index ea526ff1..64942509 100644 --- a/tests/integration_tests/test_nlp_whatif.py +++ b/tests/integration_tests/test_nlp_whatif.py @@ -1,8 +1,8 @@ """Integration tests for the NLP what-if stack against a real transformer. Exercises the full-capability HuggingFace adapter (predict / embeddings / gradients) and the -HotFlip generator on the emotion distilbert model used by the demos. Skipped automatically when -``transformers``/``torch`` are not installed. +HotFlip and AblationFlip counterfactual generators on the emotion distilbert model used by the demos. +Skipped automatically when ``transformers``/``torch`` are not installed. """ import numpy as np @@ -108,6 +108,27 @@ def test_hotflip_counterfactuals_are_wellformed_words(hf): assert LABELS[int(cf_probs.argmax())] == cf.new_label +def test_ablation_flip_finds_flip(hf): + """FeatureAblation-scored token removal flips a confidently classified sample, minimally.""" + classifier, tokenizer, _ = hf + model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) + gen = AblationFlipGenerator(model) + cfs = gen.generate("i am so happy today", config={"num_examples": 3, "max_ablations": 3}) + assert cfs, "AblationFlip found no counterfactual on a confident sample" + for cf in cfs: + assert cf.new_label != cf.orig_label + assert cf.new_text != cf.original_text + assert all(new == "" for _, _, new in cf.substitutions) # removals record an empty replacement + # Removed tokens really are gone and the reported flip holds under the model. + assert model.predict([cf.new_text])[0].argmax() != model.predict([cf.original_text])[0].argmax() + # minimality: no returned removal set is a strict superset of another + sets = [frozenset(cf.flipped_positions) for cf in cfs] + for i, a in enumerate(sets): + for j, b in enumerate(sets): + if i != j: + assert not (b < a) + + def test_explainer_interactive_engine_with_classifier(hf): classifier, tokenizer, _ = hf model = HFClassifierModel(classifier, tokenizer, label_names=LABELS) diff --git a/tests/unit_tests/backend/test_nlp_lime_backend.py b/tests/unit_tests/backend/test_nlp_lime_backend.py index f34e3e8d..77ca701b 100644 --- a/tests/unit_tests/backend/test_nlp_lime_backend.py +++ b/tests/unit_tests/backend/test_nlp_lime_backend.py @@ -6,6 +6,7 @@ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_lime_backend import NlpLimeBackend +from shapash.model.base import TextModel LABEL_NAMES = ["sadness", "joy", "love", "anger", "fear", "surprise"] N_CLASSES = len(LABEL_NAMES) @@ -23,6 +24,13 @@ def _fake_classifier(texts: list[str]) -> np.ndarray: return probs +class _FakeTextModel(TextModel): + """Prediction-only ``TextModel`` over :func:`_fake_classifier`.""" + + def predict(self, texts: list[str]) -> np.ndarray: + return _fake_classifier(texts) + + def _make_lime_backend() -> NlpLimeBackend: return NlpLimeBackend( _fake_classifier, @@ -151,6 +159,33 @@ def test_run_explainer_sparse_weights(self): _LIME_COMPUTE_ARGS["num_features"], ) + def test_run_explainer_without_label_names_explains_every_column(self): + # LIME's own default would explain class 1 only, leaving every other column silently zero. + backend = NlpLimeBackend(_fake_classifier, explainer_compute_args=_LIME_COMPUTE_ARGS) + raw = backend.run_explainer(_SAMPLE_TEXTS[:1]) + self.assertEqual(raw.values[0].shape[1], N_CLASSES) + self.assertEqual(raw.base_values.shape, (1, N_CLASSES)) + self.assertTrue(all(np.any(raw.values[0][:, col]) for col in range(N_CLASSES))) + + # --- TextModel input --- + + def test_accepts_a_text_model_scored_through_predict(self): + model = _FakeTextModel(LABEL_NAMES) + backend = NlpLimeBackend(model, explainer_compute_args=_LIME_COMPUTE_ARGS) + self.assertEqual(backend.model, model.predict) + raw = backend.run_explainer(_SAMPLE_TEXTS[:1]) + self.assertEqual(raw.values[0].shape[1], N_CLASSES) + + def test_label_names_default_to_the_text_model_s(self): + backend = NlpLimeBackend(_FakeTextModel(LABEL_NAMES)) + self.assertEqual(backend._classes, LABEL_NAMES) + self.assertEqual(backend.explainer.class_names, LABEL_NAMES) + + def test_explicit_label_names_override_the_text_model_s(self): + renamed = [name.upper() for name in LABEL_NAMES] + backend = NlpLimeBackend(_FakeTextModel(LABEL_NAMES), label_names=renamed) + self.assertEqual(backend._classes, renamed) + # --- get_local_contributions --- def test_get_local_contributions_returns_nlp_contributions(self): diff --git a/tests/unit_tests/explainer/test_nlp_explainer.py b/tests/unit_tests/explainer/test_nlp_explainer.py index 96c7df73..3a645f41 100644 --- a/tests/unit_tests/explainer/test_nlp_explainer.py +++ b/tests/unit_tests/explainer/test_nlp_explainer.py @@ -325,6 +325,32 @@ def test_explain_caching_skips_rerun(self): xpl.explain(_SAMPLE_TEXTS) # same data — must hit in-memory cache self.assertEqual(mocked_predict.call_count, 1, "memoization must skip the second _predict call") + def test_explain_with_a_bare_ndarray_classifier_fn(self): + # No patching of _predict: a bare scoring callable has no TextModel, so predictions come + # from the legacy path, which must accept a probability array. + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=_make_lime_backend()) + explanation = xpl.explain(_SAMPLE_TEXTS) + expected = _fake_classifier(_SAMPLE_TEXTS) + self.assertEqual(list(explanation.y_prob.columns), LABEL_NAMES) + np.testing.assert_allclose(explanation.y_prob.to_numpy(), expected) + self.assertEqual(list(explanation.y_pred), [LABEL_NAMES[i] for i in expected.argmax(axis=1)]) + self.assertEqual(explanation.backend_name, "nlp_lime") + + def test_predict_names_ndarray_columns_by_position_without_label_names(self): + xpl = NlpExplainer(_fake_classifier, backend=_make_lime_backend()) + pred_df = xpl._predict(_SAMPLE_TEXTS, pd.RangeIndex(len(_SAMPLE_TEXTS))) + self.assertEqual(list(pred_df.columns), ["prediction"] + [str(i) for i in range(N_CLASSES)]) + + def test_predict_rejects_label_names_that_do_not_match_the_array(self): + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES[:2], backend=_make_lime_backend()) + with self.assertRaisesRegex(ValueError, "label_names has 2 entries"): + xpl._predict(_SAMPLE_TEXTS, pd.RangeIndex(len(_SAMPLE_TEXTS))) + + def test_predict_rejects_a_one_dimensional_array(self): + xpl = NlpExplainer(lambda texts: np.zeros(len(texts)), backend=_make_lime_backend()) + with self.assertRaisesRegex(ValueError, "n_texts, n_classes"): + xpl._predict(_SAMPLE_TEXTS, pd.RangeIndex(len(_SAMPLE_TEXTS))) + # --------------------------------------------------------------------------- # compile() cache key @@ -565,6 +591,12 @@ def test_two_models_share_a_cache_dir_without_collision(self): self.assertEqual(backend_b.calls, 1, "loaded the other model's cached contributions") np.testing.assert_allclose(explanation.values[0], 2.0) + def test_a_bare_callable_refuses_a_cache_dir(self): + # Every function hashes alike ("function"), so two different callables would share entries. + xpl = NlpExplainer(_fake_classifier, label_names=LABEL_NAMES, backend=_MarkerBackend(marker=1.0)) + with tempfile.TemporaryDirectory() as cache_dir, self.assertRaisesRegex(ValueError, "stable identity"): + xpl.explain(_SAMPLE_TEXTS, cache_dir=cache_dir) + def test_same_model_and_backend_reload_from_disk(self): # The cache must still *work* — a fresh instance skips the expensive run. with tempfile.TemporaryDirectory() as cache_dir: diff --git a/tests/unit_tests/explainer/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py index 65cb60d5..6623f3fb 100644 --- a/tests/unit_tests/explainer/test_nlp_plotter.py +++ b/tests/unit_tests/explainer/test_nlp_plotter.py @@ -250,6 +250,11 @@ def test_waterfall_refuses_on_a_non_additive_backend(self): self.assertIn("nlp_lime", message) self.assertIn("tokens", message) # points at the plot that is still valid + def test_sentence_omits_the_base_plus_sum_summary_on_a_non_additive_backend(self): + # "Base + Σ = Total" asserts additivity; LIME's intercept + surrogate weights is no prediction. + self.assertNotIn("Base:", str(_make_explanation(is_additive=False).plot.sentence(row=0, label_idx=0))) + self.assertIn("Base:", str(_make_explanation(is_additive=True).plot.sentence(row=0, label_idx=0))) + def test_the_other_plots_stay_available_on_a_non_additive_backend(self): explanation = _make_explanation(is_additive=False) self.assertIsInstance(explanation.plot.tokens(row=0, label_idx=0), go.Figure) diff --git a/tests/unit_tests/model/test_hf.py b/tests/unit_tests/model/test_hf.py index 8cd73add..ab8f363f 100644 --- a/tests/unit_tests/model/test_hf.py +++ b/tests/unit_tests/model/test_hf.py @@ -401,3 +401,23 @@ def test_softmax_of_logits_is_predict(self): def test_classifier_model_advertises_the_capability(self): model = HFClassifierModel(_TinyClassifier(), _FakeTokenizer(), label_names=["neg", "pos"]) self.assertTrue(has_capabilities(model, SupportsLogits)) + + +class TestHFPipelineModelIdentity(unittest.TestCase): + """``model_id`` enters ``NlpExplainer``'s cache key, so two pipelines must not share it.""" + + @staticmethod + def _pipeline(checkpoint): + pipeline = FakePipeline(_rows([("negative", 0.1), ("positive", 0.9)])) + pipeline.model = type("_Model", (), {"name_or_path": checkpoint})() + return pipeline + + def test_model_id_carries_the_checkpoint(self): + self.assertNotEqual( + HFPipelineModel(self._pipeline("org/model-a")).model_id, + HFPipelineModel(self._pipeline("org/model-b")).model_id, + ) + self.assertIn("org/model-a", HFPipelineModel(self._pipeline("org/model-a")).model_id) + + def test_model_id_falls_back_to_the_class_name_without_a_checkpoint(self): + self.assertEqual(HFPipelineModel(FakePipeline([])).model_id, "HFPipelineModel") diff --git a/tests/unit_tests/webapp/test_nlp_app.py b/tests/unit_tests/webapp/test_nlp_app.py index d28cf704..598dc541 100644 --- a/tests/unit_tests/webapp/test_nlp_app.py +++ b/tests/unit_tests/webapp/test_nlp_app.py @@ -237,6 +237,14 @@ def test_tabs_degrade_without_scatter_or_whatif(self): self.assertNotIn("left-tabs-body-editor", ids) self.assertNotIn("upper-right-tabs-body-counterfactual", ids) + def test_waterfall_tab_is_not_mounted_for_a_non_additive_explanation(self): + # Same gate as ``.plot.waterfall``: a running sum of LIME surrogate weights means nothing. + app = NlpWebApp(_make_explanation(backend_name="nlp_lime", is_additive=False), engine=None) + found: set = set() + _collect_ids(app.app.layout, found) + self.assertEqual(app._tab_groups["lower-right-tabs"], ["highlight"]) + self.assertNotIn("waterfall-graph", found) + def test_counterfactual_tab_only_with_editor(self): # Editor without CF: editor tab present on the left, no counterfactual tab on the right. app, ids = self._ids(FakeEngine(can_edit=True, can_cf=False)) From f4519da8ace025b5ac6ca9ddc7cd699be27046b4 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 1 Oct 2026 15:56:32 +0200 Subject: [PATCH 132/144] fix(nlp): score every word with LIME by default - default num_features to "all", sized per text from LIME's own split - cast vocabulary tokens to plain str - add show_progress, sharing _progress_iter on NlpBackend with LIG - fix random_state=0 in the docstring examples; document passing it as an int --- shapash/backend/nlp_backend.py | 17 +++++ shapash/backend/nlp_captum_lig_backend.py | 14 ----- shapash/backend/nlp_lime_backend.py | 61 ++++++++++++++---- .../backend/test_nlp_lime_backend.py | 63 +++++++++++++++++++ 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index bb94cd9c..575bab0f 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -18,11 +18,13 @@ from __future__ import annotations import re +from collections.abc import Iterable from dataclasses import dataclass from typing import ClassVar, Literal import numpy as np +from shapash._optional import import_optional_module from shapash.backend.backend import Backend from shapash.model.base import has_capabilities @@ -195,3 +197,18 @@ def get_local_contributions( values=[explain_data.values[i] for i in subset], base_values=None if base_values is None else base_values[subset], ) + + def _progress_iter(self, items: list[str]) -> Iterable[str]: + """Wrap ``items`` in a ``tqdm`` bar when the backend's ``show_progress`` is set, else return it unchanged. + + Shared by the backends that loop over texts one at a time (LIG, LIME). Best-effort and + dependency-free: ``tqdm`` is imported with ``errors="ignore"`` so a missing install simply + yields the plain list rather than raising. Read with ``getattr`` so a backend without the + option (or one built via ``object.__new__``) runs silently. + """ + if not getattr(self, "show_progress", False): + return items + tqdm_mod = import_optional_module("tqdm", errors="ignore") + if tqdm_mod is None: + return items + return tqdm_mod.tqdm(items, desc=f"{self.name} attribution", unit="text") diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index bbb73b64..a4aec084 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -26,7 +26,6 @@ from __future__ import annotations import re -from collections.abc import Iterable import numpy as np @@ -302,16 +301,3 @@ def run_explainer(self, x) -> NlpContributions: values=contributions, base_values=np.stack(base_values, axis=0), ) - - def _progress_iter(self, items: list[str]) -> Iterable[str]: - """Wrap ``items`` in a ``tqdm`` bar when ``show_progress`` is set, else return it unchanged. - - Best-effort and dependency-free: ``tqdm`` is imported with ``errors="ignore"`` so a missing - install simply yields the plain list rather than raising. - """ - if not self.show_progress: - return items - tqdm_mod = import_optional_module("tqdm", errors="ignore") - if tqdm_mod is None: - return items - return tqdm_mod.tqdm(items, desc="LIG attribution", unit="text") diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index aa29d41c..2893b9e8 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -8,8 +8,9 @@ LIME works at word level (bag-of-words by default) rather than at the subword or token level used by SHAP. Each sample's ``token_strings`` is therefore the list of unique vocabulary words found by the ``split_expression`` tokeniser, not -HuggingFace subword tokens. Only the top ``num_features`` words per label -receive a non-zero weight; all others are zero in the dense matrix. +HuggingFace subword tokens. Every word is scored by default (``num_features="all"``), so +a zero weight means LIME found no effect — not that the word fell outside a top-k. +Shapash's plots pick their own top-k at display time. """ from __future__ import annotations @@ -17,7 +18,7 @@ import numpy as np try: - from lime.lime_text import LimeTextExplainer + from lime.lime_text import IndexedCharacters, IndexedString, LimeTextExplainer _lime_available = True except ImportError: @@ -58,19 +59,27 @@ class NlpLimeBackend(NlpBackend): Keyword arguments forwarded to ``LimeTextExplainer.__init__``. Supported keys: ``kernel_width``, ``kernel``, ``verbose``, ``feature_selection``, ``split_expression``, ``bow``, - ``random_state``, ``char_level``. + ``random_state`` (pass as an int), ``char_level``. Use ``{"explainer": SubclassOfLimeTextExplainer, ...rest...}`` to inject a custom explainer class (mirrors the SHAP escape hatch). explainer_compute_args : dict, optional Keyword arguments forwarded to ``LimeTextExplainer.explain_instance`` - on every call. Supported keys: ``num_features`` (default 10), - ``num_samples`` (default 5000), ``distance_metric`` (default + on every call. Supported keys: ``num_features`` (default ``"all"``: + every word of the text; an int keeps LIME's top-k, zero-filling the + rest, which then reads as "no effect"), ``num_samples`` (default 5000), ``distance_metric`` (default ``'cosine'``), ``model_regressor``, ``labels``, ``top_labels``. If neither ``labels`` nor ``top_labels`` is provided, ``labels`` is automatically set to ``range(len(label_names))``. + show_progress : bool, default False + When True, show a ``tqdm`` progress bar over the texts (LIME scores + ``num_samples`` perturbations per text, so a batch is slow). Best-effort: + without ``tqdm`` installed the loop runs silently. Examples -------- + LIME samples random perturbations, so every example fixes ``random_state``: without it the + weights change from run to run. + With a ``TextModel`` adapter — the same object ``NlpShapBackend`` and ``NlpCaptumLigBackend`` take; ``label_names`` comes from the model: @@ -88,14 +97,15 @@ class NlpLimeBackend(NlpBackend): >>> backend = NlpLimeBackend( ... clf.predict_proba, ... label_names=list(clf.classes_), - ... explainer_compute_args={"num_features": 15, "num_samples": 3000}, + ... explainer_args={"random_state": 0}, + ... explainer_compute_args={"num_samples": 3000}, ... ) With a HuggingFace pipeline, which must return every class score: >>> from transformers import pipeline >>> pipe = pipeline("text-classification", model="...", top_k=None) - >>> backend = NlpLimeBackend(pipe, label_names=["NEGATIVE", "POSITIVE"]) + >>> backend = NlpLimeBackend(pipe, label_names=["NEGATIVE", "POSITIVE"], explainer_args={"random_state": 0}) Then hand the backend to ``NlpExplainer`` along with the same model (the ``TextModel`` itself when you have one — that keeps the What-if Lab available): @@ -132,6 +142,7 @@ def __init__( mask_string: str | None = None, explainer_args: dict | None = None, explainer_compute_args: dict | None = None, + show_progress: bool = False, ) -> None: if not _lime_available: raise ImportError("lime is required for NlpLimeBackend — pip install lime") @@ -144,6 +155,12 @@ def __init__( model = model.predict super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.mask_string = mask_string + self.show_progress = show_progress + # Written into the settings rather than applied silently, so the choice is visible on the + # backend and part of NlpExplainer's cache key — a result computed under LIME's own default + # (10) is never served for "every word". + # A new dict, not ``setdefault``: the caller's own dict must not be modified. + self.explainer_compute_args = {"num_features": "all", **self.explainer_compute_args} if "explainer" in self.explainer_args: lime_params = {k: v for k, v in self.explainer_args.items() if k != "explainer"} @@ -188,6 +205,24 @@ def _classifier_fn(self, texts: list[str]) -> np.ndarray: return matrix return np.array(result, dtype=np.float64) + def _count_features(self, text: str) -> int: + """Number of features LIME will see in ``text``: its distinct words (or characters). + + Splits the text exactly as ``explain_instance`` is about to — same class, same settings read + off the explainer — so ``num_features`` covers every word and LIME zero-fills none. An upper + bound would not do: LIME's sparse ``highest_weights`` path pads the selection up to + ``num_features`` instead of capping it. + """ + explainer = self.explainer + bow = getattr(explainer, "bow", True) + mask_string = getattr(explainer, "mask_string", None) + if getattr(explainer, "char_level", False): + indexed = IndexedCharacters(text, bow=bow, mask_string=mask_string) + else: + split_expression = getattr(explainer, "split_expression", r"\W+") + indexed = IndexedString(text, bow=bow, split_expression=split_expression, mask_string=mask_string) + return max(1, indexed.num_words()) + def run_explainer(self, x) -> NlpContributions: """Run LimeTextExplainer on each sample and normalise output. @@ -220,11 +255,15 @@ def run_explainer(self, x) -> NlpContributions: base_values_list: list[list[float]] = [] data: list[list[str]] = [] - for text in texts: - exp = self.explainer.explain_instance(text, self._classifier_fn, **compute_args) + for text in self._progress_iter(texts): + text_args = compute_args + if compute_args.get("num_features") == "all": + text_args = {**compute_args, "num_features": self._count_features(text)} + exp = self.explainer.explain_instance(text, self._classifier_fn, **text_args) indexed_string = exp.domain_mapper.indexed_string - vocab: list[str] = list(indexed_string.inverse_vocab) + # Plain ``str``: LIME hands back ``np.str_``, which leaks into reprs and serialisation. + vocab: list[str] = [str(word) for word in indexed_string.inverse_vocab] n_words = len(vocab) # Dense weight matrix — same shape contract as NlpShapBackend values. Classes LIME did not diff --git a/tests/unit_tests/backend/test_nlp_lime_backend.py b/tests/unit_tests/backend/test_nlp_lime_backend.py index 77ca701b..7b43979d 100644 --- a/tests/unit_tests/backend/test_nlp_lime_backend.py +++ b/tests/unit_tests/backend/test_nlp_lime_backend.py @@ -1,6 +1,7 @@ """Unit tests for ``NlpLimeBackend`` — LIME-based word attribution for NLP models.""" import unittest +from unittest.mock import patch import numpy as np @@ -159,6 +160,68 @@ def test_run_explainer_sparse_weights(self): _LIME_COMPUTE_ARGS["num_features"], ) + def test_num_features_defaults_to_the_distinct_word_count(self): + # LIME's own default (10) zero-fills every word past the top 10 — indistinguishable from "no effect". + text = "one two three four five six seven eight nine ten eleven twelve one" + backend = NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES, explainer_compute_args={"num_samples": 50}) + with patch.object(backend.explainer, "explain_instance", wraps=backend.explainer.explain_instance) as spy: + raw = backend.run_explainer([text]) + self.assertEqual(spy.call_args.kwargs["num_features"], 12) + self.assertEqual(len(raw.token_strings[0]), 12) + + def test_every_word_gets_a_weight_by_default(self): + text = "one two three four five six seven eight nine ten eleven twelve" + backend = NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES, explainer_compute_args={"num_samples": 50}) + captured = {} + original = backend.explainer.explain_instance + + def _capture(*args, **kwargs): + captured["exp"] = original(*args, **kwargs) + return captured["exp"] + + with patch.object(backend.explainer, "explain_instance", side_effect=_capture): + backend.run_explainer([text]) + for label_idx in range(N_CLASSES): + self.assertEqual(len(captured["exp"].local_exp[label_idx]), 12) + + def test_feature_count_follows_the_explainer_settings(self): + char = NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES, explainer_args={"char_level": True}) + self.assertEqual(char._count_features("abca"), 3) + positional = NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES, explainer_args={"bow": False}) + self.assertEqual(positional._count_features("a b a"), 3) + self.assertEqual(self.backend._count_features("a b a"), 2) + self.assertEqual(self.backend._count_features(""), 1) + + def test_explicit_num_features_is_respected(self): + with patch.object(self.backend.explainer, "explain_instance", wraps=self.backend.explainer.explain_instance) as spy: + self.backend.run_explainer(_SAMPLE_TEXTS[:1]) + self.assertEqual(spy.call_args.kwargs["num_features"], _LIME_COMPUTE_ARGS["num_features"]) + + def test_default_is_written_into_the_settings(self): + # Visible on the backend and part of NlpExplainer's cache key, so a result computed under + # LIME's own top-10 default is never reused for "every word". + backend = NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES) + self.assertEqual(backend.explainer_compute_args["num_features"], "all") + self.assertEqual(self.backend.explainer_compute_args["num_features"], _LIME_COMPUTE_ARGS["num_features"]) + + def test_token_strings_are_plain_str(self): + # LIME's own vocabulary is np.str_ (a str subclass, so isinstance alone would not catch it). + raw = self.backend.run_explainer(_SAMPLE_TEXTS) + self.assertTrue(all(type(w) is str for words in raw.token_strings for w in words)) + + def test_show_progress(self): + self.assertFalse(self.backend.show_progress) + backend = NlpLimeBackend( + _fake_classifier, label_names=LABEL_NAMES, explainer_compute_args=_LIME_COMPUTE_ARGS, show_progress=True + ) + self.assertTrue(backend.show_progress) + self.assertEqual(len(backend.run_explainer(_SAMPLE_TEXTS).values), len(_SAMPLE_TEXTS)) + + def test_caller_settings_dict_is_not_modified(self): + settings = {"num_samples": 50} + NlpLimeBackend(_fake_classifier, label_names=LABEL_NAMES, explainer_compute_args=settings) + self.assertEqual(settings, {"num_samples": 50}) + def test_run_explainer_without_label_names_explains_every_column(self): # LIME's own default would explain class 1 only, leaving every other column silently zero. backend = NlpLimeBackend(_fake_classifier, explainer_compute_args=_LIME_COMPUTE_ARGS) From ca033e0682a7f80806ff5d6f7af7614270566d28 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 1 Oct 2026 16:06:28 +0200 Subject: [PATCH 133/144] feat(nlp): add cross-backend comparison - add explanation.plot.compare(others, kind="heatmap"|"bars"|"highlight") - align each backend's word units to the reference by character span (NaN where a backend attributes nothing, e.g. punctuation for LIME) - normalise per backend (max_abs by default, sum_abs or raw) - show Spearman and top-k overlap against the reference in the subtitle - warn when the explanations are in different output spaces --- shapash/compute/token_alignment.py | 213 +++++++++++++ shapash/explainer/nlp_plotter.py | 179 ++++++++++- shapash/plots/plot_backend_comparison.py | 300 ++++++++++++++++++ .../compute/test_token_alignment.py | 123 +++++++ .../unit_tests/explainer/test_nlp_plotter.py | 91 ++++++ .../plots/test_plot_backend_comparison.py | 94 ++++++ 6 files changed, 999 insertions(+), 1 deletion(-) create mode 100644 shapash/compute/token_alignment.py create mode 100644 shapash/plots/plot_backend_comparison.py create mode 100644 tests/unit_tests/compute/test_token_alignment.py create mode 100644 tests/unit_tests/plots/test_plot_backend_comparison.py diff --git a/shapash/compute/token_alignment.py b/shapash/compute/token_alignment.py new file mode 100644 index 00000000..e9e9e6e1 --- /dev/null +++ b/shapash/compute/token_alignment.py @@ -0,0 +1,213 @@ +"""Align per-token contributions from different NLP backends onto one shared set of word units. + +Backends do not agree on what a "token" is, even after each one merges subwords back into words: + +- ``nlp_shap`` and ``nlp_captum_lig`` emit words in sentence order, punctuation as its own unit — + but LIG reads them back off the tokenizer, so an uncased model hands back ``"the"`` where SHAP + kept ``"The"``. +- ``nlp_lime`` (``bow=True``, its default) emits each *distinct* word once, in first-appearance + order, case-sensitive, with punctuation dropped by its ``split_expression``. A word that occurs + twice gets one weight that stands for every occurrence. + +A positional ``zip`` across backends is therefore wrong as soon as LIME is involved. This module +locates every backend's tokens in the source text to get character spans, then reads each +reference unit's value off the other backend in three steps: + +1. **span** — the other backend's token that starts inside the reference unit's span; +2. **exact word** — otherwise, the other backend's token with the same string (this is how a + repeated word reaches LIME's single bag-of-words weight); +3. **case-folded word** — otherwise, the same match ignoring case. + +A unit none of these resolve is ``NaN``: the backend did not attribute it (LIME and punctuation), +which is different from attributing it zero. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence + +import numpy as np +import pandas as pd + + +def locate_tokens(text: str, tokens: Sequence[str]) -> list[tuple[int, int] | None]: + """Character span of each token in ``text``, searched left to right, ignoring case. + + Each search starts where the previous match ended, so a repeated word maps to its successive + occurrences. A token that cannot be found (special tokens, normalisation the tokenizer applied) + gets ``None`` and leaves the cursor where it was, so one miss does not shift every later token. + + Parameters + ---------- + text : str + The source text the tokens were produced from. + tokens : sequence of str + Token strings in the order the backend emitted them. + + Returns + ------- + list of (int, int) or None + ``(start, end)`` per token, ``None`` where the token was not found. + """ + haystack = text.casefold() + cursor = 0 + spans: list[tuple[int, int] | None] = [] + for token in tokens: + needle = str(token).strip().casefold() + start = _find_word(haystack, needle, cursor) if needle else -1 + if start < 0: + spans.append(None) + continue + end = start + len(needle) + spans.append((start, end)) + cursor = end + return spans + + +def _find_word(haystack: str, needle: str, cursor: int) -> int: + """First occurrence of ``needle`` at or after ``cursor`` that is not part of a longer word. + + A plain substring search would place LIME's ``"i"`` inside ``"didn"``. Word-boundary guards + apply only on the sides where ``needle`` itself starts/ends with a word character, so + punctuation tokens and contraction pieces (``"'"``, ``"t"`` after ``"didn'"``) still match. + Falls back to the plain substring search for scripts written without spaces (CJK), where no + boundary exists to guard. + """ + left = r"(?<!\w)" if re.match(r"\w", needle) else "" + right = r"(?!\w)" if re.search(r"\w$", needle) else "" + found = re.compile(left + re.escape(needle) + right).search(haystack, cursor) + return found.start() if found else haystack.find(needle, cursor) + + +def align_token_values( + text: str, + reference_tokens: Sequence[str], + tokens: Sequence[str], + values: np.ndarray, +) -> np.ndarray: + """Project one backend's per-token ``values`` onto ``reference_tokens``. + + See the module docstring for the three matching steps. + + Parameters + ---------- + text : str + The source text both tokenizations come from. + reference_tokens : sequence of str + The units to align onto, in sentence order. + tokens : sequence of str + The other backend's tokens. + values : np.ndarray + The other backend's 1-D contributions, one per entry of ``tokens``. + + Returns + ------- + np.ndarray + Shape ``(len(reference_tokens),)``. ``NaN`` where the other backend attributed nothing. + """ + values = np.asarray(values, dtype=float) + if values.ndim != 1 or len(values) != len(tokens): + raise ValueError(f"values must be 1-D with one entry per token, got shape {values.shape} for {len(tokens)}.") + + ref_spans = locate_tokens(text, reference_tokens) + spans = locate_tokens(text, tokens) + + by_start = {span[0]: i for i, span in enumerate(spans) if span is not None} + exact: dict[str, int] = {} + folded: dict[str, int] = {} + for i, token in enumerate(tokens): + exact.setdefault(str(token), i) + folded.setdefault(str(token).casefold(), i) + + out = np.full(len(reference_tokens), np.nan) + for j, (ref_token, ref_span) in enumerate(zip(reference_tokens, ref_spans, strict=True)): + match = None + if ref_span is not None: + match = next((by_start[p] for p in range(*ref_span) if p in by_start), None) + if match is None: + match = exact.get(str(ref_token), folded.get(str(ref_token).casefold())) + if match is not None: + out[j] = values[match] + return out + + +def normalize_contributions(values: np.ndarray, method: str | None = "max_abs") -> np.ndarray: + """Rescale one backend's aligned values so different backends share a scale. + + Backends explain different quantities — a probability, a logit, a local linear surrogate's + weights — so their raw magnitudes are not comparable; only the *shape* of each attribution is. + + Parameters + ---------- + values : np.ndarray + 1-D aligned contributions, ``NaN`` for unattributed units (kept as ``NaN``). + method : {"max_abs", "sum_abs", None} + ``"max_abs"`` divides by the largest magnitude (values land in ``[-1, 1]``, the strongest + unit reads ±1); ``"sum_abs"`` divides by the total magnitude (each value is a signed share + of the attribution mass); ``None`` leaves the raw values. + + Returns + ------- + np.ndarray + """ + values = np.asarray(values, dtype=float) + if method is None: + return values + if method == "max_abs": + scale = np.nanmax(np.abs(values)) if np.isfinite(values).any() else 0.0 + elif method == "sum_abs": + scale = np.nansum(np.abs(values)) + else: + raise ValueError(f"normalize must be 'max_abs', 'sum_abs' or None, got {method!r}.") + return values / scale if scale > 0 else values + + +def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.DataFrame: + """Pairwise agreement between backends' aligned contributions on one text. + + Parameters + ---------- + aligned : mapping of str to np.ndarray + Backend name → 1-D contributions on the same reference units (``NaN`` = unattributed). + top_k : int + Size of the top set for the ``top_k_overlap`` column. + + Returns + ------- + pd.DataFrame + One row per unordered backend pair, with columns ``backend_a``, ``backend_b``, + ``spearman`` (rank correlation of the signed values, over the units both attributed), + ``sign_agreement`` (share of those units where both signs match) and ``top_k_overlap`` + (share of the ``top_k`` highest-magnitude units the two have in common). Scale-free by + construction, so it is safe across backends with different output spaces. + """ + names = list(aligned) + rows = [] + for a_pos, a in enumerate(names): + for b in names[a_pos + 1 :]: + va, vb = np.asarray(aligned[a], dtype=float), np.asarray(aligned[b], dtype=float) + both = np.isfinite(va) & np.isfinite(vb) + xa, xb = va[both], vb[both] + # A rank correlation is undefined when either side is constant (e.g. LIME all zeros on a + # class it did not select a word for); say so with NaN rather than a numpy warning. + ranked = both.sum() > 1 and np.ptp(xa) > 0 and np.ptp(xb) > 0 + spearman = float(pd.Series(xa).corr(pd.Series(xb), method="spearman")) if ranked else np.nan + signs = float(np.mean(np.sign(xa) == np.sign(xb))) if both.any() else np.nan + k = min(top_k, int(both.sum())) + if k > 0: + top_a = set(np.argsort(-np.abs(xa), kind="stable")[:k]) + top_b = set(np.argsort(-np.abs(xb), kind="stable")[:k]) + overlap = len(top_a & top_b) / k + else: + overlap = np.nan + rows.append( + { + "backend_a": a, + "backend_b": b, + "spearman": spearman, + "sign_agreement": signs, + "top_k_overlap": overlap, + } + ) + return pd.DataFrame(rows, columns=["backend_a", "backend_b", "spearman", "sign_agreement", "top_k_overlap"]) diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index 1be274ec..ded7dff9 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -18,19 +18,23 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +import warnings +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any, Literal import numpy as np from dash import html from plotly import graph_objs as go from shapash.compute.embeddings import Embedding, projection_coords +from shapash.compute.token_alignment import align_token_values, backend_agreement, normalize_contributions from shapash.explainer.nlp_explanation import ( WORD_AGGREGATIONS, aggregate_word_contributions, select_label_column, word_contributions_by_sample, ) +from shapash.plots.plot_backend_comparison import plot_backend_bars, plot_backend_heatmap, plot_backend_highlight from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_scatter import plot_scatter from shapash.plots.plot_sentence_highlight import plot_sentence_highlight @@ -238,6 +242,179 @@ def sentence(self, row: int = 0, label_idx: int = 0, notebook: bool = False) -> ) return DashHtmlPreview(div) if notebook else div + # ── cross-backend comparison ──────────────────────────────────────────────────────── + def compare( + self, + others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation], + row: int = 0, + label_idx: int | None = None, + kind: Literal["heatmap", "bars", "highlight"] = "heatmap", + normalize: Literal["max_abs", "sum_abs"] | None = "max_abs", + max_tokens: int | None = None, + top_k: int = 5, + title: str | None = None, + notebook: bool = False, + ) -> go.Figure | html.Div | DashHtmlPreview: + """Compare this explanation's contributions on one text with other backends' on the same text. + + This explanation is the *reference*: its word units are the ones displayed, and every other + backend's values are aligned onto them (see :mod:`shapash.compute.token_alignment` — LIME's + bag-of-words vocabulary does not line up positionally with SHAP's or LIG's word sequence). + Pick a sequence backend (SHAP, LIG) as the reference so every occurrence of a word is shown. + + Parameters + ---------- + others : NlpExplanation, sequence of NlpExplanation, or mapping of str to NlpExplanation + Explanations of the same texts by other backends (or the same backend configured + differently). Labels default to the backend name; pass a mapping to choose them. + row : int + Positional index of the sample (see :meth:`tokens`). Must hold the same text in every + explanation. + label_idx : int, optional + Class to compare on. ``None`` (default) uses this row's predicted class. + kind : {"heatmap", "bars", "highlight"} + ``"heatmap"`` — tokens × backends grid; ``"bars"`` — grouped bars per token; + ``"highlight"`` — one highlighted sentence per backend (a Dash component). + normalize : {"max_abs", "sum_abs", None} + How each backend's values are rescaled before display (see + :func:`~shapash.compute.token_alignment.normalize_contributions`). Backends explain + different quantities (probability, logit, surrogate weights), so the default scales each + to ``[-1, 1]``; ``None`` shows raw values, which is only meaningful between backends + sharing an ``output_space``. + max_tokens : int, optional + Keep only the ``max_tokens`` units with the largest magnitude in *any* backend, in + sentence order. + top_k : int + Size of the top set in the agreement line's top-k overlap. + title : str, optional + Overrides the default, which names the class. + notebook : bool + For ``kind="highlight"`` only — wrap the component for notebook display (see + :meth:`sentence`). + + Returns + ------- + plotly.graph_objs.Figure, dash.html.Div or DashHtmlPreview + + Notes + ----- + A ``NaN`` (drawn as ``·``, a missing bar, or struck-through) means the backend attributed + nothing to that unit — LIME drops punctuation. LIME also zero-fills every word outside its + ``num_features`` top words, so a LIME ``0`` may mean "not selected" rather than "no effect". + + Backends with a different ``output_space`` (SHAP and LIME in probability, Captum LIG in logit) + trigger a ``UserWarning``: rescaling aligns their magnitudes, but probability is a non-linear + function of the logits, so even their rankings can legitimately differ. + + The subtitle reports, for each other backend against the reference, the Spearman rank + correlation and top-k overlap of the aligned values (see + :func:`~shapash.compute.token_alignment.backend_agreement`) — both scale-free, so they do + not depend on ``normalize``. + + Examples + -------- + >>> shap_exp.plot.compare({"LIME": lime_exp, "LIG": lig_exp}, row=3, kind="heatmap").show() + """ + exp = self._exp + named = self._name_explanations(others) + if label_idx is None: + label_idx = exp.label_to_idx.get(str(exp.y_pred.iloc[row]), 0) + label_idx = self._check_label_idx(label_idx) + ref_tokens, ref_values, _ = self._slice(row, label_idx) + text = str(exp.texts.iloc[row]) + + spaces = {name: other.output_space for name, other in named.items()} + if len(set(spaces.values())) > 1: + listing = ", ".join(f"{name}={space}" for name, space in spaces.items()) + warnings.warn( + f"Comparing backends across output spaces ({listing}). Normalisation removes the scale gap " + "but not the softmax non-linearity, so rankings and agreement scores mix method and space " + "differences. For a like-for-like comparison with a logit-space backend, use " + "NlpShapBackend(..., output_space='logit').", + UserWarning, + stacklevel=2, + ) + + aligned: dict[str, np.ndarray] = {} + for name, other in named.items(): + if str(other.texts.iloc[row]) != text: + raise ValueError(f"Row {row} holds a different text in {name!r} than in the reference explanation.") + if other.n_classes != exp.n_classes or ( + other.label_names and exp.label_names and list(other.label_names) != list(exp.label_names) + ): + raise ValueError(f"{name!r} explains different classes than the reference explanation.") + if other is exp: + aligned[name] = ref_values + else: + values = select_label_column(other.values[row], label_idx) + aligned[name] = align_token_values(text, ref_tokens, other.token_strings[row], values) + + agreement = backend_agreement(aligned, top_k=top_k) + ref_name = next(iter(named)) + vs_ref = agreement[agreement["backend_a"] == ref_name] + subtitle = " · ".join( + f"{r.backend_b} vs {ref_name}: ρ={r.spearman:.2f}, top-{top_k}={r.top_k_overlap:.0%}" + for r in vs_ref.itertuples() + ) + + shown = {name: normalize_contributions(v, normalize) for name, v in aligned.items()} + if max_tokens is not None and max_tokens < len(ref_tokens): + strength = np.nanmax(np.abs(np.vstack(list(shown.values()))), axis=0) + keep = np.sort(np.argsort(-np.nan_to_num(strength, nan=-1.0), kind="stable")[:max_tokens]) + ref_tokens = [ref_tokens[i] for i in keep] + shown = {name: v[keep] for name, v in shown.items()} + + if kind == "highlight": + div = plot_backend_highlight(ref_tokens, shown, subtitle=subtitle) + return DashHtmlPreview(div) if notebook else div + + class_name = self._label_name(label_idx) + if title is None: + title = f"Backend comparison — {class_name}" if class_name else "Backend comparison" + value_label = {"max_abs": "Contribution (÷ max |value|)", "sum_abs": "Share of Σ|value|"}.get( + normalize or "", "Contribution (raw)" + ) + if kind == "heatmap": + return plot_backend_heatmap(ref_tokens, shown, title=title, subtitle=subtitle, colorbar_title=value_label) + if kind == "bars": + return plot_backend_bars(ref_tokens, shown, title=title, subtitle=subtitle, xaxis_title=value_label) + raise ValueError(f"kind must be 'heatmap', 'bars' or 'highlight', got {kind!r}.") + + def _name_explanations( + self, others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation] + ) -> dict[str, NlpExplanation]: + """Reference first, then ``others``, under unique display labels. + + A default label is the backend name without its ``nlp_`` prefix; two explanations from the + same backend are told apart by ``output_space`` first (SHAP in probability vs logit space), + then by a counter. + """ + exp = self._exp + if isinstance(others, Mapping): + pairs: list[tuple[str | None, NlpExplanation]] = [(str(k), v) for k, v in others.items()] + elif isinstance(others, Sequence): + pairs = [(None, o) for o in others] + else: + pairs = [(None, others)] + pairs.insert(0, (None, exp)) + + defaults = [o.backend_name.removeprefix("nlp_") for _, o in pairs] + # Only entries left to their default label can clash by backend: a caller-chosen label is kept. + unnamed = [d for (given, _), d in zip(pairs, defaults, strict=True) if given is None] + named: dict[str, NlpExplanation] = {} + for (given, other), default in zip(pairs, defaults, strict=True): + if given is not None: + label = given + elif unnamed.count(default) > 1: + label = f"{default} ({other.output_space})" + else: + label = default + base, n = label, 2 + while label in named: + label, n = f"{base} #{n}", n + 1 + named[label] = other + return named + # ── batch-level plots ─────────────────────────────────────────────────────────────── def word_importance( self, diff --git a/shapash/plots/plot_backend_comparison.py b/shapash/plots/plot_backend_comparison.py new file mode 100644 index 00000000..f68597d6 --- /dev/null +++ b/shapash/plots/plot_backend_comparison.py @@ -0,0 +1,300 @@ +"""Side-by-side views of several NLP backends' contributions on the same text. + +Every function here takes the *already aligned* data — one list of reference word units and, per +backend, one value per unit (``NaN`` where that backend attributed nothing) — so alignment and +normalisation stay in :mod:`shapash.compute.token_alignment` and these stay pure renderers. +Reach them through :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.compare`. + +Three display options, each answering a slightly different question: + +- :func:`plot_backend_heatmap` — tokens × backends grid: *where* along the sentence do the + methods agree? The most compact view, and the only one that stays readable past ~4 backends. +- :func:`plot_backend_bars` — grouped bars per token: *how much* do they differ on a given word? +- :func:`plot_backend_highlight` — one highlighted copy of the sentence per backend: the familiar + text-highlight read, stacked so the eye can scan down a column. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +import numpy as np +from dash import html +from plotly import graph_objs as go + +from shapash.plots.plot_sentence_highlight import _parse_rgb, _shap_style +from shapash.style.style_utils import DEFAULT_NLP_THEME + +# Backend identity colors, assigned in this fixed order (never cycled) — the reference categorical +# palette's first slots, validated for colour-vision-deficiency separation. Only the grouped bar +# view needs identity colors; the other two encode the backend by row. +BACKEND_COLORS = ("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#4a3aa7", "#e34948") + +_NEUTRAL = "rgb(242, 242, 242)" +_MUTED_INK = "#8a8a8a" +_GRID = "#eeeeee" + + +def _check(tokens: Sequence[str], values_by_backend: Mapping[str, np.ndarray]) -> dict[str, np.ndarray]: + if not values_by_backend: + raise ValueError("values_by_backend is empty — pass at least one backend.") + out = {} + for name, values in values_by_backend.items(): + arr = np.asarray(values, dtype=float) + if arr.shape != (len(tokens),): + raise ValueError(f"values for {name!r} have shape {arr.shape}, expected ({len(tokens)},).") + out[name] = arr + return out + + +def _opaque(color: str) -> str: + r, g, b = _parse_rgb(color) + return f"rgb({r},{g},{b})" + + +def plot_backend_heatmap( + tokens: Sequence[str], + values_by_backend: Mapping[str, np.ndarray], + title: str = "Backend comparison", + subtitle: str | None = None, + colorbar_title: str = "Contribution", + width: int | None = None, + height: int | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, +) -> go.Figure: + """Tokens × backends grid, one diverging-colored cell per (backend, word). + + Parameters + ---------- + tokens : sequence of str + Reference word units, in sentence order (columns, left to right). + values_by_backend : mapping of str to np.ndarray + Backend label → 1-D aligned contributions, one per token (rows, top to bottom in mapping + order). ``NaN`` marks a unit the backend did not attribute; it is drawn as a ``·``. + title, subtitle : str + Figure title, and an optional smaller line under it (e.g. agreement scores). + colorbar_title : str + Colorbar label — say whether the values are normalised. + width, height : int, optional + Figure size in pixels. Default to a width that gives each token ~46px and a height that + gives each backend ~46px. + color_positive, color_negative : str + Poles of the diverging scale, around a neutral gray at zero. Same defaults as every other + NLP chart. + + Returns + ------- + go.Figure + """ + values_by_backend = _check(tokens, values_by_backend) + names = list(values_by_backend) + z = np.vstack([values_by_backend[n] for n in names]) + finite = z[np.isfinite(z)] + zmax = float(np.abs(finite).max()) if finite.size and np.abs(finite).max() > 0 else 1.0 + x = np.arange(len(tokens)) + + hover_tokens = np.tile(np.asarray(tokens, dtype=object), (len(names), 1)) + fig = go.Figure( + go.Heatmap( + z=z, + x=x, + y=names, + customdata=hover_tokens, + zmin=-zmax, + zmax=zmax, + zmid=0, + colorscale=[[0.0, _opaque(color_negative)], [0.5, _NEUTRAL], [1.0, _opaque(color_positive)]], + xgap=2, + ygap=2, + hoverongaps=False, + hovertemplate="<b>%{customdata}</b><br>%{y}: %{z:+.3f}<extra></extra>", + colorbar=dict(title=dict(text=colorbar_title, side="right"), thickness=12, outlinewidth=0), + ) + ) + + # Unattributed cells: an explicit mark rather than a blank, so "not attributed" never reads as + # "attributed zero". + rows, cols = np.where(~np.isfinite(z)) + if rows.size: + fig.add_trace( + go.Scatter( + x=cols, + y=[names[r] for r in rows], + mode="text", + text=["·"] * rows.size, + textfont=dict(color=_MUTED_INK, size=16), + customdata=[tokens[int(c)] for c in cols], + hovertemplate="<b>%{customdata}</b><br>%{y}: not attributed<extra></extra>", + showlegend=False, + ) + ) + + title_text = title if subtitle is None else f"{title}<br><sup>{subtitle}</sup>" + fig.update_layout( + title=dict(text=title_text, x=0.5), + width=width or max(600, 46 * len(tokens) + 220), + height=height or 46 * len(names) + 190, + plot_bgcolor="white", + xaxis=dict( + tickmode="array", + tickvals=x, + ticktext=list(tokens), + tickangle=-45, + showgrid=False, + zeroline=False, + ), + yaxis=dict(autorange="reversed", showgrid=False, automargin=True), + margin=dict(l=20, r=20, t=80, b=40), + ) + return fig + + +def plot_backend_bars( + tokens: Sequence[str], + values_by_backend: Mapping[str, np.ndarray], + title: str = "Backend comparison", + subtitle: str | None = None, + xaxis_title: str = "Contribution", + width: int = 900, + height: int | None = None, + colors: Sequence[str] = BACKEND_COLORS, +) -> go.Figure: + """Grouped horizontal bars: one group per word (sentence order, top to bottom), one bar per backend. + + Parameters + ---------- + tokens : sequence of str + Reference word units, in sentence order. + values_by_backend : mapping of str to np.ndarray + Backend label → 1-D aligned contributions, one per token. ``NaN`` units get no bar. + title, subtitle : str + Figure title, and an optional smaller line under it. + xaxis_title : str + Value-axis label — say whether the values are normalised. + width, height : int, optional + Figure size in pixels. ``height`` defaults to give each bar ~12px. + colors : sequence of str + Backend identity colors, assigned in order. More backends than colors raises: a generated + extra hue would not be distinguishable, so use :func:`plot_backend_heatmap` instead. + + Returns + ------- + go.Figure + """ + values_by_backend = _check(tokens, values_by_backend) + if len(values_by_backend) > len(colors): + raise ValueError( + f"{len(values_by_backend)} backends but only {len(colors)} identity colors — " + "use the heatmap view for this many." + ) + y = np.arange(len(tokens)) + fig = go.Figure() + for color, (name, values) in zip(colors, values_by_backend.items(), strict=False): + fig.add_trace( + go.Bar( + x=values, + y=y, + name=name, + orientation="h", + marker=dict(color=color, line=dict(color="white", width=1)), + customdata=list(tokens), + hovertemplate="<b>%{customdata}</b><br>" + name + ": %{x:+.3f}<extra></extra>", + ) + ) + + title_text = title if subtitle is None else f"{title}<br><sup>{subtitle}</sup>" + fig.update_layout( + title=dict(text=title_text, x=0.5), + barmode="group", + bargap=0.25, + bargroupgap=0.05, + width=width, + height=height or max(400, 12 * len(tokens) * len(values_by_backend) + 40 * len(tokens) // 2 + 160), + plot_bgcolor="white", + xaxis=dict(title=xaxis_title, zeroline=True, zerolinecolor="#333333", zerolinewidth=1, gridcolor=_GRID), + yaxis=dict(tickmode="array", tickvals=y, ticktext=list(tokens), autorange="reversed", automargin=True), + legend=dict(orientation="h", x=0.5, xanchor="center", y=1.0, yanchor="bottom", traceorder="normal"), + margin=dict(l=20, r=20, t=100, b=40), + ) + return fig + + +def plot_backend_highlight( + tokens: Sequence[str], + values_by_backend: Mapping[str, np.ndarray], + subtitle: str | None = None, + color_positive: str = DEFAULT_NLP_THEME.xpl_positive, + color_negative: str = DEFAULT_NLP_THEME.xpl_negative, +) -> html.Div: + """One highlighted copy of the sentence per backend, stacked. + + Each row is colored on its own magnitude scale (its largest unit is fully saturated), since + backends' raw values are in different units. Every row renders the same reference units with + the same padding, so a word sits at the same horizontal position in every row as long as the + rows wrap at the same width. + + Parameters + ---------- + tokens : sequence of str + Reference word units, in sentence order. + values_by_backend : mapping of str to np.ndarray + Backend label → 1-D aligned contributions. ``NaN`` units are drawn struck-through in gray. + subtitle : str, optional + A line shown under the legend (e.g. agreement scores). + color_positive, color_negative : str + ``rgb(...)``/``rgba(...)`` strings, as for + :func:`~shapash.plots.plot_sentence_highlight.plot_sentence_highlight`. + + Returns + ------- + html.Div + """ + values_by_backend = _check(tokens, values_by_backend) + pos_rgb, neg_rgb = _parse_rgb(color_positive), _parse_rgb(color_negative) + span_base = {"padding": "3px 5px", "borderRadius": "3px", "margin": "2px 1px", "display": "inline-block"} + unattributed = {**span_base, "color": "#aaaaaa", "textDecoration": "line-through"} + + rows = [] + for name, values in values_by_backend.items(): + finite = values[np.isfinite(values)] + max_abs = float(np.abs(finite).max()) if finite.size and np.abs(finite).max() > 0 else 1.0 + spans = [] + for tok, val in zip(tokens, values, strict=True): + if not np.isfinite(val): + spans.append(html.Span(f"{tok} ", style=unattributed, title=f"{tok}: not attributed")) + else: + style = {**span_base, **_shap_style(float(val), max_abs, pos_rgb, neg_rgb)} + spans.append(html.Span(f"{tok} ", style=style, title=f"{tok}: {float(val):+.4f}")) + rows.append( + html.Div( + [ + html.Div(name, style={"fontSize": "0.8em", "fontWeight": "600", "color": "#444"}), + html.Div(spans, style={"lineHeight": "2.2", "fontSize": "1.0em"}), + ], + style={"padding": "8px 12px", "borderBottom": "1px solid #eeeeee"}, + ) + ) + + legend = html.Div( + [ + html.Span("■ ", style={"color": color_positive}), + html.Span("positive ", style={"fontSize": "0.78em", "color": "#555"}), + html.Span("■ ", style={"color": color_negative}), + html.Span("negative ", style={"fontSize": "0.78em", "color": "#555"}), + html.Span( + "struck-through", style={"fontSize": "0.78em", "color": "#aaa", "textDecoration": "line-through"} + ), + html.Span(" not attributed", style={"fontSize": "0.78em", "color": "#555"}), + ], + style={"marginBottom": "4px"}, + ) + header = [legend] + if subtitle: + header.append(html.Div(subtitle, style={"fontSize": "0.78em", "color": "#555", "marginBottom": "6px"})) + return html.Div( + [ + *header, + html.Div(rows, style={"backgroundColor": "#fafafa", "border": "1px solid #eeeeee", "borderRadius": "4px"}), + ] + ) diff --git a/tests/unit_tests/compute/test_token_alignment.py b/tests/unit_tests/compute/test_token_alignment.py new file mode 100644 index 00000000..70c2f0d9 --- /dev/null +++ b/tests/unit_tests/compute/test_token_alignment.py @@ -0,0 +1,123 @@ +"""Unit tests for ``shapash.compute.token_alignment`` — putting backends' word units side by side. + +The token lists below are copied from real ``nlp_shap`` / ``nlp_lime`` / ``nlp_captum_lig`` runs on +``distilbert-base-uncased-emotion``: SHAP keeps case and punctuation, LIG lowercases, LIME emits a +case-sensitive bag of distinct words with punctuation dropped. +""" + +import numpy as np +import pytest + +from shapash.compute.token_alignment import ( + align_token_values, + backend_agreement, + locate_tokens, + normalize_contributions, +) + +TEXT = "The waiting room was cold and I felt nervous, really nervous, about the results." +SHAP_TOKENS = ["The", "waiting", "room", "was", "cold", "and", "I", "felt", "nervous", ","] +SHAP_TOKENS += ["really", "nervous", ",", "about", "the", "results", "."] +LIG_TOKENS = [t.lower() for t in SHAP_TOKENS] +LIME_TOKENS = ["The", "waiting", "room", "was", "cold", "and", "I", "felt", "nervous", "really", "about", "the"] +LIME_TOKENS += ["results"] + + +class TestLocateTokens: + def test_repeated_words_map_to_successive_occurrences(self): + spans = locate_tokens(TEXT, SHAP_TOKENS) + assert [TEXT[a:b] for a, b in spans] == SHAP_TOKENS + assert spans[8] != spans[11] # the two "nervous" + + def test_case_is_ignored(self): + assert locate_tokens(TEXT, LIG_TOKENS) == locate_tokens(TEXT, SHAP_TOKENS) + + def test_a_short_word_is_not_found_inside_a_longer_one(self): + # A plain substring search would place "i" inside "didn". + assert locate_tokens("I didn't, did i", ["didn", "i"]) == [(2, 6), (14, 15)] + + def test_contraction_pieces_still_match(self): + assert locate_tokens("didn't", ["didn", "'", "t"]) == [(0, 4), (4, 5), (5, 6)] + + def test_missing_token_gets_none_and_keeps_the_cursor(self): + assert locate_tokens("a b", ["a", "[SEP]", "b"]) == [(0, 1), None, (2, 3)] + + def test_unspaced_script_falls_back_to_substring_search(self): + assert locate_tokens("我喜欢你", ["我", "喜欢", "你"]) == [(0, 1), (1, 3), (3, 4)] + + +class TestAlignTokenValues: + def test_identical_tokenization_is_the_identity(self): + values = np.arange(len(SHAP_TOKENS), dtype=float) + np.testing.assert_array_equal(align_token_values(TEXT, SHAP_TOKENS, SHAP_TOKENS, values), values) + + def test_lowercased_tokenization_aligns_by_span(self): + values = np.arange(len(LIG_TOKENS), dtype=float) + np.testing.assert_array_equal(align_token_values(TEXT, SHAP_TOKENS, LIG_TOKENS, values), values) + + def test_bag_of_words_reaches_every_occurrence_and_skips_punctuation(self): + values = np.arange(1.0, len(LIME_TOKENS) + 1) + out = align_token_values(TEXT, SHAP_TOKENS, LIME_TOKENS, values) + lime = dict(zip(LIME_TOKENS, values, strict=True)) + for token, value in zip(SHAP_TOKENS, out, strict=True): + if token in {",", "."}: + assert np.isnan(value) + else: + assert value == lime[token] + # The second "nervous" has no LIME span of its own: it gets the one bag-of-words weight. + assert out[8] == out[11] == lime["nervous"] + + def test_bag_of_words_keeps_case_distinct_words_apart(self): + values = np.arange(1.0, len(LIME_TOKENS) + 1) + out = align_token_values(TEXT, SHAP_TOKENS, LIME_TOKENS, values) + assert out[0] == values[LIME_TOKENS.index("The")] + assert out[14] == values[LIME_TOKENS.index("the")] + + def test_rejects_mismatched_values(self): + with pytest.raises(ValueError, match="one entry per token"): + align_token_values("a b", ["a", "b"], ["a", "b"], np.array([1.0])) + + +class TestNormalize: + def test_max_abs(self): + np.testing.assert_allclose(normalize_contributions(np.array([2.0, -4.0, np.nan])), [0.5, -1.0, np.nan]) + + def test_sum_abs(self): + np.testing.assert_allclose(normalize_contributions(np.array([1.0, -3.0]), "sum_abs"), [0.25, -0.75]) + + def test_none_is_raw(self): + np.testing.assert_array_equal(normalize_contributions(np.array([2.0, -4.0]), None), [2.0, -4.0]) + + @pytest.mark.parametrize("values", [np.zeros(3), np.full(2, np.nan)]) + def test_degenerate_input_is_left_alone(self, values): + np.testing.assert_array_equal(normalize_contributions(values), values) + + def test_unknown_method(self): + with pytest.raises(ValueError, match="normalize"): + normalize_contributions(np.ones(2), "zscore") + + +class TestBackendAgreement: + def test_one_row_per_pair_and_identical_backends_agree_fully(self): + v = np.array([0.1, -0.5, 0.9, 0.0, -0.2]) + df = backend_agreement({"a": v, "b": v * 10, "c": -v}, top_k=2) + assert list(zip(df.backend_a, df.backend_b, strict=True)) == [("a", "b"), ("a", "c"), ("b", "c")] + ab = df.iloc[0] + assert ab.spearman == pytest.approx(1.0) + assert ab.sign_agreement == 1.0 + assert ab.top_k_overlap == 1.0 + assert df.iloc[1].spearman == pytest.approx(-1.0) + + def test_nan_units_are_excluded(self): + df = backend_agreement({"a": np.array([1.0, 2.0, 3.0]), "b": np.array([1.0, np.nan, 3.0])}, top_k=5) + row = df.iloc[0] + assert row.spearman == pytest.approx(1.0) + assert row.top_k_overlap == 1.0 # k shrinks to the 2 shared units + + def test_constant_backend_has_no_rank_correlation(self): + df = backend_agreement({"a": np.array([1.0, 2.0, 3.0]), "b": np.zeros(3)}) + assert np.isnan(df.iloc[0].spearman) + + def test_no_shared_unit_gives_nan(self): + df = backend_agreement({"a": np.array([1.0, np.nan]), "b": np.array([np.nan, 1.0])}) + assert df.iloc[0][["spearman", "sign_agreement", "top_k_overlap"]].isna().all() diff --git a/tests/unit_tests/explainer/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py index 6623f3fb..50f090a8 100644 --- a/tests/unit_tests/explainer/test_nlp_plotter.py +++ b/tests/unit_tests/explainer/test_nlp_plotter.py @@ -9,6 +9,7 @@ import copy import unittest +import warnings from dataclasses import replace import numpy as np @@ -366,5 +367,95 @@ def test_absent_in_scope_names_the_scope(self): self.explanation.plot.word_profile("happy", sample_indices=[3]) +class TestPlotterCompare(unittest.TestCase): + """``compare`` aligns other backends onto this one's units and hands the result to a renderer.""" + + def setUp(self): + self.ref = _make_explanation() + # A LIME-shaped artifact of the same texts: bag of words, its own order, a different scale. + self.lime = replace( + self.ref, + token_strings=[["world", "hello"], ["happy", "i"], ["ok"]], + values=[ + np.array([[5.0, -5.0], [3.0, -3.0]]), + np.array([[0.0, 8.0], [0.0, 2.0]]), + np.array([[1.0, -1.0]]), + ], + backend_name="nlp_lime", + is_additive=False, + ) + + def test_heatmap_aligns_the_other_backend_onto_reference_units(self): + fig = self.ref.plot.compare(self.lime, row=1, label_idx=1, normalize=None) + heat = fig.data[0] + self.assertEqual(list(heat.y), ["shap", "lime"]) + self.assertEqual(list(fig.layout.xaxis.ticktext), ["i", "am", "happy"]) + z = np.asarray(heat.z, dtype=float) + np.testing.assert_allclose(z[0], [0.2, 0.4, 0.6]) + np.testing.assert_allclose(z[1], [2.0, np.nan, 8.0]) + self.assertIn("pos", fig.layout.title.text) + self.assertIn("lime vs shap", fig.layout.title.text) + + def test_normalizes_by_default(self): + z = np.asarray(self.ref.plot.compare(self.lime, row=1, label_idx=1).data[0].z, dtype=float) + self.assertEqual(np.nanmax(np.abs(z[1])), 1.0) + + def test_default_class_is_the_predicted_one(self): + # Row 1 predicts "neg" (index 0); LIME gave class 0 all zeros. + z = np.asarray(self.ref.plot.compare(self.lime, row=1, normalize=None).data[0].z, dtype=float) + np.testing.assert_allclose(z[0], [0.1, 0.3, 0.5]) + np.testing.assert_allclose(z[1], [0.0, np.nan, 0.0]) + + def test_bars_and_highlight(self): + bars = self.ref.plot.compare([self.lime], row=0, kind="bars") + self.assertEqual([t.name for t in bars.data], ["shap", "lime"]) + self.assertIsInstance(self.ref.plot.compare(self.lime, kind="highlight"), html.Div) + self.assertIsInstance(self.ref.plot.compare(self.lime, kind="highlight", notebook=True), DashHtmlPreview) + + def test_max_tokens_keeps_the_strongest_units_in_sentence_order(self): + # Normalised: shap [0.33, 0.67, 1.0], lime [0.25, nan, 1.0] — "i" is weakest in both. + fig = self.ref.plot.compare(self.lime, row=1, label_idx=1, max_tokens=2) + self.assertEqual(list(fig.layout.xaxis.ticktext), ["am", "happy"]) + + def test_mapping_sets_labels_and_same_backend_is_told_apart_by_space(self): + logit = replace(self.ref, output_space="logit") + fig = self.ref.plot.compare({"LIME": self.lime}, row=0) + self.assertEqual(list(fig.data[0].y), ["shap", "LIME"]) + with self.assertWarns(UserWarning): + fig = self.ref.plot.compare(logit, row=0) + self.assertEqual(list(fig.data[0].y), ["shap (probability)", "shap (logit)"]) + fig = self.ref.plot.compare({"shap": replace(self.ref)}, row=0) + self.assertEqual(list(fig.data[0].y), ["shap", "shap #2"]) + + def test_warns_across_output_spaces_and_not_within_one(self): + lig = replace(self.lime, backend_name="nlp_captum_lig", output_space="logit") + with self.assertWarnsRegex(UserWarning, "shap=probability, captum_lig=logit"): + self.ref.plot.compare(lig, row=0) + with warnings.catch_warnings(): + warnings.simplefilter("error") + self.ref.plot.compare(self.lime, row=0) + + def test_rejects_a_different_text(self): + other = replace(self.lime, texts=self.ref.texts.replace("hello world", "goodbye world")) + with self.assertRaisesRegex(ValueError, "different text"): + self.ref.plot.compare(other, row=0) + + def test_rejects_different_classes(self): + other = replace(self.lime, label_names=["a", "b"]) + with self.assertRaisesRegex(ValueError, "different classes"): + self.ref.plot.compare(other, row=0) + + def test_rejects_unknown_kind(self): + with self.assertRaisesRegex(ValueError, "kind"): + self.ref.plot.compare(self.lime, kind="violin") + + def test_does_not_touch_either_artifact(self): + before = [copy.deepcopy(e.values) for e in (self.ref, self.lime)] + self.ref.plot.compare(self.lime, row=1, kind="bars") + for exp, values in zip((self.ref, self.lime), before, strict=True): + for a, b in zip(exp.values, values, strict=True): + np.testing.assert_array_equal(a, b) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/plots/test_plot_backend_comparison.py b/tests/unit_tests/plots/test_plot_backend_comparison.py new file mode 100644 index 00000000..8d271c52 --- /dev/null +++ b/tests/unit_tests/plots/test_plot_backend_comparison.py @@ -0,0 +1,94 @@ +"""Unit tests for the cross-backend comparison renderers.""" + +import numpy as np +import pytest +from dash import html +from plotly import graph_objs as go + +from shapash.plots.plot_backend_comparison import ( + BACKEND_COLORS, + plot_backend_bars, + plot_backend_heatmap, + plot_backend_highlight, +) + +TOKENS = ["I", "love", "it", "!"] +VALUES = {"shap": np.array([0.1, 0.9, -0.2, 0.05]), "lime": np.array([0.0, 1.0, -0.1, np.nan])} + + +def _texts(component) -> list[str]: + """Every string child under a Dash component, depth first.""" + children = getattr(component, "children", None) + if isinstance(children, str): + return [children] + if children is None: + return [] + if not isinstance(children, list): + children = [children] + return [s for child in children for s in _texts(child)] + + +class TestHeatmap: + def test_one_row_per_backend_one_column_per_token(self): + fig = plot_backend_heatmap(TOKENS, VALUES) + heat = fig.data[0] + assert isinstance(heat, go.Heatmap) + assert list(heat.y) == ["shap", "lime"] + assert np.asarray(heat.z).shape == (2, 4) + assert list(fig.layout.xaxis.ticktext) == TOKENS + + def test_scale_is_symmetric_around_zero(self): + heat = plot_backend_heatmap(TOKENS, VALUES).data[0] + assert heat.zmin == -heat.zmax == -1.0 + + def test_unattributed_cells_are_marked(self): + fig = plot_backend_heatmap(TOKENS, VALUES) + marks = fig.data[1] + assert list(marks.x) == [3] and list(marks.y) == ["lime"] + + def test_no_marker_trace_when_everything_is_attributed(self): + assert len(plot_backend_heatmap(TOKENS, {"shap": VALUES["shap"]}).data) == 1 + + def test_repeated_tokens_keep_their_own_column(self): + fig = plot_backend_heatmap(["a", "a"], {"x": np.array([1.0, -1.0])}) + assert list(fig.layout.xaxis.tickvals) == [0, 1] + + def test_subtitle(self): + assert "ρ=0.5" in plot_backend_heatmap(TOKENS, VALUES, subtitle="ρ=0.5").layout.title.text + + def test_rejects_misaligned_values(self): + with pytest.raises(ValueError, match="expected"): + plot_backend_heatmap(TOKENS, {"shap": np.ones(3)}) + + def test_rejects_no_backend(self): + with pytest.raises(ValueError, match="empty"): + plot_backend_heatmap(TOKENS, {}) + + +class TestBars: + def test_one_trace_per_backend_in_fixed_color_order(self): + fig = plot_backend_bars(TOKENS, VALUES) + assert [t.name for t in fig.data] == ["shap", "lime"] + assert [t.marker.color for t in fig.data] == list(BACKEND_COLORS[:2]) + assert fig.layout.barmode == "group" + + def test_refuses_more_backends_than_colors(self): + many = {str(i): np.zeros(len(TOKENS)) for i in range(3)} + with pytest.raises(ValueError, match="heatmap"): + plot_backend_bars(TOKENS, many, colors=BACKEND_COLORS[:2]) + + +class TestHighlight: + def test_one_row_per_backend_with_every_token(self): + div = plot_backend_highlight(TOKENS, VALUES, subtitle="agreement") + assert isinstance(div, html.Div) + texts = _texts(div) + assert "shap" in texts and "lime" in texts and "agreement" in texts + assert texts.count("love ") == 2 + + def test_unattributed_token_is_struck_through(self): + div = plot_backend_highlight(TOKENS, VALUES) + rows = div.children[-1].children + lime_spans = rows[1].children[1].children + assert lime_spans[3].style["textDecoration"] == "line-through" + assert lime_spans[3].title == "!: not attributed" From 24c0aec39cd67fc16bf02e48b4dc4475bf423faa Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Thu, 1 Oct 2026 16:33:48 +0200 Subject: [PATCH 134/144] test: fix test_nlp_plotter replacing assertWarns with pytest.warns the two tests used assertWarns, which loops over every module in sys.modules and reads __warningregistry__. Once other tests have loaded transformers, that read triggers its lazy imports, and one of them pulls in torchvision which is not installed --- tests/unit_tests/explainer/test_nlp_plotter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/explainer/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py index 50f090a8..3d55f196 100644 --- a/tests/unit_tests/explainer/test_nlp_plotter.py +++ b/tests/unit_tests/explainer/test_nlp_plotter.py @@ -14,6 +14,7 @@ import numpy as np import pandas as pd +import pytest from dash import html from plotly import graph_objs as go @@ -421,7 +422,8 @@ def test_mapping_sets_labels_and_same_backend_is_told_apart_by_space(self): logit = replace(self.ref, output_space="logit") fig = self.ref.plot.compare({"LIME": self.lime}, row=0) self.assertEqual(list(fig.data[0].y), ["shap", "LIME"]) - with self.assertWarns(UserWarning): + # pytest.warns, not assertWarns: the latter walks sys.modules and trips transformers' lazy imports + with pytest.warns(UserWarning): fig = self.ref.plot.compare(logit, row=0) self.assertEqual(list(fig.data[0].y), ["shap (probability)", "shap (logit)"]) fig = self.ref.plot.compare({"shap": replace(self.ref)}, row=0) @@ -429,7 +431,7 @@ def test_mapping_sets_labels_and_same_backend_is_told_apart_by_space(self): def test_warns_across_output_spaces_and_not_within_one(self): lig = replace(self.lime, backend_name="nlp_captum_lig", output_space="logit") - with self.assertWarnsRegex(UserWarning, "shap=probability, captum_lig=logit"): + with pytest.warns(UserWarning, match="shap=probability, captum_lig=logit"): self.ref.plot.compare(lig, row=0) with warnings.catch_warnings(): warnings.simplefilter("error") From 74c47540217a266e7418671925871d031cb78a8b Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 5 Oct 2026 14:39:01 +0200 Subject: [PATCH 135/144] feat(nlp): add corpus-level backend agreement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - corpus_agreement() scores every text × backend pair; plot.compare_corpus() shows the distribution - add pearson, cosine and per-backend top-word share to the agreement scores - rename compute/token_alignment.py to compute/backend_comparison.py; add explainer/nlp_comparison.py --- ...ken_alignment.py => backend_comparison.py} | 62 ++++- shapash/explainer/nlp_comparison.py | 213 ++++++++++++++++++ shapash/explainer/nlp_plotter.py | 156 +++++++------ shapash/plots/plot_backend_comparison.py | 96 +++++++- ...lignment.py => test_backend_comparison.py} | 39 +++- .../explainer/test_nlp_comparison.py | 88 ++++++++ .../unit_tests/explainer/test_nlp_plotter.py | 26 +++ .../plots/test_plot_backend_comparison.py | 51 +++++ 8 files changed, 649 insertions(+), 82 deletions(-) rename shapash/compute/{token_alignment.py => backend_comparison.py} (74%) create mode 100644 shapash/explainer/nlp_comparison.py rename tests/unit_tests/compute/{test_token_alignment.py => test_backend_comparison.py} (74%) create mode 100644 tests/unit_tests/explainer/test_nlp_comparison.py diff --git a/shapash/compute/token_alignment.py b/shapash/compute/backend_comparison.py similarity index 74% rename from shapash/compute/token_alignment.py rename to shapash/compute/backend_comparison.py index e9e9e6e1..b7ca352b 100644 --- a/shapash/compute/token_alignment.py +++ b/shapash/compute/backend_comparison.py @@ -1,6 +1,8 @@ -"""Align per-token contributions from different NLP backends onto one shared set of word units. +"""Compare NLP backends' contributions on one text, at the array level: align, rescale, score agreement. -Backends do not agree on what a "token" is, even after each one merges subwords back into words: +This module knows nothing about explanations; the layer that reads them is +:mod:`shapash.explainer.nlp_comparison`. Its first job is alignment, since backends do not +agree on what a "token" is, even after each one merges subwords back into words: - ``nlp_shap`` and ``nlp_captum_lig`` emit words in sentence order, punctuation as its own unit — but LIG reads them back off the tokenizer, so an uncased model hands back ``"the"`` where SHAP @@ -163,6 +165,20 @@ def normalize_contributions(values: np.ndarray, method: str | None = "max_abs") return values / scale if scale > 0 else values +# Columns of :func:`backend_agreement`, in order. +AGREEMENT_SCORES = [ + "backend_a", + "backend_b", + "spearman", + "pearson", + "cosine", + "sign_agreement", + "top_k_overlap", + "top_share_a", + "top_share_b", +] + + def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.DataFrame: """Pairwise agreement between backends' aligned contributions on one text. @@ -176,11 +192,25 @@ def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.D Returns ------- pd.DataFrame - One row per unordered backend pair, with columns ``backend_a``, ``backend_b``, - ``spearman`` (rank correlation of the signed values, over the units both attributed), - ``sign_agreement`` (share of those units where both signs match) and ``top_k_overlap`` - (share of the ``top_k`` highest-magnitude units the two have in common). Scale-free by - construction, so it is safe across backends with different output spaces. + One row per unordered backend pair. Scores over the units both backends attributed: + + - ``spearman`` — rank correlation of the signed values. Every unit counts the same, so + when one word carries most of the attribution, the order of the near-zero rest decides it. + - ``pearson`` — linear correlation of the signed values: each unit counts by its size, but + measured around each backend's mean, so a shift both backends share (every word slightly + positive) does not count as agreement. + - ``cosine`` — cosine similarity of the signed values, i.e. Pearson around zero rather than + around the mean: zero is "no effect" for an attribution, so a shared shift does count. + Each unit counts by its size, so it asks whether the two tell the same story on the + words that matter. + - ``sign_agreement`` — share of units where both signs match. + - ``top_k_overlap`` — share of the ``top_k`` highest-magnitude units the two have in common. + + And, per backend, over the units it attributed: ``top_share_a`` / ``top_share_b``, the + largest unit's share of the total magnitude — how concentrated the attribution is, which + says how much to trust ``spearman`` against ``cosine``. + + All are scale-free, so they are safe across backends with different output spaces. """ names = list(aligned) rows = [] @@ -189,10 +219,13 @@ def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.D va, vb = np.asarray(aligned[a], dtype=float), np.asarray(aligned[b], dtype=float) both = np.isfinite(va) & np.isfinite(vb) xa, xb = va[both], vb[both] - # A rank correlation is undefined when either side is constant (e.g. LIME all zeros on a + # A correlation is undefined when either side is constant (e.g. LIME all zeros on a # class it did not select a word for); say so with NaN rather than a numpy warning. ranked = both.sum() > 1 and np.ptp(xa) > 0 and np.ptp(xb) > 0 spearman = float(pd.Series(xa).corr(pd.Series(xb), method="spearman")) if ranked else np.nan + pearson = float(np.corrcoef(xa, xb)[0, 1]) if ranked else np.nan + norm = float(np.linalg.norm(xa) * np.linalg.norm(xb)) + cosine = float(xa @ xb) / norm if norm > 0 else np.nan signs = float(np.mean(np.sign(xa) == np.sign(xb))) if both.any() else np.nan k = min(top_k, int(both.sum())) if k > 0: @@ -206,8 +239,19 @@ def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.D "backend_a": a, "backend_b": b, "spearman": spearman, + "pearson": pearson, + "cosine": cosine, "sign_agreement": signs, "top_k_overlap": overlap, + "top_share_a": _top_share(va), + "top_share_b": _top_share(vb), } ) - return pd.DataFrame(rows, columns=["backend_a", "backend_b", "spearman", "sign_agreement", "top_k_overlap"]) + return pd.DataFrame(rows, columns=AGREEMENT_SCORES) + + +def _top_share(values: np.ndarray) -> float: + """The largest attributed unit's share of the total magnitude; ``NaN`` when there is none.""" + magnitude = np.abs(values[np.isfinite(values)]) + total = magnitude.sum() + return float(magnitude.max() / total) if total > 0 else np.nan diff --git a/shapash/explainer/nlp_comparison.py b/shapash/explainer/nlp_comparison.py new file mode 100644 index 00000000..d4ed5999 --- /dev/null +++ b/shapash/explainer/nlp_comparison.py @@ -0,0 +1,213 @@ +"""Put several :class:`~shapash.explainer.nlp_explanation.NlpExplanation` objects of the same texts side by side. + +The array-level work — aligning word units, rescaling, scoring agreement — lives in +:mod:`shapash.compute.backend_comparison`, which knows nothing about explanations. This module is the +layer above it: it names the explanations, checks they explain the same texts and classes, warns +when they are in different output spaces, and reads one text's values out of each. +:meth:`NlpPlotter.compare <shapash.explainer.nlp_plotter.NlpPlotter.compare>` uses it for one +text, :func:`corpus_agreement` for many. +""" + +from __future__ import annotations + +import warnings +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +from shapash.compute.backend_comparison import AGREEMENT_SCORES, align_token_values, backend_agreement +from shapash.explainer.nlp_explanation import select_label_column + +if TYPE_CHECKING: + from shapash.explainer.nlp_explanation import NlpExplanation + +AGREEMENT_COLUMNS = ["row", "text", "label", *AGREEMENT_SCORES] + + +def name_explanations( + reference: NlpExplanation, + others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation], +) -> dict[str, NlpExplanation]: + """Reference first, then ``others``, under unique display labels. + + A default label is the backend name without its ``nlp_`` prefix; two explanations from the + same backend are told apart by ``output_space`` first (SHAP in probability vs logit space), + then by a counter. A label given in a mapping is kept. + """ + if isinstance(others, Mapping): + pairs: list[tuple[str | None, NlpExplanation]] = [(str(k), v) for k, v in others.items()] + elif isinstance(others, Sequence): + pairs = [(None, o) for o in others] + else: + pairs = [(None, others)] + pairs.insert(0, (None, reference)) + + defaults = [o.backend_name.removeprefix("nlp_") for _, o in pairs] + # Only entries left to their default label can clash by backend: a caller-chosen label is kept. + unnamed = [d for (given, _), d in zip(pairs, defaults, strict=True) if given is None] + named: dict[str, NlpExplanation] = {} + for (given, other), default in zip(pairs, defaults, strict=True): + if given is not None: + label = given + elif unnamed.count(default) > 1: + label = f"{default} ({other.output_space})" + else: + label = default + base, n = label, 2 + while label in named: + label, n = f"{base} #{n}", n + 1 + named[label] = other + return named + + +def check_comparable(named: Mapping[str, NlpExplanation], stacklevel: int = 2) -> None: + """Raise if the explanations explain different classes; warn if they are in different output spaces. + + Parameters + ---------- + named : mapping of str to NlpExplanation + Reference first, as returned by :func:`name_explanations`. + stacklevel : int + Passed to :func:`warnings.warn`, so the warning points at the caller's line. + """ + ref_name, reference = next(iter(named.items())) + for name, other in named.items(): + if other.n_classes != reference.n_classes or ( + other.label_names and reference.label_names and list(other.label_names) != list(reference.label_names) + ): + raise ValueError(f"{name!r} explains different classes than the reference explanation {ref_name!r}.") + + spaces = {name: other.output_space for name, other in named.items()} + if len(set(spaces.values())) > 1: + listing = ", ".join(f"{name}={space}" for name, space in spaces.items()) + warnings.warn( + f"Comparing backends across output spaces ({listing}). Normalisation removes the scale gap " + "but not the softmax non-linearity, so rankings and agreement scores mix method and space " + "differences. For a like-for-like comparison with a logit-space backend, use " + "NlpShapBackend(..., output_space='logit').", + UserWarning, + stacklevel=stacklevel + 1, + ) + + +def predicted_label_idx(explanation: NlpExplanation, row: int) -> int: + """Column index of the class ``explanation`` predicted for ``row`` (0 when it cannot be resolved).""" + return explanation.label_to_idx.get(str(explanation.y_pred.iloc[row]), 0) + + +def align_row(named: Mapping[str, NlpExplanation], row: int, label_idx: int) -> tuple[list[str], dict[str, np.ndarray]]: + """One text's contributions for one class, every backend aligned onto the reference's word units. + + Parameters + ---------- + named : mapping of str to NlpExplanation + Reference first, as returned by :func:`name_explanations`. + row : int + Positional index of the text; it must be the same text in every explanation. + label_idx : int + Output column to read. + + Returns + ------- + tuple of (list of str, dict of str to np.ndarray) + The reference's word units, and per label the aligned 1-D values (``NaN`` = not attributed). + """ + reference = next(iter(named.values())) + if not -len(reference) <= row < len(reference): + raise IndexError(f"row={row} is out of range for {len(reference)} sample(s).") + text = str(reference.texts.iloc[row]) + ref_tokens = list(reference.token_strings[row]) + ref_values = np.asarray(select_label_column(reference.values[row], label_idx), dtype=float) + + aligned: dict[str, np.ndarray] = {} + for name, other in named.items(): + if other is reference: + aligned[name] = ref_values + continue + if len(other) != len(reference) or str(other.texts.iloc[row]) != text: + raise ValueError(f"Row {row} holds a different text in {name!r} than in the reference explanation.") + values = select_label_column(other.values[row], label_idx) + aligned[name] = align_token_values(text, ref_tokens, other.token_strings[row], values) + return ref_tokens, aligned + + +def corpus_agreement( + reference: NlpExplanation, + others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation], + rows: Sequence[int] | None = None, + label_idx: int | None = None, + top_k: int = 5, +) -> pd.DataFrame: + """Agreement between backends on every text of a corpus (or a sample of it), one line per text and pair. + + The per-text scores are those :meth:`NlpPlotter.compare + <shapash.explainer.nlp_plotter.NlpPlotter.compare>` shows in its subtitle; collecting them over + many texts shows how far two backends agree *in general* on this model and data, and which texts + they disagree on most — the ones worth opening with ``compare(row=...)``. Nothing is recomputed: + every explanation must already hold the texts. + + Parameters + ---------- + reference : NlpExplanation + The explanation whose word units the others are aligned onto. Prefer a sequence backend + (SHAP, LIG) over LIME's bag of words. + others : NlpExplanation, sequence of NlpExplanation, or mapping of str to NlpExplanation + Explanations of the same texts by other backends. Labels as in ``compare``. + rows : sequence of int, optional + Positional indices of the texts to score. ``None`` (default) scores every text. + label_idx : int, optional + Class to compare on. ``None`` (default) uses each text's predicted class (by the reference). + top_k : int + Size of the top set in ``top_k_overlap``. + + Returns + ------- + pd.DataFrame + Columns ``row``, ``text``, ``label`` (the class compared), ``backend_a``, ``backend_b``, + ``spearman``, ``pearson``, ``cosine``, ``sign_agreement``, ``top_k_overlap``, ``top_share_a``, + ``top_share_b`` — see :func:`~shapash.compute.backend_comparison.backend_agreement`. A low + ``spearman`` with a high ``cosine`` and a high top share means the backends agree on the + word that matters and only order the small rest differently. Every pair of backends is scored, + not only pairs with the reference. Aggregate with ``groupby(["backend_a", "backend_b"])``. + + Warns + ----- + UserWarning + When the explanations are in different output spaces (see ``compare``). + + Examples + -------- + >>> scores = corpus_agreement(shap_exp, {"LIG": lig_exp}, rows=range(50)) + >>> scores.groupby(["backend_a", "backend_b"])[["spearman", "top_k_overlap"]].median() + >>> scores.nsmallest(5, "spearman")[["row", "text", "spearman"]] # where they disagree most + """ + named = name_explanations(reference, others) + check_comparable(named, stacklevel=2) + return score_rows(named, rows=rows, label_idx=label_idx, top_k=top_k) + + +def score_rows( + named: Mapping[str, NlpExplanation], + rows: Sequence[int] | None = None, + label_idx: int | None = None, + top_k: int = 5, +) -> pd.DataFrame: + """The body of :func:`corpus_agreement`, on explanations already named and checked.""" + reference = next(iter(named.values())) + rows = range(len(reference)) if rows is None else rows + names = reference.label_names + + frames = [] + for row in rows: + idx = predicted_label_idx(reference, row) if label_idx is None else label_idx % reference.n_classes + _, aligned = align_row(named, row, idx) + scores = backend_agreement(aligned, top_k=top_k) + scores.insert(0, "label", names[idx] if names is not None and idx < len(names) else idx) + scores.insert(0, "text", str(reference.texts.iloc[row])) + scores.insert(0, "row", int(row)) + frames.append(scores) + if not frames: + return pd.DataFrame(columns=AGREEMENT_COLUMNS) + return pd.concat(frames, ignore_index=True)[AGREEMENT_COLUMNS] diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index ded7dff9..c730a9ff 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -18,7 +18,6 @@ from __future__ import annotations -import warnings from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal @@ -26,15 +25,27 @@ from dash import html from plotly import graph_objs as go +from shapash.compute.backend_comparison import backend_agreement, normalize_contributions from shapash.compute.embeddings import Embedding, projection_coords -from shapash.compute.token_alignment import align_token_values, backend_agreement, normalize_contributions +from shapash.explainer.nlp_comparison import ( + align_row, + check_comparable, + name_explanations, + predicted_label_idx, + score_rows, +) from shapash.explainer.nlp_explanation import ( WORD_AGGREGATIONS, aggregate_word_contributions, select_label_column, word_contributions_by_sample, ) -from shapash.plots.plot_backend_comparison import plot_backend_bars, plot_backend_heatmap, plot_backend_highlight +from shapash.plots.plot_backend_comparison import ( + plot_backend_agreement, + plot_backend_bars, + plot_backend_heatmap, + plot_backend_highlight, +) from shapash.plots.plot_confusion_matrix import plot_confusion_matrix from shapash.plots.plot_scatter import plot_scatter from shapash.plots.plot_sentence_highlight import plot_sentence_highlight @@ -258,7 +269,7 @@ def compare( """Compare this explanation's contributions on one text with other backends' on the same text. This explanation is the *reference*: its word units are the ones displayed, and every other - backend's values are aligned onto them (see :mod:`shapash.compute.token_alignment` — LIME's + backend's values are aligned onto them (see :mod:`shapash.compute.backend_comparison` — LIME's bag-of-words vocabulary does not line up positionally with SHAP's or LIG's word sequence). Pick a sequence backend (SHAP, LIG) as the reference so every occurrence of a word is shown. @@ -277,7 +288,7 @@ def compare( ``"highlight"`` — one highlighted sentence per backend (a Dash component). normalize : {"max_abs", "sum_abs", None} How each backend's values are rescaled before display (see - :func:`~shapash.compute.token_alignment.normalize_contributions`). Backends explain + :func:`~shapash.compute.backend_comparison.normalize_contributions`). Backends explain different quantities (probability, logit, surrogate weights), so the default scales each to ``[-1, 1]``; ``None`` shows raw values, which is only meaningful between backends sharing an ``output_space``. @@ -308,7 +319,7 @@ def compare( The subtitle reports, for each other backend against the reference, the Spearman rank correlation and top-k overlap of the aligned values (see - :func:`~shapash.compute.token_alignment.backend_agreement`) — both scale-free, so they do + :func:`~shapash.compute.backend_comparison.backend_agreement`) — both scale-free, so they do not depend on ``normalize``. Examples @@ -316,38 +327,12 @@ def compare( >>> shap_exp.plot.compare({"LIME": lime_exp, "LIG": lig_exp}, row=3, kind="heatmap").show() """ exp = self._exp - named = self._name_explanations(others) + named = name_explanations(exp, others) + check_comparable(named, stacklevel=2) if label_idx is None: - label_idx = exp.label_to_idx.get(str(exp.y_pred.iloc[row]), 0) + label_idx = predicted_label_idx(exp, row) label_idx = self._check_label_idx(label_idx) - ref_tokens, ref_values, _ = self._slice(row, label_idx) - text = str(exp.texts.iloc[row]) - - spaces = {name: other.output_space for name, other in named.items()} - if len(set(spaces.values())) > 1: - listing = ", ".join(f"{name}={space}" for name, space in spaces.items()) - warnings.warn( - f"Comparing backends across output spaces ({listing}). Normalisation removes the scale gap " - "but not the softmax non-linearity, so rankings and agreement scores mix method and space " - "differences. For a like-for-like comparison with a logit-space backend, use " - "NlpShapBackend(..., output_space='logit').", - UserWarning, - stacklevel=2, - ) - - aligned: dict[str, np.ndarray] = {} - for name, other in named.items(): - if str(other.texts.iloc[row]) != text: - raise ValueError(f"Row {row} holds a different text in {name!r} than in the reference explanation.") - if other.n_classes != exp.n_classes or ( - other.label_names and exp.label_names and list(other.label_names) != list(exp.label_names) - ): - raise ValueError(f"{name!r} explains different classes than the reference explanation.") - if other is exp: - aligned[name] = ref_values - else: - values = select_label_column(other.values[row], label_idx) - aligned[name] = align_token_values(text, ref_tokens, other.token_strings[row], values) + ref_tokens, aligned = align_row(named, row, label_idx) agreement = backend_agreement(aligned, top_k=top_k) ref_name = next(iter(named)) @@ -380,40 +365,75 @@ def compare( return plot_backend_bars(ref_tokens, shown, title=title, subtitle=subtitle, xaxis_title=value_label) raise ValueError(f"kind must be 'heatmap', 'bars' or 'highlight', got {kind!r}.") - def _name_explanations( - self, others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation] - ) -> dict[str, NlpExplanation]: - """Reference first, then ``others``, under unique display labels. + def compare_corpus( + self, + others: NlpExplanation | Sequence[NlpExplanation] | Mapping[str, NlpExplanation], + rows: Sequence[int] | None = None, + label_idx: int | None = None, + metric: Literal["spearman", "pearson", "cosine", "sign_agreement", "top_k_overlap"] = "spearman", + top_k: int = 5, + title: str | None = None, + ) -> go.Figure: + """How far backends agree over many texts: one dot per text, one row per backend pair. + + The corpus-level counterpart of :meth:`compare`: the score in ``compare``'s subtitle, + computed for every text in ``rows`` (see + :func:`~shapash.explainer.nlp_comparison.corpus_agreement` for the table itself). Run it + on a sample to see whether two backends tell the same story on this model before choosing + one; hover a low dot for its row and open it with ``compare(row=...)``. - A default label is the backend name without its ``nlp_`` prefix; two explanations from the - same backend are told apart by ``output_space`` first (SHAP in probability vs logit space), - then by a counter. + Parameters + ---------- + others : NlpExplanation, sequence of NlpExplanation, or mapping of str to NlpExplanation + Explanations of the same texts by other backends, labelled as in :meth:`compare`. + rows : sequence of int, optional + Positional indices of the texts to score. ``None`` (default) scores every text. + label_idx : int, optional + Class to compare on. ``None`` (default) uses each text's predicted class. + metric : {"spearman", "pearson", "cosine", "sign_agreement", "top_k_overlap"} + The score to plot. ``"spearman"`` asks whether every word is ordered the same; + ``"pearson"`` and ``"cosine"`` whether the words that carry the attribution agree (see + :func:`~shapash.compute.backend_comparison.backend_agreement` for how they differ). + top_k : int + Size of the top set, for ``metric="top_k_overlap"``. + title : str, optional + Overrides the default title. + + Returns + ------- + plotly.graph_objs.Figure + + Notes + ----- + Every pair of backends is plotted, not only pairs with this (the reference) explanation. + Texts on which a score is undefined — Spearman on a backend that gave every word the same + value — are left out of that pair's row. Mixed output spaces warn, as in :meth:`compare`. + + Examples + -------- + >>> shap_exp.plot.compare_corpus({"LIG": lig_exp, "LIME": lime_exp}, rows=range(100)).show() """ - exp = self._exp - if isinstance(others, Mapping): - pairs: list[tuple[str | None, NlpExplanation]] = [(str(k), v) for k, v in others.items()] - elif isinstance(others, Sequence): - pairs = [(None, o) for o in others] - else: - pairs = [(None, others)] - pairs.insert(0, (None, exp)) - - defaults = [o.backend_name.removeprefix("nlp_") for _, o in pairs] - # Only entries left to their default label can clash by backend: a caller-chosen label is kept. - unnamed = [d for (given, _), d in zip(pairs, defaults, strict=True) if given is None] - named: dict[str, NlpExplanation] = {} - for (given, other), default in zip(pairs, defaults, strict=True): - if given is not None: - label = given - elif unnamed.count(default) > 1: - label = f"{default} ({other.output_space})" - else: - label = default - base, n = label, 2 - while label in named: - label, n = f"{base} #{n}", n + 1 - named[label] = other - return named + if label_idx is not None: + label_idx = self._check_label_idx(label_idx) + named = name_explanations(self._exp, others) + check_comparable(named, stacklevel=2) + agreement = score_rows(named, rows=rows, label_idx=label_idx, top_k=top_k) + + axis_title = { + "spearman": "Spearman ρ", + "pearson": "Pearson r", + "cosine": "Cosine similarity", + "sign_agreement": "Sign agreement", + }.get(metric, f"Top-{top_k} overlap") + medians = agreement.groupby(["backend_a", "backend_b"], sort=False)[metric].median() + n_texts = agreement["row"].nunique() + subtitle = f"{n_texts} text{'s' if n_texts != 1 else ''} · median: " + ", ".join( + f"{b} vs {a} {m:.2f}" for (a, b), m in medians.items() + ) + if title is None: + class_name = self._label_name(label_idx) if label_idx is not None else None + title = f"Backend agreement — {class_name}" if class_name else "Backend agreement — predicted class" + return plot_backend_agreement(agreement, metric=metric, axis_title=axis_title, title=title, subtitle=subtitle) # ── batch-level plots ─────────────────────────────────────────────────────────────── def word_importance( diff --git a/shapash/plots/plot_backend_comparison.py b/shapash/plots/plot_backend_comparison.py index f68597d6..6402d801 100644 --- a/shapash/plots/plot_backend_comparison.py +++ b/shapash/plots/plot_backend_comparison.py @@ -2,7 +2,7 @@ Every function here takes the *already aligned* data — one list of reference word units and, per backend, one value per unit (``NaN`` where that backend attributed nothing) — so alignment and -normalisation stay in :mod:`shapash.compute.token_alignment` and these stay pure renderers. +normalisation stay in :mod:`shapash.compute.backend_comparison` and these stay pure renderers. Reach them through :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.compare`. Three display options, each answering a slightly different question: @@ -12,6 +12,9 @@ - :func:`plot_backend_bars` — grouped bars per token: *how much* do they differ on a given word? - :func:`plot_backend_highlight` — one highlighted copy of the sentence per backend: the familiar text-highlight read, stacked so the eye can scan down a column. + +:func:`plot_backend_agreement` is the corpus-level counterpart: one agreement score per text, +reached through :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.compare_corpus`. """ from __future__ import annotations @@ -19,6 +22,7 @@ from collections.abc import Mapping, Sequence import numpy as np +import pandas as pd from dash import html from plotly import graph_objs as go @@ -298,3 +302,93 @@ def plot_backend_highlight( html.Div(rows, style={"backgroundColor": "#fafafa", "border": "1px solid #eeeeee", "borderRadius": "4px"}), ] ) + + +def plot_backend_agreement( + agreement: pd.DataFrame, + metric: str = "spearman", + axis_title: str | None = None, + title: str = "Backend agreement across texts", + subtitle: str | None = None, + width: int = 900, + height: int | None = None, + color: str = BACKEND_COLORS[0], +) -> go.Figure: + """Distribution of one agreement score over many texts: a box plus one dot per text, per backend pair. + + Answers "how much do these backends agree in general, and on which texts do they not?". The box + gives the median and quartiles; each dot is a text, and hovering it gives its row and an + excerpt, to open with ``compare(row=...)``. + + Parameters + ---------- + agreement : pd.DataFrame + One line per (text, backend pair), with columns ``row``, ``text``, ``label``, + ``backend_a``, ``backend_b`` and ``metric`` — as returned by + :func:`~shapash.explainer.nlp_comparison.corpus_agreement`. Lines whose score is + ``NaN`` (undefined for that text) are not drawn. + metric : {"spearman", "pearson", "cosine", "sign_agreement", "top_k_overlap"} + Which score to show. The three correlations' axis spans ``[-1, 1]``, the two shares ``[0, 1]``. + axis_title : str, optional + Score-axis label. Defaults to the column name. + title, subtitle : str + Figure title, and an optional smaller line under it. + width, height : int, optional + Figure size in pixels. ``height`` defaults to ~70px per pair. + color : str + One color for every pair: the pairs are told apart by their row, not by hue. + + Returns + ------- + go.Figure + """ + signed = metric in {"spearman", "pearson", "cosine"} + if not signed and metric not in {"sign_agreement", "top_k_overlap"}: + raise ValueError(f"metric must be 'spearman', 'cosine', 'sign_agreement' or 'top_k_overlap', got {metric!r}.") + missing = {"row", "text", "label", "backend_a", "backend_b", metric} - set(agreement.columns) + if missing: + raise ValueError(f"agreement is missing column(s) {sorted(missing)}.") + + pairs = list(dict.fromkeys(zip(agreement["backend_a"], agreement["backend_b"], strict=True))) + fig = go.Figure() + for a, b in pairs: + scores = agreement[(agreement["backend_a"] == a) & (agreement["backend_b"] == b)].dropna(subset=[metric]) + excerpts = [t if len(t) <= 80 else t[:77] + "…" for t in scores["text"].astype(str)] + fig.add_trace( + go.Box( + x=scores[metric], + name=f"{b} vs {a}", + orientation="h", + boxpoints="all", + jitter=0.5, + pointpos=0, + hoveron="points", + line=dict(color=color, width=1.5), + fillcolor="rgba(0,0,0,0)", + marker=dict(color=color, size=8, opacity=0.6, line=dict(color="white", width=1)), + customdata=np.column_stack([scores["row"], scores["label"].astype(str), excerpts]) + if len(scores) + else None, + hovertemplate="<b>row %{customdata[0]}</b> · %{customdata[1]}<br>%{customdata[2]}" + "<br>%{x:.2f}<extra></extra>", + showlegend=False, + ) + ) + + title_text = title if subtitle is None else f"{title}<br><sup>{subtitle}</sup>" + fig.update_layout( + title=dict(text=title_text, x=0.5), + width=width, + height=height or 70 * len(pairs) + 200, + plot_bgcolor="white", + xaxis=dict( + title=axis_title or metric, + range=[-1.05, 1.05] if signed else [-0.03, 1.03], + gridcolor=_GRID, + zeroline=signed, + zerolinecolor="#bbbbbb", + ), + yaxis=dict(autorange="reversed", automargin=True, showgrid=False), + margin=dict(l=20, r=20, t=80, b=40), + ) + return fig diff --git a/tests/unit_tests/compute/test_token_alignment.py b/tests/unit_tests/compute/test_backend_comparison.py similarity index 74% rename from tests/unit_tests/compute/test_token_alignment.py rename to tests/unit_tests/compute/test_backend_comparison.py index 70c2f0d9..353b49b1 100644 --- a/tests/unit_tests/compute/test_token_alignment.py +++ b/tests/unit_tests/compute/test_backend_comparison.py @@ -1,4 +1,4 @@ -"""Unit tests for ``shapash.compute.token_alignment`` — putting backends' word units side by side. +"""Unit tests for ``shapash.compute.backend_comparison`` — putting backends' word units side by side. The token lists below are copied from real ``nlp_shap`` / ``nlp_lime`` / ``nlp_captum_lig`` runs on ``distilbert-base-uncased-emotion``: SHAP keeps case and punctuation, LIG lowercases, LIME emits a @@ -8,7 +8,7 @@ import numpy as np import pytest -from shapash.compute.token_alignment import ( +from shapash.compute.backend_comparison import ( align_token_values, backend_agreement, locate_tokens, @@ -104,9 +104,36 @@ def test_one_row_per_pair_and_identical_backends_agree_fully(self): assert list(zip(df.backend_a, df.backend_b, strict=True)) == [("a", "b"), ("a", "c"), ("b", "c")] ab = df.iloc[0] assert ab.spearman == pytest.approx(1.0) + assert ab.pearson == pytest.approx(1.0) + assert ab.cosine == pytest.approx(1.0) assert ab.sign_agreement == 1.0 assert ab.top_k_overlap == 1.0 assert df.iloc[1].spearman == pytest.approx(-1.0) + assert df.iloc[1].cosine == pytest.approx(-1.0) + # Each backend's own concentration: 0.9 of a total magnitude of 1.7. + assert ab.top_share_a == ab.top_share_b == pytest.approx(0.9 / 1.7) + + def test_one_dominant_unit_cosine_agrees_where_spearman_does_not(self): + # The demo's "I'm furious." case: same dominant word, the small rest ordered in reverse. + a = np.array([5.0, 0.1, 0.2, 0.3, 0.4]) + b = np.array([5.0, 0.4, 0.3, 0.2, 0.1]) + row = backend_agreement({"a": a, "b": b}).iloc[0] + assert row.spearman < 0.5 + assert row.cosine > 0.99 + assert row.pearson > 0.99 + assert row.top_share_a == pytest.approx(5.0 / 6.0) + + def test_pearson_ignores_a_shift_both_share_and_cosine_does_not(self): + a = np.array([1.0, 1.1, 0.9]) + b = np.array([1.0, 0.9, 1.1]) + row = backend_agreement({"a": a, "b": b}).iloc[0] + assert row.pearson == pytest.approx(-1.0) + assert row.cosine > 0.98 + + def test_top_share_ignores_unattributed_units(self): + row = backend_agreement({"a": np.array([1.0, 3.0]), "b": np.array([np.nan, 2.0])}).iloc[0] + assert row.top_share_a == pytest.approx(0.75) + assert row.top_share_b == 1.0 def test_nan_units_are_excluded(self): df = backend_agreement({"a": np.array([1.0, 2.0, 3.0]), "b": np.array([1.0, np.nan, 3.0])}, top_k=5) @@ -116,8 +143,12 @@ def test_nan_units_are_excluded(self): def test_constant_backend_has_no_rank_correlation(self): df = backend_agreement({"a": np.array([1.0, 2.0, 3.0]), "b": np.zeros(3)}) - assert np.isnan(df.iloc[0].spearman) + assert np.isnan(df.iloc[0].spearman) and np.isnan(df.iloc[0].pearson) def test_no_shared_unit_gives_nan(self): df = backend_agreement({"a": np.array([1.0, np.nan]), "b": np.array([np.nan, 1.0])}) - assert df.iloc[0][["spearman", "sign_agreement", "top_k_overlap"]].isna().all() + assert df.iloc[0][["spearman", "pearson", "cosine", "sign_agreement", "top_k_overlap"]].isna().all() + + def test_all_zero_backend_has_no_cosine_or_top_share(self): + row = backend_agreement({"a": np.array([1.0, 2.0]), "b": np.zeros(2)}).iloc[0] + assert np.isnan(row.cosine) and np.isnan(row.top_share_b) diff --git a/tests/unit_tests/explainer/test_nlp_comparison.py b/tests/unit_tests/explainer/test_nlp_comparison.py new file mode 100644 index 00000000..269a7aca --- /dev/null +++ b/tests/unit_tests/explainer/test_nlp_comparison.py @@ -0,0 +1,88 @@ +"""Unit tests for ``shapash.explainer.nlp_comparison`` — several explanations of the same texts, side by side.""" + +import warnings +from dataclasses import replace + +import numpy as np +import pytest + +from shapash.explainer.nlp_comparison import AGREEMENT_COLUMNS, align_row, corpus_agreement, name_explanations +from tests.unit_tests.explainer.test_nlp_plotter import _make_explanation + + +@pytest.fixture +def ref(): + return _make_explanation() + + +@pytest.fixture +def lime(ref): + # Bag of words in its own order; on row 1 LIME and SHAP rank "happy" above "i" for class 1. + return replace( + ref, + token_strings=[["world", "hello"], ["happy", "i"], ["ok"]], + values=[ + np.array([[5.0, -5.0], [3.0, -3.0]]), + np.array([[0.0, 8.0], [0.0, 2.0]]), + np.array([[1.0, -1.0]]), + ], + backend_name="nlp_lime", + is_additive=False, + ) + + +class TestCorpusAgreement: + def test_one_line_per_text_and_pair_on_the_predicted_class(self, ref, lime): + df = corpus_agreement(ref, lime) + assert list(df.columns) == AGREEMENT_COLUMNS + assert list(df.row) == [0, 1, 2] + assert list(df.label) == ["pos", "neg", "pos"] # y_pred + assert set(zip(df.backend_a, df.backend_b, strict=True)) == {("shap", "lime")} + assert df.text.iloc[1] == "i am happy today" + # Row 0, class "pos": shap [-1, -2], lime aligned onto "hello world" [-3, -5] — same ranks. + assert df.spearman.iloc[0] == pytest.approx(1.0) + # Row 1, class "neg": LIME is constant zero; row 2 has one word. No rank correlation either way. + assert df.spearman.iloc[1:].isna().all() + + def test_fixed_class_and_a_subset_of_rows(self, ref, lime): + df = corpus_agreement(ref, {"LIME": lime}, rows=[1], label_idx=-1) + assert list(df.row) == [1] + assert list(df.label) == ["pos"] + assert df.backend_b.iloc[0] == "LIME" + assert df.spearman.iloc[0] == pytest.approx(1.0) + + def test_every_pair_is_scored(self, ref, lime): + df = corpus_agreement(ref, [lime, replace(ref, backend_name="nlp_captum_lig")], rows=[0]) + assert list(zip(df.backend_a, df.backend_b, strict=True)) == [ + ("shap", "lime"), + ("shap", "captum_lig"), + ("lime", "captum_lig"), + ] + + def test_label_is_the_column_index_without_class_names(self): + one_d = _make_explanation(ndim=1) + df = corpus_agreement(one_d, replace(one_d, backend_name="nlp_lime"), rows=[0]) + assert df.label.iloc[0] == 0 + + def test_no_rows_gives_an_empty_table(self, ref, lime): + df = corpus_agreement(ref, lime, rows=[]) + assert df.empty and list(df.columns) == AGREEMENT_COLUMNS + + def test_warns_across_output_spaces(self, ref, lime): + with pytest.warns(UserWarning, match="output spaces") as record: + corpus_agreement(ref, replace(lime, output_space="logit"), rows=[0]) + assert record[0].filename == __file__ + with warnings.catch_warnings(): + warnings.simplefilter("error") + corpus_agreement(ref, lime, rows=[0]) + + def test_rejects_a_different_corpus(self, ref, lime): + other = replace(lime, texts=lime.texts.replace("ok", "fine")) + with pytest.raises(ValueError, match="Row 2 holds a different text"): + corpus_agreement(ref, other) + + +class TestAlignRow: + def test_out_of_range_row(self, ref, lime): + with pytest.raises(IndexError, match="row=5"): + align_row(name_explanations(ref, lime), 5, 0) diff --git a/tests/unit_tests/explainer/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py index 3d55f196..81c5ce06 100644 --- a/tests/unit_tests/explainer/test_nlp_plotter.py +++ b/tests/unit_tests/explainer/test_nlp_plotter.py @@ -459,5 +459,31 @@ def test_does_not_touch_either_artifact(self): np.testing.assert_array_equal(a, b) + def test_compare_corpus_plots_one_dot_per_text(self): + fig = self.ref.plot.compare_corpus(self.lime) + self.assertEqual([t.name for t in fig.data], ["lime vs shap"]) + # Rows 1 and 2 have no Spearman (LIME constant on "neg"; a one-word text): one dot left. + self.assertEqual(len(fig.data[0].x), 1) + self.assertIn("predicted class", fig.layout.title.text) + self.assertIn("3 texts · median: lime vs shap", fig.layout.title.text) + + def test_compare_corpus_on_a_fixed_class_and_metric(self): + fig = self.ref.plot.compare_corpus({"LIME": self.lime}, rows=[0], label_idx=1, metric="top_k_overlap", top_k=1) + self.assertIn("pos", fig.layout.title.text) + self.assertIn("1 text ·", fig.layout.title.text) + self.assertEqual(fig.layout.xaxis.title.text, "Top-1 overlap") + self.assertEqual(self.ref.plot.compare_corpus(self.lime, metric="cosine").layout.xaxis.title.text, "Cosine similarity") + self.assertEqual(self.ref.plot.compare_corpus(self.lime, metric="pearson").layout.xaxis.title.text, "Pearson r") + self.assertEqual(self.ref.plot.compare_corpus(self.lime, title="X").layout.title.text.split("<br>")[0], "X") + + def test_compare_corpus_warns_at_the_call_site_across_spaces(self): + with pytest.warns(UserWarning, match="output spaces") as record: + self.ref.plot.compare_corpus(replace(self.lime, output_space="logit")) + self.assertEqual(record[0].filename, __file__) + + def test_compare_corpus_rejects_a_bad_class(self): + with self.assertRaises(IndexError): + self.ref.plot.compare_corpus(self.lime, label_idx=5) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/plots/test_plot_backend_comparison.py b/tests/unit_tests/plots/test_plot_backend_comparison.py index 8d271c52..165114bc 100644 --- a/tests/unit_tests/plots/test_plot_backend_comparison.py +++ b/tests/unit_tests/plots/test_plot_backend_comparison.py @@ -1,12 +1,14 @@ """Unit tests for the cross-backend comparison renderers.""" import numpy as np +import pandas as pd import pytest from dash import html from plotly import graph_objs as go from shapash.plots.plot_backend_comparison import ( BACKEND_COLORS, + plot_backend_agreement, plot_backend_bars, plot_backend_heatmap, plot_backend_highlight, @@ -92,3 +94,52 @@ def test_unattributed_token_is_struck_through(self): lime_spans = rows[1].children[1].children assert lime_spans[3].style["textDecoration"] == "line-through" assert lime_spans[3].title == "!: not attributed" + + +class TestAgreement: + AGREEMENT = pd.DataFrame( + { + "row": [0, 0, 0, 1, 1, 1], + "text": ["short", "short", "short", "x" * 100, "x" * 100, "x" * 100], + "label": ["joy"] * 6, + "backend_a": ["shap", "shap", "lig", "shap", "shap", "lig"], + "backend_b": ["lig", "lime", "lime", "lig", "lime", "lime"], + "spearman": [0.8, 0.2, np.nan, 0.6, 0.4, 0.5], + } + ) + + def test_one_box_per_pair_with_one_dot_per_scored_text(self): + fig = plot_backend_agreement(self.AGREEMENT, subtitle="median") + assert [t.name for t in fig.data] == ["lig vs shap", "lime vs shap", "lime vs lig"] + assert list(fig.data[0].x) == [0.8, 0.6] + assert list(fig.data[2].x) == [0.5] # the NaN text is left out + assert fig.data[0].boxpoints == "all" + assert list(fig.layout.xaxis.range) == [-1.05, 1.05] + assert "median" in fig.layout.title.text + + def test_hover_carries_row_and_a_truncated_excerpt(self): + fig = plot_backend_agreement(self.AGREEMENT) + row, label, excerpt = fig.data[0].customdata[1] + assert (str(row), label) == ("1", "joy") + assert len(excerpt) == 78 and excerpt.endswith("…") + + @pytest.mark.parametrize("metric", ["pearson", "cosine"]) + def test_correlations_use_the_signed_range(self, metric): + fig = plot_backend_agreement(self.AGREEMENT.assign(**{metric: 0.9}), metric=metric) + assert list(fig.layout.xaxis.range) == [-1.05, 1.05] + + def test_share_metrics_use_the_unit_range(self): + df = self.AGREEMENT.assign(top_k_overlap=0.5) + fig = plot_backend_agreement(df, metric="top_k_overlap", axis_title="Top-5 overlap") + assert list(fig.layout.xaxis.range) == [-0.03, 1.03] + assert fig.layout.xaxis.title.text == "Top-5 overlap" + + def test_pair_with_no_scored_text_is_an_empty_box(self): + fig = plot_backend_agreement(self.AGREEMENT[self.AGREEMENT.backend_a == "lig"].iloc[:1]) + assert len(fig.data[0].x) == 0 + + def test_rejects_unknown_metric_and_missing_column(self): + with pytest.raises(ValueError, match="metric"): + plot_backend_agreement(self.AGREEMENT, metric="kendall") + with pytest.raises(ValueError, match="sign_agreement"): + plot_backend_agreement(self.AGREEMENT, metric="sign_agreement") From 90dcf0b422fc36c97653eb65d550500d5e658099 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 5 Oct 2026 17:54:05 +0200 Subject: [PATCH 136/144] docs: add compare backends notebook --- tutorial/nlp/compare_backends.ipynb | 817 ++++++++++++++++++++++++++++ 1 file changed, 817 insertions(+) create mode 100644 tutorial/nlp/compare_backends.ipynb diff --git a/tutorial/nlp/compare_backends.ipynb b/tutorial/nlp/compare_backends.ipynb new file mode 100644 index 00000000..07840747 --- /dev/null +++ b/tutorial/nlp/compare_backends.ipynb @@ -0,0 +1,817 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "efb1b5d8", + "metadata": {}, + "source": [ + "# Comparing different NLP Backends" + ] + }, + { + "cell_type": "markdown", + "id": "de35b23c", + "metadata": {}, + "source": [ + "We load an emotion classifier with six classes (sadness, joy, love, anger, fear, surprise) from Hugging Face" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78695f97", + "metadata": {}, + "outputs": [], + "source": [ + "from shapash.model import HFClassifierModel\n", + "\n", + "model = HFClassifierModel.from_pretrained(\"bhadresh-savani/distilbert-base-uncased-emotion\")\n", + "texts = [\n", + " \"I didn't expect the movie to be this good, I'm so happy!\",\n", + " \"The waiting room was cold and I felt nervous, really nervous, about the results.\",\n", + " \"Why would you do that? I'm furious.\",\n", + " \"We finally got the keys to our new house today.\",\n", + " \"I miss my grandmother more than I can say.\",\n", + " \"He forgot my birthday again and didn't even apologise.\",\n", + " \"There is a strange noise downstairs and I'm home alone.\",\n", + " \"She held my hand the whole evening and I never wanted it to end.\",\n", + " \"Wow, I really did not see that ending coming!\",\n", + " \"The train is late again, this is so annoying.\",\n", + " \"Everything feels grey since the move, I can't find any joy in it.\",\n", + " \"My best friend surprised me with a cake at work.\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "29947a13", + "metadata": {}, + "source": [ + "In `shapash[nlp]` three explain backends are currently available:\n", + "- **LIME** (wrapped from lib [lime](https://github.com/marcotcr/lime)): LIME deletes random sets of words, then fits a simple weighted linear model to the model's predictions to get words' contributions.\n", + "- **SHAP** (wrapped from lib [shap](https://github.com/shap/shap)): SHAP masks groups of words and splits the prediction fairly between them (Shapley values), so the contributions add up to the prediction minus the baseline (the prediction on the fully masked text).\n", + "- **Layer Integrated Gradients (LIG)** (wrapped from lib [Captum](https://captum.ai/api/integrated_gradients.html)): LIG integrates the model's gradients along a path from a baseline input to the real text in embedding space, so the contributions add up to the prediction (as a logit) minus the prediction on the baseline.\n", + "\n", + "The backends have very different behaviour under the hood, so it is useful to compare them on some texts. \n", + "\n", + "In the following we compare explanations in logit space from SHAP and LIG. LIG only explains logits, so SHAP is switched to logit space to compare in the same space (mixing spaces otherwise triggers a warning). As an exercise you could run the comparison with LIME and SHAP in probability space." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ff8bce31", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'SHAP': 'logit', 'LIG': 'logit'}\n" + ] + } + ], + "source": [ + "from shapash.backend import NlpCaptumLigBackend, NlpShapBackend\n", + "from shapash.explainer.nlp_explainer import NlpExplainer\n", + "from pathlib import Path\n", + "\n", + "backends = {\n", + " \"SHAP\": NlpShapBackend(model, label_names=model.label_names, output_space=\"logit\"),\n", + " \"LIG\": NlpCaptumLigBackend(model, label_names=model.label_names),\n", + "}\n", + "\n", + "# Explain the same texts once per backend (cached on disk, so re-runs are instant)\n", + "cache_dir = Path(\"nlp_cache\")\n", + "explanations = {\n", + " name: NlpExplainer(model, backend=backend).explain(texts, cache_dir=cache_dir) for name, backend in backends.items()\n", + "}\n", + "shap_exp, lig_exp = explanations.values()\n", + "print({name: exp.output_space for name, exp in explanations.items()}) # both \"logit\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c438197d", + "metadata": {}, + "outputs": [], + "source": [ + "# Shap is the reference backend against which we compare others.\n", + "others = {\"LIG\": lig_exp}" + ] + }, + { + "cell_type": "markdown", + "id": "e53b7d63", + "metadata": {}, + "source": [ + "Now we have two explanation objects from SHAP and LIG and we can plot them together in several ways." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "859c4718", + "metadata": {}, + "outputs": [], + "source": [ + "# One text — the three display options\n", + "row = 6\n", + "heatmap = shap_exp.plot.compare(others, row=row, kind=\"heatmap\")\n", + "bars = shap_exp.plot.compare(others, row=row, kind=\"bars\")\n", + "highlight = shap_exp.plot.compare(others, row=row, kind=\"highlight\", notebook=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e5ea2e2c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzIAAAEaCAYAAADKVWJfAAAQAElEQVR4AeydBWAURxuG30sCxYJroVCgFCsttAUKbYEW58fdCcHdCe4uwd2d4lrcC8VdCxQproHg5JJ/v0nuuAghcpvcXd7A7OiOPHu57Lsz862TH39IgARIgARIgARIgARIgARIwM4IOIE/JEAC4STA4iRAAiRAAiRAAiRAAtFNgEImuq8A2ycBEiCBmECAYyQBEiABEiABKxOgkLEyUFZHAiRAAiRAAiRAAtYgwDpIgARCJ0AhEzof5pIACZAACZAACZAACZAACdgggRCEjA32kl0iARIgARIgARIgARIgARIgAQsCFDIWMBgkgQgT4IkkQAIkQAIkQAIkQAJRSoBCJkpxszESIAESIAETAfokQAIkQAIkEBkCFDKRocdzSYAESIAESIAESCDqCLAlEiABCwIUMhYwGCQBEiABEiABEiABEiABErAPAmETMvYxFvaSBEiABEiABEiABEiABEgghhCgkIkhF5rDjHoCbJEESIAESIAESIAESEA/AhQy+rFlzSRAAiRAAuEjwNIkQAIkQAIkEGYCFDJhRsWCJEACJEACJEACJGBrBNgfEoi5BChkYu6158hJgARIgARIgARIgARIwG4JRFjI2O2I2XESIAESIAESIAESIAESIAG7J0AhY/eXkAOwIwLsKgmQAAmQAAmQAAmQgJUIUMhYCSSrIQESIAES0IMA6yQBEiABEiCBkAlQyITMhakkQAIkQAIkQAIkYJ8E2GsSiCEEKGRiyIXmMEmABEiABEiABEiABEjAkQhYU8g4EheOhQRIgARIgARIgARIgARIwIYJUMjY8MVh12ICAY6RBEiABEiABEiABEggIgQoZCJCjeeQAAmQAAlEHwG2TAIkQAIkQAIaAQoZDQL/kwAJkAAJkAAJkIAjE+DYSMARCVDIOOJV5ZhIgARIgARIgARIgARIwMEJ6CxkHJweh0cCJEACJEACJEACJEACJBAtBChkogU7GyWBUAgwiwRIgARIgARIgARI4JMEKGQ+iYgFSIAESIAEbJ0A+0cCJEACJBDzCFDIxLxrzhGTgEMQKFW7K3IWcTO774o2Qpm6Hug2ZDrO/3NdlzFKm10HTtWl7rBW2qbnOFRu1DusxR2yXK/hs1CsRieHHJvloF69fqs+zz+VbYkG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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "heatmap" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "39532d73", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bars" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "6c6c27b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div style=\"display:inline-block;max-width:100%;background-color:#ffffff;color:#212529\"><div><div style=\"margin-bottom:4px\"><span style=\"color:rgba(244, 192, 0, 1.0)\">■ </span><span style=\"font-size:0.78em;color:#555\">positive </span><span style=\"color:rgba(74, 99, 138, 0.7)\">■ </span><span style=\"font-size:0.78em;color:#555\">negative </span><span style=\"font-size:0.78em;color:#aaa;text-decoration:line-through\">struck-through</span><span style=\"font-size:0.78em;color:#555\"> not attributed</span></div><div style=\"font-size:0.78em;color:#555;margin-bottom:6px\">LIG vs shap: ρ=0.67, top-5=60%</div><div style=\"background-color:#fafafa;border:1px solid #eeeeee;border-radius:4px\"><div style=\"padding:8px 12px;border-bottom:1px solid #eeeeee\"><div style=\"font-size:0.8em;font-weight:600;color:#444\">shap</div><div style=\"line-height:2.2;font-size:1.0em\"><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(253,245,214);color:#111111\" title=\"There: +0.1574\">There </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,251,241);color:#111111\" title=\"is: +0.0511\">is </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,250,237);color:#111111\" title=\"a: +0.0697\">a </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(244,192,0);color:#111111\" title=\"strange: +1.0000\">strange </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(252,242,206);color:#111111\" title=\"noise: +0.1910\">noise </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(253,248,227);color:#111111\" title=\"downstairs: +0.1065\">downstairs </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,253,249);color:#111111\" title=\"and: +0.0202\">and </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,249,233);color:#111111\" title=\"I: +0.0860\">I </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,252,243);color:#111111\" title=\"&#x27;: +0.0461\">&#x27; </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,252,244);color:#111111\" title=\"m: +0.0395\">m </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(253,245,215);color:#111111\" title=\"home: +0.1539\">home </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(253,243,209);color:#111111\" title=\"alone: +0.1797\">alone </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,253,249);color:#111111\" title=\".: +0.0205\">. </span></div></div><div style=\"padding:8px 12px;border-bottom:1px solid #eeeeee\"><div style=\"font-size:0.8em;font-weight:600;color:#444\">LIG</div><div style=\"line-height:2.2;font-size:1.0em\"><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,250,238);color:#111111\" title=\"There: +0.0663\">There </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(242,244,247);color:#111111\" title=\"is: -0.0682\">is </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,254,254);color:#111111\" title=\"a: +0.0033\">a </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(244,192,0);color:#111111\" title=\"strange: +1.0000\">strange </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,251,240);color:#111111\" title=\"noise: +0.0553\">noise </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,249,232);color:#111111\" title=\"downstairs: +0.0876\">downstairs </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,254,251);color:#111111\" title=\"and: +0.0151\">and </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(249,250,251);color:#111111\" title=\"I: -0.0288\">I </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(253,254,254);color:#111111\" title=\"&#x27;: -0.0063\">&#x27; </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(250,251,252);color:#111111\" title=\"m: -0.0241\">m </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,250,237);color:#111111\" title=\"home: +0.0696\">home </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(252,241,202);color:#111111\" title=\"alone: +0.2070\">alone </span><span style=\"padding:3px 5px;border-radius:3px;margin:2px 1px;display:inline-block;background-color:rgb(254,252,245);color:#111111\" title=\".: +0.0370\">. </span></div></div></div></div></div>" + ], + "text/plain": [ + "Div([Div(children=[Span(children='■ ', style={'color': 'rgba(244, 192, 0, 1.0)'}), Span(children='positive ', style={'fontSize': '0.78em', 'color': '#555'}), Span(children='■ ', style={'color': 'rgba(74, 99, 138, 0.7)'}), Span(children='negative ', style={'fontSize': '0.78em', 'color': '#555'}), Span(children='struck-through', style={'fontSize': '0.78em', 'color': '#aaa', 'textDecoration': 'line-through'}), Span(children=' not attributed', style={'fontSize': '0.78em', 'color': '#555'})], style={'marginBottom': '4px'}), Div(children='LIG vs shap: ρ=0.67, top-5=60%', style={'fontSize': '0.78em', 'color': '#555', 'marginBottom': '6px'}), Div(children=[Div(children=[Div(children='shap', style={'fontSize': '0.8em', 'fontWeight': '600', 'color': '#444'}), Div(children=[Span(children='There ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(253,245,214)', 'color': '#111111'}, title='There: +0.1574'), Span(children='is ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,251,241)', 'color': '#111111'}, title='is: +0.0511'), Span(children='a ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,250,237)', 'color': '#111111'}, title='a: +0.0697'), Span(children='strange ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(244,192,0)', 'color': '#111111'}, title='strange: +1.0000'), Span(children='noise ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(252,242,206)', 'color': '#111111'}, title='noise: +0.1910'), Span(children='downstairs ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(253,248,227)', 'color': '#111111'}, title='downstairs: +0.1065'), Span(children='and ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,253,249)', 'color': '#111111'}, title='and: +0.0202'), Span(children='I ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,249,233)', 'color': '#111111'}, title='I: +0.0860'), Span(children=\"' \", style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,252,243)', 'color': '#111111'}, title=\"': +0.0461\"), Span(children='m ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,252,244)', 'color': '#111111'}, title='m: +0.0395'), Span(children='home ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(253,245,215)', 'color': '#111111'}, title='home: +0.1539'), Span(children='alone ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(253,243,209)', 'color': '#111111'}, title='alone: +0.1797'), Span(children='. ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,253,249)', 'color': '#111111'}, title='.: +0.0205')], style={'lineHeight': '2.2', 'fontSize': '1.0em'})], style={'padding': '8px 12px', 'borderBottom': '1px solid #eeeeee'}), Div(children=[Div(children='LIG', style={'fontSize': '0.8em', 'fontWeight': '600', 'color': '#444'}), Div(children=[Span(children='There ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,250,238)', 'color': '#111111'}, title='There: +0.0663'), Span(children='is ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(242,244,247)', 'color': '#111111'}, title='is: -0.0682'), Span(children='a ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,254,254)', 'color': '#111111'}, title='a: +0.0033'), Span(children='strange ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(244,192,0)', 'color': '#111111'}, title='strange: +1.0000'), Span(children='noise ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,251,240)', 'color': '#111111'}, title='noise: +0.0553'), Span(children='downstairs ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,249,232)', 'color': '#111111'}, title='downstairs: +0.0876'), Span(children='and ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,254,251)', 'color': '#111111'}, title='and: +0.0151'), Span(children='I ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(249,250,251)', 'color': '#111111'}, title='I: -0.0288'), Span(children=\"' \", style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(253,254,254)', 'color': '#111111'}, title=\"': -0.0063\"), Span(children='m ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(250,251,252)', 'color': '#111111'}, title='m: -0.0241'), Span(children='home ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,250,237)', 'color': '#111111'}, title='home: +0.0696'), Span(children='alone ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(252,241,202)', 'color': '#111111'}, title='alone: +0.2070'), Span(children='. ', style={'padding': '3px 5px', 'borderRadius': '3px', 'margin': '2px 1px', 'display': 'inline-block', 'backgroundColor': 'rgb(254,252,245)', 'color': '#111111'}, title='.: +0.0370')], style={'lineHeight': '2.2', 'fontSize': '1.0em'})], style={'padding': '8px 12px', 'borderBottom': '1px solid #eeeeee'})], style={'backgroundColor': '#fafafa', 'border': '1px solid #eeeeee', 'borderRadius': '4px'})])" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "highlight" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "dea30299", + "metadata": {}, + "outputs": [], + "source": [ + "# Variations: a specific class instead of the predicted one, with raw values (meaningful here since\n", + "# both backends share the logit space); only the 8 strongest words.\n", + "idx = model.label_names.index(\"joy\")\n", + "\n", + "joy_raw = shap_exp.plot.compare(others, row=0, label_idx=idx, normalize=None)\n", + "top8 = shap_exp.plot.compare(others, row=1, max_tokens=8)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "f4938255", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AiwZBShAAQpETIABhoh5cW8KUIACFKAABawo8PrNO5T4Mp8Vc2RWTiTAqlKAAhSggJ0JMMBgZxeExaEABShAAQo4k4B0rQipxYEzOThmXVkrClCAAhRwNgEGGJztirO+FKAABSjgEALyZgnpRhCRypzdtyjAGAEROdZe95XxEAb3aGKvxbPvcrF0FKAABShAASsLMMBgZVBmRwEKUIACFKAABawhwDwoQAEKUIACRhNggMFoV4zlpQAFKEABClDAHgRYBgpQgAIUoAAFAgkwwBAIhIsUoAAFKEABCjiCAOtAAQpQgAIUoEB0CzDAEN3iPB8FKEABClCAAgANKEABClCAAhRwOAEGGBzukrJCFKAABShAgagLMAcKUIACFKAABSgQUQEGGCIqxv0pQAEKUIACMS/AElCAAhSgAAUoQAG7E2CAwe4uCQtEAQpQgALGF2ANKEABClCAAhSggPMJMMDgfNecNaYABShAAQpQgAIUoAAFKEABClhdgAEGq5MyQwpQgAIUiKoAj6cABShAAQpQgAIUMJ4AAwzGu2YsMQUoQIGYFuD5KUABClCAAhSgAAUoEESAAYYgJFxBAQpQwOgCLD8FKEABClCAAhSgAAWiX4ABhug35xkpQAFnF2D9KUABClCAAhSgAAUo4IACDDA44EVllShAgagJ8GgKUIACFKAABShAAQpQIOICDDBE3IxHUIACMSvAs1OAAhSgAAUoQAEKUIACdijAAIMdXhQWiQLGFmDpKUABClCAAhSgAAUoQAFnFGCAwRmvOuvs3AKsPQUoQAEKUIACFKAABShAARsIMMBgA1RmSYGoCPBYClCAAhSgAAUoQAEKUIACRhRggMGIV41ljkkBnpsCFKAABShAAQpQgAIUoAAFghFggCEYFK4ysgDLTgEKUIACFKAABShAAQpQgAIxIcAAQ0yoO/M5WXcKUIACFKAABShAAQpQgAIUcEgBBhgc8rJGvlI8kgIUoAAFKEABClCAAhSgAAUoEBkBBhgioxZzx/DMFKAABShAAQpQgAIUoAAFKEABuxSw+wCD18PTME4Kvqz6lb97/yGMnPR63H/wCEZPel28nnnC6OlDXR5pdTFu0uvxxPMpjJ70utzTvleMnvS6eD19ACMncz08b8HL4MlcFwf6/fj86kgYOZmvybMn2s9hYydzXZ78DS+DJ70uz549g5GT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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "joy_raw" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "a7c87224", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlgAAAEaCAYAAADNHikmAAAQAElEQVR4AeydBXwURxvGn0sCxR2Ka6FoKV5oKRSHD3d3dw/u7u7FSylS3IpDS3GK0wJFirt7km/eSfa4CCEJd5e7y8Mvs7MjO/LfkHvufWdn3Xz4jwRIgARIgARIgARIwKoE3MB/JEACJEACJOBwBDggEnBuAhRYzn3/OHoSIAESIAESIAEHJECB5YA3hUMiAWsQYBskQAIkQALhR4ACK/zYs2cSIAESIAESIAEXJUCB9cEbywISIAESIAESIAESCBsBCqywceNVJEACJEACJBA+BNirUxCgwHKK28RBkgAJkAAJkAAJOBMBCixnulscKwmQgDUIsA0SIAESsDkBCiybI2YHJEACJEACJEACEY0ABVZEu+PWmC/bIAESIAESIAESCJYABVaweFhIAiRAAiRAAiTgLAQcaZwUWI50NzgWEiABEiABEiABlyBAgeUSt5GTIAESIAFrEGAbJEAC1iJAgWUtkmyHBEiABEiABEiABPwIUGD5gWBEAtYgwDZIgARIgARIQAhQYAkFBhIgARIgARIgARKwIgEHE1hWnBmbIgESIAESIAESIIFwIkCBFU7g2S0JkAAJkIATEeBQSSCUBCiwQgmM1UmABEiABEiABEjgYwQosD5GiOUkQALWIMA2SIAESCBCEaDAilC3m5MlARIgARIgARKwBwEKLHtQtkYfbIMESIAESIAESMBpCFBgOc2t4kBJgARIgARIwPEIcERBE6DACpoLc0mABEiABEiABEggzAQosMKMjheSAAmQgDUIsA0SIAFXJECB5Yp3lXMiARIgARIgARIIVwIUWOGKn51bgwDbIAESIAESIAFHI0CB5Wh3hOMJFwKlandHlsINzSF70SYoU9cTPYbNwpl/LttkTNJn98EzbNJ2SBtt13siKjfpG9LqLlmvz8gfUaxGF5ecm+WkXrx8rX+fvynbGg06DLcs4jkJkIANCLgBNmiVTZKAExLIlD4V5o731GHC4Lb44dsc2H/kDOq0HYrzl6454Yw45JAQ+KFADtSrWiIkVZ26zvqt+7Dut31oXLM0erSt7dRz4eBJwBkI0ILlDHeJY7QLgdgxoyNfjkw6yIdut1Y1MXlIe7x58xZ/HDxllzGwE/sTKFowJxpUK2n/ju3c4517j3SPzeqU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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "top8" + ] + }, + { + "cell_type": "markdown", + "id": "1a609968", + "metadata": {}, + "source": [ + "### The corpus - one agreement score per text and backend pair" + ] + }, + { + "cell_type": "markdown", + "id": "9498774f", + "metadata": {}, + "source": [ + "Before choosing a particular backend, we would rather like to investigate how they compare on a corpus than on a single sentence. Then we can compute useful metrics: Spearman correlation, Pearson correlation, cosine similarity, sign agreement ratio, overlapping top-k words ratio, or the strongest word's share of a backend's total attribution.\n", + "\n", + "| Metric | Question it answers | Range | Note |\n", + "|---|---|---|---|\n", + "| `spearman` | Do both backends rank *every* word in the same order? | −1 to 1 | Every word counts the same, so when one word dominates, the order of the near-zero rest decides it. |\n", + "| `pearson` | Do the contribution values vary together, word by word? | −1 to 1 | Bigger words count more; measured around each backend's mean. |\n", + "| `cosine` | Do both backends put their attribution on the same words, with the same signs? | −1 to 1 | Like Pearson but measured around zero (\"no effect\"); the best single \"same story?\" score. |\n", + "| `sign_agreement` | On what share of words do they agree on the direction (pushes toward / away from the class)? | 0 to 1 | Small near-zero words can flip sign easily. |\n", + "| `top_k_overlap` | What share of the *k* most important words do they have in common? | 0 to 1 | Ignores signs and values beyond the top *k*. |\n", + "| `top_share_a` / `top_share_b` | How concentrated is each backend's attribution, i.e. the strongest word's share of the total? | 1/n to 1 | Not an agreement score: it says how far to trust `spearman`. High → prefer `cosine`. |" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "86a19fcb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>row</th>\n", + " <th>text</th>\n", + " <th>label</th>\n", + " <th>backend_a</th>\n", + " <th>backend_b</th>\n", + " <th>spearman</th>\n", + " <th>pearson</th>\n", + " <th>cosine</th>\n", + " <th>sign_agreement</th>\n", + " <th>top_k_overlap</th>\n", + " <th>top_share_a</th>\n", + " <th>top_share_b</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>0</th>\n", + " <td>0</td>\n", + " <td>I didn't expect the movie to be this good, I'm...</td>\n", + " <td>joy</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.55</td>\n", + " <td>0.95</td>\n", + " <td>0.96</td>\n", + " <td>0.67</td>\n", + " <td>0.6</td>\n", + " <td>0.41</td>\n", + " <td>0.42</td>\n", + " </tr>\n", + " <tr>\n", + " <th>1</th>\n", + " <td>1</td>\n", + " <td>The waiting room was cold and I felt nervous, ...</td>\n", + " <td>fear</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.80</td>\n", + " <td>0.87</td>\n", + " <td>0.91</td>\n", + " <td>0.88</td>\n", + " <td>0.8</td>\n", + " <td>0.20</td>\n", + " <td>0.30</td>\n", + " </tr>\n", + " <tr>\n", + " <th>2</th>\n", + " <td>2</td>\n", + " <td>Why would you do that? I'm furious.</td>\n", + " <td>anger</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.27</td>\n", + " <td>0.99</td>\n", + " <td>0.99</td>\n", + " <td>0.73</td>\n", + " <td>0.8</td>\n", + " <td>0.69</td>\n", + " <td>0.76</td>\n", + " </tr>\n", + " <tr>\n", + " <th>3</th>\n", + " <td>3</td>\n", + " <td>We finally got the keys to our new house today.</td>\n", + " <td>joy</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.76</td>\n", + " <td>0.87</td>\n", + " <td>0.93</td>\n", + " <td>0.82</td>\n", + " <td>0.8</td>\n", + " <td>0.14</td>\n", + " <td>0.19</td>\n", + " </tr>\n", + " <tr>\n", + " <th>4</th>\n", + " <td>4</td>\n", + " <td>I miss my grandmother more than I can say.</td>\n", + " <td>sadness</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.79</td>\n", + " <td>0.75</td>\n", + " <td>0.77</td>\n", + " <td>0.50</td>\n", + " <td>0.8</td>\n", + " <td>0.26</td>\n", + " <td>0.58</td>\n", + " </tr>\n", + " <tr>\n", + " <th>5</th>\n", + " <td>5</td>\n", + " <td>He forgot my birthday again and didn't even ap...</td>\n", + " <td>anger</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.85</td>\n", + " <td>0.85</td>\n", + " <td>0.90</td>\n", + " <td>0.92</td>\n", + " <td>0.6</td>\n", + " <td>0.50</td>\n", + " <td>0.30</td>\n", + " </tr>\n", + " <tr>\n", + " <th>6</th>\n", + " <td>6</td>\n", + " <td>There is a strange noise downstairs and I'm ho...</td>\n", + " <td>fear</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.67</td>\n", + " <td>0.98</td>\n", + " <td>0.97</td>\n", + " <td>0.69</td>\n", + " <td>0.6</td>\n", + " <td>0.47</td>\n", + " <td>0.60</td>\n", + " </tr>\n", + " <tr>\n", + " <th>7</th>\n", + " <td>7</td>\n", + " <td>She held my hand the whole evening and I never...</td>\n", + " <td>anger</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.67</td>\n", + " <td>0.89</td>\n", + " <td>0.85</td>\n", + " <td>0.73</td>\n", + " <td>0.8</td>\n", + " <td>0.20</td>\n", + " <td>0.30</td>\n", + " </tr>\n", + " <tr>\n", + " <th>8</th>\n", + " <td>8</td>\n", + " <td>Wow, I really did not see that ending coming!</td>\n", + " <td>joy</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.55</td>\n", + " <td>0.80</td>\n", + " <td>0.86</td>\n", + " <td>0.73</td>\n", + " <td>0.6</td>\n", + " <td>0.30</td>\n", + " <td>0.51</td>\n", + " </tr>\n", + " <tr>\n", + " <th>9</th>\n", + " <td>9</td>\n", + " <td>The train is late again, this is so annoying.</td>\n", + " <td>anger</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.81</td>\n", + " <td>0.99</td>\n", + " <td>0.98</td>\n", + " <td>0.91</td>\n", + " <td>0.6</td>\n", + " <td>0.56</td>\n", + " <td>0.70</td>\n", + " </tr>\n", + " <tr>\n", + " <th>10</th>\n", + " <td>10</td>\n", + " <td>Everything feels grey since the move, I can't ...</td>\n", + " <td>sadness</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.72</td>\n", + " <td>0.87</td>\n", + " <td>0.87</td>\n", + " <td>0.76</td>\n", + " <td>0.6</td>\n", + " <td>0.31</td>\n", + " <td>0.49</td>\n", + " </tr>\n", + " <tr>\n", + " <th>11</th>\n", + " <td>11</td>\n", + " <td>My best friend surprised me with a cake at work.</td>\n", + " <td>surprise</td>\n", + " <td>shap</td>\n", + " <td>LIG</td>\n", + " <td>0.39</td>\n", + " <td>0.98</td>\n", + " <td>0.97</td>\n", + " <td>0.82</td>\n", + " <td>0.6</td>\n", + " <td>0.57</td>\n", + " <td>0.78</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "text/plain": [ + " row text label \\\n", + "0 0 I didn't expect the movie to be this good, I'm... joy \n", + "1 1 The waiting room was cold and I felt nervous, ... fear \n", + "2 2 Why would you do that? I'm furious. anger \n", + "3 3 We finally got the keys to our new house today. joy \n", + "4 4 I miss my grandmother more than I can say. sadness \n", + "5 5 He forgot my birthday again and didn't even ap... anger \n", + "6 6 There is a strange noise downstairs and I'm ho... fear \n", + "7 7 She held my hand the whole evening and I never... anger \n", + "8 8 Wow, I really did not see that ending coming! joy \n", + "9 9 The train is late again, this is so annoying. anger \n", + "10 10 Everything feels grey since the move, I can't ... sadness \n", + "11 11 My best friend surprised me with a cake at work. surprise \n", + "\n", + " backend_a backend_b spearman pearson cosine sign_agreement \\\n", + "0 shap LIG 0.55 0.95 0.96 0.67 \n", + "1 shap LIG 0.80 0.87 0.91 0.88 \n", + "2 shap LIG 0.27 0.99 0.99 0.73 \n", + "3 shap LIG 0.76 0.87 0.93 0.82 \n", + "4 shap LIG 0.79 0.75 0.77 0.50 \n", + "5 shap LIG 0.85 0.85 0.90 0.92 \n", + "6 shap LIG 0.67 0.98 0.97 0.69 \n", + "7 shap LIG 0.67 0.89 0.85 0.73 \n", + "8 shap LIG 0.55 0.80 0.86 0.73 \n", + "9 shap LIG 0.81 0.99 0.98 0.91 \n", + "10 shap LIG 0.72 0.87 0.87 0.76 \n", + "11 shap LIG 0.39 0.98 0.97 0.82 \n", + "\n", + " top_k_overlap top_share_a top_share_b \n", + "0 0.6 0.41 0.42 \n", + "1 0.8 0.20 0.30 \n", + "2 0.8 0.69 0.76 \n", + "3 0.8 0.14 0.19 \n", + "4 0.8 0.26 0.58 \n", + "5 0.6 0.50 0.30 \n", + "6 0.6 0.47 0.60 \n", + "7 0.8 0.20 0.30 \n", + "8 0.6 0.30 0.51 \n", + "9 0.6 0.56 0.70 \n", + "10 0.6 0.31 0.49 \n", + "11 0.6 0.57 0.78 " + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from shapash.explainer.nlp_comparison import corpus_agreement\n", + "\n", + "scores = corpus_agreement(shap_exp, others, top_k=5) # rows=[0, 1, 2, ...] to score only some texts\n", + "scores.round(2)" + ] + }, + { + "cell_type": "markdown", + "id": "8b6a1ed4", + "metadata": {}, + "source": [ + "Some findings:\n", + "- Row 2 (\"I'm furious\"): Spearman rho is 0.27 but cosine is 0.99, because one word carries about 70% of the attribution and the low ρ is only about the near-zero rest. It's the best illustration of why one metric isn't enough.\n", + "- Row 4: the backends genuinely disagree. Cosine is 0.77, sign agreement is 0.50, and LIG concentrates on one word (top share 0.58) while SHAP spreads it out (0.26)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f609d129", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th></th>\n", + " <th>spearman</th>\n", + " <th>pearson</th>\n", + " <th>cosine</th>\n", + " <th>sign_agreement</th>\n", + " <th>top_k_overlap</th>\n", + " <th>top_share_a</th>\n", + " <th>top_share_b</th>\n", + " </tr>\n", + " <tr>\n", + " <th>backend_a</th>\n", + " <th>backend_b</th>\n", + " <th></th>\n", + " <th></th>\n", + " <th></th>\n", + " <th></th>\n", + " <th></th>\n", + " <th></th>\n", + " <th></th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>shap</th>\n", + " <th>LIG</th>\n", + " <td>0.69</td>\n", + " <td>0.88</td>\n", + " <td>0.92</td>\n", + " <td>0.75</td>\n", + " <td>0.6</td>\n", + " <td>0.36</td>\n", + " <td>0.5</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "text/plain": [ + " spearman pearson cosine sign_agreement top_k_overlap \\\n", + "backend_a backend_b \n", + "shap LIG 0.69 0.88 0.92 0.75 0.6 \n", + "\n", + " top_share_a top_share_b \n", + "backend_a backend_b \n", + "shap LIG 0.36 0.5 " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# In general: aggregate and compute the median over texts, per backend pair. \n", + "# Read spearman with cosine and top_share: a low spearman with a high cosine and a high top_share means \n", + "# both backends agree on the word that carries the prediction and only order the near-zero rest differently.\n", + "\n", + "metrics = [\"spearman\", \"pearson\", \"cosine\", \"sign_agreement\", \"top_k_overlap\", \"top_share_a\", \"top_share_b\"]\n", + "scores.groupby([\"backend_a\", \"backend_b\"])[metrics].median().round(2)" + ] + }, + { + "cell_type": "markdown", + "id": "ab837e76", + "metadata": {}, + "source": [ + "Conclusion: SHAP and LIG agree well overall (median cosine 0.92, ρ 0.69); they differ mostly on how they spread the small words." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "76b0cfd2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>row</th>\n", + " <th>label</th>\n", + " <th>spearman</th>\n", + " <th>pearson</th>\n", + " <th>cosine</th>\n", + " <th>top_share_a</th>\n", + " <th>text</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>4</th>\n", + " <td>4</td>\n", + " <td>sadness</td>\n", + " <td>0.79</td>\n", + " <td>0.75</td>\n", + " <td>0.77</td>\n", + " <td>0.26</td>\n", + " <td>I miss my grandmother more than I can say.</td>\n", + " </tr>\n", + " <tr>\n", + " <th>7</th>\n", + " <td>7</td>\n", + " <td>anger</td>\n", + " <td>0.67</td>\n", + " <td>0.89</td>\n", + " <td>0.85</td>\n", + " <td>0.20</td>\n", + " <td>She held my hand the whole evening and I never...</td>\n", + " </tr>\n", + " <tr>\n", + " <th>8</th>\n", + " <td>8</td>\n", + " <td>joy</td>\n", + " <td>0.55</td>\n", + " <td>0.80</td>\n", + " <td>0.86</td>\n", + " <td>0.30</td>\n", + " <td>Wow, I really did not see that ending coming!</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "text/plain": [ + " row label spearman pearson cosine top_share_a \\\n", + "4 4 sadness 0.79 0.75 0.77 0.26 \n", + "7 7 anger 0.67 0.89 0.85 0.20 \n", + "8 8 joy 0.55 0.80 0.86 0.30 \n", + "\n", + " text \n", + "4 I miss my grandmother more than I can say. \n", + "7 She held my hand the whole evening and I never... \n", + "8 Wow, I really did not see that ending coming! " + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Where they disagree most: the texts worth opening with ``compare``.\n", + "worst = scores.nsmallest(3, \"cosine\")\n", + "worst[[\"row\", \"label\", \"spearman\", \"pearson\", \"cosine\", \"top_share_a\", \"text\"]].round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "0629c1a2", + "metadata": {}, + "outputs": [], + "source": [ + "# The same, as plots — one dot per text (hover for its row), the box gives median and quartiles\n", + "corpus_metric = shap_exp.plot.compare_corpus(others, metric=\"cosine\")\n", + "corpus_topk = shap_exp.plot.compare_corpus(others, metric=\"top_k_overlap\", top_k=5)\n", + "\n", + "# Then drill down into the text they disagree on most.\n", + "worst_text = shap_exp.plot.compare(others, row=int(worst[\"row\"].iloc[0]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "a0a92fad", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "corpus_metric" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "a3c95958", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "worst_text" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "shapash (3.13.13)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 41ae4db2d081348792f162b423b0a6a6881dd149 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 11:01:31 +0200 Subject: [PATCH 137/144] feat(nlp): record character spans for every explained unit Each NLP backend now records, for every unit it explains, the (start, end) character spans of the source text that unit covers. These spans are a coordinate all backends can share, even though they cut a text into units differently and have no common token axis. - New shapash.compute.spans: build spans from tokenizer offsets, trim whitespace, partition a text into groups every unit fits in, aggregate unit values onto those groups, and locate spans from strings for older artifacts. - NlpContributions and NlpExplanation carry token_spans. The .xpl file stores them in token_spans.parquet. Older files load without spans. - SHAP reads the masker tokenizer's offsets. Words are taken from the source text, so characters shared by two tokens are no longer duplicated, and tokens that share a character merge into one word. - LIG reads offsets through the new SupportsCaptumIG.token_offsets(). Words are shown as written (case, accents, emoji), and units covering only whitespace are folded into the baseline, so attributions still sum to the total. - LIME records every occurrence of each word. - The cache key now includes a unit-layout version, so cached explanations are recomputed once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- shapash/backend/nlp_backend.py | 7 + shapash/backend/nlp_captum_lig_backend.py | 60 ++++ shapash/backend/nlp_lime_backend.py | 20 ++ shapash/backend/nlp_shap_backend.py | 111 ++++++-- shapash/compute/spans.py | 269 ++++++++++++++++++ shapash/explainer/nlp_explainer.py | 11 +- shapash/explainer/nlp_explanation.py | 52 ++++ shapash/model/base.py | 16 ++ shapash/model/encoder.py | 13 + .../backend/test_nlp_captum_lig_backend.py | 45 ++- .../backend/test_nlp_lime_backend.py | 13 +- .../backend/test_nlp_shap_backend.py | 104 ++++++- tests/unit_tests/compute/test_spans.py | 122 ++++++++ .../explainer/test_nlp_explanation.py | 48 ++++ tests/unit_tests/model/test_torch_models.py | 23 +- 15 files changed, 863 insertions(+), 51 deletions(-) create mode 100644 shapash/compute/spans.py create mode 100644 tests/unit_tests/compute/test_spans.py diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index 575bab0f..215d6dce 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -58,11 +58,17 @@ class NlpContributions: Baseline prediction for each sample, shape ``(n_samples, n_classes)`` or ``(n_samples,)``. ``None`` when the backend has no reference at all (see :attr:`NlpBackend.reference_kind`). + token_spans : list[list[tuple[tuple[int, int], ...]]] or None + Per sample, per unit: the ``(start, end)`` character spans of the source text the unit + stands for — one for a word, one per occurrence for a bag-of-words unit, none when the + backend could not place it. The coordinate backends share (see + :mod:`shapash.compute.spans`). ``None`` when the backend records no spans at all. """ token_strings: list[list[str]] values: list[np.ndarray] base_values: np.ndarray | None + token_spans: list[list[tuple[tuple[int, int], ...]]] | None = None class NlpBackend(Backend): @@ -196,6 +202,7 @@ def get_local_contributions( token_strings=[explain_data.token_strings[i] for i in subset], values=[explain_data.values[i] for i in subset], base_values=None if base_values is None else base_values[subset], + token_spans=None if explain_data.token_spans is None else [explain_data.token_spans[i] for i in subset], ) def _progress_iter(self, items: list[str]) -> Iterable[str]: diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index a4aec084..c7f2ff55 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -18,6 +18,14 @@ the pipeline's softmax probability output, not ``model.logits``, so the two backends' numbers are on different scales and are not directly comparable. +Each word is then placed in the source text through the tokenizer's offsets +(:meth:`~shapash.model.base.SupportsCaptumIG.token_offsets`): its span is recorded +(``NlpContributions.token_spans``) and its display string is that span of the text, not the +tokenizer's rebuilt string — which an uncased tokenizer lowercases and strips of accents, and which +reads ``[UNK]`` for an emoji. A word covering only whitespace (byte-level BPE gives a lone ``Ġ``/``Ċ`` +its own word id on a double space or a newline) is not a word: its attribution goes to the baseline, +like a special token's. + Unlike the SHAP/LIME backends (which wrap a plain text callable), this backend needs the embedding module and a logits forward pass, so it consumes a :class:`~shapash.model.base.TextModel` that implements :class:`~shapash.model.base.SupportsCaptumIG` (e.g. ``HFClassifierModel``). @@ -31,6 +39,7 @@ from shapash._optional import import_optional_module from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.compute.spans import Span, locate_spans, span_from_offsets, trim_span from shapash.model.base import SupportsCaptumIG _NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' @@ -171,6 +180,50 @@ def _valid_alignment( return alignment +def _place_words( + text: str, + words: list[str], + contributions: np.ndarray, + base_values: np.ndarray, + word_positions: list[list[int]] | None, + offsets: list[tuple[int, int]] | None, +) -> tuple[list[str], np.ndarray, np.ndarray, list[tuple[Span, ...]]]: + """Give each word its span and its source-text string; fold whitespace-only words into the baseline. + + With ``word_positions`` (the exact tokenizer grouping) and ``offsets``, a word's span is read off + its tokens' offsets. Without them — a slow tokenizer, which reports neither — words keep their + token-built strings and are located in ``text`` by search, the approximate path. + + Returns + ------- + tuple[list[str], np.ndarray, np.ndarray, list[tuple[tuple[int, int], ...]]] + Words, their contributions, the baseline (with any folded word's attribution), and spans. + """ + if word_positions is None or offsets is None: + return words, contributions, base_values, locate_spans(text, words) + base = base_values.astype(float).copy() + keep: list[int] = [] + placed: list[str] = [] + spans: list[tuple[Span, ...]] = [] + for k, positions in enumerate(word_positions): + span = trim_span(text, span_from_offsets(offsets, positions)) + if span is None: + base = base + contributions[k] # covers no visible character: not a word (see module docstring) + continue + keep.append(k) + placed.append(text[span[0] : span[1]]) + spans.append((span,)) + return placed, contributions[keep], base, spans + + +def _valid_offsets(offsets: list[tuple[int, int]] | None, n_tokens: int) -> list[tuple[int, int]] | None: + """Return ``offsets`` only when there is exactly one per attributed token, else ``None``. + + The same guard as :func:`_valid_alignment`: offsets come from a second encoding of the text. + """ + return offsets if offsets is not None and len(offsets) == n_tokens else None + + def _model_special_tokens(model: SupportsCaptumIG) -> set[str] | None: """Return the model tokenizer's ``all_special_tokens`` as a set, or ``None`` when unavailable.""" specials = getattr(getattr(model, "tokenizer", None), "all_special_tokens", None) @@ -260,6 +313,7 @@ def run_explainer(self, x) -> NlpContributions: contributions: list[np.ndarray] = [] base_values: list[np.ndarray] = [] data: list[list[str]] = [] + spans: list[list[tuple[Span, ...]]] = [] for text in self._progress_iter(list(x)): input_ids, attention_mask, tokens = model.encode(text) @@ -286,18 +340,24 @@ def run_explainer(self, x) -> NlpContributions: # heuristic (with the model's own special set) only for a slow tokenizer. stacked = np.stack(per_class, axis=-1) # (seq, n_classes) alignment = _valid_alignment(model.word_alignment(text), n_tokens=stacked.shape[0]) + offsets = _valid_offsets(getattr(model, "token_offsets", lambda _: None)(text), n_tokens=stacked.shape[0]) if alignment is not None: word_tokens, word_contribs, base_logits = _aggregate_by_alignment(stacked, base_logits, alignment) else: word_tokens, word_contribs, base_logits = _aggregate_subwords( list(tokens), stacked, base_logits, special_tokens=_model_special_tokens(model) ) + word_tokens, word_contribs, base_logits, word_spans = _place_words( + text, word_tokens, word_contribs, base_logits, None if alignment is None else alignment[1], offsets + ) contributions.append(word_contribs) # (n_words, n_classes) base_values.append(base_logits) data.append(word_tokens) + spans.append(word_spans) return NlpContributions( token_strings=data, values=contributions, base_values=np.stack(base_values, axis=0), + token_spans=spans, ) diff --git a/shapash/backend/nlp_lime_backend.py b/shapash/backend/nlp_lime_backend.py index 2893b9e8..9ae434d2 100644 --- a/shapash/backend/nlp_lime_backend.py +++ b/shapash/backend/nlp_lime_backend.py @@ -11,6 +11,10 @@ HuggingFace subword tokens. Every word is scored by default (``num_features="all"``), so a zero weight means LIME found no effect — not that the word fell outside a top-k. Shapash's plots pick their own top-k at display time. + +Each word records the character span of every occurrence (``NlpContributions.token_spans``), read +off LIME's own ``IndexedString`` — so a bag-of-words unit carries all the places its one weight +stands for, which is what lets it be compared with a backend that attributes each occurrence. """ from __future__ import annotations @@ -254,6 +258,7 @@ def run_explainer(self, x) -> NlpContributions: contributions: list[np.ndarray] = [] base_values_list: list[list[float]] = [] data: list[list[str]] = [] + spans: list[list[tuple[tuple[int, int], ...]]] = [] for text in self._progress_iter(texts): text_args = compute_args @@ -277,9 +282,24 @@ def run_explainer(self, x) -> NlpContributions: contributions.append(weight_matrix) base_values_list.append([exp.intercept.get(i, 0.0) for i in range(n_classes)]) data.append(vocab) + spans.append(_word_spans(indexed_string, vocab)) return NlpContributions( token_strings=data, values=contributions, base_values=np.array(base_values_list), + token_spans=spans, ) + + +def _word_spans(indexed_string, vocab: list[str]) -> list[tuple[tuple[int, int], ...]]: + """Every occurrence's ``(start, end)`` for each of LIME's features, from its ``IndexedString``. + + ``string_position`` gives a word's start offsets — all occurrences under ``bow=True``, its one + position otherwise — in the text LIME split, which is the text it was handed. + """ + out = [] + for word_id, word in enumerate(vocab): + starts = np.atleast_1d(indexed_string.string_position(word_id)) + out.append(tuple((int(start), int(start) + len(word)) for start in starts)) + return out diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index fb297f1c..cd5b97b4 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -14,22 +14,32 @@ into the baseline so ``base + Σ(word contributions)`` keeps SHAP's additive guarantee. Word boundaries come from :func:`_merges`: flush on source-text whitespace *or* on a word/non-word -transition. A whitespace-only rule (what this module used previously) is wrong in two ways — it +transition — except that tokens sharing a source character always merge (see below). A whitespace-only rule (what this module used previously) is wrong in two ways — it glues punctuation onto its neighbours whenever the source has no space around it (``"superb!!!"``, ``"enjoy.Overall,I"``), and under the leading-space fallback regime it never fires at all, so an entire sample collapses into a single "word". + +Each word also records the character span it covers (``NlpContributions.token_spans``), read off the +masker tokenizer's offset mapping — the same call ``token_segments`` makes and then discards — and +its display string is that span of the source text. Concatenating segments instead is wrong when +two tokens share a character, which offsets make visible: byte-level BPE splits a multi-byte +character (``é``, an emoji) across tokens that all map to it, and SentencePiece can give a bare +``▁`` the offsets of the character after it. ``token_segments`` then hands each of those tokens the +same character, so concatenation printed ``éétait`` and an emoji became two "words". Tokens whose +spans overlap are one word, and the word's text comes from the source, never from the segments. """ from __future__ import annotations import re -import unicodedata +from collections.abc import Sequence from typing import Literal, cast import numpy as np import shap from shapash.backend.nlp_backend import NlpBackend, NlpContributions +from shapash.compute.spans import Span, is_unsegmented_script, locate_spans, span_from_offsets, trim_span from shapash.model.base import SupportsLogits, TextModel, has_capabilities # SHAP's masker reports special tokens as blank segments on its offset-mapping path; their @@ -42,14 +52,6 @@ # ``[LAUGHTER]``, which would then be silently folded into the baseline instead of shown as a word. _BRACKET_SPECIAL_RE = re.compile(r"^\[.*\]$") -# Scripts written without inter-word spaces. A character-class boundary test sees such a sentence as -# one uninterrupted run of word characters, so :func:`_merges` would collapse it into a single -# "word"; breaking between these characters keeps units at the character level instead — the only -# tokenizer-free option, and what ``BertPreTokenizer`` does anyway (it inserts whitespace around -# CJK). Hangul is deliberately absent: Korean *is* space-segmented, so its subword pieces must merge -# like any other alphabetic script's. -_UNSEGMENTED_SCRIPTS = ("CJK", "HIRAGANA", "KATAKANA", "THAI", "LAO", "KHMER", "MYANMAR") - def _is_special(segment: str, special_tokens: frozenset[str] | None) -> bool: """True when ``segment`` is a special token whose attribution belongs in the baseline.""" @@ -61,11 +63,6 @@ def _is_special(segment: str, special_tokens: frozenset[str] | None) -> bool: return bool(_BRACKET_SPECIAL_RE.match(stripped)) -def _is_unsegmented_script(char: str) -> bool: - """True when ``char`` belongs to a script written without spaces between words.""" - return unicodedata.name(char, "").startswith(_UNSEGMENTED_SCRIPTS) - - def _merges(buffer: str, following: str) -> bool: """Whether the segment ``following`` continues the word currently held in ``buffer``. @@ -82,7 +79,10 @@ def _merges(buffer: str, following: str) -> bool: last, first = buffer[-1], following[0] if not (last.isalnum() or last == "_") or not (first.isalnum() or first == "_"): return False - return not (_is_unsegmented_script(last) or _is_unsegmented_script(first)) + # A script written without spaces (CJK, Thai, ...) would otherwise collapse into one "word"; + # breaking between its characters keeps units at the character level instead — the only + # tokenizer-free option, and what ``BertPreTokenizer`` does anyway. + return not (is_unsegmented_script(last) or is_unsegmented_script(first)) def _masker_special_tokens(explainer) -> frozenset[str] | None: @@ -92,6 +92,25 @@ def _masker_special_tokens(explainer) -> frozenset[str] | None: return frozenset(specials) if specials else None +def _segment_offsets(explainer, text: str, n_segments: int) -> list[Span] | None: + """Character offsets of the tokens behind ``text``'s SHAP segments, or ``None`` when unavailable. + + Repeats the call ``shap.maskers.Text.token_segments`` makes on its offset-mapping path, whose + offsets it uses and discards. ``None`` — so spans fall back to locating the words in the text — + when no tokenizer is reachable, it cannot return offsets (a slow tokenizer: SHAP is then on its + leading-space regime), or the count does not match the segments. + """ + tokenizer = getattr(getattr(explainer, "masker", None), "tokenizer", None) + if tokenizer is None: + return None + try: + offsets = tokenizer(text, return_offsets_mapping=True)["offset_mapping"] + except (NotImplementedError, TypeError, KeyError, ValueError): + return None + spans = [(0, 0) if o is None else (int(o[0]), int(o[1])) for o in offsets] + return spans if len(spans) == n_segments else None + + def _resolve_text_model( model: TextModel, masker, output_space: Literal["probability", "logit"] ) -> tuple[object, object]: @@ -129,11 +148,14 @@ def _aggregate_subwords( contributions: np.ndarray, base_values: np.ndarray, special_tokens: frozenset[str] | None = None, -) -> tuple[list[str], np.ndarray, np.ndarray]: - """Merge SHAP's segments into whole words and fold specials into the baseline. + text: str | None = None, + offsets: Sequence[Span] | None = None, +) -> tuple[list[str], np.ndarray, np.ndarray, list[tuple[Span, ...]]]: + """Merge SHAP's segments into whole words, fold specials into the baseline, and locate each word. Word boundaries come from :func:`_merges`, so punctuation becomes its own unit while genuine - subword pieces still merge. Special-token attribution is added to ``base_values`` rather than + subword pieces still merge; with ``offsets``, tokens covering a shared character merge too (see + the module docstring). Special-token attribution is added to ``base_values`` rather than discarded, so ``base + Σ(word contributions)`` still equals the model output. Parameters @@ -148,25 +170,46 @@ def _aggregate_subwords( The masker tokenizer's ``all_special_tokens``. When given, non-blank specials are detected by membership — model-derived, not guessed. When ``None`` (a bare scoring callable, or a tokenizer-less masker), the bracket regex :data:`_BRACKET_SPECIAL_RE` stands in. + text : str or None + The explained text. Needed for ``offsets`` to be used, and to locate words without them. + offsets : sequence of (int, int) or None + Each segment's token offsets into ``text`` (:func:`_segment_offsets`). When given, word + spans and strings come from them; otherwise words are located in ``text`` by search, the + approximate path left for a masker that exposes no offsets. Returns ------- - tuple[list[str], np.ndarray, np.ndarray] - Word strings, per-word contributions ``(n_words, n_classes)``, and the adjusted baseline - ``(n_classes,)`` with special-token attribution folded in. + tuple[list[str], np.ndarray, np.ndarray, list[tuple[tuple[int, int], ...]]] + Word strings, per-word contributions ``(n_words, n_classes)``, the adjusted baseline + ``(n_classes,)`` with special-token attribution folded in, and each word's character spans + (``()`` where it could not be placed; all ``()`` without ``text``). """ + offs: Sequence[Span] = offsets if text is not None and offsets is not None and len(offsets) == len(tokens) else () + use_offsets = bool(offs) words: list[str] = [] word_rows: list[np.ndarray] = [] + word_spans: list[tuple[Span, ...]] = [] base = base_values.astype(float).copy() buffer_text = "" buffer_row: np.ndarray | None = None + buffer_positions: list[int] = [] def _flush() -> None: - nonlocal buffer_text, buffer_row + nonlocal buffer_text, buffer_row, buffer_positions if buffer_row is not None: - words.append(buffer_text.strip()) + span = trim_span(text, span_from_offsets(offs, buffer_positions)) if use_offsets and text else None + words.append(text[span[0] : span[1]] if span is not None and text is not None else buffer_text.strip()) word_rows.append(buffer_row) - buffer_text, buffer_row = "", None + word_spans.append((span,) if span is not None else ()) + buffer_text, buffer_row, buffer_positions = "", None, [] + + def _shares_a_character(i: int) -> bool: + """Whether token ``i + 1`` covers a character the current word already does.""" + if not use_offsets or i + 1 >= len(tokens): + return False + span = span_from_offsets(offs, buffer_positions) + start, end = offs[i + 1] + return span is not None and end > start and start < span[1] for i, (tok, row) in enumerate(zip(tokens, contributions, strict=True)): if _is_special(tok, special_tokens): @@ -175,15 +218,18 @@ def _flush() -> None: continue buffer_text += tok buffer_row = row.astype(float).copy() if buffer_row is None else buffer_row + row + buffer_positions.append(i) # A special token never continues a word (it is blank, or bracketed), so the raw next # segment is lookahead enough — no need to skip over specials to find the next content one. following = tokens[i + 1] if i + 1 < len(tokens) else "" - if not _merges(buffer_text, following): + if not (_merges(buffer_text, following) or _shares_a_character(i)): _flush() _flush() + if not use_offsets and text is not None: + word_spans = locate_spans(text, words) stacked = np.stack(word_rows, axis=0) if word_rows else np.zeros((0, contributions.shape[-1])) - return words, stacked, base + return words, stacked, base, word_spans class NlpShapBackend(NlpBackend): @@ -348,21 +394,26 @@ def run_explainer(self, x) -> NlpContributions: contributions: list[np.ndarray] = [] base_values: list[np.ndarray] = [] data: list[list[str]] = [] - for tokens, values, base in zip( - shap_explanation.data, shap_explanation.values, shap_explanation.base_values, strict=True + spans: list[list[tuple[Span, ...]]] = [] + for text, tokens, values, base in zip( + list(x), shap_explanation.data, shap_explanation.values, shap_explanation.base_values, strict=True ): - words, word_contribs, word_base = _aggregate_subwords( + words, word_contribs, word_base, word_spans = _aggregate_subwords( list(tokens), np.asarray(values), np.asarray(base), special_tokens=getattr(self, "_special_tokens", None), + text=str(text), + offsets=_segment_offsets(self.explainer, str(text), len(tokens)), ) data.append(words) contributions.append(word_contribs) base_values.append(word_base) + spans.append(word_spans) return NlpContributions( token_strings=data, values=contributions, base_values=np.stack(base_values, axis=0), + token_spans=spans, ) diff --git a/shapash/compute/spans.py b/shapash/compute/spans.py new file mode 100644 index 00000000..d9ec87ea --- /dev/null +++ b/shapash/compute/spans.py @@ -0,0 +1,269 @@ +"""Character spans — the coordinate every NLP backend's units share. + +Backends do not agree on what a unit is: SHAP merges its masker's segments into words, LIG groups +subwords by the tokenizer's ``word_ids()``, LIME splits on a regex and keeps one weight per distinct +word. They do not share a token axis either — SHAP segments the source string, LIME never sees a +tokenizer. What they all can say is *which characters of the source text a unit stands for*, so +that is the coordinate: each unit carries the ``(start, end)`` spans it covers, as Python string +indices into the text the backend explained. + +A unit has **one or more** spans: one for a word in a sequence backend, one per occurrence for a +bag-of-words unit (LIME's single weight for every occurrence of a word). A unit with no span is +one the backend could not place (only possible on a legacy, string-located artifact). + +This module is the array-level vocabulary on top of that coordinate: build spans +(:func:`span_from_offsets`, and the legacy :func:`locate_spans` / :func:`locate_all`), partition a +text into groups every unit fits in (:func:`merge_overlapping`), and aggregate unit values onto +groups (:func:`association_matrix`, :func:`aggregate`). Cross-backend comparison is one use; +reading explanations at word or sentence granularity is another. +""" + +from __future__ import annotations + +import re +import unicodedata +from collections.abc import Iterable, Sequence + +import numpy as np + +Span = tuple[int, int] +UnitSpans = tuple[Span, ...] + +# Scripts written without inter-word spaces: a word-boundary test sees a whole sentence as one +# word, so neither a boundary-guarded search nor a word/non-word split applies to them. Hangul is +# deliberately absent: Korean *is* space-segmented. +_UNSEGMENTED_SCRIPTS = ("CJK", "HIRAGANA", "KATAKANA", "THAI", "LAO", "KHMER", "MYANMAR") + + +def is_unsegmented_script(char: str) -> bool: + """True when ``char`` belongs to a script written without spaces between words.""" + return unicodedata.name(char, "").startswith(_UNSEGMENTED_SCRIPTS) + + +def span_from_offsets(offsets: Sequence[Sequence[int] | None], positions: Iterable[int]) -> Span | None: + """The character span covered by the tokens at ``positions``, from a tokenizer's offset mapping. + + Empty offsets (``start == end``: special tokens, and the zero-width offsets some tokenizers give + a bare word-start marker) cover no character and are skipped. + + Parameters + ---------- + offsets : sequence of (int, int) or None + ``offset_mapping`` of one encoded text, one entry per token. + positions : iterable of int + The token indices composing the unit. + + Returns + ------- + (int, int) or None + ``(min start, max end)`` over the covering tokens; ``None`` when none covers a character. + """ + covered: list[Span] = [] + for i in positions: + offset = offsets[i] + if offset is not None and offset[1] > offset[0]: + covered.append((int(offset[0]), int(offset[1]))) + if not covered: + return None + return min(start for start, _ in covered), max(end for _, end in covered) + + +def trim_span(text: str, span: Span | None) -> Span | None: + """``span`` shrunk to the visible characters of ``text`` it covers; ``None`` when there are none. + + Tokenizers disagree on whether an offset includes the space before a word: most trim it, some + byte-level BPE tokenizers (ModernBERT) keep it, so ``" didn"`` would cover ``(1, 6)`` where + another backend's ``"didn"`` covers ``(2, 6)``. Trimming every span makes units comparable and + their text clean, and a unit covering only whitespace (a lone ``Ġ``/``Ċ``) has no span at all. + """ + if span is None: + return None + start, end = span + while start < end and text[start].isspace(): + start += 1 + while end > start and text[end - 1].isspace(): + end -= 1 + return (start, end) if end > start else None + + +def merge_overlapping(spans: Iterable[Span]) -> list[Span]: + """Partition the covered text into the smallest groups that every input span fits inside. + + Spans that overlap — directly or through a chain of others — end up in one group; spans that + merely touch (``a.end == b.start``) do not. Applied to several backends' units at once, this is + the finest common unit they can all be read on: where they split a text alike, the groups are + their units; where one backend's unit spans several of another's (``"didn't"`` against + ``"didn" "'" "t"``), the group is the larger one. + + Parameters + ---------- + spans : iterable of (int, int) + Character spans, in any order. Empty spans are ignored. + + Returns + ------- + list of (int, int) + Disjoint groups, in text order. + """ + groups: list[Span] = [] + for start, end in sorted(s for s in spans if s[1] > s[0]): + if groups and start < groups[-1][1]: + groups[-1] = (groups[-1][0], max(groups[-1][1], end)) + else: + groups.append((start, end)) + return groups + + +def association_matrix(groups: Sequence[Span], units: Sequence[Sequence[Span]]) -> np.ndarray: + """How many of each unit's spans fall in each group: shape ``(n_groups, n_units)``. + + A span belongs to the group that contains its start. With groups from :func:`merge_overlapping` + over the same spans it is always wholly inside that group; a coarser partition built otherwise + (sentences) assigns a span straddling two groups to the first. A bag-of-words unit counts once + per occurrence, so its single weight reaches every place the word appears. + + Parameters + ---------- + groups : sequence of (int, int) + Disjoint spans, in text order. + units : sequence of sequence of (int, int) + Each unit's spans. + + Returns + ------- + np.ndarray + Integer counts. + """ + starts = np.array([g[0] for g in groups], dtype=int) + ends = np.array([g[1] for g in groups], dtype=int) + matrix = np.zeros((len(groups), len(units)), dtype=int) + for j, unit in enumerate(units): + for start, end in unit: + if end <= start: + continue + i = int(np.searchsorted(starts, start, side="right")) - 1 + if i >= 0 and start < ends[i]: + matrix[i, j] += 1 + return matrix + + +def aggregate(matrix: np.ndarray, values: np.ndarray) -> np.ndarray: + """Each group's value: the sum of its units' values, ``NaN`` where no unit falls in it. + + A sum, because a group of an additive backend's units then keeps the additive guarantee (its + value is the attribution of all of them together). ``NaN`` keeps "no unit here" apart from + "units here that sum to zero". + + Parameters + ---------- + matrix : np.ndarray + ``(n_groups, n_units)``, from :func:`association_matrix`. + values : np.ndarray + ``(n_units,)`` or ``(n_units, n_classes)``. + + Returns + ------- + np.ndarray + ``(n_groups,)`` or ``(n_groups, n_classes)``. + """ + values = np.asarray(values, dtype=float) + out = matrix.astype(float) @ values + empty = matrix.sum(axis=1) == 0 + out[empty] = np.nan + return out + + +# --- Legacy: spans recovered from strings ------------------------------------------------------ +# +# An artifact saved before units carried spans holds strings only. These recover spans by search, +# which is what alignment used to rely on everywhere. It is approximate by nature — a tokenizer's +# string is not always a substring of the text (stripped accents, ``[UNK]``, NFKC-normalised +# characters), and a miss cannot be told from a word that is not there — so it is only ever a +# fallback for such files. + + +def locate_spans(text: str, tokens: Sequence[str]) -> list[UnitSpans]: + """Each token's span in ``text``, searched left to right, ignoring case. + + Each search starts where the previous match ended, so a repeated word maps to its successive + occurrences. A token that cannot be found gets no span and leaves the cursor where it was. + + Parameters + ---------- + text : str + The source text the tokens were produced from. + tokens : sequence of str + Token strings in the order the backend emitted them. + + Returns + ------- + list of tuple of (int, int) + One span per located token, ``()`` for one not found. + """ + # Offsets found in the folded text must be offsets in ``text``, so the fold must keep its length: + # ``casefold`` does not always ("ß" -> "ss"), and then ``lower``, then no folding, stands in. + fold = next( + (f for f in (str.casefold, str.lower) if len(f(text)) == len(text)), + lambda s: s, + ) + haystack = fold(text) + cursor = 0 + spans: list[UnitSpans] = [] + for token in tokens: + needle = fold(str(token).strip()) + start = _find_word(haystack, needle, cursor) if needle else -1 + if start < 0: + spans.append(()) + continue + end = start + len(needle) + spans.append(((start, end),)) + cursor = end + return spans + + +def locate_all(text: str, tokens: Sequence[str]) -> list[UnitSpans]: + """Every whole-word occurrence of each token in ``text``, case-sensitive — for a bag of words. + + Parameters + ---------- + text : str + The source text. + tokens : sequence of str + Distinct words, each standing for all its occurrences. + + Returns + ------- + list of tuple of (int, int) + All spans of each token, ``()`` where it does not occur. + """ + out: list[UnitSpans] = [] + for token in tokens: + needle = str(token).strip() + if not needle: + out.append(()) + continue + found = tuple((m.start(), m.end()) for m in _word_pattern(needle).finditer(text)) + out.append(found) + return out + + +def _word_pattern(needle: str) -> re.Pattern: + """``needle`` not glued to a longer word on a side where it begins/ends with a word character.""" + left = r"(?<!\w)" if re.match(r"\w", needle) else "" + right = r"(?!\w)" if re.search(r"\w$", needle) else "" + return re.compile(left + re.escape(needle) + right) + + +def _find_word(haystack: str, needle: str, cursor: int) -> int: + """First occurrence of ``needle`` at or after ``cursor`` that is not part of a longer word. + + A plain substring search would place ``"i"`` inside ``"didn"``, and once one token lands too far + right every token after it is searched past its true place. The plain search is kept only for + scripts written without spaces, where there is no boundary to guard. + """ + found = _word_pattern(needle).search(haystack, cursor) + if found: + return found.start() + if any(is_unsegmented_script(c) for c in needle): + return haystack.find(needle, cursor) + return -1 diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 02cdfebb..8f199b61 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -57,6 +57,14 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.xpl" +# Version of how backends cut a text into units and what each unit carries. It enters the cache key +# because the key covers texts/model/backend but not shapash itself: when a release changes the +# units (v2 — every unit carries its character spans, unit strings are read off the source text, +# whitespace-only LIG units fold into the baseline), entries computed before would otherwise be +# served unchanged. Bump it whenever a backend's units change. +_UNITS_LAYOUT = "units-v2" + + def _load_cached(cache_path: Path | None) -> NlpExplanation | None: """Read a cached explanation, or return ``None`` when the entry cannot be read. @@ -386,6 +394,7 @@ def explain( token_strings=contributions.token_strings, values=contributions.values, base_values=contributions.base_values, + token_spans=contributions.token_spans, y_pred=y_pred, y_prob=y_prob, y_true=None, @@ -953,7 +962,7 @@ def _compute_key(self, text_list: list[str]) -> str: space = getattr(backend, "output_space", None) baseline = getattr(backend, "baseline_token", None) backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}:{space}:{baseline!r}" - return hash_corpus(text_list, f"{model_id}|{backend_id}|{self.label_names!r}") + return hash_corpus(text_list, f"{_UNITS_LAYOUT}|{model_id}|{backend_id}|{self.label_names!r}") def _require_text_model(self) -> TextModel: text_model = getattr(self, "_text_model", None) diff --git a/shapash/explainer/nlp_explanation.py b/shapash/explainer/nlp_explanation.py index 75420e74..4f9e1bd1 100644 --- a/shapash/explainer/nlp_explanation.py +++ b/shapash/explainer/nlp_explanation.py @@ -318,6 +318,11 @@ class NlpExplanation: The token the backend substituted for an absent word (``"[MASK]"``, ``"[PAD]"``, ``"..."``) — read off the backend instance at ``explain()`` time. ``None`` when the method has no single substitute (LIME removing words) or when the file predates this field. + token_spans : list[list[tuple[tuple[int, int], ...]]] or None + Per sample, per unit: the ``(start, end)`` character spans of ``texts`` the unit stands for + — one for a word, one per occurrence for a bag-of-words unit (LIME). The coordinate every + backend shares, which is what lets two backends' units be compared without matching strings + (:mod:`shapash.compute.spans`). ``None`` on a file saved before this field existed. """ texts: pd.Series @@ -339,6 +344,7 @@ class NlpExplanation: architecture: str | None = None is_signed: bool = True baseline_token: str | None = None + token_spans: list[list[tuple[tuple[int, int], ...]]] | None = None # ClassVar, not a field: it is a shared constant, not per-explanation data, so it stays out # of ``fields()`` — and therefore out of ``__init__``, ``replace()`` and ``save()``. @@ -371,6 +377,25 @@ def __post_init__(self) -> None: if self.base_values is not None: self.base_values.setflags(write=False) self._check_shared_index() + self._check_token_spans() + + def _check_token_spans(self) -> None: + """:attr:`token_spans`, when present, must give every unit of every sample its spans. + + Raises + ------ + ValueError + If a sample has a different number of span entries than of units. + """ + if self.token_spans is None: + return + if len(self.token_spans) != len(self.token_strings): + raise ValueError( + f"token_spans has {len(self.token_spans)} sample(s), token_strings {len(self.token_strings)}." + ) + for i, (spans, tokens) in enumerate(zip(self.token_spans, self.token_strings, strict=True)): + if len(spans) != len(tokens): + raise ValueError(f"Sample {i}: {len(spans)} token_spans entries for {len(tokens)} unit(s).") def _check_shared_index(self) -> None: """Every index-bearing field must be indexed like :attr:`texts`. @@ -410,6 +435,7 @@ def _check_shared_index(self) -> None: "output_space", "is_signed", "baseline_token", + "token_spans", "model_id", "architecture", } @@ -1006,6 +1032,7 @@ def save(self, path: str | Path) -> None: """ contrib_df, base_df, values_ndim = _contributions_to_frames(self.token_strings, self.values, self.base_values) samples_df = _samples_to_frame(self.texts, self.y_pred, self.y_prob, self.y_true) + spans_df = None if self.token_spans is None else _spans_to_frame(self.token_spans) meta = { "shapash_version": _shapash_version, @@ -1028,6 +1055,7 @@ def save(self, path: str | Path) -> None: "values_ndim": values_ndim, "has_base_values": base_df is not None, "has_ground_truth": self.y_true is not None, + "has_token_spans": spans_df is not None, } with zipfile.ZipFile(Path(path), "w", compression=zipfile.ZIP_DEFLATED) as zf: @@ -1036,6 +1064,8 @@ def save(self, path: str | Path) -> None: if base_df is not None: _write_parquet(zf, "base_values.parquet", base_df) _write_parquet(zf, "samples.parquet", samples_df) + if spans_df is not None: + _write_parquet(zf, "token_spans.parquet", spans_df) @classmethod def load(cls, path: str | Path) -> NlpExplanation: @@ -1056,6 +1086,8 @@ def load(cls, path: str | Path) -> NlpExplanation: contrib_df = _read_parquet(zf, "contributions.parquet") base_df = _read_parquet(zf, "base_values.parquet") if meta.get("has_base_values", True) else None samples_df = _read_parquet(zf, "samples.parquet") + # Absent on a file saved before units carried spans. + spans_df = _read_parquet(zf, "token_spans.parquet") if meta.get("has_token_spans", False) else None token_strings, values, base_values = _frames_to_contributions( contrib_df, base_df, meta["values_ndim"], meta["n_samples"], meta["n_classes"] @@ -1088,6 +1120,7 @@ def load(cls, path: str | Path) -> NlpExplanation: # the fact rather than showing a stale placeholder. model_id=meta.get("model_id"), architecture=meta.get("architecture"), + token_spans=None if spans_df is None else _frame_to_spans(spans_df, [len(t) for t in token_strings]), ) @@ -1189,6 +1222,25 @@ def _frames_to_contributions( return token_strings, values, base_values +def _spans_to_frame(token_spans: list[list[tuple[tuple[int, int], ...]]]) -> pd.DataFrame: + """Per-unit spans -> one row per span. A unit with several spans (LIME) has several rows, one with none has none.""" + rows = [ + (sample_idx, token_idx, start, end) + for sample_idx, sample in enumerate(token_spans) + for token_idx, unit in enumerate(sample) + for start, end in unit + ] + return pd.DataFrame(rows, columns=["sample_idx", "token_idx", "start", "end"]).astype("int64") + + +def _frame_to_spans(df: pd.DataFrame, n_units: list[int]) -> list[list[tuple[tuple[int, int], ...]]]: + """Inverse of :func:`_spans_to_frame`; ``n_units`` restores units (and samples) that had no span.""" + collected: list[list[list[tuple[int, int]]]] = [[[] for _ in range(n)] for n in n_units] + for sample_idx, token_idx, start, end in df[["sample_idx", "token_idx", "start", "end"]].itertuples(index=False): + collected[sample_idx][token_idx].append((int(start), int(end))) + return [[tuple(unit) for unit in sample] for sample in collected] + + def _samples_to_frame( texts: pd.Series, y_pred: pd.Series, y_prob: pd.DataFrame | None, y_true: pd.Series | None ) -> pd.DataFrame: diff --git a/shapash/model/base.py b/shapash/model/base.py index 4f4e6c28..da5e0f21 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -465,6 +465,22 @@ def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[in """ return None + def token_offsets(self, text: str) -> list[tuple[int, int]] | None: + """Character offsets into ``text`` of each token :meth:`encode` produces. + + Positions index the same subword axis as :meth:`encode` and :meth:`word_alignment`. Special + tokens cover no character (``(0, 0)``). This is what places an attributed word in the source + text, so it can be shown as written and compared with another backend's units without + matching strings (an uncased tokenizer's ``cafe`` is not a substring of ``café``). + + Returns + ------- + list of (int, int) or None + ``(start, end)`` per token, or ``None`` when the tokenizer cannot report offsets (a slow + tokenizer), so the caller falls back to locating words by their strings. + """ + return None + class SupportsLogits(ABC): """Capability: score raw text in pre-softmax logit space. diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index 70aaab23..c8511778 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -761,3 +761,16 @@ def word_alignment(self, text: str) -> tuple[list[str], list[list[int]], list[in word_positions[-1].append(i) words = [self.tokenizer.convert_tokens_to_string([tokens[i] for i in pos]).strip() for pos in word_positions] return words, word_positions, special_positions + + def token_offsets(self, text: str) -> list[tuple[int, int]] | None: + """Character offsets of each token, re-encoded with :meth:`encode`'s truncation (see base ABC). + + ``None`` for a slow tokenizer, which cannot return an offset mapping. + """ + if not getattr(self.tokenizer, "is_fast", False): + return None + try: + offsets = self._tokenize(text, return_offsets_mapping=True)["offset_mapping"] + except (NotImplementedError, TypeError, KeyError): + return None + return [(0, 0) if o is None else (int(o[0]), int(o[1])) for o in offsets] diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py index df39c66c..7c982067 100644 --- a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -10,7 +10,12 @@ import numpy as np from shapash.backend import NlpCaptumLigBackend, get_backend_cls_from_name -from shapash.backend.nlp_captum_lig_backend import _aggregate_by_alignment, _aggregate_subwords +from shapash.backend.nlp_captum_lig_backend import ( + _aggregate_by_alignment, + _aggregate_subwords, + _place_words, + _valid_offsets, +) from shapash.model.base import SupportsCaptumIG, TextModel @@ -131,6 +136,44 @@ def test_preserves_completeness(self): np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) +class TestPlaceWords(unittest.TestCase): + """Words read off the source text through token offsets — offsets copied from real tokenizers.""" + + def test_words_are_shown_as_written(self): + # distilbert-base-uncased: the tokenizer rebuilds "cafe" and "[UNK]"; the text says otherwise. + text = "Le café 😍" + offsets = [(0, 0), (0, 2), (3, 6), (6, 7), (8, 9), (0, 0)] + words, values, base, spans = _place_words( + text, ["le", "cafe", "[UNK]"], np.array([[1.0], [2.0], [3.0]]), np.zeros(1), [[1], [2, 3], [4]], offsets + ) + self.assertEqual(words, ["Le", "café", "😍"]) + self.assertEqual(spans, [((0, 2),), ((3, 7),), ((8, 9),)]) + np.testing.assert_allclose(values[:, 0], [1.0, 2.0, 3.0]) + self.assertEqual(base[0], 0.0) + + def test_whitespace_only_words_fold_into_the_baseline(self): + # roberta-base on "Hello world\n\nok": a lone "Ġ" (zero-width) and two "Ċ" get word ids. + text = "Hello world\n\nok" + offsets = [(0, 0), (0, 5), (6, 6), (7, 12), (12, 13), (13, 14), (14, 16), (0, 0)] + positions = [[1], [2], [3], [4], [5], [6]] + contribs = np.arange(1.0, 7.0)[:, None] + words, values, base, _ = _place_words( + text, ["Hello", "", "world", "", "", "ok"], contribs, np.zeros(1), positions, offsets + ) + self.assertEqual(words, ["Hello", "world", "ok"]) + np.testing.assert_allclose(values[:, 0], [1.0, 3.0, 6.0]) + self.assertEqual(base[0], 2 + 4 + 5) # completeness kept: nothing is dropped + + def test_without_offsets_words_keep_their_strings_and_are_located(self): + words, _, _, spans = _place_words("I am", ["i", "am"], np.ones((2, 1)), np.zeros(1), None, None) + self.assertEqual((words, spans), (["i", "am"], [((0, 1),), ((2, 4),)])) + + def test_valid_offsets_need_one_entry_per_token(self): + self.assertIsNone(_valid_offsets([(0, 1)], n_tokens=2)) + self.assertIsNone(_valid_offsets(None, n_tokens=2)) + self.assertEqual(_valid_offsets([(0, 1), (1, 2)], n_tokens=2), [(0, 1), (1, 2)]) + + class TestProgressIter(unittest.TestCase): """``_progress_iter`` is captum-free — exercise it via ``object.__new__`` to skip ``__init__``.""" diff --git a/tests/unit_tests/backend/test_nlp_lime_backend.py b/tests/unit_tests/backend/test_nlp_lime_backend.py index 7b43979d..bf1027da 100644 --- a/tests/unit_tests/backend/test_nlp_lime_backend.py +++ b/tests/unit_tests/backend/test_nlp_lime_backend.py @@ -125,6 +125,15 @@ def hf_model(texts): # --- run_explainer --- + def test_run_explainer_records_every_occurrence_of_each_word(self): + text = "good movie, good cast" + raw = self.backend.run_explainer([text]) + spans = dict(zip(raw.token_strings[0], raw.token_spans[0], strict=True)) + self.assertEqual(spans["good"], ((0, 4), (12, 16))) + self.assertEqual(spans["cast"], ((17, 21),)) + for word, unit in spans.items(): + self.assertTrue(all(text[a:b] == word for a, b in unit)) + def test_run_explainer_returns_nlp_contributions(self): raw = self.backend.run_explainer(_SAMPLE_TEXTS) self.assertIsInstance(raw, NlpContributions) @@ -193,7 +202,9 @@ def test_feature_count_follows_the_explainer_settings(self): self.assertEqual(self.backend._count_features(""), 1) def test_explicit_num_features_is_respected(self): - with patch.object(self.backend.explainer, "explain_instance", wraps=self.backend.explainer.explain_instance) as spy: + with patch.object( + self.backend.explainer, "explain_instance", wraps=self.backend.explainer.explain_instance + ) as spy: self.backend.run_explainer(_SAMPLE_TEXTS[:1]) self.assertEqual(spy.call_args.kwargs["num_features"], _LIME_COMPUTE_ARGS["num_features"]) diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index a6d290e7..271fa0c0 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -10,7 +10,7 @@ import numpy as np from shapash.backend import NlpShapBackend, get_backend_cls_from_name -from shapash.backend.nlp_shap_backend import _aggregate_subwords +from shapash.backend.nlp_shap_backend import _aggregate_subwords, _segment_offsets from shapash.model.base import SupportsLogits, TextModel @@ -32,7 +32,7 @@ def test_merges_subwords_and_drops_specials(self): contribs = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 2.0], [4.0, 3.0], [5.0, 4.0], [6.0, 5.0]], dtype=float) base = np.array([10.0, 20.0]) - words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + words, word_contribs, new_base, _ = _aggregate_subwords(tokens, contribs, base) self.assertEqual(words, ["i", "am", "happy"]) # "happy" = "hap" + "py ": contributions summed. @@ -48,7 +48,7 @@ def test_splits_contraction_at_the_apostrophe(self): contribs = np.arange(8 * 2, dtype=float).reshape(8, 2) base = np.zeros(2) - words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + words, word_contribs, new_base, _ = _aggregate_subwords(tokens, contribs, base) self.assertEqual(words, ["i", "don", "'", "t", "feel", "sad"]) np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) @@ -81,7 +81,7 @@ def test_punctuation_is_not_glued_to_neighbouring_words(self): ] contribs = np.arange(len(tokens), dtype=float).reshape(-1, 1) - words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, word_contribs, new_base, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual( words, @@ -115,7 +115,7 @@ def test_leading_space_regime_does_not_collapse_the_sample(self): tokens = ["", "The", " acting", " was", " superb", "!", "!", "!", " I", " enjoy", ""] contribs = np.arange(len(tokens), dtype=float).reshape(-1, 1) - words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, _, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual(words, ["The", "acting", "was", "superb", "!", "!", "!", "I", "enjoy"]) @@ -123,7 +123,7 @@ def test_subwords_merge_in_both_segment_regimes(self): for tokens in (["up", "dating "], [" up", "dating"]): with self.subTest(tokens=tokens): contribs = np.array([[1.0], [2.0]]) - words, word_contribs, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, word_contribs, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual(words, ["updating"]) np.testing.assert_allclose(word_contribs, [[3.0]]) @@ -131,7 +131,7 @@ def test_unsegmented_script_does_not_collapse_into_one_word(self): # CJK has no inter-word spaces, so a character-class rule would see one long word run. tokens = ["\u6211", "\u559c", "\u6b22"] contribs = np.arange(3, dtype=float).reshape(-1, 1) - words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, _, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual(words, ["\u6211", "\u559c", "\u6b22"]) def test_korean_subwords_still_merge(self): @@ -139,7 +139,7 @@ def test_korean_subwords_still_merge(self): # Hangul is deliberately excluded from the unsegmented-script list. tokens = ["\uc601\ud654", "\ub294 ", "\uc88b", "\uc558\ub2e4"] contribs = np.arange(4, dtype=float).reshape(-1, 1) - words, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, _, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual(words, ["\uc601\ud654\ub294", "\uc88b\uc558\ub2e4"]) def test_bracket_text_folds_into_baseline_only_without_a_special_set(self): @@ -147,21 +147,21 @@ def test_bracket_text_folds_into_baseline_only_without_a_special_set(self): tokens = ["ha ", "[LAUGHTER] ", "good "] contribs = np.array([[1.0], [2.0], [3.0]]) - words, _, new_base = _aggregate_subwords( + words, _, new_base, _ = _aggregate_subwords( tokens, contribs, np.zeros(1), special_tokens=frozenset({"[CLS]", "[SEP]"}) ) self.assertEqual(words, ["ha", "[LAUGHTER]", "good"]) np.testing.assert_allclose(new_base, [0.0]) # Without one (bare callable), the bracket regex still guards against leaking [CLS]/[SEP]. - words, _, new_base = _aggregate_subwords(["[CLS] ", "good ", "[SEP] "], contribs, np.zeros(1)) + words, _, new_base, _ = _aggregate_subwords(["[CLS] ", "good ", "[SEP] "], contribs, np.zeros(1)) self.assertEqual(words, ["good"]) np.testing.assert_allclose(new_base, [4.0]) def test_declared_special_token_folds_into_baseline(self): tokens = ["<s> ", "good ", "</s> "] contribs = np.array([[1.0], [2.0], [3.0]]) - words, word_contribs, new_base = _aggregate_subwords( + words, word_contribs, new_base, _ = _aggregate_subwords( tokens, contribs, np.zeros(1), special_tokens=frozenset({"<s>", "</s>"}) ) self.assertEqual(words, ["good"]) @@ -173,7 +173,7 @@ def test_preserves_completeness(self): contribs = np.random.default_rng(0).normal(size=(5, 3)) base = np.array([0.5, -0.5, 1.0]) - _, word_contribs, new_base = _aggregate_subwords(tokens, contribs, base) + _, word_contribs, new_base, _ = _aggregate_subwords(tokens, contribs, base) # base + Σ over words must equal the original base + Σ over every token (nothing lost). np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) @@ -181,7 +181,7 @@ def test_preserves_completeness(self): def test_all_special_returns_empty_words(self): tokens = ["", ""] contribs = np.array([[1.0, 2.0], [3.0, 4.0]]) - words, word_contribs, new_base = _aggregate_subwords(tokens, contribs, np.zeros(2)) + words, word_contribs, new_base, _ = _aggregate_subwords(tokens, contribs, np.zeros(2)) self.assertEqual(words, []) self.assertEqual(word_contribs.shape, (0, 2)) np.testing.assert_allclose(new_base, [4.0, 6.0]) @@ -190,11 +190,86 @@ def test_last_token_without_trailing_space_is_flushed(self): # The final real word before the closing special token often has no trailing space. tokens = ["", "hello ", "world", ""] contribs = np.array([[0.0], [1.0], [2.0], [0.0]]) - words, word_contribs, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) + words, word_contribs, _, _ = _aggregate_subwords(tokens, contribs, np.zeros(1)) self.assertEqual(words, ["hello", "world"]) np.testing.assert_allclose(word_contribs, [[1.0], [2.0]]) +class TestWordSpans(unittest.TestCase): + """With the masker tokenizer's offsets, words are placed in the source text and read off it. + + Segments and offsets are copied from real ``shap.maskers.Text(tokenizer).token_segments`` runs. + """ + + def test_tokens_sharing_a_character_are_one_word_read_from_the_source(self): + # albert-base-v2: SentencePiece gives a bare "▁" the offsets of the "é" after it, so + # token_segments hands that "é" to two tokens and concatenation printed "éétait". + text = "Le café était bon" + segments = ["", "Le ", "café ", "é", "éta", "it ", "bon", ""] + offsets = [(0, 0), (0, 2), (3, 7), (8, 9), (8, 11), (11, 13), (14, 17), (0, 0)] + contribs = np.arange(8, dtype=float)[:, None] + words, values, base, spans = _aggregate_subwords(segments, contribs, np.zeros(1), text=text, offsets=offsets) + self.assertEqual(words, ["Le", "café", "était", "bon"]) + self.assertEqual(spans, [((0, 2),), ((3, 7),), ((8, 13),), ((14, 17),)]) + np.testing.assert_allclose(values[:, 0], [1.0, 2.0, 3 + 4 + 5, 6.0]) + self.assertEqual(base[0], 0 + 7) + + def test_a_multi_byte_character_split_across_tokens_is_one_word(self): + # roberta-base: byte-level BPE splits the emoji into two tokens mapped to the same character; + # each became its own "😍" unit. + text = "I love it 😍 so" + segments = ["", "I ", "love ", "it ", "😍", "😍 ", "so", ""] + offsets = [(0, 0), (0, 1), (2, 6), (7, 9), (10, 11), (10, 11), (12, 14), (0, 0)] + words, values, _, spans = _aggregate_subwords( + segments, np.ones((8, 1)), np.zeros(1), text=text, offsets=offsets + ) + self.assertEqual(words, ["I", "love", "it", "😍", "so"]) + self.assertEqual(spans[3], ((10, 11),)) + self.assertEqual(values[3, 0], 2.0) + + def test_without_offsets_words_are_located_by_search(self): + words, _, _, spans = _aggregate_subwords( + ["", "up", "dating ", ""], np.ones((4, 1)), np.zeros(1), text="updating" + ) + self.assertEqual((words, spans), (["updating"], [((0, 8),)])) + + def test_without_text_there_are_no_spans(self): + _, _, _, spans = _aggregate_subwords(["a ", "b"], np.ones((2, 1)), np.zeros(1)) + self.assertEqual(spans, [(), ()]) + + def test_offsets_that_do_not_match_the_segments_are_ignored(self): + words, _, _, spans = _aggregate_subwords( + ["ab "], np.ones((1, 1)), np.zeros(1), text="ab", offsets=[(0, 1), (1, 2)] + ) + self.assertEqual((words, spans), (["ab"], [((0, 2),)])) + + +class _OffsetTokenizer: + def __init__(self, offsets=None, error=None): + self.offsets, self.error = offsets, error + + def __call__(self, text, return_offsets_mapping=False): + if self.error is not None: + raise self.error + return {"offset_mapping": self.offsets} + + +def _explainer_with(tokenizer): + masker = type("Masker", (), {"tokenizer": tokenizer})() + return type("Explainer", (), {"masker": masker})() + + +class TestSegmentOffsets(unittest.TestCase): + def test_reads_the_masker_tokenizer(self): + explainer = _explainer_with(_OffsetTokenizer([(0, 0), (0, 2), None])) + self.assertEqual(_segment_offsets(explainer, "ab", 3), [(0, 0), (0, 2), (0, 0)]) + + def test_none_when_unavailable_or_mismatched(self): + self.assertIsNone(_segment_offsets(object(), "ab", 1)) + self.assertIsNone(_segment_offsets(_explainer_with(_OffsetTokenizer(error=NotImplementedError())), "ab", 1)) + self.assertIsNone(_segment_offsets(_explainer_with(_OffsetTokenizer([(0, 2)])), "ab", 3)) + + class FakeShapExplanation: def __init__(self, data, values, base_values): self.data = data @@ -222,6 +297,7 @@ def test_aggregates_subwords_end_to_end(self): raw = backend.run_explainer(["updating"]) self.assertEqual(raw.token_strings, [["updating"]]) + self.assertEqual(raw.token_spans, [[((0, 8),)]]) np.testing.assert_allclose(raw.values[0], [[5.0, 3.0]]) # CLS ([1,0]) + SEP ([4,3]) folded into the base. np.testing.assert_allclose(raw.base_values, [[0.1 + 1.0 + 4.0, 0.2 + 0.0 + 3.0]]) diff --git a/tests/unit_tests/compute/test_spans.py b/tests/unit_tests/compute/test_spans.py new file mode 100644 index 00000000..e627812c --- /dev/null +++ b/tests/unit_tests/compute/test_spans.py @@ -0,0 +1,122 @@ +"""Unit tests for ``shapash.compute.spans`` — character spans, the coordinate NLP backends share.""" + +import numpy as np +import pytest + +from shapash.compute.spans import ( + aggregate, + association_matrix, + locate_all, + locate_spans, + merge_overlapping, + span_from_offsets, + trim_span, +) + + +class TestSpanFromOffsets: + def test_covers_the_tokens_of_a_word(self): + offsets = [(0, 0), (0, 3), (3, 7), (8, 10), (0, 0)] + assert span_from_offsets(offsets, [1, 2]) == (0, 7) + + def test_zero_width_and_missing_offsets_cover_nothing(self): + # A special token (0, 0), a bare word-start marker (3, 3), a None from a tokenizer. + offsets = [(0, 0), (3, 3), None, (3, 4)] + assert span_from_offsets(offsets, [0, 1, 2]) is None + assert span_from_offsets(offsets, [1, 3]) == (3, 4) + + def test_tokens_sharing_a_character(self): + # Byte-level BPE splits "é" into two tokens that both map to it. + assert span_from_offsets([(6, 7), (6, 7)], [0, 1]) == (6, 7) + + +class TestTrimSpan: + @pytest.mark.parametrize( + ("span", "trimmed"), + [((1, 6), (2, 6)), ((0, 3), (0, 3)), ((6, 8), None), ((2, 7), (2, 6)), (None, None)], + ) + def test_shrinks_to_visible_characters(self, span, trimmed): + # ModernBERT keeps the space before a word in its offsets: " didn" at (1, 6). + assert trim_span("I didn t", span) == trimmed + + +class TestMergeOverlapping: + def test_identical_partitions_stay_as_they_are(self): + units = [(0, 4), (4, 5), (5, 6)] + assert merge_overlapping(units + units) == units + + def test_a_coarser_unit_absorbs_the_finer_ones_it_covers(self): + # SHAP "didn" "'" "t" against SentencePiece LIG "didn't". + assert merge_overlapping([(2, 6), (6, 7), (7, 8), (2, 8)]) == [(2, 8)] + + def test_chains_of_overlap_merge_and_touching_spans_do_not(self): + assert merge_overlapping([(0, 3), (2, 5), (4, 6), (6, 8)]) == [(0, 6), (6, 8)] + + def test_order_and_empty_spans_are_ignored(self): + assert merge_overlapping([(5, 6), (3, 3), (0, 2)]) == [(0, 2), (5, 6)] + + +class TestAssociationAndAggregate: + def test_each_occurrence_of_a_bag_of_words_unit_counts(self): + groups = [(0, 4), (5, 9), (10, 14)] + units = [((0, 4), (10, 14)), ((5, 9),), ()] + matrix = association_matrix(groups, units) + np.testing.assert_array_equal(matrix, [[1, 0, 0], [0, 1, 0], [1, 0, 0]]) + np.testing.assert_array_equal(aggregate(matrix, np.array([2.0, 3.0, 99.0])), [2.0, 3.0, 2.0]) + + def test_a_group_sums_its_units_and_a_group_without_any_is_nan(self): + groups = [(0, 8), (8, 9)] + matrix = association_matrix(groups, [((2, 6),), ((6, 7),), ((7, 8),)]) + out = aggregate(matrix, np.array([[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]])) + np.testing.assert_array_equal(out[0], [6.0, 60.0]) + assert np.isnan(out[1]).all() + + def test_a_span_straddling_two_groups_goes_to_the_one_holding_its_start(self): + np.testing.assert_array_equal(association_matrix([(0, 5), (5, 10)], [((3, 7),)]), [[1], [0]]) + + +class TestLocateSpans: + """The legacy path, for artifacts saved before units carried spans.""" + + def test_repeated_words_map_to_successive_occurrences(self): + assert locate_spans("a cat and a cat", ["a", "cat", "and", "a", "cat"]) == [ + ((0, 1),), + ((2, 5),), + ((6, 9),), + ((10, 11),), + ((12, 15),), + ] + + def test_case_is_ignored(self): + assert locate_spans("The cat", ["the", "CAT"]) == [((0, 3),), ((4, 7),)] + + def test_a_short_word_is_not_found_inside_a_longer_one(self): + assert locate_spans("I didn't, did i", ["didn", "i"]) == [((2, 6),), ((14, 15),)] + + def test_contraction_pieces_still_match(self): + assert locate_spans("didn't", ["didn", "'", "t"]) == [((0, 4),), ((4, 5),), ((5, 6),)] + + def test_missing_token_gets_no_span_and_keeps_the_cursor(self): + assert locate_spans("a b", ["a", "[SEP]", "b"]) == [((0, 1),), (), ((2, 3),)] + + def test_a_rewritten_word_is_not_placed_inside_another(self): + # An uncased tokenizer's "tre" (from "très") used to land inside a later "entre", sending + # every following token past its place. + assert locate_spans("très bien entre nous", ["tre", "bien", "entre"]) == [(), ((5, 9),), ((10, 15),)] + + def test_unspaced_script_falls_back_to_substring_search(self): + assert locate_spans("我喜欢你", ["我", "喜欢", "你"]) == [((0, 1),), ((1, 3),), ((3, 4),)] + + def test_length_changing_case_fold_keeps_offsets_on_the_text(self): + text = "Straße ok" + assert [text[a:b] for ((a, b),) in locate_spans(text, ["Straße", "ok"])] == ["Straße", "ok"] + + +class TestLocateAll: + def test_every_whole_word_occurrence_case_sensitive(self): + assert locate_all("Good good, Good! goods", ["Good", "good", "bad", ""]) == [ + ((0, 4), (11, 15)), + ((5, 9),), + (), + (), + ] diff --git a/tests/unit_tests/explainer/test_nlp_explanation.py b/tests/unit_tests/explainer/test_nlp_explanation.py index b7023dc9..b5ec9952 100644 --- a/tests/unit_tests/explainer/test_nlp_explanation.py +++ b/tests/unit_tests/explainer/test_nlp_explanation.py @@ -209,6 +209,54 @@ def test_legacy_file_without_semantics_fields_reads_as_signed_with_unknown_basel self.assertIsNone(loaded.baseline_token) +class TestTokenSpans(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.path = Path(self._tmp.name) / "spans.zip" + # One span per word, several for a bag-of-words unit, none for an unplaced unit; a sample + # with zero units. + self.spans = [[((0, 5),), ((6, 11),)], [((0, 1),), ((2, 4), (20, 22)), ()], []] + self.expl = replace( + _make_explanation(values_ndim=2, with_base=True, with_true=False, with_prob=False), token_spans=self.spans + ) + + def test_round_trip(self): + self.expl.save(self.path) + with zipfile.ZipFile(self.path) as zf: + self.assertTrue(json.loads(zf.read("meta.json"))["has_token_spans"]) + self.assertEqual(NlpExplanation.load(self.path).token_spans, self.spans) + + def test_a_file_without_spans_loads_none(self): + replace(self.expl, token_spans=None).save(self.path) + with zipfile.ZipFile(self.path) as zf: + self.assertNotIn("token_spans.parquet", zf.namelist()) + self.assertIsNone(NlpExplanation.load(self.path).token_spans) + + def test_a_file_from_before_spans_existed_loads_none(self): + self.expl.save(self.path) + with zipfile.ZipFile(self.path) as zin: + meta = json.loads(zin.read("meta.json")) + del meta["has_token_spans"] + members = {i.filename: zin.read(i.filename) for i in zin.infolist() if i.filename != "token_spans.parquet"} + members["meta.json"] = json.dumps(meta).encode() + legacy = self.path.with_name("legacy.zip") + with zipfile.ZipFile(legacy, "w") as zout: + for name, data in members.items(): + zout.writestr(name, data) + self.assertIsNone(NlpExplanation.load(legacy).token_spans) + + def test_spans_must_match_the_units(self): + with self.assertRaisesRegex(ValueError, "Sample 1: 2 token_spans entries for 3"): + replace(self.expl, token_spans=[self.spans[0], self.spans[1][:2], []]) + with self.assertRaisesRegex(ValueError, "2 sample"): + replace(self.expl, token_spans=self.spans[:2]) + + def test_is_a_computed_field_carried_by_relabelled(self): + relabelled = self.expl.relabelled(self.expl.texts.reset_index(drop=True)) + self.assertIs(relabelled.token_spans, self.expl.token_spans) + + class TestCorpusIdentity(unittest.TestCase): """``corpus_id`` — the texts-only digest that pairs an explanation with its embeddings. diff --git a/tests/unit_tests/model/test_torch_models.py b/tests/unit_tests/model/test_torch_models.py index 8faa2a20..3b472402 100644 --- a/tests/unit_tests/model/test_torch_models.py +++ b/tests/unit_tests/model/test_torch_models.py @@ -6,6 +6,7 @@ shared spine unchanged. """ +import re import unittest from types import SimpleNamespace @@ -302,12 +303,18 @@ class _FastFakeTokenizer(_FakeTokenizer): is_fast = True - def __call__(self, texts, padding=True, truncation=True, return_tensors=None, max_length=None): + def __call__( + self, texts, padding=True, truncation=True, return_tensors=None, max_length=None, return_offsets_mapping=False + ): self.calls.append({"truncation": truncation, "max_length": max_length}) words = texts.split() if isinstance(texts, str) else list(texts) if truncation and max_length is not None: words = words[:max_length] - return _FastEnc(list(range(len(words))), list(range(len(words)))) + enc = _FastEnc(list(range(len(words))), list(range(len(words)))) + if return_offsets_mapping: + starts = [m.start() for m in re.finditer(r"\S+", texts)][: len(words)] + enc["offset_mapping"] = [(start, start + len(word)) for start, word in zip(starts, words, strict=True)] + return enc def convert_ids_to_tokens(self, ids): return [f"t{i}" for i in ids] @@ -349,11 +356,13 @@ def test_every_tokenization_path_uses_it(self): "encode": lambda m: m.encode(long_text), "token_gradients": lambda m: m.token_gradients(long_text, target_class=0), "word_alignment": lambda m: m.word_alignment(long_text), + "token_offsets": lambda m: m.token_offsets(long_text), } for name, run in paths.items(): with self.subTest(path=name): - # word_alignment short-circuits on a slow tokenizer, so give it a fast one. - tokenizer = _FastFakeTokenizer() if name == "word_alignment" else _FakeTokenizer() + # word_alignment/token_offsets short-circuit on a slow tokenizer, so give them a fast one. + fast = name in {"word_alignment", "token_offsets"} + tokenizer = _FastFakeTokenizer() if fast else _FakeTokenizer() model = self._model(max_length=3, tokenizer=tokenizer) run(model) self.assertTrue(model.tokenizer.calls, f"{name} performed no tokenization") @@ -369,6 +378,12 @@ def test_word_alignment_positions_stay_within_the_truncated_axis(self): self.assertEqual(len(words), 3) self.assertTrue(all(p < 3 for pos in word_positions for p in pos)) + def test_token_offsets_index_the_truncated_axis_and_need_a_fast_tokenizer(self): + text = "alpha beta gamma delta" + offsets = self._model(max_length=3, tokenizer=_FastFakeTokenizer()).token_offsets(text) + self.assertEqual([text[a:b] for a, b in offsets], ["alpha", "beta", "gamma"]) + self.assertIsNone(self._model(tokenizer=_FakeTokenizer()).token_offsets(text)) + def test_encode_and_gradients_agree_on_length(self): model = self._model(max_length=4) long_text = " ".join(f"w{i}" for i in range(20)) From f9011854adf0a4e8385b4efb7e5b4d4e1b35cb8e Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 11:04:00 +0200 Subject: [PATCH 138/144] refactor(nlp): align backend comparison on character spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend comparison now matches units by the characters they cover instead of searching for their strings in the text. String search failed silently on every tokenizer except DistilBERT's: words were dropped (stripped accents, glued punctuation, emoji) or given another word's value (naive vs naïve). - align_units merges all backends' units into the finest groups every unit fits in, then sums each backend's values per group. A group with no unit is NaN. Where one backend's unit covers several of another's ("didn't" against "didn" "'" "t"), the finer units are summed into it, so no attribution is lost. LIME's single weight per word counts at each occurrence. - align_row reads all backends on these shared units instead of the reference backend's words. - align_token_values and locate_tokens are removed. - Explanations saved without spans fall back to string search, and check_comparable warns about it. - backend_agreement adds a coverage score: the share of groups both backends attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- shapash/compute/backend_comparison.py | 157 +++++------------ shapash/explainer/nlp_comparison.py | 56 +++++-- shapash/explainer/nlp_plotter.py | 10 +- shapash/plots/plot_backend_comparison.py | 6 +- .../compute/test_backend_comparison.py | 158 ++++++++++++------ .../explainer/test_nlp_comparison.py | 26 +++ .../unit_tests/explainer/test_nlp_plotter.py | 2 + 7 files changed, 231 insertions(+), 184 deletions(-) diff --git a/shapash/compute/backend_comparison.py b/shapash/compute/backend_comparison.py index b7ca352b..11ab7da2 100644 --- a/shapash/compute/backend_comparison.py +++ b/shapash/compute/backend_comparison.py @@ -1,137 +1,62 @@ """Compare NLP backends' contributions on one text, at the array level: align, rescale, score agreement. This module knows nothing about explanations; the layer that reads them is -:mod:`shapash.explainer.nlp_comparison`. Its first job is alignment, since backends do not -agree on what a "token" is, even after each one merges subwords back into words: - -- ``nlp_shap`` and ``nlp_captum_lig`` emit words in sentence order, punctuation as its own unit — - but LIG reads them back off the tokenizer, so an uncased model hands back ``"the"`` where SHAP - kept ``"The"``. -- ``nlp_lime`` (``bow=True``, its default) emits each *distinct* word once, in first-appearance - order, case-sensitive, with punctuation dropped by its ``split_expression``. A word that occurs - twice gets one weight that stands for every occurrence. - -A positional ``zip`` across backends is therefore wrong as soon as LIME is involved. This module -locates every backend's tokens in the source text to get character spans, then reads each -reference unit's value off the other backend in three steps: - -1. **span** — the other backend's token that starts inside the reference unit's span; -2. **exact word** — otherwise, the other backend's token with the same string (this is how a - repeated word reaches LIME's single bag-of-words weight); -3. **case-folded word** — otherwise, the same match ignoring case. - -A unit none of these resolve is ``NaN``: the backend did not attribute it (LIME and punctuation), -which is different from attributing it zero. +:mod:`shapash.explainer.nlp_comparison`. Its first job is alignment, since backends do not agree on +what a unit is, even after each one merges subwords back into words: + +- ``nlp_shap`` splits wherever a letter meets a non-letter: ``"didn" "'" "t"``, ``"great" "."``. +- ``nlp_captum_lig`` groups by the tokenizer's ``word_ids()``, i.e. its pre-tokenizer's pieces: + ``"didn" "'" "t"`` on WordPiece, ``"didn" "'t"`` on byte-level BPE, ``"didn't"`` and ``"great."`` + on SentencePiece, which splits on whitespace only. +- ``nlp_lime`` (``bow=True``, its default) emits each *distinct* word once, punctuation dropped; one + weight stands for every occurrence. + +So neither position nor string identifies a unit across backends. Character spans do (see +:mod:`shapash.compute.spans`): every unit records the source characters it covers, and +:func:`align_units` reads all backends on the finest groups they can all be expressed in — the +spans of every unit, merged where they overlap. A group's value is the sum of the backend's units +inside it (each occurrence of a bag-of-words unit counts), and ``NaN`` when the backend has no unit +there: it did not attribute those characters (LIME and punctuation), which is different from +attributing them zero. """ from __future__ import annotations -import re from collections.abc import Mapping, Sequence import numpy as np import pandas as pd +from shapash.compute.spans import Span, aggregate, association_matrix, merge_overlapping -def locate_tokens(text: str, tokens: Sequence[str]) -> list[tuple[int, int] | None]: - """Character span of each token in ``text``, searched left to right, ignoring case. - Each search starts where the previous match ended, so a repeated word maps to its successive - occurrences. A token that cannot be found (special tokens, normalisation the tokenizer applied) - gets ``None`` and leaves the cursor where it was, so one miss does not shift every later token. +def align_units( + units: Mapping[str, tuple[Sequence[Sequence[Span]], np.ndarray]], +) -> tuple[list[Span], dict[str, np.ndarray]]: + """Read several backends' unit values on one set of groups of the same text. Parameters ---------- - text : str - The source text the tokens were produced from. - tokens : sequence of str - Token strings in the order the backend emitted them. + units : mapping of str to (sequence of spans per unit, np.ndarray) + Backend name → its units' spans and their values, ``(n_units,)`` or + ``(n_units, n_classes)``. A unit with no span cannot be placed and is left out. Returns ------- - list of (int, int) or None - ``(start, end)`` per token, ``None`` where the token was not found. + tuple of (list of (int, int), dict of str to np.ndarray) + The groups' spans, in text order, and per backend its values on them (``NaN`` where it has + no unit). Groups are the units' spans merged where they overlap + (:func:`~shapash.compute.spans.merge_overlapping`), so where every backend splits the text + alike, a group is exactly one unit of each. """ - haystack = text.casefold() - cursor = 0 - spans: list[tuple[int, int] | None] = [] - for token in tokens: - needle = str(token).strip().casefold() - start = _find_word(haystack, needle, cursor) if needle else -1 - if start < 0: - spans.append(None) - continue - end = start + len(needle) - spans.append((start, end)) - cursor = end - return spans - - -def _find_word(haystack: str, needle: str, cursor: int) -> int: - """First occurrence of ``needle`` at or after ``cursor`` that is not part of a longer word. - - A plain substring search would place LIME's ``"i"`` inside ``"didn"``. Word-boundary guards - apply only on the sides where ``needle`` itself starts/ends with a word character, so - punctuation tokens and contraction pieces (``"'"``, ``"t"`` after ``"didn'"``) still match. - Falls back to the plain substring search for scripts written without spaces (CJK), where no - boundary exists to guard. - """ - left = r"(?<!\w)" if re.match(r"\w", needle) else "" - right = r"(?!\w)" if re.search(r"\w$", needle) else "" - found = re.compile(left + re.escape(needle) + right).search(haystack, cursor) - return found.start() if found else haystack.find(needle, cursor) - - -def align_token_values( - text: str, - reference_tokens: Sequence[str], - tokens: Sequence[str], - values: np.ndarray, -) -> np.ndarray: - """Project one backend's per-token ``values`` onto ``reference_tokens``. - - See the module docstring for the three matching steps. - - Parameters - ---------- - text : str - The source text both tokenizations come from. - reference_tokens : sequence of str - The units to align onto, in sentence order. - tokens : sequence of str - The other backend's tokens. - values : np.ndarray - The other backend's 1-D contributions, one per entry of ``tokens``. - - Returns - ------- - np.ndarray - Shape ``(len(reference_tokens),)``. ``NaN`` where the other backend attributed nothing. - """ - values = np.asarray(values, dtype=float) - if values.ndim != 1 or len(values) != len(tokens): - raise ValueError(f"values must be 1-D with one entry per token, got shape {values.shape} for {len(tokens)}.") - - ref_spans = locate_tokens(text, reference_tokens) - spans = locate_tokens(text, tokens) - - by_start = {span[0]: i for i, span in enumerate(spans) if span is not None} - exact: dict[str, int] = {} - folded: dict[str, int] = {} - for i, token in enumerate(tokens): - exact.setdefault(str(token), i) - folded.setdefault(str(token).casefold(), i) - - out = np.full(len(reference_tokens), np.nan) - for j, (ref_token, ref_span) in enumerate(zip(reference_tokens, ref_spans, strict=True)): - match = None - if ref_span is not None: - match = next((by_start[p] for p in range(*ref_span) if p in by_start), None) - if match is None: - match = exact.get(str(ref_token), folded.get(str(ref_token).casefold())) - if match is not None: - out[j] = values[match] - return out + groups = merge_overlapping(span for spans, _ in units.values() for unit in spans for span in unit) + aligned = {} + for name, (spans, raw) in units.items(): + values = np.asarray(raw, dtype=float) + if len(spans) != len(values): + raise ValueError(f"{name!r}: {len(values)} value(s) for {len(spans)} unit(s).") + aligned[name] = aggregate(association_matrix(groups, spans), values) + return groups, aligned def normalize_contributions(values: np.ndarray, method: str | None = "max_abs") -> np.ndarray: @@ -176,6 +101,7 @@ def normalize_contributions(values: np.ndarray, method: str | None = "max_abs") "top_k_overlap", "top_share_a", "top_share_b", + "coverage", ] @@ -210,6 +136,10 @@ def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.D largest unit's share of the total magnitude — how concentrated the attribution is, which says how much to trust ``spearman`` against ``cosine``. + And ``coverage``: the share of units both backends attributed, i.e. what the scores above + stand on. Low coverage is expected against LIME on punctuation-heavy text; on two sequence + backends it means they attribute different parts of the text (one truncated, say). + All are scale-free, so they are safe across backends with different output spaces. """ names = list(aligned) @@ -245,6 +175,7 @@ def backend_agreement(aligned: Mapping[str, np.ndarray], top_k: int = 5) -> pd.D "top_k_overlap": overlap, "top_share_a": _top_share(va), "top_share_b": _top_share(vb), + "coverage": float(both.mean()) if len(both) else np.nan, } ) return pd.DataFrame(rows, columns=AGREEMENT_SCORES) diff --git a/shapash/explainer/nlp_comparison.py b/shapash/explainer/nlp_comparison.py index d4ed5999..938182ae 100644 --- a/shapash/explainer/nlp_comparison.py +++ b/shapash/explainer/nlp_comparison.py @@ -17,7 +17,8 @@ import numpy as np import pandas as pd -from shapash.compute.backend_comparison import AGREEMENT_SCORES, align_token_values, backend_agreement +from shapash.compute.backend_comparison import AGREEMENT_SCORES, align_units, backend_agreement +from shapash.compute.spans import UnitSpans, locate_all, locate_spans from shapash.explainer.nlp_explanation import select_label_column if TYPE_CHECKING: @@ -63,7 +64,7 @@ def name_explanations( def check_comparable(named: Mapping[str, NlpExplanation], stacklevel: int = 2) -> None: - """Raise if the explanations explain different classes; warn if they are in different output spaces. + """Raise if the explanations explain different classes; warn on different output spaces or missing spans. Parameters ---------- @@ -91,14 +92,39 @@ def check_comparable(named: Mapping[str, NlpExplanation], stacklevel: int = 2) - stacklevel=stacklevel + 1, ) + unplaced = [name for name, other in named.items() if other.token_spans is None] + if unplaced: + warnings.warn( + f"{', '.join(map(repr, unplaced))} carry no character spans (saved by an older shapash), so " + "their units are located by searching the text for their strings. That misplaces words a " + "tokenizer rewrote (stripped accents, [UNK], normalised characters). Re-run explain() for " + "an exact alignment.", + UserWarning, + stacklevel=stacklevel + 1, + ) + def predicted_label_idx(explanation: NlpExplanation, row: int) -> int: """Column index of the class ``explanation`` predicted for ``row`` (0 when it cannot be resolved).""" return explanation.label_to_idx.get(str(explanation.y_pred.iloc[row]), 0) +def unit_spans(explanation: NlpExplanation, row: int) -> list[UnitSpans]: + """Each unit's character spans in ``row``'s text. + + Read off :attr:`~shapash.explainer.nlp_explanation.NlpExplanation.token_spans`. An artifact saved + before units carried spans gets them by search instead: every occurrence of each word for LIME's + bag of words, successive occurrences otherwise — approximate, which :func:`check_comparable` warns + about. + """ + if explanation.token_spans is not None: + return list(explanation.token_spans[row]) + text, tokens = str(explanation.texts.iloc[row]), explanation.token_strings[row] + return locate_all(text, tokens) if explanation.backend_name == "nlp_lime" else locate_spans(text, tokens) + + def align_row(named: Mapping[str, NlpExplanation], row: int, label_idx: int) -> tuple[list[str], dict[str, np.ndarray]]: - """One text's contributions for one class, every backend aligned onto the reference's word units. + """One text's contributions for one class, every backend read on the same units. Parameters ---------- @@ -112,25 +138,23 @@ def align_row(named: Mapping[str, NlpExplanation], row: int, label_idx: int) -> Returns ------- tuple of (list of str, dict of str to np.ndarray) - The reference's word units, and per label the aligned 1-D values (``NaN`` = not attributed). + The units as written in the text, and per label the aligned 1-D values (``NaN`` = not + attributed). Units are every backend's units merged where they overlap + (:func:`~shapash.compute.backend_comparison.align_units`): the reference's words wherever no + other backend groups the text more coarsely. """ reference = next(iter(named.values())) if not -len(reference) <= row < len(reference): raise IndexError(f"row={row} is out of range for {len(reference)} sample(s).") text = str(reference.texts.iloc[row]) - ref_tokens = list(reference.token_strings[row]) - ref_values = np.asarray(select_label_column(reference.values[row], label_idx), dtype=float) - aligned: dict[str, np.ndarray] = {} + units = {} for name, other in named.items(): - if other is reference: - aligned[name] = ref_values - continue - if len(other) != len(reference) or str(other.texts.iloc[row]) != text: + if other is not reference and (len(other) != len(reference) or str(other.texts.iloc[row]) != text): raise ValueError(f"Row {row} holds a different text in {name!r} than in the reference explanation.") - values = select_label_column(other.values[row], label_idx) - aligned[name] = align_token_values(text, ref_tokens, other.token_strings[row], values) - return ref_tokens, aligned + units[name] = (unit_spans(other, row), select_label_column(other.values[row], label_idx)) + groups, aligned = align_units(units) + return [text[start:end] for start, end in groups], aligned def corpus_agreement( @@ -151,8 +175,8 @@ def corpus_agreement( Parameters ---------- reference : NlpExplanation - The explanation whose word units the others are aligned onto. Prefer a sequence backend - (SHAP, LIG) over LIME's bag of words. + The explanation the others are named and checked against; units are shared by all (see + :func:`align_row`). others : NlpExplanation, sequence of NlpExplanation, or mapping of str to NlpExplanation Explanations of the same texts by other backends. Labels as in ``compare``. rows : sequence of int, optional diff --git a/shapash/explainer/nlp_plotter.py b/shapash/explainer/nlp_plotter.py index c730a9ff..0f65d475 100644 --- a/shapash/explainer/nlp_plotter.py +++ b/shapash/explainer/nlp_plotter.py @@ -268,10 +268,12 @@ def compare( ) -> go.Figure | html.Div | DashHtmlPreview: """Compare this explanation's contributions on one text with other backends' on the same text. - This explanation is the *reference*: its word units are the ones displayed, and every other - backend's values are aligned onto them (see :mod:`shapash.compute.backend_comparison` — LIME's - bag-of-words vocabulary does not line up positionally with SHAP's or LIG's word sequence). - Pick a sequence backend (SHAP, LIG) as the reference so every occurrence of a word is shown. + This explanation is the *reference* the agreement line scores the others against. Every + backend is read on the same units, placed by the character spans each unit covers (see + :mod:`shapash.compute.backend_comparison`): its own words wherever the backends split the text + alike, a larger unit where one backend groups more coarsely (SentencePiece's ``"didn't"`` + against SHAP's ``"didn" "'" "t"``), each backend's value there being the sum of its units + inside. A LIME word counts at every occurrence, since its one weight stands for all of them. Parameters ---------- diff --git a/shapash/plots/plot_backend_comparison.py b/shapash/plots/plot_backend_comparison.py index 6402d801..93167a72 100644 --- a/shapash/plots/plot_backend_comparison.py +++ b/shapash/plots/plot_backend_comparison.py @@ -1,7 +1,7 @@ """Side-by-side views of several NLP backends' contributions on the same text. -Every function here takes the *already aligned* data — one list of reference word units and, per -backend, one value per unit (``NaN`` where that backend attributed nothing) — so alignment and +Every function here takes the *already aligned* data — one list of units shared by all backends and, +per backend, one value per unit (``NaN`` where that backend attributed nothing) — so alignment and normalisation stay in :mod:`shapash.compute.backend_comparison` and these stay pure renderers. Reach them through :meth:`~shapash.explainer.nlp_plotter.NlpPlotter.compare`. @@ -234,7 +234,7 @@ def plot_backend_highlight( """One highlighted copy of the sentence per backend, stacked. Each row is colored on its own magnitude scale (its largest unit is fully saturated), since - backends' raw values are in different units. Every row renders the same reference units with + backends' raw values are in different units. Every row renders the same shared units with the same padding, so a word sits at the same horizontal position in every row as long as the rows wrap at the same width. diff --git a/tests/unit_tests/compute/test_backend_comparison.py b/tests/unit_tests/compute/test_backend_comparison.py index 353b49b1..67abd49e 100644 --- a/tests/unit_tests/compute/test_backend_comparison.py +++ b/tests/unit_tests/compute/test_backend_comparison.py @@ -1,81 +1,139 @@ -"""Unit tests for ``shapash.compute.backend_comparison`` — putting backends' word units side by side. +"""Unit tests for ``shapash.compute.backend_comparison`` — putting backends' units side by side. -The token lists below are copied from real ``nlp_shap`` / ``nlp_lime`` / ``nlp_captum_lig`` runs on -``distilbert-base-uncased-emotion``: SHAP keeps case and punctuation, LIG lowercases, LIME emits a -case-sensitive bag of distinct words with punctuation dropped. +Token lists are copied from real ``nlp_shap`` / ``nlp_captum_lig`` / ``nlp_lime`` runs, each on the +tokenizer named in the test. Spans are what those runs record: the characters of the text a unit +covers, whatever string the backend printed for it. """ import numpy as np import pytest -from shapash.compute.backend_comparison import ( - align_token_values, - backend_agreement, - locate_tokens, - normalize_contributions, -) +from shapash.compute.backend_comparison import align_units, backend_agreement, normalize_contributions TEXT = "The waiting room was cold and I felt nervous, really nervous, about the results." SHAP_TOKENS = ["The", "waiting", "room", "was", "cold", "and", "I", "felt", "nervous", ","] SHAP_TOKENS += ["really", "nervous", ",", "about", "the", "results", "."] -LIG_TOKENS = [t.lower() for t in SHAP_TOKENS] LIME_TOKENS = ["The", "waiting", "room", "was", "cold", "and", "I", "felt", "nervous", "really", "about", "the"] LIME_TOKENS += ["results"] -class TestLocateTokens: - def test_repeated_words_map_to_successive_occurrences(self): - spans = locate_tokens(TEXT, SHAP_TOKENS) - assert [TEXT[a:b] for a, b in spans] == SHAP_TOKENS - assert spans[8] != spans[11] # the two "nervous" +def _spans(text, words): + """Each word's span, taking successive occurrences — what a sequence backend records.""" + out, cursor = [], 0 + for word in words: + start = text.index(word, cursor) + out.append(((start, start + len(word)),)) + cursor = start + len(word) + return out - def test_case_is_ignored(self): - assert locate_tokens(TEXT, LIG_TOKENS) == locate_tokens(TEXT, SHAP_TOKENS) - def test_a_short_word_is_not_found_inside_a_longer_one(self): - # A plain substring search would place "i" inside "didn". - assert locate_tokens("I didn't, did i", ["didn", "i"]) == [(2, 6), (14, 15)] +def _bag_spans(text, words): + """Every occurrence of each distinct word — what LIME's bag of words records.""" + import re - def test_contraction_pieces_still_match(self): - assert locate_tokens("didn't", ["didn", "'", "t"]) == [(0, 4), (4, 5), (5, 6)] + return [tuple((m.start(), m.end()) for m in re.finditer(rf"\b{re.escape(w)}\b", text)) for w in words] - def test_missing_token_gets_none_and_keeps_the_cursor(self): - assert locate_tokens("a b", ["a", "[SEP]", "b"]) == [(0, 1), None, (2, 3)] - def test_unspaced_script_falls_back_to_substring_search(self): - assert locate_tokens("我喜欢你", ["我", "喜欢", "你"]) == [(0, 1), (1, 3), (3, 4)] +def _labels(text, groups): + return [text[a:b] for a, b in groups] -class TestAlignTokenValues: - def test_identical_tokenization_is_the_identity(self): +class TestAlignUnits: + def test_identical_units_are_the_identity(self): values = np.arange(len(SHAP_TOKENS), dtype=float) - np.testing.assert_array_equal(align_token_values(TEXT, SHAP_TOKENS, SHAP_TOKENS, values), values) - - def test_lowercased_tokenization_aligns_by_span(self): - values = np.arange(len(LIG_TOKENS), dtype=float) - np.testing.assert_array_equal(align_token_values(TEXT, SHAP_TOKENS, LIG_TOKENS, values), values) + groups, aligned = align_units( + {"a": (_spans(TEXT, SHAP_TOKENS), values), "b": (_spans(TEXT, SHAP_TOKENS), values)} + ) + assert _labels(TEXT, groups) == SHAP_TOKENS + np.testing.assert_array_equal(aligned["b"], values) + + def test_strings_play_no_part(self): + # distilbert-base-uncased LIG on "Le café était très bon.": the tokenizer lowercases and strips + # accents, so none of its strings but "bon" and "." occur in the text. Its spans do. + text = "Le café était très bon." + shap = _spans(text, ["Le", "café", "était", "très", "bon", "."]) + values = np.arange(6, dtype=float) + groups, aligned = align_units({"shap": (shap, values), "lig": (shap, values * 10)}) + assert _labels(text, groups) == ["Le", "café", "était", "très", "bon", "."] + np.testing.assert_array_equal(aligned["lig"], values * 10) def test_bag_of_words_reaches_every_occurrence_and_skips_punctuation(self): - values = np.arange(1.0, len(LIME_TOKENS) + 1) - out = align_token_values(TEXT, SHAP_TOKENS, LIME_TOKENS, values) - lime = dict(zip(LIME_TOKENS, values, strict=True)) - for token, value in zip(SHAP_TOKENS, out, strict=True): - if token in {",", "."}: + lime_values = np.arange(1.0, len(LIME_TOKENS) + 1) + groups, aligned = align_units( + { + "shap": (_spans(TEXT, SHAP_TOKENS), np.zeros(len(SHAP_TOKENS))), + "lime": (_bag_spans(TEXT, LIME_TOKENS), lime_values), + } + ) + lime = dict(zip(LIME_TOKENS, lime_values, strict=True)) + for label, value in zip(_labels(TEXT, groups), aligned["lime"], strict=True): + if label in {",", "."}: assert np.isnan(value) else: - assert value == lime[token] - # The second "nervous" has no LIME span of its own: it gets the one bag-of-words weight. - assert out[8] == out[11] == lime["nervous"] + assert value == lime[label] + # Both "nervous": the one bag-of-words weight stands for each. + assert aligned["lime"][8] == aligned["lime"][11] == lime["nervous"] def test_bag_of_words_keeps_case_distinct_words_apart(self): - values = np.arange(1.0, len(LIME_TOKENS) + 1) - out = align_token_values(TEXT, SHAP_TOKENS, LIME_TOKENS, values) - assert out[0] == values[LIME_TOKENS.index("The")] - assert out[14] == values[LIME_TOKENS.index("the")] + lime_values = np.arange(1.0, len(LIME_TOKENS) + 1) + _, aligned = align_units( + { + "shap": (_spans(TEXT, SHAP_TOKENS), np.zeros(len(SHAP_TOKENS))), + "lime": (_bag_spans(TEXT, LIME_TOKENS), lime_values), + } + ) + assert aligned["lime"][0] == lime_values[LIME_TOKENS.index("The")] + assert aligned["lime"][14] == lime_values[LIME_TOKENS.index("the")] + + def test_a_coarser_backend_merges_the_finer_units_it_spans(self): + # xlm-roberta LIG groups by whitespace ("didn't", "great."), SHAP splits at punctuation. + text = "I didn't love it, great." + shap_words = ["I", "didn", "'", "t", "love", "it", ",", "great", "."] + lig_words = ["I", "didn't", "love", "it,", "great."] + shap_values = np.arange(1.0, 10.0) + groups, aligned = align_units( + {"shap": (_spans(text, shap_words), shap_values), "lig": (_spans(text, lig_words), np.arange(5.0))} + ) + assert _labels(text, groups) == lig_words + # Each group carries the sum of the finer backend's units inside it: nothing is dropped. + np.testing.assert_array_equal(aligned["shap"], [1.0, 2 + 3 + 4, 5.0, 6 + 7, 8 + 9]) + np.testing.assert_array_equal(aligned["lig"], np.arange(5.0)) + assert aligned["shap"].sum() == shap_values.sum() + + def test_a_unit_starting_inside_another_is_not_lost(self): + # roberta LIG keeps "'t" whole; LIME drops the apostrophe and keeps "t" — which starts after + # the group starts, and used to come back NaN. + text = "I didn't" + _, aligned = align_units( + { + "lig": (_spans(text, ["I", "didn", "'t"]), np.array([1.0, 2.0, 3.0])), + "lime": (_bag_spans(text, ["I", "didn", "t"]), np.array([10.0, 20.0, 30.0])), + } + ) + np.testing.assert_array_equal(aligned["lime"], [10.0, 20.0, 30.0]) + + def test_a_backend_that_stops_early_is_nan_on_the_rest(self): + # LIG truncates at the model's max length; SHAP does not. + text = "one two three" + _, aligned = align_units( + { + "shap": (_spans(text, ["one", "two", "three"]), np.ones(3)), + "lig": (_spans(text, ["one", "two"]), np.ones(2)), + } + ) + assert np.isnan(aligned["lig"][2]) and not np.isnan(aligned["shap"]).any() + + def test_a_unit_without_spans_is_left_out(self): + _, aligned = align_units({"a": ([((0, 1),), ()], np.array([1.0, 5.0]))}) + np.testing.assert_array_equal(aligned["a"], [1.0]) + + def test_two_dimensional_values(self): + _, aligned = align_units({"a": ([((0, 1),), ((0, 1),)], np.array([[1.0, 2.0], [3.0, 4.0]]))}) + np.testing.assert_array_equal(aligned["a"], [[4.0, 6.0]]) def test_rejects_mismatched_values(self): - with pytest.raises(ValueError, match="one entry per token"): - align_token_values("a b", ["a", "b"], ["a", "b"], np.array([1.0])) + with pytest.raises(ValueError, match="1 value"): + align_units({"a": ([((0, 1),), ((2, 3),)], np.array([1.0]))}) class TestNormalize: @@ -145,6 +203,10 @@ def test_constant_backend_has_no_rank_correlation(self): df = backend_agreement({"a": np.array([1.0, 2.0, 3.0]), "b": np.zeros(3)}) assert np.isnan(df.iloc[0].spearman) and np.isnan(df.iloc[0].pearson) + def test_coverage_is_the_share_of_units_both_attributed(self): + df = backend_agreement({"a": np.array([1.0, 2.0, 3.0, np.nan]), "b": np.array([1.0, np.nan, 3.0, 4.0])}) + assert df.iloc[0].coverage == 0.5 + def test_no_shared_unit_gives_nan(self): df = backend_agreement({"a": np.array([1.0, np.nan]), "b": np.array([np.nan, 1.0])}) assert df.iloc[0][["spearman", "pearson", "cosine", "sign_agreement", "top_k_overlap"]].isna().all() diff --git a/tests/unit_tests/explainer/test_nlp_comparison.py b/tests/unit_tests/explainer/test_nlp_comparison.py index 269a7aca..bc1ce087 100644 --- a/tests/unit_tests/explainer/test_nlp_comparison.py +++ b/tests/unit_tests/explainer/test_nlp_comparison.py @@ -21,6 +21,7 @@ def lime(ref): return replace( ref, token_strings=[["world", "hello"], ["happy", "i"], ["ok"]], + token_spans=[[((6, 11),), ((0, 5),)], [((5, 10),), ((0, 1),)], [((0, 2),)]], values=[ np.array([[5.0, -5.0], [3.0, -3.0]]), np.array([[0.0, 8.0], [0.0, 2.0]]), @@ -83,6 +84,31 @@ def test_rejects_a_different_corpus(self, ref, lime): class TestAlignRow: + def test_units_are_merged_where_a_backend_groups_more_coarsely(self, ref): + # A SentencePiece-like LIG keeps "am happy" as one unit on row 1 (made up, for the shape). + coarse = replace( + ref, + token_strings=[["hello", "world"], ["i", "am happy"], ["ok"]], + token_spans=[[((0, 5),), ((6, 11),)], [((0, 1),), ((2, 10),)], [((0, 2),)]], + values=[ref.values[0], np.array([[1.0, 1.0], [9.0, 9.0]]), ref.values[2]], + backend_name="nlp_captum_lig", + ) + units, aligned = align_row(name_explanations(ref, coarse), 1, 1) + assert units == ["i", "am happy"] + np.testing.assert_allclose(aligned["shap"], [0.2, 0.4 + 0.6]) + np.testing.assert_allclose(aligned["captum_lig"], [1.0, 9.0]) + + def test_legacy_artifacts_without_spans_are_located_by_string_and_warn(self, ref, lime): + legacy_ref, legacy_lime = replace(ref, token_spans=None), replace(lime, token_spans=None) + with pytest.warns(UserWarning, match="no character spans"): + df = corpus_agreement(legacy_ref, legacy_lime, rows=[0]) + assert df.spearman.iloc[0] == pytest.approx(1.0) + # Same alignment as the exact path on text the tokenizer did not rewrite. + exact = align_row(name_explanations(ref, lime), 1, 1) + located = align_row(name_explanations(legacy_ref, legacy_lime), 1, 1) + assert exact[0] == located[0] + np.testing.assert_array_equal(exact[1]["lime"], located[1]["lime"]) + def test_out_of_range_row(self, ref, lime): with pytest.raises(IndexError, match="row=5"): align_row(name_explanations(ref, lime), 5, 0) diff --git a/tests/unit_tests/explainer/test_nlp_plotter.py b/tests/unit_tests/explainer/test_nlp_plotter.py index 81c5ce06..34ae727d 100644 --- a/tests/unit_tests/explainer/test_nlp_plotter.py +++ b/tests/unit_tests/explainer/test_nlp_plotter.py @@ -55,6 +55,7 @@ def _make_explanation( is_additive=is_additive, reference_kind="distribution" if with_base else "none", output_space="probability", + token_spans=[[((0, 5),), ((6, 11),)], [((0, 1),), ((2, 4),), ((5, 10),)], [((0, 2),)]], ) @@ -377,6 +378,7 @@ def setUp(self): self.lime = replace( self.ref, token_strings=[["world", "hello"], ["happy", "i"], ["ok"]], + token_spans=[[((6, 11),), ((0, 5),)], [((5, 10),), ((0, 1),)], [((0, 2),)]], values=[ np.array([[5.0, -5.0], [3.0, -3.0]]), np.array([[0.0, 8.0], [0.0, 2.0]]), From 4780191eca4d17eff066f4bdc664ee62316e9507 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 11:05:17 +0200 Subject: [PATCH 139/144] test(nlp): check unit alignment across tokenizer families Runs the real SHAP, LIG and LIME backends on six tokenizer families: WordPiece (uncased), byte-level BPE, ModernBERT BPE, and SentencePiece (XLM-R unigram, uncased ALBERT, DeBERTa-v3). The input texts are chosen to break string matching: accents (precomposed and decomposed), emoji, curly quotes, full-width characters, double spaces and newlines, contractions, and CJK. The tests check that every unit is placed and reads as written, that SHAP and LIG cover the same characters, that SHAP stays additive, that alignment keeps each backend's total, and that truncation shows up as partial coverage. The models are tiny and randomly initialised from each checkpoint's config, so only the config and tokenizer are downloaded. SHAP's Text masker rebuilds its input from segments. That can collapse whitespace and repeat characters shared by two tokens, so the additivity test checks against the string the masker actually builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../test_nlp_unit_alignment.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 tests/integration_tests/test_nlp_unit_alignment.py diff --git a/tests/integration_tests/test_nlp_unit_alignment.py b/tests/integration_tests/test_nlp_unit_alignment.py new file mode 100644 index 00000000..bd6453ab --- /dev/null +++ b/tests/integration_tests/test_nlp_unit_alignment.py @@ -0,0 +1,197 @@ +"""Backend units across tokenizer families: placed in the source text, and comparable across backends. + +Each backend cuts a text into units its own way, and each tokenizer family moves the cuts again +(WordPiece splits every punctuation mark, byte-level BPE glues symbols together, SentencePiece splits +on whitespace only, uncased tokenizers rewrite the text). These tests run the real SHAP, LIG and LIME +``run_explainer`` through ``NlpExplainer.explain`` on one checkpoint per family, against text built +to break string matching (accents, decomposed accents, emoji, curly quotes, full-width characters, +double spaces, newlines), and check the invariants alignment relies on: + +- every unit is placed: it has spans, and its string is the source text at its span; +- no unit is whitespace; +- SHAP and LIG cover the same characters, since both read the same tokenizer's offsets; +- aligning backends drops nothing: each backend's values sum to the same total before and after. + +The models are randomly initialised from each checkpoint's config at a tiny size, so only the config +and tokenizer are downloaded; the numbers mean nothing, the units are real. Skipped when the ``nlp`` +or ``lime`` extra is missing or a checkpoint cannot be fetched. +""" + +import numpy as np +import pytest + +transformers = pytest.importorskip("transformers") +torch = pytest.importorskip("torch") +pytest.importorskip("captum") +pytest.importorskip("lime") +pytestmark = pytest.mark.nlp + +from shapash.backend import NlpCaptumLigBackend, NlpLimeBackend, NlpShapBackend # noqa: E402 +from shapash.explainer.nlp_comparison import align_row, corpus_agreement, name_explanations # noqa: E402 +from shapash.explainer.nlp_explainer import NlpExplainer # noqa: E402 +from shapash.model import HFClassifierModel # noqa: E402 + +CHECKPOINTS = { + "wordpiece-uncased": "distilbert-base-uncased", + "byte-bpe": "bhadresh-savani/roberta-base-emotion", + "bpe-modernbert": "Alibaba-NLP/gte-modernbert-base", + "sentencepiece-unigram": "cardiffnlp/twitter-xlm-roberta-base-sentiment", + "sentencepiece-uncased": "bhadresh-savani/albert-base-v2-emotion", + "sentencepiece-deberta": "microsoft/deberta-v3-small", +} + +TEXTS = [ + "I didn't like it, but she's sure it won't fail!", + "Le café était très bon, naïve and naive.", + "Le café était très bon.", # decomposed accents (NFD) + "Superb!!!Really enjoy.Overall,I loved it…", + "Well… it’s “fine” — I love it \U0001f60d\U0001f44d\U0001f3fd", + "Hello world\n\nThis is\tgreat.", + "A state-of-the-art final: ABC costs $3.50 on 2024-01-05.", + "Die Straße ist groß. İstanbul, 我喜欢 snake_case.", +] +LABELS = ["a", "b"] + +# Shrinks every common config to one tiny layer; attributes a family does not have are skipped. +_TINY = { + "num_hidden_layers": 1, + "hidden_size": 32, + "intermediate_size": 64, + "num_attention_heads": 2, + "embedding_size": 32, + "pooler_hidden_size": 32, + "n_layers": 1, + "dim": 32, + "hidden_dim": 64, + "n_heads": 2, +} + + +def _tiny_model(checkpoint: str) -> HFClassifierModel: + try: + config = transformers.AutoConfig.from_pretrained(checkpoint) + tokenizer = transformers.AutoTokenizer.from_pretrained(checkpoint) + except Exception as exc: # network / cache miss + pytest.skip(f"{checkpoint} unavailable: {exc}") + for key, value in _TINY.items(): + if hasattr(config, key): + setattr(config, key, value) + config.num_labels = len(LABELS) + torch.manual_seed(0) + classifier = ( + transformers.AutoModelForSequenceClassification.from_config(config).float().eval() + ) # some configs pin fp16 + return HFClassifierModel(classifier, tokenizer, label_names=LABELS, max_length=128) + + +@pytest.fixture(scope="module", params=list(CHECKPOINTS), ids=list(CHECKPOINTS)) +def explanations(request): + model = _tiny_model(CHECKPOINTS[request.param]) + backends = { + "shap": NlpShapBackend( + model, label_names=LABELS, output_space="logit", explainer_compute_args={"max_evals": 500} + ), + "lig": NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}), + "lime": NlpLimeBackend(model, label_names=LABELS, explainer_compute_args={"num_samples": 50}), + } + out = { + name: NlpExplainer(model, label_names=LABELS, backend=backend).explain(TEXTS) + for name, backend in backends.items() + } + out["model"] = model + out["shap_masker"] = backends["shap"].explainer.masker + return out + + +def _shap_input(masker, text: str) -> str: + """The string SHAP's ``Text`` masker hands the model when nothing is masked. + + Not always ``text``: the masker rebuilds it from its segments, so it collapses whitespace and, + where tokens share a character (an emoji split by byte-level BPE), repeats it. An upstream + behaviour of ``shap.maskers.Text``, outside what alignment can fix. + """ + segments, _ = masker.token_segments(text) + return str(masker(np.ones(len(segments), dtype=bool), text)[0][0]) + + +@pytest.mark.parametrize("backend", ["shap", "lig", "lime"]) +def test_every_unit_is_placed_and_reads_as_written(explanations, backend): + exp = explanations[backend] + for row, text in enumerate(TEXTS): + for word, spans in zip(exp.token_strings[row], exp.token_spans[row], strict=True): + assert spans, f"{word!r} has no span in {text!r}" + assert word.strip() == word and word, f"whitespace in unit {word!r}" + for start, end in spans: + assert text[start:end] == word + if backend != "lime": # a sequence backend: one span per unit, in order, disjoint + flat = [unit[0] for unit in exp.token_spans[row]] + assert all(len(unit) == 1 for unit in exp.token_spans[row]) + assert all(a[1] <= b[0] for a, b in zip(flat, flat[1:], strict=False)) + + +def test_shap_and_lig_cover_the_same_characters(explanations): + for row, text in enumerate(TEXTS): + covered = {} + for backend in ("shap", "lig"): + chars = set() + for ((start, end),) in explanations[backend].token_spans[row]: + chars.update(range(start, end)) + covered[backend] = chars + assert covered["shap"] == covered["lig"], f"row {row}: {text!r}" + visible = {i for i, c in enumerate(text) if not c.isspace()} + assert covered["shap"] <= visible + + +def test_shap_stays_exactly_additive_after_merging(explanations): + # Against the string SHAP explains (see _shap_input): merging units must not move the total. + exp, model = explanations["shap"], explanations["model"] + logits = model.predict_logits([_shap_input(explanations["shap_masker"], text) for text in TEXTS]) + for row in range(len(TEXTS)): + np.testing.assert_allclose(exp.base_values[row] + exp.values[row].sum(axis=0), logits[row], atol=1e-4) + + +@pytest.mark.parametrize("label_idx", [0, 1]) +def test_alignment_drops_nothing(explanations, label_idx): + named = name_explanations(explanations["shap"], {"lig": explanations["lig"], "lime": explanations["lime"]}) + for row in range(len(TEXTS)): + units, aligned = align_row(named, row, label_idx) + text = TEXTS[row] + assert all(unit and unit == unit.strip() for unit in units) + # Both sequence backends attribute every group: same tokenizer, same characters. + assert np.isfinite(aligned["shap"]).all() and np.isfinite(aligned["lig"]).all(), text + for name, key in (("shap", "shap"), ("lig", "lig")): + total = explanations[key].values[row][:, label_idx].sum() + assert np.nansum(aligned[name]) == pytest.approx(total, rel=1e-6, abs=1e-9) + # LIME's one weight counts at every occurrence. + lime = explanations["lime"] + occurrences = np.array([len(spans) for spans in lime.token_spans[row]]) + total = (lime.values[row][:, label_idx] * occurrences).sum() + assert np.nansum(aligned["lime"]) == pytest.approx(total, rel=1e-6, abs=1e-9) + + +def test_corpus_agreement_runs_on_every_family(explanations): + df = corpus_agreement(explanations["shap"], {"lig": explanations["lig"], "lime": explanations["lime"]}, label_idx=1) + shap_lig = df[(df.backend_a == "shap") & (df.backend_b == "lig")] + assert len(shap_lig) == len(TEXTS) + assert (shap_lig.coverage == 1.0).all() + + +def test_a_truncated_backend_shows_as_partial_coverage(): + model = _tiny_model(CHECKPOINTS["wordpiece-uncased"]) + model.max_length = 8 # LIG truncates; SHAP's masker segments the whole text + text = ["one two three four five six seven eight nine ten eleven twelve"] + shap = NlpExplainer( + model, + label_names=LABELS, + backend=NlpShapBackend( + model, label_names=LABELS, output_space="logit", explainer_compute_args={"max_evals": 200} + ), + ).explain(text) + lig = NlpExplainer( + model, + label_names=LABELS, + backend=NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}), + ).explain(text) + assert len(lig.token_strings[0]) < len(shap.token_strings[0]) + row = corpus_agreement(shap, {"lig": lig}, label_idx=0).iloc[0] + assert 0 < row.coverage < 1 From 092751c917bba3751fcb5de8c6da675f88ff2f05 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 11:06:20 +0200 Subject: [PATCH 140/144] docs(nlp): drop references to internal design docs from code Some docstrings and comments pointed to design notes that are not part of the repository. Those references are removed; the explanations stay in the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- shapash/backend/nlp_captum_lig_backend.py | 3 +-- shapash/backend/nlp_shap_backend.py | 3 +-- shapash/model/base.py | 2 +- shapash/model/encoder.py | 6 +++--- tests/unit_tests/backend/test_nlp_backend.py | 4 ++-- tests/unit_tests/backend/test_nlp_shap_backend.py | 2 +- tests/unit_tests/explainer/test_nlp_explanation.py | 2 +- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index c7f2ff55..662f2463 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -250,8 +250,7 @@ class (e.g. ``{"n_steps": 50, "internal_batch_size": 16}``). ``n_steps`` default ``[MASK]`` reference (see :attr:`baseline_token`) makes the integrand less smooth than ``[PAD]`` did: on 100 DistilBERT-emotion texts (predicted class) the worst completeness error was 0.84 logits at 50 steps and 0.31 at 100 (``[PAD]``: 0.11 / 0.06). The step count barely moves the - word *ranking* — only how exactly the values sum to the logit gap — - ``docs/architecture/explanation-space.md`` §10.4 and §10.7. + word *ranking* — only how exactly the values sum to the logit gap. show_progress : bool, default False When True, wrap the per-sample attribution loop in a ``tqdm`` progress bar (LIG runs one integration per class per sample, so a batch is slow). Best-effort: if ``tqdm`` is not diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index cd5b97b4..2247795b 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -286,8 +286,7 @@ class NlpShapBackend(NlpBackend): ``TextModel`` implementing :class:`~shapash.model.base.SupportsLogits`, and is incompatible with ``explainer_args`` (which bring their own model). Same runtime and masking; the contributions stop cancelling across classes and stop saturating near 0/1, sum exactly to - ``logits(x) - base``, and share their scale with ``nlp_captum_lig`` - (``docs/architecture/explanation-space.md`` §10). + ``logits(x) - base``, and share their scale with ``nlp_captum_lig``. Raises ------ diff --git a/shapash/model/base.py b/shapash/model/base.py index da5e0f21..2e244c55 100644 --- a/shapash/model/base.py +++ b/shapash/model/base.py @@ -488,7 +488,7 @@ class SupportsLogits(ABC): The text-level counterpart of :meth:`SupportsCaptumIG.logits` (which takes tensors): what a perturbation explainer wraps when it should explain logits rather than the probabilities :meth:`TextModel.predict` returns. Probability-space attributions cancel exactly across classes - and saturate near 0/1; logit-space ones do neither (``docs/architecture/explanation-space.md``). + and saturate near 0/1; logit-space ones do neither. """ @abstractmethod diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index c8511778..766b27a1 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -699,9 +699,9 @@ def _baseline_token_id(self) -> int: ``mask`` comes first because it is what SHAP's ``Text`` masker substitutes (it takes the tokenizer's ``mask_token``), so LIG and SHAP measure against the same "word absent" input. - Aligning on it raised SHAP<->LIG rank agreement from 0.46 to 0.57 on DistilBERT - (``docs/architecture/explanation-space.md`` §10.4). Explicit ``None`` checks, not ``or``: id - ``0`` is a legitimate token id (BERT's ``[PAD]``, RoBERTa's ``<s>``). + Aligning on it raised SHAP<->LIG rank agreement from 0.46 to 0.57 on DistilBERT. + Explicit ``None`` checks, not ``or``: id ``0`` is a legitimate token id (BERT's ``[PAD]``, + RoBERTa's ``<s>``). """ for candidate in ( self.tokenizer.mask_token_id, diff --git a/tests/unit_tests/backend/test_nlp_backend.py b/tests/unit_tests/backend/test_nlp_backend.py index e3f044e3..7fb2b47d 100644 --- a/tests/unit_tests/backend/test_nlp_backend.py +++ b/tests/unit_tests/backend/test_nlp_backend.py @@ -37,7 +37,7 @@ def test_surrounding_whitespace_is_ignored(self): class TestBackendContractAttributes(unittest.TestCase): - """``reference_kind`` / ``is_additive`` / ``output_space`` / `requires_model_capabilities`` (A9 / A11).""" + """``reference_kind`` / ``is_additive`` / ``output_space`` / ``requires_model_capabilities``.""" def test_nlp_shap_backend(self): self.assertEqual(NlpShapBackend.reference_kind, "none") @@ -87,7 +87,7 @@ class _RequiresEmbeddings(_MinimalBackend): class TestRequiresModelCapabilitiesGuard(unittest.TestCase): - """``NlpBackend.__init__`` centralizes the capability check (A11 point 2).""" + """``NlpBackend.__init__`` centralizes the capability check.""" def test_raises_when_model_lacks_required_capability(self): with self.assertRaises(TypeError): diff --git a/tests/unit_tests/backend/test_nlp_shap_backend.py b/tests/unit_tests/backend/test_nlp_shap_backend.py index 271fa0c0..4a9ff52c 100644 --- a/tests/unit_tests/backend/test_nlp_shap_backend.py +++ b/tests/unit_tests/backend/test_nlp_shap_backend.py @@ -376,7 +376,7 @@ def predict_logits(self, texts): class TestOutputSpace(unittest.TestCase): - """``output_space`` is a constructor choice, read back off the instance (explanation-space.md §8, phase 3).""" + """``output_space`` is a constructor choice, read back off the instance.""" def test_unknown_space_is_refused(self): with self.assertRaises(ValueError): diff --git a/tests/unit_tests/explainer/test_nlp_explanation.py b/tests/unit_tests/explainer/test_nlp_explanation.py index b5ec9952..88b49034 100644 --- a/tests/unit_tests/explainer/test_nlp_explanation.py +++ b/tests/unit_tests/explainer/test_nlp_explanation.py @@ -161,7 +161,7 @@ def _resave_without_output_space(self, expl: NlpExplanation) -> Path: def test_legacy_file_without_output_space_defaults_by_backend(self): # nlp_captum_lig has always explained raw logits, everything else probabilities — a single - # global default would mislabel one of the two. See docs/architecture/explanation-space.md §5.3. + # global default would mislabel one of the two. shap_expl = _make_explanation(values_ndim=2, with_base=True, with_true=True, with_prob=True) lig_expl = replace(shap_expl, backend_name="nlp_captum_lig") From 7e2d5ca476a64ce0f10f59dc2bfc863eaa809b2f Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 13:11:40 +0200 Subject: [PATCH 141/144] feat(nlp): mask token ids instead of strings in SHAP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shap.maskers.Text re-tokenizes masked strings, so the model can see another text ("était" -> "éétait", collapsed whitespace), and hiding a token changes its neighbours ("Ġlove" -> "love"). - NlpShapBackend masks the model's own encoding when the model exposes token ids with a fast tokenizer: hidden tokens take LIG's [MASK] reference ids, and each word is one branch of the Partition hierarchy. Other models, an explicit masker= or explainer_args keep the string path; backend.masking reports which path ran. NlpExplainer's default backend takes the new path. - A shared WordLayout (compute/word_layout.py) builds words, spans and baseline folding for both SHAP and LIG, so they report identical units. - The cache key uses shapash.__version__ instead of a units layout version. - Tests: exact token-id SHAP values on an additive fake model, word layout unit tests, and integration checks across six tokenizer families (additivity against the text, SHAP/LIG unit identity, SHAP vs LIME in probability space, LIG fallbacks without offsets). --- shapash/backend/nlp_captum_lig_backend.py | 88 ++---- shapash/backend/nlp_shap_backend.py | 299 +++++++++++++++++- shapash/compute/word_layout.py | 167 ++++++++++ shapash/explainer/nlp_explainer.py | 42 ++- shapash/model/encoder.py | 5 +- .../test_nlp_unit_alignment.py | 131 +++++--- .../backend/test_nlp_captum_lig_backend.py | 34 +- .../backend/test_nlp_shap_token_ids.py | 253 +++++++++++++++ tests/unit_tests/compute/test_word_layout.py | 77 +++++ 9 files changed, 924 insertions(+), 172 deletions(-) create mode 100644 shapash/compute/word_layout.py create mode 100644 tests/unit_tests/backend/test_nlp_shap_token_ids.py create mode 100644 tests/unit_tests/compute/test_word_layout.py diff --git a/shapash/backend/nlp_captum_lig_backend.py b/shapash/backend/nlp_captum_lig_backend.py index 662f2463..9260b081 100644 --- a/shapash/backend/nlp_captum_lig_backend.py +++ b/shapash/backend/nlp_captum_lig_backend.py @@ -7,24 +7,21 @@ ``plot_sentence_highlight`` / ``plot_token_highlight`` renderers consume it unchanged. LIG attributes at the tokenizer's *subword* granularity (``[CLS]``, ``[SEP]``, ``##`` continuation -pieces). To match the word-level highlights the SHAP backend produces, ``_aggregate_subwords`` merges -each word's subwords into a single contribution and folds the special tokens' attribution into the -baseline (see below) — so the output carries whole words, not ``##`` fragments. +pieces). To match the word-level highlights the SHAP backend produces, each word's subwords are +summed into a single contribution and the special tokens' attribution folds into the baseline (see +below), so the output carries whole words, not ``##`` fragments. LIG is a *completion* method: for each class ``c`` the token attributions sum to ``logits(x)[c] - logits(baseline)[c]``. We therefore report ``base_values[c] = logits(baseline)[c]`` so the additive ``base + Σ = total`` summary in ``plot_sentence_highlight`` stays consistent. These -values live in raw logit space — **not** the same space ``NlpShapBackend`` reports: SHAP explains -the pipeline's softmax probability output, not ``model.logits``, so the two backends' numbers are on -different scales and are not directly comparable. - -Each word is then placed in the source text through the tokenizer's offsets -(:meth:`~shapash.model.base.SupportsCaptumIG.token_offsets`): its span is recorded -(``NlpContributions.token_spans``) and its display string is that span of the text, not the -tokenizer's rebuilt string — which an uncased tokenizer lowercases and strips of accents, and which -reads ``[UNK]`` for an emoji. A word covering only whitespace (byte-level BPE gives a lone ``Ġ``/``Ċ`` -its own word id on a double space or a newline) is not a word: its attribution goes to the baseline, -like a special token's. +values live in raw logit space — the space ``NlpShapBackend(output_space="logit")`` reports; its +default explains softmax probabilities, on a different scale. + +With the tokenizer's offsets (:meth:`~shapash.model.base.SupportsCaptumIG.token_offsets`), words +come from :func:`~shapash.compute.word_layout.word_layout`, the layout the SHAP backend masks: each +word records its span (``NlpContributions.token_spans``) and reads as that span of the text, and a word +covering only whitespace folds into the baseline, like a special token. Without offsets (a slow +tokenizer), words keep their token-built strings and are located in the text by search. Unlike the SHAP/LIME backends (which wrap a plain text callable), this backend needs the embedding module and a logits forward pass, so it consumes a :class:`~shapash.model.base.TextModel` that @@ -39,7 +36,8 @@ from shapash._optional import import_optional_module from shapash.backend.nlp_backend import NlpBackend, NlpContributions -from shapash.compute.spans import Span, locate_spans, span_from_offsets, trim_span +from shapash.compute.spans import Span, locate_spans +from shapash.compute.word_layout import word_layout from shapash.model.base import SupportsCaptumIG _NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' @@ -180,42 +178,6 @@ def _valid_alignment( return alignment -def _place_words( - text: str, - words: list[str], - contributions: np.ndarray, - base_values: np.ndarray, - word_positions: list[list[int]] | None, - offsets: list[tuple[int, int]] | None, -) -> tuple[list[str], np.ndarray, np.ndarray, list[tuple[Span, ...]]]: - """Give each word its span and its source-text string; fold whitespace-only words into the baseline. - - With ``word_positions`` (the exact tokenizer grouping) and ``offsets``, a word's span is read off - its tokens' offsets. Without them — a slow tokenizer, which reports neither — words keep their - token-built strings and are located in ``text`` by search, the approximate path. - - Returns - ------- - tuple[list[str], np.ndarray, np.ndarray, list[tuple[tuple[int, int], ...]]] - Words, their contributions, the baseline (with any folded word's attribution), and spans. - """ - if word_positions is None or offsets is None: - return words, contributions, base_values, locate_spans(text, words) - base = base_values.astype(float).copy() - keep: list[int] = [] - placed: list[str] = [] - spans: list[tuple[Span, ...]] = [] - for k, positions in enumerate(word_positions): - span = trim_span(text, span_from_offsets(offsets, positions)) - if span is None: - base = base + contributions[k] # covers no visible character: not a word (see module docstring) - continue - keep.append(k) - placed.append(text[span[0] : span[1]]) - spans.append((span,)) - return placed, contributions[keep], base, spans - - def _valid_offsets(offsets: list[tuple[int, int]] | None, n_tokens: int) -> list[tuple[int, int]] | None: """Return ``offsets`` only when there is exactly one per attributed token, else ``None``. @@ -287,7 +249,7 @@ def __init__( ) -> None: super().__init__(model, preprocessing, label_names, explainer_args, explainer_compute_args) self.show_progress = show_progress - # Same token SHAP's Text masker substitutes (the tokenizer's mask token) whenever one exists. + # The token SHAP substitutes too (the tokenizer's mask token) whenever one exists. self.baseline_token = model.baseline_token captum_attr = import_optional_module("captum.attr", extra=_NLP_EXTRA) self.explainer = captum_attr.LayerIntegratedGradients(model.logits, model.embedding_layer) @@ -340,15 +302,21 @@ def run_explainer(self, x) -> NlpContributions: stacked = np.stack(per_class, axis=-1) # (seq, n_classes) alignment = _valid_alignment(model.word_alignment(text), n_tokens=stacked.shape[0]) offsets = _valid_offsets(getattr(model, "token_offsets", lambda _: None)(text), n_tokens=stacked.shape[0]) - if alignment is not None: - word_tokens, word_contribs, base_logits = _aggregate_by_alignment(stacked, base_logits, alignment) - else: - word_tokens, word_contribs, base_logits = _aggregate_subwords( - list(tokens), stacked, base_logits, special_tokens=_model_special_tokens(model) + if alignment is not None and offsets is not None: + # The exact path: words, their spans and strings from the model's own encoding. + layout = word_layout(text, alignment[1], offsets) + word_tokens, word_contribs, base_logits, word_spans = layout.aggregate( + stacked, base_logits, per="token" ) - word_tokens, word_contribs, base_logits, word_spans = _place_words( - text, word_tokens, word_contribs, base_logits, None if alignment is None else alignment[1], offsets - ) + else: + # No offsets to place words with: keep the token-built strings and search for them. + if alignment is not None: + word_tokens, word_contribs, base_logits = _aggregate_by_alignment(stacked, base_logits, alignment) + else: + word_tokens, word_contribs, base_logits = _aggregate_subwords( + list(tokens), stacked, base_logits, special_tokens=_model_special_tokens(model) + ) + word_spans = locate_spans(text, word_tokens) contributions.append(word_contribs) # (n_words, n_classes) base_values.append(base_logits) data.append(word_tokens) diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index 2247795b..ca922138 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -5,6 +5,44 @@ (``get_local_contributions``, common ``__init__`` skeleton) lives in ``NlpBackend`` (see ``nlp_backend.py``). +It masks text one of two ways: in the model's token ids when the model exposes them, as a string +otherwise. + +Token-id masking +---------------- +``shap.maskers.Text`` masks *strings*: it cuts the text into segments, glues the kept ones back +together with ``" [MASK]"`` for the hidden ones, and lets the model tokenize the result again. Two +things go wrong on the way, and both change what the model is asked: + +- with nothing masked, the glued string is not the text. Tokens sharing a character (a byte-level + BPE emoji or ``’``, a SentencePiece ``▁`` holding the next character's offsets) put it in two + segments, so ``était`` becomes ``éétait``; runs of whitespace collapse to one space. SHAP stays + additive, but for that other string; +- hiding one token changes its neighbours. Tokenizing ``"<mask>love"`` again gives ``love`` + where the text had ``Ġlove``, and ``"[MASK]believably"`` turns ``##bel`` into ``bel``, so a + coalition is not "these tokens hidden, the others as they were". + +A model exposing its token ids (:class:`~shapash.model.base.SupportsCaptumIG`, with a fast +tokenizer) is masked in its ids instead, by :class:`_TokenIdMasker`. The text is encoded once, by +the model (its truncation, its special tokens); a masked input is that encoding with the hidden +positions set to the model's :meth:`~shapash.model.base.SupportsCaptumIG.reference_ids`, the +``[MASK]`` reference LIG integrates from. With nothing hidden the model sees exactly its encoding of +the text, every masked input has the same length, and with everything hidden it sees LIG's +reference. + +What SHAP perturbs are the pieces of the text's :class:`~shapash.compute.word_layout.WordLayout`: a +word's tokens, with tokens sharing a character as one piece, so a masked input never holds half a +character. Special tokens stay as they are in every input. The hierarchy SHAP's Partition explainer +walks keeps each word one branch (its pieces first, then words pairwise), so a word's value, the sum +of its pieces', is its Owen value as a single player. Words are then reported as LIG reports them, +through the same layout. + +String masking +-------------- +Any other model (a pipeline, a bare callable, or an explicit ``masker=`` / ``explainer_args``) goes +through ``shap.maskers.Text`` and keeps the defects above. The rest of this docstring is about that +path. + ``shap.maskers.Text.token_segments`` emits *segments* of the source string rather than the tokenizer's raw subword strings, and it does so in three different regimes: the offset-mapping path slices each token up to the start of the next (so a segment carries the *trailing* gap text), the @@ -33,14 +71,19 @@ import re from collections.abc import Sequence -from typing import Literal, cast +from dataclasses import dataclass, field +from typing import Any, Literal, cast import numpy as np import shap +from shapash._optional import import_optional_module from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.compute.spans import Span, is_unsegmented_script, locate_spans, span_from_offsets, trim_span -from shapash.model.base import SupportsLogits, TextModel, has_capabilities +from shapash.compute.word_layout import WordLayout, word_layout +from shapash.model.base import SupportsCaptumIG, SupportsLogits, TextModel, has_capabilities + +_NLP_EXTRA = 'Install the NLP extra: pip install "shapash[nlp]".' # SHAP's masker reports special tokens as blank segments on its offset-mapping path; their # attribution is folded into the baseline during word aggregation. @@ -232,6 +275,198 @@ def _shares_a_character(i: int) -> bool: return words, stacked, base, word_spans +# ---------------------------------------------------------------------------------------------------- +# Token-id masking (see the module docstring) +# ---------------------------------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class _Encoding: + """One text as :class:`_TokenIdMasker` masks it: the model's encoding, its reference, its words. + + Attributes + ---------- + input_ids : np.ndarray + The model's encoding of the text, ``(seq,)``. + reference_ids : np.ndarray + ``input_ids`` with every piece's tokens set to the model's reference id, ``(seq,)``. + layout : WordLayout + The text's words; their pieces are the features SHAP masks. + feature_of_token : np.ndarray + Each token's feature index, ``-1`` for a token in no feature (a special token). + """ + + input_ids: np.ndarray + reference_ids: np.ndarray + layout: WordLayout + feature_of_token: np.ndarray = field(init=False, repr=False) + + def __post_init__(self) -> None: + owner = np.full(len(self.input_ids), -1, dtype=int) + for f, positions in enumerate(self.layout.pieces): + owner[positions] = f + object.__setattr__(self, "feature_of_token", owner) + + +def _supports_token_ids(model: Any) -> bool: + """Whether ``model`` can be masked in its token ids. + + It needs the id-level surface of :class:`~shapash.model.base.SupportsCaptumIG` (``encode``, + ``reference_ids``, ``logits``) and a tokenizer that reports word ids and offsets — a fast one; + a slow tokenizer reports neither, so pieces could not be grouped into words or placed. + """ + if not has_capabilities(model, SupportsCaptumIG): + return False + try: + return model.word_alignment("a") is not None and model.token_offsets("a") is not None + except Exception: # noqa: BLE001 — a probe: any failure means "not supported", never a crash + return False + + +def _encode(model: SupportsCaptumIG, text: str) -> _Encoding: + """Encode ``text`` with ``model`` and cut the encoding into words and pieces. + + Raises + ------ + ValueError + When the model's word alignment or offsets do not cover its encoding (they come from + separate tokenizer calls, so a tokenizer disagreeing with itself would misplace every value). + """ + input_ids, _, _ = model.encode(text) + ids = np.asarray(input_ids[0].tolist(), dtype=np.int64) + alignment = model.word_alignment(text) + offsets = model.token_offsets(text) + if alignment is None or offsets is None or len(offsets) != len(ids): + raise ValueError(f"{type(model).__name__} gave no word alignment or offsets matching its encoding of {text!r}.") + layout = word_layout(text, alignment[1], offsets) + reference = np.asarray(model.reference_ids(input_ids)[0].tolist(), dtype=np.int64) + masked = np.zeros(len(ids), dtype=bool) + masked[[p for piece in layout.pieces for p in piece]] = True + return _Encoding(input_ids=ids, reference_ids=np.where(masked, reference, ids), layout=layout) + + +def _balanced(nodes: list[int], rows: list[list[float]], sizes: dict[int, int], next_id: int) -> tuple[int, int]: + """Merge ``nodes`` pairwise, level by level, into one; append the merges to ``rows``.""" + while len(nodes) > 1: + merged = [] + for a, b in zip(nodes[::2], nodes[1::2], strict=False): + sizes[next_id] = sizes[a] + sizes[b] + rows.append([a, b, 0.0, sizes[next_id]]) + merged.append(next_id) + next_id += 1 + if len(nodes) % 2: + merged.append(nodes[-1]) + nodes = merged + return nodes[0], next_id + + +def _clustering(layout: WordLayout) -> np.ndarray: + """The feature hierarchy, as the linkage matrix SHAP's Partition explainer reads. + + Each word's pieces merge first, so every word is one branch; the words then merge pairwise with + their neighbours. Heights are the cluster sizes, rescaled to ``(0, 1]``, which is also what + ``shap.maskers.Text`` uses. + """ + n = layout.n_pieces + rows: list[list[float]] = [] + sizes = dict.fromkeys(range(n), 1) + next_id, first = n, 0 + word_nodes = [] + for word in layout.words: + node, next_id = _balanced(list(range(first, first + len(word))), rows, sizes, next_id) + word_nodes.append(node) + first += len(word) + if word_nodes: + _balanced(word_nodes, rows, sizes, next_id) + tree = np.array(rows, dtype=float).reshape(-1, 4) + if len(tree): + tree[:, 2] = tree[:, 3] / tree[:, 3].max() + return tree + + +class _TokenIdMasker(shap.maskers.Masker): + """SHAP masker hiding a text's pieces in the model's own encoding (see the module docstring). + + Parameters + ---------- + model : SupportsCaptumIG + The model whose encoding is masked; :func:`_supports_token_ids` must hold. + """ + + # The ``Text`` masker's default: SHAP's batch size moves which nodes the Owen loop expands under + # a ``max_evals`` budget, so keeping it keeps the documented trade-offs of that knob. + default_batch_size = 5 + + def __init__(self, model: SupportsCaptumIG) -> None: + self.model = model + self.immutable_outputs = True + # What a hidden token becomes, read like ``shap.maskers.Text.mask_token``. + self.mask_token = model.baseline_token + self._text: str | None = None + self._encoding: _Encoding | None = None + + def encoding(self, s: str) -> _Encoding: + """The :class:`_Encoding` of ``s``, cached for the text being explained.""" + if self._encoding is None or s != self._text: + self._encoding, self._text = _encode(self.model, s), s + return self._encoding + + def __call__(self, mask: np.ndarray, s: str) -> tuple[np.ndarray]: + """The encoding of ``s`` with every feature ``mask`` hides set to its reference id.""" + encoding = self.encoding(s) + owner = encoding.feature_of_token + hidden = owner >= 0 + hidden[hidden] = ~np.asarray(mask, dtype=bool)[owner[hidden]] + return (np.where(hidden, encoding.reference_ids, encoding.input_ids)[None, :],) + + def shape(self, s: str) -> tuple[int, int]: + """One masked input per mask, over the text's features.""" + return (1, self.encoding(s).layout.n_pieces) + + def mask_shapes(self, s: str) -> list[tuple[int]]: + """One boolean per feature.""" + return [(self.encoding(s).layout.n_pieces,)] + + def feature_names(self, s: str) -> list[list[str]]: + """Each feature's tokens, as the tokenizer writes them.""" + encoding = self.encoding(s) + convert = self.model.tokenizer.convert_ids_to_tokens # type: ignore[attr-defined] + return [["".join(convert(encoding.input_ids[piece].tolist())) for piece in encoding.layout.pieces]] + + def clustering(self, s: str) -> np.ndarray: + """See :func:`_clustering`.""" + return _clustering(self.encoding(s).layout) + + +class _TokenIdScorer: + """The model scored on token ids, as SHAP calls it with :class:`_TokenIdMasker`'s inputs. + + Parameters + ---------- + model : SupportsCaptumIG + The model behind the masker. + output_space : {"probability", "logit"} + ``"logit"`` returns :meth:`~shapash.model.base.SupportsCaptumIG.logits`; ``"probability"`` + their softmax, which is what :meth:`~shapash.model.base.TextModel.predict` returns for a + model exposing logits. + """ + + def __init__(self, model: SupportsCaptumIG, output_space: Literal["probability", "logit"]) -> None: + self.model = model + self.output_space = output_space + + def __call__(self, input_ids: np.ndarray) -> np.ndarray: + """``(n, n_classes)`` scores for ``n`` same-length id rows (one text's masked inputs).""" + torch = import_optional_module("torch", extra=_NLP_EXTRA) + device = self.model.embedding_layer.weight.device + ids = torch.as_tensor(np.asarray(input_ids), dtype=torch.long, device=device) + with torch.no_grad(): + logits = self.model.logits(ids, torch.ones_like(ids)).float() + if self.output_space == "probability": + logits = torch.softmax(logits, dim=-1) + return logits.cpu().numpy() + + class NlpShapBackend(NlpBackend): """SHAP backend for text classification models (HuggingFace pipelines, etc.). @@ -244,14 +479,18 @@ class NlpShapBackend(NlpBackend): A :class:`~shapash.model.base.TextModel`, whose scoring surface is chosen by ``output_space``; or a text callable accepted by ``shap.Explainer`` (e.g. a ``transformers.pipeline`` with ``return_all_scores=True``), which is taken to return - probabilities. + probabilities. A ``TextModel`` exposing token ids (``SupportsCaptumIG`` with a fast tokenizer) + is masked in its own encoding (see the module docstring) unless ``masker`` or + ``explainer_args`` is given. preprocessing : None Unused; accepted for interface compatibility with ``BaseBackend``. label_names : list[str] or None Class names in the same order as the model output columns. masker : any, optional - Forwarded to ``shap.Explainer`` when ``explainer_args`` is not given. - Typically ``None`` for text (SHAP auto-selects a ``TextMasker``). + Forwarded to ``shap.Explainer`` when ``explainer_args`` is not given. Typically ``None``: a + model exposing token ids is then masked in its encoding, and SHAP infers a ``Text`` masker + for a pipeline. Passing one (e.g. ``shap.maskers.Text(tokenizer)``) always takes the string + path. explainer_args : dict, optional Keyword arguments forwarded to ``shap.Explainer.__init__``. Use ``{"explainer": SomeExplainerClass, ...}`` to inject a custom @@ -264,7 +503,7 @@ class NlpShapBackend(NlpBackend): converged: any imdb review over ~40 words exhausts it and lands 8-45% (max|Δ| / max|value|) from the fully-traversed explanation, which costs 3k-23k evals. - ``batch_size`` (default: the ``Text`` masker's 5) caps how many masked variants reach + ``batch_size`` (default 5, on both masking paths) caps how many masked variants reach ``model`` per call, and is *not* free. The Owen loop drains a whole batch from its best-first queue before pushing any children, so a larger batch expands different nodes under the same ``max_evals`` and deterministically shifts the values, within the error band @@ -272,7 +511,7 @@ class NlpShapBackend(NlpBackend): 512-token forward pass already saturates the GPU. batch_size : int or None, optional Batch size applied to ``model`` when it is a ``transformers`` pipeline that does not - already have one. Default 64. Pass ``None`` to leave the pipeline untouched. Distinct from + already have one (string path only). Default 64. Pass ``None`` to leave the pipeline untouched. Distinct from the explainer's ``batch_size`` above: this one only regroups the strings SHAP has already chosen, so it never changes the explanation. @@ -297,9 +536,9 @@ class NlpShapBackend(NlpBackend): """ name = "nlp_shap" - # No reference is learned from data: the ``Text`` masker infers its own masking - # scheme from the pipeline/tokenizer (``masker=None`` below), so ``fit`` has - # nothing backend-specific to learn here. + # No reference is learned from data: a hidden token becomes the model's reference id (token-id + # masking) or the tokenizer's mask token (``Text`` masker), so ``fit`` has nothing + # backend-specific to learn here. reference_kind = "none" # Shapley values satisfy the efficiency axiom by construction, and stay additive # even under the Partition/Owen path SHAP silently takes for text. Owen values satisfy efficiency too. @@ -329,7 +568,14 @@ def __init__( "output_space='logit' cannot be combined with explainer_args: those build the explainer " "around their own model, whose output space this backend cannot check." ) - if isinstance(model, TextModel): + # How the text is masked: in the model's own encoding whenever it allows it and the caller + # brought no masker or explainer of their own (see the module docstring). + self.masking: Literal["token_ids", "text"] = "text" + if isinstance(model, TextModel) and masker is None and not explainer_args and _supports_token_ids(model): + masker = _TokenIdMasker(model) # type: ignore[arg-type] + model = _TokenIdScorer(model, output_space) # type: ignore[arg-type] + self.masking = "token_ids" + elif isinstance(model, TextModel): model, masker = _resolve_text_model(model, masker, output_space) elif output_space == "logit": raise TypeError( @@ -357,6 +603,8 @@ def __init__( self.explainer = self.explainer_args["explainer"](**shap_params) elif self.explainer_args: self.explainer = shap.Explainer(**self.explainer_args) + elif self.masking == "token_ids": + self.explainer = shap.Explainer(model, masker=self.masker, algorithm="partition") else: # ``masker=None`` lets SHAP auto-infer a Text masker from a transformers pipeline (the # HFClassifierModel/pipeline path); an explicit masker is required when ``model`` is a plain @@ -388,6 +636,8 @@ def run_explainer(self, x) -> NlpContributions: Ragged list of value arrays, baseline predictions, and token strings per sample. """ + if getattr(self, "masking", "text") == "token_ids": + return self._run_token_ids(list(x)) shap_explanation = self.explainer(x, **self.explainer_compute_args) contributions: list[np.ndarray] = [] @@ -416,3 +666,30 @@ def run_explainer(self, x) -> NlpContributions: base_values=np.stack(base_values, axis=0), token_spans=spans, ) + + def _run_token_ids(self, texts: list[str]) -> NlpContributions: + """Explain each text on its token-id features, then report its words (see the module docstring).""" + masker: _TokenIdMasker = self.masker + contributions: list[np.ndarray] = [] + base_values: list[np.ndarray] = [] + data: list[list[str]] = [] + spans: list[list[tuple[Span, ...]]] = [] + for text in map(str, texts): + encoding = masker.encoding(text) + if encoding.layout.n_pieces == 0: # nothing to hide: the text's own score is the baseline + values = np.zeros((0, 0)) + base = self.model(encoding.input_ids[None, :])[0] + else: + explanation = self.explainer([text], **self.explainer_compute_args) + values, base = explanation.values[0], explanation.base_values[0] + words, word_values, word_base, word_spans = encoding.layout.aggregate(values, base) + data.append(words) + contributions.append(word_values) + base_values.append(word_base) + spans.append(word_spans) + return NlpContributions( + token_strings=data, + values=contributions, + base_values=np.stack(base_values, axis=0), + token_spans=spans, + ) diff --git a/shapash/compute/word_layout.py b/shapash/compute/word_layout.py new file mode 100644 index 00000000..abcd6ccf --- /dev/null +++ b/shapash/compute/word_layout.py @@ -0,0 +1,167 @@ +"""Words of an encoded text: which tokens make each word, and where each word sits in the text. + +A sequence backend explains a text on the model's own encoding and reports words. The tokenizer +says which tokens belong to which word (its ``word_ids()``, through +:meth:`~shapash.model.base.SupportsCaptumIG.word_alignment`) and which characters each token covers +(its offset mapping). :func:`word_layout` turns the two into a :class:`WordLayout`, and +:meth:`WordLayout.aggregate` sums per-token or per-piece values into words. The LIG and SHAP +backends both build their words this way, so they report the same units. + +Three rules keep a word's value additive and its string as written: + +- a character belongs to one word and one piece. Tokens sharing a character (a byte-level BPE emoji + or ``é`` split across bytes, a SentencePiece ``▁`` given the next character's offsets) are one + piece, and a word whose first token shares a character with the previous word joins that word; +- a word's span is its tokens' offsets, whitespace trimmed, and its string is the text at that span, + never the tokenizer's rebuilt string (which an uncased tokenizer lowercases and strips of accents, + and which reads ``[UNK]`` for an emoji); +- values belonging to no visible word fold into the baseline, so ``base + Σ words`` is unchanged. + That covers special tokens, and a word covering only whitespace: byte-level BPE gives a lone + ``Ġ``/``Ċ`` its own word id on a double space or a newline. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Literal + +import numpy as np + +from shapash.compute.spans import Span, span_from_offsets, trim_span + + +@dataclass(frozen=True) +class WordLayout: + """One encoded text, cut into words and each word into pieces. + + Attributes + ---------- + text : str + The encoded text. + words : list of list of list of int + Per word, its pieces; per piece, its token positions. In text order. + spans : list of (int, int) or None + Each word's character span in ``text``, whitespace trimmed; ``None`` for a word covering no + visible character. + """ + + text: str + words: list[list[list[int]]] + spans: list[Span | None] + + @property + def pieces(self) -> list[list[int]]: + """Every piece's token positions, in text order.""" + return [piece for word in self.words for piece in word] + + @property + def n_pieces(self) -> int: + """How many pieces the words hold.""" + return sum(len(word) for word in self.words) + + def aggregate( + self, values: np.ndarray, base_values: np.ndarray, *, per: Literal["piece", "token"] = "piece" + ) -> tuple[list[str], np.ndarray, np.ndarray, list[tuple[Span, ...]]]: + """Sum values into words; fold whatever is not a visible word into the baseline. + + Parameters + ---------- + values : np.ndarray + ``(n_pieces, n_classes)`` with ``per="piece"``; ``(n_tokens, n_classes)``, one row per + token of the encoding, with ``per="token"``. A token in no word (a special token) then + folds into the baseline. + base_values : np.ndarray + ``(n_classes,)``. + per : {"piece", "token"}, default "piece" + What a row of ``values`` stands for. + + Returns + ------- + tuple[list[str], np.ndarray, np.ndarray, list[tuple[tuple[int, int], ...]]] + Word strings (read from ``text``), per-word values ``(n_words, n_classes)``, the baseline + with every folded value, and each word's span. + """ + base = np.asarray(base_values, dtype=float).reshape(-1).copy() + n_classes = base.shape[0] + values = np.asarray(values, dtype=float) + if per == "token": + values = values.reshape(-1, n_classes) + in_word = np.zeros(len(values), dtype=bool) + in_word[[p for piece in self.pieces for p in piece]] = True + base = base + values[~in_word].sum(axis=0) + values = np.array([values[piece].sum(axis=0) for piece in self.pieces]) + values = values.reshape(self.n_pieces, n_classes) + + words: list[str] = [] + rows: list[np.ndarray] = [] + spans: list[tuple[Span, ...]] = [] + first = 0 + for word, span in zip(self.words, self.spans, strict=True): + row = values[first : first + len(word)].sum(axis=0) + first += len(word) + if span is None: + base = base + row + continue + words.append(self.text[span[0] : span[1]]) + rows.append(row) + spans.append((span,)) + stacked = np.stack(rows, axis=0) if rows else np.zeros((0, n_classes)) + return words, stacked, base, spans + + +def word_layout(text: str, word_positions: Sequence[Sequence[int]], offsets: Sequence[Sequence[int]]) -> WordLayout: + """Cut an encoded text into words and pieces, from the tokenizer's word grouping and offsets. + + Parameters + ---------- + text : str + The encoded text. + word_positions : sequence of sequence of int + Per word, its token positions, as ``word_alignment`` reports them (special tokens are in no + word). + offsets : sequence of (int, int) + The tokenizer's offset mapping for ``text``, one entry per token of the encoding. + + Returns + ------- + WordLayout + + Raises + ------ + ValueError + When a position indexes past ``offsets``: the two come from separate tokenizer calls, so a + tokenizer disagreeing with itself would misplace every value. + """ + if any(p >= len(offsets) for positions in word_positions for p in positions): + raise ValueError(f"Word positions index past the {len(offsets)} token offsets of {text!r}.") + words = _group_pieces(word_positions, offsets) + spans = [trim_span(text, span_from_offsets(offsets, [p for piece in word for p in piece])) for word in words] + return WordLayout(text=text, words=words, spans=spans) + + +def _group_pieces(word_positions: Sequence[Sequence[int]], offsets: Sequence[Sequence[int]]) -> list[list[list[int]]]: + """Group each word's tokens into pieces: tokens sharing a character are one piece. + + A token with no extent (a bare word-start marker some tokenizers give empty offsets) joins the + piece it sits in. A word whose first token shares a character with the previous word's last + piece is the same word: a character is never split between two words. + """ + words: list[list[list[int]]] = [] + end = -1 # end of the current piece's characters; -1 while it covers none + for positions in word_positions: + for k, p in enumerate(positions): + start, stop = offsets[p] + empty = stop <= start + overlaps = not empty and 0 <= start < end + if k == 0 and not (words and overlaps): + words.append([[p]]) + end = -1 + elif k == 0 or empty or end < 0 or overlaps: + words[-1][-1].append(p) + else: + words[-1].append([p]) + end = -1 + if not empty: + end = max(end, stop) + return words diff --git a/shapash/explainer/nlp_explainer.py b/shapash/explainer/nlp_explainer.py index 8f199b61..7c5ce2c3 100644 --- a/shapash/explainer/nlp_explainer.py +++ b/shapash/explainer/nlp_explainer.py @@ -19,6 +19,7 @@ import numpy as np import pandas as pd +from shapash.__version__ import __version__ from shapash.backend.nlp_backend import NlpBackend, NlpContributions from shapash.backend.nlp_shap_backend import NlpShapBackend from shapash.compute.diagnostics.label_noise import ( @@ -57,20 +58,12 @@ def _cache_file(data_hash: str, cache_dir: Path) -> Path: return cache_dir / f"{data_hash}.xpl" -# Version of how backends cut a text into units and what each unit carries. It enters the cache key -# because the key covers texts/model/backend but not shapash itself: when a release changes the -# units (v2 — every unit carries its character spans, unit strings are read off the source text, -# whitespace-only LIG units fold into the baseline), entries computed before would otherwise be -# served unchanged. Bump it whenever a backend's units change. -_UNITS_LAYOUT = "units-v2" - - def _load_cached(cache_path: Path | None) -> NlpExplanation | None: """Read a cached explanation, or return ``None`` when the entry cannot be read. - An unreadable entry is a **miss, not a failure**: the cache key covers texts/model/backend but - not the shapash version, so a layout change leaves entries the new reader cannot parse, and a - cache exists so the caller need not care whether the answer was on disk. + An unreadable entry is a **miss, not a failure**: a damaged file, or one written by a shapash + build whose layout this reader cannot parse (the key changes with every release, not with every + commit), and a cache exists so the caller need not care whether the answer was on disk. :meth:`NlpExplanation.load` called directly still raises. Scoped to the errors a stale or damaged entry produces (a missing meta key or parquet member, @@ -170,17 +163,16 @@ def __init__( self._text_model = None # Default backend only when the caller brought none. Building it reads the model's SHAP surface - # (``shap_callable`` + its companion ``shap_masker``: ``None`` for a pipeline-backed model, which - # SHAP can infer a Text masker from; explicit when the callable is a bare scoring function) — - # which must not happen when an explicit backend makes those values unused, or an adapter that - # never intends to be explained by SHAP could not be used at all. + # (its token ids when it exposes them; otherwise ``shap_callable`` + its companion + # ``shap_masker``, see ``NlpShapBackend``) — which must not happen when an explicit backend + # makes that surface unused, or an adapter that never intends to be explained by SHAP could not + # be used at all. if backend is not None: self.backend: NlpBackend = backend else: self.backend = NlpShapBackend( - model=self._text_model.shap_callable if self._text_model is not None else model, + model=self._text_model if self._text_model is not None else model, label_names=self.label_names, - masker=self._text_model.shap_masker if self._text_model is not None else None, explainer_args=explainer_args or {}, explainer_compute_args=explainer_compute_args or {}, ) @@ -939,10 +931,11 @@ def _compute_key(self, text_list: list[str]) -> str: The cached explanation is a function of the texts **and** of everything that scores them, so all of it belongs in the key: the model's own identity declaration (:attr:`~shapash.model.base.TextModel.model_id` — checkpoint, pooling, normalization, head - weights), the backend's registered ``name``, its explainer settings, the explanation space and - baseline token it reports (both constructor choices, not class constants — a probability-space - and a logit-space ``nlp_shap`` share every other part of the key), and ``label_names`` (which - fixes the column order of ``y_prob``). + weights), the backend's registered ``name``, its explainer settings, the explanation space, + baseline token and masking it reports (constructor choices, not class constants — a + probability-space and a logit-space ``nlp_shap`` share every other part of the key), and + ``label_names`` (which fixes the column order of ``y_prob``). The shapash version is in it too: a + release may change how backends compute or cut units, and must not read an older one's entries. Keying on the texts alone — as this once did — means swapping the backend (SHAP -> LIG), the model, or the label order and pointing at the same ``cache_dir`` silently reloads the @@ -961,8 +954,11 @@ def _compute_key(self, text_list: list[str]) -> str: compute_args = sorted(getattr(backend, "explainer_compute_args", {}).items()) space = getattr(backend, "output_space", None) baseline = getattr(backend, "baseline_token", None) - backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}:{space}:{baseline!r}" - return hash_corpus(text_list, f"{_UNITS_LAYOUT}|{model_id}|{backend_id}|{self.label_names!r}") + # How SHAP masks (token ids or strings) changes its values, and an explicit ``masker=`` + # selects the string path without touching any other part of the key. + masking = getattr(backend, "masking", None) + backend_id = f"{type(backend).name}:{args!r}:{compute_args!r}:{space}:{baseline!r}:{masking}" + return hash_corpus(text_list, f"{__version__}|{model_id}|{backend_id}|{self.label_names!r}") def _require_text_model(self) -> TextModel: text_model = getattr(self, "_text_model", None) diff --git a/shapash/model/encoder.py b/shapash/model/encoder.py index 766b27a1..1e0e8f0f 100644 --- a/shapash/model/encoder.py +++ b/shapash/model/encoder.py @@ -697,8 +697,9 @@ def encode(self, text: str): def _baseline_token_id(self) -> int: """The id substituted for content tokens in :meth:`reference_ids`: mask, else pad, else unk, else 0. - ``mask`` comes first because it is what SHAP's ``Text`` masker substitutes (it takes the - tokenizer's ``mask_token``), so LIG and SHAP measure against the same "word absent" input. + ``mask`` comes first because it is what SHAP substitutes for a hidden token too — through + these reference ids when it masks token ids, through the tokenizer's ``mask_token`` with + ``shap.maskers.Text`` — so LIG and SHAP measure against the same "word absent" input. Aligning on it raised SHAP<->LIG rank agreement from 0.46 to 0.57 on DistilBERT. Explicit ``None`` checks, not ``or``: id ``0`` is a legitimate token id (BERT's ``[PAD]``, RoBERTa's ``<s>``). diff --git a/tests/integration_tests/test_nlp_unit_alignment.py b/tests/integration_tests/test_nlp_unit_alignment.py index bd6453ab..f616e8c2 100644 --- a/tests/integration_tests/test_nlp_unit_alignment.py +++ b/tests/integration_tests/test_nlp_unit_alignment.py @@ -9,7 +9,9 @@ - every unit is placed: it has spans, and its string is the source text at its span; - no unit is whitespace; -- SHAP and LIG cover the same characters, since both read the same tokenizer's offsets; +- SHAP and LIG report the same units, since both read the model's own encoding; +- SHAP is additive against the text itself: it masks token ids, so the model never sees a rebuilt string; +- SHAP in probability space (its default) compares with LIME, which explains probabilities too; - aligning backends drops nothing: each backend's values sum to the same total before and after. The models are randomly initialised from each checkpoint's config at a tiny size, so only the config @@ -17,7 +19,10 @@ or ``lime`` extra is missing or a checkpoint cannot be fetched. """ +import warnings + import numpy as np +import pandas as pd import pytest transformers = pytest.importorskip("transformers") @@ -91,6 +96,7 @@ def explanations(request): "shap": NlpShapBackend( model, label_names=LABELS, output_space="logit", explainer_compute_args={"max_evals": 500} ), + "shap_prob": NlpShapBackend(model, label_names=LABELS, explainer_compute_args={"max_evals": 500}), "lig": NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}), "lime": NlpLimeBackend(model, label_names=LABELS, explainer_compute_args={"num_samples": 50}), } @@ -99,21 +105,10 @@ def explanations(request): for name, backend in backends.items() } out["model"] = model - out["shap_masker"] = backends["shap"].explainer.masker + assert backends["shap"].masking == "token_ids" return out -def _shap_input(masker, text: str) -> str: - """The string SHAP's ``Text`` masker hands the model when nothing is masked. - - Not always ``text``: the masker rebuilds it from its segments, so it collapses whitespace and, - where tokens share a character (an emoji split by byte-level BPE), repeats it. An upstream - behaviour of ``shap.maskers.Text``, outside what alignment can fix. - """ - segments, _ = masker.token_segments(text) - return str(masker(np.ones(len(segments), dtype=bool), text)[0][0]) - - @pytest.mark.parametrize("backend", ["shap", "lig", "lime"]) def test_every_unit_is_placed_and_reads_as_written(explanations, backend): exp = explanations[backend] @@ -129,27 +124,55 @@ def test_every_unit_is_placed_and_reads_as_written(explanations, backend): assert all(a[1] <= b[0] for a, b in zip(flat, flat[1:], strict=False)) -def test_shap_and_lig_cover_the_same_characters(explanations): +def test_shap_and_lig_report_the_same_units(explanations): + shap, lig = explanations["shap"], explanations["lig"] for row, text in enumerate(TEXTS): - covered = {} - for backend in ("shap", "lig"): - chars = set() - for ((start, end),) in explanations[backend].token_spans[row]: - chars.update(range(start, end)) - covered[backend] = chars - assert covered["shap"] == covered["lig"], f"row {row}: {text!r}" - visible = {i for i, c in enumerate(text) if not c.isspace()} - assert covered["shap"] <= visible - - -def test_shap_stays_exactly_additive_after_merging(explanations): - # Against the string SHAP explains (see _shap_input): merging units must not move the total. + assert shap.token_spans[row] == lig.token_spans[row], f"row {row}: {text!r}" + assert shap.token_strings[row] == lig.token_strings[row] + + +def test_shap_is_exactly_additive_against_the_text_itself(explanations): exp, model = explanations["shap"], explanations["model"] - logits = model.predict_logits([_shap_input(explanations["shap_masker"], text) for text in TEXTS]) + logits = model.predict_logits(TEXTS) for row in range(len(TEXTS)): np.testing.assert_allclose(exp.base_values[row] + exp.values[row].sum(axis=0), logits[row], atol=1e-4) +def test_shap_and_lig_measure_against_the_same_reference(explanations): + # Both hide every content token behind the model's reference id, so both baselines are the + # reference's logits — except where a whitespace-only word (a byte-level "Ġ"/"Ċ") folds its own, + # method-specific value into them: compare on texts without one. + shap, lig = explanations["shap"], explanations["lig"] + for row, text in enumerate(TEXTS): + if any(c in text for c in "\n\t") or " " in text: + continue + np.testing.assert_allclose(shap.base_values[row], lig.base_values[row], atol=1e-4) + + +def test_shap_and_lime_compare_in_probability_space(explanations): + prob, logit, lime = explanations["shap_prob"], explanations["shap"], explanations["lime"] + assert (prob.output_space, lime.output_space) == ("probability", "probability") + # The output space changes the values, never the units. + assert prob.token_spans == logit.token_spans and prob.token_strings == logit.token_strings + probs = explanations["model"].predict(TEXTS) + for row in range(len(TEXTS)): + np.testing.assert_allclose(prob.base_values[row] + prob.values[row].sum(axis=0), probs[row], atol=1e-5) + with warnings.catch_warnings(): + warnings.simplefilter("error") # same space: no cross-space warning + df = corpus_agreement(prob, {"lime": lime}, label_idx=1) + assert len(df) == len(TEXTS) + assert np.isfinite(df.spearman.fillna(0)).all() + # LIME splits words its own way, so coverage is partial, but it is the same against either space. + pd.testing.assert_series_equal( + df.coverage.reset_index(drop=True), + corpus_agreement(logit, {"lime": lime}, label_idx=1).coverage.reset_index(drop=True), + ) + for row in range(len(TEXTS)): + units, aligned = align_row(name_explanations(prob, {"lime": lime}), row, 1) + assert np.isfinite(aligned["shap"]).all() + assert np.nansum(aligned["shap"]) == pytest.approx(prob.values[row][:, 1].sum(), rel=1e-6, abs=1e-9) + + @pytest.mark.parametrize("label_idx", [0, 1]) def test_alignment_drops_nothing(explanations, label_idx): named = name_explanations(explanations["shap"], {"lig": explanations["lig"], "lime": explanations["lime"]}) @@ -178,20 +201,40 @@ def test_corpus_agreement_runs_on_every_family(explanations): def test_a_truncated_backend_shows_as_partial_coverage(): model = _tiny_model(CHECKPOINTS["wordpiece-uncased"]) - model.max_length = 8 # LIG truncates; SHAP's masker segments the whole text + model.max_length = 8 # SHAP and LIG explain the encoding, cut at 6 words; LIME splits the whole text text = ["one two three four five six seven eight nine ten eleven twelve"] - shap = NlpExplainer( - model, - label_names=LABELS, - backend=NlpShapBackend( - model, label_names=LABELS, output_space="logit", explainer_compute_args={"max_evals": 200} - ), - ).explain(text) - lig = NlpExplainer( - model, - label_names=LABELS, - backend=NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}), - ).explain(text) - assert len(lig.token_strings[0]) < len(shap.token_strings[0]) - row = corpus_agreement(shap, {"lig": lig}, label_idx=0).iloc[0] - assert 0 < row.coverage < 1 + explain = { + name: NlpExplainer(model, label_names=LABELS, backend=backend).explain(text) + for name, backend in { + "shap": NlpShapBackend( + model, label_names=LABELS, output_space="logit", explainer_compute_args={"max_evals": 200} + ), + "lig": NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}), + "lime": NlpLimeBackend(model, label_names=LABELS, explainer_compute_args={"num_samples": 50}), + }.items() + } + assert explain["shap"].token_strings == explain["lig"].token_strings == [text[0].split()[:6]] + def coverage(other): + return corpus_agreement(explain["shap"], {other: explain[other]}, label_idx=0).coverage.iloc[0] + + assert coverage("lig") == 1.0 + assert coverage("lime") == pytest.approx(0.5) + + +@pytest.mark.parametrize("missing", ["offsets", "alignment and offsets"]) +def test_lig_without_offsets_locates_its_words(monkeypatch, missing): + # A slow tokenizer reports no offsets (and no word ids): words keep their token-built strings + # and are found in the text by search. On a plain lowercase text that gives the offsets path's units + # and values. + model = _tiny_model(CHECKPOINTS["wordpiece-uncased"]) + text = ["i love unbelievably good tea"] + backend = NlpCaptumLigBackend(model, label_names=LABELS, explainer_compute_args={"n_steps": 2}) + exact = backend.run_explainer(text) + monkeypatch.setattr(model, "token_offsets", lambda text: None) + if missing == "alignment and offsets": + monkeypatch.setattr(model, "word_alignment", lambda text: None) + located = backend.run_explainer(text) + assert exact.token_strings == located.token_strings == [text[0].split()] + assert exact.token_spans == located.token_spans == [[((0, 1),), ((2, 6),), ((7, 19),), ((20, 24),), ((25, 28),)]] + np.testing.assert_allclose(located.values[0], exact.values[0], atol=1e-6) + np.testing.assert_allclose(located.base_values, exact.base_values, atol=1e-6) diff --git a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py index 7c982067..f3d467c5 100644 --- a/tests/unit_tests/backend/test_nlp_captum_lig_backend.py +++ b/tests/unit_tests/backend/test_nlp_captum_lig_backend.py @@ -13,7 +13,6 @@ from shapash.backend.nlp_captum_lig_backend import ( _aggregate_by_alignment, _aggregate_subwords, - _place_words, _valid_offsets, ) from shapash.model.base import SupportsCaptumIG, TextModel @@ -136,37 +135,8 @@ def test_preserves_completeness(self): np.testing.assert_allclose(new_base + word_contribs.sum(axis=0), base + contribs.sum(axis=0)) -class TestPlaceWords(unittest.TestCase): - """Words read off the source text through token offsets — offsets copied from real tokenizers.""" - - def test_words_are_shown_as_written(self): - # distilbert-base-uncased: the tokenizer rebuilds "cafe" and "[UNK]"; the text says otherwise. - text = "Le café 😍" - offsets = [(0, 0), (0, 2), (3, 6), (6, 7), (8, 9), (0, 0)] - words, values, base, spans = _place_words( - text, ["le", "cafe", "[UNK]"], np.array([[1.0], [2.0], [3.0]]), np.zeros(1), [[1], [2, 3], [4]], offsets - ) - self.assertEqual(words, ["Le", "café", "😍"]) - self.assertEqual(spans, [((0, 2),), ((3, 7),), ((8, 9),)]) - np.testing.assert_allclose(values[:, 0], [1.0, 2.0, 3.0]) - self.assertEqual(base[0], 0.0) - - def test_whitespace_only_words_fold_into_the_baseline(self): - # roberta-base on "Hello world\n\nok": a lone "Ġ" (zero-width) and two "Ċ" get word ids. - text = "Hello world\n\nok" - offsets = [(0, 0), (0, 5), (6, 6), (7, 12), (12, 13), (13, 14), (14, 16), (0, 0)] - positions = [[1], [2], [3], [4], [5], [6]] - contribs = np.arange(1.0, 7.0)[:, None] - words, values, base, _ = _place_words( - text, ["Hello", "", "world", "", "", "ok"], contribs, np.zeros(1), positions, offsets - ) - self.assertEqual(words, ["Hello", "world", "ok"]) - np.testing.assert_allclose(values[:, 0], [1.0, 3.0, 6.0]) - self.assertEqual(base[0], 2 + 4 + 5) # completeness kept: nothing is dropped - - def test_without_offsets_words_keep_their_strings_and_are_located(self): - words, _, _, spans = _place_words("I am", ["i", "am"], np.ones((2, 1)), np.zeros(1), None, None) - self.assertEqual((words, spans), (["i", "am"], [((0, 1),), ((2, 4),)])) +class TestValidOffsets(unittest.TestCase): + """Offsets come from a second tokenizer call: used only when they match the attributed tokens.""" def test_valid_offsets_need_one_entry_per_token(self): self.assertIsNone(_valid_offsets([(0, 1)], n_tokens=2)) diff --git a/tests/unit_tests/backend/test_nlp_shap_token_ids.py b/tests/unit_tests/backend/test_nlp_shap_token_ids.py new file mode 100644 index 00000000..f793686b --- /dev/null +++ b/tests/unit_tests/backend/test_nlp_shap_token_ids.py @@ -0,0 +1,253 @@ +"""Unit tests for the SHAP backend's token-id masking path. + +The fake model's logits are a sum over its tokens (an additive game), so every exact Shapley/Owen +value is known in closed form: a feature is worth the sum of ``W[id] - W[mask]`` over its tokens. +""" + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("shap") +pytestmark = pytest.mark.nlp + +from shapash.backend import NlpShapBackend # noqa: E402 +from shapash.backend.nlp_shap_backend import ( # noqa: E402 + _clustering, + _encode, + _supports_token_ids, + _TokenIdMasker, +) +from shapash.model.base import SupportsCaptumIG, SupportsLogits, TextModel # noqa: E402 + +CLS, SEP, MASK = 1, 2, 3 +TEXT = "un café 😍\n\nok" +# (id, offsets, word id): a byte-level split "é" (two tokens on one character), a zero-width +# word-start marker before the emoji, and a newline that is a word of its own. +PLAN = [ + (CLS, (0, 0), None), + (10, (0, 2), 0), # un + (11, (3, 6), 1), # caf + (12, (6, 7), 1), # é, first byte + (13, (6, 7), 1), # é, second byte + (14, (8, 8), 2), # ▁, zero width + (15, (8, 9), 2), # 😍 + (16, (9, 11), 3), # Ċ + (17, (11, 13), 4), # ok + (SEP, (0, 0), None), +] +ONLY_SPECIALS = " " + + +class _Tokenizer: + def convert_ids_to_tokens(self, ids): + return [f"t{i}" for i in ids] + + +class AdditiveModel(TextModel, SupportsCaptumIG, SupportsLogits): + """Logits = Σ W[token id], over a fixed encoding plan per text.""" + + def __init__(self, fast=True): + super().__init__(label_names=["neg", "pos"]) + self.fast = fast + self.tokenizer = _Tokenizer() + self.weights = np.random.default_rng(0).normal(size=(20, 2)) + self._embedding = torch.nn.Embedding(20, 2) + self._embedding.weight.data = torch.tensor(self.weights, dtype=torch.float32) + + def _plan(self, text): + return PLAN if text == TEXT else [(CLS, (0, 0), None), (SEP, (0, 0), None)] + + def predict(self, texts): + return torch.softmax(torch.tensor(self.predict_logits(texts)), dim=-1).numpy() + + def predict_logits(self, texts): + return np.stack([self.weights[[i for i, _, _ in self._plan(t)]].sum(axis=0) for t in texts]) + + @property + def embedding_layer(self): + return self._embedding + + def encode(self, text): + ids = torch.tensor([[i for i, _, _ in self._plan(text)]]) + return ids, torch.ones_like(ids), [f"t{i}" for i in ids[0].tolist()] + + def reference_ids(self, input_ids): + return torch.tensor([[i if i in (CLS, SEP) else MASK for i in input_ids[0].tolist()]]) + + @property + def baseline_token(self): + return "[MASK]" + + def logits(self, input_ids, attention_mask): + return self._embedding(input_ids).sum(dim=1) + + def word_alignment(self, text): + if not self.fast: + return None + plan = self._plan(text) + positions: dict[int, list[int]] = {} + for p, (_, _, w) in enumerate(plan): + if w is not None: + positions.setdefault(w, []).append(p) + specials = [p for p, (_, _, w) in enumerate(plan) if w is None] + return [f"w{w}" for w in positions], list(positions.values()), specials + + def token_offsets(self, text): + return [o for _, o, _ in self._plan(text)] if self.fast else None + + +def _token_value(model, token_id): + return model.weights[token_id] - model.weights[MASK] + + +class TestEncoding: + def test_features_words_and_reference(self): + encoding = _encode(AdditiveModel(), TEXT) + # The features are the layout's pieces: "é"'s two tokens are one, the marker joins the emoji. + assert encoding.layout.words == [[[1]], [[2], [3, 4]], [[5, 6]], [[7]], [[8]]] + assert encoding.layout.n_pieces == 6 + # Specials keep their ids in the reference; every feature position is masked. + assert encoding.reference_ids.tolist() == [CLS] + [MASK] * 8 + [SEP] + assert encoding.feature_of_token.tolist() == [-1, 0, 1, 2, 2, 3, 3, 4, 5, -1] + + def test_mismatched_offsets_are_refused(self): + model = AdditiveModel() + model.token_offsets = lambda text: [(0, 1)] + with pytest.raises(ValueError, match="no word alignment or offsets"): + _encode(model, TEXT) + + def test_an_alignment_past_the_encoding_is_refused(self): + model = AdditiveModel() + model.word_alignment = lambda text: (["w"], [[99]], []) + with pytest.raises(ValueError, match="index past"): + _encode(model, TEXT) + + +class TestMasker: + def test_nothing_hidden_is_the_encoding_everything_hidden_the_reference(self): + masker = _TokenIdMasker(AdditiveModel()) + encoding = masker.encoding(TEXT) + (kept,) = masker(np.ones(6, dtype=bool), TEXT) + (hidden,) = masker(np.zeros(6, dtype=bool), TEXT) + assert kept.tolist() == [encoding.input_ids.tolist()] + assert hidden.tolist() == [encoding.reference_ids.tolist()] + + def test_hiding_a_feature_touches_only_its_tokens(self): + masker = _TokenIdMasker(AdditiveModel()) + mask = np.ones(6, dtype=bool) + mask[2] = False # "é" + (out,) = masker(mask, TEXT) + expected = [p[0] for p in PLAN] + expected[3] = expected[4] = MASK + assert out.tolist() == [expected] + + def test_shapes_names_and_mask_token(self): + masker = _TokenIdMasker(AdditiveModel()) + assert masker.shape(TEXT) == (1, 6) + assert masker.mask_shapes(TEXT) == [(6,)] + assert masker.feature_names(TEXT) == [["t10", "t11", "t12t13", "t14t15", "t16", "t17"]] + assert masker.mask_token == "[MASK]" + + def test_every_word_is_one_branch_of_the_hierarchy(self): + layout = _encode(AdditiveModel(), TEXT).layout + tree = _TokenIdMasker(AdditiveModel()).clustering(TEXT) + n = layout.n_pieces + assert tree.shape == (n - 1, 4) + leaves = {i: {i} for i in range(n)} + for k, (a, b, height, size) in enumerate(tree): + leaves[n + k] = leaves[int(a)] | leaves[int(b)] + assert size == len(leaves[n + k]) and 0 < height <= 1 + first = 0 + for word in layout.words: + assert set(range(first, first + len(word))) in leaves.values() + first += len(word) + + def test_a_single_feature_has_an_empty_hierarchy(self): + layout = _encode(AdditiveModel(), TEXT).layout + single = type(layout)(TEXT, [[[1]]], [(0, 2)]) + assert _clustering(single).shape == (0, 4) + + +class TestSupportsTokenIds: + def test_a_fast_captum_model_is_supported(self): + assert _supports_token_ids(AdditiveModel()) + + def test_a_slow_tokenizer_or_a_failing_probe_is_not(self): + assert not _supports_token_ids(AdditiveModel(fast=False)) + broken = AdditiveModel() + broken.word_alignment = lambda text: 1 / 0 + assert not _supports_token_ids(broken) + + def test_a_model_without_ids_is_not(self): + assert not _supports_token_ids(object()) + + +class TestShapBackendOnTokenIds: + def test_values_are_the_exact_additive_contributions(self): + model = AdditiveModel() + backend = NlpShapBackend(model, label_names=model.label_names, output_space="logit") + assert backend.masking == "token_ids" + assert backend.baseline_token == "[MASK]" + out = backend.run_explainer([TEXT]) + assert out.token_strings == [["un", "café", "😍", "ok"]] + assert out.token_spans == [[((0, 2),), ((3, 7),), ((8, 9),), ((11, 13),)]] + expected = [ + _token_value(model, 10), + _token_value(model, 11) + _token_value(model, 12) + _token_value(model, 13), + _token_value(model, 14) + _token_value(model, 15), + _token_value(model, 17), + ] + np.testing.assert_allclose(out.values[0], expected, atol=1e-5) + # Additive against the logits of the text itself, not of a rebuilt string. + np.testing.assert_allclose( + out.base_values[0] + out.values[0].sum(axis=0), model.predict_logits([TEXT])[0], atol=1e-5 + ) + + def test_probability_space_is_additive_against_predict(self): + model = AdditiveModel() + out = NlpShapBackend(model, label_names=model.label_names).run_explainer([TEXT]) + np.testing.assert_allclose(out.base_values[0] + out.values[0].sum(axis=0), model.predict([TEXT])[0], atol=1e-5) + + def test_a_text_with_nothing_to_hide_has_no_words(self): + model = AdditiveModel() + out = NlpShapBackend(model, label_names=model.label_names, output_space="logit").run_explainer([ONLY_SPECIALS]) + assert out.token_strings == [[]] and out.values[0].shape == (0, 2) + np.testing.assert_allclose(out.base_values[0], model.predict_logits([ONLY_SPECIALS])[0], atol=1e-5) + + def test_an_explicit_masker_or_a_slow_tokenizer_takes_the_string_path(self): + import shap + + class _Tok: + mask_token = "[MASK]" + + def __call__(self, s, return_offsets_mapping=False): + return {"input_ids": [0], "offset_mapping": [(0, 0)]} + + model = AdditiveModel() + explicit = NlpShapBackend( + model, label_names=model.label_names, output_space="logit", masker=shap.maskers.Text() + ) + assert explicit.masking == "text" + slow = AdditiveModel(fast=False) + slow.tokenizer = None + with pytest.raises(TypeError, match="tokenizer to mask text with"): + # The string path needs a real tokenizer; reaching that error proves the fallback. + NlpShapBackend(slow, label_names=slow.label_names, output_space="logit") + + +class TestNlpExplainerDefault: + def test_the_default_backend_masks_token_ids_and_the_key_says_so(self): + import shap + + from shapash.explainer.nlp_explainer import NlpExplainer + + model = AdditiveModel() + default = NlpExplainer(model) + assert default.backend.masking == "token_ids" + strings = NlpExplainer( + model, + backend=NlpShapBackend(model, label_names=model.label_names, masker=shap.maskers.Text()), + ) + assert strings.backend.masking == "text" + assert default._compute_key([TEXT]) != strings._compute_key([TEXT]) diff --git a/tests/unit_tests/compute/test_word_layout.py b/tests/unit_tests/compute/test_word_layout.py new file mode 100644 index 00000000..134f92a0 --- /dev/null +++ b/tests/unit_tests/compute/test_word_layout.py @@ -0,0 +1,77 @@ +"""Unit tests for ``shapash.compute.word_layout`` — the words of an encoded text, shared by LIG and SHAP.""" + +import numpy as np +import pytest + +from shapash.compute.word_layout import _group_pieces, word_layout + +TEXT = "un café 😍\n\nok" +# Specials at 0 and 9; "é" split over two byte tokens; a zero-width "▁" before the emoji; a newline +# with a word id of its own (byte-level "Ċ"). +OFFSETS = [(0, 0), (0, 2), (3, 6), (6, 7), (6, 7), (8, 8), (8, 9), (9, 11), (11, 13), (0, 0)] +WORDS = [[1], [2, 3, 4], [5, 6], [7], [8]] + + +class TestWordLayout: + def test_pieces_never_split_a_character(self): + layout = word_layout(TEXT, WORDS, OFFSETS) + # "é"'s two tokens are one piece; the zero-width marker joins the emoji. + assert layout.words == [[[1]], [[2], [3, 4]], [[5, 6]], [[7]], [[8]]] + assert layout.pieces == [[1], [2], [3, 4], [5, 6], [7], [8]] + assert layout.n_pieces == 6 + + def test_spans_are_trimmed_and_whitespace_words_have_none(self): + assert word_layout(TEXT, WORDS, OFFSETS).spans == [(0, 2), (3, 7), (8, 9), None, (11, 13)] + + def test_a_word_sharing_a_character_with_the_previous_one_joins_it(self): + assert _group_pieces([[0], [1, 2]], [(0, 3), (2, 4), (4, 6)]) == [[[0, 1], [2]]] + + def test_positions_past_the_offsets_are_refused(self): + with pytest.raises(ValueError, match="index past"): + word_layout(TEXT, [[99]], OFFSETS) + + +class TestAggregate: + def test_per_piece_sums_words_and_folds_whitespace_words(self): + layout = word_layout(TEXT, WORDS, OFFSETS) + values = np.arange(12, dtype=float).reshape(6, 2) + words, word_values, base, spans = layout.aggregate(values, np.array([100.0, 200.0])) + assert words == ["un", "café", "😍", "ok"] + np.testing.assert_array_equal(word_values, [[0, 1], [6, 8], [6, 7], [10, 11]]) + np.testing.assert_array_equal(base, [108.0, 209.0]) # + the newline's (8, 9) + assert spans == [((0, 2),), ((3, 7),), ((8, 9),), ((11, 13),)] + + def test_per_token_folds_specials_and_keeps_the_total(self): + layout = word_layout(TEXT, WORDS, OFFSETS) + values = np.random.default_rng(0).normal(size=(len(OFFSETS), 3)) + base = np.array([0.5, -0.5, 1.0]) + words, word_values, new_base, _ = layout.aggregate(values, base, per="token") + assert words == ["un", "café", "😍", "ok"] + np.testing.assert_allclose(word_values[1], values[2:5].sum(axis=0)) + np.testing.assert_allclose(new_base, base + values[[0, 7, 9]].sum(axis=0)) # specials + newline + np.testing.assert_allclose(new_base + word_values.sum(axis=0), base + values.sum(axis=0)) + + def test_words_are_shown_as_written(self): + # distilbert-base-uncased rebuilds "cafe" and "[UNK]"; the text says otherwise. + layout = word_layout("Le café 😍", [[1], [2, 3], [4]], [(0, 0), (0, 2), (3, 6), (6, 7), (8, 9), (0, 0)]) + words, values, base, spans = layout.aggregate(np.arange(6.0)[:, None], np.zeros(1), per="token") + assert words == ["Le", "café", "😍"] + assert spans == [((0, 2),), ((3, 7),), ((8, 9),)] + np.testing.assert_allclose(values[:, 0], [1.0, 5.0, 4.0]) + assert base[0] == 0 + 5 + + def test_lone_whitespace_tokens_fold_into_the_baseline(self): + # roberta-base on "Hello world\n\nok": a lone "Ġ" (zero-width) and two "Ċ" get word ids. + text = "Hello world\n\nok" + offsets = [(0, 0), (0, 5), (6, 6), (7, 12), (12, 13), (13, 14), (14, 16), (0, 0)] + layout = word_layout(text, [[1], [2], [3], [4], [5], [6]], offsets) + words, values, base, _ = layout.aggregate(np.arange(1.0, 7.0)[:, None], np.zeros(1)) + assert words == ["Hello", "world", "ok"] + np.testing.assert_allclose(values[:, 0], [1.0, 3.0, 6.0]) + assert base[0] == 2 + 4 + 5 + + def test_no_words(self): + layout = word_layout(" ", [], [(0, 0), (0, 0)]) + words, values, base, spans = layout.aggregate(np.ones((2, 2)), np.zeros(2), per="token") + assert (words, spans, values.shape) == ([], [], (0, 2)) + np.testing.assert_array_equal(base, [2.0, 2.0]) From e868f422eb256456c798040993dd23cb29e506f0 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 14:08:33 +0200 Subject: [PATCH 142/144] docs(demo): describe token-id SHAP masking in serve_nlp --- demo/serve_nlp.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/demo/serve_nlp.py b/demo/serve_nlp.py index 61cf9b4e..7adfd062 100644 --- a/demo/serve_nlp.py +++ b/demo/serve_nlp.py @@ -78,9 +78,10 @@ The sentence-highlight attribution method is selectable (a Captum-backed alternative to the default, see the ``[nlp]`` extra): -* ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (KernelSHAP, default) or ``lig`` - (Captum ``LayerIntegratedGradients``). The two are cached in **separate** subdirectories, so you can - flip between them freely without ``--recompute``. +* ``--attribution {shap,lig}`` — sentence-highlight method: ``shap`` (SHAP's Partition explainer, + masking the model's token ids; default) or ``lig`` (Captum ``LayerIntegratedGradients``). The two + are cached in **separate** subdirectories, so you can flip between them freely without + ``--recompute``. * ``--output-space {probability,logit}`` — the space the contributions live in. ``shap`` explains the softmax probabilities by default and can explain the raw logits instead; ``lig`` is always ``logit`` (passing ``probability`` with it is rejected). The space is part of the cache key, so switching needs @@ -89,9 +90,10 @@ 100-step integration instead of running it through the model in one shot. Lower it (e.g. ``2`` or ``1``) if you hit a CUDA out-of-memory error, especially on memory-hungry architectures like DeBERTa. -``lig`` is also the method most sensitive to *truncation* actually being configured — every HF -classifier here is built through ``HFClassifierModel.from_pretrained(..., max_length="auto")``, which -resolves a safe length even when the checkpoint's tokenizer reports none. +Both methods explain the model's own encoding, so both depend on *truncation* actually being +configured — every HF classifier here is built through +``HFClassifierModel.from_pretrained(..., max_length="auto")``, which resolves a safe length even when +the checkpoint's tokenizer reports none. Both counterfactual generators are offered live in the What-if Lab — ``hotflip`` (gradient-based token substitution) and ``ablation`` (leave-one-out token removal) — and are switched from a @@ -963,9 +965,9 @@ def build_reducer() -> pacmap.PaCMAP: """The dimensionality reducer for the scatter — a *demo* choice, not a library one. ``NlpExplainer.compute_embeddings`` owns everything that must stay consistent (which space the - texts are embedded in, and the caching of the vectors); the reducer is applied on top, because which manifold method suits your data is a modelling decision. - PaCMAP is a good default for text clusters; swap in UMAP, t-SNE, or drop the argument entirely for - the built-in PCA. + texts are embedded in, and the caching of the vectors); the reducer is applied on top, because + which manifold method suits your data is a modelling decision. PaCMAP is a good default for text + clusters; swap in UMAP, t-SNE, or drop the argument entirely for the built-in PCA. """ return pacmap.PaCMAP(n_components=2, n_neighbors=5, MN_ratio=0.5, FP_ratio=2.0) @@ -1059,10 +1061,11 @@ def main() -> None: # exactly what fit's precompute branch keys on.) logger.info( - "attribution=%s (%s) | counterfactual=%s | can_edit=%s | can_counterfactual=%s | can_find_similar=%s" - " | can_detect_label_noise=%s | can_probe_labels=%s", + "attribution=%s (%s, masking=%s) | counterfactual=%s | can_edit=%s | can_counterfactual=%s" + " | can_find_similar=%s | can_detect_label_noise=%s | can_probe_labels=%s", config.attribution, xpl.backend.output_space, + getattr(xpl.backend, "masking", "-"), # SHAP only: token ids, or strings for a pipeline-only model ",".join(name for name, _ in xpl.available_cf_generators()) or "none", xpl.can_edit(), xpl.can_counterfactual(), From 455a224375c7541c9999bae102114ea74be79fd9 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Wed, 7 Oct 2026 14:18:18 +0200 Subject: [PATCH 143/144] fix(nlp): restore the SHAP progress bar on the token-id path --- shapash/backend/nlp_backend.py | 17 +++++++++-------- shapash/backend/nlp_shap_backend.py | 10 ++++++++-- .../backend/test_nlp_shap_token_ids.py | 11 +++++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/shapash/backend/nlp_backend.py b/shapash/backend/nlp_backend.py index 215d6dce..ef6eafa2 100644 --- a/shapash/backend/nlp_backend.py +++ b/shapash/backend/nlp_backend.py @@ -205,15 +205,16 @@ def get_local_contributions( token_spans=None if explain_data.token_spans is None else [explain_data.token_spans[i] for i in subset], ) - def _progress_iter(self, items: list[str]) -> Iterable[str]: - """Wrap ``items`` in a ``tqdm`` bar when the backend's ``show_progress`` is set, else return it unchanged. - - Shared by the backends that loop over texts one at a time (LIG, LIME). Best-effort and - dependency-free: ``tqdm`` is imported with ``errors="ignore"`` so a missing install simply - yields the plain list rather than raising. Read with ``getattr`` so a backend without the - option (or one built via ``object.__new__``) runs silently. + def _progress_iter(self, items: list[str], enabled: bool | None = None) -> Iterable[str]: + """Wrap ``items`` in a ``tqdm`` bar when progress is on, else return it unchanged. + + Shared by the backends that loop over texts one at a time (LIG, LIME, token-id SHAP). Progress + is on when ``enabled`` says so, or, when it is ``None``, when the backend's ``show_progress`` is + set. Best-effort and dependency-free: ``tqdm`` is imported with ``errors="ignore"`` so a missing + install simply yields the plain list rather than raising. ``show_progress`` is read with + ``getattr`` so a backend without the option (or one built via ``object.__new__``) runs silently. """ - if not getattr(self, "show_progress", False): + if not (getattr(self, "show_progress", False) if enabled is None else enabled): return items tqdm_mod = import_optional_module("tqdm", errors="ignore") if tqdm_mod is None: diff --git a/shapash/backend/nlp_shap_backend.py b/shapash/backend/nlp_shap_backend.py index ca922138..bd0f8e6d 100644 --- a/shapash/backend/nlp_shap_backend.py +++ b/shapash/backend/nlp_shap_backend.py @@ -509,6 +509,8 @@ class NlpShapBackend(NlpBackend): under the same ``max_evals`` and deterministically shifts the values, within the error band above. Worth ~1.6x on short texts; 0.98x and 2.2x peak memory on imdb-length ones, where a 512-token forward pass already saturates the GPU. + + ``silent=True`` hides the progress bar over the texts, shown by default as SHAP does. batch_size : int or None, optional Batch size applied to ``model`` when it is a ``transformers`` pipeline that does not already have one (string path only). Default 64. Pass ``None`` to leave the pipeline untouched. Distinct from @@ -674,13 +676,17 @@ def _run_token_ids(self, texts: list[str]) -> NlpContributions: base_values: list[np.ndarray] = [] data: list[list[str]] = [] spans: list[list[tuple[Span, ...]]] = [] - for text in map(str, texts): + # One explainer call per text, so SHAP's own progress bar (on by default, off with + # ``silent=True``) would never show: draw it over the texts instead, and keep each call quiet. + compute_args = {**self.explainer_compute_args, "silent": True} + show_progress = not self.explainer_compute_args.get("silent", False) + for text in self._progress_iter(list(map(str, texts)), enabled=show_progress): encoding = masker.encoding(text) if encoding.layout.n_pieces == 0: # nothing to hide: the text's own score is the baseline values = np.zeros((0, 0)) base = self.model(encoding.input_ids[None, :])[0] else: - explanation = self.explainer([text], **self.explainer_compute_args) + explanation = self.explainer([text], **compute_args) values, base = explanation.values[0], explanation.base_values[0] words, word_values, word_base, word_spans = encoding.layout.aggregate(values, base) data.append(words) diff --git a/tests/unit_tests/backend/test_nlp_shap_token_ids.py b/tests/unit_tests/backend/test_nlp_shap_token_ids.py index f793686b..35e8df29 100644 --- a/tests/unit_tests/backend/test_nlp_shap_token_ids.py +++ b/tests/unit_tests/backend/test_nlp_shap_token_ids.py @@ -251,3 +251,14 @@ def test_the_default_backend_masks_token_ids_and_the_key_says_so(self): ) assert strings.backend.masking == "text" assert default._compute_key([TEXT]) != strings._compute_key([TEXT]) + + +class TestProgress: + def test_a_bar_over_the_texts_unless_silent(self, capsys): + pytest.importorskip("tqdm") + model = AdditiveModel() + NlpShapBackend(model, label_names=model.label_names).run_explainer([TEXT, TEXT]) + assert "nlp_shap attribution" in capsys.readouterr().err + silent = NlpShapBackend(model, label_names=model.label_names, explainer_compute_args={"silent": True}) + silent.run_explainer([TEXT]) + assert capsys.readouterr().err == "" From f3b1e2082ba3d191174d282abff8c44f7b49ab43 Mon Sep 17 00:00:00 2001 From: Milton Minervino <milton.minervino@inria.fr> Date: Mon, 12 Oct 2026 16:01:43 +0200 Subject: [PATCH 144/144] fix: fix mypy errors --- shapash/_optional.py | 19 ++++++++++++++++++- shapash/backend/__init__.py | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/shapash/_optional.py b/shapash/_optional.py index 34a083a0..431a3303 100644 --- a/shapash/_optional.py +++ b/shapash/_optional.py @@ -1,7 +1,24 @@ import importlib import warnings from types import ModuleType -from typing import Literal +from typing import Literal, overload + + +@overload +def import_optional_module( + module_name: str, + extra: str = ..., + errors: Literal["raise"] = ..., +) -> ModuleType: ... + + +@overload +def import_optional_module( + module_name: str, + extra: str = ..., + *, + errors: Literal["warn", "ignore"], +) -> ModuleType | None: ... def import_optional_module( diff --git a/shapash/backend/__init__.py b/shapash/backend/__init__.py index 74a0af2d..248e2459 100644 --- a/shapash/backend/__init__.py +++ b/shapash/backend/__init__.py @@ -11,7 +11,7 @@ from .shap_backend import ShapBackend -def get_backend_cls_from_name(name: str) -> type[BaseBackend]: +def get_backend_cls_from_name(name: str) -> type[Backend]: """Find a backend class by its name. Parameters @@ -21,7 +21,7 @@ def get_backend_cls_from_name(name: str) -> type[BaseBackend]: Returns ------- - type[BaseBackend] + type[Backend] The backend class matching ``name``. Raises