训练可解释的提升计算机 - 分类(预览版)

本文介绍如何使用可解释的提升机器(EBM)训练分类模型。 可解释性增强模型是一种机器学习技术,它将梯度提升的强大功能与模型解释性的强调相结合。 它创建了一个决策树的集成,类似于梯度提升算法,但特别强调生成易于人类阅读的模型。 EBM 不仅提供准确的预测,而且还为这些预测提供清晰直观的解释。 它们非常适合用于了解驱动模型决策的基础因素的应用程序,例如医疗保健、财务和法规合规性。

在 SynapseML 中,可以使用由 Apache Spark 提供支持的 EBM 的可缩放实现来训练新模型。 本文介绍如何使用 Apache Spark 在 Microsoft Fabric 中应用 EBM 的可伸缩性和可解释性。

重要

此功能处于 预览版中。

本文逐步讲解如何从记录 NYC 黄色出租车行程的 Azure 开放数据集获取和预处理数据。 然后,您可以以确定给定行程是否会发生为最终目标来训练预测模型。

可解释的提升计算机的优点

当机器学习模型的透明度和理解性至关重要时,EBM 提供独特的可解释性和预测能力混合,使其成为理想的选择。 借助 EBM,用户可以深入了解驱动预测的基础因素,使他们能够了解模型为何做出特定决策或预测,这对于在 AI 系统中构建信任至关重要。

他们能够发现数据中的复杂关系,同时提供清晰和可解释的结果,使他们在金融、医疗保健和欺诈检测等领域具有宝贵的价值。 在这些领域,模型可解释性不仅可取,而且通常是法规要求。 最终,选择 EBM 的用户可以在模型性能和透明度之间取得平衡,确保 AI 解决方案准确、易于理解和负责。

先决条件

  • 依次选择 + 和“笔记本”,在工作区中创建一个新笔记本。

安装库

首先,安装 Azure 机器学习开放数据集库,该库授予对数据集的访问权限。 此安装步骤对于有效访问和使用数据集至关重要。

%pip install azureml-opendatasets

导入 MLflow

使用 MLflow 可以跟踪模型的参数和结果。 以下代码片段演示如何使用 MLflow 进行试验和跟踪。 ebm_classification_nyc_taxi 值是记录信息的试验的名称。

import mlflow

# Set up the experiment name
mlflow.set_experiment("ebm_classification_nyc_taxi")

导入库

接下来,添加以下代码以导入用于分析的基本包:

# Import necessary packages
import interpret
from pyspark.ml.feature import StringIndexer, VectorAssembler
from pyspark.ml import Pipeline
from pyspark.mllib.evaluation import BinaryClassificationMetrics
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import DoubleType
from synapse.ml.ebm import EbmClassification

这些导入的包提供分析和建模任务所需的基本工具和功能。

加载数据

接下来,添加以下代码,从 Azure 机器学习开放数据集检索 NYC 黄色出租车数据,并选择性地向下采样数据集以加快开发:

# Import NYC Yellow Taxi data from Azure Open Datasets
from azureml.opendatasets import NycTlcYellow

from datetime import datetime
from dateutil import parser

# Define the start and end dates for the dataset
end_date = parser.parse('2018-05-08 00:00:00')
start_date = parser.parse('2018-05-01 00:00:00')

# Load the NYC Yellow Taxi dataset within the specified date range
nyc_tlc = NycTlcYellow(start_date=start_date, end_date=end_date)
nyc_pd = nyc_tlc.to_pandas_dataframe()
nyc_tlc_df = spark.createDataFrame(nyc_pd)

为了促进开发并降低计算开销,可以缩小数据集的采样范围。

# For development convenience, consider down-sampling the dataset
sampled_taxi_df = nyc_tlc_df.sample(True, 0.001, seed=1234)

向下采样可实现更快、更具成本效益的开发体验,尤其是在处理大型数据集时。

准备数据

在本部分中,你将通过筛选出具有离群值和无关变量的行来准备数据集。

生成功能

为预期增强模型性能的新派生功能创建代码:

taxi_df = sampled_taxi_df.select('totalAmount', 'fareAmount', 'tipAmount', 'paymentType', 'rateCodeId', 'passengerCount',
                                'tripDistance', 'tpepPickupDateTime', 'tpepDropoffDateTime',
                                date_format('tpepPickupDateTime', 'hh').alias('pickupHour'),
                                date_format('tpepPickupDateTime', 'EEEE').alias('weekdayString'),
                                (unix_timestamp(col('tpepDropoffDateTime')) - unix_timestamp(col('tpepPickupDateTime'))).alias('tripTimeSecs'),
                                (when(col('tipAmount') > 0, 1).otherwise(0)).alias('tipped')
                                )\
                        .filter((sampled_taxi_df.passengerCount > 0) & (sampled_taxi_df.passengerCount < 8)
                                & (sampled_taxi_df.tipAmount >= 0) & (sampled_taxi_df.tipAmount <= 25)
                                & (sampled_taxi_df.fareAmount >= 1) & (sampled_taxi_df.fareAmount <= 250)
                                & (sampled_taxi_df.tipAmount < sampled_taxi_df.fareAmount)
                                & (sampled_taxi_df.tripDistance > 0) & (sampled_taxi_df.tripDistance <= 100)
                                & (sampled_taxi_df.rateCodeId <= 5)
                                & (sampled_taxi_df.paymentType.isin({"1", "2"}))
                                )

创建新变量后,请添加代码以删除从中派生的列。 现在,数据帧已更精简以适合模型输入。 此外,还可以基于新创建的列生成更多功能:

taxi_featurised_df = taxi_df.select('totalAmount', 'fareAmount', 'tipAmount', 'paymentType', 'passengerCount',
                                    'tripDistance', 'weekdayString', 'pickupHour', 'tripTimeSecs', 'tipped',
                                    when((taxi_df.pickupHour <= 6) | (taxi_df.pickupHour >= 20), "Night")
                                    .when((taxi_df.pickupHour >= 7) & (taxi_df.pickupHour <= 10), "AMRush")
                                    .when((taxi_df.pickupHour >= 11) & (taxi_df.pickupHour <= 15), "Afternoon")
                                    .when((taxi_df.pickupHour >= 16) & (taxi_df.pickupHour <= 19), "PMRush")
                                    .otherwise(0).alias('trafficTimeBins'))\
                                    .filter((taxi_df.tripTimeSecs >= 30) & (taxi_df.tripTimeSecs <= 7200))

这些数据准备步骤可确保您的数据集既得到精炼又得到优化,以用于后续的建模过程。

对数据进行编码

在 SynapseML EBM 的上下文中,Estimator 期望输入数据采用 org.apache.spark.mllib.linalg.Vector的形式,实质上是 Doubles的向量。 因此,必须将任何分类(字符串)变量转换为数值表示形式,并且任何拥有数值但非数值数据类型的变量都必须强制转换为数值数据类型。

目前,SynapseML EBM 采用 StringIndexer 方法来管理分类变量。 目前,SynapseML EBM 不提供针对分类功能的专用处理。

这里是一系列需要添加到代码中的步骤,以便为使用 SynapseML EBM 准备数据:

# Convert categorical features into numerical representations using StringIndexer
sI1 = StringIndexer(inputCol="trafficTimeBins", outputCol="trafficTimeBinsIndex")
sI2 = StringIndexer(inputCol="weekdayString", outputCol="weekdayIndex")

# Apply the encodings to create a new dataframe
encoded_df = Pipeline(stages=[sI1, sI2]).fit(taxi_featurised_df).transform(taxi_featurised_df)
final_df = encoded_df.select('fareAmount', 'paymentType', 'passengerCount', 'tripDistance',
                             'pickupHour', 'tripTimeSecs', 'trafficTimeBinsIndex', 'weekdayIndex',
                             'tipped')

# Ensure that any string representations of numbers in the data are cast to numeric data types
final_df = final_df.withColumn('paymentType', final_df['paymentType'].cast(DoubleType()))
final_df = final_df.withColumn('pickupHour', final_df['pickupHour'].cast(DoubleType()))

# Define the label column name
labelColumnName = 'tipped'

# Assemble the features into a vector
assembler = VectorAssembler(outputCol='features')
assembler.setInputCols([c for c in final_df.columns if c != labelColumnName])
vectorized_final_df = assembler.transform(final_df)

这些数据准备步骤对于将数据格式与 SynapseML EBM 的要求保持一致至关重要,可确保准确有效的模型训练。

生成测试和训练数据集

现在,使用简单的拆分添加代码,将数据集划分为训练集和测试集。 70% 的数据用于训练,30% 用于测试模型:

# Decide on the split between training and test data from the dataframe 
trainingFraction = 0.7
testingFraction = (1-trainingFraction)
seed = 1234

# Split the dataframe into test and training dataframes
train_data_df, test_data_df = vectorized_final_df.randomSplit([trainingFraction, testingFraction], seed=seed)

通过此数据划分,我们可以在大量部分训练模型,同时保留另一部分以有效评估其性能。

训练模型

现在,添加代码训练 EBM 模型,然后使用受试者工作特征(ROC)曲线下的面积(AUROC)作为指标来评估其性能。

# Create an instance of the EBMClassifier
estimator = EbmClassification()
estimator.setLabelCol(labelColumnName)

# Fit the EBM model to the training data
model = estimator.fit(train_data_df)

# Make predictions for tip (1/0 - yes/no) on the test dataset and evaluate using AUROC
predictions = model.transform(test_data_df)
predictionsAndLabels = predictions.select("prediction", labelColumnName)
predictionsAndLabels = predictionsAndLabels.withColumn(labelColumnName, predictionsAndLabels[labelColumnName].cast(DoubleType()))

# Calculate AUROC using BinaryClassificationMetrics
metrics = BinaryClassificationMetrics(predictionsAndLabels.rdd)
print("Area under ROC = %s" % metrics.areaUnderROC)

此过程需要训练训练训练数据集上的 EBM 模型,然后使用它对测试数据集进行预测,然后评估模型的性能(使用 AUROC 作为关键指标)。

查看全局说明

若要可视化模型的总体说明,可以添加代码以获取可视化包装器并利用 interpretshow 的方法。 可视化包装器充当桥梁,有助于模型的可视化体验。 下面介绍了如何执行此操作:

wrapper = model.getVizWrapper()
explanation = wrapper.explain_global()

接下来,添加代码以导入 interpret 库,并使用 show 方法显示说明:

import interpret
interpret.show(explanation)

全局说明的屏幕截图。

术语“重要性”表示每个术语(特征或交互)对预测的平均绝对贡献(分数)。 在整个训练数据集中均衡这些贡献,同时结合每个箱中的样本数量和样本权重(如果适用)。 前 15 个最重要的术语显示在说明中。

查看本地说明

提供的解释在全局级别,但在某些情况下,每个功能输出也是有价值的。 训练程序和模型都提供设置 featurescores的功能,在填充时会引入另一个矢量值列。 此列中的每个向量与特征列的长度匹配,每个值对应于同一索引处的功能。 这些值表示每个特征的值对模型的最终输出的贡献。

不同于全局解释,目前不存在与针对每个功能的输出的 interpret 可视化效果的直接集成。 这是因为全局可视化效果主要随功能数量(通常很少)进行缩放,而局部解释随行数(如果是 Spark 数据框,则行数可能很大)进行缩放。

下面是用于设置和使用featurescores的代码:

prediction2 = model.setFeatureScoresCol("featurescores").transform(train_data_df)

为了说明目的,让我们添加代码来打印第一个示例的详细信息:

# Convert to Pandas for easier inspection
predictions_pandas = prediction2.toPandas()
predictions_list = prediction2.collect()

# Extract the first example from the collected predictions.
first = predictions_list[0]

# Print the lengths of the features and feature scores.
print('Length of the features is', len(first['features']), 'while the feature scores have length', len(first['featurescores']))

# Print the values of the features and feature scores.
print('Features are', first['features'])
print('Feature scores are', first['featurescores'])