第2章 端到端的机器学习项目

  在本章中,假设你是一位刚刚受雇于某房地产公司的数据科学家,并会完成一个端到端(end-to-end)的项目。下面是你需要经历的主要步骤:

  1. 衡量全局。
  2. 获取数据。
  3. 探索数据,对数据进行可视化,从而增加灵感。
  4. 准备数据,使其可直接用于机器学习算法。
  5. 选择一个模型并对其进行训练。
  6. 对模型进行微调。
  7. 展现你的解决方案。
  8. 启动、监视以及维护该系统。

使用真实数据

  在学习机器学习时,不要总是使用人工数据集进行实验,最好能够使用真实世界中的数据。幸运的是,现在已经存在数以千计的涵盖各个领域的开放数据集,常见的包括:

  在本章中,我们选择来自StatLib仓库的加州房屋价格数据集(图2-1)。该数据集基于1990年的加州人口普查数据。该数据集虽然不是最新的,但是它有许多值得我们学习的地方,因此我们先假设它就是最新的数据吧。为了方便教学,我们在该数据集中添加了一个分类属性并删除了一些特征。

图2-1. 加州房屋价格

衡量全局

  欢迎来到机器学习房产公司!你需要完成的第一个任务是利用加州人口普查数据构建一个用于预测加州房屋售价的模型。该人口普查数据包括加州每个街区组的人口、平均收入、房屋平均价格等特征信息。街区组(block group)是美国人口普查局发布数据的最小地理单位(一个街区组通常包括600至3000人口),为了方便,我们直接将街区组叫做“地区”。

  你构建的模型需要学习这些数据,并预测任何地区(给定该地区其他特征信息的条件下)的房屋平均售价。


小窍门

因为你是一个做事很有条理的数据科学家,你会先取出你的机器学习项目清单。附录B可以作为一个机器学习参考清单,它适用于大多数机器学习项目,不过你自己需要确保它满足你自己的需求。在本章中,我们会遍历该清单里面的大多数项,但是也会跳过几个不言而喻的或者在随后章节会讨论的项。

提出问题

  构建模型通常不是最终目标,因此你需要问你老板的第一个问题是该项目的确切的商业目标是什么?公司希望如何使用该模型?如何通过该模型获取收益?这一点非常重要,因为它决定着你应该如何分析问题、如何选择算法、如何对构建的模型进行性能评估以及它值得花费多少精力来调节该模型。

  老板的回答是,你的模型的输出(某地区的房屋平均价格的预测值)会与许多其它信号[注释]一起喂给另一个机器学习系统(图2-2)。这个下游系统会判断在某个给定区域是否值得投资。明确这一点是非常重要的,因为它直接影响着你所在公司的财政收入。

图2-2. 房地产投资的机器学习流水线


流水线

数据流水线(pipelines)指的是数据处理组件所构成的序列。流水线在机器学习系统中非常常见,因为要操作的数据量非常大,且很多数据还需要进行转换。

组件通常异步地运行。每个组件先读入大量的数据,然后处理数据,最后再将处理结果生成另一种数据。在某段时间之后,该流水线的下一个组件会读取并处理这些数据,生成自己的输出数据,以此类推。每个组件几乎都是自给自足的:组件间的接口仅仅是这些数据。流水线使得系统的控制非常简单,不同的团队可以专注于不同的组件。此外,如果某个组件发生故障,其下游的组件可以使用故障组件最后的输出来确保自己(至少在某段时间内)依然可以正确地运转。这使得整个架构非常健壮。

另一方面,如果没有实现合适的监视程序,则组件出现故障时不会被立即察觉,数据没有更新,从而整个系统的性能会降低。

  第二个需要问的问题是当前的解决方案是怎样的(如果存在),因为它能给你一个性能参考标准,并激发你解决该问题的灵感。老板的回答是地区的房屋价格是由专家手工评估的:有一个团队负责搜集关于该地区的最新信息,并使用一个复杂的规则来计算房屋的平均价格。

  这样的方法成本太高,也太耗费时间,且评估结果也不够好:他们发现他们自己评估出的房屋平均价格与真实价格之间存在超过10%的偏差。这也是为什么公司希望尝试通过该地区所已知的其它数据来构建一个可以预测该地区房屋平均价格的原因。人口普查数据看上去是一个不错的数据集,因为它包含数以千计的地区的房屋平均价格以及其它的一些数据。

  有了上面的这些信息,你可以开始设计你的系统了。首先,你需要提出问题:这是监督学习、非监督学习还是半监督学习?这是一个分类任务、回归任务还是其它某种任务?应该使用批学习(batch learning)还是在线学习(online learning)技术?请先暂停阅读,自己先思考一下这些问题。

  已经想到答案了吗?我们一起来看看:很明显,这是一个典型的监督学习任务,因为已知一些带标签的训练样本(每个实例都对应了一个输出——例如地区的房屋平均价格)。此外,这也是一个典型的回归任务,因为你需要去预测值。更具体地说,这是一个多变量回归问题,因为系统会使用多个特征进行预测(会使用地区的人口、平均收入等)。在第一章中,你只基于GDP这一个特征对人们的生活满意度进行预测,因此那是一个单变量回归问题。最后,由于没有连续的数据流进入系统,也没有快速调整数据的特殊需求,且数据足够小能直接填充在内存中,所以批学习就足够了。


小窍门

如果数据非常庞大,你可以在多个服务器上进行批学习(使用MapReduce技术,我们随后会介绍),或者你也可以直接使用在线学习技术。

选择一个性能度量方法

  下一步,你需要选择一个性能度量方法。回归问题最典型的性能度量方法是均方根误差(Root Mean Square Error, RMSE)。均方根误差用于衡量系统与其期望值之间的差距,值越大表示误差越大。公式2-1是均方根误差的公式。

公式2-1. 均方根误差(RMSE)


注意

这个公式引入了几个在机器学习领域非常常见的符号,这些符号也将贯穿在整本书中。

  • m是你正在使用RMSE进行度量的数据集的实例的个数。
    • 例如,如果你在包含2,000个地区的验证机上评估RMSE,则m=2,000
  • $\mathbf{x}^{(i)}$是数据集中第$i^{th}$个样本的所有特征构成(不包括标签)的向量,$y^{(i)}$是其对应的标签(即该样本的期望输出值)。

    • 例如,如果数据集中的第一个地区的经度是-118.29$^。$,纬度是33.91°,包含1,416个样本实例,平均收入是38,372美元,房屋平均价格是156,400美元(忽略其它特征),那么:并且:
  • $\mathbf{X}$是一个矩阵,包含了数据集中所有样本实例的所有特征值(不包括标签),每行代表一个样本实例,第$i^{th}$行等于$\mathbf{x}^{(i)}$的转置,记为$\mathbf{(x^{(i)})}^T$。

    • 例如,如果依然以之前的描述表示第一个区域,则矩阵$\mathbf{X}$则表示为:
  • h是你系统的预测函数,也叫作假设函数(hypothesis)。当你的系统给定某个样本实例的特征向量$\mathbf{x^{(i)}}$时,它会输出该样本实例对应的预测值$\hat{y}^{(i)}=h(\mathbf{(x^{(i)})})$。
    • 例如,如果你的系统预测在第一个地区的房屋平均价格是158,400美元,则$\hat{y}^{(1)}=h(\mathbf{x^{(1)}})=158,400$。该样本实例的预测误差是$\hat{y}^{(1)}-y^{(1)}=2,000$。
  • $RMSE(\mathbf{X}, h)$是你使用假设函数h时在样本上所测量出来的代价(cost)函数。
    我们使用小写斜体字表示标量值(例如$m$或者$y^{(i)}$)和函数名(例如$h$),小写粗体字表示向量(例如$\mathbf{x^{(i)}}$),大写粗体字表示矩阵(例如$\mathbf{X}$)。

  尽管均方根误差是回归任务首选的性能度量方法,但是在某些情形下,使用其它的函数可能更好。例如,假设存在很多极端地区。此时,你可以选择使用绝对平均误差(Mean Absolute Error, MAE)。绝对平均误差有时也叫作平均绝对离差(Average Absolute Deviation, AAD)。

公式2-2. 绝对平均误差

  RMSE和MAE这两种方法都是用于度量两个向量之间的距离,即测量预测向量与目标向量之间的距离。各种测量距离的方法包括:

  • 计算RMSE对应于欧几里得范数:这是你目前所熟悉的记号。它也叫作$ℓ_2$范数,记做$\mathbf{∥·∥_2}$(或直接为$\mathbf{∥·∥}$)。
  • 计算MAE对应于$ℓ_1$范数,记做$\mathbf{∥·∥_1}$。$ℓ_1$范数有时也叫作曼哈顿范数,因为如果你沿着城市的正交街区行走,它测量出来的是城市中两点之间的距离。
  • 一般地说,包含有$n$个元素的向量$v$的$ℓ_k$范数的定义是$\mathbf{∥v∥_k} = (|v_0|^k + |v_1|^k + |v_n|^k)^{\frac{1}{k}}$。$ℓ_0$表示想两种非零元素的个数,ℓ∞表示向量中绝对值最大的元素的绝对值。
  • 范数的下表索引越大,该范数越青睐于向量中的大值而忽略小值。这就是为什么RMSE比MAE对极端值更敏感。但是当极端值非常少见的时候(例如在一个钟形曲线中),RMSE的性能非常好,是最优选择。

检查假设

  最后,列举并验证(你或其它人所提出的)假设是一个非常好的做法,这有助于在项目的早期捕获严重的问题。例如,你的系统输出的地区的价格会被喂给下游机器学习系统,我们也假设这些价格是这样使用的。但是如果下游系统将价格转换为分类(例如”便宜“、”平均“或者”贵“)并使用这些分类而不直接使用价格呢?在这种情形下,获取完美的价格一点也不重要,你的系统只需要确保分类正确就足够了。如果这样的话,这个问题就不是回归任务而是分类任务了。相信你不希望在研究了数个月的回归系统后才发现这一点。

  幸运的是,当与负责下游系统的团队交流后,你能信誓旦旦地说他们确实需要实际的价格,而不是分类。Great!你已经准备好了,绿灯已经亮了,开始写代码吧!

获取数据

是时候撸起袖子加油干了!请拿好你的电脑,在Jupyter notebook中运行后续的代码示例。完整的代码示例已经托管到https://github.com/ageron/handson-ml

创建工作空间

首先,你需要安装好Python。你的系统中可能已经安装好Python了,如果没有,请前往官网https://www.python.org/进行下载并安装。

然后,你需要创建一个工作空间目录,用于保存机器学习代码和数据集。打开终端,输入如下的命令(在提示符$后面):

1
2
$ export ML_PATH="$HOME/ml"      # 可以修改为自己喜爱的目录
$ mkdir -p $ML_PATH

此外,你还需要一些额外的Python模块:Jupyter、NumPy、Pandas、Matplotlib以及Scikit-Learn。如果你已经安装好了,可以直接跳至”下载数据集”;如果没有还安装好,你可以有很多方法对他们(及其依赖)进行安装。你可以使用你的系统的包管理系统(例如Ubuntu的apt-get,macOS的MacPorts或者HomeBrew)或者使用Anaconda的包管理系统或者Python自带的包管理系统pip进行安装。下面的命令可以检查你的系统是否已经安装pip:

1
2
$ pip3 --version
pip 9.0.1 from [...]/lib/python3.5/site-packages (python 3.5)

你需要确保你安装的pip是最新版本(至少需要>1.4以上的版本),以支持二进制模块的安装(即wheels)。要升级pip模块,这接输入:

1
2
3
4
$ pip3 install --upgrade pip
Collecting pip
[...]
Successfully installed pip-9.0.1


创建一个隔离环境

如果你想在一个隔离环境中运行(强烈推荐,因为它能避免多个项目的库版本冲突),请先输入下面的pip命令安装virtualenv:
1
2
3
4
5
> $ pip3 install --user --upgrade virtualenv
> Collecting virtualenv
> [...]
> Successfully installed virtualenv
>

1
2
3
4
5
6
7
8
> 然后,通过下面的命令创建一个隔离的Python环境:
> ``` bash
$ cd $ML_PATH
$ virtualenv env
Using base prefix '[...]'
New python executable in [...]/ml/env/bin/python3.5
Also creating executable in [...]/ml/env/bin/python
Installing setuptools, pip, wheel...done.

之后,每当你想激活该环境的时候,只需要打开终端并输入:

1
2
3
> $ cd $ML_PATH
> $ source env/bin/activate
>

1
2
3
4
5
6
> 当环境激活后,你使用pip命令安装的所有软件包都会被安装到这个隔离环境中,Python只会访问这些包(如果你希望访问系统的软件包,在创建环境的时候加上virtualenv的`--system-site-packages`选项)。更多信息请查看virtualenv的文档。

现在,你可以简单地使用pip命令安装所需要的模块以及它们的依赖(如果你没有使用virtualevn,你可能需要管理员权限或者加上`--user`选项):

​``` bash
$ pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learnCollecting jupyter Downloading jupyter-1.0.0-py2.py3-none-any.whlCollecting matplotlib [...]

下面的命令可用于检验这些依赖是否安装成功:

1
$ python3 -c "import jupyter, matplotlib, numpy, pandas, scipy, sklearn"

如果没有任何输出信息和错误信息,则说明安装成功。现在,你可以输入如下的命令来启动Jupyter:

1
2
3
4
5
6
$ jupyter notebook
[I 15:24 NotebookApp] Serving notebooks from local directory: [...]/ml
[I 15:24 NotebookApp] 0 active kernels
[I 15:24 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
[I 15:24 NotebookApp] Use Control-C to stop this server and shut down all
kernels (twice to skip confirmation).

上面的命令会启动一个Jupyter服务,并监听8888端口。你可以在你的网页浏览器中打开 http://localhost:8888 来访问该服务(当服务启动时通常会自动打开)。你将会在网页中看到空的工作空间目录(如果使用了virtualenv的话,会包含一个env目录)。

点击按钮New新建一个Python notebook并选择一个合适的Python版本(图2-3)。

该步骤做了三件事儿:首先,在你的工作空间中创建了一个名为Untitled.ipyno的新的notebook文件;第二,启动了一个Python内核来运行这个notebook;第三,在新标签中打开这个notebook。我们先将其重名了为”Housing”:点击Untutiled并输入新Housing(文件名会自动重命名为Housing.ipynb)。

图2-3. 新建Python notebook

notebook包含许多单元格,每个单元格可以包含可执行代码或者格式化文本。我们新建的notebook当前只包含一个标签为“In [1]:”的空单元格。在单元格中输入print("Hello world!"),然后点击运行按钮或者输入Shift+Enter(图2-4),单元格的内容就会被发送给这个notebook的Python内核。内核会运行这段代码,然后返回其输出,输出结果将显示在本单元格的下面。由于已经到了notebook的末尾,因此会自动创建一个新的单元格。请继续阅读Jupyter帮助菜单中的用户接口教程学习更多的基础操作。

图2-3. Hello world

下载数据

通常,数据是存放在关系型数据库中(或其它常见的数据存储中)并遍布于多个表格、文档、文件中。要访问数据,你需要先获取证书、访问授权,并熟悉该数据的规划。不过在本项目中获取数据非常简单:你只需要下载一个压缩文件housing.tgz。这个压缩文件中包含一个名为housing.csv的CSV文件,而这个CSV文件里面包含所有的数据。

你可以使用浏览器进行下载,并运行tar xzf housing.tgz解压并提取CSV文件。但是我们这里创建一个函数来做这件事。这是非常有用的,尤其是数据有周期性改动的时候,因为这有助于实现自动化。如果你要在多个机器上安装数据集,则自动化地取数据就非常有用。

下面是获取数据的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
import tarfile
from six.moves import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"

def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
if not os.path.isdir(housing_path):
os.makedirs(housing_path)
tgz_path = os.path.join(housing_path, "housing.tgz")
urllib.request.urlretrieve(housing_url, tgz_path)
housing_tgz = tarfile.open(tgz_path)
housing_tgz.extractall(path=housing_path)
housing_tgz.close()

当调用fetch_housing_data()时,它会在你的工作空间创建datasets/housing目录,下载housing.tgz文件并提取housing.csv文件到该目录。

现在我们使用Pandas来加载数据,我们需要再写一个小函数来加载数据:

1
2
3
4
5
import pandas as pd

def load_housing_data(housing_path=HOUSING_PATH):
csv_path = os.path.join(housing_path, "housing.csv")
return pd.read_csv(csv_path)

该函数返回一个包含所有数据的Pandas DataFrame对象。

快速查看数据的结构

我们使用DataFrame的head()方法来查看数据的前5行(图2-5)。

图2-5. 数据集中前5行

每一行表示一个地区。一共有10个属性(图中只截取了前6个):longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomemedian_house_value以及ocean_proximity

info方法可用于查看数据的基本信息,尤其是总行数、每个属性的类型和非空值的个数(图2-6)。

图2-6. Housing info

可以看到,数据集中有20,640个样本实例,这对于标准的机器学习来说是非常少的,但是非常适合我们进行入门学习。需要注意到的是,total_bedrooms属性只有20,433个非空值,因此我们可以判定有207个地区没有统计该特征。这一点会在本章后续部分介绍。

ocean_proximity字段外,所有属性都是用数字表示的。ocean_proximity属性的类型是对象,因此它可以包含所有Python对象,但又由于数据是从CSV文件中加载而来的,所以它肯定是文本属性。在查看前5行时,你可能已经注意到,ocean_proximity这一列的内容是重复的,因此ocean_proximity可能是一个分类属性。你可以使用value_counts()方法查看ocean_proximity存在哪些分类以及每个分类包含多少地区:

1
2
3
4
5
6
7
>>> housing["ocean_proximity"].value_counts()
<1H OCEAN 9136
INLAND 6551
NEAR OCEAN 2658
NEAR BAY 2290
ISLAND 5
Name: ocean_proximity, dtype: int64

我们再看看其它字段。descrie()方法可用于显示数值属性的摘要(图2-7):

图2-7. 数值属性的摘要

countmeanmin以及max都是自解释的。注意,空值已经被忽略了(例如,total_bedroomscount是20,433,不是20,640)。std表示的是标准差,用于度量样本值的分散程度。25%50%75%表示的是相应的百分位值:一个百分位值表示小于该值的样本占总样本的比例是该百分位值对应的百分比。例如,在所有地区中,25%的地区的房屋平均价格小于18,50%的地区的房屋平均价格小于29,75%的地区的房屋的平均价格小于37。这些百分位值也被叫做25$^{th}$百分位(或者1$^{st}Q$)、中值以及75$^{th}$百分位(或者3$^{st}Q$)。

另一个快速获取你正在操作的数据的直观感受的方法是画出数值属性的直方图。直方图展示了在某个给定区间范围内(水平坐标轴)的样本实例的数量(竖直坐标轴)。你既可以每次画出一个属性的直方图,也可以调用hist()方法一次性画出整个数据集中所有数值属性的直方图(图2-8)。例如,可以看出,大约超过800个地区的房屋平均价格在100,000美元左右。

1
2
3
4
%matplotlib inline   # only in a Jupyter notebook
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()

注意

hist()方法依赖于Matplotlib,而Matplotlib又依赖于一个用户指定的图形后端来显示在屏幕上。因此,在画图之前需要指定Matplotlib使用什么后端。最简单的方法是使用Jupyter的魔法命令%matplotlib inline ,然后Jupyter会设置Matplotlib使用Jupyter自己的后端,然后图像会渲染在notebook里面。注意,在Jupyter notebook里面调用show()是可选,因为当单元格执行时Jupyter会自动画出图形。

图2-8. 所有数值属性的直方图

在这个直方图里面需要注意的几点:

  1. 首先,平均收入属性不像是以美元(USD)为单位的。当与收集数据的团队检查后,发现这些数据其实已经被缩放了,且大于15(实际上是15.0001)的被截断了,小于0.5(实际上是0.4999)的也被截断了。在机器学习中使用一个经过预处理的属性是比较常见的,这不会造成什么问题,但是你应当尽量理解这些数据是如何被处理的。
  2. 房屋平均年龄和平均价格也是被截断过的。后者可能是一个严重的因为,因为这是你的目标属性(标签),你的机器学习算法在学习时永远无法摆脱这个束缚。你需要与你的下游团队(即会使用你的系统的输出的团队)进行核查,判断这会不会影响他们。如果他们告诉你他们需要精确的预测,即甚至超过500,000美元,那么你有两种选择:
    a. 收集那些被截断过的地区的正确标签。
    b. 从训练集中移除这些地区(测试集中也移除,因为你的系统无法评估与测试是否超过了500,000美元)。
  3. 这些属性具有不同的比例,我们将在本章后面探讨特征缩放的时候再具体讨论。
  4. 最后,许多直方图都具有重尾分布(tail heavy):在平均值右边的样本扩展比左边扩展得更远。这会使得某些机器学习算法检测某些模式变得更困难。我们将在后面尝试转换这些属性,使其具有钟行分布。

希望你对我们当前正在处理的这个数据集有了更好的理解!

警告

等一下!在对数据进行更深入研究之前,你需要创建一个测试集,把它放在一边,并且不要查看它。

创建一个测试集

你可能很奇怪为什么需要将部分数据分开,毕竟我们目前对数据只做了简单的了解,而我们在决定使用什么算法前应当对其进行更深入的探索。是的,但是大脑是一个神奇的模式识别系统,它很容易过拟合:如果查看了测试集,它可能偶然会发现测试集中表面看起来有趣的模式,从而导致最终选择了一个有点特殊的模型。当你用测试集评估泛化误差时,评估过程会表现得太过自信,但是当你实际启动系统时表现的性能却达不到你的期望值。这叫做数据迁就(data snooping)偏差。

理论上,创建测试集非常简单:随机挑选一些样本实例(通常是数据集的20%)并将它们放到一边:

1
2
3
4
5
6
7
8
import numpy as np

def split_train_test(data, test_ratio):
shuffled_indices = np.random.permutation(len(data))
test_set_size = int(len(data) * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
return data.iloc[train_indices], data.iloc[test_indices]

然后可以像这样使用该函数:

1
2
3
>>> train_set, test_set = split_train_test(housing, 0.2)
>>> print(len(train_set), "train +", len(test_set), "test")
16512 train + 4128 test

是的,这的确有效,但是它不够完美:如果你再次运行程序,它会产生不同的测试集!这样多次过后,你(或者你的机器学习算法)将会看过所有的数据集——这是你期望避免的。

其中一种解决办法是在第一次运行时保存该测试集,今后再次运行时直接加载第一次所保存的测试集。另一种办法是在调用np.random.permutaion()前设置随机数生成器的种子(例如 np.random.seed(42)),然后上面的代码就会返回相同的打乱的索引。

但是当数据集有更新时,上面的两种方法都失效了。一个更常用的解决办法是使用每个样本实例的标识符来决定该样本是否要被放到测试集中(假设样本实例具有唯一的、稳定的标识符)。例如,你可以计算每个样本实例的哈希值,只保留哈希值的最后一个字节,如果这个值小于等于51(256的20%左右),就将其放到测试集中。这样能确保即使数据集有更新时多次运行程序也依然能保持一致性。新的数据集会包含新样本实例的20%,但是不会包含之前在训练集中的任何样本实例。下面是一种实现方法:

1
2
3
4
5
6
7
8
9
import hashlib

def test_set_check(identifier, test_ratio, hash):
return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio

def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
return data.loc[~in_test_set], data.loc[in_test_set]

不幸的是,房屋数据集并没有标识符列,这是最简单的解决办法是使用行索引作为ID:

1
2
housing_with_id = housing.reset_index()   # 添加一个index列
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

如果你使用行索引作为唯一标识符,你需要确保新数据被追加到员数据集的默认,并且确保每一行都永远不会被删掉。如果无法满足此要求,那么你可以尝试使用最稳定的特征来构建一个唯一标识符。例如,一个地区的经纬度在几百万年内都会保持稳定,所以你可以将他们结合起来构成一个ID:

1
2
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")

Scikit-Learn提供了一些用于将数据集拆分为多个子集的函数,其中最简单的函数是train_test_split,该函数的作用与我们前面所写的函数split_trian_test总体上相同,不过额外多了一点功能。首先,有一个参数random_state可用于设置随机数发生器种子;其次,你可以向它传递多个行数相同的数据集,它会以相同的索引拆分这些数据集(这非常有用,例如,如果你有一个独立的DataFrame标签):

1
2
3
from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

到目前为止,我们仅考虑了随机抽样的方法。如果你的数据集足够大(尤其是相对于属性的数量),随机抽样基本没啥问题;否则,你有引入抽样偏差的风险。当一个调查公司需要向1,000个人打电话咨询问题的话,他们不是简单地从电话簿中随机地挑选1,000个人,而是要确保这1,000个人非常具有代表性,能代表整个人口。例如,美国的人口组成是51.3%的女性和48.7%的男性,因此一个精心考虑的调查过程应当在调查样本中维持这个比例:513个女性核487个男性。这叫做分层抽样:总人口被划分成各个具有相似性的群组(阶层),从各个阶层抽样出的样本实例的个数需要确保测试集能够代表整个人口。如果你直接使用随机抽样,则会有10%的可能性生成一个不合理的数据集(女性人口少于49%或者多余54%),那么最终你的调查结果将会产生严重的偏差。

假设你正在与一个专家聊天,专家告诉你,平均收入是影响房屋平均价格的最重要的属性,那么你需要确保测试集能代表整个数据集中的收入分类。由于平均收入是一个连续的数值属性,你首先需要创建一个收入分类属性。我们再仔细地看看平均收入的直方图:大多数平均收入都聚集在$20,000-$50,000之间,但是有一小部分收入超过$60,000。确保你的数据集中每个阶层都有足够的样本实例非常重要,否则对各个阶层的评估可能会产生偏差。因此,最好不要有太多阶层,且每个阶层应当足够大。下面的代码穿件了一个收入分类属性:先将收入除以1.5(限制收入分类的个数),再使用ceil对其进行向上取整(确保分类是离散值),然后再将所有大于5的合并为分类5:

1
2
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)

最终的收入分类如图2-9所示:

图 2-9. 收入分类直方图

现在你可以基于收入分类进行分层抽样了。你可以直接使用Scikit-Learn提供的类StratifiledShuffleSplit

1
2
3
4
5
6
from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing, housing["income_cat"]):
strat_train_set = housing.loc[train_index]
strat_test_set = housing.loc[test_index]

我们再检查下结果是否是像我们期望的那样。我们可以直接查看测试集中的收入分类的比例:

1
2
3
4
5
6
7
>>> strat_test_set["income_cat"].value_counts() / len(strat_test_set)
3.0 0.350533
2.0 0.318798
4.0 0.176357
5.0 0.114583
1.0 0.039729
Name: income_cat, dtype: float64

你可以通过类似的代码测试输入分类在完整数据集中的比例。图2-10比较了整个数据集、使用分类抽样产生的数据集、使用纯随机抽样产生的数据集中输入分类的比例。可以看到,使用分层抽样产生的测试集的收入分类的比例与完整数据集的收入分类的比例几乎完全相同,而使用随机抽样产生的数据集的比例与前二者之间存在明显的偏差。

图 2-10. 分层抽样Vs随机抽样

最后,你还需要移除income_cat属性以确保数据恢复到原始状态。

1
2
for set_ in (strat_train_set, strat_test_set):
set_.drop("income_cat", axis=1, inplace=True)

我们花了大量的时间来产生测试集,其原因是:这是机器学习项目中容易被忽略却又非常重要的一部分。此外,我们在随后讨论交叉验证的时候还会用到这些思想。现在可以进入下一阶段了:探索数据。

探索并可视化数据

到目前为止,你只是匆匆瞥了一眼待数据,只有一些非常浅显的认识,现在我们希望更深入地探索这些数据。

首先,请先确保已经将测试集放一边了,我们先在只探索训练集。通常,如果训练集太大的话,需要先对数据集进行抽样,以确保操作简单、快速。不过,我们这里的数据集足够小,因此我们可以直接操作整个数据集。首先创建一个副本,我们在这里操作的是副本,这样就不会影响原始训练集;

1
housing = strat_train_set.copy()

地理数据的可视化

由于数据中存在地理信息(经度和纬度),因此我们可以创建一个由所有地区构成的散点图对数据进行可视化:

1
housing.plot(kind="scatter", x="longitude", y="latitude")

图2-11. 数据的地理位置散点图

这看上去的确像加利福利亚州,但是除此之外你看不到任何特殊模式。我们可以设置alpha选项为0.1来简化图形且保持足够大的数据点密度:

1
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

图2-12. 更好的可视化——高亮高密度地区

现在看起来好多了:我们可以清晰地看到高密度地区,即湾区、洛杉矶周围、圣地牙哥周围以及中央谷的一条长线。

虽然我们的大脑通常非常擅长于在图像中发现某种模式,但是你仍然需要调节某些可视化参数使该模式更明显。

我们来看看房屋价格(图2-13)。每个小圆圈的半径代表该地区人口数(选项$s$),颜色代表价格(选项$c$)。我们会使用一个预定义的颜色映射(选项$cmap$)——$jet$, 其指由蓝色(价格低)逐渐变为红色(价格高):

1
2
3
4
5
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
s=housing["population"]/100, label="population", figsize=(10,7),
c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
)
plt.legend()

查看数据的相互关系

Experimenting with Attribute Combinations

准备数据

数据清洗

处理文本和分类属性

自定义转换器

特征缩放

转换流水线

选择并训练模型

在训练集上进行训练并评估

更好的评估方式: 交叉验证

模型微调

网格搜索

随机搜索

Ensemble methods

Analyze the Best Models and Their Errors

在测试集上评估你的系统

启动、监视以及维护系统

试试看!

练习题