获取数据

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

创建工作空间

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

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

  1. &# x24; export ML_PATH="&# x24;HOME/ml" # 可以更改路径
  2. &# x24; mkdir -p &# x24;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

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

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

  1. $ pip3 install --upgrade pip
  2. Collecting pip
  3. [...]
  4. Successfully installed pip-9.0.1

创建独立环境

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

  1. $ pip3 install --user --upgrade virtualenv
  2. Collecting virtualenv
  3. [...]
  4. Successfully installed virtualenv

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

  1. &# x24; cd &# x24;ML_PATH
    &# x24; 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.

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

  1. &# x24; cd &# x24;ML_PATH
    &# x24; source env/bin/activate

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

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

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

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

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

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

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

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

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

获取数据 - 图1

图 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

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

下载数据

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

你可以使用浏览器下载,运行tar xzf housing.tgz解压出csv文件,但是更好的办法是写一个小函数来做这件事。如果数据变动频繁,这么做是非常好的,因为可以让你写一个小脚本随时获取最新的数据(或者创建一个定时任务来做)。如果你想在多台机器上安装数据集,获取数据自动化也是非常好的。

下面是获取数据的函数:

  1. import os
  2. import tarfile
  3. from six.moves import urllib
  4. DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
  5. HOUSING_PATH = "datasets/housing"
  6. HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/housing.tgz"
  7. def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
  8. if not os.path.isdir(housing_path):
  9. os.makedirs(housing_path)
  10. tgz_path = os.path.join(housing_path, "housing.tgz")
  11. urllib.request.urlretrieve(housing_url, tgz_path)
  12. housing_tgz = tarfile.open(tgz_path)
  13. housing_tgz.extractall(path=housing_path)
  14. housing_tgz.close()

现在,当你调用fetch_housing_data(),就会在工作空间创建一个datasets/housing目录,下载housing.tgz文件,解压出housing.csv

然后使用Pandas加载数据。还是用一个小函数来加载数据:

  1. import pandas as pd
  2. def load_housing_data(housing_path=HOUSING_PATH):
  3. csv_path = os.path.join(housing_path, "housing.csv")
  4. return pd.read_csv(csv_path)

这个函数会返回一个包含所有数据的 Pandas DataFrame 对象。

快速查看数据结构

使用DataFramehead()方法查看该数据集的前5行(见图 2-5)。

获取数据 - 图3

图 2-5 数据集的前五行

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

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

获取数据 - 图4

图 2-6 房屋信息

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

所有的属性都是数值的,除了离大海距离这项。它的类型是对象,因此可以包含任意 Python 对象,但是因为该项是从 CSV 文件加载的,所以必然是文本类型。在刚才查看数据前五项时,你可能注意到那一列的值是重复的,意味着它可能是一项表示类别的属性。可以使用value_counts()方法查看该项中都有哪些类别,每个类别中都包含有多少个街区:

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

再来看其它字段。describe()方法展示了数值属性的概括(见图 2-7)。

获取数据 - 图5

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

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

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

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

获取数据 - 图6

图 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 美元,你则有两个选项:

    1. 对于设了上限的标签,重新收集合适的标签;
    2. 将这些街区从训练集移除(也从测试集移除,因为若房价超出 500000 美元,你的系统就会被差评)。
  3. 这些属性值有不同的量度。我们会在本章后面讨论特征缩放。

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

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

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

创建测试集

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

理论上,创建测试集很简单:只要随机挑选一些实例,一般是数据集的 20%,放到一边:

  1. import numpy as np
  2. def split_train_test(data, test_ratio):
  3. shuffled_indices = np.random.permutation(len(data))
  4. test_set_size = int(len(data) * test_ratio)
  5. test_indices = shuffled_indices[:test_set_size]
  6. train_indices = shuffled_indices[test_set_size:]
  7. return data.iloc[train_indices], data.iloc[test_indices]

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

  1. >>> train_set, test_set = split_train_test(housing, 0.2)
  2. >>> print(len(train_set), "train +", len(test_set), "test")
  3. 16512 train + 4128 test

这个方法可行,但是并不完美:如果再次运行程序,就会产生一个不同的测试集!多次运行之后,你(或你的机器学习算法)就会得到整个数据集,这是需要避免的。

解决的办法之一是保存第一次运行得到的测试集,并在随后的过程加载。另一种方法是在调用np.random.permutation()之前,设置随机数生成器的种子(比如np.random.seed(42)),以产生总是相同的洗牌指数(shuffled indices)。

但是如果数据集更新,这两个方法都会失效。一个通常的解决办法是使用每个实例的ID来判定这个实例是否应该放入测试集(假设每个实例都有唯一并且不变的ID)。例如,你可以计算出每个实例ID的哈希值,只保留其最后一个字节,如果该值小于等于 51(约为 256 的 20%),就将其放入测试集。这样可以保证在多次运行中,测试集保持不变,即使更新了数据集。新的测试集会包含新实例中的 20%,但不会有之前位于训练集的实例。下面是一种可用的方法:

  1. import hashlib
  2. def test_set_check(identifier, test_ratio, hash):
  3. return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio
  4. def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
  5. ids = data[id_column]
  6. in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
  7. return data.loc[~in_test_set], data.loc[in_test_set]

不过,房产数据集没有ID这一列。最简单的方法是使用行索引作为 ID:

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

如果使用行索引作为唯一识别码,你需要保证新数据都放到现有数据的尾部,且没有行被删除。如果做不到,则可以用最稳定的特征来创建唯一识别码。例如,一个区的维度和经度在几百万年之内是不变的,所以可以将两者结合成一个 ID:

  1. housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
  2. train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")

Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数是train_test_split,它的作用和之前的函数split_train_test很像,并带有其它一些功能。首先,它有一个random_state参数,可以设定前面讲过的随机生成器种子;第二,你可以将种子传递给多个行数相同的数据集,可以在相同的索引上分割数据集(这个功能非常有用,比如你的标签值是放在另一个DataFrame里的):

  1. from sklearn.model_selection import train_test_split
  2. train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

目前为止,我们采用的都是纯随机的取样方法。当你的数据集很大时(尤其是和属性数相比),这通常可行;但如果数据集不大,就会有采样偏差的风险。当一个调查公司想要对 1000 个人进行调查,它们不是在电话亭里随机选 1000 个人出来。调查公司要保证这 1000 个人对人群整体有代表性。例如,美国人口的 51.3% 是女性,48.7% 是男性。所以在美国,严谨的调查需要保证样本也是这个比例:513 名女性,487 名男性。这称作分层采样(stratified sampling):将人群分成均匀的子分组,称为分层,从每个分层去取合适数量的实例,以保证测试集对总人数有代表性。如果调查公司采用纯随机采样,会有 12% 的概率导致采样偏差:女性人数少于 49%,或多于 54%。不管发生那种情况,调查结果都会严重偏差。

假设专家告诉你,收入中位数是预测房价中位数非常重要的属性。你可能想要保证测试集可以代表整体数据集中的多种收入分类。因为收入中位数是一个连续的数值属性,你首先需要创建一个收入类别属性。再仔细地看一下收入中位数的柱状图(图 2-9)(译注:该图是对收入中位数处理过后的图):

获取数据 - 图7

图 2-9 收入分类的柱状图

大多数的收入中位数的值聚集在 2-5(万美元),但是一些收入中位数会超过 6。数据集中的每个分层都要有足够的实例位于你的数据中,这点很重要。否则,对分层重要性的评估就会有偏差。这意味着,你不能有过多的分层,且每个分层都要足够大。后面的代码通过将收入中位数除以 1.5(以限制收入分类的数量),创建了一个收入类别属性,用ceil对值舍入(以产生离散的分类),然后将所有大于 5的分类归入到分类 5:

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

现在,就可以根据收入分类,进行分层采样。你可以使用 Scikit-Learn 的StratifiedShuffleSplit类:

  1. from sklearn.model_selection import StratifiedShuffleSplit
  2. split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
  3. for train_index, test_index in split.split(housing, housing["income_cat"]):
  4. strat_train_set = housing.loc[train_index]
  5. strat_test_set = housing.loc[test_index]

检查下结果是否符合预期。你可以在完整的房产数据集中查看收入分类比例:

  1. >>> housing["income_cat"].value_counts() / len(housing)
  2. 3.0 0.350581
  3. 2.0 0.318847
  4. 4.0 0.176308
  5. 5.0 0.114438
  6. 1.0 0.039826
  7. Name: income_cat, dtype: float64

使用相似的代码,还可以测量测试集中收入分类的比例。图 2-10 对比了总数据集、分层采样的测试集、纯随机采样测试集的收入分类比例。可以看到,分层采样测试集的收入分类比例与总数据集几乎相同,而随机采样数据集偏差严重。

获取数据 - 图8

图 2-10 分层采样和纯随机采样的样本偏差比较

现在,你需要删除income_cat属性,使数据回到初始状态:

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

我们用了大量时间来生成测试集的原因是:测试集通常被忽略,但实际是机器学习非常重要的一部分。还有,生成测试集过程中的许多思路对于后面的交叉验证讨论是非常有帮助的。接下来进入下一阶段:数据探索。