特征工程系列:特征筛选的原理与实现
1. 什么是特征工程
-
特征工程是利用数据领域的相关知识来创建能够使机器学习算法达到最佳性能的特征的过程
-
特征工程又包含了Feature Selection(特征选择)、Feature Extraction(特征提取)和Feature construction(特征构造)等子问题
-
获取尽可能小的特征子集,不显著降低分类精度、不影响分类分布以及特征子集应具有稳定、适应性强等特点。
2. 特征选择的方法
2.1 Filter方法 过滤法
- 先进行特征选择,然后去训练学习器,所以特征选择的过程与学习器无关。
- 主要思想:对每一维特征“打分”,即给每一维的特征赋予权重,这样的权重就代表着该特征的重要性,然后依据权重排序。
- 主要方法:
- Chi-squared test(卡方检验)
- Information gain(信息增益)
- Correlation coefficient scores(相关系数)
- 优点:运行速度快,是一种非常流行的特征选择方法。
- 缺点:无法提供反馈,特征选择的标准/规范的制定是在特征搜索算法中完成,学习算法无法向特征搜索算法传递对特征的需求。另外,可能处理某个特征时由于任意原因表示该特征不重要,但是该特征与其他特征结合起来则可能变得很重要。
2.2 Wrapper方法 封装式
-
直接把最后要使用的分类器作为特征选择的评价函数,对于特定的分类器选择最优的特征子集。
-
主要思想:围绕学习算法展开,将子集的选择看作是一个搜索寻优问题,生成不同的组合,对组合进行评价,再与其他的组合进行比较。
-
主要方法:递归特征消除算法。
-
优点:对特征进行搜索时围绕学习算法展开的,能够考虑学习算法所属的任意学习偏差,从而确定最佳子特征,真正关注的是学习问题本身。由于每次尝试针对特定子集时必须运行学习算法,所以能够关注到学习算法的学习偏差/归纳偏差,因此封装能够发挥巨大的作用。
-
缺点:运行速度远慢于过滤算法,实际应用用封装方法没有过滤方法流行。
2.3 Embedded方法 嵌入式
-
将特征选择嵌入到模型训练当中,学习期间自身自动选择特征, 通过训练确定特征的权值系数
-
主要思想:在模型既定的情况下学习出对提高模型准确性最好的特征。也就是在确定模型的过程中,挑选出那些对模型的训练有重要意义的特征。
-
主要方法:用带有L1正则化的项完成特征选择(也可以结合L2惩罚项来优化)、随机森林平均不纯度减少法/平均精确度减少法。
-
优点:对特征进行搜索时围绕学习算法展开的,能够考虑学习算法所属的任意学习偏差。训练模型的次数小于Wrapper方法,比较节省时间。
缺点:运行速度慢。
3. 特征选择实现
3.1 去掉取值变化小的特征 要有区分度
-
样本的方差值,可以认为给定一个阈值,抛弃哪些小于某个阈值的特征
-
离散型变量:
假设某特征的特征值只有0和1,并且在所有输入样本中,95%的实例的该特征取值都是1,那就可以认为这个特征作用不大。如果100%都是1,那这个特征就没意义了
-
连续型变量:
需要将连续变量离散化之后才能用
-
-
不太好用
-
from sklearn.feature_selection import VarianceThreshold VarianceThreshold(threshold=0.1)
3.2 单变量特征选择
- 单变量特征选择方法独立的衡量每个特征与响应变量之间的关系
- 易于运行,易于理解,通常对于理解数据有较好的效果(但对特征优化、提高泛化能力来说不一定有效)
3.2.1 Pearson相关系数 连续型
-
-
就是用 x i x_i xi、 x j x_j xj的协方差除以 x i x_i xi的标准差和 x j x_j xj的标准差,可以看成一种剔除了两个变量量纲影响、标准化后的特殊协方差。
-
协方差是度量各个维度偏离其均值的程度
-
主要用于连续型特征筛选 不适用于离散型
-
优点:**相关系数计算速度快、易于计算,经常在拿到数据(经过清洗和特征提取之后的)之后第一时间就执行。**Pearson相关系数能够表征丰富的关系,符合表示关系的正负,绝对值能够表示强度。
-
缺点 :它只对线性关系敏感,如果关系是非线性的,即便两个变量具有一一对应的关系,相关系数系数也可能会接近0。
-
import numpy as np from scipy.stats import pearsonr size = 1000 x = np.random.normal(0, 1, size) # 计算两变量间的相关系数 print("Lower noise {}".format(pearsonr(x, x + np.random.normal(0, 1, size)))) print("Higher noise {}".format(pearsonr(x, x + np.random.normal(0, 10, size))))
3.2.2 互信息 和 最大信息系数 MINE 离散型
-
如果变量不是独立的,可以通过联合概率分布与边缘概率分布乘积之间的 Kullback-Leibler 散度来判断它们是否“接近”于相互独立。
-
互信息方法
- 熵H(Y)与条件熵H(Y|X)之间的差称为互信息
- I ( x , y ) = H ( x ) − H ( x ∣ y ) = H ( y ) − H ( y ∣ x ) I(x,y) = H(x) - H(x|y) = H(y) - H(y|x) I(x,y)=H(x)−H(x∣y)=H(y)−H(y∣x)
- ID3决策树的特征选择规则 选择信息增益大的特征
- 互信息法也是评价定性自变量对定性因变量的相关性的,但是并不方便直接用于特征选择
-
最大信息系数
-
首先寻找一种最优的离散方式,然后把互信息取值转换成一种度量方式,取值区间为[0,1]
-
from minepy import MINE x = np.random.normal(0,10,300) z = x * x m = MINE() m.compute_score(x,z) print(m.mic())
-
3.2.3 距离相关系数
-
为了克服Pearson相关系数只对线性数据敏感的弱点而生
-
-
x 和 x 2 x^2 x2 的pearson相关系数为0 但是并不独立
-
''' pdist 计算距离函数 squareform 用来压缩矩阵函数 numbapri 加速运行 ''' from scipy.spatial.distance import pdist,squareform import numpy as np from numbapro import jit,float32 def distcorr(X, Y): """ Compute the distance correlation function >>> a = [1,2,3,4,5] >>> b = np.array([1,2,9,4,4]) >>> distcorr(a, b) 0.762676242417 """ # 将输入视为最少一维的数组 X = np.atleast_1d(X) Y = np.atleast_1d(Y) # np.prod 所有元素的乘积 if np.prod(X.shape) == len(X): X = X[:, None] if np.prod(Y.shape) == len(Y): Y = Y[:, None] X = np.atleast_2d(X) Y = np.atleast_2d(Y) n = X.shape[0] if Y.shape[0] != X.shape[0]: raise ValueError('Number of samples must match') a = squareform(pdist(X)) b = squareform(pdist(Y)) A = a - a.mean(axis=0)[None, :] - a.mean(axis=1)[:, None] + a.mean() B = b - b.mean(axis=0)[None, :] - b.mean(axis=1)[:, None] + b.mean() dcov2_xy = (A * B).sum()/float(n * n) dcov2_xx = (A * A).sum()/float(n * n) dcov2_yy = (B * B).sum()/float(n * n) dcor = np.sqrt(dcov2_xy)/np.sqrt(np.sqrt(dcov2_xx) * np.sqrt(dcov2_yy)) return dcor
3.2.4 基于学习模型的特征排序 cross_val_score
- 直接使用你要用的机器学习算法,针对每个单独的特征和响应变量建立预测模型
- 如果特征与响应变量之间的关系是非线性的,可以用树的方法
from sklearn.model_selection import cross_val_score, ShuffleSplit
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
#Load boston housing dataset as an example
boston = load_boston()
X = boston["data"]
Y = boston["target"]
names = boston["feature_names"]
# 树的个数 n_estimators = 20
rf = RandomForestRegressor(n_estimators=20, max_depth=4)
scores = []
# 使用每个特征单独训练模型,并获取每个模型的评分来作为特征选择的依据。
# cross_val_score(estimator,X,Y,打分方式,cv 几折)
# ShuffleSplit(n_splits 划分次数 test_size 比例)
for i in range(X.shape[1]):
score = cross_val_score(rf, X[:, i:i+1], Y, scoring="r2",
cv=ShuffleSplit(n_splits=3,test_size=0.3))
scores.append((round(np.mean(score),3),names[i]))
print(sorted(scores, reverse=True))
输出:[(0.659, 'LSTAT'), (0.544, 'RM'), (0.473, 'INDUS'), (0.407, 'NOX'),
(0.319, 'PTRATIO'), (0.258, 'TAX'), (0.221, 'CRIM'), (0.19, 'RAD'),
(0.145, 'ZN'), (0.136, 'DIS'), (0.101, 'AGE'), (0.084, 'B'),
(-0.002, 'CHAS')]
3.2.5 卡方检验 离散型 chi2
-
两个事件的独立性或者描述实际观察值与期望值的偏离程度。
-
卡方值越大,实际观察值与期望值偏离越大,说明两个变量越不可能是独立无关的,
-
CHI值越大说明对原假设的偏离越大
-
在特征选择中,低卡方值表明它们具有相似的类分布
-
参考链接
-
Y 需要是离散型
-
from sklearn.feature_selection import SelectKBest ,chi2 #选择相关性最高的前5个特征 X_chi2 = SelectKBest(chi2, k=5).fit_transform(X, Y) X_chi2.shape 输出:(27, 5)
3.3 线性模型与正则化
-
单变量特征选择可以用于理解数据、数据的结构、特点,也可以用于排除不相关特征,但是它不能发现冗余特征
-
正则化模型
-
把额外的约束或者惩罚项加到已有模型(损失函数)上,以防止过拟合并提高泛化能力。损失函数由原来的E(X,Y)变为E(X,Y)+alpha||w||
- w是模型系数组成的向量(有些地方也叫参数parameter,coefficients)
- ||·||一般是L1或者L2范数,alpha是一个可调的参数,控制着正则化的强度。
- L1正则化和L2正则化也称为Lasso和Ridge。
3.3.1 L1范式 Lasso
-
L1范数是指向量中各个元素绝对值之和
-
因此L1正则化迫使那些弱的特征所对应的系数变成0,往往会使学到的模型很稀疏(系数w经常为0),这个特性使得L1正则化成为一种很好的特征选择方法。
-
Lasso能够挑出一些优质特征,同时让其他特征的系数趋于0
3.3.2 L2范式 Ridge
-
L2范数是指向量各元素的平方和然后求平方根
-
L2正则化会让系数的取值变得平均,不会变得稀疏
-
L2正则化对于特征选择来说一种稳定的模型,不像L1正则化那样,系数会因为细微的数据变化而波动。L2正则化对于特征理解来说更加有用:表示能力强的特征对应的系数是非零。
3.3.3 代码实现
-
普通线性模型
-
from sklearn.datasets import load_boston from sklearn.linear_model import LinearRegression boston = load_boston() X = boston.data Y = boston.target reg = LinearRegression() reg.fit(X,Y) #coef 系数 按照重要度排序 获得下标 coefSort = reg.coef_.argsort() featureName = boston.feature_names[coefSort] featureCoefScore = reg.coef_[coefSort] print("featureName:", featureName) print("featureCoefSore:", featureCoefScore) ''' featureName: ['NOX' 'DIS' 'PTRATIO' 'LSTAT' 'CRIM' 'TAX' 'AGE' 'B' 'INDUS' 'ZN' 'RAD' 'CHAS' 'RM'] featureCoefSore: [-1.77666112e+01 -1.47556685e+00 -9.52747232e-01 -5.24758378e-01 -1.08011358e-01 -1.23345939e-02 6.92224640e-04 9.31168327e-03 2.05586264e-02 4.64204584e-02 3.06049479e-01 2.68673382e+00 3.80986521e+00] '''
-
L1正则化线性模型
-
from sklearn.linear_model import Lasso from sklearn.preprocessing import StandardScaler from sklearn.datasets import load_boston def print_linear(coefs,names=None,sort=False): if len(names)==0: names = ["X%s" % x for x in range(len(coefs))] lst = zip(coefs,names) if sort: lst = sorted(lst,key=lambda x:abs(x[0]),reverse=True) return ' + '.join('%s * %s' %(round(coef,3),name) for coef,name in lst) boston = load_boston() scaler = StandardScaler() X = scaler.fit_transform(boston.data) Y = boston.target featureNames = boston.feature_names lasso = Lasso(alpha=.3) lasso.fit(X,Y) print("Lasso model:{}".format(print_linear(lasso.coef_,names=featureNames,sort=True))) ''' Lasso model:-3.705 * LSTAT + 2.993 * RM + -1.756 * PTRATIO + -1.081 * DIS + -0.699 * NOX + 0.628 * B + 0.54 * CHAS + -0.242 * CRIM + 0.082 * ZN + -0.0 * INDUS + -0.0 * AGE + 0.0 * RAD + -0.0 * TAX '''
-
L2正则化线性模型
-
from sklearn.linear_model import Ridge,LinearRegression def print_liner(coefs,names=None,sort=False): if len(names)==0: names = ["X%s" % x for x in range(len(coefs))] lst = zip(coefs,names) if sort: lst = sorted(lst,key=lambda x:abs(x[0]),reverse=True) return ' + '.join('%s * %s' %(round(coef,3),name) for coef,name in lst) for i in range(5): np.random.seed(seed=i) print("random seed {}".format(i)) x_seed = np.random.normal(0,.1,100) X1 = x_seed + np.random.normal(0,.1,100) X2 = x_seed + np.random.normal(0,.1,100) X3 = x_seed + np.random.normal(0,.1,100) Y = X1 + X2 + X3 +np.random.normal(0,.1,100) X = np.array([X1,X2,X3]).T lr = LinearRegression() lr.fit(X,Y) print("Linear model: {}".format(print_linear(lr.coef_))) ridge = Ridge(alpha=10) ridge.fit(X,Y) print("Ridge model: {}".format(print_linear(ridge.coef_))) ''' random seed 0 Linear model: 0.728 * X0 + 2.309 * X1 + -0.082 * X2 Ridge model: 0.938 * X0 + 1.059 * X1 + 0.877 * X2 random seed 1 Linear model: 1.152 * X0 + 2.366 * X1 + -0.599 * X2 Ridge model: 0.984 * X0 + 1.068 * X1 + 0.759 * X2 random seed 2 Linear model: 0.697 * X0 + 0.322 * X1 + 2.086 * X2 Ridge model: 0.972 * X0 + 0.943 * X1 + 1.085 * X2 random seed 3 Linear model: 0.287 * X0 + 1.254 * X1 + 1.491 * X2 Ridge model: 0.919 * X0 + 1.005 * X1 + 1.033 * X2 random seed 4 Linear model: 0.187 * X0 + 0.772 * X1 + 2.189 * X2 Ridge model: 0.964 * X0 + 0.982 * X1 + 1.098 * X2 线性回归的系数变化很大,具体取决于生成的数据。 对于L2正则化模型,系数非常稳定并且密切反映数据的生成方式(所有系数接近1) '''
3.4 随机森林选择
- 随机森林准确性高、鲁棒性好、易于使用
3.4.1 平均不纯度减少(基尼系数) RandomForestRegressor
-
随机森林由多颗CART决策树构成,决策树中的每一个节点都是关于某个特征的条件
-
对于分类问题,一般采用基尼系数
-
对于回归问题,通常采用的是方差或者最小二乘拟合。
-
当训练决策树的时候,可以计算出每个特征减少了多少树的不纯度。并把它平均减少的不纯度作为特征选择的标准。基尼系数越小这个特征越好
-
from sklearn.datasets import load_boston from sklearn.ensemble import RandomForestRegressor import numpy as np boston = load_boston() X = boston.data Y = boston.target featureNames = boston.feature_names rf = RandomForestRegressor() rf.fit(X, Y) print("Features sorted by their score:") print(sorted(zip(map(lambda x: round(x,4),rf.feature_importances_),featureNames),reverse=True)) ''' Features sorted by their score: [(0.4429, 'RM'), (0.3648, 'LSTAT'), (0.0666, 'DIS'), (0.0365, 'CRIM'), (0.0229, 'NOX'), (0.0163, 'PTRATIO'), (0.0134, 'TAX'), (0.0133, 'AGE'), (0.0112, 'B'), (0.006, 'INDUS'), (0.0036, 'RAD'), (0.0013, 'CHAS'), (0.0012, 'ZN')] '''
3.4.2 平均精确度减少
-
通过直接度量每个特征对模型精确率的影响来进行特征选择。
-
主要思路是打乱每个特征的特征值顺序,并且度量顺序变动对模型的精确率的影响
- 对于不重要的变量来说,打乱顺序对模型的精确率影响不会太大
- 对于重要的变量来说,打乱顺序就会降低模型的精确率
-
from sklearn.model_selection import ShuffleSplit from sklearn.metrics import r2_score from sklearn.ensemble import RandomForestRegressor from collections import defaultdict from sklearn.datasets import load_boston boston = load_boston() X = boston.data Y = boston.target featureNames = boston.feature_names ''' r2_score r2=1-(真实值和预测值的平方差)/(真实值和均值的平方差) # https://aijishu.com/a/1060000000079690 https://blog.csdn.net/u012735708/article/details/84337262 ''' rf = RandomForestRegressor() scores = defaultdict(list) for train_idx,test_idx in ShuffleSplit(len(X),n_splits=3,test_size=0.3).split(X): X_train,X_test = X[train_idx],X[test_idx] Y_train,Y_test = Y[train_idx],Y[test_idx] rf.fit(X_train,Y_train) acc = r2_score(Y_test,rf.predict(X_test)) for i in range(X.shape[1]): X_t = X_test.copy() np.random.shuffle(X_t[:,i]) shuff_acc = r2_score(Y_test,rf.predict(X_t)) scores[featureNames[i]].append((acc-shuff_acc)/acc) print("Feature sorted by scores") print(sorted([(round(np.mean(score),4),feat) for feat,score in scores.items()],reverse=True)) ''' Feature sorted by scores [(0.6932, 'LSTAT'), (0.543, 'RM'), (0.0797, 'DIS'), (0.0362, 'CRIM'), (0.0353, 'NOX'), (0.0197, 'PTRATIO'), (0.0134, 'TAX'), (0.0085, 'AGE'), (0.0047, 'B'), (0.0041, 'INDUS'), (0.0024, 'RAD'), (0.0002, 'CHAS'), (0.0001, 'ZN')] '''
3.5 顶层特征选择
3.5.1 稳定性选择
-
稳定性选择是一种基于二次抽样和选择算法相结合较新的方法,选择算法可以是回归、SVM或其他类似的方法。
-
它的主要思想是在不同的数据子集和特征子集上运行特征选择算法,不断的重复,最终汇总特征选择结果。
-
比如可以统计某个特征被认为是重要特征的频率(被选为重要特征的次数除以它所在的子集被测试的次数)。
-
from sklearn.linear_model import RandomizedLasso #可能存在版本问题 from sklearn.datasets import load_boston boston = load_boston() X = boston.data Y = boston.target featureNames = boston.feature_names rlasso = RandomizedLasso(alpha=0.025) rlasso.fit(X,Y) print("Features sorted by their score:") print(sorted(zip(map(lambda x:round(x,4),rlasso.score_),featureNames),reverse=True))
3.5.2 递归特征消除 RFE
-
递归特征消除的主要思想是反复的构建模型(如SVM或者回归模型)然后选出最好的(或者最差的)的特征(可以根据系数来选),把选出来的特征放到一遍,然后在剩余的特征上重复这个过程,直到所有特征都遍历了。
-
这个过程中特征被消除的次序就是特征的排序。因此,这是一种寻找最优特征子集的贪心算法。
-
RFE的稳定性很大程度上取决于在迭代的时候底层用哪种模型。
-
- 假如RFE采用的普通的回归,没有经过正则化的回归是不稳定的,那么RFE就是不稳定的。
- 假如RFE采用的是Ridge,而用Ridge正则化的回归是稳定的,那么RFE就是稳定的。
-
from sklearn.datasets import load_boston from sklearn.feature_selection import RFE from sklearn.linear_model import LinearRegression boston = load_boston() X = boston.data Y = boston.target featureNames = boston.feature_names lr = LinearRegression() rfe = RFE(lr, n_features_to_select=3) rfe.fit(X,Y) print("Features sorted by their rank:") print(sorted(zip(map(lambda x: round(x, 4), rfe.ranking_),featureNames))) ''' Features sorted by their rank: [(1, 'CHAS'), (1, 'NOX'), (1, 'RM'), (2, 'PTRATIO'), (3, 'DIS'), (4, 'LSTAT'), (5, 'RAD'), (6, 'CRIM'), (7, 'INDUS'), (8, 'ZN'), (9, 'TAX'), (10, 'B'), (11, 'AGE')] '''
4. 总结
- 关于训练模型的特征筛选
- 数据预处理后,先排除取值变化很小的特征。如果机器资源充足,并且希望尽量保留所有信息,可以把阈值设置得比较高,或者只过滤离散型特征只有一个取值的特征。
- 如果数据量过大,计算资源不足(内存不足以使用所有数据进行训练、计算速度过慢),可以使用单特征选择法排除部分特征。这些被排除的特征并不一定完全被排除不再使用,在后续的特征构造时也可以作为原始特征使用。
- 如果此时特征量依然非常大,或者是如果特征比较稀疏时,可以使用PCA主成分分析和LDA线性判别等方法进行特征降维。
- 经过样本采样和特征预筛选后,训练样本可以用于训练模型。但是可能由于特征数量比较大而导致训练速度慢,或者想进一步筛选有效特征或排除无效特征(或噪音),我们可以使用正则化线性模型选择法、随机森林选择法或者顶层特征选择法进一步进行特征筛选。
- 特征筛选是为了理解数据或更好地训练模型,我们应该根据自己的目标来选择适合的方法。为了更好/更容易地训练模型而进行的特征筛选,如果计算资源充足,应尽量避免过度筛选特征,因为特征筛选很容易丢失有用的信息。如果只是为了减少无效特征的影响,为了避免过拟合,可以选择随机森林和XGBoost等集成模型来避免对特征过拟合。