docstr: Creates a tensor operation to calculate expected gradients with respect to the provided input operation.
  This will throw an error if you haven't first called input_to_samples_delta.
  
  Args:
      output_op: The output layer of your model, or the layer to take the expected gradients with respect to.
      cond_input_op: An operation that returns interpolations between inputs and background reference samples.
                     The output from input_to_samples_delta.
      sparse_labels_op: For multi-class problems, the true labels of your input data. Assumes that there are a discrete
                        number of classes from 0 to num_classes and this tensor provides the integer label (NOT ONE HOT ENCODED)
                        for each batch of input. This tensor is used to index into the gradient operation such that the
                        explanations returned for each sample are of its true class. You can also manipulate 
                        this operation to get explanations for a specific class, or set it to 
                        None to get an operation that returns explanations for multiple classes.
  Returns:
      expected_grads: A tensor of shape (None, ...), the same shape as an input batch. The expected gradients with 
                      respect to the input batch.
code: def shap_value_op(
      self, output_op, cond_input_op, sparse_labels_op=None
  ):
      """
      Creates a tensor operation to calculate expected gradients with respect to the provided input operation.
      This will throw an error if you haven't first called input_to_samples_delta.
  
      Args:
          output_op: The output layer of your model, or the layer to take the expected gradients with respect to.
          cond_input_op: An operation that returns interpolations between inputs and background reference samples.
                         The output from input_to_samples_delta.
          sparse_labels_op: For multi-class problems, the true labels of your input data. Assumes that there are a discrete
                            number of classes from 0 to num_classes and this tensor provides the integer label (NOT ONE HOT ENCODED)
                            for each batch of input. This tensor is used to index into the gradient operation such that the
                            explanations returned for each sample are of its true class. You can also manipulate
                            this operation to get explanations for a specific class, or set it to
                            None to get an operation that returns explanations for multiple classes.
      Returns:
          expected_grads: A tensor of shape (None, ...), the same shape as an input batch. The expected gradients with
                          respect to the input batch.
      """
      multi_output = True
      if len(output_op.shape) == 1:
          multi_output = False
  
      refs_per_input = self.samples_delta.get_shape().as_list()[1]
  
      if multi_output:
          if sparse_labels_op is None:
              print(
                  "You have requested multi-class values, but have not provided a labels tensor. This may be memory intensive..."
              )
  
              # Of shape (None, num_classes, ...)
              gradient_tensor = self._grad_across_multi_output(
                  output_op, cond_input_op
              )
              gradient_tensor = tf.reshape(
                  gradient_tensor,
                  shape=[-1, refs_per_input, output_op.shape[-1]]
                  + cond_input_op.get_shape().as_list()[1:],
                  name="gradient_tensor",
              )
              mult_grads = tf.expand_dims(self.samples_delta, axis=2)
              mult_grads = tf.multiply(
                  mult_grads, gradient_tensor, name="mult_grads"
              )
              expected_grads = tf.reduce_mean(mult_grads, axis=1)
              return expected_grads
  
          with tf.device("/cpu:0"):
              sparse_labels_op = tf.tile(
                  tf.expand_dims(sparse_labels_op, axis=1),
                  (1, refs_per_input),
              )
              sparse_labels_op = tf.reshape(
                  sparse_labels_op,
                  (tf.shape(cond_input_op)[0],),
                  name="sparse_labels_op",
              )
  
          # Gradients will be of shape (None, ...)
          gradient_tensor = self._grad_across_multi_output(
              output_op, cond_input_op, sparse_labels_op
          )
      else:
          assert (
              sparse_labels_op is None
          ), "You have passed in a sparse_labels_op, but your model is not multi-output"
          # Gradients will be of shape (None, ...) - same shape as input
          gradient_tensor = tf.gradients(output_op, cond_input_op)[0]
  
      # Reshape the gradient tensor into (None, k, ...) so that we can average across the k.
      # Note: it is important that the axis ordering is (None, k, ...). Keeping the k before
      # the feature dimensions allows the reshaping after applying the model output to behave correctly.
      # This is because the right-most dimensions are squashed first when reshaping.
      gradient_tensor = tf.reshape(
          gradient_tensor,
          shape=[-1, refs_per_input]
          + gradient_tensor.get_shape().as_list()[1:],
          name="gradient_tensor",
      )
  
      # Multiply gradients and input-reference difference
      mult_grads = tf.multiply(
          self.samples_delta, gradient_tensor, name="mult_grads"
      )
  
      # Average over k, the background references
      expected_grads = tf.reduce_mean(
          mult_grads, axis=1, name="expected_grads"
      )
      return expected_grads
  
file_path: attributionpriors/attributionpriors/ops.py
imports: import tensorflow
  import numpy
label_desc: Use image node_id 5 for calling the AttributionPriorExplainer obj's underlying member method code with example usage: obj.shap_value_op(output_op, cond_input_op, sparse_labels_op) and returns: expected_grads, expected_grads