机器学习口袋参考(全)
原文:
annas-archive.org/md5/dd771df8c19ec4613e3638bc1f862b92
译者:飞龙
前言
机器学习和数据科学目前非常流行,是发展迅速的目标。我大部分职业生涯都在使用 Python 和数据,并且希望有一本实体书可以提供我在工业界和工作坊教学中使用的常见方法的参考,以解决结构化机器学习问题。
这本书是我认为最好的资源和示例的集合,用于攻击一个有结构化数据的预测建模任务。有许多库执行所需的部分任务,我已经尝试将它们纳入其中,因为我在咨询或行业工作中应用了这些技术。
许多人可能会抱怨缺乏深度学习技术。这些可能需要另外一本书来详细介绍。我也更喜欢简单的技术,而且行业内的其他人似乎也同意。对于非结构化数据(视频、音频、图像)使用深度学习,对于结构化数据使用强大的工具如 XGBoost。
我希望这本书能为您解决紧迫的问题提供有用的参考。
期望什么
本书提供了解决常见结构化数据问题的深入示例。它介绍了各种库和模型、它们的权衡、如何调整它们以及如何解释它们。
代码片段的大小应该适合你在自己的项目中使用和调整。
本书适用对象
如果你刚开始学习机器学习,或者已经从事机器学习多年,这本书应该作为一个有价值的参考。它假设你有一些 Python 的知识,并且完全不涉及语法。相反,它展示了如何使用各种库来解决现实世界中的问题。
这并不取代深入的课程,但应该作为应用机器学习课程可能涵盖的内容的参考。(注:作者将其用作他所教授的数据分析和机器学习课程的参考。)
本书中使用的约定
本书采用以下排版约定:
斜体
表示新术语、网址、电子邮件地址、文件名和文件扩展名。
等宽字体
用于程序清单,以及在段落中引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
小贴士
这个元素表示一个提示或建议。
注意
这个元素表示一般的注意事项。
警告
这个元素表示警告或注意事项。
使用代码示例
附加材料(代码示例、练习等)可在https://github.com/mattharrison/ml_pocket_reference获得。
本书的目的是帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您要复制大量代码,否则无需征得我们的许可。例如,编写使用本书多个代码片段的程序不需要许可。出售或分发包含 O’Reilly 书籍示例的 CD-ROM 需要许可。引用本书并引用示例代码回答问题不需要许可。将本书大量示例代码整合到产品文档中需要许可。
我们感谢您的致意,但并不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Machine Learning Pocket Reference by Matt Harrison (O’Reilly). Copyright 2019 Matt Harrison, 978-1-492-04754-4.”
如果您认为您对代码示例的使用超出了合理使用范围或以上给出的权限,请随时联系我们:[email protected]。
O’Reilly 在线学习
注
近 40 年来,O’Reilly Media 提供技术和商业培训、知识和见解,帮助企业取得成功。
我们独特的专家和创新者网络通过书籍、文章、会议以及我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深度学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频资源。更多信息,请访问http://oreilly.com。
如何联系我们
请向出版商提出关于本书的评论和问题:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938(美国或加拿大)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们为本书设有一个网页,列出勘误、示例和任何额外信息。您可以访问此页面:http://www.oreilly.com/catalog/9781492047544。
要评论或询问关于本书的技术问题,请发送电子邮件至[email protected]。
有关我们的书籍、课程、会议和新闻的更多信息,请访问我们的网站http://www.oreilly.com。
在 Facebook 上找到我们:http://facebook.com/oreilly
在 Twitter 上关注我们:http://twitter.com/oreillymedia
在 YouTube 上观看我们:http://www.youtube.com/oreillymedia
致谢
感谢我的妻子和家人的支持。我对 Python 社区提供的优秀语言和工具集深表感激。与 Nicole Tache 的合作愉快,她提供了极好的反馈意见。我的技术审阅者 Mikio Braun、Natalino Busa 和 Justin Francis 使我保持了诚实。谢谢!
第一章:介绍
这不太像是一本教学手册,而更像是关于机器学习的笔记、表格和示例。它是作者在培训期间作为额外资源创建的,旨在作为实体笔记本分发。参与者(喜欢纸质材料的物理特性)可以添加自己的笔记和想法,并获得经过筛选的示例的宝贵参考。
我们将介绍如何使用结构化数据进行分类。其他常见的机器学习应用包括预测连续值(回归)、创建聚类或试图降低维度等。本书不讨论深度学习技术。虽然这些技术在非结构化数据上表现良好,但大多数人推荐本书中的技术来处理结构化数据。
我们假设您具有 Python 的知识和熟悉度。学习如何使用pandas 库来处理数据非常有用。我们有许多使用 pandas 的示例,它是处理结构化数据的优秀工具。然而,如果您不熟悉 numpy,一些索引操作可能会令人困惑。完整覆盖 pandas 可能需要一本专著来讨论。
使用的库
本书使用了许多库。这既可能是好事,也可能是坏事。其中一些库可能难以安装或与其他库的版本冲突。不要觉得您需要安装所有这些库。使用“即时安装”并仅在需要时安装您想要使用的库。
>>> import autosklearn, catboost,
category_encoders, dtreeviz, eli5, fancyimpute,
fastai, featuretools, glmnet_py, graphviz,
hdbscan, imblearn, janitor, lime, matplotlib,
missingno, mlxtend, numpy, pandas, pdpbox, phate,
pydotplus, rfpimp, scikitplot, scipy, seaborn,
shap, sklearn, statsmodels, tpot, treeinterpreter,
umap, xgbfir, xgboost, yellowbrick
>>> for lib in [
... autosklearn,
... catboost,
... category_encoders,
... dtreeviz,
... eli5,
... fancyimpute,
... fastai,
... featuretools,
... glmnet_py,
... graphviz,
... hdbscan,
... imblearn,
... lime,
... janitor,
... matplotlib,
... missingno,
... mlxtend,
... numpy,
... pandas,
... pandas_profiling,
... pdpbox,
... phate,
... pydotplus,
... rfpimp,
... scikitplot,
... scipy,
... seaborn,
... shap,
... sklearn,
... statsmodels,
... tpot,
... treeinterpreter,
... umap,
... xgbfir,
... xgboost,
... yellowbrick,
... ]:
... try:
... print(lib.__name__, lib.__version__)
... except:
... print("Missing", lib.__name__)
catboost 0.11.1
category_encoders 2.0.0
Missing dtreeviz
eli5 0.8.2
fancyimpute 0.4.2
fastai 1.0.28
featuretools 0.4.0
Missing glmnet_py
graphviz 0.10.1
hdbscan 0.8.22
imblearn 0.4.3
janitor 0.16.6
Missing lime
matplotlib 2.2.3
missingno 0.4.1
mlxtend 0.14.0
numpy 1.15.2
pandas 0.23.4
Missing pandas_profiling
pdpbox 0.2.0
phate 0.4.2
Missing pydotplus
rfpimp
scikitplot 0.3.7
scipy 1.1.0
seaborn 0.9.0
shap 0.25.2
sklearn 0.21.1
statsmodels 0.9.0
tpot 0.9.5
treeinterpreter 0.1.0
umap 0.3.8
xgboost 0.81
yellowbrick 0.9
注意
大多数这些库可以使用pip
或conda
轻松安装。对于fastai
,我需要使用pip install --no-deps fastai
。umap
库可以使用pip install umap-learn
安装。janitor
库可以使用pip install pyjanitor
安装。autosklearn
库可以使用pip install auto-sklearn
安装。
我通常使用 Jupyter 进行分析。您也可以使用其他笔记本工具。请注意,一些工具如 Google Colab 已预安装了许多库(尽管它们可能是过时版本)。
在 Python 中安装库有两个主要选项。一个是使用pip
(Python 包管理工具的缩写),这是一个随 Python 一起安装的工具。另一个选项是使用Anaconda。我们将两者都介绍。
使用 Pip 进行安装
在使用pip
之前,我们将创建一个沙盒环境,将我们的库安装到其中。这称为名为env
的虚拟环境:
$ python -m venv env
注意
在 Macintosh 和 Linux 上,使用python
;在 Windows 上,使用python3
。如果 Windows 在命令提示符中未能识别它,请重新安装或修复您的安装,并确保选中“将 Python 添加到我的 PATH”复选框。
然后,您激活环境,这样当您安装库时,它们将进入沙盒环境而不是全局的 Python 安装中。由于许多这些库会发生变化并进行更新,最好在每个项目基础上锁定版本,这样您就知道您的代码将可以运行。
这是如何在 Linux 和 Macintosh 上激活虚拟环境:
$ source env/bin/activate
注意到提示符已更新,表示我们正在使用虚拟环境:
(env) $ which python
env/bin/python
在 Windows 上,您需要通过运行此命令激活环境:
C:> env\Scripts\activate.bat
再次注意到,提示符已更新,表示我们正在使用虚拟环境:
(env) C:> where python
env\Scripts\python.exe
在所有平台上,您可以使用pip
安装包。要安装 pandas,键入:
(env) $ pip install pandas
一些包名称与库名称不同。您可以使用以下命令搜索包:
(env) $ pip search libraryname
安装了包之后,你可以使用pip
创建一个包含所有包版本的文件:
(env) $ pip freeze > requirements.txt
使用此requirements.txt
文件,您可以轻松地将包安装到新的虚拟环境中:
(other_env) $ pip install -r requirements.txt
使用 Conda 安装
conda
工具与 Anaconda 捆绑,允许我们创建环境并安装包。
要创建一个名为env
的环境,请运行:
$ conda create --name env python=3.6
要激活此环境,请运行:
$ conda activate env
这将在 Unix 和 Windows 系统上更新提示符。现在你可以使用以下命令搜索包:
(env) $ conda search libraryname
要安装像 pandas 这样的包,请运行:
(env) $ conda install pandas
要创建包含包需求的文件,请运行:
(env) $ conda env export > environment.yml
要在新环境中安装这些要求,请运行:
(other_env) $ conda create -f environment.yml
警告
本书提到的一些库无法从 Anaconda 的仓库安装。不要担心。事实证明,你可以在 conda 环境中使用pip
(无需创建新的虚拟环境),并使用pip
安装这些库。
第二章:机器学习流程概述
数据挖掘的跨行业标准过程(CRISP-DM)是一种进行数据挖掘的过程。它包括几个步骤,可供持续改进参考。它们包括:
-
业务理解
-
数据理解
-
数据准备
-
建模
-
评估
-
部署
图 2-1 展示了我创建预测模型的工作流程,该模型扩展了 CRISP-DM 方法论。下一章节中的详细步骤将覆盖这些基本步骤。
图 2-1. 机器学习的通用工作流程。
第三章:分类演练:泰坦尼克数据集
本章将通过使用泰坦尼克数据集来解决一个常见的分类问题。后面的章节将深入探讨并扩展在分析过程中执行的常见步骤。
项目布局建议
进行探索性数据分析的一个很好的工具是Jupyter。Jupyter 是一个支持 Python 和其他语言的开源笔记本环境。它允许您创建代码或 Markdown 内容的单元格。
我倾向于使用 Jupyter 有两种模式。一种是用于探索性数据分析和快速尝试。另一种是更多地以可交付的方式使用,我使用 Markdown 单元格格式化报告,并插入代码单元格以说明重要的观点或发现。如果你不小心,你的笔记本可能需要一些重构和应用软件工程实践(删除全局变量,使用函数和类等)。
cookiecutter 数据科学包建议了一个布局,用于创建一个分析,以便轻松复制和共享代码。
导入
这个例子主要基于pandas、scikit-learn和Yellowbrick。pandas 库为我们提供了方便的数据整理工具。scikit-learn 库具有出色的预测建模能力,而 Yellowbrick 是一个用于评估模型的可视化库。
>>> import matplotlib.pyplot as plt
>>> import pandas as pd
>>> from sklearn import (
... ensemble,
... preprocessing,
... tree,
... )
>>> from sklearn.metrics import (
... auc,
... confusion_matrix,
... roc_auc_score,
... roc_curve,
... )
>>> from sklearn.model_selection import (
... train_test_split,
... StratifiedKFold,
... )
>>> from yellowbrick.classifier import (
... ConfusionMatrix,
... ROCAUC,
... )
>>> from yellowbrick.model_selection import (
... LearningCurve,
... )
警告
您可能会发现在线文档和示例包括星号导入,例如:
from pandas import *
避免使用星号导入。显式表达会使您的代码更易于理解。
提出问题
在这个例子中,我们想要创建一个预测模型来回答一个问题。它将根据个人和旅行特征对个体是否在泰坦尼克号船难中幸存进行分类。这是一个玩具示例,但它作为一个教学工具,展示了建模的许多步骤。我们的模型应该能够获取乘客信息并预测该乘客在泰坦尼克号上是否会幸存。
这是一个分类问题,因为我们正在预测幸存的标签;他们是否幸存下来。
数据术语
我们通常用一个数据矩阵来训练模型。(我更喜欢使用 pandas DataFrames,因为它很方便有列标签,但 numpy 数组也可以使用。)
对于监督学习,如回归或分类,我们的目的是有一个将特征转换为标签的函数。如果我们将其写成代数公式,它会像这样:
y = f(X)
X 是一个矩阵。每一行代表数据的一个样本或关于一个个体的信息。X 中的每一列都是一个特征。我们函数的输出 y 是一个包含标签(用于分类)或值(用于回归)的向量(见图 3-1)。
图 3-1。结构化数据布局。
这是标准命名过程,用于命名数据和输出。如果您阅读学术论文或查看库的文档,它们会遵循这种约定。在 Python 中,我们使用变量名X
来保存样本数据,即使变量的大写是违反标准命名约定(PEP 8)的。不用担心,每个人都这样做,如果您将变量命名为x
,可能会让人觉得有点奇怪。变量y
存储标签或目标。
表 3-1 显示了一个基本数据集,包括两个样本和每个样本的三个特征。
表 3-1. 样本(行)和特征(列)
pclass | age | sibsp |
---|---|---|
1 | 29 | 0 |
1 | 2 | 1 |
收集数据
我们将加载一个 Excel 文件(确保已安装 pandas 和 xlrd¹),其中包含泰坦尼克号的特征。它有许多列,包括一个包含个体生存情况的 survived 列:
>>> url = (
... "http://biostat.mc.vanderbilt.edu/"
... "wiki/pub/Main/DataSets/titanic3.xls"
... )
>>> df = pd.read_excel(url)
>>> orig_df = df
数据集包括以下列:
-
pclass - 乘客等级(1 = 1 等,2 = 2 等,3 = 3 等)
-
生存 - 生存(0 = 否,1 = 是)
-
姓名 - 姓名
-
性别 - 性别
-
年龄 - 年龄
-
sibsp - 船上兄弟姐妹/配偶的数量
-
parch - 船上父母/子女的数量
-
票 - 票号
-
票价 - 乘客票价
-
舱室 - 舱室
-
登船 - 登船点(C = 瑟堡,Q = 皇后镇,S = 南安普顿)
-
船 - 救生艇
-
尸体 - 尸体识别号码
-
家庭/目的地 - 家庭/目的地
Pandas 可以读取此电子表格,并将其转换为 DataFrame。我们需要抽查数据,并确保可以进行分析。
清洗数据
一旦我们拥有数据,我们需要确保它以我们可以用来创建模型的格式存在。大多数 scikit-learn 模型要求我们的特征是数字(整数或浮点数)。此外,如果模型传递了缺失值(pandas 或 numpy 中的NaN
),许多模型将失败。某些模型如果数据经过标准化(具有平均值为 0 和标准偏差为 1)处理,性能会更好。我们将使用 pandas 或 scikit-learn 解决这些问题。此外,泰坦尼克号数据集具有泄漏特征。
泄漏特征是包含有关未来或目标信息的变量。拥有关于目标的数据并不是坏事,并且在模型创建时我们经常会有这些数据。然而,如果这些变量在我们对新样本进行预测时不可用,我们应该将它们从模型中删除,因为它们会泄漏未来的数据。
清洗数据可能需要一些时间。最好有专业主题专家(SME),可以提供处理异常值或缺失数据的指导。
>>> df.dtypes
pclass int64
survived int64
name object
sex object
age float64
sibsp int64
parch int64
ticket object
fare float64
cabin object
embarked object
boat object
body float64
home.dest object
dtype: object
我们通常看到int64
,float64
,datetime64[ns]
或object
。这些是 pandas 用于存储数据列的类型。int64
和float64
是数值类型。datetime64[ns]
存储日期和时间数据。object
通常意味着它存储字符串数据,虽然可能是字符串和其他类型的组合。
在从 CSV 文件读取时,pandas 将尝试将数据强制转换为适当的类型,但会退回到 object
。从电子表格、数据库或其他系统读取数据可能会提供 DataFrame 中更好的类型。无论如何,浏览数据并确保类型合理都是值得的。
整数类型通常没问题。浮点类型可能有一些缺失值。日期和字符串类型将需要转换或用于特征工程数字类型。低基数的字符串类型称为分类列,从中创建虚拟列可能是值得的(pd.get_dummies
函数负责此操作)。
注意
在 pandas 0.23 版本之前,如果类型为 int64
,我们可以确保没有缺失值。如果类型为 float64
,值可能是所有浮点数,也可能是类似整数的数字,带有缺失值。pandas 库将具有缺失数字的整数值转换为浮点数,因为这种类型支持缺失值。object
通常意味着字符串类型(或字符串和数字混合)。
从 pandas 0.24 开始,有一个新的 Int64
类型(请注意大小写)。这不是默认的整数类型,但您可以强制转换为此类型并支持缺失数字。
pandas-profiling 库包括一个配置报告。您可以在笔记本中生成此报告。它将总结列的类型,并允许您查看分位数统计、描述统计、直方图、常见值和极端值的详细信息(见图 3-2 和 3-3):
>>> import pandas_profiling
>>> pandas_profiling.ProfileReport(df)
图 3-2. Pandas-profiling 概要。
图 3-3. Pandas-profiling 变量详细信息。
使用 DataFrame 的 .shape
属性来检查行数和列数:
>>> df.shape
(1309, 14)
使用 .describe
方法获取摘要统计信息,并查看非空数据的计数。此方法的默认行为是仅报告数值列。这里的输出被截断,仅显示前两列:
>>> df.describe().iloc[:, :2]
pclass survived
count 1309.000000 1309.000000
mean 2.294882 0.381971
std 0.837836 0.486055
min 1.000000 0.000000
25% 2.000000 0.000000
50% 3.000000 0.000000
75% 3.000000 1.000000
max 3.000000 1.000000
计数统计仅包括不是 NaN 的值,因此用于检查列是否缺少数据是有用的。还可以通过查看最小值和最大值来检查是否存在异常值。总结统计是一种方法。绘制直方图或箱线图是稍后将要看到的视觉表示。
我们将需要处理缺失数据。使用 .isnull
方法查找具有缺失值的列或行。在 DataFrame 上调用 .isnull
返回一个新的 DataFrame,其中每个单元格包含 True
或 False
值。在 Python 中,这些值分别计算为 1
和 0
,这使我们可以对它们进行求和或计算缺失百分比(通过计算均值)。
代码指示每列中缺失数据的计数:
>>> df.isnull().sum()
pclass 0
survived 0
name 0
sex 0
age 263
sibsp 0
parch 0
ticket 0
fare 1
cabin 1014
embarked 2
boat 823
body 1188
home.dest 564
dtype: int64
提示
用.mean
替换.sum
以获得 null 值的百分比。默认情况下,调用这些方法将沿着 axis 0(沿着索引)应用操作。如果你想获得每个样本的缺失特征的计数,你可以沿 axis 1(沿着列)应用此方法:
>>> df.isnull().sum(axis=1).loc[:10]
0 1
1 1
2 2
3 1
4 2
5 1
6 1
7 2
8 1
9 2
dtype: int64
一家中小企业可以帮助确定如何处理缺失数据。年龄列可能是有用的,所以保留它并插值值可能会为模型提供一些信号。大多数值缺失的列(舱位、船和尸体)往往没有提供价值,可以删除。
body 列(身体识别号)对于许多行来说是缺失的。无论如何,我们都应该删除这一列,因为它泄漏了数据。这一列表示乘客没有生存;我们的模型可能会利用这一点来作弊。我们会把它拿出来。(如果我们创建一个模型来预测乘客是否会死亡,知道他们有一个身体识别号的先验信息会让我们知道他们已经死了。我们希望我们的模型不知道这些信息,而是根据其他列进行预测。)同样,船列泄漏了相反的信息(乘客幸存了)。
让我们来看看一些缺失数据的行。我们可以创建一个布尔数组(一个包含True
或False
以指示行是否有缺失数据的系列),并使用它来检查缺失数据的行:
>>> mask = df.isnull().any(axis=1)
>>> mask.head() # rows
0 True
1 True
2 True
3 True
4 True
dtype: bool
>>> df[mask].body.head()
0 NaN
1 NaN
2 NaN
3 135.0
4 NaN
Name: body, dtype: float64
我们稍后将为年龄列填充(或推导出)缺失值。
类型为object
的列往往是分类的(但它们也可能是高基数字符串数据,或者是列类型的混合)。对于我们认为是分类的object
列,可以使用.value_counts
方法来检查值的计数:
>>> df.sex.value_counts(dropna=False)
male 843
female 466
Name: sex, dtype: int64
请记住,pandas 通常会忽略 null 或 NaN 值。如果你想包括这些值,请使用dropna=False
来显示 NaN 的计数:
>>> df.embarked.value_counts(dropna=False)
S 914
C 270
Q 123
NaN 2
Name: embarked, dtype: int64
对于处理缺失的登船值,我们有几个选项。使用 S 可能看起来是合乎逻辑的,因为那是最常见的值。我们可以深入研究数据,尝试确定是否有其他更好的选项。我们也可以删除这两个值。或者,因为这是分类的,我们可以忽略它们,并使用 pandas 来创建虚拟列,如果这两个样本只是每个选项都有 0 个条目的话。对于这个特征,我们将选择后者。
创建特征
我们可以删除没有方差或没有信号的列。在这个数据集中没有这样的特征,但是如果有一个名为“is human”的列,其中每个样本都有 1,那么这一列将不提供任何信息。
或者,除非我们正在使用自然语言处理或从文本列中提取数据,其中每个值都不同,否则模型将无法利用此列。姓名列就是一个例子。有些人已经从姓名中提取了标题 t,并将其视为分类。
我们还想删除泄露信息的列。船和 body 列都泄漏了乘客是否幸存的信息。
pandas 的.drop
方法可以删除行或列:
>>> name = df.name
>>> name.head(3)
0 Allen, Miss. Elisabeth Walton
1 Allison, Master. Hudson Trevor
2 Allison, Miss. Helen Loraine
Name: name, dtype: object
>>> df = df.drop(
... columns=[
... "name",
... "ticket",
... "home.dest",
... "boat",
... "body",
... "cabin",
... ]
... )
我们需要从字符串列创建虚拟列。这将为性别和登船港口创建新的列。Pandas 提供了一个方便的get_dummies
函数来实现:
>>> df = pd.get_dummies(df)
>>> df.columns
Index(['pclass', 'survived', 'age', 'sibsp',
'parch', 'fare', 'sex_female', 'sex_male',
'embarked_C', 'embarked_Q', 'embarked_S'],
dtype='object')
此时,性别 _male 和性别 _female 列是完全逆相关的。通常我们会删除任何具有完美或非常高正负相关性的列。多重共线性可能会影响某些模型中特征重要性和系数的解释。以下是删除性别 _male 列的代码:
>>> df = df.drop(columns="sex_male")
或者,我们可以在get_dummies
调用中添加一个drop_first=True
参数:
>>> df = pd.get_dummies(df, drop_first=True)
>>> df.columns
Index(['pclass', 'survived', 'age', 'sibsp',
'parch', 'fare', 'sex_male',
'embarked_Q', 'embarked_S'],
dtype='object')
创建一个带有特征的 DataFrame(X
)和一个带有标签的系列(y
)。我们也可以使用 numpy 数组,但那样我们就没有列名了:
>>> y = df.survived
>>> X = df.drop(columns="survived")
提示
我们可以使用pyjanitor 库来替换最后两行:
>>> import janitor as jn
>>> X, y = jn.get_features_targets(
... df, target_columns="survived"
... )
示例数据
我们总是希望在不同的数据集上进行训练和测试。否则,你不会真正知道你的模型在未见过的数据上的泛化能力。我们将使用 scikit-learn 将 30%的数据分离出来进行测试(使用random_state=42
以消除比较不同模型时的随机性影响):
>>> X_train, X_test, y_train, y_test = model_selection.train_test_split(
... X, y, test_size=0.3, random_state=42
... )
插补数据
年龄列存在缺失值。我们需要从数值中插补年龄。我们只想在训练集上进行插补,然后使用该插补器填充测试集的数据。否则我们会泄漏数据(通过向模型提供未来信息来作弊)。
现在我们有了测试和训练数据,我们可以在训练集上填补缺失值,并使用训练好的插补器填补测试数据集。fancyimpute 库实现了许多算法。不幸的是,这些算法大多数不是以归纳的方式实现的。这意味着你不能先调用.fit
再调用.transform
,这也意味着你不能基于模型训练的方式来对新数据进行插补。
IterativeImputer
类(原本在 fancyimpute 中,但已迁移到 scikit-learn)支持归纳模式。要使用它,我们需要添加一个特殊的实验性导入(从 scikit-learn 版本 0.21.2 开始):
>>> from sklearn.experimental import (
... enable_iterative_imputer,
... )
>>> from sklearn import impute
>>> num_cols = [
... "pclass",
... "age",
... "sibsp",
... "parch",
... "fare",
... "sex_female",
... ]
>>> imputer = impute.IterativeImputer()
>>> imputed = imputer.fit_transform(
... X_train[num_cols]
... )
>>> X_train.loc[:, num_cols] = imputed
>>> imputed = imputer.transform(X_test[num_cols])
>>> X_test.loc[:, num_cols] = imputed
如果我们想使用中位数进行插补,我们可以使用 pandas 来实现:
>>> meds = X_train.median()
>>> X_train = X_train.fillna(meds)
>>> X_test = X_test.fillna(meds)
标准化数据
对数据进行归一化或预处理后,许多模型的性能会有所提高。特别是那些依赖距离度量来确定相似性的模型。(请注意,树模型会单独处理每个特征,因此不需要此要求。)
我们将对数据进行标准化预处理。标准化是将数据转换为均值为零、标准差为一的形式。这样模型不会认为具有较大数值范围的变量比具有较小数值范围的变量更重要。我将结果(numpy 数组)放回到 pandas DataFrame 中,以便更轻松地进行操作(并保留列名)。
我通常也不会对虚拟列进行标准化,所以我会忽略它们:
>>> cols = "pclass,age,sibsp,fare".split(",")
>>> sca = preprocessing.StandardScaler()
>>> X_train = sca.fit_transform(X_train)
>>> X_train = pd.DataFrame(X_train, columns=cols)
>>> X_test = sca.transform(X_test)
>>> X_test = pd.DataFrame(X_test, columns=cols)
重构
在这一点上,我喜欢重构我的代码。我通常制作两个函数。一个用于一般清理,另一个用于将其分成训练和测试集,并执行那些需要在这些集上以不同方式进行的突变:
>>> def tweak_titanic(df):
... df = df.drop(
... columns=[
... "name",
... "ticket",
... "home.dest",
... "boat",
... "body",
... "cabin",
... ]
... ).pipe(pd.get_dummies, drop_first=True)
... return df
>>> def get_train_test_X_y(
... df, y_col, size=0.3, std_cols=None
... ):
... y = df[y_col]
... X = df.drop(columns=y_col)
... X_train, X_test, y_train, y_test = model_selection.train_test_split(
... X, y, test_size=size, random_state=42
... )
... cols = X.columns
... num_cols = [
... "pclass",
... "age",
... "sibsp",
... "parch",
... "fare",
... ]
... fi = impute.IterativeImputer()
... X_train.loc[
... :, num_cols
... ] = fi.fit_transform(X_train[num_cols])
... X_test.loc[:, num_cols] = fi.transform(
... X_test[num_cols]
... )
...
... if std_cols:
... std = preprocessing.StandardScaler()
... X_train.loc[
... :, std_cols
... ] = std.fit_transform(
... X_train[std_cols]
... )
... X_test.loc[
... :, std_cols
... ] = std.transform(X_test[std_cols])
...
... return X_train, X_test, y_train, y_test
>>> ti_df = tweak_titanic(orig_df)
>>> std_cols = "pclass,age,sibsp,fare".split(",")
>>> X_train, X_test, y_train, y_test = get_train_test_X_y(
... ti_df, "survived", std_cols=std_cols
... )
基线模型
创建一个做某些非常简单的基线模型可以让我们有比较的依据。请注意,使用默认的.score
结果给出的是准确率,这可能会误导。一个问题,其中正例是 10,000 中的 1,通过始终预测负例很容易得到超过 99%的准确率。
>>> from sklearn.dummy import DummyClassifier
>>> bm = DummyClassifier()
>>> bm.fit(X_train, y_train)
>>> bm.score(X_test, y_test) # accuracy
0.5292620865139949
>>> from sklearn import metrics
>>> metrics.precision_score(
... y_test, bm.predict(X_test)
... )
0.4027777777777778
不同的家族
这段代码尝试了多种算法家族。"没有免费午餐"定理指出,没有一个算法能在所有数据上表现良好。然而,对于某些有限的数据集,可能有一个算法能够在该集上表现良好。(这些天结构化学习的流行选择是一种树提升算法,如 XGBoost。)
在这里,我们使用几个不同的家族,并使用 k 折交叉验证比较 AUC 得分和标准差。一个平均分数稍微低一些但标准差较小的算法可能是一个更好的选择。
因为我们使用了 k 折交叉验证,我们将向模型提供所有的X
和y
:
>>> X = pd.concat([X_train, X_test])
>>> y = pd.concat([y_train, y_test])
>>> from sklearn import model_selection
>>> from sklearn.dummy import DummyClassifier
>>> from sklearn.linear_model import (
... LogisticRegression,
... )
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.neighbors import (
... KNeighborsClassifier,
... )
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.svm import SVC
>>> from sklearn.ensemble import (
... RandomForestClassifier,
... )
>>> import xgboost
>>> for model in [
... DummyClassifier,
... LogisticRegression,
... DecisionTreeClassifier,
... KNeighborsClassifier,
... GaussianNB,
... SVC,
... RandomForestClassifier,
... xgboost.XGBClassifier,
... ]:
... cls = model()
... kfold = model_selection.KFold(
... n_splits=10, random_state=42
... )
... s = model_selection.cross_val_score(
... cls, X, y, scoring="roc_auc", cv=kfold
... )
... print(
... f"{model.__name__:22} AUC: "
... f"{s.mean():.3f} STD: {s.std():.2f}"
... )
DummyClassifier AUC: 0.511 STD: 0.04
LogisticRegression AUC: 0.843 STD: 0.03
DecisionTreeClassifier AUC: 0.761 STD: 0.03
KNeighborsClassifier AUC: 0.829 STD: 0.05
GaussianNB AUC: 0.818 STD: 0.04
SVC AUC: 0.838 STD: 0.05
RandomForestClassifier AUC: 0.829 STD: 0.04
XGBClassifier AUC: 0.864 STD: 0.04
堆叠
如果你在走 Kaggle 的路线(或者想要以解释性为代价的最大性能),堆叠 是一个选项。堆叠分类器使用其他模型的输出来预测目标或标签。我们将使用之前模型的输出并将它们结合起来,看看堆叠分类器是否可以做得更好:
>>> from mlxtend.classifier import (
... StackingClassifier,
... )
>>> clfs = [
... x()
... for x in [
... LogisticRegression,
... DecisionTreeClassifier,
... KNeighborsClassifier,
... GaussianNB,
... SVC,
... RandomForestClassifier,
... ]
... ]
>>> stack = StackingClassifier(
... classifiers=clfs,
... meta_classifier=LogisticRegression(),
... )
>>> kfold = model_selection.KFold(
... n_splits=10, random_state=42
... )
>>> s = model_selection.cross_val_score(
... stack, X, y, scoring="roc_auc", cv=kfold
... )
>>> print(
... f"{stack.__class__.__name__} "
... f"AUC: {s.mean():.3f} STD: {s.std():.2f}"
... )
StackingClassifier AUC: 0.804 STD: 0.06
在这种情况下,看起来性能稍微下降了一点,以及标准差。
创建模型
我将使用随机森林分类器来创建一个模型。这是一个灵活的模型,往往能够给出不错的开箱即用结果。记得用我们之前拆分的训练和测试数据(调用.fit
)来训练它:
>>> rf = ensemble.RandomForestClassifier(
... n_estimators=100, random_state=42
... )
>>> rf.fit(X_train, y_train)
RandomForestClassifier(bootstrap=True,
class_weight=None, criterion='gini',
max_depth=None, 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=1, oob_score=False, random_state=42,
verbose=0, warm_start=False)
评估模型
现在我们有了一个模型,我们可以使用测试数据来查看模型对它之前未见过的数据的泛化能力。分类器的.score
方法返回预测准确率的平均值。我们希望确保用测试数据调用.score
方法(假定它在训练数据上表现更好):
>>> rf.score(X_test, y_test)
0.7964376590330788
我们还可以查看其他指标,比如精确度:
>>> metrics.precision_score(
... y_test, rf.predict(X_test)
... )
0.8013698630136986
基于树的模型的一个好处是可以检查特征重要性。特征重要性告诉您一个特征对模型的贡献有多大。请注意,去除一个特征并不意味着分数会相应下降,因为其他特征可能是共线的(在这种情况下,我们可以删除性别 _male 或性别 _female 列,因为它们具有完美的负相关性):
>>> for col, val in sorted(
... zip(
... X_train.columns,
... rf.feature_importances_,
... ),
... key=lambda x: x[1],
... reverse=True,
... )[:5]:
... print(f"{col:10}{val:10.3f}")
age 0.277
fare 0.265
sex_female 0.240
pclass 0.092
sibsp 0.048
特征重要性是通过查看错误增加来计算的。如果去除一个特征增加了模型的错误,那么这个特征就更重要。
我非常喜欢 SHAP 库,用于探索模型认为重要的特征,并解释预测。该库适用于黑盒模型,我们稍后会展示它。
优化模型
模型有控制它们行为的超参数。通过改变这些参数的值,我们改变它们的性能。Sklearn 有一个网格搜索类来评估具有不同参数组合的模型,并返回最佳结果。我们可以使用这些参数来实例化模型类:
>>> rf4 = ensemble.RandomForestClassifier()
>>> params = {
... "max_features": [0.4, "auto"],
... "n_estimators": [15, 200],
... "min_samples_leaf": [1, 0.1],
... "random_state": [42],
... }
>>> cv = model_selection.GridSearchCV(
... rf4, params, n_jobs=-1
... ).fit(X_train, y_train)
>>> print(cv.best_params_)
{'max_features': 'auto', 'min_samples_leaf': 0.1,
'n_estimators': 200, 'random_state': 42}
>>> rf5 = ensemble.RandomForestClassifier(
... **{
... "max_features": "auto",
... "min_samples_leaf": 0.1,
... "n_estimators": 200,
... "random_state": 42,
... }
... )
>>> rf5.fit(X_train, y_train)
>>> rf5.score(X_test, y_test)
0.7888040712468194
我们可以传递scoring
参数给GridSearchCV
以优化不同的指标。详见第十二章了解指标及其含义的列表。
混淆矩阵
混淆矩阵允许我们查看正确的分类,以及假阳性和假阴性。也许我们希望优化假阳性或假阴性,不同的模型或参数可以改变这一点。我们可以使用 sklearn 获取文本版本,或使用 Yellowbrick 进行绘图(参见图 3-4):
>>> from sklearn.metrics import confusion_matrix
>>> y_pred = rf5.predict(X_test)
>>> confusion_matrix(y_test, y_pred)
array([[196, 28],
[ 55, 114]])
>>> mapping = {0: "died", 1: "survived"}
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> cm_viz = ConfusionMatrix(
... rf5,
... classes=["died", "survived"],
... label_encoder=mapping,
... )
>>> cm_viz.score(X_test, y_test)
>>> cm_viz.poof()
>>> fig.savefig(
... "images/mlpr_0304.png",
... dpi=300,
... bbox_inches="tight",
... )
图 3-4. Yellowbrick 混淆矩阵。这是一个有用的评估工具,显示了底部的预测类别和侧面的真实类别。一个好的分类器应该在对角线上有所有的值,并且其他单元格中为零。
ROC 曲线
接收操作特征曲线(ROC)图是评估分类器常用的工具。通过测量曲线下面积(AUC),我们可以得到一个比较不同分类器的度量。它绘制了真正率与假阳性率。我们可以使用 sklearn 来计算 AUC:
>>> y_pred = rf5.predict(X_test)
>>> roc_auc_score(y_test, y_pred)
0.7747781065088757
或者使用 Yellowbrick 来可视化绘图:
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> roc_viz = ROCAUC(rf5)
>>> roc_viz.score(X_test, y_test)
0.8279691030696217
>>> roc_viz.poof()
>>> fig.savefig("images/mlpr_0305.png")
图 3-5. ROC 曲线。显示真正率与假阳性率。一般来说,曲线越凸出越好。通过测量 AUC 可以得到一个评估数字。接近 1 表示更好。低于 0.5 是一个较差的模型。
学习曲线
学习曲线用于告诉我们是否有足够的训练数据。它使用数据的增加部分来训练模型,并测量得分(参见图 3-6)。如果交叉验证得分继续上升,那么我们可能需要投资获取更多数据。以下是一个 Yellowbrick 示例:
>>> import numpy as np
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> cv = StratifiedKFold(12)
>>> sizes = np.linspace(0.3, 1.0, 10)
>>> lc_viz = LearningCurve(
... rf5,
... cv=cv,
... train_sizes=sizes,
... scoring="f1_weighted",
... n_jobs=4,
... ax=ax,
... )
>>> lc_viz.fit(X, y)
>>> lc_viz.poof()
>>> fig.savefig("images/mlpr_0306.png")
图 3-6. 这个学习曲线显示,随着我们添加更多的训练样本,我们的交叉验证(测试)分数似乎在改善。
部署模型
使用 Python 的pickle
模块,我们可以持久化模型并加载它们。一旦我们有了一个模型,我们调用.predict
方法来获得分类或回归结果:
>>> import pickle
>>> pic = pickle.dumps(rf5)
>>> rf6 = pickle.loads(pic)
>>> y_pred = rf6.predict(X_test)
>>> roc_auc_score(y_test, y_pred)
0.7747781065088757
使用Flask部署预测的 Web 服务非常普遍。现在有其他商业和开源产品推出,支持部署。其中包括Clipper,Pipeline,和Google 的云机器学习引擎。
¹ 即使我们不直接调用这个库,当我们加载一个 Excel 文件时,pandas 在后台利用它。
第四章:缺失数据
我们需要处理缺失数据。前一章节展示了一个例子。本章将更深入地探讨。如果数据缺失,大多数算法将无法工作。值得注意的例外是最近的增强库:XGBoost、CatBoost 和 LightGBM。
和许多机器学习中的其他事物一样,如何处理缺失数据没有硬性答案。此外,缺失数据可能代表不同的情况。想象一下,人口普查数据返回,一个年龄特征被报告为缺失。这是因为样本不愿透露他们的年龄?他们不知道他们的年龄?问问题的人甚至忘记询问年龄?缺失的年龄是否有模式?它是否与另一个特征相关?它是否完全是随机的?
处理缺失数据的方法有多种:
-
删除任何带有缺失数据的行。
-
删除任何带有缺失数据的列。
-
填补缺失值
-
创建一个指示列来表示数据缺失
检查缺失数据
让我们回到泰坦尼克号的数据。因为 Python 将 True
和 False
分别视为 1
和 0
,我们可以在 pandas 中利用这一技巧来获取缺失数据的百分比:
>>> df.isnull().mean() * 100
pclass 0.000000
survived 0.000000
name 0.000000
sex 0.000000
age 20.091673
sibsp 0.000000
parch 0.000000
ticket 0.000000
fare 0.076394
cabin 77.463713
embarked 0.152788
boat 62.872422
body 90.756303
home.dest 43.086325
dtype: float64
要可视化缺失数据的模式,可以使用 missingno 库。该库对于查看连续的缺失数据区域非常有用,这表明缺失数据不是随机的(见 图 4-1)。matrix
函数在右侧包括一个火花线。这里的模式也表明非随机的缺失数据。您可能需要限制样本数量以便看到这些模式:
>>> import missingno as msno
>>> ax = msno.matrix(orig_df.sample(500))
>>> ax.get_figure().savefig("images/mlpr_0401.png")
图 4-1. 数据缺失的位置。作者看不出明显的模式。
我们可以使用 pandas 创建一个缺失数据计数的条形图(见 图 4-2):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> (1 - df.isnull().mean()).abs().plot.bar(ax=ax)
>>> fig.savefig("images/mlpr_0402.png", dpi=300)
图 4-2. 使用 pandas 的非缺失数据百分比。船和身体有漏洞,所以我们应该忽略它们。有些年龄缺失很有趣。
或者使用 missingno 库创建相同的图(见 图 4-3):
>>> ax = msno.bar(orig_df.sample(500))
>>> ax.get_figure().savefig("images/mlpr_0403.png")
图 4-3. 使用 missingno 的非缺失数据百分比。
我们可以创建一个热力图,显示数据缺失的相关性(见 图 4-4)。在这种情况下,看起来数据缺失的位置并没有相关性:
>>> ax = msno.heatmap(df, figsize=(6, 6))
>>> ax.get_figure().savefig("/tmp/mlpr_0404.png")
图 4-4. 缺失数据与 missingno 的相关性。
我们可以创建一个树状图,显示数据缺失的聚类情况(见 图 4-5)。处于同一水平的叶子预测彼此的存在(空或填充)。垂直臂用于指示不同聚类的差异程度。短臂意味着分支相似:
>>> ax = msno.dendrogram(df)
>>> ax.get_figure().savefig("images/mlpr_0405.png")
图 4-5. 缺失数据的树状图与 missingno。我们可以看到右上角没有缺失数据的列。
删除缺失数据
pandas 库可以使用.dropna
方法删除所有带有缺失数据的行:
>>> df1 = df.dropna()
要删除列,我们可以注意哪些列缺失,并使用.drop
方法。可以传入一个列名列表或单个列名:
>>> df1 = df.drop(columns="cabin")
或者,我们可以使用.dropna
方法,并设置axis=1
(沿着列轴删除):
>>> df1 = df.dropna(axis=1)
谨慎处理删除数据。我通常把这看作是最后的选择。
填补数据
一旦你有一个预测数据的工具,你可以用它来预测缺失数据。定义缺失值的值的一般任务称为填充。
如果你在填补数据,你需要建立一个流水线,并且在模型创建和预测时使用相同的填补逻辑。scikit-learn 中的 SimpleImputer
类将处理平均值、中位数和最常见的特征值。
默认行为是计算平均值:
>>> from sklearn.impute import SimpleImputer
>>> num_cols = df.select_dtypes(
... include="number"
... ).columns
>>> im = SimpleImputer() # mean
>>> imputed = im.fit_transform(df[num_cols])
提供strategy='median'
或strategy='most_frequent'
以将替换值更改为中位数或最常见值。如果你希望用常数值填充,比如-1
,可以使用strategy='constant'
与fill_value=-1
结合使用。
小贴士
在 pandas 中,你可以使用.fillna
方法来填补缺失值。确保不要泄漏数据。如果你使用平均值进行填充,请确保在模型创建和预测时使用相同的平均值。
最频繁和常量策略可以用于数字或字符串数据。平均值和中位数需要数字数据。
fancyimpute 库实现了许多算法并遵循 scikit-learn 接口。遗憾的是,大多数算法是传导的,这意味着你不能在拟合算法后单独调用.transform
方法。IterativeImputer
是归纳的(已从 fancyimpute 迁移到 scikit-learn)并支持在拟合后进行转换。
添加指示列
数据本身的缺失可能为模型提供一些信号。pandas 库可以添加一个新列来指示缺失值:
>>> def add_indicator(col):
... def wrapper(df):
... return df[col].isna().astype(int)
...
... return wrapper
>>> df1 = df.assign(
... cabin_missing=add_indicator("cabin")
... )
第五章:清理数据
我们可以使用通用工具如 pandas 和专业工具如 pyjanitor 来帮助清理数据。
列名
在使用 pandas 时,使用 Python 友好的列名可以进行属性访问。pyjanitor 的 clean_names
函数将返回一个列名为小写并用下划线替换空格的 DataFrame:
>>> import janitor as jn
>>> Xbad = pd.DataFrame(
... {
... "A": [1, None, 3],
... " sales numbers ": [20.0, 30.0, None],
... }
... )
>>> jn.clean_names(Xbad)
a _sales_numbers_
0 1.0 20.0
1 NaN 30.0
2 3.0 NaN
提示
我建议使用索引赋值、.assign
方法、.loc
或 .iloc
赋值来更新列。我还建议不要使用属性赋值来更新 pandas 中的列。由于可能会覆盖同名列的现有方法,属性赋值不能保证可靠地工作。
pyjanitor 库很方便,但不能去除列周围的空白。我们可以使用 pandas 更精细地控制列的重命名:
>>> def clean_col(name):
... return (
... name.strip().lower().replace(" ", "_")
... )
>>> Xbad.rename(columns=clean_col)
a sales_numbers
0 1.0 20.0
1 NaN 30.0
2 3.0 NaN
替换缺失值
pyjanitor 中的 coalesce
函数接受一个 DataFrame 和一个要考虑的列列表。这类似于 Excel 和 SQL 数据库中的功能。它返回每行的第一个非空值:
>>> jn.coalesce(
... Xbad,
... columns=["A", " sales numbers "],
... new_column_name="val",
... )
val
0 1.0
1 30.0
2 3.0
如果我们想要用特定值填充缺失值,可以使用 DataFrame 的 .fillna
方法:
>>> Xbad.fillna(10)
A sales numbers
0 1.0 20.0
1 10.0 30.0
2 3.0 10.0
或者 pyjanitor 的 fill_empty
函数:
>>> jn.fill_empty(
... Xbad,
... columns=["A", " sales numbers "],
... value=10,
... )
A sales numbers
0 1.0 20.0
1 10.0 30.0
2 3.0 10.0
经常情况下,我们会使用更精细的方法在 pandas、scikit-learn 或 fancyimpute 中执行每列的空值替换。
在创建模型之前,可以使用 pandas 来进行健全性检查,确保处理了所有的缺失值。以下代码在 DataFrame 中返回一个布尔值,用于检查是否有任何缺失的单元格:
>>> df.isna().any().any()
True
第六章:探索
有人说,将一个专家培训成数据科学家比反过来要容易得多。我不完全同意这一观点,但数据确实有细微差别,专家可以帮助分析这些差异。通过理解业务和数据,他们能够创建更好的模型并对业务产生更大的影响。
在创建模型之前,我将进行一些探索性数据分析。这不仅让我对数据有所了解,还是与控制数据的业务单元会面并讨论问题的好借口。
数据大小
再次提醒,我们在这里使用泰坦尼克号数据集。pandas 的 .shape
属性会返回行数和列数的元组:
>>> X.shape
(1309, 13)
我们可以看到这个数据集有 1,309 行和 13 列。
汇总统计
我们可以使用 pandas 获取数据的汇总统计信息。.describe
方法还将给出非 NaN 值的计数。让我们查看第一列和最后一列的结果:
>>> X.describe().iloc[:, [0, -1]]
pclass embarked_S
count 1309.000000 1309.000000
mean -0.012831 0.698243
std 0.995822 0.459196
min -1.551881 0.000000
25% -0.363317 0.000000
50% 0.825248 1.000000
75% 0.825248 1.000000
max 0.825248 1.000000
计数行告诉我们这两列都是填充的。没有缺失值。我们还有均值、标准差、最小值、最大值和四分位数值。
注意
pandas DataFrame 有一个 iloc
属性,可以根据索引位置进行索引操作。它允许我们通过标量、列表或切片传递行位置,然后我们可以添加逗号并传递列位置作为标量、列表或切片。
这里我们提取第二行和第五行以及最后三列:
>>> X.iloc[[1, 4], -3:]
sex_male embarked_Q embarked_S
677 1.0 0 1
864 0.0 0 1
还有一个 .loc
属性,我们可以根据名称而不是位置输出行和列。这是 DataFrame 的相同部分:
>>> X.loc[[677, 864], "sex_male":]
sex_male embarked_Q embarked_S
677 1.0 0 1
864 0.0 0 1
直方图
直方图是可视化数值数据的强大工具。您可以看到有多少个模式,并查看分布(见 图 6-1)。pandas 库有一个 .plot
方法来显示直方图:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> X.fare.plot(kind="hist", ax=ax)
>>> fig.savefig("images/mlpr_0601.png", dpi=300)
图 6-1. Pandas 直方图。
使用 seaborn 库,我们可以绘制一个连续值的直方图,并与目标变量进行对比(见 图 6-2):
fig, ax = plt.subplots(figsize=(12, 8))
mask = y_train == 1
ax = sns.distplot(X_train[mask].fare, label='survived')
ax = sns.distplot(X_train[~mask].fare, label='died')
ax.set_xlim(-1.5, 1.5)
ax.legend()
fig.savefig('images/mlpr_0602.png', dpi=300, bbox_inches='tight')
图 6-2. Seaborn 直方图。
散点图
散点图显示了两个数值列之间的关系(见 图 6-3)。同样,使用 pandas 很容易。如果数据重叠,调整 alpha
参数:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> X.plot.scatter(
... x="age", y="fare", ax=ax, alpha=0.3
... )
>>> fig.savefig("images/mlpr_0603.png", dpi=300)
图 6-3. Pandas 散点图。
这两个特征之间似乎没有太多的相关性。我们可以使用 .corr
方法在两个(pandas)列之间进行皮尔逊相关性分析来量化相关性:
>>> X.age.corr(X.fare)
0.17818151568062093
联合绘图
Yellowbrick 有一个更复杂的散点图,包括边缘的直方图以及一个称为 联合绘图 的回归线(见 图 6-4):
>>> from yellowbrick.features import (
... JointPlotVisualizer,
... )
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> jpv = JointPlotVisualizer(
... feature="age", target="fare"
... )
>>> jpv.fit(X["age"], X["fare"])
>>> jpv.poof()
>>> fig.savefig("images/mlpr_0604.png", dpi=300)
图 6-4. Yellowbrick 联合绘图。
警告
在这个 .fit
方法中,X
和 y
分别指代一列。通常情况下,X
是一个 DataFrame,而不是一个 Series。
您还可以使用 seaborn 库创建一个联合图(见图 6-5):
>>> from seaborn import jointplot
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> p = jointplot(
... "age", "fare", data=new_df, kind="reg"
... )
>>> p.savefig("images/mlpr_0605.png", dpi=300)
图 6-5. Seaborn 联合图。
对角网格
seaborn 库可以创建一个对角网格(见图 6-6)。这个图是列和核密度估计的矩阵。要通过 DataFrame 的某一列进行着色,可以使用 hue
参数。通过目标变量进行着色,我们可以看到特征对目标的不同影响:
>>> from seaborn import pairplot
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> vars = ["pclass", "age", "fare"]
>>> p = pairplot(
... new_df, vars=vars, hue="target", kind="reg"
... )
>>> p.savefig("images/mlpr_0606.png", dpi=300)
图 6-6. Seaborn 对角网格。
箱线图和小提琴图
Seaborn 提供了多种绘制分布的图形。我们展示了箱线图和小提琴图的示例(见图 6-7 和 图 6-8)。这些图形可以将一个特征与目标变量可视化:
>>> from seaborn import box plot
>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> boxplot(x="target", y="age", data=new_df)
>>> fig.savefig("images/mlpr_0607.png", dpi=300)
图 6-7. Seaborn 箱线图。
小提琴图可以帮助可视化分布:
>>> from seaborn import violinplot
>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> violinplot(
... x="target", y="sex_male", data=new_df
... )
>>> fig.savefig("images/mlpr_0608.png", dpi=300)
图 6-8. Seaborn 小提琴图。
比较两个序数值
这是 pandas 代码,用于比较两个序数类别。我将年龄分为十个分位数,将 pclass 分为三个区间。该图被归一化,以填充所有垂直区域。这使得很容易看出,在第 40% 分位数中,大多数票是第三等舱的(见图 6-9):
>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> (
... X.assign(
... age_bin=pd.qcut(
... X.age, q=10, labels=False
... ),
... class_bin=pd.cut(
... X.pclass, bins=3, labels=False
... ),
... )
... .groupby(["age_bin", "class_bin"])
... .size()
... .unstack()
... .pipe(lambda df: df.div(df.sum(1), axis=0))
... .plot.bar(
... stacked=True,
... width=1,
... ax=ax,
... cmap="viridis",
... )
... .legend(bbox_to_anchor=(1, 1))
... )
>>> fig.savefig(
... "image/mlpr_0609.png",
... dpi=300,
... bbox_inches="tight",
... )
注意
这些行:
.groupby(["age_bin", "class_bin"])
.size()
.unstack()
可以用以下内容替换:
.pipe(lambda df: pd.crosstab(
df.age_bin, df.class_bin)
)
在 pandas 中,通常有多种方法可以完成某项任务,还有一些辅助函数可组合其他功能,如 pd.crosstab
。
图 6-9. 比较序数值。
相关性
Yellowbrick 可以在特征之间创建成对比较(见图 6-10)。此图显示皮尔逊相关性(algorithm
参数也接受 'spearman'
和 'covariance'
):
>>> from yellowbrick.features import Rank2D
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> pcv = Rank2D(
... features=X.columns, algorithm="pearson"
... )
>>> pcv.fit(X, y)
>>> pcv.transform(X)
>>> pcv.poof()
>>> fig.savefig(
... "images/mlpr_0610.png",
... dpi=300,
... bbox_inches="tight",
... )
图 6-10. 用 Yellowbrick 创建的协方差相关性。
seaborn 库中还有类似的热图(见图 6-11)。我们需要将相关性 DataFrame 作为数据传入。遗憾的是,除非矩阵中的值允许,或者我们添加 vmin
和 vmax
参数,否则颜色条不会跨越 -1 到 1:
>>> from seaborn import heatmap
>>> fig, ax = plt.subplots(figsize=(8, 8))
>>> ax = heatmap(
... X.corr(),
... fmt=".2f",
... annot=True,
... ax=ax,
... cmap="RdBu_r",
... vmin=-1,
... vmax=1,
... )
>>> fig.savefig(
... "images/mlpr_0611.png",
... dpi=300,
... bbox_inches="tight",
... )
图 6-11. Seaborn 热图。
pandas 库也可以提供 DataFrame 列之间的相关性。我们只显示结果的前两列。默认方法是 'pearson'
,但也可以将 method
参数设置为 'kendall'
、'spearman'
或一个返回两列之间浮点数的自定义可调用函数:
>>> X.corr().iloc[:, :2]
pclass age
pclass 1.000000 -0.440769
age -0.440769 1.000000
sibsp 0.060832 -0.292051
parch 0.018322 -0.174992
fare -0.558831 0.177205
sex_male 0.124617 0.077636
embarked_Q 0.230491 -0.061146
embarked_S 0.096335 -0.041315
高度相关的列并不增加价值,反而可能影响特征重要性和回归系数的解释。以下是查找相关列的代码。在我们的数据中,没有任何高度相关的列(记住我们已删除 sex_male
列)。
如果我们有相关的列,我们可以选择从特征数据中删除 level_0 或 level_1 中的列之一:
>>> def correlated_columns(df, threshold=0.95):
... return (
... df.corr()
... .pipe(
... lambda df1: pd.DataFrame(
... np.tril(df1, k=-1),
... columns=df.columns,
... index=df.columns,
... )
... )
... .stack()
... .rename("pearson")
... .pipe(
... lambda s: s[
... s.abs() > threshold
... ].reset_index()
... )
... .query("level_0 not in level_1")
... )
>>> correlated_columns(X)
Empty DataFrame
Columns: [level_0, level_1, pearson]
Index: []
使用具有更多列的数据集,我们可以看到许多列之间存在相关性:
>>> c_df = correlated_columns(agg_df)
>>> c_df.style.format({"pearson": "{:.2f}"})
level_0 level_1 pearson
3 pclass_mean pclass 1.00
4 pclass_mean pclass_min 1.00
5 pclass_mean pclass_max 1.00
6 sibsp_mean sibsp_max 0.97
7 parch_mean parch_min 0.95
8 parch_mean parch_max 0.96
9 fare_mean fare 0.95
10 fare_mean fare_max 0.98
12 body_mean body_min 1.00
13 body_mean body_max 1.00
14 sex_male sex_female -1.00
15 embarked_S embarked_C -0.95
RadViz
RadViz 图将每个样本显示在一个圆圈上,特征显示在圆周上(见 图 6-12)。数值被标准化,你可以想象每个图像都有一个弹簧,根据数值将样本拉向它。
这是一种用于可视化目标间可分性的技术之一。
Yellowbrick 可以做到这一点:
>>> from yellowbrick.features import RadViz
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> rv = RadViz(
... classes=["died", "survived"],
... features=X.columns,
... )
>>> rv.fit(X, y)
>>> _ = rv.transform(X)
>>> rv.poof()
>>> fig.savefig("images/mlpr_0612.png", dpi=300)
图 6-12. Yellowbrick RadViz 图。
pandas 库也可以绘制 RadViz 图(见 图 6-13):
>>> from pandas.plotting import radviz
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> radviz(
... new_df, "target", ax=ax, colormap="PiYG"
... )
>>> fig.savefig("images/mlpr_0613.png", dpi=300)
图 6-13. Pandas RadViz 图。
平行坐标
对于多变量数据,您可以使用平行坐标图来直观地查看聚类情况(见 图 6-14 和 图 6-15)。
再次,这里是 Yellowbrick 版本:
>>> from yellowbrick.features import (
... ParallelCoordinates,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pc = ParallelCoordinates(
... classes=["died", "survived"],
... features=X.columns,
... )
>>> pc.fit(X, y)
>>> pc.transform(X)
>>> ax.set_xticklabels(
... ax.get_xticklabels(), rotation=45
... )
>>> pc.poof()
>>> fig.savefig("images/mlpr_0614.png", dpi=300)
图 6-14. Yellowbrick 平行坐标图。
还有一个 pandas 版本:
>>> from pandas.plotting import (
... parallel_coordinates,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> parallel_coordinates(
... new_df,
... "target",
... ax=ax,
... colormap="viridis",
... alpha=0.5,
... )
>>> ax.set_xticklabels(
... ax.get_xticklabels(), rotation=45
... )
>>> fig.savefig(
... "images/mlpr_0615.png",
... dpi=300,
... bbox_inches="tight",
... )
图 6-15. Pandas 平行坐标图。
第七章:数据预处理
本章将探讨使用此数据的常见预处理步骤:
>>> X2 = pd.DataFrame(
... {
... "a": range(5),
... "b": [-100, -50, 0, 200, 1000],
... }
... )
>>> X2
a b
0 0 -100
1 1 -50
2 2 0
3 3 200
4 4 1000
标准化
某些算法(如 SVM)在数据标准化时表现更好。每列的均值应为 0,标准偏差应为 1。Sklearn 提供了一个.fit_transform
方法,结合了.fit
和.transform
:
>>> from sklearn import preprocessing
>>> std = preprocessing.StandardScaler()
>>> std.fit_transform(X2)
array([[-1.41421356, -0.75995002],
[-0.70710678, -0.63737744],
[ 0\. , -0.51480485],
[ 0.70710678, -0.02451452],
[ 1.41421356, 1.93664683]])
拟合后,我们可以检查各种属性:
>>> std.scale_
array([ 1.41421356, 407.92156109])
>>> std.mean_
array([ 2., 210.])
>>> std.var_
array([2.000e+00, 1.664e+05])
这是一个 pandas 版本。请记住,如果用于预处理,您需要跟踪原始的均值和标准偏差。稍后用于预测的任何样本都需要使用这些相同的值进行标准化:
>>> X_std = (X2 - X2.mean()) / X2.std()
>>> X_std
a b
0 -1.264911 -0.679720
1 -0.632456 -0.570088
2 0.000000 -0.460455
3 0.632456 -0.021926
4 1.264911 1.732190
>>> X_std.mean()
a 4.440892e-17
b 0.000000e+00
dtype: float64
>>> X_std.std()
a 1.0
b 1.0
dtype: float64
fastai 库也实现了这一功能:
>>> X3 = X2.copy()
>>> from fastai.structured import scale_vars
>>> scale_vars(X3, mapper=None)
>>> X3.std()
a 1.118034
b 1.118034
dtype: float64
>>> X3.mean()
a 0.000000e+00
b 4.440892e-17
dtype: float64
缩放到范围
缩放到范围是将数据转换为 0 到 1 之间的值。限制数据的范围可能是有用的。但是,如果您有异常值,可能需要谨慎使用此方法:
>>> from sklearn import preprocessing
>>> mms = preprocessing.MinMaxScaler()
>>> mms.fit(X2)
>>> mms.transform(X2)
array([[0\. , 0\. ],
[0.25 , 0.04545],
[0.5 , 0.09091],
[0.75 , 0.27273],
[1\. , 1\. ]])
这是一个 pandas 版本:
>>> (X2 - X2.min()) / (X2.max() - X2.min())
a b
0 0.00 0.000000
1 0.25 0.045455
2 0.50 0.090909
3 0.75 0.272727
4 1.00 1.000000
虚拟变量
我们可以使用 pandas 从分类数据创建虚拟变量。这也称为独热编码或指示编码。如果数据是名义(无序)的,虚拟变量特别有用。pandas 中的get_dummies
函数为分类列创建多个列,每列中的值为 1 或 0,如果原始列有该值:
>>> X_cat = pd.DataFrame(
... {
... "name": ["George", "Paul"],
... "inst": ["Bass", "Guitar"],
... }
... )
>>> X_cat
name inst
0 George Bass
1 Paul Guitar
这是 pandas 版本。注意drop_first
选项可以用来消除一列(虚拟列中的一列是其他列的线性组合)。
>>> pd.get_dummies(X_cat, drop_first=True)
name_Paul inst_Guitar
0 0 0
1 1 1
pyjanitor 库还具有使用expand_column
函数拆分列的功能:
>>> X_cat2 = pd.DataFrame(
... {
... "A": [1, None, 3],
... "names": [
... "Fred,George",
... "George",
... "John,Paul",
... ],
... }
... )
>>> jn.expand_column(X_cat2, "names", sep=",")
A names Fred George John Paul
0 1.0 Fred,George 1 1 0 0
1 NaN George 0 1 0 0
2 3.0 John,Paul 0 0 1 1
如果我们有高基数名义数据,我们可以使用标签编码。这将在下一节介绍。
标签编码器
虚拟变量编码的替代方法是标签编码。这将把分类数据转换为数字。对于高基数数据非常有用。该编码器强加序数性,这可能是需要的也可能不需要的。它所占空间比独热编码少,而且一些(树)算法可以处理此编码。
标签编码器一次只能处理一列:
>>> from sklearn import preprocessing
>>> lab = preprocessing.LabelEncoder()
>>> lab.fit_transform(X_cat)
array([0,1])
如果您已经编码了值,则可以应用.inverse_transform
方法对其进行解码:
>>> lab.inverse_transform([1, 1, 0])
array(['Guitar', 'Guitar', 'Bass'], dtype=object)
您也可以使用 pandas 进行标签编码。首先,将列转换为分类列类型,然后从中提取数值代码。
此代码将从 pandas 系列创建新的数值数据。我们使用.as_ordered
方法确保分类是有序的:
>>> X_cat.name.astype(
... "category"
... ).cat.as_ordered().cat.codes + 1
0 1
1 2
dtype: int8
频率编码
处理高基数分类数据的另一种选择是频率编码。这意味着用训练数据中的计数替换类别的名称。我们将使用 pandas 来执行此操作。首先,我们将使用 pandas 的.value_counts
方法创建一个映射(将字符串映射到计数的 pandas 系列)。有了映射,我们可以使用.map
方法进行编码:
>>> mapping = X_cat.name.value_counts()
>>> X_cat.name.map(mapping)
0 1
1 1
Name: name, dtype: int64
确保存储训练映射,以便以后使用相同的数据对未来数据进行编码。
从字符串中提取类别
提高 Titanic 模型准确性的一种方法是从姓名中提取称号。找到最常见的三重组的一个快速方法是使用 Counter
类:
>>> from collections import Counter
>>> c = Counter()
>>> def triples(val):
... for i in range(len(val)):
... c[val[i : i + 3]] += 1
>>> df.name.apply(triples)
>>> c.most_common(10)
[(', M', 1282),
(' Mr', 954),
('r. ', 830),
('Mr.', 757),
('s. ', 460),
('n, ', 320),
(' Mi', 283),
('iss', 261),
('ss.', 261),
('Mis', 260)]
我们可以看到,“Mr.” 和 “Miss.” 非常常见。
另一个选项是使用正则表达式提取大写字母后跟小写字母和句点:
>>> df.name.str.extract(
... "([A-Za-z]+)\.", expand=False
... ).head()
0 Miss
1 Master
2 Miss
3 Mr
4 Mrs
Name: name, dtype: object
我们可以使用 .value_counts
查看这些的频率:
>>> df.name.str.extract(
... "([A-Za-z]+)\.", expand=False
... ).value_counts()
Mr 757
Miss 260
Mrs 197
Master 61
Dr 8
Rev 8
Col 4
Mlle 2
Ms 2
Major 2
Dona 1
Don 1
Lady 1
Countess 1
Capt 1
Sir 1
Mme 1
Jonkheer 1
Name: name, dtype: int64
注意
对正则表达式的完整讨论超出了本书的范围。此表达式捕获一个或多个字母字符的组。该组后面将跟随一个句点。
使用这些操作和 pandas,您可以创建虚拟变量或将低计数的列组合到其他类别中(或将它们删除)。
其他分类编码
categorical_encoding 库 是一组 scikit-learn 转换器,用于将分类数据转换为数值数据。该库的一个优点是它支持输出 pandas DataFrame(不像 scikit-learn 那样将它们转换为 numpy 数组)。
该库实现的一个算法是哈希编码器。如果您事先不知道有多少类别,或者正在使用词袋表示文本,这将会很有用。它将分类列哈希到 n_components
。如果您使用在线学习(可以更新模型),这将非常有用:
>>> import category_encoders as ce
>>> he = ce.HashingEncoder(verbose=1)
>>> he.fit_transform(X_cat)
col_0 col_1 col_2 col_3 col_4 col_5 col_6 col_7
0 0 0 0 1 0 1 0 0
1 0 2 0 0 0 0 0 0
序数编码器可以将具有顺序的分类列转换为单个数字列。在这里,我们将大小列转换为序数数字。如果在映射字典中找不到一个值,则使用 -1
的默认值:
>>> size_df = pd.DataFrame(
... {
... "name": ["Fred", "John", "Matt"],
... "size": ["small", "med", "xxl"],
... }
... )
>>> ore = ce.OrdinalEncoder(
... mapping=[
... {
... "col": "size",
... "mapping": {
... "small": 1,
... "med": 2,
... "lg": 3,
... },
... }
... ]
... )
>>> ore.fit_transform(size_df)
name size
0 Fred 1.0
1 John 2.0
2 Matt -1.0
此 参考 解释了 categorical_encoding 库的许多算法。
如果您有高基数数据(大量唯一值),考虑使用其中一个贝叶斯编码器,它们会为每个分类列输出单独的列。这些包括 TargetEncoder
, LeaveOneOutEncoder
, WOEEncoder
, JamesSteinEncoder
和 MEstimateEncoder
。
例如,要将 Titanic 生存列转换为目标的后验概率和给定标题(分类)信息的先验概率的混合,可以使用以下代码:
>>> def get_title(df):
... return df.name.str.extract(
... "([A-Za-z]+)\.", expand=False
... )
>>> te = ce.TargetEncoder(cols="Title")
>>> te.fit_transform(
... df.assign(Title=get_title), df.survived
... )["Title"].head()
0 0.676923
1 0.508197
2 0.676923
3 0.162483
4 0.786802
Name: Title, dtype: float64
日期特征工程
fastai 库有一个 add_datepart
函数,它将根据日期时间列生成日期属性列。这对大多数机器学习算法很有用,因为它们无法从日期的数值表示中推断出这种类型的信号:
>>> from fastai.tabular.transform import (
... add_datepart,
... )
>>> dates = pd.DataFrame(
... {
... "A": pd.to_datetime(
... ["9/17/2001", "Jan 1, 2002"]
... )
... }
... )
>>> add_datepart(dates, "A")
>>> dates.T
0 1
AYear 2001 2002
AMonth 9 1
AWeek 38 1
ADay 17 1
ADayofweek 0 1
ADayofyear 260 1
AIs_month_end False False
AIs_month_start False True
AIs_quarter_end False False
AIs_quarter_start False True
AIs_year_end False False
AIs_year_start False True
AElapsed 1000684800 1009843200
警告
add_datepart
会改变 DataFrame,这是 pandas 能做到的,但通常不这样做!
添加 col_na 特征
fastai 库曾经有一个函数用于创建一个列以填充缺失值(使用中位数)并指示缺失值。知道一个值是否缺失可能会有一些信号。以下是该函数的副本及其使用示例:
>>> from pandas.api.types import is_numeric_dtype
>>> def fix_missing(df, col, name, na_dict):
... if is_numeric_dtype(col):
... if pd.isnull(col).sum() or (
... name in na_dict
... ):
... df[name + "_na"] = pd.isnull(col)
... filler = (
... na_dict[name]
... if name in na_dict
... else col.median()
... )
... df[name] = col.fillna(filler)
... na_dict[name] = filler
... return na_dict
>>> data = pd.DataFrame({"A": [0, None, 5, 100]})
>>> fix_missing(data, data.A, "A", {})
{'A': 5.0}
>>> data
A A_na
0 0.0 False
1 5.0 True
2 5.0 False
3 100.0 False
下面是一个 pandas 版本:
>>> data = pd.DataFrame({"A": [0, None, 5, 100]})
>>> data["A_na"] = data.A.isnull()
>>> data["A"] = data.A.fillna(data.A.median())
手动特征工程
我们可以使用 pandas 生成新特征。对于 Titanic 数据集,我们可以添加聚合船舱数据(每个船舱的最大年龄、平均年龄等)。要获得每个船舱的聚合数据并将其合并回原始数据中,使用 pandas 的 .groupby
方法创建数据。然后使用 .merge
方法将其与原始数据对齐:
>>> agg = (
... df.groupby("cabin")
... .agg("min,max,mean,sum".split(","))
... .reset_index()
... )
>>> agg.columns = [
... "_".join(c).strip("_")
... for c in agg.columns.values
... ]
>>> agg_df = df.merge(agg, on="cabin")
如果你想总结“好”或“坏”列,你可以创建一个新列,该列是聚合列的总和(或另一个数学操作)。这在某种程度上是一种艺术,也需要理解数据。
第八章:特征选择
我们使用特征选择来选择对模型有用的特征。无关的特征可能会对模型产生负面影响。相关的特征可能会使回归中的系数(或者树模型中的特征重要性)不稳定或难以解释。
维度诅咒是另一个需要考虑的问题。随着数据维度的增加,数据变得更加稀疏。这可能使得除非有更多数据,否则很难提取信号。随着维度的增加,邻居计算的效用通常会减弱。
此外,训练时间通常是列数的函数(有时甚至比线性更差)。如果能够用简明和准确的列,可以在更短的时间内获得更好的模型。我们将通过上一章节的agg_df
数据集来演示一些示例。请记住,这是具有一些额外列(用于舱室信息)的泰坦尼克号数据集的聚合数值数据集。因为这个数据集正在聚合每个舱室的数值值,它将展示许多相关性。其他选项包括 PCA 和查看树分类器的.feature_importances_
。
共线性列
我们可以使用之前定义的correlated_columns
函数或运行以下代码,以查找具有 0.95 或更高相关系数的列:
>>> limit = 0.95
>>> corr = agg_df.corr()
>>> mask = np.triu(
... np.ones(corr.shape), k=1
... ).astype(bool)
>>> corr_no_diag = corr.where(mask)
>>> coll = [
... c
... for c in corr_no_diag.columns
... if any(abs(corr_no_diag[c]) > threshold)
... ]
>>> coll
['pclass_min', 'pclass_max', 'pclass_mean',
'sibsp_mean', 'parch_mean', 'fare_mean',
'body_max', 'body_mean', 'sex_male', 'embarked_S']
Yellowbrick 的Rank2
可视化器,如前所示,将绘制一个相关性热图。
rfpimp 包有一个多重共线性的可视化。plot_dependence_heatmap
函数为训练数据集中的每个数值列训练一个随机森林。依赖值是预测该列的袋外(OOB)估计的 R2 分数(见图 8-1)。
使用此图的建议方法是找到接近 1 的值。X 轴上的标签是预测 Y 轴标签的特征。如果一个特征预测另一个特征,可以移除被预测特征(Y 轴上的特征)。在我们的例子中,fare
预测pclass
、sibsp
、parch
和embarked_Q
。我们应该能够保留fare
并移除其他特征,从而获得类似的性能:
>>> rfpimp.plot_dependence_heatmap(
... rfpimp.feature_dependence_matrix(X_train),
... value_fontsize=12,
... label_fontsize=14,
... figsize=(8, 8),sn
... )
>>> fig = plt.gcf()
>>> fig.savefig(
... "images/mlpr_0801.png",
... dpi=300,
... bbox_inches="tight",
... )
图 8-1. 依赖热图。Pclass、sibsp、parch 和 embarked_Q 可以从 fare 中预测,因此我们可以移除它们。
下面的代码展示了,如果我们移除这些列,我们可以得到类似的分数:
>>> cols_to_remove = [
... "pclass",
... "sibsp",
... "parch",
... "embarked_Q",
... ]
>>> rf3 = RandomForestClassifier(random_state=42)
>>> rf3.fit(
... X_train[
... [
... c
... for c in X_train.columns
... if c not in cols_to_remove
... ]
... ],
... y_train,
... )
>>> rf3.score(
... X_test[
... [
... c
... for c in X_train.columns
... if c not in cols_to_remove
... ]
... ],
... y_test,
... )
0.7684478371501272
>>> rf4 = RandomForestClassifier(random_state=42)
>>> rf4.fit(X_train, y_train)
>>> rf4.score(X_test, y_test)
0.7659033078880407
Lasso 回归
如果使用 lasso 回归,可以设置一个alpha
参数作为正则化参数。随着值的增加,对不那么重要的特征给予较小的权重。在这里,我们使用LassoLarsCV
模型迭代各种 alpha 值,并跟踪特征系数(见图 8-2):
>>> from sklearn import linear_model
>>> model = linear_model.LassoLarsCV(
... cv=10, max_n_alphas=10
... ).fit(X_train, y_train)
>>> fig, ax = plt.subplots(figsize=(12, 8))
>>> cm = iter(
... plt.get_cmap("tab20")(
... np.linspace(0, 1, X.shape[1])
... )
... )
>>> for i in range(X.shape[1]):
... c = next(cm)
... ax.plot(
... model.alphas_,
... model.coef_path_.T[:, i],
... c=c,
... alpha=0.8,
... label=X.columns[i],
... )
>>> ax.axvline(
... model.alpha_,
... linestyle="-",
... c="k",
... label="alphaCV",
... )
>>> plt.ylabel("Regression Coefficients")
>>> ax.legend(X.columns, bbox_to_anchor=(1, 1))
>>> plt.xlabel("alpha")
>>> plt.title(
... "Regression Coefficients Progression for Lasso Paths"
... )
>>> fig.savefig(
... "images/mlpr_0802.png",
... dpi=300,
... bbox_inches="tight",
... )
图 8-2. 在 Lasso 回归过程中,随着 alpha 变化,特征的系数。
递归特征消除。
递归特征消除将删除最弱的特征,然后拟合一个模型(参见图 8-3)。它通过传入具有 .coef_
或 .feature_importances_
属性的 scikit-learn 模型来实现:
>>> from yellowbrick.features import RFECV
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> rfe = RFECV(
... ensemble.RandomForestClassifier(
... n_estimators=100
... ),
... cv=5,
... )
>>> rfe.fit(X, y)
>>> rfe.rfe_estimator_.ranking_
array([1, 1, 2, 3, 1, 1, 5, 4])
>>> rfe.rfe_estimator_.n_features_
4
>>> rfe.rfe_estimator_.support_
array([ True, True, False, False, True,
True, False, False])
>>> rfe.poof()
>>> fig.savefig("images/mlpr_0803.png", dpi=300)
图 8-3. 递归特征消除。
我们将使用递归特征消除来找出前 10 个最重要的特征。(在这个聚合数据集中,我们发现泄漏了生存列!)
>>> from sklearn.feature_selection import RFE
>>> model = ensemble.RandomForestClassifier(
... n_estimators=100
... )
>>> rfe = RFE(model, 4)
>>> rfe.fit(X, y)
>>> agg_X.columns[rfe.support_]
Index(['pclass', 'age', 'fare', 'sex_male'], dtype='object')
互信息。
Sklearn 提供了非参数测试,将使用 k 近邻算法来确定特征与目标之间的互信息。互信息量化了观察另一个变量带来的信息增益。该值为零或更高。如果值为零,则它们之间没有关系(参见图 8-4)。这个数字不受限制,并代表特征与目标之间共享的比特数:
>>> from sklearn import feature_selection
>>> mic = feature_selection.mutual_info_classif(
... X, y
... )
>>> fig, ax = plt.subplots(figsize=(10, 8))
>>> (
... pd.DataFrame(
... {"feature": X.columns, "vimp": mic}
... )
... .set_index("feature")
... .plot.barh(ax=ax)
... )
>>> fig.savefig("images/mlpr_0804.png")
图 8-4. 互信息图。
主成分分析。
特征选择的另一个选项是运行主成分分析。一旦获得了主要的主成分,就检查对它们贡献最大的特征。这些特征具有更大的方差。请注意,这是一种无监督算法,不考虑 y
。
查看“PCA”以获取更多细节。
特征重要性。
大多数树模型在训练后提供了 .feature_importances_
属性。更高的重要性通常意味着在将特征从模型中移除时存在更高的错误。有关各种树模型的详细信息,请参阅各章节。
第九章:不平衡类别
如果您正在对数据进行分类,并且类的大小不相对平衡,那么对更流行的类别的偏向可能会在模型中体现出来。例如,如果您有 1 个正例和 99 个负例,您可以通过将所有内容分类为负例获得 99%的准确率。处理不平衡类别的各种选项。
使用不同的度量标准
一个提示是使用除准确率之外的度量(AUC 是一个不错的选择)来校准模型。当目标大小不同时,精确度和召回率也是更好的选择。但是,还有其他考虑的选项。
基于树的算法和集成方法
基于树的模型可能根据较小类的分布表现更好。如果它们倾向于聚集,它们可以更容易地被分类。
集成方法可以进一步帮助提取少数类。装袋和提升是树模型(如随机森林和极端梯度增强(XGBoost))中的选项。
惩罚模型
许多 scikit-learn 分类模型支持class_weight
参数。将其设置为'balanced'
将尝试正则化少数类,并激励模型正确分类它们。或者,您可以进行网格搜索,并通过传递将类映射到权重的字典来指定权重选项(给较小类更高的权重)。
XGBoost库有max_delta_step
参数,可以设置为 1 到 10,使更新步骤更加保守。它还有scale_pos_weight
参数,用于设置负样本到正样本的比例(针对二元类)。此外,对于分类,eval_metric
应设置为'auc'
而不是默认值'error'
。
KNN 模型有一个weights
参数,可以偏向邻近的邻居。如果少数类样本靠近在一起,将此参数设置为'distance'
可能会提高性能。
上采样少数类
您可以通过几种方式增加少数类。以下是一个 sklearn 的实现:
>>> from sklearn.utils import resample
>>> mask = df.survived == 1
>>> surv_df = df[mask]
>>> death_df = df[~mask]
>>> df_upsample = resample(
... surv_df,
... replace=True,
... n_samples=len(death_df),
... random_state=42,
... )
>>> df2 = pd.concat([death_df, df_upsample])
>>> df2.survived.value_counts()
1 809
0 809
Name: survived, dtype: int64
我们还可以使用 imbalanced-learn 库进行随机替换的采样:
>>> from imblearn.over_sampling import (
... RandomOverSampler,
... )
>>> ros = RandomOverSampler(random_state=42)
>>> X_ros, y_ros = ros.fit_sample(X, y)
>>> pd.Series(y_ros).value_counts()
1 809
0 809
dtype: int64
生成少数类数据
imbalanced-learn 库还可以使用 Synthetic Minority Over-sampling Technique(SMOTE)和 Adaptive Synthetic(ADASYN)采样方法生成少数类的新样本。SMOTE 通过选择其 k 个最近邻之一,连接到其中之一,并沿着该线选择一个点来工作。ADASYN 与 SMOTE 类似,但会从更难学习的样本生成更多样本。imbanced-learn 中的类名为over_sampling.SMOTE
和over_sampling.ADASYN
。
下采样多数类
平衡类别的另一种方法是对多数类进行下采样。以下是一个 sklearn 的例子:
>>> from sklearn.utils import resample
>>> mask = df.survived == 1
>>> surv_df = df[mask]
>>> death_df = df[~mask]
>>> df_downsample = resample(
... death_df,
... replace=False,
... n_samples=len(surv_df),
... random_state=42,
... )
>>> df3 = pd.concat([surv_df, df_downsample])
>>> df3.survived.value_counts()
1 500
0 500
Name: survived, dtype: int64
提示
在进行下采样时不要使用替换。
imbalanced-learn 库还实现了各种下采样算法:
ClusterCentroids
此类使用 K-means 来合成具有质心的数据。
RandomUnderSampler
此类随机选择样本。
NearMiss
此类使用最近邻来进行下采样。
TomekLink
此类通过移除彼此接近的样本来进行下采样。
EditedNearestNeighbours
此类移除具有邻居不在大多数或完全相同类别中的样本。
RepeatedNearestNeighbours
此类重复调用 EditedNearestNeighbours
。
AllKNN
此类类似,但在下采样迭代期间增加了最近邻居的数量。
CondensedNearestNeighbour
此类选择要下采样的类的一个样本,然后迭代该类的其他样本,如果 KNN 不会误分类,则将该样本添加进去。
OneSidedSelection
此类移除噪声样本。
NeighbourhoodCleaningRule
此类使用 EditedNearestNeighbours
的结果,并对其应用 KNN。
InstanceHardnessThreshold
此类训练模型,然后移除概率低的样本。
所有这些类都支持 .fit_sample
方法。
先上采样再下采样
imbalanced-learn 库实现了 SMOTEENN
和 SMOTETomek
,它们都是先上采样然后再应用下采样来清理数据。
第十章:分类
分类是一种基于特征对样本进行标记的监督学习机制。监督学习意味着我们对于分类有标签,或者对于回归有数字,算法应该学习这些。
我们将在本章中查看各种分类模型。Sklearn 实现了许多常见且有用的模型。我们还将看到一些不在 sklearn 中的模型,但实现了 sklearn 接口。因为它们遵循相同的接口,所以很容易尝试不同系列的模型,并查看它们的性能如何。
在 sklearn 中,我们创建一个模型实例,并在训练数据和训练标签上调用 .fit
方法。现在,我们可以使用拟合模型调用.predict
方法(或.predict_proba
或.predict_log_proba
方法)。要评估模型,我们使用 .score
方法与测试数据和测试标签。
更大的挑战通常是将数据安排成与 sklearn 兼容的形式。数据(X
)应该是一个(m 行 n 列)的 numpy 数组(或 pandas DataFrame),其中每个样本数据都有 n 个特征(列)。标签(y
)是一个大小为 m 的向量(或 pandas series),其中每个样本都有一个值(类)。
.score
方法返回平均准确度,单独可能不足以评估分类器。我们将看到其他评估指标。
我们将研究许多模型,并讨论它们的效率,它们所需的预处理技术,如何防止过拟合以及模型是否支持结果的直观解释。
sklearn 类型模型实现的一般方法包括:
fit(X, y[, sample_weight])
拟合模型
predict(X)
预测类别
predict_log_proba(X)
预测对数概率
predict_proba(X)
预测概率
score(X, y[, sample_weight])
获得准确度
逻辑回归
逻辑回归通过使用逻辑函数来估计概率。(注意;尽管它的名字中有回归,但它用于分类。)这一直是大多数科学的标准分类模型。
以下是我们将为每个模型包含的一些模型特征:
运行时效率
如果不使用'liblinear'
求解器,则可以使用n_jobs
。
预处理数据
如果solver
设置为'sag'
或'saga'
,则标准化以确保收敛正常。可以处理稀疏输入。
防止过拟合
C
参数控制正则化。(较低的C
表示更多的正则化,较高表示较少。)可以指定penalty
为'l1'
或'l2'
(默认值)。
解释结果
拟合模型的.coef_
属性显示决策函数的系数。x 的一个单位变化会使对数几率比按系数变化。.intercept_
属性是基线条件的逆对数几率。
这是使用此模型的一个示例:
>>> from sklearn.linear_model import (
... LogisticRegression,
... )
>>> lr = LogisticRegression(random_state=42)
>>> lr.fit(X_train, y_train)
LogisticRegression(C=1.0, class_weight=None,
dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100,
multi_class='ovr', n_jobs=1, penalty='l2',
random_state=42, solver='liblinear',
tol=0.0001, verbose=0, warm_start=False)
>>> lr.score(X_test, y_test)
0.8040712468193384
>>> lr.predict(X.iloc[[0]])
array([1])
>>> lr.predict_proba(X.iloc[[0]])
array([[0.08698937, 0.91301063]])
>>> lr.predict_log_proba(X.iloc[[0]])
array([[-2.4419694 , -0.09100775]])
>>> lr.decision_function(X.iloc[[0]])
array([2.35096164])
实例参数:
penalty='l2'
惩罚范数,'l1'
或'l2'
。
dual=False
使用双重制定(仅适用于'l2'
和'liblinear'
)。
C=1.0
正浮点数。逆正则化强度。数值越小,正则化越强。
fit_intercept=True
为决策函数添加偏置。
intercept_scaling=1
如果fit_intercept
和'liblinear'
,则缩放截距。
max_iter=100
最大迭代次数。
multi_class='ovr'
对每个类使用一对其余,或对于'multinomial'
,训练一个类。
class_weight=None
字典或'balanced'
。
solver='liblinear'
'liblinear'
适用于小数据。'newton-cg'
、'sag'
、'saga'
和'lbfgs'
适用于多类数据。'liblinear'
和'saga'
只适用于'l1'
惩罚。其他适用于'l2'
。
tol=0.0001
停止容差。
verbose=0
如果非零整数,则显示详细信息。
warm_start=False
如果为True
,记住以前的拟合。
njobs=1
要使用的 CPU 数。-1
是全部。仅适用于multi_class='over'
且solver
不是'liblinear'
。
拟合后的属性:
coef_
决策函数系数
intercept_
决策函数的截距
n_iter_
迭代次数
截距是基准条件的对数几率。我们可以将其转换回百分比准确率(比例):
>>> lr.intercept_
array([-0.62386001])
使用逆 logit 函数,我们可以看到生存的基线是 34%:
>>> def inv_logit(p):
... return np.exp(p) / (1 + np.exp(p))
>>> inv_logit(lr.intercept_)
array([0.34890406])
我们可以检查系数。系数的逆 logit 给出正例的比例。在这种情况下,如果票价上涨,我们更有可能生存。如果性别是男性,我们更不可能生存:
>>> cols = X.columns
>>> for col, val in sorted(
... zip(cols, lr.coef_[0]),
... key=lambda x: x[1],
... reverse=True,
... ):
... print(
... f"{col:10}{val:10.3f} {inv_logit(val):10.3f}"
... )
fare 0.104 0.526
parch -0.062 0.485
sibsp -0.274 0.432
age -0.296 0.427
embarked_Q -0.504 0.377
embarked_S -0.507 0.376
pclass -0.740 0.323
sex_male -2.400 0.083
Yellowbrick 还可以可视化系数。此可视化器具有relative=True
参数,使最大值为 100(或-100),其他值为该百分比(见图 10-1):
>>> from yellowbrick.features.importances import (
... FeatureImportances,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> fi_viz = FeatureImportances(lr)
>>> fi_viz.fit(X, y)
>>> fi_viz.poof()
>>> fig.savefig("images/mlpr_1001.png", dpi=300)
图 10-1. 特征重要性(相对于最大绝对回归系数)。
朴素贝叶斯
朴素贝叶斯是一种概率分类器,假设数据特征之间独立。它在文本分类应用中很受欢迎,如捕捉垃圾邮件。该模型的一个优点是,因为它假设特征独立,所以可以用少量样本训练模型(缺点是不能捕捉特征之间的交互作用)。这个简单模型也可以处理具有许多特征的数据。因此,它作为一个良好的基准模型。
sklearn 中有三类:GaussianNB
、MultinomialNB
和BernoulliNB
。第一种假设高斯分布(具有正态分布的连续特征),第二种用于离散发生计数,第三种用于离散布尔特征。
此模型具有以下属性:
运行时效率
训练 O(Nd),其中 N 是训练样本数,d 是维度。测试 O(cd),其中 c 是类数。
预处理数据
假设数据是独立的。在删除共线列后,应该表现更好。对于连续数值数据,可能需要对数据进行分箱。高斯假设正态分布,可能需要转换数据以转换为正态分布。
防止过拟合
表现出高偏差和低方差(集成不会减少方差)。
解释结果
百分比是样本属于某一类的概率,基于先验概率。
这是使用该模型的示例:
>>> from sklearn.naive_bayes import GaussianNB
>>> nb = GaussianNB()
>>> nb.fit(X_train, y_train)
GaussianNB(priors=None, var_smoothing=1e-09)
>>> nb.score(X_test, y_test)
0.7837150127226463
>>> nb.predict(X.iloc[[0]])
array([1])
>>> nb.predict_proba(X.iloc[[0]])
array([[2.17472227e-08, 9.99999978e-01]])
>>> nb.predict_log_proba(X.iloc[[0]])
array([[-1.76437798e+01, -2.17472227e-08]])
实例参数:
priors=None
类别的先验概率。
var_smoothing=1e-9
添加到方差以进行稳定计算。
拟合后的属性:
class_prior_
类别的概率
class_count_
类别的计数
theta_
每类别每列的均值
sigma_
每类别每列的方差
epsilon_
对每个方差添加的附加值
提示
这些模型容易受到零概率问题的影响。如果尝试对没有训练数据的新样本进行分类,其概率将为零。一种解决方案是使用Laplace 平滑。Sklearn 使用 alpha
参数控制此功能,默认为 1
,并在 MultinomialNB
和 BernoulliNB
模型上启用平滑。
支持向量机
支持向量机(SVM)是一种算法,试图在不同类别之间拟合一条(或平面或超平面)线,以最大化从线到类别点的距离。通过这种方式,它试图找到类别之间的稳健分割。支持向量是分隔超平面边缘的点。
注意
sklearn 中有几种不同的 SVM 实现。SVC
使用 libsvm
库,而 LinearSVC
使用 liblinear
库。
还有 linear_model.SGDClassifier
,当使用默认的 loss
参数时,它实现了 SVM。本章将描述第一次实现。
SVM 通常表现良好,并且可以通过核技巧支持线性空间或非线性空间。核技巧是指我们可以通过最小化一个比实际映射点到新维度更容易计算的公式来在新维度中创建决策边界的想法。默认核函数是径向基函数('rbf'
),由 gamma
参数控制,并可以将输入空间映射到高维空间。
SVM 具有以下特性:
运行时效率
scikit-learn 的实现是 O(n⁴),因此很难扩展到大尺寸。使用线性核或 LinearSVC
模型可以提高运行时性能,但可能会降低准确性。增加 cache_size
参数可以将其降至 O(n³)。
预处理数据
该算法不具备尺度不变性。强烈建议对数据进行标准化。
防止过拟合
C
(惩罚参数)控制正则化。较小的值允许在超平面上有更小的间隔。较高的gamma
值倾向于过拟合训练数据。LinearSVC
模型支持loss
和penalty
参数以支持正则化。
解释结果
检查 .support_vectors_
,尽管这些很难解释。对于线性核,可以检查 .coef_
。
下面是使用 scikit-learn 的 SVM 实现的示例:
>>> from sklearn.svm import SVC
>>> svc = SVC(random_state=42, probability=True)
>>> svc.fit(X_train, y_train)
SVC(C=1.0, cache_size=200, class_weight=None,
coef0=0.0, decision_function_shape='ovr',
degree=3, gamma='auto', kernel='rbf',
max_iter=-1, probability=True, random_state=42,
shrinking=True, tol=0.001, verbose=False)
>>> svc.score(X_test, y_test)
0.8015267175572519
>>> svc.predict(X.iloc[[0]])
array([1])
>>> svc.predict_proba(X.iloc[[0]])
array([[0.15344656, 0.84655344]])
>>> svc.predict_log_proba(X.iloc[[0]])
array([[-1.87440289, -0.16658195]])
要获取概率,请使用 probability=True
,这会减慢模型的拟合速度。
这类似于感知器,但会找到最大间隔。如果数据不是线性可分的,它将最小化误差。或者,可以使用不同的核函数。
实例参数:
C=1.0
惩罚参数。值越小,决策边界越紧(更容易过拟合)。
cache_size=200
缓存大小(MB)。增加这个值可以提高在大数据集上的训练时间。
class_weight=None
字典或'balanced'
。使用字典为每个类设置C
。
coef0=0.0
多项式和 sigmoid 核的独立项。
decision_function_shape='ovr'
使用一对其余('ovr'
)或一对一。
degree=3
多项式核的次数。
gamma='auto'
核系数。可以是一个数字,'scale'
(0.22 版中的默认值,1 /(num features
* X.std()
)),或'auto'
(默认优先,1 / num features
)。较低的值会导致过拟合训练数据。
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_
支持向量
n_support_vectors_
每类支持向量的计数
coef_
系数(线性核)。
K-最近邻
K-最近邻(KNN)算法基于到一些训练样本的距离进行分类。这种算法族称为基于实例的学习,因为没有要学习的参数。该模型假设距离足以进行推断;否则,它对底层数据或其分布不做任何假设。
棘手的部分是选择合适的 k 值。此外,维度诅咒可能会妨碍距离度量,因为在高维空间中,最近邻和最远邻之间的差异很小。
最近邻模型具有以下属性:
运行时效率
训练 O(1),但需要存储数据。测试 O(Nd),其中 N 是训练样本的数量,d 是维度。
预处理数据
是的,当标准化时,基于距离的计算性能更好。
防止过拟合。
增加 n_neighbors
。对于 L1 或 L2 度量,修改 p
。
解释结果
解释 k 最近邻样本(使用 .kneighbors
方法)。如果可以解释这些邻居,它们可以解释你的结果。
这是使用模型的一个例子:
>>> from sklearn.neighbors import (
... KNeighborsClassifier,
... )
>>> knc = KNeighborsClassifier()
>>> knc.fit(X_train, y_train)
KNeighborsClassifier(algorithm='auto',
leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=5,
p=2, weights='uniform')
>>> knc.score(X_test, y_test)
0.7837150127226463
>>> knc.predict(X.iloc[[0]])
array([1])
>>> knc.predict_proba(X.iloc[[0]])
array([[0., 1.]])
属性:
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'
,在这种情况下,更接近的点具有更大的影响力。
距离度量包括:'euclidean'
、'manhattan'
、'chebyshev'
、'minkowski'
、'wminkowski'
、'seuclidean'
、'mahalanobis'
、'haversine'
、'hamming'
、'canberra'
、'braycurtis'
、'jaccard'
、'matching'
、'dice'
、'rogerstanimoto'
、'russellrao'
、'sokalmichener'
、'sokalsneath'
或一个可调用的(用户定义的)函数。
注意
如果 k 是偶数且邻居分开,结果取决于训练数据的顺序。
决策树
决策树就像去看医生,医生会问一系列问题来确定症状的原因。我们可以使用一个过程创建决策树,并提出一系列问题来预测目标类。此模型的优点包括对非数值数据的支持(在某些实现中),几乎不需要数据准备(无需缩放),支持处理非线性关系,揭示特征重要性并且易于解释。
用于创建的默认算法称为分类与回归树(CART)。它使用基尼不纯度或指数度量来构建决策。这通过循环特征并找到给出最低误分类概率的值来完成。
提示
默认值会导致一个完全生长(过拟合)的树。使用诸如 max_depth
和交叉验证来控制这一情况。
决策树具有以下特性:
运行时效率
对于创建,循环遍历每个 m 特征,并对所有 n 个样本进行排序,O(mn log n)。对于预测,您沿着树行走,O( height)。
预处理数据
缩放不是必需的。需要处理丢失的值并转换为数值。
防止过拟合
将 max_depth
设置为较低的数值,提高 min_impurity_decrease
。
解释结果
可以逐步浏览选择树。因为有步骤,树在处理线性关系时效果不佳(数字的微小变化可能导致不同路径)。该树还高度依赖于训练数据。微小变化可能改变整棵树。
这里是使用 scikit-learn 库的示例:
>>> from sklearn.tree import DecisionTreeClassifier
>>> dt = DecisionTreeClassifier(
... random_state=42, max_depth=3
... )
>>> dt.fit(X_train, y_train)
DecisionTreeClassifier(class_weight=None,
criterion='gini', 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')
>>> dt.score(X_test, y_test)
0.8142493638676844
>>> dt.predict(X.iloc[[0]])
array([1])
>>> dt.predict_proba(X.iloc[[0]])
array([[0.02040816, 0.97959184]])
>>> dt.predict_log_proba(X.iloc[[0]])
array([[-3.8918203 , -0.02061929]])
实例参数:
class_weight=None
字典中类的权重。'balanced'
将值设置为类频率的倒数比例。对于多类问题,需要一个字典列表,每个类别使用一对多(OVR)。
criterion='gini'
分裂函数,'gini'
或 'entropy'
。
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'
。
拟合后的属性:
classes_
类标签
feature_importances_
基尼重要性数组
n_classes_
类的数量
n_features_
特征数
tree_
底层树对象
使用此代码查看树(见 图 10-2):
>>> import pydotplus
>>> from io import StringIO
>>> from sklearn.tree import export_graphviz
>>> dot_data = StringIO()
>>> tree.export_graphviz(
... dt,
... out_file=dot_data,
... feature_names=X.columns,
... class_names=["Died", "Survived"],
... filled=True,
... )
>>> g = pydotplus.graph_from_dot_data(
... dot_data.getvalue()
... )
>>> g.write_png("images/mlpr_1002.png")
对于 Jupyter,使用:
from IPython.display import Image
Image(g.create_png())
图 10-2. 决策树。
dtreeviz 包 可帮助理解决策树的工作原理。它创建带有标记直方图的树,提供宝贵的见解(见 图 10-3)。这里是一个示例。在 Jupyter 中,我们可以直接显示 viz
对象。如果我们从脚本中工作,我们可以调用 .save
方法创建 PDF、SVG 或 PNG:
>>> viz = dtreeviz.trees.dtreeviz(
... dt,
... X,
... y,
... target_name="survived",
... feature_names=X.columns,
... class_names=["died", "survived"],
... )
>>> viz
图 10-3. dtreeviz 输出。
特征重要性显示基尼重要性(使用该特征减少错误):
>>> for col, val in sorted(
... zip(X.columns, dt.feature_importances_),
... key=lambda x: x[1],
... reverse=True,
... )[:5]:
... print(f"{col:10}{val:10.3f}")
sex_male 0.607
pclass 0.248
sibsp 0.052
fare 0.050
age 0.043
您还可以使用 Yellowbrick 来可视化特征重要性(见 图 10-4):
>>> from yellowbrick.features.importances import (
... FeatureImportances,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> fi_viz = FeatureImportances(dt)
>>> fi_viz.fit(X, y)
>>> fi_viz.poof()
>>> fig.savefig("images/mlpr_1004.png", dpi=300)
图 10-4. 决策树的特征重要性(基尼系数)(归一化到男性重要性)。
随机森林
随机森林是决策树的集成。它使用 装袋(bagging)来纠正决策树过拟合的倾向。通过在样本的随机子样本和数据的随机特征上训练多个树,可以降低方差。
因为它们训练在数据的子样本上,随机森林可以评估 OOB 错误并评估性能。它们还可以通过对所有树平均特征重要性来跟踪特征重要性。
理解 bagging 的直觉源自康多塞的 1785 年的一篇文章。其核心是,如果你要创建一个陪审团,你应该加入任何有超过 50%的机会提供正确裁决的人,并平均他们的决策。每次添加另一位成员(并且他们的选择过程是独立于其他人的),你都会得到更好的结果。
随机森林的理念是在训练数据的不同列上创建许多决策树的“森林”。如果每棵树有超过 50%的正确分类几率,应该合并它们的预测。随机森林既可用于分类也可用于回归,尽管最近已经不再流行梯度提升树。
它具有以下属性:
运行时效率
需要创建 j 棵随机树。这可以使用n_jobs
并行进行。每棵树的复杂度为 O(mn log n),其中 n 是样本数,m 是特征数。对于创建,遍历每个 m 个特征,并对所有 n 个样本排序,O(mn log n)。对于预测,遍历树 O(高度)。
预处理数据
不必要。
防止过拟合
增加更多的树(n_estimators
)。使用较低的max_depth
。
解释结果
支持特征重要性,但我们没有一个可以浏览的单个决策树。可以检查集成中的单个树。
这是一个例子:
>>> from sklearn.ensemble import (
... RandomForestClassifier,
... )
>>> rf = RandomForestClassifier(random_state=42)
>>> rf.fit(X_train, y_train)
RandomForestClassifier(bootstrap=True,
class_weight=None, criterion='gini',
max_depth=None, 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=1, oob_score=False,
random_state=42, verbose=0, warm_start=False)
>>> rf.score(X_test, y_test)
0.7862595419847328
>>> rf.predict(X.iloc[[0]])
array([1])
>>> rf.predict_proba(X.iloc[[0]])
array([[0., 1.]])
>>> rf.predict_log_proba(X.iloc[[0]])
array([[-inf, 0.]])
实例参数(这些选项反映了决策树):
bootstrap=True
构建树时的自举法。
class_weight=None
字典中类的权重。'balanced'
会设置为类频率的倒数比例。默认为每个类的值为 1。对于多类别,需要每个类的字典列表(OVR)。
criterion='gini'
分裂函数,'gini'
或'entropy'
。
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=1
用于拟合和预测的作业数。
oob_score=False
是否估算oob_score
。
random_state=None
随机种子。
verbose=0
冗余性。
warm_start=False
拟合新的森林或使用现有的森林。
拟合后的属性:
classes_
类标签。
feature_importances_
基尼重要性数组。
n_classes_
类的数量。
n_features_
特征数量。
oob_score_
OOB 分数。未在树中使用的每个观察的平均准确度。
特征重要性显示基尼重要性(使用该特征减少误差):
>>> for col, val in sorted(
... zip(X.columns, rf.feature_importances_),
... key=lambda x: x[1],
... reverse=True,
... )[:5]:
... print(f"{col:10}{val:10.3f}")
age 0.285
fare 0.268
sex_male 0.232
pclass 0.077
sibsp 0.059
提示
随机森林分类器通过确定每个特征的不纯度平均减少(也称为基尼重要性)来计算特征重要性。减少分类不确定性的特征得分较高。
如果特征在规模或分类列的基数上变化,则这些数字可能会有所偏差。排列重要性是一个更可靠的分数(其中每列的值被排列并测量准确度下降)。删除列重要性是一个更可靠的机制(其中删除每列并重新评估模型),但遗憾的是这需要为删除的每列创建一个新模型。请参阅 rfpimp
包中的 importances
函数:
>>> import rfpimp
>>> rf = RandomForestClassifier(random_state=42)
>>> rf.fit(X_train, y_train)
>>> rfpimp.importances(
... rf, X_test, y_test
... ).Importance
Feature
sex_male 0.155216
fare 0.043257
age 0.033079
pclass 0.027990
parch 0.020356
embarked_Q 0.005089
sibsp 0.002545
embarked_S 0.000000
Name: Importance, dtype: float64
XGBoost
虽然 sklearn 有一个 GradientBoostedClassifier
,但最好使用使用极限提升的第三方实现。这些通常提供更好的结果。
XGBoost 是 scikit-learn 之外的一个流行库。它创建一个弱树,然后“提升”后续的树以减少残差误差。它试图捕捉并解决任何错误中的模式,直到它们看起来是随机的。
XGBoost 具有以下特性:
运行效率
XGBoost 可并行化。使用 n_jobs
选项指示 CPU 数量。使用 GPU 可获得更好的性能。
预处理数据
树模型无需缩放。需要对分类数据进行编码。
防止过拟合
如果经过 N 轮后没有改善,则可以设置 early_stopping_rounds=N
参数停止训练。L1 和 L2 正则化分别由 reg_alpha
和 reg_lambda
控制。较高的数值更为保守。
解释结果
具有特征重要性。
XGBoost 在 .fit
方法中有一个额外的参数。early_stopping_rounds
参数可以与 eval_set
参数结合使用,告诉 XGBoost 如果在这么多提升轮之后评估指标没有改善,则停止创建树。eval_metric
也可以设置为以下之一:'rmse'
、'mae'
、'logloss'
、'error'
(默认)、'auc'
、'aucpr'
,以及自定义函数。
以下是使用该库的示例:
>>> import xgboost as xgb
>>> xgb_class = xgb.XGBClassifier(random_state=42)
>>> xgb_class.fit(
... X_train,
... y_train,
... early_stopping_rounds=10,
... eval_set=[(X_test, y_test)],
... )
XGBClassifier(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='binary:logistic', random_state=42,
reg_alpha=0, reg_lambda=1, scale_pos_weight=1,
seed=None, silent=True, subsample=1)
>>> xgb_class.score(X_test, y_test)
0.7862595419847328
>>> xgb_class.predict(X.iloc[[0]])
array([1])
>>> xgb_class.predict_proba(X.iloc[[0]])
array([[0.06732017, 0.93267983]], dtype=float32)
实例参数:
max_depth=3
最大深度。
learning_rate=0.1
提升(在 0 和 1 之间)的学习率(也称为 eta)。在每次提升步骤之后,新添加的权重会按此因子进行缩放。值越低,越保守,但也需要更多的树来收敛。在调用 .train
时,可以传递一个 learning_rates
参数,这是每一轮的速率列表(例如,[.1]*100 + [.05]*100
)。
n_estimators=100
轮次或提升树的数量。
silent=True
相反于冗长。在运行提升过程时是否打印消息。
objective='binary:logistic'
分类的学习任务或可调用对象。
booster='gbtree'
可以是 'gbtree'
、'gblinear'
或 'dart'
。
nthread=None
弃用。
n_jobs=1
要使用的线程数。
gamma=0
控制剪枝。范围是 0 到无穷大。进一步分割叶子所需的最小损失减少。较高的 gamma 更加保守。如果训练和测试分数发散,请插入一个较高的数字(大约为 10)。如果训练和测试分数接近,请使用较低的数字。
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 正则化(权重平方的根)鼓励小权重。增加以更加保守。
scale_pos_weight=1
负/正权重比例。
base_score=.5
初始预测。
seed=None
已弃用。
random_state=0
随机种子。
missing=None
missing
的解释值。None
表示 np.nan
。
importance_type='gain'
特征重要性类型:'gain'
、'weight'
、'cover'
、'total_gain'
或 'total_cover'
。
属性:
coef_
gblinear 学习器的系数
feature_importances_
gbtree 学习器的特征重要性
特征重要性是使用该特征的所有节点的平均增益:
>>> for col, val in sorted(
... zip(
... X.columns,
... xgb_class.feature_importances_,
... ),
... key=lambda x: x[1],
... reverse=True,
... )[:5]:
... print(f"{col:10}{val:10.3f}")
fare 0.420
age 0.309
pclass 0.071
sex_male 0.066
sibsp 0.050
XGBoost 可以绘制特征重要性(参见图 10-5)。它有一个 importance_type
参数。默认值是 "weight"
,即特征出现在树中的次数。也可以是 "gain"
,显示特征使用时的平均增益,或者是 "cover"
,即受到拆分影响的样本数:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> xgb.plot_importance(xgb_class, ax=ax)
>>> fig.savefig("images/mlpr_1005.png", dpi=300)
图 10-5. 特征重要性显示权重(特征在树中出现的次数)。
我们可以在 Yellowbrick 中绘制这个,它会归一化这些值(参见图 10-6):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> fi_viz = FeatureImportances(xgb_class)
>>> fi_viz.fit(X, y)
>>> fi_viz.poof()
>>> fig.savefig("images/mlpr_1006.png", dpi=300)
图 10-6. XGBoost 的 Yellowbrick 特征重要性(归一化为 100)。
XGBoost 提供了树的文本表示和图形表示。这是文本表示:
>>> booster = xgb_class.get_booster()
>>> print(booster.get_dump()[0])
0:[sex_male<0.5] yes=1,no=2,missing=1
1:[pclass<0.23096] yes=3,no=4,missing=3
3:[fare<-0.142866] yes=7,no=8,missing=7
7:leaf=0.132530
8:leaf=0.184
4:[fare<-0.19542] yes=9,no=10,missing=9
9:leaf=0.024598
10:leaf=-0.1459
2:[age<-1.4911] yes=5,no=6,missing=5
5:[sibsp<1.81278] yes=11,no=12,missing=11
11:leaf=0.13548
12:leaf=-0.15000
6:[pclass<-0.95759] yes=13,no=14,missing=13
13:leaf=-0.06666
14:leaf=-0.1487
叶子中的值是类 1 的分数。可以使用 logistic 函数将其转换为概率。如果决策落到叶子 7,则类 1 的概率为 53%。这是单棵树的分数。如果我们的模型有 100 棵树,您将对每个叶子值求和,并使用 logistic 函数获得概率:
>>> # score from first tree leaf 7
>>> 1 / (1 + np.exp(-1 * 0.1238))
0.5309105310475829
这是模型中第一棵树的图形版本(参见图 10-7):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> xgb.plot_tree(xgb_class, ax=ax, num_trees=0)
>>> fig.savefig("images/mlpr_1007.png", dpi=300)
图 10-7. XGBoost 的树形结构。
xgbfir 包 是建立在 XGBoost 之上的一个库。该库提供关于特征重要性的各种度量。独特之处在于,它提供关于列和列对的这些度量,因此您可以查看交互。此外,您还可以获取关于三元组(三列)交互的信息。
它提供的度量有:
Gain
每个特征或特征交互的总增益
FScore
每个特征或特征交互可能的分割数量
wFScore
每个特征或特征交互可能的分割数量,按照分割发生的概率加权
Average wFScore
wFScore
除以 FScore
Average Gain
Gain
除以 FScore
Expected Gain
每个特征或特征交互的总增益,按照收集增益的概率加权
接口只是将其导出到电子表格,因此我们将使用 pandas 将其读取回来。这里是列重要性:
>>> import xgbfir
>>> xgbfir.saveXgbFI(
... xgb_class,
... feature_names=X.columns,
... OutputXlsxFile="fir.xlsx",
... )
>>> pd.read_excel("/tmp/surv-fir.xlsx").head(3).T
0 1 2
Interaction sex_male pclass fare
Gain 1311.44 585.794 544.884
FScore 42 45 267
wFScore 39.2892 21.5038 128.33
Average wFScore 0.935458 0.477861 0.480636
Average Gain 31.2247 13.0177 2.04076
Expected Gain 1307.43 229.565 236.738
Gain Rank 1 2 3
FScore Rank 4 3 1
wFScore Rank 3 4 1
Avg wFScore Rank 1 5 4
Avg Gain Rank 1 2 4
Expected Gain Rank 1 3 2
Average Rank 1.83333 3.16667 2.5
Average Tree Index 32.2381 20.9778 51.9101
Average Tree Depth 0.142857 1.13333 1.50562
从这个表格中,我们看到 sex_male 在增益、平均 wFScore、平均增益和预期增益中排名较高,而 fare 在 FScore 和 wFScore 中占据前列。
让我们来看看列之间的交互对:
>>> pd.read_excel(
... "fir.xlsx",
... sheet_name="Interaction Depth 1",
... ).head(2).T
Interaction pclass|sex_male age|sex_male
Gain 2090.27 964.046
FScore 35 18
wFScore 14.3608 9.65915
Average wFScore 0.410308 0.536619
Average Gain 59.722 53.5581
Expected Gain 827.49 616.17
Gain Rank 1 2
FScore Rank 5 10
wFScore Rank 4 8
Avg wFScore Rank 8 5
Avg Gain Rank 1 2
Expected Gain Rank 1 2
Average Rank 3.33333 4.83333
Average Tree Index 18.9714 38.1111
Average Tree Depth 1 1.11111
在这里,我们看到排名前两位的交互包括 sex_male 列与 pclass 和 age 的组合。如果您只能使用两个特征来建模,您可能会选择 pclass 和 sex_male。
最后,让我们来看一下三元组:
>>> pd.read_excel(
... "fir.xlsx",
... sheet_name="Interaction Depth 2",
... ).head(1).T
0
Interaction fare|pclass|sex_male
Gain 2973.16
FScore 44
wFScore 8.92572
Average wFScore 0.202857
Average Gain 67.5719
Expected Gain 549.145
Gain Rank 1
FScore Rank 1
wFScore Rank 4
Avg wFScore Rank 21
Avg Gain Rank 3
Expected Gain Rank 2
Average Rank 5.33333
Average Tree Index 16.6591
Average Tree Depth 2
由于空间限制,这里只展示了第一个三元组,但是电子表格中还有更多:
>>> pd.read_excel(
... "/tmp/surv-fir.xlsx",
... sheet_name="Interaction Depth 2",
... )[["Interaction", "Gain"]].head()
Interaction Gain
0 fare|pclass|sex_male 2973.162529
1 age|pclass|sex_male 1621.945151
2 age|sex_male|sibsp 1042.320428
3 age|fare|sex_male 366.860828
4 fare|fare|sex_male 196.224791
使用 LightGBM 进行梯度增强
LightGBM 是 Microsoft 的一种实现。LightGBM 使用采样机制来处理连续值。这使得树的创建更快(比如 XGBoost),并减少了内存使用。
LightGBM 也是深度优先增长树(以叶子为基础而不是层次为基础)。因此,与使用 max_depth
控制过拟合不同,应使用 num_leaves
(其中此值 < 2^(max_depth
))。
注意
当前安装这个库需要编译器,并且比简单的 pip install
更复杂一些。
它具有以下特性:
运行效率
可以利用多个 CPU。通过使用分箱,比 XGBoost 快 15 倍。
预处理数据
对将分类列编码为整数(或 pandas 的 Categorical
类型)提供了一些支持,但是与独热编码相比,AUC 表现似乎有所下降。
防止过拟合
减小 num_leaves
。增加 min_data_in_leaf
。使用 min_gain_to_split
和 lambda_l1
或 lambda_l2
。
解释结果
可用的特征重要性。单棵树弱,往往难以解释。
下面是使用该库的一个示例:
>>> import lightgbm as lgb
>>> lgbm_class = lgb.LGBMClassifier(
... random_state=42
... )
>>> lgbm_class.fit(X_train, y_train)
LGBMClassifier(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)
>>> lgbm_class.score(X_test, y_test)
0.7964376590330788
>>> lgbm_class.predict(X.iloc[[0]])
array([1])
>>> lgbm_class.predict_proba(X.iloc[[0]])
array([[0.01637168, 0.98362832]])
实例参数:
boosting_type='gbdt'
可以是:'gbdt'
(梯度提升树),'rf'
(随机森林),'dart'
(Dropouts Meet Multiple Additive Regression Trees),或者'goss'
(基于梯度的单侧采样)。
class_weight=None
字典或者'balanced'
。在多类问题中,使用字典设置每个类标签的权重。对于二元问题,使用is_unbalance
或scale_pos_weight
。
colsample_bytree=1.0
范围(0, 1.0]。选择每个增强轮次的特征百分比。
importance_type='split'
如何计算特征重要性。'split'
表示特征使用次数。'gain'
表示特征分裂的总增益。
learning_rate=0.1
范围(0, 1.0]。增强学习的学习率。较小的值减缓过拟合,因为增强轮次的影响减少。较小的数字应该提供更好的性能,但会需要更多的num_iterations
。
max_depth=-1
最大树深度。-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
None
是分类器的'binary'
或'multiclass'
。可以是函数或字符串。
random_state=42
随机种子。
reg_alpha=0.0
L1 正则化(权重的均值)。增加以更加保守。
reg_lambda=0.0
L2 正则化(权重的平方根)。增加以更加保守。
silent=True
详细模式。
subsample=1.0
下一轮使用的样本比例。
subsample_for_bin=200000
创建箱子所需的样本。
subsample_freq=0
子采样频率。设置为 1 以启用。
基于'splits'
(产品使用次数)的特征重要性:
>>> for col, val in sorted(
... zip(cols, lgbm_class.feature_importances_),
... key=lambda x: x[1],
... reverse=True,
... )[:5]:
... print(f"{col:10}{val:10.3f}")
fare 1272.000
age 1182.000
sibsp 118.000
pclass 115.000
sex_male 110.000
LightGBM 库支持创建特征重要性图(见图 10-8)。默认基于'splits'
,即特征使用次数。如果要更改为'gain'
,可以指定'importance_type'
:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> lgb.plot_importance(lgbm_class, ax=ax)
>>> fig.tight_layout()
>>> fig.savefig("images/mlpr_1008.png", dpi=300)
图 10-8. LightGBM 的特征重要性拆分。
警告
截至版本 0.9,Yellowbrick 不支持与 LightGBM 一起创建特征重要性图。
我们还可以创建决策树(见图 10-9):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> lgb.plot_tree(lgbm_class, tree_index=0, ax=ax)
>>> fig.savefig("images/mlpr_1009.png", dpi=300)
图 10-9. LightGBM 树。
提示
在 Jupyter 中,使用以下命令查看树:
lgb.create_tree_digraph(lgbm_class)
TPOT
TPOT 使用遗传算法尝试不同的模型和集成方法。由于考虑了多个模型、预处理步骤以及这些模型的超参数和集成选项,因此可能需要几小时甚至几天才能运行。在典型的机器上,一代可能需要五分钟或更长时间才能运行。
其具有以下属性:
运行效率
可以花费几小时或几天。使用n_jobs=-1
可以使用所有 CPU。
预处理数据
您需要删除 NaN 和分类数据。
防止过拟合
理想情况下,结果应使用交叉验证以最小化过拟合。
解释结果
取决于结果。
这是使用该库的示例:
>>> from tpot import TPOTClassifier
>>> tc = TPOTClassifier(generations=2)
>>> tc.fit(X_train, y_train)
>>> tc.score(X_test, y_test)
0.7888040712468194
>>> tc.predict(X.iloc[[0]])
array([1])
>>> tc.predict_proba(X.iloc[[0]])
array([[0.07449919, 0.92550081]])
实例参数:
generations=100
运行的迭代次数。
population_size=100
遗传编程的种群大小。通常较大的大小性能更好,但需要更多内存和时间。
offspring_size=None
每代的后代数。默认为population_size
。
mutation_rate=.9
算法的变异率[0, 1]。默认为 0.9。
crossover_rate=.1
交叉率(在一代中繁殖多少个管道)。范围[0, 1]。默认为 0.1。
scoring='accuracy'
评分机制。使用 sklearn 字符串。
cv=5
交叉验证折数。
subsample=1
对训练实例进行子采样。范围[0, 1]。默认为 1。
n_jobs=1
使用的 CPU 数量,-1 表示所有核心。
max_time_mins=None
运行的最大分钟数。
max_eval_time_mins=5
评估单个管道的最大分钟数。
random_state=None
随机种子。
config_dict
优化的配置选项。
warm_start=False
重用以前的.fit
调用。
memory=None
可以缓存管道。'auto'
或路径会持久化到一个目录。
use_dask=False
使用 dask。
periodic_checkpoint_folder=None
最佳管道定期保存的文件夹路径。
early_stop=None
在运行这么多代没有改进后停止。
verbosity=0
0 = 无,1 = 最小,2 = 高,或 3 = 全部。2 及以上显示进度条。
disable_update_check=False
禁用版本检查。
属性:
evaluated_individuals_
包含所有评估过的管道的字典。
fitted_pipeline_
最佳管道。
完成后,可以导出管道:
>>> tc.export("tpot_exported_pipeline.py")
结果可能如下所示:
import numpy as np
import pandas as pd
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.model_selection import \
train_test_split
from sklearn.pipeline import make_pipeline, \
make_union
from sklearn.preprocessing import Normalizer
from tpot.builtins import StackingEstimator
# NOTE: Make sure that the class is labeled
# 'target' in the data file
tpot_data = pd.read_csv('PATH/TO/DATA/FILE',
sep='COLUMN_SEPARATOR', dtype=np.float64)
features = tpot_data.drop('target', axis=1).values
training_features, testing_features, \
training_target, testing_target = \
train_test_split(features,
tpot_data['target'].values, random_state=42)
# Score on the training set was:0.8122535043953432
exported_pipeline = make_pipeline(
Normalizer(norm="max"),
StackingEstimator(
estimator=ExtraTreesClassifier(bootstrap=True,
criterion="gini", max_features=0.85,
min_samples_leaf=2, min_samples_split=19,
n_estimators=100)),
ExtraTreesClassifier(bootstrap=False,
criterion="entropy", max_features=0.3,
min_samples_leaf=13, min_samples_split=9,
n_estimators=100)
)
exported_pipeline.fit(training_features,
training_target)
results = exported_pipeline.predict(
testing_features)
第十一章:模型选择
本章将讨论优化超参数,并且探讨模型是否需要更多数据以提升表现。
验证曲线
创建验证曲线是确定超参数合适值的一种方法。验证曲线是一个图表,展示模型性能如何随超参数数值变化而变化(见图 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)
图 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",
... )
图 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 库中的基于树的模型包括 .feature_``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")
图 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,
... )
图 13-4. 样本 20 的 Shapley 特征贡献。此图显示基值和推向死亡的特征。
您还可以可视化整个数据集的解释(将其旋转 90 度并沿 x 轴绘制)(见图 13-5):
>>> shap.force_plot(
... s.expected_value[1],
... shap_vals[1],
... feature_names=X_test.columns,
... )
图 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,
... )
图 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)
图 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,
... )
图 14-1. 特征重要性。这表明 RM(房间数量)增加价格,年龄并不重要,而 LSTAT(低收入人口比例)降低价格。
SVM
支持向量机也可以执行回归任务。
SVM 具有以下属性:
运行效率
scikit-learn 实现的时间复杂度为 O(n⁴),因此在大规模数据集上很难扩展。使用线性核或 LinearSVR
模型可以提高运行时性能,可能会牺牲一些准确性。增加 cache_size
参数可以将复杂度降低到 O(n³)。
预处理数据
该算法不是尺度不变的,因此强烈建议对数据进行标准化。
防止过拟合
C
(惩罚参数)控制正则化。较小的值允许较小的超平面间隔。较高的 gamma
值会导致过拟合训练数据。LinearSVR
模型支持 loss
和 penalty
参数进行正则化。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
图 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_alpha
和reg_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)
图 14-7. XGBoost 树。
LightGBM 回归
梯度提升树库 LightGBM 也支持回归。正如分类章节中提到的那样,由于用于确定节点分裂的抽样机制,它可能比 XGBoost 创建树更快。
它是按深度优先方式生成树的,因此限制深度可能会损害模型。它具有以下特性:
运行效率
可以利用多个 CPU。通过使用分箱,可以比 XGBoost 快 15 倍。
预处理数据
对将分类列编码为整数(或 pandas 的Categorical
类型)提供了一些支持,但与独热编码相比,AUC 似乎表现较差。
预防过拟合
降低num_leaves
。增加min_data_in_leaf
。使用lambda_l1
或lambda_l2
的min_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
模块包含用于评估回归模型的指标。以 loss
或 error
结尾的指标函数应该最小化。以 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)
图 15-4. 预测误差。绘制了预测的 y(y-hat)与实际 y 的图形。
第十六章:解释回归模型
大多数用于解释分类模型的技术同样适用于回归模型。在本章中,我将展示如何使用 SHAP 库解释回归模型。
我们将解释波士顿房屋数据集的 XGBoost 模型:
>>> import xgboost as xgb
>>> xgr = xgb.XGBRegressor(
... random_state=42, base_score=0.5
... )
>>> xgr.fit(bos_X_train, bos_y_train)
Shapley
我非常喜欢 Shapley 因为它对模型不可知。这个库还为我们提供了全局对模型的洞察,并帮助解释单个预测。如果你有一个黑盒模型,我发现它非常有用。
我们首先来看看索引 5 的预测。我们的模型预测值为 27.26:
>>> sample_idx = 5
>>> xgr.predict(bos_X.iloc[[sample_idx]])
array([27.269186], dtype=float32)
要使用模型,我们必须从我们的模型创建一个TreeExplainer
,并估算我们样本的 SHAP 值。如果我们想在 Jupyter 上使用交互界面,我们还需要调用initjs
函数:
>>> import shap
>>> shap.initjs()
>>> exp = shap.TreeExplainer(xgr)
>>> vals = exp.shap_values(bos_X)
有了解释器和 SHAP 值,我们可以创建一个力图来解释预测(见图 16-1)。这告诉我们基础预测值为 23,人口状态(LSTAT)和财产税率(TAX)将价格推高,而房间数(RM)将价格推低:
>>> shap.force_plot(
... exp.expected_value,
... vals[sample_idx],
... bos_X.iloc[sample_idx],
... )
图 16-1. 回归的力图。由于人口状态和税率,预期值从 23 增至 27。
我们也可以查看所有样本的力图,以获得整体行为的感觉。如果我们在 Jupyter 上使用交互式 JavaScript 模式,我们可以将鼠标悬停在样本上,查看影响结果的特征(见图 16-2):
>>> shap.force_plot(
... exp.expected_value, vals, bos_X
... )
图 16-2. 所有样本的回归力图。
从样本的力图中,我们看到 LSTAT 特征有很大的影响。为了可视化 LSTAT 如何影响结果,我们可以创建一个依赖图。库将自动选择一个特征进行着色(您可以提供interaction_index
参数来设置您自己的)。
从 LSTAT 的依赖图中(见图 16-3),我们可以看到随着 LSTAT 的增加(低社会地位人口的百分比),SHAP 值下降(推动目标向下)。非常低的 LSTAT 值会提升 SHAP。通过查看 TAX(财产税率)的颜色,我们可以看到随着税率的降低(更蓝色),SHAP 值上升:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> shap.dependence_plot("LSTAT", vals, bos_X)
>>> fig.savefig(
... "images/mlpr_1603.png",
... bbox_inches="tight",
... dpi=300,
... )
图 16-3. LSTAT 的依赖图。随着 LSTAT 的增加,预测值下降。
这里是另一个依赖图,在图 16-4 中展示了 DIS(到就业中心的距离)。看起来这个特征的影响很小,除非它非常小:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> shap.dependence_plot(
... "DIS", vals, bos_X, interaction_index="RM"
... )
>>> fig.savefig(
... "images/mlpr_1604.png",
... bbox_inches="tight",
... dpi=300,
... )
图 16-4. DIS 的依赖图。除非 DIS 非常小,否则 SHAP 保持相对平稳。
最后,我们将使用总结图来查看特征的全局效果(见 Figure 16-5)。顶部的特征对模型影响最大。从这个视角可以看出,RM(房间数)的大值显著提升了目标值,而中等和较小值则略微降低了它:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> shap.summary_plot(vals, bos_X)
>>> fig.savefig(
... "images/mlpr_1605.png",
... bbox_inches="tight",
... dpi=300,
... )
图 16-5. 总结图。最重要的特征位于顶部。
SHAP 库是您工具箱中的一个很好的工具。它帮助理解特征的全局影响,同时也有助于解释单个预测。
第十七章:降维
有许多技术将特征分解为较小的子集。这对于探索性数据分析、可视化、制作预测模型或聚类很有用。
在本章中,我们将使用各种技术探索泰坦尼克号数据集。我们将查看 PCA、UMAP、t-SNE 和 PHATE。
这是数据:
>>> ti_df = tweak_titanic(orig_df)
>>> std_cols = "pclass,age,sibsp,fare".split(",")
>>> X_train, X_test, y_train, y_test = get_train_test_X_y(
... ti_df, "survived", std_cols=std_cols
... )
>>> X = pd.concat([X_train, X_test])
>>> y = pd.concat([y_train, y_test])
主成分分析(PCA)
主成分分析(PCA)接受一个行(样本)和列(特征)的矩阵(X)。PCA 返回一个新的矩阵,其列是原始列的线性组合。这些线性组合最大化方差。
每列与其他列正交(成直角)。列按方差递减的顺序排序。
Scikit-learn 有这个模型的实现。在运行算法之前最好标准化数据。调用.fit
方法后,您将可以访问一个.explained_variance_ratio_
属性,列出每列中方差的百分比。
PCA 对于在二维(或三维)中可视化数据很有用。它还用作预处理步骤,以过滤数据中的随机噪声。它适合于找到全局结构,但不适合找到局部结构,并且在处理线性数据时表现良好。
在这个例子中,我们将在泰坦尼克号特征上运行 PCA。PCA 类是 scikit-learn 中的一个转换器;您调用.fit
方法来教它如何获取主成分,然后调用.transform
将矩阵转换为主成分矩阵:
>>> from sklearn.decomposition import PCA
>>> from sklearn.preprocessing import (
... StandardScaler,
... )
>>> pca = PCA(random_state=42)
>>> X_pca = pca.fit_transform(
... StandardScaler().fit_transform(X)
... )
>>> pca.explained_variance_ratio_
array([0.23917891, 0.21623078, 0.19265028,
0.10460882, 0.08170342, 0.07229959,
0.05133752, 0.04199068])
>>> pca.components_[0]
arrayarray([-0.63368693, 0.39682566,
0.00614498, 0.11488415, 0.58075352,
-0.19046812, -0.21190808, -0.09631388])
实例参数:
n_components=None
生成的组件数量。如果为None
,则返回与列数相同的组件数量。可以是一个浮点数(0, 1),那么将创建所需数量的组件以获得该方差比例。
copy=True
如果为True
,将在.fit
时改变数据。
whiten=False
转换后的白化数据以确保无关联的组件。
svd_solver='auto'
如果n_components
小于最小维度的 80%,则运行'randomized'
SVD(更快但是近似)。否则运行'full'
。
tol=0.0
对奇异值的容差。
iterated_power='auto'
'randomized'
svd_solver
的迭代次数。
random_state=None
'randomized'
svd_solver
的随机状态。
属性:
components_
主成分(原始特征的线性组合权重列)。
explained_variance_
每个组件的方差量。
explained_variance_ratio_
每个组件的方差量归一化(总和为 1)。
singular_values_
每个组件的奇异值。
mean_
每个特征的均值。
n_components_
当n_components
是浮点数时,这是组件的大小。
noise_variance_
估计的噪声协方差。
绘制解释方差比例的累积和被称为屏幕图(见图 17-1)。它将显示组件中存储了多少信息。您可以使用肘方法来确定使用多少个组件:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> ax.plot(pca.explained_variance_ratio_)
>>> ax.set(
... xlabel="Component",
... ylabel="Percent of Explained variance",
... title="Scree Plot",
... ylim=(0, 1),
... )
>>> fig.savefig(
... "images/mlpr_1701.png",
... dpi=300,
... bbox_inches="tight",
... )
图 17-1. PCA 屏幕图。
查看数据的另一种方法是使用累计图(见图 17-2)。我们的原始数据有 8 列,但从图中看来,如果仅使用 4 个 PCA 成分,我们可以保留大约 90% 的方差:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> ax.plot(
... np.cumsum(pca.explained_variance_ratio_)
... )
>>> ax.set(
... xlabel="Component",
... ylabel="Percent of Explained variance",
... title="Cumulative Variance",
... ylim=(0, 1),
... )
>>> fig.savefig("images/mlpr_1702.png", dpi=300)
图 17-2. PCA 累计解释方差。
特征对成分的影响有多大?使用 matplotlib 的 imshow
函数将成分沿 x 轴和原始特征沿 y 轴绘制出来(见图 17-3)。颜色越深,原始列对成分的贡献越大。
看起来第一个成分受到 pclass、age 和 fare 列的影响很大。(使用光谱色图(cmap
)强调非零值,并提供 vmin
和 vmax
为色条图例添加限制。)
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> plt.imshow(
... pca.components_.T,
... cmap="Spectral",
... vmin=-1,
... vmax=1,
... )
>>> plt.yticks(range(len(X.columns)), X.columns)
>>> plt.xticks(range(8), range(1, 9))
>>> plt.xlabel("Principal Component")
>>> plt.ylabel("Contribution")
>>> plt.title(
... "Contribution of Features to Components"
... )
>>> plt.colorbar()
>>> fig.savefig("images/mlpr_1703.png", dpi=300)
图 17-3. 主成分分析中的 PCA 特征。
另一种查看数据的方式是使用条形图(见图 17-4)。每个成分显示了来自原始数据的贡献:
>>> fig, ax = plt.subplots(figsize=(8, 4))
>>> pd.DataFrame(
... pca.components_, columns=X.columns
... ).plot(kind="bar", ax=ax).legend(
... bbox_to_anchor=(1, 1)
... )
>>> fig.savefig("images/mlpr_1704.png", dpi=300)
图 17-4. 主成分分析中的 PCA 特征。
如果我们有很多特征,可能希望通过仅显示满足最小权重要求的特征来限制上述图形。以下是找出前两个成分中具有至少 0.5 绝对值的所有特征的代码:
>>> comps = pd.DataFrame(
... pca.components_, columns=X.columns
... )
>>> min_val = 0.5
>>> num_components = 2
>>> pca_cols = set()
>>> for i in range(num_components):
... parts = comps.iloc[i][
... comps.iloc[i].abs() > min_val
... ]
... pca_cols.update(set(parts.index))
>>> pca_cols
{'fare', 'parch', 'pclass', 'sibsp'}
PCA 常用于以两个成分可视化高维数据集。在这里,我们在 2D 中可视化了 Titanic 的特征。它们根据生存状态进行了着色。有时可视化中可能会出现聚类。在这种情况下,似乎没有幸存者的聚类现象(见图 17-5)。
我们使用 Yellowbrick 生成此可视化:
>>> from yellowbrick.features.pca import (
... PCADecomposition,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> colors = ["rg"[j] for j in y]
>>> pca_viz = PCADecomposition(color=colors)
>>> pca_viz.fit_transform(X, y)
>>> pca_viz.poof()
>>> fig.savefig("images/mlpr_1705.png", dpi=300)
图 17-5. Yellowbrick PCA 绘图。
如果要根据一列着色散点图并添加图例(而不是色条图),则需要在 pandas 或 matplotlib 中循环每个颜色并单独绘制该组(或使用 seaborn)。下面我们还将纵横比设置为我们查看的成分解释方差的比率(见图 17-6)。因为第二个成分仅具有第一个成分的 90%,所以它会稍微短一些。
这是 seaborn 版本:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pca_df = pd.DataFrame(
... X_pca,
... columns=[
... f"PC{i+1}"
... for i in range(X_pca.shape[1])
... ],
... )
>>> pca_df["status"] = [
... ("deceased", "survived")[i] for i in y
... ]
>>> evr = pca.explained_variance_ratio_
>>> ax.set_aspect(evr[1] / evr[0])
>>> sns.scatterplot(
... x="PC1",
... y="PC2",
... hue="status",
... data=pca_df,
... alpha=0.5,
... ax=ax,
... )
>>> fig.savefig(
... "images/mlpr_1706.png",
... dpi=300,
... bbox_inches="tight",
... )
图 17-6. 带有图例和相对纵横比的 Seaborn PCA。
下面,我们通过在散点图上显示一个载荷图来增强可视化效果。这种图被称为双标图,因为它包含散点图和载荷(见图 17-7)。载荷显示特征的强度和它们的相关性。如果它们的角度接近,则它们可能相关。如果角度为 90 度,则它们可能不相关。最后,如果它们之间的角度接近 180 度,则它们具有负相关性:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pca_df = pd.DataFrame(
... X_pca,
... columns=[
... f"PC{i+1}"
... for i in range(X_pca.shape[1])
... ],
... )
>>> pca_df["status"] = [
... ("deceased", "survived")[i] for i in y
... ]
>>> evr = pca.explained_variance_ratio_
>>> x_idx = 0 # x_pc
>>> y_idx = 1 # y_pc
>>> ax.set_aspect(evr[y_idx] / evr[x_idx])
>>> x_col = pca_df.columns[x_idx]
>>> y_col = pca_df.columns[y_idx]
>>> sns.scatterplot(
... x=x_col,
... y=y_col,
... hue="status",
... data=pca_df,
... alpha=0.5,
... ax=ax,
... )
>>> scale = 8
>>> comps = pd.DataFrame(
... pca.components_, columns=X.columns
... )
>>> for idx, s in comps.T.iterrows():
... plt.arrow(
... 0,
... 0,
... s[x_idx] * scale,
... s[y_idx] * scale,
... color="k",
... )
... plt.text(
... s[x_idx] * scale,
... s[y_idx] * scale,
... idx,
... weight="bold",
... )
>>> fig.savefig(
... "images/mlpr_1707.png",
... dpi=300,
... bbox_inches="tight",
... )
图 17-7. Seaborn 双图绘图和加载图。
根据先前的树模型,我们知道年龄、票价和性别对乘客是否生存很重要。第一个主成分受 pclass、年龄和票价的影响,而第四个主成分受性别的影响。让我们将这些组件相互绘制。
同样,这个图是根据组件方差比例调整了绘图的纵横比(见图 17-8)。
此图似乎更准确地区分了幸存者:
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pca_df = pd.DataFrame(
... X_pca,
... columns=[
... f"PC{i+1}"
... for i in range(X_pca.shape[1])
... ],
... )
>>> pca_df["status"] = [
... ("deceased", "survived")[i] for i in y
... ]
>>> evr = pca.explained_variance_ratio_
>>> ax.set_aspect(evr[3] / evr[0])
>>> sns.scatterplot(
... x="PC1",
... y="PC4",
... hue="status",
... data=pca_df,
... alpha=0.5,
... ax=ax,
... )
>>> fig.savefig(
... "images/mlpr_1708.png",
... dpi=300,
... bbox_inches="tight",
... )
图 17-8. PCA 组件 1 与 4 的图示。
Matplotlib 可以创建漂亮的图表,但在交互图上不太实用。在执行 PCA 时,查看散点图通常很有用。我已经包含了一个使用 Bokeh 库 的函数,用于与散点图进行交互(见图 17-9)。它在 Jupyter 中运行良好:
>>> from bokeh.io import output_notebook
>>> from bokeh import models, palettes, transform
>>> from bokeh.plotting import figure, show
>>>
>>> def bokeh_scatter(
... x,
... y,
... data,
... hue=None,
... label_cols=None,
... size=None,
... legend=None,
... alpha=0.5,
... ):
... """
... x - x column name to plot
... y - y column name to plot
... data - pandas DataFrame
... hue - column name to color by (numeric)
... legend - column name to label by
... label_cols - columns to use in tooltip
... (None all in DataFrame)
... size - size of points in screen space unigs
... alpha - transparency
... """
... output_notebook()
... circle_kwargs = {}
... if legend:
... circle_kwargs["legend"] = legend
... if size:
... circle_kwargs["size"] = size
... if hue:
... color_seq = data[hue]
... mapper = models.LinearColorMapper(
... palette=palettes.viridis(256),
... low=min(color_seq),
... high=max(color_seq),
... )
... circle_kwargs[
... "fill_color"
... ] = transform.transform(hue, mapper)
... ds = models.ColumnDataSource(data)
... if label_cols is None:
... label_cols = data.columns
... tool_tips = sorted(
... [
... (x, "@{}".format(x))
... for x in label_cols
... ],
... key=lambda tup: tup[0],
... )
... hover = models.HoverTool(
... tooltips=tool_tips
... )
... fig = figure(
... tools=[
... hover,
... "pan",
... "zoom_in",
... "zoom_out",
... "reset",
... ],
... toolbar_location="below",
... )
...
... fig.circle(
... x,
... y,
... source=ds,
... alpha=alpha,
... **circle_kwargs
... )
... show(fig)
... return fig
>>> res = bokeh_scatter(
... "PC1",
... "PC2",
... data=pca_df.assign(
... surv=y.reset_index(drop=True)
... ),
... hue="surv",
... size=10,
... legend="surv",
... )
图 17-9. 带有工具提示的 Bokeh 散点图。
Yellowbrick 也可以在三维中绘图(见图 17-10):
>>> from yellowbrick.features.pca import (
... PCADecomposition,
... )
>>> colors = ["rg"[j] for j in y]
>>> pca3_viz = PCADecomposition(
... proj_dim=3, color=colors
... )
>>> pca3_viz.fit_transform(X, y)
>>> pca3_viz.finalize()
>>> fig = plt.gcf()
>>> plt.tight_layout()
>>> fig.savefig(
... "images/mlpr_1710.png",
... dpi=300,
... bbox_inches="tight",
... )
图 17-10. Yellowbrick 3D PCA。
scprep 库(这是 PHATE 库的依赖项,我们稍后会讨论)具有一个有用的绘图函数。rotate_scatter3d
函数可以生成一个在 Jupyter 中动画显示的图形(见图 17-11)。这使得理解 3D 图形变得更容易。
您可以使用此库可视化任何 3D 数据,而不仅限于 PHATE:
>>> import scprep
>>> scprep.plot.rotate_scatter3d(
... X_pca[:, :3],
... c=y,
... cmap="Spectral",
... figsize=(8, 6),
... label_prefix="Principal Component",
... )
图 17-11. scprep 3D PCA 动画。
如果您在 Jupyter 中将 matplotlib 的单元格魔术模式更改为 notebook
,您可以从 matplotlib 获取交互式 3D 绘图(见图 17-12)。
>>> from mpl_toolkits.mplot3d import Axes3D
>>> fig = plt.figure(figsize=(6, 4))
>>> ax = fig.add_subplot(111, projection="3d")
>>> ax.scatter(
... xs=X_pca[:, 0],
... ys=X_pca[:, 1],
... zs=X_pca[:, 2],
... c=y,
... cmap="viridis",
... )
>>> ax.set_xlabel("PC 1")
>>> ax.set_ylabel("PC 2")
>>> ax.set_zlabel("PC 3")
图 17-12. Matplotlib 在笔记本模式下的交互式 3D PCA。
警告
请注意,从:
% matplotlib inline
转到:
% matplotlib notebook
有时可能会导致 Jupyter 停止响应。请谨慎使用。
UMAP
统一流形逼近和投影 (UMAP) 是一种使用流形学习的降维技术。因此,它倾向于将相似的项目在拓扑上保持在一起。它试图同时保留全局和局部结构,与偏好局部结构的 t-SNE 相反(详见“t-SNE”)。
Python 实现不支持多核。
特征归一化是将值放在相同尺度上的一个好主意。
UMAP 对超参数(n_neighbors
、min_dist
、n_components
或 metric
)非常敏感。以下是一些示例:
>>> import umap
>>> u = umap.UMAP(random_state=42)
>>> X_umap = u.fit_transform(
... StandardScaler().fit_transform(X)
... )
>>> X_umap.shape
(1309, 2)
实例参数:
n_neighbors=15
本地邻域大小。较大意味着使用全局视图,较小意味着更局部。
n_components=2
嵌入的维度数。
metric='euclidean'
用于距离的度量。可以是一个接受两个 1D 数组并返回浮点数的函数。
n_epochs=None
训练时期的数量。默认为 200 或 500(取决于数据大小)。
learning_rate=1.0
嵌入优化的学习率。
init='spectral'
初始化类型。谱嵌入是默认值。可以是'random'
或 numpy 数组的位置。
min_dist=0.1
0 到 1 之间。嵌入点之间的最小距离。较小意味着更多聚集,较大意味着分散。
spread=1.0
确定嵌入点的距离。
set_op_mix_ratio=1.0
0 到 1 之间:模糊联合(1)或模糊交集(0)。
local_connectivity=1.0
本地连接的邻居数。增加此值会创建更多的局部连接。
repulsion_strength=1.0
排斥强度。较高的值给负样本更多权重。
negative_sample_rate=5
负样本每个正样本。更高的值具有更多的排斥力,更多的优化成本和更好的准确性。
transform_queue_size=4.0
用于最近邻搜索的侵略性。较高的值是较低的性能但更好的准确性。
a=None
参数来控制嵌入。如果等于None
,UMAP 则从min_dist
和spread
中确定这些值。
b=None
参数来控制嵌入。如果等于None
,UMAP 则从min_dist
和spread
中确定这些值。
random_state=None
随机种子。
metric_kwds=None
如果函数用于metric
,则用于额外参数的指标字典。也可以使用minkowski
(和其他指标)来参数化。
angular_rp_forest=False
使用角度随机投影。
target_n_neighbors=-1
简易设置的邻居数。
target_metric='categorical'
用于使用监督降维。也可以是'L1'
或'L2'
。还支持一个接受X
中两个数组作为输入并返回它们之间距离值的函数。
target_metric_kwds=None
如果target_metric
的函数被使用,使用的指标字典。
target_weight=0.5
权重因子。介于 0.0 和 1.0 之间,其中 0 表示仅基于数据,而 1 表示仅基于目标。
transform_seed=42
变换操作的随机种子。
verbose=False
冗余性。
属性:
embedding_
嵌入结果
让我们可视化泰坦尼克号数据集上 UMAP 的默认结果(参见图 17-13):
>>> fig, ax = plt.subplots(figsize=(8, 4))
>>> pd.DataFrame(X_umap).plot(
... kind="scatter",
... x=0,
... y=1,
... ax=ax,
... c=y,
... alpha=0.2,
... cmap="Spectral",
... )
>>> fig.savefig("images/mlpr_1713.png", dpi=300)
图 17-13. UMAP 结果。
要调整 UMAP 结果,首先关注n_neighbors
和min_dist
超参数。以下是更改这些值的示例(见图 17-14 和 17-15):
>>> X_std = StandardScaler().fit_transform(X)
>>> fig, axes = plt.subplots(2, 2, figsize=(6, 4))
>>> axes = axes.reshape(4)
>>> for i, n in enumerate([2, 5, 10, 50]):
... ax = axes[i]
... u = umap.UMAP(
... random_state=42, n_neighbors=n
... )
... X_umap = u.fit_transform(X_std)
...
... pd.DataFrame(X_umap).plot(
... kind="scatter",
... x=0,
... y=1,
... ax=ax,
... c=y,
... cmap="Spectral",
... alpha=0.5,
... )
... ax.set_title(f"nn={n}")
>>> plt.tight_layout()
>>> fig.savefig("images/mlpr_1714.png", dpi=300)
图 17-14. 调整 UMAP 结果n_neighbors
。
>>> fig, axes = plt.subplots(2, 2, figsize=(6, 4))
>>> axes = axes.reshape(4)
>>> for i, n in enumerate([0, 0.33, 0.66, 0.99]):
... ax = axes[i]
... u = umap.UMAP(random_state=42, min_dist=n)
... X_umap = u.fit_transform(X_std)
... pd.DataFrame(X_umap).plot(
... kind="scatter",
... x=0,
... y=1,
... ax=ax,
... c=y,
... cmap="Spectral",
... alpha=0.5,
... )
... ax.set_title(f"min_dist={n}")
>>> plt.tight_layout()
>>> fig.savefig("images/mlpr_1715.png", dpi=300)
图 17-15. 调整 UMAP 结果min_dist
。
有时在 UMAP 之前执行 PCA 以减少维数并加快计算速度。
t-SNE
t-分布随机邻域嵌入(t-SNE)技术是一种可视化和降维技术。它使用输入的分布和低维嵌入,并最小化它们之间的联合概率。由于计算量大,可能无法在大数据集上使用这种技术。
t-SNE 的一个特征是对超参数非常敏感。此外,虽然它能够很好地保留局部聚类,但全局信息并未保留。最后,这不是一个确定性算法,可能不会收敛。
在使用此技术之前标准化数据是一个好主意:
>>> from sklearn.manifold import TSNE
>>> X_std = StandardScaler().fit_transform(X)
>>> ts = TSNE()
>>> X_tsne = ts.fit_transform(X_std)
实例参数:
n_components=2
嵌入的维度数。
perplexity=30.0
建议的取值范围为 5 到 50。较小的数值倾向于形成更紧密的聚类。
early_exaggeration=12.0
控制簇紧密度和它们之间的间距。较大的值意味着较大的间距。
learning_rate=200.0
通常在 10 到 1000 之间。如果数据看起来像一个球,则降低它。如果数据看起来压缩,则增加它。
n_iter=1000
迭代次数。
n_iter_without_progress=300
如果在这些迭代次数之后没有进展,则中止。
min_grad_norm=1e-07
如果梯度范数低于此值,则优化停止。
metric='euclidean'
来自scipy.spatial.distance.pdist
、pairwise.PAIRWISE_DISTANCE_METRIC
或函数的距离度量。
init='random'
嵌入初始化。
verbose=0
冗长性。
random_state=None
随机种子。
method='barnes_hut'
梯度计算算法。
angle=0.5
用于梯度计算。小于 0.2 会增加运行时间。大于 0.8 会增加错误。
属性:
embedding_
嵌入向量。
kl_divergence_
Kullback-Leibler 散度。
n_iter_
迭代次数。
这里展示了使用 matplotlib 进行 t-SNE 结果的可视化(见图 17-16):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> colors = ["rg"[j] for j in y]
>>> scat = ax.scatter(
... X_tsne[:, 0],
... X_tsne[:, 1],
... c=colors,
... alpha=0.5,
... )
>>> ax.set_xlabel("Embedding 1")
>>> ax.set_ylabel("Embedding 2")
>>> fig.savefig("images/mlpr_1716.png", dpi=300)
图 17-16。使用 matplotlib 进行 t-SNE 结果。
改变perplexity
的值可能会对绘图产生重大影响(见图 17-17)。
>>> fig, axes = plt.subplots(2, 2, figsize=(6, 4))
>>> axes = axes.reshape(4)
>>> for i, n in enumerate((2, 30, 50, 100)):
... ax = axes[i]
... t = TSNE(random_state=42, perplexity=n)
... X_tsne = t.fit_transform(X)
... pd.DataFrame(X_tsne).plot(
... kind="scatter",
... x=0,
... y=1,
... ax=ax,
... c=y,
... cmap="Spectral",
... alpha=0.5,
... )
... ax.set_title(f"perplexity={n}")
... plt.tight_layout()
... fig.savefig("images/mlpr_1717.png", dpi=300)
图 17-17。改变t-SNE
的perplexity
。
PHATE
通过 Affinity-based Trajectory Embedding(PHATE)进行热扩散的潜力是高维数据可视化的工具。它倾向于同时保留全局结构(如 PCA)和局部结构(如 t-SNE)。
PHATE 首先编码局部信息(接近的点应该保持接近)。它使用“扩散”来发现全局数据,然后减少维度:
>>> import phate
>>> p = phate.PHATE(random_state=42)
>>> X_phate = p.fit_transform(X)
>>> X_phate.shape
实例参数:
n_components=2
维度数。
knn=5
核的邻居数。如果嵌入是断开的或数据集大于 10 万个样本,则增加。
decay=40
核的衰减率。降低此值会增加图的连通性。
n_landmark=2000
用于标记的地标点。
t='auto'
扩散力度。对数据进行平滑处理。如果嵌入缺乏结构,请增加;如果结构紧密而紧凑,请减少。
gamma=1
对数潜力(在 -1 到 1 之间)。如果嵌入集中在单个点周围,请尝试将其设置为 0。
n_pca=100
用于邻域计算的主成分数。
knn_dist='euclidean'
KNN 距离度量。
mds_dist='euclidean'
多维缩放(MDS)度量。
mds='metric'
MDS 算法用于降维。
n_jobs=1
要使用的 CPU 数量。
random_state=None
随机种子。
verbose=1
冗余性。
属性(请注意这些后面没有 _
):
X
输入数据
embedding
嵌入空间
diff_op
扩散算子
graph
基于输入构建的 KNN 图
这是使用 PHATE 的一个示例(见 Figure 17-18):
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> phate.plot.scatter2d(p, c=y, ax=ax, alpha=0.5)
>>> fig.savefig("images/mlpr_1718.png", dpi=300)
图 17-18. PHATE 结果。
如上所述的实例参数中,有一些参数可以调整以改变模型的行为。以下是调整 knn
参数的示例(见 Figure 17-19)。请注意,如果使用 .set_params
方法,它将加快计算速度,因为它使用预计算的图和扩散算子:
>>> fig, axes = plt.subplots(2, 2, figsize=(6, 4))
>>> axes = axes.reshape(4)
>>> p = phate.PHATE(random_state=42, n_jobs=-1)
>>> for i, n in enumerate((2, 5, 20, 100)):
... ax = axes[i]
... p.set_params(knn=n)
... X_phate = p.fit_transform(X)
... pd.DataFrame(X_phate).plot(
... kind="scatter",
... x=0,
... y=1,
... ax=ax,
... c=y,
... cmap="Spectral",
... alpha=0.5,
... )
... ax.set_title(f"knn={n}")
... plt.tight_layout()
... fig.savefig("images/mlpr_1719.png", dpi=300)
图 17-19. 更改 PHATE 的 knn
参数。
第十八章:聚类
聚类是一种无监督机器学习技术,用于将群体分成几个组。它是无监督的,因为我们没有给模型任何标签;它只是检查特征并确定哪些样本相似并属于一个簇。在本章中,我们将研究 K 均值和层次聚类方法。我们还将再次使用各种技术探索泰坦尼克号数据集。
K 均值
K 均值算法需要用户选择簇的数量或“k”。然后它随机选择 k 个质心,并根据从质心到每个样本的距离度量将每个样本分配给一个簇。分配完成后,它根据分配给一个标签的每个样本的中心重新计算质心。然后它根据新的质心再次将样本分配到簇中。经过几次迭代后,它应该会收敛。
因为聚类使用距离度量来确定哪些样本相似,所以行为可能会根据数据的规模而变化。您可以标准化数据并使所有特征处于相同的比例。有些人建议,如果规模提示某些特征更重要,SME 可能会建议不要标准化。我们将在这个例子中对数据进行标准化。
在这个例子中,我们将对泰坦尼克号乘客进行聚类。我们将从两个簇开始,看看聚类是否能够分开生存(我们不会将生存数据泄漏到聚类中,只使用X
,而不是y
)。
无监督算法具有.fit
方法和.predict
方法。我们只将X
传递给.fit
:
>>> from sklearn.cluster import KMeans
>>> X_std = preprocessing.StandardScaler().fit_transform(
... X
... )
>>> km = KMeans(2, random_state=42)
>>> km.fit(X_std)
KMeans(algorithm='auto', copy_x=True,
init='k-means', max_iter=300,
n_clusters=2, n_init=10, n_jobs=1,
precompute_distances='auto',
random_state=42, tol=0.0001, verbose=0)
在模型训练后,我们可以调用.predict
方法将新样本分配给一个簇:
>>> X_km = km.predict(X)
>>> X_km
array([1, 1, 1, ..., 1, 1, 1], dtype=int32)
实例参数:
n_clusters=8
创建的簇的数量。
init='kmeans++'
初始化方法。
n_init=10
使用不同质心运行算法的次数。最佳得分将获胜。
max_iter=300
运行的迭代次数。
tol=0.0001
收敛的公差。
precompute_distances='auto'
预计算距离(需要更多内存但更快)。如果n_samples
* n_clusters
小于或等于 1200 万,auto
将预先计算。
verbose=0
冗余性。
random_state=None
随机种子。
copy_x=True
在计算之前复制数据。
n_jobs=1
要使用的 CPU 数量。
algorithm='auto'
K 均值算法。'full'
适用于稀疏数据,但'elkan'
更高效。'auto'
在密集数据中使用'elkan'
。
属性:
cluster_centers_
中心的坐标
labels_
样本的标签
inertia_
到聚类质心的平方距离之和
n_iter_
迭代次数
如果事先不知道需要多少个簇,可以以一系列大小运行算法并评估各种指标。这可能有些棘手。
您可以使用 .inertia_
计算自己的肘部图。寻找曲线弯曲的位置,因为那可能是选择聚类数量的一个良好选择。在这种情况下,曲线很平滑,但在八个之后似乎没有太多改善(见 Figure 18-1)。
对于没有肘部的图,我们有几个选择。我们可以使用其他指标,其中一些如下所示。我们还可以检查聚类的可视化,看看聚类是否可见。我们可以向数据添加特征,看看是否有帮助进行聚类。
这里是肘部图的代码:
>>> inertias = []
>>> sizes = range(2, 12)
>>> for k in sizes:
... k2 = KMeans(random_state=42, n_clusters=k)
... k2.fit(X)
... inertias.append(k2.inertia_)
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pd.Series(inertias, index=sizes).plot(ax=ax)
>>> ax.set_xlabel("K")
>>> ax.set_ylabel("Inertia")
>>> fig.savefig("images/mlpr_1801.png", dpi=300)
图 18-1. 看起来相当平滑的肘部图。
当地面真实标签未知时,Scikit-learn 具有其他聚类指标。我们也可以计算并绘制这些指标。轮廓系数 是介于 -1 和 1 之间的值。得分越高越好。1 表示紧密的聚类,0 表示重叠的聚类。根据这个度量,两个聚类给我们带来了最佳分数。
Calinski-Harabasz 指数 是介于类间离散度和类内离散度之间的比率。分数越高越好。对于这个指标,两个聚类给出了最佳分数。
Davis-Bouldin 指数 是每个聚类与最接近的聚类之间相似性的平均值。分数从 0 开始。0 表示更好的聚类。
在这里,我们将绘制惯性、轮廓系数、Calinski-Harabasz 指数和 Davies-Bouldin 指数在一系列聚类大小上的情况,以查看数据是否有明确的聚类大小(见 Figure 18-2)。大多数这些指标都同意选择两个聚类:
>>> from sklearn import metrics
>>> inertias = []
>>> sils = []
>>> chs = []
>>> dbs = []
>>> sizes = range(2, 12)
>>> for k in sizes:
... k2 = KMeans(random_state=42, n_clusters=k)
... k2.fit(X_std)
... inertias.append(k2.inertia_)
... sils.append(
... metrics.silhouette_score(X, k2.labels_)
... )
... chs.append(
... metrics.calinski_harabasz_score(
... X, k2.labels_
... )
... )
... dbs.append(
... metrics.davies_bouldin_score(
... X, k2.labels_
... )
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> (
... pd.DataFrame(
... {
... "inertia": inertias,
... "silhouette": sils,
... "calinski": chs,
... "davis": dbs,
... "k": sizes,
... }
... )
... .set_index("k")
... .plot(ax=ax, subplots=True, layout=(2, 2))
... )
>>> fig.savefig("images/mlpr_1802.png", dpi=300)
图 18-2. 聚类指标。这些指标大多数同意选择两个聚类。
另一种确定聚类的技术是可视化每个聚类的轮廓分数。Yellowbrick 有一个此类的可视化工具(见 Figure 18-3)。
在这个图中,垂直的虚线是平均分数。一种解释方法是确保每个聚类都突出于平均水平之上,并且聚类分数看起来还不错。确保使用相同的 x 轴限制 (ax.set_xlim
)。我会从这些图中选择两个聚类:
>>> from yellowbrick.cluster.silhouette import (
... SilhouetteVisualizer,
... )
>>> fig, axes = plt.subplots(2, 2, figsize=(12, 8))
>>> axes = axes.reshape(4)
>>> for i, k in enumerate(range(2, 6)):
... ax = axes[i]
... sil = SilhouetteVisualizer(
... KMeans(n_clusters=k, random_state=42),
... ax=ax,
... )
... sil.fit(X_std)
... sil.finalize()
... ax.set_xlim(-0.2, 0.8)
>>> plt.tight_layout()
>>> fig.savefig("images/mlpr_1803.png", dpi=300)
图 18-3. Yellowbrick 轮廓可视化器
聚合(层次)聚类
聚合聚类是另一种方法。你从每个样本单独的聚类开始。然后将“最近的”聚类组合起来。重复此过程,同时跟踪最近的大小。
当您完成这些操作时,您将得到一棵dendrogram,或者一棵跟踪聚类创建时间和距离度量的树。您可以使用 scipy 库可视化这棵树。
我们可以使用 scipy 创建一棵树状图(见 Figure 18-4)。如您所见,如果样本很多,则叶节点很难阅读:
>>> from scipy.cluster import hierarchy
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> dend = hierarchy.dendrogram(
... hierarchy.linkage(X_std, method="ward")
... )
>>> fig.savefig("images/mlpr_1804.png", dpi=300)
图 18-4. Scipy 层次聚类树状图
一旦您有了树状图,您就拥有了所有的聚类(从样本数为一到样本数为止)。高度表示加入时相似聚类的相似程度。为了找出数据中有多少个聚类,您需要在最高的线交叉处“切割”一条水平线。
在这种情况下,当您执行该切割时,看起来有三个聚类。
前一个图表太嘈杂了,其中包含了所有的样本。您还可以使用truncate_mode
参数将叶子节点合并为单个节点(参见图 18-5):
>>> from scipy.cluster import hierarchy
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> dend = hierarchy.dendrogram(
... hierarchy.linkage(X_std, method="ward"),
... truncate_mode="lastp",
... p=20,
... show_contracted=True,
... )
>>> fig.savefig("images/mlpr_1805.png", dpi=300)
图 18-5. 截断的层次聚类树状图。如果我们在最大的垂直线上切割,我们将得到三个聚类。
一旦我们知道需要多少个聚类,我们可以使用 scikit-learn 创建一个模型:
>>> from sklearn.cluster import (
... AgglomerativeClustering,
... )
>>> ag = AgglomerativeClustering(
... n_clusters=4,
... affinity="euclidean",
... linkage="ward",
... )
>>> ag.fit(X)
注意
fastcluster 包 提供了一个优化的凝聚聚类包,如果 scikit-learn 的实现速度太慢的话。
理解聚类
在 Titanic 数据集上使用 K-means,我们将得到两个聚类。我们可以使用 pandas 中的分组功能来检查聚类之间的差异。下面的代码检查每个特征的均值和方差。看起来 pclass 的均值变化相当大。
我将生存数据重新放回,看看聚类是否与此相关:
>>> km = KMeans(n_clusters=2)
>>> km.fit(X_std)
>>> labels = km.predict(X_std)
>>> (
... X.assign(cluster=labels, survived=y)
... .groupby("cluster")
... .agg(["mean", "var"])
... .T
... )
cluster 0 1
pclass mean 0.526538 -1.423831
var 0.266089 0.136175
age mean -0.280471 0.921668
var 0.653027 1.145303
sibsp mean -0.010464 -0.107849
var 1.163848 0.303881
parch mean 0.387540 0.378453
var 0.829570 0.540587
fare mean -0.349335 0.886400
var 0.056321 2.225399
sex_male mean 0.678986 0.552486
var 0.218194 0.247930
embarked_Q mean 0.123548 0.016575
var 0.108398 0.016345
embarked_S mean 0.741288 0.585635
var 0.191983 0.243339
survived mean 0.596685 0.299894
var 0.241319 0.210180
注意
在 Jupyter 中,您可以将以下代码添加到 DataFrame 中,并突出显示每行的高低值。这对于直观地查看哪些值在上述聚类摘要中显著是有用的:
.style.background_gradient(cmap='RdBu', axis=1)
在图 18-6 中,我们绘制了每个聚类的均值条形图:
>>> fig, ax = plt.subplots(figsize=(6, 4))
... (
... X.assign(cluster=labels, survived=y)
... .groupby("cluster")
... .mean()
... .T.plot.bar(ax=ax)
... )
>>> fig.savefig(
... "images/mlpr_1806.png",
... dpi=300,
... bbox_inches="tight",
... )
图 18-6. 每个聚类的均值
我还喜欢绘制 PCA 组件,但是按聚类标签着色(见图 18-7)。在这里,我们使用 Seaborn 来执行此操作。将hue
的值更改为深入研究聚类中显著的特征也很有趣。
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> sns.scatterplot(
... "PC1",
... "PC2",
... data=X.assign(
... PC1=X_pca[:, 0],
... PC2=X_pca[:, 1],
... cluster=labels,
... ),
... hue="cluster",
... alpha=0.5,
... ax=ax,
... )
>>> fig.savefig(
... "images/mlpr_1807.png",
... dpi=300,
... bbox_inches="tight",
... )
图 18-7. 聚类的 PCA 图
如果我们想检查单个特征,可以使用 pandas 的.describe
方法:
>>> (
... X.assign(cluster=label)
... .groupby("cluster")
... .age.describe()
... .T
... )
cluster 0 1
count 362.000000 947.000000
mean 0.921668 -0.280471
std 1.070188 0.808101
min -2.160126 -2.218578
25% 0.184415 -0.672870
50% 0.867467 -0.283195
75% 1.665179 0.106480
max 4.003228 3.535618
我们还可以创建一个替代模型来解释这些聚类。在这里,我们使用决策树来解释它们。这还显示了 pclass(其均值差异很大)非常重要:
>>> dt = tree.DecisionTreeClassifier()
>>> dt.fit(X, labels)
>>> for col, val in sorted(
... zip(X.columns, dt.feature_importances_),
... key=lambda col_val: col_val[1],
... reverse=True,
... ):
... print(f"{col:10}{val:10.3f}")
pclass 0.902
age 0.074
sex_male 0.016
embarked_S 0.003
fare 0.003
parch 0.003
sibsp 0.000
embarked_Q 0.000
我们可以通过图 18-8 来可视化决策。它显示 pclass 是第一个特征,用于做出决策:
>>> dot_data = StringIO()
>>> tree.export_graphviz(
... dt,
... out_file=dot_data,
... feature_names=X.columns,
... class_names=["0", "1"],
... max_depth=2,
... filled=True,
... )
>>> g = pydotplus.graph_from_dot_data(
... dot_data.getvalue()
... )
>>> g.write_png("images/mlpr_1808.png")
图 18-8. 解释聚类的决策树
第十九章:管道
Scikit-learn 使用管道的概念。使用 Pipeline
类,您可以将转换器和模型链在一起,并像对待整个 scikit-learn 模型一样对待整个过程。甚至可以插入自定义逻辑。
分类管道
这是在管道内使用 tweak_titanic
函数的示例:
>>> from sklearn.base import (
... BaseEstimator,
... TransformerMixin,
... )
>>> from sklearn.pipeline import Pipeline
>>> def tweak_titanic(df):
... df = df.drop(
... columns=[
... "name",
... "ticket",
... "home.dest",
... "boat",
... "body",
... "cabin",
... ]
... ).pipe(pd.get_dummies, drop_first=True)
... return df
>>> class TitanicTransformer(
... BaseEstimator, TransformerMixin
... ):
... def transform(self, X):
... # assumes X is output
... # from reading Excel file
... X = tweak_titanic(X)
... X = X.drop(column="survived")
... return X
...
... def fit(self, X, y):
... return self
>>> pipe = Pipeline(
... [
... ("titan", TitanicTransformer()),
... ("impute", impute.IterativeImputer()),
... (
... "std",
... preprocessing.StandardScaler(),
... ),
... ("rf", RandomForestClassifier()),
... ]
... )
有了管道,我们可以对其调用 .fit
和 .score
:
>>> from sklearn.model_selection import (
... train_test_split,
... )
>>> X_train2, X_test2, y_train2, y_test2 = train_test_split(
... orig_df,
... orig_df.survived,
... test_size=0.3,
... random_state=42,
... )
>>> pipe.fit(X_train2, y_train2)
>>> pipe.score(X_test2, y_test2)
0.7913486005089059
管道可以在网格搜索中使用。我们的 param_grid
需要将参数前缀设为管道阶段的名称,后跟两个下划线。在下面的示例中,我们为随机森林阶段添加了一些参数:
>>> params = {
... "rf__max_features": [0.4, "auto"],
... "rf__n_estimators": [15, 200],
... }
>>> grid = model_selection.GridSearchCV(
... pipe, cv=3, param_grid=params
... )
>>> grid.fit(orig_df, orig_df.survived)
现在我们可以提取最佳参数并训练最终模型。(在这种情况下,随机森林在网格搜索后没有改善。)
>>> grid.best_params_
{'rf__max_features': 0.4, 'rf__n_estimators': 15}
>>> pipe.set_params(**grid.best_params_)
>>> pipe.fit(X_train2, y_train2)
>>> pipe.score(X_test2, y_test2)
0.7913486005089059
我们可以在使用 scikit-learn 模型的管道中使用:
>>> metrics.roc_auc_score(
... y_test2, pipe.predict(X_test2)
... )
0.7813688715131023
回归管道
这是在波士顿数据集上执行线性回归的管道示例:
>>> from sklearn.pipeline import Pipeline
>>> reg_pipe = Pipeline(
... [
... (
... "std",
... preprocessing.StandardScaler(),
... ),
... ("lr", LinearRegression()),
... ]
... )
>>> reg_pipe.fit(bos_X_train, bos_y_train)
>>> reg_pipe.score(bos_X_test, bos_y_test)
0.7112260057484934
如果我们想要从管道中提取部分来检查它们的属性,我们可以使用 .named_steps
属性进行操作:
>>> reg_pipe.named_steps["lr"].intercept_
23.01581920903956
>>> reg_pipe.named_steps["lr"].coef_
array([-1.10834602, 0.80843998, 0.34313466,
0.81386426, -1.79804295, 2.913858 ,
-0.29893918, -2.94251148, 2.09419303,
-1.44706731, -2.05232232, 1.02375187,
-3.88579002])_
我们也可以在度量计算中使用管道:
>>> from sklearn import metrics
>>> metrics.mean_squared_error(
... bos_y_test, reg_pipe.predict(bos_X_test)
... )
21.517444231177205
PCA 管道
Scikit-learn 管道也可以用于 PCA。
这里我们对泰坦尼克号数据集进行标准化并对其执行 PCA:
>>> pca_pipe = Pipeline(
... [
... (
... "std",
... preprocessing.StandardScaler(),
... ),
... ("pca", PCA()),
... ]
... )
>>> X_pca = pca_pipe.fit_transform(X)
使用 .named_steps
属性,我们可以从管道的 PCA 部分提取属性:
>>> pca_pipe.named_steps[
... "pca"
... ].explained_variance_ratio_
array([0.23917891, 0.21623078, 0.19265028,
0.10460882, 0.08170342, 0.07229959,
0.05133752, 0.04199068])
>>> pca_pipe.named_steps["pca"].components_[0]
array([-0.63368693, 0.39682566, 0.00614498,
0.11488415, 0.58075352, -0.19046812,
-0.21190808, -0.09631388])
标签:口袋,...,机器,参考,df,fig,import,test,ax
From: https://www.cnblogs.com/apachecn/p/18253007