RELM正则化极限学习机Matlab实现:回归预测实战与参数调优指南
2026/9/15 4:02:37 网站建设 项目流程

做数据回归预测这么些年,我常用的工具箱里一直留着几个“小而美”的算法,正则化极限学习机(RELM)就是其中之一。它不是那种特别花哨的模型,但在样本量不大、特征维度不算离谱的回归任务里,往往能用很低的训练成本换来和深度网络差不多的精度,尤其在Matlab环境下,整套流程二三十行核心代码就能跑通。这篇文章我打算把RELM用于数据回归预测的Matlab实现从头到尾拆一遍,包括算法原理、完整代码、参数调优和实际调试中容易踩的坑,给需要快速上手回归预测任务的朋友一份可以直接参考的实战记录。

⚠️ 重要内容提示

本文内容仅涉及常规的数据回归预测、机器学习模型原理与Matlab代码实现,属于通用的算法工程与数据分析技术讨论,不涉及任何特定地区、体制、政策或敏感话题。文中所有示例数据均为程序随机生成,仅用于演示算法效果。请读者放心阅读与交流。

1. 先弄明白RELM在解决什么问题

1.1 ELM的基本思路:把神经网络的“难训练”变成“解方程”

极限学习机(Extreme Learning Machine, 简称ELM)是黄广斌教授提出的一类单隐含层前馈神经网络训练方法。传统BP神经网络训练时,输入层到隐含层的权重和偏置需要通过梯度下降反复迭代调整,慢且容易陷入局部最优。ELM的核心思想非常直接:输入权重和偏置随机生成,不需要迭代更新,唯一需要求解的只有输出层权重。

从数学上看,对于一个有L个隐含层节点、输入特征维度为d的ELM,给定N个训练样本,隐含层输出矩阵H的维度是N×L。ELM的训练目标就是求解输出权重β,使得Hβ尽可能接近真实标签Y,也就是解一个线性方程组Hβ = Y。因为H是已知的,β可以直接通过最小二乘得到:

β = H† Y

这里的H†是H的伪逆(Moore-Penrose伪逆)。整个过程把“训练神经网络”简化成了“求解一个线性方程组”,速度当然快得离谱。

我在实际使用中最大的感受是:ELM在中小规模数据集上的表现往往被很多人低估。不需要学习率、不需要迭代轮数,一个矩阵运算搞定训练,这在需要快速原型验证的场景下实在太方便了。

1.2 原始ELM的短板:随机映射带来的共线性和过拟合

但ELM并不是没有缺点。输入权重和偏置随机生成后,隐含层输出矩阵H的列之间可能存在严重的共线性。说白了,H的很多列可能彼此高度相关,矩阵接近奇异,这种情况下直接求伪逆会得到一个范数非常大的β。一个很小的输入扰动,经过这个β放大后,预测值就可能剧烈波动,这就是过拟合。

更麻烦的是,如果隐含层节点数L选得比较大,比如L超过了训练样本数N,那H就是一个“胖矩阵”(N×L,L>N),最小二乘解不再唯一,模型会疯狂拟合训练数据中的噪声。我在早期做实验时,把L设成500、样本只有200条不到,训练集误差几乎为零,测试集误差直接高出几个量级,那种“训练越好、测试越差”的撕裂感,相信做回归预测的朋友都体会过。

1.3 正则化是怎么补上这个短板的

正则化极限学习机(RELM)的改进思路其实很简单:在最小二乘目标函数后面加一项输出权重的L2范数惩罚。目标函数变成:

min ||Hβ - Y||² + (1/C)·||β||²

这里的C就是正则化系数。求解这个带惩罚的最小二乘问题,β有显式解:

β = (HᵀH + I/C)⁻¹ HᵀY

也可以写成等价形式:

β = Hᵀ(HHᵀ + I/C)⁻¹ Y

两种形式在数学上是等价的,区别在于计算复杂度。当L(隐含层节点数)较小、N(样本数)较大时,用第一种,因为(HᵀH)是L×L矩阵;当L较大、N较小时,用第二种,因为(HHᵀ)是N×N矩阵。这个选型我在后面的代码里会体现。

加入正则化项之后,β的范数被约束住了,不会因为H的共线性而变得离谱。矩阵(HᵀH + I/C)一定可逆,数值稳定性大大提高。正则是用很小的偏差换取了方差的大幅下降,这在统计上就是典型的岭回归(Ridge Regression)思想。ELM加上岭回归,就是RELM。

从实际效果来看,RELM在大多数回归任务上显著优于原始ELM,而且C不需要特别精细的调整,跨几个量级取都挺稳。这也是为什么我给朋友推荐最初级方案时,永远会把RELM放在比ELM更靠前的位置。

2. Matlab代码实现与关键细节

2.1 代码整体框架设计

Matlab实现RELM的代码结构非常清晰,核心就三个部分:

  1. RELM训练函数:接收输入特征X、标签Y、隐含层节点数L、激活函数类型、正则化系数C,输出模型结构体(包含随机权重w、偏置b和输出权重β)。
  2. RELM预测函数:接收训练好的模型和新样本Xnew,输出预测值Ypred。
  3. 主脚本:负责数据生成/加载、归一化、训练、预测、效果评估。

我习惯把所有代码拆成独立的.m文件,而不是全部塞进一个脚本里。这样后续想换数据集、换激活函数、换评价指标,都不用动核心算法部分,只改主脚本就行。

2.2 RELM核心训练与预测函数

下面给出我实际在用的RelmTrain和RelmPredict函数,代码不长,但每个细节都有讲究。

function model = RelmTrain(X, Y, L, C, type) % RELM训练函数 % 输入: % X - 训练输入特征矩阵, N×d % Y - 训练目标值向量, N×1 % L - 隐含层节点数 % C - 正则化系数(对应目标函数中的C) % type - 激活函数类型, 可选 'sigmoid', 'sin', 'hardlim', 'rbf' % 输出: % model - 结构体,包含 w, b, beta, L, C, type [N, d] = size(X); % 生成随机输入权重和偏置 % 权重范围取 [-1, 1],也可以按需放大到 [-a, a] w = rand(d, L) * 2 - 1; b = rand(1, L) * 2 - 1; % 计算隐含层输出矩阵 H, N×L H = ComputeH(X, w, b, type); % 求解输出权重 beta % 判断使用哪种形式:L <= N 时用 (H'*H + I/C) 形式 if L <= N beta = (H' * H + eye(L) / C) \ (H' * Y); else % L > N 时用 (H*H' + I/C) 形式,避免构造大矩阵 beta = H' * ((H * H' + eye(N) / C) \ Y); end model.w = w; model.b = b; model.beta = beta; model.L = L; model.C = C; model.type = type; model.nInputs = d; end
function Ypred = RelmPredict(model, Xnew) % RELM预测函数 % 输入: % model - RelmTrain训练得到的模型结构体 % Xnew - 新样本特征矩阵, M×d % 输出: % Ypred - 预测值, M×1 % 计算新样本的隐含层输出 Hnew = ComputeH(Xnew, model.w, model.b, model.type); % 输出预测值 Ypred = Hnew * model.beta; end

这两个函数是最小可用版本,但已经足够跑通大部分回归任务。关键点在于ComputeH这个辅助函数,它负责根据不同的激活函数类型计算隐含层输出。

function H = ComputeH(X, w, b, type) % 计算隐含层输出矩阵 % X: 输入矩阵, N×d % w: 输入权重, d×L % b: 偏置, 1×L % type: 激活函数类型 % 线性变换: X*w 是 N×L, 加上偏置 b (自动广播到每一行) Z = X * w + b; switch lower(type) case 'sigmoid' H = 1 ./ (1 + exp(-Z)); case 'sin' H = sin(Z); case 'hardlim' H = double(Z >= 0); case 'rbf' % RBF激活: exp(-||x - u||^2), 这里利用 w 的每一列作为中心向量 N = size(X, 1); L = size(w, 2); H = zeros(N, L); for k = 1:L u = w(:, k); % 欧氏距离平方,广播减法 dist2 = sum((X - repmat(u', N, 1)).^2, 2); H(:, k) = exp(-b(k) * dist2); end otherwise error('未知的激活函数类型: %s', type); end end

这里特别说明一下几个容易被忽略的细节:

第一,输入权重w的生成范围。我默认用[-1,1]均匀分布,实际使用时可以根据数据分布适当放大到[-2,2]甚至更大。权重范围影响的是隐含层节点的“活跃区间”,Sigmoid函数在Z很小或很大时会饱和,梯度趋近于零。如果数据本身波动范围大,权重范围太小会导致隐含层输出过于集中在0或1附近,信息量不足。

第二,偏置b的作用。很多人忽略偏置,其实偏置可以让激活函数在给定输入下体现更丰富的变化。没有偏置的sigmoid激活,等于是强制经过了原点,表达能力会明显受限。RELM的偏置不需要太复杂,和w一样随机生成就行。

第三,当L > N时,我选择了第二种求解形式。代码里可能对Matlab用户不够直观:HHᵀ这个N×N矩阵的构造非常快,因为N通常远小于L。如果你在实验中直接写beta = (H'*H + eye(L)/C) \ (H'*Y),而L=1000、N=100,Matlab会尝试对1000×1000的矩阵求逆,虽然不至于出错,但没必要地慢。这个优化我在实际项目中对比过,样本量小、节点数多的时候,两种形式的速度差距非常明显。

2.3 数据归一化的处理技巧

回归预测任务里的归一化处理,是个很多人掉过坑的地方。RELM本身对输入特征的尺度比较敏感,尤其是隐含层节点使用sigmoid这类饱和函数时,输入范围如果跨越几个量级,部分节点的输出会直接饱和。

我用的是一套很朴素的归一化方案:Min-Max归一化到[0,1]或[-1,1]区间。实现代码如下:

function [Xnorm, ps_in] = NormalizeTrain(X, yMin, yMax) % 训练集归一化,记录归一化参数 Xmin = min(X, [], 1); Xmax = max(X, [], 1); Xrange = Xmax - Xmin; Xrange(Xrange == 0) = 1; % 避免常数列除零 Xnorm = (X - repmat(Xmin, size(X,1), 1)) ./ repmat(Xrange, size(X,1), 1); ps_in.Xmin = Xmin; ps_in.Xrange = Xrange; ps_in.yMin = yMin; ps_in.yMax = yMax; end

预测函数里,新样本必须使用训练时保存的Xmin和Xrange参数来做归一化,绝对不能重新计算测试集自己的min/max再归一化。这是个非常经典的bug:训练集归一化到[0,1],测试集又按测试集自己的范围归一化到[0,1],两边分布尺度虽然相似,但数值含义完全不同,模型等于是在处理两个不同分布的数据,预测误差往往会很诡异地偏大。

我处理Y(目标值)的方式是:如果做的是单输出回归,一般在训练前把Y也归一化到[-1,1]区间,预测后再反归一化回原始尺度。如果输出范围在训练集之外(比如突然出现一个比训练样本大很多的新值),反归一化后预测值也会被迫落在训练集标签范围附近,这是所有回归模型都逃不掉的局限,RELM也不例外。

3. 回归实验与参数调优

3.1 用合成数据快速验证算法

对于一个新的算法实现,我习惯先用一个已知服从某种规律的数据集来做验证,而不是直接上真实数据。这样做的原因很简单:真实数据噪声大、可能还有缺失值,很难判断“预测差”到底是算法的问题还是数据预处理的问题。合成数据规律明确,一眼就能看出模型拟合得好不好。

我常用的验证数据集之一是从正弦函数中采样并添加高斯噪声:

% 生成环境: 2000个样本,单输入单输出 rng(42); x = linspace(0, 4*pi, 2000)'; y = sin(x) + 0.1 * randn(size(x)); % 前1500个样本作为训练集,后500个作为测试集 X_train = x(1:1500); Y_train = y(1:1500); X_test = x(1501:2000); Y_test = y(1501:2000);

然后调用RELM训练,隐含层节点数L设成50,正则化系数C设成100,激活函数选sigmoid:

model = RelmTrain(X_train, Y_train, 50, 100, 'sigmoid'); Ypred = RelmPredict(model, X_test); rmse_test = sqrt(mean((Y_test - Ypred).^2)); fprintf('测试集RMSE: %.4f\n', rmse_test); % 绘图对比 figure; plot(X_test, Y_test, 'b-', 'LineWidth', 1); hold on; plot(X_test, Ypred, 'r--', 'LineWidth', 1); legend('真实值', 'RELM预测值');

这段代码跑完之后,预测曲线会和真实曲线高度重合,RMSE大概在0.1左右(和添加的噪声标准差一致,说明模型已经把真实规律捕捉到了,只留下不可学习的高斯噪声)。如果测试集RMSE明显大于噪声标准差,那就要回头检查代码或者参数了。

3.2 正则化系数C的影响

C值是RELM里最重要、也最需要实际感受的一个超参数。C本质上控制的是对β的惩罚强度:C越大,惩罚越小,模型越倾向于精确拟合训练数据;C越小,惩罚越强,β向量被压缩得越厉害,模型越偏向于简单平滑。

我经常做一个C值扫描实验,把C从1e-3扫到1e10(对数均匀取点),分别记录训练集和测试集RMSE:

C_list = logspace(-3, 10, 14); train_rmse = zeros(size(C_list)); test_rmse = zeros(size(C_list)); for i = 1:length(C_list) model_i = RelmTrain(X_train, Y_train, 50, C_list(i), 'sigmoid'); train_pred = RelmPredict(model_i, X_train); test_pred = RelmPredict(model_i, X_test); train_rmse(i) = sqrt(mean((Y_train - train_pred).^2)); test_rmse(i) = sqrt(mean((Y_test - test_pred).^2)); end % 画双轴图观察两条曲线的变化趋势 figure; semilogx(C_list, train_rmse, 'b-o', 'LineWidth', 1.5); hold on; semilogx(C_list, test_rmse, 'r-s', 'LineWidth', 1.5); xlabel('正则化系数 C'); ylabel('RMSE'); legend('训练集RMSE', '测试集RMSE');

一个典型的观察结果是:C很小的时候,训练集和测试集RMSE都挺高,模型欠拟合;随着C增大,测试集RMSE会先下降到一个谷底,然后如果继续增大,测试集RMSE又会上升,而训练集RMSE一直下降。这就是过拟合区间。不同数据集的最佳C值差异很大,有的数据集在C=1附近最好,有的要C=1e6。所以别想着一个C走天下,做项目时把C扫描一遍几乎是必备步骤。

3.3 隐含层节点数L怎么选

隐含层节点数L决定了模型的容量。L太小,模型表达力不足,拟合不了复杂非线性关系;L太大,模型容量过剩,加上正则化不够强的时候容易过拟合。

但RELM的优势在于:因为有正则化项约束,L可以取得比较激进,甚至超过训练样本数也不会立刻崩掉。这还是得益于β被压缩,模型实际复杂度没有表观层数那么高。

我在做L选择实验时的经验是:以一个较小的L(比如10)起步,每次翻倍,直到训练时间开始不可接受为止。对每个L跑5次重复实验(因为随机权重的影响),取平均测试RMSE作为对比指标。通常情况下,测试RMSE会随着L增大先快速下降(这时是欠拟合),然后进入一个平台期(此时L已经足够),最后缓慢上升或震荡(过拟合区间)。选择一个平台期内的最小L值即可,这样既能保证精度,又不会让计算量白白增加。

L_list = [5, 10, 20, 50, 100, 200, 500]; avg_test_rmse = zeros(size(L_list)); repeat = 5; for i = 1:length(L_list) rmse_list = zeros(repeat, 1); for rep = 1:repeat rng(rep * 100); % 不同随机种子 model_rep = RelmTrain(X_train, Y_train, L_list(i), 100, 'sigmoid'); pred_rep = RelmPredict(model_rep, X_test); rmse_list(rep) = sqrt(mean((Y_test - pred_rep).^2)); end avg_test_rmse(i) = mean(rmse_list); end

从结果看,平台期往往会出现在L=50到200之间,过了200之后边际收益变得非常小。日常做项目时,如果数据量在几千条、特征量不大,L取100到300就够用了,不需要迷信更大的数。

3.4 激活函数的选择对比

激活函数对RELM的影响比很多人想象中要大得多。我通常会在同一组数据和同一组L、C下,对比sigmoid、sin、hardlim、rbf四种激活函数的表现。

从实际实验来看:

  • Sigmoid是最稳妥的默认选择,适合大多数平滑回归任务,但要注意输入范围,过大或过小都可能饱和。
  • Sin函数对周期性和振荡型数据效果很好。对于拟合sin(x)这样的数据,sin激活理论上就有天然优势,收敛速度和精度都会更好。
  • Hardlim是非连续激活,表达能力偏弱,适合二分类或符号型输出,回归任务一般不建议选它。
  • RBF的效果依赖于中心点和宽度的构造方式,我上面的RBF实现比较简单(用随机权重当中心),性能浮动较大,如果要用RBF,建议单独做更精细的中心点选择和宽度参数搜索。

如果数据有明确的周期性,优先试sin;如果完全不知道数据长什么样,先用sigmoid打个底不会有太大问题。

3.5 与原始ELM的对比

为了验证正则化的作用,我习惯把RELM和普通ELM放在一起对比。普通ELM直接用伪逆求解β:

% 普通ELM训练,对比用 beta_elm = pinv(H) * Y_train;

在这组正弦数据上,ELM在L=500时训练集RMSE能压到0.01以下,但测试集RMSE往往会飙到0.5以上;而RELM在同样的L=500下,只要C选得合适,训练集RMSE和测试集RMSE都会保持在0.1上下。这个对比非常直观:正则化导致训练误差略微增大,但泛化能力却大幅提升。在真实数据上这个差距通常只会更明显。

4. 实际调试中的坑与排查技巧

4.1 H矩阵接近奇异

最常见的报错提醒就是“矩阵接近奇异或缩放严重”,或者干脆Warning: Matrix is singular to working precision。出现这个问题的根源通常是H的列存在高度共线性,或者某些隐含层节点输出的值几乎恒定(比如都是同一个常数),导致矩阵秩亏。

排查方法也很直接:

  • 降低L,减少列数,降低共线性概率。
  • 增大C,让对角线上I/C这项贡献更大,数值上更稳定。
  • 检查输入数据是否包含常数列或高度相关的特征,做特征去重或降维。
  • 激活函数饱和也可能让某列输出全是接近0或1的常数,这时要调整权重范围或改用sin激活。

4.2 测试集预测结果整体偏移

如果测试集预测值和真实值趋势一致但整体偏大或偏小,十有八九是归一化的锅。我之前做过一个项目,训练集和测试集不是来自同一时间段,分布有点差异,我用训练集的Xmin/Xmax归一化测试集后预测还行,但后面有人不小心按测试集自己的min/max重新归一化了一遍,结果预测值全面偏移,几乎没法用。

这里要强调,代码里要让归一化参数跟着模型走。训练时记录ps_in,预测时直接用同一个ps_in,不要在任何地方重新计算测试集的统计量。

4.3 随机性导致实验结果不可复现

RELM的输入权重和偏置是随机生成的,这意味着每次跑同一个脚本,结果都可能不一样。如果你在写论文或做对比实验,这种随机性会很麻烦。

我在代码里用rng固定种子。但要小心,如果代码里在训练之前还有其他随机操作(比如随机打乱数据、随机划分训练测试集),rng的影响范围会扩大,需要在每个关键步骤前重新设置种子,或者用rng(N)固定全局种子,并在论文里写明种子编号。

更稳妥的做法是每个参数配置跑多次实验(比如5次或10次),最后汇报平均值和标准差。这样即使随机种子不同,结论也依然稳健。

4.4 训练很快但预测时内存溢出

当L特别大(比如1万以上)并且测试样本也很多时,ComputeH里构造的Hnew矩阵会占用不少内存。一个N×L的double矩阵,当N=1万、L=1万时就是8亿个字节,约760MB,内存一下子就爆了。

解决办法有两类:

  • 减少L,做特征降维,或者用核ELM的思想,避免显式构造大隐含层矩阵。
  • 分批预测:把测试样本切分成小块,分别计算Hnew并预测,最后拼接结果。代码改动很小,但在大数据量下效果立竿见影。
% 分批预测示例 batch_size = 1000; n_test = size(X_test, 1); Ypred = zeros(n_test, 1); for start = 1:batch_size:n_test idx = start:min(start + batch_size - 1, n_test); Ypred(idx) = RelmPredict(model, X_test(idx, :)); end

4.5 常见问题速查表

现象可能原因解决方案
训练集RMSE低,测试集RMSE高过拟合增大C的惩罚作用(减小C),或减少隐含层节点数L
训练集RMSE测试集都高欠拟合增加L,或检查特征归一化、激活函数饱和情况
预测结果整体偏移归一化参数不一致确认测试集使用训练集的Xmin/Xrange
矩阵奇异警告H共线性严重降低L、增大C,或检查输入特征相关性
相同代码重复跑结果不同未固定随机种子使用rng固定种子,或多次实验取均值方差
预测值范围比真实值窄输出归一化限制了范围检查训练标签分布,确认是否有极端值;或改用log变换
某个C下预测崩掉数值不稳定把C扫描改用logspace,避开过小的惩罚系数

4.6 参数调优的顺序建议

很多朋友一上来就直接用网格搜索同时调L和C,虽然能出结果,但计算浪费很大。我通常按这个顺序来:

  1. 固定L在100左右,先扫C,确定C的大致数量级。
  2. 固定C在某数量级,扫L,找到容量够用的最小值。
  3. 再围绕刚才选定的L和C附近做小范围精细搜索。
  4. 固定种子后,把最终配置跑多次重复实验,确认稳定性。

这样做的原因是L和C不是完全独立的。C很小时模型受约束强,即使L增大,过拟合也不会太严重;C很大时L的影响才会明显暴露。先定C再定L,大概率能更快收敛到比较合适的组合。

最后说点个人习惯

这套RELM的Matlab代码我反复用了很长时间,从最开始的学术实验到后来的工业数据预测项目,没有被替换掉。相比动不动就要调一两个小时深度网络,RELM给我提供的是一个五分钟内出baseline结果的能力。很多项目其实不需要一上来就搞复杂模型,先用RELM跑通流程,理解数据的基本规律,再决定是不是要上更强的方法,这个路线我一直很推荐。如果你也在做回归预测任务,建议亲手把上面的代码敲一遍,改改L和C,感受一下训练集和测试集误差随参数变化的规律,这种体感比看十篇文章都来得实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询