
简介面向多输入单输出回归预测任务这份资源提供基于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_iSVR只惩罚落在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)目标则是第i1个时刻的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_layers1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) 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, epochs25, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) 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(fEpoch {epoch1}: train_loss{loss.item():.4f}, val_loss{val_loss.item():.4f})把标准化后的数据转成PyTorch张量再构造DataLoaderX_train_tensor torch.tensor(X_train_flat, dtypetorch.float32) y_train_tensor torch.tensor(y_train_s, dtypetorch.float32) X_val_tensor torch.tensor(X_val_flat, dtypetorch.float32) y_val_tensor torch.tensor(y_val_s, dtypetorch.float32) X_test_tensor torch.tensor(X_test_flat, dtypetorch.float32) y_test_tensor torch.tensor(y_test_s, dtypetorch.float32) train_dataset TensorDataset(X_train_tensor, y_train_tensor) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) model LSTMReg(input_sizeF, hidden_size32, num_layers1) train_lstm(model, train_loader, None, epochs20)这里必须在shuffleFalse因为时序样本一旦打乱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(kernelrbf, C10, epsilon0.05, gammascale) 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, step0.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)))实际项目里我经常会遇到验证集上w0.7但测试集上w0.3更好的情况。这说明数据分布发生了漂移也说明权重优化不是一次性工作而是要在滚动预测中反复重算。这个场景在下一章会具体处理。下面是一组典型的实验结果注意数值本身不重要重要的是组合后指标的变化方向模型验证集RMSE测试集RMSE测试集R2LSTM0.2130.2260.881SVR0.1980.2070.902LSTM-SVR等权0.2100.2180.888LSTM-SVR权重优化0.1860.1920.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层必须在两层之间加入dropoutPyTorch里nn.LSTM的dropout参数只在多层时生效。5.2 SVR核函数与正则参数SVR最常用的是RBF核它通过gamma控制单个样本的影响半径。gamma取scale时等于1/(特征维数)通常已经是稳健默认值取auto时等于1/特征维数对于36维输入会偏小。C控制误分类惩罚epsilon控制回归带的宽度。下面是参数选型参考参数取值范围说明TIME_STEP8 / 12 / 24 / 48根据数据周期确定越长计算量越大LSTM hidden_size16 / 32 / 64超过128容易过拟合LSTM num_layers1 / 22层需配合dropoutSVR C1 / 10 / 100C越大越依赖新样本SVR epsilon0.01 / 0.05 / 0.1太大预测平滑太小过拟合SVR gammascale / 0.01 / 0.1控制RBF核的影响范围权重搜索步长0.01更大步长会错过峰值附近的最优w5.3 5个容易翻车的细节5.3.1 归一化信息泄漏StandardScaler只能在训练集上fit验证集和测试集只transform。如果对整个数据集一起fit会把测试集的均值和方差泄漏进训练过程组合模型权重会过度乐观。5.3.2 时间顺序被打乱LSTM的DataLoader和SVR的样本都必须保持时间顺序。尤其在批训练时shuffleFalse是必须的。很多人在这里随手保留默认shuffleTrue结果LSTM学到了未来信息验证集R2高达0.99上测试集直接掉到0.5。5.3.3 权重优化在训练集而不是验证集w如果直接在训练集上优化会让组合模型记住训练噪声而不是学两个模型的互补关系。正确做法是训练集只训练LSTM和SVR验证集确定w测试集评估最终模型。5.3.4 双向LSTM造成的信息泄漏时间序列预测里一旦用双向LSTM模型在t时刻的输出已经参考了t1时刻的输入。这在滚动预测中是不合法的。回归预测只用单向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_splits4): tscv TimeSeriesSplit(n_splitsn_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, w0.2]范围内以0.001步长搜索这样能更快捕捉权重的连续漂移同时避免过大的计算量。对于多输入单输出回归这个做法比每次从头做完整网格搜索稳健得多。本文还有配套的精品资源点击获取