回归预测:小样本高精度实用指南)
经常有人问我有没有那种不用调参、训练又快、精度还不错的回归预测模型 我一般会先反问一句你的数据量有多大如果回答是小样本、几十到几百个样本那我十有八九会推荐正则化极限学习机RELM。这个模型在MATLAB里实现起来非常干净核心代码也就几十行却能啃下巴特福德等经典回归数据集的精度排名前几的成绩而且训练过程几乎是在瞬间完成的。这篇博客我想把RELM从原理到代码、从参数选择到坑点排查完整地掰开揉碎讲一遍尤其是代码里每一步为什么要这么写、那些看起来不起眼的细节怎么影响最终精度这些才是真正值钱的经验。1. 为什么回归预测要选RELM从ELM到RELM的进化逻辑1.1 ELM到底解决了一个什么问题传统的前馈神经网络比如最经典的BP神经网络训练过程是靠反向传播算法反复迭代修改权重。这个过程有三个让人头疼的毛病第一是慢梯度下降是串行迭代的数据多、网络大的时候训练一个模型动不动就好几分钟第二是容易陷进局部最优不同的初始权重可能收敛到完全不同的结果第三是对超参数极其敏感学习率、动量因子、隐含层节点数、激活函数每个参数都可能让结果产生巨大的波动。极限学习机Extreme Learning MachineELM是南洋理工大学的黄广斌教授在2006年前后提出来的核心想法非常反直觉既然反向传播这么麻烦那干脆不要反向传播了。隐含层的输入权重和偏置直接随机生成训练过程中完全固定不动唯一需要计算的就是输出层的权重。而这个输出权重根本不需要迭代求解用最小二乘法一次就能算出解析解。这样一来训练过程从迭代优化变成了矩阵运算速度提升几个数量级而且还能保证全局最优——因为最小二乘解的解析表达式就是全局最优解不存在什么局部极值的问题。我第一次在MATLAB里跑通ELM时说实话有点不太相信每个分类的结果好得让我震惊但训练时间显示0.1秒都不到。当时实验室跑同样的数据用BP神经网络训练时间以分钟计算。差距真的是数量级的不是一点半点。1.2 经典的ELM存在着一个躲不开的隐患经典的ELM虽然快但有一个很严重的隐患因为输入权重和偏置是随机生成的当隐含层节点数量比较多的时候隐含层输出矩阵会存在多重共线性也就是说矩阵里有很多行或列之间存在近似线性相关的关系。这时候直接做最小二乘求解就会遇到矩阵求逆不稳定、解灾难性地膨胀等问题模型的泛化能力直线下降。这个现象在训练集上表现得特别明显训练集的拟合精度高得吓人R²甚至能到0.999但测试集的误差反而很大。啊这就是我特别熟悉的一课也是新手最常用的典型翻车场景——过度拟合。ELM训练得越快越容易忽略这一点它的随机性强更容易在训练集上死记硬背。1.3 RELM的正则化思路RELM的改进思路说起来很简单但特别实用。它借鉴了岭回归的思想在ELM的优化目标里加了一个正则化项让原先只追求训练误差最小化的目标变成了同时兼顾训练误差和输出权重的范数也就是同时让模型的复杂度尽量低。具体表达式[ \min_{\boldsymbol{\beta}} \frac{1}{2}|\boldsymbol{\beta}|^2 \frac{C}{2} \sum_{i1}^{N} |\boldsymbol{\varepsilon}_i|^2 ]其中(|\boldsymbol{\beta}|^2)就是输出权重的L2范数(\boldsymbol{\varepsilon}_i)是第i个样本的训练误差C是正则化系数。前面这项约束了模型的复杂度后面这项约束了训练误差C就是两者之间的调节旋钮。利用KKT条件等推导最后输出权重的解析解可以写成[ \boldsymbol{\beta} \left(\frac{\mathbf{I}}{C} \mathbf{H}^T \mathbf{H}\right)^{-1} \mathbf{H}^T \mathbf{T} ]看到没有这就是岭回归的另一个形式只不过把核矩阵从原始输入换成了隐含层输出矩阵H。正则化系数C起到了非常关键的作用C越大正则化约束越弱模型越倾向于精确拟合训练数据但可能过拟合C越小正则化的约束越强输出权重会被压得更小模型的泛化能力更强但可能欠拟合。这个C怎么选在后面代码部分我会详细讲实操方法。1.4 四种典型回归模型的适用场景对比为了让你更清楚RELM在什么位置我把几类常见的回归模型放一起对比一下模型训练速度是否需要迭代小样本表现超参数数量主要痛点BP神经网络慢是一般多调参困难、容易被局部最优困住SVM/SVR中等是好中大规模数据训练慢、核函数选什么很难高斯过程回归中等偏慢否好少求逆矩阵的计算量随样本数平方上升ELM极快否容易过拟合少随机性强、解不稳定RELM极快否很稳少C和隐含层节点数需要交叉验证选择这个对比已经说明问题了RELMs几乎保留了极限学习机的全部优点同时用正则化把不稳定过拟合这两个最大的坑给填上了。如果你处理的数据正好是小样本回归预测非常适合优先试一下RELM。2. MATLAB代码详解从零搭建RELM回归模型2.1 数据准备与预处理归一化不能不做在动手写核心代码之前数据预处理这块必须重视起来归一化的位置尤其重要。我在实际测试中发现一个高频错误有人把整个数据集归一化完之后才划分训练集和测试集这一步其实有信息泄漏的风险因为归一化的统计信息最大最小值是从全体数据算出来的测试集的信息提前暴露给了模型。正确做法是先划分数据再用训练集的统计指标去做归一化。但在ELM/RELM的应用场景里大部分代码为了方便都是先归一化再划分实测下来在小样本场景下误差不算太明显。不过我个人建议既然原理上已经清楚了还是按标准流程来标准流程用mapminmax函数就需要单独传训练集的最大最小值给测试集代码稍微绕一点但严谨很多。MATLAB里归一化最常用的函数是mapminmax默认把数据映射到[-1, 1]区间。需要特别注意的是mapminmax对矩阵是按行操作的而我们的原始数据通常是每行一个样本所以用的时候要么转置要么设置好维度。我习惯写成% 假设data是N×M的矩阵每行一个样本最后一列是目标值 X data(:, 1:end-1); % 转置为 M×N每列一个样本 T data(:, end); % 1×N [Xn, Xps] mapminmax(X, -1, 1); [Tn, Tps] mapminmax(T, -1, 1);这样Xn就是归一化后的输入矩阵每一列是一个样本方便后面矩阵运算。Tps里保存着目标值的归一化参数预测完之后反归一化要用到它。如果是分类任务这一步不太重要因为分类的结果只需要看索引就可以了但回归任务的目标值必须反归一化不然你的预测结果全在区间[-1, 1]里面看起来毫无意义——这一步是回归任务里最容易被新手忽略的。2.2 核心代码逐行拆解RELM的核心代码大概就是这么几段我拆开逐行讲。第一步设置随机种子。这个细节在初学阶段容易被忽略但在实际工程项目里相当重要。因为ELM/RELM的输入权重和偏置都是随机生成的同样的代码在不同机器、不同运行时间下跑出来的精度可能不一样有时差得还挺多。设置随机种子的目的是为了让实验可复现论文里能够让别人验证。rng(42); % 设置随机种子保证每次运行结果一致第二步初始化模型的超参数。这里的核心其实只有两个隐含层节点数L和正则化系数C。L 30; % 隐含层节点数 C 10; % 正则化系数 n size(Xn, 1); % 输入特征维度隐含层节点数L的选择我在实践里的经验是从10开始按10、20、30、50、100这样去试用交叉验证选最好的。节点数太少模型拟合能力不够训练集和测试集都欠拟合节点数太多就是典型的过拟合状态训练集R²接近1测试集却掉得厉害。C的范围建议在[0.001, 10000]之间按数量级去扫最好用对数网格。第三步随机生成输入权重W和隐含层偏置B。W rand(n, L) * 2 - 1; % 输入权重n×L每个元素在[-1, 1] B rand(1, L) * 2 - 1; % 隐含层偏置1×L这里采用-1到1之间的均匀分布是常见做法。其实W的取值范围对不同数据集的表现差异很大有的数据适合更窄的range有的更宽。后面我会专门讲一个对W做缩放的经验技巧。这里先用标准做法。第四步计算隐含层输出矩阵H。H是N×L的矩阵N是样本数L是节点数每个元素是某个样本在某个隐含层节点上的激活值。H tansig(Xn * W B);这里用了一个tansig函数就是双曲正切S型传递函数 (\tanh(x))。ELM里最常用的三个激活函数是sigmoid、tansig和radbas实测下来tansig在很多回归数据集上表现比较均衡。sigmoid容易饱和radbas对参数更敏感。如果数据特征是极端的分布可以换一下试试但除非有明显问题用tansig作为默认值就好。第五步也是RELM的核心一步求解输出权重β。前面推导过公式是[ \boldsymbol{\beta} \left(\frac{\mathbf{I}}{C} \mathbf{H}^T \mathbf{H}\right)^{-1} \mathbf{H}^T \mathbf{T} ]在MATLAB里这么写I eye(L); % L×L的单位矩阵 beta (I/C H * H) \ (H * Tn);这里我特意用了左除运算符\而不是inv函数因为左除在MATLAB里是经过优化和数值稳定性处理的求解线性方程时不会像直接求逆那样容易产生大的数值误差。从左除运算本身来看这一步也是把矩阵求逆的过程整合进了求解过程里。凡是碰到求逆的运算我都推荐优先用左除或pinv而不是inv。需要留意的是这里(I/C HH)是L×L的矩阵只要L设得不是特别大这个求逆运算的开销几乎可以忽略。所以隐含层节点数在几百甚至上千时RELM照样能秒算。第六步用训练好的模型对训练集和测试集分别做预测然后反归一化。Y_train_pred H * beta; % 训练集预测值归一化 Y_test_pred H_test * beta; % 测试集预测值归一化 Y_train_pred mapminmax(reverse, Y_train_pred, Tps); % 反归一化为原始量纲 Y_test_pred mapminmax(reverse, Y_test_pred, Tps);这里要特别小心矩阵维度的匹配。mapminmax(reverse, ...)要求输入是行向量排列的矩阵所以必须用Y_train_pred把N×1的列向量转成1×N的行向量。等你反归一化完后需要再转置回来变成N×1列向量方便后续计算误差指标。第七步计算性能指标。回归任务里最常用的三个指标是决定系数R²、均方根误差RMSE和平均绝对误差MAE。R2 1 - sum((T_test - Y_test_pred).^2) / sum((T_test - mean(T_test)).^2); RMSE sqrt(mean((T_test - Y_test_pred).^2)); MAE mean(abs(T_test - Y_test_pred));R²越接近1越好RMSE和MAE越小越好。这三个指标加起来基本能把模型的表现描述得很全面了R²看相对拟合程度RMSE看大误差的惩罚MAE看平均偏差水平。2.3 W和B的随机性到底对结果影响多大这个是我在实际测试里特别留意过的一个问题。相同的数据、相同的L和C只是重新运行一次让W和B重新随机生成测试集R²能差出0.05甚至更多。这一点让很多第一次用RELM的人非常困惑明明代码一模一样怎么两次跑出来的结果不一样原因就在于W和B是随机生成的。虽然RELM在数学上保证了给定W和B之后β的求解是基于全局最优的解析解但不同的随机W和B会把你带到不同的视角去看数据最终精度自然会有差异。应对这个问题的标准做法是多次运行取平均。具体做法是外层循环运行50次或100次RELM每次都重新随机生成W和B记录每次的测试集R²、RMSE最后取平均值作为模型的稳定性能同时记录标准差看它的稳定性。num_runs 50; R2_list zeros(num_runs, 1); for i 1:num_runs rng(i); % 每次种子不一样保证结果互相独立 % ... 完整训练流程 ... R2_list(i) R2; end fprintf(平均R² %.4f ± %.4f\n, mean(R2_list), std(R2_list));这组数据通常会给你一个很直观的结论RELM在多次运行下的稳定性其实比BP神经网络要强不少。因为BP每次运行的初始条件不一样而训练过程又对初始条件敏感方差更大。RELM虽然W和B随机但输出β有解析解所以每次运行的波动相对可控多次平均之后精度非常可靠。3. 完整实现与实测结果分析3.1 全套可直接复制的MATLAB代码我把完整代码放在这里你可以直接Copy到MATLAB里运行。示例数据我这里用的是一个经典的波士顿房价数据集的简化版本当然你自己替换成自己的数据也是一样的流程关键是理解每一段的逻辑。%% RELM回归预测完整代码实现 clear; clc; close all; rng(42); %% 1. 导入数据 % data的每一行是一个样本最后一列是目标值 % 这里用自带示例或你自己读入的数据 data load(your_data.txt); X data(:, 1:end-1); T data(:, end); %% 2. 划分训练集与测试集按8:2比例保持原有顺序 ratio 0.8; num_train round(size(X, 1) * ratio); X_train X(1:num_train, :); T_train T(1:num_train); X_test X(num_train1:end, :); T_test T(num_train1:end); %% 3. 归一化 [X_train_n, Xps] mapminmax(X_train, -1, 1); [T_train_n, Tps] mapminmax(T_train, -1, 1); X_test_n mapminmax(apply, X_test, Xps); X_train_n X_train_n; T_train_n T_train_n; X_test_n X_test_n; %% 4. 初始化参数 L 30; C 10; n_feature size(X_train_n, 2); %% 5. 训练RELM W rand(n_feature, L) * 2 - 1; B rand(1, L) * 2 - 1; H_train tansig(X_train_n * W B); % N×L beta (eye(L) / C H_train * H_train) \ (H_train * T_train_n); %% 6. 测试 H_test tansig(X_test_n * W B); Y_train_pred_n H_train * beta; Y_test_pred_n H_test * beta; %% 7. 反归一化 Y_train_pred mapminmax(reverse, Y_train_pred_n, Tps); Y_test_pred mapminmax(reverse, Y_test_pred_n, Tps); %% 8. 指标计算与绘图 R2_train 1 - sum((T_train - Y_train_pred).^2) / sum((T_train - mean(T_train)).^2); RMSE_train sqrt(mean((T_train - Y_train_pred).^2)); MAE_train mean(abs(T_train - Y_train_pred)); R2_test 1 - sum((T_test - Y_test_pred).^2) / sum((T_test - mean(T_test)).^2); RMSE_test sqrt(mean((T_test - Y_test_pred).^2)); MAE_test mean(abs(T_test - Y_test_pred)); fprintf(训练集R²%.4f, RMSE%.4f, MAE%.4f\n, R2_train, RMSE_train, MAE_train); fprintf(测试集R²%.4f, RMSE%.4f, MAE%.4f\n, R2_test, RMSE_test, MAE_test); figure(Position, [100, 100, 1200, 450]); subplot(1, 2, 1); plot(T_train, b-, LineWidth, 1.5); hold on; plot(Y_train_pred, r--, LineWidth, 1.5); legend(真实值, 预测值, Location, northwest); title(训练集预测结果对比); xlabel(样本序号); ylabel(目标值); grid on; subplot(1, 2, 2); plot(T_test, b-, LineWidth, 1.5); hold on; plot(Y_test_pred, r--, LineWidth, 1.5); legend(真实值, 预测值, Location, northwest); title(测试集预测结果对比); xlabel(样本序号); ylabel(目标值); grid on;这版代码为了方便阅读没有加入循环多次取平均的逻辑。实际做实验的时候建议把训练和测试过程包在一个函数里然后外层循环调用如果你想在项目里直接用最好加一个for循环把30或50次的结果都记录下来取平均。3.2 一组真实的参数扫描与结果记录我用一个100个样本、8个输入特征的数据集跑了一组参数扫描记录下结果你看看这个模式隐含层节点L正则化系数C训练集R²测试集R²测试集RMSE结论1010.9230.8910.187欠拟合节点数不够3010.9720.9340.112表现不错5010.9880.9180.137开始有轻微过拟合10010.9970.8520.198明显过拟合300.0010.9510.9210.149正则化过强略有欠拟合300.10.9660.9310.121比较均衡30100.9790.9350.108当前最优区域301000.9840.9290.116正则化稍弱过拟合风险上升50100.9920.9250.124节点多但C也大整体一般8010000.9990.8630.205经典翻车组合从这组数据里能看到几个很典型的规律第一C固定时L从10涨到100训练集R²一路上涨但测试集R²先涨后跌存在一个最优区间不是越大越好。第二L固定时C太小和太大都会让测试集精度下降C太大会导致过拟合C太小会让模型过于平滑而信息丢失。第三最优组合往往在L中等、C中等的区域比如L30、C10附近。3.3 还有一个容易被忽视的点测试集划分方式的影响我踩过一个坑是数据顺序对结果的影响。如果原始数据按某种规律排列比如前80%是低值区间、后20%是高值区间这样按顺序切分的训练集和测试集分布会有很大的差异模型在训练集上学的低值区间规律对测试集的高值区间完全不适用测试R²甚至会变成负的。解决办法是随机打乱数据再划分训练集和测试集。在MATLAB里直接randperm做索引重排idx randperm(size(X, 1)); X X(idx, :); T T(idx);然后重新按比例划分。这样保证训练集和测试集尽量同分布。对于时间序列数据不能随便打乱顺序要用前N个预测后M个的滚动窗口方式划分。但RELM本身不是专门为时间序列设计的如果是时间序列回归预测更建议用LSTM或者带时序结构的模型RELM如果强行做时序预测需要把历史时刻的数据构造为滚动输入特征。这块如果你需要我可以再单独展开讲。4. 使用RELM过程中的典型坑与排查方法4.1 常见问题速查表我在使用RELM的过程中遇到过不少问题有些问题很隐蔽这里整理成一个排查表方便你遇到问题时直接对着查现象可能原因排查与解决思路所有预测值几乎相同(输出恒定)H矩阵出现了秩亏或者激活函数饱和检查L是否过大、W的随机范围是否太大、输入数据是否存在大量重复样本训练集R²≈1但测试集R²很低过拟合增大C、减小L或者用交叉验证重新扫描参数结果每次运行波动极大W和B随机、未设种子、模型本身方差大设置固定种子或多次运行取平均值训练和测试结果都极差C过大导致欠拟合或数据没归一化检查C的量级确认做了归一化处理调用inv时报矩阵接近奇异H矩阵多重共线性严重换用左除\或pinv增大C减小L隐含层数加到很多R²反而下跌节点冗余造成过拟合用交叉验证找L的最优区间数据集小但测试集R²为负数据划分不随机、测试集分布偏移打乱数据顺序重新划分或增加训练集比例4.2 排查实操一个真实案例有一次我用RELM拟合一个工业过程的数据24个输入特征样本只有80个训练时R²到了0.998我差点以为找到了一把万能钥匙结果测试集R²是-0.12比瞎猜还差。排查流程我分享一下先看归一化有没有问题确认用的是训练集统计量没泄漏测试集然后看参数L150明显偏大C0.001又太强这两个组合刚好是最容易过拟合的组合最后我把L降到40、C升到5测试集R²回到了0.86。这个案例其实特别典型问题几乎全出在参数组合上。所以强烈建议RELM在使用时一定要做参数扫描而不是凭感觉定。L和C两个参数一起扫每次记录测试集R²形成一个热力图一眼就能找到最佳区域。这在MATLAB里也就几十行代码的事但对模型性能的提升往往是决定性的。4.3 关于H矩阵的数值状态检查还有一个很有用的调试技巧在训练前先检查H矩阵的状态。H_train tansig(X_train_n * W B); fprintf(H矩阵维度: %d × %d\n, size(H_train, 1), size(H_train, 2)); fprintf(H矩阵条件数: %.4e\n, cond(H_train * H_train)); fprintf(H矩阵内部NaN数量: %d\n, sum(sum(isnan(H_train)))); fprintf(H矩阵内部Inf数量: %d\n, sum(sum(isinf(H_train))));条件数cond如果超过1e12就说明矩阵病态得非常严重求逆结果会有很大误差。这时候要么增大C让(I/C HH)这一项对角占优来改善条件数要么减小L降低共线性程度要么检查一下输入W的取值是否过大导致激活函数饱和也就是tansig的输出全部跑到±1上。另外强烈建议在计算H之后立刻检查NaN和Inf。在数据有NaN的情况下ELM/RELM不会报错而是把NaN值一路传播轻则beta里有NaN重则整个预测结果全部是NaN。但不会报错这件事本身就很坑人不检查根本发现不了。5. RELM的横向对比与扩展方向5.1 RELM与BP、RVM、GPR的实战对比我把RELM和几个常见模型放在同一批数据集上做了对比这个对比结果可以给你选型时提供一个参考坐标。选用的是100个样本、8个特征的小样本回归基准每个模型都用最优超参数设置多次运行取平均值模型训练时间测试集R²测试集RMSE备注BP神经网络(3层)~8秒0.840.188调参过程心累学习率≈0.01时最优径向基RBF网络~2秒0.860.173径向基宽度参数敏感支持向量回归SVR~1秒0.890.148核函数和C惩罚参数比较敏感高斯过程回归GPR~0.3秒0.920.126小样本神器但对数值异常很敏感ELM0.02秒0.870.165快但方差大容易过拟合RELM0.03秒0.940.105速度快精度高方差可控RELM几乎以最小的计算代价拿到了最高的测试精度这一点在小样本场景里非常有价值。高斯过程回归在小样本上也很强但受限于协方差矩阵求逆的复杂度是O(N³)数据一旦上了几千个样本就开始变得非常吃力。而RELM的复杂度主要在HH的L×L矩阵运算上L通常几百就足够所以即使样本上万也能轻松跑。5.2 从回归到分类RELM的通用性扩展RELM不止能用于回归做分类任务也只需要改动最后两步把回归的输出层换成softmax或者直接用多个输出节点对应多个类别然后取最大输出值的节点索引作为预测类别。归一化目标值这里从连续值变成0/1标签矩阵其余代码几乎不动。如果你手头有一个分类数据集完全可以直接试点这个方式效果通常也不差。5.3 如果再叠一层核核极限学习机(KELM)的扩展可能RELM再往前走一步就是核极限学习机KELM它的思路是把隐含层的随机映射换成核函数映射不再需要显式指定隐含层节点数用什么核函数来决定映射方式。它的精度往往比原版的RELM更高但计算量也上来了因为需要计算N×N的核矩阵在小样本场景下很有竞争力。如果你对RELM已经用得很熟下一步可以考虑往KELM方向深入。6. 几个经验总结与实用建议写到这里把这段时间反复实验积累的几条经验汇总给你建议收藏备用。第一RELM最适合的场景是小样本、中低维度的回归预测和分类任务。样本量在几十到几千之间特征维度在几个到几十个之间这种配置下它比大多数模型都要快精度又不吃亏。如果是超高维数据建议先做特征筛选或者降维再上RELM。第二参数扫描是必须做的。L和C两个参数写一个双重循环在L∈{10, 20, 30, 50, 80}、C∈{0.001, 0.01, 0.1, 1, 10, 100, 1000}这个范围内扫一遍每次记录测试R²一共35次运行在MATLAB里跑完也就几秒钟的事。几秒钟换来的模型精度提升绝对值回票价。第三每次实验都设置随机种子并且多次运行取平均。RELM的随机性决定了单次运行结果没有代表性如果你在论文里报告一个单次运行的结果很容易被审稿人质疑。按我前面的做法跑50次取平均报告R²均值±标准差这才显得严谨可靠。第四小心H矩阵的数值病态问题。正则化系数C要选在一个合理的区间不建议太小比如不要小于1e-6否则(I/C HH)这一项可能数值上不稳定。更小的C从数学上讲等价于更强的L2正则化但在浮点数精度限制下会引起数值问题推荐用左除运算来求解不要用显式的inv函数。第五W的随机范围值得当作一个额外参数来调。默认-1到1可能不是最优的有时把它缩放到-0.5到0.5能获得更稳定的结果。但缩放方式对整个H矩阵的影响实际上体现在激活函数的输入范围上激活函数是tansig的话在接近0的区域它的响应最灵敏所以W范围越小神经元越不容易饱和。这个参数很少有人提到但实测它对精度的影响有时比L和C还要明显。我还是想多提醒一句任何模型都不是万能的。RELM在小样本、非线性映射、特征维度适中的场景下性价比极高但它不是时间序列建模的首选也不是超高维特征建模的首选更不是大规模深度特征学习的好手。选模型之前先问问自己的数据长什么样再决定用哪把钥匙开哪把锁。我个人在拿到一个新数据集的时候第一件做的事永远是先用RELM跑个基线因为它的训练速度允许我把参数网格扫得够宽能在几分钟内摸清数据的天花板在哪里再决定要不要上更复杂的模型。这个习惯帮我避开了很多上来就怼深度学习、最后发现数据量根本不够的弯路也算是一个省时间的入门技巧吧。