Google Article
如何在 TensorFlow 中训练增强树模型
2019 年 3 月 5 日

以及如何在本地和全局解释它们

作者:Chris Rawles、Natalia Ponomareva 和 Zhenyu Tan

## TL;DR:
# Train model.
est = tf.estimator.BoostedTreesClassifier(feature_columns, n_batches_per_layer)
est.train(train_input_fn)
# Per instance model interpretability:
pred_dict = est.experimental_predict_with_explanations(pred_input_fn)
# Global gain-based feature importances:
importances = est.experimental_feature_importances()
树集成方法,如梯度增强决策树和随机森林,是在处理结构化数据时最流行且最有效的机器学习工具之一。树集成方法训练速度快,无需大量调整即可运行良好,并且不需要大型数据集进行训练。

在 TensorFlow 中,梯度增强树可以使用 tf.estimator API 使用,该 API 还支持深度神经网络、宽而深模型等。对于增强树,支持使用预定义的均方误差损失 (BoostedTreesRegressor) 进行回归,以及使用交叉熵损失 (BoostedTreesClassifier) 进行分类。用户还可以选择使用任何二次可微分的自定义损失(通过将其提供给 BoostedTreesEstimator)。

在这篇文章中,我们将展示如何在 TensorFlow 中训练增强树模型,然后我们将演示如何使用特征重要性解释训练后的模型,以及如何解释模型对单个示例的预测。以下所有代码都已准备好用于 TensorFlow 2.0(预制估计器 在 TensorFlow 2.0 中得到完全支持)。这篇文章中所有代码都可以在 TensorFlow 文档中找到 这里这里
可视化增强树模型的预测曲面。梯度增强树是一种集成技术,它将来自多个(想想 10 个、100 个甚至 1000 个)树模型的预测相结合。增加树木的数量通常会提高拟合的质量。试试完整的示例 这里

在 TensorFlow 中训练增强树模型

增强树估计器支持不适合工作程序内存的大型数据集,并且它还提供分布式训练。但是,出于演示目的,让我们在一个小型数据集上训练增强树模型:泰坦尼克号数据集。这个(相当病态)数据集的目标是根据乘客的特征(如年龄、性别、等级等)预测乘客在泰坦尼克号沉船事故中幸存的概率。

首先,让我们导入必要的包并加载我们的数据集。
import numpy as np
import pandas as pd
import tensorflow as tf
tf.enable_eager_execution()

# Load dataset.
dftrain = pd.read_csv('https://storage.googleapis.com/tf-datasets/titanic/train.csv')
dfeval = pd.read_csv('https://storage.googleapis.com/tf-datasets/titanic/eval.csv')
y_train = dftrain.pop('survived')
y_eval = dfeval.pop('survived')
接下来,让我们定义 feature_column 来用于我们的 estimator 模型。特征列与所有 TensorFlow 估计器一起工作,它们的目的是定义用于建模的特征。此外,它们还提供一些特征工程功能,如 one-hot 编码、归一化 和分箱。在 CATEGORICAL_COLUMNS 中的字段将从分类列转换为 one-hot 编码列(指示器列)。
fc = tf.feature_column
CATEGORICAL_COLUMNS = ['sex', 'n_siblings_spouses', 'parch', 'class', 'deck', 
                       'embark_town', 'alone']
NUMERIC_COLUMNS = ['age', 'fare']
  
def one_hot_cat_column(feature_name, vocab):
  return tf.feature_column.indicator_column(
      tf.feature_column.categorical_column_with_vocabulary_list(feature_name,
                                                                vocab))
feature_columns = []
for feature_name in CATEGORICAL_COLUMNS:
  # Need to one-hot encode categorical features.
  vocabulary = dftrain[feature_name].unique()
  feature_columns.append(one_hot_cat_column(feature_name, vocabulary))
  
for feature_name in NUMERIC_COLUMNS:
  feature_columns.append(tf.feature_column.numeric_column(feature_name,
                                                          dtype=tf.float32))
您可以查看特征列生成的转换。例如,以下是在对单个示例使用 indicator_column 时的输出。
example = dict(dftrain.head(1))
class_fc = tf.feature_column.indicator_column(tf.feature_column.categorical_column_with_vocabulary_list('class', ('First', 'Second', 'Third')))
print('Feature value: "{}"'.format(example['class'].iloc[0]))
print('One-hot encoded: ', tf.keras.layers.DenseFeatures([class_fc])(example).numpy())

# Feature value: "Third"
# One-hot encoded:  [[0. 0. 1.]]
接下来,您需要创建输入函数。这些函数将指定数据如何被读入我们的模型,用于训练和推断。您将使用 tf.data API 中的 from_tensor_slices 方法直接从 Pandas 中读取数据。这适用于较小的内存中数据集。对于更大的数据集,tf.data API 支持各种文件格式(包括 csv),以便您可以处理不适合内存的数据集。
# Use entire batch since this is such a small dataset.
NUM_EXAMPLES = len(y_train)

def make_input_fn(X, y, n_epochs=None, shuffle=True):
  def input_fn():
    dataset = tf.data.Dataset.from_tensor_slices((dict(X), y))
    if shuffle:
      dataset = dataset.shuffle(NUM_EXAMPLES)
    # For training, cycle thru dataset as many times as need (n_epochs=None).    
    dataset = dataset.repeat(n_epochs)
    # In memory training doesn't use batching.
    dataset = dataset.batch(NUM_EXAMPLES)
    return dataset
  return input_fn

# Training and evaluation input functions.
train_input_fn = make_input_fn(dftrain, y_train)
eval_input_fn = make_input_fn(dfeval, y_eval, shuffle=False, n_epochs=1)
让我们首先训练一个逻辑回归模型以获得基准。
linear_est = tf.estimator.LinearClassifier(feature_columns)

# Train model.
linear_est.train(train_input_fn, max_steps=100)

# Evaluation.
result = linear_est.evaluate(eval_input_fn)
然后,训练增强树模型涉及与上述相同的过程。
# Since data fits into memory, use entire dataset per layer. It will be faster.
# Above one batch is defined as the entire dataset. 
n_batches = 1
est = tf.estimator.BoostedTreesClassifier(feature_columns,
                                          n_batches_per_layer=n_batches)

# The model will stop training once the specified number of trees is built, not 
# based on the number of steps.
est.train(train_input_fn, max_steps=100)

# Eval.
result = est.evaluate(eval_input_fn)

模型理解

对于许多最终用户来说,“为什么”和“如何”与预测一样重要。例如,最近的欧盟法规强调了用户的“解释权”,该法规规定用户应该能够获得对公司对其产生重大影响的决策的解释(来源)。此外,美国公平信用报告法要求机构披露“模型中不利影响消费者信用评分的所有关键因素,其总数不超过四个”(来源)。

模型可解释性还可以帮助机器学习 (ML) 从业人员在模型开发阶段检测偏差。这种洞察力可以帮助 ML 从业人员更好地调试和理解他们的模型。

通常有两种级别的模型可解释性:局部可解释性和全局可解释性。局部可解释性是指在单个示例级别上理解模型的预测,而全局可解释性是指理解整个模型。

可解释性技术通常特定于模型类型(例如,树方法、神经网络等)并利用学习的参数。例如,基于增益的特征重要性特定于树方法,而 集成梯度 技术利用神经网络中的梯度。

相比之下,还有一些模型无关的方法,例如 LIMEshap。LIME 通过构建训练局部代理模型来逼近底层黑盒模型的预测来操作。shap 方法将博弈论与局部解释联系起来,通过为每个特征分配在条件作用于该特征时预期模型预测的变化来实现。

理解单个预测:方向特征贡献

我们已经实现了由 Palczewska 等人 以及 Saabas 在 解释随机森林 中提出的局部特征贡献方法。这种方法也适用于 scikit-learn 的 treeinterpreter 包。

简而言之,这种技术允许人们通过分析在添加拆分时预测如何变化来理解模型如何对单个实例进行预测。从初始预测开始(通常称为偏差,通常定义为训练标签的平均值),该技术遍历预测路径,计算在对特征进行拆分后预测的变化。对于每个拆分,预测的变化归因于用于拆分的特征。在所有拆分和所有树木中,这些归因被加起来以指示每个特征的总贡献。

该方法返回与每个特征相关的数值。我们将这些值称为方向特征贡献 (DFC),以将它们与其他评估特征影响的方式(例如特征重要性)区分开来,特征重要性通常指的是全局特征重要性。DFC 允许检查单个示例,并提供洞察力,了解模型为什么对特定示例进行了预测。使用这种技术,您可以创建类似这样的可视化效果。
泰坦尼克号数据集中的一个实例的 DFC。非正式地,这可以解释为“adult_male 为 True “贡献”约 -0.11 到最终概率,而 Deck 为 B 贡献约 +0.08 到最终概率,等等。”
DFC 的一个很好的特性是,来自每个特征的贡献之和将加起来等于实际预测。例如,如果模型中有五个特征,并且对于给定实例,DFC 为

{sex_female: 0.2, age: 0.05, fare= -0.02, num_siblings_aboard=-0.1, fare: 0.09}

则预测概率将是这些值的总和:0.22。

我们还可以汇总整个数据集的 DFC,以获得对整个模型的洞察力,以实现全局解释。
整个评估数据集中的 top DFC 的平均绝对值。
fare 值与具有 LOWESS 拟合的贡献。跨示例的贡献提供了比单个特征重要性指标更详细的信息。通常,更高的 fare 特征会导致模型将预测推向更接近 1.0 的方向(增加生存的机会)。

操作方法:TensorFlow 中的方向特征贡献

以下所有代码都可以在 增强树模型理解笔记本 中找到。

首先,您需要使用 tf.estimator API 训练增强树估计器,如上所述。
params = {
  'n_trees': 50,
  'max_depth': 3,
  'n_batches_per_layer': 1,
  # You must enable center_bias = True to get DFCs. This will force the model to 
  # make an initial prediction before using any features (e.g. use the mean of 
  # the training labels for regression or log odds for classification when
  # using cross entropy loss).
  'center_bias': True
}
est = tf.estimator.BoostedTreesClassifier(feature_columns, **params)
# Train model.
est.train(train_input_fn, max_steps=100)
# Evaluation.
results = est.evaluate(eval_input_fn)
训练完我们的模型后,我们可以使用 est.experimental_predict_with_explanations 获取模型解释。(注意:该方法名为 experimental,因为我们可能会在删除 experimental 前缀之前修改 API。)

使用 pandas,您可以轻松地可视化 DFC。
# Make predictions.
pred_dicts = list(est.experimental_predict_with_explanations(eval_input_fn))
df_dfc = pd.DataFrame([pred['dfc'] for pred in pred_dicts])
# Plot results.
ID = 182
example = df_dfc.iloc[ID]  # Choose ith example from evaluation set.
TOP_N = 8  # View top 8 features.
sorted_ix = example.abs().sort_values()[-TOP_N:].index
ax = example[sorted_ix].plot(kind='barh')
在我们的 Colab 中,我们包含了一个示例,该示例添加了贡献分布,以了解特定实例的 DFC 与其余评估集的比较方式。
单个示例的贡献,以红色显示。阴影蓝色区域显示了整个验证集的特征贡献分布。
我们还注意到,还有其他第三方模型无关的可解释性方法可以与 TensorFlow 协同工作,例如 LIMEshap。请参阅下面的其他资源以获取更多链接。

模型级别可解释性:基于增益和置换特征重要性

有不同的方法可以实现增强树模型的模型级别理解(即全局可解释性)。我们之前已经表明,您可以汇总整个数据集的 DFC 以实现全局可解释性。这也适用于汇总其他局部解释值,例如由 LIME 或 shap(如上所述)生成的那些值。

下面我们讨论的另外两种技术是基于增益的特征重要性和置换特征重要性。基于增益的特征重要性衡量在特定特征上拆分时的损失变化,而置换特征重要性是通过对评估集进行评估来计算的,方法是逐个对每个特征进行随机排列,并将模型性能的损失归因于随机排列的特征。置换特征重要性具有模型无关的优点,但是,这两种方法在潜在预测变量的测量尺度或类别数量存在差异的情况下可能不可靠(来源)。

在 TensorFlow 中的增强树估计器中,基于增益的特征重要性使用 est.experimental_feature_importances 获取。以下是一个包含绘图的完整示例。
# Get importances
importances = est.experimental_feature_importances(normalize=True)
df_imp = pd.Series(importances)

# Visualize importances.
N = 8
ax = (df_imp.iloc[0:N][::-1]
    .plot(kind='barh'))

置换特征重要性可以按如下方式计算。
def permutation_importances(est, X_eval, y_eval, metric, features):
    """Column by column, shuffle values and observe effect on eval set.
        source: http://explained.ai/rf-importance/index.html
    A similar approach can be done during training. See "Drop-column importance"
    in the above article."""
    def accuracy_metric(est, X, y):
        """TensorFlow estimator accuracy."""
        eval_input_fn = make_input_fn(X,
                                      y=y,
                                      shuffle=False,
                                      n_epochs=1)
        return est.evaluate(input_fn=eval_input_fn)['accuracy']
    
    baseline = metric(est, X_eval, y_eval)
    imp = []
    for col in features:
        save = X_eval[col].copy()
        X_eval[col] = np.random.permutation(X_eval[col])
        m = metric(est, X_eval, y_eval)
        X_eval[col] = save
        imp.append(baseline - m)
    return np.array(imp)

importances = permutation_importances(est, dfeval, y_eval, accuracy_metric,
                                      dftrain.columns)

相关变量和其他注意事项

当两个或多个特征相关时,许多模型解释工具会提供关于特征影响的扭曲视图。例如,如果您训练了一个包含两个高度相关的特征的集成树模型,则与单独包含其中任何一个特征相比,这两个特征的基于增益的特征重要性将更低。

在泰坦尼克号数据集上,假设我们不小心两次编码了乘客的等级 - 以两个变量 class 和 pclass 的形式。使用独热编码对这些分类特征进行编码并训练模型后,我们观察到乘客处于三等舱具有预测能力 - 我们可以看到两次。

在删除其中一个特征 (pclass) 并重新检查特征重要性后,乘客处于三等舱的显著性大约翻倍。

在这种情况下,这两个特征是完全相关的,但是即使是部分相关的特征也会发生同样的现象,只是程度较轻。

因此,对于我们上面讨论的技术,建议删除高度相关的特征。这不仅有助于解释性,还会加快模型训练速度。此外,维护较少的特征比维护大量特征更容易。

最后,我们注意到 Strobl 等人介绍了一种称为条件变量重要性的新技术,利用特征置换来帮助提供对相关变量存在时特征影响的更现实估计。有关更多详细信息,请查看论文

结论

可以使用 tf.estimator API 在 TensorFlow 中获得梯度提升决策树,该 API 允许用户快速尝试不同的机器学习模式。对于梯度提升决策树,TensorFlow 中提供了局部模型可解释性(使用Palczewska 等人 和 Saabas (解释随机森林) 通过 experimental_predict_with_explanations 概述的方法的每个实例的可解释性)和全局级可解释性(基于增益和置换特征重要性)。这些方法可以帮助从业人员更好地理解他们的模型。

TensorFlow Boosted Trees 的发布得益于许多人的努力,包括但不限于 Soroush Radpour、Younghee Kwon、Mustafa Ispir、Salem Haykal 和 Yan Facai。

附加资源

与 TensorFlow 一起使用的其他模型可解释性方法
置换特征重要性(Brieman,2001)
下一篇文章
How to train Boosted Trees models in TensorFlow

以及如何从本地和全局层面解读它们作者:Chris Rawles、Natalia Ponomareva 和 Zhenyu Tan

## TL;DR: # 训练模型。 est = tf.estimator.BoostedTreesClassifier(feature_columns, n_batches_per_layer) est.train(train_input_fn) # 每个实例的模型可解释性: pred_dict = est.experimental_predict_with_explanations(pred_input_fn) # 全局基于增益的特征重要性: importanc…