首页 > 代码库 > 数据预处理与特征选择

数据预处理与特征选择

数据预处理和特征选择是数据挖掘与机器学习中关注的重要问题,坊间常说:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。特征工程就是将原始数据转化为有用的特征,更好的表示预测模型处理的实际问题,提升对于未知数据的预测准确性。下图给出了特征工程包含的内容:

技术分享

本文数据预处理与特征选择的代码均采用sklearn所提供的方法,并使用sklearn中的IRIS(鸢尾花)数据集来对特征处理功能进行说明,IRIS数据集由Fisher在1936年整理,包含4个特征:Sepal.Length(花萼长度)、Sepal.Width(花萼宽度)、Petal.Length(花瓣长度)、Petal.Width(花瓣宽度)),特征值都为正浮点数,单位为厘米。目标值为鸢尾花的3个分类:Iris Setosa(山鸢尾)、Iris Versicolour(杂色鸢尾),Iris Virginica(维吉尼亚鸢尾)。导入IRIS数据集的代码如下:

from sklearn.datasets import load_iris

iris = load_iris() # 导入IRIS数据集
iris.data # 特征矩阵
iris.target # 目标向量

1. 数据预处理

1.1 归一化

我们在对数据进行分析的时候,往往会遇到单个数据的各个维度量纲不同的情况,比如对房子进行价格预测的线性回归问题中,房子的特征包括面积(平方米)、房间数(个)两个维度,采用梯度下降进行训练的过程如下图所示:

技术分享

算法在寻找最优值的时候,由于图像“细长”,所以要来回找垂直线,两个特征的取值区间相差越大,图像就越“细长”,梯度下降就越慢,还可能永远无法收敛。因此需要使用归一化的方法将特征的取值区间缩放到某个特定的范围,例如[0, 1]等,下面介绍两种方法:

1.1.1 区间缩放(Min-Max scaling)

区间缩放法将原始数据中特征的取值区间转换到[0 1]范围,归一化公式如下:

  • xnorm=x?xminxmax?xmin<script type="math/tex" id="MathJax-Element-1">x_{norm}=\frac{x-x_{min}}{x_{max}-x_{min}}</script>

该方法实现对原始数据的等比例缩放,其中x<script type="math/tex" id="MathJax-Element-2">x</script>为原始数据,xnorm<script type="math/tex" id="MathJax-Element-3">x_{norm}</script>为归一化后的数据,xmax<script type="math/tex" id="MathJax-Element-4">x_{max}</script>和xmin<script type="math/tex" id="MathJax-Element-5">x_{min}</script>分别为原始数据的最大值和最小值。使用preproccessing库的MinMaxScaler类对数据进行区间缩放的代码如下:

from sklearn.preprocessing import MinMaxScaler

#区间缩放,返回值为缩放到[0, 1]区间的数据
MinMaxScaler().fit_transform(iris.data)

1.1.2 标准化(standardization)

该方法将原始数据归一化成均值为0、方差1的数据,归一化公式如下:

  • xnorm=x?μσ<script type="math/tex" id="MathJax-Element-6">x_{norm}=\frac{x-\mu}{\sigma }</script>

其中,μ<script type="math/tex" id="MathJax-Element-7">μ</script>和σ<script type="math/tex" id="MathJax-Element-8">σ</script>分别为原始数据的均值和方法。该种归一化方式要求原始数据的分布可以近似为高斯分布,否则归一化的效果会变得很糟糕。使用preproccessing库的StandardScaler类对数据进行标准化的代码如下:

from sklearn.preprocessing import StandardScaler

#标准化,返回值为标准化后的数据
StandardScaler().fit_transform(iris.data)

上述方法是对每个特征(特征矩阵中的列)进行归一化,除此之外,我们还可以对每个样本(特征矩阵中的行)进行归一化,将样本所对应的特征向量转化为单位向量,其目的在于样本向量在点乘运算或其他核函数计算相似性时,拥有统一的标准。使用preproccessing库的Normalizer类对数据进行归一化的代码如下:

from sklearn.preprocessing import Normalizer

#归一化,返回值为归一化后的数据
Normalizer().fit_transform(iris.data)

1.2 特征二值化

特征二值化的方法是将特征的取值转化为0或1。例如,在房价预测问题中对于“是否为学区房”这一特征,取值为1表示该房是学区房,反之则为0。在sklearn中可以设置一个阈值,大于阈值的赋值为1,小于等于阈值的赋值为0。使用preproccessing库的Binarizer类对数据进行二值化的代码如下:

from sklearn.preprocessing import Binarizer

#二值化,阈值设置为3,返回值为二值化后的数据
Binarizer(threshold=3).fit_transform(iris.data)

1.3 one-hot编码

对于离散特征,例如,性别:{男,女},可以采用one-hot编码的方式将特征表示为一个m维向量,其中m为特征的取值个数。在one-hot向量中只有一个维度的值为1,其余为0。以“性别”这个特征为例,我们可以用向量 “1,0”表示“男”,向量 “0,1”表示“女”。使用one-hot编码可将离散特征的取值扩展到了欧式空间,便于进行相似度计算。使用preproccessing库的OneHotEncoder类对数据进行one-hot编码的代码如下:

from sklearn.preprocessing import OneHotEncoder

#对IRIS数据集的目标值进行one-hot编码
OneHotEncoder().fit_transform(iris.target.reshape((-1,1)))

1.4 缺失值计算

在实际应用中,我们得到的数据往往不完整,可以用以下方法进行处理:

  1. 最简单直接的方法是删除含有缺失值的数据删,这种做法的缺点是可能会导致信息丢失
  2. 通过已有数据计算相应特征的平均数、中位数、众数等来补全缺失值
  3. 建立一个模型来“预测”缺失的数据。(KNN, Matrix completion等方法)
  4. 引入虚拟变量(dummy variable)来表征是否有缺失,是否有补全

用preproccessing库的Imputer类对数据进行缺失值计算的代码如下:

from numpy import vstack, array, nan
from sklearn.preprocessing import Imputer

#缺失值计算,返回值为计算缺失值后的数据
#参数missing_value为缺失值的表示形式,默认为NaN
#对数据集新增一个样本,4个特征均赋值为NaN,表示数据缺失
#参数strategy为缺失值填充方式,默认为mean(均值)
Imputer().fit_transform(vstack((array([nan, nan, nan, nan]), iris.data)))

1.5 数据变换

常见的数据变换有基于多项式的、基于指数函数的、基于对数函数的。4个特征,度为2的多项式转换公式如下:

技术分享

使用preproccessing库的PolynomialFeatures类对数据进行多项式转换的代码如下:

from sklearn.preprocessing import PolynomialFeatures

#多项式转换
#参数degree为度,默认值为2
PolynomialFeatures().fit_transform(iris.data)

基于单变元函数的数据变换可以使用一个统一的方式完成,使用preproccessing库的FunctionTransformer对数据进行对数函数转换的代码如下:

from numpy import log1p
from sklearn.preprocessing import FunctionTransformer

#自定义转换函数为对数函数的数据变换
#第一个参数是单变元函数
FunctionTransformer(log1p).fit_transform(iris.data)

1.6 样本不均衡

样本不均衡指的是数据集中的正样本数量与负样本数量的比例失衡。例如,实际应用中,负样本的数量通常远远大于正样本。样本不均衡的危害:造成分类器在多数类精度较高,少数类的分类精度很低,甚至造成分类器失效。解决方案分为以下两种:

  1. 欠采样:通过减少多数类样本来提高少数类的分类性能
    • 随机地去掉一些多数类样本来减小多数类的规模,该方法的缺点是会丢失多数类的一些重要信息,不能够充分利用已有的信息
    • 通过一定规则有选择的去掉对分类作用不大的多数样本(保留与正样本较为接近的负样本)
  2. 过抽样:通过改变训练数据的分布来消除或减小数据的不平衡
    • 对少数类样本进行复制,该方法的缺点是可能导致过拟合,因为没有给少数类增加任何新的信息
  3. 算法层面
    • 改进损失函数的权重,加大少数样本的权值
    • 采用集成学习(bagging, boosting)

2. 特征选择

数据预处理完成后,接下来需要从给定的特征集合中筛选出对当前学习任务有用的特征,这个过程称为特征选择(feature selection)。通常来说,从两个方面来选择特征:

  • 特征是否发散:如果一个特征不发散,例如方差接近于0,也就是说样本在这个特征上基本上没有差异,这个特征对于样本的区分并没有什么用。
  • 特征与目标的相关性:这点比较显见,与目标相关性高的特征,应当优选选择。

常见的特征选择方法可分为三类:过滤法(Filter)、包裹法(Wrapper)、嵌入法(Embedding)

  1. 过滤法:按照发散性或者相关性对各个特征进行评分,设定阈值或者待选择阈值的个数,选择特征。
  2. 包裹法:包裹式特征选择直接把最终要使用的学习器性能作为特征子集的评价标准。
  3. 嵌入法:将特征选择过程和机器训练过程融合为一体。两者在同一优化过程中完成,即在学习器训练过程中自动进行了特征选择。

2.1 过滤法(filter)

2.1.1 方差过滤法

方差过滤法需要计算每个特征的方差,然后根据阈值删除取值小于阈值的特征。例如,假设某特征的取值为0和1,且训练集中有90%以上的数据在该特征的取值为1,那么可认为该特征对于区分不同数据的作用不大。方差过滤法只能用于筛选离散的特征,如果特征的取值是连续的,就需要将连续值离散化之后才能用。使用feature_selection库的VarianceThreshold类来选择特征的代码如下:

from sklearn.feature_selection import VarianceThreshold

#方差选择法,返回值为特征选择后的数据
#参数threshold为方差的阈值
VarianceThreshold(threshold=3).fit_transform(iris.data)

2.1.2 皮尔森相关系数

皮尔森相关系数(Pearson Correlation)是一种最简单的,能帮助理解特征和目标变量之间关系的方法,该方法衡量的是变量之间的线性相关性,结果的取值区间为[-1,1],-1表示完全的负相关(这个变量下降,那个就会上升),+1表示完全的正相关,0表示没有线性相关。用feature_selection库的SelectKBest类结合Pearson 相关系数来选择特征的代码如下:

from sklearn.feature_selection import SelectKBest
from scipy.stats import pearsonr

#选择K个最好的特征,返回选择特征后的数据
#第一个参数为计算评估特征是否好的函数,该函数输入特征矩阵和目标向量,输出二元组(评分,P值)的数组,数组第i项为第i个特征的评分和P值。在此定义为计算相关系数
#参数k为选择的特征个数
SelectKBest(lambda X, Y: array(map(lambda x:pearsonr(x, Y), X.T)).T, k=2).fit_transform(iris.data, iris.target)

2.1.3 互信息和最大信息系数

互信息(Mutual information)用于评价离散特征对离散目标变量的相关性,互信息计算公式如下:

技术分享

互信息用于特征选择有以下缺点:

  1. 它不属于度量方式,也没有办法归一化,在不同数据及上的结果无法做比较;
  2. 对于连续变量的计算不是很方便(X和Y都是集合,x,y都是离散的取值),通常变量需要先离散化,而互信息的结果对离散化的方式很敏感。

最大信息系数(Maximal Information Coefficient, MIC)克服了这两个问题。它首先寻找一种最优的离散化方式,然后把互信息取值转换成一种度量方式,取值区间在[0,1]。minepy提供了MIC功能,代码如下:

from minepy import MINE

m = MINE()
x = np.random.uniform(-1, 1, 10000)
m.compute_score(x, x**2)
printm.mic()

2.1.4 卡方检验

卡方检验用于衡量离散特征对离散目标变量的相关性,其计算公式为:

技术分享

其中,Ai<script type="math/tex" id="MathJax-Element-9">A_i</script>为特征A第 i 个取值的观察频数,Ei<script type="math/tex" id="MathJax-Element-10">E_i</script>为特征A第 i 个取值的期望频数,n为总频数,pi<script type="math/tex" id="MathJax-Element-11">p_i</script>为第 i 个取值的期望频率。用feature_selection库的SelectKBest类结合卡方检验来选择特征的代码如下:

from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2

#选择K个最好的特征,返回选择特征后的数据
SelectKBest(chi2, k=2).fit_transform(iris.data, iris.target)

2.2 包裹法(Wrapper)

2.2.1 递归特征消除

递归特征消除的主要思想是反复的构建模型(如SVM或者回归模型)然后选出最好的(或者最差的)的特征(可以根据系数来选),把选出来的特征放到一边,然后在剩余的特征上重复这个过程。feature_selection库的RFE类来选择特征的代码如下:

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

#递归特征消除法,返回特征选择后的数据
#参数estimator为基模型
#参数n_features_to_select为选择的特征个数
RFE(estimator=LogisticRegression(), n_features_to_select=2).fit_transform(iris.data, iris.target)

上面的代码中,首先,通过全部特征利用logistic回归训练评估函数,得出每个特征的权重。然后,将最小权重的特征从特征集合中去除。循环执行以上两个过程,直到特征数达成需要。

2.2.2 Las Vegas Wrapper(LVW)

  LVW是典型的包裹式特征选择方法,该算法将最终要使用的学习器的性能作为特征子集的评价标准,然后针对特征空间中的不同子集,计算每个子集的预测效果,效果最好的,即作为最终被挑选出来的特征子集。算法流程如下图所示:

技术分享

其中数据集为 D,特征集为 A,则 LVW 每次从特征集 A 中随机产生一个特征子集 A′,然后使用交叉验证的方法(伪代码的第 8 步)估计学习器在特征子集 A′ 上的误差,若该误差小于之前获得的最小误差,或者与之前的最小误差相当但 A′ 中包含的特征数更少,则将 A′ 保留下来。  由于 LVW 算法每次评价子集 A′ 时,都需要重新训练学习器,计算开销很大,因此设置了参数 T 来控制停止条件。但当特征数很多(即 |A| 很大)并且 T 设置得很大时,可能算法运行很长时间都不能停止。

2.3 嵌入法(Embedding)

2.3.1 L1<script type="math/tex" id="MathJax-Element-12">L1</script>正则化

正则化就是把额外的约束或者惩罚项加到已有模型(损失函数)上,以防止过拟合并提高泛化能力。损失函数由原来的E(X,Y)<script type="math/tex" id="MathJax-Element-13">E(X,Y)</script>变为E(X,Y)+α||w||L1/L2<script type="math/tex" id="MathJax-Element-14">E(X,Y)+\alpha||w||_{L1/L2}</script>,w是模型的参数,||?||<script type="math/tex" id="MathJax-Element-15">||·||</script>一般采用一范数(L1)或者二范数(L2),α<script type="math/tex" id="MathJax-Element-16">\alpha</script>用于控制正则化的强度。L1正则化和L2正则化也称为Lasso和Ridge。

与L2正则化相比,L1正则化往往会得到一个更为稀疏的w,这意味着w的一些分量会趋于0,而非零分量的特征才会出现在最终的模型里。因此L1正则化用于进行特征选择,L2正则化用于防止过拟合。

为了说明这一点,下面举一个直观的例子,如下图所示:假定w有两个属性,我们将其作为两个坐标轴,平方误差项取值相同的点的连线构成等值线。加入L1/L2正则化项的模型的解为平方误差等值线与对应范数等值线的交点。从图中可以看出,采用L1范数时,平方误差等值线与范数等值线的交点常出现在坐标轴上,即对应分量为0;而采用L2范数时,两者交点常出现在某象限,即分量均非0。因此采用L1范数更容易得到稀疏解。

技术分享

2.3.2 随机森林

随机森林由多个决策树构成。决策树中的每一个节点都是基于某个特征的将数据集按照不同的label一分为二。利用随机森林提供的不纯度可以进行特征选择,对于分类问题,通常采用基尼指数或者信息增益;对于回归问题,通常采用的是方差或者最小二乘拟合。当训练随机森林时,可算出每个特征平均减少了多少不纯度,并把它平均减少的不纯度作为特征选择的依据。基于随机森林的特征选择代码如下:

from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
import numpy as np

#Load boston housing dataset as an example
boston = load_boston()
X = boston["data"]
Y = boston["target"]
names = boston["feature_names"]
rf = RandomForestRegressor()
rf.fit(X, Y)
print"Features sorted by their score:"print sorted(zip(map(lambdax: round(x, 4), rf.feature_importances_), names), reverse=True)

输出:

Features sorted by their score: [(0.5298, ‘LSTAT‘), (0.4116, ‘RM‘), (0.0252, ‘DIS‘), (0.0172, ‘CRIM‘), (0.0065, ‘NOX‘), (0.0035, ‘PTRATIO‘), (0.0021, ‘TAX‘), (0.0017, ‘AGE‘), (0.0012, ‘B‘), (0.0008, ‘INDUS‘), (0.0004, ‘RAD‘), (0.0001, ‘CHAS‘), (0.0, ‘ZN‘)]

关于随机森林与GBDT的介绍可参考:随机森林与GBDT

3. 降维

当特征数量较多时,会造成计算量大,训练时间长的问题;甚至当特征数量多于样本数量时,每个样本都具有自己的独特性,样本点在高维空间中较为分散,因此会造成过拟合。所以降低特征矩阵维度也是必不可少的。常见的降维方法有线性判别分析(LDA)和主成分分析法(PCA)。

3.1 线性判别分析法(LDA)

LDA的思想是:将数据映射到一个低维空间,使得不同类别数据的间隔尽量大(类间间隔大),同一类别中的数据间隔尽量小(类内间隔小),这样可以最好的将不同类别的数据分隔开。使用lda库的LDA类选择特征的代码如下:

from sklearn.lda import LDA

#线性判别分析法,返回降维后的数据
#参数n_components为降维后的维数
LDA(n_components=2).fit_transform(iris.data, iris.target)

3.2 主成分分析法(PCA)

PCA与LDA有着非常近似的意思,LDA的输入数据是带标签的,而PCA的输入数据是不带标签的,所以PCA是一种无监督学习。LDA通常来说是作为一个独立的算法存在,给定了训练数据后,将会得到一系列的判别函数(discriminate function),之后对于新的输入,就可以进行预测了。而PCA更像是一个预处理的方法,它可以将原本的数据降低维度,而使得降低了维度的数据之间的方差最大。使用decomposition库的PCA类选择特征的代码如下:

from sklearn.decomposition import PCA

#主成分分析法,返回降维后的数据
#参数n_components为主成分数目
PCA(n_components=2).fit_transform(iris.data)

参考资料

  • 周志华. 机器学习 : = Machine learning[M]. 清华大学出版社, 2016.
  • 使用sklearn做单机特征工程
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    数据预处理与特征选择