用ELMAN神经网络预测天然气用气量:MATLAB完整实现
2026/9/17 2:14:42 网站建设 项目流程

简介:ELMAN神经网络属于递归神经网络的一类,通过上下文层保留历史信息,对天然气消费量这类时序数据有较好的拟合能力。本资源以用气量预测为主题,提供完整可运行的MATLAB工程,面向时间序列建模初学者以及需要快速搭建预测模型的开发者。压缩包内共有六个文件,核心包括两个Excel表格,存放历史日用气量与可能影响用气的因素;若干MATLAB源码负责数据读取、归一化、网络构建与训练,另有自动生成的备份文件辅助比对修改。整个资源包仅约290KB,体量轻便,下载后可离线运行与反复调试。目前已有185人浏览学习。通过该案例,读者能够理解从原始数据到最终预测输出的完整链条,学会设置网络层数、神经元数量与训练参数,并借助误差曲线评估模型效果。该工程还可延伸至电力负荷、交通流量等其它时间序列预测场景,适合作为课程设计或入门科研的起点。代码注释清晰,结构分块明确,便于读者逐步调试并替换为自己的数据。

1. 为什么天然气用气量预测偏偏选了ELMAN

做能源调度的人都有一个共识:用气量预测比电价预测更难受。电价有明确的峰谷时段,天然气的消费曲线却同时叠加了日周期、周周期、季节性波动和突发降温带来的脉冲式跳变。传统BP神经网络在这种序列上经常出现同一个毛病——预测值总比真实值慢半拍,因为前馈网络把每个时间点当独立样本处理,完全没有“记住上一刻发生了什么”的能力。这个项目采用ELMAN神经网络来啃这块骨头,本质上是把时间维度显式地建进网络结构里,让隐藏层多出一组承接层节点,专门负责保存上一时刻的隐层状态,再反馈回当前输入。对天然气的日消费序列来说,这种结构就像给普通前馈网络加了一截“短期记忆”,在处理连续几天的用气惯性、寒潮累积效应这类问题上,比BP和普通前馈网络有明显的结构优势。适合谁来用?做城市燃气调度、能源规划、工业园区用气量预估的工程师,以及需要把时间序列预测从理论推到MATLAB可运行结果的在校研究者。整个项目代码完整,数据齐全,从数据读取到训练、预测、画图一条链都是通的,拿来做基线模型再合适不过。

2. 把时间序列变成ELMAN能吃的样本:数据处理与网络结构设计

2.1 原始数据文件结构怎么理解

压缩包里有两个Excel数据文件,附件1.xlsx附件2.xlsx,从常见的数据组织方式来看,这两个文件对应的分别是训练集和测试集,或者一个是原始序列、另一个是带外部变量的扩展表。最稳妥的做法是先读进来看一眼列名。

% 读取训练数据 train_data = readtable('附件1.xlsx'); % 显示前几行,确认列名和数据格式 disp(train_data(1:5, :));

readtable会自动识别表头,输出的是table类型,方便按列名索引。如果数据里第一列是时间戳,第二列是用气量,那后续处理就比较直接;如果还有温度、节假日等额外特征列,则需要单独做特征筛选。多数情况下燃气公司提供的历史数据是类似两列的格式——日期和日用量,少数会附带平均温度、是否为工作日等标记,这些在ELMAN里可以作为额外输入节点。

2.2 数据归一的必要性:ELMAN对输入尺度比BP更敏感

ELMAN的隐藏层激活函数通常是tansig,输出层是purelintansig的输出范围是[-1,1],如果输入数据是日均几十万立方米的用气量,不经过归一化直接丢进去,激活函数会进入饱和区,梯度接近零,训练基本推不动。归一化的标准做法是mapminmax,把数据映射到[-1,1]区间。

% 提取用气量序列,假设第二列是用气量 load_data = train_data{:, 2}; % 归一化到[-1,1] [data_norm, ps] = mapminmax(load_data', -1, 1);

mapminmax返回两个值,第一个是归一化后的数据,第二个是结构体ps,里面保存了原始数据的最小值、最大值和映射公式。后面预测完必须用mapminmax('reverse', output, ps)还原成真实量纲,否则预测结果会停留在[-1,1]区间,没法直接用。整个归一化过程必须在训练集上计算ps,测试集用同一个ps变换,不能各自独立做,否则两个数据集不在同一个尺度空间里,模型等于白训。

2.3 滞后步长选择:构造ELMAN的输入输出样本对

ELMAN虽然内部有反馈,但外部输入仍然需要构造“用前几步预测下一步”的监督样本。滞后步长的选择直接影响预测精度:步长太小,网络看不到足够的序列上下文;步长太大,输入维度膨胀,训练时间变长且容易过拟合。日度天然气数据一般取7到14天,覆盖一周的周期性。

% 构造输入输出样本对 lag = 7; % 用前7天预测第8天 n = length(data_norm); inputs = zeros(lag, n-lag); targets = zeros(1, n-lag); for i = lag+1:n inputs(:, i-lag) = data_norm(i-lag:i-1); % 前7天 targets(i-lag) = data_norm(i); % 第8天 end

这里的inputs是7行乘N列的矩阵,每一列是一个样本,包含连续7天的归一化用气量;targets是对应的第8天真实值。MATLAB的神经网络工具箱默认接受“样本按列排列”的格式,行数是输入维度,列数是样本数量,排列顺序反了会直接报维度错误。滞后步长选7的好处是一周的周期性被完整包含进去,周一和周一之间、周末和周末之间的模式可以对齐。

2.4 承接层尺寸对网络记忆容量的影响

ELMAN的承接层(context layer)节点数量和隐藏层节点数量相等,它保存的是隐藏层上一时刻的输出,并在当前时刻作为额外的输入重新进入隐藏层。承接层的存在让网络在t时刻的输出不仅依赖当前输入,还间接依赖t-1、t-2时刻的信息。隐藏层节点数决定了这个记忆通道的宽度——节点太少,记忆容量不够,复杂序列模式学不进去;节点太多,参数量骤增,训练数据量不够就容易记住噪声。

MATLAB中创建ELMAN网络的标准代码是newelm,但注意R2010a之后的版本已经用feedforwardnet和自定义网络代替。当前主流的写法是:

% 创建ELMAN网络 net = newelm(minmax(inputs), [10, 1], {'tansig', 'purelin'}, 'traingdx');

第一个参数是输入数据的范围矩阵,用于初始化权值;第二个参数[10, 1]表示隐藏层10个节点、输出层1个节点;第三个参数是两个层的激活函数;traingdx是带动量项和自适应学习率的梯度下降法,是ELMAN训练的常用选择。隐藏层10个节点是一个合理的起点,对于日度天然气数据来说,序列复杂度不高,10个节点已经能捕获基本的用气模式;如果换成小时级数据或者包含多个外部变量的场景,则需要适当上调到15到20个。

3. MATLAB代码完整跑通:从main.mmain1.m的工程实现

3.1 主脚本main.m内部的典型流程拆解

main.m是这个项目的核心脚本,打开后能看到一个完整的ELMAN预测流程。整个流程分成五个步骤:读数据、构造样本、建网络、训练、预测画图。实际工程中我习惯在这五个步骤之间加disp输出,方便追踪每一步的执行状态,避免脚本跑了几分钟还不知道卡在哪一步。

%% 加载数据 raw_data = readmatrix('附件1.xlsx'); % 读取数值矩阵 flow = raw_data(:, 2); % 取用气量列 %% 归一化 [flow_norm, ps] = mapminmax(flow', -1, 1); %% 构造训练样本 lag = 7; [train_x, train_y] = create_sequence(flow_norm, lag); % 自定义函数,构造输入输出对 %% 创建ELMAN网络 net = newelm(minmax(train_x), [12, 1], {'tansig', 'purelin'}, 'traingdx'); net.trainParam.epochs = 1000; % 最大迭代次数 net.trainParam.goal = 1e-5; % 目标误差 net.trainParam.lr = 0.01; % 学习率 %% 训练 [net, tr] = train(net, train_x, train_y); %% 测试与反归一化 test_raw = readmatrix('附件2.xlsx'); test_flow = test_raw(:, 2); [test_norm] = mapminmax('apply', test_flow', ps); % 用训练集的ps做归一化 test_x = create_sequence(test_norm, lag); pred_norm = sim(net, test_x); % 预测 pred = mapminmax('reverse', pred_norm, ps); % 反归一化

readmatrix是R2019a之后引入的函数,比xlsread更快,处理纯数值表格时优先使用。mapminmax('apply', data, ps)的作用是使用训练阶段保存的归一化参数ps来转换新数据,这样测试集和训练集就在同一个映射空间内。最关键的一句是sim(net, test_x)——在MATLAB神经网络工具箱里,训练和预测是分开的,train函数负责更新权重,sim函数负责前向计算输出,这两个不能混用。如果预测时序列构造成本比训练集少了一步,输出长度会比真实值多或少,画图时会出现数组维度不匹配的报错。

3.2main1.m.asv文件是干什么用的

.asv是MATLAB的自动保存文件。当你编辑脚本但没有手动保存时,MATLAB会每隔几分钟自动生成一个.asv备份。main.asvmain1.asv分别对应main.mmain1.m的上一个版本。如果主脚本改坏了,直接重命名.asv.m就能找回改之前的版本。

main1.mmain.m的差异,常见做法有两种可能:一种是main.m做的是单步预测(用前7天预测下一天),main1.m做的是多步滚动预测(把预测值当输入,继续往后推);另一种是main.m用的是历史数据对比,main1.m用的是未来预测。至于具体是哪一种,跑一下看预测步长就能判断。一般来说,main.m对应的是论文里的主实验结果,main1.m是附加实验或者对比实验。

3.3create_sequence函数的实现细节

上面代码里用到了自定义函数create_sequence,这个函数在MATLAB里需要单独存成一个.m文件,或者直接内嵌在主脚本末尾。写成单独文件的好处是可以同时被main.mmain1.m调用,避免两段重复代码不一致。

function [inputs, targets] = create_sequence(data, lag) % CREATE_SEQUENCE 构造ELMAN训练样本 % 输入: data - 归一化后的序列(1xN) % lag - 滞后步长 % 输出: inputs - lag x (N-lag) 矩阵 % targets - 1 x (N-lag) 向量 n = length(data); inputs = zeros(lag, n-lag); targets = zeros(1, n-lag); for i = lag+1:n inputs(:, i-lag) = data(i-lag:i-1); targets(i-lag) = data(i); end end

这段代码的核心逻辑在循环体里:data(i-lag:i-1)取的是当前时刻之前的lag个值,构成输入向量;data(i)是当前时刻的真实值,构成目标值。每次循环窗口向后滑动一个时间点,这样构造出的相邻样本之间有大量重叠信息,这是时间序列预测的标准做法。需要注意这种构造方式下各样本不是独立同分布的,前向网络会因此存在一定的信息冗余,但对ELMAN这类循环网络来说,这恰好模拟了时间轴上的连续观测场景。

3.4 训练参数设置的建议值和调整方向

newelm创建网络后,训练参数一般需要手调。epochs设1000次只是上限,实际训练里如果误差在几百次后就稳定了,再多的迭代只会浪费算力。判断收敛与否的关键指标有两个:tr.best_epoch表示验证误差最小的迭代轮次,tr.best_perf是对应的最小误差值。如果best_epoch远小于epochs,说明收敛得很快,可以适当减少迭代次数;如果best_epoch接近1000,说明还没收敛完,需要增大迭代次数或者调整学习率。

lr学习率的关键性最大。traingdx自带自适应学习率调整机制,初始学习率设在0.01到0.05之间通常问题不大。但如果训练过程中误差出现了剧烈震荡,那就是学习率偏大,权重在最优解附近来回摆动;如果误差下降像蜗牛爬,500个epoch才降了一点点,说明学习率太小或者隐藏层节点数不够。还有一种常见情况是误差函数前期下降很快、后期完全不动,这通常是因为网络陷入了局部极小值,改一下随机种子或换traingdm(带动量的梯度下降)可以缓解。

4. 模型评估与预测效果核验:误差指标和可视化对比

4.1 评估指标计算:MSE、RMSE、MAE、R² 一个都不能少

模型训练完,评估是躲不掉的步骤。写论文、做汇报,光贴一张预测对比图远远不够。均方误差(MSE)对大误差值更敏感,平均绝对误差(MAE)更贴近真实感知误差,决定系数R²反映模型对序列方差的解释能力。这组指标在MATLAB里几行代码就能算完。

% 计算误差指标 err = pred - real; % pred是预测值,real是真实值,注意先反归一化再计算 mse_val = mean(err.^2); rmse_val = sqrt(mse_val); mae_val = mean(abs(err)); ss_res = sum(err.^2); ss_tot = sum((real - mean(real)).^2); r2 = 1 - ss_res / ss_tot; fprintf('MSE: %.4f\nRMSE: %.4f\nMAE: %.4f\nR²: %.4f\n', mse_val, rmse_val, mae_val, r2);

指标的计算顺序很关键——必须在反归一化之后算,因为在[-1,1]空间里算误差和在实际量纲下算误差是完全不同的尺度。fprintf在MATLAB里的%.4f是格式化输出,\n是换行。R²越接近1说明模型解释能力越强,但一个隐藏层10个节点的ELMAN如果R²能在0.9以上,就已经是不错的水平;如果R²低于0.7,优先检查数据预处理和数据切分是不是出了问题,而不是急着堆网络深度。这一点对天然气这类有强周期性的序列来说特别重要。

4.2 预测曲线和误差曲线的MATLAB绘图实现

可视化对比是判断模型质量最直观的方式。绘图代码需要输出两张图:一张是预测值与真实值随时间变化的对比曲线,另一张是残差分布图。残差图中如果出现明显的时间段性波动,说明模型没有捕获到某些周期性特征。

%% 预测值与真实值对比 t = 1:length(real); figure; plot(t, real, 'b-', 'LineWidth', 1.5); hold on; plot(t, pred, 'r--', 'LineWidth', 1.5); legend('真实值', '预测值'); xlabel('时间(天)'); ylabel('用气量'); title('ELMAN神经网络用气量预测结果'); grid on; %% 残差图 figure; plot(t, err, 'k-', 'LineWidth', 1); xlabel('时间(天)'); ylabel('预测误差'); title('预测残差分布'); grid on;

hold on的作用是让两条曲线画在同一个坐标系里,不加这个命令,第二个plot会覆盖第一个图。残差图的观察重点是:残差是否围绕零轴均匀分布,如果某一段连续时间的残差全是正值或全是负值,说明模型在该时间段存在系统性偏差。比如冬天用气高峰期的预测残差如果是系统性负值,说明模型对寒潮期间的用气量变化反应不够快,这时需要把滞后步长调大,或者引入温度特征作为额外输入。

4.3 模型过拟合的快速判断方法

ELMAN是递归网络,参数量相对不大,但数据量太小时照样过拟合。快速判断方法是比较训练集误差和测试集误差的差距:训练集MSE远小于测试集MSE,基本可以断定过拟合。另一个更直观的判断看训练过程的收敛曲线——train函数会自动弹出一个训练窗口,里面有三条曲线(训练误差、验证误差、测试误差),如果验证误差在某个迭代点之后开始上升而训练误差继续下降,这就是典型的过拟合信号,此时应该停止训练,将epochs截断在tr.best_epoch的位置。

天然气日度数据的样本量通常在几百到几千条之间,对于10个隐藏层节点的ELMAN来说属于中等规模数据。如果过拟合现象严重,第一选择不是加正则化,而是减少隐藏层节点数或者增大滞后步长——这两个操作分别降低了模型的记忆容量和输入复杂度,比L2正则化更直接有效。

4.4 两个预测文件的对比场景:单步预测与滚动预测

main.mmain1.m如果分别对应单步预测和滚动预测,那么在评估方式上要注意一个关键区别:单步预测的每个输入都是真实的历史值,误差不会向后传播;滚动预测是把前一步的预测值当作输入继续预测下一步,误差会随着步数增加而累积。因此滚动预测的误差曲线通常呈现喇叭口形状——时间越往后,误差越大。

%% 滚动预测示例(main1.m的核心逻辑) steps = length(test_flow) - lag; rolling_input = test_norm(1:lag)'; % 初始输入为前lag个真实值 rolling_pred = zeros(1, steps); for i = 1:steps p = sim(net, rolling_input); % 单步预测 rolling_pred(i) = p; % 保存预测结果 rolling_input = [rolling_input(2:end), p]; % 滚动窗口:丢掉最老的值,加入预测值 end

滚动预测的核心在窗口更新那一行:rolling_input(2:end)去掉了窗口里最早的一个值,然后拼接上最新的预测值p。这模拟的是真实业务中“没有未来数据可用”的场景。如果滚动预测的衰减速度很快(预测5步之后误差就已经不可接受),说明ELMAN的递归深度不足以捕获该序列的长程依赖,此时可以考虑换成NARX网络或者LSTM。

5. 把预测结果接进业务:可视化存档与模型更新的工程化技巧

模型能在MATLAB里跑通只是第一步,真正要把它用起来,得让预测结果能以标准格式输出、存档、并在新数据到达时自动更新模型。这一章把预测从“实验室脚本”推到“可持续运行的预测模块”层面。

预测结果最标准的输出格式是Excel表格,保留两个关键列:预测日期和预测用气量。MATLAB的writetable函数一行代码就能完成数据落盘。

% 生成预测结果表 pred_table = table(dates, pred', 'VariableNames', {'Date', 'PredictedFlow'}); writetable(pred_table, 'prediction_output.xlsx');

VariableNames指定的列名会直接作为Excel的表头,后续做报表或者对接其他系统时可以直接读取。注意pred在MATLAB里可能是行向量,写表前要转置成列向量,否则输出维度对不上。日期列建议用datetime类型存储,后面接任何分析工具都不会出时区或格式问题。

关于模型更新的策略,工程上常见的做法是每周用最新数据重新训练一次,而不是每次预测前都从头训练。重训的触发条件可以是一个固定时间表(比如每周日凌晨跑一次),也可以设一个误差阈值——当本周预测的平均绝对误差超过某个警戒值时自动触发重训。重训时保留原有训练参数作为初始值,用新数据继续迭代50-100轮,这样模型能平滑过渡到新的数据分布,避免从一个随机初始状态重新开始而导致的预测波动。

还有一个容易踩的坑是文件路径的管理。代码里附件1.xlsx是中文文件名,MATLAB在Windows下读取没问题,但在Linux服务器上跑就很容易因为编码问题找不到文件。建议代码里统一用英文文件名,或者加一段路径修正逻辑来判断当前操作系统。这部分虽然不起眼,但往往决定了预测脚本能不能稳定地在生产环境里每周定时执行,而不是每次都要人盯着手动改路径。

最后提一下ELMAN的自动调参方向,这段思路在跑不同数据集时很实用。隐藏层节点数先取输入维度的1.5到2倍试一轮,记录训练集和测试集的RMSE;然后往上和往下各调一档,对比三次结果取最优。学习率在0.005到0.05之间按对数间隔扫三四个点。一次完整实验的总耗时控制在几分钟量级,批量跑也不心疼。这些参数在天然气日预测场景下就是足够好的基线,后续如果换了数据集,这套流程也能快速迁移。

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

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

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

立即咨询