
简介MATLAB实现的CNN-BiLSTM多输入回归预测完整源码面向需要将卷积网络与双向长短期记忆网络结合用于回归任务的科研人员、研究生及工程开发者尤其适合在能源、交通、经济等领域开展多变量时序预测的读者。数据为典型多输入回归样例包含12个输入特征与1个输出变量代码覆盖数据读取、归一化预处理、CNN特征提取、BiLSTM时序建模、全连接回归输出及RMSE/MAE等指标计算并配有网络结构图与训练过程可视化曲线。资源压缩包内共6个文件包括m源程序、xlsx数据文件、3幅png结果图以及docx配套说明文档整体大小仅333KB结构清晰、便于对照学习。目前已有2173人学习资料热度和认可度均较高。借助该资源可快速搭建自己的回归预测框架理解CNN与BiLSTM联合建模的核心流程程序在不同MATLAB版本下可能出现乱码可用记事本打开复制到新文件并在MATLAB2020b及以上环境中运行即可复现。1. CNN-BiLSTM多输入回归预测一套到手能跑的MATLAB源码省掉三天的排错时间把 CNN 和 BiLSTM 拼在一起做多输入回归预测听起来是论文里的黑匣子实际上代码跑通后就是一个输入多列特征、输出一列连续值的回归器。这份 MATLAB 源码包自带实测数据和完整训练流程不需要你从零搭网络拿到手先跑通再把特征列换成你自己的数据改一改维度参数就能复现。搜“CNN-BiLSTM 多输入回归预测”能找到一堆模型结构图但真正能跑、能换数据、能把坑说明白的代码很少这就是这份资源的价值所在。它适合正在做本科毕设、研究生小论文或者需要用 MATLAB 验证预测算法的工程师省掉的是踩维度坑、调参数、反归一化出错这几天的排错时间。2. 先把模型和数据对上路CNN-BiLSTM的结构选型、输入组织与第一版可跑代码多输入回归预测里最常见的翻车点不是模型不先进而是数据维度不匹配。所以这一章先讲清楚 CNN 和 BiLSTM 在回归任务里各管什么再讲数据怎么组织、网络怎么搭。2.1 CNN和BiLSTM各管什么多输入回归的输入是一个“样本数×特征数”的矩阵传统 BP 网络用全连接层把所有特征直接揉在一起特征之间的局部关系会被淹没在一堆权重更新里。CNN 的卷积核在特征维度上滑动每次只看相邻几个特征的组合相当于自动做局部特征工程BiLSTM 的作用在后面它把 CNN 输出的特征序列按正反两个方向扫描让前段特征和后段特征都能相互影响这个特性在特征排列顺序有一定语义的场景下特别明显。选型理由要落到“为什么是这两个而不是别的”。CNN 参数量小、卷积核权重共享不容易被小样本数据带偏BiLSTM 对序列依赖的建模能力强但单独拿它处理“特征彼此独立”的表格型数据网络很容易记住噪声而不是规律。两个拼起来CNN 先做一层滤波和特征提取BiLSTM 再抓长距离依赖等于一级滤波一级建模整体比单用 LSTM 或单用 BP 在多数回归数据上更稳。这个组合对样本量中等偏小几百到几千条的数据也比较友好不会像深层全连接网络那样轻易过拟合。模型优势短板适用场景BP简单、收敛快特征关系被扁平化处理特征少、样本多、纯表格数据LSTM擅长序列依赖对噪声敏感、易过拟合有真实时间步的预测CNN-BiLSTM局部特征双向依赖维度配置稍复杂多特征回归、传感器序列预测2.2 数据怎么放才不报错两种输入组织方式“为什么照网上的教程写了 sequenceInputLayer 还是报维度错误”这个问题十有八九是输入格式不对。MATLAB 的 trainNetwork 对序列数据要求用 cell 数组每个 cell 放一个样本的输入矩阵矩阵的行数是每个时间步的特征数列数是时间步数。对没有真实时间戳、只有多列特征的表格数据常见做法是把特征排列成“一个时间步一个特征”的形式也就是每个样本的特征向量转置后变成 f×1序列长度正好等于特征个数。% 加载数据X是n×f矩阵Y是n×1矩阵 load(demo_data.mat, X, Y); % 先打乱顺序再按7:1.5:1.5划分训练/验证/测试 rng(42); idx randperm(size(X, 1)); n size(X, 1); nTrain floor(0.7 * n); nVal floor(0.15 * n); XTrain X(idx(1:nTrain), :); YTrain Y(idx(1:nTrain), :); XVal X(idx(nTrain1:nTrainnVal), :); YVal Y(idx(nTrain1:nTrainnVal), :); XTest X(idx(nTrainnVal1:end), :); YTest Y(idx(nTrainnVal1:end), :);这段代码的逻辑是先把数据行顺序打乱再按比例切成三段。rng(42) 固定随机种子保证每次跑同一个脚本得到完全相同的划分结果这对后面调参复现很重要。randperm 生成随机排列的索引用它去取原始数据行。划分比例这里选 7:1.5:1.5 而不是 8:1:1是因为 LSTM 类模型需要验证集来盯过拟合验证集太小的话训练曲线上的验证 loss 波动会很大没法判断拐点。样本量只有几百条时可以改成 6:2:2多给一些数据给验证和测试。提示打乱顺序必须在特征缩放之前完成否则归一化的统计量会混入验证集和测试集的分布信息这在后面会造成测试结果虚高。接下来把矩阵形式的数据转成 cell 数组这是 trainNetwork 能读的格式% 表格数据转序列每个样本的特征向量转成一列 XTrainSeq cell(nTrain, 1); for i 1:nTrain XTrainSeq{i} XTrain(i, :); % f×1特征顺序就是时间步顺序 end这里 XTrain(i,:) 的作用是把一行特征变成列向量第一个特征落在第 1 个时间步最后一个特征落在第 f 个时间步。sequenceInputLayer 的第一个参数对应的是每个时间步的特征个数在这种组织方式下等于 1。如果你的数据本身有真实时间维度每个样本是 T×f 的矩阵那就不需要转置直接把 T×f 矩阵放进 cell后面的 sequenceInputLayer 参数改成 f 即可。两种方案的差别直接影响卷积层怎么滑动手前先分辨清楚你的数据到底属于哪种。2.3 网络结构怎么搭一版能直接跑的层配置对“特征即时间步”的方案网络定义如下numFeatures 1; % 方案A每个时间步只有1个特征 numResponses 1; % 单输出回归 layers [ sequenceInputLayer(numFeatures, Normalization, zscore) convolution1dLayer(3, 32, Padding, same, Name, conv1) reluLayer maxPooling1dLayer(2, Stride, 2, Name, pool1) bilstmLayer(64, OutputMode, last, Name, bilstm1) fullyConnectedLayer(32) reluLayer fullyConnectedLayer(numResponses) regressionLayer ];逐层拆开解释。convolution1dLayer(3, 32) 里的 3 是卷积核宽度意思是每次覆盖 3 个连续时间步的特征32 是滤波器个数输出 32 组特征。Padding 设为 same卷积后序列长度保持不变这样后面池化层即使把长度压一半也不至于让序列短到没法接 BiLSTM。maxPooling1dLayer(2, Stride, 2) 把序列长度压缩一半保留主要特征同时降低计算量。如果你的数据总共只有五六个特征这个池化层可以直接去掉因为压完之后序列只剩两三个时间步BiLSTM 基本学不到什么。bilstmLayer(64, OutputMode, last) 里 64 是隐单元数量OutputModelast 表示只取最后时间步的隐状态作为输出这正是回归任务的标准配置如果误设成 sequence模型会退化成逐时间步预测结果往往是一条平线。网络搭完之后训练选项按下面这个模板设置options trainingOptions(adam, ... MaxEpochs, 150, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... GradientThreshold, 1, ... Shuffle, every-epoch, ... ValidationData, {XValSeq, YVal}, ... ValidationFrequency, 10, ... Plots, training-progress, ... Verbose, true); net trainNetwork(XTrainSeq, YTrain, layers, options);adam 是自适应学习率的默认选择对 CNN-BiLSTM 这种结构没有特殊理由建议换 sgdm。GradientThreshold1 是这组参数里最不能省的一项BiLSTM 的循环结构在反向传播时梯度很容易爆炸加了阈值之后梯度范数被截断训练稳定性会明显改善。ValidationFrequency10 表示每 10 个 iteration 在验证集上算一次 loss配合 Plotstraining-progress 能实时看到训练和验证两条曲线的变化。这里把验证集以 {XValSeq, YVal} 的形式传入训练选项而不是手动早停训练过程中 MATLAB 会自动记录验证 loss 的变化。3. 训练参数不是玄学学习率、批量大小、过拟合控制的一次调参记录同一份数据、同一套网络结构不同人跑出来的结果差距很大。问题通常不在网络框架画得漂不漂亮而是一组训练选项没配合好。这一章把参数按“先设稳、再调优”的顺序讲每一步都可以照抄。3.1 三个参数先设稳再谈调优起步阶段先固定三件事训练轮数、批量大小、初始学习率。下面这张表是我在多组数据上的通用试探范围。参数推荐试探范围影响常见误用MaxEpochs100300太少欠拟合太多过拟合不看曲线盲目设大MiniBatchSize16 / 32 / 64影响梯度稳定性和显存占用样本少却强行用大batchInitialLearnRate0.0010.01决定收敛速度和稳定性图快设到0.1直接爆炸我一般先把“epoch150、batch32、lr0.005”这组值跑一遍看 loss 曲线的形态再决定往哪边动。如果训练 loss 到后期还在明显下行把 epoch 加到 300如果曲线震荡得像锯齿把 batch 翻倍或者把 lr 减半如果每轮 loss 下降很慢反而要把 lr 往上提一些因为 0.005 对一个较深的网络来说可能偏低。注意每次只动一个参数改了三个参数之后模型变好了你是没法知道到底哪个起作用的这不是玄学是变量控制。3.2 归一化的两种写法和它们对结果的影响sequenceInputLayer 的 Normalization,zscore 选项会在网络内部对输入做标准化。但对表格型数据我仍然建议手动做一次归一化原因有两个第一验证集和测试集在送入网络前只能用训练集算出的均值和标准差去缩放如果全部交给网络层处理它是在每个 batch 内部统计的验证集和测试集的分布和训练集略有差异时结果就不稳定第二手动归一化之后中间变量和权重都好解释排查问题方便很多。% 只用训练集计算缩放参数验证集和测试集直接用同一套 muX mean(XTrain, 1); sigmaX std(XTrain, 0, 1); XTrain (XTrain - muX) ./ sigmaX; XVal (XVal - muX) ./ sigmaX; XTest (XTest - muX) ./ sigmaX; % 回归目标也做同样的标准化预测完再反归一化 muY mean(YTrain); sigmaY std(YTrain); YTrainNorm (YTrain - muY) ./ sigmaY; YValNorm (YVal - muY) ./ sigmaY; YTestNorm (YTest - muY) ./ sigmaY;这段代码里最关键的一行是 muX 和 sigmaX 只在 XTrain 上计算验证集和测试集只是套用绝对不能用全样本的统计量。如果用了全样本统计量测试集的信息在训练之前就已经暴露给模型测试 R² 虚高换到真实数据上一预测就露馅这是回归预测里最典型的“数据泄漏”。Y 的标准化不是必须的但 Y 的量级在几千以上时不标准化regressionLayer 的 loss 会被大数值样本主导收敛变慢预测值的波动也会被放大。预测完之后别忘了反归一化把 YPred 乘上 sigmaY 加回 muY。3.3 过拟合怎么看、怎么拉回来训练 loss 一直降、验证 loss 在某个 epoch 掉头回升这是教科书式的过拟合信号。看到验证曲线出现拐点按顺序加三种手段不用同时全上。在 BiLSTM 后面插入 dropout 是最直接的一招layers [ ... bilstmLayer(64, OutputMode, last) dropoutLayer(0.2, Name, drop1) fullyConnectedLayer(32) ... ];dropout 比率 0.2 对回归场景通常够了调太高会让预测值整体偏低、方差偏大。如果加了 dropout 之后验证 loss 还是压不住就把 BiLSTM 隐单元数量从 64 减到 32减小网络容量。还有一招是在 trainingOptions 里加 L2Regularization, 0.001让权重本身受到约束。三种手段经常是 add 和 drop 的组合不是叠得越多越好。训练曲线的读法可以直接对照这张表排查曲线现象可能原因优先调整训练loss不降学习率太低或网络太弱lr提高到0.01或加卷积滤波器数loss震荡成锯齿batch太小或lr偏高batch翻倍或lr乘0.5loss变成NaN梯度爆炸或数据脏降lr、检查数据NaN、GradientThreshold保持1训练loss低但验证loss高过拟合加dropout、L2、减隐单元数另外可以在 trainingOptions 里加上 ValidationPatience, 20验证 loss 连续 20 次迭代没有改善就自动停掉训练这是防过拟合最省事的一招。我自己在 CNN-BiLSTM 上都固定开着它因为数据噪声大时多跑几十个 epoch 纯属浪费时间。4. 预测结果怎么才算好R²、RMSE、MAE与三张判断图训练完不等于模型能用。多输入回归预测最容易糊弄人的地方是 loss 很低但实际预测一塌糊涂因为 regressionLayer 的 loss 是 MSE它被数值大的样本主导。这一章把预测结果拆开用指标和图判断到底行不行。4.1 回归预测的三个硬指标测试集预测和指标计算放在一起写一次跑完YPred predict(net, XTestSeq, MiniBatchSize, 32); YPred double(YPred); % 反归一化回到原始量纲再评估 YPred YPred * sigmaY muY; YTestRaw YTest * sigmaY muY; SS_res sum((YTestRaw - YPred).^2); SS_tot sum((YTestRaw - mean(YTestRaw)).^2); R2 1 - SS_res / SS_tot; RMSE sqrt(mean((YTestRaw - YPred).^2)); MAE mean(abs(YTestRaw - YPred)); fprintf(R2 %.4f, RMSE %.4f, MAE %.4f\n, R2, RMSE, MAE);predict 返回的结果可能是单精度先转成 double 再计算。如果训练时对 Y 做了标准化评估之前必须反归一化否则量纲不对R² 和 RMSE 都会失真。R² 越接近 1 说明模型抓住了大部分数据变差R² 为负说明模型比“直接预测均值”还差这种结果基本是数据泄漏或网络没训练起来别再看曲线了回去检查数据划分和归一化。RMSE 对异常点敏感MAE 反映平均误差水平两个值差距大的时候比如 RMSE 是 MAE 的两倍以上说明有一小撮样本被预测得特别离谱要回去看那些点到底长什么样。4.2 散点图和误差曲线怎么看模型有没有“盯上”结构指标是压缩过的数字图能暴露指标掩盖的问题。第一张图是真实值和预测值的散点figure(Color, w); scatter(YTestRaw, YPred, 20, filled); hold on; lims [min([YTestRaw; YPred]), max([YTestRaw; YPred])]; plot(lims, lims, r--, LineWidth, 1.5); xlabel(真实值); ylabel(预测值); axis equal; title(预测值 vs 真实值);散点越贴合那条 45 度虚线说明预测在整体量级上跟住了真实值。如果点分布不是一条带子而是中间窄两头宽说明高值区预测方差大常见原因是训练样本里大数值样本占比太少。第二种图是残差曲线figure(Color, w); plot(1:length(YTestRaw), YTestRaw - YPred, o-); yline(0, k--, LineWidth, 1); xlabel(测试样本序号); ylabel(残差);残差曲线如果出现明显的趋势段比如前半段全是正残差、后半段全是负残差说明测试集里某个特征区间没在训练集中被覆盖到模型被“外推”了。这种情况调参救不回来要回去补数据。判断 R² 可以用一个大致的参考0.9 以上算强0.7 到 0.9 属于中等可用低于 0.7 基本要把模型或数据重做一轮。同时把 RMSE 和 YTestRaw 的标准差比一下如果 RMSE 接近标准差说明模型并没有比“直接预测均值”好多少。4.3 一个被忽略的细节多输出怎么改源码默认是单输出回归改成多输出只需要两步第一步把 Y 从 n×1 换成 n×m第二步把全连接层里的 numResponses 设为 m。核心改动就这一行numResponses size(YTrain, 2); % m列multi-output训练完之后每一列的预测值和真实值单独计算指标不要揉成一个数。多输出列之间的量级差异大时对每一列分别做 zscore 再训练效果更好评估时再按列反归一化回原单位。还有一个容易忽略的点网络输出的列顺序就是 YTrain 的列顺序如果在数据处理时对列做了排序或改名输出和真实值对不上指标全乱这一点我踩过后才知道所有测试脚本一开始就要把列名和顺序锁死。5. 避坑清单五个典型翻车现场与排查路径网上能跑通的 CNN-BiLSTM 代码换到自己的数据上十有八九会翻车。下面五条是我在类似数据集上反复遇到过的真实问题每条按现象、原因、解决三行式写排查路径可以直接照抄。5.1 训练loss正常下降测试R²却是负的现象训练过程曲线漂亮训练 loss 一路往下测试集的 R² 却是负的预测结果像随机数。原因绝大多数情况是数据泄漏——归一化的均值和标准差用了全样本统计量或者打乱顺序之前就完成了缩放。这样一来测试集的信息在训练前已经暴露给模型模型看着学到了东西实际学的是缩放参数。另外验证集和测试集划分后分布差异过大也会导致这个问题。解决严格执行“先打乱、再划分、只用训练集计算 mu 和 sigma”的顺序rng 固定到同一个种子保证每次对比都基于同一套划分。改完这一步R² 通常从负值直接回到正常区间。我在所有回归项目里都把这个顺序写死在脚本开头不给偷懒留机会。5.2 loss在某个epoch突然变成NaN现象训练到 50 个 epoch 左右日志里 loss 变成 NaN进度图直接断掉。原因学习率过大导致梯度过冲LSTM 的循环结构会放大这种梯度爆炸数据里存在 NaN 或 Inf 也会一路带进去。还有一个隐蔽原因特征列里有常数列标准化时会出现除以 0 的情况结果全是 NaN。解决先把数据检查写进脚本三行代码过一遍assert(~any(isnan(X(:))), 数据含有 NaN); assert(~any(isinf(X(:))), 数据含有 Inf); assert(~any(std(X, 0, 1) 0), 存在常数特征列无法标准化);然后把 InitialLearnRate 降到 0.001GradientThreshold 保持 1。90% 的 NaN 问题靠这两个动作能解决剩下的就去查数据源头。5.3 预测值是一条平线现象测试集预测出的 YPred 几乎是同一个常数散点图画出来是一条水平带。原因bilstmLayer 如果误设成 OutputModesequence每个时间步都产生一个输出最后接回归层的时候模型只能学出一个接近均值的常数输出。另一个常见原因是网络容量太弱卷积加少量隐单元不足以拟合回归目标。解决先确认 bilstmLayer 的 OutputModelast这是回归任务的标准写法。然后把隐单元从 64 加到 128卷积滤波器从 32 加到 64重新训练。如果训练集的 R² 还是低那基本就是结构太弱而不是数据问题。反过来如果训练集 R² 很高但预测还是平线问题出在反归一化或者输出层没有接对要回去查数据流。5.4 维度不匹配报错看最后一行才有效现象trainNetwork 一跑就报错红字铺了满屏真正的核心信息在最后一行类似“预期输入维度是 64但实际得到的是 32”。原因池化层把序列长度从 f 压到了大概 f/2BiLSTM 的隐单元数量改了之后全连接层的输入维度和上一层输出对不上更多时候是改了输出列数但忘了同步修改 fullyConnectedLayer 的输出维度。解决训练之前先跑一遍 analyzeNetwork(layers)它会列出每一层的输出尺寸一眼就能看出哪一层衔接不上。需要特别盯的是池化层之后的序列长度如果数据只有 5 个特征池化之后序列变成 2 或 3BiLSTM 基本学不到东西这种场景建议直接删掉池化层。改输出维度时把 fullyConnectedLayer(numResponses) 里的 numResponses 和 YTrain 列数保持一致这是最常见的低级错误但也是最容易忽略的。5.5 过拟合猛训练R²高达0.97测试却只有0.4现象训练集拟合得非常好测试集明显拉胯两个 R² 差距巨大。原因样本量少而网络容量太大CNN-BiLSTM 把训练集噪声当成规律记住了数据切分不均匀也会让测试集分布偏移。这种差距不是靠调学习率能缩小的问题的根子在容量和数据量不匹配。解决先用 k 折交叉验证估算真实水平把七三开改成 5 折或 8 折。小样本下 8 折的结果比单次划分的结论更接近真实泛化能力。加了 dropout 和 L2 还压不住就减网络宽度把 BiLSTM 隐单元从 64 减到 32卷积滤波器从 32 减到 16别一味堆正则。模型容量和数据量匹配才是正路。6. 让结果可信的进阶技巧五次重划分的稳定性评估流水线单次七三开的结果只是运气的一个样本。同一个模型在同一个数据集上换一种划分方式R² 可能差出 0.1 甚至更多尤其是样本量只有几百条的时候。要判断这个模型是不是“稳定地好”就得跑多次随机划分看指标均值和方差。这一节写一个可以直接套进自己脚本的循环骨架。% wrapper重复5次随机划分统计R2与RMSE的分布 numReps 5; R2List zeros(numReps, 1); RMSList zeros(numReps, 1); for rep 1:numReps rng(rep); % 每次固定不同种子结果可复现 % 这里放你的完整流程 % 划分数据 - 用训练集算mu/sigma做归一化 - 搭网络 - 训练 - 预测 - 反归一化 % 假设循环结束时已得到 YTestRaw 和 YPred R2List(rep) 1 - sum((YTestRaw - YPred).^2) / sum((YTestRaw - mean(YTestRaw)).^2); RMSList(rep) sqrt(mean((YTestRaw - YPred).^2)); end fprintf(R2 %.3f ± %.3f\n, mean(R2List), std(R2List)); fprintf(RMSE %.3f ± %.3f\n, mean(RMSList), std(RMSList));这里面有三个细节值得注意。第一rng(rep) 必须用固定种子而不是 rng(shuffle)这样同一份脚本任何时候重跑得到的五次划分和结果完全一致这是论文复盘和调参对照的前提。第二归一化的 mu 和 sigma 必须在循环内部每次重新计算千万不能放在循环外面算一次带进去否则每个 rep 之间信息串味稳定性报告就是假的。第三5 次是底线样本量允许就做 10 次最终报告写成“R² 0.87 ± 0.03”比只写一个 0.89 的信息量大得多别人也能直接看出你的结论是否可靠。从那以后我拿到一份新数据的 CNN-BiLSTM 任务第一件事永远是先跑一遍这个五次的循环结果稳了我才敢去调架构细节。这个习惯帮我挡掉了不少“单次跑得好、换数据就翻车”的尴尬场面。希望帮到你。本文还有配套的精品资源点击获取