简介:一套基于Matlab的CNN-BiLSTM卷积双向长短期记忆神经网络时间序列预测完整源码与数据集,面向计算机、电子信息、数学等专业学生课程设计、期末大作业及毕业设计,也适合时序预测算法研究者参考。压缩包共5个文件,含3个.m源码脚本(模型构建、数据处理与误差计算)、1个.mat数据文件及1个.xlsx风电场预测数据集,整体约760KB,结构紧凑。代码采用参数化编程,方便修改网络结构与超参数,注释详细,运行于Matlab2023及以上版本,可输出MAE、MAPE、MSE、RMSE、R2等多指标评价结果。已有110人学习下载,资源由机器学习之心整理,配套源码与数据可直接运行,便于快速复现CNN-BiLSTM预测流程并在此基础上扩展改进。
1. CNN-BiLSTM 时间序列预测:这套 Matlab 源码能解决什么问题
用 Matlab 做 CNN-BiLSTM 时间序列预测,有个反直觉的结论先说在前面:把一维卷积放在双向 LSTM 前面,效果往往比单独用 LSTM 或单独用卷积都稳,尤其处理风电功率这种波动大、尖峰多的序列。这份资源不是一两个函数文件,而是一条完整链路:data_process.m负责数据预处理,CNNBiLSTM.m负责网络搭建与训练,calc_error.m负责输出 MAE、MAPE、MSE、RMSE、R2 五项指标,配套的风电场预测.xlsx是可直接运行的数据。要交课程设计、期末大作业或毕业设计,拿这套代码跑通、改参数,比从零拼网络省事得多。适合两类人:一类是 Matlab 基础一般、需要完整可运行代码交差的学生;另一类是已经会 LSTM、想看看卷积特征提取到底能给时序预测带来什么变化的研究型用户。下文按「数据怎么准备 → 网络怎么搭 → 参数怎么设 → 坑在哪」的顺序拆。
2. 数据准备:滑动窗口、归一化与训练测试划分
拿到压缩包先别急着双击CNNBiLSTM.m。我跑这类代码的习惯是先把文件清单过一遍,搞清楚谁在哪个环节干活,不然报错都不知道去哪找。这套资源里五个文件配合关系如下:
| 文件 | 职责 |
|---|---|
data_process.m | 读取 Excel、划分训练/测试集、归一化、滑动窗口构造监督样本 |
CNNBiLSTM.m | 主程序:定义网络、设置训练参数、训练并保存模型 |
CNNBiLSTM.mat | 训练好的网络对象与归一化参数,二次复现时可加载,跳过重训 |
calc_error.m | 计算 MAE、MAPE、MSE、RMSE、R2 并绘制预测对比图 |
风电场预测.xlsx | 配套的风电功率时间序列数据 |
作者注释风格偏工程化,变量命名前后一致,顺着data_process.m往下读基本不会卡壳。但要注意一点:这份代码标注的运行环境是 Matlab 2023 及以上,如果你的版本太老,后面第 5 章会讲到具体会踩什么坑。
2.1 一维时序为什么要切成滑动窗口样本
原始的风电功率序列是一列数,网络没法直接拿「一整条序列」去学。常见做法是把一维序列切成「用过去 winSize 个点预测下一个点」的样本对,也就是 sequence-to-one 结构。窗口大小 winSize 是整个数据管线里最值得调的参数:太小,模型看不到趋势;太大,样本数量骤减且训练变慢。风电功率场景里常见取 12 或 24(对应小时粒度的一天/半天周期),工业负荷、交通流这类带明显周期性的序列也类似。
data_process.m里核心切片逻辑大致长这样:
% data_process.m 中滑动窗口构造成 (X, y) 的核心示意 function [X, y] = makeSamples(seq, winSize) n = length(seq); X = zeros(n - winSize, winSize); % 每一行是一个时间窗口 y = zeros(n - winSize, 1); % 窗口之后的那一个点 for i = 1 : n - winSize X(i, :) = seq(i : i + winSize - 1); y(i) = seq(i + winSize); end end逻辑说明:X的每一行是连续winSize个历史值,y是紧跟着窗口的下一个时刻的值。比如窗口长度 24,就是用 1-24 点预测第 25 点,用 2-25 点预测第 26 点,依此类推。循环写法最直观,缺点是数据量大时偏慢,但对几百到几千条的风电序列完全够用,没必要为了省这几毫秒引入额外复杂度。
参数说明:winSize在data_process.m头部定义,我一般会先把它设成数据明显周期长度的整数倍。如果你打开 Excel 发现数据是整点采样的,先设 24 跑一版作为基线,再往 12、48 两个方向试,观察验证集指标变化。
2.2 归一化与划分顺序:这条线决定指标可不可信
时间序列预测里最容易犯的错是把归一化放在切分之前、对整条序列求 min/max。这样做的后果是测试段的数值范围在训练阶段就已经暴露了,属于典型的数据泄露。正确的顺序是:先按时间切出训练段和测试段,只对训练段求 min 和 max,测试段套用同一组 min/max 做变换。
% 顺序划分:时间序列不能随机打乱 trainLen = round(length(raw) * 0.8); trainRaw = raw(1 : trainLen); testRaw = raw(trainLen + 1 : end); % 只从训练段统计归一化参数 minV = min(trainRaw); maxV = max(trainRaw); trainNorm = (trainRaw - minV) / (maxV - minV); testNorm = (testRaw - minV) / (maxV - minV); % 注意:套用训练段的 min/max逻辑说明:第一段是按 8:2 比例沿时间轴切分,不用randperm,时序数据一旦随机打乱,未来信息就会混入训练批次。第二段是归一化,minV、maxV只来自训练段,这保证了测试集的「未来信息」没有任何一条经过训练阶段。所有预测做完后,要把结果乘以(maxV - minV) + minV还原回原始量纲再去算指标,这个动作在第 5 章还会再强调。
参数说明:划分比例不是死的,样本量多、周期完整时可以给训练集更多比例,比如 85%。如果序列短到只有一两百个点,8:2 会让测试集只有几十个样本,指标波动很大,这种情况建议把训练比例提到 9:1,或者改用交叉验证式的时间序列划分。
3. 网络结构:Conv1D 与 BiLSTM 怎么搭、层参数怎么定
3.1 为什么把一维卷积放在 BiLSTM 前面
很多入门者拿到代码会疑惑:LSTM 本身就能学时序,为什么还要在前面叠卷积?这里有两个实际原因。第一,一维卷积相当于在时间轴方向做局部特征提取,卷积核扫过窗口时能捕捉到爬坡、尖峰、平台这类局部模式,相当于给 BiLSTM 做了一次特征筛选,而不是把原始噪声数据直接灌进去。第二,卷积层的感受野有限,天然对相邻点的随机抖动有平滑作用,风电功率这类高频波动序列直接喂 LSTM,噪声会被当成有效信号记住,导致过拟合。
对比来看,纯 LSTM 对长序列的记忆能力强,但对局部形态不敏感;纯 CNN 能提局部特征却无法建模长期双向依赖。CNN-BiLSTM 的组合是让卷积先做局部抽象,BILSTM 再在双向上下文上建模,序列越长、波动越剧烈,这个组合的优势越明显。这也是这份源码选用卷积在前、循环层在后的原因。
3.2 CNN-BiLSTM 网络结构逐层拆解
CNNBiLSTM.m里的网络定义核心部分大致如下:
% CNN-BiLSTM 网络结构定义(CNNBiLSTM.m 核心) layers = [ sequenceInputLayer(1) % 每个样本是一条 1×winSize 的序列 convolution1dLayer(3, 32, 'Padding', 'same') % 一维卷积:核大小 3,32 个滤波器 batchNormalizationLayer % 批归一化:加速收敛 reluLayer % 激活 maxPooling1dLayer(2, 'Stride', 2) % 池化:时间维减半 bilstmLayer(64, 'OutputMode', 'last') % 双向 LSTM,64 个隐藏单元,输出最后时刻 dropoutLayer(0.2) % 丢弃率 0.2,防过拟合 fullyConnectedLayer(1) % 回归输出 regressionLayer ];逻辑说明:sequenceInputLayer负责把每个样本按序列读入;convolution1dLayer(3, 32)用 3 个点的卷积核在时间方向上滑动,输出 32 个通道的特征图;批归一化对每个 mini-batch 做标准化,能明显减少调参时对初始学习率的敏感度;池化层把时间维减半,剪掉冗余信息,也降低后面 BiLSTM 的计算量。bilstmLayer(64)同时看窗口内正向和反向的信息,配合'OutputMode', 'last'输出最后一步的隐状态,因为这里每个样本只对应一个预测值。最后接一层全连接压缩成 1 个数,用回归层算损失。
参数说明:卷积核大小 3 是通用起点,比 5 和 7 更不容易把邻近噪声揉进特征;numFilters32 意味着网络提取 32 种局部模式,数据复杂度高时可以加到 64,但参数量也会翻倍。bilstmLayer的隐藏单元数 64 是这套数据量下的合理值,如果你把学习率调到合适区间后训练损失还是降不动,优先检查的是隐藏单元数而不是层数——多数场景 64 到 128 已经够用,叠两层 BiLSTM 收益不明显但训练时间翻倍。
3.3 一个容易忽略的数据流细节
上面网络里sequenceInputLayer(1)的意思是每个时间步只有 1 个特征。要让 BiLSTM 真正按时间步展开,data_process.m输出的样本需要组织成「每个样本一条长度 winSize 的序列」。常见做法是把 N×winSize 的矩阵转成 cell 数组:
% 将 (N×winSize) 矩阵转成 N 个 cell,每个 cell 是 1×winSize 的序列 XC = num2cell(X, 2)'; % 每个 cell 一行 XC = cellfun(@(x) x(:)', XC, 'UniformOutput', false);逻辑说明:Matlab 里 sequence 输入的标准形式是「特征 × 时间步」的矩阵或 cell 数组。如果不做这一步、直接把 N×winSize 的矩阵喂给sequenceInputLayer(winSize),Matlab 会把它解释成「每个样本只有一个时间步、winSize 个特征」,BiLSTM 的时间维度实际被压成了 1,双向循环退化为一个特征全连接映射。这也是后面第 5 章要重点讲的坑。
4. 训练与多指标评估:从 trainingOptions 到 MAE/RMSE/R2
4.1 trainingOptions:时序预测的训练参数怎么设
网络结构定好后,训练参数是决定指标能不能看的关键。CNNBiLSTM.m里的训练配置一般会包含下面这些字段:
options = trainingOptions('adam', ... 'MaxEpochs', 200, ... 'InitialLearnRate', 0.005, ... 'MiniBatchSize', 64, ... 'GradientThreshold', 1, ... 'Shuffle', 'never', ... % 时序关键:禁止打乱 'ValidationData', {XVal, yVal}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', false);逻辑说明:优化器选 Adam,它自带动量与自适应学习率,对 0.001~0.01 这档初始学习率不敏感,适合这种单序列小样本场景。Shuffle必须设成'never':时间序列一旦在 mini-batch 里被打乱,短期上下文被拆散,而且验证集里的「未来片段」可能混进训练批次,等于人为制造数据泄露。ValidationData用于每个 epoch 的验证监控,ValidationFrequency控制多少轮验证一次,调得太小拖慢训练,太大又看不清验证曲线变化。
参数说明:InitialLearnRate0.005 是风电这类波动序列的常见起点,如果你发现训练损失震荡不降,先降到 0.001;梯度裁剪阈值 1 能挡住个别大梯度把整个网络参数冲飞。MiniBatchSize64 在几百条样本的场景下是安全的,样本总量不足 1000 时可以减到 32,避免最后一个 batch 只有个位数样本导致指标抖动。
4.2 calc_error.m:五个指标各自在看什么
训练完的预测结果需要反归一化后交给calc_error.m做量化评估。核心计算逻辑如下:
function [mae, mape, mse, rmse, r2] = calc_error(yTrue, yPred) e = yTrue - yPred; % 误差向量 mae = mean(abs(e)); % 平均绝对误差 mape = mean(abs(e) ./ abs(yTrue)) * 100; % 平均绝对百分比误差(%) mse = mean(e .^ 2); % 均方误差 rmse = sqrt(mse); % 均方根误差 r2 = 1 - sum(e .^ 2) / sum((yTrue - mean(yTrue)) .^ 2); % 决定系数 end逻辑说明:mae和rmse带有原始量纲,能直观对应到功率数值误差;mape是无量纲百分比,适合和其他数据集的模型对比,但注意当真实值接近 0 时比例会被放大,风电夜间停机时段容易出现个别很大的比值把 MAPE 拉高。r2是决定系数,公式里的分母是真实值的总平方和,也就是「拿均值做预测」的误差水平,当 r2 为负时,说明模型连均值预测都不如,这时候不要谈调参,先回去查数据泄露问题。
| 指标 | 说明 | 场景用途 |
|---|---|---|
| MAE | 平均绝对误差,带量纲 | 直观反映平均偏差多少功率单位 |
| MAPE | 平均绝对百分比误差 | 跨数据集对比的百分比口径 |
| MSE | 均方误差,放大离群误差 | 训练损失函数的选择依据 |
| RMSE | 均方根误差,与原始量纲一致 | 论文里最常报的回归指标 |
| R2 | 决定系数,1 为完美拟合 | 判断模型是否优于均值基线 |
4.3 训练曲线与预测图的判读习惯
训练完成后不要急着把预测图截图放进报告,先看三条曲线:训练 loss、验证 loss、预测对比图。验证 loss 先降后升是过拟合的典型信号,这时先降学习率再加大 dropout,而不是继续加 epoch。预测对比图如果看起来曲线贴得很近,先算一下误差是集中在尖峰段还是平缓段——风电功率的尖峰往往是模型最拉胯的地方,加卷积核数量或增大 winSize 通常会先改善这一段。
保存模型的常见做法是在训练结束后执行save('CNNBiLSTM.mat', 'net', 'minV', 'maxV'),下次直接load进工作区做预测,省掉重新训练的时间。这份资源里的CNNBiLSTM.mat就扮演这个角色,适合做推理演示和答辩现场快速出图。
5. 避坑:数据泄露、反归一化与复现性 5 个常见翻车点
下面是复现 CNN-BiLSTM 时序预测时最容易翻车的五个点,按我排查的顺序整理。
5.1 预测曲线像历史值延迟拷贝
现象:预测曲线几乎是把输入窗口整体往后平移,R2 很高但 MAPE 异常,曲线尖峰全对不上。
原因:最常见的是样本组织方式不对。把 N×winSize 矩阵直接喂给sequenceInputLayer(winSize),BiLSTM 的时间步实际是 1,双向 LSTM 没有真正在时间轴上展开,学到的只是窗口到目标值的静态映射;另一种可能是 winSize 太小,序列自相关性主导了预测。
解决:打开data_process.m检查样本输出格式。如果喂给网络的是普通矩阵,按第 3.3 节的 cell 数组方式组织,让每个样本成为长度为 winSize 的序列。同时把 winSize 拉到能覆盖一个完整周期。
5.2 测试集指标好到离谱,换数据就崩
现象:测试集 R2 到 0.99,RMSE 低得不像话,但把训练好的模型拿到新数据上一测,指标立刻恶化。
原因:归一化的 min/max 是从包含测试段的全序列上统计的,或者归一化之后才切分训练测试集。测试段的数值范围在训练时就已经泄露,指标当然好看。
解决:严格先切分、再对训练段求 min/max,测试段只用训练段的参数变换。这一点没有讨论余地,也是我检查别人代码时第一眼看的位置。
5.3 指标小到离谱或大到没边,先查量纲
现象:MAPE 算出来是 0.02%,RMSE 是 0.005,看起来完美,但和实际物理量一对完全对不上。
原因:指标在归一化后的 0-1 区间上计算,没有反归一化。归一化后的误差当然小,但这个数字没有物理意义。
解决:预测完成后先执行pred = pred * (maxV - minV) + minV还原量纲,再调用calc_error.m。真实风电功率误差在 MW 量级,看到 0.00x 的数字基本可以断定没还原。
5.4 同一份代码两次跑结果不一样
现象:完全相同的参数,连续跑两次,验证集指标能差出几个百分点,提交实验记录时说不清。
原因:trainNetwork的权重随机初始化,加上 Adam 优化时 mini-batch 的随机采样,导致每次训练的落点不同。
解决:训练前加一行rng(0)固定随机种子。这样讨论不同超参的对比实验才公平,否则学习率的影响可能被随机性淹没。
5.5 低版本 Matlab 报错与中文注释乱码
现象:打开.m文件中文注释显示乱码,运行时报bilstmLayerundefined,或者convolution1dLayer找不到。
原因:资源注明运行环境是 Matlab 2023 及以上;低于这个版本,Deep Learning Toolbox 的类函数名可能不可用,老版本编辑器对中文编码的解析也有问题。
解决:优先按说明装 2023 及以上版本;文件乱码时用 Matlab 编辑器打开后另存为 UTF-8 编码;运行前用ver检查 Deep Learning Toolbox 是否安装。
6. 更进一步:多步预测与换数据集时的迁移顺序
这套代码是单步预测:用过去 winSize 个点预测下一个点。实际项目里经常要预测未来 12 小时甚至更远,这时有两种改法。迭代法最简单,把预测值滚动接回窗口:
% 多步预测(迭代法):预测未来 h 步 h = 12; inputWin = XN(end, :); % 取最后一段已知窗口(归一化后) preds = zeros(h, 1); for i = 1 : h p = predict(net, {inputWin}); % 单步预测 preds(i) = p; inputWin = [inputWin(2:end), p]; % 丢弃最老点,接入预测值 end preds = preds * (maxV - minV) + minV; % 反归一化逻辑说明:predict的输出需要在归一化空间内滚动更新,预测步数越长误差累积越明显,12 步以内迭代法够用,再往后建议改成 seq-to-seq 结构让网络一次输出多步。换成其他数据集时,我习惯按这个顺序迁移:先替换 Excel 路径和列号;再按新数据的采样周期重设 winSize;随后确认归一化参数只来自训练段;最后固定rng跑一轮基线,再开始调学习率和隐藏单元数。这套流程走完,指标达不到预期时问题一定出在这四步之外。从那以后我拿到这类时序预测源码,第一件事永远是打开data_process.m看窗口构造和归一化写没写对,而不是先看网络结构——数据线对了,模型才有讨论前提。希望帮到你。
本文还有配套的精品资源,点击获取