欢迎光临
我们一直在努力

Stein Variational Gradient Descent (SVGD)

文章目录

  • 前言
  • 一、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)

  • 二、代码编写与实验
    • 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=1n[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)

    xixi+ϵϕ(xi),从而使整个粒子群所代表的分布更接近目标分布。

2. 公式右侧

右侧是一个求和平均,意味着每个粒子的最终移动方向,是由所有粒子

x

j

x_j

xj (

j

=

1

n

j=1 到 n

j=1n) 共同“投票” 决定的。

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(2h2ajai2)

其中,

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

    ajai2,共

    N

    (

    N

    1

    )

    /

    2

    N(N-1)/2

    N(N1)/2 个。

  • 取中位数:找到这些距离的中位数(median),记为 med。
  • 计算带宽:带宽

    h

    h

    h 由以下公式给出(常用):

    h

    =

    m

    e

    d

    2

    log

    N

    h = \\frac{med}{\\sqrt{2 \\log N}}

    h=2logN

    med

  • 或者在一些实现中会使用更简化的版本:

    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实用且高效的原因所在。

    赞(0)
    未经允许不得转载:171主机测评 » Stein Variational Gradient Descent (SVGD)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址