资讯动态

极限学习机(ELM)原理详解:从MATLAB实现到调参实战

发布时间:2026/9/13 14:18:53 来源:尧图企业网站定制
简介极限学习机ELM的MATLAB实现代码来自黄广斌教授团队面向机器学习入门者、信号处理与模式识别方向的研究人员。代码核心为单隐层前馈网络通过随机生成输入权重和偏置、解析计算输出权重大幅提升训练速度。资源包仅含1个m文件文件体积约2KB结构极为精简适合直接阅读算法主流程也可作为改进ELM、在线序列学习等方向的起点。压缩包形态为zip方便在MATLAB中快速解压调用。当前已有608人学习下载说明该基础实现具有一定参考价值。借助这段代码读者可以快速验证ELM在分类或回归任务上的表现理解其数学原理与编程实现之间的对应关系为后续扩展核极限学习机、深度极限学习机等变体奠定基础。1. ELM 可不只是那颗随机初始化就能用的灵丹做表格类数据预测的工程师大多遇到过这种场景特征已经清洗到三十到五十维样本也就几千条跑一个常规的单隐层反向传播网络却需要反复调学习率、批大小和隐层节点数稍不留神就过拟合。极限学习机ELM的价值恰恰在于绕开这场拉锯战隐层的权重和偏置用随机数一次性生成输出层权重通过解析解一步算出整个“训练”过程只是一次矩阵伪逆运算。ELM.zip 这个压缩包在开发者之间流传了很久里面基本就是黄广斌团队公开的 MATLAB 实现函数签名是 elmtrain 和 elmpredict。它适合拿来快速验证数据有没有可学结构也适合做离线预测和资源受限的推理场景。2. ELM 原理随机投影、解析解和泛化性的边界2.1 从单隐层前馈网络到极限学习机标准的单隐层前馈网络可以写成这样$$ f_L(x) \sum_{i1}^{L} \beta_i , g(w_i \cdot x b_i) $$其中 $w_i$ 是输入权重$b_i$ 是偏置$g(\cdot)$ 是激活函数$\beta_i$ 是输出权重。传统 BP 把 $w_i$、$b_i$、$\beta_i$ 全部当作可学习变量用梯度下降迭代更新。问题在于 $w_i$ 和 $b_i$ 一旦进入非线性激活函数损失平面就是高度非凸的训练行为对学习率、初始化方法和输入归一化极其敏感。黄广斌的观察是如果换一个角度看问题不求“在全参数空间寻优”而是“先用一组固定的非线性基函数把输入映射到高维空间再在输出层做线性回归”那 $w_i$ 和 $b_i$ 就不再需要参与学习。把它们固定为随机值整个网络只剩下 $\beta$ 这一组线性参数。这才是 ELM 能把“训练神经网络”变成“解一个线性方程组”的根本原因。2.2 为什么随机权重不会拖垮精度第一反应通常是随机生成的 $w_i$、$b_i$ 靠谱吗答案在大多数场景下是靠谱的前提是隐层节点数 $L$ 足够大且激活函数选得对路。隐层做的事情本质上是一次随机特征映射把输入从原始空间投射到高维随机特征空间。随机投影在维数足够高的时候能够大概率保持样本之间的线性可分结构这个结论在随机傅里叶特征和随机权值网络的相关工作里都有支撑。ELM 相当于是把这一几何性质直接拿来当学习算法用因此它不需要像 BP 那样在隐层上做迭代式的特征微调。不过这里有一个边界需要说清楚ELM 的特征映射没有层次结构每一层都是对原始输入的一次独立随机投影。它对特征工程做得比较充分的表格数据、传感器数据和中等规模样本很有效但对原始图像、长语音这类需要逐层抽象的信号ELM 的单层结构很难和深层网络竞争。换句话说ELM 的“上限”取决于你对输入特征的理解程度。2.3 输出权重的解析解把 $N$ 个训练样本代入模型写成矩阵形式$$ H \beta T $$其中 $H$ 是 $N \times L$ 的隐层输出矩阵第 $i$ 行是第 $i$ 个样本经过激活函数后的特征向量$T$ 是 $N \times m$ 的目标矩阵。为了让解存在且数值稳定实际求解的是最小范数最小二乘解$$ \beta H^{\dagger} T $$$H^{\dagger}$ 表示摩尔-彭罗斯伪逆。当样本量大于隐层节点数时更常见的做法是用带正则化的正规方程$$ \beta \left( H^T H \frac{I}{C} \right)^{-1} H^T T $$这里 $C$ 是正则化系数$I$ 是单位矩阵。$C$ 越大正则项越弱结果越接近纯最小二乘解$C$ 越小输出权重被压缩得越厉害泛化性通常会变好一些。当隐层节点数超过样本量时$H^T H$ 是 $L \times L$ 的矩阵求逆代价高且容易病态应该换用下面这个等价形式$$ \beta H^T \left( H H^T \frac{I}{C} \right)^{-1} T $$这两个公式对应了 ELM 代码里最常被改写的两行也是判断一个实现是否“加了正则化”的标志。2.4 训练复杂度与 ELM、BP、SVM 的定位差异构建 $H$ 的复杂度是 $O(NL)$伪逆求解的复杂度大约在 $O(\min(N,L)^2 \max(N,L))$ 的量级。对几千到几万样本的表格数据单次训练就是几十毫秒的事。推理阶段更便宜只有一次矩阵乘法和一次激活函数计算这对嵌入式端和实时预测场景非常友好也是 ELM 到现在没有被淘汰的直接原因。方法需要优化的变量训练方式典型训练耗时主要调参对象ELM输出权重 $\beta$最小二乘伪逆一步完成毫秒到秒级隐层节点数 L、正则化 C、激活函数BP全部权重与偏置迭代反向传播分钟级起学习率、批大小、网络结构、正则化SVM / RBF支持向量与对偶系数求解凸二次规划随样本量超线性增长C、核参数 $\gamma$3. 用 MATLAB 跑通黄广斌风格的 ELM 最小代码3.1 完整可跑的 elm_train 与 elm_predict下面这两段保留了黄广斌那版 ELM 代码最核心的写法包括输入权重在 $[-1,1]$ 均匀采样、偏置在 $[0,1]$ 采样、输出权重用伪逆一步求解。function [IW, B, LW, TF] elm_train(P, T, N, TF) % ELM 训练函数黄广斌经典代码风格 % P : 输入矩阵R×Q每列是一个样本 % T : 目标矩阵S×Q每列是对应样本的期望输出 % N : 隐层节点数 % TF: 激活函数sig / sin / hardlim [R, Q] size(P); % 随机输入权重统一落在[-1,1] IW rand(N, R) * 2 - 1; % 随机偏置[0,1] 均匀分布 B rand(N, 1); % 计算隐层输出矩阵 HN×Q H IW * P repmat(B, 1, Q); switch TF case sig H 1 ./ (1 exp(-H)); case sin H sin(H); case hardlim H double(H 0); end % 输出权重最小范数最小二乘解等价于求解 Hβ T LW pinv(H) * T; endfunction Y elm_predict(P, IW, B, LW, TF) % P : 待预测输入R×Q % Y : 预测结果S×Q Q size(P, 2); H IW * P repmat(B, 1, Q); switch TF case sig H 1 ./ (1 exp(-H)); case sin H sin(H); case hardlim H double(H 0); end % 还原成 S×Q 的输出布局 Y (H * LW); end这段代码里最容易被忽略的是数据布局P 是 R×Q每一列是一个样本而不是像 sklearn 或 pandas 那样按行存样本。从 Python 转过来的人第一版代码经常会在这里把转置写错导致 H 的维度直接爆掉。建议在加载数据之后统一转置同时用size(P, 1)和size(P, 2)打印出来核对一遍。LW pinv(H) * T从数学上等价于对 $H\beta T$ 求最小范数最小二乘解。之所以不直接写pinv(H) * T是因为这里 H 被组织成 N×Q输出权重按 S×N 的转置形式存放这样做能让预测函数里的(H * LW)直接还原出 S×Q 的矩阵和训练数据的目标布局保持一致。3.2 伪逆什么时候会不够用手动加正则化直接用伪逆求解在隐层节点数不多、数据干净的情况下表现很好。但如果把 L 加到很大或者训练数据里带了明显噪声$H$ 会出现近似线性相关的列伪逆解会放大噪声。常见的做法是把输出权重的求解改成带正则化的正规方程% 正则化系数 C一般从 1 开始试 C 1; % 当样本数 Q 隐层节点数 N 时用这个形式 LW (H * H eye(N) / C) \ (H * T); % 当隐层节点数比样本数还大时换用这个形式避免大矩阵求逆 % LW H / (H * H eye(Q) / C) * T;这一改动等价于在目标函数里加了 $\frac{1}{C}|\beta|^2$ 这一项。它不会让训练误差变成零但会让验证误差变得更稳。调 C 的经验值是从 1 开始训练误差太高就往大调验证误差反弹就往小调后文第五章会给出完整的扫描顺序。3.3 把同一套逻辑移植到 Python 的 numpy很多新项目已经不再使用 MATLAB但 ELM 的核心逻辑在 numpy 里也就是十几行的事。下面的实现改成按行存样本符合 Python 生态的习惯import numpy as np def elm_fit(X, y, hidden100, CNone, actsigmoid): # X: n×d每行一个样本y: n×m 或 n n, d X.shape rng np.random.default_rng(0) # 随机投影矩阵[-1, 1] W rng.uniform(-1, 1, (d, hidden)) b rng.uniform(-1, 1, (hidden,)) H X W b # n×hidden if act sigmoid: H 1 / (1 np.exp(-H)) elif act sin: H np.sin(H) if C is None: # 经典伪逆解 beta np.linalg.pinv(H) y else: # 带正则化的正规方程 beta np.linalg.inv(H.T H np.eye(hidden) / C) H.T y return W, b, beta def elm_predict(X, W, b, beta, actsigmoid): H X W b if act sigmoid: H 1 / (1 np.exp(-H)) elif act sin: H np.sin(H) return H beta这套代码和 MATLAB 版本的区别只在矩阵布局上Python 版直接要求 X 每行一个样本H 是 n×hiddenbeta 是 hidden×m预测是 H beta。两种布局没有优劣之分只是约定不同但如果在同一个项目里混用两套代码转置问题会非常折磨人建议固定一种。3.4 L、C 和激活函数的初始值参考表参数初始值范围影响不良表现L 隐层节点数输入维度的 510 倍特征空间容量过小欠拟合过大训练误差低但验证误差反弹C 正则化系数1之后 1e-21e6 对数扫描输出权重幅度过小欠拟合过大等于无正则化激活函数sig / tanh 优先非线性映射形态周期数据用 sin 更稳hardlim 适合二分类激活函数的选择对 ELM 的影响比很多人想象得大。sigmoid 的输出被压缩到 $(0,1)$适合大多数连续值回归tanh 零中心化梯度特性稍好sin 在处理有周期震荡的数据时隐层输出能保留相位信息效果经常比 sigmoid 好一截。hardlim 则由于不可导的特性在 BP 里没法用但在 ELM 里反而因为不需要反向传播而可以随意使用。4. ELM 实战回归、分类和两个必踩的坑4.1 特征归一化不做归一化的后果ELM 的隐层输出是 $H IW \cdot P b$其中 $IW$ 是在 $[-1,1]$ 均匀采样的随机矩阵。如果某个特征的取值范围是 $[0, 10^5]$这个特征和随机权重相乘后就会主导整个线性组合激活函数很容易饱和到同一个值等于这个特征维度变成了一个常数。常见做法是在训练前把输入归一化到 $[-1,1]$测试集使用训练集相同的映射参数[Xn, psX] mapminmax(X, -1, 1); % 测试集必须用训练集的映射参数不能重新独立归一化 Xn_test mapminmax(apply, X_test, psX);注意mapminmax默认是按行处理而 ELM 代码里每一列是一个样本所以传入前要做一次转置。目标变量最好也归一化尤其是回归任务输出落在激活函数最容易表达的非饱和区间可以降低对输出权重的数值压力。4.2 回归任务拟合带噪声的 sinc 函数下面用 1000 个样本拟合 $\sin(x)/x$刻意加一点高斯噪声来模拟真实数据。这个例子在 CPU 上跑完训练加预测不会超过 0.1 秒。% 生成数据sinc 函数加噪声 N 1000; x linspace(-10, 10, N); y sin(x) ./ x; y(isnan(y)) 1; % 修正 x0 处的 0/0 y y 0.05 * randn(N, 1); % 加噪声 % 归一化并转成每列一个样本的布局 [Xn, psX] mapminmax(x, -1, 1); [Yn, psY] mapminmax(y, -1, 1); % 随机划分训练集和测试集 rng(1); idx randperm(N); trIdx idx(1:700); teIdx idx(701:end); % 训练和预测 [IW, B, LW, TF] elm_train(Xn(:, trIdx), Yn(:, trIdx), 80, sig); Yhat elm_predict(Xn(:, teIdx), IW, B, LW, sig); % 反归一化并计算 RMSE Yhat mapminmax(reverse, Yhat, psY); rmse sqrt(mean((Yhat - y(teIdx)).^2)); fprintf(RMSE %.4f\n, rmse);这里把隐层节点数设成 80对一维输入来说已经足够。如果 RMSE 明显偏高先检查数据归一化是否做对再检查激活函数是否饱和如果训练集 RMSE 很低而测试集很高问题出在 L 太大或没有加正则化回到第 3.2 节把正则化版本换上即可。4.3 分类任务的输出编码one-hot 还是标签用 ELM 做分类时目标矩阵不能直接用 1、2、3 这种整数标签作为回归目标否则等价于给类别之间强加了一个序关系类别 1 和类别 2 的距离会被当成类别 1 和类别 3 距离的一半。常见做法是把标签展开成 one-hot 矩阵比如 3 类问题时目标矩阵是 3×Q每一列是某个样本的类别指示向量。% 假设 y_label 是 1×Q 的行向量取值 1..S S max(y_label); T full(ind2vec(y_label, S)); % S×Q 的 0/1 矩阵 % 预测后取每列最大值的下标作为类别 Y elm_predict(P_test, IW, B, LW, sig); [~, pred] max(Y, [], 1);如果预测矩阵的每一列数值都非常接近比如 sigmoid 输出都在 0.5 附近说明隐层特征没能把类别分开。优先检查归一化其次把 L 调大再不行就换 sin 或 tanh 激活函数。4.4 训练误差低、测试误差高先查 H 的条件数遇到训练误差很低但测试误差明显恶化不要急着加正则化或调结构先看 $H$ 的条件数。条件数过大说明 $H$ 的列近似线性相关伪逆解会把微小噪声放大成很大的输出权值。% 在训练函数里把 H 保存出来然后执行 cH cond(H, 2); fprintf(H condition number: %.2e\n, cH);条件数超过 $10^{12}$ 基本可以断定 $H$ 病态。此时把正则化系数 C 设在 0.01 到 1 之间通常能让条件数迅速回落。另一个方向是把 L 减半因为隐层节点数过多往往是列相关的根源。这个检查步骤比反复试 C 更值得先做因为它直接告诉你是“数值问题”还是“模型容量问题”。5. 三个能直接抄的 ELM 进阶技巧交叉验证选 L、核 ELM、C 的粗调顺序5.1 用 k 折验证选隐层宽度训练误差随 L 增大而下降几乎必然发生所以只看训练误差选 L 一定会选出过拟合模型。标准做法是固定 C 为 1对候选 L 做 k 折验证。from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state0) for L in [20, 50, 100, 200, 400]: val_loss [] for tr, va in kf.split(X): W, b, beta elm_fit(X[tr], y[tr], hiddenL, C1.0) pred elm_predict(X[va], W, b, beta) val_loss.append(np.mean((pred - y[va]) ** 2)) print(L, np.mean(val_loss))验证误差随 L 的变化通常是先下降后上升最低点附近就是合适的容量范围。如果最小值出现在列表边缘说明搜索区间没覆盖到位应该把 L 向那个方向继续延伸。5.2 核 ELM 把随机性彻底去掉如果不想纠结隐层宽度直接用核版本。核 ELM 不再显式构造 $H$而是计算样本间的核矩阵$$ f(x) k_x^T \left( \frac{I}{C} \Omega \right)^{-1} T $$其中 $\Omega_{ij} K(x_i, x_j)$ 是训练集的核矩阵$k_x$ 是待预测样本与所有训练样本的核向量。RBF 核是最常用的选择核带宽用中位距离做启发式初始化。优点是没有随机性同样的问题每次跑结果完全一致也不需要选 L缺点是需要保存 $N \times N$ 的核矩阵样本超过一万以后内存压力很大。5.3 C 的粗调规律与推荐顺序C 的常见调参错误是只在 ${0.1, 1, 10}$ 里选但这个区间往往不够。ELM 输出权重的最小二乘解对 C 的动态范围响应很宽我会从 $10^{-2}$ 到 $10^6$ 按对数间隔取十个值固定 L 后逐个跑验证集记录验证误差。训练误差会随着 C 增大单调下降验证误差则先是缓慢下降进入过拟合区后反弹。取验证误差最低处对应的 C一般会是 $10^0$ 到 $10^4$ 之间的某个值。如果整个区间里验证误差都没有明显变化说明模型容量本身才是瓶颈回头加大 L 比继续扫 C 更有效。把“先在 C1 时选 L再固定 L 扫 C最后试 sin 或 tanh 激活函数”这套顺序固定下来能省掉大多数 ELM 调参里反复做无用功的时间。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价