Source code for interpretdl.interpreter.smooth_grad_v2

import numpy as np

from tqdm import tqdm
from .abc_interpreter import InputGradientInterpreter
from ..data_processor.readers import images_transform_pipeline, preprocess_save_path
from ..data_processor.visualizer import explanation_to_vis, show_vis_explanation, save_image


[docs]class SmoothGradInterpreterV2(InputGradientInterpreter): """ Smooth Gradients Interpreter. For input gradient based interpreters, the target issue is generally the vanilla input gradient's noises. The basic idea of reducing the noises is to use different similar inputs to get the input gradients and do the average. Smooth Gradients method solves the problem of meaningless local variations in partial derivatives by adding random noise to the inputs multiple times and take the average of the gradients. This ``SmoothGradInterpreterV2`` only optimizes the GPU usage issue where large GPU memory usage may cause an error for large models and large batch sizes. More details regarding the Smooth Gradients method can be found in the original paper: http://arxiv.org/abs/1706.03825. """ def __init__(self, model: callable, device: str = 'gpu:0'): """ Args: model (callable): A model with :py:func:`forward` and possibly :py:func:`backward` functions. device (str): The device used for running ``model``, options: ``"cpu"``, ``"gpu:0"``, ``"gpu:1"`` etc. """ InputGradientInterpreter.__init__(self, model, device)
[docs] def interpret(self, inputs: str or list(str) or np.ndarray, labels: list or np.ndarray = None, noise_amount: int = 0.1, n_samples: int = 50, split: int = 2, gradient_of: str = 'probability', resize_to: int = 224, crop_to: int = None, visual: bool = True, save_path: str = None) -> np.ndarray: """The technical details of the SmoothGrad method are described as follows: SmoothGrad generates ``n_samples`` noised inputs, with the noise scale of ``noise_amount``, and then computes the gradients *w.r.t.* these noised inputs. The final explanation is averaged gradients. The difference to :class:`SmoothGradInterpreter` is an additional argument ``split``, where total samples are divided into ``split`` parts to pass the model, to avoid large GPU memory usages. Args: inputs (str or list): The input image filepath or a list of filepaths or numpy array of read images. labels (list or np.ndarray, optional): The target labels to analyze. The number of labels should be equal to the number of images. If None, the most likely label for each image will be used. Default: ``None``. noise_amount (int, optional): Noise level of added noise to the image. The std of Gaussian random noise is ``noise_amount`` * (x :sub:`max` - x :sub:`min`). Default: ``0.1``. n_samples (int, optional): The number of new images generated by adding noise. Default: ``50``. split (int, optional): The number of splits. Default: ``2``. gradient_of (str, optional): compute the gradient of ['probability', 'logit' or 'loss']. Default: ``'probability'``. SmoothGrad uses probability for all tasks by default. resize_to (int, optional): Images will be rescaled with the shorter edge being ``resize_to``. Defaults to ``224``. crop_to (int, optional): After resize, images will be center cropped to a square image with the size ``crop_to``. If None, no crop will be performed. Defaults to ``None``. visual (bool, optional): Whether or not to visualize the processed image. Default: ``True``. save_path (str, optional): The filepath(s) to save the processed image(s). If None, the image will not be saved. Default: ``None``. Returns: np.ndarray: the explanation result. """ imgs, data = images_transform_pipeline(inputs, resize_to, crop_to) # print(imgs.shape, data.shape, imgs.dtype, data.dtype) # (1, 224, 224, 3) (1, 3, 224, 224) uint8 float32 assert len(data) == 1, "interpret each sample individually, it is optimized." self._build_predict_fn(gradient_of=gradient_of) # obtain the labels (and initialization). _, predicted_label, predicted_proba = self.predict_fn(data, labels) self.predicted_label = predicted_label self.predicted_proba = predicted_proba if labels is None: labels = predicted_label labels = np.array(labels).reshape((1, )) # SmoothGrad max_axis = tuple(np.arange(1, data.ndim)) stds = noise_amount * (np.max(data, axis=max_axis) - np.min(data, axis=max_axis)) data_noised = [] for i in range(n_samples): noise = np.concatenate( [np.float32(np.random.normal(0.0, stds[j], (1, ) + tuple(d.shape))) for j, d in enumerate(data)]) data_noised.append(data + noise) data_noised = np.concatenate(data_noised, axis=0) # print(data_i.shape, labels.shape) # print(data_noised.shape) # n_samples, 3, 224, 224 # splits, to avoid large GPU memory usage. if split > 1: chunk = n_samples // split gradient_chunks = [] for i in range(split - 1): gradients_i, _, _ = self.predict_fn(data_noised[i * chunk:(i + 1) * chunk], np.repeat(labels, chunk)) gradient_chunks.append(gradients_i) gradients_s, _, _ = self.predict_fn(data_noised[chunk * (split - 1):], np.repeat(labels, n_samples - chunk * (split - 1))) gradient_chunks.append(gradients_s) gradients = np.concatenate(gradient_chunks, axis=0) else: # one split. gradients, _, _ = self.predict_fn(data_noised, np.repeat(labels, n_samples)) avg_gradients = np.mean(gradients, axis=0, keepdims=True) # visualize and save image. if save_path is None and not visual: # no need to visualize or save explanation results. pass else: save_path = preprocess_save_path(save_path, 1) # print(imgs[i].shape, avg_gradients[i].shape) vis_explanation = explanation_to_vis(imgs[i], np.abs(avg_gradients[0]).sum(0), style='overlay_grayscale') if visual: show_vis_explanation(vis_explanation) if save_path[i] is not None: save_image(save_path[i], vis_explanation) # intermediate results, for possible further usages. self.labels = labels return avg_gradients