本章会用到的数据
公开下载: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_id、date、close,先按日期对齐再生成收益。
- 选择边界:惩罚强度只在开发期扩展窗口选择;最终测试期只开一次,并与 OLS、零收益和训练均值基线同窗比较。
- 决策边界:复杂候选若未同时越过事先确定基线,结果就是
no-increment / no-deploy;失败证据必须保留。
核心警示: 训练期拟合和稀疏系数都不是实际使用证据;只有确定规则下的时间外增量比较能够支持当前样本的候选决策。
案例边界:全程离线读取中国公开行情;测试表不得反馈到选模。
核心问题:为何复杂的金融模型常常预测失败?
我们经常看到拥有数百个变量的复杂计量经济学模型。
- 直觉上:包含的变量越多,信息就越丰富,模型应该越“准确”。
- 实际上:过于复杂的模型在样本外(out-of-sample)的预测中,表现往往非常糟糕,甚至不如简单模型。
本章的核心,就是解决这个理论与现实之间的矛盾。
本讲座的学习目标
理论层面
- 深刻理解:为什么我们需要在模型复杂度和预测能力之间做出权衡(偏差-方差的权衡)。
- 掌握模型选择的方法:学会如何从众多候选模型中,科学地挑选出“最优”模型。
技术层面
- 掌握正则化的核心思想与方法:学会使用岭回归、套索回归等技术,来“惩罚”不必要的模型复杂度。
- 具备实践能力:能够使用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 形。
- 变化方向和最小点必须由给定候选集与开发窗验证,不能由示意图预设。
过拟合:模型“记住”了噪声而非规律
过拟合 (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()
第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, "欠拟合 (高偏差)")
第3步:“恰到好处”的模型(3阶)
一个3阶多项式模型较好地捕捉了数据的真实规律。这是偏差和方差较好平衡的表现。
代码
# 拟合并绘制当前阶数的多项式,比较欠拟合、适度拟合与过拟合。
plot_poly_fit(3, X_sample, y_sample, X_true, y_true, "良好拟合")
第4步:复杂模型(15阶)的过拟合
一个15阶多项式模型为了穿过每一个数据点而剧烈扭曲。它学习了噪声。这是高方差的表现。
代码
# 拟合并绘制当前阶数的多项式,比较欠拟合、适度拟合与过拟合。
plot_poly_fit(15, X_sample, y_sample, X_true, y_true, "过拟合 (高方差)")
我们的武器:模型选择与正则化
为了避免过拟合,找到最佳的模型复杂度,我们有两种主要策略:
- 模型选择 (Model Selection):
- 也称为特征选择 (Feature Selection)。
- 从一个大的特征集合中,挑选出一个子集来构建模型。
- 是一种“硬”选择,要么保留特征,要么丢弃。
- 正则化 (Regularization):
- 使用所有特征,但在模型训练时对系数的大小施加“惩罚”。
- 是一种“软”选择,通过压缩系数来降低模型的有效复杂度。
第一部分:模型选择方法
核心思想:我们有很多备选的预测变量(特征),如何挑选出一个“最优”的组合?
- 例子:在预测一只股票的收益时,我们可能有上百个备选因子(公司规模、估值、动量、宏观经济指标等)。把所有因子都放进模型可能导致过拟合。
我们将介绍两种主流的自动化选择方法。
模型选择方法 1:最佳子集选择法
最佳子集选择法 (Best Subset Selection) 是一种暴力但彻底的方法。
算法步骤:
设 \(M^{(0)}\) 为不含任何特征的“零模型”(只有截距项)。
对于 \(k = 1, 2, \dots, p\) (其中 \(p\) 是总特征数):
- 拟合所有包含 \(k\) 个特征的组合模型。总共有 \(\binom{p}{k}\) 个。
- 在这 \(\binom{p}{k}\) 个模型中,根据某种准则(如RSS最低)选出最佳模型,记为 \(M^{(k)}\)。
我们现在有 \(p+1\) 个候选模型:\(M^{(0)}, M^{(1)}, \dots, M^{(p)}\)。
使用交叉验证 (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) 是一种更高效的“贪心”算法。
算法步骤:
设 \(M^{(0)}\) 为不含任何特征的“零模型”。
对于 \(k = 1, 2, \dots, p\):
- 以 \(M^{(k-1)}\) 为基础,尝试加入一个尚未被包含的特征。
- 在所有可能的 \((p-k+1)\) 个新模型中,选择那个提升最大(如RSS下降最多)的模型,记为 \(M^{(k)}\)。
我们得到一个由 \(p+1\) 个模型组成的序列:\(M^{(0)}, M^{(1)}, \dots, M^{(p)}\)。
使用交叉验证或信息准则在序列中选出最终模型。
前向分步算法的优缺点
- 优点: 计算效率极高。它只需要评估 \(1 + \sum_{k=1}^{p}(p-k+1) = 1 + \frac{p(p+1)}{2}\) 个模型,远小于 \(2^p\)。
- 缺点: 它是一种“贪心”算法,不能保证找到全局最优解。因为它每一步做出的都是局部最优选择,一旦一个变量被选入模型,就再也不会被移除。
补充:后向分步算法
与前向分步相反,后向分步算法 (Backward Stepwise Selection) 从完整模型开始,逐步移除使当前选择准则恶化最少的特征。
算法步骤:
设 \(M^{(p)}\) 为包含所有 \(p\) 个特征的完整模型。
对于 \(k = p, p-1, \dots, 1\):
- 以 \(M^{(k)}\) 为基础,尝试移除一个特征。
- 在所有可能的 \(k\) 个新模型中,选择那个性能下降最小(如RSS增加最少)的模型,记为 \(M^{(k-1)}\)。
同样,我们得到一个模型序列,再从中选择最优。
如何在候选模型中做出最终选择?
前向/后向分步算法都为我们提供了一个模型序列 (\(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\);不能由示意图或训练拟合预设“最优”平衡。
岭回归的几何解释
岭回归的解可以看作是两个几何形状的交点:
- 损失函数等高线 (RSS Contours): 在系数空间中,这些是以OLS解为中心的一系列椭圆。
- L2惩罚约束: \(\sum \beta_j^2 \le C\)。在二维空间中,这是一个以原点为中心的圆形区域。
岭回归的解就是椭圆等高线与圆形区域首次相切的点。
套索回归 (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的解同样可以看作是两个几何形状的交点:
- 损失函数等高线 (RSS Contours): 同样是以OLS解为中心的一系列椭圆。
- L1惩罚约束: \(\sum |\beta_j| \le C\)。在二维空间中,这是一个菱形 (diamond) 区域。
Lasso的解就是椭圆等高线与菱形区域首次相交的点。
几何解释:为什么Lasso能产生稀疏解?
- 由于L1约束区域是一个带有尖角的菱形,损失函数的椭圆等高线很大概率会首先与菱形的一个顶点相交。
- 这些顶点恰好位于坐标轴上,意味着其中一个(或多个)系数为0。
- 相比之下,L2 约束边界光滑,有限惩罚下通常不产生精确零;特殊数据关系或数值阈值是例外。
岭回归 vs. 套索回归:总结
| 惩罚项 |
系数平方和 (\(\sum \beta_j^2\)) |
系数绝对值和 (\(\sum |\beta_j|\)) |
| 系数 |
有限惩罚下通常不精确为 0,不以稀疏化为目标 |
可产生精确 0,但零/非零依赖数据与惩罚口径 |
| 变量筛选 |
不以稀疏筛选为目标 |
形成候选稀疏集,不等于真实重要性或因果识别 |
| 适用场景 |
相关预测变量较多、希望整体收缩 |
预测任务中希望候选稀疏且会检查选择稳定性 |
| 相关特征 |
常整体收缩,稳定性仍须实测 |
入选集合可能不稳定,须做重抽样或跨窗检查 |
弹性网络 (Elastic Net): 两全其美
弹性网络 (Elastic Net) 同时结合了L1和L2惩罚项,试图集两家之长。
为何需要弹性网络?
弹性网络在某些情况下优于Lasso:
- 处理高度相关的特征: 在某些设计和调参取值下,弹性网络可能比 Lasso 更明显地呈现分组选择倾向;这不是保证,仍须检查跨窗入选稳定性。
- \(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() # 核对公开数据与列顺序
计算得到的每日对数收益率
| 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()
岭回归路径图解读
- 整体惩罚增强会使解受到更强收缩,但一般相关设计下,单个坐标的绝对值不保证单调,可能变号。
- 当前路径形状必须从运行图读取;有限惩罚下 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()
Lasso路径图解读:特征选择的威力
- L1 路径可在有限
alpha 下产生精确零;一般相关设计中,单个坐标不保证单调,可能进入、退出或变号。
- 在当前 A 股样本中,哪些系数先变为 0 必须从上图和运行输出读取,不能沿用美股/债券案例的固定叙述。
- 如果三个系数同时接近 0,应解释为“这组三个同日收益对恒瑞下一日收益缺少稳定增量信号”,而不是强行宣称完成了有效选股。
步骤5:如何选择候选 alpha?使用开发窗交叉验证
我们不能凭感觉选择 alpha。科学的方法是使用交叉验证 (Cross-Validation)。
sklearn 提供 LassoCV 和 RidgeCV;它们按指定开发折与指标选择候选 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) # 报告与事先确定门槛一致的条件性结论
| 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
- 对 \(\beta\) 的影响: \(\lambda\) 的值控制着对参数 \(\beta\) 的收缩 (shrinkage) 程度。
- 当 \(\lambda=0\) 时,没有惩罚,参数 \(\beta\) 是普通最小二乘的解。
- 整体惩罚增强使解受到更强收缩;除正交等特殊设计外,单个坐标的绝对值不保证单调,可能进入、退出或变号。
- 对可解释性的影响: 当 \(\lambda > 0\) 时,正则化可能会降低单个参数 \(\beta\) 的直接可解释性。
- 在给定线性规格下,未惩罚系数首先是控制其他变量后的条件关联;只有额外识别假设成立才可作因果边际效应。
- 正则化系数相对未惩罚目标引入收缩偏差,以换取可能的方差下降;是否改善样本外风险仍须验证。
- Lasso 的稀疏表示可减少当前预测模型中的活跃坐标,但不能据此认定哪些变量“真正重要”或具有因果作用;相关特征下还须报告选择稳定性。
程序操作题:使用套索回归预测下一交易日收益率
要求:使用本章公开 A 股收益数据训练套索回归。将 \(\lambda\)(alpha)设为 0.001、0.005、0.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×窗口的逐特征答案
程序操作题:确定选择与暂不采用路径
代码
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': '保留简单模型'}
程序操作题:结果解读
- 读取证据:从 表 1 比较各 alpha 在三个相邻扩展窗口中的非零特征数与验证 MSE。
- 解释归零:若全部归零且误差接近零收益基准,则当前特征没有稳定的下一日增量信号。
- 禁止误读:稀疏性本身不等于预测成功。
完整判断标准
- 系数证据:每个 alpha 的逐特征系数与零状态。
- 误差证据:平均/最差窗 MSE、同期零收益基准和跨窗口选择稳定性。
- 确定决策:按上一页规则确定 alpha,或输出
保留简单模型。
- 证据边界:只有开发期确定规格后才可一次性评估最终测试;系数非零不是因果证据。
本章小结
- 核心冲突: 建模的根本挑战在于偏差-方差的权衡。我们的目标是最小化总预测误差,而非仅仅拟合训练数据。
- 过拟合是敌人: 过于复杂的模型会学习到噪声,导致样本外预测能力差。
- 两大策略:
- 模型选择 (如前向分步法) 通过增减特征来控制复杂度。
- 正则化 (岭、套索、弹性网络) 通过对系数大小施加惩罚来控制复杂度。
- Lasso vs. Ridge: L1 可产生候选稀疏集,但不识别真实重要性或因果;L2 常对相关预测变量作整体收缩,不以稀疏化为目标。
- 实践要点: 量纲不同且使用统一惩罚时,逐特征标准化是常见可比口径,并且只在训练折拟合;\(\lambda\) 只由开发窗选择。
学完后应能
- 在开发窗比较惩罚强度,报告零收益基线与非零系数稳定性。
- 识别失败条件:空窗、最终测试集参与选择,或只凭稀疏性宣称成功。
- 衔接第 8 章:把线性惩罚换成分裂与剪枝,但继续沿用第 6 章的开发—最终测试说明。