机器学习项目流程(三)为机器学习准备数据
为机器学习准备数据
下面我们开始为机器学习算法准备数据,首先我们将训练集中的 label(也就是median_house_value)分出来:
housing = strat_train_set.drop('median_house_value', axis=1)
housing_labels = strat_train_set['median_house_value'].copy()
drop函数会返回drop掉指定属性之后的数据集的副本,并不会影响原始的strat_train_set 数据集。接下来是数据清洗。
数据清洗
大多数机器学习算法都无法处理缺失属性,所以我们要尤其注意缺失值。之前我们有注意到 total_bedrooms 属性是有缺失值的,现在我们对它进行修正,主要有3种方式:
- 丢弃缺失值所在的整条条目
- 丢弃含缺失值的整个属性
- 为缺失值指定值(例如0、平均值、中值,等等)
对应于以上3种方法的pandas 函数为 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,用于处理缺失值,可以指定使用每个属性的中位数填充对应它属性的缺失值:
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='median')
由于中位数仅存在于数值型属性中,我们需要创建一个原始数据的副本,并将text类型的属性(ocean_proximity)剔除:
housing_num = housing.drop('ocean_proximity', axis=1)
然后使用fit() 方法,将imputer 实例应用的housing_num:
imputer.fit(housing_num)
imputer 会计算每个属性的中位数,并将结果存储在它的statistics_ 实例变量中。虽然当前仅有 total_bedrooms 属性有缺失值,但是我们无法确保在之后的新数据中,其他属性没有缺失值,所以我们最好是将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])
现在我们可以使用这个计算好的imputer应用到训练数据,使用中位数将缺失值补全:
X = imputer.transform(housing_num)
这个结果是一个NumPy 数组,包含的是转换后的属性数值。如果要将它转回Pandas DataFrame,我们可以用:
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
以上提到了 scikit-learn 的一个用法,我们下面介绍一下 sklearn 的结构。它主要的模式有:
- 一致性:所有对象使用同样的接口:
- Estimators:任何可以基于数据集而估算出一些参数的对象称为estimator(上面提到的imputer 就是一个estimator)。推测动作的执行由fit() 方法调用,传入的参数为一个数据集(或是两个数据集,监督学习中的训练数据集以及label 数据集)。其他任何用于指导推测过程的参数称为超参数(例如imputer的strategy)。它们在配置时,必须以实例变量的方式进行配置,一般是构造方法传入参数(例如 imputer = SimpleImputer(strategy='median') )
- Transformers:某些estimators(例如imputer)也可以转换一个数据集,这些称为transformers。同样,这些API调用也非常简单:通过调用trasform() 方法,将数据集作为参数传入,然后返回转换后的数据集。这种转换一般基于的是学习到的参数,例如一个imputer。所有transformers也包含一个很方便的方法 fit_transform(),它等同于调用fit() ,然后 transform() (不过有时候fit_transform() 是优化过的,并且运行地更快)
- Predictors:最后,一些estimators可以基于给定的数据集做预测,它们称为predictors。例如,LinearRegression 模型是一个predictor。Predictor包含一个predict() 方法,接收一个数据集的参数,并返回对应此数据集的预测。它还有一个方法是score(),用于衡量模型在测试集上的准确度。
- 访问变量:所有estimator的超参数都可以直接通过public 实例变量访问到(例如imputer.strategy),并且所有estimator 学习到的参数也都可以通过public 的实例变量加上一个下划线访问到(例如imputer.statistics_)
- 常规表示类:数据集由NumPy 数组或是SciPy 稀疏矩阵表示,而不是什么自定义的类去表示。超参数也仅仅是常规的Python strings 或数字
- 复用:已完成的工作或是代码可以很方便地在之后被复用。例如我们可以很方便地创建一个estimator的pipeline
- 合理的默认值:sklean为大部分参数提供了合理的默认值,有助于我们快速创建系统原型。
处理Text以及类别型属性
之前我们遗留了类别型属性 ocean_proximity 未处理,因为它是一个text的属性,所以我们也无法为它计算中值。
housing_cat = housing[['ocean_proximity']]
housing_cat.head()
17606 <1H OCEAN
18632 <1H OCEAN
14650 NEAR OCEAN
3230 INLAND
3555 <1H OCEAN
Name: ocean_proximity, dtype: object
大部分机器学习算法擅长处理数值型属性,所以我们可以将这些类别型属性转换为数值型。在这个例子中,我们使用 sklearn 的OrdinalEncoder 类:
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
array([[0.],
[0.],
[4.],
[1.],
[0.],
[1.],
[0.],
[1.],
[0.],
[0.]])
可以看到在执行OrdinalEncoder 之后,返回了一个数组,分别对应的是类别的编号。正如我们之前介绍过的sklearn的规则,这些类别属性存储在 categories_ 实例变量中:
ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
在这个例子中,categories_ 仅包含了一个一元数组,这是因为输入的数据中仅有一个类别型属性。如果输入数据有多个类别型属性,则每个属性均会对应返回一元数组。
使用这种类别表示方法会有一定的局限性,例如一些 ML 算法会认为数字接近的两者可能会更相似(或是说更倾向于属于同样的一个类别)。如果这个类别0-4表示的是等级(例如差、中等、良好、优秀这四个等级),那这种表示方式没有太大的问题。 但是很明显在这里仅仅是地域的分类而已,并没有严格的等级分类,所以使用 0-4 的范围表示这些类别是不太合适的。
对于这个问题,我们可以采用one-hot 编码。One-hot 编码概念比较简单,用 2 进制的每一位对应一个类别,然后为这个类别对应的位置1,其他置0。例如,这个属性中一共有5个类别,那对应这个属性我们先指定为[0, 0, 0, 0, 0],若是某条数据的ocean_proximity 值为 INLAND,INLAND 对应 categories_ 里数组元素的第1位,所以这条数据的ocean_proximity 属性的one-hot 编码为 [0, 1, 0, 0, 0]。这个新的one-hot 编码属性称为dummy 属性。sklearn提供了一个 OneHotEncoder 类,用于将类别型属性转换为one-hot 向量:
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
需要注意的是,输出是一个SciPy 稀疏矩阵,而不是NumPy 数组。这点非常重要,特别是在数据集中有大量类别型属性的时候。在one-hot 编码后,我们会得到一个非常大的矩阵,并且里面大部分元素都是0,并且每行中仅会有一个元素是1。若是使用大量的内存去存储这些0值,会非常浪费,所以sklearn中,使用了一个稀疏矩阵用于仅存储非0 元素的位置。我们使用它时,直接当它是一个正常的2D矩阵使用即可。但是如果确实需要将它转换为一个NumPy (Dense Array)数组的话,可以调用toarray() 方法:
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.]])
同样,在执行 one-hot 编码后,我们也是从 OneHotEncoder 对象中的 categories_ 变量中获取类别:
cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
如果类别型属性有很多可能的类别(例如国家代码,职业,物种等等),那么使用one-hot 编码的话,会产生非常大的输入属性。这样会导致训练过程变慢,并影响性能。如果真遇到了这种情况,则一般尽量使用具有代表意义的数值属性去表示这个类别属性。例如本例中的 ocean_proximity,我们可以使用“距离海岸的距离”这一数值型属性去替换原有的类别型属性。同样,对于country code,我们可以用这个国家的人口、或是人均gdp等去替换。还一种方式是使用学习到的低维向量(称为embedding)来替换这个类别型属性。每个类别的表示方式都可以在训练中学习到,这个涉及到representation learning,在此不会深入探讨。
自定义Transformers
前面我们介绍了几个transformers,(例如ordinal_encoder.fit_transform(housing_cat),这个方法将类别型属性数据转换为数值型属性)我们也可以自定义transformers。比如我们需要做一些自定义的数据清理操作,或是合并几个特定的属性。sklearn 基于的是 duck typing(并不是继承),所以我们需要做的仅仅是创建一个类,然后实现3个方法即可:fit()(返回self),transform() 以及 fit_transform()。对于最后一个方法,我们可以简单地添加一个TransformerMixin 作为基类即可。如果我们添加了 BaseEstimator 作为基类(并且不在构造器中指定 *args 以及 **kargs),则我们可以得到两个额外的方法(get_params() 以及 set_params()),这两个方法对于自动化的超参数调整会非常有帮助。下面是一个简单的 transformer 类的例子,我们在这个例子中添加了一个组合属性(在前面的文章中有提到):
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, y=None):
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]
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)
在这个例子中,transformer 有一个超参数 add_bedrooms_per_room,默认设置为True(一般最好是提供默认的超参数)。这个超参数可以让我们更易于判断在添加参数前后,模型的精准度。同样,我们也可以在超参数中指定排除一些我们不太确定的数据、做一些数据准备工作。这样的编程方式也会帮助我们更容易去调试一些属性组合,并节省我们的时间。
特征缩放
在准备数据中,一个非常重要的转换是特征缩放。大部分机器学习算法都无法正常处理好取值范围不同的数值型属性。在这个例子中,total_rooms 的范围从 2 到 39320,但是median_income 的范围仅为0 到 15。在此需要注意的是,对label 属性做特征归一是没必要的。
有两种常用的方法用于将所有属性映射到同样的取值范围内:最小-最大缩放(min-max scaling) 以及标准化(standardization)。
Min-max scaling 的方法很简单:将数值移动并缩放到0 到 1的范围内。将数值先减去最小值,然后除以(最大值-最小值)。sklearn提供了一个MinMaxScaler的transformer 实现它,它提供了一个feature_range 的超参数,可以让我们将结果映射到这个范围内(如果不想使用默认的0 – 1 的范围的话)。
标准化(standardization)的方法比较不一样:首先使用数值减去平均值(所以标准化后的值的平均数总为0),然后除以标准差,所以最后的分布会有单位方差(unit-variance)。不像min-max scaling, standardization的结果并不会被限定到一个特定取值范围内(如0 到 1),这个在一些算法中(如神经网络,输入值的取值范围一般限定为0 到 1)会成为一个问题。但是 min-max scaling 会受到最小值与最大值变动的影响,例如假设出现了一个异常值,它的 median_income 为 100,且仅有此一条,则其他所有数据的范围(为 0 到 15)会落在0 到 0.15 之间,仅有一条数据会落在 1。而standardization 基本就不会受到这种最大值与最小值变动而产生的影响。sklearn 提供了一个StandardSclaer 的transformer,用于做standardization。
Transformation 管道
我们可以看到,现在我们有很多的数据转换步骤去处理,并且还要按照特定的顺序进行处理。对此,sklearn 提供了一个Pipeline 的类,用于处理连续的transformations。下面是一个针对数值型属性的小的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 构造器传入了一组 name/estimator pair,作为顺序执行的步骤。除了最后的 estimator外,其他的都必须为transformers(也就是说,它们都必须包含一个 fit_transform() 方法)。这里的name可以指定任意字唯一字符串(不能包含双下划线):这些后续会被用于更方便地优化超参数。
当我们调用了pipeline的fit() 方法后,它会按顺序调用transformers 的 fit_transform() 方法,并将前一个的输出作为下一个的输入,直到运行到最后的estimator,在最后一步仅调用fit() 方法。
在目前为止,我们已经分别处理了类别型的列以及数值型的列。但是如果能用单独的一个transformer 去同时处理所有列的话,肯定会更方便一些。在0.20 版本之后,sklearn引入了ColumnTransformer,用于这个需求:
from sklearn.compose import ColumnTransformer
num_attributes = list(housing_num)
cat_attributes = ['ocean_proximity']
full_pipeline = ColumnTransformer([
('num', num_pipeline, num_attributes),
('cat', OneHotEncoder(), cat_attributes),
])
housing_prepared = full_pipeline.fit_transform(housing)
如上代码所示,首先我们将数值型列名以及列别型列名分别放入两个list,然后构造一个ColumnTransformer 对象。在构造这个对象时,需要传入一组元组,每个元组包含一个名称,一个transformer,以及一组transformer 会应用到列名。最后将这个ColumnTransformer 应用到housing 数据集上。
需要注意的是,OneHotEncoder 返回的是一个稀疏矩阵,而num_pipeline 返回的是一个密集矩阵。但返回结果包含密集矩阵以及稀疏矩阵时,ColumnTransformer 会推断最终矩阵的疏密度(也就是包含非0元素的比例),如果疏密度小于给定的阈值(默认是0.3,sparse_threshold=0.3),则返回一个稀疏矩阵。在这个例子中,它返回的是一个密集矩阵。至此,我们已经对数据做了预处理。
接下来我们会选择机器学习模型并进行训练。