特征处理是特征工程的核心部分,sklearn提供了较为完整的特征处理方法,包括数据预处理,特征选择,降维等。首次接触到sklearn,通常会被其丰富且方便的算法模型库吸引,但是这里介绍的特征处理库也非常强大!
经过前人的总结,特征工程已经形成了接近标准化的流程,如下图所示(此图来自此网友,若侵权,联系我,必删除)
1 导入数据
本文中使用sklearn中的IRIS(鸢尾花)数据集来对特征处理功能进行说明。IRIS数据集由Fisher在1936年整理,包括4个特征(Sepal.Length(花萼长度)、Sepal.Width(花萼宽度)、Petal.Length(花瓣长度)、Petal.Width(花瓣宽度)),特征值都为正浮点数,单位为厘米,目标值为鸢尾花的分类(Iris Setosa(山鸢尾)、Iris Versicolour(杂色鸢尾),Iris Virginica(维吉尼亚鸢尾))。导入IRIS数据集的代码如下:
from sklearn.datasets import load_iris # 导入IRIS数据集 iris = load_iris() # 特征矩阵 data = iris.data # 目标向量 target = iris.target
从本地导入数据集的代码如下:
# 导入本地的iris数据集 dataframe = pd.read_csv('iris.csv',header=None) iris_data = dataframe.values # print(type(iris_data)) #<class 'numpy.ndarray'> # 特征矩阵 data = iris_data[:,0:-1] # 目标向量 target = iris_data[:,-1]
其中iris.txt的数据集如下:
5.1,3.5,1.4,0.2,Iris-setosa 4.9,3.0,1.4,0.2,Iris-setosa 4.7,3.2,1.3,0.2,Iris-setosa 4.6,3.1,1.5,0.2,Iris-setosa 5.0,3.6,1.4,0.2,Iris-setosa 5.4,3.9,1.7,0.4,Iris-setosa 4.6,3.4,1.4,0.3,Iris-setosa 5.0,3.4,1.5,0.2,Iris-setosa 4.4,2.9,1.4,0.2,Iris-setosa 4.9,3.1,1.5,0.1,Iris-setosa 5.4,3.7,1.5,0.2,Iris-setosa 4.8,3.4,1.6,0.2,Iris-setosa 4.8,3.0,1.4,0.1,Iris-setosa 4.3,3.0,1.1,0.1,Iris-setosa 5.8,4.0,1.2,0.2,Iris-setosa 5.7,4.4,1.5,0.4,Iris-setosa 5.4,3.9,1.3,0.4,Iris-setosa 5.1,3.5,1.4,0.3,Iris-setosa 5.7,3.8,1.7,0.3,Iris-setosa 5.1,3.8,1.5,0.3,Iris-setosa 5.4,3.4,1.7,0.2,Iris-setosa 5.1,3.7,1.5,0.4,Iris-setosa 4.6,3.6,1.0,0.2,Iris-setosa 5.1,3.3,1.7,0.5,Iris-setosa 4.8,3.4,1.9,0.2,Iris-setosa 5.0,3.0,1.6,0.2,Iris-setosa 5.0,3.4,1.6,0.4,Iris-setosa 5.2,3.5,1.5,0.2,Iris-setosa 5.2,3.4,1.4,0.2,Iris-setosa 4.7,3.2,1.6,0.2,Iris-setosa 4.8,3.1,1.6,0.2,Iris-setosa 5.4,3.4,1.5,0.4,Iris-setosa 5.2,4.1,1.5,0.1,Iris-setosa 5.5,4.2,1.4,0.2,Iris-setosa 4.9,3.1,1.5,0.1,Iris-setosa 5.0,3.2,1.2,0.2,Iris-setosa 5.5,3.5,1.3,0.2,Iris-setosa 4.9,3.1,1.5,0.1,Iris-setosa 4.4,3.0,1.3,0.2,Iris-setosa 5.1,3.4,1.5,0.2,Iris-setosa 5.0,3.5,1.3,0.3,Iris-setosa 4.5,2.3,1.3,0.3,Iris-setosa 4.4,3.2,1.3,0.2,Iris-setosa 5.0,3.5,1.6,0.6,Iris-setosa 5.1,3.8,1.9,0.4,Iris-setosa 4.8,3.0,1.4,0.3,Iris-setosa 5.1,3.8,1.6,0.2,Iris-setosa 4.6,3.2,1.4,0.2,Iris-setosa 5.3,3.7,1.5,0.2,Iris-setosa 5.0,3.3,1.4,0.2,Iris-setosa 7.0,3.2,4.7,1.4,Iris-versicolor 6.4,3.2,4.5,1.5,Iris-versicolor 6.9,3.1,4.9,1.5,Iris-versicolor 5.5,2.3,4.0,1.3,Iris-versicolor 6.5,2.8,4.6,1.5,Iris-versicolor 5.7,2.8,4.5,1.3,Iris-versicolor 6.3,3.3,4.7,1.6,Iris-versicolor 4.9,2.4,3.3,1.0,Iris-versicolor 6.6,2.9,4.6,1.3,Iris-versicolor 5.2,2.7,3.9,1.4,Iris-versicolor 5.0,2.0,3.5,1.0,Iris-versicolor 5.9,3.0,4.2,1.5,Iris-versicolor 6.0,2.2,4.0,1.0,Iris-versicolor 6.1,2.9,4.7,1.4,Iris-versicolor 5.6,2.9,3.6,1.3,Iris-versicolor 6.7,3.1,4.4,1.4,Iris-versicolor 5.6,3.0,4.5,1.5,Iris-versicolor 5.8,2.7,4.1,1.0,Iris-versicolor 6.2,2.2,4.5,1.5,Iris-versicolor 5.6,2.5,3.9,1.1,Iris-versicolor 5.9,3.2,4.8,1.8,Iris-versicolor 6.1,2.8,4.0,1.3,Iris-versicolor 6.3,2.5,4.9,1.5,Iris-versicolor 6.1,2.8,4.7,1.2,Iris-versicolor 6.4,2.9,4.3,1.3,Iris-versicolor 6.6,3.0,4.4,1.4,Iris-versicolor 6.8,2.8,4.8,1.4,Iris-versicolor 6.7,3.0,5.0,1.7,Iris-versicolor 6.0,2.9,4.5,1.5,Iris-versicolor 5.7,2.6,3.5,1.0,Iris-versicolor 5.5,2.4,3.8,1.1,Iris-versicolor 5.5,2.4,3.7,1.0,Iris-versicolor 5.8,2.7,3.9,1.2,Iris-versicolor 6.0,2.7,5.1,1.6,Iris-versicolor 5.4,3.0,4.5,1.5,Iris-versicolor 6.0,3.4,4.5,1.6,Iris-versicolor 6.7,3.1,4.7,1.5,Iris-versicolor 6.3,2.3,4.4,1.3,Iris-versicolor 5.6,3.0,4.1,1.3,Iris-versicolor 5.5,2.5,4.0,1.3,Iris-versicolor 5.5,2.6,4.4,1.2,Iris-versicolor 6.1,3.0,4.6,1.4,Iris-versicolor 5.8,2.6,4.0,1.2,Iris-versicolor 5.0,2.3,3.3,1.0,Iris-versicolor 5.6,2.7,4.2,1.3,Iris-versicolor 5.7,3.0,4.2,1.2,Iris-versicolor 5.7,2.9,4.2,1.3,Iris-versicolor 6.2,2.9,4.3,1.3,Iris-versicolor 5.1,2.5,3.0,1.1,Iris-versicolor 5.7,2.8,4.1,1.3,Iris-versicolor 6.3,3.3,6.0,2.5,Iris-virginica 5.8,2.7,5.1,1.9,Iris-virginica 7.1,3.0,5.9,2.1,Iris-virginica 6.3,2.9,5.6,1.8,Iris-virginica 6.5,3.0,5.8,2.2,Iris-virginica 7.6,3.0,6.6,2.1,Iris-virginica 4.9,2.5,4.5,1.7,Iris-virginica 7.3,2.9,6.3,1.8,Iris-virginica 6.7,2.5,5.8,1.8,Iris-virginica 7.2,3.6,6.1,2.5,Iris-virginica 6.5,3.2,5.1,2.0,Iris-virginica 6.4,2.7,5.3,1.9,Iris-virginica 6.8,3.0,5.5,2.1,Iris-virginica 5.7,2.5,5.0,2.0,Iris-virginica 5.8,2.8,5.1,2.4,Iris-virginica 6.4,3.2,5.3,2.3,Iris-virginica 6.5,3.0,5.5,1.8,Iris-virginica 7.7,3.8,6.7,2.2,Iris-virginica 7.7,2.6,6.9,2.3,Iris-virginica 6.0,2.2,5.0,1.5,Iris-virginica 6.9,3.2,5.7,2.3,Iris-virginica 5.6,2.8,4.9,2.0,Iris-virginica 7.7,2.8,6.7,2.0,Iris-virginica 6.3,2.7,4.9,1.8,Iris-virginica 6.7,3.3,5.7,2.1,Iris-virginica 7.2,3.2,6.0,1.8,Iris-virginica 6.2,2.8,4.8,1.8,Iris-virginica 6.1,3.0,4.9,1.8,Iris-virginica 6.4,2.8,5.6,2.1,Iris-virginica 7.2,3.0,5.8,1.6,Iris-virginica 7.4,2.8,6.1,1.9,Iris-virginica 7.9,3.8,6.4,2.0,Iris-virginica 6.4,2.8,5.6,2.2,Iris-virginica 6.3,2.8,5.1,1.5,Iris-virginica 6.1,2.6,5.6,1.4,Iris-virginica 7.7,3.0,6.1,2.3,Iris-virginica 6.3,3.4,5.6,2.4,Iris-virginica 6.4,3.1,5.5,1.8,Iris-virginica 6.0,3.0,4.8,1.8,Iris-virginica 6.9,3.1,5.4,2.1,Iris-virginica 6.7,3.1,5.6,2.4,Iris-virginica 6.9,3.1,5.1,2.3,Iris-virginica 5.8,2.7,5.1,1.9,Iris-virginica 6.8,3.2,5.9,2.3,Iris-virginica 6.7,3.3,5.7,2.5,Iris-virginica 6.7,3.0,5.2,2.3,Iris-virginica 6.3,2.5,5.0,1.9,Iris-virginica 6.5,3.0,5.2,2.0,Iris-virginica 6.2,3.4,5.4,2.3,Iris-virginica 5.9,3.0,5.1,1.8,Iris-virginica
2,数据预处理
通过特征提取,我们能得到未经处理的特征,这时的特征可能有以下问题:
- 1,不属于同一量纲:即特征的规格不一样,不能放在一起比较。无量纲化可以解决这一问题。
- 2,信息亢余:对于某些定量特征,其包含的有效信息为区间划分,例如学习成绩,假若只关心“及格”或者“不及格”,那么需要将定量的考分,转换成“1”和“0”表示及格和不及格。二值化可以解决这一问题。
- 3,定性特征不能直接使用:某些机器学习算法和模型只能接受定量特征的输入,那么需要将定性特征转换为定量特征。最简单的方式是为每一种定性值指定一个定量值,但是这种方式过于灵活,增加了调参的工作。通常使用哑编码的方式将定性特征转化为定量特征:假设有N种定性值,则将这一个特征扩展为N种特征,当原始特征值为第i种定性值时,第i个扩展特征赋值为1,其他扩展特征赋值为0,哑编码的方式相比直接指定的方式,不用增加调参的工作,对于线性模型来说,使用哑编码后的特征可达到非线性的效果
- 4,存在缺失值:缺失值需要补充
- 5,信息利用率低:不同的机器学习算法和模型对数据中信息的利用是不同的,之前提到在线性模型中,使用对定性特征哑编码可以达到非线性的效果。类似的,对于定量变量多项式化,或者进行其他的转换,都能达到非线性的效果。
我们使用sklearn中的preprocessing库来进行数据预处理,可以覆盖以上问题的解决方案。
2.1 无量纲化
无量纲化使不同规格的数据转换到同一规则。常见的无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,其转换成标准正态分布。区间缩放法利用了边界值信息,将特征的取值区间缩放到某个特点的范围,例如[0,1]等。
2.1.1 标准化
标准化需要计算特征的均值和标准差,公式表达为:
使用preprocessing库的StandardScaler类对数据进行标准化的代码如下:
from sklearn.preprocessing import StandardScaler #标准化,返回值为标准化后的数据 StandardScaler().fit_transform(iris.data)
StandardScler计算训练集的平均值和标准差,以便测试数据及使用相同的变换,变换后的各维特征有0均值,单位方差(也叫z-score规范化),计算方式是将特征值减去均值,除以标准差。
fit 用于计算训练数据的均值和方差,后面就会使用均值和方差来转换训练数据
fit_transform 不仅计算训练数据的均值和方差,还会用计算出来的均值和方差来转换训练数据,从而把数据转化成标准的正态分布。
transform 很显然,这只是进行转换,只是把训练数据转换成标准的正态分布。
2.1.2 区间缩放法(最小-最大规范化)
区间缩放法的思路有很多,常见的一种为利用两个极值进行缩放,公式表达为:
使用preproccessing库的MinMaxScaler类对数据进行区间缩放的代码如下:
from sklearn.preprocessing import MinMaxScaler #区间缩放,返回值为缩放到[0, 1]区间的数据 MinMaxScaler().fit_transform(iris.data)
区间缩放是对原始数据进行线性变换,变换到[0,1] 区间(当然也可以是其他固定最小最大值的区间)
2.2 对定量特征二值化
定量特征二值化的核心在于设定一个阈值,大于阈值的赋值为1,小于等于阈值的赋值为0,公式如下:
使用preprocessing库的Binarizer类对数据进行二值化的代码如下:
from sklearn.preprocessing import Binarizer #二值化,阈值设置为3,返回值为二值化后的数据 Binarizer(threshold=3).fit_transform(iris.data)
给定阈值,将特征转化为0/1,最主要的是确定阈值设置。
2.3 对定性特征哑编码
由于IRIS数据集的特征皆为定量特征,故使用其目标值进行哑编码(实际上是不需要的)。使用preprocessing库的OneHotEncoder类对数据进行哑编码的代码如下:
from sklearn.preprocessing import OneHotEncoder #哑编码,对IRIS数据集的目标值,返回值为哑编码后的数据 OneHotEncoder().fit_transform(iris.target.reshape((-1,1)))
One-hot编码是使一种对离散特征值的编码方式,在LR模型中常用到,用于给线性模型增加非线性能力。
2.4 缺失值计算
缺失值是指粗糙数据中由于缺少信息而造成的数据的聚类,分组,删除或者截断。它指的是现有数据集中某个或者某些属性的值时不完全的。
缺失值的处理目前有两种方法:删除缺失值和填充缺失值。
2.4.1 删除缺失值
如果一个样本或者变量中所包含的缺失值超过一定的比例,比如超过样本或者变量的一半,此时这个样本或者变量所含有的信息是有限的,如果我们强行对数据进行填充处理,可能会加入过大的人工信息,导致建模效果大打折扣,这种情况下,我们一般选择从数据中剔除整个样本或者变量,即删除缺失值。
2.4.2 缺失值填充
-
随机填充法
从字面上理解就是找一个随机数,对缺失值进行填充,这种方法没有考虑任何的数据特性,填充后可能还是会出现异常值等情况,一般情况下不建议使用。
-
均值填充法
寻找与缺失值变量相关性最大的那个变量把数据分成几个组,然后分别计算每个组的均值,然后把均值填入缺失的位置作为它的值,如果找不到相关性较好的变量,也可以统计变量已有数据的均值,然后把它填入缺失位置。这种方法会在一定程度上改变数据的分布。
-
最相似填充法
在数据集中找到一个与它最相似的样本,然后用这个样本的值对缺失值进行填充。
与均值填充法有点类似,寻找与缺失值变量(比如x)相关性最大的那个变量(比如y),然后按照变量y的值进行排序,然后得到相应的x的排序,最后用缺失值所在位置的前一个值来代替缺失值。
-
回归填充法
把缺失值变量作为一个目标变量y,把缺失值变量已有部分数据作为训练集,寻找与其高度相关的变量x建立回归方程,然后把缺失值变量y所在位置对应的x作为预测集,对缺失进行预测,用预测结果来代替缺失值。
-
k近邻填充法
利用knn算法,选择缺失值的最近k个近邻点,然后根据缺失值所在的点离这几个点距离的远近进行加权平均来估计缺失值。
2.4.3 示例——均值填充法
由于IRIS数据集没有缺失值,故对数据集新增一个样本,4个特征均赋值为NaN,表示数据缺失。使用preprocessing库的Imputer类对数据进行缺失值计算的代码如下:
from numpy import vstack, array, nan from sklearn.preprocessing import Imputer #缺失值计算,返回值为计算缺失值后的数据 #参数missing_value为缺失值的表示形式,默认为NaN #参数strategy为缺失值填充方式,默认为mean(均值) Imputer().fit_transform(vstack((array([nan, nan, nan, nan]), iris.data)))
2.5 数据变换
常用的数据变换有基于多项式的,基于指数函数的,基于对数函数的,4个特征,度为2的多项式转换公式如下:
使用preprocessing库的PolynomialFeatures类对数据进行多项式转换的代码如下:
from sklearn.preprocessing import PolynomialFeatures #多项式转换 #参数degree为度,默认值为2 PolynomialFeatures().fit_transform(iris.data)
基于单变元函数的数据变换可以使用一个统一的方式完成,使用preprocessing库的FunctionTransformer 对数据进行对数函数转换的代码如下:
from numpy import log1p from sklearn.preprocessing import FunctionTransformer #自定义转换函数为对数函数的数据变换 #第一个参数是单变元函数 FunctionTransformer(log1p).fit_transform(iris.data)
2.6 异常值处理
-
截断法
数据预处理第一步,通常是对异常值的处理。“截断”方法具体步骤为:首先,求一次过程中原始数据的上四分位值Q3,作为首尾无效数据限界点;然后从原始数据的开头向后和尾部向前,提出所有小于Q3数值,直到碰到第一个不小于Q3的数据,则停止截断。
首先,要得到数据的熵四分位数和下四分位数,利用np.percentile(),用法如下:
import numpy as np x = np.array([[1,2,3],[7,8,9]]) Q1 = np.percentile(x,25) # 1st quartile Q3 = np.percentile(x,75) # 3st quartile
假设数据集时x = [1,2,3,98,99,10000],显然最后一个数10000是一个超限点,它的Q1 = 25,Q3 = 75,四分位距IQR(the interquartile range)=Q1 - Q3。若上下界分别扩大0.5倍,令k = 1.5 为high = Q3 + k*(Q3-Q1),下界为low = Q1-k*(Q3-Q1),即上界为-50下界为150,显然10000超限,如果想调整上界下界的范围,调整系数即可。
四分位数(Quartile)是统计学中分位数的一种,即把全部数值由小到大排列并分成四等份。 处于三个切割点位置的数值就是四分位数。 第一四分位数 (Q1)。又称“较小四分位数”,等于该样本中全部数值由小到大排列后第25%的数字。 第二四分位数 (Q2)。又称“中位数”,等于该样本中全部数值由小到大排列后第50%的数字。 第三四分位数 (Q3),又称“较大四分位数”,等于该样本中全部数值由小到大排列后第75%的数字。 第三四分位数与第一四分位数的差距又称四分位距(InterQuartile Range, IQR)
对于一个矩阵df,按列循环找到每列数据的异常值,如果某个样本含有n个以上的超限特征,返回行号。
注意:在进行这一步之前,先要处理好缺失值的标签量。
# outlier detection def detect_outliers(df,n,feature_name): ''' df: the feature dataframe; n: if outlier feature more than n features: the name of columns detected return the index ''' outlier_indices=[] for col in feature_name: Q1 = np.percentile(df[col],25) Q3 = np.percentile(df[col],75) # interquartile range(IQR) IQR = Q3 - Q1 outlier_step = 1.5 * IQR # Determine a list of indeices of ouliers for feature col outlier_list_col = df[(df[col] < Q1 - outlier_step) | (df[col] > Q3 + outlier_step)].index.tolist() # append the found oulier indices for col to the list of outlier indices outlier_indices.extend(outlier_list_col) # select observations containing more than 2 outliers outlier_indices = Counter(outlier_indices) multiple_outliers = list(k for k, v in outlier_indices.items() if v > n) return multiple_outliers
经过检查后,假设特征矩阵有10(列)个特征,规范包含大于4列超过了范围,返回行号。
ouliter_result = detect_outliers(feature, 4, feature.columns.tolist())
-
单变量异常值检测(格拉布斯法)
首先,将变量按照其值从小到大进行顺序排列x1,x2.....xn
其次,计算平均值x拔和标准差S,
同时计算偏离值,即平均值与最大值之差和平均值与最小值之差,然后确定一个可疑值,一般是偏离平均值较大的那个。
计算统计量gi(残差与标准差的比值),i为可疑值的序列号。
再者,将gi与格拉布斯表给出的临界值GP(n)比较,如果计算的Gi值大于表中的临界值GP(n),则能判断该测量数据是异常值,可以剔除。这里临界值GP(n)与两个参数有关:检出水平α和测量次数n 。
检出水平α:如果要求严格,检出水平α可以定得小一些,例如定α=0.01,那么置信概率P=1-α=0.99;如果要求不严格,α可以定得大一些,例如定α=0.10,即P=0.90;通常定α=0.05,P=0.95。
-
多变量异常值检测(基于距离计算)
基于距离的多变量异常值检测类似与k近邻算法的思路,一般的思路是计算各样本点到中心点的距离,如果距离太大,则判断为异常值,这里距离的度量一般使用马氏距离(Mahalanobis Distance)。因为马氏距离不受量纲的影响,而且在多元条件下,马氏距离还考虑了变量之间的相关性,这使得它优于欧氏距离。
2.7 回顾
3 特征选择
当数据预处理完成后,我们需要选择有意义的特征输入机器学习的算法和模型进行训练。通常来说,从两个方面考虑来选择特征:
- 特征是否发散:如果一个特征不发散,例如方差接近于0,也就是说样本在这个特征上基本没有差异,这个特征对样本的区分并没有什么用。
- 特征与目标的相关性:这点比较明显,与目标相关性高的特征,应当优先选择。除方差法外,我们介绍的其他方法均从相关性考虑。
根据特征选择的形式又可以将特征选择方法分为3种:
- Filter:过滤法,按照发散性或者相关性对各个特征进行评分,设定阈值或者选择阈值的个数,选择特征。
- Wrapper:包装法,根据目标函数(通常是预测效果),每次选择若干特征,护着排除若干特征。
- Embedded:嵌入法,先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。
我们使用sklearn中的feature_selection库来进行特征选择。
3.1 Filter
3.1.1 方差选择法
使用方差选择法,先要计算各个特征的方差,然后根据阈值,选择方差大于阈值的特征。使用feature_selection库的VarianceThreshold类来选择特征的代码如下:
from sklearn.feature_selection import VarianceThreshold #方差选择法,返回值为特征选择后的数据 #参数threshold为方差的阈值 VarianceThreshold(threshold=3).fit_transform(iris.data)
3.1.2 相关系数法
使用相关系数法,先要计算各个特征对目标值的相关系数以及相关系数的P值,用feature_selection库的SelectKBest类结合相关系数来选择特征的代码如下:
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)
3.1.3 卡方检验
经典的卡方检验是检验定性自变量对定性因变量的相关性。假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距,构建统计量:
这个统计量的含义简而言之就是自变量对因变量的相关性,用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)
3.1.4 互信息法
经典的互信息也是评价定性自变量对定性因变量的相关性的,互信息计算公式如下:
为了处理定量数据,最大信息系数法被提出,使用feature_selection库的SelectKBest类结合最大信息系数法来选择特征的代码如下:
from sklearn.feature_selection import SelectKBest from minepy import MINE #由于MINE的设计不是函数式的,定义mic方法将其为函数式的,返回一个二元组,二元组的第2项设置成固定的P值0.5 def mic(x, y): m = MINE() m.compute_score(x, y) return (m.mic(), 0.5) #选择K个最好的特征,返回特征选择后的数据 SelectKBest(lambda X, Y: array(map(lambda x:mic(x, Y), X.T)).T, k=2).fit_transform(iris.data, iris.target)
3.2 Wrapper
Wrapper方与特征过滤不同,它不单看特征和目标直接的关联性,而是从添加这个特征后模型最终的表现来评估特征的好坏。而在一个特征空间中,产生特征子集的过程可以看成是一个搜索问题。目前主要用的一个Wrapper方法是递归特征消除法。
递归特征消除的主要思想是不断使用从特征空间中抽取出来的特征子集构建模型,然后选出最好的的特征,把选出来的特征放到一遍,然后在剩余的特征上重复这个过程,直到所有特征都遍历了。这个过程中特征被消除的次序就是特征的排序。这是一种寻找最优特征子集的贪心算法。
3.2.1 递归特征消除法
递归特征消除法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练,使用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)
3.3 Embedded
Embedded方法是在模型构建的同时选择最好的特征。最为常用的一个Embedded方法就是:正则化。正则化就是把额外的约束或者惩罚项加到已有模型的损失函数上,以防止过拟合并提高泛化能力。正则化分为L1正则化(Lasso)和L2正则化(Ridge回归)。
L1正则化是将所有系数的绝对值之和乘以一个系数作为惩罚项加到损失函数上,现在模型寻找最优解的过程中,需要考虑正则项的影响,即如何在正则项的约束下找到最小损失函数。同样的L2正则化也是将一个惩罚项加到损失函数上,不过惩罚项是参数的平方和。其他还有基于树的特征选择等。
3.3.1 基于惩罚项的特征选择法
使用带惩罚项的基模型,除了筛选出特外,同时也进行了降维。使用feature_selection库的SelectFromModel类结合带L1惩罚项的逻辑回归模型,来选择特征的代码如下:
from sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression #带L1惩罚项的逻辑回归作为基模型的特征选择 SelectFromModel(LogisticRegression(penalty="l1", C=0.1)).fit_transform(iris.data, iris.target)
L1惩罚项降维的原理在于保留多个对目标值具有同等相关性的特征中的一个,所以没选到的特征不代表不重要,故可以结合L2惩罚项来优化。具体操作为:若一个特征在L1中的权重为1,选择在L2中的权值差别不大且在L1中权值为0的特征构成同类集合,将这一集合中的特征平分L1中的权值,故需要构建一个新的逻辑回归模型:
from sklearn.linear_model import LogisticRegression class LR(LogisticRegression): def __init__(self, threshold=0.01, dual=False, tol=1e-4, C=1.0, fit_intercept=True, intercept_scaling=1, class_weight=None, random_state=None, solver='liblinear', max_iter=100, multi_class='ovr', verbose=0, warm_start=False, n_jobs=1): #权值相近的阈值 self.threshold = threshold LogisticRegression.__init__(self, penalty='l1', dual=dual, tol=tol, C=C, fit_intercept=fit_intercept, intercept_scaling=intercept_scaling, class_weight=class_weight, random_state=random_state, solver=solver, max_iter=max_iter, multi_class=multi_class, verbose=verbose, warm_start=warm_start, n_jobs=n_jobs) #使用同样的参数创建L2逻辑回归 self.l2 = LogisticRegression(penalty='l2', dual=dual, tol=tol, C=C, fit_intercept=fit_intercept, intercept_scaling=intercept_scaling, class_weight = class_weight, random_state=random_state, solver=solver, max_iter=max_iter, multi_class=multi_class, verbose=verbose, warm_start=warm_start, n_jobs=n_jobs) def fit(self, X, y, sample_weight=None): #训练L1逻辑回归 super(LR, self).fit(X, y, sample_weight=sample_weight) self.coef_old_ = self.coef_.copy() #训练L2逻辑回归 self.l2.fit(X, y, sample_weight=sample_weight) cntOfRow, cntOfCol = self.coef_.shape #权值系数矩阵的行数对应目标值的种类数目 for i in range(cntOfRow): for j in range(cntOfCol): coef = self.coef_[i][j] #L1逻辑回归的权值系数不为0 if coef != 0: idx = [j] #对应在L2逻辑回归中的权值系数 coef1 = self.l2.coef_[i][j] for k in range(cntOfCol): coef2 = self.l2.coef_[i][k] #在L2逻辑回归中,权值系数之差小于设定的阈值,且在L1中对应的权值为0 if abs(coef1-coef2) < self.threshold and j != k and self.coef_[i][k] == 0: idx.append(k) #计算这一类特征的权值系数均值 mean = coef / len(idx) self.coef_[i][idx] = mean return self
使用feature_selection库的SelectFromModel类结合带L1以及L2惩罚项的逻辑回归模型,来选择特征的代码如下:
from sklearn.feature_selection import SelectFromModel #带L1和L2惩罚项的逻辑回归作为基模型的特征选择 #参数threshold为权值系数之差的阈值 SelectFromModel(LR(threshold=0.5, C=0.1)).fit_transform(iris.data, iris.target)
3.3.2 基于树模型的特征选择法
树模型中GBDT也可以用来作为基模型来进行特征选择,使用feature_selection库的SelectFromModel类结合GBDT模型,来选择特征的代码如下:
from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import GradientBoostingClassifier #GBDT作为基模型的特征选择 SelectFromModel(GradientBoostingClassifier()).fit_transform(iris.data, iris.target)
3.4 回顾
4 降维
当特征选择完成后,可以直接训练模型了,但是可能由于特征矩阵过大,导致计算量大,训练时间比较长的问题,因此降低特征矩阵维度也是必不可少的。常见的降维方法除了以上提到的基于L1惩罚项的模型以外,另外有主成分分析法(PCA)和线性判别分析(LDA),线性判别分析本身也是一种分类模型。PCA和LDA有很多的相似点,其本质是要将原始的样本映射到维度更低的样本空间中,但是PCA和LDA的映射目标不一样:PCA是为了让映射后的样本具有最大的发散性,而LDA是为了让映射后的样本有最好的分类性能,所以说PCA是一种无监督的降维方法,而LDA是一种有监督的降维方法。
4.1 主成分分析法(PCA)
使用decomposition库的PCA类选择特征的代码如下:
from sklearn.decomposition import PCA #主成分分析法,返回降维后的数据 #参数n_components为主成分数目 PCA(n_components=2).fit_transform(iris.data)
主成分分析原理及其Python实现博文:可以点击这里
4.2 线性判别分析(LDA)
使用lda库的LDA类选择特征的代码如下:
from sklearn.lda import LDA #线性判别分析法,返回降维后的数据 #参数n_components为降维后的维数 LDA(n_components=2).fit_transform(iris.data, iris.target)
线性判别分析原理及其Python实现博文:可以点击这里
4.3 回顾
5 使用sklearn进行数据挖掘
那么我们可以使用sklearn完成几乎所有特征处理的工作,而且不管是数据预处理,还是特征选择,抑或降维,他们都是通过某个类的方法fit_transform完成的,fit_transform要不只带一个参数:特征矩阵,要不带两个参数:特征矩阵加目标向量。这些难道都是巧合吗?还是故意设计成这样?方法fit_transform中有fit这一单词,它和训练模型的fit方法有关联吗?
5.1 数据挖掘的步骤
数据挖掘通常包括数据采集,数据分析,特征工程,训练模型,模型评估等步骤。使用sklearn工具可以方便地进行特征工程和模型训练的工作,上面也提到了这些难道都是巧合吗?还是故意设计成这样?方法fit_transform中有fit这一单词,它和训练模型的fit方法有关联吗?
显然,这不是巧合,这是sklearn的设计风格。我们能够更加优雅的使用sklearn进行特征工程和模型训练工作。此时,我们从一个数据挖掘的场景入手:
我们使用sklearn进行虚线框内的工作(sklearn也可以进行文本特征提取),通过分析sklearn源码,我们可以看到除训练,预测和评估以外,处理其他工作的类都实现了3个方法:fit transform fit_transform 。从命名中可以看到,fit_transform方法是先调用fit然后调用transform,我们只需要关注fit方法和transform方法即可。
transform方法主要用来对特征进行转换。从可利用信息的角度来说,转换分为无信息转换和有信息转换。无信息转换是指不利用任何其他信息进行转换,比如指数、对数函数转换等。有信息转换从是否利用目标值向量又可分为无监督转换和有监督转换。无监督转换指只利用特征的统计信息的转换,统计信息包括均值、标准差、边界等等,比如标准化、PCA法降维等。有监督转换指既利用了特征信息又利用了目标值信息的转换,比如通过模型选择特征、LDA法降维等。通过总结常用的转换类,我们得到下表:
不难看出,只有有信息的转换类的fit方法才实际有用,显然fit方法的主要工作是获取特征信息和目标值信息,在这点上,fit方法和模型训练时的fit方法就能够联系在一起了:都是通过分析特征和目标值 ,提取有价值的信息,对于转换类来说是某些统计量,对于模型来说可能是特征的权值系数等。另外,只有有监督的转换类的fit和transform方法才需要特征和目标值两个参数。fit方法无用不代表没实现,而是除合法性校验以外,其并没有对特征和目标值进行任何处理,Normalizer的fit方法实现如下:
def fit(self, X, y=None): """Do nothing and return the estimator unchanged This method is just there to implement the usual API and hence work in pipelines. """ X = check_array(X, accept_sparse='csr') return self
基于这些特征处理工作都有共同的方法,那么试想可不可以将他们组合在一起?在本文假设的场景中,我们可以看到这些工作的组合形式有两种:流水线式和并行式。基于流水线组合的工作需要依次进行,前一个工作的输出是后一个工作的输入;基于并行式的工作可以同时进行,其使用同样的输入,所有工作完成后将各自的输出合并之后输出。sklearn提供了包pipeline来完成流水线式和并行式的工作。
5.1.1 数据初貌
在此,我们仍然使用IRIS数据集来进行说明,为了适应提出的场景,对元数据集需要稍微加工:
from numpy import hstack, vstack, array, median, nan from numpy.random import choice from sklearn.datasets import load_iris #特征矩阵加工 #使用vstack增加一行含缺失值的样本(nan, nan, nan, nan) #使用hstack增加一列表示花的颜色(0-白、1-黄、2-红),花的颜色是随机的,意味着颜色并不影响花的分类 iris.data = hstack((choice([0, 1, 2], size=iris.data.shape[0]+1).reshape(-1,1), vstack((iris.data, array([nan, nan, nan, nan]).reshape(1,-1))))) #目标值向量加工 #增加一个目标值,对应含缺失值的样本,值为众数 iris.target = hstack((iris.target, array([median(iris.target)])))
5.1.2 关键技术
并行处理,流水线处理,自动化调参,持久化是使用sklearn优雅的进行数据挖掘的核心。并行处理和流水线处理将多个特征处理工作,甚至包括模型训练工作组合成一个工作(从代码的角度来说,即将多个对象组成了一个对象)。在组合的前提下,自动化调参技术帮我们省去了人工调参的繁琐。训练好的模型是储存在内存中的数据,持久化能够将这些数据保存在文件系统中,之后使用时候无需再进行训练,直接从文件系统中加载即可。
5.2 并行处理
并行处理使得多个特征处理工作能够并行的进行,根据对特征矩阵的读取方式不同,可分为整体并行处理和部分并行处理。整体并行处理,即并行处理的每个工作的输入都是特征矩阵的整体;部分并行处理,即可定义每个工作需要输入的特征矩阵的列。
5.2.1 整体并行处理
pipeline包提供了FeatureUnion类来进行整体并行处理:
from numpy import log1p from sklearn.preprocessing import FunctionTransformer from sklearn.preprocessing import Binarizer from sklearn.pipeline import FeatureUnion #新建将整体特征矩阵进行对数函数转换的对象 step2_1 = ('ToLog', FunctionTransformer(log1p)) #新建将整体特征矩阵进行二值化类的对象 step2_2 = ('ToBinary', Binarizer()) #新建整体并行处理对象 #该对象也有fit和transform方法,fit和transform方法均是并行地调用需要并行处理的对象的fit和transform方法 #参数transformer_list为需要并行处理的对象列表,该列表为二元组列表,第一元为对象的名称,第二元为对象 step2 = ('FeatureUnion', FeatureUnion(transformer_list=[step2_1, step2_2, step2_3]))
5.2.2 部分并行处理
整体并行处理有其缺陷,在一些场景下,我们只需要对特征矩阵的某些列进行转换,而不是所有列,pipeline并没有提供相应的类(仅OneHotEncoder类实现了该功能)需要我们再FeatureUnion的基础上进行优化:
from sklearn.pipeline import FeatureUnion, _fit_one_transformer, _fit_transform_one, _transform_one from sklearn.externals.joblib import Parallel, delayed from scipy import sparse import numpy as np #部分并行处理,继承FeatureUnion class FeatureUnionExt(FeatureUnion): #相比FeatureUnion,多了idx_list参数,其表示每个并行工作需要读取的特征矩阵的列 def __init__(self, transformer_list, idx_list, n_jobs=1, transformer_weights=None): self.idx_list = idx_list FeatureUnion.__init__(self, transformer_list=map(lambda trans:(trans[0], trans[1]), transformer_list), n_jobs=n_jobs, transformer_weights=transformer_weights) #由于只部分读取特征矩阵,方法fit需要重构 def fit(self, X, y=None): transformer_idx_list = map(lambda trans, idx:(trans[0], trans[1], idx), self.transformer_list, self.idx_list) transformers = Parallel(n_jobs=self.n_jobs)( #从特征矩阵中提取部分输入fit方法 delayed(_fit_one_transformer)(trans, X[:,idx], y) for name, trans, idx in transformer_idx_list) self._update_transformer_list(transformers) return self #由于只部分读取特征矩阵,方法fit_transform需要重构 def fit_transform(self, X, y=None, **fit_params): transformer_idx_list = map(lambda trans, idx:(trans[0], trans[1], idx), self.transformer_list, self.idx_list) result = Parallel(n_jobs=self.n_jobs)( #从特征矩阵中提取部分输入fit_transform方法 delayed(_fit_transform_one)(trans, name, X[:,idx], y, self.transformer_weights, **fit_params) for name, trans, idx in transformer_idx_list) Xs, transformers = zip(*result) self._update_transformer_list(transformers) if any(sparse.issparse(f) for f in Xs): Xs = sparse.hstack(Xs).tocsr() else: Xs = np.hstack(Xs) return Xs #由于只部分读取特征矩阵,方法transform需要重构 def transform(self, X): transformer_idx_list = map(lambda trans, idx:(trans[0], trans[1], idx), self.transformer_list, self.idx_list) Xs = Parallel(n_jobs=self.n_jobs)( #从特征矩阵中提取部分输入transform方法 delayed(_transform_one)(trans, name, X[:,idx], self.transformer_weights) for name, trans, idx in transformer_idx_list) if any(sparse.issparse(f) for f in Xs): Xs = sparse.hstack(Xs).tocsr() else: Xs = np.hstack(Xs) return Xs
在本文提出的场景中,我们对特征矩阵的第1列(花的颜色)进行定性特征编码,对第2,3,4列进行对数函数转换,对第5列进行定量特征二值化处理。使用FeatureUnionExt类进行部分并行处理的代码如下:
from numpy import log1p from sklearn.preprocessing import OneHotEncoder from sklearn.preprocessing import FunctionTransformer from sklearn.preprocessing import Binarizer #新建将部分特征矩阵进行定性特征编码的对象 step2_1 = ('OneHotEncoder', OneHotEncoder(sparse=False)) #新建将部分特征矩阵进行对数函数转换的对象 step2_2 = ('ToLog', FunctionTransformer(log1p)) #新建将部分特征矩阵进行二值化类的对象 step2_3 = ('ToBinary', Binarizer()) #新建部分并行处理对象 #参数transformer_list为需要并行处理的对象列表,该列表为二元组列表,第一元为对象的名称,第二元为对象 #参数idx_list为相应的需要读取的特征矩阵的列 step2 = ('FeatureUnionExt', FeatureUnionExt(transformer_list=[step2_1, step2_2, step2_3], idx_list=[[0], [1, 2, 3], [4]]))
5.3 流水线处理
pipeline包提供了Pipeline类来进行流水线处理,流水线上除最后一个工作以外,其他都要执行fit_transfrom方法,且上一个工作输出作为下一个工作的输入。最后一个工作必须实现fit方法,输入为上一个工作的输出;但是不限定一定有transform方法,因为流水线的最后一个工作可能是训练!
根据文中提出的场景,结合并行处理,构造完整的流水线的代码如下:
from numpy import log1p from sklearn.preprocessing import Imputer from sklearn.preprocessing import OneHotEncoder from sklearn.preprocessing import FunctionTransformer from sklearn.preprocessing import Binarizer from sklearn.preprocessing import MinMaxScaler from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import chi2 from sklearn.decomposition import PCA from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline #新建计算缺失值的对象 step1 = ('Imputer', Imputer()) #新建将部分特征矩阵进行定性特征编码的对象 step2_1 = ('OneHotEncoder', OneHotEncoder(sparse=False)) #新建将部分特征矩阵进行对数函数转换的对象 step2_2 = ('ToLog', FunctionTransformer(log1p)) #新建将部分特征矩阵进行二值化类的对象 step2_3 = ('ToBinary', Binarizer()) #新建部分并行处理对象,返回值为每个并行工作的输出的合并 step2 = ('FeatureUnionExt', FeatureUnionExt(transformer_list=[step2_1, step2_2, step2_3], idx_list=[[0], [1, 2, 3], [4]])) #新建无量纲化对象 step3 = ('MinMaxScaler', MinMaxScaler()) #新建卡方校验选择特征的对象 step4 = ('SelectKBest', SelectKBest(chi2, k=3)) #新建PCA降维的对象 step5 = ('PCA', PCA(n_components=2)) #新建逻辑回归的对象,其为待训练的模型作为流水线的最后一步 step6 = ('LogisticRegression', LogisticRegression(penalty='l2')) #新建流水线处理对象 #参数steps为需要流水线处理的对象列表,该列表为二元组列表,第一元为对象的名称,第二元为对象 pipeline = Pipeline(steps=[step1, step2, step3, step4, step5, step6])
5.4 自动化调参
网格搜索为自动化调参的常用技术之一,grid_search包提供了自动化调参的工具,包括GridSearchCV类。对组合好的对象进行训练以及调参的代码如下:
from sklearn.grid_search import GridSearchCV #新建网格搜索对象 #第一参数为待训练的模型 #param_grid为待调参数组成的网格,字典格式,键为参数名称(格式“对象名称__子对象名称__参数名称”),值为可取的参数值列表 grid_search = GridSearchCV(pipeline, param_grid={'FeatureUnionExt__ToBinary__threshold':[1.0, 2.0, 3.0, 4.0], 'LogisticRegression__C':[0.1, 0.2, 0.4, 0.8]}) #训练以及调参 grid_search.fit(iris.data, iris.target)
5.5 持久化
externals.joblib包提供了dump和load方法来持久化和加载内存数据:
#持久化数据 #第一个参数为内存中的对象 #第二个参数为保存在文件系统中的名称 #第三个参数为压缩级别,0为不压缩,3为合适的压缩级别 dump(grid_search, 'grid_search.dmp', compress=3) #从文件系统中加载数据到内存中 grid_search = load('grid_search.dmp')
5.6 回顾
注意:组合和持久化都会涉及pickle技术,在Sklearn的技术文档中有说明,将lambda定义函数作为FunctionTransformer的自定义转换函数将不能pickle化。
参考资料:http://www.cnblogs.com/jasonfreak/p/5448462.html
http://www.cnblogs.com/jasonfreak/p/5448385.html
(在此做笔记的目的是学习,并掌握特征工程,不喜勿喷,谢谢)