在这个阶段,主动把一部分数据放在一边,这听起来可能有点奇怪。毕竟,你只是快速浏览了一下数据,在决定使用哪种算法之前,你肯定应该学习关于它的更多的知识,对吧?这是对的,但是你的大脑是一个惊人的模式检测系统,这意味着它很容易过度拟合:如果您查看了测试集,您可能会在测试数据中发现一些看似有趣的模式,从而导致您选择一种特定的机器学习模型。这样的话,当您使用测试集来估计泛化错误时,您的估计将过于乐观,您将得到一个不会像您所预期的那样执行的系统。这被称为数据窥探偏差(data snooping bias)。

从理论上来说,创建一个测试集很简单:随机选取一些实例,通常是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

这是可行的,但它并不完美:如果您再次运行该程序,它将生成一个不同的测试集!随着时间的推移,你(或者你的机器学习算法)将会看到整个数据集,这是你想要避免的。

一个解决方案是在第一次运行时保存测试集,然后在后续的运行中加载它。另一个选项是在调用np.random.permutation()之前,设置随机数生成器的种子(例如,np.random.seed(42)),所以它总是产生相同的洗牌指数。

但是这两个解决方案都将在下次获取更新的数据集时被打破。一个常见的解决方案是使用每个实例的标识符来决定它是否应该进入测试集(假设实例具有惟一且不可变的标识符)。例如,您可以计算每个实例的标识符的散列码,只保留哈希的最后一个字节,并将该实例放在测试集中,如果该值较低或等于51(256的20%)。这将确保测试集在多次运行时保持一致,即使您刷新了数据集。新的测试集将包含20%的新实例,但是它不会包含以前在训练集中的任何实例。下面是一个实现:

import hashlib
def test_set_check(identifier, test_ratio, hash):
    return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio
def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
    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:

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-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人是整个人口的代表。例如,美国的人口由51.3%的女性和48.7%的男性组成,因此美国的一项很好的调查将试图维持这个比例:513名女性和487名男性。这被称为分层抽样[stratified sampling]:人口被划分为同构的子群,称为strata,从各层抽取适当数量的实例,以保证测试集是总体人口的代表。如果他们使用纯随机抽样,则大约有12%的几率会出现一个不对称的测试集,其中的女性比例低于49%或者超过54%。无论哪种方式,调查结果都会有明显的偏差。

假设你和专家聊天,他们告诉你,收入中值是预测房价中值的一个非常重要的因素。你可能想要确保测试集代表了整个数据集的各种收入类别。由于收入中值是一个连续的数字属性,您首先需要创建一个收入类别属性。让我们更仔细地看看收入中值的直方图(见图2-9):

图2 - 9 收入类别的直方图

大多数收入中值值都集中在2-5(数万美元)左右,但是一些收入中值的值却远远超过了6。重要的是在你的数据集中有足够数量的实例用于每一层,否则,层的重要性的估计可能是有偏差的。意味着你不应该有太多的层,而每个层应该足够大。下面的代码通过将收入中值除以1.5(以限制收入类别的数量)来创建收入类别属性,并使用ceil(有离散的类别)进行舍入,然后将所有大于5的类别合并为第5类:

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

现在你可以根据收入类别进行分层抽样了。为此,您可以使用Scikitt - 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]

让我们看看这是否符合预期。你可以线看看整个房屋数据集的收入类别的比例:

>>> housing["income_cat"].value_counts() / len(housing)
3.0   0.350581
2.0   0.318847
4.0   0.176308
5.0   0.114438
1.0   0.039826
Name: income_cat, dtype: float64

使用类似的代码,您可以在测试集中测量收入类别的比例。图2-10比较了整体数据集(verall),由分层抽样生成的测试集,使用纯随机抽样生成的测试集中的收入类别比例。如您所见,使用分层抽样生成的测试集的收入类别比例几乎与完整数据集相同,而使用纯随机抽样生成的测试集是相当扭曲的。

图2 - 10 分层与纯随机抽样的抽样偏差比较

现在,您应该删除income_cat属性,以便数据恢复到原来的状态:

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

我们花了相当多的时间在测试集的生成上,原因是:这是机器学习项目中经常被忽略但却很关键的部分。此外,当我们讨论交叉验证时,这些想法中的许多将是有用的。现在是进入下一个阶段的时候了:探索数据。

results matching ""

    No results matching ""