def _get_dict(self, values, keys):
     if isinstance(values, dict):
         _values = defaultdict(list, values)
         for key in keys:
             _values[key] = listify(_values[key])
     else:
         _values = defaultdict(list)
         values = listify(values)
         for k in keys:
             _values[k] = values
     return _values
 def _get_seed_inputs(self, seed_inputs, input_ranges):
     # Prepare input_ranges
     input_ranges = ((low, 1.) if high is None else (low, high)
                     for (low, high) in input_ranges)
     input_ranges = ((0., high) if low is None else (low, high)
                     for (low, high) in input_ranges)
     input_ranges = ((low, high, (high - low) * 0.1)
                     for (low, high) in input_ranges)
     # Prepare input_shape
     input_shapes = (input_tensor.shape[1:]
                     for input_tensor in self.model.inputs)
     # Prepare seed_inputs
     if seed_inputs is None or len(seed_inputs) == 0:
         seed_inputs = [None] * len(self.model.inputs)
         seed_inputs = (tf.random.uniform(shape, low + margin,
                                          high - margin)
                        for X, (low, high, margin), shape in zip(
                            seed_inputs, input_ranges, input_shapes))
     else:
         seed_inputs = listify(seed_inputs)
     # Convert numpy to tf-tensor
     seed_inputs = (
         X if tf.is_tensor(X) else tf.Variable(X, dtype=input_tensor.dtype)
         for X, input_tensor in zip(seed_inputs, self.model.inputs))
     # Do expand_dims when tensor doesn't have the dim for samples
     seed_inputs = (
         tf.expand_dims(X, axis=0)
         if len(X.shape) < len(input_tensor.shape) else X
         for X, input_tensor in zip(seed_inputs, self.model.inputs))
     return list(seed_inputs)
 def _get_seed_inputs(self, seed_inputs, input_ranges):
     # Prepare seed_inputs
     if seed_inputs is None or len(seed_inputs) == 0:
         # Replace None to 0.0-1.0 or any properly value
         input_ranges = ((0., 1.) if low is None and high is None else
                         (low, high) for low, high in input_ranges)
         input_ranges = ((high - np.abs(high / 2.0),
                          high) if low is None else (low, high)
                         for low, high in input_ranges)
         input_ranges = ((low, low + np.abs(low * 2.0)) if high is None else
                         (low, high) for low, high in input_ranges)
         input_ranges = list(input_ranges)
         # Prepare input_shape
         input_shapes = (input_tensor.shape[1:]
                         for input_tensor in self.model.inputs)
         # Generae seed-inputs
         seed_inputs = (tf.random.uniform(
             shape, low,
             high) for (low,
                        high), shape in zip(input_ranges, input_shapes))
     else:
         seed_inputs = listify(seed_inputs)
     # Convert numpy to tf-tensor
     seed_inputs = (
         tf.constant(X, dtype=input_tensor.dtype)
         for X, input_tensor in zip(seed_inputs, self.model.inputs))
     # Do expand_dims when tensor doesn't have the dim for samples
     seed_inputs = (
         tf.expand_dims(X, axis=0)
         if len(X.shape) < len(input_tensor.shape) else X
         for X, input_tensor in zip(seed_inputs, self.model.inputs))
     return list(seed_inputs)
Exemple #4
0
 def _get_losses_for_multiple_outputs(self, loss):
     losses = listify(loss)
     if len(losses) == 1 and len(losses) < len(self.model.outputs):
         losses = losses * len(self.model.outputs)
     if len(losses) != len(self.model.outputs):
         raise ValueError(('The model has {} outputs, '
                           'but the number of loss-functions you passed is {}.').format(
                               len(self.model.outputs), len(losses)))
     return losses
Exemple #5
0
 def _get_gradients(self, seed_inputs, losses, gradient_modifier):
     with tf.GradientTape() as tape:
         tape.watch(seed_inputs)
         outputs = self.model(seed_inputs)
         outputs = listify(outputs)
         loss_values = [loss(output) for output, loss in zip(outputs, losses)]
     grads = tape.gradient(loss_values, seed_inputs)
     if gradient_modifier is not None:
         grads = [gradient_modifier(g) for g in grads]
     return grads
Exemple #6
0
 def _get_seed_inputs_for_multiple_inputs(self, seed_input):
     seed_inputs = listify(seed_input)
     if len(seed_inputs) != len(self.model.inputs):
         raise ValueError(('The model has {} inputs, '
                           'but the number of seed-inputs tensors you passed is {}.').format(
                               len(self.model.inputs), len(seed_inputs)))
     seed_inputs = (x if tf.is_tensor(x) else tf.constant(x) for x in seed_inputs)
     seed_inputs = (tf.expand_dims(x, axis=0) if len(x.shape) == len(tensor.shape[1:]) else x
                    for x, tensor in zip(seed_inputs, self.model.inputs))
     return list(seed_inputs)
Exemple #7
0
 def _prepare_list(self,
                   value,
                   list_length_if_created,
                   empty_list_if_none=True,
                   convert_tuple_to_list=True):
     values = listify(value,
                      empty_list_if_none=empty_list_if_none,
                      convert_tuple_to_list=convert_tuple_to_list)
     if len(values) == 1 and list_length_if_created > 1:
         values = values * list_length_if_created
     return values
 def _prepare_inputmodifier_dictionary(self, input_modifier):
     input_modifiers = listify(input_modifier)
     input_modifiers = self._prepare_dictionary(
         input_modifiers, [l.name for l in self.model.inputs])
     if len(input_modifiers) != 0 and len(input_modifiers) != len(
             self.model.inputs):
         raise ValueError(
             'The model has {} inputs, but you passed {} as input_modifiers. '
             'When the model has multipul inputs, '
             'you must pass a dictionary as input_modifiers.'.format(
                 len(self.model.inputs), input_modifier))
     return input_modifiers
 def _get_input_ranges(self, input_range):
     input_ranges = listify(input_range,
                            return_empty_list_if_none=False,
                            convert_tuple_to_list=False)
     if len(input_ranges) == 1 and len(self.model.inputs) > 1:
         input_ranges = input_ranges * len(self.model.inputs)
     input_ranges = [(None, None) if r is None else r for r in input_ranges]
     for i, r in enumerate(input_ranges):
         if len(r) != 2:
             raise ValueError(
                 'The length of input range tuple must be 2 (Or it is just `None`, not tuple), '
                 'but you passed {} as `input_ranges[{}]`.'.format(r, i))
     return input_ranges
Exemple #10
0
 def _get_gradients(self, seed_inputs, losses, gradient_modifier, training):
     with tf.GradientTape(watch_accessed_variables=False,
                          persistent=True) as tape:
         tape.watch(seed_inputs)
         outputs = self.model(seed_inputs, training=training)
         outputs = listify(outputs)
         loss_values = [
             loss(output) for output, loss in zip(outputs, losses)
         ]
     grads = tape.gradient(
         loss_values,
         seed_inputs,
         unconnected_gradients=tf.UnconnectedGradients.ZERO)
     if gradient_modifier is not None:
         grads = [gradient_modifier(g) for g in grads]
     return grads
Exemple #11
0
 def _prepare_dictionary(self,
                         values,
                         keys,
                         default_value=list,
                         empty_list_if_none=True,
                         convert_tuple_to_list=True):
     if isinstance(values, dict):
         values = defaultdict(default_value, values)
     else:
         _values = defaultdict(default_value)
         for k in keys:
             _values[k] = values
         values = _values
     for key in values.keys():
         values[key] = listify(values[key],
                               empty_list_if_none=empty_list_if_none,
                               convert_tuple_to_list=convert_tuple_to_list)
     return values
    def __call__(self,
                 loss,
                 seed_input=None,
                 input_range=(0, 255),
                 input_modifiers=[Jitter(2), Rotate(2)],
                 regularizers=[TotalVariation(10.),
                               L2Norm(10.)],
                 steps=200,
                 optimizer=tf.optimizers.RMSprop(1., 0.95),
                 normalize_gradient=True,
                 gradient_modifier=None,
                 callbacks=None,
                 training=False):
        """Generate the model inputs that maximize the output of the given `loss` functions.

        # Arguments
            loss: A loss function. If the model has multiple outputs, you can use a different
                loss on each output by passing a list of losses. The loss value that will be
                maximized will then be the sum of all individual losses
                (and all regularization values).
            seed_input: `None`(default) or An N-dim Numpy array. When `None`, the seed_input
                value will be generated with randome noise.  If the model has multiple inputs,
                you have to pass a list of N-dim Numpy arrays.
            input_range: A tuple that specifies the input range as a `(min, max)` tuple. If the
                model has multiple inputs, you can use a different input range on each input by
                passing as list of input ranges. When `None` or a `(None, None)` tuple, the range of
                a input value (i.e., the result of this function) will be no applied any limitation.
            input_modifiers: A input modifier function or a list of input modifier functions.
                You can also use a instance of `tf_keras-vis.input_modifiers. InputModifier`'s
                subclass, instead of a function. If the model has multiple inputs, you have to pass
                a dictionary of input modifier functions or instances on each model inputs:
                such as `input_modifiers={'input_a': [ input_modifier_a_1, input_modifier_a_2 ],
                'input_b': input_modifier_b, ... }`.
            regularizers: A regularization function or a list of regularization functions. You can
                also use a instance of `tf_keras-vis.regularizers.Regularizer`'s subclass,
                instead of a function. A regularization value will be calculated with
                a corresponding model input will add to the loss value.
            steps: The number of gradient descent iterations.
            optimizer: A `tf.optimizers.Optimizer` instance.
            normalize_gradient: This option is disabled and will be removed at version 0.6.0.
            gradient_modifier: A function to modify gradients. This function is executed before
                normalizing gradients.
            callbacks: A `tf_keras_vis.callbacks.OptimizerCallback` instance or a list of them.
            training: A bool whether the model's trainig-mode turn on or off.
        # Returns
            An Numpy arrays when the model has a single input and `seed_input` is None or An N-dim
            Numpy Array, Or a list of Numpy arrays when otherwise. The Numpy is that the model
            inputs that maximize the out of `loss`.
        # Raises
            ValueError: In case of invalid arguments for `loss`, `input_range`, `input_modifiers`
                or `regularizers`.
        """
        # losses
        losses = self._get_losses_for_multiple_outputs(loss)

        # Get initial seed-inputs
        input_ranges = self._get_input_ranges(input_range)
        seed_inputs = self._get_seed_inputs(seed_input, input_ranges)

        # input_modifiers
        input_modifiers = self._get_input_modifiers(input_modifiers)

        # regularizers
        regularizers = self._get_regularizers(regularizers)

        callbacks = listify(callbacks)
        for callback in callbacks:
            callback.on_begin()

        for i in range(check_steps(steps)):
            # Apply input modifiers
            for j, name in enumerate(self.model.input_names):
                for modifier in input_modifiers[name]:
                    seed_inputs[j] = modifier(seed_inputs[j])

            seed_inputs = [tf.Variable(X) for X in seed_inputs]
            # Calculate gradients
            with tf.GradientTape(watch_accessed_variables=False) as tape:
                tape.watch(seed_inputs)
                outputs = self.model(seed_inputs, training=training)
                outputs = listify(outputs)
                loss_values = (loss(output)
                               for output, loss in zip(outputs, losses))
                loss_values = (tf.stack(loss_value, axis=0) if isinstance(
                    loss_value, (list, tuple)) else loss_value
                               for loss_value in loss_values)
                loss_values = [
                    K.mean(tf.reshape(loss_value, (X.shape[0], -1)), axis=1)
                    for loss_value, X in zip(loss_values, seed_inputs)
                ]
                # Calculate regularization values
                regularizations = [(regularizer.name, regularizer(seed_inputs))
                                   for regularizer in regularizers]
                regularized_loss_values = [
                    (-1. * loss_value) + sum([v for _, v in regularizations])
                    for loss_value in loss_values
                ]
            grads = tape.gradient(
                regularized_loss_values,
                seed_inputs,
                unconnected_gradients=tf.UnconnectedGradients.ZERO)
            grads = listify(grads)
            if gradient_modifier is not None:
                grads = (gradient_modifier(g) for g in grads)
            if normalize_gradient:
                # XXX `normalize_gradient` option is not working correctly.
                # XXX So this option will be removed as of version 0.6.0.
                # XXX For now, disable this option.
                # grads = (K.l2_normalize(g, axis=tuple(range(len(g))[1:])) for g in grads)
                pass
            optimizer.apply_gradients(zip(grads, seed_inputs))

            for callback in callbacks:
                callback(i,
                         self._apply_clip(seed_inputs, input_ranges),
                         grads,
                         loss_values,
                         outputs,
                         regularizations=regularizations,
                         overall_loss=regularized_loss_values)

        for callback in callbacks:
            callback.on_end()

        cliped_value = self._apply_clip(seed_inputs, input_ranges)
        if len(self.model.inputs) == 1 and (seed_input is None or
                                            not isinstance(seed_input, list)):
            cliped_value = cliped_value[0]

        return cliped_value
Exemple #13
0
 def __init__(self, indices, epsilon=0.05):
     super().__init__('CategoricalSmoothedScore')
     self.indices = listify(indices)
     self.epsilon = epsilon
Exemple #14
0
 def __init__(self, indices, depth):
     super().__init__('CategoricalScore')
     self.indices = listify(indices)
     self.depth = depth
Exemple #15
0
    def __call__(self,
                 loss,
                 seed_input,
                 penultimate_layer=-1,
                 activation_modifier=lambda cam: K.relu(cam),
                 normalize_gradient=True):
        """Generate a gradient based class activation map (CAM) by using positive gradient of
            penultimate_layer with respect to loss.

            For details on Grad-CAM, see the paper:
            [Grad-CAM: Why did you say that? Visual Explanations from Deep Networks via
            Gradient-based Localization](https://arxiv.org/pdf/1610.02391v1.pdf).

        # Arguments
            loss: A loss function. If the model has multipul outputs, you can use a different
                loss on each output by passing a list of losses.
            seed_input: An N-dim Numpy array. If the model has multipul inputs,
                you have to pass a list of N-dim Numpy arrays.
            penultimate_layer: A number of integer or a tf.keras.layers.Layer object.
            normalize_gradient: True to normalize gradients.
            activation_modifier: A function to modify gradients.
        # Returns
            The heatmap image or a list of their images that indicate the `seed_input` regions
                whose change would most contribute  the loss value,
        # Raises
            ValueError: In case of invalid arguments for `loss`, or `penultimate_layer`.
        """
        losses = self._prepare_losses(loss)
        seed_inputs = [
            x if tf.is_tensor(x) else tf.constant(x)
            for x in listify(seed_input)
        ]
        seed_inputs = [
            tf.expand_dims(seed_input, axis=0)
            if X.shape == input_tensor.shape[1:] else X
            for X, input_tensor in zip(seed_inputs, self.model.inputs)
        ]
        if len(seed_inputs) != len(self.model.inputs):
            raise ValueError('')

        penultimate_output_tensor = self._find_penultimate_output(
            self.model, penultimate_layer)
        model = tf.keras.Model(inputs=self.model.inputs,
                               outputs=self.model.outputs +
                               [penultimate_output_tensor])
        with tf.GradientTape() as tape:
            tape.watch(seed_inputs)
            outputs = model(seed_inputs)
            outputs = listify(outputs)
            loss_values = [loss(y) for y, loss in zip(outputs[:-1], losses)]
            penultimate_outputs = outputs[-1]
        grads = tape.gradient(loss_values, penultimate_outputs)
        if normalize_gradient:
            grads = K.l2_normalize(grads)
        weights = K.mean(grads, axis=tuple(np.arange(len(grads.shape))[1:-1]))
        cam = np.asarray([
            np.sum(o * w, axis=-1)
            for o, w in zip(penultimate_outputs, weights)
        ])
        if activation_modifier is not None:
            cam = activation_modifier(cam)
        input_dims_list = (X.shape[1:-1] for X in seed_inputs)
        output_dims = penultimate_outputs.shape[1:-1]
        zoom_factors = ([
            i / (j * 1.0) for i, j in iter(zip(input_dims, output_dims))
        ] for input_dims in input_dims_list)
        cams = [
            np.asarray([zoom(v, factor) for v in cam])
            for factor in zoom_factors
        ]
        if len(self.model.inputs) == 1 and not isinstance(seed_input, list):
            cams = cams[0]
        return cams
Exemple #16
0
    def __call__(self,
                 loss,
                 seed_input,
                 smooth_samples=0,
                 smooth_noise=0.20,
                 keepdims=False,
                 gradient_modifier=lambda grads: K.abs(grads)):
        """Generate an attention map that appears how output value changes with respect to a small
            change in input image pixels.
            See details: https://arxiv.org/pdf/1706.03825.pdf

        # Arguments
            loss: A loss function. If the model has multipul outputs, you can use a different
                loss on each output by passing a list of losses.
            seed_input: An N-dim Numpy array. If the model has multipul inputs,
                you have to pass a list of N-dim Numpy arrays.
            smooth_samples: The number of calculating gradients iterations. If set to zero,
                the noise for smoothing won't be generated.
            keepdims: A boolean that whether to keep the channels-dim or not.
            smooth_noise: Noise level that is recommended no tweaking when there is no reason.
            gradient_modifier: A function to modify gradients. By default, the function modify
                gradients to `absolute` values.
        # Returns
            The heatmap image indicating the `seed_input` regions whose change would most contribute
            towards maximizing the loss value, Or a list of their images.
            A list of Numpy arrays that the model inputs that maximize the out of `loss`.
        # Raises
            ValueError: In case of invalid arguments for `loss`, or `seed_input`.
        """
        losses = self._prepare_losses(loss)
        seed_inputs = [
            X if tf.is_tensor(X) else tf.constant(X)
            for X in listify(seed_input)
        ]
        seed_inputs = [
            tf.expand_dims(seed_input, axis=0)
            if X.shape == input_tensor.shape[1:] else X
            for X, input_tensor in zip(seed_inputs, self.model.inputs)
        ]
        if len(seed_inputs) != len(self.model.inputs):
            raise ValueError('')

        if smooth_samples > 0:
            axes = [tuple(range(1, len(X.shape))) for X in seed_inputs]
            sigmas = [
                smooth_noise * (np.max(X, axis=axis) - np.min(X, axis=axis))
                for X, axis in zip(seed_inputs, axes)
            ]
            total_gradients = (np.zeros_like(X) for X in seed_inputs)
            for i in range(check_steps(smooth_samples)):
                seed_inputs_plus_noise = [
                    tf.constant(
                        np.concatenate([
                            x + np.random.normal(0., s, (1, ) + x.shape)
                            for x, s in zip(X, sigma)
                        ])) for X, sigma in zip(seed_inputs, sigmas)
                ]
                gradients, loss_values, outputs = self._get_gradients(
                    self.model, seed_inputs_plus_noise, losses,
                    gradient_modifier)
                total_gradients = (
                    total + g for total, g in zip(total_gradients, gradients))
            grads = [g / smooth_samples for g in total_gradients]
        else:
            grads, loss_values, outputs = self._get_gradients(
                self.model, seed_inputs, losses, gradient_modifier)

        if not keepdims:
            grads = [np.max(g, axis=-1) for g in grads]
        if len(self.model.inputs) == 1 and not isinstance(seed_input, list):
            grads = grads[0]
        return grads
 def _get_regularizers(self, regularizer):
     regularizers = listify(regularizer)
     return regularizers
Exemple #18
0
 def __init__(self, indices, epsilon=0.05):
     super().__init__('SmoothedLoss')
     self.indices = listify(indices)
     self.epsilon = epsilon
Exemple #19
0
    def __call__(self,
                 loss,
                 seed_input,
                 penultimate_layer=-1,
                 seek_penultimate_conv_layer=True,
                 activation_modifier=lambda cam: K.relu(cam),
                 expand_cam=True,
                 batch_size=32,
                 max_N=None,
                 training=False):
        """Generate score-weighted class activation maps (CAM) by using gradient-free visualization method.

            For details on Score-CAM, see the paper:
            [Score-CAM: Score-Weighted Visual Explanations for Convolutional Neural Networks ]
            (https://arxiv.org/pdf/1910.01279.pdf).

        # Arguments
            loss: A loss function. If the model has multiple outputs, you can use a different
                loss on each output by passing a list of losses.
            seed_input: An N-dim Numpy array. If the model has multiple inputs,
                you have to pass a list of N-dim Numpy arrays.
            penultimate_layer: A number of integer or a tf.keras.layers.Layer object.
            seek_penultimate_conv_layer: True to seek the penultimate layter that is a subtype of
                `keras.layers.convolutional.Conv` class.
                If False, the penultimate layer is that was elected by penultimate_layer index.
            activation_modifier: A function to modify activations.
            expand_cam: True to expand cam to same as input image size.
                ![Note] Even if the model has multiple inputs, this function return only one cam
                value (That's, when `expand_cam` is True, multiple cam images are generated from
                a model that has multiple inputs).
            batch_size: Integer or None. Number of samples per batch.
                If unspecified, batch_size will default to 32.
            max_N: Integer or None. If None, we do NOT recommend, because it takes huge time.
                If not None, that's setting Integer, run as Faster-ScoreCAM.
                Set larger number, need more time to visualize CAM but to be able to get
                clearer attention images.
                (see for details: https://github.com/tabayashi0117/Score-CAM#faster-score-cam)
            training: A bool whether the model's trainig-mode turn on or off.
        # Returns
            The heatmap image or a list of their images that indicate the `seed_input` regions
                whose change would most contribute  the loss value,
        # Raises
            ValueError: In case of invalid arguments for `loss`, or `penultimate_layer`.
        """

        # Preparing
        losses = self._get_losses_for_multiple_outputs(loss)
        seed_inputs = self._get_seed_inputs_for_multiple_inputs(seed_input)
        penultimate_output_tensor = self._find_penultimate_output(
            penultimate_layer, seek_penultimate_conv_layer)
        # Processing score-cam
        penultimate_output = tf.keras.Model(inputs=self.model.inputs,
                                            outputs=penultimate_output_tensor)(
                                                seed_inputs, training=training)
        # For efficiently visualizing, extract maps that has a large variance.
        # This excellent idea is devised by tabayashi0117.
        # (see for details: https://github.com/tabayashi0117/Score-CAM#faster-score-cam)
        if max_N is not None and max_N > -1:
            activation_map_std = tf.math.reduce_std(
                penultimate_output,
                axis=tuple(range(penultimate_output.ndim)[1:-1]),
                keepdims=True)
            _, top_k_indices = tf.math.top_k(activation_map_std, max_N)
            top_k_indices, _ = tf.unique(tf.reshape(top_k_indices, (-1, )))
            penultimate_output = tf.gather(penultimate_output,
                                           top_k_indices,
                                           axis=-1)
        channels = penultimate_output.shape[-1]

        # Upsampling activation-maps
        penultimate_output = penultimate_output.numpy()
        input_shapes = [seed_input.shape for seed_input in seed_inputs]
        factors = (zoom_factor(penultimate_output.shape[:-1], input_shape[:-1])
                   for input_shape in input_shapes)
        upsampled_activation_maps = [
            zoom(penultimate_output, factor + (1, )) for factor in factors
        ]
        map_shapes = [
            activation_map.shape
            for activation_map in upsampled_activation_maps
        ]

        # Normalizing activation-maps
        min_activation_maps = (np.min(activation_map,
                                      axis=tuple(
                                          range(activation_map.ndim)[1:-1]),
                                      keepdims=True)
                               for activation_map in upsampled_activation_maps)
        max_activation_maps = (np.max(activation_map,
                                      axis=tuple(
                                          range(activation_map.ndim)[1:-1]),
                                      keepdims=True)
                               for activation_map in upsampled_activation_maps)
        normalized_activation_maps = (
            (activation_map - min_activation_map) /
            (max_activation_map - min_activation_map)
            for activation_map, min_activation_map, max_activation_map in zip(
                upsampled_activation_maps, min_activation_maps,
                max_activation_maps))

        # Masking inputs
        input_tile_axes = (
            (map_shape[-1], ) + tuple(np.ones(len(input_shape), np.int))
            for input_shape, map_shape in zip(input_shapes, map_shapes))
        mask_templates = (np.tile(
            seed_input,
            axes) for seed_input, axes in zip(seed_inputs, input_tile_axes))
        map_transpose_axes = ((len(map_shape) - 1, ) +
                              tuple(range(len(map_shape))[:-1])
                              for map_shape in map_shapes)
        masks = (np.transpose(activation_map, transpose_axis)
                 for activation_map, transpose_axis in zip(
                     normalized_activation_maps, map_transpose_axes))
        map_tile_axes = (
            tuple(np.ones(len(map_shape), np.int)) + (input_shape[-1], )
            for input_shape, map_shape in zip(input_shapes, map_shapes))
        masks = (np.tile(np.expand_dims(activation_map, axis=-1), tile_axis)
                 for activation_map, tile_axis in zip(masks, map_tile_axes))
        masked_seed_inputs = (
            mask_template * mask
            for mask_template, mask in zip(mask_templates, masks))
        masked_seed_inputs = [
            np.reshape(masked_seed_input, (-1, ) + masked_seed_input.shape[2:])
            for masked_seed_input in masked_seed_inputs
        ]

        # Predicting masked seed-inputs
        preds = self.model.predict(masked_seed_inputs, batch_size=batch_size)
        preds = (np.reshape(prediction, (channels, -1, prediction.shape[-1]))
                 for prediction in listify(preds))

        # Calculating weights
        weights = ([loss(p) for p in prediction]
                   for loss, prediction in zip(losses, preds))
        weights = (np.array(w, dtype=np.float32) for w in weights)
        weights = (np.reshape(w, (channels, -1)) for w in weights)
        weights = np.array(list(weights), dtype=np.float32)
        weights = np.sum(weights, axis=0)
        weights = np.transpose(weights, (1, 0))

        # Generate cam
        cam = K.batch_dot(penultimate_output, weights)
        if activation_modifier is not None:
            cam = activation_modifier(cam)

        if not expand_cam:
            return cam

        factors = (zoom_factor(cam.shape, X.shape) for X in seed_inputs)
        cam = [zoom(cam, factor) for factor in factors]
        if len(self.model.inputs) == 1 and not isinstance(seed_input, list):
            cam = cam[0]
        return cam
    def __call__(self,
                 loss,
                 seed_input=None,
                 input_range=(0, 255),
                 input_modifiers=[Jitter(2), Rotate(2)],
                 regularizers=[TotalVariation(10.),
                               L2Norm(10.)],
                 steps=200,
                 optimizer=tf.optimizers.RMSprop(1., 0.95),
                 normalize_gradient=True,
                 gradient_modifier=None,
                 callbacks=None):
        """Generate the model inputs that maximize the output of the given `loss` functions.

        # Arguments
            loss: A loss function. If the model has multipul outputs, you can use a different
                loss on each output by passing a list of losses. The loss value that will be
                maximized will then be the sum of all individual losses
                (and all regularization values).
            seed_input: `None`(default) or An N-dim Numpy array. When `None`, the seed_input
                value will be generated with randome noise.  If the model has multipul inputs,
                you have to pass a list of N-dim Numpy arrays.
            input_range: A tuple that specifies the input range as a `(min, max)` tuple. If the
                model has multipul inputs, you can use a different input range on each input by
                passing as list of input ranges. When `None` or a `(None, None)` tuple, the range of
                a input value (i.e., the result of this function) will be no applied any limitation.
            input_modifiers: A input modifier function or a list of input modifier functions.
                You can also use a instance of `tf_keras-vis.input_modifiers. InputModifier`'s
                subclass, instead of a function. If the model has multipul inputs, you have to pass
                a dictionary of input modifier functions or instances on each model inputs:
                such as `input_modifiers={'input_a': [ input_modifier_a_1, input_modifier_a_2 ],
                'input_b': input_modifier_b, ... }`.
            regularizers: A regularization function or a list of regularization functions. You can
                also use a instance of `tf_keras-vis.regularizers.Regularizer`'s subclass,
                instead of a function. If the model has multipul outputs, you can use
                a dictionary of regularization functions or instances on each model outputs:
                such as `regularizers={'output_a': [ regularizer_a_1, regularizer_a_2 ],
                'output_b': regularizer_b, ... }`. A regularization value will be calculated with
                a corresponding model input will add to the loss value.
            steps: The number of gradient descent iterations.
            optimizer: A `tf.optimizers.Optimizer` instance.
            normalize_gradient: True to normalize gradients. Normalization avoids too small or
                large gradients and ensures a smooth gradient descent process.
            gradient_modifier: A function to modify gradients. This function is executed before
                normalizing gradients.
            callbacks: A `tf_keras_vis.callbacks.OptimizerCallback` instance or a list of them.
        # Returns
            An Numpy arrays when the model has a single input and `seed_input` is None or An N-dim
            Numpy Array, Or a list of Numpy arrays when otherwise. The Numpy is that the model
            inputs that maximize the out of `loss`.
        # Raises
            ValueError: In case of invalid arguments for `loss`, `input_range`, `input_modifiers`
                or `regularizers`.
        """
        # losses
        losses = self._prepare_losses(loss)

        # Get initial seed-inputs
        input_ranges = self._prepare_input_ranges(input_range)
        seed_inputs = self._get_seed_inputs(seed_input, input_ranges)

        # input_modifiers
        input_modifiers = self._prepare_inputmodifier_dictionary(
            input_modifiers)

        # regularizers
        regularizers = self._prepare_regularizer_dictionary(regularizers)

        callbacks = listify(callbacks)
        for callback in callbacks:
            callback.on_begin()

        for i in range(check_steps(steps)):
            # Apply input modifiers
            for j, input_layer in enumerate(self.model.inputs):
                for modifier in input_modifiers[input_layer.name]:
                    seed_inputs[j] = modifier(seed_inputs[j])
            seed_inputs = [tf.Variable(X) for X in seed_inputs]

            # Calculate gradients
            with tf.GradientTape() as tape:
                tape.watch(seed_inputs)
                outputs = self.model(seed_inputs)
                outputs = listify(outputs)
                loss_values = [
                    loss(output) for output, loss in zip(outputs, losses)
                ]
                # Calculate regularization values
                regularization_values = [[
                    (regularizer.name, regularizer(seed_inputs))
                    for regularizer in regularizers[output_layer.name]
                ] for output_layer in self.model.outputs]
                ys = [(-1. * loss_value) +
                      sum([rv for (_, rv) in regularization_value])
                      for loss_value, regularization_value in zip(
                          loss_values, regularization_values)]
            grads = tape.gradient(ys, seed_inputs)
            grads = listify(grads)
            if gradient_modifier is not None:
                grads = [gradient_modifier(g) for g in grads]
            if normalize_gradient:
                grads = [K.l2_normalize(g) for g in grads]
            optimizer.apply_gradients(zip(grads, seed_inputs))

            for callback in callbacks:
                callback(i,
                         self._apply_clip(seed_inputs, input_ranges),
                         grads,
                         loss_values,
                         outputs,
                         regularizations=regularization_values,
                         overall_loss=ys)

        for callback in callbacks:
            callback.on_end()

        cliped_value = self._apply_clip(seed_inputs, input_ranges)
        if len(self.model.inputs) == 1 and (seed_input is None or
                                            not isinstance(seed_input, list)):
            cliped_value = cliped_value[0]

        return cliped_value