AAAMLP-Chapter-7: Feature Selection

295 阅读5分钟

当你完成了成百上千个特征创建之后,是时候挑选其中最重要的特征了。

你肯定不想创建成百上千个无用特征,同时,过多的特征也被称为维度灾难。

如果你有许多特征,你必须有大量的数据来捕获这些特征。具体的数量对应关系,需要通过恰当的模型验证和模型训练时间来检查。

最简单的特征选择方式是删除方差值很低的特征,即该特征所有数据的方差值非常低,接近 0 ,这意味着该特征对于所有数据都差不多,因此对模型的训练不起作用。删除此类特征可以避免无意义的计算,还可以减少模型复杂度。

可通过 VarianceThreshold 方法来实现。

import numpy as np
from sklearn.feature_selection import VarianceThreshold

x = np.random.randint(1, 15, size=(10, 6)).astype(float)
x[:, 2] = 1
print(x)
var_thres = VarianceThreshold(threshold=0.1)
print(var_thres.fit_transform(x))

我们也可以删除具有高相关性的特征,对于数值特征之间的相关性计算,可以使用 Pearson Correlation 完成。

import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing

data = fetch_california_housing()
x = data['data']
columns = data['feature_names']
y = data['target']

df = pd.DataFrame(x, columns=columns)
df.loc[:, 'MedInc_Sqrt'] = df.MedInc.apply(np.sqrt)
df.corr()

我们从 MedInc 特征中派生出 MedInc_Sqrt ,然后计算各特征的相关系数,从中可以看出,这两列相关系数非常高,因此可以从中删除一列。


现在,我们可以进一步介绍特征选择的单变量方法,Univariate Feature Selection。该方法对每列特征运行给定的得分函数,根据得分对特征排序。

Mutual Information、ANOVA F-test 和 chi2chi^2 是最著名的单变量特征选择方法的得分函数。

必须注意到,只有数据中全部都是非负值时,才可以使用 chi2chi^2 方法。在自然语言处理中很有用。

下边用代码创建一个单变量特征选择的包装方法,该方法可以用在任何新问题中。

from sklearn.feature_selection import chi2, f_classif, mutual_info_classif
from sklearn.feature_selection import f_regression, mutual_info_regression
from sklearn.feature_selection import SelectKBest, SelectPercentile

class UnivariateFeatureSelection:
    def __init__(
        self,
        n_features,
        problem_type,
        scoring
    ):
        if problem_type == 'classification':
            valid_scoring = {
                'chi2': chi2,
                'f_classif': f_classif,
                'mutual_info_classif': mutual_info_classif
            }
        else:
            valid_scoring = {
                'f_regression': f_regression,
                'mutual_info_regression': mutual_info_regression
            }
        if scoring not in valid_scoring:
            raise Exception('Invalid scoring function.')
        if isinstance(n_features, int):
            self.selection = SelectKBest(
                valid_scoring[scoring],
                k=n_features
            )
        elif isinstance(n_features, float):
            self.selection = SelectPercentile(
                valid_scoring[scoring],
                percentile=int(n_features * 100)
            )
        else:
            raise Exception('Invalid type of feature')

    def fit(self, x, y):
        self.selection.fit(x, y)

    def transform(self, x):
        return self.selection.transform(x)

    def fit_transform(self, x, y):
        return self.selection.fit_transform(x, y)

时刻牢记,创建较少的重要特征比创建上百个特征要好得多。

单变量特征选择的效果有时候不会很好,人们通常在训练机器学习模型的时候使用该方法。


最简单的特征选择形式是贪心特征选择 Greedy Feature Selection。在该方法中,首先要确定使用的模型,然后选择 Loss/Score 函数,最后迭代计算每一列特征,如果该列特征可以提升模型的 Loss/Score,就将该列特征添加到保留特征组里。

贪心特征选择非常简单,但是你得明白贪心二字的含义,该方法的计算耗时非常高,如果使用不当,还会导致模型过拟合。

下边用代码实现。

import pandas as pd
from sklearn import linear_model, metrics
from sklearn.datasets import make_classification

class GreedyFeatureSelection:
    def evaluate_score(self, x, y):
        model = linear_model.LogisticRegression()
        model.fit(x, y)
        return metrics.roc_auc_score(
            y, 
            model.predict_proba(x)[:, 1]
        )

    def _feature_selection(self, x, y):
        good_features = []
        best_scores = []
        num_features = x.shape[1]
        while True:
            this_feature = None
            best_score = 0
            for feature in range(num_features):
                if feature in good_features:
                    continue
                selected_features = good_features + [feature]
                x_train = x[:, selected_features]
                score = self.evaluate_score(x_train, y)
                if score > best_score:
                    this_feature = feature
                    best_score = score
            if this_feature != None:
                good_features.append(this_feature)
                best_scores.append(best_score)
            if len(best_scores) > 2:
                if best_scores[-1] < best_scores[-2]:
                    break
        return best_scores[:-1], good_features[:-1]

    def __call__(self, x, y):
        scores, features = self._feature_selection(x, y)
        return x[:, features], scores

X, y = make_classification(n_samples=1000, n_features=100)
X_transformed, scores = GreedyFeatureSelection()(X, y)
print(X_transformed.shape[1])

import seaborn as sns
sns.lineplot(scores)

GreedyFeatureSelection 最后返回了特征选择带来的 AUC 优化值 scores,可以将其绘制成图,直观的检查模型选择过程中,每一轮迭代增加一个特征的效果。可以看到在某个时刻得分无法提升,因此选择停止。

另一个著名的贪心方法是递归特征消除 Recursive Feature Elimination (RFE)。

在上一个方法中,我们从一个特征开始计算,然后每轮迭代添加一个新特征,最终得到的一组特征,都是可以提升模型得分的特征。

而通过 RFE,我们从所有特征开始,每轮迭代减少一个提升模型得分最少的特征。

但是,我们如何知道哪个特征提升的模型得分最少?

如果使用 SVM 或者逻辑回归,我们需要每个特征的系数,该系数决定特征的重要度。在使用基于树的模型时,我们也需要特征重要度。

度量每轮迭代中的特征重要度,然后消除最不重要的特征,持续迭代,直到特征数量到达指定值。

现在,我们可以决定保留多少个特征了。

创建 RFE 操作非常方便,scikit-learn 提供了开箱即用的 RFE。

import pandas as pd
from sklearn.feature_selection import RFE
from sklearn.linear_model import LinearRegression
from sklearn.datasets import fetch_california_housing

data = fetch_california_housing()
x = data['data']
columns = data['feature_names']
y = data['target']

model = LinearRegression()
rfe = RFE(
    estimator=model,
    n_features_to_select=3
)
rfe.fit(x, y)
x_transformed = rfe.transform(x)

至此,我们见识了两种特征选择贪心方式。

你也可以训练模型,然后根据模型计算特征系数或特征重要度。

如果你需要计算特征系数,你可以选择一个阈值,系数高于该阈值的特征被保留,否则被消除。  

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_diabetes

data = load_diabetes()
x = data['data']
columns = data['feature_names']
y = data['target']

model = RandomForestRegressor()
model.fit(x, y)

importances = model.feature_importances_
idx = np.argsort(importances)
plt.title('Feature Importances')
plt.barh(range(len(idx)), importances[idx], align='center')
plt.yticks(range(len(idx)), [columns[i] for i in idx])
plt.xlabel('Random Forest Feature Importance')
plt.show()

结果绘制如下:

拿到了各个特征重要性排序之后,选择最优的特征很容易。

可以基于某个模型选择出特征,然后在另一个模型上训练与测试。例如,你可以使用 Logistic Regression 模型的重要系数选择特征,然后使用这些特征去训练随机森林。

import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_diabetes
from sklearn.feature_selection import SelectFromModel

data = load_diabetes()
x = data['data']
columns = data['feature_names']
y = data['target']

model = RandomForestRegressor()
sfm = SelectFromModel(
    estimator=model
)
sfm.fit(x, y)
x_transformed = sfm.transform(x)
support = sfm.get_support()
print([
    x for x, y in zip(columns, support) if y == True
])

代码输出 ['bmi', 's5'],结合重要性排序图,可以发现这些特征是重要度最高的特征。

最后我们没提到的特征选择方法是 L1 (Lasso) penalization。当我们做 L1 正则化时,大多数系数会成为 0 或接近 0,然后我们选择系数非 0 的特征。

你可以在上述代码中使用支持 L1 惩罚的模型替换随机森林模型,如 lasso regression。

所有基于树的模型都提供特征重要性度量,因此上述代码框架可以直接用在 XGBoost、LightGBM、CatBoost 模型上。特征重要度函数名称可能不一样,导致结果不同,但是使用思路是一样的。

最后提醒一下,做特征选择时必须非常小心,只在训练集上做特征选择,然后在验证集上根据选择的特征验证模型,不会导致模型过拟合。