code: def get_total_attribution(
      model: LudwigModel,
      target_feature_name: str,
      target_idx: Optional[int],
      feature_inputs: List[Variable],
      baseline: List[torch.Tensor],
      nsamples: int,
      run_config: ExplanationRunConfig,
  ) -> Tuple[
      npt.NDArray[np.float64], Dict[str, List[List[Tuple[str, float]]]]
  ]:
      """Compute the total attribution for each input feature for each row in the input data.
  
      Args:
          model: The Ludwig model to explain.
          target_feature_name: The name of the target feature to explain.
          target_idx: The index of the target feature label to explain if the target feature is a category.
          feature_inputs: The preprocessed input data as a list of tensors of length [num_features].
          baseline: The baseline input data as a list of tensors of length [num_features].
          nsamples: The total number of samples in the input data.
  
      Returns:
          The token-attribution pair for each token in the input feature for each row in the input data. The members of
          the output tuple are structured as follows:
  
          `total_attribution_rows`: (npt.NDArray[np.float64]) of shape [num_rows, num_features]
          The total attribution for each input feature for each row in the input data.
  
          `feat_to_token_attributions`: (Dict[str, List[List[Tuple[str, float]]]]) with values of shape
          [num_rows, seq_len, 2]
  
          `total_attribution_global`: (npt.NDArray[np.float64]) of shape [num_features]
          The attribution for each input feature aggregated across all input data.
      """
      input_features: LudwigFeatureDict = model.model.input_features
  
      # Configure the explainer, which includes wrapping the model so its interface conforms to
      # the format expected by Captum.
      model.model.zero_grad()
      explanation_model = WrapperModule(
          model.model, target_feature_name
      )
  
      layers = []
      for feat_name, feat in input_features.items():
          if feat.type() in EMBEDDED_TYPES:
              # Get embedding layer from encoder, which is the first child of the encoder.
              target_layer = feat.encoder_obj.get_embedding_layer()
  
              # If the current layer matches any layer in the list, make a deep copy of the layer.
              if len(layers) > 0 and any(
                  target_layer == layer for layer in layers
              ):
                  # Replace the layer with a deep copy of the layer to ensure that the attributions unique for each input
                  # feature that uses a shared layer.
                  # Recommended here: https://github.com/pytorch/captum/issues/794#issuecomment-1093021638
                  replace_layer_with_copy(feat, target_layer)
                  target_layer = (
                      feat.encoder_obj.get_embedding_layer()
                  )  # get the new copy
          else:
              # Get the wrapped input layer.
              target_layer = explanation_model.input_maps.get(feat_name)
  
          layers.append(target_layer)
  
      explainer = LayerIntegratedGradients(explanation_model, layers)
  
      feature_inputs_splits = [
          ipt.split(run_config.batch_size) for ipt in feature_inputs
      ]
      baseline = [t.to(DEVICE) for t in baseline]
  
      total_attribution_rows = None
      total_attribution_global = None
      feat_to_token_attributions = defaultdict(list)
      for input_batch in zip(*feature_inputs_splits):
          input_batch = [ipt.to(DEVICE) for ipt in input_batch]
          attribution = explainer.attribute(
              tuple(input_batch),
              baselines=tuple(baseline),
              target=target_idx,
              # https://captum.ai/docs/faq#i-am-facing-out-of-memory-oom-errors-when-using-captum-how-do-i-resolve-this
              internal_batch_size=run_config.batch_size,
          )
  
          attributions_reduced = []
          for a in attribution:
              a_reduced = a.detach().cpu()
              if a_reduced.ndim == 2 or a_reduced.ndim == 3:
                  # Reduces category-level attributions of shape [batch_size, embedding_dim] by summing over the
                  # embedding dimension to get attributions of shape [batch_size].
                  # Reduces token-level attributions of shape [batch_size, sequence_length, embedding_dim] by summing
                  # over the embedding dimension to get attributions of shape [batch_size, sequence_length]. We keep
                  # the sequence dimension so we can map the attributions to the tokens.
                  a_reduced = a_reduced.sum(dim=-1)
              elif a_reduced.ndim == 4:
                  # Reduce pixel-level attributions of shape [batch_size, num_channels, height, width] by summing
                  # over the channel and spatial dimensions to get attributions of shape [batch_size].
                  a_reduced = a_reduced.sum(dim=(1, 2, 3))
              attributions_reduced.append(a_reduced)
  
          for inputs, attrs, (name, feat) in zip(
              input_batch, attributions_reduced, input_features.items()
          ):
              if feat.type() == TEXT:
                  tok_attrs = get_token_attributions(
                      model, name, inputs.detach().cpu(), attrs
                  )
                  feat_to_token_attributions[name].append(tok_attrs)
  
          # Reduce attribution to [num_input_features, batch_size] by summing over the sequence dimension (if present).
          attribution = [
              a.sum(dim=-1) if a.ndim == 2 else a
              for a in attributions_reduced
          ]
          attribution = np.stack(attribution)
  
          # Transpose to [batch_size, num_input_features]
          attribution = attribution.T
  
          if total_attribution_rows is not None:
              total_attribution_rows = np.concatenate(
                  [total_attribution_rows, attribution], axis=0
              )
          else:
              total_attribution_rows = attribution
  
          if total_attribution_global is not None:
              total_attribution_global += attribution.sum(axis=0)
          else:
              total_attribution_global = attribution.sum(axis=0)
  
      total_attribution_global /= nsamples
  
      feat_to_token_attributions = {
          k: [e for lst in v for e in lst]
          for k, v in feat_to_token_attributions.items()
      }
  
      return (
          total_attribution_rows,
          feat_to_token_attributions,
          total_attribution_global,
      )
  
imports: import copy
  import gc
  import logging
  from collections import defaultdict
  from dataclasses import dataclass
  from typing import Dict, List, Optional, Tuple
  import numpy
  import numpy.typing
  import pandas
  import torch
  from captum.attr import LayerIntegratedGradients, TokenReferenceBase
  from captum.attr._utils.input_layer_wrapper import InputIdentity
  from torch.autograd import Variable
  from tqdm import tqdm
  from ludwig.api import LudwigModel
  from ludwig.api_annotations import PublicAPI
  from ludwig.constants import BINARY, CATEGORY, DATE, IMAGE, INPUT_FEATURES, MINIMUM_BATCH_SIZE, NAME, NUMBER, PREPROCESSING, SEQUENCE, SET, TEXT, UNKNOWN_SYMBOL
  from ludwig.data.preprocessing import preprocess_for_prediction
  from ludwig.explain.explainer import Explainer
  from ludwig.explain.explanation import ExplanationsResult
  from ludwig.explain.util import get_pred_col, replace_layer_with_copy
  from ludwig.features.feature_utils import LudwigFeatureDict
  from ludwig.models.ecd import ECD
  from ludwig.utils.torch_utils import DEVICE
label_desc: Use image node_id 4 for calling a global function with example usage: get_total_attribution(model, target_feature_name, target_idx, feature_inputs, baseline, nsamples, run_config) and returns: total_attribution_rows,  feat_to_token_attributions,  total_attribution_global