做了几年多因子选股,我一直被一个矛盾卡着:模型在预测收益上做得不错,可换成组合权重之后,夏普常常不尽如人意。后来我试着把夏普比率直接写进可微层,用稀疏切点组合优化把整条链路改写成端到端训练,这才算把这个矛盾摆在桌面上解决。
先说结论:这套做法最大的价值,不是省掉中间步骤,而是让组合优化器本身开始根据夏普反馈调整自己的输入。如果你也在做量化组合、资产配置或者多因子策略,并且受够了预测模型和组合优化器各管各的,这篇文章应该能帮你少走不少弯路。下面我会从原理、代码、训练细节到踩坑记录,完整拆一遍。
1. 从两阶段到端到端:我为什么要折腾可微层
1.1 两阶段流程的三宗罪
大多数人做组合优化,用的是教科书里经典的两阶段流程:先用机器学习模型预测下一期收益和协方差,再把预测结果丢进均值-方差优化器或切点组合求解器,得到最终权重。这个流程本身没毛病,但实际跑久了,会明显感觉到三个问题。
第一,目标不一致。预测模型训练时用的是 MSE 或分类准确率,而组合优化真正关心的是夏普比率、最大回撤、换手率。预测误差在非线性优化过程中会被放大,收益预测的小偏差可能导致权重剧烈变化。一个在验证集上 RMSE 很漂亮的收益预测模型,换成组合权重之后表现平庸,甚至不如简单的等权组合,这种场景我见过太多次。
第二,误差无法回传。组合优化器是一个独立的求解步骤,它内部没有梯度通路。哪怕你发现组合夏普很差,也没办法计算“优化器对预测模型参数应该做什么修改”。于是所有调参都只能靠人工在外部试,比如改收益预测的窗口、调整风险厌恶系数、加各种正则项。整个过程又慢又靠手感。
第三,正则化参数难调。为了不让优化器产生极端权重,大家通常会加很多约束,比如单只股票权重上限、行业中性化、换手惩罚等。约束一多,系统复杂度就上去了,每个参数都得手动搜索,组合表现对某个约束特别敏感时,整个策略会变得极其脆弱。
对比之下,端到端训练是直接以组合层面的目标来驱动整个模型。组合优化器不再是一个不可干预的黑盒,而是一个可以被梯度穿过的可微层。这两者的差别,相当于“先把零件各自打磨好再组装”和“整机一起联调”的差别。自动驾驶里的端到端方案也是同一个思路:与其分模块堆感知、预测、规划,不如让最终行驶表现直接影响中间模块参数。
1.2 优化器变成可微层,意味着什么
所谓可微层,本质上就是神经网络里一个特殊的算子:前向传播时,它接收输入参数,求解一个优化问题,输出解作为结果;反向传播时,它根据优化问题的最优性条件,把损失对解的梯度传回给输入参数。
放到组合优化场景里,输入参数就是预测出来的收益期望 μ 和协方差 Σ,输出就是组合权重 w。如果这个优化问题是凸的,那么当它的解对参数连续变化时,梯度可以通过隐函数定理或者 KKT 条件解析地算出来。这个技术叫可微凸优化层,cvxpylayers 这类库就是专门干这个的。
把优化器变成一层之后,整个训练流程就变成了一个大的计算图:特征 → 编码网络 → μ、Σ → 可微优化层 → 权重 → 组合收益序列 → 夏普损失 → 反向传播。每一环都参与训练,损失函数可以精确地告诉特征编码器:“你输出的 μ 让权重偏向低夏普方向,需要调整。”
我第一次跑通这个图的时候,很直观的感受是:之前的模型是“模型自己在猜正确答案”,现在变成了“模型在帮着组合一起找正确答案”,两者的搜索空间完全不同。
1.3 谁适合用这套方案
这套方案适合三种人。第一种是做多因子选股的研究员,手里已经有因子和收益预测模型,但苦于优化环节拖后腿。第二种是做资产配置的,需要周期性输出一篮子资产权重,并且在意换手和持仓数量。第三种是刚接触深度学习加组合优化方向的量化工程师,想在自己的系统里引入端到端框架,但不知道怎么把优化器塞进网络。
当然,基础门槛也有。你得熟悉 PyTorch 的基本用法,知道凸优化是什么,对夏普比率、协方差这些概念不能太陌生。如果你只会调参,不会写模型,那建议先拿小型组合做个验证,再往生产环境迁移。
2. 稀疏切点组合优化到底是什么
2.1 夏普比率、切点组合和有效前沿
夏普比率是组合超额收益除以组合波动率,衡量每承担一单位风险能换来多少超额回报。把夏普比率最大化,得到的组合在有效前沿上有一个专门的名字,叫切点组合。它和无风险收益率组合之后,可以构成资本配置线上的最优组合。
如果预期收益向量是 μ,协方差矩阵是 Σ,无风险利率是 r_f,那么无约束情况下切点组合的权重可以写成:
w_tangency = Σ^{-1}(μ - r_f 1) / (1^T Σ^{-1}(μ - r_f 1))
这个公式很多教科书都写过,但实际应用有个问题:它默认可以做空,而且权重可能非常极端,某个资产权重是正 300%,另一个是负 250%,这种结果在实盘里根本没法用。于是我们会加约束:权重和为 1,权重大于等于 0,甚至给单资产设上限。
加了约束之后,切点组合不再有一个简单的解析式,而是一个需要数值求解的优化问题。而一旦我们想把稀疏性加进去,这个问题会变得更复杂。这也是为什么我建议把优化器做成可微层来求解,而不是人肉推公式。
2.2 为什么要加稀疏约束
金融世界里有一个很实际的诉求:组合里不要装太多资产。持仓数量越多,交易成本越高,冲击成本越难控制,投后管理也越麻烦。比如你选出 500 只股票的收益预测,如果优化器全部拿去配权重,哪怕单只权重很小,买卖一遍的费用也会吃掉不少超额收益。与其这样,不如只选其中最有把握的 20 到 30 只。
稀疏切点组合优化,就是要在保留切点组合风险收益性质的同时,让权重尽量稀疏。这里“稀疏”有两个方向可以理解:一个是硬约束,比如要求非零权重数量不超过 K;另一个是软约束,比如在目标函数里加 L1 正则项,让权重朝 0 收缩。
L0 范数直接约束持仓数量,但不可导,不适合放进优化器求梯度。L1 范数是 L0 的凸松弛,既能让权重变稀疏,又能保持整个优化问题是凸的,放在可微层里很合适。实际落地时,我会用“L1 惩罚 + 权重非负 + 权重和等于 1”的组合方式,这样既能控制持仓数量,又不会出现空头头寸。
2.3 理论层好写,落地层要另想办法
理论上的稀疏切点组合问题,写成数学形式很简单:
max w^T μ - λ ||w||_1 s.t. w^T Σ w ≤ σ^2, 1^T w = 1, w ≥ 0
或者直接写成均值-方差加 L1 的形式:
max w^T μ - λ ||w||_1 - (γ/2) w^T Σ w s.t. 1^T w = 1, w ≥ 0
问题是怎么把它放进深度学习框架。普通求解器只负责给解,不负责给梯度。你不能指望模型训练时,求解器自动告诉你“μ 该往哪个方向调”。这就是可微层存在的意义,它把“求解”和“求梯度”一起解决了。但要注意,并不是随便什么优化问题都能做可微层,前向问题必须是凸的,并且最好能被标准锥规划求解器识别。所以我们的目标函数设计,要优先考虑凸性,而不是纯粹地追求最大化夏普这个非凸目标。
3. 把夏普比率写进可微层的设计思路
3.1 为什么不能把最大化夏普直接扔进凸优化层
看到标题,你可能会想:既然目标是最大化夏普,那为什么不直接在可微层里求解一个最大化夏普的问题,比如:
max (μ - r_f 1)^T w / sqrt(w^T Σ w)
这个想法很自然,但它不是凸优化。因为目标函数是一个分式,分子是线性的,分母是二次根号,整个比例函数在组合空间里既非凹也非凸。cvxpylayers 和大多数可微凸优化库都要求问题满足 DCP 规则,这种形式大概率过不了 DCP 校验。
有人会通过 Charnes-Cooper 变换把最大化夏普转换成 SOCP 问题,这在一定条件下是可行的。但一旦再加上 L1 稀疏惩罚,变换会变得非常绕,求解稳定性和梯度质量都很难保证。我在实验早期试过这条路,调试成本很高,最后放弃了。
更务实的做法是:在优化层内部我们不直接最大化夏普,而是用一个“代理目标”,外层再用真实夏普作为损失函数。代理目标就是前面提到的均值-方差加 L1 的凸优化问题。它虽然不是夏普本身,但在风险厌恶系数 γ 的调节下,它的无约束解方向与切点组合一致。加上 L1 后,输出就成了稀疏切点组合的数值近似。
3.2 外层夏普损失,内层凸代理:端到端的真正写法
我最后采用的端到端架构长这样:
特征向量 x → 编码网络 → 输出 μ 和 Σ → 进入凸优化可微层 → 得到稀疏权重 w → 用 w 构造组合收益序列 → 计算组合夏普 → 作为损失函数 → 反向传播。
可微层内部实际求解的问题是:
max w^T μ - λ ||w||_1 - (γ/2) w^T Σ w s.t. 1^T w = 1, w ≥ 0
其中 μ 和 Σ 是网络输出,λ 和 γ 可以是超参数,也可以是可学习参数。外层损失函数就是负的样本内/样本外夏普,也就是把夏普比率真正“写进”了可微优化层的训练信号里。
这个架构的好处是,梯度可以从夏普损失一路传回编码网络。因为可微层根据 KKT 条件提供了优化层输出的梯度,所以编码网络能学习到“什么样的 μ 和 Σ 会让组合更接近最优夏普”。这比两阶段方法里“预测得准”要直接得多。
3.3 梯度是怎么传回来的
关于梯度回传,很多人会担心不可导问题。实际上,对于一个参数化的凸优化问题,最优解 w* 是参数 θ(这里是 μ、Σ、λ、γ)的隐函数。我们写出 KKT 条件,再对参数求导,可以得到一个线性系统,解出 dw*/dθ。这个矩阵的规模取决于变量和约束的数量,通常可以用伴随法求解,计算量不大。
cvxpylayers 把这套求导封装好了。你只要定义好 cvxpy 的优化问题,注明哪些是参数,哪些是变量,它就会在反向传播时自动计算梯度。它不是用数值差分,而是基于锥规划求解器返回的雅可比信息,所以梯度精度很高。
不过有个坑:L1 项在 w_i = 0 处不可导。实际处理中,次梯度或近似梯度可以工作,但如果很多权重被稀疏到 0,梯度中关于 λ 的路径会变得不稳定。这也是我建议 λ 在初期不要作为可学习参数的原因,先固定它,等模型结构稳定了再考虑放开。
4. 手写一个稀疏切点可微层:代码实战
4.1 环境依赖与问题定义
先说明一下我用到的环境:Python 3.10,PyTorch 2.1,cvxpy 1.4,cvxpylayers 0.1.5。安装直接用 pip 就能搞定。
pip install cvxpy cvxpylayerscvxpylayers 会依赖 cvxpy 和若干求解器,默认建议把 SCS 和 CLARABEL 装好,因为它内部求导依赖这些锥规划求解器。
我们的资产数量先设置成 20 只。需要定义四个参数:预测收益 μ、协方差 Σ、稀疏系数 λ、风险厌恶系数 γ。还需要定义一个变量 w,也就是最终组合权重。
4.2 用cvxpylayers实现可微层
下面这个代码片段就是核心可微层的定义:
import cvxpy as cp import torch from cvxpylayers.torch import CvxpyLayer n = 20 mu = cp.Parameter(n) Sigma = cp.Parameter((n, n), PSD=True) lam = cp.Parameter(nonneg=True) gamma = cp.Parameter(nonneg=True) w = cp.Variable(n) objective = cp.Maximize( mu @ w - lam * cp.norm(w, 1) - gamma / 2 * cp.quad_form(w, Sigma) ) constraints = [cp.sum(w) == 1, w >= 0] problem = cp.Problem(objective, constraints) assert problem.is_dcp() layer = CvxpyLayer( problem, parameters=[mu, Sigma, lam, gamma], variables=[w] )这里有三个细节值得注意。
第一个,Sigma必须声明为PSD=True,否则求解器不会把它当半正定矩阵,cp.quad_form(w, Sigma)的凸性校验会出问题。第二个,lam和gamma都声明为nonneg=True,这样能保证目标函数是凹函数且优化问题符合 DCP。第三个,CvxpyLayer的参数顺序必须和parameters列表顺序一致,后面调用时也要保持同样的顺序。
定义好之后,前向和反向都非常直接:
mu_t = torch.randn(n, requires_grad=True, dtype=torch.double) A = torch.randn(n, n, dtype=torch.double) Sigma_t = A @ A.T + torch.eye(n) * 1e-4 lam_t = torch.tensor(0.1, requires_grad=True, dtype=torch.double) gamma_t = torch.tensor(5.0, requires_grad=True, dtype=torch.double) w_t, = layer(mu_t, Sigma_t, lam_t, gamma_t) portfolio_return = (mu_t * w_t).sum() portfolio_vol = torch.sqrt(w_t @ Sigma_t @ w_t) sharpe = portfolio_return / portfolio_vol loss = -sharpe loss.backward()我一般会强制使用torch.double类型,因为 float32 在求解高维协方差问题时精度不够,容易导致梯度偏差。如果模型本身是 float32,可以把可微层内部单独转成 double,算完再转回去。
4.3 把可微层接进PyTorch网络
可微层本身不是一个torch.nn.Module,所以我会包一层,方便放进网络里使用:
class SparseTangencyLayer(torch.nn.Module): def __init__(self, n_assets): super().__init__() self.n_assets = n_assets mu = cp.Parameter(n_assets) Sigma = cp.Parameter((n_assets, n_assets), PSD=True) lam = cp.Parameter(nonneg=True) gamma = cp.Parameter(nonneg=True) w = cp.Variable(n_assets) objective = cp.Maximize( mu @ w - lam * cp.norm(w, 1) - gamma * cp.quad_form(w, Sigma) / 2 ) constraints = [cp.sum(w) == 1, w >= 0] self.layer = CvxpyLayer( cp.Problem(objective, constraints), parameters=[mu, Sigma, lam, gamma], variables=[w] ) def forward(self, mu, sigma, lam, gamma): return self.layer(mu, sigma, lam, gamma)[0]然后前面的编码网络可以直接输出 mu 和一个低秩因子,用来合成协方差矩阵:
class Encoder(torch.nn.Module): def __init__(self, input_dim, n_assets, hidden_dim=64): super().__init__() self.n_assets = n_assets self.fc1 = torch.nn.Linear(input_dim, hidden_dim) self.fc_mu = torch.nn.Linear(hidden_dim, n_assets) self.fc_A = torch.nn.Linear(hidden_dim, n_assets * n_assets) def forward(self, x): h = torch.relu(self.fc1(x)) mu = self.fc_mu(h) A = self.fc_A(h).view(-1, self.n_assets, self.n_assets) sigma = A @ A.transpose(1, 2) + torch.eye(self.n_assets) * 1e-4 return mu, sigma这里我用了低秩分解来保证协方差矩阵一定是对称半正定,这是这个框架里最容易踩坑的地方。如果直接让网络输出一个 n×n 矩阵,大概率不是半正定,求解器会直接报错或者给出无效解。多一步A @ A.T + εI,问题就没了。
5. 训练端到端模型的关键节点
5.1 数据准备与特征构造
训练这种端到端模型,数据准备与普通 ML 有很大区别。不能简单地把所有样本混在一起随机打乱,因为金融数据有时间序列属性,否则会引入前视偏差。
我用的数据是截面日频数据,主要包括动量、估值、质量、波动率等标准化因子。每一时刻 t,一组 N 只股票的因子矩阵是 X_t,下一期收益向量是 r_{t+1}。模型输入 X_t,输出下一期的组合权重 w_t,然后用 r_{t+1} 计算这期的组合收益。滚动窗口训练,训练集、验证集、测试集严格按时间顺序切分。
还要注意特征标准化。因子横截面的 Z-Score 很常用,但 Z-Score 的均值和标准差只能用当时已经发生的数据计算,不能使用未来数据。我一般会先用滚动窗口计算均值和标准差,再对当期特征做标准化。
协方差矩阵的估计也很关键。网络输出的协方差不是用历史数据直接算,而是要经过低秩分解保证正定。不过在实际实验中,我建议用两种协方差做对比:一种是网络输出的动态协方差,一种是历史样本协方差,看看可微层到底靠收益 μ 的改进多,还是靠协方差 Σ 的改进多。很多时候你会发现,收益预测端的改进更显著。
5.2 模型结构和超参数设置
模型结构不需要太复杂,我用的是一个两层的编码网络,隐藏单元 64 或者 128。激活函数用 GELU,因为它在训练早期比 ReLU 更平滑,能减少优化层的梯度冲击。
损失函数是负夏普:
loss = - (mean(R_p) / std(R_p) + eps) * sqrt(252)
其中 R_p 是组合在一个训练批次里的日收益序列。加 eps 是为了防止波动率接近 0 时除零。计算夏普时用哪个频率的平方根取决于收益序列的频率,日频就乘 sqrt(252)。
超参数方面,我常用的初始值是:
- Adam 优化器,学习率 1e-3
- λ 初始值 0.1
- γ 初始值 5.0
- 梯度裁剪 max grad norm 1.0
- dropout 0.1
λ 和 γ 前 50 轮固定,等编码网络基本稳定之后,再考虑把 γ 作为可学习参数。为什么?因为如果训练一开始就让 γ 参与学习,优化层的输出权重会很不稳定,梯度信号容易在早期被拉飞。固定一段再放开,模型能更快收敛。
5.3 回测验证:别让夏普损失变成自欺欺人
端到端模型很容易陷入一种情况:训练集上的夏普做得很好,验证集上一塌糊涂。原因就是模型学会了过度拟合历史收益序列,优化层又把这种过拟合放大到权重上。
我通常会在验证集上做滚动回测,每 20 个交易日更新一次组合权重,维护一个组合净值曲线。不只关注夏普,还关注最大回撤、卡玛比率、换手率,以及持仓数量中位数。
持仓数量是稀疏性的直接指标。如果 λ=0.1 时,组合里平均持仓数量是 30,说明稀疏性达到预期;如果变成 5,说明 λ 过大,模型基本是在赌少数几只股票。这个指标比夏普更容易暴露问题。
换手率也要盯。端到端模型的权重可能会频繁变化,因为梯度信号鼓励模型追逐短期收益。我会在损失函数里加一个换手惩罚项,比如:
loss = -sharpe + eta * mean(|w_t - w_{t-1}|)
eta 不需要太大,0.01 到 0.05 就足够抑制过度交易。
6. 踩坑实录:常见问题与排查技巧
6.1 协方差矩阵不满足半正定怎么办
这个是最常见的问题。如果你直接让神经网络输出一个 n×n 协方差矩阵,训练几次之后大概率会遇到 cvxpy 报错,信息类似于“You are solving a parameterized problem that is not DCP”或者“PSD constraint not satisfied”。
解决办法我在前面已经提过,用Sigma = A @ A.T + epsilon * I来参数化。这里 A 的形状可以设成 (n, k),k 取小于 n 的数,比如 n=20 时 k=10,这样既保证半正定,又降低了网络输出维度,还能起到正则化作用。
有个细节:epsilon 别设太大,否则协方差矩阵会被对角项主导,失去资产间的相关性信息。我常用 1e-4 到 1e-3,在数值稳定和信息保留之间取平衡。
6.2 稀疏参数lambda到底怎么调
λ 调不好,会出现两种极端。λ 太小,权重完全不稀疏,每只股票都有小仓位的权重,看上去像被稀释了一样;λ 太大,权重集中到一两只股票,组合风险急剧上升。
我一开始把 λ 设成可学习参数,用梯度直接更新。结果发现 λ 经常跑到一个极端值,然后很难拉回来。原因很简单,λ 的梯度在权重稀疏到 0 的区域,连续性很差,更新不稳定。
后来我改用固定 λ 加手动微调的策略:训练一小段,查看当前非零权重数量,如果数量太多,就把 λ 乘以 1.5,重新训练;如果太少,就除以 1.5。这样迭代几轮,很容易找到符合预期的稀疏度。如果你一定想让它可学习,可以加一个直通估计器,但收益有限,成本不小,我不太推荐。
还有个办法是按比例缩放 μ。因为 L1 惩罚项和收益项之间存在尺度关系,如果 μ 的均值为 0.001,λ=0.1 可能显得非常大,权重会被极度稀疏化。所以在输入可微层之前,我会对 μ 做标准化处理,让它处在一个类似 Z-Score 的尺度上,这样 λ 的取值就有了稳定的语义。
6.3 训练慢、梯度不稳、过拟合怎么办
可微凸优化层最让人头疼的就是慢。cvxpylayers 的底层要调锥规划求解器,前向求解一个 20 资产的组合优化问题,单次只要几毫秒,但如果 batch 大、资产数量多,比如 100 只股票,每个 batch 32 个样本,一次前向就要解 32 个凸优化问题,速度会让人崩溃。
我的应对思路有三个。第一,batch size 设小一点,比如 16,反正金融数据样本量不像 CV 那么大,batch 太大也没必要。第二,资产维度太高时,先用 PCA 或者聚类降到 20 维以内,把降维后的资产当作组合标的;可微层只处理降维后的组合,网络主体部分仍然处理原始特征。第三,如果必须用大 batch,可以探索用 GPU 版的求解器,但目前生态还不成熟,我一般还是用 CPU 跑,把 batch 控制在合理范围。
梯度不稳通常表现为训练早期的 loss 剧烈震荡。我会做两件事:一是给编码网络最后一层的输出做 LayerNorm,把 μ 的输出限制在一个比较稳定的区间;二是梯度裁剪,把梯度的 L2 范数裁剪到 1.0。这两步加起来,能解决大部分震荡问题。
过拟合是端到端模型的长期对手。除了常规的 dropout 和 weight decay,我还会刻意在验证集上做“冻结参数回测”,也就是训练完模型后,把参数完全冻结,然后在历史数据上滚动回测,看看它的真实稳定性。如果训练集和验证集差距过大,我宁可减少模型复杂度,比如把隐藏层从 64 降到 32,只保留最核心的特征。
6.4 常见问题速查表
我把实操中遇到比较多的问题整理成了一张表,方便你有问题的时候快速对照。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| cvxpy 报 DCP 校验失败 | 目标函数非凹,或协方差矩阵未声明 PSD | 改用代理目标,给 Sigma 参数加 PSD=True |
| 求解器输出权重全是 NaN | 协方差矩阵不正定,或 dtype 精度低 | 用 A@A.T + epsilon*I 参数化协方差,改用 double 类型 |
| 权重不稀疏 | λ 太小,或 μ 尺度太大 | 增大 λ,或对 μ 做 Z-Score 标准化 |
| 权重集中到一只股票 | λ 太大,或 γ 太小 | 调小 λ,调大 γ |
| 训练时 loss 剧烈震荡 | 梯度范数过大,λ 或 γ 更新过快 | 梯度裁剪,固定 λ 和 γ 若干轮 |
| 验证集夏普远低于训练集 | 过拟合,未来信息泄漏 | 严格时间切分,降低模型容量,增加换手惩罚 |
| 训练速度非常慢 | batch 太大,资产维度太高 | 减小 batch,先降维到 20 维以下 |
7. 实战中最值得留意的几个细节
最后分享几个我在实际跑这套方案时踩过坑之后总结出的细节,希望对你有用。
第一个细节,可微层一定要单独测试。别急着把它接进大网络。我习惯先用随机生成的正定矩阵和收益向量,把可微层的输出与闭式解对比一下。如果是无约束情况,直接对比Σ^{-1}(μ - r_f 1)的方向;如果有非负约束,就对比 cvxpy 直接求解的结果。这一关过了,再接网络会省很多事。
第二个细节,训练早期把 γ 固定得大一点。γ 越大,优化层越重视风险,输出的权重会更平滑,梯度信号也更稳定。等模型开始收敛,再逐步调小 γ,让组合有更多收益进攻性。这个顺序基本适用于大多数资产池。
第三个细节,不要贪心,别一上来就端到端。你可以先让编码网络单独预测收益,把预测结果丢进一个非可微的稀疏切点优化器,得到一组基线权重和基线夏普。然后把优化器换成可微层,用端到端损失微调整个网络。这样能直观看到端到端训练带来的增益,也方便排查是网络的问题还是优化层的问题。
第四个细节,记录每次前向之后非零权重的数量。我在训练循环里会打印当前 batch 平均持仓数、平均换手率、组合样本内夏普。这三个指标比单独看 loss 更能说明模型是否健康。如果持仓数一直走低,说明 λ 的影响正在变大;如果换手率飙高,说明模型开始追逐噪声了。
这套端到端方案并不是万能药,它会增加系统复杂度,也要处理更多训练细节。但当你把夏普比率真正写进可微层那一刻,组合优化就从“预测好了再去优化”变成了“为了组合最优而学习预测”,这个思路上的转变,值回票价。