本章中,你会假装作为被一家地产公司刚刚雇佣的数据科学家,完整地学习一个案例项目。下面是主要步骤:
- 项目概述。
- 获取数据。
- 发现并可视化数据,发现规律。
- 为机器学习算法准备数据。
- 选择模型,进行训练。
- 微调模型。
- 给出解决方案。
- 部署、监控、维护系统。
2.1 使用真实数据
学习机器学习时,最好使用真实数据,而不是人工数据集。幸运的是,有上千个开源数据集可以进行选择,涵盖多个领域。以下是一些可以查找的数据的地方:
- 流行的开源数据仓库:
- 准入口(提供开源数据列表)
- 其它列出流行开源数据仓库的网页:
本章,我们选择的是 StatLib 的加州房产价格数据集(见下图)。这个数据集是基于 1990 年加州普查的数据。数据已经有点老(1990 年还能买一个湾区不错的房子),但是它有许多优点,利于学习,所以假设这个数据为最近的。为了便于教学,我们添加了一个类别属性,并除去了一些特征。
2.2 项目概览
欢迎来到机器学习房地产公司!你的第一个任务是利用加州普查数,建立一个加州房价模型。这个数据包含每个街区组的人口
、收入中位数
、房价中位数
等指标。街区组是美国调查局发布样本数据的最小地理单位(一个街区通常有 600 到 3000 人)。我们将其简称为“街区”。
你的模型要利用这个数据进行学习,然后根据其它指标,预测任何街区的的房价中位数。
提示:你是一个有条理的数据科学家,你要做的第一件事是拿出你的机器学习项目清单。你可以使用附录 B 中的清单;这个清单适用于大多数的机器学习项目,但是你还是要确认它是否满足需求。在本章中,我们会检查许多清单上的项目,但是也会跳过一些简单的,有些会在后面的章节再讨论。
2.3 划定问题
首先要问老板的问题是,企业的目标到底是什么?建立一个模型可能不是最终目标。公司希望如何使用这个模型并从中受益?知道目标很重要,因为它将决定你如何框定问题,选择什么样的算法,用什么样的性能衡量标准来评估你的模型,以及你要花多少精力去调整它。
你的老板回答说,你的模型的输出(对一个地区的房价中位数的预测)将和许多其他指标一起被送入另一个机器学习系统(见图房地产投资的机器学习流水线)。这个下游系统将决定投资于某个地区是否值得。做好这一点至关重要,因为它直接影响到收益。
流水线
一系列的数据处理组件被称为数据流水线。流水线在机器学习系统中很常见,因为有许多数据要处理和转换。
组件通常是异步运行的。每个组件拉取进大量数据,然后进行处理,然后将数据传输到另一个数据容器中,而后流水线中的另一个组件收入这个数据,然后输出,这个过程依次进行下去。每个组件都是独立的:组件间的接口只是数据容器。这样可以让系统更便于理解(根据数据流的图来理解),不同的项目组可以关注于不同的组件。进而,如果一个组件失效了,下游的组件使用失效组件最后生产的数据,通常可以正常运行(一段时间)。这样就使整个架构鲁棒性**很强。
另一方面,如果没有监控,失效的组件会在不被注意的情况下运行一段时间。数据会受到污染,整个系统的性能就会下降。
下一个要问的问题是,现在的解决方案效果如何。老板通常会给一个参考性能,以及如何解决问题。老板说,现在街区的房价是靠专家手工估计的,专家队伍收集最新的关于一个区的信息(不包括房价中位数),他们使用复杂的规则进行估计。这种方法费钱费时间,而且估计结果不理想,误差率大概有 15%。
OK,有了这些信息,你就可以开始设计系统了。首先,你需要划定问题:有监督学习或无监督学习,还是强化学习?这是个分类任务、回归任务,还是其它的?要使用批量学习还是线上学习?再继续阅读之前,请暂停一下,尝试自己回答下这些问题。
你能回答出来吗?一起看下答案:很明显,这是一个典型的监督学习任务,因为你要使用的是有标签的训练样本(每个实例都有预定的产出,即街区的房价中位数)。并且,这是一个典型的回归任务,因为你要预测一个值。讲的更细些,这是一个多元回归问题,因为系统要使用多个变量进行预测(要使用街区的人口,收入中位数等等)。这也是一个单变量回归问题,因为我们只想预测每个区的一个值。如果我们试图预测每个区的多个值,那就是一个多变量回归问题。最后,没有连续的数据流进入系统,没有必要快速调整以适应不断变化的数据,而且数据量足够小,可以放入内存中,所以普通的批量学习应该就可以了。
提示:如果数据量很大,你可以要么在多个服务器上对批量学习做拆分(使用 MapReduce 技术,后面会看到),或是使用线上学习。
2.4 获取数据
2.4.1 准备工作
安装一个 Python 科学计算环境比如 Anaconda,使用 Anaconda 的包管理系统,或者使用 Python 自己的包管理器pip,它是 Python 安装包( 2.7.9 版本后)自带的。可以用下面的命令检查pip版本:
C:\Users\Administrator> pip3 --version
如果需要更新pip版本,可以使用下面的命令:
PS C:\Users\Administrator> python -m pip install --upgrade pip
下载anaconda,用 Jupyter notebook 运行下面的代码示例。完整的Jupyter notebook 页面如下图所示:
现在点击按钮 New 创建一个新的 Python 注本,选择合适的 Python 版本
这一步做了三件事:首先,在工作空间中创建了一个新的 notebook 文件Untitled.ipynb
;第二,它启动了一个 Jupyter 的 Python 内核来运行这个 notebook;第三,在一个新栏中打开这个 notebook。接下来,点击 Untitled,将这个 notebook 重命名为ML2 California house price
(这会将ipynb
文件自动命名为ML2 California house pricet.ipynb
)。如下图所示:
2.4.2 下载数据
一般情况下,您需要的数据可以在相关的数据库中获得,要访问它,你首先需要获得你的凭证和访问授权,并熟悉数据模式。然而,在这个项目中,事情就简单多了:你只需要下载一个压缩文件——housing.tgz,其中包含一个名为housing.csv的包含所有数据的CSV文件。
你可以用浏览器下载文件,然后运行housing.tgz来解压并提取CSV文件,但最好是创建一个函数来完成。如果数据定期更新的话,拥有一个下载数据的函数是非常有用的:你可以编写一个小脚本,使用该函数来获取最新的数据。如果你需要在多台机器上安装数据集,自动获取数据的过程也很有用。
下面是获取数据的代码:
import os
import tarfile
import urllib
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/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 加载数据。类似地,写一个小函数来加载数据:
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, engine='python')
2.5 开始对数据进行分析
2.5.1 导入相关的库和数据集
用Python对数据处理需要建立在学习并且会运用Python基础语法,以及熟悉并掌握pandas, numpy ,matplotlib以及sklearn这几个库的基础上。本笔记对数据处理过程中用到的pandas, numpy ,matplotlib库中的函数以及用法,不再做额外的讲解,如果读者遇到困难,需暂停本课程,预先学习以上提及的三个库。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
使用pd.read_csv()来查看数据集的内容:
每行代表一个区。有10个属性:经度(longitude)、纬度(latitude)、房龄中位数(housing_median_age)、总房间数(total_rooms)、总卧室数(total_bedrooms)、人口(population)、住户数(households)、收入中位数(median_income)、房屋价格中位数(median_house_value)和距离海洋的距离(ocean_proximity)
再用 dataset. info() 方法可以快速得到数据的描述,特别是总行数、每个属性的类型和非空值的数量:
数据集中有20,640个sample,我们称之为样本或实例(以下统称为实例),以机器学习的标准来看,这个数据集相当小,但对于入门者来说已经很完美了。请注意,total_bedrooms 属性只有20433个非空值,这意味着有207个地区缺少这个特征。这一点在后续的过程需要要注意。
除了 ocean_proximity 的类型是 object 之外,所有的属性都是数值型的。当你看前五行的时候,你可能注意到,ocean_proximity中的值是重复的,这说明它可能是一个分类属性。通过使用value_counts()方法,你可以知道有哪些分类,以及每个分类有多少个实例。
我们再用dataset.describe()方法,来查看数据集的统计信息,例如,最小值,最大值,平均值,中位数等等
需要注意的是,空值是被忽略的(例如,total_bedrooms是20,433,而不是20,640)。std行显示的是标准差,它衡量了数值的分散程度。25%、50%和75%的行分别显示了相应的百分位数:百分位数表示一组观测数据中的某一特定百分比的观测值。例如,对于housing_median_age,有25%的地区低于18,50%的地区低于29,75%的地区低于37。
另一个快速了解你所处理的数据的方法是为每个数值属性绘制一个直方图。
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
#对数据集中各个特征值绘制直方图,观察数据的分布情况
housing.hist(bins=50, figsize=(16,9))
plt.show()
从直方图中,你可能会发现如下几个现象:
- 首先,median_income属性看起来不像是以美元(USD)表示的。在向收集数据的团队核实后,你被告知,数据被缩放过了,且上限为15(实际上是15.0001),较高的收入中位数为15,较低的收入中位数为0.5(实际上是0.4999)。这些数字大致代表了数万元(如3实际上是指3万元左右)。在机器学习中,使用预处理的属性是很常见的,这不一定是问题,但你应该试着去了解数据是如何计算出来的。
- housing_median_age和median_house_value也有上限。后者可能是一个严重的问题,因为它是你的目标属性(标签)。你的机器学习算法可能学习到的价格永远不会超过这个限制。你需要和你的客户团队进行检查,看看这是否是一个问题。如果他们告诉你,即使超过50万美元,他们也需要精确的预测,那么你有两个选择:
a. 为那些标签被封顶的地区再重新收集适当的标签。
b. 将这些区域从训练集中删除(也从测试集中删除,因为如果你的系统预测值超过50万美元,那么它的评估应该不会很差)。 - 这些属性有非常不同的尺度。在本章的后面,我们j将探讨特征缩放的问题。
- 最后,许多直方图都是长尾的:它们向中位数的右边延伸的幅度比左边大得多。这可能会使一些机器学习算法在检测模式时有些困难。我们稍后将尝试转换这些属性,使其具有钟形分布。
希望你现在对你所处理的数据有了更好的了解。
2.5.2 创建测试集
在这个阶段将部分数据放在一边,听起来可能很奇怪。毕竟,你只是简单地看了一眼数据,在决定使用什么算法之前,你肯定应该先了解更多的数据,对吗?这是事实,但你的大脑是一个神奇的模式检测系统,这意味着它极易出现过度拟合的情况:如果你看测试数据集,你可能会在测试数据中偶然发现一些看似有趣的模式,导致你选择某种特定的机器学习模型。当你用测试集估计泛化误差的时候,你的估计会过于乐观,你的估计会得到一个性能不如预期得系统。这就是所谓的数据窥探偏差。
创建一个测试集理论上很简单:随机抽取一些实例,通常是数据集的20%(如果你的数据集非常大的话,或者更小),然后把它们放在一边。
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人。他们试图确保这1000人能够代表整个人群。例如,美国人口中女性占51.3%,男性占48.7%,所以在美国进行的调查中,如果调查得好,会尽量在样本中保持这个比例。女性513人,男性487人。这就是所谓的分层抽样:人口被分成同质的子群(男性与女性),称为层,从每层中抽取合适数量的实例,以保证测试集在总人口中具有代表性。如果进行调查的人员使用纯随机抽样,则抽样得到不平衡测试集(女性少于49%或女性超过54%)的机率大约为12%(二项分布的概率计算可得)。 无论哪种方式,调查结果都会有明显的偏差。
假设您与房地产专家聊天,他们告诉您收入中位数(median_income)是预测房价中位数(median_house_value)的重要属性。 您可能要确保测试集能够代表整个数据集中的各种收入类别。由于收入中位数是连续的数字属性,因此您首先需要创建收入类别属性。 让我们仔细地看一看收入中位数直方图:
import seaborn as sns
sns.set_style('darkgrid') # darkgrid,whitegrid,dark,white,ticks
sns.histplot(housing["median_income"],kde=False)
大多数收入中位数都集中在1.5到6之间(即15,000美元至60,000美元),但是有些收入中位数远远超过6。重要的是,您的数据集中每个层必须有足够数量的实例,否则每一层重要性的估计可能会产生偏差。 以下代码使用函数 pd.cut() 创建具有五个类别(标记为1到5)的收入类别属性:
#通过观察median_income的分布情况(左偏分布)即大部分数据都分布在0-6的区间范围内,
#因此通过把0-6的范围内均匀分割成5部分,并创建了一个收入类别属性"income_cat"
housing["income_cat"] = pd.cut(housing["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
housing["income_cat"].value_counts()
再对新创建的这些收入类别”income_cat”做直方图:
现在可以使用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]
strat_train_set
再查看训练集中的各个收入类别占分层抽样后的总数据比例:
下图中对比了**总数据集、分层采样的测试集、纯随机采样测试集的收入分类比例**
。
可以看到,使用分层抽样生成的测试集的收入类别比例几乎与整个数据集中的收入类别比例相同,而使用纯随机抽样生成的测试集则存在偏差。
#分层抽样与随机抽样在数据还原程度上的差异
def income_cat_proportions(data):
return data["income_cat"].value_counts() / len(data)
train_set,test_set = train_test_split(housing,test_size = 0.2,random_state=42)
compare_props = pd.DataFrame({
"Overall":income_cat_proportions(housing),
"Stratified":income_cat_proportions(strat_train_set),
"Random":income_cat_proportions(train_set)
}).sort_index()
compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100
compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100
compare_props
2.5.3 数据可视化
根据已知的地理信息(纬度和经度),创建所有地区的散点图以可视化数据:
#创建一个所有街区的散点图,横坐标为经度,纵坐标为纬度
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1,figsize=(12,6))
现在让我们做一个房屋价格的热力图。
#房价散点图。每个圈的半径表示街区的人口(选项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,figsize=(12,6)
)
plt.legend()
这张图片告诉您,房价与地理位置(例如,靠近海洋)和人口密度密切相关。 聚类算法对于检测主聚类和添加新特征(与聚类中心的接近度)应该是有用的。 海洋邻近属性也可能有用,尽管在北加利福尼亚州沿海地区的房屋价格不是太高,因此这不是一个普遍适用的规则。
2.5.4 相关性
使用pd.corr()方法轻松计算每对属性之间的标准相关系数:
#使用corr()方法计算出每对属性间的标准相关系数(也称作皮尔逊相关系数):
corr_matrix = housing.corr()
sns.heatmap(data=corr_matrix,cmap='Blues')
现在来看下每个属性和房价中位数的关联度:
corr_matrix["median_house_value"].sort_values(ascending=False)
相关系数的范围是 -1 到 1。
当接近 1 时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。
当相关系数接近 -1 时,意味强负相关;你可以看到,纬度和房价中位数有轻微的负相关性(即,越往北,房价越可能降低)。
最后,相关系数接近 0,意味没有线性相关性。下图显示了各种图以及它们的水平轴和垂直轴之间的相关系数
警告:相关系数只测量线性关系(如果
x
上升,y
则上升或下降)。相关系数可能会完全忽略非线性关系(即,如果x
接近 0,则y
值会变高)。在前面的计算结果中,底部的许多行的相关系数接近于 0,尽管它们的轴并不独立:这些就是非线性关系的例子。另外,第二行的相关系数等于 1 或 -1;这和斜率没有任何关系。例如,以英寸为单位的高度与以英尺或纳米为单位的高度的相关系数为1。
另一种检测属性间相关系数的方法是使用 Pandas 的**scatter_matrix**
函数,它能画出每个数值属性对每个其它数值属性的图。因为现在共有 11 个数值属性,你可以得到张图,在一页上画不下,所以只关注几个和房价中位数最有可能相关的属性:
g = sns.pairplot(housing,vars=["median_house_value", "median_income", "total_rooms","housing_median_age"])
最有希望用来预测房价中位数的属性是收入中位数,因此将这张图放大:
housing.plot(kind="scatter", x="median_income",y="median_house_value",
alpha=0.1)
从这幅图可以看出,
首先,相关性确实非常强; 您可以清楚地看到上升趋势,并且这些点并不太分散。
第二,我们之前注意到的价格上限可以清楚地看到一条水平线,为50万美元。
但是此图显示了其他不太明显的直线:一条水平线在$ 450,000附近,另一条在$ 350,000附近,也许是一条在$ 280,000附近,还有一些在这之下,您可能想要尝试删除相应的区域。
2.5.5 不同属性的组合
希望前面的部分为您提供一些探索数据并获得见解的方法。 您确定了一些可能需要清除的数据怪癖,然后再将数据提供给机器学习算法,并且发现了属性之间的有趣关联,尤其是与目标属性的关联。 您还注意到某些属性的尾部分布很重,因此您可能需要对其进行变换(例如,通过计算对数)。 当然,每个项目的实际操作会有很大不同,但是总体思路是相似的。
在为机器学习算法准备数据之前,您可能要做的最后一件事是尝试各种属性组合。 例如,如果您不知道有多少个家庭,那么一个地区的房间总数并不是很有用。 您真正想要的是每个家庭的房间数。 同样,卧室总数本身并不是很有用:您可能希望将其与房间数进行比较。 每个家庭的人口似乎也是一个有趣的属性组合。
让我们创建以下新属性:
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)
新的bedrooms_per_room属性与房屋中位数的相关性远大于房间或卧室的总数。 显然,卧室/房间比率较低的房屋往往更昂贵。 每个家庭的房间数量也比一个地区的房间总数更相关——显然,房屋越大,价格越昂贵。
2.6 为机器学习算法准备数据
是时候为机器学习算法准备数据了。 出于以下几个原因,您应该为此目的编写函数,而不是手动执行此操作:
**●函数可以轻松地在任何数据集上重现这些转换(例如,下次获取新的数据集时)。
●可以逐步构建转换函数,您可以在以后的项目中重用这些转换函数。
●可以在实时系统中使用这些功能来转换新数据,然后再将其提供给算法。
●这可以让你方便地尝试多种数据转换,查看那些转换方法结合起来效果最好。
首先我们恢复到干净的训练集(通过再次复制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()
2.6.1 数据清洗
缺失值处理
Scikit-Learn提供了一个方便的类来处理缺失的值:SimpleImputer。 使用方法是: 首先,您需要创建一个SimpleImputer实例,指定使用该属性的中位数替换每个属性的缺失值:
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
因为只有数值属性才能算出中位数,我们需要创建一份不包括文本属性ocean_proximity的数据副本:
housing_num = housing.drop("ocean_proximity", axis=1)
使用fit()方法将进行拟合然后用“拟合后”的imputer,通过将缺失值替换为学习到的中位数来转换训练集:
X=imputer.fit_transform(housing_num)
X
结果是包含转换特征的NumPy数组。 如果您想将其放回DataFrame中,很简单:
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
housing_tr.info()
scikit-Learn 设计的 API 设计的非常好。它的主要设计原则是:
- 一致性:所有对象的接口一致且简单:
- 估计器(estimator)。任何可以基于数据集而对一些参数进行估计的对象都被成为估计器(比如,
imputer
就是个估计器)。估计本身是通过fit()
方法,只需要一个数据集作为参数(对于监督学习算法,需要两个数据集;第二个数据集包含标签)。任何其它用来指导估计过程的参数都被当做超参数(比如imputer
的strategy
),并且超参数要被设置成实例变器(通常是通过构造器参数)。- 转换器(transformer)。一些估计器(比如
imputer
)也可以转换数据集,这些估计器被称为转换器。API也是相当简单:转换是通过transform()
方法,被转换的数据集作为参数。返回的是经过转换的数据集。转换过程依赖学习到的参数,比如imputer
的例子。所有的转换都有一个便捷的方法fit_transform()
,等同于调用fit()
再transform()
(但有时fit_transform()
经过优化,运行的更快)。- 预测器(predictor)。最后,一些估计器可以根据给出的数据集做预测,这些估计器称为预测器。例如,上一章的
LinearRegression
模型就是一个预测器:它根据一个国家的人均 GDP 预测生活满意度。预测器有一个predict()
方法,可以用新实例的数据集做出相应的预测。预测器还有一个score()
方法,可以根据测试集(和相应的标签,如果是监督学习算法的话)对预测进行衡器。- 检查。所有估计器的超参数都可以通过公共实例变器直接访问(比如,
imputer.strategy
),并且所有估计器学习到的参数也可以通过公共实例变器添加下划线后缀访问(比如,imputer.statistic_
)。- 类不可扩散。数据集被表示成 NumPy 数组或 SciPy 稀疏矩阵,而不是自制的类。超参数只是普通的 Python 字符串或数字。
- 构成。尽可能使用现存的模块。例如,用任意的转换器序列加上一个估计器,就可以做成一个流水线,后面会看到例子。
- 合理的默认值。scikit-Learn 给大多数参数提供了合理的默认值,很容易就能创建一个系统。
处理文本和类别属性
到目前为止,我们仅处理数字属性,但现在让我们看一下文本属性。 在此数据集中,只有一个:ocean_proximity属性。 让我们看一下它的值:
housing_cat = housing[["ocean_proximity"]]
housing_cat
观察数据可以发现每个值代表一个类别。 因此,此属性是分类属性。 大多数机器学习算法都喜欢使用数字,所以让我们把这些文本标签转换为数字。
方法一:LabelEncoder
scikit-Learn
为这个任务提供了一个转换器LabelEncoder
:
from sklearn.preprocessing import LabelEncoder
encoder_1 = LabelEncoder()
housing_cat_LabelEncoder = encoder_1.fit_transform(housing_cat)
housing_cat_LabelEncoder
这种做法的问题是,机器学习的算法会认为两个临近的值比两个疏远的值要更相似。显然这样不对(比如,分类 0 和 4 比 0 和 1 更相似)。
要解决这个问题,一个常见的方法是给每个分类创建一个二元属性:
当分类是
**<1H OCEAN**
,该属性为 1(否则为 0),
当分类是**INLAND**
,另一个属性等于 1(否则为 0),以此类推。
这称作独热编码(One-Hot Encoding
),因为只有一个属性会等于 1(热),其余会是 0(冷)。
方法二:OneHotEncoder
scikit-Learn
提供了一个编码器OneHotEncoder
,用于将整书分类值转变为独热向量。注意fit_transform()
用于 2D 数组,而housing_cat_encoded
是一个 1D 数组,所以需要将其变形:
from sklearn.preprocessing import OneHotEncoder
encoder_2 = OneHotEncoder()
housing_cat_1hot = encoder_2.fit_transform(housing_cat.values).toarray()
housing_cat_1hot
注意输出结果是一个 SciPy
稀疏矩阵,而不是 NumPy
数组。当类别属性有数千个分类时,这样非常有用
。经过独热编码,我们得到了一个有数千列的矩阵,这个矩阵每行只有一个 1,其余都是 0。
使用大量内存来存储这些 0 非常浪费,所以稀疏矩阵只存储非零元素的位置。你可以像一个 2D 数据那样进行使用,但是如果你真的想将其转变成一个(密集的)NumPy
数组,只需调用toarray()
方法:
方法三:OrdinalEncoder
Scikit-Learn的OrdinalEncoder类:多维特征专用,能够将多个文本类型分类特征转换为数值。
from sklearn.preprocessing import OrdinalEncoder
encoder_3= OrdinalEncoder()
housing_cat_OE=encoder_3.fit_transform(housing_cat)
housing_cat_OE
方法一和方法三这两种做法的问题是,ML 算法会认为两个临近的值比两个疏远的值要更相似比如(分类 0 和 4 比 0 和 1 更相似),显然这样不对。
方法三:LabelBinarizer
用类LabelBinarizer,我们可以用一步执行这两个转换(从文本分类到整数分类,再从整数分类到独热向量):
from sklearn.preprocessing import LabelBinarizer
encoder = LabelBinarizer()
housing_cat_1hot = encoder.fit_transform(housing_cat)
housing_cat_1hot
如果分类属性有大量可能的类别(例如,国家/地区代码,专业,物种),则独热编码将导致大量输入特征。这可能会减慢训练速度并降低性能。 如果发生这种情况,您可能希望使用与类别相关的有用数字特征来替换分类输入:例如,您可以用到海洋的距离来替换ocean_proximity特征。或者,您可以使用嵌入的方法用可学习的低维向量来替换每个类别,每个类别的表示形式将在训练期间学习。 这是一个表示学习的例子
2.6.2 自定义转换器
尽管Scikit-Learn提供了许多有用的转换器,但是您将需要为诸如自定义清理操作或组合特定属性之类的任务编写自己的转换器。 您将希望您的转换器与Scikit-Learn(例如管道)无缝适配。因此您要做的就是创建一个类并实现三种方法:fit()(返回自身),transform()和 fit_transform()。
您只需添加TransformerMixin作为基类即可。 如果将BaseEstimator添加为基类(并在构造函数中避免args和*kargs),则还将获得两个额外的属性(get_params()和set_params()),这些方法对于自动超参数调整非常有用。
例如,这是一个转换器,它添加了我们前面讨论的组合属性:
from sklearn.base import BaseEstimator, TransformerMixin#导入sklearn里面的BaseEstimator和TransformerMixin两个基类
rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6 #给出rooms, bedrooms, population, household 四个属性的索引
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] #求出每个house的拥有的room数
population_per_household = X[:, population_ix] / X[:, household_ix] #求出每个house的承载的population数
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix] #求出每个room的拥有的bedrooms数
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) #用transform函数新增属性
housing_extra_attribs = pd.DataFrame(
housing_extra_attribs,
columns=list(housing.columns)+["rooms_per_household", "population_per_household"])#将新增的两个属性转化为dataframe格式,并添加列名
housing_extra_attribs.head()
在此示例中,转换器具有一个超参数add_bedrooms_per_room,默认情况下设置为True(提供合理的默认值通常很有用)。 通过此超参数,您可以轻松确定添加此属性是否对机器学习算法有所帮助。 通常,您可以添加一个超参数来控制您不确定的任何数据准备步骤。 这些数据准备步骤的自动化程度越高,您可以自动尝试的组合就越多,从而更有可能找到一个很好的组合(并节省大量时间)。
2.6.3 特征缩放
当输入的数值属性量度不同时,机器学习算法的性能都不会好。
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。
线性函数归一化(许多人称其为归一化(normalization))很简单:值被转变、重新缩放,直到范围变成 0 到 1。我们通过减去最小值,然后再除以最大值与最小值的差值,来进行归一化。scikit-Learn 提供了一个转换器MinMaxScaler来实现这个功能。它有一个超参数feature_range,可以让你改变范围,如果不希望范围是 0 到 1。
标准化就很不同:首先减去平均值(所以标准化值的平均值总是 0),然后除以方差,使得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值得范围是 0 到 1)。但是,标准化受到异常值的影响很小。例如,假设一个街区的收入中位数是 100。归一化会将其它范围是 0 到 15 的值变为 0-0.15,但是标准化不会受什么影响。scikit-Learn 提供了一个转换器StandardScaler来进行标准化。
与所有转换一样,将缩放器仅用于拟合训练数据,这一点很重要,只有这样,您才能使用它们来转换训练集和测试集(以及新数据)。
2.6.4 转换流水线
许多数据转换步骤,需要按一定的顺序执行。幸运的是,scikit-Learn
提供了类Pipeline
,来进行这一系列的转换。下面是一个数值属性的小流水线:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
housing_num_tr
Pipeline构造器需要一个定义步骤顺序的名字/估计器对的列表。除了最后一个估计器,其余都要是转换器
当你调用流水线的fit()方法,就会对所有转换器顺序调用fit_transform()方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个估计器,它只执行fit()方法。
流水线暴露相同的方法作为最终的估计器。在这个例子中,最后的估计器是一个StandardScaler,它是一个转换器,因此这个流水线有一个transform()方法,可以顺序对数据做所有转换(它还有一个fit_transform方法可以使用,就不必先调用fit()再进行transform())
用ColumnTransformer构造一个处理所有列的转换器
到目前为止,我们已经分别处理了分类属性和数值属性。 拥有一个能够处理所有列的转换器会更方便。 在0.20版中,Scikit-Learn为此目的引入了ColumnTransformer,而且好消息是它可与DataFrames一起使用。 让我们使用它来将所有转换应用于房屋数据:
from sklearn.compose import ColumnTransformer
num_attribs = list(housing_num)#获得数值列的列名称列表
cat_attribs = ["ocean_proximity"]#获得类别列的列名称列表
full_pipeline=ColumnTransformer([
('nums', num_pipeline,num_attribs),#数值列指定用num_pipeline转换
('cat', OneHotEncoder(),cat_attribs)#类别列指定用OneHotEncoder()转换
])
housing_prepared=full_pipeline.fit_transform(housing)
housing_prepared.round(4)
首先,我们导入ColumnTransformer类,然后获得数值属性名称的列表和分类属性名称的列表,最后构造一个ColumnTransformer 实例。 构造时需要一个元组列表,其中每个元组包含名称,转换器和应用转换器的列的名称(或索引)列表。
用FeatureUnion构造多个不同的转换器并且合并输出
Scikit-Learn 提供了一个类FeatureUnion
实现这个功能。你给它一列转换器(可以是所有的转换器),当调用它的transform()
方法,每个转换器的transform()
会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的fit()
方法就会调用每个转换器的fit()
)。一个完整的处理数值和类别属性的流水线如下所示:
'''
每个子流水线都以一个选择转换器开始:通过选择对应的属性(数值或分类)、丢弃其它的,来转换数据
'''
#因为Scikit-Learn 没有工具来处理 PandasDataFrame,因此我们需要写一个简单的自定义转换器 将输出DataFrame转变成一个 NumPy 数组
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.pipeline import FeatureUnion
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
#构造处理数值列的第一个Pipeline
num_pipeline_2 = Pipeline([
('selector', DataFrameSelector(num_attribs)),
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
#构造处理类别列的第二个Pipeline
cat_pipeline_2 = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('OneHotEncoder', OneHotEncoder()),
])
full_pipeline_2 = FeatureUnion(transformer_list=[
("num_pipeline_2", num_pipeline_2),
("cat_pipeline_2", cat_pipeline_2),
])
housing_prepared_2 = full_pipeline_2.fit_transform(housing)
housing_prepared_2
#注意这里housing_prepared_2的数据类型如housing_cat_1hot一样是一个 SciPy 稀疏矩阵,而不是 NumPy 数组,需要调用toarray()方法
housing_prepared_2.toarray()
2.7 选择并训练模型
至此完成前三个阶段的任务后,数据准备工作已经全部完成了。
我们现在已经得到了清洗过适合带入机器学习算法的特征数据集housing_prepared和标签数据集housing_labels
接下来我们就可以选择合适的算法,然后带入数据进行预测了#先选择一个线性回归模型做训练模型
2.7.1在训练集上训练和评估
线性回归
基于前面的工作,接下来要做的比你想的要简单许多。我们先来训练一个线性回归模型:
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:", lin_reg.predict(some_data_prepared))
print("Labels:", list(some_labels))
模型可以运行但是预测并不怎么准确(比如,第一个预测偏离了 41%!)。让我们使用 scikit-Learn 的meansquared_error函数,用全部训练集来计算下这个回归模型的 RMSE:
什么是RMSE?
Root Mean Square Error,均方根误差。是观测值与真实值偏差的平方和与观测次数m比值的平方根。用来衡量观测值同真值之间的偏差,回归问题的典型指标是均方根误差(RMSE)。均方根误差测量的是系统预测误差的标准差,其对于离群点比较敏感。下面的公式 展示了计算 RMSE 的方法:

RMSE公式中各个符号的含义:
m是测量 RMSE 的数据集中的实例数量。例如,如果用一个含有 200 个街区的验证集求 RMSE,则m = 200是数据集第i个实例的所有特征值(不包含标签值)的向量,
是它的标签值(这个实例的输出值)。例如:如果数据集中的第一个街区位于经度 –118.29°,纬度 33.91°,有 1416名居民,收入中位数是 $38372,房价中位数是 $156400,那么所有这些关于x的特征可以用向量来表示:
大X是一个矩阵,X是包含数据集中所有实例的所有特征值(不包含标签)的矩阵。每一行是一个实例,第i行是 的转置矩阵,标记为
这里转置矩阵的概念相当于把列向量转化为行向量。例如,多个实例的特征向量组成的矩阵X就是:
h是系统的预测函数,也称为假设(hypothesis)。当系统收到一个实例的特征向量: $x^{(i)}$,就会输出这个实例的一个预测值 (
读作y-hat即y-帽)。例如,如果系统预测第一区的房价中位数是 158400刀,则
。预测误差是
。
RMSE(X,h)是使用假设h在样本集上测量的损失函数(loss function):
RMSE是评估回归算法性能的一个可靠的指标,在有些情况下,可能需要另外的函数指标。例如,假设存在许多异常的街区。此时,你可能需要使用平均绝对误差(Mean Absolute Error ),也称作平均绝对偏差,这就是另外的一个公式了:
下面我们使用scikit-Learn 的mean_squared_error函数,用全部训练集来计算下这个回归模型的RMSE:
#使用scikit-Learn 的mean_squared_error函数,用全部训练集来计算下这个回归模型的MSE:
from sklearn.metrics import mean_squared_error
housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
#把求出的MSE开平方得到RMSE
lin_rmse = np.sqrt(lin_mse)
lin_rmse
我们注意到预测误差 达到了68628 这肯定让人无法满意。这是一个模型欠拟合训练数据的例子。当这种情况发生时,意味着数据集的特征没有提供足够多的信息来做出一个好的预测,或者模型并不强大。修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或去掉模型上的限制。这个模型还没有正则化,所以排除了最后一个选项。你可以尝试添加更多特征(比如,人口的对数值),但是首先让我们尝试一个更为复杂的模型,看看效果。
决策树
让我们训练一个DecisionTreeRegressor。 这是一个功能强大的模型,能够发现数据中的复杂非线性关系。
#导入决策树算法包
from sklearn.tree import DecisionTreeRegressor
#实例化一个模型
tree_reg = DecisionTreeRegressor()
#对模型求解
tree_reg.fit(housing_prepared, housing_labels)
#用求RMSE的方法在训练集上评估该模型
housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse
发生了什么?没有误差?这个模型可能是绝对完美的吗?更大可能性是这个模型严重过拟合数据。如何确定呢?如前所述,当你有足够信心启动模型之前,都不要碰测试集,因此你需要使用训练集的部分数据来做训练,用另外一部分来做模型验证。
2.7.2 使用K折交叉验证做更佳的评估
评估决策树模型的一种方法是用函数train_test_split来分割训练集,得到一个更小的训练集和一个验证集,然后用更小的训练集来训练模型,用验证集来评估。这需要一定工作量,并不难而且也可行。
另一种更好的方法是使用 scikit-Learn 的交叉验证功能。下面的代码采用了 K折交叉验证(K-fold cross-validation):它随机地将训练集分成5个不同的子集,成为“折”,然后训练评估决策树模型 5 次,每次选一个不用的折来做评估,用其它 4 个来做训练。结果是一个包含 5 个评分的数组。
#使用5折交叉验证来评估决策树模型
from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=5)
tree_rmse_scores = np.sqrt(-scores).round()
tree_rmse_scores
#注意,注意,注意:scikit-Learn 交叉验证功能期望的是效用函数(越大越好)而不是损失函数(越低越好),因此得分函数实际上与负的 MSE
#这就是为什么前面的代码在计算平方根时带入的是-scores。
需要注意的是,Scikit-Learn中的得分是越大越好的,在上述示例中,得分scores实际上为 —MSE 。
scores=scores.round()
def display_scores(scores):
print("Scores:", scores)
print("Mean:", scores.mean())
print("Standard deviation:", scores.std())
display_scores(tree_rmse_scores)
决策树的5折交叉验证评分大约是 71460,通常波动有 ±1270。如果只有一个验证集,就得不到这些信息。但是交叉验证的代价是训练了模型多次,不可能总是这样。让我们计算下线性回归模型的的分数:
from sklearn.model_selection import cross_val_score
lin_reg_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=5)
lin_rmse_scores = np.sqrt(-lin_reg_scores).round()
display_scores(lin_rmse_scores)
线性回归模型的5折交叉验证评分大约是 68874,通常波动有 ±2218,比决策树模型好一点,但是仍然不够理想。
决策树模型过拟合很严重,它的性能比线性回归模型还差。
随机森林模型
现在让我们尝试最后一个模型:RandomForestRegressor。 我们将在第7章中看到,随机森林的工作原理是对特征的随机子集训练多棵决策树,然后对它们的预测取平均。 基于多个模型构建一个强的模型称为集成学习,它通常是进一步提升ML算法的好方法。
#注意随机森林方法导入的位置
from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
from sklearn.model_selection import cross_val_score
forest_reg_scores = cross_val_score(forest_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=5)
forest_rmse_scores = np.sqrt(-forest_reg_scores).round()
display_scores(forest_rmse_scores)
随机森林看起来很有希望。但是,训练集的评分仍然比验证集的评分低很多。
解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。
在深入随机森林之前,你应该尝试下机器学习算法的其它类型模型(不同核心的支持向量机,神经网络,等等),
不要在调节超参数上花费太多时间,目标是列出一个可能模型的列表(两到五个)
提示:你要保存每个试验过的模型,以便后续可以再用。要确保有超参数和训练参数,以及交叉验证评分,和实际的预测值。这可以让你比较不同类型模型的评分,还可以比较误差种类。你可以用 Python 的模块
pickle
,非常方便地保存 Scikit-Learn 模型,或使用sklearn.externals.joblib
,后者序列化大 NumPy 数组更有效率:
import joblib
joblib.dump(forest_reg, "forest_reg.pkl")#joblib.dump(要保存的模型名称, "要保存的模型名称.pkl")
# 加载模型
forest_reg_loaded = joblib.load("forest_reg.pkl")
2.7.3 模型微调
假设您现在有一个模型的清单。 您现在需要对其进行微调。 让我们来看看几种方法。
方法一:网格搜索(GridSearchCV)
scikit-Learn 的GridSearchCV能用交叉验证试验所有可能超参数值的组合。例如,下面的代码搜索了RandomForestRegressor超参数值的最佳组合:
from sklearn.model_selection import GridSearchCV
#当你不能确定超参数该有什么值,一个简单的方法是尝试10 的连续次幂如(10,100,1000)
#(如果想要一个粒度更小的搜寻,可以用更小的数,就像在这个例子中对超参数n_estimators做的)
param_grid = [
{'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},#estimators:估算器
{'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)
param_grid告诉 scikit-Learn 首先评估所有的列在第一个dict中的n_estimators和max_features的3 × 4 = 12种组合(不用担心这些超参数的含义,会在第 7 章中解释)。然后尝试第二个dict中超参数的2 × 3 = 6种组合,这次会将超参数bootstrap设为False而不是True(后者是该超参数的默认值)。
总之,网格搜索会探索12 + 6 = 18种RandomForestRegressor的超参数组合,会训练每个模型五次(因为用的是五折交叉验证)。换句话说,训练总共有18 × 5 = 90轮!K 折将要花费大量时间,完成后,你就能获得参数的最佳组合。
grid_search.best_params_#通过GridSearchCV方法获得的最佳参数
#获得最佳估算器best_estimator_
grid_search.best_estimator_
注意:如果
GridSearchCV
是以(默认值)refit=True
开始运行的,则一旦用交叉验证找到了最佳的估计器,就会在整个训练集上重新训练。这是一个好方法,因为用更多数据训练会提高性能。
当然,也可以得到评估得分:
#查看不同"params"对应的"mean_test_score"平均评估分
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
通过观察上面各超参数对应的RMSE分数可知最小的RMSE对应的超参数是max_features=8, n_estimators=30
网格搜索可以自动判断是否添加一个你不确定的特征(比如,使用转换器CombinedAttributesAdder的超参数add_bedrooms_per_room)。它还能用相似的方法来自动找到处理异常值、缺失特征、特征选择等任务的最佳方法。
方法二:随机搜索(RandomizedSearchCV)
探索相对较少的组合时,就像前面的例子,网格搜索还可以。但是当超参数的搜索空间很大时,最好使用RandomizedSearchCV。这个类的使用方法和类GridSearchCV很相似,但它不是尝试所有可能的组合,而是通过选择每个超参数的一个随机值的特定数量的随机组合。这个方法有两个优点:
● 如果你让随机搜索运行,比如 1000 次,它会探索每个超参数的 1000 个不同的值(而不是像网格搜索那样,只搜索每 个超参数的几个值)。
● 你可以方便地通过设定搜索次数,控制超参数搜索的计算量。
方法三:集成方法
另一种微调系统的方法是将表现最好的模型组合起来。组合(集成)之后的性能通常要比单独的模型要好(就像随机森林要比单独的决策树要好),特别是当单独模型的误差类型不同时。
分析最佳模型和它们的误差
通过检查最佳模型,可以对问题进行深入的了解。 例如,RandomForestRegressor给出了了每个属性对做出准确预测的重要性:
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances
我们在相应的属性名称旁边显示这些重要性得分:
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_encoder=full_pipeline.named_transformers_['cat']
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances,attributes), reverse=True)
有了这些信息,您可以尝试删除一些不太有用的特征。比如:本例子中只要一个分类ocean_proximity(即原始数据集中有文字标签那一列)就够了,其他特征可以删除。
2.8 用测试集评估系统
调整模型后,您最终将拥有一个性能良好的系统,现在是时候在测试集中评估最终模型了。 这个过程没有什么特别的,只需从测试集中获取预测变量和标签,运行full_pipeline即可转换数据(调用transform(),而不是fit_transform()——您不希望拟合测试集!),然后评估测试中的最终模型 :
final_model = grid_search.best_estimator_
X_test = strat_test_set.drop("median_house_value", axis=1) #strat_test_set 是经过分层抽样处理后的测试集
y_test = strat_test_set["median_house_value"].copy()
X_test_prepared = full_pipeline.transform(X_test) #注意这里调用的是transform()而不是fit_transform()!
final_predictions = final_model.predict(X_test_prepared)
final_mse = mean_squared_error(y_test, final_predictions)#带入测试集中的标签值和预测值
final_rmse = np.sqrt(final_mse)
final_rmse
在某些情况下,这种泛化误差的点估计值不足以具有说服力:如果它仅比当前的模型好 0.1% ,该怎么办? 您可能想知道此估计的精确度。 为此,您可以使用scipy.stats.t.interval()计算出泛化误差的95%置信区间:
#用scipy.stats.t.interval()计算出泛化误差的95%置信区间:
from scipy import stats
confidence = 0.95#置信区间设为95%
squared_errors = (final_predictions - y_test) ** 2#求出误差的平方
np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1,
loc=squared_errors.mean(),
scale=stats.sem(squared_errors)))
如果您进行了许多超参数调整,测试集上的性能通常会比使用交叉验证所测得的结果差一点(因为系统最终会进行微调使其在验证数据上表现很好,但在未知数据集上可能会表现不佳) 。
现在进入项目的预启动阶段:您需要展示您的解决方案(突出您学到的知识,有效的方法和无效的方法,做出的假设以及系统的局限性),记录所有内容并创建具有清晰可视化效果的漂亮演示文稿以及易于记忆的陈述(例如,“中位数收入是房价的第一预测因子”)。 在加利福尼亚的住房示例中,系统的最终性能并不好于专家的价格估计,后者通常下降了约20%,但启动它可能仍然是一个好主意,尤其是如果这可以腾出一些时间 给专家,这样他们就可以从事更有趣和更有成果的工作。
2.9 启动、监控、维护系统
现在,您需要准备好解决方案以进行实际应用(例如,完善代码,编写文档和测试等),然后,您可以将模型部署到生产环境中。一种实现方法是保存训练好的Scikit-Learn模型(例如,使用joblib),包括完整的预处理和预测管道,然后在生产环境中加载模型,并通过调用predict()进行预测。例如,也许该模型将在网站内使用:用户将输入有关区域的一些数据,然后单击“估计价格”按钮。这会将包含数据的查询发送到Web服务器,然后将其转发到Web应用程序,最后您的代码将只需调用模型的predict()方法(在服务器启动时加载模型,而不是每次都加载使用模型)。或者,您可以将模型包装在专用Web服务中,您的Web应用程序可以通过REST API查询该模型。这使得在不中断主应用程序的情况下更轻松地将模型升级到新版本。它还可以简化扩展,因为您可以根据需要启动任意数量的Web服务,并在这些Web服务之间平衡来自Web应用程序的请求。而且,它允许您的Web应用程序使用任何语言,而不仅仅是Python。
2.10 实践!
这一章能告诉你机器学习项目是什么样的,你能用学到的工具训练一个好系统。你已经看到,大部分的工作是数据准备步骤、搭建监测工具、建立人为评估的流水线和自动化定期模型训练,当然,最好能了解整个过程、熟悉三或四种算法,而不是在探索高级算法上浪费全部时间,导致在全局上的时间不够。
因此,如果你还没这样做,现在最好拿起台电脑,选择一个感兴趣的数据集,将整个流程从头到尾完成一遍。一个不错的着手开始的地点是竞赛网站,比如 http://kaggle.com/:你会得到一个数据集,一个目标,以及分享经验的人。
2.11 练习
使用本章的房产数据集:
- 尝试一个支持向量机回归器(
sklearn.svm.SVR
),使用多个超参数,比如kernel="linear"
(多个超参数C
值)。现在不用担心这些超参数是什么含义。最佳的SVR
预测表现如何? - 尝试用
RandomizedSearchCV
替换GridSearchCV
。 - 尝试在准备流水线中添加一个只选择最重要属性的转换器。
- 尝试创建一个单独的可以完成数据准备和最终预测的流水线。
- 使用
GridSearchCV
自动探索一些准备过程中