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 _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: 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_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_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 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 test_simple_ablation_int_to_int_nt(self) -> None: ablation_algo = NoiseTunnel(FeatureAblation(BasicModel())) inp = torch.tensor([[-3, 1, 2]]).float() self._ablation_test_assert( ablation_algo, inp, [[-3.0, 0.0, 0.0]], perturbations_per_eval=(1, 2, 3), stdevs=1e-10, )
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 _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 test_multi_input_ablation_with_mask_nt(self) -> None: ablation_algo = NoiseTunnel( FeatureAblation(BasicModel_MultiLayer_MultiInput())) inp1 = torch.tensor([[23.0, 100.0, 0.0], [20.0, 50.0, 30.0]]) inp2 = torch.tensor([[20.0, 50.0, 30.0], [0.0, 100.0, 0.0]]) inp3 = torch.tensor([[0.0, 100.0, 10.0], [2.0, 10.0, 3.0]]) mask1 = torch.tensor([[1, 1, 1], [0, 1, 0]]) mask2 = torch.tensor([[0, 1, 2]]) mask3 = torch.tensor([[0, 1, 2], [0, 0, 0]]) expected = ( [[492.0, 492.0, 492.0], [200.0, 200.0, 200.0]], [[80.0, 200.0, 120.0], [0.0, 400.0, 0.0]], [[0.0, 400.0, 40.0], [60.0, 60.0, 60.0]], ) self._ablation_test_assert( ablation_algo, (inp1, inp2, inp3), expected, additional_input=(1, ), feature_mask=(mask1, mask2, mask3), stdevs=1e-10, ) self._ablation_test_assert( ablation_algo, (inp1, inp2), expected[0:1], additional_input=(inp3, 1), feature_mask=(mask1, mask2), perturbations_per_eval=(1, 2, 3), stdevs=1e-10, ) expected_with_baseline = ( [[468.0, 468.0, 468.0], [184.0, 192.0, 184.0]], [[68.0, 188.0, 108.0], [-12.0, 388.0, -12.0]], [[-16.0, 384.0, 24.0], [12.0, 12.0, 12.0]], ) self._ablation_test_assert( ablation_algo, (inp1, inp2, inp3), expected_with_baseline, additional_input=(1, ), feature_mask=(mask1, mask2, mask3), baselines=(2, 3.0, 4), perturbations_per_eval=(1, 2, 3), stdevs=1e-10, )
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 _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 attribute( self, inputs: TensorOrTupleOfTensorsGeneric, baselines: Union[TensorOrTupleOfTensorsGeneric, Callable[..., TensorOrTupleOfTensorsGeneric]], n_samples: int = 5, stdevs: Union[float, Tuple[float, ...]] = 0.0, target: TargetType = None, additional_forward_args: Any = None, return_convergence_delta: bool = False, ) -> Union[TensorOrTupleOfTensorsGeneric, Tuple[ TensorOrTupleOfTensorsGeneric, Tensor]]: r""" Args: inputs (tensor or tuple of tensors): Input for which SHAP attribution values are computed. If `forward_func` takes a single tensor as input, a single input tensor should be provided. If `forward_func` takes multiple tensors as input, a tuple of the input tensors should be provided. It is assumed that for all given input tensors, dimension 0 corresponds to the number of examples, and if multiple input tensors are provided, the examples must be aligned appropriately. baselines (tensor, tuple of tensors, callable): Baselines define the starting point from which expectation is computed and can be provided as: - a single tensor, if inputs is a single tensor, with the first dimension equal to the number of examples in the baselines' distribution. The remaining dimensions must match with input tensor's dimension starting from the second dimension. - a tuple of tensors, if inputs is a tuple of tensors, with the first dimension of any tensor inside the tuple equal to the number of examples in the baseline's distribution. The remaining dimensions must match the dimensions of the corresponding input tensor starting from the second dimension. - callable function, optionally takes `inputs` as an argument and either returns a single tensor or a tuple of those. It is recommended that the number of samples in the baselines' tensors is larger than one. n_samples (int, optional): The number of randomly generated examples per sample in the input batch. Random examples are generated by adding gaussian random noise to each sample. Default: `5` if `n_samples` is not provided. stdevs (float, or a tuple of floats optional): The standard deviation of gaussian noise with zero mean that is added to each input in the batch. If `stdevs` is a single float value then that same value is used for all inputs. If it is a tuple, then it must have the same length as the inputs tuple. In this case, each stdev value in the stdevs tuple corresponds to the input with the same index in the inputs tuple. Default: 0.0 target (int, tuple, tensor or list, optional): Output indices for which gradients are computed (for classification cases, this is usually the target class). If the network returns a scalar value per example, no target index is necessary. For general 2D outputs, targets can be either: - a single integer or a tensor containing a single integer, which is applied to all input examples - a list of integers or a 1D tensor, with length matching the number of examples in inputs (dim 0). Each integer is applied as the target for the corresponding example. For outputs with > 2 dimensions, targets can be either: - A single tuple, which contains #output_dims - 1 elements. This target index is applied to all examples. - A list of tuples with length equal to the number of examples in inputs (dim 0), and each tuple containing #output_dims - 1 elements. Each tuple is applied as the target for the corresponding example. Default: None additional_forward_args (any, optional): If the forward function requires additional arguments other than the inputs for which attributions should not be computed, this argument can be provided. It can contain a tuple of ND tensors or any arbitrary python type of any shape. In case of the ND tensor the first dimension of the tensor must correspond to the batch size. It will be repeated for each `n_steps` for each randomly generated input sample. Note that the gradients are not computed with respect to these arguments. Default: None return_convergence_delta (bool, optional): Indicates whether to return convergence delta or not. If `return_convergence_delta` is set to True convergence delta will be returned in a tuple following attributions. Default: False Returns: **attributions** or 2-element tuple of **attributions**, **delta**: - **attributions** (*tensor* or tuple of *tensors*): Attribution score computed based on GradientSHAP with respect to each input feature. Attributions will always be the same size as the provided inputs, with each value providing the attribution of the corresponding input index. If a single tensor is provided as inputs, a single tensor is returned. If a tuple is provided for inputs, a tuple of corresponding sized tensors is returned. - **delta** (*tensor*, returned if return_convergence_delta=True): This is computed using the property that the total sum of forward_func(inputs) - forward_func(baselines) must be very close to the total sum of the attributions based on GradientSHAP. Delta is calculated for each example in the input after adding `n_samples` times gaussian noise to each of them. Therefore, the dimensionality of the deltas tensor is equal to the `number of examples in the input` * `n_samples` The deltas are ordered by each input example and `n_samples` noisy samples generated for it. Examples:: >>> # ImageClassifier takes a single input tensor of images Nx3x32x32, >>> # and returns an Nx10 tensor of class probabilities. >>> net = ImageClassifier() >>> gradient_shap = GradientShap(net) >>> input = torch.randn(3, 3, 32, 32, requires_grad=True) >>> # choosing baselines randomly >>> baselines = torch.randn(20, 3, 32, 32) >>> # Computes gradient shap for the input >>> # Attribution size matches input size: 3x3x32x32 >>> attribution = gradient_shap.attribute(input, baselines, target=5) """ # since `baselines` is a distribution, we can generate it using a function # rather than passing it as an input argument baselines = _format_callable_baseline(baselines, inputs) assert isinstance(baselines[0], torch.Tensor), ( "Baselines distribution has to be provided in a form " "of a torch.Tensor {}.".format(baselines[0])) input_min_baseline_x_grad = InputBaselineXGradient( self.forward_func, self.multiplies_by_inputs) input_min_baseline_x_grad.gradient_func = self.gradient_func nt = NoiseTunnel(input_min_baseline_x_grad) # NOTE: using attribute.__wrapped__ to not log attributions = nt.attribute.__wrapped__( nt, # self inputs, nt_type="smoothgrad", nt_samples=n_samples, stdevs=stdevs, draw_baseline_from_distrib=True, baselines=baselines, target=target, additional_forward_args=additional_forward_args, return_convergence_delta=return_convergence_delta, ) return attributions
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