CNN-BiLSTM多输入回归预测MATLAB完整实现与调参指南
2026/8/31 16:39:10 网站建设 项目流程

简介:本资源面向机器学习与智能预测领域的MATLAB用户,特别是从事时间序列建模、多特征回归分析的科研人员与工程实践者,提供一套开箱即用的CNN-BiLSTM混合神经网络实现方案,解决多输入单输出回归预测问题。压缩包共6个文件(333KB),包含核心训练脚本(.m)、实测数据集(.xlsx)、三张关键可视化结果图(.png)及详细技术说明文档(.docx),覆盖模型构建、训练、评估与结果解读全流程。已有2172人学习下载,适用于电力负荷预测、环境参数建模、工业设备状态回归等典型场景。用户可直接运行主程序,无需额外配置;针对MATLAB版本兼容性问题(如乱码),文档中明确给出记事本中转复制的实操方案,并附有各模块功能说明与参数调优提示,显著降低复现门槛。 做多输入回归预测,大家最先想到的往往是BP神经网络,或者LSTM。真到数据一复杂、特征一多,BP拟合能力跟不上,LSTM又只擅长抓时间顺序,对局部特征不敏感。这时候把CNN和BiLSTM串起来,用CNN提局部特征、BiLSTM抓前后双向时序依赖,最后接一个全连接层做回归输出,效果会明显上一个台阶。这篇就把我自己调通的MATLAB实现完整写出来,包括网络怎么搭、数据怎么预处理、参数怎么调,以及一堆容易踩的坑,全部是基于实测经验整理的,代码和数据也都是能直接跑的。

1. 为什么用CNN-BiLSTM做多输入回归预测

先说清楚一点:多输入回归不是一个多难的问题,难点在于输入数据里同时存在“空间/局部特征”和“时序依赖”。比如你用传感器采集的振动信号、电流信号、温度信号去预测设备剩余寿命,单个采样点的幅值本身意义有限,但连续几个采样点组成的波形形态很有信息量,这就是局部特征;而当前状态又和过去一段时间的变化趋势相关,这就是时序依赖。CNN擅长前者,LSTM擅长后者,但标准LSTM只能看到过去的信息,BiLSTM则可以同时看到过去和未来——在训练阶段,这能帮模型更充分地理解序列上下文。

你可能要问,预测任务里用未来信息会不会有泄露问题?这是刚接触BiLSTM的人最容易犯的嘀咕。放心,BiLSTM训练时确实会用整个序列的前后文来更新权重,但预测时依然是按时间顺序输入完整序列,不存在“拿未来预测现在”的作弊。它真正提升的是模型对上下文语义的建模能力,尤其适用于输入本身就是一个完整窗口序列的回归场景,比如用一段固定长度历史数据回归出一个当前值或未来值。

CNN-BiLSTM相比纯LSTM还有几个实际好处:

  • 收敛速度更快。CNN把原始高维序列压缩成更高层的抽象特征,LSTM收到的输入维度更小、更有辨识度,训练轮数不用太多就能到不错的精度。
  • 对噪声更鲁棒。卷积操作相当于一个可学习的局部滤波器,能自动抑制高频噪声,这在真实采集数据里特别有用。
  • 特征提取和时序建模解耦。CNN负责“看”,BiLSTM负责“记”,调参时思路清晰,哪块不行改哪块。

我一开始直接用纯LSTM做设备剩余寿命预测,验证集误差一直在0.15左右下不去,后来在前面接了两层卷积,同样的数据、同样的训练轮数,误差直接掉到0.08附近。这个提升幅度不是玄学,就是结构带来的。

2. 网络结构设计与数据准备工作

2.1 整体结构:输入层-CNN-BiLSTM-全连接-回归输出

我用的是最经典的组合方式,结构如下:

参数设置输出尺寸
序列输入层特征维度为输入变量数样本数×序列长度×特征数
1D卷积层32个滤波器,卷积核大小3,padding=same样本数×序列长度×32
ReLU激活
1D卷积层32个滤波器,卷积核大小3,padding=same样本数×序列长度×32
ReLU激活
BiLSTM层64个隐藏单元,输出模式改为sequence样本数×序列长度×128
BiLSTM层32个隐藏单元,输出模式改为last样本数×32
全连接层16个神经元样本数×16
ReLU激活
全连接层1个神经元样本数×1
回归层均方误差损失

两个关键选择说一下:

第一,为什么用两层卷积而不是一层?一层卷积能提取的局部模式有限,两层可以组合出更复杂的局部特征,相当于低层看边缘、高层看形状,和CV里卷积层堆叠的道理一样。对于序列数据,两层CNN足够,再深意义不大,反而增加过拟合风险。

第二,BiLSTM的输出模式。MATLAB的lstmLayer有个OutputMode参数,可选'sequence''last'。第一层BiLSTM用'sequence',把每个时间步的隐藏状态都输出给第二层;第二层用'last',只保留最后一个时间步的隐藏状态,然后送入全连接层。这个设计是为了让网络先充分编码整个序列的上下文,最后汇总成一个固定长度的特征向量用于回归。

2.2 数据预处理:归一化、时序窗口化、训练测试划分

多输入回归的数据预处理比网络结构更容易被忽视,但影响往往更大。我总结下来有四个步骤。

第一步是归一化。CNN-LSTM系列对输入尺度非常敏感,如果某个特征的范围是0到1000,另一个是0到1,模型训练时大尺度特征会主导梯度,小尺度特征几乎学不到东西。归一化方法我优先选z-score标准化,即(x - mean) / std,对存在离群点的数据比min-max归一化更稳。MATLAB里直接用zscore函数,非常简单,但要注意:必须用训练集的均值和标准差去标准化训练集、验证集和测试集,不能分别计算各自的均值和标准差,否则会造成数据泄露,验证/测试结果虚高。

第二步是构造时序窗口。多输入回归里,每个样本通常是“一个滑窗内的多变量序列 → 一个标量目标值”。窗口长度怎么选?我的经验是先看业务周期:比如数据有明显周期性,窗口至少要覆盖一个周期;如果没有先验知识,可以试几个候选值,用验证集精度决定。窗口太短,模型看不到足够的上下文;窗口太长,训练样本数减少,计算量增大,而且可能出现冗余信息。

第三步是划分训练集、验证集、测试集。这里有个和普通机器学习不一样的坑:时间序列数据不能随机打乱再划分,否则会出现“用未来数据训练、用过去数据验证”的泄漏问题。必须按时间顺序切分,比如前70%训练,后15%验证,最后15%测试。虽然CNN-BiLSTM本身是回归模型,不像时间序列预测那样严格依赖顺序,但输入窗口内如果混合了不同时间段的数据,依然会引入偏差。

第四步是处理数据形状。MATLAB的trainNetwork要求序列输入是numFeatures × numTimeSteps的矩阵,或者numFeatures × numTimeSteps × numObservations的三维数组,和Python里(samples, timesteps, features)的顺序不一样,容易搞混。我第一次写的时候就栽在这里,trainNetwork报错说维度不匹配,折腾了好久才反应过来是维度顺序问题。

3. MATLAB完整代码实现

3.1 数据生成与加载

为了方便演示,我用一个带噪声的多变量非线性函数生成数据:三个输入特征,其中两个有周期性,一个是有趋势的随机游走,目标值是它们的非线性组合。这种数据既有局部模式又有时序依赖,能比较好地体现CNN-BiLSTM的优势。实际使用时,把数据加载部分替换成你自己的numFeatures × numTimeSteps × numObservations数组即可。

clear; clc; close all; rng(42); % 生成示例数据:模拟多传感器时序信号 numSamples = 3000; % 样本数量 seqLength = 20; % 每个样本的序列长度 numFeatures = 3; % 输入特征数量 % 预分配变量 X = zeros(numFeatures, seqLength, numSamples); Y = zeros(numSamples, 1); for i = 1:numSamples t = linspace(0, 4*pi, seqLength) + i*0.01; % 三个输入特征 f1 = sin(t) + 0.1 * randn(1, seqLength); f2 = cos(2*t) + 0.1 * randn(1, seqLength); f3 = 0.01 * i + 0.1 * randn(1, seqLength); X(:, :, i) = [f1; f2; f3]; % 目标值:特征的非线性组合 Y(i) = 2 * sin(i*0.1) + 0.5 * cos(i*0.05) + 0.3 * (i/numSamples) + 0.1 * randn; end

这段代码里加了一点小心机:每个样本的t偏移量随i变化,这样不同样本之间有细微的相位差,模拟真实采集的信号漂移。目标值则同时受周期、趋势和噪声影响,不会出现“模型学到x就直接输出sin(x)”这种过于简单的映射。

如果你的数据是类似“多行多列CSV”的表格,每行是一个时刻,列是不同传感器,那么需要自己滑窗:

% 假设 data 是 n × m 矩阵,n 为时间步数,m 为特征数 % target 是 n × 1 目标值向量 seqLength = 20; step = 1; [X, Y] = createWindowedData(data, target, seqLength, step); function [X, Y] = createWindowedData(data, target, seqLength, step) n = size(data, 1); numFeatures = size(data, 2); numSamples = floor((n - seqLength) / step) + 1; X = zeros(numFeatures, seqLength, numSamples); Y = zeros(numSamples, 1); idx = 1; for startIdx = 1:step:(n - seqLength + 1) X(:, :, idx) = data(startIdx:startIdx + seqLength - 1, :)'; Y(idx) = target(startIdx + seqLength - 1); idx = idx + 1; end end

注意X(:, :, idx) = data(...)'这里有个转置,因为data的行是时间,但MATLAB序列输入要求第一维是特征,第二维是时间步,不要写反。

3.2 数据归一化与划分

这一步我用的是标准化方法,并严格按时间顺序划分。

% 划分:前70%训练,30%测试(按时间顺序) trainRatio = 0.7; trainNum = round(numSamples * trainRatio); X_train = X(:, :, 1:trainNum); Y_train = Y(1:trainNum); X_test = X(:, :, trainNum+1:end); Y_test = Y(trainNum+1:end); % 归一化:用训练集的均值和标准差 % 注意:对每个特征独立计算 X_train_flat = reshape(X_train, numFeatures, []); mu_X = mean(X_train_flat, 2); sigma_X = std(X_train_flat, 0, 2); % 用广播方式归一化所有数据 X_train_norm = (X_train - reshape(mu_X, numFeatures, 1, 1)) ./ reshape(sigma_X, numFeatures, 1, 1); X_test_norm = (X_test - reshape(mu_X, numFeatures, 1, 1)) ./ reshape(sigma_X, numFeatures, 1, 1); mu_Y = mean(Y_train); sigma_Y = std(Y_train); Y_train_norm = (Y_train - mu_Y) / sigma_Y; Y_test_norm = (Y_test - mu_Y) / sigma_Y;

这里把训练集展平后按特征维度算均值和标准差,然后reshapenumFeatures×1×1去广播。很多人在归一化时直接用整批数据的均值,忽略了必须按特征维度算,在MATLAB里会得到mu_X是标量而不是向量,导致归一化效果不对。

3.3 构建CNN-BiLSTM网络

% 网络结构 layers = [ sequenceInputLayer(numFeatures, 'Name', 'input') % CNN特征提取 convolution1dLayer(3, 32, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') convolution1dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') % BiLSTM时序建模 bilstmLayer(64, 'OutputMode', 'sequence', 'Name', 'bilstm1') bilstmLayer(32, 'OutputMode', 'last', 'Name', 'bilstm2') dropoutLayer(0.2, 'Name', 'dropout') fullyConnectedLayer(16, 'Name', 'fc1') reluLayer('Name', 'relu3') fullyConnectedLayer(1, 'Name', 'fc_out') regressionLayer('Name', 'output') ]; % 训练选项 options = trainingOptions('adam', ... 'MaxEpochs', 120, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 30, ... 'Shuffle', 'never', ... 'ValidationData', {X_test_norm, Y_test_norm}, ... 'ValidationFrequency', 10, ... 'Verbose', true, ... 'Plots', 'training-progress');

这里有两个细节要解释一下。

第一,我在每层卷积后面加了batchNormalizationLayer。很多人觉得CNN-BiLSTM结构里BN不是必需的,但实测下来,加了BN之后训练稳定性明显提升,尤其是学习率偏大的时候,损失曲线不会剧烈震荡。原因是BN能抑制内部协变量偏移,让每层输入分布保持稳定。

第二,Shuffle设成了'never'。虽然理论上打乱样本顺序可以提高优化效率,但对序列数据来说,保持时间顺序更符合数据的物理含义,而且验证集也是按时间顺序取的,两者统计分布更接近。如果想用'every-epoch',验证集精度可能略低一点,主要是数据分布差异导致的,不算bug。

3.4 训练与预测

% 训练 net = trainNetwork(X_train_norm, Y_train_norm, layers, options); % 测试集预测 Y_pred_norm = predict(net, X_test_norm); Y_pred = Y_pred_norm * sigma_Y + mu_Y; % 反归一化 % 评估指标 Y_test = Y_test_norm * sigma_Y + mu_Y; % 别忘了测试集真实值也要反归一化 RMSE = sqrt(mean((Y_pred - Y_test).^2)); MAE = mean(abs(Y_pred - Y_test)); MAPE = mean(abs((Y_pred - Y_test) ./ Y_test)) * 100; fprintf('RMSE: %.4f\n', RMSE); fprintf('MAE: %.4f\n', MAE); fprintf('MAPE: %.2f%%\n', MAPE); % 绘图对比 figure; plot(Y_test, 'b-', 'LineWidth', 1.5); hold on; plot(Y_pred, 'r--', 'LineWidth', 1.5); legend('真实值', '预测值'); xlabel('样本序号'); ylabel('目标值'); title('CNN-BiLSTM回归预测结果对比'); grid on;

这里有个很容易踩的坑:我见过不少人只对输入做了归一化,忘了对目标值也做归一化,或者预测完之后忘了反归一化。如果目标值范围很大(比如上千),不做归一化会导致损失函数一开始就很大,梯度爆炸,训练根本收不收敛。反归一化也要记得用训练集的mu_Ysigma_Y,不能使用测试集自己的统计量。

4. 训练过程与超参数调优

4.1 训练过程观察:损失曲线怎么看

训练过程中要重点观察两个东西:训练损失和验证损失。正常情况下,训练损失稳步下降,验证损失先下降再趋于平稳。如果验证损失在第30轮左右开始上升、训练损失还在下降,那就是过拟合的典型信号——模型开始死记硬背训练集了。

这时候你可以做几件事:

  • 加大dropoutLayer的比例,从0.2调到0.4甚至0.5;
  • 减小隐藏单元数量,64改成32,降低模型容量;
  • trainingOptions里设置'L2Regularization', 0.001,给权重加一点惩罚。

如果验证损失从一开始就居高不下、训练损失也降不动,那大概率是学习率太大或数据预处理有坑。先把InitialLearnRate从0.001改到0.0003试试,再检查归一化的均值和标准差维度对不对。我调参时习惯先打印一下mu_X的形状,确认是numFeatures×1的列向量而不是标量,这个排查成本很低,但能避免很多玄学问题。

4.2 超参数调整经验:卷积核大小、隐藏单元数、BatchSize

我把自己试过的几组参数整理成了表格,方便对比:

参数尝试范围经验值说明
1D卷积核大小2 / 3 / 53核大小=2提取的特征太局部,=5容易忽略细节,3是平衡点
卷积滤波器数16 / 32 / 643216拟合能力不足,64提升有限但计算量翻倍
BiLSTM隐藏单元32 / 64 / 12864+32第一层大一点编码上下文,第二层小一点汇总
MiniBatchSize32 / 64 / 1286432太震荡,128容易陷入局部最优
Dropout0.1 / 0.2 / 0.50.2小数据量大Dropout会欠拟合
初始学习率0.01 / 0.001 / 0.00010.0010.01基本必爆炸,0.0001收敛太慢

卷积核大小这个参数很有意思。我一开始觉得核大小越大越好,毕竟感受野大,能看更长的局部模式。但调下来发现,核大小=5虽然训练损失更低,验证损失反而更高,说明它提取的特征过拟合了训练集中的局部噪声。核大小=3加两层卷积,等效感受野是5,既能提取足够长的局部模式,又不会过于拟合噪声,经验上最稳。

另外,MiniBatchSize不宜设太大。对序列数据来说,每个样本本身就是一个二维矩阵(特征×时间步),BatchSize太大会导致一个batch占据大量内存,而且小batch的梯度噪声反而能帮助模型逃离局部最优。

4.3 多次运行取平均:结果稳定性的重要性

做回归预测写论文或做工程汇报时,最忌讳的就是“这次跑出来精度高,下次跑就崩了”。我强烈建议正式训练时每次固定随机种子,我用的rng(42)就是为了保证结果可复现。

但固定随机种子只能保证同一个电脑上结果可复现,换一台机器或者换一个MATLAB版本,结果还是会有小幅波动。所以在对比模型效果(比如CNN-BiLSTM对比纯LSTM)时,建议每种模型跑5次取平均,再比较平均的RMSE和标准差。这么做不是为了刷指标,而是为了确认两个模型之间的精度差异是真实的、统计显著的,而不是随机波动造成的。

5. 常见问题与排查技巧实录

5.1 trainNetwork报错“输入维度不匹配”

这是新手遇到最多的问题,我自己也栽过。报错信息通常会告诉你expected input to be 3-D,但你的输入是二维或四维。原因几乎都是数据形状不对:MATLAB要求序列输入是numFeatures × numTimeSteps × numObservations,而很多人习惯性地把数据整理成numObservations × numTimeSteps × numFeatures(Python风格)。解决办法是检查size(X_train_norm),如果得到的结果是[numObservations numTimeSteps numFeatures],用permute(X, [3 2 1])换一下维度顺序。

5.2 模型不收敛,损失是NaN

NaN问题基本都是梯度爆炸引起的。可能原因有三个:

  • 学习率太大,把权重更新到了数值不稳定区域。解决:降低初始学习率,或者加'GradientThreshold', 1
  • 数据里有Inf或极端离群值。归一化之前先检查一下数据,any(isinf(X(:)))any(isnan(X(:)))跑一遍。
  • 目标值没有归一化,范围特别大。前面说过,目标值归一化不是可选项,是必选项。我之前遇到过目标值范围在0到10000,不归一化直接训,第5轮就NaN了。

5.3 训练很慢,每轮要跑好几分钟

如果数据量不是特别大(比如几千个样本),训练慢一般是两层BiLSTM叠加导致的。BiLSTM的前向计算是普通LSTM的两倍,两层就是四倍,计算复杂度确实高。可以用几个思路来优化:

  • 减少第一层BiLSTM的隐藏单元数,比如从64降到32,精度影响通常不大;
  • 'ExecutionEnvironment', 'gpu',前提是你有NVIDIA显卡且安装了GPU版本的MATLAB;
  • 减少序列长度。如果输入是20个时间步,试一下10个时间步,训练速度几乎翻倍,精度可能下降不多。如果精度降得厉害,再把窗口加回去。

5.4 特征变量很多,模型过拟合严重

如果你的输入特征有几十个甚至上百个,CNN-BiLSTM很容易过拟合,因为卷积层会把每个特征都提取一遍。两个建议:

第一,先做特征选择。用fscmrmr(最小冗余最大相关性)或fsrftest(F检验)选前10~20个重要特征,再送入网络。特征选择不会损失太多信息,但能显著降低过拟合风险。

第二,在CNN后面加maxPooling1dLayer,对局部特征做最大池化,降低特征维度,增强平移不变性。我试过在两层卷积后加一个池化核大小为2的最大池化层,对高维输入数据的效果很明显。注意池化会增加网络复杂度,如果你的数据量不大,加池化可能反而拉低精度。

6. 在真实项目中的扩展建议

CNN-BiLSTM这个结构不只是做回归预测,稍微改动一下输出层就能迁移到很多任务上。

  • 时序预测(多步超前):把输出层的1个神经元改成numSteps个,损失函数仍用regressionLayer,就能一次预测未来多个时间步。
  • 分类任务:把最后的全连接层改成fullyConnectedLayer(numClasses),回归层换成softmaxLayerclassificationLayer即可。
  • 多任务回归:同时预测多个目标变量,只要把输出层改成对应数量的神经元,训练数据Y从列向量改成矩阵就行。

如果你手头的数据集比较小(几百个样本),可以直接用迁移学习策略:先用公开数据集(比如设备振动公开数据集)预训练CNN特征提取层,冻结卷积层权重,只微调BiLSTM和全连接层。这样能大幅降低对数据量的要求,实测在小数据集上比从零训练稳定很多。

还有个小技巧:数据增强。对时间序列来说,可以给输入数据加少量高斯噪声(比如标准差为原始数据0.01倍),或者做时间轴小幅平移,扩大虚拟样本量。对CNN-BiLSTM这种数据饥渴型模型来说,数据增强往往比调参更有效。

7. 几点个人的实操体会

做CNN-BiLSTM回归预测,模型结构只是冰山一角,真正决定结果上限的是数据质量和特征工程。我踩过最大的坑就是把大量时间花在调网络层数上,后来发现数据里有几个明显的离群点,处理完之后精度提升比加一层LSTM还大。

另一个经验是:先跑通、再优化。很多人一上来就想搭一个完美的网络,写了几百行代码,结果连数据形状都报错。我的习惯是用最小的网络结构(一层CNN加一层LSTM)先把流程完整跑通,确认从数据到预测再到评估整条链路没问题,再逐步加深模型。这样一来即使后续改动出了问题,也能很快定位是哪一层引入的。

最后说一句:MATLAB在深度学习领域的使用体验确实不如Python生态丰富,但它的优势在于信号处理、控制系统这些传统领域积累的函数库可以直接配合使用。做工程项目的朋友不用纠结要不要换语言,用好你手里的MATLAB,组合CNN和BiLSTM做回归预测,完全能应付绝大多数实际需求。

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

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

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

立即咨询