学习机器学习时,我们最好使用真实的数据来做实验,而不仅是使用人工数据集。在网络上有成千上万的涉及各种领域的开放数据集可供选择。有一些地方可以查看获得数据:
在本章中,我们选择了StatLib repository中的California Housing Prices数据集(参见图2-1)。该数据集基于1990年加州人口普查的数据。它并不完全是最近的,但它有很多学习的地方,所以我们将假设它是最近的数据。为了教学目的,我们还添加了一个分类属性并删除了一些特性。
图(2-1)加州房价
你被要求完成的第一项任务是使用加州人口普查数据建立一个加州房价模型。这些数据包含人口、收入中值、房价中值等指标,适用于加利福尼亚州的每个街区组。块组是美国人口普查局发布样本数据的最小地理单位(一个块组通常有600到3000人)。我们就简称他们为“区域”。
您的模型应该从这些数据中学习,并能够预测任何地区的房价中值,给出所有其他指标。
要问老板的第一个问题是,公司的目的到底是什么;建立一个模型可能不是最终目标。公司期望如何使用和受益于这个模型?这很重要,因为它将决定您如何构建问题,您将选择什么算法,您将使用什么性能度量来评估您的模型,以及您应该花费多少精力来调整它。
你的老板回答说,你的模型的输出(对一个地区房价中位数的预测)以及许多其他的信号将被输入到另一个机器学习系统(见图2-2)。这个下一阶段系统将决定在某个地区投资是否值得。处理好这一点至关重要,因为它直接影响到收益。
下一个要问的问题是,当前的解决方案是什么样子的(如果有的话)。它通常会给你一个参考性能,以及如何解决问题的见解。你的老板回答说,该地区的房价目前是由专家手动估算的:一个团队收集有关该地区的最新信息(不包括房价中值),然后使用复杂的规则来进行估算。这既昂贵又耗时,而且他们的估计也不准确;他们的典型错误率约为15%。
有了这些信息,现在就可以开始设计系统了。首先,你需要框定问题:它是监督的,非监督的,还是强化的学习?它是一个分类任务,一个回归任务,还是别的什么?你应该使用批量学习还是在线学习技术?在你继续阅读之前,先试着自己回答这些问题。
你找到答案了吗?让我们看看:这显然是一个典型的监督学习任务,因为你被给予了标记的训练实例(每个实例都带有预期的输出,即,该区房价中位数)。此外,这也是一个典型的回归任务,因为你被要求预测一个值。更具体地说,这是一个多变量回归问题,因为系统将使用多个特征来进行预测(它将使用该地区的人口、中等收入等)。在第一章中,你仅仅根据一个特征,即人均GDP来预测生活满意度,所以那是一个单变量回归问题。最后,系统中没有连续的数据流,也不需要特别快速地调整数据的变化,而且数据足够小,可以放入内存中,所以普通的批处理学习应该没有问题。
下一步是选择一个性能度量。回归问题的典型性能度量是均方根误差(RMSE)。它测量系统在其预测中所犯错误的标准偏差。例如,RMSE等于50,000意味着68%的系统预测落在实际价值50,000美元以内,95%的预测落在实际价值100,000美元以内。方程2-1给出了计算RMSE的数学公式。
R M S E ( X , h ) = 1 m ∑ i = 1 m ( h ( x ( i ) ) − y ( i ) ) 2 RMSE(X,h) = \sqrt{\dfrac{1}{m} \sum_{i=1}^m(h(x^{(i)})-y^{(i)})^2} RMSE(X,h)=m1i=1∑m(h(x(i))−y(i))2
在公式中,m是数据集中实例的数量,x是数据集中实例的所有特征值的向量,X是一个矩阵,包含数据集中所有实例的所有特征值。h是系统的预测函数,也称为假设。RMSE(X,h)是使用假设h在一组示例中度量的成本函数。
尽管RMSE通常是回归任务的首选性能度量,但在某些环境中,您可能更喜欢使用另一个函数。例如,假设有许多异常数据。在这种情况下,可以考虑使用平均绝对误差(也称为平均绝对偏差;见方程2 - 2):
M A E ( X , h ) = 1 m ∑ i = 1 m ∣ h ( x ( i ) ) − y ( i ) ∣ MAE(X,h) = \dfrac{1} {m}\sum_{i=1}^m\vert{h(x^{(i)})-y^{(i)}}\vert MAE(X,h)=m1i=1∑m∣h(x(i))−y(i)∣
RMSE和MAE都是测量两个向量之间距离的方法:预测向量和目标值向量。各种距离测量或标准都是合适的:
在Jupyter notebook中浏览程序范例。完整的Jupyter notebook在https://github.com/ageron/handson-ml。
首先需要安装Python。你可能已经安装了。如果没有,可以在https://www.python.org/下载。
接下来,为机器学习代码和数据集创建一个工作空间目录。打开终端并输入以下命令(在$ prompts之后(linux)):
$ export ML_PATH = "$HOME/ml" #你可以使用你自定义的地址
$ mkdir -p $ML_PATH
您需要一些Python模块:Jupyter、NumPy、panda、Matplotlib和Scikit-Learn。如果你已经安装了所有这些模块,你可以安全地跳到第43页的“下载数据”。如果你还没有,这里有很多方法来安装(以及服务)。(安装部分跳过,需要下载的自行观看原书或者百度)
在典型的环境中,您的数据应当在关系数据库(或其他一些公共数据存储)中可用,并分布在多个表/文档/文件中。为了访问数据,你首先需要获得你的凭证和访问授权,以及熟悉自己的数据模式。但是,在我们要做的这个项目中,不必要这么麻烦:只需下载一个压缩文件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 = 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):
os.makedirs(housing_path, exist_ok=True)
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(),会创建一个 data/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)
这个函数返回包含所有数据的Pandas DataFrame对象
使用DataFrame的head()方法查看前五行
每行代表一个地区。有10个属性:logitude(经度)、latitude(纬度)、housing_median_age(住房年龄中位数)、total_rooms(总房间数)、total_bedrooms(总卧室数)、population(人口)、households(家庭数)、median_income(收入中位数)、median_house_value(房价中位数)和ocean_proximity(临海距离)。
info()方法对于快速描述数据非常有用,特别是总的行数、每个属性的类型和非空值的数量(参见图2-6)。
数据集中有20,640个实例,这按照机器学习的标准,它相当小,但是对于入门来说很完美。请注意,total_bed_rooms属性只有20,433个非空值,也就是说有207个地区没有这个属性。我们稍后需要处理这个问题。
除了海洋邻近域所有的属性都是数值。它是object类型,所以它可以容纳任何类型的Python对象,但是因为这是从CSV文件加载的数据,所以它是一个文本属性。当你查看前5行时,可能会注意到该列中有的值是重复的,这意味着它可能是一个离散属性。您可以使value_counts()方法查找存在哪些类别以及每个类别有多少个区:
看看其他方法。describe()方法可以对数值属性进行总结
count、mean、min和max行都知道意思。这里要注意,null值被忽略了(例如,total_bedroom数据个数是20,433,而不是20,640)。std行显示了标准偏差(度量值的分散程度)。25%、50%和75%的行表示相应的百分位数:这表示一组观测中低于给定百分比以下的值。例如,25%的地区housing_median_age低于18,50%低于29,75%低于37。这通常被称为25th百分位(或1st四分位)、中位数和75h百分位(或3st四分位)。
另一种快速了解数据类型的方法是为每个数值属性绘制直方图。直方图显示具有给定值范围(在纵轴上)的实例的数量(在横轴上)。你可以一次绘制一个属性,也可以调用整个数据集上的hist()方法,它会绘制每个数值属性的直方图(参见图2-8)。例如,您可以看到略多于1000个地区的median_house_value值大约等于50万美元。
注意这些柱状图中的一些事情:
通过这些我们现在对正在处理的数据类型有了更好的理解。
在这个阶段,留出部分数据可能听起来很奇怪。毕竟,您只是粗略地看了一下数据,在决定使用什么算法之前,您肯定应该对数据有了更多的了解。这是真的,但你的大脑是一个了不起的模式检测系统,这意味着它很容易过拟合:如果你看看测试集,您可能会在测试数据中偶然发现一些看似有趣的模式,从而选择一种特定的机器学习模型。当您使用测试集估计泛化误差时,您的估计将过于乐观,您将启动的系统不会像预期的那么好。这叫做数据窥探偏差。
创建一个测试集在理论上是非常简单的:只是随机选择一些实例,通常是数据集的20%,然后把它们放在一边:
然后我们用一下这个函数看看它能否运行
不过这里有个问题,它每次运行都会得到不同的测试集,到了最后,你的机器学习算法会看到所有的数据集,这是必须要避免的。
一种解决方案是在第一次运行时保存测试集,然后在运行的时候加载它就可以了。另一种选择是设置随机数生成器(比如,np.random.seed(42))在调用np.random.permutation()之前,这样每次打乱都是生成相同的索引。
但这两种解决方案都将在下次获取更新的数据集时失效。通常的解决方案是使用每个实例的标识符来决定它是否应该进入测试集(假设实例有惟一且不可变的标识符)。例如,可以计算每个实例标识符的哈希值,只保留哈希值的最后一个字节,如果该值小于或等于51(256的前20%),则将该实例放入测试集中。这将确保测试集在多次运行时保持一致,即使您刷新数据集也是如此。新的测试集将包含20%的新实例,但是它将不包含任何之前在训练集中的实例。这种实现是可能的.
但是我们没有可以作为标识符的一列,最简单的解决方法是将每一行作为ID。
如果使用行索引作为惟一标识符,则需要确保新数据会追加到数据集的末尾,并且不会删除任何行。这是不可能的,所以应该尝试使用最稳定的特性来构建唯一标识符。例如,一个地区的经度和纬度在几百万年内是稳定的,所以可以将它们组合成一个ID,如下所示:
Scikit-Learn提供了一些函数来以各种方式将数据集分割成多个子集。最简单的函数是train_test_split,它的功能与前面定义的函数split_train_test基本相同,只是增加了一些额外的功能。首先是一个random_state参数允许您设置随机数生成器的种子,正如前面所解释的那样,其次,你可以传递给它具有相同行数的多个数据集,它会在相同的索引上拆分它们(如果你有一个单独的DataFrame标签,这是非常有用的):
到目前为止,我们已经考虑了纯随机抽样方法。如果数据集足够大(特别是相对于属性的数量而言),这通常没有问题,但是如果数据集不够大,则可能会引入显著的抽样偏差。当一家调查公司决定给1000个人打电话问几个问题时,他们不会在电话亭里随机挑选1000个人。他们必须确保这1000人代表全体人民。例如,美国人口中女性占51.3%,男性占48.7%,因此,一项在美国进行的良好的调查会在样本中保持这一比例:513名女性和487名男性。这叫做分层抽样:人口被划分为同质的子组,称为阶层,并从每个阶层中抽取正确的实例数,以保证测试集代表总体人口。如果他们使用的是随机取样,那么在一个有偏差的测试集中,有12%的几率是女性少于49%或者多于54%。不管怎样,调查结果都会有很大的偏差。
假设你和专家聊天,他们告诉你中等收入是预测中等房价的一个非常重要的因素。所以要确保测试集代表整个数据集中的各种收入类别。由于收入中位数是一个连续的数值属性,所以首先需要创建一个收入类别属性。先看看收入中位数直方图(图2-9)
大多数收入中位数在2-5(数万美元)左右,但一些收入中值远远超过6。你的数据集中每个阶层拥有足够的实例数量是很重要,否则对社会阶层的重要性的估计会出现偏差。也就是说不应该有太多的阶层,但是每一个阶层都应该足够大。下面的代码通过将收入中位数除以1.5来创建一个收入类别属性(以限制收入类别的数量),并使用ceil(对有离散的类别)进行舍入,然后将所有大于5的类别合并到类别5中:
现在已经可以在收入类别中进行分层取样了,这个可以使用Scikit-Learn的StratifiedShuffleSplit类:
检查一下它能否正常工作,查看收入类别占整个数据集的比例:
使用类似的代码,您可以测量测试集中的收入类别比例。图2-10比较了整体数据集、分层抽样生成的测试集和纯随机抽样生成的测试集中的收入类别比例。正如您所看到的,使用分层抽样生成的测试集的收入类别比例几乎与完整数据集中的收入类别比例相同,而使用纯粹随机抽样生成的测试集则非常不准确。
然后我们要删除income_cat属性让数据集变回原来的样子:
我们在测试集生成上花费了大量的时间,这是因为这是机器学习项目中经常被忽视但却至关重要的一部分。此外,这些想法中有许多将在我们稍后讨论交叉验证时有用。现在该进入下一个阶段了:研究数据。
到目前为止,您只需要快速浏览一下数据,就可以大致了解正在操作的数据类型。现在我们的目标是更深入一点。
首先,确保你已经把测试集放在一边,你只是在探索训练集。此外,如果训练集非常大,你可能想要采样一个探索集,使操作简单和快速。在我们的例子中,这个集合非常小,所以你可以直接操作整个集合。让我们创建一个副本,这样你就可以在不损害训练集的情况下使用它:
由于存在地理信息(纬度和经度),所以最好创建一个所有区域的散点图来可视化数据(图2-11):
这看起来很像加州,但除此之外,很难看出任何特别的规律。将alpha选项设置为0.1使得数据点密度高的地方更容易可视化(图2-12):
现在情况好多了:你可以清楚地看到高密度区域,即旧金山湾区和洛杉矶、圣地亚哥周围,加上中央山谷的一长串高密度区域,尤其是萨克拉门托和弗雷斯诺周围。
更一般地说,我们的大脑非常善于发现图片上的特点,但你可能需要设置一下可视化参数,让这些特点脱颖而出。
现在看看房价(图2-13)。每个圆的半径代表该地区的人口(属性s),颜色代表价格(属性c),我们将使用一个称为jet的预定义的颜色地图(属性cmap),它的范围从蓝色(低价格)到红色(高价格):
这幅图告诉你,房价与地理位置(例如,靠近大海)和人口密度有很大关系。使用集群算法来检测主集群,并添加新的特性来度量与集群中心的距离,这可能会很有用。ocean_proximity属性可能也有用,虽然在北加州,沿海地区的房价并不太高,所以它不是一个通用的规律。
由于数据集不是太大,可以使用corr()方法轻松计算每一对属性之间的标准相关系数(也称为Pearson’s r).
现在让我们看看每个属性与中位数房屋价值的相关性:
相关系数在-1到1之间。当它接近1时,表示正相关关系很强;例如,中位数的房屋价值随着中位数收入的增加而增加。当系数接近-1时,说明存在很强的负相关关系;你可以看到在纬度和房屋价值中位数之间有一个小的负相关。当你向北走的时候,价格有下降的趋势。最后,系数接近于零意味着不存在线性相关。图2-14显示了水平轴和垂直轴之间的各种图以及相关系数。
检查属性之间相关性的另一种方法是使用panda的scatter_matrix函数,该函数将每个数值属性与每个其他数值属性作图。由于现在有11个数值属性,您将得到112=121个图,这些图在一页纸上是放不下的,所以让我们来关注几个最有价值的曲线,它们似乎与住房价值中值相关(图2-15):
如果panda将每个变量与自己作图,主对角线(从左上角到右下角)将布满直线,这不会很有用。因此,取而代之的是panda显示每个属性的直方图(还有其他选项;更多细节见pandas’ documentation)。
预测中位数房屋价值最有希望的属性是中位数收入,让我们放大其相关的散点图(图2-16):
这个情节揭示了一些事情。首先,相关性确实很强;你可以清楚地看到上升的趋势,这些点不是很分散。第二,我们之前注意到的价格上限是一条50万美元的水平线。但这幅图还揭示了其他一些不那么明显的直线:一条横线在45万美元左右,另一条在35万美元左右,或许还有一条在28万美元左右,再往下还有几条。您可能想要尝试删除相应的区域来阻止算法从学习中重现这些数据怪癖。
前几节已经让您了解了一些探索数据和分析的方法。在将数据提供给机器学习算法之前,您确定了一些可能需要清理的数据怪癖,并发现了属性之间有趣的相关性,特别是与目标属性之间的相关性。还注意到,有些属性的重尾分布比较大,因此可能需要转换它们(例如,通过计算它们的对数)。当然,您的里程会随着每个项目的不同而有很大的不同,但是总体思想是相似的。
在为机器学习算法准备数据之前,您可能想做的最后一件事是尝试各种属性组合。例如,如果您不知道一个地区有多少家庭,那么这个地区的房间总数就不是很有用。你真正想要的是每户的房间数。同样,卧室总数本身也不是很有用:您可能想要将它与房间数进行比较。以及每户人口看起来是一个有趣的属性组合。现在创建这些新属性:
我们再来看看属性之间的相关性
不错,这个新的属性bedrooms_per_room与房屋价值中值的相关性要比与房间或卧室总数的相关性大得多。显然,卧室/房间比例较低的房子往往更贵。每户的房间数也比一个地区的房间总数更能说明问题——显然房子越大,价格越贵。
这一轮的探索不必是绝对彻底的;关键是要有一个正确的开始,并迅速获得结论,这将帮助你得到第一个适合良好的原型。但是这是一个迭代的过程:一旦你有了一个原型并开始运行,你就可以分析它的输出以获得更多的见解并回到这个探索步骤。
现在是为机器学习算法准备数据的时候了。你应该写一些函数来代替手工操作,这样做有几个很好的理由:
大多数机器学习算法不能处理缺少的特性,我们要创建一个小函数来解决它们。之前我们说过一个total_bedrooms属性有缺失值,我们对其进行修正,有三种方法:
可以通过使用DataFrame的dropna(),drop(),和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实例,指定你想用属性的中位数替换每个属性缺失的值:
由于这个中位数只能用来计算数值属性,我们需要创建一个没有文本属性ocean_proximity的数据:
现在可以使用fit()方法将imputer实例与训练数据进行拟合:
imputer只是简单地计算每个属性的中位数,并将结果存储在其statistics_ 实例变量中。只有total bedroons属性有缺失的值,但是我们不能确定在系统运行后新数据中是否会有缺失的值,所以将imputer应用到所有的数值属性上会更安全:
现在,您可以使用这个“训练”imputer来转换训练集通过习得的中位数替换缺失的值
结果是一个包含转换后的特性的普通Numpy数组。如果你想把它放回panda DataFrame,很简单:
前面我们忽略了分类属性ocean_proximity,因为它是文本属性,所以我们无法计算它的中位数。大多数机器学习算法都是先处理数字的,所以让我们把这些文本标签转换成数字。
Scikit-Learn为这个任务提供了一个名为LabelEncoder(这里我用的是OrdinaEncoder因为import LableEncode会报错)的转换器:
这样更好:现在我们可以在任何ML算法中使用这个数值数据。你可以看看这个编码器已经使用classes_属性(在OrdinaEncoder中应该使用categories_属性)产生的映射(“<1H OCEAN”映射到0,“INLAND”映射到1,等等):
这种表示法的一个问题是,ML算法会假设两个相邻的值比两个遥远的值更相似。显然情况并非如此(例如类别0和4比0和1)类别相似。为了解决这个问题,一个常见的解决方案是创建一个二进制属性/类别:当类别为“<1H OCEAN”时,一个属性等于1(否则为0),当类别为“INLAND”时,另一个属性等于1(否则为0),等等。这称为one-hot编码,因为只有一个属性将等于1 (hot),而其他属性将等于0 (cold)
Scikit-Learn提供了一个OneHotEncoder编码器,用于将整数分类值转换成one-hot向量。让我们将这些类别编码为one-hot向量。注意,fit_transform()需要一个二维数组,但是cat编码的是一个一维数组,因此我们需要对它进行重新设计(如果重新设计,需要进行reshape(-1,1),我这里直接将文本属性转换成了one-hot):
注意,输出是一个SciPy稀疏矩阵,而不是NumPy数组。这在有包含数千个类别的分类属性时非常有用。经过一次热编码之后,我们得到了一个有数千列的矩阵,除了每一行有一个1之外,其他都是0。使用大量的内存来存储零是非常浪费的,因此一个稀疏矩阵只存储非零元素的位置。你可以像使用一个普通的2D数组一样使用它,但是如果你真的想把它转换成一个NumPy数组,只需调用toarray()方法:
我们可以使用LabelBinarizer类一次性应用这两种转换(从文本类别到整数类别,然后从整数类别到热门向量)(在这里也无法导入LableBinarizer类,如果直接转换照 In [65]也是可以做到的,默认情况下得到的是一个稀疏矩阵,可以设置OneHotEncoder(sparse=False)直接获得数组):
虽然Scikit-Learn提供了许多有用的转换,但是您需要编写自己的任务,比如自定义清理操作或组合特定的属性。定制的transformer要能够与scikit - learn的功能(如pipelines)无缝地工作,而且由于scikit - learn依赖于duck类型(而不是inheritance),所以您只需创建一个类并实现三个方法:fit()(返回原参数)、transform()和fit-transform()。还可以通过添加TransformerMixin作为基类来获得最后一个类。另外,如果您将BaseEstima tor作为基类添加(并在构造函数中避免*args和**kargs),您将获得两个额外的方法(get_params()和set_params()),这对于自动超参数调优非常有用。例如,这里有一个小的transformer类,它添加了我们前面讨论的属性:
在这个例子中,转换器有一个超参数add_bedrooms_per_room,它被默认设置为True(提供合理的默认值通常是有帮助的)。这个超参数测量器可以让您轻松地找出添加这个属性是否有助于机器学习算法。更一般地,您可以将超参数添加到gate任何您不能100%确定的数据准备步骤。这些数据准备步骤越多,您就可以自己尝试更多的组合,从而更有可能找到一个好的组合(并节省大量时间)
需要应用于数据的最重要的转换之一是特性缩放。除了少数例外,当输入的数值属性具有非常不同的尺度时,机器学习算法会表现得不好。housing的数据也是如此:总的房间数在6到39320之间,而收入中位数只有0到15之间。请注意,通常不需要调整目标值。
有两种常见的方法可以使所有属性具有相同的比例:归一化和标准化。
归一化非常简单:转换数据和重新缩放,结果从0到1。我们通过减去最小值,然后除以最大值减去最小值来做到这一点。Scikit-Learn提供了一个名为MinMaxScaler的转换器,它有一个feature_range超参数,如果出于某种原因不想要0-1,可以使用它来更改范围。
标准化不同:首先它减去平均值(所以标准化值的平均值是零),然后除以方差,这样结果分布就有单位方差。与归一化不同,标准化不会将值限制在特定的范围内,这对于某些算法(例如,神经网络通常期望输入值在0到1之间)来说可能是个问题。例如,假设一个地区的收入中位数等于100(这是错误的)。归一化会将所有其他值从0-15压缩到0-0.15,而标准化则不会受到太大影响。Scikit-Learn提供了一个称为 StandardScaler的转换器。
可以看到,有许多数据转换步骤需要按正确的顺序执行。Scikit-Learn提供了PIpeline类来帮助处理这样的转换序列。下面是数值属性的一个小pipelines:
pipelines构造函数接受定义步骤序列的名称或估计器对的列表。除了最后一个估计器外,其他估计器都必须是转换器(它们必须有一个fit_transform()方法)。名字可以是你喜欢的任何名字。
当您调用pipelines的fit()方法时,它会依次调用所有转换器上的fit_transform(),并将每个调用的输出作为参数传递给下一个调用,直到它到达最后的估计器,在最后的估计器只调用fit()方法。
pipelines公开与最终估计器相同的方法。在这个例子中,最后估计器是StandardScaler,这是一个转换器,因此pipelines有一个transform()方法,适用于所有的转换序列中的数据(它也有一个fit_transform方法可以使用,而不是调用fit()和transform())。
现在已经有了一个数值的pipelines,还需要对分类值应用LabelBinarizer进行处理:如何将这些转换添加到一个单的pipelines中?Scikit-Learn为此提供了一个FeatureUnion类。你给它一个转换器的列表(整个transformer pipelines),当其transform()方法并行运行每个转换器的transform()方法,等待他们的输出,然后将它们连起来并返回结果(当然,也称其fit()方法调用所有每个转换器的fit()方法)。处理数值属性和分类属性的完整pipelines可能是这样的:
然后我们运行一下
每个子pipeline都以一个选择转换器开始:它只是通过选择所需的属性(数值或分类)来转换数据,删除其余的属性,并将结果DataFrame转换为NumPy数组。scikit中没有任何东西可以处理panda数据流,因此我们需要为这个任务编写一个简单的自定义转换器:
到了最后一步了!你提出了问题,你得到了数据并对其进行了研究,你对一个训练集和一个测试集进行了采样,你编写了转换管道来清理并为机器学习算法自动准备你的数据。现在,您已经准备好选择和培训一个机器学习模型。
好消息是,由于前面所有这些步骤,事情现在将比您想象的简单得多。让我们首先训练一个线性回归模型,就像我们在前一章所做的:
现在你有了一个有效的线性回归模型。让我们从训练集中的几个例子来尝试一下:
这是有效的,尽管预测并不完全准确让我们使用Scikit-Learn的 mean_squared_error函数来测量这个回归模型在整个训练集上的均方误差:
好吧,这比什么都没有强,但显然不是一个很好的分数:大多数地区的房屋价值中值在12万美元到26.5万美元之间,所以一个典型的预测误差为68,628美元不是很令人满意。这是一个模型与训练数据不搭配的例子。当这种情况发生时,它可能意味着特征无法提供足够的信息做出好的预测,或者模型不够强大。正如我们在前一章中看到的,修复欠拟合的主要方法是选择一个更强大的模型,为训练算法提供更好的特征,或者减少模型上的约束。这个模型不是规范化的,因此排除了最后一个选项。您可以尝试添加更多的特性(例如,population的对数),但首先让我们尝试一个更复杂的模型,看看它是如何做到的。
让我们训练一个DecisionTreeRegressor。这是一个强大的模型,能够在数据中找到复杂的非线性关系(决策树在第6章有更详细的介绍)。这个代码应该看起来很熟悉:
现在模型已经训练好了,让我们在训练集上对其进行评估:
但是误差结果是0,一点错误都没有吗?这个模型真的是绝对完美的吗?当然,更有可能的是,模型严重地过度拟合了数据。怎么确定的呢?正如我们前面看到的,在准备好启动一个您确信的模型之前,您不希望接触测试集,因此您需要使用部分训练集来进行训练,而部分训练集用于模型验证。
评估决策树模型的一种方法是使用train_test_split函数可以将训练集划分成更小的训练集和验证集,然后对小训练集训练你的模型和用验证集验证评价模型。这是一个没有什么太困难的工作,它会处理好。
一个不错的选择是使用Scikit-Learn的交叉验证功能。下面的代码执行K-fold交叉验证:它将训练集随机分成10个称为fold的不同子集,然后对决策树模型进行10次培训和评估,每次选择一个不同的fold进行评估,然后对其他9个折叠进行训练。结果是一个包含10个评价分数的数组:
看看结果:
现在决策树看起来不像之前那么好了。事实上,它似乎比线性回归模型更糟。请注意,交叉验证不仅允许您获得对模型性能的估计,而且还允许您度量此估计的精确程度(即,其标准差)。决策树的值约为71,200,一般±3,200。如果只使用一个验证集,就不会有这些信息。但是交叉验证是以对模型进行多次训练为代价的,所以并不总是可行的。
让我们为线性回归模型计算相同的分数进行确定:
这是正确的:决策树模型过度拟合的情况非常严重,以至于它的性能比线性回归模型更差。
现在让我们尝试最后一个模型:RandonForestRegressor。正如我们将在第七章中看到的,随机森林通过在特征的随机子集上训练许多决策树,然后平均它们的预测来工作。在许多其他模型的基础上构建模型称为集成学习,这通常是进一步推动ML算法的好方法。我们将跳过大部分的代码,因为它与其他模型本质上是相同的:
哇,这好多了:随机森林看起来很有前途。但是,注意训练集的分数仍然比验证集的分数低得多,这意味着模型仍然对训练集进行过拟合。解决方案是简化模型,约束模型,或者获得更多的训练数据。然而,在您深入随机森林之前,您应该尝试来自不同类别机器学习算法的许多其他模型(几个具有不同内核的支持端口向量机,一个神经网络,等等),而不要花费太多时间来调整超参数。我们的目标是列出一些(2-5个)可用的模型。
让我们假设您现在有一个有前途的模型的候选列表。现在需要对它们进行微调。让我们来看看几个方法。
一种方法是手动修改超参数,直到找到超参数值的最佳组合。这将是非常乏味的工作,您可能没有时间探索出最佳组合。
这里我们可以让Scikit-Learn的GridSearchCV来帮你搜索。您所需要做的就是告诉它您想让它试验哪些超参数,以及要试验哪些值,然后它将使用交叉验证来评估所有可能的超参数值组合。例如,下面的代码为RandomForestRegressor搜索超参数值的最佳组合:
这个参数网格告诉Scikit-Learn首先计算所有3 x 4 = 12个n_estimators 和max_features超参数值的组合,这些超参数值在第一个dict中指定(现在不要担心这些超参数意味着什么;它们将在第7章中进行解释),然后在第二个dict中尝试所有2 x 3 = 6个超参数值的组合,但是这次将bootstrap超参数设置为False而不是True(这是这个超参数的默认值)。
总之,网格搜索将探索12 +6 = 18个RandomForestRegressor超参数值的组合,并将对每个模型进行5次训练(因为我们使用5次交叉验证)。换句话说,总而言之,将会有18 x 5 = 90轮的训练,这可能需要相当长的时间,但当它完成时,可以得到最好的参数组合如下:
也可以直接得到最好的估计值:
当然,评价分数也有,让我们看看网格搜索中每个超参数组合的得分:
在本例中,我们通过将max_features超参数设置为8,将n_estimators超参数设置为30来获得最佳解决方案。此组合的RMSE得分为49682,比您先前使用默认超参数值要好(52,583)。这里我们已经成功微调好了我们的模型。
当您探索相对较少的组合时,比如前面的示例,网格搜索方法是不错的,但是当超参数搜索空间很大时,通常更可取的方法是使用RandomizedSearchCV。这个类的使用方式与GridSearchCV类非常相似,但是它不是尝试所有可能的组合,而是通过在每次迭代中为每个超参数选择一个随机值来计算给定数量的随机组合。这种方法有两个主要好处:
调整系统的另一种方法是尝试组合性能最好的模型。群(或“集成”)的表现通常比最好的单个模型更好(就像随机森林比它们所依赖的单个决策树表现得更好一样),特别是当单个模型犯了非常不同类型的错误时。我们将在第7章更详细地讨论这个主题。
通过检查最好的模型,您通常会对这个问题有很好的见解。例如,RandomForestRegressor可以指出每个属性对于做出准确预测的相对重要性:
将这些重要性得分显示在它们相应的属性名旁边:
有了这些信息,可以尝试删除一些不太有用的特性(例如,只有一个ocean_proximity类别是真正有用的,所以您可以尝试删除其他类别)。
您还应该查看系统所犯的特定错误,然后尝试理解它为什么会犯这些错误,以及什么可以修复这个问题(添加额外的特性,或者相反,删除不提供信息的特性,清除异常值,等等)。
在对模型进行了一段时间的调整之后,您最终得到了一个可以执行的系统。现在是时候评估测试集上的最终模型了。这个过程没有什么特别之处;只需从测试集中获取预测器和标签,运行full_pipeline来转换数据(调用transform(),而不是fit transform()!),然后在测试集中评估最终模型:
如果您进行了大量超参数调优,那么性能通常会比使用交叉验证测量的性能稍差一些(因为您的系统最终进行了调优,以便在验证数据上表现良好,而在未知数据集上可能表现不佳)。在这个例子中不是这样的,但是当这种情况发生时,您必须抵制调整超参数以使测试集上的数字看起来不错;这些改进不太可能推广到新的数据。
现在是项目问世前的阶段:你需要展示你的解决方案(特别是你所学到的,什么有用什么没用,做了什么假设,你的系统的局限性是什么),记录一切,用清晰的可视化创建好的演讲和容易记住的语句(例如,“中值收入是预测房价的第一预测因子”)。