监督学习研讨会-全-

监督学习研讨会(全)

原文:annas-archive.org/md5/20b62d9571c391689a2d53277f7e2459

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

第一章:本书概览

想了解机器学习技术和数据分析如何在全球范围内引领企业发展吗?从生物信息学分析到气候变化预测,机器学习在我们的社会中扮演着越来越重要的角色。

尽管现实世界中的应用可能看起来复杂,但本书通过逐步互动的方式简化了初学者的监督学习。通过使用实时数据集,您将学习如何使用 Python 进行监督学习,以构建高效的预测模型。

从监督学习的基础知识开始,您将很快理解如何自动化手动任务,并通过 Jupyter 和像 pandas 这样的 Python 库评估数据。接下来,您将使用数据探索和可视化技术开发强大的监督学习模型,然后了解如何区分变量,并使用散点图、热图和箱线图表示它们之间的关系。在使用回归和分类模型处理实时数据集以预测未来结果后,您将掌握高级集成技术,如提升方法和随机森林。最后,您将了解监督学习中模型评估的重要性,并学习评估回归和分类任务的度量标准。

在本书结束时,您将掌握自己进行实际监督学习 Python 项目所需的技能。

读者对象

如果您是初学者或刚刚入门的数据科学家,正在学习如何实现机器学习算法来构建预测模型,那么本书适合您。为了加速学习过程,建议具备扎实的 Python 编程基础,因为您将编辑类或函数,而不是从零开始创建。

章节概览

第一章,基础知识,将介绍监督学习、Jupyter 笔记本以及一些最常见的 pandas 数据方法。

第二章,探索性数据分析与可视化,教授您如何对新数据集进行探索和分析。

第三章,线性回归,教授您如何解决回归问题和进行分析,介绍线性回归、多个线性回归和梯度下降法。

第四章,自回归,教授您如何实现自回归方法来预测依赖于过去值的未来值。

第五章,分类技术,介绍分类问题,包括线性回归和逻辑回归、k 近邻算法和决策树的分类方法。

第六章,集成建模,教授您如何检视不同的集成建模方法,包括它们的优点和局限性。

第七章,模型评估,展示了如何通过使用超参数和模型评估指标来提高模型的性能。

约定

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名以如下方式显示:“使用 pandas 的 read_csv 函数加载包含 synth_temp.csv 数据集的 CSV 文件,然后显示前五行数据。”

屏幕上显示的单词,例如在菜单或对话框中,也会以这种形式出现在文本中:“通过点击 Jupyter notebook 首页的 titanic.csv 文件来打开它。”

一段代码的设置如下:

print(data[pd.isnull(data.damage_millions_dollars)].shape[0])

print(data[pd.isnull(data.damage_millions_dollars) &

(data.damage_description != 'NA')].shape[0])

新术语和重要单词以如下方式显示:“监督式学习意味着数据的标签在训练过程中已经提供,从而让模型能够基于这些标签进行学习。”

代码展示

跨越多行的代码使用反斜杠( \ )进行拆分。当代码执行时,Python 会忽略反斜杠,将下一行的代码视为当前行的延续。

例如:

history = model.fit(X, y, epochs=100, batch_size=5, verbose=1, \

validation_split=0.2, shuffle=False)

注释被添加到代码中,以帮助解释特定的逻辑。单行注释使用#符号,如下所示:

打印数据集的大小

print("数据集中的示例数量 = ", X.shape[0])

print("每个示例的特征数 = ", X.shape[1])

多行注释用三个引号括起来,如下所示:

"""

为随机数生成器定义一个种子,以确保结果可复现

结果将是可重复的

"""

seed = 1

np.random.seed(seed)

random.set_seed(seed)

设置你的环境

在详细探讨本书内容之前,我们需要设置一些特定的软件和工具。在接下来的部分,我们将看到如何操作。

安装与设置

本书中的所有代码都是在 Jupyter Notebooks 和 Python 3.7 上执行的。安装 Anaconda 后,Jupyter Notebooks 和 Python 3.7 可供使用。以下部分列出了在 Windows、macOS 和 Linux 系统上安装 Anaconda 的说明。

在 Windows 上安装 Anaconda

以下是完成安装所需遵循的步骤:

访问 https://www.anaconda.com/products/individual 并点击下载按钮。

在 Anaconda 安装程序/Windows 部分,选择 Python 3.7 版本的安装程序。

确保安装与你的计算机架构(32 位或 64 位)相符的版本。你可以在操作系统的“系统属性”窗口中找到此信息。

下载完成后,双击文件,按照屏幕上的指示完成安装。

这些安装将在你系统的 'C' 盘执行。不过,你可以选择更改安装目标。

在 macOS 上安装 Anaconda

访问 www.anaconda.com/products/individual 并点击下载按钮。

在 Anaconda 安装程序/MacOS 部分,选择 (Python 3.7) 64 位图形安装程序。

下载完安装程序后,双击文件,并按照屏幕上的指示完成安装。

在 Linux 上安装 Anaconda

访问 www.anaconda.com/products/individual 并点击下载按钮。

在 Anaconda 安装程序/Linux 部分,选择 (Python 3.7) 64 位 (x86) 安装程序。

下载完安装程序后,在终端运行以下命令:bash ~/Downloads/Anaconda-2020.02-Linux-x86_64.sh

按照终端中出现的指示完成安装。

你可以通过访问此网站了解有关各种系统安装的更多详情:docs.anaconda.com/anaconda/install/

安装库

pip 在 Anaconda 中预装。安装完 Anaconda 后,可以使用 pip 安装所有必需的库,例如:pip install numpy。或者,你也可以使用 pip install –r requirements.txt 来安装所有必需的库。你可以在 packt.live/3hSJgYy 找到 requirements.txt 文件。

练习和活动将在 Jupyter Notebooks 中执行。Jupyter 是一个 Python 库,可以像其他 Python 库一样通过 pip install jupyter 安装,但幸运的是,它在 Anaconda 中已预装。要打开笔记本,只需在终端或命令提示符中运行命令 jupyter notebook。

访问代码文件

你可以在 packt.live/2TlcKDf 找到本书的完整代码文件。你也可以通过使用互动实验环境 packt.live/37QVpsD 直接在浏览器中运行许多活动和练习。

我们已经尽力支持所有活动和练习的互动版本,但我们仍然推荐进行本地安装,以防这些互动支持不可用。

如果你在安装过程中遇到任何问题或有任何疑问,请通过邮件联系我们:workshops@packt.com。

第二章:1. 基础知识

概述

本章将向你介绍监督学习,使用 Anaconda 管理编码环境,以及使用 Jupyter notebooks 创建、管理和运行代码。它还涵盖了一些在监督学习中最常用的 Python 包:pandas、NumPy、Matplotlib 和 seaborn。本章结束时,你将能够安装并加载 Python 库到你的开发环境中,用于分析和机器学习问题。你还将能够使用 pandas 加载外部数据源,并使用多种方法搜索、过滤和计算数据的描述性统计信息。本章将使你能够评估数据源中诸如缺失数据、类别不平衡和样本量不足等问题的潜在影响。

介绍

机器学习和人工智能的研究和应用最近引起了技术和商业界的广泛关注。先进的数据分析和机器学习技术在推动许多领域取得巨大进展方面表现出巨大的潜力,例如个性化医疗、自驾汽车,以及解决世界上一些最大的挑战,例如应对气候变化(参见《使用机器学习应对气候变化》:https://arxiv.org/pdf/1906.05433.pdf)。

本书旨在帮助你利用当今数据科学和机器学习领域中的独特发展机遇。全球范围内,私营企业和政府都在认识到数据驱动的产品和服务的价值和效率。与此同时,硬件成本的降低和开源软件解决方案显著降低了学习和应用机器学习技术的门槛。

在这里,我们将重点介绍监督式机器学习(简称监督学习)。我们稍后将解释不同类型的机器学习,但我们先从一些简单的信息开始。监督学习的经典示例是开发一个算法来区分猫和狗的图片。监督的部分源于两个方面;首先,我们有一组图片,其中正确答案是已知的。我们称这种数据为标签数据。其次,我们进行一个过程,不断测试我们的算法是否能够根据图片预测“猫”或“狗”,并在预测错误时对算法进行修正。从高层次上讲,这个过程类似于教孩子。然而,训练一个算法通常需要比教一个孩子识别猫和狗更多的数据!幸运的是,我们有越来越多的数据源可以利用。请注意,在开发我们的算法时,使用了“学习”和“训练”这两个词。这些词看起来似乎是赋予机器和计算机程序人的特质,但它们已经深深根植于机器学习(和人工智能)文献中,所以我们就用它们,并理解它们。在这里,“训练”指的是将标签数据提供给算法,并对算法进行调整,以便根据数据最好地预测标签。监督的意思是,数据的标签在训练过程中已提供,使得模型能够从这些标签中学习。

现在让我们了解监督学习与其他形式机器学习的区别。

何时使用监督学习

通常,如果你试图自动化或复制一个现有的过程,那么问题就是一个监督学习问题。举个例子,假设你是一本评论并排名各个时期发型的杂志的出版商。你的读者经常向你发送远多于你能够手动处理的发型图片进行评审。为了节省时间,你希望自动化排序你收到的发型图片,按照时间顺序从 1960 年代和 1980 年代的发型开始,正如你在下图中所看到的:

图 1.1:来自不同时期的发型图片

图 1.1:来自不同时期的发型图片

为了创建你的发型排序算法,你首先需要收集大量发型图片,并手动为每一张图片标注对应的时间时期。这样的数据集(称为标签数据集)是输入数据(发型图片),其所需的输出信息(时间时期)是已知并记录下来的。这类问题是经典的监督学习问题;我们正在尝试开发一个算法,它可以接收一组输入,并学会返回我们告知它正确的答案。

Python 包和模块

Python 是最常用的机器学习编程语言之一,并且是本书中使用的语言。

虽然 Python 中包含的标准功能确实功能丰富,但 Python 的真正强大之处在于额外的库(也称为包),这些库由于开源许可,可以通过几个简单的命令轻松下载和安装。在本书中,我们一般假设您的系统已经使用 Anaconda 配置,Anaconda 是一个用于 Python 的开源环境管理器。根据您的系统,您可以使用 Anaconda 配置多个虚拟环境,每个环境都配置有特定的包,甚至不同版本的 Python。使用 Anaconda 可以解决许多准备进行机器学习的要求,因为许多最常用的包都已经预构建在 Anaconda 中。有关 Anaconda 安装说明,请参阅前言。

在本书中,我们将使用以下附加的 Python 包:

NumPy(发音为 Num Pie,可在 https://www.numpy.org/ 获取):NumPy(即数字 Python 的简称)是 Python 科学计算的核心组成部分之一。NumPy 提供了多个数据类型,这些数据类型衍生出了许多其他数据结构,包括线性代数、向量和矩阵,以及关键的随机数功能。

SciPy(发音为 Sigh Pie,可在 https://www.scipy.org 获取):SciPy 与 NumPy 一起,是核心的科学计算包。SciPy 提供了许多统计工具、信号处理工具和其他功能,如傅里叶变换。

pandas(可在 https://pandas.pydata.org/ 获取):pandas 是一个高性能的库,用于加载、清洗、分析和操作数据结构。

Matplotlib(可在 https://matplotlib.org/ 获取):Matplotlib 是 Python 中用于创建数据集图表和绘图的基础库,也是其他 Python 绘图库的基础包。Matplotlib 的 API 设计与 Matlab 的绘图库对齐,以便于轻松过渡到 Python。

Seaborn(可在 https://seaborn.pydata.org/ 获取):Seaborn 是一个基于 Matplotlib 构建的绘图库,提供了吸引人的颜色和线条样式,以及一些常见的绘图模板。

Scikit-learn(可在 https://scikit-learn.org/stable/ 获取):Scikit-learn 是一个 Python 机器学习库,提供了许多数据挖掘、建模和分析技术,且有一个简单的 API。Scikit-learn 内置了许多机器学习算法,包括分类、回归和聚类技术。

这些包构成了一个多功能的机器学习开发环境的基础,每个包都提供了一个关键功能集。如前所述,使用 Anaconda 时,您已经安装了所有必需的包,并且可以随时使用。如果需要安装 Anaconda 安装包中未包含的包,可以通过在 Jupyter 笔记本单元格中输入并执行以下代码来安装:

!conda install

例如,如果我们想安装 Seaborn,可以运行以下命令:

!conda install seaborn

要在笔记本中使用这些包,我们只需要导入它:

import matplotlib

在 Pandas 中加载数据

pandas 能够读取和写入多种不同的文件格式和数据结构,包括 CSV、JSON、HDF5 文件,以及 SQL 和 Python Pickle 格式。pandas 的输入/输出文档可以在 https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html 找到。我们将继续通过加载 CSV 文件来研究 pandas 的功能。

注意

本章使用的数据集可以通过以下链接在我们的 GitHub 仓库中找到:https://packt.live/2vjyPK9。下载整个仓库到您的系统后,您可以在 Datasets 文件夹中找到数据集。此外,这个数据集是 Titanic: Machine Learning from Disaster 数据集,最初可以在 https://www.kaggle.com/c/Titanic/data 找到。

该数据集包含了著名的泰坦尼克号上的乘客名单,以及他们的年龄、幸存状态和兄弟姐妹/父母的数量。在开始将数据加载到 Python 之前,关键是花时间查看数据集提供的信息,以便我们能够深入理解它所包含的内容。下载数据集并将其放置在您的工作目录中。

从数据描述中,我们可以看到我们有以下字段:

survival: 这告诉我们一个人是否幸存(0 = 否,1 = 是)。

pclass: 这是社会经济地位的代理,其中头等舱代表上层,二等舱代表中层,三等舱代表下层。

sex: 这告诉我们一个人是男性还是女性。

age: 如果年龄小于 1,则这是一个分数值;例如,0.25 表示 3 个月。如果年龄是估算的,它将以 xx.5 的形式表示。

sibsp: 兄弟姐妹定义为兄弟、姐妹、继兄或继姐,配偶定义为丈夫或妻子。

parch: 父母是母亲或父亲,而子女则是女儿、儿子、继女或继子。仅与保姆一起旅行的孩子没有与父母一起旅行。因此,该字段赋值为 0。

ticket: 这是乘客的票号。

fare: 这是乘客的票价。

cabin: 这告诉我们乘客的舱号。

embark: 登船地点是乘客登船的地点。

请注意,数据集提供的信息没有说明数据是如何收集的。survival、pclass 和 embarked 字段被称为分类变量,因为它们被分配到一组固定的标签或类别中,以指示其他信息。例如,在 embarked 中,C 标签表示乘客在瑟堡登船,而 survival 中的 1 表示他们在沉船事故中幸存。

练习 1.01:加载并总结 Titanic 数据集

在本练习中,我们将把 Titanic 数据集读入 Python 并进行一些基本的总结操作:

打开一个新的 Jupyter notebook。

使用简写语法导入 pandas 和 numpy 包:

import pandas as pd

import numpy as np

通过点击 Jupyter notebook 首页上的 titanic.csv 文件来打开并读取文件,如下图所示:

图 1.2:打开 CSV 文件

图 1.2:打开 CSV 文件

该文件是一个 CSV 文件,可以将其视为一个表格,每一行代表表格中的一行,每个逗号分隔表格中的列。幸运的是,我们无需以原始文本形式处理这些表格,可以使用 pandas 加载它们:

图 1.3:CSV 文件内容

图 1.3:CSV 文件内容

注释

花点时间查阅 pandas 文档中关于 read_csv 函数的说明,网址:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html。注意加载 CSV 数据到 pandas DataFrame 中的不同选项。

在可执行的 Jupyter notebook 单元格中,执行以下代码来从文件加载数据:

df = pd.read_csv(r'..\Datasets\titanic.csv')

pandas 的 DataFrame 类提供了一整套可以在其内容上执行的属性和方法,涵盖从排序、过滤、分组方法到描述性统计,以及绘图和转换等功能。

注释

打开 pandas DataFrame 对象的文档,网址:https://pandas.pydata.org/pandas-docs/stable/reference/frame.html。

使用 DataFrame 的 head() 方法读取前十行数据:

注释

以下代码片段中的 # 符号表示代码注释。注释是添加到代码中以帮助解释特定逻辑的部分。

df.head(10) # 检查前 10 个样本

输出将如下所示:

图 1.4:读取前 10 行

图 1.4:读取前 10 行

注释

要访问此特定部分的源代码,请参考 https://packt.live/2Ynb7sf。

你也可以在网上运行这个示例,网址:https://packt.live/2BvTRrG。你必须执行整个笔记本以获得预期结果。

在此示例中,我们展示了 DataFrame 中信息的可视化表示。我们可以看到数据以表格化的方式组织,几乎像电子表格一样。不同类型的数据被组织成列,而每个样本被组织成行。每一行都分配了一个索引值,并以粗体数字 0 到 9 显示在 DataFrame 的左侧。每一列都分配了一个标签或名称,如粗体所示,在 DataFrame 的顶部。

DataFrame 作为一种电子表格的类比是合理的。如本章所示,我们可以像在电子表格程序中那样对数据进行排序、过滤和计算。虽然本章未涉及,但值得注意的是,DataFrame 还包含了类似电子表格的透视表功能(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.pivot_table.html)。

练习 1.02:索引和选择数据

现在我们已经加载了一些数据,让我们使用 DataFrame 的选择和索引方法来访问一些感兴趣的数据。这个练习是练习 1.01,“加载并总结泰坦尼克数据集”的延续:

通过使用列的标签,以类似于常规字典的方式选择单个列,如下所示:

df['Age']

输出结果如下:

0 22.0

1 38.0

2 26.0

3 35.0

4 35.0

...

1304 NaN

1305 39.0

1306 38.5

1307 NaN

1308 NaN

名称:年龄,长度:1309,数据类型:float64

如果列名中没有空格,我们还可以使用点操作符。如果列名中有空格,则需要使用括号表示法:

df.Age

输出结果如下:

0 22.0

1 38.0

2 26.0

3 35.0

4 35.0

...

1304 NaN

1305 39.0

1306 38.5

1307 NaN

1308 NaN

名称:年龄,长度:1309,数据类型:float64

使用括号表示法一次选择多个列,如下所示:

df[['Name', 'Parch', 'Sex']]

输出结果如下:

图 1.5:选择多个列

图 1.5:选择多个列

注意

由于展示需要,输出已被截断。

使用 iloc 选择第一行:

df.iloc[0]

输出结果如下:

图 1.6:选择第一行

图 1.6:选择第一行

使用 iloc 选择前三行:

df.iloc[[0,1,2]]

输出结果如下:

图 1.7:选择前三行

图 1.7:选择前三行

接下来,获取所有可用列的列表:

columns = df.columns # 提取列名列表

print(columns)

输出结果如下:

图 1.8:获取所有列

图 1.8:获取所有列

使用此列名列表和标准的 Python 切片语法,获取第 2、3、4 列及其对应的值:

df[columns[1:4]] # 第 2、3、4 列

输出结果如下:

图 1.9:获取第二、第三和第四列

图 1.9:获取第二列、第三列和第四列

使用 len 运算符获取 DataFrame 中的行数:

len(df)

输出结果如下所示:

1309

使用以行中心的方法获取第 2 行的 Fare 列值:

df.iloc[2]['Fare'] # 以行为中心

输出结果如下所示:

7.925

使用点操作符来访问列,如下所示:

df.iloc[2].Fare # 以行为中心

输出结果如下所示:

7.925

使用以列为中心的方法,如下所示:

df['Fare'][2] # 以列为中心

输出结果如下所示:

7.925

使用以列为中心的方法并使用点操作符,如下所示:

df.Fare[2] # 以列为中心

输出结果如下所示:

7.925

注意

要访问这一特定部分的源代码,请参考 packt.live/2YmA7jb

你也可以在 packt.live/3dmk0qf 上在线运行此示例。你必须执行整个笔记本才能得到预期的结果。

在这个练习中,我们学习了如何使用 pandas 的 read_csv() 函数将数据加载到 Python 中的 Jupyter 笔记本中。然后,我们探讨了 pandas 通过以 DataFrame 形式呈现数据,如何便捷地选择 DataFrame 中的特定项并查看其内容。在理解了这些基础知识后,我们将进一步探索更高级的索引和数据选择方法。

练习 1.03:高级索引和选择

在掌握了基本的索引和选择方法之后,我们可以将注意力转向更高级的索引和选择方法。在这个练习中,我们将探索几种重要的方法来执行高级索引和数据选择。这个练习是练习 1.01 "加载并总结 Titanic 数据集" 的延续:

创建一个包含 21 岁以下乘客姓名和年龄的列表,如下所示:

child_passengers = df[df.Age < 21][['Name', 'Age']]

child_passengers.head()

输出结果如下所示:

图 1.10:21 岁以下乘客的姓名和年龄列表

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-W5SL6I98.jpg)

图 1.10:21 岁以下乘客的姓名和年龄列表

计算有多少名儿童乘客,如下所示:

print(len(child_passengers))

输出结果如下所示:

249

计算年龄在 21 岁到 30 岁之间的乘客数量。此步骤不使用 Python 的逻辑与运算符,而是使用和符号(&)。具体操作如下:

注意

这里展示的代码片段使用了反斜杠(\)将逻辑分割成多行。当代码执行时,Python 会忽略反斜杠,并将下一行代码视为当前行的直接延续。

young_adult_passengers = df.loc[(df.Age > 21) \

& (df.Age < 30)]

len(young_adult_passengers)

输出结果如下所示:

279

查找第一类或第三类票的乘客。我们不会使用 Python 的逻辑或运算符,而是使用管道符号(|)。具体操作如下:

df.loc[(df.Pclass == 3) | (df.Pclass ==1)]

输出如下:

图 1.11:既是头等舱票持有者又是三等舱票持有者的乘客数量

图 1.11:既是头等舱票持有者又是三等舱票持有者的乘客数量

查找不是头等或者三等舱票持有者的乘客。不要简单地选择二等舱票持有者,而是使用 ~ 符号作为非逻辑运算符。操作如下:

df.loc[~((df.Pclass == 3) | (df.Pclass == 1))]

输出如下:

图 1.12:不是头等舱或三等舱票持有者的乘客数量

图 1.12:不是头等舱或三等舱票持有者的乘客数量

我们不再需要未命名的 0 列,因此使用 del 运算符删除它:

del df['Unnamed: 0']

df.head()

输出如下:

图 1.13:del 运算符

图 1.13:del 运算符

注意

要访问此特定部分的源代码,请参考 https://packt.live/3empSRO。

您也可以在 https://packt.live/3fEsPgK 上在线运行此示例。为了获得期望的结果,您必须执行整个笔记本。

在这个练习中,我们学习了如何使用条件运算符从 DataFrame 中选择数据,并返回我们想要的子集。我们还看到如何删除我们不需要的列(在这种情况下,未命名列只包含对分析无关的行号)。现在,让我们深入了解一些 pandas 的强大功能。

Pandas 方法

现在我们对一些 pandas 基础知识和一些更高级的索引和选择工具感到自信,让我们看一些其他 DataFrame 方法。要获取 DataFrame 中可用的所有方法的完整列表,可以参考类文档。

注意

pandas 的文档可以在 https://pandas.pydata.org/pandas-docs/stable/reference/frame.html 找到。

现在您应该知道 DataFrame 中有多少方法可用。这些方法太多,无法在本章节详细介绍,因此我们将选择一些能为您提供优秀启动的方法。

我们已经看到了一个方法的用法,head(),它提供了 DataFrame 的前五行。如果需要,我们可以通过提供行数来选择更多或更少的行,如下所示:

df.head(n=20) # 20 行

df.head(n=32) # 32 行

或者,您可以使用 tail() 函数查看 DataFrame 结尾的指定行数。

另一个有用的方法是 describe,这是一种快速获取 DataFrame 中数据描述统计信息的方法。我们可以看到样本大小(计数)、均值、最小值、最大值、标准差以及第 25、50 和 75 百分位数对所有数值数据列返回(请注意,文本列已被省略):

df.describe()

输出如下:

图 1.14:describe 方法

图 1.14:describe 方法

请注意,只有数值型数据的列被包含在摘要中。这个简单的命令为我们提供了大量有用的信息;通过查看 count(即有效样本数)的值,我们可以看到年龄(Age)类别中有 1,046 个有效样本,但票价(Fare)有 1,308 个,而生存(Survived)列只有 891 个有效样本。我们可以看到,最年轻的人是 0.17 岁,平均年龄为 29.898 岁,最年长的乘客为 80 岁。最小票价为 £0,平均票价为 £33.30,最贵票价为 £512.33。如果我们看一下 Survived 列,我们有 891 个有效样本,均值为 0.38,这意味着大约 38% 的人存活。

我们还可以通过调用 DataFrame 的相应方法,分别获取每一列的这些值,如下所示:

df.count()

输出将如下所示:

Cabin 295

Embarked 1307

Fare 1308

Pclass 1309

Ticket 1309

Age 1046

Name 1309

Parch 1309

Sex 1309

SibSp 1309

Survived 891

dtype: int64

但我们也有一些包含文本数据的列,例如 Embarked、Ticket、Name 和 Sex。那么这些怎么办呢?我们如何获取这些列的描述性信息?我们仍然可以使用 describe 方法,只是需要传递更多的信息。默认情况下,describe 只会包含数值列,并计算 25%、50% 和 75% 的分位数,但我们可以通过传递 include = 'all' 参数来配置它以包含文本列,如下所示:

df.describe(include='all')

输出将如下所示:

图 1.15:带文本列的 describe 方法

图 1.15:带文本列的 describe 方法

这样更好了——现在我们有了更多信息。查看 Cabin 列,我们可以看到有 295 个条目,其中有 186 个唯一值。最常见的值是 C32、C25 和 C27,它们出现了 6 次(来自 freq 值)。类似地,查看 Embarked 列,我们看到有 1,307 个条目,3 个唯一值,最常出现的值是 S,共有 914 个条目。

注意我们在 describe 输出表中出现了 NaN 值。NaN,即非数字(Not a Number),在 DataFrame 中非常重要,因为它表示缺失或不可用的数据。pandas 库能够读取包含缺失或不完整信息的数据源,既是一个福音,也是一个诅咒。许多其他库在遇到缺失信息时会直接无法导入或读取数据文件,而 pandas 能够读取数据也意味着必须妥善处理这些缺失数据。

当查看 describe 方法的输出时,你会注意到 Jupyter 笔记本将其呈现为与我们通过 read_csv 读取的原始 DataFrame 相同的方式。这样做是有充分理由的,因为 describe 方法返回的结果本身就是一个 pandas DataFrame,因此它具备与从 CSV 文件读取的数据相同的方法和特征。可以使用 Python 内置的 type 函数轻松验证这一点,如以下代码所示:

type(df.describe(include='all'))

输出将如下所示:

pandas.core.frame.DataFrame

现在我们已经有了数据集的摘要,让我们深入研究一下,以便更好地理解可用数据。

注意

对可用数据的全面理解对于任何监督学习问题都至关重要。数据的来源和类型、收集的方式,以及可能由于收集过程中的错误所导致的问题,都对最终模型的性能产生影响。

希望到现在为止,你已经习惯使用 pandas 提供数据的高级概览。接下来,我们将花一些时间更深入地研究数据。

练习 1.04:使用聚合方法

我们已经看到如何索引或选择 DataFrame 中的行或列,并使用高级索引技术根据特定标准过滤可用数据。另一个方便的选择方法是 groupby 方法,它提供了一种快速选择一组数据的方法,并通过 DataFrameGroupBy 对象提供额外的功能。本练习是练习 1.01《加载并总结泰坦尼克号数据集》的延续:

使用 groupby 方法对 Embarked 列下的数据进行分组,以找出 Embarked 有多少种不同的值:

embarked_grouped = df.groupby('Embarked')

print(f'共有{len(embarked_grouped)}个 Embarked 组')

输出将如下所示:

共有 3 个 Embarked 组

显示 embarked_grouped.groups 的输出,以查看 groupby 方法实际执行了什么操作:

embarked_grouped.groups

输出将如下所示:

图 1.16:embarked_grouped.groups 的输出

图 1.16:embarked_grouped.groups 的输出

我们可以看到,这三个组分别是 C、Q 和 S,并且 embarked_grouped.groups 实际上是一个字典,其中键是组,值是属于该组的行或索引。

使用 iloc 方法检查第 1 行,并确认它属于 Embarked 组 C:

df.iloc[1]

输出将如下所示:

图 1.17:检查第 1 行

图 1.17:检查第 1 行

由于这些组是字典类型,我们可以遍历它们并对每个单独的组执行计算。计算每个组的平均年龄,如下所示:

对于 name, group 在 embarked_grouped 中:

print(name, group.Age.mean())

输出将如下所示:

C 32.33216981132075

Q 28.63

S 29.245204603580564

另一种选择是使用 aggregate 方法,简称 agg,并提供一个函数来应用于各列。使用 agg 方法来确定每个组的均值:

embarked_grouped.agg(np.mean)

输出将如下所示:

图 1.18:使用 agg 方法

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-0S7SC7MC.jpg)

图 1.18:使用 agg 方法

那么,agg 究竟是如何工作的,我们可以传递什么类型的函数给它呢?在回答这些问题之前,我们首先需要考虑 DataFrame 中每一列的数据类型,因为每一列都会通过此函数产生我们所看到的结果。每个 DataFrame 由一组 pandas 序列数据的列组成,这在许多方面类似于一个列表。因此,任何可以接受列表或类似可迭代对象并计算出单一结果值的函数,都可以与 agg 一起使用。

定义一个简单的函数,返回列中的第一个值,然后将该函数传递给 agg,作为示例:

def first_val(x):

return x.values[0]

embarked_grouped.agg(first_val)

输出将如下所示:

图 1.19:使用.agg 方法与函数

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-0S7SC7MC.jpg)

图 1.19:使用.agg 方法与函数

注意

要访问此特定部分的源代码,请参考 https://packt.live/2NlEkgM。

你也可以在 https://packt.live/2AZnq51 上在线运行此示例。你必须执行整个 Notebook 才能获得预期的结果。

在这个练习中,我们展示了如何在 DataFrame 中对数据进行分组,从而允许使用.agg()应用额外的函数,比如计算组的均值。这类操作在分析和准备数据时非常常见。

分位数

前一个练习展示了如何找到均值。在统计数据分析中,我们也经常感兴趣的是数据集中某个值以下或以上,某个比例的点会落在其中。这些点被称为分位数。例如,如果我们有一个从 1 到 10,001 的数字序列,25%的分位数值为 2,501。也就是说,在值 2,501 处,25%的数据位于该截止值以下。分位数在数据可视化中经常使用,因为它们能传达数据分布的感觉。特别是,Matplotlib 中的标准箱线图会绘制一个由四个分位数中的第一和第三个分位数界定的箱体。

例如,让我们建立以下 DataFrame 的 25%分位数:

import pandas as pd

df = pd.DataFrame({"A":[1, 6, 9, 9]})

计算 DataFrame 的 25%分位数

df.quantile(0.25, axis = 0)

输出将如下所示:

A 4.75

Name: 0.25, dtype: float64

从前面的输出中可以看出,4.75 是 DataFrame 的 25%分位数值。

注意

有关分位数方法的更多信息,请参考 https://pandas.pydata.org/pandas-docs/stable/reference/frame.html。

本书稍后我们将继续使用分位数的概念,深入探讨数据。

Lambda 函数

实现 agg 的一种常见且有用的方法是通过使用 Lambda 函数。

Lambda 或匿名函数(在其他语言中也称为内联函数)是小型的单表达式函数,可以声明并使用,而无需通过 def 关键字进行正式的函数定义。Lambda 函数本质上是为了方便而提供的,通常不用于长时间的复杂任务。Lambda 函数的主要优点是它们可以在不适合或不方便定义函数的地方使用,例如在其他表达式或函数调用中。Lambda 函数的标准语法如下(总是以 lambda 关键字开头):

lambda <输入值>: <计算返回的值>

现在我们来做一个练习,创建一些有趣的 Lambda 函数。

练习 1.05:创建 Lambda 函数

在本练习中,我们将创建一个 Lambda 函数,返回列中的第一个值,并与 agg 一起使用。本练习是练习 1.01“加载和汇总泰坦尼克数据集”的延续:

将 first_val 函数写为 Lambda 函数,并传递给 agg:

embarked_grouped = df.groupby('Embarked')

embarked_grouped.agg(lambda x: x.values[0])

输出结果如下:

图 1.20:使用 agg 方法和 Lambda 函数

图 1.20:使用 agg 方法和 Lambda 函数

显然,我们得到相同的结果,但请注意,Lambda 函数的使用更加方便,尤其是考虑到它仅仅是暂时使用的。

我们还可以通过列表将多个函数传递给 agg,从而在数据集上应用这些函数。传递 Lambda 函数以及 NumPy 的均值和标准差函数,如下所示:

embarked_grouped.agg([lambda x: x.values[0], np.mean, np.std])

输出结果如下:

图 1.21:使用 agg 方法和多个 Lambda 函数

图 1.21:使用 agg 方法和多个 Lambda 函数

将 numpy.sum 应用于 Fare 列,并将 Lambda 函数应用于 Age 列,通过传递一个包含列名(作为字典的键)和相应函数(作为字典的值)的字典给 agg,从而使得可以对 DataFrame 中的不同列应用不同的函数:

embarked_grouped.agg({'Fare': np.sum, \

'Age': lambda x: x.values[0]})

输出结果如下:

图 1.22:使用 agg 方法和包含不同列的字典

图 1.22:使用 agg 方法和包含不同列的字典

最后,使用多个列执行 groupby 方法。提供包含列列表(性别和登船港口)以进行分组,如下所示:

age_embarked_grouped = df.groupby(['Sex', 'Embarked'])

age_embarked_grouped.groups

输出结果如下:

图 1.23:使用 groupby 方法和多个列

图 1.23:使用 groupby 方法进行多列分组

与仅通过 Embarked 列进行分组计算时类似,我们可以看到这里返回了一个字典,其中键是 Sex 和 Embarked 列的组合,以元组形式返回。字典中的第一个键值对是元组 ('Male', 'S'),值对应于具有该特定组合的行索引。每个 Sex 和 Embarked 列中唯一值的组合都会有一个键值对。

注意

要查看此特定部分的源代码,请参考 https://packt.live/2B1jAZl。

你也可以在 https://packt.live/3emqwPe 上在线运行这个示例。你必须执行整个 Notebook 才能获得期望的结果。

这标志着我们对数据检查和处理的简要探索的结束。接下来,我们将进入数据科学中最重要的话题之一——数据质量。

数据质量考虑

在任何机器学习问题中,无论是监督学习还是无监督学习,所使用数据的质量对最终模型的表现至关重要,因此在规划任何机器学习项目时,数据质量应处于首要位置。作为一个简单的经验法则,如果你拥有干净的数据、足够的数量,并且输入数据类型与期望输出之间有良好的相关性,那么选用哪种类型的监督学习模型及其具体细节对获得良好的结果变得不那么重要。

然而,实际上这种情况很少发生。通常,关于数据量、数据质量或信噪比、输入与输出之间的相关性,或三者的某种组合,都会存在一些问题。因此,我们将在本章的最后一部分考虑可能出现的一些数据质量问题以及解决这些问题的一些机制。我们之前提到,在任何机器学习问题中,深入了解数据集对于构建高性能模型至关重要。

在研究数据质量并尝试解决数据中存在的一些问题时,尤其如此。如果没有对数据集的全面理解,数据清理过程中可能会引入额外的噪声或其他未预见的问题,从而导致性能进一步下降。

注意

Titanic 数据集的详细描述和其中包含的数据类型可以在 pandas 中的加载数据部分找到。如果你需要快速回顾,可以回去复习那部分内容。

管理缺失数据

正如我们之前讨论的,pandas 读取带有缺失值的数据的能力既是一种福音也是一种诅咒,可以说是在继续开发我们的监督学习模型之前需要解决的最常见问题。最简单但不一定最有效的方法是简单地删除或忽略那些缺失数据的条目。我们可以在 pandas 中轻松地使用 DataFrame 上的 dropna 方法来实现这一点:

完整数据 = df.dropna()

仅仅删除具有缺失数据的行有一个非常重要的后果,那就是我们可能会丢失很多重要信息。这在泰坦尼克号数据集中非常明显,因为很多行包含缺失数据。如果我们简单地忽略这些行,我们将从 1,309 个样本开始,最终只剩下 183 个条目。使用略多于 10% 的数据开发一个合理的监督学习模型将非常困难。以下代码显示了使用 dropna() 方法处理缺失条目的示例:

len(df)

前面的输入产生以下输出:

1309

dropna() 方法的实现如下:

len(df.dropna())

前面的输入产生以下输出:

183

因此,除了早期的探索阶段外,简单丢弃所有具有无效信息的行通常是不可接受的。我们可以确定哪些行实际上缺少信息,以及缺失信息是某些列特有的问题还是整个数据集中所有列都存在的问题。我们也可以使用 aggregate 来帮助我们:

df.aggregate(lambda x: x.isna().sum())

输出如下:

船舱 1014

登船港口 2

票价 1

客舱等级 0

票号 0

年龄 263

姓名 0

父母/子女 0

性别 0

兄弟姐妹/配偶 0

生还 418

类型:int64

现在这很有用!我们可以看到绝大多数缺失信息在船舱列中,一些在年龄中,还有一些在生还中。这是数据清洗过程中我们可能需要做出明智判断的首次之一。

对于船舱列我们想要做什么?这里缺失的信息太多了,事实上,可能无法以任何合理的方式使用它。我们可以尝试通过查看姓名、年龄和父母/兄弟姐妹的数量来恢复信息,看看是否可以将一些家庭匹配在一起提供信息,但在这个过程中会有很多不确定性。我们也可以简化列,使用船上舱位的级别而不是确切的舱位号,这样可能会更好地与姓名、年龄和社会地位相关联。这很不幸,因为船舱和生还之间可能存在很好的相关性,也许那些在船的下层甲板上的乘客可能更难撤离。我们可以仅检查具有有效船舱值的行,看看舱位信息是否具有任何预测能力;但是,目前,我们将简单地将船舱视为一个合理的输入(或特征)而忽略。

我们可以看到,Embarked 和 Fare 列之间只有三条缺失样本。如果我们决定模型需要这两列,那么直接删除这些行是一个合理的选择。我们可以使用索引技巧来实现这一点,其中 ~ 代表取反操作,或者反转结果(也就是说,当 df.Embarked 和 df.Fare 不为 NaN 时):

df_valid = df.loc[(~df.Embarked.isna()) & (~df.Fare.isna())]

缺失的年龄值更为有趣,因为有太多缺失年龄值的行,无法简单地将它们丢弃。但我们在这里有更多的选项,因为我们可以对一些合理的值有更高的信心。最简单的选择是直接用数据集的平均年龄填充缺失的年龄值:

df_valid[['Age']] = df_valid[['Age']]\

.fillna(df_valid.Age.mean())

这样是可以的,但可能有更好的填充数据的方式,而不仅仅是给所有 263 个人赋相同的值。记住,我们的目标是清理数据,以最大化输入特征和生存率的预测能力。虽然简单,但让每个人都有相同的值似乎不太合理。如果我们查看每个票务等级(Pclass)成员的平均年龄呢?这可能会提供一个更好的估计,因为随着票务等级从 1 到 3 下降,平均年龄也逐渐降低,以下代码可以看到这一点:

df_valid.loc[df.Pclass == 1, 'Age'].mean()

上述输入会产生以下输出:

37.956806510096975

第二等级的平均年龄如下:

df_valid.loc[df.Pclass == 2, 'Age'].mean()

上述输入会产生以下输出:

29.52440879717283

第三等级的平均年龄如下:

df_valid.loc[df.Pclass == 3, 'Age'].mean()

上述输入会产生以下输出:

26.23396338788047

如果我们同时考虑人的性别和票务等级(社会地位)呢?这里的平均年龄也会有所不同吗?让我们来探讨一下:

for name, grp in df_valid.groupby(['Pclass', 'Sex']):

print('%i' % name[0], name[1], '%0.2f' % grp['Age'].mean())

输出将如下所示:

1 女性 36.84

1 男性 41.03

2 女性 27.50

2 男性 30.82

3 女性 22.19

3 男性 25.86

我们可以看到,所有票务等级中的男性通常年龄较大。性别和票务等级的组合提供的信息比仅仅用平均年龄填充缺失值要丰富得多。为此,我们将使用 transform 方法,它可以对 Series 或 DataFrame 的内容应用一个函数,并返回另一个具有变换值的 Series 或 DataFrame。当与 groupby 方法结合使用时,这非常强大:

mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\

transform(lambda x: \

x.fillna(x.mean()))

df_valid.loc[:, 'Age'] = mean_ages

这两行代码包含了很多内容,我们来逐步解析。首先看看第一行:

mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\

transform(lambda x: \

x.fillna(x.mean()))

我们已经熟悉 df_valid.groupby(['Pclass', 'Sex'])['Age'],它根据票务等级和性别对数据进行分组,并返回仅包含 Age 列的数据。lambda x: x.fillna(x.mean()) Lambda 函数将输入的 pandas 系列填充 NaN 值为该系列的均值。

第二行将 mean_ages 中的填充值分配给 Age 列。注意使用了 loc[:, 'Age']索引方法,表示将 Age 列中的所有行赋值为 mean_ages 中的值:

df_valid.loc[:, 'Age'] = mean_ages

我们已经描述了几种填充 Age 列缺失值的方法,但这绝不是一个详尽的讨论。我们可以使用更多的方法来填充缺失数据:我们可以在分组数据的均值的一个标准差范围内应用随机值,也可以考虑根据性别、父母/子女数量(Parch)或兄弟姐妹数量,甚至是票务等级、性别和父母/子女数量来对数据进行分组。关于此过程中做出的决策,最重要的是最终的预测准确性。我们可能需要尝试不同的选项,重新运行模型,并考虑对最终预测准确性的影响。因此,选择能为模型提供最大预测能力的特征或组件。你会发现,在这个过程中,你会尝试不同的特征,运行模型,查看最终结果,然后重复这个过程,直到你对性能感到满意。

这个监督学习问题的最终目标是根据我们所掌握的信息预测泰坦尼克号乘客的生存情况。所以,这意味着“是否生存”这一列提供了我们训练的标签。如果我们缺少 418 个标签该怎么办?如果这是一个我们可以控制数据收集并访问数据来源的项目,我们显然可以通过重新收集数据或请求明确标签来进行修正。但在泰坦尼克号数据集中,我们没有这种能力,因此必须做出另一个有根据的判断。一个方法是从训练数据中删除这些行,之后用在(更小的)训练集上训练的模型来预测其他人的结果(这实际上是 Kaggle 泰坦尼克号比赛中的任务)。在某些商业问题中,我们可能没有简单忽略这些行的选项;我们可能在尝试预测一个非常关键过程的未来结果,而这些数据就是我们所拥有的。我们可以尝试一些无监督学习技术,看看是否能发现一些生存信息的模式以供使用。然而,通过无监督技术来估计真实标签,可能会给数据集引入显著的噪声,从而降低我们准确预测生存情况的能力。

类别不平衡

缺失数据并不是数据集中可能存在的唯一问题。类别不平衡——即一个类别或多个类别的样本数量大大超过其他类别——可能是一个显著问题,特别是在分类问题中(我们将在第五章“分类技术”中深入讨论分类问题),即我们试图预测一个样本属于哪个类别。通过查看我们的“Survived”列,可以看到数据集中死亡的人数(Survived 等于 0)远多于生还的人数(Survived 等于 1),如下代码所示:

len(df.loc[df.Survived ==1])

输出如下:

342

死亡人数为:

len(df.loc[df.Survived ==0])

输出如下:

549

如果我们不考虑这个类别不平衡问题,我们模型的预测能力可能会大大降低,因为在训练过程中,模型只需要猜测这个人没有生还,就可以正确预测 61%的时间(549 / (549 + 342))。如果实际生还率是 50%,那么在应用于未见数据时,我们的模型会过于频繁地预测“没有生还”。

管理类别不平衡有几种可选方法,其中一种方法,类似于缺失数据场景,是随机删除过度代表类的样本,直到达到平衡为止。再次强调,这种方法并不理想,甚至可能不适当,因为它涉及忽略可用的数据。一个更具建设性的例子可能是通过随机复制数据集中不足代表类的样本来进行过采样,以增加样本数量。虽然删除数据可能导致由于丢失有用信息而产生准确性问题,但对不足代表类进行过采样可能会导致无法预测未见数据的标签,这也被称为过拟合(我们将在第六章“集成建模”中讨论)。

向过采样数据的输入特征中添加一些随机噪声可能有助于防止一定程度的过拟合,但这在很大程度上取决于数据集本身。与缺失数据一样,检查任何类别不平衡修正对整体模型性能的影响非常重要。使用 append 方法将更多数据复制到 DataFrame 中是相对简单的,append 方法的工作方式与列表非常相似。如果我们想把第一行复制到 DataFrame 的末尾,可以这样做:

df_oversample = df.append(df.iloc[0])

样本量过小

机器学习领域可以视为更大统计学领域的一个分支。因此,置信度和样本量的原理也可以用于理解小数据集的问题。回想一下,如果我们从一个高方差的数据源中获取测量数据,那么这些测量的 不确定性程度也会很高,并且为了在均值的值上获得特定的置信度,我们需要更多的样本。样本原理可以应用于机器学习数据集。那些特征方差较大的数据集,通常需要更多的样本才能获得合理的性能,因为更高的置信度也是必需的。

有一些技术可以用来弥补样本量减少的问题,比如迁移学习。然而,这些技术超出了本书的范围。然而,最终来说,使用小数据集所能做的事有限,显著的性能提升可能只有在增加样本量后才会发生。

活动 1.01:实现 Pandas 函数

在这个活动中,我们将测试我们在本章中学到的各种 Pandas 函数。我们将使用相同的 Titanic 数据集进行练习。

执行的步骤如下:

打开一个新的 Jupyter 笔记本。

使用 Pandas 加载 Titanic 数据集,并对数据集使用 head 函数以显示数据集的顶部行。描述所有列的总结数据。

我们不需要 Unnamed: 0 列。在练习 1.03: 高级索引和选择中,我们演示了如何使用 del 命令删除该列。还有其他方法可以删除此列吗?不使用 del 删除此列。

计算 DataFrame 列的均值、标准差、最小值和最大值,而不使用 describe。注意,可以使用 df.min()df.max() 函数找到最小值和最大值。

使用 quantile 方法获取 33%、66% 和 99%的分位数值。

使用 groupby 方法找出每个舱位的乘客数量。

使用选择/索引方法计算每个舱位的乘客数量。可以使用 unique() 方法找出每个舱位的唯一值。

确认第 6 步和第 7 步的答案是否匹配。

确定第三舱中最年长的乘客是谁。

对于许多机器学习问题,常常需要将数值数据缩放到 0 和 1 之间。使用 agg 方法与 Lambda 函数将 Fare 和 Age 列的数据缩放到 0 和 1 之间。

数据集中有一位个体没有列出票价值,可以通过以下方式确认:

df_nan_fare = df.loc[(df.Fare.isna())]

df_nan_fare

输出将如下所示:

图 1.24:没有列出票价的个体

图 1.24:没有列出票价的个体

使用 groupby 方法,将主数据框中该行的 NaN 值替换为与该行对应的相同类别和登船地点的平均票价值。

输出结果如下:

图 1.25:没有列出票价详情的个人输出

图 1.25:没有列出票价详情的个人输出

注意

本活动的解决方案可以通过此链接找到。

通过本活动,我们回顾了所有基本的数据加载、检查和操作方法,以及一些基本的总结统计方法。

总结

本章介绍了监督学习的概念,并提供了一些应用案例,包括自动化手动任务,如识别 1960 年代和 1980 年代的发型。在此介绍中,我们遇到了标记数据集的概念,以及将一个信息集(输入数据或特征)映射到相应标签的过程。我们通过实践方法,使用 Jupyter 笔记本和功能强大的 pandas 库,讲解了数据加载和清理的过程。请注意,本章仅涵盖了 pandas 功能的一小部分,实际上,整个书籍都可以专门讲解这个库。建议您熟悉阅读 pandas 文档,并通过实践继续提升您的 pandas 技能。本章的最后部分讨论了在开发高性能监督学习模型时需要考虑的一些数据质量问题,包括缺失数据、类别不平衡和样本量过小等问题。我们讨论了管理这些问题的多种选择,并强调了将这些缓解措施与模型性能进行对比检查的重要性。在下一章中,我们将扩展本章所涉及的数据清理过程,并研究数据探索和可视化过程。数据探索是任何机器学习解决方案中的关键部分,因为如果没有对数据集的全面了解,几乎不可能对所提供的信息进行建模。

第三章:2. 探索性数据分析与可视化

概述

本章带领我们了解如何对一个新的数据集进行探索和分析。到本章结束时,你将能够解释数据探索的重要性,并能够传达数据集的汇总统计信息。你将能够可视化数据中缺失值的模式,并能够适当地替换空值。你将学会识别连续特征、分类特征,并可视化各个变量的值分布。你还将能够使用相关性和可视化来描述和分析不同类型变量之间的关系。

介绍

假设我们有一个问题陈述,涉及预测某次地震是否引发了海啸。我们如何决定使用什么模型?我们对现有的数据了解多少?什么都不知道!但如果我们不了解数据,最终可能会建立一个不太可解释或不可靠的模型。在数据科学中,彻底理解我们所处理的数据非常重要,以便生成高度信息化的特征,并因此构建准确而强大的模型。为了获得这种理解,我们对数据进行探索性分析,看看数据能告诉我们关于特征和目标变量(你试图通过其他变量预测的值)之间关系的信息。了解数据甚至有助于我们解释所构建的模型,并找出改进其准确性的方法。我们采取的做法是让数据揭示其结构或模型,这有助于我们获得一些新的、往往是意想不到的见解。

我们将首先简要介绍探索性数据分析,然后逐步解释汇总统计和中心值。本章还将教你如何查找和可视化缺失值,并描述处理缺失值问题的各种填充策略。接下来的部分将专注于可视化。具体来说,本章教你如何创建各种图表,如散点图、直方图、饼图、热图、配对图等。让我们从探索性数据分析开始。

探索性数据分析(EDA)

探索性数据分析(EDA)被定义为一种分析数据集并总结其主要特征的方法,通过这种方法得出有用的结论,通常采用可视化方法。

EDA 的目的是:

发现数据集中的模式

发现异常值

对数据行为形成假设

验证假设

从基本的摘要统计到复杂的可视化帮助我们直观地理解数据本身,这在形成关于数据的新假设并揭示哪些参数影响目标变量时极为重要。通常,发现目标变量如何在单一特征上变化,会给我们提供该特征可能有多重要的指示,而多个特征组合的变化有助于我们提出新的有信息量的特征工程思路。

大多数探索性分析和可视化的目的是理解特征与目标变量之间的关系。这是因为我们希望找出我们所拥有的数据与我们要预测的值之间存在(或不存在)什么关系。

EDA 可以告诉我们:

有缺失值、脏数据或异常值的特征

有助于目标识别的具有信息性的特征

特征与目标之间的关系类型

数据可能需要的进一步特征,我们当前并没有

可能需要单独考虑的边缘情况

可能需要应用于数据集的过滤条件

存在不正确或虚假数据点

现在我们已经了解了 EDA 的重要性以及它能告诉我们什么,接下来让我们讨论 EDA 到底涉及什么。EDA 可以包括从查看基本的摘要统计数据到可视化多变量之间的复杂趋势。尽管如此,即便是简单的统计数据和图表也可以是强大的工具,因为它们可能揭示出关于数据的重要事实,这些事实可能会改变我们建模的视角。当我们看到表示数据的图表时,相比于仅仅是原始数据和数字,我们能更容易地检测到趋势和模式。这些可视化还可以让我们提出“如何?”和“为什么?”这样的问题,并对数据集形成可以通过进一步可视化验证的假设。这是一个持续的过程,最终会加深我们对数据的理解。

我们将用于探索性分析和可视化的数据集来自 NOAA 的显著地震数据库,该数据库作为公共数据集可以在 Google BigQuery 上访问(表 ID:'bigquery-public-data.noaa_significant_earthquakes.earthquakes')。我们将使用其中一部分列,相关元数据可以在console.cloud.google.com/bigquery?project=packt-data&folder&organizationId&p=bigquery-public-data&d=noaa_significant_earthquakes&t=earthquakes&page=table获取,并将其加载到 pandas DataFrame 中进行探索。我们主要将使用 Matplotlib 进行大多数可视化,同时也会使用 Seaborn 和 Missingno 库进行部分可视化。然而需要注意的是,Seaborn 只是 Matplotlib 功能的封装,因此,使用 Seaborn 绘制的图形也可以通过 Matplotlib 绘制。我们将通过这两种库的可视化来保持内容的趣味性。

探索和分析将基于一个示例问题:根据我们拥有的数据,我们想预测地震是否引发了海啸。这将是一个分类问题(更多内容请参见第五章,分类技术),目标变量是 flag_tsunami 列。

在我们开始之前,首先导入所需的库,这些库将用于我们大多数的数据操作和可视化。

在 Jupyter Notebook 中,导入以下库:

import json

import pandas as pd

import numpy as np

import missingno as msno

from sklearn.impute import SimpleImputer

import matplotlib.pyplot as plt

import seaborn as sns

我们还可以读取包含每列数据类型的元数据,这些数据以 JSON 文件的形式存储。使用以下命令来读取。此命令以可读格式打开文件,并使用 json 库将文件读取为字典:

with open('..\dtypes.json', 'r') as jsonfile:

dtyp = json.load(jsonfile)

注意

前述命令的输出可以在这里找到:https://packt.live/3a4Zjhm

概要统计和中心值

为了了解我们的数据真实情况,我们使用一种称为数据概况的技术。数据概况被定义为检查现有信息来源(例如,数据库或文件)中的数据,并收集该数据的统计信息或信息性摘要的过程。其目的是确保你充分理解数据,并能够及早识别数据可能在项目中带来的挑战,这通常通过总结数据集并评估其结构、内容和质量来实现。

数据概况包括收集描述性统计信息和数据类型。常见的数据概况命令包括你之前见过的命令,例如 data.describe()、data.head() 和 data.tail()。你还可以使用 data.info(),它会告诉你每列中有多少非空值,以及这些值的数据类型(非数字类型表示为对象类型)。

练习 2.01:总结我们的数据集统计信息

在本练习中,我们将使用之前学到的概要统计函数,初步了解我们的数据集:

注意

数据集可以在我们的 GitHub 仓库中找到,链接如下:https://packt.live/2TjU9aj

将地震数据读取到 pandas DataFrame 中,并使用我们在前一节中使用 json 库读取的 dtyp 字典,指定 CSV 中每列的数据类型。首先加载所需的库和我们已经准备好的包含数据类型的 JSON 文件。你可以在读取数据之前检查数据类型:

import json

import pandas as pd

import numpy as np

import missingno as msno

from sklearn.impute import SimpleImputer

import matplotlib.pyplot as plt

import seaborn as sns

with open('../dtypes.json', 'r') as jsonfile:

dtyp = json.load(jsonfile)

dtyp

输出将如下所示:

图 2.1:检查数据类型

图 2.1:检查数据类型

使用 data.info() 函数获取数据集的概览:

data = pd.read_csv('../Datasets/earthquake_data.csv', dtype = dtyp)

data.info()

输出将如下所示:

图 2.2:数据集概览

图 2.2:数据集概览

打印数据集的前五行和最后五行。前五行打印如下:

data.head()

data.tail()

输出将如下所示:

图 2.3:前五行和最后五行

图 2.3:前五行和最后五行

我们可以从这些输出中看到,数据集有 28 列,但并不是所有的列都显示出来。只显示了前 10 列和最后 10 列,省略号表示其中有未显示的列。

使用 data.describe() 查找数据集的汇总统计信息。运行 data.describe().T:

data.describe().T

这里,.T 表示我们正在对应用它的 DataFrame 进行转置,即将列变成行,反之亦然。将其应用于 describe() 函数,使我们能更容易地查看输出,每一行在转置后的 DataFrame 中对应一个特征的统计信息。

我们应该得到类似这样的输出:

图 2.4:汇总统计信息

图 2.4:汇总统计信息

注意

要访问该特定部分的源代码,请参考 packt.live/2Yl5qer

你也可以在线运行这个示例,访问 packt.live/2V3I76D。你必须执行整个 Notebook 才能得到期望的结果。

请注意,这里 describe() 函数只显示数值型列的统计信息。这是因为我们无法对非数值型列计算统计数据(尽管我们可以像稍后所见那样可视化它们的值)。

缺失值

当数据点的某个特征没有记录值(即为空值)时,我们称数据为缺失数据。在实际数据集中出现缺失值是不可避免的;没有数据集是完美的。然而,理解数据缺失的原因以及是否有某些因素影响了数据丢失非常重要。理解并识别这一点可以帮助我们以合适的方式处理其余数据。例如,如果数据是随机缺失的,那么剩余的数据很可能仍然能够代表整个数据集。但如果缺失的数据并非随机缺失,而我们假设它是随机的,这可能会导致我们的分析和后续建模出现偏差。

让我们来看一下缺失数据的常见原因(或机制):

完全随机缺失(MCAR):如果数据集中缺失的值与任何其他记录的变量或外部参数之间没有任何相关性,那么这些缺失值被称为 MCAR。这意味着其余数据仍然能够代表整个群体,尽管这种情况很少发生,且假设缺失数据是完全随机的通常是不现实的。

例如,在一项研究中,如果要确定 K12 学生肥胖的原因,MCAR 的情况是父母忘记带孩子去诊所参加研究。

随机缺失(MAR):如果数据缺失的原因与已记录的数据相关,而与未记录的数据无关,那么这些数据就被称为 MAR。由于无法通过统计方法验证数据是否为 MAR,我们只能依赖是否存在合理的可能性来判断。

以 K12 研究为例,缺失数据的原因是父母搬到了其他城市,导致孩子不得不退出研究;缺失与研究本身无关。

非随机缺失(MNAR):既不是 MAR 也不是 MCAR 的数据被称为 MNAR。这种情况通常是不可忽略的非响应情况,也就是说,缺失的变量值与其缺失的原因有关。

继续使用案例研究的例子,如果父母因为研究的性质感到不悦,不希望孩子被欺负,因此将孩子从研究中撤出,那么数据就是 MNAR(非随机缺失)。

查找缺失值

所以,现在我们知道了为什么了解数据缺失背后的原因如此重要,接下来让我们讨论如何在数据集中找到这些缺失值。对于一个 pandas DataFrame,最常见的做法是使用 .isnull() 方法,这个方法会在 DataFrame 上创建一个缺失值的掩码(即一个布尔值的 DataFrame),用来指示缺失值的位置——任何位置上为 True 的值表示该位置是缺失值,而 False 则表示该位置有有效值。

注意

.isnull() 方法与 .isna() 方法可以互换使用,在 pandas DataFrame 中,两者做的事情完全相同——之所以有两个方法做同一件事,是因为 pandas DataFrame 最初是基于 R DataFrame 开发的,因此复用了很多 R DataFrame 的语法和思想。

缺失数据是否随机可能一开始并不明显。通过两种常见的可视化技术,我们可以发现数据集中特征的缺失值性质:

缺失矩阵:这是一种数据密集型的显示方式,可以让我们快速可视化数据完成情况的模式。它可以帮助我们快速查看一个特征(以及多个特征)中的缺失值是如何分布的,缺失值的数量是多少,以及它们与其他特征的关联频率。

空值相关性热图:该热图直观地描述了每对特征之间的空值关系(或数据完整性关系);即它衡量了一个变量的存在或缺失如何影响另一个变量的存在。

类似于常规的相关性分析,空值相关性值的范围从 -1 到 1,前者表示一个变量出现时,另一个变量肯定不出现,后者则表示两个变量同时存在。值为 0 表示一个变量的空值对另一个变量的空值没有影响。

练习 2.02:可视化缺失值

让我们首先通过查看每个特征的缺失值数量和百分比,来分析缺失值的性质,然后使用 Python 中的 missingno 库绘制空值矩阵和相关性热图。我们将继续使用前面练习中的相同数据集。

请注意,本练习是练习 2.01:总结我们的数据集统计信息的延续。

以下步骤将帮助你完成这个练习,以可视化数据集中的缺失值:

计算每列缺失值的数量和百分比,并按降序排列。我们将使用 .isnull() 函数对 DataFrame 进行掩码操作。然后,可以使用 .sum() 函数计算每列的空值数量。类似地,空值的比例可以通过对 DataFrame 掩码使用 .mean() 并乘以 100 来转换为百分比。

然后,我们使用 pd.concat() 函数将空值的总数和百分比合并到一个 DataFrame 中,接着按照缺失值的百分比对行进行排序,并打印出该 DataFrame:

mask = data.isnull()

total = mask.sum()

percent = 100*mask.mean()

missing_data = pd.concat([total, percent], axis=1,join='outer', \

keys=['count_missing', 'perc_missing'])

missing_data.sort_values(by='perc_missing', ascending=False, \

inplace=True)

missing_data

输出结果如下:

图 2.5:每列缺失值的数量和百分比

图 2.5:每列缺失值的数量和百分比

在这里,我们可以看到 state、total_damage_millions_dollars 和 damage_millions_dollars 列的缺失值超过 90%,意味着数据集中这些列的可用数据点不到 10%。另一方面,year、flag_tsunami、country 和 region_code 列没有缺失值。

绘制空值矩阵。首先,我们使用 .any() 函数从前一步骤的 DataFrame mask 中找出包含空值的列。然后,我们使用 missingno 库绘制一个空值矩阵,针对数据集中仅包含缺失值的列,从 500 条随机数据中取样:

nullable_columns = data.columns[mask.any()].tolist()

msno.matrix(data[nullable_columns].sample(500))

plt.show()

输出结果如下:

图 2.6:缺失值矩阵

图 2.6:缺失值矩阵

在这里,黑色线条表示非空值,而白色线条表示该列中存在缺失值。总体来看,location_name 列似乎完全填充(我们从前面的步骤中得知,这一列实际上只有一个缺失值),而 latitude 和 longitude 列似乎大部分完整,但有些地方较为稀疏。

右侧的火花图总结了数据完整性的整体形态,并指出了数据集中最大和最小缺失值所在的行。请注意,这仅适用于 500 个数据点的样本。

绘制缺失值相关性热图。我们将使用 missingno 库绘制缺失值相关性热图,仅针对那些存在缺失值的列:

msno.heatmap(data[nullable_columns], figsize=(18,18))

plt.show()

输出结果如下:

图 2.7:缺失值相关性热图

图 2.7:缺失值相关性热图

在这里,我们还可以看到一些标有<1 的框:这仅意味着这些情况下的相关性值都接近 1.0,但仍然不是完全一致。我们可以看到在 injuries 和 total_injuries 之间有<1 的值,这意味着每个类别中的缺失值是相关的。我们需要深入挖掘,了解这些缺失值是否因为基于相同或类似的信息而相关,或者是出于其他原因。

注意

要访问此特定部分的源代码,请参考 https://packt.live/2YSXq3k。

您也可以在 https://packt.live/2Yn3Us7 上在线运行此示例。您必须执行整个 Notebook 才能获得所需的结果。

缺失值的填充策略

处理列中缺失值有多种方法。最简单的方法是直接删除包含缺失值的行;然而,这可能会导致丧失其他列中的有价值信息。另一种选择是对数据进行填充,即用从已知数据部分推断出的有效值替换缺失值。常见的填充方法如下:

创建一个与其他值不同的新值,以替换列中的缺失值,从而区分这些行。然后,使用非线性机器学习算法(如集成模型或支持向量机),将这些值分离出来。

使用列中的适当中心值(均值、中位数或众数)来替换缺失值。

使用模型(例如 K 近邻或高斯混合模型)来学习最佳值,以替换缺失值。

Python 有一些函数可以用来将列中的空值替换为静态值。实现这一点的一种方式是使用 pandas 本身的.fillna(0)函数:这里没有歧义——替换空数据点的静态值就是传递给函数的参数(括号中的值)。

然而,如果列中空值的数量较大,并且无法立即确定可以用来替换每个空值的合适中心值,那么我们可以从建模的角度出发,选择删除含有空值的行,或者直接删除整个列,因为它可能不会增加任何重要的价值。这可以通过在 DataFrame 上使用.dropna()函数来完成。可以传递给该函数的参数如下:

axis: 这定义了是删除行还是列,具体取决于为参数分配 0 或 1 的值。

how: 可以为此参数分配“all”或“any”值,表示行/列是否应包含所有空值才能删除该列,或者是否应在至少有一个空值时删除该列。

thresh: 这定义了行/列应具有的最小空值数量,才会被删除。

此外,如果无法确定一个适当的替代值来填充分类特征的空值,那么删除该列的一个可能替代方案是为该特征创建一个新的类别,用来表示空值。

注意

如果可以通过直观理解或领域知识立即得出一个适合的值来替换列中的空值,那么我们可以当场进行替换。请记住,任何此类数据更改应在代码中进行,而绝不是直接在原始数据上操作。这样做的一个原因是它使得将来可以轻松地更新策略;另一个原因是它使得其他人如果以后审查这项工作时,可以清楚地看到在哪里进行了更改。直接更改原始数据可能会导致数据版本控制问题,并使得其他人无法重现你的工作。在很多情况下,推断会在后续的探索阶段变得更加明显。在这种情况下,我们可以在找到合适的方法时,随时替换空值。

练习 2.03:使用 Pandas 进行填充

让我们来看一下缺失值,并在具有至少一个空值的基于时间(连续)的特征中将它们替换为零(如月份、日期、小时、分钟和秒)。我们这么做是因为对于那些没有记录值的情况,可以安全地假设事件发生在时间段的开始。这项操作是练习 2.02:可视化缺失值的延续:

创建一个包含我们想要填充的列名的列表:

time_features = ['month', 'day', 'hour', 'minute', 'second']

使用 .fillna() 填充空值。我们将使用 pandas 自带的 .fillna() 函数,并传递 0 作为参数来替换这些列中的缺失值:

data[time_features] = data[time_features].fillna(0)

使用 .info() 函数查看填充列的空值计数:

data[time_features].info()

输出结果如下:

图 2.8:空值计数

图 2.8:空值计数

如今,我们可以看到数据框中所有特征的值都不再是空值。

注意

要访问该特定部分的源代码,请参阅 https://packt.live/2V9nMx3。

你也可以在线运行这个示例,网址为 https://packt.live/2BqoZZM。你必须执行整个 Notebook 才能得到预期的结果。

练习 2.04:使用 Scikit-Learn 执行填充

在本练习中,你将使用 scikit-learn 的 SimpleImputer 类来替换描述相关的类别特征中的空值。在练习 2.02:可视化缺失值中,我们看到几乎所有这些特征都包含超过 50% 的缺失值。用中央值替换这些缺失值可能会导致我们构建的模型产生偏差,从而认为它们无关紧要。我们不妨将这些空值替换为一个单独的类别,赋值为 NA。本练习是练习 2.02:可视化缺失值的延续:

创建一个包含我们希望填充其值的列名的列表:

description_features = ['injuries_description', \

'damage_description', \

'total_injuries_description', \

'total_damage_description']

创建 SimpleImputer 类的对象。在这里,我们首先创建一个 imp 对象,并使用表示我们希望如何填充数据的参数对其进行初始化。我们将传递给对象初始化的参数如下:

missing_values:这是缺失值的占位符,也就是说,missing_values 参数中所有出现的值都会被填充。

strategy:这是填充策略,可以是 mean、median、most_frequent(即众数)或 constant。前三者仅能用于数值数据,并将使用指定的中央值沿每列替换缺失值,而最后一种将根据 fill_value 参数使用常数来替换缺失值。

fill_value:指定用来替换所有缺失值的值。如果保持默认值,当填充数值数据时,填充值将为 0,对于字符串或对象数据类型,则为 missing_value 字符串:

imp = SimpleImputer(missing_values=np.nan, \

strategy='constant', \

fill_value='NA')

执行填充操作。我们将使用 imp.fit_transform() 来实际执行填充。它接受包含空值的 DataFrame 作为输入,并返回填充后的 DataFrame:

data[description_features] = \

imp.fit_transform(data[description_features])

使用 .info() 函数查看插补列的空值计数:

data[description_features].info()

输出结果如下:

图 2.9:空值计数

图 2.9:空值计数

要访问此特定部分的源代码,请参考 https://packt.live/3ervLgk。

你也可以在线运行这个示例,访问 https://packt.live/3doEX3G。你必须执行整个 Notebook 才能得到预期的结果。

在前两个练习中,我们研究了使用 pandas 和 scikit-learn 方法进行缺失值插补的两种方法。这些方法是我们在对底层数据几乎没有或没有任何信息时可以使用的非常基础的方法。接下来,我们将看看我们可以使用的更高级的技术来填补缺失数据。

练习 2.05:使用推断值进行插补

让我们用来自类别 damage_description 特征的信息替换连续的 damage_millions_dollars 特征中的空值。虽然我们可能不知道具体的损失金额,但类别特征能提供关于地震损失金额区间的信息。这个练习是练习 2.04(使用 scikit-learn 进行插补)的延续:

查找多少行具有空的 damage_millions_dollars 值,并且其中有多少行具有非空的 damage_description 值:

print(data[pd.isnull(data.damage_millions_dollars)].shape[0])

print(data[pd.isnull(data.damage_millions_dollars) \

& (data.damage_description != 'NA')].shape[0])

输出结果如下:

5594

3849

正如我们所见,3849 个空值(在 5594 个空值中)可以通过另一个变量轻松替代。例如,我们知道所有列名以 _description 结尾的变量是描述字段,包含一些原始数值列可能缺失的数据估算值。对于死亡、伤害和总伤害,相关的类别值表示如下:

0 = 无

1 = 少量(~1 到 50 死亡)

2 = 一些(~51 到 100 死亡)

3 = 多(~101 到 1000 死亡)

4 = 非常多(~1001 或更多死亡)

关于 damage_millions_dollars,相应的类别值表示如下:

0 = 无

1 = 有限(大致对应不到 100 万美元)

2 = 中等(~1 到 500 万美元)

3 = 严重(~>5 到 2400 万美元)

4 = 极端(~2500 万美元或更多)

查找每个类别的平均 damage_millions_dollars 值。由于 damage_description 中的每个类别代表一个值的范围,因此我们从已经有的非空值中找到每个类别的平均 damage_millions_dollars 值。这些值为该类别提供了一个合理的最可能的估算值:

category_means = data[['damage_description', \

'damage_millions_dollars']]\

.groupby('damage_description').mean()

category_means

输出结果如下:

图 2.10:每个类别的平均 damage_millions_dollars 值

图 2.10:每个类别的平均 damage_millions_dollars 值

请注意,前面定义的前三个值具有直观意义:0.42 位于 0 和 1 之间,3.1 位于 1 和 5 之间,13.8 位于 5 和 24 之间。最后一个类别定义为 2500 万或更多;事实证明,这些极端情况的均值非常高(3575!)。

将均值存储为字典。在这一步,我们将包含均值的 DataFrame 转换为字典(Python dict 对象),以便方便地访问它们。

此外,由于新创建的 NA 类别的值(前一步骤中的插补值)是 NaN,且 0 类别的值缺失(数据集中没有 damage_description 等于 0 的行),我们还将这些值显式地添加到字典中:

replacement_values = category_means\

.damage_millions_dollars.to_dict()

replacement_values['NA'] = -1

replacement_values['0'] = 0

replacement_values

输出结果如下:

图 2.11:均值字典

图 2.11:均值字典

创建一系列替代值。对于 damage_description 列中的每个值,我们使用 map 函数将类别值映射到均值。使用 .map() 函数将列中的键映射到 replacement_values 字典中对应元素的值:

imputed_values = data.damage_description.map(replacement_values)

替换列中的空值。我们通过使用 np.where 作为三元运算符来完成这一步:第一个参数是掩码,第二个参数是当掩码为正时取值的系列,第三个参数是当掩码为负时取值的系列。

这确保了 np.where 返回的数组只会用 imputed_values 系列中的值替换 damage_millions_dollars 中的空值:

data['damage_millions_dollars'] = \

np.where(data.damage_millions_dollars.isnull(), \

data.damage_description.map(replacement_values), \

data.damage_millions_dollars)

使用 .info() 函数查看插补列的空值计数:

data[['damage_millions_dollars']].info()

输出结果如下:

图 2.12:空值计数

图 2.12:空值计数

我们可以看到,在替换后,damage_millions_dollars 列中没有空值。

注意

要访问此特定部分的源代码,请参阅 https://packt.live/3fMRqQo。

您还可以在 https://packt.live/2YkBgYC 上在线运行此示例。您必须执行整个 Notebook,才能获得预期结果。

在本节中,我们已研究了多种替换缺失值的方法。某些情况下,我们将缺失值替换为零;另一些情况下,我们通过进一步了解数据集的信息来推测,可以用描述性字段的信息和我们已有值的均值来替换缺失值。在处理真实数据时,这类决策和步骤非常常见。我们还注意到,偶尔在数据足够充分且缺失值实例较少的情况下,我们可以直接删除这些实例。在接下来的活动中,我们将使用不同的数据集,以帮助你练习并巩固这些方法。

活动 2.01:汇总统计与缺失值

在本活动中,我们将回顾本章到目前为止我们所研究的一些汇总统计和缺失值探索内容。我们将使用一个新的数据集“房价:高级回归技术”,该数据集可在 Kaggle 上获得。

注意

原始数据集可通过以下链接获取:https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data,或者访问我们的 GitHub 仓库:https://packt.live/2TjU9aj。

虽然用于练习的地震数据集是为了处理分类问题(当目标变量只有离散值时),但我们在活动中将使用的数据集是为了解决回归问题(当目标变量为连续值范围时)。我们将使用 pandas 函数生成汇总统计,并通过空值矩阵和空值相关性热图来可视化缺失值。

执行的步骤如下:

读取数据(house_prices.csv)。

使用 pandas 的 .info() 和 .describe() 方法查看数据集的汇总统计。

info() 方法的输出如下所示:

图 2.13:info() 方法的输出(简略版)

图 2.13:info() 方法的输出(简略版)

info() 方法的输出如下所示:

图 2.14:describe() 方法的输出(简略版)

图 2.14:describe() 方法的输出(简略版)

注意

为了展示方便,info() 和 describe() 方法的输出已被截断。你可以在这里找到完整的输出:https://packt.live/2TjZSgi

查找 DataFrame 中每一列的缺失值总数和缺失值百分比,并按缺失百分比降序显示至少有一个空值的列。

绘制空值矩阵和空值相关性热图。

空值矩阵如下所示:

图 2.15:空值矩阵

图 2.15:空值矩阵

空值相关性热图如下所示:

图 2.16:空值相关性热图

图 2.16:空值相关性热图

删除缺失值超过 80% 的列。

将 FireplaceQu 列中的空值替换为 NA 值。

注意

此活动的解决方案可通过此链接找到。

现在,您应该能够使用我们学到的方法来调查任何类型表格数据中的缺失值。

值的分布

在本节中,我们将研究各个变量的行为——它们取什么样的值,这些值的分布情况如何,以及如何通过视觉手段表示这些分布。

目标变量

目标变量可以是连续值(在回归问题中)或离散值(如分类问题中)。本章我们研究的问题涉及预测地震是否引发海啸,即 flag_tsunami 变量,该变量只有两个离散值,使其成为分类问题。

一种可视化方法是使用柱状图来显示有多少地震引发了海啸,有多少没有引发。在柱状图中,每个柱子代表变量的一个单一离散值,柱子的高度等于具有相应离散值的数据点的计数。这使我们能够很好地比较每个类别的绝对计数。

Exercise 2.06: 绘制柱状图

让我们看看我们的数据集中有多少地震引发了海啸。我们将使用 value_counts() 方法对列进行操作,并直接在返回的 pandas 系列上使用 .plot(kind='bar') 函数。这个练习是 Exercise 2.05: 使用推断值进行插补的延续:

使用 plt.figure() 开始绘图:

plt.figure(figsize=(8,6))

接下来,键入我们的主要绘图命令:

data.flag_tsunami.value_counts().plot(kind='bar', \

color = ('grey', \

'black'))

设置显示参数并显示图表:

plt.ylabel('数据点数目')

plt.xlabel('flag_tsunami')

plt.show()

输出如下:

Figure 2.17: 显示引发海啸的地震数量的柱状图

图 2.17: 显示引发海啸的地震数量的柱状图

从这个条形图中,我们可以看到大多数地震并没有引发海啸,实际上不到三分之一的地震确实引发了海啸。这显示数据集略微不平衡。

注:

要访问此特定部分的源代码,请参考 https://packt.live/2Yn4UfR。

您还可以在线运行此示例,网址为 https://packt.live/37QvoJI。您必须执行整个笔记本才能获得所需的结果。

让我们更仔细地看看这些 Matplotlib 命令的作用:

plt.figure(figsize=(8,6)): 此命令定义了我们的图表大小,提供了宽度和高度数值。这是在任何绘图命令之前始终首先执行的命令。

plt.xlabel() 和 plt.ylabel():这些命令接受字符串作为输入,并允许我们指定绘图的 X 和 Y 轴标签应该是什么。

plt.show():这是绘制可视化时所写的最终命令,用于在 Jupyter 笔记本中内联显示图表。

类别数据

类别变量是那些取离散值,表示不同类别或观察级别的变量,这些值可以是字符串对象或整数值。例如,我们的目标变量 flag_tsunami 是一个类别变量,有两个类别,分别是 Tsu 和 No。

类别变量可以分为两种类型:

名义变量:没有优先顺序的类别标签的变量称为名义变量。我们数据集中的一个名义变量例子是 location_name。这个变量的值不能说是有顺序的,即一个位置不大于另一个位置。同样,更多这样的变量例子包括颜色、鞋类类型、种族类型等。

有序变量:具有某种顺序关系的变量称为有序变量。我们数据集中的一个例子是 damage_description,因为每个值表示一个逐渐增加的损害程度。另一个例子可以是星期几,它的值从星期一到星期天,具有一定的顺序关系,我们知道星期四在星期三之后但在星期五之前。

虽然有序变量可以用对象数据类型表示,但它们通常也用数值数据类型表示,这常常使得它们与连续变量之间的区别变得困难。

处理数据集中的类别变量时面临的一个主要挑战是高基数,即类别或不同值的数量非常大,每个值出现的次数相对较少。例如,location_name 有大量唯一的值,每个值在数据集中出现的次数很少。

此外,非数值的类别变量总是需要进行某种形式的预处理,以将其转换为数值格式,以便可以供机器学习模型进行训练。如何将类别变量转换为数值而不丢失上下文信息是一大挑战,尽管这些信息对于人类来说容易理解(由于领域知识或常识),但对于计算机来说却很难自动理解。例如,像国家或位置名称这样的地理特征本身无法表明不同值之间的地理接近性,但这可能是一个重要特征——如果东南亚地区发生的地震比欧洲发生的地震引发更多的海啸呢?仅通过将该特征编码为数值,无法捕捉到这种信息。

练习 2.07:识别类别变量的数据类型

让我们确定 Earthquake 数据集中哪些变量是分类变量,哪些是连续变量。正如我们现在所知道的,分类变量也可以具有数值,因此仅有数值数据类型并不能保证变量是连续的。本练习是练习 2.05:使用推测值进行插补的延续:

查找所有数值型和对象型的列。我们在 DataFrame 上使用 .select_dtypes() 方法,创建一个子集 DataFrame,包含数值型(np.number)和分类型(np.object)列,然后打印每个列的列名。对于数值列,使用以下命令:

numeric_variables = data.select_dtypes(include=[np.number])

numeric_variables.columns

输出结果如下:

图 2.18:所有数值型列

图 2.18:所有数值型列

对于分类列,使用以下命令:

object_variables = data.select_dtypes(include=[np.object])

object_variables.columns

输出结果如下:

图 2.19:所有对象类型的列

图 2.19:所有对象类型的列

在这里,显然,属于对象类型的列是分类变量。为了区分数值列中的分类变量和连续变量,让我们看看每个特征的唯一值数量。

查找数值特征的唯一值个数。我们在 DataFrame 上使用 select_dtypes 方法,查找每列中的唯一值数量,并按升序排列结果序列。对于数值列,使用以下命令:

numeric_variables.nunique().sort_values()

输出结果如下:

图 2.20:数值特征的唯一值数量

图 2.20:数值特征的唯一值数量

对于分类列,使用以下命令:

object_variables.nunique().sort_values()

输出结果如下:

图 2.21:分类列的唯一值数量

图 2.21:分类列的唯一值数量

注意

要访问此特定部分的源代码,请参考 https://packt.live/2YlSmFt。

您也可以在 https://packt.live/31hnuIr 在线运行此示例。您必须执行整个 Notebook 才能得到预期的结果。

对于数值变量,我们可以看到前九个变量具有显著较少的唯一值,远少于其余行,因此这些变量很可能是分类变量。然而,我们必须记住,某些变量可能只是具有较低范围的四舍五入值的连续变量。另外,月份和日期在这里不应视为分类变量。

练习 2.08:计算分类值的计数

对于具有分类值的列,查看特征的唯一值(类别)及其频率是非常有用的,即每个不同值在数据集中出现的频率。让我们找出 injuries_description 分类变量中每个 0 到 4 标签和 NaN 值的出现次数。此练习是练习 2.07:识别分类变量的数据类型的延续:

使用 value_counts() 函数对 injuries_description 列进行操作,找出每个类别的频率。使用 value_counts 会以降序的形式返回每个值的频率,结果为 pandas 系列:

counts = data.injuries_description.value_counts(dropna=False)

计数

输出应如下所示:

图 2.22:每个类别的频率

图 2.22:每个类别的频率

按顺序变量的升序排序值。如果我们希望按值本身的频率排序,我们可以重置索引以得到一个 DataFrame,并按索引排序(即按顺序变量):

counts.reset_index().sort_values(by='index')

输出如下所示:

图 2.23:排序后的值

图 2.23:排序后的值

注意

要访问此特定部分的源代码,请参考 https://packt.live/2Yn5URj。

你也可以在 https://packt.live/314dYIr 上在线运行此示例。你必须执行整个 Notebook 才能得到所需结果。

练习 2.09:绘制饼图

由于我们样本数据中的目标变量是分类变量,在练习 2.06:绘制柱状图中,我们展示了可视化分类值分布的一种方法(使用柱状图)。另一个可以清楚显示每个类别如何作为整体数据集的一部分的方法是饼图。我们将绘制一个饼图来可视化 damage_description 变量的离散值分布。此练习是练习 2.08:计算类别值计数的延续:

将数据格式化为需要绘制的形式。在这里,我们对列运行 value_counts() 并按索引排序系列:

counts = data.damage_description.value_counts()

counts = counts.sort_index()

绘制饼图。plt.pie() 类别绘制饼图并使用计数数据。我们将使用在练习 2.06:绘制柱状图中描述的相同三个步骤进行绘制:

fig, ax = plt.subplots(figsize=(10,10))

slices = ax.pie(counts, \

labels=counts.index, \

colors = ['white'], \

wedgeprops = {'edgecolor': 'black'})

patches = slices[0]

hatches = ['/', '\', '|', '-', '+', 'x', 'o', 'O', '.', '*']

对每个补丁执行循环,范围为补丁数量:

patches[patch].set_hatch(hatches[patch])

plt.title('饼图显示\ndamage_description 的计数 '\

'类别')

plt.show()

输出如下所示:

图 2.24:显示 damage_description 类别计数的饼图

图 2.24:饼图显示损害描述类别的计数

注意

要访问此特定部分的源代码,请参考 https://packt.live/37Ovj9s。

你也可以在 https://packt.live/37OvotM 在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。

图 2.24 展示了五个损害描述类别中每个类别的相对项目数量。请注意,最好做额外的工作将无意义的标签更改为类别标签——回忆一下从 EDA 讨论中提到的:

0 = 无

1 = 有限(大致对应于低于 100 万美元)

2 = 中等(约 100 万到 500 万美元)

3 = 严重(约 500 万到 2400 万美元)

4 = 极端(约 2500 万美元或更多)

此外,虽然饼图能给我们一个快速的视觉印象,告诉我们哪些是最大和最小的类别,但它并没有提供实际的数量信息,因此添加这些标签将提升图表的价值。你可以使用本书中仓库里的代码来更新图表。

连续数据

连续变量可以取任何数量的值,通常是整数(例如,死亡人数)或浮动数据类型(例如,山的高度)。了解特征值的基本统计信息是非常有用的:从 describe()函数的输出中得到的最小值、最大值和百分位数值可以为我们提供一个合理的估计。

然而,对于连续变量,查看其在所处范围内的分布情况也是非常有用的。由于我们不能简单地找到单个值的计数,因此我们将值按升序排列,分组为均匀大小的区间,并计算每个区间的计数。这为我们提供了底层的频率分布,并绘制这个分布会得到一个直方图,这让我们能够检查数据的形状、中心值和变异性。

直方图为我们提供了一个便捷的视图,让我们可以一眼看出数据的表现,包括底层分布(例如,正态分布或指数分布)、异常值、偏斜程度等。

注意

很容易将条形图和直方图混淆。主要的区别在于,直方图用于绘制已分箱的连续数据,用以可视化频率分布,而条形图可以用于多种其他用途,包括表示分类变量,正如我们所做的那样。此外,在直方图中,箱数是可以变化的,因此箱内值的范围由箱数决定,直方图中条形的高度也是如此。在条形图中,条形的宽度通常不具有传达意义,而高度通常是类别的属性,比如计数。

最常见的频率分布之一是高斯(或正态)分布。这是一个对称分布,具有钟形曲线,表示数据集中中间范围附近的值出现的频率最高,随着远离中间,出现的频率对称性地减少。你几乎肯定见过高斯分布的例子,因为许多自然和人为过程生成的值几乎呈现高斯分布。因此,数据与高斯分布的比较是非常常见的。

它是一个概率分布,曲线下方的面积等于 1,如图 2.25 所示:

图 2.25:高斯(正态)分布

图 2.25:高斯(正态)分布

像正态分布这样的对称分布可以通过两个参数完全描述——均值(µ)和标准差(σ)。例如,在图 2.25 中,均值为 7.5。然而,许多实际数据并不遵循正态分布,可能是非对称的。数据的非对称性通常被称为偏斜。

偏度

如果一个分布不对称,则称为偏斜,偏度衡量的是变量相对于其均值的非对称性。其值可以为正、负(或未定义)。在前一种情况下,尾部位于分布的右侧,而后一种情况则表示尾部位于左侧。

但是,必须注意的是,粗短的尾部与细长的尾部对偏度值的影响是相同的。

峰度

峰度是衡量变量分布尾部程度的指标,用于衡量一个尾部相对于另一个尾部的异常值情况。高峰度值表示尾部较胖,并且存在异常值。与偏度概念类似,峰度也描述了分布的形状。

练习 2.10:绘制直方图

让我们使用 Seaborn 库绘制 eq_primary 特征的直方图。这个练习是练习 2.09,绘制饼图的延续:

使用 plt.figure() 来初始化绘图:

plt.figure(figsize=(10,7))

sns.distplot() 是我们用来绘制直方图的主要命令。第一个参数是一维数据,它决定了要绘制直方图的数据,而 bins 参数定义了箱子的数量和大小。使用方法如下:

sns.distplot(data.eq_primary.dropna(), \

bins=np.linspace(0,10,21))

使用 plt.show() 显示图表:

plt.show()

输出将如下所示:

图 2.26:示例主特征的直方图

图 2.26:示例主特征的直方图

图表为我们提供了一个标准化(或归一化)的直方图,这意味着直方图柱形下方的面积等于 1。此外,直方图上方的线是核密度估计,它能给我们一个变量概率分布的大致形态。

注意

若要访问此特定部分的源代码,请参考 https://packt.live/2BwZrdj。

你也可以在网上运行此示例,网址为 https://packt.live/3fMSxj2。你必须执行整个 Notebook 才能获得期望的结果。

从图表中,我们可以看到 eq_primary 的值大多介于 5 和 8 之间,这意味着大多数地震的震级处于中等至高值范围,几乎没有地震的震级是低值或极高值。

练习 2.11:计算偏度和峰度

让我们使用 pandas 中的核心函数来计算数据集中所有特征的偏度和峰度值。此练习是练习 2.10: 绘制直方图的延续:

使用 .skew() DataFrame 方法计算所有特征的偏度,然后按升序排列这些值:

data.skew().sort_values()

输出将如下所示:

图 2.27: 数据集中所有特征的偏度值

图 2.27: 数据集中所有特征的偏度值

使用 .kurt() DataFrame 方法计算所有特征的峰度:

data.kurt()

输出将如下所示:

图 2.28: 数据集中所有特征的峰度值

图 2.28: 数据集中所有特征的峰度值

这里我们可以看到,某些变量的峰度值与 0 的偏差较大。这意味着这些列具有长尾分布。但是,这些变量尾部的值(表示死亡人数、受伤人数和损失金额),在我们的案例中,可能是我们需要特别关注的异常值。较大的值实际上可能表示有额外的力量加剧了地震带来的破坏,即海啸。

注意

若要访问此特定部分的源代码,请参考 https://packt.live/2Yklmh0。

你也可以在网上运行此示例,网址为 https://packt.live/37PcMdj。你必须执行整个 Notebook 才能获得期望的结果。

活动 2.02:可视化表示值的分布

在此活动中,我们将通过创建不同的图表,如直方图和饼图,来实现上一节中学到的内容。此外,我们将计算数据集特征的偏度和峰度。这里我们将使用在活动 2.01: 总结统计与缺失值中使用的相同数据集,即“房价:高级回归技巧”数据集。我们将使用不同类型的图表来可视化该数据集的值分布。本活动是活动 2.01: 总结统计与缺失值的延续:

执行的步骤如下:

使用 Matplotlib 绘制目标变量 SalePrice 的直方图。

输出结果如下所示:

图 2.29:目标变量的直方图

图 2.29:目标变量的直方图

找出每个列中具有对象类型的唯一值的数量。

创建一个数据框,表示 HouseStyle 列中每个类别值的出现次数。

绘制表示这些计数的饼图。

输出结果如下所示:

图 2.30:表示计数的饼图

图 2.30:表示计数的饼图

找出每个列中具有数字类型的唯一值的数量。

使用 seaborn 绘制 LotArea 变量的直方图。

输出结果如下所示:

图 2.31:LotArea 变量的直方图

图 2.31:LotArea 变量的直方图

计算每列值的偏斜度和峰度值。

偏斜度值的输出将如下所示:

图 2.32:每列的偏斜值

图 2.32:每列的偏斜值

对峰度值的输出将如下所示:

图 2.33:每列的峰度值

图 2.33:每列的峰度值

注意

本活动的解决方案可以通过此链接找到。

我们已经看到了如何更详细地了解数据的性质,特别是通过开始理解数据的分布,使用直方图或密度图、数据的相对计数使用饼图,以及检查变量的偏斜度和峰度,作为发现潜在问题数据、异常值等的第一步。

到现在为止,你应该已经能够熟练处理各种数据统计量,例如摘要统计、计数和数值分布。使用直方图和密度图等工具,你可以探索数据集的形状,并通过计算如偏斜度和峰度等统计量来增强对数据集的理解。你应该逐渐培养一些直觉,识别出需要进一步调查的标志,如较大的偏斜度或峰度值。

数据中的关系

寻找数据中变量之间关系的重要性有两个原因:

确定哪些特征可能重要是至关重要的,因为找到与目标变量有强关系的特征有助于特征选择过程。

寻找不同特征之间的关系也很有用,因为数据集中的变量通常不是完全独立的,这会以多种方式影响我们的建模。

现在,有多种方式可以可视化这些关系,这主要取决于我们试图找到关系的变量类型,以及我们在方程或比较中考虑的变量数量。

两个连续变量之间的关系

建立两个连续变量之间的关系基本上是观察其中一个变量在另一个变量增加时如何变化。最常见的可视化方法是使用散点图,我们将每个变量沿单一轴(当有两个变量时是二维平面中的 X 和 Y 轴)绘制,并在 X-Y 平面中使用标记绘制每个数据点。这种可视化方式可以很好地帮助我们了解这两个变量之间是否存在某种关系。

然而,如果我们想量化两个变量之间的关系,最常见的方法是找出它们之间的相关性。如果目标变量是连续的,并且与另一个变量有很高的相关性,这表明该特征将在模型中占据重要地位。

皮尔逊相关系数

皮尔逊相关系数是常用的相关系数,用于展示一对变量之间的线性关系。该公式返回一个介于 -1 和 +1 之间的值,其中:

+1 表示强正相关

-1 表示强负相关

0 表示完全没有关系

找出特征对之间的相关性也非常有用。在某些模型中,高度相关的特征可能会引发问题,包括系数在数据或模型参数发生微小变化时剧烈波动。在极端情况下,完全相关的特征(例如 X2 = 2.5 * X1)会导致某些模型(包括线性回归)返回未定义的系数(如 Inf)。

注意

在拟合线性模型时,特征之间的高度相关性可能会导致模型不可预测且变化较大。这是因为线性模型中每个特征的系数可以解释为在保持其他特征不变的情况下,目标变量的单位变化。然而,当一组特征不是独立的(即它们是相关的)时,我们无法确定每个特征对目标变量的独立变化影响,导致系数大幅波动。

要查找 DataFrame 中每个数值特征与其他特征之间的配对相关性,我们可以使用 DataFrame 上的 .corr() 函数。

练习 2.12:绘制散点图

我们来绘制一个散点图,X 轴为主要地震震级,Y 轴为相应的受伤人数。此练习是练习 2.11(计算偏度和峰度)的延续:

筛选出非空值。由于我们知道两列中都有空值,首先我们筛选出只包含非空行的数据:

data_to_plot = data[~pd.isnull(data.injuries) \

& ~pd.isnull(data.eq_primary)]

创建并显示散点图。我们将使用 Matplotlib 的 plt.scatter(x=..., y=...) 命令作为绘制数据的主要命令。x 和 y 参数指定在哪个轴上考虑哪个特征。它们接受单维数据结构,如列表、元组或 pandas 系列。我们还可以向 scatter 函数发送更多参数,以定义例如要使用的图标来绘制单个数据点。例如,要使用红色十字作为图标,我们需要发送参数 marker='x',c='r':

plt.figure(figsize=(12,9))

plt.scatter(x=data_to_plot.eq_primary, y=data_to_plot.injuries)

plt.xlabel('主要地震震级')

plt.ylabel('受伤人数')

plt.show()

输出如下:

Figure 2.34: 散点图

Figure 2.34: 散点图

从图中我们可以推断,尽管看不出受伤人数与地震震级之间有趋势,但随着震级增加,受伤人数较多的地震数量有所增加。然而,对于大多数地震,似乎没有明显的关系。

注意

要访问此特定部分的源代码,请参阅 https://packt.live/314eupR。

您也可以在 https://packt.live/2YWtbsm 上在线运行此示例。为了获得期望的结果,您必须执行整个笔记本。

练习 2.13: 绘制相关性热图

我们将使用 seaborn 的 sns.heatmap() 函数在数据集中的特征间相关性值上绘制一个相关性热图。这是练习 2.12 绘制散点图的延续。

传递给 sns.heatmap() 函数的可选参数为 square 和 cmap,分别表示绘制的每个像素为正方形,并指定使用的颜色方案:

绘制包含所有特征的基本热图:

plt.figure(figsize = (12,10))

sns.heatmap(data.corr(), square=True, cmap="YlGnBu")

plt.show()

输出如下:

Figure 2.35: 相关性热图

Figure 2.35: 相关性热图

我们可以从图右侧的色条中看到,最小值约为 -0.2,是最浅的色调,这是对相关性值的误表示,实际上相关性值的范围是从 -1 到 1。

在更定制的热图中绘制特征子集。我们将使用 vmin 和 vmax 参数指定上下限,并在特定特征子集上重新绘制带有注释的热图,指定成对相关性值。我们还将更改颜色方案,以便更好地解释—中性的白色表示无相关性,越来越深的蓝色和红色表示更高的正相关和负相关值:

特征子集 = ['震源深度', '主要地震震级', '震级 (MW)', \

'震级 (MS)', '震级 (MB)', '强度', \

'纬度', '经度', '受伤人数', \

'damage_millions_dollars','total_injuries', \

'total_damage_millions_dollars']

plt.figure(figsize = (12,10))

sns.heatmap(data[feature_subset].corr(), square=True, \

annot=True, cmap="RdBu", vmin=-1, vmax=1)

plt.show()

输出结果如下:

图 2.36:自定义相关性热图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-Y1Y2KFEB.jpg)

图 2.36:自定义相关性热图

注意

要访问此特定部分的源代码,请参考 https://packt.live/2Z1lPUB。

你也可以在 https://packt.live/2YntBc8 上在线运行这个示例。你必须执行整个笔记本才能得到预期的结果。

现在,虽然我们可以计算相关性值,但这仅仅给出了线性关系的一个指示。为了更好地判断是否存在可能的依赖关系,我们可以绘制特征对之间的散点图,这在变量之间的关系未知时尤其有用,且可视化数据点如何散布或分布可以让我们初步判断这两个变量是否可能有关联(以及如何关联)。

使用成对图

成对图对于一次性可视化多个特征对之间的关系非常有用,可以使用 Seaborn 的 .pairplot() 函数绘制。在接下来的练习中,我们将创建一个成对图,并可视化数据集中特征之间的关系。

练习 2.14:实现一个成对图

在本练习中,我们将查看数据集中具有最高成对相关性的特征之间的成对图。此练习是练习 2.13《绘制相关性热图》的延续:

定义一个列表,其中包含要创建成对图的特征子集:

feature_subset = ['focal_depth', 'eq_primary', 'eq_mag_mw', \

'eq_mag_ms', 'eq_mag_mb', 'intensity',]

使用 seaborn 创建成对图。传递给绘图函数的参数是 kind='scatter',这表示我们希望网格中每一对变量之间的单独图像显示为散点图,diag_kind='kde',这表示我们希望对角线(即特征对相同的地方)上的图形是一个核密度估计。

还需要注意的是,这里对角线对称的图将本质上是相同的,只是坐标轴被反转了:

sns.pairplot(data[feature_subset].dropna(), kind ='scatter', \

diag_kind='kde')

plt.show()

输出结果如下:

图 2.37:具有最高成对相关性的特征之间的成对图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-8A67TY8S.jpg)

图 2.37:具有最高成对相关性的特征之间的成对图

我们已经成功地通过成对图可视化了数据集中具有高相关性的特征之间的关系。

注意

要访问此特定部分的源代码,请参考 https://packt.live/2Ni11T0。

你也可以在 https://packt.live/3eol7aj 上在线运行这个示例。你必须执行整个笔记本才能得到预期的结果。

连续变量与分类变量之间的关系

查看一个分类变量和一个连续变量之间关系的常见方法是使用条形图或箱线图:

条形图有助于比较离散参数集的变量值,是最常见的图表类型之一。每个条形代表一个分类值,条形的高度通常代表该类别下连续变量的聚合值(例如平均值、总和或该类别下连续变量值的计数)。

箱线图是用来表示每个分类变量的离散值对应的连续变量分布的矩形。它不仅能有效地可视化离群值,还能帮助我们比较分类变量不同类别之间的连续变量分布。矩形的上下边缘分别代表第一四分位数和第三四分位数,中间的线代表中位数,矩形上下方的点(或异常值)表示离群值。

练习 2.15:绘制条形图

让我们使用条形图可视化由不同强度级别的地震产生的海啸总数。这个练习是练习 2.14 的延续,内容为:实现一个配对图(Pairplot):

对 flag_tsunami 变量进行预处理。在使用 flag_tsunami 变量之前,我们需要对其进行预处理,将 No 值转换为零,Tsu 值转换为一。这样就可以得到二进制目标变量。为此,我们使用 .loc 操作符设置列中的值,: 表示对所有行设置值,第二个参数指定要设置值的列名:

data.loc[:,'flag_tsunami'] = data.flag_tsunami\

.apply(lambda t: int(str(t) == 'Tsu'))

从我们要绘制的数据中删除所有具有空强度值的行:

subset = data[~pd.isnull(data.intensity)][['intensity',\

'flag_tsunami']]

查找每个强度级别的海啸总数并显示 DataFrame。为了以条形图可视化这些数据,我们需要按强度级别对行进行分组,然后对 flag_tsunami 值求和,以得到每个强度级别的海啸总数:

data_to_plot = subset.groupby('intensity').sum()

data_to_plot

输出结果如下:

图 2.38:每个强度级别的海啸总数

图 2.38:每个强度级别的海啸总数

使用 Matplotlib 的 plt.bar(x=..., height=...) 方法绘制条形图,该方法需要两个参数,第一个指定绘制条形图的 x 值,第二个指定每个条形的高度。这两个参数都是一维数据结构,且必须具有相同的长度:

plt.figure(figsize=(12,9))

plt.bar(x=data_to_plot.index, height=data_to_plot.flag_tsunami)

plt.xlabel('地震强度')

plt.ylabel('海啸数量')

plt.show()

输出将如下所示:

图 2.39: 条形图

图 2.39: 条形图

从这个图中,我们可以看到,随着地震强度的增加,海啸数量也随之增加,但当强度超过 9 时,海啸数量似乎突然下降。

思考一下为什么会发生这种情况。也许只是因为强度这么高的地震较少,因此海啸也就更少。或者,这可能是一个完全独立的因素;也许高强度地震历史上发生在陆地上,无法触发海啸。请探索数据来找出原因。

注意

要访问此特定部分的源代码,请访问 https://packt.live/3enFjsZ。

你也可以在线运行此示例,网址:https://packt.live/2V5apxV。你必须执行整个笔记本才能得到预期的结果。

练习 2.16: 可视化箱型图

在本练习中,我们将绘制一个箱型图,表示发生至少 100 次地震的国家中 eq_primary 的变化。此练习是练习 2.15“绘制条形图”的延续:

查找发生超过 100 次地震的国家。我们将计算数据集中所有国家的频次计数。然后,我们将创建一个仅包含那些频次大于 100 的国家的系列:

country_counts = data.country.value_counts()

top_countries = country_counts[country_counts > 100]

top_countries

输出将如下所示:

图 2.40: 超过 100 次地震的国家

图 2.40: 超过 100 次地震的国家

子集筛选数据框,只保留那些国家属于前述集合的行。要筛选行,我们使用 .isin() 方法在 pandas 系列上选择那些包含传递为参数的类数组对象中的值的行:

subset = data[data.country.isin(top_countries.index)]

创建并显示箱型图。绘制数据的主要命令是 sns.boxplot(x=..., y=..., data=..., order=)。x 和 y 参数是数据框中要绘制在每个轴上的列名——前者被认为是分类变量,后者是连续变量。data 参数接受数据框,并且 order 参数接受一个类别名称列表,指示在 X 轴上显示类别的顺序:

plt.figure(figsize=(15, 15))

sns.boxplot(x='country', y="eq_primary", data=subset, \

order=top_countries.index)

plt.show()

输出将如下所示:

图 2.41: 箱型图

图 2.41: 箱型图

注意

要访问此特定部分的源代码,请访问 https://packt.live/2zQHPZw。

你也可以在线运行此示例,网址:https://packt.live/3hPAzhN。你必须执行整个笔记本才能得到预期的结果。

两个分类变量之间的关系

当我们仅查看一对分类变量以寻找它们之间的关系时,最直观的方法是基于第一个类别划分数据,然后根据第二个分类变量进一步细分数据,查看结果计数以了解数据点的分布。虽然这可能看起来有些困惑,但一种常见的可视化方法是使用堆叠条形图。与常规条形图一样,每个条形图表示一个分类值。但每个条形图会再次被细分为颜色编码的类别,这些类别表示主类别中有多少数据点属于每个子类别(即第二个类别)。通常,类别数量较多的变量被认为是主类别。

练习 2.17:绘制堆叠条形图

在这个练习中,我们将绘制一个堆叠条形图,表示每个强度级别发生的海啸数量。这个练习是练习 2.16(可视化箱形图)的延续:

查找每个分组值的强度和 flag_tsunami 的数据点数量:

grouped_data = data.groupby(['intensity', \

'flag_tsunami']).size()

grouped_data

输出结果如下:

图 2.42:每个分组值的强度和 flag_tsunami 的数据点

图 2.42:每个分组值的强度和 flag_tsunami 的数据点

使用.unstack()方法在结果 DataFrame 上获取 level-1 索引(flag_tsunami)作为列:

data_to_plot = grouped_data.unstack()

data_to_plot

输出结果如下:

图 2.43:level-1 索引

图 2.43:level-1 索引

创建堆叠条形图。我们首先使用 sns.set()函数来指示我们想使用 seaborn 作为可视化库。然后,我们可以轻松地使用 pandas 中的.native .plot()函数,通过传递 kind='bar'和 stacked=True 参数来绘制堆叠条形图:

sns.set()

data_to_plot.plot(kind='bar', stacked=True, figsize=(12,8))

plt.show()

输出结果如下:

图 2.44:堆叠条形图

图 2.44:堆叠条形图

注意

要访问此特定部分的源代码,请参阅 https://packt.live/37SnqA8。

你也可以在线运行此示例,网址为 https://packt.live/3dllvVx。你必须执行整个 Notebook 才能获得所需的结果。

该图现在让我们能够可视化和解释每个强度级别导致海啸的地震所占的比例。在练习 2.15:绘制条形图中,我们看到对于强度大于 9 的地震,海啸的数量减少了。从这张图中,我们现在可以确认,这主要是因为地震本身的数量在超过 10 级之后减少了;甚至在 11 级时,海啸的比例有所增加。

活动 2.03:数据中的关系

在这个活动中,我们将复习上一节关于数据关系的知识。我们将使用在活动 2.01: 总结统计和缺失值中使用的相同数据集,即《房价:高级回归技术》。我们将使用不同的图表来突出数据集中的变量关系。这个活动是活动 2.01: 总结统计和缺失值的延续:

执行的步骤如下:

绘制数据集的相关性热图。

输出应类似于以下内容:

图 2.45:房屋数据集的相关性热图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-KZM5Q1AT.jpg)

图 2.45:房屋数据集的相关性热图

使用以下特征子集绘制一个更紧凑的热图,并附加相关值的注释:

feature_subset = ['GarageArea','GarageCars','GarageCond', \

'GarageFinish','GarageQual','GarageType', \

'GarageYrBlt','GrLivArea','LotArea', \

'MasVnrArea','SalePrice']

输出应类似于以下内容:

图 2.46:房屋数据集选择变量的相关性热图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-R3PJ6CKC.jpg)

图 2.46:房屋数据集选择变量的相关性热图

显示相同特征子集的配对图,直方图在对角线上,散点图在其他位置。

输出将如下所示:

图 2.47:相同特征子集的配对图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-SYRP2X30.jpg)

图 2.47:相同特征子集的配对图

创建一个箱型图,以显示不同 GarageCars 类别中 SalePrice 的变化:

输出将如下所示:

图 2.48:展示不同 GarageCars 类别中 SalePrice 变化的箱型图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-RV8XI78X.jpg)

图 2.48:展示不同 GarageCars 类别中 SalePrice 变化的箱型图

使用 seaborn 绘制折线图,展示较旧和较新建的房屋的 SalePrice 变化:

输出将如下所示:

图 2.49:展示 SalePrice 变化的折线图

针对较旧和较新建的房屋

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-0972CWFE.jpg)

图 2.49:展示较旧和较新建的房屋 SalePrice 变化的折线图

注意

该活动的解决方案可以通过此链接找到。

你已经学会了如何使用 seaborn 包中的更高级方法来可视化大量变量,使用相关性热图、配对图和箱型图等图表。通过箱型图,你学会了如何可视化一个变量在另一个分类变量中的范围。箱型图还直接可视化了四分位数和异常值,使其成为你 EDA 工具包中的强大工具。你还创建了一些初步的折线图和散点图,这对于可视化随时间或其他变量变化的连续数据非常有帮助。

总结

在本章中,我们首先讨论了为什么数据探索是建模过程中的重要部分,以及它如何帮助我们不仅为建模过程进行数据预处理,还能帮助我们工程化有意义的特征并提高模型的准确性。本章不仅侧重于对数据集及其特征的基本概述,还通过创建结合多个特征的可视化图表来获得深入见解。我们介绍了如何使用 pandas 的核心功能找到数据集的摘要统计信息。我们讨论了如何查找缺失值,并讲解了它们的重要性,同时学习了如何使用 Missingno 库来分析缺失值,以及如何使用 pandas 和 scikit-learn 库来填补缺失值。接着,我们学习了如何研究数据集中变量的单变量分布,并使用条形图、饼图和直方图等方法将其可视化,适用于分类变量和连续变量。最后,我们学习了如何探索变量之间的关系,并了解了如何使用散点图、热力图、箱线图和堆叠条形图等方式来展示它们。

在接下来的章节中,我们将开始探索有监督的机器学习算法。现在,我们已经对如何探索现有数据集有了一定的了解,可以进入建模阶段。下一章将介绍回归,这是一类主要用于构建连续目标变量模型的算法。

第四章:3. 线性回归

概述

本章涵盖回归问题及其分析,介绍了线性回归、多个线性回归和梯度下降法。到本章结束时,你将能够区分回归问题和分类问题。你将能够在回归问题中实现梯度下降法,并将其应用于其他模型架构。你还将能够使用线性回归为 x-y 平面中的数据构建线性模型,评估线性模型的性能,并利用评估结果选择最佳模型。此外,你还将能够执行特征工程,创建虚拟变量,以构建复杂的线性模型。

介绍

在第一章《基础知识》和第二章《探索性数据分析与可视化》中,我们介绍了 Python 中有监督机器学习的概念,以及加载、清理、探索和可视化原始数据源所需的基本技术。我们讨论了在进行进一步分析之前,全面理解数据的重要性,以及初步数据准备过程有时可能占据整个项目大部分时间的问题。特别地,我们考虑了所有变量之间的相关性、寻找并处理缺失值,以及通过直方图、条形图和密度图理解数据的形态。在本章中,我们将深入探讨模型构建过程,并使用线性回归构建我们的第一个有监督机器学习解决方案。

回归与分类问题

我们在第一章《基础》中讨论了两种不同的方法:监督学习和无监督学习。监督学习问题旨在将输入信息映射到已知的输出值或标签,但还有两个子类别需要考虑。监督学习问题可以进一步分为回归问题或分类问题。本章的回归问题旨在预测或建模连续值,例如,从历史数据中预测明天的温度(以摄氏度为单位),或基于产品的销售历史预测未来的产品销售额。相对而言,分类问题则不是返回一个连续值,而是预测属于一个或多个指定类别或类的成员身份。第一章《基础》中提到的监督学习问题示例,就是想要判断发型是属于 1960 年代还是 1980 年代,这是一个良好的监督分类问题示例。在那里,我们试图预测发型是属于两个不同类别中的一个,其中类别 1 为 1960 年代,类别 2 为 1980 年代。其他分类问题包括预测泰坦尼克号乘客是否生还,或经典的 MNIST 问题(http://yann.lecun.com/exdb/mnist/)。(MNIST 是一个包含 70,000 张标注过的手写数字图像的数据库,数字从 0 到 9。MNIST 分类任务是从这 70,000 张输入图像中选取一张,预测或分类该图像中写的是哪一个数字(0 到 9)。该模型必须预测该图像属于 10 个不同类别中的哪一个。)

机器学习工作流

在我们开始讨论回归问题之前,我们首先需要了解创建任何机器学习模型(无论是监督回归还是其他类型模型)所涉及的六个主要阶段。这些阶段如下:

商业理解

数据理解

数据准备

建模

评估

部署

该工作流由一个知名的开放行业标准 CRISP-DM(跨行业数据挖掘标准流程)描述,可以如下查看:

图 3.1:CRISP-DM 工作流

图 3.1:CRISP-DM 工作流

建议确保完全理解这个流程以及本节所描述的内容,因为每个阶段对于实现良好的模型性能和满足业务需求至关重要。在这里,我们回顾每个阶段的关键要素。

商业理解

任何数据分析和建模项目的第一阶段并不是直接跳入数据或建模,而是理解我们为什么要分析这些数据,以及我们的模型和结论对业务的影响是什么。作为数据处理人员,你可能没有这阶段所需的全部领域知识;解决方案是花时间与业务中的利益相关者进行互动,他们了解痛点和业务目标。不要低估这一阶段的重要性。从流程图中还可以看到,业务理解阶段和数据理解阶段之间有反馈流动,评估阶段也会反馈到业务理解阶段。换句话说,这些都是持续进行的阶段,你应当尽力不断发现更多关于你所研究问题的业务方面的信息。在这一阶段的初步工作中,你还应该制定一个初步的整体项目计划。

数据理解

在大多数实际项目中,可能会有多个潜在的数据来源,并且这些数据来源可能随着时间的推移而变化。本阶段的目的是获取数据,并对其有足够的了解,以便选择解决问题所需的数据。这可能导致确定需要更多数据。基本步骤是先确定初始可用数据,并制作数据清单。接着,审查数据,这可能包括将数据读取到 Python 中并评估数据质量;常见的问题如缺失值、异常值等可以在此阶段发现,并与业务团队讨论,确定最佳的处理方案。尽管流行文献中广泛描述了填补缺失值的方法,但你不应立即跳到使用工具“修复”数据中的问题——此阶段的目标是理解这些问题,并与业务利益相关者讨论合适的解决方案。请注意,可能比填补缺失值或进行填补过程更合适的做法是直接舍弃缺失值的数据实例。

除了数据清单外,本阶段的一个关键输出是描述数据、所发现内容和预期行动的报告。为了得到这个输出,需要进行一些 EDA(探索性数据分析),正如在第二章《探索性数据分析与可视化》中所描述的那样。

数据准备

在数据准备阶段,我们使用从前一阶段确定为合适的数据,并对其进行清洗和转换,使其能够用于建模。这是第一章《基础》中的一个重要部分,因此在本节中不会进一步分析。然而,重要的是要充分理解数据规范、收集和清理/整理过程的关键性。如果输入数据不理想,我们无法期望产生高性能的系统。关于数据质量的一个常用词句是“垃圾进,垃圾出”。如果你使用低质量的数据,就会得到低质量的结果。在我们的发型示例中,我们希望样本大小至少为数百个,理想情况下应为数千个,并且这些样本已正确标注为 1960 年代或 1980 年代的样本。我们不希望使用错误标注或甚至不属于这两个年代的样本。

请注意,在数据准备过程中,完全有可能发现数据的额外方面,并且在此过程中可能需要进行额外的可视化,以便得到用于建模的数据集。

建模

建模阶段包括两个子阶段:模型架构规范和模型训练。

模型架构规范:在更复杂的项目中,这些步骤可能是迭代相关的。在许多情况下,可能有多种模型类型(如线性回归、人工神经网络、梯度提升等)适用于当前问题。因此,有时调查不止一个模型架构是有益的,并且为了做到这一点,必须训练并比较这些模型的预测能力。

训练:建模的第二个子阶段是训练,在这个阶段,我们使用现有的数据和已知的结果,通过一个过程“学习”候选模型的参数。在这里,我们必须建立训练过程的设计和执行;这些细节将根据选择的模型架构和输入数据的规模而有所不同。例如,对于非常大的数据集,我们可能需要将数据流式传输或流动通过训练过程,因为数据太大,无法完全存入计算机内存,而对于较小的数据集,我们可以一次性使用所有数据。

评估

工作流程的下一阶段是对模型进行评估,得出最终的性能指标。这是我们判断模型是否值得发布、是否比以前的版本更好,或者是否已经在不同的编程语言或开发环境之间有效迁移的机制。我们将在第七章《模型评估》中更详细地讨论一些这些指标,因此在此阶段不会详细展开。只需要记住,无论使用何种方法,都需要能够一致地报告并独立地衡量模型相对于指标的表现,且需要使用适当的样本来自数据中进行评估。

部署

在一个完整的数据分析工作流程中,大多数模型一旦开发完成,就需要进行部署以供使用。部署在某些应用中至关重要,例如,在电子商务网站上,模型可能作为推荐系统的基础,每次更新时,模型必须重新部署到 Web 应用程序中。部署的形式多种多样,从简单地共享 Jupyter notebook,到在代码提交时自动更新网站代码,再到主仓库。尽管部署非常重要,但它超出了本书的范围,我们不会在接下来的内容中深入讨论。

在进入回归建模之前,让我们做一些最终的数据准备练习。为此,我们创建了一个合成的数据集,记录了从 1841 年到 2010 年的空气温度数据,该数据集可在本书附带的代码包中或在 GitHub 上找到,网址为 https://packt.live/2Pu850C。该数据集包含的数值旨在展示本章的主题,不能与科学研究中收集的数据混淆。

练习 3.01:使用移动平均法绘制数据

正如我们在第一章《基础知识》中讨论的,以及在前面的章节中提到的,充分理解所使用的数据集对于构建高性能的模型至关重要。所以,考虑到这一点,让我们通过这个练习加载、绘制并查询数据源:

导入 numpy、pandas 和 matplotlib 包:

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

使用 pandas 的 read_csv 函数加载包含 synth_temp.csv 数据集的 CSV 文件,并显示前五行数据:

df = pd.read_csv('../Datasets/synth_temp.csv')

df.head()

输出将如下所示:

图 3.2:前五行

图 3.2:前五行

对于我们的目的,我们不希望使用所有这些数据,但让我们看一下每年有多少个数据点。创建一个打印语句,输出 1841 年、1902 年和 2010 年的点数,并制作一个简单的图表,显示每年的数据点数:

快速查看每年数据点的数量

print('There are ' + str(len(df.loc[df['Year'] == 1841])) \

  • ' points in 1841\n' + 'and ' \

  • str(len(df.loc[df['Year'] == 2010])) \

  • ' 2010 年的数据点\n' + '以及 ' \

  • str(len(df.loc[df['Year'] == 1902])) \

  • ' 1902 年的数据点']

看到每年有不同数量的数据点,做个快速图表看看

fig, ax = plt.subplots()

ax.plot(df['Year'].unique(), [len(df.loc[df['Year'] == i]) \

for i in df['Year'].unique()])

plt.show()

输出将如下所示:

图 3.3:每年数据点数量不同

图 3.3:每年数据点数量不同

我们看到每年的数据点数量不同。还需要注意的是,我们没有关于每年各个数据点测量时间的确切信息。如果这很重要,我们可能需要询问相关业务负责人是否能获得这些信息。

让我们切片 DataFrame,去除所有 1901 年及之前的数据,因为我们可以看到这些年份的数据较少:

从 1902 年开始切片

df = df.loc[df.Year > 1901]

df.head()

输出将如下所示:

图 3.4:1902 年及以后数据的子集

图 3.4:1902 年及以后数据的子集

快速绘制图表以可视化数据:

快速绘制图表以了解目前为止的情况

fig, ax = plt.subplots()

ax.scatter(df.Year, df.RgnAvTemp)

plt.show()

输出将如下所示:

图 3.5:过滤日期后的原始数据基本可视化

图 3.5:过滤日期后的原始数据基本可视化

我们可以看到每年有相当大的差异。将数据按年分组,使用 DataFrame 的 agg 方法来创建年度平均值。这绕过了我们每年都有多个未知日期的数据点的问题,但仍然使用了所有数据:

按年汇总

df_group_year = (df.groupby('Year').agg('mean')\

.rename(columns = {'RgnAvTemp' : 'AvgTemp'}))

print(df_group_year.head())

print(df_group_year.tail())

输出将如下所示:

图 3.6:每年平均数据

图 3.6:每年平均数据

和以前一样,进行快速可视化,方法如下:

可视化按年份平均的结果

fig, ax = plt.subplots()

ax.scatter(df_group_year.index, df_group_year['AvgTemp'])

plt.show()

数据现在将如下所示:

图 3.7:每年平均数据

图 3.7:每年平均数据

由于数据仍然有噪声,移动平均滤波器可以提供一个有用的整体趋势指示器。移动平均滤波器简单地计算最近 N 个值的平均值,并将此平均值分配给第 N 个样本。使用 10 年的窗口计算温度测量的移动平均值:

window = 10

smoothed_df = \

pd.DataFrame(df_group_year.AvgTemp.rolling(window).mean())

smoothed_df.colums = 'AvgTemp'

print(smoothed_df.head(14))

print(smoothed_df.tail())

我们将得到以下输出:

图 3.8:10 年移动平均温度

图 3.8:10 年移动平均温度

请注意,前 9 个样本为 NaN,这是因为移动平均滤波器窗口的大小。窗口大小是 10,因此需要 9 个样本(10-1)来生成第一个平均值,因此前 9 个样本为 NaN。rolling() 方法有额外的选项,可以将值延伸到左侧或右侧,或允许基于更少的点计算早期值。在这种情况下,我们将只过滤掉它们:

过滤掉 NaN 值

smoothed_df = smoothed_df[smoothed_df['AvgTemp'].notnull()]

快速绘制图表以了解目前的进展

fig, ax = plt.subplots()

ax.scatter(smoothed_df.index, smoothed_df['AvgTemp'])

plt.show()

输出如下:

图 3.9:预处理温度数据的可视化

图 3.9:预处理温度数据的可视化

最后,按年份绘制测量数据以及移动平均信号:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

raw_plot_data = df[df.Year > 1901]

ax.scatter(raw_plot_data.Year, \

raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'blue', s = 1.5)

年度平均

annual_plot_data = df_group_year\

.filter(items = smoothed_df.index, axis = 0)

ax.scatter(annual_plot_data.index, \

annual_plot_data.AvgTemp, \

label = '年度平均', c = 'k')

移动平均

ax.plot(smoothed_df.index, smoothed_df.AvgTemp, \

c = 'r', linestyle = '--', \

label = f'{window} 年移动平均')

ax.set_title('平均空气温度测量', fontsize = 16)

使刻度包括第一个和最后一个年份

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出如下:

图 3.10:年度平均温度与 10 年移动平均叠加图

图 3.10:年度平均温度与 10 年移动平均叠加图

我们可以通过调整 y 轴的尺度,专注于我们最感兴趣的部分——年度平均值,从而改进图表。这是大多数可视化中的一个重要方面,即尺度应优化以向读者传递最有用的信息:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

raw_plot_data = df[df.Year > 1901]

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'blue', s = 1.5)

年度平均

annual_plot_data = df_group_year\

.filter(items = smoothed_df.index, axis = 0)

ax.scatter(annual_plot_data.index, annual_plot_data.AvgTemp, \

label = '年度平均', c = 'k')

移动平均

ax.plot(smoothed_df.index, smoothed_df.AvgTemp, c = 'r', \

linestyle = '--', \

label = f'{window} 年移动平均')

ax.set_title('平均空气温度测量', fontsize = 16)

使刻度包括第一个和最后一个年份

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(17, 20)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

最终图表应如下所示:

图 3.11:原始数据、年均值和平滑数据的最终图表

图 3.11:原始数据、年均值和平滑数据的最终图表

看图 3.11,我们可以立即得出几个有趣的观察结果。首先,温度从 1902 年到大约 1950 年保持相对一致,之后温度趋势上升直到数据结束。其次,测量值中即使在每年平均后仍然存在波动或噪声。第三,1960 年左右似乎有一个变化,这可能代表测量方法的变化或其他因素;我们可能需要与业务团队进一步沟通以更全面地了解这一点。

最后,请注意,在存在趋势的时期,移动平均值通常位于原始数据的右侧。这是滚动()方法中默认参数的直接结果;每个移动平均值是当前点和其左侧 9 个点的平均值。

注意

要访问本节的源代码,请参阅 https://packt.live/316S0o6。

你也可以在 https://packt.live/2CmpJPZ 在线运行这个示例。你必须执行整个 Notebook 才能得到期望的结果。

活动 3.01:使用移动平均绘制数据

在本次活动中,我们获取了来自德克萨斯州奥斯丁的天气信息数据集(austin_weather.csv),该数据集可以在随附的源代码中找到,我们将查看日均温度的变化情况。我们将为此数据集绘制一个移动平均滤波器。

注意

原始数据集可以在这里找到:https://www.kaggle.com/grubenm/austin-weather

需要执行的步骤如下:

导入 pandas 和 matplotlib.pyplot。

将数据集从 CSV 文件加载到 pandas DataFrame 中。

我们只需要日期和 TempAvgF 列;请从数据集中删除所有其他列。

最初,我们只关注第一年的数据,因此需要提取该信息。

在 DataFrame 中创建一个列来存储年份值,并从 Date 列中的字符串中提取年份值,将这些值分配到 Year 列中。

重复此过程以提取月份值,并将这些值作为整数存储在 Month 列中。

再次重复此过程,将日值存储为 Day 列中的整数。

将第一年的数据复制到 DataFrame 中。

计算 20 天的移动平均滤波器。

绘制原始数据和移动平均信号,x 轴表示年份中的天数。

输出应如下所示:

图 3.12:温度数据叠加在 20 天移动平均线上

图 3.12:温度数据叠加在 20 天移动平均线上

注意

本活动的解答可以通过这个链接找到。

你已经学习了如何从 CSV 文件加载数据,如何删除不需要的列,如何从包含日期的文本字段中提取信息,如何使用移动平均法平滑数据,以及如何可视化结果。

线性回归

我们将通过选择线性模型开始对回归模型的研究。线性模型是一个非常好的初步选择,因为它们具有直观的性质,而且在预测能力上也非常强大,前提是数据集包含一定程度的线性或多项式关系,输入特征和输出值之间有某种联系。线性模型的直观性质通常源于能够将数据绘制在图表上,并观察数据中呈现出的趋势模式,例如,输出(数据的 y 轴值)与输入(x 轴值)呈正相关或负相关。线性回归模型的基本组件通常也在高中数学课程中学习过。你可能还记得,直线的方程定义如下:

图 3.13:直线方程

图 3.13:直线方程

在这里,x 是输入值,y 是相应的输出或预测值。模型的参数包括直线的斜率(y 值的变化除以 x 的变化,也叫梯度),在方程中用β1 表示,以及 y 截距值β1,表示直线与 y 轴交点的位置。通过这样的模型,我们可以提供β1 和β0 参数的值来构建一个线性模型。

例如,y = 1 + 2 * x 的斜率为 2,这意味着 y 值的变化速度是 x 值的两倍;该直线在 y 轴的截距为 1,以下图所示:

图 3.14:直线参数和线性模型

图 3.14:直线参数和线性模型

所以,我们了解了定义直线所需的参数,但这并没有做什么特别有趣的事情。我们只是规定了模型的参数来构建一条直线。我们想要做的是,使用一个数据集来构建一个最能描述该数据集的模型。根据上一节的内容,我们希望选择线性模型作为模型架构,然后训练模型来找到最佳的β0 和β1 值。如前所述,这个数据集需要具有某种程度的线性关系,才能使线性模型成为一个好的选择。

最小二乘法

机器学习中使用的许多技术其实早在机器学习作为描述出现之前就已经存在。有些技术体现了统计学的元素,而其他技术则已经在科学中被用来“拟合”数据很长时间。最小二乘法用于找出最能代表一组数据的直线方程,这就是其中之一,最早创建于 19 世纪初。这种方法可以用来说明监督学习回归模型的许多关键概念,因此我们将在这里从它开始。

最小二乘法侧重于最小化预测的 y 值与实际 y 值之间的误差平方。最小化误差的思想是机器学习中的基本概念,也是几乎所有学习算法的基础。

尽管使用最小二乘法的简单线性回归可以写成简单的代数表达式,但大多数包(如 scikit-learn)在“幕后”会有更通用的优化方法。

Scikit-Learn 模型 API

Scikit-learn API 使用类似的代码模式,无论构建的是何种类型的模型。通用流程是:

导入你想要使用的模型类型的类。

在这里,我们将使用 from sklearn.linear_model import LinearRegression

实例化模型类的一个实例。此处将设置超参数。对于简单的线性回归,我们可以使用默认值。

使用拟合方法并应用我们想要建模的 x 和 y 数据。

检查结果,获取指标,然后进行可视化。

让我们使用这个工作流程,在下一个练习中创建一个线性回归模型。

练习 3.02:使用最小二乘法拟合线性模型

在本练习中,我们将使用最小二乘法构建第一个线性回归模型,来可视化每年时间范围内的气温,并使用评估指标评估模型的表现:

注意

我们将使用与练习 3.01 中相同的 synth_temp.csv 数据集:使用移动平均法绘制数据。

从 scikit-learn 的 linear_model 模块导入 LinearRegression 类,并导入我们需要的其他包:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

加载数据。对于本练习,我们将使用之前使用的相同的合成温度数据:

加载数据

df = pd.read_csv('../Datasets/synth_temp.csv')

重复之前的数据预处理过程:

切片从 1902 年开始

df = df.loc[df.Year > 1901]

按年份进行汇总

df_group_year = df.groupby(['Year']).agg({'RgnAvTemp' : 'mean'})

df_group_year.head(12)

添加年列,以便我们可以在模型中使用它。

df_group_year['Year'] = df_group_year.index

df_group_year = \

df_group_year.rename(columns = {'RgnAvTemp' : 'AvTemp'})

df_group_year.head()

数据应如下所示:

图 3.15:预处理后的数据

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-RJBJLLG8.jpg)

图 3.15:预处理后的数据

实例化 LinearRegression 类。然后,我们可以使用我们的数据来拟合模型。最初,我们只会将温度数据拟合到年份。在以下代码中,请注意该方法要求 x 数据为 2D 数组,并且我们仅传递了年份。我们还需要使用 reshape 方法,并且在 (-1, 1) 参数中,-1 表示“该值从数组的长度和剩余维度中推断出来”:

构建模型并检查结果

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(df_group_year['Year'].values.reshape((-1, 1)), \

df_group_year.AvTemp)

print('模型斜率 = ', linear_model.coef_[0])

print('模型截距 = ', linear_model.intercept_)

r2 = linear_model.score(df_group_year['Year']\

.values.reshape((-1, 1)), \

df_group_year.AvTemp)

print('r 平方 = ', r2)

注意

有关 scikit-learn 的更多阅读,请参考以下链接: scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html

输出将如下所示:

图 3.16:使用拟合方法的结果

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-CEGQBU5B.jpg)

图 3.16:使用拟合方法的结果

注意使用 score 方法,这是模型对象的方法,用于获得 r2 值。这个度量叫做决定系数,是线性回归中广泛使用的度量。r2 越接近 1,说明我们的模型对数据的预测越精确。有多个公式可以用来计算 r2。这里有一个例子:

图 3.17:r2 计算

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-ZH6EW0LQ.jpg)

图 3.17:r2 计算

从图 3.17 中,你可以通过注意到分子是预测误差的总和,而分母是数据与均值的变动之和,来对 r2 有一些理解。因此,随着预测误差的减小,r2 会增大。这里需要强调的是,r2 只是“拟合优度”的一个度量——在本例中,指的是简单的直线如何拟合给定的数据。在更复杂的现实世界监督学习问题中,我们会使用更稳健的方法来优化模型并选择最佳/最终模型。特别是,一般情况下,我们会在未用于训练的数据上评估模型,因为在训练数据上评估会给出过于乐观的性能度量。第七章《模型评估》将讨论这一点。

为了可视化结果,我们需要将一些数据传递给模型的 predict 方法。一个简单的方法是直接重用我们用来拟合模型的数据:

生成可视化预测

pred_X = df_group_year.loc[:, 'Year']

pred_Y = linear_model.predict(df_group_year['Year']\

.values.reshape((-1, 1)))

现在,我们已经拥有可视化结果所需的一切:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

raw_plot_data = df[df.Year > 1901]

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'red', s = 1.5)

年度平均值

ax.scatter(df_group_year.Year, df_group_year.AvTemp, \

label = '年平均', c = 'k', s = 10)

线性拟合

ax.plot(pred_X, pred_Y, c = "blue", linestyle = '-.', \

linewidth = 4, label = '线性拟合')

ax.set_title('平均气温测量', fontsize = 16)

使刻度包含第一个和最后一个年份

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(15, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出结果如下:

图 3.18:线性回归 - 第一个简单的线性模型

图 3.18:线性回归 - 第一个简单的线性模型

从图 3.18 中可以看出,直线并不是数据的一个很好的模型。我们将在一个活动之后回到这个问题。

注意

要访问此特定部分的源代码,请参阅 https://packt.live/2NwANg1。

你也可以在 https://packt.live/2Z1qQfT 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。

我们已经看到了如何加载一些数据,如何从 scikit-learn 导入 LinearRegression 类,并使用 fit、score 和 predict 方法构建模型,查看性能指标,并可视化结果。在此过程中,我们介绍了最小二乘法,给出了一些数学背景,并展示了部分计算过程。

我们看到,对于我们的合成温度数据,线性模型并不完全适合这些数据。没关系。在大多数情况下,早期生成一个基准模型是一个好习惯,这个模型可以作为更复杂模型性能的比较基准。因此,我们可以将这里开发的线性模型视为一个简单的基准模型。

在继续之前,需要注意的是,当报告机器学习模型的性能时,训练模型所使用的数据不能用于评估模型性能,因为这会给出模型性能的过于乐观的视角。我们将在第七章《模型评估》中讨论验证的概念,包括评估和报告模型性能。然而,本章中我们将使用训练数据来检查模型性能;只要记住,在完成第七章《模型评估》后,你会更清楚如何做。

活动 3.02:使用最小二乘法进行线性回归

对于这个活动,我们将使用在前一个活动中使用的德克萨斯州奥斯汀的天气数据集。我们将使用最小二乘法为该数据集绘制线性回归模型。

要执行的步骤如下:

导入必要的包、类等。如果需要,请参阅练习 3.02:使用最小二乘法拟合线性模型。

从 csv 文件加载数据(austin_weather.csv)。

检查数据(使用 head() 和 tail() 方法)。

df.head()的输出将如下所示:

图 3.19:df.head()的输出

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-UPOYLJPO.jpg)

图 3.19:df.head()的输出

df.tail()的输出将如下所示:

图 3.20:df.tail()的输出

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-LPNVRK9C.jpg)

图 3.20:df.tail()的输出

删除除 Date 和 TempAvgF 列以外的所有列。

创建新的 Year、Month 和 Day 列,并通过解析 Date 列来填充它们。

创建一个新的列用于移动平均,并用 TempAvgF 列的 20 天移动平均值填充它。

切割出一整年的数据用于模型训练。确保该年份的数据没有因移动平均而缺失。此外,创建一个 Day_of_Year 列(应从 1 开始)。

创建一个散点图,显示原始数据(原始 TempAvgF 列),并在其上叠加 20 天移动平均线。

绘图将如下所示:

图 3.21:原始数据与叠加的 20 天移动平均

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-5VDXTR6A.jpg)

图 3.21:原始数据与叠加的 20 天移动平均

使用默认参数创建线性回归模型,即计算模型的 y 截距,并且不对数据进行归一化。

现在拟合模型,其中输入数据是年份的天数(1 到 365),输出是平均温度。打印模型的参数和 r² 值。

结果应如下所示:

模型斜率: [0.04304568]

模型截距: 62.23496914044859

模型 r² 值: 0.09549593659736466

使用相同的 x 数据从模型中生成预测。

创建一个新的散点图,像之前一样,添加模型预测的叠加图层。

图 3.22:原始数据、20 天移动平均和线性拟合

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-3A1PPP86.jpg)

图 3.22:原始数据、20 天移动平均和线性拟合

注意

该活动的解决方案可以通过此链接找到。

基于之前的练习,你已经了解了如何加载并使用 scikit-learn 中的 LinearRegression 类,以及 fit、score 和 predict 方法。不出所料,产生直线的简单线性模型并不是该数据的最佳模型。在后续练习中,我们将探讨可能的改进方法。

你已经学会了如何加载数据、将其结构化以适应 scikit-learn API,并使用 LinearRegression 类将一条简单的直线拟合到数据上。显然,这对该数据来说是一个不理想的模型,因此我们将探索改进模型的方法,从下一个话题“具有分类变量的线性回归”开始。

具有分类变量的线性回归

模型架构选择阶段有一个方面与数据准备阶段有些重叠:特征工程。广义上讲,特征工程涉及创建附加的特征(在我们这里是列),并将它们用于模型中以提高模型性能。特征可以通过转换现有特征(例如取对数或平方根)来工程化,或者以某种方式生成并添加到数据集中。举个后者的例子,我们可以从数据集中的日期信息中提取出月份、日期、星期几等。虽然像月份这样的新特征可以是一个数值,但在大多数监督学习的情况下,简单地使用这种特征的数值并不是最佳实践。一个简单的思路是:如果我们将 1 月到 12 月编码为 1 到 12,那么模型可能会给 12 月更多的权重,因为 12 月比 1 月大 12 倍。此外,当日期从 12 月切换回 1 月时,值会发生人为的阶跃变化。因此,这样的特征被认为是名义类别的。名义类别变量是具有多个可能值的特征,但这些值的顺序不包含任何信息,甚至可能会误导。还有一些类别变量确实有隐含的顺序,它们被称为有序类别变量。例如,“小”、“中”、“大”、“特大”和“巨大”等。

为了处理大多数机器学习模型中的任何类型的类别数据,我们仍然需要将其转换为数字。这种转换的通用方法叫做编码。一种非常强大但易于理解的编码方法是使用独热编码将类别特征转换为数值。

使用独热编码时,类别特征的每个可能值都会变成一列。在对应于给定值的列中,如果该数据实例在该值下具有该特征,则输入 1,否则输入 0。一个例子会让这一点更加清晰,如下图所示:

图 3.23:名义类别列的独热编码

图 3.23:名义类别列的独热编码

因此,通过创建这些列并在适当的位置插入 1,我们让模型“知道”名义类别变量的存在,但不会对任何特定值赋予额外的权重。在图 3.23 中的例子中,如果我们试图模拟狗的预期寿命,在使用独热编码之前,我们只有饮食和体重作为预测因素。应用独热编码后,我们预计能够得到一个更好的模型,因为我们的直觉是:在其他因素相等的情况下,一些犬种比其他犬种活得更久。在接下来的练习中,我们将看到如何使用编码来利用线性模型的强大能力来模拟复杂的行为。

注意

还有许多其他可能的编码分类变量的方法;例如,参见《神经网络分类器的分类变量编码技术比较研究》:https://pdfs.semanticscholar.org/0c43/fb9cfea23e15166c58e24106ce3605b20229.pdf

在某些情况下,最佳方法可能取决于所使用的模型类型。例如,线性回归要求特征之间没有线性依赖(我们将在本章后面进一步讨论)。独热编码实际上会引入这个问题,因为第 n 类别实际上可以通过其他 n-1 类别来确定——直观地,在图 3.23 中,如果比格犬、拳师犬、吉娃娃、柯利犬和德国牧羊犬都是 0,那么迷你杜宾犬就是 1(假设一个实例不可能有多个有效类别)。因此,在进行线性回归时,我们使用稍有不同的编码方法,即虚拟变量。虚拟变量和独热编码的唯一区别是我们去掉了 n 个列中的一个,从而消除了依赖关系。

练习 3.03:引入虚拟变量

在本练习中,我们将向线性回归模型中引入虚拟变量,以提高其性能。

我们将使用与之前练习相同的 synth_temp 数据集:

导入所需的包和类:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

加载数据:

加载数据

df = pd.read_csv('../Datasets/synth_temp.csv')

从 1902 年起切片数据框,然后计算每年的平均值:

从 1902 年起切片

print(df.head())

df = df.loc[df.Year > 1901]

print(df.head())

输出将如下所示:

图 3.24:切片 1902 后的输出

图 3.24:切片 1902 后的输出

按年份汇总

df_group_year = df.groupby(['Year', 'Region'])\

.agg({'RgnAvTemp':'mean'})

"""

注意,.droplevel() 方法会移除多重索引

通过 .agg() 方法添加()以简化操作

后续分析中

"""

print(df_group_year.head(12))

print(df_group_year.tail(12))

数据应如下所示:

图 3.25:按地区划分的年均温度

图 3.25:按地区划分的年均温度

使用索引(即年份)级别 0 和地区列(即索引级别 1)来添加 Year 列和 Region 列:

添加地区列,以便我们可以用它来创建虚拟变量

df_group_year['Region'] = df_group_year.index.get_level_values(1)

添加 Year 列,以便我们可以在模型中使用它

df_group_year['Year'] = df_group_year.index.get_level_values(0)

重置长轴上的索引

df_group_year = df_group_year.droplevel(0, axis = 0)

df_group_year = df_group_year.reset_index(drop = True)

也许温度水平或变化因地区而异。让我们看一下每个地区的整体平均温度:

按地区检查数据

region_temps = df_group_year.groupby('Region').agg({'RgnAvTemp':'mean'})

colors = ['red', 'green', 'blue', 'black', 'lightcoral', \

'palegreen','skyblue', 'lightslategray', 'magenta', \

'chartreuse', 'lightblue', 'olive']

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

ax.bar(region_temps.index, region_temps.RgnAvTemp, \

color = colors, alpha = 0.5)

ax.set_title('平均空气温度测量值', fontsize = 16)

ax.set_xlabel('区域', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.tick_params(labelsize = 12)

plt.show()

结果应如下所示:

图 3.26: 各区域的整体平均温度

图 3.26: 各区域的整体平均温度

我们看到,平均而言,区域之间的温差可达 5 度。因此,考虑区域可能对模型有益。为此,我们将从 Region 列创建虚拟变量。

Pandas 有一个名为 get_dummies() 的 DataFrame 方法,我们可以用它来满足我们的需求。首先,我们创建一个包含新列的新 DataFrame。请注意,它们已经填充了零和一。然后,我们将虚拟变量列与数据合并,并删除 Region 列,因为它现在是冗余的:

将分类变量 'region' 转换为虚拟变量

dummy_cols = pd.get_dummies(df_group_year.Region, \

drop_first = True)

df_group_year = pd.concat([df_group_year, dummy_cols], axis = 1)

print(df_group_year.head())

print(df_group_year.tail())

结果应如下所示:

图 3.27: 添加区域的虚拟变量

图 3.27: 添加区域的虚拟变量

请注意,在 get_dummies 方法中,我们设置了 drop_first = True 参数以删除其中一列,正如前面讨论的那样。

我们现在创建一个线性模型,和之前一样,使用 Year 列和所有虚拟列:

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(df_group_year.loc[:, 'Year':'L'], \

df_group_year.RgnAvTemp)

r2 = linear_model.score(df_group_year.loc[:, 'Year':'L'], \

df_group_year.RgnAvTemp)

print('r 平方 ', r2)

输出结果如下:

r 平方 0.7778768442731825

r2 值比之前高得多,看起来很有前景。从包含虚拟变量的 DataFrame 生成预测结果,然后将所有内容可视化到图表中:

构建用于预测的模型数据

pred_X = df_group_year.drop(['RgnAvTemp', 'Region'], axis = 1)

pred_Y = linear_model.predict(pred_X.values)

preds = pd.concat([df_group_year.RgnAvTemp, \

df_group_year.Region, \

pred_X, pd.Series(pred_Y)], axis = 1)

preds.rename(columns = {0 : 'pred_temp'}, inplace = True)

print(preds.head())

数据应如下所示:

图 3.28: 新模型的预测结果

图 3.28: 新模型的预测结果

为了绘图,我们通过从预测结果中抽样来减少杂乱:

定义原始数据和预测值的样本

设置随机种子,以确保结果可重复

np.random.seed(42)

plot_data = preds.sample(n = 100)

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

raw_plot_data = plot_data

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = 'Raw Data', c = 'red', s = 1.5)

年度平均值

annual_plot_data = df_group_year.groupby('Year').agg('mean')

ax.scatter(annual_plot_data.index, annual_plot_data.RgnAvTemp, \

label = 'Annual average', c = 'k', s = 10)

让我们也可视化线性拟合结果:

fit_data = plot_data

for i in range(len(plot_data.Region.unique())):

region = plot_data.Region.unique()[i]

plot_region = fit_data.loc[fit_data.Region == region, :]

ax.scatter(plot_region.Year, plot_region.pred_temp, \

edgecolor = colors[i], facecolor = "none", \

s = 80, label = region)

绘制连接原始数据和预测值的虚线

for i in fit_data.index:

ax.plot([fit_data.Year[i], fit_data.Year[i]], \

[fit_data.pred_temp[i], fit_data.RgnAvTemp[i]], \

'-', linewidth = 0.1, c = "red")

ax.set_title('Mean Air Temperature Measurements', fontsize = 16)

使刻度包括第一年和最后一年

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('Year', fontsize = 14)

ax.set_ylabel('Temperature (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(15, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

注意

为确保无错误执行,您应在编写步骤 9 和 10 的代码后再运行该单元。

绘制结果应如下所示:

图 3.29:新模型的预测

图 3.29:新模型的预测

我们可以看到,模型正在为不同区域预测不同的水平,尽管仍未完全跟随趋势,但相比之前,它已能解释更多的变异性。

现在让我们通过绘制一个区域来结束,以便了解模型的效果:

我们先绘制一个区域

region_B = preds.loc[preds.B == 1, :]

np.random.seed(42)

plot_data = region_B.sample(n = 50)

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

ax.scatter(plot_data.Year, plot_data.RgnAvTemp, \

label = 'Raw Data', c = 'red', s = 1.5)

ax.scatter(plot_data.Year, plot_data.pred_temp, \

label = "Predictions", facecolor = "none", \

edgecolor = "blue", s = 80)

绘制连接原始数据和预测值的虚线:

for i in plot_data.index:

ax.plot([plot_data.Year[i], plot_data.Year[i]], \

[plot_data.pred_temp[i], plot_data.RgnAvTemp[i]], \

'-', linewidth = 0.1, c = "red")

使刻度包括第一年和最后一年

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('Year', fontsize = 14)

ax.set_ylabel('Temperature (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(16, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

注意

为确保无错误执行,您应在编写步骤 11 和 12 的代码后再运行该单元。

结果应如下所示:

图 3.30:B 区域的预测

图 3.30:B 区域的预测

注意

若要访问此特定部分的源代码,请参见 https://packt.live/2YogxDt。

你也可以通过 https://packt.live/311LDCx 在线运行这个示例。你必须执行整个 Notebook 才能获得所需结果。

我们现在有了一个改进的模型,它能够跟随数据的大部分变化。然而,我们可以看到,仍然没有捕捉到大约在 1960 年左右出现的趋势变化。为了解决这个问题,我们将探索使用线性回归通过使用 x 数据的幂(即多项式模型)来拟合模型。首先,你将通过使用奥斯汀温度数据集来练习使用虚拟变量。

活动 3.03:虚拟变量

对于这项活动,我们将使用在前一项活动中使用的奥斯汀(德州)天气数据集。在本次活动中,我们将使用虚拟变量来增强该数据集的线性回归模型。

需要执行的步骤如下:

从 scikit-learn 加载 LinearRegression 类,以及 fit、score 和 predict 方法,还需要导入 pandas 和 matplotlib.pyplot。

加载 austin_weather.csv 数据集,删除除 Date 和 TempAvgF 列以外的所有列,并从数据中创建 Year、Month 和 Day 列。

创建一个 20 天的移动平均列并填充数据,然后切片出完整的第一年数据(从第 1 天到第 365 天—即 2015 年)。切片后,重置数据框的索引(使用 Pandas 核心方法 reset_index)。现在,创建一个 Day_of_Year 列并填充数据(请记住,第一天应为 1,而不是 0)。

绘制原始数据和移动平均线相对于 Day_of_Year 的图表。

图表应如下所示:

图 3.31:奥斯汀气温和移动平均线

图 3.31:奥斯汀气温和移动平均线

现在,研究将月份添加到模型中是否能够改进模型。为此,使用 pandas 的 get_dummies 方法对数据框的 Month 列创建一个 dummy_vars 数据框,并将虚拟列重命名为 Jan 至 Dec。现在,将 dummy_vars 合并到新的数据框 df_one_year 中。

显示数据框并确认虚拟列已存在。

使用最小二乘法线性回归模型,并将模型拟合到 Day_of_Year 值和虚拟变量上,以预测 TempAvgF。

获取模型参数和 r2 值。r2 值应该比前一项活动中的值大得多。

使用 Day_of_Year 值和虚拟变量,预测 df_one_year 数据中的温度。

绘制原始数据、20 天移动平均线和新的预测值。

输出将如下所示:

图 3.32:使用月份虚拟变量的线性回归结果

图 3.32:使用月份虚拟变量的线性回归结果

注意

本次活动的解决方案可以通过此链接找到。

你已经学习了如何使用 scikit-learn API 中的 LinearRegression 类来拟合包含虚拟变量的数据。使用 get_dummies pandas 方法生成了额外的变量列来编码月份,以改进模型。新模型的一个有用特性是,它考虑了温度的季节性变化,以及任何整体趋势。然而,它是相当分段的,这可能不完全符合预测的业务需求。

到目前为止,你应该已经对基本的线性回归以及使用 scikit-learn 接口和 get_dummies pandas 方法感到熟悉。我们还介绍了一些可视化的要点,并在使用虚拟变量的背景下引入了特征工程的概念。接下来我们将介绍多项式回归,它将特征工程带入一个不同的方向,同时仍然利用线性回归的强大功能。

线性回归的多项式模型

线性回归模型并不限于直线线性模型。我们可以使用完全相同的技术拟合一些更复杂的模型。在合成的温度数据中,我们可以看到趋势的上升曲线。因此,除了任何整体(随时间变化的线性)趋势外,可能还存在与时间的正幂相关的趋势。如果我们使用自变量的整数幂来构建模型,这就叫做多项式回归。对于幂次为 2 的情况,方程式如下所示。注意,我们将多项式的阶数称为最高幂次,因此这是一个阶数为 2 的多项式:

图 3.33:二阶多项式的方程

图 3.33:二阶多项式的方程

添加这个平方项将趋势线从直线转变为具有曲率的线。一般来说,多项式模型在拟合给定数据时可能非常强大,但它们可能无法很好地在数据范围之外进行外推。这将是过拟合的一个例子,尤其是在多项式阶数增加时,这一点尤为明显。

因此,通常情况下,除非有明确的业务需求或已知的潜在模型表明需要采取不同方法,否则你应该有限地使用多项式回归,并保持阶数较低:

图 3.34:二阶多项式的 y 与 x 的关系图

图 3.34:二阶多项式的 y 与 x 的关系图

注意,这里我们正在查看一个简单的模型,其中只有一个特征变量 x。在更复杂的情况下,我们可能有多个特征。随着特征数量的增加,方程中的项数会迅速增加。为了构建多项式,像 scikit-learn 这样的回归包提供了自动生成多项式特征的方法(例如,sklearn.preprocessing.PolynomialFeatures)。在这里,我们将手动构建一个简单的多项式模型,以说明这一方法。

练习 3.04:线性回归的多项式模型

为了使用线性回归拟合多项式模型,我们需要创建提升到所需幂次的特征。回顾一下之前关于特征工程的讨论;我们将创建新的特征,即将原始自变量提升到一个幂次:

从 synth_temp.csv 数据开始,加载包和类,然后像以前一样预处理数据:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

加载数据

df = pd.read_csv('../Datasets/synth_temp.csv')

从 1902 年及以后开始切片

df = df.loc[df.Year > 1901]

按年汇总

df_group_year = df.groupby(['Year']).agg({'RgnAvTemp' : 'mean'})

现在,我们使用索引添加 Year 列,然后通过将 Year 列的值平方来计算 Year2 列:

添加 Year 列,以便我们可以在模型中使用它

df_group_year['Year'] = df_group_year.index

df_group_year = df_group_year\

.rename(columns = {'RgnAvTemp' : 'AvTemp'})

添加一个 Year**2 列,构建二次多项式模型

df_group_year['Year2'] = df_group_year['Year']**2

print(df_group_year.head())

print(df_group_year.tail())

结果如下:

图 3.35:带有年份和年份平方的年度温度数据

图 3.35:带有年份和年份平方的年度温度数据

将数据拟合到模型中。这次,我们需要提供两组值作为模型的输入,Year 和 Year2,相当于将 x 和 x2 传递给多项式方程。由于我们提供了两列数据,因此不需要重塑输入数据,它将默认作为一个 N x 2 的数组提供。目标 y 值保持不变:

构建模型并检查结果

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(df_group_year.loc[:, ['Year', 'Year2']], \

df_group_year.AvTemp)

print('模型系数 = ', linear_model.coef_)

print('模型截距 = ', linear_model.intercept_)

r2 = linear_model.score(df_group_year.loc[:, ['Year', 'Year2']], \

df_group_year.AvTemp)

print('r 平方 = ', r2)

输出结果如下:

模型系数 = [-1.02981369e+00 2.69257683e-04]

模型截距 = 1002.0087338444181

r 平方 = 0.9313996496373635

该模型在虚拟变量方法上有所改进,但让我们通过可视化结果来看看它是否更合理。首先,生成预测结果。这里,我们额外采取一步,将预测延伸到未来 10 年,看看这些预测是否合理。在大多数监督学习问题中,最终目标是预测以前未知的数据值。由于我们的模型仅使用 Year 和 Year2 变量,我们可以生成一个年份值的列表,然后像之前一样平方它们,预测未来 10 年的温度:

生成可视化的预测

pred_X = df_group_year.loc[:, ['Year', 'Year2']]

pred_Y = linear_model.predict(pred_X)

生成未来 10 年的预测

pred_X_future = pd.DataFrame(list(range(2011, 2021)))\

.rename(columns = {0 : 'Year'})

pred_X_future['Year2'] = pred_X_future['Year']**2

pred_Y_future = linear_model.predict(pred_X_future)

现在,创建一个可视化:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

raw_plot_data = df

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'red', s = 1.5)

年度平均

ax.scatter(df_group_year.Year, df_group_year.AvTemp, \

label = '年度平均', c = 'k', s = 10)

线性拟合

ax.plot(pred_X.Year, pred_Y, c = "blue", linestyle = '-.', \

linewidth = 4, label = '线性拟合')

可视化未来预测:

ax.plot(pred_X_future.Year, pred_Y_future, c = "purple", \

linestyle = '--', linewidth = 4, \

label = '未来预测')

ax.set_title('平均气温测量', fontsize = 16)

使得刻度包含首尾年份

tick_years = [1902] + list(range(1910, 2021, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(15, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

结果如下:

图 3.36:使用二次多项式模型的线性回归

图 3.36:使用二次多项式模型的线性回归

注意

要访问此特定部分的源代码,请参考 https://packt.live/3fSusaR.

你也可以在线运行这个例子,访问 https://packt.live/2BulmCd。你必须执行整个笔记本才能获得期望的结果。

参考图 3.36,我们可以看到使用多项式模型的性能优势,趋势线几乎跟随 10 年移动平均。这是一个相对较好的拟合,因为每年平均的原始数据有一定噪声。在这种情况下,不应期望模型能够完美地拟合数据。如果我们的模型能够完美地拟合观察到的样本,那么就有很强的过拟合数据的风险,导致对未知样本的预测能力差。例如,假设我们对这些数据进行了 10 阶多项式拟合。结果模型会在数据点上上下波动,最后一个数据点可能会非常陡峭地上升或下降,这将导致糟糕的预测。在这种情况下,使用二阶多项式拟合,我们可以看到未来的趋势似乎是合理的,尽管这仍然需要验证。

活动 3.04:使用线性回归进行特征工程

我们尝试了标准线性模型以及包含虚拟变量的模型。在这项工作中,我们将创建一些周期特征,试图更好地拟合数据。周期特征源自在独立变量某个范围内重复的函数。在图 3.37 中,我们可以看到,年初的数据接近相同的值,而在中间,温度先升高然后降低。这在直觉上是合理的,因为我们知道在温带气候中,存在一年一度的温度周期。因此,如果我们在 1 年的时间尺度上包含周期性特征,可能会改进模型。我们可以构造正弦和余弦函数,其行为符合所需。

当拟合一个含有工程周期特征的模型时,我们面临一个额外的挑战,即确定如何将特征的周期循环与实际数据对齐。在这种情况下,您可以将其视为时间偏移量,这是我们事先不知道的。您也可以将偏移量视为超参数——拟合模型并不会给出这个值,因此我们必须以其他方式找到最佳值。在下图中,所需的偏移量为Δt:

图 3.37:展示随时间周期性行为的一些数据及适合数据的候选函数

图 3.37:展示随时间周期性行为的一些数据及适合数据的候选函数

幸运的是,如果我们使用正弦和余弦函数作为我们的特征,有一种方法可以解决这个问题。这是一个数学事实,即任何给定的正弦函数在单一周期内(如图 3.37 中的原始数据)可以表示为相同周期的正弦和余弦函数的线性组合。在下一张图中,我们展示了一个周期为 365 天的正弦和余弦函数以及图 3.37 中的原始数据。我们还展示了与原始数据非常匹配的正弦和余弦函数的线性组合。因此,要将正弦(或余弦)函数拟合到我们的数据中,我们只需设计两个特征,一个是时间的正弦值,另一个是时间的余弦值。线性回归然后将找到最佳系数,就像处理任何其他特征一样。请注意,这意味着我们知道周期是多少:

图 3.38:正弦和余弦函数的线性组合将匹配一个经未知时间偏移量移动的正弦函数

图 3.38:正弦和余弦函数的线性组合将匹配一个经未知时间偏移量移动的正弦函数

我们最后需要知道的是如何构建正弦和余弦函数。为此,我们可以使用 NumPy 方法 sin 和 cos。我们知道我们需要 1 年的周期,而我们的数据是按天计算的。编写一个具有 365 天周期的正弦函数的正确方法如下所示:

![图 3.39:周期为 365 天的正弦函数![图 3.36:部分数据在时间上表现出周期性行为及拟合数据的候选函数图 3.39:周期为 365 天的正弦函数或者,在 Python 中,具有 365 天周期的正弦函数如下:Figure 3.40: 一个周期为 365 天的 Python 系列

Figure 3.41: 在线性回归模型中使用正弦和余弦特征的预期结果

图 3.40:一个周期为 365 天的 Python 系列

现在,让我们继续进行使用周期函数来拟合奥斯汀温度数据的活动。要执行的步骤如下:

加载软件包和类(numpy、pandas、LinearRegression 和 matplotlib)。

执行与之前相同的预处理,直到创建 Day_of_Year 列的步骤。

添加一个 Day_of_Year 的正弦列和一个余弦列。

对平均温度与 Day_of_Year 以及正弦和余弦特征进行线性回归。

打印模型的参数和 r2 分数。

使用新特征生成预测。

可视化原始数据和新模型。

输出如下:

Figure 3.41: 在线性回归模型中使用正弦和余弦特征的预期结果

Figure 3.40: 一个周期为 365 天的 Python 系列

图 3.41:在线性回归模型中使用正弦和余弦特征的预期结果

到目前为止,您已经了解到我们可以通过将函数应用于现有特征来工程化特征,例如多项式函数或周期函数。您已经看到如何使用正弦和余弦函数构建周期函数以适应任意正弦或余弦函数。在这种情况下,我们假设了 365 天的周期,这是基于地球温带地区的年度气候周期而合理的。在实际业务案例中,我们可能不确定周期,或者可能同时存在多个周期(例如销售中的每周、每月和每季度周期)。此外,使用多项式和正弦/余弦等函数很容易导致过拟合,从而导致非常差的外推。

注意

您可以通过此链接找到此活动的解决方案。

在图 3.41 中,我们看到新模型在一年内平稳变化,并在年末返回接近年初的值。由于我们已经知道一年中存在总体趋势,这个模型因包含 Day_of_Year 特征,显示出年末的温度比年初略高。在特征工程方面,我们可以考虑使用日期(将其转换为整数)并在超过 1 年的数据上拟合模型,以捕捉长期趋势。

通用模型训练

构建线性回归模型的最小二乘法是一种有用且准确的训练方法,假设数据集的维度较低,并且系统内存足够大以管理数据集。

近年来,大型数据集变得更加容易获取,许多大学、政府,甚至一些公司都将大型数据集免费发布到网上;因此,在使用最小二乘法回归建模时,可能会相对容易超过系统内存。在这种情况下,我们需要采用不同的训练方法,比如梯度下降,这种方法不容易受到高维度的影响,可以处理大规模数据集,并且避免使用内存密集型的矩阵运算。

在我们更详细地探讨梯度下降之前,我们将以更一般的形式回顾一下模型训练的过程,因为大多数训练方法,包括梯度下降,都是遵循这个通用过程的。以下是模型训练过程中的参数更新循环概述:

图 3.42:通用模型参数更新循环

图 3.42:通用模型参数更新循环

训练过程包括将模型及其参数(包括超参数)反复暴露于一组样本训练数据,并将模型预测出的值传递给指定的代价或误差函数。代价函数与某些超参数一起,决定了如何计算“更新参数”模块中的更新,如图 3.42 所示。

代价函数用于确定模型与目标值之间的接近程度,并作为训练过程中进展的衡量标准。然而,代价函数也与一些超参数一起用于确定参数更新。例如,在我们的线性回归案例中,代价函数是均方误差。

最小二乘法,我们展示为构建线性回归模型的一种方法,最小化均方误差(MSE),因此称为最小二乘法。因此,我们可以将训练过程的图示更新为以下内容:

图 3.43:通用训练过程

图 3.43:通用训练过程

梯度下降

梯度下降的过程可以总结为一种根据系统中的误差,通过代价函数更新模型参数的方式。可以选择多种代价函数,具体取决于拟合的模型类型或解决的问题。我们将选择简单但有效的均方误差代价函数。

回想一下,直线方程可以写作如下:

图 3.44:直线方程

图 3.44:直线方程

下图展示了代价函数 J 在 β0 和 β1 范围值下的图像。最优的参数集是代价函数最小的参数,这个点被称为代价函数的全局最小值。我们可以将其类比为在徒步旅行中寻找山谷中最低点的过程。直观地说,不管我们站在何处,如果不在底部,那么我们正站在斜坡上,要到达底部,就需要朝下坡走。因此,斜坡就是梯度,而找到最小值的过程就是梯度下降:

图 3.45:简单的两参数线性回归中梯度下降的可视化表现

图 3.45:简单的两参数线性回归中梯度下降的可视化表现

如前图所示,在每个训练周期中,参数 β0 和 β1 会更新,以朝着最陡峭的斜坡(梯度)方向移动,最终找到 J(β) 的最小值。

让我们更详细地了解梯度下降算法:

梯度下降从对所有 β 值进行初始随机猜测开始。注意,在某些模型中,这一步骤,称为初始化,可能会通过选择某个特定的分布来约束初始值的采样。初始化的选择可以被视为一个超参数,并且可能会影响最终结果,尤其是在像人工神经网络这样的复杂模型中。大多数 Python 实现的方法都有很好的默认值,使用默认值是很常见的做法。

使用 β 的随机值为训练集中的每个样本做出预测,然后计算代价函数 J(β)。

然后,β 值会被更新,进行一个与误差成比例的小调整,以尽量减少误差。通常,尝试直接从当前 β 值跳到能够最小化 J(β) 的值并不是最好的方法,因为如图 3.45 所示,损失面可能并不平滑。大多数实际的损失面,即使在三维空间中,也有多个峰值和谷值。对于更复杂的代价函数面,存在多个最小值,称为局部最小值,所有最小值中的最低点就是全局最小值。非凸代价函数可能会在找到全局最小值时带来挑战,研究界在高效寻找非凸表面全局最小值方面投入了大量努力。超参数学习率,用 γ 表示,用于在每次训练时调整步长。

该过程在以下图表中进行了可视化,简化为二维:

图 3.46:梯度下降过程

图 3.46:梯度下降过程

在这个简化的情况下,使用较低的学习率导致路径 A,并且我们陷入局部最小值。路径 C 是由于非常高的学习率而不收敛。路径 B 使用中间的学习率值,并收敛到全局最小值。

作为设置学习率的一般提示,开始较大,例如约为 0.1,如果找不到解决方案,即错误为 NaN 或波动剧烈,则将学习率降低为 10 的因子。一旦找到允许误差随着 epoch 相对平稳下降的学习率,可以测试其他超参数,包括学习率,以达到最佳结果。

尽管这个过程听起来复杂,但实际上并不像看起来那么可怕。梯度下降可以通过一次性猜测参数值、计算猜测误差、对参数进行微小调整并持续重复此过程直到误差在最小值处收敛来概括。为了加强我们的理解,让我们看一个更具体的例子。我们将使用梯度下降来训练我们在练习 3.02 中构建的原始线性回归模型:使用最小二乘法拟合线性模型,将最小二乘法替换为梯度下降。

练习 3.05:使用梯度下降进行线性回归

在这个练习中,我们将手动实现梯度下降算法。我们将使用如前所述的 synth_temp.csv 数据:

与之前一样导入包和类:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

from sklearn.metrics import r2_score

在我们开始梯度下降过程之前,我们需要实现一些关键函数。

编写一个函数来定义我们的线性模型。这里使用线性代数乘法将参数(β)和输入值 x 之间的乘积形式,这是使用简化形式的线性模型的优势所在。

模型函数

def h_x(Beta, X):

计算 X 和 Betas 的矩阵点积

返回 np.dot(Beta, X).flatten()

我们还需要编写一个评估成本函数 J(β) 的函数:

成本函数

def J_beta(pred, true):

均方误差

return np.mean((pred - true) ** 2)

最后,我们需要实现更新参数的函数:

更新函数

def update(pred, true, X, gamma):

返回 gamma * np.sum((true - pred) * X, axis = 1)

接下来,加载数据,从 1902 年起切片,计算年均值,并添加年份列:

载入数据

df = pd.read_csv('../Datasets/synth_temp.csv')

切片 1902 年及以后

df = df.loc[df.Year > 1901]

按年份卷起

df_group_year = df.groupby(['Year']).agg({'RgnAvTemp' : 'mean'})

添加年份列以便在模型中使用

df_group_year['Year'] = df_group_year.index

df_group_year = \

df_group_year.rename(columns = {'RgnAvTemp' : 'AvTemp'})

现在,我们将构建训练数据。首先,我们需要在使用梯度下降之前,将数据缩放到 0 到 1 之间。某些机器学习算法可以处理原始数据(如常规线性回归),但在使用梯度下降时,如果变量的尺度差异很大,那么某些参数的梯度值将远大于其他参数的梯度值。如果数据没有缩放,原始数据可能会扭曲成本函数表面上的下降,从而偏移结果。直观地讲,在我们的例子中,Year 数据的量级是千,而 AvTemp 数据的量级是十。因此,Year 变量会在参数的影响力上占主导地位。

机器学习中使用了多种缩放方法。例如,将数据归一化到特定范围(如(0, 1)或(-1, 1)),以及标准化(将数据缩放到均值为 0,标准差为 1)。在这里,我们将 x 和 y 数据归一化到范围(0, 1):

对数据进行缩放并添加 X0 序列

X_min = df_group_year.Year.min()

X_range = df_group_year.Year.max() - df_group_year.Year.min()

Y_min = df_group_year.AvTemp.min()

Y_range = df_group_year.AvTemp.max() - df_group_year.AvTemp.min()

scale_X = (df_group_year.Year - X_min) / X_range

train_X = pd.DataFrame({'X0' : np.ones(df_group_year.shape[0]), \

'X1' : scale_X}).transpose()

train_Y = (df_group_year.AvTemp - Y_min) / Y_range

print(train_X.iloc[:, :5])

print(train_Y[:5])

输出应如下所示:

图 3.47:用于梯度下降的归一化数据

图 3.47:用于梯度下降的归一化数据

请注意,train_Y 的值是真实值,也称为真实标签(ground truth)。

如我们所学,我们需要初始化参数值。请注意,我们使用带有常数值的 NumPy random.seed() 方法。设置 random.seed 将在每次运行笔记本时重现相同的结果。在模型开发过程中以及探索超参数时,这非常有用,因为你可以看到所做更改的影响,而不是随机初始化的影响。reshape() 方法用于将数据转化为正确的矩阵形式:

初始化 Beta 和学习率 gamma

np.random.seed(42)

Beta = np.random.randn(2).reshape((1, 2)) * 0.1

print('初始 Beta\n', Beta)

值应如下所示:

初始 Beta [[ 0.04967142 -0.01382643]]

我们还需要设置一些超参数,包括学习率 gamma 和训练周期的最大次数(epochs):

gamma = 0.0005

max_epochs = 100

做出初步预测并使用定义的 h_x 和 J_beta 函数计算该预测的误差或成本:

y_pred = h_x(Beta, train_X)

print('初始成本 J(Beta) = ' + str(J_beta(y_pred, train_Y)))

输出将如下所示:

初始成本 J(Beta) = 0.18849128813354338

我们现在准备使用循环来迭代训练过程。在这里,我们存储了 epoch 和 cost 值,以便稍后进行可视化。同时,我们每 10 个 epoch 输出一次成本函数和 epoch 的值:

epochs = []

costs = []

for epoch in range(max_epochs):

Beta += update(y_pred, train_Y, train_X, gamma)

y_pred = h_x(Beta, train_X)

cost = J_beta(y_pred, train_Y)

if epoch % 10 == 0:

print('新成本 J(Beta) = ' + str(round(cost, 3)) \

  • ' 在第 ' + str(epoch) + ' 轮')

epochs.append(epoch)

costs.append(cost)

输出将如下所示:

图 3.48:每 10 个 epoch 的训练结果

图 3.48:每 10 个 epoch 的训练结果

在图 3.48 中观察到,成本函数在前 20 个周期内快速下降,然后改进速度放缓。这是梯度下降训练中的典型模式,尤其是当学习率处于合理值时。

可视化训练历史:

绘制训练历史

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

ax.plot(epochs, costs)

ax.tick_params(labelsize = 14)

ax.set_ylabel('成本函数 J(' + r'\(\theta\)' + ')', \

fontsize = 18)

ax.set_xlabel('Epoch', fontsize = 18)

plt.show()

输出将如下所示:

图 3.49:成本函数与 epoch 的关系图(前 100 个 epoch)

图 3.49:成本函数与 epoch 的关系图(前 100 个 epoch)

在图 3.49 中,我们可以看到,当我们停止更新时,成本函数仍在下降。因此,我们可以通过增大 max_epochs 超参数重新运行训练,看看结果是否有所改善。

使用我们之前导入的 sklearn.metrics 中的 r2_score 函数,计算使用梯度下降训练的模型的 R 平方值:

计算 r 平方值

r2 = r2_score(train_Y, y_pred)

print('r squared = ', r2)

输出应与以下内容类似:

r squared = 0.5488427996385263

注意,您可以调整学习率和最大 epoch 参数,观察它们对训练历史和 r2 值的影响。

现在,我们使用训练数据生成预测值,以便可视化结果模型。在这个单元格中,我们首先使用缩放后的训练数据进行预测,因为模型系数是基于缩放输入的,然后使用我们在缩放过程中保存的(Y_min 和 Y_range)值将结果还原为“实际”值。请注意,我们使用我们的模型函数 h_x 来生成预测值。另外,为了方便起见,我们将 pred_X 替换为原始年份值,用于可视化:

生成预测以便可视化

pred_X = train_X

进行预测

pred_Y = h_x(Beta, pred_X)

将预测值还原为实际值

pred_Y = (pred_Y * Y_range) + Y_min

替换 X 为原始值

pred_X = df_group_year['Year']

使用缩放数据进行回归的一个影响是,β 值是相对于缩放后的数据,而不是原始数据。在许多情况下,我们希望得到未缩放的参数值。特别是在线性回归中,未缩放的系数可以解释为自变量每单位变化时因变量的变化。例如,在我们的案例中,β1 是直线的“斜率”,表示每增加 1 年,平均年气温的变化。我们现在可以计算未缩放模型的参数:

将系数缩放回实际值

Beta0 = (Y_min + Y_range * Beta[0, 0] \

  • Y_range * Beta[0, 1] * X_min / X_range)

Beta1 = Y_range * Beta[0, 1] / X_range

可视化结果:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

raw_plot_data = df

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'red', s = 1.5)

年度平均

ax.scatter(df_group_year.Year, df_group_year.AvTemp, \

label = '年度平均', c = 'k', s = 10)

线性拟合

ax.plot(pred_X, pred_Y, c = "blue", linestyle = '-.', \

linewidth = 4, label = '线性拟合')

将模型绘制到图表上:

ax.text(1902, 20, 'Temp = ' + str(round(Beta0, 2)) \

+' + ' + str(round(Beta1, 4)) + ' * Year', \

fontsize = 16, backgroundcolor = 'white')

ax.set_title('平均气温测量', fontsize = 16)

使刻度包含第一个和最后一个年份

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(15, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

注意

为确保无错误执行,你应该在编写完步骤 15 和 16 的代码后再运行该单元。

输出结果如下:

图 3.50:使用梯度下降法测量的平均气温

图 3.50:使用梯度下降法测量的平均气温

注意

要访问该特定部分的源代码,请参考 https://packt.live/3diOR76。

你还可以在 https://packt.live/2YWvviZ 在线运行这个例子。你必须执行整个笔记本,才能得到期望的结果。

你刚刚用梯度下降法训练了你的第一个模型。这是一个重要的步骤,因为这个简单的工具可以用来构建更复杂的模型,如逻辑回归和神经网络模型。然而,我们首先必须注意一个重要的观察:梯度下降模型产生的 r-squared 值不如最小二乘法模型高,而且线的方程也不同。

话虽如此,梯度下降过程还有更多的选项可以调整,包括不同类型的梯度下降算法、更高级的学习率使用方法以及在训练过程中如何提供数据。这些修改超出了本书的范围,因为一本书可以专门讨论梯度下降过程及其性能优化方法。通过足够的实验,我们可以将两个结果匹配到任意精度,但在本案例中,这样做并不是高效的时间利用。

在本练习中,我们直接实现了梯度下降;然而,我们通常不会使用这种实现,而是利用现有的高效优化包。scikit-learn 的梯度下降方法包含了许多优化,并且只需几行代码即可使用。以下内容来自 scikit-learn 文档(参考 https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDRegressor.html):“SGD 代表随机梯度下降:每次估计一个样本的损失梯度,模型会在过程中按递减强度(即学习率)更新。”

到目前为止,在我们的示例中,我们直接使用了所有数据进行线性回归方法,或者通过梯度下降方法在每次更新时使用所有数据。然而,使用梯度下降时,我们可以完全控制何时更新我们对参数的估计。

练习 3.06:优化梯度下降

在本练习中,我们将使用 scikit-learn 模块中的 SGDRegressor,它利用随机梯度下降来训练模型。

在本练习中,我们从 synth_temp.csv 数据开始,和之前一样:

如之前一样导入所需的包和类,并添加 SGDRegressor:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

来自 sklearn.metrics 导入 r2_score

来自 sklearn.linear_model 导入 SGDRegressor

加载数据并进行与之前相同的预处理和标准化:

加载数据

df = pd.read_csv('../Datasets/synth_temp.csv')

从 1902 年开始切片

df = df.loc[df.Year > 1901]

按年份汇总

df_group_year = df.groupby(['Year']).agg({'RgnAvTemp' : 'mean'})

添加 Year 列,以便在模型中使用该列

df_group_year['Year'] = df_group_year.index

df_group_year = df_group_year\

.rename(columns = {'RgnAvTemp' : 'AvTemp'})

标准化数据

X_min = df_group_year.Year.min()

X_range = df_group_year.Year.max() - df_group_year.Year.min()

Y_min = df_group_year.AvTemp.min()

Y_range = df_group_year.AvTemp.max() - df_group_year.AvTemp.min()

scale_X = (df_group_year.Year - X_min) / X_range

train_X = scale_X.ravel()

train_Y = ((df_group_year.AvTemp - Y_min) / Y_range).ravel()

我们通过调用 SGDRegressor 来实例化模型,并传递超参数。在这里,我们设置了 NumPy random.seed 方法,且由于我们没有为 SGDRegressor 提供种子或方法,它将使用 NumPy 随机生成器:

创建模型对象

np.random.seed(42)

model = SGDRegressor(loss = 'squared_loss', max_iter = 100, \

learning_rate = 'constant', eta0 = 0.0005, \

tol = 0.00009, penalty = 'none')

我们通过调用模型对象的 fit 方法来拟合模型:

拟合模型

model.fit(train_X.reshape((-1, 1)), train_Y)

输出应如下所示,回显调用中使用的参数:

图 3.51:调用模型对象的 fit 方法后的输出

图 3.51:调用模型对象的 fit 方法后的输出

我们现在想要从模型中提取系数,并像之前的练习那样将其重新缩放,以便可以直接比较结果:

Beta0 = (Y_min + Y_range * model.intercept_[0] \

  • Y_range * model.coef_[0] * X_min / X_range)

Beta1 = Y_range * model.coef_[0] / X_range

print(Beta0)

print(Beta1)

输出应如下所示:

-0.5798539884018439

0.009587734834970016

如前所述,我们现在生成预测结果,然后使用 r2_score 函数计算 r2。请注意,由于我们使用的是 scikit-learn 方法,我们通过模型对象的 predict 方法来返回预测结果:

生成预测结果

pred_X = df_group_year['Year']

pred_Y = model.predict(train_X.reshape((-1, 1)))

计算 r squared 值

r2 = r2_score(train_Y, pred_Y)

print('r squared = ', r2)

结果将类似于以下内容:

r squared = 0.5436475116024911

最后,我们将预测结果重新缩放回实际温度以进行可视化:

将预测结果缩放回实际值

pred_Y = (pred_Y * Y_range) + Y_min

现在,展示结果:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

raw_plot_data = df

ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \

label = '原始数据', c = 'red', s = 1.5)

年度平均值

ax.scatter(df_group_year.Year, df_group_year.AvTemp, \

label = '年度平均', c = 'k', s = 10)

线性拟合

ax.plot(pred_X, pred_Y, c = "blue", linestyle = '-.', \

linewidth = 4, label = '线性拟合')

将模型绘制在图上

ax.text(1902, 20, '温度 = ' + str(round(Beta0, 2)) +' + ' \

  • str(round(Beta1, 4)) + ' * 年份', fontsize = 16)

ax.set_title('平均空气温度测量', fontsize = 16)

使刻度包括第一个和最后一个年份

tick_years = [1902] + list(range(1910, 2011, 10))

ax.set_xlabel('年份', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)C)', fontsize = 14)

ax.set_ylim(15, 21)

ax.set_xticks(tick_years)

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出将如下所示:

图 3.52:使用 scikit-learn 接口进行线性拟合的梯度下降结果

图 3.52:使用 scikit-learn 接口进行线性拟合的梯度下降结果

注意

要访问此特定部分的源代码,请参考 https://packt.live/2zWIadm。

您也可以在 https://packt.live/3eqhroj 上在线运行此示例。您必须执行整个 Notebook 才能得到期望的结果。

将此图与使用梯度下降手动实现构建的图进行比较。注意相似之处:这让我们确信两种梯度下降实现都是正确的。然而,现在我们可以利用 scikit-learn 实现 SGD 的全部功能。对于更复杂的问题,这可能至关重要。例如,您可能已经注意到 SGDRegressor 支持正则化方法,但我们没有使用它们。一些正则化方法向成本函数方程添加调整,以对相对较大的参数值(使参数变小的因子)施加惩罚。有其他方法专门适用于某些模型(例如,人工神经网络有几种额外的正则化方法可供使用)。正则化的一个重要用途是减少过拟合,尽管到目前为止我们使用的简单模型中不存在过拟合问题。有关正则化的进一步讨论可在第六章“集成建模”中找到。

活动 3.05:梯度下降

在此活动中,我们将实现与“活动 3.02: 使用最小二乘法进行线性回归”相同的模型,但我们将使用梯度下降过程。

要执行的步骤如下:

导入模块和类;在本例中:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

from sklearn.metrics import r2_score

from sklearn.linear_model import SGDRegressor

加载数据(austin_weather.csv),并预处理以创建 Day_of_Year 列并切片一整年(2015 年)。

创建经过缩放的 X 和 Y 数据,每种情况都在 0 和 1 之间进行缩放。

使用 SGDRegressor 实例化模型。记得设置 NumPy 的 random.seed() 方法。

拟合模型。

提取经过重新缩放的模型系数 Theta0 和 Theta1,并将它们打印出来。

使用缩放数据生成预测,使用 r2_score 方法获取拟合的 r2 值,然后打印出 r2。

重新缩放预测值以用于绘图。

创建可视化图表,显示原始数据、20 天移动平均线以及新的线性拟合线。在图表上包含模型方程式。

输出应如下所示:

图 3.53:优化的梯度下降预测趋势线

图 3.53:优化的梯度下降预测趋势线

注意

可通过此链接找到此活动的解决方案。

到目前为止,您应该已经熟悉使用 scikit-learn 的 SGDRegressor 接口以及理解梯度下降的基本过程。您还应该对何时使用 SGD 而不是标准线性回归有一些想法。

我们现在已经覆盖了数据平滑和简单线性回归,手动实现了梯度下降算法,并使用 scikit-learn 的 SGDRegressor 接口将梯度下降应用于线性回归。你已经看到如何使用虚拟变量、和多项式特征以及正弦/余弦特征进行一些特征工程。你可能会注意到,大部分代码是用来准备数据和可视化结果的。对于机器学习来说,这并不奇怪——理解和处理数据通常是最重要的任务,并且对成功至关重要。接下来我们将讨论回归的另一个应用——多元线性回归。

多元线性回归

我们已经涵盖了常规线性回归,以及带有多项式和其他项的线性回归,并考虑了用最小二乘法和梯度下降法训练它们。本章的这一部分考虑了一种额外的线性回归类型:多元线性回归,其中使用多个变量(或特征)来构建模型。事实上,我们已经在不明确提及的情况下使用了多元线性回归——当我们添加虚拟变量时,或者再添加正弦和余弦项时,我们实际上是在拟合多个 x 变量以预测单一的 y 变量。

让我们考虑一个简单的例子,说明多元线性回归如何自然成为建模的解决方案。假设你看到以下图表,它显示了一个假设的技术工人在长期职业生涯中的年度总收入。你可以看到,随着时间推移,他们的收入在增加,但数据中有一些异常的跳跃和斜率变化:

图 3.54:假设工人职业生涯中的收入

图 3.54:假设工人职业生涯中的收入

你可能猜测这位工人时常换工作,导致了收入的波动。然而,假设根据我们得到的数据,薪酬是每年每周平均工作小时数与小时工资的乘积。直观地讲,每年的总收入应当是总工作小时数与小时工资的乘积。我们可以构建一个多元线性模型,使用年份、工资和每周工作小时数来预测总收入,而不是简单的收入与年份的线性模型。在这个假设的情况下,使用多元线性回归与简单线性回归相比,结果如下图所示:

图 3.55:简单线性回归与多元线性回归在假设数据集上的比较

图 3.55:简单线性回归与多元线性回归在假设数据集上的比较

红色圆圈似乎比简单的蓝色线条更能满足模型需求。数据中仍然存在一些未被解释的特征——也许在某些年份有奖金或退休基金配对,或者与数据相关的其他因素我们尚不清楚。尽管如此,在这种情况下,使用多个 x 变量是合理的。

在继续之前,让我们先讨论一些细节。我们将使用 pandas 中的 corr() 方法,它接受一个 DataFrame 并计算所有变量之间的成对相关性。在接下来的练习中,我们将研究两个关键问题。首先,当执行多元回归时,如果某些变量之间高度相关,可能会导致模型出现问题。这个问题被称为多重共线性,它可能导致系数估计对数据或模型的微小变化不稳定。在极端情况下,如果一个变量实际上是其他变量的线性组合,模型可能变得奇异;在某些方法中,线性相关的变量的系数可能会返回为 Inf,或者出现其他错误。其次,我们还希望了解这些变量是否会对预测结果产生影响。让我们通过解决一个练习来更好地理解这一点。

练习 3.07: 多元线性回归

对于这个练习,我们将使用一个 UCI 数据集,它包含一个组合循环电厂的功率输出和几个可能的解释变量:

注意

原始数据和描述文件可从 https://archive.ics.uci.edu/ml/datasets/Combined+Cycle+Power+Plant 获得

或者,您也可以在我们的存储库中找到数据: https://packt.live/2Pu850C

加载模块和类;注意,我们在这里添加了 seaborn,以便在一些可视化中使用:

导入 pandas 为 pd

导入 numpy 为 np

导入 matplotlib.pyplot 为 plt

导入 seaborn 为 sns

来自 sklearn.linear_model 的导入 LinearRegression

加载并检查数据:

注意

以下代码片段中的三引号(""")用于表示多行代码注释的开始和结束。注释用于帮助解释代码中的特定逻辑。

"""

加载并检查数据

来自描述文件

(https://archive.ics.uci.edu/ml/machine-learning-databases/00294/)

变量包括:

注意:描述文件中某些变量名称不正确

环境温度 (AT)

环境压力 (AP)

相对湿度 (RH)

排气真空 (V)

因变量是

每小时净电能输出 (PE)

"""

power_data = pd.read_csv\

('../Datasets/combined_cycle_power_plant.csv')

print(power_data.shape)

print(power_data.head())

missings = power_data.isnull().sum()

print(missings)

结果应如下所示:

图 3.56: 组合动力循环数据集没有缺失值

图 3.56: 组合动力循环数据集没有缺失值

由于我们之前没有使用过这个数据,让我们进行一些快速的 EDA(探索性数据分析)。首先,我们将查看所有变量之间的相关性:

"""

快速 EDA

相关性分析

"""

corr = power_data.corr()

用于 seaborn 中热图的掩码

mask = np.ones((power_data.shape[1], power_data.shape[1]))

mask = [[1 if j< i else 0 \

for j in range(corr.shape[0])] \

for i in range(corr.shape[1])]

fig, ax = plt.subplots(figsize = (10, 7))

"""

绘制相关矩阵的热图

隐藏上三角形(重复项)

"""

sns.heatmap(corr, cmap = 'jet_r', square = True, linewidths = 0.5, \

center = 0, annot = True, mask = mask, \

annot_kws = {"size" : 12}, \

xticklabels = power_data.columns, \

yticklabels = power_data.columns)

plt.show()

该图表应如下所示:

图 3.57:变量相关性图表

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-JMMJK0S2.jpg)

图 3.57:变量相关性图表

在前面的数据中,x 变量之间最强的相关性是 V 和 AT 之间的 0.84。因此,这里应该没有多重共线性问题。

我们希望能够显示出 x 变量会影响我们试图预测的目标变量 PE。在图表的最后一行,我们可以看到 PE 与所有其他变量之间有显著的相关性,这是它们在模型中都将是有价值的一个良好指示。如果特征非常多,我们可能会考虑删除一些在减少模型噪音方面不太重要的变量。然而,这些相关系数只是成对的,即使我们看到低相关性,也需要更多的工作来证明删除某个变量的合理性。

关于可视化,我们使用了 seaborn 的热图功能来生成图表。由于相关值是对称的,比如 AT 和 V 之间的相关性与 V 和 AT 之间的相关性是相同的。因此,网格的右上三角形会与左下三角形镜像,所以热图方法提供了一种方式来隐藏我们想要忽略的方块。我们通过 mask 变量实现这一点,mask 是一个与相关矩阵形状相同的矩阵,并隐藏任何在 mask 中对应为 False 值的方块。我们使用了嵌套的列表推导式来将 False 值(整数 0)放入 mask 中。

最后,请注意对角线上的值都是 1;根据定义,一个变量与自身的相关性是完美的。你可能会看到这些方块用于绘制变量分布或其他有价值的信息,同时,右上(三角形)或左下(三角形)部分的方块可以用于额外的信息,例如下一步中的图表。

使用 seaborn 的 pairplot 来可视化所有变量之间的成对关系。这些信息可以补充相关图,并且如前所述,有时会将其与相关图合并成一个网格:

(2) 查看成对的变量关系

plot_grid = sns.pairplot(power_data)

结果应该如下所示:

图 3.58:Seaborn 数据的 pairplot 图

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-J58CFLEM.jpg)

图 3.58:Seaborn 数据的 pairplot 图

默认情况下,这个图表展示了每个变量与其他变量之间的散点图,并且每个变量沿对角线的分布。请注意,右上三角形是左下三角形的镜像,且每个图表的坐标轴是翻转的。

沿着对角线,我们可以看到所有变量的分布。我们可以看到 RH 变量向左偏斜;我们可以考虑对该列应用变换,例如 numpy.log(power_data['RH'])或 numpy.sqrt(power_data['RH'])。但在本次练习中,我们将保持原样。

从这个图表中我们还可以观察到底部行的散点图;注意,AT 与 PE 的负相关性最大,AT 与 PE 的关系呈现出明显的负趋势,这在直观上是有意义的。向右移动时,相关性变得较弱,这与散点图一致。在第三张图中,PE 与 AP 的关系,我们可以看到一些积极相关的迹象。

现在,我们将数据结构化以用于线性回归模型,拟合模型,并获得预测值和 r2 值。这与我们在之前的练习中做的方式相同:

结构化数据

X_train = power_data.drop('PE', axis = 1)

Y_train = power_data['PE']

拟合模型

model = LinearRegression()

model.fit(X_train, Y_train)

获取预测值

Y_pred = model.predict(X_train)

r2 = model.score(X_train, Y_train)

print('模型系数 ' + str(model.coef_))

print('r2 值 ' + str(round(r2, 3)))

输出结果应如下所示:

图 3.59:多重线性回归拟合结果

图 3.59:多重线性回归拟合结果

相对较高的 r2 值是模型在预测 PE 时可能有效的良好信号。

对于多重线性回归,我们无法像绘制预测变量与 x 变量的图那样轻松地可视化结果。然而,有一个非常强大的可视化方法几乎可以在任何情况下使用,那就是将预测值与真实值绘制在一起。为了便于解释,最好使这个图对称(x 和 y 的轴范围应相同)——完美的预测值应沿对角线分布。我们添加一条对角线以帮助视觉解释:

fig, ax = plt.subplots(figsize=(10, 10))

设置一些限制

PE_range = max(power_data.PE) - min(power_data.PE)

plot_range = [min(power_data.PE) - 0.05 * PE_range, \

max(power_data.PE) + 0.05 * PE_range]

ax.scatter(Y_train, Y_pred)

ax.set_xlim(plot_range)

ax.set_ylim(plot_range)

ax.set_xlabel('实际 PE 值', fontsize = 14)

ax.set_ylabel('预测 PE 值', fontsize = 14)

ax.plot(plot_range, plot_range, c = "black")

plt.show()

结果如下:

图 3.60:多重线性回归的预测值与实际 PE 值对比

图 3.60:多重线性回归的预测值与实际 PE 值对比

图 3.60 显示,大多数预测结果都位于对角线上。也有一些值的预测显著高于大多数,我们可能需要对这些特定点进行进一步调查。此外,在最高值处,预测通常偏低,这可能表明模型中缺少一些特征工程或其他数据。回想一下,我们没有对任何变量进行变换;在这里尝试进行变换可能会有所帮助,看是否能改进结果。我们在这里不会进一步探讨这个问题。

注意

要访问此特定部分的源代码,请参考 packt.live/2CwfIzZ

你也可以在线运行此示例,网址是 packt.live/37UzZuK。你必须执行整个 Notebook 才能获得预期的结果。

我们已经看到,使用多元线性回归是工具集中的一个强大补充,并且是我们已掌握的方法的一个非常简单的扩展。事实上,多元线性回归在回归问题中,往往能够与更复杂的模型表现得一样好甚至更好。尽管我们在这里没有讨论,但使用多元线性回归与人工神经网络相比的一个好处是,多元线性回归模型的系数可以解释为每个 x 变量对预测变量的影响估计;在某些情况下,这种解释非常有价值。

总结

在本章中,我们迈出了构建机器学习模型并使用带标签的数据集进行预测的第一步。我们通过查看多种构建线性模型的方式开始了分析,首先使用精确的最小二乘法,这是在使用现有计算机内存处理小量数据时非常有效的方法。线性模型的性能可以通过使用虚拟变量来提高,这些虚拟变量是由分类变量创建的,能够为模型添加额外的特征和背景。随后,我们使用带有多项式模型的线性回归分析来进一步提高性能,为数据集拟合更自然的曲线,并探讨了通过增加正弦和余弦序列作为预测变量的其他非线性特征工程。

作为显式线性回归的一个概括,我们实现了梯度下降算法。正如我们所指出的,梯度下降虽然不像最小二乘法那样精确(对于给定的迭代次数或轮次),但能够处理任意大的数据集和更多的变量。此外,使用广义梯度下降引入了许多其他参数,即所谓的超参数,我们作为数据科学家可以对其进行优化,从而提高模型性能。我们将进一步的模型优化研究推迟到第七章《模型评估》。

现在我们已经对线性回归有了充分的理解,在下一章中我们将深入研究自回归模型。

第五章:4. 自回归

概述

本章将教你如何实现自回归模型,作为一种基于过去值预测未来值的方法。在本章结束时,你将能够创建一个自回归模型,并使用自回归构建时间序列回归模型。你将完全掌握如何使用自回归模型对数据集进行建模,并预测未来的值。

引言

在上一章中,我们研究了用于构建线性回归模型的不同方法。我们学习了如何使用最小二乘法开发线性模型。我们利用虚拟变量来提高这些线性模型的性能。我们还通过多项式模型进行线性回归分析,以提升模型的表现。接下来,我们实现了梯度下降算法,能够轻松处理大数据集和大量变量。

在本章中,我们将开发自回归模型。自回归是一种特殊类型的回归,可以根据数据集中的前期数据经验预测未来的值。

自回归模型

自回归模型是经典的或“标准”的建模方法,适用于时间序列数据(即任何随时间变化的数据),并且可以补充之前讨论的线性回归技术。自回归模型常用于经济学和金融行业的预测,因为它们在单变量时间序列(即除了时间之外没有其他 x 变量)以及非常大的数据集(如流数据或高频传感器数据)中非常有效,后者在处理时可能会遇到内存或性能问题。“自”(auto)部分指的是这些模型利用时间序列与自身过去的相关性,因此称为自回归。此外,许多系统没有相关的因果模型——时间序列数据被认为是随机的。例如,股票价格随时间的变化。尽管已经进行并且仍在进行许多尝试,旨在开发股票市场行为的预测性因果模型,但成功的案例寥寥无几。因此,我们可以将某一股票符号的价格随时间变化视为随机序列,并使用自回归方法进行建模尝试。

注意

为了说明自回归模型,我们将使用 1986 年至 2018 年间的标准普尔 500 日收盘价,这些数据可以在与本书相关的仓库中找到(packt.live/2w3ZkDw)。

原始数据集可以在这里找到:

www.kaggle.com/pdquant/sp500-daily-19862018

该数据的图形表示如下图所示:

图 4.1:标准普尔 500 日收盘价

图 4.1:标准普尔 500 日收盘价

自回归模型背后的主要原理是,给定足够的历史观察数据,可以对未来做出合理的预测;也就是说,我们本质上是在构建一个模型,将数据集作为自我回归,使用过去的值作为预测因子。选择自回归模型的一个关键因素是未来值与过去值在特定滞后时间点之间有足够的相关性。滞后时间指的是模型使用的数据回溯多长时间来预测未来。

练习 4.01:创建自回归模型

在这个练习中,我们将使用自回归方法,尝试预测未来一年内标准普尔 500 指数的收盘价:

注意

本练习适用于较早版本的 pandas,请确保使用以下命令将 pandas 降级:

pip install pandas==0.24.2

导入必要的包和类。在这个练习中,我们介绍了 statsmodels 包,它包括广泛的统计和建模函数,包括自回归。如果你之前没有从终端提示符安装 statsmodels,请使用以下命令:

conda install -c anaconda statsmodels

如果你没有使用 Anaconda(或 Miniconda),而是通过 pip 安装,请使用以下命令:

pip install -U statsmodels

一旦在系统上安装了 statsmodels,加载以下内容:

import pandas as pd

import numpy as np

from statsmodels.tsa.ar_model import AR

from statsmodels.graphics.tsaplots import plot_acf

import matplotlib.pyplot as plt

加载标准普尔 500 数据(spx.csv),并将日期列转换为 datetime 数据类型:

df = pd.read_csv('../Datasets/spx.csv')

df['date'] = pd.to_datetime(df['date'])

print(df.head())

print(df.tail())

我们将得到以下输出:

图 4.2:标准普尔 500 历史数据

图 4.2:标准普尔 500 历史数据

如果你仔细查看图 4.2 中的数据,你可能会发现一些数据缺失——例如,1986 年 1 月 4 日和 1986 年 1 月 5 日没有数据。这些是市场关闭的周末日期。自回归模型,特别是包含大量数据的模型,对于缺失值通常不敏感,原因有至少两个。首先,由于模型一次前进一个时间段,并且在高阶模型中使用多个过去的值,预测对于缺失值的敏感度比基于单一滞后值的模型要低。其次,像这里的情况一样,大部分缺失值是周期性的(星期六每 7 天重复一次,星期天每 7 天重复一次,依此类推),如果我们有大量数据,模型会自动考虑这些缺失的日期。然而,正如我们将看到的,预测超出现有数据的时间范围时,自回归模型的预测不确定性会变得很大。

将原始数据集与日期数据类型进行比较绘制:

fig, ax = plt.subplots(figsize = (10, 7))

ax.plot(df.date, df.close)

ax.set_title('标准普尔 500 每日收盘价', fontsize = 16)

ax.set_ylabel('价格 ($)', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

plt.show()

输出将如下所示:

图 4.3:标准普尔 500 指数收盘价的图

图 4.3:标准普尔 500 指数收盘价的图

在构建自回归模型之前,我们应该首先检查模型是否能够用于自回归。正如我们之前提到的,自回归模型的成功依赖于能否利用过去的值通过线性模型预测未来的值。这意味着未来的值应该与过去的值有很强的相关性。我们可以通过使用 statsmodelsplot_acf 函数(绘制自相关函数)来进行检查。我们之前提到过,回顾过去多长时间被称为滞后;我们将覆盖 plot_acf 的默认最大值,并绘制从 0 天到 4,000 天的滞后:

max_lag = 4000

fig, ax = plt.subplots(figsize = (10, 7))

acf_plot = plot_acf(x = df.close, ax = ax, lags = max_lag, \

use_vlines = False, alpha = 0.9, \

title = '标准普尔 500 指数的自相关与滞后关系')

ax.grid(True)

ax.text(1000, 0.01, '90% 置信区间')

ax.set_xlabel('滞后', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

plt.show()

结果应该如下所示:

图 4.4:标准普尔 500 指数收盘价与滞后(天数)的自相关图

图 4.4:标准普尔 500 指数收盘价与滞后(天数)的自相关图

我们可以这样理解这张图。首先,根据定义,在滞后为 0 时,序列与自身完全相关,因此自相关函数(ACF)值为 1.0。然后,我们可以看到,随着滞后的增加,序列的相关性逐渐减弱,这意味着越往后的数据点与我们预测的值相关性越小。这是典型的随机或斯托卡斯蒂克序列(注意周期性序列在 ACF 图中会有峰值和谷值—我们稍后会看到这一点)。此外,通过在函数调用中选择该值,我们绘制了一个 90%的置信区间,并用蓝色阴影标示。这个区间的意义是,任何超出该区间的滞后都被认为在统计上显著—换句话说,相关性在统计上是有效的。为了构建自回归模型,我们必须有超出置信区间的 ACF 值才能成功。在这种情况下,从 0 到若干天,我们有相当高的相关性,这可以在模型中使用(稍后会详细讨论)。最后,我们可以看到,在大约 2,750 天以上的滞后下,相关性为负—也就是说,过去的值预测了与未来相反的结果。然而,在这种情况下,那些长期的负滞后并不非常显著。

为了对 ACF 结果有一些直观的理解,我们选择了一个相对较短的滞后期 100 天,并将原始数据和滞后数据绘制在同一张图上。我们可以通过使用 pandas.shift()函数来做到这一点:

spx_shift_100 = df.copy()

spx_shift_100['close'] = df.close.shift(100)

fix, ax = plt.subplots(figsize = (10, 7))

ax.plot(df.date, df.close, c = "blue")

ax.plot(spx_shift_100.date, spx_shift_100.close, c = "red")

plt.show()

输出将如下所示:

图 4.5:标准普尔 500 指数收盘价(蓝色)和滞后 100 天的收盘价(红色)

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-C5NFMM99.jpg)

图 4.5:标准普尔 500 指数收盘价(蓝色)和滞后 100 天的收盘价(红色)

在图 4.5 中,红色线条表示 100 天前的值,与蓝色线条(表示给定日期的实际值)进行对比。我们看到,在值增加的阶段,过去的值低于实际值,而在值减少的阶段,情况则相反。这是直观上合理的。重要的是,在大部分时间段内,两条曲线之间的垂直间距看起来大致恒定。这意味着,直观上,过去与现在的关系大致相似。如果你仔细思考这些曲线,你还会看到自回归模型的局限性——预测值总是看起来像最近的历史,因此当情况发生变化时,预测会变得不准确,直到模型“追赶上”新的行为。

还有一种方式可以帮助我们可视化正在分析的相关性。在多元线性回归的案例中,我们引入了一个图表,绘制了预测值与实际值的对比;完美的预测沿对角线分布。同样,如果我们不再绘制滞后值和实际值相对于时间的图,而是将滞后值与实际值作图,我们会看到什么样的效果呢?

print(spx_shift_100.head(), '\n', spx_shift_100.tail())

fig, ax = plt.subplots(figsize = (7, 7))

ax.scatter(df.loc[100:, 'close'], spx_shift_100.loc[100:, 'close'])

ax.set_xlim(0, 3000)

ax.set_ylim(0, 3000)

plt.show()

输出将如下所示:

日期 收盘价

0 1986-01-02 NaN

1 1986-01-03 NaN

2 1986-01-06 NaN

3 1986-01-07 NaN

4 1986-01-08 NaN

日期 收盘价

8187 2018-06-25 2823.81

8188 2018-06-26 2821.98

8189 2018-06-27 2762.13

8190 2018-06-28 2648.94

8191 2018-06-29 2695.14

图表将如下所示:

图 4.6:滞后 100 天的标准普尔收盘值与实际值的比较

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-RLYG1WTV.jpg)

图 4.6:滞后 100 天的标准普尔收盘价与实际值的比较

图 4.6 显示,大多数滞后 100 天的值沿着对角线分布,这意味着滞后值与实际值之间的关系在所有实际值中都是相似的。

我们可以在不同的滞后时间范围内创建类似图 4.6 的图表,尝试理解哪些滞后时间对模型有用,哪些没有用。在此之前,了解图 4.6 中的自相关函数(ACF)值将是有用的,这样我们可以将滞后图中的内容与相关函数值关联起来。statsmodels 的 plot_acf 函数基于一个底层的 numpy 函数——correlate。我们可以使用它来获取图 4.6 中显示的值:

"""

statsmodels 的 plot_acf 基于 numpy 的 correlate 函数

函数,因此我们可以生成实际的值来

插图,因此我们可以稍后为一些图表加标签

acf 图的标准表示中,值位于

lag 0 == 1;correlate 函数返回未缩放的

这些值使我们得到第一个值,用于缩放到 1

函数中要测试的值必须具有

从未移动序列中减去的均值

两个序列

"""

corr0 = np.correlate(df.close[0: ] - df.close.mean(), \

df.close[0: ] - df.close.mean(), \

mode = 'valid')

corrs = [np.correlate(df.close[:(df.close.shape[0] - i)] \

  • df.close.mean(), df.close[i: ]

  • df.close.mean(), mode = 'valid')

for i in range(max_lag)] / corr0

请注意,我们从相关函数中使用的每个值中减去了基础序列(df.close)的均值。这是一个数学上的微妙之处,目的是与 plot_act 函数的输出保持一致。

现在,与其在不同滞后值下手动创建许多图表,不如创建一个函数来生成一个图表网格,我们可以用不同的滞后范围、图表数量等来使用它。我们将传递 df.close 序列给函数,同时传递前面的 corrs 值,并设置控制图表的参数:

"""

绘制一定范围的实用函数

描述自相关的图

"""

def plot_lag_grid(series, corrs, axis_min, axis_max, \

num_plots, total_lag, n_rows, n_cols):

lag_step = int(total_lag / num_plots)

fig = plt.figure(figsize = (18, 16))

for i in range(num_plots):

corr = corrs[lag_step * i]

ax = fig.add_subplot(n_rows, n_cols, i + 1)

ax.scatter(series, series.shift(lag_step * i))

ax.set_xlim(axis_min, axis_max)

ax.set_ylim(axis_min, axis_max)

ax.set_title('lag = ' + str(lag_step * i))

ax.text(axis_min + 0.05 * (axis_max - axis_min), \

axis_max - 0.05 * (axis_max - axis_min), \

'correlation = ' + str(round(corr[0], 3)))

fig.tight_layout()

plt.show()

我们现在已经准备好深入理解相关性与滞后图之间的关系。我们将调用这个函数:

"""

创建一个网格,查看数据在增大的滞后期下的相关性

滞后与原始数据的相关性

“完美”的相关性将表现为对角线

离直线越远,相关性越差

"""

plot_lag_grid(df.close, corrs, df.close.min(), df.close.max(), \

num_plots = 16, total_lag = 480, \

n_rows = 4, n_cols = 4)

这将产生如下结果:

图 4.7:在不同值下的滞后图

图 4.7:在不同值下的滞后图

在图 4.7 中,我们可以看到滞后图的外观逐渐恶化,这与每个图中显示的 ACF 函数值直接相关。这让我们得出一个结论:尝试使用更长的滞后期将会给模型增加噪声,到滞后 60 时噪声较大,而滞后 30 时噪声较小。现在,我们可以使用另一个 statsmodels 函数来开发模型,并查看它与我们想法的契合程度。

statsmodels 的 AR 函数与相关的 fit 方法一起,构建了一个自回归模型。使用默认设置时,它将确定最大滞后期,返回从滞后期 1 到最大滞后期的所有参数,并允许我们对现有数据范围内和未来进行预测:

"""

statsmodels AR 函数构建了一个自回归模型

使用所有默认设置时,它将确定最大滞后期

并提供所有模型系数

"""

model = AR(df.close)

model_fit = model.fit()

现在,模型拟合包含了所有模型信息

max_lag = model_fit.k_ar

"""

请注意,使用默认设置时,最大滞后期为

计算公式为 round(12*(nobs/100.)**(1/4.))

请访问 https://www.statsmodels.org/devel/generated/statsmodels.tsa.ar_model.AR.fit.html#statsmodels.tsa.ar_model.AR.fit

"""

print('最大滞后期:' + str(max_lag))

print('系数:\n' + str(model_fit.params))

输出结果如下:

图 4.8:来自 statsmodels AR 函数的滞后系数

图 4.8:来自 statsmodels AR 函数的滞后系数

请注意,对于每个权重和一个常数,模型有 36 个系数——该函数确定了在模型中使用的最大滞后期为 36 天。预测值生成和结果可视化非常简单:

我们想预测的未来时长

max_forecast = 365

从模型中生成预测值

pred_close = pd.DataFrame({'pred_close': \

model_fit.predict(start = max_lag, \

end = df.shape[0] \

  • max_forecast - 1)})

附加日期以便可视化

pred_close['date'] = df.loc[pred_close.index, 'date'].reindex()

pred_close.loc[(max(df.index) + 1):, 'date'] = \

pd.to_datetime([max(df.date) \

  • pd.Timedelta(days = i) \

for i in range(1, max_forecast + 1)])

"""

将预测结果与真实数据叠加进行可视化

以及对未来的外推

"""

fig, ax = plt.subplots(figsize = (10, 7))

ax.plot(df.date, df.close, c = "blue", linewidth = 4, \

label = '实际 SPX 收盘'

ax.plot(pred_close.loc[0 : len(df.close), 'date'], \

pred_close.loc[0 : len(df.close), 'pred_close'], \

c = "yellow", linewidth = 0.5, \

label = '预测 SPX 收盘'

ax.plot(pred_close.loc[len(df.close):, 'date'], \

pred_close.loc[len(df.close):, 'pred_close'], \

c = "red", linewidth = 2, label = '预测 SPX 收盘')

ax.set_xlabel('日期', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

ax.legend()

plt.show()

结果将如下所示:

图 4.9:S&P 500 收盘值、预测值和预测(未来)值来自自回归模型,滞后期为 36

图 4.9:来自自回归模型(滞后期为 36)的 S&P 500 收盘值、预测值和预测(未来)值

请注意,预测值在跟随数据集方面表现非常好,并且在数据集结束后,预测值相对线性。由于模型是从前一个样本的线性模型构建的,而且在 yt+1 之后,随着基于过去预测的预测值增多,每个预测都带有一定误差,因此预测的确定性会逐渐降低。

拟合效果相当好。我们可以像线性回归一样比较预测值与实际值之间的差异。由于预测值直到超过原始数据的最大滞后期才开始,因此我们需要处理这个问题——我们至少需要最大滞后期数量的历史值才能预测下一个值——在本例中是 36 个值。结果是我们需要对比数据集的索引进行偏移,这在进行线性回归时并不需要:

比较预测值与实际值

fig, ax = plt.subplots(figsize = (10, 7))

ax.scatter(df.loc[max_lag:(df.shape[0] - 1), 'close'], \

pred_close.loc[max_lag:(df.shape[0] - 1), 'pred_close'])

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_xlabel('SPX 实际值', fontsize = 14)

ax.set_ylabel('SPX 预测值', fontsize = 14)

plt.show()

这提供了如下图表:

图 4.10:预测的 S&P 500 收盘值与实际值的对比

图 4.10:预测的 S&P 500 收盘值与实际值的对比

在图 4.10 中,似乎所有值的预测都很好。然而,我们可以通过查看残差来深入分析,残差是实际值与预测值之间的差异:

使用与之前相同的方法计算残差,以考虑日期偏移:

fig, ax = plt.subplots(figsize = (10, 7))

residuals = pd.DataFrame({'date' : (df.loc[max_lag:\

(df.shape[0] - 1), 'date']),

'residual' : df.loc[max_lag:\

(df.shape[0] - 1), 'close'] \

  • pred_close.loc\

[max_lag:(df.shape[0] - 1), \

'pred_close']})

ax.scatter(residuals.date, residuals.residual)

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_xlabel('日期', fontsize = 14)

ax.set_ylabel('残差 (' + r'\(SPX_{act} - SPX_{pred}\)' \

  • ')', fontsize = 14)

plt.show()

这产生了以下图表:

图 4.11:自回归模型的残差值与时间的关系 S&P 500 收盘价的模型

图 4.11:S&P 500 收盘价的自回归模型残差值与时间的关系

图 4.11 显示,残差均匀地分布在 0 附近,这意味着模型的偏差最小,而且它们似乎随时间有所增加。然而,这种特性不一定意味着存在问题——最好将数据视为实际值的百分比——直观地说,对于一个同样准确的模型,随着值变大,残差会更大。我们可以将数据转换为百分比,并查看这一点。

通过将残差除以实际值并乘以 100 来计算残差的百分比。请注意,如果实际值接近零,百分比值可能存在问题,但在这种情况下这不是问题:

fig, ax = plt.subplots(figsize = (10, 7))

pct_residuals = pd.DataFrame({'date' : residuals.date, \

'pct_residual' : 100 \

  • residuals.residual \

/ df.loc[max_lag:(df.shape[0] - 1), \

'close']})

ax.scatter(pct_residuals.date, pct_residuals.pct_residual)

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_xlabel('日期', fontsize = 14)

ax.set_ylabel('%残差 100 *(' \

  • r'\(SPX_{act} - SPX_{pred}\)' + ') / ' \

  • r'\(SPX_{act}\)', fontsize = 14)

plt.show()

输出将如下所示:

图 4.12:实际值与时间的残差百分比关系

图 4.12:实际值与时间的残差百分比关系

注意

若要访问此特定部分的源代码,请参阅 https://packt.live/3eAi6DG。

你也可以在线运行这个示例,网址为 https://packt.live/2Z0uEh4。你必须执行整个 Notebook 才能得到预期的结果。

现在,既然练习已经顺利完成,升级 pandas 的版本以继续顺利运行本书剩余部分的练习和活动。要升级 pandas,请运行:

pip install pandas==1.0.3

我们现在看到,整个期间的百分比误差非常相似,只有少数几个期间误差增大。除去 1987 年的一个异常值,大多数值都在 10%以内,绝大多数值都在 5%以内。我们可以得出结论,这似乎是一个相当不错的模型。然而,在看到未来结果之前,我们应该保留判断——即超出我们用于模型的数据的预测结果。我们在图 4.12 中看到,数据结束后的未来预测呈现出相当线性的上升趋势。

我们将把获取最新的标准普尔 500 指数收盘数据并将其与此模型的 1 年预测数据进行比较的任务留给你。我们还必须强调,完全验证这样的模型需要将训练数据、验证数据和测试数据分开,并执行如交叉验证等测试,这将在第六章“集成建模”中详细讲解。作为一个提示,数据后的全年最差误差约为 20%,平均误差为 1.4%,而预测期结束时的误差为 0.8%。我们必须强调,预测股市是非常具有挑战性的,预测期能够保持相对均匀的正增长可能是偶然的。

通过使用自回归模型进行此练习,我们可以看到,即使在缺失数据的情况下,使用这些模型也具有显著的预测能力。对 S&P 500 数据集使用的自回归模型能够有效地在观察样本范围内提供预测。然而,在此范围之外,当预测没有采样数据的未来值时,预测能力可能会有所限制。在这种特定情况下,未来的预测似乎是合理的。接下来,我们将进行一个可能对这种模型类型更具挑战性的练习。

活动 4.01: 基于周期数据的自回归模型

在此活动中,我们将使用自回归模型来拟合奥斯汀天气数据集并预测未来值。该数据与股票市场数据具有不同的特征,将展示应用自回归模型时面临的一些挑战。

要执行的步骤如下:

导入所需的包和类。与股票市场练习一样,我们需要 pandas、numpy、来自 statsmodels.tsa.ar_model 的 AR 函数、来自 statsmodels.graphics.tsaplots 的 plot_acf 函数,以及当然的 matplotlib.pyplot。

加载奥斯汀天气数据(austin_weather.csv),并将日期列转换为 datetime 格式,如之前所示。

绘制完整的平均温度值集(df.TempAvgF),横坐标为日期。

输出应如下所示:

图 4.13: 奥斯汀温度数据的多年绘图

图 4.13: 奥斯汀温度数据的多年绘图

构建自相关图,以查看平均温度是否可以与自回归模型一起使用。考虑此图与练习 4.01“创建自回归模型”之间的差异,并思考其原因。

绘图应如下所示:

图 4.14: 自相关与滞后(天数)

图 4.14: 自相关与滞后(天数)

使用 numpy.correlate()函数提取实际的 ACF 值,如练习 4.01“创建自回归模型”中所示。

使用相同的 plot_lag_grid 函数来研究不同相关值下的相关性与滞后图。考虑到原始数据显然在大约 365 天的周期上重复,并且自相关模型在如此长的滞后期下可能不太有效。查看短期和长期滞后,并理解数据。

短滞后期的输出将如下所示:

图 4.15: 短滞后期的滞后图

图 4.15: 短滞后期的滞后图

较长滞后期的输出将如下所示:

图 4.16: 较长滞后期的滞后图

图 4.16: 较长滞后期的滞后图

使用 statsmodelsAR 函数和 model.fit()方法对数据进行建模。从 model.fit()方法中获取最大模型滞后期。打印系数。默认情况下使用了多少项?

使用 365 天的最大预测期(问:为什么这是合理的?)并生成模型预测结果。使用之前相同的方法,将正确的日期与预测结果匹配,这样我们就能将它们与原始数据一起可视化。

绘制预测结果(包括 365 天的预测),以及原始数据集。

结果应如下所示:

图 4.17:奥斯汀温度数据、内数据预测与外数据预测

图 4.17:奥斯汀温度数据、内数据预测与外数据预测

图 4.18:数据末尾附近预测的详细信息

图 4.18:数据末尾附近预测的详细信息

我们可以从图 4.18 看出,正如股市案例中所示,用于拟合模型的数据范围内的预测效果良好。然而,未来的预测似乎并不那么理想。它们在合理的趋势上起步,但随后趋于一个明显不准确的值,持续到部分预测期。这是自回归模型局限性的一个很好的例子;它们可能在短期内对难以预测的序列非常有效,但长期预测可能会出现显著误差。要在信任此类模型之前严格评估它们,唯一的方法是使用第六章中的集成建模方法。

注意事项

本活动的解答可以通过以下链接找到。

在这一部分,我们使用了自相关、滞后图和自回归等多种工具,构建了一个时间序列的预测模型。这些模型构建速度快,对于单变量数据(只有时间作为 x 变量时)效果良好,且能提供较好的短期预测。

我们已经探讨了自回归模型,作为时间序列数据中线性回归模型的替代方案。自回归模型对于没有底层模型或明显预测变量的单变量时间序列非常有用。在经济或金融建模中,自回归广泛应用,尤其是当我们认为序列是随机的或是随机变量时。我们已经看到,在某些情况下,自回归模型可以非常强大,但在存在周期性或其他非恒定时间行为的情况下,这些模型可能受到限制。根本原因是,自回归模型是过去值的线性组合,因此未来的预测总是反映了近期的过去。通常,自回归模型最适合用于相对短期的预测,尽管“短期”的定义是相对的。

总结

在本章中,我们研究了自回归模型的使用,该模型根据序列中先前数据的时间行为预测未来值。通过使用自回归建模,我们能够准确地建模 1986 年到 2018 年期间以及未来一年的标准普尔 500 指数收盘价。另一方面,自回归模型在预测德克萨斯州奥斯汀的年度周期性温度数据时,表现似乎更为有限。

既然我们已经有了回归问题的经验,我们将在下一章转向分类问题。

第六章:5. 分类技术

概述

本章介绍了分类问题,使用线性回归和逻辑回归、K 最近邻和决策树进行分类。你还将简要了解人工神经网络作为一种分类技术。

到本章结束时,你将能够实现逻辑回归并解释如何使用它将数据分类到特定的组或类别中。你还将能够使用 K 最近邻算法进行分类,使用决策树进行数据分类,包括 ID3 算法。此外,你将能够识别数据中的熵,并解释决策树(如 ID3)如何通过减少熵来进行分类。

介绍

在前几章中,我们通过回归技术开始了有监督机器学习之旅,预测给定输入数据集上的连续变量输出。现在我们将转向另一类机器学习问题:分类。回想一下,分类任务的目的是将给定的输入数据分类到两个或更多指定的类别中。

因此,回归是估计给定输入数据的连续值的任务(例如,根据房子的地点和尺寸估计房价),而分类则是预测给定输入数据的(离散)标签。例如,一个著名的机器学习分类任务是电子邮件的垃圾邮件检测,任务是预测给定的电子邮件是否为垃圾邮件或非垃圾邮件。在这里,垃圾邮件和非垃圾邮件是这个任务的标签,输入数据是电子邮件,或者更准确地说,是电子邮件中不同字段的文本数据,如主题、正文和收件人。文本数据将经过预处理,转化为数值特征,以便用于分类模型。因为这个任务只有两个标签,所以它被称为二分类任务。如果分类任务中有两个以上的标签,则称为多类分类任务。

有各种类型的分类模型,采用不同的学习算法,每种方法都有其优缺点。但本质上,所有模型都使用带标签的数据集进行训练,训练完成后,可以对未标记的数据样本进行标签预测。在本章中,我们将扩展第三章《线性回归》和第四章《自回归》中学到的概念,并将其应用于带有类别标签的数据集,而不是连续值作为输出。我们将讨论一些著名的分类模型,并将其应用于一些示例标签数据集。

普通最小二乘法作为分类器

在上一章中,我们将最小二乘法(OLS)作为线性回归应用于预测连续变量输出,但它也可以用于预测一组数据属于哪个类别。基于最小二乘法的分类器虽然不如本章后续介绍的其他分类器强大,但它们在理解分类过程时尤其有用。回顾一下,基于最小二乘法的分类器是一个非概率性的线性二分类器。它之所以是非概率性的,是因为它不会像逻辑回归那样对预测生成置信度。它是一个线性分类器,因为它与其参数/系数之间具有线性关系。

现在,假设我们有一个虚构的数据集,其中包含两个不同的组,X 和 O,如图 5.1 所示。我们可以通过首先使用最小二乘法线性回归来拟合一条直线方程,从而构造一个线性分类器。对于任何位于直线之上的值,将预测为 X 类,而对于任何位于直线之下的值,将预测为 O 类。任何能够被直线分开的数据集都被称为线性可分的(如我们的例子所示),这是机器学习问题中一个重要的数据类型子集。在这种情况下,直线将被称为决策边界。更一般地说,决策边界被定义为将数据分开的超平面。在这种情况下,决策边界是线性的。也可能存在决策边界是非线性的情况。像我们例子中的数据集,可以通过线性分类器(如基于 OLS 的分类器)或具有线性核的支持向量机(SVM)进行学习。

然而,这并不意味着线性模型只能有线性的决策边界。线性分类器/模型是指与模型的参数/权重(β)具有线性关系的模型,但不一定与输入(x)具有线性关系。根据输入的不同,线性模型可能会有线性或非线性的决策边界。如前所述,线性模型的例子包括最小二乘法、支持向量机(SVM)和逻辑回归,而非线性模型的例子包括 KNN、随机森林、决策树和人工神经网络(ANN)。我们将在本章后续部分介绍更多这些模型:

图 5.1:最小二乘法作为分类器

图 5.1:最小二乘法作为分类器

练习 5.01:最小二乘法作为分类器

这个练习包含了一个使用最小二乘法(OLS)作为分类器的设计示例。在本练习中,我们将使用一个完全虚构的数据集,并测试最小二乘法模型作为分类器的效果。为了实现 OLS,我们将使用 sklearn 的 LinearRegression API。数据集由手动选择的 x 和 y 值组成,用于散点图,这些值大致被分为两组。该数据集专门为本练习设计,旨在演示如何将线性回归用作分类器,相关代码文件随书附赠,并且也可以在 GitHub 上找到,网址为 https://packt.live/3a7oAY8:

导入所需的包:

import matplotlib.pyplot as plt

import matplotlib.lines as mlines

import numpy as np

import pandas as pd

from sklearn.linear_model import LinearRegression

from sklearn.model_selection import train_test_split

将 linear_classifier.csv 数据集加载到 pandas DataFrame 中:

df = pd.read_csv('../Datasets/linear_classifier.csv')

df.head()

输出将如下所示:

图 5.2:前五行

图 5.2:前五行

浏览数据集时,每一行包含一组 x、y 坐标,以及与数据所属类别相关的标签,类别可以是叉号(x)或圆圈(o)。

绘制数据的散点图,并用对应的类别标签作为每个点的标记:

plt.figure(figsize=(10, 7))

for label, label_class in df.groupby('labels'):

plt.scatter(label_class.values[:,0], label_class.values[:,1], \

label=f'类 {label}', marker=label, c='k')

plt.legend()

plt.title("线性分类器");

我们将获得以下散点图:

图 5.3 线性分类器的散点图

图 5.3 线性分类器的散点图

为了公正地评估模型,我们应将训练数据集分为训练集和测试集。我们将在接下来的步骤中按照 60:40 的比例进行训练/测试数据的划分:

df_train, df_test = train_test_split(df.copy(), test_size=0.4, \

random_state=12)

使用上一章中的 scikit-learn LinearRegression API,将一个线性模型拟合到训练数据集的 x、y 坐标,并打印出线性方程:

拟合线性回归模型

model = LinearRegression()

model.fit(df_train.x.values.reshape((-1, 1)), \

df_train.y.values.reshape((-1, 1)))

打印出参数

print(f'y = {model.coef_[0][0]}x + {model.intercept_[0]}')

输出将如下所示:

y = 1.2718120805369124x + 8.865771812080538

注意

在本章的练习和活动中,由于随机化的原因,输出结果可能会与你得到的结果略有不同。

绘制拟合的趋势线于测试数据集上:

绘制趋势线

trend = model.predict(np.linspace(0, 10).reshape((-1, 1)))

plt.figure(figsize=(10, 7))

for label, label_class in df_test.groupby('labels'):

plt.scatter(label_class.values[:,0], label_class.values[:,1], \

label=f'类 {label}', marker=label, c='k')

plt.plot(np.linspace(0, 10), trend, c='k', label='趋势线')

plt.legend()

plt.title("线性分类器");

输出将如下所示:

图 5.4:带趋势线的散点图

图 5.4:带趋势线的散点图

使用拟合的趋势线后,可以应用分类器。对于测试数据集中的每一行,判断 x, y 点是否位于线性模型(或趋势线)之上或之下。如果点位于趋势线以下,模型预测为 o 类;如果位于趋势线以上,预测为 x 类。将这些值作为预测标签列:

做出预测

y_pred = model.predict(df_test.x.values.reshape((-1, 1)))

pred_labels = []

for _y, _y_pred in zip(df_test.y, y_pred):

if _y < _y_pred:

pred_labels.append('o')

else:

pred_labels.append('x')

df_test['预测标签'] = pred_labels

df_test.head()

输出将如下所示:

图 5.5:前五行数据

图 5.5:前五行数据

绘制具有相应真实标签的点。对于那些标签被正确预测的点,绘制相应的类别。对于那些错误预测的点,绘制一个菱形:

plt.figure(figsize=(10, 7))

for idx, label_class in df_test.iterrows():

if label_class.labels != label_class['预测标签']:

label = 'D'

s=70

else:

label = label_class.labels

s=50

plt.scatter(label_class.values[0], label_class.values[1], \

label=f'类 {label}', marker=label, c='k', s=s)

plt.plot(np.linspace(0, 10), trend, c='k', label='趋势线')

plt.title("线性分类器");

incorrect_class = mlines.Line2D([], [], color='k', marker='D', \

markersize=10, \

label='错误分类');

plt.legend(handles=[incorrect_class]);

输出将如下所示:

图 5.6:显示错误预测的散点图

图 5.6:显示错误预测的散点图

我们可以看到,在这个图中,线性分类器在这个完全虚构的数据集中做出了两个错误的预测,一个发生在 x = 1,另一个发生在 x = 3。

注意

要访问此特定部分的源代码,请参考 https://packt.live/3hT3Fwy。

你也可以在 https://packt.live/3fECHai 在线运行这个例子。你必须执行整个 Notebook 才能获得预期结果。

但如果我们的数据集不是线性可分的,且我们无法使用直线模型对数据进行分类,这种情况非常常见。此外,前面的方式并没有给出关于预测的置信度度量。为了应对这些挑战,我们转向其他分类方法,其中许多使用不同的模型,但过程在逻辑上是从我们简化的线性分类器模型延续下来的。

逻辑回归

逻辑回归模型,或称为 logit 模型,是一种线性模型,已被有效应用于多个领域的分类任务。回顾上一节中 OLS 模型的定义,逻辑回归模型以输入特征的线性组合作为输入。在本节中,我们将使用它来分类手写数字图像。在理解逻辑回归模型时,我们也迈出了理解一个非常强大的机器学习模型——人工神经网络的关键一步。那么,逻辑回归模型究竟是什么呢?就像由线性或直线函数组成的 OLS 模型一样,逻辑回归模型由标准的逻辑函数组成,在数学上,它看起来大致如下:

图 5.7:逻辑函数

图 5.7:逻辑函数

从实际的角度来看,经过训练后,这个函数返回输入信息属于特定类别或组的概率。在前面的方程中,x 是输入特征向量(一个数字数组,每个数字代表输入数据的一个特征),β1 是模型的参数向量,通过训练模型来学习,β0 是偏置项或偏置项(另一个参数),它帮助模型处理输入(x)和输出(y)之间的常数偏移值关系,p(x) 是数据样本 x 属于某个类别的输出概率。例如,如果我们有两个类别 A 和 B,那么 p(x) 是类别 A 的概率,1-p(x) 是类别 B 的概率。

那么,我们是如何得出逻辑函数的呢?逻辑回归模型的产生源于希望通过 x 的线性函数来建模数据点属于两个类别(A 和 B)中类别 A 的概率对数。该模型具有以下形式:

图 5.8:逻辑回归模型的逻辑函数

图 5.8:逻辑回归模型的逻辑函数

我们这里考虑的是二分类的情况,只有两个类别 A 和 B,尽管我们也可以通过一对多分类的技巧轻松扩展讨论到多分类问题。稍后会在后续章节中进一步讨论这个问题。但现在,由于我们知道只有两个类别,我们知道:

图 5.9:概率分布的求和

图 5.9:概率分布的求和

使用前面的两个方程式,我们可以得到:

图 5.10:用于二分类的逻辑回归模型的逻辑函数

图 5.10:用于二分类的逻辑回归模型的逻辑函数

现在,如果我们将类别 A 视为我们的目标类别,我们可以将 p(类别=A) 替换为 y(目标输出):

图 5.11:通过替换 p(类别=A)得到的逻辑回归模型的逻辑函数

图 5.11:通过替换 p(class=A) 得到的逻辑回归模型的逻辑函数

上述方程的左侧通常被称为对数几率(log-odds),因为它是几率比的对数,几率比是类 A 的概率与类 B 的概率之比。那么,为什么这很重要呢?对于像逻辑回归这样的线性模型,模型的对数几率与输入 x 之间的线性关系意味着模型的线性性。

通过稍微重新排列上述方程,我们得到逻辑函数:

图 5.12:逻辑函数

图 5.12:逻辑函数

请注意 e 的指数部分,即 β0 + β1x,并且这种关系是两个训练参数或权重 β0 和 β1,以及输入特征向量 x 的线性函数。如果我们假设 β0 = 0 和 β1 = 1,并在范围 (-6, 6) 内绘制逻辑函数,我们将得到如下结果:

图 5.13:逻辑函数曲线

图 5.13:逻辑函数曲线

注意

S 型曲线围绕点 x = -β0 中心,因此,如果 β0 不为零,曲线将不会围绕点 x = 0 中心,如前图所示。

查看图 5.13,我们注意到分类的一些重要方面。首先要注意的是,如果我们查看函数两端的 y 轴上的概率值,当 x = -6 时,值几乎为零,而当 x = 6 时,值接近一。虽然看起来这些值实际上是 0 和 1,但实际情况并非如此。逻辑函数在这些极限点趋近于零和一,只有当 x 处于正无穷或负无穷时,才会等于零或一。从实际角度看,这意味着逻辑函数永远不会返回大于或等于 1 的概率,或者小于或等于 0 的概率,这对于分类任务来说是完美的。无论如何,我们永远不会有大于 1 的概率,因为根据定义,概率为 1 表示事件发生的确定性。同样,我们也不能有小于 0 的概率,因为根据定义,概率为 0 表示事件不发生的确定性。逻辑函数趋近但永远不会等于 1 或 0,这意味着结果或分类中总会存在一定的不确定性。

关于逻辑函数的最后一个方面是,当 x = 0 时,概率为 0.5。如果我们得到这个结果,那么说明模型对对应类别的结果同样不确定;也就是说,它实际上并没有任何明确的判断。

注意

正确理解和解释分类模型(如逻辑回归)提供的概率信息非常重要。将这个概率分数视为给定训练数据的变异性后,输入信息属于某一特定类别的概率。一个常见的错误是将这个概率分数作为判断模型预测是否可靠的客观指标;然而,这并不一定是正确的。例如,一个模型可能提供 99.99%的概率,认为某些数据属于特定类别,但它仍然可能完全错误。

我们使用概率值的目的是选择分类器的预测类别。在模型输出概率与我们决定预测类别之间,存在一个概率阈值。我们需要决定一个阈值τ,它位于 0 和 1 之间,从而可以定义两个类别(例如,A 和 B),具体定义如下:

模型输出概率介于 0 和τ之间的数据样本属于类别 A。

模型输出概率介于τ和 1 之间的数据样本属于类别 B。

假设我们有一个模型,它用于预测某一组数据属于类别 A 还是类别 B,并且我们决定阈值为 0.5(这实际上是一个非常常见的选择)。如果逻辑回归模型返回的概率是 0.7,那么我们会将类别 B 作为模型的预测类别。如果概率只有 0.2,那么模型的预测类别将是类别 A。

练习 5.02: 逻辑回归作为分类器 – 二元分类器

对于本次练习,我们将使用著名的 MNIST 数据集样本(可通过 http://yann.lecun.com/exdb/mnist/ 或在 GitHub 上的 https://packt.live/3a7oAY8 获取)。该数据集包含从 0 到 9 的手写数字图像及其对应的标签。MNIST 数据集包括 60,000 个训练样本和 10,000 个测试样本,每个样本都是 28 x 28 像素的灰度图像。在本次练习中,我们将使用逻辑回归来构建一个分类器。我们首先要构建的是一个二元分类器,用于判断图像是手写数字 0 还是 1:

对于本次练习,我们需要导入一些依赖库。请执行以下导入语句:

import struct

import numpy as np

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.linear_model import LogisticRegression

我们还需要下载 MNIST 数据集。你只需做这一步一次,因此在此步骤之后,可以随意注释掉或删除这些代码块。请按照以下方式下载图像数据:

request = \

urllib.request.urlopen('http://yann.lecun.com/exdb'\

'/mnist/train-images-idx3-ubyte.gz')

with open('../Datasets/train-images-idx3-ubyte.gz', 'wb') as f:

f.write(request.read())

request = \

urllib.request.urlopen('http://yann.lecun.com/exdb'\

'/mnist/t10k-images-idx3-ubyte.gz')

with open('../Datasets/t10k-images-idx3-ubyte.gz', 'wb') as f:

f.write(request.read())

下载数据的相应标签:

request = \

urllib.request.urlopen('http://yann.lecun.com/exdb'\

'/mnist/train-labels-idx1-ubyte.gz')

with open('../Datasets/train-labels-idx1-ubyte.gz', 'wb') as f:

f.write(request.read())

request = \

urllib.request.urlopen('http://yann.lecun.com/exdb'\

'/mnist/t10k-labels-idx1-ubyte.gz')

with open('../Datasets/t10k-labels-idx1-ubyte.gz', 'wb') as f:

f.write(request.read())

一旦所有文件都成功下载,请使用以下命令解压缩本地目录中的文件(适用于 Windows):

!ls *.gz #!dir *.gz for windows

输出将如下所示:

t10k-images-idx3-ubyte.gz train-images-idx3-ubyte.gz

t10k-labels-idx1-ubyte.gz train-images-idx1-ubyte.gz

注意

对于 Linux 和 macOS,请使用 !ls *.gz 命令检查本地目录中的文件。

加载下载的数据。无需过多担心数据读取的具体细节,因为这些是针对 MNIST 数据集的特定内容:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read())).reshape((size, rows, cols))

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

一如既往,彻底理解数据至关重要,因此请创建训练样本中前 10 张图像的图像图。注意这些是灰度图像,并且对应的标签是 0 到 9 的数字:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出将如下所示:

图 5.14:训练图像

图 5.14:训练图像

由于初始分类器旨在分类数字 0 或数字 1 的图像,我们必须首先从数据集中选择这些样本:

samples_0_1 = np.where((labels == 0) | (labels == 1))[0]

images_0_1 = img[samples_0_1]

labels_0_1 = labels[samples_0_1]

samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1))

images_0_1_test = img_test[samples_0_1_test]\

.reshape((-1, rows * cols))

labels_0_1_test = labels_test[samples_0_1_test]

可视化从 0 选择的一个样本和从手写数字 1 中选取的另一个样本,以确保我们已正确分配数据。

这里是 0 的代码:

sample_0 = np.where((labels == 0))[0][0]

plt.imshow(img[sample_0], cmap='gray');

输出将如下所示:

图 5.15:第一张手写图像

图 5.15:第一张手写图片

这里是 1 的代码:

sample_1 = np.where((labels == 1))[0][0]

plt.imshow(img[sample_1], cmap='gray');

输出将如下所示:

图 5.16:第二张手写图片

图 5.16:第二张手写图片

我们几乎到达了可以开始构建模型的阶段。然而,由于每个样本是一个图片,并且数据采用矩阵格式,因此我们必须先重新排列每张图片。模型需要以向量形式提供图片,即每张图片的所有信息都存储在一行中。按照如下步骤执行:

images_0_1 = images_0_1.reshape((-1, rows * cols))

images_0_1.shape

现在,我们可以使用选定的图片和标签来构建并拟合逻辑回归模型:

model = LogisticRegression(solver='liblinear')

model.fit(X=images_0_1, y=labels_0_1)

输出将如下所示:

图 5.17:逻辑回归模型

图 5.17:逻辑回归模型

请注意,scikit-learn API 对逻辑回归的调用与线性回归的一致。还有一个额外的参数 solver,它指定了要使用的优化过程类型。我们在这里提供了该参数的默认值,以抑制 scikit-learn 在该版本中要求指定 solver 的警告。solver 参数的具体内容超出了本章的范围,添加它的目的是为了抑制警告消息。

检查该模型在相应训练数据上的表现:

model.score(X=images_0_1, y=labels_0_1)

输出将如下所示:

1.0

在这个例子中,模型能够以 100%的准确率预测训练标签。

使用模型显示训练数据的前两个预测标签:

model.predict(images_0_1) [:2]

输出将如下所示:

array([0, 1], dtype=uint8)

逻辑回归模型是如何做出分类决策的?来看一下模型为训练集生成的一些概率:

model.predict_proba(images_0_1)[:2]

输出将如下所示:

array([[9.99999999e-01, 9.89532857e-10],

[4.56461358e-09, 9.99999995e-01]])

我们可以看到,对于每个预测,都会有两个概率值。对于每张图片的预测,第一个值是该图片是数字 0 的概率,第二个值是数字 1 的概率。这两个值相加为 1。我们可以看到,在第一个例子中,数字 0 的预测概率为 0.9999999,因此,预测为数字 0。同理,第二个例子的情况也相反。

注意

理想情况下,这两个概率应该相加为 1,但由于计算限制和截断误差,它们几乎等于 1。

计算模型在测试集上的表现,以检查其在未见数据上的表现:

model.score(X=images_0_1_test, y=labels_0_1_test)

输出将如下所示:

0.9995271867612293

注意

请参阅第七章《模型评估》,了解更好的客观评估模型性能的方法。

我们可以看到,逻辑回归是一种强大的分类器,能够区分手写的 0 和 1 样本。

注意

若要访问此特定部分的源代码,请参见 packt.live/3dqqEvH

你也可以在线运行此示例,网址是 packt.live/3hT6FJm。你必须执行整个 Notebook 才能获得期望的结果。

现在我们已经在一个二分类问题上训练了一个逻辑回归模型,让我们将模型扩展到多个类。实际上,我们将使用相同的数据集,而不是只将其分类为两个类别或数字 0 和 1,而是将其分类为所有 10 个类别,或数字 0-9。实际上,逻辑回归的多类分类是通过一对多分类来实现的。也就是说,对于分类为 10 个类别,我们将训练 10 个二分类器。每个分类器将有一个数字作为第一类,其他 9 个数字作为第二类。通过这种方式,我们得到了 10 个二分类器,然后共同用于做出预测。换句话说,我们从每个二分类器中获取预测概率,最终的输出数字/类别是其分类器给出最高概率的那个。

练习 5.03: 逻辑回归 – 多类分类器

在前面的练习中,我们研究了使用逻辑回归对两个组别进行分类。然而,逻辑回归也可以用于将输入信息分类为 k 个不同的组,这就是我们将在本练习中探讨的多类分类器。加载 MNIST 训练和测试数据的过程与前一个练习完全相同:

导入所需的包:

import struct

import numpy as np

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.linear_model import LogisticRegression

加载训练/测试图像及其对应的标签:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

可视化数据的一个样本:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出将如下所示:

图 5.18:示例数据

图 5.18:示例数据

鉴于训练数据量非常大,我们将选择一部分数据来减少训练时间及所需的系统资源:

np.random.seed(0) # 提供一致的随机数

selection = np.random.choice(len(img), 5000)

selected_images = img[selection]

selected_labels = labels[selection]

请注意,在本示例中,我们使用了来自所有 10 个类别的数据,而不仅仅是类别 0 和 1,因此我们将此示例视为一个多类分类问题。

再次将输入数据重塑为向量形式,以便后续使用:

selected_images = selected_images.reshape((-1, rows * cols))

selected_images.shape

输出结果如下:

(5000, 784)

下一个单元格故意被注释掉了。暂时保留这段代码注释:

selected_images = selected_images / 255.0

img_test = img_test / 255.0

构建逻辑回归模型。该模型有一些额外的参数如下:求解器的 lbfgs 值适用于多类别问题,且需要更多的 max_iter 迭代才能收敛。multi_class 参数设置为 multinomial,以便计算整个概率分布的损失:

model = LogisticRegression(solver='lbfgs', \

multi_class='multinomial', \

max_iter=500, tol=0.1)

model.fit(X=selected_images, y=selected_labels)

输出结果如下:

图 5.19:逻辑回归模型

图 5.19:逻辑回归模型

注意

详细信息请参阅文档:https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html。

确定训练集的准确度评分:

model.score(X=selected_images, y=selected_labels)

输出结果如下:

1.0

确定训练集前两个预测结果,并绘制带有相应预测的图像:

model.predict(selected_images)[:2]

输出结果如下:

array([4, 1], dtype=uint8)

显示训练集前两个样本的图像,以查看我们是否正确:

plt.subplot(1, 2, 1)

plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray');

plt.axis('off');

plt.subplot(1, 2, 2)

plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray');

plt.axis('off');

输出结果如下:

图 5.20:绘制两个选定图像

图 5.20:绘制两个选定图像

再次打印出模型为训练集中的第一个样本提供的概率得分。确认每个类别有 10 个不同的值:

model.predict_proba(selected_images)[0]

输出结果如下:

图 5.21:预测值数组

图 5.21:预测值数组

请注意,在第一个样本的概率数组中,第五个(索引四)样本的概率最高,这表明预测结果为 4。

计算模型在测试集上的准确率。这将为模型在实际环境中的表现提供一个合理的估计,因为模型从未见过测试集中的数据。由于模型没有接触过这些数据,预计测试集的准确率会略低于训练集:

model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)

输出将如下所示:

0.878

在测试集上检查时,模型的准确率为 87.8%。当应用测试集时,性能下降是预期中的,因为这是模型第一次看到这些样本;而在训练过程中,训练集数据是反复呈现给模型的。

找到带有注释掉的代码的单元格,如第 4 步所示。取消注释该单元格中的代码:

selected_images = selected_images / 255.0

img_test = img_test / 255.0

这个单元格仅仅是将所有图像的值缩放到 0 到 1 之间。灰度图像的像素值范围在 0 到 255 之间,其中 0 代表黑色,255 代表白色。

点击“重启并运行全部”以重新运行整个 Notebook。

找到训练集的误差:

model.score(X=selected_images, y=selected_labels)

我们将得到以下得分:

0.986

找到测试集的误差:

model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)

我们将得到以下得分:

0.9002

注意

要访问此特定部分的源代码,请参考 packt.live/2B1CNKe

你也可以在网上运行这个示例,网址是 packt.live/3fQU4Vd。你必须执行整个 Notebook 才能得到预期的结果。

正常化图像对系统整体性能有何影响?训练误差更大了!我们从训练集的 100%准确率下降到 98.6%。是的,训练集的性能有所下降,但测试集的准确率从 87.8%上升到了 90.02%。测试集的性能更为重要,因为模型之前没有见过这些数据,因此它更能代表我们期望模型在实际应用中的表现。那么,为什么会有更好的结果呢?

回想我们在第二章《探索性数据分析与可视化》中讨论的关于归一化和数据缩放方法的内容。现在让我们回顾一下图 5.13,并注意曲线在接近-6 和+6 时的形状。曲线在接近 0 和接近 1 时分别饱和或变平。所以,如果我们使用的是 0 到 255 之间的图像(或 x 值),由逻辑函数定义的类概率会落在曲线的平坦区域内。位于此区域内的预测不太可能发生太大变化,因为它们需要非常大的 x 值变化才能使 y 发生有意义的变化。将图像缩放到 0 和 1 之间,最初会将预测值接近 p(x) = 0.5,因此,x 的变化对 y 的值影响更大。这可以实现更灵敏的预测,并导致在训练集上犯几个错误,但在测试集上更多的正确预测。建议在进行逻辑回归模型的训练和测试之前,将输入值缩放到 0 到 1 或-1 到 1 之间。

以下函数是一种将 NumPy 数组的值缩放到 0 和 1 之间的方法:

定义scale_input(x)函数:

normalized = (x - min(x)) / (max(x) - min(x))

返回normalized

上述缩放方法被称为最小-最大缩放(min-max scaling),因为它是基于数组的最小值和最大值进行缩放的。Z 缩放和均值缩放是其他著名的缩放方法。

因此,我们已经成功地使用逻辑回归模型解决了一个多类分类问题。现在让我们进行一个类似于练习 5.02:“逻辑回归作为分类器 – 二分类器”的活动,这次我们将解决一个二分类问题。不过,这次我们将使用一个更简单的模型——线性回归分类器。

活动 5.01:普通最小二乘法分类器 – 二分类器

在这个活动中,我们将构建一个基于 OLS(线性回归)的二类分类器,使用 MNIST 数据集对数字 0 和 1 进行分类。

要执行的步骤如下:

导入所需的依赖:

导入struct

导入numpynp

导入gzip

导入urllib.request

导入matplotlib.pyplotplt

从数组库导入array

sklearn.linear_model导入LinearRegression

将 MNIST 数据加载到内存中。

可视化数据样本。

构建一个线性分类器模型,用于分类数字 0 和 1。我们将创建的模型是用来确定样本是否为数字 0 或 1。为此,我们首先需要选择这些样本。

使用 0 和 1 的一个样本图像可视化所选信息。

为了将图像信息提供给模型,我们必须首先将数据展平,使得每个图像的形状为 1 x 784 个像素。

构建模型;使用LinearRegression API 并调用fit函数。

确定训练集上的准确性。

使用 0.5 的阈值确定每个训练样本的标签预测值。大于 0.5 的值分类为 1;小于或等于 0.5 的值分类为 0。

计算预测训练值与实际值之间的分类准确率。

与测试集进行性能比较。

注意

此活动的解决方案可以通过此链接找到。

这里有一个有趣的点需要注意,那就是在这里的测试集表现比练习 5.02:逻辑回归作为分类器——二元分类器的表现更差。两种情况的数据集完全相同,但模型不同。正如预期的那样,线性回归分类器作为一个更简单的模型,其测试集表现比更强大的逻辑回归模型要差。

选择 K 最佳特征选择

现在我们已经建立了如何在 MNIST 数据集上训练和测试线性回归与逻辑回归模型,接下来我们将使用逻辑回归模型在另一个数据集上解决一个多类分类问题。作为下一个练习的前提条件,让我们快速讨论一种特定的特征选择方法——选择 K 最佳特征选择方法。在这种方法中,我们根据 K 个最高分数来选择特征。分数是基于评分函数得出的,该函数输入特征(X)和目标(y),并返回每个特征的分数。一个这样的函数的例子可能是计算标签(y)与特征(X)之间的 ANOVA F 值的函数。此评分函数的实现可以通过 scikit-learn 获得:https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.f_classif.html#sklearn.feature_selection.f_classif。然后,特征会根据分数的降序排列,我们从这个排序列表中选择前 K 个特征。选择 K 最佳特征选择方法的实现也可以通过 scikit-learn 获得:https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectKBest.html。此外,以下是一个示例代码,展示如何在 scikit-learn 中使用此方法:

从 sklearn.datasets 导入 load_digits

从 sklearn.feature_selection 导入 SelectKBest 和 chi2

X, y = load_digits(return_X_y=True)

X.shape

(1797, 64)

X_new = SelectKBest(chi2, k=20).fit_transform(X, y)

X_new.shape

(1797, 20)

现在我们进入下一个练习,解决一个多类分类问题。

练习 5.04:使用逻辑回归进行乳腺癌诊断分类

在这个练习中,我们将使用乳腺癌诊断数据集(可在 https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic) 下载,也可以在 GitHub 上找到 https://packt.live/3a7oAY8)。该数据集是 UCI 机器学习库的一部分(https://archive.ics.uci.edu/ml/index.php)。数据集包含数字化的乳腺肿块细针穿刺(FNA)图像中细胞核的特征,每个细胞核都标有恶性或良性的标签。特征包括(共 30 个),例如细胞核的平均半径、半径误差、最差半径、平均纹理、纹理误差和最差纹理等。我们将使用数据集提供的这些特征来对恶性细胞和良性细胞进行分类。

需要执行的步骤如下:

导入所需的包。对于这个练习,我们需要 pandas 包来加载数据,Matplotlib 包来绘图,以及 scikit-learn 来创建逻辑回归模型。导入所有必需的包和相关模块:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.linear_model import LogisticRegression

from sklearn.feature_selection import SelectKBest

from sklearn.model_selection import train_test_split

使用 pandas 加载乳腺癌诊断数据集并检查前五行:

df = pd.read_csv('../Datasets/breast-cancer-data.csv')

df.head()

输出将如下所示:

图 5.22:乳腺癌数据集的前五行

图 5.22:乳腺癌数据集的前五行

此外,将数据集分解为输入(X)和输出(y)变量:

X, y = df[[c for c in df.columns if c != 'diagnosis']], df.diagnosis

下一步是特征工程。我们使用 scikit-learn 的 SelectKBest 特征选择模块。基本上,这个方法根据得分函数检查每个特征对目标输出的影响力。你可以在这里阅读更多细节: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectKBest.html:

"""

限制为两个最佳特征,以便

我们可以在图表中可视化它们

"""

skb_model = SelectKBest(k=2)

X_new = skb_model.fit_transform(X, y)

获取 k 个最佳列名称

mask = skb_model.get_support() # 布尔值列表

selected_features = [] # 你的 K 个最佳特征列表

for bool, feature in zip(mask, df.columns):

if bool:

selected_features.append(feature)

print(selected_features)

输出将如下所示:

['worst perimeter', 'worst concave points']

现在,让我们可视化这两个最重要的特征与目标(诊断)的相关性,以及它们如何很好地区分这两类诊断:

markers = {'benign': {'marker': 'o'}, \

'malignant': {'marker': 'x'},}

plt.figure(figsize=(10, 7))

for name, group in df.groupby('diagnosis'):

plt.scatter(group[selected_features[0]], \

group[selected_features[1]], label=name, \

marker=markers[name]['marker'],)

plt.title(f'诊断分类 {selected_features[0]} 与 \

{selected_features[1]}');

plt.xlabel(selected_features[0]);

plt.ylabel(selected_features[1]);

plt.legend();

输出结果如下:

图 5.23:特征选择的散点图

图 5.23:特征选择的散点图

在构建模型之前,我们必须先将诊断值转换为可以在模型中使用的标签。将良性诊断字符串替换为值 0,将恶性诊断字符串替换为值 1:

diagnoses = ['良性', '恶性',]

output = [diagnoses.index(diag) for diag in df.diagnosis]

同时,为了公平地评估模型,我们应将训练数据集分为训练集和验证集:

train_X, valid_X, \

train_y, valid_y = train_test_split(df[selected_features], output, \

test_size=0.2, random_state=123)

使用 selected_features 和分配的诊断标签创建模型:

model = LogisticRegression(solver='liblinear')

model.fit(df[selected_features], output)

输出结果如下:

LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,

intercept_scaling=1, l1_ratio=None, max_iter=100,

multi_class='warn', n_jobs=None, penalty='l2',

random_state=None, solver='liblinear', tol=0.0001,

verbose=0,

warm_start=False)

计算模型在验证集上的准确性:

model.score(valid_X, valid_y)

输出结果如下:

0.9385964912280702

使用随机选择的 selected_features 构建另一个模型并比较性能:

selected_features = ['mean radius', # 在此列出特征 \

'mean texture', 'compactness error']

train_X, valid_X, \

train_y, valid_y = train_test_split(df[selected_features], output, \

test_size=0.2, random_state=123)

model = LogisticRegression(solver='liblinear')

model.fit(train_X, train_y)

model.score(valid_X, valid_y)

输出结果如下:

0.8859649122807017

这个降低的准确性表明,确实,使用两个最重要的特征比使用三个随机选择的特征能更强大。

使用所有可用信息构建另一个模型并比较性能:

selected_features = [feat for feat in df.columns \

如果 feat != 'diagnosis' # 在此列出特征

]

train_X, valid_X, \

train_y, valid_y = train_test_split(df[selected_features], output, \

test_size=0.2, random_state=123)

model = LogisticRegression(solver='liblinear')

model.fit(train_X, train_y)

model.score(valid_X, valid_y)

输出结果如下:

0.9824561403508771

注意

若要访问此特定部分的源代码,请参考 https://packt.live/2YWxjIN。

你也可以在线运行这个示例,网址是 https://packt.live/2Bx8NWt。你必须执行整个 Notebook 才能得到预期的结果。

通过使用所有特征来提高性能表明,即使是那些并非最重要的特征,也仍然在提高模型性能方面发挥了作用。

使用 K-近邻分类

现在我们已经熟悉了使用逻辑回归创建多类分类器,并且这些模型的性能也不错,我们将把注意力转向另一种类型的分类器:K-近邻(KNN)分类器。KNN 是一种非概率的非线性分类器。它不会预测类别的概率。而且,由于它不会学习任何参数,因此没有参数的线性组合,因而它是一个非线性模型:

图 5.24:KNN 的可视化表示

图 5.24:KNN 的可视化表示

图 5.24 展示了 KNN 分类器的工作原理。两种不同的符号,X 和 O,代表属于两个不同类别的数据点。中心的实心圆是需要分类的测试点,内层虚线圆显示了 k=3 时的分类过程,而外层虚线圆显示了 k=5 时的分类过程。这里的意思是,如果 k=3,我们只看离测试点最近的三个数据点,这给人一种虚线圆圈包含这三个最近数据点的感觉。

KNN 是最简单的数据分类“学习”算法之一。这里使用引号中的“学习”是有明确指示的,因为 KNN 并不像其他方法(如逻辑回归)那样从数据中学习并通过参数或权重对这些学习进行编码。KNN 使用基于实例的学习或懒学习,因为它仅仅存储或记住所有的训练样本及其相应的类别。它的名字“K-近邻”来源于这样一个事实:当测试样本提供给算法进行分类预测时,它通过对 k 个最近数据点的多数投票来决定相应的类别。如果我们看图 5.24,并假设 k=3,最近的三个数据点位于内层虚线圆内,在这种情况下,分类结果将是一个空心圆(O)。

然而,如果我们取 k=5,则最近的五个点位于外部虚线圆内,分类结果为叉号(X)(三个叉号与两个空心圆)。那么,我们如何选择 k 呢?从学术角度来看,我们应该将 KNN 模型的性能(误差)绘制成 k 的函数。查看图中是否有肘部(elbow)点,并观察当 k 增加时,误差是否不再显著变化;这意味着我们已经找到了 k 的最优值。更实际地说,k 的选择取决于数据,较大的 k 值能够减少噪声对分类的影响,但也会使类别之间的边界变得不那么明显。

上图突出了 KNN 分类中应考虑的一些特性:

如前所述,k 的选择非常重要。在这个简单的例子中,由于两类样本的接近,k 从 3 切换到 5 使得分类预测发生了变化。由于最终分类是通过多数投票确定的,因此使用奇数值的 k 通常是有益的,以确保投票过程中有赢家。如果选择偶数值的 k,并且发生投票平局,那么可以使用多种方法来打破平局,包括:

将 k 减 1,直到打破平局

基于最小欧几里得距离选择最近点的类别

应用加权函数使测试点更倾向于靠近的邻居

KNN 模型能够形成极其复杂的非线性边界,这在对具有高度非线性边界的图像或数据集进行分类时具有优势。考虑到在图 5.24 中,随着 k 的增加,测试点从空心圆分类变为交叉点,我们可以看到这里可能会形成一个复杂的边界。

由于分类过程实际上只依赖于附近的点,KNN 模型对数据中的局部特征非常敏感。

由于 KNN 模型会记住所有训练信息以进行预测,因此它们在泛化到新的、未见过的数据时可能会遇到困难。

KNN 还有一个变种,它不是指定最近邻的数量,而是指定围绕测试点的半径大小来查找邻居。这种方法称为半径邻居分类,本章不予讨论,但在理解 KNN 的同时,你也将理解半径邻居分类,并学习如何通过 scikit-learn 使用该模型。

注意

我们对 KNN 分类的解释以及接下来的练习将研究具有两个特征或两个维度的数据建模,因为这可以简化可视化并更好地理解 KNN 建模过程。接下来,我们将在活动 5.02:KNN 多类分类器中对具有更多维度的数据集进行分类,在那里我们将使用 KNN 对 MNIST 数据集进行分类。记住,尽管有太多维度以至于无法绘制图像,但这并不意味着它不能用 N 维度进行分类。

为了便于可视化 KNN 过程,我们将在接下来的练习中关注乳腺癌诊断数据集。该数据集作为本书附带的代码文件的一部分提供。

练习 5.05:KNN 分类

在本练习中,我们将使用 KNN 分类算法,在乳腺癌诊断数据集上构建模型,并通过计算其准确率来评估模型的表现:

本练习中,我们需要导入 pandas、Matplotlib 以及 scikit-learn 中的 KNeighborsClassifier 和 train_test_split 子模块。我们将使用简写 KNN 以便快速访问:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.neighbors import KNeighborsClassifier as KNN

from sklearn.model_selection import train_test_split

加载乳腺癌诊断数据集并查看前五行:

df = pd.read_csv('../Datasets/breast-cancer-data.csv')

df.head()

输出结果如下:

图 5.25:前五行

图 5.25:前五行

在此阶段,我们需要从数据集中选择最合适的特征用于分类器。我们可以简单地选择所有 30 个特征。然而,由于这个练习是为了展示 KNN 过程,我们将任意选择平均半径和最差半径。构建一个散点图,展示每个类别的平均半径与最差半径的关系,并标明对应的诊断类型:

markers = {'良性': {'marker': 'o', 'facecolor': 'g', \

'edgecolor': 'g'}, \

'恶性': {'marker': 'x', 'facecolor': 'r', \

'edgecolor': 'r'},}

plt.figure(figsize=(10, 7))

for name, group in df.groupby('diagnosis'):

plt.scatter(group['mean radius'], group['worst radius'], \

label=name, marker=markers[name]['marker'], \

facecolors=markers[name]['facecolor'], \

edgecolor=markers[name]['edgecolor'])

plt.title('乳腺癌诊断分类 平均半径 '\

'与最差半径');

plt.xlabel('平均半径');

plt.ylabel('最差半径');

plt.legend();

输出结果如下:

图 5.26:癌症数据的散点图

图 5.26:癌症数据的散点图

在实际开始训练模型之前,我们先将训练数据集按 80:20 的比例拆分为训练集和验证集,以便稍后使用验证集公正地评估模型性能:

train_X, valid_X, \

train_y, valid_y = train_test_split(df[['mean radius', \

'最差半径']], \

df.diagnosis, test_size=0.2, \

random_state=123)

构建一个 KNN 分类器模型,k = 3,并将其拟合到训练数据上:

model = KNN(n_neighbors=3)

model.fit(X=train_X, y=train_y)

输出结果如下:

图 5.27:K 邻近分类器

图 5.27:K 邻近分类器

检查模型在验证集上的表现:

model.score(X=valid_X, y=valid_y)

输出将显示性能得分:

0.9385964912280702

如我们所见,验证集上的准确率超过了 93%。接下来,通过一个练习,我们将尝试理解 KNN 模型在训练过程中形成的决策边界。我们将在练习中绘制这些边界。

注意

要查看此特定部分的源代码,请参见 https://packt.live/3dovRUH。

你也可以在 https://packt.live/2V5hYEP 上在线运行此示例。你必须执行整个 Notebook 才能获得期望的结果。

练习 5.06:可视化 KNN 边界

为了可视化 KNN 分类器生成的决策边界,我们需要遍历预测空间,即“平均半径”和“最差半径”的最小值和最大值,并确定模型在这些点上的分类结果。一旦完成遍历,我们就可以绘制模型所做的分类决策:

导入所有相关的包。我们还需要使用 NumPy 来完成此练习:

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from matplotlib.colors import ListedColormap

from sklearn.neighbors import KNeighborsClassifier as KNN

将数据集加载到 pandas DataFrame 中:

df = pd.read_csv('../Datasets/breast-cancer-data.csv')

df.head()

输出结果如下:

图 5.28:前五行数据

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-A9HFJUOT.jpg)

图 5.28:前五行数据

虽然我们可以使用诊断字符串来创建之前练习中的模型,但在绘制决策边界时,将诊断映射为不同的整数值会更加有用。为此,首先创建一个标签列表以供后续参考,并通过遍历该列表,将现有标签替换为列表中对应的索引:

labelled_diagnoses = ['良性', '恶性',]

for idx, label in enumerate(labelled_diagnoses):

df.diagnosis = df.diagnosis.replace(label, idx)

df.head()

输出结果如下:

图 5.29:前五行数据

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-MBA2EF5A.jpg)

图 5.29:前五行数据

请注意在 for 循环定义中使用了 enumerate 函数。在遍历 for 循环时,enumerate 函数提供了列表中值的索引以及每次迭代中的值。我们将值的索引赋给 idx 变量,将值赋给 label。以这种方式使用 enumerate 提供了一种简单的方法来将物种字符串替换为唯一的整数标签。

构建一个 KNN 分类模型,同样使用三个最近邻,并拟合“平均半径”和“最差半径”以及新标签化的诊断数据:

model = KNN(n_neighbors=3)

model.fit(X=df[['mean radius', 'worst radius']], y=df.diagnosis)

输出结果如下:

图 5.30:K-邻居分类器

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-WKLMXVYM.jpg)

图 5.30:K-邻居分类器

为了可视化我们的决策边界,我们需要在信息空间内创建一个网格或预测范围,即所有可能的“平均半径”和“最差半径”的值的组合。从“平均半径”和“最差半径”的最小值减去 1 单位开始,最终在“平均半径”和“最差半径”的最大值上加 1 单位,使用 NumPy 的 arange 函数在这些范围内按 0.1(间隔)创建一系列值:

spacing = 0.1

mean_radius_range = np.arange(df['mean radius'].min() - 1, \

df['mean radius'].max() + 1, spacing)

worst_radius_range = np.arange(df['worst radius'].min() - 1, \

df['worst radius'].max() + 1, spacing)

使用 NumPy 的 meshgrid 函数将两个范围合并成网格:

创建网格

xx, yy = np.meshgrid(mean_radius_range, worst_radius_range)

查看 xx:

xx

输出结果如下:

图 5.31: 网格 xx 值数组

图 5.31: 网格 xx 值数组

查看 yy:

yy

输出结果如下:

图 5.32: 网格 yy 值数组

图 5.32: 网格 yy 值数组

使用 np.c_ 将网格拼接成一个 NumPy 数组:

pred_x = np.c_[xx.ravel(), yy.ravel()] # 拼接结果

pred_x

输出结果如下:

图 5.33: 预测值数组

图 5.33: 预测值数组

虽然这个函数调用看起来有点神秘,但它其实只是将两个独立的数组拼接在一起(参考 https://docs.scipy.org/doc/numpy/reference/generated/numpy.c_.html),是拼接的简写形式。

生成网格的类别预测:

pred_y = model.predict(pred_x).reshape(xx.shape)

pred_y

输出结果如下:

图 5.34: 预测的 y 值数组

图 5.34: 预测的 y 值数组

为了一致地可视化边界,我们需要两组一致的颜色;一组较浅的颜色用于决策边界,另一组较深的颜色用于训练集的点。使用 ListedColormaps 创建两个色图:

创建色图

cmap_light = ListedColormap(['#6FF6A5', '#F6A56F',])

cmap_bold = ListedColormap(['#0EE664', '#E6640E',])

为了突出决策边界,首先根据诊断类型绘制训练数据,使用 cmap_bold 色图,并为每种不同的诊断类型使用不同的标记:

markers = {'良性': {'marker': 'o', 'facecolor': 'g', \

'edgecolor': 'g'}, \

'恶性': {'marker': 'x', 'facecolor': 'r', \

'edgecolor': 'r'},}

plt.figure(figsize=(10, 7))

for name, group in df.groupby('diagnosis'):

diagnoses = labelled_diagnoses[name]

plt.scatter(group['mean radius'], group['worst radius'], \

c=cmap_bold.colors[name], \

label=labelled_diagnoses[name], \

marker=markers[diagnoses]['marker'])

plt.title('乳腺癌诊断分类平均半径 '\

'vs 最差半径');

plt.xlabel('平均半径');

plt.ylabel('最差半径');

plt.legend();

输出结果如下:

图 5.35: 突出显示决策边界的散点图

图 5.35: 突出显示决策边界的散点图

使用先前制作的预测网格,绘制决策边界以及训练数据:

plt.figure(figsize=(10, 7))

plt.pcolormesh(xx, yy, pred_y, cmap=cmap_light);

plt.scatter(df['mean radius'], df['worst radius'], c=df.diagnosis, cmap=cmap_bold, edgecolor='k', s=20);

plt.title('乳腺癌诊断决策边界平均半径 '\

'vs 最差半径');

plt.xlabel('平均半径');

plt.ylabel('最差半径');

plt.text(15, 12, '良性', ha='center',va='center', \

size=20,color='k');

plt.text(15, 30, '恶性', ha='center',va='center', \

size=20,color='k');

输出将如下所示:

图 5.36:决策边界

图 5.36:决策边界

注意

要访问此特定部分的源代码,请参阅 https://packt.live/3dpxPnY。

你也可以在 https://packt.live/3drmBPE 在线运行此示例。你必须执行整个 Notebook 才能得到期望的结果。

因此,我们既训练了一个 KNN 分类器,又理解了 knn 决策边界是如何形成的。接下来,我们将训练一个 KNN 多类分类器,应用于不同的数据集,并评估其性能。

活动 5.02:KNN 多类分类器

在本次活动中,我们将使用 KNN 模型将 MNIST 数据集分类为 10 个不同的数字类。

要执行的步骤如下:

导入以下包:

导入 struct

导入 numpynp

导入 gzip

导入 urllib.request

导入 matplotlib.pyplotplt

array 导入 array

sklearn.neighbors 导入 KNeighborsClassifierKNN

将 MNIST 数据加载到内存中;首先是训练图像,然后是训练标签,然后是测试图像,最后是测试标签。

可视化数据的一个样本。

构建一个 KNN 分类器,使用三个最近邻来分类 MNIST 数据集。同样,为了节省处理能力,随机采样 5,000 张图像用于训练。

为了将图像信息提供给模型,我们必须首先将数据展平,使得每个图像的形状为 1 x 784 像素。

使用 k=3 构建 KNN 模型,并将数据拟合到模型中。请注意,在本次活动中,我们为模型提供了 784 个特征或维度,而不仅仅是 2 个。

确定在训练集上的得分。

显示模型在训练数据上的前两个预测。

将性能与测试集进行比较。

输出将如下所示:

0.9376

注意

此活动的解决方案可以通过此链接找到。

如果我们将前面的测试集性能与第 5.03 题中的逻辑回归——多类分类器的表现进行比较,我们会发现,在相同的数据集上,knn 模型在此任务中优于逻辑回归分类器。这并不意味着 knn 总是优于逻辑回归,但在这个任务和这个数据集上,它确实表现得更好。

使用决策树进行分类

在本章中,我们将研究另一种强大的分类方法——决策树。决策树在许多应用中都有广泛的使用,例如自然语言处理等。本章将探讨 ID3 方法在分类类别数据中的应用,同时使用 scikit-learn 中的 CART 实现作为另一种分类数据集的方法。那么,决策树到底是什么呢?

如其名称所示,决策树是一种学习算法,基于输入信息按顺序做出一系列决策,以做出最终分类。回想你小时候的生物课,你可能通过二分法分类工具(dichotomous keys)来分类不同类型的动物,类似于决策树的过程。就像上面展示的二分法工具的例子一样,决策树旨在根据一系列决策或问题步骤的结果对信息进行分类:

图 5.37:使用二分法分类动物

图 5.37:使用二分法分类动物

根据使用的决策树算法不同,决策步骤的实现可能略有差异,但我们将专门讨论 ID3 算法的实现。ID3 算法的目的是根据每个决策提供的最大信息增益来对数据进行分类。为了更好地理解这一设计,我们还需要理解两个额外的概念:熵和信息增益。

注意

ID3 算法最早由澳大利亚研究员罗斯·昆兰(Ross Quinlan)在 1985 年提出(https://doi.org/10.1007/BF00116251)。

熵:简单来说,熵表示信号的不确定性程度。例如,如果一场足球比赛(soccer)距离结束还有 5 分钟,而比分是 5-0,我们就会说这场比赛的熵很低,换句话说,我们几乎可以确定 5 个进球的队伍将获胜。然而,如果比分是 1-1,那么这场比赛的熵将被认为很高(不确定性)。在信息论的背景下,熵是由随机数据源提供信息的平均速率。从数学角度看,这个熵定义为:

图 5.38:熵方程

图 5.38:熵方程

在这种情况下,当数据的随机源产生约 0.5 的概率值时,事件携带更多的信息,因为与数据源产生极端(高或低)概率值时相比,最终结果相对不确定。

信息增益:这是量化如果我们事先知道变量 a 的信息(在机器学习模型中,变量 a 通常是一个特征)时,可以减少的不确定性。换句话说,变量 a 可以提供关于某个事件多少信息。给定数据集 S 和要观察的属性 a,信息增益在数学上定义为:

图 5.39:信息增益方程

图 5.39:信息增益方程

数据集 S 的属性 a 的信息增益等于 S 的熵减去 S 在属性 a 条件下的熵,或者 S 的数据集熵减去集合 t 中元素数与源 S 中元素总数的比例,再乘以 t 的熵,其中 t 是属性 a 中的某个类别。

如果你觉得这里的数学有点难理解,不用担心,它比看起来要简单得多。为了更好地理解 ID3 过程,我们将使用与 Quinlan 在原始论文中提供的相同数据集,逐步讲解该过程。

练习 5.07:ID3 分类

在这个练习中,我们将对一个数据集执行 ID3 分类。在原始论文中,Quinlan 提供了一个包含 10 个天气观察样本的小数据集,标记为 P 表示天气适合,例如,适合周六早晨的板球比赛(或者对于我们北美的朋友来说,是棒球),如果天气不适合比赛,则标记为 N。论文中描述的示例数据集将在本练习中创建:

导入所需的包:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

在 Jupyter Notebook 中,创建一个如下的 pandas DataFrame 训练集:

df = pd.DataFrame()

df['Outlook'] = ['晴天', '晴天', '阴天', '雨天', '雨天', \

'雨天','阴天','晴天','晴天','雨天',\

'晴天','阴天','阴天','雨天']

df['Temperature'] = ['炎热', '炎热', '炎热', '温和', '凉爽', '凉爽', \

'凉爽','温和','凉爽','温和','温和',\

'温和','炎热','温和',]

df['Humidity'] = ['高', '高', '高', '高', '正常', \

'正常', '正常', '高', '正常', \

'正常', '正常', '高', '正常', '高']

df['Windy'] = ['弱', '强', '弱', '弱', '弱', '强', \

'强','弱','弱','弱','强','强',\

'弱','强']

df['Decision'] = ['N', 'N', 'P', 'P', 'P', 'N', 'P', 'N', 'P', \

'P','P', 'P', 'P', 'N']

df

输出将如下所示:

图 5.40:pandas DataFrame

图 5.40:pandas DataFrame

在原始论文中,ID3 算法通过随机选取训练集中的小样本并将树拟合到这个窗口开始。这对于大数据集可能是一个有用的方法,但考虑到我们的数据集相当小,我们将直接从整个训练集开始。第一步是计算决策列的熵,这里有两个可能的值或类别,P 和 N:

P 的概率

p_p = len(df.loc[df.Decision == 'P']) / len(df)

N 的概率

p_n = len(df.loc[df.Decision == 'N']) / len(df)

entropy_decision = -p_n * np.log2(p_n) - p_p * np.log2(p_p)

print(f'H(S) = {entropy_decision:0.4f}')

输出将如下所示:

H(S) = 0.94403

我们需要重复这个计算,因此将其包装成一个函数:

def f_entropy_decision(data):

p_p = len(data.loc[data.Decision == 'P']) / len(data)

p_n = len(data.loc[data.Decision == 'N']) / len(data)

返回 -p_n * np.log2(p_n) - p_p * np.log2(p_p)

下一步是计算哪个属性在 Outlook、Temperature、Humidity 和 Windy 中提供了最高的信息增益。从 Outlook 参数开始,确定在晴天、阴天和雨天情况下每个决策的概率。我们需要评估以下方程:

图 5.41:信息增益

图 5.41:信息增益

使用 pandas groupby 方法在 Python 中构建这个方程:

IG_decision_Outlook = entropy_decision # H(S)

创建一个字符串以打印出整体方程

overall_eqn = 'Gain(Decision, Outlook) = Entropy(Decision)'

"""迭代遍历 outlook 的值,并计算每个决策的概率和熵值

"""

对于 name, Outlook in df.groupby('Outlook'):

num_p = len(Outlook.loc[Outlook.Decision == 'P'])

num_n = len(Outlook.loc[Outlook.Decision != 'P'])

num_Outlook = len(Outlook)

print(f'p(Decision=P|Outlook={name}) = {num_p}/{num_Outlook}')

print(f'p(Decision=N|Outlook={name}) = {num_n}/{num_Outlook}')

print(f'p(Outlook={name}) = {num_Outlook}/{len(df)}')

print(f'Entropy(Decision|Outlook={name}) = '\

f'-{num_p}/{num_Outlook}.log2({num_p}/{num_Outlook}) - '\

f'{num_n}/{num_Outlook}.log2({num_n}/{num_Outlook})')

entropy_decision_outlook = 0

不能计算 0 的对数,因此需要添加检查

如果 num_p != 0:

entropy_decision_outlook -= (num_p / num_Outlook) \

  • np.log2(num_p / num_Outlook)

不能计算 0 的对数,因此需要添加检查

如果 num_n != 0:

entropy_decision_outlook -= (num_n / num_Outlook) \

  • np.log2(num_n / num_Outlook)

IG_decision_Outlook -= (num_Outlook / len(df)) \

  • entropy_decision_outlook

print()

overall_eqn += f' - p(Outlook={name}).'

overall_eqn += f'Entropy(Decision|Outlook={name})'

print(overall_eqn)

print(f'Gain(Decision, Outlook) = {IG_decision_Outlook:0.4f}')

输出将如下所示:

图 5.42:熵和增益概率

图 5.42:熵和增益概率

Outlook 的最终增益方程可以重新写为:

图 5.43:信息增益的方程

图 5.43:信息增益的方程

我们需要重复这个过程好几次,因此将其包装成一个方便以后使用的函数:

def IG(data, column, ent_decision=entropy_decision):

IG_decision = ent_decision

对于 name, temp in data.groupby(column):

p_p = len(temp.loc[temp.Decision == 'P']) / len(temp)

p_n = len(temp.loc[temp.Decision != 'P']) / len(temp)

entropy_decision = 0

如果 p_p != 0:

entropy_decision -= (p_p) * np.log2(p_p)

如果 p_n != 0:

entropy_decision -= (p_n) * np.log2(p_n)

IG_decision -= (len(temp) / len(df)) * entropy_decision

return IG_decision

对其他每一列重复这个过程,计算相应的信息增益:

for col in df.columns[:-1]:

print(f'Gain(Decision, {col}) = {IG(df, col):0.4f}')

输出将如下所示:

Gain(Decision, Outlook) = 0.2467

Gain(Decision, Temperature) = 0.0292

Gain(Decision, Humidity) = 0.1518

Gain(Decision, Windy) = 0.0481

这些信息提供了决策树的第一个决策。我们希望按最大信息增益进行拆分,所以我们选择在 Outlook 上进行拆分。看一下根据 Outlook 拆分后的数据:

for name, temp in df.groupby('Outlook'):

print('-' * 15)

print(name)

print('-' * 15)

print(temp)

print('-' * 15)

输出将如下所示:

图 5.44:信息增益

图 5.44:信息增益

请注意,所有多云记录的决策为 P。这为我们的决策树提供了第一个终止叶子节点。如果是多云,我们将进行游戏,而如果是雨天或阳光明媚,则有可能不进行游戏。目前为止,决策树可以表示为以下图示:

图 5.45:决策树

图 5.45:决策树

注意

该图是手动创建的,仅供参考,并不包含在附带的源代码中,也没有从中获得。

现在我们重复这个过程,通过信息增益进行拆分,直到所有数据被分配完并且所有树的分支都终止。首先,移除多云的样本,因为它们不再提供任何额外的信息:

df_next = df.loc[df.Outlook != 'overcast']

df_next

输出将如下所示:

图 5.46:移除多云样本后的数据

图 5.46:移除多云样本后的数据

现在,我们将关注阳光样本,并重新运行增益计算,以确定最佳的阳光信息拆分方式:

df_sunny = df_next.loc[df_next.Outlook == 'sunny']

对阳光样本重新计算熵值:

entropy_decision = f_entropy_decision(df_sunny)

entropy_decision

输出将如下所示:

0.9709505944546686

对阳光样本进行增益计算:

for col in df_sunny.columns[1:-1]:

print(f'Gain(Decision, {col}) = \

{IG(df_sunny, col, entropy_decision):0.4f}')

输出将如下所示:

Gain(Decision, Temperature) = 0.8281

Gain(Decision, Humidity) = 0.9710

Gain(Decision, Windy) = 0.6313

再次选择最大增益,这次是湿度。根据湿度分组数据:

for name, temp in df_sunny.groupby('Humidity'):

print('-' * 15)

print(name)

print('-' * 15)

print(temp)

print('-' * 15)

输出将如下所示:

图 5.47:根据湿度分组后的数据

图 5.47:根据湿度分组后的数据

我们可以看到,当湿度较高时,决策是“不玩”,而当湿度正常时,决策是“玩”。因此,更新后的决策树表示为:

图 5.48:具有两个值的决策树

图 5.48:具有两个值的决策树

所以,最后一组需要分类的数据是雨天预报数据。提取仅包含降雨数据的部分并重新运行熵计算:

df_rain = df_next.loc[df_next.Outlook == 'rain']

entropy_decision = f_entropy_decision(df_rain)

entropy_decision

输出如下:

0.9709505944546686

重复对雨天子集的增益计算:

for col in df_rain.columns[1:-1]:

print(f'Gain(Decision, {col}) = \

{IG(df_rain, col, entropy_decision):0.4f}')

输出如下:

Gain(Decision, Temperature) = 0.6313

Gain(Decision,Humidity) = 0.6313

Gain(Decision, Windy) = 0.9710

同样,基于最大增益值的划分需要基于“Windy”值进行划分。因此,将剩余信息按“Windy”进行分组:

for name, temp in df_rain.groupby('Windy'):

print('-' * 15)

print(name)

print('-' * 15)

print(temp)

print('-' * 15)

输出如下:

图 5.49:按 Windy 分组的数据

图 5.49:按 Windy 分组的数据

最后,我们有了所有终止叶节点,完成树的构建,因为基于“Windy”属性的划分提供了两个集合,其中每个集合都指示“玩”(P)或“不玩”(N)值。我们的完整决策树如下:

图 5.50:最终决策树

图 5.50:最终决策树

注释

要访问该特定部分的源代码,请参考 https://packt.live/37Rh7fX。

您还可以在线运行此示例,网址是 https://packt.live/3hTz4Px。您必须执行整个笔记本,以获得所需的结果。

决策树,类似于 KNN 模型,是判别模型。判别模型旨在最大化给定特征的数据类别的条件概率。与判别模型相对的是生成模型,生成模型学习数据类别和特征的联合概率,从而学习数据的分布以生成人工样本。

那么,在决策树的情况下,我们如何利用未见过的信息进行预测呢?只需沿着树走。查看每个节点处做出的决策,并应用未见样本的数据。预测最终会是终止叶节点所指定的标签。假设我们有一个即将到来的星期六的天气预报,并且我们想预测是否能去玩。天气预报如下:

图 5.51:即将到来的星期六的天气预报

图 5.51:即将到来的星期六的天气预报

该决策树的结构如下(虚线圈表示树中选择的叶子):

图 5.52:使用决策树进行新预测

图 5.52:使用决策树进行新预测

现在,希望你对决策树的基本概念和顺序决策过程有了合理的理解。掌握了决策树的原理后,我们将继续探讨如何使用 scikit-learn 提供的功能应用更复杂的模型。

分类与回归树

scikit-learn 的决策树方法实现了 CART 方法,它提供了在分类和回归问题中使用决策树的能力。CART 与 ID3 的不同之处在于,决策是通过将特征值与计算得到的值进行比较来做出的。更准确地说,我们可以看到,在 ID3 算法中,决策是基于数据集中存在的特征值做出的。这种方法在数据是分类数据时表现良好;然而,一旦数据变为连续数据,这种方法就不再有效。在这种情况下,使用 CART,它计算与特征值进行比较的阈值。而且,因为在这种比较中,只有两种可能的结果——(a)特征值大于(或等于)阈值,或者,(b)特征值小于(或等于)阈值——因此,CART 结果为二叉树。

相反,ID3 创建多叉树,因为如前所述,在 ID3 中,决策是基于现有特征值做出的,如果特征是分类的,则树会根据类别的数量分支成多个分支。ID3 与 CART 之间的另一个区别是,ID3 使用信息增益作为度量标准来找到最佳切分,而 CART 使用另一个度量,称为基尼不纯度度量。从数学上讲,我们可以回忆起我们定义的熵为:

图 5.53:熵的定义

图 5.53:熵的定义

因此,基尼不纯度定义为:

图 5.54:基尼不纯度的定义

图 5.54:基尼不纯度的定义

从概念上讲,这衡量的是以下内容:如果我们随机选择数据集中的一个数据点,并根据数据集中的类别分布随机对其进行分类(标记),那么将数据点分类错误的概率是多少?

在讨论了基于 CART 和 ID3 的决策树方法后,接下来我们将使用 CART 方法解决一个分类问题。

练习 5.08:使用 CART 决策树进行乳腺癌诊断分类

在本练习中,我们将使用 scikit-learn 的决策树分类器对乳腺癌诊断数据进行分类,这可以应用于分类和回归问题:

导入所需的包:

import numpy as np

import pandas as pd

导入 matplotlib.pyplot 为 plt

来自 sklearn.tree 的 DecisionTreeClassifier

来自 sklearn.model_selection 的 train_test_split

加载乳腺癌数据集:

df = pd.read_csv('../Datasets/breast-cancer-data.csv')

df.head()

输出结果如下:

图 5.55:前五行

图 5.55:前五行

在实际开始训练模型之前,我们将训练数据集进一步分割为训练集和验证集,比例为 70:30,以便稍后使用验证集公平地评估模型性能:

train_X, valid_X, \

train_y, valid_y = train_test_split(df[set(df.columns)\

-{'diagnosis'}], df.diagnosis, \

test_size=0.3, random_state=123)

将模型拟合到训练数据并检查相应的准确率:

model = DecisionTreeClassifier()

model = model.fit(train_X, train_y)

model.score(train_X, train_y)

输出结果如下:

1.0

我们的模型在训练集上达到了 100% 的准确率。

检查在测试集上的表现:

model.score(valid_X, valid_y)

输出准确率应小于 1,理想情况下为:

0.9415204678362573

决策树的一个优点是我们可以直观地表示模型并查看发生了什么。安装所需的依赖:

!conda install python-graphviz

导入绘图包:

导入 graphviz

来自 sklearn.tree 的 export_graphviz

绘制模型:

dot_data = export_graphviz(model, out_file=None)

graph = graphviz.Source(dot_data)

graph

输出结果如下:

图 5.56:CART 决策树的决策

图 5.56:CART 决策树的决策

此图展示了 scikit-learn 模型中 CART 决策树的决策过程。节点的第一行是该步骤所做的决策。第一个节点 X[1] <= 16.795,表示训练数据在第 1 列上按小于或等于 16.795 进行分割。那些第 1 列值小于 16.795 的样本(共 254 个)会进一步在第 25 列上进行划分。同样,第 1 列值大于等于 16.795 的样本(共 144 个)会在第 28 列上进一步划分。此决策/分支过程一直持续,直到达到终止条件。终止条件可以通过多种方式定义。以下是其中一些:

树已被遍历,所有终止叶节点已构建/找到。

纯度(衡量一个节点内元素所属不同类别的数量)在某一特定节点处低于给定阈值。

某一特定节点的元素数量低于阈值数量。

注意

要访问该部分的源代码,请参见 https://packt.live/31btfY5。

你也可以在 https://packt.live/37PJTO4 上在线运行此示例。你必须执行整个 Notebook 才能得到预期的结果。

在继续下一个主题之前,我们先使用 CART 决策树对 MNIST 数字数据集执行一个二分类任务。任务是将数字 0 和 1 的图像分类为数字(或类别)0 和 1。

活动 5.03:使用 CART 决策树进行二分类

在本次活动中,我们将使用 MNIST 数据集构建基于 CART 决策树的分类器,用于对两个数字进行分类:0 和 1。

要执行的步骤如下:

导入所需的依赖:

import struct

import numpy as np

import pandas as pd

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier

将 MNIST 数据加载到内存中。

可视化数据样本。

构建一个 CART 决策树分类器模型,用于分类数字 0 和 1。我们要创建的模型是确定样本是数字 0 还是 1。为此,我们首先需要选择这些样本。

使用数字 0 和数字 1 的一个样本图像,来可视化所选信息。

为了将图像信息提供给模型,我们必须首先将数据展平,使得每个图像变为 1 x 784 像素的形状。

构建模型;使用 DecisionTreeClassifier API 并调用 fit 函数。

确定训练集的准确率。

比较在测试集上的表现。

输出结果如下:

0.9962174940898345

注意

本次活动的解决方案可以通过此链接找到。

这里有一个有趣的点需要注意的是,测试集的表现比在活动 5.01:普通最小二乘分类器 – 二分类器中的表现要好得多。两个情况下的数据集完全相同,但模型不同。这表明基于 CART 决策树的模型在此二分类任务中比基于 OLS 的模型表现得更好。

现在我们已经了解了用于分类的决策树,接下来我们将讨论一种在工业界和学术界广泛使用的最流行且强大的机器学习模型之一——人工神经网络。

人工神经网络

我们将研究的最终分类模型类型是人工神经网络(ANNs)。首先,这种模型的灵感来自于人类大脑的功能。更具体来说,我们试图在数学上模拟互联神经元的结构,因此得名“神经网络”。本质上,人工神经网络架构看起来像图 5.57 所示:

图 5.57:神经网络架构示例

图 5.57:神经网络架构示例

极左侧是输入数据 X,扩展到 N0 个不同的特征维度。这个例子有两个隐藏层,h1 和 h2,分别有 N1 和 N2 个神经元。等等,什么是神经元?这个命名来源于类比人脑,人工神经网络中的神经元本质上是网络/图中的一个节点。最后,在图中,有输出层 Y,它包含了 N 个类别,用于多类分类任务的示例。图中的每一条箭头表示一个网络权重或参数。正如你所看到的,这些模型因此可能拥有大量的箭头/参数,这使得它们既复杂又强大。这些权重的作用方式是,例如,h11 是所有输入特征 x1, x2 … xN0 的加权和,通过激活函数处理后得出。

等等,什么是激活函数?在神经网络中,每个神经元或节点内部都有一个隐式的非线性函数。这个函数有助于让模型变得非线性(从而更加复杂),如果去掉这些非线性,那么几个隐藏层将会坍塌(由于一系列矩阵乘法的结果),最终变成一个极其简单的线性模型。这个线性模型意味着数据的输出类别可以表示为输入特征的加权和,而这在人工神经网络(ANN)中显然不是这样的。神经网络中常用的非线性激活函数有 sigmoid、tanh(双曲正切)和修正线性单元(ReLU)。事实上,如果我们使用 sigmoid 作为激活函数,且省略所有隐藏层,并将类别数限制为两个,我们就得到了以下的神经网络:

图 5.58: 无隐藏层的神经网络二分类器

图 5.58: 无隐藏层的神经网络二分类器

这看起来很熟悉吗?这个模型实际上和我们的逻辑回归模型完全相同!首先,我们取所有输入特征 x1, x2 …. xN0 的加权和,然后应用 sigmoid 或逻辑函数来获得最终输出。然后,这个输出与真实标签进行比较,计算损失。而且,类似于前一章讨论的线性回归模型,神经网络通过梯度下降来导出最优的权重或参数集,目的是最小化损失。尽管如此,由于神经网络模型比线性回归模型复杂得多,前者更新参数的方式远比后者精密,采用了叫做反向传播(backpropagation)的方法。反向传播的数学细节超出了本章的范围,但我们鼓励读者进一步阅读相关内容。

练习 5.09: 神经网络 – 多类分类器

神经网络可以用于多类分类任务,并不限于二分类。在这个练习中,我们将研究一个 10 类分类问题,换句话说,就是 MNIST 数字分类任务。加载 MNIST 训练和测试数据的过程与之前的练习完全相同:

导入所需的包:

import struct

import numpy as np

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.neural_network import MLPClassifier

加载训练/测试图像及其对应的标签:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read())).reshape((size, rows, cols))

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

可视化数据样本:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出将如下所示:

图 5.59:示例数据

图 5.59:示例数据

由于训练数据量非常大,我们将选择一个数据子集,以减少训练时间和所需的系统资源:

np.random.seed(0) # 设置一致的随机数

selection = np.random.choice(len(img), 5000)

selected_images = img[selection]

selected_labels = labels[selection]

再次将输入数据重塑为向量形式,以供后续使用:

selected_images = selected_images.reshape((-1, rows * cols))

selected_images.shape

输出将如下所示:

(5000, 784)

接下来,我们对图像数据进行归一化处理。我们将所有的图像值缩放到 0 到 1 之间。原始的灰度图像包含像素值在 0 到 255 之间(包括 0 和 255),其中 0 表示黑色,255 表示白色。归一化非常重要,因为它有助于梯度下降算法的有效运行。未经归一化的数据在权重更新过程中更容易出现梯度值衰减或爆炸,从而导致权重更新极小:

selected_images = selected_images / 255.0

img_test = img_test / 255.0

构建神经网络(或多层感知器)模型。此模型有几个额外的参数,如下所示:solver 的 sgd 值告诉模型使用随机梯度下降,并且需要额外的 max_iter 迭代才能收敛到解决方案。hidden_layer_sizes 参数本质上描述了模型的架构,换句话说,就是有多少隐藏层,每个隐藏层中有多少个神经元。例如,(20, 10, 5)表示 3 个隐藏层,分别有 20、10 和 5 个神经元。learning_rate_init 参数给出了梯度下降算法的初始学习率:

model = MLPClassifier(solver='sgd', hidden_layer_sizes=(100,), \

max_iter=1000, random_state=1, \

learning_rate_init=.01)

model.fit(X=selected_images, y=selected_labels)

输出将如下所示:

图 5.60:神经网络模型

图 5.60:神经网络模型

注意

有关参数的更多信息,请参阅文档:https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html#sklearn.neural_network.MLPClassifier

确定训练集的准确率得分:

model.score(X=selected_images, y=selected_labels)

输出将如下所示:

1.0

确定训练集的前两个预测并绘制图像以及对应的预测结果:

model.predict(selected_images)[:2]

输出将如下所示:

array([4, 1], dtype=uint8)

显示训练集中前两个样本的图像,查看我们的预测是否正确:

plt.subplot(1, 2, 1)

plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray');

plt.axis('off');

plt.subplot(1, 2, 2)

plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray');

plt.axis('off');

输出将如下所示:

 图 5.61:来自训练数据集的样本图像

图 5.61:来自训练数据集的样本图像

再次打印出模型为训练集的第一个样本提供的概率分数。确认每个类别都有 10 个不同的值,分别对应 10 个类别:

model.predict_proba(selected_images)[0]

输出将如下所示:

图 5.62:预测类别概率数组

图 5.62:预测类别概率数组

注意,在第一个样本的概率数组中,第五个(数字 4)数字的概率是最高的,因此预测为 4。

计算模型在测试集上的准确率。这将提供模型在实际应用中的合理表现估计,因为它从未见过测试集中的数据。预计测试集的准确率会略低于训练集,因为模型没有接触过这些数据:

model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)

输出将如下所示:

0.9384

如果将这些神经网络模型的训练集和测试集得分(1 和 0.9384)与在练习 5.03《逻辑回归——多类分类器》中获得的逻辑回归模型得分(0.986 和 0.9002)进行比较,可以看到神经网络模型的表现超出了逻辑回归模型的预期。这是因为神经网络需要学习的参数比逻辑回归模型多得多,导致神经网络更复杂,从而更强大。相反,如果我们构建一个没有隐藏层且使用 sigmoidal 激活函数的神经网络二分类器,它本质上就与逻辑回归模型相同。

注意

要访问此特定部分的源代码,请参见 https://packt.live/2NjfiyX。

你也可以在 https://packt.live/3dowv4z 上在线运行这个示例。你必须执行整个笔记本以获得期望的结果。

在我们结束本章之前,让我们通过神经网络完成最后一个分类任务,这次我们将使用乳腺癌诊断分类数据集。

活动 5.04:使用人工神经网络进行乳腺癌诊断分类

在本活动中,我们将使用乳腺癌诊断数据集(可通过 https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic) 或 GitHub 上的 https://packt.live/3a7oAY8 获取)。该数据集是 UCI 机器学习库的一部分(https://archive.ics.uci.edu/ml/index.php)。该数据集包含数字化图像中乳腺肿块细针穿刺(FNA)样本中细胞核的特征,并为每个细胞核标注了恶性和良性标签。特征包括 30 个项目,如细胞核的平均半径、半径误差、最差半径、平均纹理、纹理误差和最差纹理等。在本活动中,我们将使用数据集中提供的特征来区分恶性细胞和良性细胞。

执行的步骤如下:

导入所需的包。对于本活动,我们需要 pandas 包来加载数据,matplotlib 包来绘图,scikit-learn 包来创建神经网络模型,并且将数据集划分为训练集和测试集。导入所有所需的包和相关模块以完成这些任务:

导入 pandas 作为 pd

导入 matplotlib.pyplot 作为 plt

从 sklearn.neural_network 导入 MLPClassifier

从 sklearn.model_selection 导入 train_test_split

从 sklearn 导入 preprocessing

使用 pandas 加载乳腺癌诊断数据集,并检查前五行数据。

下一步是特征工程。这个数据集的不同列具有不同的量级,因此,在构建和训练神经网络模型之前,我们需要对数据集进行归一化处理。为此,我们使用 sklearn 的 MinMaxScaler API,它将每列的值归一化到 0 和 1 之间,正如本章的逻辑回归部分所讨论的那样(见练习 5.03,逻辑回归——多类分类器):https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html。

在我们构建模型之前,必须首先将诊断值转换为模型中可以使用的标签。将良性诊断字符串替换为值 0,将恶性诊断字符串替换为值 1。

此外,为了公正地评估模型,我们应该将训练数据集分为训练集和验证集。

使用归一化数据集和分配的诊断标签来创建模型。

计算模型在验证集上的准确性。

输出将类似于以下内容:

0.9824561403508771

注意

这个活动的解决方案可以通过此链接找到。

如果我们将这个验证集的准确度结果与活动 5.02:KNN 多类分类器的结果进行比较,我们会发现人工神经网络在相同数据集上比逻辑回归模型表现得更好。这也是预期的,因为前者是比后者更复杂且更强大的机器学习模型。

总结

本章中我们涵盖了多个强大且极其有用的分类模型,从使用 OLS 作为分类器开始,然后我们通过使用逻辑回归分类器观察到性能的显著提升。接着我们进入了记忆型模型,如 KNN,虽然 KNN 模型易于拟合,但它能够在分类过程中形成复杂的非线性边界,即便是将图像作为输入信息也能做到。随后,我们讨论了决策树和 ID3 算法。我们看到,像 KNN 模型一样,决策树通过规则来记忆训练数据,从而以相当高的准确度做出预测。最后,我们以其中一个最强大的分类模型——人工神经网络,结束了对分类问题的介绍。我们简要介绍了前馈神经网络的基础知识,并通过一个练习展示了它在分类任务中优于逻辑回归模型的表现。

在下一章中,我们将扩展本章所学的内容,涵盖集成技术,包括提升方法,以及非常有效的随机森林模型。

第七章:6. 集成建模

概述

本章讨论了执行集成建模的不同方法及其优缺点。通过本章内容的学习,你将能够识别机器学习模型中的欠拟合和过拟合问题。你还将能够使用决策树设计一个袋装分类器,并实现自适应提升和梯度提升模型。最后,你将能够利用多个分类器构建一个堆叠集成模型。

介绍

在前面的章节中,我们讨论了两种类型的监督学习问题:回归和分类。我们研究了每种类型的若干算法,并深入了解了这些算法是如何工作的。

但有时这些算法,无论多么复杂,似乎都无法在我们拥有的数据上表现良好。这可能有多种原因——也许数据不够好,也许我们试图找到的趋势根本不存在,或者模型本身过于复杂。

等等,什么?!模型过于复杂怎么会是问题?如果一个模型过于复杂,并且数据不足,那么模型可能会非常拟合数据,甚至学习到噪声和异常值,这正是我们不希望发生的。

通常情况下,当一个复杂的单一算法给出的结果与实际结果相差甚远时,将多个模型的结果进行聚合可以得到一个更接近实际情况的结果。这是因为考虑所有模型的结果时,单个模型的误差很可能会相互抵消。

将多个算法组合在一起以给出综合预测的这种方法是集成建模的基础。集成方法的最终目标是以某种方式将多个表现不佳的基础估计器(即单个算法)组合起来,从而提高系统的整体性能,并使得算法的集成模型比单个算法更为健壮,能够更好地进行泛化。

本章的前半部分,我们将讨论如何通过构建集成模型来帮助我们建立一个健壮的系统,该系统能够做出准确的预测,而不会增加方差。我们将首先讨论模型表现不佳的一些原因,然后介绍偏差和方差的概念,以及过拟合和欠拟合问题。我们将把集成建模作为解决这些性能问题的一种方案,并讨论可用于解决不同类型问题的集成方法,尤其是针对表现不佳的模型。

我们将讨论三种集成方法;即:bagging、boosting 和 stacking。每种方法将从基本理论讲起,并讨论每种方法适合解决的用例,以及哪些用例可能不适合该方法。我们还将通过一些练习,使用 Python 中的 scikit-learn 库来实现这些模型。

在深入探讨这些主题之前,我们首先要熟悉一个数据集,使用这个数据集来演示和理解本章将要涉及的不同概念。接下来的练习将帮助我们做到这一点。在进入练习之前,有必要先了解一下独热编码的概念。

独热编码

那么,什么是独热编码呢?在机器学习中,我们有时会遇到分类输入特征,比如姓名、性别和颜色。这些特征包含的是标签值,而非数值,例如姓名中的 John 和 Tom,性别中的 male 和 female,颜色中的 red、blue 和 green。在这里,blue 就是分类特征——颜色的一个标签。所有机器学习模型都可以处理数值数据,但许多机器学习模型无法处理分类数据,因为它们的基础算法设计方式不支持这种数据。例如,决策树可以处理分类数据,但逻辑回归不能。

为了在使用像逻辑回归这样的模型时仍能利用分类特征,我们将这些特征转化为可用的数值格式。图 6.1 显示了这种转换的样子:

图 6.1:独热编码

图 6.1:独热编码

图 6.2 显示了应用独热编码后数据集的变化:

图 6.2:应用独热编码

图 6.2:应用独热编码

基本上,在这个例子中,有三种颜色类别——红色、蓝色和绿色,因此需要三个二元变量——color_red、color_blue 和 color_green。使用 1 来表示该颜色的二元变量,其他颜色使用 0 表示。这些二元变量——color_red、color_blue 和 color_green——也被称为虚拟变量。有了这些信息,我们可以继续进行我们的练习。

练习 6.01:导入模块并准备数据集

在这个练习中,我们将导入本章所需的所有模块,并将数据集整理好,以便进行接下来的练习:

导入所有需要的模块来操作数据和评估模型:

import pandas as pd

import numpy as np

%matplotlib inline

import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split

from sklearn.metrics import accuracy_score

from sklearn.model_selection import KFold

我们在本次练习中使用的数据集是 Titanic 数据集,它在前几章中已经介绍过。

读取数据集并打印前五行:

data = pd.read_csv('titanic.csv')

data.head()

注意

上述代码假设数据集存储在与本次练习的 Jupyter Notebook 相同的文件夹中。如果数据集保存在 Datasets 文件夹中,则需要使用以下代码:data = pd.read_csv('../Datasets/titanic.csv')

输出如下:

图 6.3: 前五行

图 6.3: 前五行

为了使数据集准备好使用,我们将添加一个 preprocess 函数,该函数将预处理数据集,并将其转换为 scikit-learn 库可以接受的格式。

首先,我们创建一个 fix_age 函数来预处理年龄列并获取整数值。如果年龄为 null,该函数返回 -1,以区分其他有效值,否则返回实际值。然后,我们将此函数应用于年龄列。

接着,我们将性别(Gender)列转换为二元变量,女性为 1,男性为 0,然后使用 pandas 的 get_dummies 函数为登船港口(Embarked)列创建虚拟二元列。之后,我们将包含虚拟列的 DataFrame 与其余的数值列结合,生成最终的 DataFrame,该函数将返回该 DataFrame:

def preprocess(data):

def fix_age(age):

if np.isnan(age):

return -1

else:

return age

data.loc[:, 'Age'] = data.Age.apply(fix_age)

data.loc[:, 'Gender'] = data.Gender.apply(lambda s: \

int(s == 'female'))

embarked = pd.get_dummies(data.Embarked, \

prefix='Emb')[['Emb_C',\

'Emb_Q','Emb_S']]

cols = ['Pclass','Gender','Age','SibSp','Parch','Fare']

return pd.concat([data[cols], embarked], axis=1).values

将数据集拆分为训练集和验证集。

我们将数据集拆分为两部分——一个用于在练习过程中训练模型(train),另一个用于进行预测并评估每个模型的性能(val)。我们将使用之前编写的函数分别预处理训练集和验证集数据。

在这里,Survived 二元变量是目标变量,用于确定每行数据中的个体是否在泰坦尼克号沉船中幸存。因此,我们将创建 y_trainy_val 作为从两个数据子集拆分出的因变量列:

train, val = train_test_split(data, test_size=0.2, random_state=11)

x_train = preprocess(train)

y_train = train['Survived'].values

x_val = preprocess(val)

y_val = val['Survived'].values

print(x_train.shape)

print(y_train.shape)

print(x_val.shape)

print(y_val.shape)

您应该得到如下输出:

(712, 9)

(712,)

(179, 9)

(179,)

如我们所见,数据集现在已分为两部分,训练集包含 712 个数据点,验证集包含 179 个数据点。

注意

要访问该特定部分的源代码,请参阅 packt.live/2Nm6KHM

您也可以在线运行此示例,访问 packt.live/2YWh9zg。您必须执行整个 Notebook 才能获得预期结果。

在这个练习中,我们首先加载了数据并导入了必要的 Python 模块。然后,我们对数据集的不同列进行了预处理,使其可以用于训练机器学习模型。最后,我们将数据集分为两个子集。现在,在对数据集进行进一步操作之前,我们将尝试理解机器学习中的两个重要概念——过拟合和欠拟合。

过拟合与欠拟合

假设我们将一个有监督学习算法拟合到我们的数据上,然后使用该模型对一个独立的验证集进行预测。根据模型的泛化能力——即它对独立验证集中的数据点进行预测的能力——来评估其性能。

有时候,我们会发现模型无法做出准确的预测,并且在验证数据上表现较差。这种差劲的表现可能是因为模型过于简单,无法适当地建模数据,或者模型过于复杂,无法对验证数据集进行泛化。在前一种情况下,模型具有高偏差,导致欠拟合;在后一种情况下,模型具有高方差,导致过拟合。

偏差

机器学习模型预测中的偏差表示预测的目标值与数据点的真实目标值之间的差异。如果平均预测值与真实值相差较远,则模型被认为具有高偏差;反之,如果平均预测值接近真实值,则模型被认为具有低偏差。

高偏差表示模型无法捕捉数据中的复杂性,无法识别输入和输出之间的相关关系。

方差

机器学习模型预测中的方差表示预测值与真实值之间的分散程度。如果预测值分散且不稳定,则模型被认为具有高方差;反之,如果预测值一致且不太分散,则模型被认为具有低方差。

高方差表明模型无法泛化,并且在模型之前未见过的数据点上无法做出准确的预测。如图所示,这些圆的中心表示数据点的真实目标值,圆点表示数据点的预测目标值:

图 6.4:具有高偏差和低偏差的数据显示点的视觉表示

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-9XB2XBIW.jpg)

图 6.4:具有高偏差和低偏差的数据显示点的视觉表示

欠拟合

假设我们在训练数据集上拟合一个简单的模型,比如一个简单的线性模型。我们拟合了一个能够在一定程度上表示训练数据中 X(输入数据)和 Y(目标输出)数据点之间关系的函数,但我们发现训练误差依然很高:

图 6.5:回归中的欠拟合与理想拟合

图 6.5:回归中的欠拟合与理想拟合

例如,看看图 6.5 中显示的两张回归图。第一张图展示了一个将直线拟合到数据的模型,而第二张图展示了一个尝试将相对复杂的多项式拟合到数据的模型,它似乎很好地表示了 X 和 Y 之间的映射关系。

如果我们仔细观察第一个模型(图中左侧),直线通常远离个别数据点,而第二个模型中的数据点则紧密贴合曲线。根据我们在上一节中定义的偏差,我们可以说第一个模型具有较高的偏差。而如果参考模型的方差定义,第一个模型的预测相对一致,因为它对给定输入总是预测一个固定的直线输出。因此,第一个模型具有较低的方差,我们可以说它展示了欠拟合的特征,因为它具有高偏差和低方差的特征;也就是说,尽管它无法捕捉到输入与输出之间的复杂映射,但它在预测上保持一致。该模型在训练数据和验证数据上的预测误差较大。

过拟合

假设我们训练了一个非常复杂的模型,它几乎能够完美地预测训练数据集上的数据。我们已经成功地拟合了一个函数来表示训练数据中 X 和 Y 数据点之间的关系,使得训练数据上的预测误差极低:

图 6.6:回归中的理想拟合与过拟合

图 6.6:回归中的理想拟合与过拟合

通过查看图 6.6 中的两张图,我们可以看到第二张图展示了一个试图将一个高度复杂的函数拟合到数据点上的模型,而左侧的图表示了给定数据的理想拟合。

很明显,当我们尝试使用第二张图中的模型预测在训练集未出现的 X 数据点对应的 Y 值时,我们会发现预测值与真实值相差甚远。这就是过拟合的表现,模型对数据的拟合过于完美,以至于无法对新数据点进行泛化,因为模型学到了训练数据中的随机噪声和离群点。该模型表现出高方差和低偏差的特征:尽管平均预测值与真实值接近,但与真实值相比,它们的分散性较大。

过拟合可能发生的另一种情况是当数据点的数量小于或等于我们尝试拟合到模型的多项式的阶数时。因此,我们应该避免使用以下类型的模型:

多项式的阶数 > 数据点的数量

在数据集极其小的情况下,甚至尝试拟合一个简单的模型也可能导致过拟合。

克服欠拟合与过拟合的问题

从前面的章节中我们可以看到,当我们从过于简单的模型走向过于复杂的模型时,我们会从一个具有高偏差、低方差的欠拟合模型过渡到一个具有低偏差、高方差的过拟合模型。监督学习算法的目标是实现低偏差和低方差,达到欠拟合与过拟合之间的平衡。这也有助于算法从训练数据到验证数据的良好泛化,从而在模型从未见过的数据上表现出色。

当模型欠拟合数据时,提高模型复杂度是改善性能的最佳方式,从而识别数据中的相关关系。这可以通过添加新特征或创建高偏差模型的集成来实现。然而,在这种情况下,增加更多的训练数据并不会有帮助,因为限制因素是模型复杂度,更多的数据并不能减少模型的偏差。

然而,过拟合问题更难解决。以下是一些常用的技术,用于克服过拟合带来的问题:

获取更多数据:一个高度复杂的模型很容易在小数据集上发生过拟合,但在更大的数据集上则不容易发生。

降维:减少特征数量有助于使模型更简单。

正则化:在成本函数中添加一个新的项,以调整系数(特别是在线性回归中,高次项系数)向较低的值逼近。

集成建模:聚合多个过拟合模型的预测可以有效消除预测中的高方差,表现得比单个过拟合训练数据的模型更好。

我们将在本章的后续部分详细讨论集成建模技术。常见的集成类型包括:

袋装法:Bootstrap 聚合法的简称,该技术也用于减少模型的方差并避免过拟合。它涉及一次取出特征和数据点的一个子集,在每个子集上训练一个模型,并随后将所有模型的结果聚合成最终的预测。

提升法:该技术用于减少偏差,而不是减少方差,涉及逐步训练新的模型,重点关注前一个模型中分类错误的数据点。

堆叠法:该技术的目标是提高分类器的预测能力,因为它涉及训练多个模型,然后使用组合算法通过利用所有这些模型的预测输入来进行最终预测。

让我们从袋装法开始,然后再介绍提升法和堆叠法。

袋装法

术语“bagging”来源于一种叫做自助法聚合(bootstrap aggregation)的方法。为了实现一个成功的预测模型,了解在什么情况下我们可以通过使用自助法方法来构建集成模型是很重要的。这类模型在工业界和学术界都得到了广泛应用。

其中一个应用是,这些模型可以用于维基百科文章的质量评估。诸如文章长度、参考文献数量、标题数量和图片数量等特征被用来构建分类器,将维基百科文章分为低质量或高质量文章。在为此任务尝试的多个模型中,随机森林模型——一种我们将在下一节讨论的基于 bagging 的集成分类器——优于其他所有模型,如 SVM、逻辑回归甚至神经网络,其最佳精度和召回率分别为 87.3%和 87.2%。这展示了这类模型的强大能力以及它们在实际应用中的潜力。

在这一节中,我们将讨论如何使用自助法方法创建一个最小化方差的集成模型,并看看我们如何构建一个决策树集成模型,也就是随机森林算法。但什么是自助法,它如何帮助我们构建稳健的集成模型呢?

自助法

自助法方法本质上是指从包含随机选择数据点的数据集中绘取多个样本(每个样本称为重采样),其中每个重采样中的数据点可能会有重叠,并且每个数据点被从整个数据集中选择的概率是相等的:

图 6.7:随机选择数据点

图 6.7:随机选择数据点

从前面的图示中,我们可以看到,从主数据集中取出的五个重采样样本各不相同,并具有不同的特征。因此,在这些重采样上训练模型将会得出不同的预测结果。

以下是自助法的优点:

每个重采样可能包含与整个数据集不同的特征,从而为我们提供了不同的视角来观察数据的行为。

使用自助法的算法具有强大的构建能力,能够更好地处理未见过的数据,尤其是在较小的数据集上,这类数据集往往会导致过拟合。

自助法方法可以通过使用具有不同变异性和特征的数据集测试模型的稳定性,从而得出一个更加稳健的模型。

现在我们已经了解了什么是自助抽样,那么袋装集成究竟是做什么的呢?简单来说,袋装意味着聚合并行模型的输出,每个模型都是通过自助抽样数据构建的。它本质上是一个集成模型,在每次重采样时生成多个预测器版本,并利用这些版本得到一个聚合的预测器。聚合步骤为我们提供了一个元预测,这个过程涉及对回归问题时的连续数值预测进行求平均,而在分类问题中,则是进行投票。投票可以分为两种类型:

硬投票(基于类别的投票)

软投票(概率投票)

在硬投票中,我们考虑由基础估计器预测的各个类别的多数意见,而在软投票中,我们首先平均各类别的概率值,然后再做出预测。

下图为我们提供了一个可视化的示意,展示了如何通过图 6.7 中所示的自助抽样构建袋装估计器:

![图 6.8:通过自助抽样构建的袋装估计器图 6.8:通过自助抽样构建的袋装估计器

图 6.8:通过自助抽样构建的袋装估计器

由于每个模型本质上是独立的,因此所有基础模型可以并行训练,这大大加快了训练过程,因为重采样的数据集比原始数据集小,从而使我们能够充分利用当前计算能力。

袋装本质上有助于减少整个集成的方差。它通过引入随机化到其构建过程中来实现这一点,通常用于那些容易对训练数据过拟合的基础预测器。在这里需要考虑的主要因素是训练数据集的稳定性(或缺乏稳定性):袋装在数据轻微扰动会导致模型结果显著变化的情况下表现有效,也就是高方差的模型。这就是袋装如何帮助减少方差的方式。

scikit-learn 使用 BaggingClassifier 和 BaggingRegressor 来分别实现用于分类和回归任务的通用袋装集成方法。这些方法的主要输入是用于每次重采样的基础估计器,以及要使用的估计器数量(即重采样次数)。

练习 6.02:使用袋装分类器

在这个练习中,我们将使用 scikit-learn 的袋装分类器作为我们的集成方法,并使用决策树分类器作为基础估计器。我们知道决策树容易发生过拟合,因此它们具有高方差和低偏差,这两者都是用于袋装集成中的基础估计器的重要特征。

我们将在这个练习中使用 Titanic 数据集。请在开始本练习之前,先完成练习 6.01:导入模块并准备数据集:

导入基础分类器和集成分类器:

从 sklearn.tree 导入决策树分类器(DecisionTreeClassifier)

from sklearn.ensemble import BaggingClassifier

指定超参数并初始化模型。

在这里,我们将首先指定基础估计器的超参数,我们使用决策树分类器,并将熵或信息增益作为分裂准则。我们不会对树的深度或每棵树的叶子数量/大小设置任何限制,以便它完全生长。接下来,我们将为 Bagging 分类器定义超参数,并将基础估计器对象作为超参数传递给分类器。

我们将以 50 个基础估计器作为示例,这些估计器将并行运行,并利用机器上可用的所有进程(通过指定 n_jobs=-1 实现)。此外,我们将指定 max_samples 为 0.5,表示引导法中的数据点数量应为总数据集的一半。我们还将设置一个随机状态(为任意值,且在整个过程中保持不变)以确保结果的可重复性:

dt_params = {'criterion': 'entropy', 'random_state': 11}

dt = DecisionTreeClassifier(**dt_params)

bc_params = {'base_estimator': dt, 'n_estimators': 50, \

'max_samples': 0.5, 'random_state': 11, 'n_jobs': -1}

bc = BaggingClassifier(**bc_params)

将 Bagging 分类器模型拟合到训练数据上并计算预测准确率。

现在让我们拟合 Bagging 分类器,并找出训练集和验证集的元预测值。接下来,我们计算训练集和验证数据集的预测准确率:

bc.fit(x_train, y_train)

bc_preds_train = bc.predict(x_train)

bc_preds_val = bc.predict(x_val)

print('Bagging 分类器:\n> 训练数据上的准确率 = {:.4f}'\

'\n> 验证数据上的准确率 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=bc_preds_train),\

accuracy_score(y_true=y_val, y_pred=bc_preds_val)))

输出如下:

Bagging 分类器:

训练数据上的准确率 = 0.9270

验证数据上的准确率 = 0.8659

将决策树模型拟合到训练数据上,以比较预测准确率。

现在让我们也拟合决策树(来自步骤 2 中初始化的对象),这样我们就可以比较集成模型和基础预测器的预测准确率:

dt.fit(x_train, y_train)

dt_preds_train = dt.predict(x_train)

dt_preds_val = dt.predict(x_val)

print('决策树:\n> 训练数据上的准确率 = {:.4f}'\

'\n> 验证数据上的准确率 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=dt_preds_train),\

accuracy_score(y_true=y_val, y_pred=dt_preds_val)))

输出如下:

决策树:

训练数据上的准确率 = 0.9831

验证数据上的准确率 = 0.7709

在这里,我们可以看到,尽管决策树的训练准确率远高于袋装分类器,但其在验证数据集上的准确率较低,这清楚地表明决策树对训练数据进行了过拟合。而袋装集成方法则通过减少整体方差,产生了更为准确的预测。

注意

要访问这一特定部分的源代码,请参阅 https://packt.live/37O6735。

你也可以在 https://packt.live/2Nh3ayB 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。

接下来,我们将讨论可能是最广为人知的基于袋装的机器学习模型——随机森林模型。随机森林是一个袋装集成模型,使用决策树作为基础估计器。

随机森林

决策树常面临的一个问题是,在每个节点上的分割是通过贪婪算法完成的,旨在最小化叶节点的熵。考虑到这一点,袋装分类器中的基础估计器决策树在分割特征时仍然可能非常相似,因此它们的预测也可能非常相似。然而,袋装方法只有在基础模型的预测不相关时,才有助于减少预测的方差。

随机森林算法通过不仅对整体训练数据集中的数据点进行自助抽样,还对每棵树可分割的特征进行自助抽样,从而试图克服这个问题。这确保了当贪婪算法在寻找最佳分割特征时,整体最佳特征可能并不总是出现在基础估计器的自助抽样特征中,因此不会被选择——这导致基础树具有不同的结构。这个简单的调整使得最好的估计器能够以一种方式进行训练,从而使得森林中每棵树的预测与其他树的预测之间的相关性较低。

随机森林中的每个基础估计器都有一个数据点的随机样本和一个特征的随机样本。由于该集成由决策树组成,因此该算法被称为随机森林。

练习 6.03:使用随机森林构建集成模型

随机森林的两个主要参数是用于训练每个基础决策树的特征的分数和数据点的分数。

在本次练习中,我们将使用 scikit-learn 的随机森林分类器来构建集成模型。

本次练习中,我们将使用泰坦尼克号数据集。这个练习是练习 6.02《使用袋装分类器》的延续:

导入集成分类器:

来自 sklearn.ensembleRandomForestClassifier

指定超参数并初始化模型。

在这里,我们将使用熵(entropy)作为决策树的分裂标准,森林中包含 100 棵树。与之前一样,我们不会指定树的深度或叶子节点的大小/数量的限制。与袋装分类器不同,袋装分类器在初始化时需要输入 max_samples,而随机森林算法只需要 max_features,表示自助法样本中的特征数量(或比例)。我们将此值设置为 0.5,以便每棵树只考虑六个特征中的三个:

rf_params = {'n_estimators': 100, 'criterion': 'entropy', \

'max_features': 0.5, 'min_samples_leaf': 10, \

'random_state': 11, 'n_jobs': -1}

rf = RandomForestClassifier(**rf_params)

将随机森林分类器模型拟合到训练数据,并计算预测准确度。

现在,我们将拟合随机森林模型,并找到训练集和验证集的元预测。接下来,找出训练和验证数据集的预测准确度:

rf.fit(x_train, y_train)

rf_preds_train = rf.predict(x_train)

rf_preds_val = rf.predict(x_val)

print('随机森林:\n> 训练数据集准确度 = {:.4f}'\

'\n> 验证数据集准确度 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=rf_preds_train), \

accuracy_score(y_true=y_val, y_pred=rf_preds_val)))

输出如下:

随机森林:

训练数据集准确度 = 0.8385

验证数据集准确度 = 0.8771

如果将随机森林在我们的数据集上的预测准确度与袋装分类器进行比较,我们会发现验证集上的准确度几乎相同,尽管后者在训练数据集上的准确度较高。

注意

若要访问该特定部分的源代码,请参阅 https://packt.live/3dlvGtd。

你也可以在 https://packt.live/2NkSPS5 上在线运行此示例。你必须执行整个 Notebook 才能获得预期结果。

提升

我们接下来要讨论的第二种集成技术是提升(boosting),它通过逐步训练新模型来集中关注上一模型中被误分类的数据点,并利用加权平均将弱模型(具有较高偏差的欠拟合模型)转化为更强的模型。与袋装方法不同,袋装方法中的每个基础估计器可以独立训练,而提升算法中的每个基础估计器的训练依赖于前一个模型。

尽管提升(boosting)也使用了自助法(bootstrapping)概念,但与袋装(bagging)不同,它的实现方式不同,因为每个数据样本都有权重,这意味着某些自助法样本在训练中可能会比其他样本更频繁地被使用。在训练每个模型时,算法会跟踪哪些特征最有用,以及哪些数据样本的预测误差最大;这些样本会被赋予更高的权重,并被认为需要更多的迭代才能正确训练模型。

在预测输出时,提升集成会对每个基础估计器的预测结果进行加权平均,给训练阶段错误较低的预测结果赋予更高的权重。这意味着,对于在某次迭代中被错误分类的数据点,这些数据点的权重会被增加,从而使下一个模型更可能正确分类这些数据点。

与袋装法(bagging)类似,所有提升(boosting)基础估计器的结果被汇总以产生一个元预测。然而,与袋装法不同,提升集成的准确性会随着基础估计器数量的增加而显著提高:

图 6.9:一个提升集成模型

图 6.9:一个提升集成模型

在图示中,我们可以看到,在每次迭代之后,被错误分类的点的权重(以较大的图标表示)增加,以便下一个训练的基础估计器能够集中关注这些点。最终的预测器将每个基础估计器的决策边界进行汇总。

提升在实际应用中被广泛使用。例如,商业化的网页搜索引擎 Yahoo 和 Yandex 在其机器学习排名引擎中使用了提升的变体。排名是根据搜索查询找到最相关文档的任务。特别是对于 Yandex 来说,他们采用了一种基于梯度提升(gradient boosting)的方法来构建集成树模型,该模型通过实现最低的折扣累积增益 4.14123,比其他模型,包括 Yandex 之前使用的模型表现得更好。这显示了基于提升的建模在现实场景中的巨大实用价值。

注意

阅读更多关于 Yandex 的信息,请访问以下链接: http://webmaster.ya.ru/replies.xml?item_no=5707&ncrnd=5118.

自适应提升

现在让我们谈谈一种叫做自适应提升(adaptive boosting)的技术,它最适用于提升决策树桩在二分类问题中的表现。决策树桩本质上是深度为一的决策树(仅对单一特征进行一次分裂),因此是弱学习器。自适应提升工作的主要原理与此相同:通过提高基础估计器无法正确分类的区域,将一组弱学习器转化为强学习器。

首先,首个基学习器从主训练集抽取一个自助法的数据点,并拟合一个决策桩来分类这些样本点,接着,训练过的决策桩会拟合整个训练数据。对于那些被错误分类的样本,增加权重,使这些数据点在下一个基学习器的自助法抽样中被选中的概率更高。然后,再次对新抽样的数据点进行训练,进行分类。接着,由这两个基学习器组成的小型集成模型被用来分类整个训练集中的数据点。第二轮中被错误分类的数据点会被赋予更高的权重,以提高它们被选中的概率,直到集成模型达到它应该包含的基学习器数量的限制。

自适应提升的一个缺点是,算法很容易受到噪声数据点和异常值的影响,因为它尝试完美拟合每一个数据点。因此,如果基学习器数量过多,它容易出现过拟合。

练习 6.04:实现自适应提升

在这个练习中,我们将使用 scikit-learn 的自适应提升算法进行分类,即 AdaBoostClassifier:

我们将再次使用 Titanic 数据集。这个练习是练习 6.03《使用随机森林构建集成模型》的延续:

导入分类器:

from sklearn.ensemble import AdaBoostClassifier

指定超参数并初始化模型。

在这里,我们首先指定基学习器的超参数,使用的是一个最大深度为 1 的决策树分类器,即一个决策桩。接着,我们会定义 AdaBoost 分类器的超参数,并将基学习器对象作为超参数传递给分类器:

dt_params = {'max_depth': 1, 'random_state': 11}

dt = DecisionTreeClassifier(**dt_params)

ab_params = {'n_estimators': 100, 'base_estimator': dt, \

'random_state': 11}

ab = AdaBoostClassifier(**ab_params)

将模型拟合到训练数据上。

现在,让我们拟合 AdaBoost 模型,并计算训练集和验证集的元预测。接着,我们来计算训练集和验证集的数据集上的预测准确度:

ab.fit(x_train, y_train)

ab_preds_train = ab.predict(x_train)

ab_preds_val = ab.predict(x_val)

print('自适应提升:\n> 训练数据的准确率 = {:.4f}'\

'\n> 验证数据的准确率 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=ab_preds_train), \

accuracy_score(y_true=y_val, y_pred=ab_preds_val)

))

输出结果如下:

自适应提升:

训练数据的准确率 = 0.8272

验证数据的准确率 = 0.8547

计算模型在不同数量的基学习器下,对训练数据和验证数据的预测准确度。

之前我们提到过,准确率随着基本估计器数量的增加而增加,但如果使用的基本估计器过多,模型也有可能出现过拟合。现在让我们计算预测准确率,以便找出模型开始过拟合训练数据的点:

ab_params = {'base_estimator': dt, 'random_state': 11}

n_estimator_values = list(range(10, 210, 10))

train_accuracies, val_accuracies = [], []

对于 n_estimators 中的每个值:

ab = AdaBoostClassifier(n_estimators=n_estimators, **ab_params)

ab.fit(x_train, y_train)

ab_preds_train = ab.predict(x_train)

ab_preds_val = ab.predict(x_val)

train_accuracies.append(accuracy_score(y_true=y_train, \

y_pred=ab_preds_train))

val_accuracies.append(accuracy_score(y_true=y_val, \

y_pred=ab_preds_val))

绘制折线图,以可视化训练和验证数据集上预测准确率的趋势:

plt.figure(figsize=(10,7))

plt.plot(n_estimator_values, train_accuracies, label='训练')

plt.plot(n_estimator_values, val_accuracies, label='验证')

plt.ylabel('准确率')

plt.xlabel('n_estimators')

plt.legend()

plt.show()

输出结果如下:

图 6.10:预测准确率的趋势

图 6.10:预测准确率的趋势

正如前面提到的,我们可以看到训练准确率几乎始终随着决策树桩数量的增加(从 10 到 200)而不断提高。然而,验证准确率在 0.84 到 0.86 之间波动,并且随着决策树桩数量的增加而开始下降。这是因为 AdaBoost 算法也在尝试拟合噪声数据点和异常值。

注意

要访问此特定部分的源代码,请参阅 https://packt.live/2V4zB7K。

您也可以在线运行此示例,网址为 https://packt.live/3dhSBpu。您必须执行整个 Notebook 才能获得预期的结果。

梯度提升

梯度提升是提升方法的扩展,它将提升视为一个优化问题。定义了一个损失函数,代表了误差残差(预测值与真实值之间的差异),并使用梯度下降算法来优化损失函数。

在第一步中,添加一个基本估计器(即弱学习器),并在整个训练数据集上进行训练。计算预测的损失,并为减少误差残差,更新损失函数,添加更多的基本估计器,来处理现有估计器表现不佳的数据点。接下来,算法迭代地添加新的基本估计器并计算损失,使得优化算法能够更新模型并最小化残差。

在自适应提升的情况下,决策树桩被用作基础估计器的弱学习器。然而,对于梯度提升方法,可以使用更大的树,但弱学习器仍然需要受到限制,通过设置最大层数、节点数、分裂数或叶节点数的限制。这确保了基础估计器仍然是弱学习器,但它们可以以贪婪的方式构建。

从之前的章节我们知道,梯度下降算法可以用来最小化一组参数,比如回归方程中的系数。然而,在构建集成时,我们使用的是决策树,而不是需要优化的参数。在每一步计算损失后,梯度下降算法必须调整将要加入集成的新树的参数,以减少损失。这种方法更常被称为功能梯度下降。

练习 6.05:实现 GradientBoostingClassifier 来构建集成模型

梯度提升分类器的两个主要参数是用于训练每棵基础决策树的特征的比例和需要进行自助抽样的数据点比例。

在这个练习中,我们将使用 scikit-learn 的梯度提升分类器来构建提升集成模型。

这个练习是练习 6.04:实现自适应提升的延续。

导入集成分类器:

来自 sklearn.ensemble 的导入 GradientBoostingClassifier

指定超参数并初始化模型。

在这里,我们将使用 100 棵决策树作为基础估计器,每棵树的最大深度为 3,并且每个叶节点至少有 5 个样本。尽管我们没有像之前的示例那样使用决策树桩,但树仍然很小,可以认为是一个弱学习器:

gbc_params = {'n_estimators': 100, 'max_depth': 3, \

'min_samples_leaf': 5, 'random_state': 11}

gbc = GradientBoostingClassifier(**gbc_params)

将梯度提升模型拟合到训练数据,并计算预测准确率。

现在,让我们拟合集成模型并找到训练集和验证集的元预测。接下来,我们将计算训练集和验证集上的预测准确率:

gbc.fit(x_train, y_train)

gbc_preds_train = gbc.predict(x_train)

gbc_preds_val = gbc.predict(x_val)

print('梯度提升分类器:'\

'\n> 训练数据的准确率 = {:.4f}'\

'\n> 验证数据的准确率 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=gbc_preds_train), \

accuracy_score(y_true=y_val, y_pred=gbc_preds_val)))

输出如下:

梯度提升分类器:

训练数据上的准确率 = 0.8961

验证数据上的准确率 = 0.8771

注意

要访问此特定部分的源代码,请参阅 https://packt.live/37QANjZ。

你也可以在线运行这个示例,网址是 https://packt.live/2YljJ2D。你必须执行整个 Notebook 才能获得期望的结果。

我们可以看到,梯度提升集成模型在训练和验证数据集上的准确度都高于自适应提升集成模型。

堆叠

堆叠(Stacking)或堆叠泛化,也叫元集成(meta ensembling),是一种模型集成技术,旨在将多个模型的预测结果合并并作为特征,生成一个新的模型。堆叠模型通常会超越每个单独模型的表现,因为它加入了平滑效应,并且能“选择”在某些情境下表现最好的基础模型。考虑到这一点,堆叠通常在每个基础模型彼此之间有显著差异时最为有效。

堆叠在实际应用中被广泛使用。一个著名的例子来自著名的 Netflix 竞赛,两位顶尖参赛者基于堆叠模型构建了解决方案。Netflix 是一个知名的流媒体平台,这场竞赛的内容是构建最好的推荐引擎。获胜算法基于特征加权线性堆叠(feature-weighted-linear-stacking),其基本上是通过从单个模型/算法(如奇异值分解(SVD)、限制玻尔兹曼机(RBM)和 K 最近邻(KNN))中提取元特征。例如,一个元特征是 60 因子序数 SVD 的标准差。这些元特征被认为是实现获胜模型所必需的,证明了堆叠在实际应用中的强大能力。

堆叠使用基础模型的预测作为训练最终模型时的附加特征——这些被称为元特征。堆叠模型本质上充当一个分类器,确定每个模型在何处表现良好,何处表现较差。

然而,不能简单地将基础模型在完整的训练数据上训练,在完整的验证数据集上生成预测,然后将这些预测用于二级训练。这么做有可能导致你的基础模型预测已经“看到”测试集,因此在输入这些预测时会发生过拟合。

需要注意的是,对于每一行的元特征,其值不能使用包含该行的训练数据中的模型进行预测,因为这样我们就有可能发生过拟合,因为基础预测模型已经“看到”了该行的目标变量。常见的做法是将训练数据分成 k 个子集,这样,在为每个子集寻找元特征时,我们只在剩余数据上训练模型。这样做还避免了模型已经“看到”数据而发生过拟合的问题:

图 6.11:堆叠集成

图 6.11:堆叠集成

上面的图示展示了如何执行此操作:我们将训练数据划分为 k 个子集,并通过在其余 k-1 个子集上训练模型来找到每个子集上的基础模型预测值。因此,一旦我们获得了每个子集的元预测值,我们就可以将这些元预测值与原始特征一起用于训练堆叠模型。

练习 6.06:构建堆叠模型

在这个练习中,我们将使用支持向量机(scikit-learn 的 LinearSVC)和 k 近邻(scikit-learn 的 KNeighborsClassifier)作为基础预测器,而堆叠模型将是一个逻辑回归分类器。

这个练习是练习 6.05 的延续,练习 6.05 中实现了 GradientBoostingClassifier 来构建集成模型:

导入基础模型和用于堆叠的模型:

基础模型

来自 sklearn.neighbors 的 KNeighborsClassifier

来自 sklearn.svm 的 LinearSVC

堆叠模型

来自 sklearn.linear_model 的 LogisticRegression

创建一个新的训练集,新增基础预测器的预测结果列。

我们需要为每个模型的预测值创建两个新列,作为集成模型的特征,分别用于测试集和训练集。由于 NumPy 数组是不可变的,我们将创建一个新的数组,该数组的行数与训练数据集相同,列数比训练数据集多两个。创建完数据集后,我们可以打印它来查看效果:

x_train_with_metapreds = np.zeros((x_train.shape[0], \

x_train.shape[1]+2))

x_train_with_metapreds[:, :-2] = x_train

x_train_with_metapreds[:, -2:] = -1

print(x_train_with_metapreds)

输出结果如下:

图 6.12:预测值的新列

图 6.12:预测值的新列

如我们所见,每行的末尾有两个额外的列,填充了-1 值。

使用 k 折交叉验证策略训练基础模型。

设定 k=5. 对于这五个子集,在其他四个子集上进行训练,并在第五个子集上进行预测。这些预测结果应该被添加到新的 NumPy 数组中用于存放基本预测结果的占位符列中。

首先,我们初始化 KFold 对象,设置 k 值和随机状态以保持可复现性。kf.split()函数接受需要拆分的数据集作为输入,并返回一个迭代器,每个迭代器元素对应于训练和验证子集的索引列表。在每次循环中,这些索引值可以用于划分训练数据,并为每行执行训练和预测。

一旦数据被适当地划分,我们在四分之五的数据上训练两个基础预测器,并在剩余的五分之一行上进行预测。然后,这些预测结果被插入我们在第 2 步初始化时填充为-1 的两个占位符列中:

kf = KFold(n_splits=5, random_state=11)

for train_indices, val_indices in kf.split(x_train):

kfold_x_train, kfold_x_val = x_train[train_indices], \

x_train[val_indices]

kfold_y_train, kfold_y_val = y_train[train_indices], \

y_train[val_indices]

svm = LinearSVC(random_state=11, max_iter=1000)

svm.fit(kfold_x_train, kfold_y_train)

svm_pred = svm.predict(kfold_x_val)

knn = KNeighborsClassifier(n_neighbors=4)

knn.fit(kfold_x_train, kfold_y_train)

knn_pred = knn.predict(kfold_x_val)

x_train_with_metapreds[val_indices, -2] = svm_pred

x_train_with_metapreds[val_indices, -1] = knn_pred

创建一个新的验证集,添加来自基础预测器的预测的附加列。

如同我们在步骤 2 中所做的,我们也将在验证数据集中为基础模型预测添加两列占位符:

x_val_with_metapreds = np.zeros((x_val.shape[0], \

x_val.shape[1]+2))

x_val_with_metapreds[:, :-2] = x_val

x_val_with_metapreds[:, -2:] = -1

print(x_val_with_metapreds)

输出结果如下:

图 6.13:来自基础预测器的预测的附加列

图 6.13:来自基础预测器的预测的附加列

在完整的训练集上拟合基础模型,以获取验证集的元特征。

接下来,我们将在完整的训练数据集上训练两个基础预测器,以获取验证数据集的元预测值。这类似于我们在步骤 3 中为每个折叠所做的工作:

svm = LinearSVC(random_state=11, max_iter=1000)

svm.fit(x_train, y_train)

knn = KNeighborsClassifier(n_neighbors=4)

knn.fit(x_train, y_train)

svm_pred = svm.predict(x_val)

knn_pred = knn.predict(x_val)

x_val_with_metapreds[:, -2] = svm_pred

x_val_with_metapreds[:, -1] = knn_pred

训练堆叠模型并使用最终预测计算准确度。

最后的步骤是使用训练数据集的所有列加上基础估算器的元预测来训练逻辑回归模型。我们使用该模型来计算训练集和验证集的预测准确度:

lr = LogisticRegression(random_state=11)

lr.fit(x_train_with_metapreds, y_train)

lr_preds_train = lr.predict(x_train_with_metapreds)

lr_preds_val = lr.predict(x_val_with_metapreds)

print('堆叠分类器:\n> 训练数据集上的准确度 = {:.4f}'\

'\n> 验证数据集上的准确度 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=lr_preds_train), \

accuracy_score(y_true=y_val, y_pred=lr_preds_val)))

输出结果如下:

堆叠分类器:

训练数据集上的准确度 = 0.7837

验证数据集上的准确度 = 0.8827

注意

由于随机化,你可能会得到一个与前一步骤中呈现的输出略有不同的结果。

比较与基础模型的准确度。

为了了解堆叠模型带来的性能提升,我们计算基础预测器在训练集和验证集上的准确度,并将其与堆叠模型的准确度进行比较:

print('SVM:\n> 训练数据集上的准确度 = {:.4f}'\

'\n> 验证数据集上的准确度 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=svm.predict(x_train)), \

accuracy_score(y_true=y_val, y_pred=svm_pred)))

print('kNN:\n> 训练数据的准确度 = {:.4f}'\

'\n> 验证数据的准确度 = {:.4f}'\

.format(accuracy_score(y_true=y_train, \

y_pred=knn.predict(x_train)), \

accuracy_score(y_true=y_val, y_pred=knn_pred)))

输出如下:

SVM

训练数据的准确度 = 0.7205

验证数据的准确度 = 0.7430

kNN:

训练数据的准确度 = 0.7921

验证数据的准确度 = 0.6816

注意

由于随机化,您可能会得到与前一步骤中呈现的输出略有不同的结果。

如我们所见,堆叠模型不仅给出了比任何基模型显著更高的验证准确度,而且它的准确度几乎达到 89%,是本章讨论的所有集成模型中最高的。

注意

要访问此特定部分的源代码,请参考 packt.live/37QANjZ

您也可以在 packt.live/2YljJ2D 上在线运行此示例。您必须执行整个 Notebook 才能获得预期结果。

活动 6.01:使用独立和集成算法的堆叠

在本活动中,我们将使用波士顿房价:高级回归技术数据库(可以在 archive.ics.uci.edu/ml/machine-learning-databases/housing/ 或 GitHub 上的 packt.live/2Vk002e 获得)。

该数据集旨在解决回归问题(即,目标变量具有一系列连续值)。在本活动中,我们将使用决策树、k 最近邻、随机森林和梯度提升算法训练各个回归模型。然后,我们将构建一个堆叠线性回归模型,使用所有这些算法,并比较每个模型的表现。我们将使用平均绝对误差(MAE)作为本活动的评估标准。

注意

MAE 函数(mean_absolute_error())可以像以前使用的 accuracy_score()度量一样使用。

需要执行的步骤如下:

导入相关库。

读取数据。

预处理数据集以去除空值,并进行独热编码以处理类别变量,为建模准备数据。

将数据集划分为训练和验证 DataFrame。

初始化字典以存储训练和验证的 MAE 值。

使用以下超参数训练一个 DecisionTreeRegressor 模型(dt)并保存分数:

dt_params = {

'criterion': 'mae',

'min_samples_leaf': 15,

'random_state': 11

}

使用以下超参数训练一个 KNeighborsRegressor 模型(knn)并保存分数:

knn_params = {

'n_neighbors': 5

}

使用以下超参数训练一个 RandomForestRegressor 模型(rf)并保存分数:

rf_params = {

'n_estimators': 20,

'criterion': 'mae',

'max_features': 'sqrt',

'min_samples_leaf': 10,

'random_state': 11,

'n_jobs': -1

}

使用以下超参数训练一个 GradientBoostingRegressor 模型(gbr)并保存分数:

gbr_params = {

'n_estimators': 20,

'criterion': 'mae',

'max_features': 'sqrt',

'min_samples_leaf': 10,

'random_state': 11

}

准备训练和验证数据集,四个元估计器的超参数与前面的步骤中使用的超参数相同。

训练一个线性回归模型(lr)作为堆叠模型。

可视化每个单独模型和堆叠模型的训练和验证误差。

输出将如下所示:

图 6.14:训练和验证误差的可视化

图 6.14:训练和验证误差的可视化

注意

这个活动的解决方案可以通过以下链接找到。

因此,我们已经成功演示了堆叠作为一种集成技术,在不同数据集上,相较于任何单一机器学习模型,在验证集准确性方面表现更优。

总结

在本章中,我们首先讨论了过拟合和欠拟合,以及它们如何影响模型在未见数据上的表现。本章探讨了集成建模作为这些模型的解决方案,并进一步讨论了可以使用的不同集成方法,以及它们如何减少在进行预测时遇到的整体偏差或方差。我们首先讨论了袋装算法,并介绍了自助采样的概念。

接着,我们以随机森林作为经典的袋装集成方法的例子,解决了涉及在之前看到的泰坦尼克号数据集上构建袋装分类器和随机森林分类器的练习。然后,我们讨论了提升算法,它们如何成功地减少系统中的偏差,并理解了如何实现自适应提升和梯度提升。我们讨论的最后一种集成方法是堆叠,从练习中我们看到,堆叠在所有实现的集成方法中给出了最佳的准确性得分。虽然构建集成模型是减少偏差和方差的好方法,并且这些模型通常比单一模型表现更好,但它们本身也有各自的问题和使用场景。袋装在避免过拟合时非常有效,而提升方法则能减少偏差和方差,尽管它仍然可能会有过拟合的趋势。堆叠则适用于当一个模型在部分数据上表现良好,而另一个模型在另一部分数据上表现更好时。

在下一章,我们将详细探讨如何通过查看验证技术来克服过拟合和欠拟合的问题,验证技术即是判断我们模型表现的方式,以及如何使用不同的指标作为标志,为我们的使用案例构建最佳模型。

第八章:7. 模型评估

概述

本章介绍了如何通过使用超参数和模型评估指标来提升模型的性能。你将了解如何使用多种指标评估回归和分类模型,并学习如何选择合适的指标来评估和调优模型。

本章结束时,你将能够实施各种采样技术并进行超参数调整,以找到最佳模型。你还将具备计算特征重要性以进行模型评估的能力。

介绍

在前几章中,我们讨论了两种监督学习问题:回归和分类,然后介绍了集成模型,这些模型是由多个基础模型组合而成。我们构建了多个模型并讨论了它们的工作原理和原因。然而,这些还不足以将模型投入生产。模型开发是一个迭代过程,模型训练步骤之后是验证和更新步骤,如下图所示:

图 7.1:机器学习模型开发过程

图 7.1:机器学习模型开发过程

本章将解释上图流程图中的外围步骤;我们将讨论如何选择合适的超参数,并如何使用合适的错误指标进行模型验证。提高模型性能是通过迭代执行这两个任务来完成的。那么,为什么评估模型如此重要呢?假设你已经训练好了模型,并提供了一些超参数,做出了预测,并得到了准确度。这就是模型的核心,但你如何确保你的模型能够发挥最佳性能呢?我们需要确保你制定的性能评估标准确实能够代表模型,并且它在未见过的测试数据集上也能表现良好。确保模型是最佳版本的关键步骤发生在初始训练之后:评估和提高模型性能的过程。本章将带你了解这一过程中所需的核心技术。

在本章中,我们将首先讨论为什么模型评估很重要,并介绍几种用于回归任务和分类任务的评估指标,这些指标可以用来量化模型的预测性能。接下来,我们将讨论保留数据集和 k 折交叉验证,并解释为什么必须有一个独立于验证集的测试集。之后,我们将探讨可以用来提高模型性能的策略。在上一章中,我们讨论了高偏差或高方差的模型如何导致次优的性能,以及如何通过构建模型集成来帮助我们建立一个稳健的系统,使预测更加准确,而不会增加整体方差。我们还提到了以下技术来避免将模型过度拟合到训练数据:

获取更多数据:一个高度复杂的模型可能容易在小数据集上过拟合,但在较大数据集上可能不容易过拟合。

降维:减少特征数量可以帮助使模型更简单。

正则化:在代价函数中添加一个新的项,以便将系数(特别是线性回归中的高次项系数)调整到较小的值。

在本章中,我们将介绍学习曲线和验证曲线,这些曲线可以帮助我们查看训练和验证误差的变化,了解模型是否需要更多数据,以及适当的复杂度级别在哪里。接下来我们将讨论超参数调优以提高性能,并简要介绍特征重要性。

注意

本章的所有相关代码可以在这里找到:https://packt.live/2T1fCWM。

导入模块并准备数据集

在前面的练习和活动中,我们使用了诸如平均绝对误差(MAE)和准确率等术语。在机器学习中,这些被称为评估指标,在接下来的部分中,我们将讨论一些有用的评估指标,它们是什么,如何使用以及何时使用它们。

注意

虽然这一部分没有被定位为练习,但我们鼓励你通过执行呈现的代码,仔细跟进这一部分。我们将在接下来的练习中使用这里展示的代码。

我们现在将加载在第六章《集成建模》中训练的数据和模型。我们将使用在活动 6.01《使用独立和集成算法进行堆叠》中创建的堆叠线性回归模型,以及在练习 6.06《使用随机森林构建集成模型》中创建的随机森林分类模型,来预测乘客的生存情况。

首先,我们需要导入相关的库:

import pandas as pd

import numpy as np

import pickle

%matplotlib inline

import matplotlib.pyplot as plt

接下来,加载第六章集成建模中处理过的数据文件。我们将使用 pandas 的 read_csv() 方法读取我们准备好的数据集,这些数据集将在本章的练习中使用。首先,我们将读取房价数据:

house_prices_reg = \

pd.read_csv('../Datasets/boston_house_prices_regression.csv')

house_prices_reg.head()

我们将看到以下输出:

图 7.2:房价数据集的前五行

图 7.2:房价数据集的前五行

接下来,我们将读取 Titanic 数据:

titanic_clf = pd.read_csv('../Datasets/titanic_classification.csv')

titanic_clf.head()

我们将看到以下输出:

图 7.3:Titanic 数据集的前五行

图 7.3:Titanic 数据集的前五行

接下来,使用 pickle 库从二进制文件加载本章练习中将使用的模型文件:

使用open('../../Saved_Models/titanic_regression.pkl', 'rb')打开文件:

reg = pickle.load(f)

使用open('../../Saved_Models/random_forest_clf.pkl', 'rb')打开文件:

rf = pickle.load(f)

使用open('../../Saved_Models/stacked_linear_regression.pkl',\

'rb') as f:

reg = pickle.load(f)

到目前为止,我们已经成功加载了必要的数据集以及从前面的练习和活动中训练好的机器学习模型。在开始使用这些加载的数据集和模型探索评估指标之前,让我们首先了解不同类型的评估指标。

注意

你可以在以下链接找到保存的模型文件:https://packt.live/2vjoSwf。

评估指标

评估机器学习模型是任何项目中的一个关键部分:一旦我们让模型从训练数据中学习,下一步就是衡量模型的性能。我们需要找到一种指标,不仅能够告诉我们模型预测的准确性,还能让我们比较多个模型的性能,从而选择最适合我们用例的模型。

定义一个指标通常是我们在定义问题陈述时要做的第一件事,而且是在开始进行探索性数据分析之前,因为提前规划并思考如何评估我们所构建的任何模型的表现,以及如何判断其是否达到最佳表现,都是一个好主意。最终,计算性能评估指标将成为机器学习管道的一部分。

不用多说,回归任务和分类任务的评估指标是不同的,因为前者的输出值是连续的,而后者的输出值是类别型的。在本节中,我们将探讨可以用来量化模型预测性能的不同指标。

回归指标

对于输入变量 X,回归模型给出了一个预测值,该值可以取一系列不同的值。理想的情况是模型预测的值尽可能接近实际值 y。因此,二者之间的差异越小,模型表现得越好。回归度量通常涉及查看每个数据点的预测值与实际值之间的数值差异(即残差或误差值),然后以某种方式聚合这些差异。

让我们看一下下面的图表,该图展示了每个 X 点的实际值与预测值:

图 7.4:线性回归问题中实际输出与预测输出之间的残差

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-R47Y9F4G.jpg)

图 7.4:线性回归问题中实际输出与预测输出之间的残差

然而,我们不能仅仅对所有数据点求平均值,因为可能会有数据点的预测误差为正或负,最终聚合会取消掉许多误差,严重高估模型的性能。

相反,我们可以考虑每个数据点的绝对误差并计算 MAE,公式如下:

图 7.5:平均绝对误差

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-HF45QXRY.jpg)

图 7.5:平均绝对误差

这里,yi 和 ŷi 分别是第 i 个数据点的实际值和预测值。

MAE 是一个线性评分函数,这意味着在聚合误差时,它对每个残差赋予相等的权重。MAE 可以取从零到无穷大的任何值,且对误差的方向(正或负)不敏感。由于这些是误差指标,通常情况下,较低的值(尽可能接近零)是更可取的。

为了避免误差方向影响性能估计,我们还可以对误差项进行平方处理。对平方误差求平均得到均方误差(MSE):

图 7.6:均方误差

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-E2RVLXAK.jpg)

图 7.6:均方误差

虽然 MAE 与目标变量 y 具有相同的单位,但 MSE 的单位将是 y 的平方单位,这可能会使得在实际应用中评估模型时,MSE 的解释性稍差。不过,如果我们取 MSE 的平方根,就得到了均方根误差(RMSE):

图 7.7:均方根误差

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-TZPF7OIL.jpg)

图 7.7:均方根误差

由于在求平均之前,误差被平方,甚至少数较高的误差值也可能导致 RMSE 值显著增加。这意味着 RMSE 在判断我们希望惩罚大误差的模型时,比 MAE 更有用。

由于 MAE 和 RMSE 与目标变量具有相同的单位,因此很难判断某个特定的 MAE 或 RMSE 值是好还是坏,因为没有可参考的尺度。为了解决这个问题,通常使用一个度量标准,即 R² 得分或 R-squared 得分:

图 7.8:R-squared 得分

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-BTCJID5U.jpg)

图 7.8:R 平方分数

R2 分数的下限为 -∞,上限为 1。基本模型将目标变量预测为训练数据集目标值的均值 μ,数学表示为:

图 7.9:训练数据集目标值的均值表达式

图 7.9:训练数据集目标值的均值表达式

因此,对于基本模型:

图 7.10:基本模型目标变量的表达式

图 7.10:基本模型目标变量的表达式

记住这一点,如果 R2 的值为负,则表示训练模型的预测比简单预测所有数据的均值还差;如果 R2 值接近 1,则表示模型的 MSE 接近 0。

练习 7.01:计算回归指标

在本练习中,我们将使用与第六章“集成建模”中活动 6.01“单一算法与集成算法堆叠”中训练的相同模型和处理后的数据集来计算回归指标。我们将使用 scikit-learn 实现的 MAE 和 MSE:

注意

在开始本练习之前,确保已按照“导入模块和准备数据集”部分列出的方式导入相关库和模型。

此练习的代码可以在这里找到:

导入指标函数:

from sklearn.metrics import mean_absolute_error, \

mean_squared_error, r2_score

from math import sqrt

使用已加载的模型对给定数据进行预测。我们将使用与活动 6.01“单一算法与集成算法堆叠”中相同的特征,在第六章“集成建模”中,并使用该模型对加载的数据集进行预测。我们保存的列 y 是目标变量,我们将相应地创建 X 和 y:

X = house_prices_reg.drop(columns=['y'])

y = house_prices_reg['y'].values

y_pred = reg.predict(X)

计算 MAE、RMSE 和 R2 分数。让我们打印预测值中的 MAE 和 RMSE 的值。同时,打印模型的 R2 分数:

print('平均绝对误差 = {}'\

.format(mean_absolute_error(y, y_pred)))

print('均方根误差 = {}'\

.format(sqrt(mean_squared_error(y, y_pred))))

print('R 平方分数 = {}'.format(r2_score(y, y_pred)))

输出将如下所示:

平均绝对误差 = 2.874084343939712

均方根误差 = 4.50458397908091

R 平方分数 = 0.7634986504091822

我们可以看到,RMSE 高于 MAE。这表明存在一些残差特别高的数据点,较大的 RMSE 值突出了这一点。但是 R2 分数接近 1,表明该模型与基本模型相比,实际上具有接近理想的性能,基本模型会预测目标变量为平均值。

注意

要访问此特定部分的源代码,请参考 https://packt.live/3epdfp3。

你也可以在网上运行这个示例,网址是 packt.live/3hMLBnY。你必须执行整个笔记本才能获得期望的结果。

分类指标

对于一个输入变量 X,分类任务会给出一个预测值,该值可以取有限的一组值(在二分类问题中为两个值)。由于理想情况下是预测每个数据点的类别与实际类别相同,因此没有衡量预测类别与实际类别之间距离的标准。因此,判断模型性能的标准就简单得多,基本上就是判断模型是否正确预测了类别。

判断分类模型的性能可以通过两种方式进行:使用数值指标,或通过绘制曲线并观察曲线的形状。我们将更详细地探讨这两种方法。

数值指标

判断模型性能最简单、最基本的方法是计算正确预测与总预测数之比,这给出了准确率,如下图所示:

图 7.11:准确率

Figure 7.11: Accuracy

图 7.11:准确率

尽管准确率指标无论类别数目如何都是相同的,接下来的几个指标将在二分类问题的背景下讨论。此外,在许多情况下,准确率可能不是判断分类任务性能的最佳指标。

我们来看一个欺诈检测的示例:假设问题是检测某封邮件是否欺诈。我们的数据集在这种情况下是高度倾斜的(或不平衡的,也就是说,一个类别的数据点比另一个类别的数据点要多得多),其中 10,000 封邮件中有 100 封(占总数的 1%)被标记为欺诈(属于类别 1)。假设我们构建了两个模型:

第一个模型简单地将每封邮件预测为不是欺诈邮件,也就是说,10,000 封邮件中每一封都被分类为类别 0。在这种情况下,10,000 封邮件中的 9,900 封被正确分类,这意味着模型的准确率为 99%。

第二个模型预测了 100 封欺诈邮件为欺诈邮件,但也错误地将另外 100 封邮件预测为欺诈邮件。在这种情况下,100 个数据点在 10,000 个数据中被错误分类,模型的准确率为 99%。

我们如何比较这两个模型呢?构建欺诈检测模型的目的是让我们了解欺诈邮件检测的效果:正确分类欺诈邮件比将非欺诈邮件错误分类为欺诈邮件更为重要。尽管这两个模型的准确率相同,第二个模型实际上比第一个更有效。

由于准确率无法捕捉到这一点,我们需要混淆矩阵,一个具有 n 种不同预测值和实际值组合的表格,其中 n 是类别数。混淆矩阵本质上给出了分类问题预测结果的总结。图 7.12 显示了一个二分类问题的混淆矩阵示例:

图 7.12:混淆矩阵

图 7.12:混淆矩阵

由于这是一个二分类问题,前面的混淆矩阵可以直接视为一个混淆表,换句话说,它是一个包含真阳性、真阴性、假阳性和假阴性的矩阵,如图 7.13 所示。混淆表的大小始终是 2 x 2,无论是二分类还是多分类。在多分类的情况下,如果我们使用一对多分类方法,那么会有与类数相同数量的混淆表:

图 7.13:混淆表

图 7.13:混淆表

以下是混淆表中使用的术语的含义:

真阳性和真阴性:这些是分别在正类和负类中正确预测的数据点数量。

假阳性:这也被称为类型 1 错误,指的是实际上属于负类的数据点,但被预测为正类的数量。延续前面的例子,假阳性案例会是如果一封正常的邮件被分类为欺诈邮件。

假阴性:这也被称为类型 2 错误,指的是实际上属于正类的数据点,但被预测为负类的数量。一个假阴性案例的例子是,如果一封欺诈邮件被分类为不是欺诈邮件。

从混淆矩阵中可以得出两个极其重要的指标:精确度和召回率:

图 7.14:精确度

图 7.14:精确度

图 7.15:召回率

图 7.15:召回率

精确度告诉我们预测为正类的有多少实际上是正类(从模型认为相关的结果中,有多少实际是相关的?),而召回率告诉我们有多少实际的正类被正确预测为正类(从实际相关的结果中,有多少被包括在模型的相关结果列表中?)。这两个指标在两类之间存在不平衡时尤其有用。

通常,模型的精度和召回率之间存在权衡:如果你必须召回所有相关结果,模型会生成更多不准确的结果,从而降低精度。另一方面,如果从生成的结果中获得更高比例的相关结果,则需要尽量减少包含的结果。在大多数情况下,你会优先考虑精度或召回率,这完全取决于问题的具体要求。例如,由于所有欺诈邮件是否被正确分类更为重要,因此召回率将是一个需要最大化的关键指标。

接下来的问题是,我们如何通过一个单一的数字来评估模型,而不是平衡两个独立的指标,既考虑精度又考虑召回率。F1 分数将两者结合为一个单一的数字,能够公平地评判模型,它等于精度和召回率的调和平均数:

图 7.16: F1 分数

图 7.16: F1 分数

F1 分数的值始终介于 0(如果精度或召回率为 0)和 1(如果精度和召回率都为 1)之间。分数越高,表示模型的性能越好。F1 分数对精度和召回率给予相等的权重。它是 Fβ 指标的一个具体实例,其中 β 可以调整,以便对两个参数(召回率或精度分数)中的一个给予更多的权重,公式如下:

图 7.17: F β 分数

图 7.17: F β 分数

当 β < 1 时,更加注重精度,而 β > 1 时,更加注重召回率。F1 分数取 β = 1,给这两个参数相等的权重。

曲线图

有时候,我们不是预测类别,而是得到类别的概率。在二分类任务中,正类(A 类)和负类(B 类)的类别概率总和始终为 1(或 1),这意味着如果我们将分类概率等于 A 类的概率并应用一个阈值,我们实际上可以将其作为一个临界值来进行四舍五入(取 1)或舍去(取 0),从而得出最终的输出类别。

通常,通过改变阈值,我们可以得到一些分类概率接近 0.5 的数据点,这些数据点会从一个类别转换到另一个类别。例如,当阈值为 0.5 时,概率为 0.4 的数据点会被归为 B 类,而概率为 0.6 的数据点会被归为 A 类。但是,如果我们将阈值改为 0.35 或 0.65,这两个数据点就会被归为另一个类别。

事实证明,改变概率阈值会改变精确度和召回率的值,这可以通过绘制精确度-召回率曲线来捕捉。该图的 Y 轴表示精确度,X 轴表示召回率,对于从 0 到 1 的一系列阈值,绘制每个(召回率,精确度)点。连接这些点得到曲线。以下图形提供了一个示例:

图 7.18:精确度-召回率曲线

图 7.18:精确度-召回率曲线

我们知道,在理想情况下,精确度和召回率的值将是 1。这意味着,当将阈值从 0 增加到 1 时,精确度将保持在 1,但召回率会随着越来越多(相关)数据点被正确分类而从 0 增加到 1。因此,在理想情况下,精确度-召回率曲线本质上会是一个正方形,曲线下面积(AUC)将等于 1。

因此,我们可以看到,和 F1 分数一样,AUC 也是一个由精确度和召回率行为衍生的指标,它结合了这两者的值来评估模型的表现。我们希望模型能够实现尽可能高且接近 1 的 AUC。

接收者操作特征(ROC)曲线是另一种用于可视化分类模型性能的技术。ROC 曲线绘制了 Y 轴上的真正阳性率(TPR)和 X 轴上的假阳性率(FPR)之间的关系,跨越不同的分类概率阈值。TPR 与召回率完全相同(也被称为模型的敏感性),而 FPR 是特异度的补数(即 1 – FPR = 特异度);这两者都可以通过以下公式从混淆矩阵中得出:

图 7.19:真正阳性率

图 7.19:真正阳性率

图 7.20:假阳性率

图 7.20:假阳性率

以下图示显示了 ROC 曲线的示例,通过改变概率阈值,以使曲线上的每个点都代表一个(TPR,FPR)数据点,这些点对应于特定的概率阈值:

图 7.21:ROC 曲线

图 7.21:ROC 曲线

ROC 曲线在类平衡较好的情况下更为有用,因为它们通过在假阳性率中使用真阴性来表示模型在类别不平衡的数据集上的有利输出(而这一点在精确度-召回率曲线中没有体现)。

练习 7.02:计算分类指标

在本次练习中,我们将使用第六章《集成建模》中训练的随机森林模型,并使用其预测结果生成混淆矩阵,计算精确度、召回率和 F1 分数,以此来评估我们的模型。我们将使用 scikit-learn 的实现来计算这些指标:

注意

在开始这个练习之前,请确保你已经导入了相关的库和模型,详见“导入模块和准备数据集”部分。

导入相关库和函数:

from sklearn.metrics import (accuracy_score, confusion_matrix, \

precision_score, recall_score, f1_score)

使用模型对所有数据点进行类别预测。我们将使用与之前相同的特征,并使用随机森林分类器对加载的数据集进行预测。scikit-learn 中的每个分类器都有一个.predict_proba()函数,我们将在这里与标准的.predict()函数一起使用,分别给出类别概率和类别:

X = titanic_clf.iloc[:, :-1]

y = titanic_clf.iloc[:, -1]

y_pred = rf.predict(X)

y_pred_probs = rf.predict_proba(X)

计算准确率:

print('Accuracy Score = {}'.format(accuracy_score(y, y_pred)))

输出将如下所示:

Accuracy Score = 0.6251402918069585

62.5%的准确率并不是很高,特别是考虑到如果每次猜测输出都像掷硬币一样,那么准确率将是 50%。然而,本次练习的目标是理解指标的作用。因此,在发现我们的分类器在准确率方面表现不佳后,我们将转向其他一些指标,帮助我们更详细地分析模型的表现。

打印混淆矩阵:

print(confusion_matrix(y_pred=y_pred, y_true=y))

输出将如下所示:

图 7.22:混淆矩阵

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-882W286D.jpg)

图 7.22:混淆矩阵

在这里,我们可以看到模型似乎有很多假阴性,这意味着我们可以预期该模型的召回率极低。同样,由于假阳性的数量只有一个,我们可以预期该模型具有较高的精度。

计算精度和召回率:

print('Precision Score = {}'.format(precision_score(y, y_pred)))

print('Recall Score = {}'.format(recall_score(y, y_pred)))

输出将如下所示:

Precision Score = 0.9

Recall Score = 0.02631578947368421

计算 F1 得分:

print('F1 Score = {}'.format(f1_score(y, y_pred)))

输出将如下所示:

F1 Score = 0.05113636363636364

我们可以看到,由于召回率极低,这也影响了 F1 得分,使得它接近零。

注释

若要访问该特定部分的源代码,请参考 packt.live/2V6mbYQ

你还可以在线运行此示例,网址为 packt.live/37XirOr。你必须执行整个 Notebook 才能得到预期结果。

现在我们已经讨论了可以用来衡量模型预测性能的指标,接下来我们将讨论验证策略,在这些策略中,我们将使用某个指标来评估模型在不同情况下的表现。

数据集拆分

在确定一个模型表现如何时,一个常见的错误是计算模型在其训练数据上的预测误差,并基于在训练数据集上高准确度的结果得出模型表现非常好的结论。

这意味着我们正在尝试在模型已经见过的数据上进行测试,也就是说,模型已经学习了训练数据的行为,因为它已经接触过这些数据——如果要求模型再次预测训练数据的行为,它无疑会表现得很好。而且,在训练数据上的表现越好,模型对数据的了解程度可能越高,以至于它甚至学会了数据中的噪声和异常值的行为。

现在,高训练准确度导致模型具有高方差,正如我们在上一章中看到的那样。为了获得模型表现的无偏估计,我们需要找到模型在它没有在训练过程中接触过的数据上的预测准确度。这就是持留数据集发挥作用的地方。

持留数据

持留数据集是指从训练模型时被暂时保留的样本,它本质上对模型来说是“未见过”的。由于噪声是随机的,持留数据点很可能包含与训练数据集中的数据行为不同的异常值和噪声数据点。因此,计算持留数据集上的表现可以帮助我们验证模型是否过拟合,并且为我们提供模型表现的无偏见视角。

我们在上一章开始时将泰坦尼克号数据集分为训练集和验证集。那么,这个验证数据集是什么?它与测试数据集有什么不同?我们经常看到验证集和测试集这两个术语被交替使用——尽管它们都表示一个持留数据集,但它们在目的上有所不同:

验证数据:在模型从训练数据中学习后,它的表现会在验证数据集上进行评估。然而,为了使模型发挥最佳性能,我们需要对模型进行微调,并反复评估更新后的模型表现,这一过程是在验证数据集上完成的。通常,表现最好且通过验证数据集验证过的微调模型会被选为最终模型。

因此,尽管模型本质上没有从数据中学习,但它会在每次改进的迭代中多次接触到验证数据集。可以说,验证集虽然间接,但确实影响了模型。

测试数据:选择的最终模型现在将在测试数据集上进行评估。测量的性能将在此数据集上提供一个无偏的度量,这个度量将作为模型的最终性能指标。这个最终评估是在模型已经在合并的训练和验证数据集上完全训练之后进行的。在计算这个度量值后,不再对模型进行训练或更新。

这意味着模型只在计算最终性能度量时暴露于测试数据集一次。

需要记住的是,验证数据集绝不能用于评估模型的最终性能:如果模型已经看到并且在后续的修改中专门为提高在验证集上的表现而进行过调整,那么我们对模型真实性能的估计将会有正向偏差。

然而,只有一个保留的验证数据集确实存在一些局限性,因为模型在每次改进迭代中只进行一次验证,因此使用这个单一的评估可能很难捕捉到预测的不确定性。

将数据划分为训练集和验证集会减少用于训练模型的数据量,这可能导致模型具有较高的方差。

最终模型可能会过拟合于这个验证集,因为它是为了最大化在这个数据集上的表现而进行调优的。

如果我们使用称为 k 折交叉验证的验证技术,而不是使用单一的验证数据集,那么这些挑战是可以克服的。

K 折交叉验证

K 折交叉验证是一种验证技术,它通过本质上将验证集轮换成 k 个折叠,从而帮助我们得到一个无偏的模型性能估计。它是如何工作的:

首先,我们选择 k 的值并将数据划分为 k 个子集。

然后,我们将第一个子集作为验证集,其余数据用于训练模型。

我们在验证子集上测量模型的性能。

然后,我们将第二个子集作为验证子集,重复这一过程。

一旦我们完成了 k 次迭代,我们将所有折叠的性能度量值进行汇总,并呈现最终的度量值。

下图直观地解释了这一点:

图 7.23:K 折交叉验证

图 7.23:K 折交叉验证

尽管这种验证方法计算开销较大,但其好处超过了成本。这种方法确保模型在训练数据集中的每个样本上都被验证一次,并且最终获得的性能估计不会偏向于验证数据集,尤其是在小型数据集的情况下。一个特殊情况是留一交叉验证,其中 k 的值等于数据点的数量。

抽样

现在我们已经讨论了用于拆分数据集以进行模型训练和验证的策略,让我们来讨论如何将数据点分配到这些拆分中。我们可以在拆分中采样数据的方式有两种,分别如下:

随机抽样:这就是从整体数据集中随机分配样本到训练集、验证集和/或测试集中。随机拆分数据仅在所有数据点彼此独立时有效。例如,如果数据以时间序列的形式呈现,随机拆分就不适用了,因为数据点是有序的,并且每个数据点都依赖于前一个数据点。随机拆分数据会破坏这种顺序,并且不会考虑这种依赖关系。一个常见的现实世界示例是手写数字分类任务,因为在这种情况下,所有数据样本(手写数字的图像)彼此独立,且数据在所有 10 个类别(数字)之间大致均匀分布。

分层抽样:这是一种确保每个子集的目标变量值分布与原始数据集相同的方法。例如,如果原始数据集中的两个类别的比例是 3:7,则分层抽样确保每个子集也包含按 3:7 比例分布的两个类别。

分层抽样很重要,因为在一个与模型训练数据集目标值分布不同的数据集上测试模型,可能会给出一个不代表模型实际性能的性能估计。

这种抽样技术的实际应用示例是在金融交易中的欺诈检测。由于欺诈事件发生的频率较低,因此欺诈(FRAUD)和非欺诈(NOT_FRAUD)类别之间的不平衡非常大。例如,假设我们有 1,000 笔金融交易,其中有 5 笔是欺诈性的,我们必须使用分层抽样来将这些交易分成训练集和测试集。如果不使用分层抽样,那么所有 5 笔欺诈交易可能都被分配到训练集(或测试集),这将导致我们无法进行有效的验证。

训练集、验证集和测试集样本的大小在模型评估过程中也起着重要作用。保留一个大数据集用于测试模型的最终性能,将有助于我们获得一个无偏的模型性能估计,并减少预测的方差,但如果测试集太大,以至于由于缺乏训练数据而影响模型的训练能力,这将严重影响模型的效果。这一点在较小的数据集中特别重要。

练习 7.03:使用分层抽样进行 K-折交叉验证

在此练习中,我们将实现基于 scikit-learn 的随机森林分类器的 K 折交叉验证与分层抽样。scikit-learn 中的 StratifiedKFold 类实现了交叉验证和抽样的结合,我们将在本练习中使用它:

注意

在开始此练习之前,请确保已导入《导入模块和准备数据集》部分中列出的相关库和模型。

导入相关类。我们将导入 scikit-learn 的 StratifiedKFold 类,它是 KFold 的变体,返回分层折叠,并与 RandomForestClassifier 一起使用:

from sklearn.metrics import accuracy_score

from sklearn.model_selection import StratifiedKFold

from sklearn.ensemble import RandomForestClassifier

为训练准备数据并初始化 k 折对象。在这里,我们将使用五折来评估模型,因此将 n_splits 参数设置为 5:

X = titanic_clf.iloc[:, :-1].values

y = titanic_clf.iloc[:, -1].values

skf = StratifiedKFold(n_splits=5)

为每个折叠训练一个分类器并记录得分。StratifiedKFold 类的功能类似于我们在上一章中使用的 KFold 类,上一章为第六章《集成建模》中第 6.06 节《构建堆叠模型》中的内容。对于每个五折交叉验证,我们将在其他四折上进行训练,并在第五折上进行预测,然后计算预测结果与第五折之间的准确率。如上一章所示,skf.split() 函数接受要拆分的数据集作为输入,并返回一个迭代器,其中包含用于将训练数据细分为训练和验证的索引值:

scores = []

for train_index, val_index in skf.split(X, y):

X_train, X_val = X[train_index], X[val_index]

y_train, y_val = y[train_index], y[val_index]

rf_skf = RandomForestClassifier(**rf.get_params())

rf_skf.fit(X_train, y_train)

y_pred = rf_skf.predict(X_val)

scores.append(accuracy_score(y_val, y_pred))

scores

输出将如下所示:

图 7.24: 使用随机森林分类器的得分

图 7.24: 使用随机森林分类器的得分

打印聚合的准确率得分:

print('平均准确率得分 = {}'.format(np.mean(scores)))

输出将如下所示:

平均准确率得分 = 0.7105606912862568

注意

要查看此特定部分的源代码,请参考 https://packt.live/316TUF5。

你也可以在 https://packt.live/2V6JilY 在线运行此示例。你必须执行整个 Notebook,才能获得预期结果。

因此,我们已经演示了如何使用 k 折交叉验证来对模型性能进行稳健评估。我们在前述方法中使用了分层抽样,确保训练集和验证集具有相似的类别分布。接下来,我们将专注于如何提升模型性能。

性能提升策略

监督式机器学习模型的性能提升是一个迭代过程,通常需要不断的更新和评估循环才能得到完美的模型。尽管本章之前的部分讨论了评估策略,本节将讨论模型更新:我们将讨论一些方法,帮助我们确定模型需要什么来提升性能,以及如何对模型进行这些调整。

训练和测试误差的变化

在前一章中,我们介绍了欠拟合和过拟合的概念,并提到了解决这些问题的一些方法,随后引入了集成模型。但我们没有讨论如何识别我们的模型是否在训练数据上出现了欠拟合或过拟合。

通常查看学习曲线和验证曲线是很有帮助的。

学习曲线

学习曲线显示了随着训练数据量增加,训练误差和验证误差的变化。通过观察曲线的形状,我们可以大致判断增加更多数据是否会对建模产生积极影响,并可能提高模型的性能。

我们来看以下图:虚线表示验证误差,实线表示训练误差。左侧的图显示这两条曲线都趋向于一个较高的误差值。这意味着模型具有较高的偏差,增加更多数据不太可能影响模型的表现。因此,我们不必浪费时间和金钱去收集更多数据,而需要做的只是增加模型的复杂性。

另一方面,右侧的图显示了即使训练集中的数据点数量不断增加,训练误差和测试误差之间仍然存在显著差异。这个较大的差距表明系统的方差较高,这意味着模型出现了过拟合。在这种情况下,增加更多的数据点可能有助于模型更好地泛化,正如下图所示:

图 7.25:数据量增加的学习曲线

图 7.25:数据量增加的学习曲线

那么我们如何识别完美的学习曲线呢?当我们的模型具有低偏差和低方差时,我们会看到类似下图的曲线。它显示了低训练误差(低偏差)以及训练曲线和验证曲线之间的低差距(低方差),因为它们会趋于一致。在实践中,我们能看到的最好的学习曲线是那些趋向不可减少的误差值(由于数据集中的噪声和异常值存在),如以下图所示:

图 7.26:低偏差和低方差模型在训练数据量增加时,训练误差和验证误差的变化

图 7.26:低偏差和低方差模型在训练数据量增加时,训练误差和验证误差的变化

验证曲线

正如我们之前讨论的,机器学习模型的目标是能够对未见过的数据进行泛化。验证曲线帮助我们找到一个理想的点,这个点介于欠拟合和过拟合的模型之间,在这里模型能够很好地进行泛化。在前一章中,我们谈到了模型复杂度如何影响预测性能:我们说过,随着我们从一个过于简单的模型到一个过于复杂的模型,我们会从一个欠拟合的高偏差低方差模型,过渡到一个过拟合的低偏差高方差模型。

验证曲线展示了随着模型参数值变化,训练和验证误差的变化,其中该模型参数在某种程度上控制着模型的复杂度——这可以是线性回归中的多项式度数,或者是决策树分类器的深度:

图 7.27:随着模型复杂度增加,训练和验证的变化

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-1MWNFP2X.jpg)

图 7.27:随着模型复杂度增加,训练和验证的变化

前面的图示展示了随着模型复杂度(其模型参数是一个指标)变化,验证和训练误差是如何变化的。我们还可以看到,阴影区域之间的点就是总误差最小的地方,这正是欠拟合和过拟合之间的最佳点。找到这个点将帮助我们找到模型参数的理想值,从而构建一个低偏差和低方差的模型。

超参数调整

我们之前已经多次讨论了超参数调整。现在,让我们讨论为什么它如此重要。首先,需要注意的是,模型参数与模型超参数是不同的:前者是模型内部的,并且是从数据中学习得来的,而后者则定义了模型本身的架构。

超参数的示例包括以下内容:

用于线性回归的多项式特征的度数

决策树分类器的最大深度

随机森林分类器中要包含的树的数量

梯度下降算法使用的学习率

定义模型架构的设计选择可以对模型的表现产生巨大影响。通常,超参数的默认值是有效的,但找到超参数的完美组合可以大大提升模型的预测能力,因为默认值可能完全不适合我们正在尝试建模的问题。

在下面的图示中,我们可以看到改变两个超参数的值如何导致模型得分的巨大差异:

图 7.28:随着两个模型参数(X 轴和 Y 轴)值的变化,模型得分(Z 轴)的变化

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-2PLE7KLZ.jpg)

图 7.28:随着两个模型参数(X 轴和 Y 轴)值的变化,模型得分(Z 轴)的变化

通过探索一系列可能的值来找到完美的组合,这就是所谓的超参数调优。由于没有可用于最大化模型表现的损失函数,超参数调优通常只是通过实验不同的组合,并选择在验证过程中表现最好的组合。

我们有几种方法可以进行模型的超参数调优:

手动调优:当我们手动选择超参数的值时,这被称为手动调优。它通常效率低下,因为通过手工解决高维度优化问题不仅会很慢,而且也无法让模型达到最佳性能,因为我们可能不会尝试每一种超参数值的组合。

网格搜索:网格搜索涉及对每一个超参数值的组合进行模型训练和评估,并选择表现最佳的组合。由于这需要对超参数空间进行详尽的采样,因此从计算角度来看,它是相当低效的。

随机搜索:虽然第一种方法因为尝试的组合太少被认为效率低下,但第二种方法因为尝试的组合太多也被认为低效。随机搜索通过从之前定义的网格中随机选择一个超参数组合的子集,然后只对这些组合进行训练和评估,从而解决了这一问题。或者,我们还可以为每个超参数提供一个统计分布,从中随机抽取值。

随机搜索的逻辑由 Bergstra 和 Bengio 提出,指出如果网格中至少 5% 的点能提供接近最优的解,那么通过 60 次试验,随机搜索能够以 95% 的概率找到这一区域。

注意:

你可以阅读 Bergstra 和 Bengio 的论文,网址是 www.jmlr.org/papers/v13/bergstra12a.html

贝叶斯优化:前两种方法涉及独立地尝试超参数值的组合,并记录每个组合的模型表现。然而,贝叶斯优化通过顺序地进行实验并利用前一个实验的结果来改善下一个实验的采样方法。

练习 7.04:使用随机搜索进行超参数调优

在本练习中,我们将使用随机搜索方法进行超参数调优。我们将定义一个超参数范围的网格,并使用 RandomizedSearchCV 方法从该网格中随机采样。我们还将对每个组合的值进行 K 折交叉验证。本练习是练习 7.03“使用分层抽样执行 K 折交叉验证”的延续:

导入随机搜索的类:

from sklearn.ensemble import RandomForestClassifier

from sklearn.model_selection import RandomizedSearchCV

准备训练数据并初始化分类器。这里,我们将初始化我们的随机森林分类器而不传递任何参数,因为这只是一个基础对象,将为每个网格点实例化进行随机搜索:

X = titanic_clf.iloc[:, :-1].values

y = titanic_clf.iloc[:, -1].values

rf_rand = 随机森林分类器()

def report(results, max_rank=3):

for rank in range(1, max_rank+1):

results_at_rank = np.flatnonzero\

(results['rank_test_score'] == i)

def report(results, n_top=3):

for i in range(1, n_top + 1):

candidates = np.flatnonzero\

(results['rank_test_score'] == i)

for candidate in candidates:

print("排名模型:{0}".format(i))

print("平均验证得分:{0:.3f}(标准差:{1:.3f})"\

.format(results['mean_test_score'][candidate], \

results['std_test_score'][candidate]))

print("参数:{0}".format(results['params']\

[candidate]))

print("")

指定要采样的参数。这里,我们将列出每个超参数在网格中需要的不同值:

param_dist = {"n_estimators": list(range(10,210,10)), \

"max_depth": list(range(3,20)), \

"max_features": list(range(1, 10)), \

"min_samples_split": list(range(2, 11)), \

"bootstrap": [True, False], \

"criterion": ["gini", "entropy"]}

执行随机搜索。我们初始化随机搜索对象,指定我们要运行的试验总数、参数值字典、评分函数以及 K 折交叉验证的折数。然后,我们调用 .fit() 函数执行搜索:

n_iter_search = 60

random_search = RandomizedSearchCV(rf_rand, \

param_distributions=param_dist, \

scoring='accuracy', \

n_iter=n_iter_search, cv=5)

random_search.fit(X, y)

输出结果将如下所示:

图 7.29:随机搜索 CV 输出

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-VHCZRJC2.jpg)

图 7.29:随机搜索 CV 输出

打印前五名模型的得分和超参数。将结果字典转换为 pandas DataFrame,并按 rank_test_score 排序。然后,对于前五行,打印排名、平均验证得分和超参数:

results = pd.DataFrame(random_search.cv_results_)\

.sort_values('rank_test_score')

for i, row in results.head().iterrows():

print("模型排名:{}".format(row.rank_test_score))

print("平均验证得分:{:.3f}(标准差:{:.3f})"\

.format(row.mean_test_score, row.std_test_score))

print("模型超参数:{}\n".format(row.params))

输出结果将如下所示:

图 7.30:前五名模型的得分和超参数

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-AFPFRK4C.jpg)

图 7.30:前五名模型的得分和超参数

生成随机搜索 CV 结果的报告

report(random_search.cv_results_)

输出结果将如下所示:

图 7.31:随机搜索 CV 结果报告

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/spr-lrn-ws/img/image-ZKVL6UYV.jpg)

图 7.31:随机搜索 CV 结果报告

注:

要访问该部分的源代码,请参阅 https://packt.live/314tqUX。

你也可以在 https://packt.live/2V3YC2z 在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。

我们可以看到,表现最佳的模型只有 70 棵树,而排名第 2 到第 7 的模型有 160 多棵树。此外,排名第 5 的模型只有 10 棵树,仍然表现得与更复杂的模型相当。这表明,随机森林模型中的树木数量并不完全能反映模型的表现。

一个模型的表现会受到其他因素的影响,包括以下几点:

每棵树使用的最大特征数(max_features)

每棵树选择的特征有多具描述性

这些特征集在树间的区别有多大

用于训练每棵树的数据样本数

数据实例在决策树中经过多少次决策(max_depth)

树叶中允许的最小样本数(min_samples_split)等等。

特征重要性

尽管关注模型性能至关重要,但理解模型中各特征如何对预测结果产生影响也是很重要的:

我们需要能够向相关利益相关者解释模型及其不同变量如何影响预测,以便他们了解为什么我们的模型是成功的。

数据可能存在偏差,在这些数据上训练模型可能会影响模型的表现,并导致模型评估结果偏颇,在这种情况下,通过找到重要特征并分析它们来解释模型的能力将有助于调试模型的性能。

除了前一点之外,还必须注意到某些模型偏差可能是社会上或法律上不可接受的。例如,如果一个模型的表现良好,因为它隐式地对基于种族的特征赋予了很高的权重,这可能会引发问题。

除了这些点,寻找特征重要性还可以帮助进行特征选择。如果数据具有高维度并且训练后的模型具有较高的方差,去除那些重要性低的特征是一种通过降维来降低方差的方式。

练习 7.05:使用随机森林进行特征重要性分析

在这个练习中,我们将从之前加载的随机森林模型中找出特征的重要性。这个练习是练习 7.04 “使用随机搜索进行超参数调优”的延续。

找到特征重要性。让我们找到特征的重要性并将其保存在一个 pandas DataFrame 中,索引设置为列名,并按降序对该 DataFrame 进行排序:

feat_imps = pd.DataFrame({'importance': rf.feature_importances_}, \

index=titanic_clf.columns[:-1])

feat_imps.sort_values(by='importance', ascending=False, \

inplace=True)

绘制特征重要性柱状图:

feat_imps.plot(kind='bar', figsize=(10,7))

plt.legend()

plt.show()

输出将如下所示:

图 7.32:特征直方图

图 7.32:特征直方图

在这里,我们可以看到,性别、票价和舱位特征似乎具有最高的重要性;也就是说,它们对目标变量的影响最大。

注意

要访问此特定部分的源代码,请参考 https://packt.live/2YYnxWz。

你也可以在 https://packt.live/2Yo896Y 在线运行这个示例。你必须执行整个 Notebook 才能获得所需的结果。

活动 7.01:最终测试项目

在本活动中,我们将使用在第五章《分类技术》中使用的乳腺癌诊断数据集(有关数据集的详细信息,请参见活动 5.04,使用人工神经网络进行乳腺癌诊断分类),来解决一个二分类问题,其中我们必须预测乳腺细胞是良性还是恶性,给定的特征。在这个问题中,我们希望最大化我们的召回率;也就是说,我们希望能够识别所有恶性细胞,因为如果错过其中任何一个,我们可能会误判没有癌症,而实际上有癌症。而且,我们不希望发生这种情况。

我们将使用 scikit-learn 中的梯度提升分类器来训练模型。本活动作为一个最终项目,旨在帮助巩固本书中学到的概念的实践方面,特别是在本章中。

我们将使用随机搜索与交叉验证来为模型找到最优的超参数组合。然后,我们将使用梯度提升算法在数据集的一部分上构建最终分类器,并使用我们在数据集的剩余部分上学到的分类度量来评估其性能。我们将使用精确度和召回率作为此活动的评估标准。

执行的步骤如下:

导入相关库。

读取 breast-cancer-data.csv 数据集。

将数据集分成训练集和测试集。

选择一个基础模型,并定义与该模型对应的超参数值范围,以进行超参数调优。

定义初始化 RandomizedSearchCV 对象的参数,并使用 K 折交叉验证找到最佳的模型超参数。

将训练数据集进一步划分为训练集和验证集,并在划分后的训练数据集上使用最终超参数训练一个新模型。

计算与验证集相关的预测的准确性、精确度和召回率,并打印混淆矩阵。

尝试不同的阈值,以找到具有高召回率的最优点。绘制精确度-召回率曲线。

输出将如下所示:

图 7.33:精确度召回率曲线

图 7.33:精确度召回率曲线

确定一个阈值,用于与测试数据集相关的预测。

输出将如下所示:

图 7.34:精确度和召回率随着阈值增加的变化

图 7.34:随着阈值增加而变化的精确度和召回率

预测测试数据集的最终数值。

输出如下:

图 7.35:癌症数据集的预测结果

图 7.35:癌症数据集的预测结果

注意:

可通过此链接找到此活动的解决方案。

概要

本章讨论了为什么在监督学习中模型评估很重要,并查看了用于评估回归和分类任务的几个重要指标。我们看到,虽然回归模型评估相对直接,但分类模型的性能可以用多种方式衡量,具体取决于我们希望模型优先考虑的内容。除了数值指标外,我们还看了如何绘制精确率-召回率曲线和 ROC 曲线,以更好地解释和评估模型性能。在此之后,我们讨论了为什么通过计算模型在训练数据上的预测误差来评估模型是个坏主意,以及如何在模型已经看到的数据上测试模型会导致模型具有很高的方差。因此,我们引入了保留数据集的概念,并演示了为什么 K 折交叉验证是一种有用的策略,以及确保模型训练和评估过程保持无偏的抽样技术。性能改进策略的最后一部分从讨论学习曲线和验证曲线开始,以及如何解释它们以推动模型开发过程朝着找到性能更好的模型方向发展。接着是一节关于通过调整超参数来提升性能的讨论,并简要介绍了特征重要性的概念。

从监督学习和回归分类模型的基本原理到集成学习和模型性能评估的概念,我们现在已经为我们的监督学习工具包添加了所有必要的工具。这意味着我们已经准备好开始处理真实的监督学习项目,并应用我们通过这个研讨会获得的所有知识和技能。

第九章

附录

1. 基础知识

第十章:活动 1.01:实现 Pandas 函数

打开一个新的 Jupyter notebook。

使用 pandas 加载 Titanic 数据集:

import pandas as pd

df = pd.read_csv(r'../Datasets/titanic.csv')

使用以下方法在数据集上调用 head 函数:

查看数据的前五个样本

df.head()

输出将如下所示:

图 1.26:前五行

图 1.26:前五行

使用以下方法调用 describe 函数:

df.describe(include='all')

输出将如下所示:

图 1.27:describe()的输出

图 1.27:describe()的输出

我们不需要 Unnamed: 0 列。可以通过以下方法删除该列,而无需使用 del 命令:

del df['Unnamed: 0']

df = df[df.columns[1:]] # 使用列

df.head()

输出将如下所示:

图 1.28:删除 Unnamed: 0 列后的前五行

图 1.28:删除 Unnamed: 0 列后的前五行

计算数据框列的平均值、标准差、最小值和最大值,无需使用 describe:

df.mean()

输出将如下所示:

图 1.29:mean()的输出

图 1.29:mean()的输出

现在,计算标准差:

df.std()

输出将如下所示:

图 1.30:std()的输出

图 1.30:std()的输出

计算列的最小值:

df.min()

输出将如下所示:

图 1.31:min()的输出

图 1.31:min()的输出

接下来,计算数据框中列的最大值。

df.max()

输出将如下所示:

图 1.32:max()的输出

图 1.32:max()的输出

使用 33%、66%和 99%的分位数方法,如下所示代码片段:

df.quantile(0.33)

输出将如下所示:

图 1.33:33%分位数的输出

图 1.33:33%分位数的输出

类似地,使用 66%的分位数方法:

df.quantile(0.66)

输出将如下所示:

图 1.34:66%分位数的输出

图 1.34:66%分位数的输出

使用相同的方法处理 99%:

df.quantile(0.99)

输出将如下所示:

图 1.35:99%分位数的输出

图 1.35:99%分位数的输出

使用 groupby 方法查找每个类别的乘客数量:

class_groups = df.groupby('Pclass')

for name, index in class_groups:

print(f'类别: {name}: {len(index)}')

输出将如下所示:

类别:1:323

类别:2:277

类别:3:709

使用选择/索引方法找出每个类别的乘客数量:

for clsGrp in df.Pclass.unique():

num_class = len(df[df.Pclass == clsGrp])

print(f'类别 {clsGrp}: {num_class}')

结果将如下所示:

类别 3:709

类别 1:323

类别 2:277

第 6 步和第 7 步的答案是匹配的。

确定第三类中最年长的乘客:

third_class = df.loc[(df.Pclass == 3)]

third_class.loc[(third_class.Age == third_class.Age.max())]

输出结果如下:

图 1.36:第三类中最年长的乘客

图 1.36:第三类中最年长的乘客

对于许多机器学习问题,将数值缩放至 0 和 1 之间是非常常见的做法。使用 agg 方法和 Lambda 函数将 Fare 和 Age 列缩放到 0 和 1 之间:

fare_max = df.Fare.max()

age_max = df.Age.max()

df.agg({'Fare': lambda x: x / fare_max, \

'Age': lambda x: x / age_max,}).head()

输出结果如下:

图 1.37:将数值缩放至 0 和 1 之间

图 1.37:将数值缩放至 0 和 1 之间

确定数据集中没有列出票价的个人条目:

df_nan_fare = df.loc[(df.Fare.isna())]

df_nan_fare

输出结果如下:

图 1.38:没有列出票价的个人

图 1.38:没有列出票价的个人

使用 groupby 方法将此行的 NaN 值替换为与相同舱位和登船地点对应的平均票价值:

embarked_class_groups = df.groupby(['Embarked', 'Pclass'])

indices = embarked_class_groups\

.groups[(df_nan_fare.Embarked.values[0], \

df_nan_fare.Pclass.values[0])]

mean_fare = df.iloc[indices].Fare.mean()

df.loc[(df.index == 1043), 'Fare'] = mean_fare

df.iloc[1043]

输出结果如下:

图 1.39:没有列出票价详细信息的个人输出

图 1.39:没有列出票价详细信息的个人输出

要访问此部分的源代码,请参阅 https://packt.live/2AWHbu0。

您也可以在线运行此示例,网址是 https://packt.live/2NmAnse。您必须执行整个笔记本才能获得期望的结果。

2. 探索性数据分析与可视化

第十一章:活动 2.01:汇总统计与缺失值

完成此活动的步骤如下:

导入所需的库:

import json

import pandas as pd

import numpy as np

import missingno as msno

from sklearn.impute import SimpleImputer

import matplotlib.pyplot as plt

import seaborn as sns

读取数据。使用 pandas 的 .read_csv 方法将 CSV 文件读取到 pandas DataFrame 中:

data = pd.read_csv('../Datasets/house_prices.csv')

使用 pandas 的 .info().describe() 方法查看数据集的汇总统计信息:

data.info()

data.describe().T

info() 方法的输出将如下所示:

图 2.50:info() 方法的输出(缩略)

图 2.50:info() 方法的输出(缩略)

describe() 方法的输出将如下所示:

图 2.51:describe() 方法的输出(缩略)

图 2.51:describe() 方法的输出(缩略)

查找每列中缺失值的总数和缺失值的总百分比,并按缺失百分比降序显示至少有一个空值的列。

正如我们在练习 2.02:可视化缺失值 中所做的那样,我们将对 DataFrame 使用 .isnull() 函数来获取一个掩码,使用 .sum() 函数计算每列中的空值数量,使用 .mean() 函数计算空值的比例,并乘以 100 将其转换为百分比。然后,我们将使用 pd.concat() 将缺失值的总数和百分比合并到一个 DataFrame 中,并根据缺失值的百分比对行进行排序:

mask = data.isnull()

total = mask.sum()

percent = 100*mask.mean()

missing_data = pd.concat([total, percent], axis=1, join='outer', \

keys=['count_missing', 'perc_missing'])

missing_data.sort_values(by='perc_missing', ascending=False, \

inplace=True)

missing_data[missing_data.count_missing > 0]

输出将如下所示:

图 2.52:每列缺失值的总数和百分比

图 2.52:每列缺失值的总数和百分比

绘制空值矩阵和空值相关热图。首先,我们找到至少有一个空值的列名列表。然后,使用 missingno 库为这些列中的数据绘制空值矩阵(如同在练习 2.02:可视化缺失值 中所做的那样),并绘制空值相关热图:

nullable_columns = data.columns[mask.any()].tolist()

msno.matrix(data[nullable_columns].sample(500))

plt.show()

msno.heatmap(data[nullable_columns], vmin = -0.1, \

figsize=(18,18))

plt.show()

空值矩阵如下所示:

图 2.53:空值矩阵

图 2.53:空值矩阵

空值相关热图将如下所示:

图 2.54:空值相关热图

图 2.54:空值相关性热图

删除缺失值超过 80% 的列。使用我们在第 2 步中创建的 DataFrame 的 .loc 操作符,仅选择缺失值少于 80% 的列:

data = data.loc[:,missing_data[missing_data.perc_missing < 80].index]

将 FireplaceQu 列中的空值替换为 NA 值。使用 .fillna() 方法将空值替换为 NA 字符串:

data['FireplaceQu'] = data['FireplaceQu'].fillna('NA')

data['FireplaceQu']

输出应如下所示:

图 2.55:替换空值

图 2.55:替换空值

注意

要访问此特定部分的源代码,请参阅 https://packt.live/316c4a0。

您还可以在 https://packt.live/2Z21v5c 上在线运行此示例。您必须执行整个 Notebook 才能获得所需的结果。

活动 2.02:以可视化方式表示值的分布

使用 Matplotlib 绘制目标变量 SalePrice 的直方图。首先,我们使用 plt.figure 命令初始化图形并设置图形大小。然后,使用 matplotlib 的 .hist() 函数作为主要绘图函数,将 SalePrice 系列对象传递给它以绘制直方图。最后,我们指定坐标轴标签并显示图形:

plt.figure(figsize=(8,6))

plt.hist(data.SalePrice, bins=range(0,800000,50000))

plt.ylabel('房屋数量')

plt.xlabel('销售价格')

plt.show()

输出将如下所示:

图 2.56:目标变量的直方图

图 2.56:目标变量的直方图

查找每个列中具有对象类型的唯一值的数量。通过对原始 DataFrame 使用 .select_dtypes 函数来选择那些具有 numpy.object 数据类型的列,创建一个名为 object_variables 的新 DataFrame。然后,使用 .nunique() 函数查找此 DataFrame 中每列的唯一值数量,并对结果进行排序:

object_variables = data.select_dtypes(include=[np.object])

object_variables.nunique().sort_values()

输出将如下所示:

图 2.57:每个具有对象类型的列中的唯一值数量(已截断)

图 2.57:每个具有对象类型的列中的唯一值数量(已截断)

创建一个 DataFrame 来表示 HouseStyle 列中每个类别值的出现次数。使用 .value_counts() 函数按降序计算每个值的频率,以 pandas 系列的形式,然后重置索引以生成 DataFrame,并根据索引排序值:

counts = data.HouseStyle.value_counts(dropna=False)

counts.reset_index().sort_values(by='index')

输出将如下所示:

图 2.58:HouseStyle 列中每个类别值的出现次数

图 2.58:HouseStyle 列中每个类别值的出现次数

绘制一个饼图表示这些计数。如同第 1 步中一样,我们使用 plt.figure()初始化图形,并分别使用 plt.title()和 plt.show()方法设置图表标题和显示图形。主要绘图函数是 plt.pie(),我们将前一步创建的系列传递给它:

fig, ax = plt.subplots(figsize=(10,10))

slices = ax.pie(counts, labels = counts.index, \

colors = ['white'], \

wedgeprops = {'edgecolor': 'black'})

patches = slices[0]

hatches = ['/', '\', '|', '-', '+', 'x', 'o', 'O', '.', '*']

colors = ['white', 'white', 'lightgrey', 'white', \

'lightgrey', 'white', 'lightgrey', 'white']

for patch in range(len(patches)):

patches[patch].set_hatch(hatches[patch])

patches[patch].set_facecolor(colors[patch])

plt.title('显示不同房屋样式计数的饼图')

plt.show()

输出结果如下:

图 2.59:表示计数的饼图

图 2.59:表示计数的饼图

找出每一列中具有数字类型的唯一值数量。如同在第 2 步中执行的那样,现在选择具有 numpy.number 数据类型的列,并使用.nunique()查找每列的唯一值数量。将结果序列按降序排序:

numeric_variables = data.select_dtypes(include=[np.number])

numeric_variables.nunique().sort_values(ascending=False)

输出结果如下:

图 2.60:每个数值列中唯一值的数量(已截断)

图 2.60:每个数值列中唯一值的数量(已截断)

使用 seaborn 绘制 LotArea 变量的直方图。使用 seaborn 的.distplot()函数作为主要绘图函数,需要传递 DataFrame 中的 LotArea 系列(去除任何空值,使用.dropna()方法删除空值)。为了改善图形视图,还可以设置 bins 参数,并使用 plt.xlim()指定 X 轴范围:

plt.figure(figsize=(10,7))

sns.distplot(data.LotArea.dropna(), bins=range(0,100000,1000))

plt.xlim(0,100000)

plt.show()

输出结果如下:

图 2.61:LotArea 变量的直方图

图 2.61:LotArea 变量的直方图

计算每列值的偏度和峰度值:

data.skew().sort_values()

data.kurt()

偏度值的输出将是:

图 2.62:每列的偏度值(已截断)

图 2.62:每列的偏度值(已截断)

峰度值的输出将是:

图 2.63:每列的峰度值(已截断)

图 2.63:每列的峰度值(已截断)

注意

要查看此特定部分的源代码,请访问 https://packt.live/3fR91qj。

你还可以在 https://packt.live/37PYOI4 上在线运行此示例。你必须执行整个 Notebook 才能获得期望的结果。

活动 2.03:数据内的关系

绘制数据集的相关性热图。正如我们在练习 2.13:绘制相关性热图中所做的那样,使用 seaborn 的.heatmap()函数绘制热图,并传递通过 pandas 的.corr()函数计算出的特征相关性矩阵。除此之外,使用 cmap 参数将颜色映射设置为 RdBu,并分别使用 vmin 和 vmax 参数将颜色刻度的最小值和最大值设置为-1 和 1:

plt.figure(figsize = (12,10))

sns.heatmap(data.corr(), square=True, cmap="RdBu", \

vmin=-1, vmax=1)

plt.show()

输出结果如下:

图 2.64:数据集的相关性热图

图 2.64:数据集的相关性热图

使用以下特征子集绘制更紧凑的热图,并在热图上添加相关性值注释:

feature_subset = ['GarageArea','GarageCars','GarageCond', \

'GarageFinish', 'GarageQual','GarageType', \

'GarageYrBlt','GrLivArea','LotArea', \

'MasVnrArea','SalePrice']

现在与前一步相同,这次只选择数据集中的上述列,并将参数 annot 添加到主绘图函数中,值为 True,其他内容保持不变:

plt.figure(figsize = (12,10))

sns.heatmap(data[feature_subset].corr(), square=True, \

annot=True, cmap="RdBu", vmin=-1, vmax=1)

plt.show()

输出结果如下:

图 2.65:带有相关性注释的特征子集相关性热图对于相关性值

图 2.65:带有相关性值注释的特征子集相关性热图

显示相同特征子集的 Pairplot,主对角线为 KDE 图,其他位置为散点图。使用 seaborn 的.pairplot()函数绘制 DataFrame 中选定列的非空值的 Pairplot。为了渲染对角线的 KDE 图,将 kde 传递给 diag_kind 参数,而将 scatter 传递给 kind 参数,以设置所有其他图为散点图:

sns.pairplot(data[feature_subset].dropna(), \

kind ='scatter', diag_kind='kde')

plt.show()

输出结果如下:

图 2.66:相同特征子集的 Pairplot

图 2.66:相同特征子集的 Pairplot

创建一个箱线图,展示每个 GarageCars 类别下 SalePrice 的变化。在这里使用的主要绘图函数是 seaborn 的.boxplot()函数,我们将 DataFrame 以及 x 和 y 参数传递给它,前者是分类变量,后者是我们想要查看每个类别内部变化的连续变量,即 GarageCars 和 SalePrice:

plt.figure(figsize=(10, 10))

sns.boxplot(x='GarageCars', y="SalePrice", data=data)

plt.show()

输出结果如下:

图 2.67: 箱型图显示每个 GarageCars 类别中销售价格的变化

图 2.67: 箱型图显示每个 GarageCars 类别中销售价格的变化

使用 seaborn 绘制折线图,显示从较旧到最近建成的公寓销售价格的变化。在这里,我们将使用 seaborn 的 .lineplot() 函数绘制折线图。由于我们想查看销售价格的变化,因此我们将销售价格作为 y 变量,并且由于变化跨越了一段时间,我们将建造年份 (YearBuilt) 作为 x 变量。考虑到这一点,我们将相应的系列数据作为值传递给主要绘图函数的 y 和 x 参数。同时,我们还传递 ci=None 参数,以隐藏图中线条周围的标准偏差指示器:

plt.figure(figsize=(10,7))

sns.lineplot(x=data.YearBuilt, y=data.SalePrice, ci=None)

plt.show()

输出将如下所示:

图 2.68: 折线图显示从较旧到最近建成的公寓在销售价格上的变化建成公寓

图 2.68: 折线图显示从较旧到最近建成的公寓销售价格的变化

图 2.68 展示了如何使用折线图来突出显示整体趋势以及短期时间周期中的波动。你可能想将此图表与相同数据的散点图进行比较,并考虑每种图表传达了什么信息。

注意

要访问此部分的源代码,请参阅 https://packt.live/2Z4bqHM。

你也可以在线运行这个示例,网址是 https://packt.live/2Nl5ggI。你必须执行整个 Notebook 才能得到预期的结果。

3. 线性回归

第十二章:活动 3.01:使用移动平均绘制数据

加载所需的两个包:

import pandas as pd

import matplotlib.pyplot as plt

从 CSV 文件将数据集加载到 pandas 数据框中:

df = pd.read_csv('../Datasets/austin_weather.csv')

df.head()

输出将显示 austin_weather.csv 文件的前五行:

图 3.61:奥斯丁天气数据的前五行(请注意右侧的附加列未显示)

图 3.61:奥斯丁天气数据的前五行(请注意右侧的附加列未显示)

由于我们只需要日期和温度列,我们将从数据集中删除所有其他列:

df = df.loc[:, ['Date', 'TempAvgF']]

df.head()

输出将如下所示:

图 3.62:奥斯丁天气数据的日期和温度列

图 3.62:奥斯丁天气数据的日期和温度列

最初,我们只关心第一年的数据,因此我们需要仅提取该信息。在数据框中为年份创建一列,从日期列中的字符串提取年份值作为整数,并将这些值赋给年份列(请注意,温度是按天记录的)。重复此过程以创建月份和日期列,然后提取第一年的数据:

df.loc[:, 'Year'] = df.loc[:, 'Date'].str.slice(0, 4).astype('int')

df.loc[:, 'Month'] = df.loc[:, 'Date'].str.slice(5, 7).astype('int')

df.loc[:, 'Day'] = df.loc[:, 'Date'].str.slice(8, 10).astype('int')

df = df.loc[df.index < 365]

print(df.head())

print(df.tail())

输出将如下所示:

图 3.63:包含一年数据的新数据框

图 3.63:包含一年数据的新数据框

使用 rolling() 方法计算 20 天移动平均:

window = 20

rolling = df.TempAvgF.rolling(window).mean()

print(rolling.head())

print(rolling.tail())

输出将如下所示:

图 3.64:带有移动平均数据的数据框

图 3.64:带有移动平均数据的数据框

绘制原始数据和移动平均数据,x 轴为年份中的天数:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

ax.scatter(df.index, df.TempAvgF, \

label = '原始数据', c = 'k')

移动平均

ax.plot(rolling.index, rolling, c = 'r', \

linestyle = '--', label = f'{window} 天移动平均')

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天数', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df.index.min(), df.index.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出将如下所示:

图 3.65:带有 20 天移动平均线的数据

图 3.65:带有 20 天移动平均线的数据

注意

要访问此特定部分的源代码,请参考 https://packt.live/2Nl5m85。

你也可以在 https://packt.live/3epJvs6 上在线运行这个例子。你必须执行整个笔记本才能得到期望的结果。

活动 3.02:使用最小二乘法的线性回归

导入所需的包和类:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

从 CSV 文件(austin_weather.csv)加载数据并检查数据(使用 head() 和 tail() 方法):

加载数据并检查

df = pd.read_csv('../Datasets/austin_weather.csv')

print(df.head())

print(df.tail())

df.head() 的输出将如下所示:

图 3.66:df.head() 的输出

图 3.66:df.head() 的输出

df.tail() 的输出将如下所示:

图 3.67:df.tail() 的输出

图 3.67:df.tail() 的输出

删除除了 Date 和 TempAvgF 列以外的所有列:

df = df.loc[:, ['Date', 'TempAvgF']]

df.head()

输出将如下所示:

图 3.68:用于活动 3.02 的两列

图 3.68:用于活动 3.02 的两列

创建新的 Year、Month 和 Day 列,并通过解析 Date 列来填充它们:

添加一些有用的列

df.loc[:, 'Year'] = df.loc[:, 'Date']\

.str.slice(0, 4).astype('int')

df.loc[:, 'Month'] = df.loc[:, 'Date']\

.str.slice(5, 7).astype('int')

df.loc[:, 'Day'] = df.loc[:, 'Date']\

.str.slice(8, 10).astype('int')

print(df.head())

print(df.tail())

输出将如下所示:

图 3.69:增强数据

图 3.69:增强数据

创建一个新的列用于移动平均,并用 TempAvgF 列的 20 天移动平均填充它:

"""

设置 20 天窗口,然后使用它来平滑温度并填充到新列中

"""

window = 20

df['20_d_mov_avg'] = df.TempAvgF.rolling(window).mean()

print(df.head())

print(df.tail())

输出将如下所示:

图 3.70:添加 20 天移动平均线

图 3.70:添加 20 天移动平均线

切割出一整年的数据用于模型。确保该年份没有因移动平均而缺失数据。同时创建一个 Day_of_Year 列(它应该从 1 开始):

"""

现在让我们精确切割出一年时间的数据

日历的开始和结束日期

从之前的输出中我们可以看到

2014 年是第一个拥有完整数据的年份,

然而,它仍然会有 NaN 值

移动平均,所以我们将使用 2015 年

"""

df_one_year = df.loc[df.Year == 2015, :].reset_index()

df_one_year['Day_of_Year'] = df_one_year.index + 1

print(df_one_year.head())

print(df_one_year.tail())

输出将如下所示:

图 3.71:一年的数据

图 3.71:一年的数据

创建原始数据(原始 TempAvgF 列)的散点图,并叠加 20 天移动平均线:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

label = '原始数据', c = 'k')

移动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

label = f'{window}天移动平均')

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天数', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出将如下所示:

图 3.72:叠加了 20 天移动平均的原始数据

图 3.72:叠加了 20 天移动平均的原始数据

使用默认参数创建一个线性回归模型,即为模型计算一个 y 截距,并且不对数据进行归一化。年份的天数(1 到 365)构成输入数据,平均温度构成输出数据。打印模型的参数和 r²值:

拟合线性模型

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(df_one_year['Day_of_Year']\

.values.reshape((-1, 1)), \

df_one_year.TempAvgF)

print('模型斜率:', linear_model.coef_)

print('模型截距:', linear_model.intercept_)

print('模型 r²值:', \

linear_model.score(df_one_year['Day_of_Year']\

.values.reshape((-1, 1)), \

df_one_year.TempAvgF))

结果应如下所示:

模型斜率:[0.04304568]

模型截距:62.23496914044859

模型 r²值:0.09549593659736466

请注意,r²值非常低,这并不令人惊讶,因为数据的斜率随时间有显著变化,而我们拟合的是一个具有常数斜率的单一线性模型。

使用相同的 x 数据从模型生成预测值:

使用训练数据进行预测

y_pred = linear_model.predict(df_one_year['Day_of_Year']\

.values.reshape((-1, 1)))

x_pred = df_one_year.Day_of_Year

如之前一样,创建一个新的散点图,并叠加模型的预测结果:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

label = '原始数据', c = 'k')

移动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

label = f'{window}天移动平均')

线性模型

ax.plot(x_pred, y_pred, c = "blue", linestyle = '-.', \

label = '线性模型')

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天数', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出将如下所示:

图 3.73:原始数据、20 天移动平均和线性拟合

图 3.73:原始数据,20 天滑动平均和线性拟合

注意

要访问本节的源代码,请参阅 https://packt.live/2CwEKyT.

你也可以在 https://packt.live/3hKJSzD 在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。

活动 3.03:虚拟变量

导入所需的包和类:

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

加载并检查数据:

加载数据并检查

df = pd.read_csv('../Datasets/austin_weather.csv')

print(df.head())

print(df.tail())

df.head()的输出应如下所示:

图 3.74:df.head()函数的输出

图 3.74:df.head()函数的输出

df.tail()的输出应如下所示:

图 3.75:df.tail()函数的输出

图 3.75:df.tail()函数的输出

执行与之前相同的预处理。去除除 Date 和 TempAvgF 列外的所有列。添加 Year、Month 和 Day 列。创建一个包含 20 天滑动平均的新列。切出第一个完整年份(2015 年):

df = df.loc[:, ['Date', 'TempAvgF']]

添加一些有用的列

df.loc[:, 'Year'] = df.loc[:, 'Date'].str.slice(0, 4).astype('int')

df.loc[:, 'Month'] = df.loc[:, 'Date'].str.slice(5, 7).astype('int')

df.loc[:, 'Day'] = df.loc[:, 'Date'].str.slice(8, 10).astype('int')

"""

设置一个 20 天的滑动窗口,然后用它来平滑数据

在新列中存储温度

"""

window = 20

df['20_d_mov_avg'] = df.TempAvgF.rolling(window).mean()

"""

现在让我们切片出完整的一年

日历的开始和结束日期

我们从之前的输出中看到

2014 年是第一个完整的数据年份,

然而,它仍然会有 NaN 值

滑动平均,因此我们将使用 2015 年数据

"""

df_one_year = df.loc[df.Year == 2015, :].reset_index()

df_one_year['Day_of_Year'] = df_one_year.index + 1

print(df_one_year.head())

print(df_one_year.tail())

数据应该如下所示:

图 3.76:预处理数据

图 3.76:预处理数据

可视化结果:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

label = '原始数据', c = 'k')

滑动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

label = f'{window}天滑动平均')

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天数', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

图表应该如下所示:

图 3.77:奥斯丁温度及其滑动平均

图 3.77:奥斯丁温度及其滑动平均

我们可以看到温度从一月开始上升,直到九月左右,然后再次下降。这是一个明显的季节性循环。作为第一次改进,我们可以在模型中加入月份。如同在虚拟变量介绍中所述,如果我们仅将月份编码为 1 到 12 的整数,模型可能会认为 12 月(12)比 1 月(1)更重要。所以,我们将月份编码为虚拟变量来避免这一问题:

使用月份作为虚拟变量

dummy_vars = pd.get_dummies(df_one_year['Month'], drop_first = True)

dummy_vars.columns = ['Feb', 'Mar', 'Apr', 'May', 'Jun', \

'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

df_one_year = pd.concat([df_one_year, dummy_vars], \

axis = 1).drop('Month', axis = 1)

df_one_year

数据应如下所示:

图 3.78:使用虚拟变量增强的月份数据

图 3.78:使用虚拟变量增强的月份数据

现在,使用 Day_of_Year 和虚拟变量拟合一个线性模型,并打印模型系数和 R² 值:

使用月份虚拟变量拟合模型

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(pd.concat([df_one_year.Day_of_Year, \

df_one_year.loc[:, 'Feb':'Dec']], \

axis = 1),

df_one_year['TempAvgF'])

print('模型系数:', linear_model.coef_)

print('模型截距:', linear_model.intercept_)

print('模型 R 平方:', \

linear_model.score(pd.concat([df_one_year.Day_of_Year, \

df_one_year.loc[:, 'Feb':'Dec']], \

axis = 1),

df_one_year['TempAvgF']))

结果应如下所示:

模型系数:[ 0.03719346 1.57445204 9.35397321 19.16903518 22.02065629 26.80023439

30.17121033 30.82466482 25.6117698 15.71715435 1.542969 -4.06777548]

模型截距:48.34038858048261

模型 R 平方:0.7834805472165678

注意系数的符号——第一个值与 Day_of_Year 相关,接下来是 1 月到 12 月的值。1 月、2 月、3 月、11 月和 12 月的系数为负,而 6 月到 9 月的系数为正。这对德州的季节来说是合理的。

现在,使用单年数据进行预测,并可视化结果:

使用数据进行预测

y_pred = \

linear_model.predict(pd.concat([df_one_year.Day_of_Year, \

df_one_year.loc[:, 'Feb':'Dec']], \

axis = 1))

x_pred = df_one_year.Day_of_Year

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1]);

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

标签 = '原始数据', c = 'k')

移动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

标签 = f'{window}天移动平均')

回归预测

ax.plot(x_pred, y_pred, c = "blue", linestyle = '-.', \

标签 = '线性模型 w/虚拟变量'

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天数', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12, loc = 'upper left')

plt.show()

输出应如下所示:

图 3.79:带有月份虚拟变量的线性回归结果

图 3.79:带有月份虚拟变量的线性回归结果

注意

要访问该特定部分的源代码,请参阅 https://packt.live/3enegOg。

你也可以在 https://packt.live/2V4VgMM 在线运行这个例子。你必须执行整个 Notebook 才能得到期望的结果。

活动 3.04:线性回归特征工程

加载所需的包和类:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

from sklearn.linear_model import LinearRegression

加载数据并进行预处理,直到添加 Day_of_Year 为止:

加载数据

df = pd.read_csv('../Datasets/austin_weather.csv')

df = df.loc[:, ['Date', 'TempAvgF']]

添加一些有用的列

df.loc[:, 'Year'] = df.loc[:, 'Date'].str.slice(0, 4).astype('int')

df.loc[:, 'Month'] = df.loc[:, 'Date'].str.slice(5, 7).astype('int')

df.loc[:, 'Day'] = df.loc[:, 'Date'].str.slice(8, 10).astype('int')

"""

设置一个 20 天窗口,然后使用它进行平滑处理

新列中的温度

"""

window = 20

df['20_d_mov_avg'] = df.TempAvgF.rolling(window).mean()

"""

现在让我们准确地切割出一年

日历的开始和结束日期

我们从之前的输出中可以看到

2014 年是第一个有完整数据的年份,

然而,它仍然会有 NaN 值

移动平均值,因此我们将使用 2015 年的数据

"""

df_one_year = df.loc[df.Year == 2015, :].reset_index()

df_one_year['Day_of_Year'] = df_one_year.index + 1

现在,进行特征工程,我们构建 Day_of_Year 的正弦和余弦,周期为 365 天:

为 Day_of_Year 添加两个列,分别表示其正弦和余弦值

df_one_year['sine_Day'] = np.sin(2 * np.pi \

  • df_one_year['Day_of_Year'] / 365)

df_one_year['cosine_Day'] = np.cos(2 * np.pi \

  • df_one_year['Day_of_Year'] / 365)

df_one_year

数据应如下所示:

图 3.80:包含新特征 sine_Day 和 cosine_Day 的奥斯丁天气数据

图 3.80:包含新特征 sine_Day 和 cosine_Day 的奥斯丁天气数据

我们现在可以使用 scikit-learn 的 LinearRegression 类拟合模型,并打印系数和 R² 值:

使用 Day_of_Year 和 sin/cos 拟合模型

linear_model = LinearRegression(fit_intercept = True)

linear_model.fit(df_one_year[['Day_of_Year', 'sine_Day', \

'cosine_Day']],\

df_one_year['TempAvgF'])

print('模型系数:', linear_model.coef_)

print('模型截距:', linear_model.intercept_)

print('模型 R 平方值:', \

linear_model.score(df_one_year[['Day_of_Year', 'sine_Day', \

'cosine_Day']],\

df_one_year['TempAvgF']))

输出应如下所示:

模型系数: [ 1.46396364e-02 -5.57332499e+00 -1.67824174e+01]

模型截距:67.43327530313064

模型 R 平方值:0.779745650129063

请注意,r2 值与我们通过虚拟变量得到的结果差不多。然而,我们来看看预测结果,看看这个模型是否比以前更合适或更不合适。

使用增强数据生成预测:

使用数据进行预测

y_pred = \

linear_model.predict(df_one_year[['Day_of_Year', 'sine_Day', \

'cosine_Day']])

x_pred = df_one_year.Day_of_Year

现在,查看结果:

fig = plt.figure(figsize=(10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

label = '原始数据', c = 'k')

移动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

label = f'{window} 天移动平均')

回归预测

ax.plot(x_pred, y_pred, c = "blue", linestyle = '-.', \

label = '线性模型带正余弦拟合')

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('天', fontsize = 14)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12, loc = 'upper left')

输出结果如下:

图 3.81:奥斯丁温度数据,带有移动平均叠加和周期性特征拟合叠加

图 3.81:奥斯丁温度数据,带有移动平均叠加和周期性特征拟合叠加

注意

要访问此特定部分的源代码,请参见 https://packt.live/3dvkmet.

您还可以在线运行此示例,网址:https://packt.live/3epnOIJ。您必须执行整个笔记本才能获得预期的结果。

Activity 3.05: 梯度下降

导入模块和类:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

from sklearn.metrics import r2_score

from sklearn.linear_model import SGDRegressor

加载数据(austin_weather.csv)并进行预处理,直到创建 Day_of_Year 列并切割出完整的一年数据(2015 年):

加载数据并检查

df = pd.read_csv('../Datasets/austin_weather.csv')

df = df.loc[:, ['Date', 'TempAvgF']]

添加基于时间的列

df.loc[:, 'Year'] = df.loc[:, 'Date'].str.slice(0, 4).astype('int')

df.loc[:, 'Month'] = df.loc[:, 'Date'].str.slice(5, 7).astype('int')

df.loc[:, 'Day'] = df.loc[:, 'Date'].str.slice(8, 10).astype('int')

"""

设置一个 20 天窗口,然后使用该窗口进行平滑

温度放在一个新的列中

"""

window = 20

df['20_d_mov_avg'] = df.TempAvgF.rolling(window).mean()

"""

现在让我们精确切割出一年的数据

日历开始和结束日期

从之前的输出可以看到

2014 年是第一个完整数据的年份,

但是,它仍然会对某些值产生 NaN

移动平均,因此我们将使用 2015 年数据

"""

df_one_year = df.loc[df.Year == 2015, :].reset_index()

df_one_year['Day_of_Year'] = df_one_year.index + 1

print(df_one_year.head())

print(df_one_year.tail())

输出结果如下:

图 3.82:缩放前的预处理数据

图 3.82:缩放前的预处理数据

缩放数据以进行训练:

缩放数据

X_min = df_one_year.Day_of_Year.min()

X_range = df_one_year.Day_of_Year.max() \

  • df_one_year.Day_of_Year.min()

Y_min = df_one_year.TempAvgF.min()

Y_range = df_one_year.TempAvgF.max() \

  • df_one_year.TempAvgF.min()

scale_X = (df_one_year.Day_of_Year - X_min) / X_range

train_X = scale_X.ravel()

train_Y = ((df_one_year.TempAvgF - Y_min) / Y_range).ravel()

设置随机种子,实例化 SGDRegressor 模型对象,并拟合模型到训练数据:

创建模型对象

np.random.seed(42)

model = SGDRegressor(loss = 'squared_loss', max_iter = 100, \

learning_rate = 'constant', eta0 = 0.0005, \

tol = 0.00009, penalty = 'none')

拟合模型

model.fit(train_X.reshape((-1, 1)), train_Y)

输出应如下所示:

图 3.83:使用 SGDRegressor 的模型对象

图 3.83:使用 SGDRegressor 的模型对象

提取模型系数并重新缩放:

Beta0 = (Y_min + Y_range * model.intercept_[0] \

  • Y_range * model.coef_[0] * X_min / X_range)

Beta1 = Y_range * model.coef_[0] / X_range

print(Beta0)

print(Beta1)

输出应类似于以下内容:

61.45512325422412

0.04533603293003107

使用缩放后的数据生成预测值,然后获取 r2 值:

生成预测

pred_X = df_one_year['Day_of_Year']

pred_Y = model.predict(train_X.reshape((-1, 1)))

计算 r 平方值

r2 = r2_score(train_Y, pred_Y)

print('r 平方 = ', r2)

结果应类似于以下内容:

r 平方 = 0.09462157379706759

将预测结果缩放回实际值并可视化结果:

将预测值缩放回实际值

pred_Y = (pred_Y * Y_range) + Y_min

fig = plt.figure(figsize = (10, 7))

ax = fig.add_axes([1, 1, 1, 1])

原始数据

ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \

label = '原始数据', c = 'k')

移动平均

ax.plot(df_one_year.Day_of_Year, df_one_year['20_d_mov_avg'], \

c = 'r', linestyle = '--', \

label = f'{window} 日移动平均')

回归预测

ax.plot(pred_X, pred_Y, c = "blue", linestyle = '-.', \

linewidth = 4, label = '线性拟合(来自 SGD)')

将模型添加到图表上

ax.text(1, 85, 'Temp = ' + str(round(Beta0, 2)) + ' + ' \

  • str(round(Beta1, 4)) + ' * 日期', fontsize = 16)#

ax.set_title('空气温度测量', fontsize = 16)

ax.set_xlabel('日期', fontsize = 16)

ax.set_ylabel('温度 (\(^\circ\)F)', fontsize = 14)

ax.set_xticks(range(df_one_year.Day_of_Year.min(), \

df_one_year.Day_of_Year.max(), 30))

ax.tick_params(labelsize = 12)

ax.legend(fontsize = 12)

plt.show()

输出结果如下:

图 3.84:使用 SGDRegressor 进行线性回归的结果

图 3.84:使用 SGDRegressor 进行线性回归的结果

注意

若要访问此特定部分的源代码,请参阅 https://packt.live/2AY1bMZ。

你也可以在 https://packt.live/2NgCI86 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。

4. 自回归

第十三章:活动 4.01:基于周期数据的自回归模型

导入必要的包、类和库。

注意

该活动将使用早期版本的 pandas,请确保使用以下命令降级 pandas 版本:

pip install pandas==0.24.2

代码如下:

import pandas as pd

import numpy as np

from statsmodels.tsa.ar_model import AR

from statsmodels.graphics.tsaplots import plot_acf

import matplotlib.pyplot as plt

加载数据并将 Date 列转换为 datetime 类型:

df = pd.read_csv('../Datasets/austin_weather.csv')

df.Date = pd.to_datetime(df.Date)

print(df.head())

print(df.tail())

df.head()的输出应如下所示:

图 4.22:df.head()的输出

图 4.22:df.head()的输出

df.tail()的输出应如下所示:

图 4.23:df.tail()的输出

图 4.23:df.tail()的输出

绘制完整的平均温度值集(df.TempAvgF),以 Date 为 x 轴:

fig, ax = plt.subplots(figsize = (10, 7))

ax.scatter(df.Date, df.TempAvgF)

plt.show()

输出如下所示:

图 4.24:奥斯汀温度数据的多年变化图

图 4.24:奥斯汀温度数据的多年变化图

注意数据的周期性行为。考虑到温度随年度天气周期变化,这是合理的。

构建一个自相关图(使用 statsmodels),以查看是否可以使用自回归模型来预测平均温度。自回归模型的滞后期在哪些地方是可接受的,哪些地方则不可接受?请查看以下代码:

max_lag = 730

fig, ax = plt.subplots(figsize = (10, 7))

acf_plot = plot_acf(x = df.TempAvgF, ax = ax, lags = max_lag, \

use_vlines = False, alpha = 0.9, \

title = '奥斯汀温度的自相关'

'vs. 滞后')

ax.grid(True)

ax.text(280, -0.01, '90%置信区间', fontsize = 9)

ax.set_xlabel('滞后', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

图表应如下所示:

图 4.25:自相关与滞后(天数)的关系

图 4.25:自相关与滞后(天数)的关系

只有当自相关线位于 90%置信区间之外时,滞后期才是可接受的,置信区间以阴影区域表示。请注意,在这种情况下,我们看到的是峰值和谷值,而不是稳定下降的 ACF 值。这应该符合你的直觉,因为原始数据展示了周期性模式。另外,请注意,存在非常强的正相关和负相关。在大约 180 天(半年)左右,可能会利用强烈的负相关,但那是一个更高级的时间序列话题,超出了我们这里的讨论范围。从图 4.25 中可以得出的主要结论是,在短滞后期后,ACF 有一个非常陡峭的下降。现在,使用与之前相同的方法查看滞后图与 ACF 的关系。

获取实际的 ACF 值:

corr0 = np.correlate(df.TempAvgF[0: ] - df.TempAvgF.mean(), \

df.TempAvgF[0: ] - df.TempAvgF.mean(), mode = 'valid')

corrs = [np.correlate(df.TempAvgF[:(df.TempAvgF.shape[0] - i)] \

  • df.TempAvgF.mean(), df.TempAvgF[i: ] \

  • df.TempAvgF.mean(), mode = 'valid')

for i in range(max_lag)] / corr0

我们需要在练习 4.01 中开发的相同实用网格绘图函数,用于创建自回归模型:

"""

用于绘制一系列滞后图的实用函数

展示自相关的图

"""

def plot_lag_grid(series, corrs, axis_min, axis_max, \

num_plots, total_lag, n_rows, n_cols):

lag_step = int(total_lag / num_plots)

fig = plt.figure(figsize = (18, 16))

for i, var_name in enumerate(range(num_plots)):

corr = corrs[lag_step * i]

ax = fig.add_subplot(n_rows, n_cols, i + 1)

ax.scatter(series, series.shift(lag_step * i))

ax.set_xlim(axis_min, axis_max)

ax.set_ylim(axis_min, axis_max)

ax.set_title('lag = ' + str(lag_step * i))

ax.text(axis_min + 0.05 * (axis_max - axis_min), \

axis_max - 0.05 * (axis_max - axis_min), \

'相关性 = ' + str(round(corr[0], 3)))

fig.tight_layout()

plt.show()

现在,既然我们已经有迹象表明我们对短滞后感兴趣,但也发现半年和一年附近有强相关性,让我们看看两个时间尺度:

plot_lag_grid(df.TempAvgF, corrs, df.TempAvgF.min(), \

df.TempAvgF.max(), 9, 45, 3, 3)

plot_lag_grid(df.TempAvgF, corrs, df.TempAvgF.min(), \

df.TempAvgF.max(), 9, 405, 3, 3)

短滞后的输出结果如下:

图 4.26:短滞后的滞后图

图 4.26:短滞后的滞后图

较长滞后的输出结果如下:

图 4.27:较长滞后的滞后图

图 4.27:较长滞后的滞后图

我们可以从图 4.26 看出,相关性从滞后 5 到 40 一直在下降。随着时间尺度的延长,图 4.27 显示相关性快速下降,然后随着滞后接近一年时逐渐改善。这与原始数据的图表直觉相吻合(附注——这应该加强 EDA 的重要性)。

我们从初步分析中预期,自回归模型将专注于相对短的滞后。让我们使用 statsmodelsAR 函数来构建一个模型并查看结果:

"""

statsmodels AR 函数构建自回归模型

使用所有默认设置,它将确定最大滞后

并提供所有模型系数

"""

model = AR(df.TempAvgF)

model_fit = model.fit()

模型拟合现在包含了所有模型信息

max_lag = model_fit.k_ar

"""

请注意,使用默认设置时,最大滞后是

计算方法为 round(12*(nobs/100.)**(1/4.))

请参见 www.statsmodels.org/devel/generated/statsmodels.tsa.ar_model.AR.fit.html#statsmodels.tsa.ar_model.AR.fit

"""

print('最大滞后:' + str(max_lag))

print('系数:\n' + str(model_fit.params))

我们想预测多远的未来

max_forecast = 365

从模型生成预测值

pred_temp = pd.DataFrame({'pred_temp': \

model_fit.predict(start = max_lag, \

end = df.shape[0] \

  • max_forecast - 1)})

附加日期以便可视化

pred_temp['Date'] = df.loc[pred_temp.index, 'Date'].reindex()

pred_temp.loc[(max(df.index) + 1):, 'Date'] = \

pd.to_datetime([max(df.Date) \

  • pd.Timedelta(days = i)

for i in range(1, max_forecast + 1)])

结果是一个具有最多 23 天滞后的模型:

图 4.28:奥斯汀温度数据的 AR 模型

图 4.28:奥斯汀温度数据的 AR 模型

在同一图表上绘制预测、预报和原始数据:

"""

可视化叠加在实际数据上的预测

以及对未来的外推

"""

fig, ax = plt.subplots(figsize = (10, 7))

ax.plot(df.Date, df.TempAvgF, c = "blue", \

linewidth = 4, label = '实际平均温度')

ax.plot(pred_temp.loc[0 : len(df.TempAvgF), 'Date'], \

pred_temp.loc[0 : len(df.TempAvgF), 'pred_temp'], \

c = "yellow", linewidth = 0.5, \

label = '预测温度')

ax.plot(pred_temp.loc[len(df.TempAvgF):, 'Date'], \

pred_temp.loc[len(df.TempAvgF):, 'pred_temp'], \

c = "red", linewidth = 2, \

label = '预测温度')

ax.set_xlabel('日期', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_title('奥斯汀德克萨斯州每日平均温度')

ax.tick_params(axis = 'both', labelsize = 12)

ax.legend()

plt.show()

输出结果如下:

图 4.29:奥斯汀温度预测与预报

图 4.29:奥斯汀温度预测与预报

让我们放大查看数据的最后 30 天,以及前 30 个预测值:

放大查看原始数据末端附近的窗口

window = 30

fig, ax = plt.subplots(figsize = (10, 7))

ax.plot(df.Date[(len(df.TempAvgF) - window) : len(df.TempAvgF)], \

df.TempAvgF[(len(df.TempAvgF) - window) : \

len(df.TempAvgF)], \

c = "blue", linewidth = 4, \

label = '实际平均温度')

ax.plot(pred_temp.Date.iloc[(-max_forecast \

  • window)😦-max_forecast)], \

pred_temp.pred_temp.iloc[(-max_forecast \

  • window)😦-max_forecast)], \

c = "red", linewidth = 2, label = '预测温度')

ax.plot(pred_temp.loc[len(df.TempAvgF):\

(len(df.TempAvgF) + window), 'Date'], \

pred_temp.loc[len(df.TempAvgF):\

(len(df.TempAvgF) + window), 'pred_temp'], \

c = "green", linewidth = 2, label = '预测温度')

ax.set_xlabel('日期', fontsize = 14)

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_title('奥斯汀德克萨斯州每日平均温度')

ax.tick_params(axis = 'both', labelsize = 12)

ax.set_xticks(pd.date_range(df.Date[len(df.TempAvgF) - window], \

df.Date[len(df.TempAvgF) - 1] \

  • pd.Timedelta(days = window), 5))

ax.legend()

plt.show()

我们将得到以下输出:

图 4.30:数据末端预测的详细情况

图 4.30:数据末端预测的详细情况

注释

若要访问此特定部分的源代码,请参考 https://packt.live/3hOXUQL.

你也可以在 https://packt.live/313Vmbl 在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。

现在活动已成功完成,升级 pandas 的版本,以便顺利运行书中其余部分的练习和活动。要升级 pandas,运行:

pip install pandas==1.0.3

5. 分类技术

第十四章:Activity 5.01: 普通最小二乘分类器 – 二元分类器

解决方案:

导入所需的依赖项:

import struct

import numpy as np

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.linear_model import LinearRegression

将 MNIST 数据加载到内存中:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read())).reshape((size, rows, cols))

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

可视化数据样本:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出结果如下:

图 5.63:样本数据

图 5.63:样本数据

构建一个线性分类器模型,用于分类数字 0 和 1。我们将要创建的模型是为了判断样本是数字 0 还是数字 1。为了实现这一点,我们首先需要选择仅包含这些样本的数据:

samples_0_1 = np.where((labels == 0) | (labels == 1))[0]

images_0_1 = img[samples_0_1]

labels_0_1 = labels[samples_0_1]

samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1))

images_0_1_test = img_test[samples_0_1_test]\

.reshape((-1, rows * cols))

labels_0_1_test = labels_test[samples_0_1_test]

可视化选定的信息。以下是数字 0 的代码:

sample_0 = np.where((labels == 0))[0][0]

plt.imshow(img[sample_0], cmap='gray');

输出结果如下:

图 5.64:第一个样本数据

图 5.64:第一个样本数据

以下是数字 1 的代码:

sample_1 = np.where((labels == 1))[0][0]

plt.imshow(img[sample_1], cmap='gray');

输出结果如下:

图 5.65:第二个样本数据

图 5.65:第二个样本数据

为了将图像信息提供给模型,我们必须先将数据展平,使每张图像的形状为 1 x 784 像素:

images_0_1 = images_0_1.reshape((-1, rows * cols))

images_0_1.shape

输出结果如下:

(12665, 784)

让我们构建模型;使用 LinearRegression API 并调用 fit 函数:

model = LinearRegression()

model.fit(X=images_0_1, y=labels_0_1)

输出结果如下:

LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,

normalize=False)

确定训练集准确性:

model.score(X=images_0_1, y=labels_0_1)

输出结果如下:

0.9705320567708795

确定每个训练样本的标签预测值,使用 0.5 的阈值。大于 0.5 的值分类为 1,而小于或等于 0.5 的值分类为 0:

y_pred = model.predict(images_0_1) > 0.5

y_pred = y_pred.astype(int)

y_pred

输出将如下所示:

array([0, 1, 1, ..., 1, 0, 1])

计算预测训练值与实际值的分类准确率:

np.sum(y_pred == labels_0_1) / len(labels_0_1)

输出将如下所示:

0.9947887879984209

  1. 比较在测试集上的性能:

y_pred = model.predict(images_0_1_test) > 0.5

y_pred = y_pred.astype(int)

np.sum(y_pred == labels_0_1_test) / len(labels_0_1_test)

输出将如下所示:

0.9938534278959811

注意

要访问此特定部分的源代码,请参阅 https://packt.live/3emRZAk。

您还可以在 https://packt.live/37T4bGh 上在线运行此示例。必须执行整个笔记本才能获得预期的结果。

活动 5.02:KNN 多类别分类器

导入以下包:

import struct

import numpy as np

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.neighbors import KNeighborsClassifier as KNN

将 MNIST 数据加载到内存中。

训练图像:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read())).reshape((size, rows, cols))

训练标签:

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

测试图像:

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

测试标签:

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

可视化数据样本:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出将如下所示:

图 5.66:样本图像

图 5.66:样本图像

构建一个 k=3 的 KNN 分类器来分类 MNIST 数据集。同样,为了节省处理能力,随机抽取 5,000 张图像用于训练:

np.random.seed(0)

selection = np.random.choice(len(img), 5000)

selected_images = img[selection]

selected_labels = labels[selection]

为了将图像信息提供给模型,我们必须首先将数据展平,使得每个图像的形状为 1 x 784 像素:

selected_images = selected_images.reshape((-1, rows * cols))

selected_images.shape

输出将如下所示:

(5000, 784)

构建三邻居 KNN 模型并拟合数据到模型中。请注意,在此活动中,我们为模型提供了 784 个特征或维度,而不仅仅是 2:

model = KNN(n_neighbors=3)

model.fit(X=selected_images, y=selected_labels)

输出将如下所示:

KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',

metric_params=None, n_jobs=None, n_neighbors=3, p=2,

weights='uniform')

确定模型在训练集上的得分:

model.score(X=selected_images, y=selected_labels)

输出将如下所示:

0.9692

显示模型对训练数据的前两个预测结果:

model.predict(selected_images)[:2]

plt.subplot(1, 2, 1)

plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray');

plt.axis('off');

plt.subplot(1, 2, 2)

plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray');

plt.axis('off');

输出将如下所示:

图 5.67:测试集的前两个值

图 5.67:测试集的前两个值

比较模型在测试集上的表现:

model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)

输出将如下所示:

0.9376

注意

要访问此特定部分的源代码,请参考 https://packt.live/313xdlc。

您也可以在 https://packt.live/2Nl6DMo 上在线运行此示例。您必须执行整个 Notebook 才能获得预期结果。

活动 5.03:使用 CART 决策树进行二分类

解决方案:

导入所需的依赖项:

import struct

import numpy as np

import pandas as pd

import gzip

import urllib.request

import matplotlib.pyplot as plt

from array import array

from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier

加载 MNIST 数据到内存:

with gzip.open('../Datasets/train-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img = np.array(array("B", f.read())).reshape((size, rows, cols))

with gzip.open('../Datasets/train-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels = np.array(array("B", f.read()))

with gzip.open('../Datasets/t10k-images-idx3-ubyte.gz', 'rb') as f:

magic, size, rows, cols = struct.unpack(">IIII", f.read(16))

img_test = np.array(array("B", f.read()))\

.reshape((size, rows, cols))

with gzip.open('../Datasets/t10k-labels-idx1-ubyte.gz', 'rb') as f:

magic, size = struct.unpack(">II", f.read(8))

labels_test = np.array(array("B", f.read()))

可视化数据的一个样本:

for i in range(10):

plt.subplot(2, 5, i + 1)

plt.imshow(img[i], cmap='gray');

plt.title(f'{labels[i]}');

plt.axis('off')

输出将如下所示:

图 5.68:样本数据

图 5.68:样本数据

构建一个线性分类器模型来分类数字 0 和 1。我们将要创建的模型是确定样本是否为数字 0 或 1。为此,我们首先需要仅选择这些样本:

samples_0_1 = np.where((labels == 0) | (labels == 1))[0]

images_0_1 = img[samples_0_1]

labels_0_1 = labels[samples_0_1]

samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1))

images_0_1_test = img_test[samples_0_1_test]\

.reshape((-1, rows * cols))

labels_0_1_test = labels_test[samples_0_1_test]

可视化选定的信息。以下是 0 的代码:

sample_0 = np.where((labels == 0))[0][0]

plt.imshow(img[sample_0], cmap='gray');

输出将如下所示:

图 5.69:第一组样本数据

图 5.69:第一组样本数据

以下是 1 的代码:

sample_1 = np.where((labels == 1))[0][0]

plt.imshow(img[sample_1], cmap='gray');

输出将如下所示:

图 5.70:第二组样本数据

图 5.70:第二组样本数据

为了将图像信息提供给模型,我们必须先将数据展平,使得每个图像的形状为 1 x 784 像素:

images_0_1 = images_0_1.reshape((-1, rows * cols))

images_0_1.shape

输出将如下所示:

(12665, 784)

让我们构建模型;使用 DecisionTreeClassifier API 并调用 fit 函数:

model = DecisionTreeClassifier(random_state=123)

model = model.fit(X=images_0_1, y=labels_0_1)

model

输出将如下所示:

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=None, splitter='best')

确定训练集的准确率:

model.score(X=images_0_1, y=labels_0_1)

输出将如下所示:

1.0

使用 0.5 的阈值来确定每个训练样本的标签预测值。大于 0.5 的值分类为 1,小于或等于 0.5 的值分类为 0:

y_pred = model.predict(images_0_1) > 0.5

y_pred = y_pred.astype(int)

y_pred

计算预测的训练值与实际值的分类准确率:

np.sum(y_pred == labels_0_1) / len(labels_0_1)

与测试集的表现进行比较:

y_pred = model.predict(images_0_1_test) > 0.5

y_pred = y_pred.astype(int)

np.sum(y_pred == labels_0_1_test) / len(labels_0_1_test)

输出将如下所示:

0.9962174940898345

注意

若要访问此特定部分的源代码,请参阅 https://packt.live/3hNUJbT。

您还可以在线运行此示例,网址:https://packt.live/2Cq5W25。您必须执行整个 Notebook 才能获得期望的结果。

活动 5.04:使用人工神经网络进行乳腺癌诊断分类

导入所需的包。对于这个活动,我们将需要 pandas 包来加载数据,matplotlib 包用于绘图,scikit-learn 用于创建神经网络模型,以及将数据集分割为训练集和测试集。导入所有必要的包和相关模块以完成这些任务:

import pandas as pd

导入 matplotlib.pyplot

来自 sklearn.neural_network 的 MLPClassifier

来自 sklearn.model_selection 的 train_test_split

来自 sklearn 的预处理

使用 pandas 加载乳腺癌诊断数据集并检查前五行:

df = pd.read_csv('../Datasets/breast-cancer-data.csv')

df.head()

输出结果如下:

图 5.71:乳腺癌数据集的前五行

图 5.71:乳腺癌数据集的前五行

此外,拆分数据集为输入(X)和输出(y)变量:

X, y = df[[c for c in df.columns if c != 'diagnosis']], df.diagnosis

下一步是特征工程。该数据集的不同列具有不同的量级;因此,在构建和训练神经网络模型之前,我们对数据集进行归一化。为此,我们使用来自 sklearn 的 MinMaxScaler API,它将每一列的值归一化到 0 和 1 之间,正如本章的逻辑回归部分所讨论的那样(请参阅练习 5.03,逻辑回归–多类分类器):https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html:

X_array = X.values # 返回一个 numpy 数组

min_max_scaler = preprocessing.MinMaxScaler()

X_array_scaled = min_max_scaler.fit_transform(X_array)

X = pd.DataFrame(X_array_scaled, columns=X.columns)

检查归一化数据集的前五行:

X = pd.DataFrame(X_array_scaled, columns=X.columns)

X.head()

输出结果如下:

图 5.72:归一化数据集的前五行

图 5.72:归一化数据集的前五行

在我们构建模型之前,必须首先将诊断值转换为可以在模型中使用的标签。将“良性”诊断字符串替换为值 0,将“恶性”诊断字符串替换为值 1:

诊断类别 = ['良性', '恶性',]

输出 = [diagnoses.index(diag) for diag in y]

此外,为了公平地评估模型,我们应将训练数据集分割为训练集和验证集:

train_X, valid_X, \

train_y, valid_y = train_test_split(X, output, \

test_size=0.2, random_state=123)

使用归一化后的数据集和分配的诊断标签创建模型:

model = MLPClassifier(solver='sgd', hidden_layer_sizes=(100,), \

max_iter=1000, random_state=1, \

learning_rate_init=.01)

model.fit(X=train_X, y=train_y)

输出结果如下:

MLPClassifier(activation='relu', alpha=0.0001, batch_size='auto',

beta_1=0.9, beta_2=0.999, early_stopping=False,

epsilon=1e-08, hidden_layer_sizes=(100,),

learning_rate='constant',

learning_rate_init=0.01, max_iter=1000, momentum=0.9,

n_iter_no_change=10, nesterovs_momentum=True, power_t=0.5,

random_state=1, shuffle=True, solver='sgd', tol=0.0001,

validation_fraction=0.1, verbose=False, warm_start=False)

计算模型在验证集上的准确性:

model.score(valid_X, valid_y)

输出结果如下:

0.9824561403508771

注意

要访问此特定部分的源代码,请参考 https://packt.live/3dpNt2G。

你也可以在 https://packt.live/37OpdWM 上在线运行此示例。你必须执行整个 Notebook 才能得到期望的结果。

6. 集成建模

第十五章:活动 6.01:使用独立算法和集成算法的堆叠

解决方案

导入相关库:

导入 pandas 库为 pd

导入 numpy 为 np

导入 seaborn 为 sns

%matplotlib inline

导入 matplotlib.pyplot 为 plt

来自 sklearn.model_selection 导入 train_test_split

来自 sklearn.metrics 导入 mean_absolute_error

来自 sklearn.model_selection 导入 KFold

来自 sklearn.linear_model 导入 LinearRegression

来自 sklearn.tree 导入 DecisionTreeRegressor

来自 sklearn.neighbors 导入 KNeighborsRegressor

来自 sklearn.ensemble 导入 GradientBoostingRegressor, \

RandomForestRegressor

读取数据:

data = pd.read_csv('boston_house_prices.csv')

data.head()

注意

上面的代码片段假设数据集与练习笔记本在同一文件夹中。然而,如果您的数据集位于 Datasets 文件夹中,您需要使用以下代码:data = pd.read_csv('../Datasets/boston_house_prices.csv')

您将获得以下输出:

图 6.15:波士顿房价数据集的顶部几行

图 6.15:波士顿房价数据集的顶部几行

对数据集进行预处理,删除空值,为建模准备数据:

检查有多少列的空值比例小于 10%

perc_missing = data.isnull().mean()*100

cols = perc_missing[perc_missing < 10].index.tolist()

您将获得以下输出:

图 6.16:列的数量

图 6.16:列的数量

然后填补缺失值(如果有的话):

data_final = data.fillna(-1)

将数据集划分为训练集和验证集 DataFrame:

train, val = train, val = train_test_split(data_final, \

test_size=0.2, \

random_state=11)

x_train = train.drop(columns=['PRICE'])

y_train = train['PRICE'].values

x_val = val.drop(columns=['PRICE'])

y_val = val['PRICE'].values

初始化字典以存储训练集和验证集的 MAE 值:

train_mae_values, val_mae_values = {}, {}

使用以下超参数训练决策树(dt)模型并保存分数:

dt_params = {'criterion': 'mae', 'min_samples_leaf': 15, \

'random_state': 11}

dt = DecisionTreeRegressor(**dt_params)

dt.fit(x_train, y_train)

dt_preds_train = dt.predict(x_train)

dt_preds_val = dt.predict(x_val)

train_mae_values['dt'] = mean_absolute_error(y_true=y_train, \

y_pred=dt_preds_train)

val_mae_values['dt'] = mean_absolute_error(y_true=y_val, \

y_pred=dt_preds_val)

使用以下超参数训练 k-近邻(knn)模型并保存分数:

knn_params = {'n_neighbors': 5}

knn = KNeighborsRegressor(**knn_params)

knn.fit(x_train, y_train)

knn_preds_train = knn.predict(x_train)

knn_preds_val = knn.predict(x_val)

train_mae_values['knn'] = mean_absolute_error(y_true=y_train, \

y_pred=knn_preds_train)

val_mae_values['knn'] = mean_absolute_error(y_true=y_val, \

y_pred=knn_preds_val)

使用以下超参数训练随机森林(rf)模型并保存分数:

rf_params = {'n_estimators': 20, 'criterion': 'mae', \

'max_features': 'sqrt', 'min_samples_leaf': 10, \

'random_state': 11, 'n_jobs': -1}

rf = RandomForestRegressor(**rf_params)

rf.fit(x_train, y_train)

rf_preds_train = rf.predict(x_train)

rf_preds_val = rf.predict(x_val)

train_mae_values['rf'] = mean_absolute_error(y_true=y_train, \

y_pred=rf_preds_train)

val_mae_values['rf'] = mean_absolute_error(y_true=y_val, \

y_pred=rf_preds_val)

训练一个梯度提升回归(gbr)模型,使用以下超参数并保存评分:

gbr_params = {'n_estimators': 20, 'criterion': 'mae', \

'max_features': 'sqrt', 'min_samples_leaf': 10, \

'random_state': 11}

gbr = GradientBoostingRegressor(**gbr_params)

gbr.fit(x_train, y_train)

gbr_preds_train = gbr.predict(x_train)

gbr_preds_val = gbr.predict(x_val)

train_mae_values['gbr'] = mean_absolute_error(y_true=y_train, \

y_pred=gbr_preds_train)

val_mae_values['gbr'] = mean_absolute_error(y_true=y_val, \

y_pred=gbr_preds_val)

准备训练和验证数据集,其中四个元估计器使用与之前步骤相同的超参数。首先,构建训练集:

num_base_predictors = len(train_mae_values) # 4

x_train_with_metapreds = np.zeros((x_train.shape[0], \

x_train.shape[1]+num_base_predictors))

x_train_with_metapreds[:, :-num_base_predictors] = x_train

x_train_with_metapreds[:, -num_base_predictors:] = -1

kf = KFold(n_splits=5, random_state=11)

for train_indices, val_indices in kf.split(x_train):

kfold_x_train, kfold_x_val = x_train.iloc[train_indices], \

x_train.iloc[val_indices]

kfold_y_train, kfold_y_val = y_train[train_indices], \

y_train[val_indices]

predictions = []

dt = DecisionTreeRegressor(**dt_params)

dt.fit(kfold_x_train, kfold_y_train)

predictions.append(dt.predict(kfold_x_val))

knn = KNeighborsRegressor(**knn_params)

knn.fit(kfold_x_train, kfold_y_train)

predictions.append(knn.predict(kfold_x_val))

gbr = GradientBoostingRegressor(**gbr_params)

rf.fit(kfold_x_train, kfold_y_train)

predictions.append(rf.predict(kfold_x_val))

gbr = GradientBoostingRegressor(**gbr_params)

gbr.fit(kfold_x_train, kfold_y_train)

predictions.append(gbr.predict(kfold_x_val))

for i, preds in enumerate(predictions):

x_train_with_metapreds[val_indices, -(i+1)] = preds

准备验证集:

x_val_with_metapreds = np.zeros((x_val.shape[0], \

x_val.shape[1]+num_base_predictors))

x_val_with_metapreds[:, :-num_base_predictors] = x_val

x_val_with_metapreds[:, -num_base_predictors:] = -1

predictions = []

dt = DecisionTreeRegressor(**dt_params)

dt.fit(x_train, y_train)

predictions.append(dt.predict(x_val))

knn = KNeighborsRegressor(**knn_params)

knn.fit(x_train, y_train)

predictions.append(knn.predict(x_val))

gbr = GradientBoostingRegressor(**gbr_params)

rf.fit(x_train, y_train)

predictions.append(rf.predict(x_val))

gbr = GradientBoostingRegressor(**gbr_params)

gbr.fit(x_train, y_train)

predictions.append(gbr.predict(x_val))

for i, preds in enumerate(predictions):

x_val_with_metapreds[:, -(i+1)] = preds

训练一个线性回归(lr)模型作为堆叠模型:

lr = LinearRegression(normalize=True)

lr.fit(x_train_with_metapreds, y_train)

lr_preds_train = lr.predict(x_train_with_metapreds)

lr_preds_val = lr.predict(x_val_with_metapreds)

train_mae_values['lr'] = mean_absolute_error(y_true=y_train, \

y_pred=lr_preds_train)

val_mae_values['lr'] = mean_absolute_error(y_true=y_val, \

y_pred=lr_preds_val)

可视化每个单独模型和堆叠模型的训练和验证误差:

mae_scores = pd.concat([pd.Series(train_mae_values, name='train'), \

pd.Series(val_mae_values, name='val')], \

axis=1)

mae_scores

首先,你会得到以下输出:

图 6.17:训练和验证误差的值

图 6.17:训练和验证误差的值

现在,使用以下代码绘制 MAE 分数的条形图:

mae_scores.plot(kind='bar', figsize=(10,7))

plt.ylabel('MAE')

plt.xlabel('模型')

plt.show()

最终输出将如下所示:

图 6.18:训练和验证误差的可视化

图 6.18:训练和验证误差的可视化

注意

要访问此特定部分的源代码,请参考 https://packt.live/3fNqtMG。

你也可以在 https://packt.live/2Yn2VIl 在线运行这个示例。你必须执行整个笔记本才能获得预期的结果。

7. 模型评估

第十六章:活动 7.01:最终测试项目

导入相关库:

import pandas as pd

import numpy as np

import json

%matplotlib inline

import matplotlib.pyplot as plt

from sklearn.preprocessing import OneHotEncoder

from sklearn.model_selection import RandomizedSearchCV, train_test_split

from sklearn.ensemble import GradientBoostingClassifier

from sklearn.metrics import (accuracy_score, precision_score, \

recall_score, confusion_matrix, precision_recall_curve)

读取 breast-cancer-data.csv 数据集:

data = pd.read_csv('../Datasets/breast-cancer-data.csv')

data.info()

让我们将输入数据(X)和目标(y)分开:

X = data.drop(columns=['diagnosis'])

y = data['diagnosis'].map({'malignant': 1, 'benign': 0}.get).values

将数据集分割为训练集和测试集:

X_train, X_test, \

y_train, y_test = train_test_split(X, y, \

test_size=0.2, random_state=11)

print(X_train.shape)

print(y_train.shape)

print(X_test.shape)

print(y_test.shape)

应该得到以下输出:

(455, 30)

(455,)

(114, 30)

(114,)

选择一个基础模型,并定义要搜索超参数调整的模型的超参数值范围。让我们使用梯度提升分类器作为我们的模型。然后,以字典形式定义要调整的所有超参数的值范围:

meta_gbc = GradientBoostingClassifier()

param_dist = {'n_estimators': list(range(10, 210, 10)), \

'criterion': ['mae', 'mse'],\

'max_features': ['sqrt', 'log2', 0.25, 0.3, \

0.5, 0.8, None], \

'max_depth': list(range(1, 10)), \

'min_samples_leaf': list(range(1, 10))}

定义参数以初始化 RandomizedSearchCV 对象,并使用 K 折交叉验证来识别最佳模型超参数。定义随机搜索所需的参数,包括 cv 设为 5,表示使用 5 折交叉验证来选择超参数。然后初始化 RandomizedSearchCV 对象,并使用 .fit() 方法启动优化:

rand_search_params = {'param_distributions': param_dist, \

'scoring': 'accuracy', 'n_iter': 100, \

'cv': 5, 'return_train_score': True, \

'n_jobs': -1, 'random_state': 11 }

random_search = RandomizedSearchCV(meta_gbc, **rand_search_params)

random_search.fit(X_train, y_train)

应该得到以下输出:

图 7.36:RandomizedSearchCSV 对象

图 7.36:RandomizedSearchCSV 对象

调整完成后,找到获得最高平均测试分数的位置(迭代次数)。找到相应的超参数并保存到字典中:

idx = np.argmax(random_search.cv_results_['mean_test_score'])

final_params = random_search.cv_results_['params'][idx]

final_params

应该得到以下输出:

图 7.37:超参数

图 7.37:超参数

将训练数据集进一步拆分为训练集和验证集,并在训练数据集上使用最终的超参数训练一个新模型。使用 scikit-learn 的 train_test_split() 方法将 X 和 y 拆分为训练集和验证集,验证集占数据集的 15%:

train_X, val_X, \

train_y, val_y = train_test_split(X_train, y_train, \

test_size=0.15, random_state=11)

train_X.shape, train_y.shape, val_X.shape, val_y.shape

你应该得到以下输出:

((386, 30), (386,), (69, 30), (69,))

使用最终的超参数训练梯度增强分类模型,并根据训练和验证集进行预测。同时,计算验证集的概率:

gbc = GradientBoostingClassifier(**final_params)

gbc.fit(train_X, train_y)

preds_train = gbc.predict(train_X)

preds_val = gbc.predict(val_X)

pred_probs_val = np.array([each[1] \

for each in gbc.predict_proba(val_X)])

计算预测的准确度、精度和召回率,并打印混淆矩阵:

print('train accuracy_score = {}'\

.format(accuracy_score(y_true=train_y, y_pred=preds_train)))

print('validation accuracy_score = {}'\

.format(accuracy_score(y_true=val_y, y_pred=preds_val)))

print('confusion_matrix: \n{}'\

.format(confusion_matrix(y_true=val_y, y_pred=preds_val)))

print('precision_score = {}'\

.format(precision_score(y_true=val_y, y_pred=preds_val)))

print('recall_score = {}'\

.format(recall_score(y_true=val_y, y_pred=preds_val)))

你应该得到以下输出:

图 7.38: 评估得分和混淆矩阵

图 7.38: 评估得分和混淆矩阵

尝试不同的阈值,找到具有较高召回率的最佳点。

绘制精度-召回率曲线:

plt.figure(figsize=(10,7))

精度, 召回率, \

thresholds = precision_recall_curve(val_y, \

pred_probs_val)

plt.plot(recall, precision)

plt.xlabel('召回率')

plt.ylabel('精度')

plt.show()

输出将如下所示:

图 7.39: 精度-召回率曲线

图 7.39: 精度-召回率曲线

"""

绘制精度和召回率随阈值增加的变化。

"""

PR_variation_df = pd.DataFrame({'precision': precision, \

'recall': recall}, \

index=list(thresholds)+[1])

PR_variation_df.plot(figsize=(10,7))

plt.xlabel('阈值')

plt.ylabel('P/R 值')

plt.show()

你应该得到以下输出:

图 7.40: 精度和召回率随阈值增加的变化

图 7.40: 精度和召回率随阈值增加的变化

最终确定一个阈值,用于根据测试数据集进行预测。我们设定一个值,例如 0.05。该值完全取决于你在前一步探索中认为最合适的值:

final_threshold = 0.05

根据测试数据集预测最终值并将其保存到文件中。使用第 10 步中确定的最终阈值,找出训练集中每个值的类别。然后,将最终预测写入 final_predictions.csv 文件:

pred_probs_test = np.array([每个值[1] \

对于每个值 in gbc.predict_proba(X_test)])

preds_test = (pred_probs_test > final_threshold).astype(int)

preds_test

输出将如下所示:

图 7.41:测试数据集的最终值预测

图 7.41:测试数据集的最终值预测

或者,您也可以以 CSV 格式获取输出:

使用 open('final_predictions.csv', 'w') 打开文件:

f.writelines([str(val)+'\n' for val in preds_test])

输出将是如下的 CSV 文件:

图 7.42:最终值的输出

图 7.42:最终值的输出

注意

要访问此特定部分的源代码,请参阅 https://packt.live/2Ynw6Lt。

您也可以在 https://packt.live/3erAajt 在线运行此示例。必须执行整个 Notebook 才能得到期望的结果。

posted @ 2025-09-04 14:12  绝不原创的飞龙  阅读(16)  评论(0)    收藏  举报