Python-可解释的机器学习-五-

82 阅读1小时+

Python 可解释的机器学习(五)

原文:annas-archive.org/md5/9b99a159e8340372894ac9bde8bbd5d9

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:单调约束和模型调优以提高解释性

大多数模型类都有超参数,可以通过调整来提高执行速度、增强预测性能和减少过拟合。减少过拟合的一种方法是在模型训练中引入正则化。在第三章解释性挑战中,我们将正则化称为一种补救的解释性属性,它通过惩罚或限制来降低复杂性,迫使模型学习输入的更稀疏表示。正则化模型具有更好的泛化能力,这就是为什么强烈建议使用正则化调整模型以避免对训练数据的过拟合。作为副作用,正则化模型通常具有更少的特征和交互,这使得模型更容易解释——更少的噪声意味着更清晰的信号

尽管有许多超参数,但我们只会关注那些通过控制过拟合来提高解释性的参数。在一定意义上,我们还将回顾通过前几章中探讨的类别不平衡相关超参数来减轻偏差。

第二章解释性的关键概念,解释了三个影响解释性的模型属性:非线性、交互性和非单调性。如果模型自行其是,它可能会学习到一些虚假的、反直觉的非线性和交互性。正如在第十章,为解释性进行特征选择和工程中讨论的那样,可以通过仔细的特征工程来设置限制以防止这种情况。然而,我们如何为单调性设置限制呢?在本章中,我们将学习如何使用单调约束来实现这一点。同样,单调约束可以是模型与特征工程的对应物,而正则化可以是我们在第十章中涵盖的特征选择方法的模型对应物!

本章我们将涵盖的主要主题包括:

  • 通过特征工程设置限制

  • 调整模型以提高解释性

  • 实现模型约束

技术要求

本章的示例使用了mldatasetspandasnumpysklearnxgboostlightgbmcatboosttensorflowbayes_opttensorflow_latticematplotlibseabornscipyxaishap库。如何安装这些库的说明在序言中。

本章的代码位于此处:packt.link/pKeAh

任务

算法公平性问题具有巨大的社会影响,从福利资源的分配到救命手术的优先级,再到求职申请的筛选。这些机器学习算法可以决定一个人的生计或生命,而且往往是边缘化和最脆弱的群体从这些算法中受到最恶劣的对待,因为这些算法持续传播从数据中学到的系统性偏见。因此,贫困家庭可能被错误地归类为虐待儿童;种族少数群体在医疗治疗中可能被优先级过低;而女性可能被排除在高薪技术工作之外。即使在涉及不那么直接和个性化的风险的情况下,如在线搜索、Twitter/X 机器人账户和社交媒体档案,社会偏见如精英主义、种族主义、性别歧视和年龄歧视也会得到加强。

本章将继续延续第六章的主题,即锚点和反事实解释。如果您不熟悉这些技术,请回过头去阅读第六章,以获得对问题的深入了解。第六章中的再犯案例是算法偏差的一个例子。开发COMPAS 算法(其中COMPAS代表矫正犯人管理配置文件替代制裁)的公司的联合创始人承认,在没有与种族相关的问题的情况下很难给出分数。这种相关性是分数对非裔美国人产生偏见的主要原因之一。另一个原因是训练数据中黑人被告可能被过度代表。我们无法确定这一点,因为我们没有原始的训练数据,但我们知道非白人少数族裔在服刑人员群体中被过度代表。我们还知道,由于与轻微毒品相关罪行相关的编码歧视和黑人社区的过度执法,黑人通常在逮捕中被过度代表。

那么,我们该如何解决这个问题呢?

第六章锚点和反事实解释中,我们通过一个代理模型成功地证明了 COMPAS 算法存在偏见。对于本章,让我们假设记者发表了你的发现,一个算法正义倡导团体阅读了文章并联系了你。制作犯罪评估工具的公司没有对偏见承担责任,声称他们的工具只是反映了现实。该倡导团体雇佣你来证明机器学习模型可以被训练得对黑人被告的偏见显著减少,同时确保该模型仅反映经过验证的刑事司法现实

这些被证实的现实包括随着年龄增长,再犯风险单调下降,以及与先前的强烈相关性,这种相关性随着年龄的增长而显著增强。学术文献支持的另一个事实是,女性在总体上显著不太可能再犯和犯罪。

在我们继续之前,我们必须认识到监督学习模型在从数据中捕获领域知识方面面临几个障碍。例如,考虑以下情况:

  • 样本、排除或偏见偏差:如果您的数据并不能真正代表模型意图推广的环境,会怎样?如果是这样,领域知识将与您在数据中观察到的结果不一致。如果产生数据的那个环境具有固有的系统性或制度性偏见,那么数据将反映这些偏见。

  • 类别不平衡:如第十一章“偏差缓解和因果推断方法”中所述,类别不平衡可能会使某些群体相对于其他群体更有利。在追求最高准确率的最有效途径中,模型将从这个不平衡中学习,这与领域知识相矛盾。

  • 非单调性:特征直方图中的稀疏区域或高杠杆异常值可能导致模型在领域知识要求单调性时学习到非单调性,任何之前提到的问题都可能促成这一点。

  • 无影响力的特征:一个未正则化的模型将默认尝试从所有特征中学习,只要它们携带一些信息,但这会阻碍从相关特征中学习或过度拟合训练数据中的噪声。一个更简约的模型更有可能支持由领域知识支持的特性。

  • 反直觉的交互作用:如第十章“用于可解释性的特征选择和工程”中提到的,模型可能会偏好与领域知识支持的交互作用相反的反直觉交互作用。作为一种副作用,这些交互作用可能会使一些与它们相关的群体受益。在第六章“锚点和反事实解释”中,我们通过理解双重标准证明了这一点。

  • 例外情况:我们的领域知识事实基于总体理解,但在寻找更细粒度的模式时,模型会发现例外,例如女性再犯风险高于男性的区域。已知现象可能不支持这些模型,但它们可能是有效的,因此我们必须小心不要在我们的调整努力中抹去它们。

该倡导组织已验证数据仅足以代表佛罗里达州的一个县,并且他们已经向您提供了一个平衡的数据集。第一个障碍很难确定和控制。第二个问题已经得到解决。现在,剩下的四个问题就交给你来处理了!

方法

您已经决定采取三步走的方法,如下所示:

  • 使用特征工程设置护栏:借鉴第六章“锚点和反事实解释”中学习到的经验,以及我们已有的关于先验和年龄的领域知识,我们将设计一些特征。

  • 调整模型以提高可解释性:一旦数据准备就绪,我们将使用不同的类别权重和过拟合预防技术调整许多模型。这些方法将确保模型不仅泛化能力更好,而且更容易解释。

  • 实施模型约束:最后但同样重要的是,我们将对最佳模型实施单调性和交互约束,以确保它们不会偏离可信和公平的交互。

在最后两个部分中,我们将确保模型准确且公平地执行。我们还将比较数据和模型之间的再犯风险分布,以确保它们一致。

准备工作

你可以在这里找到这个示例的代码:github.com/PacktPublishing/Interpretable-Machine-Learning-with-Python-2E/blob/main/12/Recidivism_part2.ipynb

加载库

要运行此示例,您需要安装以下库:

  • mldatasets 用于加载数据集

  • pandasnumpy 用于操作

  • sklearn(scikit-learn)、xgboostlightgbmcatboosttensorflowbayes_opttensorflow_lattice 用于分割数据和拟合模型

  • matplotlibseabornscipyxaishap 以可视化解释

您应该首先加载所有这些库,如下所示:

import math
import os
import copy
import mldatasets
import pandas as pd
import numpy as np
from sklearn import preprocessing, model_selection, metrics,\
    linear_model, svm, neural_network, ensemble
import xgboost as xgb
import lightgbm as lgb
import catboost as cb
import tensorflow as tf
from bayes_opt import BayesianOptimization
import tensorflow_lattice as tfl
from tensorflow.keras.wrappers.scikit_learn import\
                                                  KerasClassifier
import matplotlib.pyplot as plt
import seaborn as sns
import scipy
import xai
import shap 

让我们检查 tensorflow 是否加载了正确的版本,使用 print(tf.__version__)。这应该是 2.8 版本及以上。

理解和准备数据

我们将数据以这种方式加载到我们称为 recidivism_df 的 DataFrame 中:

recidivism_df = mldatasets.**load**("recidivism-risk-balanced") 

应该有超过 11,000 条记录和 11 个列。我们可以使用 info() 验证这一点,如下所示:

recidivism_df.info() 

上一段代码输出了以下内容:

RangeIndex: 11142 entries, 0 to 11141
Data columns (total 12 columns):
#   Column                   Non-Null Count  Dtype  
---  ------                   --------------  -----  
0   sex                      11142 non-null  object
1   age                      11142 non-null  int64
2   race                     11142 non-null  object
3   juv_fel_count            11142 non-null  int64
4   juv_misd_count           11142 non-null  int64
5   juv_other_count          11142 non-null  int64
6   priors_count             11142 non-null  int64
7   c_charge_degree          11142 non-null  object
8   days_b_screening_arrest  11142 non-null  float64
9   length_of_stay           11142 non-null  float64
10  compas_score             11142 non-null  int64
11  is_recid                 11142 non-null  int64
dtypes: float64(2), int64(7), object(3) 

输出检查无误。没有缺失值,除了三个特征(sexracecharge_degree)外,所有特征都是数值型的。这是我们用于 第六章锚点和反事实解释 的相同数据,因此数据字典完全相同。然而,数据集已经通过采样方法进行了平衡,这次它没有为我们准备,因此我们需要这样做,但在这样做之前,让我们了解平衡做了什么。

验证采样平衡

我们可以使用 XAI 的 imbalance_plot 检查 raceis_recid 的分布情况。换句话说,它将统计每个 race-is_recid 组合的记录数量。这个图将使我们能够观察每个 race 的被告中是否有再犯人数的不平衡。代码可以在以下片段中查看:

categorical_cols_l = [
    'sex', 'race', 'c_charge_degree', 'is_recid', 'compas_score'
]
xai.**imbalance_plot**(
    recidivism_df,
    'race',
    'is_recid',
    categorical_cols=categorical_cols_l
) 

前面的代码输出了图 12.1,它描述了所有种族的is_recid=0is_recid=1的数量相等。然而,其他种族在数量上与其他种族不相等。顺便提一下,这个数据集版本将所有其他种族都归入了其他类别,选择不upsample 其他downsample其他两个种族以实现总数相等,是因为它们在被告人口中代表性较低。这种平衡选择是在这种情况下可以做的许多选择之一。从人口统计学角度看,这完全取决于你的数据应该代表什么。被告?囚犯?普通民众中的平民?以及在哪一层面?县一级?州一级?国家一级?

输出结果如下:

图片

图 12.1:按种族分布的两年再犯率(is_recid)

接下来,让我们计算每个特征与目标变量单调相关性的程度。Spearman 等级相关系数在本章中将起到关键作用,因为它衡量了两个特征之间的单调性。毕竟,本章的一个技术主题是单调约束,主要任务是产生一个显著减少偏差的模型。

我们首先创建一个新的 DataFrame,其中不包含compas_scorerecidivism_corr_df)。使用这个 DataFrame,我们输出一个带有feature列的彩色 DataFrame,其中包含前 10 个特征的名称,以及另一个带有所有 10 个特征与第 11 个特征(目标变量)的 Spearman 相关系数(correlation_to_target)。代码如下所示:

recidivism_corr_df = recidivism_df.**drop**(
    ['compas_score'], axis=1
)
pd.**DataFrame**(
    {'feature': recidivism_corr_df.columns[:-1],
     'correlation_to_target':\
          scipy.stats.**spearmanr**(recidivism_corr_df).\
          correlation[10,:-1]
    }
).style.background_gradient(cmap='coolwarm') 

前面的代码输出了图 12.2所示的 DataFrame。最相关的特征是priors_count,其次是age、三个青少年计数和sexc_charge_degreedays_b_screening_arrestlength_of_stayrace的系数可以忽略不计。

输出结果如下:

表格描述自动生成

图 12.2:在特征工程之前,所有特征对目标变量的 Spearman 系数

接下来,我们将学习如何使用特征工程将一些领域知识“嵌入”到特征中。

使用特征工程设置护栏

第六章锚点和反事实解释中,我们了解到除了race之外,在我们解释中最突出的特征是agepriors_countc_charge_degree。幸运的是,数据现在已经平衡,因此这种不平衡导致的种族偏见现在已经消失。然而,通过锚点和反事实解释,我们发现了一些令人不安的不一致性。在agepriors_count的情况下,这些不一致性是由于这些特征的分布方式造成的。我们可以通过特征工程来纠正分布问题,从而确保模型不会从不均匀的分布中学习。在c_charge_degree的情况下,由于它是分类的,它缺乏可识别的顺序,这种缺乏顺序导致了不直观的解释。

在本节中,我们将研究序列化离散化交互项,这是通过特征工程设置护栏的三种方式。

序列化

c_charge_degree category:
recidivism_df.c_charge_degree.**value_counts()** 

前面的代码生成了以下输出:

(F3)     6555
(M1)     2632
(F2)      857
(M2)      768
(F1)      131
(F7)      104
(MO3)      76
(F5)        7
(F6)        5
(NI0)       4
(CO3)       2
(TCX)       1 

每个电荷度数对应电荷的重力。这些重力有一个顺序,使用分类特征时会丢失。我们可以通过用相应的顺序替换每个类别来轻松解决这个问题。

我们可以对此顺序进行很多思考。例如,我们可以查看判决法或指南——对于不同的程度,实施了最低或最高的监禁年数。我们还可以查看这些人的平均暴力统计数据,并将这些信息分配给电荷度数。每个此类决策都存在潜在的偏见,如果没有充分的证据支持它,最好使用整数序列。所以,我们现在要做的就是创建一个字典(charge_degree_code_rank),将度数映射到从低到高对应的重力等级的数字。然后,我们可以使用pandasreplace函数使用这个字典来进行替换。以下代码片段中可以看到代码:

charge_degree_code_rank = {
    '(F10)': 15, '(F9)':14, '(F8)':13,\
    '(F7)':12, '(TCX)':11, '(F6)':10, '(F5)':9,\
    '(F4)':8, '(F3)':7, '(F2)':6, '(F1)':5, '(M1)':4,\
    '(NI0)':4, '(M2)':3, '(CO3)':2, '(MO3)':1, '(X)':0
}
recidivism_df.c_charge_degree.**replace**(
    charge_degree_code_rank, inplace=True
) 

评估这种顺序如何对应再犯概率的一种方法是通过一条线图,显示随着电荷度数的增加,它如何变化。我们可以使用一个名为plot_prob_progression的函数来做这件事,它接受一个连续特征作为第一个参数(c_charge_degree),以衡量一个二元特征的概率(is_recid)。它可以按区间(x_intervals)分割连续特征,甚至可以使用分位数(use_quantiles)。最后,你可以定义轴标签和标题。以下代码片段中可以看到代码:

mldatasets.**plot_prob_progression**(
    recidivism_df.**c_charge_degree**,
    recidivism_df.**is_recid**, x_intervals=12,
    use_quantiles=False,
    xlabel='Relative Charge Degree',
    title='Probability of Recidivism by Relative Charge Degree'
) 

前面的代码生成了图 12.3 中的图表。随着现在排名的电荷度数的增加,趋势是 2 年再犯的概率降低,除了排名 1。在概率下方有柱状图显示了每个排名的观测值的分布。由于分布非常不均匀,你应该谨慎对待这种趋势。你会注意到一些排名,如 0、8 和 13-15,没有在图表中,因为电荷度数的类别存在于刑事司法系统中,但在数据中不存在。

输出结果如下:

图表,折线图  自动生成的描述

图 12.3:按电荷度数的概率进展图

在特征工程方面,我们无法做更多的事情来改进 c_charge_degree,因为它已经代表了现在带有顺序的离散类别。除非我们有证据表明否则,任何进一步的转换都可能导致信息的大量丢失。另一方面,连续特征本质上具有顺序;然而,由于它们携带的精度水平,可能会出现问题。因为小的差异可能没有意义,但数据可能告诉模型否则。不均匀的分布和反直觉的交互只会加剧这个问题。

离散化

为了理解如何最佳地离散化我们的年龄连续特征,让我们尝试两种不同的方法。我们可以使用等宽离散化,也称为固定宽度箱或区间,这意味着箱的大小由 决定,其中 N 是箱的数量。另一种方法是使用等频离散化,也称为分位数,这确保每个箱大约有相同数量的观测值。尽管如此,有时由于直方图的偏斜性质,可能无法以 N 种方式分割它们,因此你可能最终得到 N-1N-2 个分位数。

使用 plot_prob_progression 比较这两种方法很容易,但这次我们生成了两个图表,一个使用固定宽度箱(use_quantiles=False),另一个使用分位数(use_quantiles=True)。代码可以在下面的代码片段中看到:

mldatasets.plot_**prob_progression**(
    recidivism_df.**age**,
    recidivism_df.**is_recid**,
    x_intervals=7,
    use_quantiles=False,
    title='Probability of Recidivism by Age Discretized in Fix-Width \
    Bins',
    xlabel='Age'
)
mldatasets.**plot_prob_progression**(
    recidivism_df.**age**,
    recidivism_df.**is_recid**,
    x_intervals=7, use_quantiles=True,
    title='Probability of Recidivism by Age Discretized \
    in Quantiles',
    xlabel='Age'
) 
Figure 12.4. By looking at the Observations portion of the fixed-width bin plot, you can tell that the histogram for the age feature is right-skewed, which causes the probability to shoot up for the last bin. The reason for this is that some outliers exist in this bin. On the other hand, the fixed-frequency (quantile) plot histogram is more even, and probability consistently decreases. In other words, it’s monotonic—as it should be, according to our domain knowledge on the subject.

输出结果如下:

图 12.4:比较两种年龄离散化方法

很容易观察到为什么使用分位数对特征进行箱化是一个更好的方法。我们可以将 age 工程化为一个新的特征,称为 age_grouppandasqcut 函数可以执行基于分位数的离散化。代码可以在下面的代码片段中看到:

recidivism_df['age_group'] = pd.**qcut**(
    recidivism_df.**age**, 7, precision=0
).astype(str) 

因此,我们现在已经将age离散化为age_group。然而,必须注意的是,许多模型类会自动进行离散化,那么为什么还要这么做呢?因为这允许你控制其影响。否则,模型可能会选择不保证单调性的桶。例如,模型可能会在可能的情况下始终使用 10 个分位数。尽管如此,如果你尝试在age上使用这种粒度(x_intervals=10),你最终会在概率进展中遇到峰值。我们的目标是确保模型会学习到ageis_recid的发病率之间存在单调关系,如果我们允许模型选择可能或可能不达到相同目标的桶,我们就无法确定这一点。

我们将移除age,因为age_group包含了我们所需的所有信息。但是等等——你可能会问——移除这个变量会不会丢失一些重要信息?是的,但仅仅是因为它与priors_count的交互作用。所以,在我们丢弃任何特征之前,让我们检查这种关系,并意识到通过创建交互项,我们如何通过移除age来保留一些丢失的信息,同时保持交互。

交互项和非线性变换

我们从第六章锚点和反事实解释中已经知道,agepriors_count是最重要的预测因子之一,我们可以观察到它们如何一起影响再犯的发病率(is_recid),使用plot_prob_contour_map。这个函数产生带有彩色等高线区域的等高线,表示不同的幅度。它们在地理学中很有用,可以显示海拔高度。在机器学习中,它们可以显示一个二维平面,表示特征与度量之间的交互。在这种情况下,维度是agepriors_count,度量是再犯的发病率。这个函数接收到的参数与plot_prob_progression相同,只是它接受对应于x轴和y轴的两个特征。代码可以在下面的代码片段中看到:

mldatasets.plot_**prob_contour_map**(
    recidivism_df.**age**,
    recidivism_df.**priors_count**,
    recidivism_df.**is_recid**,
    use_quantiles=True,
    xlabel='Age',
    ylabel='Priors Count',
    title='Probability of Recidivism by Age/Priors Discretized in \
    Quantiles'
) 
Figure 12.5, which shows how, when discretized by quantiles, the probability of 2-year recidivism increases, the lower the age and the higher the priors_count. It also shows histograms for both features. priors_count is very right-skewed, so discretization is challenging, and the contour map does not offer a perfectly diagonal progression between the bottom right and top left. And if this plot looks familiar, it’s because it’s just like the partial dependence interaction plots we produced in *Chapter 4*, *Global Model-Agnostic Interpretation Methods*, except it’s not measured against the predictions of a model but the ground truth (is_recid). We must distinguish between what the data can tell us directly and what the model has learned from it.

输出结果如下:

图片

图 12.5:年龄和先前的计数再犯概率等高线图

我们现在可以构建一个包含两个特征的交互项。即使等高线图将特征离散化以观察更平滑的进展,我们也不需要将这种关系离散化。有意义的是将其作为每年priors_count的比率。但是从哪一年开始算起?当然是被告成年以来的年份。但是要获得这些年份,我们不能使用age - 18,因为这会导致除以零,所以我们将使用17代替。当然,有许多方法可以做到这一点。最好的方法是我们假设年龄有小数,通过减去 18,我们可以计算出非常精确的priors_per_year比率。然而,不幸的是,我们并没有这样的数据。你可以在下面的代码片段中看到代码:

recidivism_df['priors_per_year'] =\
            recidivism_df['priors_count']/(recidivism_df['age'] - 17) 

黑盒模型通常会自动找到交互项。例如,神经网络中的隐藏层具有所有一阶交互项,但由于非线性激活,它并不仅限于线性组合。然而,“手动”定义交互项甚至非线性转换,一旦模型拟合完成,我们可以更好地解释这些交互项。此外,我们还可以对它们使用单调约束,这正是我们稍后将在priors_per_year上所做的。现在,让我们检查其单调性是否通过plot_prob_progression保持。查看以下代码片段:

mldatasets.**plot_prob_progression**(
    recidivism_df.**priors_per_year**,
    recidivism_df.**is_recid**,
    x_intervals=8,
    xlabel='Priors Per Year',
    title='Probability of Recidivism by Priors per Year (\
    according to data)'
) 

前面的代码片段输出以下截图,显示了新特征的几乎单调进展:

图表,折线图  自动生成的描述

图 12.6:priors_per_year的先验概率进展

priors_per_year不是更单调的原因是 3.0 以上的priors_per_year区间非常稀疏。因此,对这些少数被告强制执行该特征的单调性将非常不公平,因为他们呈现了 75%的风险下降。解决这一问题的方法之一是将它们左移,将这些观察结果中的priors_per_year设置为-1,如下面的代码片段所示:

recidivism_df.loc[recidivism_df.priors_per_year > 3,\
                  'priors_per_year'] = -1 

当然,这种移动会略微改变特征的解释,考虑到-1的少数值实际上意味着超过3。现在,让我们生成另一个等高线图,但这次是在age_grouppriors_per_year之间。后者将按分位数(y_intervals=6, use_quantiles=True)进行离散化,以便更容易观察到再犯概率。以下代码片段显示了代码:

mldatasets.**plot_prob_contour_map**(
    recidivism_df.**age_group**,
    recidivism_df.**priors_per_year**,
    recidivism_df.**is_recid**,
    y_intervals=6,
    use_quantiles=True,
    xlabel='Age Group',
    title='Probability of Recidivism by Age/Priors per Year \
    Discretized in Quantiles', ylabel='Priors Per Year'
) 
 generates the contours in *Figure 12.7*. It shows that, for the most part, the plot moves in one direction. We were hoping to achieve this outcome because it allows us, through one interaction feature, to control the monotonicity of what used to involve two features.

输出结果如下:

![图片,B18406_12_07.png]

图 12.7:age_grouppriors_per_year的再犯概率等高线图

几乎一切准备就绪,但age_group仍然是分类的,所以我们必须将其编码成数值形式。

分类别编码

对于age_group的最佳分类编码方法是序数编码,也称为标签编码,因为它会保留其顺序。我们还应该对数据集中的其他两个分类特征进行编码,即sexrace。对于sex,序数编码将其转换为二进制形式——相当于虚拟编码。另一方面,race是一个更具挑战性的问题,因为它有三个类别,使用序数编码可能会导致偏差。然而,是否使用独热编码取决于你使用的模型类别。基于树的模型对序数特征没有偏差问题,但其他基于特征权重的模型,如神经网络和逻辑回归,可能会因为这种顺序而产生偏差。

考虑到数据集已经在种族上进行了平衡,因此这种情况发生的风险较低,我们稍后无论如何都会移除这个特征,所以我们将继续对其进行序数编码。

为了对三个特征进行序数编码,我们将使用 scikit-learn 的OrdinalEncoder。我们可以使用它的fit_transform函数一次性拟合和转换特征。然后,我们还可以趁机删除不必要的特征。请看下面的代码片段:

cat_feat_l = ['sex', 'race', 'age_group']
ordenc = preprocessing.**OrdinalEncoder**(dtype=np.int8)
recidivism_df[cat_feat_l] =\
                  ordenc.**fit_transform**(recidivism_df[cat_feat_l])
recidivism_df.drop(['age', 'priors_count', 'compas_score'],\
                    axis=1, inplace=True) 

现在,我们还没有完全完成。我们仍然需要初始化我们的随机种子并划分我们的数据为训练集和测试集。

其他准备工作

下一步的准备工作很简单。为了确保可重复性,让我们在需要的地方设置随机种子,然后将我们的y设置为is_recid,将X设置为其他所有特征。我们对这两个进行train_test_split。最后,我们使用X后跟y重建recidivism_df DataFrame。这样做只有一个原因,那就是is_recid是最后一列,这将有助于下一步。代码可以在这里看到:

rand = 9
os.environ['PYTHONHASHSEED'] = str(rand)
tf.random.set_seed(rand)
np.random.seed(rand)
y = recidivism_df['is_recid']
X = recidivism_df.drop(['is_recid'], axis=1).copy()
X_train, X_test, y_train, y_test = model_selection.**train_test_split**(
    X, y, test_size=0.2, random_state=rand
)
recidivism_df = X.join(y) 

现在,我们将验证 Spearman 的相关性是否在需要的地方有所提高,在其他地方保持不变。请看下面的代码片段:

pd.DataFrame(
    {
        'feature': X.columns,
        'correlation_to_target':scipy.stats.**spearmanr**(recidivism_df).\
        correlation[10,:-1]
    }
).style.background_gradient(cmap='coolwarm') 

前面的代码输出了图 12.8中所示的 DataFrame。请将其与图 12.2进行比较。请注意,在分位数离散化后,age与目标变量的单调相关性略有降低。一旦进行序数编码,c_charge_degree的相关性也大大提高,而priors_per_year相对于priors_count也有所改善。其他特征不应受到影响,包括那些系数最低的特征。

输出如下:

表格描述自动生成图 12.8:所有特征与目标变量的 Spearman 相关系数(特征工程后)

系数最低的特征在模型中可能也是不必要的,但我们将让模型通过正则化来决定它们是否有用。这就是我们接下来要做的。

调整模型以提高可解释性

传统上,正则化是通过在系数或权重上施加惩罚项(如L1L2弹性网络)来实现的,这会减少最不相关特征的影响。如第十章“可解释性特征选择和工程”部分的嵌入式方法中所示,这种正则化形式在特征选择的同时也减少了过拟合。这使我们来到了正则化的另一个更广泛的概念,它不需要惩罚项。通常,这相当于施加限制或停止标准,迫使模型限制其复杂性。

除了正则化,无论是其狭义(基于惩罚)还是广义(过拟合方法),还有其他方法可以调整模型以提高可解释性——也就是说,通过调整训练过程来提高模型的公平性、责任性和透明度。例如,我们在第十章特征选择和可解释性工程中讨论的类别不平衡超参数,以及第十一章偏差缓解和因果推断方法中的对抗性偏差,都有助于提高公平性。此外,我们将在本章进一步研究的约束条件对公平性、责任性和透明度也有潜在的好处。

有许多不同的调整可能性和模型类别。如本章开头所述,我们将关注与可解释性相关的选项,但也将模型类别限制在流行的深度学习库(Keras)、一些流行的树集成(XGBoost、随机森林等)、支持向量机SVMs)和逻辑回归。除了最后一个,这些都被认为是黑盒模型。

调整 Keras 神经网络

对于 Keras 模型,我们将通过超参数调整和分层 K 折交叉验证来选择最佳正则化参数。我们将按照以下步骤进行:

  1. 首先,我们需要定义模型和要调整的参数。

  2. 然后,我们进行调整。

  3. 接下来,我们检查其结果。

  4. 最后,我们提取最佳模型并评估其预测性能。

让我们详细看看这些步骤。

定义模型和要调整的参数

我们首先应该创建一个函数(build_nn_mdl)来构建和编译一个可正则化的 Keras 模型。该函数接受一些参数,以帮助调整模型。它接受一个包含隐藏层中神经元数量的元组(hidden_layer_sizes),以及应用于层核的 L1(l1_reg)和 L2(l1_reg)正则化值。最后,它还接受dropout参数,与 L1 和 L2 惩罚不同,它是一种随机正则化方法,因为它采用随机选择。请看以下代码片段:

def **build_nn_mdl**(hidden_layer_sizes, l1_reg=0, l2_reg=0, dropout=0):
    nn_model = tf.keras.Sequential([
        tf.keras.Input(shape=[len(X_train.keys())]),\
        tf.keras.layers.experimental.preprocessing.**Normalization**()
    ])
    reg_args = {}
    if (l1_reg > 0) or (l2_reg > 0):
        reg_args = {'kernel_regularizer':\
                    tf.keras.regularizers.**l1_l2**(l1=l1_reg, l2=l2_reg)}
    for hidden_layer_size in hidden_layer_sizes:
        nn_model.add(tf.keras.layers.**Dense**(hidden_layer_size,\
                        activation='relu', ****reg_args**))
    if dropout > 0:
        nn_model.add(tf.keras.layers.**Dropout**(dropout))
    nn_model.add(tf.keras.layers.**Dense**(1, activation='sigmoid'))
    nn_model.compile(
        loss='binary_crossentropy',
        optimizer=tf.keras.optimizers.Adam(lr=0.0004),
        metrics=['accuracy',tf.keras.metrics.AUC(name='auc')]
)
    return nn_model 

之前的功能将模型(nn_model)初始化为一个 Sequential 模型,其输入层与训练数据中的特征数量相对应,并添加一个 Normalization() 层来标准化输入。然后,如果任一惩罚项超过零,它将设置一个字典(reg_args),将 kernel_regularizer 分配给 tf.keras.regularizers.l1_l2 并用这些惩罚项初始化。一旦添加了相应的 hidden_layer_size 的隐藏(Dense)层,它将 reg_args 字典作为额外参数传递给每个层。在添加所有隐藏层之后,它可以选择添加 Dropout 层和具有 sigmoid 激活的最终 Dense 层。然后,模型使用 binary_crossentropy 和具有较慢学习率的 Adam 优化器编译,并设置为监控 accuracyauc 指标。

运行超参数调整

现在我们已经定义了模型和要调整的参数,我们初始化了 RepeatedStratifiedKFold 交叉验证器,它将训练数据分成五份,总共重复三次(n_repeats),每次重复使用不同的随机化。然后我们为网格搜索超参数调整创建一个网格(nn_grid)。它只测试三个参数(l1_regl2_regdropout)的两个可能选项,这将产生 种组合。我们将使用 scikit-learn 包装器(KerasClassifier)来使我们的模型与 scikit-learn 网格搜索兼容。说到这一点,我们接下来初始化 GridSearchCV,它使用 Keras 模型(estimator)执行交叉验证网格搜索(param_grid)。我们希望它根据精度(scoring)选择最佳参数,并且在过程中不抛出错误(error_score=0)。最后,我们像使用任何 Keras 模型一样拟合 GridSearchCV,传递 X_trainy_trainepochsbatch_size。代码可以在以下代码片段中看到:

cv = model_selection.**RepeatedStratifiedKFold**(
    n_splits=5,
    n_repeats=3,
    random_state=rand
)
nn_grid = {
    'hidden_layer_sizes':[(80,)],
    'l1_reg':[0,0.005],
    'l2_reg':[0,0.01],
    'dropout':[0,0.05]
}
nn_model = KerasClassifier(build_fn=build_nn_mdl)
nn_grid_search = model_selection.**GridSearchCV**(
    estimator=nn_model,
    cv=cv,
    n_jobs=-1,
    param_grid=nn_grid,
    scoring='precision',
    error_score=0
)
nn_grid_result = nn_grid_search.**fit**(
    X_train.astype(float),
    y_train.astype(float),
    epochs=400,batch_size=128
) 

接下来,我们可以检查网格搜索的结果。

检查结果

一旦完成网格搜索,你可以使用以下命令输出最佳参数:print(nn_grid_result.best_params_)。或者,你可以将所有结果放入一个 DataFrame 中,按最高精度(sort_values)排序,并按以下方式输出:

pd.**DataFrame**(nn_grid_result.**cv_results**_)[
    [
        'param_hidden_layer_sizes',
        'param_l1_reg',
        'param_l2_reg',
        'param_dropout',
        'mean_test_score',
        'std_test_score',
        'rank_test_score'
    ]
].**sort_values**(by='rank_test_score') 
Figure 12.9. The unregularized model is dead last, showing that all regularized model combinations performed better. One thing to note is that given the 1.52% standard deviations (std_test_score) and that the top performer is only 2.2% from the lowest performer, in this case, the benefits are marginal from a precision standpoint, but you should use a regularized model nonetheless because of other benefits.

输出如下所示:

表格描述自动生成图 12.9:神经网络模型交叉验证网格搜索的结果

评估最佳模型

网格搜索产生的另一个重要元素是表现最佳模型(nn_grid_result.best_estimator_)。我们可以创建一个字典来存储我们将在本章中拟合的所有模型(fitted_class_mdls),然后使用 evaluate_class_mdl 评估这个正则化的 Keras 模型,并将评估结果同时保存在字典中。请查看以下代码片段:

fitted_class_mdls = {}
fitted_class_mdls['keras_reg'] = mldatasets.**evaluate_class_mdl**(
    nn_grid_result.best_estimator_,
    X_train.astype(float),
    X_test.astype(float),
    y_train.astype(float),
    y_test.astype(float),
    plot_roc=False,
    plot_conf_matrix=True,
    **ret_eval_dict=****True**
) 
Figure 12.10. The accuracy is a little bit better than the original COMPAS model from *Chapter 6*, *Anchors and Counterfactual Explanations*, but the strategy to optimize for higher precision while regularizing yielded a model with nearly half as many false positives but 50% more false negatives.

输出如下所示:

图表,树状图  自动生成的描述图 12.10:正则化 Keras 模型的评估

通过使用自定义损失函数或类权重,可以进一步校准类平衡,正如我们稍后将要做的。接下来,我们将介绍如何调整其他模型类。

调整其他流行模型类

在本节中,我们将拟合许多不同的模型,包括未正则化和正则化的模型。为此,我们将从广泛的参数中选择,这些参数执行惩罚正则化,通过其他方式控制过拟合,并考虑类别不平衡。

相关模型参数的简要介绍

供您参考,有两个表格包含用于调整许多流行模型的参数。这些已经被分为两部分。Part A(图 12.11)包含五个具有惩罚正则化的 scikit-learn 模型。Part B(图 12.12)显示了所有树集成,包括 scikit-learn 的随机森林模型和来自最受欢迎的增强树库(XGBoost、LightGBM 和 CatBoost)的模型。

Part A 可以在这里查看:

表格,日历  自动生成的描述图 12.11:惩罚正则化 scikit-learn 模型的调整参数

在图 12.11 中,您可以在列中观察到模型,在行中观察到相应的参数名称及其默认值在右侧。在参数名称和默认值之间,有一个加号或减号,表示是否改变默认值的一个方向或另一个方向应该使模型更加保守。这些参数还按以下类别分组:

  • 算法:一些训练算法不太容易过拟合,但这通常取决于数据。

  • 正则化:仅在更严格的意义上。换句话说,控制基于惩罚的正则化的参数。

  • 迭代:这控制执行多少个训练轮次、迭代或 epoch。调整这个方向或另一个方向可能会影响过拟合。在基于树的模型中,估计器或树的数量是类似的。

  • 学习率:这控制学习发生的速度。它与迭代一起工作。学习率越低,需要的迭代次数越多以优化目标函数。

  • 提前停止:这些参数控制何时停止训练。这允许您防止您的模型对训练数据过拟合。

  • 类别不平衡:对于大多数模型,这在损失函数中惩罚了较小类别的误分类,对于基于树的模型,特别是这样,它被用来重新加权分割标准。无论如何,它只与分类器一起工作。

  • 样本权重:我们在第十一章“偏差缓解和因果推断方法”中利用了这一点,根据样本分配权重以减轻偏差。

标题中既有分类模型也有回归模型,并且它们共享相同的参数。请注意,scikit-learn 的LinearRegressionLogisticRegression下没有特色,因为它没有内置的正则化。无论如何,我们将在本节中仅使用分类模型。

B 部分可以在这里看到:

表格,日历  自动生成的描述

表格,日历  自动生成的描述

图 12.12:树集成模型的调整参数

图 12.12图 12.11非常相似,除了它有更多仅在树集成中可用的参数类别,如下所示:

  • 特征采样:这种方法通过在节点分裂、节点或树训练中考虑较少的特征来实现。因为它随机选择特征,所以它是一种随机正则化方法。

  • 树的大小:这通过最大深度、最大叶子数或其他限制其增长的参数来约束树,从而反过来抑制过拟合。

  • 分裂:任何控制树中节点如何分裂的参数都可以间接影响过拟合。

  • 袋装:也称为自助聚合,它首先通过自助采样开始,这涉及到从训练数据中随机抽取样本来拟合弱学习器。这种方法减少了方差,有助于减少过拟合,并且相应地,采样参数通常在超参数调整中很突出。

  • 约束:我们将在下一节中进一步详细解释这些内容,但这是如何将特征约束以减少或增加对输出的影响。它可以在数据非常稀疏的领域减少过拟合。然而,减少过拟合通常不是主要目标,而交互约束可以限制哪些特征可以交互。

请注意,图 12.12中带有星号(*)的参数表示在fit函数中设置的,而不是用模型初始化的。此外,除了 scikit-learn 的RandomForest模型外,所有其他参数通常有许多别名。对于这些,我们使用 scikit-learn 的包装函数,但所有参数也存在于原生版本中。我们不可能在这里解释每个模型参数,但建议您直接查阅文档以深入了解每个参数的作用。本节的目的在于作为指南或参考。

接下来,我们将采取与我们对 Keras 模型所做类似的步骤,但一次针对许多不同的模型,最后我们将评估最适合公平性的最佳模型。

批量超参数调整模型

好的——既然我们已经快速了解了我们可以拉动的哪些杠杆来调整模型,那么让我们定义一个包含所有模型的字典,就像我们在其他章节中所做的那样。这次,我们包括了一个用于网格搜索的参数值的grid。看看下面的代码片段:

class_mdls = {
    'logistic':{
        'model':linear_model.**LogisticRegression**(random_state=rand,\
                                                max_iter=1000),
        'grid':{
            'C':np.linspace(0.01, 0.49, 25),
            'class_weight':[{0:6,1:5}],
            'solver':['lbfgs', 'liblinear', 'newton-cg']
        }
     },
    'svc':{
        'model':svm.**SVC**(probability=True, random_state=rand),
        'grid':{'C':[15,25,40], 'class_weight':[{0:6,1:5}]}
    },
    'nu-svc':{
        'model':svm.**NuSVC**(
            probability=True,
            random_state=rand
        ),
        'grid':{
            'nu':[0.2,0.3], 'gamma':[0.6,0.7],\
            'class_weight':[{0:6,1:5}]}
        },
    'mlp':{
        'model':neural_network.**MLPClassifier**(
            random_state=rand,
            hidden_layer_sizes=(80,),
            early_stopping=True
        ),
        'grid':{
            'alpha':np.linspace(0.05, 0.15, 11),
            'activation':['relu','tanh','logistic']}
        },
        'rf':{
            'model':ensemble.**RandomForestClassifier**(
                random_state=rand, max_depth=7, oob_score=True, \
                bootstrap=True
             ),
            'grid':{
                'max_features':[6,7,8],
                'max_samples':[0.75,0.9,1],
                'class_weight':[{0:6,1:5}]}
            },
    'xgb-rf':{
        'model':xgb.**XGBRFClassifier**(
            seed=rand, eta=1, max_depth=7, n_estimators=200
        ),
        'grid':{
            'scale_pos_weight':[0.85],
            'reg_lambda':[1,1.5,2],
            'reg_alpha':[0,0.5,0.75,1]}
        },
    'xgb':{
        'model':xgb.**XGBClassifier**(
            seed=rand, eta=1, max_depth=7
        ),
        'grid':{
            'scale_pos_weight':[0.7],
            'reg_lambda':[1,1.5,2],
            'reg_alpha':[0.5,0.75,1]}
        },
    'lgbm':{
        'model':lgb.**LGBMClassifier**(
            random_seed=rand,
            learning_rate=0.7,
            max_depth=5
        ),
        'grid':{
            'lambda_l2':[0,0.5,1],
            'lambda_l1':[0,0.5,1],
            'scale_pos_weight':[0.8]}
        },
    'catboost':{
        'model':cb.**CatBoostClassifier**(
            random_seed=rand,
            depth=5,
            learning_rate=0.5,
            verbose=0
        ),
        'grid':{
            'l2_leaf_reg':[2,2.5,3],
            'scale_pos_weight':[0.65]}
        }
} 

下一步是为字典中的每个模型添加一个for循环,然后deepcopy它并使用fit来生成一个“基础”的非正则化模型。接下来,我们使用evaluate_class_mdl对其进行评估,并将其保存到我们之前为 Keras 模型创建的fitted_class_mdls字典中。现在,我们需要生成模型的正则化版本。因此,我们再次进行deepcopy,并遵循与 Keras 相同的步骤进行RepeatedStratifiedKFold交叉验证网格搜索,并且我们也以相同的方式进行评估,将结果保存到拟合模型字典中。代码如下所示:

for mdl_name in class_mdls:
    base_mdl = copy.deepcopy(class_mdls[mdl_name]['model'])
    base_mdl = base_mdl.**fit**(X_train, y_train)
    fitted_class_mdls[mdl_name+'_base'] = \
        mldatasets.**evaluate_class_mdl**(
            base_mdl, X_train, X_test,y_train, y_test,
            plot_roc=False, plot_conf_matrix=False,
            show_summary=False, ret_eval_dict=True
    )
    reg_mdl = copy.deepcopy(class_mdls[mdl_name]['model'])
    grid = class_mdls[mdl_name]['grid']
    cv = model_selection.**RepeatedStratifiedKFold**(
        n_splits=5, n_repeats=3, random_state=rand
    )
    grid_search = model_selection.**GridSearchCV**(
    estimator=reg_mdl, cv=cv, param_grid=grid,
    scoring='precision', n_jobs=-1, error_score=0, verbose=0
    )
    grid_result = grid_search.**fit**(X_train, y_train)
    fitted_class_mdls[mdl_name+'_reg'] =\
        mldatasets.**evaluate_class_mdl**(
            grid_result.**best_estimator**_, X_train, X_test, y_train,
            y_test, plot_roc=False,
            plot_conf_matrix=False, show_summary=False,
            ret_eval_dict=True
    )
    fitted_class_mdls[mdl_name+'_reg']['cv_best_params'] =\
        grid_result.**best_params**_ 

一旦代码执行完毕,我们可以根据精确度对模型进行排名。

根据精确度评估模型

我们可以提取拟合模型字典的指标,并将它们放入一个 DataFrame 中,使用from_dict。然后我们可以根据最高的测试精确度对模型进行排序,并为最重要的两个列着色编码,这两个列是precision_testrecall_test。代码可以在下面的代码片段中看到:

class_metrics = pd.DataFrame.from_dict(fitted_class_mdls, 'index')[
    [
        'accuracy_train',
        'accuracy_test',
        'precision_train',
        'precision_test',
        'recall_train',
        'recall_test',
        'roc-auc_test',
        'f1_test',
        'mcc_test'
    ]
]
with pd.option_context('display.precision', 3):
    html = class_metrics.sort_values(
        by='precision_test', ascending=False
    ).style.background_gradient(
        cmap='plasma',subset=['precision_test']
    ).background_gradient(
        cmap='viridis', subset=['recall_test'])
html 

前面的代码将输出图 12.13所示的 DataFrame。你可以看出,正则化树集成模型在排名中占据主导地位,其次是它们的非正则化版本。唯一的例外是正则化 Nu-SVC,它排名第一,而它的非正则化版本排名最后!

输出如下所示:

表格描述自动生成

图 12.13:根据交叉验证网格搜索的顶级模型

你会发现,Keras 正则化神经网络模型的精确度低于正则化逻辑回归,但召回率更高。确实,我们希望优化高精确度,因为它会影响假阳性,这是我们希望最小化的,但精确度可以达到 100%,而召回率可以是 0%,如果那样的话,你的模型就不好了。同时,还有公平性,这关乎于保持低假阳性率,并且在种族间均匀分布。因此,这是一个权衡的问题,追求一个指标并不能让我们达到目标。

评估最高性能模型的公平性

为了确定如何进行下一步,我们必须首先评估我们的最高性能模型在公平性方面的表现。我们可以使用compare_confusion_matrices来完成这项工作。正如你使用 scikit-learn 的confusion_matrix一样,第一个参数是真实值或目标值(通常称为y_true),第二个是模型的预测值(通常称为y_pred)。这里的区别是它需要两组y_truey_pred,一组对应于观察的一个部分,另一组对应于另一个部分。在这四个参数之后,你给每个部分起一个名字,所以这就是以下两个参数告诉你的内容。最后,compare_fpr=True确保它将比较两个混淆矩阵之间的假阳性率FPR)。看看下面的代码片段:

y_test_pred = fitted_class_mdls['catboost_reg']['preds_test']
_ = mldatasets.**compare_confusion_matrices**(
    y_test[X_test.race==1],
    y_test_pred[X_test.race==1],
    y_test[X_test.race==0],
    y_test_pred[X_test.race==0],
    'Caucasian',
    'African-American',
    **compare_fpr=****True**
)
y_test_pred =  fitted_class_mdls['catboost_base']['preds_test']
_ = mldatasets.**compare_confusion_matrices**(
    y_test[X_test.race==1],
    y_test_pred[X_test.race==1],
    y_test[X_test.race==0],
    y_test_pred[X_test.race==0],
    'Caucasian',
    'African-American',
    **compare_fpr=****True**
) 
Figure 12.14 and *Figure 12.15*, corresponding to the regularized and base models, respectively. You can see *Figure 12.14* here:

图表,树状图图表,描述自动生成

图 12.14:正则化 CatBoost 模型之间的混淆矩阵

图 12.15告诉我们,正则化模型的 FPR 显著低于基础模型。您可以看到输出如下:

图表,瀑布图,树状图图表,描述自动生成

图 12.15:基础 CatBoost 模型之间的混淆矩阵

然而,如图 12.15 所示的基础模型与正则化模型的 FPR 比率为 1.11,而正则化模型的 FPR 比率为 1.47,尽管整体指标相似,但差异显著。但在尝试同时实现几个目标时,很难评估和比较模型,这就是我们将在下一节中要做的。

使用贝叶斯超参数调整和自定义指标优化公平性

我们的使命是生产一个具有高精确度和良好召回率,同时在不同种族间保持公平性的模型。因此,实现这一使命将需要设计一个自定义指标。

设计一个自定义指标

我们可以使用 F1 分数,但它对精确度和召回率的处理是平等的,因此我们不得不创建一个加权指标。我们还可以考虑每个种族的精确度和召回率的分布情况。实现这一目标的一种方法是通过使用标准差,它量化了这种分布的变化。为此,我们将用精确度的一半作为组间标准差来惩罚精确度,我们可以称之为惩罚后的精确度。公式如下:

![图片,B18406_12_003.png]

我们可以对召回率做同样的处理,如图所示:

![图片,B18406_12_004.png]

然后,我们为惩罚后的精确度和召回率做一个加权平均值,其中精确度是召回率的两倍,如图所示:

![图片,B18406_12_005.png]

为了计算这个新指标,我们需要创建一个可以调用weighted_penalized_pr_average的函数。它接受y_truey_pred作为预测性能指标。然而,它还包括X_group,它是一个包含组值的pandas序列或数组,以及group_vals,它是一个列表,它将根据这些值对预测进行子集划分。在这种情况下,组是race,可以是 0 到 2 的值。该函数包括一个for循环,遍历这些可能的值,通过每个组对预测进行子集划分。这样,它可以计算每个组的精确度和召回率。之后,函数的其余部分只是简单地执行之前概述的三个数学运算。代码可以在以下片段中看到:

def **weighted_penalized_pr_average**(y_true, y_pred, X_group,\
                    group_vals, penalty_mult=0.5,\
                    precision_mult=2,\
                    recall_mult=1):
    precision_all = metrics.**precision_score**(
        y_true, y_pred, zero_division=0
    )
    recall_all = metrics.**recall_score**(
        y_true, y_pred, zero_division=0
    )
    p_by_group = []
    r_by_group = []
    for group_val in group_vals:
        in_group = X_group==group_val
        p_by_group.append(metrics.**precision_score**(
            y_true[in_group], y_pred[in_group], zero_division=0
            )
        )
        r_by_group.append(metrics.**recall_score**(
            y_true[in_group], y_pred[in_group], zero_division=0
            )
        )
    precision_all = precision_all - \
                   (np.array(p_by_group).std()*penalty_mult)
    recall_all = recall_all -\
                (np.array(r_by_group).std()*penalty_mult)
    return ((precision_all*precision_mult)+
            (recall_all*recall_mult))/\
            (precision_mult+recall_mult) 

现在,为了使这个函数发挥作用,我们需要运行调整。

运行贝叶斯超参数调整

贝叶斯优化是一种 全局优化方法,它使用黑盒目标函数的后验分布及其连续参数。换句话说,它根据过去的结果顺序搜索下一个要测试的最佳参数。与网格搜索不同,它不会在网格上尝试固定参数组合,而是利用它已经知道的信息并探索未知领域。

bayesian-optimization 库是模型无关的。它所需的所有东西是一个函数以及它们的界限参数。它将在这些界限内探索这些参数的值。该函数接受这些参数并返回一个数字。这个数字,或目标,是贝叶斯优化算法将最大化的。

以下代码是用于 objective 函数的,它使用四个分割和三个重复初始化一个 RepeatedStratifiedKFold 交叉验证。然后,它遍历分割并使用它们拟合 CatBoostClassifier。最后,它计算每个模型训练的 weighted_penalized_pr_average 自定义指标并将其追加到一个列表中。最后,该函数返回所有 12 个训练样本的自定义指标的中位数。代码在以下片段中显示:

def **hyp_catboost**(l2_leaf_reg, scale_pos_weight):
    cv = model_selection.**RepeatedStratifiedKFold**(
        n_splits=4,n_repeats=3, random_state=rand
    )
    metric_l = []
    for train_index, val_index in cv.split(X_train, y_train):
        X_train_cv, X_val_cv = X_train.iloc[train_index],\
                               X_train.iloc[val_index]
        y_train_cv, y_val_cv = y_train.iloc[train_index],
                               y_train.iloc[val_index]
        mdl = cb.**CatBoostClassifier**(
            random_seed=rand, learning_rate=0.5, verbose=0, depth=5,\
            l2_leaf_reg=l2_leaf_reg, scale_pos_weight=scale_pos_weight
        )
        mdl = mdl.**fit**(X_train_cv, y_train_cv)
        y_val_pred = mdl.**predict**(X_val_cv)
        metric = **weighted_penalized_pr_average**(
            y_val_cv,y_val_pred, X_val_cv['race'], range(3)
        )
        metric_l.**append**(metric)
    return np.**median**(np.array(metric_l)) 

现在函数已经定义,运行贝叶斯优化过程很简单。首先,设置参数界限字典(pbounds),使用 hyp_catboost 函数初始化 BayesianOptimization,然后使用 maximize 运行它。maximize 函数接受 init_points,它设置初始使用随机探索运行的迭代次数。然后,n_iter 是它应该执行的优化迭代次数以找到最大值。我们将 init_pointsn_iter 分别设置为 37,因为可能需要很长时间,但这些数字越大越好。代码可以在以下片段中看到:

pbounds = {
    'l2_leaf_reg': (2,4),
    'scale_pos_weight': (0.55,0.85)
    }
optimizer = **BayesianOptimization**(
    **hyp_catboost**,
    pbounds, 
    random_state=rand
)
optimizer.maximize(init_points=3, n_iter=7) 

一旦完成,你可以访问最佳参数,如下所示:

print(optimizer.max['params']) 

它将返回一个包含参数的字典,如下所示:

{'l2_leaf_reg': 2.0207483077713997, 'scale_pos_weight': 0.7005623776446217} 

现在,让我们使用这些参数拟合一个模型并评估它。

使用最佳参数拟合和评估模型

使用这些参数初始化 CatBoostClassifier 与将 best_params 字典作为参数传递一样简单。然后,你所需要做的就是 fit 模型并评估它(evaluate_class_mdl)。代码在以下片段中显示:

cb_opt = cb.**CatBoostClassifier**(
    random_seed=rand,
    depth=5,
    learning_rate=0.5,
    verbose=0,
    **optimizer.max['params']
)
cb_opt = cb_opt.**fit**(X_train, y_train)
fitted_class_mdls['catboost_opt'] = mldatasets.**evaluate_class_mdl**(
    cb_opt,
    X_train,
    X_test,
    y_train,
    y_test,
    plot_roc=False,
    plot_conf_matrix=True,
    **ret_eval_dict=****True**
) 

前面的代码片段输出了以下预测性能指标:

Accuracy_train:  0.9652		Accuracy_test:   0.8192
Precision_test:  0.8330		Recall_test:     0.8058
ROC-AUC_test:    0.8791		F1_test:         0.8192 

这些是我们迄今为止达到的最高 Accuracy_testPrecision_testRecall_test 指标。现在让我们看看模型使用 compare_confusion_matrices 进行公平性测试的表现。请看以下代码片段:

y_test_pred = fitted_class_mdls['catboost_opt']['preds_test']
_ = mldatasets.**compare_confusion_matrices**(
    y_test[X_test.race==1],
    y_test_pred[X_test.race==1],
    y_test[X_test.race==0],
    y_test_pred[X_test.race==0],
    'Caucasian',
    'African-American',
    **compare_fpr=****True**
) 

前面的代码输出了 图 12.16,它显示了迄今为止我们获得的一些最佳公平性指标,如你所见:

图表 描述自动生成

图 12.16:优化后的 CatBoost 模型不同种族之间的混淆矩阵比较

这些结果很好,但我们不能完全确信模型没有种族偏见,因为特征仍然存在。衡量其影响的一种方法是通过特征重要性方法。

通过特征重要性来检查种族偏见

尽管 CatBoost 在大多数指标上,包括准确率、精确率和 F1 分数,都是我们表现最好的模型,但我们正在使用 XGBoost 前进,因为 CatBoost 不支持交互约束,我们将在下一节中实现。但首先,我们将比较它们在发现哪些特征重要方面的差异。此外,SHapley Additive exPlanationsSHAP)值提供了一种稳健的方法来衡量和可视化特征重要性,因此让我们为我们的优化 CatBoost 和正则化 XGBoost 模型计算它们。为此,我们需要用每个模型初始化TreeExplainer,然后使用shap_values为每个模型生成值,如下面的代码片段所示:

fitted_cb_mdl = fitted_class_mdls['catboost_opt']['fitted']
shap_cb_explainer = shap.**TreeExplainer**(fitted_cb_mdl)
shap_cb_values = shap_cb_explainer.**shap_values**(X_test)
fitted_xgb_mdl = fitted_class_mdls['xgb_reg']['fitted']
shap_xgb_explainer = shap.**TreeExplainer**(fitted_xgb_mdl)
shap_xgb_values = shap_xgb_explainer.**shap_values**(X_test) 

接下来,我们可以使用 Matplotlib 的subplot功能并排生成两个summary_plot图,如下所示:

ax0 = plt.subplot(1, 2, 1)
shap.**summary_plot**(
    **shap_xgb_values**,
    X_test,
    plot_type="dot",
    plot_size=None,
    show=False
)
ax0.set_title("XGBoost SHAP Summary")
ax1 = plt.subplot(1, 2, 2)
shap.**summary_plot**(
    **shap_cb_values**,
    X_test,
    plot_type="dot",
    plot_size=None,
    show=False
)
ax1.set_title("Catboost SHAP Summary") 
Figure 12.17, which shows how similar CatBoost and XGBoost are. This similarity shouldn’t be surprising because, after all, they are both gradient-boosted decision trees. The bad news is that race is in the top four for both. However, the prevalence of the shade that corresponds to lower feature values on the right suggests that African American (race=0) negatively correlates with recidivism.

输出结果如下:

图 12.17:XGBoost 正则化和 CatBoost 优化模型的 SHAP 总结图

在任何情况下,从训练数据中移除race是有意义的,但我们必须首先确定模型为什么认为这是一个关键特征。请看以下代码片段:

shap_xgb_interact_values =\
                shap_xgb_explainer.shap_interaction_values(X_test) 

第四章全局模型无关解释方法中,我们讨论了评估交互效应。现在是时候回顾这个话题了,但这次,我们将提取 SHAP 的交互值(shap_interaction_values)而不是使用 SHAP 的依赖图。我们可以很容易地使用summary_plot图对 SHAP 交互进行排序。SHAP 总结图非常有信息量,但它并不像交互热图那样直观。为了生成带有标签的热图,我们必须将shap_xgb_interact_values的总和放在 DataFrame 的第一个轴上,然后使用特征的名称命名列和行(index)。其余的只是使用 Seaborn 的heatmap函数将 DataFrame 绘制为热图。代码可以在下面的代码片段中看到:

shap_xgb_interact_avgs = np.abs(
    **shap_xgb_interact_values**
).mean(0)
np.fill_diagonal(shap_xgb_interact_avgs, 0)
shap_xgb_interact_df = pd.**DataFrame**(shap_xgb_interact_avgs)
shap_xgb_interact_df.columns = X_test.columns
shap_xgb_interact_df.index = X_test.columns
sns.**heatmap**(shap_xgb_interact_df, cmap='Blues', annot=True,\
            annot_kws={'size':13}, fmt='.2f', linewidths=.5) 

上述代码生成了图 12.18所示的热图。它展示了racelength_of_stayage_grouppriors per year之间的相互作用最为强烈。当然,一旦我们移除race,这些相互作用就会消失。然而,鉴于这一发现,如果这些特征中内置了种族偏见,我们应该仔细考虑。研究支持了age_grouppriors_per_year的必要性,这使length_of_stay成为审查的候选者。我们不会在本章中这样做,但这确实值得思考:

图形用户界面,应用程序描述自动生成

图 12.18:正则化 XGBoost 模型的 SHAP 交互值热图

图 12.18中得到的另一个有趣的见解是特征如何被聚类。你可以在c_charge_degreepriors_per_year之间的右下象限画一个框,因为一旦我们移除race,大部分的交互都将位于这里。限制令人烦恼的交互有很多好处。例如,为什么所有青少年犯罪特征,如juv_fel_count,都应该与age_group交互?为什么sex应该与length_of_stay交互?接下来,我们将学习如何围绕右下象限设置一个围栏,通过交互约束限制这些特征之间的交互。我们还将确保priors_per_year单调约束

实现模型约束

我们将首先讨论如何使用 XGBoost 以及所有流行的树集成实现约束,因为它们的参数名称相同(见图 12.12)。然后,我们将使用 TensorFlow Lattice 进行操作。但在我们继续之前,让我们按照以下方式从数据中移除race

X_train_con = X_train.**drop**(['race'], axis=1).copy()
X_test_con = X_test.**drop**(['race'], axis=1).copy() 

现在,随着race的消失,模型可能仍然存在一些偏见。然而,我们进行的特征工程和将要施加的约束可以帮助模型与这些偏见对齐,考虑到我们在第六章中发现的锚点和反事实解释的双重标准。话虽如此,生成的模型可能在对测试数据的性能上会较差。这里有两大原因,如下所述:

  • 信息丢失:种族,尤其是与其他特征的交互,影响了结果,因此不幸地携带了一些信息。

  • 现实与政策驱动理想的错位:当实施这些约束的主要原因是确保模型不仅符合领域知识,而且符合理想,而这些理想可能不在数据中明显体现时,这种情况就会发生。我们必须记住,一整套制度化的种族主义可能已经玷污了真实情况。模型反映了数据,但数据反映了地面的现实,而现实本身是有偏见的。

考虑到这一点,让我们开始实施约束!

XGBoost 的约束

在本节中,我们将采取三个简单的步骤。首先,我们将定义我们的训练参数,然后训练和评估一个约束模型,最后检查约束的效果。

设置正则化和约束参数

我们使用 print(fitted_class_mdls['xgb_reg']['cv_best_params']) 来获取我们正则化 XGBoost 模型的最佳参数。它们位于 best_xgb_params 字典中,包括 etamax_depth。然后,为了对 priors_per_year 应用单调约束,我们首先需要知道其位置和单调相关性的方向。从 图 12.8 中,我们知道这两个问题的答案。它是最后一个特征,相关性是正的,所以 mono_con 元组应该有九个项目,最后一个是一个 1,其余的是 0s。至于交互约束,我们只允许最后五个特征相互交互,前四个也是如此。interact_con 元组是一个列表的列表,反映了这些约束。代码可以在下面的片段中看到:

**best_xgb_params** = {'eta': 0.3, 'max_depth': 28,\
                   'reg_alpha': 0.2071, 'reg_lambda': 0.6534,\
                   'scale_pos_weight': 0.9114}
**mono_con** = (0,0,0,0,0,0,0,0,1)
**interact_con** = [[4, 5, 6, 7, 8],[0, 1, 2, 3]] 

接下来,我们将使用这些约束条件训练和评估 XGBoost 模型。

训练和评估约束模型

现在,我们将使用这些约束条件训练和评估我们的约束模型。首先,我们使用我们的约束和正则化参数初始化 XGBClassifier 模型,然后使用缺少 race 特征的训练数据 (X_train_con) 来拟合它。然后,我们使用 evaluate_class_mdl 评估预测性能,并与 compare_confusion_matrices 比较公平性,就像我们之前所做的那样。代码可以在下面的片段中看到:

xgb_con = xgb.XGBClassifier(
    seed=rand,monotone_constraints=**mono_con**,\
    interaction_constraints=**interact_con**, ****best_xgb_params**
)
xgb_con = xgb_con.**fit**(X_train_con, y_train)
fitted_class_mdls['xgb_con'] = mldatasets.**evaluate_class_mdl**(
    xgb_con, X_train_con, X_test_con, y_train, y_test,\
    plot_roc=False, ret_eval_dict=True
)
y_test_pred = fitted_class_mdls['xgb_con']['preds_test']
_ = mldatasets.**compare_confusion_matrices**(
    y_test[X_test.race==1],
    y_test_pred[X_test.race==1],
    y_test[X_test.race==0],
    y_test_pred[X_test.race==0],
    'Caucasian',
    'African-American',
     **compare_fpr=****True**
) 
Figure 12.19 and some predictive performance metrics. If we compare the matrices to those in *Figure 12.16*, racial disparities, as measured by our FPR ratio, took a hit. Also, predictive performance is lower than the optimized CatBoost model across the board, by 24%. We could likely increase these metrics a bit by performing the same *Bayesian hyperparameter tuning* on this model.

可以在这里看到混淆矩阵的输出:

图表描述自动生成

图 12.19:约束 XGBoost 模型不同种族之间的混淆矩阵比较

有一个需要考虑的事情是,尽管种族不平等是本章的主要关注点,但我们还希望确保模型在其他方面也是最优的。正如之前所述,这是一个权衡。例如,被告的 priors_per_year 越多,风险越高,这是很自然的,我们通过单调约束确保了这一点。让我们验证这些结果!

检查约束

观察约束条件在作用中的简单方法是将 SHAP summary_plot 绘制出来,就像我们在 图 12.17 中所做的那样,但这次我们只绘制一个。请看下面的 ode 程序片段:

fitted_xgb_con_mdl = fitted_class_mdls['xgb_con']['fitted']
shap_xgb_con_explainer = shap.**TreeExplainer**(fitted_xgb_con_mdl)
shap_xgb_con_values = shap_xgb_con_explainer.**shap_values**(
    X_test_con
)
shap.**summary_plot**(
    shap_xgb_con_values, X_test_con, plot_type="dot"
) 

上述代码生成了 图 12.20。这展示了从左到右的 priors_per_year 是一个更干净的梯度,这意味着较低的值持续产生负面影响,而较高的值产生正面影响——正如它们应该的那样!

你可以在这里看到输出:

图表描述自动生成

图 12.20:约束 XGBoost 模型的 SHAP 概述图

接下来,让我们通过 图 12.7 中的数据视角检查我们看到的 age_grouppriors_per_year 的交互。我们也可以通过添加额外的参数来为模型使用 plot_prob_contour_map,如下所示:

  • 拟合的模型 (fitted_xgb_con_mdl)

  • 用于模型推理的 DataFrame (X_test_con)

  • 在每个轴上比较的 DataFrame 中两列的名称(x_coly_col

结果是一个交互部分依赖图,类似于第四章中展示的,全局模型无关解释方法,只不过它使用数据集(recidivism_df)为每个轴创建直方图。我们现在将创建两个这样的图进行比较——一个用于正则化的 XGBoost 模型,另一个用于约束模型。此代码的示例如下:

mldatasets.**plot_prob_contour_map**(
    recidivism_df.**age_group**, recidivism_df.**priors_per_year**,
    recidivism_df.**is_recid**, x_intervals=ordenc.categories_[2],
    y_intervals=6, use_quantiles=True, xlabel='Age Group',
    ylabel='Priors Per Year', model=**fitted_xgb_mdl**,
    X_df=**X_test**,x_col='age_group',y_col='priors_per_year',
    title='Probability of Recidivism by Age/Priors per Year \
          (according to XGBoost Regularized Model)'
)
mldatasets.**plot_prob_contour_map**(
    recidivism_df.**age_group**, recidivism_df.**priors_per_year**,
    recidivism_df.is_recid, x_intervals=ordenc.categories_[2],
    y_intervals=6, use_quantiles=True, xlabel='Age Group',
    ylabel='Priors Per Year', model=**fitted_xgb_con_mdl**,
    X_df=**X_test_con**,x_col='age_group',y_col='priors_per_year',
    title='(according to XGBoost Constrained Model)'
) 

上述代码生成了图 12.21中显示的图表。它表明正则化的 XGBoost 模型反映了数据(参见图 12.7)。另一方面,约束的 XGBoost 模型平滑并简化了等高线,如下所示:

图表,自动生成描述

图 12.21:根据 XGBoost 正则化和约束模型,针对 age_group 和 priors_per_year 的再犯概率等高线图

接下来,我们可以从图 12.18生成 SHAP 交互值热图,但针对的是约束模型。代码相同,但使用shap_xgb_con_explainer SHAP 解释器和X_test_con数据。代码的示例如下:

shap_xgb_interact_values =\
        shap_xgb_con_explainer.**shap_interaction_values**(X_test_con)
shap_xgb_interact_df =\
        pd.**DataFrame**(np.sum(**shap_xgb_interact_values**, axis=0))
shap_xgb_interact_df.columns = X_test_con.columns
shap_xgb_interact_df.index = X_test_con.columns
sns.**heatmap**(
    shap_xgb_interact_df, cmap='RdBu', annot=True,
    annot_kws={'size':13}, fmt='.0f', linewidths=.5
) 
Figure 12.22. It shows how the interaction constraints were effective because of zeros in the lower-left and lower-right quadrants, which correspond to interactions between the two groups of features we separated. If we compare with *Figure 12.18*, we can also tell how the constraints shifted the most salient interactions, making age_group and length_of_stay by far the most important ones.

输出结果如下:

包含应用的图片,自动生成描述

图 12.22:约束 XGBoost 模型的 SHAP 交互值热图

现在,让我们看看 TensorFlow 是如何通过 TensorFlow Lattice 实现单调性和其他“形状约束”的。

TensorFlow Lattice 的约束条件

神经网络在寻找loss函数的最优解方面可以非常高效。损失与我们要预测的后果相关联。在这种情况下,那将是 2 年的再犯率。在伦理学中,功利主义(或后果主义)的公平观只要模型的训练数据没有偏见,就没有问题。然而,义务论的观点是,伦理原则或政策驱动着伦理问题,并超越后果。受此启发,TensorFlow LatticeTFL)可以在模型中将伦理原则体现为模型形状约束。

晶格是一种插值查找表,它通过插值近似输入到输出的网格。在高维空间中,这些网格成为超立方体。每个输入到输出的映射通过校准层进行约束,并且支持许多类型的约束——不仅仅是单调性。图 12.23展示了这一点:

图表,自动生成描述

图 12.23:TensorFlow Lattice 支持的约束条件

图 12.23展示了几个形状约束。前三个应用于单个特征(x),约束了线,代表输出。最后两个应用于一对特征(x[1]和x[2]),约束了彩色等高线图()。以下是对每个约束的简要说明:

  • 单调性:这使得函数()相对于输入(x)总是增加(1)或减少(-1)。

  • 凸性:这迫使函数()相对于输入(x)是凸的(1)或凹的(-1)。凸性可以与单调性结合,产生图 12.23中的效果。

  • 单峰性:这类似于单调性,不同之处在于它向两个方向延伸,允许函数()有一个单一的谷底(1)或峰值(-1)。

  • 信任:这迫使一个单调特征(x[1])依赖于另一个特征(x[2])。图 12.23中的例子是爱德华兹信任,但也有一个具有不同形状约束的梯形信任变体。

  • 支配性:单调支配性约束一个单调特征(x[1])定义斜率或效果的方向,当与另一个特征(x[2])比较时。另一种选择,范围支配性,类似,但两个特征都是单调的。

神经网络特别容易过拟合,控制它的杠杆相对更难。例如,确切地说,隐藏节点、dropout、权重正则化和 epoch 的哪种组合会导致可接受的过拟合水平是难以确定的。另一方面,在基于树的模型中移动单个参数,即树深度,朝一个方向移动,可能会将过拟合降低到可接受的水平,尽管可能需要许多不同的参数才能使其达到最佳状态。

强制形状约束不仅增加了可解释性,还因为简化了函数而正则化了模型。TFL 还支持基于惩罚的正则化,针对每个特征或校准层的核,利用拉普拉斯海森扭转皱纹正则化器通过 L1 和 L2 惩罚。这些正则化器的作用是使函数更加平坦、线性或平滑。我们不会详细解释,但可以说,存在正则化来覆盖许多用例。

实现框架的方法也有几种——太多,这里无法一一详述!然而,重要的是指出,这个例子只是实现它的几种方法之一。TFL 内置了预定义的估计器,它们抽象了一些配置。您还可以使用 TFL 层创建一个自定义估计器。对于 Keras,您可以使用预制的模型,或者使用 TensorFlow Lattice 层构建一个 Keras 模型。接下来,我们将进行最后一项操作!

初始化模型和 Lattice 输入

现在我们将创建一系列输入层,每个输入层包含一个特征。这些层连接到校准层,使每个输入适合符合个体约束和正则化的分段线性PWL)函数,除了sex,它将使用分类校准。所有校准层都输入到一个多维晶格层,通过一个具有sigmoid激活的密集层产生输出。这个描述可能有点难以理解,所以您可以自由地跳到图 12.24以获得一些视觉辅助。

顺便说一下,有许多种类的层可供连接,以产生深度晶格网络DLN),包括以下内容:

  • 线性用于多个输入之间的线性函数,包括具有支配形状约束的函数。

  • 聚合用于对多个输入执行聚合函数。

  • 并行组合将多个校准层放置在单个函数中,使其与 Keras Sequential层兼容。

在这个例子中,我们不会使用这些层,但也许了解这些会激发您进一步探索 TensorFlow Lattice 库。无论如何,回到这个例子!

首先要定义的是lattice_sizes,它是一个元组,对应于每个维度的顶点数。在所选架构中,每个特征都有一个维度,因此我们需要选择九个大于或等于 2 的数字。对于分类特征的基数较小的特征或连续特征的拐点,需要较少的顶点。然而,我们也可能想通过故意选择更少的顶点来限制特征的表达能力。例如,juv_fel_count有 10 个唯一值,但我们将只给它分配两个顶点。lattice_sizes如下所示:

lattice_sizes = [2, 2, 2, 2, 4, 5, 7, 7, 7] 

接下来,我们初始化两个列表,一个用于放置所有输入层(model_inputs)和另一个用于校准层(lattice_inputs)。然后,对于每个特征,我们逐一定义一个输入层使用tf.keras.layers.Input和一个校准层使用分类校准(tfl.layers.CategoricalCalibration)或 PWL 校准(tfl.layers.PWLCalibration)。每个特征的所有输入和校准层都将分别添加到各自的列表中。校准层内部发生的事情取决于特征。所有 PWL 校准都使用input_keypoints,它询问 PWL 函数应该在何处分段。有时,使用固定宽度(np.linspace)回答这个问题是最好的,而有时使用固定频率(np.quantile)。分类校准则使用桶(num_buckets),它对应于类别的数量。所有校准器都有以下参数:

  • output_min:校准器的最小输出

  • output_max:校准器的最大输出——始终必须与输出最小值 + 晶格大小 - 1 相匹配

  • monotonicity:是否应该单调约束 PWL 函数,如果是,如何约束

  • kernel_regularizer:如何正则化函数

除了这些参数之外,convexityis_cyclic(对于单调单峰)可以修改约束形状。看看下面的代码片段:

model_inputs = []
lattice_inputs = []
sex_input = **tf.keras.layers.Input**(shape=[1], name='sex')
lattice_inputs.append(**tfl.layers.CategoricalCalibration**(
    name='sex_calib',
    num_buckets=2,
    output_min=0.0,
    output_max=lattice_sizes[0] - 1.0,
    kernel_regularizer=tf.keras.regularizers.l1_l2(l1=0.001),
    kernel_initializer='constant')(sex_input)
)
model_inputs.append(sex_input)
juvf_input = **tf.keras.layers.Input**(shape=[1],\
                                   name='juv_fel_count')
lattice_inputs.append(**tfl.layers.PWLCalibration**(
    name='juvf_calib',
    **monotonicity**='none',
    input_keypoints=np.linspace(0, 20, num=5, dtype=np.float32),
    output_min=0.0,
    output_max=lattice_sizes[1] - 1.0,\
    kernel_regularizer=tf.keras.regularizers.l1_l2(l1=0.001),
    kernel_initializer='equal_slopes')(juvf_input)
)
model_inputs.append(juvf_input)
age_input = **tf.keras.layers.Input**(shape=[1], name='age_group')
lattice_inputs.append(**tfl.layers.PWLCalibration**(
    name='age_calib',
    **monotonicity**='none',
    input_keypoints=np.linspace(0, 6, num=7, dtype=np.float32),
    output_min=0.0,
    output_max=lattice_sizes[7] - 1.0,
    kernel_regularizer=('hessian', 0.0, 1e-4))(age_input)
)
model_inputs.append(age_input)
priors_input = **tf.keras.layers.Input**(shape=[1],\
                                     name='priors_per_year')
lattice_inputs.append(**tfl.layers.PWLCalibration**(
    name='priors_calib',
    **monotonicity**='increasing',
    input_keypoints=np.quantile(X_train_con['priors_per_year'],
                                np.linspace(0, 1, num=7)),
    output_min=0.0,
    output_max=lattice_sizes[8]-1.0)(priors_input))
model_inputs.append(priors_input) 

因此,我们现在有一个包含 model_inputs 的列表和另一个包含校准层的列表,这些校准层将成为 lattice 的输入(lattice_inputs)。我们现在需要做的就是将这些连接到一个 lattice 上。

使用 TensorFlow Lattice 层构建 Keras 模型

我们已经将这个模型的前两个构建块连接起来。现在,让我们创建最后两个构建块,从 lattice (tfl.layers.Lattice) 开始。作为参数,它接受 lattice_sizes、输出最小值和最大值以及它应该执行的 monotonicities。注意,最后一个参数 priors_per_year 的单调性设置为 increasing。然后,lattice 层将输入到最终的部件,即具有 sigmoid 激活的 Dense 层。代码如下所示:

lattice = **tfl.layers.Lattice**(
    name='lattice',
    lattice_sizes=**lattice_sizes**,
    **monotonicities**=[
        'none', 'none', 'none', 'none', 'none',
        'none', 'none', 'none', **'increasing'**
    ],
    output_min=0.0, output_max=1.0)(**lattice_inputs**)
model_output = tf.keras.layers.**Dense**(1, name='output',
                                     activation='sigmoid')(lattice) 

前两个构建块作为 inputs 现在可以与最后两个作为 outputs 通过 tf.keras.models.Model 连接起来。哇!我们现在有一个完整的模型,代码如下所示:

tfl_mdl = **tf.keras.models.Model**(inputs=model_inputs,
                                outputs=model_output) 

你总是可以运行 tfl_mdl.summary() 来了解所有层是如何连接的,但使用 tf.keras.utils.plot_model 更直观,如下面的代码片段所示:

tf.keras.utils.plot_model(tfl_mdl, rankdir='LR') 

上述代码生成了 图 12.24 中显示的模型图:

图  描述自动生成

图 12.24:带有 TFL 层的 Keras 模型图

接下来,我们需要编译模型。我们将使用 binary_crossentropy 损失函数和 Adam 优化器,并使用准确率和 曲线下面积AUC)作为指标,如下面的代码片段所示:

tfl_mdl.**compile**(
    loss='binary_crossentropy',
    optimizer=tf.keras.optimizers.Adam(lr=0.001),
    metrics=['accuracy',tf.keras.metrics.AUC(name='auc')]
) 

我们现在几乎准备就绪了!接下来是最后一步。

训练和评估模型

如果你仔细观察 图 12.24,你会注意到模型没有一层输入,而是有九层,这意味着我们必须将我们的训练和测试数据分成九部分。我们可以使用 np.split 来做这件事,这将产生九个 NumPy 数组的列表。至于标签,TFL 不接受单维数组。使用 expand_dims,我们将它们的形状从 (N,) 转换为 (N,1),如下面的代码片段所示:

X_train_expand = np.**split**(
    X_train_con.values.astype(np.float32),
    indices_or_sections=9,
    axis=1
)
y_train_expand = np.**expand_dims**(
    y_train.values.astype(np.float32),
    axis=1
)
X_test_expand = np.**split**(
    X_test_con.values.astype(np.float32),
    indices_or_sections=9,
    axis=1)
y_test_expand = np.**expand_dims**(
    y_test.values.astype(np.float32),
    axis=1
) 

接下来是训练!为了防止过拟合,我们可以通过监控验证 AUC (val_auc) 来使用 EarlyStopping。为了解决类别不平衡问题,在 fit 函数中,我们使用 class_weight,如下面的代码片段所示:

es = tf.keras.callbacks.**EarlyStopping**(
    monitor='**val_auc**',
    mode='max',
    patience=40,
    restore_best_weights=True
)
tfl_history = tfl_mdl.**fit**(
    X_train_expand,
    y_train_expand,
    **class_weight**={0:18, 1:16},
    batch_size=128,
    epochs=300,
    validation_split=0.2,
    shuffle=True,
    callbacks=[**es**]
) 

一旦模型训练完成,我们可以使用 evaluate_class_mdl 来输出预测性能的快速摘要,就像我们之前做的那样,然后使用 compare_confusion_matrices 来检查公平性,就像我们之前所做的那样。代码如下所示:

fitted_class_mdls['tfl_con'] = mldatasets.**evaluate_class_mdl**(
    tfl_mdl,
    X_train_expand,
    X_test_expand,
    y_train.values.astype(np.float32),
    y_test.values.astype(np.float32),
    plot_roc=False,
    ret_eval_dict=True
)
y_test_pred = fitted_class_mdls['tfl_con']['preds_test']
_ = mldatasets.**compare_confusion_matrices**(
    y_test[X_test.race==1],
    y_test_pred[X_test.race==1],
    y_test[X_test.race==0],
    y_test_pred[X_test.race==0],
    'Caucasian',
    'African-American',
    compare_fpr=True
) 
Figure 12.25. The TensorFlow Lattice model performs much better than the regularized Keras model, yet the FPR ratio is better than the constrained XGBoost model. It must be noted that XGBoost’s parameters were previously tuned. With TensorFlow Lattice, a lot could be done to improve FPR, including using a custom loss function or better early-stopping metrics that somehow account for racial disparities.

输出如下所示:

图表,树状图  描述自动生成

图 12.25:约束 TensorFlow Lattice 模型在种族之间的混淆矩阵比较

接下来,我们将根据本章学到的内容得出一些结论,并确定我们是否完成了任务。

任务完成

通常,数据会因为表现不佳、不可解释或存在偏见而被责备,这可能是真的,但在准备和模型开发阶段可以采取许多不同的措施来改进它。为了提供一个类比,这就像烘焙蛋糕。你需要高质量的原料,是的。但似乎微小的原料准备和烘焙本身——如烘焙温度、使用的容器和时间——的差异可以产生巨大的影响。天哪!甚至是你无法控制的事情,如大气压力或湿度,也会影响烘焙!甚至在完成之后,你有多少种不同的方式可以评估蛋糕的质量?

本章讨论了这些许多细节,就像烘焙一样,它们既是精确科学的一部分,也是艺术形式的一部分。本章讨论的概念也具有深远的影响,特别是在如何优化没有单一目标且具有深远社会影响的问题方面。一种可能的方法是结合指标并考虑不平衡。为此,我们创建了一个指标:一个加权平均的精确率召回率,它惩罚种族不平等,并且我们可以为所有模型高效地计算它并将其放入模型字典(fitted_class_mdls)。然后,就像我们之前做的那样,我们将其放入 DataFrame 并输出,但这次是按照自定义指标(wppra_test)排序。代码可以在下面的代码片段中看到:

for mdl_name in fitted_class_mdls:
    fitted_class_mdls[mdl_name]['wppra_test'] =\
    **weighted_penalized_pr_average**(
        y_test,
        fitted_class_mdls[mdl_name]['preds_test'],
        X_test['race'],
        range(3)
    )
class_metrics = pd.**DataFrame.from_dict**(fitted_class_mdls, 'index')[
    ['precision_test', 'recall_test', 'wppra_test']
]
with pd.option_context('display.precision', 3):
    html = class_metrics.**sort_values**(
        by='**wppra_test**',
        ascending=False
        ).style.background_gradient(
           cmap='plasma',subset=['precision_test']
        ).background_gradient(
           cmap='viridis', subset=['recall_test'])
html 

上一段代码生成了图 12.26中显示的 DataFrame:

表格描述自动生成

图 12.26:按加权惩罚精确率-召回率平均值自定义指标排序的本章顶级模型

图 12.26中,很容易提出最上面的其中一个模型。然而,它们是用race作为特征进行训练的,并且没有考虑到证明的刑事司法现实。然而,性能最高的约束模型——XGBoost 模型(xgb_con)——没有使用race,确保了priors_per_year是单调的,并且不允许age_group与青少年犯罪特征相互作用,而且与原始模型相比,它在显著提高预测性能的同时做到了这一切。它也更公平,因为它将特权群体和弱势群体之间的 FPR 比率从 1.84x(第六章中的图 6.2)降低到 1.39x(图 12.19)。它并不完美,但这是一个巨大的改进!

任务是证明准确性和领域知识可以与公平性的进步共存,我们已经成功地完成了它。话虽如此,仍有改进的空间。因此,行动计划将不得不向您的客户展示受约束的 XGBoost 模型,并继续改进和构建更多受约束的模型。未受约束的模型应仅作为基准。

如果你将本章的方法与第十一章中学习的那些方法(偏差缓解和因果推断方法)相结合,你可以实现显著的公平性改进。我们没有将这些方法纳入本章,以专注于通常不被视为偏差缓解工具包一部分的模型(或内处理)方法,但它们在很大程度上可以协助达到这一目的,更不用说那些旨在使模型更可靠的模型调优方法了。

摘要

阅读本章后,你现在应该了解如何利用数据工程来增强可解释性,正则化来减少过拟合,以及约束来符合政策。主要的目标是设置护栏和遏制阻碍可解释性的复杂性。

在下一章中,我们将探讨通过对抗鲁棒性来增强模型可靠性的方法。

数据集来源

进一步阅读

  • Hastie, T. J., Tibshirani, R. J. 和 Friedman, J. H. (2001). 统计学习的要素. Springer-Verlag, 纽约,美国

  • Wang, S. & Gupta, M. (2020). 通过单调性形状约束的德性伦理. AISTATS. arxiv.org/abs/2001.11990

  • Cotter, A., Gupta, M., Jiang, H., Ilan, E. L., Muller, J., Narayan, T., Wang, S. 和 Zhu, T. (2019). 集合函数的形状约束. ICML. proceedings.mlr.press/v97/cotter19a.html

  • Gupta, M. R., Cotter A., Pfeifer, J., Voevodski, K., Canini, K., Mangylov, A., Moczydlowski, W. 和 van Esbroeck, A. (2016). 单调校准插值查找表. 机器学习研究杂志 17(109):1−47. arxiv.org/abs/1505.06378

  • Noble, S. (2018). 压迫算法:谷歌时代的数据歧视. NYU Press

在 Discord 上了解更多

要加入本书的 Discord 社区——在那里您可以分享反馈,向作者提问,并了解新发布的内容——请扫描下面的二维码:

packt.link/inml

第十三章:对抗性鲁棒性

机器学习解释有许多关注点,从知识发现到具有实际伦理影响的高风险问题,如上一两章中探讨的公平性问题。在本章中,我们将关注涉及可靠性、安全性和安全性的问题。

正如我们在第七章可视化卷积神经网络中使用的对比解释方法所意识到的那样,我们可以轻易地欺骗图像分类器做出令人尴尬的错误预测。这种能力可能具有严重的后果。例如,一个肇事者可以在让路标志上贴上一个黑色贴纸,尽管大多数司机仍然会将其识别为让路标志,但自动驾驶汽车可能就不再能识别它,从而导致撞车。银行劫匪可能穿着一种冷却服装来欺骗银行保险库的热成像系统,尽管任何人都可能注意到它,但成像系统却无法做到。

风险不仅限于复杂的图像分类器。其他模型也可能被欺骗!在第六章,锚点和反事实解释中产生的反事实示例,就像对抗性示例一样,但目的是欺骗。攻击者可以利用任何误分类示例,在决策边界上进行对抗性操作。例如,垃圾邮件发送者可能会意识到调整一些电子邮件属性可以增加绕过垃圾邮件过滤器的可能性。

复杂模型更容易受到对抗性攻击。那么我们为什么还要信任它们呢?!我们当然可以使它们更加可靠,这就是对抗性鲁棒性的含义。对手可以通过多种方式故意破坏模型,但我们将重点关注逃避攻击,并简要解释其他形式的攻击。然后我们将解释两种防御方法:空间平滑预处理和对抗性训练。最后,我们将展示一种鲁棒性评估方法。

这些是我们将要讨论的主要主题:

  • 了解逃避攻击

  • 使用预处理防御针对攻击

  • 通过对鲁棒分类器的对抗性训练来防御任何逃避攻击

技术要求

本章的示例使用了mldatasetsnumpysklearntensorflowkerasadversarial-robustness-toolboxmatplotlibseaborn库。如何安装所有这些库的说明在前言中。

本章的代码位于此处:packt.link/1MNrL

任务

全球私人签约保安服务行业市场规模超过 2500 亿美元,年增长率为约 5%。然而,它面临着许多挑战,例如在许多司法管辖区缺乏足够培训的保安和专业的安全专家,以及一系列意外的安全威胁。这些威胁包括广泛的协调一致的网络安全攻击、大规模暴乱、社会动荡,以及最后但同样重要的是,由大流行带来的健康风险。确实,2020 年通过勒索软件、虚假信息攻击、抗议活动和 COVID-19 等一系列事件考验了该行业。

在此之后,美国最大的医院网络之一要求他们的签约保安公司监控医院内访客和员工佩戴口罩的正确性。保安公司因为这项请求而感到困扰,因为它分散了保安人员应对其他威胁(如入侵者、斗殴患者和挑衅访客)的精力。该公司在每个走廊、手术室、候诊室和医院入口都有视频监控。每次都不可能监控到每个摄像头的画面,因此保安公司认为他们可以用深度学习模型来协助保安:

这些模型已经能够检测到异常活动,例如在走廊里奔跑和在物业任何地方挥舞武器。他们向医院网络提出建议,希望添加一个新模型来检测口罩的正确使用。在 COVID-19 之前,医院各区域已经实施了强制佩戴口罩的政策,而在 COVID-19 期间,则要求在所有地方佩戴口罩。医院管理员希望根据未来的大流行风险水平来开启和关闭这一监控功能。他们意识到,人员会感到疲惫并忘记戴上口罩,或者有时口罩会部分滑落。许多访客也对佩戴口罩持敌对态度,他们可能会在进入医院时戴上口罩,但如果没有保安在场,就会摘下。这并不总是故意的,因此他们不希望像对其他威胁一样,在每次警报时都派遣保安:

杆上的黄色标志  描述由低置信度自动生成

图 13.1:像这样的雷达速度标志有助于遏制超速

意识是雷达速度标志(见图 13.1)的一种非常有效的方法,它通过仅让驾驶员意识到他们开得太快,从而使道路更安全。同样,在繁忙走廊的尽头设置屏幕,显示最近错误或故意未遵守强制佩戴口罩规定的人的快照,可能会让违规者感到尴尬。系统将记录反复违规者,以便保安可以找到他们,要么让他们遵守规定,要么要求他们离开现场。

对于试图欺骗模型规避合规性的访客,存在一些担忧,因此安全公司雇佣你来确保模型在面对这种对抗性攻击时具有鲁棒性。安全官员在之前注意到一些低技术含量的诡计,例如人们在意识到摄像头正在监控他们时,会暂时用手或毛衣的一部分遮住他们的脸。在一个令人不安的事件中,访客降低了灯光,并在摄像头上喷了一些凝胶,在另一个事件中,有人涂鸦了他们的嘴巴。然而,人们对更高技术攻击的担忧,例如干扰摄像头的无线信号或直接向摄像头照射高功率激光。执行这些攻击的设备越来越容易获得,可能会对更大规模的监控功能,如防止盗窃,产生影响。安全公司希望这种鲁棒性练习能够为改善每个监控系统和模型的努力提供信息。

最终,安全公司希望使用他们监控的医院中的面部图像来生成自己的数据集。同时,从外部来源合成的面具面部图像是他们短期内将模型投入生产的最佳选择。为此,你被提供了一组合成的正确和错误面具面部图像及其未面具对应图像的大型数据集。这两个数据集被合并成一个,原始的 1,024 × 1,024 尺寸被减少到缩略图的 124 × 124 尺寸。此外,为了提高效率,从这些数据集中采样了大约 21,000 张图像。

方法

你已经决定采取四步方法:

  • 探索几种可能的规避攻击,以了解模型对这些攻击的脆弱性以及它们作为威胁的可靠性

  • 使用预处理方法来保护模型免受这些攻击

  • 利用对抗性再训练来生成一个本质上对许多此类攻击更不易受影响的鲁棒分类器

  • 使用最先进的方法评估鲁棒性,以确保医院管理员相信该模型具有对抗性鲁棒性

让我们开始吧!

准备工作

你可以在以下位置找到这个示例的代码:github.com/PacktPublishing/Interpretable-Machine-Learning-with-Python-2E/tree/main/13/Masks.ipynb

加载库

要运行此示例,你需要安装以下库:

  • mldatasets 用于加载数据集

  • numpysklearn (scikit-learn) 用于操作它

  • tensorflow 用于拟合模型

  • matplotlibseaborn 用于可视化解释

你应该首先加载所有这些:

import math
import os
import warnings
warnings.filterwarnings("ignore")
import mldatasets
import numpy as np
from sklearn import preprocessing
import tensorflow as tf
from tensorflow.keras.utils import get_file
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import metrics
from art.estimators.classification import KerasClassifier
from art.attacks.evasion import FastGradientMethod,\
                      ProjectedGradientDescent, BasicIterativeMethod
from art.attacks.evasion import CarliniLInfMethod
from art.attacks.evasion import AdversarialPatchNumpy
from art.defences.preprocessor import SpatialSmoothing
from art.defences.trainer import AdversarialTrainer
from tqdm.notebook import tqdm 

让我们用 print(tf.__version__) 检查 TensorFlow 是否加载了正确的版本。版本应该是 2.0 及以上。

我们还应该禁用即时执行,并验证它是否已通过以下命令完成:

tf.compat.v1.disable_eager_execution()
print('Eager execution enabled:', tf.executing_eagerly()) 

输出应该显示为 False

在 TensorFlow 中,开启急切执行模式意味着它不需要计算图或会话。这是 TensorFlow 2.x 及以后版本的默认设置,但在之前的版本中不是,所以你需要禁用它以避免与为 TensorFlow 早期版本优化的代码不兼容。

理解和准备数据

我们将数据加载到四个 NumPy 数组中,对应于训练/测试数据集。在此过程中,我们将X面部图像除以 255,因为这样它们的值将在零和一之间,这对深度学习模型更好。我们称这种特征缩放。我们需要记录训练数据的min_max_,因为我们稍后会需要这些信息:

X_train, X_test, y_train, y_test = mldatasets.load(
    "maskedface-net_thumbs_sampled", prepare=True
)
X_train, X_test = X_train / 255.0, X_test / 255.0
min_ = X_train.min()
max_ = X_train.max() 

当我们加载数据时,始终验证数据非常重要,以确保数据没有被损坏:

print('X_train dim:\t%s' % (X_train.**shape**,))
print('X_test dim:\t%s' % (X_test.**shape**,))
print('y_train dim:\t%s' % (y_train.**shape**,))
print('y_test dim:\t%s' % (y_test.**shape**,))
print('X_train min:\t%s' % (**min_**))
print('X_train max:\t%s' % (**max_**))
print('y_train labels:\t%s' % (np.**unique**(y_train))) 
that they are not one-hot encoded. Indeed, by printing the unique values (np.unique(y_train)), we can tell that labels are represented as text: Correct for correctly masked, Incorrect for incorrectly masked, and None for no mask:
X_train dim:    (16800, 128, 128, 3)
X_test dim: (4200, 128, 128, 3)
y_train dim:    (16800, 1)
y_test dim: (4200, 1)
X_train min:    0.0
X_train max:    1.0
y_train labels: ['Correct' 'Incorrect' 'None'] 

因此,我们需要执行的一个预处理步骤是将y标签独热编码OHE),因为我们需要 OHE 形式来评估模型的预测性能。一旦我们初始化OneHotEncoder,我们就需要将其fit到训练数据(y_train)中。我们还可以将编码器中的类别提取到一个列表(labels_l)中,以验证它包含所有三个类别:

ohe = preprocessing.**OneHotEncoder**(sparse=False)
ohe.**fit**(y_train)
labels_l = ohe.**categories_**[0].tolist()
print(labels_l) 

为了确保可复现性,始终以这种方式初始化你的随机种子:

rand = 9
os.environ['PYTHONHASHSEED'] = str(rand)
tf.random.set_seed(rand)
np.random.seed(rand) 

使机器学习真正可复现意味着也要使其确定性,这意味着使用相同的数据进行训练将产生具有相同参数的模型。在深度学习中实现确定性非常困难,并且通常依赖于会话、平台和架构。如果你使用 NVIDIA GPU,你可以安装一个名为framework-reproducibility的库。

本章我们将要学习的许多对抗攻击、防御和评估方法都非常资源密集,所以如果我们用整个测试数据集来使用它们,它们可能需要数小时才能完成单个方法!为了提高效率,强烈建议使用测试数据集的样本。因此,我们将使用np.random.choice创建一个中等大小的 200 张图像样本(X_test_mdsample, y_test_mdsample)和一个小型 20 张图像样本(X_test_smsample, y_test_smsample):

sampl_md_idxs = np.random.choice(X_test.shape[0], 200, replace=False)
X_test_mdsample = X_test[sampl_md_idxs]
y_test_mdsample = y_test[sampl_md_idxs]
sampl_sm_idxs = np.random.choice(X_test.shape[0], 20, replace=False)
X_test_smsample = X_test[sampl_sm_idxs]
y_test_smsample = y_test[sampl_sm_idxs] 

我们有两个样本大小,因为某些方法在较大的样本大小下可能需要太长时间。现在,让我们看看我们的数据集中有哪些图像。在先前的代码中,我们已经从我们的测试数据集中取了一个中等和一个小样本。我们将使用以下代码将我们小样本中的每张图像放置在一个 4 × 5 的网格中,类别标签位于其上方:

plt.subplots(figsize=(15,12))
for s in range(20):
    plt.subplot(4, 5, s+1)
    plt.title(y_test_smsample[s][0], fontsize=12)
    plt.imshow(X_test_smsample[s], interpolation='spline16')
    plt.axis('off')
plt.show() 

上述代码在图 13.2中绘制了图像网格:

一个人物的拼贴画  描述由中等置信度自动生成

图 13.2:一个带有遮挡和未遮挡面部的小型测试数据集样本

图 13.2 展示了各种年龄、性别和种族的正面和反面、带口罩和不带口罩的面部图像。尽管种类繁多,但关于这个数据集的一个需要注意的事项是,它只展示了浅蓝色的外科口罩,且图像大多是正面角度。理想情况下,我们会生成一个包含所有颜色和类型口罩的更大数据集,并在训练前或训练期间对其进行随机旋转、剪切和亮度调整,以进一步增强模型的鲁棒性。这些增强将使模型更加鲁棒。尽管如此,我们必须区分这种一般类型的鲁棒性和对抗鲁棒性。

加载 CNN 基础模型

您不必训练 CNN 基础模型,但相关的代码已提供在 GitHub 仓库中。预训练模型也已存储在那里。我们可以快速加载模型并输出其摘要,如下所示:

model_path = **get_file**('CNN_Base_MaskedFace_Net.hdf5',\
    'https://github.com/PacktPublishing/Interpretable-Machine- \
    Learning-with-Python/blob/master/models/ \
    CNN_Base_MaskedFace_Net.hdf5?raw=true')
base_model = tf.keras.models.**load_model**(model_path)
base_model.**summary**() 

上述代码片段输出了以下摘要:

Model: "CNN_Base_MaskedFaceNet_Model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 126, 126, 16)      448       _________________________________________________________________
maxpool2d_1 (MaxPooling2D)   (None, 63, 63, 16)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 61, 61, 32)        4640      
_________________________________________________________________
maxpool2d_2 (MaxPooling2D)   (None, 30, 30, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 28, 28, 64)        18496     
_________________________________________________________________
maxpool2d_3 (MaxPooling2D)   (None, 14, 14, 64)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 12, 12, 128)       73856     
_________________________________________________________________
maxpool2d_4 (MaxPooling2D)   (None, 6, 6, 128)         0         
_________________________________________________________________
flatten_6 (Flatten)          (None, 4608)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 768)               3539712   
_________________________________________________________________
dropout_6 (Dropout)          (None, 768)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 3)                 2307      
=================================================================
Total params: 3,639,459
Trainable params: 3,639,459
Non-trainable params: 0
_________________________________________________________________ 

摘要几乎包含了我们需要了解的所有关于模型的信息。它有四个卷积层 (Conv2D),每个卷积层后面都跟着一个最大池化层 (MaxPooling2D)。然后是一个 Flatten 层和一个全连接层 (Dense)。接着,在第二个 Dense 层之前还有更多的 Dropout。自然地,这个最终层有三个神经元,对应于每个类别。

评估 CNN 基础分类器

我们可以使用 evaluate_multiclass_mdl 函数和测试数据集来评估模型。参数包括模型 (base_model)、我们的测试数据 (X_test) 和相应的标签 (y_test),以及类名 (labels_l) 和编码器 (ohe)。最后,由于准确率很高,我们不需要绘制 ROC 曲线(plot_roc=False)。此函数返回预测标签和概率,我们可以将这些变量存储起来以供以后使用:

y_test_pred, y_test_prob = mldatasets.**evaluate_multiclass_mdl**(
    base_model,
    X_test,
    y_test,
    labels_l,
    ohe,
    plot_conf_matrix=True,
    predopts={"verbose":1}
) 

上述代码生成了 图 13.3,其中包含每个类别的混淆矩阵和性能指标:

图 13.3:在测试数据集上评估的基础分类器的混淆矩阵和预测性能指标

尽管图 13.3 中的混淆矩阵似乎表明分类完美,但请注意圈出的区域。一旦我们看到召回率(99.5%)的分解,我们就可以知道模型在错误地分类带口罩的面部图像时存在一些问题。

现在,我们可以开始攻击这个模型,以评估它的实际鲁棒性!

了解逃避攻击

有六种广泛的对抗攻击类别:

  • 规避攻击:设计一个输入,使其能够导致模型做出错误的预测,尤其是当它不会欺骗人类观察者时。它可以是定向的或非定向的,这取决于攻击者意图欺骗模型将特定类别(定向)或任何类别(非定向)误分类。攻击方法可以是白盒攻击,如果攻击者可以完全访问模型及其训练数据集,或者黑盒攻击,只有推理访问。灰盒攻击位于中间。黑盒攻击总是模型无关的,而白盒和灰盒方法可能不是。

  • 投毒攻击:将错误的训练数据或参数注入模型,其形式取决于攻击者的能力和访问权限。例如,对于用户生成数据的系统,攻击者可能能够添加错误的数据或标签。如果他们有更多的访问权限,也许他们可以修改大量数据。他们还可以调整学习算法、超参数或数据增强方案。像规避攻击一样,投毒攻击也可以是定向的或非定向的。

  • 推理攻击:通过模型推理提取训练数据集。推理攻击也以多种形式出现,可以通过成员推理进行间谍活动(隐私攻击),以确认一个示例(例如,一个特定的人)是否在训练数据集中。属性推理确定一个示例类别(例如,种族)是否在训练数据中表示。输入推理(也称为模型反演)有攻击方法可以从模型中提取训练数据集,而不是猜测和确认。这些具有广泛的隐私和监管影响,尤其是在医疗和法律应用中。

  • 特洛伊木马攻击:这会在推理期间通过触发器激活恶意功能,但需要重新训练模型。

  • 后门攻击:类似于特洛伊木马,但即使模型从头开始重新训练,后门仍然存在。

  • 重编程:在训练过程中通过悄悄引入专门设计以产生特定输出的示例来远程破坏模型。例如,如果你提供了足够多的标记为虎鲨的示例,其中四个小黑方块总是出现在相同的位置,模型就会学习到那是一个虎鲨,无论它是什么,从而故意迫使模型过度拟合。

前三种是最受研究的对抗攻击形式。一旦我们根据阶段和目标将它们分开,攻击可以进一步细分(见图 13.4)。阶段是指攻击实施时,因为它可以影响模型训练或其推理,而目标是攻击者希望从中获得什么。本章将仅处理规避破坏攻击,因为我们预计医院访客、患者和工作人员偶尔会破坏生产模型:

图片

图 13.4:按阶段和目标分类的对抗攻击方法表

尽管我们使用白盒方法来攻击、防御和评估模型的鲁棒性,但我们并不期望攻击者拥有这种级别的访问权限。我们只会使用白盒方法,因为我们完全访问了模型。在其他情况下,例如带有热成像系统和相应模型以检测犯罪者的银行监控系统,我们可能会预期专业攻击者使用黑盒方法来寻找漏洞!因此,作为该系统的防御者,我们明智的做法是尝试相同的攻击方法。

我们将用于对抗鲁棒性的库称为对抗鲁棒性工具箱ART),它由LF AI & 数据基金会支持——这些人还支持其他开源项目,如 AIX360 和 AIF360,这些项目在第十一章中进行了探讨,即偏差缓解和因果推断方法。ART 要求攻击模型被抽象为估计器或分类器,即使它是黑盒的。在本章的大部分内容中,我们将使用 KerasClassifier,但在最后一节中,我们将使用 TensorFlowV2Classifier。初始化 ART 分类器很简单。你必须指定 model,有时还有其他必需的属性。对于 KerasClassifier,所有剩余的属性都是可选的,但建议你使用 clip_values 来指定特征的取值范围。许多攻击是输入排列,因此了解允许或可行的输入值是什么至关重要:

base_classifier = **KerasClassifier**(
    model=base_model, clip_values=(min_, max_)
)
y_test_mdsample_prob = np.**max**(
    y_test_prob[sampl_md_idxs], axis=1
)
y_test_smsample_prob = np.**max**(
    y_test_prob[sampl_sm_idxs], axis=1
) 

在前面的代码中,我们还准备了两个数组,用于预测中等和较小样本的类别概率。这完全是可选的,但这些有助于在绘制一些示例时将预测概率放置在预测标签旁边。

快速梯度符号法攻击

最受欢迎的攻击方法之一是快速梯度符号法FSGMFGM)。正如其名所示,它利用深度学习模型的梯度来寻找对抗性示例。它对输入图像的像素进行小的扰动,无论是加法还是减法。使用哪种方法取决于梯度的符号,这表明根据像素的强度,哪个方向会增加或减少损失。

与所有 ART 攻击方法一样,你首先通过提供 ART 估计器或分类器来初始化它。FastGradientMethod 还需要一个攻击步长 eps,这将决定攻击强度。顺便提一下,eps 代表 epsilon (),它代表误差范围或无穷小近似误差。小的步长会导致像素强度变化不太明显,但它也会错误分类较少的示例。较大的步长会导致更多示例被错误分类,并且变化更明显:

attack_fgsm = **FastGradientMethod**(base_classifier, eps=0.1) 

初始化后,下一步是generate对抗示例。唯一必需的属性是原始示例(X_test_mdsample)。请注意,FSGM 可以是针对特定目标的,因此在初始化中有一个可选的targeted属性,但你还需要在生成时提供相应的标签。这种攻击是非针对特定目标的,因为攻击者的意图是破坏模型:

X_test_fgsm = attack_fgsm.**generate**(X_test_mdsample) 

与其他方法相比,使用 FSGM 生成对抗示例非常快,因此称之为“快速”!

现在,我们将一举两得。首先,使用evaluate_multiclass_mdl评估对抗示例(X_test_fgsm)对我们基础分类器模型(base_classifier.model)的模型。然后我们可以使用compare_image_predictions来绘制图像网格,对比随机选择的对抗示例(X_test_fgsm)与原始示例(X_test_mdsample)及其相应的预测标签(y_test_fgsm_predy_test_mdsample)和概率(y_test_fgsm_proby_test_mdsample_prob)。我们自定义标题并限制网格为四个示例(num_samples)。默认情况下,compare_image_predictions仅比较误分类,但可以通过将可选属性use_misclass设置为False来比较正确分类:

y_test_fgsm_pred, y_test_fgsm_prob =\
    mldatasets.**evaluate_multiclass_mdl**(\
        base_classifier.model, X_test_fgsm, y_test_mdsample,\
        labels_l, ohe, plot_conf_matrix=False, plot_roc=False
    )
y_test_fgsm_prob = np.**max**(y_test_fgsm_prob, axis=1)
mldatasets.**compare_image_predictions**(
    X_test_fgsm, X_test_mdsample, y_test_fgsm_pred,\
    y_test_mdsample.flatten(), y_test_fgsm_prob,\
    y_test_mdsample_prob, title_mod_prefix="Attacked:",\
    title_difference_prefix="FSGM Attack Average Perturbation:",\
    num_samples=4
) 

之前的代码首先输出一个表格,显示模型在 FSGM 攻击示例上的准确率仅为 44%!尽管这不是针对特定目标的攻击,但它对正确遮挡的面部效果最为显著。所以假设,如果肇事者能够造成这种程度的信号扭曲或干扰,他们将严重削弱公司监控口罩合规性的能力。

代码还输出了图 13.5,该图显示了由 FSGM 攻击引起的一些误分类。攻击在图像中几乎均匀地分布了噪声。它还显示图像仅通过均方误差 0.092 进行了修改,由于像素值介于 0 和 1 之间,这意味着 9.2%。如果你要校准攻击以使其更难检测但仍然具有影响力,你必须注意,eps为 0.1 会导致 9.2%的平均绝对扰动,这会将准确性降低到 44%:

一个人物的拼贴画  描述由低置信度自动生成

图 13.5:比较基础分类器 FSGM 攻击前后图像的图表

说到更难检测的攻击,我们现在将了解 Carlini 和 Wagner 攻击。

Carlini 和 Wagner 无穷范攻击

在 2017 年,Carlini 和 WagnerC&W)采用了三种基于范数的距离度量:img/B18406_13_002.pngimg/B18406_13_003.png,和img/B18406_13_004.png,测量原始样本与对抗样本之间的差异。在其他论文中,这些度量已经被讨论过,包括 FSGM。C&W 引入的创新是如何利用这些度量,使用基于梯度下降的优化算法来近似损失函数的最小值。具体来说,为了避免陷入局部最小值,他们在梯度下降中使用多个起始点。为了使过程“生成一个有效的图像”,它评估了三种方法来约束优化问题。在这种情况下,我们想要找到一个对抗样本,该样本与原始图像之间的距离是最小的,同时仍然保持现实性。

所有的三种 C&W 攻击(img/B18406_13_002.pngimg/B18406_13_003.png,和img/B18406_13_004.png)都使用 Adam 优化器快速收敛。它们的主要区别是距离度量,其中img/B18406_13_004.png可以说是最好的一个。它定义如下:

img/B18406_13_009.png

由于它是到任何坐标的最大距离,你确保对抗样本不仅在“平均”上最小化差异,而且在特征空间的任何地方都不会有太大差异。这就是使攻击更难以检测的原因!

使用 C&W 无穷范数攻击初始化和生成对抗样本与 FSGM 类似。要初始化CarliniLInfMethod,我们可以可选地定义一个batch_size(默认为128)。然后,为了generate一个非目标对抗攻击,与 FSGM 相同。在非目标攻击中只需要X,而在目标攻击中需要y

attack_cw = **CarliniLInfMethod**(
    base_classifier, batch_size=40
)
X_test_cw = attack_cw.**generate**(X_test_mdsample) 

我们现在将评估 C&W 对抗样本(X_test_cw),就像我们评估 FSGM 一样。代码完全相同,只是将fsgm替换为cw,并在compare_image_predictions中更改不同的标题。就像 FSGM 一样,以下代码将生成一个分类报告和图像网格(图 13.6):

y_test_cw_pred, y_test_cw_prob =\
    mldatasets.**evaluate_multiclass_mdl**(
        base_classifier.model, X_test_cw, y_test_mdsample, labels_l,\
        ohe, plot_conf_matrix=False, plot_roc=False
    )
y_test_cw_prob = np.**max**(y_test_cw_prob, axis=1)
mldatasets.**compare_image_predictions**(
    X_test_cw,\
    X_test_mdsample, y_test_cw_pred,\
    y_test_mdsample.flatten(), y_test_cw_prob,\
    y_test_mdsample_prob, title_mod_prefix="Attacked:",\
    title_difference_prefix="C&W Inf Attack Average Perturbation",\
    num_samples=4
) 

如前述代码输出,C&W 对抗样本在我们的基础模型中具有 92%的准确率。这种下降足以使模型对其预期用途变得无用。如果攻击者仅对摄像机的信号进行足够的干扰,他们就能达到相同的结果。而且,正如你从图 13.6中可以看出,与 FSGM 相比,0.3%的扰动非常小,但它足以将 8%的分类错误,包括网格中看起来明显的四个分类错误。

一个孩子的拼贴,描述由低置信度自动生成

图 13.6:比较基础分类器中 C&W 无穷范数攻击与原始图像的绘图

有时候,攻击是否被检测到并不重要。重点是做出声明,这正是对抗补丁所能做到的。

目标对抗补丁攻击

对抗性补丁APs)是一种鲁棒、通用且具有针对性的方法。你可以生成一个补丁,既可以叠加到图像上,也可以打印出来并物理放置在场景中以欺骗分类器忽略场景中的其他所有内容。它旨在在各种条件和变换下工作。与其他对抗性示例生成方法不同,没有意图隐藏攻击,因为本质上,你用补丁替换了场景中可检测的部分。该方法通过利用期望变换EOT)的变体来工作,该变体在图像的不同位置对给定补丁的变换上进行图像训练。它所学习的是在训练示例中欺骗分类器最多的补丁。

这种方法比 FSGM 和 C&W 需要更多的参数和步骤。首先,我们将使用AdversarialPatchNumpy,这是可以与任何神经网络图像或视频分类器一起工作的变体。还有一个适用于 TensorFlow v2 的版本,但我们的基础分类器是KerasClassifier。第一个参数是分类器(base_classifier),我们将定义的其他参数是可选的,但强烈推荐。缩放范围scale_minscale_max尤其重要,因为它们定义了补丁相对于图像的大小可以有多大——在这种情况下,我们想测试的最小值不小于 40%,最大值不大于 70%。除此之外,定义一个目标类别(target)也是有意义的。在这种情况下,我们希望补丁针对“正确”类别。对于learning_rate和最大迭代次数(max_iter),我们使用默认值,但请注意,这些可以调整以提高补丁对抗的有效性:

attack_ap = **AdversarialPatchNumpy**(
    base_classifier, scale_min=0.4, scale_max=0.7,\
    learning_rate=5., max_iter=500,\
    batch_size=40, target=0
) 

我们不希望补丁生成算法在图像的每个地方都浪费时间测试补丁,因此我们可以通过使用布尔掩码来指导这种努力。这个掩码告诉它可以在哪里定位补丁。为了制作这个掩码,我们首先创建一个 128 × 128 的零数组。然后我们在像素 80–93 和 45–84 之间的矩形区域内放置 1,这大致对应于覆盖大多数图像中嘴巴的中心区域。最后,我们扩展数组的维度,使其变为(1, W, H),并将其转换为布尔值。然后我们可以使用小尺寸测试数据集样本和掩码来继续generate补丁:

placement_mask = np.**zeros**((128,128))
placement_mask[**80****:****93****,****45****:****83**] = 1
placement_mask = np.**expand_dims**(placement_mask, axis=0).astype(bool)
patch, patch_mask = attack_ap.**generate**(
    x=X_test_smsample,
    y=ohe.transform(y_test_smsample),
    mask=placement_mask
) 

我们现在可以使用以下代码片段绘制补丁:

plt.**imshow**(patch * patch_mask) 

上述代码生成了图 13.7中的图像。正如预期的那样,它包含了掩码中发现的许多蓝色阴影。它还包含明亮的红色和黄色色调,这些色调在训练示例中大多缺失,这会混淆分类器:

图表描述自动生成,置信度低

图 13.7:AP 生成的图像,误分类为正确掩码

与其他方法不同,generate没有生成对抗样本,而是一个单独的补丁,这是一个我们可以放置在图像上以创建对抗样本的图像。这个任务是通过apply_patch完成的,它接受原始示例X_test_smsample和一个比例;我们将使用 55%。还建议使用一个mask,以确保补丁被应用到更有意义的地方——在这种情况下,是嘴巴周围的区域:

X_test_ap = attack_ap.**apply_patch**(
    X_test_smsample,
    scale=0.55,
    mask=placement_mask
) 

现在是时候评估我们的攻击并检查一些误分类了。我们将像以前一样做,并重用生成图 13.5图 13.7的代码,只是我们将变量替换为ap和相应的标题:

y_test_ap_pred, y_test_ap_prob =\
    mldatasets.evaluate_multiclass_mdl(
        base_classifier.model, X_test_ap, y_test_smsample,
        labels_l, ohe, plot_conf_matrix=False, plot_roc=False
    )
y_test_ap_prob = np.max(y_test_ap_prob, axis=1)
mldatasets.compare_image_predictions(
    X_test_ap, X_test_smsample, y_test_ap_pred,\
    y_test_smsample.flatten(), y_test_ap_prob,
    y_test_smsample_prob, title_mod_prefix="Attacked:",\
    title_difference_prefix="AP Attack Average Perturbation:", num_samples=4
) 

前面的代码给出了我们攻击的准确率结果为 65%,考虑到它训练的样本数量很少,这个结果相当不错。与其它方法相比,AP 需要更多的数据。一般来说,定向攻击需要更多的样本来理解如何最好地针对某一类。前面的代码还生成了图 13.8中的图像网格,展示了假设人们如果在前额前拿着一张硬纸板,他们可以轻易地欺骗模型:

包含人物、姿势、不同、相同 描述自动生成

图 13.8:比较 AP 攻击与原始图像的基础分类器的图表

到目前为止,我们已经研究了三种攻击方法,但还没有解决如何防御这些攻击的问题。接下来,我们将探讨一些解决方案。

使用预处理防御定向攻击

有五种广泛的对抗防御类别:

  • 预处理:改变模型的输入,使其更难以攻击。

  • 训练:训练一个新的健壮模型,该模型旨在克服攻击。

  • 检测:检测攻击。例如,你可以训练一个模型来检测对抗样本。

  • Transformer:修改模型架构和训练,使其更健壮——这可能包括蒸馏、输入过滤器、神经元剪枝和重新学习等技术。

  • 后处理:改变模型输出以克服生产推理或模型提取攻击。

只有前四种防御可以与规避攻击一起工作,在本章中,我们只涵盖前两种:预处理对抗训练。FGSM 和 C&W 可以用这两种方法中的任何一种来防御,但 AP 更难防御,可能需要更强的检测Transformer方法。

在我们进行防御之前,我们必须先发起有针对性的攻击。我们将使用投影梯度下降法PGD),这是一种非常强大的攻击方法,其输出与 FSGM 非常相似——也就是说,它会产生噪声图像。在这里我们不会详细解释 PGD,但重要的是要注意,就像 FSGM 一样,它被视为一阶对抗者,因为它利用了关于网络的一阶信息(由于梯度下降)。此外,实验证明,对 PGD 的鲁棒性确保了对任何一阶对抗者的鲁棒性。具体来说,PGD 是一种强大的攻击,因此它为结论性的基准提供了依据。

要对正确掩码的类别发起有针对性的攻击,最好只选择那些没有被正确掩码的示例(x_test_notmasked)、它们对应的标签(y_test_notmasked)和预测概率(y_test_notmasked_prob)。然后,我们想要创建一个包含我们想要生成对抗性示例的类别(Correct)的数组(y_test_masked):

**not_masked_idxs** = np.**where**(y_test_smsample != 'Correct')[0]
X_test_notmasked = X_test_smsample[**not_masked_idxs**]
y_test_notmasked = y_test_smsample[**not_masked_idxs**]
y_test_notmasked_prob = y_test_smsample_prob[**not_masked_idxs**]
y_test_masked = np.array(
    ['Correct'] * X_test_notmasked.shape[0]
).reshape(-1,1) 

我们将ProjectedGradientDescent初始化与 FSGM 相同,除了我们将设置最大扰动(eps)、攻击步长(eps_step)、最大迭代次数(max_iter)和targeted=True。正是因为它是针对性的,所以我们将同时设置Xy

attack_pgd = **ProjectedGradientDescent**(
    base_classifier, eps=0.3, eps_step=0.01,\
    max_iter=40, targeted=True
)
X_test_pgd = attack_pgd.**generate**(
    X_test_notmasked, y=ohe.transform(y_test_masked)
) 

现在,让我们像之前一样评估 PGD 攻击,但这次,让我们绘制混淆矩阵(plot_conf_matrix=True):

y_test_pgd_pred, y_test_pgd_prob =\
    mldatasets.**evaluate_multiclass_mdl**(
        base_classifier.model, X_test_pgd, y_test_notmasked,\
        labels_l, ohe, plot_conf_matrix=True, plot_roc=False
    )
y_test_pgd_prob = np.**max**(y_test_pgd_prob, axis=1) 
Figure 13.9. The PGD attack was so effective that it produced an accuracy of 0%, making all unmasked and incorrectly masked examples appear to be masked:

图表描述自动生成

图 13.9:针对基础分类器评估的 PGD 攻击示例的混淆矩阵

接下来,让我们运行compare_image_prediction来查看一些随机误分类:

mldatasets.**compare_image_predictions**(
    X_test_pgd, X_test_notmasked, y_test_pgd_pred,\
    y_test_notmasked.flatten(), y_test_pgd_prob,\
    y_test_smsample_prob, title_mod_prefix="Attacked:",\
    num_samples=4, title_difference_prefix="PGD Attack Average Perturbation:"
) 

上述代码在图 13.10中绘制了图像网格。平均绝对扰动是我们迄今为止看到的最高值,达到 14.7%,并且网格中所有未掩码的面部都被分类为正确掩码:

一个人拼贴,描述自动生成,中等置信度

图 13.10:比较基础分类器的 PGD 攻击图像与原始图像的绘图

准确率不能变得更差,图像的颗粒度已经无法修复。那么我们如何对抗噪声呢?如果你还记得,我们之前已经处理过这个问题了。在第七章可视化卷积神经网络中,SmoothGrad通过平均梯度改进了显著性图。这是一个不同的应用,但原理相同——就像人类一样,噪声显著性图比平滑的显著性图更难以解释,而颗粒图像比平滑图像对模型来说更难以解释。

空间平滑只是说模糊的一种花哨说法!然而,它作为对抗防御方法引入的新颖之处在于,所提出的实现(SpatialSmoothing)要求在滑动窗口中使用中位数而不是平均值。window_size是可配置的,建议在最有用的防御位置进行调整。一旦防御初始化,你就可以插入对抗示例(X_test_pgd)。它将输出空间平滑的对抗示例(X_test_pgd_ss):

defence_ss = **SpatialSmoothing**(window_size=11)
X_test_pgd_ss, _ = **defence_ss**(X_test_pgd) 

现在,我们可以将产生的模糊对抗示例评估如前所述——首先,使用evaluate_multiclass_mdl获取预测标签(y_test_pgd_ss_pred)和概率(y_test_pgd_ss_prob),以及一些预测性能指标输出。使用compare_image_predictions绘制图像网格,让我们使用use_misclass=False来正确分类图像——换句话说,就是成功防御的对抗示例:

y_test_pgd_ss_pred, y_test_pgd_ss_prob =\
    mldatasets.**evaluate_multiclass_mdl**(
        base_classifier.model, X_test_pgd_ss,\
        y_test_notmasked, labels_l, ohe,\
        plot_conf_matrix=False, plot_roc=False
)
y_test_pgd_ss_prob = np.**max**(y_test_pgd_ss_prob, axis=1)
mldatasets.**compare_image_predictions**(
    X_test_pgd_ss, X_test_notmasked, y_test_pgd_ss_pred,\
    y_test_notmasked.flatten(), y_test_pgd_ss_prob,\
    y_test_notmasked_prob, use_misclass=False,\
    title_mod_prefix="Attacked+Defended:", num_samples=4,\
    title_difference_prefix="PGD Attack & Defended Average:"
) 

上述代码得到 54%的准确率,这比空间平滑防御之前的 0%要好得多。它还生成了图 13.11,展示了模糊如何有效地阻止 PGD 攻击。它甚至将平均绝对扰动减半!

一群人的拼贴,描述自动生成,置信度低

图 13.11:比较空间平滑 PGD 攻击图像与基础分类器原始图像的图表

接下来,我们将在我们的工具箱中尝试另一种防御方法:对抗训练!

通过对鲁棒分类器进行对抗训练来抵御任何逃避攻击

第七章可视化卷积神经网络中,我们确定了一个垃圾图像分类器,它很可能在市政回收厂预期的环境中表现不佳。在样本外数据上的糟糕表现是由于分类器是在大量公开可用的图像上训练的,这些图像与预期的条件不匹配,或者与回收厂处理的材料的特征不匹配。章节的结论呼吁使用代表其预期环境的图像来训练网络,以创建一个更鲁棒的模型。

为了模型的鲁棒性,训练数据的多样性至关重要,但前提是它能够代表预期的环境。在统计学的术语中,这是一个关于使用样本进行训练的问题,这些样本能够准确描述总体,从而使模型学会正确地分类它们。对于对抗鲁棒性,同样的原则适用。如果你增强数据以包括可能的对抗攻击示例,模型将学会对它们进行分类。这就是对抗训练的本质。

对抗鲁棒性领域的机器学习研究人员建议这种防御形式对任何类型的规避攻击都非常有效,本质上可以保护它。但话虽如此,它并非坚不可摧。其有效性取决于在训练中使用正确类型的对抗样本、最优的超参数等等。研究人员概述了一些指导方针,例如增加隐藏层中的神经元数量,并使用 PGD 或 BIM 生成训练对抗样本。BIM代表基本迭代方法。它类似于 FSGM,但速度不快,因为它通过迭代来逼近原始图像在-邻域内的最佳对抗样本。eps属性限制了这一邻域。

训练一个鲁棒模型可能非常耗费资源。虽然我们也可以下载一个已经训练好的,但这很重要,要理解如何使用 ART 来完成这一过程。我们将解释这些步骤,以便有选择地使用 ART 完成模型训练。否则,只需跳过这些步骤并下载训练好的模型。robust_modelbase_model非常相似,除了我们在四个卷积(Conv2D)层中使用等大小的过滤器。我们这样做是为了降低复杂性,以抵消我们通过将第一隐藏(Dense)层中的神经元数量翻倍所增加的复杂性,正如机器学习研究人员所建议的:

robust_model = tf.keras.models.**Sequential**([
    tf.keras.layers.**InputLayer**(input_shape=X_train.shape[1:]),
    tf.keras.layers.**Conv2D**(32, kernel_size=(3, 3), activation='relu'),
    tf.keras.layers.**MaxPooling2D**(pool_size=(2, 2)),
    tf.keras.layers.**Conv2D**(32, kernel_size=(3, 3), activation='relu'),
    tf.keras.layers.**MaxPooling2D**(pool_size=(2, 2)),
    tf.keras.layers.**Conv2D**(32, kernel_size=(3, 3), activation='relu'),
    tf.keras.layers.**MaxPooling2D**(pool_size=(2, 2)),
    tf.keras.layers.**Conv2D**(32, kernel_size=(3, 3), activation='relu'),
    tf.keras.layers.**MaxPooling2D**(pool_size=(2, 2)),
    tf.keras.layers.**Flatten**(),
    tf.keras.layers.**Dense**(3072, activation='relu'),
    tf.keras.layers.**Dropout**(0.2),
    tf.keras.layers.**Dense**(3, activation='softmax')
], name='CNN_Robust_MaskedFaceNet_Model')
robust_model.**compile**(
    optimizer=tf.keras.optimizers.Adam(lr=0.001),
    loss='categorical_crossentropy',
    metrics=['accuracy'])
robust_model.**summary**() 

前面代码中的summary()输出了以下内容。你可以看到可训练参数总数约为 360 万,与基础模型相似:

Model: "CNN_Robust_MaskedFaceNet_Model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 126, 126, 32)      896       
_________________________________________________________________
maxpool2d_1 (MaxPooling2D)   (None, 63, 63, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 61, 61, 32)        9248      
_________________________________________________________________
maxpool2d_2 (MaxPooling2D)   (None, 30, 30, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 28, 28, 32)        9248      
_________________________________________________________________
maxpool2d_3 (MaxPooling2D)   (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 12, 12, 32)        9248      
_________________________________________________________________
maxpool2d_4 (MaxPooling2D)   (None, 6, 6, 32)          0         
_________________________________________________________________
flatten (Flatten)            (None, 1152)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 3072)              3542016   
_________________________________________________________________
dropout (Dropout)            (None, 3072)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 3)                 9219      
=================================================================
Total params: 3,579,875
Trainable params: 3,579,875
Non-trainable params: 0
_________________________________________________________________ 

接下来,我们可以通过首先使用robust_model初始化一个新的KerasClassifier来对抗性地训练模型。然后,我们初始化这个分类器上的BasicIterativeMethod攻击。最后,我们使用robust_classifier和 BIM 攻击初始化AdversarialTrainer并对其进行fit。请注意,我们将 BIM 攻击保存到了一个名为attacks的变量中,因为这个变量可能是一系列 ART 攻击而不是单一的一个。另外,请注意AdversarialTrainer有一个名为ratio的属性。这个属性决定了训练样本中有多少是对抗样本。这个百分比会极大地影响对抗攻击的有效性。如果它太低,可能无法很好地处理对抗样本,如果太高,可能在与非对抗样本的交互中效果较差。如果我们运行trainer,它可能需要很多小时才能完成,所以请不要感到惊讶:

robust_classifier = **KerasClassifier**(
    model=robust_model, clip_values=(min_, max_)
)
attacks = **BasicIterativeMethod**(
    robust_classifier, eps=0.3, eps_step=0.01, max_iter=20
)
trainer = **AdversarialTrainer**(
    robust_classifier, attacks, ratio=0.5
)
trainer.fit(
    X_train, ohe.transform(y_train), nb_epochs=30, batch_size=128
) 

如果你没有训练robust_classifier,你可以下载一个预训练的robust_model,并像这样用它来初始化robust_classifier

model_path = **get_file**(
    'CNN_Robust_MaskedFace_Net.hdf5',
    'https://github.com/PacktPublishing/Interpretable-Machine- \
    Learning-with-Python/blob/master/models/ \
    CNN_Robust_MaskedFace_Net.hdf5?raw=true'
)
robust_model = tf.keras.models.**load_model**(model_path)
robust_classifier = **KerasClassifier**(
    model=robust_model, clip_values=(min_, max_)
) 

现在,让我们使用evaluate_multiclass_mdl来评估robust_classifier对原始测试数据集的性能。我们将plot_conf_matrix设置为True以查看混淆矩阵:

y_test_robust_pred, y_test_robust_prob =\
mldatasets.**evaluate_multiclass_mdl**(
    robust_classifier.model, X_test, y_test, labels_l, ohe,\
    plot_conf_matrix=True, predopts={"verbose":1}
) 

上述代码输出了图 13.12中的混淆矩阵和性能指标。它的准确率比基础分类器低 1.8%。大多数错误分类都是将正确遮挡的面部错误地分类为错误遮挡。在选择 50%的对抗示例比例时,肯定存在权衡,或者我们可以调整超参数或模型架构来改进这一点:

图表,树状图图表  自动生成的描述

图 13.12:鲁棒分类器的混淆度指标和性能指标

让我们看看鲁棒模型在对抗攻击中的表现。我们再次使用FastGradientMethod,但这次,将base_classifier替换为robust_classifier

attack_fgsm_robust = **FastGradientMethod**(
    robust_classifier, eps=0.1
)
X_test_fgsm_robust = attack_fgsm_robust.**generate**(X_test_mdsample) 

接下来,我们可以使用evaluate_multiclass_mdlcompare_image_predictions来衡量和观察我们攻击的有效性,但这次针对的是robust_classifier

y_test_fgsm_robust_pred, y_test_fgsm_robust_prob =\
    mldatasets.**evaluate_multiclass_mdl**(
        robust_classifier.model, X_test_fgsm_robust,\
        y_test_mdsample, labels_l, ohe,\
        plot_conf_matrix=False, plot_roc=False
    )
y_test_fgsm_robust_prob = np.**max**(
    y_test_fgsm_robust_prob, axis=1
)
mldatasets.**compare_image_predictions**(
    X_test_fgsm_robust, X_test_mdsample,
    y_test_fgsm_robust_pred, num_samples=4,\
    y_test_mdsample.flatten(), y_test_fgsm_robust_prob,\
    y_test_mdsample_prob, title_mod_prefix="Attacked:",\
    title_difference_prefix="FSGM Attack Average Perturbation:"
) 
base_classifier, it yielded a 44% accuracy. That was quite an improvement! The preceding code also produces the image grid in *Figure 13.13*. You can tell how the FSGM attack against the robust model makes less grainy and more patchy images. On average, they are less perturbed than they were against the base model because so few of them were successful, but those that were significantly degraded. It appears as if the FSGM reduced their color depth from millions of possible colors (24+ bits) to 256 (8-bit) or 16 (4-bit) colors. Of course, an evasion attack can’t actually do that, but what happened was that the FSGM algorithm converged at the same shades of blue, brown, red, and orange that could fool the classifier! Other shades remain unaltered: 

婴儿拼贴  使用低置信度自动生成的描述

图 13.13:比较鲁棒分类器被 FSGM 攻击与原始图像的图表

到目前为止,我们只评估了模型的鲁棒性,但只针对一种攻击强度,没有考虑可能的防御措施,因此评估了其鲁棒性。在下一节中,我们将研究一种实现这一目标的方法。

评估对抗鲁棒性

在任何工程实践中测试你的系统以了解它们对攻击或意外故障的脆弱性是必要的。然而,安全是一个你必须对你的解决方案进行压力测试的领域,以确定需要多少级别的攻击才能使你的系统崩溃超过可接受的阈值。此外,弄清楚需要多少级别的防御来遏制攻击也是非常有用的信息。

比较模型鲁棒性与攻击强度

现在我们有两个分类器可以与同等强度的攻击进行比较,我们尝试不同的攻击强度,看看它们在所有这些攻击中的表现如何。我们将使用 FSGM,因为它速度快,但你可以使用任何方法!

我们可以评估的第一个攻击强度是没有攻击强度。换句话说,没有攻击的情况下,对测试数据集的分类准确率是多少?我们已经有存储了基础模型(y_test_pred)和鲁棒模型(y_test_robust_pred)的预测标签,所以这可以通过 scikit-learn 的accuracy_score指标轻松获得:

accuracy_base_0 = metrics.accuracy_score(
    y_test, y_test_pred
)
accuracy_robust_0 = metrics.accuracy_score(
    y_test, y_test_robust_pred
) 

现在,我们可以在 0.01 和 0.9 之间迭代一系列攻击强度(eps_range)。使用linspace,我们可以生成 0.01 和 0.09 之间的 9 个值和 0.1 和 0.9 之间的 9 个值,并将它们concatenate成一个单一的数组。我们将通过for循环测试这 18 个eps值的所有攻击,攻击每个模型,并使用evaluate检索攻击后的准确度。相应的准确度被附加到两个列表(accuracy_baseaccuracy_robust)中。在for循环之后,我们将 0 添加到eps_range中,以考虑任何攻击之前的准确度:

eps_range = np.**concatenate**(
    (np.linspace(0.01, 0.09, 9), np.linspace(0.1, 0.9, 9)), axis=0
).tolist()
accuracy_base = [accuracy_base_0]
accuracy_robust = [accuracy_robust_0]
for **eps** in tqdm(eps_range, desc='EPS'):
    attack_fgsm.set_params(**{'eps': **eps**})
    X_test_fgsm_base_i =attack_fgsm.**generate**(X_test_mdsample)
    _, accuracy_base_i =\
    base_classifier.model.**evaluate**(
        X_test_fgsm_base_i, ohe.transform(y_test_mdsample)
    )
    attack_fgsm_robust.set_params(**{'eps': **eps**})
    X_test_fgsm_robust_i=attack_fgsm_robust.**generate**(
        X_test_mdsample
    )
    _, accuracy_robust_i =\
        robust_classifier.model.**evaluate**(
            X_test_fgsm_robust_i, ohe.transform(y_test_mdsample)
            )
    accuracy_base.append(accuracy_base_i)
    accuracy_robust.append(accuracy_robust_i) 
eps_range = [0] + eps_range 

现在,我们可以使用以下代码绘制两个分类器在所有攻击强度下的准确度图:

fig, ax = plt.subplots(figsize=(14,7))
ax.plot(
    np.array(eps_range), np.array(accuracy_base),\
    'b–', label='Base classifier'
)
ax.plot(
    np.array(eps_range), np.array(accuracy_robust),\
    'r–', label='Robust classifier'
)
legend = ax.legend(loc='upper center')
plt.xlabel('Attack strength (eps)')
plt.ylabel('Accuracy') 

之前的代码生成了图 13.14,该图展示了鲁棒模型在攻击强度为 0.02 和 0.3 之间表现更好,但之后则始终比基准模型差大约 10%:

图表,折线图  自动生成描述

图 13.14:在多种 FSGM 攻击强度下对鲁棒和基准分类器的准确度进行测量

图 13.14未能考虑的是防御措施。例如,如果医院摄像头持续受到干扰或篡改,安全公司不保护他们的模型将是失职的。对于这种攻击,最简单的方法是使用某种平滑技术。

对抗性训练也产生了一个经验上鲁棒的分类器,你不能保证它在某些预定义的情况下会工作,这就是为什么需要可验证的防御措施。

任务完成

任务是执行他们面部口罩模型的一些对抗性鲁棒性测试,以确定医院访客和员工是否可以规避强制佩戴口罩的规定。基准模型在许多规避攻击中表现非常糟糕,从最激进的到最微妙的。

你还研究了这些攻击的可能防御措施,例如空间平滑和对抗性重新训练。然后,你探索了评估你提出的防御措施鲁棒性的方法。你现在可以提供一个端到端框架来防御这种攻击。话虽如此,你所做的一切只是一个概念验证。

现在,你可以提出训练一个可验证的鲁棒模型来对抗医院预期遇到的最常见的攻击。但首先,你需要一个一般鲁棒模型的成分。为此,你需要使用原始数据集中的所有 210,000 张图片,对它们进行许多关于遮罩颜色和类型的变体,并使用合理的亮度、剪切和旋转变换进一步增强。最后,鲁棒模型需要用几种攻击进行训练,包括几种 AP 攻击。这些攻击很重要,因为它们模仿了最常见的合规规避行为,即用身体部位或衣物物品隐藏面部。

摘要

阅读本章后,您应该了解如何对机器学习模型进行攻击,特别是逃避攻击。您应该知道如何执行 FSGM、BIM、PGD、C&W 和 AP 攻击,以及如何通过空间平滑和对抗性训练来防御它们。最后但同样重要的是,您应该了解如何评估对抗性鲁棒性。

下一章是最后一章,它概述了关于机器学习解释未来发展的想法。

数据集来源

  • Adnane Cabani,Karim Hammoudi,Halim Benhabiles 和 Mahmoud Melkemi,2020,MaskedFace-Net - 在 COVID-19 背景下正确/错误佩戴口罩的人脸图像数据集,Smart Health,ISSN 2352–6483,Elsevier:doi.org/10.1016/j.smhl.2020.100144(由 NVIDIA 公司提供的 Creative Commons BY-NC-SA 4.0 许可证)

  • Karras, T.,Laine, S.,和 Aila, T.,2019,用于生成对抗网络的基于风格的生成器架构。2019 IEEE/CVF 计算机视觉与模式识别会议(CVPR),4396–4405:arxiv.org/abs/1812.04948(由 NVIDIA 公司提供的 Creative Commons BY-NC-SA 4.0 许可证)

进一步阅读

在 Discord 上了解更多

要加入这本书的 Discord 社区——在那里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:

packt.link/inml

二维码

第十四章:机器学习可解释性的未来是什么?

在过去的十三章中,我们探讨了机器学习(ML)可解释性的领域。正如前言所述,这是一个广泛的研究领域,其中大部分甚至还没有离开实验室,尚未得到广泛应用,本书无意涵盖所有内容。相反,目标是深入介绍各种可解释性工具,以便作为初学者的起点,甚至补充更高级读者的知识。本章将总结我们在机器学习可解释性方法生态系统中的所学,并推测接下来会发生什么!

这些是我们将在本章中讨论的主要主题:

  • 理解机器学习可解释性的当前格局

  • 对机器学习可解释性未来的推测

理解机器学习可解释性的当前格局

首先,我们将提供一些关于本书如何与机器学习可解释性的主要目标相关联以及从业者如何开始应用这些方法来实现这些广泛目标的背景信息。然后,我们将讨论研究中的当前增长领域。

将一切联系在一起!

第一章中所述,解释、可解释性和可解释性;以及为什么这一切都很重要?,在讨论机器学习可解释性时,有三个主要主题:公平性、责任和透明度(FAT),每个主题都提出了一系列关注点(见图 14.1)。我想我们都可以同意,这些都是模型所期望的特性!确实,这些关注点都为人工智能(AI)系统的改进提供了机会。这些改进首先通过利用模型解释方法来评估模型、确认或反驳假设以及发现问题开始。

你的目标将取决于你在机器学习(ML)工作流程中的哪个阶段。如果模型已经投入生产,目标可能是用一系列指标来评估它,但如果模型仍处于早期开发阶段,目标可能是找到指标无法发现的更深层问题。也许你也在像我们在第四章中做的那样,仅仅使用黑盒模型进行知识发现——换句话说,利用模型从数据中学习,但没有计划将其投入生产。如果是这种情况,你可能会确认或反驳你对数据的假设,以及由此产生的模型:

图 14.1:机器学习解释方法

无论如何,这些目标都不是相互排斥的,你很可能始终在寻找问题并反驳假设,即使模型看起来表现良好!

并且无论目标是什么,主要关注点是什么,都建议您使用许多解释方法,这不仅是因为没有哪种技术是完美的,而且还因为所有问题和目标都是相互关联的。换句话说,没有一致性就没有正义,没有透明性就没有可靠性。

实际上,你可以从下到上阅读图 14.1,就像它是一座金字塔一样,因为透明性是基础,其次是第二层的问责制,最终,公平性是顶部的樱桃。

因此,即使目标是评估模型公平性,模型也应进行压力测试以确保其鲁棒性。应理解大多数相关特征重要性和交互作用。否则,如果预测不鲁棒且不透明,那就无关紧要了。

图 14.1中涵盖了多种解释方法,这些方法绝不是所有可用的解释方法。它们代表了背后有良好维护的开源库的最受欢迎的方法。在本书中,我们简要地提到了它们中的大多数,尽管其中一些只是简要提及。未讨论的用斜体表示,而讨论过的旁边提供了相关的章节编号。本书重点介绍了模型无关黑盒监督学习模型方法。然而,在这个领域之外,还有许多其他解释方法,例如强化学习、生成模型或仅用于线性回归的许多统计方法。即使在监督学习黑盒模型领域内,也有数百种针对特定应用的应用特定模型解释方法,这些应用范围从化学图 CNN 到客户流失分类器。

话虽如此,本书中讨论的许多方法可以定制用于各种应用。集成梯度可用于解释音频分类器和天气预报模型。敏感性分析可用于金融建模和传染病风险模型。因果推断方法可以用来改善用户体验和药物试验。

改进是这里的关键词,因为解释方法有另一面!

在本书中,这一面被称为为解释性调整,这意味着为 FAT 问题创造解决方案。这些解决方案可以在图 14.2中欣赏到:

图片

图 14.2:处理 FAT 问题的工具集

我观察到五种解释性解决方案的方法:

  • 缓解偏差:任何为考虑偏差而采取的纠正措施。请注意,这里的偏差指的是数据中的采样、排除、偏见和测量偏差,以及任何引入到机器学习工作流程中的其他偏差。

  • 设置护栏:任何确保模型受到约束,使其不与领域知识相矛盾且不自信地预测的解决方案。

  • 增强可靠性:任何增加预测信心和一致性的修复,不包括通过减少复杂性来实现的修复。

  • 减少复杂性:任何引入稀疏性的方法。作为一种副作用,这通常通过更好地泛化来增强可靠性。

  • 确保隐私:任何努力确保第三方无法获取私有数据和模型架构。我们在这本书中没有涵盖这种方法。

这些方法还可以应用于以下三个领域:

  • 数据(“预处理”):通过修改训练数据

  • 模型(“处理中”):通过修改模型、其参数或训练过程

  • 预测(“后处理”):通过干预模型的推理

有一个第四个领域可能会影响其他三个领域——即数据和算法治理。这包括规定某些方法或框架的法规和标准。这是一个缺失的列,因为很少有行业和司法管辖区有法律规定应该应用哪些方法和方法来遵守 FAT。例如,治理可以强制执行解释算法决策、数据来源或鲁棒性认证阈值的标准。我们将在下一节中进一步讨论这一点。

你可以从图 14.2中看出,许多方法在 FAT 上重复使用。特征选择和工程、单调约束正则化对三者都有益,但并不总是通过相同的方法来实现。数据增强也可以提高公平性和问责制的可靠性。与图 14.1一样,斜体中的内容在书中没有涉及,其中三个主题脱颖而出:不确定性估计对抗鲁棒性隐私保护是迷人的主题,值得有自己的一本书。

当前趋势

AI 采用的最重要的障碍之一是缺乏可解释性,这也是 50-90%的 AI 项目从未起飞的部分原因(关于这一点,请参阅进一步阅读部分的相关文章),另一个原因是由于不遵守 FAT 而产生的道德违规行为。在这方面,可解释机器学习iML)有力量引领整个 ML,因为它可以帮助实现这两个目标,如图 14.1 和图 14.2 中的相应方法。

幸运的是,我们正在见证对 iML 的兴趣和生产力的增加,这主要是在可解释人工智能XAI)的背景下——参见图 14.3。在科学界,iML 仍然是最受欢迎的术语,但在公共场合 XAI 占主导地位:

图形用户界面,应用程序描述自动生成

图 14.3:iML 和 XAI 的出版和搜索趋势

这意味着,正如机器学习开始标准化、监管、整合到众多其他学科一样,解释性也将很快获得一席之地。

机器学习(ML)正在取代所有行业的软件。随着越来越多的自动化,更多的模型被部署到云端,而人工智能物联网AIoT)的出现将使情况变得更糟。部署并不是传统上属于机器学习实践者的领域。这就是为什么机器学习越来越多地依赖于机器学习运维MLOps)。自动化速度的加快意味着需要更多的工具来构建、测试、部署和监控这些模型。同时,还需要对工具、方法和指标进行标准化。虽然这个过程缓慢但确实在发生。自 2017 年以来,我们已经有了开放神经网络交换ONNX),这是一个用于互操作性的开放标准。在撰写本文时,国际标准化组织ISO)正在编写超过二十项 AI 标准(其中一项已发布),其中一些涉及可解释性。自然地,由于机器学习模型类别、方法、库、服务提供商和实践的整合,一些事物将因常用而标准化。随着时间的推移,每个领域都将出现一或几个。最后,鉴于机器学习在算法决策中的重要作用,它被监管只是时间问题。只有一些金融市场监管交易算法,例如美国的证券交易委员会SEC)和英国的金融服务管理局FCA)。除此之外,只有数据隐私和来源法规得到广泛执行,例如美国的 HIPAA 和巴西的 LGPD。欧盟的通用数据保护条例GDPR)在算法决策的“解释权”方面更进一步,但预期的范围和方法仍然不明确。

可解释人工智能(XAI)与机器学习(IML)——哪一个该使用?

我的看法:尽管在行业中它们被视为同义词,且iML被视为更学术性的术语,但机器学习实践者,即使在工业界,也应该对使用iML这个术语持谨慎态度。词语可能具有过大的暗示力。可解释性意味着完全理解,但可解释性留有出错的空间,这在讨论模型时总是应该如此,尤其是对于极其复杂的黑盒模型。此外,人工智能被公众想象为万能的灵丹妙药,或者被诋毁为危险的。无论哪种情况,与iML这个术语一样,它都使得那些认为它是万能灵丹妙药的人更加自大,也许可以平息那些认为它是危险的人的担忧。XAI 这个术语可能作为一个营销术语在发挥作用。然而,对于构建模型的人来说,可解释性这个词语的暗示力可能会让我们对自己的解释过于自信。话虽如此,这仅仅是一个观点。

机器学习可解释性正在快速发展,但落后于机器学习。一些解释工具已经集成到云生态系统中,从 SageMaker 到 DataRobot。它们尚未完全自动化、标准化、整合和监管,但毫无疑问,这将会发生。

对机器学习可解释性的未来进行推测

我习惯了听到这个时期被比喻为“人工智能的蛮荒西部”,或者更糟糕的是,“人工智能淘金热”!这让人联想到一个未开发、未驯服的领土被急切地征服,或者更糟糕的是,被文明化。然而,在 19 世纪,美国的西部地区与其他地区的地球上的其他地区并没有太大的不同,并且已经被美洲原住民居住了数千年,所以这个比喻并不完全适用。用我们能够通过机器学习实现的准确性和信心来预测,会让我们的祖先感到惊恐,并且对我们人类来说,这并不是一个“自然”的位置。这更像是飞行而不是探索未知土地。

文章《迈向机器学习的喷气时代》(在本章末尾的“进一步阅读”部分链接)提出了一个更适合的比喻,即人工智能就像航空业的黎明。它是新的、令人兴奋的,人们仍然对我们从下面能做什么感到惊奇(见图 14.4)!

然而,航空业还有待发挥其潜力。在 barnstorming 时代几十年后,航空业成熟为安全、可靠和高效的商业航空喷气时代。在航空业的情况下,承诺是它可以在不到一天的时间内可靠地将货物和人员运送到地球另一半。在人工智能的情况下,承诺是它可以做出公平、负责任和透明的决策——也许不是针对任何决策,但至少是它被设计去做的决策,除非它是通用人工智能AGI)的例子:

包含文本、户外、旧式描述自动生成

图 14.4:20 世纪 20 年代的 barnstorming(美国国会图书馆的印刷与照片部)

那么,我们如何才能达到那里?以下是我预期在追求达到机器学习喷气时代的过程中可能会发生的一些想法。

机器学习的新愿景

由于我们打算在人工智能方面走得更远,比以往任何时候都要远,明天的机器学习从业者必须更加意识到天空的危险。在这里,我指的是预测和规范性分析的新前沿。风险众多,涉及各种偏见和假设,已知和潜在的数据问题,以及我们模型的数学属性和局限性。很容易被机器学习模型欺骗,认为它们是软件。然而,在这个类比中,软件在本质上是完全确定性的——它牢牢地扎根于地面,而不是在天空中悬浮!

为了使民用航空变得安全,需要一种新的思维方式——一种新的文化。二战时期的战斗机飞行员,尽管他们能力出众,但也必须重新训练以在民用航空中工作。这不是同一个任务,因为当你知道你正在携带乘客,并且风险很高时,一切都会改变。

伦理 AI,以及由此产生的 iML,最终需要这种认识,即模型直接或间接地携带乘客“在船上”,并且模型并不像看起来那样稳健。一个稳健的模型必须能够可靠地经受住几乎任何条件,一次又一次地,就像今天的飞机一样。为此,我们需要使用更多的工具,这些工具以解释方法的形式出现。

多学科方法

对于符合 FAT 原则的模型,需要与许多学科更紧密地集成。这意味着 AI 伦理学家、律师、社会学家、心理学家、以人为本的设计师以及无数其他职业的更大参与。他们将与 AI 技术专家和软件工程师一起,将最佳实践编码到标准和法规中。

足够的标准化

不仅需要新的标准来规范代码、指标和方法,还需要规范语言。数据背后的语言主要来自统计学、数学、计算机科学和计量经济学,这导致了很多混淆。

执行监管

很可能需要所有生产模型满足以下规范:

  • 能够通过认证证明其稳健和公平

  • 能够使用 TRACE 命令解释其预测背后的推理,在某些情况下,还必须与预测一起提供推理

  • 可以拒绝他们不确定的预测

  • 为所有预测提供置信水平(参见“进一步阅读”部分的一致性预测教程和书籍)

  • 拥有训练数据的元数据(即使匿名)和作者身份,以及必要时,符合监管要求的证书和与公共账本(可能是区块链)相关的元数据

  • 拥有类似于网站的证书,以确保一定程度的信任

  • 过期后停止工作,直到用新数据进行重新训练

  • 当它们在模型诊断失败时自动离线,并且只有通过诊断后才能再次上线

  • 拥有持续训练/持续集成CT/CI)管道,帮助定期重新训练模型并执行模型诊断,以避免任何模型停机时间

  • 当它们在灾难性失败并造成公共损害时,由认证的 AI 审计师进行诊断

新法规可能会催生新的职业,例如 AI 审计师和模型诊断工程师。但它们也将支持 MLOps 工程师和 ML 自动化工具。

具有内置解释的无缝机器学习自动化

在未来,我们不会编写 ML 管道;它将主要是一个拖放功能,带有仪表板提供各种指标。它将主要实现自动化。自动化不应令人惊讶,因为一些现有的库执行自动特征选择模型训练。一些增强可解释性的程序可能自动执行,但大多数程序将需要人工判断。然而,解释应该贯穿整个流程,就像大多数飞机主要由自己飞行,但飞机上仍然有仪器提醒飞行员问题一样;价值在于向机器学习实践者提供每一步的潜在问题和改进信息。它是否找到了推荐用于单调约束的特征?它是否发现了可能需要调整的一些不平衡?它是否发现了可能需要一些纠正的数据异常?向实践者展示他们需要看到的内容,以便做出明智的决定,并让他们做出决定。

与 MLOps 工程师更紧密的集成

通过一键操作训练、验证和部署的、可认证的稳健模型需要的不只是云基础设施,还需要工具、配置以及接受过 MLOps 培训的人员来监控它们并在定期间隔进行维护。

摘要

可解释机器学习是一个广泛的话题,本书只覆盖了其最重要领域的一些方面,在诊断和治疗两个层面上进行了探讨。实践者可以在机器学习管道的任何地方利用工具包提供的工具。然而,选择何时以及如何应用它们取决于实践者。

最重要的是要熟悉工具。不使用可解释的机器学习工具包就像驾驶一架几乎没有仪器或完全没有仪器的飞机。就像驾驶飞机在不同的天气条件下运行一样,机器学习模型在不同的数据条件下运行,要成为一名熟练的飞行员或机器学习工程师,我们不能过于自信,而应该用我们的仪器来验证或排除假设。就像航空业花了几十年才成为最安全的交通方式一样,人工智能也需要几十年才能成为最安全的决策方式。这将需要全球村子的共同努力,但这将是一次激动人心的旅程!记住,预测未来的最好方法就是创造它

进一步阅读

在 Discord 上了解更多

要加入这本书的 Discord 社区——在那里你可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:

packt.link/inml