前一阵朋友拿一组工业设备的振动特征跑回归候选特征有四十几列样本却只有百来条。最小二乘拟合的时候 R² 能到 0.9换到新数据上预测完全崩掉。这种场景LASSO 回归基本是绕不开的第一选择——它能在几十上百个候选变量里自动挑出真正有贡献的那几个还顺手解决了共线性和过拟合的问题。这篇文章我就完整聊一下在 MATLAB 里用 LASSO 做特征选择的全过程原理讲了什么、函数怎么调、参数怎么理解、实际跑数据会踩哪些坑最后给一份可以直接改来用的流水线代码。适合正在做数据分析、科研建模、工程预测尤其是特征数远大于样本量、又需要可解释特征名单的读者。1. 为什么偏偏是 LASSO稀疏性、共线性与砍特征的数学直觉很多人第一次接触 LASSO 是在教材的公式里在最小二乘目标函数后面加了一个 λ 乘以系数绝对值之和的惩罚项。公式本身不难难的是理解它为什么能把一堆系数干脆利落地压成零。这一节不画复杂推导就讲清楚三个问题稀疏性从哪来、共线性怎么被处理、以及它和岭回归、弹性网的区别。1.1 从普通最小二乘到惩罚项LASSO 到底改了什么普通最小二乘回归的目标是让残差平方和最小它对每个特征都来者不拒只要数据里有一点点相关性它就会把对应系数拽出来哪怕这个相关只是噪声造成的。样本一少、特征一多这种什么都想用的倾向就变成了灾难——模型自由度太大训练集上拟合得很完美测试集上一塌糊涂。LASSO 做的事情是给系数大小本身加了价格。目标函数写出来是[ \min_{\beta} \frac{1}{2n} \sum_{i1}^{n} (y_i - X_i \beta)^2 \lambda \sum_{j1}^{p} |\beta_j| ]后一项是 L1 范数。它的作用不是让所有系数一起变小那么简单而是让一部分系数直接被压到零。λ 越大压得越狠最终能留下来的特征就越少。这本质上是个以少量偏差换大量方差降低的交易系数有一点收缩但模型的稳定性和可解释性大幅提升。在 MATLAB 里这一项对应的是lasso函数里的Lambda参数。你给一个 λ它就从头优化一遍求解路径给一串 λ它就把整条系数路径都算出来。很多人第一次用的时候只关注最终特征列表忽略了 λ 才是整个算法的灵魂。后面我会详细说怎么选 λ这里先建立概念。1.2 稀疏性不是玄学几何直觉与软阈值为什么 L1 惩罚能产生精确的零而 L2 惩罚岭回归只能把系数缩小但不能让它刚好等于零一个直观的解释是L2 惩罚对应一个圆形的约束区域L1 惩罚对应一个尖角的菱形。最小二乘解落在哪个位置取决于数据拟合的最优点和约束区域的第一次接触。菱形在坐标轴上有尖角最优点很自然会落在某个尖角上也就是某些系数正好为零。换成圆形的岭约束接触点基本都在坐标轴之外的曲线上所以所有系数都还活着只是被缩小了。这就是为什么岭回归永远做不到真正的特征选择而 LASSO 可以。用软阈值的说法更工程化每次迭代中系数绝对值小于某个阈值的那一批直接会被清零。注意这个阈值和 λ 直接相关λ 越大阈值越高清零的特征越多。这个机制让 LASSO 在 p 远大于 n 的时候依然能工作但它最多只能选出不超过样本量的非零系数——这是维度诅咒的物理极限后面踩坑部分会再提到。1.3 LASSO vs 岭回归 vs 弹性网什么时候选谁光知道 LASSO 会砍特征还不够实际建模时你得知道什么时候该用它什么时候改换武器。三者的核心差异我整理成一张表方法惩罚形式稀疏性系数有界性适用场景普通最小二乘无无无p 远小于 n特征之间基本正交岭回归L2无只收缩有界但不为零特征高度相关但需要保留全部特征LASSOL1有自动选特征有界且精确为零特征多、样本少追求稀疏可解释模型弹性网L1 L2 混合有有界且精确为零特征抱团高度共线L1 选一个、L2 分权这里特别提醒一句如果你的特征里存在一组高度相关的变量LASSO 往往会在这组里随机挑一个留下而不是稳定地选出业务上最重要的那个。因为相关性太强时它们的惩罚代价几乎一样谁留下谁被删受初始权重影响很大。这种情况我建议改用弹性网也就是把lasso的Alpha参数从 1 调小到 0.5 附近让 L2 部分帮一下抱团特征的权重分配或者后面我要讲的多次自助重采样。2. MATLAB 的 lasso 函数参数体系和输出结构一次性说清MATLAB 里跑 LASSO 已经做得很完善核心就是一行[B, FitInfo] lasso(X, y, ...)。但很多教程对FitInfo里的字段、Lambda的选择方式讲得不够透导致使用者只会默认参数跑一遍、抄一个特征名单遇到复杂数据立刻懵。这一节把它彻底拆开。2.1 函数签名与核心参数Lambda、CV、Alpha 的含义函数的基本调用是[B, FitInfo] lasso(X, y, Lambda, lambdaSeq, CV, 10, Alpha, 1, PredictorNames, names)逐项说清楚这些参数到底在干什么。第一个是Lambda。你可以不给让函数自己生成对数等间隔的 λ 序列从最大惩罚所有系数全为零到最小惩罚近似 OLS覆盖一整条路。你也可以给一组自定义的 λ但最常见的是空着不管交给交叉验证去选。官方文档里生成的序列存在FitInfo.Lambda里一共默认 100 个点样本量少或特征量级差异大时可以配合LambdaRatio调整最大最小 λ 的比例防止序列过短或过长。第二个是CV。这是最有价值的一个参数传入正整数 k 表示做 k 折交叉验证也可以传一个cvpartition对象来精细控制怎么划分数据。开了 CV 之后MATLAB 会为每个 λ 都计算一遍交叉验证误差返回的FitInfo里会给你两个自动选好的 λLambdaMinMSE和Lambda1SE。千万注意交叉验证的折数并不是越大越好数据量小的时候 5 折比 10 折更稳因为每一折留出的样本更多误差估计的方差更小。第三个是Alpha。前面说过Alpha1 是纯 LASSOAlpha0 是岭回归0 到 1 之间是弹性网。工程里如果特征共线问题不明显直接 Alpha1 没问题若是高维、强相关特征扎堆建议设置成 0.5 跑一下对比稀疏程度和预测误差再做决定。这个参数很少被教程提到但在实际问题里经常是救命的那一个。第四个是PredictorNames。它是一个长度等于列数的字符串元胞数组作用是在lassoPlot图例和FitInfo里用可读的特征名代替 X1、X2。它不参与计算纯粹是提高结果可读性。建模不是跑完就算了你要跟业务方解释保留的是哪几个特征名字会比列号有意义得多。2.2 输出结果 FitInfo 里到底装着什么lasso返回的第二个输出FitInfo是个结构体新手往往只取B就完事实际上大部分决策信息都在FitInfo里。字段说明我列在这里字段含义Lambda本次求解用的 λ 序列1×L 向量MSE每个 λ 对应的交叉验证 MSELambdaMinMSE使 CV MSE 最小的 λLambda1SE在最小 MSE 一个标准误范围内取最大最稀疏的 λIndexMinMSE / Index1SE上面两个 λ 对应 B 的第几列Intercept每个 λ 对应的截距项1×L 向量mu / sigma / xs标准化前的均值、标准差以及标准化后的训练数据LambdaMinMSE是个容易让人误会的名字。它并不是每个 λ 的最小 MSE而是CV MSE 达到最小时的 λ。代码里用FitInfo.IndexMinMSE和FitInfo.Index1SE去索引 B 的列比手动写find(FitInfo.Lambda FitInfo.LambdaMinMSE)更靠谱因为浮点数比较会踩精度坑。A的大小永远是特征数 × λ 个数。也就是说B 的每一列对应一个 λ 下的系数向量。特征选择不是取一个 B而是在 B 的某个列里找出非零行。这一点想清楚后面提取特征名单就不会绕弯。2.3 标准化是半透明的一个很多人没意识到的细节lasso函数官方说明里默认对 X 做标准化再求解求解完再把系数还原到原始尺度。这意味着直接算出来的 B 就是你拿到原始数据时可以直接乘的系数预测新样本用Xnew * B intercept就行不需要手动对 Xnew 做标准化。很多教程里先用 zscore 标准化 X再用 OLS的做法在这里是重复劳动而且会把系数尺度搞错。这个设计对用户是友好的但它也带来两个隐患。第一当你自己观察 B 的系数大小来判断特征重要性时不同量纲特征还原后的系数差异并不完全反映标准化后的重要性最好结合FitInfo.mu和FitInfo.sigma还原出的标准化系数或者用FitInfo.xs重新拟合一遍才能做公平比较。第二如果某个特征本身的方差极大它在标准化的序列中可能因为数值问题导致 λ 序列选择异常。所以我通常在喂给lasso之前先手动剔除近零方差列把明显离群的极值做截断这样求解路径更稳定。3. 实操完整链路从脏数据到可用特征名单理论说多了没用直接走一遍数据流程。这里我把完整链路拆成六个环节每一步都说明为什么这么干方便你直接套用到自己的数据上。3.1 数据预处理缺失值、常数列、分类变量建模前先把数据洗干净。缺失值方面lasso不接受 NaN要么删除带缺失的样本要么用中位数/均值填充。我个人倾向对数值特征用中位数填充因为均值容易被离群值带偏而lasso对离群值又比较敏感一个极端点就可能让某条 λ 路径的系数完全失真。然后是常数列和近零方差列。一个常在数值不变的列标准化后全都是零计算中会产生退化问题即使没退化它在 LASSO 里也纯粹是噪声。所以第一步要过滤掉。分类变量要注意它不能直接当数值灌进去尤其是无序多分类必须先做哑变量编码。比如一个故障类型特征有 A/B/C 三档拆成两列 0/1 的虚拟变量就好。MATLAB 里可以dummyvar(categorical(T.type))快速生成。整个预处理阶段我都建议写成一个函数或者脚本片段输出一个干净的X、一个y、以及一份特征名列表。特征名列表尤其重要因为后续PredictorNames参数、fitlm的表格输出、结果存档全都靠它没有名字等你要解释业务结论时会非常痛苦。3.2 训练测试划分的最优姿势先定数据边界再谈特征选择抱持特征选择也必须严格基于训练集这条铁律。常见的错误是先把全量数据归一化、把异常值截断、甚至用全量数据跑一遍 LASSO然后再把选出的特征放到训练/测试划分里——这会在测试集上引入信息泄漏算出的预测误差乐观到失真。正确的顺序是划分训练集/测试集之后的预处理统计量完全由训练集计算。例如中位数填充的数值要用训练集的中位数异常值截断的阈值要用训练集的 10% 和 90% 分位数哑变量编码的类别集合也要和训练集保持一致免得测试集里冒出训练集没见过的类别。用 MATLAB 的cvpartition时最好同时固定随机数种子rng(42)保证可复现。交叉验证的折数我放在lasso的CV参数里但如果你要严格保证训练/测试划分在 LASSO 之前就不要把 CV10 理解为外面套的回测。rng(42); cvp cvpartition(numel(y), HoldOut, 0.25); Xtr X(cvp.training, :); ytr y(cvp.training); Xte X(cvp.test, :); yte y(cvp.test);这一步做好了后面的结果才有人信。3.3 跑交叉验证并可视化两条曲线决定 λ接下来真正跑 LASSO。用交叉验证的同时我会把两条曲线都画出来CV MSE 曲线和系数路径图。[B, FitInfo] lasso(Xtr, ytr, CV, 10, Alpha, 1, PredictorNames, featureNames); figure; lassoPlot(B, FitInfo, PlotType, CV); xlabel(log(Lambda)); title(交叉验证 MSE 曲线); figure; lassoPlot(B, FitInfo, PlotType, Lambda, XScale, log); title(系数路径);CV 那张图里 MATLAB 会标出LambdaMinMSE和Lambda1SE两条竖线。你们一定会有个疑问到底选哪个我的原则是如果目的是做预测、要尽量压低误差选LambdaMinMSE如果目的是解释、要尽量简洁的特征名单优先选Lambda1SE因为它牺牲了一丁点精度换来更少、更稳定的特征。真实的工程权衡里我几乎总用Lambda1SE对应的特征然后用这些特征再重新拟合一个不带惩罚项的模型把偏差还回去。系数路径图的好处在于你可以看到每个特征在整个 λ 区间上是什么时候进入、什么时候消失的。一个真正稳定的特征会在很宽的 λ 范围内保持非零一个脆弱的特征只在很窄的 λ 区间内蹦出来很快又变回零。这种耐久度信息比单看某个 λ 下的入选名单更有价值。建议把这两张图都存下来写报告时能直接用。3.4 特征名单提取与第二阶段建模LASSO 选的变量要回归 OLS使用Lambda1SE对应的那一列 B把非零系数的特征拽出来。这步的关键是别搞混λ 列索引和特征行索引两个维度。idxLambda FitInfo.Index1SE; idxSelected find(B(:, idxLambda) ~ 0); selectedNames featureNames(idxSelected); coefsLasso B(idxSelected, idxLambda); interceptLasso FitInfo.Intercept(idxLambda); % 第二阶段用选中的特征跑一次无惩罚回归 XtrSel Xtr(:, idxSelected); mdl fitlm(XtrSel, ytr);为什么要跑第二阶段因为 LASSO 的系数是有偏的它为了让系数归零而施加了收缩保留下来的系数普遍偏向零。如果你想用这些系数做最终解释或者搭建预测模型直接用 LASSO 的系数会系统性低估真实效应。正确做法是把特征名单当作筛选结果再用普通最小二乘对这几个筛选出的变量做无偏估计得到系数、标准误、p 值和置信区间。特征选择这件事已经由 LASSO 做了第二阶段的 OLS 不需要再承担稀疏化责任自由度足够。在测试集上验证时注意预测要用第二阶段模型的系数而不是coefsLasso。我见过有人把特征名单和系数混在一起预测效果说不清楚是哪个模型的容易造成 self-inflicted 混乱。pred predict(mdl, Xte(:, idxSelected)); rmse sqrt(mean((yte - pred).^2)); rsq 1 - sum((yte - pred).^2) / sum((yte - mean(yte)).^2);4. 实战踩坑记录五个高频问题的排查思路每个跳过的地方几乎都是同行问我的高频问题。这里只挑最实在的五个来说每个都给出根因分析和排查路径而不是丢一个模糊答案。4.1 两个高度相关的特征LASSO 选谁看运气场景回放某数据里温度_A和温度_B是同一个传感器不同通道的耦合输出相关系数 0.98。跑完 LASSO两次运行因为随机抽样不同一个保留 A 删掉 B另一次反过来。这种特征选择不稳定在中高共线数据里非常典型。根因L1 惩罚让两个高度相关、解释力几乎一样的特征存在等价竞争目标函数上留下的代价几乎无差因此惩罚路径对初始权重和数据扰动极度敏感。你得到的特征名单具有很大的随机性。排查思路先检查特征相关矩阵把相关系数超过 0.8 的簇先聚在一起然后对这组特征可以考虑先做主成分浓缩或者改用弹性网Alpha0.5跑一遍看选择是否稳定。更工程化的办法是跑多次自助重采样固定好训练集之后做 20 次有放回抽样每次跑lasso并记录保留的特征最后统计入选频率。频率超过 0.8 的才真正可靠频率低于 0.3 的干脆放弃。这个频率榜比一次性名单要可信得多。4.2 Lambda 序列全空、B 全零或全非零λ 范围的边界问题场景回放你直接调用lasso(X, y, CV, 5)跑完一看FitInfo.Lambda的第一个元素是InfB 的第一列全是零最后几列又几乎全非零——你以为该怎么选 λ却发现选出来的是极端模型。根因MATLAB 自动生成的 λ 序列通常从所有系数全零的最大惩罚开始所以第一列全零是故意的。但如果你的特征量级差异大到离谱——某个特征原始值是 10^5 量级另一个是 10^-3标准化后的解释力差异极大函数默认序列可能没有足够细地覆盖中间区域导致有效 λ 只有少数几个。此时你看到的不是均匀路径而是跳到极端结果。排查思路先检查特征方差分布把方差太小的列去掉再把明显离群的极值做 winsorize 截断比如用 1% 和 99% 分位数替换边界值。同时可以手动指定LambdaRatio让序列长度更合理或者显式传入一段自定义Lambda例如logspace(log10(0.001), log10(1), 100)。只要看到 CV 曲线是一段平滑的 U 型而不是单边悬崖λ 范围才是健康的。4.3 分类特征直接灌进去预测错误和高维灾难场景回放有个特征叫设备状态取值是正常/预警/故障直接当作数值特征 0/1/2 放进 X 跑 LASSO。结果选出来的特征含设备状态但系数为正、为负的解释很尴尬故障比正常高 2难道故障是对预测目标有利的线性关系吗根因无序分类变量被当成连续变量后编码顺序被赋予了虚假的数值意义。这既影响稀疏性也影响最终解释。而且如果一个分类特征有 20 个取值、转成 19 列哑变量后某几列可能被 LASSO 选中但你根本无法把它们解释成一个整体特征。正确解法对无序分类特征做哑变量编码并在结果解读时把同一源特征的所有哑变量合成一组看待。如果 LASSO 只选中其中一两个水平只能说该特征在特定水平下和量级上有差异而不是该特征整体重要。如果需要整体判断一个分类变量的重要性可以用一次全模型的 F 检验或者组变量选择方法这里先点到为止。4.4 先跑 LASSO 再划训练集信息泄漏的经典死法场景回放一位同行拿全量 500 条数据跑了一遍lasso选出 8 个特征然后随机挑 400 条做训练、100 条做测试测试 R² 高达 0.97上线后跌到 0.3。根因全量数据参与了特征选择相当于特征名单是用测试集答案偷看出来的。全量跑 LASSO 时cv 误差本身就是基于所有样本计算特征选择的目标函数已经见过测试样本因此测试集误差被系统性低估。这和调参时用测试集反复试是同一类问题。排查路径严格用训练集跑特征选择。你要是担心训练集太小导致选择不稳定可以用嵌套交叉验证外层划分训练/测试内层在训练集里再做折数更小的 CV 选 λ。MATLAB 里可以手动用两层cvpartition实现逻辑不复杂但能有效避免泄漏。跑完模型后无论如何都应该在从未参与特征选择的测试集上做最终评估。4.5 预测新数据时维度对不齐索引、标准化和截距的连环坑场景回放你在测试集上预测代码写成Xte * B(:, idxLambda) FitInfo.Intercept(idxLambda)结果报矩阵维度不一致于是把B改成全量却发现预测离谱。这种情况通常有三层坑。第一层idxLambda是列索引B(:, idxLambda)拿的是那一列系数X 必须是选中特征的子矩阵也就是Xte(:, idxSelected)不是全量 Xte。第二层如果特征选择后你重新用fitlm拟合预测要用predict(mdl, XteSel)不要和 LASSO 的系数混在一起。第三层FitInfo.Intercept是每个 λ 对应一个截距的行向量取的时候用FitInfo.Intercept(idxLambda)不要拿整个向量去加。排查技巧在写预测代码前打印三个尺寸size(B)、size(Xte)、numel(idxSelected)对一眼就知道哪里差列。逻辑无误后先用训练集的predict重算一遍和ytr对比确认数值级别正常再去测测试集。把预测代码封装成一个接受特征序号列表的小函数后续就不会反复踩维度坑。5. 可复用的特征选择流水线评价标准与后续姿势整个流程稳定跑通之后值得把你的做法固化成一个模板。这一节给一个较完整的代码骨架以及判断结果是否可靠的一串检查点。5.1 完整代码骨架从数据读入到结果存档function selResult lassoFeatureSelection(Xtr, ytr, Xte, yte, featureNames, opts) arguments Xtr double ytr double Xte double yte double featureNames string opts.Alpha (1,1) double 1 opts.CV (1,1) double 10 opts.UseParallel (1,1) logical false end if opts.UseParallel opt statset(UseParallel, true); else opt statset(UseParallel, false); end [B, FitInfo] lasso(Xtr, ytr, ... Alpha, opts.Alpha, ... CV, opts.CV, ... PredictorNames, cellstr(featureNames), ... Options, opt); idxLambda FitInfo.Index1SE; idxSelected find(B(:, idxLambda) ~ 0); XtrSel Xtr(:, idxSelected); mdl fitlm(XtrSel, ytr); pred predict(mdl, Xte(:, idxSelected)); rmse sqrt(mean((yte - pred).^2)); rsq 1 - sum((yte - pred).^2) / sum((yte - mean(yte)).^2); selResult.B B; selResult.FitInfo FitInfo; selResult.idxSelected idxSelected; selResult.selectedNames featureNames(idxSelected); selResult.model mdl; selResult.rmse rmse; selResult.rsq rsq; end这个骨架把选特征和第二阶段建模封装在一起输出结果里保存了FitInfo完整结构方便事后回查 λ 的决策依据。我把UseParallel做成可选项数据量稍大或 CV 折数多时可以并行加速但注意跑并行时随机种子没法完全复现必要的时候还是关掉并行。5.2 结果解读清单别只盯 RMSE 一个数字跑出结果以后按这串清单逐项检查每一项都能帮你发现隐藏问题。特征数量是不是符合业务预期如果 40 个候选特征选了 35 个说明稀疏化几乎没有发生很可能是 λ 取小了或特征之间纠缠不清。入选特征的系数符号是否符合业务常识如果温度越高目标越大是常识但符号相反可能是异常值或编码方向反了。交叉验证选的Lambda1SE和LambdaMinMSE对应的特征数量差别大不大如果差别非常大说明模型在精度和稀疏之间跳变剧烈稳定性存疑。测试集 RMSE 和训练集 RMSE 的差距是否在可接受范围超过一倍基本就是过拟合。重复跑同一次流程固定随机种子结果是否一致如果不一致问题出在 CV 划分稳定性或共线特征竞争上回到第 4.1 节处理。最后一个小建议存档时不要只存idxSelected和model把FitInfo也保存下来。过两个星期别人来问你为什么选这几个特征、当时 λ 选的多少、CV MSE 是多少你能从存档里把当时的判断依据翻出来这种可追溯性在工程协作中特别加分。5.3 下一步可以怎么扩展流程跑顺之后一般会有几个常见的升级需求。一是交互项与高次项的升维把原始特征做平方、交叉乘积后一起丢给 LASSO它会自动帮你从膨胀后的特征空间里挑重要合成特征相当于把非线性势也纳入特征选择。二是把Alpha放进一个外循环比如在 [0.2, 0.5, 0.8, 1] 之间比较 CV MSE选个更优的弹性网配置。三是和别的特征选择方法对照比如随机森林的predictorImportance或逐步回归交叉验证各自在测试集上的表现用来互相印证特征名单的合理性。我自己做项目时习惯至少要两种方法互相验证如果 LASSO 选出的名单和随机森林重要性排名高度重叠这个名单就非常可信了。这套流程跑顺之后LASSO 也就从公式变成了你工具箱里一个趁手的筛子。特征选择不是一步到位的自动挡它要你理解 λ 的含义、警惕共线性的不稳定、并坚持用训练集完成所有选择过程。把这些细节拿稳后再难的高维建模场景你至少能先拿到一张干净、可解释、经得起测试集回测的特征清单剩下的事就是拿这张清单结合业务去判断了。
阅读完成 · 觉得有帮助?