ARTICLE DETAIL

资讯详情

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

宽度学习BLS的五种Python实现:从标准到增量的工程实践

宽度学习BLS的五种Python实现:从标准到增量的工程实践 简介这份Python代码包以简洁的脚本形式呈现了宽度学习Broad Learning System的五种典型实现面向进行序列数据建模与特征交互研究的开发者尤其适合视频分析、自然语言处理和时间序列预测等场景。代码不仅包含基础BLS模型还覆盖在线学习、增量学习、迁移学习与多任务学习四种扩展策略可应对数据流更新、小样本迁移及多任务联合训练等不同需求。压缩包共5个py文件整体仅13KB各文件按模块划分清晰分别对应模型主体、特征映射、增强映射以及不同增量分支的实现便于阅读、调试与快速嵌入既有项目。该代码库已吸引1730人学习下载适合具备基础Python能力的科研人员或工程师直接调用。通过运行这些脚本能够直观理解双线性变换如何捕获特征交互并深入掌握各增量模型的参数更新与结构调整逻辑从而加速自身算法验证。1. 宽度学习BLS的五种Python代码形态不调反向传播的“够用就好”五种形式的宽度学习BLS代码用python语言落地时第一印象往往是这不就是个带随机映射的线性回归吗但跑过一轮增量学习和伪逆更新之后你会意识到它和深度学习的思考方式完全相反——不调反向传播不求梯度把特征节点和增强节点拼成一个宽矩阵一次伪逆解出输出权重。适合做表格数据、量化因子、工业预测这类样本量不大、又要求快速迭代的场景。如果你正被深度模型的训练时长和调参玄学困住BLS提供的是一个低门槛、易复现、能增量更新的备选方案。五种形式分别对应五种不同诉求下面从最小可用的标准版开始。2. 先拿标准BLS开刀特征节点、增强节点与一次伪逆求解2.1 BLS的核心数学A [Z | H]W A⁺Y 到底在算什么宽度学习的命名来自“宽度”而非“深度”。它不堆层数而是把输入映射成两组并行的特征一组是特征节点Z一组是增强节点H然后横向拼接成A。整个模型的输出权重W pinv(A) Y其中pinv是Moore-Penrose伪逆。和神经网络比它没有梯度下降、没有学习率、没有epoch数学上就是一次最小二乘。常见做法里特征节点由随机权重We映射生成Z φ(X We βe)φ可以是线性或tansig增强节点再从Z映射一次H ξ(Z Wh βh)ξ一般选tansig。因为两个映射矩阵都是随机初始化后固定不动整个模型对X来说是一个带随机特征的线性系统所以求解W退化为一个岭回归问题W (AᵀA cI)⁻¹AᵀY。这里的c是L2正则系数是BLS里最值得调的参数之一后面会专门说它。和深度网络相比BLS的黑匣子成分确实少一些。A的每一列对应一个随机特征W的每一行告诉你这个特征对输出的贡献。你甚至可以打印W的分布来检查哪些节点是死的、哪些是活的。这种可解释性在工业场景里很实用客户问“为什么预测高了”你能指着某个特征节点说它的权重最大而它的输入主要来自哪些原始字段。2.2 最小可用标准BLS40行Python类与你该改的第一组参数直接给一个能跑通手写数字识别的标准BLS类。依赖只有numpy连scikit-learn都只用它的数据集部分。import numpy as np from numpy.linalg import pinv def tansig(x): return 2.0 / (1.0 np.exp(-2.0 * x)) - 1.0 class BLSBase: def __init__(self, n110, N110, n2200, c1e-3, seed7): self.n1 n1 # 每组特征节点个数 self.N1 N1 # 特征节点组数总特征节点 n1 * N1 self.n2 n2 # 增强节点个数 self.c c # L2 正则系数 self.seed seed def _init_weights(self, d, rng): # 特征节点映射每组一套随机权重 self.We [rng.normal(size(d, self.n1)) for _ in range(self.N1)] self.be [rng.normal(size(self.n1,)) for _ in range(self.N1)] # 增强节点映射从特征节点再映射一次 d_z self.n1 * self.N1 self.Wh rng.normal(size(d_z, self.n2)) self.bh rng.normal(size(self.n2,)) def _feature(self, X): # 拼特征节点 Z np.hstack([tansig(X w b) for w, b in zip(self.We, self.be)]) # 拼增强节点 H tansig(Z self.Wh self.bh) return np.hstack([Z, H]) def fit(self, X, Y): rng np.random.RandomState(self.seed) # 归一化到 [0,1]避免原始字段量纲差异放大随机映射方差 self.xmin, self.xmax X.min(0), X.max(0) Xs (X - self.xmin) / (self.xmax - self.xmin 1e-8) self._init_weights(Xs.shape[1], rng) A self._feature(Xs) # 岭回归解比直接 pinv(A) 更抗病态矩阵 d A.shape[1] self.W np.linalg.solve(A.T A self.c * np.eye(d), A.T Y) def predict(self, X): Xs (X - self.xmin) / (self.xmax - self.xmin 1e-8) return self._feature(Xs) self.W逻辑说明fit先生成两组随机映射权重并保存之后不管训练还是预测都走同一套_feature流程。之所以用np.linalg.solve(A.T A cI, A.T Y)而不是直接pinv(A) Y是因为A的列之间可能存在相关性AᵀA加上cI之后数值稳定性好很多不会出现W里突然冒出几个NaN。预测阶段只是做一次矩阵乘法没有迭代这也是BLS速度快的根源。参数说明n1和N1共同决定特征节点总量常见做法是n1取10到20N1取10左右特征节点总数控制在100到200n2是增强节点数它对精度影响最直接一般从100开始试再按内存余量往上加。c默认1e-3如果你的数据集特征之间高度相关把c调到1e-2或1e-1W会明显更平滑。这个类里所有随机权重由seed控制所以同样的数据和参数每次跑出来的结果完全一致。2.3 为什么BLS不吃显存却吃内存矩阵规模估算方法不少第一次接触BLS的人误以为它和深度学习一样吃GPU实际上BLS的瓶颈在内存和SVD。假设你有N个样本特征节点d_z个增强节点n2个A的尺寸是N × (d_z n2)。以10000个样本、100个特征节点、1000个增强节点为例A是10000 × 1100的矩阵float64占88MB能接受但如果把n2加到10000A变成10000 × 10100约800MB中间伪逆计算还要额外分配几倍内存32G内存的机器也会告警。所以正确做法不是一次把n2拉满而是先用小n2跑通再用增量学习的思路把增强节点一批批加上去。这也是为什么很多BLS代码里都有“内存估算”这类调试函数——不是装样子是真实翻车后补的。估算方法很简单A的内存字节数 N × (d_z n2) × 8算完再乘以3作为伪逆的临时开销就是你该预留的内存。特征工程阶段用float32能省一半内存精度损失通常可以接受。3. 五种形式的BLS代码逐段拆解增量、卷积、模糊、鲁棒与深度特征3.1 增量学习BLS代码只往增广矩阵右侧拼新节点伪逆用迭代更新增量学习是BLS最吸引人的特性。训练完一批数据之后发现精度不够不需要重训整个模型只追加增强节点用伪逆的增量公式更新输出权重。def incremental_pinv(A_pinv, A, B, Y): A: 旧特征矩阵 (n, k)B: 新增增强节点 (n, p) D A_pinv B # 旧伪逆对新节点的投影shape (k, p) C B - A D # 新节点中与旧列空间正交的残差 C_pinv pinv(C) # 残差伪逆p 很小所以很快 A1_pinv np.vstack([A_pinv - D C_pinv, C_pinv]) return A1_pinv, A1_pinv Y # 新伪逆和新权重逻辑说明这个公式来自分块矩阵伪逆的推导。B新增的p个增强节点里有一部分信息已经能被旧A的列空间表达剩下的残差C才是真正新增的信息。C的伪逆规模只有p × Np一般取20到200所以计算量远小于全量重算。如果你要追加的是新样本而不是新节点公式要换成另一种形式但常见需求是先加节点所以这个版本最实用。参数说明p是单次新增的增强节点数建议20到200。p越大单次提升越明显但每次伪逆计算也越重p太小则迭代次数多浮点误差累积。另一个隐藏参数是旧矩阵A必须常驻内存否则增量公式算不了。每次增量后可以用np.linalg.norm(A1_pinv A1 - I)检查伪逆质量超过1e-3就说明误差累积严重需要全量重算一次。3.2 卷积特征BLS代码CNN提特征BLS做分类头卷积BLS的思路不是把BLS卷积化而是用CNN当特征提取器把卷积特征展平后作为BLS的输入。两个部分各干各的CNN参数用反向传播更新BLS的输出权重仍然伪逆一次求解。import torch import torch.nn as nn class ConvFeature(nn.Module): def __init__(self, in_ch1): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), ) def forward(self, x): return self.conv(x) # 输出展平特征例如 (N, 32*7*7) # 训练流程示例先用常规分类损失训几个 epoch再固定特征层 for p in fe.parameters(): p.requires_grad False Z_cnn fe(x_tensor).detach().cpu().numpy() H tansig(Z_cnn np.random.normal(size(Z_cnn.shape[1], 200))) A np.hstack([Z_cnn, H]) W pinv(A) Y_onehot逻辑说明卷积层负责把原始图像转成更高层的抽象特征BLS负责在这些特征上做快速分类。和标准BLS相比这里的Z_cnn替代了随机特征节点特征质量由CNN的卷积核决定。如果完全不训练CNN直接提取随机卷积特征也能跑但精度通常比标准BLS还差——卷积核随机时展平特征的信息密度不一定比纯随机映射高。参数说明卷积核数量16和32是起步配置图像更复杂可以翻倍池化层用2×2是常见做法能降维但也会丢位置信息。关键参数是“训练几个epoch再固定”一般1到3个epoch足够让卷积核有些区分度。detach()必须写否则后续转numpy会破坏计算图。注意用预训练权重需要联网下载离线环境可以直接用ConvFeature()的随机初始化虽然精度差一截但能跑通流程。3.3 模糊BLS代码隶属度矩阵当特征节点小样本更稳模糊BLS把特征节点的随机映射换成模糊隶属度计算。每个样本不再是简单地投影到一个随机方向而是计算它属于各个聚类中心的隶属度。from sklearn.cluster import KMeans def fuzzy_feature(X, n_clusters20, sigmaNone): km KMeans(n_clustersn_clusters, n_init5, random_state0).fit(X) centers km.cluster_centers_ dist2 ((X[:, None, :] - centers[None, :, :]) ** 2).sum(-1) if sigma is None: # 用每个样本到最近中心的距离中位数当带宽避免全零或全一 sigma np.median(dist2.min(axis1)) U np.exp(-dist2 / (2 * sigma ** 2)) return U / (U.sum(axis1, keepdimsTrue) 1e-12)逻辑说明输出的U矩阵每一行是该样本对20个聚类中心的归一化隶属度替代标准BLS里的特征节点Z。因为隶属度是基于数据分布算出来的而不是随机映射所以小样本场景下它比随机特征稳定——随机映射在小样本时方差很大同一个样本换seed特征就完全不同模糊隶属度不会这样。参数说明n_clusters对应特征节点的维度20到50比较常见太少了表达能力不足太多了聚类本身不稳定。sigma是高斯带宽这个参数很敏感sigma太大所有隶属度都接近特征失去区分度sigma太小隶属度全是0。代码里用最近距离中位数做启发式默认值省去手动调的麻烦但如果你发现精度不理想优先扫sigma乘0.5到2.0之间的几个值。3.4 鲁棒BLS代码迭代加权最小二乘压住标签噪声标准BLS对离群点和标签噪声敏感。鲁棒BLS的做法是在伪逆求解外面套一层迭代加权让残差大的样本权重降下来。def fit_robust(A, Y, iters10, base_c1e-3): # 先用标准岭回归初始化 d A.shape[1] W np.linalg.solve(A.T A base_c * np.eye(d), A.T Y) for _ in range(iters): r Y - A W # 按样本平均残差给权重残差大的样本权重小 w 1.0 / (np.abs(r).mean(axis1) 1e-6) w w / w.mean() # 归一化保持尺度稳定 W np.linalg.solve((A * w[:, None]).T A base_c * np.eye(d), (A * w[:, None]).T Y) return W逻辑说明第一轮先算出初始W然后计算每个样本的残差残差大的样本在下一轮被降权残差小的样本权重变大。这样标签被改错的个别样本不会把W拉偏太多。迭代几次后W收敛到一个对噪声相对鲁棒的解。这里用(A * w[:, None]).T A实现加权最小二乘比直接拼对角权重矩阵内存省得多。参数说明iters取5到10通常就够再多可能把正常样本也压下去。base_c沿用标准BLS的L2系数。w的归一化很关键不归一化会导致每一轮W的尺度整体偏移精度反而下降。用这个函数时注意A必须已经是去掉均值或者归一化后的特征矩阵否则残差计算会被特征量纲带偏。3.5 深度特征BLS代码迁移预训练模型接一个宽输出层深度特征BLS是迁移学习的一个轻量变体用预训练模型提取特征然后接BLS做输出层。相比直接微调整个深度模型它训练快、不易过拟合、部署时只需要保存特征提取器和W矩阵。from torchvision import models backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) backbone.fc nn.Identity() # 替换掉最后的全连接分类头 feat backbone(x_tensor).detach().cpu().numpy() # 输出 (N, 512) d_z feat.shape[1] Z feat # 深度特征直接作为特征节点 H tansig(Z np.random.normal(size(d_z, 300))) A np.hstack([Z, H]) W pinv(A) Y_onehot逻辑说明预训练模型在ImageNet上学到的底层特征对其他视觉任务有很强的迁移能力。BLS在这里的作用是一个宽输出层——512维深度特征拼接300个增强节点一次伪逆解出分类权重。整个过程只需要把数据过一次backbone拿到特征训练BLS的时间几乎可以忽略。如果你做的是非图像任务可以把这个思路里的resnet18换成你自己训练过的因子模型本质一样。参数说明backbone的输出维度决定特征节点维度resnet18是512维换成resnet50就是2048维。n2从300起步因为深度特征本身已经很强增强节点太多反而容易过拟合。第一次运行会下载预训练权重网络不通的环境可以改用weightsNone的随机初始化版本但精度会明显下降。保存模型时只需要保存backbone的state_dict和BLS的W、随机映射权重部署包能压到几MB。4. BLS避坑指南伪逆数值震荡、内存爆炸与参数初值玄学4.1 W出现NaN训练过程看起来一切正常现象同样的数据跑标准BLS时W里有NaNpredict结果全是垃圾值但训练过程不报错。检查数据没有缺失值归一化也做了问题悬而未决。原因AᵀA矩阵接近奇异。当增强节点数量大于样本量或者两个随机映射产生了高度相关的列时AᵀA的行列式接近零直接pinv(A)或者np.linalg.solve都会数值失稳。解决把L2正则系数c从默认1e-3调大到1e-2甚至1e-1给AᵀA对角线加足够大的扰动。或者在np.linalg.solve外面包一层异常捕获检测到LinAlgError就把c翻倍重试。另一个习惯是别一上来就把n2拉满先小规模跑通确认数值稳定后再增量加节点。4.2 内存直接爆炸增强节点设了5000就MemoryError现象32G内存的机器N2万样本n1*N1100n25000A矩阵预估只有800MB但运行到伪逆一步内存直接爆掉。原因伪逆不是只算一个A矩阵。pinv内部要做SVD分解会生成若干临时矩阵内存峰值通常是A的3到5倍。此外Python的numpy临时变量如果不及时释放GC也不会立刻回收内存峰值进一步恶化。解决先把n2降到1000跑通再用incremental_pinv分批加节点。每加一批就手动del临时变量并gc.collect()。用float32替代float64也能救急A的内存直接减半SVD的临时矩阵也相应变小。这个坑是所有想把BLS规模做大的人都要过的坎——无脑加大n2的路径走不通增量更新才是正解。4.3 精度死活上不去增强节点加了又加还是没变化现象digits这类简单数据集标准BLS跑出来精度只有九成出头换成模糊BLS也没提升n2从200加到2000毫无变化开始怀疑BLS本身不行。原因特征节点和增强节点的激活函数搭配有问题。特征节点用tansig、增强节点也用tansig两层都是饱和函数随机映射的方差会被压得很小整个A矩阵的信息量不足。另一个常见原因是增强节点与特征节点高度相关新加的节点并没有提供新信息。解决特征节点保持线性映射去掉tansig或者用RBF增强节点用tansig这是最常见的搭配。检查节点相关性打印np.corrcoef(A.T)的均值如果超过0.8说明随机映射退化需要调整随机权重初始化的尺度——把Wh的初始化方差从1.0降到0.5或者提到2.0都值得一试。这个参数就是玄学不同数据集的最佳尺度差异很大。4.4 增量学习后误差不降反升重算一遍又好了现象用增量伪逆公式追加200个增强节点训练精度反而比增量前低了几个百分点。把整个模型全量重算一遍精度又恢复正常。原因增量伪逆公式在浮点运算下会有误差累积。每次增量只算残差C的伪逆C本身已经是“旧A列空间之外”的部分数值范围可能非常小伪逆结果会放大浮点误差。多次增量之后A1_pinv不再满足伪逆的性质W自然走偏。解决每次增量后接纳一个校验步骤np.linalg.norm(A1_pinv A1 - np.eye(k))这个值超过1e-3就触发一次全量重算。一般增量10到20次后强制全量重算一次误差就能控制在可接受范围。保存旧A矩阵是必须的别为了省内存把A删了否则遇到这种情况就真的没有后悔药。4.5 训练快但预测慢线上部署时被卡住现象BLS训练只要几秒钟模型小、速度快结果上线后发现单次预测的耗时比训练一个epoch还长完全没法接受。原因预测时要重新走一遍特征生成流程先算Z再算H最后拼接乘W。如果数据维度高、增强节点多这一串矩阵乘法并不比训练时的单次前向传播少多少。很多BLS实现把W_e、W_h、A的尺寸固定但预测代码没有做向量化优化。解决把多个样本拼成一个batch预测而不是单条单条调接口矩阵乘法在batch维度上的效率高得多。如果延迟要求到毫秒级把W_e和W_h合并成一个大矩阵W_all一次矩阵乘法算完所有随机映射再用numpy的BLAS后端自动并行。这一步做好了预测耗时能降到原来的三分之一以下。5. 用digits数据集验证五种BLSpython数据分析与可视化的对比方法5.1 统一评测脚本精度、训练时间、内存五种形式都写完了怎么证明它们各自的价值拿scikit-learn自带的digits数据集做统一对比。这个数据集1797个样本、64维、10分类规模小到所有形式都能在几秒内跑完非常适合做基准测试。import time import numpy as np from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X, Y load_digits(return_X_yTrue) Y_onehot np.eye(10)[Y] X_train, X_test, Y_train, Y_test train_test_split( X, Y_onehot, test_size0.3, random_state0) def evaluate(model, Xtr, Ytr, Xte, Yte): t0 time.time() model.fit(Xtr, Ytr) fit_time time.time() - t0 pred model.predict(Xte) return accuracy_score(Yte.argmax(1), pred.argmax(1)), fit_time acc, ft evaluate(BLSBase(n110, N110, n2300, c1e-3), X_train, Y_train, X_test, Y_test) print(fstandard bls acc{acc:.4f} fit_time{ft:.3f}s)逻辑说明这段脚本把评价拆成精度和训练时间两个维度。Y先转one-hot因为BLS的输出层是线性回归输出维度对应类别数argmax取最终类别。每次实验固定随机种子和数据划分确保结果可复现。如果你要对比五种形式把evaluate里的模型替换成对应的类或函数即可。参数说明test_size取0.3是常见比例random_state固定是为了不同模型之间可比。需要额外留意的坑是模糊BLS和鲁棒BLS的接口和标准BLS不一样一个要传聚类数一个要传迭代次数对比脚本里得单独适配不能指望所有类都有完全相同的fit签名。5.2 对比结论的读法什么时候该选哪种形式形式适用场景主要代价取舍倾向标准BLS快速基线、工业表格数据大n2时内存吃紧绝大多数项目的起步选择增量BLS在线更新、模型迭代需要保留旧A矩阵训练时间随节点数增长最平缓卷积BLS图像分类、局部特征敏感需要GPU和少量训练精度上限高复杂度也最高模糊BLS小样本、低噪声鲁棒性要求sigma带宽要调训练集小于500时最稳鲁棒BLS标签噪声、离群点迭代求解训练略慢数据清洗成本高的场景首选深度特征BLS迁移学习、特征复用依赖预训练权重下载精度和部署体积的平衡最好这个结论表不是让你照抄而是提示一个判断方法先跑标准BLS拿到基线如果基线精度已经够用其他形式都不用看。只有当基线不满足要求时再根据短板选——训练时间太长走增量精度不够走卷积或深度特征标签不可信走鲁棒样本太少走模糊。每种形式都对应一个明确的“为什么”而不是哪个听起来高级就上哪个。python数据分析与可视化在这一步的作用是把对比结果画成图表横轴是增强节点数n2从100到1000纵轴是精度五种形式各画一条曲线。你会看到标准BLS和增量BLS的曲线几乎重合因为增量更新的W理论上等价于全量重算卷积和深度特征的曲线起点更高但增长更平缓鲁棒BLS在干净数据上精度略低但你把测试标签随机改掉百分之五再跑它就反超了。这种图比任何文字表述都直观。6. 进阶把BLS封装成支持增量更新与Excel导出的Python工具类增量学习用得最好的一类场景是python量化交易策略代码里的因子更新每天收盘后进来一批新数据你不想把历史所有数据重新训练一遍只想让模型在旧基础上吸收今天的增量。把BLS封装成类之后调用方只需要一行model.update(X_new, y_new)其他逻辑全部收敛在内部。class BLSIncremental(BLSBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.A_old None self.A_pinv_old None def update(self, X, Y, p50): Xs (X - self.xmin) / (self.xmax - self.xmin 1e-8) Z self._feature(Xs)[:, :self.n1 * self.N1] # 只取旧特征节点 rng np.random.RandomState(self.seed len(self.A_old.T)) H_add tansig(Z rng.normal(size(Z.shape[1], p)) rng.normal(size(p,))) A_new np.hstack([self.A_old, H_add]) self.A_pinv_old, self.W incremental_pinv( self.A_pinv_old, self.A_old, H_add, Y) self.A_old A_new这段代码的要点是把之前的incremental_pinv函数收进类的update方法让外部只感知一个“更新”动作。实际操作中还有一个更朴素但有效的小习惯每更新20次手动做一次全量重算把浮点误差清零。这相当于给模型一个定期校准的节拍器。另一个实用技巧是把W矩阵导出到Excel用python写入excel的方式做权重分析。权重绝对值大的那一行对应的特征节点就是当前模型最重要的特征来源。很多工业场景里比起一个精度的数字客户更愿意看到“哪些变量在起作用”——一张权重热力图比训练曲线更有说服力。我自己的血泪经验是早期贪图省事把n2直接设成5000一跑就内存翻车后来老老实实增量加节点速度和精度都稳了。BLS的乐趣就在于它把“模型更新”从重训变成了一种轻量操作但代价是你得尊重它的数值边界。希望帮到你。本文还有配套的精品资源点击获取
返回列表