资讯动态

Lasso回归与glmnet实战:高维数据特征筛选、交叉验证与结果解读

发布时间:2026/10/3 9:33:47 来源:尧图企业网站定制
做组学数据分析这几年我最有感触的场景就是拿到手一张表达矩阵两万行基因六十个样本。直接跑线性回归XX 不可逆系数根本求不出来换逐步回归第一步就卡死在计算上。Lasso回归就是专门在这种场景下干特征筛选这件事的。如果你手头是转录组、单细胞、微生物组这类高维数据想从几万个变量里筛出真正和分组、预后相关的少数几个R语言的 glmnet 包是首选没有之一。这篇就基于我的实际经验把 Lasso 的数学逻辑、glmnet的实操流程、结果解读方法和常见坑完整过一遍争取让你一次跑通、还能把图看懂。先说清楚这篇适合谁打算用表达矩阵做疾病标志物筛选的人做生存分析想找预后基因的人或者纯粹想搞清楚 Lasso 系数路径图、交叉验证图怎么看的人。不需要你数学多好但至少要懂一点线性回归知道什么是特征、什么是标签。我会用一组模拟的转录组场景贯穿全文代码可以直接改着用。1. 为什么选Lasso高维数据筛选的底层逻辑1.1 Lasso干了什么事一个带约束的回归Lasso的全称是 Least Absolute Shrinkage and Selection Operator翻译过来就是“最小绝对收缩和选择算子”。它做的事情本质是在普通最小二乘的目标函数后面加了一个惩罚项min β (y - Xβ)T(y - Xβ) λ∑|βj|公式不长但信息量很大。前面一半是残差平方和衡量模型拟合得好不好后面一半是 L1 惩罚把所有系数的绝对值加起来乘上一个系数 λ。λ 的大小决定了惩罚的力度λ 等于 0 的时候Lasso 就退化成普通最小二乘λ 非常大时所有系数都会被压到 0模型退化成一个只有截距的空模型真正有用的是中间这一段——一部分系数被压缩成精确的 0另一部分保留非零值。被压缩成精确 0 意味着什么意味着对应的变量被模型“剔除”了。这就是 Lasso 能同时完成回归和变量筛选的原因。普通线性回归在高维场景下之所以失效是因为 n样本量小于 p变量数时XX 不满秩有无穷多个解模型不知道选哪个加了 L1 惩罚之后目标函数变成了严格凸函数极小值点唯一数学上能稳定求解。这里有个很多初学者容易混淆的点Lasso 不是“先筛选变量再回归”它是在同一个目标函数里同时完成系数估计和变量选择。这比传统的两步走策略更优雅也更不容易过拟合。1.2 L1惩罚为什么能把系数压成零我最早学 Lasso 的时候一直想不通一个问题L2 惩罚岭回归也能把系数压缩变小为什么它不会把系数变成精确的 0L1 就能关键在于惩罚项的几何形状。为了直观假设模型只有两个系数 β1 和 β2。L2 惩罚项 β1² β2² ≤ t 对应的是一个圆形区域L1 惩罚项 |β1| |β2| ≤ t 对应的是一个菱形区域。残差平方和的等高线是一个椭圆我们要求的就是椭圆和约束区域相切的那个点。圆形是光滑的切点大概率落在圆弧上也就是说 β1 和 β2 都不为零只是整体变小菱形有尖角尖角恰好落在坐标轴上椭圆等高线碰到坐标轴上的尖角的概率远大于碰到光滑边的概率所以解经常落在轴上也就是其中一个系数正好等于 0。用生活化的比喻来说你往一个圆碗里放一个玻璃珠珠子大概率停在碗底的某个圆滑位置但如果你把碗做成菱形带尖角珠子更容易卡在角上。Lasso 的“角”就是坐标轴上的零点这就是稀疏解的几何来源。在算法层面glmnet 用的是坐标下降法。固定其他所有系数只更新当前这一个系数。因为目标函数对单个系数来说是二次函数加绝对值它的最小值可以用一个被称为“软阈值”soft-threshold的公式直接算出来当这个变量的梯度贡献绝对值小于 λ 时更新结果直接是 0。所以“精确为 0”不是近似结果而是每一步迭代的确定性输出。1.3 什么时候该用、什么时候不该用Lasso 绝不是万能药我见过不少人把它用错地方。先说说合适的场景第一变量数量远大于样本量比如基因数上万、样本只有几十个第二认为只有一小部分变量与结局相关其余大多是噪声第三想要一个可解释性强的稀疏模型比如拿一二十个基因去建评分。不太合适的场景也有如果变量之间存在很强的分组相关性比如一个通路里的基因表达高度共线Lasso 会倾向于只从这一组里挑一个代表不会把整组都选出来。这是它的本性不是 bug。这种情况下Elastic Net弹性网络会更合适它同时带 L1 和 L2 惩罚能把成组的强相关变量一起选进来。另外如果你最关心的是预测精度而不是变量可解释性随机森林、XGBoost 这类非线性模型往往表现更好Lasso 的线性假设在这里是天然短板。2. 核心参数与结果解读lambda选择是最大的坑2.1 数据预处理标准化是前提不是可选项Lasso 对变量的量纲极其敏感。因为惩罚项 λ∑|βj| 对每个系数施加的是同样的力度如果某个变量的数值范围是 0 到 1另一个变量的范围是 0 到 10000那同样的惩罚对后者的约束力实际上会弱很多模型会偏向于保留量纲大的变量哪怕它在生物学上并不重要。glmnet 包默认设置了 standardize TRUE也就是在拟合之前自动把每个变量做中心化和标准化拟合完成后再把系数换算回原始尺度。所以对大多数情况你不需要手动 scale。但理解这个机制很重要因为当你自己动手标准化、设置 standardize FALSE 时得到的系数是标准化尺度上的两个模型的非零变量名单会基本一致但系数的大小和排序含义不同。我的建议是如果你后续要用系数构建风险评分直接信任 glmnet 默认返回的系数对应原始表达量如果你是想比较变量相对重要性用标准化系数。基因表达数据的预处理还有几个前置步骤一般我会先过滤掉在所有样本里表达量都很低的基因再做 log2(TPM 1) 变换然后再交给 glmnet。直接把原始整数 counts 丢进去通常效果不好因为高表达基因的方差天然比低表达基因大干扰变量选择。2.2 lambda序列与交叉验证lambda.min和lambda.1se怎么选glmnet 不会只在一个 λ 下拟合模型它会自动生成一条从大到小的 λ 序列从大到小逐个尝试。最大的 λ记作 λmax刚好是所有系数都为零的临界值然后逐步降低非零系数越来越多。整个过程非常高效因为算法用了“热启动”技巧计算完前一个 λ 的解之后用它作为下一个 λ 的初始值收敛极快。这也是 glmnet 在大矩阵上依然跑得飞快的重要原因。问题来了这一串 λ 里选哪个glmnet 提供了交叉验证函数 cv.glmnet默认做 10 折交叉验证对每一个 λ 计算模型在验证集上的误差。误差最小的那个 λ 记为 lambda.min为了保险它还计算了一个 lambda.1se意思是取“误差在最小值一个标准误范围内、但 λ 值最大”的位置。这两个值的使用原则我总结成一句话以预测为首要目标选 lambda.min以筛选稳定变量、追求模型简洁为首要目标选 lambda.1se。在基因表达数据里lambda.min 常常筛出几十上百个基因lambda.1se 往往只有十几个基因。我个人经验是lambda.1se 变量在后期的独立验证里更稳健假阳性率更低。代价是会漏掉一些边缘相关的变量但做科研时我宁可错杀也不愿拿一长串名单去下游验证。一个容易忽略的细节cv.glmnet 的结果受随机种子影响。交叉验证划分样本的随机性会导致 lambda.min 和 lambda.1se 有微小波动变量名单偶尔也会变。所以跑 CV 之前一定要设置 set.seed()最好多试几个种子看看结果稳不稳定。这一点在后面稳定性检验里还会细说。2.3 family参数与模型适配glmnet 的核心参数里除了 alpha另一个必须搞清楚的就是 family。它决定模型类型选错了整个结果都不可用。我整理了一张常用表family模型类型y 的格式常用 predict typegaussian线性回归连续数值response预测均值binomial二分类逻辑回归0/1 或两水平因子response概率、class类别multinomial多分类逻辑回归多水平因子class类别poisson泊松回归非负整数计数response速率coxCox 比例风险回归Surv 对象累积风险做转录组相关的分析用得最多的是 binomial 和 cox。想找区分病例和对照的标志物用 binomial想做预后模型、找和生存时间相关的基因用 cox。这两种的实操结构很接近都要先构建表达矩阵然后给 y 赋值。特别注意一点binomial 的 y 如果是因子两个水平的顺序会影响结果的符号方向。建议明确把 y 编码成 0/1 数值比如 0 代表对照组、1 代表疾病组这样系数为正就表示该基因高表达增加疾病风险解读起来不会绕弯。2.4 系数路径图怎么读、怎么看plot(cvfit$glmnet.fit, xvar lambda) 画出来的是系数路径图。横轴是 log(lambda)纵轴是每个变量的系数值每条彩色曲线代表一个基因。从右往左看这张图最右边 λ 非常大所有系数都等于 0曲线汇聚在 0 点随着 λ 变小某些系数率先离开 0说明这些变量最早进入模型、与结局的关联信号最强继续往左更多的系数被激活。曲线离 0 越远说明在某个 λ 下这个变量的系数越大。看路径图不要只看最终名单。我通常关注的是哪些基因在很靠右的位置就偏离了 0这些是强信号变量哪些基因直到 λ 很小才被选进来这些往往很可疑可能是搭了别人的便车。路径图的另一个用途是判断稳定性——曲线抖得厉害说明系数不稳定光滑单调上升的曲线更可信。3. 实操过程从表达矩阵到稳定变量集3.1 数据格式与处理示范先用一个模拟场景串联整个过程。假设拿到一个转录组数据集60 个样本其中 30 个疾病组、30 个对照组表达矩阵筛掉低表达基因后还剩 5000 个基因。目标是筛出与疾病状态相关的标志性基因。数据格式必须满足 glmnet 的要求x 是一个矩阵行是样本、列是基因行名是样本 ID、列名是基因名y 是向量或因子和 x 的行一一对应。如果你的原始数据是 data.frame记得用 as.matrix() 转成矩阵这一步最容易忘。另外glmnet 不接受 data.frame 里的因子列自动转换所有类别变量必须自己先做成虚拟变量。实际操作中我还会提前做一个操作把基因名列名统一成规范的基因名用 make.names() 去掉特殊字符否则后面提取系数时列名对不上非常容易踩坑。3.2 核心代码实现10折交叉验证与系数提取下面这段代码是我在实际项目里的固定套路。注意所有关键位置都加了注释。library(glmnet) # 设置随机种子保证 CV 划分结果可复现 set.seed(2024) # expr_mat60行 x 5000列的矩阵 # group因子30个0 30个1 # 建议 y 显式编码成数值0/1 y - ifelse(group disease, 1, 0) # 10折交叉验证alpha1 表示纯 Lasso # type.measureauc 适用于二分类如果用分类错误率可改成 class cvfit - cv.glmnet( x as.matrix(expr_mat), y y, family binomial, alpha 1, nfolds 10, type.measure auc ) # 画出交叉验证曲线 plot(cvfit) # 提取 lambda.1se 对应的系数 coef_res - coef(cvfit, s lambda.1se) # 稀疏矩阵转成普通向量 coef_vec - as.numeric(coef_res) names(coef_vec) - rownames(coef_res) # 剔除截距项保留非零系数的基因 selected_genes - names(coef_vec)[ coef_vec ! 0 names(coef_vec) ! (Intercept) ] # 输出结果方便下游富集分析使用 write.csv( data.frame(gene selected_genes, coefficient coef_vec[coef_vec ! 0]), file lasso_selected_genes.csv )跑完这段代码你在终端会看到一个非零变量数量的数字。如果屏幕上显示 47 个非零系数而其中大部分基因的系数都特别小不要急着认为模型找到了 47 个标志物——首先要做的是观察系数分布过滤掉那些系数绝对值极小的“凑数基因”。type.measure 我特意选了一个“auc”。为什么不用默认的 deviance因为二分类问题里如果两组样本不平衡AUC 比分类错误率稳健得多它不依赖阈值直接衡量模型把所有样本正确排序的能力。如果你关心的是“分对多少”那就用 class。3.3 结果解读与生物学注释拿到非零基因列表之后第一件事不是去做 GO 富集而是检查名单的合理性。第一看方向。系数为正的基因在疾病组高表达系数为负的基因在对照组高表达。挑几个已知的基因查一下文献看方向是否符合已有认知。这一步很快但能提前发现标签是否搞反、数据是否有批次效应等低级错误。第二看量级。我会把系数从大到小排序画个条形图。通常前十几个基因贡献了主要信号后面的基因系数很小。在转录组数据里这类小系数基因大概率是噪声即使被 λ 筛进来也扛不住后续验证。第三才轮到生物学注释。把基因列表交给 clusterProfiler 做 GO/KEGG 富集分析看看它们是不是集中在某条已知通路里。如果富集结果杂乱无章、什么通路都显著我反而会怀疑这批变量是假阳性如果富集到一两条和疾病机制高度吻合的通路心里就踏实很多。这里多说一句GO 富集是给 Lasso 结果做“验收”不是做“筛选”。不要反过来用富集分析筛选基因那会导致严重的循环论证。3.4 稳定性检验与下游衔接Lasso 变量选择的不稳定性是真实存在的尤其样本量小的时候换一个随机种子筛出的基因名单可能差 20%。这不代表前面的分析白做了而是提示你需要做一次“频率筛选”。我常用的方案是 Bootstrap Lasso有放回抽样 100 次每次都用同样的交叉验证流程最后统计每个基因在 100 轮里有多少次被选中。只在超过 80% 轮次里被选中的基因才是真正的核心变量。set.seed(123) stab_count - table(NULL) boot_genes - lapply(1:100, function(i) { idx - sample(nrow(expr_mat), replace TRUE) fit - cv.glmnet( x expr_mat[idx, ], y y[idx], family binomial, alpha 1, type.measure auc ) cfit - coef(fit, s lambda.1se) names(which(cfit[, 1] ! 0))[names(which(cfit[, 1] ! 0)) ! (Intercept)] }) freq_table - sort(table(unlist(boot_genes)), decreasing TRUE) stable_genes - names(freq_table)[freq_table 80]这段代码跑了之后你看结果会发现一个很有意思的现象很多基因偶尔被选一次少数基因几乎每次都被选。那 80% 及以上频率的基因就是经得起数据扰动考验的稳定信号。下游的 GO/KEGG 富集、独立队列验证、qPCR 验证都应该围绕这些稳定基因开展而不是围绕单次 CV 的完整名单。如果是生存数据流程几乎一样只是把 family 改成 coxy 换成 Surv 对象。筛选出的预后基因最后可以拟合一个风险评分riskScore ∑βj × expr_j然后按中位数分组画 KM 曲线这是临床文章里非常经典的分析框架。4. 常见问题与排查技巧实录4.1 类别变量处理one-hot的坑Lasso 不接受因子变量直接输入必须先转成虚拟变量。R 里用 model.matrix 可以一步到位但有几个容易被忽略的点。第一model.matrix 默认会为因子变量生成 n-1 个虚拟变量第一水平被当作基线。这在统计上没问题但在 Lasso 里基线水平的“信息”并没有消失它被折叠到截距里了。如果你的分类变量有几十个水平这种处理会把一个变量拆成几十列Lasso 可能只从中选出一两个水平形成很碎片化的结论。我的建议是水平数少的因子可以这样处理水平数多的先用业务逻辑合并低频类别。比如临床数据里的“就诊医院”有 20 个中心其中 15 个中心样本量只有两三个直接 one-hot 会产生一堆稀疏列模型很不稳定。先把样本量小于某个阈值的中心合并成“其他”再进 Lasso效果会好很多。第二虚拟变量列名的可读性。model.matrix 自动生成的列名会带着因子的变量名前缀和水平名如果你的因子水平名里带空格或中文后面列名对不上非常头疼。建议转之前先做 make.names。4.2 报错“不存在叫glmnet这个名字的程辑包”这个报错大概是 R 初学者最常遇到的。原因很简单包没装或者没加载。glmnet 在 CRAN 上直接执行 install.packages(glmnet) 就能装。如果安装速度极慢或者报错通常需要换一个网络镜像在 RStudio 的 Global Options 里把默认镜像换掉再重新安装。还有一种情况代码里用了 library(glmnet)但控制台提示找不到依赖包。glmnet 依赖 Matrix 包一般会随安装自动带上但如果你的 R 版本太老可能需要手动升级 R 或者单独 install.packages(Matrix)。这里提醒一句决定做生物学分析之前先把 R 更新到当前主流版本能省掉很多环境类报错。4.3 数据里有缺失值glmnet不接受NAglmnet 对缺失值零容忍x 或 y 里任何一个 NA 都会直接报错。这在表达矩阵里很常见某个基因在部分样本里没有检测到或者临床变量有缺失。最简单的做法是删掉缺失率高的基因或样本。这一步要谨慎删除太多会把有效信息丢掉。缺失率低于 5% 的基因可以用该基因在所有样本中的中位数填充但只能作为权宜之计。更推荐的做法是用 KNN 插补R 里有 impute 包默认取该基因在 K 个最相似样本中的加权平均来填补。插补之前一定要做 log2 变换在原始 counts 级别插补很容易因为个别极端值产生负值。4.4 强相关变量、组效应Lasso的软肋前面说过Lasso 对强相关变量倾向于只选其中一个代表。这对解读结果影响很大你筛出来的“标志基因”不一定是生物学上最重要的可能只是某个高度相关通路里的“代言人”。检测方法很简单把最终选中的基因两两算相关系数如果发现有几个基因之间的相关系数超过 0.9就要小心了。处理策略有两个。一是接受这种代表性在文章里明确说明“该基因代表某条通路”二是改用弹性网络将 alpha 设为 0.5让 L1 和 L2 惩罚各占一半这样强相关的成组基因有机会被同时选出来。alpha 本身也可以作为调参对象用 cv.glmnet 同时搜索 alpha 和 lambda但计算量会明显上升。4.5 我筛出的变量每次跑都不一样怎么办除了设置种子更本质的解决办法是承认单次 CV 的不确定性用频率制胜。我在 3.4 节里给的 Bootstrap 方法就是干这个的。还有一种情况会让变量名单特别飘样本量太小。假设只有 20 个样本5000 个变量10 折交叉验证每折训练集只有 18 个样本模型在不同折里学到的模式差异极大。这种数据本身就不该指望 Lasso 能稳定筛出 20 个基因。能做的有扩大样本量、降低基因数先做无监督过滤、用更保守的 lambda.1se。但归根结底统计方法不能无中生有信息量不够时稳定筛选是奢望。4.6 Lasso不给你p值怎么向审稿人交代很多人跑完 Lasso 之后会问这些基因显著吗P 值是多少glmnet 的输出里没有 p 值这不是因为它偷懒而是因为 Lasso 做了变量选择经典的假设检验框架在这里不再成立。变量选择过程本身会引入偏差拿常规方法算出来的 p 值会过小属于“吃自己的狗粮”。如果审稿人硬要 p 值我一般给两条路。第一条路是“选择后推断”用 selectiveInference 包做 post-selection inference它能输出条件型 p 值思想是“在已经知道我们做了 Lasso 选择的前提下重新校准”。这个包用起来略繁琐需要传入原始的惩罚参数但对严谨的统计审稿人来说很受用。第二条路更接地气把 Lasso 当成筛子选出的基因放进普通逻辑回归模型里重新拟合报告这里的 Wald p 值。这样做出来的 p 值在严格意义上偏乐观但作为探索性分析结果写进文章很多期刊是接受的。只要你在方法部分写清楚“P 值来自对 Lasso 选定基因的后续标准回归未校正选择效应”审稿人一般不会揪着不放。我个人的习惯是两条路都做正文放选择后推断补充材料放二次拟合结果既严谨又实用。最后再分享一个小技巧很多教程到这里就结束了但我还想补一个细节从 coef 提取出来的基因名单命名风格可能和你的原始矩阵不完全一致有些人直接在 Excel 里手动匹配基因名既容易出错又浪费时间。我建议在提取基因列表之后立刻用一个交叉验证函数对比一下名单里的名字是否 100% 能在原始矩阵的列名里找到找不到的单独输出一个警告文件。别问我为什么强调这个我在一个项目里因为一个基因名的连字符格式差异整整查了两天才发现是匹配失误从那以后这一步就写成了固定流程。做组学数据分析模型跑通只是第一步把变量名单解释得让别人信服才是真正花时间的地方。Lasso 帮你把两万个基因缩小到二十个是给你一个聚焦的起点而不是终点。祝你跑通代码之后拿到的每个基因都有故事可讲。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑