2.5 机器学习算法得数据准备

现在,终于是时候给你的机器学习算法准备数据了。这里你应该编写函数来执行,而不是手动操作,但是现在,让我们先回到一个干净的训练集(再次复制strat_train_set),然后见预测器和标签分开,因为这里我们不一定对它们使用相同的转换方式(需要注意drop()会创建一个数据副本,但是不影响strat_train_set):

  1. housing = strat_train_set.drop("median_house_value",axis=1)
  2. housing_labels = strat_train_set["median_house_value"].copy()

2.5.1 数据清理

大部分的机器学习算法无法在缺失的特征上工作,所以我们要创建一些函数来辅助它。前面我们注意到total_bedrooms属性有部分值缺失,所以我们要解决它。有以下三种选择:

  1. 放弃这些相应的区域
  2. 放弃整个属性
  3. 将缺失的值设置为某个值(0,平均数或者中位数等)。

通过DataFrame的dropna()、drop()和fillna()方法,可以轻松的完成这些操作:

  1. housing.dropna(subset=["total_bedrooms"]) #option 1
  2. housing.drop("total_bedrooms",axis=1) #option 2
  3. median = housing["total_bedrooms"].median() #option 3
  4. housing["total_bedrooms"].fillna(median,inplace=True)

如果选择方法3,你需要计算出训练集的中位数值,然后用它填充训练集中的缺失值,但也别忘了保存这个计算出来的中位数值,因为后面可能需要用到。当重新评估系统是,你需要更换测试集中的缺失值;或者在系统上线时,需要使用新数据代替缺失值。Scikit-Learn提供了一个非常容易上手的类来处理缺失值:SimpleImputer。使用方法如下:首先,你需要创建一个SimpleImputer实例,指定你要用属性的中位数值替换该属性的缺失值:

  1. from sklearn.impute import SimpleImputer
  2. imputer = SimpleImputer(strategy="median")

由于中位数值只能在数值属性上计算,所以我们需要创建一个没有文本属性ocean_proximity的数据副本:

  1. housing_num = housing.drop("ocean_proximity",axis=1)

使用fit()方法将imputer实例适配到训练数据:

  1. imputer.fit(housing_num)

这里imputer仅仅只是计算了每个属性的中位数值,并将结果存在其实例变量statistics_中。虽然只有total_bedrooms这个属性存在缺失值,但是我们无法确认系统启动之后新数据中是否一定不存在任何缺失值,所以稳妥起见,还是将imputer应用于所有的数值属性:

  1. >>>imputer.statistics_
  2. array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. ,
  3. 408. , 3.5409])
  4. >>>housing_num.median().values
  5. array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. ,
  6. 408. , 3.5409])

现在你可以用这个“训练有素”的imputer将缺失值替换成中位数值从而完成训练测试集转换:

  1. X = imputer.transform(housing_num)

结果是一个包含转换后特征的Numpy数组。如果你想将它放回pandas DataFrame,也很简单:

  1. housing_tr = pd.DataFrame(X,columns=housing_num.columns,index=housing_num.index)

2.5.2 处理文本和分类属性

到目前为止,我们只处理了数值属性,但现在让我们看一下文本属性。在此数据集中,只有一个:ocean_proximity属性。我们看看前10个实例的值:

  1. >>>housing_cat = housing[["ocean_proximity"]]
  2. >>>housing_cat.head(10)
  3. ocean_proximity
  4. 17606 <1H OCEAN
  5. 18632 <1H OCEAN
  6. 14650 NEAR OCEAN
  7. 3230 INLAND
  8. 3555 <1H OCEAN
  9. 19480 INLAND
  10. 8879 <1H OCEAN
  11. 13685 INLAND
  12. 4937 <1H OCEAN
  13. 4861 <1H OCEAN

它不是任意文本,而是有限个可能的取值,每个值代表一个类别。因此,此属性是分类属性。大多数机器学习算法更喜欢使用数字,因此让我们将这些类别从文本转到数字。为此,我们可以使用Scikit-Learn的OrdinalEncoder类:

  1. >>>from sklearn.preprocessing import OrdinalEncoder
  2. >>>ordinal_encoder = OrdinalEncoder()
  3. >>>housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
  4. >>>housing_cat_encoded[:10]
  5. array([[0.],
  6. [0.],
  7. [4.],
  8. [1.],
  9. [0.],
  10. [1.],
  11. [0.],
  12. [1.],
  13. [0.],
  14. [0.]])

你可以使用Categories_实例变量获取类别列表。这个列表包含每个类别属性的以为数组(在这种情况下,这个列表包含一个数组,因为只有一个类别属性):

  1. >>>ordinal_encoder.categories_
  2. [array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
  3. dtype=object)]

这种表征方式产生的一个问题是,机器学习算法会认为两个相近的值比两个离得较远的值更为相似一些。在某些情况下这是对的(对一些有序类别,像“坏”“平均”“好”“优秀”),但是,对ocean_proximity而言情况并非如此。为了解决这个问题,就需要使用独热编码。Scikit-Learn提供了一个OneHotEncoder编码器,可以将类别值转换为独热向量。我们用它来将类别编码为独热向量。

  1. >>>from sklearn.preprocessing import OneHotEncoder
  2. >>>cat_encoder = OneHotEncoder()
  3. >>>housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
  4. >>>housing_cat_1hot
  5. <16512x5 sparse matrix of type '<class 'numpy.float64'>'
  6. with 16512 stored elements in Compressed Sparse Row format>

注意到这里的输出是一个SciPy稀疏矩阵,而不是一个NumPy数组。稀疏矩阵仅存储非零元素的位置,而你依旧可以像使用一个普通二维数组那样来使用它,当然也可以调用toarray()方法即可将它转为一个(密集的)NumPy数组:

  1. >>>housing_cat_1hot.toarray()
  2. array([[1., 0., 0., 0., 0.],
  3. [1., 0., 0., 0., 0.],
  4. [0., 0., 0., 0., 1.],
  5. ...,
  6. [0., 1., 0., 0., 0.],
  7. [1., 0., 0., 0., 0.],
  8. [0., 0., 0., 1., 0.]])

你可以再次使用编码器的categories_实例变量来得到类别列表:

  1. >>>cat_encoder.categories_
  2. [array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
  3. dtype=object)]

提示如果类别属性具有大量可能的类别(例如,国家代码、专业、物种),那么独热编码会导致大量的输入特征,这可能会减慢训练并降低性能。如果发生这种情况,你可能想要用相关的数字特征代替类别输入。例如,你可以用于海洋的距离来替换ocean_proximity特征(类似地,可以使用该国家的人口和人均GDP来代替国家代码)。或者,你可以用可学习的低维向量(称为嵌入)来替换每个类别。每个类别的表征可以在训练期间学习。这是表征学习的示例(更多细节参见第13章和 第17章)

2.5.3 自定义转换器

虽然Scikit-Learn提供了许多有用的转换器,但是你任然需要为一些诸如自定义清理操作或组合特定属性等任务编写自己的转换器。你当然希望让自己的转换器与Scikit-Learn自身的功能(比如流水线)无缝衔接,而由于Scikit-Learn依赖于鸭子类型的编译,而不是继承,所以你所需要的只是创建一个类,然后应用以下三种方法:fit()(返回self)、transform()、fit_transform()。
你可以通过添加TransformerMixin作为基类,直接得到最后一种方法。同时,如果添加BaseEstimator作为基类(并在构造函数中避免args和*kargs),你还能额外获得两种非常有用的自动调整超参数的方法(get_params()和set_params())。
例如,我们前面讨论过的组合属性,这里有个简单的转换器类,用来添加组合后的属性:

  1. from sklearn.base import BaseEstimator,TransformerMIxin
  2. rooms_ix,bedrooms_ix,population_ix,households_ix = 3,4,5,6
  3. class CombinedAttributesAdder(BaseEstimator,TransformerMixin):
  4. def __init__(self,add_bedrooms_per_room = True): # no *args or **kargs
  5. self.add_bedrooms_per_room = add_bedrooms_per_room
  6. def fit(self,X,y=None):
  7. return self #nothing else to do
  8. def transform(self,X):
  9. rooms_per_household = X[:,rooms_ix] / X[:,households_ix]
  10. population_per_household = X[:,population_ix] / X[:,households_ix]
  11. if self.add_bedrooms_per_room:
  12. bedrooms_per_room = X[:,bedrooms_ix] / X[:,rooms_ix]
  13. return np.c_[X,rooms_per_household,population_per_household,bedrooms_per_room]
  14. else:
  15. return np.c_[X,rooms_per_household,population_per_household]
  16. attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
  17. housing_extra_attribs = attr_adder.transform(housing.values)

在本例中,转换器有一个超参数add_bedrooms_per_room默认设置为True。这个超参数可以让你轻松的知晓添加这个属性是否有助于机器学习算法。更一般地,如果你对数据准备地步骤没有充分地信心,就可以添加这个超参数来把关。这些数据准备步骤地执行越自动化,你自动尝试地组合也就越多,从而有更大可能从中找到一个重要的组合,还节省了大量的时间。

2.5.4 特征缩放

最重要也最需要应用到数据上的转换就是特征缩放。Scikit-Learn提供了一个标准化的转换器StandadScaler。

2.5.5 转换流水线

真如你所见,许多数据转换的步骤需要以正确的顺序来执行。而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)

到目前为止,我们分别处理了类别和数值列。拥有一个能够处理所有列的转换器会更方便,将适当的转换应用于每个列。在0.20版中,Scikit-Learn为此引入了ColumnTransformer,好消息是它于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)