diff --git a/.gitignore b/.gitignore index 94787884..59f94c98 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,7 @@ outputs/ cache/ /.env bin/* -docs/source/** +docs/source/docs_api/** eval_results/* eval_bench/** @@ -71,7 +71,7 @@ assets/demo/* bench_out/* fig_1_images/* tutorials/* -scripts/bench_out/* +scriptsbench_out/* CLAUDE.md .vscode/ quanda_benchmark_tutorial_cache/* @@ -88,4 +88,8 @@ tmp_*/* scripts/paper_tex/** scratch/* slurm/get_logs.sh -scripts/delete_hf.py \ No newline at end of file +scripts/delete_hf.py +scripts/plot_results_LOCAL.py +scripts/**/plot_*_LOCAL.sh +scripts/PLOT_ALL_LOCAL.sh +slurm_LOCAL/* diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29b..00000000 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6b511400..36f02457 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -dilyabareeva@gmail.com. +[dilyabareeva@gmail.com](mailto:dilyabareeva@gmail.com). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28767e0b..85c6567b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,12 @@ Currently, the following markers are available to filter tests: - benchmarks: Benchmark modules - global_ranking: global_ranking modules - self_influence: self_influence methods of explainers +- tasks: task modules +- integration: integration tests +- slow: tests marked as slow (excluded by default; run with `pytest -m slow`) +- production_bench: production benchmark sanity checks, run only when explicitly specified + +The authoritative list lives in `pytest.ini`. Ideally, all contributions should include tests to ensure correctness. @@ -187,7 +193,7 @@ To contribute a new benchmark you generally do not need to override these four c 1. **A subclass of** `Benchmark` under the appropriate `quanda/benchmarks/{downstream_eval,heuristics,ground_truth}/` subdirectory. Subclasses customize behavior via: - `__init__` — accept any benchmark-specific fields beyond what the base `__init__` already stores (`model`, `train_dataset`, `eval_dataset`, `checkpoints`, `checkpoints_load_func`, `device`, `val_dataset`, `use_predictions`). - - `_extra_kwargs_from_config(cls, config, train_dataset, eval_dataset, metadata_dir, load_meta_from_disk)` — extract any subclass-specific kwargs from the YAML and return them as a dict; they get passed into `__init__` by `from_config`. + - `_extra_kwargs_from_config(cls, config, train_dataset, eval_dataset, metadata_dir, load_fresh)` — extract any subclass-specific kwargs from the YAML and return them as a dict; they get passed into `__init__` by `from_config`. - `_compute_and_save_indices(self, config, batch_size)` — only override if your benchmark needs to cache extra metadata on the train pass (filtered eval indices, ranking caches, etc.). - `evaluate(self, explainer_cls, expl_kwargs, batch_size)` — runs the explainer over `eval_dataset`, feeds the attributions to the corresponding `Metric` via `update`/`compute`, and returns the result dict (must contain `"score"`). diff --git a/LICENSE b/LICENSE index d7c82505..50998003 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Dilyara Bareeva, Galip Ümit Yolcu +Copyright (c) 2026 Dilyara Bareeva, Galip Ümit Yolcu, Anna Hedström, Niklas Schmolenski, Thomas Wiegand, Wojciech Samek, Sebastian Lapuschkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ba0c2781..42d024d4 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,14 @@

-![py_versions](https://img.shields.io/badge/python-3.10%20%7C%203.11-3A76A8) -![PyPI - Version](https://img.shields.io/pypi/v/quanda?color=EB9C38) +![py_versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-3A76A8) ![mypy](https://img.shields.io/badge/mypy-checked-7EAF6E) ![ruff](https://img.shields.io/badge/ruff-checked-7D53BA) [![codecov](https://codecov.io/gh/dilyabareeva/quanda/graph/badge.svg?token=6SZS1VISQF)](https://codecov.io/gh/dilyabareeva/quanda) ![PyPI - License](https://img.shields.io/pypi/l/quanda?color=A20E0C) -[![Documentation Status](https://readthedocs.org/projects/quanda/badge/?version=latest)](https://quanda.readthedocs.io/en/latest/?badge=latest) [![arXiv](https://img.shields.io/badge/arXiv-2410.07158-b31b1b.svg)](https://arxiv.org/abs/2410.07158) -**quanda** _is currently under active development. Note the release version to ensure reproducibility of your work. Expect changes to API._ +**quanda** _quanda is under active development. Note the release version to ensure reproducibility of your work. Contributions, bug reports, and feature requests are welcome._ [📑 Shortcut to paper!](https://arxiv.org/pdf/2410.07158) @@ -56,24 +54,22 @@ Although there are various demonstrations of TDA’s potential for interpretabil - **Metrics**: **quanda** provides a set of metrics to evaluate the effectiveness of TDA methods. These metrics are based on the latest research in the field. - **Benchmarking**: **quanda** provides a benchmarking tool to evaluate the performance of TDA methods on a given model, dataset and problem. As many TDA evaluation methods require access to ground truth, our benchmarking tools allow to generate a controlled setting with ground truth, and then compare the performance of different TDA methods on this setting. -### Supported TDA Methods +### Supported TDA Libraries -| Method Name | Repository | Reference | -|----------------------------|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| -| Similarity Influence | [Captum](https://github.com/pytorch/captum/tree/master) | [Caruana et al., 1999](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2232607/) | -| Arnoldi Influence Function | [Captum](https://github.com/pytorch/captum/tree/master) | [Schioppa et al., 2022](https://arxiv.org/abs/2112.03052); [Koh and Liang, 2017](https://proceedings.mlr.press/v70/koh17a.html) | -| TracIn | [Captum](https://github.com/pytorch/captum/tree/master) | [Pruthi et al., 2020](https://proceedings.neurips.cc/paper/2020/hash/e6385d39ec9394f2f3a354d9d2b88eec-Abstract.html) | -| TRAK | [TRAK](https://github.com/MadryLab/trak) | [Park et al., 2023](https://proceedings.mlr.press/v202/park23c.html) | -| Representer Point Selection | [Representer Point Selection](https://github.com/chihkuanyeh/Representer_Point_Selection) | [Yeh et al., 2018](https://proceedings.neurips.cc/paper/2018/hash/8a7129b8f3edd95b7d969dfc2c8e9d9d-Abstract.html) | -| Kronfluence | [Kronfluence](https://github.com/pomonam/kronfluence) | [Grosse et al., 2023](https://arxiv.org/abs/2308.03296) | -| Dattri (Influence Functions: Explicit / CG / LiSSA / DataInf, Arnoldi, EK-FAC, TracInCP, Grad-Dot, Grad-Cos, TRAK) | [Dattri](https://github.com/TRAIS-Lab/dattri) | [Deng et al., 2024](https://arxiv.org/abs/2410.04555) | +| Library | Reference | +|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Captum](https://github.com/pytorch/captum/tree/master) (Similarity Influence, Arnoldi Influence Function, TracIn) | [Caruana et al., 1999](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2232607/); [Schioppa et al., 2022](https://arxiv.org/abs/2112.03052); [Koh and Liang, 2017](https://proceedings.mlr.press/v70/koh17a.html); [Pruthi et al., 2020](https://proceedings.neurips.cc/paper/2020/hash/e6385d39ec9394f2f3a354d9d2b88eec-Abstract.html) | +| [TRAK](https://github.com/MadryLab/trak) (TRAK) | [Park et al., 2023](https://proceedings.mlr.press/v202/park23c.html) | +| [Representer Point Selection](https://github.com/chihkuanyeh/Representer_Point_Selection) (Representer Point Selection) | [Yeh et al., 2018](https://proceedings.neurips.cc/paper/2018/hash/8a7129b8f3edd95b7d969dfc2c8e9d9d-Abstract.html) | +| [Kronfluence](https://github.com/pomonam/kronfluence) (Kronfluence) | [Grosse et al., 2023](https://arxiv.org/abs/2308.03296) | +| [Dattri](https://github.com/TRAIS-Lab/dattri) (Influence Functions: Explicit / CG / LiSSA / DataInf, Arnoldi, EK-FAC, TracInCP, Grad-Dot, Grad-Cos, TRAK) | [Deng et al., 2024](https://arxiv.org/abs/2410.04555) | ### Metrics - **Linear Datamodeling Score** ([Park et al., 2023](https://proceedings.mlr.press/v202/park23c.html)): Measures the correlation between the (grouped) attribution scores and the actual output of models trained on different subsets of the training set. For each subset, the linear datamodeling score compares the actual model output to the sum of attribution scores from the subset using Spearman rank correlation. -- **Identical Class / Identical Subclass** ([Hanawa et al., 2021](https://openreview.net/forum?id=9uvhpyQwzM_)): Measures the proportion of identical classes or subclasses in the top-1 training samples over the test dataset. If the attributions are based on similarity, they are expected to be predictive of the class of the test datapoint, as well as different subclasses under a single label. +- **Class Detection / Subclass Detection** ([Hanawa et al., 2021](https://openreview.net/forum?id=9uvhpyQwzM_)): Measures the proportion of identical classes or subclasses in the top-1 training samples over the test dataset. If the attributions are based on similarity, they are expected to be predictive of the class of the test datapoint, as well as different subclasses under a single label. - **Model Randomization** ([Hanawa et al., 2021](https://openreview.net/forum?id=9uvhpyQwzM_)): Measures the correlation between the original TDA and the TDA of a model with randomized weights. Since the attributions are expected to depend on model parameters, the correlation between original and randomized attributions should be low. @@ -81,13 +77,13 @@ Although there are various demonstrations of TDA’s potential for interpretabil - **Mislabeled Data Detection** ([Koh and Liang, 2017](https://proceedings.mlr.press/v70/koh17a.html)): Computes the proportion of noisy training labels detected as a function of the percentage of inspected training samples. The samples are inspected in order according to their global TDA ranking, which is computed using local attributions. This produces a cumulative mislabeling detection curve. We expect to see a curve that rapidly increases as we check more of the training data, thus we compute the area under this curve -- **Shortcut Detection** ([Yolcu et al., 2024](https://proceedings.mlr.press/v70/koh17a/koh17a.pdf)): Assuming a known [shortcut](https://www.nature.com/articles/s42256-020-00257-z), or [Clever-Hans](https://www.nature.com/articles/s41467-019-08987-4) effect has been identified in the model, this metric evaluates how effectively a TDA method can identify shortcut samples as the most influential in predicting cases with the shortcut artifact. This process is referred to as _Domain Mismatch Debugging_ in the original paper. +- **Shortcut Detection** ([Yolcu et al., 2025](https://openreview.net/pdf?id=qfx81N884A)): Assuming a known [shortcut](https://www.nature.com/articles/s42256-020-00257-z), or [Clever-Hans](https://www.nature.com/articles/s41467-019-08987-4) effect has been identified in the model, this metric evaluates how effectively a TDA method can identify shortcut samples as the most influential in predicting cases with the shortcut artifact. This process is referred to as _Domain Mismatch Debugging_ in the original paper. - **Mixed Datasets** ([Hammoudeh and Lowd, 2022](https://dl.acm.org/doi/abs/10.1145/3548606.3559335)): In a setting where a model has been trained on two datasets: a clean dataset (e.g. CIFAR-10) and an adversarial (e.g. zeros from MNIST), this metric evaluates how well the model ranks the importance (attribution) of adversarial samples compared to clean samples when making predictions on an adversarial example. -- **Mean Reciprocal Rank (MRR)** ([Chang et al., 2024](https://aclanthology.org/2022.findings-emnlp.180)): For fact-tracing settings, measures the mean reciprocal rank of the highest-ranked entailing proponent across fact queries. +- **Mean Reciprocal Rank (MRR)** ([Akyurek et al., 2022](https://aclanthology.org/2022.findings-emnlp.180)): For fact-tracing settings, measures the mean reciprocal rank of the highest-ranked entailing proponent across fact queries. -- **Recall@k** ([Chang et al., 2024](https://aclanthology.org/2022.findings-emnlp.180)): For fact-tracing settings, measures the proportion of facts for which an entailing proponent appears in the top-k retrievals. +- **Recall@k** ([Akyurek et al., 2022](https://aclanthology.org/2022.findings-emnlp.180)): For fact-tracing settings, measures the proportion of facts for which an entailing proponent appears in the top-k retrievals. - **Tail Patch** ([Chang et al., 2024](https://openreview.net/forum?id=gLa96FlWwn)): For fact-tracing settings, measures the incremental change in target-sequence probability after taking a single training step on retrieved proponents. @@ -112,14 +108,14 @@ Although there are various demonstrations of TDA’s potential for interpretabil ### Benchmarks -**quanda** comes with a few pre-computed benchmarks that can be conveniently used for evaluation in a plug-and-play manner. We are planning to significantly expand the number of benchmarks in the future. The following benchmarks are currently available: +**quanda** comes with a few pre-computed benchmarks that can be conveniently used for evaluation in a plug-and-play manner. We are planning to significantly expand the number of benchmarks in the future. The benchmark IDs listed below are to be passed to `load_pretrained`. The following benchmarks are currently available: - + @@ -127,7 +123,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -137,7 +133,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -147,7 +143,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -157,7 +153,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -167,13 +163,13 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + - + @@ -183,7 +179,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -207,7 +203,7 @@ Although there are various demonstrations of TDA’s potential for interpretabil - + @@ -222,13 +218,13 @@ Although there are various demonstrations of TDA’s potential for interpretabil ### Installation -To install the latest release of **quanda** use: +To install **quanda** from a local clone of this repository, run: ```setup -pip install quanda +pip install -e . ``` -**quanda** requires Python 3.10 or 3.11. It is recommended to use a virtual environment to install the package. +**quanda** requires Python 3.10, 3.11 or 3.12. It is recommended to use a virtual environment to install the package. ### Basic Usage @@ -379,6 +375,7 @@ score = subclass_detect.evaluate( explainer_cls=CaptumSimilarity, expl_kwargs=explainer_kwargs, batch_size=batch_size, + max_eval_n=16, )["score"] print(f"Subclass Detection Score: {score}") ``` @@ -455,6 +452,7 @@ score = mislabeling_detection.evaluate( explainer_cls=CaptumSimilarity, expl_kwargs=explainer_kwargs, batch_size=batch_size, + max_eval_n=16, )["score"] print(f"Mislabeling Detection Score: {score}") ``` @@ -542,7 +540,7 @@ We have included a few [tutorials](tutorials) to demonstrate the usage of **quan To install the library with tutorial dependencies, run: ```bash -pip install quanda[tutorials] +pip install -e '.[tutorials]' ``` ## 👩‍💻Contributing diff --git a/config/eval/awa2_resnet50.yaml b/config/eval/awa2_resnet50.yaml index 6a9dac06..83c9bf5c 100644 --- a/config/eval/awa2_resnet50.yaml +++ b/config/eval/awa2_resnet50.yaml @@ -12,7 +12,7 @@ hydra: n_jobs: 1 bench: awa2_class_detection -root_dir: ${cluster_or_local:/data/cluster/users/bareeva/quanda_output_new2,/data2/bareeva/Projects/quanda/cluster_output_new2} +root_dir: bench_out cache_dir: ${root_dir}/eval_bench/awa2 results_dir: ${root_dir}/eval_results/awa2 diff --git a/config/eval/bert_qnli.yaml b/config/eval/bert_qnli.yaml index d366700e..e088ddf6 100644 --- a/config/eval/bert_qnli.yaml +++ b/config/eval/bert_qnli.yaml @@ -12,7 +12,7 @@ hydra: n_jobs: 1 bench: qnli_class_detection -root_dir: ${cluster_or_local:/data/cluster/users/bareeva/quanda_output_new2,/data2/bareeva/Projects/quanda/cluster_output_new2} +root_dir: bench_out cache_dir: ${root_dir}/eval_bench/qnli results_dir: ${root_dir}/eval_results/qnli device: cuda:0 diff --git a/config/eval/cifar_resnet9.yaml b/config/eval/cifar_resnet9.yaml index 3279642c..92f8f5a8 100644 --- a/config/eval/cifar_resnet9.yaml +++ b/config/eval/cifar_resnet9.yaml @@ -12,7 +12,7 @@ hydra: n_jobs: 1 bench: cifar_class_detection -root_dir: ${cluster_or_local:/data/cluster/users/bareeva/quanda_output_new2,/data2/bareeva/Projects/quanda/cluster_output_new2} +root_dir: bench_out cache_dir: ${root_dir}/eval_bench/cifar results_dir: ${root_dir}/eval_results/cifar diff --git a/config/eval/gpt2_trex.yaml b/config/eval/gpt2_trex.yaml index 53f6e348..b0c27203 100644 --- a/config/eval/gpt2_trex.yaml +++ b/config/eval/gpt2_trex.yaml @@ -12,7 +12,7 @@ hydra: n_jobs: 1 bench: gpt2_trex_openwebtext_ft_mrr -root_dir: ${cluster_or_local:/data/cluster/users/bareeva/quanda_output_new2,/data2/bareeva/Projects/quanda/cluster_output_new2} +root_dir: bench_out cache_dir: ${root_dir}/eval_bench/gpt2_trex results_dir: ${root_dir}/eval_results/gpt2_trex device: cuda:0 diff --git a/config/eval/mnist_lenet.yaml b/config/eval/mnist_lenet.yaml index c2a8890e..914dbddd 100644 --- a/config/eval/mnist_lenet.yaml +++ b/config/eval/mnist_lenet.yaml @@ -12,7 +12,7 @@ hydra: n_jobs: 1 bench: mnist_class_detection -root_dir: ${cluster_or_local:/data/cluster/users/bareeva/quanda_output_new2,/data2/bareeva/Projects/quanda/cluster_output_new2} +root_dir: bench_out cache_dir: ${root_dir}/eval_bench/mnist results_dir: ${root_dir}/eval_results/mnist diff --git a/docs/Makefile b/docs/Makefile index 9ef23a98..901d37c4 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -22,6 +22,7 @@ help: rst: @sphinx-apidoc -o source/docs_api ../quanda --module-first --separate --force + @sed -i '/^\.\. automodule:: quanda$$/,/^$$/ { /:members:/d; /:undoc-members:/d; }' source/docs_api/quanda.rst clean: diff --git a/docs/source/_static/components-darkmode.png b/docs/source/_static/components-darkmode.png index 497e25b1..0d2a03d1 100644 Binary files a/docs/source/_static/components-darkmode.png and b/docs/source/_static/components-darkmode.png differ diff --git a/docs/source/background.rst b/docs/source/background.rst index 9ad6e0e8..beb4c88f 100644 --- a/docs/source/background.rst +++ b/docs/source/background.rst @@ -1,5 +1,5 @@ What is Training Data Attribution? -========== +================================== The interpretability of neural network decisions is an active area of research which has seen a variety of approaches over time. Most of the initial focus was on feature attribution methods, which highlight features in the input space that are responsible for a specific prediction (`Simonyan et al., 2014 `_; `Bach et al., 2015 `_; `Lundberg and Lee, 2017 `_). These methods were often criticized for being unreliable and difficult to understand (`Adebayo et al., 2018 `_; `Ghorbani et al., 2019 `_). In response, researchers explored new directions, such as concept-based (`Poeta et al., 2023 `_) and mechanistic interpretability (`Bereska and Gavves `_) methods. Recently, **Training Data Attribution** (TDA) has gained attention as a promising approach for enhancing the interpretability of neural networks. diff --git a/docs/source/conf.py b/docs/source/conf.py index 4ecb87af..fe29c1a3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ project = "quanda" copyright = f"{str(datetime.utcnow().year)}, Dilyara Bareeva, Galip Ümit Yolcu" author = "Dilyara Bareeva, Galip Ümit Yolcu" -release = "05.10.2024" +release = "05.05.2026" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -30,6 +30,9 @@ ] source_suffix = [".rst", ".md"] autosummary_generate = True +numpydoc_class_members_toctree = False +numpydoc_show_class_members = False +suppress_warnings = ["ref.python"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] rst_epilog = """ .. |quanda| raw:: html diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index a3d7cd02..1d0519bb 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -10,7 +10,8 @@ In this guide, you will get a summary of the main components of If you have any questions regarding the codebase, please `open an issue `__ or write us -at dilyabareeva@gmail.com or galip.uemit.yolcu@hhi.fraunhofer.de. +at `dilyabareeva@gmail.com `__ or +`galip.uemit.yolcu@hhi.fraunhofer.de `__. Table of Contents ----------------- @@ -122,6 +123,12 @@ Currently, the following markers are available to filter tests: - benchmarks: Benchmark modules - global_ranking: global_ranking modules - self_influence: self_influence methods of explainers +- tasks: task modules +- integration: integration tests +- slow: tests marked as slow (excluded by default; run with ``pytest -m slow``) +- production_bench: production benchmark sanity checks, run only when explicitly specified + +The authoritative list lives in ``pytest.ini``. Ideally, all contributions should include tests to ensure correctness. @@ -330,7 +337,7 @@ these four classmethods . What you should provide is: ``checkpoints_load_func``, ``device``, ``val_dataset``, ``use_predictions``). - ``_extra_kwargs_from_config(cls, config, train_dataset, - eval_dataset, metadata_dir, load_meta_from_disk)`` — extract any + eval_dataset, metadata_dir, load_fresh)`` — extract any subclass-specific kwargs from the YAML and return them as a dict; they get passed into ``__init__`` by ``from_config``. - ``_compute_and_save_indices(self, config, batch_size)`` — only diff --git a/docs/source/explainers.rst b/docs/source/explainers.rst new file mode 100644 index 00000000..23218f87 --- /dev/null +++ b/docs/source/explainers.rst @@ -0,0 +1,154 @@ +Explainer Wrappers +================== + +|quanda| ships wrappers around several existing TDA libraries, exposing them +through a single :doc:`Explainer ` interface. +The tables below list every wrapper class and cite the paper that introduced +the underlying method. All wrapper sources live under +``quanda/explainers/wrappers/``. + +All wrappers can be imported directly from ``quanda.explainers.wrappers``, +for example: + +.. code:: python + + from quanda.explainers.wrappers import ( + CaptumSimilarity, + TRAK, + Kronfluence, + RepresenterPoints, + DattriIFExplicit, + ) + +Captum +------ +Wrappers around the influence methods provided by `Captum +`_. Source: +``quanda/explainers/wrappers/captum_influence.py``. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Wrapper + - Reference + * - ``CaptumSimilarity`` + - Similarity between test and training samples in the representation + space of a chosen layer. See `Captum's SimilarityInfluence docs + `__. + * - ``CaptumArnoldi`` + - Schioppa et al., 2022. *Scaling Up Influence Functions.* + `arXiv:2112.03052 `__ + * - ``CaptumTracInCP`` + - Pruthi et al., 2020. *Estimating Training Data Influence by Tracing + Gradient Descent.* `NeurIPS 2020 + `__ + * - ``CaptumTracInCPFast`` + - Pruthi et al., 2020. *Estimating Training Data Influence by Tracing + Gradient Descent.* `NeurIPS 2020 + `__ + * - ``CaptumTracInCPFastRandProj`` + - Pruthi et al., 2020. *Estimating Training Data Influence by Tracing + Gradient Descent.* `NeurIPS 2020 + `__ + +Representer Point Selection +--------------------------- +Source: ``quanda/explainers/wrappers/representer_points.py``. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Wrapper + - Reference + * - ``RepresenterPoints`` + - Yeh et al., 2018. *Representer Point Selection for Explaining Deep + Neural Networks.* `NeurIPS 2018 + `__ + — original implementation: `chihkuanyeh/Representer_Point_Selection + `__ + +TRAK +---- +Source: ``quanda/explainers/wrappers/trak_wrapper.py``. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Wrapper + - Reference + * - ``TRAK`` + - Park et al., 2023. *TRAK: Attributing Model Behavior at Scale.* + `ICML 2023 `__ + — original implementation: `MadryLab/trak + `__ + +Kronfluence +----------- +Source: ``quanda/explainers/wrappers/kronfluence.py``. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Wrapper + - Reference + * - ``Kronfluence (incl. EK-FAC)`` + - Grosse et al., 2023. *Studying Large Language Model Generalization with + Influence Functions.* `arXiv:2308.03296 + `__ + — original implementation: `pomonam/kronfluence + `__ + +Dattri +------ +Wrappers around the unified TDA family provided by `Dattri +`_ (Deng et al., 2024, +`arXiv:2410.04555 `__). Source: +``quanda/explainers/wrappers/dattri_influence.py``. + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Wrapper + - Reference + * - ``DattriIFExplicit`` + - Koh and Liang, 2017. *Understanding Black-box Predictions via Influence + Functions.* `ICML 2017 + `__ + * - ``DattriIFCG`` + - Koh and Liang, 2017 (conjugate-gradient solver). `ICML 2017 + `__ + * - ``DattriIFLiSSA`` + - Agarwal et al., 2017. *Second-Order Stochastic Optimization for Machine + Learning in Linear Time.* `JMLR 2017 + `__ + * - ``DattriIFDataInf`` + - Kwon et al., 2024. *DataInf: Efficiently Estimating Data Influence in + LoRA-tuned LLMs and Diffusion Models.* `ICLR 2024 + `__ + * - ``DattriArnoldi`` + - Schioppa et al., 2022. *Scaling Up Influence Functions.* + `arXiv:2112.03052 `__ + * - ``DattriEKFAC`` + - Grosse et al., 2023. *Studying Large Language Model Generalization with + Influence Functions.* `arXiv:2308.03296 + `__ + * - ``DattriTracInCP`` + - Pruthi et al., 2020. *Estimating Training Data Influence by Tracing + Gradient Descent.* `NeurIPS 2020 + `__ + * - ``DattriGradDot`` + - Charpiat et al., 2019. *Input Similarity from the Neural Network + Perspective.* `NeurIPS 2019 + `__ + * - ``DattriGradCos`` + - Charpiat et al., 2019. *Input Similarity from the Neural Network + Perspective.* `NeurIPS 2019 + `__ + * - ``DattriTRAK`` + - Park et al., 2023. *TRAK: Attributing Model Behavior at Scale.* + `ICML 2023 `__ diff --git a/docs/source/how_to_evaluate.rst b/docs/source/how_to_evaluate.rst index b4ccf5ce..eae92a28 100644 --- a/docs/source/how_to_evaluate.rst +++ b/docs/source/how_to_evaluate.rst @@ -1,5 +1,5 @@ How to Assess the Quality of Attributions? -========== +========================================== Evaluation of interpretability approaches is a challenging task, as it is often difficult to define a ground truth for interpretability. Although there are various demonstrations of TDA’s potential for interpretability and practical applications, the critical question of how TDA methods should be effectively evaluated remains open. While methods based on estimating counterfactual retraining effects have a well-defined ground truth, this ground truth is computationally demanding and is not feasibly computable for large scale experiments. To address these shortcomings, several approaches have been proposed by the community, which can be categorized into three groups: @@ -17,7 +17,7 @@ As some of the methods are designed to approximate LOO effects, ground truth can
Downstream Task Evaluators -To remedy the challenges associated with ground truth evaluation, the literature proposes to assess the utility of a TDA method within the context of an end-task. The most commonly used evaluation criteria is Mislabeling Detection (`Koh and Liang, 2017 `_; `Yeh et al., 2018 `_; `Pruthi et al., 2020 `_) which compares different TDA methods in terms of their usefulness for detecting mislabeled samples after training the network on a dataset of which the labels are deliberately poisoned. Other examples could be detecting backdoor attacks (`Karthikeyan et al., 2021 `_; `Yolcu et al., 2024 `_) or predicting the model decision from its attributions (`Hanawa et al., 2021 `_). +To remedy the challenges associated with ground truth evaluation, the literature proposes to assess the utility of a TDA method within the context of an end-task. The most commonly used evaluation criteria is Mislabeling Detection (`Koh and Liang, 2017 `_; `Yeh et al., 2018 `_; `Pruthi et al., 2020 `_) which compares different TDA methods in terms of their usefulness for detecting mislabeled samples after training the network on a dataset of which the labels are deliberately poisoned. Other examples could be detecting backdoor attacks (`Karthikeyan et al., 2021 `_; `Yolcu et al., 2025 `_) or predicting the model decision from its attributions (`Hanawa et al., 2021 `_). .. raw:: html diff --git a/docs/source/index.rst b/docs/source/index.rst index e3a8b657..eb7ef146 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,7 +4,7 @@ Welcome to |quanda|'s documentation! |quanda| is a toolkit for **quan**\ titative evaluation of **d**\ ata **a**\ ttribution methods in **PyTorch**. .. note:: - |quanda| is currently in development. We are actively working on expanding the library and improving the documentation. If you have any questions, please `open an issue `_ or write us at dilyabareeva@gmail.com or galip.uemit.yolcu@hhi.fraunhofer.de. + |quanda| is under active development. Note the release version to ensure reproducibility of your work. Contributions, bug reports, and feature requests are welcome. .. figure:: _static/fig_1_source.png :alt: Figure 1 @@ -73,49 +73,29 @@ Here we list the main components of |quanda| along with basic explanations of th
Benchmarks -Note that many metrics require training models in controlled settings, e.g. with mislabeled samples that are known. This means that the corresponding :doc:`Metric ` objects can only be used if the user has prepared this controlled setup. Furthermore, :doc:`Metric ` objects require generating the attributions beforehand. |quanda| provides a benchmarking tool to evaluate the performance of TDA methods on a given model, dataset and problem. For each :doc:`Metric ` object, |quanda| provides a :doc:`Benchmark ` object. The :doc:`Benchmark ` objects handle the creation of the controlled setup, training the model, generating the attributions and evaluating them using the corresponding :doc:`Metric ` object, if needed. Finally, we provide precomputed benchmarks, which can be used by initializing the object with the ``load`` method. These precomputed benchmarks allow the user to skip the creation of the controlled setup to directly start the evaluation process, while providing a standard benchmark for practitioners and researchers to compare their methods with. +Note that many metrics require training models in controlled settings, e.g. with mislabeled samples that are known. This means that the corresponding :doc:`Metric ` objects can only be used if the user has prepared this controlled setup. Furthermore, :doc:`Metric ` objects require generating the attributions beforehand. |quanda| provides a benchmarking tool to evaluate the performance of TDA methods on a given model, dataset and problem. For each :doc:`Metric ` object, |quanda| provides a :doc:`Benchmark ` object. The :doc:`Benchmark ` objects handle the creation of the controlled setup, training the model, generating the attributions and evaluating them using the corresponding :doc:`Metric ` object, if needed. Finally, we provide precomputed benchmarks, which can be used by initializing the object with the ``load_pretrained`` method. These precomputed benchmarks allow the user to skip the creation of the controlled setup to directly start the evaluation process, while providing a standard benchmark for practitioners and researchers to compare their methods with. .. raw:: html
-Supported TDA Methods ---------------------- +Supported TDA Libraries +----------------------- .. list-table:: :header-rows: 1 - * - Method - - Repository + * - Library - Reference - - Description - * - Similarity Influence - - `Captum `_ - - `Caruana et al., 1999 `_ - - Ranks the training samples based on their similarity to the test sample - * - Arnoldi Influence Functions - - `Captum `_ - - `Schioppa et al., 2022 `_ - - Estimates LOO effects, following (`Koh and Liang, 2017 `_) - * - TracIn - - `Captum `_ - - `Pruthi et al., 2020 `_ - - Tracks the contribution of training points in the loss reduction throughout training, via a linear approximation - * - Representer Point Selection - - `Representer Point Selection `_ + * - `Captum `_ (Similarity Influence, Arnoldi Influence Functions, TracIn) + - `Caruana et al., 1999 `_; `Schioppa et al., 2022 `_; `Koh and Liang, 2017 `_; `Pruthi et al., 2020 `_ + * - `Representer Point Selection `_ (Representer Point Selection) - `Yeh et al., 2018 `_ - - Trains the model with L2 regularization on the final layer, which produces an interpretable surrogate model - * - TRAK - - `TRAK `_ + * - `TRAK `_ (TRAK) - `Park et al., 2023 `_ - - Uses an empirical Neural Tangent Kernel surrogate model for which a theoretical TDA formula exists - * - Kronfluence - - `Kronfluence `_ + * - `Kronfluence `_ (Kronfluence) - `Grosse et al., 2023 `_ - - Estimates LOO effects with EK-FAC-based approximations to the inverse Hessian - * - Dattri (Influence Functions: Explicit / CG / LiSSA / DataInf, Arnoldi, EK-FAC, TracInCP, Grad-Dot, Grad-Cos, TRAK) - - `Dattri `_ + * - `Dattri `_ (Influence Functions: Explicit / CG / LiSSA / DataInf, Arnoldi, EK-FAC, TracInCP, Grad-Dot, Grad-Cos, TRAK) - `Deng et al., 2024 `_ - - Provides a unified family of TDA methods (influence functions, TracIn, gradient similarity, TRAK) via the ``Dattri`` library. Evaluation Metrics ------------------ @@ -132,12 +112,12 @@ In this section, we list the evaluation criteria that are currently available in - `Park et al., 2023 `_ - Measures the correlation between the (grouped) attribution scores and the actual output of models trained on different subsets of the training set. For each subset, the linear datamodeling score compares the actual model output with the sum of attribution scores from the subset using Spearman rank correlation. - Ground Truth - * - Identical Class / Identical Subclass + * - Class Detection / Subclass Detection - `Hanawa et al., 2021 `_ - Measures the proportion of identical classes or subclasses in the top-1 training samples over the test dataset. If the attributions are based on similarity, they are expected to be predictive of the class of the test datapoint, as well as different subclasses under a single label. - Downstream Task Evaluator * - Shortcut Detection - - `Yolcu et al., 2024 `_ + - `Yolcu et al., 2025 `_ - Assuming a known `shortcut `_, or `Clever-Hans `_ effect has been identified in the model, this metric evaluates how effectively a TDA method can identify shortcut samples as the most influential in predicting cases with the shortcut artifact. This process is referred to as **Domain Mismatch Debugging** in the original paper. - Downstream Task Evaluator * - Mislabeled Data Detection @@ -157,21 +137,64 @@ In this section, we list the evaluation criteria that are currently available in - In a setting where a model has been trained on two datasets: a clean dataset (e.g. CIFAR-10) and an adversarial (e.g. zeros from MNIST), this metric evaluates how well the model ranks the importance (attribution) of adversarial samples compared to clean samples when making predictions on an adversarial example. - Heuristic * - Mean Reciprocal Rank (MRR) - - `Chang et al., 2024 `_ + - `Akyurek et al., 2022 `_ - For fact-tracing settings, measures the mean reciprocal rank of the highest-ranked entailing proponent across fact queries. - Downstream Task Evaluator * - Recall@k - - `Chang et al., 2024 `_ + - `Akyurek et al., 2022 `_ - For fact-tracing settings, measures the proportion of facts for which an entailing proponent appears in the top-k retrievals. - Downstream Task Evaluator * - Tail Patch - - `Chang et al., 2024 `_ + - `Chang et al., 2025 `_ - For fact-tracing settings, measures the incremental change in target-sequence probability after taking a single training step on retrieved proponents. - Downstream Task Evaluator +Metric Interpretation Guideline ++++++++++++++++++++++++++++++++ + +.. list-table:: + :header-rows: 1 + + * - Metric + - Output range + - Better + * - `ClassDetection `_ + - ``[0, 1]`` + - higher + * - `SubclassDetection `_ + - ``[0, 1]`` + - higher + * - `MislabelingDetection `_ + - ``[0, 1]`` + - higher + * - `ShortcutDetection `_ + - ``[0, 1]`` + - higher + * - `MixedDatasets `_ + - ``[0, 1]`` + - higher + * - `TopKCardinality `_ + - ``[0, 1]`` + - higher + * - `ModelRandomization `_ + - ``[-1, 1]`` + - closer to 0 + * - `LinearDatamodelingScore `_ + - ``[-1, 1]`` + - higher + * - `MRR `_ + - ``[0, 1]`` + - higher + * - `RecallAtK `_ + - ``[0, 1]`` + - higher + * - `TailPatch `_ + - ``[-1, 1]`` + - higher + Benchmarks ---------- -|quanda| comes with a number of pre-computed benchmarks that can be conveniently used for evaluation in a plug-and-play manner. We are planning to significantly expand the number of benchmarks in the future. Currently available benchmarks span vision (MNIST / LeNet, CIFAR-10 / ResNet-9, AWA2 / ResNet-50), text classification (QNLI / BERT), and causal language modeling (T-REx / GPT-2 fine-tuned on OpenWebText). +|quanda| comes with a number of pre-computed benchmarks that can be conveniently used for evaluation in a plug-and-play manner. We are planning to significantly expand the number of benchmarks in the future. Currently available benchmarks span vision (MNIST / LeNet, CIFAR-10 / ResNet-9, AWA2 / ResNet-50), text classification (QNLI / BERT), and causal language modeling (T-REx / GPT-2 fine-tuned on OpenWebText). The benchmark IDs listed below are to be passed to ``load_pretrained``. .. list-table:: :header-rows: 1 @@ -179,51 +202,91 @@ Benchmarks * - Metric - Type - Modality - - Benchmarks (Dataset / Model) + - Benchmark IDs (Dataset / Model) * - `TopKCardinalityMetric `_ - Heuristic - - Vision / Text - - mnist_top_k_cardinality, cifar_top_k_cardinality, awa2_top_k_cardinality, qnli_top_k_cardinality + - Vision + - | mnist_top_k_cardinality (MNIST / LeNet) + | cifar_top_k_cardinality (CIFAR-10 / ResNet-9) + | awa2_top_k_cardinality (AWA2 / ResNet-50) + * - + - + - Text + - qnli_top_k_cardinality (QNLI / BERT) * - `ModelRandomizationMetric `_ - Heuristic - - Vision / Text - - mnist_model_randomization, cifar_model_randomization, awa2_model_randomization, qnli_model_randomization + - Vision + - | mnist_model_randomization (MNIST / LeNet) + | cifar_model_randomization (CIFAR-10 / ResNet-9) + | awa2_model_randomization (AWA2 / ResNet-50) + * - + - + - Text + - qnli_model_randomization (QNLI / BERT) * - `MixedDatasetsMetric `_ - Heuristic - - Vision / Text - - mnist_mixed_datasets, cifar_mixed_datasets, awa2_mixed_datasets, qnli_mixed_datasets + - Vision + - | mnist_mixed_datasets (MNIST / LeNet) + | cifar_mixed_datasets (CIFAR-10 / ResNet-9) + | awa2_mixed_datasets (AWA2 / ResNet-50) + * - + - + - Text + - qnli_mixed_datasets (QNLI / BERT) * - `ClassDetectionMetric `_ - Downstream Task Evaluator - - Vision / Text - - mnist_class_detection, cifar_class_detection, awa2_class_detection, qnli_class_detection + - Vision + - | mnist_class_detection (MNIST / LeNet) + | cifar_class_detection (CIFAR-10 / ResNet-9) + | awa2_class_detection (AWA2 / ResNet-50) + * - + - + - Text + - qnli_class_detection (QNLI / BERT) * - `SubclassDetectionMetric `_ - Downstream Task Evaluator - Vision - - mnist_subclass_detection, cifar_subclass_detection, awa2_subclass_detection + - | mnist_subclass_detection (MNIST / LeNet) + | cifar_subclass_detection (CIFAR-10 / ResNet-9) + | awa2_subclass_detection (AWA2 / ResNet-50) * - `MislabelingDetectionMetric `_ - Downstream Task Evaluator - - Vision / Text - - mnist_mislabeling_detection, cifar_mislabeling_detection, awa2_mislabeling_detection, qnli_mislabeling_detection + - Vision + - | mnist_mislabeling_detection (MNIST / LeNet) + | cifar_mislabeling_detection (CIFAR-10 / ResNet-9) + | awa2_mislabeling_detection (AWA2 / ResNet-50) + * - + - + - Text + - qnli_mislabeling_detection (QNLI / BERT) * - `ShortcutDetectionMetric `_ - Downstream Task Evaluator - Vision - - mnist_shortcut_detection, cifar_shortcut_detection, awa2_shortcut_detection + - | mnist_shortcut_detection (MNIST / LeNet) + | cifar_shortcut_detection (CIFAR-10 / ResNet-9) + | awa2_shortcut_detection (AWA2 / ResNet-50) * - `MRRMetric `_ - Downstream Task Evaluator - Causal LM - - gpt2_trex_openwebtext_ft_mrr + - gpt2_trex_openwebtext_ft_mrr (T-REx / GPT-2 fine-tuned on OpenWebText) * - `RecallAtKMetric `_ - Downstream Task Evaluator - Causal LM - - gpt2_trex_openwebtext_ft_recall_at_k + - gpt2_trex_openwebtext_ft_recall_at_k (T-REx / GPT-2 fine-tuned on OpenWebText) * - `TailPatchMetric `_ - Downstream Task Evaluator - Causal LM - - gpt2_trex_openwebtext_ft_tail_patch + - gpt2_trex_openwebtext_ft_tail_patch (T-REx / GPT-2 fine-tuned on OpenWebText) * - `LinearDatamodelingMetric `_ - Ground Truth - - Vision / Text - - mnist_linear_datamodeling, cifar_linear_datamodeling, awa2_linear_datamodeling, qnli_linear_datamodeling + - Vision + - | mnist_linear_datamodeling (MNIST / LeNet) + | cifar_linear_datamodeling (CIFAR-10 / ResNet-9) + | awa2_linear_datamodeling (AWA2 / ResNet-50) + * - + - + - Text + - qnli_linear_datamodeling (QNLI / BERT) Citation -------- @@ -248,6 +311,7 @@ If you are using |quanda| for your scientific research, please also make sure to :maxdepth: 2 quickstart + explainers tutorials .. toctree:: diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 44b44326..3a313a45 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -4,13 +4,13 @@ Quickstart Installation ------------ -To install the latest release of |quanda|, use the following command in your terminal: +To install |quanda| from a local clone of the repository, use the following command in your terminal: .. code-block:: console - (.venv) $ pip install quanda + (.venv) $ pip install -e . -|quanda| requires Python 3.10 or 3.11. It is recommended to use a virtual environment to install the package. +|quanda| requires Python 3.10, 3.11 or 3.12. It is recommended to use a virtual environment to install the package. .. note:: In the examples that follow, we will demonstrate the generation of explanations generated using ``SimilarityInfluence`` data attributor from ``Captum``. diff --git a/docs/source/tutorial_pages/benchmarks.rst b/docs/source/tutorial_pages/benchmarks.rst index b9ea56be..9aa692de 100644 --- a/docs/source/tutorial_pages/benchmarks.rst +++ b/docs/source/tutorial_pages/benchmarks.rst @@ -3,6 +3,10 @@ Benchmarks Tutorial Welcome to the benchmark tutorial of |quanda|. This tutorial walks you through the process of using the benchmarking tools in |quanda| to evaluate a data attribution method. This tutorial covers 3 different examples of benchmarks. It includes all different initialization schemes: training a benchmark from scratch using ``train()``, loading a benchmark from a YAML configuration using ``from_config()``, and downloading a precomputed benchmark using ``load_pretrained()``. +.. seealso:: + + The :doc:`Linear Datamodeling Score (LDS) ` page covers caveats specific to the most expensive benchmark in |quanda|, including how to precompute and reuse counterfactual subset logits across explainers. + To install the library with tutorial dependencies, run: .. code:: bash @@ -122,6 +126,12 @@ The YAML configuration file specifies all required components: The class grouping can be set to ``random`` in the configuration to randomly assign classes into superclasses, which is the approach we will take in this tutorial. +.. important:: + + The configuration must specify ``bench_save_dir``: the directory under which the trained benchmark (model checkpoints and metadata) is saved. There should be enough disk space to save the main model and M subset models for LDS (if applicable) under this directory. If training multiple benchmarks from scratch, make sure to set different ``bench_save_dir`` for each to avoid overwriting. + + If multiple training jobs must share a ``bench_save_dir`` (e.g. concurrent runs of the same benchmark), pass ``use_pid=True`` to ``train`` (or ``train_and_push_to_hub``) to suffix checkpoint and metadata directories with the current process id and avoid clobbering each other's outputs. By default ``use_pid=False``. + .. note:: Please note that calling ``SubclassDetection.train`` will initiate model training, therefore it will potentially take a long time. @@ -151,3 +161,20 @@ Now that we have trained the model on the MNIST dataset with grouped classes as :start-after: # START15 :end-before: # END15 :dedent: + +Caching and Sharing Explanations +-------------------------------- +Computing attributions is typically the most expensive step in TDA evaluation. To avoid recomputing them every time, every ``Benchmark`` exposes an ``explain`` classmethod that precomputes attributions over the evaluation dataset and writes them to disk together with an ``explanations_config.yaml`` describing how they were generated. A subsequent call to ``benchmark.evaluate(..., cache_dir=, use_cached_expl=True)`` reads from that cache instead of recomputing. + +The cache directory is keyed on: + +- the benchmark id (or its ``explanations_group``, see below), +- the explainer class name, +- a stable hash of ``expl_kwargs``, +- the eval-subsample parameters ``max_eval_n`` and ``eval_seed``. + +Changing any of these produces a different cache key, so cached explanations stay coupled to the exact setup they were computed on. + +**Sharing across benchmarks.** Several benchmarks (e.g. ``ClassDetection`` and ``LinearDatamodelingMetric``) can be defined on top of the same model + train/eval datasets. Setting a common ``explanations_group`` in their YAML configs replaces the per-benchmark id segment of the cache key with a shared one, so a single attribution pass can drive multiple evaluations. Only opt in when the grouped benchmarks really share those inputs — mismatched inputs under a shared group will silently corrupt results. + +See :doc:`lds` for the analogous mechanism that caches counterfactual subset logits across explainers in the LDS benchmark. diff --git a/docs/source/tutorial_pages/lds.rst b/docs/source/tutorial_pages/lds.rst new file mode 100644 index 00000000..2f767136 --- /dev/null +++ b/docs/source/tutorial_pages/lds.rst @@ -0,0 +1,54 @@ +Linear Datamodeling Score (LDS) +=============================== + +The :doc:`LinearDatamodelingMetric <../docs_api/quanda.metrics.ground_truth.linear_datamodeling>` is a ground-truth metric (Park et al., 2023): it measures how well an attribution method predicts the actual change in model output when retraining on different subsets of the training data. The corresponding :doc:`LinearDatamodeling <../docs_api/quanda.benchmarks.ground_truth.linear_datamodeling>` benchmark wraps this end-to-end — including training the counterfactual subset models — and is the most expensive benchmark in |quanda|. This page collects the practical caveats you should know before running it. + +Caveats +------- + +**1. ``load_pretrained`` downloads M subset checkpoints.** LDS retrains the model on ``M`` random subsets of the training data (default ``m=100`` in the published configs). ``LinearDatamodeling.load_pretrained(...)`` therefore pulls down ``M+1`` checkpoints from the Hugging Face Hub (the main model plus every subset). For the ``mnist_linear_datamodeling`` / ``cifar_linear_datamodeling`` / ``awa2_linear_datamodeling`` / ``qnli_linear_datamodeling`` benchmarks this is 100 subset checkpoints; expect a sizeable download and disk footprint on first use. + +**2. Counterfactual subset logits should be precomputed once and reused.** During evaluation, each subset model is run over the eval dataset to produce *counterfactual logits*, which are then correlated with the explainer's group attributions. These per-subset logits depend only on the subset checkpoints and the eval subsample — they do **not** depend on the explainer being evaluated. Recomputing them inside every ``evaluate(...)`` call is wasteful, so :class:`LinearDatamodeling` exposes ``cache_subset_logits`` that runs the M forward passes once and writes them to disk; subsequent ``evaluate(...)`` calls pass that directory via ``subset_logits_dir=...`` and skip the recomputation. + +To parallelize computations, you can use ``cache_subset_logits_per_idx`` for a single subset index, so the M forward passes can be split across workers. + +**3. Training subset models from scratch.** Calling ``LinearDatamodeling.train(config)`` trains the main model and then iterates through all ``M`` subset models sequentially in the same process — fine for small benchmarks, but you will usually want to parallelize for larger runs. + +The recommended pattern is to split training into two phases. ``train(config, skip_subsets=True)`` trains and persists the main model and writes the metadata (split ids, plus the subset-id file whose name is set by the ``subset_ids`` field in the config — ``lds_subsets.yaml`` in the shipped configs) but does **not** train any subset models. Then ``train_subset(config, idx=...)`` rebuilds the benchmark from that metadata, trains a single subset ``idx``, and persists its checkpoint. ``train_subset`` is designed to be the unit of work for an array job — call it once per worker / GPU / SLURM task and the M subsets train in parallel rather than back-to-back. The passed config must contain a ``bench_save_dir`` field, which is used to save the main model, the subset checkpoints, and the metadata that links them together. + +.. literalinclude:: ../../../tests/integration/test_benchmark_integration.py + :language: python + :start-after: # START19 + :end-before: # END19 + :dedent: + +.. literalinclude:: ../../../tests/integration/test_benchmark_integration.py + :language: python + :start-after: # START20 + :end-before: # END20 + :dedent: + +The companion ``scripts/train_lds_subset.py`` wraps ``train_subset`` as a CLI entry point that takes a single ``--idx``, intended for SLURM array jobs. + +Both methods accept either a config dict, a registered ``bench_id``, or a path to a benchmark YAML. + +Precomputing and reusing subset logits +-------------------------------------- + +The example below loads the published ``mnist_linear_datamodeling`` benchmark via ``load_pretrained``, then populates the subset-logits and explanations caches before calling ``evaluate``. The same ``subset_logits_dir`` can be passed to every ``evaluate(...)`` call regardless of explainer; the same explanations ``cache_dir`` + ``use_cached_expl=True`` can be reused whenever the explainer / ``expl_kwargs`` / eval-subsample match. + +.. literalinclude:: ../../../tests/integration/test_benchmark_integration.py + :language: python + :start-after: # START17 + :end-before: # END17 + :dedent: + +.. literalinclude:: ../../../tests/integration/test_benchmark_integration.py + :language: python + :start-after: # START18 + :end-before: # END18 + :dedent: + +Likewise, call ``explain`` once per explainer and reuse the returned cache directory across re-evaluations or across sibling benchmarks that share the same model + train/eval datasets via a common ``explanations_group`` in the YAML. + +All of the classmethods on this page (``load_pretrained``, ``train``, ``train_subset``, ``explain``, ``cache_subset_logits``, ``cache_subset_logits_per_idx``) accept ``bench_id`` / ``config`` either as a registered string (e.g. ``"mnist_linear_datamodeling"``), a path to a benchmark YAML, or a config dict. diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst index 0fcd3fb2..48ad7a7e 100644 --- a/docs/source/tutorials.rst +++ b/docs/source/tutorials.rst @@ -11,9 +11,11 @@ The tutorials currently included in |quanda| are: - `Explainers `_: shows how different explainers can be used with |quanda|. This tutorial goes through all the explainers that are included in |quanda| and walks through the steps of initializing the ``Explainer`` object, generating explanations and plotting them. - `Metrics `_: shows how to use the metrics in |quanda| to evaluate the performance of a method. This tutorial goes through all the metrics that are included in |quanda| and walks through the steps of initializing the metric and evaluating the performance of a TDA method. - :doc:`Benchmarks `: shows how to use the benchmarking tools in |quanda| to evaluate a data attribution method. This tutorial includes 3 different examples of benchmarks. +- :doc:`Linear Datamodeling Score (LDS) `: caveats and best practices for the LDS benchmark, including how to precompute and reuse counterfactual subset logits across explainers. .. toctree:: :hidden: tutorial_pages/benchmarks + tutorial_pages/lds diff --git a/quanda/benchmarks/base.py b/quanda/benchmarks/base.py index 2f1790cd..c20edac0 100644 --- a/quanda/benchmarks/base.py +++ b/quanda/benchmarks/base.py @@ -33,11 +33,12 @@ from quanda.utils.common import ( CheckpointLoadFunc, DatasetSplit, - _stable_repr, - _subsample_dataset, chunked_logits, class_accuracy, load_last_checkpoint, + resolve_config, + stable_repr, + subsample_dataset, ) from quanda.utils.datasets.dataset_handlers import get_dataset_handler from quanda.utils.datasets.transformed.base import TransformedDataset @@ -47,7 +48,7 @@ def _hash_expl_kwargs(expl_kwargs: Optional[dict]) -> str: """Stable short hash of sorted expl_kwargs for explanation repo IDs.""" payload = json.dumps( - expl_kwargs or {}, sort_keys=True, default=_stable_repr + expl_kwargs or {}, sort_keys=True, default=stable_repr ) return hashlib.sha1(payload.encode()).hexdigest()[:10] @@ -190,9 +191,13 @@ def load_pretrained( (metadata, model) must already be present under ``cache_dir``. By default False. load_fresh : bool, optional - If True, re-download metadata and model from the Hub, + If True, re-download the metadata snapshot from the Hub + and regenerate any cached split/wrapper metadata, overwriting the local cache. Incompatible with - ``offline=True``. By default False. + ``offline=True``. By default False. Note: model + checkpoints are reused from the local cache when present + regardless of this flag — delete the ckpt directory to + force a re-download. Returns ------- @@ -227,7 +232,6 @@ def load_pretrained( ) obj = cls.from_config( cfg, - load_meta_from_disk=True, offline=offline, load_fresh=load_fresh, device=device, @@ -238,18 +242,37 @@ def load_pretrained( @classmethod def from_config( cls, - config: dict, - load_meta_from_disk: bool = True, + config: Union[dict, str], offline: bool = False, device: str = "cpu", metadata_suffix: str = "", load_fresh: bool = False, ) -> "Benchmark": - """Initialize the benchmark from a dictionary.""" - if offline and load_fresh: - raise ValueError( - "offline=True and load_fresh=True are incompatible." - ) + """Initialize the benchmark from a config. + + Parameters + ---------- + config : Union[dict, str] + The benchmark configuration dictionary, a path to a YAML file + or registered ``bench_id`` + (see :data:`quanda.benchmarks.resources.config_map.config_map`). + offline : bool, optional + If True, no HTTP request is issued to the Hub; all assets + (metadata, model) must already be present under + ``config['bench_save_dir']``. By default False. + device : str, optional + Device to load the model on, by default "cpu". + metadata_suffix : str, optional + Suffix to disambiguate metadata directories. By default "". + load_fresh: bool, False + If True, regenerate any cached split/wrapper metadata + (overwriting local files) and, when ``offline=False``, + re-download metadata/model from the Hub. When False + (default), cached metadata is reused if present and only + missing pieces are generated. + + """ + config = resolve_config(config) cache_dir = config.get("bench_save_dir", "./tmp") metadata_dir = MetadataConfigParser.get_metadata_dir( cfg=config, @@ -260,20 +283,20 @@ def from_config( train_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config.get("train_dataset"), metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) val_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config.get("val_dataset"), metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) eval_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config.get("eval_dataset"), metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) @@ -283,7 +306,6 @@ def from_config( bench_save_dir=config["bench_save_dir"], ckpts=_resolve_ckpts(config), offline=offline, - load_fresh=load_fresh, device=device, ) ) @@ -293,7 +315,7 @@ def from_config( train_dataset=train_dataset, eval_dataset=eval_dataset, metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, ) return cls( @@ -317,7 +339,7 @@ def _extra_kwargs_from_config( train_dataset: Union[torch.utils.data.Dataset, datasets.Dataset], eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract subclass-specific kwargs from config. @@ -333,8 +355,9 @@ def _extra_kwargs_from_config( The parsed evaluation dataset. metadata_dir : str Path to the metadata directory. - load_meta_from_disk : bool - Whether metadata was loaded from disk. + load_fresh : bool + If True, regenerate any cached subclass-specific metadata + instead of reusing it. Returns ------- @@ -347,39 +370,48 @@ def _extra_kwargs_from_config( @classmethod def train( cls, - config: dict, + config: Union[dict, str], logger: Optional[L.pytorch.loggers.logger.Logger] = None, device: str = "cpu", batch_size: int = 64, - load_meta_from_disk: bool = False, + load_fresh: bool = True, + use_pid: bool = False, ) -> "Benchmark": """Train a model using the provided configuration. Parameters ---------- - config : dict - Dictionary containing the configuration. + config : dict | str + Either a configuration dict, a registered ``bench_id`` (see + :data:`quanda.benchmarks.resources.config_map.config_map`), + or a path to a benchmark YAML. The config must specify + ``bench_save_dir``, the directory under which the trained + benchmark (checkpoints and metadata) is saved. logger : Optional[lightning.pytorch.loggers.logger.Logger], optional Logger to be used for logging, by default None. device : str, optional Device to use for training, by default "cpu" batch_size : int, optional Batch size for training, by default 8 - load_meta_from_disk : bool, optional - If True, reuse existing metadata (splits, class mappings, - etc.) from the cache instead of regenerating. By default - False — training regenerates metadata so that a fresh - training run is reproducible from the config alone. + load_fresh : bool, optional + If True (default), regenerate splits/class mappings/etc. + so a fresh training run is reproducible from the config + alone. Set to False to reuse cached metadata. + use_pid : bool, optional + If True, suffix checkpoint and metadata directories with + the current process id to disambiguate concurrent runs. By + default False. Returns ------- None """ - pid_suffix = f"_pid{os.getpid()}" + config = resolve_config(config) + pid_suffix = f"_pid{os.getpid()}" if use_pid else "" obj = cls.from_config( config, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, device=device, metadata_suffix=pid_suffix, ) @@ -489,18 +521,48 @@ def train( @classmethod def train_and_push_to_hub( cls, - config: dict, + config: Union[dict, str], logger: Optional[L.pytorch.loggers.logger.Logger] = None, device: str = "cpu", batch_size: int = 64, - load_meta_from_disk: bool = False, + load_fresh: bool = True, + use_pid: bool = False, ): # pragma: no cover - """Train a model using the provided config and push to HF hub.""" + """Train a model using the provided config and push to HF hub. + + Parameters + ---------- + config : Union[dict, str] + Either a configuration dict, a registered ``bench_id`` (see + :data:`quanda.benchmarks.resources.config_map.config_map`), + or a path to a benchmark YAML. + logger : Optional[lightning.pytorch.loggers.logger.Logger], optional + Logger to be used for logging, by default None. + device : str, optional + Device to use for training, by default "cpu". + batch_size : int, optional + Batch size for training, by default 64. + load_fresh : bool, optional + If True (default), regenerate splits/class mappings/etc. so a + fresh training run is reproducible from the config alone. Set + to False to reuse cached metadata. + use_pid : bool, optional + If True, suffix checkpoint and metadata directories with the + current process id to disambiguate concurrent runs. By default + False. + + Returns + ------- + Benchmark + The trained benchmark instance. + + """ + config = resolve_config(config) skip_main_train = bool(config.get("skip_main_train", False)) if skip_main_train: obj = cls.from_config( config, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, device=device, ) obj._compute_and_save_indices(config, batch_size) @@ -510,7 +572,8 @@ def train_and_push_to_hub( logger=logger, device=device, batch_size=batch_size, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, + use_pid=use_pid, ) if not isinstance(obj.model, PyTorchModelHubMixin): raise TypeError( @@ -954,7 +1017,7 @@ def _download_explanations( @classmethod def explain( cls, - config: dict, + config: Union[dict, str], explainer_cls: type, expl_kwargs: Optional[dict] = None, batch_size: int = 8, @@ -967,11 +1030,43 @@ def explain( ) -> "Benchmark": """Compute and persist explanations for ``eval_dataset`` to disk. - Mirrors :meth:`train` but produces per-batch explanation tensors - plus an ``explanations_config.yaml`` describing how the cache - was generated. Returns the benchmark instance with - ``self._explanations_dir`` and ``self._explanations_id`` set. + Parameters + ---------- + config : Union[dict, str] + Benchmark config dict, registered ``bench_id``, or path to a + benchmark YAML. + explainer_cls : type + Explainer subclass to instantiate. + expl_kwargs : Optional[dict], optional + Extra kwargs forwarded to ``explainer_cls``, by default None. + batch_size : int, optional + Batch size used when iterating the eval dataset, by default 8. + explanations_id : Optional[str], optional + HF-style id for the cached explanations. If None, derived from + ``config`` via :func:`default_explanations_id`. By default None. + cache_dir : Optional[str], optional + Directory to write explanations into. If None, derived from + ``config['bench_save_dir']`` and ``explanations_id``. + device : str, optional + Device to load the model on, by default "cpu". + max_eval_n : Optional[int], optional + Cap on the number of eval samples; ``None`` means all. By + default 1000. + eval_seed : int, optional + Seed used when sampling the eval subset, by default 42. + inference_batch_size : Optional[int], optional + If set, every model forward run during prediction is split + into sub-batches of this size. ``None`` keeps the full + ``batch_size`` forward. + + Returns + ------- + Benchmark + The benchmark instance with ``_explanations_dir`` and + ``_explanations_id`` populated. + """ + config = resolve_config(config) obj = cls.from_config(config, device=device) if explanations_id is None: explanations_id = default_explanations_id( @@ -1012,7 +1107,7 @@ def explain( k: ( v if isinstance(v, (str, int, float, bool, type(None))) - else _stable_repr(v) + else stable_repr(v) ) for k, v in (expl_kwargs or {}).items() } @@ -1045,7 +1140,7 @@ def explain( @classmethod def explain_and_push_to_hub( cls, - config: dict, + config: Union[dict, str], explainer_cls: type, expl_kwargs: Optional[dict] = None, batch_size: int = 8, @@ -1055,7 +1150,40 @@ def explain_and_push_to_hub( max_eval_n: Optional[int] = 1000, eval_seed: int = 42, ): # pragma: no cover - """Compute explanations then upload them as a HF dataset repo.""" + """Compute explanations then upload them as a HF dataset repo. + + Parameters + ---------- + config : Union[dict, str] + Benchmark config dict, registered ``bench_id``, or path to a + benchmark YAML. + explainer_cls : type + Explainer subclass to instantiate. + expl_kwargs : Optional[dict], optional + Extra kwargs forwarded to ``explainer_cls``, by default None. + batch_size : int, optional + Batch size used when iterating the eval dataset, by default 8. + explanations_id : Optional[str], optional + HF repo id under which to upload the explanations. If None, + derived from ``config`` via :func:`default_explanations_id`. + cache_dir : Optional[str], optional + Directory to write explanations into before upload, by + default None. + device : str, optional + Device to load the model on, by default "cpu". + max_eval_n : Optional[int], optional + Cap on the number of eval samples; ``None`` means all. By + default 1000. + eval_seed : int, optional + Seed used when sampling the eval subset, by default 42. + + Returns + ------- + Benchmark + The benchmark instance after upload. + + """ + config = resolve_config(config) obj = cls.explain( config=config, explainer_cls=explainer_cls, @@ -1099,7 +1227,7 @@ def _iter_explanations( If ``precomputed_explanations`` is provided, batch ``i`` is read from the cache; otherwise ``explainer.explain`` is called. """ - eval_dataset = _subsample_dataset( + eval_dataset = subsample_dataset( eval_dataset, max_n=max_eval_n, seed=eval_seed ) ds_handler = get_dataset_handler(dataset=eval_dataset) diff --git a/quanda/benchmarks/config_parser.py b/quanda/benchmarks/config_parser.py index 9ea32dc0..a39e3024 100644 --- a/quanda/benchmarks/config_parser.py +++ b/quanda/benchmarks/config_parser.py @@ -91,7 +91,7 @@ def parse_dataset_cfg( cls, ds_config: Optional[dict], metadata_dir: str = ".tmp/meta", - load_meta_from_disk: bool = True, + load_fresh: bool = False, splits_cfg: Optional[dict] = None, ): """Return the dataset using the given parameters. @@ -102,9 +102,10 @@ def parse_dataset_cfg( Dataset configuration dictionary. metadata_dir : str Directory used for on-disk split and wrapper metadata. - load_meta_from_disk : bool - If True, load pre-existing split/wrapper metadata from disk - instead of regenerating. + load_fresh : bool + If False (default), reuse cached split/wrapper metadata from + disk when present and only generate what's missing. If True, + regenerate everything and overwrite any cached files. splits_cfg : Optional[dict] Top-level ``splits:`` registry mapping split names to their recipes (``{filename, ratios, seed}``). Datasets reference an @@ -116,13 +117,13 @@ def parse_dataset_cfg( splits_cfg = splits_cfg or {} dataset = cls._load_dataset_from_cfg( - ds_config, metadata_dir, load_meta_from_disk, splits_cfg + ds_config, metadata_dir, load_fresh, splits_cfg ) wrapper = copy.deepcopy(ds_config.get("wrapper", None)) if wrapper is not None: return cls._apply_wrapper( - dataset, ds_config, wrapper, metadata_dir, load_meta_from_disk + dataset, ds_config, wrapper, metadata_dir, load_fresh ) return dataset @@ -132,7 +133,7 @@ def split_dataset( dataset: torch.utils.data.Dataset, ds_config: dict, metadata_dir: str, - load_meta_from_disk: bool = True, + load_fresh: bool = False, splits_cfg: Optional[dict] = None, ): """Split the dataset using the given parameters. @@ -145,8 +146,8 @@ def split_dataset( The dataset configuration dictionary. metadata_dir: str Directory to store the metadata. - load_meta_from_disk: bool - Whether to load metadata from disk. + load_fresh: bool + If True, regenerate the split even if a cached file exists. splits_cfg: Optional[dict] Top-level splits registry (name -> recipe). @@ -163,7 +164,7 @@ def split_dataset( recipe = cls._resolve_split_recipe(split_ref, splits_cfg or {}) splits = cls._load_split_if_exists_or_generate( dataset, - load_meta_from_disk, + load_fresh, metadata_dir, recipe["filename"], split_ratios=recipe["ratios"], @@ -186,13 +187,13 @@ def _load_dataset_from_cfg( cls, ds_config: dict, metadata_dir: str, - load_meta_from_disk: bool = True, + load_fresh: bool = False, splits_cfg: Optional[dict] = None, ) -> torch.utils.data.Dataset: """Load dataset based on configuration.""" if "single_class_dataset" not in ds_config: return cls._load_hf_dataset_from_config( - ds_config, metadata_dir, load_meta_from_disk, splits_cfg + ds_config, metadata_dir, load_fresh, splits_cfg ) elif ds_config["single_class_dataset"]: return cls._load_single_class_dataset( @@ -206,7 +207,7 @@ def _load_hf_dataset_from_config( cls, ds_config: dict, metadata_dir: str, - load_meta_from_disk: bool = True, + load_fresh: bool = False, splits_cfg: Optional[dict] = None, ) -> Union[torch.utils.data.Dataset, hf_datasets.Dataset]: """Load a HuggingFace dataset based on configuration.""" @@ -223,7 +224,7 @@ def _load_hf_dataset_from_config( base_dataset, ds_config, metadata_dir, - load_meta_from_disk, + load_fresh, splits_cfg or {}, ) @@ -273,7 +274,7 @@ def _apply_indices( base_dataset: Union[torch.utils.data.Dataset, hf_datasets.Dataset], ds_config: dict, metadata_dir: str, - load_meta_from_disk: bool = True, + load_fresh: bool = False, splits_cfg: Optional[dict] = None, ) -> Union[torch.utils.data.Dataset, hf_datasets.Dataset]: """Apply indices to the dataset based on configuration.""" @@ -284,7 +285,7 @@ def _apply_indices( split_name = ds_config.get("split_name", "train") split = cls._load_split_if_exists_or_generate( base_dataset, - load_meta_from_disk, + load_fresh, metadata_dir, split_recipe["filename"], split_ratios=split_recipe["ratios"], @@ -321,7 +322,7 @@ def _apply_filter( dataset: torch.utils.data.Dataset, ds_config: dict, metadata_dir: str, - load_meta_from_disk: bool = True, + load_fresh: bool = False, ): """Apply the filter to the dataset. @@ -329,12 +330,14 @@ def _apply_filter( produced by ``_compute_and_save_indices`` only when a ``filter_by_*`` flag is set. Its absence is treated as "no filter applied" rather than a strict error — configs commonly - declare a filename without ever producing the file. + declare a filename without ever producing the file. When + ``load_fresh`` is True, any existing filter is skipped (the + post-training step will regenerate it). """ filter_indices_cfg = ds_config.get("filter_indices", None) if filter_indices_cfg is None: return dataset - if not load_meta_from_disk: + if load_fresh: return dataset filter_filename = filter_indices_cfg.get( "split_filename", "DOESNT_EXIST" @@ -357,9 +360,14 @@ def _apply_wrapper( ds_config: dict, wrapper_cfg: dict, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> torch.utils.data.Dataset: - """Apply a wrapper to the dataset based on configuration.""" + """Apply a wrapper to the dataset based on configuration. + + Wrapper metadata is auto-resolved: cached files are reused when + present and ``load_fresh`` is False; otherwise metadata is + generated and saved. + """ wrapper_cfg = dict(wrapper_cfg) wrapper_cls = transform_wrappers[wrapper_cfg.pop("type")] # check if wrapper_cls is a subclass of TransformedDataset @@ -369,24 +377,20 @@ def _apply_wrapper( ) kwargs = wrapper_cfg + meta_filename = "DOESNT_EXIST" + loaded_from_disk = False if "metadata" in kwargs: metadata_args = dict(kwargs.pop("metadata", {})) meta_filename = metadata_args.pop( "metadata_filename", "DOESNT_EXIST" ) - if load_meta_from_disk: - if not wrapper_cls.metadata_cls.exists( - metadata_dir, meta_filename - ): - raise FileNotFoundError( - f"Wrapper metadata '{meta_filename}' not found in " - f"{metadata_dir}. Re-run with " - f"load_meta_from_disk=False to regenerate it." - ) - loaded_meta = wrapper_cls.metadata_cls.load( + if not load_fresh and wrapper_cls.metadata_cls.exists( + metadata_dir, meta_filename + ): + kwargs["metadata"] = wrapper_cls.metadata_cls.load( metadata_dir, meta_filename ) - kwargs["metadata"] = loaded_meta + loaded_from_disk = True else: kwargs["metadata"] = wrapper_cls.metadata_cls(**metadata_args) @@ -394,7 +398,7 @@ def _apply_wrapper( mapping = ClassMapping.resolve( kwargs.pop("class_to_group"), metadata_dir, - load_meta_from_disk, + load_fresh=load_fresh, ) kwargs["class_to_group"] = mapping.class_to_group kwargs["n_classes"] = mapping.n_classes @@ -411,9 +415,9 @@ def _apply_wrapper( wrapped_dataset, ds_config, metadata_dir, - load_meta_from_disk, + load_fresh, ) - if not load_meta_from_disk: + if not loaded_from_disk and meta_filename != "DOESNT_EXIST": filtered_dataset.metadata.save(metadata_dir, meta_filename) return filtered_dataset @@ -421,28 +425,22 @@ def _apply_wrapper( def _load_split_if_exists_or_generate( cls, dataset, - load_meta_from_disk, + load_fresh, metadata_dir, split_filename, split_ratios: Optional[dict] = None, ): """Load the split from disk or generate it. - When ``load_meta_from_disk=True``, the split file must already - exist; a ``FileNotFoundError`` is raised if it does not. When - ``load_meta_from_disk=False``, a new split is generated and - saved to disk. + When ``load_fresh=False`` (default), reuses the cached split if + present; otherwise generates a new split and saves it. When + ``load_fresh=True``, always regenerates and overwrites. """ if split_ratios is None: split_ratios = {"train": 0.9, "test": 0.1} - if load_meta_from_disk: - if not DatasetSplit.exists(metadata_dir, split_filename): - raise FileNotFoundError( - f"Split file '{split_filename}' not found in " - f"{metadata_dir}. Re-run with " - f"load_meta_from_disk=False to regenerate it, or " - f"populate the cache first." - ) + if not load_fresh and DatasetSplit.exists( + metadata_dir, split_filename + ): return DatasetSplit.load(metadata_dir, split_filename) split = DatasetSplit.split(len(dataset), 42, split_ratios) split.save(metadata_dir, split_filename) @@ -732,12 +730,42 @@ class FactTracingConfigParser: @classmethod def parse_fact_tracing_cfg( - cls, cfg: dict + cls, + cfg: dict, + offline: bool = False, + load_fresh: bool = False, ) -> Tuple[hf_datasets.Dataset, hf_datasets.Dataset, torch.Tensor, int]: - """Build ``(prompt_ds, evidence_ds, entailment_labels, pad_id)``.""" + """Build ``(prompt_ds, evidence_ds, entailment_labels, pad_id)``. + + Parameters + ---------- + cfg : dict + Fact-tracing configuration dictionary. + offline : bool, optional + If True, no HTTP request is issued; the HF source dataset + must already be present in the local cache. By default False. + load_fresh : bool, optional + If True, force re-download of the HF source dataset, + overwriting the local cache. Incompatible with + ``offline=True``. By default False. + + """ + if offline and load_fresh: + raise ValueError( + "offline=True and load_fresh=True are incompatible: " + "cannot refresh the cache without network access." + ) tokenize, pad_id = resolve_tokenizer(cfg["tokenizer"]) + if load_fresh: + download_mode = "force_redownload" + elif offline: + download_mode = "reuse_cache_if_exists" + else: + download_mode = "reuse_dataset_if_exists" ds = load_dataset( - cfg["dataset_str"], split=cfg.get("dataset_split", "train") + cfg["dataset_str"], + split=cfg.get("dataset_split", "train"), + download_mode=download_mode, ) num_prompts = cfg.get("num_prompts", 20) @@ -745,7 +773,6 @@ def parse_fact_tracing_cfg( max_length = cfg.get("max_length", 128) max_evidence_per_prompt = cfg.get("max_evidence_per_prompt", 5) - # Sample prompt entries if num_prompts < len(ds): random.seed(seed) indices = random.sample(range(len(ds)), num_prompts) @@ -753,7 +780,25 @@ def parse_fact_tracing_cfg( else: sampled_dataset = ds - # Tokenize prompt + answer and mask prompt in labels + prompt_dataset = cls._build_prompt_dataset( + sampled_dataset, tokenize, max_length + ) + evidence_dataset, evidence_map = cls._build_evidence_dataset( + sampled_dataset, tokenize, max_length, max_evidence_per_prompt + ) + entailment_labels = cls._build_entailment_matrix( + len(prompt_dataset), len(evidence_dataset), evidence_map + ) + + return prompt_dataset, evidence_dataset, entailment_labels, pad_id + + @staticmethod + def _build_prompt_dataset( + sampled_dataset: hf_datasets.Dataset, + tokenize: Callable, + max_length: int, + ) -> hf_datasets.Dataset: + """Tokenize prompt+answer, masking prompt and padding in labels.""" input_ids = [] attention_mask = [] labels = [] @@ -777,11 +822,8 @@ def parse_fact_tracing_cfg( )["input_ids"] prompt_len = len(prompt_ids) - # Mask out prompt from loss label_ids = encoded["input_ids"].copy() label_ids[:prompt_len] = [-100] * prompt_len - - # Mask out padding tokens in labels for i in range(len(encoded["input_ids"])): if encoded["attention_mask"][i] == 0: label_ids[i] = -100 @@ -806,18 +848,24 @@ def parse_fact_tracing_cfg( columns=["input_ids", "attention_mask", "labels"], output_all_columns=True, ) + return prompt_dataset - # Gather evidence sentences + @staticmethod + def _build_evidence_dataset( + sampled_dataset: hf_datasets.Dataset, + tokenize: Callable, + max_length: int, + max_evidence_per_prompt: int, + ) -> Tuple[hf_datasets.Dataset, List[int]]: + """Tokenize evidence sentences and return the evidence→prompt map.""" evidence_sentences = [] evidence_map = [] - for i, entry in enumerate(sampled_dataset): selected = entry["evidence_sentences"][:max_evidence_per_prompt] for sentence in selected: evidence_sentences.append(sentence) evidence_map.append(i) - # Tokenize evidence sentences evidence_input_ids = [] evidence_attention_mask = [] evidence_labels = [] @@ -831,7 +879,6 @@ def parse_fact_tracing_cfg( evidence_input_ids.append(encoded["input_ids"]) evidence_attention_mask.append(encoded["attention_mask"]) - # Create labels and mask out padding tokens label_ids = encoded["input_ids"].copy() for i in range(len(encoded["input_ids"])): if encoded["attention_mask"][i] == 0: @@ -851,13 +898,7 @@ def parse_fact_tracing_cfg( columns=["input_ids", "attention_mask", "labels"], output_all_columns=True, ) - - # Create entailment matrix - entailment_labels = cls._build_entailment_matrix( - len(prompt_dataset), len(evidence_dataset), evidence_map - ) - - return prompt_dataset, evidence_dataset, entailment_labels, pad_id + return evidence_dataset, evidence_map @staticmethod def _build_entailment_matrix( diff --git a/quanda/benchmarks/downstream_eval/_fact_tracing.py b/quanda/benchmarks/downstream_eval/_fact_tracing.py index dac52305..215047a5 100644 --- a/quanda/benchmarks/downstream_eval/_fact_tracing.py +++ b/quanda/benchmarks/downstream_eval/_fact_tracing.py @@ -19,7 +19,12 @@ from quanda.explainers import Explainer from quanda.metrics import Metric from quanda.utils.cache import BatchedCachedExplanations -from quanda.utils.common import CheckpointLoadFunc, _subsample_indices, ds_len +from quanda.utils.common import ( + CheckpointLoadFunc, + ds_len, + resolve_config, + subsample_indices, +) class FactTracingBenchmark(Benchmark): @@ -70,29 +75,42 @@ def __init__( @classmethod def from_config( cls, - config: dict, - load_meta_from_disk: bool = True, + config: Union[dict, str], offline: bool = False, device: str = "cpu", metadata_suffix: str = "", load_fresh: bool = False, ) -> "FactTracingBenchmark": - """Build the benchmark from a YAML-derived config dict. + """Initialize the benchmark from a config. + + Parameters + ---------- + config : Union[dict, str] + The benchmark configuration dictionary, a path to a YAML file + or registered ``bench_id`` + (see :data:`quanda.benchmarks.resources.config_map.config_map`). + offline : bool, optional + If True, no HTTP request is issued to the Hub; all assets + (metadata, model) must already be present under + ``config['bench_save_dir']``. By default False. + device : str, optional + Device to load the model on, by default "cpu". + metadata_suffix : str, optional + Suffix to disambiguate metadata directories. By default "". + load_fresh: bool, False + If True, regenerate any cached split/wrapper metadata + (overwriting local files) and, when ``offline=False``, + re-download metadata/model from the Hub. When False + (default), cached metadata is reused if present and only + missing pieces are generated. - Loads prompts/evidence/entailment via - :func:`load_fact_tracing_datasets_from_cfg` (which bypasses the - generic dataset parser because one HF dataset fans out into - both splits) and the model via the standard - :class:`ModelConfigParser` path. """ - if offline and load_fresh: - raise ValueError( - "offline=True and load_fresh=True are incompatible." - ) - + config = resolve_config(config) prompt_ds, evidence_ds, entailment_labels, _ = ( FactTracingConfigParser.parse_fact_tracing_cfg( - config["fact_tracing"] + config["fact_tracing"], + offline=offline, + load_fresh=load_fresh, ) ) @@ -102,7 +120,6 @@ def from_config( bench_save_dir=config.get("bench_save_dir", "./tmp"), ckpts=_resolve_ckpts(config), offline=offline, - load_fresh=load_fresh, device=device, ) ) @@ -156,7 +173,7 @@ def _evaluate_dataset( if self.entailment_labels is None: self._entailment_labels_aligned = None else: - indices = _subsample_indices( + indices = subsample_indices( ds_len(eval_dataset), max_eval_n, eval_seed ) self._entailment_labels_aligned = self.entailment_labels[indices] diff --git a/quanda/benchmarks/downstream_eval/class_detection.py b/quanda/benchmarks/downstream_eval/class_detection.py index 8a21fd8a..51430dd0 100644 --- a/quanda/benchmarks/downstream_eval/class_detection.py +++ b/quanda/benchmarks/downstream_eval/class_detection.py @@ -74,7 +74,7 @@ def _extra_kwargs_from_config( train_dataset: torch.utils.data.Dataset, eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract class detection kwargs from config.""" return { diff --git a/quanda/benchmarks/downstream_eval/mislabeling_detection.py b/quanda/benchmarks/downstream_eval/mislabeling_detection.py index 088680ca..553b61ac 100644 --- a/quanda/benchmarks/downstream_eval/mislabeling_detection.py +++ b/quanda/benchmarks/downstream_eval/mislabeling_detection.py @@ -2,7 +2,7 @@ import logging import os -from typing import Optional +from typing import Optional, Union import torch import yaml @@ -14,7 +14,12 @@ ) from quanda.metrics.downstream_eval import MislabelingDetectionMetric from quanda.utils.cache import ExplanationsCache -from quanda.utils.common import _subsample_dataset, class_accuracy, ds_len +from quanda.utils.common import ( + class_accuracy, + ds_len, + resolve_config, + subsample_dataset, +) from quanda.utils.datasets.transformed.label_flipping import ( LabelFlippingDataset, ) @@ -187,7 +192,7 @@ def evaluate( "labels." ) - train_dataset = _subsample_dataset( + train_dataset = subsample_dataset( self.train_dataset, max_n=max_eval_n, seed=eval_seed ) if not isinstance(train_dataset, LabelFlippingDataset): @@ -231,7 +236,7 @@ def evaluate( @classmethod def explain( cls, - config: dict, + config: Union[dict, str], explainer_cls: type, expl_kwargs: Optional[dict] = None, batch_size: int = 8, @@ -251,7 +256,11 @@ def explain( here parameterize the train-dataset subsample over which self-influence is computed. ``inference_batch_size`` is ignored since there is no eval-time inference pass. + + ``config`` accepts a config dict, a registered ``bench_id``, or + a path to a benchmark YAML. """ + config = resolve_config(config) obj = cls.from_config(config, device=device) if explanations_id is None: explanations_id = default_explanations_id( @@ -279,7 +288,7 @@ def explain( "Training dataset in Mislabeling Metric should have " f"flipped labels, got {type(obj.train_dataset).__name__}." ) - train_dataset = _subsample_dataset( + train_dataset = subsample_dataset( obj.train_dataset, max_n=max_eval_n, seed=eval_seed ) explainer = obj._prepare_explainer( diff --git a/quanda/benchmarks/downstream_eval/shortcut_detection.py b/quanda/benchmarks/downstream_eval/shortcut_detection.py index 99390d79..bd266921 100644 --- a/quanda/benchmarks/downstream_eval/shortcut_detection.py +++ b/quanda/benchmarks/downstream_eval/shortcut_detection.py @@ -89,7 +89,7 @@ def _extra_kwargs_from_config( train_dataset: torch.utils.data.Dataset, eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract shortcut detection kwargs from config.""" if not isinstance(eval_dataset, SampleTransformationDataset): diff --git a/quanda/benchmarks/downstream_eval/subclass_detection.py b/quanda/benchmarks/downstream_eval/subclass_detection.py index 4027d1ed..c0dbce28 100644 --- a/quanda/benchmarks/downstream_eval/subclass_detection.py +++ b/quanda/benchmarks/downstream_eval/subclass_detection.py @@ -90,7 +90,7 @@ def _extra_kwargs_from_config( train_dataset: torch.utils.data.Dataset, eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract subclass detection kwargs from config.""" if not isinstance(train_dataset, LabelGroupingDataset): @@ -132,9 +132,6 @@ def evaluate( Keyword arguments for the explainer, by default None. batch_size: int, optional Batch size for the evaluation, by default 8. - max_eval_n: Optional[int], optional - Maximum number of evaluation samples to use. If None, uses the - entire evaluation dataset. By default 1000. max_eval_n: Optional[int], optional Maximum number of evaluation samples to use. If None, uses the entire evaluation dataset. By default 1000. diff --git a/quanda/benchmarks/ground_truth/linear_datamodeling.py b/quanda/benchmarks/ground_truth/linear_datamodeling.py index 51132373..1d1252cb 100644 --- a/quanda/benchmarks/ground_truth/linear_datamodeling.py +++ b/quanda/benchmarks/ground_truth/linear_datamodeling.py @@ -6,7 +6,7 @@ import random import warnings from copy import deepcopy -from typing import Callable, List, Optional, Tuple +from typing import Callable, List, Optional, Tuple, Union import lightning as L import torch @@ -22,9 +22,10 @@ LinearDatamodelingMetric, ) from quanda.utils.common import ( - _subsample_dataset, chunked_logits, class_accuracy, + resolve_config, + subsample_dataset, ) from quanda.utils.datasets.dataset_handlers import get_dataset_handler from quanda.utils.functions import correlation_functions @@ -213,12 +214,13 @@ def _train_subset_models( @classmethod def train( # type: ignore[override] cls, - config: dict, + config: Union[dict, str], logger: Optional[L.pytorch.loggers.logger.Logger] = None, device: str = "cpu", batch_size: int = 64, skip_subsets: bool = False, - load_meta_from_disk: bool = False, + load_fresh: bool = True, + use_pid: bool = False, ) -> "LinearDatamodeling": """Train main model and subset models. @@ -239,9 +241,13 @@ def train( # type: ignore[override] If True, skip the subset training loop. Used when subsets are trained out-of-band (e.g. one-by-one in parallel workers via :meth:`train_subset`). - load_meta_from_disk : bool, optional - If True, reuse existing metadata (splits, subset_ids, etc.) - from the cache instead of regenerating. By default False. + load_fresh : bool, optional + If True (default), regenerate splits/subset_ids/etc. + instead of reusing the cache. + use_pid : bool, optional + If True, suffix checkpoint and metadata directories with + the current process id to disambiguate concurrent runs. By + default False. Returns ------- @@ -249,12 +255,14 @@ def train( # type: ignore[override] The trained benchmark instance. """ + config = resolve_config(config) obj = super().train( config=config, logger=logger, device=device, batch_size=batch_size, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, + use_pid=use_pid, ) if not isinstance(obj, LinearDatamodeling): raise TypeError("Expected a LinearDatamodeling instance.") @@ -278,12 +286,12 @@ def train( # type: ignore[override] @classmethod def train_subset( cls, - config: dict, + config: Union[dict, str], idx: int, device: str = "cpu", batch_size: int = 64, push_to_hub: bool = False, - load_meta_from_disk: bool = True, + load_fresh: bool = False, ) -> "LinearDatamodeling": """Train and save a single subset model by index. @@ -304,16 +312,16 @@ def train_subset( Batch size. push_to_hub : bool, optional If True, push the resulting subset checkpoint to HF Hub. - load_meta_from_disk : bool, optional - Whether to load existing metadata (subset_ids, etc.) from disk. - If False, will regenerate metadata from the main model and - which may lead to different subset splits if the generation is - not deterministic (e.g. if the seed is not fixed). By default True. + load_fresh : bool, optional + If True, regenerate cached metadata (subset_ids, etc.). + Doing so can change the subset splits if generation is not + deterministic. By default False — reuse cached metadata. """ + config = resolve_config(config) obj = cls.from_config( config, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, offline=True, device=device, ) @@ -343,23 +351,24 @@ def train_subset( @classmethod def generate_and_push_metadata( - cls, config: dict + cls, config: Union[dict, str] ) -> None: # pragma: no cover """Regenerate LDS metadata locally and push it to HF Hub. - Calls ``from_config`` with ``load_meta_from_disk=False, offline=True`` - to materialize splits and subset_ids under the metadata dir, then + Calls ``from_config`` with ``load_fresh=True, offline=True`` to + materialize splits and subset_ids under the metadata dir, then uploads that dir to ``meta_id``. """ from huggingface_hub import HfApi # local import; optional dep path + config = resolve_config(config) metadata_dir = MetadataConfigParser.get_metadata_dir( cfg=config, bench_save_dir=config["bench_save_dir"] ) meta_id = config.get( "meta_id", f"{config['repo_id']}/{config['id']}_metadata" ) - cls.from_config(config, load_meta_from_disk=False, offline=True) + cls.from_config(config, load_fresh=True, offline=True) api = HfApi() api.create_repo(repo_id=meta_id, repo_type="dataset", exist_ok=True) @@ -372,7 +381,7 @@ def generate_and_push_metadata( @classmethod def push_subset( cls, - config: dict, + config: Union[dict, str], idx: int, ) -> None: """Push an already-trained subset checkpoint to HF Hub. @@ -381,6 +390,7 @@ def push_subset( """ from huggingface_hub import HfApi # local import; optional dep path + config = resolve_config(config) local_ckpt_dir, repo_id = _subset_ckpt_paths(config, idx) if not os.path.isdir(local_ckpt_dir): @@ -400,7 +410,7 @@ def _extra_kwargs_from_config( train_dataset: torch.utils.data.Dataset, eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract linear datamodeling kwargs from config.""" m = config.get("m", 100) @@ -428,13 +438,7 @@ def _extra_kwargs_from_config( generator.manual_seed(seed) subset_meta = f"{metadata_dir}/{config['subset_ids']}" - if load_meta_from_disk: - if not os.path.exists(subset_meta): - raise FileNotFoundError( - f"Subset ids file not found at {subset_meta}. " - f"Re-run with load_meta_from_disk=False to " - f"regenerate it." - ) + if not load_fresh and os.path.exists(subset_meta): with open(subset_meta, "r") as f: subset_ids = yaml.safe_load(f) else: @@ -464,13 +468,15 @@ def _extra_kwargs_from_config( @classmethod def train_and_push_to_hub( cls, - config: dict, + config: Union[dict, str], logger: Optional[L.pytorch.loggers.logger.Logger] = None, device: str = "cpu", batch_size: int = 64, - load_meta_from_disk: bool = False, + load_fresh: bool = True, + use_pid: bool = False, ): # pragma: no cover """Train a model using the provided config and push to HF hub.""" + config = resolve_config(config) skip_subsets = bool(config.get("skip_subsets", False)) cls._push_subsets_during_train = not skip_subsets cls._lds_skip_subsets = skip_subsets @@ -480,7 +486,8 @@ def train_and_push_to_hub( logger=logger, device=device, batch_size=batch_size, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, + use_pid=use_pid, ) finally: cls._push_subsets_during_train = False @@ -537,12 +544,13 @@ def _load_subset_model( @classmethod def subset_logits_cache_dir( cls, - config: dict, + config: Union[dict, str], batch_size: int = 8, max_eval_n: Optional[int] = 1000, eval_seed: int = 42, ) -> str: """Return default local cache dir for counterfactual subset logits.""" + config = resolve_config(config) repo = config.get("repo_id", "quanda-bench-test") group = config.get("explanations_group", config["id"]) logits_id = ( @@ -569,7 +577,7 @@ def _collect_eval_batches( methods batch the eval set identically (same batch boundaries = same ``i`` indexing as the ``_iter_explanations`` consumer). """ - eval_dataset = _subsample_dataset( + eval_dataset = subsample_dataset( obj.eval_dataset, max_n=max_eval_n, seed=eval_seed ) ds_handler = get_dataset_handler(dataset=eval_dataset) @@ -583,7 +591,7 @@ def _collect_eval_batches( @classmethod def cache_subset_logits_per_idx( cls, - config: dict, + config: Union[dict, str], idx: int, batch_size: int = 8, cache_dir: Optional[str] = None, @@ -592,7 +600,12 @@ def cache_subset_logits_per_idx( eval_seed: int = 42, inference_batch_size: Optional[int] = None, ) -> str: - """Cache counterfactual logits for a **single** subset index.""" + """Cache counterfactual logits for a **single** subset index. + + ``config`` accepts a config dict, a registered ``bench_id``, or + a path to a benchmark YAML. + """ + config = resolve_config(config) obj = cls.from_config(config, device=device) if not isinstance(obj, LinearDatamodeling): raise TypeError("Expected a LinearDatamodeling instance.") @@ -633,7 +646,7 @@ def cache_subset_logits_per_idx( @classmethod def cache_subset_logits( cls, - config: dict, + config: Union[dict, str], batch_size: int = 8, cache_dir: Optional[str] = None, device: str = "cpu", @@ -641,7 +654,12 @@ def cache_subset_logits( eval_seed: int = 42, inference_batch_size: Optional[int] = None, ) -> str: - """Cache counterfactual logits for every (subset, eval batch).""" + """Cache counterfactual logits for every (subset, eval batch). + + ``config`` accepts a config dict, a registered ``bench_id``, or + a path to a benchmark YAML. + """ + config = resolve_config(config) obj = cls.from_config(config, device=device) if not isinstance(obj, LinearDatamodeling): raise TypeError("Expected a LinearDatamodeling instance.") diff --git a/quanda/benchmarks/heuristics/mixed_datasets.py b/quanda/benchmarks/heuristics/mixed_datasets.py index 30ed81b4..7480fd33 100644 --- a/quanda/benchmarks/heuristics/mixed_datasets.py +++ b/quanda/benchmarks/heuristics/mixed_datasets.py @@ -1,7 +1,7 @@ """Mixed Datasets benchmark module.""" import logging -from typing import List, Optional +from typing import List, Optional, Union import torch from torch.utils.data import Subset @@ -13,7 +13,7 @@ ModelConfigParser, ) from quanda.metrics.heuristics.mixed_datasets import MixedDatasetsMetric -from quanda.utils.common import class_accuracy, ds_len +from quanda.utils.common import class_accuracy, ds_len, resolve_config logger = logging.getLogger(__name__) @@ -88,8 +88,7 @@ def __init__( @classmethod def from_config( cls, - config: dict, - load_meta_from_disk: bool = True, + config: Union[dict, str], offline: bool = False, device: str = "cpu", metadata_suffix: str = "", @@ -101,9 +100,6 @@ def from_config( ---------- config : dict Dictionary containing the configuration. - load_meta_from_disk : str - Loads dataset metadata from disk if True, otherwise generates - it, default True. offline : bool, optional If True, no HTTP request is issued to the Hub, by default False. @@ -113,15 +109,13 @@ def from_config( Suffix to add to the metadata directory name, by default "". User to prevent assets clashing when multiprocessing. load_fresh : bool, optional - If True, force re-download of the model checkpoints from the - Hub, overwriting the local cache. Incompatible with - ``offline=True``. By default False. + If True, force re-download of the model checkpoints from + the Hub and regenerate cached metadata, overwriting the + local cache. Incompatible with ``offline=True``. + By default False. """ - if offline and load_fresh: - raise ValueError( - "offline=True and load_fresh=True are incompatible." - ) + config = resolve_config(config) metadata_dir = MetadataConfigParser.get_metadata_dir( cfg=config, bench_save_dir=config.get("bench_save_dir", "./tmp"), @@ -131,26 +125,26 @@ def from_config( train_base_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config["train_dataset"], metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) val_base_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config.get("val_dataset", None), metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) adv_dataset = DatasetConfigParser.parse_dataset_cfg( ds_config=config["adv_dataset"], metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) split_datasets = DatasetConfigParser.split_dataset( dataset=adv_dataset, ds_config=config["adv_dataset"], metadata_dir=metadata_dir, - load_meta_from_disk=load_meta_from_disk, + load_fresh=load_fresh, splits_cfg=splits_cfg, ) adv_base_dataset = split_datasets["train"] @@ -181,7 +175,6 @@ def from_config( bench_save_dir=config["bench_save_dir"], ckpts=_resolve_ckpts(config), offline=offline, - load_fresh=load_fresh, device=device, ) ) diff --git a/quanda/benchmarks/heuristics/model_randomization.py b/quanda/benchmarks/heuristics/model_randomization.py index da988a4e..c4024a9f 100644 --- a/quanda/benchmarks/heuristics/model_randomization.py +++ b/quanda/benchmarks/heuristics/model_randomization.py @@ -76,7 +76,7 @@ def _extra_kwargs_from_config( train_dataset: torch.utils.data.Dataset, eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract model randomization kwargs from config.""" return { diff --git a/quanda/benchmarks/heuristics/top_k_cardinality.py b/quanda/benchmarks/heuristics/top_k_cardinality.py index 41fa45e3..c9d35b8b 100644 --- a/quanda/benchmarks/heuristics/top_k_cardinality.py +++ b/quanda/benchmarks/heuristics/top_k_cardinality.py @@ -62,7 +62,7 @@ def _extra_kwargs_from_config( train_dataset: Union[torch.utils.data.Dataset, datasets.Dataset], eval_dataset: torch.utils.data.Dataset, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool, ) -> dict: """Extract top_k from config.""" return {"top_k": config["top_k"]} diff --git a/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_MixedDatasets.yaml b/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_MixedDatasets.yaml index 2adf3bce..08187283 100644 --- a/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_MixedDatasets.yaml +++ b/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_MixedDatasets.yaml @@ -47,7 +47,7 @@ val_dataset: id: 2fc831c-awa2_resnet50_MixedDatasets bench: MixedDatasets adversarial_label: 0 -bench_save_dir: /data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_ShortcutDetection.yaml b/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_ShortcutDetection.yaml index 33b3d138..cf1c2ff8 100644 --- a/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_ShortcutDetection.yaml +++ b/quanda/benchmarks/resources/configs/2fc831c-awa2_resnet50_ShortcutDetection.yaml @@ -73,7 +73,7 @@ val_dataset: id: 2fc831c-awa2_resnet50_ShortcutDetection bench: ShortcutDetection adversarial_label: 0 -bench_save_dir: /data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_ClassDetection.yaml b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_ClassDetection.yaml index 6ad63adb..92d96528 100644 --- a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_ClassDetection.yaml +++ b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_ClassDetection.yaml @@ -44,7 +44,7 @@ val_dataset: id: GIT_TAG-awa2_resnet50_ClassDetection bench: ClassDetection adversarial_label: 0 -bench_save_dir: /data/cluster/users/bareeva/quanda_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_LDS.yaml b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_LDS.yaml index 18b825f3..7b2c2ba1 100644 --- a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_LDS.yaml +++ b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_LDS.yaml @@ -44,7 +44,7 @@ val_dataset: id: GIT_TAG-awa2_resnet50_LDS bench: LDS adversarial_label: 0 -bench_save_dir: /data/cluster/users/bareeva/quanda_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_MislabelingDetection.yaml b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_MislabelingDetection.yaml index 3a0ad4b9..91d02008 100644 --- a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_MislabelingDetection.yaml +++ b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_MislabelingDetection.yaml @@ -50,7 +50,7 @@ val_dataset: id: GIT_TAG-awa2_resnet50_MislabelingDetection bench: MislabelingDetection adversarial_label: 0 -bench_save_dir: /data/cluster/users/bareeva/quanda_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_SubclassDetection.yaml b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_SubclassDetection.yaml index 7b2cbed4..d239974a 100644 --- a/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_SubclassDetection.yaml +++ b/quanda/benchmarks/resources/configs/5d5968d-awa2_resnet50_SubclassDetection.yaml @@ -80,7 +80,7 @@ val_dataset: id: GIT_TAG-awa2_resnet50_SubclassDetection bench: SubclassDetection adversarial_label: 0 -bench_save_dir: /data/cluster/users/bareeva/quanda_output_new2/eval_bench/awa2 +bench_save_dir: bench_out/eval_bench/awa2 log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/benchmarks/resources/configs/99a4f7b-bert_qnli_MixedDatasets.yaml b/quanda/benchmarks/resources/configs/99a4f7b-bert_qnli_MixedDatasets.yaml index 9975264b..c69e8e31 100644 --- a/quanda/benchmarks/resources/configs/99a4f7b-bert_qnli_MixedDatasets.yaml +++ b/quanda/benchmarks/resources/configs/99a4f7b-bert_qnli_MixedDatasets.yaml @@ -65,7 +65,7 @@ logger: project: quanda-bench id: 99a4f7b-bert_qnli_MixedDatasets bench: MixedDatasets -bench_save_dir: /data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/qnli +bench_save_dir: bench_out/eval_bench/qnli log_dir: hydra_logs repo_id: quanda-bench-test cache_dir: tmp diff --git a/quanda/explainers/base.py b/quanda/explainers/base.py index a06a9ab3..0619e584 100644 --- a/quanda/explainers/base.py +++ b/quanda/explainers/base.py @@ -167,14 +167,7 @@ def self_influence(self, batch_size: int = 32) -> torch.Tensor: return influences def load_last_checkpoint(self): - """Load the model from the checkpoint file. - - Parameters - ---------- - checkpoint : str - Path to the checkpoint file. - - """ + """Load the last checkpoint in ``self.checkpoints`` into the model.""" load_last_checkpoint( model=self.model, checkpoints=self.checkpoints, diff --git a/quanda/explainers/wrappers/captum_influence.py b/quanda/explainers/wrappers/captum_influence.py index b6b9b551..dbeba1f4 100644 --- a/quanda/explainers/wrappers/captum_influence.py +++ b/quanda/explainers/wrappers/captum_influence.py @@ -1003,7 +1003,7 @@ class CaptumTracInCPFast(CaptumInfluence): ---------- (1) Pruthi, Garima, et al. (2020). "Estimating training data influence by tracing gradient descent." - Advances in Neural Information Processing Systems 33. (19920-19930). + Advances in Neural Information Processing Systems 33. (19920-19930). (2) https://github.com/pytorch/captum/blob/master/captum/influence/_core/ tracincp_fast_rand_proj.py diff --git a/quanda/explainers/wrappers/dattri_influence.py b/quanda/explainers/wrappers/dattri_influence.py index 803decfe..1cac3a37 100644 --- a/quanda/explainers/wrappers/dattri_influence.py +++ b/quanda/explainers/wrappers/dattri_influence.py @@ -91,16 +91,18 @@ def __init__( train_dataset : torch.utils.data.Dataset Training dataset to be used for the influence computation. loss_func : Callable - Builder for dattri's `AttributionTask` loss, with signature: - ``` - def loss_func( - model: torch.nn.Module, - ) -> Callable[[Dict[str, torch.Tensor], Tuple], torch.Tensor]: - ... - ``` - The returned callable takes `(params, batch)` and returns a + Builder for dattri's ``AttributionTask`` loss, with signature:: + + def loss_func( + model: torch.nn.Module, + ) -> Callable[ + [Dict[str, torch.Tensor], Tuple], torch.Tensor + ]: + ... + + The returned callable takes ``(params, batch)`` and returns a per-sample loss tensor (compatible with - `torch.func.functional_call`). + ``torch.func.functional_call``). attributor_cls : type The dattri attributor class. attributor_kwargs : Dict[str, Any] diff --git a/quanda/explainers/wrappers/kronfluence.py b/quanda/explainers/wrappers/kronfluence.py index 761c6eb6..58459c5d 100644 --- a/quanda/explainers/wrappers/kronfluence.py +++ b/quanda/explainers/wrappers/kronfluence.py @@ -25,8 +25,8 @@ ) from quanda.utils.common import ( CheckpointLoadFunc, - _replace_conv1d_with_linear, process_targets, + replace_conv1d_with_linear, resolve_device, ) from quanda.utils.tasks import TaskLiterals @@ -49,11 +49,11 @@ class Kronfluence(Explainer): ---------- (1) Roger Grosse, Juhan Bae, Cem Anil, Nelson Elhage, Alex Tamkin, Amirhossein Tajdini, Benoit Steiner, - Dustin Li, Esin Durmus, Ethan Perez, Evan Hubinger, Kamilė Lukošiūtė, - Karina Nguyen, Nicholas Joseph, - Sam McCandlish, Jared Kaplan, Samuel R. Bowman. (2023). - "Studying large language model generalization with influence - functions". arXiv preprint arXiv:2308.03296. + Dustin Li, Esin Durmus, Ethan Perez, Evan Hubinger, Kamilė Lukošiūtė, + Karina Nguyen, Nicholas Joseph, + Sam McCandlish, Jared Kaplan, Samuel R. Bowman. (2023). + "Studying large language model generalization with influence + functions". arXiv preprint arXiv:2308.03296. (2) https://github.com/pomonam/kronfluence @@ -204,7 +204,7 @@ def _prepare_model(self) -> nn.Module: """ model_copy = copy.deepcopy(self.model) model_copy.to(self.device) - _replace_conv1d_with_linear(model_copy) + replace_conv1d_with_linear(model_copy) prepared_model = prepare_model(model=model_copy, task=self.task) return prepared_model diff --git a/quanda/explainers/wrappers/representer_points.py b/quanda/explainers/wrappers/representer_points.py index d856668b..bca47e70 100644 --- a/quanda/explainers/wrappers/representer_points.py +++ b/quanda/explainers/wrappers/representer_points.py @@ -178,6 +178,8 @@ def __init__( checkpoints: Optional[Union[str, List[str]]] = None, checkpoints_load_func: Optional[CheckpointLoadFunc] = None, cache_dir: str = "./cache", + activations_cache_dir: Optional[str] = None, + activations_id: Optional[str] = None, features_postprocess: Optional[Callable] = None, lmbd: float = 0.003, epoch: int = 3000, @@ -188,6 +190,7 @@ def __init__( batch_size: int = 32, load_from_disk: bool = True, show_progress: bool = True, + random_init: bool = False, ): """Initialize the RepresenterPoints class. @@ -214,7 +217,21 @@ def __init__( Ignored, for the same reason as ``checkpoints``. Defaults to None. cache_dir : str, optional - The directory to save the cache, defaults to "./cache". + Directory for the trained representer coefficients + (``*_repr_weights.pt``). Depends on training hyperparameters, + so it is safe — and expected — to vary this per run. + Defaults to "./cache". + activations_cache_dir : Optional[str], optional + Directory for cached penultimate-layer activations of the + training set. Activations only depend on + ``(model checkpoint, train_dataset, features_layer)``, so this + can be pointed at a shared location to reuse them across runs + with different hyperparameters. Defaults to ``cache_dir``. + activations_id : Optional[str], optional + Identifier under which activations are stored inside + ``activations_cache_dir``. Should encode the model + dataset + + ``features_layer`` but NOT the training hyperparameters. + Defaults to ``model_id``. features_postprocess : Optional[Callable], optional A postprocessing function for the features, defaults to None. lmbd : float, optional @@ -236,6 +253,10 @@ def __init__( Whether to load the activations from disk, defaults to True. show_progress : bool, optional Whether to show the training progress, defaults to True. + random_init : bool, optional + If True, the initial representer W is initialized randomly + instead of from the trained classifier weights. Defaults to + False. """ logger.info("Initializing Representer Point Selection explainer...") @@ -249,6 +270,8 @@ def __init__( self.model_id = model_id self.cache_dir = cache_dir + self.activations_cache_dir = activations_cache_dir or cache_dir + self.activations_id = activations_id or model_id self.normalize = normalize self.features_layer = features_layer self.classifier_layer = classifier_layer @@ -259,16 +282,19 @@ def __init__( self.epsilon = epsilon self.features_postprocess = features_postprocess self.show_progress = show_progress + self.random_init = random_init self.dataloader = torch.utils.data.DataLoader( self.train_dataset, batch_size=batch_size, shuffle=False ) + os.makedirs(self.cache_dir, exist_ok=True) + os.makedirs(self.activations_cache_dir, exist_ok=True) with default_tensor_type(self.device): act_dataset = AV.generate_dataset_activations( - path=cache_dir, + path=self.activations_cache_dir, model=model, - model_id=model_id, + model_id=self.activations_id, layers=[features_layer], dataloader=self.dataloader, load_from_disk=load_from_disk, @@ -425,6 +451,18 @@ def explain( explanations = torch.gather(explanations, dim=-1, index=indices) return torch.squeeze(explanations) + def _train_step(self, model, optimizer, x, y, N): + """Run a single optimizer step and return loss, phi_loss, grad_loss.""" + optimizer.zero_grad() + (Phi, L2) = model(x, y) + loss = L2 * self.lmbd + Phi / N + phi_loss = (Phi / N).detach().cpu().numpy() + loss.backward() + if model.W.grad is None: + raise ValueError("Gradient is None") + grad_loss = torch.mean(torch.abs(model.W.grad)).item() + return loss, phi_loss, grad_loss + def train(self): """Train the model to obtain the representer point coefficients. @@ -457,6 +495,8 @@ def train(self): w_and_b = torch.concatenate( [weight_linear.T, bias_linear.unsqueeze(0)] ) + if self.random_init: + w_and_b = torch.randn_like(w_and_b) model = RepresenterSoftmax(w_and_b, self.device) x = nn.Parameter(samples_with_bias.to(self.device)) @@ -474,21 +514,14 @@ def train(self): best_W = model.W.data.clone() init_grad = float("inf") + grad_loss = float("inf") for epoch in range(self.epoch): - phi_loss = 0 - optimizer.zero_grad() - (Phi, L2) = model(x, y) - loss = L2 * self.lmbd + Phi / N - phi_loss += (Phi / N).detach().cpu().numpy() - loss.backward() + loss, phi_loss, grad_loss = self._train_step( + model, optimizer, x, y, N + ) temp_W = model.W.data - if model.W.grad is None: - raise ValueError("Gradient is None") - - grad_loss = torch.mean(torch.abs(model.W.grad)).item() - if epoch == 0: init_grad = grad_loss best_W = temp_W @@ -502,6 +535,7 @@ def train(self): "Stopping criteria reached in epoch :{}".format(epoch) ) break + assert model.W.grad is not None self.backtracking_line_search(model, model.W.grad, x, y, loss, N) if self.show_progress: pbar.set_description( @@ -512,6 +546,11 @@ def train(self): pbar.update(1) + if grad_loss == init_grad: + raise ValueError( + "Gradient did not decrease during training. Consider " + "increasing the number of epochs or the learning rate." + ) # calculate w based on the representer theorem's decomposition temp = torch.matmul( x, nn.Parameter(best_W.to(self.device), requires_grad=True) diff --git a/quanda/metrics/base.py b/quanda/metrics/base.py index d777713d..0d134d0b 100644 --- a/quanda/metrics/base.py +++ b/quanda/metrics/base.py @@ -200,14 +200,7 @@ def state_dict(self) -> dict: raise NotImplementedError def load_last_checkpoint(self): - """Load the model from the checkpoint file. - - Parameters - ---------- - checkpoint : str - Path to the checkpoint file. - - """ + """Load the last checkpoint in ``self.checkpoints`` into the model.""" load_last_checkpoint( model=self.model, checkpoints=self.checkpoints, diff --git a/quanda/utils/common.py b/quanda/utils/common.py index fe52c393..87253a36 100644 --- a/quanda/utils/common.py +++ b/quanda/utils/common.py @@ -29,6 +29,150 @@ CheckpointLoadFunc = Callable[[torch.nn.Module, str], Any] +@dataclass +class DatasetSplit(ABC): + """Class to store dynamically named splits (e.g., train, val, test).""" + + splits: Dict[str, torch.Tensor] + + def __getitem__(self, key): + """Get the indices for the specified key.""" + if key not in self.splits: + raise KeyError(f"Key '{key}' not found in splits.") + return self.splits[key] + + def __init__( + self, + splits: Dict[str, torch.Tensor], + ): + """Create a DatasetSplit from a dictionary of indices. + + Parameters + ---------- + splits : Dict[str, torch.Tensor] + A list of indices for the split. + + Returns + ------- + DatasetSplit: An object with a single split named 'default'. + + """ + if not splits: + raise ValueError("splits cannot be empty.") + self.splits = splits + + @classmethod + def split( + cls, n_indices: int, seed: int, split_ratios: Dict[str, float] + ) -> "DatasetSplit": + """Split the indices into named sets based on split_ratios. + + Parameters + ---------- + n_indices : int + Total number of indices to split. + seed : int + Random seed for reproducibility. + split_ratios : Dict[str, float] + A dictionary where keys are split names (e.g., 'train', 'val', + 'test') and values are the ratios for each split. + + Returns + ------- + DatasetSplit: An object with keys corresponding to split_ratios. + + """ + if not split_ratios: + raise ValueError("split_ratios cannot be empty.") + + total_ratio = sum(split_ratios.values()) + if total_ratio > 1.0: + raise ValueError("Sum of split ratios must not exceed 1.0") + + torch.manual_seed(seed) + indices = torch.randperm(n_indices) + + split_indices = {} + start = 0 + for i, (name, ratio) in enumerate(split_ratios.items()): + end = start + int(ratio * n_indices) + split_indices[name] = indices[start:end] + start = end + + return cls(splits=split_indices) + + @classmethod + def load(cls, path: str, name: str) -> "DatasetSplit": + """Load the split from disk.""" + with open(os.path.join(path, name), "r") as f: + data = yaml.safe_load(f) + splits = {k: torch.tensor(v) for k, v in data.items()} + return cls(splits=splits) + + def save(self, path: str, name: str) -> None: + """Save the split to disk atomically.""" + os.makedirs(path, exist_ok=True) + data = {k: v.tolist() for k, v in self.splits.items()} + final_path = os.path.join(path, name) + tmp_path = f"{final_path}.tmp.{os.getpid()}" + with open(tmp_path, "w") as f: + yaml.safe_dump(data, f) + os.replace(tmp_path, final_path) + + def to_dict(self) -> Dict[str, torch.Tensor]: + """Convert splits to dictionary.""" + return self.splits + + @staticmethod + def exists(path: str, name: str) -> bool: + """Check if split file exists.""" + return os.path.exists(os.path.join(path, name)) + + +def resolve_config(config: Union[dict, str]) -> dict: + """Resolve a benchmark ``config`` into a dict. + + Parameters + ---------- + config : Union[dict, str] + Either a config dict (passed through unchanged), a registered + ``bench_id`` (resolved via + :data:`quanda.benchmarks.resources.config_map.config_map`), or + a path to a benchmark YAML file. + + Returns + ------- + dict + The resolved benchmark configuration. + + Raises + ------ + TypeError + If ``config`` is not a ``dict`` or ``str``, or if the loaded + YAML does not parse to a mapping. + + """ + if isinstance(config, dict): + return config + if isinstance(config, str): + # Lazy import to avoid a hard dep from utils → benchmarks. + from quanda.benchmarks.resources.config_map import config_map + + path = str(config_map[config]) if config in config_map else config + with open(path, "r") as f: + cfg = yaml.safe_load(f) + if not isinstance(cfg, dict): + raise TypeError( + f"YAML at {path} did not parse to a dict (got " + f"{type(cfg).__name__})." + ) + return cfg + raise TypeError( + f"config must be a dict, a registered bench_id, or a YAML path; " + f"got {type(config).__name__}." + ) + + def chunked_logits( model: torch.nn.Module, inputs: Any, @@ -57,25 +201,6 @@ def _call(batch: Any) -> torch.Tensor: return torch.cat([_call(chunk) for chunk in chunks], dim=0) -def _get_module_from_name(model: torch.nn.Module, layer_name: str) -> Any: - """Get a module from a model by name. - - Parameters - ---------- - model : torch.nn.Module - The model to extract the module from. - layer_name : str - The name of the module to extract. - - Returns - ------- - Any - The module extracted from the model. - - """ - return reduce(getattr, layer_name.split("."), model) - - def get_parent_module_from_name( model: torch.nn.Module, layer_name: str ) -> Any: @@ -238,11 +363,6 @@ def _load_flexible_state_dict( if isinstance(device, str): device = torch.device(device) - # torch.load on the same .pth is called repeatedly by callers like - # TracInCPFast (once per test batch). A single transient read error - # from the kernel ("PytorchStreamReader ... file read failed") is - # enough to kill a multi-hour run, so retry a few times before - # giving up. last_err: Optional[Exception] = None for attempt in range(3): try: @@ -487,110 +607,10 @@ def load_last_checkpoint( checkpoints_load_func(model, checkpoints[-1]) -@dataclass -class DatasetSplit(ABC): - """Class to store dynamically named splits (e.g., train, val, test).""" - - splits: Dict[str, torch.Tensor] - - def __getitem__(self, key): - """Get the indices for the specified key.""" - if key not in self.splits: - raise KeyError(f"Key '{key}' not found in splits.") - return self.splits[key] - - def __init__( - self, - splits: Dict[str, torch.Tensor], - ): - """Create a DatasetSplit from a dictionary of indices. - - Parameters - ---------- - splits : Dict[str, torch.Tensor] - A list of indices for the split. - - Returns - ------- - DatasetSplit: An object with a single split named 'default'. - - """ - if not splits: - raise ValueError("splits cannot be empty.") - self.splits = splits - - @classmethod - def split( - cls, n_indices: int, seed: int, split_ratios: Dict[str, float] - ) -> "DatasetSplit": - """Split the indices into named sets based on split_ratios. - - Parameters - ---------- - n_indices : int - Total number of indices to split. - seed : int - Random seed for reproducibility. - split_ratios : Dict[str, float] - A dictionary where keys are split names (e.g., 'train', 'val', - 'test') and values are the ratios for each split. - - Returns - ------- - DatasetSplit: An object with keys corresponding to split_ratios. - - """ - if not split_ratios: - raise ValueError("split_ratios cannot be empty.") - - total_ratio = sum(split_ratios.values()) - if total_ratio > 1.0: - raise ValueError("Sum of split ratios must not exceed 1.0") - - torch.manual_seed(seed) - indices = torch.randperm(n_indices) - - split_indices = {} - start = 0 - for i, (name, ratio) in enumerate(split_ratios.items()): - end = start + int(ratio * n_indices) - split_indices[name] = indices[start:end] - start = end - - return cls(splits=split_indices) - - @classmethod - def load(cls, path: str, name: str) -> "DatasetSplit": - """Load the split from disk.""" - with open(os.path.join(path, name), "r") as f: - data = yaml.safe_load(f) - splits = {k: torch.tensor(v) for k, v in data.items()} - return cls(splits=splits) - - def save(self, path: str, name: str) -> None: - """Save the split to disk atomically.""" - os.makedirs(path, exist_ok=True) - data = {k: v.tolist() for k, v in self.splits.items()} - final_path = os.path.join(path, name) - tmp_path = f"{final_path}.tmp.{os.getpid()}" - with open(tmp_path, "w") as f: - yaml.safe_dump(data, f) - os.replace(tmp_path, final_path) - - def to_dict(self) -> Dict[str, torch.Tensor]: - """Convert splits to dictionary.""" - return self.splits - - @staticmethod - def exists(path: str, name: str) -> bool: - """Check if split file exists.""" - return os.path.exists(os.path.join(path, name)) - - _DEFAULT_REPR_RE = re.compile(r" object at 0x[0-9a-fA-F]+>") -def _stable_repr(obj: Any) -> str: +def stable_repr(obj: Any) -> str: """Process-stable string form of ``obj`` for hashing/serialization.""" if callable(obj) and hasattr(obj, "__qualname__"): module = getattr(obj, "__module__", "") or "" @@ -604,12 +624,12 @@ def _stable_repr(obj: Any) -> str: fq = f"{module}.{qualname}" if module else qualname attrs = getattr(obj, "__dict__", None) if attrs: - inner = json.dumps(attrs, sort_keys=True, default=_stable_repr) + inner = json.dumps(attrs, sort_keys=True, default=stable_repr) return f"{fq}({inner})" return fq -def _subsample_indices(n: int, max_n: Optional[int], seed: int) -> List[int]: +def subsample_indices(n: int, max_n: Optional[int], seed: int) -> List[int]: """Deterministic subsample of ``range(n)`` matching ``_subsample_dataset``. Returns the full ``range(n)`` (as a list) when no subsampling is needed. @@ -619,7 +639,7 @@ def _subsample_indices(n: int, max_n: Optional[int], seed: int) -> List[int]: return sorted(random.Random(seed).sample(range(n), max_n)) -def _subsample_dataset( +def subsample_dataset( dataset: torch.utils.data.Dataset, max_n: Optional[int], seed: int, @@ -637,13 +657,13 @@ def _subsample_dataset( n = len(dataset) # type: ignore[arg-type] if max_n >= n: return dataset - indices = _subsample_indices(n, max_n, seed) + indices = subsample_indices(n, max_n, seed) if hasattr(dataset, "filtered"): return dataset.filtered(indices) return torch.utils.data.Subset(dataset, indices) -def _replace_conv1d_with_linear(model: nn.Module) -> None: +def replace_conv1d_with_linear(model: nn.Module) -> None: """Swap HF ``Conv1D`` modules in-place with ``nn.Linear`` equivalents. HF GPT-2 uses ``Conv1D`` (a transposed Linear) for attention/MLP @@ -652,7 +672,7 @@ def _replace_conv1d_with_linear(model: nn.Module) -> None: """ for name, module in model.named_children(): if len(list(module.children())) > 0: - _replace_conv1d_with_linear(module) + replace_conv1d_with_linear(module) if module.__class__.__name__ == "Conv1D": weight = cast(torch.Tensor, module.weight) bias = cast(torch.Tensor, module.bias) diff --git a/quanda/utils/datasets/dataset_handlers.py b/quanda/utils/datasets/dataset_handlers.py index e753e510..a26b31cf 100644 --- a/quanda/utils/datasets/dataset_handlers.py +++ b/quanda/utils/datasets/dataset_handlers.py @@ -421,7 +421,7 @@ def __init__( self.label_key = label_key def collate(self, samples: List[Dict[str, Any]]) -> List[torch.Tensor]: - """Stack HF dict samples into a list [*input_keys, label_key]. + """Stack HF dict samples into a list ``[*input_keys, label_key]``. Projects each sample onto the required keys *before* collation so that non-numeric columns (e.g. raw ``"sentence"``/``"hypothesis"`` diff --git a/quanda/utils/datasets/transformed/metadata.py b/quanda/utils/datasets/transformed/metadata.py index 930fdcc3..6a18136d 100644 --- a/quanda/utils/datasets/transformed/metadata.py +++ b/quanda/utils/datasets/transformed/metadata.py @@ -269,7 +269,7 @@ def resolve( cls, spec: dict, metadata_dir: str, - load_meta_from_disk: bool, + load_fresh: bool = False, ) -> "ClassMapping": """Resolve a ``class_to_group`` config spec to a ``ClassMapping``. @@ -277,6 +277,8 @@ def resolve( - ``{0: g0, 1: g1, ...}`` — direct mapping (integer keys). - ``{ctg_filename, n_classes, n_groups, seed?}`` — file-backed; load if exists, otherwise generate from ``seed`` and save. + ``load_fresh=True`` forces regeneration even if a cached file + is present. """ if spec and all(isinstance(k, int) for k in spec.keys()): mapping = {int(k): int(v) for k, v in spec.items()} @@ -291,13 +293,7 @@ def resolve( n_groups = int(spec["n_groups"]) seed = int(spec.get("seed", 42)) - if load_meta_from_disk: - if not cls.exists(metadata_dir, ctg_filename): - raise FileNotFoundError( - f"Class mapping '{ctg_filename}' not found in " - f"{metadata_dir}. Re-run with " - f"load_meta_from_disk=False to regenerate it." - ) + if not load_fresh and cls.exists(metadata_dir, ctg_filename): return cls.load(metadata_dir, ctg_filename) mapping = cls._generate(n_classes, n_groups, seed) diff --git a/scripts/PLOT_ALL.sh b/scripts/PLOT_ALL.sh deleted file mode 100644 index d51d484c..00000000 --- a/scripts/PLOT_ALL.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" - -bash "$DIR/mnsit_lenet_bench/plot_mnist.sh" -bash "$DIR/cifar_resnet9_bench/plot_cifar.sh" -bash "$DIR/bert_qnli_bench/plot_qnli.sh" -bash "$DIR/gpt2_trex_bench/plot_gpt2.sh" -bash "$DIR/awa2_resnet50_bench/plot_awa2.sh" diff --git a/scripts/REPRODUCE_ALL.sh b/scripts/REPRODUCE_ALL.sh new file mode 100755 index 00000000..0f9031c7 --- /dev/null +++ b/scripts/REPRODUCE_ALL.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# The following script is not meant to be run and only serves as a representation of the sequence of steps performed to train and evaluate all benchmarks. +set -e + +DIR="$(dirname "$0")" + +# 1) Train benchmarks (with hyperparam sweep) +bash "$DIR/mnsit_lenet_bench/train_mnist.sh" +bash "$DIR/cifar_resnet9_bench/train_cifar.sh" +bash "$DIR/awa2_resnet50_bench/train_awa2.sh" +bash "$DIR/bert_qnli_bench/train_qnli.sh" + +# 2) Train LDS models +bash "$DIR/mnsit_lenet_bench/train_mnist_lds.sh" +bash "$DIR/cifar_resnet9_bench/train_cifar_lds.sh" +bash "$DIR/awa2_resnet50_bench/train_awa2_lds.sh" +bash "$DIR/bert_qnli_bench/train_qnli_lds.sh" + +# 3) Collect LDS submodel logits +bash "$DIR/awa2_resnet50_bench/compute_lds_subset_logits_awa2.sh" \ + --start 0 --end 100 \ + --batch-size 64 --max-eval-n 1000 --eval-seed 42 \ + --inference-batch-size 64 --device cuda:0 +bash "$DIR/bert_qnli_bench/compute_lds_subset_logits_qnli.sh" \ + --start 0 --end 100 \ + --batch-size 8 --max-eval-n 1000 --eval-seed 42 \ + --inference-batch-size 32 --device cuda:0 + +# 4) Run eval +bash "$DIR/mnsit_lenet_bench/eval_mnist_pt1.sh" +bash "$DIR/mnsit_lenet_bench/eval_mnist_pt2.sh" +bash "$DIR/cifar_resnet9_bench/eval_cifar_pt1.sh" +bash "$DIR/cifar_resnet9_bench/eval_cifar_pt2.sh" +bash "$DIR/awa2_resnet50_bench/eval_awa2_pt1.sh" +bash "$DIR/awa2_resnet50_bench/eval_awa2_pt2.sh" +bash "$DIR/bert_qnli_bench/eval_qnli.sh" +bash "$DIR/gpt2_trex_bench/eval_mrr.sh" +bash "$DIR/gpt2_trex_bench/eval_recall_at_k.sh" +bash "$DIR/gpt2_trex_bench/eval_tail_patch.sh" diff --git a/scripts/awa2_resnet50_bench/bench_defs.sh b/scripts/awa2_resnet50_bench/bench_defs.sh index 1c4186ba..043b16e8 100755 --- a/scripts/awa2_resnet50_bench/bench_defs.sh +++ b/scripts/awa2_resnet50_bench/bench_defs.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Benchmark definitions: dataset params and sweep hyperparams for AwA2 / ResNet50. -# Source this file, then use: ${BENCH_PARAMS[Name]} and ${BENCH_SWEEP[Name]} declare -A BENCH_PARAMS declare -A BENCH_SWEEP diff --git a/scripts/awa2_resnet50_bench/compute_lds_subset_logits_awa2_all.sh b/scripts/awa2_resnet50_bench/compute_lds_subset_logits_awa2_all.sh deleted file mode 100755 index 95f21377..00000000 --- a/scripts/awa2_resnet50_bench/compute_lds_subset_logits_awa2_all.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -M=100 -STRIDE=10 - -BATCH_SIZE=64 -MAX_EVAL_N=1000 -EVAL_SEED=42 -INFERENCE_BATCH_SIZE=64 -DEVICE=cuda:0 - -for start in $(seq 0 "$STRIDE" "$((M - STRIDE))"); do - end=$((start + STRIDE)) - sbatch slurm/slurm_job.sbatch \ - scripts/awa2_resnet50_bench/compute_lds_subset_logits_awa2.sh \ - --start "$start" --end "$end" \ - --batch-size "$BATCH_SIZE" \ - --max-eval-n "$MAX_EVAL_N" \ - --eval-seed "$EVAL_SEED" \ - --inference-batch-size "$INFERENCE_BATCH_SIZE" \ - --device "$DEVICE" -done diff --git a/scripts/awa2_resnet50_bench/eval_all_awa2.sh b/scripts/awa2_resnet50_bench/eval_all_awa2.sh deleted file mode 100644 index 309d5691..00000000 --- a/scripts/awa2_resnet50_bench/eval_all_awa2.sh +++ /dev/null @@ -1,7 +0,0 @@ -set -euo pipefail - -jid1=$(sbatch --parsable slurm/slurm_job.sbatch scripts/awa2_resnet50_bench/eval_awa2_pt1.sh) -[[ -n $jid1 ]] || { echo "pt1 submission failed"; exit 1; } - -sbatch --dependency=afterok:$jid1 slurm/slurm_job.sbatch scripts/awa2_resnet50_bench/eval_awa2_pt2.sh - diff --git a/scripts/awa2_resnet50_bench/eval_awa2_arnoldi.sh b/scripts/awa2_resnet50_bench/eval_awa2_arnoldi.sh deleted file mode 100755 index 809e6d7f..00000000 --- a/scripts/awa2_resnet50_bench/eval_awa2_arnoldi.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Worker mode: run a single (method, benchmark) pair. -if [ "${1:-}" = "--run" ]; then - source "$(dirname "$0")/eval_defs.sh" - EVAL_CONFIG_NAME="awa2_resnet50" - PARALLEL=false - methods=("$2") - benchmarks=("$3") - source "$(dirname "$0")/../eval.sh" - exit -fi - -# Submitter mode: one sbatch job per (method, benchmark); -# every pt2 job waits on all pt1 jobs. -methods=( - arnoldi -) - -bench_pt1=( - awa2_class_detection - #awa2_subclass_detection - #awa2_shortcut_detection - #awa2_mixed_datasets - #awa2_mislabeling_detection -) - -bench_pt2=( - awa2_linear_datamodeling - awa2_top_k_cardinality - awa2_model_randomization -) - -pt1_jids=() -for method in "${methods[@]}"; do - for bench in "${bench_pt1[@]}"; do - jid=$(sbatch --parsable slurm/slurm_job.sbatch "$0" --run "$method" "$bench") - [[ -n $jid ]] || { echo "$method $bench pt1 submission failed"; exit 1; } - pt1_jids+=("$jid") - done -done - -dep=$(IFS=:; echo "${pt1_jids[*]}") -for method in "${methods[@]}"; do - for bench in "${bench_pt2[@]}"; do - sbatch --dependency=afterok:$dep slurm/slurm_job.sbatch "$0" --run "$method" "$bench" - done -done diff --git a/scripts/awa2_resnet50_bench/eval_awa2_local_arnoldi.sh b/scripts/awa2_resnet50_bench/eval_awa2_local_arnoldi.sh deleted file mode 100755 index 2a184d19..00000000 --- a/scripts/awa2_resnet50_bench/eval_awa2_local_arnoldi.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -source "$(dirname "$0")/eval_defs.sh" - -EVAL_CONFIG_NAME="awa2_resnet50" - -benchmarks=( - awa2_linear_datamodeling - awa2_top_k_cardinality - awa2_model_randomization -) - -methods=( - #similarity - #representer_points - #tracincpfast - arnoldi - #trak - #random -) -PARALLEL=false - -source "$(dirname "$0")/../eval.sh" "$@" diff --git a/scripts/awa2_resnet50_bench/eval_awa2_pt1.sh b/scripts/awa2_resnet50_bench/eval_awa2_pt1.sh index dd35cae1..8c928016 100755 --- a/scripts/awa2_resnet50_bench/eval_awa2_pt1.sh +++ b/scripts/awa2_resnet50_bench/eval_awa2_pt1.sh @@ -5,11 +5,19 @@ source "$(dirname "$0")/eval_defs.sh" EVAL_CONFIG_NAME="awa2_resnet50" benchmarks=( + awa2_class_detection + awa2_subclass_detection + awa2_shortcut_detection awa2_mixed_datasets ) methods=( + similarity + representer_points + tracincpfast arnoldi + trak + random ) PARALLEL=false diff --git a/scripts/awa2_resnet50_bench/eval_awa2_pt2.sh b/scripts/awa2_resnet50_bench/eval_awa2_pt2.sh index 10228d8c..416e27fa 100755 --- a/scripts/awa2_resnet50_bench/eval_awa2_pt2.sh +++ b/scripts/awa2_resnet50_bench/eval_awa2_pt2.sh @@ -7,11 +7,18 @@ EVAL_CONFIG_NAME="awa2_resnet50" benchmarks=( awa2_mixed_datasets + awa2_top_k_cardinality + awa2_model_randomization + awa2_mislabeling_detection + awa2_linear_datamodeling ) methods=( similarity representer_points + tracincpfast + arnoldi + trak random ) PARALLEL=false @@ -19,15 +26,3 @@ PARALLEL=false source "$(dirname "$0")/../eval.sh" "$@" -methods=( - tracincpfast -) - -source "$(dirname "$0")/../eval.sh" "$@" - - -methods=( - trak -) - -source "$(dirname "$0")/../eval.sh" "$@" \ No newline at end of file diff --git a/scripts/awa2_resnet50_bench/eval_awa2_tracin.sh b/scripts/awa2_resnet50_bench/eval_awa2_tracin.sh deleted file mode 100755 index 9083384c..00000000 --- a/scripts/awa2_resnet50_bench/eval_awa2_tracin.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -source "$(dirname "$0")/eval_defs.sh" - -EVAL_CONFIG_NAME="awa2_resnet50" - -benchmarks=( - #awa2_class_detection - #awa2_subclass_detection - #awa2_shortcut_detection - awa2_mixed_datasets - awa2_linear_datamodeling - awa2_top_k_cardinality - awa2_model_randomization - awa2_mislabeling_detection -) - -methods=( - #similarity - #representer_points - tracincpfast - #arnoldi - #trak - #random -) -PARALLEL=false - -source "$(dirname "$0")/../eval.sh" "$@" diff --git a/scripts/awa2_resnet50_bench/eval_awa2_trak.sh b/scripts/awa2_resnet50_bench/eval_awa2_trak.sh deleted file mode 100755 index 32780636..00000000 --- a/scripts/awa2_resnet50_bench/eval_awa2_trak.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -source "$(dirname "$0")/eval_defs.sh" - -EVAL_CONFIG_NAME="awa2_resnet50" - -benchmarks=( - awa2_class_detection - awa2_subclass_detection - awa2_shortcut_detection - awa2_mixed_datasets - awa2_linear_datamodeling - awa2_top_k_cardinality - awa2_model_randomization - awa2_mislabeling_detection -) - -methods=( - #similarity - #representer_points - trak - #arnoldi - #trak - #random -) -PARALLEL=false - -source "$(dirname "$0")/../eval.sh" "$@" diff --git a/scripts/awa2_resnet50_bench/eval_defs.sh b/scripts/awa2_resnet50_bench/eval_defs.sh index 2518bd42..2d5ab711 100755 --- a/scripts/awa2_resnet50_bench/eval_defs.sh +++ b/scripts/awa2_resnet50_bench/eval_defs.sh @@ -1,12 +1,11 @@ #!/bin/bash -# Explainer sweep definitions for AwA2 ResNet50 benchmark evaluation. declare -A EXPL_SWEEP EXPL_SWEEP[similarity]="explainer.kwargs.layers=flatten explainer.kwargs.batch_size=128 device=cuda:0 hydra.launcher.n_jobs=1 batch_size=128" -EXPL_SWEEP[representer_points]="explainer.kwargs.features_layer=flatten explainer.kwargs.classifier_layer=fc explainer.kwargs.batch_size=128 device=cuda:0 explainer.kwargs.normalize=true,false hydra.launcher.n_jobs=1 batch_size=128" +EXPL_SWEEP[representer_points]="explainer.kwargs.features_layer=flatten explainer.kwargs.classifier_layer=fc explainer.kwargs.batch_size=128 device=cuda:1 explainer.kwargs.normalize=true,false hydra.launcher.n_jobs=1 batch_size=128 +explainer.kwargs.random_init=true" EXPL_SWEEP[tracincpfast]="explainer.kwargs.batch_size=256 batch_size=256 device=cuda:0" EXPL_SWEEP[arnoldi]="explainer.kwargs.layers=[fc] explainer.kwargs.projection_dim=50 explainer.kwargs.arnoldi_dim=100 explainer.kwargs.batch_size=256 +explainer.kwargs.precompute_data_ratio=0.1 device=cuda:1 hydra.launcher.n_jobs=1" -EXPL_SWEEP[trak]="explainer.kwargs.proj_dim=1024,2048 explainer.kwargs.batch_size=32 device=cuda:0 hydra.launcher.n_jobs=1" +EXPL_SWEEP[trak]="explainer.kwargs.proj_dim=1024,2048,4096 explainer.kwargs.batch_size=32 device=cuda:0 hydra.launcher.n_jobs=1" EXPL_SWEEP[random]="device=cuda:0 explainer.kwargs.seed=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40 hydra.launcher.n_jobs=5 batch_size=128 device=cuda:1" EXPL_SWEEP[kronfluence]="explainer.kwargs.task_module._target_=quanda.explainers.wrappers.kronfluence_tasks.ImageClassificationTask explainer.kwargs.task_module.tracked_modules=[layer4.2.conv3,fc] +explainer.kwargs.score_args._target_=kronfluence.arguments.ScoreArguments +explainer.kwargs.score_args.use_measurement_for_self_influence=true explainer.kwargs.batch_size=64 device=cuda:0 batch_size=1000 inference_batch_size=64 hydra.launcher.n_jobs=1" diff --git a/scripts/awa2_resnet50_bench/plot_awa2.sh b/scripts/awa2_resnet50_bench/plot_awa2.sh deleted file mode 100755 index 1edc4b6e..00000000 --- a/scripts/awa2_resnet50_bench/plot_awa2.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" -ROOT="$(realpath "$DIR/../..")" -RESULTS_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_results/awa2" -OUT="${OUT:-$DIR/bar_rank.png}" - -python "$DIR/../plot_results.py" \ - --results-dir "$RESULTS_DIR" \ - --config "$DIR/awa2_plot_config.json" \ - --out "$OUT" \ - "$@" diff --git a/scripts/awa2_resnet50_bench/train_awa2.sh b/scripts/awa2_resnet50_bench/train_awa2.sh index a5a93140..a780e8e3 100755 --- a/scripts/awa2_resnet50_bench/train_awa2.sh +++ b/scripts/awa2_resnet50_bench/train_awa2.sh @@ -6,11 +6,13 @@ CONFIG_NAME="awa2_resnet50" CONFIG_MAP_PREFIX="awa2" benchmarks=( - #ClassDetection - #SubclassDetection + ClassDetection + SubclassDetection MixedDatasets - #ShortcutDetection - #MislabelingDetection + ShortcutDetection + MislabelingDetection + LDS ) +PARALLEL=false source "$(dirname "$0")/../train.sh" "$@" diff --git a/scripts/awa2_resnet50_bench/train_awa2_lds.sh b/scripts/awa2_resnet50_bench/train_awa2_lds.sh index 26e1ceb3..481c3f82 100755 --- a/scripts/awa2_resnet50_bench/train_awa2_lds.sh +++ b/scripts/awa2_resnet50_bench/train_awa2_lds.sh @@ -9,4 +9,5 @@ source "$(dirname "$0")/../train_lds.sh" \ --n-lds-parallel 1 \ --hf-push-sleep 10 \ --gpu-split false \ - "$@" + --start 0 \ + --end 100 diff --git a/scripts/awa2_resnet50_bench/train_awa2_lds_all.sh b/scripts/awa2_resnet50_bench/train_awa2_lds_all.sh deleted file mode 100755 index f9faa7c9..00000000 --- a/scripts/awa2_resnet50_bench/train_awa2_lds_all.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -M=100 -STRIDE=10 - -for start in $(seq 0 "$STRIDE" "$((M - STRIDE))"); do - end=$((start + STRIDE - 1)) - sbatch slurm/slurm_job.sbatch \ - scripts/awa2_resnet50_bench/train_awa2_lds.sh \ - --start "$start" --end "$end" -done diff --git a/scripts/awa2_resnet50_bench/train_awa2_per_bench.sh b/scripts/awa2_resnet50_bench/train_awa2_per_bench.sh deleted file mode 100755 index 291ab955..00000000 --- a/scripts/awa2_resnet50_bench/train_awa2_per_bench.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Train a single benchmark. Hydra n_jobs is capped by MAX_PARALLEL -# (default 6 — sized for one 40GB GPU at batch_size=64; lower if you -# hit OOM, raise on a bigger GPU). The sweep cardinality (n_trials) is -# whatever bench_defs.sh sets per benchmark; optuna runs them in -# batches of MAX_PARALLEL. -# -# Usage: train_awa2_per_bench.sh BENCH_NAME [extra train.sh args] -# Env: MAX_PARALLEL — override the parallelism cap. - -source "$(dirname "$0")/bench_defs.sh" - -CONFIG_NAME="awa2_resnet50" -CONFIG_MAP_PREFIX="awa2" - -BENCH="$1" -shift - -MAX_PARALLEL=${MAX_PARALLEL:-3} - -if [ -z "${BENCH_PARAMS[$BENCH]+x}" ]; then - echo "Error: unknown benchmark '$BENCH'" >&2 - exit 1 -fi - -BENCH_SWEEP[$BENCH]="${BENCH_SWEEP[$BENCH]} hydra.launcher.n_jobs=${MAX_PARALLEL} hydra.sweeper.n_jobs=${MAX_PARALLEL}" - -benchmarks=("$BENCH") - -source "$(dirname "$0")/../train.sh" "$@" diff --git a/scripts/awa2_resnet50_bench/train_awa2_per_bench_all.sh b/scripts/awa2_resnet50_bench/train_awa2_per_bench_all.sh deleted file mode 100755 index caee94fc..00000000 --- a/scripts/awa2_resnet50_bench/train_awa2_per_bench_all.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -BENCHMARKS=( - ClassDetection - SubclassDetection - MixedDatasets - ShortcutDetection - MislabelingDetection -) - -for bench in "${BENCHMARKS[@]}"; do - sbatch slurm/slurm_job.sbatch \ - scripts/awa2_resnet50_bench/train_awa2_per_bench.sh \ - "$bench" -done - \ No newline at end of file diff --git a/scripts/awa2_resnet50_bench/train_awa2_pipeline_all.sh b/scripts/awa2_resnet50_bench/train_awa2_pipeline_all.sh deleted file mode 100755 index b54708ec..00000000 --- a/scripts/awa2_resnet50_bench/train_awa2_pipeline_all.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# Chain two fan-outs with SLURM dependencies: -# 1. LDS subset training (M/STRIDE jobs) -# 2. LDS subset logit computation — runs after stage 1 succeeds -# Any failure in a stage cancels the dependent stages via --kill-on-invalid-dep. - -set -euo pipefail - -M=100 -STRIDE=1 - -# ---- Stage 1: LDS subset training ------------------------------------------ -stage2_ids=() -for start in $(seq 0 "$STRIDE" "$((M - STRIDE))"); do - end=$((start + STRIDE - 1)) - jid=$(sbatch --parsable \ - slurm/slurm_job.sbatch \ - scripts/awa2_resnet50_bench/train_awa2_lds.sh \ - --start "$start" --end "$end") - stage2_ids+=("$jid") -done -dep2=$(IFS=:; echo "${stage2_ids[*]}") - -# ---- Stage 2: compute LDS subset logits ------------------------------------ -BATCH_SIZE=64 -MAX_EVAL_N=1000 -EVAL_SEED=42 -INFERENCE_BATCH_SIZE=64 -DEVICE=cuda:0 - -stage3_ids=() -for start in $(seq 0 "$STRIDE" "$((M - STRIDE))"); do - end=$((start + STRIDE)) - jid=$(sbatch --parsable \ - --dependency=afterok:"$dep2" --kill-on-invalid-dep=yes \ - slurm/slurm_job.sbatch \ - scripts/awa2_resnet50_bench/compute_lds_subset_logits_awa2.sh \ - --start "$start" --end "$end" \ - --batch-size "$BATCH_SIZE" \ - --max-eval-n "$MAX_EVAL_N" \ - --eval-seed "$EVAL_SEED" \ - --inference-batch-size "$INFERENCE_BATCH_SIZE" \ - --device "$DEVICE") - stage3_ids+=("$jid") -done - -echo "Stage 1 (LDS train): ${stage2_ids[*]}" -echo "Stage 2 (LDS logits): ${stage3_ids[*]}" diff --git a/scripts/bert_qnli_bench/bench_defs.sh b/scripts/bert_qnli_bench/bench_defs.sh index 34de087c..7ffa1568 100755 --- a/scripts/bert_qnli_bench/bench_defs.sh +++ b/scripts/bert_qnli_bench/bench_defs.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Benchmark definitions: dataset params and sweep hyperparams for QNLI / BERT. -# Source this file, then use: ${BENCH_PARAMS[Name]} and ${BENCH_SWEEP[Name]} declare -A BENCH_PARAMS declare -A BENCH_SWEEP diff --git a/scripts/bert_qnli_bench/compute_lds_subset_logits_qnli_all.sh b/scripts/bert_qnli_bench/compute_lds_subset_logits_qnli_all.sh deleted file mode 100755 index 3662a7be..00000000 --- a/scripts/bert_qnli_bench/compute_lds_subset_logits_qnli_all.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -M=100 -STRIDE=10 - -BATCH_SIZE=8 -MAX_EVAL_N=1000 -EVAL_SEED=42 -INFERENCE_BATCH_SIZE=32 -DEVICE=cuda:0 - -for start in $(seq 0 "$STRIDE" "$((M - STRIDE))"); do - end=$((start + STRIDE)) - sbatch slurm/slurm_job.sbatch \ - scripts/bert_qnli_bench/compute_lds_subset_logits_qnli.sh \ - --start "$start" --end "$end" \ - --batch-size "$BATCH_SIZE" \ - --max-eval-n "$MAX_EVAL_N" \ - --eval-seed "$EVAL_SEED" \ - --inference-batch-size "$INFERENCE_BATCH_SIZE" \ - --device "$DEVICE" -done diff --git a/scripts/bert_qnli_bench/eval_defs.sh b/scripts/bert_qnli_bench/eval_defs.sh index c4936294..09699d4b 100755 --- a/scripts/bert_qnli_bench/eval_defs.sh +++ b/scripts/bert_qnli_bench/eval_defs.sh @@ -3,8 +3,8 @@ declare -A EXPL_SWEEP EXPL_SWEEP[similarity]="explainer.kwargs.layers=dropout explainer.kwargs.similarity_metric.path=quanda.utils.functions.cosine_similarity,quanda.utils.functions.dot_product_similarity +explainer.kwargs.task=text_classification device=cuda:0 batch_size=32" -EXPL_SWEEP[trak]="explainer.kwargs.proj_dim=2048 explainer.kwargs.lambda_reg=1e-5 +explainer.kwargs.task=text_classification device=cuda:0 explainer.kwargs.batch_size=8" +EXPL_SWEEP[trak]="explainer.kwargs.proj_dim=2048 +explainer.kwargs.task=text_classification device=cuda:0 explainer.kwargs.batch_size=8 +explainer.kwargs.random_init=true,false" EXPL_SWEEP[dattri_tracin]="+explainer.kwargs.task=text_classification +explainer.kwargs.layer_name=[classifier.weight,classifier.bias] device=cuda:0 explainer.kwargs.batch_size=8 batch_size=1000" EXPL_SWEEP[random]="device=cuda:1 explainer.kwargs.seed=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40 hydra.launcher.n_jobs=5" EXPL_SWEEP[kronfluence]="explainer.kwargs.task_module.tracked_modules=[bert.pooler.dense,classifier] +explainer.kwargs.score_args._target_=kronfluence.arguments.ScoreArguments +explainer.kwargs.score_args.use_measurement_for_self_influence=true +explainer.kwargs.task=text_classification device=cuda:0 batch_size=1000 inference_batch_size=32" -EXPL_SWEEP[representer_points]="explainer.kwargs.features_layer=dropout explainer.kwargs.classifier_layer=classifier explainer.kwargs.normalize=true +explainer.kwargs.task=text_classification explainer.kwargs.batch_size=32 device=cuda:0" \ No newline at end of file +EXPL_SWEEP[representer_points]="explainer.kwargs.features_layer=dropout explainer.kwargs.classifier_layer=classifier explainer.kwargs.normalize=true,false +explainer.kwargs.task=text_classification explainer.kwargs.batch_size=32 device=cuda:1 +explainer.kwargs.random_init=true,false" \ No newline at end of file diff --git a/scripts/bert_qnli_bench/plot_qnli.sh b/scripts/bert_qnli_bench/plot_qnli.sh deleted file mode 100755 index 22ef0b3d..00000000 --- a/scripts/bert_qnli_bench/plot_qnli.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" -ROOT="$(realpath "$DIR/../..")" -RESULTS_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_results/qnli" -OUT="${OUT:-$DIR/bar_rank.png}" - -python "$DIR/../plot_results.py" \ - --results-dir "$RESULTS_DIR" \ - --config "$DIR/qnli_plot_config.json" \ - --out "$OUT" \ - "$@" diff --git a/scripts/bert_qnli_bench/train_qnli.sh b/scripts/bert_qnli_bench/train_qnli.sh index 949af5b2..3300891a 100755 --- a/scripts/bert_qnli_bench/train_qnli.sh +++ b/scripts/bert_qnli_bench/train_qnli.sh @@ -6,7 +6,10 @@ CONFIG_NAME="bert_qnli" CONFIG_MAP_PREFIX="qnli" benchmarks=( + ClassDetection MixedDatasets + MislabelingDetection + LDS ) source "$(dirname "$0")/../train.sh" --parallel false --train-only false "$@" diff --git a/scripts/bert_qnli_bench/train_qnli_lds.sh b/scripts/bert_qnli_bench/train_qnli_lds.sh index bcb0466f..4ceded13 100755 --- a/scripts/bert_qnli_bench/train_qnli_lds.sh +++ b/scripts/bert_qnli_bench/train_qnli_lds.sh @@ -10,6 +10,4 @@ source "$(dirname "$0")/../train_lds.sh" \ --hf-push-sleep 30 \ --gpu-split false \ --start 0 \ - --end 100 \ - --train-only true \ - --push-only true + --end 100 diff --git a/scripts/cifar_resnet9_bench/bench_defs.sh b/scripts/cifar_resnet9_bench/bench_defs.sh index 3ae0f616..34f1be0c 100755 --- a/scripts/cifar_resnet9_bench/bench_defs.sh +++ b/scripts/cifar_resnet9_bench/bench_defs.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Benchmark definitions: dataset params and sweep hyperparams for CIFAR-10 / ResNet9. -# Source this file, then use: ${BENCH_PARAMS[Name]} and ${BENCH_SWEEP[Name]} declare -A BENCH_PARAMS declare -A BENCH_SWEEP diff --git a/scripts/cifar_resnet9_bench/eval_all_cifar.sh b/scripts/cifar_resnet9_bench/eval_all_cifar.sh deleted file mode 100644 index a5ef4fe7..00000000 --- a/scripts/cifar_resnet9_bench/eval_all_cifar.sh +++ /dev/null @@ -1,7 +0,0 @@ -set -euo pipefail - -jid1=$(sbatch --parsable slurm/slurm_job.sbatch scripts/cifar_resnet9_bench/eval_cifar_pt1.sh) -[[ -n $jid1 ]] || { echo "pt1 submission failed"; exit 1; } - -sbatch --dependency=afterok:$jid1 slurm/slurm_job.sbatch scripts/cifar_resnet9_bench/eval_cifar_pt2.sh - diff --git a/scripts/cifar_resnet9_bench/eval_defs.sh b/scripts/cifar_resnet9_bench/eval_defs.sh index 7a34cdcb..03879054 100755 --- a/scripts/cifar_resnet9_bench/eval_defs.sh +++ b/scripts/cifar_resnet9_bench/eval_defs.sh @@ -1,5 +1,4 @@ #!/bin/bash -# Explainer sweep definitions for CIFAR-10 ResNet9 benchmark evaluation. declare -A EXPL_SWEEP diff --git a/scripts/cifar_resnet9_bench/plot_cifar.sh b/scripts/cifar_resnet9_bench/plot_cifar.sh deleted file mode 100755 index fc9d8cf6..00000000 --- a/scripts/cifar_resnet9_bench/plot_cifar.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" -ROOT="$(realpath "$DIR/../..")" -RESULTS_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_results/cifar" -OUT="${OUT:-$DIR/bar_rank.png}" - -python "$DIR/../plot_results.py" \ - --results-dir "$RESULTS_DIR" \ - --config "$DIR/cifar_plot_config.json" \ - --out "$OUT" \ - "$@" diff --git a/scripts/cifar_resnet9_bench/train_cifar_lds.sh b/scripts/cifar_resnet9_bench/train_cifar_lds.sh index 636d2c7a..9f9428b0 100755 --- a/scripts/cifar_resnet9_bench/train_cifar_lds.sh +++ b/scripts/cifar_resnet9_bench/train_cifar_lds.sh @@ -6,4 +6,6 @@ CONFIG_NAME="cifar_resnet9" source "$(dirname "$0")/../train_lds.sh" \ --n-lds-parallel 20 \ - --hf-push-sleep 20 + --hf-push-sleep 20 \ + --start 0 \ + --end 100 diff --git a/scripts/compute_lds_subset_logits.sh b/scripts/compute_lds_subset_logits.sh index 00edadb2..a0347c19 100755 --- a/scripts/compute_lds_subset_logits.sh +++ b/scripts/compute_lds_subset_logits.sh @@ -34,11 +34,7 @@ if [ -z "$START" ] || [ -z "$END" ]; then exit 1 fi -if [ -d "/data/cluster/users/bareeva" ]; then - BENCH_SAVE_DIR="/data/cluster/users/bareeva/quanda_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -else - BENCH_SAVE_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -fi +BENCH_SAVE_DIR="bench_out/${CONFIG_MAP_KEY}" CONFIG_PATH=$(python -c " from quanda.benchmarks.resources.config_map import config_map diff --git a/scripts/gpt2_trex_bench/eval_all_gpt2_trex.sh b/scripts/gpt2_trex_bench/eval_all_gpt2_trex.sh deleted file mode 100755 index 43c78db6..00000000 --- a/scripts/gpt2_trex_bench/eval_all_gpt2_trex.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -set -euo pipefail - -methods=( - random - kronfluence_gpt2 - dattri_if_datainf - dattri_trak - similarity -) - -mrr_jids=() -for method in "${methods[@]}"; do - jid=$(sbatch --parsable slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_mrr.sh \ - --method "$method") - [[ -n $jid ]] || { echo "mrr submission failed for $method"; exit 1; } - mrr_jids+=("$jid") -done - -for i in "${!methods[@]}"; do - method="${methods[$i]}" - jid="${mrr_jids[$i]}" - - sbatch --dependency=afterok:$jid slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_recall_at_k.sh \ - --method "$method" - - sbatch --dependency=afterok:$jid slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_tail_patch.sh \ - --method "$method" -done diff --git a/scripts/gpt2_trex_bench/eval_defs.sh b/scripts/gpt2_trex_bench/eval_defs.sh index 92923959..a31a3143 100755 --- a/scripts/gpt2_trex_bench/eval_defs.sh +++ b/scripts/gpt2_trex_bench/eval_defs.sh @@ -1,19 +1,15 @@ #!/bin/bash -# Per-explainer Hydra sweeps for the GPT-2 / TREx fact-tracing benchmarks. declare -A EXPL_SWEEP LOSS_PATH="quanda.explainers.wrappers.dattri_losses" -# Common dattri overrides — apply to every dattri-family explainer that -# uses one of the BERT base yamls. DATTRI_BASE='+explainer.kwargs.task=causal_lm +explainer.kwargs.hf_input_keys=[input_ids,attention_mask] explainer.kwargs.batch_size=1' EXPL_SWEEP[random]="explainer.kwargs.seed=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40 device=cuda:0" EXPL_SWEEP[similarity]="explainer.kwargs.layers=transformer.ln_f explainer.kwargs.similarity_metric.path=quanda.utils.functions.cosine_similarity,quanda.utils.functions.dot_product_similarity +explainer.kwargs.task=causal_lm device=cuda:0 batch_size=32" -# Methods with dedicated GPT-2 yamls (defaults already correct): EXPL_SWEEP[kronfluence_gpt2]="device=cuda:0 batch_size=1" EXPL_SWEEP[dattri_trak]="device=cuda:0 +explainer.kwargs.layer_name=['transformer.h.11.mlp.c_fc.weight','transformer.h.11.mlp.c_proj.weight'] explainer.kwargs.projector_kwargs.proj_dim=1024" diff --git a/scripts/gpt2_trex_bench/eval_mrr_all.sh b/scripts/gpt2_trex_bench/eval_mrr_all.sh deleted file mode 100755 index 29983cfe..00000000 --- a/scripts/gpt2_trex_bench/eval_mrr_all.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -methods=( - random - kronfluence_gpt2 - dattri_if_datainf - dattri_trak - similarity -) - -for method in "${methods[@]}"; do - sbatch slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_mrr.sh \ - --method "$method" -done diff --git a/scripts/gpt2_trex_bench/eval_recall_at_k_all.sh b/scripts/gpt2_trex_bench/eval_recall_at_k_all.sh deleted file mode 100755 index a5debc9e..00000000 --- a/scripts/gpt2_trex_bench/eval_recall_at_k_all.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -methods=( - random - kronfluence_gpt2 - dattri_if_datainf - dattri_trak - similarity -) - -for method in "${methods[@]}"; do - sbatch slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_recall_at_k.sh \ - --method "$method" -done diff --git a/scripts/gpt2_trex_bench/eval_tail_patch_all.sh b/scripts/gpt2_trex_bench/eval_tail_patch_all.sh deleted file mode 100755 index 4325f229..00000000 --- a/scripts/gpt2_trex_bench/eval_tail_patch_all.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -methods=( - random - kronfluence_gpt2 - dattri_if_datainf - dattri_trak - similarity -) - -for method in "${methods[@]}"; do - sbatch slurm/slurm_job.sbatch \ - scripts/gpt2_trex_bench/eval_tail_patch.sh \ - --method "$method" -done diff --git a/scripts/gpt2_trex_bench/plot_gpt2.sh b/scripts/gpt2_trex_bench/plot_gpt2.sh deleted file mode 100755 index c0034371..00000000 --- a/scripts/gpt2_trex_bench/plot_gpt2.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" -ROOT="$(realpath "$DIR/../..")" -RESULTS_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_results/gpt2_trex" -OUT="${OUT:-$DIR/bar_rank.png}" - -python "$DIR/../plot_results.py" \ - --results-dir "$RESULTS_DIR" \ - --config "$DIR/gpt2_plot_config.json" \ - --out "$OUT" \ - "$@" diff --git a/scripts/mnsit_lenet_bench/bench_defs.sh b/scripts/mnsit_lenet_bench/bench_defs.sh index 7923019c..a683cf6b 100644 --- a/scripts/mnsit_lenet_bench/bench_defs.sh +++ b/scripts/mnsit_lenet_bench/bench_defs.sh @@ -1,6 +1,5 @@ #!/bin/bash # Benchmark definitions: dataset params and sweep hyperparams. -# Source this file, then use: ${BENCH_PARAMS[Name]} and ${BENCH_SWEEP[Name]} declare -A BENCH_PARAMS declare -A BENCH_SWEEP diff --git a/scripts/mnsit_lenet_bench/eval_all_mnist.sh b/scripts/mnsit_lenet_bench/eval_all_mnist.sh deleted file mode 100644 index dc3837c6..00000000 --- a/scripts/mnsit_lenet_bench/eval_all_mnist.sh +++ /dev/null @@ -1,6 +0,0 @@ -set -euo pipefail - -jid1=$(sbatch --parsable slurm/slurm_job.sbatch scripts/mnsit_lenet_bench/eval_mnist_pt1.sh) -[[ -n $jid1 ]] || { echo "pt1 submission failed"; exit 1; } - -sbatch --dependency=afterok:$jid1 slurm/slurm_job.sbatch scripts/mnsit_lenet_bench/eval_mnist_pt2.sh diff --git a/scripts/mnsit_lenet_bench/eval_defs.sh b/scripts/mnsit_lenet_bench/eval_defs.sh index c6b68f6e..fe3ec20e 100755 --- a/scripts/mnsit_lenet_bench/eval_defs.sh +++ b/scripts/mnsit_lenet_bench/eval_defs.sh @@ -1,7 +1,5 @@ #!/bin/bash # Explainer sweep definitions for MNIST LeNet benchmark evaluation. -# Source this file, then use: ${EXPL_SWEEP[method]} -# Values are Hydra multirun overrides on explainer.kwargs.* declare -A EXPL_SWEEP diff --git a/scripts/mnsit_lenet_bench/plot_mnist.sh b/scripts/mnsit_lenet_bench/plot_mnist.sh deleted file mode 100755 index e5e68091..00000000 --- a/scripts/mnsit_lenet_bench/plot_mnist.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -DIR="$(dirname "$0")" -ROOT="$(realpath "$DIR/../..")" -RESULTS_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_results/mnist" -OUT="${OUT:-$DIR/bar_rank.png}" - -python "$DIR/../plot_results.py" \ - --results-dir "$RESULTS_DIR" \ - --config "$DIR/mnist_plot_config.json" \ - --out "$OUT" \ - "$@" diff --git a/scripts/mnsit_lenet_bench/train_mnist_lds.sh b/scripts/mnsit_lenet_bench/train_mnist_lds.sh index 53acde4b..47bccda7 100755 --- a/scripts/mnsit_lenet_bench/train_mnist_lds.sh +++ b/scripts/mnsit_lenet_bench/train_mnist_lds.sh @@ -6,4 +6,6 @@ CONFIG_NAME="mnist_lenet" source "$(dirname "$0")/../train_lds.sh" \ --n-lds-parallel 16 \ - --hf-push-sleep 60 + --hf-push-sleep 60 \ + --start 0 \ + --end 100 diff --git a/scripts/plot_results.py b/scripts/plot_results.py deleted file mode 100644 index ee4b6c86..00000000 --- a/scripts/plot_results.py +++ /dev/null @@ -1,624 +0,0 @@ -"""Render bar-rank plot from local eval JSON results.""" - -from __future__ import annotations - -import argparse -import glob -import json -import os -import re - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from matplotlib import rcParams - -METHOD_COLORS = { - "representer_points": "#EB9C38", - "arnoldi": "#E41517", - "tracincpfast": "#7EAF6E", - "trak": "#7D53BA", - "similarity": "#6F97B1", - "random": "#90918B", - "kronfluence": "#FDAEB9", - "kronfluence_gpt2": "#FDAEB9", - "dattri_trak": "#7D53BA", - "dattri_if_datainf": "#204541", -} -_FALLBACK_COLOR = "#90918B" - -METHOD_LABELS = { - "representer_points": "ReprPoints", - "arnoldi": "ArnoldiInf", - "tracincpfast": "TracInCP", - "trak": "TRAK-1", - "similarity": "Similarity", - "random": "Random", - "kronfluence": "Kronfluence", - "kronfluence_gpt2": "Kronfluence", - "dattri_trak": "TRAK-1", - "dattri_if_datainf": "DataInf", -} - -BENCH_LABEL_SUFFIXES = { - "class_detection": "Class\nDetection", - "subclass_detection": "Subclass\nDetection", - "mislabeling_detection": "Mislabeling\nDetection", - "shortcut_detection": "Shortcut\nDetection", - "mixed_datasets": "Mixed Dataset\nSeparation", - "top_k_cardinality": "Top-K\nCardinality", - "model_randomization": "Model\nRandomization", - "linear_datamodeling": "LDS", -} - -BENCH_ORDER = ( - "class_detection", - "subclass_detection", - "mislabeling_detection", - "shortcut_detection", - "mixed_datasets", - "top_k_cardinality", - "linear_datamodeling", - "model_randomization", - "mrr", - "recall_at_k", - "tail_patch", -) - - -def _bench_rank(bench_id: str) -> int: - for i, suffix in enumerate(BENCH_ORDER): - if bench_id == suffix or bench_id.endswith("_" + suffix): - return i - return len(BENCH_ORDER) - - -MIN_ABS_BENCH_SUBSTRINGS = ("model_randomization",) -SIDE_PANEL_BENCH_SUBSTRINGS = ( - "tail_patch", - "model_randomization", - "linear_datamodeling", -) - -NO_CI_EXEMPT_SUBSTRINGS = ("mislabeling_detection", "top_k_cardinality") - - -def _ci_exempt(bench_id: str) -> bool: - return any(s in bench_id for s in NO_CI_EXEMPT_SUBSTRINGS) - - -def _is_min_abs(bench_id: str) -> bool: - return any(s in bench_id for s in MIN_ABS_BENCH_SUBSTRINGS) - - -def _is_side_panel(bench_id: str) -> bool: - return any(s in bench_id for s in SIDE_PANEL_BENCH_SUBSTRINGS) - - -def _detect_setting(benches: list[str]) -> str | None: - """Common dataset prefix from bench ids (e.g. 'cifar' from - 'cifar_class_detection'); None if benches don't share a prefix.""" - if not benches: - return None - prefix = benches[0].split("_", 1)[0] - if all(b.startswith(prefix + "_") for b in benches): - return prefix - return None - - -def _scalar(score): - if isinstance(score, (int, float)): - return float(score) - if isinstance(score, dict): - v = next(iter(score.values()), None) - return float(v) if isinstance(v, (int, float)) else None - return None - - -def _bench_version_from_path(path: str) -> str | None: - """Third `__`-separated segment of the filename, e.g. - 'bdb919e-default_ClassDetection'. Identifies the bench config the - result was produced under.""" - parts = os.path.basename(path).split("__") - return parts[2] if len(parts) >= 3 else None - - -def _canonical_bench_versions() -> dict[str, str]: - """Map bench_id → canonical version (yaml stem) from config_map.py.""" - from quanda.benchmarks.resources.config_map import config_map - - out = {} - for bench_id, path in config_map.items(): - stem, _ = os.path.splitext(os.path.basename(str(path))) - out[bench_id] = stem - return out - - -def _prefer_canonical_versions( - df: pd.DataFrame, canonical: dict[str, str] -) -> pd.DataFrame: - """Within each (bench, method, kwargs) group with multiple versions, - drop rows whose bench_version doesn't match the canonical one if - canonical is present in the group.""" - drop_mask = pd.Series(False, index=df.index) - for (bench, method, kw), grp in df.groupby( - ["bench", "method", "kwargs_key"] - ): - if grp["bench_version"].nunique() <= 1: - continue - canon = canonical.get(bench) - if canon is None or canon not in set(grp["bench_version"]): - continue - drop_mask.loc[grp.index] = grp["bench_version"] != canon - return df[~drop_mask] - - -def _warn_multiple_bench_versions( - df: pd.DataFrame, canonical: dict[str, str] -) -> None: - for (bench, method, _), grp in df.groupby( - ["bench", "method", "kwargs_key"] - ): - versions = sorted(set(grp["bench_version"].dropna())) - if len(versions) <= 1: - continue - canon = canonical.get(bench) - suffix = ( - f"; canonical {canon!r} not found" - if canon and canon not in versions - else "" - ) - print( - f"warning: multiple bench versions for " - f"bench={bench!r} method={method!r}: {versions}{suffix}" - ) - - -def load_scores( - results_dir: str, methods: list[str], benches: list[str] -) -> pd.DataFrame: - rows = [] - for path in glob.glob(os.path.join(results_dir, "*.json")): - with open(path) as f: - d = json.load(f) - score = _scalar(d.get("score")) - if score is None: - continue - rows.append( - { - "method": d.get("method"), - "bench": d.get("bench_id"), - "score": score, - "ci_low": _scalar(d.get("ci_low")), - "ci_high": _scalar(d.get("ci_high")), - "mtime": os.path.getmtime(path), - "bench_version": _bench_version_from_path(path), - "kwargs_key": json.dumps( - d.get("expl_kwargs") or {}, sort_keys=True - ), - } - ) - df = pd.DataFrame(rows) - df = df[df["method"].isin(methods) & df["bench"].isin(benches)] - df = df.dropna(subset=["score"]) - canonical = _canonical_bench_versions() - df = _prefer_canonical_versions(df, canonical) - _warn_multiple_bench_versions(df, canonical) - - is_random = df["method"] == "random" - random_stats = ( - df[is_random].groupby("bench")["score"].agg(["mean", "std", "count"]) - ) - - non_random = df[~is_random].copy() - non_random["__rank"] = non_random.apply( - lambda r: abs(r.score) if _is_min_abs(r.bench) else -r.score, - axis=1, - ) - best = non_random.loc[ - non_random.groupby(["method", "bench"])["__rank"].idxmin() - ].drop(columns="__rank") - bars_df = best.pivot(index="method", columns="bench", values="score") - ci_low_df = best.pivot(index="method", columns="bench", values="ci_low") - ci_high_df = best.pivot(index="method", columns="bench", values="ci_high") - return bars_df, ci_low_df, ci_high_df, random_stats - - -def _discover(results_dir: str) -> tuple[list[str], list[str]]: - methods, benches = set(), set() - for path in glob.glob(os.path.join(results_dir, "*.json")): - with open(path) as f: - d = json.load(f) - if d.get("method"): - methods.add(d["method"]) - if d.get("bench_id"): - benches.add(d["bench_id"]) - return sorted(methods), sorted(benches) - - -def _parse_args() -> argparse.Namespace: - ap = argparse.ArgumentParser() - ap.add_argument( - "--results-dir", - default="/data2/bareeva/Projects/quanda/cluster_output_new/eval_results/cifar", - ) - ap.add_argument( - "--config", - default=os.path.join( - os.path.dirname(__file__), - "cifar_resnet9_bench", - "cifar_plot_config.json", - ), - help=( - "JSON config with keys: methods, benches, method_labels, " - "bench_labels. If omitted, methods/benches are discovered " - "from results-dir and labels default to ids." - ), - ) - ap.add_argument( - "--out", - default=os.path.join(os.path.dirname(__file__), "bar_rank.png"), - ) - return ap.parse_args() - - -def _load_config(path: str | None) -> dict: - if not path: - return {} - with open(path) as f: - text = re.sub(r"(?m)^\s*//.*$|//[^\n\"]*$", "", f.read()) - return json.loads(text) - - -def _resolve_methods_benches( - cfg: dict, results_dir: str -) -> tuple[list[str], list[str]]: - methods = cfg.get("methods") - benches = cfg.get("benches") - if methods is None or benches is None: - disc_methods, disc_benches = _discover(results_dir) - methods = methods or disc_methods - benches = benches or disc_benches - return methods, sorted(benches, key=_bench_rank) - - -def _apply_setting_to_outpath(out: str, setting: str | None) -> str: - if not setting: - return out - out_dir = os.path.dirname(out) - out_base, out_ext = os.path.splitext(os.path.basename(out)) - if setting in out_base: - return out - return os.path.join(out_dir, f"{out_base}_{setting}{out_ext}") - - -def _default_bench_labels( - setting: str | None, benches: list[str] -) -> dict[str, str]: - if not setting: - return {} - out = {} - for b in benches: - suffix = b[len(setting) + 1 :] if b.startswith(setting + "_") else b - if suffix in BENCH_LABEL_SUFFIXES: - out[b] = BENCH_LABEL_SUFFIXES[suffix] - return out - - -def _warn_missing_ci( - df: pd.DataFrame, - ci_low_df: pd.DataFrame, - ci_high_df: pd.DataFrame, - benches: list[str], - bar_methods: list[str], -) -> None: - for b in benches: - if _ci_exempt(b) or b not in ci_low_df.columns: - continue - for m in bar_methods: - if m not in ci_low_df.index or m not in df.index: - continue - if pd.isna(df.loc[m, b]): - continue - if pd.isna(ci_low_df.loc[m, b]) and pd.isna(ci_high_df.loc[m, b]): - print( - f"warning: benchmark {b!r} is missing error bars " - f"for explainer {m!r}" - ) - - -def _draw_bench_bars( - ax, - df: pd.DataFrame, - ci_low_df: pd.DataFrame, - ci_high_df: pd.DataFrame, - random_stats: pd.DataFrame, - metric: str, - bench_id: str, - is_min_abs: bool, - group_start_px: float, - bar_px: int, - inner_pad_px: int, - group_w_px: float, - colors: list[str], - random_color: str, -) -> float: - values = df[metric].values - valid = ~np.isnan(values) - if is_min_abs: - # Closest-to-zero first. - sorted_idx = np.argsort(np.abs(values[valid])) - else: - sorted_idx = np.argsort(values[valid])[::-1] - sorted_values = values[valid][sorted_idx] - orig_idx = np.flatnonzero(valid)[sorted_idx] - n_bars = len(sorted_values) - - x_positions = ( - group_start_px - + bar_px / 2 - + np.arange(n_bars) * (bar_px + inner_pad_px) - ) - ax.bar( - x_positions, - sorted_values, - width=bar_px, - color=[colors[i % len(colors)] for i in orig_idx], - edgecolor="none", - label=metric, - ) - - lows = ci_low_df[metric].values[orig_idx] - highs = ci_high_df[metric].values[orig_idx] - err_mask = ~np.isnan(lows) & ~np.isnan(highs) - if err_mask.any(): - yerr = np.vstack( - [ - np.maximum(sorted_values[err_mask] - lows[err_mask], 0), - np.maximum(highs[err_mask] - sorted_values[err_mask], 0), - ] - ) - ax.errorbar( - x_positions[err_mask], - sorted_values[err_mask], - yerr=yerr, - fmt="none", - ecolor="black", - elinewidth=0.7, - capsize=1, - capthick=0.7, - zorder=5, - ) - - if bench_id in random_stats.index: - mu = random_stats.loc[bench_id, "mean"] - line_x = (group_start_px, group_start_px + group_w_px) - ax.hlines( - mu, - *line_x, - colors=random_color, - linewidth=1.1, - linestyles=(0, (2, 1)), - zorder=6, - ) - - return group_start_px + group_w_px / 2 - - -def _style_panel( - ax, - panel_w: float, - xtick_positions: list[float], - xtick_labels: list[str], - tick_fontsize_pt: int, -) -> None: - ax.set_xlim(0, panel_w) - ax.set_facecolor("#FFFFFF") - ax.yaxis.grid( - True, - linewidth=0.3, - zorder=0, - color="gray", - linestyle="dashed", - ) - ax.set_axisbelow(True) - ax.set_xticks(xtick_positions) - ax.set_xticklabels( - xtick_labels, - rotation=0, - ha="center", - fontsize=tick_fontsize_pt, - ) - ax.xaxis.tick_top() - ax.xaxis.set_label_position("top") - ax.tick_params(axis="x", pad=1, size=0, width=0.5) - ax.tick_params( - axis="y", - labelsize=tick_fontsize_pt, - pad=1, - size=3, - width=0.5, - ) - for spine in ax.spines.values(): - spine.set_linewidth(0.3) - spine.set_color("black") - - -def _append_random_rows( - df: pd.DataFrame, - random_stats: pd.DataFrame, - benches: list[str], - bench_labels: dict[str, str], - method_labels: dict[str, str], -) -> pd.DataFrame: - if random_stats.empty: - return df - random_label = method_labels.get("random", "random") - for stat in ("mean", "std"): - row = {"explainer": f"{random_label} ({stat})"} - for b in benches: - row[bench_labels.get(b, b)] = ( - random_stats.loc[b, stat] - if b in random_stats.index - else np.nan - ) - df = pd.concat([df, pd.DataFrame([row])], ignore_index=True) - return df - - -def main(): - args = _parse_args() - cfg = _load_config(args.config) - methods, benches = _resolve_methods_benches(cfg, args.results_dir) - - setting = _detect_setting(benches) - args.out = _apply_setting_to_outpath(args.out, setting) - - bar_methods = [m for m in methods if m != "random"] - - method_labels = {**METHOD_LABELS, **cfg.get("method_labels", {})} - bench_labels = { - **_default_bench_labels(setting, benches), - **cfg.get("bench_labels", {}), - } - colors = [METHOD_COLORS.get(m, _FALLBACK_COLOR) for m in bar_methods] - random_color = "#000000" - - df, ci_low_df, ci_high_df, random_stats = load_scores( - args.results_dir, methods, benches - ) - _warn_missing_ci(df, ci_low_df, ci_high_df, benches, bar_methods) - rename_idx = {m: method_labels.get(m, m) for m in bar_methods} - rename_cols = {b: bench_labels.get(b, b) for b in benches} - df = df.reindex(index=bar_methods, columns=benches) - df = df.rename(index=rename_idx, columns=rename_cols) - ci_low_df = ci_low_df.reindex(index=bar_methods, columns=benches).rename( - index=rename_idx, columns=rename_cols - ) - ci_high_df = ci_high_df.reindex(index=bar_methods, columns=benches).rename( - index=rename_idx, columns=rename_cols - ) - df.index.name = "explainer" - df.reset_index(inplace=True) - ci_low_df = ci_low_df.reset_index(drop=True) - ci_high_df = ci_high_df.reset_index(drop=True) - - metric_pairs = [ - (b, bench_labels.get(b, b), _is_min_abs(b)) for b in benches - ] - metric_pairs = [p for p in metric_pairs if not df[p[1]].isna().all()] - - rcParams["font.family"] = "DejaVu Sans" - rcParams["font.weight"] = "normal" - - main_pairs = [p for p in metric_pairs if not _is_side_panel(p[0])] - side_pairs = [p for p in metric_pairs if _is_side_panel(p[0])] - groups = [g for g in (main_pairs, side_pairs) if g] - - bar_px = 7 - inner_pad_px = 1 - axes_pad_px = 4 - group_gap_px = 20 - panel_gap_px = 31 - left_margin_px = 35 - right_margin_px = 16 - top_margin_px = 22 - bottom_margin_px = 8 - out_height_px = 100 - dpi = 96 - save_dpi = 4 * dpi - - n_explainers = len(df) - group_w_px = n_explainers * bar_px + (n_explainers - 1) * inner_pad_px - panels_px = [ - 2 * axes_pad_px + len(g) * group_w_px + (len(g) - 1) * group_gap_px - for g in groups - ] - total_w_px = ( - left_margin_px - + sum(panels_px) - + (len(groups) - 1) * panel_gap_px - + right_margin_px - ) - - width_in = total_w_px / dpi - height_in = out_height_px / dpi - - tick_fontsize_pt = 6 - rcParams["font.size"] = tick_fontsize_pt - rcParams["axes.labelsize"] = tick_fontsize_pt - rcParams["xtick.labelsize"] = tick_fontsize_pt - rcParams["ytick.labelsize"] = tick_fontsize_pt - - fig = plt.figure(figsize=(width_in, height_in), dpi=dpi) - fig.patch.set_facecolor("#FAFAF2") - - plot_y_frac = bottom_margin_px / out_height_px - plot_h_frac = ( - out_height_px - top_margin_px - bottom_margin_px - ) / out_height_px - - axes = [] - x_off_px = left_margin_px - for panel_w in panels_px: - ax = fig.add_axes( - [ - x_off_px / total_w_px, - plot_y_frac, - panel_w / total_w_px, - plot_h_frac, - ] - ) - axes.append(ax) - x_off_px += panel_w + panel_gap_px - - for ax, group, panel_w in zip(axes, groups, panels_px): - g_metrics = [p[1] for p in group] - xtick_positions = [] - for j, (bench_id, metric, is_min_abs) in enumerate(group): - group_start_px = axes_pad_px + j * (group_w_px + group_gap_px) - xtick_positions.append( - _draw_bench_bars( - ax, - df, - ci_low_df, - ci_high_df, - random_stats, - metric, - bench_id, - is_min_abs, - group_start_px, - bar_px, - inner_pad_px, - group_w_px, - colors, - random_color, - ) - ) - _style_panel(ax, panel_w, xtick_positions, g_metrics, tick_fontsize_pt) - - axes[0].set_ylabel("Metric score", fontsize=tick_fontsize_pt) - - plt.savefig(args.out, bbox_inches=None, pad_inches=0, dpi=save_dpi) - print( - f"wrote {args.out} " - f"({total_w_px}x{out_height_px} px @ {dpi} dpi layout)" - ) - n_per_bench = ", ".join( - f"{b}={int(c)}" for b, c in random_stats["count"].items() - ) - print(f"random runs: {n_per_bench}") - - csv_path = os.path.join( - os.path.dirname(args.out) or ".", - os.path.splitext(os.path.basename(args.out))[0] + ".csv", - ) - df = _append_random_rows( - df, random_stats, benches, bench_labels, method_labels - ) - df.to_csv(csv_path, index=False) - print(f"wrote {csv_path}") - - -if __name__ == "__main__": - main() diff --git a/scripts/prefetch_bench.py b/scripts/prefetch_bench.py index 34f48e58..37b4fcf2 100644 --- a/scripts/prefetch_bench.py +++ b/scripts/prefetch_bench.py @@ -5,18 +5,10 @@ import os import hydra -from omegaconf import DictConfig, OmegaConf +from omegaconf import DictConfig from quanda.benchmarks import bench_dict -OmegaConf.register_new_resolver( - "cluster_or_local", - lambda cluster, local: ( - cluster if os.path.isdir("/data/cluster/users/bareeva") else local - ), - replace=True, -) - _SUFFIX_TO_CLASS = { "class_detection": "ClassDetection", "subclass_detection": "SubclassDetection", diff --git a/scripts/run_bench_eval.py b/scripts/run_bench_eval.py index 6bac1e37..e7f98125 100644 --- a/scripts/run_bench_eval.py +++ b/scripts/run_bench_eval.py @@ -16,14 +16,6 @@ from quanda.benchmarks.base import default_explanations_id from quanda.benchmarks.resources.config_map import config_map -OmegaConf.register_new_resolver( - "cluster_or_local", - lambda cluster, local: ( - cluster if os.path.isdir("/data/cluster/users/bareeva") else local - ), - replace=True, -) - _SUFFIX_TO_CLASS = { "class_detection": "ClassDetection", "subclass_detection": "SubclassDetection", diff --git a/scripts/train.py b/scripts/train.py index d0fd9e6c..343c18fd 100644 --- a/scripts/train.py +++ b/scripts/train.py @@ -18,7 +18,11 @@ def main(cfg: DictConfig) -> Tuple[float]: bench_cls = bench_dict[cfg.bench] logger = LoggerConfigParser.parse_logger(cfg) bench = bench_cls.train( - cfg, logger=logger, device=device, batch_size=cfg.batch_size + cfg, + logger=logger, + device=device, + batch_size=cfg.batch_size, + use_pid=True, ) scores = bench.sanity_check() print(f"Sanity check scores: {scores}") diff --git a/scripts/train.sh b/scripts/train.sh index 991707e6..34ca9d7a 100755 --- a/scripts/train.sh +++ b/scripts/train.sh @@ -1,10 +1,5 @@ #!/bin/bash # Shared benchmark training logic. -# Dataset-specific scripts should set the following before sourcing this file: -# - CONFIG_NAME: Hydra config name (e.g. "mnist_lenet", "cifar_resnet9") -# - CONFIG_MAP_PREFIX: Prefix for config_map.py keys (e.g. "mnist", "cifar") -# - benchmarks: Array of benchmark names to run -# and source their own bench_defs.sh (BENCH_PARAMS / BENCH_SWEEP). export PYTHONPATH="$PYTHONPATH:$(dirname $(dirname $(realpath $0)))" @@ -23,11 +18,7 @@ cfg_output_dir="quanda/benchmarks/resources/configs" commit_tag=$(git rev-parse --short HEAD 2>/dev/null || echo "GIT_TAG") mkdir -p logs -if [ -d "/data/cluster/users/bareeva" ]; then - bench_save_dir_override="bench_save_dir=/data/cluster/users/bareeva/quanda_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -else - bench_save_dir_override="bench_save_dir=/data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -fi +bench_save_dir_override="bench_save_dir=bench_out/${CONFIG_MAP_PREFIX}" # Map benchmark names to config_map.py keys declare -A BENCH_CONFIG_MAP_KEY diff --git a/scripts/train_and_push_to_hub.py b/scripts/train_and_push_to_hub.py index 1e3f1f8b..5702ba8e 100644 --- a/scripts/train_and_push_to_hub.py +++ b/scripts/train_and_push_to_hub.py @@ -23,6 +23,7 @@ def main(cfg: DictConfig) -> Tuple[float]: logger=logger, device=cfg.device, batch_size=cfg.batch_size, + use_pid=True, ) return 0.0 diff --git a/scripts/train_lds.sh b/scripts/train_lds.sh index 6ea2da28..9133b644 100755 --- a/scripts/train_lds.sh +++ b/scripts/train_lds.sh @@ -1,10 +1,5 @@ #!/bin/bash -# Train and push the M subset models for an LDS benchmark. Subset training -# fans out up to N_LDS_PARALLEL workers. -# -# Required from the caller's env: -# CONFIG_MAP_PREFIX key prefix in benchmarks/resources/config_map.py; -# used to resolve the registered LDS config id. +# Train and push the M subset models for an LDS benchmark. export PYTHONPATH="$PYTHONPATH:$(dirname $(dirname $(realpath $0)))" @@ -50,11 +45,7 @@ resolve_indices() { CFG_DIR="quanda/benchmarks/resources/configs" mkdir -p logs -if [ -d "/data/cluster/users/bareeva" ]; then - BENCH_SAVE_DIR="/data/cluster/users/bareeva/quanda_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -else - BENCH_SAVE_DIR="/data2/bareeva/Projects/quanda/cluster_output_new2/eval_bench/${CONFIG_MAP_PREFIX}" -fi +BENCH_SAVE_DIR="bench_out/${CONFIG_MAP_PREFIX}" SAVE_OVERRIDE="bench_save_dir=${BENCH_SAVE_DIR}" # ---------- helpers ---------- diff --git a/scripts/train_lds_subset.py b/scripts/train_lds_subset.py index 5d5e4181..abfe08c7 100644 --- a/scripts/train_lds_subset.py +++ b/scripts/train_lds_subset.py @@ -1,9 +1,4 @@ -"""Train (or push) a single LDS subset model by index. - -Used by parallel orchestration scripts: workers invoke this with -``--idx I`` to train subset ``I`` locally; a final serial pass invokes -it with ``--push-only`` to upload each subset checkpoint to HF Hub. -""" +"""Train (or push) a single LDS subset model by index.""" import argparse import os diff --git a/slurm/build.sh b/slurm/build.sh deleted file mode 100755 index 374ab1ca..00000000 --- a/slurm/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Build the quanda apptainer image. - -set -euo pipefail - -cd "$(dirname "$0")/.." -apptainer build --force --fakeroot slurm/env_quanda.sif slurm/env_quanda.def diff --git a/slurm/copy_slurm_job.sh b/slurm/copy_slurm_job.sh deleted file mode 100644 index b7fd9ca9..00000000 --- a/slurm/copy_slurm_job.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -SRC="bareeva@vca-gpu-0503-01:/data/cluster/users/bareeva/quanda_output_new2" -DST="/data2/bareeva/Projects/quanda/cluster_output_new2" - -mkdir -p "$DST" -rsync -au "$SRC/" "$DST/" -rsync -au "$DST/" "$SRC/" - - - -#before=$(find /data/cluster/users/bareeva/quanda_output_new2/eval_results -type f | wc -l); find /data/cluster/users/bareeva/quanda_output_new2/eval_results -type f -not -newermt 2026-04-29 -delete; after=$(find /data/cluster/users/bareeva/quanda_output_new2/eval_results -type f | wc -l); echo "Files before: $before"; echo "Files after: $after"; echo "Deleted: $((before - after))"; echo; echo "Remaining oldest files:"; find /data/cluster/users/bareeva/quanda_output_new2/eval_results -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort | head -3 \ No newline at end of file diff --git a/slurm/debug.sh b/slurm/debug.sh deleted file mode 100755 index cc812257..00000000 --- a/slurm/debug.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Drop into an interactive shell inside the quanda container. -set -euo pipefail - -export CUDA_VISIBLE_DEVICES=1 - -apptainer shell --nv \ - --env HF_HOME=/data/cluster/users/bareeva/.hf_cache \ - --bind "$(pwd):/workspace" \ - --bind /data/cluster/users/bareeva:/data/cluster/users/bareeva \ - --pwd /workspace \ - "$(dirname "$0")/env_quanda.sif" diff --git a/slurm/env_quanda.def b/slurm/env_quanda.def deleted file mode 100644 index 7797200f..00000000 --- a/slurm/env_quanda.def +++ /dev/null @@ -1,62 +0,0 @@ -Bootstrap: docker -From: pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel - -%files - pyproject.toml /opt/quanda/pyproject.toml - README.md /opt/quanda/README.md - -%post - # Install system dependencies - apt-get update && apt-get install -y \ - git \ - wget \ - curl \ - build-essential \ - gcc \ - g++ \ - && rm -rf /var/lib/apt/lists/* - - # Install uv - curl -LsSf https://astral.sh/uv/install.sh | sh - . $HOME/.local/bin/env - - # Create virtual environment with uv using Python 3.11 - # (quanda requires >=3.10,<3.12; 3.11 matches tox type env) - $HOME/.local/bin/uv venv /opt/venv --python 3.11 - . /opt/venv/bin/activate - - # Stub out the package tree so ``pip install .[dev]`` resolves without - # needing the real source (which is bind-mounted at runtime). - mkdir -p /opt/quanda/quanda - touch /opt/quanda/quanda/__init__.py - - cd /opt/quanda - # setuptools-scm needs a version when building outside a git tree. - SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 \ - $HOME/.local/bin/uv pip install --no-cache-dir ".[dev]" - - # `.[dev]` can pull a newer torch (cu13) that mismatches the base - # image's nvcc 12.4. Re-pin to the cu124 wheel before building fast-jl. - $HOME/.local/bin/uv pip install --no-cache-dir --force-reinstall \ - --index-url https://download.pytorch.org/whl/cu124 \ - torch==2.6.0 torchvision==0.21.0 - - # traker[fast] builds fast-jl against the base image's torch + nvcc; - # --no-build-isolation is required so it sees the env's torch. - $HOME/.local/bin/uv pip install --no-cache-dir --no-build-isolation "traker[fast]" - -%environment - export PATH=/opt/venv/bin:$HOME/.local/bin:$PATH - - # Prevent Python from using ~/.local packages - export PYTHONNOUSERSITE=1 - - # Make the bind-mounted source tree importable as ``quanda`` - export PYTHONPATH=/workspace:$PYTHONPATH - - # Cache locations - export HF_HOME=/data/cluster/users/bareeva/.hf_cache - export PIP_CACHE_DIR=~/.cache/pip - -%runscript - exec python "$@" diff --git a/slurm/run.sh b/slurm/run.sh deleted file mode 100755 index 40ec3906..00000000 --- a/slurm/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Interactive one-off run of a Python or Bash script inside the quanda container. -# Usage: ./slurm/run.sh path/to/script.{py,sh} [args...] -set -euo pipefail - -case "$1" in - *.sh) interpreter=bash ;; - *) interpreter=python ;; -esac - -apptainer exec --nv \ - --env HF_HOME=/data/cluster/users/bareeva/.hf_cache \ - --bind "$(pwd):/workspace" \ - --bind /data/cluster/users/bareeva:/data/cluster/users/bareeva \ - --pwd /workspace \ - "$(dirname "$0")/env_quanda.sif" "$interpreter" "$@" diff --git a/slurm/slurm_job.sbatch b/slurm/slurm_job.sbatch deleted file mode 100644 index 122e36d8..00000000 --- a/slurm/slurm_job.sbatch +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -#SBATCH --job-name=quanda -#SBATCH --output=log/%j_%x.out -#SBATCH --error=log/%j_%x.err -#SBATCH --ntasks=1 -#SBATCH --cpus-per-task=8 -#SBATCH --gpus=1 -#SBATCH --mem=64G -#SBATCH --partition=gpu3,gpu5 - -# Hydra surfaces full tracebacks for scripts under scripts/*. -export HYDRA_FULL_ERROR=1 -mkdir -p log - -apptainer exec --nv \ - --env HF_HOME=/data/cluster/users/bareeva/.hf_cache \ - --bind ${PWD}:/workspace \ - --bind /data/cluster/users/bareeva:/data/cluster/users/bareeva \ - --pwd /workspace \ - $PWD/slurm/env_quanda.sif \ - bash "$@" diff --git a/tests/assets/mnist_local_bench/0_rand_0.pth b/tests/assets/mnist_local_bench/0_rand_0.pth new file mode 100644 index 00000000..a5afc5fa Binary files /dev/null and b/tests/assets/mnist_local_bench/0_rand_0.pth differ diff --git a/tests/benchmarks/downstream_eval/test_class_detection.py b/tests/benchmarks/downstream_eval/test_class_detection.py index ebd2ca5d..66c17bad 100644 --- a/tests/benchmarks/downstream_eval/test_class_detection.py +++ b/tests/benchmarks/downstream_eval/test_class_detection.py @@ -11,7 +11,7 @@ @pytest.mark.benchmarks @pytest.mark.parametrize( - "test_id, explainer_cls, task, model, dataset, config, batch_size, expected_score", + "test_id, explainer_cls, task, model, dataset, config, batch_size, min_score", [ ( "mnist", @@ -21,7 +21,7 @@ "load_mnist_dataset", "load_mnist_unit_test_config", 8, - 0.75, + 0.5, ), ], ) @@ -33,10 +33,11 @@ def test_class_detection_kronfluence_vision( dataset, config, batch_size, - expected_score, + min_score, tmp_path, request, ): + torch.manual_seed(0) config = request.getfixturevalue(config) config["cache_dir"] = str(tmp_path) model = request.getfixturevalue(model) @@ -55,6 +56,7 @@ def test_class_detection_kronfluence_vision( checkpoints=[checkpoint_path], checkpoints_load_func=get_load_state_dict_func("cpu"), use_predictions=config.get("use_predictions", True), + s=1, ) expl_kwargs = {"task_module": task, "cache_dir": str(tmp_path)} @@ -65,12 +67,12 @@ def test_class_detection_kronfluence_vision( batch_size=batch_size, )["score"] - assert math.isclose(score, expected_score, abs_tol=0.00001) + assert score >= min_score @pytest.mark.benchmarks @pytest.mark.parametrize( - "test_id, explainer_cls, task, model, dataset, batch_size, expected_score", + "test_id, explainer_cls, task, model, dataset, batch_size, min_score", [ ( "dummy_text", @@ -79,7 +81,7 @@ def test_class_detection_kronfluence_vision( "load_simple_classifier", "load_text_dataset", 2, - 1.0, + 0.75, ), ], ) @@ -90,7 +92,7 @@ def test_class_detection_kronfluence_text( model, dataset, batch_size, - expected_score, + min_score, tmp_path, request, ): @@ -122,7 +124,7 @@ def test_class_detection_kronfluence_text( batch_size=batch_size, )["score"] - assert math.isclose(score, expected_score, abs_tol=0.00001) + assert score >= min_score @pytest.mark.slow diff --git a/tests/benchmarks/downstream_eval/test_mislabeling_detection.py b/tests/benchmarks/downstream_eval/test_mislabeling_detection.py index 92199082..b18deec5 100644 --- a/tests/benchmarks/downstream_eval/test_mislabeling_detection.py +++ b/tests/benchmarks/downstream_eval/test_mislabeling_detection.py @@ -12,7 +12,7 @@ from quanda.explainers.wrappers import CaptumSimilarity from quanda.metrics.downstream_eval import MislabelingDetectionMetric from quanda.utils.cache import ExplanationsCache -from quanda.utils.common import _subsample_dataset +from quanda.utils.common import subsample_dataset from quanda.utils.datasets.transformed import LabelFlippingDataset from quanda.utils.functions import cosine_similarity @@ -85,7 +85,7 @@ def test_mislabeling_detection( dst_eval = MislabelingDetection.from_config( config=config, - load_meta_from_disk=False, + load_fresh=True, offline=True, device="cpu", ) @@ -110,7 +110,7 @@ def fake_self_influence(self, batch_size=8): # so evaluate(use_cached_expl=True) loads it from disk. cache_dir = str(tmp_path / "expl_cache") os.makedirs(cache_dir, exist_ok=True) - train_subset = _subsample_dataset( + train_subset = subsample_dataset( train_dataset, max_n=max_eval_n, seed=eval_seed ) precomputed_si = torch.arange(len(train_subset), dtype=torch.float32) @@ -133,7 +133,7 @@ def fake_self_influence(self, batch_size=8): # MislabelingDetectionMetric call using the same precomputed # tensor + remapped indices. This pins down the cache-load + # subset-remap path without depending on a magic number. - train_subset = _subsample_dataset( + train_subset = subsample_dataset( train_dataset, max_n=max_eval_n, seed=eval_seed ) reference_si = torch.arange(len(train_subset), dtype=torch.float32) @@ -154,7 +154,7 @@ def _make_bench(config, tmp_path): config["cache_dir"] = str(tmp_path) return MislabelingDetection.from_config( config=config, - load_meta_from_disk=False, + load_fresh=True, offline=True, device="cpu", ) @@ -210,7 +210,7 @@ def test_mislabeling_evaluate( monkeypatch.setattr( md, - "_subsample_dataset", + "subsample_dataset", lambda dataset, max_n, seed: torch.utils.data.Subset( dataset, list(range(5)) ), diff --git a/tests/benchmarks/downstream_eval/test_mixed_datasets.py b/tests/benchmarks/downstream_eval/test_mixed_datasets.py index 1ef95b64..acc6db12 100644 --- a/tests/benchmarks/downstream_eval/test_mixed_datasets.py +++ b/tests/benchmarks/downstream_eval/test_mixed_datasets.py @@ -107,7 +107,7 @@ def test_train_dataset_indexing_is_correct(config_name, tmp_path): "train_acc": 0.95, "val_acc": 0.79, "train_adversarial_memorization": 0.95, - "eval_adversarial_memorization": 0.95, + "eval_adversarial_memorization": 0.94, }, ), ], diff --git a/tests/benchmarks/downstream_eval/test_shortcut_detection.py b/tests/benchmarks/downstream_eval/test_shortcut_detection.py index 4862ff31..97d461c8 100644 --- a/tests/benchmarks/downstream_eval/test_shortcut_detection.py +++ b/tests/benchmarks/downstream_eval/test_shortcut_detection.py @@ -93,7 +93,7 @@ def test_shortcut_extra_kwargs_missing_cls_idx_raises(): train_dataset=fake_ds, eval_dataset=fake_ds, metadata_dir="/tmp", - load_meta_from_disk=False, + load_fresh=True, ) @@ -135,13 +135,13 @@ def test_shortcut_detection( with pytest.raises(expected_score): dst_eval = ShortcutDetection.from_config( config=config, - load_meta_from_disk=load_from_disk, + load_fresh=not load_from_disk, ) return dst_eval = ShortcutDetection.from_config( config=config, - load_meta_from_disk=load_from_disk, + load_fresh=not load_from_disk, ) score = dst_eval.evaluate( diff --git a/tests/benchmarks/ground_truth/test_linear_datamodeling.py b/tests/benchmarks/ground_truth/test_linear_datamodeling.py index 057dd801..490cb39c 100644 --- a/tests/benchmarks/ground_truth/test_linear_datamodeling.py +++ b/tests/benchmarks/ground_truth/test_linear_datamodeling.py @@ -315,29 +315,6 @@ def test_push_subset_missing_ckpt_dir_raises(tmp_path): LinearDatamodeling.push_subset(config=config, idx=0) -@pytest.mark.benchmarks -def test_extra_kwargs_missing_subset_ids_raises( - load_mnist_linear_datamodeling_config, tmp_path -): - """Missing subset_ids file + load_meta_from_disk=True raises.""" - config = load_mnist_linear_datamodeling_config - metadata_dir = str(tmp_path / "meta") - os.makedirs(metadata_dir, exist_ok=True) - - with pytest.raises(FileNotFoundError, match="Subset ids file not found"): - LinearDatamodeling._extra_kwargs_from_config( - config=config, - train_dataset=torch.utils.data.TensorDataset( - torch.randn(4, 1, 28, 28), torch.randint(0, 10, (4,)) - ), - eval_dataset=torch.utils.data.TensorDataset( - torch.randn(4, 1, 28, 28), torch.randint(0, 10, (4,)) - ), - metadata_dir=metadata_dir, - load_meta_from_disk=True, - ) - - @pytest.mark.benchmarks @pytest.mark.parametrize("skip_subsets", [False, True]) def test_train_and_push_to_hub(mocker, skip_subsets): @@ -555,8 +532,7 @@ def test_lds_cache_subset_logits_writes_per_batch_files(mocker, tmp_path): fake.eval_dataset = mocker.MagicMock() mocker.patch.object(LinearDatamodeling, "from_config", return_value=fake) mocker.patch( - "quanda.benchmarks.ground_truth.linear_datamodeling." - "_subsample_dataset", + "quanda.benchmarks.ground_truth.linear_datamodeling.subsample_dataset", side_effect=lambda ds, **kw: ds, ) handler = mocker.MagicMock() @@ -787,8 +763,7 @@ def test_evaluate_dataset_skips_missing_subset_logits_file(mocker, tmp_path): def _patch_lds_cache_dependencies(mocker, batches, fake_logits): """Mock the I/O collaborators around cache_subset_logits_per_idx.""" mocker.patch( - "quanda.benchmarks.ground_truth.linear_datamodeling." - "_subsample_dataset", + "quanda.benchmarks.ground_truth.linear_datamodeling.subsample_dataset", side_effect=lambda ds, **kw: ds, ) handler = mocker.MagicMock() @@ -947,8 +922,7 @@ def test_lds_cache_subset_logits_per_idx_chunked_inference( batch = torch.randn(5, 4) mocker.patch( - "quanda.benchmarks.ground_truth.linear_datamodeling." - "_subsample_dataset", + "quanda.benchmarks.ground_truth.linear_datamodeling.subsample_dataset", side_effect=lambda ds, **kw: ds, ) handler = mocker.MagicMock() @@ -1009,8 +983,7 @@ def forward(self, **kwargs): "attention_mask": torch.ones(5, 3), } mocker.patch( - "quanda.benchmarks.ground_truth.linear_datamodeling." - "_subsample_dataset", + "quanda.benchmarks.ground_truth.linear_datamodeling.subsample_dataset", side_effect=lambda ds, **kw: ds, ) handler = mocker.MagicMock() diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py index be9aba79..725bb0f3 100644 --- a/tests/benchmarks/test_benchmarks.py +++ b/tests/benchmarks/test_benchmarks.py @@ -1,6 +1,5 @@ """Contains tests common to all benchmarks.""" -import functools import math import os @@ -180,58 +179,20 @@ def _no_network(self, *args, **kwargs): @pytest.mark.benchmarks -@pytest.mark.parametrize( - "test_id, entrypoint, bench_cls, fixture_or_bench_id", - [ - ( - "load_pretrained", - "load_pretrained", - ClassDetection, - "mnist_class_detection_unit", - ), - ( - "from_config_class", - "from_config", - ClassDetection, - "load_mnist_unit_test_config", - ), - ( - "from_config_mixed", - "from_config", - MixedDatasets, - "load_mnist_mixed_config", - ), - ], -) -def test_offline_and_fresh_incompatible( - test_id, - entrypoint, - bench_cls, - fixture_or_bench_id, - tmp_path, - request, -): - """offline=True and load_fresh=True must raise on every entrypoint.""" - if entrypoint == "load_pretrained": - call = functools.partial( - bench_cls.load_pretrained, - bench_id=fixture_or_bench_id, +def test_load_pretrained_offline_and_fresh_incompatible(tmp_path): + """offline=True and load_fresh=True must raise in load_pretrained. + + For from_config the combination is meaningful (regenerate cached + metadata locally without hitting the Hub), so it is allowed. + """ + with pytest.raises(ValueError, match="incompatible"): + ClassDetection.load_pretrained( + bench_id="mnist_class_detection_unit", cache_dir=str(tmp_path), offline=True, load_fresh=True, device="cpu", ) - else: - config = request.getfixturevalue(fixture_or_bench_id) - config["bench_save_dir"] = str(tmp_path) - call = functools.partial( - bench_cls.from_config, - config=config, - offline=True, - load_fresh=True, - ) - with pytest.raises(ValueError, match="incompatible"): - call() @pytest.mark.benchmarks @@ -362,21 +323,6 @@ def test_iter_explanations_requires_explainer_when_no_cache(): ) -@pytest.mark.benchmarks -def test_load_meta_from_disk_missing_raises( - load_mnist_shortcut_config, tmp_path -): - """load_meta_from_disk=True must raise when metadata is missing.""" - config = load_mnist_shortcut_config - config["bench_save_dir"] = str(tmp_path) - with pytest.raises(FileNotFoundError): - ShortcutDetection.from_config( - config=config, - load_meta_from_disk=True, - offline=True, - ) - - @pytest.mark.benchmarks @pytest.mark.parametrize( "test_id, bench_id, bench_cls", @@ -672,7 +618,7 @@ def test_bench_from_config( config["cache_dir"] = "bench_out" dst_eval = bench_cls.from_config( config=config, - load_meta_from_disk=load_from_disk, + load_fresh=not load_from_disk, offline=offline, ) @@ -725,7 +671,7 @@ def test_bench_from_config_bootstrap( dst_eval = bench_cls.from_config( config=config, - load_meta_from_disk=True, + load_fresh=False, offline=True, ) @@ -857,7 +803,7 @@ def test_train_from_config( dst_eval = bench_cls.train( config=config, logger=logger, - # load_meta_from_disk=load_from_disk, + # load_fresh=not load_from_disk, ) score = dst_eval.evaluate( @@ -918,7 +864,7 @@ def test_save_filtered_indices( bench = ShortcutDetection.from_config( config=config, - load_meta_from_disk=False, + load_fresh=True, offline=True, ) @@ -954,7 +900,7 @@ def test_filter_by_prediction_branches( bench = ShortcutDetection.from_config( config=config, - load_meta_from_disk=False, + load_fresh=True, offline=True, ) @@ -1068,7 +1014,7 @@ def test_sanity_from_config( config["cache_dir"] = str(tmp_path) dst_eval = bench_cls.from_config( config=config, - load_meta_from_disk=load_from_disk, + load_fresh=not load_from_disk, ) sanity_results = dst_eval.sanity_check() diff --git a/tests/benchmarks/test_config_parser.py b/tests/benchmarks/test_config_parser.py index aa34cd75..be9d7ebb 100644 --- a/tests/benchmarks/test_config_parser.py +++ b/tests/benchmarks/test_config_parser.py @@ -175,36 +175,6 @@ def test_resolve_split_recipe_returns_copy(): assert result is not recipe -@pytest.mark.utils -def test_apply_wrapper_missing_metadata_raises( - load_mnist_mislabeling_config, tmp_path -): - """load_meta_from_disk=True with missing metadata file raises.""" - config = load_mnist_mislabeling_config - metadata_dir = str(tmp_path / "meta") - os.makedirs(metadata_dir, exist_ok=True) - - dummy_ds = torch.utils.data.TensorDataset( - torch.randn(4, 1, 28, 28), torch.randint(0, 10, (4,)) - ) - - ds_cfg = config["train_dataset"] - wrapper_cfg = dict(ds_cfg["wrapper"]) - wrapper_cfg["metadata"] = { - **wrapper_cfg["metadata"], - "metadata_filename": "not_there.yaml", - } - - with pytest.raises(FileNotFoundError, match="Wrapper metadata"): - DatasetConfigParser._apply_wrapper( - dataset=dummy_ds, - ds_config=ds_cfg, - wrapper_cfg=wrapper_cfg, - metadata_dir=metadata_dir, - load_meta_from_disk=True, - ) - - @pytest.mark.utils def test_load_pretrained_base_returns_none_when_key_absent(): """Without ``pretrained_model_name`` in the cfg, ``load_pretrained_base`` diff --git a/tests/explainers/test_cache_explainer.py b/tests/explainers/test_cache_explainer.py index acb993f7..794f21c8 100644 --- a/tests/explainers/test_cache_explainer.py +++ b/tests/explainers/test_cache_explainer.py @@ -15,7 +15,7 @@ from quanda.benchmarks.downstream_eval import ClassDetection from quanda.explainers.wrappers import CaptumSimilarity from quanda.utils.cache import BatchedCachedExplanations, ExplanationsCache -from quanda.utils.common import _stable_repr +from quanda.utils.common import stable_repr from quanda.utils.functions import cosine_similarity @@ -30,7 +30,7 @@ def test_hash_expl_kwargs_is_order_invariant(): def test_hash_expl_kwargs_is_stable_for_callables(): """Callables must hash by module.qualname, not ``id(obj)``.""" - rep = _stable_repr(cosine_similarity) + rep = stable_repr(cosine_similarity) assert "at 0x" not in rep assert rep == ( f"{cosine_similarity.__module__}.{cosine_similarity.__qualname__}" @@ -96,7 +96,7 @@ def test_benchmark_explain_and_precomputed_evaluate_match( } direct = ClassDetection.from_config( - config=config, load_meta_from_disk=True, offline=True + config=config, load_fresh=False, offline=True ) baseline = direct.evaluate( explainer_cls=CaptumSimilarity, @@ -132,7 +132,7 @@ def test_benchmark_explain_and_precomputed_evaluate_match( fresh = ClassDetection.from_config( config=config, - load_meta_from_disk=True, + load_fresh=False, offline=True, ) cached_score = fresh.evaluate( diff --git a/tests/integration/test_benchmark_integration.py b/tests/integration/test_benchmark_integration.py index 495fa066..ecf03610 100644 --- a/tests/integration/test_benchmark_integration.py +++ b/tests/integration/test_benchmark_integration.py @@ -10,6 +10,7 @@ ShortcutDetection, SubclassDetection, ) +from quanda.benchmarks.ground_truth import LinearDatamodeling # END1 # START14_1 @@ -139,7 +140,7 @@ def test_benchmark_integration( # END9 # Override for faster testing - representer_points_args["epoch"] = 1 + representer_points_args["epoch"] = 5 # START10 attributors = { @@ -187,3 +188,72 @@ def test_benchmark_integration( }, ) # END15 + + lds_cache = os.path.join(cache_dir, "lds_bench") + + # START17 + lds_config = "mnist_linear_datamodeling_unit" + + lds_bench = LinearDatamodeling.load_pretrained( + bench_id=lds_config, + cache_dir=lds_cache, + device=device, + ) + # END17 + + # START18 + expl_kwargs = { + "model_id": "mnist_lds_tutorial", + "layers": "fc_2", + "cache_dir": os.path.join(cache_dir, "lds_captum_similarity"), + } + expl_cache_dir = os.path.join(cache_dir, "lds_explanations") + + LinearDatamodeling.explain( + config=lds_config, + explainer_cls=CaptumSimilarity, + expl_kwargs=expl_kwargs, + cache_dir=expl_cache_dir, + device=device, + batch_size=8, + ) + + subset_logits_dir = LinearDatamodeling.cache_subset_logits( + config=lds_config, + cache_dir=os.path.join(cache_dir, "lds_subset_logits"), + device=device, + batch_size=8, + ) + + lds_results = lds_bench.evaluate( + explainer_cls=CaptumSimilarity, + expl_kwargs=expl_kwargs, + cache_dir=expl_cache_dir, + use_cached_expl=True, + subset_logits_dir=subset_logits_dir, + batch_size=8, + ) + print(f"Linear Datamodeling Score: {lds_results['score']}") + # END18 + + from quanda.utils.common import resolve_config as _resolve_lds_cfg + + lds_train_config = _resolve_lds_cfg(lds_config) + lds_train_config["model"]["trainer"]["max_epochs"] = 1 + + # START19 + LinearDatamodeling.train( + lds_train_config, + device=device, + skip_subsets=True, + ) + # END19 + + # START20 + for idx in range(3): + LinearDatamodeling.train_subset( + lds_train_config, + idx=idx, + device=device, + ) + # END20 diff --git a/tests/integration/test_quickstart.py b/tests/integration/test_quickstart.py index 27128054..d797f98f 100644 --- a/tests/integration/test_quickstart.py +++ b/tests/integration/test_quickstart.py @@ -8,6 +8,7 @@ import quanda from quanda.benchmarks.downstream_eval import ( + ClassDetection, MislabelingDetection, SubclassDetection, ) @@ -144,15 +145,48 @@ def test_quickstart( ) # END7_1 + # START_TRAK_0 + class_detect = ClassDetection.load_pretrained( + bench_id="mnist_class_detection", cache_dir=cache_dir + ) + # START_TRAK_0 + + subclass_detect.train_dataset.dataset = torch.utils.data.Subset( + subclass_detect.train_dataset.dataset, range(64) + ) + class_detect.train_dataset = torch.utils.data.Subset( + class_detect.train_dataset, range(64) + ) + # START7_2 score = subclass_detect.evaluate( explainer_cls=CaptumSimilarity, expl_kwargs=explainer_kwargs, batch_size=batch_size, + max_eval_n=16, )["score"] print(f"Subclass Detection Score: {score}") # END7_2 + # START_TRAK_1 + trak_expl_kwargs = { + "model_id": "trak_subclass_detect_model_id", + "cache_dir": cache_dir, + "proj_dim": 512, + "seed": 42, + } + # END_TRAK_1 + + # START_TRAK_2 + result = class_detect.evaluate( + explainer_cls=quanda.TRAK, + expl_kwargs=trak_expl_kwargs, + batch_size=batch_size, + max_eval_n=16, + )["score"] + print(f"Class Detection Score (TRAK): {result}") + # END_TRAK_2 + # START9 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" model.to(DEVICE) @@ -209,11 +243,16 @@ def test_quickstart( ) # END13_2 + mislabeling_detection.train_dataset.dataset = torch.utils.data.Subset( + mislabeling_detection.train_dataset.dataset, range(64) + ) + # START14 score = mislabeling_detection.evaluate( explainer_cls=CaptumSimilarity, expl_kwargs=explainer_kwargs, batch_size=batch_size, + max_eval_n=16, )["score"] print(f"Mislabeling Detection Score: {score}") # END14 diff --git a/tests/utils/datasets/transformed/test_metadata.py b/tests/utils/datasets/transformed/test_metadata.py index 0ee14a85..b8695875 100644 --- a/tests/utils/datasets/transformed/test_metadata.py +++ b/tests/utils/datasets/transformed/test_metadata.py @@ -106,9 +106,7 @@ def test_metadata_validate_raises(test_id, kwargs, error_match): def test_class_mapping_resolve_integer_keys(tmp_path): """A spec with int keys is returned as a direct ClassMapping.""" spec = {0: 0, 1: 1, 2: 0, 3: 1} - mapping = ClassMapping.resolve( - spec=spec, metadata_dir=str(tmp_path), load_meta_from_disk=False - ) + mapping = ClassMapping.resolve(spec=spec, metadata_dir=str(tmp_path)) assert mapping.class_to_group == spec assert mapping.n_classes == 4 assert mapping.n_groups == 2 diff --git a/tests/utils/test_common.py b/tests/utils/test_common.py index e68ff683..5e2931b9 100644 --- a/tests/utils/test_common.py +++ b/tests/utils/test_common.py @@ -1,14 +1,64 @@ import pytest import torch +import yaml from quanda.utils.common import ( DatasetSplit, class_accuracy, get_targets, make_func, + resolve_config, ) +@pytest.mark.utils +def test_resolve_config_dict_passes_through(): + cfg = {"id": "x", "bench": "ClassDetection"} + assert resolve_config(cfg) is cfg + + +@pytest.mark.utils +def test_resolve_config_registered_bench_id(): + """A registered ``bench_id`` resolves via ``config_map`` and parses.""" + cfg = resolve_config("mnist_class_detection_unit") + assert isinstance(cfg, dict) + assert cfg.get("id") + assert cfg.get("bench") + + +@pytest.mark.utils +def test_resolve_config_yaml_path(tmp_path): + """An unregistered string is treated as a path to a YAML file.""" + path = tmp_path / "cfg.yaml" + path.write_text(yaml.safe_dump({"id": "from-disk", "k": 7})) + cfg = resolve_config(str(path)) + assert cfg == {"id": "from-disk", "k": 7} + + +@pytest.mark.utils +def test_resolve_config_rejects_non_mapping_yaml(tmp_path): + """A YAML file that doesn't parse to a dict raises ``TypeError``.""" + path = tmp_path / "list.yaml" + path.write_text(yaml.safe_dump([1, 2, 3])) + with pytest.raises(TypeError, match="did not parse to a dict"): + resolve_config(str(path)) + + +@pytest.mark.utils +@pytest.mark.parametrize("bad", [None, 42, 3.14, ["x"], object()]) +def test_resolve_config_rejects_other_types(bad): + with pytest.raises(TypeError, match="must be a dict"): + resolve_config(bad) + + +@pytest.mark.utils +def test_resolve_config_missing_path_raises(tmp_path): + """An unregistered string that isn't a real file raises ``FileNotFoundError``.""" + missing = str(tmp_path / "does_not_exist.yaml") + with pytest.raises(FileNotFoundError): + resolve_config(missing) + + @pytest.mark.utils @pytest.mark.parametrize( "test_id", diff --git a/tutorials/demo_benchmarks.ipynb b/tutorials/demo_benchmarks.ipynb index 22307624..52d23711 100644 --- a/tutorials/demo_benchmarks.ipynb +++ b/tutorials/demo_benchmarks.ipynb @@ -2,12 +2,24 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "3aedf05ce959fce0", "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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AICVkhwEAAAAAAAAAAArJTkIAAAAAAAAAABWSnIQAAAAAAAAAACslOQgAAAAAAAAAABYKclBAAAAAAAAAACwUpKDAAAAAAAAAABgpSQHAQAAAAAAAADASkkOAgAAAAAAAACAlZIcBAAAAAAAAAAAKyU5CAAAAAAAAAAAVkpyEAAAAAAAAAAArJTkIAAAAAAAAAAAWCnJQQAAAAAAAAAAsFKSgwAAAAAAAAAAYKUkBwEAAAAAAAAAwEpJDgIAAAAAAAAAgJWSHAQAAAAAAAAAACslOQgAAAAAAAAAAFZKchAAAAAAAAAAAKyU5CAAAAAAAAAAAFgpyUEAAAAAAAAAALBSkoMAAAAAAAAAAGClJAcBAAAAAAAAAMBKSQ4CAAAAAAAAAICVkhwEAAAAAAAAAAArJTkIAAAAAAAAAABWSnIQAAAAAAAAAACslOQgAAAAAAAAAABYKclBAAAAAAAAAACwUpKDAAAAAAAAAABgpSQHAQAAAAAAAADASkkOAgAAAAAAAACAlZIcBAAAAAAAAAAAKyU5CAAAAAAAAAAAVkpyEAAAAAAAAAAArJTkIAAAAAAAAAAAWCnJQQAAAAAAAAAAsFKSgwAAAAAAAAAAYKUkBwEAAAAAAAAAwEpJDgIAAAAAAAAAgJWSHAQAAAAAAAAAACslOQgAAAAAAAAAAFZKchAAAAAAAAAAAKyU5CAAAAAAAAAAAFgpyUEAAAAAAAAAALBSkoMAAAAAAAAAAGClJAcBAAAAAAAAAMBKSQ4CAAAAAAAAAICVkhwEAAAAAAAAAAArJTkIAAAAAAAAAABWSnIQAAAAAAAAAACslOQgAAAAAAAAAABYKclBAAAAAAAAAACwUpKDAAAAAAAAAABgpSQHAQAAAAAAAADASkkOAgAAAAAAAACAlZIcBAAAAAAAAAAAKyU5CAAAAAAAAAAAVkpyEAAAAAAAAAAArJTkIAAAAAAAAAAAWCnJQQAAAAAAAAAAsFKSgwAAAAAAAAAAYKUkBwEAAAAAAAAAwEpJDgIAAAAAAAAAgJWSHAQAAAAAAAAAACslOQgAAAAAAAAAAFZKchAAAAAAAAAAAKyU5CAAAAAAAAAAAFgpyUEAAAAAAAAAALBSkoMAAAAAAAAAAGClJAcBAAAAAAAAAMBKSQ4CAAAAAAAAAICVkhwEAAAAAAAAAAArJTkIAAAAAAAAAABW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", + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from IPython.display import Image\n", "\n", @@ -32,10 +44,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "70692853", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/bareeva/Projects/quanda/.venv/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], "source": [ "import os\n", "import sys\n", @@ -59,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "9f3d5ae4", "metadata": {}, "outputs": [], @@ -90,10 +111,43 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "1043f55a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-06 12:11:34,147 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/datasets/quanda-bench-test/124627f-default_ShortcutDetection_metadata/revision/main \"HTTP/1.1 200 OK\"\n", + "Fetching 6 files: 100%|██████████| 6/6 [00:00<00:00, 1558.74it/s]\n", + "2026-05-06 12:11:34,286 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/main/README.md \"HTTP/1.1 307 Temporary Redirect\"\n", + "2026-05-06 12:11:34,293 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/datasets/ylecun/mnist/77f3279092a1c1579b2250db8eafed0ad422088c/README.md \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:34,424 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:34,788 - httpx - INFO - HTTP Request: HEAD https://s3.amazonaws.com/datasets.huggingface.co/datasets/datasets/ylecun/mnist/ylecun/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:34,921 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/datasets/ylecun/mnist/revision/77f3279092a1c1579b2250db8eafed0ad422088c \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:35,045 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/.huggingface.yaml \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:35,257 - httpx - INFO - HTTP Request: GET https://datasets-server.huggingface.co/info?dataset=ylecun/mnist \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:35,386 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/datasets/ylecun/mnist/tree/77f3279092a1c1579b2250db8eafed0ad422088c/mnist?recursive=true&expand=false \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:35,513 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/datasets/ylecun/mnist/tree/77f3279092a1c1579b2250db8eafed0ad422088c?recursive=false&expand=false \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:35,657 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/dataset_infos.json \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:37,449 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/main/README.md \"HTTP/1.1 307 Temporary Redirect\"\n", + "2026-05-06 12:11:37,456 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/datasets/ylecun/mnist/77f3279092a1c1579b2250db8eafed0ad422088c/README.md \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:37,585 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:37,693 - httpx - INFO - HTTP Request: HEAD https://s3.amazonaws.com/datasets.huggingface.co/datasets/datasets/ylecun/mnist/ylecun/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:37,828 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/.huggingface.yaml \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:37,962 - httpx - INFO - HTTP Request: GET https://datasets-server.huggingface.co/info?dataset=ylecun/mnist \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:38,092 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/dataset_infos.json \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:39,405 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/main/README.md \"HTTP/1.1 307 Temporary Redirect\"\n", + "2026-05-06 12:11:39,411 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/datasets/ylecun/mnist/77f3279092a1c1579b2250db8eafed0ad422088c/README.md \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:39,540 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:39,665 - httpx - INFO - HTTP Request: HEAD https://s3.amazonaws.com/datasets.huggingface.co/datasets/datasets/ylecun/mnist/ylecun/mnist.py \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:39,796 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/.huggingface.yaml \"HTTP/1.1 404 Not Found\"\n", + "2026-05-06 12:11:39,929 - httpx - INFO - HTTP Request: GET https://datasets-server.huggingface.co/info?dataset=ylecun/mnist \"HTTP/1.1 200 OK\"\n", + "2026-05-06 12:11:40,059 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/datasets/ylecun/mnist/resolve/77f3279092a1c1579b2250db8eafed0ad422088c/dataset_infos.json \"HTTP/1.1 404 Not Found\"\n" + ] + } + ], "source": [ "cache_dir = str(os.path.join(os.getcwd(), \"quanda_benchmark_tutorial_cache\"))\n", "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", @@ -114,10 +168,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "e516d03e70a7710f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/jpeg": "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", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "shortcut_img = benchmark.train_dataset[benchmark.train_dataset.transform_indices[15]][0]\n", "tensor_img = torch.concat([shortcut_img, shortcut_img, shortcut_img], dim=0)\n", @@ -127,10 +194,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "d2f4e896", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(0.9982)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# load a model checkpoint\n", "benchmark.load_last_checkpoint()\n", @@ -351,7 +429,15 @@ "id": "d860e0bb", "metadata": {}, "outputs": [], - "source": "with open(\n \"tests/assets/mnist_local_bench/83edb41-default_MislabelingDetection.yaml\",\n \"r\",\n) as f:\n mislabel_config = yaml.safe_load(f)\n\nmislabel_config" + "source": [ + "with open(\n", + " \"tests/assets/mnist_local_bench/83edb41-default_MislabelingDetection.yaml\",\n", + " \"r\",\n", + ") as f:\n", + " mislabel_config = yaml.safe_load(f)\n", + "\n", + "mislabel_config" + ] }, { "cell_type": "markdown", @@ -373,7 +459,21 @@ "id": "d7028613", "metadata": {}, "outputs": [], - "source": "with open(\n \"tests/assets/mnist_local_bench/83edb41-default_SubclassDetection.yaml\",\n \"r\",\n) as f:\n subclass_config = yaml.safe_load(f)\n\n# Override for faster training in this tutorial\nsubclass_config[\"model\"][\"trainer\"][\"max_epochs\"] = 5\n\nbenchmark = SubclassDetection.train(\n subclass_config,\n device=device,\n)" + "source": [ + "with open(\n", + " \"tests/assets/mnist_local_bench/83edb41-default_SubclassDetection.yaml\",\n", + " \"r\",\n", + ") as f:\n", + " subclass_config = yaml.safe_load(f)\n", + "\n", + "# Override for faster training in this tutorial\n", + "subclass_config[\"model\"][\"trainer\"][\"max_epochs\"] = 5\n", + "\n", + "benchmark = SubclassDetection.train(\n", + " subclass_config,\n", + " device=device,\n", + ")" + ] }, { "cell_type": "markdown", @@ -423,4 +523,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/tutorials/demo_explainers.ipynb b/tutorials/demo_explainers.ipynb index c472b577..c00052f4 100644 --- a/tutorials/demo_explainers.ipynb +++ b/tutorials/demo_explainers.ipynb @@ -1,5 +1,29 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c35082bf", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AAAAYKYkBwEAAAAAAAAAwExJDgIAAAAAAAAAgJmSHAQAAAAAAAAAADMlOQgAAAAAAAAAAGZKchAAAAAAAAAAAMyU5CAAAAAAAAAAAJgpyUEAAAAAAAAAADBTkoMAAAAAAAAAAGCmJAcBAAAAAAAAAMBMSQ4CAAAAAAAAAICZkhwEAAAAAAAAAAAzJTkIAAAAAAAAAABmSnIQAAAAAAAAAADMlOQgAAAAAAAAAACYKclBAAAAAAAAAAAwU5KDAAAAAAAAAABgpiQHAQAAAAAAAADATEkOAgAAAAAAAACAmZIcBAAAAAAAAAAAMyU5CAAAAAAAAAAAZkpyEAAAAAAAAAAAzJTkIAAAAAAAAAAAmCnJQQAAAAAAAAAAMFOSgwAAAAAAAAAAYKYkBwEAAAAAAAAAwExJDgIAAAAAAAAAgJmSHAQAAAAAAAAAADMlOQgAAAAAAAAAAGZKchAAAAAAAAAAAMyU5CAAAAAAAAAAAJgpyUEAAAAAAAAAADBTkoMAAAAAAAAAAGCmJAcBAAAAAAAAAMBMSQ4CAAAAAAAAAICZkhwEAAAAAAAAAAAzJTkIAAAAAAAAAABmSnIQAAAAAAAAAADMlOQgAAAAAAAAAACYKclBAAAAAAAAAAAwU5KDAAAAAAAAAABgpiQHAQAAAAAAAADATEkOAgAAAAAAAACAmZIcBAAAAAAAAAAAMyU5CAAAAAAAAAAAZkpyEAAAAAAAAAAAzJTkIAAAAAAAAAAAmCnJQQAAAAAAAAAAMFOSgwAAAAAAAAAAYKYkBwEAAAAAAAAAwExJDgIAAAAAAAAAgJmSHAQAAAAAAAAAADMlOQgAAAAAAAAAAGZKchAAAAAAAAAAAMyU5CAAAAAAAAAAAJgpyUEAAAAAAAAAADBTkoMAAAAAAAAAAGCmJAcBAAAAAAAAAMBMSQ4CAAAAAAAAAICZkhwEAAAAAAAAAAAzJTkIAAAAAAAAAABmSnIQAAAAAAAAAADMlOQgAAAAAAAAAACYKclBAAAAAAAAAAAwU5KDAAAAAAAAAABgpiQHAQAAAAAAAADATEkOAgAAAAAAAACAmZIcBAAAAAAAAAAAMyU5CAAAAAAAAAAAZkpyEAAAAAAAAAAAzJTkIAAAAAAAAAAAmCnJQQAAAAAAAAAAMFOSgwAAAAAAAAAAYKYkBwEAAAAAAAAAwExJDgIAAAAAAAAA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", + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(\"../assets/demo/quanda_explainers_demo.png\")" + ] + }, { "cell_type": "markdown", "id": "intro",
Metric Type ModalityBenchmarks (Dataset / Model)Benchmark IDs (Dataset / Model)
TopKCardinalityMetric Heuristic Visionmnist_top_k_cardinality (MNIST / LeNet), cifar_top_k_cardinality (CIFAR-10 / ResNet-9), awa2_top_k_cardinality (AWA2 / ResNet-50)mnist_top_k_cardinality (MNIST / LeNet)
cifar_top_k_cardinality (CIFAR-10 / ResNet-9)
awa2_top_k_cardinality (AWA2 / ResNet-50)
TextModelRandomizationMetric Heuristic Visionmnist_model_randomization (MNIST / LeNet), cifar_model_randomization (CIFAR-10 / ResNet-9), awa2_model_randomization (AWA2 / ResNet-50)mnist_model_randomization (MNIST / LeNet)
cifar_model_randomization (CIFAR-10 / ResNet-9)
awa2_model_randomization (AWA2 / ResNet-50)
TextMixedDatasetsMetric Heuristic Visionmnist_mixed_datasets (MNIST / LeNet), cifar_mixed_datasets (CIFAR-10 / ResNet-9), awa2_mixed_datasets (AWA2 / ResNet-50)mnist_mixed_datasets (MNIST / LeNet)
cifar_mixed_datasets (CIFAR-10 / ResNet-9)
awa2_mixed_datasets (AWA2 / ResNet-50)
TextClassDetectionMetric Downstream-Task-Evaluator Visionmnist_class_detection (MNIST / LeNet), cifar_class_detection (CIFAR-10 / ResNet-9), awa2_class_detection (AWA2 / ResNet-50)mnist_class_detection (MNIST / LeNet)
cifar_class_detection (CIFAR-10 / ResNet-9)
awa2_class_detection (AWA2 / ResNet-50)
TextSubclassDetectionMetric Downstream-Task-Evaluator Visionmnist_subclass_detection (MNIST / LeNet), cifar_subclass_detection (CIFAR-10 / ResNet-9), awa2_subclass_detection (AWA2 / ResNet-50)mnist_subclass_detection (MNIST / LeNet)
cifar_subclass_detection (CIFAR-10 / ResNet-9)
awa2_subclass_detection (AWA2 / ResNet-50)
MislabelingDetectionMetric Downstream-Task-Evaluator Visionmnist_mislabeling_detection (MNIST / LeNet), cifar_mislabeling_detection (CIFAR-10 / ResNet-9), awa2_mislabeling_detection (AWA2 / ResNet-50)mnist_mislabeling_detection (MNIST / LeNet)
cifar_mislabeling_detection (CIFAR-10 / ResNet-9)
awa2_mislabeling_detection (AWA2 / ResNet-50)
TextShortcutDetectionMetric Downstream-Task-Evaluator Visionmnist_shortcut_detection (MNIST / LeNet), cifar_shortcut_detection (CIFAR-10 / ResNet-9), awa2_shortcut_detection (AWA2 / ResNet-50)mnist_shortcut_detection (MNIST / LeNet)
cifar_shortcut_detection (CIFAR-10 / ResNet-9)
awa2_shortcut_detection (AWA2 / ResNet-50)
MRRMetricLinearDatamodelingMetric Ground Truth Visionmnist_linear_datamodeling (MNIST / LeNet), cifar_linear_datamodeling (CIFAR-10 / ResNet-9), awa2_linear_datamodeling (AWA2 / ResNet-50)mnist_linear_datamodeling (MNIST / LeNet)
cifar_linear_datamodeling (CIFAR-10 / ResNet-9)
awa2_linear_datamodeling (AWA2 / ResNet-50)
Text