permutation的问题在于计算量随着特征的增加而线性增加,对于维度很高的数据基本上难以使用。下面介绍一下kaggle 大佬 oliver 发明的 null importance 特征重要性计算方法。
xgboost特征重要性计算算法的问题
-
树模型自带的特征重要性评估可能偏向于具有高基数的类别特征或连续特征。这是因为,这些特征在决策树分裂时,因为其多样的分割点,往往能带来更显著的不纯度降低(如基尼不纯度或MSE减少量)。特别是在使用LightGBM和XGBoost这类模型时,哪怕有非常重要的离散特征,模型仍然会倾向于增加连续型特征/高基数特征的分裂次数,导致重要特征无法体现。
-
对于xgb和lgb等这类拟合能力超强的模型来说,很多和标签完全无关的特征甚至是随机加入的噪声,都能通过海量的子树与标签建立密切的联系,即只要子树的量够多,哪怕是随机噪声和标签之间都能建立映射。而且在数据集中加入噪声作为特征后,直接输出xgboost给出的特征重要性排序,噪声很有可能是优于正常的特征的,如果我们根据xgboost自身的特征重要性排序来删除特征,则会删除掉很多潜在的有用的信息。
为什么不能用permutation?
之前的文章介绍过permutation,其是处理噪声问题的一种方法,但是该方法的最大问题在于:其计算量与特征维度正成比,面对高维数据计算过慢,而在进行kaggle比赛时,所使用的特征通常定然在千维以上。而null importance则可以较好的解决这样的问题。
null importance的核心思想
Null Importance的核心思想是:一个较好的特征与标签必然具有较高的关联程度,因此,如果将标签随机打乱,好的特征的特征重要性必然出现大幅度的下滑,而差的特征,或者说与标签无关的特征的特征重要性则不会有太大的改变。因此计算两种特征重要性,一种是实际的特征重要性,另一种是打乱标签训练得到的null importance,然后使用后者对前者做一个修正,以得到真实的特征重要性。
如果感觉这段有点难于理解,可以先看具体算法步骤。
具体算法步骤
-
计算基准特征重要性:
- 首先,训练一个标准的树模型,以此模型输出的特征重要度作为基准(实际重要性)。
-
生成Null Importance分布:
- 打乱数据的标签,破坏特征与标签之间的实际关联。
- 使用与步骤一同样相同的方法重新训练模型,输出特征重要度,称为Null Importance。这代表了模型在不考虑真实标签关联时对特征的随机评价。
- 由于标签被打乱,导致每次计算的Null Importance可能会有很大的波动,因此需要多次重复上述过程,建立Null Importance的分布,以更准确的评估特征重要性。
-
评估特征分数:
- 对于有用的特征,其实际重要性远高于Null Importance。
- 对于无用或者不那么重要的特征,其Null Importance很可能接近甚至超过实际重要性。
- 结合步骤1和步骤2的结果,我们可以通过比较实际重要性和Null Importance来评分每个特征。这种比较可以用多种方法实现,例如差值、比值等。 oliver 使用的评分公式是实际重要性与Null Importance分布的75%分位数的对数比值。
-
特征选择与决策:
- 根据计算出的特征分数,我们可以设定一个阈值,以决定保留哪些特征。
- 还可以通过观察实际重要性与Null Importance之间的差异,来做出关于特征选择的更直观的决定。
对于第三步:特征分数的计算,oliver给出的衡量方案核心逻辑对应的代码如下:
python
feature_scores = []
for _f in actual_imp_df['feature'].unique():
f_null_imps_gain = null_imp_df.loc[null_imp_df['feature'] == _f, 'importance_gain'].values
f_act_imps_gain = actual_imp_df.loc[actual_imp_df['feature'] == _f, 'importance_gain'].mean()
gain_score = np.log(1e-10 + f_act_imps_gain / (1 + np.percentile(f_null_imps_gain, 75))) # Avoid didvide by zero
f_null_imps_split = null_imp_df.loc[null_imp_df['feature'] == _f, 'importance_split'].values
f_act_imps_split = actual_imp_df.loc[actual_imp_df['feature'] == _f, 'importance_split'].mean()
split_score = np.log(1e-10 + f_act_imps_split / (1 + np.percentile(f_null_imps_split, 75))) # Avoid didvide by zero
feature_scores.append((_f, split_score, gain_score))
scores_df = pd.DataFrame(feature_scores, columns=['feature', 'split_score', 'gain_score'])
scores_df = scores_df.sort_values('split_score', ascending=False)
其使用了计算出的null importance的75%分位数与真实重要性的比值,而后取对数,即:
gain_score = np.log(1e-10 + f_act_imps_gain / (1 + np.percentile(f_null_imps_gain, 75)))
笔者在公司的算法赛中使用了null importance 进行了特征重要性筛选,将1600维度压到了500多维,发现线上F1是有少量提升的。