特征选择基础:方差过滤、互信息、卡方检验、LASSO特征选择

RNA-seq 数据有个特点——基因数量动不动两万起步,样本就几十个。你把两万维直接喂到模型里,训练慢不说,还容易过拟合。说白了,特征选择就是帮你把真正有用的信号挑出来,把噪音扔掉。

我个人习惯是这样:先用方差过滤把“万年不变”的基因干掉,再用互信息或卡方检验挑出跟表型相关的特征,最后用 LASSO 做一次精细筛选。这套组合拳我在 TCGA 项目里用过几次,基本能把特征从两万降到几百,模型收敛速度快一个数量级。

方差过滤 – 最简单的筛子

先想一个问题:一个基因在所有样本里表达量几乎一样,它能用来区分样本吗?肯定不能。方差过滤就是干掉那些“没变化”的特征。

原理很简单——计算每个基因的表达值方差,低于某个阈值的直接丢掉。我一般设阈值方式比较暴力:保留方差最大的前 10% 或 20%。你也可以用中位数或四分位间距。

注意:方差过滤只适合已经做过归一化的数据(比如 log2(TPM+1))。原始 count 数据方差跟表达量正相关,直接过滤会把低表达但生物学意义强的基因也丢掉。
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 个桶),效果更稳。

互信息计算量跟特征数平方相关。两万个基因硬算,我机器跑过 40 分钟。建议先做方差过滤降维到 5000 以内再用互信息。

卡方检验 – 分类目标的神器

如果你的表型是离散的(肿瘤 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,这是我反复验证过的黄金特征选择流水线。RNA-seq 数据按这个顺序跑,基本不会漏掉关键基因,也不会保留太多噪音。

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 对特征之间的共线性比较敏感。如果你先用了方差过滤+互信息,共线性已经降了不少,再用 LASSO 效果就比较稳定。我一般先做一次相关性聚类,把高度相关的基因(|r|>0.9)随机保留一个,再进 LASSO。

四种方法对比

方法类型适用变量计算速度主要坑点
方差过滤无监督过滤连续极快可能丢掉方差小但有生物学意义的基因
互信息单变量过滤连续+离散较慢需离散化;大特征数时慢
卡方检验单变量过滤离散目标快需先二值化;小样本失效
LASSO嵌入式选择连续+离散中等不稳定;需调C;共线性敏感

下面这张图把四种方法在特征选择流水线中的位置和关系画了出来:

RNA-seq 特征选择流水线 原始表达矩阵 (20000+ 基因) 步骤1: 方差过滤 (保留高变异基因) 互信息 捕捉非线性关系 卡方检验 分类目标快速筛选 LASSO 嵌入正则化选择 交集/并集 → 候选特征集 精选特征 (100~500 基因) 💡 实际推荐流程: 先方差过滤 → 互信息/卡方 → LASSO 精细筛选