特征选择基础:方差过滤、互信息、卡方检验、LASSO特征选择
RNA-seq 数据有个特点——基因数量动不动两万起步,样本就几十个。你把两万维直接喂到模型里,训练慢不说,还容易过拟合。说白了,特征选择就是帮你把真正有用的信号挑出来,把噪音扔掉。
我个人习惯是这样:先用方差过滤把“万年不变”的基因干掉,再用互信息或卡方检验挑出跟表型相关的特征,最后用 LASSO 做一次精细筛选。这套组合拳我在 TCGA 项目里用过几次,基本能把特征从两万降到几百,模型收敛速度快一个数量级。
方差过滤 – 最简单的筛子
先想一个问题:一个基因在所有样本里表达量几乎一样,它能用来区分样本吗?肯定不能。方差过滤就是干掉那些“没变化”的特征。
原理很简单——计算每个基因的表达值方差,低于某个阈值的直接丢掉。我一般设阈值方式比较暴力:保留方差最大的前 10% 或 20%。你也可以用中位数或四分位间距。
import numpy as np
from sklearn.feature_selection import VarianceThreshold
# expr 是样本×基因的矩阵
selector = VarianceThreshold(threshold=0.01)
expr_high_var = selector.fit_transform(expr)
# 保留的基因索引
selected_idx = selector.get_support(indices=True)
print(f"保留 {len(selected_idx)} 个基因,原始 {expr.shape[1]}")
我在项目中遇到过一件事:有一次阈值设得太高了(0.05),结果丢掉了一个肿瘤标志基因 TP53,因为它几乎在所有样本里都突变表达,方差反而很小。后来我把阈值改成了“保留前 5000 高方差基因”,不再用固定阈值,这个问题就解决了。
互信息 – 非线性关系的捕手
方差过滤只看基因自身,不看目标变量。如果你想知道“这个基因和癌症类型有没有关系”,互信息(Mutual Information)是个好工具。
互信息能捕捉任何非线性关系——不像 Pearson 相关系数只拟合线性。你想想看,很多生物学关系是剂量效应、开关效应,不是一条直线能描述的。
from sklearn.feature_selection import mutual_info_classif
# labels: 分类标签,比如 cancer vs normal
mi = mutual_info_classif(expr_high_var, labels, random_state=42)
# 选出前 200 个基因
top_idx = np.argsort(mi)[-200:]
print("Top 5 MI scores:", mi[top_idx[-5:]])
我记得有次做三分类(Luminal A, Luminal B, Basal),互信息直接把我常用的差异基因列表里的排名打乱了——好几个低表达但强特异性的基因排名冲到了前面,后来验证确实有效。不过这里有个坑:互信息对离散化敏感,我习惯先对连续表达值做等宽分箱(比如分 10 个桶),效果更稳。
卡方检验 – 分类目标的神器
如果你的表型是离散的(肿瘤 vs 正常,药物响应 vs 不响应),卡方检验是个又快又直观的方法。它检验每个基因的表达值是否跟类别独立。
但直接对连续值做卡方检验不行——必须先把表达量离散化。我习惯按中位数或四分位数把表达量切成“高/低”或“高/中/低”几个水平。
from sklearn.feature_selection import chi2, SelectKBest
# 先二值化:>中位数为1,否则0
expr_binary = (expr_high_var > np.median(expr_high_var, axis=0)).astype(int)
selector = SelectKBest(chi2, k=100)
expr_selected = selector.fit_transform(expr_binary, labels)
# 查看卡方统计量
chi_scores = selector.scores_
print("Highest chi2 score:", chi_scores.max())
卡方检验的优点是快——sklearn 实现里它甚至不需要拟合,直接算统计量就行。但要注意样本量太小时卡方近似可能失效,我习惯每条类别至少要有 20 个样本以上才敢用。
LASSO 特征选择 – 自带正则化的筛选
前面几种方法都是“单变量过滤”——每个基因独立打分,不考虑基因之间的相互作用。LASSO 是“多变量嵌入型”选择:它用 L1 正则化让不重要的特征权重直接变成 0。
简单说,LASSO 在拟合线性模型的时候,会自动把一些没用的系数压缩到 0。剩下的非零系数对应的基因就是它选出来的。实际操作时,我通常会调一下正则化参数 λ(C 的倒数),让选出来的特征数在 50~200 之间,这个范围对于后续模型比较友好。
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(expr_selected)
# C 是正则化强度的倒数,越小惩罚越强
lasso = LogisticRegression(penalty='l1', solver='saga', C=0.1, max_iter=10000)
lasso.fit(X_scaled, labels)
# 非零系数的基因
selected_genes = np.where(lasso.coef_.flatten() != 0)[0]
print(f"LASSO 选出 {len(selected_genes)} 个特征")
我踩过一个坑:LASSO 在特征数远大于样本数的时候,选出来的特征稳定性很差。同样的数据,换一组交叉验证折,选出的基因都不一样。解决方案是用稳定性选择(Stability Selection)——重复跑多次 LASSO,保留出现频率超过 80% 的特征。我在一篇泛癌分型文章里就用了这个方法,审稿人专门表扬了稳定性验证。
四种方法对比
| 方法 | 类型 | 适用变量 | 计算速度 | 主要坑点 |
|---|---|---|---|---|
| 方差过滤 | 无监督过滤 | 连续 | 极快 | 可能丢掉方差小但有生物学意义的基因 |
| 互信息 | 单变量过滤 | 连续+离散 | 较慢 | 需离散化;大特征数时慢 |
| 卡方检验 | 单变量过滤 | 离散目标 | 快 | 需先二值化;小样本失效 |
| LASSO | 嵌入式选择 | 连续+离散 | 中等 | 不稳定;需调C;共线性敏感 |
下面这张图把四种方法在特征选择流水线中的位置和关系画了出来: