简介面向电力系统与机器学习优化领域研究者的分布鲁棒优化项目代码针对风光出力、负荷波动等数据分布不精确或不完整场景适合希望将理论模型落地为可运行示例的开发者与工程师。资源包共4个文件包含Python实现脚本、依赖说明文本、交互式运行配置以及HTML可视化页面压缩包整体仅6KB结构紧凑但覆盖了从模型构建到求解的关键环节目前已有113人学习。代码展示了Wasserstein球半径的计算、不确定性变量样本的标准化处理以及利用数学工具将分布鲁棒约束转换为易求解形式的具体做法。通过可运行示例读者能直观理解推土机距离在构建模糊集时的几何意义并辨别不同变量环境与研究假设下的转换差异对电网运行规划、储能管理及可再生能源集成的学习者而言这份项目代码提供了清晰算法演示和二次开发基础有助于快速上手分布鲁棒优化实践。1. 分布偏移面前普通训练为什么不够硬Wasserstein DRO 在解决什么问题训练集上表现良好的模型一旦数据采集环境发生改变效果往往会突然滑落甚至不如一份简单规则脚本。这种问题通常不是模型复杂度的锅而是训练目标本身只对“当前这批数据”负责没有为分布偏移留出预算。分布鲁棒优化DRO换了一个思路把训练集的经验分布看作圆心用 Wasserstein 距离画一个半径为 eps 的球把球内所有“长得像但可能不一样”的分布都当成潜在的测试分布要求模型在最坏的那种分布上依然可控。Wasserstein 距离在这里不是论文里的抽象概念它是“把一个分布变成另一个分布需要多少搬运成本”的可计算度量也是把 DRO 从 min-max 博弈落到可训练目标的关键。这篇笔记面向做机器学习落地的工程师先讲清楚为什么用 Wasserstein 球而不是 KL 散度再给一套基于 PyTorch 的最小可复现代码最后是我调参时踩过的五个坑。不需要提前懂最优传输的整套理论读完后能自己跑起来就行。2. Wasserstein 球与对偶形式想做鲁棒优化先搞清这三个关键点Wasserstein DRO 的理论看起来唬人落到工程上其实只需要把三件事搞清楚Wasserstein 距离该怎么算、DRO 的对偶形式长什么样、模糊集半径 eps 和成本函数怎么选。这一章按这个顺序拆全部指向第三、四章的代码与调参。2.1 为什么不用 KL 散度Wasserstein 球能容忍支撑集移动Wasserstein 距离又被称为推土机距离。把分布 P 看成一堆土分布 Q 看成另一堆土Wasserstein 距离就是把 P 挪成 Q 需要的最小运输成本公式写出来就是$W_p(P,Q)\left(\inf_{\pi\in\Pi(P,Q)}\mathbb{E}_{(X,Y)\sim\pi}|X-Y|^p\right)^{1/p}$其中 $\Pi(P,Q)$ 是所有以 P 为边缘、Q 为另一个边缘的联合分布也就是所有“怎么把土从 P 搬到 Q”的运输方案。当 p1 时有一个特别好用的对偶形式$W_1(P,Q)\sup_{f:|f|{\mathrm{Lip}}\le1}\left(\mathbb{E}{P}[f(X)]-\mathbb{E}_{Q}[f(X)]\right)$这个对偶形式的意思是如果函数 f 是 1-Lipschitz 的两个分布的 W1 距离等于它们在 f 上期望差的最大值。后面 DRO 对偶公式的推导起点就在这里。那为什么 DRO 的模糊集要用 Wasserstein 球而不是 KL 散度球因为 KL 对“支撑集不重叠”的分布直接给无穷大。所谓支撑集就是分布取值不为零的区域。训练数据里 x 分布在 [0,1]线上数据因为采集设备换了量程x 集中在 [3,4]两个分布的支撑集几乎不重叠KL 直接发散优化目标根本不知道该往哪个方向修。而 Wasserstein 距离对这类整体平移给出线性增长的成本搬到 [3,4] 区域需要付出 3 个单位的搬运费。现实中均值平移恰恰是最常见的分布偏移形态所以 KL 散度天然不适合做这类鲁棒优化Wasserstein 距离更贴合实际。2.2 从 min-max 到可计算的罚项对偶形式与 Lipschitz 正则DRO 的原始目标是一个 min-max 问题模型参数在最小化模糊集里的分布却要在最大化损失$\min_{\theta}\sup_{Q:W(Q,\hat{P}n)\le\varepsilon}\mathbb{E}{Q}[\ell(Z;\theta)]$直接解这个双层优化不现实工程上靠的是拉格朗日对偶。把 Wasserstein 球约束写成带拉格朗日乘子 lambda 的形式后目标变成$\min_{\theta}\min_{\lambda\ge0}\left{\lambda\varepsilon\frac{1}{n}\sum_{i1}^{n}\sup_{Z}\left[\ell(Z;\theta)-\lambda c(Z,Z_i)\right]\right}$lambda 可以理解成“每单位运输成本的价格”$\sup_Z$ 那一项的物理含义是固定 theta 和 lambda 时把单个样本 $Z_i$ 扰动到 Z 能带来的最大净收益也就是模型损失的增量减去运费。整个式子把“不知道哪个分布最坏”转化成了“对每个样本找最坏扰动”这就是能写进代码的形式。再往下走一步当损失函数关于 Z 是 L-Lipschitz 时有一个工程上常用的近似上界$\sup_{Q:W(Q,\hat{P}n)\le\varepsilon}\mathbb{E}{Q}[\ell];\approx;\frac{1}{n}\sum_{i}\ell(Z_i;\theta)\varepsilon L_\theta$也就是说Wasserstein DRO 约等于在普通经验风险最小化后面加一个 Lipschitz 正则项。这个结论解释了为什么很多 DRO 实现看起来和对抗训练一模一样限制每个样本在半径内找最坏扰动本质上就是在近似优化这个上界。第四章的代码正是按这个思路写的不需要真正求解一个分布空间上的优化问题。2.3 预算怎么分同一半径在不同数据尺度下根本不是同一个鲁棒水平eps 的含义必须落在数据尺度上理解。Wasserstein 球半径是“平均运输成本预算”如果特征做了 z-score 标准化每列均值 0 方差 1那么 eps0.1 表示允许每个样本平均移动 0.1 个标准差。相同的 eps0.1放在像素值 0-255 的图像数据上几乎没有约束力放在量级 1e-4 的物理特征上又把模型压得完全动弹不得。这一步不处理后面全是玄学调参。我一般会先把特征标准化再把 eps 设成训练集内中位成对距离的 1% 到 10%然后按验证集表现微调。成本函数决定扰动形状也决定内循环里用哪个范数对应关系如下成本函数 $c(z,z)$对偶范数扰动形状适用场景$|z-z|_2$$|\cdot|_2$各方向均匀的球形特征间有相关性时的默认选项$|z-z|_1$$|\cdot|_\infty$逐坐标稀疏扰动单个特征单独坏掉或缺失型偏移$(z-z)^T M (z-z)$$M^{-1}$ 范数椭球已知特征相关结构时第三行的 Mahalanobis 成本在工程上一般等价于“先白化再用 L2 成本”这样处理后特征间相关性就不会污染扰动方向。第二章的表格放到后面避坑章节会反复用到先留个印象即可。3. 用 PyTorch 从零实现 Wasserstein DRO 训练器内循环与外循环的拆解这一章直接给出可以复制运行的最小实现。任务是线性回归特征维度 5训练样本 1000这样可以在一分钟内在 CPU 上看到完整效果。整体结构分三部分数据与模型定义、内循环构造最坏扰动、外循环更新参数。3.1 最小实现构造最坏扰动内循环与参数更新外循环import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader torch.manual_seed(0) n, d 1000, 5 x torch.randn(n, d) w_true torch.randn(d) y x w_true 0.1 * torch.randn(n) class LinearModel(nn.Module): def __init__(self, dim): super().__init__() self.fc nn.Linear(dim, 1) def forward(self, z): return self.fc(z).squeeze() def make_loader(x, y, bs128): ds TensorDataset(x, y) return DataLoader(ds, batch_sizebs, shuffleTrue)数据是合成的线性回归任务真值权重随机生成噪声 0.1。用线性任务是为了便于对比真实场景下把 x、y 换成自己的特征矩阵即可。训练入口函数如下def wasserstein_adv_step(model, x, y, eps, lam, n_inner, lr_adv): 在Wasserstein球内为每个样本构造最坏扰动。 eps: 球半径平均运输成本预算 lam: 运输成本价格权衡“增加损失”和“省运费” n_inner: 梯度上升步数 delta torch.zeros_like(x, requires_gradTrue) for _ in range(n_inner): adv_loss model(x delta).squeeze().mse_loss(y) - lam * delta.norm(dim1).mean() grads torch.autograd.grad(adv_loss, delta, create_graphFalse)[0] with torch.no_grad(): delta (delta lr_adv * grads).detach().requires_grad_(True) # 投影回半径范数超过eps的直接缩放到边界属于“预算均分”近似 with torch.no_grad(): scale torch.clamp(eps / (delta.norm(dim1, keepdimTrue) 1e-12), max1.0) delta (delta * scale).detach().requires_grad_(True) return delta.detach() def train_wdro(model, loader, eps, lam, n_inner3, lr_adv0.05, lr_model1e-2, epochs30, use_advTrue): opt torch.optim.Adam(model.parameters(), lrlr_model) for epoch in range(epochs): for xb, yb in loader: delta (wasserstein_adv_step(model, xb, yb, eps, lam, n_inner, lr_adv) if use_adv else 0) loss model(xb delta).squeeze().mse_loss(yb) opt.zero_grad() loss.backward() opt.step()内循环里 adv_loss 的第一项是模型在扰动样本上的 MSE 损失梯度上升会推动扰动向“让模型更错”的方向走第二项扣掉运输成本lam 越大扰动越舍不得走远。每步更新后按半径投影保证扰动不飞出 Wasserstein 球。每步更新前的 detach 是刻意的如果保留整条计算图内循环步数多时会累积 n_inner 层的图外循环虽然不回传但显存和速度都会被拖垮。提示lam 一般取 0.1 到 10n_inner 取 2 到 5 就够。lr_adv 不要给大建议是模型学习率的 1/5 到 1/10否则内循环会震荡。3.2 与普通 ERM 对比在同一个分布偏移测试集上验证鲁棒性def evaluate(model, x, y): model.eval() with torch.no_grad(): return model(x).squeeze().mse_loss(y).item() model_erm LinearModel(d) train_wdro(model_erm, make_loader(x, y), eps0.0, use_advFalse) model_dro LinearModel(d) train_wdro(model_dro, make_loader(x, y), eps0.1, lam1.0, n_inner3, lr_adv0.05, use_advTrue) x_test torch.randn(200, d) y_test x_test w_true 0.1 * torch.randn(200) x_shift x_test * 1.5 0.5 # 均值平移 方差放大模拟“换了采集环境” print(fERM clean: {evaluate(model_erm, x_test, y_test):.4f} shifted: {evaluate(model_erm, x_shift, y_test):.4f}) print(fDRO clean: {evaluate(model_dro, x_test, y_test):.4f} shifted: {evaluate(model_dro, x_shift, y_test):.4f})不同随机种子下具体数值会变但相对趋势稳定ERM 在 clean 测试集上略好但 shifted 集上掉点明显Wasserstein DRO 的 clean 损失小幅上升shifted 损失显著更稳。这个一升一降正是鲁棒优化的本质拿一点平均性能换最坏情况的保障。如果跑出来 shift 和 clean 几乎一样大概率是 eps 没生效回看 2.3 的尺度问题。3.3 两处工程近似逐样本均分预算与有限步内循环代码里有两处近似需要知道边界。第一处是预算均分真正的 Wasserstein 球约束的是平均运输成本最优分配会把预算倾斜给“最危险”的样本而逐样本统一半径会偏保守让所有样本都只能小幅度扰动。当数据里明确存在“少数样本极危险”的形态时可以在 batch 上做全局投影取代逐样本投影代价是代码复杂度上升。第二处是有限步内循环n_inner3 算出的不是精确 sup但对抗训练的经验表明扰动方向比扰动大小重要哪怕只走两三步就能提供有效信号。如果内循环震荡优先调小 lr_adv 而不是加步数后者只会让扰动在球内部来回旋转。这套近似对线性模型和神经网络都适用是 DRO 从论文走向工程最常用的路径。4. 避坑Wasserstein DRO 训练中最容易翻车的五个细节下面五个坑按出现频率排序前两个几乎每个第一次做 DRO 的人都会踩后三个看具体场景。它们的共同点大多不是理论问题而是工程尺度问题。能避开这五个基本就避开了整套方案的绝大多数翻车现场。4.1 现象eps 调了半天没效果模型加了 DRO 之后shifted 和 clean 的指标跟普通 ERM 几乎一模一样eps 从 1e-3 试到 1.0 都没有反映。原因基本是数据没有标准化eps 的绝对量级与特征尺度不匹配或者 eps 实在太小模糊集只覆盖了经验分布附近极小的范围最坏情况分布和原分布的区别可以忽略。解决方法是先把特征按列做 z-score再把 eps 设成训练集内中位成对距离的 1% 到 10% 去扫。验证方式很简单训练完成后打印实际扰动的平均范数它应该约等于 eps否则说明内循环没有真正在球内工作。4.2 现象换了数据集结果天差地别同一套代码在 A 数据集上有效换到 B 数据集上完全失效。原因是 Wasserstein 球对度量尺度非常敏感特征里如果混入了一列量级特别大的高方差列L2 范数会被这一列主导扰动预算全部花在它身上其他特征一点没动。这种“大尺度列劫持扰动”在真实业务特征里很常见尤其是 ID 类或计数类特征。解决方法是逐列标准化后再进入训练或者改用 Mahalanobis 成本在成本层面把特征相关性考虑进去。4.3 现象内循环震荡模型不收敛训练 loss 出现明显跳动甚至比 ERM 还高。原因通常是 lr_adv 过大梯度上升在非光滑的超平面上来回弹或者 n_inner 太大扰动在球内部反复旋转始终不稳定。解决方法是把 lr_adv 降到模型学习率的 1/10 左右n_inner 限制在 5 以内同时检查内循环里是否做了 detach 截断。还有个容易被忽略的点投影时用 clamp 把范数缩放到边界而不是直接裁到 [-eps, eps]后者对 L2 成本来说是错的会把扰动方向截成方形的角。4.4 现象加了 DRO 后干净样本掉点太多shifted 集确实稳住了但 clean 集崩得心疼。原因基本是 eps 给得过大模糊集里包含了过于离谱的分布模型被逼得过度保守。解决方法是把 eps 当作超参数在验证集上画出“clean 损失随 eps 上升、shift 损失随 eps 下降”的权衡曲线选拐点而不是选 shift 最优。要注意 DRO 本质上就是拿平均性能换最坏情况保障掉点不可避免。如果 10% 的 clean 掉点能换一半的 shift 掉点这笔账通常划算如果掉点更多说明 eps 越界了。4.5 现象对偶范数选错鲁棒性只在某个方向有效模型在一种偏移下很稳换一种偏移立刻失效。原因是成本函数和扰动形状必须匹配业务偏移的形态L2 成本给所有特征均匀的小扰动适合“整体漂移”场景但实际业务里如果只是单个传感器坏了少数特征出现坏值L1 成本加无穷范数的稀疏扰动才更贴合。解决方法是先判断业务偏移是整体漂移还是个别特征坏值再按 2.3 的表格选成本。不确定时默认用 L2不要一上来就上 L1后续的优化器调参会更顺。5. 进阶压力测试与 epsilon 选择验证你的 DRO 真的有用5.1 列一张偏移矩阵而不是只看一个测试集单靠一个 shift 测试集说明不了问题我习惯构造一组偏移矩阵来验证 DRO 的鲁棒边界偏移类型构造方式DRO 是否典型有效均值平移x_shift x mu有效W1 成本线性反映方差缩放x_shift x * s部分有效需成本匹配相关性漂移对特征做线性变换Mahalanobis 成本更有效标签扰动随机翻转部分 y基本无效应处理标签本身这个表告诉我DRO 不是万能药它擅长的是特征空间的分布偏移对标签噪声基本无能为力。压力测试选对了才能知道 eps 该往哪个方向调。5.2 选 epsilon 的习惯扫曲线找拐点epss [0.01, 0.05, 0.1, 0.3, 1.0] for e in epss: m LinearModel(d) train_wdro(m, make_loader(x, y), epse, lam1.0, n_inner3, lr_adv0.05, use_advTrue) print(e, evaluate(m, x_test, y_test), evaluate(m, x_shift, y_test))我一般会在小验证集上扫这五个值把 clean 和 shifted 的损失分别记录选弧线拐点对应的 eps。宁可小幅起步因为 DRO 没有后悔药eps 一旦给大模型会变得非常钝连训练集都拟合不动事后缩小 eps 还得重新训练一遍。5.3 还能往前走的两步基础版本跑通后值得尝试的方向有两个一是把 W1 换成 W2同时对 x 和 y 一起扰动覆盖标签漂移的场景二是把 Wasserstein DRO 和域泛化的表示学习结合在特征提取层后面加上这个鲁棒目标而不是只用在最后一层。我自己第一版也把 eps 当绝对尺度设结果 clean 崩了、shift 一点没救回来排查半天发现就是数据没标准化。这类问题回头看全是工程细节跟理论难度无关。先承认这一点再动手写内循环比啃公式快得多。希望帮到你。本文还有配套的精品资源点击获取