1use crate::{
2 Background, DifferentiablePredict, Explainer, Explanation, Result, ShapError,
3 UncertainExplanation,
4};
5use ndarray::{Array2, Array3, ArrayView2, Axis, Slice};
6use rand::{rngs::StdRng, Rng, SeedableRng};
7pub struct GradientExplainer<M> {
10 model: M,
11 background: Background,
12 nsamples: usize,
13 seed: u64,
14 local_smoothing: f64,
15 batch_size: usize,
16}
17impl<M> GradientExplainer<M> {
18 pub fn new(model: M, background: Background) -> Self {
19 Self {
20 model,
21 background,
22 nsamples: 256,
23 seed: 0,
24 local_smoothing: 0.0,
25 batch_size: 256,
26 }
27 }
28 pub fn with_nsamples(mut self, n: usize) -> Self {
29 self.nsamples = n;
30 self
31 }
32 pub fn with_seed(mut self, s: u64) -> Self {
33 self.seed = s;
34 self
35 }
36 pub fn with_local_smoothing(mut self, s: f64) -> Self {
37 self.local_smoothing = s;
38 self
39 }
40 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
42 self.batch_size = batch_size;
43 self
44 }
45}
46impl<M: DifferentiablePredict> GradientExplainer<M> {
47 pub fn explain_with_uncertainty(
50 &self,
51 x: ArrayView2<'_, f64>,
52 repeats: usize,
53 ) -> Result<UncertainExplanation> {
54 if repeats < 2 {
55 return Err(ShapError::InvalidConfiguration(
56 "uncertainty estimation requires at least two repeats".into(),
57 ));
58 }
59 let mut runs = Vec::with_capacity(repeats);
60 for repeat in 0..repeats {
61 runs.push(
62 GradientExplainer {
63 model: &self.model,
64 background: self.background.clone(),
65 nsamples: self.nsamples,
66 seed: self.seed.wrapping_add(repeat as u64),
67 local_smoothing: self.local_smoothing,
68 batch_size: self.batch_size,
69 }
70 .explain(x)?,
71 );
72 }
73 let shape = runs[0].values().dim();
74 let mut mean = Array3::<f64>::zeros(shape);
75 for run in &runs {
76 ndarray::Zip::from(&mut mean)
77 .and(run.values())
78 .for_each(|average, &value| *average += value);
79 }
80 mean.mapv_inplace(|value| value / repeats as f64);
81 let mut variance = Array3::<f64>::zeros(shape);
82 for run in &runs {
83 ndarray::Zip::from(&mut variance)
84 .and(run.values())
85 .and(&mean)
86 .for_each(|sum, &value, &average| *sum += (value - average).powi(2));
87 }
88 let standard_errors =
89 variance.mapv(|value| (value / ((repeats - 1) * repeats) as f64).sqrt());
90 let explanation = Explanation::new(mean, runs[0].base_values().to_owned(), x.to_owned())?;
91 UncertainExplanation::new(explanation, standard_errors, repeats)
92 }
93}
94impl<M: DifferentiablePredict> Explainer for GradientExplainer<M> {
95 fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
96 let m = self.background.n_features();
97 if x.nrows() == 0 {
98 return Err(ShapError::EmptyData);
99 }
100 if x.ncols() != m {
101 return Err(ShapError::DimensionMismatch {
102 expected: format!("{m} features"),
103 found: format!("{}", x.ncols()),
104 });
105 }
106 if self.nsamples == 0
107 || self.batch_size == 0
108 || !self.local_smoothing.is_finite()
109 || self.local_smoothing < 0.
110 {
111 return Err(ShapError::InvalidConfiguration(
112 "nsamples and batch size must be positive and local smoothing non-negative".into(),
113 ));
114 }
115 let prediction = self.model.predict(self.background.data())?;
116 if prediction.nrows() != self.background.n_samples() || prediction.ncols() == 0 {
117 return Err(ShapError::DimensionMismatch {
118 expected: format!("{} background predictions", self.background.n_samples()),
119 found: format!("{:?}", prediction.dim()),
120 });
121 }
122 let base = prediction.mean_axis(Axis(0)).unwrap();
123 let o = base.len();
124 crate::error::checked_f64_shape(&[x.nrows(), m, o], "gradient explanation")?;
125 crate::error::checked_f64_shape(&[self.nsamples, m], "gradient sampling batch")?;
126 let bases = Array2::from_shape_fn((x.nrows(), o), |(_, k)| base[k]);
127 let std = feature_std(&self.background);
128 let mut values = Array3::zeros((x.nrows(), m, o));
129 for n in 0..x.nrows() {
130 let mut rng = StdRng::seed_from_u64(crate::coalition::sample_seed(self.seed, x.row(n)));
131 let mut points = Array2::zeros((self.nsamples, m));
132 let mut deltas = Array2::zeros((self.nsamples, m));
133 for s in 0..self.nsamples {
134 let b = rng.gen_range(0..self.background.n_samples());
135 let alpha = rng.gen::<f64>();
136 for j in 0..m {
137 let noise = if self.local_smoothing > 0. {
138 gaussian(&mut rng) * self.local_smoothing * std[j]
139 } else {
140 0.
141 };
142 let delta = x[[n, j]] + noise - self.background.data()[[b, j]];
143 deltas[[s, j]] = delta;
144 points[[s, j]] = self.background.data()[[b, j]] + alpha * delta
145 }
146 }
147 for start in (0..self.nsamples).step_by(self.batch_size) {
148 let end = start.saturating_add(self.batch_size).min(self.nsamples);
149 let gradients = self
150 .model
151 .gradients(points.slice_axis(Axis(0), Slice::from(start..end)))?;
152 if gradients.dim() != (end - start, m, o) {
153 return Err(ShapError::DimensionMismatch {
154 expected: format!("({}, {m}, {o}) gradients", end - start),
155 found: format!("{:?}", gradients.dim()),
156 });
157 }
158 if gradients.iter().any(|v| !v.is_finite()) {
159 return Err(ShapError::ModelError(
160 "gradient contains a non-finite value".into(),
161 ));
162 }
163 for j in 0..m {
164 for k in 0..o {
165 values[[n, j, k]] += (start..end)
166 .map(|sample| gradients[[sample - start, j, k]] * deltas[[sample, j]])
167 .sum::<f64>()
168 / self.nsamples as f64
169 }
170 }
171 }
172 }
173 Explanation::new(values, bases, x.to_owned())
174 }
175}
176fn gaussian<R: Rng + ?Sized>(rng: &mut R) -> f64 {
177 let u1 = rng.gen::<f64>().max(f64::MIN_POSITIVE);
178 let u2 = rng.gen::<f64>();
179 (-2. * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
180}
181fn feature_std(bg: &Background) -> Vec<f64> {
182 let mean = bg.data().mean_axis(Axis(0)).unwrap();
183 (0..bg.n_features())
184 .map(|j| {
185 (bg.data()
186 .column(j)
187 .iter()
188 .map(|x| (x - mean[j]).powi(2))
189 .sum::<f64>()
190 / bg.n_samples() as f64)
191 .sqrt()
192 })
193 .collect()
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::Predict;
200 use ndarray::array;
201 struct Linear;
202 impl Predict for Linear {
203 fn predict(&self, x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
204 Ok(x.map_axis(Axis(1), |r| 2. * r[0] - r[1])
205 .insert_axis(Axis(1)))
206 }
207 }
208 impl DifferentiablePredict for Linear {
209 fn gradients(&self, x: ArrayView2<'_, f64>) -> Result<Array3<f64>> {
210 Ok(Array3::from_shape_fn((x.nrows(), 2, 1), |(_, j, _)| {
211 if j == 0 {
212 2.
213 } else {
214 -1.
215 }
216 }))
217 }
218 }
219 #[test]
220 fn expected_gradients_is_exact_for_linear_models() {
221 let e = GradientExplainer::new(Linear, Background::new(array![[0., 0.]]).unwrap())
222 .with_nsamples(32)
223 .with_batch_size(3)
224 .explain(array![[3., 4.]].view())
225 .unwrap();
226 assert!((e.values()[[0, 0, 0]] - 6.).abs() < 1e-12);
227 assert!((e.values()[[0, 1, 0]] + 4.).abs() < 1e-12);
228 assert!((e.reconstructed()[[0, 0]] - 2.).abs() < 1e-12);
229 }
230
231 #[test]
232 fn reports_uncertainty_for_stochastic_expected_gradients() {
233 let e =
234 GradientExplainer::new(Linear, Background::new(array![[0., 0.], [2., 4.]]).unwrap())
235 .with_nsamples(16)
236 .explain_with_uncertainty(array![[3., 4.]].view(), 4)
237 .unwrap();
238 assert_eq!(e.repeats(), 4);
239 assert_eq!(e.standard_errors().dim(), (1, 2, 1));
240 assert!(e.standard_errors().iter().all(|value| value.is_finite()));
241 }
242}