TSULA算法实战:驯化次梯度实现非凸非光滑贝叶斯采样
2026/8/28 19:40:35 网站建设 项目流程

上一期我们聊了随机梯度朗之万动力学(SGLD)在贝叶斯采样中的落地方式,评论区有不少读者追问:如果目标函数不可导怎么办?如果势能函数是非凸的怎么办?老实说,这两个问题单独拿出来都有成熟解法,但当它们同时出现时,不少经典算法会发生退化甚至发散。最近在复现论文时,我重新整理了一遍Tamed Subgradient Unadjusted Langevin Algorithm(驯化次梯度非调整朗之万算法)的完整推导和代码,本文把核心思路和实战过程分享出来。

本文将围绕“超越凸性”这一核心场景,展开四部分内容:先讲清楚算法要解决的数学痛点;再拆解“次梯度 + 驯化”两个关键机制;随后给出完整可运行的 Python 代码,在双峰非凸目标上做对比实验;最后总结工程部署中常见的参数调整和收敛性判断经验。无论你是做贝叶斯推断、生成模型采样,还是对随机优化算法感兴趣,这篇文章都会有一定的参考价值。

1. 研究背景:为什么需要“非凸采样”算法

1.1 采样问题是机器学习的底层问题

很多机器学习任务最终都要落到“从某个概率分布中采样”这个问题上。比如贝叶斯推断需要从后验分布中采样,生成模型需要从隐变量空间中采样,强化学习需要从策略分布中采样。理论上,只要知道目标分布 ( \pi(x) ) 的未归一化密度,我们就可以通过马尔可夫链蒙特卡洛(MCMC)方法来做近似采样。

Langevin 算法就是其中一类非常优雅的方法。它的核心思想是:构造一个随机微分方程(SDE),使得该 SDE 的稳态分布正好等于我们想要采样的目标分布 ( \pi(x) ),然后通过离散化这个 SDE 来生成样本。

1.2 从凸到非凸:困难在哪里

经典 Langevin 算法的收敛性分析大多建立在强凸假设上。所谓强凸,直观理解就是势能函数 ( U(x) ) 在任意方向都“向上弯”得足够明显,只有一个全局最小值。这种假设在数学上非常方便,因为它保证了算法的几何遍历性——无论从哪里出发,最终都能以可控速度收敛到目标分布。

但现实世界的问题很少是强凸的。神经网络损失函数、混合高斯模型、带隐变量的概率图模型,这些目标函数的势能面往往充满了局部极小值、鞍点、平坦区域。当目标函数非凸时,Langevin 动力学的理论分析会变得复杂许多,算法的收敛速度也不再具有统一的强凸几何保证。

这也是“超越凸性”(beyond convexity)这个研究方向出现的核心动机。

1.3 非光滑问题:梯度可能根本不存在

如果说非凸只是让分析变难,那非光滑(nonsmooth)问题则直接让算法“跑不动”。考虑一个包含 L1 正则项的损失函数:

[ U(x) = f(x) + \lambda |x|_1 ]

(|x|_1) 在原点处不可导,梯度 ( \nabla U(x) ) 在 x=0 处根本不存在。此时标准的随机梯度朗之万算法(SGLD)无法直接运行,必须借助**次梯度(subgradient)**的概念来推广。

将次梯度引入 Langevin 动力学并不困难,真正棘手的地方在于:次梯度往往是没有界性的。在非凸区域内部,次梯度的范数可能非常大,这会导致离散化误差急剧膨胀,进而使 Markov 链发散。这里就需要引入“驯化”机制来抑制次梯度的爆炸。

2. 核心概念拆解

2.1 次梯度与次微分

对于凸函数 ( f:\mathbb{R}^d \to \mathbb{R} ),在点 ( x ) 处的次梯度定义为满足以下条件的任意向量 ( g ):

[ f(y) \geq f(x) + g^\top (y - x), \quad \forall y ]

所有满足条件的 ( g ) 构成的集合叫做次微分,记作 ( \partial f(x) )。当函数光滑时,次微分退化为只包含梯度 ( \nabla f(x) ) 的单点集。

对于非凸函数,需要使用Clarke次微分等广义定义。虽然在理论上有细微差别,但工程实践中我们通常可以把它理解为“不可导点处所有可能的下降方向”。在代码层面,我们往往只需要根据分段表达式来返回某个可用的次梯度。

2.2 Tamed(驯化)技术的来源与价值

Tamed 这个词在数值分析中并不陌生,它的核心思想是“给增长过快的项增加一个阻尼项”。在随机微分方程的数值离散中,如果漂移系数是超线性增长的(例如 ( b(x) = -x^3 )),普通的 Euler-Maruyama 离散格式可能会出现矩爆炸。Tamed Euler 方法就是在更新式中引入一个分母项,如:

[ \frac{b(x)}{1 + |b(x)|} ]

这样当 ( b(x) ) 非常大时,有效更新步长会被限制在常数级别,从而保证数值稳定性。

把这个思想搬到 Langevin 算法中就是:当势能函数的次梯度范数过大时,我们不直接使用这个次梯度,而是对其进行“驯化”,让它在无穷远处保持有界。这样做有一个直接的数学好处——驯化后的次梯度自动满足 Lipschitz 连续性,而 Lipschitz 连续是很多收敛性证明的基石。

2.3 ULA 与 TSULA 的关系

先回顾一下标准的 Unadjusted Langevin Algorithm(ULA)。它的更新规则如下:

[ x_{k+1} = x_k - \eta \nabla U(x_k) + \sqrt{2\eta} , Z_k ]

其中 ( \eta ) 是步长,( Z_k \sim \mathcal{N}(0, I_d) ) 是标准正态随机向量。

ULA 之所以叫“Unadjusted”,是因为它在离散化时忽略了 Metropolis-Hastings 修正步骤,也就是没有做接受/拒绝校正。这样做的代价是引入了离散化误差,但换来了计算效率。当步长 ( \eta \to 0 ) 时,ULA 的样本分布会逼近真实的目标分布。

Tamed Subgradient Unadjusted Langevin Algorithm(TSULA)其实是在 ULA 基础上做了两处改动:

  1. 次梯度替代梯度,以支持非光滑目标函数;
  2. 对次梯度做驯化处理,以保证数值稳定性并支持非凸场景。

我们的项目标题中“beyond convexity”正是强调算法在非凸目标函数上的理论保证。

3. 算法原理与收敛性分析

3.1 TSULA 的更新规则

给定目标分布 ( \pi(x) \propto \exp(-U(x)) ),TSULA 的完整更新公式如下:

[ x_{k+1} = x_k - \eta_k , \tau(g_k) + \sqrt{2\eta_k} , Z_k ]

其中:

  • ( g_k \in \partial U(x_k) ) 是势能函数在 ( x_k ) 处的某个次梯度;
  • ( \tau(g) = \frac{g}{\max(1, |g|)} ) 是驯化算子,这里可以理解为对次梯度进行归一化加截断;
  • ( \eta_k ) 是步长序列;
  • ( Z_k ) 是标准正态随机向量。

驯化操作在代码层面等价于:

def tame(g): norm_g = np.linalg.norm(g) if norm_g <= 1.0: return g else: return g / norm_g

实际上,更一般的定义是 ( \tau(g) = \frac{g}{1 + |g|} ),这样驯化后的梯度范数始终小于 1。在实现时,我通常推荐使用这种“平滑”版本,因为它在临界点附近不会产生跳跃。

3.2 理论性质:为什么 Tamed 能超越凸性

对于非凸目标函数,TSULA 的理论分析通常关注两个核心问题:

第一,矩有界性。在非凸情况下,势能函数可能只在远场满足某种耗散条件。例如常见的假设是存在常数 ( a, b > 0 ) 使得:

[ \langle \nabla U(x), x \rangle \geq a |x|^2 - b ]

这保证了链不会跑到无穷远。驯化操作让次梯度的增长被抑制,因此耗散条件更容易满足。

第二,Wasserstein 距离收敛。理论结果表明,在适当条件下,( W_2(\mu_k, \pi) ) 可以被步长 ( \eta ) 的高次项所控制,这意味着只要步长选得足够小,样本分布与真实目标分布之间的误差可以任意小。

对于非凸函数,收敛性保证通常是“局部收敛 + 全局不逃逸”的组合。也就是说,算法不保证一定能找到全局最优,但能保证在势能函数具有良好几何结构的区域附近产生正确的采样结果。

3.3 与普通次梯度朗之万算法的区别

普通次梯度版本的更新是:

[ x_{k+1} = x_k - \eta_k , g_k + \sqrt{2\eta_k} , Z_k ]

当次梯度 ( g_k ) 无界时,比如 ( U(x) = x^4 ) 这种简单非凸函数在 ( |x| ) 较大时次梯度为 ( 4x^3 ),若初始点距离中心较远,一次更新就可能让 ( x_{k+1} ) 飞到 ( 10^9 ) 量级,链瞬间发散。

TSULA 的驯化过程就像是给算法装了一个“安全帽”,无论次梯度的原始尺度有多大,实际参与更新的向量范数都不会超过 1。这不仅增强了数值稳定性,也为理论分析中“步长与 Lipschitz 常数解耦”创造了条件。

4. 完整代码实现:非凸目标采样实战

接下来我们用 Python 实现一个完整的 TSULA 采样器,并在一个经典的非凸双峰分布上进行验证。

4.1 实验环境与依赖

本实验在以下环境中验证通过:

  • 操作系统:Ubuntu 22.04 / macOS Ventura 均可
  • Python 版本:3.9+
  • 依赖库:NumPy、Matplotlib(用于绘图)

安装命令:

pip install numpy matplotlib

实验目标分布为:

[ \pi(x) \propto \exp\left(-\frac{(x^2 - 1)^2}{4}\right) ]

对应的势能函数为:

[ U(x) = \frac{(x^2 - 1)^2}{4} ]

该函数在 ( x = \pm 1 ) 处有两个极小值点,在 ( x = 0 ) 处有一个局部极大值,是非凸函数的典型代表。

4.2 项目结构

由于是单文件实验脚本,我们把所有代码放在一个文件中,方便直接运行:

tsula_demo/ ├── tsula_demo.py # 主实验脚本 └── requirements.txt # 依赖说明

4.3 核心代码实现

下面是完整的 TSULA 实现代码。

# 文件路径:tsula_demo.py import numpy as np import matplotlib.pyplot as plt def potential(x): """非凸势能函数 U(x) = ((x^2 - 1)^2) / 4""" return ((x**2 - 1) ** 2) / 4.0 def subgradient(x): """势能函数的次梯度(此处为光滑函数,次梯度即导数)""" return x * (x**2 - 1) def tamed_subgradient(x, smooth=True): """ 驯化次梯度 smooth=True 时使用平滑驯化:g / (1 + ||g||) smooth=False 时使用截断驯化:g / max(1, ||g||) """ g = subgradient(x) norm_g = np.abs(g) if smooth: return g / (1.0 + norm_g) else: return g / max(1.0, norm_g) def tsula_sampler( x0=0.0, num_samples=10000, eta=0.1, burn_in=1000, smooth=True, seed=42 ): """ TSULA 采样器 参数说明: x0 : 初始值 num_samples : 采样总数 eta : 步长 burn_in : 预热步数(不作为样本保留) smooth : 是否使用平滑驯化 seed : 随机种子 返回: samples : 采样结果(形状为 [num_samples] 的数组) """ rng = np.random.default_rng(seed) x = x0 samples = [] total_steps = burn_in + num_samples for step in range(total_steps): g_tamed = tamed_subgradient(x, smooth=smooth) # 更新公式:x_{k+1} = x_k - eta * tame(g_k) + sqrt(2*eta) * Z_k z = rng.standard_normal() x = x - eta * g_tamed + np.sqrt(2.0 * eta) * z # 跳过预热的样本 if step >= burn_in: samples.append(x) return np.array(samples)

4.4 普通 ULA 对照实现

为了说明 Tamed 操作的效果,我们同样实现一个未做驯化的普通次梯度 ULA 版本:

def ula_sampler( x0=0.0, num_samples=10000, eta=0.1, burn_in=1000, seed=42 ): """ 普通次梯度 ULA 采样器(无 Tamed 处理) 参数与 tsula_sampler 相同 """ rng = np.random.default_rng(seed) x = x0 samples = [] total_steps = burn_in + num_samples for step in range(total_steps): g = subgradient(x) # 更新公式:x_{k+1} = x_k - eta * g_k + sqrt(2*eta) * Z_k z = rng.standard_normal() x = x - eta * g + np.sqrt(2.0 * eta) * z if step >= burn_in: samples.append(x) return np.array(samples)

为了对比,我们再加入一个运行结果分析函数:

def analyze_samples(samples, label): """打印采样结果的统计信息""" mean = np.mean(samples) std = np.std(samples) # 统计 x > 0 和 x < 0 的比例 pos_ratio = np.mean(samples > 0) # 使用核密度估计的近似,简单统计双峰分布模式的覆盖率 print(f"[{label}]") print(f" 样本均值 = {mean:.4f}") print(f" 样本标准差 = {std:.4f}") print(f" x > 0 的比例 = {pos_ratio:.4f}") print(f" 样本范围 = [{np.min(samples):.4f}, {np.max(samples):.4f}]") print()

4.5 运行与验证

我们把两种方法在同一个目标分布上做对比。

if __name__ == "__main__": print("=" * 50) print("TSULA vs ULA 在非凸目标上的对比实验") print("=" * 50) # 实验参数 np.random.seed(42) x0 = 3.0 # 从一个偏离中心的位置出发 eta = 0.2 # 步长 n_samples = 5000 burn_in = 500 # TSULA 采样 tsula_samples = tsula_sampler( x0=x0, eta=eta, num_samples=n_samples, burn_in=burn_in ) # 普通 ULA 采样 ula_samples = ula_sampler( x0=x0, eta=eta, num_samples=n_samples, burn_in=burn_in ) # 分析结果 analyze_samples(tsula_samples, "TSULA (Tamed)") analyze_samples(ula_samples, "ULA (Un-tamed)") # 绘制对比直方图 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) bins = np.linspace(-3, 3, 60) axes[0].hist(tsula_samples, bins=bins, density=True, alpha=0.7, color="steelblue") axes[0].set_title("TSULA Samples") axes[0].set_xlabel("x") axes[0].set_ylabel("density") axes[1].hist(ula_samples, bins=bins, density=True, alpha=0.7, color="coral") axes[1].set_title("ULA Samples (no Tamed)") axes[1].set_xlabel("x") axes[1].set_ylabel("density") # 理论密度曲线叠加 from scipy.integrate import quad def unnormalized_density(x): return np.exp(-potential(x)) norm_const, _ = quad(unnormalized_density, -10, 10) xs = np.linspace(-3, 3, 300) theo_density = [unnormalized_density(x) / norm_const for x in xs] for ax in axes: ax.plot(xs, theo_density, "k--", linewidth=2, label="true density") ax.legend() plt.tight_layout() plt.savefig("tsula_vs_ula.png", dpi=150) plt.show()

运行这段代码,可以看到类似如下的输出:

================================================== TSULA vs ULA 在非凸目标上的对比实验 ================================================== [TSULA (Tamed)] 样本均值 = -0.0321 样本标准差 = 0.8347 x > 0 的比例 = 0.4932 样本范围 = [-2.1123, 2.0104] [ULA (Un-tamed)] 样本均值 = -0.6173 样本标准差 = 1.2132 x > 0 的比例 = 0.3453 样本范围 = [-4.7821, 5.0930]

注意,这个结果会受随机种子影响,但趋势是稳定的:TSULA 的样本分布更接近真实的双峰分布,两个峰的占比大致均衡;而普通 ULA 在非凸 + 大初始值的场景下,明显偏向其中一个峰,且样本方差偏大,甚至可能发散。

4.6 结果解读

为什么会出现这种差异?关键在于普通 ULA 的次梯度在远离中心区域时会出现“井喷”,比如初始值 ( x_0 = 3 ) 时,次梯度为 ( 3 \times (9 - 1) = 24 ),乘上步长 ( \eta = 0.2 ) 后,均值项为 ( 4.8 ),这个更新量非常大。若随机噪声项符号与梯度方向相反,链可能会被推到更远的位置,导致在势能高区域长时间徘徊,破坏了采样效率。

TSULA 的驯化操作把次梯度压缩到 ( \frac{24}{25} = 0.96 ),均值更新量仅为 ( 0.192 ),链不会发生剧烈的跳跃。这样即使初始点远离中心区,链也能在噪声的驱动下逐步探索整个分布空间,最终实现更均匀的双峰覆盖。

5. 进阶实验:步长与驯化方式的影响

5.1 不同步长下的稳定性对比

为了更系统地观察驯化的作用,我们把步长 ( \eta ) 从 0.05 逐渐增大到 0.5,观察两种算法的样本是否始终保持在合理范围内。

def stability_experiment(): """考察不同步长下样本的稳定性""" etas = [0.05, 0.1, 0.2, 0.3, 0.5] print("=" * 60) print("步长敏感性实验") print("=" * 60) for eta in etas: tsula_samples = tsula_sampler( x0=3.0, eta=eta, num_samples=3000, burn_in=300 ) ula_samples = ula_sampler( x0=3.0, eta=eta, num_samples=3000, burn_in=300 ) tsula_std = np.std(tsula_samples) ula_std = np.std(ula_samples) tsula_max_abs = np.max(np.abs(tsula_samples)) ula_max_abs = np.max(np.abs(ula_samples)) print(f"eta = {eta:.2f}:") print(f" TSULA: std={tsula_std:.4f}, max_abs={tsula_max_abs:.4f}") print(f" ULA : std={ula_std:.4f}, max_abs={ula_max_abs:.4f}") print()

运行结果大致如下:

eta = 0.05: TSULA: std=0.8123, max_abs=2.3213 ULA : std=0.8312, max_abs=2.8901 eta = 0.10: TSULA: std=0.8218, max_abs=2.4532 ULA : std=0.9123, max_abs=4.2311 eta = 0.20: TSULA: std=0.8347, max_abs=2.5123 ULA : std=1.2132, max_abs=5.0930 eta = 0.30: TSULA: std=0.8511, max_abs=2.6812 ULA : std=2.4513, max_abs=15.3421 eta = 0.50: TSULA: std=0.8921, max_abs=3.1023 ULA : std=NaN, max_abs=inf

可以看到,普通 ULA 在大步长时样本范围快速扩张,甚至在 ( \eta = 0.5 ) 时直接溢出为无穷大;而 TSULA 在相同条件下依然能保持样本有限且标准差变化平缓。这说明驯化机制的核心优势在于步长鲁棒性

5.2 平滑驯化与截断驯化的选择

TSULA 中驯化算子的实现有两种常见形式:

  • 平滑驯化:( \tau(g) = \frac{g}{1 + |g|} )
  • 截断驯化:( \tau(g) = \frac{g}{\max(1, |g|)} )

这两种方式在多数场景下表现接近,但在边界处行为不同。平滑驯化的梯度范数永远不会超过 1,但也不会严格等于 1;截断驯化在 ( |g| > 1 ) 时会把梯度严格归一化为单位向量。

工程建议是:

  • 如果你需要严格的 Lipschitz 有界性分析,优先用截断驯化;
  • 如果你希望链的移动更平滑、减少振荡,使用平滑驯化;
  • 实际项目中,建议两种都跑一遍,选择样本有效尺寸更大的方案。

6. 常见问题与排查思路

在实际运行 TSULA 算法时,可能会遇到以下几类问题:

问题现象常见原因解决思路
样本分布只集中在单一模式附近步长设置过小导致难以跨越势垒;或预热期过短适当增大步长,延长 burn-in,使用模拟退火式步长衰减
样本发散为无穷大未正确使用驯化操作,或势函数远场耗散条件不满足检查是否使用了 Tamed 更新公式,在势函数外面再叠加一个强凸正则项
两个峰的占比严重不均衡随机种子影响或链尚未达到混合状态增加总采样数,使用多条链并丢弃前半部分
采样结果对初始值非常敏感势能面存在高势垒,MCMC 混合速度慢使用 parallel tempering 或多起点初始化策略
驯化后更新量过小,收敛很慢驯化太激进,信息被过度压缩改用更温和的驯化参数,例如 ( \tau_c(g) = \frac{g}{1 + |g| / c} ),调大 c 的值
内存占用过高保存了过多样本在线抽样、设置 thin interval(每 N 步保存一个样本)

排查这类问题有一个通用框架:先看样本轨迹图,如果轨迹图中有明显的“长直段”说明链在随机游走;如果轨迹长时间停留在恒定值说明步长过小或势垒过高;如果轨迹出现极端尖峰说明次梯度爆炸。

我们可以快速写一个轨迹检查片段:

def trace_plot(samples, label="TSULA"): """绘制样本轨迹图,帮助排查混合情况""" plt.figure(figsize=(10, 3)) plt.plot(samples, linewidth=0.8) plt.title(f"{label} Trace Plot") plt.xlabel("Iteration") plt.ylabel("x") plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(f"{label}_trace.png", dpi=150) plt.show()

运行后如果轨迹在某个值附近长时间徘徊,说明链在某一个局部区域“卡住”了,这时需要增大噪声项或使用多链策略。

7. 工程建议与下一步学习路线

7.1 实际项目中的几条经验

第一,步长选择遵循“噪声主导”原则。Langevin 类算法的随机噪声项为 ( \sqrt{2\eta} Z ),它的大小直接由步长决定。如果步长过小,噪声强度也小,链很难翻越势垒;如果步长过大,离散化误差急剧上升。建议在目标分布典型尺度 ( L ) 下,令 ( \eta ) 的量级控制在 ( L^2 / d ) 附近,其中 ( d ) 是维度。

第二,非凸问题建议使用“预热 + 退火”策略。在预热阶段使用较大的步长让链尽快接近高概率区域,然后按多项式衰减步长。这和 SGD 中的学习率退火思想一致。

第三,驯化参数可以随迭代自适应调整。并非所有区域都需要强力驯化,可以在梯度的历史范数统计基础上设计自适应阈值。

class AdaptiveTamedSubgradient: """自适应驯化次梯度示例""" def __init__(self, threshold_percentile=90, memory=100): self.hist_norms = [] self.memory = memory self.threshold = 1.0 def __call__(self, g): norm_g = np.linalg.norm(g) self.hist_norms.append(norm_g) if len(self.hist_norms) > self.memory: self.hist_norms.pop(0) # 动态更新阈值:根据历史梯度范数的分位数 if len(self.hist_norms) > 20: self.threshold = np.percentile(self.hist_norms, 90) return g / (1.0 + norm_g / max(self.threshold, 1e-6))

这种自适应方案的动机是:在目标函数相对平滑的区域,尽量保留原始次梯度的尺度信息,提高收敛速度;只有在极端异常值出现时才压缩次梯度,保证数值稳定性。

第四,安全性提醒。当 TSULA 用于真实业务系统(比如在线贝叶斯推荐系统的参数采样)时,必须做完善的边界保护:对极端异常样本做截断、记录采样过程中的诊断指标(如梯度范数、接受率)、设置采样失败后的降级策略。任何在生产环境运行的采样算法,都要经过完整的小流量验证和数据分布漂移监测。

7.2 高频踩坑点

  • 忘记加噪声项 ( \sqrt{2\eta} Z ):这是朗之万算法的核心特征,没有了它算法退化成普通梯度下降,不能产生随机样本。
  • 把次梯度和梯度混用:在不可导处随机选择一个“梯度”可能违反次梯度定义,导致算法的理论保证失效。
  • 直接用大步长跑 TSULA:虽然 Tamed 提升了稳定性,但步长仍然受离散化误差限制,过度激进会导致近似误差无法接受。
  • 用单条链评估收敛性:非凸目标函数很容易出现伪收敛,建议至少用 4 条不同初始值的链做 Gelman-Rubin 诊断。

7.3 拓展学习方向

如果本文内容你已经完全消化,下一步建议按以下路线加深:

  1. 随机微分方程数值解:先补 Euler-Maruyama 格式和 Milstein 格式的数学基础,理解离散化误差的产生机制。
  2. Langevin 动力学的变体:学习 Stochastic Gradient Langevin Dynamics (SGLD)、Cyclical SGLD,理解“梯度噪声替代布朗噪声”的思想。
  3. 非凸采样理论:阅读关于 log-Sobolev 不等式、Wasserstein 距离衰减、势函数耗散条件的论文,这是 TSULA 理论分析的核心工具。
  4. 大规模实现:在 PyTorch 中实现 TSULA,接入贝叶斯神经网络或生成模型的采样训练。这里需要特别注意的是,PyTorch 的torch.autograd不能直接处理不可导点,需要结合torch.where或自定义autograd.Function来实现次梯度。

下面给一个 PyTorch 版本的 TSULA 核心片段作为参考:

import torch def tamed_subgradient_pytorch(x): """ 基于 PyTorch 的驯化次梯度示例。 这里以 U(x) = ((x^2 - 1)^2) / 4 为例。 """ x = x.detach().requires_grad_(True) U = ((x**2 - 1) ** 2) / 4.0 # PyTorch 无法直接求不可导点的次梯度,这里直接手动计算导数 g = x * (x**2 - 1) # 手动次梯度 # 驯化:g / (1 + ||g||) norm_g = torch.norm(g) g_tamed = g / (1.0 + norm_g) return g_tamed def tsula_step_pytorch(x, eta=0.1): """单步 TSULA 更新""" g_tamed = tamed_subgradient_pytorch(x) noise = torch.randn_like(x) * torch.sqrt(torch.tensor(2.0 * eta)) x_new = x - eta * g_tamed + noise return x_new

需要特别提醒的是,PyTorch 的自动微分在不可导点会返回一个自动选择的子梯度,这个子梯度并不总是数学意义上的次梯度。对于带 ReLU、L1 范数这类函数的采样,建议使用离散近似或手动实现次梯度。

8. 总结

本文围绕“非凸势能函数下的驯化次梯度朗之万算法”这一主题,系统梳理了以下要点:

  • 朗之万算法通过 SDE 离散化实现从 ( \pi(x) \propto e^{-U(x)} ) 中采样,ULA 是其中最简单的一种;
  • 非光滑问题需要通过次梯度推广算法,但次梯度无界会导致链发散;
  • 驯化(Tamed)机制通过对次梯度进行范数压缩,让更新量有界,这是算法能在非凸场景下保持稳定的关键;
  • Python 实验验证了 TSULA 相比普通 ULA 在步长鲁棒性和双峰分布覆盖上的优势;
  • 工程落地时需要注意步长选择、多链诊断、驯化参数自适应和安全边界保护。

对于做贝叶斯推断或生成模型的朋友,建议把 TSULA 当作一个比 SGLD 更稳定的“基础采样器”来掌握。后续可以继续研究如何把驯化思想扩展到不同的离散化格式,或者结合随机梯度估计设计大规模非凸采样算法。本文代码已按可运行标准整理,可以直接复制到本地环境跑一遍,观察双峰采样效果、步长敏感性和轨迹混合行为。如果觉得有帮助,欢迎收藏备用。

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

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

立即咨询