特征筛选与降维基本概念
做ADMET肝毒性预测,说白了就是拿一堆分子特征去训练模型,然后判断这化合物会不会伤肝。但你想想看,一个分子能算出来的描述符——分子量、LogP、氢键供体/受体数、可旋转键数、拓扑极性表面积……再加上ECFP、MACCS这类指纹特征,轻轻松松几百上千维。特征一多,麻烦就来了。
我个人习惯是,在开始建模之前,先花至少三分之一的时间做特征工程。为什么?因为原始特征里藏着太多陷阱。这一章我就跟你聊聊,为什么要做特征筛选和降维,以及实际项目中会遇到哪些坑。
为什么非要折腾特征筛选和降维?
给你三个核心理由,都是我在真实项目里摔过跟头才深刻理解的。
- 维度灾难:特征维度越高,数据在空间里就越稀疏。ADMET数据集通常也就几百到几千个化合物,特征维度如果上千,每个样本周围都是"真空区",模型根本学不到靠谱的决策边界。我做过一个肝毒性分类任务,原始特征2800多维,不加筛选直接跑随机森林,训练集准确率99%,验证集直接掉到62%——这差距你感受一下。
- 过拟合风险:特征越多,模型越容易记住噪声。肝毒性数据本身就有很大的生物变异性,同一个化合物在不同实验条件下结果都可能不一样。你再喂进去几百个冗余特征,模型很快就"学歪了"。我记得有次用Lasso做筛选,正则化一开,2800个特征直接砍到47个,验证集AUC反而涨了0.15。
- 计算效率:这个最实在。特征多,训练慢,迭代也慢。你做超参搜索、交叉验证,一次跑十几个小时,谁能受得了?把特征从2000降到100,训练时间能从2小时缩到5分钟。
一句话总结:特征筛选和降维不是为了"炫技",而是为了让模型更稳、更快、更可解释。在ADMET肝毒性这种高风险场景下,少而精的特征远比多而杂的强。
实际挑战——我在项目中遇到的坎
道理都懂,真做起来就没那么简单了。我简单列几个常见的坑,都是我亲自踩过的。
多重共线性
分子描述符之间天然就存在相关性。比如分子量和拓扑极性表面积经常高度相关,LogP和芳香环数量也有耦合。两个高度相关的特征丢进模型,权重分配就会变得极其不稳定。你今天跑出来LogP最重要,明天跑出来可能就变成芳香环最重要——模型自己都"精神分裂"了。
信息损失
降维不是万能的。PCA这种线性方法,遇到非线性关系就容易丢信息。我在一个肝毒性项目里试过把2000维指纹降到50维PCA,结果模型AUC从0.83跌到0.71。后来换成基于树模型的特征重要性筛选,保留了127个特征,AUC反而升到了0.88。所以选方法一定要看数据特点。
生物数据的特殊性
ADMET数据噪声大、批次效应明显。不同实验室测的同一化合物的肝毒性结果可能都不一样。特征筛选时如果不注意,很容易选出那些"恰好与批次相关"的假阳性特征。我吃过这个亏:筛选出的top10特征里,有5个是批次标签——你说冤不冤?
注意:特征筛选必须在训练集内完成,绝对不能让验证集或测试集参与任何筛选过程。我曾经见过有人先在全量数据上做方差过滤,再做训练测试拆分——这相当于"数据泄露",会导致对模型性能的严重高估。
解决思路与方法分类
特征筛选和降维的方法有很多,但归纳起来就三大类。我按自己的使用经验给你拆开聊聊。
| 类别 | 典型方法 | 核心思路 | 我的使用建议 |
|---|---|---|---|
| 过滤法 | 方差阈值、卡方检验、互信息、皮尔逊相关系数 | 不依赖模型,直接算特征与标签之间的统计量 | 适合做第一道粗筛,快速砍掉明显无用的特征 |
| 包裹法 | 递归特征消除RFE、前向/后向选择 | 用模型性能作为特征子集的评价标准 | 效果通常最好,但计算开销大,适合特征量较少时使用 |
| 嵌入法 | Lasso回归、树模型特征重要性、LightGBM内置筛选 | 在模型训练过程中自动完成特征选择 | 性价比最高,我80%的项目都用这类方法 |
| 降维法 | PCA、t-SNE、UMAP、Autoencoder | 将高维特征映射到低维空间,保留主要信息 | 适合可视化、去噪,但可解释性会下降 |
过滤法——快但粗糙
先算每个特征与标签之间的相关性或信息量,设个阈值筛一遍。比如方差过滤:如果一个特征在所有样本上几乎都是同一个值,那它对分类基本没贡献。我常用互信息做初筛,能捕捉非线性关系,比皮尔逊相关系数更靠谱。
from sklearn.feature_selection import VarianceThreshold, SelectKBest, mutual_info_classif
# 第一步:方差过滤,去掉方差小于0.01的特征
var_sel = VarianceThreshold(threshold=0.01)
X_var = var_sel.fit_transform(X)
# 第二步:互信息筛选,保留top 100
mi_sel = SelectKBest(mutual_info_classif, k=100)
X_mi = mi_sel.fit_transform(X_var, y)
包裹法——稳但慢
把特征子集的选择看作一个搜索问题,用模型的表现来评估每个子集的好坏。RFE是典型代表:每次去掉一个最不重要的特征,迭代直到达到目标特征数。效果好,但计算量也大——你想想,如果1000个特征,每次都要重新训练模型。
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
# 用随机森林做包裹法,保留50个特征
estimator = RandomForestClassifier(n_estimators=100)
rfe = RFE(estimator, n_features_to_select=50)
X_rfe = rfe.fit_transform(X, y)
嵌入法——我的首选
Lasso回归自带L1正则化,训练完系数为0的特征就是被筛掉的。树模型则可以直接输出特征重要性。这种方法既高效又稳定,我大部分项目都用它做主力。尤其是LightGBM,训练快、自带特征重要性,还能处理缺失值,简直是ADMET数据的利器。
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# Lasso做特征筛选,C越小正则化越强
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
lasso = LogisticRegression(penalty='l1', C=0.1, solver='saga')
lasso.fit(X_scaled, y)
selected = X.columns[lasso.coef_.flatten() != 0]
降维法——当特征实在太乱时
PCA是最经典的无监督降维方法。它不关心标签,只找方差最大的方向。t-SNE和UMAP主要用于可视化,能把高维数据映射到2D或3D,让你"看"清楚样本的分布。但注意:降维后的特征失去了原始物理含义,解释起来比较费劲。
from sklearn.decomposition import PCA
# 保留95%方差的主成分
pca = PCA(n_components=0.95)
X_pca = pca.fit_transform(X)
print(f"原始维度: {X.shape[1]}, 降维后: {X_pca.shape[1]}")
小建议:实际项目中我习惯"先粗筛再精选"——先用过滤法快速砍掉明显无用的特征,再用嵌入法或包裹法做精细筛选。这样效率高,效果也有保障。
下面这张图是我自己整理的特征筛选与降维方法体系,帮你把整体脉络理清楚。
你看这个结构:从高维ADMET特征出发,可以走特征筛选的路(过滤法、包裹法、嵌入法),也可以走降维的路(PCA、t-SNE、UMAP),还可以两者结合。我自己的经验是——先用过滤法粗筛到300维以内,再用嵌入法精筛到50-100维,最后可视化时用PCA或t-SNE看分布。这套组合拳在ADMET肝毒性项目里屡试不爽。
经验之谈:别一上来就上PCA。先问问自己——我需要保留特征的可解释性吗?如果是做肝毒性预测,后续可能要跟药化专家讨论哪些结构片段导致毒性,那就优先用特征筛选,保留原始特征名。如果只是为了构建高精度黑箱模型,降维完全可以上。
特征筛选和降维没有银弹。不同方法适合不同场景,关键是要理解你的数据特点、模型需求以及业务目标。嗯,这一块内容不少,但你在实战中多试几次,慢慢就找到感觉了。