ARTICLE DETAIL

资讯详情

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

RELM正则化极限学习机Matlab实现:回归预测实战与参数调优指南

RELM正则化极限学习机Matlab实现:回归预测实战与参数调优指南 做数据回归预测这么些年我常用的工具箱里一直留着几个“小而美”的算法正则化极限学习机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×LLN最小二乘解不再唯一模型会疯狂拟合训练数据中的噪声。我在早期做实验时把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的代码结构非常清晰核心就三个部分RELM训练函数接收输入特征X、标签Y、隐含层节点数L、激活函数类型、正则化系数C输出模型结构体包含随机权重w、偏置b和输出权重β。RELM预测函数接收训练好的模型和新样本Xnew输出预测值Ypred。主脚本负责数据生成/加载、归一化、训练、预测、效果评估。我习惯把所有代码拆成独立的.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; endfunction 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)而L1000、N100Matlab会尝试对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激活函数选sigmoidmodel 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对数均匀取点分别记录训练集和测试集RMSEC_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值差异很大有的数据集在C1附近最好有的要C1e6。所以别想着一个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从结果看平台期往往会出现在L50到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在L500时训练集RMSE能压到0.01以下但测试集RMSE往往会飙到0.5以上而RELM在同样的L500下只要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矩阵当N1万、L1万时就是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, :)); end4.5 常见问题速查表现象可能原因解决方案训练集RMSE低测试集RMSE高过拟合增大C的惩罚作用减小C或减少隐含层节点数L训练集RMSE测试集都高欠拟合增加L或检查特征归一化、激活函数饱和情况预测结果整体偏移归一化参数不一致确认测试集使用训练集的Xmin/Xrange矩阵奇异警告H共线性严重降低L、增大C或检查输入特征相关性相同代码重复跑结果不同未固定随机种子使用rng固定种子或多次实验取均值方差预测值范围比真实值窄输出归一化限制了范围检查训练标签分布确认是否有极端值或改用log变换某个C下预测崩掉数值不稳定把C扫描改用logspace避开过小的惩罚系数4.6 参数调优的顺序建议很多朋友一上来就直接用网格搜索同时调L和C虽然能出结果但计算浪费很大。我通常按这个顺序来固定L在100左右先扫C确定C的大致数量级。固定C在某数量级扫L找到容量够用的最小值。再围绕刚才选定的L和C附近做小范围精细搜索。固定种子后把最终配置跑多次重复实验确认稳定性。这样做的原因是L和C不是完全独立的。C很小时模型受约束强即使L增大过拟合也不会太严重C很大时L的影响才会明显暴露。先定C再定L大概率能更快收敛到比较合适的组合。最后说点个人习惯这套RELM的Matlab代码我反复用了很长时间从最开始的学术实验到后来的工业数据预测项目没有被替换掉。相比动不动就要调一两个小时深度网络RELM给我提供的是一个五分钟内出baseline结果的能力。很多项目其实不需要一上来就搞复杂模型先用RELM跑通流程理解数据的基本规律再决定是不是要上更强的方法这个路线我一直很推荐。如果你也在做回归预测任务建议亲手把上面的代码敲一遍改改L和C感受一下训练集和测试集误差随参数变化的规律这种体感比看十篇文章都来得实在。
返回列表