《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(二):端到端的机器学习

最近有时间看书了,把以前想看的书捡一下,翻译中顺便加上自己的理解,写个读书笔记。

假装最近聘请了一家房地产公司的数据科学家,然后通过一个示例项目,从头至尾了解机器学习。 以下是要完成的主要步骤:

  1. 查看大图。
  2. 获取数据。
  3. 发现并可视化数据以获得洞察力。
  4. 准备机器学习算法的数据。
  5. 选择一个模型并进行训练。
  6. 微调模型。
  7. 展示您的解决方案。
  8. 启动,监控和维护您的系统。

一、使用真实的数据

当正在学习机器学习时,最好是使用实际上真实世界的数据实验,不只是人工数据集。幸运的是,有成千上万的开放数据集可供选择,囊括了所有多的领域。这里有几个地方,可以看看得到的数据:

流行的开放数据存储库:

  • 加州大学欧文分校机器学习知识库
  • Kaggle数据集
  • 亚马逊的AWS数据集

元(数据)门户网站(他们列出的开放数据仓库):

  • http://dataportals.org/
  • http://opendatamonitor.eu/
  • http://quandl.com/

其他网页列出许多流行的开放式数据仓库:

  • 维基百科的机器学习数据集的列表
  • Quora.com问题
  • 数据集版(Subreddit)

我们从StatLib存储库中选择了California Housing Price数据集。此数据集的基础上,从1990年加州人口普查数据。确实这不正是最近的数据(你还可以在海湾地区支付的起一个不错的房子),但它有许多学习特质,所以我们将假装它是最新数据。我们还增加了分类属性和删除一些功能教学目的。

二、观察这张大图:

欢迎到机器学习房产公司!你被要求执行的第一个任务是要建立一个使用加州人口普查数据在美国加州房价的模型。此数据在加州每个块组具有度量,如人口、平均收入、平均住房价格等。 块组是美国人口调查局发布的样本数据的最小地理单位(一个块组通常具有600至3000人的人口)。我们只是把它们称为“地区”的简称。你的模型应该从这些数据中学习,并能够预测的中位房价在任何地区,给所有其他指标。

Note:由于您是一个组织良好的数据科学家,所以您要做的第一件事就是展出你的机器学习的项目清单。 您可以从附录B中的那个开始;它应该运作得相当好对于大多数机器学习项目,但要确保适应它您的需求。 我们将通过许多清单项目,但我们也会跳过一些,因为他们是自我解释或因为它们将在以后再讨论。

解决问题

问你的老板的第一个问题是业务目标究竟是什么;建设一个模型可能并不是最终目标。 公司如何期望使用和从这个模型受益? 这很重要,因为它将决定你如何解决问题,您将选择哪种算法,您将使用哪种性能指标评估您的模型,以及您应该花多少精力来调整它。你的老板回答你模型的输出(对区域中位数的预测-价格)与许多其他信号(送入机器学习系统的一条信息通常被称为参考香农信息论的信号:你想有一个高的信号/噪声比)将被送到另一个机器学习系统。 这个下游系统将确定它是否值得投资于某一特定领域与否。 做到这一点至关重要,因为它直接影响到收入。

管线
数据处理组件中的序列被称为一数据流水线。管道是机器学习系统很常见的,因为有很多数据操纵和许多数据转换申请的。

组件通常异步运行。每个组件在一个大数据量的压入,对其进行处理,并推出的结果在另一数据存储,然后一段时间后,在管道中的下一个组件压入这些数据,并推出了自己的输出等等。每部件是相当自包含:部件之间的界面是简单地将数据存储。这使得系统非常简单把握(与数据流图的帮助下),以及不同的团队可以专注于不同的部件。此外,如果组件破裂下来,下游组件可经常继续通过仅使用从破碎的最后输出(至少暂时的)正常运行零件。这使得建筑十分强劲。

在另一方面,损坏的元件可以被忽视了一段时间,如果适当的监测未实现。得到的数据陈旧,整个系统的性能下降。

接下来的问题是目前的解决方案是什么样子(无论如何)。它会经常给你做参考性能,以及如何解决这个问题的见解。你的老板回答说,在区房价格目前专家估计手动:一个团队收集关于区上的最新信息,当他们无法获得中位数的房价,他们估计它使用复杂的规则。这是昂贵且耗时,而且他们的估计并不是很好; 他们的典型错误率约为15%。

有了所有这些信息,您现在可以开始设计系统了。首先,您需要解决问题:是监督,无人监督还是增强学习?它是分类任务,回归任务还是其他什么?您应该使用批量学习还是在线学习技巧?在您继续阅读之前,请暂停和试着为自己回答这些问题。

找到了答案吗?让我们看看:这显然是一种典型的监督学习任务。因为给了你标记的训练样例(每个实例都带有预期的产出,即该区的中位房价)。此外,它也是一个典型的回归任务,因为要求你预测一个值。更具体地说,这是一个多变量由于系统将使用多个功能进行预测,因此会出现回归问题(它将使用该地区的人口,收入中位数等)。在前一篇中,你预测生活满意度只基于一个特征,人均GDP,所以它是一个单变量回归问题。最后,没有连续的数据流进入在系统中,没有特别需要适应快速变化的数据和数据小到足以适应内存,所以普通批量学习应该做得很好。

Note:如果数据量巨大,您可以拆分批量学习,跨多个服务器工作(使用MapReduce技术,我们将在后面看到),或者你可以使用在线学习技术。

选择性能指标

下一步是选择性能指标。 一个典型的性能指标回归问题是均方根误差(RMSE)。 它衡量标准系统在其预测中所犯错误的偏差。 例如,RMSE等于50,000意味着系统预测的约68%落在50,000美元之内实际价值,大约95%的预测落在实际的100,000美元之内值。计算RMSE的数学公式:

批注

这个等式引入了几个非常常见的机器学习符号将在本书中使用:

  • m是您测量RMSE的数据集中的实例数。

----例如,如果您在2,000个区域的验证集上评估RMSE,则m = 2,000。

  • x^{(i)}是在数据集中的第i个实例的所有特征值(不包括标签)的矢量,而y^{(i)}是它的标签(该实例的期望的输出值)。

----例如,如果数据集中的第一个区域位于经度-118.29°,纬度33.91°,并且它有1,416个居民,中位收入为38,372美元,房屋中值为156,400美元(暂时忽略其他功能),然后:

和:

  • X是包含所有实例的所有特征值(不包括标签)的矩阵数据集。 每个实例有一行,第i行等于转置x^{(i)},即(x^{(i)})^{T}

----例如,如果第一个区域如上所述,则矩阵X看起来像这样:

  • h是系统的预测函数,也称为假设。 当您的系统被赋予实例的特征向量x^{(i)}时,它为该实例输出预测值\hat{y}^{(i)}= h(x^{(i)})(ŷ发音为“y-hat”)。

----例如,如果您的系统预测第一区的中位房价是158,400美元,那么\hat{y}^{(1)}= h(x^{(1)})= 158,400。该区域的预测误差为\hat{y}^{(1)} - y^{(1)}= 2,000。

  • RMSE(X,h)是使用您的假设h在示例集上测量的成本函数。

我们使用小写斜体字体作为标量值(例如m或y^{(i)})和函数名称(例如h),矢量的小写粗体字体(例如x^{(i)})和大写粗体字体
矩阵(如X)。

 

尽管RMSE通常是回归任务的首选性能指标,但在某些情况下,您可能更喜欢使用其他功能。 例如,假设有许多异常区。 在这种情况下,你可以考虑使用平均绝对误差(也称为平均绝对偏差):

RMSE和MAE都是测量两个向量之间距离的方法:预测的向量目标值的向量。 各种距离测量,或规范,是可能的:

  • 计算平方和的根(RMSE)对应于欧几里德范数:它是你熟悉的距离概念。它也被称为l_{2}范数,即\left \| \cdot \right \|_{2}(或只是\left \| \cdot \right \|)。
  • 计算绝对值之和(MAE)对应于l_{1}范数,记为\left \| \cdot \right \|_{1}。它有时被称为曼哈顿规范,因为它只能测量城市中两点之间的距离,如果你只能沿着正交的城市街区旅行。
  • 更一般地,包含n个元素的向量v的l_{k}范数被定义为\left \| v \right \|_{k} =( \left | v_{0} \right |^{k} + \left | v_{1} \right |^{k} + \cdot \cdot \cdot + \left | v_{n} \right |^{k}) ^{\frac{1}{k}} \cdot l_{0}只给出了向量的基数(即元素的数量),l_{\infty}给出向量中的最大绝对值。
  • 标准指数越高,它越关注大值而忽略小值。 这就是RMSE对异常值比MAE更敏感的原因。 但是当异常值呈指数级罕见时(如钟形曲线),RMSE会执行很好,通常是首选。

检查假设

最后,列出并验证迄今为止(由你或其他人)做出的假设是一种良好做法;这可以在早期发现严重的问题。例如,系统输出的区域价格将被输入下游机器学习系统,我们假设这些价格将被这样使用。但是,如果下游系统实际将价格转换为类别(例如,“便宜”,“中等”或“昂贵”),然后使用这些类别而不是价格本身呢?在这种情况下,完全正确地获得价格并不重要;你的系统只需要正确的类别。如果是这样,那么问题应该被定义为分类任务,而不是回归任务。你不希望发现这一点,尤其是在回归系统已经工作数月之后。

幸运的是,在与负责下游系统的团队交谈之后,您确信他们确实需要实际价格,而不仅仅是类别。太棒了!你已经完成了设置,灯光是绿色的,你现在可以开始编码!

三、获取数据

是时候动手了。 不要犹豫拿起您的笔记本电脑,并在Jupyter notebook中浏览以下代码示例。完整的Jupyter笔记本可在https://github.com/ageron/handson-ml上找到。

创建工作区
首先,需要安装Python。 它可能已经安装在系统上。 如果没有,可以访问https://www.python.org/
接下来,需要为机器学习代码和数据集创建工作空间目录。 打开终端并键入以下命令(在$提示后):

export ML_PATH =“$HOME/ml”
mkdir -p $ML_PATH

你需要一些Python模块:Jupyter,NumPy,Panda,Matplotlib和Scikit-learn。 如果你已经有Jupyter安装所有这些模块在运行,可以跳过到“下载数据”。也可以使用系统的打包系统(例如,在Ubuntu上使用apt-get,在macOS上使用MacPorts或HomeBrew),安装科学Python发行版(如Anaconda)并使用其包装系统,或者只使用Python自己的包装系统pip,这是默认包含在Python二进制安装程序中(自Python 2.7.9起)。你可以通过键入以下命令来检查是否安装了pip:

pip3 --version

您应确保安装了最新版本的pip,至少版本> 1.4以支持二进制模块安装(像 wheels)。要升级pip模块,请键入:

pip3 install --upgrade pip

创建一个孤立的环境

如果您希望在隔离环境中工作(强烈建议您这样做,以便可以在不存在冲突的库版本的情况下处理不同的项目),请通过运行以下pip命令安装virtualenv:

pip3 install --user --upgrade virtualenv

现在您可以通过键入以下内容来创建隔离的Python环境:

cd $ML_PATH
virtualenv env

现在,每次要激活此环境时,只需打开终端并键入:

cd $ML_PATH
source env/bin/activate

当环境处于活动状态时,使用pip安装的任何软件包都将安装在此隔离环境中,而Python只能访问这些软件包(如果您还想访问系统的站点软件包,则应使用virtualenv的--system-site-packages选项创建环境)。 查看virtualenv的文档以获取更多信息。

现在,你可以使用以下简单的pip命令安装所有必需的模块及其依赖项:

pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learn

要检查安装,请尝试导入每个模块,如下所示:

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

应该没有输出也没有错误。 现在你可以输入以下命令启动Jupyter:

jupyter notebook

Jupyter服务器现在在你的终端中运行,侦听端口8888。可以通过将Web浏览器打开到http://localhost:8888/来访问此服务器(这通常在服务器启动时自动发生)。你应该看到空的工作空间目录(如果你遵循前面的virtualenv说明,则只包含env目录)。

现在通过单击New按钮并选择适当的Python版本来创建一个新的Python笔记本。


这有三件事:首先,它在你的工作区中创建一个名为Untitled.ipynb的新笔记本文件; 第二,它启动了一个Jupyter Python内核来运行这个笔记本;第三,它在一个新标签中打开这个笔记本。应该首先将此笔记本重命名为“Housing”(这将自动将文件重命名为Housing.ipynb),方法是单击Untitled并键入新名称。

notebook包含单元格列表。 每个单元格可以包含可执行代码或格式化文本。现在,notebook只包含一个空代码单元,标记为“In [1]:”。 尝试在单元格中键入print(“Hello world!”),然后单击播放按钮或按Shift-Enter键。 这会将当前单元格发送到此notebook的Python内核,该内核运行它并返回输出。 结果显示在单元格下方,因为我们到达了notebook的末尾,所以会自动创建一个新单元格。 通过Jupyter的“帮助”菜单中的用户界面导览来学习基础知识。

在典型环境中,你的数据可以在关系数据库(或其他一些常见数据存储区)中获得,并分布在多个表/文档/文件中。要访问它,首先需要获取凭据和访问权限,并熟悉数据模式。然而,在这个项目中,事情要简单得多:你只需要下载一个压缩文件housing.tgz,它包含一个逗号分隔值(CSV)文件,名为housing.csv,包含所有数据。
可以使用Web浏览器下载它,并运行tar xzf housing.tgz来解压缩文件并解压缩CSV文件,但最好创建一个小函数来执行此操作。如果数据定期更改,它特别有用,因为它允许你编写一个小脚本,你可以在需要获取最新数据时运行该脚本(或者你可以设置定期作业以定期自动执行此操作)。如果需要在多台计算机上安装数据集,则自动获取数据的过程也很有用。

这是获取数据的函数:

import os
import tarfile
from six.moves import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = "datasets/housing"
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/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()时,它会在工作区中创建数据集/住房目录,下载housing.tgz文件,并从中提取housing.csv这个目录。现在使用Pandas加载数据。 应该再写一个小函数加载数据:

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()方法查看前五行。

每行代表一个区。 有10个属性(您可以在屏幕截图中看到前6个):经度,纬度,房屋平均年龄,房间,卧室房间,人口,住户,平均收入,平均房子价格和近海情况。info()方法可用于快速描述数据,特别是行总数,以及每个属性的类型和非空值的数量。

数据集中有20,640个实例,这意味着机器学习标准相当小,但它是完美的入门。 请注意,total_bed rooms属性只有20,433个非空值,这意味着207个区域缺少此功能。 我们稍后需要处理这个问题。
除ocean_proximity字段外,所有属性都是数字。 它的类型是object,因此它可以包含任何类型的Python对象,但是由于从CSV文件加载了这些数据,因此知道它必须是文本属性。 当查看前五行时,可能会注意到该列中的值是重复的,这意味着它可能是一个分类属性。 可以使用value_counts()方法找出存在哪些类别以及每个类别属于多少个区域:

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

我们来看看其他领域。 describe()方法显示了数字属性的摘要。

count,mean,min和max行是不言自明的。请注意,忽略空值(例如,total_bedrooms的计数为20,433,而不是20,640)。 std行显示标准偏差(测量值的分散程度)。
25%,50%和75%的行显示相应的百分位数:百分位数表示一组观察值中观察到的给定百分比下降的值。例如,25%的地区的Housing_median_age低于18,而50%低于29,75%低于37。这些通常被称为第25百分位(或第1四分位数),中位数和第75百分位数(或第3个四分位数)。
另一种快速了解您正在处理的数据类型的方法是为每个数字属性绘制直方图。直方图显示具有给定值范围(在水平轴上)的实例数(在垂直轴上)。你可以一次绘制一个属性,也可以在整个数据集上调用hist()方法,并为每个数字属性绘制直方图。例如,可以看到略超过800个区域的median_house_value等于约500,000美元。

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

Note:hist()方法依赖于Matplotlib,而matplotlib又依赖于用户指定的图形后端在屏幕上绘制。 所以在你可以绘制任何东西之前,你需要指定Matplotlib应该使用哪个后端。 最简单的选择是使用Jupyter的魔术命令%matplotlib inline。 这告诉Jupyter设置Matplotlib所以它使用Jupyter自己的后端。 然后在笔记本本身内呈现图。 请注意,在Jupyter笔记本中调用show()是可选的,因为Jupyter会在执行单元格时自动显示绘图。

请注意这些直方图中的一些内容:

  1. 首先,收入中位数属性看起来不像以美元(USD)表示。在与收集数据的团队核实后,你被告知数据已经缩放并且上限为15(实际为15.0001)以获得更高的中位数收入,以及0.5(实际上为0.4999)以获得更低的中位数收入。使用预处理属性在机器学习中很常见,它不一定是个问题,但你应该尝试了解数据的计算方式。
  2. 住房年龄中位数和房屋中位数也受到限制。后者可能是一个严重的问题,因为它是您的目标属性(您的标签)。您的机器学习算法可能会了解价格永远不会超出该限制。您需要咨询您的客户团队(将使用您系统输出的团队),看看这是否是一个问题。如果他们告诉您他们需要超过$ 500,000的精确预测,那么您主要有两种选择:a)为标签上限的区域收集适当的标签。b)从训练集(以及测试集)中删除这些区域,因为如果预测超出系统值$ 500,000,则不应对系统进行评估)。
  3. 这些属性的尺度差别很大。当我们探索特征缩放时,我们将在后面讨论这个问题。
  4. 最后,许多直方图都是尾重的:它们向中间的右侧延伸得比左侧更远。这可能会使某些机器学习算法难以检测模式。我们将尝试转换这些属性。

希望您现在能够更好地理解您正在处理的数据类型。

Warning:等一下! 在进一步查看数据之前,需要创建一个测试集,将其放在一边,永远不要管它。

创建测试集

在这个阶段自愿留出部分数据可能听起来很奇怪。 毕竟,您只需快速浏览一下数据,在决定使用哪种算法之前,你肯定应该学到更多关于它的信息,对吗? 这是事实,但你的大脑是一个惊人的模式检测系统,这意味着它很容易过度拟合:如果你看一下测试集,你可能会偶然发现测试数据中一些看似有趣的模式,导致你选择一个 特别的机器学习模型。 当你使用测试集估计泛化错误时,你的估计将过于乐观,并且你将启动一个性能不如预期的系统。 这称为数据窥探偏差
创建测试集在理论上非常简单:只需随机选择一些实例,通常是数据集的20%,并将它们放在一边:

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]

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

>>> 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.permutation()之前设置随机数生成器的种子(例如,np.random.seed(42)),以便它总是生成相同的混洗索引。

但是,下次获取更新的数据集时,这两种解决方案都会中断。 一个常见的解决方案是使用每个实例的标识符来决定它是否应该进入测试集(假设实例具有唯一且不可变的标识符)。 例如,你可以计算每个实例的标识符的哈希值,仅保留哈希值的最后一个字节,如果此值小于或等于51(256的~20%),则将实例放入测试集中。 这可确保测试集在多次运行中保持一致,即使刷新数据集也是如此。 新测试集将包含20%的新实例,但它不包含以前在训练集中的任何实例。
这是一个可能的实现:

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:

housing_with_id = housing.reset_index()
# adds an `index` column
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

如果将行索引用作唯一标识符,则需要确保将新数据附加到数据集的末尾,并且不会删除任何行。 如果无法做到这一点,那么可以尝试使用最稳定的功能来构建唯一标识符。
例如,区域的纬度和经度保证可以稳定几百万年,因此可以将它们组合成一个ID,如下所示:

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_train_test几乎完全相同,还有一些额外的功能。 首先有一个random_state参数,允许设置随机生成器种子,如前所述,其次可以传递多个具有相同行数的数据集,并将它们分成相同的索引(这非常有用,对于例如,如果有标签的单独DataFrame):

from sklearn.model_selection import train_test_split

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

到目前为止,我们已经考虑过纯随机抽样方法。如果您的数据集足够大(特别是相对于属性数量),这通常很好,但如果不是,则存在引入显着采样偏差的风险。当一家调查公司决定给1000个人打电话问他们几个问题时,他们不会在电话亭里随机挑选1000个人。他们试图确保这1000人代表整个人口。例如,美国人口由51.3%的女性和48.7%的男性组成,因此在美国进行的良好调查将试图在样本中保持这一比例:513名女性和487名男性。这称为分层抽样:将人口划分为称为分层的同质子群,并从每个层中抽取正确的实例数,以保证测试集代表总体人口。如果他们使用纯随机抽样,则有大约12%的机会对一组偏斜的测试集进行抽样,其中女性不到49%,女性多于54%。无论哪种方式,调查结果都会有很大偏差。
假设您与专家交谈过,他们告诉您收入中位数是预测房价中位数的一个非常重要的因素。您可能希望确保测试集代表整个数据集中的各种收入类别。
由于收入中位数是连续数字属性,因此首先需要创建收入类别属性。让我们更仔细地看一下收入中位数直方图:

大多数收入中位数集中在2-5(数万美元)左右,但是一些收入中位数远远超过6。在每个阶层的数据集中有足够数量的实例,或者估计阶层的重要性可能有偏见。 这意味着你不应该有太多的阶层,每个阶层应该足够大。 以下代码通过将收入中位数除以1.5(以限制收入类别的数量),并使用ceil(具有离散类别)进行四舍五入,然后将大于5的所有类别合并为类别5来创建收入类别属性:

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

现在,已准备好根据收入类别进行分层抽样。 为此,你可以使用Scikit-Learn的StratifiedShuffleSplit类:

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]

让我们看看这是否按预期工作。 可以从完整住房数据集中查看收入类别比例开始:

>>> housing["income_cat"].value_counts() / len(housing)
3.0
0.350581
2.0
0.318847
4.0
0.176308
5.0
0.114438
1.0
0.039826
Name: income_cat, dtype: float64

使用类似的代码,可以测量测试集中的收入类别比例。
下图比较了整体数据集中的收入类别比例,使用分层抽样生成的测试集和使用纯随机抽样生成的测试集。 如你所见,使用分层抽样生成的测试集的收入类别比例几乎与完整数据集中的相同,而使用纯随机抽样生成的测试集非常偏斜。

现在你应该删除income_cat属性,以便数据恢复到原始状态:

for set in (strat_train_set, strat_test_set):
    set.drop(["income_cat"], axis=1, inplace=True)

我们在测试集生成上花了相当多的时间是有充分理由的:这是机器学习项目中经常被忽视但关键的部分。 此外,当我们讨论交叉验证时,其中许多想法将会有用。 现在是时候进入下一阶段:探索数据。

四、发现并可视化数据以获得洞察力

到目前为止,只需快速浏览一下数据,就可以大致了解正在操作的数据类型。 现在的目标是更深入一点。
首先,确保你已将测试集放在一边,而只是在探索训练集。 此外,如果训练集非常大,你可能需要对探测集进行采样,以便轻松快速地进行操作。 在我们的例子中,该集很小,所以你可以直接在全套上工作。 让我们创建一个副本,这样你就可以在不损害训练集的情况下使用它:

housing = strat_train_set.copy()

可视化地理数据

由于存在地理信息(纬度和经度),因此最好创建所有区域的散点图以显示数据:

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

这看起来像加利福尼亚州,但除此之外很难看到任何特定的模式。 将alpha选项设置为0.1可以更容易地显示存在高密度数据点的位置:

现在情况要好得多:你可以清楚地看到高密度区域,即湾区和洛杉矶和圣地亚哥周围地区,以及中央山谷中相当高密度的长线,特别是萨克拉门托和弗雷斯诺周围。更一般地说,我们的大脑非常善于在图片上发现图案,但您可能需要使用可视化参数来使图案突出。

现在让我们来看看房价。 每个圆的半径代表区的人口(选项s),颜色代表价格(选项c)。 我们将使用名为jet的预定义颜色映射(选项cmap),其范围从蓝色(低值)到红色(高价):

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

这张图片告诉你,房价与地点(例如,靠近海洋)和人口密度有很大关系,正如你可能已经知道的那样。使用聚类算法检测主群集并添加测量群集中心距离的新功能可能会很有用。 尽管在北加利福尼亚州,海洋邻近属性也可能是有用的。

寻找相关性

由于数据集不是太大,可以使用corr()方法轻松计算每对属性之间的标准相关系数(也称为Pearson's r):

corr_matrix = housing.corr()

现在让我们看看每个属性与房屋中值的相关程度:

>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value  1.000000
median_income       0.687170
total_rooms         0.135231
housing_median_age  0.114220
households          0.064702
total_bedrooms      0.047865
population         -0.026699
longitude          -0.047279
latitude           -0.142826
Name: median_house_value, dtype: float64

相关系数的范围从-1到1。当它接近1时,表示存在强正相关; 例如,当收入中位数上升时,房屋中位数价值往往会上升。 当系数接近-1时,意味着存在强烈的负相关;可以看到纬度和房价中位数之间存在一个小的负相关关系(即当你往北时,价格会略有下降趋势)。 最后,接近零的系数意味着没有线性相关。 下图显示了各种图以及它们的水平轴和垂直轴之间的相关系数。

Warning :相关系数仅测量线性相关(“如果x上升,则y通常上升/下降”)。它可能完全错过非线性关系(例如,“如果x接近零,那么y通常会上升”)。注意底行的所有图都具有等于零的相关系数,尽管它们的轴明显不是独立的:这些是非线性关系的例子。 此外,第二行示出了相关系数等于1或-1的示例;请注意,这与斜率无关。 例如,以英寸为单位的高度的相关系数为1,高度为英尺或纳米。

检查属性之间相关性的另一种方法是使用Pandas的scatter_matrix函数,该函数将每个数字属性与每个其他数字属性相对应。 由于现在有11个数字属性,你会得到11^{2} = 121个图,这些图不适合页面,所以让我们只关注一些似乎与中位数房屋价值最相关的有希望的属性:

from pandas.tools.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))

如果Pandas将每个变量相对于自身绘制,那么主对角线(左上角到右下角)将充满直线,这不是很有用。 因此,Pandas显示每个属性的直方图(其他选项可用;有关详细信息,请参阅Pandas的文档)。预测房屋中值的最有希望的属性是收入中位数,所以让我们放大他们的相关散点图:

housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)

该图揭示了一些东西。首先,相关性确实很强,可以清楚地看到向上趋势和点不是太分散。其次,我们之前发现的价格上限是清晰可见截至$ 500,000的水平线。 但是这个情节揭示了其他不那么明显的直线:一条约450,000美元的水平线,另一条350,000美元左右的水平线,也许一条约280,000美元,还有一些低于这条线。你可能希望尝试删除相应的区域,以防止你的算法学习重现这些数据怪癖。

尝试使用属性组合

希望前面的部分能让你了解一些可以探索数据并获得见解的方法。在将数据提供给机器学习算法之前确定了一些你可能想要清理的数据怪癖,并且你发现属性之间存在有趣的相关性,尤其是目标属性。你还注意到某些属性具有尾部分布,因此可能希望对它们进行转换(例如,通过计算它们的对数)。当然,每个项目的里程数会有很大差异,但总体思路是相似的。
在为机器学习算法准备数据之前,你可能想要做的最后一件事是尝试各种属性组合。例如,如果不知道有多少家庭,则区内的房间总数不是很有用。你真正想要的是每个家庭的房间数量。同样,卧室总数本身并不是很有用:你可能希望将它与房间数量进行比较。每个家庭的人口似乎也是一个有趣的属性组合。让我们创建这些新的属性:

housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
housing["population_per_household"]=housing["population"]/housing["households"]

现在让我们再看一下相关的矩阵:

>>> corr_matrix = housing.corr()
>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value        1.000000
median_income             0.687170
rooms_per_household       0.199343
total_rooms               0.135231
housing_median_age        0.114220
households                0.064702
total_bedrooms            0.047865
population_per_household -0.021984
population               -0.026699
longitude                -0.047279
latitude                 -0.142826
bedrooms_per_room        -0.260070
Name: median_house_value, dtype: float64

新的bedroom_per_room属性与房屋中位数值的相关性远高于房间或卧室的总数。 显然,卧室/房间比率较低的房屋往往更贵。 每个家庭的房间数量也比一个地区的房间总数更多信息 ——显然房屋越大,它们就越贵。
这轮探索不一定非常彻底;重点是从右脚开始,快速获得有助于你获得第一个合理的原型的见解。 但这是一个迭代过程:一旦你启动并运行原型,你就可以分析其输出以获得更多见解并回到此探索步骤。

五、为机器学习算法准备数据

是时候为你的机器学习算法准备数据了。你应该编写函数来执行此操作,而不仅仅是手动执行此操作,原因如下:

  • 这将允许你轻松地在任何数据集上重现这些转换(例如,下次获得新数据集时)。
  • 你将逐步构建可在以后的项目中重用的转换函数库。
  • 你可以在实时系统中使用这些功能来转换新数据,然后再将其提供给算法。
  • 这将使你可以轻松尝试各种转换,并查看哪种转换组合效果最佳。

但首先让我们回到一个干净的训练集(通过再次复制strat_train_set),让我们分离预测变量和标签,因为我们不一定要对预测变量和目标值应用相同的变换(注意drop()创建数据的副本,但不影响strat_train_set):

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

数据清洗

大多数机器学习算法都无法使用缺少的功能,所以让我们创建一些函数来处理它们。 您之前注意到total_bedrooms属性有一些缺失值,所以让我们解决这个问题。 你有三个选择:

  • 摆脱相应的区域。
  • 摆脱整个属性。
  • 将值设置为某个值(零,均值,中位数等)。

你可以通过DataFrame的 dropna() , drop() , and fillna()方法很简单的完成。

housing.dropna(subset=["total_bedrooms"])     # option 1
housing.drop("total_bedrooms", axis=1)        # option 2
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median)      # option 3

如果选择选项3,则应计算训练集上的中值,并使用它来填充训练集中的缺失值,但也不要忘记保存已计算的中值。 当你想要评估系统时,以及系统上线后替换新数据中的缺失值时,你将需要稍后更换测试集中的缺失值。
Scikit-Learn提供了一个方便的课程来处理缺失值:Imputer。 这是如何使用它。 首先,你需要创建一个Imputer实例,指定你要将每个属性的缺失值替换为该属性的中位数:

from sklearn.preprocessing import Imputer

imputer = Imputer(strategy="median")

由于中位数只能在数值属性上计算,我们需要创建一个没有text属性ocean_proximity的数据副本:

housing_num = housing.drop("ocean_proximity", axis=1)

现在,你可以使用fit()方法将imputer实例拟合到训练数据:

imputer.fit(housing_num)

imputer简单地计算了每个属性的中位数,并将结果存储在statistics_ instance变量中。 只有total_bedrooms属性具有缺失值,但我们无法确定系统上线后新数据中是否存在任何缺失值,因此将imputer应用于所有数值属性更安全:

>>> imputer.statistics_
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
>>> housing_num.median().values
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])

现在,你可以使用这个“训练有素”的imputer来通过替换学习中位数的缺失值来转换训练集:

X = imputer.transform(housing_num)

结果是包含转换特征的普通Numpy数组。 如果你想把它放回到Pandas DataFrame中,它很简单:

housing_tr = pd.DataFrame(X, columns=housing_num.columns)

Scikit-Learn设计

Scikit-Learn的API设计得非常好。 主要设计原则是:

1、一致性。 所有对象共享一致且简单的界面:

  • - 估计。 可以基于数据集估计一些参数的任何对象称为估计器(例如,计算器是估计器)。 估计本身由fit()方法执行,它只需要一个数据集作为参数(或两个用于监督学习算法;第二个数据集包含标签)。 指导估算过程所需的任何其他参数都被视为超参数(例如,计算机的策略),并且必须将其设置为实例变量(通常通过构造函数参数)。

  • - 转换器。一些估算器(例如imputer)也可以转换数据集;这些被称为转换器。再一次,API非常简单:转换由transform()方法执行,数据集将转换为参数。它返回转换后的数据集。这种转换通常依赖于学习的参数,就像一个imputer的情况一样。所有转换器也有一个称为fit_transform()的方便方法,相当于调用fit()然后是transform()(但有时fit_transform()被优化,跑得快得多)。

  • - 预测因子。最后,一些估算器能够在给定数据集的情况下进行预测;他们被称为预测因子。例如,前一章中的线性回归模型是一个预测因子:它根据一个国家的人均GDP预测生活满意度。预测变量具有predict()方法,该方法获取新实例的数据集并返回相应预测的数据集。它还有一个score()方法,可以测量给定测试集的预测质量(以及监督学习算法中的相应标签)。

2、检查。 可以通过公共实例变量(例如,imputer.strategy)直接访问所有估计器的超参数,并且还可以通过具有下划线后缀的公共实例变量(例如,imputer.statistics_)访问所有估计器的学习参数。

3、防扩散类。 数据集表示为NumPy数组或SciPy稀疏矩阵,而不是自制类。 超参数只是常规的Python字符串或数字。

4、组成。 现有的构建块尽可能重用。 例如,正如我们将要看到的,很容易从任意变换器序列创建一个Pipeline估算器,然后是最终估算器。

5、合理的默认值。 Scikit-Learn为大多数参数提供合理的默认值,从而可以轻松快速创建基线工作系统。

处理文本和分类属性

早些时候,我们留出了分类属性ocean_proximity,因为它是一个文本属性,因此我们无法计算其平均。 大多数机器学习算法都喜欢使用数字,所以让我们将这些文本标签转换为数字。

Scikit-Learn为此任务提供名为LabelEncoder的转换器:

>>> from sklearn.preprocessing import LabelEncoder
>>> encoder = LabelEncoder()
>>> housing_cat = housing["ocean_proximity"]
>>> housing_cat_encoded = encoder.fit_transform(housing_cat)
>>> housing_cat_encoded
array([1, 1, 4, ..., 1, 0, 3])

这样更好:现在我们可以在任何ML算法中使用这个数值数据。 你可以使用classes_属性查看此编码器已学习的映射(“<1H OCEAN”映射到0,“INLAND”映射到1,等等):

>>> print(encoder.classes_)
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']

该表示的一个问题是ML算法将假设两个附近值比两个远值更相似。 显然情况并非如此(例如,类别0和类别4比类别0和类别1更相似)。 要解决此问题,常见的解决方案是为每个类别创建一个二进制属性:当类别为“<1H OCEAN”时,一个属性等于1(否则为0),另一个属性当类别为“INLAND”时等于1(否则为0),依此类推。 这称为单热编码,因为只有一个属性等于1(热),而其他属性将为0(冷)。

Scikit-Learn提供单热编码编码器,将整数分类值转换为单热矢量。 让我们将类别编码为单热矢量。 请注意,fit_transform()需要一个2D数组,但是housing_cat_encoded是一维数组,所以我们需要重新整形:

>>> from sklearn.preprocessing import OneHotEncoder
>>> encoder = OneHotEncoder()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
>>> housing_cat_1hot
<16513x5 sparse matrix of type '<class 'numpy.float64'>'
with 16513 stored elements in Compressed Sparse Row format>

请注意,输出是SciPy稀疏矩阵,而不是NumPy数组。 当你具有包含数千个类别的分类属性时,这非常有用。 在单一编码之后,我们得到一个包含数千列的矩阵,矩阵充满零,除了每行1个。 使用大量内存主要用于存储零会非常浪费,因此稀疏矩阵仅存储非零元素的位置。 你可以像普通的2D数组一样使用它,但如果你真的想将它转换为(密集的)NumPy数组,只需调用toarray()方法:

>>> housing_cat_1hot.toarray()
array([[ 0., 1., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 0., 0., 1.],
...,
[ 0., 1., 0., 0., 0.],
[ 1., 0., 0., 0., 0.],
[ 0., 0., 0., 1., 0.]])

我们可以使用LabelBinarizer类在一次镜头中应用两种转换(从文本类别到整数类别,然后从整数类别到单热矢量):

>>> from sklearn.preprocessing import LabelBinarizer
>>> encoder = LabelBinarizer()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat)
>>> housing_cat_1hot
array([[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 1],
...,
[0, 1, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 0, 0, 1, 0]])

请注意,默认情况下,这会返回密集的NumPy数组。 你可以通过将sparse_output = True传递给LabelBinarizer构造函数来获取稀疏矩阵。

定制转换器

尽管Scikit-Learn提供了许多有用的转换器,但你需要编写自己的转换器来执行自定义清理操作或组合特定属性等任务。 你将希望你的变换器与Scikit-Learn功能(例如管道)无缝协作,并且由于Scikit-Learn依赖于duck typing(而不是继承),你只需要创建一个类并实现三个方法:fit()( 返回self),transform()和fit_transform()。 只需将TransformerMixin添加为基类,即可免费获得最后一个。 此外,如果你将BaseEstima tor添加为基类(并避免构造函数中的* args和** kargs),你将获得两个额外的方法(get_params()和set_params()),这些方法对自动超参数调整很有用。 例如,这是一个小型变换器类,它添加了我们前面讨论过的组合属性:

from sklearn.base import BaseEstimator, TransformerMixin

rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room = True): # no *args or **kargs
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self, X, y=None):
        return self # nothing else to do
    def transform(self, X, y=None):
        rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
        population_per_household = X[:, population_ix] / X[:, household_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
            return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room]
        else:
            return np.c_[X, rooms_per_household, population_per_household]

attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)

在此示例中,变换器具有一个超参数add_bedrooms_per_room,默认情况下设置为True(提供合理的默认值通常很有帮助)。 此超参数将允许你轻松找出添加此属性是否有助于机器学习算法。 更一般地说,你可以添加超参数来控制你不是100%确定的任何数据准备步骤。 你自动执行这些数据准备步骤的次数越多,你可以自动尝试的组合越多,你就越有可能找到一个很好的组合(并节省大量时间)。

特征缩放

你需要应用于数据的最重要的转换之一是特征缩放。 除了极少数例外,当输入数字属性具有非常不同的比例时,机器学习算法表现不佳。 住房数据就是这种情况:房间总数在6到39,320之间,而收入中位数范围只有0到15。请注意,通常不需要缩放目标值。
有两种常用方法可以使所有属性具有相同的比例:最小 - 最大缩放和标准化。

  • 最小 - 最大缩放(许多人称之为规范化)非常简单:移动和重新调整值,使它们最终从0到1。我们通过减去最小值并除以最大值减去最小值来实现。 为此,Scikit-Learn提供了一个名为MinMaxScaler的变压器。 它有一个feature_range超参数,如果由于某种原因你不想要0-1,它可以让你改变范围。
  • 标准化是完全不同的:首先它减去平均值(因此标准化值总是具有零均值),然后它除以方差,以便得到的分布具有单位方差。 与最小 - 最大缩放不同,标准化不将值绑定到特定范围,这对于某些算法可能是一个问题(例如,神经网络通常期望输入值范围从0到1)。 但是,标准化受异常值的影响要小得多。 例如,假设一个地区的收入中位数等于100(错误地)。 然后,最小 - 最大缩放将所有其他值从0-15压缩到0-0.15,而标准化不会受到太大影响。 Scikit-Learn提供了一个名为StandardScaler的变压器用于标准化。

Note:与所有转换一样,重要的是仅将缩放器适合训练数据,而不是整个数据集(包括测试集)。只有这样,你才能使用它们来转换训练集和测试集(以及新数据)。

转型管道

如你所见,有许多数据转换步骤需要按正确的顺序执行。 幸运的是,Scikit-Learn提供了Pipeline类来帮助完成这些转换序列。 这是一个数字属性的小管道:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
                        ('imputer', Imputer(strategy="median")),
                        ('attribs_adder', CombinedAttributesAdder()),
                        ('std_scaler', StandardScaler()),
                        ])
housing_num_tr = num_pipeline.fit_transform(housing_num)

Pipeline构造函数采用一系列名称/估算器对来定义一系列步骤。除最后一个估算器之外的所有估算器都必须是变换器(即,它们必须具有fit_transform()方法)。名字可以是你喜欢的任何名字。

当你调用管道的fit()方法时,它会在所有变换器上顺序调用fit_transform(),将每个调用的输出作为参数传递给下一个调用,直到它到达最终的估算器,它只调用fit()方法。

管道公开了与最终估算器相同的方法。在这个例子中,最后一个估计器是一个StandardScaler,它是一个变换器,所以管道有一个transform()方法,它按顺序将所有变换应用于数据(它还有一个fit_transform方法,我们可以使用而不是调用fit()然后transform())。

你现在有一个数值管道,你还需要对分类值应用LabelBinarizer:如何将这些转换连接到单个管道? Scikit-Learn为此提供了一个FeatureUnion类。你给它一个变换器列表(可以是整个变换器管道),当它的transform()方法被调用时,它并行运行每个变换器的transform()方法,等待它们的输出,然后连接它们并返回结果(当然,调用fit()方法会调用所有变换器的fit()方法)。处理数字和分类属性的完整管道可能如下所示:

from sklearn.pipeline import FeatureUnion

num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

num_pipeline = Pipeline([
                        ('selector', DataFrameSelector(num_attribs)),
                        ('imputer', Imputer(strategy="median")),
                        ('attribs_adder', CombinedAttributesAdder()),
                        ('std_scaler', StandardScaler()),
                        ])
cat_pipeline = Pipeline([
                        ('selector', DataFrameSelector(cat_attribs)),
                        ('label_binarizer', LabelBinarizer()),
                        ])
full_pipeline = FeatureUnion(transformer_list=[
                                                ("num_pipeline", num_pipeline),
                                                ("cat_pipeline", cat_pipeline),
                                                ])

你可以简单地运行整个管道:

>>> housing_prepared = full_pipeline.fit_transform(housing)
>>> housing_prepared
array([[ 0.73225807, -0.67331551,  0.58426443, ..., 0.       ,
         0.        ,  0.       ],
       [-0.99102923,  1.63234656, -0.92655887, ..., 0.       ,
         0.        ,  0.        ],
       [...]
>>> housing_prepared.shape
(16513, 17)

每个子管道都以选择器变换器开始:它只是通过选择所需的属性(数字或分类),删除其余属性,并将生成的DataFrame转换为NumPy数组来转换数据。 Scikit-Learn中没有任何东西可以处理Pandas DataFrames,因此我们需要为此任务编写一个简单的自定义变换器:

from sklearn.base import BaseEstimator, TransformerMixin

class DataFrameSelector(BaseEstimator, TransformerMixin):
    def __init__(self, attribute_names):
        self.attribute_names = attribute_names
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        return X[self.attribute_names].values

六、选择并训练模型

最后! 你解决了问题,获得了数据并对其进行了探索,你对训练集和测试集进行了采样,并编写了转换管道来自动清理和准备机器学习算法的数据。 现在可以选择并训练机器学习模型了。

训练集的训练与评估

好消息是,由于所有这些先前的步骤,事情现在比你想象的要简单得多。 让我们首先训练线性回归模型,就像我们在前一章中所做的那样:

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

完成! 现在你拥有一个有效的线性回归模型。 让我们在训练集的几个实例中试一试:

>>> some_data = housing.iloc[:5]
>>> some_labels = housing_labels.iloc[:5]
>>> some_data_prepared = full_pipeline.transform(some_data)
>>> print("Predictions:\t", lin_reg.predict(some_data_prepared))
Predictions: [ 303104. 44800. 308928. 294208. 368704.]
>>> print("Labels:\t\t", list(some_labels))
Labels:  [359400.0, 69700.0, 302100.0, 301300.0, 351900.0]

尽管预测并不准确(例如,第二次预测偏差超过50%!),但它仍然有效。 让我们使用Scikit-Learn的mean_squared_error函数在整个训练集上测量这个回归模型的RMSE:

>>> from sklearn.metrics import mean_squared_error
>>> housing_predictions = lin_reg.predict(housing_prepared)
>>> lin_mse = mean_squared_error(housing_labels, housing_predictions)
>>> lin_rmse = np.sqrt(lin_mse)
>>> lin_rmse
68628.413493824875

好吧,这比没有好,但显然不是一个好成绩:大多数地区的median_housing_values介于120,000美元和265,000美元之间,因此68,628美元的典型预测误差不是很令人满意。 这是一个不适合训练数据的模型的例子。 当发生这种情况时,它可能意味着功能不能提供足够的信息来做出良好的预测,或者模型不够强大。 正如我们在上一篇中所看到的,修复欠拟合的主要方法是选择更强大的模型,为训练算法提供更好的特征,或者减少对模型的约束。 此模型未正则化,因此排除了最后一个选项。 您可以尝试添加更多功能(例如,填充日志),但首先让我们尝试更复杂的模型来查看它的功能。

让我们训练一个DecisionTreeRegressor。 这是一个功能强大的模型,能够在数据中找到复杂的非线性关系(决策树在以后有更详细的介绍)。 现在代码应该看起来很熟悉了:

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)

现在模型已经训练好了,让我们一起根据训练集评估一下这个模型:

>>> housing_predictions = tree_reg.predict(housing_prepared)
>>> tree_mse = mean_squared_error(housing_labels, housing_predictions)
>>> tree_rmse= np.sqrt(tree_mse)
>>> tree_rmse
0.0

等等,什么!? 没有错误? 这个型号真的可以绝对完美吗? 当然,模型更可能过度拟合数据。 你怎么能确定? 正如我们之前看到的,在你准备启动你有信心的模型之前,你不希望触摸测试集,因此你需要使用部分培训集进行培训,并参与模型验证。

使用交叉验证进行更好的评估

评估决策树模型的一种方法是使用train_test_split函数将训练集拆分为较小的训练集和验证集,然后针对较小的训练集训练模型并根据验证集对其进行评估。 这是一项工作,但没有什么太困难,它会运作得相当好。
一个很好的选择是使用Scikit-Learn的交叉验证功能。 以下代码执行K-fold交叉验证:它将训练集随机分成10个不同的子集,称为折叠,然后训练和评估决策树模型10次,每次选择不同的折叠进行评估,另外9次进行训练。折叠。
结果是一个包含10个评估分数的数组:

from sklearn.model_selection import cross_val_score

scores = cross_val_score(tree_reg, housing_prepared, housing_labels,scoring="neg_mean_squared_error", cv=10)
rmse_scores = np.sqrt(-scores)

Note:Scikit-Learn交叉验证功能期望效用函数(越大越好)而不是成本函数(越低越好),因此评分函数实际上与MSE相反(即负值),这就是为什么前面的代码在计算平方根之前计算分数。

让我们看看结果:

>>> def display_scores(scores):
...     print("Scores:", scores)
...     print("Mean:", scores.mean())
...     print("Standard deviation:", scores.std())
...
>>> display_scores(tree_rmse_scores)
Scores: 74678.4916885    64766.2398337    69632.86942005    69166.67693232
        71486.76507766   73321.65695983   71860.04741226    71086.32691692
        76934.2726093    69060.93319262]
Mean: 71199.4280043
Standard deviation: 3202.70522793

现在决策树看起来不像以前那么好。 事实上,它似乎比线性回归模型表现更差! 请注意,交叉验证不仅可以获得模型性能的估计值,还可以衡量此估算值的精确程度(即其标准偏差)。 决策树的得分约为71,200,一般为±3,200。 如果您只使用了一个验证集,则不会有此信息。 但交叉验证的代价是多次训练模型,因此并非总是可行。
让我们为线性回归模型计算相同的分数,以确保:

>>> lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
... scoring="neg_mean_squared_error", cv=10)
...
>>> lin_rmse_scores = np.sqrt(-lin_scores)
>>> display_scores(lin_rmse_scores)
Scores: [ 70423.5893262    65804.84913139    66620.84314068    72510.11362141
          66414.74423281   71958.89083606    67624.90198297    67825.36117664
          72512.36533141   68028.11688067]
Mean: 68972.377566
Standard deviation: 2493.98819069

这是正确的:决策树模型过于拟合,以至于它的性能比线性回归模型差。

让我们现在尝试最后一个模型:RandomForestRegressor。 正如我们将在以后看到的,随机森林通过在特征的随机子集上训练许多决策树来工作,然后平均他们的预测。 在许多其他模型之上构建模型称为Ensemble Learning,它通常是进一步推动ML算法的好方法。 我们将跳过大部分代码,因为它与其他模型基本相同:

>>> from sklearn.ensemble import RandomForestRegressor
>>> forest_reg = RandomForestRegressor()
>>> forest_reg.fit(housing_prepared, housing_labels)
>>> [...]
>>> forest_rmse
22542.396440343684
>>> display_scores(forest_rmse_scores)
Scores: [ 53789.2879722    50256.19806622    52521.55342602    53237.44937943
          52428.82176158   55854.61222549    52158.02291609    50093.66125649
          53240.80406125   52761.50852822]
Mean: 52634.1919593
Standard deviation: 1576.20472269

哇,这要好得多:随机森林看起来非常有前途。 但请注意,训练集上的得分仍然远低于验证集,这意味着该模型仍然过度拟合训练集。 过度拟合的可能解决方案是简化模型约束模型(即,使其正规化),或获得更多的训练数据
然而,在你深入研究随机森林之前,你应该尝试各种类型的机器学习算法中的许多其他模型(几个支持向量机具有不同的内核,可能是神经网络等),而不需要花太多时间来调整超参数。 目标是将一些(两到五个)有希望的模型列入候选名单。

Note:你应该保存您实验的每个模型,这样你就可以轻松地返回到你想要的任何模型。 确保同时保存超参数和训练参数,以及交叉验证分数和实际预测。这样你就可以轻松地比较不同模型类型的分数,并比较它们所犯的错误类型。 你可以使用Python的pickle模块或使用sklearn.externals.joblib轻松保存Scikit-Learn模型,这样可以更有效地序列化大型NumPy数组:

from sklearn.externals import joblib

joblib.dump(my_model, "my_model.pkl")
# and later...
my_model_loaded = joblib.load("my_model.pkl")

七、微调你的模型

让我们假设你现在有一个有前途的模型的候选名单。 你现在需要微调它们。 让我们看看你能做到的几种方法。

网格搜索

一种方法是手动调整超参数,直到找到超参数值的完美组合。 这将是非常繁琐的工作,你可能没有时间探索许多组合。
相反,你应该让Scikit-Learn的GridSearchCV来搜索你。 你需要做的就是告诉它您想要尝试哪些超参数,以及要尝试的值,并使用交叉验证评估超参数值的所有可能组合。 例如,以下代码搜索RandomForestRegressor的超参数值的最佳组合:

from sklearn.model_selection import GridSearchCV

param_grid = [
    {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
    {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
    ]
forest_reg = RandomForestRegressor()
grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error')
grid_search.fit(housing_prepared, housing_labels)

Note:当你不知道超参数应该具有什么值时,一种简单的方法是尝试10的连续幂(如果您想要更精细的搜索,则尝试更小的数字,如本例中使用n_estimators超参数所示)。

这个param_grid告诉Scikit-Learn首先评估第一个dict中指定的n_estimators和max_features超参数值的所有3×4 = 12个组合(不要担心这些超参数现在意味着什么;它们将在以后解释),然后在第二个dict中尝试所有2×3 = 6个超参数值组合,但这次将bootstrap超参数设置为False而不是True(这是该超参数的默认值)。
总而言之,网格搜索将探索RandomForestRe gressor超参数值的12 + 6 = 18种组合,并且它将训练每个模型五次(因为我们使用五重交叉验证)。 换句话说,总共会有18×5 = 90轮的训练! 这可能需要相当长的时间,但一旦完成,你可以得到这样的最佳参数组合:

>>> grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}

Note:由于30是评估的n_estimators的最大值,因此你应该评估更高的值,因为分数可能会继续提高。

你还可以直接获得最佳估算器:

>>> grid_search.best_estimator_RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
                                                    max_features=6, max_leaf_nodes=None, min_samples_leaf=1,
                                                    min_samples_split=2, min_weight_fraction_leaf=0.0,
                                                    n_estimators=30, n_jobs=1, oob_score=False, random_state=None,
                                                    verbose=0, warm_start=False)

Note:如果使用refit = True(这是默认值)初始化GridSearchCV,那么一旦它使用交叉验证找到最佳估计量,它就会在整个训练集上重新训练它。 这通常是一个好主意,因为提供更多数据可能会改善其性能。

当然,评估分数也可用:

>>> cvres = grid_search.cv_results_
... for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
...     print(np.sqrt(-mean_score), params)
...
64912.0351358 {'max_features': 2, 'n_estimators': 3}
55535.2786524 {'max_features': 2, 'n_estimators': 10}
52940.2696165 {'max_features': 2, 'n_estimators': 30}
60384.0908354 {'max_features': 4, 'n_estimators': 3}
52709.9199934 {'max_features': 4, 'n_estimators': 10}
50503.5985321 {'max_features': 4, 'n_estimators': 30}
59058.1153485 {'max_features': 6, 'n_estimators': 3}
52172.0292957 {'max_features': 6, 'n_estimators': 10}
49958.9555932 {'max_features': 6, 'n_estimators': 30}
59122.260006 {'max_features': 8, 'n_estimators': 3}
52441.5896087 {'max_features': 8, 'n_estimators': 10}
50041.4899416 {'max_features': 8, 'n_estimators': 30}
62371.1221202 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54572.2557534 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59634.0533132 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52456.0883904 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
58825.665239 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
52012.9945396 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}

在这个例子中,我们通过将max_features超参数设置为6,并将n_estimators超参数设置为30来获得最佳解决方案。 此组合的RMSE得分为49,959,略高于您之前使用默认超参数值(即52,634)得到的得分。 恭喜,你已经成功调整了最佳模型!

Note:不要忘记,您可以将一些数据准备步骤视为超参数。 例如,网格搜索将自动确定是否添加您不确定的功能(例如,使用CombinedAttributesAdder变换器的add_bedrooms_per_room超参数)。 它可以类似地用于自动找到处理异常值,缺失特征,特征选择等的最佳方法。

随机搜索

当你探索相对较少的组合时,网格搜索方法很好,如前面的示例所示,但是当超参数搜索空间很大时,通常最好使用RandomizedSearchCV。 此类可以与GridSearchCV类大致相同的方式使用,但不是尝试所有可能的组合,而是通过在每次迭代中为每个超参数选择随机值来评估给定数量的随机组合。 这种方法有两个主要好处:

  • 如果让随机搜索运行,例如1000次迭代,此方法将为每个超参数探索1,000个不同的值(而不是每个超参数的几个值与网格搜索方法)。
  • 只需设置迭代次数,即可更好地控制要分配给超参数搜索的计算预算。

集成方法

微调系统的另一种方法是尝试组合性能最佳的模型。 组(或“组合”)通常比最佳单个模型表现更好(就像随机森林比他们依赖的单个决策树表现更好),特别是如果各个模型产生非常不同类型的错误。
我们将在以后更详细地介绍这个主题。

分析最好的模型及其错误

通过检查最佳模型,你将经常获得关于该问题的良好见解。 例如,RandomForestRegressor可以指示每个属性的相对重要性,以进行准确的预测:

>>> feature_importances = grid_search.best_estimator_.feature_importances_
>>> feature_importances
array([ 7.14156423e-02, 6.76139189e-02, 4.44260894e-02,
        1.66308583e-02, 1.66076861e-02, 1.82402545e-02,
        1.63458761e-02, 3.26497987e-01, 6.04365775e-02,
        1.13055290e-01, 7.79324766e-02, 1.12166442e-02,
        1.53344918e-01, 8.41308969e-05, 2.68483884e-03,
        3.46681181e-03])

让我们在相应的属性名称旁边显示这些重要性分数:

>>> extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
>>> cat_one_hot_attribs = list(encoder.classes_)
>>> attributes = num_attribs + extra_attribs + cat_one_hot_attribs
>>> sorted(zip(feature_importances, attributes), reverse=True)
[(0.32649798665134971, 'median_income'),
(0.15334491760305854, 'INLAND'),
(0.11305529021187399, 'pop_per_hhold'),
(0.07793247662544775, 'bedrooms_per_room'),
(0.071415642259275158, 'longitude'),
(0.067613918945568688, 'latitude'),
(0.060436577499703222, 'rooms_per_hhold'),
(0.04442608939578685, 'housing_median_age'),
(0.018240254462909437, 'population'),
(0.01663085833886218, 'total_rooms'),
(0.016607686091288865, 'total_bedrooms'),
(0.016345876147580776, 'households'),
(0.011216644219017424, '<1H OCEAN'),
(0.0034668118081117387, 'NEAR OCEAN'),
(0.0026848388432755429, 'NEAR BAY'),
(8.4130896890070617e-05, 'ISLAND')]

根据这些信息,你可能想丢弃一下没有用的特征(例如,显然只有一个ocean_proximity类别真的很有用,所以你可以尝试删除其他类别)

你还应该查看系统产生的具体错误,然后尝试理解它为什么会产生这些错误以及可以解决问题的方法(添加额外的功能,或者相反,摆脱无法提供的功能,清理异常值等)。

在测试集上评估你的系统

在调整模型一段时间后,你最终会拥有一个表现良好的系统。 现在是时候评估测试集上的最终模型了。 这个过程没有什么特别之处;只需从测试集中获取预测变量和标签,运行full_pipeline来变换数据(调用transform(),而不是fit_transform()!),并评估测试集上的最终模型:

final_model = grid_search.best_estimator_

X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()
X_test_prepared = full_pipeline.transform(X_test)
final_predictions = final_model.predict(X_test_prepared)
final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)
# => evaluates to 48,209.6

如果你进行了大量的超参数调整,那么性能通常会比你使用交叉验证测量的性能略差(因为你的系统最终会经过微调以在验证数据上表现良好,并且可能在未知数据集上表现不佳)。 在这个例子中并非如此,但是当发生这种情况时,你必须抵制调整超参数的诱惑,以使数字在测试集上看起来很好;这些改进不太可能推广到新数据。

项目预启动阶段现在到来了:你需要提出你的解决方案(高亮照亮你学到的东西,有效的东西,做了什么假设,你的系统有什么限制),记录所有内容,并创建精彩的演示文稿清晰的可视化和易于记忆的陈述(例如,“收入中位数是房价的首要预测因素”)。

八、启动,监控和维护您的系统

完美,你得到了批准推出!你需要准备好生产解决方案,特别是将生产输入数据源插入系统并编写测试。
你还需要编写监视代码以定期检查系统的实时性能,并在丢弃时触发警报。这不仅可以捕获突然断裂,还可以捕获性能下降。这很常见,因为随着数据随着时间的推移,模​​型会“腐烂”,除非模型定期接受新数据的训练。
评估系统的性能需要对系统的预测进行采样并对其进行评估。这通常需要人工分析。这些分析师可能是现场专家,也可能是众包平台上的工作人员(例如Amazon Mechanical Turk或CrowdFlower)。无论哪种方式,你都需要将人工评估管道插入你的系统。
你还应确保评估系统的输入数据质量。由于信号质量差(例如,传感器发送随机值或其他团队的输出变得陈旧),性能有时会略微降低,但可能需要一段时间才能使系统性能降低到足以触发警报。如果你监视系统的输入,你可能会更早地发现它。监控输入对在线学习系统尤为重要。

最后,你通常希望使用新数据定期训练模型。 你应该尽可能自动化此过程。 如果不这样做,你很可能每六个月(最多)刷新一次模型,并且系统的性能可能会随着时间的推移而剧烈波动。 如果你的系统是在线学习系统,则应确保定期保存其状态的快照,以便轻松回滚到以前工作状态。

九、尝试一下

希望本篇能够让你了解机器学习项目的外观,并向你展示可用于培训优秀系统的一些工具。 如你所见,许多工作都在数据准备步骤中,构建监控工具,设置人工评估管道以及自动化常规模型培训。
当然,机器学习算法也很重要,但最好是对整个过程感到满意,并且知道三到四个算法,而不是花费所有时间来探索高级算法而不是整个过程有足够的时间。
所以,如果你还没有这样做,现在是拿起笔记本电脑,选择你感兴趣的数据集,并尝试从A到Z完成整个过程的好时机。一个好的起点是 一个竞争网站,如http://kaggle.com/:你将拥有一个可以玩的数据集,一个明确的目标,以及与之分享经验的人。

十、练习

使用本章的住房数据集:

1.尝试支持向量机回归量(sklearn.svm.SVR),其中包含各种超参数,例如kernel =“linear”(具有C超参数的各种值)或kernel =“rbf”(具有C和gamma的各种值)超参数)。 不要担心这些超参数现在意味着什么。看看最好的SVR预测器表现如何?

2.尝试用RandomizedSearchCV替换GridSearchCV。

3.尝试在准备管道中添加变换器以仅选择最重要的属性。

4.尝试创建一个执行完整数据准备和最终预测的管道。

5.使用GridSearchCV自动探索一些准备选项。

这些练习的解决方案可在在线Jupyter笔记本中找到,网址为https://github.com/ageron/handson-ml

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值