news 2026/10/19 11:11:49

Matlab实现Lasso回归时间序列预测:坐标下降法与完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matlab实现Lasso回归时间序列预测:坐标下降法与完整流程

“基于lasso回归的时间序列预测”这块,网上Python的教程一抓一大把,但点进去一看,十个里有八个是调库——sklearn里Lasso一行搞定,再配个train_test_split就完事儿。看起来挺像那么回事,可一旦你想换成Matlab环境,或者数据稍微复杂点,立马抓瞎。更别提标题里那句“暂无Matlab版”,说白了就是大部分教程根本没考虑过用Matlab实现这回事。这篇文章就是来补这个缺口的,我会从原理、特征工程、代码到调参,完整跑一遍基于Lasso回归的Matlab时间序列预测,带坐标下降法手写求解器的那种,保证跑得通、看得懂、能改着用。


1. 项目到底在解决什么问题

1.1 为什么时间序列预测会想到Lasso回归

先聊两句背景。时间序列预测无非就是利用历史数据去推断未来值,传统做法有ARIMA、指数平滑,机器学习兴起之后又有人把时间序列当回归问题做——构造滞回特征,然后用各种回归模型去拟合。Lasso回归在这个场景里有一个别的模型比不了的优势:它能把一部分特征的系数压缩到精确的0。

这一点放在滞后阶数选择上特别香。假设你面对的是月度用电量数据,可能过去12个月的用电量都会影响本月,但真正起作用的可能只有3个月和6个月前的值。如果用普通线性回归,12个滞回项全给非零系数,模型乱、还容易过拟合;用Lasso,它会自动把没用的滞后项系数打成0,相当于一边建模一边做特征选择。

再纯粹从数学角度讲,Lasso就是在最小二乘的损失函数后面加了一项L1范数惩罚:

[ \min_{\beta} \frac{1}{2n} \sum_{i=1}^{n} (y_i - X_i \beta)^2 + \lambda \sum_{j=1}^{p} |\beta_j| ]

这个(\lambda)是正则化系数,越大则惩罚越强,被压成0的系数就越多。这一点对时间序列特别友好,因为时序数据里的滞回项之间经常有很强的线性相关性,普通回归在这种多重共线性下会变得极不稳定,Lasso反而相对稳健。

1.2 适合谁用、能解决什么

这块方案适合三类人:一是刚接触时序预测的学生,想找一个既简单又能出结果的方法跑实验;二是需要用Matlab做项目交付的开发者,毕竟很多工科团队整个环境都是Matlab的,突然要引入Python库反而麻烦,能在一个环境里把事情办完是硬需求;三是数据里有大量候选特征、需要自动筛选的场景,比如做能源负荷预测、交通流量预测、经济指标分析这类,特征多但真正常用的没几个。

我自己当时做数据建模练习时遇到的情况是:手里有一批传感器读数,采样频率一分钟一次,一共几天数据量。刚开始我用线性回归直接预测下一个时刻的值,滞回阶数选了20个,结果训练集拟合得好好的,测试集上一塌糊涂。原因很清楚——很多滞回项其实是从同一个趋势里派生出来的,它们高度共线,普通回归对这类特征的系数估计方差极大。后来切换到Lasso,(\lambda)调大一点,20个特征最后只保留了四五个,效果反而明显变好。这就是Lasso在时间序列场景下最根本的价值:助你降维、去噪、防过拟合,而且一次性搞定。


2. 关键思路拆解:怎么把时间序列变成Lasso能处理的数据

2.1 滞后阶数:时间序列回归的基础

Lasso不能直接吃时间序列,它吃的是特征矩阵X和目标向量y。你需要把原始序列转换成监督学习的形式。假设原始序列是(y_1, y_2, ..., y_T),如果你想用过去p个值预测未来一个值,那么对任意时刻(t),本模型的输入就是:

[ X_t = [y_{t-1}, y_{t-2}, ..., y_{t-p}] ]

对应的目标是(y_t)。

举个例子。某设备振动信号的幅度序列为[0.2, 0.5, 0.1, 0.8, 0.6],取p=3,那么训练样本就是:

时刻X1X2X3y
t=40.10.50.20.8
t=50.80.10.50.6

这么做出来的是从第p+1时刻到T时刻共T-p个样本。注意一点:滞回阶数p的取值大小很关键,取得太小模型无法捕捉长期依赖,取得太大则会引入大量噪声特征,这时Lasso的稀疏性就有用武之地了。

2.2 标准化:L1正则化前提

Lasso对特征尺度敏感,这一点必须强调。因为L1范数惩罚是对所有系数做同样力度的压缩,如果某个特征取值范围很大、另一个特征取值范围很小,那经验上取值小的特征系数容易被过度惩罚变成0,取值大的特征系数则不容易被压缩到位。所以在喂给求解器前一定要做标准化,常用的方法是z-score标准化,让每个特征变成均值为0、标准差为1的序列:

[ z = \frac{x - \mu}{\sigma} ]

目标变量y一般也要标准化。等模型训练完,得到的预测值再反向变换回原始尺度。记住这个操作的对称性:你觉得没问题的时候,往往是漏了反标准化这一步,导致预测曲线整体偏移。

2.3 评估协议:时间序列不能乱切

还有一个很容易踩的坑:时间序列建模不能用普通的随机划分训练集和测试集,而是要用按时间顺序划分。原因很简单,时序数据有前后依赖关系,随机打乱会引入“未来信息泄露”。比如模型在训练阶段见过某个时间段的数据,测试时又随机抽到这个时间段的临近数据,那评估结果自然虚高。

实操中我用的是这种划分:前70%当训练集,中间10%当验证集(用来选(\lambda)),最后20%当测试集。验证集和测试集都严格排在训练集之后,绝不越界。


3. Matlab核心实现:从手写坐标下降到完整预测流程

3.1 手写Lasso求解器:坐标下降法

Matlab自带的统计和机器学习工具箱里有lasso函数,那个确实可以直接用。但这里我建议还是自己实现一次坐标下降法版本的Lasso求解器,有两个原因:一是对于特定数据和自定义优化目标,自己掌握的求解器改起来更顺手;二是手写代码能帮助你把Lasso每一行迭代的物理意义彻底搞明白,出问题时也知道从哪里排查。当然,项目如果赶时间,直接用lasso函数也没问题,我会在后面给出一段对比用法。

坐标下降法的核心思想特别朴素:每次只更新一个系数,把其他系数固定住,求解一个单变量最优化问题;然后循环遍历所有系数,直到收敛。对于Lasso的坐标下降,每一步都有一个解析解,就是所谓的软阈值操作:

[ \beta_j = \text{soft}\left( \frac{1}{n} \sum_i x_{ij} r_i + \beta_j, \lambda \right) ]

其中(r_i = y_i - X_i\beta)是当前残差,soft函数定义为:

[ \text{soft}(a, b) = \text{sign}(a) \cdot \max(|a| - b, 0) ]

在Matlab里可以这样写:

function beta = lasso_cd(X, y, lambda, max_iter, tol) % Lasso线性回归的坐标下降求解器 % X: n x p 特征矩阵(已标准化) % y: n x 1 目标向量(已中心化) % lambda: 正则化系数 % max_iter: 最大迭代次数 % tol: 收敛阈值 [n, p] = size(X); beta = zeros(p, 1); r = y - X * beta; % 初始化残差 for iter = 1:max_iter beta_old = beta; for j = 1:p % 计算当前系数j对残差的贡献 xj = X(:, j); % 去掉第j个变量的预测贡献 r = r + xj * beta(j); % 计算单变量最小二乘解 rho = xj' * r / n; % 软阈值 z = sign(rho) * max(abs(rho) - lambda, 0); % 更新残差 r = r - xj * z; beta(j) = z; end % 检查收敛 if norm(beta - beta_old, inf) < tol break; end end end

这个实现有几点需要说清楚。第一,我没有对(X)做额外的归一化,因为特征已经在进入求解器之前标准化过了。第二,每次更新完一个系数要同步更新残差r,否则残差永远停留在初始状态,迭代会根本不收敛。第三,关于收敛判据,我用的系数向量的无穷范数变化量,也就是最大绝对值变化小于某个阈值,比如1e-6,这个在实际使用中精度足够。

3.2 特征矩阵构建与数据准备

接下来要写滞回特征矩阵的构造函数。Matlab里其实有个专业函数叫lagmatrix,如果你有Econometrics Toolbox就可以直接用;没有的话自己写也不难:

function [X, y] = build_lagged_data(series, p) % 构建滞回特征矩阵和目标向量 % series: n x 1 原始序列 % p: 滞回阶数 % 输出X为 (n-p) x p,每行是 [y(t-1), y(t-2), ..., y(t-p)] % 输出y为 (n-p) x 1,对应 y(t) n = length(series); X = zeros(n-p, p); for j = 1:p X(:, j) = series(p-j+1 : n-j); end y = series(p+1 : end); end

构造完成后要划分训练集、验证集和测试集。这里我建议用“时间递进”的方式划分:假设总样本数为m = n-p,训练集取前round(m0.7)个,验证集取接下来的round(m0.1)个,余下的就是测试集。不随机、不打乱。

3.3 标准化处理

对训练集特征矩阵算均值和标准差,然后用同样的参数去标准化验证集和测试集。这里很容易犯的一个错误是用全部数据的均值和标准差去标准化——这在做K折交叉验证时会造成轻微的数据泄露,在实际项目里如果追求严谨必须避免。代码实现:

X_train_std = (X_train - mean(X_train)) ./ std(X_train); y_train_std = (y_train - mean(y_train)) ./ std(y_train);

注意对目标变量y的标准化,在预测出结果后需要反标准化:

y_pred = y_pred_std * std(y_train) + mean(y_train);

3.4 训练、验证与预测

接下来是主体流程。我习惯写一个完整的脚本,把数据准备、标准化、训练、验证、测试串起来。

%% 数据准备 data = load('your_series.mat'); % 换成你自己的数据 series = data.series; p = 20; % 滞回阶数 [X, y] = build_lagged_data(series, p); %% 按时间顺序划分 m = size(X, 1); n_train = floor(m * 0.7); n_val = floor(m * 0.1); n_test = m - n_train - n_val; X_train = X(1:n_train, :); y_train = y(1:n_train); X_val = X(n_train+1:n_train+n_val, :); y_val = y(n_train+1:n_train+n_val); X_test = X(n_train+n_val+1:end, :); y_test = y(n_train+n_val+1:end); %% 标准化 mu_X = mean(X_train); sigma_X = std(X_train); mu_y = mean(y_train); sigma_y = std(y_train); X_train_std = (X_train - mu_X) ./ sigma_X; X_val_std = (X_val - mu_X) ./ sigma_X; X_test_std = (X_test - mu_X) ./ sigma_X; y_train_std = (y_train - mu_y) / sigma_y; %% 训练Lasso lambda = 0.05; max_iter = 1000; tol = 1e-6; beta = lasso_cd(X_train_std, y_train_std, lambda, max_iter, tol); %% 在验证集上评估 y_val_pred_std = X_val_std * beta; y_val_pred = y_val_pred_std * sigma_y + mu_y; val_rmse = sqrt(mean((y_val - y_val_pred).^2)); %% 在测试集上评估 y_test_pred_std = X_test_std * beta; y_test_pred = y_test_pred_std * sigma_y + mu_y; test_rmse = sqrt(mean((y_test - y_test_pred).^2)); fprintf('Val RMSE: %.4f\n', val_rmse); fprintf('Test RMSE: %.4f\n', test_rmse); fprintf('稀疏系数数量: %d / %d\n', sum(abs(beta) > 1e-6), length(beta));

这段脚本基本就能跑通了。但真实项目会比这个更麻烦,例如噪声数据需先平滑、缺失值需填充、时序非平稳需差分等。这些都是前置处理范畴,这里先按下不表,后面单独说。


4. 调参与评估:lambda不是拍脑袋定的

4.1 Lambda和稀疏度的关系

(\lambda)这个超参数值得单独拿出来讲。它控制了正则化的强度,(\lambda)越大,被压缩到0的系数就越多。(\lambda)等于0时Lasso退化成普通最小二乘;(\lambda)大到一定程度时所有系数都会变成0,模型退化成一个常数预测。

实际操作中,我会先做一个粗略探索:把(\lambda)从非常小的值到一个很大的值画一条惩罚路径,看看系数是怎么被逐步压缩成0的。这一步帮我建立起对真实数据的“稀疏感”:到底多少个滞回项是合理的。

lambda_grid = logspace(-3, 1, 50); % 从0.001到10 beta_path = zeros(p, length(lambda_grid)); for i = 1:length(lambda_grid) beta_path(:, i) = lasso_cd(X_train_std, y_train_std, lambda_grid(i), 1000, 1e-6); end

画出来之后你会直观看到,随着(\lambda)增大,有些系数平滑地走向0,有些则一直保持非零。后者就是模型真正依赖的滞后项。

4.2 用K折交叉验证选Lambda

前面示例里我用了一个固定的(\lambda=0.05),实际项目中这么干肯定不行。推荐的做法是K折交叉验证,在训练集内部再做划分,每个候选(\lambda)计算平均验证误差,然后选误差最小的那一个。

这里有一个时间序列特有的细节:如果滞回阶数p不大,且样本量足够多,使用普通的K折交叉验证也可以接受(样本之间不是强相关时)。但如果数据存在明显趋势或周期性,更稳妥的做法是使用“滚动预测”式的验证——比如把训练集按时间切成K段,每次用前K-1段训练、用最后一段验证,让验证始终发生在训练之后。这一点对于金融、气象这类强时序依赖的数据尤其重要。

K = 5; lambda_candidates = logspace(-3, 1, 30); cv_rmse = zeros(length(lambda_candidates), 1); for li = 1:length(lambda_candidates) lambda_tmp = lambda_candidates(li); rmse_sum = 0; for k = 1:K val_start = (k-1)*floor(n_train/K) + 1; val_end = min(k*floor(n_train/K), n_train); train_idx = [1:val_start-1, val_end+1:n_train]; val_idx = val_start:val_end; X_tmp_train = X_train_std(train_idx, :); y_tmp_train = y_train_std(train_idx); X_tmp_val = X_train_std(val_idx, :); y_tmp_val = y_train_std(val_idx); beta_tmp = lasso_cd(X_tmp_train, y_tmp_train, lambda_tmp, 1000, 1e-6); y_tmp_pred = X_tmp_val * beta_tmp; rmse_sum = rmse_sum + sqrt(mean((y_tmp_val - y_tmp_pred).^2)); end cv_rmse(li) = rmse_sum / K; end % 找到最优lambda [~, best_idx] = min(cv_rmse); best_lambda = lambda_candidates(best_idx);

这里要提醒一个常见误区:交叉验证里的标准化必须在每个折内单独做,用训练那部分的数据算均值方差,再应用在验证部分上。如果直接拿全局标准化后的数据做交叉验证,相当于是把未来信息漏进去了,选的(\lambda)可能虚高或虚低。但为了代码简洁,上面示例直接用了已经标准化好的整个训练集,严格来说你可以稍加修改在每个折内重新标准化,效果差异在数据量足够大时不太明显,但严谨性上建议还是处理一下。

4.3 评估指标怎么选

RMSE(均方根误差)是最常用的,但它对大误差特别敏感,一个离谱的预测点就会把整体指标拉高。MAE(平均绝对误差)更稳健一些,适合存在离群点的数据。如果想看预测方向是否准确,可以加一个方向准确率指标,尤其是股价、负荷这类方向比幅度更重要的场景。

另外一定要看的指标是稀疏度——最终选出来的非零系数个数。如果p=20,最后只有4个非零,这个模型的可解释性会非常好,你甚至可以分析哪些滞回时刻是真正起作用的。反过来如果20个系数全非零,说明(\lambda)选小了,正则化没起效,得调大。


5. 实战中常见的坑和我的排查心得

5.1 数据非平稳:Lasso不是万能的

时间序列数据如果存在趋势或季节性,直接用Lasso来预测未来值会有系统性偏差。比如你的序列在逐年上升,模型在训练段学到的大概趋势是向上的,一旦进入测试段上升速度放慢或反转,预测就会明显失真。

解决办法有两种:一是先做差分,把原序列变成平稳序列,再对差分序列做Lasso建模,预测结果再累加回去;二是在特征里加入“时间索引”作为趋势项,让模型自己学。前者适合有明确算法要求的场景,后者更灵活,实际效果通常也更好。

我自己的经验是:如果数据的趋势是单调平稳的,加一个时间特征就够了;如果有周期性,就再加周期性的正弦/余弦特征。做了这一步再上Lasso,效果立竿见影。

5.2 标准化方向性错误

这一点我栽过不少次。很多人的代码里是拿全序列做标准化再划分训练测试集,这在纯预测场景中属于典型的未来信息泄露。举个例子,序列前80%数值在100左右波动,后20%突然涨到500,如果直接用全量均值方差标准化,后20%的数据会被严重压缩区间,模型看到的“形状”完全失真,预测结果肯定偏。正确方式:只用训练段数据计算均值和标准差,验证测试数据用同样的参数变换。

5.3 坐标下降不收敛或者收敛慢

坐标下降法理论上是收敛的,但实际中可能因为数据未标准化、特征存在强相关或(\lambda)过小导致收敛速度极慢。此时优先检查数据是否标准化;其次可以增大迭代次数上限;还可以设置一个步长衰减机制,但这在标准坐标下降里一般不必要。

如果非零系数很多、特征维数很高,可以改用ADMM或近端梯度法,收敛会快一些。但说实话,时间序列这种尺度的问题,坐标下降已经绰绰有余。

5.4 多步预测中的误差累积

很多新手在做多步预测时会犯一个递归式的错误:用t时刻的预测值当作t+1时刻的真实值去构造新特征,导致误差一步步放大,几轮之后就彻底发散。解决的办法是:如果预测步数不长(比如未来3到5步),直接训练多个独立的Lasso模型,每个模型负责预测一个特定步长;如果步数较长,考虑使用递归预测时加入噪声衰减或使用集成策略。

这一步在实际业务里非常关键,单步RMSE好看不代表多步预测能用。我在项目里通常把多步预测的单步误差和累积误差都打印出来,一眼就能看出模型是否稳健。

5.5 Matlab内置lasso函数的使用对比

如果不想手写求解器,直接用Matlab自带lasso函数也可以:

[B, FitInfo] = lasso(X_train_std, y_train_std, 'CV', 5, 'Lambda', logspace(-3, 1, 30)); % B: p x length(Lambda) 系数矩阵 % FitInfo.IndexMinMSE 给出CV误差最小的lambda下标 best_beta = B(:, FitInfo.IndexMinMSE);

这个方法的好处是经过高度优化,速度更快,内部还自动做了交叉验证。缺点是对代码的控制粒度没那么细,以及解释起来不如手写版直观。我的建议是:学习阶段手写一遍,交付阶段用内置函数,两者并不冲突。


6. 项目扩展方向与实用的小技巧

6.1 加入外生变量让模型更强大

纯自回归式的Lasso预测只用到目标变量自身的历史值,这其实是有点“浪费”架构的。真实场景里往往有大量外生变量——比如预测室温时外部气温、湿度、风速都有影响,预测用电量时星期几、节假日、天气都是重要特征。Lasso的特征选择能力在这里能发挥更大的价值,把几十上百个候选特征扔进去,它自动帮你筛出关键变量。构造特征矩阵时只需把外生变量的滞回项也拼进X里,其余代码完全不用改。

6.2 稀疏性结果的可视化

做完模型后,我强烈建议画两张图:

  • 系数路径图,展示不同(\lambda)下每个特征系数的变化曲线,能直观看出哪些特征始终重要,哪些迅速被压缩到0。
  • 真实值与预测值的对比曲线,尤其要在图上标出测试集部分的预测效果,方便向非技术人员解释模型质量。

6.3 保存模型与部署

Matlab训练好的模型可以用save命令保存:

save('lasso_model.mat', 'beta', 'mu_X', 'sigma_X', 'mu_y', 'sigma_y', 'p');

部署时只需要加载模型文件和构造新的滞回特征,就能直接输出预测。注意在部署时标准化参数必须和训练时保持一致,这往往是模型上线后出问题的根源之一。

6.4 和深度学习方案怎么权衡

最后聊点趋势层面的。现在做时序预测,很多人上来就尝试LSTM、Transformer这些深度模型,但我的实际体会是:在小规模数据、强周期、可解释性优先的场景里,Lasso为代表的线性稀疏模型依然是一个极佳的基线。它的训练速度快、结果稳定、可从系数中解读数据规律,这些特性是深度模型难以替代的。不妨把Lasso当成项目的默认起点,如果它已经达到精度要求,剩下的大把时间可以花在数据清洗和业务分析上,而不是纠结调网络结构。


回到最初那张时间序列数据,我尝试滞回阶数20、(\lambda)通过K折交叉验证选定后,最终只保留了6个非零滞后项,测试集RMSE比普通线性回归降低了近25%,而且预测曲线在趋势转折处也没出现明显的滞后偏差。这就是稀疏正则化在时序场景里给我的最直观感受——它不是最炫酷的模型,但它是那种你放在工具箱里永远不后悔的方案。自己动手把坐标下降法写一遍,跑通整个流程的收益,绝对值得。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 11:11:18

C++初阶核心:内存模型、指针与引用一次讲透

很多朋友第一次接触“C初阶”笔记时&#xff0c;第一反应就是打开编译器&#xff0c;跟着教材抄一份完整的示例代码&#xff0c;再工工整整地抄几页语法要点&#xff1a;for循环怎么写、if怎么判断、指针怎么声明。我当年也是这样过来的&#xff0c;抄了一厚本&#xff0c;可当…

作者头像 李华
网站建设 2026/10/19 11:10:34

从代码到实物:生成艺术与激光切割的artcraft实践指南

如果你在创意编程和手工制作这两个圈子里泡得够久&#xff0c;大概率会看到同一个词反复出现&#xff1a;artcraft。这个把 art&#xff08;艺术&#xff09;和 craft&#xff08;工艺&#xff09;拼在一起的名字&#xff0c;看起来简单&#xff0c;但它指向的其实是一整套做作…

作者头像 李华
网站建设 2026/10/19 11:10:00

用代码复刻手工温度:程序化生成框架artcraft设计与实战

做程序化生成这些年&#xff0c;我一直觉得“艺术”和“工艺”之间有一道看不见的坎&#xff1a;艺术负责天马行空的创意&#xff0c;工艺负责老老实实的落地。直到我动手写了一个叫做artcraft的框架&#xff0c;才真正尝到把两者焊在一起的甜头。这个框架的核心思路极简——用…

作者头像 李华
网站建设 2026/10/19 11:09:50

AI工程师转行指南:传统机器学习与大模型应用怎么选

这几年后台私信里高频出现的一类问题&#xff0c;就是“我准备转AI了&#xff0c;该学传统机器学习还是大模型&#xff1f;”一开始我没太当回事&#xff0c;问的人多了才意识到&#xff0c;这其实是一个很多转行者都卡住的起点问题——连方向都没看清&#xff0c;就开始囤几百…

作者头像 李华
网站建设 2026/10/19 11:08:24

自研在线图表设计器:从Canvas渲染到数据模型的架构实践

去年年中我给自己定了一个有点疯狂的目标&#xff1a;不依赖任何现成的图形编辑组件&#xff0c;从零写一个叫 diagram-design 的在线图表设计器。起因很现实——业务侧一直缺一个能高度定制的拓扑编排画布&#xff0c;通用工具要么改不动&#xff0c;要么引进来之后反而被框架…

作者头像 李华