文章目录
- 前言
- 一、SVGD 核心公式
-
- 1. 公式左侧
ϕ
∗
(
x
i
)
\\phi^*(x_i)
ϕ∗(xi) - 2. 公式右侧
-
- 2.1 投票权重:核函数
k
(
x
j
,
x
i
)
k(x_j, x_i)
k(xj,xi) - 2.2 投票内容一:利用项
k
(
x
j
,
x
i
)
∇
x
j
log
p
(
x
j
)
k(x_j, x_i) \\nabla_{x_j} \\log p(x_j)
k(xj,xi)∇xjlogp(xj) - 2.3 投票内容二:探索项
∇
x
j
k
(
x
j
,
x
i
)
\\nabla_{x_j} k(x_j, x_i)
∇xjk(xj,xi)
- 2.1 投票权重:核函数
- 1. 公式左侧
- 二、代码编写与实验
-
- 1.设置目标分布
- 2.设置核函数
- 3.SVGD单步更新
- 4. 主实验流程以及结果
- 5.实验结果
- 总结
前言
Stein Variational Gradient Descent (SVGD) 是一种确定性的、基于粒子的贝叶斯推断算法。
可以把它想象成一个“粒子雕塑家”:它让一群粒子在 “指向高概率区”的吸引力 和 “防止粒子重叠”的排斥力 共同作用下移动,最终使粒子的整体分布逼近一个你想要的、但是难以计算的, 复杂的目标后验分布 。
SVGD输出的粒子集本身构成了目标分布的一个确定性近似(经验分布),因此从这个粒子集中均匀随机选取一个粒子,就等价于从这个近似分布中采样。
例如,Soft Q-learning 使用 SVGD 对难以直接计算和采样的隐式能量策略做近似采样。
一、SVGD 核心公式
Stein Variational Gradient Descent (SVGD) 的核心是一个迭代更新公式。它指导着代表近似分布的每一个“粒子”如何移动,以逼近复杂的目标分布。
其核心更新公式如下:
ϕ
∗
(
x
i
)
=
1
n
∑
j
=
1
n
[
k
(
x
j
,
x
i
)
∇
x
j
log
p
(
x
j
)
+
∇
x
j
k
(
x
j
,
x
i
)
]
\\phi^*(x_i) = \\frac{1}{n} \\sum_{j=1}^{n} \\left[ k(x_j, x_i) \\nabla_{x_j} \\log p(x_j) + \\nabla_{x_j} k(x_j, x_i) \\right]
ϕ∗(xi)=n1j=1∑n[k(xj,xi)∇xjlogp(xj)+∇xjk(xj,xi)]
1. 公式左侧
ϕ
∗
(
x
i
)
\\phi^*(x_i)
ϕ∗(xi)
- 含义:这是为第
i
i
i 个粒子x
i
x_i
xi 计算出的最优更新方向。 - 作用:粒子
x
i
x_i
xi 的“更新导航指令”。在每次迭代中,粒子x
i
x_i
xi 会沿着这个方向移动一小步,即x
i
←
x
i
+
ϵ
ϕ
∗
(
x
i
)
x_i \\leftarrow x_i + \\epsilon \\phi^*(x_i)
xi←xi+ϵϕ∗(xi),从而使整个粒子群所代表的分布更接近目标分布。
2. 公式右侧
右侧是一个求和平均,意味着每个粒子的最终移动方向,是由所有粒子
x
j
x_j
xj (
j
=
1
到
n
j=1 到 n
j=1到n) 共同“投票” 决定的。
2.1 投票权重:核函数
k
(
x
j
,
x
i
)
k(x_j, x_i)
k(xj,xi)
-
含义:一个正定核函数,用于衡量两个粒子
x
j
x_j
xj 和
x
i
x_i
xi 之间的相似度或距离。通常使用RBF(高斯)核。
-
解释:它决定了“投票者”
x
j
x_j
xj 对“被更新者”
x
i
x_i
xi 的影响力大小。
- 如果
x
j
x_j
xj 和x
i
x_i
xi 在空间中非常接近,k
(
x
j
,
x
i
)
k(x_j, x_i)
k(xj,xi) 的值就 大,x
j
x_j
xj 的“意见”就会被更多地采纳。 - 如果它们相距甚远,
k
(
x
j
,
x
i
)
k(x_j, x_i)
k(xj,xi) 的值就 小,x
j
x_j
xj 的“意见”几乎可以被忽略。 - 这种机制使得粒子的更新具有局部性,即粒子主要受其邻近粒子的影响。
- 如果
-
常用的核函数:RBF(高斯)核。
RBF(Radial Basis Function)核 在实践中,RBF(Radial Basis Function)核,也就是高斯核,是绝对的主流选择。它的数学形式如下:
k
(
a
j
,
a
i
)
=
exp
(
−
∥
a
j
−
a
i
∥
2
2
h
2
)
k(a_j, a_i) = \\exp\\left( -\\frac{\\|a_j – a_i\\|^2}{2h^2} \\right)
k(aj,ai)=exp(−2h2∥aj−ai∥2)
其中,
h
h
h 就是带宽参数。
为什么不选别的?
- 效果与效率的平衡:RBF核在灵活性和计算效率之间提供了很好的平衡。
- 理论基础:它是SVGD原始论文中建议和使用的标准核函数。
- 其他选择:虽然有研究提到逆多元二次(IMQ)核在某些场景下可能表现更好,但RBF核因其实现简单和普遍的良好表现,仍然是大多数应用的首选。
带宽(Bandwidth,
h
h
h)的设置 带宽
h
h
h 是RBF核的核心参数,它决定了粒子之间“影响力”的范围。
-
h
h
h 太小:核函数衰减极快,粒子间有效作用范围很小,“斥力”过强,粒子会过度分散,难以收敛到目标分布。 -
h
h
h 太大:核函数变化平缓,“斥力”很弱,粒子容易受“引力”主导而坍缩到一起,失去多样性。
标准方案:中位数启发式(Median Heuristic)
这是目前最主流且实践证明有效的自适应方法,其核心思想是让带宽
h
h
h 与粒子当前的分布尺度相匹配。 其计算步骤如下:
N
N
N 个粒子
{
a
1
,
.
.
.
,
a
N
}
\\{a_1, …, a_N\\}
{a1,…,aN} 之间所有的成对平方距离
∥
a
j
−
a
i
∥
2
\\|a_j – a_i\\|^2
∥aj−ai∥2,共
N
(
N
−
1
)
/
2
N(N-1)/2
N(N−1)/2 个。
h
h
h 由以下公式给出(常用):
h
=
m
e
d
2
log
N
h = \\frac{med}{\\sqrt{2 \\log N}}
h=2logNmed
或者在一些实现中会使用更简化的版本:
h
=
m
e
d
h = med
h=med 或者
h
=
m
e
d
2
log
N
h = \\frac{med^2}{\\log N}
h=logNmed2
实践中,你可以在每步SVGD迭代时都重新计算
h
h
h(动态更新),也可以在几步或整个训练过程中固定使用一个初始计算的值(静态固定)。动态更新通常更稳健。
2.2 投票内容一:利用项
k
(
x
j
,
x
i
)
∇
x
j
log
p
(
x
j
)
k(x_j, x_i) \\nabla_{x_j} \\log p(x_j)
k(xj,xi)∇xjlogp(xj)
- 含义:这是驱动粒子 “利用” 已有知识、朝向目标分布高概率区域移动的力。其中
∇
x
j
log
p
(
x
j
)
\\nabla_{x_j} \\log p(x_j)
∇xjlogp(xj) 是 “打分函数” ,p
(
x
)
p(x)
p(x) 其实就是粒子需要逼近的复杂的目标分布。 - 解释:
∇
x
j
log
p
(
x
j
)
\\nabla_{x_j} \\log p(x_j)
∇xjlogp(xj) 是目标分布p
p
p 在粒子x
j
x_j
xj 处的对数梯度(Score Function)。它指明了从x
j
x_j
xj 出发,哪个方向能让p
p
p的值增长最快。这一项的整体效果是,每个粒子都会加权平均所有其他粒子的“建议方向”,然后朝着目标分布概率密度更高的区域移动。
2.3 投票内容二:探索项
∇
x
j
k
(
x
j
,
x
i
)
\\nabla_{x_j} k(x_j, x_i)
∇xjk(xj,xi)
- 含义:这是保证粒子群 “探索” 多样性、避免粒子坍缩到一点的力。
- 解释:这是核函数的梯度。
- 当两个粒子
x
j
x_j
xj 和x
i
x_i
xi 靠得太近时,k
k
k 值会很大,其梯度∇
x
j
k
(
x
j
,
x
i
)
\\nabla_{x_j} k(x_j, x_i)
∇xjk(xj,xi) 会指向让它们互相远离的方向。 - 这就像在粒子之间施加了一种 “排斥力” ,防止所有粒子都拥挤在同一个最优解上,从而保持粒子群的多样性,实现更好的探索。
- 当两个粒子
二、代码编写与实验
以下实验将使用 一维双峰高斯混合模型 作为复杂目标分布,使用 从标准高斯采样的初始粒子群 作为简单逼近分布。通过 SVGD 迭代,将看到粒子“分裂”并覆盖两个峰。
1.设置目标分布
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm, gaussian_kde
from scipy.spatial.distance import pdist
# ———- 1. 定义目标分布 (双峰高斯混合) ———-
mu1, sigma1 = –3.0, 0.8
mu2, sigma2 = 3.0, 0.8
w1, w2 = 0.5, 0.5 # 双峰权重
# 双峰高斯混合概率密度值
def target_density(x):
"""计算目标分布的概率密度值"""
return w1 * norm.pdf(x, mu1, sigma1) + w2 * norm.pdf(x, mu2, sigma2)
# 双峰高斯混合 “打分函数”
def target_score(x):
"""
计算 log p(x) 的梯度 (score function)
输入 x: shape (N, 1)
输出: shape (N, 1)
"""
# 计算两个分量的密度
p1 = w1 * norm.pdf(x, mu1, sigma1)
p2 = w2 * norm.pdf(x, mu2, sigma2)
p = p1 + p2
# 梯度的分子: sum( w_i * N_i * (-(x – mu_i) / sigma_i^2) )
grad_p1 = p1 * (–(x – mu1) / sigma1 ** 2)
grad_p2 = p2 * (–(x – mu2) / sigma2 ** 2)
# 防止除以 0 (加上极小值)
return (grad_p1 + grad_p2) / (p + 1e-12)
2.设置核函数
# ———- 2. 定义 RBF 核及其梯度 ———-
def rbf_kernel(x, y, h=None):
"""
计算 RBF 核矩阵
x: (N, 1), y: (M, 1) -> 返回 (N, M)
"""
# 计算平方距离矩阵 (利用广播)
diff = x[:, None, :] – y[None, :, :] # (N, M, 1)
sq_dist = np.sum(diff ** 2, axis=2) # (N, M)
if h is None:
# 中值启发式: 基于所有粒子间距离的中位数
flat_dist = sq_dist.flatten()
# 排除自身距离 0
median_dist = np.median(flat_dist[flat_dist > 0]) if np.any(flat_dist > 0) else 1.0
h = median_dist / np.log(len(x) + 1) # 常用公式
h = max(h, 1e-3) # 防止过小
return np.exp(–sq_dist / h), h
3.SVGD单步更新
# ———- 3. SVGD 单步更新 ———-
# 返回更新的粒子和带宽
def svgd_update(x, score_func, lr=0.1):
"""
执行一步 SVGD 更新
x: (n_particles, 1)
"""
n = x.shape[0]
# 计算核矩阵和带宽
K, h = rbf_kernel(x, x)
# 计算目标分布的 score
score_vals = score_func(x) # (n, 1)
# 计算梯度 phi
# 初始化更新量
phi = np.zeros_like(x)
for i in range(n):
# 第 i 个粒子的核值 K[:, i] 形状 (n, 1)
k_i = K[:, i:i + 1]
# 计算核梯度 dk/dx_j = -2 * (x_j – x_i) / h * K[j, i]
# 注意: x – x[i] 形状 (n, 1)
grad_k = –2 * (x – x[i]) / h * k_i
# 累加: (1/n) * sum_j [ k(x_j, x_i)*score(x_j) + grad_x_j k(x_j, x_i) ]
phi_i = np.mean(k_i * score_vals + grad_k, axis=0, keepdims=True)
phi[i] = phi_i
# 更新粒子
x = x + lr * phi
return x, h
4. 主实验流程以及结果
# ———- 4. 主实验流程 ———-
def run_svgd_experiment():
# 设置随机种子
np.random.seed(42)
# 超参数
n_particles = 100 # 粒子个数
iters = 300 # 迭代次数
lr = 0.3
# 初始化粒子 (从简单高斯分布采样)
x_init = np.random.randn(n_particles, 1) * 1.0 # 粒子初始位置
x = x_init.copy()
# 用于记录过程
history = []
history.append(x.copy())
# SVGD 迭代
print("开始 SVGD 迭代…")
for t in range(iters):
x, h = svgd_update(x, target_score, lr=lr)
if t % 50 == 0 or t == iters – 1:
history.append(x.copy())
print(f"Iter {t:3d}, 带宽 h = {h:.4f}")
# ———- 5. 可视化 ———-
# 定义要展示的迭代步
plot_iters = [0, 1, 2, 3, 299]
n_steps = len(plot_iters)
# 创建 2 行 3 列的子图网格,第一个子图放密度对比,其余放各迭代步的粒子分布
fig, axes = plt.subplots(2, 3, figsize=(16, 9))
# 将 axes 展平,方便按顺序索引
ax_list = axes.flatten()
# 生成用于绘制真实密度的 x 轴
x_plot = np.linspace(–7, 7, 500)
true_pdf = target_density(x_plot)
# —- 子图 0: 原始密度对比图(Initial vs Final)—-
ax0 = ax_list[0]
ax0.plot(x_plot, true_pdf, 'b-', linewidth=2.5, label='Target')
# 初始粒子 KDE
"""
输入一维样本数组,
gaussian_kde 基于高斯核做核密度估计,
构造出平滑连续的概率密度函数(PDF)对象;
之后你传入坐标点评估,才能得到对应位置的密度值
"""
kde_init = gaussian_kde(x_init.flatten())
ax0.plot(x_plot, kde_init(x_plot), 'k–', linewidth=1.5, alpha=0.7, label='Initial (KDE)')
# 最终粒子 KDE
kde_final = gaussian_kde(x.flatten())
ax0.plot(x_plot, kde_final(x_plot), 'r-', linewidth=2.5, label='SVGD Final (KDE)')
# 地毯点:最终粒子
ax0.scatter(x.flatten(), np.zeros_like(x.flatten()) – 0.02,
c='red', s=10, alpha=0.6, label='Final particles')
ax0.set_xlabel('x')
ax0.set_ylabel('Density')
ax0.set_title('Initial vs Final Approximation')
ax0.legend()
ax0.grid(alpha=0.3)
# —- 子图 1~5: 分别显示各迭代步的粒子分布 —-
# 颜色列表,用于区分不同迭代步
colors = ['gray', 'green', 'orange', 'purple', 'red']
# 循环绘制每个迭代步
for idx, it in enumerate(plot_iters):
ax = ax_list[idx + 1] # 从第二个子图开始
# 从 history 中取出对应迭代步的粒子
if it < len(history):
particles = history[it].flatten()
else:
particles = history[–1].flatten() # 保险
# 绘制真实分布(灰色虚线,作为参考)
ax.plot(x_plot, true_pdf, 'k–', linewidth=1.0, alpha=0.5, label='Target')
# 绘制该迭代步粒子的 KDE
kde = gaussian_kde(particles)
ax.plot(x_plot, kde(x_plot), color=colors[idx], linewidth=2, label=f'Iter {it}')
# 绘制地毯点(粒子位置)
# ax.scatter(particles, np.zeros_like(particles) – 0.01 * (idx + 1),
# c=colors[idx], s=8, alpha=0.6)
ax.scatter(particles, np.zeros_like(particles),
c=colors[idx], s=8, alpha=0.6)
ax.set_xlabel('x')
ax.set_ylabel('Density')
ax.set_title(f'Iteration {it}')
ax.legend()
ax.grid(alpha=0.3)
ax.set_xlim(–7, 7) # 统一 x 轴范围
# 隐藏可能多余的空子图(如果有)
# 此处 2×3=6 个子图正好全部使用,无多余
plt.tight_layout()
plt.show()
if __name__ == "__main__":
run_svgd_experiment()
5.实验结果
由于这个目标分布和初始分布较为简单与相似,所以迭代3~4次,就差不多可以用这些粒子模拟目标分布。

总结
SVGD收敛后,粒子集 {
x
i
x_i
xi} 就是目标分布
p
p
p 的近似样本集。因此,通过均匀随机抽取这些粒子,就可以完成对该复杂分布的近似采样。这正是SVGD实用且高效的原因所在。





