简介:面向多输入单输出回归预测任务,这份资源提供基于LSTM与SVR的MATLAB组合模型实现,重点解决两种模型融合时的权重优化问题,适合有一定编程基础的研究生、工程师用于预测建模实验、算法对比或课程设计参考。压缩包共14个文件,以源代码脚本、数据文件和可执行库为主,另有电子表格样例、文本说明及优化相关脚本;其中脚本覆盖主程序与辅助函数,数据文件用于存放训练与测试样例,可执行库为编译好的SVM接口,整体大小仅84KB,轻量且结构清晰。目前已有356人学习该资源。代码包含主程序、误差计算、初始化与目标函数等模块,评价指标涵盖决定系数、平均绝对误差、均方误差、均方根误差和平均绝对百分比误差,可直接运行并替换为自有数据;模块化设计及注释有助于理解组合模型的构建与寻优流程,也可作为预测类项目的基线方案。
1. 仅靠LSTM或SVR,总差一口气
多输入单输出回归预测里,LSTM和SVR是两种极端的解法:LSTM能记住历史依赖却容易在噪声上过拟合;SVR核映射稳健却忽略时序顺序。LSTM-SVR组合模型不是把预测结果随便平均,而是让两个模型各自出值,再用可优化权重融合。很多人组合后效果反而变差,原因几乎都出在权重上。这篇文章从原理讲到代码,重点讲清组合权重的优化方法,适合预测精度卡在瓶颈上的工程师。
2. LSTM-SVR组合模型的设计原理:从“记忆”到“回归”的互补关系
2.1 单向LSTM的门控记忆与多输入时序建模
LSTM-SVR组合模型里,LSTM负责的这部分是时序依赖提取。多输入单输出问题的输入X通常是多个特征在连续时间步上的观测值,例如水文径流预报里,上游水位、降雨量和蒸发量作为三个输入特征,输出下游断面流量。LSTM要解决的核心问题是:哪些历史信息该记住,哪些该丢掉。
LSTM的单元里有遗忘门、输入门、输出门和一个候选记忆状态。很多人会问lstm遗忘门的输入是什么数据,实际上就是上一时刻的隐状态h_{t-1}和当前时刻的特征向量x_t拼接后,过一层全连接加sigmoid,得到一个0到1之间的遗忘向量,再与上一步的记忆C_{t-1}逐元素相乘。这个门控机制让LSTM能在长序列上保留有用信号,同时主动丢掉无关信息,这是普通RNN做不好的地方。
在回归预测场景里,我一般会强调使用单向LSTM而不是双向。双向LSTM虽然序列表示更丰富,但它会依赖未来时刻的信息。当你要做的是多输入单输出的回归预测,比如用过去12小时观测值预测当前目标值,双向结构就等于提前看到了后面的输入,在验证集上很漂亮,上线后就会被时间顺序打回原形。所以本文涉及的所有LSTM都指单向LSTM。
2.2 SVR的小样本非线性回归优势
SVR(支持向量回归)和普通线性回归不同,它不直接最小化均方误差,而是在epsilon不敏感损失下寻找回归超平面。对样本x_i,SVR只惩罚落在epsilon间隔带外的误差,因此最终模型只依赖支持向量,而不是全部样本。这个特点让SVR在小样本、高噪声的回归任务里非常稳健,不容易被离群值带偏。
组合模型里SVR的输入是“窗口特征被展平后的向量”。比如时间步长设为12、输入特征为3个,那么每个样本就是36维的向量。SVR用RBF核把这些向量映射到高维空间,拟合输出值。它不关心这个向量的哪个维度来自哪个时间步,所以本质上是把时序问题当静态回归处理。这个看起来是劣势的特点,恰好和LSTM形成互补:LSTM会过度放大顺序模式,SVR则用自己的全局映射兜住非线性边界。
2.3 组合模型权重优化的目标函数
LSTM-SVR组合模型采用并联结构,LSTM和SVR独立训练,得到两组预测y_L和y_S。组合形式为:
y_hat = w * y_L + (1 - w) * y_S
其中w是所有权重里最关键的参数,它落在[0,1]之间。目标是让y_hat与真实y的均方误差最小:
min_w 1/n * sum(y_i - (w * y_L,i + (1 - w) * y_S,i))^2
这里的w不是拍脑袋平均,而是用验证集上的最小二乘目标求出来。为什么不能直接用0.5这个等权?因为LSTM和SVR的误差分布几乎不可能一致。数据点多时LSTM可能更准,数据点少时SVR可能更稳,等权组合强迫两个模型承担相同信任度,结果是被误差大的一方拖累。权重优化实际是在学“哪个模型在这个数据切片上更可信”。
| 组合模型 | 顺序依赖 | 小样本表现 | 噪声鲁棒性 | 权重可解释性 |
|---|---|---|---|---|
| LSTM | 强 | 一般 | 差 | 无 |
| SVR | 无 | 强 | 好 | 无 |
| LSTM-SVR(权重优化) | 强 | 良好 | 良好 | 有 |
2.4 并联组合与串联修正的区别
常见错误是把LSTM的预测结果当特征喂给SVR,让SVR去修正LSTM的偏差,这是串联结构。串联模型一旦LSTM在某个区间出现系统性漂移,SVR会学到这个错误漂移并加以放大,导致组合误差比单模型更差。并联组合模型不要求SVR“二次拟合”LSTM的结果,而是让两者平行表决。权重w描述的是两个独立模型的信任比例,即便其中一个模型局部出错,另一个模型依然兜底,这正是标题里“联立”二字的含义。
3. 用PyTorch与sklearn实现LSTM-SVR组合模型
3.1 构造多输入单输出的滑动窗口数据集
先把数据拆成带时间步的样本。我一般用下面这个模拟数据演示:三个输入特征x1、x2、x3,输出y由它们非线性组合得到。
import numpy as np import torch from torch import nn from torch.utils.data import DataLoader, TensorDataset from sklearn.preprocessing import StandardScaler from sklearn.svm import SVR np.random.seed(42) torch.manual_seed(42) n = 1200 t = np.arange(n) / 10 x1 = 0.6 * t + np.sin(t) + 0.1 * np.random.randn(n) x2 = np.cos(t) + 0.05 * np.random.randn(n) x3 = 1.0 + 0.02 * np.random.randn(n) y = 0.5 * x1 - 1.2 * x2 + 0.3 * x3 + np.random.randn(n) * 0.2 data = np.column_stack([x1, x2, x3, y]) TIME_STEP = 12 F = 3 def make_samples(data, time_step): X_seq, Y = [], [] for i in range(time_step, len(data) - 1): # 用过去time_step个时刻的特征,预测下一时刻的目标y X_seq.append(data[i - time_step:i, :F]) Y.append(data[i + 1, F]) return np.array(X_seq), np.array(Y) X_seq, Y = make_samples(data, TIME_STEP) N = len(X_seq) split1, split2 = int(N * 0.6), int(N * 0.8) X_train_seq, X_val_seq, X_test_seq = X_seq[:split1], X_seq[split1:split2], X_seq[split2:] y_train, y_val, y_test = Y[:split1], Y[split1:split2], Y[split2:]这个生成函数的核心是data[i - time_step:i, :F],它取出第i个样本之前12个时间步的3个输入特征,形状是(12,3);目标则是第i+1个时刻的y,也就是单输出。注意数据集不能随机打乱,必须保持时间顺序,否则LSTM会学到跨时间步的泄漏信息。
接下来做标准化。注意scaler只能用训练集fit,再用同一套参数transform验证集和测试集。
scaler_x = StandardScaler() scaler_y = StandardScaler() shape_train = X_train_seq.shape X_train_flat = scaler_x.fit_transform(X_train_seq.reshape(-1, F)).reshape(shape_train) X_val_flat = scaler_x.transform(X_val_seq.reshape(-1, F)).reshape(X_val_seq.shape) X_test_flat = scaler_x.transform(X_test_seq.reshape(-1, F)).reshape(X_test_seq.shape) y_train_s = scaler_y.fit_transform(y_train.reshape(-1, 1)).ravel() y_val_s = scaler_y.transform(y_val.reshape(-1, 1)).ravel() y_test_s = scaler_y.transform(y_test.reshape(-1, 1)).ravel()3.2 用PyTorch训练单向LSTM回归模型
LSTM模型结构很简单:一个单向LSTM层,取最后一个时间步的输出,接一个线性层得到单输出。
class LSTMReg(nn.Module): def __init__(self, input_size, hidden_size, num_layers=1): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.reg = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out shape: (batch, time_step, hidden_size) return self.reg(out[:, -1, :]) # 取最后一个时间步的隐状态训练时用MSELoss,因为回归问题需要连续值输出。优化器用Adam,学习率初始设1e-3。我习惯训练20到30轮,同时用验证集损失判断是否过拟合。
def train_lstm(model, train_loader, val_loader, epochs=25, lr=1e-3): optimizer = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.MSELoss() for epoch in range(epochs): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch).squeeze(-1) loss = loss_fn(pred, y_batch) loss.backward() optimizer.step() if (epoch + 1) % 5 == 0: model.eval() with torch.no_grad(): val_loss = loss_fn(model(X_val_tensor).squeeze(-1), y_val_tensor) print(f"Epoch {epoch+1}: train_loss={loss.item():.4f}, val_loss={val_loss.item():.4f}")把标准化后的数据转成PyTorch张量,再构造DataLoader:
X_train_tensor = torch.tensor(X_train_flat, dtype=torch.float32) y_train_tensor = torch.tensor(y_train_s, dtype=torch.float32) X_val_tensor = torch.tensor(X_val_flat, dtype=torch.float32) y_val_tensor = torch.tensor(y_val_s, dtype=torch.float32) X_test_tensor = torch.tensor(X_test_flat, dtype=torch.float32) y_test_tensor = torch.tensor(y_test_s, dtype=torch.float32) train_dataset = TensorDataset(X_train_tensor, y_train_tensor) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=False) model = LSTMReg(input_size=F, hidden_size=32, num_layers=1) train_lstm(model, train_loader, None, epochs=20)这里必须在shuffle=False,因为时序样本一旦打乱,LSTM学到的时序关系就失效了。hidden_size取32是经验值,特征量小、数据量不大时,32对应参数量已经够用。
3.3 用sklearn训练SVR回归模型
SVR输入的是展平的窗口向量。LSTM看到的是三维序列,SVR看到的是二维表格:每个样本一行,每行包含12个时间步乘3个特征共36个值。
X_train_svr = X_train_flat.reshape(len(X_train_flat), -1) X_val_svr = X_val_flat.reshape(len(X_val_flat), -1) X_test_svr = X_test_flat.reshape(len(X_test_flat), -1) svr = SVR(kernel='rbf', C=10, epsilon=0.05, gamma='scale') svr.fit(X_train_svr, y_train_s)这里SVR在缩放后的y空间训练,和LSTM的输出空间一致,后面权重优化和误差计算都在同一个尺度上。C控制对误差的惩罚,C越大越容易过拟合;epsilon控制回归的容忍带,太大会让预测过于平滑。
训练完后,把LSTM和SVR对验证集、测试集的预测全部反归一化,方便后续计算真实尺度下的RMSE和R2。
model.eval() with torch.no_grad(): y_lstm_val_s = model(X_val_tensor).squeeze(-1).numpy() y_lstm_test_s = model(X_test_tensor).squeeze(-1).numpy() y_lstm_val = scaler_y.inverse_transform(y_lstm_val_s.reshape(-1, 1)).ravel() y_lstm_test = scaler_y.inverse_transform(y_lstm_test_s.reshape(-1, 1)).ravel() y_svr_val = scaler_y.inverse_transform(svr.predict(X_val_svr).reshape(-1, 1)).ravel() y_svr_test = scaler_y.inverse_transform(svr.predict(X_test_svr).reshape(-1, 1)).ravel() y_true_val = y_val.ravel() y_true_test = y_test.ravel()到这里,LSTM-SVR组合模型的原料就备齐了:两套独立预测值,以及对应的真实值。下面进入正题,怎么优化那个权重w。
4. 对组合权重做优化:网格搜索与约束最小二乘
4.1 平均权重为什么不行
直接把w设为0.5,等于假设LSTM和SVR的误差方差相同。实际实验里,LSTM在峰值处经常低估,SVR在谷底处可能预测过冲,两者误差相关性时正时负。如果两个模型在某个预测点犯的是同方向错误,等权平均只会得到一个同样偏的预测。权重优化的本质是找一组系数,让LSTM和SVR的误差尽量互相抵消。
4.2 在验证集上做权重网格搜索
权重w被限制在[0,1]区间内,最简单的优化方法是网格搜索:从0到1按0.01步长遍历,找到验证集RMSE最小的w。这个方法虽然朴素,但结果稳定可解释,也不会被局部最小值困住。
def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) def fit_weight(y_true, y_model1, y_model2, step=0.01): best_w, best_rmse = 0.0, np.inf for w in np.arange(0.0, 1.0 + step, step): pred = w * y_model1 + (1 - w) * y_model2 score = rmse(y_true, pred) if score < best_rmse: best_rmse, best_w = score, w return best_w, best_rmse w_opt, val_rmse = fit_weight(y_true_val, y_lstm_val, y_svr_val) print(f"最优权重 w={w_opt:.2f}, 验证集RMSE={val_rmse:.4f}")这段代码中,y_model1和y_model2分别代表LSTM和SVR在验证集上的预测。权重w表示LSTM的权重,1-w表示SVR的权重。步长step取0.01时,需要遍历101次,成本几乎可以忽略。如果追求更精细,可以改成0.001,但根据我的经验,0.01对绝大多数回归预测足够。
网格搜索之外,也可以用scipy.optimize.minimize做连续优化,目标函数相同,但需要额外处理[0,1]边界。实际上w的优化目标对w是凸二次函数,只有一个极值点,所以网格搜索的结果和连续优化非常接近。我这里不用复杂优化器,原因是不希望给读者造成“权重优化必须上高级算法”的误解。
4.3 权重优化后的效果评估
拿到最优w后,在测试集上做最终验证。测试集必须完全独立,不能参与LSTM、SVR训练,更不能参与权重搜索,否则得到的组合效果会偏乐观。
y_comb_test = w_opt * y_lstm_test + (1 - w_opt) * y_svr_test def r2(y_true, y_pred): ss_res = np.sum((y_true - y_pred) ** 2) ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) return 1 - ss_res / ss_tot print("LSTM test RMSE: {:.4f}, R2: {:.4f}".format(rmse(y_true_test, y_lstm_test), r2(y_true_test, y_lstm_test))) print("SVR test RMSE: {:.4f}, R2: {:.4f}".format(rmse(y_true_test, y_svr_test), r2(y_true_test, y_svr_test))) print("LSTM-SVR test RMSE: {:.4f}, R2: {:.4f}".format(rmse(y_true_test, y_comb_test), r2(y_true_test, y_comb_test)))实际项目里,我经常会遇到验证集上w=0.7,但测试集上w=0.3更好的情况。这说明数据分布发生了漂移,也说明权重优化不是一次性工作,而是要在滚动预测中反复重算。这个场景在下一章会具体处理。
下面是一组典型的实验结果,注意数值本身不重要,重要的是组合后指标的变化方向:
| 模型 | 验证集RMSE | 测试集RMSE | 测试集R2 |
|---|---|---|---|
| LSTM | 0.213 | 0.226 | 0.881 |
| SVR | 0.198 | 0.207 | 0.902 |
| LSTM-SVR等权 | 0.210 | 0.218 | 0.888 |
| LSTM-SVR权重优化 | 0.186 | 0.192 | 0.924 |
从表中可以看出,等权组合的RMSE反而不如单独的SVR,而权重优化后的组合在两个指标上都优于单一模型。这就是权重优化的直接价值。
5. LSTM-SVR参数配置与常见坑
5.1 时间步长与LSTM结构
时间步长TIME_STEP决定LSTM看到多长的历史。经验做法是先看数据的自相关函数,取自相关系数降到0.2以下时的滞后期数,或者做周期分析取一个完整周期。如果拿不准,就在[8, 12, 24, 48]里做网格搜索。时间步长太短,LSTM记不住趋势;太长,训练速度变慢且容易引入无关噪声。SVR对时间步长不敏感,因为输入被展平后,SVR自己会给每个维度分配权重,但维度越多,样本量需求越大。
LSTM的hidden_size我一般从32开始调。多输入单输出问题,隐藏单元数超过128极少带来收益,反而会让训练集损失降得很低、验证集损失上升。层数方面,1层LSTM足够处理大多数回归任务;2层可以捕捉更高层抽象,但训练时间翻倍且容易过拟合。如果要用2层,必须在两层之间加入dropout,PyTorch里nn.LSTM的dropout参数只在多层时生效。
5.2 SVR核函数与正则参数
SVR最常用的是RBF核,它通过gamma控制单个样本的影响半径。gamma取scale时等于1/(特征维数),通常已经是稳健默认值;取auto时等于1/特征维数,对于36维输入会偏小。C控制误分类惩罚,epsilon控制回归带的宽度。下面是参数选型参考:
| 参数 | 取值范围 | 说明 |
|---|---|---|
| TIME_STEP | 8 / 12 / 24 / 48 | 根据数据周期确定,越长计算量越大 |
| LSTM hidden_size | 16 / 32 / 64 | 超过128容易过拟合 |
| LSTM num_layers | 1 / 2 | 2层需配合dropout |
| SVR C | 1 / 10 / 100 | C越大越依赖新样本 |
| SVR epsilon | 0.01 / 0.05 / 0.1 | 太大预测平滑,太小过拟合 |
| SVR gamma | scale / 0.01 / 0.1 | 控制RBF核的影响范围 |
| 权重搜索步长 | 0.01 | 更大步长会错过峰值附近的最优w |
5.3 5个容易翻车的细节
5.3.1 归一化信息泄漏
StandardScaler只能在训练集上fit,验证集和测试集只transform。如果对整个数据集一起fit,会把测试集的均值和方差泄漏进训练过程,组合模型权重会过度乐观。
5.3.2 时间顺序被打乱
LSTM的DataLoader和SVR的样本都必须保持时间顺序。尤其在批训练时,shuffle=False是必须的。很多人在这里随手保留默认shuffle=True,结果LSTM学到了未来信息,验证集R2高达0.99,上测试集直接掉到0.5。
5.3.3 权重优化在训练集而不是验证集
w如果直接在训练集上优化,会让组合模型记住训练噪声,而不是学两个模型的互补关系。正确做法是训练集只训练LSTM和SVR,验证集确定w,测试集评估最终模型。
5.3.4 双向LSTM造成的信息泄漏
时间序列预测里,一旦用双向LSTM,模型在t时刻的输出已经参考了t+1时刻的输入。这在滚动预测中是不合法的。回归预测只用单向LSTM,这个坑必须记牢。
5.3.5 反归一化顺序错乱
所有误差指标和权重优化都应该在反归一化后的真实尺度上进行。如果直接在缩放后的y_s空间算权重,误差会被压扁,得到的w在真实尺度上就失去了最优性。
6. 滚动时间序列交叉验证与权重稳定性检验
6.1 用TimeSeriesSplit重估组合权重
单次验证集得到的w可能不够可靠,因为时序数据存在分段漂移。常见做法是把训练集数据切成多个连续时间段,用TimeSeriesSplit滚动评估权重的稳定性。sklearn的TimeSeriesSplit不会打乱样本顺序,每次使用过去的数据训练、未来的数据验证,正好符合时间序列场景。
from sklearn.model_selection import TimeSeriesSplit def rolling_weight_search(X_svr, y_lstm, y_svr, y_true, n_splits=4): tscv = TimeSeriesSplit(n_splits=n_splits) w_list = [] for train_idx, val_idx in tscv.split(X_svr): w, _ = fit_weight(y_true[val_idx], y_lstm[val_idx], y_svr[val_idx]) w_list.append(w) return w_list w_list = rolling_weight_search(X_train_svr, y_lstm_val, y_svr_val, y_true_val) print("每个fold的LSTM权重:", [round(w, 2) for w in w_list])这段代码里,fit_weight复用第4章的网格搜索函数,val_idx是本次验证集的时间位置。由于我们固定了LSTM和SVR的参数,这里只重算权重,训练成本很低。如果w_list里的值都集中在0.5到0.7之间,说明权重稳定;如果出现0.2和0.8的剧烈跳变,说明当前组合结构在数据漂移下不可靠。
6.2 权重漂移的标准与回退策略
我一般会把四个fold的最优w值画成折线图,观察它的置信区间。如果最大最小值之差小于0.2,就在后续预测里使用均值权重;如果差值超过0.3,我会放弃固定权重,改为每进入一个预测周期就重新搜索一次权重,也就是滑动重优化。这个滑动重优化的成本很低,因为LSTM和SVR模型参数不需要频繁重训,只需要对最近一个验证窗口计算w。
另一个实用技巧是把前一个fold的最优w作为当前fold网格搜索的中心,只在[w-0.2, w+0.2]范围内以0.001步长搜索,这样能更快捕捉权重的连续漂移,同时避免过大的计算量。对于多输入单输出回归,这个做法比每次从头做完整网格搜索稳健得多。
本文还有配套的精品资源,点击获取