From 0fadae6f6c8679abebe0a0c18a12e4c046d545fb Mon Sep 17 00:00:00 2001 From: "Documenter.jl" Date: Wed, 17 Apr 2024 12:49:30 +0000 Subject: [PATCH] build based on 871d0ae --- dev/.documenter-siteinfo.json | 2 +- dev/CHANGELOG/index.html | 2 + dev/LICENSE | 21 + dev/_contribute/index.html | 2 - dev/_intro.qmd | 18 +- dev/assets/resources/index.html | 2 +- dev/contribute.qmd | 30 +- dev/contribute/index.html | 2 +- dev/contribute/performance/index.html | 4 +- dev/explanation/architecture.qmd | 60 +- dev/explanation/architecture/index.html | 2 +- .../figure-commonmark/mermaid-figure-1.png | Bin 0 -> 296623 bytes dev/explanation/categorical/index.html | 4 +- .../generators/clap_roar/index.html | 4 +- dev/explanation/generators/clue/index.html | 4 +- dev/explanation/generators/dice/index.html | 4 +- .../generators/feature_tweak/index.html | 4 +- dev/explanation/generators/generic/index.html | 4 +- .../generators/gravitational/index.html | 4 +- dev/explanation/generators/greedy/index.html | 4 +- .../generators/growing_spheres/index.html | 4 +- .../generators/overview/index.html | 4 +- dev/explanation/generators/probe/index.html | 4 +- dev/explanation/generators/revise/index.html | 4 +- dev/explanation/index.html | 2 +- dev/explanation/optimisers/jsma/index.html | 4 +- .../optimisers/overview/index.html | 2 +- dev/extensions/index.html | 2 +- dev/extensions/laplace_redux/index.html | 4 +- dev/extensions/neurotree/index.html | 4 +- .../custom_generators/index.html | 4 +- dev/how_to_guides/custom_models/index.html | 4 +- dev/how_to_guides/index.html | 2 +- dev/index.html | 6 +- .../figure-commonmark/cell-10-output-1.svg | 26 +- .../figure-commonmark/cell-15-output-1.svg | 2766 ++++++------- .../figure-commonmark/cell-5-output-1.svg | 168 +- dev/objects.inv | Bin 6402 -> 6544 bytes dev/reference/index.html | 99 +- dev/release-notes/index.html | 2 + dev/search_index.js | 2 +- dev/tutorials/benchmarking/index.html | 4 +- dev/tutorials/convergence.qmd | 68 + dev/tutorials/convergence/index.html | 13 + .../figure-commonmark/cell-7-output-1.svg | 3416 +++++++++++++++++ dev/tutorials/data_catalogue/index.html | 4 +- dev/tutorials/data_preprocessing/index.html | 4 +- dev/tutorials/evaluation/index.html | 4 +- dev/tutorials/generators/index.html | 4 +- dev/tutorials/index.html | 2 +- dev/tutorials/model_catalogue/index.html | 4 +- dev/tutorials/models/index.html | 4 +- dev/tutorials/parallelization/index.html | 4 +- dev/tutorials/simple_example/index.html | 4 +- dev/tutorials/whistle_stop/index.html | 4 +- dev/www/pkg_architecture.mmd | 37 - dev/www/pkg_architecture.png | Bin 159381 -> 0 bytes 57 files changed, 5206 insertions(+), 1660 deletions(-) create mode 100644 dev/CHANGELOG/index.html create mode 100755 dev/LICENSE delete mode 100644 dev/_contribute/index.html create mode 100644 dev/explanation/architecture_files/figure-commonmark/mermaid-figure-1.png create mode 100644 dev/release-notes/index.html create mode 100644 dev/tutorials/convergence.qmd create mode 100644 dev/tutorials/convergence/index.html create mode 100644 dev/tutorials/convergence_files/figure-commonmark/cell-7-output-1.svg delete mode 100644 dev/www/pkg_architecture.mmd delete mode 100644 dev/www/pkg_architecture.png diff --git a/dev/.documenter-siteinfo.json b/dev/.documenter-siteinfo.json index 65d08137e..c0ca55f2a 100644 --- a/dev/.documenter-siteinfo.json +++ b/dev/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.10.2","generation_timestamp":"2024-04-16T07:13:22","documenter_version":"1.3.0"}} \ No newline at end of file +{"documenter":{"julia_version":"1.10.2","generation_timestamp":"2024-04-17T12:49:05","documenter_version":"1.4.0"}} \ No newline at end of file diff --git a/dev/CHANGELOG/index.html b/dev/CHANGELOG/index.html new file mode 100644 index 000000000..bceb4d704 --- /dev/null +++ b/dev/CHANGELOG/index.html @@ -0,0 +1,2 @@ + +Changelog ยท CounterfactualExplanations.jl

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Note: We try to adhere to these practices as of version 1.1.1.

[Unreleased]

Added

  • Adds a section on Convergence to the documentation, Changelog.jl functionality and a few doc tests. [#429]

[1.1.2] - 2024-04-16

Changed

  • Replaces the GIF in the README and introduction of docs for a static image.

[1.1.1] - 2024-04-15

Added

  • Added tests for LaplaceRedux extension. Bumped upper compat bound for LaplaceRedux.jl. [#428]

<!โ€“ Links generated by Changelog.jl โ€“>

[#428]: https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues/428 [#429]: https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues/429

diff --git a/dev/LICENSE b/dev/LICENSE new file mode 100755 index 000000000..cffa01eea --- /dev/null +++ b/dev/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Patrick Altmeyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dev/_contribute/index.html b/dev/_contribute/index.html deleted file mode 100644 index 9e8b802dc..000000000 --- a/dev/_contribute/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Contributing ยท CounterfactualExplanations.jl

Contributing

Our goal is to provide a go-to place for Counterfactual Explanations in Julia. To this end, the following is a non-exhaustive list of enhancements we have planned:

  1. Additional counterfactual generators and predictive models.
  2. Additional datasets for testing, evaluation and benchmarking.
  3. Support for regression models.

For a complete list, have a look at outstanding issue.

How to contribute?

Any sort of contribution is welcome, in particular:

  1. Should you spot any errors or something is not working, please just open an issue.
  2. If you want to contribute your code, please proceed as follows:
    • Fork this repo and clone your fork: git clone https://github.com/your_username/CounterfactualExplanations.jl.
    • Implement your modifications and submit a pull request.
  3. For any other questions or comments, you can also start a discussion.
diff --git a/dev/_intro.qmd b/dev/_intro.qmd index 7bb2b5b36..5425f8b18 100644 --- a/dev/_intro.qmd +++ b/dev/_intro.qmd @@ -2,7 +2,7 @@ *Counterfactual Explanations and Algorithmic Recourse in Julia.* -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliatrustworthyai.github.io/CounterfactualExplanations.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliatrustworthyai.github.io/CounterfactualExplanations.jl/dev) [![Build Status](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/juliatrustworthyai/CounterfactualExplanations.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/juliatrustworthyai/CounterfactualExplanations.jl) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) [![License](https://img.shields.io/github/license/juliatrustworthyai/CounterfactualExplanations.jl)](assets/intro.gif) [![Package Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/CounterfactualExplanations/)](https://pkgs.genieframework.com?packages=CounterfactualExplanations) [![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliatrustworthyai.github.io/CounterfactualExplanations.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliatrustworthyai.github.io/CounterfactualExplanations.jl/dev) [![Build Status](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/juliatrustworthyai/CounterfactualExplanations.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/juliatrustworthyai/CounterfactualExplanations.jl) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) [![License](https://img.shields.io/github/license/juliatrustworthyai/CounterfactualExplanations.jl)](LICENSE) [![Package Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/CounterfactualExplanations/)](https://pkgs.genieframework.com?packages=CounterfactualExplanations) [![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) ```{julia} #| echo: false @@ -349,21 +349,7 @@ Our ambition is to enhance the package through the following features: 1. Support for all supervised machine learning models trained in [`MLJ.jl`](https://alan-turing-institute.github.io/MLJ.jl/dev/). 2. Support for regression models. -## ๐Ÿ›  Contribute - -Contributions of any kind are very much welcome! Take a look at the [issue](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues) to see what things we are currently working on. - -If any of the below applies to you, this might be the right open-source project for you: - -- You're an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia. -- You're experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI. -- You're new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning. - -[\@pat-alt](https://github.com/pat-alt) here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open [issue](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues) or start a [discussion](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/discussions). - -For more details on how to contribute see [here](https://juliatrustworthyai.github.io/CounterfactualExplanations.jl/dev/contribute/). Please follow the [SciML ColPrac guide](https://github.com/SciML/ColPrac). - -There are also some general pointers for people looking to contribute to any of our Taija packages [here](https://github.com/JuliaTrustworthyAI#general-pointers-for-contributors). +{{< include /docs/src/contribute.qmd >}} ## ๐ŸŽ“ Citation diff --git a/dev/assets/resources/index.html b/dev/assets/resources/index.html index 4b6deab0b..ac16fc5a5 100644 --- a/dev/assets/resources/index.html +++ b/dev/assets/resources/index.html @@ -1,2 +1,2 @@ -๐Ÿ“š Additional Resources ยท CounterfactualExplanations.jl
+๐Ÿ“š Additional Resources ยท CounterfactualExplanations.jl
diff --git a/dev/contribute.qmd b/dev/contribute.qmd index 4b1b5f6fe..5694e3238 100644 --- a/dev/contribute.qmd +++ b/dev/contribute.qmd @@ -1,17 +1,25 @@ -```@meta -CurrentModule = CounterfactualExplanations -``` +## ๐Ÿ›  Contribute -# ๐Ÿ›  Contribute +Contributions of any kind are very much welcome! Take a look at the [issue](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues) to see what things we are currently working on. If you have an idea for a new feature or want to report a bug, please open a new issue. -Contributions of any kind are very much welcome! Take a look at the [issue](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues) to see what things we are currently working on. +### Development -If any of the below applies to you, this might be the right open-source project for you: +If your looking to contribute code, it may be helpful to check out the [Explanation](explanation/index.qmd) section of the docs. -- You're an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia. -- You're experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI. -- You're new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning. +#### Testing -[\@pat-alt](https://github.com/pat-alt) here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open [issue](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues) or start a [discussion](https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/discussions). +Please always make sure to add tests for any new features or changes. -For more details on how to contribute see [here](https://www.paltmeyer.com/CounterfactualExplanations.jl/dev/contributing/). Please follow the [SciML ColPrac guide](https://github.com/SciML/ColPrac). \ No newline at end of file +#### Documentation + +If you add new features or change existing ones, please make sure to update the documentation accordingly. The documentation is written in [Documenter.jl](https://juliadocs.github.io/Documenter.jl/stable/) and is located in the `docs/src` folder. + +#### Log Changes + +As of version `1.1.1`, we have tried to be more stringent about logging changes. Please make sure to add a note to the [CHANGELOG.md](CHANGELOG.md) file for any changes you make. It is sufficient to add a note under the `Unreleased` section. + +### General Pointers + +There are also some general pointers for people looking to contribute to any of our Taija packages [here](https://github.com/JuliaTrustworthyAI#general-pointers-for-contributors). + +Please follow the [SciML ColPrac guide](https://github.com/SciML/ColPrac). \ No newline at end of file diff --git a/dev/contribute/index.html b/dev/contribute/index.html index 9a43980a7..024ec198b 100644 --- a/dev/contribute/index.html +++ b/dev/contribute/index.html @@ -1,2 +1,2 @@ -๐Ÿ›  Contribute ยท CounterfactualExplanations.jl

๐Ÿ›  Contribute

Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on.

If any of the below applies to you, this might be the right open-source project for you:

  • Youโ€™re an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia.
  • Youโ€™re experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI.
  • Youโ€™re new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning.

@pat-alt here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open issue or start a discussion.

For more details on how to contribute see here. Please follow the SciML ColPrac guide.

+๐Ÿ›  Contribute ยท CounterfactualExplanations.jl

๐Ÿ›  Contribute

Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on. If you have an idea for a new feature or want to report a bug, please open a new issue.

Development

If your looking to contribute code, it may be helpful to check out the Explanation section of the docs.

Testing

Please always make sure to add tests for any new features or changes.

Documentation

If you add new features or change existing ones, please make sure to update the documentation accordingly. The documentation is written in Documenter.jl and is located in the docs/src folder.

Log Changes

As of version 1.1.1, we have tried to be more stringent about logging changes. Please make sure to add a note to the CHANGELOG.md file for any changes you make. It is sufficient to add a note under the Unreleased section.

General Pointers

There are also some general pointers for people looking to contribute to any of our Taija packages here.

Please follow the SciML ColPrac guide.

diff --git a/dev/contribute/performance/index.html b/dev/contribute/performance/index.html index f2e7c7657..1f1d49560 100644 --- a/dev/contribute/performance/index.html +++ b/dev/contribute/performance/index.html @@ -1,5 +1,5 @@ -- ยท CounterfactualExplanations.jl
Random.seed!(42)
+- ยท CounterfactualExplanations.jl
Random.seed!(42)
 # Counteractual data and model:
 data = TaijaData.load_linearly_separable()
 counterfactual_data = DataPreprocessing.CounterfactualData(data...)
@@ -12,4 +12,4 @@
 # Search:
 generator = GenericGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
data_large = TaijaData.load_linearly_separable(100000)
-counterfactual_data_large = DataPreprocessing.CounterfactualData(data_large...)
@time generate_counterfactual(x, target, counterfactual_data, M, generator)
@time generate_counterfactual(x, target, counterfactual_data_large, M, generator)
+counterfactual_data_large = DataPreprocessing.CounterfactualData(data_large...)
@time generate_counterfactual(x, target, counterfactual_data, M, generator)
@time generate_counterfactual(x, target, counterfactual_data_large, M, generator)
diff --git a/dev/explanation/architecture.qmd b/dev/explanation/architecture.qmd index 9ffe0a055..e5c5a6729 100644 --- a/dev/explanation/architecture.qmd +++ b/dev/explanation/architecture.qmd @@ -10,8 +10,62 @@ eval(setup_docs) ## Package Architecture -> Modular, composable, scalable! +The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) `Models` is concerned with making any arbitrary model compatible with the package; 2) `Generators` is used to implement arbitrary counterfactual search algorithms.^[We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.] -The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) `Models` is concerned with making any arbitrary model compatible with the package; 2) `Generators` is used to implement arbitrary counterfactual search algorithms.^[We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.] The core function of the package `generate_counterfactual` uses an instance of type `<: AbstractFittedModel` produced by the `Models` module and an instance of type `<: AbstractGenerator` produced by the `Generators` module. +The core function of the package, [`generate_counterfactual`](@ref), uses an instance of type [`AbstractFittedModel`](@ref) produced by the `Models` module and an instance of type [`AbstractGenerator`](@ref) produced by the `Generators` module. -![](../www/pkg_architecture.png) \ No newline at end of file +Metapackages from the [Taija](https://github.com/JuliaTrustworthyAI) ecosystem provide additional functionality such as datasets, language interoperability, parallelization, and plotting. The `CounterfactualExplanations` package is designed to be used in conjunction with these metapackages, but can also be used as a standalone package. + +```{mermaid} +%%| fig-width: 6.5 +%%| echo: false + +flowchart TB + + classDef taija fill:#b5c6ff,stroke:#333,color:#fff; + classDef module fill:#cb3c33,stroke:#333,color:#fff,stroke-width:4px; + classDef struct fill:#389826,stroke:#333,color:#fff; + classDef funct fill:#9558b2,stroke:#333,color:#fff; + + %% Taija + interop(["TaijaInteroperability.jl"]) + data(["TaijaData.jl"]) + parallel(["TaijaParallel.jl"]) + plotting(["TaijaPlotting.jl"]) + + %% Modules + data_pre(["DataPreprocessing"]) + models(["Models"]) + obj(["Objectives"]) + generators(["Generators"]) + eval(["Evaluation"]) + + %% Structs + c_data["CounterfactualData"] + model["<:AbstractFittedModel"] + generator["<:AbstractGenerator"] + ce["CounterfactualExplanation"] + + %% Functions + generate_counterfactual{{"generate_counterfactual"}} + evaluate{{"evaluate"}} + plot{{"plot"}} + + class interop,data,parallel,plotting taija; + class vae,c_data,model,generator,ce struct; + class data_pre,models,generators,eval,obj module; + class generate_counterfactual,evaluate,plot funct; + + %% Graph + data -. data .-> c_data + data_pre ===o c_data + interop -.-> model + models ===o model + generators & obj ===o generator + c_data & model & generator ===> generate_counterfactual + generate_counterfactual ===o ce + eval ===> evaluate + ce ===o evaluate & plot + parallel -.-> generate_counterfactual & evaluate + plotting -.-> plot +``` \ No newline at end of file diff --git a/dev/explanation/architecture/index.html b/dev/explanation/architecture/index.html index 9c90936fb..d2ce3602d 100644 --- a/dev/explanation/architecture/index.html +++ b/dev/explanation/architecture/index.html @@ -1,2 +1,2 @@ -Package Architecture ยท CounterfactualExplanations.jl

Package Architecture

Modular, composable, scalable!

The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) Models is concerned with making any arbitrary model compatible with the package; 2) Generators is used to implement arbitrary counterfactual search algorithms.[1] The core function of the package generate_counterfactual uses an instance of type <: AbstractFittedModel produced by the Models module and an instance of type <: AbstractGenerator produced by the Generators module.

[1] We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.

+Package Architecture ยท CounterfactualExplanations.jl

Package Architecture

The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) Models is concerned with making any arbitrary model compatible with the package; 2) Generators is used to implement arbitrary counterfactual search algorithms.[1]

The core function of the package, generate_counterfactual, uses an instance of type AbstractFittedModel produced by the Models module and an instance of type AbstractGenerator produced by the Generators module.

Metapackages from the Taija ecosystem provide additional functionality such as datasets, language interoperability, parallelization, and plotting. The CounterfactualExplanations package is designed to be used in conjunction with these metapackages, but can also be used as a standalone package.

[1] We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.

diff --git a/dev/explanation/architecture_files/figure-commonmark/mermaid-figure-1.png b/dev/explanation/architecture_files/figure-commonmark/mermaid-figure-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7dfefdfb9eb8b1eb543c94bd32e18e6779daf14a GIT binary patch literal 296623 zcmeEtRalf?^zMKtD5)q&gMvtRw}_PF&?zN3l0%mT2ojQlba#ie(wzfHi}b+IL&w?k z`#n{9dCS?=bPDkuf6uW-uGSm`=YKYPk4{=9s~j*R8)AW34stuLm+p#@$P_6 zBzueSArN|q;!7DV&*bfS4___q>znGWT|lxd~1@0i%X44=$)3cb46cYAJJuOLPAk>HUDG=HUwGzDx8vqiAk=Y zpg>7cQDlSp=~G$MObv8>C?zf?hIC6E3j$&9A0E~zq_XF6)x|!b=VM}G zCNkylYwV|XqzxA`<-+uWF=qq8+Rm@SHz@$GN>^B3yk!9L9plx{m)j(2Y(2u;gfSBLLadyxPuidvaqlq z7h79ZMK>AITT^OZzI*}Gfck^e11_#Yk`IiYNw>h+8{4;%i6+|)P2vE}R) zX5hX!HVYb}K!d;go|x!ip1XC2VC0)TEyY$8=1|Xqx&CqoKR<;_=bN6$XKmLWL|8$D z3xODViPO>qT<(i*X2-bw`@+o1!ND=ffCUNkcIb|vN#d~xdOPqv10-mtC^db4aWQ*} za~B1rs9U~4joqUyCkP@vKVN|^;IiWFP;vp-+zwG{2wY)zp;^GPha|;)##Tv5DHJXY zv5QJb$g>^KwVf>1pQ*9e1?wcS;4rL3L@~(ngGa5bt-HZj0cJj7l!lbc>FX!14J2k! z@V&LUzC6QBHPB$mrJmn&?~SobFOwYz8~6zb1pU&>tA1@Lr3gH^5pZ*n_P9mXc&ypS zgOGyvHT%2uREQ|LQdaA~HI8`tB*2|79H1hIrcq%8s zr10z4uiztoJ;^jRI^VtbCSI$LR@c`tp+Kt_>6ExlXzSYU{r&ylNx-!!2d6kU!PH+L zJd5=u0`ek0Iyy6qvq2%?d^{~SDTxy72m}%s1t#|^Iy3W0^VRXJ_u-(BqAxf^LZWUP z=^9zD{`4l8Um%5Y*f@bvB}Vm{;M-|BB?gbS z)FF_3zB@$J`C>aiWx>f0gFgp(7*T9krwRaO0^p7X%{}#^nQGfC9*eGx?-mhXz!aOS z;9EKp*Qd(>T}_H*u^@!B;(B#+E@8ZUe3<8V2LLRnNuFibI!r$RV<4aaJ4V0rSRk5x zPV|B?6&MFI7fRH0mHe>8NwVbW-&^}}SP<#c2c)F%dHB?38$MTT`x0g?5c;6durM^p znynO%1r2%<@S*eo0VTgemVBhnc&-X&#C2^gt&Is?(}|Vd7;Ox)Izj#h0z@}<9TcGC zG5?gTY7Jt5Yzn6mLS_Whj)9FO0TbgVkqsuA2p3w&KRP=4pNUa_!oh0tG(l_}tO)a^7dRSrmSVF6e&@Y3r>avEA6It#GhqDF@dVyIZ}?X)6d^2>b=8jI^GAu?VS!xxvVAB@K;l zT@f@zUWaS{rPy4)MwVMUp-|7N3+8Wdt}sFA8y=q87|jBe=l~M}>E?-C6{>JLDH9P9 zk^koVa&#y~48ES?fdmH?lPE^^y3p53cN+Pci2yu!Umbx)ISqx|m4GlGSxqAC*LakO- zDZT)wJxV_J0io=dAjXzT2!!mE3B{B1f`U~r2OBzLpTx>f_-Mz6lMEBo0lwjUOoarM z2=a0Q1BMqnjcQ`;^TC=XLpu`iPD?Qb8m5`Je*$Z%1#idK3J zHM|vcpkk{`AVmq1Zc>ii_+cX?P`D0MSWK1});fIYMf3TO*Je{jPw~h7&9^g+2FMP> zCsDiP)w0{MKU9HQ@oL#5%{Wlb*eNwy95Mg+@uNtd*6SsWs9T5%M4E=%>%bDkQu5i9 zyJ8>jYb!AV0lsWTS={qz+HtMS_H?Pch&%1-1uStW`~LpdP^Lm;og<;b8P7T2YgN@r z45C2QBm@KjKtmt^cuq^mO+00|nvXO~jQXEKVbno0qS@|21rEWnC~mve83^RkM0%_% zu$q^i)E!XngX#SBZs8IYr{H6Qva`ST!x6cemsT2QU(ZzItWY0RaI7jj z9TdBqAE%r}J4?%5Gr?aiNWXeJ8;%9dDNL#OVb&hBxzo4{zJUer%}etyY}{!mnk=2_ zikcD05ezC=BPtMBu9I$t)^kyqL#59J=u7S@P}$kOW@liKiIlqj86kCjI#W-t9{d$V z)^%;5AkLmixeu-YJGrtH%YMdxCY@3t@WxJAx{r?#jElYy6w4>nHFEx33Pv)hCj;x> z^8pcvVw5=j`w@Vzw^rG=cM8nHIVP$FO!zzyh%@_}q!}AZgZoiBIN1}|g~rZVo!kWT zpqx>PxL7_c1Ndau&o?ZdCA8rAv~Wrx4zb8Yvl(kJ-u;SW`au-gxr!f3sG6fMs}Dp9 zL8>Nxe?ANX?-AQ=%95w^;|Jg1&u$@t%8TV3tLjK%6Hc?X9(`Oy++2p9Kj{N#Zfc$E z{7S0~3-QRx%Bo>C-jF}w`ZC`nubv@gBf3E(Cl{=-xr&AU>R&?3v2tVPoCh6X9Xs{w zsT(FZV0m~SaPZRK7v<-VfYXQ`6k2?l*A<~IHphXaa~@n=onI{8(AK1grbIW0y-xYd>-MQVwmm%|{EvPW*ZqI; z0Y~l$+Y?0zsa|UoIk~y7a{SXvA4~Z+JyT1iPU%yD8Yc&xi>T17@~(sqX+sbik8%sr zu!CBPi$!?^4BpkRl7&+Wu-c(-!GG_}H^Kn}uwU(q(*bFn$#=GuGZn>?WIByr~;j|1O;(@$Cc@1Oa+Q#+wSwaqsaoLQOjZ?vj= zqLP`+9Y{KD@8OU0ImZp=0B!S6^XKTTylz^bukXzv5ajpDO5S<*S=c#%Jy@BThPEQ{ z$K+f}kqwKdEwM?tEeBNR1I&+Jnq`X!3F(qPd#(QVZSGavEoV*=yp08)-HBrTDu=RJ zj)F3%=%7_03k*My#Nn<&m0zy^p5pcMfeRz2-?uU7Z^@na>5dKH(GBOwqPVm)N1yq` zApRa(B=I|0+F5&Cb=`-Fxoc+xgNKQ%p)a1ita~@l?|@lai_f|IQ_NRW%#UV`Jb>sCFl)oP6N&}Ne~PJz39XlM z$_bn`H4CqJ;f02xqn$4vAqW@Ga&EMge!ys*hx+=)0OQM#=P;Zx+rncS)pgY3b=V}4U`4%Molk)V83z-5UN&WK2mt?=KcI{afX37~ExnjU z9w~^9TOl7NtXMj;BGYfqvOU;xUQlff3=K^dm9_M}xW|u|Yl{Rm4zZpV@Xj!WNRhd* zu**rY@Yb7P&2a>OS_T)JN}}LG6%mc779c{AFf`UaJCJWcKvcNzF2D>^!U;FA>)f)( zYgw@nvbot@zGH4_T6W8Y700Mq`>||AB~{hhGT?7O=_n=YU!)&JbK-iUJ+2@dQrj|x<1(YLDQY>jFb zDL%$P=<%a3Ju+Ohv?DfGH|f01anZUSu!tmK=O_3q>broXsO~nOq{7gvqS?yxnnYq# zkdP0ix5ifPL4T-R%v^%i8%&w_lwj7ApPnwAVB%dM2%E7~_5&dhQ%ONB;mANT*!Xh_ z9_9Mu{h5k5*_k)Ixjt!Kb#H_fk%Hx%O055>u;}g*#e>uUkqQ`>aV^IHz^x*QF^YGx zcefSpLTvu>(r5-GRCmV;&hRfaIxoM%Fq)5d2%}{T@gdT46aWr@LNZe}Hc?FQDRf^` z&7PN+_$!!At14@pJoccqz=i6;XAjg&j;a5d94G~%if#FIAXBoz+y~rpmm;v zG`qYVzlkAjO9I(hMnNt#V^3OH(g^)dE-!sc3~H3}QoUJS4Z%u7C@~ucaD{dh?2Ci4 zF{oj);+lrcG&*2sT&aS*#RzNM75Xo8SsflfH)(V)0WcI6tYjDcuFlajwXh#|2p+$Y_XI2Z#el)b5TMR6obUdW#rNFn z4MDfh@=7k;@H-G`?m=ozsRYCy?rL`Rnp-A}WLzc#r5+GS$2W|44!D?Id{WlKFVJJP zL0OjZu;EXB_gs)$F%&SXM_4Q*OD@|$z!zm^zUZd$NQUuXSJZ+-Iq!dgszr3ZP=HiK zV`9)(+tCe3B-6Muy^VO`CEe=8L#nqS^p;*& zo2IL|p-+rfR`)&m0bc@MNpI~R0Hmjajtj;3oIj;OLA^h*9OYI&i{L5NlDSni+=-uc{>5Tn=o+@W(ta8r@N$KTE5ObBCP9Yat=cERV*-16I&0D=RiYvtrKu zQ+E1Xj1ULH=cSQaW%g{7CVn&x*2~@}tw%3DvzUHzHB`-mf>mn*P!0$>| zPcI)c7vMQW=F>*Lo?rPUDx4^O3qo%srn0&3Qb6A(=J?DP3lhQsBqo4AJphxT`LuP} zt)4kg^@=~-v4cu`&jc|p#l}8tMo${rEi~1`DzIueKyB({D?9mbNO*~QcSBU4IQ9if@f#?YEWQ#RvdVf;c1Y6<;0BPnC zAXj+ffEkfGAAHmugVDyU;PTsWP>&hk4dVUU!}}>LG}P?^RVVL@WFb)owB5I(msnMC zD*6&aPn5DF=$0tY`(!S1xAo?#%p~9nKKl>b&JdjKcVLwvF?5xLwgkPz5IaTOyo^@w zoQHCg5kLtUTXcQBe{l#%*)P>0fLRgYa1u!7a-6QoXtnM5lftYDhH%xWB(cYeAi@t6 zgWR&A=e=BVyn{8s-eY27Dgf4l;47ez><$1`+5qNKWPZfML`swZB8l)0go$FTLRB{G zQd0v5y**Q-3`7COzzQG5s#$K_Z5eMTmZH|6p-pTUwa*s=v-_u|9E|8P+pumekjBZA z8cdTi?WT79T3J;E3n$Je1U}3n@VGvNAba2nbzV7I4KWZqjU)!NGsU?`TYU*I%=(v{ z^3~EL|C>`_&;Yk$1ptnTB3=8Ho^SVXu;aHBz7@23n_;Y3OQdW@1W9rYP^pJXZ`zwD_?TKXRs5e>`rwrLGx{H-U~0qyb2Y%-9^yvp+jn zjqh({oL}LYNd8O#)S#Swa0XSgb{Ip6!B3|Q;KkV58v(~i-8A5;rsW*T*1t+Pju0>@ z;K0{W4a4Tto-W_O_Fjw?Nw2*)xrWcCnY1+R(&juwO zXyd9Tz8r?lS7mNEn;KxE^VqEgcUPlrow3;@B#glq2O^&sq%j0eqYZ%D}rQHK=v4^>GL^zVvT>6+vAfU_Yr>YVo}v z@kR*n+lH;?d(gD=l7L9`_H;~#d`C0L$t_^9RT@cZ8b^u{dx!Nmhk6iOV4pJ8{cv$9@HSu zP2Sm?AL5Z~Yq$NU;+s~wln)^|A{{v(E1K|B-B?*zMu8SHvwsa{W85! zBiR$A+RTtz&xV29rETp=0C(4;d!8x?OY*-$>l#`BXwmfdZ!z&dv+7?J6UnaHw76xq zg^E2FD_Nu)1_vCI3ne^Q?!eq0#ph~zQ|}vOhUDb}K|5c%7a2?TTX)8FSR${lPf1J` z+kwx1k{wvwb1rA&^P1K=E|3t5k8|Ic_=b`M*fKku!0ZCw@u^2Po&F1p@6T0gVEsUI z3_cSG0^}=b^{(kS|5Jlx5Ty@3n>h*WT=_~-ZaLk@QGs`sU)05t{^ovk(jB4;;#1@6 zT?QS4)5`L5L6BkK1a&>7$gSz8Q#FV2{tk>9>!}_+0c3xhpbjxtKDoBOzP=Ts zCRrizjQ+v5foUQS`Pv_%5JYXQs%+jXZlbO^jGa*Y1PePKSJ2^4`gE1GA?puw1?xr_ zG1wtJ?g8ToLBst~c{*FrGH?&Zfk=OZ8W|M>Oc>Ujewf%0D=GNqhYTmn?7yphAR6*- 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

`pS>C)BLw&8^kbK| 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%0DFSPO-8l=Ey)NCUn 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 z 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 z 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%0A)RM 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 -Categorical Features ยท CounterfactualExplanations.jl

Categorical Features

To illustrate how data is preprocessed under the hood, we consider a simple toy dataset with three categorical features (name, grade and sex) and one continuous feature (age):

X = (
+Categorical Features ยท CounterfactualExplanations.jl

Categorical Features

To illustrate how data is preprocessed under the hood, we consider a simple toy dataset with three categorical features (name, grade and sex) and one continuous feature (age):

X = (
     name=categorical(["Danesh", "Lee", "Mary", "John"]),
     grade=categorical(["A", "B", "A", "C"], ordered=true),
     sex=categorical(["male","female","male","male"]),
@@ -117,4 +117,4 @@
  0.0
  0.0
  1.0
- 1.85
+ 1.85
diff --git a/dev/explanation/generators/clap_roar/index.html b/dev/explanation/generators/clap_roar/index.html index cffdd9597..f2f31acad 100644 --- a/dev/explanation/generators/clap_roar/index.html +++ b/dev/explanation/generators/clap_roar/index.html @@ -1,6 +1,6 @@ -ClaPROAR ยท CounterfactualExplanations.jl

ClaPROARGenerator

The ClaPROARGenerator was introduced in Altmeyer et al. (2023).

Description

The acronym Clap stands for classifier-preserving. The approach is loosely inspired by ROAR (Upadhyay, Joshi, and Lakkaraju 2021). Altmeyer et al. (2023) propose to explicitly penalize the loss incurred by the classifer when evaluated on the counterfactual $x^\prime$ at given parameter values. Formally, we have

\[\begin{aligned} +ClaPROAR ยท CounterfactualExplanations.jl

ClaPROARGenerator

The ClaPROARGenerator was introduced in Altmeyer et al. (2023).

Description

The acronym Clap stands for classifier-preserving. The approach is loosely inspired by ROAR (Upadhyay, Joshi, and Lakkaraju 2021). Altmeyer et al. (2023) propose to explicitly penalize the loss incurred by the classifer when evaluated on the counterfactual $x^\prime$ at given parameter values. Formally, we have

\[\begin{aligned} \text{extcost}(f(\mathbf{s}^\prime)) = l(M(f(\mathbf{s}^\prime)),y^\prime) \end{aligned}\]

for each counterfactual $k$ where $l$ denotes the loss function used to train $M$. This approach is based on the intuition that (endogenous) model shifts will be triggered by counterfactuals that increase classifier loss (Altmeyer et al. 2023).

Usage

The approach can be used in our package as follows:

generator = ClaPROARGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

Comparison to GenericGenerator

The figure below compares the outcome for the GenericGenerator and the ClaPROARGenerator.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

Upadhyay, Sohini, Shalmali Joshi, and Himabindu Lakkaraju. 2021. โ€œTowards Robust and Reliable Algorithmic Recourse.โ€ https://arxiv.org/abs/2102.13620.

+plot(ce)

Comparison to GenericGenerator

The figure below compares the outcome for the GenericGenerator and the ClaPROARGenerator.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

Upadhyay, Sohini, Shalmali Joshi, and Himabindu Lakkaraju. 2021. โ€œTowards Robust and Reliable Algorithmic Recourse.โ€ https://arxiv.org/abs/2102.13620.

diff --git a/dev/explanation/generators/clue/index.html b/dev/explanation/generators/clue/index.html index b3452835d..bd7b68d1b 100644 --- a/dev/explanation/generators/clue/index.html +++ b/dev/explanation/generators/clue/index.html @@ -1,5 +1,5 @@ -CLUE ยท CounterfactualExplanations.jl

CLUEGenerator

In this tutorial, we introduce the CLUEGenerator, a counterfactual generator based on the Counterfactual Latent Uncertainty Explanations (CLUE) method proposed by Antorรกn et al. (2020).

Description

The CLUEGenerator leverages differentiable probabilistic models, such as Bayesian Neural Networks (BNNs), to estimate uncertainty in predictions. It aims to provide interpretable counterfactual explanations by identifying input patterns that lead to predictive uncertainty. The generator utilizes a latent variable framework and employs a decoder from a variational autoencoder (VAE) to generate counterfactual samples in latent space.

The CLUE algorithm minimizes a loss function that combines uncertainty estimates and the distance between the generated counterfactual and the original input. By optimizing this loss function iteratively, the CLUEGenerator generates counterfactuals that are similar to the original observation but assigned low uncertainty.

The formula for predictive entropy is as follow:

\[\begin{aligned} +CLUE ยท CounterfactualExplanations.jl

CLUEGenerator

In this tutorial, we introduce the CLUEGenerator, a counterfactual generator based on the Counterfactual Latent Uncertainty Explanations (CLUE) method proposed by Antorรกn et al. (2020).

Description

The CLUEGenerator leverages differentiable probabilistic models, such as Bayesian Neural Networks (BNNs), to estimate uncertainty in predictions. It aims to provide interpretable counterfactual explanations by identifying input patterns that lead to predictive uncertainty. The generator utilizes a latent variable framework and employs a decoder from a variational autoencoder (VAE) to generate counterfactual samples in latent space.

The CLUE algorithm minimizes a loss function that combines uncertainty estimates and the distance between the generated counterfactual and the original input. By optimizing this loss function iteratively, the CLUEGenerator generates counterfactuals that are similar to the original observation but assigned low uncertainty.

The formula for predictive entropy is as follow:

\[\begin{aligned} H(y^*|x^*, D) &= - \sum_{k=1}^{K} p(y^*=c_k|x^*, D) \log p(y^*=c_k|x^*, D) \end{aligned}\]

Usage

While using one must keep in mind that the CLUE algorithim is meant to find a more robust datapoint of the same class, using CLUE generator without any additional penalties/losses will mean that it is not a counterfactual generator. The generated result will be of the same class as the original input, but a more robust datapoint.

CLUE works best for BNNโ€™s. The CLUEGenerator can be used with any differentiable probabilistic model, but the results may not be as good as with BNNs.

The CLUEGenerator can be used in the following manner:

generator = CLUEGenerator()
 M = fit_model(counterfactual_data, :DeepEnsemble)
@@ -7,4 +7,4 @@
 ce = generate_counterfactual(
     x, target, counterfactual_data, M, generator;
     convergence=conv)
-plot(ce)

Extra: The CLUE generator can also be used upon already having achieved a counterfactual with a different generator. In this case, you can use CLUE and make the counterfactual more robust.

Note: The above documentation is based on the information provided in the CLUE paper. Please refer to the original paper for more detailed explanations and implementation specifics.

References

Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.

+plot(ce)

Extra: The CLUE generator can also be used upon already having achieved a counterfactual with a different generator. In this case, you can use CLUE and make the counterfactual more robust.

Note: The above documentation is based on the information provided in the CLUE paper. Please refer to the original paper for more detailed explanations and implementation specifics.

References

Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.

diff --git a/dev/explanation/generators/dice/index.html b/dev/explanation/generators/dice/index.html index dc52e678a..fe95ab726 100644 --- a/dev/explanation/generators/dice/index.html +++ b/dev/explanation/generators/dice/index.html @@ -1,5 +1,5 @@ -DiCE ยท CounterfactualExplanations.jl

DiCEGenerator

The DiCEGenerator can be used to generate multiple diverse counterfactuals for a single factual.

Description

Counterfactual Explanations are not unique and there are therefore many different ways through which valid counterfactuals can be generated. In the context of Algorithmic Recourse this can be leveraged to offer individuals not one, but possibly many different ways to change a negative outcome into a positive one. One might argue that it makes sense for those different options to be as diverse as possible. This idea is at the core of DiCE, a counterfactual generator introduce by Mothilal, Sharma, and Tan (2020) that generate a diverse set of counterfactual explanations.

Defining Diversity

To ensure that the generated counterfactuals are diverse, Mothilal, Sharma, and Tan (2020) add a diversity constraint to the counterfactual search objective. In particular, diversity is explicitly proxied via Determinantal Point Processes (DDP).

We can implement DDP in Julia as follows:[1]

using LinearAlgebra
+DiCE ยท CounterfactualExplanations.jl

DiCEGenerator

The DiCEGenerator can be used to generate multiple diverse counterfactuals for a single factual.

Description

Counterfactual Explanations are not unique and there are therefore many different ways through which valid counterfactuals can be generated. In the context of Algorithmic Recourse this can be leveraged to offer individuals not one, but possibly many different ways to change a negative outcome into a positive one. One might argue that it makes sense for those different options to be as diverse as possible. This idea is at the core of DiCE, a counterfactual generator introduce by Mothilal, Sharma, and Tan (2020) that generate a diverse set of counterfactual explanations.

Defining Diversity

To ensure that the generated counterfactuals are diverse, Mothilal, Sharma, and Tan (2020) add a diversity constraint to the counterfactual search objective. In particular, diversity is explicitly proxied via Determinantal Point Processes (DDP).

We can implement DDP in Julia as follows:[1]

using LinearAlgebra
 function ddp_diversity(X::AbstractArray{<:Real, 3})
     xs = eachslice(X, dims = ndims(X))
     K = [1/(1 + norm(x .- y)) for x in xs, y in xs]
@@ -40,4 +40,4 @@
             num_counterfactuals=n_cf, convergence=conv
       )
     )
-end

The figure below shows the resulting counterfactual paths. As expected, the resulting counterfactuals are more dispersed across the feature domain for higher choices of $\lambda_2$

References

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

[1] With thanks to the respondents on Discourse

+end

The figure below shows the resulting counterfactual paths. As expected, the resulting counterfactuals are more dispersed across the feature domain for higher choices of $\lambda_2$

References

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

[1] With thanks to the respondents on Discourse

diff --git a/dev/explanation/generators/feature_tweak/index.html b/dev/explanation/generators/feature_tweak/index.html index d5c1c9ed8..736468b85 100644 --- a/dev/explanation/generators/feature_tweak/index.html +++ b/dev/explanation/generators/feature_tweak/index.html @@ -1,5 +1,5 @@ -FeatureTweak ยท CounterfactualExplanations.jl

FeatureTweakGenerator

Feature Tweak refers to the generator introduced by Tolomei et al. (2017). Our implementation takes inspiration from the featureTweakPy library.

Description

Feature Tweak is a powerful recourse algorithm for ensembles of tree-based classifiers such as random forests. Though the problem of understanding how an input to an ensemble model could be transformed in such a way that the model changes its original prediction has been proven to be NP-hard (Tolomei et al. 2017), Feature Tweak provides an algorithm that manages to tractably solve this problem in multiple real-world applications. An example of a problem Feature Tweak is able to efficiently solve, explored in depth in Tolomei et al. (2017) is the problem of transforming an advertisement that has been classified by the ensemble model as a low-quality advertisement to a high-quality one through small changes to its features. With the help of Feature Tweak, advertisers can both learn about the reasons a particular ad was marked to have a low quality, as well as receive actionable suggestions about how to convert a low-quality ad into a high-quality one.

Though Feature Tweak is a powerful way of avoiding brute-force search in an exponential search space, it does not come without disadvantages. The primary limitations of the approach are that itโ€™s currently only applicable to tree-based classifiers and works only in the setting of binary classification. Another problem is that though the algorithm avoids exponential-time search, it is often still computationally expensive. The algorithm may be improved in the future to tackle all of these shortcomings.

The following equation displays how a true negative instance x can be transformed into a positively predicted instance xโ€™. To be more precise, xโ€™ is the best possible transformation among all transformations **x***, computed with a cost function ฮด.

\[\begin{aligned} +FeatureTweak ยท CounterfactualExplanations.jl

FeatureTweakGenerator

Feature Tweak refers to the generator introduced by Tolomei et al. (2017). Our implementation takes inspiration from the featureTweakPy library.

Description

Feature Tweak is a powerful recourse algorithm for ensembles of tree-based classifiers such as random forests. Though the problem of understanding how an input to an ensemble model could be transformed in such a way that the model changes its original prediction has been proven to be NP-hard (Tolomei et al. 2017), Feature Tweak provides an algorithm that manages to tractably solve this problem in multiple real-world applications. An example of a problem Feature Tweak is able to efficiently solve, explored in depth in Tolomei et al. (2017) is the problem of transforming an advertisement that has been classified by the ensemble model as a low-quality advertisement to a high-quality one through small changes to its features. With the help of Feature Tweak, advertisers can both learn about the reasons a particular ad was marked to have a low quality, as well as receive actionable suggestions about how to convert a low-quality ad into a high-quality one.

Though Feature Tweak is a powerful way of avoiding brute-force search in an exponential search space, it does not come without disadvantages. The primary limitations of the approach are that itโ€™s currently only applicable to tree-based classifiers and works only in the setting of binary classification. Another problem is that though the algorithm avoids exponential-time search, it is often still computationally expensive. The algorithm may be improved in the future to tackle all of these shortcomings.

The following equation displays how a true negative instance x can be transformed into a positively predicted instance xโ€™. To be more precise, xโ€™ is the best possible transformation among all transformations **x***, computed with a cost function ฮด.

\[\begin{aligned} \mathbf{x}^\prime = \arg_{\mathbf{x^*}} \min \{ {\delta(\mathbf{x}, \mathbf{x^*}) | \hat{f}(\mathbf{x}) = -1 \wedge \hat{f}(\mathbf{x^*}) = +1} \} \end{aligned}\]

Example

In this example we apply the Feature Tweak algorithm to a decision tree and a random forest trained on the moons dataset. We first load the data and fit the models:

n = 500
 counterfactual_data = CounterfactualData(TaijaData.load_moons(n)...)
@@ -31,4 +31,4 @@
     colorbar=false,
 )
 
-display(plot(p1, p2; size=(800, 400)))

References

Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.

+display(plot(p1, p2; size=(800, 400)))

References

Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.

diff --git a/dev/explanation/generators/generic/index.html b/dev/explanation/generators/generic/index.html index 7cab6a55a..75f71b860 100644 --- a/dev/explanation/generators/generic/index.html +++ b/dev/explanation/generators/generic/index.html @@ -1,4 +1,4 @@ -Generic ยท CounterfactualExplanations.jl

GenericGenerator

We use the term generic to relate to the basic counterfactual generator proposed by Wachter, Mittelstadt, and Russell (2017) with $L1$-norm regularization. There is also a variant of this generator that uses the distance metric proposed in Wachter, Mittelstadt, and Russell (2017), which we call WachterGenerator.

Description

As the term indicates, this approach is simple: it forms the baseline approach for gradient-based counterfactual generators. Wachter, Mittelstadt, and Russell (2017) were among the first to realise that

[โ€ฆ] explanations can, in principle, be offered without opening the โ€œblack box.โ€

โ€” Wachter, Mittelstadt, and Russell (2017)

Gradient descent is performed directly in the feature space. Concerning the cost heuristic, the authors choose to penalize the distance of counterfactuals from the factual value. This is based on the intuitive notion that larger feature perturbations require greater effort.

Usage

The approach can be used in our package as follows:

generator = GenericGenerator()
+Generic ยท CounterfactualExplanations.jl

GenericGenerator

We use the term generic to relate to the basic counterfactual generator proposed by Wachter, Mittelstadt, and Russell (2017) with $L1$-norm regularization. There is also a variant of this generator that uses the distance metric proposed in Wachter, Mittelstadt, and Russell (2017), which we call WachterGenerator.

Description

As the term indicates, this approach is simple: it forms the baseline approach for gradient-based counterfactual generators. Wachter, Mittelstadt, and Russell (2017) were among the first to realise that

[โ€ฆ] explanations can, in principle, be offered without opening the โ€œblack box.โ€

โ€” Wachter, Mittelstadt, and Russell (2017)

Gradient descent is performed directly in the feature space. Concerning the cost heuristic, the authors choose to penalize the distance of counterfactuals from the factual value. This is based on the intuitive notion that larger feature perturbations require greater effort.

Usage

The approach can be used in our package as follows:

generator = GenericGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

References

Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.

+plot(ce)

References

Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.

diff --git a/dev/explanation/generators/gravitational/index.html b/dev/explanation/generators/gravitational/index.html index bff57c371..8399d5e68 100644 --- a/dev/explanation/generators/gravitational/index.html +++ b/dev/explanation/generators/gravitational/index.html @@ -1,8 +1,8 @@ -Gravitational ยท CounterfactualExplanations.jl

GravitationalGenerator

The GravitationalGenerator was introduced in Altmeyer et al. (2023). It is named so because it generates counterfactuals that gravitate towards some sensible point in the target domain.

Description

Altmeyer et al. (2023) extend the general framework as follows,

\[\begin{aligned} +Gravitational ยท CounterfactualExplanations.jl

GravitationalGenerator

The GravitationalGenerator was introduced in Altmeyer et al. (2023). It is named so because it generates counterfactuals that gravitate towards some sensible point in the target domain.

Description

Altmeyer et al. (2023) extend the general framework as follows,

\[\begin{aligned} \mathbf{s}^\prime &= \arg \min_{\mathbf{s}^\prime \in \mathcal{S}} \{ {\text{yloss}(M(f(\mathbf{s}^\prime)),y^*)} \\ &+ \lambda_1 {\text{cost}(f(\mathbf{s}^\prime))} + \lambda_2 {\text{extcost}(f(\mathbf{s}^\prime))} \} \end{aligned} \]

where $\text{cost}(f(\mathbf{s}^\prime))$ denotes the proxy for costs faced by the individual. โ€œThe newly introduced term $\text{extcost}(f(\mathbf{s}^\prime))$ is meant to capture and address external costs incurred by the collective of individuals in response to changes in $\mathbf{s}^\prime$.โ€ (Altmeyer et al. 2023)

For the GravitationalGenerator we have,

\[\begin{aligned} \text{extcost}(f(\mathbf{s}^\prime)) = \text{dist}(f(\mathbf{s}^\prime),\bar{x}^*) \end{aligned}\]

where $\bar{x}$ is some sensible point in the target domain, for example, the subsample average $\bar{x}^*=\text{mean}(x)$, $x \in \mathcal{D}_1$.

There is a tradeoff then, between the distance of counterfactuals from their factual value and the chosen point in the target domain. The chart below illustrates how the counterfactual outcome changes as the penalty $\lambda_2$ on the distance to the point in the target domain is increased from left to right (holding the other penalty term constant).

Usage

The approach can be used in our package as follows:

generator = GravitationalGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-display(plot(ce))

Comparison to GenericGenerator

The figure below compares the outcome for the GenericGenerator and the GravitationalGenerator.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

+display(plot(ce))

Comparison to GenericGenerator

The figure below compares the outcome for the GenericGenerator and the GravitationalGenerator.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

diff --git a/dev/explanation/generators/greedy/index.html b/dev/explanation/generators/greedy/index.html index f75e5705f..277b5ac1d 100644 --- a/dev/explanation/generators/greedy/index.html +++ b/dev/explanation/generators/greedy/index.html @@ -1,5 +1,5 @@ -Greedy ยท CounterfactualExplanations.jl

GreedyGenerator

We use the term greedy to describe the counterfactual generator introduced by Schut et al. (2021).

Description

The Greedy generator works under the premise of generating realistic counterfactuals by minimizing predictive uncertainty. Schut et al. (2021) show that for models that incorporates predictive uncertainty in their predictions, maximizing the predictive probability corresponds to minimizing the predictive uncertainty: by construction, the generated counterfactual will therefore be realistic (low epistemic uncertainty) and unambiguous (low aleatoric uncertainty).

For the counterfactual search Schut et al. (2021) propose using a Jacobian-based Saliency Map Attack(JSMA). It is greedy in the sense that it is an โ€œiterative algorithm that updates the most salient feature, i.e.ย the feature that has the largest influence on the classification, by $\delta$ at each stepโ€ (Schut et al. 2021).

Usage

The approach can be used in our package as follows:

M = fit_model(counterfactual_data, :DeepEnsemble)
+Greedy ยท CounterfactualExplanations.jl

GreedyGenerator

We use the term greedy to describe the counterfactual generator introduced by Schut et al. (2021).

Description

The Greedy generator works under the premise of generating realistic counterfactuals by minimizing predictive uncertainty. Schut et al. (2021) show that for models that incorporates predictive uncertainty in their predictions, maximizing the predictive probability corresponds to minimizing the predictive uncertainty: by construction, the generated counterfactual will therefore be realistic (low epistemic uncertainty) and unambiguous (low aleatoric uncertainty).

For the counterfactual search Schut et al. (2021) propose using a Jacobian-based Saliency Map Attack(JSMA). It is greedy in the sense that it is an โ€œiterative algorithm that updates the most salient feature, i.e.ย the feature that has the largest influence on the classification, by $\delta$ at each stepโ€ (Schut et al. 2021).

Usage

The approach can be used in our package as follows:

M = fit_model(counterfactual_data, :DeepEnsemble)
 generator = GreedyGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

References

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

+plot(ce)

References

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

diff --git a/dev/explanation/generators/growing_spheres/index.html b/dev/explanation/generators/growing_spheres/index.html index 84b2a7935..acc95a797 100644 --- a/dev/explanation/generators/growing_spheres/index.html +++ b/dev/explanation/generators/growing_spheres/index.html @@ -1,6 +1,6 @@ -GrowingSpheres ยท CounterfactualExplanations.jl

GrowingSpheres

Growing Spheres refers to the generator introduced by Laugel et al. (2017). Our implementation takes inspiration from the CARLA library.

Principle of the Proposed Approach

In order to interpret a prediction through comparison, the Growing Spheres algorithm focuses on finding an observation belonging to the other class and answers the question: โ€œConsidering an observation and a classifier, what is the minimal change we need to apply in order to change the prediction of this observation?โ€. This problem is similar to inverse classification but applied to interpretability.

Explaining how to change a prediction can help the user understand what the model considers as locally important. The Growing Spheres approach provides insights into the classifierโ€™s behavior without claiming any causal knowledge. It differs from other interpretability approaches and is not concerned with the global behavior of the model. Instead, it aims to provide local insights into the classifierโ€™s decision-making process.

The algorithm finds the closest โ€œennemyโ€ observation, which is an observation classified into the other class than the input observation. The final explanation is the difference vector between the input observation and the ennemy.

Finding the Closest Ennemy

The algorithm solves the following minimization problem to find the closest ennemy:

\[e^* = \arg \min_{e \in X} \{ c(x, e) \,|\, f(e) \neq f(x) \}\]

The cost function c(x, e) is defined as:

\[c(x, e) = ||x - e||_2 + \gamma ||x - e||_0\]

where ||.||_2 is the Euclidean norm and ||.||_0 is the sparsity measure. The weight gamma balances the importance of sparsity in the cost function.

To approximate the solution, the Growing Spheres algorithm uses a two-step heuristic approach. The first step is the Generation phase, where observations are generated in spherical layers around the input observation. The second step is the Feature Selection phase, where the generated observation with the smallest change in each feature is selected.

Example

generator = GrowingSpheresGenerator()
+GrowingSpheres ยท CounterfactualExplanations.jl

GrowingSpheres

Growing Spheres refers to the generator introduced by Laugel et al. (2017). Our implementation takes inspiration from the CARLA library.

Principle of the Proposed Approach

In order to interpret a prediction through comparison, the Growing Spheres algorithm focuses on finding an observation belonging to the other class and answers the question: โ€œConsidering an observation and a classifier, what is the minimal change we need to apply in order to change the prediction of this observation?โ€. This problem is similar to inverse classification but applied to interpretability.

Explaining how to change a prediction can help the user understand what the model considers as locally important. The Growing Spheres approach provides insights into the classifierโ€™s behavior without claiming any causal knowledge. It differs from other interpretability approaches and is not concerned with the global behavior of the model. Instead, it aims to provide local insights into the classifierโ€™s decision-making process.

The algorithm finds the closest โ€œennemyโ€ observation, which is an observation classified into the other class than the input observation. The final explanation is the difference vector between the input observation and the ennemy.

Finding the Closest Ennemy

The algorithm solves the following minimization problem to find the closest ennemy:

\[e^* = \arg \min_{e \in X} \{ c(x, e) \,|\, f(e) \neq f(x) \}\]

The cost function c(x, e) is defined as:

\[c(x, e) = ||x - e||_2 + \gamma ||x - e||_0\]

where ||.||_2 is the Euclidean norm and ||.||_0 is the sparsity measure. The weight gamma balances the importance of sparsity in the cost function.

To approximate the solution, the Growing Spheres algorithm uses a two-step heuristic approach. The first step is the Generation phase, where observations are generated in spherical layers around the input observation. The second step is the Feature Selection phase, where the generated observation with the smallest change in each feature is selected.

Example

generator = GrowingSpheresGenerator()
 M = fit_model(counterfactual_data, :DeepEnsemble)
 ce = generate_counterfactual(
     x, target, counterfactual_data, M, generator)
-plot(ce)

References

Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ arXiv. https://doi.org/10.48550/arXiv.1712.08443.

+plot(ce)

References

Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ arXiv. https://doi.org/10.48550/arXiv.1712.08443.

diff --git a/dev/explanation/generators/overview/index.html b/dev/explanation/generators/overview/index.html index a951343b0..3920ba350 100644 --- a/dev/explanation/generators/overview/index.html +++ b/dev/explanation/generators/overview/index.html @@ -1,5 +1,5 @@ -Overview ยท CounterfactualExplanations.jl

Counterfactual Generators

Counterfactual generators form the very core of this package. The generator_catalogue can be used to inspect the available generators:

generator_catalogue
Dict{Symbol, Any} with 11 entries:
+Overview ยท CounterfactualExplanations.jl

Counterfactual Generators

Counterfactual generators form the very core of this package. The generator_catalogue can be used to inspect the available generators:

generator_catalogue
Dict{Symbol, Any} with 11 entries:
   :gravitational   => GravitationalGenerator
   :growing_spheres => GrowingSpheresGenerator
   :revise          => REVISEGenerator
@@ -12,4 +12,4 @@
   :generic         => GenericGenerator
   :greedy          => GreedyGenerator

The following sections provide brief descriptions of all of them.

Gradient-based Counterfactual Generators

At the time of writing, all generators are gradient-based: that is, counterfactuals are searched through gradient descent. In Altmeyer et al. (2023) we lay out a general methodological framework that can be applied to all of these generators:

\[\begin{aligned} \mathbf{s}^\prime &= \arg \min_{\mathbf{s}^\prime \in \mathcal{S}} \left\{ {\text{yloss}(M(f(\mathbf{s}^\prime)),y^*)}+ \lambda {\text{cost}(f(\mathbf{s}^\prime)) } \right\} -\end{aligned} \]

โ€œHere $\mathbf{s}^\prime=\left\{s_k^\prime\right\}_K$ is a $K$-dimensional array of counterfactual states and $f: \mathcal{S} \mapsto \mathcal{X}$ maps from the counterfactual state space to the feature space.โ€ (Altmeyer et al. 2023)

For most generators, the state space is the feature space ($f$ is the identity function) and the number of counterfactuals $K$ is one. Latent Space generators instead search counterfactuals in some latent space $\mathcal{S}$. In this case, $f$ corresponds to the decoder part of the generative model, that is the function that maps back from the latent space to inputs.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

+\end{aligned} \]

โ€œHere $\mathbf{s}^\prime=\left\{s_k^\prime\right\}_K$ is a $K$-dimensional array of counterfactual states and $f: \mathcal{S} \mapsto \mathcal{X}$ maps from the counterfactual state space to the feature space.โ€ (Altmeyer et al. 2023)

For most generators, the state space is the feature space ($f$ is the identity function) and the number of counterfactuals $K$ is one. Latent Space generators instead search counterfactuals in some latent space $\mathcal{S}$. In this case, $f$ corresponds to the decoder part of the generative model, that is the function that maps back from the latent space to inputs.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

diff --git a/dev/explanation/generators/probe/index.html b/dev/explanation/generators/probe/index.html index 7ba826bba..81687169e 100644 --- a/dev/explanation/generators/probe/index.html +++ b/dev/explanation/generators/probe/index.html @@ -1,5 +1,5 @@ -PROBE ยท CounterfactualExplanations.jl

ProbeGenerator

The ProbeGenerator is designed to navigate the trade-offs between costs and robustness in Algorithmic Recourse (Pawelczyk et al. 2022).

Description

The goal of ProbeGenerator is to find a recourse xโ€™ whose prediction at any point y within some set around xโ€™ belongs to the positive class with probability 1 - r, where r is the recourse invalidation rate. It minimizes the gap between the achieved and desired recourse invalidation rates, minimizes recourse costs, and also ensures that the resulting recourse achieves a positive model prediction.

Explanation

The loss function this generator is defined below. R is a hinge loss parameter which helps control for robustness. The loss and penalty functions can still be chosen freely.

\[\begin{aligned} +PROBE ยท CounterfactualExplanations.jl

ProbeGenerator

The ProbeGenerator is designed to navigate the trade-offs between costs and robustness in Algorithmic Recourse (Pawelczyk et al. 2022).

Description

The goal of ProbeGenerator is to find a recourse xโ€™ whose prediction at any point y within some set around xโ€™ belongs to the positive class with probability 1 - r, where r is the recourse invalidation rate. It minimizes the gap between the achieved and desired recourse invalidation rates, minimizes recourse costs, and also ensures that the resulting recourse achieves a positive model prediction.

Explanation

The loss function this generator is defined below. R is a hinge loss parameter which helps control for robustness. The loss and penalty functions can still be chosen freely.

\[\begin{aligned} R(x'; \sigma^2 I) + l(f(x'), s) + \lambda d_c(x', x) \end{aligned}\]

R uses the following formula to control for noise. It generates small perturbations and checks how often the counterfactual explanation flips back to a factual one, when small amounts of noise are added to it.

\[\begin{aligned} \Delta(x^{\hat{E}}) &= E_{\varepsilon}[h(x^{\hat{E}}) - h(x^{\hat{E}} + \varepsilon)] @@ -10,4 +10,4 @@ generator = CounterfactualExplanations.Generators.ProbeGenerator(opt=opt) conv = CounterfactualExplanations.Convergence.InvalidationRateConvergence(;invalidation_rate=0.5) ce = generate_counterfactual(x, target, counterfactual_data, M, generator, convergence=conv) -plot(ce)

Choosing different invalidation rates makes the counterfactual more or less robust. The following plot shows the counterfactuals generated for different invalidation rates.

References

Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.

+plot(ce)

Choosing different invalidation rates makes the counterfactual more or less robust. The following plot shows the counterfactuals generated for different invalidation rates.

References

Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.

diff --git a/dev/explanation/generators/revise/index.html b/dev/explanation/generators/revise/index.html index c10003b35..8fe978375 100644 --- a/dev/explanation/generators/revise/index.html +++ b/dev/explanation/generators/revise/index.html @@ -1,5 +1,5 @@ -REVISE ยท CounterfactualExplanations.jl

REVISEGenerator

REVISE is a Latent Space generator introduced by Joshi et al. (2019).

Description

The current consensus in the literature is that Counterfactual Explanations should be realistic: the generated counterfactuals should look like they were generated by the data-generating process (DGP) that governs the problem at hand. With respect to Algorithmic Recourse, it is certainly true that counterfactuals should be realistic in order to be actionable for individuals.[1] To address this need, researchers have come up with various approaches in recent years. Among the most popular approaches is Latent Space Search, which was first proposed in Joshi et al. (2019): instead of traversing the feature space directly, this approach relies on a separate generative model that learns a latent space representation of the DGP. Assuming the generative model is well-specified, access to the learned latent embeddings of the data comes with two advantages:

  1. Since the learned DGP is encoded in the latent space, the generated counterfactuals will respect the learned representation of the data. In practice, this means that counterfactuals will be realistic.
  2. The latent space is typically a compressed (i.e.ย lower dimensional) version of the feature space. This makes the counterfactual search less costly.

There are also certain disadvantages though:

  1. Learning generative models is (typically) an expensive task, which may well outweigh the benefits associated with utlimately traversing a lower dimensional space.
  2. If the generative model is poorly specified, this will affect the quality of the counterfactuals.[2]

Anyway, traversing latent embeddings is a powerful idea that may be very useful depending on the specific context. This tutorial introduces the concept and how it is implemented in this package.

Usage

The approach can be used in our package as follows:

generator = REVISEGenerator()
+REVISE ยท CounterfactualExplanations.jl

REVISEGenerator

REVISE is a Latent Space generator introduced by Joshi et al. (2019).

Description

The current consensus in the literature is that Counterfactual Explanations should be realistic: the generated counterfactuals should look like they were generated by the data-generating process (DGP) that governs the problem at hand. With respect to Algorithmic Recourse, it is certainly true that counterfactuals should be realistic in order to be actionable for individuals.[1] To address this need, researchers have come up with various approaches in recent years. Among the most popular approaches is Latent Space Search, which was first proposed in Joshi et al. (2019): instead of traversing the feature space directly, this approach relies on a separate generative model that learns a latent space representation of the DGP. Assuming the generative model is well-specified, access to the learned latent embeddings of the data comes with two advantages:

  1. Since the learned DGP is encoded in the latent space, the generated counterfactuals will respect the learned representation of the data. In practice, this means that counterfactuals will be realistic.
  2. The latent space is typically a compressed (i.e.ย lower dimensional) version of the feature space. This makes the counterfactual search less costly.

There are also certain disadvantages though:

  1. Learning generative models is (typically) an expensive task, which may well outweigh the benefits associated with utlimately traversing a lower dimensional space.
  2. If the generative model is poorly specified, this will affect the quality of the counterfactuals.[2]

Anyway, traversing latent embeddings is a powerful idea that may be very useful depending on the specific context. This tutorial introduces the concept and how it is implemented in this package.

Usage

The approach can be used in our package as follows:

generator = REVISEGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
 plot(ce)

Worked 2D Examples

Below we load 2D data and train a VAE on it and plot the original samples against their reconstructions.

# output: true
 
@@ -52,4 +52,4 @@
 # Define generator:
 generator = REVISEGenerator(opt=Flux.Adam(0.1))
 # Generate recourse:
-ce = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence=conv)

The chart below shows the results:

References

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

[1] In general, we believe that there may be a trade-off between creating counterfactuals that respect the DGP vs.ย counterfactuals reflect the behaviour of the black-model in question - both accurately and complete.

[2] We believe that there is another potentially crucial disadvantage of relying on a separate generative model: it reallocates the task of learning realistic explanations for the data from the black-box model to the generative model.

+ce = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence=conv)

The chart below shows the results:

References

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

[1] In general, we believe that there may be a trade-off between creating counterfactuals that respect the DGP vs.ย counterfactuals reflect the behaviour of the black-model in question - both accurately and complete.

[2] We believe that there is another potentially crucial disadvantage of relying on a separate generative model: it reallocates the task of learning realistic explanations for the data from the black-box model to the generative model.

diff --git a/dev/explanation/index.html b/dev/explanation/index.html index 4e85ffb4e..b6781753c 100644 --- a/dev/explanation/index.html +++ b/dev/explanation/index.html @@ -1,2 +1,2 @@ -Overview ยท CounterfactualExplanations.jl

Explanation

In this section you will find detailed explanations about the methodology and code.

Explanation clarifies, deepens and broadens the readerโ€™s understanding of a subject.

โ€” Diรกtaxis

In other words, you come here because you are interested in understanding how all of this actually works ๐Ÿค“.

+Overview ยท CounterfactualExplanations.jl

Explanation

In this section you will find detailed explanations about the methodology and code.

Explanation clarifies, deepens and broadens the readerโ€™s understanding of a subject.

โ€” Diรกtaxis

In other words, you come here because you are interested in understanding how all of this actually works ๐Ÿค“.

diff --git a/dev/explanation/optimisers/jsma/index.html b/dev/explanation/optimisers/jsma/index.html index 2779c3cee..d9128725d 100644 --- a/dev/explanation/optimisers/jsma/index.html +++ b/dev/explanation/optimisers/jsma/index.html @@ -1,5 +1,5 @@ -JSMA ยท CounterfactualExplanations.jl

Jacobian-based Saliency Map Attack

To search counterfactuals, Schut et al. (2021) propose to use a Jacobian-Based Saliency Map Attack (JSMA) inspired by the literature on adversarial attacks. It works by moving in the direction of the most salient feature at a fixed step size in each iteration. Schut et al. (2021) use this optimisation rule in the context of Bayesian classifiers and demonstrate good results in terms of plausibility โ€” how realistic counterfactuals are โ€” and redundancy โ€” how sparse the proposed feature changes are.

JSMADescent

To implement this approach in a reusable manner, we have added JSMA as a Flux optimiser. In particular, we have added a class JSMADescent<:Flux.Optimise.AbstractOptimiser, for which we have overloaded the Flux.Optimise.apply! method. This makes it possible to reuse JSMADescent as an optimiser in composable generators.

The optimiser can be used with with any generator as follows:

using CounterfactualExplanations.Generators: JSMADescent
+JSMA ยท CounterfactualExplanations.jl

Jacobian-based Saliency Map Attack

To search counterfactuals, Schut et al. (2021) propose to use a Jacobian-Based Saliency Map Attack (JSMA) inspired by the literature on adversarial attacks. It works by moving in the direction of the most salient feature at a fixed step size in each iteration. Schut et al. (2021) use this optimisation rule in the context of Bayesian classifiers and demonstrate good results in terms of plausibility โ€” how realistic counterfactuals are โ€” and redundancy โ€” how sparse the proposed feature changes are.

JSMADescent

To implement this approach in a reusable manner, we have added JSMA as a Flux optimiser. In particular, we have added a class JSMADescent<:Flux.Optimise.AbstractOptimiser, for which we have overloaded the Flux.Optimise.apply! method. This makes it possible to reuse JSMADescent as an optimiser in composable generators.

The optimiser can be used with with any generator as follows:

using CounterfactualExplanations.Generators: JSMADescent
 generator = GenericGenerator() |>
     gen -> @with_optimiser(gen,JSMADescent(;ฮท=0.1))
-ce = generate_counterfactual(x, target, counterfactual_data, M, generator)

The figure below compares the resulting counterfactual search outcome to the corresponding outcome with generic Descent.

plot(p1,p2,size=(1000,400))

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

+ce = generate_counterfactual(x, target, counterfactual_data, M, generator)

The figure below compares the resulting counterfactual search outcome to the corresponding outcome with generic Descent.

plot(p1,p2,size=(1000,400))

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

diff --git a/dev/explanation/optimisers/overview/index.html b/dev/explanation/optimisers/overview/index.html index bba51c67f..102d8a0c3 100644 --- a/dev/explanation/optimisers/overview/index.html +++ b/dev/explanation/optimisers/overview/index.html @@ -1,2 +1,2 @@ -Overview ยท CounterfactualExplanations.jl

Optimisation Rules

Counterfactual search is an optimization problem. Consequently, the choice of the optimisation rule affects the generated counterfactuals. In the short term, we aim to enable users to choose any of the available Flux optimisers. This has not been sufficiently tested yet, and you may run into issues.

Custom Optimisation Rules

Flux optimisers are specifically designed for deep learning, and in particular, for learning model parameters. In counterfactual search, the features are the free parameters that we are optimising over. To this end, some custom optimisation rules are necessary to incorporate ideas presented in the literature. In the following, we introduce those rules.

+Overview ยท CounterfactualExplanations.jl

Optimisation Rules

Counterfactual search is an optimization problem. Consequently, the choice of the optimisation rule affects the generated counterfactuals. In the short term, we aim to enable users to choose any of the available Flux optimisers. This has not been sufficiently tested yet, and you may run into issues.

Custom Optimisation Rules

Flux optimisers are specifically designed for deep learning, and in particular, for learning model parameters. In counterfactual search, the features are the free parameters that we are optimising over. To this end, some custom optimisation rules are necessary to incorporate ideas presented in the literature. In the following, we introduce those rules.

diff --git a/dev/extensions/index.html b/dev/extensions/index.html index 8cacbc1fc..2764e59cb 100644 --- a/dev/extensions/index.html +++ b/dev/extensions/index.html @@ -1,2 +1,2 @@ -Overview ยท CounterfactualExplanations.jl

โ›“๏ธ Extensions

In this section, you will find information about package extensions of the CounterfactualExplanations package. Extensions are a relatively new feature of Julia that allows users to conditionally load code based on the presence of other packages. This is useful for creating packages that extend the functionality of other packages, without requiring the user to install the package being extended.

+Overview ยท CounterfactualExplanations.jl

โ›“๏ธ Extensions

In this section, you will find information about package extensions of the CounterfactualExplanations package. Extensions are a relatively new feature of Julia that allows users to conditionally load code based on the presence of other packages. This is useful for creating packages that extend the functionality of other packages, without requiring the user to install the package being extended.

diff --git a/dev/extensions/laplace_redux/index.html b/dev/extensions/laplace_redux/index.html index 262de58cd..e5cd81c84 100644 --- a/dev/extensions/laplace_redux/index.html +++ b/dev/extensions/laplace_redux/index.html @@ -1,5 +1,5 @@ -LaplaceRedux ยท CounterfactualExplanations.jl

LaplaceRedux.jl

LaplaceRedux.jl is one of Taijaโ€™s own packages that provides a framework for Effortless Bayesian Deep Learning through Laplace Approximation for Flux.jl neural networks. The methodology was first proposed by Immer, Korzepa, and Bauer (2020) and implemented in Python by Daxberger et al. (2021). This is relevant to the work on counterfactual explanations (CE), because research has shown that counterfactual explanations for Bayesian models are typically more plausible, because Bayesian models are able to capture the uncertainty in the data (Schut et al. 2021).

Read More

To learn more about Laplace Redux, head over to the official documentation.

Example

The extension will be loaded automatically when loading the LaplaceRedux package (assuming the CounterfactualExplanations package is also loaded).

using LaplaceRedux

Next, we will fit a neural network with Laplace Approximation to the moons dataset using our standard package API for doing so. By default, the Bayesian prior is optimized through empirical Bayes using the LaplaceRedux package.

# Fit model to data:
+LaplaceRedux ยท CounterfactualExplanations.jl

LaplaceRedux.jl

LaplaceRedux.jl is one of Taijaโ€™s own packages that provides a framework for Effortless Bayesian Deep Learning through Laplace Approximation for Flux.jl neural networks. The methodology was first proposed by Immer, Korzepa, and Bauer (2020) and implemented in Python by Daxberger et al. (2021). This is relevant to the work on counterfactual explanations (CE), because research has shown that counterfactual explanations for Bayesian models are typically more plausible, because Bayesian models are able to capture the uncertainty in the data (Schut et al. 2021).

Read More

To learn more about Laplace Redux, head over to the official documentation.

Example

The extension will be loaded automatically when loading the LaplaceRedux package (assuming the CounterfactualExplanations package is also loaded).

using LaplaceRedux

Next, we will fit a neural network with Laplace Approximation to the moons dataset using our standard package API for doing so. By default, the Bayesian prior is optimized through empirical Bayes using the LaplaceRedux package.

# Fit model to data:
 data = CounterfactualData(load_moons()...)
 M = fit_model(data, :LaplaceRedux; n_hidden=16)
LaplaceReduxExt.LaplaceReduxModel(Laplace(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, :all, nothing, :full, LaplaceRedux.Curvature.GGN(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, Flux.Losses.logitcrossentropy, Array{Float32}[[-1.3098596 0.59241515; 0.91760206 0.02950162; โ€ฆ ; -0.018356863 0.12850936; -0.5381665 -0.7872097], [-0.2581085, -0.90997887, -0.5418944, -0.23735572, 0.81020063, -0.3033359, -0.47902864, -0.6432098, -0.038013518, 0.028280666, 0.009903266, -0.8796683, 0.41090682, 0.011093224, -0.1580453, 0.7911349], [3.092321 -2.4660816 โ€ฆ -0.3446268 -1.465249; -2.9468734 3.167357 โ€ฆ 0.31758657 1.7140366], [-0.3107697, 0.31076983]], 1.0, :all, nothing), 1.0, 0.0, Float32[-1.3098596, 0.91760206, 0.5239727, -1.1579771, -0.851813, -1.9411169, 0.47409698, 0.6679365, 0.8944433, 0.663116  โ€ฆ  -0.3172857, 0.15530388, 1.3264753, -0.3506721, -0.3446268, 0.31758657, -1.465249, 1.7140366, -0.3107697, 0.31076983], [0.10530027048093525 0.0 โ€ฆ 0.0 0.0; 0.0 0.10530027048093525 โ€ฆ 0.0 0.0; โ€ฆ ; 0.0 0.0 โ€ฆ 0.10530027048093525 0.0; 0.0 0.0 โ€ฆ 0.0 0.10530027048093525], [0.10066431429751965 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 20.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 1.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 1.0101451389641771], [1.1006643142975197 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 21.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 2.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 2.010145138964177], [0.9412600568016627 0.003106911671721699 โ€ฆ 0.003743740333409532 -0.003743452315572739; 0.003106912946573237 0.6539263732691709 โ€ฆ 0.0030385955287734246 -0.0030390041204196414; โ€ฆ ; 0.0037437406323562283 0.003038591829991259 โ€ฆ 0.9624905710233649 0.03750911813897676; -0.0037434526145225856 -0.0030390004216833593 โ€ฆ 0.03750911813898124 0.9624905774453485], 82, 250, 2, 997.8087484836578), :classification_multi)

Finally, we select a factual instance and generate a counterfactual explanation for it using the generic gradient-based CE method.

# Select a factual instance:
 target = 1
@@ -14,4 +14,4 @@
     decision_threshold=0.9, max_iter=100
 )
 ce = generate_counterfactual(x, target, data, M, generator; convergence=conv)
-plot(ce, alpha=0.1)

References

Daxberger, Erik, Agustinus Kristiadi, Alexander Immer, Runa Eschenhagen, Matthias Bauer, and Philipp Hennig. 2021. โ€œLaplace Redux-Effortless Bayesian Deep Learning.โ€ Advances in Neural Information Processing Systems 34.

Immer, Alexander, Maciej Korzepa, and Matthias Bauer. 2020. โ€œImproving Predictions of Bayesian Neural Networks via Local Linearization.โ€ https://arxiv.org/abs/2008.08400.

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

+plot(ce, alpha=0.1)

References

Daxberger, Erik, Agustinus Kristiadi, Alexander Immer, Runa Eschenhagen, Matthias Bauer, and Philipp Hennig. 2021. โ€œLaplace Redux-Effortless Bayesian Deep Learning.โ€ Advances in Neural Information Processing Systems 34.

Immer, Alexander, Maciej Korzepa, and Matthias Bauer. 2020. โ€œImproving Predictions of Bayesian Neural Networks via Local Linearization.โ€ https://arxiv.org/abs/2008.08400.

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

diff --git a/dev/extensions/neurotree/index.html b/dev/extensions/neurotree/index.html index 7243a9d83..038eb6dbd 100644 --- a/dev/extensions/neurotree/index.html +++ b/dev/extensions/neurotree/index.html @@ -1,5 +1,5 @@ -NeuroTrees ยท CounterfactualExplanations.jl

NeuroTreeModels.jl

NeuroTreeModels.jl is a package that provides a framework for training differentiable tree-based models. This is relevant to the work on counterfactual explanations (CE), which often assumes that the underlying black-box model is differentiable with respect to its input. The literature on CE therefore regularly focuses exclusively on explaining deep learning models. This is at odds with the fact that the literature also typically focuses on tabular data, which is often best modeled by tree-based models (Grinsztajn, Oyallon, and Varoquaux 2022). The extension for NeuroTreeModels.jl provides a way to bridge this gap by allowing users to apply existing gradient-based CE methods to differentiable tree-based models.

Experimental Feature

Please note that this extension is still experimental. Neither the behaviour of differentiable tree-based models nor their interplay with counterfactual explanations is well understood at this point. If you encounter any issues, please report them to the package maintainers. Your feedback is highly appreciated.

Please also note that this extension is only tested on Julia 1.9 and higher, due to compatibility issues.

Example

The extension will be loaded automatically when loading the NeuroTreeModels package (assuming the CounterfactualExplanations package is also loaded).

using NeuroTreeModels

Next, we will fit a NeuroTree model to the moons dataset using our standard package API for doing so.

# Fit model to data:
+NeuroTrees ยท CounterfactualExplanations.jl

NeuroTreeModels.jl

NeuroTreeModels.jl is a package that provides a framework for training differentiable tree-based models. This is relevant to the work on counterfactual explanations (CE), which often assumes that the underlying black-box model is differentiable with respect to its input. The literature on CE therefore regularly focuses exclusively on explaining deep learning models. This is at odds with the fact that the literature also typically focuses on tabular data, which is often best modeled by tree-based models (Grinsztajn, Oyallon, and Varoquaux 2022). The extension for NeuroTreeModels.jl provides a way to bridge this gap by allowing users to apply existing gradient-based CE methods to differentiable tree-based models.

Experimental Feature

Please note that this extension is still experimental. Neither the behaviour of differentiable tree-based models nor their interplay with counterfactual explanations is well understood at this point. If you encounter any issues, please report them to the package maintainers. Your feedback is highly appreciated.

Please also note that this extension is only tested on Julia 1.9 and higher, due to compatibility issues.

Example

The extension will be loaded automatically when loading the NeuroTreeModels package (assuming the CounterfactualExplanations package is also loaded).

using NeuroTreeModels

Next, we will fit a NeuroTree model to the moons dataset using our standard package API for doing so.

# Fit model to data:
 data = CounterfactualData(load_moons()...)
 M = fit_model(
     data, :NeuroTree; 
@@ -17,4 +17,4 @@
     decision_threshold=0.9, max_iter=100
 )
 ce = generate_counterfactual(x, target, data, M, generator; convergence=conv)
-plot(ce, alpha=0.1)

References

Grinsztajn, Lรฉo, Edouard Oyallon, and Gaรซl Varoquaux. 2022. โ€œWhy Do Tree-Based Models Still Outperform Deep Learning on Tabular Data?โ€ https://arxiv.org/abs/2207.08815.

+plot(ce, alpha=0.1)

References

Grinsztajn, Lรฉo, Edouard Oyallon, and Gaรซl Varoquaux. 2022. โ€œWhy Do Tree-Based Models Still Outperform Deep Learning on Tabular Data?โ€ https://arxiv.org/abs/2207.08815.

diff --git a/dev/how_to_guides/custom_generators/index.html b/dev/how_to_guides/custom_generators/index.html index 76f789dee..7bc7cb750 100644 --- a/dev/how_to_guides/custom_generators/index.html +++ b/dev/how_to_guides/custom_generators/index.html @@ -1,5 +1,5 @@ -... add custom generators ยท CounterfactualExplanations.jl

How to add Custom Generators

As we will see in this short tutorial, building custom counterfactual generators is straightforward. We hope that this will facilitate contributions through the community.

Generic generator with dropout

To illustrate how custom generators can be implemented we will consider a simple example of a generator that extends the functionality of our GenericGenerator. We have noted elsewhere that the effectiveness of counterfactual explanations depends to some degree on the quality of the fitted model. Another, perhaps trivial, thing to note is that counterfactual explanations are not unique: there are potentially many valid counterfactual paths. One interesting (or silly) idea following these two observations might be to introduce some form of regularization in the counterfactual search. For example, we could use dropout to randomly switch features on and off in each iteration. Without dwelling further on the usefulness of this idea, let us see how it can be implemented.

The first code chunk below implements two important steps: 1) create an abstract subtype of the AbstractGradientBasedGenerator and 2) create a constructor similar to the GenericConstructor, but with one additional field for the probability of dropout.

# Abstract suptype:
+... add custom generators ยท CounterfactualExplanations.jl

How to add Custom Generators

As we will see in this short tutorial, building custom counterfactual generators is straightforward. We hope that this will facilitate contributions through the community.

Generic generator with dropout

To illustrate how custom generators can be implemented we will consider a simple example of a generator that extends the functionality of our GenericGenerator. We have noted elsewhere that the effectiveness of counterfactual explanations depends to some degree on the quality of the fitted model. Another, perhaps trivial, thing to note is that counterfactual explanations are not unique: there are potentially many valid counterfactual paths. One interesting (or silly) idea following these two observations might be to introduce some form of regularization in the counterfactual search. For example, we could use dropout to randomly switch features on and off in each iteration. Without dwelling further on the usefulness of this idea, let us see how it can be implemented.

The first code chunk below implements two important steps: 1) create an abstract subtype of the AbstractGradientBasedGenerator and 2) create a constructor similar to the GenericConstructor, but with one additional field for the probability of dropout.

# Abstract suptype:
 abstract type AbstractDropoutGenerator <: AbstractGradientBasedGenerator end
 
 # Constructor:
@@ -55,4 +55,4 @@
     x, target, counterfactual_data, M, generator;
     num_counterfactuals=5)
 
-plot(ce)

+plot(ce)

diff --git a/dev/how_to_guides/custom_models/index.html b/dev/how_to_guides/custom_models/index.html index 377464321..10b68f739 100644 --- a/dev/how_to_guides/custom_models/index.html +++ b/dev/how_to_guides/custom_models/index.html @@ -1,5 +1,5 @@ -... add custom models ยท CounterfactualExplanations.jl

How to add Custom Models

Adding custom models is possible and relatively straightforward, as we will demonstrate in this guide.

Custom Models

Apart from the default models you can use any arbitrary (differentiable) model and generate recourse in the same way as before. Only two steps are necessary to make your own Julia model compatible with this package:

  1. The model needs to be declared as a subtype of <:CounterfactualExplanations.Models.AbstractFittedModel.
  2. You need to extend the functions CounterfactualExplanations.Models.logits and CounterfactualExplanations.Models.probs for your custom model.

How FluxModel was added

To demonstrate how this can be done in practice, we will reiterate here how native support for Flux.jl models was enabled (Innes 2018). Once again we use synthetic data for an illustrative example. The code below loads the data and builds a simple model architecture that can be used for a multi-class prediction task. Note how outputs from the final layer are not passed through a softmax activation function, since the counterfactual loss is evaluated with respect to logits. The model is trained with dropout.

# Data:
+... add custom models ยท CounterfactualExplanations.jl

How to add Custom Models

Adding custom models is possible and relatively straightforward, as we will demonstrate in this guide.

Custom Models

Apart from the default models you can use any arbitrary (differentiable) model and generate recourse in the same way as before. Only two steps are necessary to make your own Julia model compatible with this package:

  1. The model needs to be declared as a subtype of <:CounterfactualExplanations.Models.AbstractFittedModel.
  2. You need to extend the functions CounterfactualExplanations.Models.logits and CounterfactualExplanations.Models.probs for your custom model.

How FluxModel was added

To demonstrate how this can be done in practice, we will reiterate here how native support for Flux.jl models was enabled (Innes 2018). Once again we use synthetic data for an illustrative example. The code below loads the data and builds a simple model architecture that can be used for a multi-class prediction task. Note how outputs from the final layer are not passed through a softmax activation function, since the counterfactual loss is evaluated with respect to logits. The model is trained with dropout.

# Data:
 N = 200
 data = TaijaData.load_blobs(N; centers=4, cluster_std=0.5)
 counterfactual_data = DataPreprocessing.CounterfactualData(data...)
@@ -51,4 +51,4 @@
 # Counterfactual search:
 generator = GenericGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

References

Innes, Mike. 2018. โ€œFlux: Elegant Machine Learning with Julia.โ€ Journal of Open Source Software 3 (25): 602. https://doi.org/10.21105/joss.00602.

+plot(ce)

References

Innes, Mike. 2018. โ€œFlux: Elegant Machine Learning with Julia.โ€ Journal of Open Source Software 3 (25): 602. https://doi.org/10.21105/joss.00602.

diff --git a/dev/how_to_guides/index.html b/dev/how_to_guides/index.html index 1d051dd0b..abdd24071 100644 --- a/dev/how_to_guides/index.html +++ b/dev/how_to_guides/index.html @@ -1,2 +1,2 @@ -Overview ยท CounterfactualExplanations.jl

How-To Guides

In this section, you will find a series of how-to-guides that showcase specific use cases of counterfactual explanations (CE).

How-to guides are directions that take the reader through the steps required to solve a real-world problem. How-to guides are goal-oriented.

โ€” Diรกtaxis

In other words, you come here because you may have some particular problem in mind, would like to see how it can be solved using CE and then most likely head off again ๐Ÿซก.

+Overview ยท CounterfactualExplanations.jl

How-To Guides

In this section, you will find a series of how-to-guides that showcase specific use cases of counterfactual explanations (CE).

How-to guides are directions that take the reader through the steps required to solve a real-world problem. How-to guides are goal-oriented.

โ€” Diรกtaxis

In other words, you come here because you may have some particular problem in mind, would like to see how it can be solved using CE and then most likely head off again ๐Ÿซก.

diff --git a/dev/index.html b/dev/index.html index 0ebdc50a1..ea7925843 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,5 +1,5 @@ -๐Ÿ  Home ยท CounterfactualExplanations.jl

Documentation for CounterfactualExplanations.jl.

CounterfactualExplanations

Counterfactual Explanations and Algorithmic Recourse in Julia.

Stable Dev Build Status Coverage Code Style: Blue License Package Downloads Aqua QA

CounterfactualExplanations.jl is a package for generating Counterfactual Explanations (CE) and Algorithmic Recourse (AR) for black-box algorithms. Both CE and AR are related tools for explainable artificial intelligence (XAI). While the package is written purely in Julia, it can be used to explain machine learning algorithms developed and trained in other popular programming languages like Python and R. See below for a short introduction and other resources or dive straight into the docs.

There is also a corresponding paper, Explaining Black-Box Models through Counterfactuals, which has been published in JuliaCon Proceedings. Please consider citing the paper, if you use this package in your work:

DOI DOI

@article{Altmeyer2023,
+๐Ÿ  Home ยท CounterfactualExplanations.jl

Documentation for CounterfactualExplanations.jl.

CounterfactualExplanations

Counterfactual Explanations and Algorithmic Recourse in Julia.

Stable Dev Build Status Coverage Code Style: Blue License Package Downloads Aqua QA

CounterfactualExplanations.jl is a package for generating Counterfactual Explanations (CE) and Algorithmic Recourse (AR) for black-box algorithms. Both CE and AR are related tools for explainable artificial intelligence (XAI). While the package is written purely in Julia, it can be used to explain machine learning algorithms developed and trained in other popular programming languages like Python and R. See below for a short introduction and other resources or dive straight into the docs.

There is also a corresponding paper, Explaining Black-Box Models through Counterfactuals, which has been published in JuliaCon Proceedings. Please consider citing the paper, if you use this package in your work:

DOI DOI

@article{Altmeyer2023,
   doi = {10.21105/jcon.00130},
   url = {https://doi.org/10.21105/jcon.00130},
   year = {2023},
@@ -32,7 +32,7 @@
 ce = generate_counterfactual(
   x, target, counterfactual_data, M, generator; 
   num_counterfactuals=3, convergence=conv,
-)

The plot below shows the resulting counterfactual path:

โ˜‘๏ธ Implemented Counterfactual Generators

Currently, the following counterfactual generators are implemented:

  • ClaPROAR (Altmeyer et al. 2023)
  • CLUE (Antorรกn et al. 2020)
  • DiCE (Mothilal, Sharma, and Tan 2020)
  • FeatureTweak (Tolomei et al. 2017)
  • Generic
  • GravitationalGenerator (Altmeyer et al. 2023)
  • Greedy (Schut et al. 2021)
  • GrowingSpheres (Laugel et al. 2017)
  • PROBE (Pawelczyk et al. 2022)
  • REVISE (Joshi et al. 2019)
  • Wachter (Wachter, Mittelstadt, and Russell 2017)

๐ŸŽฏ Goals and limitations

The goal of this library is to contribute to efforts towards trustworthy machine learning in Julia. The Julia language has an edge when it comes to trustworthiness: it is very transparent. Packages like this one are generally written in pure Julia, which makes it easy for users and developers to understand and contribute to open-source code. Eventually, this project aims to offer a one-stop-shop of counterfactual explanations.

Our ambition is to enhance the package through the following features:

  1. Support for all supervised machine learning models trained in MLJ.jl.
  2. Support for regression models.

๐Ÿ›  Contribute

Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on.

If any of the below applies to you, this might be the right open-source project for you:

  • Youโ€™re an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia.
  • Youโ€™re experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI.
  • Youโ€™re new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning.

@pat-alt here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open issue or start a discussion.

For more details on how to contribute see here. Please follow the SciML ColPrac guide.

There are also some general pointers for people looking to contribute to any of our Taija packages here.

๐ŸŽ“ Citation

If you want to use this codebase, please consider citing the corresponding paper:

@article{Altmeyer2023,
+)

The plot below shows the resulting counterfactual path:

โ˜‘๏ธ Implemented Counterfactual Generators

Currently, the following counterfactual generators are implemented:

  • ClaPROAR (Altmeyer et al. 2023)
  • CLUE (Antorรกn et al. 2020)
  • DiCE (Mothilal, Sharma, and Tan 2020)
  • FeatureTweak (Tolomei et al. 2017)
  • Generic
  • GravitationalGenerator (Altmeyer et al. 2023)
  • Greedy (Schut et al. 2021)
  • GrowingSpheres (Laugel et al. 2017)
  • PROBE (Pawelczyk et al. 2023)
  • REVISE (Joshi et al. 2019)
  • Wachter (Wachter, Mittelstadt, and Russell 2017)

๐ŸŽฏ Goals and limitations

The goal of this library is to contribute to efforts towards trustworthy machine learning in Julia. The Julia language has an edge when it comes to trustworthiness: it is very transparent. Packages like this one are generally written in pure Julia, which makes it easy for users and developers to understand and contribute to open-source code. Eventually, this project aims to offer a one-stop-shop of counterfactual explanations.

Our ambition is to enhance the package through the following features:

  1. Support for all supervised machine learning models trained in MLJ.jl.
  2. Support for regression models.

๐Ÿ›  Contribute

Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on. If you have an idea for a new feature or want to report a bug, please open a new issue.

Development

If your looking to contribute code, it may be helpful to check out the Explanation section of the docs.

Testing

Please always make sure to add tests for any new features or changes.

Documentation

If you add new features or change existing ones, please make sure to update the documentation accordingly. The documentation is written in Documenter.jl and is located in the docs/src folder.

Log Changes

As of version 1.1.1, we have tried to be more stringent about logging changes. Please make sure to add a note to the CHANGELOG.md file for any changes you make. It is sufficient to add a note under the Unreleased section.

General Pointers

There are also some general pointers for people looking to contribute to any of our Taija packages here.

Please follow the SciML ColPrac guide.

๐ŸŽ“ Citation

If you want to use this codebase, please consider citing the corresponding paper:

@article{Altmeyer2023,
   doi = {10.21105/jcon.00130},
   url = {https://doi.org/10.21105/jcon.00130},
   year = {2023},
@@ -43,4 +43,4 @@
   author = {Patrick Altmeyer and Arie van Deursen and Cynthia C. s. Liem},
   title = {Explaining Black-Box Models through Counterfactuals},
   journal = {Proceedings of the JuliaCon Conferences}
-}

๐Ÿ“š References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

Kaggle. 2011. โ€œGive Me Some Credit, Improve on the State of the Art in Credit Scoring by Predicting the Probability That Somebody Will Experience Financial Distress in the Next Two Years.โ€ Kaggle. https://www.kaggle.com/c/GiveMeSomeCredit.

Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ https://arxiv.org/abs/1712.08443.

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.

Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.

+}

๐Ÿ“š References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia CS Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In 2023 IEEE Conference on Secure and Trustworthy Machine Learning (SaTML), 418โ€“31. IEEE.

Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

Kaggle. 2011. โ€œGive Me Some Credit, Improve on the State of the Art in Credit Scoring by Predicting the Probability That Somebody Will Experience Financial Distress in the Next Two Years.โ€ https://www.kaggle.com/c/GiveMeSomeCredit; Kaggle. https://www.kaggle.com/c/GiveMeSomeCredit.

Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ https://arxiv.org/abs/1712.08443.

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2023. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ https://arxiv.org/abs/2203.06768.

Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.

Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.

Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.

diff --git a/dev/index_files/figure-commonmark/cell-10-output-1.svg b/dev/index_files/figure-commonmark/cell-10-output-1.svg index 77539e195..971166a4f 100644 --- a/dev/index_files/figure-commonmark/cell-10-output-1.svg +++ b/dev/index_files/figure-commonmark/cell-10-output-1.svg @@ -1,25 +1,25 @@ - + - + - + - + - + - - - + + + - + - + - - - + + + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/index_files/figure-commonmark/cell-5-output-1.svg b/dev/index_files/figure-commonmark/cell-5-output-1.svg index d095af72b..20e579893 100644 --- a/dev/index_files/figure-commonmark/cell-5-output-1.svg +++ b/dev/index_files/figure-commonmark/cell-5-output-1.svg @@ -1,98 +1,98 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/objects.inv b/dev/objects.inv index 38f116fe0fc205ed855fe34a40432400cb6e20a8..adf412197994874b39819cfea0c6f0db85bdb539 100644 GIT binary patch delta 5640 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 delta 5483 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 h0eS8=qsfF%3%|5^#$vP -๐Ÿง Reference ยท CounterfactualExplanations.jl

Reference

In this reference, you will find a detailed overview of the package API.

Reference guides are technical descriptions of the machinery and how to operate it. Reference material is information-oriented.

โ€” Diรกtaxis

In other words, you come here because you want to take a very close look at the code ๐Ÿง.

Content

Exported functions

CounterfactualExplanations.CounterfactualExplanation โ€” Method
function CounterfactualExplanation(;
+๐Ÿง Reference ยท CounterfactualExplanations.jl

Reference

In this reference, you will find a detailed overview of the package API.

Reference guides are technical descriptions of the machinery and how to operate it. Reference material is information-oriented.

โ€” Diรกtaxis

In other words, you come here because you want to take a very close look at the code ๐Ÿง.

Content

Exported functions

CounterfactualExplanations.CounterfactualExplanation โ€” Method
function CounterfactualExplanation(;
 	x::AbstractArray,
 	target::RawTargetType,
 	data::CounterfactualData,
@@ -8,14 +8,14 @@
 	num_counterfactuals::Int = 1,
 	initialization::Symbol = :add_perturbation,
     convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,
-)

Outer method to construct a CounterfactualExplanation structure.

source
CounterfactualExplanations.generate_counterfactual โ€” Method
generate_counterfactual(
     x::Base.Iterators.Zip,
     target::RawTargetType,
     data::CounterfactualData,
     M::Models.AbstractFittedModel,
     generator::AbstractGenerator;
     kwargs...,
-)

Overloads the generate_counterfactual method to accept a zip of factuals x and return a vector of counterfactuals.

source
CounterfactualExplanations.generate_counterfactual โ€” Method
generate_counterfactual(
     x::Matrix,
     target::RawTargetType,
     data::CounterfactualData,
@@ -25,31 +25,46 @@
     initialization::Symbol=:add_perturbation,
     convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,
     timeout::Union{Nothing,Real}=nothing,
-)

The core function that is used to run counterfactual search for a given factual x, target, counterfactual data, model and generator. Keywords can be used to specify the desired threshold for the predicted target class probability and the maximum number of iterations.

Arguments

  • x::Matrix: Factual data point.
  • target::RawTargetType: Target class.
  • data::CounterfactualData: Counterfactual data.
  • M::Models.AbstractFittedModel: Fitted model.
  • generator::AbstractGenerator: Generator.
  • num_counterfactuals::Int=1: Number of counterfactuals to generate for factual.
  • initialization::Symbol=:add_perturbation: Initialization method. By default, the initialization is done by adding a small random perturbation to the factual to achieve more robustness.
  • convergence::Union{AbstractConvergence,Symbol}=:decision_threshold: Convergence criterion. By default, the convergence is based on the decision threshold.
  • timeout::Union{Nothing,Int}=nothing: Timeout in seconds.

Examples

Generic generator

using CounterfactualExplanations
+)

The core function that is used to run counterfactual search for a given factual x, target, counterfactual data, model and generator. Keywords can be used to specify the desired threshold for the predicted target class probability and the maximum number of iterations.

Arguments

  • x::Matrix: Factual data point.
  • target::RawTargetType: Target class.
  • data::CounterfactualData: Counterfactual data.
  • M::Models.AbstractFittedModel: Fitted model.
  • generator::AbstractGenerator: Generator.
  • num_counterfactuals::Int=1: Number of counterfactuals to generate for factual.
  • initialization::Symbol=:add_perturbation: Initialization method. By default, the initialization is done by adding a small random perturbation to the factual to achieve more robustness.
  • convergence::Union{AbstractConvergence,Symbol}=:decision_threshold: Convergence criterion. By default, the convergence is based on the decision threshold. Possible values are :decision_threshold, :max_iter, :generator_conditions or a conrete convergence object (e.g. DecisionThresholdConvergence).
  • timeout::Union{Nothing,Int}=nothing: Timeout in seconds.

Examples

Generic generator

julia> using CounterfactualExplanations
 
-# Data:
-using CounterfactualExplanations.Data
-using Random
-Random.seed!(1234)
-xs, ys = Data.toy_data_linear()
-X = hcat(xs...)
-counterfactual_data = CounterfactualData(X,ys')
+julia> using TaijaData
+       
+        # Counteractual data and model:
 
-# Model
-using CounterfactualExplanations.Models: LogisticModel, probs 
-# Logit model:
-w = [1.0 1.0] # true coefficients
-b = 0
-M = LogisticModel(w, [b])
+julia> counterfactual_data = CounterfactualData(load_linearly_separable()...);
 
-# Randomly selected factual:
-x = select_factual(counterfactual_data,rand(1:size(X)[2]))
-y = round(probs(M, x)[1])
-target = round(probs(M, x)[1])==0 ? 1 : 0 
+julia> M = fit_model(counterfactual_data, :Linear);
 
-# Counterfactual search:
-generator = GenericGenerator()
-ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
source
CounterfactualExplanations.generate_counterfactual โ€” Method
generate_counterfactual(
+julia> target = 2;
+
+julia> factual = 1;
+
+julia> chosen = rand(findall(predict_label(M, counterfactual_data) .== factual));
+
+julia> x = select_factual(counterfactual_data, chosen);
+       
+       # Search:
+
+julia> generator = Generators.GenericGenerator();
+
+julia> ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
+CounterfactualExplanation
+Convergence: โœ… after 7 steps.

Broadcasting

The generate_counterfactual method can also be broadcasted over a tuple containing an array. This allows for generating multiple counterfactuals in parallel.

julia> chosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 5);
+
+julia> xs = select_factual(counterfactual_data, chosen);
+
+julia> ces = generate_counterfactual.(xs, target, counterfactual_data, M, generator)
+5-element Vector{CounterfactualExplanation}:
+ CounterfactualExplanation
+Convergence: โœ… after 7 steps.
+ CounterfactualExplanation
+Convergence: โœ… after 7 steps.
+ CounterfactualExplanation
+Convergence: โœ… after 8 steps.
+ CounterfactualExplanation
+Convergence: โœ… after 6 steps.
+ CounterfactualExplanation
+Convergence: โœ… after 7 steps.
source
CounterfactualExplanations.generate_counterfactual โ€” Method
generate_counterfactual(
     x::Matrix,
     target::RawTargetType,
     data::DataPreprocessing.CounterfactualData,
@@ -60,17 +75,17 @@
         decision_threshold=(1 / length(data.y_levels)), max_iter=1000
     ),
     kwrgs...,
-)

Overloads the generate_counterfactual for the GrowingSpheresGenerator generator.

source
CounterfactualExplanations.generate_counterfactual โ€” Method
generate_counterfactual(
     x::Vector{<:Matrix},
     target::RawTargetType,
     data::CounterfactualData,
     M::Models.AbstractFittedModel,
     generator::AbstractGenerator;
     kwargs...,
-)

Overloads the generate_counterfactual method to accept a vector of factuals x and return a vector of counterfactuals.

source
CounterfactualExplanations.target_probs โ€” Function
target_probs(
     ce::CounterfactualExplanation,
     x::Union{AbstractArray,Nothing}=nothing,
-)

Returns the predicted probability of the target class for x. If x is nothing, the predicted probability corresponding to the counterfactual value is returned.

source
CounterfactualExplanations.update! โ€” Method
update!(ce::CounterfactualExplanation)

An important subroutine that updates the counterfactual explanation. It takes a snapshot of the current counterfactual search state and passes it to the generator. Based on the current state the generator generates perturbations. Various constraints are then applied to the proposed vector of feature perturbations. Finally, the counterfactual search state is updated.

source
CounterfactualExplanations.Convergence.converged โ€” Method
converged(convergence::MaxIterConvergence, ce::CounterfactualExplanation)

Checks if the counterfactual search has converged when the convergence criterion is maximum iterations. This means the counterfactual search will not terminate until the maximum number of iterations has been reached independently of the other convergence criteria.

source
CounterfactualExplanations.Convergence.hinge_loss โ€” Method
hinge_loss(convergence::InvalidationRateConvergence, ce::AbstractCounterfactualExplanation)

Calculates the hinge loss of a counterfactual explanation.

Arguments

  • convergence::InvalidationRateConvergence: The convergence criterion to use.
  • ce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the hinge loss for.

Returns

The hinge loss of the counterfactual explanation.

source
CounterfactualExplanations.Convergence.invalidation_rate โ€” Method
invalidation_rate(ce::AbstractCounterfactualExplanation)

Calculates the invalidation rate of a counterfactual explanation.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the invalidation rate for.
  • kwargs: Additional keyword arguments to pass to the function.

Returns

The invalidation rate of the counterfactual explanation.

source
CounterfactualExplanations.update! โ€” Method
update!(ce::CounterfactualExplanation)

An important subroutine that updates the counterfactual explanation. It takes a snapshot of the current counterfactual search state and passes it to the generator. Based on the current state the generator generates perturbations. Various constraints are then applied to the proposed vector of feature perturbations. Finally, the counterfactual search state is updated.

source
CounterfactualExplanations.Convergence.DecisionThresholdConvergence โ€” Type
DecisionThresholdConvergence

Convergence criterion based on the target class probability threshold. The search stops when the target class probability exceeds the predefined threshold.

Fields

  • decision_threshold::AbstractFloat: The predefined threshold for the target class probability.
  • max_iter::Int: The maximum number of iterations.
  • min_success_rate::AbstractFloat: The minimum success rate for the target class probability.
source
CounterfactualExplanations.Convergence.GeneratorConditionsConvergence โ€” Type
GeneratorConditionsConvergence

Convergence criterion for counterfactual explanations based on the generator conditions. The search stops when the gradients of the search objective are below a certain threshold and the generator conditions are satisfied.

Fields

  • decision_threshold::AbstractFloat: The threshold for the decision probability.
  • gradient_tol::AbstractFloat: The tolerance for the gradients of the search objective.
  • max_iter::Int: The maximum number of iterations.
  • min_success_rate::AbstractFloat: The minimum success rate for the generator conditions (across counterfactuals).
source
CounterfactualExplanations.Convergence.converged โ€” Method
converged(convergence::MaxIterConvergence, ce::CounterfactualExplanation)

Checks if the counterfactual search has converged when the convergence criterion is maximum iterations. This means the counterfactual search will not terminate until the maximum number of iterations has been reached independently of the other convergence criteria.

source
CounterfactualExplanations.Convergence.hinge_loss โ€” Method
hinge_loss(convergence::InvalidationRateConvergence, ce::AbstractCounterfactualExplanation)

Calculates the hinge loss of a counterfactual explanation.

Arguments

  • convergence::InvalidationRateConvergence: The convergence criterion to use.
  • ce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the hinge loss for.

Returns

The hinge loss of the counterfactual explanation.

source
CounterfactualExplanations.Convergence.invalidation_rate โ€” Method
invalidation_rate(ce::AbstractCounterfactualExplanation)

Calculates the invalidation rate of a counterfactual explanation.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the invalidation rate for.
  • kwargs: Additional keyword arguments to pass to the function.

Returns

The invalidation rate of the counterfactual explanation.

source
CounterfactualExplanations.Evaluation.benchmark โ€” Method
benchmark(
     data::CounterfactualData;
     models::Dict{<:Any,<:Any}=standard_models_catalogue,
     generators::Union{Nothing,Dict{<:Any,<:AbstractGenerator}}=nothing,
@@ -82,7 +97,7 @@
     store_ce::Bool=false,
     parallelizer::Union{Nothing,AbstractParallelizer}=nothing,
     kwrgs...,
-)

Runs the benchmarking exercise as follows:

  1. Randomly choose a factual and target label unless specified.
  2. If no pretrained models are provided, it is assumed that a dictionary of callable model objects is provided (by default using the standard_models_catalogue).
  3. Each of these models is then trained on the data.
  4. For each model separately choose n_individuals randomly from the non-target (factual) class. For each generator create a benchmark as in benchmark(xs::Union{AbstractArray,Base.Iterators.Zip}).
  5. Finally, concatenate the results.

If vertical_splits is specified to an integer, the computations are split vertically into vertical_splits chunks. In this case, the results are stored in a temporary directory and concatenated afterwards.

source
CounterfactualExplanations.Evaluation.benchmark โ€” Method
benchmark(
+)

Runs the benchmarking exercise as follows:

  1. Randomly choose a factual and target label unless specified.
  2. If no pretrained models are provided, it is assumed that a dictionary of callable model objects is provided (by default using the standard_models_catalogue).
  3. Each of these models is then trained on the data.
  4. For each model separately choose n_individuals randomly from the non-target (factual) class. For each generator create a benchmark as in benchmark(xs::Union{AbstractArray,Base.Iterators.Zip}).
  5. Finally, concatenate the results.

If vertical_splits is specified to an integer, the computations are split vertically into vertical_splits chunks. In this case, the results are stored in a temporary directory and concatenated afterwards.

source
CounterfactualExplanations.Evaluation.benchmark โ€” Method
benchmark(
     x::Union{AbstractArray,Base.Iterators.Zip},
     target::RawTargetType,
     data::CounterfactualData;
@@ -95,19 +110,19 @@
     store_ce::Bool=false,
     parallelizer::Union{Nothing,AbstractParallelizer}=nothing,
     kwrgs...,
-)

First generates counterfactual explanations for factual x, the target and data using each of the provided models and generators. Then generates a Benchmark for the vector of counterfactual explanations as in benchmark(counterfactual_explanations::Vector{CounterfactualExplanation}).

source
CounterfactualExplanations.Evaluation.benchmark โ€” Method
benchmark(
     counterfactual_explanations::Vector{CounterfactualExplanation};
     meta_data::Union{Nothing,<:Vector{<:Dict}}=nothing,
     measure::Union{Function,Vector{Function}}=default_measures,
     store_ce::Bool=false,
-)

Generates a Benchmark for a vector of counterfactual explanations. Optionally meta_data describing each individual counterfactual explanation can be supplied. This should be a vector of dictionaries of the same length as the vector of counterfactuals. If no meta_data is supplied, it will be automatically inferred. All measure functions are applied to each counterfactual explanation. If store_ce=true, the counterfactual explanations are stored in the benchmark.

source
CounterfactualExplanations.Evaluation.evaluate โ€” Function
evaluate(
+)

Generates a Benchmark for a vector of counterfactual explanations. Optionally meta_data describing each individual counterfactual explanation can be supplied. This should be a vector of dictionaries of the same length as the vector of counterfactuals. If no meta_data is supplied, it will be automatically inferred. All measure functions are applied to each counterfactual explanation. If store_ce=true, the counterfactual explanations are stored in the benchmark.

source
CounterfactualExplanations.Evaluation.evaluate โ€” Function
evaluate(
     ce::CounterfactualExplanation;
     measure::Union{Function,Vector{Function}}=default_measures,
     agg::Function=mean,
     report_each::Bool=false,
     output_format::Symbol=:Vector,
     pivot_longer::Bool=true
-)

Just computes evaluation measures for the counterfactual explanation. By default, no meta data is reported. For report_meta=true, meta data is automatically inferred, unless this overwritten by meta_data. The optional meta_data argument should be a vector of dictionaries of the same length as the vector of counterfactual explanations.

source
CounterfactualExplanations.Evaluation.validity โ€” Method
validity(ce::CounterfactualExplanation; ฮณ=0.5)

Checks of the counterfactual search has been successful with respect to the probability threshold ฮณ. In case multiple counterfactuals were generated, the function returns the proportion of successful counterfactuals.

source
CounterfactualExplanations.DataPreprocessing.CounterfactualData โ€” Method
CounterfactualData(
+)

Just computes evaluation measures for the counterfactual explanation. By default, no meta data is reported. For report_meta=true, meta data is automatically inferred, unless this overwritten by meta_data. The optional meta_data argument should be a vector of dictionaries of the same length as the vector of counterfactual explanations.

source
CounterfactualExplanations.Evaluation.validity โ€” Method
validity(ce::CounterfactualExplanation; ฮณ=0.5)

Checks of the counterfactual search has been successful with respect to the probability threshold ฮณ. In case multiple counterfactuals were generated, the function returns the proportion of successful counterfactuals.

source
CounterfactualExplanations.DataPreprocessing.CounterfactualData โ€” Method
CounterfactualData(
     X::AbstractMatrix, y::AbstractMatrix;
     mutability::Union{Vector{Symbol},Nothing}=nothing,
     domain::Union{Any,Nothing}=nothing,
@@ -117,43 +132,43 @@
 )

This outer constructor method prepares features X and labels y to be used with the package. Mutability and domain constraints can be added for the features. The function also accepts arguments that specify which features are categorical and which are continues. These arguments are currently not used.

Examples

using CounterfactualExplanations.Data
 x, y = toy_data_linear()
 X = hcat(x...)
-counterfactual_data = CounterfactualData(X,y')
source
CounterfactualExplanations.Models.DecisionTreeModel โ€” Method
DecisionTreeModel(data::CounterfactualData; kwargs...)

Constructs a new TreeModel object wrapped around a decision tree from the data in a CounterfactualData object. Not called by the user directly.

Arguments

  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • model::TreeModel: A TreeModel object.
source
CounterfactualExplanations.Models.Linear โ€” Method
Linear(data::CounterfactualData; kwargs...)

Constructs a model with one linear layer. If the output is binary, this corresponds to logistic regression, since model outputs are passed through the sigmoid function. If the output is multi-class, this corresponds to multinomial logistic regression, since model outputs are passed through the softmax function.

source
CounterfactualExplanations.Models.RandomForestModel โ€” Method
RandomForestModel(data::CounterfactualData; kwargs...)

Constructs a new TreeModel object wrapped around a random forest from the data in a CounterfactualData object. Not called by the user directly.

Arguments

  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • model::TreeModel: A TreeModel object.
source
CounterfactualExplanations.Models.fit_model โ€” Function
fit_model(
+)

Outer constructor method that accepts a Tables.MatrixTable. By default, the indices of categorical and continuous features are automatically inferred the features' scitype.

source
CounterfactualExplanations.Models.DecisionTreeModel โ€” Method
DecisionTreeModel(data::CounterfactualData; kwargs...)

Constructs a new TreeModel object wrapped around a decision tree from the data in a CounterfactualData object. Not called by the user directly.

Arguments

  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • model::TreeModel: A TreeModel object.
source
CounterfactualExplanations.Models.Linear โ€” Method
Linear(data::CounterfactualData; kwargs...)

Constructs a model with one linear layer. If the output is binary, this corresponds to logistic regression, since model outputs are passed through the sigmoid function. If the output is multi-class, this corresponds to multinomial logistic regression, since model outputs are passed through the softmax function.

source
CounterfactualExplanations.Models.RandomForestModel โ€” Method
RandomForestModel(data::CounterfactualData; kwargs...)

Constructs a new TreeModel object wrapped around a random forest from the data in a CounterfactualData object. Not called by the user directly.

Arguments

  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • model::TreeModel: A TreeModel object.
source
CounterfactualExplanations.Models.fit_model โ€” Function
fit_model(
     counterfactual_data::CounterfactualData, model::Symbol=:MLP;
     kwrgs...
-)

Fits one of the available default models to the counterfactual_data. The model argument can be used to specify the desired model. The available values correspond to the keys of the all_models_catalogue dictionary.

source
CounterfactualExplanations.Models.logits โ€” Method
logits(M::AbstractFittedModel, X::AbstractArray)

Generic method that is compulsory for all models. It returns the raw model predictions. In classification this is sometimes referred to as logits: the non-normalized predictions that are fed into a link function to produce predicted probabilities. In regression (not currently implemented) raw outputs typically correspond to final outputs. In other words, there is typically no normalization involved.

source
CounterfactualExplanations.Models.logits โ€” Method
logits(M::TreeModel, X::AbstractArray)

Calculates the logit scores output by the model M for the input data X.

Arguments

  • M::TreeModel: The model selected by the user.
  • X::AbstractArray: The feature vector for which the logit scores are calculated.

Returns

  • logits::Matrix: A matrix of logits for each output class for each data point in X.

Example

logits = Models.logits(M, x) # calculates the logit scores for each output class for the data point x

source
CounterfactualExplanations.Models.model_evaluation โ€” Method
model_evaluation(M::AbstractFittedModel, test_data::CounterfactualData)

Helper function to compute F-Score for AbstractFittedModel on a (test) data set. By default, it computes the accuracy. Any other measure, e.g. from the StatisticalMeasures package, can be passed as an argument. Currently, only measures applicable to classification tasks are supported.

source
CounterfactualExplanations.Models.predict_label โ€” Method
predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::AbstractArray)

Returns the predicted output label for a given model M, data set counterfactual_data and input data X.

source
CounterfactualExplanations.Models.predict_label โ€” Method
predict_label(M::TreeModel, X::AbstractArray)

Returns the predicted label for X.

Arguments

  • M::TreeModel: The model selected by the user.
  • X::AbstractArray: The input array for which the label is predicted.

Returns

  • labels::AbstractArray: The predicted label for each data point in X.

Example

label = Models.predict_label(M, x) # returns the predicted label for each data point in x

source
CounterfactualExplanations.Models.predict_proba โ€” Method
predict_proba(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::Union{Nothing,AbstractArray})

Returns the predicted output probabilities for a given model M, data set counterfactual_data and input data X.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::AbstractFittedModel, X::AbstractArray)

Generic method that is compulsory for all models. It returns the normalized model predictions, so the predicted probabilities in the case of classification. In regression (not currently implemented) this method is redundant.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::TreeModel, X::AbstractArray{<:Number, 2})

Calculates the probability scores for each output class for the two-dimensional input data matrix X.

Arguments

  • M::TreeModel: The TreeModel.
  • X::AbstractArray: The feature vector for which the predictions are made.

Returns

  • p::Matrix: A matrix of probability scores for each output class for each data point in X.

Example

probabilities = Models.probs(M, X) # calculates the probability scores for each output class for each data point in X.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::TreeModel, X::AbstractArray{<:Number, 1})

Works the same way as the probs(M::TreeModel, X::AbstractArray{<:Number, 2}) method above, but handles 1-dimensional rather than 2-dimensional input data.

source
CounterfactualExplanations.Generators.FeatureTweakGenerator โ€” Method
FeatureTweakGenerator(; penalty::Union{Nothing,Function,Vector{Function}}=Objectives.distance_l2, ฯต::AbstractFloat=0.1)

Constructs a new Feature Tweak Generator object.

Uses the L2-norm as the penalty to measure the distance between the counterfactual and the factual. According to the paper by Tolomei et al., another recommended choice for the penalty in addition to the L2-norm is the L0-norm. The L0-norm simply minimizes the number of features that are changed through the tweak.

Arguments

  • penalty::Union{Nothing,Function,Vector{Function}}: The penalty function to use for the generator. Defaults to distance_l2.
  • ฯต::AbstractFloat: The tolerance value for the feature tweaks. Described at length in Tolomei et al. (https://arxiv.org/pdf/1706.06691.pdf). Defaults to 0.1.

Returns

  • generator::FeatureTweakGenerator: A non-gradient-based generator that can be used to generate counterfactuals using the feature tweak method.
source
CounterfactualExplanations.Models.logits โ€” Method
logits(M::AbstractFittedModel, X::AbstractArray)

Generic method that is compulsory for all models. It returns the raw model predictions. In classification this is sometimes referred to as logits: the non-normalized predictions that are fed into a link function to produce predicted probabilities. In regression (not currently implemented) raw outputs typically correspond to final outputs. In other words, there is typically no normalization involved.

source
CounterfactualExplanations.Models.logits โ€” Method
logits(M::TreeModel, X::AbstractArray)

Calculates the logit scores output by the model M for the input data X.

Arguments

  • M::TreeModel: The model selected by the user.
  • X::AbstractArray: The feature vector for which the logit scores are calculated.

Returns

  • logits::Matrix: A matrix of logits for each output class for each data point in X.

Example

logits = Models.logits(M, x) # calculates the logit scores for each output class for the data point x

source
CounterfactualExplanations.Models.model_evaluation โ€” Method
model_evaluation(M::AbstractFittedModel, test_data::CounterfactualData)

Helper function to compute F-Score for AbstractFittedModel on a (test) data set. By default, it computes the accuracy. Any other measure, e.g. from the StatisticalMeasures package, can be passed as an argument. Currently, only measures applicable to classification tasks are supported.

source
CounterfactualExplanations.Models.predict_label โ€” Method
predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::AbstractArray)

Returns the predicted output label for a given model M, data set counterfactual_data and input data X.

source
CounterfactualExplanations.Models.predict_label โ€” Method
predict_label(M::TreeModel, X::AbstractArray)

Returns the predicted label for X.

Arguments

  • M::TreeModel: The model selected by the user.
  • X::AbstractArray: The input array for which the label is predicted.

Returns

  • labels::AbstractArray: The predicted label for each data point in X.

Example

label = Models.predict_label(M, x) # returns the predicted label for each data point in x

source
CounterfactualExplanations.Models.predict_proba โ€” Method
predict_proba(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::Union{Nothing,AbstractArray})

Returns the predicted output probabilities for a given model M, data set counterfactual_data and input data X.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::AbstractFittedModel, X::AbstractArray)

Generic method that is compulsory for all models. It returns the normalized model predictions, so the predicted probabilities in the case of classification. In regression (not currently implemented) this method is redundant.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::TreeModel, X::AbstractArray{<:Number, 2})

Calculates the probability scores for each output class for the two-dimensional input data matrix X.

Arguments

  • M::TreeModel: The TreeModel.
  • X::AbstractArray: The feature vector for which the predictions are made.

Returns

  • p::Matrix: A matrix of probability scores for each output class for each data point in X.

Example

probabilities = Models.probs(M, X) # calculates the probability scores for each output class for each data point in X.

source
CounterfactualExplanations.Models.probs โ€” Method
probs(M::TreeModel, X::AbstractArray{<:Number, 1})

Works the same way as the probs(M::TreeModel, X::AbstractArray{<:Number, 2}) method above, but handles 1-dimensional rather than 2-dimensional input data.

source
CounterfactualExplanations.Generators.FeatureTweakGenerator โ€” Method
FeatureTweakGenerator(; penalty::Union{Nothing,Function,Vector{Function}}=Objectives.distance_l2, ฯต::AbstractFloat=0.1)

Constructs a new Feature Tweak Generator object.

Uses the L2-norm as the penalty to measure the distance between the counterfactual and the factual. According to the paper by Tolomei et al., another recommended choice for the penalty in addition to the L2-norm is the L0-norm. The L0-norm simply minimizes the number of features that are changed through the tweak.

Arguments

  • penalty::Union{Nothing,Function,Vector{Function}}: The penalty function to use for the generator. Defaults to distance_l2.
  • ฯต::AbstractFloat: The tolerance value for the feature tweaks. Described at length in Tolomei et al. (https://arxiv.org/pdf/1706.06691.pdf). Defaults to 0.1.

Returns

  • generator::FeatureTweakGenerator: A non-gradient-based generator that can be used to generate counterfactuals using the feature tweak method.
source
CounterfactualExplanations.Generators.GradientBasedGenerator โ€” Method
GradientBasedGenerator(;
 	loss::Union{Nothing,Function}=nothing,
 	penalty::Penalty=nothing,
 	ฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing,
 	latent_space::Bool::false,
 	opt::Flux.Optimise.AbstractOptimiser=Flux.Descent(),
     generative_model_params::NamedTuple=(;),
-)

Default outer constructor for GradientBasedGenerator.

Arguments

  • loss::Union{Nothing,Function}=nothing: The loss function used by the model.
  • penalty::Penalty=nothing: A penalty function for the generator to penalize counterfactuals too far from the original point.
  • ฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing: The weight of the penalty function.
  • latent_space::Bool=false: Whether to use the latent space of a generative model to generate counterfactuals.
  • opt::Flux.Optimise.AbstractOptimiser=Flux.Descent(): The optimizer to use for the generator.
  • generative_model_params::NamedTuple: The parameters of the generative model associated with the generator.

Returns

  • generator::GradientBasedGenerator: A gradient-based counterfactual generator.
source
CounterfactualExplanations.Generators.conditions_satisfied โ€” Method
conditions_satisfied(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)

The default method to check if the all conditions for convergence of the counterfactual search have been satisified for gradient-based generators. By default, gradient-based search is considered to have converged as soon as the proposed feature changes for all features are smaller than one percent of its standard deviation.

source
CounterfactualExplanations.Generators.feature_tweaking! โ€” Method
feature_tweaking!(ce::AbstractCounterfactualExplanation)

Returns a counterfactual instance of ce.x based on the ensemble of classifiers provided.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Returns

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Example

ce = feature_tweaking!(ce) # returns a counterfactual inside the ce.sโ€ฒ field based on the ensemble of classifiers provided

source
CounterfactualExplanations.Generators.hinge_loss โ€” Method
hinge_loss(convergence::AbstractConvergence, ce::AbstractCounterfactualExplanation)

The default hinge loss for any convergence criterion. Can be overridden inside the Convergence module as part of the definition of specific convergence criteria.

source
CounterfactualExplanations.Objectives.ddp_diversity โ€” Method
ddp_diversity(
+)

Default outer constructor for GradientBasedGenerator.

Arguments

  • loss::Union{Nothing,Function}=nothing: The loss function used by the model.
  • penalty::Penalty=nothing: A penalty function for the generator to penalize counterfactuals too far from the original point.
  • ฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing: The weight of the penalty function.
  • latent_space::Bool=false: Whether to use the latent space of a generative model to generate counterfactuals.
  • opt::Flux.Optimise.AbstractOptimiser=Flux.Descent(): The optimizer to use for the generator.
  • generative_model_params::NamedTuple: The parameters of the generative model associated with the generator.

Returns

  • generator::GradientBasedGenerator: A gradient-based counterfactual generator.
source
CounterfactualExplanations.Generators.conditions_satisfied โ€” Method
conditions_satisfied(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)

The default method to check if the all conditions for convergence of the counterfactual search have been satisified for gradient-based generators. By default, gradient-based search is considered to have converged as soon as the proposed feature changes for all features are smaller than one percent of its standard deviation.

source
CounterfactualExplanations.Generators.feature_tweaking! โ€” Method
feature_tweaking!(ce::AbstractCounterfactualExplanation)

Returns a counterfactual instance of ce.x based on the ensemble of classifiers provided.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Returns

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Example

ce = feature_tweaking!(ce) # returns a counterfactual inside the ce.sโ€ฒ field based on the ensemble of classifiers provided

source
CounterfactualExplanations.Generators.hinge_loss โ€” Method
hinge_loss(convergence::AbstractConvergence, ce::AbstractCounterfactualExplanation)

The default hinge loss for any convergence criterion. Can be overridden inside the Convergence module as part of the definition of specific convergence criteria.

source
Flux.Losses.logitbinarycrossentropy โ€” Method
Flux.Losses.logitbinarycrossentropy(ce::AbstractCounterfactualExplanation)

Simply extends the logitbinarycrossentropy method to work with objects of type AbstractCounterfactualExplanation.

source
Flux.Losses.logitcrossentropy โ€” Method
Flux.Losses.logitcrossentropy(ce::AbstractCounterfactualExplanation)

Simply extends the logitcrossentropy method to work with objects of type AbstractCounterfactualExplanation.

source
Flux.Losses.mse โ€” Method
Flux.Losses.mse(ce::AbstractCounterfactualExplanation)

Simply extends the mse method to work with objects of type AbstractCounterfactualExplanation.

source

Internal functions

Flux.Losses.logitbinarycrossentropy โ€” Method
Flux.Losses.logitbinarycrossentropy(ce::AbstractCounterfactualExplanation)

Simply extends the logitbinarycrossentropy method to work with objects of type AbstractCounterfactualExplanation.

source
Flux.Losses.logitcrossentropy โ€” Method
Flux.Losses.logitcrossentropy(ce::AbstractCounterfactualExplanation)

Simply extends the logitcrossentropy method to work with objects of type AbstractCounterfactualExplanation.

source
Flux.Losses.mse โ€” Method
Flux.Losses.mse(ce::AbstractCounterfactualExplanation)

Simply extends the mse method to work with objects of type AbstractCounterfactualExplanation.

source

Internal functions

CounterfactualExplanations.decode_array โ€” Method
decode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)

Helper function to decode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.

source
CounterfactualExplanations.decode_state โ€” Function

function decode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )

Applies all the applicable decoding functions:

  1. If applicable, map the state variable back from the latent space to the feature space.
  2. If and where applicable, inverse-transform features.
  3. Reconstruct all categorical encodings.

Finally, the decoded counterfactual is returned.

source
CounterfactualExplanations.encode_array โ€” Method
encode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)

Helper function to encode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.

source
CounterfactualExplanations.encode_state โ€” Function

function encode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing} = nothing, )

Applies all required encodings to x:

  1. If applicable, it maps x to the latent space learned by the generative model.
  2. If and where applicable, it rescales features.

Finally, it returns the encoded state variable.

source
CounterfactualExplanations.guess_likelihood โ€” Method
guess_likelihood(y::RawOutputArrayType)

Guess the likelihood based on the scientific type of the output array. Returns a symbol indicating the guessed likelihood and the scientific type of the output array.

source
CounterfactualExplanations.initialize_state โ€” Method
initialize_state(ce::CounterfactualExplanation)

Initializes the starting point for the factual(s):

  1. If ce.initialization is set to :identity or counterfactuals are searched in a latent space, then nothing is done.
  2. If ce.initialization is set to :add_perturbation, then a random perturbation is added to the factual following following Slack (2021): https://arxiv.org/abs/2106.02666. The authors show that this improves adversarial robustness.
source
CounterfactualExplanations.decode_array โ€” Method
decode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)

Helper function to decode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.

source
CounterfactualExplanations.decode_state โ€” Function

function decode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )

Applies all the applicable decoding functions:

  1. If applicable, map the state variable back from the latent space to the feature space.
  2. If and where applicable, inverse-transform features.
  3. Reconstruct all categorical encodings.

Finally, the decoded counterfactual is returned.

source
CounterfactualExplanations.encode_array โ€” Method
encode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)

Helper function to encode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.

source
CounterfactualExplanations.encode_state โ€” Function

function encode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing} = nothing, )

Applies all required encodings to x:

  1. If applicable, it maps x to the latent space learned by the generative model.
  2. If and where applicable, it rescales features.

Finally, it returns the encoded state variable.

source
CounterfactualExplanations.guess_likelihood โ€” Method
guess_likelihood(y::RawOutputArrayType)

Guess the likelihood based on the scientific type of the output array. Returns a symbol indicating the guessed likelihood and the scientific type of the output array.

source
CounterfactualExplanations.initialize_state โ€” Method
initialize_state(ce::CounterfactualExplanation)

Initializes the starting point for the factual(s):

  1. If ce.initialization is set to :identity or counterfactuals are searched in a latent space, then nothing is done.
  2. If ce.initialization is set to :add_perturbation, then a random perturbation is added to the factual following following Slack (2021): https://arxiv.org/abs/2106.02666. The authors show that this improves adversarial robustness.
source
CounterfactualExplanations.map_to_latent โ€” Function

function maptolatent( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )

Maps x from the feature space $\mathcal{X}$ to the latent space learned by the generative model.

source
Base.vcat โ€” Method
Base.vcat(bmk1::Benchmark, bmk2::Benchmark)

Vertically concatenates two Benchmark objects.

source
CounterfactualExplanations.Evaluation.compute_measure โ€” Method
compute_measure(ce::CounterfactualExplanation, measure::Function, agg::Function)

Computes a single measure for a counterfactual explanation. The measure is applied to the counterfactual explanation ce and aggregated using the aggregation function agg.

source
CounterfactualExplanations.map_to_latent โ€” Function

function maptolatent( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )

Maps x from the feature space $\mathcal{X}$ to the latent space learned by the generative model.

source
Base.vcat โ€” Method
Base.vcat(bmk1::Benchmark, bmk2::Benchmark)

Vertically concatenates two Benchmark objects.

source
CounterfactualExplanations.Evaluation.compute_measure โ€” Method
compute_measure(ce::CounterfactualExplanation, measure::Function, agg::Function)

Computes a single measure for a counterfactual explanation. The measure is applied to the counterfactual explanation ce and aggregated using the aggregation function agg.

source
CounterfactualExplanations.Evaluation.to_dataframe โ€” Method
evaluate_dataframe(
     ce::CounterfactualExplanation,
     measure::Vector{Function},
     agg::Function,
     report_each::Bool,
     pivot_longer::Bool,
     store_ce::Bool,
-)

Evaluates a counterfactual explanation and returns a dataframe of evaluation measures.

source
CounterfactualExplanations.DataPreprocessing.convert_to_1d โ€” Method
convert_to_1d(y::Matrix, y_levels::AbstractArray)

Helper function to convert a one-hot encoded matrix to a vector of labels. This is necessary because MLJ models require the labels to be represented as a vector, but the synthetic datasets in this package hold the labels in one-hot encoded form.

Arguments

  • y::Matrix: The one-hot encoded matrix.
  • y_levels::AbstractArray: The levels of the categorical variable.

Returns

  • labels: A vector of labels.
source
CounterfactualExplanations.DataPreprocessing.get_generative_model โ€” Method
get_generative_model(counterfactual_data::CounterfactualData)

Returns the underlying generative model. If there is no existing model available, the default generative model (VAE) is used. Otherwise it is expected that existing generative model has been pre-trained or else a warning is triggered.

source
CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj โ€” Method
preprocess_data_for_mlj(data::CounterfactualData)

Helper function to preprocess data::CounterfactualData for MLJ models.

Arguments

  • data::CounterfactualData: The data to be preprocessed.

Returns

  • (df_x, y): A tuple containing the preprocessed data, with df_x being a DataFrame object and y being a categorical vector.

Example

X, y = preprocessdatafor_mlj(data)

source
CounterfactualExplanations.DataPreprocessing.train_test_split โ€” Method
train_test_split(data::CounterfactualData;test_size=0.2,keep_class_ratio=false)

Splits data into train and test split.

Arguments

  • data::CounterfactualData: The data to be preprocessed.
  • test_size=0.2: Proportion of the data to be used for testing.
  • keep_class_ratio=false: Decides whether to sample equally from each class, or keep their relative size.

Returns

  • (train_data::CounterfactualData, test_data::CounterfactualData): A tuple containing the train and test splits.

Example

train, test = traintestsplit(data, testsize=0.1, keepclass_ratio=true)

source
CounterfactualExplanations.Models.TreeModel โ€” Type
TreeModel <: AbstractNonDifferentiableJuliaModel

Constructor for tree-based models from the MLJ library.

Arguments

  • model::Any: The model selected by the user. Must be a model from the MLJ library.
  • likelihood::Symbol: The likelihood of the model. Must be one of [:classification_binary, :classification_multi].

Returns

  • TreeModel: A tree-based model from the MLJ library wrapped inside the TreeModel class.
source
CounterfactualExplanations.Models.get_individual_classifiers โ€” Method
get_individual_classifiers(M::TreeModel)

Returns the individual classifiers in the forest. If the input is a decision tree, the method returns the decision tree itself inside an array.

Arguments

  • M::TreeModel: The model selected by the user.

Returns

  • classifiers::AbstractArray: An array of individual classifiers in the forest.

Example

classifiers = Models.getindividualclassifiers(M) # returns the individual classifiers in the forest

source
CounterfactualExplanations.Models.train โ€” Method
train(M::TreeModel, data::CounterfactualData; kwargs...)

Fits the model M to the data in the CounterfactualData object. This method is not called by the user directly.

Arguments

  • M::TreeModel: The wrapper for a TreeModel.
  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • M::TreeModel: The fitted TreeModel.
source
CounterfactualExplanations.GenerativeModels.VAE โ€” Type
VAE <: AbstractGenerativeModel

Constructs the Variational Autoencoder. The VAE is a subtype of AbstractGenerativeModel. Any (sub-)type of AbstractGenerativeModel is accepted by latent space generators.

source
Base.rand โ€” Function

Random.rand(encoder::Encoder, x, device=cpu)

Draws random samples from the latent distribution.

source
CounterfactualExplanations.Generators.calculate_delta โ€” Method
calculate_delta(ce::AbstractCounterfactualExplanation, penalty::Vector{Function})

Calculates the penalty for the proposed feature tweak.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Returns

  • delta::Float64: The calculated penalty for the proposed feature tweak.
source
CounterfactualExplanations.Generators.esatisfactory_instance โ€” Method
esatisfactory_instance(generator::FeatureTweakGenerator, x::AbstractArray, paths::Dict{String, Dict{String, Any}})

Returns an epsilon-satisfactory counterfactual for x based on the paths provided.

Arguments

  • generator::FeatureTweakGenerator: The feature tweak generator.
  • x::AbstractArray: The factual instance.
  • paths::Dict{String, Dict{String, Any}}: A list of paths to the leaves of the tree to be used for tweaking the feature.

Returns

  • esatisfactory::AbstractArray: The epsilon-satisfactory instance.

Example

esatisfactory = esatisfactory_instance(generator, x, paths) # returns an epsilon-satisfactory counterfactual for x based on the paths provided

source
CounterfactualExplanations.Generators.feature_selection! โ€” Method
feature_selection!(ce::AbstractCounterfactualExplanation)

Perform feature selection to find the dimension with the closest (but not equal) values between the ce.x (factual) and ce.sโ€ฒ (counterfactual) arrays.

Arguments

  • ce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.

Returns

  • nothing

The function iteratively modifies the ce.sโ€ฒ counterfactual array by updating its elements to match the corresponding elements in the ce.x factual array, one dimension at a time, until the predicted label of the modified ce.sโ€ฒ matches the predicted label of the ce.x array.

source
CounterfactualExplanations.Generators.find_closest_dimension โ€” Method
find_closest_dimension(factual, counterfactual)

Find the dimension with the closest (but not equal) values between the factual and counterfactual arrays.

Arguments

  • factual: The factual array.
  • counterfactual: The counterfactual array.

Returns

  • closest_dimension: The index of the dimension with the closest values.

The function iterates over the indices of the factual array and calculates the absolute difference between the corresponding elements in the factual and counterfactual arrays. It returns the index of the dimension with the smallest difference, excluding dimensions where the values in factual and counterfactual are equal.

source
CounterfactualExplanations.Generators.find_counterfactual โ€” Method
find_counterfactual(model, factual_class, counterfactual_data, counterfactual_candidates)

Find the first counterfactual index by predicting labels.

Arguments

  • model: The fitted model used for prediction.
  • target_class: Expected target class.
  • counterfactual_data: Data required for counterfactual generation.
  • counterfactual_candidates: The array of counterfactual candidates.

Returns

  • counterfactual: The index of the first counterfactual found.
source
CounterfactualExplanations.Generators.growing_spheres_generation! โ€” Method
growing_spheres_generation(ce::AbstractCounterfactualExplanation)

Generate counterfactual candidates using the growing spheres generation algorithm.

Arguments

  • ce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.

Returns

  • nothing

This function applies the growing spheres generation algorithm to generate counterfactual candidates. It starts by generating random points uniformly on a sphere, gradually reducing the search space until no counterfactuals are found. Then it expands the search space until at least one counterfactual is found or the maximum number of iterations is reached.

The algorithm iteratively generates counterfactual candidates and predicts their labels using the model stored in ce.M. It checks if any of the predicted labels are different from the factual class. The process of reducing the search space involves halving the search radius, while the process of expanding the search space involves increasing the search radius.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Function, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided with additional keyword arguments.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided with additional keyword arguments.

source
CounterfactualExplanations.Generators.hyper_sphere_coordinates โ€” Method
hyper_sphere_coordinates(n_search_samples::Int, instance::Vector{Float64}, low::Int, high::Int; p_norm::Int=2)

Generates candidate counterfactuals using the growing spheres method based on hyper-sphere coordinates.

The implementation follows the Random Point Picking over a sphere algorithm described in the paper: "Learning Counterfactual Explanations for Tabular Data" by Pawelczyk, Broelemann & Kascneci (2020), presented at The Web Conference 2020 (WWW). It ensures that points are sampled uniformly at random using insights from: http://mathworld.wolfram.com/HyperspherePointPicking.html

The growing spheres method is originally proposed in the paper: "Comparison-based Inverse Classification for Interpretability in Machine Learning" by Thibaut Laugel et al (2018), presented at the International Conference on Information Processing and Management of Uncertainty in Knowledge-Based Systems (2018).

Arguments

  • n_search_samples::Int: The number of search samples (int > 0).
  • instance::AbstractArray: The input point array.
  • low::AbstractFloat: The lower bound (float >= 0, l < h).
  • high::AbstractFloat: The upper bound (float >= 0, h > l).
  • p_norm::Integer: The norm parameter (int >= 1).

Returns

  • candidate_counterfactuals::Array: An array of candidate counterfactuals.
source
CounterfactualExplanations.Generators.search_path โ€” Function
search_path(tree::Union{DecisionTree.Leaf, DecisionTree.Node}, target::RawTargetType, path::AbstractArray)

Return a path index list with the inequality symbols, thresholds and feature indices.

Arguments

  • tree::Union{DecisionTree.Leaf, DecisionTree.Node}: The root node of a decision tree.
  • target::RawTargetType: The target class.
  • path::AbstractArray: A list containing the paths found thus far.

Returns

  • paths::AbstractArray: A list of paths to the leaves of the tree to be used for tweaking the feature.

Example

paths = search_path(tree, target) # returns a list of paths to the leaves of the tree to be used for tweaking the feature

source
CounterfactualExplanations.Generators.โˆ‚h โ€” Method
โˆ‚h(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the complexity penalty at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.

If the penalty is not provided, it returns 0.0. By default, Zygote never works out the gradient for constants and instead returns 'nothing', so we need to add a manual step to override this behaviour. See here: https://discourse.julialang.org/t/zygote-gradient/26715.

source
CounterfactualExplanations.Generators.โˆ‚โ„“ โ€” Method
โˆ‚โ„“(generator::AbstractGradientBasedGenerator, M::Union{Models.LogisticModel, Models.BayesianLogisticModel}, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the loss function at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.

source
CounterfactualExplanations.Generators.โˆ‡ โ€” Method
โˆ‡(generator::AbstractGradientBasedGenerator, M::Models.AbstractDifferentiableModel, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the counterfactual search objective for gradient-based generators. It simply computes the weighted sum over partial derivates. It assumes that Zygote.jl has gradient access. If the counterfactual is being generated using Probe, the hinge loss is added to the gradient.

source
CounterfactualExplanations.DataPreprocessing.convert_to_1d โ€” Method
convert_to_1d(y::Matrix, y_levels::AbstractArray)

Helper function to convert a one-hot encoded matrix to a vector of labels. This is necessary because MLJ models require the labels to be represented as a vector, but the synthetic datasets in this package hold the labels in one-hot encoded form.

Arguments

  • y::Matrix: The one-hot encoded matrix.
  • y_levels::AbstractArray: The levels of the categorical variable.

Returns

  • labels: A vector of labels.
source
CounterfactualExplanations.DataPreprocessing.get_generative_model โ€” Method
get_generative_model(counterfactual_data::CounterfactualData)

Returns the underlying generative model. If there is no existing model available, the default generative model (VAE) is used. Otherwise it is expected that existing generative model has been pre-trained or else a warning is triggered.

source
CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj โ€” Method
preprocess_data_for_mlj(data::CounterfactualData)

Helper function to preprocess data::CounterfactualData for MLJ models.

Arguments

  • data::CounterfactualData: The data to be preprocessed.

Returns

  • (df_x, y): A tuple containing the preprocessed data, with df_x being a DataFrame object and y being a categorical vector.

Example

X, y = preprocessdatafor_mlj(data)

source
CounterfactualExplanations.DataPreprocessing.train_test_split โ€” Method
train_test_split(data::CounterfactualData;test_size=0.2,keep_class_ratio=false)

Splits data into train and test split.

Arguments

  • data::CounterfactualData: The data to be preprocessed.
  • test_size=0.2: Proportion of the data to be used for testing.
  • keep_class_ratio=false: Decides whether to sample equally from each class, or keep their relative size.

Returns

  • (train_data::CounterfactualData, test_data::CounterfactualData): A tuple containing the train and test splits.

Example

train, test = traintestsplit(data, testsize=0.1, keepclass_ratio=true)

source
CounterfactualExplanations.Models.TreeModel โ€” Type
TreeModel <: AbstractNonDifferentiableJuliaModel

Constructor for tree-based models from the MLJ library.

Arguments

  • model::Any: The model selected by the user. Must be a model from the MLJ library.
  • likelihood::Symbol: The likelihood of the model. Must be one of [:classification_binary, :classification_multi].

Returns

  • TreeModel: A tree-based model from the MLJ library wrapped inside the TreeModel class.
source
CounterfactualExplanations.Models.get_individual_classifiers โ€” Method
get_individual_classifiers(M::TreeModel)

Returns the individual classifiers in the forest. If the input is a decision tree, the method returns the decision tree itself inside an array.

Arguments

  • M::TreeModel: The model selected by the user.

Returns

  • classifiers::AbstractArray: An array of individual classifiers in the forest.

Example

classifiers = Models.getindividualclassifiers(M) # returns the individual classifiers in the forest

source
CounterfactualExplanations.Models.train โ€” Method
train(M::TreeModel, data::CounterfactualData; kwargs...)

Fits the model M to the data in the CounterfactualData object. This method is not called by the user directly.

Arguments

  • M::TreeModel: The wrapper for a TreeModel.
  • data::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.

Returns

  • M::TreeModel: The fitted TreeModel.
source
CounterfactualExplanations.GenerativeModels.VAE โ€” Type
VAE <: AbstractGenerativeModel

Constructs the Variational Autoencoder. The VAE is a subtype of AbstractGenerativeModel. Any (sub-)type of AbstractGenerativeModel is accepted by latent space generators.

source
Base.rand โ€” Function

Random.rand(encoder::Encoder, x, device=cpu)

Draws random samples from the latent distribution.

source
CounterfactualExplanations.Generators.calculate_delta โ€” Method
calculate_delta(ce::AbstractCounterfactualExplanation, penalty::Vector{Function})

Calculates the penalty for the proposed feature tweak.

Arguments

  • ce::AbstractCounterfactualExplanation: The counterfactual explanation object.

Returns

  • delta::Float64: The calculated penalty for the proposed feature tweak.
source
CounterfactualExplanations.Generators.esatisfactory_instance โ€” Method
esatisfactory_instance(generator::FeatureTweakGenerator, x::AbstractArray, paths::Dict{String, Dict{String, Any}})

Returns an epsilon-satisfactory counterfactual for x based on the paths provided.

Arguments

  • generator::FeatureTweakGenerator: The feature tweak generator.
  • x::AbstractArray: The factual instance.
  • paths::Dict{String, Dict{String, Any}}: A list of paths to the leaves of the tree to be used for tweaking the feature.

Returns

  • esatisfactory::AbstractArray: The epsilon-satisfactory instance.

Example

esatisfactory = esatisfactory_instance(generator, x, paths) # returns an epsilon-satisfactory counterfactual for x based on the paths provided

source
CounterfactualExplanations.Generators.feature_selection! โ€” Method
feature_selection!(ce::AbstractCounterfactualExplanation)

Perform feature selection to find the dimension with the closest (but not equal) values between the ce.x (factual) and ce.sโ€ฒ (counterfactual) arrays.

Arguments

  • ce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.

Returns

  • nothing

The function iteratively modifies the ce.sโ€ฒ counterfactual array by updating its elements to match the corresponding elements in the ce.x factual array, one dimension at a time, until the predicted label of the modified ce.sโ€ฒ matches the predicted label of the ce.x array.

source
CounterfactualExplanations.Generators.find_closest_dimension โ€” Method
find_closest_dimension(factual, counterfactual)

Find the dimension with the closest (but not equal) values between the factual and counterfactual arrays.

Arguments

  • factual: The factual array.
  • counterfactual: The counterfactual array.

Returns

  • closest_dimension: The index of the dimension with the closest values.

The function iterates over the indices of the factual array and calculates the absolute difference between the corresponding elements in the factual and counterfactual arrays. It returns the index of the dimension with the smallest difference, excluding dimensions where the values in factual and counterfactual are equal.

source
CounterfactualExplanations.Generators.find_counterfactual โ€” Method
find_counterfactual(model, factual_class, counterfactual_data, counterfactual_candidates)

Find the first counterfactual index by predicting labels.

Arguments

  • model: The fitted model used for prediction.
  • target_class: Expected target class.
  • counterfactual_data: Data required for counterfactual generation.
  • counterfactual_candidates: The array of counterfactual candidates.

Returns

  • counterfactual: The index of the first counterfactual found.
source
CounterfactualExplanations.Generators.growing_spheres_generation! โ€” Method
growing_spheres_generation(ce::AbstractCounterfactualExplanation)

Generate counterfactual candidates using the growing spheres generation algorithm.

Arguments

  • ce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.

Returns

  • nothing

This function applies the growing spheres generation algorithm to generate counterfactual candidates. It starts by generating random points uniformly on a sphere, gradually reducing the search space until no counterfactuals are found. Then it expands the search space until at least one counterfactual is found or the maximum number of iterations is reached.

The algorithm iteratively generates counterfactual candidates and predicts their labels using the model stored in ce.M. It checks if any of the predicted labels are different from the factual class. The process of reducing the search space involves halving the search radius, while the process of expanding the search space involves increasing the search radius.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Function, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided with additional keyword arguments.

source
CounterfactualExplanations.Generators.h โ€” Method
h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)

Overloads the h function for the case where a single penalty function is provided with additional keyword arguments.

source
CounterfactualExplanations.Generators.hyper_sphere_coordinates โ€” Method
hyper_sphere_coordinates(n_search_samples::Int, instance::Vector{Float64}, low::Int, high::Int; p_norm::Int=2)

Generates candidate counterfactuals using the growing spheres method based on hyper-sphere coordinates.

The implementation follows the Random Point Picking over a sphere algorithm described in the paper: "Learning Counterfactual Explanations for Tabular Data" by Pawelczyk, Broelemann & Kascneci (2020), presented at The Web Conference 2020 (WWW). It ensures that points are sampled uniformly at random using insights from: http://mathworld.wolfram.com/HyperspherePointPicking.html

The growing spheres method is originally proposed in the paper: "Comparison-based Inverse Classification for Interpretability in Machine Learning" by Thibaut Laugel et al (2018), presented at the International Conference on Information Processing and Management of Uncertainty in Knowledge-Based Systems (2018).

Arguments

  • n_search_samples::Int: The number of search samples (int > 0).
  • instance::AbstractArray: The input point array.
  • low::AbstractFloat: The lower bound (float >= 0, l < h).
  • high::AbstractFloat: The upper bound (float >= 0, h > l).
  • p_norm::Integer: The norm parameter (int >= 1).

Returns

  • candidate_counterfactuals::Array: An array of candidate counterfactuals.
source
CounterfactualExplanations.Generators.search_path โ€” Function
search_path(tree::Union{DecisionTree.Leaf, DecisionTree.Node}, target::RawTargetType, path::AbstractArray)

Return a path index list with the inequality symbols, thresholds and feature indices.

Arguments

  • tree::Union{DecisionTree.Leaf, DecisionTree.Node}: The root node of a decision tree.
  • target::RawTargetType: The target class.
  • path::AbstractArray: A list containing the paths found thus far.

Returns

  • paths::AbstractArray: A list of paths to the leaves of the tree to be used for tweaking the feature.

Example

paths = search_path(tree, target) # returns a list of paths to the leaves of the tree to be used for tweaking the feature

source
CounterfactualExplanations.Generators.โˆ‚h โ€” Method
โˆ‚h(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the complexity penalty at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.

If the penalty is not provided, it returns 0.0. By default, Zygote never works out the gradient for constants and instead returns 'nothing', so we need to add a manual step to override this behaviour. See here: https://discourse.julialang.org/t/zygote-gradient/26715.

source
CounterfactualExplanations.Generators.โˆ‚โ„“ โ€” Method
โˆ‚โ„“(generator::AbstractGradientBasedGenerator, M::Union{Models.LogisticModel, Models.BayesianLogisticModel}, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the loss function at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.

source
CounterfactualExplanations.Generators.โˆ‡ โ€” Method
โˆ‡(generator::AbstractGradientBasedGenerator, M::Models.AbstractDifferentiableModel, ce::AbstractCounterfactualExplanation)

The default method to compute the gradient of the counterfactual search objective for gradient-based generators. It simply computes the weighted sum over partial derivates. It assumes that Zygote.jl has gradient access. If the counterfactual is being generated using Probe, the hinge loss is added to the gradient.

source
+)

Additional penalty for ClaPROARGenerator.

source
diff --git a/dev/release-notes/index.html b/dev/release-notes/index.html new file mode 100644 index 000000000..db681924c --- /dev/null +++ b/dev/release-notes/index.html @@ -0,0 +1,2 @@ + +Release Notes ยท CounterfactualExplanations.jl

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Note: We try to adhere to these practices as of version 1.1.1.

[Unreleased]

Added

  • Adds a section on Convergence to the documentation, Changelog.jl functionality and a few doc tests. #429

[1.1.2] - 2024-04-16

Changed

  • Replaces the GIF in the README and introduction of docs for a static image.

[1.1.1] - 2024-04-15

Added

  • Added tests for LaplaceRedux extension. Bumped upper compat bound for LaplaceRedux.jl. #428
diff --git a/dev/search_index.js b/dev/search_index.js index 754d24736..9cefba0c5 100644 --- a/dev/search_index.js +++ b/dev/search_index.js @@ -1,3 +1,3 @@ var documenterSearchIndex = {"docs": -[{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/models/#Handling-Models","page":"Handling Models","title":"Handling Models","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The typical use-case for Counterfactual Explanations and Algorithmic Recourse is as follows: users have trained some supervised model that is not inherently interpretable and are looking for a way to explain it. In this tutorial, we will see how pre-trained models can be used with this package.","category":"page"},{"location":"tutorials/models/#Models-trained-in-Flux.jl","page":"Handling Models","title":"Models trained in Flux.jl","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"We will train a simple binary classifier in Flux.jl on the popular Moons dataset:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"n = 500\ndata = TaijaData.load_moons(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nX = counterfactual_data.X\ny = counterfactual_data.y\nplt = plot()\nscatter!(counterfactual_data)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"(Image: )","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The following code chunk sets up a Deep Neural Network for the task at hand:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"data = Flux.DataLoader((X,y),batchsize=1)\ninput_dim = size(X,1)\nn_hidden = 32\nactivation = relu\noutput_dim = 2\nnn = Chain(\n Dense(input_dim, n_hidden, activation),\n Dropout(0.1),\n Dense(n_hidden, output_dim)\n)\nloss(yhat, y) = Flux.Losses.logitcrossentropy(nn(yhat), y)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Next, we fit the network to the data:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"using Flux.Optimise: update!, Adam\nopt = Adam()\nepochs = 100\navg_loss(data) = mean(map(d -> loss(d[1],d[2]), data))\nshow_every = epochs/5\n# Training:\nfor epoch = 1:epochs\n for d in data\n gs = gradient(Flux.params(nn)) do\n l = loss(d...)\n end\n update!(opt, Flux.params(nn), gs)\n end\n if epoch % show_every == 0\n println(\"Epoch \" * string(epoch))\n @show avg_loss(data)\n end\nend","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Epoch 20\navg_loss(data) = 0.1407434f0\nEpoch 40\navg_loss(data) = 0.11345118f0\nEpoch 60\navg_loss(data) = 0.046319224f0\nEpoch 80\navg_loss(data) = 0.011847609f0\nEpoch 100\navg_loss(data) = 0.007242911f0","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"To prepare the fitted model for use with our package, we need to wrap it inside a container. For plain-vanilla models trained in Flux.jl, the corresponding constructor is called FluxModel. There is also a separate constructor called FluxEnsemble, which applies to Deep Ensembles. Deep Ensembles are a popular approach to approximate Bayesian Deep Learning and have been shown to generate good predictive uncertainty estimates (Lakshminarayanan, Pritzel, and Blundell 2016).","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The appropriate API call to wrap our simple network in a container follows below:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"M = FluxModel(nn)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"FluxModel(Chain(Dense(2 => 32, relu), Dropout(0.1, active=false), Dense(32 => 2)), :classification_binary)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The likelihood function of the output variable is automatically inferred from the data. The generic plot() method can be called on the model and data to visualise the results:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"plot(M, counterfactual_data)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"(Image: )","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Our model M is now ready for use with the package.","category":"page"},{"location":"tutorials/models/#References","page":"Handling Models","title":"References","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/dice/#DiCEGenerator","page":"DiCE","title":"DiCEGenerator","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The DiCEGenerator can be used to generate multiple diverse counterfactuals for a single factual.","category":"page"},{"location":"explanation/generators/dice/#Description","page":"DiCE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Counterfactual Explanations are not unique and there are therefore many different ways through which valid counterfactuals can be generated. In the context of Algorithmic Recourse this can be leveraged to offer individuals not one, but possibly many different ways to change a negative outcome into a positive one. One might argue that it makes sense for those different options to be as diverse as possible. This idea is at the core of DiCE, a counterfactual generator introduce by Mothilal, Sharma, and Tan (2020) that generate a diverse set of counterfactual explanations.","category":"page"},{"location":"explanation/generators/dice/#Defining-Diversity","page":"DiCE","title":"Defining Diversity","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"To ensure that the generated counterfactuals are diverse, Mothilal, Sharma, and Tan (2020) add a diversity constraint to the counterfactual search objective. In particular, diversity is explicitly proxied via Determinantal Point Processes (DDP).","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"We can implement DDP in Julia as follows:[1]","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"using LinearAlgebra\nfunction ddp_diversity(X::AbstractArray{<:Real, 3})\n xs = eachslice(X, dims = ndims(X))\n K = [1/(1 + norm(x .- y)) for x in xs, y in xs]\n return det(K)\nend","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Below we generate some random points in mathbbR^2 and apply gradient ascent on this function evaluated at the whole array of points. As we can see in the animation below, the points are sent away from each other. In other words, diversity across the array of points increases as we ascend the ddp_diversity function.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"lims = 5\nN = 5\nX = rand(2,1,N)\nT = 50\nฮท = 0.1\nanim = @animate for t in 1:T\n X .+= gradient(ddp_diversity, X)[1]\n Z = reshape(X,2,N)\n scatter(\n Z[1,:],Z[2,:],ms=25, \n xlims=(-lims,lims),ylims=(-lims,lims),\n label=\"\",colour=1:N,\n size=(500,500),\n title=\"Diverse Counterfactuals\"\n )\nend\ngif(anim, joinpath(www_path, \"dice_intro.gif\"))","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#Usage","page":"DiCE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"generator = DiCEGenerator()\nconv = CounterfactualExplanations.Convergence.GeneratorConditionsConvergence()\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=5, convergence=conv\n)\nplot(ce)","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#Effect-of-Penalty","page":"DiCE","title":"Effect of Penalty","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"ฮ›โ‚‚ = [0.1, 1.0, 5.0]\nces = []\nn_cf = 5\nusing Flux\nfor ฮปโ‚‚ โˆˆ ฮ›โ‚‚ \n ฮป = [0.00, ฮปโ‚‚]\n generator = DiCEGenerator(ฮป=ฮป)\n ces = vcat(\n ces...,\n generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=n_cf, convergence=conv\n )\n )\nend","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The figure below shows the resulting counterfactual paths. As expected, the resulting counterfactuals are more dispersed across the feature domain for higher choices of lambda_2","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#References","page":"DiCE","title":"References","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"[1] With thanks to the respondents on Discourse","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"how_to_guides/#How-To-Guides","page":"Overview","title":"How-To Guides","text":"","category":"section"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"In this section, you will find a series of how-to-guides that showcase specific use cases of counterfactual explanations (CE).","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"How-to guides are directions that take the reader through the steps required to solve a real-world problem. How-to guides are goal-oriented.โ€” Diรกtaxis","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"In other words, you come here because you may have some particular problem in mind, would like to see how it can be solved using CE and then most likely head off again ๐Ÿซก.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/generators/#Handling-Generators","page":"Handling Generators","title":"Handling Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Generating Counterfactual Explanations can be seen as a generative modelling task because it involves generating samples in the input space: x sim mathcalX. In this tutorial, we will introduce how Counterfactual GradientBasedGenerators are used. They are discussed in more detail in the explanatory section of the documentation.","category":"page"},{"location":"tutorials/generators/#Composable-Generators","page":"Handling Generators","title":"Composable Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"warning: Breaking Changes Expected\nWork on this feature is still in its very early stages and breaking changes should be expected. ","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"One of the key objectives for this package is Composability. It turns out that many of the various counterfactual generators that have been proposed in the literature, essentially do the same thing: they optimize an objective function. Formally we have,","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"\nbeginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS left textyloss(M(f(mathbfs^prime))y^*)+ lambda textcost(f(mathbfs^prime)) right \nendaligned \n qquad(1)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"where textyloss denotes the main loss function and textcost is a penalty term (Altmeyer et al. 2023).","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Without going into further detail here, the important thing to mention is that Equationย 1 very closely describes how counterfactual search is actually implemented in the package. In other words, all off-the-shelf generators currently implemented work with that same objective. They just vary in the way that penalties are defined, for example. This gives rise to an interesting idea:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Why not compose generators that combine ideas from different off-the-shelf generators?","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"The GradientBasedGenerator class provides a straightforward way to do this, without requiring users to build custom GradientBasedGenerators from scratch. It can be instantiated as follows:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"generator = GradientBasedGenerator()","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"By default, this creates a generator that simply performs gradient descent without any penalties. To modify the behaviour of the generator, you can define the counterfactual search objective function using the @objective macro:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"@objective(generator, logitbinarycrossentropy + 0.1distance_l2 + 1.0ddp_diversity)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Here we have essentially created a version of the DiCEGenerator:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"ce = generate_counterfactual(x, target, counterfactual_data, M, generator; num_counterfactuals=5)\nplot(ce)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Multiple macros can be chained using Chains.jl making it easy to create entirely new flavours of counterfactual generators. The following generator, for example, combines ideas from DiCE (Mothilal, Sharma, and Tan 2020) and REVISE (Joshi et al. 2019):","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"@chain generator begin\n @objective logitcrossentropy + 1.0ddp_diversity # DiCE (Mothilal et al. 2020)\n @with_optimiser Flux.Adam(0.1) \n @search_latent_space # REVISE (Joshi et al. 2019)\nend","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Letโ€™s take this generator to our MNIST dataset and generate a counterfactual explanation for turning a 0 into a 8.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/#Off-the-Shelf-Generators","page":"Handling Generators","title":"Off-the-Shelf Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Off-the-shelf generators are just default recipes for counterfactual generators. Currently, the following off-the-shelf counterfactual generators are implemented in the package:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"generator_catalogue","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Dict{Symbol, Any} with 11 entries:\n :gravitational => GravitationalGenerator\n :growing_spheres => GrowingSpheresGenerator\n :revise => REVISEGenerator\n :clue => CLUEGenerator\n :probe => ProbeGenerator\n :dice => DiCEGenerator\n :feature_tweak => FeatureTweakGenerator\n :claproar => ClaPROARGenerator\n :wachter => WachterGenerator\n :generic => GenericGenerator\n :greedy => GreedyGenerator","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"To specify the type of generator you want to use, you can simply instantiate it:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"# Search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"We generally make an effort to follow the literature as closely as possible when implementing off-the-shelf generators.","category":"page"},{"location":"tutorials/generators/#References","page":"Handling Generators","title":"References","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/simple_example/#Simple-Example","page":"Simple Example","title":"Simple Example","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"In this tutorial, we will go through a simple example involving synthetic data and a generic counterfactual generator.","category":"page"},{"location":"tutorials/simple_example/#Data-and-Classifier","page":"Simple Example","title":"Data and Classifier","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Below we generate some linearly separable data and fit a simple MLP classifier with batch normalization to it. For more information on generating data and models, refer to the Handling Data and Handling Models tutorials respectively.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"# Counteractual data and model:\nflux_training_params.batchsize = 10\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ncounterfactual_data.standardize = true\nM = fit_model(counterfactual_data, :MLP, batch_norm=true)","category":"page"},{"location":"tutorials/simple_example/#Counterfactual-Search","page":"Simple Example","title":"Counterfactual Search","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Next, determine a target and factual class for our counterfactual search and select a random factual instance to explain.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"target = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Finally, we generate and visualize the generated counterfactual:","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"# Search:\ngenerator = WachterGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"(Image: )","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/optimisers/jsma/#Jacobian-based-Saliency-Map-Attack","page":"JSMA","title":"Jacobian-based Saliency Map Attack","text":"","category":"section"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"To search counterfactuals, Schut et al. (2021) propose to use a Jacobian-Based Saliency Map Attack (JSMA) inspired by the literature on adversarial attacks. It works by moving in the direction of the most salient feature at a fixed step size in each iteration. Schut et al. (2021) use this optimisation rule in the context of Bayesian classifiers and demonstrate good results in terms of plausibility โ€” how realistic counterfactuals are โ€” and redundancy โ€” how sparse the proposed feature changes are.","category":"page"},{"location":"explanation/optimisers/jsma/#JSMADescent","page":"JSMA","title":"JSMADescent","text":"","category":"section"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"To implement this approach in a reusable manner, we have added JSMA as a Flux optimiser. In particular, we have added a class JSMADescent<:Flux.Optimise.AbstractOptimiser, for which we have overloaded the Flux.Optimise.apply! method. This makes it possible to reuse JSMADescent as an optimiser in composable generators.","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"The optimiser can be used with with any generator as follows:","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"using CounterfactualExplanations.Generators: JSMADescent\ngenerator = GenericGenerator() |>\n gen -> @with_optimiser(gen,JSMADescent(;ฮท=0.1))\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"The figure below compares the resulting counterfactual search outcome to the corresponding outcome with generic Descent.","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"plot(p1,p2,size=(1000,400))","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"(Image: )","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/data_catalogue/#Data-Catalogue","page":"Data Catalogue","title":"Data Catalogue","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"To allow researchers and practitioners to test and compare counterfactual generators, the TAIJA environment includes the package TaijaData.jl which comes with pre-processed synthetic and real-world benchmark datasets from different domains. This page explains how to use TaijaData.jl in tandem with CounterfactualExplanations.jl.","category":"page"},{"location":"tutorials/data_catalogue/#Synthetic-Data","page":"Data Catalogue","title":"Synthetic Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The following dictionary can be used to inspect the available methods to generate synthetic datasets where the key indicates the name of the data and the value is the corresponding method:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:synthetic]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 6 entries:\n :overlapping => load_overlapping\n :linearly_separable => load_linearly_separable\n :blobs => load_blobs\n :moons => load_moons\n :circles => load_circles\n :multi_class => load_multi_class","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The chart below shows the generated data using default parameters:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"plts = []\n_height = 200\n_n = length(keys(data_catalogue[:synthetic]))\nfor (key, fun) in data_catalogue[:synthetic]\n data = fun()\n counterfactual_data = DataPreprocessing.CounterfactualData(data...)\n plt = plot()\n scatter!(counterfactual_data, title=key)\n plts = [plts..., plt]\nend\nplot(plts..., size=(_n * _height, _height), layout=(1, _n))","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/data_catalogue/#Real-World-Data","page":"Data Catalogue","title":"Real-World Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"As for real-world data, the same dictionary can be used to inspect the available data from different domains.","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:tabular]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 5 entries:\n :german_credit => load_german_credit\n :california_housing => load_california_housing\n :credit_default => load_credit_default\n :adult => load_uci_adult\n :gmsc => load_gmsc","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:vision]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 3 entries:\n :fashion_mnist => load_fashion_mnist\n :mnist => load_mnist\n :cifar_10 => load_cifar_10","category":"page"},{"location":"tutorials/data_catalogue/#Loading-Data","page":"Data Catalogue","title":"Loading Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"To load or generate any of the datasets listed above, you can just use the corresponding method, for example:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"data = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Optionally, you can specify how many samples you want to generate like so:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"n = 100\ndata = TaijaData.load_overlapping(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"This also applies to real-world datasets, which by default are loaded in their entirety. If n is supplied, the dataset will be randomly undersampled:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"data = TaijaData.load_mnist(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The undersampled dataset is automatically balanced:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"sum(counterfactual_data.y; dims=2)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"10ร—1 Matrix{Int64}:\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"We can also use a helper function to split the data into train and test sets:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"train_data, test_data = \n CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"how_to_guides/custom_generators/#How-to-add-Custom-Generators","page":"... add custom generators","title":"How to add Custom Generators","text":"","category":"section"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"As we will see in this short tutorial, building custom counterfactual generators is straightforward. We hope that this will facilitate contributions through the community.","category":"page"},{"location":"how_to_guides/custom_generators/#Generic-generator-with-dropout","page":"... add custom generators","title":"Generic generator with dropout","text":"","category":"section"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"To illustrate how custom generators can be implemented we will consider a simple example of a generator that extends the functionality of our GenericGenerator. We have noted elsewhere that the effectiveness of counterfactual explanations depends to some degree on the quality of the fitted model. Another, perhaps trivial, thing to note is that counterfactual explanations are not unique: there are potentially many valid counterfactual paths. One interesting (or silly) idea following these two observations might be to introduce some form of regularization in the counterfactual search. For example, we could use dropout to randomly switch features on and off in each iteration. Without dwelling further on the usefulness of this idea, let us see how it can be implemented.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"The first code chunk below implements two important steps: 1) create an abstract subtype of the AbstractGradientBasedGenerator and 2) create a constructor similar to the GenericConstructor, but with one additional field for the probability of dropout.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"# Abstract suptype:\nabstract type AbstractDropoutGenerator <: AbstractGradientBasedGenerator end\n\n# Constructor:\nstruct DropoutGenerator <: AbstractDropoutGenerator\n loss::Function # loss function\n penalty::Function\n ฮป::AbstractFloat # strength of penalty\n latent_space::Bool\n opt::Any # optimizer\n generative_model_params::NamedTuple\n p_dropout::AbstractFloat # dropout rate\nend\n\n# Instantiate:\ngenerator = DropoutGenerator(\n Flux.logitbinarycrossentropy,\n CounterfactualExplanations.Objectives.distance_l1,\n 0.1,\n false,\n Flux.Optimise.Descent(0.1),\n (;),\n 0.5,\n)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"Next, we define how feature perturbations are generated for our dropout generator: in particular, we extend the relevant function through a method that implemented the dropout logic.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"using CounterfactualExplanations.Generators\nusing StatsBase\nfunction Generators.generate_perturbations(\n generator::AbstractDropoutGenerator, \n ce::CounterfactualExplanation\n)\n sโ€ฒ = deepcopy(ce.sโ€ฒ)\n new_sโ€ฒ = Generators.propose_state(generator, ce)\n ฮ”sโ€ฒ = new_sโ€ฒ - sโ€ฒ # gradient step\n\n # Dropout:\n set_to_zero = sample(\n 1:length(ฮ”sโ€ฒ),\n Int(round(generator.p_dropout*length(ฮ”sโ€ฒ))),\n replace=false\n )\n ฮ”sโ€ฒ[set_to_zero] .= 0\n return ฮ”sโ€ฒ\nend","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"Finally, we proceed to generate counterfactuals in the same way we always do:","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"# Data and Classifier:\nM = fit_model(counterfactual_data, :DeepEnsemble)\n\n# Factual and Target:\nyhat = predict_label(M, counterfactual_data)\ntarget = 2 # target label\ncandidates = findall(vec(yhat) .!= target)\nchosen = rand(candidates)\nx = select_factual(counterfactual_data, chosen)\n\n# Counterfactual search:\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n num_counterfactuals=5)\n\nplot(ce)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"(Image: )","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/parallelization/#Parallelization","page":"Parallelization","title":"Parallelization","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Version 0.1.15 adds support for parallelization through multi-processing. Currently, the only available backend for parallelization is MPI.jl.","category":"page"},{"location":"tutorials/parallelization/#Available-functions","page":"Parallelization","title":"Available functions","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Parallelization is only available for certain functions. To check if a function is parallelizable, you can use parallelizable function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"using CounterfactualExplanations.Evaluation: evaluate, benchmark\nprintln(parallelizable(generate_counterfactual))\nprintln(parallelizable(evaluate))\nprintln(parallelizable(predict_label))","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"true\ntrue\nfalse","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"In the following, we will generate multiple counterfactuals and evaluate them in parallel:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"chosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 1000)\nxs = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/parallelization/#Multi-threading","page":"Parallelization","title":"Multi-threading","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"We first instantiate an ThreadParallelizer object:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"parallelizer = ThreadsParallelizer()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ThreadsParallelizer()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To generate counterfactuals in parallel, we use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ces = @with_parallelizer parallelizer begin\n generate_counterfactual(\n xs,\n target,\n counterfactual_data,\n M,\n generator\n )\nend","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Generating counterfactuals ... 0%| | ETA: 0:01:29 (89.14 ms/it)Generating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:01 ( 1.59 ms/it)\n\n1000-element Vector{AbstractCounterfactualExplanation}:\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n โ‹ฎ\n CounterfactualExplanation\nConvergence: โœ… after 9 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To evaluate counterfactuals in parallel, we again use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"@with_parallelizer parallelizer evaluate(ces)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Evaluating counterfactuals ... 0%| | ETA: 0:07:03 ( 0.42 s/it)Evaluating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:00 ( 0.86 ms/it)\n\n1000-element Vector{Any}:\n Vector[[1.0], Float32[3.2939816], [0.0]]\n Vector[[1.0], Float32[3.019046], [0.0]]\n Vector[[1.0], Float32[3.701171], [0.0]]\n Vector[[1.0], Float32[2.5611918], [0.0]]\n Vector[[1.0], Float32[2.9027307], [0.0]]\n Vector[[1.0], Float32[3.7893882], [0.0]]\n Vector[[1.0], Float32[3.5026522], [0.0]]\n Vector[[1.0], Float32[3.6317568], [0.0]]\n Vector[[1.0], Float32[3.084984], [0.0]]\n Vector[[1.0], Float32[3.2268934], [0.0]]\n Vector[[1.0], Float32[2.834947], [0.0]]\n Vector[[1.0], Float32[3.656587], [0.0]]\n Vector[[1.0], Float32[2.5985842], [0.0]]\n โ‹ฎ\n Vector[[1.0], Float32[4.067538], [0.0]]\n Vector[[1.0], Float32[3.02231], [0.0]]\n Vector[[1.0], Float32[2.748292], [0.0]]\n Vector[[1.0], Float32[2.9483426], [0.0]]\n Vector[[1.0], Float32[3.066149], [0.0]]\n Vector[[1.0], Float32[3.6018147], [0.0]]\n Vector[[1.0], Float32[3.0138078], [0.0]]\n Vector[[1.0], Float32[3.5724509], [0.0]]\n Vector[[1.0], Float32[3.117551], [0.0]]\n Vector[[1.0], Float32[2.9670508], [0.0]]\n Vector[[1.0], Float32[3.4107168], [0.0]]\n Vector[[1.0], Float32[3.0252533], [0.0]]","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Benchmarks can also be run with parallelization by specifying parallelizer argument:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"# Models:\nbmk = benchmark(counterfactual_data; parallelizer = parallelizer)","category":"page"},{"location":"tutorials/parallelization/#MPI","page":"Parallelization","title":"MPI","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"note: Note\nTo use MPI, you need to have MPI installed on your machine. Running the following code straight from a running Julia session will work if you have MPI installed on your machine, but it will be run on a single process. To execute the code on multiple processes, you need to run it from the command line with mpirun or mpiexec. For example, to run a script on 4 processes, you can run the following command from the command line:\n\nmpiexecjl --project -n 4 julia -e 'include(\"docs/src/srcipts/mpi.jl\")'For more information, see MPI.jl. ","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"We first instantiate an MPIParallelizer object:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"import MPI\nMPI.Init()\nparallelizer = MPIParallelizer(MPI.COMM_WORLD; threaded=true)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Precompiling MPIExt\n โœ“ TaijaParallel โ†’ MPIExt\n 1 dependency successfully precompiled in 3 seconds. 255 already precompiled.\n[ Info: Precompiling MPIExt [48137b38-b316-530b-be8a-261f41e68c23]\nโ”Œ Warning: Module TaijaParallel with build ID ffffffff-ffff-ffff-0001-2d458926c256 is missing from the cache.\nโ”‚ This may mean TaijaParallel [bf1c2c22-5e42-4e78-8b6b-92e6c673eeb0] does not support precompilation but is imported by a module that does.\nโ”” @ Base loading.jl:1948\n[ Info: Skipping precompilation since __precompile__(false). Importing MPIExt [48137b38-b316-530b-be8a-261f41e68c23].\n[ Info: Using `MPI.jl` for multi-processing.\n\nRunning on 1 processes.\n\nMPIExt.MPIParallelizer(MPI.Comm(1140850688), 0, 1, nothing, true)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To generate counterfactuals in parallel, we use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ces = @with_parallelizer parallelizer begin\n generate_counterfactual(\n xs,\n target,\n counterfactual_data,\n M,\n generator\n )\nend","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Generating counterfactuals ... 9%|โ–‹ | ETA: 0:00:01 ( 1.15 ms/it)Generating counterfactuals ... 19%|โ–ˆโ– | ETA: 0:00:01 ( 1.07 ms/it)Generating counterfactuals ... 29%|โ–ˆโ–ˆ | ETA: 0:00:01 ( 1.10 ms/it)Generating counterfactuals ... 39%|โ–ˆโ–ˆโ–Š | ETA: 0:00:01 ( 1.08 ms/it)Generating counterfactuals ... 49%|โ–ˆโ–ˆโ–ˆโ– | ETA: 0:00:01 ( 1.08 ms/it)Generating counterfactuals ... 59%|โ–ˆโ–ˆโ–ˆโ–ˆโ– | ETA: 0:00:00 ( 1.08 ms/it)Generating counterfactuals ... 69%|โ–ˆโ–ˆโ–ˆโ–ˆโ–Š | ETA: 0:00:00 ( 1.08 ms/it)Generating counterfactuals ... 79%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–Œ | ETA: 0:00:00 ( 1.07 ms/it)Generating counterfactuals ... 89%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–Ž| ETA: 0:00:00 ( 1.07 ms/it)Generating counterfactuals ... 99%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‰| ETA: 0:00:00 ( 1.06 ms/it)Generating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:01 ( 1.06 ms/it)\n\n1000-element Vector{AbstractCounterfactualExplanation}:\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n โ‹ฎ\n CounterfactualExplanation\nConvergence: โœ… after 9 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To evaluate counterfactuals in parallel, we again use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"@with_parallelizer parallelizer evaluate(ces)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"1000-element Vector{Any}:\n Vector[[1.0], Float32[3.0941274], [0.0]]\n Vector[[1.0], Float32[3.0894346], [0.0]]\n Vector[[1.0], Float32[3.5737448], [0.0]]\n Vector[[1.0], Float32[2.6201036], [0.0]]\n Vector[[1.0], Float32[2.8519764], [0.0]]\n Vector[[1.0], Float32[3.7762523], [0.0]]\n Vector[[1.0], Float32[3.4162796], [0.0]]\n Vector[[1.0], Float32[3.6095932], [0.0]]\n Vector[[1.0], Float32[3.1347957], [0.0]]\n Vector[[1.0], Float32[3.0313473], [0.0]]\n Vector[[1.0], Float32[2.7612567], [0.0]]\n Vector[[1.0], Float32[3.6191392], [0.0]]\n Vector[[1.0], Float32[2.610616], [0.0]]\n โ‹ฎ\n Vector[[1.0], Float32[4.0844703], [0.0]]\n Vector[[1.0], Float32[3.0119], [0.0]]\n Vector[[1.0], Float32[2.4461186], [0.0]]\n Vector[[1.0], Float32[3.071967], [0.0]]\n Vector[[1.0], Float32[3.132917], [0.0]]\n Vector[[1.0], Float32[3.5403214], [0.0]]\n Vector[[1.0], Float32[3.0588162], [0.0]]\n Vector[[1.0], Float32[3.5600657], [0.0]]\n Vector[[1.0], Float32[3.2205954], [0.0]]\n Vector[[1.0], Float32[2.896302], [0.0]]\n Vector[[1.0], Float32[3.2603998], [0.0]]\n Vector[[1.0], Float32[3.1369917], [0.0]]","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"tip: Tip\nNote that parallelizable processes can be supplied as input to the macro either as a block or directly as an expression.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Benchmarks can also be run with parallelization by specifying parallelizer argument:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"# Models:\nbmk = benchmark(counterfactual_data; parallelizer = parallelizer)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"The following code snippet shows a complete example script that uses MPI for running a benchmark in parallel:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"using CounterfactualExplanations\nusing CounterfactualExplanations.Evaluation: benchmark\nusing CounterfactualExplanations.Models\nimport MPI\n\nMPI.Init()\n\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, :Linear)\nfactual = 1\ntarget = 2\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 100)\nxs = select_factual(counterfactual_data, chosen)\ngenerator = GenericGenerator()\n\nparallelizer = MPIParallelizer(MPI.COMM_WORLD)\n\nbmk = benchmark(counterfactual_data; parallelizer=parallelizer)\n\nMPI.Finalize()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"The file can be executed from the command line as follows:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"mpiexecjl --project -n 4 julia -e 'include(\"docs/src/srcipts/mpi.jl\")'","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanation","category":"page"},{"location":"tutorials/#Tutorials","page":"Overview","title":"Tutorials","text":"","category":"section"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"In this section, you will find a series of tutorials that should help you gain a basic understanding of Conformal Prediction and how to apply it in Julia using this package.","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"Tutorials are lessons that take the reader by the hand through a series of steps to complete a project of some kind. Tutorials are learning-oriented.โ€” Diรกtaxis","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"In other words, you come here because you are new to this topic and are looking for a first peek at the methodology and code ๐Ÿซฃ.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"explanation/generators/growing_spheres/#GrowingSpheres","page":"GrowingSpheres","title":"GrowingSpheres","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Growing Spheres refers to the generator introduced by Laugel et al. (2017). Our implementation takes inspiration from the CARLA library.","category":"page"},{"location":"explanation/generators/growing_spheres/#Principle-of-the-Proposed-Approach","page":"GrowingSpheres","title":"Principle of the Proposed Approach","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"In order to interpret a prediction through comparison, the Growing Spheres algorithm focuses on finding an observation belonging to the other class and answers the question: โ€œConsidering an observation and a classifier, what is the minimal change we need to apply in order to change the prediction of this observation?โ€. This problem is similar to inverse classification but applied to interpretability.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Explaining how to change a prediction can help the user understand what the model considers as locally important. The Growing Spheres approach provides insights into the classifierโ€™s behavior without claiming any causal knowledge. It differs from other interpretability approaches and is not concerned with the global behavior of the model. Instead, it aims to provide local insights into the classifierโ€™s decision-making process.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The algorithm finds the closest โ€œennemyโ€ observation, which is an observation classified into the other class than the input observation. The final explanation is the difference vector between the input observation and the ennemy.","category":"page"},{"location":"explanation/generators/growing_spheres/#Finding-the-Closest-Ennemy","page":"GrowingSpheres","title":"Finding the Closest Ennemy","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The algorithm solves the following minimization problem to find the closest ennemy:","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"e^* = arg min_e in X c(x e) f(e) neq f(x) ","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The cost function c(x, e) is defined as:","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"c(x e) = x - e_2 + gamma x - e_0","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"where ||.||_2 is the Euclidean norm and ||.||_0 is the sparsity measure. The weight gamma balances the importance of sparsity in the cost function.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"To approximate the solution, the Growing Spheres algorithm uses a two-step heuristic approach. The first step is the Generation phase, where observations are generated in spherical layers around the input observation. The second step is the Feature Selection phase, where the generated observation with the smallest change in each feature is selected.","category":"page"},{"location":"explanation/generators/growing_spheres/#Example","page":"GrowingSpheres","title":"Example","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"generator = GrowingSpheresGenerator()\nM = fit_model(counterfactual_data, :DeepEnsemble)\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"(Image: )","category":"page"},{"location":"explanation/generators/growing_spheres/#References","page":"GrowingSpheres","title":"References","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ arXiv. https://doi.org/10.48550/arXiv.1712.08443.","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"contribute/#Contribute","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on.","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"If any of the below applies to you, this might be the right open-source project for you:","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"Youโ€™re an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia.\nYouโ€™re experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI.\nYouโ€™re new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning.","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"@pat-alt here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open issue or start a discussion.","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"For more details on how to contribute see here. Please follow the SciML ColPrac guide.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/clap_roar/#ClaPROARGenerator","page":"ClaPROAR","title":"ClaPROARGenerator","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The ClaPROARGenerator was introduced in Altmeyer et al. (2023).","category":"page"},{"location":"explanation/generators/clap_roar/#Description","page":"ClaPROAR","title":"Description","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The acronym Clap stands for classifier-preserving. The approach is loosely inspired by ROAR (Upadhyay, Joshi, and Lakkaraju 2021). Altmeyer et al. (2023) propose to explicitly penalize the loss incurred by the classifer when evaluated on the counterfactual x^prime at given parameter values. Formally, we have","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"beginaligned\ntextextcost(f(mathbfs^prime)) = l(M(f(mathbfs^prime))y^prime)\nendaligned","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"for each counterfactual k where l denotes the loss function used to train M. This approach is based on the intuition that (endogenous) model shifts will be triggered by counterfactuals that increase classifier loss (Altmeyer et al. 2023).","category":"page"},{"location":"explanation/generators/clap_roar/#Usage","page":"ClaPROAR","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"generator = ClaPROARGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"(Image: )","category":"page"},{"location":"explanation/generators/clap_roar/#Comparison-to-GenericGenerator","page":"ClaPROAR","title":"Comparison to GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The figure below compares the outcome for the GenericGenerator and the ClaPROARGenerator.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"(Image: )","category":"page"},{"location":"explanation/generators/clap_roar/#References","page":"ClaPROAR","title":"References","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"Upadhyay, Sohini, Shalmali Joshi, and Himabindu Lakkaraju. 2021. โ€œTowards Robust and Reliable Algorithmic Recourse.โ€ https://arxiv.org/abs/2102.13620.","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/architecture/#Package-Architecture","page":"Package Architecture","title":"Package Architecture","text":"","category":"section"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"Modular, composable, scalable!","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) Models is concerned with making any arbitrary model compatible with the package; 2) Generators is used to implement arbitrary counterfactual search algorithms.[1] The core function of the package generate_counterfactual uses an instance of type <: AbstractFittedModel produced by the Models module and an instance of type <: AbstractGenerator produced by the Generators module.","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"(Image: )","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"[1] We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"explanation/#Explanation","page":"Overview","title":"Explanation","text":"","category":"section"},{"location":"explanation/","page":"Overview","title":"Overview","text":"In this section you will find detailed explanations about the methodology and code.","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"Explanation clarifies, deepens and broadens the readerโ€™s understanding of a subject.โ€” Diรกtaxis","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"In other words, you come here because you are interested in understanding how all of this actually works ๐Ÿค“.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"extensions/laplace_redux/#[LaplaceRedux.jl](https://github.com/JuliaTrustworthyAI/LaplaceRedux.jl)","page":"LaplaceRedux","title":"LaplaceRedux.jl","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"LaplaceRedux.jl is one of Taijaโ€™s own packages that provides a framework for Effortless Bayesian Deep Learning through Laplace Approximation for Flux.jl neural networks. The methodology was first proposed by Immer, Korzepa, and Bauer (2020) and implemented in Python by Daxberger et al. (2021). This is relevant to the work on counterfactual explanations (CE), because research has shown that counterfactual explanations for Bayesian models are typically more plausible, because Bayesian models are able to capture the uncertainty in the data (Schut et al. 2021).","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"tip: Read More\nTo learn more about Laplace Redux, head over to the official documentation.","category":"page"},{"location":"extensions/laplace_redux/#Example","page":"LaplaceRedux","title":"Example","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"The extension will be loaded automatically when loading the LaplaceRedux package (assuming the CounterfactualExplanations package is also loaded).","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"using LaplaceRedux","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Next, we will fit a neural network with Laplace Approximation to the moons dataset using our standard package API for doing so. By default, the Bayesian prior is optimized through empirical Bayes using the LaplaceRedux package.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"# Fit model to data:\ndata = CounterfactualData(load_moons()...)\nM = fit_model(data, :LaplaceRedux; n_hidden=16)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"LaplaceReduxExt.LaplaceReduxModel(Laplace(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, :all, nothing, :full, LaplaceRedux.Curvature.GGN(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, Flux.Losses.logitcrossentropy, Array{Float32}[[-1.3098596 0.59241515; 0.91760206 0.02950162; โ€ฆ ; -0.018356863 0.12850936; -0.5381665 -0.7872097], [-0.2581085, -0.90997887, -0.5418944, -0.23735572, 0.81020063, -0.3033359, -0.47902864, -0.6432098, -0.038013518, 0.028280666, 0.009903266, -0.8796683, 0.41090682, 0.011093224, -0.1580453, 0.7911349], [3.092321 -2.4660816 โ€ฆ -0.3446268 -1.465249; -2.9468734 3.167357 โ€ฆ 0.31758657 1.7140366], [-0.3107697, 0.31076983]], 1.0, :all, nothing), 1.0, 0.0, Float32[-1.3098596, 0.91760206, 0.5239727, -1.1579771, -0.851813, -1.9411169, 0.47409698, 0.6679365, 0.8944433, 0.663116 โ€ฆ -0.3172857, 0.15530388, 1.3264753, -0.3506721, -0.3446268, 0.31758657, -1.465249, 1.7140366, -0.3107697, 0.31076983], [0.10530027048093525 0.0 โ€ฆ 0.0 0.0; 0.0 0.10530027048093525 โ€ฆ 0.0 0.0; โ€ฆ ; 0.0 0.0 โ€ฆ 0.10530027048093525 0.0; 0.0 0.0 โ€ฆ 0.0 0.10530027048093525], [0.10066431429751965 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 20.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 1.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 1.0101451389641771], [1.1006643142975197 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 21.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 2.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 2.010145138964177], [0.9412600568016627 0.003106911671721699 โ€ฆ 0.003743740333409532 -0.003743452315572739; 0.003106912946573237 0.6539263732691709 โ€ฆ 0.0030385955287734246 -0.0030390041204196414; โ€ฆ ; 0.0037437406323562283 0.003038591829991259 โ€ฆ 0.9624905710233649 0.03750911813897676; -0.0037434526145225856 -0.0030390004216833593 โ€ฆ 0.03750911813898124 0.9624905774453485], 82, 250, 2, 997.8087484836578), :classification_multi)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Finally, we select a factual instance and generate a counterfactual explanation for it using the generic gradient-based CE method.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"# Select a factual instance:\ntarget = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, data) .== factual))\nx = select_factual(data, chosen)\n\n# Generate counterfactual explanation:\nฮท = 0.01\ngenerator = GenericGenerator(; opt=Descent(ฮท), ฮป=0.01)\nconv = CounterfactualExplanations.Convergence.DecisionThresholdConvergence(;\n decision_threshold=0.9, max_iter=100\n)\nce = generate_counterfactual(x, target, data, M, generator; convergence=conv)\nplot(ce, alpha=0.1)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"(Image: )","category":"page"},{"location":"extensions/laplace_redux/#References","page":"LaplaceRedux","title":"References","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Daxberger, Erik, Agustinus Kristiadi, Alexander Immer, Runa Eschenhagen, Matthias Bauer, and Philipp Hennig. 2021. โ€œLaplace Redux-Effortless Bayesian Deep Learning.โ€ Advances in Neural Information Processing Systems 34.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Immer, Alexander, Maciej Korzepa, and Matthias Bauer. 2020. โ€œImproving Predictions of Bayesian Neural Networks via Local Linearization.โ€ https://arxiv.org/abs/2008.08400.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"Random.seed!(42)\n# Counteractual data and model:\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, :Linear)\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)\n\n# Search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"data_large = TaijaData.load_linearly_separable(100000)\ncounterfactual_data_large = DataPreprocessing.CounterfactualData(data_large...)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"@time generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"@time generate_counterfactual(x, target, counterfactual_data_large, M, generator)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/feature_tweak/#FeatureTweakGenerator","page":"FeatureTweak","title":"FeatureTweakGenerator","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Feature Tweak refers to the generator introduced by Tolomei et al. (2017). Our implementation takes inspiration from the featureTweakPy library.","category":"page"},{"location":"explanation/generators/feature_tweak/#Description","page":"FeatureTweak","title":"Description","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Feature Tweak is a powerful recourse algorithm for ensembles of tree-based classifiers such as random forests. Though the problem of understanding how an input to an ensemble model could be transformed in such a way that the model changes its original prediction has been proven to be NP-hard (Tolomei et al. 2017), Feature Tweak provides an algorithm that manages to tractably solve this problem in multiple real-world applications. An example of a problem Feature Tweak is able to efficiently solve, explored in depth in Tolomei et al. (2017) is the problem of transforming an advertisement that has been classified by the ensemble model as a low-quality advertisement to a high-quality one through small changes to its features. With the help of Feature Tweak, advertisers can both learn about the reasons a particular ad was marked to have a low quality, as well as receive actionable suggestions about how to convert a low-quality ad into a high-quality one.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Though Feature Tweak is a powerful way of avoiding brute-force search in an exponential search space, it does not come without disadvantages. The primary limitations of the approach are that itโ€™s currently only applicable to tree-based classifiers and works only in the setting of binary classification. Another problem is that though the algorithm avoids exponential-time search, it is often still computationally expensive. The algorithm may be improved in the future to tackle all of these shortcomings.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"The following equation displays how a true negative instance x can be transformed into a positively predicted instance xโ€™. To be more precise, xโ€™ is the best possible transformation among all transformations **x***, computed with a cost function ฮด.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"beginaligned\nmathbfx^prime = arg_mathbfx^* min delta(mathbfx mathbfx^*) hatf(mathbfx) = -1 wedge hatf(mathbfx^*) = +1 \nendaligned","category":"page"},{"location":"explanation/generators/feature_tweak/#Example","page":"FeatureTweak","title":"Example","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"In this example we apply the Feature Tweak algorithm to a decision tree and a random forest trained on the moons dataset. We first load the data and fit the models:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"n = 500\ncounterfactual_data = CounterfactualData(TaijaData.load_moons(n)...)\n\n# Classifiers\ndecision_tree = CounterfactualExplanations.Models.fit_model(\n counterfactual_data, :DecisionTree; max_depth=5, min_samples_leaf=3\n)\nforest = Models.fit_model(counterfactual_data, :RandomForest)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Next, we select a point to explain and a target class to transform the point to. We then search for counterfactuals using the FeatureTweakGenerator:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"# Select a point to explain:\nx = float32.([1, -0.5])[:,:]\nfactual = Models.predict_label(forest, x)\ntarget = counterfactual_data.y_levels[findall(counterfactual_data.y_levels != factual)][1]\n\n# Search for counterfactuals:\ngenerator = FeatureTweakGenerator(ฯต=0.1)\ntree_counterfactual = generate_counterfactual(\n x, target, counterfactual_data, decision_tree, generator\n)\nforest_counterfactual = generate_counterfactual(\n x, target, counterfactual_data, forest, generator\n)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"The resulting counterfactuals are shown below:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"p1 = plot(\n tree_counterfactual;\n colorbar=false,\n title=\"Decision Tree\",\n)\n\np2 = plot(\n forest_counterfactual; title=\"Random Forest\",\n colorbar=false,\n)\n\ndisplay(plot(p1, p2; size=(800, 400)))","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"(Image: )","category":"page"},{"location":"explanation/generators/feature_tweak/#References","page":"FeatureTweak","title":"References","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/clue/#CLUEGenerator","page":"CLUE","title":"CLUEGenerator","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"In this tutorial, we introduce the CLUEGenerator, a counterfactual generator based on the Counterfactual Latent Uncertainty Explanations (CLUE) method proposed by Antorรกn et al. (2020).","category":"page"},{"location":"explanation/generators/clue/#Description","page":"CLUE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUEGenerator leverages differentiable probabilistic models, such as Bayesian Neural Networks (BNNs), to estimate uncertainty in predictions. It aims to provide interpretable counterfactual explanations by identifying input patterns that lead to predictive uncertainty. The generator utilizes a latent variable framework and employs a decoder from a variational autoencoder (VAE) to generate counterfactual samples in latent space.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUE algorithm minimizes a loss function that combines uncertainty estimates and the distance between the generated counterfactual and the original input. By optimizing this loss function iteratively, the CLUEGenerator generates counterfactuals that are similar to the original observation but assigned low uncertainty.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The formula for predictive entropy is as follow:","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"beginaligned\nH(y^*x^* D) = - sum_k=1^K p(y^*=c_kx^* D) log p(y^*=c_kx^* D)\nendaligned","category":"page"},{"location":"explanation/generators/clue/#Usage","page":"CLUE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"While using one must keep in mind that the CLUE algorithim is meant to find a more robust datapoint of the same class, using CLUE generator without any additional penalties/losses will mean that it is not a counterfactual generator. The generated result will be of the same class as the original input, but a more robust datapoint.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"CLUE works best for BNNโ€™s. The CLUEGenerator can be used with any differentiable probabilistic model, but the results may not be as good as with BNNs.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUEGenerator can be used in the following manner:","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"generator = CLUEGenerator()\nM = fit_model(counterfactual_data, :DeepEnsemble)\nconv = CounterfactualExplanations.Convergence.MaxIterConvergence(max_iter=1000)\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n convergence=conv)\nplot(ce)","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"(Image: )","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Extra: The CLUE generator can also be used upon already having achieved a counterfactual with a different generator. In this case, you can use CLUE and make the counterfactual more robust.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Note: The above documentation is based on the information provided in the CLUE paper. Please refer to the original paper for more detailed explanations and implementation specifics.","category":"page"},{"location":"explanation/generators/clue/#References","page":"CLUE","title":"References","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/revise/#REVISEGenerator","page":"REVISE","title":"REVISEGenerator","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"REVISE is a Latent Space generator introduced by Joshi et al. (2019).","category":"page"},{"location":"explanation/generators/revise/#Description","page":"REVISE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The current consensus in the literature is that Counterfactual Explanations should be realistic: the generated counterfactuals should look like they were generated by the data-generating process (DGP) that governs the problem at hand. With respect to Algorithmic Recourse, it is certainly true that counterfactuals should be realistic in order to be actionable for individuals.[1] To address this need, researchers have come up with various approaches in recent years. Among the most popular approaches is Latent Space Search, which was first proposed in Joshi et al. (2019): instead of traversing the feature space directly, this approach relies on a separate generative model that learns a latent space representation of the DGP. Assuming the generative model is well-specified, access to the learned latent embeddings of the data comes with two advantages:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Since the learned DGP is encoded in the latent space, the generated counterfactuals will respect the learned representation of the data. In practice, this means that counterfactuals will be realistic.\nThe latent space is typically a compressed (i.e.ย lower dimensional) version of the feature space. This makes the counterfactual search less costly.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"There are also certain disadvantages though:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Learning generative models is (typically) an expensive task, which may well outweigh the benefits associated with utlimately traversing a lower dimensional space.\nIf the generative model is poorly specified, this will affect the quality of the counterfactuals.[2]","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Anyway, traversing latent embeddings is a powerful idea that may be very useful depending on the specific context. This tutorial introduces the concept and how it is implemented in this package.","category":"page"},{"location":"explanation/generators/revise/#Usage","page":"REVISE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"generator = REVISEGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#Worked-2D-Examples","page":"REVISE","title":"Worked 2D Examples","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Below we load 2D data and train a VAE on it and plot the original samples against their reconstructions.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# output: true\n\ncounterfactual_data = CounterfactualData(load_overlapping()...)\nX = counterfactual_data.X\ny = counterfactual_data.y\ninput_dim = size(X, 1)\nusing CounterfactualExplanations.GenerativeModels: VAE, train!, reconstruct\nvae = VAE(input_dim; nll=Flux.Losses.mse, epochs=100, ฮป=0.01, latent_dim=2, hidden_dim=32)\nflux_training_params.verbose = true\ntrain!(vae, X, y)\nXฬ‚ = reconstruct(vae, X)[1]\np0 = scatter(X[1, :], X[2, :], color=:blue, label=\"Original\", xlab=\"xโ‚\", ylab=\"xโ‚‚\")\nscatter!(Xฬ‚[1, :], Xฬ‚[2, :], color=:orange, label=\"Reconstructed\", xlab=\"xโ‚\", ylab=\"xโ‚‚\")\np1 = scatter(X[1, :], Xฬ‚[1, :], color=:purple, label=\"\", xlab=\"xโ‚\", ylab=\"xฬ‚โ‚\")\np2 = scatter(X[2, :], Xฬ‚[2, :], color=:purple, label=\"\", xlab=\"xโ‚‚\", ylab=\"xฬ‚โ‚‚\")\nplt2 = plot(p1,p2, layout=(1,2), size=(800, 400))\nplot(p0, plt2, layout=(2,1), size=(800, 600))","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Next, we train a simple MLP for the classification task. Then we determine a target and factual class for our counterfactual search and select a random factual instance to explain.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"M = fit_model(counterfactual_data, :MLP)\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Finally, we generate and visualize the generated counterfactual:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Search:\ngenerator = REVISEGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#3D-Example","page":"REVISE","title":"3D Example","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"To illustrate the notion of Latent Space search, letโ€™s look at an example involving 3-dimensional input data, which we can still visualize. The code chunk below loads the data and implements the counterfactual search.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Data and Classifier:\ncounterfactual_data = CounterfactualData(load_blobs(k=3)...)\nX = counterfactual_data.X\nys = counterfactual_data.output_encoder.labels.refs\nM = fit_model(counterfactual_data, :MLP)\n\n# Randomly selected factual:\nx = select_factual(counterfactual_data,rand(1:size(counterfactual_data.X,2)))\ny = predict_label(M, counterfactual_data, x)[1]\ntarget = counterfactual_data.y_levels[counterfactual_data.y_levels .!= y][1]\n\n# Generate recourse:\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The figure below demonstrates the idea of searching counterfactuals in a lower-dimensional latent space: on the left, we can see the counterfactual search in the 3-dimensional feature space, while on the right we can see the corresponding search in the latent space.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#MNIST-data","page":"REVISE","title":"MNIST data","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Letโ€™s carry the ideas introduced above over to a more complex example. The code below loads MNIST data as well as a pre-trained classifier and generative model for the data.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"using CounterfactualExplanations.Models: load_mnist_mlp, load_mnist_ensemble, load_mnist_vae\ncounterfactual_data = CounterfactualData(load_mnist()...)\nX, y = CounterfactualExplanations.DataPreprocessing.unpack_data(counterfactual_data)\ninput_dim, n_obs = size(counterfactual_data.X)\nM = load_mnist_mlp()\nvae = load_mnist_vae()","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The F1-score of our pre-trained image classifier on test data is: 0.94","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Before continuing, we supply the pre-trained generative model to our data container:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"counterfactual_data.generative_model = vae # assign generative model","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Now letโ€™s define a factual and target label:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Randomly selected factual:\nRandom.seed!(2023)\nfactual_label = 8\nx = reshape(X[:,rand(findall(predict_label(M, counterfactual_data).==factual_label))],input_dim,1)\ntarget = 3\nfactual = predict_label(M, counterfactual_data, x)[1]","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Using REVISE, we are going to turn a randomly drawn 8 into a 3.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The API call is the same as always:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"ฮณ = 0.95\nconv = \n CounterfactualExplanations.Convergence.DecisionThresholdConvergence(decision_threshold=ฮณ)\n# Define generator:\ngenerator = REVISEGenerator(opt=Flux.Adam(0.1))\n# Generate recourse:\nce = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence=conv)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The chart below shows the results:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#References","page":"REVISE","title":"References","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"[1] In general, we believe that there may be a trade-off between creating counterfactuals that respect the DGP vs.ย counterfactuals reflect the behaviour of the black-model in question - both accurately and complete.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"[2] We believe that there is another potentially crucial disadvantage of relying on a separate generative model: it reallocates the task of learning realistic explanations for the data from the black-box model to the generative model.","category":"page"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/optimisers/overview/#Optimisation-Rules","page":"Overview","title":"Optimisation Rules","text":"","category":"section"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"Counterfactual search is an optimization problem. Consequently, the choice of the optimisation rule affects the generated counterfactuals. In the short term, we aim to enable users to choose any of the available Flux optimisers. This has not been sufficiently tested yet, and you may run into issues.","category":"page"},{"location":"explanation/optimisers/overview/#Custom-Optimisation-Rules","page":"Overview","title":"Custom Optimisation Rules","text":"","category":"section"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"Flux optimisers are specifically designed for deep learning, and in particular, for learning model parameters. In counterfactual search, the features are the free parameters that we are optimising over. To this end, some custom optimisation rules are necessary to incorporate ideas presented in the literature. In the following, we introduce those rules.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/data_preprocessing/#Handling-Data","page":"Handling Data","title":"Handling Data","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The package works with custom data containers that contain the input and output data as well as information about the type and mutability of features. In this tutorial, we will see how data can be prepared for use with the package.","category":"page"},{"location":"tutorials/data_preprocessing/#Basic-Functionality","page":"Handling Data","title":"Basic Functionality","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To demonstrate the basic way to prepare data, letโ€™s look at a standard benchmark dataset: Fisherโ€™s classic iris dataset. We can use MLDatasets to load this data.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"dataset = Iris()","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Our data constructor CounterfactualData needs at least two inputs: features X and targets y.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"X = dataset.features\ny = dataset.targets","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we convert the input data to a Tables.MatrixTable (following MLJ.jl) convention. Concerning the target variable, we just assign grab the first column of the data frame.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"X = table(Tables.matrix(X))\ny = y[:,1]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Now we can feed these two ingredients to our constructor:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(X, y)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Under the hood, the constructor performs basic preprocessing steps. For example, the output variable y is automatically one-hot encoded:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data.y","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"3ร—150 Matrix{Bool}:\n 1 1 1 1 1 1 1 1 1 1 1 1 1 โ€ฆ 0 0 0 0 0 0 0 0 0 0 0 0\n 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Similarly, a transformer used to scale continuous input features is automatically fitted:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data.dt","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"ZScoreTransform{Float64, Vector{Float64}}(4, 2, [5.843333333333335, 3.0540000000000007, 3.7586666666666693, 1.1986666666666672], [0.8280661279778629, 0.4335943113621737, 1.7644204199522617, 0.7631607417008414])","category":"page"},{"location":"tutorials/data_preprocessing/#Categorical-Features","page":"Handling Data","title":"Categorical Features","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"For the counterfactual search, it is important to distinguish between continuous and categorical features. This is because categorical features cannot be perturbed arbitrarily: they can take specific discrete values, but not just any value on the real line.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Consider the following example:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"y = rand([1,0],4)\nX = (\n name=categorical([\"Danesh\", \"Lee\", \"Mary\", \"John\"]),\n grade=categorical([\"A\", \"B\", \"A\", \"C\"], ordered=true),\n sex=categorical([\"male\",\"female\",\"male\",\"male\"]),\n height=[1.85, 1.67, 1.5, 1.67],\n)\nschema(X)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name โ”‚ Multiclass{4} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ grade โ”‚ OrderedFactor{3} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ sex โ”‚ Multiclass{2} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Typically, in the context of Unserpervised Learning, categorical features are one-hot or dummy encoded. To this end, we could use MLJ, for example:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"hot = OneHotEncoder()\nmach = MLJBase.fit!(machine(hot, X))\nW = MLJBase.transform(mach, X)\nX = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"In all likelihood, this pre-processing step already happens at the stage, when the supervised model is trained. Since our counterfactual generators need to work in the same feature domain as the model they are intended to explain, we assume that categorical features are already encoded.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The CounterfactualData constructor takes two optional arguments that can be used to specify the indices of categorical and continuous features. By default, all features are assumed to be continuous. For categorical features, the constructor expects an array of arrays of integers (Vector{Vector{Int}}) where each subarray includes the indices of all one-hot encoded rows related to a single categorical feature. In the example above, the name feature is one-hot encoded across rows 1, 2, 3 and 4 of X, the grade feature is encoded across the following three rows, etc.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"schema(W)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name__Danesh โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__John โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Lee โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Mary โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__A โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__B โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__C โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__female โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__male โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The code chunk below assigns the categorical and continuous feature indices:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"features_categorical = [\n [1,2,3,4], # name\n [5,6,7], # grade\n [8,9] # sex\n]\nfeatures_continuous = [10]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"When instantiating the data container, these indices just need to be supplied as keyword arguments:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(\n X,y;\n features_categorical = features_categorical,\n features_continuous = features_continuous\n)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"This will ensure that the discrete domain of categorical features is respected in the counterfactual search. We achieve this through a form of Projected Gradient Descent and it works for any of our counterfactual generators.","category":"page"},{"location":"tutorials/data_preprocessing/#Example","page":"Handling Data","title":"Example","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To see this in action, letโ€™s load some synthetic data using MLJ:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"N = 1000\nX, ys = MLJBase.make_blobs(N, 2; centers=2, as_table=false, center_box=(-5 => 5), cluster_std=0.5)\nys .= ys.==2","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we generate a synthetic categorical feature based on the output variable. First, we define the discrete levels:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"cat_values = [\"X\",\"Y\",\"Z\"]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we impose that the categorical feature is most likely to take the first discrete level, namely X, whenever y is equal to 1.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"xcat = map(ys) do y\n if y==1\n x = sample(cat_values, Weights([0.8,0.1,0.1]))\n else\n x = sample(cat_values, Weights([0.1,0.1,0.8]))\n end\nend\nxcat = categorical(xcat)\nX = (\n x1 = X[:,1],\n x2 = X[:,2],\n x3 = xcat\n)\nschema(X)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"As above, we use a OneHotEncoder to transform the data:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"hot = OneHotEncoder()\nmach = MLJBase.fit!(machine(hot, X))\nW = MLJBase.transform(mach, X)\nschema(W)\nX = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Finally, we assign the categorical indices and instantiate our data container:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"features_categorical = [collect(3:size(X,1))]\ncounterfactual_data = CounterfactualData(\n X,ys';\n features_categorical = features_categorical,\n)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"With the data pre-processed we can use the fit_model function to train a simple classifier:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"M = fit_model(counterfactual_data, :Linear)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Now it is finally time to generate counterfactuals. We first define 1 as our target and then choose a random sample from the non-target class:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"target = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen) ","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"5ร—1 Matrix{Float32}:\n -2.9433472\n 0.5782963\n 0.0\n 0.0\n 1.0","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The factual x belongs to group Z.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"We generate a counterfactual for x using the standard API call:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"generator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The search yields the following counterfactual:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"xโ€ฒ = counterfactual(ce)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"5-element Vector{Float32}:\n 1.1222683\n 0.7145791\n 0.0\n 0.0\n 1.0","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"It belongs to group Z.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"This is intuitive because by construction the categorical variable is most likely to take that value when y is equal to the target outcome.","category":"page"},{"location":"tutorials/data_preprocessing/#Immutable-Features","page":"Handling Data","title":"Immutable Features","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"In practice, features usually cannot be perturbed arbitrarily. Suppose, for example, that one of the features used by a bank to predict the creditworthiness of its clients is gender. If a counterfactual explanation for the prediction model indicates that female clients should change their gender to improve their creditworthiness, then this is an interesting insight (it reveals gender bias), but it is not usually an actionable transformation in practice. In such cases, we may want to constrain the mutability of features to ensure actionable and realistic recourse.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To illustrate how this can be implemented in CounterfactualExplanations.jl we will continue to work with the synthetic data from the previous section. Mutability of features can be defined in terms of four different options: 1) the feature is mutable in both directions, 2) the feature can only increase (e.g.ย age), 3) the feature can only decrease (e.g.ย time left until your next deadline) and 4) the feature is not mutable (e.g.ย skin colour, ethnicity, โ€ฆ). To specify which category a feature belongs to, you can pass a vector of symbols containing the mutability constraints at the pre-processing stage. For each feature you can choose from these four options: :both (mutable in both directions), :increase (only up), :decrease (only down) and :none (immutable). By default, nothing is passed to that keyword argument and it is assumed that all features are mutable in both directions.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Below we impose that the second feature is immutable.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(load_linearly_separable()...)\nM = fit_model(counterfactual_data, :Linear)\ncounterfactual_data.mutability = [:both, :none]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"target = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen) \nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The resulting counterfactual path is shown in the chart below. Since only the first feature can be perturbed, the sample can only move along the horizontal axis.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"plot(ce)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"(Image: ) ","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/model_catalogue/#Model-Catalogue","page":"Model Catalogue","title":"Model Catalogue","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"While in general it is assumed that users will use this package to explain their pre-trained models, we provide out-of-the-box functionality to train various simple default models. In this tutorial, we will see how these models can be fitted to CounterfactualData.","category":"page"},{"location":"tutorials/model_catalogue/#Available-Models","page":"Model Catalogue","title":"Available Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The standard_models_catalogue can be used to inspect the available default models:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"standard_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Dict{Symbol, Any} with 4 entries:\n :Linear => Linear\n :LaplaceRedux => LaplaceReduxModel\n :DeepEnsemble => FluxEnsemble\n :MLP => FluxModel","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The dictionary keys correspond to the model names. In this case, the dictionary values are constructors that can be used called on instances of type CounterfactualData to fit the corresponding model. In most cases, users will find it most convenient to use the fit_model API call instead.","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-Models","page":"Model Catalogue","title":"Fitting Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Models from the standard model catalogue are a core part of the package and thus compatible with all offered counterfactual generators and functionalities.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The all_models_catalogue can be used to inspect all models offered by the package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"all_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"However, when using models not included in the standard_models_catalogue, additional caution is advised: they might not be supported by all counterfactual generators or they might not be models native to Julia. Thus, a more thorough reading of their documentation may be necessary to make sure that they are used correctly.","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-Flux-Models","page":"Model Catalogue","title":"Fitting Flux Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"First, letโ€™s load one of the synthetic datasets. For this, weโ€™ll first need to import the TaijaData.jl package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"n = 500\ndata = TaijaData.load_multi_class(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"We could use a Deep Ensemble (Lakshminarayanan, Pritzel, and Blundell 2016) as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(counterfactual_data, :DeepEnsemble)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The returned object is an instance of type FluxEnsemble <: AbstractFittedModel and can be used in downstream tasks without further ado. For example, the resulting fit can be visualised using the generic plot() method as:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"plts = []\nfor target in counterfactual_data.y_levels\n plt = plot(M, counterfactual_data; target=target, title=\"p(y=$(target)|x,ฮธ)\")\n plts = [plts..., plt]\nend\nplot(plts...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/model_catalogue/#Importing-PyTorch-models","page":"Model Catalogue","title":"Importing PyTorch models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The package supports generating counterfactuals for any neural network that has been previously defined and trained using PyTorch, regardless of the specific architectural details of the model. To generate counterfactuals for a PyTorch model, save the model inside a .pt file and call the following function:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_loaded = TaijaInteroperability.pytorch_model_loader(\n \"$(pwd())/docs/src/tutorials/miscellaneous\",\n \"neural_network_class\",\n \"NeuralNetwork\",\n \"$(pwd())/docs/src/tutorials/miscellaneous/pretrained_model.pt\"\n)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The method pytorch_model_loader requires four arguments:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The path to the folder with a .py file where the PyTorch model is defined\nThe name of the file where the PyTorch model is defined\nThe name of the class of the PyTorch model\nThe path to the Pickle file that holds the model weights","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In the above case:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The file defining the model is inside $(pwd())/docs/src/tutorials/miscellaneous\nThe name of the .py file holding the model definition is neural_network_class\nThe name of the model class is NeuralNetwork\nThe Pickle file is located at $(pwd())/docs/src/tutorials/miscellaneous/pretrained_model.pt","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Though the model file and Pickle file are inside the same directory in this tutorial, this does not necessarily have to be the case.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The reason why the model file and Pickle file have to be provided separately is that the package expects an already trained PyTorch model as input. It is also possible to define new PyTorch models within the package, but since this is not the expected use of our package, special support is not offered for that. A guide for defining Python and PyTorch classes in Julia through PythonCall.jl can be found here.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once the PyTorch model has been loaded into the package, wrap it inside the PyTorchModel class:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_pytorch = TaijaInteroperability.PyTorchModel(model_loaded, counterfactual_data.likelihood)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"This model can now be passed into the generators like any other.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that the functionality for generating counterfactuals for Python models is only available if your Julia version is 1.8 or above. For Julia 1.7 users, we recommend upgrading the version to 1.8 or 1.9 before loading a PyTorch model into the package.","category":"page"},{"location":"tutorials/model_catalogue/#Importing-R-torch-models","page":"Model Catalogue","title":"Importing R torch models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"warning: Not fully tested\nPlease note that due to the incompatibility between RCall and PythonCall, it is not feasible to test both PyTorch and RTorch implementations within the same pipeline. While the RTorch implementation has been manually tested, we cannot ensure its consistent functionality as it is inherently susceptible to bugs.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The CounterfactualExplanations package supports generating counterfactuals for neural networks that have been defined and trained using R torch. Regardless of the specific architectural details of the model, you can easily generate counterfactual explanations by following these steps.","category":"page"},{"location":"tutorials/model_catalogue/#Saving-the-R-torch-model","page":"Model Catalogue","title":"Saving the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"First, save your trained R torch model as a .pt file using the torch_save() function provided by the R torch library. This function allows you to serialize the model and save it to a file. For example:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"torch_save(model, file = \"$(pwd())/docs/src/tutorials/miscellaneous/r_model.pt\")","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Make sure to specify the correct file path where you want to save the model.","category":"page"},{"location":"tutorials/model_catalogue/#Loading-the-R-torch-model","page":"Model Catalogue","title":"Loading the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To import the R torch model into the CounterfactualExplanations package, use the rtorch_model_loader() function. This function loads the model from the previously saved .pt file. Here is an example of how to load the R torch model:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_loaded = TaijaInteroperability.rtorch_model_loader(\"$(pwd())/docs/src/tutorials/miscellaneous/r_model.pt\")","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The rtorch_model_loader() function requires only one argument:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_path: The path to the .pt file that contains the trained R torch model.","category":"page"},{"location":"tutorials/model_catalogue/#Wrapping-the-R-torch-model","page":"Model Catalogue","title":"Wrapping the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once the R torch model has been loaded into the package, wrap it inside the RTorchModel class. This step prepares the model to be used by the counterfactual generators. Here is an example:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_R = TaijaInteroperability.RTorchModel(model_loaded, counterfactual_data.likelihood)","category":"page"},{"location":"tutorials/model_catalogue/#Generating-counterfactuals-with-the-R-torch-model","page":"Model Catalogue","title":"Generating counterfactuals with the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Now that the R torch model has been wrapped inside the RTorchModel class, you can pass it into the counterfactual generators as you would with any other model.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that RCall is not fully compatible with PythonCall. Therefore, it is advisable not to import both R torch and PyTorch models within the same Julia session. Additionally, itโ€™s worth mentioning that the R torch integration is still untested in the CounterfactualExplanations package.","category":"page"},{"location":"tutorials/model_catalogue/#Tuning-Flux-Models","page":"Model Catalogue","title":"Tuning Flux Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"By default, model architectures are very simple. Through optional arguments, users have some control over the neural network architecture and can choose to impose regularization through dropout. Letโ€™s tackle a more challenging dataset: MNIST (LeCun 1998).","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"data = TaijaData.load_mnist(10000)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ntrain_data, test_data = \n CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In this case, we will use a Multi-Layer Perceptron (MLP) but we will adjust the model and training hyperparameters. Parameters related to training of Flux.jl models are currently stored in a mutable container:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"flux_training_params","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CounterfactualExplanations.FluxModelParams\n loss: Symbol logitbinarycrossentropy\n opt: Symbol Adam\n n_epochs: Int64 100\n batchsize: Int64 1\n verbose: Bool false","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In cases like this one, where model training can be expected to take a few moments, it can be useful to activate verbosity, so letโ€™s set the corresponding field value to true. Weโ€™ll also impose mini-batch training:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"flux_training_params.verbose = true\nflux_training_params.batchsize = round(size(train_data.X,2)/10)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To account for the fact that this is a slightly more challenging task, we will use an appropriate number of hidden neurons per layer. We will also activate dropout regularization. To scale networks up further, it is also possible to adjust the number of hidden layers, which we will not do here.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_params = (\n n_hidden = 32,\n dropout = true\n)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The model_params can be supplied to the familiar API call:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(train_data, :MLP; model_params...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"FluxModel(Chain(Dense(784 => 32, relu), Dropout(0.25, active=false), Dense(32 => 10)), :classification_multi)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The model performance on our test set can be evaluated as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_evaluation(M, test_data)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"1-element Vector{Float64}:\n 0.9185","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Finally, letโ€™s restore the default training parameters:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CounterfactualExplanations.reset!(flux_training_params)","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-and-tuning-MLJ-models","page":"Model Catalogue","title":"Fitting and tuning MLJ models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Among models from the MLJ library, two models are integrated as part of the core functionality of the package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"mlj_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"These models are compatible with the Feature Tweak generator. Support for other generators has not been implemented, as both decision trees and random forests are non-differentiable tree-based models and thus, gradient-based generators donโ€™t apply for them.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Tuning MLJ models is very simple. As the first step, letโ€™s reload the dataset:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"n = 500\ndata = TaijaData.load_moons(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Using the usual procedure for fitting models, we can call the following method:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"tree = CounterfactualExplanations.Models.fit_model(counterfactual_data, :DecisionTree)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"However, itโ€™s also possible to tune the DecisionTreeClassifierโ€™s parameters. This can be done using the keyword arguments when calling fit_model() as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"tree = CounterfactualExplanations.Models.fit_model(counterfactual_data, :DecisionTree; max_depth=2, min_samples_leaf=3)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"For all supported MLJ models, every tunable parameter they have is supported as a keyword argument. The tunable parameters for the DecisionTreeModel and the RandomForestModel can be found from the documentation of the DecisionTree.jl package under the Decision Tree Classifier and Random Forest Classifier sections.","category":"page"},{"location":"tutorials/model_catalogue/#Package-extension-models","page":"Model Catalogue","title":"Package extension models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The package also includes two models which donโ€™t form a part of the core functionality of the package, but which can be accessed as package extensions. These are the EvoTreeModel from the MLJ library and the LaplaceReduxModel from LaplaceRedux.jl.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To trigger the package extensions, the weak dependency first has to be loaded with the using keyword:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"using EvoTrees","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once this is done, the extension models can be used like any other model:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(counterfactual_data, :EvoTree; model_params...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"EvoTreesExt.EvoTreeModel(machine(EvoTreeClassifier{EvoTrees.MLogLoss}\n - nrounds: 100\n - L2: 0.0\n - lambda: 0.0\n - gamma: 0.0\n - eta: 0.1\n - max_depth: 6\n - min_weight: 1.0\n - rowsample: 1.0\n - colsample: 1.0\n - nbins: 64\n - alpha: 0.5\n - tree_type: binary\n - rng: MersenneTwister(123, (0, 9018, 8016, 884))\n, โ€ฆ), :classification_multi)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The tunable parameters for the EvoTreeModel can be found from the documentation of the EvoTrees.jl package under the EvoTreeClassifier section.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that support for counterfactual generation with both LaplaceReduxModel and EvoTreeModel is not yet fully implemented.","category":"page"},{"location":"tutorials/model_catalogue/#References","page":"Model Catalogue","title":"References","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"LeCun, Yann. 1998. โ€œThe MNIST Database of Handwritten Digits.โ€","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/overview/#generators_explanation","page":"Overview","title":"Counterfactual Generators","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Counterfactual generators form the very core of this package. The generator_catalogue can be used to inspect the available generators:","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"generator_catalogue","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Dict{Symbol, Any} with 11 entries:\n :gravitational => GravitationalGenerator\n :growing_spheres => GrowingSpheresGenerator\n :revise => REVISEGenerator\n :clue => CLUEGenerator\n :probe => ProbeGenerator\n :dice => DiCEGenerator\n :feature_tweak => FeatureTweakGenerator\n :claproar => ClaPROARGenerator\n :wachter => WachterGenerator\n :generic => GenericGenerator\n :greedy => GreedyGenerator","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"The following sections provide brief descriptions of all of them.","category":"page"},{"location":"explanation/generators/overview/#Gradient-based-Counterfactual-Generators","page":"Overview","title":"Gradient-based Counterfactual Generators","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"At the time of writing, all generators are gradient-based: that is, counterfactuals are searched through gradient descent. In Altmeyer et al. (2023) we lay out a general methodological framework that can be applied to all of these generators:","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"beginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS left textyloss(M(f(mathbfs^prime))y^*)+ lambda textcost(f(mathbfs^prime)) right \nendaligned ","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"โ€œHere mathbfs^prime=lefts_k^primeright_K is a K-dimensional array of counterfactual states and f mathcalS mapsto mathcalX maps from the counterfactual state space to the feature space.โ€ (Altmeyer et al. 2023)","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"For most generators, the state space is the feature space (f is the identity function) and the number of counterfactuals K is one. Latent Space generators instead search counterfactuals in some latent space mathcalS. In this case, f corresponds to the decoder part of the generative model, that is the function that maps back from the latent space to inputs.","category":"page"},{"location":"explanation/generators/overview/#References","page":"Overview","title":"References","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"_contribute/#Contributing","page":"Contributing","title":"Contributing","text":"","category":"section"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"Our goal is to provide a go-to place for Counterfactual Explanations in Julia. To this end, the following is a non-exhaustive list of enhancements we have planned:","category":"page"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"Additional counterfactual generators and predictive models.\nAdditional datasets for testing, evaluation and benchmarking.\nSupport for regression models.","category":"page"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"For a complete list, have a look at outstanding issue.","category":"page"},{"location":"_contribute/#How-to-contribute?","page":"Contributing","title":"How to contribute?","text":"","category":"section"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"Any sort of contribution is welcome, in particular:","category":"page"},{"location":"_contribute/","page":"Contributing","title":"Contributing","text":"Should you spot any errors or something is not working, please just open an issue.\nIf you want to contribute your code, please proceed as follows:\nFork this repo and clone your fork: git clone https://github.com/your_username/CounterfactualExplanations.jl.\nImplement your modifications and submit a pull request.\nFor any other questions or comments, you can also start a discussion.","category":"page"},{"location":"assets/resources/#Further-Resources","page":"๐Ÿ“š Additional Resources","title":"Further Resources","text":"","category":"section"},{"location":"assets/resources/#JuliaCon-2022","page":"๐Ÿ“š Additional Resources","title":"JuliaCon 2022","text":"","category":"section"},{"location":"assets/resources/","page":"๐Ÿ“š Additional Resources","title":"๐Ÿ“š Additional Resources","text":"Slides: link","category":"page"},{"location":"assets/resources/#JuliaCon-Proceedings-Paper","page":"๐Ÿ“š Additional Resources","title":"JuliaCon Proceedings Paper","text":"","category":"section"},{"location":"assets/resources/","page":"๐Ÿ“š Additional Resources","title":"๐Ÿ“š Additional Resources","text":"TBD","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"reference/#Reference","page":"๐Ÿง Reference","title":"Reference","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"In this reference, you will find a detailed overview of the package API.","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Reference guides are technical descriptions of the machinery and how to operate it. Reference material is information-oriented.โ€” Diรกtaxis","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"In other words, you come here because you want to take a very close look at the code ๐Ÿง.","category":"page"},{"location":"reference/#Content","page":"๐Ÿง Reference","title":"Content","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Pages = [\"reference.md\"]\nDepth = 2","category":"page"},{"location":"reference/#Exported-functions","page":"๐Ÿง Reference","title":"Exported functions","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Modules = [\n CounterfactualExplanations, \n CounterfactualExplanations.Convergence,\n CounterfactualExplanations.Evaluation,\n CounterfactualExplanations.DataPreprocessing,\n CounterfactualExplanations.Models,\n CounterfactualExplanations.GenerativeModels, \n CounterfactualExplanations.Generators, \n CounterfactualExplanations.Objectives\n]\nPrivate = false","category":"page"},{"location":"reference/#CounterfactualExplanations.RawOutputArrayType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.RawOutputArrayType","text":"RawOutputArrayType\n\nA type union for the allowed type for the output array y.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.RawTargetType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.RawTargetType","text":"RawTargetType\n\nA type union for the allowed types for the target variable.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.flux_training_params","page":"๐Ÿง Reference","title":"CounterfactualExplanations.flux_training_params","text":"flux_training_params\n\nThe default training parameter for FluxModels etc.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.AbstractConvergence","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractConvergence","text":"An abstract type that serves as the base type for convergence objects.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractCounterfactualExplanation","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractCounterfactualExplanation","text":"Base type for counterfactual explanations.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractFittedModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractFittedModel","text":"Base type for fitted models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractGenerator","text":"An abstract type that serves as the base type for counterfactual generators.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.CounterfactualExplanation","page":"๐Ÿง Reference","title":"CounterfactualExplanations.CounterfactualExplanation","text":"A struct that collects all information relevant to a specific counterfactual explanation for a single individual.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.CounterfactualExplanation-Tuple{AbstractArray, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.CounterfactualExplanation","text":"function CounterfactualExplanation(;\n\tx::AbstractArray,\n\ttarget::RawTargetType,\n\tdata::CounterfactualData,\n\tM::Models.AbstractFittedModel,\n\tgenerator::Generators.AbstractGenerator,\n\tnum_counterfactuals::Int = 1,\n\tinitialization::Symbol = :add_perturbation,\n convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,\n)\n\nOuter method to construct a CounterfactualExplanation structure.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.EncodedOutputArrayType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EncodedOutputArrayType","text":"EncodedOutputArrayType\n\nType of encoded output array.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.EncodedTargetType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EncodedTargetType","text":"EncodedTargetType\n\nType of encoded target variable.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.OutputEncoder","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"OutputEncoder\n\nThe OutputEncoder takes a raw output array (y) and encodes it.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.OutputEncoder-Tuple{Union{Int64, AbstractFloat, String, Symbol}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"(encoder::OutputEncoder)(ynew::RawTargetType)\n\nWhen called on a new value ynew, the OutputEncoder encodes it based on the initial encoding.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.OutputEncoder-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"(encoder::OutputEncoder)()\n\nOn call, the OutputEncoder returns the encoded output array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.EvoTreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EvoTreeModel","text":"EvoTreeModel\n\nExposes the EvoTreeModel from the EvoTreesExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.LaplaceReduxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.LaplaceReduxModel","text":"LaplaceReduxModel\n\nExposes the LaplaceReduxModel from the LaplaceReduxExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.NeuroTreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.NeuroTreeModel","text":"NeuroTreeModel\n\nExposes the NeuroTreeModel from the NeuroTreeExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Base.Iterators.Zip, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Base.Iterators.Zip,\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n kwargs...,\n)\n\nOverloads the generate_counterfactual method to accept a zip of factuals x and return a vector of counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Matrix, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Matrix,\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n num_counterfactuals::Int=1,\n initialization::Symbol=:add_perturbation,\n convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,\n timeout::Union{Nothing,Real}=nothing,\n)\n\nThe core function that is used to run counterfactual search for a given factual x, target, counterfactual data, model and generator. Keywords can be used to specify the desired threshold for the predicted target class probability and the maximum number of iterations.\n\nArguments\n\nx::Matrix: Factual data point.\ntarget::RawTargetType: Target class.\ndata::CounterfactualData: Counterfactual data.\nM::Models.AbstractFittedModel: Fitted model.\ngenerator::AbstractGenerator: Generator.\nnum_counterfactuals::Int=1: Number of counterfactuals to generate for factual.\ninitialization::Symbol=:add_perturbation: Initialization method. By default, the initialization is done by adding a small random perturbation to the factual to achieve more robustness.\nconvergence::Union{AbstractConvergence,Symbol}=:decision_threshold: Convergence criterion. By default, the convergence is based on the decision threshold.\ntimeout::Union{Nothing,Int}=nothing: Timeout in seconds.\n\nExamples\n\nGeneric generator\n\nusing CounterfactualExplanations\n\n# Data:\nusing CounterfactualExplanations.Data\nusing Random\nRandom.seed!(1234)\nxs, ys = Data.toy_data_linear()\nX = hcat(xs...)\ncounterfactual_data = CounterfactualData(X,ys')\n\n# Model\nusing CounterfactualExplanations.Models: LogisticModel, probs \n# Logit model:\nw = [1.0 1.0] # true coefficients\nb = 0\nM = LogisticModel(w, [b])\n\n# Randomly selected factual:\nx = select_factual(counterfactual_data,rand(1:size(X)[2]))\ny = round(probs(M, x)[1])\ntarget = round(probs(M, x)[1])==0 ? 1 : 0 \n\n# Counterfactual search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Matrix, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, GrowingSpheresGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Matrix,\n target::RawTargetType,\n data::DataPreprocessing.CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::Generators.GrowingSpheresGenerator;\n num_counterfactuals::Int=1,\n convergence::Union{AbstractConvergence,Symbol}=Convergence.DecisionThresholdConvergence(;\n decision_threshold=(1 / length(data.y_levels)), max_iter=1000\n ),\n kwrgs...,\n)\n\nOverloads the generate_counterfactual for the GrowingSpheresGenerator generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Tuple{AbstractArray}, Vararg{Any}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"Overloads the generate_counterfactual method to accept a tuple containing and array. This allows for broadcasting over Zip iterators.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Vector{<:Matrix}, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Vector{<:Matrix},\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n kwargs...,\n)\n\nOverloads the generate_counterfactual method to accept a vector of factuals x and return a vector of counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.get_target_index-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.get_target_index","text":"get_target_index(y_levels, target)\n\nUtility that returns the index of target in y_levels.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.path","text":"path(ce::CounterfactualExplanation)\n\nA convenience method that returns the entire counterfactual path.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.target_probs","page":"๐Ÿง Reference","title":"CounterfactualExplanations.target_probs","text":"target_probs(\n ce::CounterfactualExplanation,\n x::Union{AbstractArray,Nothing}=nothing,\n)\n\nReturns the predicted probability of the target class for x. If x is nothing, the predicted probability corresponding to the counterfactual value is returned.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.terminated-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.terminated","text":"terminated(ce::CounterfactualExplanation)\n\nA convenience method that checks if the counterfactual search has terminated.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.total_steps-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.total_steps","text":"total_steps(ce::CounterfactualExplanation)\n\nA convenience method that returns the total number of steps of the counterfactual search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.update!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.update!","text":"update!(ce::CounterfactualExplanation)\n\nAn important subroutine that updates the counterfactual explanation. It takes a snapshot of the current counterfactual search state and passes it to the generator. Based on the current state the generator generates perturbations. Various constraints are then applied to the proposed vector of feature perturbations. Finally, the counterfactual search state is updated.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.convergence_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.convergence_catalogue","text":"convergence_catalogue\n\nA dictionary containing all convergence criteria.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.DecisionThresholdConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::DecisionThresholdConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is the decision threshold.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.GeneratorConditionsConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::GeneratorConditionsConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is generator_conditions.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.InvalidationRateConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::InvalidationRateConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is invalidation rate.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.MaxIterConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::MaxIterConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is maximum iterations. This means the counterfactual search will not terminate until the maximum number of iterations has been reached independently of the other convergence criteria.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.get_convergence_type-Tuple{AbstractConvergence, AbstractVector}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.get_convergence_type","text":"get_convergence_type(convergence::AbstractConvergence)\n\nReturns the convergence object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.get_convergence_type-Tuple{Symbol, AbstractVector}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.get_convergence_type","text":"get_convergence_type(convergence::Symbol)\n\nReturns the convergence object from the dictionary of default convergence types.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.hinge_loss-Tuple{CounterfactualExplanations.Convergence.InvalidationRateConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.hinge_loss","text":"hinge_loss(convergence::InvalidationRateConvergence, ce::AbstractCounterfactualExplanation)\n\nCalculates the hinge loss of a counterfactual explanation.\n\nArguments\n\nconvergence::InvalidationRateConvergence: The convergence criterion to use.\nce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the hinge loss for.\n\nReturns\n\nThe hinge loss of the counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.invalidation_rate-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.invalidation_rate","text":"invalidation_rate(ce::AbstractCounterfactualExplanation)\n\nCalculates the invalidation rate of a counterfactual explanation.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the invalidation rate for.\nkwargs: Additional keyword arguments to pass to the function.\n\nReturns\n\nThe invalidation rate of the counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.threshold_reached-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.threshold_reached","text":"threshold_reached(ce::CounterfactualExplanation)\n\nDetermines if the predefined threshold for the target class probability has been reached.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.default_measures","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.default_measures","text":"The default evaluation measures.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Evaluation.Benchmark","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.Benchmark","text":"A container for benchmarks of counterfactual explanations. Instead of subtyping DataFrame, it contains a DataFrame of evaluation measures (see this discussion for why we don't subtype DataFrame directly).\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Evaluation.Benchmark-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.Benchmark","text":"(bmk::Benchmark)(; agg=mean)\n\nReturns a DataFrame containing evaluation measures aggregated by num_counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n data::CounterfactualData;\n models::Dict{<:Any,<:Any}=standard_models_catalogue,\n generators::Union{Nothing,Dict{<:Any,<:AbstractGenerator}}=nothing,\n measure::Union{Function,Vector{Function}}=default_measures,\n n_individuals::Int=5,\n suppress_training::Bool=false,\n factual::Union{Nothing,RawTargetType}=nothing,\n target::Union{Nothing,RawTargetType}=nothing,\n store_ce::Bool=false,\n parallelizer::Union{Nothing,AbstractParallelizer}=nothing,\n kwrgs...,\n)\n\nRuns the benchmarking exercise as follows:\n\nRandomly choose a factual and target label unless specified. \nIf no pretrained models are provided, it is assumed that a dictionary of callable model objects is provided (by default using the standard_models_catalogue). \nEach of these models is then trained on the data. \nFor each model separately choose n_individuals randomly from the non-target (factual) class. For each generator create a benchmark as in benchmark(xs::Union{AbstractArray,Base.Iterators.Zip}).\nFinally, concatenate the results.\n\nIf vertical_splits is specified to an integer, the computations are split vertically into vertical_splits chunks. In this case, the results are stored in a temporary directory and concatenated afterwards. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{Union{AbstractArray, Base.Iterators.Zip}, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n x::Union{AbstractArray,Base.Iterators.Zip},\n target::RawTargetType,\n data::CounterfactualData;\n models::Dict{<:Any,<:AbstractFittedModel},\n generators::Dict{<:Any,<:AbstractGenerator},\n measure::Union{Function,Vector{Function}}=default_measures,\n xids::Union{Nothing,AbstractArray}=nothing,\n dataname::Union{Nothing,Symbol,String}=nothing,\n verbose::Bool=true,\n store_ce::Bool=false,\n parallelizer::Union{Nothing,AbstractParallelizer}=nothing,\n kwrgs...,\n)\n\nFirst generates counterfactual explanations for factual x, the target and data using each of the provided models and generators. Then generates a Benchmark for the vector of counterfactual explanations as in benchmark(counterfactual_explanations::Vector{CounterfactualExplanation}).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{Vector{CounterfactualExplanation}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n counterfactual_explanations::Vector{CounterfactualExplanation};\n meta_data::Union{Nothing,<:Vector{<:Dict}}=nothing,\n measure::Union{Function,Vector{Function}}=default_measures,\n store_ce::Bool=false,\n)\n\nGenerates a Benchmark for a vector of counterfactual explanations. Optionally meta_data describing each individual counterfactual explanation can be supplied. This should be a vector of dictionaries of the same length as the vector of counterfactuals. If no meta_data is supplied, it will be automatically inferred. All measure functions are applied to each counterfactual explanation. If store_ce=true, the counterfactual explanations are stored in the benchmark.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.evaluate","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.evaluate","text":"evaluate(\n ce::CounterfactualExplanation;\n measure::Union{Function,Vector{Function}}=default_measures,\n agg::Function=mean,\n report_each::Bool=false,\n output_format::Symbol=:Vector,\n pivot_longer::Bool=true\n)\n\nJust computes evaluation measures for the counterfactual explanation. By default, no meta data is reported. For report_meta=true, meta data is automatically inferred, unless this overwritten by meta_data. The optional meta_data argument should be a vector of dictionaries of the same length as the vector of counterfactual explanations. \n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Evaluation.redundancy-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.redundancy","text":"redundancy(ce::CounterfactualExplanation)\n\nComputes the feature redundancy: that is, the number of features that remain unchanged from their original, factual values.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.validity-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.validity","text":"validity(ce::CounterfactualExplanation; ฮณ=0.5)\n\nChecks of the counterfactual search has been successful with respect to the probability threshold ฮณ. In case multiple counterfactuals were generated, the function returns the proportion of successful counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.CounterfactualData-Tuple{AbstractMatrix, Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.CounterfactualData","text":"CounterfactualData(\n X::AbstractMatrix, y::AbstractMatrix;\n mutability::Union{Vector{Symbol},Nothing}=nothing,\n domain::Union{Any,Nothing}=nothing,\n features_categorical::Union{Vector{Int},Nothing}=nothing,\n features_continuous::Union{Vector{Int},Nothing}=nothing,\n standardize::Bool=false\n)\n\nThis outer constructor method prepares features X and labels y to be used with the package. Mutability and domain constraints can be added for the features. The function also accepts arguments that specify which features are categorical and which are continues. These arguments are currently not used. \n\nExamples\n\nusing CounterfactualExplanations.Data\nx, y = toy_data_linear()\nX = hcat(x...)\ncounterfactual_data = CounterfactualData(X,y')\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.CounterfactualData-Tuple{Tables.MatrixTable, Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.CounterfactualData","text":"function CounterfactualData(\n X::Tables.MatrixTable,\n y::RawOutputArrayType;\n kwrgs...\n)\n\nOuter constructor method that accepts a Tables.MatrixTable. By default, the indices of categorical and continuous features are automatically inferred the features' scitype.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.apply_domain_constraints-Tuple{CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.apply_domain_constraints","text":"apply_domain_constraints(counterfactual_data::CounterfactualData, x::AbstractArray)\n\nA subroutine that is used to apply the predetermined domain constraints.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.select_factual-Tuple{CounterfactualData, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.select_factual","text":"select_factual(counterfactual_data::CounterfactualData, index::Int)\n\nA convenience method that can be used to access the feature matrix.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.select_factual-Tuple{CounterfactualData, Union{UnitRange{Int64}, Vector{Int64}}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.select_factual","text":"select_factual(counterfactual_data::CounterfactualData, index::Union{Vector{Int},UnitRange{Int}})\n\nA convenience method that can be used to access the feature matrix.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData, dt::Any)\n\nBy default, all continuous features are transformable. This function returns the indices of all continuous features.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData, StatsBase.ZScoreTransform}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData, dt::ZScoreTransform)\n\nReturns the indices of all continuous features that can be transformed. For constant features ZScoreTransform returns NaN.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData)\n\nDispatches the transformable_features function to the appropriate method based on the type of the dt field.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.all_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.all_models_catalogue","text":"all_models_catalogue\n\nA dictionary containing both differentiable and non-differentiable machine learning models.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.mlj_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.mlj_models_catalogue","text":"mlj_models_catalogue\n\nA dictionary containing all machine learning models from the MLJ model registry that the package supports.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.standard_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.standard_models_catalogue","text":"standard_models_catalogue\n\nA dictionary containing all differentiable machine learning models.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.AbstractDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractDifferentiableModel","text":"Base type for differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxEnsemble","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxEnsemble","text":"FluxEnsemble <: AbstractFluxModel\n\nConstructor for deep ensembles trained in Flux.jl. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxModel","text":"FluxModel <: AbstractFluxModel\n\nConstructor for models trained in Flux.jl. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxModel","text":"FluxModel(data::CounterfactualData; kwargs...)\n\nConstructs a multi-layer perceptron (MLP).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.DecisionTreeModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.DecisionTreeModel","text":"DecisionTreeModel(data::CounterfactualData; kwargs...)\n\nConstructs a new TreeModel object wrapped around a decision tree from the data in a CounterfactualData object. Not called by the user directly.\n\nArguments\n\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nmodel::TreeModel: A TreeModel object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.Linear-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.Linear","text":"Linear(data::CounterfactualData; kwargs...)\n\nConstructs a model with one linear layer. If the output is binary, this corresponds to logistic regression, since model outputs are passed through the sigmoid function. If the output is multi-class, this corresponds to multinomial logistic regression, since model outputs are passed through the softmax function.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.RandomForestModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.RandomForestModel","text":"RandomForestModel(data::CounterfactualData; kwargs...)\n\nConstructs a new TreeModel object wrapped around a random forest from the data in a CounterfactualData object. Not called by the user directly.\n\nArguments\n\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nmodel::TreeModel: A TreeModel object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.fit_model","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.fit_model","text":"fit_model(\n counterfactual_data::CounterfactualData, model::Symbol=:MLP;\n kwrgs...\n)\n\nFits one of the available default models to the counterfactual_data. The model argument can be used to specify the desired model. The available values correspond to the keys of the all_models_catalogue dictionary.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Models.logits-Tuple{AbstractFittedModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.logits","text":"logits(M::AbstractFittedModel, X::AbstractArray)\n\nGeneric method that is compulsory for all models. It returns the raw model predictions. In classification this is sometimes referred to as logits: the non-normalized predictions that are fed into a link function to produce predicted probabilities. In regression (not currently implemented) raw outputs typically correspond to final outputs. In other words, there is typically no normalization involved.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.logits-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.logits","text":"logits(M::TreeModel, X::AbstractArray)\n\nCalculates the logit scores output by the model M for the input data X.\n\nArguments\n\nM::TreeModel: The model selected by the user.\nX::AbstractArray: The feature vector for which the logit scores are calculated.\n\nReturns\n\nlogits::Matrix: A matrix of logits for each output class for each data point in X.\n\nExample\n\nlogits = Models.logits(M, x) # calculates the logit scores for each output class for the data point x\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.model_evaluation-Tuple{AbstractFittedModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.model_evaluation","text":"model_evaluation(M::AbstractFittedModel, test_data::CounterfactualData)\n\nHelper function to compute F-Score for AbstractFittedModel on a (test) data set. By default, it computes the accuracy. Any other measure, e.g. from the StatisticalMeasures package, can be passed as an argument. Currently, only measures applicable to classification tasks are supported.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{AbstractFittedModel, CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::AbstractArray)\n\nReturns the predicted output label for a given model M, data set counterfactual_data and input data X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{AbstractFittedModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData)\n\nReturns the predicted output labels for all data points of data set counterfactual_data for a given model M.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::TreeModel, X::AbstractArray)\n\nReturns the predicted label for X.\n\nArguments\n\nM::TreeModel: The model selected by the user.\nX::AbstractArray: The input array for which the label is predicted.\n\nReturns\n\nlabels::AbstractArray: The predicted label for each data point in X.\n\nExample\n\nlabel = Models.predict_label(M, x) # returns the predicted label for each data point in x\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_proba-Tuple{AbstractFittedModel, Union{Nothing, CounterfactualData}, Union{Nothing, AbstractArray}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_proba","text":"predict_proba(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::Union{Nothing,AbstractArray})\n\nReturns the predicted output probabilities for a given model M, data set counterfactual_data and input data X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{AbstractFittedModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::AbstractFittedModel, X::AbstractArray)\n\nGeneric method that is compulsory for all models. It returns the normalized model predictions, so the predicted probabilities in the case of classification. In regression (not currently implemented) this method is redundant. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractMatrix{<:Number}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::TreeModel, X::AbstractArray{<:Number, 2})\n\nCalculates the probability scores for each output class for the two-dimensional input data matrix X.\n\nArguments\n\nM::TreeModel: The TreeModel.\nX::AbstractArray: The feature vector for which the predictions are made.\n\nReturns\n\np::Matrix: A matrix of probability scores for each output class for each data point in X.\n\nExample\n\nprobabilities = Models.probs(M, X) # calculates the probability scores for each output class for each data point in X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractVector{<:Number}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::TreeModel, X::AbstractArray{<:Number, 1})\n\nWorks the same way as the probs(M::TreeModel, X::AbstractArray{<:Number, 2}) method above, but handles 1-dimensional rather than 2-dimensional input data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generator_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generator_catalogue","text":"A dictionary containing the constructors of all available counterfactual generators.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Generators.AbstractGradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.AbstractGradientBasedGenerator","text":"AbstractGradientBasedGenerator\n\nAn abstract type that serves as the base type for gradient-based counterfactual generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.AbstractNonGradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.AbstractNonGradientBasedGenerator","text":"AbstractNonGradientBasedGenerator\n\nAn abstract type that serves as the base type for non gradient-based counterfactual generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.FeatureTweakGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.FeatureTweakGenerator","text":"Feature Tweak counterfactual generator class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.FeatureTweakGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.FeatureTweakGenerator","text":"FeatureTweakGenerator(; penalty::Union{Nothing,Function,Vector{Function}}=Objectives.distance_l2, ฯต::AbstractFloat=0.1)\n\nConstructs a new Feature Tweak Generator object.\n\nUses the L2-norm as the penalty to measure the distance between the counterfactual and the factual. According to the paper by Tolomei et al., another recommended choice for the penalty in addition to the L2-norm is the L0-norm. The L0-norm simply minimizes the number of features that are changed through the tweak.\n\nArguments\n\npenalty::Union{Nothing,Function,Vector{Function}}: The penalty function to use for the generator. Defaults to distance_l2.\nฯต::AbstractFloat: The tolerance value for the feature tweaks. Described at length in Tolomei et al. (https://arxiv.org/pdf/1706.06691.pdf). Defaults to 0.1.\n\nReturns\n\ngenerator::FeatureTweakGenerator: A non-gradient-based generator that can be used to generate counterfactuals using the feature tweak method.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GradientBasedGenerator","text":"Base class for gradient-based counterfactual generators.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.GradientBasedGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GradientBasedGenerator","text":"GradientBasedGenerator(;\n\tloss::Union{Nothing,Function}=nothing,\n\tpenalty::Penalty=nothing,\n\tฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing,\n\tlatent_space::Bool::false,\n\topt::Flux.Optimise.AbstractOptimiser=Flux.Descent(),\n generative_model_params::NamedTuple=(;),\n)\n\nDefault outer constructor for GradientBasedGenerator.\n\nArguments\n\nloss::Union{Nothing,Function}=nothing: The loss function used by the model.\npenalty::Penalty=nothing: A penalty function for the generator to penalize counterfactuals too far from the original point.\nฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing: The weight of the penalty function.\nlatent_space::Bool=false: Whether to use the latent space of a generative model to generate counterfactuals.\nopt::Flux.Optimise.AbstractOptimiser=Flux.Descent(): The optimizer to use for the generator.\ngenerative_model_params::NamedTuple: The parameters of the generative model associated with the generator.\n\nReturns\n\ngenerator::GradientBasedGenerator: A gradient-based counterfactual generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GrowingSpheresGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GrowingSpheresGenerator","text":"Growing Spheres counterfactual generator class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.GrowingSpheresGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GrowingSpheresGenerator","text":"GrowingSpheresGenerator(; n::Int=100, ฮท::Float64=0.1, kwargs...)\n\nConstructs a new Growing Spheres Generator object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.JSMADescent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.JSMADescent","text":"An optimisation rule that can be used to implement a Jacobian-based Saliency Map Attack.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.JSMADescent-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.JSMADescent","text":"Outer constructor for the JSMADescent rule.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.CLUEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.CLUEGenerator","text":"Constructor for CLUEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.ClaPROARGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.ClaPROARGenerator","text":"Constructor for ClaPGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.DiCEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.DiCEGenerator","text":"Constructor for DiCEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GenericGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GenericGenerator","text":"Constructor for GenericGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GravitationalGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GravitationalGenerator","text":"Constructor for GravitationalGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GreedyGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GreedyGenerator","text":"Constructor for GreedyGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.ProbeGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.ProbeGenerator","text":"Constructor for ProbeGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.REVISEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.REVISEGenerator","text":"Constructor for REVISEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.WachterGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.WachterGenerator","text":"Constructor for WachterGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.conditions_satisfied-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.conditions_satisfied","text":"conditions_satisfied(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to check if the all conditions for convergence of the counterfactual search have been satisified for gradient-based generators. By default, gradient-based search is considered to have converged as soon as the proposed feature changes for all features are smaller than one percent of its standard deviation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.feature_tweaking!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.feature_tweaking!","text":"feature_tweaking!(ce::AbstractCounterfactualExplanation)\n\nReturns a counterfactual instance of ce.x based on the ensemble of classifiers provided.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nReturns\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nExample\n\nce = feature_tweaking!(ce) # returns a counterfactual inside the ce.sโ€ฒ field based on the ensemble of classifiers provided\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generate_perturbations-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generate_perturbations","text":"generate_perturbations(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to generate feature perturbations for gradient-based generators through simple gradient descent.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generate_perturbations-Tuple{FeatureTweakGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generate_perturbations","text":"generate_perturbations(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to generate feature perturbations for gradient-based generators through simple gradient descent.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.hinge_loss-Tuple{AbstractConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.hinge_loss","text":"hinge_loss(convergence::AbstractConvergence, ce::AbstractCounterfactualExplanation)\n\nThe default hinge loss for any convergence criterion. Can be overridden inside the Convergence module as part of the definition of specific convergence criteria.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.@objective-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@objective","text":"objective(generator, ex)\n\nA macro that can be used to define the counterfactual search objective.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@search_feature_space-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@search_feature_space","text":"search_feature_space(generator)\n\nA simple macro that can be used to specify feature space search.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@search_latent_space-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@search_latent_space","text":"search_latent_space(generator)\n\nA simple macro that can be used to specify latent space search.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@with_optimiser-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@with_optimiser","text":"with_optimiser(generator, optimiser)\n\nA simple macro that can be used to specify the optimiser to be used.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Objectives.ddp_diversity-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.ddp_diversity","text":"ddp_diversity(\n ce::AbstractCounterfactualExplanation;\n perturbation_size=1e-5\n)\n\nEvaluates how diverse the counterfactuals are using a Determinantal Point Process (DDP).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance","text":"distance(ce::AbstractCounterfactualExplanation, p::Real=2)\n\nComputes the distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l0-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l0","text":"distance_l0(ce::AbstractCounterfactualExplanation)\n\nComputes the L0 distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l1-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l1","text":"distance_l1(ce::AbstractCounterfactualExplanation)\n\nComputes the L1 distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l2-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l2","text":"distance_l2(ce::AbstractCounterfactualExplanation)\n\nComputes the L2 (Euclidean) distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_linf-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_linf","text":"distance_linf(ce::AbstractCounterfactualExplanation)\n\nComputes the L-inf distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_mad-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_mad","text":"distance_mad(ce::AbstractCounterfactualExplanation; agg=mean)\n\nThis is the distance measure proposed by Wachter et al. (2017).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.predictive_entropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.predictive_entropy","text":"predictive_entropy(ce::AbstractCounterfactualExplanation; agg=Statistics.mean)\n\nComputes the predictive entropy of the counterfactuals. Explained in https://arxiv.org/abs/1406.2541.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.logitbinarycrossentropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.logitbinarycrossentropy","text":"Flux.Losses.logitbinarycrossentropy(ce::AbstractCounterfactualExplanation)\n\nSimply extends the logitbinarycrossentropy method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.logitcrossentropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.logitcrossentropy","text":"Flux.Losses.logitcrossentropy(ce::AbstractCounterfactualExplanation)\n\nSimply extends the logitcrossentropy method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.mse-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.mse","text":"Flux.Losses.mse(ce::AbstractCounterfactualExplanation)\n\nSimply extends the mse method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Internal-functions","page":"๐Ÿง Reference","title":"Internal functions","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Modules = [\n CounterfactualExplanations, \n CounterfactualExplanations.Convergence,\n CounterfactualExplanations.Evaluation,\n CounterfactualExplanations.DataPreprocessing,\n CounterfactualExplanations.Models, \n CounterfactualExplanations.GenerativeModels,\n CounterfactualExplanations.Generators, \n CounterfactualExplanations.Objectives\n]\nPublic = false","category":"page"},{"location":"reference/#CounterfactualExplanations.FluxModelParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.FluxModelParams","text":"FluxModelParams\n\nDefault MLP training parameters.\n\n\n\n\n\n","category":"type"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{AbstractFittedModel}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat AbstractFittedModel as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{AbstractGenerator}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat AbstractGenerator as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.adjust_shape!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.adjust_shape!","text":"adjust_shape!(ce::CounterfactualExplanation)\n\nA convenience method that adjusts the dimensions of the counterfactual state and related fields.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.adjust_shape-Tuple{CounterfactualExplanation, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.adjust_shape","text":"adjust_shape(\n ce::CounterfactualExplanation, \n x::AbstractArray\n)\n\nA convenience method that adjusts the dimensions of x.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.apply_domain_constraints!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.apply_domain_constraints!","text":"apply_domain_constraints!(ce::CounterfactualExplanation)\n\nWrapper function that applies underlying domain constraints.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.apply_mutability-Tuple{CounterfactualExplanation, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.apply_mutability","text":"apply_mutability(\n ce::CounterfactualExplanation,\n ฮ”sโ€ฒ::AbstractArray,\n)\n\nA subroutine that applies mutability constraints to the proposed vector of feature perturbations.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual","text":"counterfactual(ce::CounterfactualExplanation)\n\nA convenience method that returns the counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_label-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_label","text":"counterfactual_label(ce::CounterfactualExplanation)\n\nA convenience method that returns the predicted label of the counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_label_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_label_path","text":"counterfactual_label_path(ce::CounterfactualExplanation)\n\nReturns the counterfactual labels for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_probability","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_probability","text":"counterfactual_probability(ce::CounterfactualExplanation)\n\nA convenience method that computes the class probabilities of the counterfactual.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.counterfactual_probability_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_probability_path","text":"counterfactual_probability_path(ce::CounterfactualExplanation)\n\nReturns the counterfactual probabilities for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_array-Tuple{MultivariateStats.AbstractDimensionalityReduction, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_array","text":"decode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)\n\nHelper function to decode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_array-Tuple{StatsBase.AbstractDataTransform, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_array","text":"decode_array(dt::StatsBase.AbstractDataTransform, x::AbstractArray)\n\nHelper function to decode an array x using a data transform dt::StatsBase.AbstractDataTransform.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_state","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_state","text":"function decode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )\n\nApplies all the applicable decoding functions:\n\nIf applicable, map the state variable back from the latent space to the feature space.\nIf and where applicable, inverse-transform features.\nReconstruct all categorical encodings.\n\nFinally, the decoded counterfactual is returned.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.encode_array-Tuple{MultivariateStats.AbstractDimensionalityReduction, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_array","text":"encode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)\n\nHelper function to encode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.encode_array-Tuple{StatsBase.AbstractDataTransform, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_array","text":"encode_array(dt::StatsBase.AbstractDataTransform, x::AbstractArray)\n\nHelper function to encode an array x using a data transform dt::StatsBase.AbstractDataTransform.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.encode_state","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_state","text":"function encode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing} = nothing, )\n\nApplies all required encodings to x:\n\nIf applicable, it maps x to the latent space learned by the generative model.\nIf and where applicable, it rescales features. \n\nFinally, it returns the encoded state variable.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.factual-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual","text":"factual(ce::CounterfactualExplanation)\n\nA convenience method to retrieve the factual x.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.factual_label-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual_label","text":"factual_label(ce::CounterfactualExplanation)\n\nA convenience method to get the predicted label associated with the factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.factual_probability-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual_probability","text":"factual_probability(ce::CounterfactualExplanation)\n\nA convenience method to compute the class probabilities of the factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.find_potential_neighbours-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.find_potential_neighbours","text":"find_potential_neighbors(ce::AbstractCounterfactualExplanation)\n\nFinds potential neighbors for the selected factual data point.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.get_meta-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.get_meta","text":"get_meta(ce::CounterfactualExplanation)\n\nReturns meta data for a counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.guess_likelihood-Tuple{Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.guess_likelihood","text":"guess_likelihood(y::RawOutputArrayType)\n\nGuess the likelihood based on the scientific type of the output array. Returns a symbol indicating the guessed likelihood and the scientific type of the output array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.guess_loss-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.guess_loss","text":"guess_loss(ce::CounterfactualExplanation)\n\nGuesses the loss function to be used for the counterfactual search in case likelihood field is specified for the AbstractFittedModel instance and no loss function was explicitly declared for AbstractGenerator instance.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.in_target_class-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.in_target_class","text":"in_target_class(ce::CounterfactualExplanation)\n\nCheck if the counterfactual is in the target class.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.initialize_state-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.initialize_state","text":"initialize_state(ce::CounterfactualExplanation)\n\nInitializes the starting point for the factual(s):\n\nIf ce.initialization is set to :identity or counterfactuals are searched in a latent space, then nothing is done.\nIf ce.initialization is set to :add_perturbation, then a random perturbation is added to the factual following following Slack (2021): https://arxiv.org/abs/2106.02666. The authors show that this improves adversarial robustness.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.map_from_latent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.map_from_latent","text":"map_from_latent(\n ce::CounterfactualExplanation,\n x::Union{AbstractArray,Nothing}=nothing,\n)\n\nMaps the state variable back from the latent space to the feature space.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.map_to_latent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.map_to_latent","text":"function maptolatent( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, ) \n\nMaps x from the feature space mathcalX to the latent space learned by the generative model.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.output_dim-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.output_dim","text":"output_dim(ce::CounterfactualExplanation)\n\nA convenience method that returns the output dimension of the predictive model.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.reset!-Tuple{CounterfactualExplanations.FluxModelParams}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.reset!","text":"reset!(flux_training_params::FluxModelParams)\n\nRestores the default parameter values.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.steps_exhausted-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.steps_exhausted","text":"steps_exhausted(ce::CounterfactualExplanation)\n\nA convenience method that checks if the number of maximum iterations has been exhausted.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.target_probs_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.target_probs_path","text":"target_probs_path(ce::CounterfactualExplanation)\n\nReturns the target probabilities for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.distance_measures","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.distance_measures","text":"All distance measures.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#Base.vcat-Tuple{CounterfactualExplanations.Evaluation.Benchmark, CounterfactualExplanations.Evaluation.Benchmark}","page":"๐Ÿง Reference","title":"Base.vcat","text":"Base.vcat(bmk1::Benchmark, bmk2::Benchmark)\n\nVertically concatenates two Benchmark objects.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.compute_measure-Tuple{CounterfactualExplanation, Function, Function}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.compute_measure","text":"compute_measure(ce::CounterfactualExplanation, measure::Function, agg::Function)\n\nComputes a single measure for a counterfactual explanation. The measure is applied to the counterfactual explanation ce and aggregated using the aggregation function agg.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.to_dataframe-Tuple{Vector, Any, Bool, Bool, Bool, CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.to_dataframe","text":"evaluate_dataframe(\n ce::CounterfactualExplanation,\n measure::Vector{Function},\n agg::Function,\n report_each::Bool,\n pivot_longer::Bool,\n store_ce::Bool,\n)\n\nEvaluates a counterfactual explanation and returns a dataframe of evaluation measures.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.validity_strict-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.validity_strict","text":"validity_strict(ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has been strictly valid in the sense that it has converged with respect to the pre-specified target probability ฮณ.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat CounterfactualData as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing._subset-Tuple{CounterfactualData, Vector{Int64}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing._subset","text":"_subset(data::CounterfactualData, idx::Vector{Int})\n\nCreates a subset of the data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.convert_to_1d-Tuple{Matrix, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.convert_to_1d","text":"convert_to_1d(y::Matrix, y_levels::AbstractArray)\n\nHelper function to convert a one-hot encoded matrix to a vector of labels. This is necessary because MLJ models require the labels to be represented as a vector, but the synthetic datasets in this package hold the labels in one-hot encoded form.\n\nArguments\n\ny::Matrix: The one-hot encoded matrix.\ny_levels::AbstractArray: The levels of the categorical variable.\n\nReturns\n\nlabels: A vector of labels.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.get_generative_model-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.get_generative_model","text":"get_generative_model(counterfactual_data::CounterfactualData)\n\nReturns the underlying generative model. If there is no existing model available, the default generative model (VAE) is used. Otherwise it is expected that existing generative model has been pre-trained or else a warning is triggered.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.has_pretrained_generative_model-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.has_pretrained_generative_model","text":"has_pretrained_generative_model(counterfactual_data::CounterfactualData)\n\nChecks if generative model is present and trained.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.input_dim-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.input_dim","text":"input_dim(counterfactual_data::CounterfactualData)\n\nHelper function that returns the input dimension (number of features) of the data. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.mutability_constraints-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.mutability_constraints","text":"mutability_constraints(counterfactual_data::CounterfactualData)\n\nA convenience function that returns the mutability constraints. If none were specified, it is assumed that all features are mutable in :both directions.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj","text":"preprocess_data_for_mlj(data::CounterfactualData)\n\nHelper function to preprocess data::CounterfactualData for MLJ models.\n\nArguments\n\ndata::CounterfactualData: The data to be preprocessed.\n\nReturns\n\n(df_x, y): A tuple containing the preprocessed data, with df_x being a DataFrame object and y being a categorical vector.\n\nExample\n\nX, y = preprocessdatafor_mlj(data)\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.reconstruct_cat_encoding-Tuple{CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.reconstruct_cat_encoding","text":"reconstruct_cat_encoding(counterfactual_data::CounterfactualData, x::Vector)\n\nReconstruct the categorical encoding for a single instance. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.subsample-Tuple{CounterfactualData, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.subsample","text":"subsample(data::CounterfactualData, n::Int)\n\nHelper function to randomly subsample data::CounterfactualData.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.train_test_split-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.train_test_split","text":"train_test_split(data::CounterfactualData;test_size=0.2,keep_class_ratio=false)\n\nSplits data into train and test split.\n\nArguments\n\ndata::CounterfactualData: The data to be preprocessed.\ntest_size=0.2: Proportion of the data to be used for testing. \nkeep_class_ratio=false: Decides whether to sample equally from each class, or keep their relative size.\n\nReturns\n\n(train_data::CounterfactualData, test_data::CounterfactualData): A tuple containing the train and test splits.\n\nExample\n\ntrain, test = traintestsplit(data, testsize=0.1, keepclass_ratio=true)\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.unpack_data-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.unpack_data","text":"unpack_data(data::CounterfactualData)\n\nHelper function that unpacks data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.AbstractCustomDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractCustomDifferentiableModel","text":"Base type for custom differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractFluxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractFluxModel","text":"Base type for differentiable models written in Flux.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractMLJModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractMLJModel","text":"Base type for differentiable models from the MLJ library.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractNonDifferentiableJuliaModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractNonDifferentiableJuliaModel","text":"Base type for non-differentiable models written in pure Julia.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractNonDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractNonDifferentiableModel","text":"Base type for non-differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxEnsembleParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxEnsembleParams","text":"FluxModelParams\n\nDefault Deep Ensemble training parameters.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.TreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.TreeModel","text":"TreeModel <: AbstractNonDifferentiableJuliaModel\n\nConstructor for tree-based models from the MLJ library. \n\nArguments\n\nmodel::Any: The model selected by the user. Must be a model from the MLJ library.\nlikelihood::Symbol: The likelihood of the model. Must be one of [:classification_binary, :classification_multi].\n\nReturns\n\nTreeModel: A tree-based model from the MLJ library wrapped inside the TreeModel class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.TreeModel-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.TreeModel","text":"Outer constructor method for TreeModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.binary_to_onehot-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.binary_to_onehot","text":"binary_to_onehot(p)\n\nHelper function to turn dummy-encoded variable into onehot-encoded variable.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.build_ensemble-Tuple{Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.build_ensemble","text":"build_ensemble(K::Int;kw=(input_dim=2,n_hidden=32,output_dim=1))\n\nHelper function that builds an ensemble of K models.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.build_mlp-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.build_mlp","text":"build_mlp()\n\nHelper function to build simple MLP.\n\nExamples\n\nnn = build_mlp()\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.data_loader-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.data_loader","text":"data_loader(data::CounterfactualData)\n\nPrepares counterfactual data for training in Flux.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.get_individual_classifiers-Tuple{CounterfactualExplanations.Models.TreeModel}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.get_individual_classifiers","text":"get_individual_classifiers(M::TreeModel)\n\nReturns the individual classifiers in the forest. If the input is a decision tree, the method returns the decision tree itself inside an array.\n\nArguments\n\nM::TreeModel: The model selected by the user.\n\nReturns\n\nclassifiers::AbstractArray: An array of individual classifiers in the forest.\n\nExample\n\nclassifiers = Models.getindividualclassifiers(M) # returns the individual classifiers in the forest\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{CounterfactualExplanations.Models.TreeModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::TreeModel, data::CounterfactualData; kwargs...)\n\nFits the model M to the data in the CounterfactualData object. This method is not called by the user directly.\n\nArguments\n\nM::TreeModel: The wrapper for a TreeModel.\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nM::TreeModel: The fitted TreeModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{FluxEnsemble, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::FluxEnsemble, data::CounterfactualData; kwargs...)\n\nWrapper function to retrain.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{FluxModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::FluxModel, data::CounterfactualData; kwargs...)\n\nWrapper function to retrain FluxModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.AbstractGMParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.AbstractGMParams","text":"Base type of generative model hyperparameter container.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.AbstractGenerativeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.AbstractGenerativeModel","text":"Base type for generative model.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.Encoder","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.Encoder","text":"Encoder\n\nConstructs encoder part of VAE: a simple Flux neural network with one hidden layer and two linear output layers for the first two moments of the latent distribution.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAE","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAE","text":"VAE <: AbstractGenerativeModel\n\nConstructs the Variational Autoencoder. The VAE is a subtype of AbstractGenerativeModel. Any (sub-)type of AbstractGenerativeModel is accepted by latent space generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAE-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAE","text":"VAE(input_dim;kws...)\n\nOuter method for instantiating a VAE.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAEParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAEParams","text":"VAEParams <: AbstractGMParams\n\nThe default VAE parameters describing both the encoder/decoder architecture and the training process.\n\n\n\n\n\n","category":"type"},{"location":"reference/#Base.rand","page":"๐Ÿง Reference","title":"Base.rand","text":"Random.rand(encoder::Encoder, x, device=cpu)\n\nDraws random samples from the latent distribution.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.Decoder-Tuple{Int64, Int64, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.Decoder","text":"Decoder(input_dim::Int, latent_dim::Int, hidden_dim::Int; activation=relu)\n\nThe default decoder architecture is just a Flux Chain with one hidden layer and a linear output layer. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.get_data-Tuple{AbstractArray, AbstractArray, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.get_data","text":"get_data(X::AbstractArray, y::AbstractArray, batch_size)\n\nPreparing data for mini-batch training .\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.reconstruct","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.reconstruct","text":"reconstruct(generative_model::VAE, x, device=cpu)\n\nImplements a full pass of some input x through the VAE: x โ†ฆ xฬ‚.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.reparameterization_trick","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.reparameterization_trick","text":"reparameterization_trick(ฮผ,logฯƒ,device=cpu)\n\nHelper function that implements the reparameterization trick: z โˆผ ๐’ฉ(ฮผ,ฯƒยฒ) โ‡” z=ฮผ + ฯƒ โŠ™ ฮต, ฮต โˆผ ๐’ฉ(0,I).\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.Penalty","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.Penalty","text":"Type union for acceptable argument types for the penalty field of GradientBasedGenerator.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators._replace_nans","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators._replace_nans","text":"_replace_nans(ฮ”sโ€ฒ::AbstractArray, old_new::Pair=(NaN => 0))\n\nHelper function to deal with exploding gradients. This is only a temporary fix and will be improved.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.calculate_delta-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.calculate_delta","text":"calculate_delta(ce::AbstractCounterfactualExplanation, penalty::Vector{Function})\n\nCalculates the penalty for the proposed feature tweak.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nReturns\n\ndelta::Float64: The calculated penalty for the proposed feature tweak.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.esatisfactory_instance-Tuple{FeatureTweakGenerator, AbstractArray, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.esatisfactory_instance","text":"esatisfactory_instance(generator::FeatureTweakGenerator, x::AbstractArray, paths::Dict{String, Dict{String, Any}})\n\nReturns an epsilon-satisfactory counterfactual for x based on the paths provided.\n\nArguments\n\ngenerator::FeatureTweakGenerator: The feature tweak generator.\nx::AbstractArray: The factual instance.\npaths::Dict{String, Dict{String, Any}}: A list of paths to the leaves of the tree to be used for tweaking the feature.\n\nReturns\n\nesatisfactory::AbstractArray: The epsilon-satisfactory instance.\n\nExample\n\nesatisfactory = esatisfactory_instance(generator, x, paths) # returns an epsilon-satisfactory counterfactual for x based on the paths provided\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.feature_selection!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.feature_selection!","text":"feature_selection!(ce::AbstractCounterfactualExplanation)\n\nPerform feature selection to find the dimension with the closest (but not equal) values between the ce.x (factual) and ce.sโ€ฒ (counterfactual) arrays.\n\nArguments\n\nce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.\n\nReturns\n\nnothing\n\nThe function iteratively modifies the ce.sโ€ฒ counterfactual array by updating its elements to match the corresponding elements in the ce.x factual array, one dimension at a time, until the predicted label of the modified ce.sโ€ฒ matches the predicted label of the ce.x array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.find_closest_dimension-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.find_closest_dimension","text":"find_closest_dimension(factual, counterfactual)\n\nFind the dimension with the closest (but not equal) values between the factual and counterfactual arrays.\n\nArguments\n\nfactual: The factual array.\ncounterfactual: The counterfactual array.\n\nReturns\n\nclosest_dimension: The index of the dimension with the closest values.\n\nThe function iterates over the indices of the factual array and calculates the absolute difference between the corresponding elements in the factual and counterfactual arrays. It returns the index of the dimension with the smallest difference, excluding dimensions where the values in factual and counterfactual are equal.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.find_counterfactual-NTuple{4, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.find_counterfactual","text":"find_counterfactual(model, factual_class, counterfactual_data, counterfactual_candidates)\n\nFind the first counterfactual index by predicting labels.\n\nArguments\n\nmodel: The fitted model used for prediction.\ntarget_class: Expected target class.\ncounterfactual_data: Data required for counterfactual generation.\ncounterfactual_candidates: The array of counterfactual candidates.\n\nReturns\n\ncounterfactual: The index of the first counterfactual found.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.growing_spheres_generation!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.growing_spheres_generation!","text":"growing_spheres_generation(ce::AbstractCounterfactualExplanation)\n\nGenerate counterfactual candidates using the growing spheres generation algorithm.\n\nArguments\n\nce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.\n\nReturns\n\nnothing\n\nThis function applies the growing spheres generation algorithm to generate counterfactual candidates. It starts by generating random points uniformly on a sphere, gradually reducing the search space until no counterfactuals are found. Then it expands the search space until at least one counterfactual is found or the maximum number of iterations is reached.\n\nThe algorithm iteratively generates counterfactual candidates and predicts their labels using the model stored in ce.M. It checks if any of the predicted labels are different from the factual class. The process of reducing the search space involves halving the search radius, while the process of expanding the search space involves increasing the search radius.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, ce::AbstractCounterfactualExplanation)\n\nDispatches to the appropriate complexity function for any generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Function, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Function, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Nothing, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Nothing, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where no penalty is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Vector{<:Tuple}, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided with additional keyword arguments.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Vector{Function}, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided with additional keyword arguments.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.hyper_sphere_coordinates-Tuple{Integer, AbstractArray, AbstractFloat, AbstractFloat}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.hyper_sphere_coordinates","text":"hyper_sphere_coordinates(n_search_samples::Int, instance::Vector{Float64}, low::Int, high::Int; p_norm::Int=2)\n\nGenerates candidate counterfactuals using the growing spheres method based on hyper-sphere coordinates.\n\nThe implementation follows the Random Point Picking over a sphere algorithm described in the paper: \"Learning Counterfactual Explanations for Tabular Data\" by Pawelczyk, Broelemann & Kascneci (2020), presented at The Web Conference 2020 (WWW). It ensures that points are sampled uniformly at random using insights from: http://mathworld.wolfram.com/HyperspherePointPicking.html\n\nThe growing spheres method is originally proposed in the paper: \"Comparison-based Inverse Classification for Interpretability in Machine Learning\" by Thibaut Laugel et al (2018), presented at the International Conference on Information Processing and Management of Uncertainty in Knowledge-Based Systems (2018).\n\nArguments\n\nn_search_samples::Int: The number of search samples (int > 0).\ninstance::AbstractArray: The input point array.\nlow::AbstractFloat: The lower bound (float >= 0, l < h).\nhigh::AbstractFloat: The upper bound (float >= 0, h > l).\np_norm::Integer: The norm parameter (int >= 1).\n\nReturns\n\ncandidate_counterfactuals::Array: An array of candidate counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.propose_state-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.propose_state","text":"propose_state(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nProposes new state based on backpropagation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.search_path","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.search_path","text":"search_path(tree::Union{DecisionTree.Leaf, DecisionTree.Node}, target::RawTargetType, path::AbstractArray)\n\nReturn a path index list with the inequality symbols, thresholds and feature indices.\n\nArguments\n\ntree::Union{DecisionTree.Leaf, DecisionTree.Node}: The root node of a decision tree.\ntarget::RawTargetType: The target class.\npath::AbstractArray: A list containing the paths found thus far.\n\nReturns\n\npaths::AbstractArray: A list of paths to the leaves of the tree to be used for tweaking the feature.\n\nExample\n\npaths = search_path(tree, target) # returns a list of paths to the leaves of the tree to be used for tweaking the feature\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, ce::AbstractCounterfactualExplanation)\n\nDispatches to the appropriate loss function for any generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, Function, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, loss::Function, ce::AbstractCounterfactualExplanation)\n\nOverloads the โ„“ function for the case where a single loss function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, Nothing, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, loss::Nothing, ce::AbstractCounterfactualExplanation)\n\nOverloads the โ„“ function for the case where no loss function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‚h-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‚h","text":"โˆ‚h(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the complexity penalty at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access. \n\nIf the penalty is not provided, it returns 0.0. By default, Zygote never works out the gradient for constants and instead returns 'nothing', so we need to add a manual step to override this behaviour. See here: https://discourse.julialang.org/t/zygote-gradient/26715.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‚โ„“-Tuple{AbstractGradientBasedGenerator, AbstractDifferentiableModel, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‚โ„“","text":"โˆ‚โ„“(generator::AbstractGradientBasedGenerator, M::Union{Models.LogisticModel, Models.BayesianLogisticModel}, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the loss function at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‡-Tuple{AbstractGradientBasedGenerator, AbstractDifferentiableModel, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‡","text":"โˆ‡(generator::AbstractGradientBasedGenerator, M::Models.AbstractDifferentiableModel, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the counterfactual search objective for gradient-based generators. It simply computes the weighted sum over partial derivates. It assumes that Zygote.jl has gradient access. If the counterfactual is being generated using Probe, the hinge loss is added to the gradient.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_from_target-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_from_target","text":"distance_from_target(\n ce::AbstractCounterfactualExplanation;\n K::Int=50\n)\n\nComputes the distance of the counterfactual from a point in the target main.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.model_loss_penalty-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.model_loss_penalty","text":"function model_loss_penalty(\n ce::AbstractCounterfactualExplanation;\n agg=mean\n)\n\nAdditional penalty for ClaPROARGenerator.\n\n\n\n\n\n","category":"method"},{"location":"extensions/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"extensions/#Extensions","page":"Overview","title":"โ›“๏ธ Extensions","text":"","category":"section"},{"location":"extensions/","page":"Overview","title":"Overview","text":"In this section, you will find information about package extensions of the CounterfactualExplanations package. Extensions are a relatively new feature of Julia that allows users to conditionally load code based on the presence of other packages. This is useful for creating packages that extend the functionality of other packages, without requiring the user to install the package being extended.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/evaluation/#Performance-Evaluation","page":"Evaluating Explanations","title":"Performance Evaluation","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Now that we know how to generate counterfactual explanations in Julia, you may have a few follow-up questions: How do I know if the counterfactual search has been successful? How good is my counterfactual explanation? What does โ€˜goodโ€™ even mean in this context? In this tutorial, we will see how counterfactual explanations can be evaluated with respect to their performance.","category":"page"},{"location":"tutorials/evaluation/#Default-Measures","page":"Evaluating Explanations","title":"Default Measures","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Numerous evaluation measures for counterfactual explanations have been proposed. In what follows, we will cover some of the most important measures.","category":"page"},{"location":"tutorials/evaluation/#Single-Measure,-Single-Counterfactual","page":"Evaluating Explanations","title":"Single Measure, Single Counterfactual","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"One of the most important measures is validity, which simply determines whether or not a counterfactual explanation x^prime is valid in the sense that it yields the target prediction: M(x^prime)=t. We can evaluate the validity of a single counterfactual explanation ce using the Evaluation.evaluate function as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Evaluation: evaluate, validity\nevaluate(ce; measure=validity)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float64}}:\n [1.0]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"For a single counterfactual explanation, this evaluation measure can only take two values: it is either equal to 1, if the explanation is valid or 0 otherwise. Another important measure is distance, which relates to the distance between the factual x and the counterfactual x^prime. In the context of Algorithmic Recourse, higher distances are typically associated with higher costs to individuals seeking recourse.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Objectives: distance\nevaluate(ce; measure=distance)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float32}}:\n [3.2273161]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, distance computes the L2 (Euclidean) distance.","category":"page"},{"location":"tutorials/evaluation/#Multiple-Measures,-Single-Counterfactual","page":"Evaluating Explanations","title":"Multiple Measures, Single Counterfactual","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"You might be interested in computing not just the L2 distance, but various LP norms. This can be done by supplying a vector of functions to the measure key argument. For convenience, all default distance measures have already been collected in a vector:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Evaluation: distance_measures\ndistance_measures","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"4-element Vector{Function}:\n distance_l0 (generic function with 1 method)\n distance_l1 (generic function with 1 method)\n distance_l2 (generic function with 1 method)\n distance_linf (generic function with 1 method)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"We can use this vector of evaluation measures as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ce; measure=distance_measures)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"4-element Vector{Vector{Float32}}:\n [2.0]\n [3.2273161]\n [2.7737978]\n [2.7285953]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"If no measure is specified, the evaluate method will return all default measures,","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ce)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n [1.0]\n Float32[3.2273161]\n [0.0]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"which include:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"CounterfactualExplanations.Evaluation.default_measures","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Function}:\n validity (generic function with 1 method)\n distance (generic function with 1 method)\n redundancy (generic function with 1 method)","category":"page"},{"location":"tutorials/evaluation/#Multiple-Measures-and-Counterfactuals","page":"Evaluating Explanations","title":"Multiple Measures and Counterfactuals","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"We can also evaluate multiple counterfactual explanations at once:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"generator = DiCEGenerator()\nces = generate_counterfactual(x, target, counterfactual_data, M, generator; num_counterfactuals=5)\nevaluate(ces)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n [1.0]\n Float32[3.1955845]\n [[0.0, 0.0, 0.0, 0.0, 0.0]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, each evaluation measure is aggregated across all counterfactual explanations. To return individual measures for each counterfactual explanation you can specify report_each=true","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n BitVector[[1, 1, 1, 1, 1]]\n Vector{Float32}[[3.3671722, 3.1028512, 3.2829392, 3.0728922, 3.1520686]]\n [[0.0, 0.0, 0.0, 0.0, 0.0]]","category":"page"},{"location":"tutorials/evaluation/#Custom-Measures","page":"Evaluating Explanations","title":"Custom Measures","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"A measure is just a method that takes a CounterfactualExplanation as its only positional argument and agg::Function as a key argument specifying how measures should be aggregated across counterfactuals. Defining custom measures is therefore straightforward. For example, we could define a measure to compute the inverse target probability as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"my_measure(ce::CounterfactualExplanation; agg=mean) = agg(1 .- CounterfactualExplanations.target_probs(ce))\nevaluate(ce; measure=my_measure)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float32}}:\n [0.41711217]","category":"page"},{"location":"tutorials/evaluation/#Tidy-Output","page":"Evaluating Explanations","title":"Tidy Output","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, evaluate returns vectors of evaluation measures. The optional key argument output_format::Symbol can be used to post-process the output in two ways: firstly, to return the output as a dictionary, specify output_format=:Dict:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; output_format=:Dict, report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Dict{Symbol, Vector} with 3 entries:\n :validity => BitVector[[1, 1, 1, 1, 1]]\n :redundancy => [[0.0, 0.0, 0.0, 0.0, 0.0]]\n :distance => Vector{Float32}[[3.36717, 3.10285, 3.28294, 3.07289, 3.15207]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Secondly, to return the output as a data frame, specify output_format=:DataFrame.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; output_format=:DataFrame, report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, data frames are pivoted to long format using individual counterfactuals as the id column. This behaviour can be suppressed by specifying pivot_longer=false.","category":"page"},{"location":"tutorials/evaluation/#Multiple-Counterfactual-Explanations","page":"Evaluating Explanations","title":"Multiple Counterfactual Explanations","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"It may be necessary to generate counterfactual explanations for multiple individuals.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Below, for example, we first select multiple samples (5) from the non-target class and then generate counterfactual explanations for all of them.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"This can be done using broadcasting:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"# Factual and target:\nids = rand(findall(predict_label(M, counterfactual_data) .== factual), n_individuals)\nxs = select_factual(counterfactual_data, ids)\nces = generate_counterfactual(xs, target, counterfactual_data, M, generator; num_counterfactuals=5)\nevaluation = evaluate.(ces)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"5-element Vector{Vector{Vector}}:\n [[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n\nVector{Vector}[[[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"This leads us to our next topic: Performance Benchmarks.","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/generic/#GenericGenerator","page":"Generic","title":"GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"We use the term generic to relate to the basic counterfactual generator proposed by Wachter, Mittelstadt, and Russell (2017) with L1-norm regularization. There is also a variant of this generator that uses the distance metric proposed in Wachter, Mittelstadt, and Russell (2017), which we call WachterGenerator.","category":"page"},{"location":"explanation/generators/generic/#Description","page":"Generic","title":"Description","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"As the term indicates, this approach is simple: it forms the baseline approach for gradient-based counterfactual generators. Wachter, Mittelstadt, and Russell (2017) were among the first to realise that","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"[โ€ฆ] explanations can, in principle, be offered without opening the โ€œblack box.โ€โ€” Wachter, Mittelstadt, and Russell (2017)","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"Gradient descent is performed directly in the feature space. Concerning the cost heuristic, the authors choose to penalize the distance of counterfactuals from the factual value. This is based on the intuitive notion that larger feature perturbations require greater effort.","category":"page"},{"location":"explanation/generators/generic/#Usage","page":"Generic","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"generator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"(Image: )","category":"page"},{"location":"explanation/generators/generic/#References","page":"Generic","title":"References","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/greedy/#GreedyGenerator","page":"Greedy","title":"GreedyGenerator","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"We use the term greedy to describe the counterfactual generator introduced by Schut et al. (2021).","category":"page"},{"location":"explanation/generators/greedy/#Description","page":"Greedy","title":"Description","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"The Greedy generator works under the premise of generating realistic counterfactuals by minimizing predictive uncertainty. Schut et al. (2021) show that for models that incorporates predictive uncertainty in their predictions, maximizing the predictive probability corresponds to minimizing the predictive uncertainty: by construction, the generated counterfactual will therefore be realistic (low epistemic uncertainty) and unambiguous (low aleatoric uncertainty).","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"For the counterfactual search Schut et al. (2021) propose using a Jacobian-based Saliency Map Attack(JSMA). It is greedy in the sense that it is an โ€œiterative algorithm that updates the most salient feature, i.e.ย the feature that has the largest influence on the classification, by delta at each stepโ€ (Schut et al. 2021).","category":"page"},{"location":"explanation/generators/greedy/#Usage","page":"Greedy","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"M = fit_model(counterfactual_data, :DeepEnsemble)\ngenerator = GreedyGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"(Image: )","category":"page"},{"location":"explanation/generators/greedy/#References","page":"Greedy","title":"References","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/probe/#ProbeGenerator","page":"PROBE","title":"ProbeGenerator","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The ProbeGenerator is designed to navigate the trade-offs between costs and robustness in Algorithmic Recourse (Pawelczyk et al. 2022).","category":"page"},{"location":"explanation/generators/probe/#Description","page":"PROBE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The goal of ProbeGenerator is to find a recourse xโ€™ whose prediction at any point y within some set around xโ€™ belongs to the positive class with probability 1 - r, where r is the recourse invalidation rate. It minimizes the gap between the achieved and desired recourse invalidation rates, minimizes recourse costs, and also ensures that the resulting recourse achieves a positive model prediction.","category":"page"},{"location":"explanation/generators/probe/#Explanation","page":"PROBE","title":"Explanation","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The loss function this generator is defined below. R is a hinge loss parameter which helps control for robustness. The loss and penalty functions can still be chosen freely.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginaligned\nR(x sigma^2 I) + l(f(x) s) + lambda d_c(x x)\nendaligned","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"R uses the following formula to control for noise. It generates small perturbations and checks how often the counterfactual explanation flips back to a factual one, when small amounts of noise are added to it.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginaligned\nDelta(x^hatE) = E_varepsilonh(x^hatE) - h(x^hatE + varepsilon)\nendaligned","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The above formula is not differentiable. For this reason the generator uses the closed form version of the formula below.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginequation\nDelta tilde(x^hatE sigma^2 I) = 1 - Phi left(fracsqrtf(x^hatE)sqrtnabla f(x^hatE)^T sigma^2 I nabla f(x^hatE)right) \nendequation","category":"page"},{"location":"explanation/generators/probe/#Usage","page":"PROBE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Generating a counterfactual with the data loaded and generator chosen works as follows:","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Note: It is important to set the convergence to โ€œ:invalidation_rateโ€ here.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"M = fit_model(counterfactual_data, :DeepEnsemble)\nopt = Descent(0.01)\ngenerator = CounterfactualExplanations.Generators.ProbeGenerator(opt=opt)\nconv = CounterfactualExplanations.Convergence.InvalidationRateConvergence(;invalidation_rate=0.5)\nce = generate_counterfactual(x, target, counterfactual_data, M, generator, convergence=conv)\nplot(ce)","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Choosing different invalidation rates makes the counterfactual more or less robust. The following plot shows the counterfactuals generated for different invalidation rates.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"(Image: )","category":"page"},{"location":"explanation/generators/probe/#References","page":"PROBE","title":"References","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/gravitational/#GravitationalGenerator","page":"Gravitational","title":"GravitationalGenerator","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The GravitationalGenerator was introduced in Altmeyer et al. (2023). It is named so because it generates counterfactuals that gravitate towards some sensible point in the target domain.","category":"page"},{"location":"explanation/generators/gravitational/#Description","page":"Gravitational","title":"Description","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"Altmeyer et al. (2023) extend the general framework as follows,","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"beginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS textyloss(M(f(mathbfs^prime))y^*) + lambda_1 textcost(f(mathbfs^prime)) + lambda_2 textextcost(f(mathbfs^prime)) \nendaligned ","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"where textcost(f(mathbfs^prime)) denotes the proxy for costs faced by the individual. โ€œThe newly introduced term textextcost(f(mathbfs^prime)) is meant to capture and address external costs incurred by the collective of individuals in response to changes in mathbfs^prime.โ€ (Altmeyer et al. 2023)","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"For the GravitationalGenerator we have,","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"beginaligned\ntextextcost(f(mathbfs^prime)) = textdist(f(mathbfs^prime)barx^*) \nendaligned","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"where barx is some sensible point in the target domain, for example, the subsample average barx^*=textmean(x), x in mathcalD_1.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"There is a tradeoff then, between the distance of counterfactuals from their factual value and the chosen point in the target domain. The chart below illustrates how the counterfactual outcome changes as the penalty lambda_2 on the distance to the point in the target domain is increased from left to right (holding the other penalty term constant).","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#Usage","page":"Gravitational","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"generator = GravitationalGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\ndisplay(plot(ce))","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#Comparison-to-GenericGenerator","page":"Gravitational","title":"Comparison to GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The figure below compares the outcome for the GenericGenerator and the GravitationalGenerator.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#References","page":"Gravitational","title":"References","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/benchmarking/#Performance-Benchmarks","page":"Benchmarking Explanations","title":"Performance Benchmarks","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In the previous tutorial, we have seen how counterfactual explanations can be evaluated. An important follow-up task is to compare the performance of different counterfactual generators is an important task. Researchers can use benchmarks to test new ideas they want to implement. Practitioners can find the right counterfactual generator for their specific use case through benchmarks. In this tutorial, we will see how to run benchmarks for counterfactual generators.","category":"page"},{"location":"tutorials/benchmarking/#Post-Hoc-Benchmarking","page":"Benchmarking Explanations","title":"Post Hoc Benchmarking","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We begin by continuing the discussion from the previous tutorial: suppose you have generated multiple counterfactual explanations for multiple individuals, like below:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"# Factual and target:\nn_individuals = 5\nids = rand(findall(predict_label(M, counterfactual_data) .== factual), n_individuals)\nxs = select_factual(counterfactual_data, ids)\nces = generate_counterfactual(xs, target, counterfactual_data, M, generator; num_counterfactuals=5)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"You may be interested in comparing the outcomes across individuals. To benchmark the various counterfactual explanations using default evaluation measures, you can simply proceed as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(ces)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Under the hood, the benchmark(counterfactual_explanations::Vector{CounterfactualExplanation}) uses CounterfactualExplanations.Evaluation.evaluate(ce::CounterfactualExplanation) to generate a Benchmark object, which contains the evaluation in its most granular form as a DataFrame.","category":"page"},{"location":"tutorials/benchmarking/#Working-with-Benchmarks","page":"Benchmarking Explanations","title":"Working with Benchmarks","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"For convenience, the DataFrame containing the evaluation can be returned by simply calling the Benchmark object. By default, the aggregated evaluation measures across id (in line with the default behaviour of evaluate).","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk()","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"15ร—7 DataFrame\n Row โ”‚ sample variable value generator โ‹ฏ\n โ”‚ Base.UUID String Float64 Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff distance 3.17243 GradientBase โ‹ฏ\n 2 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff redundancy 0.0 GradientBase\n 3 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff validity 1.0 GradientBase\n 4 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b distance 3.07148 GradientBase\n 5 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b redundancy 0.0 GradientBase โ‹ฏ\n 6 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b validity 1.0 GradientBase\n 7 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af distance 3.62159 GradientBase\n 8 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af redundancy 0.0 GradientBase\n 9 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af validity 1.0 GradientBase โ‹ฏ\n 10 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b distance 2.62783 GradientBase\n 11 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b redundancy 0.0 GradientBase\n 12 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b validity 1.0 GradientBase\n 13 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 distance 2.91985 GradientBase โ‹ฏ\n 14 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 redundancy 0.0 GradientBase\n 15 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 validity 1.0 GradientBase\n 4 columns omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"To retrieve the granular dataset, simply do:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk(agg=nothing)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"75ร—8 DataFrame\n Row โ”‚ sample num_counterfactual variable v โ‹ฏ\n โ”‚ Base.UUID Int64 String F โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 distance 3 โ‹ฏ\n 2 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 2 distance 3\n 3 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 3 distance 3\n 4 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 4 distance 3\n 5 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 5 distance 3 โ‹ฏ\n 6 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 redundancy 0\n 7 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 2 redundancy 0\n 8 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 3 redundancy 0\n 9 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 4 redundancy 0 โ‹ฏ\n 10 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 5 redundancy 0\n 11 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 validity 1\n โ‹ฎ โ”‚ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฑ\n 66 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 1 redundancy 0\n 67 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 2 redundancy 0 โ‹ฏ\n 68 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 3 redundancy 0\n 69 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 4 redundancy 0\n 70 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 5 redundancy 0\n 71 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 1 validity 1 โ‹ฏ\n 72 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 2 validity 1\n 73 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 3 validity 1\n 74 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 4 validity 1\n 75 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 5 validity 1 โ‹ฏ\n 5 columns and 54 rows omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Since benchmarks return a DataFrame object on call, post-processing is straightforward. For example, we could use Tidier.jl:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"using Tidier\n@chain bmk() begin\n @filter(variable == \"distance\")\n @select(sample, variable, value)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample variable value \n โ”‚ Base.UUID String Float64 \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff distance 3.17243\n 2 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b distance 3.07148\n 3 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af distance 3.62159\n 4 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b distance 2.62783\n 5 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 distance 2.91985","category":"page"},{"location":"tutorials/benchmarking/#Metadata-for-Counterfactual-Explanations","page":"Benchmarking Explanations","title":"Metadata for Counterfactual Explanations","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Benchmarks always report metadata for each counterfactual explanation, which is automatically inferred by default. The default metadata concerns the explained model and the employed generator. In the current example, we used the same model and generator for each individual:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @group_by(sample)\n @select(sample, model, generator)\n @summarize(model=first(model),generator=first(generator))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample model โ‹ฏ\n โ”‚ Base.UUID Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff FluxModel(Chain(Dense(2 => 2)), โ€ฆ โ‹ฏ\n 2 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 3 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 4 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 5 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 FluxModel(Chain(Dense(2 => 2)), โ€ฆ โ‹ฏ\n 1 column omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Metadata can also be provided as an optional key argument.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"meta_data = Dict(\n :generator => \"Generic\",\n :model => \"MLP\",\n)\nmeta_data = [meta_data for i in 1:length(ces)]\nbmk = benchmark(ces; meta_data=meta_data)\n@chain bmk() begin\n @group_by(sample)\n @select(sample, model, generator)\n @summarize(model=first(model),generator=first(generator))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample model generator \n โ”‚ Base.UUID String String \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 27fae496-f59f-11ee-2c30-f35d1025a6d4 MLP Generic\n 2 โ”‚ 27fdcc6a-f59f-11ee-030b-152c9794c5f1 MLP Generic\n 3 โ”‚ 27fdd04a-f59f-11ee-2010-e1732ff5d8d2 MLP Generic\n 4 โ”‚ 27fdd340-f59f-11ee-1d20-050a69dcacef MLP Generic\n 5 โ”‚ 27fdd5fc-f59f-11ee-02e8-d198e436abb3 MLP Generic","category":"page"},{"location":"tutorials/benchmarking/#Ad-Hoc-Benchmarking","page":"Benchmarking Explanations","title":"Ad Hoc Benchmarking","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"So far we have assumed the following workflow:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model.\nGenerate counterfactual explanations for some individual(s) (generate_counterfactual).\nEvaluate and benchmark them (benchmark(ces::Vector{CounterfactualExplanation})).","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In many cases, it may be preferable to combine these steps. To this end, we have added support for two scenarios of Ad Hoc Benchmarking.","category":"page"},{"location":"tutorials/benchmarking/#Pre-trained-Models","page":"Benchmarking Explanations","title":"Pre-trained Models","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In the first scenario, it is assumed that the machine learning models have been pre-trained and so the workflow can be summarized as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model(s).\nGenerate counterfactual explanations and benchmark them.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We suspect that this is the most common workflow for practitioners who are interested in benchmarking counterfactual explanations for the pre-trained machine learning models. Letโ€™s go through this workflow using a simple example. We first train some models and store them in a dictionary:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"models = Dict(\n :MLP => fit_model(counterfactual_data, :MLP),\n :Linear => fit_model(counterfactual_data, :Linear),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Next, we store the counterfactual generators of interest in a dictionary as well:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"generators = Dict(\n :Generic => GenericGenerator(),\n :Gravitational => GravitationalGenerator(),\n :Wachter => WachterGenerator(),\n :ClaPROAR => ClaPROARGenerator(),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Then we can run a benchmark for individual(s) x, a pre-specified target and counterfactual_data as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(x, target, counterfactual_data; models=models, generators=generators)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In this case, metadata is automatically inferred from the dictionaries:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @filter(variable == \"distance\")\n @select(sample, variable, value, model, generator)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"8ร—5 DataFrame\n Row โ”‚ sample variable value model โ‹ฏ\n โ”‚ Base.UUID String Float64 Tupleโ€ฆ โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 2cba5eee-f59f-11ee-1844-cbc7a8372a38 distance 4.38877 (:Linear, Flux โ‹ฏ\n 2 โ”‚ 2cd740fe-f59f-11ee-35c3-1157eb1b7583 distance 4.17021 (:Linear, Flux\n 3 โ”‚ 2cd741e2-f59f-11ee-2b09-0d55ef9892b9 distance 4.31145 (:Linear, Flux\n 4 โ”‚ 2cd7420c-f59f-11ee-1996-6fa75e23bb57 distance 4.17035 (:Linear, Flux\n 5 โ”‚ 2cd74234-f59f-11ee-0ad0-9f21949f5932 distance 5.73182 (:MLP, FluxMod โ‹ฏ\n 6 โ”‚ 2cd7425c-f59f-11ee-3eb4-af34f85ffd3d distance 5.50606 (:MLP, FluxMod\n 7 โ”‚ 2cd7427a-f59f-11ee-10d3-a1df6c8dc125 distance 5.2114 (:MLP, FluxMod\n 8 โ”‚ 2cd74298-f59f-11ee-32d1-f501c104fea8 distance 5.3623 (:MLP, FluxMod\n 2 columns omitted","category":"page"},{"location":"tutorials/benchmarking/#Everything-at-once","page":"Benchmarking Explanations","title":"Everything at once","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Researchers, in particular, may be interested in combining all steps into one. This is the second scenario of Ad Hoc Benchmarking:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model(s), generate counterfactual explanations and benchmark them.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"It involves calling benchmark directly on counterfactual data (the only positional argument):","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(counterfactual_data)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"This will use the default models from standard_models_catalogue and train them on the data. All available generators from generator_catalogue will also be used:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @filter(variable == \"validity\")\n @select(sample, variable, value, model, generator)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"200ร—5 DataFrame\n Row โ”‚ sample variable value model genera โ‹ฏ\n โ”‚ Base.UUID String Float64 Symbol Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear gravit โ‹ฏ\n 2 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear growin\n 3 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear revise\n 4 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear clue\n 5 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear probe โ‹ฏ\n 6 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear dice\n 7 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear clapro\n 8 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear wachte\n 9 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear generi โ‹ฏ\n 10 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear greedy\n 11 โ”‚ 32d255e8-f59f-11ee-3e8d-a9e9f6e23ea8 validity 1.0 Linear gravit\n โ‹ฎ โ”‚ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฑ\n 191 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP gravit\n 192 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP growin โ‹ฏ\n 193 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP revise\n 194 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP clue\n 195 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP probe\n 196 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP dice โ‹ฏ\n 197 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP clapro\n 198 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP wachte\n 199 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP generi\n 200 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP greedy โ‹ฏ\n 1 column and 179 rows omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Optionally, you can instead provide a dictionary of models and generators as before. Each value in the models dictionary should be one of two things:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Either be an object M of type AbstractFittedModel that implements the Models.train method.\nOr a DataType that can be called on CounterfactualData to create an object M as in (a).","category":"page"},{"location":"tutorials/benchmarking/#Multiple-Datasets","page":"Benchmarking Explanations","title":"Multiple Datasets","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Benchmarks are run on single instances of type CounterfactualData. This is our design choice for two reasons:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We want to avoid the loops inside the benchmark method(s) from getting too nested and convoluted.\nWhile it is straightforward to infer metadata for models and generators, this is not the case for datasets.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fortunately, it is very easy to run benchmarks for multiple datasets anyway, since Benchmark instances can be concatenated. To see how, letโ€™s consider an example involving multiple datasets, models and generators:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"# Data:\ndatasets = Dict(\n :moons => CounterfactualData(load_moons()...),\n :circles => CounterfactualData(load_circles()...),\n)\n\n# Models:\nmodels = Dict(\n :MLP => FluxModel,\n :Linear => Linear,\n)\n\n# Generators:\ngenerators = Dict(\n :Generic => GenericGenerator(),\n :Greedy => GreedyGenerator(),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Then we can simply loop over the datasets and eventually concatenate the results like so:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"using CounterfactualExplanations.Evaluation: distance_measures\nbmks = []\nfor (dataname, dataset) in datasets\n bmk = benchmark(dataset; models=models, generators=generators, measure=distance_measures)\n push!(bmks, bmk)\nend\nbmk = vcat(bmks[1], bmks[2]; ids=collect(keys(datasets)))","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"When ids are supplied, then a new id column is added to the evaluation data frame that contains unique identifiers for the different benchmarks. The optional idcol_name argument can be used to specify the name for that indicator column (defaults to \"dataset\"):","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @group_by(dataset, generator)\n @filter(model == :MLP)\n @filter(variable == \"distance_l1\")\n @summarize(L1_norm=mean(value))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"4ร—3 DataFrame\n Row โ”‚ dataset generator L1_norm \n โ”‚ Symbol Symbol Float32 \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ moons Generic 1.56555\n 2 โ”‚ moons Greedy 0.819269\n 3 โ”‚ circles Generic 1.83524\n 4 โ”‚ circles Greedy 0.498953","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/whistle_stop/#Whistle-Stop-Tour","page":"Whiste-Stop Tour","title":"Whistle-Stop Tour","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"In this tutorial, we will go through a slightly more complex example involving synthetic data. We will generate Counterfactual Explanations using different generators and visualize the results.","category":"page"},{"location":"tutorials/whistle_stop/#Data-and-Classifier","page":"Whiste-Stop Tour","title":"Data and Classifier","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Choose some values for data and a model:\nn_dim = 2\nn_classes = 4\nn_samples = 400\nmodel_name = :MLP","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The code chunk below generates synthetic data and uses it to fit a classifier. The outcome variable counterfactual_data.y consists of 4 classes. The input data counterfactual_data.X consists of 2 features. We generate a total of 400 samples. On the model side, we have specified model_name = :MLP. The fit_model can be used to fit a number of default models.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"data = TaijaData.load_multi_class(n_samples)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, model_name)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The chart below visualizes our data along with the model predictions. In particular, the contour indicates the predicted probabilities generated by our classifier. By default, these are the predicted probabilities for y=1, the first label. For multi-dimensional input data is compressed into two dimensions and the decision boundary is approximated using Nearest Neighbors (this is still somewhat experimental).","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"plot(M, counterfactual_data)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"(Image: )","category":"page"},{"location":"tutorials/whistle_stop/#Counterfactual-Explanation","page":"Whiste-Stop Tour","title":"Counterfactual Explanation","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"Next, we begin by specifying our target and factual label. We then draw a random sample from the non-target (factual) class.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Factual and target:\ntarget = 2\nfactual = 4\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"This sets the baseline for our counterfactual search: we plan to perturb the factual x to change the predicted label from y=4 to our target label target=2.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"Counterfactual generators accept several default parameters that can be used to adjust the counterfactual search at a high level: for example, a Flux.jl optimizer can be supplied to define how exactly gradient steps are performed. Importantly, one can also define the threshold probability at which the counterfactual search will converge. This relates to the probability predicted by the underlying black-box model, that the counterfactual belongs to the target class. A higher decision threshold typically prolongs the counterfactual search.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Search params:\ndecision_threshold = 0.75\nnum_counterfactuals = 3","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The code below runs the counterfactual search for each generator available in the generator_catalogue. In each case, we also call the generic plot() method on the generated instance of type CounterfactualExplanation. This generates a simple plot that visualizes the entire counterfactual path. The chart below shows the results for all counterfactual generators: Factual: 4 โ†’ Target: 2.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"ces = Dict()\nplts = []\nplottable_generators = filter(((k,v),) -> k โˆ‰ [:growing_spheres, :feature_tweak], generator_catalogue)\n# Search:\nfor (key, Generator) in plottable_generators\n generator = Generator()\n ce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n num_counterfactuals = num_counterfactuals,\n convergence=GeneratorConditionsConvergence(\n decision_threshold=decision_threshold\n )\n )\n ces[key] = ce\n plts = [plts..., plot(ce; title=key, colorbar=false)]\nend","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"(Image: )","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"how_to_guides/custom_models/#How-to-add-Custom-Models","page":"... add custom models","title":"How to add Custom Models","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Adding custom models is possible and relatively straightforward, as we will demonstrate in this guide.","category":"page"},{"location":"how_to_guides/custom_models/#Custom-Models","page":"... add custom models","title":"Custom Models","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Apart from the default models you can use any arbitrary (differentiable) model and generate recourse in the same way as before. Only two steps are necessary to make your own Julia model compatible with this package:","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The model needs to be declared as a subtype of <:CounterfactualExplanations.Models.AbstractFittedModel.\nYou need to extend the functions CounterfactualExplanations.Models.logits and CounterfactualExplanations.Models.probs for your custom model.","category":"page"},{"location":"how_to_guides/custom_models/#How-FluxModel-was-added","page":"... add custom models","title":"How FluxModel was added","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"To demonstrate how this can be done in practice, we will reiterate here how native support for Flux.jl models was enabled (Innes 2018). Once again we use synthetic data for an illustrative example. The code below loads the data and builds a simple model architecture that can be used for a multi-class prediction task. Note how outputs from the final layer are not passed through a softmax activation function, since the counterfactual loss is evaluated with respect to logits. The model is trained with dropout.","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"# Data:\nN = 200\ndata = TaijaData.load_blobs(N; centers=4, cluster_std=0.5)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ny = counterfactual_data.y\nX = counterfactual_data.X\n\n# Flux model setup: \nusing Flux\ndata = Flux.DataLoader((X,y), batchsize=1)\nn_hidden = 32\noutput_dim = size(y,1)\ninput_dim = 2\nactivation = ฯƒ\nmodel = Chain(\n Dense(input_dim, n_hidden, activation),\n Dropout(0.1),\n Dense(n_hidden, output_dim)\n) \nloss(x, y) = Flux.Losses.logitcrossentropy(model(x), y)\n\n# Flux model training:\nusing Flux.Optimise: update!, Adam\nopt = Adam()\nepochs = 50\nfor epoch = 1:epochs\n for d in data\n gs = gradient(Flux.params(model)) do\n l = loss(d...)\n end\n update!(opt, Flux.params(model), gs)\n end\nend","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The code below implements the two steps that were necessary to make Flux models compatible with the package. We first declare our new struct as a subtype of <:AbstractDifferentiableModel, which itself is an abstract subtype of <:AbstractFittedModel. Computing logits amounts to just calling the model on inputs. Predicted probabilities for labels can in this case be computed by passing predicted logits through the softmax function. Finally, we just instantiate our model in the same way as always.","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"# Step 1)\nstruct MyFluxModel <: AbstractDifferentiableModel\n model::Any\n likelihood::Symbol\nend\n\n# Step 2)\n# import functions in order to extend\nimport CounterfactualExplanations.Models: logits\nimport CounterfactualExplanations.Models: probs \nlogits(M::MyFluxModel, X::AbstractArray) = M.model(X)\nprobs(M::MyFluxModel, X::AbstractArray) = softmax(logits(M, X))\nM = MyFluxModel(model, :classification_multi)","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The code below implements the counterfactual search and plots the results:","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"factual_label = 4\ntarget = 2\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual_label))\nx = select_factual(counterfactual_data, chosen) \n\n# Counterfactual search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"(Image: )","category":"page"},{"location":"how_to_guides/custom_models/#References","page":"... add custom models","title":"References","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Innes, Mike. 2018. โ€œFlux: Elegant Machine Learning with Julia.โ€ Journal of Open Source Software 3 (25): 602. https://doi.org/10.21105/joss.00602.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Documentation for CounterfactualExplanations.jl.","category":"page"},{"location":"#CounterfactualExplanations","page":"๐Ÿ  Home","title":"CounterfactualExplanations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations and Algorithmic Recourse in Julia.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: Stable) (Image: Dev) (Image: Build Status) (Image: Coverage) (Image: Code Style: Blue) (Image: License) (Image: Package Downloads) (Image: Aqua QA)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CounterfactualExplanations.jl is a package for generating Counterfactual Explanations (CE) and Algorithmic Recourse (AR) for black-box algorithms. Both CE and AR are related tools for explainable artificial intelligence (XAI). While the package is written purely in Julia, it can be used to explain machine learning algorithms developed and trained in other popular programming languages like Python and R. See below for a short introduction and other resources or dive straight into the docs.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"There is also a corresponding paper, Explaining Black-Box Models through Counterfactuals, which has been published in JuliaCon Proceedings. Please consider citing the paper, if you use this package in your work:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: DOI) (Image: DOI)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"@article{Altmeyer2023,\n doi = {10.21105/jcon.00130},\n url = {https://doi.org/10.21105/jcon.00130},\n year = {2023},\n publisher = {The Open Journal},\n volume = {1},\n number = {1},\n pages = {130},\n author = {Patrick Altmeyer and Arie van Deursen and Cynthia C. s. Liem},\n title = {Explaining Black-Box Models through Counterfactuals},\n journal = {Proceedings of the JuliaCon Conferences}\n}","category":"page"},{"location":"#Installation","page":"๐Ÿ  Home","title":"๐Ÿšฉ Installation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"You can install the stable release from Juliaโ€™s General Registry as follows:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"using Pkg\nPkg.add(\"CounterfactualExplanations\")","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CounterfactualExplanations.jl is under active development. To install the development version of the package you can run the following command:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"using Pkg\nPkg.add(url=\"https://github.com/juliatrustworthyai/CounterfactualExplanations.jl\")","category":"page"},{"location":"#Background-and-Motivation","page":"๐Ÿ  Home","title":"๐Ÿค” Background and Motivation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Machine learning models like Deep Neural Networks have become so complex, opaque and underspecified in the data that they are generally considered Black Boxes. Nonetheless, such models often play a key role in data-driven decision-making systems. This creates the following problem: human operators in charge of such systems have to rely on them blindly, while those individuals subject to them generally have no way of challenging an undesirable outcome:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"โ€œYou cannot appeal to (algorithms). They do not listen. Nor do they bend.โ€โ€” Cathy Oโ€™Neil in Weapons of Math Destruction, 2016","category":"page"},{"location":"#Enter:-Counterfactual-Explanations","page":"๐Ÿ  Home","title":"๐Ÿ”ฎ Enter: Counterfactual Explanations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations can help human stakeholders make sense of the systems they develop, use or endure: they explain how inputs into a system need to change for it to produce different decisions. Explainability benefits internal as well as external quality assurance.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations have a few properties that are desirable in the context of Explainable Artificial Intelligence (XAI). These include:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Full fidelity to the black-box model, since no proxy is involved.\nNo need for (reasonably) interpretable features as opposed to LIME and SHAP.\nClear link to Algorithmic Recourse and Causal Inference.\nLess susceptible to adversarial attacks than LIME and SHAP.","category":"page"},{"location":"#Example:-Give-Me-Some-Credit","page":"๐Ÿ  Home","title":"Example: Give Me Some Credit","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Consider the following real-world scenario: a retail bank is using a black-box model trained on their clientsโ€™ credit history to decide whether they will provide credit to new applicants. To simulate this scenario, we have pre-trained a binary classifier on the publicly available Give Me Some Credit dataset that ships with this package (Kaggle 2011).","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The figure below shows counterfactuals for 10 randomly chosen individuals that would have been denied credit initially.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Example:-MNIST","page":"๐Ÿ  Home","title":"Example: MNIST","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The figure below shows a counterfactual generated for an image classifier trained on MNIST: in particular, it demonstrates which pixels need to change in order for the classifier to predict 3 instead of 8.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Since v0.1.9 counterfactual generators are fully composable. Here we have composed a generator that combines ideas from Wachter, Mittelstadt, and Russell (2017) and Altmeyer et al. (2023):","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Compose generator:\nusing CounterfactualExplanations.Objectives: distance_from_target\ngenerator = GradientBasedGenerator()\n@chain generator begin\n @objective logitcrossentropy + 0.1distance_mad + 0.1distance_from_target\n @with_optimiser Adam(0.1) \nend\ncounterfactual_data.generative_model = vae # assign generative model","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Usage-example","page":"๐Ÿ  Home","title":"๐Ÿ” Usage example","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Generating counterfactuals will typically look like follows. Below we first fit a simple model to a synthetic dataset with linearly separable features and then draw a random sample:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Data and Classifier:\ncounterfactual_data = CounterfactualData(load_linearly_separable()...)\nM = fit_model(counterfactual_data, :Linear)\n\n# Select random sample:\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"To this end, we specify a counterfactual generator of our choice:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Counterfactual search:\ngenerator = DiCEGenerator(ฮป=[0.1,0.3])","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Here, we have chosen to use the GradientBasedGenerator to move the individual from its factual label 1 to the target label 2.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"With all of our ingredients specified, we finally generate counterfactuals using a simple API call:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"conv = conv = CounterfactualExplanations.Convergence.GeneratorConditionsConvergence()\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=3, convergence=conv,\n)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The plot below shows the resulting counterfactual path:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Implemented-Counterfactual-Generators","page":"๐Ÿ  Home","title":"โ˜‘๏ธ Implemented Counterfactual Generators","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Currently, the following counterfactual generators are implemented:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"ClaPROAR (Altmeyer et al. 2023)\nCLUE (Antorรกn et al. 2020)\nDiCE (Mothilal, Sharma, and Tan 2020)\nFeatureTweak (Tolomei et al. 2017)\nGeneric\nGravitationalGenerator (Altmeyer et al. 2023)\nGreedy (Schut et al. 2021)\nGrowingSpheres (Laugel et al. 2017)\nPROBE (Pawelczyk et al. 2022)\nREVISE (Joshi et al. 2019)\nWachter (Wachter, Mittelstadt, and Russell 2017)","category":"page"},{"location":"#Goals-and-limitations","page":"๐Ÿ  Home","title":"๐ŸŽฏ Goals and limitations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The goal of this library is to contribute to efforts towards trustworthy machine learning in Julia. The Julia language has an edge when it comes to trustworthiness: it is very transparent. Packages like this one are generally written in pure Julia, which makes it easy for users and developers to understand and contribute to open-source code. Eventually, this project aims to offer a one-stop-shop of counterfactual explanations.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Our ambition is to enhance the package through the following features:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Support for all supervised machine learning models trained in MLJ.jl.\nSupport for regression models.","category":"page"},{"location":"#Contribute","page":"๐Ÿ  Home","title":"๐Ÿ›  Contribute","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"If any of the below applies to you, this might be the right open-source project for you:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Youโ€™re an expert in Counterfactual Explanations or Explainable AI more broadly and you are curious about Julia.\nYouโ€™re experienced with Julia and are happy to help someone less experienced to up their game. Ideally, you are also curious about Trustworthy AI.\nYouโ€™re new to Julia and open-source development and would like to start your learning journey by contributing to a recent and active development. Ideally, you are familiar with machine learning.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"@pat-alt here: I am still very much at the beginning of my Julia journey, so if you spot any issues or have any suggestions for design improvement, please just open issue or start a discussion.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"For more details on how to contribute see here. Please follow the SciML ColPrac guide.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"There are also some general pointers for people looking to contribute to any of our Taija packages here.","category":"page"},{"location":"#Citation","page":"๐Ÿ  Home","title":"๐ŸŽ“ Citation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"If you want to use this codebase, please consider citing the corresponding paper:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"@article{Altmeyer2023,\n doi = {10.21105/jcon.00130},\n url = {https://doi.org/10.21105/jcon.00130},\n year = {2023},\n publisher = {The Open Journal},\n volume = {1},\n number = {1},\n pages = {130},\n author = {Patrick Altmeyer and Arie van Deursen and Cynthia C. s. Liem},\n title = {Explaining Black-Box Models through Counterfactuals},\n journal = {Proceedings of the JuliaCon Conferences}\n}","category":"page"},{"location":"#References","page":"๐Ÿ  Home","title":"๐Ÿ“š References","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Kaggle. 2011. โ€œGive Me Some Credit, Improve on the State of the Art in Credit Scoring by Predicting the Probability That Somebody Will Experience Financial Distress in the Next Two Years.โ€ Kaggle. https://www.kaggle.com/c/GiveMeSomeCredit.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ https://arxiv.org/abs/1712.08443.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/categorical/#Categorical-Features","page":"Categorical Features","title":"Categorical Features","text":"","category":"section"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"To illustrate how data is preprocessed under the hood, we consider a simple toy dataset with three categorical features (name, grade and sex) and one continuous feature (age):","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"X = (\n name=categorical([\"Danesh\", \"Lee\", \"Mary\", \"John\"]),\n grade=categorical([\"A\", \"B\", \"A\", \"C\"], ordered=true),\n sex=categorical([\"male\",\"female\",\"male\",\"male\"]),\n height=[1.85, 1.67, 1.5, 1.67],\n)\nschema(X)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Categorical features are expected to be one-hot or dummy encoded. To this end, we could use MLJ, for example:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"hot = OneHotEncoder()\nmach = fit!(machine(hot, X))\nW = transform(mach, X)\nschema(W)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name__Danesh โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__John โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Lee โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Mary โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__A โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__B โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__C โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__female โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__male โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The matrix that will be perturbed during the counterfactual search looks as follows:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"X = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10ร—4 Matrix{Float64}:\n 1.0 0.0 0.0 0.0\n 0.0 0.0 0.0 1.0\n 0.0 1.0 0.0 0.0\n 0.0 0.0 1.0 0.0\n 1.0 0.0 1.0 0.0\n 0.0 1.0 0.0 0.0\n 0.0 0.0 0.0 1.0\n 0.0 1.0 0.0 0.0\n 1.0 0.0 1.0 1.0\n 1.85 1.67 1.5 1.67","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The CounterfactualData constructor takes two optional arguments that can be used to specify the indices of categorical and continuous features. If nothing is supplied, all features are assumed to be continuous. For categorical features, the constructor expects and array of arrays of integers (Vector{Vector{Int}}) where each subarray includes the indices of a all one-hot encoded rows related to a single categorical feature. In the example above, the name feature is one-hot encoded across rows 1, 2 and 3 of X.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"features_categorical = [\n [1,2,3,4], # name\n [5,6,7], # grade\n [8,9] # sex\n]\nfeatures_continuous = [10]","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"We propose the following simple logic for reconstructing categorical encodings after perturbations:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"For one-hot encoded features with multiple classes, choose the maximum.\nFor binary features, clip the perturbed value to fall into 01 and round to the nearest of the two integers.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"function reconstruct_cat_encoding(x)\n map(features_categorical) do cat_group_index\n if length(cat_group_index) > 1\n x[cat_group_index] = Int.(x[cat_group_index] .== maximum(x[cat_group_index]))\n if sum(x[cat_group_index]) > 1\n ties = findall(x[cat_group_index] .== 1)\n _x = zeros(length(x[cat_group_index]))\n winner = rand(ties,1)[1]\n _x[winner] = 1\n x[cat_group_index] = _x\n end\n else\n x[cat_group_index] = [round(clamp(x[cat_group_index][1],0,1))]\n end\n end\n return x\nend","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Letโ€™s look at a few simple examples to see how this function works. Firstly, consider the case of perturbing a single element:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x = X[:,1]\nx[1] = 1.1\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.1\n 0.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Next, consider the case of perturbing multiple elements:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x[2] = 1.1\nx[3] = -1.2\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 1.1\n -1.2\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 0.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Finally, letโ€™s introduce a tie:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x[1] = 1.0\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 0.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"extensions/neurotree/#[NeuroTreeModels.jl](https://evovest.github.io/NeuroTreeModels.jl/dev/)","page":"NeuroTrees","title":"NeuroTreeModels.jl","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"NeuroTreeModels.jl is a package that provides a framework for training differentiable tree-based models. This is relevant to the work on counterfactual explanations (CE), which often assumes that the underlying black-box model is differentiable with respect to its input. The literature on CE therefore regularly focuses exclusively on explaining deep learning models. This is at odds with the fact that the literature also typically focuses on tabular data, which is often best modeled by tree-based models (Grinsztajn, Oyallon, and Varoquaux 2022). The extension for NeuroTreeModels.jl provides a way to bridge this gap by allowing users to apply existing gradient-based CE methods to differentiable tree-based models.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"warning: Experimental Feature\nPlease note that this extension is still experimental. Neither the behaviour of differentiable tree-based models nor their interplay with counterfactual explanations is well understood at this point. If you encounter any issues, please report them to the package maintainers. Your feedback is highly appreciated.Please also note that this extension is only tested on Julia 1.9 and higher, due to compatibility issues.","category":"page"},{"location":"extensions/neurotree/#Example","page":"NeuroTrees","title":"Example","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"The extension will be loaded automatically when loading the NeuroTreeModels package (assuming the CounterfactualExplanations package is also loaded).","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"using NeuroTreeModels","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Next, we will fit a NeuroTree model to the moons dataset using our standard package API for doing so.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"# Fit model to data:\ndata = CounterfactualData(load_moons()...)\nM = fit_model(\n data, :NeuroTree; \n depth=2, lr=5e-2, nrounds=50, batchsize=10\n)","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"NeuroTreeExt.NeuroTreeModel(NeuroTreeRegressor(loss = mlogloss, โ€ฆ), :classification_multi, NeuroTreeModels.NeuroTreeModel{NeuroTreeModels.MLogLoss, Chain{Tuple{BatchNorm{typeof(identity), Vector{Float32}, Float32, Vector{Float32}}, NeuroTreeModels.StackTree}}}(NeuroTreeModels.MLogLoss, Chain(BatchNorm(2, active=false), NeuroTreeModels.StackTree(NeuroTree[NeuroTree{Matrix{Float32}, Vector{Float32}, Array{Float32, 3}}(Float32[1.8824593 -0.28222033; -2.680499 0.67347014; โ€ฆ ; -1.0722864 1.3651229; -2.0926774 1.63557], Float32[-3.4070241, 4.545113, 1.0882677, -0.3497498, -2.766766, 1.9072449, -0.9736261, 3.9750721, 1.726214, 3.7279263 โ€ฆ -0.0664266, -0.4214582, -2.3816268, -3.1371245, 0.76548636, 2.636373, 2.4558601, 0.893434, -1.9484522, 4.793434], Float32[3.44271 -6.334693 -0.6308845 3.385659; -3.4316056 6.297003 0.7254221 -3.3283486;;; -3.7011054 -0.17596768 0.15429471 2.270125; 3.4926674 0.026218029 -0.19753197 -2.2337704;;; 1.1795454 -4.315231 0.28486454 1.9995956; -0.9651108 4.0999455 -0.05312265 -1.8039354;;; โ€ฆ ;;; 2.5076811 -0.46358463 -3.5438805 0.0686823; -2.592356 0.47884527 3.781507 -0.022692114;;; -0.59115165 -3.234046 0.09896194 2.375202; 0.5592871 3.3082843 -0.014032216 -2.1876256;;; 2.039389 -0.10134532 2.6637273 -4.999703; -2.0289893 0.3368772 -2.5739825 5.069934], tanh)])), Dict{Symbol, Any}(:feature_names => [:x1, :x2], :nrounds => 50, :device => :cpu)))","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Finally, we select a factual instance and generate a counterfactual explanation for it using the generic gradient-based CE method.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"# Select a factual instance:\ntarget = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, data) .== factual))\nx = select_factual(data, chosen)\n\n# Generate counterfactual explanation:\nฮท = 0.01\ngenerator = GenericGenerator(; opt=Descent(ฮท), ฮป=0.01)\nconv = CounterfactualExplanations.Convergence.DecisionThresholdConvergence(;\n decision_threshold=0.9, max_iter=100\n)\nce = generate_counterfactual(x, target, data, M, generator; convergence=conv)\nplot(ce, alpha=0.1)","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"(Image: )","category":"page"},{"location":"extensions/neurotree/#References","page":"NeuroTrees","title":"References","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Grinsztajn, Lรฉo, Edouard Oyallon, and Gaรซl Varoquaux. 2022. โ€œWhy Do Tree-Based Models Still Outperform Deep Learning on Tabular Data?โ€ https://arxiv.org/abs/2207.08815.","category":"page"}] +[{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/models/#Handling-Models","page":"Handling Models","title":"Handling Models","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The typical use-case for Counterfactual Explanations and Algorithmic Recourse is as follows: users have trained some supervised model that is not inherently interpretable and are looking for a way to explain it. In this tutorial, we will see how pre-trained models can be used with this package.","category":"page"},{"location":"tutorials/models/#Models-trained-in-Flux.jl","page":"Handling Models","title":"Models trained in Flux.jl","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"We will train a simple binary classifier in Flux.jl on the popular Moons dataset:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"n = 500\ndata = TaijaData.load_moons(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nX = counterfactual_data.X\ny = counterfactual_data.y\nplt = plot()\nscatter!(counterfactual_data)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"(Image: )","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The following code chunk sets up a Deep Neural Network for the task at hand:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"data = Flux.DataLoader((X,y),batchsize=1)\ninput_dim = size(X,1)\nn_hidden = 32\nactivation = relu\noutput_dim = 2\nnn = Chain(\n Dense(input_dim, n_hidden, activation),\n Dropout(0.1),\n Dense(n_hidden, output_dim)\n)\nloss(yhat, y) = Flux.Losses.logitcrossentropy(nn(yhat), y)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Next, we fit the network to the data:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"using Flux.Optimise: update!, Adam\nopt = Adam()\nepochs = 100\navg_loss(data) = mean(map(d -> loss(d[1],d[2]), data))\nshow_every = epochs/5\n# Training:\nfor epoch = 1:epochs\n for d in data\n gs = gradient(Flux.params(nn)) do\n l = loss(d...)\n end\n update!(opt, Flux.params(nn), gs)\n end\n if epoch % show_every == 0\n println(\"Epoch \" * string(epoch))\n @show avg_loss(data)\n end\nend","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Epoch 20\navg_loss(data) = 0.1407434f0\nEpoch 40\navg_loss(data) = 0.11345118f0\nEpoch 60\navg_loss(data) = 0.046319224f0\nEpoch 80\navg_loss(data) = 0.011847609f0\nEpoch 100\navg_loss(data) = 0.007242911f0","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"To prepare the fitted model for use with our package, we need to wrap it inside a container. For plain-vanilla models trained in Flux.jl, the corresponding constructor is called FluxModel. There is also a separate constructor called FluxEnsemble, which applies to Deep Ensembles. Deep Ensembles are a popular approach to approximate Bayesian Deep Learning and have been shown to generate good predictive uncertainty estimates (Lakshminarayanan, Pritzel, and Blundell 2016).","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The appropriate API call to wrap our simple network in a container follows below:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"M = FluxModel(nn)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"FluxModel(Chain(Dense(2 => 32, relu), Dropout(0.1, active=false), Dense(32 => 2)), :classification_binary)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"The likelihood function of the output variable is automatically inferred from the data. The generic plot() method can be called on the model and data to visualise the results:","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"plot(M, counterfactual_data)","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"(Image: )","category":"page"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Our model M is now ready for use with the package.","category":"page"},{"location":"tutorials/models/#References","page":"Handling Models","title":"References","text":"","category":"section"},{"location":"tutorials/models/","page":"Handling Models","title":"Handling Models","text":"Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/dice/#DiCEGenerator","page":"DiCE","title":"DiCEGenerator","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The DiCEGenerator can be used to generate multiple diverse counterfactuals for a single factual.","category":"page"},{"location":"explanation/generators/dice/#Description","page":"DiCE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Counterfactual Explanations are not unique and there are therefore many different ways through which valid counterfactuals can be generated. In the context of Algorithmic Recourse this can be leveraged to offer individuals not one, but possibly many different ways to change a negative outcome into a positive one. One might argue that it makes sense for those different options to be as diverse as possible. This idea is at the core of DiCE, a counterfactual generator introduce by Mothilal, Sharma, and Tan (2020) that generate a diverse set of counterfactual explanations.","category":"page"},{"location":"explanation/generators/dice/#Defining-Diversity","page":"DiCE","title":"Defining Diversity","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"To ensure that the generated counterfactuals are diverse, Mothilal, Sharma, and Tan (2020) add a diversity constraint to the counterfactual search objective. In particular, diversity is explicitly proxied via Determinantal Point Processes (DDP).","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"We can implement DDP in Julia as follows:[1]","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"using LinearAlgebra\nfunction ddp_diversity(X::AbstractArray{<:Real, 3})\n xs = eachslice(X, dims = ndims(X))\n K = [1/(1 + norm(x .- y)) for x in xs, y in xs]\n return det(K)\nend","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Below we generate some random points in mathbbR^2 and apply gradient ascent on this function evaluated at the whole array of points. As we can see in the animation below, the points are sent away from each other. In other words, diversity across the array of points increases as we ascend the ddp_diversity function.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"lims = 5\nN = 5\nX = rand(2,1,N)\nT = 50\nฮท = 0.1\nanim = @animate for t in 1:T\n X .+= gradient(ddp_diversity, X)[1]\n Z = reshape(X,2,N)\n scatter(\n Z[1,:],Z[2,:],ms=25, \n xlims=(-lims,lims),ylims=(-lims,lims),\n label=\"\",colour=1:N,\n size=(500,500),\n title=\"Diverse Counterfactuals\"\n )\nend\ngif(anim, joinpath(www_path, \"dice_intro.gif\"))","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#Usage","page":"DiCE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"generator = DiCEGenerator()\nconv = CounterfactualExplanations.Convergence.GeneratorConditionsConvergence()\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=5, convergence=conv\n)\nplot(ce)","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#Effect-of-Penalty","page":"DiCE","title":"Effect of Penalty","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"ฮ›โ‚‚ = [0.1, 1.0, 5.0]\nces = []\nn_cf = 5\nusing Flux\nfor ฮปโ‚‚ โˆˆ ฮ›โ‚‚ \n ฮป = [0.00, ฮปโ‚‚]\n generator = DiCEGenerator(ฮป=ฮป)\n ces = vcat(\n ces...,\n generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=n_cf, convergence=conv\n )\n )\nend","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"The figure below shows the resulting counterfactual paths. As expected, the resulting counterfactuals are more dispersed across the feature domain for higher choices of lambda_2","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"(Image: )","category":"page"},{"location":"explanation/generators/dice/#References","page":"DiCE","title":"References","text":"","category":"section"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"explanation/generators/dice/","page":"DiCE","title":"DiCE","text":"[1] With thanks to the respondents on Discourse","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"how_to_guides/#How-To-Guides","page":"Overview","title":"How-To Guides","text":"","category":"section"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"In this section, you will find a series of how-to-guides that showcase specific use cases of counterfactual explanations (CE).","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"How-to guides are directions that take the reader through the steps required to solve a real-world problem. How-to guides are goal-oriented.โ€” Diรกtaxis","category":"page"},{"location":"how_to_guides/","page":"Overview","title":"Overview","text":"In other words, you come here because you may have some particular problem in mind, would like to see how it can be solved using CE and then most likely head off again ๐Ÿซก.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/generators/#Handling-Generators","page":"Handling Generators","title":"Handling Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Generating Counterfactual Explanations can be seen as a generative modelling task because it involves generating samples in the input space: x sim mathcalX. In this tutorial, we will introduce how Counterfactual GradientBasedGenerators are used. They are discussed in more detail in the explanatory section of the documentation.","category":"page"},{"location":"tutorials/generators/#Composable-Generators","page":"Handling Generators","title":"Composable Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"warning: Breaking Changes Expected\nWork on this feature is still in its very early stages and breaking changes should be expected. ","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"One of the key objectives for this package is Composability. It turns out that many of the various counterfactual generators that have been proposed in the literature, essentially do the same thing: they optimize an objective function. Formally we have,","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"\nbeginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS left textyloss(M(f(mathbfs^prime))y^*)+ lambda textcost(f(mathbfs^prime)) right \nendaligned \n qquad(1)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"where textyloss denotes the main loss function and textcost is a penalty term (Altmeyer et al. 2023).","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Without going into further detail here, the important thing to mention is that Equationย 1 very closely describes how counterfactual search is actually implemented in the package. In other words, all off-the-shelf generators currently implemented work with that same objective. They just vary in the way that penalties are defined, for example. This gives rise to an interesting idea:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Why not compose generators that combine ideas from different off-the-shelf generators?","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"The GradientBasedGenerator class provides a straightforward way to do this, without requiring users to build custom GradientBasedGenerators from scratch. It can be instantiated as follows:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"generator = GradientBasedGenerator()","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"By default, this creates a generator that simply performs gradient descent without any penalties. To modify the behaviour of the generator, you can define the counterfactual search objective function using the @objective macro:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"@objective(generator, logitbinarycrossentropy + 0.1distance_l2 + 1.0ddp_diversity)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Here we have essentially created a version of the DiCEGenerator:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"ce = generate_counterfactual(x, target, counterfactual_data, M, generator; num_counterfactuals=5)\nplot(ce)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Multiple macros can be chained using Chains.jl making it easy to create entirely new flavours of counterfactual generators. The following generator, for example, combines ideas from DiCE (Mothilal, Sharma, and Tan 2020) and REVISE (Joshi et al. 2019):","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"@chain generator begin\n @objective logitcrossentropy + 1.0ddp_diversity # DiCE (Mothilal et al. 2020)\n @with_optimiser Flux.Adam(0.1) \n @search_latent_space # REVISE (Joshi et al. 2019)\nend","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Letโ€™s take this generator to our MNIST dataset and generate a counterfactual explanation for turning a 0 into a 8.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/#Off-the-Shelf-Generators","page":"Handling Generators","title":"Off-the-Shelf Generators","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Off-the-shelf generators are just default recipes for counterfactual generators. Currently, the following off-the-shelf counterfactual generators are implemented in the package:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"generator_catalogue","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Dict{Symbol, Any} with 11 entries:\n :gravitational => GravitationalGenerator\n :growing_spheres => GrowingSpheresGenerator\n :revise => REVISEGenerator\n :clue => CLUEGenerator\n :probe => ProbeGenerator\n :dice => DiCEGenerator\n :feature_tweak => FeatureTweakGenerator\n :claproar => ClaPROARGenerator\n :wachter => WachterGenerator\n :generic => GenericGenerator\n :greedy => GreedyGenerator","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"To specify the type of generator you want to use, you can simply instantiate it:","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"# Search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"(Image: )","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"We generally make an effort to follow the literature as closely as possible when implementing off-the-shelf generators.","category":"page"},{"location":"tutorials/generators/#References","page":"Handling Generators","title":"References","text":"","category":"section"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"tutorials/generators/","page":"Handling Generators","title":"Handling Generators","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/simple_example/#Simple-Example","page":"Simple Example","title":"Simple Example","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"In this tutorial, we will go through a simple example involving synthetic data and a generic counterfactual generator.","category":"page"},{"location":"tutorials/simple_example/#Data-and-Classifier","page":"Simple Example","title":"Data and Classifier","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Below we generate some linearly separable data and fit a simple MLP classifier with batch normalization to it. For more information on generating data and models, refer to the Handling Data and Handling Models tutorials respectively.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"# Counteractual data and model:\nflux_training_params.batchsize = 10\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ncounterfactual_data.standardize = true\nM = fit_model(counterfactual_data, :MLP, batch_norm=true)","category":"page"},{"location":"tutorials/simple_example/#Counterfactual-Search","page":"Simple Example","title":"Counterfactual Search","text":"","category":"section"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Next, determine a target and factual class for our counterfactual search and select a random factual instance to explain.","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"target = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"Finally, we generate and visualize the generated counterfactual:","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"# Search:\ngenerator = WachterGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"tutorials/simple_example/","page":"Simple Example","title":"Simple Example","text":"(Image: )","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/optimisers/jsma/#Jacobian-based-Saliency-Map-Attack","page":"JSMA","title":"Jacobian-based Saliency Map Attack","text":"","category":"section"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"To search counterfactuals, Schut et al. (2021) propose to use a Jacobian-Based Saliency Map Attack (JSMA) inspired by the literature on adversarial attacks. It works by moving in the direction of the most salient feature at a fixed step size in each iteration. Schut et al. (2021) use this optimisation rule in the context of Bayesian classifiers and demonstrate good results in terms of plausibility โ€” how realistic counterfactuals are โ€” and redundancy โ€” how sparse the proposed feature changes are.","category":"page"},{"location":"explanation/optimisers/jsma/#JSMADescent","page":"JSMA","title":"JSMADescent","text":"","category":"section"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"To implement this approach in a reusable manner, we have added JSMA as a Flux optimiser. In particular, we have added a class JSMADescent<:Flux.Optimise.AbstractOptimiser, for which we have overloaded the Flux.Optimise.apply! method. This makes it possible to reuse JSMADescent as an optimiser in composable generators.","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"The optimiser can be used with with any generator as follows:","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"using CounterfactualExplanations.Generators: JSMADescent\ngenerator = GenericGenerator() |>\n gen -> @with_optimiser(gen,JSMADescent(;ฮท=0.1))\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"The figure below compares the resulting counterfactual search outcome to the corresponding outcome with generic Descent.","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"plot(p1,p2,size=(1000,400))","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"(Image: )","category":"page"},{"location":"explanation/optimisers/jsma/","page":"JSMA","title":"JSMA","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/data_catalogue/#Data-Catalogue","page":"Data Catalogue","title":"Data Catalogue","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"To allow researchers and practitioners to test and compare counterfactual generators, the TAIJA environment includes the package TaijaData.jl which comes with pre-processed synthetic and real-world benchmark datasets from different domains. This page explains how to use TaijaData.jl in tandem with CounterfactualExplanations.jl.","category":"page"},{"location":"tutorials/data_catalogue/#Synthetic-Data","page":"Data Catalogue","title":"Synthetic Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The following dictionary can be used to inspect the available methods to generate synthetic datasets where the key indicates the name of the data and the value is the corresponding method:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:synthetic]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 6 entries:\n :overlapping => load_overlapping\n :linearly_separable => load_linearly_separable\n :blobs => load_blobs\n :moons => load_moons\n :circles => load_circles\n :multi_class => load_multi_class","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The chart below shows the generated data using default parameters:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"plts = []\n_height = 200\n_n = length(keys(data_catalogue[:synthetic]))\nfor (key, fun) in data_catalogue[:synthetic]\n data = fun()\n counterfactual_data = DataPreprocessing.CounterfactualData(data...)\n plt = plot()\n scatter!(counterfactual_data, title=key)\n plts = [plts..., plt]\nend\nplot(plts..., size=(_n * _height, _height), layout=(1, _n))","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/data_catalogue/#Real-World-Data","page":"Data Catalogue","title":"Real-World Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"As for real-world data, the same dictionary can be used to inspect the available data from different domains.","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:tabular]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 5 entries:\n :german_credit => load_german_credit\n :california_housing => load_california_housing\n :credit_default => load_credit_default\n :adult => load_uci_adult\n :gmsc => load_gmsc","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"TaijaData.data_catalogue[:vision]","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Dict{Symbol, Function} with 3 entries:\n :fashion_mnist => load_fashion_mnist\n :mnist => load_mnist\n :cifar_10 => load_cifar_10","category":"page"},{"location":"tutorials/data_catalogue/#Loading-Data","page":"Data Catalogue","title":"Loading Data","text":"","category":"section"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"To load or generate any of the datasets listed above, you can just use the corresponding method, for example:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"data = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"Optionally, you can specify how many samples you want to generate like so:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"n = 100\ndata = TaijaData.load_overlapping(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"This also applies to real-world datasets, which by default are loaded in their entirety. If n is supplied, the dataset will be randomly undersampled:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"data = TaijaData.load_mnist(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"The undersampled dataset is automatically balanced:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"sum(counterfactual_data.y; dims=2)","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"10ร—1 Matrix{Int64}:\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10\n 10","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"We can also use a helper function to split the data into train and test sets:","category":"page"},{"location":"tutorials/data_catalogue/","page":"Data Catalogue","title":"Data Catalogue","text":"train_data, test_data = \n CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"how_to_guides/custom_generators/#How-to-add-Custom-Generators","page":"... add custom generators","title":"How to add Custom Generators","text":"","category":"section"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"As we will see in this short tutorial, building custom counterfactual generators is straightforward. We hope that this will facilitate contributions through the community.","category":"page"},{"location":"how_to_guides/custom_generators/#Generic-generator-with-dropout","page":"... add custom generators","title":"Generic generator with dropout","text":"","category":"section"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"To illustrate how custom generators can be implemented we will consider a simple example of a generator that extends the functionality of our GenericGenerator. We have noted elsewhere that the effectiveness of counterfactual explanations depends to some degree on the quality of the fitted model. Another, perhaps trivial, thing to note is that counterfactual explanations are not unique: there are potentially many valid counterfactual paths. One interesting (or silly) idea following these two observations might be to introduce some form of regularization in the counterfactual search. For example, we could use dropout to randomly switch features on and off in each iteration. Without dwelling further on the usefulness of this idea, let us see how it can be implemented.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"The first code chunk below implements two important steps: 1) create an abstract subtype of the AbstractGradientBasedGenerator and 2) create a constructor similar to the GenericConstructor, but with one additional field for the probability of dropout.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"# Abstract suptype:\nabstract type AbstractDropoutGenerator <: AbstractGradientBasedGenerator end\n\n# Constructor:\nstruct DropoutGenerator <: AbstractDropoutGenerator\n loss::Function # loss function\n penalty::Function\n ฮป::AbstractFloat # strength of penalty\n latent_space::Bool\n opt::Any # optimizer\n generative_model_params::NamedTuple\n p_dropout::AbstractFloat # dropout rate\nend\n\n# Instantiate:\ngenerator = DropoutGenerator(\n Flux.logitbinarycrossentropy,\n CounterfactualExplanations.Objectives.distance_l1,\n 0.1,\n false,\n Flux.Optimise.Descent(0.1),\n (;),\n 0.5,\n)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"Next, we define how feature perturbations are generated for our dropout generator: in particular, we extend the relevant function through a method that implemented the dropout logic.","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"using CounterfactualExplanations.Generators\nusing StatsBase\nfunction Generators.generate_perturbations(\n generator::AbstractDropoutGenerator, \n ce::CounterfactualExplanation\n)\n sโ€ฒ = deepcopy(ce.sโ€ฒ)\n new_sโ€ฒ = Generators.propose_state(generator, ce)\n ฮ”sโ€ฒ = new_sโ€ฒ - sโ€ฒ # gradient step\n\n # Dropout:\n set_to_zero = sample(\n 1:length(ฮ”sโ€ฒ),\n Int(round(generator.p_dropout*length(ฮ”sโ€ฒ))),\n replace=false\n )\n ฮ”sโ€ฒ[set_to_zero] .= 0\n return ฮ”sโ€ฒ\nend","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"Finally, we proceed to generate counterfactuals in the same way we always do:","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"# Data and Classifier:\nM = fit_model(counterfactual_data, :DeepEnsemble)\n\n# Factual and Target:\nyhat = predict_label(M, counterfactual_data)\ntarget = 2 # target label\ncandidates = findall(vec(yhat) .!= target)\nchosen = rand(candidates)\nx = select_factual(counterfactual_data, chosen)\n\n# Counterfactual search:\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n num_counterfactuals=5)\n\nplot(ce)","category":"page"},{"location":"how_to_guides/custom_generators/","page":"... add custom generators","title":"... add custom generators","text":"(Image: )","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/parallelization/#Parallelization","page":"Parallelization","title":"Parallelization","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Version 0.1.15 adds support for parallelization through multi-processing. Currently, the only available backend for parallelization is MPI.jl.","category":"page"},{"location":"tutorials/parallelization/#Available-functions","page":"Parallelization","title":"Available functions","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Parallelization is only available for certain functions. To check if a function is parallelizable, you can use parallelizable function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"using CounterfactualExplanations.Evaluation: evaluate, benchmark\nprintln(parallelizable(generate_counterfactual))\nprintln(parallelizable(evaluate))\nprintln(parallelizable(predict_label))","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"true\ntrue\nfalse","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"In the following, we will generate multiple counterfactuals and evaluate them in parallel:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"chosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 1000)\nxs = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/parallelization/#Multi-threading","page":"Parallelization","title":"Multi-threading","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"We first instantiate an ThreadParallelizer object:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"parallelizer = ThreadsParallelizer()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ThreadsParallelizer()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To generate counterfactuals in parallel, we use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ces = @with_parallelizer parallelizer begin\n generate_counterfactual(\n xs,\n target,\n counterfactual_data,\n M,\n generator\n )\nend","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Generating counterfactuals ... 0%| | ETA: 0:01:29 (89.14 ms/it)Generating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:01 ( 1.59 ms/it)\n\n1000-element Vector{AbstractCounterfactualExplanation}:\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n โ‹ฎ\n CounterfactualExplanation\nConvergence: โœ… after 9 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To evaluate counterfactuals in parallel, we again use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"@with_parallelizer parallelizer evaluate(ces)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Evaluating counterfactuals ... 0%| | ETA: 0:07:03 ( 0.42 s/it)Evaluating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:00 ( 0.86 ms/it)\n\n1000-element Vector{Any}:\n Vector[[1.0], Float32[3.2939816], [0.0]]\n Vector[[1.0], Float32[3.019046], [0.0]]\n Vector[[1.0], Float32[3.701171], [0.0]]\n Vector[[1.0], Float32[2.5611918], [0.0]]\n Vector[[1.0], Float32[2.9027307], [0.0]]\n Vector[[1.0], Float32[3.7893882], [0.0]]\n Vector[[1.0], Float32[3.5026522], [0.0]]\n Vector[[1.0], Float32[3.6317568], [0.0]]\n Vector[[1.0], Float32[3.084984], [0.0]]\n Vector[[1.0], Float32[3.2268934], [0.0]]\n Vector[[1.0], Float32[2.834947], [0.0]]\n Vector[[1.0], Float32[3.656587], [0.0]]\n Vector[[1.0], Float32[2.5985842], [0.0]]\n โ‹ฎ\n Vector[[1.0], Float32[4.067538], [0.0]]\n Vector[[1.0], Float32[3.02231], [0.0]]\n Vector[[1.0], Float32[2.748292], [0.0]]\n Vector[[1.0], Float32[2.9483426], [0.0]]\n Vector[[1.0], Float32[3.066149], [0.0]]\n Vector[[1.0], Float32[3.6018147], [0.0]]\n Vector[[1.0], Float32[3.0138078], [0.0]]\n Vector[[1.0], Float32[3.5724509], [0.0]]\n Vector[[1.0], Float32[3.117551], [0.0]]\n Vector[[1.0], Float32[2.9670508], [0.0]]\n Vector[[1.0], Float32[3.4107168], [0.0]]\n Vector[[1.0], Float32[3.0252533], [0.0]]","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Benchmarks can also be run with parallelization by specifying parallelizer argument:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"# Models:\nbmk = benchmark(counterfactual_data; parallelizer = parallelizer)","category":"page"},{"location":"tutorials/parallelization/#MPI","page":"Parallelization","title":"MPI","text":"","category":"section"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"note: Note\nTo use MPI, you need to have MPI installed on your machine. Running the following code straight from a running Julia session will work if you have MPI installed on your machine, but it will be run on a single process. To execute the code on multiple processes, you need to run it from the command line with mpirun or mpiexec. For example, to run a script on 4 processes, you can run the following command from the command line:\n\nmpiexecjl --project -n 4 julia -e 'include(\"docs/src/srcipts/mpi.jl\")'For more information, see MPI.jl. ","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"We first instantiate an MPIParallelizer object:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"import MPI\nMPI.Init()\nparallelizer = MPIParallelizer(MPI.COMM_WORLD; threaded=true)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Precompiling MPIExt\n โœ“ TaijaParallel โ†’ MPIExt\n 1 dependency successfully precompiled in 3 seconds. 255 already precompiled.\n[ Info: Precompiling MPIExt [48137b38-b316-530b-be8a-261f41e68c23]\nโ”Œ Warning: Module TaijaParallel with build ID ffffffff-ffff-ffff-0001-2d458926c256 is missing from the cache.\nโ”‚ This may mean TaijaParallel [bf1c2c22-5e42-4e78-8b6b-92e6c673eeb0] does not support precompilation but is imported by a module that does.\nโ”” @ Base loading.jl:1948\n[ Info: Skipping precompilation since __precompile__(false). Importing MPIExt [48137b38-b316-530b-be8a-261f41e68c23].\n[ Info: Using `MPI.jl` for multi-processing.\n\nRunning on 1 processes.\n\nMPIExt.MPIParallelizer(MPI.Comm(1140850688), 0, 1, nothing, true)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To generate counterfactuals in parallel, we use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"ces = @with_parallelizer parallelizer begin\n generate_counterfactual(\n xs,\n target,\n counterfactual_data,\n M,\n generator\n )\nend","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Generating counterfactuals ... 9%|โ–‹ | ETA: 0:00:01 ( 1.15 ms/it)Generating counterfactuals ... 19%|โ–ˆโ– | ETA: 0:00:01 ( 1.07 ms/it)Generating counterfactuals ... 29%|โ–ˆโ–ˆ | ETA: 0:00:01 ( 1.10 ms/it)Generating counterfactuals ... 39%|โ–ˆโ–ˆโ–Š | ETA: 0:00:01 ( 1.08 ms/it)Generating counterfactuals ... 49%|โ–ˆโ–ˆโ–ˆโ– | ETA: 0:00:01 ( 1.08 ms/it)Generating counterfactuals ... 59%|โ–ˆโ–ˆโ–ˆโ–ˆโ– | ETA: 0:00:00 ( 1.08 ms/it)Generating counterfactuals ... 69%|โ–ˆโ–ˆโ–ˆโ–ˆโ–Š | ETA: 0:00:00 ( 1.08 ms/it)Generating counterfactuals ... 79%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–Œ | ETA: 0:00:00 ( 1.07 ms/it)Generating counterfactuals ... 89%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–Ž| ETA: 0:00:00 ( 1.07 ms/it)Generating counterfactuals ... 99%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‰| ETA: 0:00:00 ( 1.06 ms/it)Generating counterfactuals ... 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| Time: 0:00:01 ( 1.06 ms/it)\n\n1000-element Vector{AbstractCounterfactualExplanation}:\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n โ‹ฎ\n CounterfactualExplanation\nConvergence: โœ… after 9 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"To evaluate counterfactuals in parallel, we again use the parallelize function:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"@with_parallelizer parallelizer evaluate(ces)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"1000-element Vector{Any}:\n Vector[[1.0], Float32[3.0941274], [0.0]]\n Vector[[1.0], Float32[3.0894346], [0.0]]\n Vector[[1.0], Float32[3.5737448], [0.0]]\n Vector[[1.0], Float32[2.6201036], [0.0]]\n Vector[[1.0], Float32[2.8519764], [0.0]]\n Vector[[1.0], Float32[3.7762523], [0.0]]\n Vector[[1.0], Float32[3.4162796], [0.0]]\n Vector[[1.0], Float32[3.6095932], [0.0]]\n Vector[[1.0], Float32[3.1347957], [0.0]]\n Vector[[1.0], Float32[3.0313473], [0.0]]\n Vector[[1.0], Float32[2.7612567], [0.0]]\n Vector[[1.0], Float32[3.6191392], [0.0]]\n Vector[[1.0], Float32[2.610616], [0.0]]\n โ‹ฎ\n Vector[[1.0], Float32[4.0844703], [0.0]]\n Vector[[1.0], Float32[3.0119], [0.0]]\n Vector[[1.0], Float32[2.4461186], [0.0]]\n Vector[[1.0], Float32[3.071967], [0.0]]\n Vector[[1.0], Float32[3.132917], [0.0]]\n Vector[[1.0], Float32[3.5403214], [0.0]]\n Vector[[1.0], Float32[3.0588162], [0.0]]\n Vector[[1.0], Float32[3.5600657], [0.0]]\n Vector[[1.0], Float32[3.2205954], [0.0]]\n Vector[[1.0], Float32[2.896302], [0.0]]\n Vector[[1.0], Float32[3.2603998], [0.0]]\n Vector[[1.0], Float32[3.1369917], [0.0]]","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"tip: Tip\nNote that parallelizable processes can be supplied as input to the macro either as a block or directly as an expression.","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"Benchmarks can also be run with parallelization by specifying parallelizer argument:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"# Models:\nbmk = benchmark(counterfactual_data; parallelizer = parallelizer)","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"The following code snippet shows a complete example script that uses MPI for running a benchmark in parallel:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"using CounterfactualExplanations\nusing CounterfactualExplanations.Evaluation: benchmark\nusing CounterfactualExplanations.Models\nimport MPI\n\nMPI.Init()\n\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, :Linear)\nfactual = 1\ntarget = 2\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 100)\nxs = select_factual(counterfactual_data, chosen)\ngenerator = GenericGenerator()\n\nparallelizer = MPIParallelizer(MPI.COMM_WORLD)\n\nbmk = benchmark(counterfactual_data; parallelizer=parallelizer)\n\nMPI.Finalize()","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"The file can be executed from the command line as follows:","category":"page"},{"location":"tutorials/parallelization/","page":"Parallelization","title":"Parallelization","text":"mpiexecjl --project -n 4 julia -e 'include(\"docs/src/srcipts/mpi.jl\")'","category":"page"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"EditURL = \"https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/blob/master/CHANGELOG.md\"","category":"page"},{"location":"release-notes/#Changelog","page":"Release Notes","title":"Changelog","text":"","category":"section"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"All notable changes to this project will be documented in this file.","category":"page"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.","category":"page"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"Note: We try to adhere to these practices as of version 1.1.1.","category":"page"},{"location":"release-notes/#[Unreleased]","page":"Release Notes","title":"[Unreleased]","text":"","category":"section"},{"location":"release-notes/#Added","page":"Release Notes","title":"Added","text":"","category":"section"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"Adds a section on Convergence to the documentation, Changelog.jl functionality and a few doc tests. #429","category":"page"},{"location":"release-notes/#[1.1.2]-2024-04-16","page":"Release Notes","title":"[1.1.2] - 2024-04-16","text":"","category":"section"},{"location":"release-notes/#Changed","page":"Release Notes","title":"Changed","text":"","category":"section"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"Replaces the GIF in the README and introduction of docs for a static image. ","category":"page"},{"location":"release-notes/#[1.1.1]-2024-04-15","page":"Release Notes","title":"[1.1.1] - 2024-04-15","text":"","category":"section"},{"location":"release-notes/#Added-2","page":"Release Notes","title":"Added","text":"","category":"section"},{"location":"release-notes/","page":"Release Notes","title":"Release Notes","text":"Added tests for LaplaceRedux extension. Bumped upper compat bound for LaplaceRedux.jl. #428","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanation","category":"page"},{"location":"tutorials/#Tutorials","page":"Overview","title":"Tutorials","text":"","category":"section"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"In this section, you will find a series of tutorials that should help you gain a basic understanding of Conformal Prediction and how to apply it in Julia using this package.","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"Tutorials are lessons that take the reader by the hand through a series of steps to complete a project of some kind. Tutorials are learning-oriented.โ€” Diรกtaxis","category":"page"},{"location":"tutorials/","page":"Overview","title":"Overview","text":"In other words, you come here because you are new to this topic and are looking for a first peek at the methodology and code ๐Ÿซฃ.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"explanation/generators/growing_spheres/#GrowingSpheres","page":"GrowingSpheres","title":"GrowingSpheres","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Growing Spheres refers to the generator introduced by Laugel et al. (2017). Our implementation takes inspiration from the CARLA library.","category":"page"},{"location":"explanation/generators/growing_spheres/#Principle-of-the-Proposed-Approach","page":"GrowingSpheres","title":"Principle of the Proposed Approach","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"In order to interpret a prediction through comparison, the Growing Spheres algorithm focuses on finding an observation belonging to the other class and answers the question: โ€œConsidering an observation and a classifier, what is the minimal change we need to apply in order to change the prediction of this observation?โ€. This problem is similar to inverse classification but applied to interpretability.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Explaining how to change a prediction can help the user understand what the model considers as locally important. The Growing Spheres approach provides insights into the classifierโ€™s behavior without claiming any causal knowledge. It differs from other interpretability approaches and is not concerned with the global behavior of the model. Instead, it aims to provide local insights into the classifierโ€™s decision-making process.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The algorithm finds the closest โ€œennemyโ€ observation, which is an observation classified into the other class than the input observation. The final explanation is the difference vector between the input observation and the ennemy.","category":"page"},{"location":"explanation/generators/growing_spheres/#Finding-the-Closest-Ennemy","page":"GrowingSpheres","title":"Finding the Closest Ennemy","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The algorithm solves the following minimization problem to find the closest ennemy:","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"e^* = arg min_e in X c(x e) f(e) neq f(x) ","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"The cost function c(x, e) is defined as:","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"c(x e) = x - e_2 + gamma x - e_0","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"where ||.||_2 is the Euclidean norm and ||.||_0 is the sparsity measure. The weight gamma balances the importance of sparsity in the cost function.","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"To approximate the solution, the Growing Spheres algorithm uses a two-step heuristic approach. The first step is the Generation phase, where observations are generated in spherical layers around the input observation. The second step is the Feature Selection phase, where the generated observation with the smallest change in each feature is selected.","category":"page"},{"location":"explanation/generators/growing_spheres/#Example","page":"GrowingSpheres","title":"Example","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"generator = GrowingSpheresGenerator()\nM = fit_model(counterfactual_data, :DeepEnsemble)\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"(Image: )","category":"page"},{"location":"explanation/generators/growing_spheres/#References","page":"GrowingSpheres","title":"References","text":"","category":"section"},{"location":"explanation/generators/growing_spheres/","page":"GrowingSpheres","title":"GrowingSpheres","text":"Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ arXiv. https://doi.org/10.48550/arXiv.1712.08443.","category":"page"},{"location":"contribute/#Contribute","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on. If you have an idea for a new feature or want to report a bug, please open a new issue.","category":"page"},{"location":"contribute/#Development","page":"๐Ÿ›  Contribute","title":"Development","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"If your looking to contribute code, it may be helpful to check out the Explanation section of the docs.","category":"page"},{"location":"contribute/#Testing","page":"๐Ÿ›  Contribute","title":"Testing","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"Please always make sure to add tests for any new features or changes.","category":"page"},{"location":"contribute/#Documentation","page":"๐Ÿ›  Contribute","title":"Documentation","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"If you add new features or change existing ones, please make sure to update the documentation accordingly. The documentation is written in Documenter.jl and is located in the docs/src folder.","category":"page"},{"location":"contribute/#Log-Changes","page":"๐Ÿ›  Contribute","title":"Log Changes","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"As of version 1.1.1, we have tried to be more stringent about logging changes. Please make sure to add a note to the CHANGELOG.md file for any changes you make. It is sufficient to add a note under the Unreleased section.","category":"page"},{"location":"contribute/#General-Pointers","page":"๐Ÿ›  Contribute","title":"General Pointers","text":"","category":"section"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"There are also some general pointers for people looking to contribute to any of our Taija packages here.","category":"page"},{"location":"contribute/","page":"๐Ÿ›  Contribute","title":"๐Ÿ›  Contribute","text":"Please follow the SciML ColPrac guide.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/clap_roar/#ClaPROARGenerator","page":"ClaPROAR","title":"ClaPROARGenerator","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The ClaPROARGenerator was introduced in Altmeyer et al. (2023).","category":"page"},{"location":"explanation/generators/clap_roar/#Description","page":"ClaPROAR","title":"Description","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The acronym Clap stands for classifier-preserving. The approach is loosely inspired by ROAR (Upadhyay, Joshi, and Lakkaraju 2021). Altmeyer et al. (2023) propose to explicitly penalize the loss incurred by the classifer when evaluated on the counterfactual x^prime at given parameter values. Formally, we have","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"beginaligned\ntextextcost(f(mathbfs^prime)) = l(M(f(mathbfs^prime))y^prime)\nendaligned","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"for each counterfactual k where l denotes the loss function used to train M. This approach is based on the intuition that (endogenous) model shifts will be triggered by counterfactuals that increase classifier loss (Altmeyer et al. 2023).","category":"page"},{"location":"explanation/generators/clap_roar/#Usage","page":"ClaPROAR","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"generator = ClaPROARGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"(Image: )","category":"page"},{"location":"explanation/generators/clap_roar/#Comparison-to-GenericGenerator","page":"ClaPROAR","title":"Comparison to GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"The figure below compares the outcome for the GenericGenerator and the ClaPROARGenerator.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"(Image: )","category":"page"},{"location":"explanation/generators/clap_roar/#References","page":"ClaPROAR","title":"References","text":"","category":"section"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"explanation/generators/clap_roar/","page":"ClaPROAR","title":"ClaPROAR","text":"Upadhyay, Sohini, Shalmali Joshi, and Himabindu Lakkaraju. 2021. โ€œTowards Robust and Reliable Algorithmic Recourse.โ€ https://arxiv.org/abs/2102.13620.","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/architecture/#Package-Architecture","page":"Package Architecture","title":"Package Architecture","text":"","category":"section"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"The diagram below provides an overview of the package architecture. It is built around two core modules that are designed to be as extensible as possible through dispatch: 1) Models is concerned with making any arbitrary model compatible with the package; 2) Generators is used to implement arbitrary counterfactual search algorithms.[1]","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"The core function of the package, generate_counterfactual, uses an instance of type AbstractFittedModel produced by the Models module and an instance of type AbstractGenerator produced by the Generators module.","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"Metapackages from the Taija ecosystem provide additional functionality such as datasets, language interoperability, parallelization, and plotting. The CounterfactualExplanations package is designed to be used in conjunction with these metapackages, but can also be used as a standalone package.","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"(Image: )","category":"page"},{"location":"explanation/architecture/","page":"Package Architecture","title":"Package Architecture","text":"[1] We have made an effort to keep the code base a flexible and extensible as possible, but cannot guarantee at this point that any counterfactual generator can be implemented without further adaptation.","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"explanation/#Explanation","page":"Overview","title":"Explanation","text":"","category":"section"},{"location":"explanation/","page":"Overview","title":"Overview","text":"In this section you will find detailed explanations about the methodology and code.","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"Explanation clarifies, deepens and broadens the readerโ€™s understanding of a subject.โ€” Diรกtaxis","category":"page"},{"location":"explanation/","page":"Overview","title":"Overview","text":"In other words, you come here because you are interested in understanding how all of this actually works ๐Ÿค“.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"extensions/laplace_redux/#[LaplaceRedux.jl](https://github.com/JuliaTrustworthyAI/LaplaceRedux.jl)","page":"LaplaceRedux","title":"LaplaceRedux.jl","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"LaplaceRedux.jl is one of Taijaโ€™s own packages that provides a framework for Effortless Bayesian Deep Learning through Laplace Approximation for Flux.jl neural networks. The methodology was first proposed by Immer, Korzepa, and Bauer (2020) and implemented in Python by Daxberger et al. (2021). This is relevant to the work on counterfactual explanations (CE), because research has shown that counterfactual explanations for Bayesian models are typically more plausible, because Bayesian models are able to capture the uncertainty in the data (Schut et al. 2021).","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"tip: Read More\nTo learn more about Laplace Redux, head over to the official documentation.","category":"page"},{"location":"extensions/laplace_redux/#Example","page":"LaplaceRedux","title":"Example","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"The extension will be loaded automatically when loading the LaplaceRedux package (assuming the CounterfactualExplanations package is also loaded).","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"using LaplaceRedux","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Next, we will fit a neural network with Laplace Approximation to the moons dataset using our standard package API for doing so. By default, the Bayesian prior is optimized through empirical Bayes using the LaplaceRedux package.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"# Fit model to data:\ndata = CounterfactualData(load_moons()...)\nM = fit_model(data, :LaplaceRedux; n_hidden=16)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"LaplaceReduxExt.LaplaceReduxModel(Laplace(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, :all, nothing, :full, LaplaceRedux.Curvature.GGN(Chain(Dense(2 => 16, relu), Dense(16 => 2)), :classification, Flux.Losses.logitcrossentropy, Array{Float32}[[-1.3098596 0.59241515; 0.91760206 0.02950162; โ€ฆ ; -0.018356863 0.12850936; -0.5381665 -0.7872097], [-0.2581085, -0.90997887, -0.5418944, -0.23735572, 0.81020063, -0.3033359, -0.47902864, -0.6432098, -0.038013518, 0.028280666, 0.009903266, -0.8796683, 0.41090682, 0.011093224, -0.1580453, 0.7911349], [3.092321 -2.4660816 โ€ฆ -0.3446268 -1.465249; -2.9468734 3.167357 โ€ฆ 0.31758657 1.7140366], [-0.3107697, 0.31076983]], 1.0, :all, nothing), 1.0, 0.0, Float32[-1.3098596, 0.91760206, 0.5239727, -1.1579771, -0.851813, -1.9411169, 0.47409698, 0.6679365, 0.8944433, 0.663116 โ€ฆ -0.3172857, 0.15530388, 1.3264753, -0.3506721, -0.3446268, 0.31758657, -1.465249, 1.7140366, -0.3107697, 0.31076983], [0.10530027048093525 0.0 โ€ฆ 0.0 0.0; 0.0 0.10530027048093525 โ€ฆ 0.0 0.0; โ€ฆ ; 0.0 0.0 โ€ฆ 0.10530027048093525 0.0; 0.0 0.0 โ€ฆ 0.0 0.10530027048093525], [0.10066431429751965 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 20.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 1.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 1.0101451389641771], [1.1006643142975197 0.0 โ€ฆ -0.030656783425475176 0.030656334963944154; 0.0 21.93513766443357 โ€ฆ -2.3185940232360736 2.3185965484008193; โ€ฆ ; -0.030656783425475176 -2.3185940232360736 โ€ฆ 2.0101450999063672 -1.0101448118057204; 0.030656334963944154 2.3185965484008193 โ€ฆ -1.0101448118057204 2.010145138964177], [0.9412600568016627 0.003106911671721699 โ€ฆ 0.003743740333409532 -0.003743452315572739; 0.003106912946573237 0.6539263732691709 โ€ฆ 0.0030385955287734246 -0.0030390041204196414; โ€ฆ ; 0.0037437406323562283 0.003038591829991259 โ€ฆ 0.9624905710233649 0.03750911813897676; -0.0037434526145225856 -0.0030390004216833593 โ€ฆ 0.03750911813898124 0.9624905774453485], 82, 250, 2, 997.8087484836578), :classification_multi)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Finally, we select a factual instance and generate a counterfactual explanation for it using the generic gradient-based CE method.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"# Select a factual instance:\ntarget = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, data) .== factual))\nx = select_factual(data, chosen)\n\n# Generate counterfactual explanation:\nฮท = 0.01\ngenerator = GenericGenerator(; opt=Descent(ฮท), ฮป=0.01)\nconv = CounterfactualExplanations.Convergence.DecisionThresholdConvergence(;\n decision_threshold=0.9, max_iter=100\n)\nce = generate_counterfactual(x, target, data, M, generator; convergence=conv)\nplot(ce, alpha=0.1)","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"(Image: )","category":"page"},{"location":"extensions/laplace_redux/#References","page":"LaplaceRedux","title":"References","text":"","category":"section"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Daxberger, Erik, Agustinus Kristiadi, Alexander Immer, Runa Eschenhagen, Matthias Bauer, and Philipp Hennig. 2021. โ€œLaplace Redux-Effortless Bayesian Deep Learning.โ€ Advances in Neural Information Processing Systems 34.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Immer, Alexander, Maciej Korzepa, and Matthias Bauer. 2020. โ€œImproving Predictions of Bayesian Neural Networks via Local Linearization.โ€ https://arxiv.org/abs/2008.08400.","category":"page"},{"location":"extensions/laplace_redux/","page":"LaplaceRedux","title":"LaplaceRedux","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"Random.seed!(42)\n# Counteractual data and model:\ndata = TaijaData.load_linearly_separable()\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, :Linear)\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)\n\n# Search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"data_large = TaijaData.load_linearly_separable(100000)\ncounterfactual_data_large = DataPreprocessing.CounterfactualData(data_large...)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"@time generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"contribute/performance/","page":"-","title":"-","text":"@time generate_counterfactual(x, target, counterfactual_data_large, M, generator)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/feature_tweak/#FeatureTweakGenerator","page":"FeatureTweak","title":"FeatureTweakGenerator","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Feature Tweak refers to the generator introduced by Tolomei et al. (2017). Our implementation takes inspiration from the featureTweakPy library.","category":"page"},{"location":"explanation/generators/feature_tweak/#Description","page":"FeatureTweak","title":"Description","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Feature Tweak is a powerful recourse algorithm for ensembles of tree-based classifiers such as random forests. Though the problem of understanding how an input to an ensemble model could be transformed in such a way that the model changes its original prediction has been proven to be NP-hard (Tolomei et al. 2017), Feature Tweak provides an algorithm that manages to tractably solve this problem in multiple real-world applications. An example of a problem Feature Tweak is able to efficiently solve, explored in depth in Tolomei et al. (2017) is the problem of transforming an advertisement that has been classified by the ensemble model as a low-quality advertisement to a high-quality one through small changes to its features. With the help of Feature Tweak, advertisers can both learn about the reasons a particular ad was marked to have a low quality, as well as receive actionable suggestions about how to convert a low-quality ad into a high-quality one.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Though Feature Tweak is a powerful way of avoiding brute-force search in an exponential search space, it does not come without disadvantages. The primary limitations of the approach are that itโ€™s currently only applicable to tree-based classifiers and works only in the setting of binary classification. Another problem is that though the algorithm avoids exponential-time search, it is often still computationally expensive. The algorithm may be improved in the future to tackle all of these shortcomings.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"The following equation displays how a true negative instance x can be transformed into a positively predicted instance xโ€™. To be more precise, xโ€™ is the best possible transformation among all transformations **x***, computed with a cost function ฮด.","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"beginaligned\nmathbfx^prime = arg_mathbfx^* min delta(mathbfx mathbfx^*) hatf(mathbfx) = -1 wedge hatf(mathbfx^*) = +1 \nendaligned","category":"page"},{"location":"explanation/generators/feature_tweak/#Example","page":"FeatureTweak","title":"Example","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"In this example we apply the Feature Tweak algorithm to a decision tree and a random forest trained on the moons dataset. We first load the data and fit the models:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"n = 500\ncounterfactual_data = CounterfactualData(TaijaData.load_moons(n)...)\n\n# Classifiers\ndecision_tree = CounterfactualExplanations.Models.fit_model(\n counterfactual_data, :DecisionTree; max_depth=5, min_samples_leaf=3\n)\nforest = Models.fit_model(counterfactual_data, :RandomForest)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Next, we select a point to explain and a target class to transform the point to. We then search for counterfactuals using the FeatureTweakGenerator:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"# Select a point to explain:\nx = float32.([1, -0.5])[:,:]\nfactual = Models.predict_label(forest, x)\ntarget = counterfactual_data.y_levels[findall(counterfactual_data.y_levels != factual)][1]\n\n# Search for counterfactuals:\ngenerator = FeatureTweakGenerator(ฯต=0.1)\ntree_counterfactual = generate_counterfactual(\n x, target, counterfactual_data, decision_tree, generator\n)\nforest_counterfactual = generate_counterfactual(\n x, target, counterfactual_data, forest, generator\n)","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"The resulting counterfactuals are shown below:","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"p1 = plot(\n tree_counterfactual;\n colorbar=false,\n title=\"Decision Tree\",\n)\n\np2 = plot(\n forest_counterfactual; title=\"Random Forest\",\n colorbar=false,\n)\n\ndisplay(plot(p1, p2; size=(800, 400)))","category":"page"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"(Image: )","category":"page"},{"location":"explanation/generators/feature_tweak/#References","page":"FeatureTweak","title":"References","text":"","category":"section"},{"location":"explanation/generators/feature_tweak/","page":"FeatureTweak","title":"FeatureTweak","text":"Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/clue/#CLUEGenerator","page":"CLUE","title":"CLUEGenerator","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"In this tutorial, we introduce the CLUEGenerator, a counterfactual generator based on the Counterfactual Latent Uncertainty Explanations (CLUE) method proposed by Antorรกn et al. (2020).","category":"page"},{"location":"explanation/generators/clue/#Description","page":"CLUE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUEGenerator leverages differentiable probabilistic models, such as Bayesian Neural Networks (BNNs), to estimate uncertainty in predictions. It aims to provide interpretable counterfactual explanations by identifying input patterns that lead to predictive uncertainty. The generator utilizes a latent variable framework and employs a decoder from a variational autoencoder (VAE) to generate counterfactual samples in latent space.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUE algorithm minimizes a loss function that combines uncertainty estimates and the distance between the generated counterfactual and the original input. By optimizing this loss function iteratively, the CLUEGenerator generates counterfactuals that are similar to the original observation but assigned low uncertainty.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The formula for predictive entropy is as follow:","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"beginaligned\nH(y^*x^* D) = - sum_k=1^K p(y^*=c_kx^* D) log p(y^*=c_kx^* D)\nendaligned","category":"page"},{"location":"explanation/generators/clue/#Usage","page":"CLUE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"While using one must keep in mind that the CLUE algorithim is meant to find a more robust datapoint of the same class, using CLUE generator without any additional penalties/losses will mean that it is not a counterfactual generator. The generated result will be of the same class as the original input, but a more robust datapoint.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"CLUE works best for BNNโ€™s. The CLUEGenerator can be used with any differentiable probabilistic model, but the results may not be as good as with BNNs.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"The CLUEGenerator can be used in the following manner:","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"generator = CLUEGenerator()\nM = fit_model(counterfactual_data, :DeepEnsemble)\nconv = CounterfactualExplanations.Convergence.MaxIterConvergence(max_iter=1000)\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n convergence=conv)\nplot(ce)","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"(Image: )","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Extra: The CLUE generator can also be used upon already having achieved a counterfactual with a different generator. In this case, you can use CLUE and make the counterfactual more robust.","category":"page"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Note: The above documentation is based on the information provided in the CLUE paper. Please refer to the original paper for more detailed explanations and implementation specifics.","category":"page"},{"location":"explanation/generators/clue/#References","page":"CLUE","title":"References","text":"","category":"section"},{"location":"explanation/generators/clue/","page":"CLUE","title":"CLUE","text":"Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/revise/#REVISEGenerator","page":"REVISE","title":"REVISEGenerator","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"REVISE is a Latent Space generator introduced by Joshi et al. (2019).","category":"page"},{"location":"explanation/generators/revise/#Description","page":"REVISE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The current consensus in the literature is that Counterfactual Explanations should be realistic: the generated counterfactuals should look like they were generated by the data-generating process (DGP) that governs the problem at hand. With respect to Algorithmic Recourse, it is certainly true that counterfactuals should be realistic in order to be actionable for individuals.[1] To address this need, researchers have come up with various approaches in recent years. Among the most popular approaches is Latent Space Search, which was first proposed in Joshi et al. (2019): instead of traversing the feature space directly, this approach relies on a separate generative model that learns a latent space representation of the DGP. Assuming the generative model is well-specified, access to the learned latent embeddings of the data comes with two advantages:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Since the learned DGP is encoded in the latent space, the generated counterfactuals will respect the learned representation of the data. In practice, this means that counterfactuals will be realistic.\nThe latent space is typically a compressed (i.e.ย lower dimensional) version of the feature space. This makes the counterfactual search less costly.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"There are also certain disadvantages though:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Learning generative models is (typically) an expensive task, which may well outweigh the benefits associated with utlimately traversing a lower dimensional space.\nIf the generative model is poorly specified, this will affect the quality of the counterfactuals.[2]","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Anyway, traversing latent embeddings is a powerful idea that may be very useful depending on the specific context. This tutorial introduces the concept and how it is implemented in this package.","category":"page"},{"location":"explanation/generators/revise/#Usage","page":"REVISE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"generator = REVISEGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#Worked-2D-Examples","page":"REVISE","title":"Worked 2D Examples","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Below we load 2D data and train a VAE on it and plot the original samples against their reconstructions.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# output: true\n\ncounterfactual_data = CounterfactualData(load_overlapping()...)\nX = counterfactual_data.X\ny = counterfactual_data.y\ninput_dim = size(X, 1)\nusing CounterfactualExplanations.GenerativeModels: VAE, train!, reconstruct\nvae = VAE(input_dim; nll=Flux.Losses.mse, epochs=100, ฮป=0.01, latent_dim=2, hidden_dim=32)\nflux_training_params.verbose = true\ntrain!(vae, X, y)\nXฬ‚ = reconstruct(vae, X)[1]\np0 = scatter(X[1, :], X[2, :], color=:blue, label=\"Original\", xlab=\"xโ‚\", ylab=\"xโ‚‚\")\nscatter!(Xฬ‚[1, :], Xฬ‚[2, :], color=:orange, label=\"Reconstructed\", xlab=\"xโ‚\", ylab=\"xโ‚‚\")\np1 = scatter(X[1, :], Xฬ‚[1, :], color=:purple, label=\"\", xlab=\"xโ‚\", ylab=\"xฬ‚โ‚\")\np2 = scatter(X[2, :], Xฬ‚[2, :], color=:purple, label=\"\", xlab=\"xโ‚‚\", ylab=\"xฬ‚โ‚‚\")\nplt2 = plot(p1,p2, layout=(1,2), size=(800, 400))\nplot(p0, plt2, layout=(2,1), size=(800, 600))","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Next, we train a simple MLP for the classification task. Then we determine a target and factual class for our counterfactual search and select a random factual instance to explain.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"M = fit_model(counterfactual_data, :MLP)\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Finally, we generate and visualize the generated counterfactual:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Search:\ngenerator = REVISEGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#3D-Example","page":"REVISE","title":"3D Example","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"To illustrate the notion of Latent Space search, letโ€™s look at an example involving 3-dimensional input data, which we can still visualize. The code chunk below loads the data and implements the counterfactual search.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Data and Classifier:\ncounterfactual_data = CounterfactualData(load_blobs(k=3)...)\nX = counterfactual_data.X\nys = counterfactual_data.output_encoder.labels.refs\nM = fit_model(counterfactual_data, :MLP)\n\n# Randomly selected factual:\nx = select_factual(counterfactual_data,rand(1:size(counterfactual_data.X,2)))\ny = predict_label(M, counterfactual_data, x)[1]\ntarget = counterfactual_data.y_levels[counterfactual_data.y_levels .!= y][1]\n\n# Generate recourse:\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The figure below demonstrates the idea of searching counterfactuals in a lower-dimensional latent space: on the left, we can see the counterfactual search in the 3-dimensional feature space, while on the right we can see the corresponding search in the latent space.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#MNIST-data","page":"REVISE","title":"MNIST data","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Letโ€™s carry the ideas introduced above over to a more complex example. The code below loads MNIST data as well as a pre-trained classifier and generative model for the data.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"using CounterfactualExplanations.Models: load_mnist_mlp, load_mnist_ensemble, load_mnist_vae\ncounterfactual_data = CounterfactualData(load_mnist()...)\nX, y = CounterfactualExplanations.DataPreprocessing.unpack_data(counterfactual_data)\ninput_dim, n_obs = size(counterfactual_data.X)\nM = load_mnist_mlp()\nvae = load_mnist_vae()","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The F1-score of our pre-trained image classifier on test data is: 0.94","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Before continuing, we supply the pre-trained generative model to our data container:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"counterfactual_data.generative_model = vae # assign generative model","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Now letโ€™s define a factual and target label:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"# Randomly selected factual:\nRandom.seed!(2023)\nfactual_label = 8\nx = reshape(X[:,rand(findall(predict_label(M, counterfactual_data).==factual_label))],input_dim,1)\ntarget = 3\nfactual = predict_label(M, counterfactual_data, x)[1]","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Using REVISE, we are going to turn a randomly drawn 8 into a 3.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The API call is the same as always:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"ฮณ = 0.95\nconv = \n CounterfactualExplanations.Convergence.DecisionThresholdConvergence(decision_threshold=ฮณ)\n# Define generator:\ngenerator = REVISEGenerator(opt=Flux.Adam(0.1))\n# Generate recourse:\nce = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence=conv)","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"The chart below shows the results:","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"(Image: )","category":"page"},{"location":"explanation/generators/revise/#References","page":"REVISE","title":"References","text":"","category":"section"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"[1] In general, we believe that there may be a trade-off between creating counterfactuals that respect the DGP vs.ย counterfactuals reflect the behaviour of the black-model in question - both accurately and complete.","category":"page"},{"location":"explanation/generators/revise/","page":"REVISE","title":"REVISE","text":"[2] We believe that there is another potentially crucial disadvantage of relying on a separate generative model: it reallocates the task of learning realistic explanations for the data from the black-box model to the generative model.","category":"page"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/optimisers/overview/#Optimisation-Rules","page":"Overview","title":"Optimisation Rules","text":"","category":"section"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"Counterfactual search is an optimization problem. Consequently, the choice of the optimisation rule affects the generated counterfactuals. In the short term, we aim to enable users to choose any of the available Flux optimisers. This has not been sufficiently tested yet, and you may run into issues.","category":"page"},{"location":"explanation/optimisers/overview/#Custom-Optimisation-Rules","page":"Overview","title":"Custom Optimisation Rules","text":"","category":"section"},{"location":"explanation/optimisers/overview/","page":"Overview","title":"Overview","text":"Flux optimisers are specifically designed for deep learning, and in particular, for learning model parameters. In counterfactual search, the features are the free parameters that we are optimising over. To this end, some custom optimisation rules are necessary to incorporate ideas presented in the literature. In the following, we introduce those rules.","category":"page"},{"location":"CHANGELOG/#Changelog","page":"Changelog","title":"Changelog","text":"","category":"section"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"All notable changes to this project will be documented in this file.","category":"page"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.","category":"page"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"Note: We try to adhere to these practices as of version 1.1.1.","category":"page"},{"location":"CHANGELOG/#[Unreleased]","page":"Changelog","title":"[Unreleased]","text":"","category":"section"},{"location":"CHANGELOG/#Added","page":"Changelog","title":"Added","text":"","category":"section"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"Adds a section on Convergence to the documentation, Changelog.jl functionality and a few doc tests. [#429]","category":"page"},{"location":"CHANGELOG/#[1.1.2]-2024-04-16","page":"Changelog","title":"[1.1.2] - 2024-04-16","text":"","category":"section"},{"location":"CHANGELOG/#Changed","page":"Changelog","title":"Changed","text":"","category":"section"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"Replaces the GIF in the README and introduction of docs for a static image. ","category":"page"},{"location":"CHANGELOG/#[1.1.1]-2024-04-15","page":"Changelog","title":"[1.1.1] - 2024-04-15","text":"","category":"section"},{"location":"CHANGELOG/#Added-2","page":"Changelog","title":"Added","text":"","category":"section"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"Added tests for LaplaceRedux extension. Bumped upper compat bound for LaplaceRedux.jl. [#428]","category":"page"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"","category":"page"},{"location":"CHANGELOG/","page":"Changelog","title":"Changelog","text":"[#428]: https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues/428 [#429]: https://github.com/juliatrustworthyai/CounterfactualExplanations.jl/issues/429","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/convergence/#convergence","page":"Convergence","title":"Convergence","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"The search for counterfactuals can be seen as an optimization problem, where the goal is to find a point in the input space. One questions that has received surprisingly little attention is how to determine when the search has converged. In a recent paper, we have briefly discussed why it is important to consider convergence (Altmeyer et al. 2024):","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"One intuitive way to specify convergence is in terms of threshold probabilities: once the predicted probability p(y^+x^prime) exceeds some user-defined threshold ฮณ such that the counterfactual is valid, we could consider the search to have converged. In the binary case, for example, convergence could be defined as p(y^+x^prime) 05 in this sense. Note, however, how this can be expected to yield counterfactuals in the proximity of the decision boundary, a region characterized by high aleatoric uncertainty. In other words, counterfactuals generated in this way would generally not be plausible. To avoid this from happening, we specify convergence in terms of gradients approaching zero for all our experiments and all of our generators. This is allows us to get a cleaner read on how the different counterfactual search objectives affect counterfactual outcomes.","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"In the paper, we were primarily interested in benchmarking counterfactuals generated by different search objectives. In other contexts, however, it may be more appropriate to specify convergence in terms of threshold probabilities. Our package allows you to specify convergence in terms of gradients, threshold probabilities or simply in terms of the total number of iterations. In this section, we will show you how to do this.","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"using CounterfactualExplanations.Convergence\ngenerator = GenericGenerator(ฮป=0.01)","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"GradientBasedGenerator(nothing, CounterfactualExplanations.Objectives.distance_l1, 0.01, false, false, Descent(0.1), NamedTuple())","category":"page"},{"location":"tutorials/convergence/#Convergence-in-terms-of-gradients","page":"Convergence","title":"Convergence in terms of gradients","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"As gradients approach zero, the conditions defined by the search objective and hence the generator are satisfied. We therefore refere to this type of convergece criterium as GeneratorConditionsConvergence","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"conv = GeneratorConditionsConvergence(gradient_tol=0.01, max_iter=1000)\nce_gen = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"CounterfactualExplanation\nConvergence: โœ… after 179 steps.","category":"page"},{"location":"tutorials/convergence/#Convergence-in-terms-of-threshold-probabilities","page":"Convergence","title":"Convergence in terms of threshold probabilities","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"In this case, the search is considered to have converged once the predicted probability p(y^+x^prime) exceeds some user-defined threshold ฮณ such that the counterfactual is valid. We refer to this type of convergence criterium as DecisionThresholdConvergence.","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"conv = DecisionThresholdConvergence(decision_threshold=0.75)\nce_dec = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"CounterfactualExplanation\nConvergence: โœ… after 9 steps.","category":"page"},{"location":"tutorials/convergence/#Convergence-in-terms-of-the-total-number-of-iterations","page":"Convergence","title":"Convergence in terms of the total number of iterations","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"In this case, the search is considered to have converged once the total number of iterations exceeds some user-defined threshold max_iter. We refer to this type of convergence criterium as MaxIterConvergence.","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"conv = MaxIterConvergence(max_iter=25)\nce_max = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"CounterfactualExplanation\nConvergence: โœ… after 25 steps.","category":"page"},{"location":"tutorials/convergence/#Comparison","page":"Convergence","title":"Comparison","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"plts = []\nfor (ce, titl) in zip([ce_gen, ce_dec, ce_max], [\"Gradient Convergence\", \"Decision Threshold Convergence\", \"Max Iterations Convergence\"])\n push!(plts, plot(ce; title=titl, cbar=false))\nend\nplot(plts..., layout=(1,3), size=(1200, 380))","category":"page"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"(Image: )","category":"page"},{"location":"tutorials/convergence/#References","page":"Convergence","title":"References","text":"","category":"section"},{"location":"tutorials/convergence/","page":"Convergence","title":"Convergence","text":"Altmeyer, Patrick, Mojtaba Farmanbar, Arie van Deursen, and Cynthia CS Liem. 2024. โ€œFaithful Model Explanations Through Energy-Constrained Conformal Counterfactuals.โ€ In Proceedings of the AAAI Conference on Artificial Intelligence, 38:10829โ€“37. 10.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/data_preprocessing/#Handling-Data","page":"Handling Data","title":"Handling Data","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The package works with custom data containers that contain the input and output data as well as information about the type and mutability of features. In this tutorial, we will see how data can be prepared for use with the package.","category":"page"},{"location":"tutorials/data_preprocessing/#Basic-Functionality","page":"Handling Data","title":"Basic Functionality","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To demonstrate the basic way to prepare data, letโ€™s look at a standard benchmark dataset: Fisherโ€™s classic iris dataset. We can use MLDatasets to load this data.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"dataset = Iris()","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Our data constructor CounterfactualData needs at least two inputs: features X and targets y.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"X = dataset.features\ny = dataset.targets","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we convert the input data to a Tables.MatrixTable (following MLJ.jl) convention. Concerning the target variable, we just assign grab the first column of the data frame.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"X = table(Tables.matrix(X))\ny = y[:,1]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Now we can feed these two ingredients to our constructor:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(X, y)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Under the hood, the constructor performs basic preprocessing steps. For example, the output variable y is automatically one-hot encoded:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data.y","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"3ร—150 Matrix{Bool}:\n 1 1 1 1 1 1 1 1 1 1 1 1 1 โ€ฆ 0 0 0 0 0 0 0 0 0 0 0 0\n 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Similarly, a transformer used to scale continuous input features is automatically fitted:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data.dt","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"ZScoreTransform{Float64, Vector{Float64}}(4, 2, [5.843333333333335, 3.0540000000000007, 3.7586666666666693, 1.1986666666666672], [0.8280661279778629, 0.4335943113621737, 1.7644204199522617, 0.7631607417008414])","category":"page"},{"location":"tutorials/data_preprocessing/#Categorical-Features","page":"Handling Data","title":"Categorical Features","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"For the counterfactual search, it is important to distinguish between continuous and categorical features. This is because categorical features cannot be perturbed arbitrarily: they can take specific discrete values, but not just any value on the real line.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Consider the following example:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"y = rand([1,0],4)\nX = (\n name=categorical([\"Danesh\", \"Lee\", \"Mary\", \"John\"]),\n grade=categorical([\"A\", \"B\", \"A\", \"C\"], ordered=true),\n sex=categorical([\"male\",\"female\",\"male\",\"male\"]),\n height=[1.85, 1.67, 1.5, 1.67],\n)\nschema(X)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name โ”‚ Multiclass{4} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ grade โ”‚ OrderedFactor{3} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ sex โ”‚ Multiclass{2} โ”‚ CategoricalValue{String, UInt32} โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Typically, in the context of Unserpervised Learning, categorical features are one-hot or dummy encoded. To this end, we could use MLJ, for example:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"hot = OneHotEncoder()\nmach = MLJBase.fit!(machine(hot, X))\nW = MLJBase.transform(mach, X)\nX = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"In all likelihood, this pre-processing step already happens at the stage, when the supervised model is trained. Since our counterfactual generators need to work in the same feature domain as the model they are intended to explain, we assume that categorical features are already encoded.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The CounterfactualData constructor takes two optional arguments that can be used to specify the indices of categorical and continuous features. By default, all features are assumed to be continuous. For categorical features, the constructor expects an array of arrays of integers (Vector{Vector{Int}}) where each subarray includes the indices of all one-hot encoded rows related to a single categorical feature. In the example above, the name feature is one-hot encoded across rows 1, 2, 3 and 4 of X, the grade feature is encoded across the following three rows, etc.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"schema(W)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name__Danesh โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__John โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Lee โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Mary โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__A โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__B โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__C โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__female โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__male โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The code chunk below assigns the categorical and continuous feature indices:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"features_categorical = [\n [1,2,3,4], # name\n [5,6,7], # grade\n [8,9] # sex\n]\nfeatures_continuous = [10]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"When instantiating the data container, these indices just need to be supplied as keyword arguments:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(\n X,y;\n features_categorical = features_categorical,\n features_continuous = features_continuous\n)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"This will ensure that the discrete domain of categorical features is respected in the counterfactual search. We achieve this through a form of Projected Gradient Descent and it works for any of our counterfactual generators.","category":"page"},{"location":"tutorials/data_preprocessing/#Example","page":"Handling Data","title":"Example","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To see this in action, letโ€™s load some synthetic data using MLJ:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"N = 1000\nX, ys = MLJBase.make_blobs(N, 2; centers=2, as_table=false, center_box=(-5 => 5), cluster_std=0.5)\nys .= ys.==2","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we generate a synthetic categorical feature based on the output variable. First, we define the discrete levels:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"cat_values = [\"X\",\"Y\",\"Z\"]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Next, we impose that the categorical feature is most likely to take the first discrete level, namely X, whenever y is equal to 1.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"xcat = map(ys) do y\n if y==1\n x = sample(cat_values, Weights([0.8,0.1,0.1]))\n else\n x = sample(cat_values, Weights([0.1,0.1,0.8]))\n end\nend\nxcat = categorical(xcat)\nX = (\n x1 = X[:,1],\n x2 = X[:,2],\n x3 = xcat\n)\nschema(X)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"As above, we use a OneHotEncoder to transform the data:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"hot = OneHotEncoder()\nmach = MLJBase.fit!(machine(hot, X))\nW = MLJBase.transform(mach, X)\nschema(W)\nX = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Finally, we assign the categorical indices and instantiate our data container:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"features_categorical = [collect(3:size(X,1))]\ncounterfactual_data = CounterfactualData(\n X,ys';\n features_categorical = features_categorical,\n)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"With the data pre-processed we can use the fit_model function to train a simple classifier:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"M = fit_model(counterfactual_data, :Linear)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Now it is finally time to generate counterfactuals. We first define 1 as our target and then choose a random sample from the non-target class:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"target = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen) ","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"5ร—1 Matrix{Float32}:\n -2.9433472\n 0.5782963\n 0.0\n 0.0\n 1.0","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The factual x belongs to group Z.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"We generate a counterfactual for x using the standard API call:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"generator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"CounterfactualExplanation\nConvergence: โœ… after 7 steps.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The search yields the following counterfactual:","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"xโ€ฒ = counterfactual(ce)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"5-element Vector{Float32}:\n 1.1222683\n 0.7145791\n 0.0\n 0.0\n 1.0","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"It belongs to group Z.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"This is intuitive because by construction the categorical variable is most likely to take that value when y is equal to the target outcome.","category":"page"},{"location":"tutorials/data_preprocessing/#Immutable-Features","page":"Handling Data","title":"Immutable Features","text":"","category":"section"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"In practice, features usually cannot be perturbed arbitrarily. Suppose, for example, that one of the features used by a bank to predict the creditworthiness of its clients is gender. If a counterfactual explanation for the prediction model indicates that female clients should change their gender to improve their creditworthiness, then this is an interesting insight (it reveals gender bias), but it is not usually an actionable transformation in practice. In such cases, we may want to constrain the mutability of features to ensure actionable and realistic recourse.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"To illustrate how this can be implemented in CounterfactualExplanations.jl we will continue to work with the synthetic data from the previous section. Mutability of features can be defined in terms of four different options: 1) the feature is mutable in both directions, 2) the feature can only increase (e.g.ย age), 3) the feature can only decrease (e.g.ย time left until your next deadline) and 4) the feature is not mutable (e.g.ย skin colour, ethnicity, โ€ฆ). To specify which category a feature belongs to, you can pass a vector of symbols containing the mutability constraints at the pre-processing stage. For each feature you can choose from these four options: :both (mutable in both directions), :increase (only up), :decrease (only down) and :none (immutable). By default, nothing is passed to that keyword argument and it is assumed that all features are mutable in both directions.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"Below we impose that the second feature is immutable.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"counterfactual_data = CounterfactualData(load_linearly_separable()...)\nM = fit_model(counterfactual_data, :Linear)\ncounterfactual_data.mutability = [:both, :none]","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"target = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen) \nce = generate_counterfactual(x, target, counterfactual_data, M, generator)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"The resulting counterfactual path is shown in the chart below. Since only the first feature can be perturbed, the sample can only move along the horizontal axis.","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"plot(ce)","category":"page"},{"location":"tutorials/data_preprocessing/","page":"Handling Data","title":"Handling Data","text":"(Image: ) ","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/overview/#generators_explanation","page":"Overview","title":"Counterfactual Generators","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Counterfactual generators form the very core of this package. The generator_catalogue can be used to inspect the available generators:","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"generator_catalogue","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Dict{Symbol, Any} with 11 entries:\n :gravitational => GravitationalGenerator\n :growing_spheres => GrowingSpheresGenerator\n :revise => REVISEGenerator\n :clue => CLUEGenerator\n :probe => ProbeGenerator\n :dice => DiCEGenerator\n :feature_tweak => FeatureTweakGenerator\n :claproar => ClaPROARGenerator\n :wachter => WachterGenerator\n :generic => GenericGenerator\n :greedy => GreedyGenerator","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"The following sections provide brief descriptions of all of them.","category":"page"},{"location":"explanation/generators/overview/#Gradient-based-Counterfactual-Generators","page":"Overview","title":"Gradient-based Counterfactual Generators","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"At the time of writing, all generators are gradient-based: that is, counterfactuals are searched through gradient descent. In Altmeyer et al. (2023) we lay out a general methodological framework that can be applied to all of these generators:","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"beginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS left textyloss(M(f(mathbfs^prime))y^*)+ lambda textcost(f(mathbfs^prime)) right \nendaligned ","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"โ€œHere mathbfs^prime=lefts_k^primeright_K is a K-dimensional array of counterfactual states and f mathcalS mapsto mathcalX maps from the counterfactual state space to the feature space.โ€ (Altmeyer et al. 2023)","category":"page"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"For most generators, the state space is the feature space (f is the identity function) and the number of counterfactuals K is one. Latent Space generators instead search counterfactuals in some latent space mathcalS. In this case, f corresponds to the decoder part of the generative model, that is the function that maps back from the latent space to inputs.","category":"page"},{"location":"explanation/generators/overview/#References","page":"Overview","title":"References","text":"","category":"section"},{"location":"explanation/generators/overview/","page":"Overview","title":"Overview","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/model_catalogue/#Model-Catalogue","page":"Model Catalogue","title":"Model Catalogue","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"While in general it is assumed that users will use this package to explain their pre-trained models, we provide out-of-the-box functionality to train various simple default models. In this tutorial, we will see how these models can be fitted to CounterfactualData.","category":"page"},{"location":"tutorials/model_catalogue/#Available-Models","page":"Model Catalogue","title":"Available Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The standard_models_catalogue can be used to inspect the available default models:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"standard_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Dict{Symbol, Any} with 4 entries:\n :Linear => Linear\n :LaplaceRedux => LaplaceReduxModel\n :DeepEnsemble => FluxEnsemble\n :MLP => FluxModel","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The dictionary keys correspond to the model names. In this case, the dictionary values are constructors that can be used called on instances of type CounterfactualData to fit the corresponding model. In most cases, users will find it most convenient to use the fit_model API call instead.","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-Models","page":"Model Catalogue","title":"Fitting Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Models from the standard model catalogue are a core part of the package and thus compatible with all offered counterfactual generators and functionalities.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The all_models_catalogue can be used to inspect all models offered by the package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"all_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"However, when using models not included in the standard_models_catalogue, additional caution is advised: they might not be supported by all counterfactual generators or they might not be models native to Julia. Thus, a more thorough reading of their documentation may be necessary to make sure that they are used correctly.","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-Flux-Models","page":"Model Catalogue","title":"Fitting Flux Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"First, letโ€™s load one of the synthetic datasets. For this, weโ€™ll first need to import the TaijaData.jl package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"n = 500\ndata = TaijaData.load_multi_class(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"We could use a Deep Ensemble (Lakshminarayanan, Pritzel, and Blundell 2016) as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(counterfactual_data, :DeepEnsemble)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The returned object is an instance of type FluxEnsemble <: AbstractFittedModel and can be used in downstream tasks without further ado. For example, the resulting fit can be visualised using the generic plot() method as:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"plts = []\nfor target in counterfactual_data.y_levels\n plt = plot(M, counterfactual_data; target=target, title=\"p(y=$(target)|x,ฮธ)\")\n plts = [plts..., plt]\nend\nplot(plts...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/model_catalogue/#Importing-PyTorch-models","page":"Model Catalogue","title":"Importing PyTorch models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The package supports generating counterfactuals for any neural network that has been previously defined and trained using PyTorch, regardless of the specific architectural details of the model. To generate counterfactuals for a PyTorch model, save the model inside a .pt file and call the following function:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_loaded = TaijaInteroperability.pytorch_model_loader(\n \"$(pwd())/docs/src/tutorials/miscellaneous\",\n \"neural_network_class\",\n \"NeuralNetwork\",\n \"$(pwd())/docs/src/tutorials/miscellaneous/pretrained_model.pt\"\n)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The method pytorch_model_loader requires four arguments:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The path to the folder with a .py file where the PyTorch model is defined\nThe name of the file where the PyTorch model is defined\nThe name of the class of the PyTorch model\nThe path to the Pickle file that holds the model weights","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In the above case:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The file defining the model is inside $(pwd())/docs/src/tutorials/miscellaneous\nThe name of the .py file holding the model definition is neural_network_class\nThe name of the model class is NeuralNetwork\nThe Pickle file is located at $(pwd())/docs/src/tutorials/miscellaneous/pretrained_model.pt","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Though the model file and Pickle file are inside the same directory in this tutorial, this does not necessarily have to be the case.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The reason why the model file and Pickle file have to be provided separately is that the package expects an already trained PyTorch model as input. It is also possible to define new PyTorch models within the package, but since this is not the expected use of our package, special support is not offered for that. A guide for defining Python and PyTorch classes in Julia through PythonCall.jl can be found here.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once the PyTorch model has been loaded into the package, wrap it inside the PyTorchModel class:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_pytorch = TaijaInteroperability.PyTorchModel(model_loaded, counterfactual_data.likelihood)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"This model can now be passed into the generators like any other.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that the functionality for generating counterfactuals for Python models is only available if your Julia version is 1.8 or above. For Julia 1.7 users, we recommend upgrading the version to 1.8 or 1.9 before loading a PyTorch model into the package.","category":"page"},{"location":"tutorials/model_catalogue/#Importing-R-torch-models","page":"Model Catalogue","title":"Importing R torch models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"warning: Not fully tested\nPlease note that due to the incompatibility between RCall and PythonCall, it is not feasible to test both PyTorch and RTorch implementations within the same pipeline. While the RTorch implementation has been manually tested, we cannot ensure its consistent functionality as it is inherently susceptible to bugs.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The CounterfactualExplanations package supports generating counterfactuals for neural networks that have been defined and trained using R torch. Regardless of the specific architectural details of the model, you can easily generate counterfactual explanations by following these steps.","category":"page"},{"location":"tutorials/model_catalogue/#Saving-the-R-torch-model","page":"Model Catalogue","title":"Saving the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"First, save your trained R torch model as a .pt file using the torch_save() function provided by the R torch library. This function allows you to serialize the model and save it to a file. For example:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"torch_save(model, file = \"$(pwd())/docs/src/tutorials/miscellaneous/r_model.pt\")","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Make sure to specify the correct file path where you want to save the model.","category":"page"},{"location":"tutorials/model_catalogue/#Loading-the-R-torch-model","page":"Model Catalogue","title":"Loading the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To import the R torch model into the CounterfactualExplanations package, use the rtorch_model_loader() function. This function loads the model from the previously saved .pt file. Here is an example of how to load the R torch model:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_loaded = TaijaInteroperability.rtorch_model_loader(\"$(pwd())/docs/src/tutorials/miscellaneous/r_model.pt\")","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The rtorch_model_loader() function requires only one argument:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_path: The path to the .pt file that contains the trained R torch model.","category":"page"},{"location":"tutorials/model_catalogue/#Wrapping-the-R-torch-model","page":"Model Catalogue","title":"Wrapping the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once the R torch model has been loaded into the package, wrap it inside the RTorchModel class. This step prepares the model to be used by the counterfactual generators. Here is an example:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_R = TaijaInteroperability.RTorchModel(model_loaded, counterfactual_data.likelihood)","category":"page"},{"location":"tutorials/model_catalogue/#Generating-counterfactuals-with-the-R-torch-model","page":"Model Catalogue","title":"Generating counterfactuals with the R torch model","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Now that the R torch model has been wrapped inside the RTorchModel class, you can pass it into the counterfactual generators as you would with any other model.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that RCall is not fully compatible with PythonCall. Therefore, it is advisable not to import both R torch and PyTorch models within the same Julia session. Additionally, itโ€™s worth mentioning that the R torch integration is still untested in the CounterfactualExplanations package.","category":"page"},{"location":"tutorials/model_catalogue/#Tuning-Flux-Models","page":"Model Catalogue","title":"Tuning Flux Models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"By default, model architectures are very simple. Through optional arguments, users have some control over the neural network architecture and can choose to impose regularization through dropout. Letโ€™s tackle a more challenging dataset: MNIST (LeCun 1998).","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"data = TaijaData.load_mnist(10000)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ntrain_data, test_data = \n CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"(Image: )","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In this case, we will use a Multi-Layer Perceptron (MLP) but we will adjust the model and training hyperparameters. Parameters related to training of Flux.jl models are currently stored in a mutable container:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"flux_training_params","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CounterfactualExplanations.FluxModelParams\n loss: Symbol logitbinarycrossentropy\n opt: Symbol Adam\n n_epochs: Int64 100\n batchsize: Int64 1\n verbose: Bool false","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"In cases like this one, where model training can be expected to take a few moments, it can be useful to activate verbosity, so letโ€™s set the corresponding field value to true. Weโ€™ll also impose mini-batch training:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"flux_training_params.verbose = true\nflux_training_params.batchsize = round(size(train_data.X,2)/10)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To account for the fact that this is a slightly more challenging task, we will use an appropriate number of hidden neurons per layer. We will also activate dropout regularization. To scale networks up further, it is also possible to adjust the number of hidden layers, which we will not do here.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_params = (\n n_hidden = 32,\n dropout = true\n)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The model_params can be supplied to the familiar API call:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(train_data, :MLP; model_params...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"FluxModel(Chain(Dense(784 => 32, relu), Dropout(0.25, active=false), Dense(32 => 10)), :classification_multi)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The model performance on our test set can be evaluated as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"model_evaluation(M, test_data)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"1-element Vector{Float64}:\n 0.9185","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Finally, letโ€™s restore the default training parameters:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"CounterfactualExplanations.reset!(flux_training_params)","category":"page"},{"location":"tutorials/model_catalogue/#Fitting-and-tuning-MLJ-models","page":"Model Catalogue","title":"Fitting and tuning MLJ models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Among models from the MLJ library, two models are integrated as part of the core functionality of the package:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"mlj_models_catalogue","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"These models are compatible with the Feature Tweak generator. Support for other generators has not been implemented, as both decision trees and random forests are non-differentiable tree-based models and thus, gradient-based generators donโ€™t apply for them.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Tuning MLJ models is very simple. As the first step, letโ€™s reload the dataset:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"n = 500\ndata = TaijaData.load_moons(n)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Using the usual procedure for fitting models, we can call the following method:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"tree = CounterfactualExplanations.Models.fit_model(counterfactual_data, :DecisionTree)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"However, itโ€™s also possible to tune the DecisionTreeClassifierโ€™s parameters. This can be done using the keyword arguments when calling fit_model() as follows:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"tree = CounterfactualExplanations.Models.fit_model(counterfactual_data, :DecisionTree; max_depth=2, min_samples_leaf=3)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"For all supported MLJ models, every tunable parameter they have is supported as a keyword argument. The tunable parameters for the DecisionTreeModel and the RandomForestModel can be found from the documentation of the DecisionTree.jl package under the Decision Tree Classifier and Random Forest Classifier sections.","category":"page"},{"location":"tutorials/model_catalogue/#Package-extension-models","page":"Model Catalogue","title":"Package extension models","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The package also includes two models which donโ€™t form a part of the core functionality of the package, but which can be accessed as package extensions. These are the EvoTreeModel from the MLJ library and the LaplaceReduxModel from LaplaceRedux.jl.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"To trigger the package extensions, the weak dependency first has to be loaded with the using keyword:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"using EvoTrees","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Once this is done, the extension models can be used like any other model:","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"M = fit_model(counterfactual_data, :EvoTree; model_params...)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"EvoTreesExt.EvoTreeModel(machine(EvoTreeClassifier{EvoTrees.MLogLoss}\n - nrounds: 100\n - L2: 0.0\n - lambda: 0.0\n - gamma: 0.0\n - eta: 0.1\n - max_depth: 6\n - min_weight: 1.0\n - rowsample: 1.0\n - colsample: 1.0\n - nbins: 64\n - alpha: 0.5\n - tree_type: binary\n - rng: MersenneTwister(123, (0, 9018, 8016, 884))\n, โ€ฆ), :classification_multi)","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"The tunable parameters for the EvoTreeModel can be found from the documentation of the EvoTrees.jl package under the EvoTreeClassifier section.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Please note that support for counterfactual generation with both LaplaceReduxModel and EvoTreeModel is not yet fully implemented.","category":"page"},{"location":"tutorials/model_catalogue/#References","page":"Model Catalogue","title":"References","text":"","category":"section"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.","category":"page"},{"location":"tutorials/model_catalogue/","page":"Model Catalogue","title":"Model Catalogue","text":"LeCun, Yann. 1998. โ€œThe MNIST Database of Handwritten Digits.โ€","category":"page"},{"location":"assets/resources/#Further-Resources","page":"๐Ÿ“š Additional Resources","title":"Further Resources","text":"","category":"section"},{"location":"assets/resources/#JuliaCon-2022","page":"๐Ÿ“š Additional Resources","title":"JuliaCon 2022","text":"","category":"section"},{"location":"assets/resources/","page":"๐Ÿ“š Additional Resources","title":"๐Ÿ“š Additional Resources","text":"Slides: link","category":"page"},{"location":"assets/resources/#JuliaCon-Proceedings-Paper","page":"๐Ÿ“š Additional Resources","title":"JuliaCon Proceedings Paper","text":"","category":"section"},{"location":"assets/resources/","page":"๐Ÿ“š Additional Resources","title":"๐Ÿ“š Additional Resources","text":"TBD","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"reference/#Reference","page":"๐Ÿง Reference","title":"Reference","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"In this reference, you will find a detailed overview of the package API.","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Reference guides are technical descriptions of the machinery and how to operate it. Reference material is information-oriented.โ€” Diรกtaxis","category":"page"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"In other words, you come here because you want to take a very close look at the code ๐Ÿง.","category":"page"},{"location":"reference/#Content","page":"๐Ÿง Reference","title":"Content","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Pages = [\"reference.md\"]\nDepth = 2","category":"page"},{"location":"reference/#Exported-functions","page":"๐Ÿง Reference","title":"Exported functions","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Modules = [\n CounterfactualExplanations, \n CounterfactualExplanations.Convergence,\n CounterfactualExplanations.Evaluation,\n CounterfactualExplanations.DataPreprocessing,\n CounterfactualExplanations.Models,\n CounterfactualExplanations.GenerativeModels, \n CounterfactualExplanations.Generators, \n CounterfactualExplanations.Objectives\n]\nPrivate = false","category":"page"},{"location":"reference/#CounterfactualExplanations.RawOutputArrayType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.RawOutputArrayType","text":"RawOutputArrayType\n\nA type union for the allowed type for the output array y.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.RawTargetType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.RawTargetType","text":"RawTargetType\n\nA type union for the allowed types for the target variable.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.flux_training_params","page":"๐Ÿง Reference","title":"CounterfactualExplanations.flux_training_params","text":"flux_training_params\n\nThe default training parameter for FluxModels etc.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.AbstractConvergence","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractConvergence","text":"An abstract type that serves as the base type for convergence objects.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractCounterfactualExplanation","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractCounterfactualExplanation","text":"Base type for counterfactual explanations.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractFittedModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractFittedModel","text":"Base type for fitted models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.AbstractGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.AbstractGenerator","text":"An abstract type that serves as the base type for counterfactual generators.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.CounterfactualExplanation","page":"๐Ÿง Reference","title":"CounterfactualExplanations.CounterfactualExplanation","text":"A struct that collects all information relevant to a specific counterfactual explanation for a single individual.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.CounterfactualExplanation-Tuple{AbstractArray, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.CounterfactualExplanation","text":"function CounterfactualExplanation(;\n\tx::AbstractArray,\n\ttarget::RawTargetType,\n\tdata::CounterfactualData,\n\tM::Models.AbstractFittedModel,\n\tgenerator::Generators.AbstractGenerator,\n\tnum_counterfactuals::Int = 1,\n\tinitialization::Symbol = :add_perturbation,\n convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,\n)\n\nOuter method to construct a CounterfactualExplanation structure.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.EncodedOutputArrayType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EncodedOutputArrayType","text":"EncodedOutputArrayType\n\nType of encoded output array.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.EncodedTargetType","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EncodedTargetType","text":"EncodedTargetType\n\nType of encoded target variable.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.OutputEncoder","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"OutputEncoder\n\nThe OutputEncoder takes a raw output array (y) and encodes it.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.OutputEncoder-Tuple{Union{Int64, AbstractFloat, String, Symbol}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"(encoder::OutputEncoder)(ynew::RawTargetType)\n\nWhen called on a new value ynew, the OutputEncoder encodes it based on the initial encoding.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.OutputEncoder-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.OutputEncoder","text":"(encoder::OutputEncoder)()\n\nOn call, the OutputEncoder returns the encoded output array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.EvoTreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.EvoTreeModel","text":"EvoTreeModel\n\nExposes the EvoTreeModel from the EvoTreesExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.LaplaceReduxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.LaplaceReduxModel","text":"LaplaceReduxModel\n\nExposes the LaplaceReduxModel from the LaplaceReduxExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.NeuroTreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.NeuroTreeModel","text":"NeuroTreeModel\n\nExposes the NeuroTreeModel from the NeuroTreeExt extension.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Base.Iterators.Zip, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Base.Iterators.Zip,\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n kwargs...,\n)\n\nOverloads the generate_counterfactual method to accept a zip of factuals x and return a vector of counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Matrix, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Matrix,\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n num_counterfactuals::Int=1,\n initialization::Symbol=:add_perturbation,\n convergence::Union{AbstractConvergence,Symbol}=:decision_threshold,\n timeout::Union{Nothing,Real}=nothing,\n)\n\nThe core function that is used to run counterfactual search for a given factual x, target, counterfactual data, model and generator. Keywords can be used to specify the desired threshold for the predicted target class probability and the maximum number of iterations.\n\nArguments\n\nx::Matrix: Factual data point.\ntarget::RawTargetType: Target class.\ndata::CounterfactualData: Counterfactual data.\nM::Models.AbstractFittedModel: Fitted model.\ngenerator::AbstractGenerator: Generator.\nnum_counterfactuals::Int=1: Number of counterfactuals to generate for factual.\ninitialization::Symbol=:add_perturbation: Initialization method. By default, the initialization is done by adding a small random perturbation to the factual to achieve more robustness.\nconvergence::Union{AbstractConvergence,Symbol}=:decision_threshold: Convergence criterion. By default, the convergence is based on the decision threshold. Possible values are :decision_threshold, :max_iter, :generator_conditions or a conrete convergence object (e.g. DecisionThresholdConvergence). \ntimeout::Union{Nothing,Int}=nothing: Timeout in seconds.\n\nExamples\n\nGeneric generator\n\njulia> using CounterfactualExplanations\n\njulia> using TaijaData\n \n # Counteractual data and model:\n\njulia> counterfactual_data = CounterfactualData(load_linearly_separable()...);\n\njulia> M = fit_model(counterfactual_data, :Linear);\n\njulia> target = 2;\n\njulia> factual = 1;\n\njulia> chosen = rand(findall(predict_label(M, counterfactual_data) .== factual));\n\njulia> x = select_factual(counterfactual_data, chosen);\n \n # Search:\n\njulia> generator = Generators.GenericGenerator();\n\njulia> ce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nCounterfactualExplanation\nConvergence: โœ… after 7 steps.\n\nBroadcasting\n\nThe generate_counterfactual method can also be broadcasted over a tuple containing an array. This allows for generating multiple counterfactuals in parallel. \n\njulia> chosen = rand(findall(predict_label(M, counterfactual_data) .== factual), 5);\n\njulia> xs = select_factual(counterfactual_data, chosen);\n\njulia> ces = generate_counterfactual.(xs, target, counterfactual_data, M, generator)\n5-element Vector{CounterfactualExplanation}:\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n CounterfactualExplanation\nConvergence: โœ… after 8 steps.\n CounterfactualExplanation\nConvergence: โœ… after 6 steps.\n CounterfactualExplanation\nConvergence: โœ… after 7 steps.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Matrix, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, GrowingSpheresGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Matrix,\n target::RawTargetType,\n data::DataPreprocessing.CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::Generators.GrowingSpheresGenerator;\n num_counterfactuals::Int=1,\n convergence::Union{AbstractConvergence,Symbol}=Convergence.DecisionThresholdConvergence(;\n decision_threshold=(1 / length(data.y_levels)), max_iter=1000\n ),\n kwrgs...,\n)\n\nOverloads the generate_counterfactual for the GrowingSpheresGenerator generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Tuple{AbstractArray}, Vararg{Any}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(x::Tuple{<:AbstractArray}, args...; kwargs...)\n\nOverloads the generate_counterfactual method to accept a tuple containing and array. This allows for broadcasting over Zip iterators.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.generate_counterfactual-Tuple{Vector{<:Matrix}, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData, AbstractFittedModel, AbstractGenerator}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.generate_counterfactual","text":"generate_counterfactual(\n x::Vector{<:Matrix},\n target::RawTargetType,\n data::CounterfactualData,\n M::Models.AbstractFittedModel,\n generator::AbstractGenerator;\n kwargs...,\n)\n\nOverloads the generate_counterfactual method to accept a vector of factuals x and return a vector of counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.get_target_index-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.get_target_index","text":"get_target_index(y_levels, target)\n\nUtility that returns the index of target in y_levels.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.path","text":"path(ce::CounterfactualExplanation)\n\nA convenience method that returns the entire counterfactual path.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.target_probs","page":"๐Ÿง Reference","title":"CounterfactualExplanations.target_probs","text":"target_probs(\n ce::CounterfactualExplanation,\n x::Union{AbstractArray,Nothing}=nothing,\n)\n\nReturns the predicted probability of the target class for x. If x is nothing, the predicted probability corresponding to the counterfactual value is returned.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.terminated-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.terminated","text":"terminated(ce::CounterfactualExplanation)\n\nA convenience method that checks if the counterfactual search has terminated.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.total_steps-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.total_steps","text":"total_steps(ce::CounterfactualExplanation)\n\nA convenience method that returns the total number of steps of the counterfactual search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.update!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.update!","text":"update!(ce::CounterfactualExplanation)\n\nAn important subroutine that updates the counterfactual explanation. It takes a snapshot of the current counterfactual search state and passes it to the generator. Based on the current state the generator generates perturbations. Various constraints are then applied to the proposed vector of feature perturbations. Finally, the counterfactual search state is updated.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.convergence_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.convergence_catalogue","text":"convergence_catalogue\n\nA dictionary containing all convergence criteria.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Convergence.DecisionThresholdConvergence","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.DecisionThresholdConvergence","text":"DecisionThresholdConvergence\n\nConvergence criterion based on the target class probability threshold. The search stops when the target class probability exceeds the predefined threshold.\n\nFields\n\ndecision_threshold::AbstractFloat: The predefined threshold for the target class probability.\nmax_iter::Int: The maximum number of iterations.\nmin_success_rate::AbstractFloat: The minimum success rate for the target class probability.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Convergence.GeneratorConditionsConvergence","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.GeneratorConditionsConvergence","text":"GeneratorConditionsConvergence\n\nConvergence criterion for counterfactual explanations based on the generator conditions. The search stops when the gradients of the search objective are below a certain threshold and the generator conditions are satisfied.\n\nFields\n\ndecision_threshold::AbstractFloat: The threshold for the decision probability.\ngradient_tol::AbstractFloat: The tolerance for the gradients of the search objective.\nmax_iter::Int: The maximum number of iterations.\nmin_success_rate::AbstractFloat: The minimum success rate for the generator conditions (across counterfactuals).\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Convergence.GeneratorConditionsConvergence-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.GeneratorConditionsConvergence","text":"GeneratorConditionsConvergence(; decision_threshold=0.5, gradient_tol=1e-2, max_iter=100, min_success_rate=0.75, y_levels=nothing)\n\nOuter constructor for GeneratorConditionsConvergence.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.MaxIterConvergence","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.MaxIterConvergence","text":"MaxIterConvergence\n\nConvergence criterion based on the maximum number of iterations.\n\nFields\n\nmax_iter::Int: The maximum number of iterations.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.DecisionThresholdConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::DecisionThresholdConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is the decision threshold.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.GeneratorConditionsConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::GeneratorConditionsConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is generator_conditions.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.InvalidationRateConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::InvalidationRateConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is invalidation rate.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.converged-Tuple{CounterfactualExplanations.Convergence.MaxIterConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.converged","text":"converged(convergence::MaxIterConvergence, ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has converged when the convergence criterion is maximum iterations. This means the counterfactual search will not terminate until the maximum number of iterations has been reached independently of the other convergence criteria.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.get_convergence_type-Tuple{AbstractConvergence, AbstractVector}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.get_convergence_type","text":"get_convergence_type(convergence::AbstractConvergence)\n\nReturns the convergence object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.get_convergence_type-Tuple{Symbol, AbstractVector}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.get_convergence_type","text":"get_convergence_type(convergence::Symbol)\n\nReturns the convergence object from the dictionary of default convergence types.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.hinge_loss-Tuple{CounterfactualExplanations.Convergence.InvalidationRateConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.hinge_loss","text":"hinge_loss(convergence::InvalidationRateConvergence, ce::AbstractCounterfactualExplanation)\n\nCalculates the hinge loss of a counterfactual explanation.\n\nArguments\n\nconvergence::InvalidationRateConvergence: The convergence criterion to use.\nce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the hinge loss for.\n\nReturns\n\nThe hinge loss of the counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.invalidation_rate-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.invalidation_rate","text":"invalidation_rate(ce::AbstractCounterfactualExplanation)\n\nCalculates the invalidation rate of a counterfactual explanation.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation to calculate the invalidation rate for.\nkwargs: Additional keyword arguments to pass to the function.\n\nReturns\n\nThe invalidation rate of the counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Convergence.threshold_reached-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Convergence.threshold_reached","text":"threshold_reached(ce::CounterfactualExplanation)\n\nDetermines if the predefined threshold for the target class probability has been reached.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.default_measures","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.default_measures","text":"The default evaluation measures.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Evaluation.Benchmark","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.Benchmark","text":"A container for benchmarks of counterfactual explanations. Instead of subtyping DataFrame, it contains a DataFrame of evaluation measures (see this discussion for why we don't subtype DataFrame directly).\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Evaluation.Benchmark-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.Benchmark","text":"(bmk::Benchmark)(; agg=mean)\n\nReturns a DataFrame containing evaluation measures aggregated by num_counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n data::CounterfactualData;\n models::Dict{<:Any,<:Any}=standard_models_catalogue,\n generators::Union{Nothing,Dict{<:Any,<:AbstractGenerator}}=nothing,\n measure::Union{Function,Vector{Function}}=default_measures,\n n_individuals::Int=5,\n suppress_training::Bool=false,\n factual::Union{Nothing,RawTargetType}=nothing,\n target::Union{Nothing,RawTargetType}=nothing,\n store_ce::Bool=false,\n parallelizer::Union{Nothing,AbstractParallelizer}=nothing,\n kwrgs...,\n)\n\nRuns the benchmarking exercise as follows:\n\nRandomly choose a factual and target label unless specified. \nIf no pretrained models are provided, it is assumed that a dictionary of callable model objects is provided (by default using the standard_models_catalogue). \nEach of these models is then trained on the data. \nFor each model separately choose n_individuals randomly from the non-target (factual) class. For each generator create a benchmark as in benchmark(xs::Union{AbstractArray,Base.Iterators.Zip}).\nFinally, concatenate the results.\n\nIf vertical_splits is specified to an integer, the computations are split vertically into vertical_splits chunks. In this case, the results are stored in a temporary directory and concatenated afterwards. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{Union{AbstractArray, Base.Iterators.Zip}, Union{Int64, AbstractFloat, String, Symbol}, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n x::Union{AbstractArray,Base.Iterators.Zip},\n target::RawTargetType,\n data::CounterfactualData;\n models::Dict{<:Any,<:AbstractFittedModel},\n generators::Dict{<:Any,<:AbstractGenerator},\n measure::Union{Function,Vector{Function}}=default_measures,\n xids::Union{Nothing,AbstractArray}=nothing,\n dataname::Union{Nothing,Symbol,String}=nothing,\n verbose::Bool=true,\n store_ce::Bool=false,\n parallelizer::Union{Nothing,AbstractParallelizer}=nothing,\n kwrgs...,\n)\n\nFirst generates counterfactual explanations for factual x, the target and data using each of the provided models and generators. Then generates a Benchmark for the vector of counterfactual explanations as in benchmark(counterfactual_explanations::Vector{CounterfactualExplanation}).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.benchmark-Tuple{Vector{CounterfactualExplanation}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.benchmark","text":"benchmark(\n counterfactual_explanations::Vector{CounterfactualExplanation};\n meta_data::Union{Nothing,<:Vector{<:Dict}}=nothing,\n measure::Union{Function,Vector{Function}}=default_measures,\n store_ce::Bool=false,\n)\n\nGenerates a Benchmark for a vector of counterfactual explanations. Optionally meta_data describing each individual counterfactual explanation can be supplied. This should be a vector of dictionaries of the same length as the vector of counterfactuals. If no meta_data is supplied, it will be automatically inferred. All measure functions are applied to each counterfactual explanation. If store_ce=true, the counterfactual explanations are stored in the benchmark.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.evaluate","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.evaluate","text":"evaluate(\n ce::CounterfactualExplanation;\n measure::Union{Function,Vector{Function}}=default_measures,\n agg::Function=mean,\n report_each::Bool=false,\n output_format::Symbol=:Vector,\n pivot_longer::Bool=true\n)\n\nJust computes evaluation measures for the counterfactual explanation. By default, no meta data is reported. For report_meta=true, meta data is automatically inferred, unless this overwritten by meta_data. The optional meta_data argument should be a vector of dictionaries of the same length as the vector of counterfactual explanations. \n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Evaluation.redundancy-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.redundancy","text":"redundancy(ce::CounterfactualExplanation)\n\nComputes the feature redundancy: that is, the number of features that remain unchanged from their original, factual values.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.validity-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.validity","text":"validity(ce::CounterfactualExplanation; ฮณ=0.5)\n\nChecks of the counterfactual search has been successful with respect to the probability threshold ฮณ. In case multiple counterfactuals were generated, the function returns the proportion of successful counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.CounterfactualData-Tuple{AbstractMatrix, Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.CounterfactualData","text":"CounterfactualData(\n X::AbstractMatrix, y::AbstractMatrix;\n mutability::Union{Vector{Symbol},Nothing}=nothing,\n domain::Union{Any,Nothing}=nothing,\n features_categorical::Union{Vector{Int},Nothing}=nothing,\n features_continuous::Union{Vector{Int},Nothing}=nothing,\n standardize::Bool=false\n)\n\nThis outer constructor method prepares features X and labels y to be used with the package. Mutability and domain constraints can be added for the features. The function also accepts arguments that specify which features are categorical and which are continues. These arguments are currently not used. \n\nExamples\n\nusing CounterfactualExplanations.Data\nx, y = toy_data_linear()\nX = hcat(x...)\ncounterfactual_data = CounterfactualData(X,y')\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.CounterfactualData-Tuple{Tables.MatrixTable, Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.CounterfactualData","text":"function CounterfactualData(\n X::Tables.MatrixTable,\n y::RawOutputArrayType;\n kwrgs...\n)\n\nOuter constructor method that accepts a Tables.MatrixTable. By default, the indices of categorical and continuous features are automatically inferred the features' scitype.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.apply_domain_constraints-Tuple{CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.apply_domain_constraints","text":"apply_domain_constraints(counterfactual_data::CounterfactualData, x::AbstractArray)\n\nA subroutine that is used to apply the predetermined domain constraints.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.select_factual-Tuple{CounterfactualData, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.select_factual","text":"select_factual(counterfactual_data::CounterfactualData, index::Int)\n\nA convenience method that can be used to access the feature matrix.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.select_factual-Tuple{CounterfactualData, Union{UnitRange{Int64}, Vector{Int64}}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.select_factual","text":"select_factual(counterfactual_data::CounterfactualData, index::Union{Vector{Int},UnitRange{Int}})\n\nA convenience method that can be used to access the feature matrix.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData, dt::Any)\n\nBy default, all continuous features are transformable. This function returns the indices of all continuous features.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData, StatsBase.ZScoreTransform}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData, dt::ZScoreTransform)\n\nReturns the indices of all continuous features that can be transformed. For constant features ZScoreTransform returns NaN.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.transformable_features-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.transformable_features","text":"transformable_features(counterfactual_data::CounterfactualData)\n\nDispatches the transformable_features function to the appropriate method based on the type of the dt field.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.all_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.all_models_catalogue","text":"all_models_catalogue\n\nA dictionary containing both differentiable and non-differentiable machine learning models.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.mlj_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.mlj_models_catalogue","text":"mlj_models_catalogue\n\nA dictionary containing all machine learning models from the MLJ model registry that the package supports.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.standard_models_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.standard_models_catalogue","text":"standard_models_catalogue\n\nA dictionary containing all differentiable machine learning models.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Models.AbstractDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractDifferentiableModel","text":"Base type for differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxEnsemble","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxEnsemble","text":"FluxEnsemble <: AbstractFluxModel\n\nConstructor for deep ensembles trained in Flux.jl. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxModel","text":"FluxModel <: AbstractFluxModel\n\nConstructor for models trained in Flux.jl. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxModel","text":"FluxModel(data::CounterfactualData; kwargs...)\n\nConstructs a multi-layer perceptron (MLP).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.DecisionTreeModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.DecisionTreeModel","text":"DecisionTreeModel(data::CounterfactualData; kwargs...)\n\nConstructs a new TreeModel object wrapped around a decision tree from the data in a CounterfactualData object. Not called by the user directly.\n\nArguments\n\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nmodel::TreeModel: A TreeModel object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.Linear-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.Linear","text":"Linear(data::CounterfactualData; kwargs...)\n\nConstructs a model with one linear layer. If the output is binary, this corresponds to logistic regression, since model outputs are passed through the sigmoid function. If the output is multi-class, this corresponds to multinomial logistic regression, since model outputs are passed through the softmax function.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.RandomForestModel-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.RandomForestModel","text":"RandomForestModel(data::CounterfactualData; kwargs...)\n\nConstructs a new TreeModel object wrapped around a random forest from the data in a CounterfactualData object. Not called by the user directly.\n\nArguments\n\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nmodel::TreeModel: A TreeModel object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.fit_model","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.fit_model","text":"fit_model(\n counterfactual_data::CounterfactualData, model::Symbol=:MLP;\n kwrgs...\n)\n\nFits one of the available default models to the counterfactual_data. The model argument can be used to specify the desired model. The available values correspond to the keys of the all_models_catalogue dictionary.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Models.logits-Tuple{AbstractFittedModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.logits","text":"logits(M::AbstractFittedModel, X::AbstractArray)\n\nGeneric method that is compulsory for all models. It returns the raw model predictions. In classification this is sometimes referred to as logits: the non-normalized predictions that are fed into a link function to produce predicted probabilities. In regression (not currently implemented) raw outputs typically correspond to final outputs. In other words, there is typically no normalization involved.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.logits-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.logits","text":"logits(M::TreeModel, X::AbstractArray)\n\nCalculates the logit scores output by the model M for the input data X.\n\nArguments\n\nM::TreeModel: The model selected by the user.\nX::AbstractArray: The feature vector for which the logit scores are calculated.\n\nReturns\n\nlogits::Matrix: A matrix of logits for each output class for each data point in X.\n\nExample\n\nlogits = Models.logits(M, x) # calculates the logit scores for each output class for the data point x\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.model_evaluation-Tuple{AbstractFittedModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.model_evaluation","text":"model_evaluation(M::AbstractFittedModel, test_data::CounterfactualData)\n\nHelper function to compute F-Score for AbstractFittedModel on a (test) data set. By default, it computes the accuracy. Any other measure, e.g. from the StatisticalMeasures package, can be passed as an argument. Currently, only measures applicable to classification tasks are supported.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{AbstractFittedModel, CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::AbstractArray)\n\nReturns the predicted output label for a given model M, data set counterfactual_data and input data X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{AbstractFittedModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::AbstractFittedModel, counterfactual_data::CounterfactualData)\n\nReturns the predicted output labels for all data points of data set counterfactual_data for a given model M.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_label-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_label","text":"predict_label(M::TreeModel, X::AbstractArray)\n\nReturns the predicted label for X.\n\nArguments\n\nM::TreeModel: The model selected by the user.\nX::AbstractArray: The input array for which the label is predicted.\n\nReturns\n\nlabels::AbstractArray: The predicted label for each data point in X.\n\nExample\n\nlabel = Models.predict_label(M, x) # returns the predicted label for each data point in x\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.predict_proba-Tuple{AbstractFittedModel, Union{Nothing, CounterfactualData}, Union{Nothing, AbstractArray}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.predict_proba","text":"predict_proba(M::AbstractFittedModel, counterfactual_data::CounterfactualData, X::Union{Nothing,AbstractArray})\n\nReturns the predicted output probabilities for a given model M, data set counterfactual_data and input data X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{AbstractFittedModel, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::AbstractFittedModel, X::AbstractArray)\n\nGeneric method that is compulsory for all models. It returns the normalized model predictions, so the predicted probabilities in the case of classification. In regression (not currently implemented) this method is redundant. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractMatrix{<:Number}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::TreeModel, X::AbstractArray{<:Number, 2})\n\nCalculates the probability scores for each output class for the two-dimensional input data matrix X.\n\nArguments\n\nM::TreeModel: The TreeModel.\nX::AbstractArray: The feature vector for which the predictions are made.\n\nReturns\n\np::Matrix: A matrix of probability scores for each output class for each data point in X.\n\nExample\n\nprobabilities = Models.probs(M, X) # calculates the probability scores for each output class for each data point in X.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.probs-Tuple{CounterfactualExplanations.Models.TreeModel, AbstractVector{<:Number}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.probs","text":"probs(M::TreeModel, X::AbstractArray{<:Number, 1})\n\nWorks the same way as the probs(M::TreeModel, X::AbstractArray{<:Number, 2}) method above, but handles 1-dimensional rather than 2-dimensional input data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generator_catalogue","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generator_catalogue","text":"A dictionary containing the constructors of all available counterfactual generators.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#CounterfactualExplanations.Generators.AbstractGradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.AbstractGradientBasedGenerator","text":"AbstractGradientBasedGenerator\n\nAn abstract type that serves as the base type for gradient-based counterfactual generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.AbstractNonGradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.AbstractNonGradientBasedGenerator","text":"AbstractNonGradientBasedGenerator\n\nAn abstract type that serves as the base type for non gradient-based counterfactual generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.FeatureTweakGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.FeatureTweakGenerator","text":"Feature Tweak counterfactual generator class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.FeatureTweakGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.FeatureTweakGenerator","text":"FeatureTweakGenerator(; penalty::Union{Nothing,Function,Vector{Function}}=Objectives.distance_l2, ฯต::AbstractFloat=0.1)\n\nConstructs a new Feature Tweak Generator object.\n\nUses the L2-norm as the penalty to measure the distance between the counterfactual and the factual. According to the paper by Tolomei et al., another recommended choice for the penalty in addition to the L2-norm is the L0-norm. The L0-norm simply minimizes the number of features that are changed through the tweak.\n\nArguments\n\npenalty::Union{Nothing,Function,Vector{Function}}: The penalty function to use for the generator. Defaults to distance_l2.\nฯต::AbstractFloat: The tolerance value for the feature tweaks. Described at length in Tolomei et al. (https://arxiv.org/pdf/1706.06691.pdf). Defaults to 0.1.\n\nReturns\n\ngenerator::FeatureTweakGenerator: A non-gradient-based generator that can be used to generate counterfactuals using the feature tweak method.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GradientBasedGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GradientBasedGenerator","text":"Base class for gradient-based counterfactual generators.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.GradientBasedGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GradientBasedGenerator","text":"GradientBasedGenerator(;\n\tloss::Union{Nothing,Function}=nothing,\n\tpenalty::Penalty=nothing,\n\tฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing,\n\tlatent_space::Bool::false,\n\topt::Flux.Optimise.AbstractOptimiser=Flux.Descent(),\n generative_model_params::NamedTuple=(;),\n)\n\nDefault outer constructor for GradientBasedGenerator.\n\nArguments\n\nloss::Union{Nothing,Function}=nothing: The loss function used by the model.\npenalty::Penalty=nothing: A penalty function for the generator to penalize counterfactuals too far from the original point.\nฮป::Union{Nothing,AbstractFloat,Vector{AbstractFloat}}=nothing: The weight of the penalty function.\nlatent_space::Bool=false: Whether to use the latent space of a generative model to generate counterfactuals.\nopt::Flux.Optimise.AbstractOptimiser=Flux.Descent(): The optimizer to use for the generator.\ngenerative_model_params::NamedTuple: The parameters of the generative model associated with the generator.\n\nReturns\n\ngenerator::GradientBasedGenerator: A gradient-based counterfactual generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GrowingSpheresGenerator","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GrowingSpheresGenerator","text":"Growing Spheres counterfactual generator class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.GrowingSpheresGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GrowingSpheresGenerator","text":"GrowingSpheresGenerator(; n::Int=100, ฮท::Float64=0.1, kwargs...)\n\nConstructs a new Growing Spheres Generator object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.JSMADescent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.JSMADescent","text":"An optimisation rule that can be used to implement a Jacobian-based Saliency Map Attack.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators.JSMADescent-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.JSMADescent","text":"Outer constructor for the JSMADescent rule.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.CLUEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.CLUEGenerator","text":"Constructor for CLUEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.ClaPROARGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.ClaPROARGenerator","text":"Constructor for ClaPGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.DiCEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.DiCEGenerator","text":"Constructor for DiCEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GenericGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GenericGenerator","text":"Constructor for GenericGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GravitationalGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GravitationalGenerator","text":"Constructor for GravitationalGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.GreedyGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.GreedyGenerator","text":"Constructor for GreedyGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.ProbeGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.ProbeGenerator","text":"Constructor for ProbeGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.REVISEGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.REVISEGenerator","text":"Constructor for REVISEGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.WachterGenerator-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.WachterGenerator","text":"Constructor for WachterGenerator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.conditions_satisfied-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.conditions_satisfied","text":"conditions_satisfied(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to check if the all conditions for convergence of the counterfactual search have been satisified for gradient-based generators. By default, gradient-based search is considered to have converged as soon as the proposed feature changes for all features are smaller than one percent of its standard deviation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.feature_tweaking!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.feature_tweaking!","text":"feature_tweaking!(ce::AbstractCounterfactualExplanation)\n\nReturns a counterfactual instance of ce.x based on the ensemble of classifiers provided.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nReturns\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nExample\n\nce = feature_tweaking!(ce) # returns a counterfactual inside the ce.sโ€ฒ field based on the ensemble of classifiers provided\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generate_perturbations-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generate_perturbations","text":"generate_perturbations(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to generate feature perturbations for gradient-based generators through simple gradient descent.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.generate_perturbations-Tuple{FeatureTweakGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.generate_perturbations","text":"generate_perturbations(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to generate feature perturbations for gradient-based generators through simple gradient descent.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.hinge_loss-Tuple{AbstractConvergence, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.hinge_loss","text":"hinge_loss(convergence::AbstractConvergence, ce::AbstractCounterfactualExplanation)\n\nThe default hinge loss for any convergence criterion. Can be overridden inside the Convergence module as part of the definition of specific convergence criteria.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.@objective-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@objective","text":"objective(generator, ex)\n\nA macro that can be used to define the counterfactual search objective.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@search_feature_space-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@search_feature_space","text":"search_feature_space(generator)\n\nA simple macro that can be used to specify feature space search.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@search_latent_space-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@search_latent_space","text":"search_latent_space(generator)\n\nA simple macro that can be used to specify latent space search.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Generators.@with_optimiser-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.@with_optimiser","text":"with_optimiser(generator, optimiser)\n\nA simple macro that can be used to specify the optimiser to be used.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#CounterfactualExplanations.Objectives.ddp_diversity-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.ddp_diversity","text":"ddp_diversity(\n ce::AbstractCounterfactualExplanation;\n perturbation_size=1e-5\n)\n\nEvaluates how diverse the counterfactuals are using a Determinantal Point Process (DDP).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance","text":"distance(ce::AbstractCounterfactualExplanation, p::Real=2)\n\nComputes the distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l0-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l0","text":"distance_l0(ce::AbstractCounterfactualExplanation)\n\nComputes the L0 distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l1-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l1","text":"distance_l1(ce::AbstractCounterfactualExplanation)\n\nComputes the L1 distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_l2-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_l2","text":"distance_l2(ce::AbstractCounterfactualExplanation)\n\nComputes the L2 (Euclidean) distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_linf-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_linf","text":"distance_linf(ce::AbstractCounterfactualExplanation)\n\nComputes the L-inf distance of the counterfactual to the original factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_mad-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_mad","text":"distance_mad(ce::AbstractCounterfactualExplanation; agg=mean)\n\nThis is the distance measure proposed by Wachter et al. (2017).\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.predictive_entropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.predictive_entropy","text":"predictive_entropy(ce::AbstractCounterfactualExplanation; agg=Statistics.mean)\n\nComputes the predictive entropy of the counterfactuals. Explained in https://arxiv.org/abs/1406.2541.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.logitbinarycrossentropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.logitbinarycrossentropy","text":"Flux.Losses.logitbinarycrossentropy(ce::AbstractCounterfactualExplanation)\n\nSimply extends the logitbinarycrossentropy method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.logitcrossentropy-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.logitcrossentropy","text":"Flux.Losses.logitcrossentropy(ce::AbstractCounterfactualExplanation)\n\nSimply extends the logitcrossentropy method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Flux.Losses.mse-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"Flux.Losses.mse","text":"Flux.Losses.mse(ce::AbstractCounterfactualExplanation)\n\nSimply extends the mse method to work with objects of type AbstractCounterfactualExplanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Internal-functions","page":"๐Ÿง Reference","title":"Internal functions","text":"","category":"section"},{"location":"reference/","page":"๐Ÿง Reference","title":"๐Ÿง Reference","text":"Modules = [\n CounterfactualExplanations, \n CounterfactualExplanations.Convergence,\n CounterfactualExplanations.Evaluation,\n CounterfactualExplanations.DataPreprocessing,\n CounterfactualExplanations.Models, \n CounterfactualExplanations.GenerativeModels,\n CounterfactualExplanations.Generators, \n CounterfactualExplanations.Objectives\n]\nPublic = false","category":"page"},{"location":"reference/#CounterfactualExplanations.FluxModelParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.FluxModelParams","text":"FluxModelParams\n\nDefault MLP training parameters.\n\n\n\n\n\n","category":"type"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{AbstractFittedModel}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat AbstractFittedModel as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{AbstractGenerator}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat AbstractGenerator as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.adjust_shape!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.adjust_shape!","text":"adjust_shape!(ce::CounterfactualExplanation)\n\nA convenience method that adjusts the dimensions of the counterfactual state and related fields.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.adjust_shape-Tuple{CounterfactualExplanation, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.adjust_shape","text":"adjust_shape(\n ce::CounterfactualExplanation, \n x::AbstractArray\n)\n\nA convenience method that adjusts the dimensions of x.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.apply_domain_constraints!-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.apply_domain_constraints!","text":"apply_domain_constraints!(ce::CounterfactualExplanation)\n\nWrapper function that applies underlying domain constraints.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.apply_mutability-Tuple{CounterfactualExplanation, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.apply_mutability","text":"apply_mutability(\n ce::CounterfactualExplanation,\n ฮ”sโ€ฒ::AbstractArray,\n)\n\nA subroutine that applies mutability constraints to the proposed vector of feature perturbations.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual","text":"counterfactual(ce::CounterfactualExplanation)\n\nA convenience method that returns the counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_label-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_label","text":"counterfactual_label(ce::CounterfactualExplanation)\n\nA convenience method that returns the predicted label of the counterfactual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_label_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_label_path","text":"counterfactual_label_path(ce::CounterfactualExplanation)\n\nReturns the counterfactual labels for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.counterfactual_probability","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_probability","text":"counterfactual_probability(ce::CounterfactualExplanation)\n\nA convenience method that computes the class probabilities of the counterfactual.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.counterfactual_probability_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.counterfactual_probability_path","text":"counterfactual_probability_path(ce::CounterfactualExplanation)\n\nReturns the counterfactual probabilities for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_array-Tuple{MultivariateStats.AbstractDimensionalityReduction, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_array","text":"decode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)\n\nHelper function to decode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_array-Tuple{StatsBase.AbstractDataTransform, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_array","text":"decode_array(dt::StatsBase.AbstractDataTransform, x::AbstractArray)\n\nHelper function to decode an array x using a data transform dt::StatsBase.AbstractDataTransform.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.decode_state","page":"๐Ÿง Reference","title":"CounterfactualExplanations.decode_state","text":"function decode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, )\n\nApplies all the applicable decoding functions:\n\nIf applicable, map the state variable back from the latent space to the feature space.\nIf and where applicable, inverse-transform features.\nReconstruct all categorical encodings.\n\nFinally, the decoded counterfactual is returned.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.encode_array-Tuple{MultivariateStats.AbstractDimensionalityReduction, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_array","text":"encode_array(dt::MultivariateStats.AbstractDimensionalityReduction, x::AbstractArray)\n\nHelper function to encode an array x using a data transform dt::MultivariateStats.AbstractDimensionalityReduction.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.encode_array-Tuple{StatsBase.AbstractDataTransform, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_array","text":"encode_array(dt::StatsBase.AbstractDataTransform, x::AbstractArray)\n\nHelper function to encode an array x using a data transform dt::StatsBase.AbstractDataTransform.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.encode_state","page":"๐Ÿง Reference","title":"CounterfactualExplanations.encode_state","text":"function encode_state( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing} = nothing, )\n\nApplies all required encodings to x:\n\nIf applicable, it maps x to the latent space learned by the generative model.\nIf and where applicable, it rescales features. \n\nFinally, it returns the encoded state variable.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.factual-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual","text":"factual(ce::CounterfactualExplanation)\n\nA convenience method to retrieve the factual x.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.factual_label-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual_label","text":"factual_label(ce::CounterfactualExplanation)\n\nA convenience method to get the predicted label associated with the factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.factual_probability-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.factual_probability","text":"factual_probability(ce::CounterfactualExplanation)\n\nA convenience method to compute the class probabilities of the factual.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.find_potential_neighbours-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.find_potential_neighbours","text":"find_potential_neighbors(ce::AbstractCounterfactualExplanation)\n\nFinds potential neighbors for the selected factual data point.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.get_meta-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.get_meta","text":"get_meta(ce::CounterfactualExplanation)\n\nReturns meta data for a counterfactual explanation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.guess_likelihood-Tuple{Union{AbstractMatrix, AbstractVector}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.guess_likelihood","text":"guess_likelihood(y::RawOutputArrayType)\n\nGuess the likelihood based on the scientific type of the output array. Returns a symbol indicating the guessed likelihood and the scientific type of the output array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.guess_loss-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.guess_loss","text":"guess_loss(ce::CounterfactualExplanation)\n\nGuesses the loss function to be used for the counterfactual search in case likelihood field is specified for the AbstractFittedModel instance and no loss function was explicitly declared for AbstractGenerator instance.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.in_target_class-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.in_target_class","text":"in_target_class(ce::CounterfactualExplanation)\n\nCheck if the counterfactual is in the target class.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.initialize_state-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.initialize_state","text":"initialize_state(ce::CounterfactualExplanation)\n\nInitializes the starting point for the factual(s):\n\nIf ce.initialization is set to :identity or counterfactuals are searched in a latent space, then nothing is done.\nIf ce.initialization is set to :add_perturbation, then a random perturbation is added to the factual following following Slack (2021): https://arxiv.org/abs/2106.02666. The authors show that this improves adversarial robustness.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.map_from_latent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.map_from_latent","text":"map_from_latent(\n ce::CounterfactualExplanation,\n x::Union{AbstractArray,Nothing}=nothing,\n)\n\nMaps the state variable back from the latent space to the feature space.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.map_to_latent","page":"๐Ÿง Reference","title":"CounterfactualExplanations.map_to_latent","text":"function maptolatent( ce::CounterfactualExplanation, x::Union{AbstractArray,Nothing}=nothing, ) \n\nMaps x from the feature space mathcalX to the latent space learned by the generative model.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.output_dim-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.output_dim","text":"output_dim(ce::CounterfactualExplanation)\n\nA convenience method that returns the output dimension of the predictive model.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.reset!-Tuple{CounterfactualExplanations.FluxModelParams}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.reset!","text":"reset!(flux_training_params::FluxModelParams)\n\nRestores the default parameter values.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.steps_exhausted-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.steps_exhausted","text":"steps_exhausted(ce::CounterfactualExplanation)\n\nA convenience method that checks if the number of maximum iterations has been exhausted.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.target_probs_path-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.target_probs_path","text":"target_probs_path(ce::CounterfactualExplanation)\n\nReturns the target probabilities for each step of the search.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.distance_measures","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.distance_measures","text":"All distance measures.\n\n\n\n\n\n","category":"constant"},{"location":"reference/#Base.vcat-Tuple{CounterfactualExplanations.Evaluation.Benchmark, CounterfactualExplanations.Evaluation.Benchmark}","page":"๐Ÿง Reference","title":"Base.vcat","text":"Base.vcat(bmk1::Benchmark, bmk2::Benchmark)\n\nVertically concatenates two Benchmark objects.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.compute_measure-Tuple{CounterfactualExplanation, Function, Function}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.compute_measure","text":"compute_measure(ce::CounterfactualExplanation, measure::Function, agg::Function)\n\nComputes a single measure for a counterfactual explanation. The measure is applied to the counterfactual explanation ce and aggregated using the aggregation function agg.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.to_dataframe-Tuple{Vector, Any, Bool, Bool, Bool, CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.to_dataframe","text":"evaluate_dataframe(\n ce::CounterfactualExplanation,\n measure::Vector{Function},\n agg::Function,\n report_each::Bool,\n pivot_longer::Bool,\n store_ce::Bool,\n)\n\nEvaluates a counterfactual explanation and returns a dataframe of evaluation measures.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Evaluation.validity_strict-Tuple{CounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Evaluation.validity_strict","text":"validity_strict(ce::CounterfactualExplanation)\n\nChecks if the counterfactual search has been strictly valid in the sense that it has converged with respect to the pre-specified target probability ฮณ.\n\n\n\n\n\n","category":"method"},{"location":"reference/#Base.Broadcast.broadcastable-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"Base.Broadcast.broadcastable","text":"Treat CounterfactualData as scalar when broadcasting.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing._subset-Tuple{CounterfactualData, Vector{Int64}}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing._subset","text":"_subset(data::CounterfactualData, idx::Vector{Int})\n\nCreates a subset of the data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.convert_to_1d-Tuple{Matrix, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.convert_to_1d","text":"convert_to_1d(y::Matrix, y_levels::AbstractArray)\n\nHelper function to convert a one-hot encoded matrix to a vector of labels. This is necessary because MLJ models require the labels to be represented as a vector, but the synthetic datasets in this package hold the labels in one-hot encoded form.\n\nArguments\n\ny::Matrix: The one-hot encoded matrix.\ny_levels::AbstractArray: The levels of the categorical variable.\n\nReturns\n\nlabels: A vector of labels.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.get_generative_model-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.get_generative_model","text":"get_generative_model(counterfactual_data::CounterfactualData)\n\nReturns the underlying generative model. If there is no existing model available, the default generative model (VAE) is used. Otherwise it is expected that existing generative model has been pre-trained or else a warning is triggered.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.has_pretrained_generative_model-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.has_pretrained_generative_model","text":"has_pretrained_generative_model(counterfactual_data::CounterfactualData)\n\nChecks if generative model is present and trained.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.input_dim-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.input_dim","text":"input_dim(counterfactual_data::CounterfactualData)\n\nHelper function that returns the input dimension (number of features) of the data. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.mutability_constraints-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.mutability_constraints","text":"mutability_constraints(counterfactual_data::CounterfactualData)\n\nA convenience function that returns the mutability constraints. If none were specified, it is assumed that all features are mutable in :both directions.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.preprocess_data_for_mlj","text":"preprocess_data_for_mlj(data::CounterfactualData)\n\nHelper function to preprocess data::CounterfactualData for MLJ models.\n\nArguments\n\ndata::CounterfactualData: The data to be preprocessed.\n\nReturns\n\n(df_x, y): A tuple containing the preprocessed data, with df_x being a DataFrame object and y being a categorical vector.\n\nExample\n\nX, y = preprocessdatafor_mlj(data)\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.reconstruct_cat_encoding-Tuple{CounterfactualData, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.reconstruct_cat_encoding","text":"reconstruct_cat_encoding(counterfactual_data::CounterfactualData, x::Vector)\n\nReconstruct the categorical encoding for a single instance. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.subsample-Tuple{CounterfactualData, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.subsample","text":"subsample(data::CounterfactualData, n::Int)\n\nHelper function to randomly subsample data::CounterfactualData.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.train_test_split-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.train_test_split","text":"train_test_split(data::CounterfactualData;test_size=0.2,keep_class_ratio=false)\n\nSplits data into train and test split.\n\nArguments\n\ndata::CounterfactualData: The data to be preprocessed.\ntest_size=0.2: Proportion of the data to be used for testing. \nkeep_class_ratio=false: Decides whether to sample equally from each class, or keep their relative size.\n\nReturns\n\n(train_data::CounterfactualData, test_data::CounterfactualData): A tuple containing the train and test splits.\n\nExample\n\ntrain, test = traintestsplit(data, testsize=0.1, keepclass_ratio=true)\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.DataPreprocessing.unpack_data-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.DataPreprocessing.unpack_data","text":"unpack_data(data::CounterfactualData)\n\nHelper function that unpacks data.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.AbstractCustomDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractCustomDifferentiableModel","text":"Base type for custom differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractFluxModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractFluxModel","text":"Base type for differentiable models written in Flux.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractMLJModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractMLJModel","text":"Base type for differentiable models from the MLJ library.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractNonDifferentiableJuliaModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractNonDifferentiableJuliaModel","text":"Base type for non-differentiable models written in pure Julia.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.AbstractNonDifferentiableModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.AbstractNonDifferentiableModel","text":"Base type for non-differentiable models.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.FluxEnsembleParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.FluxEnsembleParams","text":"FluxModelParams\n\nDefault Deep Ensemble training parameters.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.TreeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.TreeModel","text":"TreeModel <: AbstractNonDifferentiableJuliaModel\n\nConstructor for tree-based models from the MLJ library. \n\nArguments\n\nmodel::Any: The model selected by the user. Must be a model from the MLJ library.\nlikelihood::Symbol: The likelihood of the model. Must be one of [:classification_binary, :classification_multi].\n\nReturns\n\nTreeModel: A tree-based model from the MLJ library wrapped inside the TreeModel class.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Models.TreeModel-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.TreeModel","text":"Outer constructor method for TreeModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.binary_to_onehot-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.binary_to_onehot","text":"binary_to_onehot(p)\n\nHelper function to turn dummy-encoded variable into onehot-encoded variable.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.build_ensemble-Tuple{Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.build_ensemble","text":"build_ensemble(K::Int;kw=(input_dim=2,n_hidden=32,output_dim=1))\n\nHelper function that builds an ensemble of K models.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.build_mlp-Tuple{}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.build_mlp","text":"build_mlp()\n\nHelper function to build simple MLP.\n\nExamples\n\nnn = build_mlp()\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.data_loader-Tuple{CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.data_loader","text":"data_loader(data::CounterfactualData)\n\nPrepares counterfactual data for training in Flux.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.get_individual_classifiers-Tuple{CounterfactualExplanations.Models.TreeModel}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.get_individual_classifiers","text":"get_individual_classifiers(M::TreeModel)\n\nReturns the individual classifiers in the forest. If the input is a decision tree, the method returns the decision tree itself inside an array.\n\nArguments\n\nM::TreeModel: The model selected by the user.\n\nReturns\n\nclassifiers::AbstractArray: An array of individual classifiers in the forest.\n\nExample\n\nclassifiers = Models.getindividualclassifiers(M) # returns the individual classifiers in the forest\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{CounterfactualExplanations.Models.TreeModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::TreeModel, data::CounterfactualData; kwargs...)\n\nFits the model M to the data in the CounterfactualData object. This method is not called by the user directly.\n\nArguments\n\nM::TreeModel: The wrapper for a TreeModel.\ndata::CounterfactualData: The CounterfactualData object containing the data to be used for training the model.\n\nReturns\n\nM::TreeModel: The fitted TreeModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{FluxEnsemble, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::FluxEnsemble, data::CounterfactualData; kwargs...)\n\nWrapper function to retrain.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Models.train-Tuple{FluxModel, CounterfactualData}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Models.train","text":"train(M::FluxModel, data::CounterfactualData; kwargs...)\n\nWrapper function to retrain FluxModel.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.AbstractGMParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.AbstractGMParams","text":"Base type of generative model hyperparameter container.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.AbstractGenerativeModel","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.AbstractGenerativeModel","text":"Base type for generative model.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.Encoder","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.Encoder","text":"Encoder\n\nConstructs encoder part of VAE: a simple Flux neural network with one hidden layer and two linear output layers for the first two moments of the latent distribution.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAE","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAE","text":"VAE <: AbstractGenerativeModel\n\nConstructs the Variational Autoencoder. The VAE is a subtype of AbstractGenerativeModel. Any (sub-)type of AbstractGenerativeModel is accepted by latent space generators. \n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAE-Tuple{Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAE","text":"VAE(input_dim;kws...)\n\nOuter method for instantiating a VAE.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.VAEParams","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.VAEParams","text":"VAEParams <: AbstractGMParams\n\nThe default VAE parameters describing both the encoder/decoder architecture and the training process.\n\n\n\n\n\n","category":"type"},{"location":"reference/#Base.rand","page":"๐Ÿง Reference","title":"Base.rand","text":"Random.rand(encoder::Encoder, x, device=cpu)\n\nDraws random samples from the latent distribution.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.Decoder-Tuple{Int64, Int64, Int64}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.Decoder","text":"Decoder(input_dim::Int, latent_dim::Int, hidden_dim::Int; activation=relu)\n\nThe default decoder architecture is just a Flux Chain with one hidden layer and a linear output layer. \n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.get_data-Tuple{AbstractArray, AbstractArray, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.get_data","text":"get_data(X::AbstractArray, y::AbstractArray, batch_size)\n\nPreparing data for mini-batch training .\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.reconstruct","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.reconstruct","text":"reconstruct(generative_model::VAE, x, device=cpu)\n\nImplements a full pass of some input x through the VAE: x โ†ฆ xฬ‚.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.GenerativeModels.reparameterization_trick","page":"๐Ÿง Reference","title":"CounterfactualExplanations.GenerativeModels.reparameterization_trick","text":"reparameterization_trick(ฮผ,logฯƒ,device=cpu)\n\nHelper function that implements the reparameterization trick: z โˆผ ๐’ฉ(ฮผ,ฯƒยฒ) โ‡” z=ฮผ + ฯƒ โŠ™ ฮต, ฮต โˆผ ๐’ฉ(0,I).\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.Penalty","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.Penalty","text":"Type union for acceptable argument types for the penalty field of GradientBasedGenerator.\n\n\n\n\n\n","category":"type"},{"location":"reference/#CounterfactualExplanations.Generators._replace_nans","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators._replace_nans","text":"_replace_nans(ฮ”sโ€ฒ::AbstractArray, old_new::Pair=(NaN => 0))\n\nHelper function to deal with exploding gradients. This is only a temporary fix and will be improved.\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.calculate_delta-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.calculate_delta","text":"calculate_delta(ce::AbstractCounterfactualExplanation, penalty::Vector{Function})\n\nCalculates the penalty for the proposed feature tweak.\n\nArguments\n\nce::AbstractCounterfactualExplanation: The counterfactual explanation object.\n\nReturns\n\ndelta::Float64: The calculated penalty for the proposed feature tweak.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.esatisfactory_instance-Tuple{FeatureTweakGenerator, AbstractArray, AbstractArray}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.esatisfactory_instance","text":"esatisfactory_instance(generator::FeatureTweakGenerator, x::AbstractArray, paths::Dict{String, Dict{String, Any}})\n\nReturns an epsilon-satisfactory counterfactual for x based on the paths provided.\n\nArguments\n\ngenerator::FeatureTweakGenerator: The feature tweak generator.\nx::AbstractArray: The factual instance.\npaths::Dict{String, Dict{String, Any}}: A list of paths to the leaves of the tree to be used for tweaking the feature.\n\nReturns\n\nesatisfactory::AbstractArray: The epsilon-satisfactory instance.\n\nExample\n\nesatisfactory = esatisfactory_instance(generator, x, paths) # returns an epsilon-satisfactory counterfactual for x based on the paths provided\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.feature_selection!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.feature_selection!","text":"feature_selection!(ce::AbstractCounterfactualExplanation)\n\nPerform feature selection to find the dimension with the closest (but not equal) values between the ce.x (factual) and ce.sโ€ฒ (counterfactual) arrays.\n\nArguments\n\nce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.\n\nReturns\n\nnothing\n\nThe function iteratively modifies the ce.sโ€ฒ counterfactual array by updating its elements to match the corresponding elements in the ce.x factual array, one dimension at a time, until the predicted label of the modified ce.sโ€ฒ matches the predicted label of the ce.x array.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.find_closest_dimension-Tuple{Any, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.find_closest_dimension","text":"find_closest_dimension(factual, counterfactual)\n\nFind the dimension with the closest (but not equal) values between the factual and counterfactual arrays.\n\nArguments\n\nfactual: The factual array.\ncounterfactual: The counterfactual array.\n\nReturns\n\nclosest_dimension: The index of the dimension with the closest values.\n\nThe function iterates over the indices of the factual array and calculates the absolute difference between the corresponding elements in the factual and counterfactual arrays. It returns the index of the dimension with the smallest difference, excluding dimensions where the values in factual and counterfactual are equal.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.find_counterfactual-NTuple{4, Any}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.find_counterfactual","text":"find_counterfactual(model, factual_class, counterfactual_data, counterfactual_candidates)\n\nFind the first counterfactual index by predicting labels.\n\nArguments\n\nmodel: The fitted model used for prediction.\ntarget_class: Expected target class.\ncounterfactual_data: Data required for counterfactual generation.\ncounterfactual_candidates: The array of counterfactual candidates.\n\nReturns\n\ncounterfactual: The index of the first counterfactual found.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.growing_spheres_generation!-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.growing_spheres_generation!","text":"growing_spheres_generation(ce::AbstractCounterfactualExplanation)\n\nGenerate counterfactual candidates using the growing spheres generation algorithm.\n\nArguments\n\nce::AbstractCounterfactualExplanation: An instance of the AbstractCounterfactualExplanation type representing the counterfactual explanation.\n\nReturns\n\nnothing\n\nThis function applies the growing spheres generation algorithm to generate counterfactual candidates. It starts by generating random points uniformly on a sphere, gradually reducing the search space until no counterfactuals are found. Then it expands the search space until at least one counterfactual is found or the maximum number of iterations is reached.\n\nThe algorithm iteratively generates counterfactual candidates and predicts their labels using the model stored in ce.M. It checks if any of the predicted labels are different from the factual class. The process of reducing the search space involves halving the search radius, while the process of expanding the search space involves increasing the search radius.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, ce::AbstractCounterfactualExplanation)\n\nDispatches to the appropriate complexity function for any generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Function, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Function, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Nothing, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Nothing, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where no penalty is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Vector{<:Tuple}, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided with additional keyword arguments.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.h-Tuple{AbstractGenerator, Vector{Function}, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.h","text":"h(generator::AbstractGenerator, penalty::Tuple, ce::AbstractCounterfactualExplanation)\n\nOverloads the h function for the case where a single penalty function is provided with additional keyword arguments.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.hyper_sphere_coordinates-Tuple{Integer, AbstractArray, AbstractFloat, AbstractFloat}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.hyper_sphere_coordinates","text":"hyper_sphere_coordinates(n_search_samples::Int, instance::Vector{Float64}, low::Int, high::Int; p_norm::Int=2)\n\nGenerates candidate counterfactuals using the growing spheres method based on hyper-sphere coordinates.\n\nThe implementation follows the Random Point Picking over a sphere algorithm described in the paper: \"Learning Counterfactual Explanations for Tabular Data\" by Pawelczyk, Broelemann & Kascneci (2020), presented at The Web Conference 2020 (WWW). It ensures that points are sampled uniformly at random using insights from: http://mathworld.wolfram.com/HyperspherePointPicking.html\n\nThe growing spheres method is originally proposed in the paper: \"Comparison-based Inverse Classification for Interpretability in Machine Learning\" by Thibaut Laugel et al (2018), presented at the International Conference on Information Processing and Management of Uncertainty in Knowledge-Based Systems (2018).\n\nArguments\n\nn_search_samples::Int: The number of search samples (int > 0).\ninstance::AbstractArray: The input point array.\nlow::AbstractFloat: The lower bound (float >= 0, l < h).\nhigh::AbstractFloat: The upper bound (float >= 0, h > l).\np_norm::Integer: The norm parameter (int >= 1).\n\nReturns\n\ncandidate_counterfactuals::Array: An array of candidate counterfactuals.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.propose_state-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.propose_state","text":"propose_state(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nProposes new state based on backpropagation.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.search_path","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.search_path","text":"search_path(tree::Union{DecisionTree.Leaf, DecisionTree.Node}, target::RawTargetType, path::AbstractArray)\n\nReturn a path index list with the inequality symbols, thresholds and feature indices.\n\nArguments\n\ntree::Union{DecisionTree.Leaf, DecisionTree.Node}: The root node of a decision tree.\ntarget::RawTargetType: The target class.\npath::AbstractArray: A list containing the paths found thus far.\n\nReturns\n\npaths::AbstractArray: A list of paths to the leaves of the tree to be used for tweaking the feature.\n\nExample\n\npaths = search_path(tree, target) # returns a list of paths to the leaves of the tree to be used for tweaking the feature\n\n\n\n\n\n","category":"function"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, ce::AbstractCounterfactualExplanation)\n\nDispatches to the appropriate loss function for any generator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, Function, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, loss::Function, ce::AbstractCounterfactualExplanation)\n\nOverloads the โ„“ function for the case where a single loss function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โ„“-Tuple{AbstractGenerator, Nothing, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โ„“","text":"โ„“(generator::AbstractGenerator, loss::Nothing, ce::AbstractCounterfactualExplanation)\n\nOverloads the โ„“ function for the case where no loss function is provided.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‚h-Tuple{AbstractGradientBasedGenerator, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‚h","text":"โˆ‚h(generator::AbstractGradientBasedGenerator, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the complexity penalty at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access. \n\nIf the penalty is not provided, it returns 0.0. By default, Zygote never works out the gradient for constants and instead returns 'nothing', so we need to add a manual step to override this behaviour. See here: https://discourse.julialang.org/t/zygote-gradient/26715.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‚โ„“-Tuple{AbstractGradientBasedGenerator, AbstractDifferentiableModel, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‚โ„“","text":"โˆ‚โ„“(generator::AbstractGradientBasedGenerator, M::Union{Models.LogisticModel, Models.BayesianLogisticModel}, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the loss function at the current counterfactual state for gradient-based generators. It assumes that Zygote.jl has gradient access.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Generators.โˆ‡-Tuple{AbstractGradientBasedGenerator, AbstractDifferentiableModel, AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Generators.โˆ‡","text":"โˆ‡(generator::AbstractGradientBasedGenerator, M::Models.AbstractDifferentiableModel, ce::AbstractCounterfactualExplanation)\n\nThe default method to compute the gradient of the counterfactual search objective for gradient-based generators. It simply computes the weighted sum over partial derivates. It assumes that Zygote.jl has gradient access. If the counterfactual is being generated using Probe, the hinge loss is added to the gradient.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.distance_from_target-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.distance_from_target","text":"distance_from_target(\n ce::AbstractCounterfactualExplanation;\n K::Int=50\n)\n\nComputes the distance of the counterfactual from a point in the target main.\n\n\n\n\n\n","category":"method"},{"location":"reference/#CounterfactualExplanations.Objectives.model_loss_penalty-Tuple{AbstractCounterfactualExplanation}","page":"๐Ÿง Reference","title":"CounterfactualExplanations.Objectives.model_loss_penalty","text":"function model_loss_penalty(\n ce::AbstractCounterfactualExplanation;\n agg=mean\n)\n\nAdditional penalty for ClaPROARGenerator.\n\n\n\n\n\n","category":"method"},{"location":"extensions/","page":"Overview","title":"Overview","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"extensions/#Extensions","page":"Overview","title":"โ›“๏ธ Extensions","text":"","category":"section"},{"location":"extensions/","page":"Overview","title":"Overview","text":"In this section, you will find information about package extensions of the CounterfactualExplanations package. Extensions are a relatively new feature of Julia that allows users to conditionally load code based on the presence of other packages. This is useful for creating packages that extend the functionality of other packages, without requiring the user to install the package being extended.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/evaluation/#Performance-Evaluation","page":"Evaluating Explanations","title":"Performance Evaluation","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Now that we know how to generate counterfactual explanations in Julia, you may have a few follow-up questions: How do I know if the counterfactual search has been successful? How good is my counterfactual explanation? What does โ€˜goodโ€™ even mean in this context? In this tutorial, we will see how counterfactual explanations can be evaluated with respect to their performance.","category":"page"},{"location":"tutorials/evaluation/#Default-Measures","page":"Evaluating Explanations","title":"Default Measures","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Numerous evaluation measures for counterfactual explanations have been proposed. In what follows, we will cover some of the most important measures.","category":"page"},{"location":"tutorials/evaluation/#Single-Measure,-Single-Counterfactual","page":"Evaluating Explanations","title":"Single Measure, Single Counterfactual","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"One of the most important measures is validity, which simply determines whether or not a counterfactual explanation x^prime is valid in the sense that it yields the target prediction: M(x^prime)=t. We can evaluate the validity of a single counterfactual explanation ce using the Evaluation.evaluate function as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Evaluation: evaluate, validity\nevaluate(ce; measure=validity)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float64}}:\n [1.0]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"For a single counterfactual explanation, this evaluation measure can only take two values: it is either equal to 1, if the explanation is valid or 0 otherwise. Another important measure is distance, which relates to the distance between the factual x and the counterfactual x^prime. In the context of Algorithmic Recourse, higher distances are typically associated with higher costs to individuals seeking recourse.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Objectives: distance\nevaluate(ce; measure=distance)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float32}}:\n [3.2273161]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, distance computes the L2 (Euclidean) distance.","category":"page"},{"location":"tutorials/evaluation/#Multiple-Measures,-Single-Counterfactual","page":"Evaluating Explanations","title":"Multiple Measures, Single Counterfactual","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"You might be interested in computing not just the L2 distance, but various LP norms. This can be done by supplying a vector of functions to the measure key argument. For convenience, all default distance measures have already been collected in a vector:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"using CounterfactualExplanations.Evaluation: distance_measures\ndistance_measures","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"4-element Vector{Function}:\n distance_l0 (generic function with 1 method)\n distance_l1 (generic function with 1 method)\n distance_l2 (generic function with 1 method)\n distance_linf (generic function with 1 method)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"We can use this vector of evaluation measures as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ce; measure=distance_measures)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"4-element Vector{Vector{Float32}}:\n [2.0]\n [3.2273161]\n [2.7737978]\n [2.7285953]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"If no measure is specified, the evaluate method will return all default measures,","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ce)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n [1.0]\n Float32[3.2273161]\n [0.0]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"which include:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"CounterfactualExplanations.Evaluation.default_measures","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Function}:\n validity (generic function with 1 method)\n distance (generic function with 1 method)\n redundancy (generic function with 1 method)","category":"page"},{"location":"tutorials/evaluation/#Multiple-Measures-and-Counterfactuals","page":"Evaluating Explanations","title":"Multiple Measures and Counterfactuals","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"We can also evaluate multiple counterfactual explanations at once:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"generator = DiCEGenerator()\nces = generate_counterfactual(x, target, counterfactual_data, M, generator; num_counterfactuals=5)\nevaluate(ces)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n [1.0]\n Float32[3.1955845]\n [[0.0, 0.0, 0.0, 0.0, 0.0]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, each evaluation measure is aggregated across all counterfactual explanations. To return individual measures for each counterfactual explanation you can specify report_each=true","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"3-element Vector{Vector}:\n BitVector[[1, 1, 1, 1, 1]]\n Vector{Float32}[[3.3671722, 3.1028512, 3.2829392, 3.0728922, 3.1520686]]\n [[0.0, 0.0, 0.0, 0.0, 0.0]]","category":"page"},{"location":"tutorials/evaluation/#Custom-Measures","page":"Evaluating Explanations","title":"Custom Measures","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"A measure is just a method that takes a CounterfactualExplanation as its only positional argument and agg::Function as a key argument specifying how measures should be aggregated across counterfactuals. Defining custom measures is therefore straightforward. For example, we could define a measure to compute the inverse target probability as follows:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"my_measure(ce::CounterfactualExplanation; agg=mean) = agg(1 .- CounterfactualExplanations.target_probs(ce))\nevaluate(ce; measure=my_measure)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"1-element Vector{Vector{Float32}}:\n [0.41711217]","category":"page"},{"location":"tutorials/evaluation/#Tidy-Output","page":"Evaluating Explanations","title":"Tidy Output","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, evaluate returns vectors of evaluation measures. The optional key argument output_format::Symbol can be used to post-process the output in two ways: firstly, to return the output as a dictionary, specify output_format=:Dict:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; output_format=:Dict, report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Dict{Symbol, Vector} with 3 entries:\n :validity => BitVector[[1, 1, 1, 1, 1]]\n :redundancy => [[0.0, 0.0, 0.0, 0.0, 0.0]]\n :distance => Vector{Float32}[[3.36717, 3.10285, 3.28294, 3.07289, 3.15207]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Secondly, to return the output as a data frame, specify output_format=:DataFrame.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"evaluate(ces; output_format=:DataFrame, report_each=true)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"By default, data frames are pivoted to long format using individual counterfactuals as the id column. This behaviour can be suppressed by specifying pivot_longer=false.","category":"page"},{"location":"tutorials/evaluation/#Multiple-Counterfactual-Explanations","page":"Evaluating Explanations","title":"Multiple Counterfactual Explanations","text":"","category":"section"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"It may be necessary to generate counterfactual explanations for multiple individuals.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"Below, for example, we first select multiple samples (5) from the non-target class and then generate counterfactual explanations for all of them.","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"This can be done using broadcasting:","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"# Factual and target:\nids = rand(findall(predict_label(M, counterfactual_data) .== factual), n_individuals)\nxs = select_factual(counterfactual_data, ids)\nces = generate_counterfactual(xs, target, counterfactual_data, M, generator; num_counterfactuals=5)\nevaluation = evaluate.(ces)","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"5-element Vector{Vector{Vector}}:\n [[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]\n\nVector{Vector}[[[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]]","category":"page"},{"location":"tutorials/evaluation/","page":"Evaluating Explanations","title":"Evaluating Explanations","text":"This leads us to our next topic: Performance Benchmarks.","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/generic/#GenericGenerator","page":"Generic","title":"GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"We use the term generic to relate to the basic counterfactual generator proposed by Wachter, Mittelstadt, and Russell (2017) with L1-norm regularization. There is also a variant of this generator that uses the distance metric proposed in Wachter, Mittelstadt, and Russell (2017), which we call WachterGenerator.","category":"page"},{"location":"explanation/generators/generic/#Description","page":"Generic","title":"Description","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"As the term indicates, this approach is simple: it forms the baseline approach for gradient-based counterfactual generators. Wachter, Mittelstadt, and Russell (2017) were among the first to realise that","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"[โ€ฆ] explanations can, in principle, be offered without opening the โ€œblack box.โ€โ€” Wachter, Mittelstadt, and Russell (2017)","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"Gradient descent is performed directly in the feature space. Concerning the cost heuristic, the authors choose to penalize the distance of counterfactuals from the factual value. This is based on the intuitive notion that larger feature perturbations require greater effort.","category":"page"},{"location":"explanation/generators/generic/#Usage","page":"Generic","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"generator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"(Image: )","category":"page"},{"location":"explanation/generators/generic/#References","page":"Generic","title":"References","text":"","category":"section"},{"location":"explanation/generators/generic/","page":"Generic","title":"Generic","text":"Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/greedy/#GreedyGenerator","page":"Greedy","title":"GreedyGenerator","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"We use the term greedy to describe the counterfactual generator introduced by Schut et al. (2021).","category":"page"},{"location":"explanation/generators/greedy/#Description","page":"Greedy","title":"Description","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"The Greedy generator works under the premise of generating realistic counterfactuals by minimizing predictive uncertainty. Schut et al. (2021) show that for models that incorporates predictive uncertainty in their predictions, maximizing the predictive probability corresponds to minimizing the predictive uncertainty: by construction, the generated counterfactual will therefore be realistic (low epistemic uncertainty) and unambiguous (low aleatoric uncertainty).","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"For the counterfactual search Schut et al. (2021) propose using a Jacobian-based Saliency Map Attack(JSMA). It is greedy in the sense that it is an โ€œiterative algorithm that updates the most salient feature, i.e.ย the feature that has the largest influence on the classification, by delta at each stepโ€ (Schut et al. 2021).","category":"page"},{"location":"explanation/generators/greedy/#Usage","page":"Greedy","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"M = fit_model(counterfactual_data, :DeepEnsemble)\ngenerator = GreedyGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"(Image: )","category":"page"},{"location":"explanation/generators/greedy/#References","page":"Greedy","title":"References","text":"","category":"section"},{"location":"explanation/generators/greedy/","page":"Greedy","title":"Greedy","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/probe/#ProbeGenerator","page":"PROBE","title":"ProbeGenerator","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The ProbeGenerator is designed to navigate the trade-offs between costs and robustness in Algorithmic Recourse (Pawelczyk et al. 2022).","category":"page"},{"location":"explanation/generators/probe/#Description","page":"PROBE","title":"Description","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The goal of ProbeGenerator is to find a recourse xโ€™ whose prediction at any point y within some set around xโ€™ belongs to the positive class with probability 1 - r, where r is the recourse invalidation rate. It minimizes the gap between the achieved and desired recourse invalidation rates, minimizes recourse costs, and also ensures that the resulting recourse achieves a positive model prediction.","category":"page"},{"location":"explanation/generators/probe/#Explanation","page":"PROBE","title":"Explanation","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The loss function this generator is defined below. R is a hinge loss parameter which helps control for robustness. The loss and penalty functions can still be chosen freely.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginaligned\nR(x sigma^2 I) + l(f(x) s) + lambda d_c(x x)\nendaligned","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"R uses the following formula to control for noise. It generates small perturbations and checks how often the counterfactual explanation flips back to a factual one, when small amounts of noise are added to it.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginaligned\nDelta(x^hatE) = E_varepsilonh(x^hatE) - h(x^hatE + varepsilon)\nendaligned","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"The above formula is not differentiable. For this reason the generator uses the closed form version of the formula below.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"beginequation\nDelta tilde(x^hatE sigma^2 I) = 1 - Phi left(fracsqrtf(x^hatE)sqrtnabla f(x^hatE)^T sigma^2 I nabla f(x^hatE)right) \nendequation","category":"page"},{"location":"explanation/generators/probe/#Usage","page":"PROBE","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Generating a counterfactual with the data loaded and generator chosen works as follows:","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Note: It is important to set the convergence to โ€œ:invalidation_rateโ€ here.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"M = fit_model(counterfactual_data, :DeepEnsemble)\nopt = Descent(0.01)\ngenerator = CounterfactualExplanations.Generators.ProbeGenerator(opt=opt)\nconv = CounterfactualExplanations.Convergence.InvalidationRateConvergence(;invalidation_rate=0.5)\nce = generate_counterfactual(x, target, counterfactual_data, M, generator, convergence=conv)\nplot(ce)","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Choosing different invalidation rates makes the counterfactual more or less robust. The following plot shows the counterfactuals generated for different invalidation rates.","category":"page"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"(Image: )","category":"page"},{"location":"explanation/generators/probe/#References","page":"PROBE","title":"References","text":"","category":"section"},{"location":"explanation/generators/probe/","page":"PROBE","title":"PROBE","text":"Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2022. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ arXiv Preprint arXiv:2203.06768.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/generators/gravitational/#GravitationalGenerator","page":"Gravitational","title":"GravitationalGenerator","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The GravitationalGenerator was introduced in Altmeyer et al. (2023). It is named so because it generates counterfactuals that gravitate towards some sensible point in the target domain.","category":"page"},{"location":"explanation/generators/gravitational/#Description","page":"Gravitational","title":"Description","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"Altmeyer et al. (2023) extend the general framework as follows,","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"beginaligned\nmathbfs^prime = arg min_mathbfs^prime in mathcalS textyloss(M(f(mathbfs^prime))y^*) + lambda_1 textcost(f(mathbfs^prime)) + lambda_2 textextcost(f(mathbfs^prime)) \nendaligned ","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"where textcost(f(mathbfs^prime)) denotes the proxy for costs faced by the individual. โ€œThe newly introduced term textextcost(f(mathbfs^prime)) is meant to capture and address external costs incurred by the collective of individuals in response to changes in mathbfs^prime.โ€ (Altmeyer et al. 2023)","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"For the GravitationalGenerator we have,","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"beginaligned\ntextextcost(f(mathbfs^prime)) = textdist(f(mathbfs^prime)barx^*) \nendaligned","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"where barx is some sensible point in the target domain, for example, the subsample average barx^*=textmean(x), x in mathcalD_1.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"There is a tradeoff then, between the distance of counterfactuals from their factual value and the chosen point in the target domain. The chart below illustrates how the counterfactual outcome changes as the penalty lambda_2 on the distance to the point in the target domain is increased from left to right (holding the other penalty term constant).","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#Usage","page":"Gravitational","title":"Usage","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The approach can be used in our package as follows:","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"generator = GravitationalGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\ndisplay(plot(ce))","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#Comparison-to-GenericGenerator","page":"Gravitational","title":"Comparison to GenericGenerator","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"The figure below compares the outcome for the GenericGenerator and the GravitationalGenerator.","category":"page"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"(Image: )","category":"page"},{"location":"explanation/generators/gravitational/#References","page":"Gravitational","title":"References","text":"","category":"section"},{"location":"explanation/generators/gravitational/","page":"Gravitational","title":"Gravitational","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/benchmarking/#Performance-Benchmarks","page":"Benchmarking Explanations","title":"Performance Benchmarks","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In the previous tutorial, we have seen how counterfactual explanations can be evaluated. An important follow-up task is to compare the performance of different counterfactual generators is an important task. Researchers can use benchmarks to test new ideas they want to implement. Practitioners can find the right counterfactual generator for their specific use case through benchmarks. In this tutorial, we will see how to run benchmarks for counterfactual generators.","category":"page"},{"location":"tutorials/benchmarking/#Post-Hoc-Benchmarking","page":"Benchmarking Explanations","title":"Post Hoc Benchmarking","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We begin by continuing the discussion from the previous tutorial: suppose you have generated multiple counterfactual explanations for multiple individuals, like below:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"# Factual and target:\nn_individuals = 5\nids = rand(findall(predict_label(M, counterfactual_data) .== factual), n_individuals)\nxs = select_factual(counterfactual_data, ids)\nces = generate_counterfactual(xs, target, counterfactual_data, M, generator; num_counterfactuals=5)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"You may be interested in comparing the outcomes across individuals. To benchmark the various counterfactual explanations using default evaluation measures, you can simply proceed as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(ces)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Under the hood, the benchmark(counterfactual_explanations::Vector{CounterfactualExplanation}) uses CounterfactualExplanations.Evaluation.evaluate(ce::CounterfactualExplanation) to generate a Benchmark object, which contains the evaluation in its most granular form as a DataFrame.","category":"page"},{"location":"tutorials/benchmarking/#Working-with-Benchmarks","page":"Benchmarking Explanations","title":"Working with Benchmarks","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"For convenience, the DataFrame containing the evaluation can be returned by simply calling the Benchmark object. By default, the aggregated evaluation measures across id (in line with the default behaviour of evaluate).","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk()","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"15ร—7 DataFrame\n Row โ”‚ sample variable value generator โ‹ฏ\n โ”‚ Base.UUID String Float64 Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff distance 3.17243 GradientBase โ‹ฏ\n 2 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff redundancy 0.0 GradientBase\n 3 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff validity 1.0 GradientBase\n 4 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b distance 3.07148 GradientBase\n 5 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b redundancy 0.0 GradientBase โ‹ฏ\n 6 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b validity 1.0 GradientBase\n 7 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af distance 3.62159 GradientBase\n 8 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af redundancy 0.0 GradientBase\n 9 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af validity 1.0 GradientBase โ‹ฏ\n 10 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b distance 2.62783 GradientBase\n 11 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b redundancy 0.0 GradientBase\n 12 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b validity 1.0 GradientBase\n 13 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 distance 2.91985 GradientBase โ‹ฏ\n 14 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 redundancy 0.0 GradientBase\n 15 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 validity 1.0 GradientBase\n 4 columns omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"To retrieve the granular dataset, simply do:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk(agg=nothing)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"75ร—8 DataFrame\n Row โ”‚ sample num_counterfactual variable v โ‹ฏ\n โ”‚ Base.UUID Int64 String F โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 distance 3 โ‹ฏ\n 2 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 2 distance 3\n 3 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 3 distance 3\n 4 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 4 distance 3\n 5 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 5 distance 3 โ‹ฏ\n 6 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 redundancy 0\n 7 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 2 redundancy 0\n 8 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 3 redundancy 0\n 9 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 4 redundancy 0 โ‹ฏ\n 10 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 5 redundancy 0\n 11 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff 1 validity 1\n โ‹ฎ โ”‚ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฑ\n 66 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 1 redundancy 0\n 67 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 2 redundancy 0 โ‹ฏ\n 68 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 3 redundancy 0\n 69 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 4 redundancy 0\n 70 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 5 redundancy 0\n 71 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 1 validity 1 โ‹ฏ\n 72 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 2 validity 1\n 73 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 3 validity 1\n 74 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 4 validity 1\n 75 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 5 validity 1 โ‹ฏ\n 5 columns and 54 rows omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Since benchmarks return a DataFrame object on call, post-processing is straightforward. For example, we could use Tidier.jl:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"using Tidier\n@chain bmk() begin\n @filter(variable == \"distance\")\n @select(sample, variable, value)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample variable value \n โ”‚ Base.UUID String Float64 \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff distance 3.17243\n 2 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b distance 3.07148\n 3 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af distance 3.62159\n 4 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b distance 2.62783\n 5 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 distance 2.91985","category":"page"},{"location":"tutorials/benchmarking/#Metadata-for-Counterfactual-Explanations","page":"Benchmarking Explanations","title":"Metadata for Counterfactual Explanations","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Benchmarks always report metadata for each counterfactual explanation, which is automatically inferred by default. The default metadata concerns the explained model and the employed generator. In the current example, we used the same model and generator for each individual:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @group_by(sample)\n @select(sample, model, generator)\n @summarize(model=first(model),generator=first(generator))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample model โ‹ฏ\n โ”‚ Base.UUID Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 239104d0-f59f-11ee-3d0c-d1db071927ff FluxModel(Chain(Dense(2 => 2)), โ€ฆ โ‹ฏ\n 2 โ”‚ 2398b3e2-f59f-11ee-3323-13d53fb7e75b FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 3 โ”‚ 2398b916-f59f-11ee-3f13-bd00858a39af FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 4 โ”‚ 2398bce8-f59f-11ee-37c1-ef7c6de27b6b FluxModel(Chain(Dense(2 => 2)), โ€ฆ\n 5 โ”‚ 2398c08a-f59f-11ee-175b-81c155750752 FluxModel(Chain(Dense(2 => 2)), โ€ฆ โ‹ฏ\n 1 column omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Metadata can also be provided as an optional key argument.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"meta_data = Dict(\n :generator => \"Generic\",\n :model => \"MLP\",\n)\nmeta_data = [meta_data for i in 1:length(ces)]\nbmk = benchmark(ces; meta_data=meta_data)\n@chain bmk() begin\n @group_by(sample)\n @select(sample, model, generator)\n @summarize(model=first(model),generator=first(generator))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"5ร—3 DataFrame\n Row โ”‚ sample model generator \n โ”‚ Base.UUID String String \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 27fae496-f59f-11ee-2c30-f35d1025a6d4 MLP Generic\n 2 โ”‚ 27fdcc6a-f59f-11ee-030b-152c9794c5f1 MLP Generic\n 3 โ”‚ 27fdd04a-f59f-11ee-2010-e1732ff5d8d2 MLP Generic\n 4 โ”‚ 27fdd340-f59f-11ee-1d20-050a69dcacef MLP Generic\n 5 โ”‚ 27fdd5fc-f59f-11ee-02e8-d198e436abb3 MLP Generic","category":"page"},{"location":"tutorials/benchmarking/#Ad-Hoc-Benchmarking","page":"Benchmarking Explanations","title":"Ad Hoc Benchmarking","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"So far we have assumed the following workflow:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model.\nGenerate counterfactual explanations for some individual(s) (generate_counterfactual).\nEvaluate and benchmark them (benchmark(ces::Vector{CounterfactualExplanation})).","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In many cases, it may be preferable to combine these steps. To this end, we have added support for two scenarios of Ad Hoc Benchmarking.","category":"page"},{"location":"tutorials/benchmarking/#Pre-trained-Models","page":"Benchmarking Explanations","title":"Pre-trained Models","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In the first scenario, it is assumed that the machine learning models have been pre-trained and so the workflow can be summarized as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model(s).\nGenerate counterfactual explanations and benchmark them.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We suspect that this is the most common workflow for practitioners who are interested in benchmarking counterfactual explanations for the pre-trained machine learning models. Letโ€™s go through this workflow using a simple example. We first train some models and store them in a dictionary:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"models = Dict(\n :MLP => fit_model(counterfactual_data, :MLP),\n :Linear => fit_model(counterfactual_data, :Linear),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Next, we store the counterfactual generators of interest in a dictionary as well:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"generators = Dict(\n :Generic => GenericGenerator(),\n :Gravitational => GravitationalGenerator(),\n :Wachter => WachterGenerator(),\n :ClaPROAR => ClaPROARGenerator(),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Then we can run a benchmark for individual(s) x, a pre-specified target and counterfactual_data as follows:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(x, target, counterfactual_data; models=models, generators=generators)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"In this case, metadata is automatically inferred from the dictionaries:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @filter(variable == \"distance\")\n @select(sample, variable, value, model, generator)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"8ร—5 DataFrame\n Row โ”‚ sample variable value model โ‹ฏ\n โ”‚ Base.UUID String Float64 Tupleโ€ฆ โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 2cba5eee-f59f-11ee-1844-cbc7a8372a38 distance 4.38877 (:Linear, Flux โ‹ฏ\n 2 โ”‚ 2cd740fe-f59f-11ee-35c3-1157eb1b7583 distance 4.17021 (:Linear, Flux\n 3 โ”‚ 2cd741e2-f59f-11ee-2b09-0d55ef9892b9 distance 4.31145 (:Linear, Flux\n 4 โ”‚ 2cd7420c-f59f-11ee-1996-6fa75e23bb57 distance 4.17035 (:Linear, Flux\n 5 โ”‚ 2cd74234-f59f-11ee-0ad0-9f21949f5932 distance 5.73182 (:MLP, FluxMod โ‹ฏ\n 6 โ”‚ 2cd7425c-f59f-11ee-3eb4-af34f85ffd3d distance 5.50606 (:MLP, FluxMod\n 7 โ”‚ 2cd7427a-f59f-11ee-10d3-a1df6c8dc125 distance 5.2114 (:MLP, FluxMod\n 8 โ”‚ 2cd74298-f59f-11ee-32d1-f501c104fea8 distance 5.3623 (:MLP, FluxMod\n 2 columns omitted","category":"page"},{"location":"tutorials/benchmarking/#Everything-at-once","page":"Benchmarking Explanations","title":"Everything at once","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Researchers, in particular, may be interested in combining all steps into one. This is the second scenario of Ad Hoc Benchmarking:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fit some machine learning model(s), generate counterfactual explanations and benchmark them.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"It involves calling benchmark directly on counterfactual data (the only positional argument):","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"bmk = benchmark(counterfactual_data)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"This will use the default models from standard_models_catalogue and train them on the data. All available generators from generator_catalogue will also be used:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @filter(variable == \"validity\")\n @select(sample, variable, value, model, generator)\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"200ร—5 DataFrame\n Row โ”‚ sample variable value model genera โ‹ฏ\n โ”‚ Base.UUID String Float64 Symbol Symbol โ‹ฏ\nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear gravit โ‹ฏ\n 2 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear growin\n 3 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear revise\n 4 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear clue\n 5 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear probe โ‹ฏ\n 6 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear dice\n 7 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear clapro\n 8 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear wachte\n 9 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear generi โ‹ฏ\n 10 โ”‚ 32d1817e-f59f-11ee-152f-a30b18c2e6f7 validity 1.0 Linear greedy\n 11 โ”‚ 32d255e8-f59f-11ee-3e8d-a9e9f6e23ea8 validity 1.0 Linear gravit\n โ‹ฎ โ”‚ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฑ\n 191 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP gravit\n 192 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP growin โ‹ฏ\n 193 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP revise\n 194 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP clue\n 195 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP probe\n 196 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP dice โ‹ฏ\n 197 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP clapro\n 198 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP wachte\n 199 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP generi\n 200 โ”‚ 3382d08a-f59f-11ee-10b3-f7d18cf7d3b5 validity 1.0 MLP greedy โ‹ฏ\n 1 column and 179 rows omitted","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Optionally, you can instead provide a dictionary of models and generators as before. Each value in the models dictionary should be one of two things:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Either be an object M of type AbstractFittedModel that implements the Models.train method.\nOr a DataType that can be called on CounterfactualData to create an object M as in (a).","category":"page"},{"location":"tutorials/benchmarking/#Multiple-Datasets","page":"Benchmarking Explanations","title":"Multiple Datasets","text":"","category":"section"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Benchmarks are run on single instances of type CounterfactualData. This is our design choice for two reasons:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"We want to avoid the loops inside the benchmark method(s) from getting too nested and convoluted.\nWhile it is straightforward to infer metadata for models and generators, this is not the case for datasets.","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Fortunately, it is very easy to run benchmarks for multiple datasets anyway, since Benchmark instances can be concatenated. To see how, letโ€™s consider an example involving multiple datasets, models and generators:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"# Data:\ndatasets = Dict(\n :moons => CounterfactualData(load_moons()...),\n :circles => CounterfactualData(load_circles()...),\n)\n\n# Models:\nmodels = Dict(\n :MLP => FluxModel,\n :Linear => Linear,\n)\n\n# Generators:\ngenerators = Dict(\n :Generic => GenericGenerator(),\n :Greedy => GreedyGenerator(),\n)","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"Then we can simply loop over the datasets and eventually concatenate the results like so:","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"using CounterfactualExplanations.Evaluation: distance_measures\nbmks = []\nfor (dataname, dataset) in datasets\n bmk = benchmark(dataset; models=models, generators=generators, measure=distance_measures)\n push!(bmks, bmk)\nend\nbmk = vcat(bmks[1], bmks[2]; ids=collect(keys(datasets)))","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"When ids are supplied, then a new id column is added to the evaluation data frame that contains unique identifiers for the different benchmarks. The optional idcol_name argument can be used to specify the name for that indicator column (defaults to \"dataset\"):","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"@chain bmk() begin\n @group_by(dataset, generator)\n @filter(model == :MLP)\n @filter(variable == \"distance_l1\")\n @summarize(L1_norm=mean(value))\n @ungroup\nend","category":"page"},{"location":"tutorials/benchmarking/","page":"Benchmarking Explanations","title":"Benchmarking Explanations","text":"4ร—3 DataFrame\n Row โ”‚ dataset generator L1_norm \n โ”‚ Symbol Symbol Float32 \nโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n 1 โ”‚ moons Generic 1.56555\n 2 โ”‚ moons Greedy 0.819269\n 3 โ”‚ circles Generic 1.83524\n 4 โ”‚ circles Greedy 0.498953","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"tutorials/whistle_stop/#Whistle-Stop-Tour","page":"Whiste-Stop Tour","title":"Whistle-Stop Tour","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"In this tutorial, we will go through a slightly more complex example involving synthetic data. We will generate Counterfactual Explanations using different generators and visualize the results.","category":"page"},{"location":"tutorials/whistle_stop/#Data-and-Classifier","page":"Whiste-Stop Tour","title":"Data and Classifier","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Choose some values for data and a model:\nn_dim = 2\nn_classes = 4\nn_samples = 400\nmodel_name = :MLP","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The code chunk below generates synthetic data and uses it to fit a classifier. The outcome variable counterfactual_data.y consists of 4 classes. The input data counterfactual_data.X consists of 2 features. We generate a total of 400 samples. On the model side, we have specified model_name = :MLP. The fit_model can be used to fit a number of default models.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"data = TaijaData.load_multi_class(n_samples)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\nM = fit_model(counterfactual_data, model_name)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The chart below visualizes our data along with the model predictions. In particular, the contour indicates the predicted probabilities generated by our classifier. By default, these are the predicted probabilities for y=1, the first label. For multi-dimensional input data is compressed into two dimensions and the decision boundary is approximated using Nearest Neighbors (this is still somewhat experimental).","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"plot(M, counterfactual_data)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"(Image: )","category":"page"},{"location":"tutorials/whistle_stop/#Counterfactual-Explanation","page":"Whiste-Stop Tour","title":"Counterfactual Explanation","text":"","category":"section"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"Next, we begin by specifying our target and factual label. We then draw a random sample from the non-target (factual) class.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Factual and target:\ntarget = 2\nfactual = 4\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"This sets the baseline for our counterfactual search: we plan to perturb the factual x to change the predicted label from y=4 to our target label target=2.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"Counterfactual generators accept several default parameters that can be used to adjust the counterfactual search at a high level: for example, a Flux.jl optimizer can be supplied to define how exactly gradient steps are performed. Importantly, one can also define the threshold probability at which the counterfactual search will converge. This relates to the probability predicted by the underlying black-box model, that the counterfactual belongs to the target class. A higher decision threshold typically prolongs the counterfactual search.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"# Search params:\ndecision_threshold = 0.75\nnum_counterfactuals = 3","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"The code below runs the counterfactual search for each generator available in the generator_catalogue. In each case, we also call the generic plot() method on the generated instance of type CounterfactualExplanation. This generates a simple plot that visualizes the entire counterfactual path. The chart below shows the results for all counterfactual generators: Factual: 4 โ†’ Target: 2.","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"ces = Dict()\nplts = []\nplottable_generators = filter(((k,v),) -> k โˆ‰ [:growing_spheres, :feature_tweak], generator_catalogue)\n# Search:\nfor (key, Generator) in plottable_generators\n generator = Generator()\n ce = generate_counterfactual(\n x, target, counterfactual_data, M, generator;\n num_counterfactuals = num_counterfactuals,\n convergence=GeneratorConditionsConvergence(\n decision_threshold=decision_threshold\n )\n )\n ces[key] = ce\n plts = [plts..., plot(ce; title=key, colorbar=false)]\nend","category":"page"},{"location":"tutorials/whistle_stop/","page":"Whiste-Stop Tour","title":"Whiste-Stop Tour","text":"(Image: )","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"how_to_guides/custom_models/#How-to-add-Custom-Models","page":"... add custom models","title":"How to add Custom Models","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Adding custom models is possible and relatively straightforward, as we will demonstrate in this guide.","category":"page"},{"location":"how_to_guides/custom_models/#Custom-Models","page":"... add custom models","title":"Custom Models","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Apart from the default models you can use any arbitrary (differentiable) model and generate recourse in the same way as before. Only two steps are necessary to make your own Julia model compatible with this package:","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The model needs to be declared as a subtype of <:CounterfactualExplanations.Models.AbstractFittedModel.\nYou need to extend the functions CounterfactualExplanations.Models.logits and CounterfactualExplanations.Models.probs for your custom model.","category":"page"},{"location":"how_to_guides/custom_models/#How-FluxModel-was-added","page":"... add custom models","title":"How FluxModel was added","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"To demonstrate how this can be done in practice, we will reiterate here how native support for Flux.jl models was enabled (Innes 2018). Once again we use synthetic data for an illustrative example. The code below loads the data and builds a simple model architecture that can be used for a multi-class prediction task. Note how outputs from the final layer are not passed through a softmax activation function, since the counterfactual loss is evaluated with respect to logits. The model is trained with dropout.","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"# Data:\nN = 200\ndata = TaijaData.load_blobs(N; centers=4, cluster_std=0.5)\ncounterfactual_data = DataPreprocessing.CounterfactualData(data...)\ny = counterfactual_data.y\nX = counterfactual_data.X\n\n# Flux model setup: \nusing Flux\ndata = Flux.DataLoader((X,y), batchsize=1)\nn_hidden = 32\noutput_dim = size(y,1)\ninput_dim = 2\nactivation = ฯƒ\nmodel = Chain(\n Dense(input_dim, n_hidden, activation),\n Dropout(0.1),\n Dense(n_hidden, output_dim)\n) \nloss(x, y) = Flux.Losses.logitcrossentropy(model(x), y)\n\n# Flux model training:\nusing Flux.Optimise: update!, Adam\nopt = Adam()\nepochs = 50\nfor epoch = 1:epochs\n for d in data\n gs = gradient(Flux.params(model)) do\n l = loss(d...)\n end\n update!(opt, Flux.params(model), gs)\n end\nend","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The code below implements the two steps that were necessary to make Flux models compatible with the package. We first declare our new struct as a subtype of <:AbstractDifferentiableModel, which itself is an abstract subtype of <:AbstractFittedModel. Computing logits amounts to just calling the model on inputs. Predicted probabilities for labels can in this case be computed by passing predicted logits through the softmax function. Finally, we just instantiate our model in the same way as always.","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"# Step 1)\nstruct MyFluxModel <: AbstractDifferentiableModel\n model::Any\n likelihood::Symbol\nend\n\n# Step 2)\n# import functions in order to extend\nimport CounterfactualExplanations.Models: logits\nimport CounterfactualExplanations.Models: probs \nlogits(M::MyFluxModel, X::AbstractArray) = M.model(X)\nprobs(M::MyFluxModel, X::AbstractArray) = softmax(logits(M, X))\nM = MyFluxModel(model, :classification_multi)","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"The code below implements the counterfactual search and plots the results:","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"factual_label = 4\ntarget = 2\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual_label))\nx = select_factual(counterfactual_data, chosen) \n\n# Counterfactual search:\ngenerator = GenericGenerator()\nce = generate_counterfactual(x, target, counterfactual_data, M, generator)\nplot(ce)","category":"page"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"(Image: )","category":"page"},{"location":"how_to_guides/custom_models/#References","page":"... add custom models","title":"References","text":"","category":"section"},{"location":"how_to_guides/custom_models/","page":"... add custom models","title":"... add custom models","text":"Innes, Mike. 2018. โ€œFlux: Elegant Machine Learning with Julia.โ€ Journal of Open Source Software 3 (25): 602. https://doi.org/10.21105/joss.00602.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CurrentModule = CounterfactualExplanations","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Documentation for CounterfactualExplanations.jl.","category":"page"},{"location":"#CounterfactualExplanations","page":"๐Ÿ  Home","title":"CounterfactualExplanations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations and Algorithmic Recourse in Julia.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: Stable) (Image: Dev) (Image: Build Status) (Image: Coverage) (Image: Code Style: Blue) (Image: License) (Image: Package Downloads) (Image: Aqua QA)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CounterfactualExplanations.jl is a package for generating Counterfactual Explanations (CE) and Algorithmic Recourse (AR) for black-box algorithms. Both CE and AR are related tools for explainable artificial intelligence (XAI). While the package is written purely in Julia, it can be used to explain machine learning algorithms developed and trained in other popular programming languages like Python and R. See below for a short introduction and other resources or dive straight into the docs.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"There is also a corresponding paper, Explaining Black-Box Models through Counterfactuals, which has been published in JuliaCon Proceedings. Please consider citing the paper, if you use this package in your work:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: DOI) (Image: DOI)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"@article{Altmeyer2023,\n doi = {10.21105/jcon.00130},\n url = {https://doi.org/10.21105/jcon.00130},\n year = {2023},\n publisher = {The Open Journal},\n volume = {1},\n number = {1},\n pages = {130},\n author = {Patrick Altmeyer and Arie van Deursen and Cynthia C. s. Liem},\n title = {Explaining Black-Box Models through Counterfactuals},\n journal = {Proceedings of the JuliaCon Conferences}\n}","category":"page"},{"location":"#Installation","page":"๐Ÿ  Home","title":"๐Ÿšฉ Installation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"You can install the stable release from Juliaโ€™s General Registry as follows:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"using Pkg\nPkg.add(\"CounterfactualExplanations\")","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"CounterfactualExplanations.jl is under active development. To install the development version of the package you can run the following command:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"using Pkg\nPkg.add(url=\"https://github.com/juliatrustworthyai/CounterfactualExplanations.jl\")","category":"page"},{"location":"#Background-and-Motivation","page":"๐Ÿ  Home","title":"๐Ÿค” Background and Motivation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Machine learning models like Deep Neural Networks have become so complex, opaque and underspecified in the data that they are generally considered Black Boxes. Nonetheless, such models often play a key role in data-driven decision-making systems. This creates the following problem: human operators in charge of such systems have to rely on them blindly, while those individuals subject to them generally have no way of challenging an undesirable outcome:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"โ€œYou cannot appeal to (algorithms). They do not listen. Nor do they bend.โ€โ€” Cathy Oโ€™Neil in Weapons of Math Destruction, 2016","category":"page"},{"location":"#Enter:-Counterfactual-Explanations","page":"๐Ÿ  Home","title":"๐Ÿ”ฎ Enter: Counterfactual Explanations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations can help human stakeholders make sense of the systems they develop, use or endure: they explain how inputs into a system need to change for it to produce different decisions. Explainability benefits internal as well as external quality assurance.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Counterfactual Explanations have a few properties that are desirable in the context of Explainable Artificial Intelligence (XAI). These include:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Full fidelity to the black-box model, since no proxy is involved.\nNo need for (reasonably) interpretable features as opposed to LIME and SHAP.\nClear link to Algorithmic Recourse and Causal Inference.\nLess susceptible to adversarial attacks than LIME and SHAP.","category":"page"},{"location":"#Example:-Give-Me-Some-Credit","page":"๐Ÿ  Home","title":"Example: Give Me Some Credit","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Consider the following real-world scenario: a retail bank is using a black-box model trained on their clientsโ€™ credit history to decide whether they will provide credit to new applicants. To simulate this scenario, we have pre-trained a binary classifier on the publicly available Give Me Some Credit dataset that ships with this package (Kaggle 2011).","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The figure below shows counterfactuals for 10 randomly chosen individuals that would have been denied credit initially.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Example:-MNIST","page":"๐Ÿ  Home","title":"Example: MNIST","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The figure below shows a counterfactual generated for an image classifier trained on MNIST: in particular, it demonstrates which pixels need to change in order for the classifier to predict 3 instead of 8.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Since v0.1.9 counterfactual generators are fully composable. Here we have composed a generator that combines ideas from Wachter, Mittelstadt, and Russell (2017) and Altmeyer et al. (2023):","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Compose generator:\nusing CounterfactualExplanations.Objectives: distance_from_target\ngenerator = GradientBasedGenerator()\n@chain generator begin\n @objective logitcrossentropy + 0.1distance_mad + 0.1distance_from_target\n @with_optimiser Adam(0.1) \nend\ncounterfactual_data.generative_model = vae # assign generative model","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Usage-example","page":"๐Ÿ  Home","title":"๐Ÿ” Usage example","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Generating counterfactuals will typically look like follows. Below we first fit a simple model to a synthetic dataset with linearly separable features and then draw a random sample:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Data and Classifier:\ncounterfactual_data = CounterfactualData(load_linearly_separable()...)\nM = fit_model(counterfactual_data, :Linear)\n\n# Select random sample:\ntarget = 2\nfactual = 1\nchosen = rand(findall(predict_label(M, counterfactual_data) .== factual))\nx = select_factual(counterfactual_data, chosen)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"To this end, we specify a counterfactual generator of our choice:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"# Counterfactual search:\ngenerator = DiCEGenerator(ฮป=[0.1,0.3])","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Here, we have chosen to use the GradientBasedGenerator to move the individual from its factual label 1 to the target label 2.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"With all of our ingredients specified, we finally generate counterfactuals using a simple API call:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"conv = conv = CounterfactualExplanations.Convergence.GeneratorConditionsConvergence()\nce = generate_counterfactual(\n x, target, counterfactual_data, M, generator; \n num_counterfactuals=3, convergence=conv,\n)","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The plot below shows the resulting counterfactual path:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"(Image: )","category":"page"},{"location":"#Implemented-Counterfactual-Generators","page":"๐Ÿ  Home","title":"โ˜‘๏ธ Implemented Counterfactual Generators","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Currently, the following counterfactual generators are implemented:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"ClaPROAR (Altmeyer et al. 2023)\nCLUE (Antorรกn et al. 2020)\nDiCE (Mothilal, Sharma, and Tan 2020)\nFeatureTweak (Tolomei et al. 2017)\nGeneric\nGravitationalGenerator (Altmeyer et al. 2023)\nGreedy (Schut et al. 2021)\nGrowingSpheres (Laugel et al. 2017)\nPROBE (Pawelczyk et al. 2023)\nREVISE (Joshi et al. 2019)\nWachter (Wachter, Mittelstadt, and Russell 2017)","category":"page"},{"location":"#Goals-and-limitations","page":"๐Ÿ  Home","title":"๐ŸŽฏ Goals and limitations","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"The goal of this library is to contribute to efforts towards trustworthy machine learning in Julia. The Julia language has an edge when it comes to trustworthiness: it is very transparent. Packages like this one are generally written in pure Julia, which makes it easy for users and developers to understand and contribute to open-source code. Eventually, this project aims to offer a one-stop-shop of counterfactual explanations.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Our ambition is to enhance the package through the following features:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Support for all supervised machine learning models trained in MLJ.jl.\nSupport for regression models.","category":"page"},{"location":"#Contribute","page":"๐Ÿ  Home","title":"๐Ÿ›  Contribute","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Contributions of any kind are very much welcome! Take a look at the issue to see what things we are currently working on. If you have an idea for a new feature or want to report a bug, please open a new issue.","category":"page"},{"location":"#Development","page":"๐Ÿ  Home","title":"Development","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"If your looking to contribute code, it may be helpful to check out the Explanation section of the docs.","category":"page"},{"location":"#Testing","page":"๐Ÿ  Home","title":"Testing","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Please always make sure to add tests for any new features or changes.","category":"page"},{"location":"#Documentation","page":"๐Ÿ  Home","title":"Documentation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"If you add new features or change existing ones, please make sure to update the documentation accordingly. The documentation is written in Documenter.jl and is located in the docs/src folder.","category":"page"},{"location":"#Log-Changes","page":"๐Ÿ  Home","title":"Log Changes","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"As of version 1.1.1, we have tried to be more stringent about logging changes. Please make sure to add a note to the CHANGELOG.md file for any changes you make. It is sufficient to add a note under the Unreleased section.","category":"page"},{"location":"#General-Pointers","page":"๐Ÿ  Home","title":"General Pointers","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"There are also some general pointers for people looking to contribute to any of our Taija packages here.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Please follow the SciML ColPrac guide.","category":"page"},{"location":"#Citation","page":"๐Ÿ  Home","title":"๐ŸŽ“ Citation","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"If you want to use this codebase, please consider citing the corresponding paper:","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"@article{Altmeyer2023,\n doi = {10.21105/jcon.00130},\n url = {https://doi.org/10.21105/jcon.00130},\n year = {2023},\n publisher = {The Open Journal},\n volume = {1},\n number = {1},\n pages = {130},\n author = {Patrick Altmeyer and Arie van Deursen and Cynthia C. s. Liem},\n title = {Explaining Black-Box Models through Counterfactuals},\n journal = {Proceedings of the JuliaCon Conferences}\n}","category":"page"},{"location":"#References","page":"๐Ÿ  Home","title":"๐Ÿ“š References","text":"","category":"section"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia CS Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In 2023 IEEE Conference on Secure and Trustworthy Machine Learning (SaTML), 418โ€“31. IEEE.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Antorรกn, Javier, Umang Bhatt, Tameem Adel, Adrian Weller, and Josรฉ Miguel Hernรกndez-Lobato. 2020. โ€œGetting a Clue: A Method for Explaining Uncertainty Estimates.โ€ https://arxiv.org/abs/2006.06848.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Kaggle. 2011. โ€œGive Me Some Credit, Improve on the State of the Art in Credit Scoring by Predicting the Probability That Somebody Will Experience Financial Distress in the Next Two Years.โ€ https://www.kaggle.com/c/GiveMeSomeCredit; Kaggle. https://www.kaggle.com/c/GiveMeSomeCredit.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Laugel, Thibault, Marie-Jeanne Lesot, Christophe Marsala, Xavier Renard, and Marcin Detyniecki. 2017. โ€œInverse Classification for Comparison-Based Interpretability in Machine Learning.โ€ https://arxiv.org/abs/1712.08443.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Pawelczyk, Martin, Teresa Datta, Johannes van-den-Heuvel, Gjergji Kasneci, and Himabindu Lakkaraju. 2023. โ€œProbabilistically Robust Recourse: Navigating the Trade-Offs Between Costs and Robustness in Algorithmic Recourse.โ€ https://arxiv.org/abs/2203.06768.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Schut, Lisa, Oscar Key, Rory Mc Grath, Luca Costabello, Bogdan Sacaleanu, Yarin Gal, et al. 2021. โ€œGenerating Interpretable Counterfactual Explanations By Implicit Minimisation of Epistemic and Aleatoric Uncertainties.โ€ In International Conference on Artificial Intelligence and Statistics, 1756โ€“64. PMLR.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Tolomei, Gabriele, Fabrizio Silvestri, Andrew Haines, and Mounia Lalmas. 2017. โ€œInterpretable Predictions of Tree-Based Ensembles via Actionable Feature Tweaking.โ€ In Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 465โ€“74. https://doi.org/10.1145/3097983.3098039.","category":"page"},{"location":"","page":"๐Ÿ  Home","title":"๐Ÿ  Home","text":"Wachter, Sandra, Brent Mittelstadt, and Chris Russell. 2017. โ€œCounterfactual Explanations Without Opening the Black Box: Automated Decisions and the GDPR.โ€ Harv. JL & Tech. 31: 841. https://doi.org/10.2139/ssrn.3063289.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"explanation/categorical/#Categorical-Features","page":"Categorical Features","title":"Categorical Features","text":"","category":"section"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"To illustrate how data is preprocessed under the hood, we consider a simple toy dataset with three categorical features (name, grade and sex) and one continuous feature (age):","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"X = (\n name=categorical([\"Danesh\", \"Lee\", \"Mary\", \"John\"]),\n grade=categorical([\"A\", \"B\", \"A\", \"C\"], ordered=true),\n sex=categorical([\"male\",\"female\",\"male\",\"male\"]),\n height=[1.85, 1.67, 1.5, 1.67],\n)\nschema(X)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Categorical features are expected to be one-hot or dummy encoded. To this end, we could use MLJ, for example:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"hot = OneHotEncoder()\nmach = fit!(machine(hot, X))\nW = transform(mach, X)\nschema(W)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ names โ”‚ scitypes โ”‚ types โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ name__Danesh โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__John โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Lee โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ name__Mary โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__A โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__B โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ grade__C โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__female โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ sex__male โ”‚ Continuous โ”‚ Float64 โ”‚\nโ”‚ height โ”‚ Continuous โ”‚ Float64 โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The matrix that will be perturbed during the counterfactual search looks as follows:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"X = permutedims(MLJBase.matrix(W))","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10ร—4 Matrix{Float64}:\n 1.0 0.0 0.0 0.0\n 0.0 0.0 0.0 1.0\n 0.0 1.0 0.0 0.0\n 0.0 0.0 1.0 0.0\n 1.0 0.0 1.0 0.0\n 0.0 1.0 0.0 0.0\n 0.0 0.0 0.0 1.0\n 0.0 1.0 0.0 0.0\n 1.0 0.0 1.0 1.0\n 1.85 1.67 1.5 1.67","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The CounterfactualData constructor takes two optional arguments that can be used to specify the indices of categorical and continuous features. If nothing is supplied, all features are assumed to be continuous. For categorical features, the constructor expects and array of arrays of integers (Vector{Vector{Int}}) where each subarray includes the indices of a all one-hot encoded rows related to a single categorical feature. In the example above, the name feature is one-hot encoded across rows 1, 2 and 3 of X.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"features_categorical = [\n [1,2,3,4], # name\n [5,6,7], # grade\n [8,9] # sex\n]\nfeatures_continuous = [10]","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"We propose the following simple logic for reconstructing categorical encodings after perturbations:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"For one-hot encoded features with multiple classes, choose the maximum.\nFor binary features, clip the perturbed value to fall into 01 and round to the nearest of the two integers.","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"function reconstruct_cat_encoding(x)\n map(features_categorical) do cat_group_index\n if length(cat_group_index) > 1\n x[cat_group_index] = Int.(x[cat_group_index] .== maximum(x[cat_group_index]))\n if sum(x[cat_group_index]) > 1\n ties = findall(x[cat_group_index] .== 1)\n _x = zeros(length(x[cat_group_index]))\n winner = rand(ties,1)[1]\n _x[winner] = 1\n x[cat_group_index] = _x\n end\n else\n x[cat_group_index] = [round(clamp(x[cat_group_index][1],0,1))]\n end\n end\n return x\nend","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Letโ€™s look at a few simple examples to see how this function works. Firstly, consider the case of perturbing a single element:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x = X[:,1]\nx[1] = 1.1\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.1\n 0.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Next, consider the case of perturbing multiple elements:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x[2] = 1.1\nx[3] = -1.2\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 1.1\n -1.2\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 0.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"Finally, letโ€™s introduce a tie:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"x[1] = 1.0\nx","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 1.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"The reconstructed one-hot-encoded vector will look like this:","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"reconstruct_cat_encoding(x)","category":"page"},{"location":"explanation/categorical/","page":"Categorical Features","title":"Categorical Features","text":"10-element Vector{Float64}:\n 0.0\n 1.0\n 0.0\n 0.0\n 1.0\n 0.0\n 0.0\n 0.0\n 1.0\n 1.85","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"CurrentModule = CounterfactualExplanations ","category":"page"},{"location":"extensions/neurotree/#[NeuroTreeModels.jl](https://evovest.github.io/NeuroTreeModels.jl/dev/)","page":"NeuroTrees","title":"NeuroTreeModels.jl","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"NeuroTreeModels.jl is a package that provides a framework for training differentiable tree-based models. This is relevant to the work on counterfactual explanations (CE), which often assumes that the underlying black-box model is differentiable with respect to its input. The literature on CE therefore regularly focuses exclusively on explaining deep learning models. This is at odds with the fact that the literature also typically focuses on tabular data, which is often best modeled by tree-based models (Grinsztajn, Oyallon, and Varoquaux 2022). The extension for NeuroTreeModels.jl provides a way to bridge this gap by allowing users to apply existing gradient-based CE methods to differentiable tree-based models.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"warning: Experimental Feature\nPlease note that this extension is still experimental. Neither the behaviour of differentiable tree-based models nor their interplay with counterfactual explanations is well understood at this point. If you encounter any issues, please report them to the package maintainers. Your feedback is highly appreciated.Please also note that this extension is only tested on Julia 1.9 and higher, due to compatibility issues.","category":"page"},{"location":"extensions/neurotree/#Example","page":"NeuroTrees","title":"Example","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"The extension will be loaded automatically when loading the NeuroTreeModels package (assuming the CounterfactualExplanations package is also loaded).","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"using NeuroTreeModels","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Next, we will fit a NeuroTree model to the moons dataset using our standard package API for doing so.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"# Fit model to data:\ndata = CounterfactualData(load_moons()...)\nM = fit_model(\n data, :NeuroTree; \n depth=2, lr=5e-2, nrounds=50, batchsize=10\n)","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"NeuroTreeExt.NeuroTreeModel(NeuroTreeRegressor(loss = mlogloss, โ€ฆ), :classification_multi, NeuroTreeModels.NeuroTreeModel{NeuroTreeModels.MLogLoss, Chain{Tuple{BatchNorm{typeof(identity), Vector{Float32}, Float32, Vector{Float32}}, NeuroTreeModels.StackTree}}}(NeuroTreeModels.MLogLoss, Chain(BatchNorm(2, active=false), NeuroTreeModels.StackTree(NeuroTree[NeuroTree{Matrix{Float32}, Vector{Float32}, Array{Float32, 3}}(Float32[1.8824593 -0.28222033; -2.680499 0.67347014; โ€ฆ ; -1.0722864 1.3651229; -2.0926774 1.63557], Float32[-3.4070241, 4.545113, 1.0882677, -0.3497498, -2.766766, 1.9072449, -0.9736261, 3.9750721, 1.726214, 3.7279263 โ€ฆ -0.0664266, -0.4214582, -2.3816268, -3.1371245, 0.76548636, 2.636373, 2.4558601, 0.893434, -1.9484522, 4.793434], Float32[3.44271 -6.334693 -0.6308845 3.385659; -3.4316056 6.297003 0.7254221 -3.3283486;;; -3.7011054 -0.17596768 0.15429471 2.270125; 3.4926674 0.026218029 -0.19753197 -2.2337704;;; 1.1795454 -4.315231 0.28486454 1.9995956; -0.9651108 4.0999455 -0.05312265 -1.8039354;;; โ€ฆ ;;; 2.5076811 -0.46358463 -3.5438805 0.0686823; -2.592356 0.47884527 3.781507 -0.022692114;;; -0.59115165 -3.234046 0.09896194 2.375202; 0.5592871 3.3082843 -0.014032216 -2.1876256;;; 2.039389 -0.10134532 2.6637273 -4.999703; -2.0289893 0.3368772 -2.5739825 5.069934], tanh)])), Dict{Symbol, Any}(:feature_names => [:x1, :x2], :nrounds => 50, :device => :cpu)))","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Finally, we select a factual instance and generate a counterfactual explanation for it using the generic gradient-based CE method.","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"# Select a factual instance:\ntarget = 1\nfactual = 0\nchosen = rand(findall(predict_label(M, data) .== factual))\nx = select_factual(data, chosen)\n\n# Generate counterfactual explanation:\nฮท = 0.01\ngenerator = GenericGenerator(; opt=Descent(ฮท), ฮป=0.01)\nconv = CounterfactualExplanations.Convergence.DecisionThresholdConvergence(;\n decision_threshold=0.9, max_iter=100\n)\nce = generate_counterfactual(x, target, data, M, generator; convergence=conv)\nplot(ce, alpha=0.1)","category":"page"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"(Image: )","category":"page"},{"location":"extensions/neurotree/#References","page":"NeuroTrees","title":"References","text":"","category":"section"},{"location":"extensions/neurotree/","page":"NeuroTrees","title":"NeuroTrees","text":"Grinsztajn, Lรฉo, Edouard Oyallon, and Gaรซl Varoquaux. 2022. โ€œWhy Do Tree-Based Models Still Outperform Deep Learning on Tabular Data?โ€ https://arxiv.org/abs/2207.08815.","category":"page"}] } diff --git a/dev/tutorials/benchmarking/index.html b/dev/tutorials/benchmarking/index.html index 2bdc8a843..b9cf4b8b7 100644 --- a/dev/tutorials/benchmarking/index.html +++ b/dev/tutorials/benchmarking/index.html @@ -1,5 +1,5 @@ -Benchmarking Explanations ยท CounterfactualExplanations.jl

Performance Benchmarks

In the previous tutorial, we have seen how counterfactual explanations can be evaluated. An important follow-up task is to compare the performance of different counterfactual generators is an important task. Researchers can use benchmarks to test new ideas they want to implement. Practitioners can find the right counterfactual generator for their specific use case through benchmarks. In this tutorial, we will see how to run benchmarks for counterfactual generators.

Post Hoc Benchmarking

We begin by continuing the discussion from the previous tutorial: suppose you have generated multiple counterfactual explanations for multiple individuals, like below:

# Factual and target:
+Benchmarking Explanations ยท CounterfactualExplanations.jl

Performance Benchmarks

In the previous tutorial, we have seen how counterfactual explanations can be evaluated. An important follow-up task is to compare the performance of different counterfactual generators is an important task. Researchers can use benchmarks to test new ideas they want to implement. Practitioners can find the right counterfactual generator for their specific use case through benchmarks. In this tutorial, we will see how to run benchmarks for counterfactual generators.

Post Hoc Benchmarking

We begin by continuing the discussion from the previous tutorial: suppose you have generated multiple counterfactual explanations for multiple individuals, like below:

# Factual and target:
 n_individuals = 5
 ids = rand(findall(predict_label(M, counterfactual_data) .== factual), n_individuals)
 xs = select_factual(counterfactual_data, ids)
@@ -180,4 +180,4 @@
    1 โ”‚ moons    Generic    1.56555
    2 โ”‚ moons    Greedy     0.819269
    3 โ”‚ circles  Generic    1.83524
-   4 โ”‚ circles  Greedy     0.498953
+ 4 โ”‚ circles Greedy 0.498953
diff --git a/dev/tutorials/convergence.qmd b/dev/tutorials/convergence.qmd new file mode 100644 index 000000000..4e969babe --- /dev/null +++ b/dev/tutorials/convergence.qmd @@ -0,0 +1,68 @@ +--- +execute: + output: true +--- + +``` @meta +CurrentModule = CounterfactualExplanations +``` + +```{julia} +#| echo: false +#| output: false + +include("$(pwd())/docs/setup_docs.jl") +eval(setup_docs) +``` + +``# [Convergence](@id convergence)``{=commonmark} + +The search for counterfactuals can be seen as an optimization problem, where the goal is to find a point in the input space. One questions that has received surprisingly little attention is how to determine when the search has converged. In a recent paper, we have briefly discussed why it is important to consider convergence [@altmeyer2024faithful]: + +> One intuitive way to specify convergence is in terms of threshold probabilities: once the predicted probability $p(y^{+}|x^{\prime})$ exceeds some user-defined threshold ฮณ such that the counterfactual is valid, we could consider the search to have converged. In the binary case, for example, convergence could be defined as $p(y^{+}|x^{\prime}) > 0.5$ in this sense. Note, however, how this can be expected to yield counterfactuals in the proximity of the decision boundary, a region characterized by high aleatoric uncertainty. In other words, counterfactuals generated in this way would generally not be plausible. To avoid this from happening, we specify convergence in terms of gradients approaching zero for all our experiments and all of our generators. This is allows us to get a cleaner read on how the different counterfactual search objectives affect counterfactual outcomes. + +In the paper, we were primarily interested in benchmarking counterfactuals generated by different search objectives. In other contexts, however, it may be more appropriate to specify convergence in terms of threshold probabilities. Our package allows you to specify convergence in terms of gradients, threshold probabilities or simply in terms of the total number of iterations. In this section, we will show you how to do this. + +```{julia} +using CounterfactualExplanations.Convergence +generator = GenericGenerator(ฮป=0.01) +``` + +## Convergence in terms of gradients + +As gradients approach zero, the conditions defined by the search objective and hence the generator are satisfied. We therefore refere to this type of convergece criterium as [`GeneratorConditionsConvergence`](@ref) + +```{julia} +conv = GeneratorConditionsConvergence(gradient_tol=0.01, max_iter=1000) +ce_gen = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv) +``` + +## Convergence in terms of threshold probabilities + +In this case, the search is considered to have converged once the predicted probability $p(y^{+}|x^{\prime})$ exceeds some user-defined threshold `ฮณ` such that the counterfactual is valid. We refer to this type of convergence criterium as [`DecisionThresholdConvergence`](@ref). + +```{julia} +conv = DecisionThresholdConvergence(decision_threshold=0.75) +ce_dec = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv) +``` + +## Convergence in terms of the total number of iterations + +In this case, the search is considered to have converged once the total number of iterations exceeds some user-defined threshold `max_iter`. We refer to this type of convergence criterium as [`MaxIterConvergence`](@ref). + +```{julia} +conv = MaxIterConvergence(max_iter=25) +ce_max = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv) +``` + +## Comparison + +```{julia} +plts = [] +for (ce, titl) in zip([ce_gen, ce_dec, ce_max], ["Gradient Convergence", "Decision Threshold Convergence", "Max Iterations Convergence"]) + push!(plts, plot(ce; title=titl, cbar=false)) +end +plot(plts..., layout=(1,3), size=(1200, 380)) +``` + +## References \ No newline at end of file diff --git a/dev/tutorials/convergence/index.html b/dev/tutorials/convergence/index.html new file mode 100644 index 000000000..87e6722b1 --- /dev/null +++ b/dev/tutorials/convergence/index.html @@ -0,0 +1,13 @@ + +Convergence ยท CounterfactualExplanations.jl

Convergence

The search for counterfactuals can be seen as an optimization problem, where the goal is to find a point in the input space. One questions that has received surprisingly little attention is how to determine when the search has converged. In a recent paper, we have briefly discussed why it is important to consider convergence (Altmeyer et al. 2024):

One intuitive way to specify convergence is in terms of threshold probabilities: once the predicted probability $p(y^{+}|x^{\prime})$ exceeds some user-defined threshold ฮณ such that the counterfactual is valid, we could consider the search to have converged. In the binary case, for example, convergence could be defined as $p(y^{+}|x^{\prime}) > 0.5$ in this sense. Note, however, how this can be expected to yield counterfactuals in the proximity of the decision boundary, a region characterized by high aleatoric uncertainty. In other words, counterfactuals generated in this way would generally not be plausible. To avoid this from happening, we specify convergence in terms of gradients approaching zero for all our experiments and all of our generators. This is allows us to get a cleaner read on how the different counterfactual search objectives affect counterfactual outcomes.

In the paper, we were primarily interested in benchmarking counterfactuals generated by different search objectives. In other contexts, however, it may be more appropriate to specify convergence in terms of threshold probabilities. Our package allows you to specify convergence in terms of gradients, threshold probabilities or simply in terms of the total number of iterations. In this section, we will show you how to do this.

using CounterfactualExplanations.Convergence
+generator = GenericGenerator(ฮป=0.01)
GradientBasedGenerator(nothing, CounterfactualExplanations.Objectives.distance_l1, 0.01, false, false, Descent(0.1), NamedTuple())

Convergence in terms of gradients

As gradients approach zero, the conditions defined by the search objective and hence the generator are satisfied. We therefore refere to this type of convergece criterium as GeneratorConditionsConvergence

conv = GeneratorConditionsConvergence(gradient_tol=0.01, max_iter=1000)
+ce_gen = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)
CounterfactualExplanation
+Convergence: โœ… after 179 steps.

Convergence in terms of threshold probabilities

In this case, the search is considered to have converged once the predicted probability $p(y^{+}|x^{\prime})$ exceeds some user-defined threshold ฮณ such that the counterfactual is valid. We refer to this type of convergence criterium as DecisionThresholdConvergence.

conv = DecisionThresholdConvergence(decision_threshold=0.75)
+ce_dec = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)
CounterfactualExplanation
+Convergence: โœ… after 9 steps.

Convergence in terms of the total number of iterations

In this case, the search is considered to have converged once the total number of iterations exceeds some user-defined threshold max_iter. We refer to this type of convergence criterium as MaxIterConvergence.

conv = MaxIterConvergence(max_iter=25)
+ce_max = generate_counterfactual(x, target, counterfactual_data, M, generator; convergence = conv)
CounterfactualExplanation
+Convergence: โœ… after 25 steps.

Comparison

plts = []
+for (ce, titl) in zip([ce_gen, ce_dec, ce_max], ["Gradient Convergence", "Decision Threshold Convergence", "Max Iterations Convergence"])
+    push!(plts, plot(ce; title=titl, cbar=false))
+end
+plot(plts..., layout=(1,3), size=(1200, 380))

References

Altmeyer, Patrick, Mojtaba Farmanbar, Arie van Deursen, and Cynthia CS Liem. 2024. โ€œFaithful Model Explanations Through Energy-Constrained Conformal Counterfactuals.โ€ In Proceedings of the AAAI Conference on Artificial Intelligence, 38:10829โ€“37. 10.

diff --git a/dev/tutorials/convergence_files/figure-commonmark/cell-7-output-1.svg b/dev/tutorials/convergence_files/figure-commonmark/cell-7-output-1.svg new file mode 100644 index 000000000..79d235e62 --- /dev/null +++ b/dev/tutorials/convergence_files/figure-commonmark/cell-7-output-1.svg @@ -0,0 +1,3416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tutorials/data_catalogue/index.html b/dev/tutorials/data_catalogue/index.html index fef0b361f..86657a5c8 100644 --- a/dev/tutorials/data_catalogue/index.html +++ b/dev/tutorials/data_catalogue/index.html @@ -1,5 +1,5 @@ -Data Catalogue ยท CounterfactualExplanations.jl

Data Catalogue

To allow researchers and practitioners to test and compare counterfactual generators, the TAIJA environment includes the package TaijaData.jl which comes with pre-processed synthetic and real-world benchmark datasets from different domains. This page explains how to use TaijaData.jl in tandem with CounterfactualExplanations.jl.

Synthetic Data

The following dictionary can be used to inspect the available methods to generate synthetic datasets where the key indicates the name of the data and the value is the corresponding method:

TaijaData.data_catalogue[:synthetic]
Dict{Symbol, Function} with 6 entries:
+Data Catalogue ยท CounterfactualExplanations.jl

Data Catalogue

To allow researchers and practitioners to test and compare counterfactual generators, the TAIJA environment includes the package TaijaData.jl which comes with pre-processed synthetic and real-world benchmark datasets from different domains. This page explains how to use TaijaData.jl in tandem with CounterfactualExplanations.jl.

Synthetic Data

The following dictionary can be used to inspect the available methods to generate synthetic datasets where the key indicates the name of the data and the value is the corresponding method:

TaijaData.data_catalogue[:synthetic]
Dict{Symbol, Function} with 6 entries:
   :overlapping        => load_overlapping
   :linearly_separable => load_linearly_separable
   :blobs              => load_blobs
@@ -38,4 +38,4 @@
  10
  10
  10

We can also use a helper function to split the data into train and test sets:

train_data, test_data = 
-    CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)
+ CounterfactualExplanations.DataPreprocessing.train_test_split(counterfactual_data)
diff --git a/dev/tutorials/data_preprocessing/index.html b/dev/tutorials/data_preprocessing/index.html index 1dd509c81..0a55394fb 100644 --- a/dev/tutorials/data_preprocessing/index.html +++ b/dev/tutorials/data_preprocessing/index.html @@ -1,5 +1,5 @@ -Handling Data ยท CounterfactualExplanations.jl

Handling Data

The package works with custom data containers that contain the input and output data as well as information about the type and mutability of features. In this tutorial, we will see how data can be prepared for use with the package.

Basic Functionality

To demonstrate the basic way to prepare data, letโ€™s look at a standard benchmark dataset: Fisherโ€™s classic iris dataset. We can use MLDatasets to load this data.

dataset = Iris()

Our data constructor CounterfactualData needs at least two inputs: features X and targets y.

X = dataset.features
+Handling Data ยท CounterfactualExplanations.jl

Handling Data

The package works with custom data containers that contain the input and output data as well as information about the type and mutability of features. In this tutorial, we will see how data can be prepared for use with the package.

Basic Functionality

To demonstrate the basic way to prepare data, letโ€™s look at a standard benchmark dataset: Fisherโ€™s classic iris dataset. We can use MLDatasets to load this data.

dataset = Iris()

Our data constructor CounterfactualData needs at least two inputs: features X and targets y.

X = dataset.features
 y = dataset.targets

Next, we convert the input data to a Tables.MatrixTable (following MLJ.jl) convention. Concerning the target variable, we just assign grab the first column of the data frame.

X = table(Tables.matrix(X))
 y = y[:,1]

Now we can feed these two ingredients to our constructor:

counterfactual_data = CounterfactualData(X, y)

Under the hood, the constructor performs basic preprocessing steps. For example, the output variable y is automatically one-hot encoded:

counterfactual_data.y
3ร—150 Matrix{Bool}:
  1  1  1  1  1  1  1  1  1  1  1  1  1  โ€ฆ  0  0  0  0  0  0  0  0  0  0  0  0
@@ -90,4 +90,4 @@
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)

The resulting counterfactual path is shown in the chart below. Since only the first feature can be perturbed, the sample can only move along the horizontal axis.

plot(ce)

<!โ€“ ## Domain constraints &#10;In some cases, we may also want to constrain the domain of some feature. For example, age as a feature is constrained to a range from 0 to some upper bound corresponding perhaps to the average life expectancy of humans. Below, for example, we impose an upper bound of $0.5$ for our two features. &#10;```{.julia} counterfactualdata.mutability = [:both, :both] counterfactualdata.domain = [(0,0) for var in counterfactualdata.featurescontinuous]

&#10;This results in the counterfactual path shown below: since features are not allowed to be perturbed beyond the upper bound, the resulting counterfactual falls just short of the threshold probability $\gamma$.
 &#10;```{.julia}
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

โ€“>

+plot(ce)

โ€“>

diff --git a/dev/tutorials/evaluation/index.html b/dev/tutorials/evaluation/index.html index c22725b49..383c9b559 100644 --- a/dev/tutorials/evaluation/index.html +++ b/dev/tutorials/evaluation/index.html @@ -1,5 +1,5 @@ -Evaluating Explanations ยท CounterfactualExplanations.jl

Performance Evaluation

Now that we know how to generate counterfactual explanations in Julia, you may have a few follow-up questions: How do I know if the counterfactual search has been successful? How good is my counterfactual explanation? What does โ€˜goodโ€™ even mean in this context? In this tutorial, we will see how counterfactual explanations can be evaluated with respect to their performance.

Default Measures

Numerous evaluation measures for counterfactual explanations have been proposed. In what follows, we will cover some of the most important measures.

Single Measure, Single Counterfactual

One of the most important measures is validity, which simply determines whether or not a counterfactual explanation $x^{\prime}$ is valid in the sense that it yields the target prediction: $M(x^{\prime})=t$. We can evaluate the validity of a single counterfactual explanation ce using the Evaluation.evaluate function as follows:

using CounterfactualExplanations.Evaluation: evaluate, validity
+Evaluating Explanations ยท CounterfactualExplanations.jl

Performance Evaluation

Now that we know how to generate counterfactual explanations in Julia, you may have a few follow-up questions: How do I know if the counterfactual search has been successful? How good is my counterfactual explanation? What does โ€˜goodโ€™ even mean in this context? In this tutorial, we will see how counterfactual explanations can be evaluated with respect to their performance.

Default Measures

Numerous evaluation measures for counterfactual explanations have been proposed. In what follows, we will cover some of the most important measures.

Single Measure, Single Counterfactual

One of the most important measures is validity, which simply determines whether or not a counterfactual explanation $x^{\prime}$ is valid in the sense that it yields the target prediction: $M(x^{\prime})=t$. We can evaluate the validity of a single counterfactual explanation ce using the Evaluation.evaluate function as follows:

using CounterfactualExplanations.Evaluation: evaluate, validity
 evaluate(ce; measure=validity)
1-element Vector{Vector{Float64}}:
  [1.0]

For a single counterfactual explanation, this evaluation measure can only take two values: it is either equal to 1, if the explanation is valid or 0 otherwise. Another important measure is distance, which relates to the distance between the factual $x$ and the counterfactual $x^{\prime}$. In the context of Algorithmic Recourse, higher distances are typically associated with higher costs to individuals seeking recourse.

using CounterfactualExplanations.Objectives: distance
 evaluate(ce; measure=distance)
1-element Vector{Vector{Float32}}:
@@ -42,4 +42,4 @@
  [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]]
  [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]
 
-Vector{Vector}[[[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]]

This leads us to our next topic: Performance Benchmarks.

+Vector{Vector}[[[1.0], Float32[3.351181], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.6405892], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[2.935012], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.5348382], [[0.0, 0.0, 0.0, 0.0, 0.0]]], [[1.0], Float32[3.9373996], [[0.0, 0.0, 0.0, 0.0, 0.0]]]]

This leads us to our next topic: Performance Benchmarks.

diff --git a/dev/tutorials/generators/index.html b/dev/tutorials/generators/index.html index c0415d538..e8f0d860d 100644 --- a/dev/tutorials/generators/index.html +++ b/dev/tutorials/generators/index.html @@ -1,5 +1,5 @@ -Handling Generators ยท CounterfactualExplanations.jl

Handling Generators

Generating Counterfactual Explanations can be seen as a generative modelling task because it involves generating samples in the input space: $x \sim \mathcal{X}$. In this tutorial, we will introduce how Counterfactual GradientBasedGenerators are used. They are discussed in more detail in the explanatory section of the documentation.

Composable Generators

Breaking Changes Expected

Work on this feature is still in its very early stages and breaking changes should be expected.

One of the key objectives for this package is Composability. It turns out that many of the various counterfactual generators that have been proposed in the literature, essentially do the same thing: they optimize an objective function. Formally we have,

\[ +Handling Generators ยท CounterfactualExplanations.jl

Handling Generators

Generating Counterfactual Explanations can be seen as a generative modelling task because it involves generating samples in the input space: $x \sim \mathcal{X}$. In this tutorial, we will introduce how Counterfactual GradientBasedGenerators are used. They are discussed in more detail in the explanatory section of the documentation.

Composable Generators

Breaking Changes Expected

Work on this feature is still in its very early stages and breaking changes should be expected.

One of the key objectives for this package is Composability. It turns out that many of the various counterfactual generators that have been proposed in the literature, essentially do the same thing: they optimize an objective function. Formally we have,

\[ \begin{aligned} \mathbf{s}^\prime &= \arg \min_{\mathbf{s}^\prime \in \mathcal{S}} \left\{ {\text{yloss}(M(f(\mathbf{s}^\prime)),y^*)}+ \lambda {\text{cost}(f(\mathbf{s}^\prime)) } \right\} \end{aligned} @@ -22,4 +22,4 @@ :greedy => GreedyGenerator

To specify the type of generator you want to use, you can simply instantiate it:

# Search:
 generator = GenericGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

We generally make an effort to follow the literature as closely as possible when implementing off-the-shelf generators.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

+plot(ce)

We generally make an effort to follow the literature as closely as possible when implementing off-the-shelf generators.

References

Altmeyer, Patrick, Giovan Angela, Aleksander Buszydlik, Karol Dobiczek, Arie van Deursen, and Cynthia Liem. 2023. โ€œEndogenous Macrodynamics in Algorithmic Recourse.โ€ In First IEEE Conference on Secure and Trustworthy Machine Learning. https://doi.org/10.1109/satml54575.2023.00036.

Joshi, Shalmali, Oluwasanmi Koyejo, Warut Vijitbenjaronk, Been Kim, and Joydeep Ghosh. 2019. โ€œTowards Realistic Individual Recourse and Actionable Explanations in Black-Box Decision Making Systems.โ€ https://arxiv.org/abs/1907.09615.

Mothilal, Ramaravind K, Amit Sharma, and Chenhao Tan. 2020. โ€œExplaining Machine Learning Classifiers Through Diverse Counterfactual Explanations.โ€ In Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency, 607โ€“17. https://doi.org/10.1145/3351095.3372850.

diff --git a/dev/tutorials/index.html b/dev/tutorials/index.html index 2c5483307..231dfc31e 100644 --- a/dev/tutorials/index.html +++ b/dev/tutorials/index.html @@ -1,2 +1,2 @@ -Overview ยท CounterfactualExplanations.jl

Tutorials

In this section, you will find a series of tutorials that should help you gain a basic understanding of Conformal Prediction and how to apply it in Julia using this package.

Tutorials are lessons that take the reader by the hand through a series of steps to complete a project of some kind. Tutorials are learning-oriented.

โ€” Diรกtaxis

In other words, you come here because you are new to this topic and are looking for a first peek at the methodology and code ๐Ÿซฃ.

+Overview ยท CounterfactualExplanations.jl

Tutorials

In this section, you will find a series of tutorials that should help you gain a basic understanding of Conformal Prediction and how to apply it in Julia using this package.

Tutorials are lessons that take the reader by the hand through a series of steps to complete a project of some kind. Tutorials are learning-oriented.

โ€” Diรกtaxis

In other words, you come here because you are new to this topic and are looking for a first peek at the methodology and code ๐Ÿซฃ.

diff --git a/dev/tutorials/model_catalogue/index.html b/dev/tutorials/model_catalogue/index.html index 2407dd6e9..fcded727b 100644 --- a/dev/tutorials/model_catalogue/index.html +++ b/dev/tutorials/model_catalogue/index.html @@ -1,5 +1,5 @@ -Model Catalogue ยท CounterfactualExplanations.jl

Model Catalogue

While in general it is assumed that users will use this package to explain their pre-trained models, we provide out-of-the-box functionality to train various simple default models. In this tutorial, we will see how these models can be fitted to CounterfactualData.

Available Models

The standard_models_catalogue can be used to inspect the available default models:

standard_models_catalogue
Dict{Symbol, Any} with 4 entries:
+Model Catalogue ยท CounterfactualExplanations.jl

Model Catalogue

While in general it is assumed that users will use this package to explain their pre-trained models, we provide out-of-the-box functionality to train various simple default models. In this tutorial, we will see how these models can be fitted to CounterfactualData.

Available Models

The standard_models_catalogue can be used to inspect the available default models:

standard_models_catalogue
Dict{Symbol, Any} with 4 entries:
   :Linear       => Linear
   :LaplaceRedux => LaplaceReduxModel
   :DeepEnsemble => FluxEnsemble
@@ -44,4 +44,4 @@
  - alpha: 0.5
  - tree_type: binary
  - rng: MersenneTwister(123, (0, 9018, 8016, 884))
-, โ€ฆ), :classification_multi)

The tunable parameters for the EvoTreeModel can be found from the documentation of the EvoTrees.jl package under the EvoTreeClassifier section.

Please note that support for counterfactual generation with both LaplaceReduxModel and EvoTreeModel is not yet fully implemented.

References

Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.

LeCun, Yann. 1998. โ€œThe MNIST Database of Handwritten Digits.โ€

+, โ€ฆ), :classification_multi)

The tunable parameters for the EvoTreeModel can be found from the documentation of the EvoTrees.jl package under the EvoTreeClassifier section.

Please note that support for counterfactual generation with both LaplaceReduxModel and EvoTreeModel is not yet fully implemented.

References

Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.

LeCun, Yann. 1998. โ€œThe MNIST Database of Handwritten Digits.โ€

diff --git a/dev/tutorials/models/index.html b/dev/tutorials/models/index.html index 11b20c28f..7baaadaaf 100644 --- a/dev/tutorials/models/index.html +++ b/dev/tutorials/models/index.html @@ -1,5 +1,5 @@ -Handling Models ยท CounterfactualExplanations.jl

Handling Models

The typical use-case for Counterfactual Explanations and Algorithmic Recourse is as follows: users have trained some supervised model that is not inherently interpretable and are looking for a way to explain it. In this tutorial, we will see how pre-trained models can be used with this package.

Models trained in Flux.jl

We will train a simple binary classifier in Flux.jl on the popular Moons dataset:

n = 500
+Handling Models ยท CounterfactualExplanations.jl

Handling Models

The typical use-case for Counterfactual Explanations and Algorithmic Recourse is as follows: users have trained some supervised model that is not inherently interpretable and are looking for a way to explain it. In this tutorial, we will see how pre-trained models can be used with this package.

Models trained in Flux.jl

We will train a simple binary classifier in Flux.jl on the popular Moons dataset:

n = 500
 data = TaijaData.load_moons(n)
 counterfactual_data = DataPreprocessing.CounterfactualData(data...)
 X = counterfactual_data.X
@@ -41,4 +41,4 @@
 Epoch 80
 avg_loss(data) = 0.011847609f0
 Epoch 100
-avg_loss(data) = 0.007242911f0

To prepare the fitted model for use with our package, we need to wrap it inside a container. For plain-vanilla models trained in Flux.jl, the corresponding constructor is called FluxModel. There is also a separate constructor called FluxEnsemble, which applies to Deep Ensembles. Deep Ensembles are a popular approach to approximate Bayesian Deep Learning and have been shown to generate good predictive uncertainty estimates (Lakshminarayanan, Pritzel, and Blundell 2016).

The appropriate API call to wrap our simple network in a container follows below:

M = FluxModel(nn)
FluxModel(Chain(Dense(2 => 32, relu), Dropout(0.1, active=false), Dense(32 => 2)), :classification_binary)

The likelihood function of the output variable is automatically inferred from the data. The generic plot() method can be called on the model and data to visualise the results:

plot(M, counterfactual_data)

Our model M is now ready for use with the package.

References

Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.

+avg_loss(data) = 0.007242911f0

To prepare the fitted model for use with our package, we need to wrap it inside a container. For plain-vanilla models trained in Flux.jl, the corresponding constructor is called FluxModel. There is also a separate constructor called FluxEnsemble, which applies to Deep Ensembles. Deep Ensembles are a popular approach to approximate Bayesian Deep Learning and have been shown to generate good predictive uncertainty estimates (Lakshminarayanan, Pritzel, and Blundell 2016).

The appropriate API call to wrap our simple network in a container follows below:

M = FluxModel(nn)
FluxModel(Chain(Dense(2 => 32, relu), Dropout(0.1, active=false), Dense(32 => 2)), :classification_binary)

The likelihood function of the output variable is automatically inferred from the data. The generic plot() method can be called on the model and data to visualise the results:

plot(M, counterfactual_data)

Our model M is now ready for use with the package.

References

Lakshminarayanan, Balaji, Alexander Pritzel, and Charles Blundell. 2016. โ€œSimple and Scalable Predictive Uncertainty Estimation Using Deep Ensembles.โ€ https://arxiv.org/abs/1612.01474.

diff --git a/dev/tutorials/parallelization/index.html b/dev/tutorials/parallelization/index.html index c256e231e..f57c500b1 100644 --- a/dev/tutorials/parallelization/index.html +++ b/dev/tutorials/parallelization/index.html @@ -1,5 +1,5 @@ -Parallelization ยท CounterfactualExplanations.jl

Parallelization

Version 0.1.15 adds support for parallelization through multi-processing. Currently, the only available backend for parallelization is MPI.jl.

Available functions

Parallelization is only available for certain functions. To check if a function is parallelizable, you can use parallelizable function:

using CounterfactualExplanations.Evaluation: evaluate, benchmark
+Parallelization ยท CounterfactualExplanations.jl

Parallelization

Version 0.1.15 adds support for parallelization through multi-processing. Currently, the only available backend for parallelization is MPI.jl.

Available functions

Parallelization is only available for certain functions. To check if a function is parallelizable, you can use parallelizable function:

using CounterfactualExplanations.Evaluation: evaluate, benchmark
 println(parallelizable(generate_counterfactual))
 println(parallelizable(evaluate))
 println(parallelizable(predict_label))
true
@@ -219,4 +219,4 @@
 
 bmk = benchmark(counterfactual_data; parallelizer=parallelizer)
 
-MPI.Finalize()

The file can be executed from the command line as follows:

mpiexecjl --project -n 4 julia -e 'include("docs/src/srcipts/mpi.jl")'
+MPI.Finalize()

The file can be executed from the command line as follows:

mpiexecjl --project -n 4 julia -e 'include("docs/src/srcipts/mpi.jl")'
diff --git a/dev/tutorials/simple_example/index.html b/dev/tutorials/simple_example/index.html index 09769d4b5..76fb57937 100644 --- a/dev/tutorials/simple_example/index.html +++ b/dev/tutorials/simple_example/index.html @@ -1,5 +1,5 @@ -Simple Example ยท CounterfactualExplanations.jl

Simple Example

In this tutorial, we will go through a simple example involving synthetic data and a generic counterfactual generator.

Data and Classifier

Below we generate some linearly separable data and fit a simple MLP classifier with batch normalization to it. For more information on generating data and models, refer to the Handling Data and Handling Models tutorials respectively.

# Counteractual data and model:
+Simple Example ยท CounterfactualExplanations.jl

Simple Example

In this tutorial, we will go through a simple example involving synthetic data and a generic counterfactual generator.

Data and Classifier

Below we generate some linearly separable data and fit a simple MLP classifier with batch normalization to it. For more information on generating data and models, refer to the Handling Data and Handling Models tutorials respectively.

# Counteractual data and model:
 flux_training_params.batchsize = 10
 data = TaijaData.load_linearly_separable()
 counterfactual_data = DataPreprocessing.CounterfactualData(data...)
@@ -10,4 +10,4 @@
 x = select_factual(counterfactual_data, chosen)

Finally, we generate and visualize the generated counterfactual:

# Search:
 generator = WachterGenerator()
 ce = generate_counterfactual(x, target, counterfactual_data, M, generator)
-plot(ce)

+plot(ce)

diff --git a/dev/tutorials/whistle_stop/index.html b/dev/tutorials/whistle_stop/index.html index 75c8b3832..d654cbf8b 100644 --- a/dev/tutorials/whistle_stop/index.html +++ b/dev/tutorials/whistle_stop/index.html @@ -1,5 +1,5 @@ -Whiste-Stop Tour ยท CounterfactualExplanations.jl

Whistle-Stop Tour

In this tutorial, we will go through a slightly more complex example involving synthetic data. We will generate Counterfactual Explanations using different generators and visualize the results.

Data and Classifier

# Choose some values for data and a model:
+Whiste-Stop Tour ยท CounterfactualExplanations.jl

Whistle-Stop Tour

In this tutorial, we will go through a slightly more complex example involving synthetic data. We will generate Counterfactual Explanations using different generators and visualize the results.

Data and Classifier

# Choose some values for data and a model:
 n_dim = 2
 n_classes = 4
 n_samples = 400
@@ -26,4 +26,4 @@
     )
     ces[key] = ce
     plts = [plts..., plot(ce; title=key, colorbar=false)]
-end

+end

diff --git a/dev/www/pkg_architecture.mmd b/dev/www/pkg_architecture.mmd deleted file mode 100644 index 9ad58d9b2..000000000 --- a/dev/www/pkg_architecture.mmd +++ /dev/null @@ -1,37 +0,0 @@ -flowchart TB - - classDef module fill:#cb3c33,stroke:#333,color:#fff,stroke-width:4px; - classDef struct fill:#389826,stroke:#333,color:#fff; - classDef funct fill:#9558b2,stroke:#333,color:#fff; - %% Components - data(["Data"]) - generative(["GenerativeModels"]) - vae["VAE <: AbstractGenerativeModel"] - data_pre(["DataPreprocessing"]) - c_data["CounterfactualData"] - models(["Models"]) - model["FluxModel <: AbstractFittedModel"] - generators(["Generators"]) - generator["GenericGenerator <: AbstractGenerator"] - interop(["Interoperability"]) - generate_counterfactual{{"generate_counterfactual"}} - ce["CounterfactualExplanation"] - - class vae,c_data,model,generator,ce struct; - class data,generative,data_pre,models,generators,interop module; - class generate_counterfactual funct; - - %% Graph - data ===> data_pre - data_pre ===o c_data - c_data ---> generative - generative ---o vae - vae ---> c_data - - models ===o model - - generators ===o generator - - c_data & model & generator ===> generate_counterfactual - - generate_counterfactual ===o ce \ No newline at end of file diff --git a/dev/www/pkg_architecture.png b/dev/www/pkg_architecture.png deleted file mode 100644 index 57a96951945a989c395e0d57d96c1d5ecb442543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159381 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