🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
在本章你将通过一个示例项目端到端地工作,假装是一家房地产公司最近聘请的数据科学家。1以下是您将要完成的主要步骤:
看大图。
获取数据。
发现和可视化数据以获得洞察力。
为机器学习算法准备数据。
选择一个模型并训练它。
微调您的模型。
提出您的解决方案。
启动、监控和维护您的系统。
什么时候你正在学习机器学习,最好用真实世界的数据做实验,而不是人工数据集。幸运的是,有数千个开放数据集可供选择,涵盖各种领域。您可以从以下几个地方获取数据:
流行的开放数据存储库
元门户(它们列出了开放的数据存储库)
其他页面列出了许多流行的开放数据存储库
在本章中,我们将使用来自 StatLib 存储库2的加利福尼亚房价数据集(见图 2-1)。该数据集基于 1990 年加州人口普查的数据。它不是最近的(湾区的一栋漂亮的房子当时仍然可以负担得起),但它有很多学习的品质,所以我们会假装它是最近的数据。出于教学目的,我添加了一个分类属性并删除了一些特征。
图 2-1。加州房价
欢迎机器学习住房公司!您的首要任务是使用加州人口普查数据建立该州的房价模型。该数据包括加州每个街区组的人口、收入中位数和房价中位数等指标。街区组是美国人口普查局发布样本数据的最小地理单位(街区组的人口通常为 600 到 3,000 人)。我们将它们简称为“区”。
您的模型应该从这些数据中学习,并能够在给定所有其他指标的情况下预测任何地区的房价中值。
小费
既然你是一个组织良好的数据科学家,你应该做的第一件事就是拿出你的机器学习项目清单。您可以从附录 B中的那个开始;它应该适用于大多数机器学习项目,但请确保使其适应您的需求。在本章中,我们将浏览许多清单项目,但我们也会跳过一些,因为它们是不言自明的,或者因为它们将在后面的章节中讨论。
这要问老板的第一个问题是业务目标到底是什么。建立模型可能不是最终目标。公司希望如何使用这种模式并从中受益?了解目标很重要,因为它将决定您如何构建问题、您将选择哪些算法、您将使用哪种性能度量来评估您的模型,以及您将花费多少精力来调整它。
你的老板回答说,你的模型的输出(一个地区房价中位数的预测)将与许多其他信号一起被馈送到另一个机器学习系统(见图 2-2)。3该下游系统将决定是否值得在特定领域进行投资。做到这一点至关重要,因为它直接影响收入。
图 2-2。用于房地产投资的机器学习管道
管道
一个数据处理组件的序列称为数据管道。管道在机器学习系统中非常常见,因为有很多数据需要处理,也有很多数据转换需要应用。
组件通常异步运行。每个组件都提取大量数据,对其进行处理,然后将结果输出到另一个数据存储中。然后,一段时间后,管道中的下一个组件提取此数据并吐出自己的输出。每个组件都是相当独立的:组件之间的接口只是数据存储。这使得系统易于掌握(借助数据流图),并且不同的团队可以专注于不同的组件。此外,如果一个组件发生故障,下游组件通常可以通过使用损坏组件的最后一个输出继续正常运行(至少一段时间)。这使得架构非常健壮。
另一方面,如果没有实施适当的监控,损坏的组件可能会在一段时间内被忽视。数据变得陈旧,整个系统的性能下降。
下一个要问老板的问题是当前的解决方案是什么样的(如果有的话)。当前的情况通常会给你一个性能参考,以及如何解决问题的见解。你的老板回答说,目前地区房价是由专家手动估算的:一个团队收集有关地区的最新信息,当他们无法获得房价中位数时,他们会使用复杂的规则进行估算。
这既费钱又费时,而且他们的估计也不大;在他们设法找出实际房价中值的情况下,他们通常会意识到他们的估计相差 20% 以上。这就是为什么该公司认为,考虑到有关该地区的其他数据,训练一个模型来预测该地区的房价中值是有用的。人口普查数据看起来像是一个很好的数据集,可以用于此目的,因为它包括数千个地区的房价中位数以及其他数据。
有了所有这些信息,您现在就可以开始设计您的系统了。首先,您需要界定问题:它是有监督的、无监督的还是强化学习?它是分类任务、回归任务还是其他什么?您应该使用批量学习还是在线学习技术?在继续阅读之前,请停下来尝试自己回答这些问题。
你找到答案了吗?让我们看看:这显然是一个典型的监督学习任务,因为你会得到带标签的训练样本(每个样本都有预期的输出,即该地区的房价中位数)。这也是一个典型的回归任务,因为您被要求预测一个值。更具体地说,这是一个多元回归问题,因为系统将使用多个特征进行预测(它将使用该地区的人口、收入中位数等)。它也是一个单变量回归问题,因为我们只是试图预测每个地区的单个值。如果我们试图预测每个地区的多个值,它将是一个多元回归问题。最后,没有连续的数据流进入系统,没有特别需要适应快速变化的数据,而且数据小到可以放入内存,所以简单的批量学习应该就可以了。
小费
如果数据很大,您可以将批量学习工作拆分到多个服务器(使用 MapReduce 技术)或使用在线学习技术。
您的下一步是选择绩效衡量标准。回归问题的典型性能度量是均方根误差 (RMSE)。它给出了系统在其预测中通常会产生多少错误的想法,对于大错误具有更高的权重。公式 2-1显示了计算 RMSE 的数学公式。
公式 2-1。均方根误差 (RMSE)
RMSE(X,h)=1m∑i=1mh(x(i))-y(i)2符号
这个equation 介绍了几种非常常见的机器学习符号,我们将在本书中使用它们:
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 ) ) ⊺。4
例如,如果第一区如刚才所述,则矩阵X如下所示:
h是您系统的预测函数,也称为假设。当您的系统被给定一个实例的特征向量x ( i )时,它会输出该实例的预测值ŷ ( i ) = h ( x ( i ) )(ŷ发音为“y-hat”)。
例如,如果您的系统预测第一区的房价中位数为 158,400 美元,则ŷ (1) = h ( x (1) ) = 158,400。该区的预测误差为ŷ (1) – y (1) = 2,000。
RMSE( X , h ) 是使用您的假设h在一组示例上测量的成本函数。
我们对标量值(如m或y ( i ))和函数名称(如h)使用小写斜体字体,对向量使用小写粗体字体(如x ( i )),对矩阵使用大写粗体字体(如X )。
尽管 RMSE 通常是回归任务的首选性能度量,但在某些情况下,您可能更喜欢使用其他函数。例如,假设有许多异常区域。在这种情况下,您可以考虑使用平均绝对误差(MAE,也称为平均绝对偏差;见公式 2-2):
公式 2-2。平均绝对误差 (MAE)
MAE(X,h)=1m∑i=1mh(x(i))-y(i)RMSE 和 MAE 都是衡量两个向量之间距离的方法:预测向量和目标值向量。各种距离度量或规范是可能的:
计算平方和 (RMSE) 的根对应于欧几里得范数:这是您熟悉的距离概念。它也被称为 ℓ 2 norm,记为 ∥ · ∥ 2(或只是 ∥ · ∥)。
计算绝对值之和 (MAE) 对应于 ℓ 1 norm,记为 ∥ · ∥ 1。这有时是之所以称为曼哈顿范数,是因为如果您只能沿着正交的城市街区旅行,它会测量城市中两点之间的距离。
更一般地,包含n 个元素的向量v的 ℓ k 范数定义为 ∥ v ∥ k = (| v 0 | k + | v 1 | k + ... + | v n | k ) 1/ k。ℓ 0给出向量中非零元素的数量,而 ℓ ∞给出向量中的最大绝对值。
范数指数越高,越关注大值而忽略小值。这就是为什么 RMSE 比 MAE 对异常值更敏感的原因。但是当异常值呈指数级罕见时(如钟形曲线),RMSE 表现非常好,通常是首选。
最后,它列出和验证迄今为止(由您或其他人)做出的假设是一种很好的做法;这可以帮助您及早发现严重的问题。例如,您的系统输出的地区价格将被输入下游机器学习系统,并且您假设这些价格将按原样使用。但是,如果下游系统将价格转换为类别(例如,“便宜”、“中等”或“昂贵”)然后使用这些类别而不是价格本身呢?在这种情况下,让价格完全正确并不重要。您的系统只需要获得正确的类别。如果是这样,那么问题应该被定义为分类任务,而不是回归任务。在使用回归系统数月后,您不想发现这一点。
幸运的是,在与负责下游系统的团队交谈后,您确信他们确实需要实际价格,而不仅仅是类别。伟大的!一切就绪,绿灯亮起,现在可以开始编码了!
它是是时候弄脏你的手了。不要犹豫,拿起您的笔记本电脑并在 Jupyter 笔记本中浏览以下代码示例。完整的 Jupyter 笔记本可在GitHub - ageron/handson-ml2: A series of Jupyter notebooks that walk you through the fundamentals of Machine Learning and Deep Learning in Python using Scikit-Learn, Keras and TensorFlow 2.获得。
第一的您将需要安装 Python。它可能已经安装在您的系统上。如果没有,您可以在Welcome to Python.org获得它。5
接下来,您需要为机器学习代码和数据集创建一个工作区目录。打开终端并键入以下命令(在$
提示之后)
- $ export ML_PATH="$HOME/ml" # 如果您愿意,可以更改路径
- $ mkdir -p $ML_PATH
你将需要许多 Python 模块:Jupyter、NumPy、pandas、Matplotlib 和 Scikit-Learn。如果你已经安装了所有这些模块运行 Jupyter,你可以安全地跳到“下载数据”。如果您还没有它们,有很多方法可以安装它们(及其依赖项)。您可以使用系统的打包系统(例如,Ubuntu 上的 apt-get,或 macOS 上的 MacPorts 或 Homebrew),安装诸如 Anaconda 之类的 Scientific Python 发行版并使用它的打包系统,或者只使用 Python 自己的打包系统 pip,它是默认情况下包含在 Python 二进制安装程序中(从 Python 2.7.9 开始)。6您可以通过键入以下命令来检查是否安装了 pip:
- $ python3 -m pip --version
- pip 19.3.1 from [...]/lib/python3.7/site-packages/pip (python 3.7)
您应该确保安装了最新版本的 pip。要升级 pip 模块,请键入以下内容(确切版本可能不同):7
- $ python3 -m pip install --user -U pip
- Collecting pip
- [...]
- Successfully installed pip-19.3.1
创建隔离环境
如果如果你想在一个隔离的环境中工作(强烈推荐这样你可以在不同的项目上工作而不会有冲突的库版本),通过运行以下 pip 命令安装 virtualenv 8(同样,如果你希望为所有人安装 virtualenv您机器上的用户,删除--user
并以管理员权限运行此命令):
- $ python3 -m pip install --user -U virtualenv
- Collecting virtualenv
- [...]
- Successfully installed virtualenv-16.7.6
现在您可以通过键入以下内容来创建一个隔离的 Python 环境:
- $ cd $ML_PATH
- $ python3 -m virtualenv my_env
- Using base prefix '[...]'
- New python executable in [...]/ml/my_env/bin/python3
- Also creating executable in [...]/ml/my_env/bin/python
- Installing setuptools, pip, wheel...done.
现在每次你想激活这个环境时,只需打开一个终端并输入以下内容:
- $ cd $ML_PATH
- $ source my_env/bin/activate # on Linux or macOS
- $ .\my_env\Scripts\activate # on Windows
要停用此环境,请键入deactivate
。当环境处于活动状态时,您使用 pip 安装的任何包都将安装在这个隔离的环境中,Python 将只能访问这些包(如果您还想访问系统的包,您应该使用 virtualenv 的--system-site-packages
选项创建环境)。查看 virtualenv 的文档以获取更多信息。
现在您可以使用这个简单的 pip 命令安装所有必需的模块及其依赖项(如果您不使用 virtualenv,则需要--user
选项或管理员权限):
- $ python3 -m pip install -U jupyter matplotlib numpy pandas scipy scikit-learn
- Collecting jupyter
- Downloading https://[...]/jupyter-1.0.0-py2.py3-none-any.whl
- Collecting matplotlib
- [...]
如果您创建了 virtualenv,则需要将其注册到 Jupyter 并为其命名:
$ python3 -m ipykernel install --user --name=python3
现在您可以通过键入以下命令来启动 Jupyter:
- $ jupyter notebook
- [...] Serving notebooks from local directory: [...]/ml
- [...] The Jupyter Notebook is running at:
- [...] http://localhost:8888/?token=60995e108e44ac8d8865a[...]
- [...] or http://127.0.0.1:8889/?token=60995e108e44ac8d8865a[...]
- [...] Use Control-C to stop this server and shut down all kernels [...]
Jupyter 服务器现在在您的终端中运行,监听端口 8888。您可以通过打开 Web 浏览器访问该服务器访问http://localhost:8888/(这通常在服务器启动时自动发生)。您应该会看到您的空工作区目录(如果您按照前面的 virtualenv 说明操作,则仅包含env目录)。
现在通过单击 New 按钮并选择适当的 Python 版本9创建一个新的 Python 笔记本(参见图 2-3)。这样做将在您的工作区中创建一个名为Untitled.ipynb的新笔记本文件,启动 Jupyter Python 内核来运行该笔记本,并在新选项卡中打开此笔记本。您应该首先将此笔记本重命名为“Housing”(这将自动将文件重命名为Housing.ipynb),方法是单击 Untitled 并输入新名称。
图 2-3。Jupyter 中的工作区
一个笔记本包含一个单元列表。每个单元格都可以包含可执行代码或格式化文本。现在笔记本只包含一个空的代码单元,标记为“In [1]:”。尝试print("Hello world!")
在单元格中输入内容并单击播放按钮(参见图 2-4)或按 Shift-Enter。这会将当前单元格发送到此笔记本的 Python 内核,后者将运行它并返回输出。结果显示在单元格下方,由于您已到达笔记本的末尾,因此会自动创建一个新单元格。通过 Jupyter 帮助菜单中的用户界面导览了解基础知识。
图 2-4。你好世界 Python 笔记本
在典型环境您的数据将在关系数据库(或其他一些常见数据存储)中可用,并分布在多个表/文档/文件中。要访问它,您首先需要获取您的凭据和访问权限10并熟悉数据模式。然而,在这个项目中,事情要简单得多:您只需下载一个压缩文件Housing.tgz,其中包含一个名为Housing.csv的逗号分隔值 (CSV) 文件,其中包含所有数据。
您可以使用 Web 浏览器下载文件并运行tar xzf 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):
- 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()
,它会在您的工作空间中创建一个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)
此函数返回一个包含所有数据的 pandas DataFrame 对象。
让我们看看使用DataFrame的head()
方法的前五行(见图2-5)。
图 2-5。数据集中的前五行
每行代表一个区。共有 10 个属性(您可以在屏幕截图中看到前 6 个):longitude
、latitude
、housing_median_age
、total_rooms
、total_bedrooms
、population
、households
、median_income
、median_house_value
和ocean_proximity
.
该info()
方法对于获取数据的快速描述很有用,特别是总行数、每个属性的类型和非空值的数量(见图 2-6)。
图 2-6。住房信息
数据集中有 20,640 个实例,这意味着按照机器学习标准它相当小,但它非常适合入门。请注意,该total_bedrooms
属性只有 20,433 个非空值,这意味着 207 个地区缺少此功能。我们稍后需要处理这个问题。
除ocean_proximity
字段外,所有属性都是数字的。它的类型是object
,所以它可以保存任何类型的 Python 对象。但是由于您从 CSV 文件加载了这些数据,因此您知道它必须是文本属性。当您查看前五行时,您可能注意到ocean_proximity
列中的值是重复的,这意味着它可能是一个分类属性。您可以使用以下方法找出存在哪些类别以及每个类别有多少个地区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()
方法显示了数字属性的摘要(图 2-7)。
图 2-7。每个数字属性的摘要
、count
、mean
和min
行max
是不言自明的。请注意,空值被忽略(例如,count
oftotal_bedrooms
是 20,433,而不是 20,640)。该std
行显示标准偏差,它衡量值的分散程度。12 25%、50% 和 75% 行显示相应的百分位数:百分位数表示一组观测值中给定百分比的观测值低于该值。例如,25% 的地区housing_median_age
低于 18,而 50% 低于 29,75% 低于 37。这些通常称为第 25 个百分位数(或第一个四分位数)、中位数和第 75 个百分位数(或第三四分位数).
另一种快速了解您正在处理的数据类型的方法是为每个数值属性绘制直方图。直方图显示具有给定值范围(在水平轴上)的实例数(在垂直轴上)。您可以一次绘制一个属性,也可以hist()
在整个数据集上调用该方法(如以下代码示例所示),它将为每个数值属性绘制一个直方图(见图 2-8):
- %matplotlib inline # only in a Jupyter notebook
- import matplotlib.pyplot as plt
- housing.hist(bins=50, figsize=(20,15))
- plt.show()
笔记
hist()
方法_依赖于 Matplotlib,而后者又依赖于用户指定的图形后端在您的屏幕上绘图。因此,在您绘制任何东西之前,您需要指定 Matplotlib 应该使用哪个后端。最简单的选择是使用 Jupyter 的魔法命令%matplotlib inline
。这告诉 Jupyter 设置 Matplotlib,以便它使用 Jupyter 自己的后端。然后在笔记本本身内渲染绘图。请注意,show()
在 Jupyter 笔记本中调用是可选的,因为 Jupyter 将在执行单元格时自动显示绘图。
图 2-8。每个数值属性的直方图
在这些直方图中,您可能会注意到一些事情:
首先,收入中位数属性看起来不像以美元 (USD) 表示。在与收集数据的团队核对后,您被告知,对于较高的收入中位数,数据已被缩放并上限为 15(实际上是 15.0001),而对于较低的收入中位数,数据已被缩放并上限为 0.5(实际上是 0.4999)。这些数字代表大约数万美元(例如,3 实际上意味着大约 30,000 美元)。使用预处理属性在机器学习中很常见,这不一定是问题,但您应该尝试了解数据是如何计算的。
房屋年龄中位数和房屋价值中位数也有上限。后者可能是一个严重的问题,因为它是您的目标属性(您的标签)。您的机器学习算法可能会了解到价格永远不会超过该限制。您需要与您的客户团队(将使用您的系统输出的团队)核实这是否是一个问题。如果他们告诉你他们需要超过 500,000 美元的精确预测,那么你有两种选择:
为标签被封顶的地区收集适当的标签。
从训练集中删除这些地区(也从测试集中删除,因为如果您的系统预测值超过 500,000 美元,则不应对其进行糟糕的评估)。
这些属性具有非常不同的尺度。当我们探索特征缩放时,我们将在本章后面讨论这个问题。
最后,许多直方图是重尾的:它们在中位数右侧比向左延伸得更远。这可能会使某些机器学习算法更难检测模式。稍后我们将尝试转换这些属性以获得更多的钟形分布。
希望您现在对您正在处理的数据类型有更好的理解。
警告
等待!在进一步查看数据之前,您需要创建一个测试集,将其放在一边,永远不要查看它。
它在这个阶段自愿搁置部分数据可能听起来很奇怪。毕竟,您只是快速浏览了数据,在决定使用什么算法之前,您当然应该了解更多关于它的知识,对吧?这是真的,但是你的大脑是一个惊人的模式检测系统,这意味着它很容易过度拟合:如果你查看测试集,你可能会在测试数据中偶然发现一些看似有趣的模式,这些模式会引导你选择一个特定类型的机器学习模型。当您使用测试集估计泛化误差时,您的估计会过于乐观,您将启动一个性能不如预期的系统。这称为数据窥探偏差。
创建测试集理论上很简单:随机选择一些实例,通常是数据集的 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]
然后你可以像这样使用这个函数:13
- >>> train_set, test_set = split_train_test(housing, 0.2)
- >>> len(train_set)
- 16512
- >>> len(test_set)
- 4128
好吧,这可行,但并不完美:如果再次运行该程序,它将生成不同的测试集!随着时间的推移,您(或您的机器学习算法)将看到整个数据集,这是您想要避免的。
一种解决方案是在第一次运行时保存测试集,然后在后续运行中加载它。另一种选择是在调用之前将随机数生成器的种子(例如,使用np.random.seed(42)
)设置为14np.random.permutation()
,以便它始终生成相同的混洗索引。
但是,当您下次获取更新的数据集时,这两种解决方案都会中断。即使在更新数据集之后也有稳定的训练/测试拆分,一个常见的解决方案是使用每个实例的标识符来决定它是否应该进入测试集(假设实例具有唯一且不可变的标识符)。例如,您可以计算每个实例标识符的哈希值,如果哈希值小于或等于最大哈希值的 20%,则将该实例放入测试集中。这可确保测试集在多次运行中保持一致,即使您刷新数据集也是如此。新的测试集将包含 20% 的新实例,但不会包含之前在训练集中的任何实例。
这是一个可能的实现:
- from zlib import crc32
-
- def test_set_check(identifier, test_ratio):
- return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32
-
- def split_train_test_by_id(data, test_ratio, id_column):
- ids = data[id_column]
- in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
- 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,像这样:15
- 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-学习提供了一些函数以各种方式将数据集拆分为多个子集。最简单的函数是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-8):大多数收入中值集中在 1.5 到 6 之间(即 15,000 美元到 60,000 美元),但一些收入中值远远超过 6。对于每个阶层,数据集中有足够数量的实例非常重要,否则估计一个阶层的重要性可能会有偏差。这意味着你不应该有太多的层,每个层都应该足够大。以下代码使用该pd.cut()
函数创建一个收入类别属性,该属性具有五个类别(标记为 1 到 5):类别 1 的范围从 0 到 1.5(即,小于 15,000 美元),类别 2 的范围从 1.5 到 3,依此类推:
- 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])
这些收入类别如图 2-9 所示:
housing["income_cat"].hist()
图 2-9。收入类别直方图
现在您已准备好根据收入类别进行分层抽样。为此,您可以使用 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_test_set["income_cat"].value_counts() / len(strat_test_set)
- 3 0.350533
- 2 0.318798
- 4 0.176357
- 5 0.114583
- 1 0.039729
- Name: income_cat, dtype: float64
使用类似的代码,您可以测量完整数据集中的收入类别比例。图 2-10比较了整个数据集、分层抽样生成的测试集中和纯随机抽样生成的测试集中的收入类别比例。如您所见,使用分层抽样生成的测试集的收入类别比例几乎与完整数据集中的相同,而使用纯随机抽样生成的测试集是倾斜的。
图 2-10。分层抽样与纯随机抽样的抽样偏差比较
现在您应该删除该income_cat
属性,以便数据恢复到其原始状态:
- for set_ in (strat_train_set, strat_test_set):
- set_.drop("income_cat", axis=1, inplace=True)
我们在测试集生成上花费了大量时间是有充分理由的:这是机器学习项目中经常被忽视但至关重要的部分。此外,当我们讨论交叉验证时,其中许多想法将很有用。现在是时候进入下一个阶段了:探索数据。
所以到目前为止,您只是快速浏览了数据,以大致了解您正在处理的数据类型。现在的目标是更深入一点。
首先,确保您已将测试集放在一边,并且您只是在探索训练集。此外,如果训练集非常大,您可能需要对探索集进行采样,以便轻松快速地进行操作。在我们的例子中,这个集合非常小,所以你可以直接在整个集合上工作。让我们创建一个副本,以便您可以在不损害训练集的情况下使用它:
housing = strat_train_set.copy()
自从有地理信息(纬度和经度),最好创建一个所有地区的散点图来可视化数据(图2-11):
housing.plot(kind="scatter", x="longitude", y="latitude")
图 2-11。数据的地理散点图
这看起来像加利福尼亚,但除此之外很难看到任何特定的模式。将alpha
选项设置为0.1
可以更轻松地可视化数据点密度高的地方(图 2-12):
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
图 2-12。突出高密度区域的更好的可视化
现在好多了:您可以清楚地看到高密度区域,即湾区以及洛杉矶和圣地亚哥周围,以及中央山谷的一条相当高密度的长线,特别是萨克拉门托和弗雷斯诺周围。
我们的大脑非常擅长发现图片中的模式,但您可能需要使用可视化参数才能使模式脱颖而出。
现在让我们看看房价(图2-13)。每个圆圈的半径代表该地区的人口(选项s
),颜色代表价格(选项c
)。我们将使用一个名为 的预定义颜色图(选项cmap
)jet
,范围从蓝色(低值)到红色(高价):
- 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()
图 2-13。加州房价:红色贵,蓝色便宜,大圆圈表示人口较多的地区
这张图片告诉您,房价与位置(例如,靠近海洋)和人口密度密切相关,您可能已经知道了。聚类算法对于检测主聚类和添加测量与聚类中心的接近程度的新特征应该是有用的。海洋邻近属性也可能有用,虽然在北加州沿海地区的房价不是太高,所以这不是一个简单的规则。
自从数据集不是太大,您可以使用以下方法轻松计算每对属性之间的标准相关系数(也称为Pearson's rcorr()
) :
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时,表示存在强负相关;您可以看到纬度和房价中值之间存在小的负相关(即,当您向北走时,价格有轻微下降的趋势)。最后,接近 0 的系数意味着没有线性相关性。图 2-14显示了各种图表以及它们的水平轴和垂直轴之间的相关系数。
图 2-14。各种数据集的标准相关系数(来源:维基百科;公共领域图片)
警告
相关系数仅衡量线性相关性(“如果x上升,则y通常会上升/下降”)。它可能会完全忽略非线性关系(例如,“如果x接近 0,那么y通常会上升”)。请注意底行的所有图的相关系数如何等于 0,尽管它们的轴显然不是独立的:这些是非线性关系的示例。此外,第二行显示了相关系数等于 1 或 –1 的示例;请注意,这与斜率无关。例如,以英寸为单位的身高与以英尺或纳米为单位的身高的相关系数为 1。
检查属性之间相关性的另一种方法是使用 pandasscatter_matrix()
函数,该函数将每个数值属性与其他所有数值属性进行对比。由于现在有 11 个数字属性,因此您将获得 11 2 = 121 个图,这将无法放在一个页面上——所以让我们只关注一些看起来与房价中值最相关的有希望的属性(图 2-15):
- from pandas.plotting import scatter_matrix
-
- attributes = ["median_house_value", "median_income", "total_rooms",
- "housing_median_age"]
- scatter_matrix(housing[attributes], figsize=(12, 8))
图 2-15。此散点矩阵绘制每个数字属性与其他所有数字属性,以及每个数字属性的直方图
如果 pandas 将每个变量与自身进行对比,则主对角线(左上角到右下角)将充满直线,这不会很有用。因此,pandas 会显示每个属性的直方图(其他选项可用;有关更多详细信息,请参阅 pandas 文档)。
预测房价中位数最有希望的属性是收入中位数,所以让我们放大它们的相关散点图(图 2-16):
- housing.plot(kind="scatter", x="median_income", y="median_house_value",
- alpha=0.1)
图 2-16。中位收入与中位房价
这个情节揭示了一些事情。第一,相关性确实很强;可以清楚的看到上升趋势,点也不是太分散。其次,我们之前注意到的价格上限清楚地显示为 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.687160
- rooms_per_household 0.146285
- total_rooms 0.135097
- housing_median_age 0.114110
- households 0.064506
- total_bedrooms 0.047689
- population_per_household -0.021985
- population -0.026920
- longitude -0.047432
- latitude -0.142724
- bedrooms_per_room -0.259984
- Name: median_house_value, dtype: float64
嘿,还不错!bedrooms_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()
操作fillna()
:
- housing.dropna(subset=["total_bedrooms"]) # option 1
- housing.drop("total_bedrooms", axis=1) # option 2
- median = housing["total_bedrooms"].median() # option 3
- housing["total_bedrooms"].fillna(median, inplace=True)
如果选择选项 3,则应计算训练集的中值并使用它来填充训练集中的缺失值。不要忘记保存您计算的中值。当您想要评估您的系统时,您将需要它来替换测试集中的缺失值,并且一旦系统上线以替换新数据中的缺失值。
Scikit-学习提供了一个方便的类来处理缺失值:SimpleImputer
. 以下是如何使用它。首先,您需要创建一个SimpleImputer
实例,指定您想用该属性的中值替换每个属性的缺失值:
- from sklearn.impute import SimpleImputer
-
- imputer = SimpleImputer(strategy="median")
由于中位数只能在数值属性上计算,因此您需要创建一个没有 text 属性的数据副本ocean_proximity
:
housing_num = housing.drop("ocean_proximity", axis=1)
现在您可以imputer
使用以下方法将实例拟合到训练数据中fit()
:
imputer.fit(housing_num)
已经简单地计算了每个属性的imputer
中值并将结果存储在其statistics_
实例变量中。只有total_bedrooms
属性有缺失值,但我们无法确定系统上线后新数据中不会有任何缺失值,因此将 应用于imputer
所有数值属性更安全:
>>> imputer.statistics_
array([ -118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
>>> housing_num.median().values
array([ -118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
现在您可以使用这个“训练过的”imputer
通过用学习的中位数替换缺失值来转换训练集:
X=imputer.transform(housing_num)
结果是一个简单的 NumPy 数组,其中包含转换后的特征。如果你想把它放回 pandas DataFrame,很简单:
- housing_tr = pd.DataFrame(X, columns=housing_num.columns,
- index=housing_num.index)
SCIKIT-LEARN 设计
Scikit-Learn 的 API设计得非常好。这些是主要的设计原则:17
一致性
所有对象共享一个一致且简单的接口:
估算器
任何可以基于数据集估计某些参数的对象称为估计器(例如,animputer
是估计器)。估计本身由该fit()
方法执行,它仅将一个数据集作为参数(或两个用于监督学习算法;第二个数据集包含标签)。指导估计过程所需的任何其他参数都被视为超参数(例如imputer
's strategy
),并且必须将其设置为实例变量(通常通过构造函数参数)。
变形金刚
一些估计器(例如imputer
)也可以转换数据集;这些被称为变压器。再一次,API 很简单:转换是由将要转换的数据集作为参数的方法执行的。transform()
它返回转换后的数据集。这种转换通常依赖于学习的参数,就像imputer
. 所有的转换器也有一个方便的方法调用fit_transform()
,它等同于调用fit()
then transform()
(但有时会fit_transform()
被优化并且运行得更快)。
预测器
最后,一些给定数据集的估计器能够做出预测;它们被称为预测器。例如,LinearRegression
前一章中的模型是一个预测器:给定一个国家的人均 GDP,它可以预测生活满意度。预测器有一个predict()
方法,它获取新实例的数据集并返回相应预测的数据集。它还有一种score()
方法可以测量预测的质量,给定一个测试集(以及相应的标签,在监督学习算法的情况下)。18
检查
所有估计器的超参数都可以通过公共实例变量(例如,imputer.strategy
)直接访问,并且所有估计器的学习参数都可以通过带有下划线后缀的公共实例变量(例如,imputer.statistics_
)访问。
不扩散类
数据集表示为 NumPy 数组或 SciPy 稀疏矩阵,而不是自制类。超参数只是普通的 Python 字符串或数字。
作品
现有的构建块被尽可能地重用。例如,很容易Pipeline
从任意序列的转换器创建一个估计器,然后是一个最终估计器,正如我们将看到的。
合理的默认值
Scikit-Learn 为大多数参数提供了合理的默认值,便于快速创建基线工作系统。
所以到目前为止,我们只处理了数字属性,但现在让我们看看文本属性。在这个数据集中,只有一个:ocean_proximity
属性。让我们看看前 10 个实例的值:
- >>> housing_cat = housing[["ocean_proximity"]]
- >>> housing_cat.head(10)
- ocean_proximity
- 17606 <1H OCEAN
- 18632 <1H OCEAN
- 14650 NEAR OCEAN
- 3230 INLAND
- 3555 <1H OCEAN
- 19480 INLAND
- 8879 <1H OCEAN
- 13685 INLAND
- 4937 <1H OCEAN
- 4861 <1H OCEAN
这不是任意文本:可能的值数量有限,每个值都代表一个类别。所以这个属性是一个分类属性。大多数机器学习算法更喜欢使用数字,所以让我们将这些类别从文本转换为数字。为此,我们可以使用 Scikit-Learn 的OrdinalEncoder
类:19
- >>> from sklearn.preprocessing import OrdinalEncoder
- >>> ordinal_encoder = OrdinalEncoder()
- >>> housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
- >>> housing_cat_encoded[:10]
- array([[0.],
- [0.],
- [4.],
- [1.],
- [0.],
- [1.],
- [0.],
- [1.],
- [0.],
- [0.]])
categories_
您可以使用实例变量获取类别列表。它是一个列表,其中包含每个类别属性的一维类别数组(在这种情况下,一个包含单个数组的列表,因为只有一个类别属性):
- >>> ordinal_encoder.categories_
- [array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
- dtype=object)]
这种表示的一个问题是 ML 算法将假设两个附近的值比两个遥远的值更相似。这在某些情况下可能很好(例如,对于“差”、“平均”、“好”和“优秀”等有序类别),但对于ocean_proximity
列显然不是这样(例如,类别 0 和4 显然比类别 0 和 1 更相似)。为了解决这个问题,一种常见的解决方案是为每个类别创建一个二元属性:当类别为“<1H OCEAN”时一个属性等于 1(否则为 0),当类别为“INLAND”时另一个属性等于 1(否则为 0),依此类推。这个称为one-hot encoding,因为只有一个属性将等于 1(热),而其他属性将等于 0(冷)。新属性有时被称为 虚拟属性。Scikit-学习提供一个OneHotEncoder
类来将分类值转换为单热向量:20
- >>> from sklearn.preprocessing import OneHotEncoder
- >>> cat_encoder = OneHotEncoder()
- >>> housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
- >>> housing_cat_1hot
- <16512x5 sparse matrix of type '
numpy.float64'>' - with 16512 stored elements in Compressed Sparse Row format>
注意输出是 SciPy稀疏矩阵,而不是 NumPy 数组。当您拥有包含数千个类别的类别属性时,这非常有用。经过 one-hot 编码后,我们得到了一个包含数千列的矩阵,该矩阵除了每行一个 1 外,其余都是 0。使用大量内存来存储零是非常浪费的,因此稀疏矩阵只存储非零元素的位置。您可以像使用普通的 2D 数组一样使用它,21但是如果您真的想将其转换为(密集)NumPy 数组,只需调用该toarray()
方法:
- >>> housing_cat_1hot.toarray()
- array([[1., 0., 0., 0., 0.],
- [1., 0., 0., 0., 0.],
- [0., 0., 0., 0., 1.],
- ...,
- [0., 1., 0., 0., 0.],
- [1., 0., 0., 0., 0.],
- [0., 0., 0., 1., 0.]])
再一次,您可以使用编码器的categories_
实例变量获取类别列表:
- >>> cat_encoder.categories_
- [array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
- dtype=object)]
尽管 Scikit-Learn 提供许多有用的转换器,您将需要为自定义清理操作或组合特定属性等任务编写自己的转换器。你将希望您的转换器与 Scikit-Learn 功能(例如管道)无缝协作,并且由于 Scikit-Learn 依赖于鸭子类型(不是继承),您需要做的就是创建一个类并实现三个方法:(fit()
返回self
)transform()
,和fit_transform()
。
TransformerMixin
您只需添加为基类即可免费获得最后一个。如果您添加BaseEstimator
为基类(并在构造函数中避免*args
使用 and **kargs
),您还将获得两个额外的方法(get_params()
和set_params()
),这对于自动超参数调整很有用。
例如,这是一个小型转换器类,它添加了我们之前讨论过的组合属性:
- from sklearn.base import BaseEstimator, TransformerMixin
-
- rooms_ix, bedrooms_ix, population_ix, households_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):
- rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
- population_per_household = X[:, population_ix] / X[:, households_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)
在这个例子中,transformer 有一个超参数,默认add_bedrooms_per_room
设置为True
(提供合理的默认值通常很有帮助)。此超参数将让您轻松找出添加此属性是否有助于机器学习算法。更一般地说,您可以添加一个超参数来控制您不能 100% 确定的任何数据准备步骤。您对这些数据准备步骤的自动化程度越高,您可以自动尝试的组合就越多,从而更有可能找到一个很好的组合(并为您节省大量时间)。
一您需要对数据应用的最重要的转换是特征缩放。除了少数例外,当输入数值属性具有非常不同的尺度时,机器学习算法表现不佳。住房数据就是这种情况:房间总数大约在 6 到 39,320 之间,而收入中位数仅在 0 到 15 之间。请注意,通常不需要缩放目标值。
那里是使所有属性具有相同比例的两种常用方法:最小-最大缩放和标准化。
最小-最大缩放(许多人称之为规范化)是最简单的:值被移位和重新缩放,以便它们最终在 0 到 1 的范围内。我们通过减去最小值并除以最大值减去最小值来做到这一点。Scikit-LearnMinMaxScaler
为此提供了一个转换器。它有一个feature_range
超参数,如果由于某种原因您不想要 0-1,您可以更改范围。
标准化是不同的:首先它减去平均值(因此标准化值的平均值总是为零),然后除以标准差,这样得到的分布就有单位方差。与最小-最大缩放不同,标准化不会将值限制在特定范围内,这对于某些算法可能是个问题(例如,神经网络通常期望输入值范围为 0 到 1)。然而,标准化受异常值的影响要小得多。例如,假设一个地区的收入中位数等于 100(错误地)。然后,最小-最大缩放会将所有其他值从 0-15 压低到 0-0.15,而标准化不会受到太大影响。Scikit-Learn 提供了一个称为StandardScaler
标准化的转换器。
警告
与所有转换一样,重要的是仅将缩放器拟合到训练数据,而不是完整数据集(包括测试集)。只有这样,您才能使用它们来转换训练集和测试集(以及新数据)。
作为您可以看到,有许多数据转换步骤需要以正确的顺序执行。幸运的是,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)
构造Pipeline
函数采用定义一系列步骤的名称/估计器对列表。除了最后一个估计器之外的所有估计器都必须是转换器(即,它们必须有一个fit_transform()
方法)。名称可以是您喜欢的任何名称(只要它们是唯一的并且不包含双下划线,__
);稍后它们将在超参数调整中派上用场。
当您调用管道的fit()
方法时,它会fit_transform()
按顺序调用所有转换器,将每个调用的输出作为参数传递给下一个调用,直到它到达最终的估计器,并为此调用fit()
方法。
管道公开了与最终估计器相同的方法。在这个例子中,最后一个估计器是 a StandardScaler
,它是一个转换器,因此管道有一个transform()
方法可以按顺序将所有转换应用于数据(当然还有一个fit_transform()
方法,这是我们使用的方法)。
到目前为止,我们已经分别处理了分类列和数值列。让单个转换器能够处理所有列,对每一列应用适当的转换会更方便。在 0.20 版本中,Scikit-LearnColumnTransformer
为此引入了 ,好消息是它与 pandas DataFrames 配合得很好。让我们用它来对住房数据进行所有转换:
- from sklearn.compose import ColumnTransformer
-
- num_attribs = list(housing_num)
- cat_attribs = ["ocean_proximity"]
-
- full_pipeline = ColumnTransformer([
- ("num", num_pipeline, num_attribs),
- ("cat", OneHotEncoder(), cat_attribs),
- ])
-
- housing_prepared = full_pipeline.fit_transform(housing)
首先我们导入ColumnTransformer
类,接下来我们得到数字列名列表和分类列名列表,然后我们构造一个ColumnTransformer
. 构造函数需要一个元组列表,其中每个元组包含一个名称、22个转换器以及应应用转换器的列的名称(或索引)列表。在这个例子中,我们指定数值列应该使用num_pipeline
我们之前定义的转换,分类列应该使用转换OneHotEncoder
。最后,我们将ColumnTransformer
其应用于房屋数据:它将每个转换器应用于适当的列,并沿第二个轴连接输出(转换器必须返回相同的行数)。
请注意,OneHotEncoder
返回一个稀疏矩阵,而num_pipeline
返回一个密集矩阵。当存在稀疏和密集矩阵的混合时,ColumnTransformer
估计最终矩阵的密度(即非零单元的比率),如果密度低于给定阈值(默认情况下,sparse_threshold=0.3
) ,则返回稀疏矩阵. 在此示例中,它返回一个密集矩阵。就是这样!我们有一个预处理管道,它获取完整的住房数据并将适当的转换应用于每一列。
小费
"drop"
如果要删除列,您可以指定字符串,而不是使用转换器,或者您可以指定"passthrough"
是否要保持列不变。默认情况下,其余列(即未列出的列)将被删除,但如果您希望以不同方式处理这些列,您可以remainder
将超参数设置为任何转换器(或)。"passthrough"
如果您使用的是 Scikit-Learn 0.19 或更早版本,您可以使用第三方库,例如sklearn-pandas
,或者您可以推出自己的自定义转换器以获得与ColumnTransformer
. 或者,您可以使用FeatureUnion
该类,它可以应用不同的转换器并连接它们的输出。但是您不能为每个转换器指定不同的列;它们都适用于整个数据。可以使用用于列选择的自定义转换器来解决此限制(有关示例,请参见 Jupyter 笔记本)。
最后!你构建问题,获取数据并进行探索,对训练集和测试集进行采样,编写转换管道以自动清理和准备机器学习算法的数据。您现在已准备好选择和训练机器学习模型。
好消息是,由于之前的所有这些步骤,现在事情将比您想象的要简单得多。让我们首先训练一个线性回归模型,就像我们在上一章中所做的那样:
- 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))
- Predictions: [ 210644.6045 317768.8069 210956.4333 59218.9888 189747.5584]
- >>> print("Labels:", list(some_labels))
- Labels: [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]
它有效,尽管预测并不完全准确(例如,第一个预测偏离了近 40%!)。让我们测量这个回归模型在整个训练集上的 RMSEScikit-Learn 的mean_squared_error()
功能:
- >>> 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.19819848922
这总比没有好,但显然不是一个很好的分数:大多数地区的median_housing_values
范围在 120,000 美元到 265,000 美元之间,因此 68,628 美元的典型预测误差并不是很令人满意。这是一个模型欠拟合训练数据的例子。发生这种情况时,可能意味着特征没有提供足够的信息来做出良好的预测,或者模型不够强大。正如我们在上一章中看到的,修复欠拟合的主要方法是选择更强大的模型,为训练算法提供更好的特征,或者减少对模型的约束。该模型未进行正则化,因此排除了最后一个选项。您可以尝试添加更多功能(例如,人口的日志),但首先让我们尝试一个更复杂的模型来看看它是如何做的。
让我们训练一个DecisionTreeRegressor
. 这是一个强大的模型,能够在数据中找到复杂的非线性关系(决策树在第 6 章中有更详细的介绍)。代码现在应该看起来很熟悉:
- 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)
- tree_rmse_scores = np.sqrt(-scores)
警告
Scikit-Learn 的交叉验证特性期望效用函数(越大越好)而不是成本函数(越低越好),所以评分函数实际上是 MSE 的反面(即负值),这就是为什么前面的代码
-scores
在计算平方根之前计算。
让我们看看结果:
- >>> def display_scores(scores):
- ... print("Scores:", scores)
- ... print("Mean:", scores.mean())
- ... print("Standard deviation:", scores.std())
- ...
- >>> display_scores(tree_rmse_scores)
- Scores: [70194.33680785 66855.16363941 72432.58244769 70758.73896782
- 71115.88230639 75585.14172901 70262.86139133 70273.6325285
- 75366.87952553 71231.65726027]
- Mean: 71407.68766037929
- Standard deviation: 2439.4345041191004
现在决策树看起来不像以前那么好。事实上,它的表现似乎比线性回归模型差!请注意,交叉验证不仅可以让您获得对模型性能的估计,还可以衡量该估计的精确程度(即其标准偏差)。决策树的得分约为 71,407,通常为 ±2,439。如果您只使用一个验证集,您将不会获得此信息。但是交叉验证是以多次训练模型为代价的,因此并不总是可行的。
让我们为线性回归模型计算相同的分数以确保:
- >>> 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: [66782.73843989 66960.118071 70347.95244419 74739.57052552
- 68031.13388938 71193.84183426 64969.63056405 68281.61137997
- 71552.91566558 67665.10082067]
- Mean: 69052.46136345083
- Standard deviation: 2731.674001798348
没错:决策树模型的过度拟合非常糟糕,以至于它的表现比线性回归模型更差。
现在让我们尝试最后一个模型:RandomForestRegressor
. 正如我们将在第 7 章中看到的那样,随机森林的工作原理是在特征的随机子集上训练许多决策树,然后平均它们的预测。建立一个在许多其他模型之上的模型称为集成学习,它通常是进一步推动 ML 算法的好方法。我们将跳过大部分代码,因为它与其他模型基本相同:
- >>> from sklearn.ensemble import RandomForestRegressor
- >>> forest_reg = RandomForestRegressor()
- >>> forest_reg.fit(housing_prepared, housing_labels)
- >>> [...]
- >>> forest_rmse
- 18603.515021376355
- >>> display_scores(forest_rmse_scores)
- Scores: [49519.80364233 47461.9115823 50029.02762854 52325.28068953
- 49308.39426421 53446.37892622 48634.8036574 47585.73832311
- 53490.10699751 50021.5852922 ]
- Mean: 50182.303100336096
- Standard deviation: 2097.0810550985693
哇,这好多了:随机森林看起来很有前途。但是,请注意,训练集上的分数仍然远低于验证集上的分数,这意味着模型仍然过拟合训练集。过度拟合的可能解决方案是简化模型、对其进行约束(即正则化)或获得更多的训练数据。然而,在深入研究随机森林之前,您应该尝试各种机器学习算法类别中的许多其他模型(例如,具有不同内核的多个支持向量机,可能还有一个神经网络),而无需花费太多时间来调整超参数。目标是选出一些(两到五个)有前途的模型。
小费
您应该保存您试验的每个模型,以便您可以轻松地返回到您想要的任何模型。确保保存超参数和训练参数,以及交叉验证分数,也许还有实际预测。这将使您能够轻松地比较不同模型类型的分数,并比较它们所犯的错误类型。你可以使用 Python 的
pickle
模块或使用joblib
库轻松保存 Scikit-Learn 模型,这更高效序列化大型 NumPy 数组(您可以使用 pip 安装此库):
- import joblib
-
- joblib.dump(my_model, "my_model.pkl")
- # and later...
- my_model_loaded = joblib.load("my_model.pkl")
让我们假设你现在有一个有前途的模型的候选名单。您现在需要对它们进行微调。让我们看一下可以做到这一点的几种方法。
一种选择是手动调整超参数,直到找到超参数值的完美组合。这将是一项非常乏味的工作,而且您可能没有时间探索多种组合。
反而,你应该让 Scikit-LearnGridSearchCV
来搜索你。您需要做的就是告诉它您希望它试验哪些超参数以及要尝试哪些值,它将使用交叉验证来评估超参数值的所有可能组合。例如,以下代码搜索 的超参数值的最佳组合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',
- return_train_score=True)
-
- grid_search.fit(housing_prepared, housing_labels)
小费
当您不知道超参数应该具有什么值时,一种简单的方法是尝试 10 的连续幂(如果您想要更细粒度的搜索,则可以尝试更小的数字,如本示例中的
n_estimators
超参数所示)。
这param_grid
告诉 Scikit-Learn 首先评估所有 3 × 4 = 12 个组合n_estimators
和max_features
第一个指定的超参数值dict
(现在不要担心这些超参数的含义;它们将在第 7 章中解释),然后尝试所有 2 × 3 = 6 次中的超参数值组合dict
,但这次bootstrap
将超参数设置为False
而不是True
(这是此超参数的默认值)。
网格搜索将探索 12 + 6 = 18 种RandomForestRegressor
超参数值的组合,并将每个模型训练 5 次(因为我们使用的是五折交叉验证)。换句话说,总而言之,将有 18 × 5 = 90 轮训练!这可能需要相当长的时间,但完成后,您可以获得如下参数的最佳组合:
- >>> grid_search.best_params_
- {'max_features': 8, 'n_estimators': 30}
小费
由于 8 和 30 是评估的最大值,您可能应该尝试使用更高的值再次搜索;分数可能会继续提高。
您还可以直接获得最佳估算器:
- >>> grid_search.best_estimator_
- RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
- max_features=8, max_leaf_nodes=None, min_impurity_decrease=0.0,
- min_impurity_split=None, min_samples_leaf=1,
- min_samples_split=2, min_weight_fraction_leaf=0.0,
- n_estimators=30, n_jobs=None, oob_score=False, random_state=None,
- verbose=0, warm_start=False)
笔记
如果GridSearchCV
使用 (这是默认值)进行初始化refit=True
,那么一旦使用交叉验证找到最佳估计器,它就会在整个训练集上重新训练它。这通常是个好主意,因为提供更多数据可能会提高其性能。
当然,评估分数也是可用的:
- >>> cvres = grid_search.cv_results_
- >>> for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
- ... print(np.sqrt(-mean_score), params)
- ...
- 63669.05791727153 {'max_features': 2, 'n_estimators': 3}
- 55627.16171305252 {'max_features': 2, 'n_estimators': 10}
- 53384.57867637289 {'max_features': 2, 'n_estimators': 30}
- 60965.99185930139 {'max_features': 4, 'n_estimators': 3}
- 52740.98248528835 {'max_features': 4, 'n_estimators': 10}
- 50377.344409590376 {'max_features': 4, 'n_estimators': 30}
- 58663.84733372485 {'max_features': 6, 'n_estimators': 3}
- 52006.15355973719 {'max_features': 6, 'n_estimators': 10}
- 50146.465964159885 {'max_features': 6, 'n_estimators': 30}
- 57869.25504027614 {'max_features': 8, 'n_estimators': 3}
- 51711.09443660957 {'max_features': 8, 'n_estimators': 10}
- 49682.25345942335 {'max_features': 8, 'n_estimators': 30}
- 62895.088889905004 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
- 54658.14484390074 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
- 59470.399594730654 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
- 52725.01091081235 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
- 57490.612956065226 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
- 51009.51445842374 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}
max_features
在此示例中,我们通过将超参数设置为8
并将n_estimators
超参数设置为 来获得最佳解决方案30
。此组合的 RMSE 得分为 49,682,略好于您之前使用默认超参数值(50,182)获得的得分。恭喜,您已成功微调您的最佳模型!
小费
不要忘记,您可以将一些数据准备步骤视为超参数。例如,网格搜索会自动找出是否添加您不确定的功能(例如,使用转换器的
add_bedrooms_per_room
超参数CombinedAttributesAdder
)。它可以类似地用于自动找到处理异常值、缺失特征、特征选择等的最佳方法。
当您探索相对较少的组合时,网格搜索方法很好,就像前面的例子一样,但是当超参数搜索空间很大时,通常最好RandomizedSearchCV
改用它。此类可以以与GridSearchCV
该类大致相同的方式使用,但它不是尝试所有可能的组合,而是通过在每次迭代时为每个超参数选择一个随机值来评估给定数量的随机组合。这种方法有两个主要好处:
如果您让随机搜索运行 1,000 次迭代,则此方法将为每个超参数探索 1,000 个不同的值(而不是使用网格搜索方法每个超参数仅几个值)。
只需设置迭代次数,您就可以更好地控制要分配给超参数搜索的计算预算。
微调系统的另一种方法是尝试组合性能最佳的模型。该组(或“集成”)通常会比最好的单个模型表现更好(就像随机森林比它们所依赖的单个决策树表现更好),特别是如果单个模型产生非常不同类型的错误。我们将在第 7 章更详细地讨论这个主题。
通过检查最佳模型,您通常会获得对问题的深刻见解。例如,RandomForestRegressor
可以指示每个属性对做出准确预测的相对重要性:
- >>> feature_importances = grid_search.best_estimator_.feature_importances_
- >>> feature_importances
- array([7.33442355e-02, 6.29090705e-02, 4.11437985e-02, 1.46726854e-02,
- 1.41064835e-02, 1.48742809e-02, 1.42575993e-02, 3.66158981e-01,
- 5.64191792e-02, 1.08792957e-01, 5.33510773e-02, 1.03114883e-02,
- 1.64780994e-01, 6.02803867e-05, 1.96041560e-03, 2.85647464e-03])
让我们在它们对应的属性名称旁边显示这些重要性分数:
- >>> 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)
- [(0.3661589806181342, 'median_income'),
- (0.1647809935615905, 'INLAND'),
- (0.10879295677551573, 'pop_per_hhold'),
- (0.07334423551601242, 'longitude'),
- (0.0629090704826203, 'latitude'),
- (0.05641917918195401, 'rooms_per_hhold'),
- (0.05335107734767581, 'bedrooms_per_room'),
- (0.041143798478729635, 'housing_median_age'),
- (0.014874280890402767, 'population'),
- (0.014672685420543237, 'total_rooms'),
- (0.014257599323407807, 'households'),
- (0.014106483453584102, 'total_bedrooms'),
- (0.010311488326303787, '<1H OCEAN'),
- (0.002856474637320158, 'NEAR OCEAN'),
- (0.00196041559947807, 'NEAR BAY'),
- (6.028038672736599e-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 47,730.2
在某些情况下,这样的泛化误差点估计不足以说服您启动:如果它仅比当前生产的模型好 0.1% 怎么办?您可能想知道这个估计的精确度。为此,您可以使用以下方法计算泛化错误的 95%置信区间scipy.stats.t.interval()
:
- >>> from scipy import stats
- >>> confidence = 0.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)))
- ...
- array([45685.10470776, 49691.25001878])
如果您进行了大量的超参数调整,性能通常会比您使用交叉验证测量的稍差(因为您的系统最终经过微调以在验证数据上表现良好,而在未知数据集上可能表现不佳)。在这个例子中不是这样,但是当这种情况发生时,你必须抵制调整超参数以使测试集上的数字看起来不错的诱惑;这些改进不太可能推广到新数据。
现在是项目启动前阶段:您需要展示您的解决方案(突出显示您学到的知识、有效的和无效的、做出的假设以及系统的局限性),记录所有内容,并创建带有清晰可视化的漂亮演示文稿和易于记忆的陈述(例如,“收入中位数是房价的第一预测指标”)。在这个加利福尼亚住房示例中,该系统的最终性能并不比专家估计的价格好,专家估计的价格通常会下降约 20%,但启动它可能仍然是一个好主意,特别是如果这可以腾出一些时间为专家们准备,以便他们可以从事更有趣和更有成效的任务。
完美,你获准发射!您现在需要为生产准备好您的解决方案(例如,润色代码、编写文档和测试,等等)。然后,您可以将模型部署到生产环境。一种方法是保存经过训练的 Scikit-Learn 模型(例如,使用joblib
),包括完整的预处理和预测管道,然后在您的生产环境中加载这个经过训练的模型,并通过调用它的predict()
方法来使用它来进行预测。例如,该模型可能会在网站中使用:用户将输入一些关于新区的数据,然后单击“估计价格”按钮。这会将包含数据的查询发送到 Web 服务器,该服务器会将其转发到您的 Web 应用程序,最后您的代码将简单地调用模型的predict()
方法(您希望在服务器启动时加载模型,而不是每次使用模型时)。或者,您可以将模型包装在一个专用的 Web 服务中,您的 Web 应用程序可以通过 REST API 23查询该服务(参见图 2-17)。这样可以更轻松地将模型升级到新版本,而不会中断主应用程序。它还简化了扩展,因为您可以根据需要启动任意数量的 Web 服务,并在这些 Web 服务之间平衡来自您的 Web 应用程序的请求。此外,它允许您的 Web 应用程序使用任何语言,而不仅仅是 Python。
图 2-17。部署为 Web 服务并由 Web 应用程序使用的模型
另一种流行的策略是将模型部署在云端,例如在 Google Cloud AI Platform(以前称为 Google Cloud ML Engine)上:只需使用保存模型joblib
并将其上传到 Google Cloud Storage (GCS),然后转到 Google Cloud AI Platform 并创建一个新的模型版本,将其指向 GCS 文件。而已!这为您提供了一个简单的 Web 服务,可以为您处理负载平衡和扩展。它接受包含输入数据(例如,一个地区)的 JSON 请求,并返回包含预测的 JSON 响应。然后,您可以在您的网站(或您使用的任何生产环境)中使用此 Web 服务。正如我们将在第 19 章中看到的,部署AI Platform 上的 TensorFlow 模型与部署 Scikit-Learn 模型没有太大区别。
但部署并不是故事的结局。您还需要编写监控代码以定期检查系统的实时性能,并在其下降时触发警报。这可能是一个急剧下降,可能是由于您的基础设施中的组件损坏,但请注意,它也可能是一个温和的衰减,很容易在很长一段时间内被忽视。这很常见,因为模型会随着时间的推移而“腐烂”:事实上,世界在变化,所以如果模型是用去年的数据训练的,它可能无法适应今天的数据。
警告
即使是经过训练对猫狗图片进行分类的模型也可能需要定期重新训练,这不是因为猫狗会在一夜之间发生变异,而是因为相机不断变化,图像格式、清晰度、亮度和尺寸比例也在不断变化。此外,明年人们可能会喜欢不同的品种,或者他们可能决定给他们的宠物戴上小帽子——谁知道呢?
因此,您需要监控模型的实时性能。但你是怎么做到的?这得看情况。在某些情况下,可以从下游指标推断模型的性能。例如,如果您的模型是推荐系统的一部分,并且它会推荐用户可能感兴趣的产品,那么很容易监控每天销售的推荐产品数量。如果这个数字下降(与不推荐的产品相比),那么主要的嫌疑人就是模型。这可能是因为数据管道被破坏了,或者模型需要根据新数据重新训练(我们将在稍后讨论)。
但是,在没有任何人工分析的情况下,并不总是可以确定模型的性能。例如,假设您训练了一个图像分类模型(参见第 3 章)来检测生产线上的多个产品缺陷。如果模型的性能下降,在成千上万的缺陷产品被运送给您的客户之前,您如何获得警报?一种解决方案是向人类评估者发送模型分类的所有图片的样本(尤其是模型不太确定的图片)。根据任务的不同,评估者可能需要是专家,也可能是非专家,例如众包平台(例如 Amazon Mechanical Turk)上的工作人员。在某些应用程序中,他们甚至可以是用户自己,例如通过调查或重新调整用途的验证码进行响应。24
无论哪种方式,您都需要建立一个监控系统(有或没有人工评估员来评估实时模型),以及所有相关流程来定义在发生故障时应该做什么以及如何为它们做准备。不幸的是,这可能需要做很多工作。事实上,这通常比构建和训练模型要多得多。
如果数据不断发展,您将需要定期更新数据集并重新训练您的模型。您应该尽可能地自动化整个过程。以下是您可以自动化的一些事情:
定期收集新数据并对其进行标记(例如,使用人工评估者)。
编写脚本来训练模型并自动微调超参数。此脚本可以自动运行,例如每天或每周,具体取决于您的需要。
编写另一个脚本,在更新的测试集上评估新模型和以前的模型,如果性能没有下降,则将模型部署到生产环境(如果下降,请确保调查原因)。
您还应该确保评估模型的输入数据质量。有时,由于信号质量不佳(例如,发送随机值的故障传感器,或其他团队的输出变得陈旧),性能会略有下降,但您的系统性能下降到足以触发警报可能需要一段时间。如果您监控模型的输入,您可能会更早发现这一点。例如,如果越来越多的输入缺少某个特征,或者其均值或标准差偏离训练集太远,或者分类特征开始包含新类别,您可以触发警报。
最后,确保您保留您创建的每个模型的备份,并准备好流程和工具以快速回滚到以前的模型,以防新模型由于某种原因开始严重失败。拥有备份还可以轻松地将新模型与以前的模型进行比较。同样,您应该保留数据集的每个版本的备份,以便在新数据集损坏时可以回滚到以前的数据集(例如,如果添加到其中的新数据结果充满异常值)。备份数据集还允许您根据任何先前的数据集评估任何模型。
小费
您可能想要创建测试集的几个子集,以评估您的模型在数据的特定部分上的执行情况。例如,您可能希望有一个仅包含最新数据的子集,或用于特定类型输入的测试集(例如,位于内陆的地区与位于海洋附近的地区)。这将使您更深入地了解模型的优势和劣势。
如您所见,机器学习涉及大量基础设施,因此如果您的第一个 ML 项目需要花费大量精力和时间来构建和部署到生产环境,请不要感到惊讶。幸运的是,一旦所有基础设施都到位,从构思到生产的速度会快得多。
希望本章能让您对机器学习项目的外观有一个很好的了解,并向您展示一些可用于训练出色系统的工具。如您所见,大部分工作都在数据准备步骤中:构建监控工具、设置人工评估管道以及自动化定期模型训练。机器学习算法当然很重要,但最好熟悉整个过程并熟悉三四种算法,而不是把所有时间都花在探索高级算法上。
所以,如果你还没有这样做,现在是拿起笔记本电脑的好时机,选择你感兴趣的数据集,并尝试从头到尾完成整个过程。一个很好的起点是一个竞赛网站,例如Kaggle: Your Machine Learning and Data Science Community:你将有一个数据集可以玩,一个明确的目标,以及可以分享经验的人。玩得开心!