利用CFNOW生成反事实解释变得更加容易,但什么是反事实解释,以及如何使用它们?
本文基于以下文章:https://www.sciencedirect.com/science/article/abs/pii/S0377221723006598
CFNOW库的地址是:https://github.com/rmazzine/CFNOW
如果你正在阅读本文,你可能知道人工智能(AI)在我们今天的世界中变得至关重要。然而,值得注意的是,看似有效的、新颖的机器学习方法,结合其广泛的受欢迎程度,可能会导致意想不到的或不可预见的后果。
这就引出了可解释人工智能(XAI)作为确保人工智能的道德和负责任发展的关键组成部分为什么至关重要。这个领域表明,解释由数百万甚至数十亿参数组成的模型并不是一个简单的问题。这个问题的答案是多方面的,因为有许多方法揭示模型的不同方面,LIME [1]和SHAP [2]是其中的知名例子。
然而,这些方法生成的解释的复杂性可能导致复杂的图表或分析,这可能引导无知专家的错误解读。规避这种复杂性的一种可能的方法是一种简单而自然的解释方法,称为反事实解释[3]。
反事实解释利用了人类自然行为来解释事物——创建“替代世界”,通过改变一些参数可以改变结果。这是一种常见做法,你可能已经做过类似的事情——“如果我早一点醒来,就不会错过公交车”,这类解释以直接的方式突出了结果的主要原因。
更深入地探究,反事实不仅仅是解释,它们还可以作为变化的指导,帮助调试异常行为,并验证某些特征是否可以在预测时潜在地修改(而不会对评分产生太大影响)。这种多功能性凸显了解释您的预测的重要性。这不仅仅是负责任的人工智能的问题,也是改进模型并将其用于预测范围之外的途径。反事实解释的一个显著特点是其与决策密切相关的性质,使其与预测的变化直接对应[6],而LIME和SHAP更适合解释分数。
鉴于明显的好处,人们可能会想为什么反事实并不更受欢迎。这是一个合理的问题!普遍采用反事实解释的主要障碍有三个方面[4, 5]:(1)缺乏用户友好且兼容的反事实生成算法,(2)反事实生成算法的低效率,(3)以及缺乏综合视觉表现形式。
但是我有一些好消息要告诉你!一个名为CFNOW(CounterFactuals NOW或CounterFactual Nearest Optimal Wololo)的新包正在解决这些挑战。CFNOW是一个多功能的Python包,可以为各种数据类型生成多个反事实解释,例如表格、图像和文本(嵌入)输入。它采用了一个与模型无关的方法,只需要最少的数据——(1)事实点(需要解释的点)和(2)预测函数。
此外,CFNOW的结构允许根据自定义逻辑开发和集成查找和微调反事实的新策略。它还提供了CounterPlots,一种用于可视化表示反事实解释的新策略。
CFNOW 的核心是将数据转换为 CF 生成器可管理的单一结构。在此之后,一个两步的过程会定位并优化找到的反事实。为了防止局部最小值,该软件包实现了禁忌搜索这一数学启发法,使其能够探索目标函数可能更优化的新区域。
接下来的部分将重点介绍如何有效地利用 CFNOW 生成表格、图像和文本(嵌入式)分类器的解释。
表格分类器
这里,我们展示了通常的东西,你有多种类型的表格数据。在下面的示例中,我将使用一个数据集,其中包含数值连续、二元分类和独热编码的分类数据,展示 CFNOW 的全部功能。
首先,您需要安装 CFNOW 包,所需的 Python 版本应大于 3.8:
pip install cfnow
(此示例的完整代码请参见:https://colab.research.google.com/drive/1GUsVfcM3I6SpYCmsBAsKMsjVdm-a6iY6?usp=sharing)
在第一部分中,我们将使用成人数据集创建一个分类器。此处没有太多新闻:
import warningsimport pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.metrics import accuracy_scorewarnings.filterwarnings("ignore", message="X does not have valid feature names, but RandomForestClassifier was fitted with feature names")
我们导入基本包以创建分类模型,并且还关闭了与没有列名进行预测相关的警告。
然后,我们开始编写分类器,其中类别 1 表示收入低于或等于 50k(<=50K),类别 0 表示高收入。
# 创建分类器import warningsimport pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.metrics import accuracy_scorewarnings.filterwarnings("ignore", message="X does not have valid feature names, but RandomForestClassifier was fitted with feature names")# 加载成人数据集dataset_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"column_names = ['age', 'workclass', 'fnlwgt', 'education', 'education-num', 'marital-status', 'occupation', 'relationship', 'race', 'sex', 'capital-gain', 'capital-loss', 'hours-per-week', 'native-country', 'income']data = pd.read_csv(dataset_url, names=column_names, na_values=" ?", skipinitialspace=True)# 删除含有缺失值的行data = data.dropna()# 确定非二元分类特征non_binary_categoricals = [column for column in data.select_dtypes(include=['object']).columns if len(data[column].unique()) > 2]binary_categoricals = [column for column in data.select_dtypes(include=['object']).columns if len(data[column].unique()) == 2]cols_numericals = [column for column in data.select_dtypes(include=['int64']).columns]# 对非二元分类特征进行独热编码data = pd.get_dummies(data, columns=non_binary_categoricals)# 将二元分类特征转换为数字# 这也将对目标变量(income)进行二值化for bc in binary_categoricals: data[bc] = data[bc].apply(lambda x: 1 if x == data[bc].unique()[0] else 0)# 将数据集分为特征和目标变量X = data.drop('income', axis=1)y = data['income']# 将数据集分为训练集和测试集X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# 初始化一个 RandomForestClassifierclf = RandomForestClassifier(random_state=42)# 训练分类器clf.fit(X_train, y_train)# 对测试集进行预测y_pred = clf.predict(X_test)# 评估分类器的准确性accuracy = accuracy_score(y_test, y_pred)print("准确度:", accuracy)
通过以上代码,我们创建了一个数据集,对其进行预处理,创建了一个分类模型,并在测试集上进行了预测和评估。
现在,让我们选择一个点(测试集的第一个点)并验证其预测结果:
clf.predict([X_test.iloc[0]])# 结果: 0 -> 高收入
现在是使用CFNOW计算如何通过最小修改特征来改变这个预测的时候了:
from cfnow import find_tabular# 接下来,我们使用CFNOW来生成最小修改,以改变分类cf_res = find_tabular( factual=X_test.iloc[0], feat_types={c: 'num' if c in cols_numericals else 'cat' for c in X.columns}, has_ohe=True, model_predict_proba=clf.predict_proba, limit_seconds=60)
上面的代码我们:
factual
添加了实例作为 pd.Seriesfeat_types
指定了特征类型 (“num”表示数字连续和“cat”表示分类)has_ohe
表示我们有 OHE 特征(它通过聚合具有相同前缀后跟下划线的特征来自动检测 OHE 特征,例如 country_brazil,country_usa,country_ireland)。model_predict_proba
包括一个预测函数limit_seconds
定义了运行的总时间阈值,这很重要,因为微调步骤可以无限期地继续进行(默认为 120 秒)
然后,在一段时间后,我们首先评估最佳反事实的类别(cf_res.cfs
的第一个索引)
clf.predict([cf_obj.cfs[0]])# 结果: 1 -> 低收入
还有CFNOW的一些不同之处,因为它还集成了 CounterPlots,我们可以绘制它们的图表,并获得更多的见解信息,如下所示:
下面的 CounterShapley 图显示了每个特征对生成反事实预测的相对重要性。在这里,我们有一些有趣的见解,表明合并的婚姻状况(如果结合在一起)对 CF 类别的贡献超过50%。
Greedy 图与 CounterShapley 图非常相似,区别在于变化的顺序。CounterShapley 图不考虑任何特定的顺序(使用 Shapley 值计算贡献),而 Greedy 图使用贪婪策略修改实例,每一步都改变对 CF 类别贡献最大的特征。这对于给出贪婪方式的情况可能是有用的(每一步选择实现目标的最佳方法)。
最后,我们有最复杂的分析,即 Constellation 图。尽管看起来很困难,但实际上它非常容易理解。每个大红点代表一个单一特征的变化(相对于标签),小点表示两个或多个特征的组合。最后,大蓝点表示 CF 分数。在这里,我们可以看到使用这些特征获得 CF 的唯一方法是将它们全部修改为它们各自的值(即,不存在生成 CF 的子集)。我们还可以深入研究特征之间的关系,可能发现有趣的模式。
在这个特定 case 中,有趣的是观察到,如果这个人是女性、离异,并且有一个孩子,那么对高收入的预测将发生改变。这个反事实可能会引发关于不同社会群体经济影响的进一步讨论。
图像分类器
正如前面提到的,CFNOW可以处理各种类型的数据,因此它也可以为图像数据生成反事实。然而,对于图像数据来说,生成反事实意味着什么呢?
答案可能各不相同,因为有多种方法可以生成反事实。可以用随机噪声替换单个像素(这是对抗性攻击中使用的方法),也可以使用更复杂的方法,涉及先进的分割技术。
CFNOW使用了一种称为quickshift的分割方法,它是一种可靠且快速的方法来检测“语义”分割。然而,你也可以整合(我邀请你这样做)其他分割技术。
仅进行分割检测是不足以生成反事实解释的。我们还需要修改这些分割段,用修改后的版本替换它们。对于这种修改,CFNOW在参数replace_mode
中定义了四个选项,我们可以选择:(默认)blur
— 添加模糊滤镜到替换的分割段上,mean
— 用平均颜色替换分割段,random
— 用随机噪声替换分割段,以及inpaint
— 根据邻近像素重建图像。
如果你想要整个代码,可以在这里找到:https://colab.research.google.com/drive/1M6bEP4x7ilSdh01Gs8xzgMMX7Uuum5jZ?usp=sharing
接下来,我将展示为这种类型的数据实现CFNOW的代码:
首先,如果你还没有安装CFNOW包,请安装一下。
pip install cfnow
现在,让我们添加一些额外的包来加载预训练模型:
pip install torch torchvision Pillow requests
然后,让我们加载数据,加载预训练模型,并创建一个与CFNOW所需数据格式兼容的预测函数:
import requestsimport numpy as npfrom PIL import Imagefrom torchvision import models, transformsimport torch# 加载预训练的ResNet模型model = models.resnet50(pretrained=True)model.eval()# 定义图像转换transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),])# 从网络上获取图像image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Sunflower_from_Silesia2.jpg/320px-Sunflower_from_Silesia2.jpg"response = requests.get(image_url, stream=True)image = np.array(Image.open(response.raw))def predict(images): if len(np.shape(images)) == 4: # 将numpy数组列表转换为一批张量 input_images = torch.stack([transform(Image.fromarray(image.astype('uint8'))) for image in images]) elif len(np.shape(images)) == 3: input_images = transform(Image.fromarray(images.astype('uint8'))) else: raise ValueError("输入必须是一批图像或者单个图像。") # 检查是否有可用的GPU,如果没有,则使用CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") input_images = input_images.to(device) model.to(device) # 执行预测 with torch.no_grad(): outputs = model(input_images) # 返回每个图像的预测分数数组 return torch.asarray(outputs).cpu().numpy()LABELS_URL = "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json"def predict_label(outputs): # 加载预训练模型使用的标签 labels = requests.get(LABELS_URL).json() # 获取预测的标签 predicted_idxs = [np.argmax(od) for od in outputs] predicted_labels = [labels[idx.item()] for idx in predicted_idxs] return predicted_labels# 检查图像的预测结果predicted_label = predict([np.array(image)])print("预测的标签:", predict_label(predicted_label))
大部分代码工作都与构建模型、获取数据以及调整数据有关,因为要使用CFNOW生成反事实,我们只需要:
从cfnow导入find_imagecf_img = find_image(img =图像,model_predict = predict)cf_img_hl = cf_img.cfs [0]print(“预测标签:”,predict_label(predict([cf_img_hl])))#显示CF图像Image.fromarray(cf_img_hl.astype('uint8'))
在上面的例子中,我们使用了所有默认的可选参数,因此,我们使用quickshift对图像进行分割,并用模糊的图像替换了分割部分。结果为,我们得到了下面这个事实预测:
变成了下面的:
那么,这个分析有什么结果呢?实际上,图像对抗可以是检测模型如何进行分类的非常有用的工具。这可以适用于以下情况:(1)我们想要验证模型为什么进行正确分类-确保它使用了正确的图像特征:在这种情况下,尽管它将向日葵误分类为雏菊,但我们可以看到将花朵进行模糊处理(而不是背景特征)会改变预测结果。它还可以(2)帮助诊断错误分类的图像,这可以为图像处理和/或数据采集提供更好的见解。
文本分类器
最后,我们有基于嵌入的文本分类器。虽然简单的文本分类器(使用更像表格数据的数据结构)可以使用表格对抗生成器,但基于嵌入的文本分类器则不太明确。
这样做的理由是嵌入具有可变数量的输入和词汇,这可能会严重影响预测分数和分类。
CFNOW使用了两种策略来解决这个问题:(1)通过删除证据或(2)通过添加反义词。第一种策略很简单,为了衡量每个词对文本的影响,我们只需将它们删除,看看哪些词我们必须删除才能改变分类。而通过添加反义词,我们可能可以保持语义结构(因为删除一个词可能会严重损害它)。
接下来的代码展示了如何在这个上下文中使用CFNOW。
如果您想要完整的代码,请在此处查看:https://colab.research.google.com/drive/1ZMbqJmJoBukqRJGqhUaPjFFRpWlujpsi?usp=sharing
首先,安装CFNOW包:
pip install cfnow
然后,安装文本分类所需的必要包:
pip install transformers
接下来,与前几节一样,我们首先构建分类器:
from transformers import DistilBertTokenizer, DistilBertForSequenceClassification
from transformers import pipeline
import numpy as np
# 加载预训练模型和分词器用于情感分析
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = DistilBertTokenizer.from_pretrained(model_name)
model = DistilBertForSequenceClassification.from_pretrained(model_name)
# 定义情感分析流程
sentiment_analysis = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
# 定义一个简单的数据集
text_factual = "我喜欢这部电影,因为它很有趣,但是我的朋友们不喜欢它,因为它太长而且无聊。"
result = sentiment_analysis(text_factual)
print(f"{text_factual}: {result[0]['label']} (置信度: {result[0]['score']:.2f})")
def pred_score_text(list_text):
if type(list_text) == str:
sa_pred = sentiment_analysis(list_text)[0]
sa_score = sa_pred['score']
sa_label = sa_pred['label']
return sa_score if sa_label == "POSITIVE" else 1.0 - sa_score
return np.array([sa["score"] if sa["label"] == "POSITIVE" else 1.0 - sa["score"] for sa in sentiment_analysis(list_text)])
对于这段代码,我们可以看到我们的实际文本的情感是负面的,并且置信度很高(≥0.9),现在让我们尝试生成反事实:
from cfnow import find_text
cf_text = find_text(text_input=text_factual, textual_classifier=pred_score_text)
result_cf = sentiment_analysis(cf_text.cfs[0])
print(f"CF: {cf_text.cfs[0]}: {result_cf[0]['label']} (置信度: {result_cf[0]['score']:.2f})")
通过上面的代码,只需改变一个词(但),分类从负面变为正面,并且置信度很高。这展示了反事实是如何有用的,因为这种微小的修改对于理解模型预测句子的方式或帮助调试不希望出现的行为具有影响。
结论
这是关于CFNOW和反事实解释的(相对而言)简要介绍。关于反事实的文献非常广泛(且正在增加),如果你想深入研究,我建议你一定要阅读这个开创性的文章[3],作者是我的博士导师David Martens教授,这是一个更好的了解反事实解释的入门方式。另外,还有像Verma等人撰写的这篇好的综述[7]。总体而言,反事实解释是一种解释复杂的机器学习算法决策的简单方便的方式,而且如果正确应用,可以做出更多的解释。CFNOW提供了一种简单、快速和灵活的生成反事实解释的方式,使从业者不仅能够解释,而且能够尽可能充分地利用他们的数据和模型的潜力。
参考文献:
[1] — https://github.com/marcotcr/lime
[2] — https://github.com/shap/shap
[3] — https://www.jstor.org/stable/26554869
[4] — https://www.mdpi.com/2076-3417/11/16/7274
[5] — https://arxiv.org/pdf/2306.06506.pdf
[6] — https://arxiv.org/abs/2001.07417
[7] — https://arxiv.org/abs/2010.10596