07 模型选择与正则化

本章会用到的数据

  • 公开下载A 股前复权日行情。代码会在首次运行时下载并保存到 data/course/

  • 这些数据能做什么:用股票收益率比较 Ridge、Lasso 与简单基准。

  • 分析时注意:按时间顺序划分样本,只用训练和验证阶段选择正则化强度。

  • 判断模型:模型没有稳定胜过简单基准时,应保留简单模型。

【课堂核心】3 学时学习安排

学习内容 分钟
动机与先修 20
Ridge 与 Lasso 45
时间有序选择 35
形成性检查 20
独立任务 40
反馈与小结 20

【可选拓展】约束几何与额外惩罚路径

约束几何证明与额外惩罚路径安排课后,不计入 3 学时课堂核心。

第7章:模型选择与正则化

寻找简单与准确的完美平衡

简单与准确的平衡 一个概念图,展示了从欠拟合(过于简单)到过拟合(过于复杂)的过渡,中间是理想的平衡点。 简单 (欠拟合) 复杂 (过拟合) 平衡

故事开篇:长三角股票预测必须先过最终测试门槛

  • 公开任务:用 stock/stock_price_pre_adjusted.h5 的 2020—2024 年前复权行情,预测江苏恒瑞医药(600276.XSHG)下一交易日收益。
  • 候选信息:同日药明康德、海康威视与上海机场收益;字段为 order_book_iddateclose,先按日期对齐再生成收益。
  • 选择边界:惩罚强度只在开发期扩展窗口选择;最终测试期只开一次,并与 OLS、零收益和训练均值基线同窗比较。
  • 决策边界:复杂候选若未同时越过事先确定基线,结果就是 no-increment / no-deploy;失败证据必须保留。

核心警示: 训练期拟合和稀疏系数都不是实际使用证据;只有确定规则下的时间外增量比较能够支持当前样本的候选决策。

案例边界:全程离线读取中国公开行情;测试表不得反馈到选模。

核心问题:为何复杂的金融模型常常预测失败?

我们经常看到拥有数百个变量的复杂计量经济学模型。

  • 直觉上:包含的变量越多,信息就越丰富,模型应该越“准确”。
  • 实际上:过于复杂的模型在样本外(out-of-sample)的预测中,表现往往非常糟糕,甚至不如简单模型。

本章的核心,就是解决这个理论与现实之间的矛盾。

本讲座的学习目标

理论层面

  1. 深刻理解:为什么我们需要在模型复杂度和预测能力之间做出权衡(偏差-方差的权衡)。
  2. 掌握模型选择的方法:学会如何从众多候选模型中,科学地挑选出“最优”模型。

技术层面

  1. 掌握正则化的核心思想与方法:学会使用岭回归、套索回归等技术,来“惩罚”不必要的模型复杂度。
  2. 具备实践能力:能够使用Python,对真实的金融数据应用正则化方法,并解释其结果。

根本矛盾:偏差-方差的权衡

  • 真实条件均值\(m(x_0)=E(Y\mid X=x_0)\)
  • 重复抽样对象:训练集记为 \(\mathcal D\)
  • 新观测响应\(Y_0=m(x_0)+\varepsilon_0\),且 \(E(\varepsilon_0\mid X=x_0)=0\)

固定 \(x_0\) 后,平方预测损失可分解为:

\[ \begin{aligned} E_{\mathcal D,Y_0}[(Y_0-\hat f_{\mathcal D}(x_0))^2] &=\big[E_{\mathcal D}\hat f_{\mathcal D}(x_0)-m(x_0)\big]^2 \\ &\quad+\operatorname{Var}_{\mathcal D}[\hat f_{\mathcal D}(x_0)] \\ &\quad+\operatorname{Var}(\varepsilon_0\mid X=x_0). \end{aligned} \]

  • 偏差平方:相对真实条件均值 \(m(x_0)\) 定义。
  • 方差:重复训练集下预测值的波动。
  • 不可约误差:新观测的条件噪声;期望同时对 \(\mathcal D\)\(Y_0\) 取值。

理解偏差 (Bias)

偏差衡量的是模型的“固执”程度,即其内在假设与数据真实规律的偏离程度。

  • 高偏差: 意味着模型过于简单,无法捕捉数据的复杂模式。
  • 表现: 欠拟合 (Underfitting)。模型在训练集和测试集上表现都很差。
  • 例子: 用一条直线去拟合非线性的数据。

理解方差 (Variance)

方差衡量的是模型的“敏感”程度,即模型对训练数据微小变化的反应有多剧烈。

  • 高方差: 意味着模型过于复杂,把训练数据中的噪声也当成了规律来学习。
  • 表现: 过拟合 (Overfitting)。模型在训练集上表现极好,但在测试集上表现很差。
  • 例子: 用一个高阶多项式去拟合每一个数据点。

可视化权衡:一个形象的比喻

想象用模型来“打靶”。

偏差-方差的权衡:打靶比喻 四个靶子,分别展示了低偏差低方差,低偏差高方差,高偏差低方差,以及高偏差高方差的情况。 低偏差, 低方差 (理想状态) 低偏差, 高方差 (过拟合) 高偏差, 低方差 (欠拟合) 高偏差, 高方差 (最差状态)

图解:偏差-方差的权衡曲线

下图是许多灵活度递增估计器的常见示意,不是对任意函数类、估计器或数据生成过程的定理。

  • 复杂度上升时,偏差常下降、方差常上升,测试风险因而可能呈 U 形。
  • 变化方向和最小点必须由给定候选集与开发窗验证,不能由示意图预设。
偏差-方差权衡的常见示意曲线 该图仅示意一种常见情形:模型复杂度增加时偏差可能下降、方差可能上升、总误差可能呈U形;具体路径须由开发窗验证。 模型复杂度 偏差-方差的权衡 偏差平方 (Bias²) 方差 (Variance) 总预测误差 欠拟合区域 过拟合区域

过拟合:模型“记住”了噪声而非规律

过拟合 (Overfitting) 是指模型对训练数据拟合得过于完美,以至于把数据中的随机噪声也当作了真实的规律来学习。

  • 后果:该模型在训练集上表现极好(例如,\(R^2\) 接近1),但在新的、未见过的数据(测试集)上表现非常差。
  • 原因:模型过于复杂,自由度太高。

可视化过拟合:一个直观的例子

我们将用多项式回归来拟合带有噪声的正弦波数据。

  • 真实规律: \(y = \sin(x)\)
  • 观测数据: \(y = \sin(x) + \epsilon\) (其中 \(\epsilon\) 是随机噪声)

我们将看到,过于高阶(复杂)的多项式模型会发生什么。

第1步:生成我们的“玩具”数据集

首先,我们生成一些模拟数据作为我们的“真实世界”。

代码
# 为“第1步:生成我们的“玩具”数据集”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“第1步:生成我们的“玩具”数据集”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 导入 Matplotlib 样式接口并绑定为 `style`,用于给正则化机制图设置一致的白底网格主题。
import matplotlib.style as style

# 为“第1步:生成我们的“玩具”数据集”,固定随机数序列,使课堂示例可重复。
np.random.seed(42)
# 建立 `X_sample` 的有序取值网格,用于展示“带有噪声的正弦波数据”随参数变化的比较结果。
X_sample = np.linspace(0, 10, 20)
# 在正弦曲线上叠加标准差 0.3 的噪声,得到用于阶数比较的观测响应。
y_sample = np.sin(X_sample) + np.random.normal(0, 0.3, len(X_sample))
# 建立 `X_true` 的有序取值网格,用于展示“带有噪声的正弦波数据”随参数变化的比较结果。
X_true = np.linspace(0, 10, 100)
# 用正弦信号叠加噪声得到回归目标,演示模型复杂度差异。
y_true = np.sin(X_true)

# 创建 `fig, ax` 画布,承载“带有噪声的正弦波数据”的并排视觉比较。
fig, ax = plt.subplots(figsize=(9, 6))
# 以 `X_sample` 为横轴、`y_sample` 为纵轴绘制散点,展示“带有噪声的正弦波数据”。
ax.scatter(X_sample, y_sample, label='观测数据 (带噪声)', color='black', zorder=5)
# 以 `X_true` 为横轴、`y_true` 为纵轴绘制曲线,展示“带有噪声的正弦波数据”。
ax.plot(X_true, y_true, label='真实规律 (无噪声)', color='crimson', lw=2.5)
# 将图题设为“我们的“玩具”数据集”,直接说明当前图形的比较目的。
ax.set_title('我们的“玩具”数据集', fontsize=16)
# 为“第1步:生成我们的“玩具”数据集”,将横轴标为“X”,明确横向编码的变量。
ax.set_xlabel('X', fontsize=12)
# 为“第1步:生成我们的“玩具”数据集”,将纵轴标为“y”,明确纵向编码的变量。
ax.set_ylabel('y', fontsize=12)
# 显示“第1步:生成我们的“玩具”数据集”图例,使颜色或线型与比较对象一一对应。
ax.legend(fontsize=11)
# 显示玩具样本及真实非线性关系,检查噪声水平和线性模型将面对的欠拟合结构。
plt.show()
横轴为输入 x、纵轴为观测 y;散点围绕正弦形态波动,作为欠拟合与过拟合的共同样本。
图 1: 带有噪声的正弦波数据

第2步:简单模型(1阶)的欠拟合

一个1阶多项式(直线)模型过于简单,无法捕捉数据的非线性趋势。这是高偏差的表现。

代码
# 为“第2步:简单模型(1阶)的欠拟合”,从 `sklearn.preprocessing` 导入`PolynomialFeatures` 用于由原始特征展开多项式项。
from sklearn.preprocessing import PolynomialFeatures
# 为“第2步:简单模型(1阶)的欠拟合”,从 `sklearn.linear_model` 导入`LinearRegression` 用于拟合普通最小二乘线性回归。
from sklearn.linear_model import LinearRegression
# 为“第2步:简单模型(1阶)的欠拟合”,从 `sklearn.pipeline` 导入`make_pipeline` 用于按给定顺序创建预处理与估计器流程。
from sklearn.pipeline import make_pipeline

# 封装指定阶数的多项式拟合与绘图,使欠拟合、适度拟合和过拟合使用同一数据口径比较。
def plot_poly_fit(degree, X_sample, y_sample, X_true, y_true, title_suffix):
    # 组装“多项式展开—线性回归”Pipeline,使变换与估计共享同一次拟合调用。
    model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
    # 用抽样训练点估计由 `degree` 指定阶数的多项式系数,供三个复杂度页面复用。
    model.fit(X_sample[:, np.newaxis], y_sample)
    # 在共同网格上生成当前 `degree` 阶模型预测,用同一真函数比较欠拟合、适度拟合与过拟合。
    y_pred = model.predict(X_true[:, np.newaxis])

    # 创建 `fig, ax` 画布,承载“1阶多项式模型:欠拟合”的并排视觉比较。
    fig, ax = plt.subplots(figsize=(9, 6))
    # 以 `X_sample` 为横轴、`y_sample` 为纵轴绘制散点,展示“1阶多项式模型:欠拟合”。
    ax.scatter(X_sample, y_sample, label='观测数据', color='black', zorder=5)
    # 以 `X_true` 为横轴、`y_true` 为纵轴绘制曲线,展示“1阶多项式模型:欠拟合”。
    ax.plot(X_true, y_true, label='真实规律', color='crimson', lw=2.5, alpha=0.4)
    # 以 `X_true` 为横轴、`y_pred` 为纵轴绘制曲线,展示“1阶多项式模型:欠拟合”。
    ax.plot(X_true, y_pred, label=f'{degree}阶多项式拟合', color='cornflowerblue', lw=3)
    # 用图题概括“第2步:简单模型(1阶)的欠拟合”的比较对象与当前计算结果。
    ax.set_title(f'模型复杂度 (阶数 = {degree}): {title_suffix}', fontsize=16)
    # 为“第2步:简单模型(1阶)的欠拟合”,将横轴标为“X”,明确横向编码的变量。
    ax.set_xlabel('X', fontsize=12)
    # 为“第2步:简单模型(1阶)的欠拟合”,将纵轴标为“y”,明确纵向编码的变量。
    ax.set_ylabel('y', fontsize=12)
    # 为“第2步:简单模型(1阶)的欠拟合”,限定纵轴范围,使比较对象使用一致尺度。
    ax.set_ylim(-2, 2)
    # 显示“第2步:简单模型(1阶)的欠拟合”图例,使颜色或线型与比较对象一一对应。
    ax.legend(fontsize=11)
    # 显示当前 `degree` 阶拟合、观测点与真实规律,检查该复杂度对应的拟合形态。
    plt.show()

# 拟合并绘制当前阶数的多项式,比较欠拟合、适度拟合与过拟合。
plot_poly_fit(1, X_sample, y_sample, X_true, y_true, "欠拟合 (高偏差)")
噪声正弦散点上叠加一条近似直线;曲线未能捕捉周期弯曲,显示欠拟合。
图 2: 1阶多项式模型:欠拟合

第3步:“恰到好处”的模型(3阶)

一个3阶多项式模型较好地捕捉了数据的真实规律。这是偏差和方差较好平衡的表现。

代码
# 拟合并绘制当前阶数的多项式,比较欠拟合、适度拟合与过拟合。
plot_poly_fit(3, X_sample, y_sample, X_true, y_true, "良好拟合")
噪声正弦散点上叠加三阶曲线;曲线捕捉主要弯曲且不过度追随单点噪声。
图 3: 3阶多项式模型:良好拟合

第4步:复杂模型(15阶)的过拟合

一个15阶多项式模型为了穿过每一个数据点而剧烈扭曲。它学习了噪声。这是高方差的表现。

代码
# 拟合并绘制当前阶数的多项式,比较欠拟合、适度拟合与过拟合。
plot_poly_fit(15, X_sample, y_sample, X_true, y_true, "过拟合 (高方差)")
噪声正弦散点上叠加高阶曲线;曲线在样本间剧烈摆动,显示过拟合。
图 4: 15阶多项式模型:过拟合

我们的武器:模型选择与正则化

为了避免过拟合,找到最佳的模型复杂度,我们有两种主要策略:

  1. 模型选择 (Model Selection):
    • 也称为特征选择 (Feature Selection)
    • 从一个大的特征集合中,挑选出一个子集来构建模型。
    • 是一种“硬”选择,要么保留特征,要么丢弃。
  2. 正则化 (Regularization):
    • 使用所有特征,但在模型训练时对系数的大小施加“惩罚”。
    • 是一种“软”选择,通过压缩系数来降低模型的有效复杂度。

第一部分:模型选择方法

核心思想:我们有很多备选的预测变量(特征),如何挑选出一个“最优”的组合?

  • 例子:在预测一只股票的收益时,我们可能有上百个备选因子(公司规模、估值、动量、宏观经济指标等)。把所有因子都放进模型可能导致过拟合。

我们将介绍两种主流的自动化选择方法。

模型选择方法 1:最佳子集选择法

最佳子集选择法 (Best Subset Selection) 是一种暴力但彻底的方法。

算法步骤:

  1. \(M^{(0)}\) 为不含任何特征的“零模型”(只有截距项)。

  2. 对于 \(k = 1, 2, \dots, p\) (其中 \(p\) 是总特征数):

    1. 拟合所有包含 \(k\) 个特征的组合模型。总共有 \(\binom{p}{k}\) 个。
    2. 在这 \(\binom{p}{k}\) 个模型中,根据某种准则(如RSS最低)选出最佳模型,记为 \(M^{(k)}\)
  3. 我们现在有 \(p+1\) 个候选模型:\(M^{(0)}, M^{(1)}, \dots, M^{(p)}\)

  4. 使用交叉验证 (Cross-Validation) 或信息准则 (AIC, BIC) 在这 \(p+1\) 个模型中选出最终的胜者。

最佳子集法的致命缺陷:计算成本

最佳子集法虽然理论上能找到每个子集大小下的最优模型,但它有一个致命问题:计算成本极高

  • 如果一个模型有 \(p\) 个备选特征,那么我们需要评估 \(2^p\) 个不同的模型。
  • \(p=10\) 时,是 \(2^{10} = 1,024\) 个模型,尚可接受。
  • \(p=20\) 时,是 \(2^{20} \approx 100\) 万个模型。
  • \(p=40\) 时,是 \(2^{40} \approx 1\) 万亿个模型,计算上已不可行。

在有成百上千备选变量的金融问题中,此方法不适用。

模型选择方法 2:前向分步算法

前向分步算法 (Forward Stepwise Selection) 是一种更高效的“贪心”算法。

算法步骤:

  1. \(M^{(0)}\) 为不含任何特征的“零模型”。

  2. 对于 \(k = 1, 2, \dots, p\):

    1. \(M^{(k-1)}\) 为基础,尝试加入一个尚未被包含的特征。
    2. 在所有可能的 \((p-k+1)\) 个新模型中,选择那个提升最大(如RSS下降最多)的模型,记为 \(M^{(k)}\)
  3. 我们得到一个由 \(p+1\) 个模型组成的序列:\(M^{(0)}, M^{(1)}, \dots, M^{(p)}\)

  4. 使用交叉验证或信息准则在序列中选出最终模型。

前向分步算法的优缺点

  • 优点: 计算效率极高。它只需要评估 \(1 + \sum_{k=1}^{p}(p-k+1) = 1 + \frac{p(p+1)}{2}\) 个模型,远小于 \(2^p\)
  • 缺点: 它是一种“贪心”算法,不能保证找到全局最优解。因为它每一步做出的都是局部最优选择,一旦一个变量被选入模型,就再也不会被移除。

补充:后向分步算法

与前向分步相反,后向分步算法 (Backward Stepwise Selection) 从完整模型开始,逐步移除使当前选择准则恶化最少的特征。

算法步骤:

  1. \(M^{(p)}\) 为包含所有 \(p\) 个特征的完整模型。

  2. 对于 \(k = p, p-1, \dots, 1\):

    1. \(M^{(k)}\) 为基础,尝试移除一个特征。
    2. 在所有可能的 \(k\) 个新模型中,选择那个性能下降最小(如RSS增加最少)的模型,记为 \(M^{(k-1)}\)
  3. 同样,我们得到一个模型序列,再从中选择最优。

如何在候选模型中做出最终选择?

前向/后向分步算法都为我们提供了一个模型序列 (\(M^{(0)}, \dots, M^{(p)}\))。但哪个才是最好的?

我们不能简单地使用训练集的RSS或\(R^2\),因为它们总是偏爱更复杂的模型。

这些工具的目标和证据口径不同,不能统称为“样本外惩罚指标”:

  • CV:折内重拟合,用未参与该折拟合的数据估计预测风险;时间序列用滚动/阻塞切分,而非给单次拟合优度加解析惩罚。
  • AIC\(-2\log L+2k\);在正则参数模型等条件下比较相对预期信息损失,偏预测效率,不是最终测试样本指标。
  • BIC\(-2\log L+k\log n\);在候选模型、先验与正则条件下近似模型证据,特定条件下偏选择一致性,目标不同于 AIC。
  • 调整后 \(R^2\)\(1-(1-R^2)(n-1)/(n-k-1)\);同一样本和响应上的自由度校正,不是样本外证据。

第二部分:正则化方法

核心思想:通过“惩罚项”收缩系数并控制预测模型复杂度;系数大小或是否为零不等于变量的现实重要性,更不提供因果识别。

  • 这是一种更平滑、更连续的模型复杂度控制方法。
  • 它通过修改模型的代价函数 (Cost Function) 来实现。

我们将学习三种最主流的正则化方法:

  • 岭回归 (Ridge Regression)
  • 套索回归 (Lasso Regression)
  • 弹性网络 (Elastic Net)

岭回归 (Ridge Regression): L2 正则化

岭回归在线性回归的代价函数上,增加了一个L2惩罚项

  • 普通线性回归 (OLS) 的代价函数:

    \[ \large{J_{OLS}(\beta) = \sum_{i=1}^{n} (y_i - \beta_0 - \sum_{j=1}^{p} \beta_j x_{ij})^2} \]

  • 岭回归的代价函数:

    \[ \large{J_{Ridge}(\beta) = \underbrace{\sum_{i=1}^{n} (y_i - \beta_0 - \sum_{j=1}^{p} \beta_j x_{ij})^2}_{\text{残差平方和 (RSS)}} + \underbrace{\lambda \sum_{j=1}^{p} \beta_j^2}_{\text{L2正则化项 (Penalty)}}} \]

理解岭回归的惩罚项

\[ \large{\lambda \sum_{j=1}^{p} \beta_j^2} \]

  • \(\beta_j\): 模型的第 \(j\) 个特征的系数。
  • \(\sum \beta_j^2\): 所有系数的平方和。这被称为L2范数 (L2-norm) 的平方。
  • \(\lambda\) (lambda): 正则化参数惩罚强度。这是一个超参数,需要我们手动设定。
    • \(\lambda = 0\) 时,岭回归等价于普通线性回归。
    • \(\lambda \to \infty\) 时,所有系数 \(\beta_j\) 都将被压缩至趋近于0。

\(\lambda\) 的作用:控制模型的复杂度

\(\lambda\) 扮演着一个“纪律委员”的角色,控制着系数的大小。

  • 小的 \(\lambda\): 惩罚力度小,模型自由度高,趋向于OLS解,可能导致高方差(过拟合)。
  • 大的 \(\lambda\): 惩罚力度大,系数被强烈压缩,模型变得简单,可能导致高偏差(欠拟合)。

我们的任务是在开发窗按预设指标选择候选 \(\lambda\);不能由示意图或训练拟合预设“最优”平衡。

岭回归的几何解释

岭回归的解可以看作是两个几何形状的交点:

  1. 损失函数等高线 (RSS Contours): 在系数空间中,这些是以OLS解为中心的一系列椭圆。
  2. L2惩罚约束: \(\sum \beta_j^2 \le C\)。在二维空间中,这是一个以原点为中心的圆形区域。

岭回归的解就是椭圆等高线与圆形区域首次相切的点。

岭回归的几何解释(优化版) RSS损失函数的椭圆等高线与L2惩罚的圆形约束区域相切,切点即为岭回归的解。此图经过几何校正,更清晰准确。 β₁ β₂ 0 RSS 等高线 L2 约束:β₁² + β₂² ≤ C OLS 解 (β̂) 岭回归解

套索回归 (Lasso Regression): L1 正则化

套索回归 (Lasso) 在代价函数上增加的是 L1惩罚项

  • 套索回归的代价函数:

    \[ \large{J_{Lasso}(\beta) = \underbrace{\sum_{i=1}^{n} (y_i - \beta_0 - \sum_{j=1}^{p} \beta_j x_{ij})^2}_{\text{残差平方和 (RSS)}} + \underbrace{\lambda \sum_{j=1}^{p} |\beta_j|}_{\text{L1正则化项 (Penalty)}}} \]

  • 关键区别: 惩罚项是系数的绝对值之和 (\(\sum |\beta_j|\)), 而非平方和。这被称为L1范数 (L1-norm)

L1惩罚项的独特之处:实现特征选择

这个看似微小的变化(从平方和到绝对值和)带来了本质上的区别:

Lasso 的 L1 惩罚可以产生精确零系数和稀疏预测模型。

零/非零取决于缩放、惩罚强度、样本与相关结构;相关特征下入选集合可能不稳定,须报告重抽样或跨窗稳定性。

Lasso回归的几何解释

Lasso的解同样可以看作是两个几何形状的交点:

  1. 损失函数等高线 (RSS Contours): 同样是以OLS解为中心的一系列椭圆。
  2. L1惩罚约束: \(\sum |\beta_j| \le C\)。在二维空间中,这是一个菱形 (diamond) 区域。

Lasso的解就是椭圆等高线与菱形区域首次相交的点。

Lasso回归的几何解释(优化版) RSS损失函数的椭圆等高线与L1惩罚的菱形约束区域相交,交点很可能在顶点上,从而产生稀疏解。此图经过几何校正,更清晰准确。 β₁ β₂ 0 L1 约束: |β₁| + |β₂| ≤ C RSS 等高线 OLS 解 (β̂) Lasso 解 (β₂=0)

几何解释:为什么Lasso能产生稀疏解?

  • 由于L1约束区域是一个带有尖角的菱形,损失函数的椭圆等高线很大概率会首先与菱形的一个顶点相交。
  • 这些顶点恰好位于坐标轴上,意味着其中一个(或多个)系数为0。
  • 相比之下,L2 约束边界光滑,有限惩罚下通常不产生精确零;特殊数据关系或数值阈值是例外。

岭回归 vs. 套索回归:总结

特性 岭回归 (Ridge, L2) 套索回归 (Lasso, L1)
惩罚项 系数平方和 (\(\sum \beta_j^2\)) 系数绝对值和 (\(\sum |\beta_j|\))
系数 有限惩罚下通常不精确为 0,不以稀疏化为目标 可产生精确 0,但零/非零依赖数据与惩罚口径
变量筛选 不以稀疏筛选为目标 形成候选稀疏集,不等于真实重要性或因果识别
适用场景 相关预测变量较多、希望整体收缩 预测任务中希望候选稀疏且会检查选择稳定性
相关特征 常整体收缩,稳定性仍须实测 入选集合可能不稳定,须做重抽样或跨窗检查

弹性网络 (Elastic Net): 两全其美

弹性网络 (Elastic Net) 同时结合了L1和L2惩罚项,试图集两家之长。

  • 弹性网络的代价函数可以看作是:

    \[ \large{J_{EN}(\beta) = \text{RSS} + \lambda \left[ \alpha \sum |\beta_j| + (1-\alpha) \frac{1}{2} \sum \beta_j^2 \right]} \]

  • 它有两个超参数:

    • \(\lambda\): 控制总体的惩罚强度。
    • \(\alpha\): 控制L1和L2惩罚的相对比例。
      • \(\alpha=1\) 是 Lasso
      • \(\alpha=0\) 是 Ridge

为何需要弹性网络?

弹性网络在某些情况下优于Lasso:

  1. 处理高度相关的特征: 在某些设计和调参取值下,弹性网络可能比 Lasso 更明显地呈现分组选择倾向;这不是保证,仍须检查跨窗入选稳定性。
  2. \(p > n\) 的情况: 在一般位置、解唯一等常见条件下,Lasso 活跃变量数通常受样本量约束;弹性网的 L2 项可放宽这一现象,但实际活跃数仍由设计与调参决定。

第三部分:正则化实战:预测股票收益

现在使用公开前复权 A 股行情,观察正则化在同一市场多只股票收益高度相关时如何工作。

  • 目标:预测江苏恒瑞医药 (600276.XSHG) 的下一交易日收益率。
  • 特征:
    • 浙江海康威视 (002415.XSHE) 的当日收益率。
    • 上海机场 (600009.XSHG) 的当日收益率。
    • 上海药明康德 (603259.XSHG) 的当日收益率。
  • 模型: 岭回归和套索回归。

步骤1:获取并准备数据

  • 数据:公开 stock/stock_price_pre_adjusted.h5
  • 样本期:2020—2024 年。
  • 价格口径:前复权,用于计算区间内连续收益。
  • 可复现边界:代码不包含网络或模拟回退。
代码
from pathlib import Path  # 管理跨平台公开数据路径
from urllib.request import urlretrieve  # 复用本章隐藏设置单元安装的浏览器标识下载器
import numpy as np  # 计算对数收益率
import pandas as pd  # 整理多只股票的日行情

# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择行情文件。
price_data_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5'), Path('C:/qiufei/data/stock/stock_price_pre_adjusted.h5'), Path('data/course/stock_price_pre_adjusted.h5')] if candidate_path.exists()), Path('data/course/stock_price_pre_adjusted.h5'))
if not price_data_path.exists():
    price_data_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/stock/stock_price_pre_adjusted.h5', price_data_path)
order_book_ids = ['600276.XSHG', '002415.XSHE', '600009.XSHG', '603259.XSHG']  # 固定长三角公司样本
price_frames = []  # 收集各公司的选择性读取结果
for order_book_id in order_book_ids:  # 逐只股票避免载入全市场大文件
    company_prices = pd.read_hdf(price_data_path, key='data', where=f'order_book_id == "{order_book_id}"', columns=['order_book_id', 'date', 'close']).reset_index()  # 将 HDF 索引恢复为公司与日期列
    price_frames.append(company_prices)  # 保存当前公司行情
local_prices = pd.concat(price_frames, ignore_index=True)  # 合并四只股票的行情
local_prices = local_prices.query("'2020-01-01' <= date <= '2024-12-31'")  # 固定教学样本期
close_prices = local_prices.pivot(index='date', columns='order_book_id', values='close').sort_index().dropna()  # 对齐共同交易日
returns = np.log(close_prices / close_prices.shift(1)).dropna()  # 计算前复权对数收益率
X_df = returns[['002415.XSHE', '600009.XSHG', '603259.XSHG']].copy()  # 使用同日市场信息构造特征
X_df.columns = ['hikvision_return', 'shanghai_airport_return', 'wuxi_apptec_return']  # 使用业务含义明确的列名
Y = returns['600276.XSHG'].shift(-1).dropna().to_numpy()  # 将恒瑞下一交易日收益作为目标
X_df = X_df.iloc[:-1]  # 与向前移动后的目标保持日期对齐
X = X_df.to_numpy()  # 转为后续模型所需数组
X_df.head()  # 核对公开数据与列顺序
计算得到的每日对数收益率
hikvision_return shanghai_airport_return wuxi_apptec_return
date
2020-01-03 0.007639 -0.000776 -0.026096
2020-01-06 0.006998 -0.022238 -0.030252
2020-01-07 -0.000290 -0.004639 0.015014
2020-01-08 -0.009347 -0.002529 -0.006277
2020-01-09 0.016589 0.006640 0.044114

常见口径:逐特征缩放使统一惩罚可比

正则化是基于系数的大小进行惩罚的。如果特征的量纲(单位或范围)不同,惩罚就会不公平。

  • 常见做法:量纲不同且使用统一 L1/L2 惩罚时,逐特征标准化可建立可比惩罚口径,并且只在训练折拟合。
  • 边界:若尺度有预先定义的经济含义,或采用惩罚因子、组惩罚及特殊特征类型,不应无条件套用同一标准化。

步骤2:数据标准化与训练/测试集划分

我们先按时间划分训练集和测试集,再让 StandardScaler 只由训练样本逐特征估计均值与标准差,并用确定统计量转换未来测试集。

代码
# 为“步骤2:数据标准化与训练/测试集划分”,导入 `StandardScaler`,供训练期按特征列估计均值与尺度后外推到未来测试期。
from sklearn.preprocessing import StandardScaler

# 检查输入样本非空
# 当任一输入为空时立即拒绝拟合,避免在空样本上估计系数。
if len(X) == 0 or len(Y) == 0:
    # 主动抛出数据说明错误,阻止缺列样本继续进入模型。
    raise ValueError("No data available for training")

split_index = int(len(X) * 0.7)  # 按日期确定训练期终点
X_train, X_test = X[:split_index], X[split_index:]  # 保持过去训练、未来测试
y_train, y_test = Y[:split_index], Y[split_index:]  # 对齐时间有序目标
# 建立缩放器;其统计量将在下一步只由时间靠前的训练样本按特征列估计。
scaler = StandardScaler()
# 逐特征估计时间靠前训练样本的均值与标准差,并转换训练特征。
X_train_scaled = scaler.fit_transform(X_train)
# 使用确定的训练样本逐特征统计量转换未来测试特征。
X_test_scaled = scaler.transform(X_test)

# 报告“训练集大小: {X_train_scaled.shape}”中的 `X_train_scaled`,核对“步骤2:数据标准化与训练/测试集划分”的样本形状。
print(f'训练集大小: {X_train_scaled.shape}')
# 报告“测试集大小: {X_test_scaled.shape}”中的 `X_test_scaled`,核对“步骤2:数据标准化与训练/测试集划分”的样本形状。
print(f'测试集大小: {X_test_scaled.shape}')
训练集大小: (847, 3)
测试集大小: (363, 3)

步骤3:岭回归系数路径

我们来看看随着惩罚参数 alpha (\(\lambda\)) 的变化,岭回归的系数是如何变化的。

代码
# 为“步骤3:岭回归系数路径”,从 `sklearn.linear_model` 导入`Ridge` 用于拟合带 L2 惩罚的线性回归。
from sklearn.linear_model import Ridge

# 建立 `alphas` 的有序取值网格,用于展示“岭回归系数随惩罚强度(alpha)的变化路径”随参数变化的比较结果。
alphas = 10**np.linspace(4, -2, 100)
# 初始化岭回归系数路径容器,按 alpha 顺序保存每个特征的收缩比较结果。
coefs = []

# 遍历 `a in alphas` 的候选超参数,逐个拟合并记录“步骤3:岭回归系数路径”的评价量。
for a in alphas:
    # 创建 `Ridge` 实例 `ridge`,用于拟合带 L2 惩罚的线性回归。
    ridge = Ridge(alpha=a, fit_intercept=True)
    # 在标准化训练集上拟合当前 alpha 的岭回归,估计各特征的 L2 收缩系数。
    ridge.fit(X_train_scaled, y_train)
    # 把 `ridge.coef_` 加入 `coefs`,形成惩罚强度与系数向量一一对应的路径。
    coefs.append(ridge.coef_)

# 创建 `fig, ax` 画布,承载“岭回归系数随惩罚强度(alpha)的变化路径”的并排视觉比较。
fig, ax = plt.subplots(figsize=(10, 6))
feature_labels = ['海康威视', '上海机场', '药明康德']
line_styles = ['-', '--', '-.']
for feature_position, feature_label in enumerate(feature_labels):
    ax.plot(alphas, np.asarray(coefs)[:, feature_position], linestyle=line_styles[feature_position], linewidth=2.4, label=feature_label)
# 将 `ax` 的横轴切换到代码指定的尺度。
ax.set_xscale('log')
# 将横轴标为“Alpha (惩罚强度)”,明确横向编码的变量。
ax.set_xlabel('Alpha (惩罚强度)', fontsize=12)
# 将纵轴标为“系数大小 (Coefficients)”,明确纵向编码的变量。
ax.set_ylabel('系数大小 (Coefficients)', fontsize=12)
# 将图题设为“岭回归系数路径”,直接说明当前图形的比较目的。
ax.set_title('岭回归系数路径', fontsize=16)
ax.legend()
# 反转当前横轴的显示方向。
plt.gca().invert_xaxis()
# 为“步骤3:岭回归系数路径”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(True, which="both", ls="--")
# 显示岭路径,从运行图读取具体形状,不预设每个坐标的绝对值单调。
plt.show()
横轴为对数惩罚强度 alpha、纵轴为各特征系数;整体惩罚增强使解受到更强收缩,单个坐标的绝对值不保证单调。
图 5: 岭回归系数随惩罚强度(alpha)的变化路径

岭回归路径图解读

  • 整体惩罚增强会使解受到更强收缩,但一般相关设计下,单个坐标的绝对值不保证单调,可能变号。
  • 当前路径形状必须从运行图读取;有限惩罚下 Ridge 通常不精确为 0,但特殊数据关系或数值阈值可出现零。

步骤4:Lasso回归系数路径

现在我们对Lasso做同样的操作,观察其系数路径。

代码
# 为“步骤4:Lasso回归系数路径”,从 `sklearn.linear_model` 导入`Lasso` 用于拟合带 L1 惩罚的线性回归。
from sklearn.linear_model import Lasso
# 导入绘图接口,在当前文档中生成 Lasso 系数路径。
import matplotlib.pyplot as plt

# 建立 `alphas_lasso` 的有序取值网格,用于展示“Lasso回归系数随惩罚强度(alpha)的变化路径”随参数变化的比较结果。
alphas_lasso = 10**np.linspace(-2, -5, 100)
# 初始化 Lasso 系数路径容器,按 alpha 顺序保存稀疏化比较结果。
coefs_lasso = []

# 遍历 `a in alphas_lasso` 的候选超参数,逐个拟合并记录“步骤4:Lasso回归系数路径”的评价量。
for a in alphas_lasso:
    # 创建 `Lasso` 实例 `lasso`,用于拟合带 L1 惩罚的线性回归。
    lasso = Lasso(alpha=a, fit_intercept=True)
    # 在标准化训练集上拟合当前 alpha 的 Lasso,估计可能精确归零的 L1 系数。
    lasso.fit(X_train_scaled, y_train)
    # 把 `lasso.coef_` 加入 `coefs_lasso`,形成惩罚强度与系数向量一一对应的路径。
    coefs_lasso.append(lasso.coef_)

# 创建 `fig, ax` 画布,承载“Lasso回归系数随惩罚强度(alpha)的变化路径”的并排视觉比较。
fig, ax = plt.subplots(figsize=(10, 6))
# 使用与岭回归相同的中文公司名和线型,便于两页对照。
for feature_position, feature_label in enumerate(feature_labels):
    ax.plot(alphas_lasso, np.asarray(coefs_lasso)[:, feature_position], linestyle=line_styles[feature_position], linewidth=2.4, label=feature_label)
# 将 `ax` 的横轴切换到代码指定的尺度。
ax.set_xscale('log')
# 将横轴标为“Alpha (惩罚强度)”,明确横向编码的变量。
ax.set_xlabel('Alpha (惩罚强度)', fontsize=12)
# 将纵轴标为“系数大小 (Coefficients)”,明确纵向编码的变量。
ax.set_ylabel('系数大小 (Coefficients)', fontsize=12)
# 将图题设为“Lasso回归系数路径”,直接说明当前图形的比较目的。
ax.set_title('Lasso回归系数路径', fontsize=16)
ax.legend()
# 反转当前横轴的显示方向。
plt.gca().invert_xaxis()
# 为“步骤4:Lasso回归系数路径”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(True, which="both", ls="--")
# 显示 Lasso 路径,从运行图读取坐标进入、退出、变号或归零的实际顺序。
plt.show()
横轴为对数惩罚强度 alpha、纵轴为各特征系数;L1 路径可在有限 alpha 下出现零,单个坐标也可能进入、退出或变号。
图 6: Lasso回归系数随惩罚强度(alpha)的变化路径

Lasso路径图解读:特征选择的威力

  • L1 路径可在有限 alpha 下产生精确零;一般相关设计中,单个坐标不保证单调,可能进入、退出或变号。
  • 在当前 A 股样本中,哪些系数先变为 0 必须从上图和运行输出读取,不能沿用美股/债券案例的固定叙述。
  • 如果三个系数同时接近 0,应解释为“这组三个同日收益对恒瑞下一日收益缺少稳定增量信号”,而不是强行宣称完成了有效选股。

步骤5:如何选择候选 alpha?使用开发窗交叉验证

我们不能凭感觉选择 alpha。科学的方法是使用交叉验证 (Cross-Validation)

sklearn 提供 LassoCVRidgeCV;它们按指定开发折与指标选择候选 alpha,不能把最终测试期纳入选择。

代码
from sklearn.model_selection import GridSearchCV, TimeSeriesSplit  # 建立训练期扩展窗口验证
from sklearn.pipeline import Pipeline  # 将缩放约束在每个验证折的训练窗口内

lasso_pipeline = Pipeline([('scaler', StandardScaler()), ('lasso', Lasso(max_iter=10000))])  # 每个时间训练折逐特征估计缩放统计量并拟合 Lasso
time_series_split = TimeSeriesSplit(n_splits=5)  # 保证每折训练观测早于验证观测
lasso_cv = GridSearchCV(lasso_pipeline, {'lasso__alpha': alphas_lasso}, cv=time_series_split, scoring='neg_mean_squared_error')  # 只用训练期选alpha
lasso_cv.fit(X_train, y_train)  # 在训练期扩展窗口完成选择并重拟合
best_alpha = lasso_cv.best_params_['lasso__alpha']  # 读取训练期验证选择的惩罚强度
print(f'扩展窗口验证找到的最优 alpha: {best_alpha:.6f}')  # 报告选择结果
best_coefs = pd.Series(lasso_cv.best_estimator_.named_steps['lasso'].coef_, index=X_df.columns)  # 使用真实特征名标注系数
# 标出交叉验证所选 alpha 对应的 Lasso 系数。
print('\n最优alpha下的系数:')
# 输出交叉验证所选 alpha 对应的系数,检查正则化后保留的变量。
print(best_coefs)
扩展窗口验证找到的最优 alpha: 0.001874

最优alpha下的系数:
hikvision_return           0.0
shanghai_airport_return    0.0
wuxi_apptec_return         0.0
dtype: float64

步骤6:评估最终模型的性能

  • 确定模型:只使用开发期选定 alpha
  • 最终评估:在从未见过的测试集上预测,与 OLS、零收益和训练均值基线同窗比较。
  • 隔离原则:最终测试行不再用于调参或重拟合。

不确定性口径

  • 方法:5,000 次配对行 bootstrap;每次共同重抽同一测试行的各模型平方误差差,不重新拟合模型。
  • 可解释条件:测试行可交换。
  • 时序依赖边界:若该假设不可信,本例只报告误差差异;下一项研究应事先选择 block bootstrap 或 HAC/Diebold–Mariano 方法。
代码
from sklearn.metrics import mean_squared_error  # 计算各确定候选在同一最终测试切片的均方误差
from sklearn.linear_model import LinearRegression  # 显式导入 OLS 基准,使当前文档可直接运行
y_pred_test = lasso_cv.predict(X_test)  # 使用开发期确定的完整Pipeline预测最终测试行
ols = LinearRegression().fit(X_train_scaled, y_train)  # 仅用训练期拟合未正则化线性基准
y_pred_ols = ols.predict(X_test_scaled)  # 对同一最终测试行生成OLS基准预测
zero_predictions = np.zeros(len(y_test))  # 事先确定零收益作为金融预测朴素基线
mean_predictions = np.repeat(y_train.mean(), len(y_test))  # 事先确定训练期均值作为数据驱动朴素基线
sealed_predictions = {'OLS': y_pred_ols, '零收益': zero_predictions, '训练均值': mean_predictions}  # 确定三条对照预测
lasso_squared_errors = np.square(y_test - y_pred_test)  # 保存Lasso逐行平方误差供配对比较
paired_differences = pd.DataFrame({name: lasso_squared_errors - np.square(y_test - predictions) for name, predictions in sealed_predictions.items()})  # 计算Lasso减基线的逐行误差差
bootstrap_rng = np.random.default_rng(20260829)  # 固定不确定性程序的随机种子
bootstrap_indices = bootstrap_rng.integers(0, len(y_test), size=(5000, len(y_test)))  # 在行可交换假设下配对重抽测试行
bootstrap_means = paired_differences.to_numpy()[bootstrap_indices].mean(axis=1)  # 不重拟合地计算每次平均误差差
bootstrap_intervals = np.quantile(bootstrap_means, [0.025, 0.975], axis=0)  # 形成逐基线95%百分位区间
lasso_mse = mean_squared_error(y_test, y_pred_test)  # 固定Lasso在最终测试切片上的点估计
baseline_mses = [mean_squared_error(y_test, predictions) for predictions in sealed_predictions.values()]  # 固定三条事先确定基线的同窗点估计
audit_table = pd.DataFrame({'基线': list(sealed_predictions), 'Lasso MSE': lasso_mse, '基线 MSE': baseline_mses, '差': paired_differences.mean().to_numpy(), '95%区间': [f'[{lo:.8f}, {hi:.8f}]' for lo, hi in zip(bootstrap_intervals[0], bootstrap_intervals[1])]})  # 每条基线一行汇总点估计与配对区间
display(audit_table.round(8))  # 负差代表Lasso误差更低;单表保留完整检查证据
has_conditional_increment = bool((bootstrap_intervals[1] < 0).all())  # 只有所有区间上界均低于零才通过预设增量门槛
sealed_conclusion = '条件性增量证据:在行可交换假设下,Lasso相对三条基线的区间均低于0;不据此重选模型。' if has_conditional_increment else 'no-increment / no-deploy:当前最终测试切片未同时越过三条基线门槛;保留失败结果且不重开测试。'  # 确定通过与失败两条路径
print(sealed_conclusion)  # 报告与事先确定门槛一致的条件性结论
基线 Lasso MSE 基线 MSE 95%区间
0 OLS 0.000399 0.000399 5.000000e-08 [-0.00000399, 0.00000429]
1 零收益 0.000399 0.000399 -3.000000e-08 [-0.00000090, 0.00000085]
2 训练均值 0.000399 0.000399 0.000000e+00 [0.00000000, 0.00000000]
no-increment / no-deploy:当前最终测试切片未同时越过三条基线门槛;保留失败结果且不重开测试。

逻辑回归中的正则化

我们刚刚讨论的正则化思想,完全可以应用于其他模型,例如逻辑回归 (Logistic Regression)

  • 目标: 逻辑回归的代价函数是对数损失函数 (Log Loss),我们同样可以在其后加上L1或L2惩罚项。

  • 加入岭回归 (L2) 正则化项:

    \[ \large{J(\beta) = -\frac{1}{n} \sum_{i=1}^{n} \left[ y_i\log(p_i) + (1-y_i)\log(1-p_i) \right] + \frac{\lambda}{2} \sum_{j=1}^{p} \beta_j^2} \]

  • 加入套索回归 (L1) 正则化项:

    \[ \large{J(\beta) = -\frac{1}{n} \sum_{i=1}^{n} \left[ \dots \right] + \lambda \sum_{j=1}^{p} |\beta_j|} \]

  • 惩罚类型penalty 可设为 'l1''l2'

  • 强度参数C 表示逆正则化强度,越小则惩罚越强。

  • 换算边界C 与讲义 \(\lambda\) 的精确换算取决于损失归一化和 solver 约定。

习题解答与讨论

现在我们来解决讲义中的习题,巩固今天学到的知识。

检查:系数收缩与变量选择

: 哪种正则化方法能达到类似于模型选择的方法?

反馈:L1 可产生精确零系数

: 套索回归 (Lasso Regression)

L1 惩罚可产生精确零与候选稀疏集,形式上类似变量筛选;但零/非零依赖缩放、惩罚、样本与相关结构,不识别真实重要性或因果作用。

知识理解题 2

: 提高正则化超参数值 \(\lambda\) 对于模型拟合中的哪种问题有帮助?

答案:知识理解题 2

: 增大 \(\lambda\) 可能降低估计方差,同时增加偏差;样本外风险可能改善,也可能恶化,不能保证解决过拟合或提升泛化。

  • 决策规则:只在开发窗按预设指标选择 \(\lambda\),确定后在最终测试期评估一次。

知识理解题 3

: 阐述 \(\lambda\) 值对模型参数 \(\beta\) 的影响。以及,如果 \(\lambda > 0\) 会如何影响 \(\beta\) 的可解释性。

答案:知识理解题 3

  1. \(\beta\) 的影响: \(\lambda\) 的值控制着对参数 \(\beta\)收缩 (shrinkage) 程度。
    • \(\lambda=0\) 时,没有惩罚,参数 \(\beta\) 是普通最小二乘的解。
    • 整体惩罚增强使解受到更强收缩;除正交等特殊设计外,单个坐标的绝对值不保证单调,可能进入、退出或变号。
  2. 对可解释性的影响: 当 \(\lambda > 0\) 时,正则化可能会降低单个参数 \(\beta\) 的直接可解释性
    • 在给定线性规格下,未惩罚系数首先是控制其他变量后的条件关联;只有额外识别假设成立才可作因果边际效应。
    • 正则化系数相对未惩罚目标引入收缩偏差,以换取可能的方差下降;是否改善样本外风险仍须验证。
    • Lasso 的稀疏表示可减少当前预测模型中的活跃坐标,但不能据此认定哪些变量“真正重要”或具有因果作用;相关特征下还须报告选择稳定性。

程序操作题:使用套索回归预测下一交易日收益率

要求:使用本章公开 A 股收益数据训练套索回归。将 \(\lambda\)(alpha)设为 0.0010.0050.01,记录各特征是否被压缩为 0,并解释为什么收益率尺度下较小的 alpha 已可能产生强收缩。

练习直接复用前面从公开 HDF5 读取并按时间切分的数据;不得用模拟数据替换。

练习:先完成再查看答案:先最终测试 alpha 比较结果

提交全部事先确定 alpha 的逐窗 MSE、零收益基线、非零系数数目和确定选择;不得据最终测试改选。若无候选稳定胜基线,必须给出暂不采用结论。

程序操作题:设置不同的 alpha

  • 候选集:3 个训练期扩展窗 × 3 个 alpha。
  • 进入门槛:每窗均胜零收益基线。
  • 确定规则:平均验证 MSE 最低;1e-12 内并列取较小 alpha。
  • 失败路径:无候选过门槛则 保留简单模型
  • 最终测试:不参与选择,仅按确定规格评估一次。
代码
alphas_to_test = [0.001, 0.005, 0.01]  # 固定题目要求的惩罚强度
exercise_feature_names = X_df.columns.tolist()  # 保留与模型列顺序一致的业务特征名
zero_tolerance = 1e-10  # 预先声明数值归零容差,避免把浮点噪声误判为活跃特征
exercise_rows = []  # 保存各扩展窗口的验证证据
exercise_splitter = TimeSeriesSplit(n_splits=3)  # 生成三个过去训练、未来验证窗口
for alpha_value in alphas_to_test:  # 对每个候选值做相同验证
    for window_id, (train_indices, validation_indices) in enumerate(exercise_splitter.split(X_train), start=1):  # 遍历扩展窗口
        exercise_model = Pipeline([('scaler', StandardScaler()), ('lasso', Lasso(alpha=alpha_value, max_iter=10000))])  # 只由当前训练窗逐特征估计均值与标准差
        exercise_model.fit(X_train[train_indices], y_train[train_indices])  # 仅用当前窗口较早数据拟合
        validation_predictions = exercise_model.predict(X_train[validation_indices])  # 预测随后验证窗
        validation_mse = mean_squared_error(y_train[validation_indices], validation_predictions)  # 计算窗口验证误差
        zero_baseline_mse = mean_squared_error(y_train[validation_indices], np.zeros(len(validation_indices)))  # 计算同期零收益基准
        fitted_coefficients = exercise_model.named_steps['lasso'].coef_  # 提取与业务特征顺序一致的折内系数
        coefficient_fields = {f'{feature_name}_coef': coefficient_value for feature_name, coefficient_value in zip(exercise_feature_names, fitted_coefficients)}  # 保存逐特征数值系数
        zero_state_fields = {f'{feature_name}_zero': bool(abs(coefficient_value) <= zero_tolerance) for feature_name, coefficient_value in zip(exercise_feature_names, fitted_coefficients)}  # 按确定容差标记逐特征零状态
        nonzero_count = int(np.count_nonzero(np.abs(fitted_coefficients) > zero_tolerance))  # 按同一容差汇总活跃特征数
        exercise_rows.append({'alpha': alpha_value, '窗口': window_id, '验证MSE': validation_mse, '零收益MSE': zero_baseline_mse, '非零特征数': nonzero_count, **coefficient_fields, **zero_state_fields})  # 保存完整逐窗答案
exercise_results = pd.DataFrame(exercise_rows)  # 整理为可检查表格
exercise_coefficient_delivery = exercise_results[['alpha', '窗口', 'hikvision_return_coef', 'hikvision_return_zero', 'shanghai_airport_return_coef', 'shanghai_airport_return_zero', 'wuxi_apptec_return_coef', 'wuxi_apptec_return_zero']].rename(columns={'hikvision_return_coef': '海康系数', 'hikvision_return_zero': '海康为零', 'shanghai_airport_return_coef': '机场系数', 'shanghai_airport_return_zero': '机场为零', 'wuxi_apptec_return_coef': '药明系数', 'wuxi_apptec_return_zero': '药明为零'})  # 用紧凑列名完成内容逐特征系数与零状态
display(exercise_coefficient_delivery.round(8))  # 输出九个 alpha×窗口的逐特征答案
表 1: 不同 Alpha 与扩展窗口下的逐特征系数和归零状态
alpha 窗口 海康系数 海康为零 机场系数 机场为零 药明系数 药明为零
0 0.001 1 0.0 True 0.0 True -0.000891 False
1 0.001 2 -0.0 True -0.0 True -0.000000 True
2 0.001 3 0.0 True 0.0 True 0.000000 True
3 0.005 1 -0.0 True 0.0 True -0.000000 True
4 0.005 2 -0.0 True -0.0 True -0.000000 True
5 0.005 3 0.0 True 0.0 True 0.000000 True
6 0.010 1 -0.0 True 0.0 True -0.000000 True
7 0.010 2 -0.0 True -0.0 True -0.000000 True
8 0.010 3 0.0 True 0.0 True 0.000000 True

程序操作题:确定选择与暂不采用路径

代码
exercise_summary = exercise_results.groupby('alpha').agg(mean_validation_mse=('验证MSE', 'mean'), worst_validation_mse=('验证MSE', 'max'), mean_zero_baseline_mse=('零收益MSE', 'mean'), all_windows_beat_baseline=('验证MSE', lambda validation_errors: bool((validation_errors.to_numpy() < exercise_results.loc[validation_errors.index, '零收益MSE'].to_numpy()).all())), nonzero_count_min=('非零特征数', 'min'), nonzero_count_max=('非零特征数', 'max')).reset_index()  # 汇总误差、基线和选择稳定性
eligible_exercise_candidates = exercise_summary.query('all_windows_beat_baseline').copy()  # 只保留每窗均胜基线的候选
best_exercise_mse = np.nan if eligible_exercise_candidates.empty else eligible_exercise_candidates['mean_validation_mse'].min()  # 找到完成要求候选的最低平均误差
near_best_exercise_candidates = eligible_exercise_candidates.query('mean_validation_mse <= @best_exercise_mse + 1e-12') if not eligible_exercise_candidates.empty else eligible_exercise_candidates  # 按事先确定容差识别并列候选
frozen_exercise_alpha = None if near_best_exercise_candidates.empty else float(near_best_exercise_candidates['alpha'].min())  # 并列时动态确定较小 alpha
exercise_decision = '保留简单模型' if frozen_exercise_alpha is None else f'确定 alpha={frozen_exercise_alpha:g}'  # 输出暂不采用或确定结论
exercise_summary_delivery = exercise_summary.assign(非零数范围=exercise_summary['nonzero_count_min'].astype(str) + '–' + exercise_summary['nonzero_count_max'].astype(str)).rename(columns={'mean_validation_mse': '平均MSE', 'worst_validation_mse': '最差MSE', 'mean_zero_baseline_mse': '平均零基线', 'all_windows_beat_baseline': '每窗胜基线'})[['alpha', '平均MSE', '最差MSE', '平均零基线', '每窗胜基线', '非零数范围']]  # 用六列紧凑完成内容避免宽表截断
display(exercise_summary_delivery.round(8))  # 展示三项候选的误差、基线与非零数范围
print({'tie_break': '平均MSE在1e-12内并列时取较小alpha', 'decision': exercise_decision})  # 报告事先确定并列规则与动态决策
{'tie_break': '平均MSE在1e-12内并列时取较小alpha', 'decision': '保留简单模型'}
表 2: 事先确定规则下的 Alpha 汇总与确定决策
alpha 平均MSE 最差MSE 平均零基线 每窗胜基线 非零数范围
0 0.001 0.000524 0.000621 0.000522 False 0–1
1 0.005 0.000523 0.000618 0.000522 False 0–0
2 0.010 0.000523 0.000618 0.000522 False 0–0

程序操作题:结果解读

  • 读取证据:从 表 1 比较各 alpha 在三个相邻扩展窗口中的非零特征数与验证 MSE。
  • 解释归零:若全部归零且误差接近零收益基准,则当前特征没有稳定的下一日增量信号。
  • 禁止误读:稀疏性本身不等于预测成功。

完整判断标准

  • 系数证据:每个 alpha 的逐特征系数与零状态。
  • 误差证据:平均/最差窗 MSE、同期零收益基准和跨窗口选择稳定性。
  • 确定决策:按上一页规则确定 alpha,或输出 保留简单模型
  • 证据边界:只有开发期确定规格后才可一次性评估最终测试;系数非零不是因果证据。

本章小结

  1. 核心冲突: 建模的根本挑战在于偏差-方差的权衡。我们的目标是最小化总预测误差,而非仅仅拟合训练数据。
  2. 过拟合是敌人: 过于复杂的模型会学习到噪声,导致样本外预测能力差。
  3. 两大策略:
    • 模型选择 (如前向分步法) 通过增减特征来控制复杂度。
    • 正则化 (岭、套索、弹性网络) 通过对系数大小施加惩罚来控制复杂度。
  4. Lasso vs. Ridge: L1 可产生候选稀疏集,但不识别真实重要性或因果;L2 常对相关预测变量作整体收缩,不以稀疏化为目标。
  5. 实践要点: 量纲不同且使用统一惩罚时,逐特征标准化是常见可比口径,并且只在训练折拟合;\(\lambda\) 只由开发窗选择。

学完后应能

  • 在开发窗比较惩罚强度,报告零收益基线与非零系数稳定性。
  • 识别失败条件:空窗、最终测试集参与选择,或只凭稀疏性宣称成功。
  • 衔接第 8 章:把线性惩罚换成分裂与剪枝,但继续沿用第 6 章的开发—最终测试说明。