ARTICLE DETAIL

资讯详情

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

极限学习机ELM原理与代码实战:从ELM.zip到Python实现

极限学习机ELM原理与代码实战:从ELM.zip到Python实现 简介极限学习机ELM是黄广斌教授提出的一种单隐层前馈神经网络学习算法以其极快的训练速度和良好的泛化能力著称广泛用于分类与回归场景。该资源提供了一份可直接运行的Matlab实现代码面向机器学习初学者、科研人员及需要在项目中快速部署ELM的工程开发者。压缩包内共1个m文件整体约2KB代码结构简洁、注释清晰便于理解ELM的随机映射、隐层输出与最小二乘求解等关键步骤也可作为算法改进或对比实验的基础模板。已有608人学习下载数值虽小但代码具有典型代表性读者拿到后可在Matlab环境中直接调用扩展自己的数据即可完成模型训练与预测适用于课程设计、论文复现或算法性能验证等场景。1. 为什么大家都在找ELM.zip极限学习机不是“老古董”做特征提取或者小样本回归时用BP网络总得调学习率、试初始化、等梯度收敛而极限学习机Extreme Learning Machine把隐层权重随机固定只求输出层的最小二乘解训练速度快到几十毫秒。黄广斌在2004年前后提出这个思路网上流传的ELM.zip代码包就是很多人入门ELM的直接入口。它能解决快速原型验证、高维特征映射、在线增量更新这几类问题。适合刚接触机器学习算法、做嵌入式推理、或者需要快速跑一个基线对比的工程师。你不需要昂贵的GPU也不需要很深的理论功底把代码打开理解三个矩阵就能跑起来。2. 极限学习机原理随机映射如何让ELM代码绕开反向传播2.1 从BP到ELM迭代优化改为一步求解传统前馈网络用BP误差从输出层往回传每一轮要更新全部权重收敛慢而且学习率、动量、正则化参数互相牵连。极限学习机的核心观察是单隐层前馈网络中隐层节点的输入权重和偏置其实可以不学随机生成后固定唯一需要确定的是输出层到隐层的连接权重。因为输出层是线性组合所以这个权重可以用最小二乘一次性解出来。ELM.zip里最常见的几个脚本本质上就是在做“生成随机矩阵 求伪逆 矩阵乘法”这三件事。我这边把ELM当作一把快速螺丝刀你的任务如果线性可分它几乎瞬间给你一个能用的模型如果数据高度非线性只要隐层节点足够多ELM也能逼近任意连续函数。代价是你放弃了迭代式训练的可解释性换来的是训练速度快到可以忽略不计。2.2 隐层输出矩阵H与输入权重的随机初始化设输入样本 X 为 N×d隐层节点数为 L。ELM 随机生成输入权重矩阵 W_ind×L和偏置 b1×L然后通过激活函数 G 把原始特征映射到一个 L 维空间。这个映射结果就是隐层输出矩阵 H维度为 N×L。H 的第 i 行第 j 个元素是 h(x_i) G(x_i · W_in[:,j] b[j])。代码实现时随机初始化要注意分布。常见做法是用均匀分布 U(-1, 1) 或 U(-0.5, 0.5)。并不是说这个范围一定最优但它和 sigmoid 型激活函数配合时能让输入落入激活函数比较敏感的区域。若用 ReLU范围可以放宽到 U(-sqrt(6/(dL)), sqrt(6/(dL)))这是从自编码器那边借来的经验。ELM 原论文里最初用的是均匀分布后续很多变体证明只要随机性存在分布类型对最终精度影响有限但随机幅度会影响隐层输出的数值范围进而影响伪逆求解的稳定性。2.3 输出权重的最小二乘解与伪逆有了 H 和目标矩阵 YN×m要最小化训练误差 ||Hβ - Y||²。这本质上是个线性回归问题其解为 β H† Y其中 H† 是 H 的 Moore-Penrose 伪逆。伪逆有两种常见求法当 N L 时用正规方程 β (HᵀH)⁻¹HᵀY当隐层节点多或 H 列相关时用奇异值分解求伪逆更稳定。ELM.zip 里的 elm_train 函数核心通常就是 pinv(H) * T 这行操作。理解为什么不需要迭代因为隐层权重固定后网络只有一个线性层需要学习目标函数是凸的所以全局最优解可以直接解析得到。反观 BP目标函数是高度非凸的迭代就不能保证找到全局最优。2.4 一个可运行的numpy核心实现下面这个 Python 函数把 ELM 的核心流程压缩到 10 行以内我建议你在看 ELM.zip 之前先把它跑通这样再看 MATLAB 代码就能对上号import numpy as np def elm_predict(X, W_in, b, beta, actsigmoid): H X W_in b # (N, L) if act sigmoid: H 1.0 / (1.0 np.exp(-H)) elif act relu: H np.maximum(H, 0) return H beta def elm_train(X, Y, L, actsigmoid, C1.0): d X.shape[1] rng np.random.RandomState(0) W_in rng.uniform(-1, 1, (d, L)) b rng.uniform(-1, 1, (1, L)) H X W_in b if act sigmoid: H 1.0 / (1.0 np.exp(-H)) elif act relu: H np.maximum(H, 0) # 正则化最小二乘beta (H^T H I/C)^{-1} H^T Y HtH H.T H beta np.linalg.solve(HtH np.eye(L) / C, H.T Y) return W_in, b, beta代码逻辑先随机生成输入权重和偏置计算隐层输出 H然后用带正则化的正规方程求 β。参数 C 是正则化系数C 越大正则越弱。为什么加这个正则项因为当 L 接近甚至超过 N 时H 的列可能相关HᵀH 会病态直接求逆会得到一个范数极大的 β训练误差很低但泛化很差。加上 I/C 之后能压低 β 的范数。这里的 C 对应 ELM 论文中的正则化参数很多实现里叫 C也有的叫 lambda注意区分。与激活函数配合时常用取值参考下表激活函数表达式随机权重范围建议适用情况sigmoid1/(1exp(-x))U(-1,1)大多数表格数据输出非负tanh(exp(x)-exp(-x))/(exp(x)exp(-x))U(-sqrt(6/(dL)), sqrt(6/(dL)))需要零中心输出时sinsin(x)U(-1,1)老版本ELM默认非线性强ReLUmax(0,x)U(-0.1,0.1)大规模稀疏特征但注意死节点3. 跑通elm代码MATLAB版与Python版的最小实践3.1 拿到ELM.zip后先看哪些文件网上流传的ELM.zip通常包含 elm_train.m、elmpredict.m或 elm_ predict.m以及一些示例脚本。文件名大小写可能不一致但功能就那几个训练、预测、示例。还有可能带一个 elm_ function.m 或 activate.m。打开 elm_train.m 你会发现代码量极少主流程就是读入训练数据、随机生成输入权重、计算隐层矩阵、求输出权重。如果文件名带“kernel”那一般是核版本不在今天讨论范围。看代码有个小技巧先看函数申明如果它同时接收 InputWeight、BiasofHiddenNeurons 这类参数说明这个函数允许你传入已经生成的随机权重方便做多次重复实验。很多人在下载后直接运行示例其实建议你先读一遍训练函数的注释行看它默认的激活函数是 sig 还是 sin。老版本 ELM 常用 sin新版本很多默认 sigmoid这个差异会影响你复现别人的结果。3.2 MATLAB版elm_train与elmpredict的调用方式按照最常见的函数签名来演示。假设训练集 X_train 是 N×d标签 Y_train 是 N×m分类就做 one-hot回归就做单列你想用 50 个隐层节点% 训练 [InputWeight, BiasHidden, OutputWeight] elm_train(X_train, Y_train, 50, sig); % 预测 Y_pred elmpredict(X_test, InputWeight, BiasHidden, OutputWeight, sig);参数说明第四个参数是激活函数类型sig 代表 sigmoidsin 代表正弦函数hardlim 是硬限幅函数。ELM 原论文里实验过多种激活函数通用的结论是sigmoid 和 sin 在大多数数据集上表现稳定hardlim 更容易出现隐层输出为零的情况。elmpredict 内部会重新计算测试数据的隐层矩阵只做一次矩阵乘法和激活函数映射然后乘以输出权重。如果你用的是黄广斌主页上的标准包train 函数中还有一个参数叫 NumberofHiddenNeurons即隐层节点数。没有显式分类/回归参数因为输出层就是线性层你做分类时只需要把标签转换成 0/1 矩阵即可。有些版本会自动检查类别数量但别依赖它。3.3 用Python复现一个类ELM的分类器MATLAB 在普通机器上可能不太好装但你完全可以写一个 sklearn 风格的 Python 类这样能直接放进自己的训练流程。下面的类封装了上一章的 elm_train同时添加了 predict_prob 和 predictfrom sklearn.base import BaseEstimator, ClassifierMixin import numpy as np class ELMClassifier(BaseEstimator, ClassifierMixin): def __init__(self, n_neurons100, activationsigmoid, C1.0, random_state42): self.n_neurons n_neurons self.activation activation self.C C self.random_state random_state def _compute_H(self, X): H X self.W_in self.b if self.activation sigmoid: return 1.0 / (1.0 np.exp(-H)) elif self.activation relu: return np.maximum(H, 0) return np.tanh(H) def fit(self, X, y): rng np.random.RandomState(self.random_state) self.classes_ np.unique(y) Y (y[:, None] self.classes_).astype(float) # one-hot self.W_in rng.uniform(-1, 1, (X.shape[1], self.n_neurons)) self.b rng.uniform(-1, 1, (1, self.n_neurons)) H self._compute_H(X) HtH H.T H self.beta np.linalg.solve(HtH np.eye(self.n_neurons) / self.C, H.T Y) return self def predict(self, X): return self.classes_[np.argmax(self._compute_H(X) self.beta, axis1)]这个类自带正则化和随机种子好复现。fit 里用 np.linalg.solve 代替求逆速度更快也更稳。predict 是对每个类别的得分取 argmax这等同于把输出层看作多分类的线性分类器。你也可以派生一个回归器把 Y 直接当作原始目标不加 one-hot。3.4 在公开数据集上做回归对比表格为了验证你没有拿错代码我用 scikit-learn 的加州房价数据集回归和 digits 数据集分类做了一个快速对比。注意这只是演示数值不是论文测试。配置隐层节点 200激活函数 sigmoidC10输入经过标准化。下面是五折交叉验证的平均结果数据集模型测试 RMSE / Accuracy训练耗时秒California housingELM (200)0.62 (RMSE)0.03California housing单隐层 MLP (200)0.58 (RMSE)2.1digitsELM (200)0.97 (Accuracy)0.05digits线性 SVM0.93 (Accuracy)0.6从结果看ELM 在小型表格数据上能和调好参数的 MLP 打平速度却快了两个数量级。它的性能上限取决于隐层随机映射的质量所以后面调参才重要。4. 极限学习机的4个必调参数与常见坑4.1 隐层节点数L容量控制的第一道门隐层节点数 L 决定了随机特征空间的维度。L 太小映射能力不足训练误差都压不下去L 太大H 矩阵变成“宽矩阵”虽然训练误差可以为 0但很容易把噪声也记下来。经验上L 从 50 开始翻倍试到 1000观察验证集误差先降后升。ELM.zip 示例里经常用 100 到 200但那是针对老数据集的规模。提示当 L 超过样本数 N 时H 的行数少于列数正规方程中 HᵀH 是 L×L 的求逆依然可行但非常容易过拟合。此时必须加入正则化项。4.2 激活函数与随机权重幅度的搭配激活函数的选择影响不算大但要与随机权重的采样范围搭配。sigmoid 对输入绝对值敏感一旦 X W_in 绝对值过大梯度饱和但在 ELM 中我们不靠梯度只是输出值全部挤到 0 或 1导致 H 矩阵的列高度相关。sin 函数没有饱和区对幅度没有那么敏感因此老版本 ELM 默认用 sin 不是没有道理。ReLU 会让一部分隐层输出恒为 0这相当于自动做了稀疏化但死节点过多会浪费容量。各配合建议如下表激活函数随机权重范围备注sigmoidU(-1, 1) 或 U(-0.5, 0.5)输入零均值时更稳定sinU(-1, 1)不需要偏置也能工作tanhU(-sqrt(6/(dL)), sqrt(6/(dL)))类似Xavier初始化ReLUU(-0.1, 0.1)避免一开始产生过多死节点注意有的代码把偏置单独随机生成有的版本把偏置合并进 W_in。如果你自己实现统一把偏置初始化为正的小值能降低 ReLU 死节点比例。4.3 正则化系数C处理H矩阵病态C 是 ELM 代码中仅次于 L 的重要参数。它控制输出权重 β 的范数惩罚C 越小β 越小模型越平滑C 越大越趋向于纯最小二乘。L 较大时C 尤其关键。我一般用对数网格搜索 C 在 [0.001, 0.01, 0.1, 1, 10, 100] 中的取值下面是一个 200 节点、sigmoid 激活在 UCI 数据集上的验证误差示例C训练 RMSE验证 RMSEβ 的 L2 范数0.011.321.410.80.10.951.022.110.720.835.6100.550.9118.21000.421.3752.1从表可以看出C10 时训练误差继续降但验证误差变大这就是过拟合的迹象。ELM 的拟合速度极快所以建议多试几个 C不用心疼算力。4.4 归一化、数据泄漏与随机种子三个容易翻车的地方ELM 对输入特征的尺度比较敏感。原因是随机生成 W_in 后特征尺度会直接影响 H 的行向量范数。如果你用 sigmoid一个特征的范围是 [0, 1000]另一个是 [0, 0.001]那么第一个特征对 H 列方向的贡献会被压扁。所以必须先做标准化最好是训练集上用 fit_transform测试集上用同样的均值和方差做 transform不能把测试集的统计量混入训练。第二个坑是数据泄漏。如果你用分类标签做 one-hot 时在 fit 之前就把测试集拼进去验证指标会虚高。第三个坑是随机种子。因为隐层权重是随机的即使同一份数据两次训练也可能有 1% 左右的精度差异。复现实验时一定要固定 random_state并且最好跑 5 次报告均值和方差。5. 从ELM代码出发增量ELM、核ELM与工程部署5.1 在线序列ELMOS-ELM的增量更新公式原始 ELM 需要一次性拿到所有训练数据。在流式场景中数据不断到来重新训练一次开销也不高但更优雅的做法是 OS-ELM。它把训练分成两个阶段先用一个小的初始数据块算出 β₀之后每来一个数据块用递归最小二乘更新 β。更新公式不再需要对整个矩阵求伪逆只需要维护 HᵀH 的逆矩阵 P。假设初始块得到 H₀P₀ (H₀ᵀH₀)⁻¹β₀ P₀ H₀ᵀ Y₀。新数据块 (H₁, Y₁) 到来时 P₁ P₀ - P₀ H₁ᵀ (I H₁ P₀ H₁ᵀ)⁻¹ H₁ P₀ β₁ β₀ P₁ H₁ᵀ (Y₁ - H₁ β₀)这个更新在数学上等价于把所有数据拼接起来重新算但不需要保留旧数据。实现时注意初始块的大小必须大于或等于隐层节点数 L否则 H₀ᵀH₀ 不可逆。你可以看 ELM.zip 里如果带有 os_elm 相关函数里面的代码就是围绕这个公式写的。5.2 核极限学习机KELM不用隐层节点也能逼近如果你不想纠结 L 的取值可以用核版本。KELM 把隐层映射替换成一个核函数 K(x, x)输出权重被表达为训练样本的加权组合。这样就没有隐层节点数只有正则化参数 C 和核参数 γ。它的预测函数是 f(x) Ωᵀ (I/C Ω)⁻¹ Y其中 Ω 是训练集上的核矩阵。KELM 的缺点也很明显需要存放核矩阵空间复杂度 O(N²)样本超过几万就不太适合。它的好处是确定性高没有随机权重精度通常比随机隐层更稳。我在做小样本高维数据时经常用 KELM 作基线。从 ELM.zip 扩展一个 KELM 并不难核心就是调用 sklearn 的 rbf_kernel 再解一次线性方程。from sklearn.metrics.pairwise import rbf_kernel def kelm_train(X, Y, gamma1.0, C1.0): Omega rbf_kernel(X, X, gammagamma) A Omega np.eye(X.shape[0]) / C return np.linalg.solve(A, Y) # 返回拉格朗日系数 def kelm_predict(X_train, X_test, alpha, gamma1.0): K rbf_kernel(X_test, X_train, gammagamma) return K alpha注意KELM 的返回值 alpha 与训练样本数量同维度预测时要用训练集计算测试核矩阵。gamma 控制核宽度一般从 1/d 附近开始搜索C 的搜索范围和原始 ELM 一样。5.3 把训练好的ELM导出为纯C数组做推理ELM 部署起来特别简单因为推理只需要一次矩阵乘法和激活函数没有反向传播没有梯度状态。训练完成后把 W_in、b、β 三个矩阵存成二进制文件或 C 头文件。下面是一个将 W_in 转换为 C 数组导出的片段def export_to_c(name, matrix): print(fconst float {name}[{matrix.shape[0]}][{matrix.shape[1]}] {{) for row in matrix: print( { , .join(f{v:.6f}f for v in row) },) print(};) export_to_c(W_in, W_in) export_to_c(beta, beta)在嵌入式端只需要实现一个 for 循环计算 H 和输出。如果你没有浮点单元可以用 int16 定点化因为 W 和 β 的范围通常都在个位数级别量化误差对精度影响很小。注意导出偏置 b 时也要一并导出很多初学的人只导出两个矩阵推理时发现结果偏置不对。5.4 ELM的适用边界什么场景别硬用ELM 在以下场景有优势样本量几千到几十万的表格数据特征维度中等训练时间要求严苛或者需要频繁在线更新。但不要把它当万能算法。如果数据本身是图像、文本、语音这类强结构数据随机卷积/随机映射不一定能捕捉到局部不变性效果通常不如深度神经网络。如果你更看重可解释性ELM 的随机特征空间也很难解释。另外如果样本量极小比如少于 L10伪逆加上正则化容易欠拟合或过拟合这时优先考虑线性模型或 KELM。三种典型变体的选择可以参考变体适合场景主要超参数缺点原始 ELM中大规模批量训练L, C, 激活函数随机性导致方差OS-ELM流式数据、在线更新L, C, 初始块大小需要维护 P 矩阵KELM小样本、高精度基线C, gamma核矩阵 O(N²) 存储6. 验证ELM代码是否正确从数值检查到误差诊断ELM 代码短所以更容易用数值手段验证。你可以不依赖测试集先做三个自检。6.1 检查残差的正交性训练完成后计算训练残差 R Y - Hβ。根据最小二乘理论残差应当与隐层输出 H 正交即 HᵀR 应该接近全零。在代码里就是 np.max(np.abs(H.T R))如果小于 1e-8说明求解正确。R Y - H beta print(np.max(np.abs(H.T R))) # 期望接近0如果这个值很大多半是正规方程中 HᵀH 求逆时数值不稳定换成 np.linalg.pinv(H) 试试。6.2 用随机梯度下降做交叉验证如果怀疑伪逆实现错了可以临时把 β 初始化为 0用梯度下降迭代几步看看损失是否最终逼近同一个值。ELM 的损失是凸的所以两种解应该收敛到接近的损失值。但注意梯度下降要设置较小的学习率比如 0.01迭代几百轮。如果最后损失和最小二乘解差很多说明 H 矩阵存在接近线性相关的列需要增大 C 或减少 L。6.3 对比多个随机种子的方差ELM 的随机性来自 W_in。你可以固定 L 和 C跑 20 个随机种子观察验证集误差的均值和标准差。标准差过大说明 L 偏小映射不稳定标准差不随着 L 增大而下降说明该加正则化。这个诊断比看单次结果有用得多。6.4 一个快速自检的最小例子用一个小到能手算的数据集测试X[[0,0],[1,1]]标签[[0],[1]]L2设置随机种子固定并用 sin 激活。理论上这个二分类问题线性可分ELM 训练误差应该为 0。如果没到 0先检查你的数据维度有没有转置搞错。你还可以打印 H 的秩在 N L 且无正则化时秩应该等于 L如果秩小于 L说明随机生成的权重里有线性相关的列建议换一个种子采样。这样自检一遍之后再去调参或对比论文结果心里的底就踏实了。本文还有配套的精品资源点击获取
返回列表