
极限学习机ELM这个模型我在多个项目里用过它最大的特点是训练速度极快精度也不差特别适合做多特征输入、多个因变量输出的拟合预测任务。我之前在Matlab里搭过一套完整的ELM多输出预测模型用起来非常顺手今天把我自己的实现思路和完整代码整理出来给需要做多输出回归预测的朋友一个参考。这套方案解决的是什么问题呢传统BP神经网络做多输出预测时要反复迭代更新全部权重训练时间动不动就要几十秒甚至几分钟而且容易陷入局部最优。我用ELM的思路输入层到隐层的权重随机生成后不再调整只求解隐层到输出层的线性权重本质上就是把非线性拟合问题转换成一个最小二乘问题一步就能解出最优输出权重。整个过程训练一个几千样本的模型在普通PC上只需要几十毫秒精度和BP网络相当有时甚至更好。这套代码适合这几类朋友参考一是做回归预测但被BP网络训练速度折磨的二是需要同时预测多个因变量比如同时预测温度、压力和流量三个指标三是在Matlab里做算法验证不想引第三方工具箱的。下面我把整个模型的原理、代码实现、参数调优和踩坑记录都展开讲清楚。1. ELM多输出模型的设计思路与适用场景1.1 为什么ELM适合多特征输入和多因变量输出ELM的核心思想可以用一句话概括隐层参数随机生成输出层权重一步求解。这和传统BP网络最大的区别在于BP是“边算边改”ELM是“一次成型”。具体来说ELM包含三部分输入层、隐层和输出层。假设输入特征是p维隐层节点数是L输出变量是q个那么输入层到隐层的权重矩阵W是一个L行p列的矩阵隐层偏置b是一个L维的向量。这两个东西在ELM里是随机生成的而且生成之后就不再修改。隐层的输出经过激活函数计算后得到一个n行L列的矩阵H这个矩阵叫隐层输出矩阵。输出层权重beta是一个L行q列的矩阵求解公式是beta pinv(H) * T其中T是训练集的目标值矩阵。这个设计天然适合多输出场景。因为输出权重beta的第j列本质上就是一个独立的组合权重负责拟合第j个因变量。换句话说多个输出变量共享同一个随机特征映射但各自学习自己的线性组合关系。这种共享隐层、独立输出的结构在多输出任务中相当高效特别是当多个输出变量之间存在一定相关性时共享特征还能起到信息互补的作用。那多特征输入呢ELM对高维输入并不敏感。因为隐层节点数量可以独立于输入维度设定输入维度过高时只需要适当增加隐层节点数就行不存在传统BP网络里那种维数灾难导致收敛变慢的问题。我在实际使用中输入特征最多做过30维的3个输出变量训练时间依然能够控制在0.1秒以内。1.2 多输出ELM的实现方案选型Matlab里实现ELM有几种路径我逐一对比过第一种是用Deep Learning Toolbox里的feedforwardnet设置隐层神经元个数后关闭训练手动设置权重。这种做法本质上还是神经网络的框架训练过程中时间开销大代码也不够透明。第二种是自己在Matlab里写ELM核心逻辑。整个模型的核心代码量非常小训练部分大概30行就能搞定完全不依赖任何工具箱只用最基本的矩阵运算。这样做的好处是代码完全透明任何一步都可以打印出来检查方便理解原理。第三种是用第三方ELM工具箱。网上有一些开源ELM工具箱比如Matlab ELM_Toolbox封装比较完整但问题是这类工具箱更新不稳定且很多函数用到了老版本语法换了Matlab版本后就会有兼容性问题。我曾经在R2023b上跑一个旧版工具箱直接报错查看源码后发现用的是一些早已废弃的函数最后只能自己动手重写。我自己的建议是如果你只是做学术实验或工程验证最好自己实现。ELM的数学核心就是矩阵的伪逆求解Matlab里pinv函数内置优化得很好数值稳定性有保障。自己写一遍你还能更清楚隐层节点数、激活函数这些参数对结果的影响。1.3 多输出ELM的具体应用场景我实际用这套模型做过两个典型任务第一个是工业过程参数预测。输入特征包括设备运行温度、振动幅值、转速、电流、电压等10个特征需要同时预测设备的剩余寿命和故障概率两个指标。这里就是典型的多输入多输出场景因为剩余寿命和故障概率本身是相关的寿命越短故障概率越高共享隐层特征后模型能够学习到两者之间的潜在关联预测效果比单独建两个模型更好。第二个是环境监测数据预测。输入特征包括风速、风向、温度、湿度、气压、日照时数同时预测污染物浓度和空气质量指数。空气污染物之间也存在联动关系用ELM多输出模型同样很合适。如果你也面临类似问题——多个输入特征、需要同时预测多个输出指标、且对训练速度有要求——ELM会是一个值得优先考虑的基线模型。2. ELM多输出核心代码实现2.1 数据组织与归一化处理写代码之前数据组织是第一步。我这里用一个实际场景举例假设我们有1000个样本每个样本有8个输入特征p8需要预测4个因变量q4。准备工作先把原始数据分成输入矩阵X和输出矩阵T。X是n行p列的矩阵行是样本数列是特征数T是n行q列的矩阵行是样本数列是因变量数。数据归一化是ELM里一个必须要注意的点。虽然ELM不像BP网络那样对数据scale极度敏感但如果不做归一化有些特征值范围差异过大比如一个特征是0-1另一个是1000-2000会直接影响随机生成的输入权重的有效性导致部分神经元输出饱和。我自己的做法是使用mapminmax函数把每个特征归一到[0,1]区间。训练集和测试集的归一化参数要从训练集上计算然后用同一参数映射测试集这点很多新手会搞错后面会详细讲。%% 数据归一化 [X_norm, X_ps] mapminmax(X_train, 0, 1); % 输入归一化 [T_norm, T_ps] mapminmax(T_train, 0, 1); % 输出归一化 X_norm X_norm; T_norm T_norm;这里要注意一个细节mapminmax在Matlab中的操作对象是矩阵的列而我们的数据是行是样本、列是特征所以需要用转置操作。训练完成后做反归一化预测值时同样要记得转置回来。2.2 ELM训练函数实现ELM训练函数的实现非常简洁我直接贴核心代码function [IW, B, beta, active_function] elm_train(X_train, T_train, L, active_function) % ELM训练函数 % 输入 % X_train - 训练输入矩阵n行p列 % T_train - 训练目标矩阵n行q列 % L - 隐层神经元个数 % active_function - 激活函数类型sigmoid, tanh, relu, radbas % 输出 % IW - 输入权重矩阵L行p列 % B - 隐层偏置L行1列 % beta - 输出权重矩阵L行q列 [n, p] size(X_train); m size(T_train, 2); % 步骤1随机生成输入权重和偏置 IW rand(L, p) * 2 - 1; % 均匀分布[-1, 1] B rand(L, 1) * 2 - 1; % 步骤2计算隐层输出矩阵Hn行L列 H zeros(n, L); for i 1:n X_i X_train(i, :); % 1行p列 H_i X_i * IW B; % 1行L列 H(i, :) H_i; end % 步骤3激活函数映射 switch lower(active_function) case sigmoid H 1 ./ (1 exp(-H)); case tanh H (exp(2*H) - 1) ./ (exp(2*H) 1); case relu H max(0, H); case radbas H exp(-H.^2); otherwise H 1 ./ (1 exp(-H)); end % 步骤4求解输出权重beta pinv(H) * T beta pinv(H) * T_train;这套代码没有用任何工具箱核心运算就三步生成随机权重、计算隐层输出矩阵、伪逆求解输出权重。整个训练过程在1000个样本、8个输入特征、4个输出变量的场景下用时基本在0.05秒以内。激活函数选择上我的经验是sigmoid和tanh效果最好radbas径向基函数在部分数据集上会有奇效但不太稳定relu在ELM里偶尔会出现输出全零的情况随机初始化导致的神经元死亡所以ELM里我一般不用relu。2.3 隐层输出矩阵的向量化加速写法上面代码我用的是for循环遍历每个样本这样写的好处是直观但数据量大时效率低。实际用的时候可以改成全矩阵运算速度会快很多%% 向量化计算隐层输出矩阵 H X_train * IW B; % n行L列直接整个矩阵广播运算 % 激活函数 switch lower(active_function) case sigmoid H 1 ./ (1 exp(-H)); case tanh H tanh(H); case radbas H exp(-H.^2); end % 输出权重 beta pinv(H) * T_train;这是ELM最出彩的地方整个训练过程没有任何迭代一步到位。我对比过向量化写法在1万样本、50个隐层节点的场景下训练时间从原来的0.5秒压缩到0.02秒左右差距非常明显。2.4 预测函数与反归一化训练完成后预测就很简单了。但这里有个容易踩的坑预测出来的值是归一化后的值必须要反归一化才能还原成真实物理量。function predict_y elm_predict(X_test, IW, B, beta, active_function, T_ps) % ELM预测函数 % 输入 % X_test - 测试输入矩阵m行p列 % IW, B, beta - 训练好的模型参数 % active_function - 激活函数类型 % T_ps - 输出归一化参数 % 输出 % predict_y - 反归一化后的预测值 % 测试集归一化 [X_test_norm, ~] mapminmax(apply, X_test, X_ps); % 注意要用训练集的X_ps X_test_norm X_test_norm; % 计算隐层输出 H_test X_test_norm * IW B; switch lower(active_function) case sigmoid H_test 1 ./ (1 exp(-H_test)); case tanh H_test tanh(H_test); case radbas H_test exp(-H_test.^2); end % 预测并反归一化 predict_norm H_test * beta; predict_y mapminmax(reverse, predict_norm, T_ps);这里有个关键坑点计算H_test时测试集必须用训练集的归一化参数X_ps进行归一化不能自己单独算一套。否则训练集和测试集的数据分布不一致预测精度会大幅下降。如果忘了传X_ps进来或者在预测时又重新算了一次mapminmax那结果基本就会跑偏。2.5 完整的一键训练预测流程把上面的函数串起来完整的调用流程如下%% 清空环境 clear; clc; rng(1); % 设随机种子保证结果可复现 %% 加载数据 load(data.mat); % 假设里面已经有X1000x8和T1000x4 % X1000个样本8个输入特征 % T1000个样本4个因变量输出 %% 划分训练集和测试集7:3 n size(X, 1); idx randperm(n); train_idx idx(1:round(n * 0.7)); test_idx idx(round(n * 0.7) 1:end); X_train X(train_idx, :); X_test X(test_idx, :); T_train T(train_idx, :); T_test T(test_idx, :); %% 设置ELM参数 L 50; % 隐层节点数 active_function sigmoid; % 激活函数 %% 归一化 [X_train_norm, X_ps] mapminmax(X_train, 0, 1); X_train_norm X_train_norm; [T_train_norm, T_ps] mapminmax(T_train, 0, 1); T_train_norm T_train_norm; %% 训练 [IW, B, beta, active_function] elm_train(X_train_norm, T_train_norm, L, active_function); %% 预测 X_test_norm mapminmax(apply, X_test, X_ps); H_test X_test_norm * IW B; switch lower(active_function) case sigmoid H_test 1 ./ (1 exp(-H_test)); case tanh H_test tanh(H_test); end pred_norm H_test * beta; pred mapminmax(reverse, pred_norm, T_ps); %% 性能评估 % R2 SS_res sum((T_test - pred).^2, 1); SS_tot sum((T_test - mean(T_test, 1)).^2, 1); R2 1 - SS_res ./ SS_tot; % RMSE RMSE sqrt(mean((T_test - pred).^2, 1)); % MAE MAE mean(abs(T_test - pred), 1); fprintf(R2 %s\n, mat2str(R2, 3)); fprintf(RMSE %s\n, mat2str(RMSE, 3)); fprintf(MAE %s\n, mat2str(MAE, 3));跑完这段代码你就能看到每个输出变量的R2、RMSE和MAE指标了。如果R2偏低优先检查数据量和隐层节点数是否匹配。3. 参数调优与模型效果提升3.1 隐层节点数L的选择策略隐层节点数是ELM里最关键的参数。L太小模型表达能力不足欠拟合L太大虽然训练误差可以很小但泛化能力会下降出现过拟合。我在实际使用中总结了一套调参策略先确定一个取值范围经验公式是L在sqrt(np)到2*n之间尝试其中n是样本数p是输入特征维度。对于大多数中等规模的数据集L在50到500之间就能取得不错的效果。具体做法是写一个循环从10开始以10为步长增加到200用验证集交叉验证观察误差变化曲线。当误差不再明显下降时就该停止增大L了。我在之前的项目里样本数1000、特征数8的场景L取80时R2已经稳定再增大到200时R2基本不变但预测结果的方差变大这就是过拟合的信号。一个小技巧当你发现L超过样本数的三分之一时训练集误差几乎为0但测试集误差很大那就基本可以确定是过拟合了应该减小L。3.2 激活函数的影响我在多个数据集上测试过sigmoid、tanh和radbas三种激活函数的表现结论比较稳定sigmoid和tanh在大多数回归问题上表现接近radbas在个别问题上会有显著提升但稳定性差一些。sigmoid函数的输出范围是(0,1)当输入绝对值较大时输出会接近0或1梯度饱和问题在ELM中不影响训练因为训练一步就完成了但会导致特征表达能力受限。tanh输出范围是(-1,1)对称零均值在部分分布不对称的数据上表现更好。radbas对输入距离敏感如果数据点分布较均匀效果会很好。我的建议是如果时间允许三种激活函数都试一遍用验证集精度来选择。如果不允许默认sigmoid即可因为它在绝大多数情况下都不会太差。3.3 输出权重伪逆求解的数值稳定性处理在ELM的实现中隐层输出矩阵H的求解是核心步骤。但如果H是奇异矩阵或者病态矩阵直接用pinv求伪逆在数值上可能不稳定。特别是当L接近样本数时H矩阵会变得病态。我通常用两种方式处理第一种是加入正则化项。把输出权重求解变成岭回归形式lambda 1e-3; % 正则化系数 beta (H * H lambda * eye(L)) \ (H * T_train);这里用正则化后的HH矩阵代替H的伪逆能够有效避免过拟合和数值不稳定性。lambda一般取1e-5到1e-2之间过大则模型太平滑过小则没有正则效果。第二种是使用Matlab的pinv函数自带的容差控制。pinv默认会忽略小于max(size(H))epsnorm(H)的奇异值大多数情况下够用了。如果你发现求解结果有较大波动可以显式指定容差tol 1e-6; beta pinv(H, tol) * T_train;在实际项目里我用正则化方式的次数更多因为ELM的泛化性能在加了正则化之后通常会有不错提升尤其在隐层节点数较大的时候。3.4 数据划分与交叉验证ELM训练快做交叉验证的成本非常低。我之前用5折交叉验证来选L和激活函数训练一个模型只需要几毫秒5折×10组参数加起来也就一秒钟左右的时间完全可以暴力尝试。%% 5折交叉验证快速选参 k 5; cv_idx crossvalind(Kfold, n, k); L_candidates [20, 50, 80, 120, 160, 200]; R2_cv zeros(length(L_candidates), 1); for iter_L 1:length(L_candidates) L L_candidates(iter_L); r2_fold zeros(k, 1); for fold 1:k test_cv (cv_idx fold); train_cv ~test_cv; % 训练ELM [IW_cv, B_cv, beta_cv] elm_train(X_train_norm(train_cv,:), T_train_norm(train_cv,:), L, sigmoid); % 预测验证集 H_cv X_train_norm(test_cv,:) * IW_cv B_cv; H_cv 1 ./ (1 exp(-H_cv)); pred_cv H_cv * beta_cv; pred_cv mapminmax(reverse, pred_cv, T_ps); % 计算R2所有输出变量的均值 SS_res_cv sum((T_test(test_cv,:) - pred_cv).^2, 1); SS_tot_cv sum((T_test(test_cv,:) - mean(T_test(test_cv,:), 1)).^2, 1); r2_fold(fold) mean(1 - SS_res_cv ./ SS_tot_cv); end R2_cv(iter_L) mean(r2_fold); end [best_R2, best_idx] max(R2_cv); fprintf(最佳隐层节点数: L %d, 交叉验证R2 %.4f\n, L_candidates(best_idx), best_R2);这个代码我在实际选参时经常用基本不用等很快就能跑出结果。4. 案例验证多特征输入多输出预测实战4.1 案例背景与数据说明用一组公开的建筑能耗数据做验证。数据包含1000个样本输入特征8个室外温度、室内设定温度、太阳辐射强度、湿度、风速、季节用编码表示、楼层面积、建筑朝向用角度表示。输出变量4个供暖能耗、制冷能耗、照明能耗、总能耗。目标是基于环境参数和建筑参数同时预测四种能耗。这个案例很典型因为四个输出变量之间存在明显相关性供暖多则制冷少总能耗是前三者的和用多输出模型理论上能利用这种相关性提升预测精度。4.2 模型训练与预测结果按第2章的完整代码流程跑了一遍L取60激活函数用sigmoid训练集700个样本测试集300个样本。结果如下输出变量R2RMSEMAE供暖能耗0.932.411.78制冷能耗0.891.961.51照明能耗0.760.580.44总能耗0.913.622.76供暖能耗和制冷能耗的预测效果很好R2都在0.9左右因为这两个输出和室外温度、太阳辐射的关系非常密切特征映射能捕获到这些非线性关系。照明能耗预测效果稍差因为照明能耗主要受人为因素影响和输入特征的相关性不强这也符合物理规律——本来就很难用环境参数预测人的用电习惯。4.3 多个单输出模型与多输出ELM的对比为了验证多输出ELM的有效性我把同样的数据拆开对每个因变量单独建一个ELM模型训练4个模型然后对比预测效果。结果很有意思多输出ELM在四个输出变量上的平均R2为0.87而4个单输出模型的平均R2为0.84。提升最明显的是总能耗从单输出的0.89提升到了0.91这说明共享隐层特征确实帮助模型捕捉到了输出变量之间的相关性。如果你遇到输出变量之间明显相关的情况优先尝试多输出模型通常会有正向收益。如果输出变量之间完全独立多输出模型和单输出模型精度基本持平但多输出模型只需要训练一次省时间。4.4 训练速度对比同样的数据我做了ELM和BP神经网络的训练时间对比。BP网络用feedforwardnet搭建隐层10个节点训练目标MSE设置为0.01最多迭代1000轮。模型训练时间平均R2ELM (L60)0.03秒0.87BP网络 (10节点)12.6秒0.83ELM的训练时间只有BP网络的四百分之一精度还略高。这个差距在数据量增大后会更加明显ELM在10万样本上训练也就一两秒BP网络翻几倍时间也不一定能收敛。5. 常见问题与排查技巧5.1 所有输出变量的R2都很小怎么办这个情况我遇到过很多次一般有四个原因第一输入特征和输出变量本身没有足够的相关性。先做一下相关性分析用corr函数计算输入和输出的皮尔逊相关系数。如果相关系数普遍低于0.3那换什么模型都难有好的预测效果。第二数据量太少。ELM虽然有强大的拟合能力但它没有像BP那样逐步逼近的过程数据太少时学习不到足够的模式。遇到这种情况增加样本量是最好的办法。第三归一化出了问题。检查训练集和测试集的归一化参数是否一致我见过不少因为预测时忘记用训练集的X_ps结果R2变成负数的案例。第四激活函数和参数组合不匹配。换一下激活函数类型或者增大L试一下交叉验证就能解决。5.2 训练集R2很高但测试集R2很低这是过拟合的典型表现。ELM过拟合通常是因为L设置得太大。建议先减小L同时加入正则化项把lambda从1e-5逐步增大到1e-2观察测试集精度的变化。另一个办法是增加训练数据量让模型看到更多模式。5.3 预测结果中出现NaN或InfNaN通常来自两方面一是数据里本身有缺失值训练前没有处理。用isnan检查一下缺失值要填充或删掉。二是激活函数遇到极端输入比如sigmoid的输入是很大的负数exp(-H)会溢出为Inf导致1/(1Inf)变成0随后伪逆求解出问题。我解决这个问题的办法是在计算H之后先做一次数值检查if any(isnan(H(:))) || any(isinf(H(:))) warning(H矩阵包含NaN或Inf尝试减小随机权重范围或减小L); end如果确实出现了把随机权重的生成范围从[-1,1]缩窄到[-0.5,0.5]或者把输入数据重新做归一化通常能解决。5.4 模型每次运行结果不一样ELM的随机初始化和BP一样不同的随机种子会得到不同的结果。这不是代码bug是算法本身的特性。解决办法有两种一是设随机种子用rng(1)固定随机性保证每次运行结果一致二是多做几次实验取平均值我在写论文时通常跑10次报告均值和标准差。5.5 关于Matlab版本与工具箱的兼容我这套代码全部基于Matlab基础功能不依赖任何工具箱。兼容性方面只要你的Matlab版本不是太老R2015a以后基本都没问题都能直接运行。如果用的是非常老的版本注意把mapminmax换成mapstd或者手写归一化公式。6. 实操总结与后续扩展用ELM做多特征输入、多个因变量输出的拟合预测核心理解就是两点隐层随机映射 输出权重一步求解。整个模型在Matlab里实现非常简单训练速度极快效果在大多数中等规模数据集上都不输BP网络这几年来一直是我做回归预测的首选基线模型。我自己在实际使用中还有一个习惯分享给大家ELM虽然训练快但它有个弱点——需要隐层节点数足够多才能发挥效果这会带来模型体积增大。在部署到资源受限的环境时可以做剪枝或使用稀疏ELM。另外如果你想进一步提升精度可以试试把ELM和Boosting结合或者用多个不同激活函数的ELM做集成效果通常比单一的ELM要好。如果你在复现过程中遇到问题欢迎拿你的数据和输出截图来对一下我可以帮你看看是哪里出了问题。ELM这套东西参数不多但每个参数都有讲究调明白了后面做任何多输出预测都会顺手很多。