Loading

使用scikit-learn进行特征选择

参考文章:《特征工程入门与实践》——第5章 特征选择:对坏属性说不
信用卡逾期数据集:credit card clients Data Set
本文代码开源链接: FeatureSelection

本文主要以信用卡逾期分类任务作为案例,讲解如何使用sklearn进行特征选择。

特征选择(Feature Selection)是特征工程中的一个重要步骤,指的是从原始数据中选择对于预测模型最好的特征的过程。给定n个特征,搜索其中k(小于等于n)个特征的子集来改善机器学习的性能。

上面这个定义包括两个需要解决的问题:

  1. 对机器学习性能的评估;
  2. 找到k个特征子集的方法。

1 机器学习模型的评估方法

在机器学习中,我们的目标是实现更好的预测性能,因此可以用评估预测性能的指标来衡量。例如分类任务的准确率和回归任务的均方根误差。

除此之外,也可以测量模型的元指标,元指标是指不直接与模型预测性能相关的指标,包括:

  • 模型拟合/训练所需的时间;
  • 拟合后的模型预测新实例的时间;
  • 需要持久化(永久保存)的数据大小。

这些元指标补充了机器学习模型的评估方法。

下面定义一个函数,作为机器学习模型性能的评估标准,后面都以这个函数对模型进行评估:

from sklearn.model_selection import GridSearchCV  # 导入网格搜索模块

# 定义一个函数搜索给定的参数,同时输出评估指标
def get_best_model_and_accuracy(model, params, X, y):
    grid = GridSearchCV(
        model,  # 要搜索的模型
        params,  # 要尝试的参数
        error_score=0.  # 如果报错,结果是0
    )
    grid.fit(X, y)  # 拟合模型和参数
    
    # 经典的性能指标
    print("Best Accuracy: {}".format(grid.best_score_))  # 最佳准确率
    print("Best Parameters: {}".format(grid.best_params_))  # 最佳参数
    print("Average Time to Fit (s): {}".format(round(grid.cv_results_['mean_fit_time'].mean(), 3)))  # 拟合的平均时间(秒)
    print("Average Time to Score (s): {}".format(round(grid.cv_results_['mean_score_time'].mean(), 3)))  # 预测的平均时间(秒),从该指标可以看出模型在真实世界的性能

特征选择算法可以智能地从数据中提取最重要的信号,并且实现以下两个效果:

  • 提升模型性能;
  • 减少训练和预测时间。

接下来从数据网站上下载信用卡逾期数据集并读取导入:

import pandas as pd
import numpy as np

# 用随机数种子保证随机数永远一致
np.random.seed(123)

# 导入数据集
credit_card_default = pd.read_excel('../datasets/default of credit card clients.xls', skiprows=1)

# 描述性统计,转置方便观察
credit_card_default.describe().T

描述性统计

数据有30000行和24列(除去1列ID),其中23列为属性值,1列为标签(default payment next month,下个月逾期)。

接下来将数据集按照特征值和标签值进行切分:

# 特征矩阵
X = credit_card_default.drop(['ID', 'default payment next month'], axis=1)

# 标签
y = credit_card_default['default payment next month']

查看一下标签值的分布情况:

y.value_counts(normalize=True)

标签值分布

由于是分类任务,所以取一个空准确率作为基准,确保机器学习的性能比基准更好。

空准确率:指的是模型只预测为1类(不做任何预测),也能达到的一个准确率,在本例中,模型把所有类别都预测为0,准确率也就是77.88%,所以准确率需要超过77.88%才有意义。

2 创建基准机器学习流水线

下面以决策树作为我们的Baseline,同时运行一下评估函数评估其性能:

from sklearn.tree import DecisionTreeClassifier

# 设置模型参数
tree_params = {
    'max_depth': [None, 1, 3, 5, 7]
}

d_tree = DecisionTreeClassifier()

# 运行模型评估函数
get_best_model_and_accuracy(d_tree, tree_params, X, y)

输出结果:

Best Accuracy: 0.8206333333333333
Best Parameters: {'max_depth': 3}
Average Time to Fit (s): 0.156
Average Time to Score (s): 0.002

从结果看出Baseline的决策树模型准确率为0.8206,后面我们需要击败的基线准确率就以此作为标准。

特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline(选取所有特征) 决策树 0.8206 0.156 0.002

3 两类特征选择方法

这里介绍两种类型的方法进行特征选择:

  • 基于统计的特征选择:依赖于统计测试;
  • 基于模型的特征选择:依赖于预处理步骤,需要训练一个辅助的机器学习模型,并利用其预测能力来选择特征(简单来说就是训练一个模型来筛选特征)。

3.1 基于统计的特征选择

基于统计主要介绍两个方法帮助选择特征:

  • 皮尔逊相关系数(pearson correlations);
  • 假设检验。

这两个方法都是单变量方法,也就是用于选择单一特征。

3.1.1 使用皮尔逊相关系数

皮尔逊相关系数会测量列(属性)之间的线性关系,在-1~1之间变化,0代表没有线性关系,-1/1代表线性关系很强。
皮尔逊相关系数要求每列是正态分布的,但是因为数据集很大(超过500的阈值),所以可以忽略这个要求。

注:根据中心极限定理,当数据量足够大时,可以认为数据是近似正态分布的。

接下来计算每一列特征之间的相关系数,同时用热力图进行可视化:

import seaborn as sns
from matplotlib import style

# 选用一个干净的主题
style.use('fivethirtyeight')

sns.heatmap(credit_card_default.corr())

相关系数

接下来过滤出相关系数超过正负0.2的特征,重新训练模型:

# 过滤出相关系数超过正负0.2的特征
highly_correlated_features = credit_card_default.columns[credit_card_default.corr()['default payment next month'].abs() > .2]
# 删除标签列
highly_correlated_features = highly_correlated_features.drop('default payment next month')

# 只选取5个高度关联的特征值,训练模型
X_subsetted = X[highly_correlated_features]

get_best_model_and_accuracy(d_tree, tree_params, X_subsetted, y)

结果如下:

Best Accuracy: 0.8213333333333332
Best Parameters: {'max_depth': 3}
Average Time to Fit (s): 0.009
Average Time to Score (s): 0.002
特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline(选取所有特征) 决策树 0.8206 0.156 0.002
相关系数 决策树 0.8213 0.009 0.002

从结果可以看出,模型准确率提升了一点点,但是拟合时间快了将近20倍。

3.1.1 使用假设检验

假设检验是一种统计学方法,可以对单个特征进行复杂的统计检验。
零假设 \(H_0\)为:特征与标签值没有关系。p值越低,拒绝零假设的概率越大,即特征与标签有关联的概率就越大,我们应该保留这个特征。

以假设检验作为特征选择器,训练模型:

from sklearn.feature_selection import SelectKBest  # 在给定目标函数后选择k个最高分
from sklearn.feature_selection import f_classif  # ANOVA测试

k_best = SelectKBest(f_classif)

# 用SelectKBest建立流水线
select_k_pipe = Pipeline([
    ('k_best', k_best),    # 特征选择器
    ('classifier', d_tree)  # 决策树分类器
])

# 流水线参数
select_k_best_pipe_params = ({
    'k_best__k': list(range(1, 23)) + ['all'],  # 特征选择器参数,all表示选择全部
    'classifier__max_depth': [None, 1, 3, 5, 7, 9, 11, 17, 15, 17, 19, 21]  # 决策树参数
})

get_best_model_and_accuracy(select_k_pipe, select_k_best_pipe_params, X, y)

模型评估结果:

Best Accuracy: 0.8213333333333332
Best Parameters: {'classifier__max_depth': 3, 'k_best__k': 5}
Average Time to Fit (s): 0.105
Average Time to Score (s): 0.003

查看选择了哪些特征:

# 把列名以及对应的p值组成DataFrame,按照p值进行排序
p_values = pd.DataFrame({
    'columns': X.columns,
    'p_value': k_best.pvalues_
}).sort_values('p_value')

# 假设检验最终选择了5个特征
p_values.head(5)

p值

假设检验最终选择的特征值是和相关系数选择的一样的。可以看出前5个特征的p值极小,几乎为0。

p值的一个常见阈值时0.05,意思是p值小于0.05的特征是显著的。

特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline(选取所有特征) 决策树 0.8206 0.156 0.002
相关系数 决策树 0.8213 0.009 0.002
假设检验 决策树 0.8213 0.105 0.003

3.2 基于模型的特征选择

3.2.1 使用决策树选择特征

下面引入SelectFromModel,和SelectKBest一样会选取最重要的前k特征,但是它使用的是机器学习模型的内部指标来评估特征的重要性

from sklearn.feature_selection import SelectFromModel

# 在流水线中使用 SelectFromModel 这个方法

# 创建基于 DecisionTreeClassifier 的 SelectFromModel
select = SelectFromModel(DecisionTreeClassifier())

select_from_pipe = Pipeline([
    ('select', select),  # 特征选择器
    ('classifier', d_tree)  # 分类器
])

select_from_pipe_params = {
    # 特征选择器参数
    'select__threshold': [.01, .05, .1, .2, .25, .3, .4, .5, .6, "mean", "median", "2.*mean"],
    'select__estimator__max_depth': [None, 1, 3, 5, 7],
    # 分类器参数
    'classifier__max_depth': [1, 3, 5, 7],
}

get_best_model_and_accuracy(select_from_pipe, select_from_pipe_params, X, y)

评估结果:

Best Accuracy: 0.8206333333333333
Best Parameters: {'classifier__max_depth': 3, 'select__estimator__max_depth': 1, 'select__threshold': 'median'}
Average Time to Fit (s): 0.176
Average Time to Score (s): 0.002

可以看到结果没有比原来的好。

特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline(选取所有特征) 决策树 0.8206 0.156 0.002
相关系数 决策树 0.8213 0.009 0.002
假设检验 决策树 0.8213 0.105 0.003
决策树 决策树 0.8206 0.176 0.002

列出模型选择的特征:

# 使用SelectFromModel的get_support()方法,列出选择的特征

# 设置流水线最佳参数
select_from_pipe.set_params(**{
    'classifier__max_depth': 3, 
    'select__estimator__max_depth': 1, 
    'select__threshold': 'median'
})

# 拟合数据
select_from_pipe.steps[0][1].fit(X, y)

# 列出选择的列
X.columns[select_from_pipe.steps[0][1].get_support()]
Index(['LIMIT_BAL', 'SEX', 'EDUCATION', 'MARRIAGE', 'AGE', 'PAY_0', 'PAY_2',
       'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6', 'BILL_AMT1', 'BILL_AMT2',
       'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1',
       'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6'],
      dtype='object')

这颗树选择了除了两个特征外的所有其他特征,但是和特征较少的树性能没什么区别。

3.2.2 使用逻辑回归作为选择器

用逻辑回归模型作为选择器,在L1和L2范数上进行网格搜索。

# 用正则化后的逻辑回归进行选择
logistic_selector = SelectFromModel(LogisticRegression(
    solver='liblinear'  # 注意:这里必须设置这个solver,否则不支持l1正则
))

# 新流水线,用逻辑回归作为参数选择器
regularization_pipe = Pipeline([
    ('select', logistic_selector),
    ('classifier', tree)
])

# 流水线参数
regularization_pipe_params = {
    # 逻辑回归特征选择器参数
    'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"],
    'select__estimator__penalty': ['l1', 'l2'],  # L1和L2正则化
    # 分类器参数
    'classifier__max_depth': [1, 3, 5, 7],
}


get_best_model_and_accuracy(regularization_pipe, regularization_pipe_params, X, y)

评估结果:

Best Accuracy: 0.8212666666666667
Best Parameters: {'classifier__max_depth': 3, 'select__estimator__penalty': 'l1', 'select__threshold': 0.05}
Average Time to Fit (s): 0.53
Average Time to Score (s): 0.002

列出模型选择的特征:

# 使用SelectFromModel的get_support()方法,列出选择的特征

# 设置流水线最佳参数
regularization_pipe.set_params(**{
    'classifier__max_depth': 3, 
    'select__estimator__penalty': 'l1', 
    'select__threshold': 0.05
})

# 拟合数据
regularization_pipe.steps[0][1].fit(X, y)

# 列出选择的列
X.columns[regularization_pipe.steps[0][1].get_support()]
Index(['SEX', 'EDUCATION', 'MARRIAGE', 'PAY_0', 'PAY_2', 'PAY_3'], dtype='object')
特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline(选取所有特征) 决策树 0.8206 0.156 0.002
相关系数 决策树 0.8213 0.009 0.002
假设检验 决策树 0.8213 0.105 0.003
决策树 决策树 0.8206 0.176 0.002
逻辑回归 决策树 0.8212 0.53 0.002

3.2.3 使用SVM作为选择器

以支持向量机作为特征选择器,构建机器学习流水线:

from sklearn.svm import LinearSVC  # SVC是线性模型,只能分割二分类数据

# 用SVC选择特征
svc_selector = SelectFromModel(LinearSVC())

svc_pipe = Pipeline([
    ('select', svc_selector),
    ('classifier', tree)
])

svc_pipe_params = {
    # SVC特征选择器参数
    'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"],
    'select__estimator__penalty': ['l1', 'l2'],  # L1和L2正则化
    'select__estimator__loss': ['squared_hinge', 'hinge'],
    'select__estimator__dual': [True, False],
    # 分类器参数
    'classifier__max_depth': [1, 3, 5, 7],
}

get_best_model_and_accuracy(svc_pipe, svc_pipe_params, X, y)

模型评估结果:

Best Accuracy: 0.8212666666666667
Best Parameters: {'classifier__max_depth': 3, 'select__estimator__dual': False, 'select__estimator__loss': 'squared_hinge', 'select__estimator__penalty': 'l1', 'select__threshold': 'mean'}
Average Time to Fit (s): 0.97
Average Time to Score (s): 0.001

查看一下选择器最终选择了哪些特征:

# 看一下选择器选择了哪些特征

# 设置流水线最佳参数
svc_pipe.set_params(**{
    'classifier__max_depth': 3, 
    'select__estimator__dual': False, 
    'select__estimator__loss': 'squared_hinge', 
    'select__estimator__penalty': 'l1', 
    'select__threshold': 'mean'
})

# 拟合数据
svc_pipe.steps[0][1].fit(X, y)

# 列出选择的列
X.columns[svc_pipe.steps[0][1].get_support()]

输出结果:

Index(['SEX', 'EDUCATION', 'MARRIAGE', 'PAY_0', 'PAY_2', 'PAY_3'], dtype='object')
特征选择器 分类器 准确率 平均拟合时间 平均预测时间
Baseline 决策树 0.8206 0.156 0.002
相关系数 决策树 0.8213 0.009 0.002
假设检验 决策树 0.8213 0.105 0.003
决策树 决策树 0.8206 0.176 0.002
逻辑回归 决策树 0.8212 0.53 0.002
支持向量机 决策树 0.8212 0.97 0.001

4.小结

本文主要介绍了两大类特征选择方法:

  1. 基于统计
    • 相关系数
    • 假设检验
    • 方差分析
    • ...
  2. 基于模型
    • 决策树
    • 逻辑回归
    • SVC
    • ...

下面是一些关于如何选用特征选择方法的经验:

  • 如果特征是分类的,那么从SelectKBest开始,用卡方或基于树的选择器。
  • 如果特征基本是定量的(例如本例),用线性模型和基于相关性的选择器一般效果更好。
  • 如果是二元分类问题,考虑使用SelectFromModel和SVC,因为SVC会查找优化二元分类任务的系数。
  • 在手动选择前,探索性数据分析会很有益处。不能低估领域知识的重要性。
posted @ 2021-12-24 09:29  活用数据  阅读(203)  评论(0编辑  收藏  举报