MATLAB实现CNN-BiLSTM多输入回归预测完整指南
2026/8/31 18:04:52 网站建设 项目流程

简介:本资源面向机器学习与智能预测方向的MATLAB初学者及工程实践者,提供一套开箱即用的CNN-BiLSTM混合神经网络多输入回归预测完整实现方案,适用于电力负荷预测、环境参数建模、工业过程变量估计等典型场景。压缩包共6个文件(333KB),含核心训练脚本(.m)、实测数据集(.xlsx)、三张关键可视化结果图(.png,涵盖训练损失、预测拟合与误差分布)以及详细技术说明文档(.docx),覆盖数据预处理、网络构建、超参配置、训练评估与结果分析全流程。已有2172人学习下载,所有代码基于MATLAB 2020b及以上版本验证通过,针对常见中文乱码问题提供明确解决方案(建议记事本中转复制)。读者可直接运行主程序复现全部结果,快速掌握卷积层提取局部特征、BiLSTM捕获双向时序依赖、多特征融合回归建模的技术要点,并基于现有结构灵活适配其他12维输入的单变量预测任务。 这些年帮人调过不少回归预测模型,MATLAB里做CNN-BiLSTM多输入回归预测算是出现频率相当高的一种需求。不管是碳排放预测、建材价格预测、设备剩余寿命估计,场景各不相同,但套路基本一致:用多个输入特征做自变量,一个或多个连续数值做输出,先让卷积神经网络抓局部特征,再用双向长短期记忆网络抓上下文时序依赖,最后过全连接层输出回归值。这篇文章我会把这个方案从数据预处理、网络搭建、训练配置到结果评估完整拆开讲,并且给出可以组装运行的MATLAB源码框架和数据组织方式。适合手里有多个输入特征数据集、想快速跑通CNN-BiLSTM回归预测模型的工程师和研究生。

1. 项目背景与整体方案设计思路

1.1 为什么选CNN-BiLSTM组合

先说结论:多输入回归预测任务里,纯LSTM、纯CNN都能做,但效果往往没有组合模型稳。原因要从两类网络各自的特性说起。

CNN(卷积神经网络)擅长的是局部特征提取。卷积核在输入序列上滑动,可以自动发现相邻特征之间的局部模式,比如两个输入变量之间的短期耦合关系、某个变量在局部窗口内的变化形态。对一维数据来说,1D CNN的计算量小、参数少,能有效降低输入中噪声干扰,把它看成一个自动的特征工程器很合适。

LSTM(长短期记忆网络)擅长捕捉时间顺序上的长期依赖,通过门控机制决定哪些信息保留、哪些信息遗忘。而BiLSTM相当于在正向LSTM基础上再叠一层反向LSTM,让网络既能看到“过去”的信息,又能看到“未来”的信息。多输入回归预测里,输入特征之间往往存在双向的上下文影响,BiLSTM这种双向建模能力非常关键。

把CNN和BiLSTM串起来,等于先让CNN把原始多输入特征做一次局部抽象,再让BiLSTM对抽象后的特征序列做双向时序建模,最后全连接层把高维特征映射到目标值。这样做的好处是:局部特征提取和全局时序依赖各司其职,模型的上限更高,训练起来也比纯BiLSTM稳定。

我实际测试过几个不含时间属性的表格型多输入数据集,CNN-BiLSTM的R²普遍比单独BiLSTM高出2到5个百分点,原因就是CNN先把不同输入特征之间的局部关系提炼出来了,BiLSTM拿到的是更高层的特征表达。

1.2 多输入回归预测的问题定义与数据结构

多输入回归预测从数学上看并不复杂:给定N个输入特征x1, x2, ..., xN,学习一个映射f,让输出y等于f(x1, x2, ..., xN),其中y是连续数值。典型例子有:用温度、湿度、风速、气压预测光伏发电量;用历史价格、库存量、采购指数预测建材价格;用振动、电流、温度等传感器数据预测设备剩余寿命。

数据组织上,我遇到最多的有两种形态。

第一种是表格型数据,每行是一个样本,每列是一个特征,最后一列是目标值。这种数据本身没有先后顺序,但为了用上CNN和BiLSTM的序列建模能力,可以画蛇添足一步:把一条样本的多个特征看成一个特征序列,按特征编号排好,作为长度等于特征数的“伪序列”喂给网络。

第二种是时间序列数据,特征本身随时间变化。这种情况往往需要用滑动窗口构造样本:每个样本取过去T个时间步的所有特征值,输出是下一个时刻的预测值。例如用过去10天的温度、湿度、风速预测明天的光伏发电量,窗口长度就是10,特征数就是3,输入数组尺寸为3×10。

这两种形态在MATLAB里的处理方式略有不同,但网络主体结构可以完全复用。

2. 数据准备与预处理实操

2.1 归一化处理与数据划分

多输入回归预测里,归一化是优先级最高的一个环节。不同特征量纲不同,比如温度可能是30,风速可能是1500瓦/平方米,如果不做归一化,CNN卷积计算时数值范围大的特征会主导梯度更新,训练非常不稳定,甚至出现NaN损失。

MATLAB里最常用的归一化函数就是mapminmax。它的作用是把数据线性映射到[-1,1]区间,公式是y = (ymax-ymin)*(x-xmin)/(xmax-xmin) + ymin。用起来很简单:

[Xnorm, psX] = mapminmax(X_train, -1, 1); [Ynorm, psY] = mapminmax(Y_train', -1, 1); Ynorm = Ynorm';

这里有个坑:psX和psY是基于训练集计算出来的,测试集和验证集必须用同一个ps做归一化,不能单独对测试集再算一次最大最小值,否则会造成数据泄露,测试结果虚高。换句话说,psX和psY要保存下来,等到预测完再反归一化时用同一个结构体。

数据划分方面,分两种情况。表格型数据可以随机打乱,按7:1.5:1.5划分训练集、验证集、测试集。时间序列数据不能随机打乱,必须按时间顺序切分,比如前70%训练,中间15%验证,最后15%测试,否则模型会看到未来信息。

写代码时建议加一行固定随机种子:

rng(42);

这样每次跑出来的结果一致,方便复现和对比实验。

2.2 MATLAB中构造CNN-BiLSTM输入格式

训练CNN-BiLSTM网络,MATLAB里的sequenceInputLayer期望输入是一个cell数组,每个cell对应一个样本。每个cell内部是一个二维数组,尺寸是featureDimension×sequenceLength。

这里稍微解释一下:featureDimension是每个时间步的特征维度,sequenceLength是序列长度。很多人第一次写MATLAB深度学习代码就卡在这,干脆写个函数自动构造:

function [XTrainCell, YTrain] = makeSlidingWindow(X, Y, windowSize) % X: n×f 的特征矩阵,f为特征个数 % Y: n×1 的目标值 % windowSize: 滑动窗口长度 n = size(X, 1); numSamples = n - windowSize + 1; XTrainCell = cell(numSamples, 1); YTrain = zeros(numSamples, 1); for i = 1:numSamples % 每个样本取连续windowSize行的特征,转置成 f×windowSize XTrainCell{i} = X(i:i+windowSize-1, :)'; % 目标值是对应窗口最后一行的Y YTrain(i) = Y(i + windowSize - 1); end end

如果数据是表格型、没有时间先后关系,依然可以复用这个思路:把窗口长度设为1,也就是每条样本本身成为一个长度为1的序列,然后靠CNN在特征维度上做卷积。但更常用的做法是把特征数目本身就当成序列长度,构造方式如下:

XTrain = num2cell(X', 1); % 每个cell为 f×1,序列长度为特征数

这样每个样本是 f×1 的“伪序列”,CNN卷积核就可以在特征维度上滑动提取局部关系了。我不建议把窗口长度设置成1,因为那样BiLSTM几乎没有时序依赖可捕捉,网络就退化了。

3. 网络构建与关键参数设置

3.1 CNN-BiLSTM网络结构逐层拆解

直接上我最常用的网络结构,它经过多个数据集验证,回归预测任务上表现稳定:

numFeatures = size(X_train, 2); % 特征数量 numResponses = size(Y_train, 2); % 输出数量,一般回归任务为1 layers = [ sequenceInputLayer(numFeatures, 'Normalization', 'none') convolution1dLayer(5, 16, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) bilstmLayer(32, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(32) reluLayer fullyConnectedLayer(numResponses) regressionLayer ];

逐层解释一下为什么这么设计。

sequenceInputLayer的Normalization参数我设成了'none',因为前面已经用mapminmax手动归一化了,这里没必要再做一次,否则等于是双重归一化,影响解释性。

convolution1dLayer(5, 16)表示卷积核长度为5,输出通道16个。这个“核长度”可以理解成一次看几个相邻输入特征的窗口。窗口长度5是一个比较通用的起点,如果特征之间相关性弱,可以降到3;如果特征维度多且相关性强,可以升到7或9。滤波数量16是一个很小的起步值,数据集不大时足够用,也可以试试32。

batchNormalizationLayer的作用是让每一层的输入分布稳定,加速收敛。它对回归模型尤其有用,因为回归模型的损失面往往比分类更“崎岖”,批量归一化可以有效缓解梯度震荡。

maxPooling1dLayer(2, 'Stride', 2)是池化层,把序列长度缩小一半,减少后续BiLSTM的计算量,同时保留主要特征。这里要特别提醒:如果输入序列长度本来就短,比如窗口长度只有10,池化后变成5,没问题;但如果窗口长度只有4,池化后再进入BiLSTM可能就只剩2个时间步,信息损失严重。这种情况建议去掉池化层,或者把Stride改成1(不降采样)。

bilstmLayer(32, 'OutputMode', 'last')是双向LSTM,隐藏单元数32。OutputMode设为'last'是回归预测的关键点,意思是BiLSTM只输出最后一个时间步的隐藏状态,然后直接接到全连接层。如果设为'sequence',输出的是每个时间步的结果,还要再接一层fullyConnected,逻辑就绕了。

dropoutLayer(0.2)在训练时随机丢弃20%的神经元,防止过拟合。回归预测的数据量通常不大,dropout基本是标配。如果发现训练集误差低、测试集误差高,可以把这个值提高到0.3到0.5。

最后的fullyConnectedLayer(32)是我自己习惯加的一层中间全连接,让BiLSTM输出的特征做一次非线性组合,之后再压缩到numResponses。直接让BiLSTM接1个输出节点也能跑,但加上中间层回归精度会稍微好一点。

3.2 训练选项与超参数调优

网络结构定好之后,训练参数对结果影响也非常大。我经常看到有人直接复制默认参数去训练,结果损失完全不下降,然后以为是网络写错了。实际上大多数时候是训练选项没配好。

下面这段训练选项配置是我在多个回归数据集上反复调过之后留下的基准配置:

options = trainingOptions('adam', ... 'MaxEpochs', 100, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, ... 'GradientThreshold', 1, ... 'Shuffle', 'every-epoch', ... 'ValidationData', {XValidCell, YValid}, ... 'ValidationFrequency', 20, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 30, ... 'LearnRateDropFactor', 0.5, ... 'Plots', 'training-progress', ... 'Verbose', 1);

每个参数都展开说一下逻辑。

MaxEpochs设100,对于小规模回归数据来说已经足够。如果验证损失在60轮之后不再下降,训练会自动表现出平台期,设置成100只是给模型充足的拟合空间。

MiniBatchSize设32是平衡稳定性和训练速度的选择。小批量太小(比如8),梯度噪声大,损失曲线震荡;太大(比如128),内存占用高,容易把GPU显存撑爆。数据集确实很大时,可以升级到64。

InitialLearnRate设0.001是Adam优化器的经典默认值。回归任务里我一般不会直接上0.01,那会导致损失曲线发散,尤其是网络里有BiLSTM这种对学习率比较敏感的层。

GradientThreshold是梯度裁剪阈值,设为1可以在梯度爆炸时把梯度截断到1以内。BiLSTM反向传播路径长,梯度爆炸概率不低,这个参数能避免训练过程中突然出现NaN。

ValidationFrequency表示每20次迭代验证一次,这个值可以根据迭代总量调整,保证训练过程中能看到验证损失的变化趋势。

学习率衰减策略选了piecewise,每30轮衰减一半。有时候损失下降到一定程度就卡住不动了,降低学习率能帮助模型越过局部最优。

4. 训练过程与回归预测结果评估

4.1 训练日志与损失曲线怎么看

训练过程中,MATLAB会实时绘制训练损失和验证损失曲线。第一次跑的时候,很多人盯着RMSE曲线乱跳就慌了,其实要看趋势而不是单点。

正常的训练曲线应该长这样:训练损失稳步下降,验证损失同步下降,最终两者都趋于平缓,并且验证损失没有明显反弹。如果训练损失继续下降但验证损失开始反弹,那就是过拟合的前兆,需要增加dropout系数或降低网络的隐藏单元数。

这里有个经验:回归预测的损失曲线不需要追求收敛到0,尤其是对真实噪声大的数据。比如目标值的范围在0到1之间,RMSE能到0.05就已经是不错的结果。纠结于损失必须非常低,反而会带着模型走向过拟合。

在训练完成之后,用save保存网络,方便后续加载使用:

save('cnn_bilstm_model.mat', 'net', 'psX', 'psY');

4.2 评估指标计算与可视化

训练完成后,使用predict函数对测试集做预测,这一步注意和归一化配合好:

pred_norm = predict(net, XTestCell); % 反归一化,恢复到原始量纲 YTest_raw = mapminmax('reverse', YTest', psY)'; pred_raw = mapminmax('reverse', pred_norm, psY)'; % 计算评估指标 rmse = sqrt(mean((pred_raw - YTest_raw).^2)); mae = mean(abs(pred_raw - YTest_raw)); mape = mean(abs((pred_raw - YTest_raw) ./ YTest_raw)) * 100; ss_res = sum((YTest_raw - pred_raw).^2); ss_tot = sum((YTest_raw - mean(YTest_raw)).^2); r2 = 1 - ss_res / ss_tot; fprintf('RMSE: %.4f\n', rmse); fprintf('MAE: %.4f\n', mae); fprintf('MAPE: %.4f%%\n', mape); fprintf('R²: %.4f\n', r2);

这四个指标各有侧重点:RMSE对较大误差敏感,适合判断模型的“极端坏情况”;MAE更稳定,反映平均误差;MAPE把误差归一化到百分比,方便跨数据集比较;R²判断模型对目标变量方差的解释程度,越接近1越好,接近0说明模型基本没学到什么东西。

接着画两张图,一张是预测值与真实值的折线对比图,一张是散点图加理想对角线:

figure; plot(YTest_raw, 'b-', 'LineWidth', 1.5); hold on; plot(pred_raw, 'r--', 'LineWidth', 1.5); legend('真实值', '预测值'); xlabel('样本序号'); ylabel('预测目标'); title('CNN-BiLSTM回归预测结果对比'); figure; scatter(YTest_raw, pred_raw, 20, 'filled'); hold on; plot([min(YTest_raw) max(YTest_raw)], [min(YTest_raw) max(YTest_raw)], 'k--'); xlabel('真实值'); ylabel('预测值'); title('真实值 vs 预测值散点图');

如果散点大致分布在对角线附近,说明模型的预测非常准确;如果散点明显偏离对角线,比如低值被系统性高估、高值被系统性低估,就要考虑是不是输入特征和目标值之间存在非线性关系,或者窗口长度设置不合适。

5. 完整源码框架与常见问题排查

5.1 完整训练流程源码框架

把上面所有环节拼起来,就是一个可以跑通的完整流程,流程分为:加载数据、归一化、划分数据集、构造滑窗输入、定义网络、配置训练选项、训练、预测、评估。

按这个顺序去组织代码,比东一块西一块写要清晰得多。

实际项目里我习惯把“构造滑窗输入”和“网络定义”封装成函数,这样只需要改数据路径和几个超参数就能套用到新数据集。如果只有一份数据和完整代码,最省事的运行方式就是放在一个脚本里按顺序跑,有问题方便定位。

下面补一个可以直接执行的迷你流程,数据用随机数模拟,用于验证网络结构是否通顺:

% 模拟数据 rng(42); n = 500; X = rand(n, 4); Y = 1.5*X(:,1) + 0.8*X(:,2).^2 + sin(X(:,3)) + 0.1*randn(n,1); % 归一化 [Xnorm, psX] = mapminmax(X', -1, 1); Xnorm = Xnorm'; Ynorm = mapminmax(Y', -1, 1)'; % 划分 splitTrain = 1:round(n*0.7); splitValid = round(n*0.7)+1:round(n*0.85); splitTest = round(n*0.85)+1:n; XTrain = Xnorm(splitTrain, :); XValid = Xnorm(splitValid, :); XTest = Xnorm(splitTest, :); YTrain = Ynorm(splitTrain); YValid = Ynorm(splitValid); YTest = Ynorm(splitTest); % 构造cell格式:将每条样本的特征维度作为序列长度 XTrainCell = num2cell(XTrain', 1); XValidCell = num2cell(XValid', 1); XTestCell = num2cell(XTest', 1); % 定义网络 numFeatures = size(XTrain, 2); layers = [ sequenceInputLayer(numFeatures) convolution1dLayer(3, 8, 'Padding', 'same') batchNormalizationLayer reluLayer bilstmLayer(16, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(16) reluLayer fullyConnectedLayer(1) regressionLayer ]; options = trainingOptions('adam', 'MaxEpochs', 60, 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, 'ValidationData', {XValidCell, YValid}, ... 'Plots', 'training-progress'); net = trainNetwork(XTrainCell, YTrain, layers, options); pred_norm = predict(net, XTestCell); pred_raw = mapminmax('reverse', pred_norm, psY)'; YTest_raw = mapminmax('reverse', YTest', psY)'; r2 = 1 - sum((YTest_raw - pred_raw).^2) / sum((YTest_raw - mean(YTest_raw)).^2); fprintf('R² = %.4f\n', r2);

这个迷你版本不到40行,跑通后你就能明白每个环节的衔接关系。真实项目只需要替换成自己的数据和滑窗函数,结构保持不变。

5.2 踩坑记录与排查技巧

我整理了几个最常见的问题,做成速查表,基本覆盖了新手和老手都会遇到的坎。

问题现象可能原因解决思路
trainNetwork报维度不匹配错误输入cell里矩阵尺寸与sequenceInputLayer期望不一致打印XTrainCell{1}的size,确认是特征数×序列长度
损失曲线不下降,一直在初始值附近学习率太低,或数据未归一化检查mapminmax是否用过;尝试学习率0.001到0.01
训练早期损失直接变成NaN学习率过高,或BiLSTM梯度爆炸降低学习率,打开GradientThreshold设为1
验证误差远大于训练误差过拟合增大dropout到0.3~0.5,减少隐藏单元数或滤波器数
预测值总是比真实值滞后一个台阶时序预测中滑动窗口太短,或模型没捕捉到趋势增大窗口长度,观察滞后是否缓解
GPU显存不足批量大小或输入序列过长减小MiniBatchSize到16或8,减小卷积核通道数
训练结果每次都不一样没有固定随机种子在开头加rng固定种子,关闭GPU训练的可复现选项影响

还有一个容易被忽视的坑:regressionLayer要求Y是列向量还是行向量?MATLAB的trainNetwork对回归输出层的标签格式要求比较严格,很多时候报错就出在这里。稳妥做法是把标签统一整理成列向量,也就是n×1或者cell数组里对应矩阵的单行。如果Y是行向量,在传入之前转置一下。

另外,如果数据量很小,比如只有几百个样本,不要堆大网络。CNN滤波器数量16、BiLSTM隐藏单元16-32就足够了。网络复杂度一大,训练集上可能拟合得很好,但测试集上误差会大得离谱。

6. 个人实操体会与扩展方向

做多输入回归预测这几年,我最大的体会是:调参永远排在数据质量后面。输入特征之间如果存在强共线性,或者有大量无意义的噪声特征,不管CNN-BiLSTM多强大,效果都有限。遇到这种情况,我会先用皮尔逊相关系数挑一遍特征,再做归一化和建模,比直接把所有特征扔进网络要稳定得多。

最后分享一个小技巧:如果训练数据特别少,又没法扩充,可以试试拿CNN-BiLSTM模型的中间层特征出来做简单的线性回归,有时泛化能力反而比端到端训练更好。至于项目里封装好的完整源码,只需要替换成你自己的数据集,把特征列和目标列对好,跑完就可以拿到各项评估指标。网上下载的大部分代码跑不通,原因往往是数据集格式没对齐,只要把上面提到的数据格式检查点挨个过一遍,基本都能解决。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询