创建一个完整的机器学习工程(三)- 数据处理

这几篇博客将通过对加州房价模型的建立,介绍如何搭建一个完整的机器学习工程。
本文将介绍在数据处理时,针对不同的问题分别采用何种方式去解决。
过程中如有任何错误,请各位指正与包涵。

文章的内容源自’Hands-On Machine Learning with Scikit-Learn and TensorFlow’一书第二章

数据(housing.cvs)来源:https://github.com/ageron/handson-ml/tree/master/datasets/housing
上一篇内容介绍了数据划分及预处理的方法,详情请见创建一个完整的机器学习工程(二)数据准备
本内容基于python进行开发,将用到的package包括sklearn, pandas, numpy,请先搭配好程序运行环境

一、问题回顾

在前两篇文章中,通过我们的观察,可以发现数据具有如下几个问题:

  • total_bedrooms属性有数据缺失
  • house_median_age, median_house_value, median_income几种属性有上限帽
  • house_hold, population, total_bedrooms, total_bedrooms几种属性有重尾现象
  • 不同属性间,数值的数量级与分布差异性很大
  • ocean_proximity作为非数值属性与房价关联性较强

因此在进行数据处理时,需要有针对性的解决这些问题。
其中重尾现象,可通过上篇文章中属性结合的方式,降低其影响。接下来,我们将重点讨论几种处理数据的方法,降低其他问题的影响。

二、数据处理

1. 数据清理

针对有缺失的数据,通常有3种可选的方式:删除相关属性、删除相关行、用其他值替换

(1)删除相关属性

虽然目前的数据只有总卧室数有缺失,但若将这一属性从数据集中全部删去,则对模型的准确性有极大的风险。同时,若新数据在别的属性上有缺失,又会面临新的问题。所以这种方式一般不推荐使用。

(2)删除相关行

实现该步骤的方式很简便,利用dropna(subset=['total_bedrooms'])即可。但若是缺失的数据较多,这么做容易使最后得到的模型欠拟合。所以这种方式一般也不推荐使用。

(3)其他值替换

不同类型的属性值,可以根据需求,用0、平均值、中位数等方式对缺失的数据进行替换。在此案例中,倾向于用该属性值的中位数来替换。
利用sklearn.impute模块中的SimpleImputer(stratedgy='median')方法可以实现这一过程。
值得注意的是,该方法要求待处理的数据为纯数值数据。所以,我们需要先创建一个新变量,来保存删掉属性’ocean_proximity’后的数据。

from sklearn.impute import SimpleImputer
housing_num = housing.drop(labels='ocean_proximity', axis=1)
imputer = SimpleImputer(strategy='median')
imputer.fit(housing_num)
# print(imputer.statistics_)
X = imputer.transform(housing_num)

注意

  • 代码中的housing,是之前分层抽样产生的训练集,而不是原始数据。详情请见上篇文章数据准备,这里不再赘述。
  • 可以调用statistics_,得到每种属性的中位数值;还可以调用strategy,查看该转换器采用的策略。

2. 标签处理

(1)Label Encoding

Scikit-Learn 提供了一个非常简便的方式将标签属性数字化,即Label Encoder
这一方法的思路是,将该属性中中不同种类的标签直接转换成从0开始的一串数字。例如此例中,将<1H OCEAN, INLAND, ISLAND, NEAR BAY, NEAR OCEAN,分别转换成0, 1, 2, 3, 4。
这一过程可通过如下代码实现

from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
housing_cat = housing['ocean_proximity']
housing_cat_encoded = encoder.fit_transform(housing_cat)

这一方式虽然简便,但有如下两个缺陷:

  1. 将属性赋予了大小关系。虽然包括此例在内的一些数据,其某属性内不同类型的标签存在优先级的关系。但是,这一方式无法处理不同标签关系同等的情况。
  2. 同时,在用此编码建立模型时,计算机会认为相邻的数字,例如1和2,代表着更紧密的关系;而差距大的数字,例如0和3,则代表差异性更大。而此例中,这样的编码与真实情况并不相符。

所以,Label Encoder的方式虽然简便,但不建议使用。

(2)One-Hot Encoding

另一种将标签属性数字化的编码方式是One-Hot Encoding
以此案例为例,这一方法的思路是,值为’<1H OCEAN’的标签,转换为10000,值为‘INLAND’的标签,转换为01000,依此类推,最后值为‘NEAR OCEAN’的标签转换为00001
这一过程可通过如下代码实现

from sklearn.preprocessing import OneHotEncoder
housing_cat = housing[['ocean_proximity']]
encoder = OneHotEncoder()
housing_cat_1hot = encoder.fit_transform(housing_cat)
  • 这里的代码与之前Label Encoding的部分略有不同,因LabelEncoder对输入参数的要求是1D array,而OneHotEncoder对输入参数的要求是2D array
    若仍旧希望输入1D array类型的参数实现one-hot编码,可通过LabelBinarizer实现,具体方式可查阅相关文档,这里不再赘述。
  • OneHotEncoder转换方法默认的返回值是SciPy的sparse matrix类型。这么做的好处是,因为one-hot编码的结果每行只有一个位置是1、其余所有位置是0,而sparse matrix仅需储存1和1出现的位置即可,从而降低了内存的占用。
  • 若希望将结果转换为numpy.array类型,可通过toarray()的方式进行转换。

3. 消除重尾

如上篇文章所述,针对此案例,可用属性结合的方式消除重尾现象。但这一方法并不适用于所有数据情况,所有针对消除重尾的方法,scikit-learn中并没有具体的模块可以完成这一步。
因此,我们需要自定义一个转换器来实现这一过程。

import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6
    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: np, y=None):
        rooms_per_household = X[:, self.rooms_ix] / X[:, self.household_ix]
        population_per_household = X[:, self.population_ix] / X[:, self.household_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, self.bedrooms_ix] / X[:, self.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]
  • 该自定义转化器类中,除了构造器外,它还有两个自定义方法:fit()transform(),其中fit()方法不实施任何操作。
  • 一个转换器类除了fit和transforme方法之外,还需要有fit_transform()方法。但通过继承TransformerMixin类,此处可不用添加该方法。
    通过继承BaseEstimator类,对于此自定义类的构造器,也无需添加*args**kargs参数。
  • 此例中,transform()负责实现属性结合,且其输入参数和返回值均是numpy.array类型。
    这么做的原因是,其输入参数可能来自之前其他转换的结果、而其转换输出的结果也可能会用于之后转换器的计算。

在主程序中可通过如下的指令调用该类的实例。

attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
X = attr_adder.transform(X)
  • 设置add_bedrooms_per_room=False,使得转换后的数据仅添加家庭平均住房数人均住房数两个属性。
  • 原书中该转换器的输入参数是抽样后的训练集,但为了保持连贯性,此处我将其替换为X,即之前数据清理后得到的结果。
  • 原书中是利用values()的方式将DataFrame转换为numpy.array类型,官方则建议使用to_numpy()方法,使程序的语义化更加明确。

4. 特征缩放

在第一篇文章数据概览中,我们发现该数据还具有一个特点,即不同属性数值的数量级于分布情况各不相同。例如房屋总数分布在6到39,320,而收入则分布于0到15。
因此,尽管这一步处理不是必须的,但将各参数适当缩放,可使得最终模型的各项系数有着更加接近的数量级。
通常,有两种方法可以进行参数缩放,归一化标准化

(1)归一化

归一化,即将数据投射至0到1的范围内。
这一过程可利用sklearn.preprocessing模块中的MinMaxScaler转换器实现。并且,如果你希望其投射的范围不是0到1,也可以通过feature_range参数来更改。
但是,该方式存在一个缺陷。即如果数据统计中有异常值(可能是统计错误、也可能是个例),例如收入统计时出现了一个100,那么按此方式缩放后,所有正常值则会被缩放至0到0.15的范围内。
因此,在出现这种情况的时候,建议用标准化的方式处理数据。

(2)标准化

标准化,即将数据减去其平均值、再除以其标准差而得到一组均值为0、方差为1的新数据。
这一过程可利用sklearn.preprocessing模块中的StandardScaler转换器实现。

from sklearn.preprocessing import StandardScaler
scalar = StandardScaler()
X = scalar.fit_transform(X)

5. 管道通信模型

通过之前的分析可以看到,对于此案例训练集数据的处理,是先将其分为数值部分标签部分。其中数值部分,将先后进行清理、消除重尾、缩放的处理;标签部分将进行编码处理。
这一过程中,数据就仿佛是水流、转换器就仿佛是管道,数据先分流成两部分,其中每部分都将流经一个或多个管道。流经多个管道时,数据从前一级转换器流入,输出的结果再流入下一级转换器中。
而将所有管道按照特定的顺序,或先后、或并排进行组合,最终得到的就是管道通信模型
利用此特性,上述的处理过程可以通过如下代码集中处理和简化。

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
# data acquisition
housing, test = data_division()
housing_num = housing.drop(labels='ocean_proximity', axis=1)

# attributes acquisition
num_attribs = list(housing_num)
cat_attribs = ['ocean_proximity']

# pipeline for numbers
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('attribs_adder', CombinedAttributesAdder()),
    ('std_scaler', StandardScaler()),
])
# pipeline for all
full_pipeline = ColumnTransformer([
    ('num', num_pipeline, num_attribs),
    ('cat', OneHotEncoder(), cat_attribs),
])

housing_prepared = full_pipeline.fit_transform(housing)
  • 之前为了消除重尾而自定义的转换器,令其转换函数的输入、输出参数类型均为numpy.array,便是为了在此进行管道化处理时方便与前、后的转化器对接。
  • Pipeleine的构造器要求,除了最后一个工具可以是估算器(estimator),其余均必须是转换器(transformer)。也即除了最后一个工具外,其余所以工具必须具有fit_transform()方法。
    而该管道是否具有transfom()乃至predict()方法,则视该管道最后一个工具的类型而定。
    关于估算器、转换器以及预测器(predictor),这里不做展开,建议仔细阅读原书中关于三者详细的介绍。
  • 在调用管道的fit()方法时,将依次调用除最后一项外其余工具的fit_transfom()方法,再调用最后一项工具的fit()方法得到返回值。
    此例中处理数值与标签的两个管道,最后一项均是转换器,所以拥有fit_transform()方法。
  • 利用ColumnTransformer可将多个管道并排,以达到分流处理的目的。
    注意到此例中,两个管道的返回值类型不同,一个是numpy.array、一个是sparse matrix。ColumnTransformer将考察numpy.array中数据的稀疏程度。若其稀疏程度超过阈值(默认为0.3),则返回值的类型为sparse matrix,否则返回值类型为numpy.array

三、程序封装

将训练集数据经过上述的步骤处理后,得到的结果将被用于训练模型。因此,为了能方便后续的程序使用其结果,最好能用函数将上述的过程进行封装,以返回值的形式将结果进行返回以便后续程序调用。
以管道通信模型为例,该过程可通过如下代码实现。

def pipeline():
    # data acquisition
    train_set, test_set = data_division()
    housing = train_set.drop('median_house_value', axis=1)
    housing_num = housing.drop('ocean_proximity', axis=1)

    # attributes acquisition
    num_attribs = list(housing_num)
    cat_attribs = ['ocean_proximity']

    # pipeline for numbers
    num_pipeline = Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('attribs_adder', CombinedAttributesAdder()),
        ('std_scaler', StandardScaler()),
    ])
    # pipeline for all
    full_pipeline = ColumnTransformer([
        ('num', num_pipeline, num_attribs),
        ('cat', OneHotEncoder(), cat_attribs),
    ])

    X_train: np.ndarray = full_pipeline.fit_transform(housing)
    y_train: pd.Series = train_set['median_house_value'].copy()

    X_test = test_set.drop('median_house_value', axis=1)
    X_test: np.ndarray = full_pipeline.transform(X_test)
    y_test: pd.Series = test_set['median_house_value'].copy()

    return X_train, X_test, y_train, y_test

与之前片段展示的略有不同的是,这里部分变量名有所更改,是为了让其意义更加明确。
程序首行data_division()的定义,来自上一篇文章关于数据划分的程序,将整个数据分层抽样划分为训练集与测试集。
程序的返回值包含4个部分,X_train与y_train用来训练模型,X_test与y_test用来检测模型。
注意测试集数据也需要经过pipeline的处理,但是最后调用的是其transform()方法。

四、小结

  • 为了解决第一篇文章中发现的几个问题,利用上一篇文章中数据进行的数据划分以及进一步的准备分析,本文对每种问题的解决思路与方式进行了讨论。
  • 结合此案例数据的特点,经过取舍,决定利用属性中位数来替换缺失数据、利用one-hot编码替换非数值标签、利用属性结合来消除重尾、利用标准化进行特征缩放,最后运用管道通信模型将上述方式统一处理。
  • 为了能使最终的预测模型面对新的数据也能有好的表现,需要仅利用训练集进行本文中提到的数据处理。

本文内容源自Hands-On Machine Learning with Scikit-Learn and TensorFlow一书,是其第二章关于数据处理部分的读书笔记。文中加入了少量自己的理解,如有不正之处,望各位指正与包涵。

https://github.com/ageron/handson-ml/
这是原书作者关于此书的链接,若想获取书中的源代码和数据可访此链接获取
本书有中文版《Scikit-Learn与TensorFlow机器学习实用指南》,如有需要可自行搜索查找
本书目前最新版是第二版,相较于第一版,其在代码部分进行了适应性升级,建议选择最新版进行阅读

如作者所说Scikit-Learn’s API is remarkably well designed.,其同一模块下各类一致的设计方式、简单的接口,极大地提升了其处理数据时的效率,同时更降低了学习的难度。这一段中还有关于估算器、转换器以及预测器的介绍,强烈建议仔细阅读书中这一段关于Scikit-Learn设计的介绍。
关于本文中涉及的各转换器,书中有提及部分接口的调用,以查看使用fit()transform()方法后,该对象的某些属性。
这两部分本文中均进行了省略,对这部分感兴趣的读者可以去原书中查看。

https://github.com/ShaoboZhang/ML_Notebook/tree/master/Chapter2
可通过此链接获取文中涉及的代码完整版,代码依照是否通过pipeline实现数据处理分为process1.py与process2.py两部分
程序运行前请配置好编译环境及相关包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anycall201

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值