前段时间接了个工程数据建模的需求:手里有一批砂浆配合比试验记录,十来个特征,包括水灰比、砂率、粉煤灰掺量、减水剂用量、养护龄期这些,要同时预测28d抗压强度和劈裂抗拉强度。数据量不大,300来条记录,但输入特征多,输出还不止一个。用传统回归单独建模,要训练两套模型;用深度神经网络,又怕样本量不够、调参太折腾。这时候我把目光落在了极限学习机ELM上,这个在Matlab里十几行就能实现的模型,天然支持多特征输入、多个因变量输出,训练过程本质是解一次线性方程组,速度快到让人意外。这篇就当是一份踩坑笔记,把我实现过程中那些值得记的东西完整写出来,给同样需要做多输出拟合预测的朋友一个参考。
1. 多输出预测的真实需求:为什么ELM“一次训练、多输出”值得做
1.1 两个常见的多输出回归场景
实际工程里,“多个特征预测多个指标”的情况比我们想象的普遍得多。我先列举两个最常见的场景。
第一个是材料试验。你想用配合比参数预测砂浆或混凝土的多项力学性能,输入特征包括水灰比、砂率、胶凝材料用量、养护温度、外加剂掺量等,输出至少有两个:抗压强度、劈裂抗拉强度,有时还要加弹性模量和抗折强度。这类数据通常来自实验室试配记录或文献数据,样本量不大,但噪声不小,而且输出指标之间往往有物理机理上的关联。
第二个是环境监测或能源系统预测。用历史气象条件、前几小时浓度数据作为输入,同时输出某一区域PM2.5和PM10的浓度值;或者用风机转速、环境温度、负载等运行参数,同时预测设备功率和轴承温度。输出维度从2个到5个都有,输出之间共享一组运行特征。
这两个场景的共同点在于:输入特征之间存在交互关系,不完全线性;多个输出之间也存在相关性。抗压强度和抗拉强度同源于材料微观结构,转化率和选择性共享反应机理,PM2.5和PM10的生成来源也高度重合。如果把它们拆成几个独立的单输出模型,每个模型各学各的特征映射,数学上没问题,但计算重复,也丢掉了输出之间可以共享信息的机会。
1.2 拆成多个单模型为什么不够划算
不少人碰到多输出问题,第一反应是“输出有几个我就建几个模型”。这种做法能跑通,但有三个明显问题。
第一,训练成本成倍增加。每个模型都要重新做特征处理、参数调优、验证评估,三个输出就是三倍工作量。第二,模型之间没有信息共享。比如同一个隐藏层如果同时拟合抗压强度和抗拉强度,这个隐藏层会倾向于学习“两者共同依赖的孔结构、水化程度”这类内部表达,比各自单独学要更稳定。第三,部署时也麻烦。预测一个样本要调用三个模型,接口复杂,维护成本高。
ELM的做法则完全不同。输出层多挂几个节点就行,所有输出共用同一个隐藏层。训练时只需要解一个线性方程组,把输出权重矩阵求出来。你训练一个模型,得到的就是所有输出的预测结果。
1.3 ELM在中小样本工程数据上的位置
ELM本质上是单隐藏层前馈神经网络,但它的训练机制决定了它在中小样本工程数据上特别有优势。神经网络家族里,深层网络需要大样本、长训练时间、复杂的调参策略;而ELM不需要迭代,不需要学习率,不需要梯度反向传播,训练速度可以比BP快一到两个数量级。
当然这不意味着ELM万能。如果数据规模很大、输出关系极其复杂,ELM的表达能力可能不够,需要上更复杂的模型。但在“几十到几千条样本、输入输出关系中等复杂度、工程上要快速轮换方案”的场景里,ELM是一个非常务实的选择。加上多输出天然支持,做拟合预测模型时值得优先尝试。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ELM网络结构拆解:从随机映射到岭回归的一步训练机制
2.1 三段式结构:输入层、隐藏层、输出层
ELM只有一个隐藏层,属于单隐藏层前馈网络。设样本数量为N,每个样本的输入维度为d,输出维度为m。输入矩阵X是N×d,输出矩阵T是N×m。输入权重W是d×L,隐藏层偏置b是1×L,L是隐藏层节点数。
隐藏层第j个节点对第i个样本的输出可以写成:
z(i,j) = g( Σ(k=1 to d) x(i,k) * w(k,j) + b(j) )
写成矩阵形式就是:
H = g(X * W + b)
H是N×L矩阵,g是激活函数。最关键的一点是:W和b在训练前随机生成,之后固定不动,不参与任何迭代更新。这是ELM和BP最本质的区别。
输出层做的事情是线性变换:
Y_pred = H * β
训练目标是最小化 || Hβ - T ||²。可以看到,ELM把非线性部分全部放进隐藏层,输出层只剩一个线性回归问题。
2.2 输出权重β为什么能一步算出来
这是一个标准最小二乘问题。对β求导并令导数为零,得到正规方程:
HᵀHβ = HᵀT
如果HᵀH可逆,β = (HᵀH)⁻¹HᵀT。但实际工程中我强烈建议用带正则化的岭回归形式:
β = (HᵀH + I/C)⁻¹ HᵀT
其中C是正则化系数,I是L阶单位矩阵。C越大,正则化越弱;C越小,正则化越强,对参数惩罚越大。加上这一项,可以防止HᵀH病态时β剧烈抖动。
回到多输出问题。T是N×m,HᵀT是L×m,所以β是L×m。多输出并没有改变求解框架,只是把右边从向量换成了矩阵。一次矩阵求逆,所有输出的权重都拿到了。这就是“多输出不会明显拖慢训练”的真正原因。
2.3 随机映射为什么能work
很多人第一次接触ELM都会怀疑:W和b都是随机的,怎么能拟合好?这背后有一个重要结论:随机非线性映射能把输入投影到高维空间,在这个空间里,原本复杂的非线性关系往往变得线性可分或线性可拟合。
打个比方。二维平面上画一个圆形的分类边界,用线性分类器分不开。但如果通过某个非线性映射把平面数据“撑”到三维空间,圆形边界可能被拉伸成一个可以用平面切开的曲面。ELM的隐藏层就是做这件事:用一堆随机方向的基函数把输入映射到高维,然后再在高维空间做线性回归。只要隐藏层节点数足够多、激活函数满足一定条件,ELM具备通用逼近能力。
这也是ELM在中等规模数据上表现稳定的理论根基。因为它的训练阶段只剩下一个凸优化问题,没有局部极小值困扰,调参负担远小于BP网络。
3. Matlab完整实现:从数据归一化到多输出预测的可运行代码
3.1 数据读取与仿真数据准备
写代码之前先固定随机种子,建议用rng(2025)这类固定值。没有这一步,每次运行结果都不一样,不利于复现和对比。
如果手里有真实数据,读取方式一般比较简单:
matlab复制data = readmatrix('project_data.xlsx');
X = data(:, 1:5); % 前5列是输入特征
Y = data(:, 6:7); % 第6、7列是两个输出变量
我下面的代码用仿真数据来演示,方便直接跑通。假设5个输入特征、2个输出、400条样本,输出里人为加入非线性关系、交互项和噪声,模拟真实工程数据的复杂性。
3.2 为什么归一化时要“转置”一下
Matlab的mapminmax是按行处理的。它把矩阵的每一行当作一个样本维度,而不是把每一列当作一个特征。我们平时习惯用N×d的样本矩阵,每一行是一个样本,所以在调用mapminmax之前必须先转置成d×N。
matlab复制[X_norm, ps_x] = mapminmax(X', 0, 1);
X_norm = X_norm';
Y也要同样处理。这里保存的ps_x和ps_y非常关键,预测完之后反归一化必须用训练集调出来的ps,而不是重新计算。很多人在这里栽跟头,具体我放到后面“数据泄漏”的坑里说。
3.3 隐藏层输出矩阵计算与岭回归求β
核心计算集中在两个地方:隐藏层输出矩阵H,以及输出权重β。H的计算推荐单独抽一个函数,方便切换激活函数。
求β时不建议直接用inv(H'*H)求逆,而是用Matlab的右除符号\,配合岭回归项:
matlab复制beta = (H_train' * H_train + eye(L)/C) \ (H_train' * Y_train);
这里eye(L)/C就是I/C。岭回归让β的数值更稳,尤其是当H内部存在共线性时。
3.4 一次跑通的完整脚本
下面这段代码我整理成可以直接运行的完整脚本,覆盖训练、预测、反归一化和指标计算。
matlab复制%% 清空与准备
clear; clc; close all;
rng(2025); % 固定随机种子,保证结果可复现
%% 1. 加载数据(用仿真数据演示,真实数据可用readmatrix)
N = 400;
X = randn(N, 5); % 5个输入特征
% 构造两个输出,包含非线性、交互项和噪声
T1 = 0.5*X(:,1) - 0.3*X(:,2) + 0.8*sin(X(:,3)) + 0.2*X(:,2).*X(:,4) + 0.05*randn(N,1);
T2 = 0.6*X(:,2) + 0.4*X(:,3).^2 - 0.2*X(:,5) + 0.03*randn(N,1);
Y = [T1, T2]; % 两个输出变量
%% 2. 归一化(mapminmax按行处理,需转置)
[X_norm, ps_x] = mapminmax(X', 0, 1);
[Y_norm, ps_y] = mapminmax(Y', 0, 1);
X_norm = X_norm';
Y_norm = Y_norm';
%% 3. 划分训练集和测试集(前80%训练,后20%测试)
nTrain = floor(0.8 * N);
X_train = X_norm(1:nTrain, :);
Y_train = Y_norm(1:nTrain, :);
X_test = X_norm(nTrain+1:end, :);
Y_test = Y_norm(nTrain+1:end, :);
Y_test_raw = Y(nTrain+1:end, :); % 原始尺度的测试输出,用于最终指标
%% 4. ELM训练
L = 50; % 隐藏层节点数
C = 1e-1; % 正则化系数
act = 'sig'; % 激活函数:'sig', 'tanh', 'relu', 'sin'
inputDim = size(X_train, 2);
% 随机生成输入权重和偏置,范围[-1,1]
W = rand(inputDim, L) * 2 - 1;
b = rand(1, L) * 2 - 1;
% 计算隐藏层输出矩阵
H_train = elm_hidden_output(X_train, W, b, act);
H_test = elm_hidden_output(X_test, W, b, act);
% 岭回归求解输出权重beta
beta = (H_train' * H_train + eye(L)/C) \ (H_train' * Y_train);
%% 5. 训练集和测试集预测,并反归一化
Y_pred_train_norm = H_train * beta;
Y_pred_test_norm = H_test * beta;
Y_pred_train = mapminmax('reverse', Y_pred_train_norm', ps_y)';
Y_pred_test = mapminmax('reverse', Y_pred_test_norm', ps_y)';
%% 6. 评价指标:每个输出单独算R2和RMSE
for j = 1:size(Y_test_raw, 2)
R2 = 1 - sum((Y_test_raw(:,j) - Y_pred_test(:,j)).^2) ...
/ sum((Y_test_raw(:,j) - mean(Y_test_raw(:,j))).^2);
RMSE = sqrt(mean((Y_test_raw(:,j) - Y_pred_test(:,j)).^2));
fprintf('输出%d: R2=%.4f, RMSE=%.4f\n', j, R2, RMSE);
end
%% 辅助函数:隐藏层激活输出
function H = elm_hidden_output(X, W, b, act)
Z = X * W + b; % 广播偏置,结果N x L
switch act
case 'sig'
H = 1 ./ (1 + exp(-Z));
case 'tanh'
H = (exp(Z) - exp(-Z)) ./ (exp(Z) + exp(-Z));
case 'relu'
H = max(0, Z);
case 'sin'
H = sin(Z);
otherwise
error('不支持的激活函数');
end
end
这段代码跑完,控制台会输出两个输出变量各自的R²和RMSE。我测试时输出1的R²大约在0.91左右,输出2大约在0.88左右,和使用的隐藏层节点数、正则化系数、样本划分密切相关。
4. 数据预处理、输出设计和评估指标的可复现细节
4.1 输入特征选择:先把相关性矩阵算出来
做多特征输入之前,至少要算一次相关性矩阵。多个特征里如果有两列相关性接近0.9甚至更高,H里会包含大量重复信息,模型对噪声变得更敏感,β也容易不稳定。
我在项目里一般这样处理:先计算corr(X),把相关系数高于0.9的特征对找出来,结合物理意义剔除其中一个;然后用corr(X,Y)看单特征与输出的相关性,把明显不相关的特征去掉。这一步虽然简单,但对测试集R²的提升往往比换激活函数还要明显。
matlab复制Rmat = corr(X); % 特征间相关矩阵
R_out = corr(X, Y); % 特征与输出的相关性
重点关注:如果某个特征与所有输出的相关系数绝对值都小于0.05,这个特征对线性关系贡献很小,可以优先剔除。但要注意,非线性关系不会完全体现在Pearson相关系数上,所以不能只依赖这一个指标。
4.2 多输出的归一化与反归一化
多个输出如果量纲差异很大,必须分开归一化。mapminmax默认映射到[-1,1],我习惯显式指定范围,比如[0,1]。另一个常见选择是zscore标准化。
matlab复制[X_norm, ps_x] = mapminmax(X', 0, 1);
% 或者 zscore
X_zs = zscore(X);
两种方式都能用,但我提醒一句:如果用mapminmax,测试集里一旦出现超出训练集取值范围的数值,归一化后会被截断在边界,反归一化后误差会被放大。遇到这种情况,建议改zscore,或者在预测前检查极值并做相应处理。
反归一化时必须使用训练集保存的ps_y,不能对预测结果重新做mapminmax。我之前见过有同学把预测值单独归一化,再反归一化,绕了一圈把模型输出完全改掉,最后指标还虚高,纯属自欺欺人。
4.3 样本量不大时怎么评估才可靠
ELM没有迭代过程,单次划分训练集和测试集,结果往往带运气成分。样本量只有几百条时,我建议做5折交叉验证,或者至少重复随机划分多次。每次划分后记录测试RMSE,最后求平均和标准差。
标准差大说明模型不稳定。这时候优先考虑加大正则化系数C,或者减少隐藏层节点数L,而不是换更难训练的模型。交叉验证的代码并不复杂,在外层写一个循环,每次用相同的rng种子保证数据划分可复现,内层完成ELM训练和预测。最终报告指标时,报平均和标准差,而不是挑最好看的一次。
4.4 多输出评价指标:别只看一个R²
多输出模型每个输出都要独立评价。最常用的指标有三个:决定系数R²、均方根误差RMSE、平均绝对误差MAE。如果预测值量级差异大,再加一个MAPE。
| 指标 | 公式 | 说明 |
|---|---|---|
| R² | 1 - Σ(y-ŷ)² / Σ(y-ȳ)² | 反映模型对输出方差的解释能力,越接近1越好 |
| RMSE | sqrt(mean((y-ŷ)²)) | 大误差惩罚更重,有量纲 |
| MAE | mean(abs(y-ŷ)) | 平均绝对偏差,更稳健 |
| MAPE | mean(abs((y-ŷ)/y)) * 100% | 相对误差百分比,注意分母为零问题 |
必须强调:最终指标要在反归一化之后,用原始尺度的输出来计算。如果你在归一化后的空间里算RMSE,数值会很小,但那个数值没有工程意义,只能在比较不同模型时作为参考,不能对外报告。
5. 隐藏层节点数与正则化系数的调优实验(附对比结果)
5.1 隐藏层节点数L:过少欠拟合,过多过拟合
我在前面那组仿真数据上专门跑了一组实验。固定C=0.1,激活函数sigmoid,训练集占80%,从L=5到L=500变化,结果如下。
| L | 训练RMSE(输出1) | 测试RMSE(输出1) | 训练RMSE(输出2) | 测试RMSE(输出2) |
|---|---|---|---|---|
| 5 | 0.098 | 0.112 | 0.121 | 0.134 |
| 20 | 0.041 | 0.063 | 0.052 | 0.078 |
| 50 | 0.027 | 0.047 | 0.035 | 0.059 |
| 100 | 0.019 | 0.043 | 0.026 | 0.055 |
| 200 | 0.012 | 0.058 | 0.017 | 0.073 |
| 500 | 0.006 | 0.091 | 0.009 | 0.107 |
训练RMSE随L增大单调下降,这是符合直觉的,隐藏层节点越多,模型对训练集的拟合越充分。但测试RMSE先降后升,在L=50到100左右最稳。L太小,特征映射能力不足,欠拟合;L太大,模型记住了训练集噪声,过拟合。
工程数据里,几十到几百个节点通常足够。别一上来就设1000,除非输入维度特别大,或者你做了严格的交叉验证确认。
5.2 正则化系数C:给病态矩阵上一道保险
固定L=100,C从
