Skip to content

可解释性/可解释性分析

PyTorch Tabular 中的可解释性功能允许用户解释和理解表格深度学习模型所做出的预测。这些功能提供了对模型决策过程的洞察,并帮助识别最具影响力的特征。一些可解释性功能是模型内置的,而许多其他功能则基于 Captum 库。

原生特征重要性

GBDT 模型中每个人都喜欢的一个功能是特征重要性。它帮助我们理解哪些特征对模型最为重要。PyTorch Tabular 为一些模型(如 GANDALF、GATE 和 FTTransformers)提供了类似的功能,这些模型原生支持特征重要性的提取。

# tabular_model 是支持模型的训练模型
tabular_model.feature_importance()

局部特征归因/解释

局部特征归因/解释帮助我们理解每个特征对特定样本预测的贡献。PyTorch Tabular 为除 TabTransformer、Tabnet 和 Mixed Density Networks 之外的所有模型提供了此功能。它基于 Captum 库。该库提供了许多用于计算特征归因的算法。PyTorch Tabular 提供了一个围绕该库的包装器,使其易于使用。支持以下算法:

  • GradientShap: https://captum.ai/api/gradient_shap.html
  • IntegratedGradients: https://captum.ai/api/integrated_gradients.html
  • DeepLift: https://captum.ai/api/deep_lift.html
  • DeepLiftShap: https://captum.ai/api/deep_lift_shap.html
  • InputXGradient: https://captum.ai/api/input_x_gradient.html
  • FeaturePermutation: https://captum.ai/api/feature_permutation.html
  • FeatureAblation: https://captum.ai/api/feature_ablation.html
  • KernelShap: https://captum.ai/api/kernel_shap.html

PyTorch Tabular 还支持解释单个实例以及一批实例。但是,较大的数据集将需要更长的时间来解释。例外的是 FeaturePermutationFeatureAblation 方法,它们仅对大批实例有意义。

大多数这些可解释性方法需要一个基线。这用于将输入的归因与基线的归因进行比较。基线可以是标量值、与输入形状相同的张量,或者是特殊字符串,如 "b|10000",表示从训练数据中抽取 10000 个样本。如果未提供基线,则使用默认基线(零)。

# tabular_model 是支持模型的训练模型

# 使用 GradientShap 方法和基线为 10000 个训练数据样本解释单个实例
tabular_model.explain(test.head(1), method="GradientShap", baselines="b|10000")

# 使用 IntegratedGradients 方法和基线为 0 解释一批实例
tabular_model.explain(test.head(10), method="IntegratedGradients", baselines=0)

查看 Captum 文档 以获取有关算法的更多详细信息,并查看 可解释性教程 以获取示例用法。

API 参考

pytorch_tabular.TabularModel.explain(data, method='GradientShap', method_args={}, baselines=None, **kwargs)

返回模型的特征归因/解释,以pandas DataFrame的形式呈现.返回的数据框形状为(样本数量, 特征数量)

Parameters:

Name Type Description Default
data DataFrame

需要解释的数据框

required
method str

用于解释模型的方法. 应为以下默认值之一:"GradientShap". 更多详情,请参考 https://captum.ai/api/attribution.html

'GradientShap'
method_args Optional[Dict]

传递给Captum方法初始化的参数.

{}
baselines Union[float, tensor, str]

用于解释的基线. 如果提供标量,将使用该值作为所有特征的基线. 如果提供张量,将使用该张量作为所有特征的基线. 如果提供类似b|<num_samples>的字符串,将使用训练数据中的那么多样本. 不推荐使用整个训练数据作为基线,因为它可能计算量很大.默认情况下,PyTorch Tabular使用训练数据中的10000个样本作为基线.你可以通过传递一个特殊字符串"b|"来配置,其中是要用作基线的样本数量.例如,"b|1000"将使用1000个样本. 如果为None,将使用captum中的默认设置(这取决于方法).对于GradientShap,它是训练数据. 默认为None.

None
**kwargs

传递给Captum方法attribute函数的额外关键字参数.

{}

Returns:

Name Type Description
DataFrame DataFrame

包含特征重要性的数据框

Source code in src/pytorch_tabular/tabular_model.py
    def explain(
        self,
        data: DataFrame,
        method: str = "GradientShap",
        method_args: Optional[Dict] = {},
        baselines: Union[float, torch.tensor, str] = None,
        **kwargs,
    ) -> DataFrame:
        """返回模型的特征归因/解释,以pandas DataFrame的形式呈现.返回的数据框形状为(样本数量, 特征数量)

Parameters:
    data (DataFrame): 需要解释的数据框
    method (str): 用于解释模型的方法.
        应为以下默认值之一:"GradientShap".
        更多详情,请参考 https://captum.ai/api/attribution.html
    method_args (Optional[Dict], optional): 传递给Captum方法初始化的参数.
    baselines (Union[float, torch.tensor, str]): 用于解释的基线.
        如果提供标量,将使用该值作为所有特征的基线.
        如果提供张量,将使用该张量作为所有特征的基线.
        如果提供类似`b|<num_samples>`的字符串,将使用训练数据中的那么多样本.
        不推荐使用整个训练数据作为基线,因为它可能计算量很大.默认情况下,PyTorch Tabular使用训练数据中的10000个样本作为基线.你可以通过传递一个特殊字符串"b|<num_samples>"来配置,其中<num_samples>是要用作基线的样本数量.例如,"b|1000"将使用1000个样本.
        如果为None,将使用captum中的默认设置(这取决于方法).对于`GradientShap`,它是训练数据.
        默认为None.

    **kwargs: 传递给Captum方法`attribute`函数的额外关键字参数.

Returns:
    DataFrame: 包含特征重要性的数据框
"""
        assert CAPTUM_INSTALLED, "Captum not installed. Please install using `pip install captum` or "
        "install PyTorch Tabular using `pip install pytorch-tabular[extra]`"
        ALLOWED_METHODS = [
            "GradientShap",
            "IntegratedGradients",
            "DeepLift",
            "DeepLiftShap",
            "InputXGradient",
            "FeaturePermutation",
            "FeatureAblation",
            "KernelShap",
        ]
        assert method in ALLOWED_METHODS, f"method should be one of {ALLOWED_METHODS}"
        if isinstance(data, pd.Series):
            data = data.to_frame().T
        if method in ["DeepLiftShap", "KernelShap"]:
            warnings.warn(
                f"{method} is computationally expensive and will take some time. For"
                " faster results, try usingsome other methods like GradientShap,"
                " IntegratedGradients etc."
            )
        if method in ["FeaturePermutation", "FeatureAblation"]:
            assert data.shape[0] > 1, f"{method} only works when the number of samples is greater than 1"
            if len(data) <= 100:
                warnings.warn(
                    f"{method} gives better results when the number of samples is"
                    " large. For better results, try using more samples or some other"
                    " methods like GradientShap which works well on single examples."
                )
        is_full_baselines = method in ["GradientShap", "DeepLiftShap"]
        is_not_supported = self.model._get_name() in [
            "TabNetModel",
            "MDNModel",
            "TabTransformerModel",
        ]
        do_baselines = method not in [
            "Saliency",
            "InputXGradient",
            "FeaturePermutation",
            "LRP",
        ]
        if is_full_baselines and (baselines is None or isinstance(baselines, (float, int))):
            raise ValueError(
                f"baselines cannot be a scalar or None for {method}. Please "
                "provide a tensor or a string like `b|<num_samples>`"
            )
        if is_not_supported:
            raise NotImplementedError(f"Attributions are not implemented for {self.model._get_name()}")

        is_embedding1d = isinstance(self.model.embedding_layer, (Embedding1dLayer, PreEncoded1dLayer))
        is_embedding2d = isinstance(self.model.embedding_layer, Embedding2dLayer)
        # Models like NODE may have no embedding dims (doing leaveOneOut encoding) even if categorical_dim > 0
        is_embbeding_dims = (
            hasattr(self.model.hparams, "embedding_dims") and self.model.hparams.embedding_dims is not None
        )
        if (not is_embedding1d) and (not is_embedding2d):
            raise NotImplementedError(
                "Attributions are not implemented for models with this type of" " embedding layer"
            )
        test_dl = self.datamodule.prepare_inference_dataloader(data)
        self.model.eval()
        # prepare import for Captum
        tensor_inp, tensor_tgt = self._prepare_input_for_captum(test_dl)
        baselines = self._prepare_baselines_captum(baselines, test_dl, do_baselines, is_full_baselines)
        # prepare model for Captum
        try:
            interp_model = _CaptumModel(self.model)
            captum_interp_cls = getattr(captum.attr, method)(interp_model, **method_args)
            if do_baselines:
                attributions = captum_interp_cls.attribute(
                    tensor_inp,
                    baselines=baselines,
                    target=(tensor_tgt if self.config.task == "classification" else None),
                    **kwargs,
                )
            else:
                attributions = captum_interp_cls.attribute(
                    tensor_inp,
                    target=(tensor_tgt if self.config.task == "classification" else None),
                    **kwargs,
                )
            attributions = self._handle_categorical_embeddings_attributions(
                attributions, is_embedding1d, is_embedding2d, is_embbeding_dims
            )
        finally:
            self.model.train()
        assert attributions.shape[1] == self.model.hparams.continuous_dim + self.model.hparams.categorical_dim, (
            "Something went wrong. The number of features in the attributions"
            f" ({attributions.shape[1]}) does not match the number of features in"
            " the model"
            f" ({self.model.hparams.continuous_dim+self.model.hparams.categorical_dim})"
        )
        return pd.DataFrame(
            attributions.detach().cpu().numpy(),
            columns=self.config.continuous_cols + self.config.categorical_cols,
        )