ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ELM极限学习机多输入多输出预测:Matlab实现与调参实战

ELM极限学习机多输入多输出预测:Matlab实现与调参实战 1. 为什么我会选择ELM来做多输入多输出预测先交代一下背景。我之前一直在用BP神经网络做各类回归预测后来接手一个项目数据维度不算夸张但样本量中等偏上特征有十几个输出变量有四个。用BP网络调参调到头秃——隐层节点数要试、学习率要试、激活函数要试关键是每次训练结果波动很大同样的参数跑两次指标能差出一截。然后我重新捡起了极限学习机Extreme Learning Machine, ELM顿时觉得这才是工程上拿来就能用的东西。ELM的核心思想其实非常朴素普通的单隐层前馈神经网络SLFN要靠反向传播反复迭代来更新输入层到隐层的权重和偏置而ELM把输入权重和隐层偏置随机生成之后固定住不再改动。整个训练过程只需要求解一个线性方程组也就是计算输出权重的Moore-Penrose广义逆矩阵。换句话说ELM把“复杂的非线性迭代优化问题”变成了“一次矩阵求逆问题”训练速度比BP快几个数量级。用生活类比的话BP像是一个学生反复刷题改错ELM则是直接给出一套随机透镜组合然后只调最后一块滤镜的透光率虽然没有像BP那样的精细打磨但绝大多数场景下效果已经很能打。关于多输出这件事ELM天然支持输出层有多个神经元即可。你只需要把训练目标矩阵的维度设置成“样本数 × 输出变量个数”ELM的输出权重矩阵就自动变成“隐层节点数 × 输出变量个数”。这一点在Matlab里实现起来特别顺不用像某些算法那样需要为每个输出单独建模再集成。这篇文章就围绕“多特征输入、多个因变量输出”这个设定来展开。我会把数据准备、归一化方式、ELM核心训练代码、参数调优思路、多输出策略对比、常见坑点全部聊一遍。里面涉及的程序语言是Matlab环境是R2019b之后的版本都适用不依赖额外工具箱只要基础环境就能跑。2. 整体方案设计单模型多输出还是多模型单输出2.1 两类多输出建模路线的对比多特征输入多因变量输出的需求在工程里非常常见比如根据设备的多项运行参数同时预测它的寿命、能耗和故障概率或者根据土壤的多项理化指标同时预测多种重金属含量。建模时可以走两条路线第一条路线是训练一个多输出ELM模型。把所有输出变量放在同一个模型里训练模型的最后一层有多个神经元每个神经元对应一个输出变量。这种方式的优点是结构统一、训练一次即可、代码简洁而且ELM的输出权重求解天然支持多输出。缺点呢就是当各个输出变量的物理量纲差异悬殊时需要在归一化环节格外小心不然网络会偏向量纲大的那个输出。第二条路线是为每个输出变量单独训练一个单输出ELM模型。四个输出变量就训练四个模型互不干扰每个模型可以单独调优隐层节点数、选择各自的激活函数。缺点是训练和预测时要循环四遍而且完全忽略了输出变量之间可能存在的相关性比如某些输出之间其实高度相关单模型多输出还能隐式利用这种相关性。我在这个项目里最终选择了单模型多输出。原因很现实几个输出变量之间存在明显的协同变化关系如果拆开建模等于人为切断信息共享另外ELM训练成本本来就低没必要为了灵活性去牺牲模型结构上的优势。如果你的输出变量之间相关性很弱甚至负相关而且量纲差异非常大那我建议拆成多个单输出模型调参时会更从容。2.2 ELM数学原理的一次通俗梳理ELM的原理这里值得稍微展开一下因为理解原理对你之后处理边界情况和调试Bug非常有帮助。设输入矩阵为 X维度是 n×dn个样本、d个特征。隐层权重 W 是随机生成的维度是 d×LL为隐层节点数。隐层偏置 b 是随机生成的维度是 1×L。隐层输出矩阵 H g(XW b)维度是 n×Lg 是激活函数常见的有sigmoid、tanh、ReLU。我们要求解的目标是输出权重 β使得 Hβ ≈ T其中 T 是目标矩阵维度是 n×mm为输出变量个数。ELM的关键就是β H† T其中 H† 是 H 的Moore-Penrose广义逆。这个公式的含义是我们在最小化误差的同时还保证了输出权重解的唯一性和最小范数特性。L 大于 n 的时候H 是“宽”矩阵用伪逆求出来的解具有最小L2范数这能增强模型的泛化性L 小于 n 的时候H 是“高”矩阵解是普通的最小二乘解。工程上通常取 L 小于 n绝大多数场景下预测稳定性更好。理解了这层原理你就能明白ELM不存在传统意义上的“训练迭代”一次矩阵逆运算就是训练的全过程训练耗时主要花在伪逆计算上。Matlab里用 pinv(H) 就能完成底层调用了SVD分解数值稳定性有保障。2.3 数据流的完整路线图整个项目的处理流程我在纸上画过很多遍实际上线性顺序就这么走原始数据整理 → 划分训练集和测试集 → 归一化处理 → ELM训练随机生成输入权重计算伪逆得到输出权重 → 测试集预测 → 反归一化 → 指标评估 → 若指标不理想则调整隐层节点数或更换激活函数重跑。这个流程里最容易出问题的不是ELM模型本身而是数据划分和归一化。数据划分要保证训练集和测试集的分布尽量一致通常按70%-30%或者80%-20%来切。归一化必须用训练集统计出的缩放参数去处理测试集绝对不能把训练集和测试集混在一起做统一归一化否则会造成信息泄露测试集指标虚高。这一点在实际操作里太容易踩坑了我见过不少朋友把整个数据集做一次归一化再划分结果测试集误差看起来很好一上真实数据就崩。3. 核心代码实现与关键细节解析3.1 数据导入与乱序划分我一般习惯先把数据放到Excel里然后用Matlab的 readtable 读进来。假设你的Excel第一列是样本编号第二列到第十三列是特征输入第十四列到第十七列是输出变量代码可以这样写%% 数据加载 clear; clc; close all; data readtable(dataset.xlsx, VariableNamingRule, preserve); X data{:, 2:13}; % 12个特征 Y data{:, 14:17}; % 4个输出变量这里需要注意readtable读进来的 data{:, 2:13} 是cell索引语法转矩阵的方式要求Excel里这些列必须是数值类型不能有文本否则会报错。如果包含表头读入之后 VariableNamingRule 设置为 preserve 可以保留原始列名。数据划分阶段一定要先做随机乱序。如果不打乱而原始数据是按时间排列的那前面的样本和后面的样本分布可能差异很大训练集和测试集的指标都会失真。做法如下%% 划分训练集与测试集 rng(42); % 固定随机种子让实验结果可复现 n size(X, 1); idx randperm(n); ratio 0.8; trainNum round(n * ratio); X_train X(idx(1:trainNum), :); Y_train Y(idx(1:trainNum), :); X_test X(idx(trainNum1:end), :); Y_test Y(idx(trainNum1:end), :);这里 rng(42) 非常重要。ELM的输入权重是随机生成的数据划分也是随机的如果不固定随机种子你每次跑出来的结果都不一样这会严重干扰你对模型性能的判断。固定种子之后同一个模型结构跑出来的结果是完全可复现的这对工程调试是底线要求。3.2 归一化的正确姿势我见过太多人在这一步翻车。归一化这里最标准的做法是调用Matlab的 mapminmax 函数。mapminmax 默认对矩阵的每一行做归一化所以转置操作是必须的。%% 归一化 [X_norm, ps_x] mapminmax(X_train, -1, 1); [Y_norm, ps_y] mapminmax(Y_train, -1, 1); X_norm X_norm; Y_norm Y_norm; % 应用训练集的归一化参数到测试集 X_test_norm mapminmax(apply, X_test, ps_x); Y_test_norm mapminmax(apply, Y_test, ps_y);这里有个重要的细节ps_x 和 ps_y 是根据训练集的均值和极差计算出来的缩放映射参数。测试集归一化时必须调用 apply 模式并且传入 ps_x不能用 mapminmax(X_test, -1, 1) 重新归一化。第二种做法的结果是测试集被独立归一化到[-1,1]完全脱离了训练集的尺度这会让模型预测值在反归一化时错得离谱且这种错误在代码层面没有任何报错属于典型的“隐性Bug”。对于输出的归一化有些文章建议把目标值归一化到[0,1]而不是[-1,1]。这取决于你用的激活函数如果隐层激活函数是sigmoid输出范围恰好是(0,1)归一化到[0,1]更搭配如果隐层激活函数是tanh输出范围是(-1,1)归一化到[-1,1]更合适。我项目里的激活函数选的是sigmoid但归一化还是用了[-1,1]。主要原因是我训练集里输出变量的取值范围本身并不对称[-1,1]区间配合线性输出层反而灵活。这里没有标准答案重点是模型输出层的激活函数通常设为纯线性让网络输出不受非线性压缩那归一化区间选什么影响就不大。3.3 ELM训练与预测核心代码写完数据准备之后ELM的核心代码反而非常短。前面已经分析过原理这里直接给出可复用的函数。function [IW, B, LW, H] elm_train(X, Y, L, activate) % X: 训练输入, n×d % Y: 训练输出, n×m % L: 隐层节点数 % activate: 激活函数类型, sigmoid / tanh / relu n size(X, 1); d size(X, 2); m size(Y, 2); % 随机生成输入权重和隐层偏置 IW rand(d, L) * 2 - 1; % 区间[-1,1] B rand(1, L) * 2 - 1; % 计算隐层输出矩阵 H H X * IW repmat(B, n, 1); % 广播偏置 switch activate case sigmoid H 1 ./ (1 exp(-H)); case tanh H tanh(H); case relu H max(0, H); otherwise error(未知激活函数); end % 计算输出权重 LW pinv(H) * Y LW pinv(H) * Y; end预测函数对应如下function Y_pred elm_predict(X, IW, B, LW, activate) % X: 新样本输入, k×d H X * IW repmat(B, size(X,1), 1); switch activate case sigmoid H 1 ./ (1 exp(-H)); case tanh H tanh(H); case relu H max(0, H); end Y_pred H * LW; end调用方式非常直接L 30; activate sigmoid; [IW, B, LW, H] elm_train(X_norm, Y_norm, L, activate); Y_pred_norm elm_predict(X_test_norm, IW, B, LW, activate); % 反归一化预测结果 Y_pred mapminmax(reverse, Y_pred_norm, ps_y);这里有两个坑要注意。第一rand(d, L) * 2 - 1 生成的输入权重区间是[-1,1]如果特征维度很高直接乘大矩阵没问题但如果特征已经做了标准化这个随机范围是合理的。第二repmat(B, n, 1) 把偏置向量复制成 n×L 的矩阵实现隐层输入的偏置叠加。这一步在Matlab里也可以用 X*IW B 直接隐式广播但显式repmat更清晰对于老版本Matlab也更兼容。pinv(H) 这一步是ELM性能的关键。pinv本身是Matlab内置的Moore-Penrose伪逆计算底层SVD实现非常稳定。如果你的矩阵H接近奇异pinv不会崩而是会给出一个范数很小的解。这一点比直接使用 inv(H*H) 要安全得多后者在H列相关时数值上几乎肯定出问题。3.4 完整主程序脚本把上面的零碎部分整合成一个完整的脚本方便直接对照参考%% ELM多输入多输出建模完整流程 clear; clc; close all; rng(42); %% 1. 数据加载 data readtable(dataset.xlsx, VariableNamingRule, preserve); X data{:, 2:13}; Y data{:, 14:17}; %% 2. 划分数据集 n size(X,1); idx randperm(n); trainNum round(n*0.8); X_train X(idx(1:trainNum), :); Y_train Y(idx(1:trainNum), :); X_test X(idx(trainNum1:end), :); Y_test Y(idx(trainNum1:end), :); %% 3. 归一化 [X_norm, ps_x] mapminmax(X_train, -1, 1); [Y_norm, ps_y] mapminmax(Y_train, -1, 1); X_norm X_norm; Y_norm Y_norm; X_test_norm mapminmax(apply, X_test, ps_x); Y_test_norm mapminmax(apply, Y_test, ps_y); %% 4. 模型训练与预测 L 30; activate sigmoid; [IW, B, LW, H] elm_train(X_norm, Y_norm, L, activate); Y_pred_norm elm_predict(X_test_norm, IW, B, LW, activate); Y_pred mapminmax(reverse, Y_pred_norm, ps_y); %% 5. 性能评估 R2 zeros(1, size(Y_test,2)); RMSE zeros(1, size(Y_test,2)); for i 1:size(Y_test,2) R2(i) 1 - sum((Y_test(:,i) - Y_pred(:,i)).^2) / sum((Y_test(:,i) - mean(Y_test(:,i))).^2); RMSE(i) sqrt(mean((Y_test(:,i) - Y_pred(:,i)).^2)); end disp(各输出变量 R2:); disp(R2); disp(各输出变量 RMSE:); disp(RMSE);这段代码直接复制到Matlab里调整好Excel路径和特征列范围就能跑出结果。特征列的范围看你的实际情况改12个特征对应第二列到第十三列四个输出对应第十四列到第十七列。4. 参数调优隐层节点数与激活函数的选择逻辑4.1 隐层节点数的经验与实测ELM唯一真正需要调的参数就是隐层节点数 L 和激活函数。输入的随机权重和偏置不需要调这也是ELM吸引我的地方——把调参维度压缩到了极致。隐层节点数 L 的选择没有严格的理论公式但工程上有几个实用的经验参考。一种是用输入特征数、输出变量数、训练样本数来综合估一个初始值比如 L sqrt(d m) 常数或者 L (d m) / 2 到 (d m) * 2 的区间里扫。另一种更靠谱的做法是直接做扫描实验%% 隐层节点数扫描 L_range 10:5:100; test_R2 zeros(length(L_range), 4); for k 1:length(L_range) L L_range(k); [IW, B, LW, H] elm_train(X_norm, Y_norm, L, sigmoid); Y_pred_norm elm_predict(X_test_norm, IW, B, LW, sigmoid); Y_pred mapminmax(reverse, Y_pred_norm, ps_y); for i 1:4 test_R2(k, i) 1 - sum((Y_test(:,i) - Y_pred(:,i)).^2) / sum((Y_test(:,i) - mean(Y_test(:,i))).^2); end end figure; plot(L_range, mean(test_R2, 2), o-); xlabel(隐层节点数 L); ylabel(平均测试集 R2);这个扫描脚本的核心思想是看平均R2随L变化的曲线。通常曲线会先快速上升然后在一个平台期波动最后出现下降那个平台期就是合适的L范围。我在自己的项目里12个输入、4个输出、样本量大概500左右L从10到100扫描下来稳定域在30到50之间最终取了40。L太大不仅训练时间会拉长更重要的是会出现过拟合迹象训练集误差几乎为零但测试集R2明显下滑。关于过拟合这里值得多说一句。ELM的伪逆解本身有最小范数性质也就是说它天然带有正则化效果所以ELM的过拟合风险比BP要小。但这不代表可以无限增大L。L接近甚至超过训练样本数时模型能“背下”几乎所有训练样本泛化能力骤降。我建议L的上限不要超过训练样本数的一半在这个界限内扫描基本安全。4.2 三个常用激活函数的实测对比激活函数的选择也是ELM调参的一部分。我用同一份数据分别跑了sigmoid、tanh和ReLU三种激活函数结果如下激活函数平均测试集R2平均RMSE训练耗时sigmoid0.9420.1530.021stanh0.9380.1610.019srelu0.9010.2110.018ssigmoid在这份数据上略胜一筹但优势其实不算巨大。tanh和sigmoid性能接近ReLU则明显差了一截。原因也不难理解ReLU在随机初始化权重的情况下隐层输出很容易出现大量死神经元——即负的加权和全部被截断为0这样隐层有效节点数大幅减少信息容量下降。如果输出变量有正有负且分布不极端我还是推荐优先试sigmoid和tanhReLU可以作为备选。但是要记住ELM的隐层输出矩阵H直接决定了输出权重如果激活函数产生了大量零值行或列pinv计算出来的解数值上就不会太稳定预测波动会变大。这里再补充一个实操细节ELM的随机输入权重和偏置虽然不用训练但它们的随机取值范围会影响隐层输出的数据分布。我习惯把输入权重和偏置都生成在[-1,1]区间如果特征本身经过了[-1,1]归一化那 H X*IW B 的取值大致会落在[-d-1, d1]的区间里d为特征维度。特征维度较高时这个累加值可能偏大导致sigmoid输出饱和。这种情况下可以把输入权重范围缩到[-0.5, 0.5]试试相当于给ELM做了一次隐式的尺度调整经常能改善测试集指标。4.3 多输出场景下的调参侧重点多输出模型的调参和单输出有个明显区别你不能只看一个输出变量的指标而要看整体表现。我在调L的时候四个输出变量的R2其实并不是同时达到峰值的。有的变量在L30时最好有的在L50时更好。这时候就要看业务上哪个输出更重要或者直接选择四个输出平均R2最高的L。另外由于所有输出共享同一个隐层各个输出变量之间的贡献会互相影响。如果某个输出变量的量纲特别大即使归一化处理过它的误差占比还是可能压过其他输出。我的做法是在评估时同时打印各输出变量的R2和RMSE并观看它们的量级。如果某个输出变量的预测效果明显差于其他变量我会先检查这个变量在训练样本中的分布是否过于集中。ELM对训练样本覆盖范围的边缘预测能力本来就有限如果某变量的测试集分布和训练集差异很大神仙模型也救不回来。5. 评估指标、可视化与结果解读5.1 多输出模型的评估指标计算多输出模型的评估不能只看单一指标我推荐每个输出变量单独计算R2、RMSE和MAE然后看整体平均。R2反映的是模型相对“只用均值预测”的提升程度RMSE反映的是绝对误差MAE则对异常值不敏感。三个指标配合使用才能全面了解模型表现。代码里R2的计算方式需要注意Matlab没有直接内置回归R2函数我都是用公式手算的function [r2, rmse, mae] regression_metrics(y_true, y_pred) ss_res sum((y_true - y_pred).^2); ss_tot sum((y_true - mean(y_true)).^2); r2 1 - ss_res / ss_tot; rmse sqrt(mean((y_true - y_pred).^2)); mae mean(abs(y_true - y_pred)); end这里有一个容易被忽视的问题R2的计算公式里 ss_tot 用的是测试集真实值的均值而不是训练集真实值的均值。两种做法结果不同虽然学术界对这个问题没有强一致结论但业界更常用测试集自身均值作为baseline。我这里按测试集自身均值计算主要是因为这样能更公平地衡量模型在未知数据上的相对表现。5.2 用可视化图表快速定位问题预测值和真实值的散点图是排查模型问题最好的工具。四个输出变量画成2×2的子图横轴是真实值纵轴是预测值理论上完美的模型所有点都落在45度对角线上。点越紧密围绕对角线模型效果越好。如果散点图呈现明显的非线性弯曲或系统性偏离说明模型结构有问题。另外一个非常实用的图是误差分布直方图figure; for i 1:4 subplot(2,2,i); err Y_test(:,i) - Y_pred(:,i); histogram(err, 30); title([输出变量, num2str(i), 误差分布]); xlabel(预测误差); ylabel(频数); end误差直方图如果呈现近似正态分布且均值接近零说明模型误差是随机的没有系统性偏差。如果直方图明显偏向一侧说明模型存在系统性高估或低估这时候要回到归一化配置和数据划分上找问题。5.3 实际项目里我遇到的指标不达标情况一次写文章前的复测中我把L设为60激活函数换成ReLU结果平均测试集R2直接掉到0.89。排查过程很有代表性我建议遇到类似情况按顺序查第一先确认训练集R2。如果训练集R2也低说明模型欠拟合加大L或换sigmoid/tanh。第二如果训练集R2很高但测试集R2崩塌那基本是过拟合降低L或者增加训练样本量。第三如果训练集和测试集指标都在合理范围但个别输出变量R2特别低检查该变量的数据分布是否异常。第四如果所有指标都不稳定跑一次固定随机种子的重复实验排除随机性干扰。我在实际项目里一个非常深刻的体会是ELM模型指标差很多时候不是模型的锅而是数据的问题。特征里混入了噪声过大的无关变量、训练样本分布严重偏斜、或者输出变量本身含有大量重复值这些都会把指标拉下来。动手调模型参数之前先把数据的质量检查一遍往往事半功倍。6. 常见问题排查与避坑经验总结6.1 高频问题速查表我把ELM做多输入多输出建模过程中最容易遇到的问题整理成一个速查表方便你对照排查现象最可能原因解决方案预测结果全是接近同一个值输出层激活函数使用了sigmoid而非线性确保输出层不加激活函数直接用 H*LW训练集指标很好测试集很差L过大导致过拟合降低L控制在样本数的三分之一以内测试集归一化后预测完全偏离测试集用了独立的mapminmax归一化必须用训练集得到的ps_x和ps_y做apply多次运行结果波动巨大没有固定随机种子脚本开头加 rng(固定数值)部分输出变量R2为负数该输出变量与特征几乎线性无关或数据分布异常检查特征有效性增加相关特征pinv计算很慢或内存不足L设置过大或样本量过大降低L或使用随机采样近似伪逆隐层输出全是0或全一样输入权重尺度太大导致激活函数饱和缩小随机权重范围到[-0.5,0.5]预测值超出物理合理范围归一化区间和数据实测范围不匹配检查ps_y结构确认反归一化维度正确这些坑我几乎都踩过。尤其是第二行的过拟合问题和第四行的随机种子问题是新手最容易忽视的。固定随机种子这件事看起来只是一个不起眼的习惯但对工程化落地的影响是决定性的。没有固定种子你连“模型参数调整后指标到底变好还是变差”都判断不了。6.2 数据泄露问题深度剖析数据泄露是机器学习里最隐蔽的坑之一在ELM这种看似简单的模型上也会发生。我记得有一个项目里我一开始为了方便先把整个数据集做归一化再划分训练测试集。结果测试集R2表现极其亮眼接近0.99我还以为自己调参调出了神效。后来交叉验证一测真实水平只有0.93左右。原因不难理解测试集的归一化参数被训练集“看见”了模型在训练时已经间接获取了测试集数据的分布信息。这在论文发表或模型评估阶段是致命的。正确做法永远是先划分再基于训练集单独做归一化然后把训练集参数apply到测试集。代码层面我已经在前面给过了这里再强调一次因为它的重要性完全值得重复。6.3 随机种子与可复现实验的工程习惯最后说一个项目管理的经验多输出ELM模型的代码通常很短容易让人忽略工程化规范。但我强烈建议从一开始就养成几个习惯。第一每个实验脚本第一行用 rng(固定值) 固定随机种子并在脚本文件名里注明种子值比如 elm_L40_sig_seed42.m。这样你回头看实验结果时能精确知道是哪份代码跑出来的数据。第二训练出的 IW、B、LW 这三个核心矩阵要及时保存成 .mat 文件方便后续部署预测时直接加载不用重新训练。save(trained_elm.mat, IW, B, LW, ps_x, ps_y, activate, L);这个习惯在工程落地时价值极大。ELM训练快但预测阶段需要的是持久化的模型参数。如果没有保存每次预测都要重新训练、重新生成随机权重结果自然不可控。保存下来之后新数据来了直接调用 elm_predict整个流程秒级完成。我在多次复现ELM实验的过程中最大的体会是ELM的魅力在于把复杂问题做减法但也正因为简单数据质量和实验规范就成了决定成败的关键。把数据准备和实验管理做好ELM在工程预测任务中的表现完全不输那些结构复杂的深度网络而训练成本和调试成本却是后者的零头。
返回列表