Skip to content

Latest commit

 

History

History
392 lines (262 loc) · 23.5 KB

2.端到端的机器学习项目.md

File metadata and controls

392 lines (262 loc) · 23.5 KB

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

本章中,你会假装作为被一家地产公司刚刚雇佣的数据科学家,完整地学习一个案例项目。下面是主要步骤:

  1. 项目概述。
  2. 获取数据。
  3. 发现并可视化数据,发现规律。
  4. 为机器学习算法准备数据。
  5. 选择模型,进行训练。
  6. 微调模型。
  7. 给出解决方案。
  8. 部署、监控、维护系统。

使用真实数据

学习机器学习时,最好使用真实数据,而不是人工数据集。幸运的是,有上千个开源数据集可以进行选择,涵盖多个领域。以下是一些可以查找的数据的地方:

本章,我们选择的是StatLib的加州房产价格数据集(见图2-1)。这个数据集是基于1990年加州普查的数据。数据已经有点老(1990年还能买一个湾区不错的房子),但是它有许多优点,利于学习,所以假设这个数据为最近的。为了便于教学,我们添加了一个分类属性,并除去了一些。

图2-1 加州房产价格

项目概览

欢迎来到机器学习房地产公司!你的第一个任务是利用加州普查数据,建立一个加州房价模型。这个数据包含每个分区组的人口、收入中位数、房价中位数等指标。

分区组是美国调查局发布样本数据的最小地理单位(一个分区通常有600到3000人)。我们将其简称为“分区”。

你的模型要利用这个数据进行学习,然后根据其它指标,预测任何分区的的房价中位数。

提示:你是一个有条理的数据科学家,你要做的第一件事是拿出你的机器学习项目清单。你可以使用附录B中的清单;这个清单适用于大多数的机器学习项目,但是你还是要确认它是否满足需求。在本章中,我们会检查许多清单上的项目,但是也会跳过一些简单的,有些会在后面的章节再讨论。

划定问题

问老板的第一个问题应该是商业目标是什么?建立模型可能不是最终目标。公司要如何使用、并从模型受益?这非常重要,因为它决定了如何划定问题,要选择什么算法,评估模型性能的指标是什么,要花多少精力进行微调。

老板告诉你你的模型的输出(一个区的房价中位数)会传给另一个机器学习系统(见图2-2),也有其它信号会传入后面的系统。这一整套系统可以确定某个区进行投资值不值。确定值不值得投资非常重要,它直接影响利润。

图2-2 房地产投资的机器学习管道

管道
一系列的数据处理组件被称为数据管道。管道在机器学习系统中很常见,因为有许多数据要处理和转换。

组件通常是异步运行的。每个组件吸纳进大量数据,进行处理,然后将数据传输到另一个数据容器中,而后管道中的另一个组件收入这个数据,然后输出,这个过程依次进行下去。每个组件都是独立的:组件间的接口只是数据容器。这样可以让系统更便于理解(记住数据流的图),不同的项目组可以关注于不同的组件。进而,如果一个组件失效了,下游的组件使用失效组件最后生产的数据,通常可以正常运行(一段时间)。这样就使整个架构相当健壮。

另一方面,如果没有监控,失效的组件会在不被注意的情况下运行一段时间。数据会受到污染,整个系统的性能就会下降。

下一个要问的问题是,现在的解决方案效果如何。老板通常会给一个参考性能,以及如何解决问题。老板说,现在分区的房价是靠专家手工估计的,专家队伍收集最新的关于一个区的信息(不包括房价中位数),他们使用复杂的规则进行估计。这种方法费钱费时间,而且估计结果不理想,误差率大概有15%。

Okay,有了这些信息,你就可以开始设计系统了。首先,你需要划定问题:监督或非监督,还是强化学习?这是个分类任务、回归任务,还是其它的?要使用批量学习还是线上学习?继续阅读之前,请暂停一下,尝试自己回答下这些问题。

你能回答出来吗?一起看下答案:很明显,这是一个典型的监督学习任务,因为你要使用的是有标签的训练样本(每个实例都有预定的产出,即分区的房价中位数)。并且,这是一个典型的回归任务,因为你要预测一个值。讲的更细些,这是一个多变量回归问题,因为系统要使用多个变量进行预测(要使用分区的人口,收入中位数等等)。在第一章中,你只是根据人均GDP来预测生活满意度,因此这是一个单变量回归问题。最后,没有连续的数据流进入系统,没有特别需求需要对数据变动作出快速适应。数据量不大可以放到内存中,因此批量学习就够了。

提示:如果数据量很大,你可以要么在多个服务器上对批量学习做拆分(使用MapReduce技术,后面会看到),或是使用线上学习。

选择性能指标

下一步是选择性能指标。回归问题的典型指标是均方根误差(RMSE)。均方根误差测量的是系统预测误差的标准差。例如,RMSE等于50000,意味着,68%的系统预测值位于实际值的$50000以内,95%的预测值位于实际值的$100000以内。等式2-1展示了计算RMSE的方法。

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

符号的含义

这个方程引入了一些常见的贯穿本书的机器学习符号:

  • m是测量RMSE的数据集中的实例数量。
    例如,如果用一个含有2000个分区的验证集求RMSE,则m = 2000。

  •   x(i)是数据集第ith个实例的所有特征值(不包含标签)的矢量,y(i)是它的标签(这个实例的输出值)。   例如,如果数据集中的第一个分区位于经度–118.29°,纬度33.91°,有1416名居民,收入中位数是$38372,房价中位数是$156400(不考虑其它特征),则有:

    和,

  • X是包含数据集中所有实例的所有特征值(不包含标签)的矩阵。每一行是一个实例,第ith行是x(i)的转置,标记为(x(i))T。
    例如,仍然是前面的第一区,矩阵X就是:

  • h是系统的预测函数,也称为假设(hypothesis)。当系统收到一个实例的特征矢量x(i),就会输出这个实例的一个预测值ŷ(i) = h(x(i))(ŷ读作y-hat)。
    例如,如果系统预测第一区的房价中位数是$158400,则ŷ(1) = h(x(1)) = 158400。预测误差是ŷ(1) – y(1) = 2000。
    RMSE(X,h)是使用假设h在样本集上测量的损失函数。

我们使用小写斜体表示标量值(例如 my(1) )和函数名(例如 h ),小写粗体表示矢量(例如 x(i) ),大写粗体表示矩阵(例如X)。(译者注:MarkDown表示粗体斜体太麻烦了,忽略字体。)

虽然大多数时候RMSE是回归任务可靠的性能指标,在有些情况下,你可能需要另外的函数。例如,假设存在许多异常的分区。此时,你可能需要使用绝对平均误差(Mean Absolute Error,也称作平均绝对偏差,见等式2-2):

等式 绝对平均误差

RMSE和MAE都是测量预测值和目标值两个矢量距离的方法。有多种测量距离的方法,或范数:

  • 计算对应欧几里得范数的平方和的根(RMSE):这个距离介绍过。它也称作ℓ2范数,标记为// · //2(或只是// · //)。

  • 计算对应于ℓ1(标记为// · //1)范数的绝对值和(MAE)。有时,也称其为曼哈顿范数,因为它测量了城市中的两点,沿着矩形的边行走的距离。

  • 更一般的,包含n个元素的矢量v的ℓk范数,定义成

    ℓ0只显示了这个矢量的基数(即,元素的个数),ℓ∞是矢量中最大的绝对值。

  • 范数的指数越高,就越关注大的值而忽略小的值。这就是为什么RMSE比MAE对异常值更敏感。但是当异常值是指数分布的(类似正态曲线),RMSE就会表现很好。

核实假设

最后,最好列出并核对迄今(你或其他人)作出的假设。,这样可以尽早发现严重的问题。例如,你的系统输出的分区房价,会传入到下游的机器学习系统,我们假设这些价格确实会被当做分区房价使用。但是如果下游系统实际上将价格转化成了分类(例如,便宜、中等、昂贵),然后使用这些分类,而不是使用价格。这样的话,获得准确的价格就不那么重要了,你只需要得到合适的分类。问题相应地就变成了一个分类问题,而不是回归任务。你可不想在一个回归系统上工作了数月,最后才发现真相。

幸运的是,在与下游系统主管探讨之后,你很确信他们需要的就是实际的价格,而不是分类。很好!整装待发,可以开始写代码了。

获取数据

开始动手。最后用Jupyter notebook完整地敲一遍示例代码。完整的代码位于https://github.com/ageron/handson-ml

创建工作空间

首先,你需要安装Python。可能已经安装过了,没有的话,可以从官网下载https://www.python.org/

接下来,需要为你的机器学习代码和数据集创建工作空间目录。打开一个终端,输入以下命令(在提示符$之后):

$ export ML_PATH="$HOME/ml"      # 可以更改路径
$ mkdir -p $ML_PATH

还需要一些Python模块:Jupyter、NumPy、Pandas、Matplotlib和Scikit-Learn。如果所有这些模块都已经在Jupyter中运行了,你可以直接跳到下一节“下载数据”。如果还没安装,有多种方法可以进行安装(包括它们的依赖)。你可以使用系统的包管理系统(比如Ubuntu上的apt-get,或macOS上的MacPorts或HomeBrew),安装一个Python科学计算环境比如Anaconda,使用Anaconda的包管理系统,或者使用Python自己的包管理器pip,它是Python安装包(自从2.7.9版本)自带的。可以用下面的命令检测是否安装pip:

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

你需要保证pip是近期的版本,至少高于1.4,以保障二进制模块文件的安装(也成为wheels)。要升级pip,可以使用下面的命令:

$ pip3 install --upgrade pip
Collecting pip
[...]
Successfully installed pip-9.0.1

创建独立环境

如果你希望在一个独立环境中工作(强烈推荐这么做,不同项目的库的版本不会冲突),用下面的pip命令安装virtualenv:

$ pip3 install --user --upgrade virtualenv
Collecting virtualenv
[...]
Successfully installed virtualenv

现在可以通过下面命令创建一个独立的Python环境:

$ cd $ML_PATH
$ virtualenv env
Using base prefix '[...]'
New python executable in [...]/ml/env/bin/python3.5
Also creating executable in [...]/ml/env/bin/python
Installing setuptools, pip, wheel...done.

以后每次想要激活这个环境,只需打开一个终端然后哦输入:

$ cd $ML_PATH
$ source env/bin/activate

启动该环境时,使用pip安装的任何包都只安装于这个独立环境中,Python指挥访问这些包(如果你希望Python能访问系统的包,创建环境时要使用包选项--system-site-)。更多信息,请查看virtualenv文档。

现在,你可以使用pip命令安装所有必需的模块和它们的依赖:

$ pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learn
Collecting jupyter
  Downloading jupyter-1.0.0-py2.py3-none-any.whl
Collecting matplotlib
  [...]

要检查安装,可以用下面的命令引入每个模块:

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

这个命令不应该有任何输出和错误。现在你可以用下面的命令打开Jupyter:

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

Jupyter服务器现在运行在终端上,监听8888端口。你可以用浏览器打开http://localhost:8888/,以访问这个服务器(服务器启动时,通常就自动打开了)。你可以看到一个空的工作空间目录(如果按照先前的virtualenv步骤,只包含env目录)。

现在点击按钮New创建一个新的Python笔记本,选择合适的Python版本(见图2-3)。

图2-3 Jupyter的工作空间

这一步做了三件事:首先,在工作空间中创建了一个新的notebook文件Untitled.ipynb;第二,它启动了一个Jupyter的Python内核来运行这个notebook;第三,在一个新栏中打开这个notebook。接下来,点击Untitled,将这个notebook重命名为Housing(这会将ipynb文件自动命名为Housing.ipynb)。

notebook包含一组代码框。每个代码框可以放入可执行代码或格式化文本。现在,notebook只有一个空的代码框,标签是“In [1]:”。在框中输入print("Hello world!"),点击运行按钮(见图2-4)或按Shift+Enter。这会将当前的代码框发送到Python内核,运行之后会返回输出。结果显示在代码框下方。由于抵达了notebook的底部,一个新的代码框会被自动创建出来。从Jupyter的Help菜单中的User Interface Tour可以学习Jupyter的基本操作。

图2-4 在notebook中打印Hello world!

下载数据

一般情况下,数据是存储于关系型数据库(或其它常见数据库)中的多个表、文档、文件。要访问数据,你首先要有密码和登录权限。,并要了解数据模式。但是在这个项目中,这一切要简单些:只要下载一个压缩文件,housing.tgz,它包含一个CSV文件housing.csv,含有所有数据。

你可以使用浏览器下载,运行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(),就会在工作空间创建一个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()方法查看该数据集的顶部5行(见图2-5)。

图2-5 数据集的顶部五行

每一行都表示一个分区。共有10个属性(截图中可以看到6个):经度、维度、房屋年龄中位数、总房间数、卧室数量、人口数、家庭数、收入中位数、房屋价值中位数、离大海距离。

info()方法可以快速查看数据的描述,包括总行数、每个属性的类型和非空值的数量(见图2-6)。

图2-6 房屋信息

数据集中共有20640个实例,按照机器学习的标准这个数据量很小,但是非常适合入门。总房间数只有20433个非空值,意味着207个分区缺少这个值。后面要对它进行处理。

所有的属性都是数值的,除了离大海距离这项。它的类型是对象,因此可以包含任意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()方法展示了数值属性的概括(见图2-7)。

图2-7 每个数值属性的概括

count、mean、min和max几行的意思很明了。注意,空值被忽略了(所以,卧室总数是20433而不是20640)。std是标准差(揭示数值的分散度)。25%、50%、75%展示了对应的分位数:每个分位数指明小于这个值,且指定分组的百分比。例如,25%的分区的房屋年龄中位数小于18,而50%的小于29,75%的小于37。这些值通常称为25th分位数(或1st四分位数),中位数,75th分位数(3rd四分位数)。

另一种快速了解数据类型的方法是画出每个数值属性的柱状图。柱状图(的纵轴)展示了特定范围的实例的个数。你还可以一次给一个属性画图,或对完整数据集调用hist()方法,后者会画出每个数值属性的柱状图(见图2-8)。例如,你可以看到略微超过800个分区的median_house_value值差不多等于$500000。

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

图2-8 每个数值属性的柱状图

笔记:hist()方法依赖于Matplotlib,后者依赖于用户指定的图形后端以打印到屏幕上。因此在画图之前,你要指定Matplotlib要使用的后端。最简单的方法是使用Jupyter的魔术命令%matplotlib inline。它会告诉Jupyter设定好Matplotlib,以使用Jupyter自己的后端。绘图就会在notebook中渲染了。注意在Jupyter中调用show()不是必要的,因为代码框执行后Jupyter会自动展示图像。

注意柱状图中的一些点:

  1. 首先,收入中位数貌似不是美元(USD)。与数据采集团队交流之后,你被告知数据是经过缩放调整的,过高收入中位数的会变为15(实际为15.0001),过低的会变为5(实际为0.4999)。在机器学习中对数据进行预处理很正常,不一定是问题,但你要明白数据是如何计算出来的。

  2. 房屋年龄中位数和房屋价值中位数也被设了上限。后者可能是个严重的问题,因为它是你的目标属性(你的标签)。你的机器学习算法可能学习到价格不会超出这个界限。你需要与下游团队核实,这是否会成为问题。如果他们告诉你他们需要明确的预测值,即使超过$500000,你则有两个选项:   a.对于设了上限的标签,重新收集合适的标签;
    b.将这些分区从训练集移除(也从测试集移除,因为若房价超出$500000,你的系统就会被差评)。

  3. 这些属性值有不同的量度。我们会在本章后面讨论特征缩放。

  4. 最后,许多柱状图的尾巴很长:相较于左边,它们在中位数的右边延伸过远。对于某些机器学习算法,这会使检测规律变得更难些。我们会在后面尝试变换处理这些属性,使其变为正态分布。

希望你现在对要处理的数据有一定了解了。

警告:稍等!再你进一步查看数据之前,你需要创建一个测试集,放在一旁,再也不看。

创建测试集

在这个阶段就分割数据,听起来很奇怪。毕竟,你只是简单快速地查看了数据而已,你需要再仔细调查下数据以决定使用什么算法。这么想是对的,但是人类的大脑是一个神奇的发现规律的系统,这意味着大脑非常容易发生过拟合:如果你查看了测试集,就会不经意地按照测试集中的规律来选择某个特定的机器学习模型。再当你使用测试集来评估误差率时,就会导致评估过于乐观,而实际部署的系统表现就会差。这称为数据透视偏差。

理论上,创建测试集很简单:只要随机挑选一些实例,一般是数据集的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