code: def shap_values(
      self,
      X,
      ranked_outputs=None,
      output_rank_order="max",
      check_additivity=True,
  ):
      # X ~ self.model_input
      # X_data ~ self.data
  
      # check if we have multiple inputs
      if not self.multi_input:
          assert not isinstance(
              X, list
          ), "Expected a single tensor model input!"
          X = [X]
      else:
          assert isinstance(X, list), "Expected a list of model inputs!"
  
      X = [x.detach().to(self.device) for x in X]
  
      model_output_values = None
  
      if ranked_outputs is not None and self.multi_output:
          with torch.no_grad():
              model_output_values = self.model(*X)
          # rank and determine the model outputs that we will explain
          if output_rank_order == "max":
              _, model_output_ranks = torch.sort(
                  model_output_values, descending=True
              )
          elif output_rank_order == "min":
              _, model_output_ranks = torch.sort(
                  model_output_values, descending=False
              )
          elif output_rank_order == "max_abs":
              _, model_output_ranks = torch.sort(
                  torch.abs(model_output_values), descending=True
              )
          else:
              emsg = "output_rank_order must be max, min, or max_abs!"
              raise ValueError(emsg)
          model_output_ranks = model_output_ranks[:, :ranked_outputs]
      else:
          model_output_ranks = (
              torch.ones((X[0].shape[0], self.num_outputs)).int()
              * torch.arange(0, self.num_outputs).int()
          )
  
      # add the gradient handles
      handles = self.add_handles(
          self.model, add_interim_values, deeplift_grad
      )
      if self.interim:
          self.add_target_handle(self.layer)
  
      # compute the attributions
      output_phis = []
      for i in range(model_output_ranks.shape[1]):
          phis = []
          if self.interim:
              for k in range(len(self.interim_inputs_shape)):
                  phis.append(
                      np.zeros(
                          (X[0].shape[0],)
                          + self.interim_inputs_shape[k][1:]
                      )
                  )
          else:
              for k in range(len(X)):
                  phis.append(np.zeros(X[k].shape))
          for j in range(X[0].shape[0]):
              # tile the inputs to line up with the background data samples
              tiled_X = [
                  X[t][j : j + 1].repeat(
                      (self.data[t].shape[0],)
                      + tuple([1 for k in range(len(X[t].shape) - 1)])
                  )
                  for t in range(len(X))
              ]
              joint_x = [
                  torch.cat((tiled_X[t], self.data[t]), dim=0)
                  for t in range(len(X))
              ]
              # run attribution computation graph
              feature_ind = model_output_ranks[j, i]
              sample_phis = self.gradient(feature_ind, joint_x)
              # assign the attributions to the right part of the output arrays
              if self.interim:
                  sample_phis, output = sample_phis
                  x, data = [], []
                  for k in range(len(output)):
                      x_temp, data_temp = np.split(output[k], 2)
                      x.append(x_temp)
                      data.append(data_temp)
                  for t in range(len(self.interim_inputs_shape)):
                      phis[t][j] = (
                          sample_phis[t][self.data[t].shape[0] :]
                          * (x[t] - data[t])
                      ).mean(0)
              else:
                  for t in range(len(X)):
                      phis[t][j] = (
                          (
                              torch.from_numpy(
                                  sample_phis[t][
                                      self.data[t].shape[0] :
                                  ]
                              ).to(self.device)
                              * (X[t][j : j + 1] - self.data[t])
                          )
                          .cpu()
                          .detach()
                          .numpy()
                          .mean(0)
                      )
          output_phis.append(phis[0] if not self.multi_input else phis)
      # cleanup; remove all gradient handles
      for handle in handles:
          handle.remove()
      self.remove_attributes(self.model)
      if self.interim:
          self.target_handle.remove()
  
      # check that the SHAP values sum up to the model output
      if check_additivity:
          if model_output_values is None:
              with torch.no_grad():
                  model_output_values = self.model(*X)
  
          _check_additivity(
              self, model_output_values.cpu(), output_phis
          )
  
      if not self.multi_output:
          return output_phis[0]
      elif ranked_outputs is not None:
          return output_phis, model_output_ranks
      else:
          return output_phis
  
file_path: shap/shap/explainers/_deep/deep_pytorch.py
imports: import warnings
  import numpy
  from packaging import version
  from .._explainer import Explainer
  from .deep_utils import _check_additivity
label_desc: Use image node_id 6 for calling the PyTorchDeep obj's underlying member method code with example usage: obj.shap_values(X, ranked_outputs, output_rank_order, check_additivity) and returns: output_phis, output_phis,  model_output_ranks, output_phis