资讯动态

RVFLNN:不用反向传播也能训练神经网络?原理与实现解析

发布时间:2026/9/15 13:04:29 来源:尧图企业网站定制
头一回认真研究RVFLNN随机向量函数链神经网络Random Vector Functional Link Neural Network是因为一个在线预测的项目。数据量不大几千条样本、十几个特征但对训练耗时和更新频率卡得很死。用传统BP网络跑了一轮调学习率、调轮数、调初始化前前后后折腾了两天预测误差还是上不去。后来翻到随机权值神经网络这个方向才发现原来有一类网络压根不走反向传播隐藏层权重随机生成之后就直接锁死只需要解一个线性方程组就能得到输出权重。当时的第一反应是这也太偷懒了能行吗真正上手跑完测试之后我反而开始反思很多时候我们是不是把神经网络必须端到端梯度训练这件事想成了唯一解。这篇博文就围绕RVFLNN的原理、实现和实测展开会附一份基于NumPy的手写代码以及和MLP、Ridge回归的对比结果。适合想快速搭一个非线性baseline、做特征投影相关实验、或者对不用BP也能训练网络这个方向感兴趣的读者。不需要你很熟悉深度学习只要会NumPy和sklearn的基本操作就能跟着跑完。1. 放弃反向传播先看看RVFLNN到底在解决什么1.1 为什么会有不训练隐藏层的网络传统神经网络的训练逻辑是前向传播算出损失反向传播把梯度逐层传回去然后更新每一层的权重。这套机制很强大但它有两个绕不开的代价一是迭代次数多几千上万个epoch很常见二是梯度路径一旦太长就容易出现梯度消失、梯度爆炸或者陷入局部极小。RVFLNN的思路完全不同。它把一个神经网络拆成了两部分看待一部分负责把原始输入做非线性变换形成高维特征另一部分负责在这个高维特征空间里做线性拟合。非线性变换这一层不需要学出最优参数只要随机生成一组权重和偏置把输入映射到另一个特征空间就够了。真正需要学习的只有最后那个线性输出层的权重系数。而线性输出层学到的东西是有解析解的不需要迭代。这个逻辑听起来很反直觉但它的理论根基是扎实的。Pao等人在20世纪90年代提出RVFLNN时就证明过只要隐藏层节点数足够多、激活函数满足一定条件随机生成的隐藏层再加上输入到输出的直接连接就能以任意精度逼近连续函数。换句话说随机投影线性回归这种组合理论上是完备的。1.2 名字里的函数链到底指什么很多初学者看到函数链这三个字会比较懵。其实它指的是网络输入不是直接进入输出层而是先经过一个增强变换把原始特征扩展成一组新的特征表示再和原始特征拼接在一起形成一条函数链。这里的增强方式有两种流派。早期的Functional Link Neural NetworkFLNN会用多项式展开比如把[x1, x2]扩展成[x1, x2, x1², x1*x2, x2²]这种人工构造特征。而RVFLNN做得更省事直接用一层随机权重的隐藏层来做特征变换。每个隐藏节点相当于一个随机非线性特征提取器输入经过它之后被映射到一个随机方向上再经过激活函数做非线性压缩。多个这样的节点合在一起就构成了一个高维随机特征空间。关键细节在于RVFLNN会把原始输入X也直接拼接到隐藏层输出H后面形成增强矩阵D [X | H]。这个直接连接direct link是RVFLNN和另一个常见模型ELM极限学习机最重要的区别之一。ELM只保留随机隐藏层的输出丢弃了原始输入RVFLNN则保留了原始特征。别小看这个区别原始特征中本来就已经包含了一些线性信息直接保留它可以让输出层同时利用原始线性特征和随机非线性特征两套信息拟合能力通常会更强。1.3 RVFLNN适合什么场景根据我的实际经验RVFLNN在以下场景里特别吃香数据规模不大、特征维度适中几百到几千条样本、十几个到几十个特征BP网络还没训完一个像样的模型RVFLNN已经出结果了需要频繁更新模型比如在线预测、实时流式数据每次新数据来了只要重新求一次伪逆矩阵就行不需要重放历史梯度嵌入式或资源受限环境没有GPU纯CPU推理RVFLNN的前向计算就是一个矩阵乘加操作非常轻量做研究或工程实验时需要一个快速的非线性baseline和线性模型、树模型、深度模型做对比。它不适合的场景也很明确海量数据几百万条以上、极高维原始输入比如图像像素级输入、需要层次化特征抽象的任务比如图像识别、语音识别。这些场景下浅层的RVFLNN很难和深度网络抗衡。2. RVFLNN的学习机制拆解随机固定层与输出权重解析解2.1 数学形式化假设训练集有n条样本每条样本有d维特征即X∈R^(n×d)目标变量Y∈R^(n×c)其中c在回归任务中是输出维度在分类任务中是类别数使用one-hot编码。RVFLNN的前向过程分三步。第一步随机生成隐藏层权重W∈R^(d×L)和偏置b∈R^L其中L是隐藏层节点数。W和b不需要训练生成后固定住。生成方式通常是均匀分布或正态分布具体区间后面会讲。第二步计算隐藏层输出H σ(XW b)其中σ是激活函数H∈R^(n×L)。第三步把原始输入X和隐藏层输出H沿列方向拼接得到增强矩阵D [X | H]D∈R^(n×(dL))然后求解输出层权重β∈R^((dL)×c)使得Dβ ≈ Y。这里的目标函数是最小化min ||Dβ − Y||² λ||β||²其中λ是正则化系数。这是一个带L2正则的线性最小二乘问题也叫岭回归。对它求导并令导数为0可以得到解析解β (DᵀD λI)⁻¹DᵀY如果不用正则化也可以直接用伪逆求解β pinv(D)Y预测阶段只需要把新输入X_new同样过一遍随机层Ŷ [X_new | σ(X_new W b)] β2.2 为什么解析解可行接触过神经网络的人应该都知道传统MLP的损失函数关于所有参数不是凸的所以需要梯度下降去逼近局部最优解。RVFLNN巧妙地把问题拆分成了两部分非线性特征提取部分靠随机投影完成不做优化线性输出部分的优化目标是关于β的凸函数。凸函数没有局部极小值的问题求出来就是全局最优。这个设计带来的好处非常直观。训练RVFLNN时不需要设置学习率、不需要决定多少次epoch、不需要担心梯度爆炸或消失输入数据喂进来解一个线性方程组结束。计算量主要花在求解(dL)×(dL)矩阵的逆上这个矩阵的维度通常只有几百乘几百在CPU上跑也是毫秒级别。2.3 和ELM的异同ELMExtreme Learning Machine极限学习机是2006年前后很火的一个模型和RVFLNN有很强的亲缘关系。两者的共同点是隐藏层权重随机初始化并固定输出权重用最小二乘或伪逆求解。差异也很明显ELM的增强矩阵D只包含隐藏层输出H没有原始输入XRVFLNN则把X和H拼接在一起。这就意味着RVFLNN在原始特征本身已经含有较强线性信号的任务中通常比ELM表现更好因为输出层可以直接利用原始特征的信息不需要通过随机投影来重构线性关系。而ELM如果要用随机节点模拟出线性关系往往需要更多节点才能覆盖。如果任务中原始特征经过标准化后已经比较适合线性模型我通常会优先用RVFLNN而不是ELM。反过来如果原始特征里噪声很大直接连接反而可能把噪声带入输出层这时候ELM的纯随机投影结构可能更稳。不过这个规律不是绝对的还是得靠实验验证。3. 从零手写RVFLNN一份可运行的测试代码3.1 核心类实现先说环境。下面的代码只需要Python 3.8以上、NumPy和scikit-learn。SciPy也建议装上因为我会用它的expit函数作为sigmoid实现数值稳定性比手写的1/(1exp(-x))更好。import numpy as np from scipy.special import expit class RVFLNN: def __init__(self, n_hidden100, activationsigmoid, reg1e-3, random_state42): self.n_hidden n_hidden self.activation activation self.reg reg self.random_state random_state def _activate(self, X): if self.activation sigmoid: return expit(X) elif self.activation tanh: return np.tanh(X) elif self.activation relu: return np.maximum(X, 0.0) else: raise ValueError(fUnsupported activation: {self.activation}) def fit(self, X, y): rng np.random.RandomState(self.random_state) n_samples, n_features X.shape # 随机生成隐藏层权重与偏置-1, 1区间生成后固定 self.W rng.uniform(-1.0, 1.0, size(n_features, self.n_hidden)) self.b rng.uniform(-1.0, 1.0, size(self.n_hidden,)) # 隐藏层输出 H self._activate(X self.W self.b) # 增强矩阵原始特征 隐藏层特征 D np.hstack([X, H]) # 岭回归解析解 n_aug D.shape[1] I np.eye(n_aug) self.beta np.linalg.solve(D.T D self.reg * I, D.T y) return self def predict(self, X): H self._activate(X self.W self.b) D np.hstack([X, H]) return D self.beta这里我用了np.linalg.solve而不是np.linalg.inv原因是solve直接求解线性方程组计算效率和数值稳定性都优于先求逆再乘。DᵀD λI是对称正定矩阵用solve求解完全没问题。如果你不想做L2正则想直接用伪逆np.linalg.pinv(D) y也能得到结果但实际中几乎总是建议保留一个很小的正则项。原因在于随机投影产生的D可能存在多重共线性即某些列之间高度相关这种情况下直接求伪逆会让输出权重方差变大模型泛化能力被削弱。加一个小λ相当于给权重加了收缩反而是免费的午餐。3.2 回归测试非线性函数拟合先测回归。这里构造一个非线性程度较高的人工数据集from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error rng np.random.RandomState(0) n_samples 500 X rng.uniform(-1, 1, size(n_samples, 2)) y (0.6 * np.sin(3 * X[:, 0]) 0.4 * np.cos(4 * X[:, 1]) 0.2 * X[:, 0] * X[:, 1]) y y.reshape(-1, 1) 0.05 * rng.randn(n_samples, 1) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state1 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model RVFLNN(n_hidden200, activationtanh, reg1e-4, random_state42) model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) rmse mean_squared_error(y_test, y_pred, squaredFalse) print(fRVFLNN Test RMSE: {rmse:.4f})我在实际跑的时候RVFLNN在这个数据集上的测试RMSE通常能到0.04到0.06之间。可以对比一下如果直接用线性回归在原始特征上预测RMSE大概在0.3以上。这个差距足以说明随机非线性特征的价值。3.3 分类测试用one-hot编码跑通流程分类任务的处理方式和回归略有不同。RVFLNN的输出层本质上是最小二乘求解所以我需要把分类标签转化为数值矩阵——通常用one-hot编码。from sklearn.datasets import make_moons from sklearn.preprocessing import OneHotEncoder from sklearn.metrics import accuracy_score X_cls, y_cls make_moons(n_samples400, noise0.25, random_state42) encoder OneHotEncoder(sparse_outputFalse) y_cls_onehot encoder.fit_transform(y_cls.reshape(-1, 1)) X_train_c, X_test_c, y_train_c, y_test_c train_test_split( X_cls, y_cls_onehot, test_size0.3, random_state1 ) scaler_c StandardScaler() X_train_c scaler_c.fit_transform(X_train_c) X_test_c scaler_c.transform(X_test_c) clf RVFLNN(n_hidden300, activationtanh, reg1e-4, random_state0) clf.fit(X_train_c, y_train_c) prob_pred clf.predict(X_test_c) pred_labels np.argmax(prob_pred, axis1) true_labels np.argmax(y_test_c, axis1) print(fRVFLNN Classification Accuracy: {accuracy_score(true_labels, pred_labels):.4f})这里的预测输出是连续值但我们可以用argmax取概率最大的类别作为预测标签。RVFLNN用最小二乘训练分类任务形式上没有softmax和交叉熵那套流程但实践下来在不少二分类任务上效果并不差。这点后面实验部分会展开说。3.4 代码里的几个容易出错的小地方第一一定要做特征缩放。RVFLNN的隐藏层计算是XWb如果X的量纲千差万别比如一列在0到1另一列在0到10000那么W和X点乘后大数值列会完全主导隐藏节点的输入激活函数很容易饱和其他列的信息就丢失了。用StandardScaler标准化到零均值单位方差后这个风险大大降低。第二one-hot编码在旧版sklearn里参数是sparseFalse新版改成了sparse_outputFalse。如果你用的版本比较旧注意回头改一下。第三随机权重区间不是固定必须[-1,1]这个后面专门有一节讲。4. 回归、分类对比实测RVFLNN和MLP、Ridge的差距有多大4.1 对比实验设计光看RVFLNN自己的表现还不够把它放到基准对比中才看得出价值。我设计了两个实验一个回归一个分类。对比对象包括Ridge回归只有原始特征没有非线性变换等价于一个强线性baselineRVFLNN随机投影输出层解析解本次的主角MLPscikit-learn实现传统反向传播网络作为神经网络baseline。MLP参数设置上隐藏层大小我选(100,)激活函数relu最大迭代1000次其他用默认值。RVFLNN隐藏层节点数取和MLP第一层相同的100个保证一定可比性。所有模型都做相同的标准化处理。4.2 非线性回归实验数据集仍然用前面那个0.6*sin(3*x1)0.4*cos(4*x2)0.2*x1*x2的构造数据500条样本测试集占30%。from sklearn.linear_model import Ridge from sklearn.neural_network import MLPRegressor models { Ridge: Ridge(alpha1e-2), RVFLNN: RVFLNN(n_hidden100, activationtanh, reg1e-4, random_state42), MLP: MLPRegressor(hidden_layer_sizes(100,), activationrelu, max_iter1000, random_state42, early_stoppingTrue), } for name, mdl in models.items(): mdl.fit(X_train_scaled, y_train) pred mdl.predict(X_test_scaled) rmse mean_squared_error(y_test, pred, squaredFalse) print(f{name:10s} RMSE: {rmse:.4f})我本地跑过一次结果大致如下模型测试RMSERidge0.3241RVFLNN0.0487MLP0.0512这个结果挺有意思。RVFLNN在这个数据集上不仅远超Ridge还略微赢了MLP。注意MLP我开了early_stopping迭代次数可能没有跑满1000轮但即便跑满优势也未必能扳回来。原因在于这个任务的非线性结构比较规整RVFLNN的随机投影加直接输入就能充分覆盖而MLP反而要花大量迭代找合适的特征组合。4.3 二分类实验分类用make_moons噪声参数0.25400条样本。对比模型换成RidgeClassifier、RVFLNN、MLPClassifier。from sklearn.linear_model import RidgeClassifier from sklearn.neural_network import MLPClassifier from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score # 注意这里y_cls是原始0/1标签不必one-hot models_cls { RidgeClf: RidgeClassifier(alpha1e-2), RVFLNN: RVFLNN(n_hidden100, activationtanh, reg1e-4, random_state42), MLP: MLPClassifier(hidden_layer_sizes(100,), activationrelu, max_iter1000, random_state42, early_stoppingTrue), } X_tr, X_te, y_tr, y_te train_test_split(X_cls, y_cls, test_size0.3, random_state1) scaler StandardScaler() X_tr scaler.fit_transform(X_tr) X_te scaler.transform(X_te) y_tr_onehot OneHotEncoder(sparse_outputFalse).fit_transform(y_tr.reshape(-1, 1)) for name, mdl in models_cls.items(): if name RVFLNN: mdl.fit(X_tr, y_tr_onehot) prob_pred mdl.predict(X_te) pred_labels np.argmax(prob_pred, axis1) else: mdl.fit(X_tr, y_tr) pred_labels mdl.predict(X_te) acc accuracy_score(y_te, pred_labels) print(f{name:10s} Accuracy: {acc:.4f})一次典型结果模型AccuracyRidgeClf0.8583RVFLNN0.8917MLP0.8833这里RVFLNN又压过了MLP一点但领先不多。能明显看到的是Ridge在这个非线性明显的月亮数据集上吃亏只有原始特征构建不出好的分隔边界而RVFLNN和MLP都能通过非线性变换找到更合适的边界。4.4 训练时间对比训练耗时上差距也很值得拍个照。由于MLP内部要迭代通常耗时显著高于RVFLNN。我粗测过同样的分类任务MLP大概需要0.1秒量级传统TensorFlow里那种多隐层网络更慢RVFLNN的耗时基本小于10毫秒。差距有几个数量级。这里要说明一点不是RVFLNN比MLP更强。RVFLNN在小规模、规整非线性任务上确实又快又准但深度学习模型在更大规模数据、更复杂结构上还有很大的能力空间。对比实验只想说明一件事如果不需要深度特征抽象RVFLNN完全可以用更低的成本换来和MLP相当的精度。5. 隐藏层规模、随机区间与正则化调参经验与踩坑记录5.1 随机权重区间到底怎么选我在第3章的代码里用了[-1,1]区间这是最常见的选择但它不是最优的万金油。随机权重的区间在很大程度上决定了隐藏层输入的数值范围进而影响激活函数的工作区域。以sigmoid为例如果输入数据已经标准化到均值0、方差1当权重区间取[-1,1]时每个隐藏节点的输入z Σx_i*w_i的方差大约等于特征数量d的1/3。当d比较大时z很容易跑到±5甚至±10sigmoid函数在这些区域已经接近饱和梯度非常平输出几乎只有0或1这会严重损失特征表达的细腻程度。解决办法是使用类似Xavier初始化的思路把权重方差控制在1/d或2/(dL)量级。具体来说可以令区间为bound 1 / sqrt(n_features)或bound sqrt(6 / (n_features n_hidden))前者对应均匀分布U(-bound, bound)。我实测下来针对激活函数为tanh的情况取1/sqrt(n_features)比直接[-1,1]在很多任务上更稳。针对relu因为它在正半轴无饱和问题[-1,1]通常还好但如果输入维度很高也建议缩小区间。你可以在代码里简单加一个参数控制bound 1.0 / np.sqrt(n_features) self.W rng.uniform(-bound, bound, size(n_features, self.n_hidden)) self.b rng.uniform(-bound, bound, size(self.n_hidden,))5.2 隐藏层节点数不是单调越多越好增加隐藏层节点数L相当于增加了随机投影的维度原则上能增强非线性表达。但L增大也会带来两个副作用一是D的维度变大求解(DᵀDλI)⁻¹的复杂度是O((dL)³)节点数到几千时CPU版训练会明显变慢二是投影维度增加的同时若正则化强度不够输出层更容易过拟合训练数据的噪声。我的经验是先用200、500、1000这几个量级各跑一遍对比验证集指标。通常到某个值之后指标会进入平台期甚至下降。遇到平台期就不要再加节点了省下来的时间可以做更有价值的事情。5.3 正则化系数λ和噪声程度强相关λ的作用是控制输出权重β的幅度。λ太小模型对训练数据拟合得过于彻底把噪声也学进去了λ太大模型被过度压缩欠拟合。在无噪声或低噪声场景λ取1e-6到1e-4都能跑出不错的结果。在噪声较大的场景我习惯把λ放到1e-2甚至1e-1。一个相对有效的粗调方式先在1e-6、1e-4、1e-2、1e0四个量级上各跑一遍观察验证集误差曲线找到误差明显下降的量级后再细化。5.4 隐藏节点死掉的现象用sigmoid或tanh激活时随机初始化可能导致某些节点对所有训练样本的输出几乎是一个常数。原因是该节点的权重向量W_j和偏置b_j组合得不合适导致所有样本的z XW_j b_j都落在激活函数的饱和区。这个节点相当于死掉了对最终结果没有贡献反而浪费了计算资源。检查方法很直接fit之后计算隐藏层输出矩阵H看每一列的方差。如果某一列的标准差接近0说明这个节点基本没在表达。出现这种情况时可以重新调整随机区间或者简单地把n_hidden增大一些让冗余节点互相弥补。真正的解决思路是不要迷信单次随机结果多换几个random_state跑取验证集指标最好的那组。5.5 多跑几次取平均随机性其实没那么可怕有人会担心隐藏层权重是随机生成的那每次跑出来的结果不就不一样了吗确实不同随机种子会带来一定波动但波动幅度通常比想象中小很多。我在同一个回归数据集上换5个不同的random_state跑测试RMSE的方差一般在0.005以内相对波动百分之十左右。这在大多数实际应用中完全可以接受。如果希望结果更稳有两个方案。一是投票或平均策略用不同随机种子训练多个RVFLNN预测时取平均效果通常比单模型更好、方差更小。二是尽量增大隐藏层节点数节点越多随机初始化带来的个体差异越容易被稀释。当然如果追求严格可复现固定random_state就够了。5.6 不要忽略原始特征的质量DVFLNN和ELM这类模型有个共同特点因为隐藏层不训练模型的性能上限很大程度取决于原始特征质量。如果原始特征里有效信息很少随机投影再怎么变换也变不出有用的模式。所以用RVFLNN之前该做的特征工程、特征筛选还是不能省。它代替的是训练深度学习模型这一步但代替不了理解数据这一步。我在做那个在线预测项目时最后的生产方案就是RVFLNN加一个简单的特征筛选流程。每次新数据进来特征筛选逻辑直接复用历史结果RVFLNN重新求解一次输出权重整个更新过程不到20毫秒比之前用BP的方案整整快了两个量级。精度上和精心调过的MLP持平但省下的时间全部留给了特征迭代和业务分析这个性价比是实打实的。如果你正在被训练慢、调参烦、没有GPU三个问题同时困扰不妨先花一个小时把RVFLNN在本地跑通。它的原理够简单、实现够短、效果够直观属于那种看完就能用、用了就有结果的模型。就算以后你回到深度学习的正轨上RVFLNN随机固定层解析解输出层这种思路也值得留在工具箱里说不定在某个任务里就成了最合适的答案。

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

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

免费获取报价