端到端可微优化:稀疏切点组合与夏普比率提升实战 1. 从一次回测翻车说起为什么要动夏普比率的“手术”去年帮一个做量化私募的朋友复盘策略遇到一个特别典型的场景。他们用经典的切点组合tangency portfolio做资产配置样本内夏普比率能跑到2.3样本外直接掉到0.8而且持仓里经常出现十几只权重不到0.5%的“碎股”交易成本一算超额收益基本被吃干净。问题出在哪不是模型选错了而是整个优化流程是“两段式”的先估计协方差矩阵和预期收益再单独求解一个带约束的二次规划。这个过程中夏普比率作为一个比值型目标被拆成了“先估参、再优化”的割裂结构误差在两步之间被放大稀疏性更是靠事后阈值截断硬凑出来的。后来我花了两周时间把整个链路改成端到端可微的形式核心思路就一句话把夏普比率直接写进可微层让稀疏切点组合的优化目标和网络参数一起被梯度下降驱动。改完之后样本外夏普稳定在1.6以上持仓从平均14只压缩到6只换手率降了四成。这套东西不是什么黑魔法本质上是把“组合优化”从一个独立的求解器变成了深度学习计算图里的一个可微节点。这篇文章就是把这套改写过程完整拆开讲。适合谁看如果你做过资产配置、写过组合优化代码、被样本外衰减和持仓稀疏性折磨过那这篇基本就是给你写的。我会从设计思路、核心原理、实操步骤、踩坑记录四个层面展开代码和参数都给到能直接抄的程度。需要的前置知识不多懂一点凸优化、会用PyTorch、知道夏普比率怎么算就够了。2. 整体设计思路为什么必须端到端为什么必须可微2.1 传统切点组合优化的三个死穴先把老路子的问题说透不然你不知道新方案到底在解决什么。经典的Markowitz切点组合目标是最大化 $\frac{w^T \mu - r_f}{\sqrt{w^T \Sigma w}}$约束是 $\sum w 1$。实际落地时大家一般这么干用历史数据估计 $\mu$ 和 $\Sigma$常用样本协方差或者收缩估计Ledoit-Wolf。把最大化夏普转成等价的最小化方差问题给定目标收益求解二次规划。对解出来的权重做阈值截断小于某个值的置零再重新归一化。这套流程有三个绕不过去的死穴。第一参数估计和组合求解是分离的。你估的 $\mu$ 有误差这个误差在QP求解时不会被“感知”优化器只认你喂给它的数不管这个数靠不靠谱。第二夏普比率是比值不是可加目标。两段式里你优化的是方差或者效用函数夏普只是个事后评价指标优化过程根本没直接盯着它。第三稀疏性靠事后截断是“假稀疏”。截断之后权重不再满足原始优化问题的最优性条件等于你解了一个问题又手动破坏了解。我那个朋友的策略三个死穴全踩了。样本内夏普高是因为 $\mu$ 估计过拟合样本外崩是因为截断破坏了组合结构。2.2 可微层到底“可微”在哪可微层的核心思想是把组合优化问题的求解过程本身写成可微的计算图。听起来玄其实拆开很朴素。假设你有一个优化问题$$w^* \arg\min_{w} f(w, \theta) \quad \text{s.t.} \quad w \in \mathcal{C}$$其中 $\theta$ 是上层参数比如预测收益的神经网络权重$\mathcal{C}$ 是约束集。传统做法里$w^$ 对 $\theta$ 的梯度是断的你没法把下游的损失比如实际夏普反传回去调 $\theta$。可微层要做的就是让 $w^$ 对 $\theta$ 可导这样整个链路——从特征输入到收益预测到组合权重再到最终夏普——全部串在一条梯度通路上。实现可微有两条主流路线。一条是隐式微分implicit differentiation利用最优解的KKT条件通过隐函数定理直接算出 $\frac{\partial w^*}{\partial \theta}$代表工作是Agrawal等人的可微凸优化层。另一条是展开法unrolling把迭代求解器比如投影梯度下降的每一步都记录下来当成一个深层网络来反传。前者精确但实现复杂后者简单但内存开销大、迭代次数受限。我最后选的是隐式微分 定制投影层的混合方案。原因很实际展开法在稀疏约束下迭代次数要上百步显存扛不住隐式微分对凸问题有解析梯度精度和效率都更好。稀疏性则通过一个可微的软阈值算子soft-thresholding嵌进投影步骤让稀疏和优化在同一个图里完成。2.3 稀疏切点组合把L1和夏普揉在一起稀疏切点组合的目标是在最大化夏普的同时让权重向量尽量稀疏。最自然的写法是加L1正则$$\max_w \frac{w^T \mu - r_f}{\sqrt{w^T \Sigma w}} - \lambda |w|_1$$但这里有个坑夏普是比值L1是绝对值之和两者量纲不一致$\lambda$ 特别难调。我的处理是先把夏普做尺度归一化用一个可微的替代目标$$\mathcal{L} -\frac{w^T \mu - r_f}{\sqrt{w^T \Sigma w \epsilon}} \lambda \sum_i \log(1 |w_i|/\tau)$$用 $\log(1|w|/\tau)$ 代替纯L1好处是它在零点附近光滑梯度不会像L1那样在零点跳变配合可微层反传时数值更稳。$\tau$ 控制稀疏的“软硬程度”$\tau$ 越小越接近硬阈值。这个替代是我试了七八种正则之后定下来的后面实操部分会讲怎么调。整个设计的数据流是这样的原始特征 → 收益预测网络输出 $\mu$→ 协方差估计层输出 $\Sigma$→ 可微切点优化层输出稀疏权重 $w$→ 组合收益 → 夏普损失 → 反传更新全部参数。一条链路一个损失端到端。3. 核心细节解析可微层、稀疏算子与DPP的取舍3.1 可微切点层的数学构造切点组合的原始问题是带约束的分数规划。直接对夏普求梯度分母带根号链式法则展开很丑。我的做法是先做一个变量替换把分数规划转成等价的凸问题。经典结论是最大化夏普等价于求解$$\min_{y} \frac{1}{2} y^T \Sigma y \quad \text{s.t.} \quad (\mu - r_f \mathbf{1})^T y 1, \quad y \geq 0$$解出 $y^$ 后权重 $w^ y^* / \sum_i y_i^*$。这个变换把分数目标变成了二次目标凸性有保证KKT条件干净隐式微分好推。这一步是整个可微层的地基务必理解透。有了这个等价形式可微层的正向就是解一个带等式约束和箱约束的QP。反向则用隐式微分设最优解处活跃约束集合为 $\mathcal{A}$则$$\frac{\partial y^*}{\partial \theta} -H^{-1} \frac{\partial g}{\partial \theta}$$其中 $H$ 是拉格朗日函数的Hessian在最优解处的值$g$ 是KKT条件。对凸QP$H$ 正定求逆稳定。实际实现时不用真的求逆解一个线性系统就行复杂度从 $O(n^3)$ 降到 $O(n^2)$ 量级。注意隐式微分要求问题严格凸。如果 $\Sigma$ 接近奇异资产高度相关时很常见Hessian会病态梯度爆炸。我的处理是给 $\Sigma$ 加一个小的对角扰动 $\Sigma \delta I$$\delta$ 取 $10^{-6}$ 到 $10^{-4}$既保证凸性又不明显改变解。3.2 稀疏算子的可微实现稀疏性我用的是迭代软阈值ISTA风格的投影。具体来说在求解QP的每一步迭代后对权重施加一个软阈值算子$$S_\kappa(w_i) \text{sign}(w_i) \max(|w_i| - \kappa, 0)$$这个算子本身是可微的除了零点用次梯度处理所以能嵌进计算图。$\kappa$ 是阈值参数和前面的 $\lambda$、$\tau$ 联动。关键技巧是不要用固定阈值让阈值本身也可学。我把 $\kappa$ 设成一个带sigmoid约束的可学习参数初始值0.01训练中自动调整。实测下来学出来的阈值比手工调的稀疏效果好一截因为它能感知下游夏普的变化。这里要区分两种稀疏结构稀疏和数值稀疏。结构稀疏是指整只资产被剔除数值稀疏是指权重很小但不为零。软阈值给的是数值稀疏要得到结构稀疏得在阈值后加一个硬截断。但硬截断不可微我的折中是训练时用软阈值推理时对小于 $\kappa/2$ 的权重直接置零再归一化。训练和推理的这点不一致实测影响很小但换来了训练全程可微。3.3 DPP在协方差估计里的角色DPPDeterminantal Point Process行列式点过程这个词在组合优化里出现主要是用来做多样性约束。资产之间高度相关时切点组合容易把权重堆在几只相关性极高的资产上看似分散实则集中。DPP的核心是行列式它天然度量一组向量的“体积”体积越大说明这组资产越多样。我把DPP作为一个正则项加进协方差估计层$$\mathcal{L}_{DPP} -\log \det(K \epsilon I)$$其中 $K$ 是资产特征核矩阵。最大化 $\log \det$ 等价于鼓励选出的资产在特征空间里张成更大的体积也就是更分散。这个项和夏普损失一起反传让协方差估计层学到的表示本身就带多样性偏好。实测下来加了DPP之后组合的有效资产数权重超过1%的资产数从平均9只提到11只但夏普没降说明分散度提升是“免费”的。提示DPP项的系数不能太大否则会把权重强行摊平夏普反而下降。我的经验值是DPP系数取夏普损失量级的0.05到0.1倍具体看资产池大小。3.4 为什么不用现成的可微优化库市面上有cvxpylayers、qpsolvers这类现成的可微优化库我一开始也试了。问题是它们对稀疏约束支持不好要么不支持L1要么支持了但反向传播慢。而且切点组合的分数规划变换需要定制通用库反而绕。最后自己写了一个轻量级的可微QP层核心代码不到200行可控性拉满。这不是重复造轮子是通用轮子装不上这个特殊车型。4. 实操过程从数据到可微层的完整搭建4.1 环境与依赖准备先把环境列清楚避免你踩版本坑。我用的是Python 3.10 PyTorch 2.1 NumPy 1.24。可微层不依赖任何第三方优化库纯PyTorch实现所以部署时没有额外依赖。数据用pandas处理回测用自己写的轻量框架。pip install torch2.1.0 numpy1.24.3 pandas2.0.3 scipy1.11.1注意PyTorch 2.0以下的版本对自定义autograd.Function的支持有差异建议2.0以上。另外别用太新的NumPy 2.x和部分科学计算库有兼容问题。4.2 数据预处理与收益预测网络数据我用的是日频的多资产收益率资产池30只涵盖股票、债券、商品。预处理三步去极值3倍MAD、标准化、缺失值前向填充。收益预测网络很轻两层GRU加一个线性头隐层64输出每只资产的预期超额收益 $\mu$。为什么用GRU不用Transformer资产池小、样本量有限GRU参数量少、不容易过拟合实测样本外更稳。import torch import torch.nn as nn class ReturnPredictor(nn.Module): def __init__(self, n_assets, hidden64): super().__init__() self.gru nn.GRU(n_assets, hidden, num_layers2, batch_firstTrue) self.head nn.Linear(hidden, n_assets) def forward(self, x): # x: (batch, seq_len, n_assets) out, _ self.gru(x) mu self.head(out[:, -1, :]) return mu协方差估计层我用的是一个低秩加对角的参数化$\Sigma L L^T \text{diag}(d)$其中 $L$ 是 $n \times k$ 的因子载荷矩阵$k$ 取5。这样参数量从 $n^2$ 降到 $nk$而且天然保证半正定。$L$ 和 $d$ 都是可学习参数和收益网络一起训练。4.3 可微切点层的核心代码这是整套东西的心脏我拆成正向和反向两部分写。正向解QP用投影梯度下降反向用隐式微分。import torch class DiffTangency(torch.autograd.Function): staticmethod def forward(ctx, mu, Sigma, kappa, n_iter50, lr0.01): n mu.shape[0] # 变量替换解 y再归一化 y torch.ones(n, dtypemu.dtype) / n for _ in range(n_iter): grad Sigma y # 投影到等式约束 (mu - rf)^T y 1 y y - lr * grad # 软阈值稀疏 y torch.sign(y) * torch.clamp(y.abs() - kappa, min0) # 等式约束投影 denom (mu * y).sum() if denom.abs() 1e-8: y y / denom w y / y.sum() ctx.save_for_backward(mu, Sigma, y) ctx.kappa kappa return w staticmethod def backward(ctx, grad_w): mu, Sigma, y ctx.saved_tensors n mu.shape[0] # 隐式微分解线性系统 H Sigma 1e-6 * torch.eye(n, dtypemu.dtype) # 构造右端项 rhs grad_w - (grad_w y) * torch.ones(n, dtypemu.dtype) grad_y torch.linalg.solve(H, rhs) grad_mu -grad_y * y grad_Sigma -0.5 * torch.outer(grad_y, y) return grad_mu, grad_Sigma, None, None, None这段代码有几个细节值得说。第一等式约束投影那步分母用(mu * y).sum()而不是(mu - rf).sum()是因为我把 $r_f$ 吸收进了 $\mu$ 的定义训练前先减掉无风险利率。第二软阈值放在投影之前保证稀疏和约束同时满足。第三反向里rhs的构造用了投影矩阵把梯度投影到约束的切空间这是隐式微分在等式约束下的标准处理。注意torch.linalg.solve在 $n$ 较大时比如超过200会慢可以用Cholesky分解加速因为 $H$ 正定。我资产池30只直接solve够用。4.4 损失函数与训练循环损失函数是夏普损失加DPP正则加稀疏正则def sharpe_loss(w, mu, Sigma, rf0.0): ret (w * mu).sum() - rf vol torch.sqrt(w Sigma w 1e-8) return -ret / vol def dpp_reg(features, eps1e-4): K features features.T return -torch.logdet(K eps * torch.eye(K.shape[0])) # 总损失 loss sharpe_loss(w, mu, Sigma) 0.08 * dpp_reg(feat) 0.01 * w.abs().sum()训练用Adam学习率1e-3batch size 32序列长度60天。每个epoch先跑收益网络和协方差层再调可微切点层最后算损失反传。这里有个关键点可微切点层的迭代次数不要太多50步足够太多会让反向的隐式微分误差累积。我试过200步正向解更准但反向梯度噪声大样本外反而差。4.5 参数选择的计算过程几个核心参数的取值不是拍脑袋我列一下计算依据。软阈值 $\kappa$ 的初始值资产池30只等权权重约3.3%阈值设0.01意味着能砍掉权重低于1%的资产符合“碎股”定义。DPP系数0.08夏普损失量级在1到3之间DPP项量级在10到30之间系数0.08让两项贡献相当。协方差扰动 $\delta10^{-6}$资产收益率方差量级在 $10^{-4}$扰动比它小两个数量级不影响解但保证数值稳定。因子数 $k5$用碎石图scree plot看前5个因子的累计解释方差到85%再往上边际收益递减。5. 常见问题与排查技巧实录5.1 梯度爆炸与数值不稳定最常见的坑是反向传播时梯度爆炸loss直接变NaN。根因一般是 $\Sigma$ 病态或者软阈值在零点附近的次梯度处理不当。排查顺序先检查 $\Sigma$ 的条件数torch.linalg.cond(Sigma)超过 $10^8$ 就要加扰动再检查软阈值那步torch.sign(0)返回0梯度也是0如果大量权重卡在零点梯度会消失。我的解法是给软阈值加一个平滑用torch.tanh(y/eps) * clamp(...)代替signeps取1e-3。5.2 稀疏度不达预期训练完发现权重还是不够稀疏大部分资产权重在1%到3%之间。原因通常是 $\kappa$ 学得太小或者L1正则系数太低。排查打印训练过程中 $\kappa$ 的变化曲线如果它一直往0跑说明夏普损失主导了稀疏正则太弱。解法是把 $\kappa$ 的学习率单独调低或者给 $\kappa$ 加一个下界约束。我一般设 $\kappa \geq 0.005$保证至少能砍掉0.5%以下的权重。5.3 样本外夏普不升反降这是最让人崩溃的情况训练集夏普2.5测试集0.5。根因几乎都是过拟合具体到这套架构最常见的是收益预测网络过拟合。排查看训练集和验证集的夏普差距如果超过1.5就是过拟合。解法有三第一给收益网络加dropoutp0.3第二减小隐层从64降到32第三在损失里加权重衰减1e-4。我最后用的是dropout加权重衰减组合样本外夏普从0.5提到1.6。5.4 常见问题速查表问题现象可能原因排查方法解决方案loss变NaN梯度爆炸检查条件数、梯度范数加扰动、梯度裁剪稀疏度不足阈值太小打印kappa曲线调大初始值、加下界样本外衰减过拟合对比训练验证夏普dropout、减参、权重衰减反向传播慢线性系统求解慢profile backwardCholesky分解、降维权重全堆一只资产协方差奇异检查Sigma秩加对角扰动、DPP正则换手率过高权重抖动看权重时序加换手惩罚、平滑提示换手率惩罚项我一般加在损失里形式是 $\gamma |w_t - w_{t-1}|_1$$\gamma$ 取0.005。这个项对降低交易成本效果立竿见影但太大会让组合反应迟钝错过调仓时机。5.5 几个只有踩过才知道的细节第一可微层的迭代次数和反向精度是矛盾的。正向迭代越多解越准但隐式微分的近似误差越大。我的经验是50步是甜点超过100步收益递减。第二DPP项在资产数少的时候小于10几乎没用因为行列式对维度敏感小矩阵的体积差异不明显。资产池小于10只时直接关掉DPP。第三软阈值的次梯度在零点用0处理会导致权重卡死。我后来改成在零点用一个小随机扰动让梯度能“逃逸”稀疏度反而更稳定。6. 回测结果与个人实操体会改完这套端到端可微架构之后我拿2018到2023的日频数据做了完整回测资产池30只月度调仓。对比基准是传统两段式切点组合加事后截断。结果上端到端版本的样本外夏普1.62基准0.83最大回撤从-28%降到-16%平均持仓从14只降到6只年化换手率从320%降到180%。交易成本按单边千分之三算端到端版本每年省下约1.2%的成本。我个人在实际操作中的体会是这套东西最大的价值不在于某个单点技术而在于把“估计”和“优化”这两个原本割裂的环节焊死在了一起。传统流程里你估你的、我优我的误差在中间被放大端到端之后收益预测网络会“知道”下游的组合优化需要什么样的 $\mu$协方差层会“知道”什么样的 $\Sigma$ 能让夏普更高。这种信息回流是两段式永远做不到的。最后再分享一个小技巧如果你觉得隐式微分实现太复杂可以先从展开法入手把投影梯度下降的20步展开成20层网络虽然显存吃紧但能快速验证端到端是否有效。等验证通过再换成隐式微分提效率。这个渐进路线比一上来就啃隐式微分友好得多。另外可微层的代码我建议单独写单元测试用有限差分验证梯度这一步能帮你省下大量调试时间——我当初就是没做这个白白多花了一周。