def test_batched_input_smoothgrad_wo_mutliplying_by_inputs(self) -> None: model = BasicModel_MultiLayer() inputs = torch.tensor( [[1.5, 2.0, 1.3], [0.5, 0.1, 2.3], [1.5, 2.0, 1.3]], requires_grad=True ) ig_wo_mutliplying_by_inputs = IntegratedGradients( model, multiply_by_inputs=False ) nt_wo_mutliplying_by_inputs = NoiseTunnel(ig_wo_mutliplying_by_inputs) ig = IntegratedGradients(model) nt = NoiseTunnel(ig) n_samples = 5 target = 0 type = "smoothgrad" attributions_wo_mutliplying_by_inputs = nt_wo_mutliplying_by_inputs.attribute( inputs, nt_type=type, nt_samples=n_samples, stdevs=0.0, target=target, n_steps=500, ) with self.assertWarns(DeprecationWarning): attributions = nt.attribute( inputs, nt_type=type, n_samples=n_samples, stdevs=0.0, target=target, n_steps=500, ) assertTensorAlmostEqual( self, attributions_wo_mutliplying_by_inputs * inputs, attributions )
def target_test_assert(self) -> None: attr_method: Attribution if target_layer: internal_algorithm = cast(Type[InternalAttribution], algorithm) attr_method = internal_algorithm(model, target_layer) else: attr_method = algorithm(model) if noise_tunnel: attr_method = NoiseTunnel(attr_method) attributions_orig = attr_method.attribute(**args) for i in range(num_examples): args["target"] = (original_targets[i] if len(original_targets) == num_examples else original_targets) args["inputs"] = (original_inputs[i:i + 1] if isinstance( original_inputs, Tensor) else tuple( original_inp[i:i + 1] for original_inp in original_inputs)) if original_additional_forward_args is not None: args["additional_forward_args"] = tuple( single_add_arg[i:i + 1] if isinstance( single_add_arg, Tensor) else single_add_arg for single_add_arg in original_additional_forward_args) if replace_baselines: if isinstance(original_inputs, Tensor): args["baselines"] = original_baselines[i:i + 1] elif isinstance(original_baselines, tuple): args["baselines"] = tuple( single_baseline[i:i + 1] if isinstance( single_baseline, Tensor) else single_baseline for single_baseline in original_baselines) self.setUp() single_attr = attr_method.attribute(**args) current_orig_attributions = ( attributions_orig[i:i + 1] if isinstance( attributions_orig, Tensor) else tuple( single_attrib[i:i + 1] for single_attrib in attributions_orig)) assertTensorTuplesAlmostEqual( self, current_orig_attributions, single_attr, delta=target_delta, mode="max", ) if len(original_targets) == num_examples: # If original_targets contained multiple elements, then # we also compare with setting targets to a list with # a single element. args["target"] = original_targets[i:i + 1] self.setUp() single_attr_target_list = attr_method.attribute(**args) assertTensorTuplesAlmostEqual( self, current_orig_attributions, single_attr_target_list, delta=target_delta, mode="max", )
def _input_x_gradient_classification_assert(self, nt_type: str = "vanilla" ) -> None: num_in = 5 input = torch.tensor([[0.0, 1.0, 2.0, 3.0, 4.0]], requires_grad=True) target = torch.tensor(5) # 10-class classification model model = SoftmaxModel(num_in, 20, 10) input_x_grad = InputXGradient(model.forward) if nt_type == "vanilla": attributions = input_x_grad.attribute(input, target) output = model(input)[:, target] output.backward() expercted = input.grad * input self.assertEqual( expercted.detach().numpy().tolist(), attributions.detach().numpy().tolist(), ) else: nt = NoiseTunnel(input_x_grad) attributions = nt.attribute(input, nt_type=nt_type, n_samples=10, stdevs=1.0, target=target) self.assertEqual(attributions.shape, input.shape)
def _saliency_base_assert( self, model: Module, inputs: TensorOrTupleOfTensorsGeneric, expected: TensorOrTupleOfTensorsGeneric, additional_forward_args: Any = None, nt_type: str = "vanilla", ) -> None: saliency = Saliency(model) self.assertFalse(saliency.uses_input_marginal_effects) if nt_type == "vanilla": attributions = saliency.attribute( inputs, additional_forward_args=additional_forward_args) else: nt = NoiseTunnel(saliency) attributions = nt.attribute( inputs, nt_type=nt_type, n_samples=10, stdevs=0.0000002, additional_forward_args=additional_forward_args, ) for input, attribution, expected_attr in zip(inputs, attributions, expected): if nt_type == "vanilla": self._assert_attribution(attribution, expected_attr) self.assertEqual(input.shape, attribution.shape)
def _saliency_base_assert( self, model, inputs, expected, additional_forward_args=None, nt_type="vanilla" ): saliency = Saliency(model) if nt_type == "vanilla": attributions = saliency.attribute( inputs, additional_forward_args=additional_forward_args ) else: nt = NoiseTunnel(saliency) attributions = nt.attribute( inputs, nt_type=nt_type, n_samples=10, stdevs=0.0000002, additional_forward_args=additional_forward_args, ) if isinstance(attributions, tuple): for input, attribution, expected_attr in zip( inputs, attributions, expected ): if nt_type == "vanilla": self._assert_attribution(attribution, expected_attr) self.assertEqual(input.shape, attribution.shape) else: if nt_type == "vanilla": self._assert_attribution(attributions, expected) self.assertEqual(inputs.shape, attributions.shape)
def _saliency_base_assert( self, model: Module, inputs: TensorOrTupleOfTensorsGeneric, expected: TensorOrTupleOfTensorsGeneric, additional_forward_args: Any = None, nt_type: str = "vanilla", n_samples_batch_size=None, ) -> Union[Tensor, Tuple[Tensor, ...]]: saliency = Saliency(model) self.assertFalse(saliency.multiplies_by_inputs) if nt_type == "vanilla": attributions = saliency.attribute( inputs, additional_forward_args=additional_forward_args) else: nt = NoiseTunnel(saliency) attributions = nt.attribute( inputs, nt_type=nt_type, nt_samples=10, nt_samples_batch_size=n_samples_batch_size, stdevs=0.0000002, additional_forward_args=additional_forward_args, ) for input, attribution, expected_attr in zip(inputs, attributions, expected): if nt_type == "vanilla": self._assert_attribution(attribution, expected_attr) self.assertEqual(input.shape, attribution.shape) return attributions
def _saliency_classification_assert(self, nt_type: str = "vanilla") -> None: num_in = 5 input = torch.tensor([[0.0, 1.0, 2.0, 3.0, 4.0]], requires_grad=True) target = torch.tensor(5) # 10-class classification model model = SoftmaxModel(num_in, 20, 10) saliency = Saliency(model) if nt_type == "vanilla": attributions = saliency.attribute(input, target) output = model(input)[:, target] output.backward() expected = torch.abs(cast(Tensor, input.grad)) self.assertEqual( expected.detach().numpy().tolist(), attributions.detach().numpy().tolist(), ) else: nt = NoiseTunnel(saliency) attributions = nt.attribute(input, nt_type=nt_type, n_samples=10, stdevs=0.0002, target=target) self.assertEqual(input.shape, attributions.shape)
def _validate_completness(self, model, inputs, target, type="vanilla", baseline=None): ig = IntegratedGradients(model.forward) for method in [ "riemann_right", "riemann_left", "riemann_middle", "riemann_trapezoid", "gausslegendre", ]: model.zero_grad() if type == "vanilla": attributions, delta = ig.attribute( inputs, baselines=baseline, target=target, method=method, n_steps=1000, return_convergence_delta=True, ) delta_expected = ig.compute_convergence_delta( attributions, baseline, inputs, target) assertTensorAlmostEqual(self, delta_expected, delta) delta_condition = all(abs(delta.numpy().flatten()) < 0.003) self.assertTrue( delta_condition, "The sum of attribution values {} is not " "nearly equal to the difference between the endpoint for " "some samples".format(delta), ) self.assertEqual([inputs.shape[0]], list(delta.shape)) else: nt = NoiseTunnel(ig) n_samples = 10 attributions, delta = nt.attribute( inputs, baselines=baseline, nt_type=type, n_samples=n_samples, stdevs=0.0002, n_steps=1000, target=target, method=method, return_convergence_delta=True, ) self.assertEqual([inputs.shape[0] * n_samples], list(delta.shape)) self.assertTrue(all(abs(delta.numpy().flatten()) < 0.05)) self.assertEqual(attributions.shape, inputs.shape)
def hook_removal_test_assert(self) -> None: attr_method: Attribution expect_error = False if layer is not None: if mode is HookRemovalMode.invalid_module: expect_error = True if isinstance(layer, list): _set_deep_layer_value(model, layer[0], ErrorModule()) else: _set_deep_layer_value(model, layer, ErrorModule()) target_layer = get_target_layer(model, layer) internal_algorithm = cast(Type[InternalAttribution], algorithm) attr_method = internal_algorithm(model, target_layer) else: attr_method = algorithm(model) if noise_tunnel: attr_method = NoiseTunnel(attr_method) if mode is HookRemovalMode.incorrect_target_or_neuron: # Overwriting target and neuron index arguments to # incorrect values. if "target" in args: args["target"] = (9999, ) * 20 expect_error = True if "neuron_selector" in args: args["neuron_selector"] = (9999, ) * 20 expect_error = True if expect_error: with self.assertRaises(AssertionError): attr_method.attribute(**args) else: attr_method.attribute(**args) def check_leftover_hooks(module): self.assertEqual(len(module._forward_hooks), 0) self.assertEqual(len(module._backward_hooks), 0) self.assertEqual(len(module._forward_pre_hooks), 0) model.apply(check_leftover_hooks)
def _validate_completness( self, model: Module, input: Tensor, target: Tensor, type: str = "vanilla", approximation_method: str = "gausslegendre", baseline: Optional[Union[Tensor, int, float, Tuple[Union[Tensor, int, float], ...]]] = None, ) -> None: ig = IntegratedGradients(model.forward) model.zero_grad() if type == "vanilla": attributions, delta = ig.attribute( input, baselines=baseline, target=target, method=approximation_method, n_steps=200, return_convergence_delta=True, ) delta_expected = ig.compute_convergence_delta( attributions, baseline, input, target) assertTensorAlmostEqual(self, delta_expected, delta) delta_condition = all(abs(delta.numpy().flatten()) < 0.005) self.assertTrue( delta_condition, "The sum of attribution values {} is not " "nearly equal to the difference between the endpoint for " "some samples".format(delta), ) self.assertEqual([input.shape[0]], list(delta.shape)) else: nt = NoiseTunnel(ig) n_samples = 10 attributions, delta = nt.attribute( input, baselines=baseline, nt_type=type, n_samples=n_samples, stdevs=0.0002, n_steps=100, target=target, method=approximation_method, return_convergence_delta=True, ) self.assertEqual([input.shape[0] * n_samples], list(delta.shape)) self.assertTrue(all(abs(delta.numpy().flatten()) < 0.05)) self.assertEqual(attributions.shape, input.shape)
def _validate_completness(self, model, inputs, target, type="vanilla"): ig = IntegratedGradients(model.forward) for method in [ "riemann_right", "riemann_left", "riemann_middle", "riemann_trapezoid", "gausslegendre", ]: model.zero_grad() if type == "vanilla": attributions, delta = ig.attribute( inputs, target=target, method=method, n_steps=1000, return_convergence_delta=True, ) # attributions are returned as tuples for the integrated_gradients self.assertAlmostEqual( attributions.sum(), model.forward(inputs)[:, target] - model.forward(0 * inputs)[:, target], delta=0.005, ) delta_expected = abs(attributions.sum().item() - ( model.forward(inputs)[:, target].item() - model.forward(0 * inputs)[:, target].item())) self.assertAlmostEqual(abs(delta).sum().item(), delta_expected, delta=0.005) self.assertEqual([inputs.shape[0]], list(delta.shape)) else: nt = NoiseTunnel(ig) n_samples = 10 attributions, delta = nt.attribute( inputs, nt_type=type, n_samples=n_samples, stdevs=0.0002, n_steps=1000, target=target, method=method, return_convergence_delta=True, ) self.assertEqual([inputs.shape[0] * n_samples], list(delta.shape)) self.assertTrue(all(abs(delta.numpy().flatten()) < 0.05)) self.assertEqual(attributions.shape, inputs.shape)
def _input_x_gradient_base_assert( self, model: Module, inputs: TensorOrTupleOfTensorsGeneric, expected_grads: TensorOrTupleOfTensorsGeneric, additional_forward_args: Any = None, nt_type: str = "vanilla", ) -> None: input_x_grad = InputXGradient(model) attributions: TensorOrTupleOfTensorsGeneric if nt_type == "vanilla": attributions = input_x_grad.attribute( inputs, additional_forward_args=additional_forward_args) else: nt = NoiseTunnel(input_x_grad) attributions = nt.attribute( inputs, nt_type=nt_type, n_samples=10, stdevs=0.0002, additional_forward_args=additional_forward_args, ) if isinstance(attributions, tuple): for input, attribution, expected_grad in zip( inputs, attributions, expected_grads): if nt_type == "vanilla": assertArraysAlmostEqual(attribution.reshape(-1), (expected_grad * input).reshape(-1)) self.assertEqual(input.shape, attribution.shape) elif isinstance(attributions, Tensor): if nt_type == "vanilla": assertArraysAlmostEqual( attributions.reshape(-1), (expected_grads * inputs).reshape(-1), delta=0.5, ) self.assertEqual(inputs.shape, attributions.shape)
def _input_x_gradient_base_assert( self, model: Module, inputs: TensorOrTupleOfTensorsGeneric, expected_grads: TensorOrTupleOfTensorsGeneric, additional_forward_args: Any = None, nt_type: str = "vanilla", ) -> None: input_x_grad = InputXGradient(model) self.assertTrue(input_x_grad.multiplies_by_inputs) attributions: TensorOrTupleOfTensorsGeneric if nt_type == "vanilla": attributions = input_x_grad.attribute( inputs, additional_forward_args=additional_forward_args, ) else: nt = NoiseTunnel(input_x_grad) attributions = nt.attribute( inputs, nt_type=nt_type, nt_samples=10, stdevs=0.0002, additional_forward_args=additional_forward_args, ) if isinstance(attributions, tuple): for input, attribution, expected_grad in zip( inputs, attributions, expected_grads ): if nt_type == "vanilla": self._assert_attribution(expected_grad, input, attribution) self.assertEqual(input.shape, attribution.shape) elif isinstance(attributions, Tensor): if nt_type == "vanilla": self._assert_attribution(expected_grads, inputs, attributions) self.assertEqual( cast(Tensor, inputs).shape, cast(Tensor, attributions).shape )
def data_parallel_test_assert(self) -> None: # Construct cuda_args, moving all tensor inputs in args to CUDA device cuda_args = {} for key in args: if isinstance(args[key], Tensor): cuda_args[key] = args[key].cuda() elif isinstance(args[key], tuple): cuda_args[key] = tuple( elem.cuda() if isinstance(elem, Tensor) else elem for elem in args[key]) else: cuda_args[key] = args[key] alt_device_ids = None cuda_model = copy.deepcopy(model).cuda() # Initialize models based on DataParallelCompareMode if mode is DataParallelCompareMode.cpu_cuda: model_1, model_2 = model, cuda_model args_1, args_2 = args, cuda_args elif mode is DataParallelCompareMode.data_parallel_default: model_1, model_2 = ( cuda_model, torch.nn.parallel.DataParallel(cuda_model), ) args_1, args_2 = cuda_args, cuda_args elif mode is DataParallelCompareMode.data_parallel_alt_dev_ids: alt_device_ids = [0] + [ x for x in range(torch.cuda.device_count() - 1, 0, -1) ] model_1, model_2 = ( cuda_model, torch.nn.parallel.DataParallel(cuda_model, device_ids=alt_device_ids), ) args_1, args_2 = cuda_args, cuda_args else: raise AssertionError( "DataParallel compare mode type is not valid.") attr_method_1: Attribution attr_method_2: Attribution if target_layer: internal_algorithm = cast(Type[InternalAttribution], algorithm) attr_method_1 = internal_algorithm( model_1, _get_deep_layer_name(model_1, target_layer)) # cuda_model is used to obtain target_layer since DataParallel # adds additional wrapper. # model_2 is always either the CUDA model itself or DataParallel if alt_device_ids is None: attr_method_2 = internal_algorithm( model_2, _get_deep_layer_name(cuda_model, target_layer)) else: # LayerDeepLift and LayerDeepLiftShap do not take device ids # as a parameter, since they must always have the DataParallel # model object directly. # Some neuron methods and GuidedGradCAM also require the # model and cannot take a forward function. if issubclass( internal_algorithm, ( LayerDeepLift, LayerDeepLiftShap, NeuronDeepLift, NeuronDeepLiftShap, NeuronDeconvolution, NeuronGuidedBackprop, GuidedGradCam, ), ): attr_method_2 = internal_algorithm( model_2, _get_deep_layer_name(cuda_model, target_layer)) else: attr_method_2 = internal_algorithm( model_2.forward, _get_deep_layer_name(cuda_model, target_layer), device_ids=alt_device_ids, ) else: attr_method_1 = algorithm(model_1) attr_method_2 = algorithm(model_2) if noise_tunnel: attr_method_1 = NoiseTunnel(attr_method_1) attr_method_2 = NoiseTunnel(attr_method_2) if attr_method_1.has_convergence_delta(): attributions_1, delta_1 = attr_method_1.attribute( return_convergence_delta=True, **args_1) self.setUp() attributions_2, delta_2 = attr_method_2.attribute( return_convergence_delta=True, **args_2) assertTensorTuplesAlmostEqual(self, attributions_1, attributions_2, mode="max", delta=dp_delta) assertTensorTuplesAlmostEqual(self, delta_1, delta_2, mode="max", delta=dp_delta) else: attributions_1 = attr_method_1.attribute(**args_1) self.setUp() attributions_2 = attr_method_2.attribute(**args_2) assertTensorTuplesAlmostEqual(self, attributions_1, attributions_2, mode="max", delta=dp_delta)
def _compute_attribution_and_evaluate( self, model: Module, inputs: TensorOrTupleOfTensorsGeneric, baselines: BaselineType = None, target: Union[None, int] = None, additional_forward_args: Any = None, type: str = "vanilla", approximation_method: str = "gausslegendre", multiply_by_inputs=True, ) -> Tuple[Tensor, ...]: r""" attrib_type: 'vanilla', 'smoothgrad', 'smoothgrad_sq', 'vargrad' """ ig = IntegratedGradients(model, multiply_by_inputs=multiply_by_inputs) self.assertEquals(ig.multiplies_by_inputs, multiply_by_inputs) if not isinstance(inputs, tuple): inputs = (inputs,) # type: ignore inputs: Tuple[Tensor, ...] if baselines is not None and not isinstance(baselines, tuple): baselines = (baselines,) if baselines is None: baselines = _tensorize_baseline(inputs, _zeros(inputs)) if type == "vanilla": attributions, delta = ig.attribute( inputs, baselines, additional_forward_args=additional_forward_args, method=approximation_method, n_steps=500, target=target, return_convergence_delta=True, ) model.zero_grad() attributions_without_delta, delta = ig.attribute( inputs, baselines, additional_forward_args=additional_forward_args, method=approximation_method, n_steps=500, target=target, return_convergence_delta=True, ) model.zero_grad() self.assertEqual([inputs[0].shape[0]], list(delta.shape)) delta_external = ig.compute_convergence_delta( attributions, baselines, inputs, target=target, additional_forward_args=additional_forward_args, ) assertArraysAlmostEqual(delta, delta_external, 0.0) else: nt = NoiseTunnel(ig) n_samples = 5 attributions, delta = nt.attribute( inputs, nt_type=type, nt_samples=n_samples, stdevs=0.00000002, baselines=baselines, target=target, additional_forward_args=additional_forward_args, method=approximation_method, n_steps=500, return_convergence_delta=True, ) with self.assertWarns(DeprecationWarning): attributions_without_delta = nt.attribute( inputs, nt_type=type, n_samples=n_samples, stdevs=0.00000002, baselines=baselines, target=target, additional_forward_args=additional_forward_args, method=approximation_method, n_steps=500, ) self.assertEquals(nt.multiplies_by_inputs, multiply_by_inputs) self.assertEqual([inputs[0].shape[0] * n_samples], list(delta.shape)) for input, attribution in zip(inputs, attributions): self.assertEqual(attribution.shape, input.shape) if multiply_by_inputs: self.assertTrue(all(abs(delta.numpy().flatten()) < 0.07)) # compare attributions retrieved with and without # `return_convergence_delta` flag for attribution, attribution_without_delta in zip( attributions, attributions_without_delta ): assertTensorAlmostEqual( self, attribution, attribution_without_delta, delta=0.05 ) return cast(Tuple[Tensor, ...], attributions)
def jit_test_assert(self) -> None: model_1 = model attr_args = args if (mode is JITCompareMode.data_parallel_jit_trace or JITCompareMode.data_parallel_jit_script): if not torch.cuda.is_available() or torch.cuda.device_count( ) == 0: raise unittest.SkipTest( "Skipping GPU test since CUDA not available.") # Construct cuda_args, moving all tensor inputs in args to CUDA device cuda_args = {} for key in args: if isinstance(args[key], Tensor): cuda_args[key] = args[key].cuda() elif isinstance(args[key], tuple): cuda_args[key] = tuple( elem.cuda() if isinstance(elem, Tensor) else elem for elem in args[key]) else: cuda_args[key] = args[key] attr_args = cuda_args model_1 = model_1.cuda() # Initialize models based on JITCompareMode if (mode is JITCompareMode.cpu_jit_script or JITCompareMode.data_parallel_jit_script): model_2 = torch.jit.script(model_1) # type: ignore elif (mode is JITCompareMode.cpu_jit_trace or JITCompareMode.data_parallel_jit_trace): all_inps = _format_input(args["inputs"]) + ( _format_additional_forward_args( args["additional_forward_args"]) if "additional_forward_args" in args and args["additional_forward_args"] is not None else tuple()) model_2 = torch.jit.trace(model_1, all_inps) # type: ignore else: raise AssertionError("JIT compare mode type is not valid.") attr_method_1 = algorithm(model_1) attr_method_2 = algorithm(model_2) if noise_tunnel: attr_method_1 = NoiseTunnel(attr_method_1) attr_method_2 = NoiseTunnel(attr_method_2) if attr_method_1.has_convergence_delta(): attributions_1, delta_1 = attr_method_1.attribute( return_convergence_delta=True, **attr_args) self.setUp() attributions_2, delta_2 = attr_method_2.attribute( return_convergence_delta=True, **attr_args) assertTensorTuplesAlmostEqual(self, attributions_1, attributions_2, mode="max") assertTensorTuplesAlmostEqual(self, delta_1, delta_2, mode="max") else: attributions_1 = attr_method_1.attribute(**attr_args) self.setUp() attributions_2 = attr_method_2.attribute(**attr_args) assertTensorTuplesAlmostEqual(self, attributions_1, attributions_2, mode="max")
def _compute_attribution_and_evaluate( self, model, inputs, baselines=None, target=None, additional_forward_args=None, type="vanilla", ): r""" attrib_type: 'vanilla', 'smoothgrad', 'smoothgrad_sq', 'vargrad' """ ig = IntegratedGradients(model) if not isinstance(inputs, tuple): inputs = (inputs,) if baselines is not None and not isinstance(baselines, tuple): baselines = (baselines,) if baselines is None: baselines = _zeros(inputs) for method in [ "riemann_right", "riemann_left", "riemann_middle", "riemann_trapezoid", "gausslegendre", ]: if type == "vanilla": attributions, delta = ig.attribute( inputs, baselines, additional_forward_args=additional_forward_args, method=method, n_steps=2000, target=target, return_convergence_delta=True, ) model.zero_grad() attributions_without_delta, delta = ig.attribute( inputs, baselines, additional_forward_args=additional_forward_args, method=method, n_steps=2000, target=target, return_convergence_delta=True, ) model.zero_grad() self.assertEqual([inputs[0].shape[0]], list(delta.shape)) delta_external = ig.compute_convergence_delta( attributions, baselines, inputs, target=target, additional_forward_args=additional_forward_args, ) assertArraysAlmostEqual(delta, delta_external, 0.0) else: nt = NoiseTunnel(ig) n_samples = 5 attributions, delta = nt.attribute( inputs, nt_type=type, n_samples=n_samples, stdevs=0.00000002, baselines=baselines, target=target, additional_forward_args=additional_forward_args, method=method, n_steps=2000, return_convergence_delta=True, ) attributions_without_delta = nt.attribute( inputs, nt_type=type, n_samples=n_samples, stdevs=0.00000002, baselines=baselines, target=target, additional_forward_args=additional_forward_args, method=method, n_steps=2000, ) self.assertEqual([inputs[0].shape[0] * n_samples], list(delta.shape)) for input, attribution in zip(inputs, attributions): self.assertEqual(attribution.shape, input.shape) self.assertTrue(all(abs(delta.numpy().flatten()) < 0.05)) # compare attributions retrieved with and without # `return_convergence_delta` flag for attribution, attribution_without_delta in zip( attributions, attributions_without_delta ): assertTensorAlmostEqual( self, attribution, attribution_without_delta, delta=0.05 ) return attributions