4、特征选择与降维:过滤式、包裹式与嵌入式方法
做ADMET预测的时候,你有没有遇到过这种情况?特征搞了上百个,模型跑得贼慢,精度还上不去。我刚开始做这个方向时也踩过这个坑——一股脑把所有分子描述符全扔进XGBoost,结果调参调得头皮发麻。后来才明白,特征选择不只是“减维度”,更是帮模型聚焦真正重要的信号。
这一章咱们就聊透三种主流的特征选择套路。说白了,过滤式、包裹式、嵌入式,它们各有脾气,用对了场景事半功倍。
4.1 为什么特征选择对ADMET预测如此重要?
ADMET数据本身就很“脏”。分子描述符之间经常高度相关,比如logP和某些疏水指数,相关性能到0.9以上。冗余特征会引入噪声,让XGBoost的分裂点被带偏。我早年一个肝毒性预测项目,原始特征300多个,做完过滤还剩80个,模型AUC直接从0.82跳到了0.87。你看,不是特征越多越好。
降维则解决另一个问题:特征空间太大,样本量又少(ADMET数据常见几百到几千个分子),容易过拟合。PCA、t-SNE这些线性/非线性方法也能帮忙,但咱们重点还是聊和监督任务结合更紧的特征选择。
4.2 过滤式方法 (Filter Methods)
过滤式方法,说白了就是“先筛再练”。不依赖于任何模型,直接用统计指标给每个特征打分,然后挑得分高的。速度快,适合海量特征的初筛。
常用指标我列个表,项目里经常用到:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 方差过滤 (VarianceThreshold) | 特征值太“平”的剔除 | 极快,无参数依赖 | 没考虑与目标变量的关系 |
| 互信息 (Mutual Information) | 连续/离散特征与分类目标 | 能捕捉非线性关系 | 对离散化敏感 |
| 卡方检验 (Chi²) | 分类特征与分类目标 | 经典,解释性强 | 只适用于非负计数特征 |
| 皮尔森相关系数 | 连续特征与连续目标 | 计算简单 | 只捕捉线性关系 |
4.3 包裹式方法 (Wrapper Methods)
包裹式方法,是把特征子集当“搜索任务”来跑。用一个模型(比如XGBoost本身)评估不同的特征组合,选表现最好的。代表方法就是递归特征消除(RFE)。
它的好处:直接优化最终性能。坏处:慢。特征多的时候,你可能要跑几十轮交叉验证,时间成本吃不消。
我通常只在特征数已经降到了50以下时,才用包裹式做最后的精细筛选。下面是个RFE配合XGBoost的代码片段:
from sklearn.feature_selection import RFECV
import xgboost as xgb
model = xgb.XGBClassifier(
n_estimators=100,
learning_rate=0.1,
max_depth=4,
subsample=0.8,
colsample_bytree=0.8,
use_label_encoder=False,
eval_metric='logloss'
)
selector = RFECV(
estimator=model,
step=1,
cv=5,
scoring='roc_auc',
min_features_to_select=10,
n_jobs=-1
)
selector.fit(X_train, y_train)
print("最优特征数:", selector.n_features_)
print("选中的特征索引:", selector.support_)
注意step别设太大,不然容易跳过好的组合。我当年设step=5做过一次,结果漏掉了4个关键特征,后来发现了恨不得抽自己。
4.4 嵌入式方法 (Embedded Methods)
嵌入式方法,直接在模型训练过程中内嵌特征选择。XGBoost自带的特征重要性就属于这一类。你训练完之后,调个feature_importances_属性,就能看到每个特征的重要性分数。
XGBoost提供了三种重要性指标:
- gain:该特征在被使用时带来的平均信息增益。最常用,也最稳定。
- cover:该特征覆盖的样本数量。偏向于选择出现频率高的特征,但可能忽略小众但关键的特征。
- frequency:简单计数该特征被用作分裂的次数。容易受树结构影响,我一般不用这个做最终筛选。
我个人的习惯:先用gain排序,取top N个特征,再和过滤式的候选集取交集,这样出来的特征既稳定又有区分度。
import xgboost as xgb
model = xgb.XGBClassifier(
n_estimators=200,
max_depth=5,
learning_rate=0.1
)
model.fit(X_train, y_train)
importance = model.feature_importances_
feature_names = list(X_train.columns)
# 按gain排序
sorted_idx = np.argsort(importance)[::-1]
print("Top 10 features (by gain):")
for i in range(10):
print(f"{feature_names[sorted_idx[i]]}: {importance[sorted_idx[i]]:.4f}")
4.5 三种方法对比
| 维度 | 过滤式 | 包裹式 | 嵌入式 |
|---|---|---|---|
| 计算成本 | 低 | 高 | 中等 |
| 与模型关系 | 无依赖 | 依赖模型 | 内嵌在模型里 |
| 特征间关系 | 独立评估 | 考虑组合 | 考虑组合 |
| 过拟合风险 | 低 | 高(需交叉验证) | 中等(受正则化影响) |
| 典型应用阶段 | 初步粗筛 | 精细调优 | 直接产出最终特征集 |
4.6 实战建议:如何在ADMET项目中组合使用
一般我走三步:
- 用过滤式做减法:先用互信息或方差过滤,把特征数从几百砍到50以内,耗时几分钟。
- 用嵌入式做排序:跑一版XGBoost看gain,选前20-30个关键特征,同时检查有没有明显的共线性。
- 用包裹式做微调:对于特别重要的任务(比如专利申请里的毒性预测),再用RFE在选出的特征上跑一遍,确保没有漏掉好组合。
嗯,到这里三种方法基本讲透了。下次你做ADMET项目,拿到原始描述符别急着开练——先想想今天聊的这几步。把特征选好,调参才能调出真功夫。