def test_point(self): res = PointDoi()(self.z) self.assertEqual(len(res), 1, 'PointDoi should return a single point') self.assertTrue(np.array_equal(B.as_array(res[0]), B.as_array(self.z)), 'Value of point should not change')
def calc_doi(self, x_input, tf_cell=False): x = x_input[0] if tf_cell else x_input batch_size = len(x) if (self._baseline is None): if B.is_tensor(x): x = B.as_array(x) baseline = np.zeros_like(x) else: baseline = self._baseline tile_dims = [1] * len(baseline.shape) tile_dims[0] = batch_size baseline = baseline[0, ...] baseline = np.tile(baseline, tuple(tile_dims)) if (B.is_tensor(x) and not B.is_tensor(baseline)): baseline = B.as_tensor(baseline) if (not B.is_tensor(x) and B.is_tensor(baseline)): baseline = B.as_array(baseline) r = self._resolution - 1. doi_out = [(1. - i / r) * x + i / r * baseline for i in range(self._resolution)] if tf_cell: doi_out = [[d, x_input[1]] for d in doi_out] return doi_out
def test_linear_point(self): doi = LinearDoi(resolution=1) res = doi(self.z) self.assertEqual(len(res), 1, 'LinearDoi should return `resolution` points') self.assertTrue( np.array_equal(B.as_array(res[0]), B.as_array(self.z)), 'When `resolution` is 1, should be the same as PointDoi')
def test_internal_channel_axis3(self): qoi = InternalChannelQoI(1, channel_axis=3) res = qoi(self.z) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([21., 14.])))
def test_internal_channel_1d(self): qoi = InternalChannelQoI(2) res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([3., -2.])))
def test_linear(self): doi = LinearDoi(baseline=np.ones(B.int_shape(self.z)), resolution=21) res = doi(self.z) self.assertEqual(len(res), 21, 'LinearDoi should return `resolution` points') self.assertTrue(np.array_equal(B.as_array(res[0]), B.as_array(self.z)), 'First point should be the original point') self.assertTrue(np.all(B.as_array(res[-1]) == 1.), 'Last point should be baseline') self.assertTrue( np.allclose(B.as_array(res[-2]), np.array([[1., 1.05, 1.1], [0.95, 0.9, 0.85]])), 'Intermediate points should interpolate from baseline')
def test_comparative(self): qoi = ComparativeQoI(1, 0) res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([1., -1.])))
def test_threshold_low_minus_high(self): qoi = ThresholdQoI(1.5, low_minus_high=True) res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([-4., -3.])))
def test_lambda(self): qoi = LambdaQoI(lambda y: y[:, 0] + y[:, 1]) res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([3., -1.])))
def test_gaussian_non_tensor(self): doi = GaussianDoi(var=1., resolution=10) res = doi(B.as_array(self.z)) self.assertEqual(len(res), 10, 'GaussianDoi should return `resolution` points') self.assertEqual(res[0].shape, B.int_shape(self.z))
def test_class(self): qoi = ClassQoI(1) res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue(np.allclose(B.as_array(res), np.array([2., -1.])))
def test_threshold_activation(self): qoi = ThresholdQoI(0.75, activation='sigmoid') res = qoi(self.y) self.assertEqual(B.int_shape(res), (2, ), 'Should return one scalar per row in the batch') self.assertTrue( np.allclose(B.as_array(res), np.array([1.1023126, -0.8881443])))
def get_activation_multiplier(self, activation): batch_size = len(activation) if (self._baseline is None): baseline = np.zeros_like(activation) else: baseline = self._baseline tile_dims = [1] * len(baseline.shape) tile_dims[0] = batch_size baseline = baseline[0, ...] baseline = np.tile(baseline, tuple(tile_dims)) if (B.is_tensor(activation) and not B.is_tensor(baseline)): baseline = B.as_tensor(baseline) if (not B.is_tensor(activation) and B.is_tensor(baseline)): baseline = B.as_array(baseline) batch_size = len(activation) return activation - baseline
def __concatenate_doi(D): if len(D) == 0: raise ValueError( 'Got empty distribution of interest. `DoI` must return at ' 'least one point.') if isinstance(D[0], DATA_CONTAINER_TYPE): transposed = [[] for _ in range(len(D[0]))] for point in D: for i, v in enumerate(point): transposed[i].append(v) return [ np.concatenate(D_i) if isinstance(D_i[0], np.ndarray) else D_i[0] for D_i in transposed ] else: if not isinstance(D[0], np.ndarray): D = [B.as_array(d) for d in D] return np.concatenate(D)
def __call__(self, z: ArrayLike) -> List[ArrayLike]: if isinstance(z, (list, tuple)) and len(z) == 1: z = z[0] self._assert_cut_contains_only_one_tensor(z) if self._baseline is None: baseline = B.zeros_like(z) else: baseline = self._baseline if (B.is_tensor(z) and not B.is_tensor(baseline)): baseline = B.as_tensor(baseline) if (not B.is_tensor(z) and B.is_tensor(baseline)): baseline = B.as_array(baseline) r = 1. if self._resolution is 1 else self._resolution - 1. return [ (1. - i / r) * z + i / r * baseline for i in range(self._resolution) ]
def test_max_class_activation_function(self): qoi = MaxClassQoI(activation=B.softmax) res = qoi(self.y) self.assertTrue( np.allclose(B.as_array(res), np.array([0.66524096, 0.66524096])))
def qoi_bprop(self, qoi, model_args, model_kwargs={}, doi_cut=None, to_cut=None, attribution_cut=None, intervention=None): """ qoi_bprop Run the model from the from_layer to the qoi layer and give the gradients w.r.t `attribution_cut` Parameters ---------- model_args, model_kwargs: The args and kwargs given to the call method of a model. This should represent the instances to obtain attributions for, assumed to be a *batched* input. if `self.model` supports evaluation on *data tensors*, the appropriate tensor type may be used (e.g., Pytorch models may accept Pytorch tensors in additon to `np.ndarray`s). The shape of the inputs must match the input shape of `self.model`. qoi: a Quantity of Interest This method will accumulate all gradients of the qoi w.r.t `attribution_cut`. doi_cut: Cut, if `doi_cut` is None, this refers to the InputCut. Cut from which to begin propagation. The shape of `intervention` must match the output shape of this layer. attribution_cut: Cut, optional if `attribution_cut` is None, this refers to the InputCut. The Cut in which attribution will be calculated. This is generally taken from the attribution slyce's attribution_cut. to_cut: Cut, optional if `to_cut` is None, this refers to the OutputCut. The Cut in which qoi will be calculated. This is generally taken from the attribution slyce's to_cut. intervention : backend.Tensor or np.array Input tensor to propagate through the model. If an np.array, will be converted to a tensor on the same device as the model. Returns ------- (backend.Tensor or np.ndarray) the gradients of `qoi` w.r.t. `attribution_cut`, keeping same type as the input. """ if attribution_cut is None: attribution_cut = InputCut() if to_cut is None: to_cut = OutputCut() y, zs = self.fprop(model_args, model_kwargs, doi_cut=doi_cut if doi_cut else InputCut(), to_cut=to_cut, attribution_cut=attribution_cut, intervention=intervention, return_tensor=True) y = to_cut.access_layer(y) grads_list = [] for z in zs: z_flat = ModelWrapper._flatten(z) qoi_out = qoi(y) grads_flat = [B.gradient(B.sum(q), z_flat) for q in qoi_out] if isinstance( qoi_out, DATA_CONTAINER_TYPE) else B.gradient( B.sum(qoi_out), z_flat) grads = [ ModelWrapper._unflatten(g, z, count=[0]) for g in grads_flat ] if isinstance(qoi_out, DATA_CONTAINER_TYPE) else ModelWrapper._unflatten( grads_flat, z, count=[0]) grads = [ attribution_cut.access_layer(g) for g in grads ] if isinstance( qoi_out, DATA_CONTAINER_TYPE) else attribution_cut.access_layer(grads) grads = [B.as_array(g) for g in grads] if isinstance( qoi_out, DATA_CONTAINER_TYPE) else B.as_array(grads) grads_list.append(grads) del y # TODO: garbage collection return grads_list[0] if len(grads_list) == 1 else grads_list
def test_linear_default_baseline(self): doi = LinearDoi(baseline=None, resolution=10) res = doi(self.z) self.assertTrue(np.all(B.as_array(res[-1]) == 0.), 'Default baseline should be zeros')
def test_max_class_no_activation(self): qoi = MaxClassQoI() res = qoi(self.y) self.assertTrue(np.allclose(B.as_array(res), np.array([3., 0.])))
def test_max_class_axis(self): qoi = MaxClassQoI(axis=0) res = qoi(self.y) self.assertTrue(np.allclose(B.as_array(res), np.array([1., 2., 3.])))