标准化、规范化、二值化等多种机器学习数据预处理方法

策略分享
数据预处理
标签: #<Tag:0x00007f61f3e28568> #<Tag:0x00007f61f3e282c0>

(iQuant) #1

预处理数据

数据预处理在众多深度学习算法中都起着重要作用,实际上,对数据进行适当处理后,很多算法能够发挥最佳效果。然而面对各种各样的数据,很多时候我们不知道怎么样才能针对性进行处理。本文介绍了Python下的机器学习工具scikit-learn。其中,“sklearn.preprocessing”模块提供了几种常见的函数和转换类,把原始的特征向量变得更适合估计器使用。

克隆策略

1. 标准化,即减去平均值再用方差缩放比例

在scikit-learn中,数据的标准化很多机器学习估计器的常见要求;如果单个特征看起来不符合标准正态分布(平均值为0,方差为1)的话,数据之后可能会有很差的表现。

实际上我们通常忽略分布的具体形态,数据转换仅指,减去每个特征的平均值,再除以他们的标准差。

例如,学习算法中目标函数的很多成分都假设,所有的特征都是围绕着0的,并且拥有相同算数级别的方差(比如SVM中的RBF核,以及线性模型中的l1,l2正则化)。如果一个特征的方差级别高于其他的特征,它会在目标函数中占据主导地位,并使得估计器不能按照预期很好地从其他特征中学习。

scale函数就提供了一个快速且简便的方法,对一个数组型数据集执行这个操作:

In [2]:
from sklearn import preprocessing
import numpy as np
X = np.array([[ 1., -1.,  2.],
               [ 2.,  0.,  0.],
               [ 0.,  1., -1.]])
X_scaled = preprocessing.scale(X)

X_scaled                                          
Out[2]:
array([[ 0.        , -1.22474487,  1.33630621],
       [ 1.22474487,  0.        , -0.26726124],
       [-1.22474487,  1.22474487, -1.06904497]])

调整后的数据平均值为0,方差为1:

In [3]:
X_scaled.mean(axis=0)
Out[3]:
array([ 0.,  0.,  0.])
In [4]:
X_scaled.std(axis=0)
Out[4]:
array([ 1.,  1.,  1.])

preprocessing模块还提供了一个类"StandardScaler",它能计算训练集的平均值和标准差,以便之后对测试集进行相同的转换。因此,这个类适合用于sklearn.pipeline.Pipeline的前几个步骤:

In [5]:
scaler = preprocessing.StandardScaler().fit(X)
scaler
Out[5]:
StandardScaler(copy=True, with_mean=True, with_std=True)
In [6]:
scaler.mean_ 
Out[6]:
array([ 1.        ,  0.        ,  0.33333333])
In [7]:
scaler.scale_ 
Out[7]:
array([ 0.81649658,  0.81649658,  1.24721913])
In [8]:
scaler.transform(X)  
Out[8]:
array([[ 0.        , -1.22474487,  1.33630621],
       [ 1.22474487,  0.        , -0.26726124],
       [-1.22474487,  1.22474487, -1.06904497]])

这个scaler之后能对新的数据进行,跟先前对训练集一样的操作:

In [9]:
scaler.transform([[-1.,  1., 0.]])    
Out[9]:
array([[-2.44948974,  1.22474487, -0.26726124]])

此外,也可以通过在创建StandardScaler时增加with_mean=False或者with_std=False语句,来阻止集中化或缩放比例。

1.1 把特征缩放到一个范围内

另一个标准化的操作,是把特征缩放到一个最小值与最大值之间(通常是0到1),或者是把每个特征的最大绝对值变到1。这分别可以通过MinMaxScaler或者MaxAbsScaler实现。

使用这种转换方式是为了增加强健性,来解决特征的标准差非常小的问题,以及在稀疏数据中保留0元素。

以下是一个把数据矩阵缩放到[0,1]范围内的一个例子:

In [10]:
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
Out[10]:
array([[ 0.5       ,  0.        ,  1.        ],
       [ 1.        ,  0.5       ,  0.33333333],
       [ 0.        ,  1.        ,  0.        ]])

相同的转换器可以用到新的测试集上:相同的缩放、平移操作会与之前对训练数据的操作保持一致:

In [11]:
X_test = np.array([[ -3., -1.,  4.]])
X_test_minmax = min_max_scaler.transform(X_test)
X_test_minmax
Out[11]:
array([[-1.5       ,  0.        ,  1.66666667]])

我们也可以找出从训练数据中学到的转换的具体特性:

In [12]:
min_max_scaler.scale_  
Out[12]:
array([ 0.5       ,  0.5       ,  0.33333333])
In [13]:
min_max_scaler.min_     
Out[13]:
array([ 0.        ,  0.5       ,  0.33333333])

如果MinMaxScaler被给予一个明确的feature_range=(min,max),完整的公式是:

X_std = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

X_scaled = X_std / (max - min) + min

MaxAbsScaler的功能很类似,但是它把训练数据缩放到了[-1,1]范围内。这对已经围绕着0的数据或者稀疏数据来说是很有意义的。

这里用了这个scaler把之前例子的数据进行了转换:

In [14]:
X_train = np.array([[ 1., -1.,  2.],
                     [ 2.,  0.,  0.],
                     [ 0.,  1., -1.]])
max_abs_scaler = preprocessing.MaxAbsScaler()
X_train_maxabs = max_abs_scaler.fit_transform(X_train)
X_train_maxabs                # doctest +NORMALIZE_WHITESPACE^
Out[14]:
array([[ 0.5, -1. ,  1. ],
       [ 1. ,  0. ,  0. ],
       [ 0. ,  1. , -0.5]])
In [15]:
X_test = np.array([[ -3., -1.,  4.]])
X_test_maxabs = max_abs_scaler.transform(X_test)
X_test_maxabs  
Out[15]:
array([[-1.5, -1. ,  2. ]])
In [16]:
max_abs_scaler.scale_  
Out[16]:
array([ 2.,  1.,  2.])

与scale一样,这个模块也提供了比较简便的函数minmax_scale以及maxabs_scale,如果你不想创建一个对象。

1.2 转换稀疏数据

把稀疏数据集中化会破坏数据中的稀疏性结构,因此不是一个理想的做法。但是,对稀疏的输入转换测度是有道理的,特别是当特征具有不同的测度的时候。

MaxAbsScaler以及maxabs_scale是特别为转换稀疏数据设计的,并且我们建议使用他们。然而,scale和StandardScaler可以接受scipy.sparse矩阵作为输入,只要在创建时说明with_mean=False。否则会产生ValueError,因为默认的集中化会破坏稀疏性,并且会分配过多的内存从而导致运行崩溃。RobustScaler不能用于稀疏输入,但你可以对稀疏输入使用transform方法。

注意,scalers同时接受Compressed Sparse Rows以及Compressed Sparse Columns形式。其他类型的稀疏输入会被转换为Compressed Sparse Rows的形式。为了避免不必要的内存复制,建议选择CSR或者CSC的表达形式。

最后,如果集中化后的数据预期非常小,使用toarray方法把稀疏输入转换为数组是另一个选择。

1.3 转换具有异常值的数据

如果你的数据有很多异常值,使用平均值和方差来进行转换可能表现不会很好。在这些情况下,你可以使用robust_scale以及RobustScaler。他们对数据的中心和范围采用更健壮的估计。

参考资料:

关于集中化和缩放比例重要性更多的讨论:Should I normalize/standardize/resclae the data?.

1.4 集中化核矩阵

如果你有一个核矩阵来计算特征空间的点积,KernelCenterer可以转换核矩阵,使其包含特征空间的内积,移去空间中的平均值。

2. 规范化

规范化是指,对单个样本缩放比例,使其范数为1。如果你计划使用平方的形式,例如点积或者其他核来量化样本之间的相似性,这个过程是有用的。

这个假设是Vector Space Model的基础,而Vector Space Model经常用在文本分类和集群环境下。

normalize函数提供了一个快速且简便的方法,对单个数组型数据集实现这个操作,使用了了l1或者l2范数:

In [17]:
X = [[ 1., -1.,  2.],
      [ 2.,  0.,  0.],
      [ 0.,  1., -1.]]
X_normalized = preprocessing.normalize(X, norm='l2')
X_normalized                                      
Out[17]:
array([[ 0.40824829, -0.40824829,  0.81649658],
       [ 1.        ,  0.        ,  0.        ],
       [ 0.        ,  0.70710678, -0.70710678]])

Preprocessing模块还提供了一个类Normalizer来执行相同的操作,它使用了Transformer API(即使fit方法在这个情况下是没有用的:这个类没有状态,因为这个操作对样本独立进行处理)。

因此,这个类适用于sklearn.pipeline.Pipeline的前几个步骤:

In [18]:
normalizer = preprocessing.Normalizer().fit(X)  # fit does nothing
normalizer
Out[18]:
Normalizer(copy=True, norm='l2')

这个normalizer和其他transformer一样,可以用到样本向量中。

In [19]:
normalizer.transform(X)        
Out[19]:
array([[ 0.40824829, -0.40824829,  0.81649658],
       [ 1.        ,  0.        ,  0.        ],
       [ 0.        ,  0.70710678, -0.70710678]])
In [20]:
normalizer.transform([[-1.,  1., 0.]])           
Out[20]:
array([[-0.70710678,  0.70710678,  0.        ]])

稀疏输入

normalize和normalizer同时接受scipy.sparse中密集的数组型,以及稀疏的矩阵作为输入。

对于稀疏输入,数据会被转换为Compressed Sparse Rows表示形式。为了避免不必要的内存复制,建议使用CSR表示形式。

3. 二值化

3.1 特征二值化

特征二值化是指,通过设置阈值,把数值的特征转换为布尔值。这对于下游概率的估计器来说是很有用的,它假设输入的数据按照多元Bernoulli分布。例如,sklearn.neural_network.BernoulliRBM。

在加工文本过程中使用布尔型特征值是很常见的(也许是为了简化概率推理),即使规范化计数,或者TF-IDF评价的特征经常在实际中表现的更好。

至于Normalizer,Binarizer类在sklearn.pipeline.Pipeline的前几个步骤会被用到。fit方法没有什么用,因为每个样本都被独立处理:

In [21]:
X = [[ 1., -1.,  2.],
      [ 2.,  0.,  0.],
      [ 0.,  1., -1.]]
binarizer = preprocessing.Binarizer().fit(X)  # fit does nothing
binarizer
Out[21]:
Binarizer(copy=True, threshold=0.0)
In [22]:
binarizer.transform(X)
Out[22]:
array([[ 1.,  0.,  1.],
       [ 1.,  0.,  0.],
       [ 0.,  1.,  0.]])

binarizer的阈值可以进行调整:

In [23]:
binarizer = preprocessing.Binarizer(threshold=1.1)
binarizer.transform(X)
Out[23]:
array([[ 0.,  0.,  1.],
       [ 1.,  0.,  0.],
       [ 0.,  0.,  0.]])

至于StandardScaler以及Normalizer类,预处理模块提供了一个伴随函数binarize,它在transformer API不必要时会被使用。

稀疏输入

binarize和Binarizer同时接受scipy.sparse中密集的数组型,以及稀疏的矩阵作为输入。

对于稀疏输入,数据会被转换为Compressed Sparse Rows表示形式。为了避免不必要的内存复制,建议使用CSR表示形式。

4. 编码类型特征

经常,特征不是通过连续值表达的,而是通过类型。例如,一个人可以拥有特征["男人", "女人"], ["来自欧洲", "来自美国", "来自亚洲"], ["使用 Firefox", "使用 Chrome", "使用 Safari", "使用 Internet Explorer"]. 这些特征可以被有效编码为整数,例如,["男人", "来自美国", "使用 Internet Explorer"]可以表示为[0, 1, 3],["女人", "来自亚国", "使用 Chrome"]可以表示为[1, 2, 1]。

这样的整数表达方式不能直接被用于scikit-learn估计器中,因为估计器会把它们当做连续的输入,把这些类别解释为有顺序的,而这并不是我们所期望的。

一种把类型特征转换为可以被scikit-learn估计器使用的方式是,使用一种one-of-K,或one-hot编码,它们在OneHotEncoder中被用到。这个估计器把每个,有m个可能值的类型特征转换为m个布尔值,只有一个值为1.

继续之前的例子:

In [24]:
enc = preprocessing.OneHotEncoder()
enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]])  
Out[24]:
OneHotEncoder(categorical_features='all', dtype=<class 'numpy.float64'>,
       handle_unknown='error', n_values='auto', sparse=True)
In [25]:
enc.transform([[0, 1, 3]]).toarray()
Out[25]:
array([[ 1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.]])

默认地,每个特征能取多少值会自动地从数据集中推测出来。也可以通过n_values明确的说明这个参数。在这个例子中,我们的数据集有两个性别,三个州,以及四个网页浏览器。之后,我们用估计器来学习,转换每个数据点。结果是,前两个数字是性别的编码,之后三个数字是州的编码,最后四个数字是网页浏览器的编码。

注意,如果训练数据有可能丢失了一些类型特征,我们必须明确设置n_values。例如,

In [26]:
enc = preprocessing.OneHotEncoder(n_values=[2, 3, 4])
# 注意第二个和第三个类型值有缺失值
# 特征
enc.fit([[1, 2, 3], [0, 2, 0]])  
Out[26]:
OneHotEncoder(categorical_features='all', dtype=<class 'numpy.float64'>,
       handle_unknown='error', n_values=[2, 3, 4], sparse=True)
In [27]:
enc.transform([[1, 0, 0]]).toarray()
Out[27]:
array([[ 0.,  1.,  1.,  0.,  0.,  1.,  0.,  0.,  0.]])

用字典表示,而不是用整数表示的类型特征,可以参照Loading features from dicts.

5. 缺失值的处理

由于各种各样的原因,很多现实生活中的数据集都会有缺失值,经常会被编码为空白,NaNs或其他占位符。然而,这些数据集与scikit-learn估计器并不兼容,因为估计器假设数组中的所有值都是数值型的,并且都有意义。处理不兼容的数据集一个基本的方法是,丢弃包含缺失值的整行或整列。但是,一些有价值的数据可能会因此丢失,尽管它们不兼容。一个更好的办法是,对缺失值进行其他处理,从数据的已有部分来推测缺失值。

Imputer类提供了处理缺失值的一些基本方法,它把缺失值用它所在行或列的平均值,中位数或众数来代替。这个类同时也允许不同的缺失值编码方式。

下面这个例子把缺失值(编码为np.nan)用它所在列的平均值代替:

In [28]:
import numpy as np
from sklearn.preprocessing import Imputer
imp = Imputer(missing_values='NaN', strategy='mean', axis=0)
imp.fit([[1, 2], [np.nan, 3], [7, 6]])
Imputer(axis=0, copy=True, missing_values='NaN', strategy='mean', verbose=0)
X = [[np.nan, 2], [6, np.nan], [7, 6]]
print(imp.transform(X))    
[[ 4.          2.        ]
 [ 6.          3.66666667]
 [ 7.          6.        ]]

Imputer也支持稀疏矩阵:

In [29]:
import scipy.sparse as sp
X = sp.csc_matrix([[1, 2], [0, 3], [7, 6]])
imp = Imputer(missing_values=0, strategy='mean', axis=0)
imp.fit(X)
Out[29]:
Imputer(axis=0, copy=True, missing_values=0, strategy='mean', verbose=0)
In [30]:
X_test = sp.csc_matrix([[0, 2], [6, 0], [7, 6]])
print(imp.transform(X_test))  
[[ 4.          2.        ]
 [ 6.          3.66666667]
 [ 7.          6.        ]]

注意,在这里,缺失值被编码为0,所以被隐性地储存在了矩阵中。当缺失值个数比观察值多很多的时候,这个形式适用。

Imputer能被用到Pipeline中,用来建立一个支持处理缺失值的复合估计器。参考Imputing missing values before builiding an estimator.

6. 生成多项式特征

经常,通过考虑输入数据的非线性特征来增加模型的复杂性是有用的。一个简单且常用的方法是使用多项式特征,它能得到特征的高次项以及交互项,通过PolynomialFeatures实现:

In [31]:
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
X = np.arange(6).reshape(3, 2)
X
Out[31]:
array([[0, 1],
       [2, 3],
       [4, 5]])
In [32]:
poly = PolynomialFeatures(2)
poly.fit_transform(X)                             
Out[32]:
array([[  1.,   0.,   1.,   0.,   0.,   1.],
       [  1.,   2.,   3.,   4.,   6.,   9.],
       [  1.,   4.,   5.,  16.,  20.,  25.]])

在这里,X的特征从(X1,X2)被转换为了(1, X_1, X_2, X_1^2, X_1X_2, X_2^2).

有时候,只有特征之间的交互项是被要求的,它可以通过设置interaction_only=True来实现:

In [33]:
X = np.arange(9).reshape(3, 3)
X      
Out[33]:
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
In [34]:
poly = PolynomialFeatures(degree=3, interaction_only=True)
poly.fit_transform(X)  
Out[34]:
array([[   1.,    0.,    1.,    2.,    0.,    0.,    2.,    0.],
       [   1.,    3.,    4.,    5.,   12.,   15.,   20.,   60.],
       [   1.,    6.,    7.,    8.,   42.,   48.,   56.,  336.]])

在这里,X的特征从(X_1, X_2, X_3)被转换为了(1, X_1, X_2, X_3, X_1X_2, X_1X_3, X_2X_3, X_1X_2X_3).

注意,在核方法(如sklearn.svm.SVC,sklearn.decomposition.KernelPCA)中使用多项式核函数时,多项式特征被隐性使用。

7. 定制转换器

经常,你想要把一个已经存在的Python函数转换为一个转换器,用来协助数据清理或处理。你可以通过FunctionTransformer来实现。例如,为了生成一个可以进行log变化的转换器,可以:

In [35]:
import numpy as np
from sklearn.preprocessing import FunctionTransformer
transformer = FunctionTransformer(np.log1p)
X = np.array([[0, 1], [2, 3]])
transformer.transform(X)
Out[35]:
array([[ 0.        ,  0.69314718],
       [ 1.09861229,  1.38629436]])

使用FunctionTransformer来定制特征选择的完整示例代码,可见Using FunctionTransformer to select columns.


【宽客学院】因子预处理
社区干货与精选整理(持续更新中...)