做零阶优化的朋友,应该都遇到过一类很让人头疼的问题:目标函数完完全全是黑箱,你只有“输入一个参数、输出一个数值”这一个接口,没有梯度、没有解析结构、甚至函数本身还可能处处不可导。前面几篇零阶优化里,我写过有限差分、写过CMA-ES、也写过基于代理模型的做法,这次想聊一个我从测度松弛思想里实际改出来的求解方案——直接测度松弛。它不是那种能塞进一行公式里炫技的方法,但对付乱糟糟的黑箱函数非常实用,尤其是当目标函数非光滑、多峰、还带着仿真噪声的时候。
这篇不是教科书式的推导,而是记录我怎么把一个问题用直接测度松弛重新建模、怎么把它离散化、怎么调参数,以及过程中踩过的坑。适合手里有黑箱优化问题、被局部极小坑过、或者单纯想给零阶优化工具箱里多塞一件武器的人看。我会尽量把“为什么这么做”讲清楚,也会给一段可以直接跑的最小Python实现,方便你拿去改造。
1. 先搞清楚直接测度松弛在解决什么问题
1.1 黑箱优化的本质难点
黑箱优化大概是所有优化里最“憋屈”的一种。你面对的只有一个求值器,参数进去,一个数出来。没有梯度,所以不能梯度下降;没有解析形式,所以不能做凸分析;更麻烦的是,真实场景里的黑箱函数往往不“规矩”。我最早接触这类问题是在调一个工业仿真器的参数,同一个参数跑三次,返回三个不一样的数值,因为里面有随机性;再比如策略参数搜索时,离散开关一多,目标函数直接跳变,平滑性根本不存在。
这种场景下,传统零阶梯度类方法其实很脆弱。有限差分看着简单,但本质是在用函数值估计梯度,一旦目标函数带噪声,差分出来的梯度方向可能完全是噪声驱动的;如果函数不可导,差分结果更是跳跃得没法用。贝叶斯优化倒是能处理一部分问题,但它要维护一个高斯过程代理模型,随着输入维度升高,拟合代价和估计方差都会快速膨胀,而且代理模型对目标函数的“平滑假设”在乱糟糟的黑箱函数上经常不成立。
所以我的需求很明确:找一种不太依赖目标函数“长相”的优化方式。它最好是只把函数值当作一串数字来用,不需要在局部做任何光滑假设,也不假设目标函数的分布形状。直接测度松弛之所以吸引我,就是因为它把整个优化问题的结构从“非线性点搜索”换成了“线性测度更新”,这个转变直接绕开了前面说的那些毛病。
1.2 从“找点”到“找一个分布”
主流优化算法大部分时间都在做同一件事:在决策空间里找下一个更优的点。这个思维很自然,但它有一个隐式假设——每个点之间独立,我只要记住当前最好的点就行。可黑箱函数多峰且不可导时,单点迭代很容易陷进局部极小,而且你看不到任何关于“哪里还有希望”的全局信息。
直接测度松弛换了一个角度:不去死磕那个点,而是想象存在一个“采样策略”。这个策略每次告诉你应该优先尝试参数空间里的哪些区域,用一个概率分布来描述。我们要优化的不再是一个向量x,而是一个概率测度μ。
用公式写出来就是:
原问题:
min_{x∈Ω} f(x)松弛后:min_{μ∈P(Ω)} ∫_Ω f(x) μ(dx) = E_{x∼μ}[f(x)]
这里有个非常关键的观察:原目标f(x)通常高度非线性,但经过测度松弛之后,目标变成了关于μ的线性泛函。线性问题比非线性问题好处理得多,这是这个方法的根基。
当然天下没有白吃的午餐,代价是决策变量从有限维空间Ω变成了无限维的概率测度空间P(Ω)。但仔细想想,这个交换是值得的:我们拿“决策变量的维度变高”换来了“优化目标线性化”,而线性化之后的算法往往更稳。
还要解释清楚为什么这叫“松弛”。因为原问题的每一个可行解x,都可以对应到P(Ω)里的一个狄拉克测度δ_x——就是把所有概率质量放在单点上。所以原问题只是这个测度优化问题的一个子集。把解范围放宽到所有概率测度,可行域变大了,所以这是一个松弛。如果不加任何额外限制,松弛后的问题虽然线性,最优解依然是一个狄拉克测度,落在f的全局极小点,问题难度并没有真正降下来。关键在于后面要加正则项或约束,把解“摊开”,让它保留多模态信息,迭代时才不会早早掉进某个局部极小。
2. 核心建模:测度空间里的线性化松弛
2.1 概率测度作为决策变量
想把这个无限维问题变成能算的东西,第一步就是离散化。最常见的做法是把μ写成有限个点质量的加权和:
μ ≈ Σ_{i=1}^N p_i δ_{x_i},其中Σ p_i = 1,p_i ≥ 0
于是目标函数近似为:
min_p Σ_i p_i f(x_i)
这个形式看起来像一个线性规划,约束只有单纯形约束,目标函数对p完全是线性的。为什么这个近似对非光滑、带噪声的目标也能扛住?因为f只在线性求和里出现,从头到尾不需要对f做任何光滑性假设。目标值有噪声也没关系,线性加权平均本身就在做某种集成,单点跳变的影响会被其他点稀释。
在实际工程中,支撑点{x_i}怎么来?我一般用两种方式:
固定网格或固定采样集:在搜索空间里均匀铺一批点,只优化权重。这个方法省事、直观,适合低维问题,维度一高就会遇到网格爆炸。5维问题每个维度放10个点就是
10^5次函数评估,这还没算迭代次数,所以固定集合只适合d ≤ 3的场景。自适应支撑集:像粒子滤波一样,迭代过程中按当前测度重采样,扔掉权重极低的点,在权重高的区域补充新点。这是我在代码里采用的方式,它对维度的容忍度高很多,因为支撑集始终保持在一个可控量级(比如500个点),而不是穷举整个空间。
有人会问:那为什么不直接选f最小的那个点?因为如果直接选最小点,那就等于没做松弛,变成了枚举加贪心。权重分布的意义在于保留了很多“目前不是最优但有潜力”的区域,这些区域会参与下一轮候选点生成,这正是探索能力的来源。
2.2 熵正则:控制探索与利用的旋钮
如果只优化min_p Σ_i p_i f(x_i),由于目标对p是线性的,最优解一定落在单纯形的顶点上,也就是其中一个p_j=1,其余全是0。换句话说,不加正则的固定支撑集测度松弛,最终结果就是选一个当前最优样本点,没有任何探索能力,也谈不上算法。
我实际使用的办法是加一个负熵正则项:
min_p Σ_i p_i f(x_i) + τ Σ_i p_i log p_i
这里的τ ≥ 0是温度参数。注意熵函数Σ p_i log p_i本身就是凹函数,加负号之后整个目标变成凸的,这就让问题有了确定的求解结构。加上Σ p_i = 1的约束,用拉格朗日一阶条件可以直接推出闭式解:
p_i = exp(-f(x_i) / τ) / Σ_j exp(-f(x_j) / τ)
这就是一个带温度的玻尔兹曼分布。这个结果我第一次推出来的时候还挺意外:一个看起来很复杂的测度优化,在固定支撑集时居然就是softmax。τ小的时候,p集中在f最小值附近,相当于利用;τ大的时候,p趋向于均匀分布,相当于探索。
所以熵正则项就像一个旋钮,直接控制了“探索”和“利用”的平衡。实际操作中我很少用一个固定的τ,而是让它随迭代退火。初始τ给比较大,让支撑集上的概率分布相对平滑,先摸清全局地形;随着迭代推进,τ逐步降到接近0,让权重逐渐尖锐化,最终落到一个确定的解附近。
2.3 离散化与支撑集管理
前面提到固定网格不适合中高维问题,那自适应支撑集具体怎么管理?我维护了一个固定容量的支撑集,假设为N=500个候选点,每一轮做五件事:
- 计算当前支撑集上所有点的
f值。 - 用当前温度
τ做softmax得到权重p。 - 用
p计算加权均值和加权协方差。 - 丢掉权重最低的一部分点,从加权协方差对应的椭圆区域里采样新点补进来。
- 按退火策略降低温度。
有人会疑惑:既然已经有了softmax解析解,为什么还要用协方差重采样?因为固定支撑集的解是“离散的个案”,它只能在原有候选点之间分配权重,无法在权重高的区域继续细化。比如全局最优落在两个网格点之间,固定支撑集再怎么调权,也只能得到两个点之间的混合分布,精度很差。自适应重采样的意义就是让支撑集本身也动起来,向高概率区域收缩,这才真正逼近连续空间里的优化问题。
协方差缩放系数alpha也很关键。我习惯取1.5~2.0。太小会让新样本过度集中在均值附近,搜索范围过早锁死;太大会让新样本过于发散,温度下降以后仍然在一堆无用区域里浪费评估预算。这个参数没有万能值,跟你目标函数的粗糙程度有关,但1.8左右通常是个好的起点。
3. 最小可运行实现:用熵正则测度松弛做黑箱优化
3.1 算法流程与关键参数
我给出一个完整可跑的最小实现,目标是一个二维多峰函数。为了让结果有说服力,这个函数刻意做成非光滑且多峰,中间还有正弦震荡项,方便观察算法怎么避免陷入局部极小。
主流程如下:
- 在搜索空间内均匀采样
n_init个点,组成初始支撑集。 - 初始化温度
tau0为初始支撑集上f值的标准差。 - 循环
n_rounds轮:- 用当前温度计算softmax权重。
- 计算加权均值、加权协方差。
- 保留权重最高的
n_keep个点。 - 从多元正态分布中采样
n_new个新点,裁剪到边界内。 - 合并新旧支撑集,更新全局最优。
- 对温度做指数退火。
关键参数我整理成了一张表:
| 参数 | 含义 | 典型值 | 调整方向 |
|---|---|---|---|
n_init | 初始候选点数 | 500 | 评估预算多就加大 |
n_rounds | 重采样轮数 | 30 | 收敛慢就增加 |
keep_ratio | 每轮保留高权重点的比例 | 0.5 | 噪声大时降到0.3 |
tau0 | 初始温度 | std(F) | 探索不足就调大 |
tau_end | 最低温度 | 0.05 | 收敛不足就调低 |
alpha | 协方差缩放系数 | 1.8 | 过早局部收敛就调大 |
为什么tau0取初始样本上f值的标准差?因为这样第一批权重的分布和目标值的波动处于同一量级。如果tau0太小,初始权重立刻变成一个近似one-hot的分布,探索空间被自己砍掉了;如果太大,权重几乎均匀,第一批重采样没有任何信息量,浪费评估次数。标准差只是一个自动且合理的基准值,我在很多问题里直接用,效果都还不错。
3.2 Python代码:核心求解器
下面是完整的求解器代码,依赖只有numpy,可以直接复制跑。
import numpy as np def blackbox_target(x): """一个多峰、非光滑的二维黑箱目标函数。 这里用解析函数模拟黑箱:调用方只能拿到标量结果, 无法拿到梯度信息。 """ x0, x1 = x[..., 0], x[..., 1] return ( np.sin(3.0 * x0) + np.cos(2.0 * x1) + 0.15 * (x0 ** 2 + x1 ** 2) + 1.2 * np.abs(x0 - x1) # 不可导点 + 0.3 * np.sin(5.0 * x0) * np.cos(5.0 * x1) # 高频扰动 ) def measure_relaxation_search( target, bounds, n_init=500, n_rounds=30, keep_ratio=0.5, tau0=None, tau_end=0.05, alpha=1.8, seed=0, ): """基于熵正则测度松弛的黑箱优化器。 Parameters ---------- target : callable 黑箱目标函数,输入形状为 (n_points, dim) 的数组, 返回形状为 (n_points,) 的数值数组。 bounds : np.ndarray 形状为 (dim, 2) 的搜索空间边界。 其余参数见上表。 """ rng = np.random.default_rng(seed) dim = bounds.shape[0] lo, hi = bounds[:, 0], bounds[:, 1] # 第一步:均匀撒候选点,作为初始测度支撑 X = rng.uniform(lo, hi, size=(n_init, dim)) F = np.array([target(x) for x in X]) if tau0 is None: tau0 = float(np.std(F)) tau = tau0 decay = (tau_end / tau0) ** (1.0 / max(n_rounds - 1, 1)) best_x = X[np.argmin(F)].copy() best_f = float(F.min()) for it in range(n_rounds): # 1. 当前测度下的熵正则权重:softmax 形式 shifted = F - np.max(F) # 数值稳定处理,防止 exp 溢出 logits = -shifted / max(tau, 1e-12) w = np.exp(logits) w /= w.sum() # 2. 用这个分布估计一阶、二阶矩 mean = w @ X diff = X - mean cov = (diff * w[:, None]).T @ diff cov += 1e-8 * np.eye(dim) # 防止协方差矩阵奇异 cov = alpha * cov # 3. 保留权重靠前的点,按当前测度采样补充新点 n_keep = int(n_init * keep_ratio) keep_idx = np.argsort(w)[::-1][:n_keep] X_keep = X[keep_idx] F_keep = F[keep_idx] n_new = n_init - n_keep X_new = rng.multivariate_normal(mean, cov, size=n_new) X_new = np.clip(X_new, lo, hi) # 边界裁剪 F_new = np.array([target(x) for x in X_new]) # 4. 合并支撑集,记录全局最优 X = np.vstack([X_keep, X_new]) F = np.concatenate([F_keep, F_new]) idx = int(np.argmin(F)) if F[idx] < best_f: best_f = float(F[idx]) best_x = X[idx].copy() # 5. 退火 tau = max(tau * decay, tau_end) return best_x, best_f, X, F, w if __name__ == "__main__": bounds = np.array([[-4.0, 4.0], [-4.0, 4.0]]) best_x, best_f, X, F, w = measure_relaxation_search( blackbox_target, bounds, seed=42 ) print("best_x:", best_x) print("best_f:", best_f)这段代码里有两个地方是“工程细节”而不是“理论细节”,但实际运行缺了它们就会出问题。第一个是shifted = F - np.max(F),这保证logits全部非正,exp不会因为数值溢出变成inf,这是softmax计算的标准操作。第二个是协方差矩阵加微小对角项,因为随着迭代推进,高权重点会越来越集中,样本协方差矩阵很可能奇异,multivariate_normal会直接报LinAlgError。
3.3 实验结果与参数调整记录
我在这台机器上跑上面的代码,bounds=[-4,4]×[-4,4],种子取42,初始tau0自动算出来大概在0.8到1.0之间。前几轮权重还比较均匀,支撑集明显散布在整个搜索空间;到第10轮左右,点开始在几个凹陷区域聚集;后面随着温度下降,逐步收敛到一个稳定解附近。这个现象的“视觉版本”如果在二维图上投影,会看到支撑集先是覆盖全局,然后慢慢分成几团,最后只留下一团。
如果你把tau0改成0.2再跑一次,会发现结果完全不一样:权重在第一轮就基本one-hot了,后面的重采样全部围绕同一个局部极小附近转,不会再有机会跳到另一个谷底。这个现象建议亲手跑一遍,比看任何文字说明都直观。我们做这种方法的,最常犯的错误就是让温度降得太快。温度退火的本质是在“多尝试”和“别乱跑”之间找平衡,前20轮的探索价值最高,越到后面越该收。
我还对比过这个算法在同样预算下和随机搜索的差距。随机搜索5000个点,一般能找到还不错的解,但很难稳定命中全局最优附近;测度松弛在8000次评估左右就能稳定收敛到更优的目标值。不过要注意,这个优势是在“函数有明显多峰结构”的前提下成立的,如果一个函数几乎全是平缓的斜坡,那测度松弛相对随机搜索的优势会小很多。
4. 实战避坑与效果对比
4.1 我踩过的一个典型坑
这个坑发生在协方差估计上。我第一次写自适应支撑集版本时,没有给协方差矩阵加对角抖动,跑到第20轮左右,multivariate_normal突然报错说矩阵奇异。原因很简单:高权重区域聚集得越来越小,样本方差趋近于0,矩阵转置乘出来后变成奇异的。我当时第一反应是增加方差下限,但更稳妥的做法是给对角线加一个很小的1e-8量级的单位阵,保证数值稳定性。如果你发现某个阶段支撑集异常收缩,也可以直接加大这个对角项到1e-4,相当于给采样过程注入一点人为的扩散力。
另一个坑是边界裁剪。新样本是从多元正态分布里采的,分布均值可能靠近搜索空间边缘,导致大量样本被clip在边界上,于是边界点的权重看起来很高,算法会误以为那里是个有价值的区域。我现在的做法是:如果高权重区域离边界很近,就把边界作为有效区域,同时把新样本采样中心稍微往边界内部偏移一点。大多数情况下,只要初始搜索范围给得不过分大,这个问题不会太严重。
4.2 和CMA-ES、贝叶斯优化放在一起怎么看
我经常被问一个问题:测度松弛和CMA-ES到底有什么区别?两者都在维护一个概率分布,看起来有点像。CMA-ES维护的是一个多元正态分布,每一轮更新均值、协方差,然后用这个分布采样下一代个体;测度松弛维护的则是更一般的角点集和权重,分布形状不限定为高斯,而且和CMA-ES最本质的区别是目标函数在优化过程中只参与线性加权,不做排序假设之外的任何运算。
贝叶斯优化则完全是另一条路线:它先拟合一个代理模型(通常是高斯过程),再通过提升概率或期望提升等采集函数决定下一个采样点。代理模型本身就是很强的假设,当目标函数非光滑、高维特征明显时,高斯过程容易把真实结构“磨平”,导致采集函数集中到错误区域。测度松弛没有这个毛病,因为它不建立代理模型,只用函数值的线性组合来推进。
我把几个常用方法放到一起做了个对比:
| 方法 | 是否需要代理模型 | 对非光滑/噪声的容忍度 | 维度扩展性 | 主要代价 |
|---|---|---|---|---|
| 有限差分零阶梯度 | 否 | 差 | 中 | 梯度估计方差大 |
| CMA-ES | 否 | 可接受 | 中低维 | 协方差矩阵维护开销大 |
| 贝叶斯优化 | 是 | 可接受 | 低维 | 代理模型拟合与采集函数开销大 |
| 直接测度松弛 | 否 | 好 | 中(配合自适应支撑) | 支撑集管理与重采样 |
我这么说不是为了证明测度松弛全面碾压其他方法。贝叶斯优化在评估预算极少、目标有噪声、低维的情况下依然非常稳;CMA-ES在20维以内的连续光滑问题上有大量工业验证。测度松弛真正的优势是“不挑食”和“实现简单”:不光滑、有噪声、离散变量混在里面,对核心流程都没有影响。缺点也很明显:如果评估预算很少(比如小于100次),它还没来得及把温度退下去,效果就不如贝叶斯优化;如果目标极便宜而且维度极高,那这个方法的支撑集管理开销也会变大,不如零阶SGD直接。
4.3 使用场景边界与选型建议
我实际用这个方法解决过的场景,大致有这几类:
- 超参数与策略参数搜索:评测一次目标要跑几分钟,样本量有限,但目标函数非常不规则,甚至带随机种子引起的噪声。
- 对抗样本与鲁棒性评估:需要在输入空间里找最大损失点,损失面往往是很多尖峰,平滑性很差。
- 仿真器校准:仿真器数值噪声大,没有梯度,而且可能存在多个等价的参数组合,测度松弛能同时保留多个候选区域,方便后续人工判断。
- 混合整数/离散黑箱问题:把离散变量作为支撑集上的因子,照样可以用权重更新,不需要专门处理离散优化。
不建议使用的场景也明确说一句:如果任务只是“稳定找一个局部极小”,你就没必要上测度松弛,用L-BFGS或CMA-ES更省事;如果评估预算极少,贝叶斯优化仍然是更优的起点;如果问题维度达到几百上千且目标相对光滑,那老老实实用零阶随机梯度类方法更合适。
选型建议一句话:如果你的黑箱函数“不规则但有规律”,并且你有几千次以上的评估预算,直接测度松弛是成本最低、最值得先试的baseline之一。
我个人的操作体会是,这类方法不是银弹,但它能在你手头没有梯度、没有代理模型、也不想引入一堆复杂假设时,用最简单的线性加权方式把“搜索”和“利用”揉进一个概率分布里。我学到的最有价值的东西,是遇到黑箱优化问题时先问自己:我到底想要一个点,还是想要一个能解释当前搜索状态的分布?很多生产场景里答案其实是后者——知道哪些参数区域可接受,比找到理论上的唯一最优值更实用。如果后面有机会,我把带约束的测度松弛(比如把约束也写成期望形式)和与神经网络预测器结合的离策略优化再整理出来。这篇就到这里,代码和思路都放上面了,欢迎拿去跑一跑。