基于书籍《Hands-on Machine Learning with Scikit-Learn, Keras & TensorFlow》的笔记
经过了前面的分析后,我们可以开始准备机器学习算法训练所需要的数据了,我们使用函数来执行整个过程而不是手动,其有以下几个好处:
现在,我们先回到一个干净的训练集(再次复制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"]) # 方法 1 housing.drop("total_bedrooms", axis=1) # 方法 2 median = housing["total_bedrooms"].median() # 方法 3 housing["total_bedrooms"].fillna(median, inplace=True)
选择方法3,需要计算出训练集的中位数值,然后用它去填充训练集中的缺失值,同时记得保存它方便在后面用来替换测试集中的缺失值,还可以用于替换系统上线后新数据的缺失值。
虽然事实上使用方法三就足够了,但我们还是要学习下Scikit-Learn为我们提供的数据转换类SimpleImputer,因为其将有益于我们在最后去构建数据转换的流水线。下面我们来学习一下如何使用它
1.创建一个SimpleImputer实例,指定你要用属性的中位数值替换该属性的缺失值:
from sklearn.impute import SimpleImputer imputer=SimpleImputer(strategy="median")
2.因为中位数值只能在数值属性上计算,所以我们需要创建一个没有文本属性ocean_proximity的数据副本:
housing_num=housing.drop("ocean_proximity",axis=1)
3.使用fit()方法方法将imputer实例适配到训练数据:
imputer.fit(housing_num) #返回self fit方法用于使用数据集训练参数-不一定必须使用
4.这里的imputer会计算每个属性的中位数值,并且将结果存储到实例变量statistics_中,因为这个函数可能被应用于新的数据集,而新的数据集的其他属性也可能存在缺失值的情况,所以稳妥起见,还是将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])
5.现在,我们可以使用这个训练好的imputer将训练集中的缺失值替换为计算好的中位数值。
X=imputer.transform(housing_num)
这里的X得到的是一个对特征转换后的Numpy数组,ndarray,想要将其变成DF,也很简单。
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing_num.index)
前面我们提到我们的数据集里面存在一个文本属性 “ocean_proximity”,经过探索,我们发现其事实上是一个类别型的文本数据,所以事情变得容易了,机器学习算法一般都更喜欢使用数字作为特征,所以我们将通过编码将其从文本转换到数字。对于编码-sklearn为我们提供了多种解决方案,常见的有orginalEncoder()和OneHotEncoder,这里我们选择使用OneHotEncoder,其是对类别编码的一种常见方式,其优点在于使得机器学习算法不会认为各类存在任何关系,即不会乱加一些奇怪的信息,但这也是它的缺点,这样的编码是稀疏的(特别是类别较多的时候会导致大量的特征输入从而减慢训练速度和降低模型性能),也许加入一些类别间的联系能够使得机器学习更容易学习到其应该学习到的模式。
代码:
>>> housing_cat = housing[["ocean_proximity"]] #构造二维数组,方便cat_encoder调用 >>> from sklearn.preprocessing import OneHotEncoder >>> cat_encoder = OneHotEncoder() >>> housing_cat_1hot = cat_encoder.fit_transform(housing_cat) >>> housing_cat_1hot #以稀疏矩阵形式存储,节省计算机存储空间,降低空间复杂度,提供了一个toarray()的方法,可以转换为稠密的Numpy数组 #ndarray <16512x5 sparse matrix of type '<class 'numpy.float64'>' with 16512 stored elements in Compressed Sparse Row format> >>> 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.]]) >>> cat_encoder.categories_ [array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'], dtype=object)]
正如前面提到的one-hot编码的稀疏特性,我们还可以使用相关的数字特征代替类别输入,例如,我们可以使用与海洋的距离来替换ocean_proximity这个特征,当然,现代机器学习一个更加常见的方法是使用一种可学习的低维向量Embedding,如果你听说过词向量,那么你将再熟悉不过它了。除了输入,输出的好的编码或许也会影响性能(这个以后在进行探讨)
转换器事实上是一个类,其一般有以下三种方法:
fit()、transform()、fit_transform().
对于自定义转换器,我们一般会添加TransformerMixin和 BaseEstimator作为基类,其能够直接获得fit_transform()方法以及在构造函数中避免*args or **kargs.
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] #c_ 拼接 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)
上面是一个实例-其将帮助我们迅速实现自动的特征组合并加入原数据集,且提供了add_bedrooms_per_room参数,这使得我们能够方便地进行控制和尝试更多与其他转换器的组合。
特征缩放:
特征缩放这个相信大家都很熟悉了,常见的有MinMax缩放,数据标准化(使用sklearn.StandadScaler类我们能很简单地实现这个功能)
跟所有的转换一样,我们都暂时不将scaler应用于测试集,直到我们确认我们在交叉验证集已经获得了不错的效果,需要进行最后的上线测试的时候。
ok,终于到了构建数据转换流水线的时候了,**构建数据转换流水线不仅能够我们以正确的顺序实现数据转换,还能够帮助我们实现整个流程的简洁可视与自动化。**使用sklearn的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()再调用transform()】的方法,将一个调用的输出作为参数传递给下一个调用方法,直到传递到最终的估算器,则只会调用fit()方法。
流水线提供的函数与最终的估算器的函数相同,例如本例子中,最后一个估算器是StandadScaler,那么流水线将会同时具有transform()和fir_transform()两个方法。
接下来,我们使用sklearn的ColumnTransformer类来合并对于类别列和数据列的处理,其可以将适当的转换应用于每一个列,与pandas DataFrames一起食用效果更佳!
ColumnTransformer的构造函数使用规则如下:其需要一个元组列表,每一个元组都包含一个名字,一个转换器(流水线),以及一个该转换器需要应用的列名(或索引)的列表。
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)
使用full_pipeline.fit_transform(housing)方法后,其将每个转换器应用于适当的列,并沿第二个轴合并输出(转换器必须返回相同数量的行)。值得一提的是,OneHotEncoder返回的是一个稀疏矩阵,而num_pipeline则返回一个密集矩阵。当两者混合在一起时,ColumnTransformer会估算最终矩阵的密度(即单元格的非零比率),如果密度低于给定的阈值(默认0.3),则返回一个稀疏矩阵。在此实例中,其返回一个密集矩阵。
Figure 关于其他超参数的使用