ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB+Elman神经网络:Excel时间序列预测完整流程

MATLAB+Elman神经网络:Excel时间序列预测完整流程 直接上干货。手里有一份Excel表格第一列是时间序列后面几列是特征数据想用MATLAB训练一个Elman神经网络做预测。这个需求我在实际项目里碰到过很多次尤其是一些做设备状态预测、销量预估、能耗分析的朋友数据都存在Excel里没有复杂数据库也不想起Python那套环境就指着MATLAB一把梭。这篇文章我就把完整流程拆开讲清楚脚本怎么读Excel、怎么把原始数据变成Elman网络能吃的样本、怎么训练和调参、怎么评估预测效果最后再把我踩过的坑一个一个列出来。这篇文章特别适合三类人一是准备做时间序列预测但更习惯MATLAB的工程师二是课程设计或毕业设计需要“Excel数据读取神经网络预测”完整案例的同学三是手里有大量历史报表、想快速验证预测方案可行性的业务分析人员。不用懂太多理论基础跟着脚本走就能把预测跑起来。1. 整体思路与方案选型1.1 为什么选Elman神经网络而不是普通BP网络先说结论预测时间序列Elman比普通BP网络更合适。原因在于Elman是一种递归神经网络它在隐藏层旁边多了一组“上下文单元”会把上一时刻隐藏层的输出缓存下来再作为当前时刻的输入一起送进网络。说白了网络内部自带了一个“记忆”能感知到数据在时间上的先后关系。普通BP网络是前馈结构它假设每个样本之间是独立的输入和输出之间是一个静态映射。但时间序列不一样今天的气温、销量、负载值往往和昨天、前天甚至上周的数据有关。你用BP网络硬套相当于让一个没有记忆的人做连续推理他每次只能看到当前这一步前面发生过什么全忘了。而Elman网络会把前面几步的“印象”保留下来再结合当前特征做判断。用一个生活化的类比BP像临时工每次接到任务都从零开始Elman像老师傅手里有个工作笔记能参考以往经验。所以在MATLAB里做时间序列预测Elman是一个性价比很高的选择。它比LSTM结构简单训练速度快数据量不大时效果也不差比BP网络又更贴合时间序列的本质。如果你要预测的对象是负荷、温度、销量、流量这类有明显时间依赖的数据Elman值得优先试。1.2 为什么用Excel加脚本而不是写死数据很多人一开始图省事直接在MATLAB代码里写一个数组比如data [1,2,3,4,5]。数据量小的时候无所谓但一旦数据变成几百行、几千行或者业务方隔几天就要更新一次数据这种写法就是灾难。Excel在这里最大的价值是“可维护性”。业务人员可以像平时填报表一样维护数据不需要懂MATLAB你只需要保证列的顺序和约定脚本一次写好之后每次跑预测只改文件路径或者文件名。这样就把“数据更新”和“模型训练”彻底解耦了。脚本的作用则是把这条流水线自动化读取、清洗、建模、评估、出图一键完成。整个流程概括起来就是Excel作为数据源脚本作为流水线Elman网络作为预测引擎。三个角色各司其职缺一不可。这样的结构还有一个好处以后想换模型比如换成LSTM或者随机森林只需要替换中间的训练部分前面的数据读取和后面的评估代码都可以复用。1.3 整体流程拆解我习惯把整个流程分成七步每一步都有明确的输入和输出准备Excel文件约定好列结构第一列是时间序列后面各列是特征数据。用MATLAB脚本读取Excel检查数据完整性处理缺失值。用滑动窗口把原始时间序列构造成“输入样本目标值”的监督学习格式。对数据进行归一化并按照时间顺序划分训练集和测试集。构建Elman神经网络新版MATLAB用layrecnet实现设置隐层节点数和训练参数。训练网络保存训练好的模型。对测试集做预测反归一化计算误差指标绘图对比。下面的章节就按这个顺序一步步来。2. 数据准备与脚本读取Excel2.1 Excel表结构怎么设计最省心先说表格结构。既然标题里已经说明了第一列放时间序列其他列放特征数据。那我建议你按下表这种格式设计时间戳特征1温度特征2湿度目标值销量2024-01-0112.5433202024-01-0213.2473562024-01-0311.851289第一列可以是日期也可以是纯序号1、2、3。如果第一列是日期脚本里可能不会直接把它当作数值特征使用更多是作为画图时的横轴如果你想让日期参与计算可以先用datenum转成序列号但一般情况下不建议这么做因为日期本身并不包含太多预测信息。后面列可以是特征也可以是目标变量。如果你要预测的是某个具体指标建议把目标值单独放在最后一列这样读取之后操作起来最清楚。我在实际中经常遇到一种情况有人把Excel文件第一行写了表头有人没写有人的第一列是日期有人第一列就是序号还有人的Excel里混着空的单元格、甚至中文备注。这些不统一的地方才是脚本读取时最容易翻车的地方。所以我的建议是拿到的Excel先手动看一眼前几行做到心里有数再决定用readtable还是readmatrix以及读取后要不要跳过表头。2.2 用脚本读取Excel的两种写法MATLAB读取Excel的常用函数有readtable、readmatrix和xlsread。xlsread比较老新版MATLAB不推荐用虽然还能跑但读取速度和处理灵活性都一般。我主要说前两个。如果你的Excel第一行是表头我优先推荐readtable。它会自动把第一行识别成变量名读进来的是一个table对象用列名访问数据非常直观% 读取Excel dataTable readtable(data.xlsx); % 查看前几行确认列名和数据格式 head(dataTable) % 提取数值部分忽略第一列时间把后面的特征列转为矩阵 % 假设第一列叫时间戳最后一列叫销量中间是特征 features dataTable{:, 2:end-1}; % 特征列 target dataTable{:, end}; % 目标列 timeStr dataTable{:, 1}; % 时间列画图用如果你的Excel里全是数值没有表头或者第一行就是数据那直接用readmatrix更省事% 读取纯数值Excel第一列是时间序号其他列是特征 data readmatrix(data.xlsx); % 查看数据的尺寸和大致内容 size(data) disp(data(1:5, :)) % 拆分数据 timeSeq data(:, 1); % 第一列时间 featMat data(:, 2:end); % 后面的列特征或目标这里有个细节要特别注意readmatrix会把所有内容按数值矩阵读入如果Excel里有中文表头或混合文本读取时会直接报错或者把那一列变成NaN。所以用readmatrix的前提是数据必须干净、纯数值。相比之下readtable容错性更好遇到非数值内容会填入NaN不会轻易中断运行。还有一个使用频率极高的坑文件路径。很多人把Excel放在桌面上MATLAB当前工作目录还在默认的matlab文件夹里一运行就报“无法读取文件”。解决办法很简单要么把Excel复制到当前目录要么用绝对路径% 绝对路径方式 data readmatrix(C:\Users\yourname\Desktop\data.xlsx);2.3 数据清洗与滑动窗口样本构造Excel数据不能指望它天生完美。最常见的几种问题缺失值、异常值、重复值。在喂给神经网络之前一定要处理。处理缺失值我一般用fillmissing。时间序列的缺失值不要直接删掉因为删掉会破坏时间连续性更好的办法是插值% 对特征矩阵按列填充缺失值线性插值 featMat fillmissing(featMat, linear, 1);如果是明显的异常跳变比如温度突然从20度跳到80度可以用移动中位数或者自己设定阈值去清洗。这一步看起来很基础但直接影响预测效果。神经网络不挑数据你喂什么它学什么脏数据学出来的只能是脏模型。清洗完之后就到了最关键的环节把原始时间序列构造成神经网络能训练的样本对。Elman虽然能记住历史信息但并不意味着你直接把一条序列丢给它它就能神奇地预测未来。标准的做法是“滑动窗口”用过去的若干个时刻预测下一个时刻。举个例子假设你只有目标值这一列数据序列是y1, y2, y3, y4, y5, ...。如果窗口大小设为3那么训练样本就是输入[y1, y2, y3]输出y4输入[y2, y3, y4]输出y5输入[y3, y4, y5]输出y6这就把时间序列预测问题转化成了标准的监督学习问题。如果你Excel里还有额外特征比如温度、湿度也可以把对应的历史值拼到输入向量里让模型有更多信息可用。窗口大小lag的选择是有讲究的。太小模型看不到足够的趋势太大输入维度暴涨训练慢还容易过拟合。我一般先试3、5、7再根据效果调整。如果你有足够多的数据还可以用自相关函数autocorr看几步滞后相关性最强再决定窗口大小。构造样本的脚本如下function [X, Y] createSlidingWindow(data, lag) % data: 列向量目标序列 % lag: 窗口大小 n length(data); X zeros(n - lag, lag); Y zeros(n - lag, 1); for i 1 : n - lag X(i, :) data(i : i lag - 1); Y(i, :) data(i lag); end end如果你有多个特征可以把每个特征的滞后值横向拼接。拼接后的每一行就是模型的一个输入样本每一行对应一个目标值。3. Elman神经网络搭建与训练3.1 Elman网络结构在MATLAB里的实现方式很多教程会让你直接用newelm但这里必须提醒你新版MATLAB的神经网络工具箱已经把这个函数移除了。你在新版本里运行newelm大概率会直接报错“未定义函数或变量”。这是因为MathWorks在R2010a之后重构了神经网络工具箱旧版API大量废弃。如果你打开的是新版本MATLAB推荐用layrecnet它是官方提供的层递归网络核心思想和Elman完全一致隐藏层的输出经过一个延迟器反馈到隐藏层输入形成短期记忆。layrecnet的基本用法如下% 构建一个带反馈的层递归网络 hiddenSizes 12; % 隐层节点数 net layrecnet(1, hiddenSizes, tansig);第一个参数1表示反馈延迟为1步也就是说网络会记住上一个时刻的隐层状态。这正好对应Elman网络经典的“上下文单元”。隐层激活函数用tansig输出层默认是线性函数。如果你真想手动搭建一个Elman结构也可以用network函数自己一个个节点接但新手不建议这么做直接用layrecnet就够了底层原理一样调试起来还省心。训练之前还要注意一个非常关键的问题layrecnet默认的数据划分方式是随机划分dividerand。时间序列最忌讳随机划分因为那等于让模型偷看未来的数据。训练时看到的“测试集”如果混在训练集之间评估结果会虚假地好看但真正做预测时立刻现原形。所以必须把划分方式改成按顺序划分net.divideFcn divideblock;这句代码的意思是把样本按顺序切块前一段训练、后一段测试保持时间上的因果关系。3.2 数据归一化与训练/测试集划分数据不进模型之前必须先归一化。神经网络对输入数据的尺度很敏感如果特征数值范围差异太大比如温度是十几、销量是几百、有的特征可能是零点几模型训练时大数值的特征会主导梯度更新小数值特征等于被淹没了。归一化我用mapminmax把数据缩放到[-1, 1]区间% X为输入矩阵每行是一个样本每列是一个特征 [Xnorm, Xps] mapminmax(X_train, -1, 1); [Ynorm, Yps] mapminmax(Y_train, -1, 1); % 注意mapminmax默认按行处理所以这里要转置 % 训练完成后反归一化要用训练时保存的ps结构体这里有一个很多人容易踩的坑归一化参数只能从训练集上计算然后用同一套参数去变换测试集。千万不要对训练集和测试集分别做mapminmax因为那样测试集的分布信息就泄露到模型评估里了评估结果失真。正确做法是把训练集的Xps和Yps保存下来后面测试集预测完再用它反归一化。训练/测试集的划分时间序列必须按时间顺序来。比如数据一共1000个样本可以取前800个训练后200个测试。不能用randperm打乱。这一点我在实际评审别人代码时见过太多次很多同学习惯性地把分类问题的随机划分带到时间序列里结果模型表现虚高落地一测就崩。% 按时间顺序划分 trainLen floor(size(X, 1) * 0.8); X_train X(1:trainLen, :); Y_train Y(1:trainLen, :); X_test X(trainLen1:end, :); Y_test Y(trainLen1:end, :);3.3 模型参数设置与训练实操数据准备好网络选好接下来就是训练。下面给出一段完整可复现的脚本涵盖读取Excel、构造样本、归一化、训练和保存模型%% 1.读取Excel数据 data readmatrix(data.xlsx); values data(:, 2); % 假设第二列是目标值第一列是时间 feat data(:, 3:end); % 如果有其他特征从第三列开始 %% 2.数据清洗 values fillmissing(values, linear); %% 3.滑动窗口构造样本 lag 5; [X, Y] createSlidingWindow(values, lag); % 如果有其他特征也可以拼进来 % X [X, feat(lag1:end, :)]; %% 4.划分训练集和测试集按时间顺序 trainLen floor(length(Y) * 0.8); X_train X(1:trainLen, :); Y_train Y(1:trainLen, :); X_test X(trainLen1:end, :); Y_test Y(trainLen1:end, :); %% 5.归一化 [Xnorm, Xps] mapminmax(X_train, -1, 1); [Ynorm, Yps] mapminmax(Y_train, -1, 1); Xnorm Xnorm; Ynorm Ynorm; % 注意测试集也用训练集的ps做变换 Xtest_norm mapminmax(apply, X_test, Xps); Xtest_norm Xtest_norm; %% 6.构建Elman网络layrecnet hiddenSizes 15; net layrecnet(1, hiddenSizes, tansig); net.trainFcn trainlm; % Levenberg-Marquardt训练算法 net.trainParam.epochs 500; % 最大迭代次数 net.trainParam.goal 1e-5; % 目标误差 net.trainParam.min_grad 1e-6; net.divideFcn divideblock; % 保持时间顺序划分 %% 7.训练网络 [net, tr] train(net, Xnorm, Ynorm); %% 8.保存模型 save(elman_model.mat, net, Xps, Yps, lag);训练函数我首选trainlmLevenberg-Marquardt它收敛快、精度高对小规模数据非常友好。但要注意它非常吃内存如果样本量很大比如几万条trainlm可能让内存直接爆掉这时候可以换成trainscg量化共轭梯度法内存占用小速度也不慢。隐层节点数也是一个需要反复试的参数。节点太少网络表达能力不够欠拟合节点太多模型把训练集的噪声都背下来了测试集上表现得一塌糊涂。我给的经验是先试12到20个节点看训练误差和测试误差的差距。如果差距明显说明过拟合了减小节点数或者把net.trainParam.mu调大一点增加正则化效果。没有万能参数只有反复对比。4. 预测效果评估与可视化4.1 预测结果的反归一化训练完成之后拿测试集输入去预测得到的是归一化之后的输出必须把它还原成原始量纲才能和真实值做对比。反归一化的时候一定要使用训练时保存下来的Yps参数%% 测试集预测 Y_pred_norm sim(net, Xtest_norm); %% 反归一化 Y_pred mapminmax(reverse, Y_pred_norm, Yps); Y_pred Y_pred; % 真实的测试目标值之前没有归一化过 Y_true Y_test;反归一化这一步看着简单实则是评估环节里最容易出错的点。有人图省事直接对预测值再调一次mapminmax那等于把预测结果重新映射到一个完全不同的尺度数值全乱了。记住归一化是一次性的反归一化用的必须是同一个ps结构体。4.2 用RMSE、MAE、MAPE三种指标评估光看曲线不好量化模型好坏还需要数值指标。我常用三个RMSE均方根误差、MAE平均绝对误差、MAPE平均绝对百分比误差。它们的计算方式如下err Y_true - Y_pred; RMSE sqrt(mean(err.^2)); MAE mean(abs(err)); MAPE mean(abs(err ./ Y_true)) * 100; fprintf(RMSE: %.4f\n, RMSE); fprintf(MAE: %.4f\n, MAE); fprintf(MAPE: %.2f%%\n, MAPE);三个指标各有侧重点。RMSE对大误差更敏感因为它先平方再开方一个特别离谱的预测会把RMSE拉得很高适合用来发现“严重偏差”MAE是平均绝对误差更直观量纲和原始数据一致MAPE是百分比误差方便跨数据集对比但它的致命弱点是当真实值接近0时MAPE会直接变成无穷大。如果你的目标值本身就是小数值或者可能为0建议改用SMAPE或者只报告RMSE和MAE。我自己做评估时一般先看RMSE再看MAPE两个都合理才觉得模型靠谱。举个例子假设销量预测的真实值和预测值对比下来RMSE在30左右、MAPE在8%以内那这个模型就有实用价值了。如果需要更全面的拟合优度还可以算一下R²决定系数也就是模型解释了多大比例的真实波动SS_res sum((Y_true - Y_pred).^2); SS_tot sum((Y_true - mean(Y_true)).^2); R2 1 - SS_res / SS_tot;R²越接近1说明拟合效果越好一般超过0.9就可以接受。4.3 绘制预测对比图数字指标只能给出一个总体印象真正判断模型好坏还得看图。把训练集和测试集的真实值画成一条线再把预测值叠加上去一眼就能看出模型在趋势拐点处是不是跟得上。figure; plot(1:length(Y_true), Y_true, b-, LineWidth, 1.5); hold on; plot(1:length(Y_true), Y_pred, r--, LineWidth, 1.5); legend(真实值, 预测值); xlabel(样本序号); ylabel(目标值); title(Elman神经网络测试集预测结果对比); grid on;如果你的Excel第一列是日期还可以把日期作为横轴画出来实用性更强。这里有一个中文乱码的小技巧如果图例或坐标轴中文显示成了方框可以加一行设置中文字体set(gca, FontName, SimHei);画完图之后我会特别留意预测曲线和真实曲线在波峰、波谷处的对应关系。如果预测曲线整体上比真实曲线“慢半拍”也就是所谓的滞后现象那说明模型学到的主要是惯性对突变不敏感。这种情况下要么增加窗口长度要么把其他相关特征比如节假日标记、天气特征加入输入。5. 常见问题排查与避坑5.1 常见问题速查表下面这张表是我长期使用MATLAB做时间序列预测时整理出来的高频问题按报错信息或现象快速定位问题来源。问题现象可能原因解决方案错误使用 network/sim输入输出维度不匹配检查Xnorm和Ynorm的行列方向确保train和sim时输入维度一致无法读取文件 data.xlsxExcel不在当前工作目录使用绝对路径或cd切换到文件所在目录newelm未定义新版MATLAB已移除旧API改用layrecnet核心思想一致MAPE为Inf或异常大真实值中存在接近0的数值改用SMAPE或只报告RMSE/MAE训练误差很小但测试误差巨大过拟合或数据泄露减少隐层节点数检查是否用了随机划分预测曲线整体滞后模型对突变不敏感增大滑动窗口或增加外生特征mapminmax输出维度混乱没有注意mapminmax按行处理输入输出都使用行向量必要时转置每次训练结果都不一样网络初始化随机训练前加rng(default)固定随机种子5.2 被忽略但很重要的三个细节第一Excel里的日期列。如果第一列是日期文本用readmatrix会直接读成NaN因为日期不是纯数值但用readtable能读成datetime类型画图时可以直接用。所以我的建议是有日期就优先readtable并单独把日期列提取出来作为画图横轴不要强行参与模型计算。第二时间序列千万不要随机划分数据集。我用divideblock就是为了防止训练集和测试集在时间上交错。如果模型在训练过程中见过未来数据评估指标会显得很漂亮但真正上线预测时效果会大打折扣。这一点几乎是时间序列新手最容易犯的错。第三训练前加一行rng(default)。神经网络每次初始化权重都是随机的如果不固定随机种子你调参时很难判断效果变好是参数动了还是纯粹运气好。固定种子之后每次跑出来的结果一致调参才有可比性。5.3 两个我反复踩过的实战教训一个是数据质量问题。有一次我拿一份生产车间的传感器数据做预测Excel里有大量缺失值和异常跳变当时图省事没清洗直接送进网络训练。结果RMSE大得离谱检查才发现有一段传感器的值连续一周都是同一个常数模型以为这里是平台期后面真实值突变时预测完全跟不上。从那以后我所有的数据都会先画一遍折线图肉眼扫一遍异常再进入建模流程。另一个教训是归一化参数的保存。早期我训练完之后没有保存Xps和Yps预测时重新计算了一次导致反归一化之后的结果和训练时的尺度不一致看起来误差特别大查了很久才发现是归一化参数前后不统一。所以现在我的脚本一律把net、Xps、Yps、lag一起存成.mat文件下次预测时直接加载保证流程是一致的。我个人实际用下来最大的体会是这套流程里最花时间的地方往往不是模型调参而是数据预处理和样本构造。Elman网络本身并不复杂数据喂得干净、窗口构造合理、训练测试不串时间预测效果基本不会差到哪里去。如果你已经跑通了单步预测接下来可以试试多步递推预测或者用真实值滚动更新输入再进阶一点可以尝试小波分解加Elman的组合。这些都是在今天这套基础脚本上做的扩展先把底子打好后面的路会顺很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进