尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ELMAN动态递归神经网络时间序列预测:MATLAB实现与参数调优

ELMAN动态递归神经网络时间序列预测:MATLAB实现与参数调优 简介这份MATLAB代码包以ELMAN动态递归神经网络为核心面向需要开展时间序列预测、回归拟合或模式识别的开发者与研究者尤其适合对RNN原理有一定了解但缺乏可运行示例的入门用户。压缩包体积仅193KB共包含3个文件1个.m主程序用于构建与训练网络1个.mat数据文件存储预处理结果1个.xlsx原始数据表三者配套可快速跑通流程。目前已有1533人学习浏览适合作为ELMAN网络入门的实战参考。主程序完整覆盖了数据导入、归一化、网络层定义、训练参数设置、预测与结果可视化等环节尤其突出反馈权重对记忆能力的影响。通过这份代码读者不仅能直接看到ELMAN在序列数据上的预测效果还能以此为骨架迁移到其他数据集或改造成LSTM/GRU进行对比实验。1. ELMAN动态递归神经网络预测到底解决什么问题你手头如果有一组随时间变化的数据——交通流量、电力负荷、股价、水位、设备振动特征——并且想预测未来几步的走势常规的前馈神经网络BP网络往往会在序列特征上栽跟头它把每个时刻的输入当成独立样本完全丢失了时间上的前后关联。ELMAN动态递归神经网络正是为这类“输入与历史状态共同决定当前输出”的问题设计的。它比普通BP网络多了一层承接层context layer能把上一时刻隐层的状态“记忆”下来再作为当前时刻的额外输入所以擅长处理短时相关的时间序列。这篇文章不讲虚的直接用MATLAB代码把从数据准备、网络训练、预测到误差评估的完整链路跑通顺手把隐层节点数、反馈延迟、学习率这些“一调就变天”的参数给你逐个说透。如果你用过BP网络或LSTM你会发现ELMAN恰好夹在两者之间比BP更懂时序比LSTM参数少、训练快适合样本量不大、周期特征明显的预测任务。2. ELMAN网络结构拆解与MATLAB中的网络创建方式2.1 承接层到底在做一件什么事ELMAN网络有个朴素的物理直觉预测下一步之前先记住上一步的“感觉”。这个“感觉”就是隐层在上一时刻的输出被原样复制到了承接层在下一时刻与外部输入拼接后一起进入隐层。用公式看更清楚——设输入为(x(t))隐层输出为(h(t))承接层输出为(c(t)h(t-1))则[ h(t) f(W_{xh} x(t) W_{ch} c(t) b_h) ] [ y(t) g(W_{hy} h(t) b_y) ]其中(W_{ch})就是承接层到隐层的权重矩阵。这个矩阵的存在意味着网络内部有了“状态”状态携带了历史信息所以ELMAN对时间序列的拟合能力天然强于静态网络。MATLAB里有两种方式创建这种结构旧版R2010a以前用newelm新版推荐elmannet。前者需要自己指定输入输出范围、隐层节点数和传递函数后者采用类似feedforwardnet的方式直接指定延迟阶数与隐层规模。2.1.1 用elmannet创建的最小示例% 创建一个ELMAN网络2步输入延迟隐层10个神经元 net elmannet(1:2, 10); % 查看网络结构 view(net);这里1:2表示承接层会记忆前两步的隐层状态而不只上一步10是隐层神经元个数。view(net)会弹出图形窗口你能直观看到输入层→隐层→承接层→输出层的连接关系。需要说明的是elmannet自动生成一个两层前馈结构并且承接层的反馈权重是固定为1的——它只拷贝状态不学习如何组合状态这也是ELMAN与NARX网络的重要区别NARX有可学习的输出反馈ELMAN只有隐层状态自反馈。2.2 旧代码里常见的newelm现在还值得学吗网上很多老教程和论文附件还在用newelm这套接口在MATLAB R2010b之后就不再推荐但你必须看得懂因为要复现别人实验时经常碰到。newelm的标准形式是net newelm(PR, [S1 S2 ...], {tf1 tf2 ...}, bt, lr, wb);PR输入取值范围的矩阵每行对应一个输入维度的[min max]。[S1 S2]各层神经元数至少要给出隐层和输出层。{tf1 tf2}各层传递函数隐层常用tansig输出层用purelin。bt训练函数默认traingdx自适应学习率动量。lr学习率默认0.01。wb权重/偏置学习函数一般用默认。newelm创建的网络其承接层数量由隐层数自动决定不需要手动指定。但在新版MATLAB里运行newelm会提示改用elmannet且newelm生成的网络对象与新版训练函数如train配合度不高经常需要额外设置net.trainParam。我的建议是新项目一律用elmannet只有读老代码时再去解析newelm参数。2.3 从feedforwardnet到elmannet你需要改掉的习惯用惯feedforwardnet的人第一次跑elmannet会踩一个坑输入数据格式。elmannet默认采用按时间步展开的单元格cell数组而不是普通数值矩阵。这意味着你必须把输入输出转成1×N的cell每个cell里是一个特征维度×1的列向量。转换代码几乎固定% 假设X是numFeatures-by-numTimeSteps矩阵Y是1-by-numTimeSteps矩阵 X_cell num2cell(X, 1); % 每一列变成一个cell Y_cell num2cell(Y, 1);不转换直接塞数值矩阵train会报“输入数据维度不一致”或“输入应为cell数组”的错误。另一个习惯差异是数据划分net.divideFcn默认是dividerand会随机把时间点打乱划分训练/验证/测试集——这对时间序列是致命的因为随机打乱会破坏时序依赖。必须改成按顺序划分net.divideFcn divideblock; % 按块划分保持时间顺序 net.divideParam.trainRatio 0.7; net.divideParam.valRatio 0.15; net.divideParam.testRatio 0.15;2.3.1 一个容易忽略的细节反馈延迟对样本数的影响elmannet(1:d, hidden)的延迟集合大小直接决定有效训练样本数。假设原始序列长度是N网络需要前d步作为输入上下文那么能构造的样本数是N - d而不是N。例如序列长度100延迟阶数取5则实际只有95个样本能参与训练。如果样本本来就少延迟阶数设得过大网络会欠拟合设成1又丢失稍长时间的记忆。常见做法是先用自相关函数autocorr观察数据的显著滞后阶数再把它作为elmannet的第一个参数。3. 从数据到预测一套可复用的MATLAB完整流程3.1 构造实验数据带趋势和噪声的周期性信号为了把流程说透不依赖任何外部数据文件我用一个合成时间序列演示正弦波叠加线性趋势和随机噪声。这个序列既有周期性、又有趋势ELMAN训练时才能看出它对趋势的延迟跟踪问题。% 生成模拟时间序列 rng(42); % 固定随机种子保证可复现 t (0:499); y 10 2*t/100 5*sin(2*pi*t/50) 0.8*randn(500,1); y y(:); % 转为行向量参数说明10是基线2*t/100是线性趋势5*sin(2*pi*t/50)是周期50步的振荡0.8*randn是噪声。合成的好处是你知道真实函数长什么样可以直观比较预测曲线和真值曲线的贴合程度。3.2 数据归一化与格式转换ELMAN使用tansig作为隐层激活函数输出范围是[-1,1]输出层若是purelin虽然可以输出任意值但训练时若原始数据量级差异过大梯度更新会不稳。所以训练前必须做归一化常见做法是映射到[-1,1]% 归一化到[-1,1] ymin min(y); ymax max(y); yn 2 * (y - ymin) / (ymax - ymin) - 1; % 构造输入输出样本 inputDelays 1:3; % 用前3步预测下一步 X zeros(length(inputDelays), length(yn)-inputDelays(end)); for i 1:length(inputDelays) d inputDelays(i); X(i,:) yn(d:end-(inputDelays(end)-d1)); end Y yn(inputDelays(end)1:end); % 转为elmannet要求的cell格式 X_cell num2cell(X, 1); Y_cell num2cell(Y, 1);代码说明循环里对每个延迟阶数d把序列从d开始截取终点对齐到end-(最大延迟-d1)这样每个阶数的长度一致最后Y是最大延迟之后的所有值。这样构造的样本语义是用第t-3, t-2, t-1步的值预测第t步的值。注意这里不是滑动窗口一步一移的构造方式而是直接按每个时间点对应的历史窗口切出样本实际效果等价但矩阵运算更快。3.3 训练网络关键训练参数设置% 创建ELMAN网络3步延迟15个隐层节点 net elmannet(inputDelays, 15); net.trainFcn trainlm; % Levenberg-Marquardt中小样本收敛快 net.trainParam.epochs 500; net.trainParam.goal 1e-5; net.trainParam.min_grad 1e-7; net.trainParam.max_fail 20; % 验证集连续20次不下降则早停 % 划分数据保持时序顺序 net.divideFcn divideblock; net.divideParam.trainRatio 0.8; net.divideParam.valRatio 0.1; net.divideParam.testRatio 0.1; % 训练 [net, tr] train(net, X_cell, Y_cell);为什么用trainlm它利用雅可比矩阵逼近二阶导在小数据集上收敛速度远超traingdx。但trainlm需要计算Jacobian内存消耗与参数数量平方相关如果你的输入维度很大或隐层节点很多建议退回traingdx或trainbr。这里数据量500个点网络参数量撑死几百个trainlm完全够用。3.4 单步预测与多步递归预测训练完成后最常见的任务是两种已知历史到时刻T预测T1单步或者要用预测出的T1继续预测T2多步递归。MATLAB里net(x_cell)默认会根据网络延迟自动完成多步计算但要小心它的输出长度取决于输入cell的个数而不是你想预测几步。% 单步预测用最后3个真值预测下一个 lastWindow X_cell(end); % 最后一个样本 predNext net(lastWindow); % 注意这里输入是单个cell输出也是单个cell predNext predNext{1}; % 多步递归预测用预测值逐步替代真实值 nSteps 10; future zeros(1, nSteps); currentWindow yn(end-2:end); % 最后3个真实值作为初始窗口 for k 1:nSteps % 窗口转成cell输入 inputCell num2cell(currentWindow, 1); inputCell reshape(inputCell, 1, []); % 网络预测下一步 nextPred net(inputCell); nextPred nextPred{end}; % 取最后一个输出 future(k) nextPred; % 更新窗口丢掉最旧加入新预测 currentWindow [currentWindow(2:end); nextPred]; end % 反归一化 futureReal (future 1) * (ymax - ymin) / 2 ymin;代码里的关键操作是reshape(inputCell, 1, [])num2cell默认产生列方向的cell数组而elmannet要求输入是1×T的cell行向量不reshape会报错。另外多步递归时每步调用net都会重置内部状态因为输入cell里每个元素被网络当作新序列的开头——严格说这并非“真正的”递归状态传递但通过我们手动更新窗口等效实现了递归预测。如果你需要更严谨的递归MATLAB推荐使用nets或predict函数或者把整个预测序列一次性接入网络的初始状态那就涉及net(x, xi, ai)的形式了。3.5 模型评估用MAPE和RMSE说话预测完要评估好坏不能只画曲线。误差指标就两个最常用均方根误差和平均绝对百分比误差。注意MAPE在数据接近零时会爆炸所以如果你的序列本身有零值或极小值改用MAE或MASE。% 假设test真实值为yTest预测值为yPred均已完成反归一化 rmse sqrt(mean((yPred - yTest).^2)); mape mean(abs((yTest - yPred) ./ yTest)) * 100; fprintf(RMSE: %.4f\nMAPE: %.2f%%\n, rmse, mape);评估时还有一个容易犯的错误把归一化后的误差直接当结果报告。因为归一化把数据压缩到[-1,1]RMSE会显得很小但反归一化后误差可能很大。所以任何对外汇报的误差指标一律要在反归一化后的尺度上计算。4. 隐层节点、延迟阶数、学习率三个必调参数与典型陷阱4.1 隐层节点数从经验公式开始用验证集收尾隐层节点数没有解析最优解但有几个经验公式被无数论文引用过(H \sqrt{I O} a)a取1~10或者(H (IO)/2)其中I是输入维度这里等于延迟阶数O是输出维度通常为1。按这个公式延迟3、输出1时H在3到13之间。我一般习惯先取输入维度6然后左右各试两三个值观察验证集的RMSE曲线选择“拐点”处的节点数——节点过少欠拟合过多则过拟合而且训练时间会陡增。用一段循环脚本批量测试是最省事的hiddenList 5:2:20; rmseList zeros(size(hiddenList)); for i 1:length(hiddenList) net elmannet(inputDelays, hiddenList(i)); % ... 同样的数据划分与训练参数 ... [net, ~] train(net, X_cell, Y_cell); yPredCell net(X_cell); yPred cell2mat(yPredCell); rmseList(i) sqrt(mean((yPred - cell2mat(Y_cell)).^2)); end注意这个循环里的误差是在归一化域内计算的只是为了横向对比不用于绝对精度。选节点数时还要看训练集和验证集误差是否同步下降。如果训练误差持续下降但验证误差上升说明过拟合节点数要往回减。4.2 延迟阶数用自相关函数代替拍脑袋延迟阶数essentially决定了网络能“看见”多长的历史。设得太短网络只能捕捉最近几帧的斜率变化遇到周期较长的数据就摸不到规律设得太长输入维度膨胀训练样本数缩减且承接层记忆被稀释。一个靠谱的做法是先跑一遍autocorr(y)看自相关图figure; autocorr(y, 50); % 画到50阶自相关图中如果某个滞后k的自相关系数显著超出蓝色置信区间通常阈值±1.96/√N说明该滞后阶有信息量。取第一个局部极小值前的显著滞后个数作为延迟集合的最大值。比如序列周期是50步自相关会在滞后50处有明显峰值那你至少要把延迟拉到20~50不然网络看不到完整周期。另外elmannet的第一参数不一定是连续1:d可以写成[1 2 3 5 8 13]这种非均匀延迟模拟“记住关键时刻”的效果但工程上连续性越强越稳定我建议从均匀延迟开始。4.3 学习率与训练函数的搭配组合学习率只对traingdx这种梯度下降类训练函数有意义用trainlm时学习率由算法内部的阻尼因子控制net.trainParam.lr基本是废的。如果你想手动调学习率就要把训练函数改成traingdx此时初始学习率通常从0.01开始观察训练误差曲线如果曲线震荡剧烈把学习率除以10如果下降极其缓慢乘以10。配合动量因子mc默认0.9可以抑制局部震荡但动量太大也会导致错过最优解。还有一个坑是trainlm的收敛性检查。当验证集误差开始上升max_fail计数归零后训练提前停止但网络参数实际停留在验证误差最小的那个点。这本来没问题可如果你改了divideFcn为divideblock后验证集只取最后一段数据——如果那段数据恰好是趋势突变区比如断崖式下跌早停就会把网络定格在一个“对近期突变欠拟合”的状态。我的解决办法是先用divideblock划分训练完后用全部数据重新训练一次同样的结构去掉划分作为最终部署模型。这样至少能保证全部数据都参与了训练预测时不会浪费最后一段信息。4.4 常见陷阱把整个序列同时用于训练和测试很多初学者用ELMAN做预测时拿完整时间序列的90%训练剩下的10%测试但测试点紧接着训练序列——这样测试集里的前几个点依赖的训练历史可能跨过划分边界造成信息泄漏。正确做法是严格的时间轴切分训练集只能是[1..N_train]测试集从N_train1之后测试预测时只允许使用测试集起点之前的历史数据。如果序列是递归预测还要保证测试过程中不混入真实值更新窗口否则就成了“开卷考试”。上述第3.4节的多步递归预测代码窗口更新用的全部是预测值这符合“闭卷”要求如果你发现测试数据上表现极好先检查一下是不是窗口里混入真实值了。5. 进阶用小波分解提升ELMAN预测精度以及验证模型稳定性的技巧5.1 小波ELMAN组合预测的整体思路ELMAN对短期波动敏感但对非平稳序列中的趋势突变和不同频率成分往往一锅粥。一个常见的改进方案是小波变换加ELMAN——先用离散小波变换DWT把原始序列分解成低频近似分量近似系数和若干高频细节分量然后对每个分量分别建立一个ELMAN网络训练预测最后叠加各分量预测值还原到原尺度。MATLAB中用wavedec做分解% 对原始信号做4层db4小波分解 [c, l] wavedec(y, 4, db4); a4 wrcoef(a, c, l, db4, 4); % 近似分量 d4 wrcoef(d, c, l, db4, 4); % 细节分量 d3 wrcoef(d, c, l, db4, 3); d2 wrcoef(d, c, l, db4, 2); d1 wrcoef(d, c, l, db4, 1);然后对每个分量独立执行第3章里的训练流程。因为小波分解得到了不同频段的局部特征低频分量用较大的延迟阶数网络能学到缓慢变化的趋势高频分量用较小的延迟阶数只需要最近几步的细节这样每个子模型都更专注。最后把各个分量的预测结果相加再反归一化。需要提醒的是分解层数不能太多——每分解一次序列长度会因下采样变短实际是近似系数长度减半但用wrcoef重构后长度恢复但层数太深会让高频分量变成纯噪声模型学到的是噪声模式。5.2 用残差自相关检验模型是否学干净了模型好坏的判断不能只看测试集RMSE。一个过头拟合噪声的模型可能照样有不错的RMSE但残差真实值减预测值里藏着明显的自相关——说明模型没学完时间依赖。检验方法residuals yTest - yPred; [r, lags] autocorr(residuals, 20); % 看lags1处是否显著超出置信区间 conf 1.96 / sqrt(length(residuals)); if abs(r(2)) conf fprintf(残差在滞后1处存在显著自相关模型信息提取不充分\n); end这里r(2)对应滞后1的自相关系数。如果显著非零建议增加隐层节点或延迟阶数如果残差的自相关近似白噪声说明模型已经学到了数据中的时序结构。这个方法同样适用于小波ELMAN组合模型——组合模型的残差白噪声程度应该优于单一模型这是你对外论证“小波分解有助于提升预测”的统计学依据。5.3 多步预测的滚动评估比单次测试更有说服力很多人只做一次“从某个断点开始预测10步”就下结论这个结论受断点位置影响极大。更稳的评估方法是滚动起点评估在测试区间内取M个不同起点每个起点都从该起点之前的历史数据出发递归预测固定长度H步汇总所有起点和步长的误差分布。这样能看出模型在时间序列不同相位上的表现差异也能暴露“预测步数增加后误差爆炸”的问题。MATLAB实现就是把第3.4节的预测过程封装成一个函数在循环里调用。最终你可以画一张误差随预测步数变化的曲线——如果曲线在某个步数后急剧上扬说明模型的递归误差累积极快这时应该考虑只做单步预测或者改用直接多步输出的训练策略用未来H步作为输出向量而不是递归预测。直接多步输出的ELMAN在MATLAB里实现也不难把输出层节点数设为H训练时构造输入窗口和对应的未来H步序列作为目标矩阵即可。本文还有配套的精品资源点击获取
返回列表