sklearn RandomForestClassifier根节点为何与最重要特征不一致
scikit-learn中RandomForestClassifier特征重要性计算规则
scikit-learn中随机森林的feature_importances_默认采用平均不纯度减少(MDI) 方法计算,规则如下:
- 对单棵决策树:遍历树的所有分裂节点,计算每个节点用对应特征分裂带来的不纯度(由
criterion参数指定为gini或熵)下降值,乘以该节点覆盖的加权样本数(无自定义样本权重时就是节点包含的样本数),得到该特征在这个节点的贡献值;把同一特征在所有节点的贡献值求和,再除以所有特征的总贡献值做归一化,保证所有特征重要性之和为1,就是单棵树的特征重要性。 - 对随机森林集成:先对每一棵基决策树按上述规则计算单树特征重要性,再对所有树的特征重要性取平均值,最后再做一次归一化,得到全局特征重要性。
单棵基决策树根节点与全局最重要特征不匹配的原因
这个现象是随机森林的训练机制导致的,属于正常情况,核心原因有三点:
- 基决策树训练存在双重随机,单棵树的分裂选择不具备全局代表性:
- 样本随机:每棵基树的训练数据是从原数据集有放回bootstrap抽样得到,平均仅包含63%左右的原样本,不同树的训练数据分布存在天然差异。
- 特征随机:随机森林训练时,每个节点分裂不会从全部特征里选最优,只会从随机抽取的
max_features个特征中选择最优分裂特征。分类任务默认max_features="sqrt",对应测试代码里10个特征的场景,每次分裂仅随机抽3个特征候选,根节点分裂时没抽到全局最重要特征的概率高达70%,自然不可能选该特征作为根节点。
- 仅可视化了森林中的第0棵基树,单棵树的结果方差极高。随机森林默认包含100棵基树(
n_estimators=100),单棵树的分裂结果受随机采样影响波动很大,不能代表上百棵树的平均统计结果。如果统计所有基树的根节点特征,会发现全局重要性最高的特征出现的占比最高,但不会达到100%。 - 特征重要性统计的是特征在全树所有分裂节点的累计贡献,不是仅统计根节点的贡献。某个特征即使很少出现在根节点位置,但如果它在大量树的中、深层节点分裂时带来很高的不纯度下降,累计贡献值也可能成为全局最高;反过来某个特征在少数树里当根节点,但其他节点几乎不用它,累计重要性也排不到第一。
单棵决策树不存在该问题的原因:单棵决策树训练时没有bootstrap采样,用全量数据训练,且所有节点分裂都从全部特征中选最优,根节点本身就是全量数据下带来单次不纯度下降最大的特征,特征重要性也是基于自身所有分裂节点计算的,自然根节点特征和自身输出的最重要特征完全匹配。
复现代码
核心训练逻辑
from sklearn.datasets import make_classification from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.tree import export_graphviz from IPython.display import Image, display from subprocess import call import matplotlib.pyplot as plt import numpy as np from sklearn.datasets import load_wine from sklearn.datasets import load_iris wines = load_wine() iris = load_iris() def create_and_fit(clf,model_name): print(clf) # 定义数据集 X, y = make_classification(n_samples=1000, n_features=10, n_informative=3, n_redundant=5, random_state=seed) # 可切换iris、wine数据集测试 # X,y = iris.data, iris.target # X,y = wines.data, wines.target # 模型训练 clf.fit(X, y) # 获取并打印特征重要性 importance = clf.feature_importances_ indices = np.argsort(importance)[::-1] for f in range(X.shape[1]): print("feature {}: ({})".format(indices[f], importance[indices[f]])) filename = model_name+model.criterion if model_name == 'forest_': print('forest') # 导出随机森林第0棵基树的结构 export_graphviz(clf.estimators_[0], out_file=filename+'.dot') else: # 导出单棵决策树的结构 export_graphviz(clf, out_file=filename+'.dot') call(['dot', '-Tpng', filename+'.dot', '-o', filename+'.png', '-Gdpi=600']) seed=0 # 随机森林训练 models = [ RandomForestClassifier(criterion='gini',max_depth=5, random_state=seed), RandomForestClassifier(criterion='entropy',max_depth=5, random_state=seed), ] names =['forest_', 'forest_'] for name, model in zip(names, models): create_and_fit(model,name)
随机森林结构可视化
# 加载gini准则随机森林第0棵基树可视化结果 Image(filename = 'forest_gini'+'.png') # 加载entropy准则随机森林第0棵基树可视化结果 Image(filename = 'forest_entropy'+'.png')
单棵决策树对照实验
# 单决策树对照训练 models = [ DecisionTreeClassifier(criterion='gini',max_depth=5, random_state=seed), DecisionTreeClassifier(criterion='entropy',max_depth=5, random_state=seed) ] names =['tree_', 'tree_'] for name, model in zip(names, models): create_and_fit(model,name)
决策树结构可视化
# 加载gini准则决策树可视化结果 Image(filename = 'tree_gini'+'.png') # 加载entropy准则决策树可视化结果 Image(filename = 'tree_entropy'+'.png')
内容的提问来源于stack exchange,提问作者asiehh
相关产品推荐
相关产品推荐

