class_docstr: This class implements a Classifier extension that wraps a classifier and a detector.
  More details in https://arxiv.org/abs/1705.07263
class_docstr_tok: ["This","class","implements","a","Classifier","extension","that","wraps","a","classifier","and","a","detector",".","More","details","in","https",":","\/\/arxiv.org\/abs\/1705.07263"]
docstr: Compute per-class derivatives w.r.t. `x`.
  
  :param x: Sample input with shape as expected by the model.
  :param label: Index of a specific per-class derivative. If an integer is provided, the gradient of that class
                output is computed for all samples. If multiple values as provided, the first dimension should
                match the batch size of `x`, and each value will be used as target for its corresponding sample in
                `x`. If `None`, then gradients for all classes will be computed for each sample.
  :param training_mode: `True` for model set to training mode and `'False` for model set to evaluation mode.
  :return: Array of gradients of input features w.r.t. each class in the form
           `(batch_size, nb_classes, input_shape)` when computing for all classes, otherwise shape becomes
           `(batch_size, 1, input_shape)` when `label` parameter is specified.
docstr_tok: ["Compute","per-class","derivatives","w.r.t",".","`","x","`",".",":","param","x",":","Sample","input","with","shape","as","expected","by","the","model",".",":","param","label",":","Index","of","a","specific","per-class","derivative",".","If","an","integer","is","provided",",","the","gradient","of","that","class","output","is","computed","for","all","samples",".","If","multiple","values","as","provided",",","the","first","dimension","should","match","the","batch","size","of","`","x","`",",","and","each","value","will","be","used","as","target","for","its","corresponding","sample","in","`","x","`",".","If","`","None","`",",","then","gradients","for","all","classes","will","be","computed","for","each","sample",".",":","param","training_mode",":","`","True","`","for","model","set","to","training","mode","and","`","'False","`","for","model","set","to","evaluation","mode",".",":","return",":","Array","of","gradients","of","input","features","w.r.t",".","each","class","in","the","form","`","(","batch_size",",","nb_classes",",","input_shape",")","`","when","computing","for","all","classes",",","otherwise","shape","becomes","`","(","batch_size",",","1",",","input_shape",")","`","when","`","label","`","parameter","is","specified","."]
code: def class_gradient(  # pylint: disable=W0221
      self,
      x: np.ndarray,
      label: Optional[Union[int, List[int], np.ndarray]] = None,
      training_mode: bool = False,
      **kwargs,
  ) -> np.ndarray:
      """
      Compute per-class derivatives w.r.t. `x`.
  
      :param x: Sample input with shape as expected by the model.
      :param label: Index of a specific per-class derivative. If an integer is provided, the gradient of that class
                    output is computed for all samples. If multiple values as provided, the first dimension should
                    match the batch size of `x`, and each value will be used as target for its corresponding sample in
                    `x`. If `None`, then gradients for all classes will be computed for each sample.
      :param training_mode: `True` for model set to training mode and `'False` for model set to evaluation mode.
      :return: Array of gradients of input features w.r.t. each class in the form
               `(batch_size, nb_classes, input_shape)` when computing for all classes, otherwise shape becomes
               `(batch_size, 1, input_shape)` when `label` parameter is specified.
      """
      if not (  # pragma: no cover
          (label is None)
          or (
              isinstance(label, int) and label in range(self.nb_classes)
          )
          or (
              isinstance(label, np.ndarray)
              and len(label.shape) == 1
              and (label < self.nb_classes).all()
              and label.shape[0] == x.shape[0]
          )
      ):
          raise ValueError(f"Label {label} is out of range.")
  
      # Compute the gradient and return
      if label is None:
          combined_grads = self._compute_combined_grads(x, label=None)
  
      elif isinstance(label, int):
          if label < self.nb_classes - 1:
              # Compute and return from the classifier gradients
              combined_grads = self.classifier.class_gradient(
                  x=x,
                  label=label,
                  training_mode=training_mode,
                  **kwargs,
              )
  
          else:
              # First compute the classifier gradients
              classifier_grads = self.classifier.class_gradient(
                  x=x, label=None, training_mode=training_mode, **kwargs
              )
  
              # Then compute the detector gradients
              detector_grads = self.detector.class_gradient(
                  x=x, label=0, training_mode=training_mode, **kwargs
              )
  
              # Chain the detector gradients for the first component
              classifier_preds = self.classifier.predict(x=x)
              maxind_classifier_preds = np.argmax(
                  classifier_preds, axis=1
              )
              max_classifier_preds = classifier_preds[
                  np.arange(x.shape[0]), maxind_classifier_preds
              ]
              first_detector_grads = (
                  max_classifier_preds[:, None, None, None, None]
                  * detector_grads
              )
  
              # Chain the detector gradients for the second component
              max_classifier_grads = classifier_grads[
                  np.arange(len(classifier_grads)),
                  maxind_classifier_preds,
              ]
              detector_preds = self.detector.predict(x=x)
              second_detector_grads = (
                  max_classifier_grads
                  * (detector_preds + 1)[:, None, None]
              )
              second_detector_grads = second_detector_grads[None, ...]
              second_detector_grads = np.swapaxes(
                  second_detector_grads, 0, 1
              )
  
              # Update detector gradients
              combined_grads = (
                  first_detector_grads + second_detector_grads
              )
  
      else:
          # Compute indexes for classifier labels and detector labels
          classifier_idx = np.where(label < self.nb_classes - 1)
          detector_idx = np.where(label == self.nb_classes - 1)
  
          # Initialize the combined gradients
          combined_grads = np.zeros(
              shape=(x.shape[0], 1, x.shape[1], x.shape[2], x.shape[3])
          )
  
          # First compute the classifier gradients for classifier_idx
          if classifier_idx:
              combined_grads[
                  classifier_idx
              ] = self.classifier.class_gradient(
                  x=x[classifier_idx],
                  label=label[classifier_idx].tolist(),
                  training_mode=training_mode,
                  **kwargs,
              )
  
          # Then compute the detector gradients for detector_idx
          if detector_idx:
              # First compute the classifier gradients for detector_idx
              classifier_grads = self.classifier.class_gradient(
                  x=x[detector_idx],
                  label=None,
                  training_mode=training_mode,
                  **kwargs,
              )
  
              # Then compute the detector gradients for detector_idx
              detector_grads = self.detector.class_gradient(
                  x=x[detector_idx],
                  label=0,
                  training_mode=training_mode,
                  **kwargs,
              )
  
              # Chain the detector gradients for the first component
              classifier_preds = self.classifier.predict(
                  x=x[detector_idx]
              )
              maxind_classifier_preds = np.argmax(
                  classifier_preds, axis=1
              )
              max_classifier_preds = classifier_preds[
                  np.arange(len(detector_idx)), maxind_classifier_preds
              ]
              first_detector_grads = (
                  max_classifier_preds[:, None, None, None, None]
                  * detector_grads
              )
  
              # Chain the detector gradients for the second component
              max_classifier_grads = classifier_grads[
                  np.arange(len(classifier_grads)),
                  maxind_classifier_preds,
              ]
              detector_preds = self.detector.predict(x=x[detector_idx])
              second_detector_grads = (
                  max_classifier_grads
                  * (detector_preds + 1)[:, None, None]
              )
              second_detector_grads = second_detector_grads[None, ...]
              second_detector_grads = np.swapaxes(
                  second_detector_grads, 0, 1
              )
  
              # Update detector gradients
              detector_grads = (
                  first_detector_grads + second_detector_grads
              )
  
              # Reassign the combined gradients
              combined_grads[detector_idx] = detector_grads
  
      return combined_grads
  
imports: from __future__ import absolute_import, division, print_function, unicode_literals
  import logging
  from typing import List, Optional, Union, Tuple, TYPE_CHECKING
  import numpy
  from art.estimators.classification.classifier import ClassifierNeuralNetwork
label_desc: Use image node_id 6 for calling the DetectorClassifier obj's underlying member method code with example usage: obj.class_gradient(x, label, training_mode) and returns: combined_grads