☰
稀疏贝叶斯学习代码实战:原理、多输出与避坑
2026/9/25 1:19:45 网站建设 项目流程

简介:面向机器学习与信号处理研究者的稀疏贝叶斯学习代码包,重点应对高维数据下特征选择与结构化稀疏建模问题,可用于模式识别、压缩感知、阵列信号处理等领域。压缩包内共十四份文件,十一份为以m为扩展名的源码脚本,两份为说明文档,一份为文本指南,整体大小约四百六十二千字节,便于快速下载与运行。代码覆盖块稀疏、多测量向量、时变稀疏等典型贝叶斯变体,并提供多组演示实验,用于验证相同向量恢复、时变系数重构以及不同信噪比下的性能表现,可帮助读者直观比较算法行为,观察稀疏解随迭代逐步稳定的过程。两份说明文档分别以一分钟和三分钟上手为特色,梳理了基础版与多测量向量版的核心调用流程与参数含义,并附有结果解读建议,适合算法研究者、研究生和工程开发者对照复现、调整与扩展。已有一千五百零九人浏览学习,是深入理解稀疏贝叶斯原理、开展实验验证的实用参考资料,也可作为课程设计与论文实验的起点。

1. 拿到稀疏贝叶斯学习的代码,先别急着跑:它到底让你看什么

如果你刚下载或复现过一份稀疏贝叶斯学习(SBL)的代码包,打开之后大概率会愣住:没有一行是那种“import 完就出结果”的黑盒调用,满屏都是循环、矩阵求逆和一个叫alpha的向量在反复迭代。这不是作者代码写得绕,而是 SBL 这一类方法本来就长这样——它把贝叶斯后验推导摊在桌面上,让你看着模型自己把不重要的基函数“挤”成零。你手里如果是几十个样本、几百个候选特征,特征之间又高度相关,普通回归要么过拟合、要么不收敛,稀疏贝叶斯学习能自动找出少数真正有用的支撑集,还顺带给出每个预测的不确定性。最适合的读者是:想复现论文算法、做压缩感知或代理模型,或者正从 SVM/Lasso 往贝叶斯方向转的工程师。把它完整跑通一遍,你对“稀疏”二字的理解会比只调包深刻得多。

2. 稀疏贝叶斯学习代码的第一个核心:稀疏性不靠 L1 惩罚

很多人第一次读到 SBL 的代码,会下意识把它等同于带稀疏惩罚的最小二乘。实际完全不是一回事。SBL 的出发点是这样:假设观测模型是y = Φw + ε,其中ε是高斯噪声,权重w的每一项都服从一个零均值高斯先验,但每一项的方差不一样,由超参数α_i控制。α_i越大,第i个权重被压向零的力量越强。真正巧妙的点是:α_i不是人工调出来的,而是由数据通过最大化边缘似然自动估计。

2.1 基函数与权重:SBL 处理的数据长什么样

SBL 代码里的Φ被称为设计矩阵,每一列是一个候选基函数。它可以是原始特征,也可以是把原始特征映射到高维空间的核——最常见的是取训练样本作为字典,算 RBF 高斯核矩阵。你的样本只有 N 个,但基函数列数可以远大于 N,这正是稀疏模型发挥价值的场景。

在高维小样本情况下,普通最小二乘的解有无数个,SVM 和 Lasso 各有各的绕法,而 SBL 绕的方式很直接:它不直接求解权重,而是求解“权重后验分布”。这个后验分布本身是高斯分布,闭式解写出来就是这个样子:

Σ = (β·ΦᵀΦ + A)⁻¹ μ = β·Σ·Φᵀy γ_i = 1 - α_i·Σ_ii α_i_new = γ_i / μ_i² β_new = (N - Σγ_i) / ‖y - Φ·μ‖²

这份公式就是 SBL 全部核心,代码里的循环只是把这份公式反复迭代到收敛。Σ是权重的后验协方差,μ是权重的后验均值,A是由所有α_i组成的对角矩阵。迭代中,如果某个α_i被更新得非常大,对应的Σ_ii会变小,后验均值μ_i也会被压到接近零,这一列就被“删掉”了。

2.2 变量表:一行公式到代码变量的映射

拿到一份代码时,第一件事是把公式里的符号和源码变量对上。这一张表会省掉你大量瞎猜的时间:

记号含义代码里的常见变量初始化建议
α_i第 i 个基函数的先验精度(方差倒数)alpha向量全 1,不能为 0
β噪声精度beta1 / var(y)
Σ权重后验协方差Sigma每轮迭代重算
μ权重后验均值mu或W每轮迭代重算
γ_i第 i 个基函数的有效参数贡献gamma迭代过程中计算
Φ设计矩阵Phi训练前构造好

γ_i的值在 0 到 1 之间,直观理解是“这一列基函数实际占用了多少个自由参数”。所有γ_i加起来就是模型的有效自由度,这个值通常远小于基函数总数。α_i不断变大时,γ_i趋近于 0,对应的权重会被剪除。你在代码里看到的mask、active_set、relevance这些名词,本质都是在描述同一个东西:哪些列的α被推向无穷大。

2.3 和 SVM、Lasso 的区别:为什么值得从那边迁过来

用 SVM 做回归或分类,输出的是一个决策函数,几乎没有天然的不确定性估计。Lasso 虽然能做稀疏,但惩罚系数λ要交叉验证,而且在特征高度相关时,Lasso 的稀疏解往往表现不稳。SBL 的三个特点正好补上这些缺口。

第一,不需要用户去调惩罚系数。α和β都是从数据里估出来的。第二,天然给出预测方差。后验公式里不仅有均值,还有协方差,这在异常检测、主动学习、多模态传感器回归这类场景里非常值钱。第三,在特征高度相关时,SBL 倾向于把相关基函数“分摊”到少数几个代表上,而不会像 Lasso 那样随机挑一个。这种特性让它在做多模态特征融合时很受欢迎,很多人复现多模态模型代码时会刻意保留一条 SBL 基线,就是因为它在这种情况下比普通稀疏模型稳。

3. 用 Python 复现最小可运行的稀疏贝叶斯回归:主循环与参数

下面给一个能直接落地的 Python 最小实现。它没有做任何工程优化,完全按上一章的五条公式写,适合先跑通再改。代码文件拆成两块:核变换函数和回归器类。

import numpy as np def rbf_kernel(X, centers, gamma=1.0): """ 以 centers 为字典构建 RBF 核矩阵。 X 形状 (n_query, dim),centers 形状 (n_center, dim) 返回形状 (n_query, n_center) """ diff = X[:, None, :] - centers[None, :, :] return np.exp(-gamma * np.sum(diff ** 2, axis=-1)) class SparseBayesianRegression: """稀疏贝叶斯回归的最小可运行版本。""" def __init__(self, n_iter=1000, tol=1e-3): self.n_iter = n_iter self.tol = tol def fit(self, Phi, y): N, M = Phi.shape y = y.reshape(-1, 1) # 噪声精度用 y 的方差初始化,比较稳 self.beta = 1.0 / np.var(y) # 每个基函数一个超参数,初始全 1 self.alpha = np.ones((M, 1)) phi_t_phi = Phi.T @ Phi phi_t_y = Phi.T @ y I = np.eye(M) # 保存训练时用到的中心,预测时要用同一个字典 self.Phi = Phi for it in range(self.n_iter): A = np.diag(self.alpha.flatten()) # 后验协方差:Sigma = (beta * Phi^T Phi + A)^-1 Sigma = np.linalg.inv(self.beta * phi_t_phi + A) # 后验均值:mu = beta * Sigma * Phi^T y mu = self.beta * (Sigma @ phi_t_y) # 有效自由度:gamma_i = 1 - alpha_i * Sigma_ii gamma = 1.0 - self.alpha.flatten() * np.diag(Sigma) # 更新 alpha:防止除零加一个小量 mu_hat = mu.flatten() ** 2 alpha_new = gamma / (mu_hat + 1e-12) # 更新噪声精度 beta residual = y - Phi @ mu self.beta = (N - gamma.sum()) / (np.sum(residual ** 2) + 1e-12) delta = np.max(np.abs(alpha_new - self.alpha.flatten())) self.alpha = alpha_new.reshape(-1, 1) if delta < self.tol: break # 剪枝:alpha 大到某个阈值,说明对应基函数被边缘化 self.active = self.alpha.flatten() < 1e6 self.mu = mu[self.active] self.Sigma = Sigma[np.ix_(self.active, self.active)] self.active_phi = self.Phi[:, self.active] return self def predict(self, Xt): """返回预测均值和预测标准差。""" Phi_t = rbf_kernel(Xt, self.Phi, gamma=self.gamma_) if hasattr(self, 'gamma_') else Xt mu_t = Phi_t[:, self.active] @ self.mu var_t = np.sum((Phi_t[:, self.active] @ self.Sigma) * Phi_t[:, self.active], axis=1) var_t = var_t + 1.0 / self.beta return mu_t.ravel(), np.sqrt(var_t)

3.1 示例代码讲解:三行核心更新到底在做什么

整个循环里最关键的是三行:算Sigma、算mu、算gamma。协方差Sigma的公式里同时出现了Phi.T @ Phi和A,含义是“数据提供的信息”加上“先验约束”。alpha里某个分量极大时,它强在该基函数方向上加了一个巨大的约束,于是Sigma_ii变小、mu_i也跟着被压小。gamma衡量的是这一列基函数被数据支撑的程度,gamma接近 1 说明该基函数活着,接近 0 说明它已经死了。

更新alpha用的是定点迭代,不是梯度下降,所以代码里看不到学习率。这也是很多新手容易找错方向的地方——你不需要调学习率,需要调的只有收敛阈值和迭代上限。

3.2 在合成数据上把它跑通

下面用一组人工生成的数据验证逻辑是否正确:60 个样本,40 个候选特征,真实只有前 3 个特征起作用。

rng = np.random.default_rng(0) N, M = 60, 40 X = rng.normal(size=(N, M)) w_true = np.zeros(M) w_true[:3] = [2.0, -1.5, 0.8] y = X @ w_true + rng.normal(scale=0.3, size=N) # 用 RBF 核把原始特征映射成核矩阵 gamma = 0.5 Phi = rbf_kernel(X, X, gamma) model = SparseBayesianRegression() model.fit(Phi, y) print("active indices:", np.where(model.active)[0]) print("weights:", model.mu.flatten())

正常情况下你会看到active indices是 3 个左右的索引,权重数值接近[2.0, -1.5, 0.8],但不要指望每次结果完全相同。样本只有 60 个,核宽也是手动给的,稀疏支撑集在小样本下本身会有一定随机性,这很正常。跑通后可以试着把gamma调大调小观察变化,这一步比任何讲解都直观。

3.3 三个必调参数:迭代上限、收敛阈值、核宽

n_iter不要舍不得给,但也没必要给太大。SBL 的迭代前期变化很快,100 轮后进入慢速修正阶段。把tol设到 1e-5 以下通常只是让程序多跑几百轮,对最终稀疏模式基本没有影响,我一般固定 1e-3。真正影响结果的是核宽gamma,它有很强的“玄学”成分。

常见做法是先算一个中位数启发式的初始值:取所有训练样本两两距离的中位数,然后设gamma = 1 / (2 * median_distance²)。如果样本特征尺度差异很大,先做标准化再算。核宽太大时,核矩阵对角占优,模型倾向于每个样本都自成一派,稀疏性会失真;核宽太小时,所有基函数相关性过高,稀疏性又不够。第一篇复现时先用中位数启发式,再在它上下按 0.3 倍、3 倍做两轮试探即可。

提示:这份代码直接对M x M矩阵求逆。当基函数数量在几千以上时,要换用第 5 章里的 Woodbury 版本,否则内存和耗时都会很难看。

4. 多输出稀疏贝叶斯回归的 MATLAB 代码:共享稀疏度的完整写法

在实际工程里,SBL 代码最常见的使用场景不是单输出,而是多输出回归。比如一次实验中同时记录多路传感器信号,目标矩阵Y的形状是N x D,要对 D 个输出同时建立稀疏模型。很多人第一反应是循环 D 次单独拟合,但这会丢掉一个关键信息:多路信号共享同一批基函数,它们的稀疏模式大概率是一致的。

4.1 为什么多输出要共享 alpha

单独拟合意味着每个输出有自己的alpha向量,同一个基函数在通道 A 里活着、在通道 B 里被剪掉,实际物理场景里这种不一致往往没有道理。共享alpha的本质是多任务学习:用一个共同的稀疏模式,加上每个输出独立的后验权重。这样的小样本估计比 D 次独立拟合稳得多。常见的 MATLAB 工程代码会把单输出函数封装成一个内部核心,外层只多一个对Y的列循环改成矩阵运算的步骤。

4.2 多输出 RVM 的主循环:完整代码与参数说明

下面这段是 MATLAB 风格的多输出共享稀疏版本。变量名沿用之前的习惯,Phi是N x M设计矩阵,Y是N x D多目标矩阵。

function [W, alpha, beta, active] = sbl_multiple_output(Phi, Y, max_iter, tol) % SBL_MULTIPLE_OUTPUT 多输出稀疏贝叶斯回归核心循环 % Phi: N x M 设计矩阵,Y: N x D 多目标 % W: M x D 后验权重均值,active: 被保留的基函数索引 [N, M] = size(Phi); [~, D] = size(Y); % 初始化:alpha 全 1,beta 用总方差 alpha = ones(M, 1); beta = 1 / var(Y(:)); PhiTPhi = Phi' * Phi; PhiTY = Phi' * Y; for it = 1:max_iter A = diag(alpha); % 后验协方差与权重均值 Sigma = inv(beta * PhiTPhi + A); W = beta * Sigma * PhiTY; % M x D % 有效自由度 gamma = 1 - alpha .* diag(Sigma); % M x 1 % 共享 alpha:分子是 gamma,分母是该行权重二范数平方 alpha_new = gamma ./ (sum(W.^2, 2) + 1e-12); % 噪声精度用全部输出残差一起更新 R = Y - Phi * W; % N x D beta = (N * D - sum(gamma)) / (sum(R(:).^2) + 1e-12); if max(abs(alpha_new - alpha)) < tol break; end alpha = alpha_new; end active = alpha < 1e6; W = W(active, :); end

这段代码和第 3 章的 Python 单输出版有一个关键差异:分母变成了sum(W.^2, 2),也就是每个基函数对应权重行向量在所有输出上的平方和。原因是多输出共享一个alpha时,证据下界最大化得到的定点方程从“除以单个mu_i²”变成了“除以该行权重向量的范数平方”。beta更新里的自由度也变成了N*D - sum(gamma),因为所有输出共同消耗参数。D=1时这套公式自动退化成单输出版本。

4.3 分类与变体:从回归复现到新模型

MATLAB 老工具箱里通常还带一个分类版本,思路是在回归循环外包一层拉普拉斯近似:把高斯误差换成伯努利似然,然后用 IRLS 迭代找出后验众数,再把 SBL 的alpha更新嵌进去。代码会比回归版长一倍,但核心没变,还是那五条公式。很多工程代码里还会出现一种“快速序列算法”,它不是每轮更新所有alpha,而是逐个把基函数加入活动集、计算增量贡献,对大型核矩阵快得多。第一次复现时先跑通共享稀疏版本,再对照快速版本的输出验证一致性,是个很稳的路径。

5. 复现稀疏贝叶斯代码的避坑记录:五个高发故障

这里整理的是我在复现 SBL 代码时反复踩过的几个坑,现象、原因和解决方式各说清楚,顺序基本按“先代码后参数”排查。

5.1 坑一:预测时矩阵尺寸对不上

现象:训练正常,到predict时ValueError,维度爆炸。

原因:训练时你用训练样本作为核中心计算了Phi,预测时直接把测试样本Xt丢进去了,没有重新计算“测试样本到训练样本”的核矩阵。测试样本数不等于训练样本数,矩阵对不上。

解决:在模型对象里保存训练时的字典,预测时调用同一个核函数。常见做法是在fit里存下self.X_train,预测时用rbf_kernel(Xt, self.X_train, gamma)重新生成矩阵,再通过active掩码取列。每次写完复制粘贴前,先检查核矩阵第一个维度是不是当前的查询样本数。

5.2 坑二:数据没标准化,alpha 疯狂震荡

现象:迭代十几轮后alpha出现负值或直接 NaN。

原因:特征列尺度差了几百倍,Phi.T @ Phi的条件数极高,对Sigma求逆时数值不稳定,gamma被算到负值,更新alpha就崩了。代码本身没问题,是数据尺度的锅。

解决:在构造核矩阵之前先把特征做 z-score 标准化。对多输出场景,Y的各列也应该缩放到相似量级,否则beta会被量纲最大的输出主导。这个步骤不属于 SBL 算法,但在所有贝叶斯代码里都是默认前置操作。

5.3 坑三:核宽选错,稀疏结果两个极端

现象:active只剩 1 个索引,或者所有基函数全活着,稀疏性完全失效。

原因:RBF 核宽gamma太大时,核矩阵接近对角阵,每个样本只和自己相似,模型会把过多权重分配到个别点;gamma太小时,所有样本高度相似,基函数之间几乎线性相关,alpha无法拉开差距。

解决:先用中位数启发式定初始值,然后只看一个指标:alpha分布是否出现明显的长尾。理想的中间状态是大部分alpha靠向极大值、少数几个alpha保持在小值。如果alpha全部集中在一个狭窄区间,优先怀疑核宽,而不是怀疑迭代次数。

5.4 坑四:直接求逆把内存打爆

现象:基函数数量到 5000 时程序变慢,到 10000 时内存直接耗尽。

原因:Sigma = inv(beta * Phi.T @ Phi + A)要构造并求逆一个M x M矩阵,复杂度O(M³)。当N < M时这样做非常浪费。

解决:用 Woodbury 恒等式把求逆转移到N x N空间,这是很多工程版 SBL 源码里的标准做法的核心思路:

K = Phi @ Phi.T # N x N S = np.linalg.inv(beta * K + np.diag(1.0 / alpha.flatten())) # N x N # 权重后验均值,W 形状 M x D W = beta * Phi.T @ (S @ Y) # 如果只要 diag(Sigma),不要显式重建 M x M 矩阵 # 可以按列循环计算:diag_Sigma_i = beta * Phi[:, i].T @ S @ Phi[:, i]

代码里的S是N x N的中间矩阵。只要N远小于M,这种写法在速度和内存上都占优。注意更新gamma时仍然需要Σ_ii,此时不需要完整重建Sigma,逐列计算花销可以接受。

5.5 坑五:alpha 迭代不收敛,残差方差被极端点带飞

现象:delta不降反升,或者beta忽大忽小,到上限也没停。

原因:beta的更新对残差里的离群点非常敏感,一个极端样本就能让噪声方差跳一个量级,反过来污染alpha更新。另一个常见原因是alpha更新没有做任何阻尼,在数值边缘来回震荡。

解决:两条路都值得试。第一,把beta更新改成每 3 到 5 轮才执行一次,让alpha先稳定下来。第二,给alpha更新加阻尼:alpha = 0.7 * alpha_new + 0.3 * alpha_old。工程代码里这两种做法很常见,不是数学上必须,但实践中能让收敛稳定很多。

6. 预测方差才是 SBL 的赠品:校准验证与主动选点

回归代码跑通后,很多人只取mu当预测值,把输出的标准差忽略掉。这浪费了 SBL 最大的价值。预测方差来自后验协方差和噪声精度的叠加,它告诉你在某个输入附近,模型有多不确定。下面这段代码可以做一个简单的覆盖率校准检查:把测试集的预测标准差按分位数分桶,看每个桶内真实误差落在 1 倍标准差内的比例,理论上应接近 68%。

def calib_check(y_true, mu, sd, groups=10): quantiles = np.linspace(0, 1, groups + 1) bounds = np.quantile(sd, quantiles) for i in range(groups): mask = (sd >= bounds[i]) & (sd < bounds[i + 1]) if mask.sum() == 0: continue diff = y_true[mask] - mu[mask] cover = np.mean(diff**2 < sd[mask]**2) print(f"sd range {bounds[i]:.3f}-{bounds[i+1]:.3f}, " f"coverage={cover:.2f}, n={mask.sum()}")

预测方差大的区域通常对应训练样本稀疏的区域,这正是主动学习要重点采集的位置。在多模态融合场景里,各模态输出的预测方差也可以直接当权重,方差小的模态给更大权重,这比固定权重或经验权重稳得多。我现在的习惯是每次训练完先打印alpha分布,再看一遍覆盖率曲线——alpha分布能一眼看出模型是稀疏了还是坍缩到某种病态,覆盖率曲线则告诉我方差到底可不可信。这两个检查比任何训练损失曲线都更能暴露问题。希望你跑完也能保留这两个习惯,会让后续用它做实操省下不少力气。

本文还有配套的精品资源,点击获取

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

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

立即咨询