Scikit-learn的模型设计与选择

2019-07-12 11:51:36 浏览数 (1)

目的:本文的目的是从头到尾构建一个管道,以便在合成数据集上访问18个机器学习模型的预测性能。

材料和方法:使用Scikit-learn,为分类任务生成类似Madelon的数据集。工作流程的主要组成部分可归纳如下:(1)创建训练和测试集。(2)然后通过Z分数归一化来缩放特征。(3)应用特征选择算法以减少特征的数量。(4)训练和评估机器学习算法。将使用接收器操作曲线(AUC)下的面积来评估18个训练分类器的预测能力。

硬件:在配备Inter(R)Core(TM)i7-8700和12 CPU @ 3.70 Ghz以及NVIDIA GeForce RTX 2080的工作站上训练和评估模型。

注意:如果是从头开始,欢迎克隆包含本文全部内容的存储库。

https://github.com/frank-ceballos/Model_Design-Selection

数据集

将使用Scikit-learn为分类任务生成类似Madelon的合成数据集。Madelon数据集是一个人工数据集,其中包含32个簇,这些簇位于具有长度为1的边的五维超立方体的顶点上。这些簇随机标记为1或-1(2个类)。

将生成的数据集将包含30个特征,其中5个将提供信息,15个将是冗余的(但提供信息),其中5个将重复,最后5个将无用,因为将随机填充噪声。数据集的列将按如下顺序排序:

  1. 信息功能 - 第1-5列:这些功能是构建模型所需的唯一功能。因此,一个五维超立方体。
  2. 冗余功能 - 第6-20列。这些特征是通过将信息特征与不同的随机权重线性组合而产生的。可以将这些视为工程特征。
  3. 重复特征 - 第21-25列:这些特征是从信息或冗余特征中随机绘制的。
  4. 无用的功能 - 第26-30栏。这些功能充满了随机噪音。

从导入库开始。

代码语言:javascript复制
###############################################################################
#                          1. Importing Libraries                             #
###############################################################################
# For reading, visualizing, and preprocessing data
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.feature_selection import RFE, RFECV
from sklearn.model_selection import train_test_split, GridSearchCV, KFold
from sklearn.preprocessing import StandardScaler, Imputer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics
 
# Classifiers
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis
from sklearn.ensemble import AdaBoostClassifier, BaggingClassifier, ExtraTreesClassifier, GradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import RidgeClassifier, SGDClassifier
from sklearn.naive_bayes import BernoulliNB, GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.svm import LinearSVC, NuSVC, SVC
from sklearn.tree import DecisionTreeClassifier, ExtraTreeClassifier

脚本1 - 导入库。

现在可以生成数据集。

代码语言:javascript复制
###############################################################################
#                                 2. Get data                                 #
###############################################################################
X, y = make_classification(n_samples = 1000, n_features = 30, n_informative = 5,
                           n_redundant = 15, n_repeated = 5,
                           n_clusters_per_class = 2, class_sep = 0.5,
                           random_state = 1000, shuffle = False)
 
# Numpy array to pandas dataframe
labels = [f"Feature {ii 1}" for ii in range(X.shape[1])]
X = pd.DataFrame(X, columns = labels)
y = pd.DataFrame(y, columns = ["Target"])

脚本2 - 获取数据:请注意设置random_state = 1000.这样做只是为了确保将使用相同的数据。如果需要可以将其删除。

通过随机抽样而无需替换,创建了训练和测试集。

代码语言:javascript复制
###############################################################################
#                        3. Create train and test set                         #
###############################################################################
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20,
                                                    random_state = 1000)

脚本3 - 创建训练和测试集。测试集的大小设置为数据的20%。

分类

将训练和调整18个分类器,并使用接收器操作曲线(AUC)下的区域评估其性能。讨论每个分类器的任何技术细节超出了本文的范围; 但是对于感兴趣的读者,可以按照下面显示的列表中的链接进行操作。每个分类器都有一个标签,用括号中的字符串表示。

  • 线性判别分析(LDA)
  • 二次判别分析(QDA)
  • Adaboost分类器(AdaBoost)
  • 套袋分级机(套袋)
  • 额外树木分类(额外树木合奏)
  • 梯度增强分级器(渐变增强)
  • 随机森林分类器(随机森林)
  • 岭分类器(岭)
  • SGD分类器(SGD)
  • 伯努利NB分类器(BNB)
  • 高斯NB分类器(GNB)
  • K最近邻分类器(KNN)
  • MLP分类器(MLP)
  • 线性SVC(LSVC)
  • Nu SVC(NuSVC)
  • SVC(SVC)
  • 决策树分类器(DTC)
  • 额外树分类器(ETC)
代码语言:javascript复制
###############################################################################
#                               4. Classifiers                                #
###############################################################################
# Create list of tuples with classifier label and classifier object
classifiers = {}
classifiers.update({"LDA": LinearDiscriminantAnalysis()})
classifiers.update({"QDA": QuadraticDiscriminantAnalysis()})
classifiers.update({"AdaBoost": AdaBoostClassifier()})
classifiers.update({"Bagging": BaggingClassifier()})
classifiers.update({"Extra Trees Ensemble": ExtraTreesClassifier()})
classifiers.update({"Gradient Boosting": GradientBoostingClassifier()})
classifiers.update({"Random Forest": RandomForestClassifier()})
classifiers.update({"Ridge": RidgeClassifier()})
classifiers.update({"SGD": SGDClassifier()})
classifiers.update({"BNB": BernoulliNB()})
classifiers.update({"GNB": GaussianNB()})
classifiers.update({"KNN": KNeighborsClassifier()})
classifiers.update({"MLP": MLPClassifier()})
classifiers.update({"LSVC": LinearSVC()})
classifiers.update({"NuSVC": NuSVC()})
classifiers.update({"SVC": SVC()})
classifiers.update({"DTC": DecisionTreeClassifier()})
classifiers.update({"ETC": ExtraTreeClassifier()})
 
# Create dict of decision function labels
DECISION_FUNCTIONS = {"Ridge", "SGD", "LSVC", "NuSVC", "SVC"}
 
# Create dict for classifiers with feature_importances_ attribute
FEATURE_IMPORTANCE = {"Gradient Boosting", "Extra Trees Ensemble", "Random Forest"}

脚本4 - 初始化分类器。

分类器超参数

在这里,将创建一个键值对包含的字典

  • key:表示分类器的字符串
  • value:相应分类器的超参数字典

这里使用的超参数决不代表每个分类器的最佳超参数网格。欢迎根据需要更改超参数网格。

代码语言:javascript复制
###############################################################################
#                             5. Hyper-parameters                             #
###############################################################################
# Initiate parameter grid
parameters = {}
 
# Update dict with LDA
parameters.update({"LDA": {"classifier__solver": ["svd"],
                                         }})
 
# Update dict with QDA
parameters.update({"QDA": {"classifier__reg_param":[0.01*ii for ii in range(0, 101)],
                                         }})
# Update dict with AdaBoost
parameters.update({"AdaBoost": {
                                "classifier__base_estimator": [DecisionTreeClassifier(max_depth = ii) for ii in range(1,6)],
                                "classifier__n_estimators": [200],
                                "classifier__learning_rate": [0.001, 0.01, 0.05, 0.1, 0.25, 0.50, 0.75, 1.0]
                                 }})
 
# Update dict with Bagging
parameters.update({"Bagging": {
                                "classifier__base_estimator": [DecisionTreeClassifier(max_depth = ii) for ii in range(1,6)],
                                "classifier__n_estimators": [200],
                                "classifier__max_features": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
                                "classifier__n_jobs": [-1]
                                }})
 
# Update dict with Gradient Boosting
parameters.update({"Gradient Boosting": {
                                        "classifier__learning_rate":[0.15,0.1,0.05,0.01,0.005,0.001],
                                        "classifier__n_estimators": [200],
                                        "classifier__max_depth": [2,3,4,5,6],
                                        "classifier__min_samples_split": [0.005, 0.01, 0.05, 0.10],
                                        "classifier__min_samples_leaf": [0.005, 0.01, 0.05, 0.10],
                                        "classifier__max_features": ["auto", "sqrt", "log2"],
                                        "classifier__subsample": [0.8, 0.9, 1]
                                         }})
 
 
# Update dict with Extra Trees
parameters.update({"Extra Trees Ensemble": {
                                            "classifier__n_estimators": [200],
                                            "classifier__class_weight": [None, "balanced"],
                                            "classifier__max_features": ["auto", "sqrt", "log2"],
                                            "classifier__max_depth" : [3, 4, 5, 6, 7, 8],
                                            "classifier__min_samples_split": [0.005, 0.01, 0.05, 0.10],
                                            "classifier__min_samples_leaf": [0.005, 0.01, 0.05, 0.10],
                                            "classifier__criterion" :["gini", "entropy"]     ,
                                            "classifier__n_jobs": [-1]
                                             }})
 
 
# Update dict with Random Forest Parameters
parameters.update({"Random Forest": {
                                    "classifier__n_estimators": [200],
                                    "classifier__class_weight": [None, "balanced"],
                                    "classifier__max_features": ["auto", "sqrt", "log2"],
                                    "classifier__max_depth" : [3, 4, 5, 6, 7, 8],
                                    "classifier__min_samples_split": [0.005, 0.01, 0.05, 0.10],
                                    "classifier__min_samples_leaf": [0.005, 0.01, 0.05, 0.10],
                                    "classifier__criterion" :["gini", "entropy"]     ,
                                    "classifier__n_jobs": [-1]
                                     }})
 
# Update dict with Ridge
parameters.update({"Ridge": {
                            "classifier__alpha": [1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 0.25, 0.50, 0.75, 1.0]
                             }})
 
# Update dict with SGD Classifier
parameters.update({"SGD": {
                            "classifier__alpha": [1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 0.25, 0.50, 0.75, 1.0],
                            "classifier__penalty": ["l1", "l2"],
                            "classifier__n_jobs": [-1]
                             }})
 
 
# Update dict with BernoulliNB Classifier
parameters.update({"BNB": {
                            "classifier__alpha": [1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 0.25, 0.50, 0.75, 1.0]
                             }})
 
# Update dict with GaussianNB Classifier
parameters.update({"GNB": {
                            "classifier__var_smoothing": [1e-9, 1e-8,1e-7, 1e-6, 1e-5]
                             }})
 
# Update dict with K Nearest Neighbors Classifier
parameters.update({"KNN": {
                            "classifier__n_neighbors": list(range(1,31)),
                            "classifier__p": [1, 2, 3, 4, 5],
                            "classifier__leaf_size": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
                            "classifier__n_jobs": [-1]
                             }})
 
# Update dict with MLPClassifier
parameters.update({"MLP": {
                            "classifier__hidden_layer_sizes": [(5), (10), (5,5), (10,10), (5,5,5), (10,10,10)],
                            "classifier__activation": ["identity", "logistic", "tanh", "relu"],
                            "classifier__learning_rate": ["constant", "invscaling", "adaptive"],
                            "classifier__max_iter": [100, 200, 300, 500, 1000, 2000],
                            "classifier__alpha": list(10.0 ** -np.arange(1, 10)),
                             }})
 
parameters.update({"LSVC": {
                            "classifier__penalty": ["l2"],
                            "classifier__C": [0.0001, 0.001, 0.01, 0.1, 1.0, 10, 100]
                             }})
 
parameters.update({"NuSVC": {
                            "classifier__nu": [0.25, 0.50, 0.75, 0.9],
                            "classifier__kernel": ["linear", "rbf", "poly"],
                            "classifier__degree": [1,2,3,4,5,6],
                             }})
 
parameters.update({"SVC": {
                            "classifier__kernel": ["linear", "rbf", "poly"],
                            "classifier__gamma": ["auto"],
                            "classifier__C": [0.1, 0.5, 1, 5, 10, 50, 100],
                            "classifier__degree": [1, 2, 3, 4, 5, 6]
                             }})
 
 
# Update dict with Decision Tree Classifier
parameters.update({"DTC": {
                            "classifier__criterion" :["gini", "entropy"],
                            "classifier__splitter": ["best", "random"],
                            "classifier__class_weight": [None, "balanced"],
                            "classifier__max_features": ["auto", "sqrt", "log2"],
                            "classifier__max_depth" : [1,2,3, 4, 5, 6, 7, 8],
                            "classifier__min_samples_split": [0.005, 0.01, 0.05, 0.10],
                            "classifier__min_samples_leaf": [0.005, 0.01, 0.05, 0.10],
                             }})
 
# Update dict with Extra Tree Classifier
parameters.update({"ETC": {
                            "classifier__criterion" :["gini", "entropy"],
                            "classifier__splitter": ["best", "random"],
                            "classifier__class_weight": [None, "balanced"],
                            "classifier__max_features": ["auto", "sqrt", "log2"],
                            "classifier__max_depth" : [1,2,3, 4, 5, 6, 7, 8],
                            "classifier__min_samples_split": [0.005, 0.01, 0.05, 0.10],
                            "classifier__min_samples_leaf": [0.005, 0.01, 0.05, 0.10],
                             }})

脚本5 - 为每个分类器定义超参数网格。

特征选择方法

机器学习可能涉及每个训练实例的数千个功能的问题。从大型队列确定特征的最佳子集是机器学习中的常见任务。通过这样做获得的好处很多。例如找到最具描述性的特征会降低模型的复杂性,从而更容易找到最佳解决方案,最重要的是,它可以减少训练模型所需的时间。在某些情况下,可以获得轻微的性能提升。

幸运的是,通常可以使用完善的方法大大减少功能的数量。但是必须注意的是,通过删除功能,系统可能会执行稍差(因为尝试使用较少的信息进行预测)。

选择要素有三种常用方法。即过滤器,包装器和嵌入式方法。完全解释它们超出了本文的范围。

在工作流程中,将首先应用过滤器方法来快速减少要素数量,然后应用包装器方法来确定最大化分类器性能所需的最少要素数量。

1.滤波方法:基于相关的特征选择

假设如果两个特征或更多特征高度相关,可以随机选择其中一个特征并丢弃其余特征而不会丢失任何信息。为了测量特征之间的相关性,将使用Spearman的相关系数。如果两个特征的Spearman相关值为1,则意味着它们完全相关,0不相关,-1高度相关,但方向相反(一个特征增加而另一个特征减少)。

在特征选择算法的这一步骤中,首先使用所有特征计算系数矩阵的绝对值,参见图1。然后,确定一组相关系数大于0.95的特征。从每组相关特征中,将选择其中一个并丢弃其余特征。欢迎随意更改此阈值。

代码语言:javascript复制
###############################################################################
#              6. Feature Selection: Removing highly correlated features      #
###############################################################################
# Filter Method: Spearman's Cross Correlation > 0.95
# Make correlation matrix
corr_matrix = X_train.corr(method = "spearman").abs()
 
# Draw the heatmap
sns.set(font_scale = 1.0)
f, ax = plt.subplots(figsize=(11, 9))
sns.heatmap(corr_matrix, cmap= "YlGnBu", square=True, ax = ax)
f.tight_layout()
plt.savefig("correlation_matrix.png", dpi = 1080)
 
# Select upper triangle of matrix
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k = 1).astype(np.bool))
 
# Find index of feature columns with correlation greater than 0.95
to_drop = [column for column in upper.columns if any(upper[column] > 0.95)]
 
# Drop features
X_train = X_train.drop(to_drop, axis = 1)
X_test = X_test.drop(to_drop, axis = 1)

脚本6 - 删除高度相关的功能。要使用0.90的阈值,请在第19行中将0.95更改为0.90。

图1 - Spearman的相关矩阵。请注意,最后5个功能与任何其他功能都不相关,因为它们充满了随机噪声。

这应该从数据集中删除6个特征,这不是很多 - 即特征13和特征21-25。但是在使用的实际数据集中,此步骤将功能数量减少了多达50%。请注意如果有数千个功能,这可能在计算上很昂贵。

2.包装器方法:通过交叉验证的递归特征消除

在删除高度相关的特征之后,将通过应用递归特征消除算法来进一步减少特征的数量。使用交叉验证(RFECV)对象的Scikit-learn递归特征消除仅允许使用具有feature_importances_或coef_属性的估计器/分类器。根据经验,注意到RFECV经常高估你真正需要的功能数量。

2.A. 调整基本估算器

首先,需要选择要与RFECV一起使用的基本估算器。为了便于说明,将选择一个随机森林分类器作为基础。可以选择以下任何一种作为基本估算器。

代码语言:javascript复制
###############################################################################
#                                Base Estimators                              #
###############################################################################
# Create dict for classifiers with feature_importances_ attribute
FEATURE_IMPORTANCE = {"Gradient Boosting", "Extra Trees Ensemble", "Random Forest"}

这在“ 分类器”部分中执行。

一旦确定了基本估算器,将调整其超参数。这样做的原因是为了降低过度拟合的风险并最大化估算器的性能。为此将创建一个Scikit-learn Pipeline对象,该对象将与Scikit-learn GridSearchCV对象一起使用。

GridSearchCV将对超参数网格执行详尽搜索,并将报告将最大化交叉验证分类器性能的超参数。将折叠数设置为5。

以下是管道中的步骤。

第1步 - 特征缩放:在算法中使用特征之前,扩展特征是一项常见任务。这样做是为了确保数据集中的所有功能具有相同的比例。因此具有较大值的要素不会在具有较小值的要素上占主导地位。将使用训练集中的样本通过 Z分数归一化来扩展数据(训练和测试)。所有要素都以零为中心,标准差为1。

第2步 - 分类器:定义要在管道中使用的分类器对象。

代码语言:javascript复制
###############################################################################
#                     7. Tuning a classifier to use with RFECV                #
###############################################################################
# Define classifier to use as the base of the recursive feature elimination algorithm
selected_classifier = "Random Forest"
classifier = classifiers[selected_classifier]
 
# Tune classifier (Took = 4.8 minutes)
    
# Scale features via Z-score normalization
scaler = StandardScaler()
 
# Define steps in pipeline
steps = [("scaler", scaler), ("classifier", classifier)]
 
# Initialize Pipeline object
pipeline = Pipeline(steps = steps)
  
# Define parameter grid
param_grid = parameters[selected_classifier]
 
# Initialize GridSearch object
gscv = GridSearchCV(pipeline, param_grid, cv = 5,  n_jobs= -1, verbose = 1, scoring = "roc_auc")
                  
# Fit gscv
print(f"Now tuning {selected_classifier}. Go grab a beer or something.")
gscv.fit(X_train, np.ravel(y_train))  
 
# Get best parameters and score
best_params = gscv.best_params_
best_score = gscv.best_score_
        
# Update classifier parameters
tuned_params = {item[12:]: best_params[item] for item in best_params}
classifier.set_params(**tuned_params)

脚本7 - 调整分类器。要更改基本估算器,请更改第5行中的分类器标签。请参阅“ 分类器”部分以查看可用标签列表。要将GridSearchCV使用的折叠数更改为10,请在第23行中设置cv = 10.同样,也可以更改评分。

调整随机森林分类器的处理时间为4.8分钟。

2.B. 使用Tuned Estimator递归选择要素

一旦调整了基本估算器,将创建另一个类似于第一个的管道,但是这个管道将在第二步中具有调整的分类器。现在出现了技术性问题。由于Scikit-learn Pipeline对象没有feature_importances_或coef_属性,如果想将它与RFECV一起使用,将不得不创建自己的管道对象。

代码语言:javascript复制
###############################################################################
#                  8. Custom pipeline object to use with RFECV                #
###############################################################################
# Select Features using RFECV
class PipelineRFE(Pipeline):
    # Source: https://ramhiser.com/post/2018-03-25-feature-selection-with-scikit-learn-pipeline/
    def fit(self, X, y=None, **fit_params):
        super(PipelineRFE, self).fit(X, y, **fit_params)
        self.feature_importances_ = self.steps[-1][-1].feature_importances_
        return self

脚本8 - 定义与RFECV和RFE兼容的自定义管道对象。

最后,可以将RFECV用于新管道。

代码语言:javascript复制
###############################################################################
#   9. Feature Selection: Recursive Feature Selection with Cross Validation   #
###############################################################################
# Define pipeline for RFECV
steps = [("scaler", scaler), ("classifier", classifier)]
pipe = PipelineRFE(steps = steps)
 
# Initialize RFECV object
feature_selector = RFECV(pipe, cv = 5, step = 1, scoring = "roc_auc", verbose = 1)
 
# Fit RFECV
feature_selector.fit(X_train, np.ravel(y_train))
 
# Get selected features
feature_names = X_train.columns
selected_features = feature_names[feature_selector.support_].tolist()

脚本9 - 使用带有交叉验证的递归功能消除(RFECV)选择功能。

现在让看看结果。在python中绘图有点疯狂,但无论如何。

代码语言:javascript复制
###############################################################################
#                             10. Performance Curve                           #
###############################################################################
# Get Performance Data
performance_curve = {"Number of Features": list(range(1, len(feature_names)   1)),
                    "AUC": feature_selector.grid_scores_}
performance_curve = pd.DataFrame(performance_curve)
 
# Performance vs Number of Features
# Set graph style
sns.set(font_scale = 1.75)
sns.set_style({"axes.facecolor": "1.0", "axes.edgecolor": "0.85", "grid.color": "0.85",
               "grid.linestyle": "-", 'axes.labelcolor': '0.4', "xtick.color": "0.4",
               'ytick.color': '0.4'})
colors = sns.color_palette("RdYlGn", 20)
line_color = colors[3]
marker_colors = colors[-1]
 
# Plot
f, ax = plt.subplots(figsize=(13, 6.5))
sns.lineplot(x = "Number of Features", y = "AUC", data = performance_curve,
             color = line_color, lw = 4, ax = ax)
sns.regplot(x = performance_curve["Number of Features"], y = performance_curve["AUC"],
            color = marker_colors, fit_reg = False, scatter_kws = {"s": 200}, ax = ax)
 
# Axes limits
plt.xlim(0.5, len(feature_names) 0.5)
plt.ylim(0.60, 0.925)
 
# Generate a bolded horizontal line at y = 0
ax.axhline(y = 0.625, color = 'black', linewidth = 1.3, alpha = .7)
 
# Turn frame off
ax.set_frame_on(False)
 
# Tight layout
plt.tight_layout()
 
# Save Figure
plt.savefig("performance_curve.png", dpi = 1080)

脚本10 - 可视化RFECV的结果。所有这些代码都做成了图2,如果更改数据集或基本估算器,则可能需要调整xlim(),ylim()和ax.axhline()。

图2 - 接收器操作员曲线下面积(AUC)与特征数量的函数关系。分类器的性能高达10个特征。

在图2中,可以看到分类器的性能是许多功能的函数。正如所看到的,性能峰值约为10个特征,AUC约为0.89; 但是,如果要检查selected_features列表的长度,会注意到RFECV确定需要超过18个功能才能达到峰值性能。

从30个特征开始就知道其中只有5个是真正必要的,并且在特征选择算法之后最终得到了超过18个代表性特征,这是有问题的。为了解决这个问题,看看图2,在视觉上确定要多少功能,使用(10例),并使用Scikit学习RFE对象与n_features_to_select设置为10。请注意参数,经过7采用了性能增益因为添加的功能很少。可以将此作为阈值,但希望包含一些冗余,因为不知道其他17个分类器的最佳功能数量。来自Scikit-learn RFE文档:

给定一个为特征赋予权重的外部估计器(例如,线性模型的系数),递归特征消除(RFE)的目标是通过递归地考虑越来越小的特征集来选择特征......该过程在递归上重复。修剪设置,直到最终达到所需的要素数量。

代码语言:javascript复制
###############################################################################
#                11. Feature Selection: Recursive Feature Selection           #
###############################################################################
# Define pipeline for RFECV
steps = [("scaler", scaler), ("classifier", classifier)]
pipe = PipelineRFE(steps = steps)
 
# Initialize RFE object
feature_selector = RFE(pipe, n_features_to_select = 10, step = 1, verbose = 1)
 
# Fit RFE
feature_selector.fit(X_train, np.ravel(y_train))
 
# Get selected features labels
feature_names = X_train.columns
selected_features = feature_names[feature_selector.support_].tolist()

脚本11 - 使用递归特征消除(RFE)来选择给定数量的特征。要将所选要素的大小更改为12,请在第9行中设置n_features_to_select = 12。

现在可能想知道为什么不使用RFE而不是RFECV。那么在现实生活中,不会事先知道你真正需要多少功能。通过使用RFECV,能够获得最佳的特征子集; 然而它经常被高估。然而从RFECV获得了性能曲线,可以从中了解需要多少功能。使用RFE的缺点是结果没有交叉验证。

功能重要性

一旦确定了所选的特征,就可以根据分类器调查它们的重要性。推测一些冗余功能实际上对分类器的信息比实际功能更多。让看看是否属实。

将首先使用所选特征训练调整的随机森林分类器。然后将使用该feature_importances_属性并使用它创建条形图。请注意,以下代码仅在选择作为基础的分类器包含属性时才有效feature_importances_。

代码语言:javascript复制
###############################################################################
#                  12. Visualizing Selected Features Importance               #
###############################################################################
# Get selected features data set
X_train = X_train[selected_features]
X_test = X_test[selected_features]
 
# Train classifier
classifier.fit(X_train, np.ravel(y_train))
 
# Get feature importance
feature_importance = pd.DataFrame(selected_features, columns = ["Feature Label"])
feature_importance["Feature Importance"] = classifier.feature_importances_
 
# Sort by feature importance
feature_importance = feature_importance.sort_values(by="Feature Importance", ascending=False)
 
# Set graph style
sns.set(font_scale = 1.75)
sns.set_style({"axes.facecolor": "1.0", "axes.edgecolor": "0.85", "grid.color": "0.85",
               "grid.linestyle": "-", 'axes.labelcolor': '0.4', "xtick.color": "0.4",
               'ytick.color': '0.4'})
 
# Set figure size and create barplot
f, ax = plt.subplots(figsize=(12, 9))
sns.barplot(x = "Feature Importance", y = "Feature Label",
            palette = reversed(sns.color_palette('YlOrRd', 15)),  data = feature_importance)
 
# Generate a bolded horizontal line at y = 0
ax.axvline(x = 0, color = 'black', linewidth = 4, alpha = .7)
 
# Turn frame off
ax.set_frame_on(False)
 
# Tight layout
plt.tight_layout()
 
# Save Figure
plt.savefig("feature_importance.png", dpi = 1080)

脚本12 - 可视化功能重要性。

图3 - 随机森林特征重要性。较大的价值意味着更重要的意义。

最重要的特征是特征6和19,它们属于冗余特征类。冗余功能似乎比信息功能(功能1-5)更重要,这似乎是违反直觉的。然后在Kaggle比赛中经常可以看到,特色工程可以给你提升。值得注意的是,机器学习分类器分配的特征重要性本质上是随机的,并不健壮。例如,如果要重新运行RFE,则可能会获得稍微不同的结果,因为没有在随机林中修复种子。如果计划是从功能重要性中得出一些结论,那么这就是需要交叉验证结果的原因。

迭代分类器调整和评估

现在确定了代表性特征的子集,调整和训练18个模型,以研究其中最高性能的模型。为此将迭代脚本4中定义的分类器,并使用脚本7使用脚本5中定义的超参数来调整它们。将对脚本7进行细微更改,并添加一些额外的代码行来评估测试集上的调优分类器性能并保存结果。

代码语言:javascript复制
###############################################################################
#                       13. Classifier Tuning and Evaluation                  #
###############################################################################
# Initialize dictionary to store results
results = {}
 
# Tune and evaluate classifiers
for classifier_label, classifier in classifiers.items():
    # Print message to user
    print(f"Now tuning {classifier_label}.")
    
    # Scale features via Z-score normalization
    scaler = StandardScaler()
    
    # Define steps in pipeline
    steps = [("scaler", scaler), ("classifier", classifier)]
    
    # Initialize Pipeline object
    pipeline = Pipeline(steps = steps)
      
    # Define parameter grid
    param_grid = parameters[classifier_label]
    
    # Initialize GridSearch object
    gscv = GridSearchCV(pipeline, param_grid, cv = 5,  n_jobs= -1, verbose = 1, scoring = "roc_auc")
                      
    # Fit gscv
    gscv.fit(X_train, np.ravel(y_train))  
    
    # Get best parameters and score
    best_params = gscv.best_params_
    best_score = gscv.best_score_
    
    # Update classifier parameters and define new pipeline with tuned classifier
    tuned_params = {item[12:]: best_params[item] for item in best_params}
    classifier.set_params(**tuned_params)
            
    # Make predictions
    if classifier_label in DECISION_FUNCTIONS:
        y_pred = gscv.decision_function(X_test)
    else:
        y_pred = gscv.predict_proba(X_test)[:,1]
    
    # Evaluate model
    auc = metrics.roc_auc_score(y_test, y_pred)
    
    # Save results
    result = {"Classifier": gscv,
              "Best Parameters": best_params,
              "Training AUC": best_score,
              "Test AUC": auc}
    
    results.update({classifier_label: result})

脚本13 - 在训练集上调整分类器并在测试集上进行评估。

脚本13运行大约需要30分钟。所有结果都将存储在名为的字典对象中results。results可以通过classifier_label访问字典的内容(请参阅“ 分类器”部分)。对于每个分类器,存储以下对象:

  • 分类器:具有训练分类器的管道对象。可以使用它来预测新样本。
  • 最佳参数:包含在训练集中获得最佳性能的参数的字典。
  • 训练AUC:在训练集中获得的交叉验证的AUC。
  • 测试AUC:在测试集中获得的AUC。

看看结果:

代码语言:javascript复制
###############################################################################
#                              14. Visualing Results                          #
###############################################################################
# Initialize auc_score dictionary
auc_scores = {
              "Classifier": [],
              "AUC": [],
              "AUC Type": []
              }
 
# Get AUC scores into dictionary
for classifier_label in results:
    auc_scores.update({"Classifier": [classifier_label]   auc_scores["Classifier"],
                       "AUC": [results[classifier_label]["Training AUC"]]   auc_scores["AUC"],
                       "AUC Type": ["Training"]   auc_scores["AUC Type"]})
    
    auc_scores.update({"Classifier": [classifier_label]   auc_scores["Classifier"],
                       "AUC": [results[classifier_label]["Test AUC"]]   auc_scores["AUC"],
                       "AUC Type": ["Test"]   auc_scores["AUC Type"]})
 
# Dictionary to PandasDataFrame
auc_scores = pd.DataFrame(auc_scores)
 
# Set graph style
sns.set(font_scale = 1.75)
sns.set_style({"axes.facecolor": "1.0", "axes.edgecolor": "0.85", "grid.color": "0.85",
               "grid.linestyle": "-", 'axes.labelcolor': '0.4', "xtick.color": "0.4",
               'ytick.color': '0.4'})
 
    
# Colors
training_color = sns.color_palette("RdYlBu", 10)[1]
test_color = sns.color_palette("RdYlBu", 10)[-2]
colors = [training_color, test_color]
 
# Set figure size and create barplot
f, ax = plt.subplots(figsize=(12, 9))
 
sns.barplot(x="AUC", y="Classifier", hue="AUC Type", palette = colors,
            data=auc_scores)
 
# Generate a bolded horizontal line at y = 0
ax.axvline(x = 0, color = 'black', linewidth = 4, alpha = .7)
 
# Turn frame off
ax.set_frame_on(False)
 
# Tight layout
plt.tight_layout()
 
# Save Figure
plt.savefig("AUC Scores.png", dpi = 1080)

脚本14 - 可视化脚本13的结果。

图4 - 训练和测试集上分类器性能的条形图。

从图4中,可以直观地确定SVC,NuSVC,Gradient Boosting和AdaBoost分类器在测试集中获得了最高性能。查看pandas dataframe对象的内容auc_scores以查看数值结果。

写在最后

可以在GitHub存储库中找到本文的所有代码。如果想使用它,只需修改脚本2并确保以下内容:

https://github.com/frank-ceballos/Model_Design-Selection

  • 加载数据并编码所有分类变量。
  • 处理任何缺失值或异常值。
  • 将特征矩阵X存储到pandas DataFrame对象中。对y中的目标执行相同的操作。

如果数据集包含大约1000个样本和30个特征,则整个过程执行大约需要30-45分钟。

现在有一些建议来确定下一步该做什么,以进一步提高这些分类器的性能。

最简单的方法是选择前五个执行分类器并运行具有不同参数的网格搜索。一旦对最佳参数的位置有所了解,就可以在参数空间中对该点进行更精细的网格搜索。在进一步调整这些分类器之后,选择最好的三分之三并在Scikit-learn 中的VotingClassifier中使用它们。这很可能会带来更高的性能,但会增加建模的复杂性。

0 人点赞