shap_rs/explainers/
auto.rs1use super::{ExactExplainer, KernelExplainer};
2use crate::{
3 Background, EvaluationConfig, Explainer, Explanation, IndependentMasker, Link, Masker, Predict,
4 Result,
5};
6use ndarray::ArrayView2;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10pub enum AutoAlgorithm {
11 Exact,
12 Kernel,
13}
14
15pub struct AutoExplainer<M, K = IndependentMasker> {
21 model: M,
22 masker: K,
23 exact_threshold: usize,
24 kernel_samples: usize,
25 seed: u64,
26 ridge: f64,
27 link: Link,
28 evaluation: EvaluationConfig,
29}
30
31impl<M> AutoExplainer<M, IndependentMasker> {
32 pub fn new(model: M, background: Background) -> Self {
33 Self::from_masker(model, IndependentMasker::new(background))
34 }
35}
36
37impl<M, K> AutoExplainer<M, K> {
38 pub fn from_masker(model: M, masker: K) -> Self {
39 Self {
40 model,
41 masker,
42 exact_threshold: 12,
43 kernel_samples: 512,
44 seed: 0,
45 ridge: 1e-10,
46 link: Link::Identity,
47 evaluation: EvaluationConfig::default(),
48 }
49 }
50 pub fn with_exact_threshold(mut self, features: usize) -> Self {
51 self.exact_threshold = features;
52 self
53 }
54 pub fn with_kernel_samples(mut self, samples: usize) -> Self {
55 self.kernel_samples = samples;
56 self
57 }
58 pub fn with_seed(mut self, seed: u64) -> Self {
59 self.seed = seed;
60 self
61 }
62 pub fn with_ridge(mut self, ridge: f64) -> Self {
63 self.ridge = ridge;
64 self
65 }
66 pub fn with_link(mut self, link: Link) -> Self {
67 self.link = link;
68 self
69 }
70 pub fn with_evaluation_config(mut self, evaluation: EvaluationConfig) -> Self {
71 self.evaluation = evaluation;
72 self
73 }
74}
75
76impl<M, K: Masker> AutoExplainer<M, K> {
77 pub fn selected_algorithm(&self) -> AutoAlgorithm {
78 if self.link == Link::Identity && self.masker.n_features() <= self.exact_threshold {
79 AutoAlgorithm::Exact
80 } else {
81 AutoAlgorithm::Kernel
82 }
83 }
84}
85
86impl<M: Predict, K: Masker> Explainer for AutoExplainer<M, K> {
87 fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
88 match self.selected_algorithm() {
89 AutoAlgorithm::Exact => ExactExplainer::from_masker(&self.model, &self.masker)
90 .with_max_features(self.exact_threshold)
91 .with_link(self.link)
92 .with_evaluation_config(self.evaluation)
93 .explain(x),
94 AutoAlgorithm::Kernel => KernelExplainer::from_masker(&self.model, &self.masker)
95 .with_nsamples(self.kernel_samples)
96 .with_seed(self.seed)
97 .with_exact_threshold(self.exact_threshold)
98 .with_ridge(self.ridge)
99 .with_link(self.link)
100 .with_evaluation_config(self.evaluation)
101 .explain(x),
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::{metrics::check_additivity, FixedMasker, FnModel};
110 use ndarray::{array, Axis};
111
112 #[test]
113 fn selects_exact_below_threshold() {
114 let model = FnModel::new(|x: ArrayView2<'_, f64>| {
115 Ok(x.map_axis(Axis(1), |row| row[0] * row[1] + row[2])
116 .insert_axis(Axis(1)))
117 });
118 let explainer =
119 AutoExplainer::from_masker(model, FixedMasker::new(array![0., 0., 0.]).unwrap());
120 assert_eq!(explainer.selected_algorithm(), AutoAlgorithm::Exact);
121 let explanation = explainer.explain(array![[2., 3., 4.]].view()).unwrap();
122 check_additivity(&explanation, array![[10.]].view(), 1e-12).unwrap();
123 }
124
125 #[test]
126 fn selects_reproducible_kernel_above_threshold() {
127 let model = FnModel::new(|x: ArrayView2<'_, f64>| {
128 Ok(x.map_axis(Axis(1), |row| row[0] * row[1] + row[2] * row[3])
129 .insert_axis(Axis(1)))
130 });
131 let explainer =
132 AutoExplainer::from_masker(model, FixedMasker::new(array![0., 0., 0., 0.]).unwrap())
133 .with_exact_threshold(2)
134 .with_kernel_samples(8)
135 .with_seed(7);
136 assert_eq!(explainer.selected_algorithm(), AutoAlgorithm::Kernel);
137 let sample = array![[1., 2., 3., 4.]];
138 let first = explainer.explain(sample.view()).unwrap();
139 let second = explainer.explain(sample.view()).unwrap();
140 assert_eq!(first, second);
141 check_additivity(&first, array![[14.]].view(), 1e-9).unwrap();
142 }
143}