使用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会查找优化二元分类任务的系数。
  • 在手动选择前,探索性数据分析会很有益处。不能低估领域知识的重要性。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容