机器学习口袋参考-三-

203 阅读39分钟

机器学习口袋参考(三)

原文:annas-archive.org/md5/dd771df8c19ec4613e3638bc1f862b92

译者:飞龙

协议:CC BY-NC-SA 4.0

第十一章:模型选择

本章将讨论优化超参数,并且探讨模型是否需要更多数据以提升表现。

验证曲线

创建验证曲线是确定超参数合适值的一种方法。验证曲线是一个图表,展示模型性能如何随超参数数值变化而变化(见图 11-1)。该图表同时展示训练数据和验证数据。验证分数可以让我们推测模型对未见数据的反应。通常,我们会选择最大化验证分数的超参数。

在下面的示例中,我们将使用 Yellowbrick 来查看改变max_depth超参数值是否会改变随机森林模型的性能。您可以提供一个scoring参数设置为 scikit-learn 模型度量(分类默认为'accuracy'):

提示

使用n_jobs参数来充分利用 CPU,并且加快运行速度。如果将其设置为-1,将会使用所有的 CPU。

>>> from yellowbrick.model_selection import (
...     ValidationCurve,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> vc_viz = ValidationCurve(
...     RandomForestClassifier(n_estimators=100),
...     param_name="max_depth",
...     param_range=np.arange(1, 11),
...     cv=10,
...     n_jobs=-1,
... )
>>> vc_viz.fit(X, y)
>>> vc_viz.poof()
>>> fig.savefig("images/mlpr_1101.png", dpi=300)

验证曲线报告。

图 11-1. 验证曲线报告。

ValidationCurve类支持scoring参数。该参数可以是自定义函数或以下选项之一,具体取决于任务。

分类scoring选项包括:'accuracy', 'average_precision', 'f1', 'f1_micro', 'f1_macro', 'f1_weighted', 'f1_samples', 'neg_log_loss', 'precision', 'recall''roc_auc'

聚类scoring选项:'adjusted_mutual_info_score', 'adjusted_rand_score', 'completeness_score', 'fowlkes_mallows_score', 'homogeneity_score', 'mutual_info_score', 'normalized_mutual_info_score''v_measure_score'

回归scoring选项:'explained_variance', 'neg_mean_absolute_error', 'neg_mean_squared_error', 'neg_mean_squared_log_error', 'neg_median_absolute_error''r2'

学习曲线

为了为您的项目选择最佳模型,您需要多少数据?学习曲线可以帮助我们回答这个问题。该曲线绘制了随着样本增加创建模型时的训练和交叉验证分数。例如,如果交叉验证分数继续上升,那可能表明更多数据将有助于模型表现更好。

下面的可视化展示了一个验证曲线,并且帮助我们探索模型的偏差和方差(见图 11-2)。如果训练分数有变化(一个大的阴影区域),则模型存在偏差误差,过于简单(欠拟合)。如果交叉验证分数有变化,则模型存在方差误差,过于复杂(过拟合)。另一个过拟合的指示是验证集的性能远远不如训练集。

这里是使用 Yellowbrick 创建学习曲线的示例:

>>> from yellowbrick.model_selection import (
...     LearningCurve,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> lc3_viz = LearningCurve(
...     RandomForestClassifier(n_estimators=100),
...     cv=10,
... )
>>> lc3_viz.fit(X, y)
>>> lc3_viz.poof()
>>> fig.savefig("images/mlpr_1102.png", dpi=300)

学习曲线图。验证分数的平稳期表明添加更多数据不会改善该模型。

图 11-2. 学习曲线图。验证分数的平稳期表明添加更多数据不会改善该模型。

这种可视化方法也可以通过改变评分选项用于回归或聚类。

第十二章:指标和分类评估

在本章中我们将涵盖以下指标和评估工具:混淆矩阵、各种指标、分类报告和一些可视化。

这将作为一个预测泰坦尼克号生存的决策树模型进行评估。

混淆矩阵

混淆矩阵有助于理解分类器的表现。

二元分类器可以有四种分类结果:真正例(TP)、真反例(TN)、假正例(FP)和假反例(FN)。前两者是正确分类。

这里是一个常见的例子,用于记忆其他结果。假设正表示怀孕,负表示未怀孕,假阳性就像声称一个男性怀孕。假阴性是声称一个怀孕的女性不是(当她显然有显示)(见图 12-1)。这些错误称为 类型 1类型 2 错误,分别参见表 12-1。

记住这些的另一种方法是,P(表示假阳性)中有一条直线(类型 1 错误),而 N(表示假阴性)中有两条竖线。

分类错误。

图 12-1. 分类错误。

表 12-1. 从混淆矩阵中得出的二元分类结果

实际预测为负预测为正
实际负样本真阴性假阳性(类型 1)
实际正样本假阴性(类型 2)真正例

这里是计算分类结果的 pandas 代码。注释显示了结果。我们将使用这些变量来计算其他指标:

>>> y_predict = dt.predict(X_test)
>>> tp = (
...     (y_test == 1) & (y_test == y_predict)
... ).sum()  # 123
>>> tn = (
...     (y_test == 0) & (y_test == y_predict)
... ).sum()  # 199
>>> fp = (
...     (y_test == 0) & (y_test != y_predict)
... ).sum()  # 25
>>> fn = (
...     (y_test == 1) & (y_test != y_predict)
... ).sum()  # 46

良好表现的分类器理想情况下在真实对角线上有高计数。我们可以使用 sklearn 的 confusion_matrix 函数创建一个 DataFrame:

>>> from sklearn.metrics import confusion_matrix
>>> y_predict = dt.predict(X_test)
>>> pd.DataFrame(
...     confusion_matrix(y_test, y_predict),
...     columns=[
...         "Predict died",
...         "Predict Survive",
...     ],
...     index=["True Death", "True Survive"],
... )
 Predict died  Predict Survive
True Death             199               25
True Survive            46              123

Yellowbrick 有一个混淆矩阵的绘图(见图 12-2):

>>> import matplotlib.pyplot as plt
>>> from yellowbrick.classifier import (
...     ConfusionMatrix,
... )
>>> mapping = {0: "died", 1: "survived"}
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> cm_viz = ConfusionMatrix(
...     dt,
...     classes=["died", "survived"],
...     label_encoder=mapping,
... )
>>> cm_viz.score(X_test, y_test)
>>> cm_viz.poof()
>>> fig.savefig("images/mlpr_1202.png", dpi=300)

混淆矩阵。左上角和右下角是正确分类。左下角是假阴性。右上角是假阳性。

图 12-2. 混淆矩阵。左上角和右下角是正确分类。左下角是假阴性。右上角是假阳性。

指标

sklearn.metrics 模块实现了许多常见的分类度量,包括:

'accuracy'

正确预测的百分比

'average_precision'

精确率-召回率曲线总结

'f1'

精度和召回率的调和平均数

'neg_log_loss'

逻辑或交叉熵损失(模型必须支持 predict_proba

'precision'

能够仅找到相关样本(不将负样本误标为正样本)

'recall'

能够找到所有正样本

'roc_auc'

ROC 曲线下的面积

这些字符串可以作为网格搜索中的 scoring 参数使用,或者你可以使用 sklearn.metrics 模块中具有相同名称但以 _score 结尾的函数。详见下面的示例。

注意

'f1', 'precision''recall' 都支持多类分类器的以下后缀:

'_micro'

全局加权平均指标

'_macro'

指标的未加权平均

'_weighted'

多类加权平均指标

'_samples'

每个样本的指标

准确率

准确率是正确分类的百分比:

>>> (tp + tn) / (tp + tn + fp + fn)
0.8142493638676844

什么是良好的准确率?这取决于情况。如果我在预测欺诈(通常是罕见事件,比如一万分之一),我可以通过始终预测不是欺诈来获得非常高的准确率。但这种模型并不是很有用。查看其他指标以及预测假阳性和假阴性的成本可以帮助我们确定模型是否合适。

我们可以使用 sklearn 来计算它:

>>> from sklearn.metrics import accuracy_score
>>> y_predict = dt.predict(X_test)
>>> accuracy_score(y_test, y_predict)
0.8142493638676844

召回率

召回率(也称为灵敏度)是正确分类的正值的百分比。(返回多少相关的结果?)

>>> tp / (tp + fn)
0.7159763313609467

>>> from sklearn.metrics import recall_score
>>> y_predict = dt.predict(X_test)
>>> recall_score(y_test, y_predict)
0.7159763313609467

精度

精度是正确预测的正预测的百分比(TP 除以(TP + FP))。(结果有多相关?)

>>> tp / (tp + fp)
0.8287671232876712

>>> from sklearn.metrics import precision_score
>>> y_predict = dt.predict(X_test)
>>> precision_score(y_test, y_predict)
0.8287671232876712

F1

F1 是召回率和精度的调和平均值:

>>> pre = tp / (tp + fp)
>>> rec = tp / (tp + fn)
>>> (2 * pre * rec) / (pre + rec)
0.7682539682539683

>>> from sklearn.metrics import f1_score
>>> y_predict = dt.predict(X_test)
>>> f1_score(y_test, y_predict)
0.7682539682539683

分类报告

Yellowbrick 有一个分类报告,显示正负值的精度、召回率和 F1 分数(见图 12-3)。颜色标记,红色越深(接近 1),得分越好:

>>> import matplotlib.pyplot as plt
>>> from yellowbrick.classifier import (
...     ClassificationReport,
... )
>>> fig, ax = plt.subplots(figsize=(6, 3))
>>> cm_viz = ClassificationReport(
...     dt,
...     classes=["died", "survived"],
...     label_encoder=mapping,
... )
>>> cm_viz.score(X_test, y_test)
>>> cm_viz.poof()
>>> fig.savefig("images/mlpr_1203.png", dpi=300)

分类报告。

图 12-3. 分类报告。

ROC

ROC 曲线说明分类器在真正例率(召回率/灵敏度)随假正例率(倒置特异性)变化时的表现(见图 12-4)。

一个经验法则是图形应该朝向左上角凸出。一个位于另一个图形左侧且上方的图形表示性能更好。这个图中的对角线表示随机猜测分类器的行为。通过计算 AUC,您可以得到一个评估性能的度量:

>>> from sklearn.metrics import roc_auc_score
>>> y_predict = dt.predict(X_test)
>>> roc_auc_score(y_test, y_predict)
0.8706304346418559

Yellowbrick 可以为我们绘制这个图:

>>> from yellowbrick.classifier import ROCAUC
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> roc_viz = ROCAUC(dt)
>>> roc_viz.score(X_test, y_test)
0.8706304346418559
>>> roc_viz.poof()
>>> fig.savefig("images/mlpr_1204.png", dpi=300)

ROC 曲线。

图 12-4. ROC 曲线。

精度-召回曲线

ROC 曲线对于不平衡类可能过于乐观。评估分类器的另一种选择是使用精度-召回曲线(见图 12-5)。分类是在找到所有需要的内容(召回率)和限制垃圾结果(精度)之间进行权衡。这通常是一个权衡。随着召回率的提高,精度通常会下降,反之亦然。

>>> from sklearn.metrics import (
...     average_precision_score,
... )
>>> y_predict = dt.predict(X_test)
>>> average_precision_score(y_test, y_predict)
0.7155150490642249

这是一个 Yellowbrick 精度-召回曲线:

>>> from yellowbrick.classifier import (
...     PrecisionRecallCurve,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> viz = PrecisionRecallCurve(
...     DecisionTreeClassifier(max_depth=3)
... )
>>> viz.fit(X_train, y_train)
>>> print(viz.score(X_test, y_test))
>>> viz.poof()
>>> fig.savefig("images/mlpr_1205.png", dpi=300)

精度-召回曲线。

图 12-5. 精度-召回曲线。

累积增益图

累积增益图可用于评估二元分类器。它将真正率(灵敏度)模型化为正预测的分数率。该图背后的直觉是按预测概率对所有分类进行排序。理想情况下,应有一个清晰的分界线,将正样本与负样本分开。如果前 10%的预测具有 30%的正样本,则应绘制从(0,0)到(.1,.3)的点。继续这个过程直至所有样本(见图 12-6)。

这通常用于确定客户反应。累积增益曲线沿 x 轴绘制支持或预测的正率。我们的图表将其标记为“样本百分比”。它沿 y 轴绘制灵敏度或真正率。在我们的图中标记为“增益”。

如果您想联系 90%会响应的客户(灵敏度),您可以从 y 轴上的 0.9 追溯到右侧,直到碰到该曲线。此时的 x 轴指示您需要联系多少总客户(支持),以达到 90%。

在这种情况下,我们不联系会对调查做出反应的客户,而是预测泰坦尼克号上的生存。如果按照我们的模型将泰坦尼克号的所有乘客排序,根据其生存可能性,如果你拿前 65%的乘客,你将得到 90%的幸存者。如果有每次联系的相关成本和每次响应的收入,您可以计算出最佳数量是多少。

一般来说,处于左上方的模型比另一个模型更好。最佳模型是上升到顶部的线(如果样本的 10%为正,它将在(.1, 1)处达到)。然后直接到右侧。如果图表在基线以下,我们最好随机分配标签以使用我们的模型。

scikit-plot 库可以创建一个累积增益图:

>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> y_probas = dt.predict_proba(X_test)
>>> scikitplot.metrics.plot_cumulative_gain(
...     y_test, y_probas, ax=ax
... )
>>> fig.savefig(
...     "images/mlpr_1206.png",
...     dpi=300,
...     bbox_inches="tight",
... )

累积增益图。如果我们根据我们的模型对泰坦尼克号上的人进行排序,查看其中的 20%,我们将获得 40%的幸存者。

图 12-6. 累积增益图。如果我们根据我们的模型对泰坦尼克号上的人进行排序,查看其中的 20%,我们将获得 40%的幸存者。

抬升曲线

抬升曲线是查看累积增益图中信息的另一种方式。抬升是我们比基线模型做得更好的程度。在我们的图中,我们可以看到,如果按照生存概率对泰坦尼克号乘客进行排序并取前 20%的人,我们的提升将约为基线模型的 2.2 倍(增益除以样本百分比)好(见图 12-7)。 (我们将获得 2.2 倍的幸存者。)

scikit-plot 库可以创建一个抬升曲线:

>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> y_probas = dt.predict_proba(X_test)
>>> scikitplot.metrics.plot_lift_curve(
...     y_test, y_probas, ax=ax
... )
>>> fig.savefig(
...     "images/mlpr_1207.png",
...     dpi=300,
...     bbox_inches="tight",
... )

抬升曲线。

图 12-7. 抬升曲线。

类别平衡

Yellowbrick 提供了一个简单的柱状图,用于查看类别大小。当相对类别大小不同时,准确率不是一个良好的评估指标(见图 12-8)。在将数据分成训练集和测试集时,请使用分层抽样,以保持类别的相对比例(当你将 stratify 参数设置为标签时,test_train_split 函数会执行此操作)。

>>> from yellowbrick.classifier import ClassBalance
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> cb_viz = ClassBalance(
...     labels=["Died", "Survived"]
... )
>>> cb_viz.fit(y_test)
>>> cb_viz.poof()
>>> fig.savefig("images/mlpr_1208.png", dpi=300)

轻微的类别不平衡。

图 12-8. 轻微的类别不平衡。

类预测错误

Yellowbrick 的类预测错误图是一个柱状图,用于可视化混淆矩阵(参见图 12-9):

>>> from yellowbrick.classifier import (
...     ClassPredictionError,
... )
>>> fig, ax = plt.subplots(figsize=(6, 3))
>>> cpe_viz = ClassPredictionError(
...     dt, classes=["died", "survived"]
... )
>>> cpe_viz.score(X_test, y_test)
>>> cpe_viz.poof()
>>> fig.savefig("images/mlpr_1209.png", dpi=300)

类预测错误。在左侧条的顶部是死亡者,但我们预测他们幸存(假阳性)。在右侧条的底部是幸存者,但模型预测为死亡(假阴性)。

图 12-9. 类预测错误。在左侧条的顶部是死亡者,但我们预测他们幸存(假阳性)。在右侧条的底部是幸存者,但模型预测为死亡(假阴性)。

判别阈值

大多数预测概率的二元分类器具有 50%的判别阈值。如果预测概率高于 50%,分类器会分配正标签。图 12-10 在 0 到 100 之间移动该阈值,并显示对精确度、召回率、f1 和队列率的影响。

这个图表对于查看精确度和召回率之间的权衡是有用的。假设我们正在寻找欺诈(并将欺诈视为正分类)。为了获得高召回率(捕捉到所有的欺诈),我们可以将所有东西都分类为欺诈。但在银行情境下,这不会盈利,而且需要大量的工作人员。为了获得高精确度(只有在确实是欺诈时才捕捉到欺诈),我们可以有一个只对极端欺诈案例触发的模型。但这会错过许多不那么明显的欺诈行为。这里存在一种权衡。

队列率是高于阈值的预测百分比。如果您正在处理欺诈案件,可以将其视为需要审查的案例百分比。

如果您有正、负和错误计算的成本,您可以确定您可以接受的阈值。

以下图表有助于查看在与队列率结合时,哪个判别阈值能够最大化 f1 得分或调整精确度或召回率至可接受水平。

Yellowbrick 提供了这个可视化工具。默认情况下,这个可视化工具对数据进行洗牌,并运行 50 次试验,其中分离出 10%作为验证集:

>>> from yellowbrick.classifier import (
...     DiscriminationThreshold,
... )
>>> fig, ax = plt.subplots(figsize=(6, 5))
>>> dt_viz = DiscriminationThreshold(dt)
>>> dt_viz.fit(X, y)
>>> dt_viz.poof()
>>> fig.savefig("images/mlpr_1210.png", dpi=300)

判别阈值。

图 12-10. 判别阈值。

第十三章:解释模型

预测模型具有不同的属性。有些设计用于处理线性数据。其他可以适应更复杂的输入。有些模型很容易解释,而其他模型则像黑盒子,不提供有关如何进行预测的深入见解。

在本章中,我们将探讨解释不同的模型。我们将查看一些使用泰坦尼克号数据的示例。

>>> dt = DecisionTreeClassifier(
...     random_state=42, max_depth=3
... )
>>> dt.fit(X_train, y_train)

回归系数

截距和回归系数解释了预期值以及特征如何影响预测。正系数表明随着特征值的增加,预测也会增加。

特征重要性

scikit-learn 库中的基于树的模型包括 .fea⁠ture_``importances_ 属性,用于检查数据集的特征如何影响模型。我们可以检查或绘制它们。

LIME

LIME 用于帮助解释黑盒模型。它执行局部解释而不是整体解释。它将帮助解释单个样本。

对于给定的数据点或样本,LIME 指示了确定结果的重要特征。它通过扰动所讨论的样本并将线性模型拟合到它来实现这一点。线性模型近似于样本附近的模型(参见 Figure 13-1)。

这里有一个例子,解释了训练数据中最后一个样本(我们的决策树预测会存活):

>>> from lime import lime_tabular
>>> explainer = lime_tabular.LimeTabularExplainer(
...     X_train.values,
...     feature_names=X.columns,
...     class_names=["died", "survived"],
... )
>>> exp = explainer.explain_instance(
...     X_train.iloc[-1].values, dt.predict_proba
... )

LIME 不喜欢使用 DataFrame 作为输入。请注意,我们使用 .values 将数据转换为 numpy 数组。

提示

如果您在 Jupyter 中进行此操作,请使用以下代码进行后续操作:

exp.show_in_notebook()

这将呈现解释的 HTML 版本。

如果我们想导出解释(或者不使用 Jupyter),我们可以创建一个 matplotlib 图形:

>>> fig = exp.as_pyplot_figure()
>>> fig.tight_layout()
>>> fig.savefig("images/mlpr_1301.png")

LIME explanation for the Titanic dataset. Features for the sample push the prediction toward the right (survival) or left (deceased).

图 13-1. 泰坦尼克号数据集的 LIME 解释。样本的特征将预测推向右侧(存活)或左侧(已故)。

尝试一下,注意到如果更改性别,结果会受到影响。下面我们获取训练数据中的倒数第二行。该行的预测为 48% 的死亡和 52% 的生还。如果我们更改性别,我们发现预测向 88% 的死亡方向移动:

>>> data = X_train.iloc[-2].values.copy()
>>> dt.predict_proba(
...     [data]
... )  # predicting that a woman lives
[[0.48062016 0.51937984]]
>>> data[5] = 1  # change to male
>>> dt.predict_proba([data])
array([[0.87954545, 0.12045455]])
注意

.predict_proba 方法返回每个标签的概率。

树解释

对于 sklearn 的基于树的模型(决策树、随机森林和额外树模型),您可以使用 treeinterpreter package。这将计算每个特征的偏差和贡献。偏差是训练集的均值。

每个贡献列表说明了它对每个标签的贡献。 (偏差加上贡献应该等于预测。)由于这是二分类问题,只有两种。我们看到 sex_male 是最重要的,其次是 age 和 fare:

>>> from treeinterpreter import (
...     treeinterpreter as ti,
... )
>>> instances = X.iloc[:2]
>>> prediction, bias, contribs = ti.predict(
...     rf5, instances
... )
>>> i = 0
>>> print("Instance", i)
>>> print("Prediction", prediction[i])
>>> print("Bias (trainset mean)", bias[i])
>>> print("Feature contributions:")
>>> for c, feature in zip(
...     contribs[i], instances.columns
... ):
...     print("  {} {}".format(feature, c))
Instance 0
Prediction [0.98571429 0.01428571]
Bias (trainset mean) [0.63984716 0.36015284]
Feature contributions:
 pclass [ 0.03588478 -0.03588478]
 age [ 0.08569306 -0.08569306]
 sibsp [ 0.01024538 -0.01024538]
 parch [ 0.0100742 -0.0100742]
 fare [ 0.06850243 -0.06850243]
 sex_male [ 0.12000073 -0.12000073]
 embarked_Q [ 0.0026364 -0.0026364]
 embarked_S [ 0.01283015 -0.01283015]
注意

此示例用于分类,但也支持回归。

部分依赖图

使用树中的特征重要性,我们知道某个特征影响了结果,但我们不知道随着特征值的变化,影响如何变化。部分依赖图允许我们可视化单个特征变化与结果之间的关系。我们将使用pdpbox来可视化年龄如何影响生存(参见图 13-2)。

此示例使用随机森林模型:

>>> rf5 = ensemble.RandomForestClassifier(
...     **{
...         "max_features": "auto",
...         "min_samples_leaf": 0.1,
...         "n_estimators": 200,
...         "random_state": 42,
...     }
... )
>>> rf5.fit(X_train, y_train)
>>> from pdpbox import pdp
>>> feat_name = "age"
>>> p = pdp.pdp_isolate(
...     rf5, X, X.columns, feat_name
... )
>>> fig, _ = pdp.pdp_plot(
...     p, feat_name, plot_lines=True
... )
>>> fig.savefig("images/mlpr_1302.png", dpi=300)

显示随着年龄变化目标发生变化的部分依赖图。

图 13-2. 显示随着年龄变化目标发生变化的部分依赖图。

我们还可以可视化两个特征之间的交互作用(参见图 13-3):

>>> features = ["fare", "sex_male"]
>>> p = pdp.pdp_interact(
...     rf5, X, X.columns, features
... )
>>> fig, _ = pdp.pdp_interact_plot(p, features)
>>> fig.savefig("images/mlpr_1303.png", dpi=300)

具有两个特征的部分依赖图。随着票价上涨和性别从男性变为女性,生存率上升。

图 13-3. 具有两个特征的部分依赖图。随着票价上涨和性别从男性变为女性,生存率上升。
注意

部分依赖图固定了样本中的特征值,然后对结果进行平均。(小心异常值和平均值。)此外,此图假设特征是独立的。(并非总是如此;例如,保持萼片的宽度稳定可能会影响其高度。)pdpbox 库还打印出单个条件期望,以更好地可视化这些关系。

替代模型

如果您有一个不可解释的模型(如 SVM 或神经网络),您可以为该模型拟合一个可解释的模型(决策树)。使用替代模型,您可以检查特征的重要性。

在这里,我们创建了一个支持向量分类器(SVC),但是训练了一个决策树(没有深度限制,以过度拟合并捕获该模型中发生的情况)来解释它:

>>> from sklearn import svm
>>> sv = svm.SVC()
>>> sv.fit(X_train, y_train)
>>> sur_dt = tree.DecisionTreeClassifier()
>>> sur_dt.fit(X_test, sv.predict(X_test))
>>> for col, val in sorted(
...     zip(
...         X_test.columns,
...         sur_dt.feature_importances_,
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:7]:
...     print(f"{col:10}{val:10.3f}")
sex_male       0.723
pclass         0.076
sibsp          0.061
age            0.056
embarked_S     0.050
fare           0.028
parch          0.005

夏普利

SHapley Additive exPlanations,(SHAP) 包可以可视化任何模型的特征贡献。这是一个非常好的包,因为它不仅适用于大多数模型,还可以解释个别预测和全局特征贡献。

SHAP 适用于分类和回归。它生成“SHAP”值。对于分类模型,SHAP 值总和为二元分类的对数几率。对于回归,SHAP 值总和为目标预测。

此库需要 Jupyter(JavaScript)以实现部分图的交互性(一些可以使用 matplotlib 渲染静态图像)。这是一个例子,用于样本 20,预测为死亡:

>>> rf5.predict_proba(X_test.iloc[[20]])
array([[0.59223553, 0.40776447]])

在样本 20 的力图中,您可以看到“基值”。这是一个被预测为死亡的女性(参见图 13-4)。我们将使用生存指数(1),因为我们希望图的右侧是生存。特征将此推向右侧或左侧。特征越大,影响越大。在这种情况下,低票价和第三类推向死亡(输出值低于 .5):

>>> import shap
>>> s = shap.TreeExplainer(rf5)
>>> shap_vals = s.shap_values(X_test)
>>> target_idx = 1
>>> shap.force_plot(
...     s.expected_value[target_idx],
...     shap_vals[target_idx][20, :],
...     feature_names=X_test.columns,
... )

样本 20 的 Shapley 特征贡献。此图显示基值和推向死亡的特征。

图 13-4. 样本 20 的 Shapley 特征贡献。此图显示基值和推向死亡的特征。

您还可以可视化整个数据集的解释(将其旋转 90 度并沿 x 轴绘制)(见图 13-5):

>>> shap.force_plot(
...     s.expected_value[1],
...     shap_vals[1],
...     feature_names=X_test.columns,
... )

数据集的 Shapley 特征贡献。

图 13-5. 数据集的 Shapley 特征贡献。

SHAP 库还可以生成依赖图。以下图(见图 13-6)可视化了年龄和 SHAP 值之间的关系(它根据 pclass 进行了着色,这是 SHAP 自动选择的;指定一个列名称作为 interaction_index 参数以选择您自己的):

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> res = shap.dependence_plot(
...     "age",
...     shap_vals[target_idx],
...     X_test,
...     feature_names=X_test.columns,
...     alpha=0.7,
... )
>>> fig.savefig(
...     "images/mlpr_1306.png",
...     bbox_inches="tight",
...     dpi=300,
... )

年龄的 Shapley 依赖图。年轻人和老年人的生存率较高。随着年龄增长,较低的 pclass 有更多的生存机会。

图 13-6. 年龄的 Shapley 依赖图。年轻人和老年人的生存率较高。随着年龄增长,较低的 pclass 有更多的生存机会。
提示

您可能会得到一个具有垂直线的依赖图。如果查看有序分类特征,则将 x_jitter 参数设置为 1 是有用的。

此外,我们可以总结所有特征。这是一个非常强大的图表,可以理解。它显示了全局影响,但也显示了个别影响。特征按重要性排名。最重要的特征位于顶部。

同时,特征根据它们的值进行了着色。我们可以看到低sex_male得分(女性)对生存有很强的推动作用,而高得分对死亡的推动作用较弱。年龄特征有点难以解释。这是因为年轻和年老的值对生存有推动作用,而中间值则对死亡有推动作用。

当您将摘要图与依赖图结合起来时,您可以深入了解模型行为(见图 13-7):

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> shap.summary_plot(shap_vals[0], X_test)
>>> fig.savefig("images/mlpr_1307.png", dpi=300)

Shapley 摘要图显示最重要的特征在顶部。着色显示特征值对目标的影响。

图 13-7. Shapley 摘要图显示最重要的特征在顶部。着色显示特征值对目标的影响。

第十四章:回归

回归是一种监督机器学习过程。它类似于分类,但不是预测标签,而是预测连续值。如果您要预测一个数字,那么使用回归。

结果表明,sklearn 支持许多相同的分类模型用于回归问题。实际上,API 是相同的,调用.fit.score.predict。对于下一代增强库 XGBoost 和 LightGBM 也是如此。

尽管分类模型和超参数有相似之处,但回归的评估指标不同。本章将回顾许多种回归模型。我们将使用Boston 房屋数据集进行探索。

在这里,我们加载数据,创建一个用于训练和测试的拆分版本,并创建另一个具有标准化数据的拆分版本:

>>> import pandas as pd
>>> from sklearn.datasets import load_boston
>>> from sklearn import (
...     model_selection,
...     preprocessing,
... )
>>> b = load_boston()
>>> bos_X = pd.DataFrame(
...     b.data, columns=b.feature_names
... )
>>> bos_y = b.target

>>> bos_X_train, bos_X_test, bos_y_train, bos_y_test = model_selection.train_test_split(
...     bos_X,
...     bos_y,
...     test_size=0.3,
...     random_state=42,
... )

>>> bos_sX = preprocessing.StandardScaler().fit_transform(
...     bos_X
... )
>>> bos_sX_train, bos_sX_test, bos_sy_train, bos_sy_test = model_selection.train_test_split(
...     bos_sX,
...     bos_y,
...     test_size=0.3,
...     random_state=42,
... )

这里是从数据集中提取的住房数据集特征的描述:

犯罪率

按城镇计算的人均犯罪率

ZN

住宅土地超过 25000 平方英尺的比例

INDUS

每个城镇非零售业务用地比例

CHAS

查尔斯河虚拟变量(如果地区与河流接壤则为 1;否则为 0)

NOX

一氧化氮浓度(每千万分之一)

房屋的平均房间数

每个住宅的平均房间数

年龄

业主自住单位建于 1940 年之前的比例

DIS

到波士顿五个就业中心的加权距离

RAD

径向高速公路的可达性指数

每 10000 美元的全额财产税率

PTRATIO

按城镇计算的师生比

B

1000(Bk - 0.63)²,其中 Bk 是城镇中黑人的比例(此数据集来自 1978 年)

LSTAT

人口的较低地位百分比

MEDV

以每 1000 美元递增的业主自住房屋的中位数价值

基线模型

基线回归模型将为我们提供一个与其他模型进行比较的标准。在 sklearn 中,.score方法的默认结果是确定系数(r²或 R²)。该数值解释了预测捕捉的输入数据变化的百分比。通常在 0 到 1 之间,但在极差模型情况下可能为负数。

DummyRegressor的默认策略是预测训练集的平均值。我们可以看到这个模型表现不佳:

>>> from sklearn.dummy import DummyRegressor
>>> dr = DummyRegressor()
>>> dr.fit(bos_X_train, bos_y_train)
>>> dr.score(bos_X_test, bos_y_test)
-0.03469753992352409

线性回归

简单线性回归教授数学和初级统计课程。它试图拟合形式为 y = mx + b 的公式,同时最小化误差的平方。求解后,我们有一个截距和系数。截距提供了一个预测的基础值,通过添加系数和输入的乘积进行修改。

这种形式可以推广到更高的维度。在这种情况下,每个特征都有一个系数。系数的绝对值越大,该特征对目标的影响越大。

该模型假设预测是输入的线性组合。对于某些数据集,这可能不够灵活。可以通过转换特征(sklearn 的preprocessing.PolynomialFeatures转换器可以创建特征的多项式组合)来增加复杂性。如果这导致过拟合,可以使用岭回归和 Lasso 回归来正则化估计器。

该模型也容易受到异方差性的影响。这意味着随着输入值的变化,预测误差(或残差)通常也会变化。如果您绘制输入与残差的图表,您将看到一个扇形或锥形。稍后我们将看到这方面的示例。

另一个要注意的问题是多重共线性。如果列之间存在高相关性,可能会影响系数的解释。这通常不会影响模型,只影响系数的含义。

线性回归模型具有以下属性:

运行效率

使用n_jobs来加快性能。

预处理数据

在训练模型之前对数据进行标准化。

防止过拟合

您可以通过不使用或添加多项式特征来简化模型。

解释结果

可以解释结果作为特征贡献的权重,但是假设特征是正态分布且独立的。您可能需要移除共线特征以提高可解释性。R²将告诉您模型解释结果中总方差的百分比。

这里是使用默认数据的样本运行:

>>> from sklearn.linear_model import (
...     LinearRegression,
... )
>>> lr = LinearRegression()
>>> lr.fit(bos_X_train, bos_y_train)
LinearRegression(copy_X=True, fit_intercept=True,
 n_jobs=1, normalize=False)
>>> lr.score(bos_X_test, bos_y_test)
0.7109203586326287
>>> lr.coef_
array([-1.32774155e-01,  3.57812335e-02,
 4.99454423e-02,  3.12127706e+00,
 -1.54698463e+01,  4.04872721e+00,
 -1.07515901e-02, -1.38699758e+00,
 2.42353741e-01, -8.69095363e-03,
 -9.11917342e-01,  1.19435253e-02,
 -5.48080157e-01])

实例参数:

n_jobs=None

使用的 CPU 数目。-1表示全部。

拟合后的属性:

coef_

线性回归系数

intercept_

线性模型的截距

.intercept_值是预期的均值。您可以看到数据缩放如何影响系数。系数的符号说明特征与目标之间的关系方向。正号表示特征增加时,标签也增加。负号表示特征增加时,标签减少。系数的绝对值越大,其影响越大:

>>> lr2 = LinearRegression()
>>> lr2.fit(bos_sX_train, bos_sy_train)
LinearRegression(copy_X=True, fit_intercept=True,
 n_jobs=1, normalize=False)
>>> lr2.score(bos_sX_test, bos_sy_test)
0.7109203586326278
>>> lr2.intercept_
22.50945471291039
>>> lr2.coef_
array([-1.14030209,  0.83368112,  0.34230461,
 0.792002, -1.7908376, 2.84189278, -0.30234582,
 -2.91772744,  2.10815064, -1.46330017,
 -1.97229956,  1.08930453, -3.91000474])

您可以使用 Yellowbrick 来可视化系数(参见图 14-1)。因为缩放后的波士顿数据是一个 numpy 数组而不是 pandas DataFrame,如果我们想使用列名,我们需要传递labels参数:

>>> from yellowbrick.features import (
...     FeatureImportances,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> fi_viz = FeatureImportances(
...     lr2, labels=bos_X.columns
... )
>>> fi_viz.fit(bos_sX, bos_sy)
>>> fi_viz.poof()
>>> fig.savefig(
...     "images/mlpr_1401.png",
...     bbox_inches="tight",
...     dpi=300,
... )

特征重要性。这表明 RM(房间数量)增加价格,年龄并不重要,而 LSTAT(低收入人口比例)降低价格。

图 14-1. 特征重要性。这表明 RM(房间数量)增加价格,年龄并不重要,而 LSTAT(低收入人口比例)降低价格。

SVM

支持向量机也可以执行回归任务。

SVM 具有以下属性:

运行效率

scikit-learn 实现的时间复杂度为 O(n⁴),因此在大规模数据集上很难扩展。使用线性核或 LinearSVR 模型可以提高运行时性能,可能会牺牲一些准确性。增加 cache_size 参数可以将复杂度降低到 O(n³)。

预处理数据

该算法不是尺度不变的,因此强烈建议对数据进行标准化。

防止过拟合

C(惩罚参数)控制正则化。较小的值允许较小的超平面间隔。较高的 gamma 值会导致过拟合训练数据。LinearSVR 模型支持 losspenalty 参数进行正则化。epsilon 参数可以提高(使用 0 可能会导致过拟合)。

解释结果

检查 .support_vectors_,尽管这些很难解释。对于线性核,您可以检查 .coef_

下面是使用库的示例:

>>> from sklearn.svm import SVR
>>> svr = SVR()
>>> svr.fit(bos_sX_train, bos_sy_train)
SVR(C=1.0, cache_size=200, coef0=0.0, degree=3,
 epsilon=0.1, gamma='auto', kernel='rbf',
 max_iter=-1, shrinking=True, tol=0.001,
 verbose=False)

>>> svr.score(bos_sX_test, bos_sy_test)
0.6555356362002485

实例参数:

C=1.0

惩罚参数。值越小,决策边界越紧(更容易过拟合)。

cache_size=200

缓存大小(MB)。增加此值可以改善大数据集上的训练时间。

coef0=0.0

多项式和 sigmoid 核的独立项。

epsilon=0.1

定义容错边距,不会对错误给予惩罚。对于较大的数据集,应该更小。

degree=3

多项式核的度。

gamma='auto'

核系数。可以是数字,'scale'(0.22 版本的默认值,1 /(特征数 * X.std())),或 'auto'(默认值,1 / 特征数)。较低的值会导致过拟合训练数据。

kernel='rbf'

核类型:'linear''poly''rbf'(默认)、'sigmoid''precomputed'或函数。

max_iter=-1

求解器的最大迭代次数。-1 表示无限制。

probability=False

启用概率估计。训练过程会变慢。

random_state=None

随机种子。

shrinking=True

使用缩减启发式。

tol=0.001

停止容差。

verbose=False

冗余性。

拟合后的属性:

support_

支持向量索引

support_vectors_

支持向量

coef_

系数(用于线性)核

intercept_

决策函数的常数

K-最近邻

KNN 模型也支持回归,通过找到 k 个最近邻的目标来预测样本。对于回归,该模型会将目标值平均,以确定预测结果。

最近邻模型具有以下特性:

运行效率

训练运行时为 O(1),但需要权衡,因为样本数据需要存储。测试运行时为 O(Nd),其中 N 是训练样本数,d 是维度。

预处理数据

是的,基于距离的计算在标准化后性能更好。

防止过拟合

增加 n_neighbors。为 L1 或 L2 距离修改 p

解释结果

解释样本的 k 最近邻(使用 .kneighbors 方法)。如果可以解释它们,这些邻居解释了你的结果。

下面是使用该模型的示例:

>>> from sklearn.neighbors import (
...     KNeighborsRegressor,
... )
>>> knr = KNeighborsRegressor()
>>> knr.fit(bos_sX_train, bos_sy_train)
KNeighborsRegressor(algorithm='auto',
 leaf_size=30, metric='minkowski',
 metric_params=None, n_jobs=1, n_neighbors=5,
 p=2, weights='uniform')

>>> knr.score(bos_sX_test, bos_sy_test)
0.747112767457727

属性:

algorithm='auto'

可以是'brute''ball_tree''kd_tree'

leaf_size=30

用于树算法。

metric='minkowski'

距离度量。

metric_params=None

用于自定义度量函数的额外参数字典。

n_jobs=1

CPU 数量。

n_neighbors=5

邻居数。

p=2

Minkowski 幂参数。1 = 曼哈顿距离(L1)。2 = 欧几里得距离(L2)。

weights='uniform'

可以是'distance',此时距离较近的点影响较大。

决策树

决策树支持分类和回归。树的每个层次都会评估特征的各种分裂。选择能够产生最低误差(不纯度)的分裂。可以调整criterion参数来确定不纯度的度量标准。

决策树具有以下属性:

运行效率

创建时,对每个 m 个特征进行循环,必须对所有 n 个样本进行排序:O(mn log n)。预测时,遍历树:O(高度)。

预处理数据

不需要缩放。需要消除缺失值并转换为数值型。

防止过拟合

max_depth设置为较低的数字,提高min_impurity_decrease

解释结果

可以通过选择树的选择步骤来步进。由于有步骤,树处理线性关系不佳(特征值的微小变化可能导致完全不同的树形成)。树也高度依赖于训练数据。小的变化可能改变整个树。

这里是使用 scikit-learn 库的一个例子:

>>> from sklearn.tree import DecisionTreeRegressor
>>> dtr = DecisionTreeRegressor(random_state=42)
>>> dtr.fit(bos_X_train, bos_y_train)
DecisionTreeRegressor(criterion='mse',
 max_depth=None, max_features=None,
 max_leaf_nodes=None, min_impurity_decrease=0.0,
 min_impurity_split=None, min_samples_leaf=1,
 min_samples_split=2,
 min_weight_fraction_leaf=0.0, presort=False,
 random_state=42, splitter='best')

>>> dtr.score(bos_X_test, bos_y_test)
0.8426751288675483

实例参数:

criterion='mse'

分裂函数。默认是均方误差(L2 损失)。'friedman_mse''mae'(L1 损失)。

max_depth=None

树的深度。默认会构建直到叶子节点包含少于min_samples_split个样本。

max_features=None

用于分裂的特征数。默认为所有。

max_leaf_nodes=None

限制叶子节点数。默认为无限制。

min_impurity_decrease=0.0

如果分裂会使不纯度减少大于等于某个值,则进行分裂。

min_impurity_split=None

已弃用。

min_samples_leaf=1

每个叶子节点所需的最小样本数。

min_samples_split=2

要求分裂节点的最小样本数。

min_weight_fraction_leaf=0.0

叶子节点所需的最小权重总和。

presort=False

如果设置为True,则可以通过小数据集或限制深度来加速训练。

random_state=None

随机种子。

splitter='best'

使用'random''best'

拟合后的属性:

feature_importances_

基尼重要性数组

max_features_

计算出的max_features

n_outputs_

输出数

n_features_

特征数

tree_

底层树对象

查看树(参见图 14-2):

>>> import pydotplus
>>> from io import StringIO
>>> from sklearn.tree import export_graphviz
>>> dot_data = StringIO()
>>> tree.export_graphviz(
...     dtr,
...     out_file=dot_data,
...     feature_names=bos_X.columns,
...     filled=True,
... )
>>> g = pydotplus.graph_from_dot_data(
...     dot_data.getvalue()
... )
>>> g.write_png("images/mlpr_1402.png")

对于 Jupyter,请使用:

from IPython.display import Image
Image(g.create_png())

决策树。

图 14-2。决策树。

这个图有点宽。在电脑上,你可以放大它的某些部分。你还可以通过限制图的深度(见图 14-3)来做到这一点。(事实证明,最重要的特征通常靠近树的顶部。)我们将使用max_depth参数来实现这一点:

>>> dot_data = StringIO()
>>> tree.export_graphviz(
...     dtr,
...     max_depth=2,
...     out_file=dot_data,
...     feature_names=bos_X.columns,
...     filled=True,
... )
>>> g = pydotplus.graph_from_dot_data(
...     dot_data.getvalue()
... )
>>> g.write_png("images/mlpr_1403.png")

决策树的前两层。

图 14-3. 决策树的前两层。

我们还可以使用 dtreeviz 包,在树的每个节点查看散点图(见图 14-4)。我们将使用深度限制为两层的树以查看详细信息:

>>> dtr3 = DecisionTreeRegressor(max_depth=2)
>>> dtr3.fit(bos_X_train, bos_y_train)
>>> viz = dtreeviz.trees.dtreeviz(
...     dtr3,
...     bos_X,
...     bos_y,
...     target_name="price",
...     feature_names=bos_X.columns,
... )
>>> viz

使用 dtviz 进行回归。

图 14-4. 使用 dtviz 进行回归。

特征重要性:

>>> for col, val in sorted(
...     zip(
...         bos_X.columns, dtr.feature_importances_
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:5]:
...     print(f"{col:10}{val:10.3f}")
RM             0.574
LSTAT          0.191
DIS            0.110
CRIM           0.061
RAD            0.018

随机森林

决策树很好因为它们是可解释的,但它们有过拟合的倾向。随机森林为了更好地泛化模型而牺牲了一些可解释性。这种模型也可用于回归。

随机森林具有以下特性:

运行效率

需要创建 j 个随机树。可以使用n_jobs并行处理。每棵树的复杂度为 O(mn log n),其中 n 是样本数,m 是特征数。创建时,循环遍历每个 m 个特征,并对所有 n 个样本进行排序:O(mn log n)。预测时,按树行走:O(高度)。

预处理数据

只要输入是数值型且没有缺失值,这些都不是必需的。

防止过拟合

添加更多树(n_estimators)。使用较低的max_depth

解释结果

支持特征重要性,但我们没有可以遍历的单棵决策树。可以检查集成中的单棵树。

这里是使用模型的示例:

>>> from sklearn.ensemble import (
...     RandomForestRegressor,
... )
>>> rfr = RandomForestRegressor(
...     random_state=42, n_estimators=100
... )
>>> rfr.fit(bos_X_train, bos_y_train)
RandomForestRegressor(bootstrap=True,
 criterion='mse', max_depth=None,
 max_features='auto', max_leaf_nodes=None,
 min_impurity_decrease=0.0,
 min_impurity_split=None,_samples_leaf=1,
 min_samples_split=2,
 min_weight_fraction_leaf=0.0,
 n_estimators=100, n_jobs=1,
 oob_score=False, random_state=42,
 verbose=0, warm_start=False)

>>> rfr.score(bos_X_test, bos_y_test)
0.8641887615545837

实例参数(这些选项与决策树相似):

bootstrap=True

构建树时使用自举法。

criterion='mse'

分裂函数,'mae'

max_depth=None

树的深度。默认会一直构建,直到叶子包含小于min_samples_split

max_features='auto'

用于分割的特征数。默认为全部。

max_leaf_nodes=None

限制叶子的数量。默认是无限制的。

min_impurity_decrease=0.0

如果分裂可以减少这个值或更多的不纯度,则分割节点。

min_impurity_split=None

已弃用。

min_samples_leaf=1

每个叶子节点的最小样本数。

min_samples_split=2

分裂节点所需的最小样本数。

min_weight_fraction_leaf=0.0

叶子节点所需的最小总权重和。

n_estimators=10

森林中的树木数量。

n_jobs=None

用于拟合和预测的作业数量。(None表示 1。)

oob_score=False

是否使用 OOB 样本来估计未见数据的得分。

random_state=None

随机种子。

verbose=0

冗余度。

warm_start=False

拟合一个新的森林或使用现有的森林。

拟合后的属性:

estimators_

树的集合

feature_importances_

基尼重要性的数组

n_classes_

类的数量

n_features_

特征数量

oob_score_

使用 OOB 估计的训练数据集的得分

特征重要性:

>>> for col, val in sorted(
...     zip(
...         bos_X.columns, rfr.feature_importances_
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:5]:
...     print(f"{col:10}{val:10.3f}")
RM             0.505
LSTAT          0.283
DIS            0.115
CRIM           0.029
PTRATIO        0.016

XGBoost 回归

XGBoost 库还支持回归。它构建一个简单的决策树,然后通过添加后续树来“增强”它。每棵树都试图纠正前一个输出的残差。实际上,这在结构化数据上效果非常好。

它具有以下属性:

运行效率

XGBoost 是可并行化的。使用n_jobs选项指定 CPU 的数量。使用 GPU 可以获得更好的性能。

预处理数据

树模型不需要缩放。需要编码分类数据。支持缺失数据!

防止过拟合

如果在 N 轮后没有改进,则可以设置early_stopping_rounds=N参数停止训练。L1 和 L2 正则化分别由reg_alphareg_lambda控制。较高的数字意味着更为保守。

解释结果

具有特征重要性。

下面是使用该库的一个示例:

>>> xgr = xgb.XGBRegressor(random_state=42)
>>> xgr.fit(bos_X_train, bos_y_train)
XGBRegressor(base_score=0.5, booster='gbtree',
 colsample_bylevel=1, colsample_bytree=1,
 gamma=0, learning_rate=0.1, max_delta_step=0,
 max_depth=3, min_child_weight=1, missing=None,
 n_estimators=100, n_jobs=1, nthread=None,
 objective='reg:linear', random_state=42,
 reg_alpha=0, reg_lambda=1, scale_pos_weight=1,
 seed=None, silent=True, subsample=1)

>>> xgr.score(bos_X_test, bos_y_test)
0.871679473122472

>>> xgr.predict(bos_X.iloc[[0]])
array([27.013563], dtype=float32)

实例参数:

max_depth=3

最大深度。

learning_rate=0.1

提升(boosting)的学习率(eta)(在 0 到 1 之间)。每次提升步骤后,新添加的权重会按此因子进行缩放。值越低越保守,但也需要更多的树来收敛。在调用.train时,可以传递一个learning_rates参数,这是每轮的速率列表(例如,[.1]*100 + [.05]*100)。

n_estimators=100

回合或增强树的数量。

silent=True

是否在运行提升时打印消息。

objective="reg:linear"

分类的学习任务或可调用对象。

booster="gbtree"

可以是'gbtree''gblinear''dart''dart'选项添加了 dropout(随机丢弃树以防止过拟合)。'gblinear'选项创建了一个正则化的线性模型(不是树,但类似于 lasso 回归)。

nthread=None

不推荐使用。

n_jobs=1

要使用的线程数。

gamma=0

进一步分割叶子所需的最小损失减少量。

min_child_weight=1

子节点的 Hessian 和的最小值。

max_delta_step=0

使更新更为保守。对于不平衡的类,设置 1 到 10。

subsample=1

用于下一次提升回合的样本分数的分数。

colsample_bytree=1

用于提升回合的列分数的分数。

colsample_bylevel=1

用于树中级别的列分数。

colsample_bynode=1

用于分割的列的分数(树中的节点)。

reg_alpha=0

L1 正则化(权重的均值)。增加以更为保守。

reg_lambda=1

L2 正则化(平方权重的根)。增加以更为保守。

base_score=.5

初始预测。

seed=None

不推荐使用。

random_state=0

随机种子。

missing=None

用于缺失值的解释值。None表示np.nan

importance_type='gain'

特征重要性类型:'gain''weight''cover''total_gain''total_cover'

属性:

coef_

gblinear 学习器的系数(booster = 'gblinear'

intercept_

gblinear 学习器的截距

feature_importances_

gbtree 学习器的特征重要性

特征重要性是该特征在所有使用它的节点上的平均增益:

>>> for col, val in sorted(
...     zip(
...         bos_X.columns, xgr.feature_importances_
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:5]:
...     print(f"{col:10}{val:10.3f}")
DIS            0.187
CRIM           0.137
RM             0.137
LSTAT          0.134
AGE            0.110

XGBoost 包括用于特征重要性的绘图功能。注意,importance_type参数会改变此图中的值(参见图 14-5)。默认使用权重来确定特征重要性:

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> xgb.plot_importance(xgr, ax=ax)
>>> fig.savefig("images/mlpr_1405.png", dpi=300)

使用权重的特征重要性(特征在树中分裂的次数)。

图 14-5. 使用权重的特征重要性(特征在树中分裂的次数)。

使用 Yellowbrick 绘制特征重要性(它将规范化feature_importances_属性)(参见图 14-6):

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> fi_viz = FeatureImportances(xgr)
>>> fi_viz.fit(bos_X_train, bos_y_train)
>>> fi_viz.poof()
>>> fig.savefig("images/mlpr_1406.png", dpi=300)

使用增益的相对重要性的特征重要性(最重要特征的百分比重要性)。

图 14-6. 使用增益的相对重要性的特征重要性(最重要特征的百分比重要性)。

XGBoost 提供了树的文本表示和图形表示。这是文本表示:

>>> booster = xgr.get_booster()
>>> print(booster.get_dump()[0])
0:[LSTAT<9.72500038] yes=1,no=2,missing=1
 1:[RM<6.94099998] yes=3,no=4,missing=3
 3:[DIS<1.48494995] yes=7,no=8,missing=7
 7:leaf=3.9599998
 8:leaf=2.40158272
 4:[RM<7.43700027] yes=9,no=10,missing=9
 9:leaf=3.22561002
 10:leaf=4.31580687
 2:[LSTAT<16.0849991] yes=5,no=6,missing=5
 5:[B<116.024994] yes=11,no=12,missing=11
 11:leaf=1.1825
 12:leaf=1.99701393
 6:[NOX<0.603000045] yes=13,no=14,missing=13
 13:leaf=1.6868
 14:leaf=1.18572915

叶子值可以解释为base_score和叶子的总和。(要验证此点,使用ntree_limit=1参数调用.predict以限制模型仅使用第一棵树的结果。)

这是树的图形版本(参见图 14-7):

fig, ax = plt.subplots(figsize=(6, 4))
xgb.plot_tree(xgr, ax=ax, num_trees=0)
fig.savefig('images/mlpr_1407.png', dpi=300)

XGBoost 树。

图 14-7. XGBoost 树。

LightGBM 回归

梯度提升树库 LightGBM 也支持回归。正如分类章节中提到的那样,由于用于确定节点分裂的抽样机制,它可能比 XGBoost 创建树更快。

它是按深度优先方式生成树的,因此限制深度可能会损害模型。它具有以下特性:

运行效率

可以利用多个 CPU。通过使用分箱,可以比 XGBoost 快 15 倍。

预处理数据

对将分类列编码为整数(或 pandas 的Categorical类型)提供了一些支持,但与独热编码相比,AUC 似乎表现较差。

预防过拟合

降低num_leaves。增加min_data_in_leaf。使用lambda_l1lambda_l2min_gain_to_split

解释结果

可用的特征重要性。单独的树较弱且往往难以解释。

这是使用模型的一个示例:

>>> import lightgbm as lgb
>>> lgr = lgb.LGBMRegressor(random_state=42)
>>> lgr.fit(bos_X_train, bos_y_train)
LGBMRegressor(boosting_type='gbdt',
 class_weight=None, colsample_bytree=1.0,
 learning_rate=0.1, max_depth=-1,
 min_child_samples=20, min_child_weight=0.001,
 min_split_gain=0.0, n_estimators=100,
 n_jobs=-1, num_leaves=31, objective=None,
 random_state=42, reg_alpha=0.0,
 reg_lambda=0.0, silent=True, subsample=1.0,
 subsample_for_bin=200000, subsample_freq=0)

>>> lgr.score(bos_X_test, bos_y_test)
0.847729219534575

>>> lgr.predict(bos_X.iloc[[0]])
array([30.31689569])

实例参数:

boosting_type='gbdt'

可以是'gbdt'(梯度提升)、'rf'(随机森林)、'dart'(dropout meet multiple additive regression trees)或'goss'(基于梯度的单侧抽样)。

num_leaves=31

最大树叶子数。

max_depth=-1

最大树深度。-1 表示无限制。较大的深度往往会导致过拟合。

learning_rate=0.1

范围为(0, 1.0]。提升(boosting)的学习率。较小的值减缓过拟合,因为提升轮次对结果的影响较小。较小的数字应该能够提供更好的性能,但会需要更多的num_iterations

n_estimators=100

树的数量或提升轮次。

subsample_for_bin=200000

创建分箱所需的样本数。

objective=None

None - 默认执行回归。可以是函数或字符串。

min_split_gain=0.0

损失减少所需的叶子分区。

min_child_weight=0.001

叶子节点所需的 hessian 权重之和。值越大越保守。

min_child_samples=20

叶子节点所需的样本数。数值越低表示过拟合越严重。

subsample=1.0

用于下一轮的样本分数比例。

subsample_freq=0

子采样频率。设置为 1 以启用。

colsample_bytree=1.0

范围是 (0, 1.0]。每轮提升选择的特征百分比。

reg_alpha=0.0

L1 正则化(权重的均值)。增加以更加保守。

reg_lambda=0.0

L2 正则化(平方权重的根)。增加以更加保守。

random_state=42

随机种子。

n_jobs=-1

线程数。

silent=True

冗长模式。

importance_type='split'

确定重要性计算方式:*split*(特征使用次数)或 *gain*(特征使用时的总增益)。

LightGBM 支持特征重要性。importance_type 参数确定计算方式(默认基于特征使用次数):

>>> for col, val in sorted(
...     zip(
...         bos_X.columns, lgr.feature_importances_
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:5]:
...     print(f"{col:10}{val:10.3f}")
LSTAT        226.000
RM           199.000
DIS          172.000
AGE          130.000
B            121.000

特征重要性图显示特征被使用的次数(见 Figure 14-8):

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> lgb.plot_importance(lgr, ax=ax)
>>> fig.tight_layout()
>>> fig.savefig("images/mlpr_1408.png", dpi=300)

显示特征被使用次数的特征重要性。

图 14-8. 显示特征被使用次数的特征重要性。
提示

在 Jupyter 中,使用以下命令查看树:

lgb.create_tree_digraph(lgbr)

第十五章:指标与回归评估

本章将评估在波士顿房价数据上训练的随机森林回归器的结果:

>>> rfr = RandomForestRegressor(
...     random_state=42, n_estimators=100
... )
>>> rfr.fit(bos_X_train, bos_y_train)

指标

sklearn.metrics 模块包含用于评估回归模型的指标。以 losserror 结尾的指标函数应该最小化。以 score 结尾的函数应该最大化。

决定系数(r²)是常见的回归指标。该值通常介于 0 到 1 之间。它表示特征对目标变量贡献的方差百分比。较高的值更好,但通常单凭这个指标很难评估模型。0.7 是一个好分数吗?这取决于数据集。对于某个数据集,0.5 可能是一个好分数,而对于另一个数据集,0.9 可能是一个坏分数。通常我们会结合其他指标或可视化来评估模型。

例如,很容易使用 r² 预测第二天的股票价格模型达到 0.99,但我不会用这个模型交易我的钱。它可能略低或略高,这可能对交易造成严重影响。

r² 指标是网格搜索中使用的默认指标。您可以使用 scoring 参数指定其他指标。

.score 方法用于计算回归模型的这一指标:

>>> from sklearn import metrics
>>> rfr.score(bos_X_test, bos_y_test)
0.8721182042634867

>>> metrics.r2_score(bos_y_test, bos_y_test_pred)
0.8721182042634867
注意

还有一个 解释方差 指标(在网格搜索中为 'explained_variance')。如果 残差(预测误差)的平均值为 0(在普通最小二乘(OLS)模型中),则解释的方差与决定系数相同:

>>> metrics.explained_variance_score(
...     bos_y_test, bos_y_test_pred
... )
0.8724890451227875

平均绝对误差(在网格搜索中使用 'neg_mean_absolute_error')表示平均绝对模型预测误差。一个完美的模型得分为 0,但与决定系数不同,该指标没有上限。然而,由于它以目标单位表示,因此更具可解释性。如果要忽略异常值,这是一个好指标。

这个度量不能表明模型有多糟糕,但可以用来比较两个模型。如果有两个模型,得分较低的模型更好。

这个数字告诉我们平均误差大约比真实值高或低两个单位:

>>> metrics.mean_absolute_error(
...     bos_y_test, bos_y_test_pred
... )
2.0839802631578945

均方根误差(在网格搜索中为 'neg_mean_squared_error')也是用目标的角度来衡量模型误差的。然而,因为它在取平方根之前先平均了误差的平方,所以会惩罚较大的误差。如果你想惩罚大误差,这是一个很好的指标。例如,偏差为八比偏差为四差两倍以上。

和平均绝对误差一样,这个度量不能表明模型有多糟糕,但可以用来比较两个模型。如果假设误差服从正态分布,这是一个不错的选择。

结果告诉我们,如果我们平方误差并求平均,结果大约是 9.5:

>>> metrics.mean_squared_error(
...     bos_y_test, bos_y_test_pred
... )
9.52886846710526

均方对数误差(在网格搜索中为'neg_mean_squared_log_error')对低估的惩罚大于高估。如果你的目标经历指数增长(如人口、股票等),这是一个很好的度量标准。

如果你取误差的对数然后平方,这些结果的平均值将是 0.021:

>>> metrics.mean_squared_log_error(
...     bos_y_test, bos_y_test_pred
... )
0.02128263061776433

残差图

好的模型(具有适当的 R2 分数)将表现出同方差性。这意味着目标值的方差对于所有输入值都是相同的。在图中绘制,这看起来像残差图中随机分布的值。如果存在模式,则模型或数据存在问题。

残差图还显示了离群值,这可能会对模型拟合产生重大影响(见图 15-1)。

Yellowbrick 可以制作残差图来可视化这一点:

>>> from yellowbrick.regressor import ResidualsPlot
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> rpv = ResidualsPlot(rfr)
>>> rpv.fit(bos_X_train, bos_y_train)
>>> rpv.score(bos_X_test, bos_y_test)
>>> rpv.poof()
>>> fig.savefig("images/mlpr_1501.png", dpi=300)

残差图。进一步的测试将表明这些残差是异方差的。

图 15-1. 残差图。进一步的测试将表明这些残差是异方差的。

异方差性

statsmodel 库包括Breusch-Pagan 测试用于异方差性。这意味着残差的方差随预测值的变化而变化。在 Breusch-Pagan 测试中,如果 p 值显著小于 0.05,则拒绝同方差性的原假设。这表明残差是异方差的,预测存在偏差。

测试确认存在异方差性:

>>> import statsmodels.stats.api as sms
>>> hb = sms.het_breuschpagan(resids, bos_X_test)
>>> labels = [
...     "Lagrange multiplier statistic",
...     "p-value",
...     "f-value",
...     "f p-value",
... ]
>>> for name, num in zip(name, hb):
...     print(f"{name}: {num:.2}")
Lagrange multiplier statistic: 3.6e+01
p-value: 0.00036
f-value: 3.3
f p-value: 0.00022

正态残差

scipy 库包括概率图Kolmogorov-Smirnov 测试,两者都用于衡量残差是否符合正态分布。

我们可以绘制一个直方图(见图 15-2)来可视化残差并检查正态性:

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> resids = bos_y_test - rfr.predict(bos_X_test)
>>> pd.Series(resids, name="residuals").plot.hist(
...     bins=20, ax=ax, title="Residual Histogram"
... )
>>> fig.savefig("images/mlpr_1502.png", dpi=300)

残差的直方图。

图 15-2. 残差的直方图。

图 15-3 显示了一个概率图。如果样本与分位数直线对齐,残差是正态的。我们可以看到这在本例中失败了:

>>> from scipy import stats
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> _ = stats.probplot(resids, plot=ax)
>>> fig.savefig("images/mlpr_1503.png", dpi=300)

残差的概率图。

图 15-3. 残差的概率图。

Kolmogorov-Smirnov 检验可以评估分布是否为正态分布。如果 p 值显著小于 0.05,则这些值不是正态分布的。

这也失败了,这告诉我们残差不服从正态分布:

>>> stats.kstest(resids, cdf="norm")
KstestResult(statistic=0.1962230021010155, pvalue=1.3283596864921421e-05)

预测误差图

预测误差图显示了真实目标与预测值之间的关系。对于一个完美的模型,这些点将在一个 45 度的直线上对齐。

由于我们的模型似乎对 y 的高端预测较低的值,因此模型存在一些性能问题。这也在残差图中明显(见图 15-4)。

这是 Yellowbrick 版本:

>>> from yellowbrick.regressor import (
...     PredictionError,
... )
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> pev = PredictionError(rfr)
>>> pev.fit(bos_X_train, bos_y_train)
>>> pev.score(bos_X_test, bos_y_test)
>>> pev.poof()
>>> fig.savefig("images/mlpr_1504.png", dpi=300)

预测误差。绘制了预测的 y(y-hat)与实际 y 的图形。

图 15-4. 预测误差。绘制了预测的 y(y-hat)与实际 y 的图形。