第 5 章 · 02 L2 正则减少零权重 本节摘要:本节讲均值-方差优化的一个通病——零权重过多,以及 PyPortfolioOpt 的解药: 。标准 maxsharpe 或 minvolatility 的输出往往把大量资产权重压到 0(「最优」组合只挑几只资产),这在样本内最优,但样本外常常欠分散、欠稳健。L2 正则在目标函数里加一个 惩罚项,鼓励权重分布更均匀——形式上与机器学习的 L2 正则完全一致(只是目的相反:ML 用它让权重小,这里用它让权重大)。本节拆解 L2 正则的数学形式、源码实现、γ 参数的调参经验,以及 cookbook 3 里「交易成本 + L2 正则」的完整调参案例,展示 γ 从默认 1 调小到 0.05 时权重如何从「过度均匀」回归「合理分散」。
本节摘要:本节讲均值-方差优化的一个通病——零权重过多,以及 PyPortfolioOpt 的解药:
objective_functions.L2_reg。标准 max_sharpe 或 min_volatility 的输出往往把大量资产权重压到 0(「最优」组合只挑几只资产),这在样本内最优,但样本外常常欠分散、欠稳健。L2 正则在目标函数里加一个γ wᵀ w惩罚项,鼓励权重分布更均匀——形式上与机器学习的 L2 正则完全一致(只是目的相反:ML 用它让权重小,这里用它让权重大)。本节拆解 L2 正则的数学形式、源码实现、γ 参数的调参经验,以及 cookbook 3 里「交易成本 + L2 正则」的完整调参案例,展示 γ 从默认 1 调小到 0.05 时权重如何从「过度均匀」回归「合理分散」。
内容来源:原项目源码
pypfopt/objective_functions.py、pypfopt/base/_base_optimizer.py、文档docs/MeanVariance.rst(L2 Regularisation 专节)、docs/UserGuide.rst、cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb,汉化并套用体系化模板。
阅读完本节,你应当能够:
γ wᵀ w,理解它如何鼓励均匀分布。ef.add_objective(objective_functions.L2_reg, gamma=...) 加正则。均值-方差优化器有个让人头疼的特性:它倾向于把大多数资产权重压成 0。回顾第 1 章的 max_sharpe 输出:
GOOG: 0.0458 AAPL: 0.0674 FB: 0.2008 BABA: 0.0849 AMZN: 0.0352 GE: 0.0000 AMD: 0.0000 WMT: 0.0000 BAC: 0.0000 GM: 0.0000 T: 0.0000 UAA: 0.0000 SHLD: 0.0000 XOM: 0.0000 RRC: 0.0000 BBY: 0.0159 MA: 0.3287 PFE: 0.2039 JPM: 0.0000 SBUX: 0.0173
20 只资产里只有 8 只权重非零,其余 12 只被压成 0。这在样本内是「数学最优」,但实务上有问题:
docs/MeanVariance.rst 引用研究:零权重过多的组合样本外表现欠佳,因为「哪些资产被压零」对输入极度敏感。docs/MeanVariance.rst 的 L2 专节给出公式。以最小方差目标为例:
原目标: minimize wᵀ Σ w 加正则后: minimize wᵀ Σ w + γ wᵀ w
wᵀ w = Σ w_i² 是权重平方和。这个惩罚项的几何直觉:
wᵀ w 越大(= 1² = 1)。wᵀ w 越小(= N × (1/N)² = 1/N)。因此最小化 wᵀ w 等价于「鼓励均匀分布」——加这个项会惩罚集中,促使优化器把权重分散到更多资产。
💡 为何叫 L2:因为这个惩罚项
wᵀ w = Σ w_i²是权重向量的 L2 范数的平方。在机器学习里,L2 正则是λ‖w‖²,形式完全一致——但目的相反:ML 里用它让权重小(防过拟合),这里用它让权重大且均匀(防过度集中)。文档明确指出这个「同形不同目的」的微妙之处。
打开 pypfopt/objective_functions.py,定位 L2_reg(源码 138-162 行):
def L2_reg(w, gamma=1): r""" L2 regularisation, i.e γ||w||^2, to increase the number of nonzero weights. Example:: ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=2) ef.min_volatility() """ L2_reg = gamma * cp.sum_squares(w) return _objective_value(w, L2_reg)
极其简洁:
gamma(默认 1):正则强度。增大 γ → 更强均匀化、更多非零权重。cp.sum_squares(w):cvxpy 原子函数,等于 Σ w_i²,即 wᵀ w。这是凸函数(平方和),符合 DCP。gamma * cp.sum_squares(w):整个正则项是 γ 加权的权重平方和。_objective_value:同样的双模式辅助函数——w 是 ndarray 时返回数值,是 cp.Variable 时返回表达式。add_objective 把正则项累加到主目标上。源码(_base_optimizer.py 347-369 行):
def add_objective(self, new_objective, **kwargs): if self._opt is not None: raise InstantiationError( "Adding objectives to an already solved problem might have unintended consequences. ..." ) self._additional_objectives.append(new_objective(self._w, **kwargs))
求解时,主目标方法(min_volatility/efficient_risk 等)会把它加进去:
# min_volatility 源码片段 self._objective = objective_functions.portfolio_variance(self._w, self.cov_matrix) for obj in self._additional_objectives: self._objective += obj # 累加额外目标
所以加 L2 正则后的实际优化问题是:
minimize wᵀ Σ w + γ wᵀ w (以 min_volatility 为例) subject to sum(w) = 1, 0 ≤ w ≤ 1
⚠️ 与 max_sharpe 不兼容:回顾第 4 章第 02 节,max_sharpe 做变量替换
y = κw后,额外目标没有按 κ 缩放,会发警告并可能失效。若要 L2 正则,改用min_volatility、efficient_risk、efficient_return或max_quadratic_utility——它们不做变量替换,与额外目标完全兼容。
γ 是 L2 正则唯一的超参数,需要调。docs/MeanVariance.rst 的 note 给出经验:
实务上,γ 必须调到达到你想要的正则强度。但若资产池较小(少于 20 只),
gamma=1是个好起点。对更大的资产池,或想要更多非零权重,增大 γ。
| γ 取值 | 行为 | 适用 |
|---|---|---|
| γ = 0 | 无正则(原始问题) | — |
| γ = 0.01~0.1 | 轻微正则,接近原始 | 想保留集中度 |
| γ = 1(默认) | 中等正则 | 小池(<20 资产)起步 |
| γ = 5~20 | 强正则,接近等权 | 大池或要高度分散 |
from pypfopt import EfficientFrontier, objective_functions ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=1) ef.max_sharpe() # 注意:这里会有警告,见下方说明
README 的官方示例确实用 max_sharpe + L2,但会触发警告。更稳妥的是:
ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=1) ef.min_volatility() # 与额外目标完全兼容
ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=1) ef.efficient_risk(target_volatility=0.20)
for gamma in [0.01, 0.1, 1, 5, 10]: ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=gamma) ef.min_volatility() nonzero = sum(v > 1e-4 for v in ef.clean_weights().values()) print(f"γ={gamma}: {nonzero} 个非零权重")
cookbook 3 提供了一个教科书级的 L2 调参案例,展示 γ 从大到小的权衡。背景:已有等权初始组合,想优化但考虑交易成本。
initial_weights = np.array([1/len(tickers)] * len(tickers)) ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.transaction_cost, w_prev=initial_weights, k=0.001) ef.min_volatility() weights = ef.clean_weights()
结果:4 只资产权重为 0——不够分散。
ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.transaction_cost, w_prev=initial_weights, k=0.001) ef.add_objective(objective_functions.L2_reg) # gamma=1 默认 ef.min_volatility()
结果:过度均匀化——权重几乎回到等权,失去了优化的意义。
ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.transaction_cost, w_prev=initial_weights, k=0.001) ef.add_objective(objective_functions.L2_reg, gamma=0.05) # 减弱正则 ef.min_volatility() ef.portfolio_performance(verbose=True)
结果:合理平衡——组合相对均衡,同时对 JD(高分散价值的资产)仍给了显著权重。
💡 调参心法:γ 不是「越大越好」也不是「越小越好」,而是找到「分散度合适、又保留优化价值」的平衡点。cookbook 3 的案例展示了完整的调参弧线:从无正则(欠分散)→ γ=1(过均匀)→ γ=0.05(刚好)。这种「先加足再往回调」是常见的调参套路。
L2 正则可以与多个目标叠加:
| 主目标 | 与 L2 兼容? | 备注 |
|---|---|---|
min_volatility |
是 | 最常搭配 |
efficient_risk |
是 | |
efficient_return |
是 | |
max_quadratic_utility |
是 | |
max_sharpe |
警告(变量替换) | 可能失效,避免 |
还可与交易成本、行业约束同时用:
ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=0.1) ef.add_objective(objective_functions.transaction_cost, w_prev=w_prev, k=0.001) ef.add_sector_constraints(sector_mapper, sector_lower, sector_upper) ef.min_volatility()
所有额外目标与约束在求解时同时生效。
γ wᵀ w 到主目标,惩罚集中、鼓励均匀——形式同 ML 的 L2,但目的相反(让权重大且均匀)。L2_reg(w, gamma=1) = gamma * cp.sum_squares(w),凸函数。add_objective 累加到 _additional_objectives,求解时 += 到主目标。min_volatility/efficient_risk/efficient_return/max_quadratic_utility 兼容;与 max_sharpe 有警告,避免。下一节是本章最后一节——自定义目标函数,看如何写一个凸目标(或非凸目标)并传给优化器。