1 引言
预处理操作是机器学习整个周期中必不可少的一个过程,也是最能快速改善模型性能的一个过程,往往稍微转换一下特征属性的形态,就能得到性能的极大提升。当然,数据预处理绝对也是耗时最长的一个过程,这一过程不仅要求洞悉整个数据集结构分布,还要探查每一个特征属性细节情况,并作出应对处理,使数据以最适合的状态传输给模型。
针对预处理操作,sklearn中提供了许多模块工具,灵活使用工具可以让数据预处理轻松很多。
本文简要介绍数据预处理中的一些主要方法,并结合sklearn中提供的模块进行实践。
2 无量纲化
对于大部分机器学习任务而言,对原始数据进行无量纲化是是建模前的必不可少的一个环节。通过无量纲化,可以消除量纲不一致对模型造成的不良影响。标准化和归一化是最为常见的两种无量纲化方法,下面分别展开介绍这两种方法。
2.1 标准化
标准化对数据的分布的进行转换,使其符合某种分布(一般指正态分布)的一种特征变换。一般而言,标准化都是指通过z-score的方法将数据转换为服从均值为0,标准差为1的标准正态分布数据,通过如下公式进行转换:
(1)sklearn.preprocessing.scale方法实现标准化
from sklearn import preprocessing
import numpy as np
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
X_scaled = preprocessing.scale(X_train)
再次查看X_train各列,我们会发现,均值和方差都已经标准化:
X_train.mean(axis=0)
array([1. , 0. , 0.33333333])
X_scaled.std(axis=0)
array([1., 1., 1.])
(2)sklearn.preprocessing.StandardScaler类实现归一化
除了scale方法外,在sklearn.preprocessing模块中还提供有一个专门的类用于实现标准化:StandardScaler,StandardScaler类会自动计算实例化类时传入的训练集的均值、标准差,并将这些信息保留,这也就意味着,对训练集的标准化方式可以复用,例如对测试集和预测样本进行同样的标准化。所以,一般来说,更加建议使用StandardScaler类来实现标准化。
# 传入一个训练集,实例化StandarScaler类
scaler = preprocessing.StandardScaler()
scaler.fit(X_train) # 收集标准化信息,均值,标准差
StandardScaler(copy=True, with_mean=True, with_std=True)
scaler.mean_ # 查看均值
array([1. , 0. , 0.33333333])
scaler.scale_ # 查看标准差
array([0.81649658, 0.81649658, 1.24721913])
创建StandarScaler类实例后,需要通过类中的transform方法对X-train进行标准化:
scaler.transform(X_train)
array([[ 0. , -1.22474487, 1.33630621], [ 1.22474487, 0. , -0.26726124], [-1.22474487, 1.22474487, -1.06904497]])
StandardScaler类中还提供有一个fit_transform方法,这个方法合并了fit和transform两个方法的功能,同时根据传入的数据集收集标准化信息,并将标准化方案应用于传入的训练集:
scaler = preprocessing.StandardScaler()
x_train = scaler.fit_transform(X_train)
x_train
array([[ 0. , -1.22474487, 1.33630621], [ 1.22474487, 0. , -0.26726124], [-1.22474487, 1.22474487, -1.06904497]])
假设现在有一个测试样本,那么,也可以通过transform方法将标准化方案应用于测试样本上:
X_test = [[-1., 1., 0.]]
scaler.transform(X_test)
array([[-2.44948974, 1.22474487, -0.26726124]])
2.2 归一化
归一化是指对数据的数值范围进行特定缩放,但不改变其数据分布的一种线性特征变换。大多数场景下,归一化都是将数据缩放到[0,1]区间范围内,计算公式如下:
(1)sklearn.preprocessing.minmax_scale方法实现归一化。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
X_train = preprocessing.minmax_scale(X_train)
X_train
array([[0.5 , 0. , 1. ], [1. , 0.5 , 0.33333333], [0. , 1. , 0. ]])
(2)sklearn.preprocessing.MinMaxScaler类实现归一化。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
min_max_scaler = preprocessing.MinMaxScaler()
X_train_minmax = min_max_scaler.fit_transform(X_train)
X_train_minmax
array([[0.5 , 0. , 1. ], [1. , 0.5 , 0.33333333], [0. , 1. , 0. ]])
使用训练好的min_max_scaler对新的测试样本进行归一化:
X_test = np.array([[-3., -1., 4.]])
X_test_minmax = min_max_scaler.transform(X_test)
X_test_minmax
array([[-1.5 , 0. , 1.66666667]])
我们知道,归一化是将特征属性值缩放到[0,1]范围,但在某些特殊的场景下,我们需要将特征属性缩放到其他范围,MinMaxScaler类通过feature_range参数也提供了这一功能,feature_range参数接受一个元组作为参数,默认值为(0,1)。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
min_max_scaler = preprocessing.MinMaxScaler(feature_range=(10,20))
X_train_minmax = min_max_scaler.fit_transform(X_train)
X_train_minmax
array([[15. , 10. , 20. ], [20. , 15. , 13.33333333], [10. , 20. , 10. ]])
(3)sklearn.preprocessing.MaxAbsScaler类实现归一化
MaxAbsScaler是专门为稀疏数据做归一化设计的,通过特征值除以整个特征集合最大绝对值实现,最终将数据投影到[-1, 1]范围内,对原来取值为0的数据并不会做出变换,所以不会影响数据的稀疏性。
最后来总结一下标准化和归一化。
标准化是依照特征矩阵的列处理数据,其通过求z-score的方法,转换为标准正态分布,和整体样本分布相关,每个样本点都能对标准化产生影响,而归一化是将样本的特征值转换到同一量纲下把数据映射到指定区间内,仅由变量的极值决定,所以对异常值较为敏感。标准化和归一化都是一种线性变换,都是对向量x按照比例压缩再进行平移。无论是标准化还是归一化,都可以将数据无量纲化,消除不同量纲对结果的影响,同时都可以加过模型的收敛速度。 标准化与归一化之间如何选择呢?
大多数机器学习算法中,会选择StandardScaler来进行特征缩放,因为MinMaxScaler对异常值非常敏感。在PCA,聚类,逻辑回归,支持向量机,神经网络这些算法中,StandardScaler往往是最好的选择。
MinMaxScaler在不涉及距离度量、梯度、协方差计算以及数据需要被压缩到特定区间时使用广泛,比如数字图像处理中量化像素强度时,都会使用MinMaxScaler将数据压缩于[0,1]区间之中。若是归一化时需要保留数据的稀疏性,则可以使用MaxAbscaler归一化。 在大多数情况下,建议先试试看StandardScaler,效果不好换MinMaxScaler。
另外,这里再提一下正则化(Normalization),很多资料把正则化与归一化、标准化放到一起讨论,虽然正则化也是数据预处理方法的一种,但我并不认为正则化是无量纲化方法。正则化通过某个特征值除以整个样本所有特征值的范数计算,使得整个样本范数为1,通常在文本分类和聚类中使用较多。sklearn中提供preprocessing.normalize方法和preprocessing.Normalizer类实现:
X_train = np.array([[ 1., -2., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
max_abs_scaler = preprocessing.MaxAbsScaler()
X_train_max_abs = max_abs_scaler.fit_transform(X_train)
X_train_max_abs
array([[ 0.5, -1. , 1. ], [ 1. , 0. , 0. ], [ 0. , 0.5, -0.5]])
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
nor = preprocessing.Normalizer()
X_train_nor = nor.fit_transform(X_train)
X_train_nor
array([[ 0.40824829, -0.40824829, 0.81649658], [ 1. , 0. , 0. ], [ 0. , 0.70710678, -0.70710678]])
3 缺失值处理
由于各种各样的原因,我们所面对的数据经常是有所缺失的,然而sklearn中实现的各个算法都假设数据没有缺失为前提,如果直接用缺失数据跑算法影响最终结果不说,也容易产生各种异常,所以在数据预处理阶段,对缺失值进行处理是很有必要的。对于缺失值处理,直接删除包含缺失值的特征属性或者样本是最简单的方法,但是这种方法却也将其他部分信息抛弃,在很多情况下,特别是数据样本不多、数据价值大时,未免得不偿失。在sklearn中,提供了诸多其他处理缺失值的方案,例如以均值、中位数、众数亦或者是指定值填充缺失值等,这些方案都在sklearn.impute模块中提供的SimpleImputer类中实现,SimpleImputer类参数如下:
import numpy as np
from sklearn.impute import SimpleImputer
imp = SimpleImputer(missing_values=np.nan, strategy='mean') # 指定缺失值为nan,以均值填充
imp.fit([[1, 2], [np.nan, 3], [7, 6]])
X = [[np.nan, 2], [6, np.nan], [7, 6]]
imp.transform(X)
array([[4. , 2. ], [6. , 3.66666667], [7. , 6. ]])
imp = SimpleImputer(missing_values=0, strategy='constant', fill_value=1) # 指定缺失值为0,指定以常数1填充
imp.fit([[5, 2], [4, 0], [7, 6]])
X = [[0, 2], [6, 0], [7, 6]]
imp.transform(X)
array([[1, 2], [6, 1], [7, 6]])
4 离散型特征属性处理
很多时候,我们所要处理的特征属性未必是连续型的,也可能是离散型,以衣服为例,款式(男款、女款),大小(X、XL、XXL),颜色(绿色、红色、白色),都是离散型特征属性。对于这类离散型特征属性,需要编码之后才能用来建模。离散型特征属性值可以分为两种:
(1)数字编码
整数编码是指对离散型属性以整数来标识,例如色泽这一特征中,以整数“0”标识“男款”,整数“1”标识“女款”。sklearn中提供了LabelEncoder和OrdinalEncoder两个类用以实现对数据的不同取值以数字标识。LabelEncoder和OrdinalEncoder会自动根据提供的训练数据进行统计,分别对每个特征属性从0开始编码,不同的是,LabelEncoder类一次只能对一个一维数组(一个特征属性)编码,而OrdinalEncoder能同时对各个特征属性编码:
enc = preprocessing.LabelEncoder() # 只能接受一个一维数组
X = ['红色', '白色', '绿色']
enc.fit(X)
X_ = enc.transform(X)
X_
array([1, 0, 2])
enc = preprocessing.OrdinalEncoder() # 可以同时多通过特征属性编码
X = [['女款', 'X', '绿色'], ['女款', 'XL', '红色'], ['男款', 'XXL', '白色']]
enc.fit(X)
X_ = enc.transform(X)
X_
array([[0., 0., 2.], [0., 1., 1.], [1., 2., 0.]])
enc.inverse_transform(X_) # 可以使用inverse_transform逆转
array([['女款', 'X', '绿色'], ['女款', 'XL', '红色'], ['男款', 'XXL', '白色']], dtype=object)
但在很多模型中,使用整数编码并不合理,特别是在聚类这类需要计算空间距离的算法模型。仔细观察上面编码,颜色这一属性有三种取值(绿色、红色、白色),分别以(2,1,0)表示,颜色之间是没有大小意义的,但以三个数字表示后,就赋予了三种属性值大小上的意义,且在算法计算距离时,绿色(2)到白色(0)的距离比红色(1)到白色(0)大,这是不合理的。
对于这类取值没有大小意义的离散型特征属性,有一种更加合适的编码方式:独热编码。
(2)独热编码
独热编码即 One-Hot 编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候,其中只有一位有效。sklearn中提供了OneHotEncoder类用以实现对数据的独热编码:
enc = preprocessing.OneHotEncoder()
X = [['女款', 'X', '绿色'], ['女款', 'XL', '红色'], ['男款', 'XXL', '白色']]
enc.fit(X)
enc.transform([['男款', 'XL', '绿色']]).toarray()
array([[0., 1., 0., 1., 0., 0., 0., 1.]])
在上述输出结果中,特征属性有多少种取值经过独热编码后就扩展为多少个维度,以款式为例,经过独热编码后,扩展为两个维度,第一维中1表示是女款,0表示非女款。
在实例化OneHotEncoder类时,可以通过categories参数指定各特征属性的所有类别,这样即使存在训练数据中没有出现的类别,在后续出现时也能正确编码:
style = ['女款', '男款']
size = [ 'X','XL','XXL']
color = ['绿色','红色','白色']
enc = preprocessing.OneHotEncoder(categories=[style, size, color])
X = [['女款', 'X', '绿色'], ['女款', 'XL', '红色']]
enc.fit(X)
enc.transform(X).toarray()
array([[1., 0., 1., 0., 0., 1., 0., 0.], [1., 0., 0., 1., 0., 0., 1., 0.]])
enc.transform([['男款', 'XXL', '白色']]).toarray() # 男款,XXL,白色三个属性值均为在X中出现,但是可以正确编码
array([[0., 1., 0., 0., 1., 0., 0., 1.]])
创建好OneHotEncoder类实例并通过训练数据后,就可以对后续的数据进行独热编码,但是,有时候却不可避免地出现categories和训练数据集中都未出现过的取值,这时候继续编码就会抛出异常。为了防止这一情况发生,我们可以在创建OneHotEncoder实例时,传入参数handle_unknown='ignore',这样的话,如果出现某一特征属性值未在categories和训练数据集中出现过,通过热独编码时,该特征属性多对应的维度都会以0来填充。
style = ['女款', '男款']
size = [ 'X','XL']
color = ['绿色','红色']
enc = preprocessing.OneHotEncoder(categories=[style, size, color],handle_unknown='ignore')
X = [['女款', 'X', '绿色'], ['女款', 'XL', '红色']]
enc.fit(X)
enc.transform([['男款', 'XXL', '白色']]).toarray() # XXL, 白色在categories和X中都为出现过
array([[0., 1., 0., 0., 0., 0.]])
独热编码解决了离散型属性难以有效刻画的问,在一定程度上也起到了扩充特征的作用,它的值只有0和1,不同的类型存储在垂直的空间。当类别的数量很多时,特征空间会变得非常大。在这种情况下,一般可以用PCA来减少维度。而且one hot encoding+PCA这种组合在实际中也非常有用。
5 连续型特征属性离散化
有时候,将连续型特征属性离散化能够显著提高模型的表现力。连续型特征属性离散化包括二值化和分段等方法。
(1)二值化
二值化是指通过一个阈值对属性值进行划分,当小于这个阈值时,将值映射为0,大于阈值时映射为1。二值化是对文本计数数据的常见操作,分析人员可以决定仅考虑某种现象的存在与否。它还可以用作考虑布尔随机变量的估计器的预处理步骤(例如,使用贝叶斯设置中的伯努利分布建模)。
sklearn中提供了Binarizer实现二值化,默认阈值为0,也就是将非正数映射为0,将正数映射为1。也可以在实例化时通过参数threshold,设置其他阈值。
X = [[ 1., -1., 2.],
[ 2., -4., 0.],
[ 3., 2., -1.]]
binarizer = preprocessing.Binarizer().fit(X)
binarizer
Binarizer(copy=True, threshold=0.0)
binarizer.transform(X)
array([[1., 0., 1.], [1., 0., 0.], [1., 1., 0.]])
binarizer = preprocessing.Binarizer(threshold=1.5).fit(X)
binarizer.transform(X)
array([[0., 0., 1.], [1., 0., 0.], [1., 1., 0.]])
(2)分段
二值化只能将数据映射为两个值,分段可以对数据进行排序后分为多个部分然后进行编码。在sklearn中,分段操作通过KBinsDiscretizer类进行。KBinsDiscretizer类有三个重要参数,必须了解一下:
X = [[-2, 1, -4, -1],
[-1, 2, -3, -0.5],
[ 0, 3, -2, 0.5],
[ 1, 4, -1, 2]]
est = preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform').fit(X)
est.transform(X)
array([[0., 0., 0., 0.], [1., 1., 1., 0.], [2., 2., 2., 1.], [2., 2., 2., 2.]])