shap_rs/explainers/
sampling.rs1use super::PermutationExplainer;
2use crate::{
3 Background, EvaluationConfig, Explainer, Explanation, IndependentMasker, Link, Masker, Predict,
4 Result, ShapError, UncertainExplanation,
5};
6use ndarray::ArrayView2;
7pub struct SamplingExplainer<M, K = IndependentMasker> {
11 model: M,
12 masker: K,
13 nsamples: usize,
14 seed: u64,
15 antithetic: bool,
16 link: Link,
17 evaluation: EvaluationConfig,
18}
19impl<M> SamplingExplainer<M, IndependentMasker> {
20 pub fn new(model: M, background: Background) -> Self {
21 Self::from_masker(model, IndependentMasker::new(background))
22 }
23}
24impl<M, K> SamplingExplainer<M, K> {
25 pub fn from_masker(model: M, masker: K) -> Self {
26 Self {
27 model,
28 masker,
29 nsamples: 256,
30 seed: 0,
31 antithetic: true,
32 link: Link::Identity,
33 evaluation: EvaluationConfig {
34 coalition_batch_size: 64,
35 cache_capacity: 65536,
36 max_model_rows: None,
37 },
38 }
39 }
40 pub fn with_nsamples(mut self, n: usize) -> Self {
41 self.nsamples = n;
42 self
43 }
44 pub fn with_seed(mut self, s: u64) -> Self {
45 self.seed = s;
46 self
47 }
48 pub fn with_antithetic(mut self, enabled: bool) -> Self {
49 self.antithetic = enabled;
50 self
51 }
52 pub fn with_link(mut self, link: Link) -> Self {
53 self.link = link;
54 self
55 }
56 pub fn with_evaluation_config(mut self, c: EvaluationConfig) -> Self {
57 self.evaluation = c;
58 self
59 }
60}
61impl<M: Predict, K: Masker> SamplingExplainer<M, K> {
62 pub fn explain_with_uncertainty(
63 &self,
64 x: ArrayView2<'_, f64>,
65 repeats: usize,
66 ) -> Result<UncertainExplanation> {
67 if repeats < 2 {
68 return Err(ShapError::InvalidConfiguration(
69 "uncertainty estimation requires at least two repeats".into(),
70 ));
71 }
72 crate::error::checked_f64_shape(
73 &[repeats, x.nrows(), x.ncols()],
74 "sampling uncertainty runs",
75 )?;
76 let mut runs = Vec::with_capacity(repeats);
77 for r in 0..repeats {
78 runs.push(
79 PermutationExplainer::from_masker(&self.model, &self.masker)
80 .with_n_permutations(self.nsamples)
81 .with_seed(self.seed.wrapping_add(r as u64))
82 .with_antithetic(self.antithetic)
83 .with_link(self.link)
84 .with_evaluation_config(self.evaluation)
85 .explain(x)?,
86 )
87 }
88 let shape = runs[0].values().dim();
89 let mut mean = ndarray::Array3::zeros(shape);
90 for run in &runs {
91 ndarray::Zip::from(&mut mean)
92 .and(run.values())
93 .for_each(|m, &x| *m += x)
94 }
95 mean.mapv_inplace(|v| v / repeats as f64);
96 let mut variance = ndarray::Array3::<f64>::zeros(shape);
97 for run in &runs {
98 ndarray::Zip::from(&mut variance)
99 .and(run.values())
100 .and(&mean)
101 .for_each(|v, &x, &m| *v += (x - m) * (x - m));
102 }
103 let standard_errors = variance.mapv(|v| (v / ((repeats - 1) * repeats) as f64).sqrt());
104 let explanation = Explanation::new(mean, runs[0].base_values().to_owned(), x.to_owned())?;
105 UncertainExplanation::new(explanation, standard_errors, repeats)
106 }
107}
108impl<M: Predict, K: Masker> Explainer for SamplingExplainer<M, K> {
109 fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
110 PermutationExplainer::from_masker(&self.model, &self.masker)
111 .with_n_permutations(self.nsamples)
112 .with_seed(self.seed)
113 .with_antithetic(self.antithetic)
114 .with_link(self.link)
115 .with_evaluation_config(self.evaluation)
116 .explain(x)
117 }
118}
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::{FixedMasker, FnModel};
123 use ndarray::{array, Axis};
124 #[test]
125 fn reports_zero_error_for_order_independent_model() {
126 let model =
127 FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.sum_axis(Axis(1)).insert_axis(Axis(1))));
128 let e = SamplingExplainer::from_masker(model, FixedMasker::new(array![0., 0.]).unwrap())
129 .with_nsamples(8)
130 .explain_with_uncertainty(array![[2., 3.]].view(), 4)
131 .unwrap();
132 assert!(e.standard_errors().iter().all(|x| *x < 1e-12));
133 assert!((e.explanation().reconstructed()[[0, 0]] - 5.).abs() < 1e-12);
134 }
135}