docstr: Compute numerical gradients of the attack loss term:
  `dL/dx = (dL/dP)*(dP/dx)` with `L = loss_attack_s; P = predict; x = adv_s`
  
  Parameters
  ----------
  X
      Instance around which gradient is evaluated.
  Y
      One-hot representation of instance labels.
  
  Returns
  -------
  Array with gradients.
docstr_tok: ["Compute","numerical","gradients","of","the","attack","loss","term",":","`","dL\/dx","=","(","dL\/dP",")","*","(","dP\/dx",")","`","with","`","L","=","loss_attack_s",";","P","=","predict",";","x","=","adv_s","`","Parameters","--","--","--","--","--","X","Instance","around","which","gradient","is","evaluated",".","Y","One-hot","representation","of","instance","labels",".","Returns","--","--","--","-","Array","with","gradients","."]
code: def get_gradients(self, X: np.ndarray, Y: np.ndarray) -> np.ndarray:
      """
      Compute numerical gradients of the attack loss term:
      `dL/dx = (dL/dP)*(dP/dx)` with `L = loss_attack_s; P = predict; x = adv_s`
  
      Parameters
      ----------
      X
          Instance around which gradient is evaluated.
      Y
          One-hot representation of instance labels.
  
      Returns
      -------
      Array with gradients.
      """
      # N = gradient batch size; F = nb of features; P = nb of prediction classes; B = instance batch size
      # dL/dP -> BxP
      preds = self.predict(X)  # NxP
      preds_pert_pos, preds_pert_neg = self.perturb(
          preds, self.eps[0], proba=True
      )  # (N*P)xP
  
      def f(preds_pert):
          return np.sum(Y * preds_pert, axis=1)
  
      def g(preds_pert):
          return np.max((1 - Y) * preds_pert, axis=1)
  
      # find instances where the gradient is 0
      idx_nograd = np.where(f(preds) - g(preds) <= -self.kappa)[0]
      if len(idx_nograd) == X.shape[0]:
          return np.zeros(self.shape)
  
      dl_df = f(preds_pert_pos) - f(preds_pert_neg)  # N*P
      dl_dg = g(preds_pert_pos) - g(preds_pert_neg)  # N*P
      dl_dp = dl_df - dl_dg  # N*P
      dl_dp = np.reshape(dl_dp, (X.shape[0], -1)) / (
          2 * self.eps[0]
      )  # NxP
  
      # dP/dx -> PxF
      X_pert_pos, X_pert_neg = self.perturb(
          X, self.eps[1], proba=False
      )  # (N*F)x(shape of X[0])
      X_pert = np.concatenate([X_pert_pos, X_pert_neg], axis=0)
      preds_concat = self.predict(X_pert)
      n_pert = X_pert_pos.shape[0]
      dp_dx = preds_concat[:n_pert] - preds_concat[n_pert:]  # (N*F)*P
      dp_dx = np.reshape(
          np.reshape(dp_dx, (X.shape[0], -1)),
          (X.shape[0], preds.shape[1], -1),
          order="F",
      ) / (
          2 * self.eps[1]
      )  # NxPxF
  
      # dL/dx -> Bx(shape of X[0])
      grads = np.einsum("ij,ijk->ik", dl_dp, dp_dx)  # NxF
      # set instances where gradient is 0 to 0
      if len(idx_nograd) > 0:
          grads[idx_nograd] = np.zeros(grads.shape[1:])
      grads = np.mean(grads, axis=0)  # B*F
      grads = np.reshape(
          grads, (self.batch_size,) + self.shape[1:]
      )  # B*(shape of X[0])
      return grads
  
file_path: alibi/alibi/explainers/cem.py
imports: import copy
  import logging
  import sys
  from typing import Any, Callable, Optional, Tuple, Union
  import numpy
  import tensorflow.compat.v1
  from alibi.api.defaults import DEFAULT_DATA_CEM, DEFAULT_META_CEM
  from alibi.api.interfaces import Explainer, Explanation, FitMixin
label_desc: Use image node_id 5 for calling the CEM obj's underlying member method code with example usage: obj.get_gradients(X, Y) and returns: grads, np, np, np