前阵子整理一批设备故障预测的案例,数据里有30多个监测变量,真正起作用的往往只有几个。最开始我用逐步回归去做特征筛选,结果选出来的变量每次训练都略有不同,解释起来很费劲。后来在Matlab里换了Lasso回归,一个命令跑完,带稀疏约束的系数解直接把重要特征挑出来了,那个清爽的系数向量看起来特别舒服。今天就在这篇里把我实际跑通的完整流程、参数选择逻辑和踩过的几个坑一起写出来,给同样在做特征筛选的朋友一个能直接上手的参考。
1. 为什么偏偏是Lasso:特征筛选到底难在哪
1.1 特征筛选的本质不是“选出来”,而是“敢舍弃”
做预测建模时,我们手里的特征往往比真正有用的多得多。这些多余特征大致分两类:一类是纯噪声,跟目标变量根本没有稳定关系;另一类是和有效特征高度相关的冗余变量。纯噪声容易理解,加进模型只会增加方差。冗余变量麻烦一点,单看每个变量跟目标都有相关性,但实际上它们提供的信息是重复的。
大多数人在这个阶段会选择先算相关系数矩阵,把相关性高的变量手动删掉一批,再用逐步回归继续筛。这个流程本身没有错,但有两个问题:第一,相关系数矩阵只捕捉线性关系,变量之间只要不是高度线性相关,它就看不出来;第二,手动删变量意味着你把自己的主观判断带进了筛选过程,一旦变量超过20个,人工判断的效率和一致性都会大幅下降。
Lasso回归解决的是另一个维度的问题:它在拟合目标的同时,对系数施加L1范数惩罚。这个惩罚的效果是迫使一部分系数被压缩到恰好等于0,而不是仅仅缩小。系数变成0,对应的特征就直接被模型“舍弃”了。换句话说,Lasso不是帮你选出特征,而是通过优化过程自动决定哪些特征不值得保留。
1.2 L1惩罚如何逼出稀疏解:一个直观但容易误解的点
Lasso的目标函数是下面这个形式:
code复制min (1/(2n)) * sum((y - Xβ)^2) + λ * sum(|β|)
前一项是常规的最小二乘损失,后一项是L1惩罚。λ是惩罚强度,越大代表我们越希望系数稀疏。很多人只知道“L1惩罚会产生稀疏解”,但说不清为什么。这里有个直观的理解方式:L1惩罚在系数空间中是一个菱形约束区域,最小二乘解落在这个菱形外面时,最优解很容易落在菱形的顶点上,而顶点正是某些系数为0的位置。L2惩罚(岭回归)对应的是圆形区域,圆滑的边界很难恰好切在坐标轴上,所以系数只会被压缩、不会被清零。
这个原理也解释了为什么Lasso在特征筛选场景下比岭回归更合适:岭回归把100个特征的系数都压缩到很小的值,但模型里仍然保留着100个特征;Lasso直接告诉你,这里面只有7个特征是值得用的。
1.3 Lasso与逐步回归、岭回归的取舍
实际应用中,我习惯把Lasso和另外两种方法放在一起对比,这样能更清楚它的定位:
| 方法 | 处理冗余特征 | 系数是否稀疏 | 稳定性 | 计算成本 |
|---|---|---|---|---|
| 逐步回归 | 能处理,但变量顺序敏感 | 是 | 较差,不同子集结果波动 | 低 |
| 岭回归 | 能压缩,但不删除 | 否 | 较好 | 低 |
| Lasso | 自动选择 | 是 | 较好 | 低到中 |
| 弹性网 | 兼顾分组选择 | 是,且成组 | 较好 | 中 |
逐步回归最大的问题在于变量进入和剔除的顺序会显著影响最终结果,在特征存在相关性的情况下尤其不稳定。岭回归则完全不做“取舍”,只做“压缩”,不满足特征筛选的最终目标。Lasso在两个维度之间找到了一个平衡点:既做了惩罚正则化,又输出稀疏系数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Matlab里跑通Lasso完整流程:从数据准备到系数输出
2.1 构造一份能说明问题的测试数据
为了把整个流程讲透,我先生成一份带“相关特征组”的模拟数据。这比随便用个内置数据集更接近真实场景,能清楚看到Lasso在特征冗余时的行为。
matlab复制% 固定随机种子,保证结果可复现
rng(2024);
n = 200; % 样本量
p = 15; % 特征数量
X = randn(n, p);
% 真实系数:只有第1、2、5、6、11个特征真正影响目标
beta_true = zeros(p, 1);
beta_true([1 2 5 6 11]) = [1.5; -2; 1.8; -0.6; 0.9];
% 人为构造相关性:第3个特征和第1个特征高度相关
% 第7个特征和第5个特征高度相关
x_corr1 = X(:, 1) + 0.15 * randn(n, 1);
x_corr2 = X(:, 5) + 0.1 * randn(n, 1);
X(:, 3) = x_corr1;
X(:, 7) = x_corr2;
% 目标变量:真实系数 + 噪声
y = X * beta_true + 0.8 * randn(n, 1);
这里的核心是让第3个特征和第1个特征、第7个特征和第5个特征几乎可以互相替代。这种情况下,普通回归会因为多重共线性导致系数估计不稳定,而Lasso则会倾向于从每个相关组里只选一个。
2.2 数据标准化:这一个步骤决定了惩罚的公不公平
特征筛选场景下,标准化是必须做对的一步。为什么?因为L1惩罚是对所有系数绝对值求和,这个求和是在所有特征之间“公平”地分配惩罚额度。如果某个特征本身的数值范围是0到10000,另一个是0到1,那么前者的系数天然会小很多,后者系数天然会大很多,惩罚对它们的约束力度完全不同。
matlab复制% 对特征做Z-score标准化
X_std = zscore(X);
% 目标变量也可以中心化,但不是必须
% y_centered = y - mean(y);
Matlab的lasso函数内部默认会自动对X做标准化,并在输出时把系数还原到原始尺度。但如果你需要在调用lasso之前自己观察数据、做可视化或者和其他函数搭配使用,最好还是手动标准化。一个容易被忽视的细节是:如果你手动标准化了X,后面做预测时也必须用训练集的均值和标准差去处理测试集,不能用测试集自己的均值标准差,否则相当于把测试集信息泄漏到了模型里。
2.3 调用lasso函数:基本用法和参数解读
matlab复制[B, FitInfo] = lasso(X_std, y, 'CV', 10);
这是最常用的一行调用。B是一个p × numLambda的矩阵,每一列对应一个λ值下的系数向量。随着λ增大,B中越来越多的系数会变成0。FitInfo是一个结构体,里面包含以下关键字段:
FitInfo.Lambda:所有尝试过的λ值序列,从大到小FitInfo.MSE:交叉验证的均方误差,长度与Lambda一致FitInfo.LambdaMinMSE:交叉验证均方误差最小时对应的λFitInfo.Lambda1SE:在最小均方误差一个标准差范围内、对应最多系数被压缩为0的λ,这是更偏稀疏选择的推荐值FitInfo.IndexMinMSE:最小MSE对应的列索引FitInfo.Index1SE:Lambda1SE对应的列索引
从Matlab R2012a开始就有lasso函数了,如果你的版本比较新,输出结构完全一致。我个人会特别看重Lambda1SE这个值,因为它代表的是“在统计意义上和最优模型没有显著差异,但系数更稀疏”的那个解。这一点在实际项目中非常有价值,因为它能帮你把特征数量降到最低,同时不损失预测精度。
2.4 查看系数路径:一张图看懂特征的重要性变化
系数路径图是Lasso分析里最有价值的一张图,它展示了每个特征系数随λ变化的轨迹。
matlab复制figure;
lassoPlot(B, FitInfo, 'PlotType', 'Lambda', 'XScale', 'log');
xlabel('Lambda');
ylabel('系数');
legend('show', 'Location', 'northeast');
grid on;
看这张图的时候,重点观察两点:第一,哪些特征的系数轨迹在较小的λ下就已经归零,说明它们相对不重要;第二,哪些特征的系数在很大的λ范围内都持续非零,说明它们是最稳定、最核心的特征。在实际案例里,我发现真正有用的特征通常会在对数尺度下横跨很大一段λ区间才归零,而噪声特征往往在λ稍微增大一点就被迅速清零。
2.5 挑选最优系数向量:用Lambda1SE还是LambdaMinMSE
在拿到FitInfo之后,最关键的一步就是决定用哪一列系数作为最终结果。
matlab复制% 选择Lambda1SE对应的系数
idx = FitInfo.Index1SE;
coef_selected = B(:, idx);
% 查看非零系数的特征编号
selected_features = find(abs(coef_selected) > 0);
disp('选中的特征编号:');
disp(selected_features');
我在这份模拟数据上跑完的结果,选中的特征通常是第1、2、5、6、11个,偶尔会从相关组第1、3个中选出一个,从第5、7个中选出一个。这正是Lasso的典型行为:它很少同时保留两个高相关的特征。
用LambdaMinMSE会得到更多的非零特征,用Lambda1SE则更精简。我的经验是:如果目标是解释和洞察业务,优先用Lambda1SE;如果目标是最大化预测精度且特征数量本身不是约束,可以考虑LambdaMinMSE,同时在测试集上验证两者差异。
3. Lambda的选择:从系数路径到交叉验证的完整逻辑
3.1 交叉验证在Lasso里到底做了什么
lasso(X, y, 'CV', 10)里的CV, 10表示进行10折交叉验证。Matlab会把数据随机分成10份,轮流拿出1份作为验证集,用剩下9份训练模型,然后计算验证集上的预测误差。这个过程会对每个λ都做一遍,最终汇总出每个λ对应的平均均方误差FitInfo.MSE。
这里有个容易被忽略的点:交叉验证的划分是随机的。如果不固定随机种子,同一份数据每次运行得到的结果会有轻微差异,λ的取值也会略有浮动。解决方法是调用前使用rng(2024)一类的固定种子。我习惯把随机种子直接写在代码最上方,这样既能保证自己复现,也能让同事跑出来的结果和我的完全一致。
3.2 为什么LambdaMinMSE不是最优选择
很多第一次用Lasso的人,看到LambdaMinMSE就认为这是最优λ,直接拿来用。这个思路在数学上成立,但在工程实践上通常不是最好的选择。
原因在于交叉验证的MSE估计本身有方差。最小的MSE点对应的模型,可能只是在这个特定划分下碰巧表现好,换一组数据就不一定了。Lambda1SE的计算逻辑是:找到最小MSE,然后在其一个标准误范围内,选择λ最大的那个值。这样选出的模型在统计意义上与最优模型没有显著差异,但特征更少、模型更简洁、对新数据的泛化能力往往更好。
在之前的模拟数据上,LambdaMinMSE可能保留8到9个特征,其中包含一些系数很小的噪声特征;而Lambda1SE通常只保留5到6个核心特征。两者的测试集预测误差几乎一样,但前者多出来的特征会给业务解释带来额外负担。
3.3 手动设置lambda范围的场景
在少数情况下,你不想用Matlab默认的λ序列。比如你已经通过经验知道惩罚强度应该在什么范围,或者你只想测试某个特定的λ。
matlab复制lambda_seq = logspace(-3, 1, 100); % 从0.001到10,对数均匀取100个点
[B_custom, FitInfo_custom] = lasso(X_std, y, 'Lambda', lambda_seq);
这时FitInfo_custom.MSE会是全空的,因为没有做交叉验证。你可以自己实现验证逻辑,或者配合外部交叉验证循环来评估。这个用法在正式建模阶段不常用,更多出现在算法对比实验里。
我个人的建议是:先跑一次默认的lasso看一下FitInfo.Lambda的范围,再根据实际需要决定要不要缩小或扩大搜索区间。默认值的覆盖范围通常已经足够,手动指定的情况并不多。
3.4 重跑一遍的稳定性检查
特征筛选最忌“一次性结果”。我建议在固定随机种子的情况下至少跑3到5次交叉验证,记录每次选中的特征组合。如果某些特征在多次运行中都被选中,说明它们是稳定的核心特征;如果某个特征时有时无,说明它处于边界位置,需要谨慎对待。
matlab复制stable_count = zeros(p, 1);
for rep = 1:5
rng(100 + rep);
[B_rep, FitInfo_rep] = lasso(X_std, y, 'CV', 10);
idx_rep = FitInfo_rep.Index1SE;
coef_rep = B_rep(:, idx_rep);
stable_count = stable_count + (abs(coef_rep) > 0);
end
disp('各特征被选中的次数(共5次):');
disp(stable_count');
这个循环看起来简单,但在实际项目中价值很大。它能帮你把“碰巧被选中”的特征和“真正稳定有效”的特征区分开,尤其是当特征间存在相关组时,这种稳定性检查至关重要。
4. 实际应用中踩过的坑:从能跑变成能用
4.1 相关特征组的“断舍离”反直觉现象
我在真实项目里遇到最多的情况是:两个特征从物理意义上说都应该有影响,但Lasso只保留了其中一个。这并不意味着另一个不重要,而是因为它们提供的信息高度重叠,Lasso认为保留一个就够了。
这一点在业务沟通时特别容易引起质疑。业务方可能会问:“为什么传感器A被保留了,同样功能的传感器B却被剔除了?”这时候如果不懂Lasso在相关组内的选择机制,很难给出合理回答。
应对策略有两个层面。第一,在技术层面接受这种“稀疏化”的代价,只要预测性能不受影响,保留一个完全足够。第二,在业务层面,对选出的特征做一次额外的相关性分析,看哪些被剔除的特征和保留特征高度相关,形成一份“特征替代关系说明”,这样跟业务方沟通就顺畅得多。
4.2 全零解的成因与应对
当λ设置得过大时,所有系数都会被压缩为0,此时B的某一列全是0。这种情况在实际项目中并不罕见,尤其是当特征数量远大于样本量、但真实有效特征又很少时。
出现全零解时,第一反应不应该是调小λ,而是思考数据本身是否真的存在可预测的信号。如果加了很多特征,但目标变量基本是随机噪声,Lasso无论怎么调λ都无法筛出有效特征,反而可能过拟合到噪声上。
如果确认数据有信号但全零解频繁出现,可以尝试以下顺序排查:
- 检查X和y是否标准化正确,尤其是X是否包含了量纲差异极大的特征
- 降低λ最大值,让搜索范围更贴近合理区间
- 改用弹性网(在Matlab中可通过
lasso的Alpha参数设置为0到1之间的值),它在处理强相关特征组时比纯Lasso更稳定
4.3 特征标准化泄漏:一个新手常犯但后果严重的错误
前面提到过,如果你手动做标准化,必须用训练集的统计量去处理测试集。这个错误的后果是:测试集的信息在训练阶段就被“看”到了,评估结果会偏乐观,上线后模型效果大幅下滑。
正确写法是这样的:
matlab复制% 划分训练集和测试集
rng(2024);
cv = cvpartition(size(X, 1), 'HoldOut', 0.3);
train_idx = training(cv);
test_idx = test(cv);
XTrain = X(train_idx, :);
yTrain = y(train_idx);
XTest = X(test_idx, :);
yTest = y(test_idx);
% 只在训练集上计算均值和标准差
mu = mean(XTrain);
sigma = std(XTrain);
% 用训练集的统计量标准化训练集和测试集
XTrain_std = (XTrain - mu) ./ sigma;
XTest_std = (XTest - mu) ./ sigma;
标准化泄漏这个问题在Lasso场景里尤其危险,因为Lasso对特征尺度敏感。一旦测试集用了自己算出的均值和标准差,相当于隐式地往模型里塞了测试集信息。我见过不止一次因为这个问题,上线后效果比验证时差一大截的情况。
4.4 中文注释乱码与旧代码运行问题
如果你用的是Matlab 2023或更高版本,打开别人传过来的旧版.m文件时,中文注释经常显示成乱码。这通常是文件编码问题,旧版Matlab默认用GBK保存文件,新版默认切换到UTF-8。遇到这种情况,直接在编辑器里用“打开”功能选择对应编码方式重新打开,再另存为UTF-8即可。
这个看起来和Lasso无关,但实际处理旧项目代码时非常影响效率。我自己的习惯是:所有脚本里只用英文注释,或者统一在文件开头注明编码,避免在不同版本之间来回踩坑。
4.5 数据量太小的时候,Lasso会失灵
Lasso虽然是正则化方法,但在极端小样本场景下(比如n=30,p=50),它的筛选结果依然具有很大的随机性。这时候交叉验证折数的设置会显著影响结果,10折交叉验证意味着每折只有3个验证样本,MSE估计方差极大。
如果样本量确实很小,我建议改用留一交叉验证(LOOCV),或者使用重复交叉验证(比如5次10折)取平均。Matlab里可以通过循环重复调用lasso并汇总结果来实现,虽然计算量大一些,但结果更稳健。
5. 从Lasso出发:进阶玩法和落地思路
5.1 弹性网:当Lasso遇到强相关特征组时的补强
Lasso在强相关特征组中通常只随机选一个,这在某些业务场景下并不理想。比如在基因表达数据分析中,一组基因通常会共同影响某个性状,你希望同时保留整组而不是只选一个代表。
弹性网通过混合L1和L2惩罚来缓解这个问题:
code复制min (1/(2n)) * sum((y - Xβ)^2) + λ * (α * sum(|β|) + (1-α)/2 * sum(β^2))
在Matlab里,lasso函数的Alpha参数默认是1,也就是纯Lasso。把它改成0.5,就变成了弹性网:
matlab复制[B_enet, FitInfo_enet] = lasso(X_std, y, 'CV', 10, 'Alpha', 0.5);
Alpha越接近0,越像岭回归,系数压缩但不会清零;Alpha适中时,既能保留Lasso的稀疏性,又能对相关特征组施加一定程度的“分组效应”。我个人的习惯是:当明确知道数据中存在高度相关的特征组,且业务上希望保留整组特征时,会用Alpha=0.5的弹性网做一次对比,再决定最终用哪种方案。
5.2 两阶段筛选:Lasso做初筛,精细模型做终选
Lasso输出的稀疏特征集虽然已经很精炼,但在某些场景下,我还会做一步“二次确认”。具体做法是:先用Lasso筛出候选特征集,然后在这个小特征集上用随机森林或带交叉验证的逐步回归再做一次筛选,作为稳健性验证。
这一步不是多余的。Lasso的筛选结果受λ选择方式影响较大,而随机森林能提供另一种视角的特征重要性排序。如果某个特征同时被Lasso和随机森林列为重要特征,那几乎可以确定它是核心特征;如果两个方法给出的结果矛盾,则需要谨慎处理,重新检查数据质量或在业务层面找原因。
5.3 把Lasso筛选结果沉淀为可复用的工程模块
在经历了多次“重写Lasso流程”之后,我把自己常用的步骤封装成了一个函数,输入训练数据,输出选中的特征编号、系数、以及一张系数路径图。这样一来,新的数据集到了手上,只需一行调用就能完成特征筛选,不用每次重头写。
matlab复制function [selected_features, coef, B, FitInfo] = lasso_feature_select(X, y, varargin)
% LASSO_FEATURE_SELECT 使用Lasso进行特征筛选
% 输入:
% X: n×p特征矩阵
% y: n×1目标变量
% varargin: 可选的参数对,如 'CV', 10, 'Alpha', 1
% 输出:
% selected_features: 被选中特征的编号列向量
% coef: 对应特征的系数值
% B, FitInfo: lasso函数的完整输出
% 随机种子参数,默认2024
p = inputParser;
addParameter(p, 'Seed', 2024);
addParameter(p, 'CV', 10);
addParameter(p, 'Alpha', 1);
parse(p, varargin{:});
rng(p.Results.Seed);
% 标准化
X_std = zscore(X);
% Lasso拟合
[B, FitInfo] = lasso(X_std, y, ...
'CV', p.Results.CV, ...
'Alpha', p.Results.Alpha);
% 选取Lambda1SE对应的系数
idx = FitInfo.Index1SE;
coef_full = B(:, idx);
% 提取非零系数
selected_features = find(abs(coef_full) > 0);
coef = coef_full(selected_features);
% 绘制系数路径图
figure;
lassoPlot(B, FitInfo, 'PlotType', 'Lambda', 'XScale', 'log');
grid on;
end
封装成函数之后,代码复用率提高了不少,出错的概率也下降了。特别是我在多个项目中来回切换时,不需要反复回忆“上次那个标准化步骤到底放在哪了”。
5.4 落地时要关注的一个细节:系数符号的物理意义
Lasso筛出的特征,不仅要看系数是否非零,还要看系数的符号是否符合常识。比如在设备故障预测中,如果某个温度特征的系数是负的,意味着该特征值越大,故障风险越低。如果这个符号和物理直觉不符,通常意味着存在隐藏的混淆因素,或者特征定义与直觉相反。
这个检查步骤在纯机器学习流程中经常被忽略,但在工程落地时却至关重要。模型上线后,业务方一定会拿实际案例来问“为什么这个变量上升,预测结果反而降低”,如果此时你才发现系数符号和业务直觉冲突,再去排查原因就被动了。我自己在每次Lasso筛选完成后,都会把非零系数和业务方一起过一遍,确认每个系数的符号都有合理解释。
这套流程跑下来,从数据准备到最终特征落地,整体思路就是:用L1惩罚强制模型做取舍,用交叉验证选择合理的惩罚力度,用稳定性检查确认特征的可靠性,最后结合业务常识验证系数符号的合理性。实际应用时,不需要把它想得太复杂,先把基础的lasso(X, y, 'CV', 10)跑通,再看系数路径、选Lambda1SE、做稳定性检查,逐步深入即可。如果遇到特征之间高度相关、筛选结果不稳定这类问题,再用弹性网、重复交叉验证和两阶段筛选去兜底,基本上就能覆盖绝大多数特征筛选场景了。
