05 线性模型

90 分钟学习安排:损失 → 正则化 → 概率决策

  • 目标: 由损失解释线性回归;比较 Ridge/Lasso;把线性分数映射为概率;在不泄漏的前提下处理不平衡。

  • 学习顺序:

    • 回归 20 分钟 → 正则化 20 分钟 → Sigmoid 预测与验证 15 分钟 → 福耀玻璃案例 25 分钟 → 练习与总结 10 分钟。

    • SVM、多分类与 SMOTE 可在课后选学。

  • 先修先答: 残差从 2 增至 4,平方损失贡献放大几倍?

  • 反馈:\(2^2=4\) 增至 \(4^2=16\),放大 4 倍;因此 MSE 对大残差特别敏感。

欢迎来到第五章:线性模型

  • 机器学习与计量经济学的交汇点
  • 从数据中寻找线性关系的艺术与科学
  • 构建更复杂模型不可或缺的基石

目标1:强大的预测能力

线性模型是进行经济预测的主力军。

  • 宏观经济: 预测GDP增长率、通货膨胀、失业率。
  • 金融市场: 预测股票收益、资产价格波动。
  • 微观行为: 预测消费者的购买倾向、企业的销售额。

掌握它,意味着你拥有了量化预测未来的基本工具。

目标2:区分预测关系与因果效应

线性回归首先描述条件均值关系;它不会自动回答“为什么”。因果分析必须先定义处理、结果与目标效应,再给出可信的识别设计。

  • 识别清单:
    • 明确时间顺序与分配/外生性来源;

    • 满足一致性/SUTVA;

    • 测量关键处理与混杂;

    • 在需要时检查重叠/正值性;

    • 事先说明模型设置与估计量。

  • 设计优先:税收、教育、定价案例需要随机试验、自然实验、工具变量、断点或可辩护的条件可忽略性,而不只是“控制更多变量”。
  • 若无识别设计:结果只能称为预测关联,不能解释为政策或价格的因果效应。

本章主要训练预测模型;因果例子只用于建立边界,不据普通回归宣称效应。

目标3:构建高级模型的基础

几乎所有现代高级机器学习模型,都以内嵌线性变换为核心。

  • 神经网络: 每个神经元都执行一次加权求和(线性变换),再通过非线性激活函数。
  • 因子模型: 在金融中,资产收益被建模为对多个风险因子的线性暴露。
  • 广义线性模型 (GLM): 将线性预测器通过链接函数扩展到各种分布的数据。

学好线性模型,是通往更广阔机器学习世界的必经之路。

本章学习内容概览

我们将遵循一条从简单到复杂的认知路径,全面掌握线性模型的“家族”。

核心主题 关键模型 解决的核心问题
基础线性模型 线性回归 (Linear Regression) 预测连续值 (如房价)
逻辑回归 (Logistic Regression) 预测概率/二分类 (如违约)
正则化与稀疏性 岭回归 (Ridge) & Lasso回归 防止模型过拟合,进行特征选择
最大间隔思想 支持向量机 (SVM) 在分类中追求最“鲁棒”的决策边界
处理复杂情况 多分类器 & 类别不平衡 应对真实世界中更复杂的分类任务

核心起点:什么是线性模型?

线性模型的核心假设是:目标变量可以表示为输入特征的加权和

对于一个有 d 个特征的样本 \(\mathbf{x} = (x_1, x_2, \ldots, x_d)\),其预测函数为:

\[ \large{f(\mathbf{x}; \mathbf{w}, b) = w_1x_1 + w_2x_2 + \ldots + w_dx_d + b} \]

使用向量表示法,可以简洁地写成:

\[ \large{f(\mathbf{x}; \mathbf{w}, b) = \mathbf{w}^T\mathbf{x} + b} \]

  • \(\mathbf{w} = (w_1, \ldots, w_d)\): 权重向量 (weights),决定了每个特征的重要性。
  • \(b\): 偏置项 (bias) 或截距项 (intercept),是模型的基准线。

线性模型的内部构造

这张图展示了线性模型如何将输入特征组合成一个预测值。

线性模型的内部构造 一张流程图,展示了输入特征 x1, x2, ..., xd 分别乘以权重 w1, w2, ..., wd,然后与偏置项 b 一起求和,最终产生输出 f(x)。 输入特征 (x) x₁ x₂ ... x_d 权重 (w) w₁ w₂ w_d Σ 加权求和 b 偏置项 f(x) = wᵀx + b 模型输出

几何直觉:决策超平面

线性模型的数学表达式 \(\mathbf{w}^T\mathbf{x} + b = 0\) 在几何上定义了一个超平面 (hyperplane)

  • 在二维空间 (d=2): 这是一条直线 (\(w_1x_1 + w_2x_2 + b = 0\))。
  • 在三维空间 (d=3): 这是一个平面 (\(w_1x_1 + w_2x_2 + w_3x_3 + b = 0\))。

这个超平面将特征空间一分为二,构成了所有线性分类器的决策边界

超平面在不同维度下的形态

超平面在不同维度下的形态 图中以“一维空间 (1D Space)、x₁、超平面 (一个点)、二维空间 (2D Space)、x₂”呈现“超平面在不同维度下的形态”涉及的对象、方向或比较关系。 一维 (1D) x₁ 超平面 (一个点) 二维 (2D) x₁ x₂ 二维超平面:直线 三维 (3D) 超平面 (一个平面)

案例:二维空间中的线性分类

  • 假设我们根据两个宏观指标 \(x_1\) (GDP增长率) 和 \(x_2\) (通胀率) 来预测经济是否处于“扩张”期(蓝圈)或“衰退”期(红星)。

  • 一个线性分类器就是要找到一条直线,来分割这两类点。

案例:二维空间中的线性分类 图中以“线性分类器的决策边界 (Decision Boundary)、x₁ (GDP增长率)、x₂ (通胀率)、w、扩张期 (蓝圈)”呈现“案例:二维空间中的线性分类”涉及的对象、方向或比较关系。 线性分类器的决策边界 (Decision Boundary) x₁ (GDP增长率) x₂ (通胀率) w 扩张期 (蓝圈) 衰退期 (红菱形)

权重向量 w 的角色:决定超平面的方向

权重向量 \(\mathbf{w}\) 不仅定义了特征的权重,它在几何上始终垂直于决策超平面

  • \(\mathbf{w}\) 的方向指向了函数 \(f(\mathbf{x})\) 值增长最快的方向。
  • 对于所有在超平面上的点 \(\mathbf{x}_A, \mathbf{x}_B\),我们有 \(\mathbf{w}^T(\mathbf{x}_A - \mathbf{x}_B) = 0\),这证明了 \(\mathbf{w}\) 与超平面内的任何向量都正交。

在上一张图中,绿色箭头代表的向量 \(\mathbf{w}\) 就垂直于黑色的决策线。

从几何到预测:决策规则

一旦我们有了超平面 \(\mathbf{w}^T\mathbf{x} + b = 0\),分类就变得非常简单。

对于一个新的数据点 \(\mathbf{x}\),我们计算 \(f(\mathbf{x}) = \mathbf{w}^T\mathbf{x} + b\) 的值:

  • 如果 \(f(\mathbf{x}) > 0\),则样本点在超平面由 \(\mathbf{w}\) 指向的一侧,我们预测其为正类(例如,标签为 +1)。
  • 如果 \(f(\mathbf{x}) < 0\),则样本点在超平面的另一侧,我们预测其为负类(例如,标签为 -1)。

这个决策函数通常写作: \(\hat{y} = \text{sign}(\mathbf{w}^T\mathbf{x} + b)\)

关键数学:点到超平面的距离

一个样本点 \(\mathbf{x}\) 到决策超平面 \(\mathbf{w}^T\mathbf{x} + b = 0\) 的距离是多少?这在SVM中至关重要。

根据解析几何,这个距离 \(r\) 的公式为:

\[ \large{r = \frac{|\mathbf{w}^T\mathbf{x} + b|}{\|\mathbf{w}\|}} \]

其中 \(\|\mathbf{w}\|\) 是权重向量 \(\mathbf{w}\) 的L2范数 (欧几里得长度),即 \(\|\mathbf{w}\| = \sqrt{w_1^2 + w_2^2 + \ldots + w_d^2}\)

在同一个固定模型内,\(|f(\mathbf{x})|\) 与几何距离成正比;跨模型比较必须用 \(|f(\mathbf{x})|/\|\mathbf{w}\|\)。几何间隔不是校准后的类别概率。

5.1 从分类到回归:线性回归

如果我们的目标不是预测离散的类别(如“扩张/衰退”),而是预测一个连续的数值(如房价、股票价格),线性模型就变成了线性回归 (Linear Regression)

这时,我们直接使用模型的输出作为预测值:

\[ \large{\hat{y} = f(\mathbf{x}; \mathbf{w}, b) = \mathbf{w}^T\mathbf{x} + b} \]

任务的目标变成了:找到最好的 \(\mathbf{w}\)\(b\),使得预测值 \(\hat{y}\) 尽可能地接近真实的观测值 \(y\)

如何度量“接近”:均方误差(MSE)

我们使用损失函数 (Loss Function) 来量化预测的“错误”程度。在线性回归中,最常用的损失函数是均方误差 (Mean Squared Error, MSE)

对于一个包含 \(N\) 个样本的数据集 \(\{(\mathbf{x}_n, y_n)\}_{n=1}^N\),MSE定义为:

\[ \large{J(\mathbf{w}, b) = \frac{1}{N} \sum_{n=1}^N (y_n - \hat{y}_n)^2 = \frac{1}{N} \sum_{n=1}^N (y_n - (\mathbf{w}^T\mathbf{x}_n + b))^2} \]

我们的目标是找到使这个 \(J(\mathbf{w}, b)\) 最小化\(\mathbf{w}\)\(b\)。这就是著名的最小二乘法 (Least Squares Method)

MSE损失函数的几何直观

最小二乘法,顾名思义,就是寻找一条线,使得所有数据点到该线的纵向距离(残差)的平方和最小。

MSE损失函数的几何直观 图中以“普通最小二乘法 (Ordinary Least Squares)、x、y、εᵢ、目标: 最小化残差平方和”呈现“MSE损失函数的几何直观”涉及的对象、方向或比较关系。 普通最小二乘法 (Ordinary Least Squares) x y εᵢ 目标:最小化 SSE 残差平方和 min Σ (εᵢ)²

求解方法1:正规方程 (Normal Equation)

把截距并入常数特征后,最小化 \(J(\mathbf{w})=N^{-1}\|\mathbf y-\mathbf X\mathbf w\|_2^2\)。令梯度为零得到 \(\mathbf X^{\mathsf T}\mathbf X\mathbf w=\mathbf X^{\mathsf T}\mathbf y\);若满列秩:

\[ \large{\mathbf{w}^* = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\mathbf{y}} \]

  • 维度:\(\mathbf X\)\(N\times(d+1)\)\(\mathbf y\)\(N\times1\)
  • 秩安全计算:\(\mathbf w^*=\mathbf X^+\mathbf y\) 是最小范数解;使用 QR/SVD 或 lstsq,不要显式求逆。

正规方程的优缺点

优点

  • 凸问题: 可用稳定的 QR/SVD 直接得到全局最小二乘解。
  • 无需调参: 没有学习率等超参数。

缺点

  • 计算成本: 直接分解随特征数增长较快;大规模稀疏问题通常采用迭代解法。
  • 秩亏损: 多重共线性或 \(d>N\) 时系数可能不唯一,但最小二乘问题仍可用 QR/SVD/伪逆求解;应同时报告秩与条件数。

求解方法2:梯度下降的核心思想

当特征数量巨大时,我们通常使用梯度下降法来迭代求解。

核心思想

像一个盲人下山,每次都沿着当前位置最陡峭的方向(梯度的反方向)走一小步,直到到达山谷(损失函数的最小值)。

梯度下降:优化路径

求解方法2:梯度下降 (Gradient Descent) 图中以“梯度下降法:可视化核心原理、全局最小值、1. 随机初始化、梯度 (最陡峭上升方向)、更新方向 (-梯度)”呈现“求解方法2:梯度下降 (Gradient Descent)”涉及的对象、方向或比较关系。 梯度下降:等高线与更新路径 全局最小值 梯度:最陡上升 更新:负梯度方向
  1. 随机初始化 \(\mathbf{w}\)\(b\)

  2. 计算损失函数关于 \(\mathbf{w}\)\(b\) 的梯度。

  3. 沿着梯度的反方向更新参数: \(\mathbf{w} \leftarrow \mathbf{w} - \eta \nabla_{\mathbf{w}}J\) \(b \leftarrow b - \eta \nabla_{b}J\)

  4. 重复步骤 2 和 3,直到收敛。 (\(\eta\)学习率 (learning rate))

问题:当特征过多或相关时,线性回归会怎样?

标准线性回归(即最小二乘法)在某些情况下会遇到麻烦:

  1. 过拟合 (Overfitting): 当特征数量 \(d\) 接近或超过样本数量 \(N\) 时,模型会变得过于复杂,完美拟合训练数据,但在新数据上表现很差。
  2. 多重共线性 (Multicollinearity):
    • 表现:特征高度相关,例如同时使用房屋面积和房间数。
    • 机制:矩阵 \(\mathbf{X}^T\mathbf{X}\) 接近奇异。
    • 后果:权重 \(\mathbf{w}\) 极不稳定且难以解释。

过拟合的直观表现

过拟合的直观表现 图中以“模型拟合对比 (Good Fit vs. Overfitting)、良好拟合 (Good Fit)、模型捕捉了数据的基本趋势、过拟合 (Overfitting)、模型学习了数据中的噪声”呈现“过拟合的直观表现”涉及的对象、方向或比较关系。 模型拟合对比 (Good Fit vs. Overfitting) 良好拟合 (Good Fit) 模型捕捉了数据的基本趋势 过拟合 (Overfitting) 模型学习了数据中的噪声

过拟合模型学习到了训练数据中的“噪声”,而不是底层的“信号”。

核心权衡:偏差 vs. 方差

  • 偏差 (Bias): 模型的预测值与真实值之间的系统性差异。高偏差意味着模型过于简单(欠拟合)。
  • 方差 (Variance): 模型在不同训练集上预测结果的变动性。高方差意味着模型对训练数据过于敏感(过拟合)。

我们的目标是找到一个在偏差和方差之间取得良好平衡的模型。

偏差—方差权衡:图形总结

核心权衡:偏差 vs. 方差 图中以“偏差-方差权衡 (Bias-Variance Tradeoff)、误差 (Error)、模型复杂度 (Model Complexity)、偏差 (Bias)、方差 (Variance)”呈现“核心权衡:偏差 vs. 方差”涉及的对象、方向或比较关系。 偏差-方差权衡 (Bias-Variance Tradeoff) 误差 (Error) 模型复杂度 (Model Complexity) 高偏差 高方差 总误差 最佳平衡点

解决方案:正则化 (Regularization)

正则化的核心思想是:在最小化训练误差的同时,对模型的复杂度进行惩罚

我们通过在损失函数中增加一个惩罚项 (Penalty Term) 来实现这一点,该项与权重 \(\mathbf{w}\) 的大小有关。

\[ \large{J_{\text{reg}}(\mathbf{w}, b) = \text{训练误差} (\text{如MSE}) + \lambda \cdot \text{模型复杂度惩罚}} \]

  • \(\lambda \ge 0\)正则化参数,由我们自己设定。它控制着对模型复杂度的惩罚力度。
  • \(\lambda = 0\): 无惩罚,退化为标准线性回归。
  • \(\lambda \to \infty\): 惩罚极重,所有权重都将趋向于0。

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

岭回归使用的惩罚项是权重向量 \(\mathbf{w}\)L2范数的平方\(\|\mathbf{w}\|_2^2 = \sum_{j=1}^d w_j^2\)

其目标函数为:

\[ \large{J_{\text{Ridge}}(\mathbf{w}, b) = \text{MSE}(\mathbf{w},b) + \lambda \sum_{j=1}^d w_j^2} \]

效果:

  • 它会收缩 (shrinkage) 系数,使它们趋向于0,但通常不会让它们恰好等于0。
  • 通过惩罚大的权重,使得模型更平滑,降低方差。
  • 能有效处理多重共线性问题,使模型更稳定。

L1 正则化:Lasso 回归

Lasso (Least Absolute Shrinkage and Selection Operator) 回归使用的惩罚项是权重向量 \(\mathbf{w}\)L1范数\(\|\mathbf{w}\|_1 = \sum_{j=1}^d |w_j|\)

其目标函数为:

\[ \large{J_{\text{Lasso}}(\mathbf{w}, b) = \text{MSE}(\mathbf{w},b) + \lambda \sum_{j=1}^d |w_j|} \]

效果:

  • Lasso 不仅收缩系数,还能将某些不重要特征的系数精确地压缩到0
  • 因此,Lasso 能够实现自动的特征选择 (Feature Selection),生成一个更稀疏、更易于解释的模型,这在经济分析中非常有用。

几何直观:为何 Lasso 能产生稀疏解?

Lasso 和 Ridge 的区别可以通过它们对权重施加的约束来看。

  • Ridge: 约束条件 \(\|\mathbf{w}\|_2^2 \le \alpha\) 是一个圆形(或球形)。
  • Lasso: 约束条件 \(\|\mathbf{w}\|_1 \le \alpha\) 是一个菱形(或高维多面体)。

当损失函数的等高线(椭圆)与约束区域相切时,对于菱形的 Lasso,切点更有可能发生在坐标轴上(即某个 \(w_j=0\)),从而产生稀疏解。

几何证据:菱形尖角更容易落在坐标轴上

Code
import numpy as np  # 构造正则约束与损失等高线网格
import matplotlib.pyplot as plt  # 绘制 L1 与 L2 几何对照
input_feature_values = np.linspace(-1.5, 1.5, 100)  # 固定横轴权重范围
target_values = np.linspace(-1.5, 1.5, 100)  # 固定纵轴权重范围
input_feature_matrix, target_grid_values = np.meshgrid(input_feature_values, target_values)  # 生成二维权重组合
l1_norm = np.abs(input_feature_matrix) + np.abs(target_grid_values)  # 计算 L1 菱形约束值
l2_norm_sq = input_feature_matrix**2 + target_grid_values**2  # 计算 L2 圆形约束值
w1_opt, w2_opt = 0.8, 1.0  # 固定未约束损失中心
loss_contour = (input_feature_matrix - w1_opt)**2 + 1.5 * (target_grid_values - w2_opt)**2  # 预先计算椭圆损失等高线
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)); fig.suptitle('正则化的几何解释', fontsize=20)
# 教学说明:--- Lasso Plot (L1) ---。
ax1.contour(input_feature_matrix, target_grid_values, loss_contour, levels=np.logspace(-1, 1, 10), colors='gray', linestyles='--')
# 执行 `ax1.contour`,生成当前步骤需要的结果或可视化。
ax1.contour(input_feature_matrix, target_grid_values, l1_norm, levels=[1.2], colors='red', linewidths=2.5)
ax1.plot(0, 1.2, 'bo', markersize=10, label='最优解') # 教学说明:Example solution point on axis。
ax1.scatter(w1_opt, w2_opt, marker='x', color='blue', s=100, label='OLS解')  # 标出 Ridge 约束下用于比较的未惩罚最优点
# 执行 `ax1.set_title`,生成当前步骤需要的结果或可视化。
ax1.set_title('Lasso (L1 约束)', fontsize=20)
# 执行 `ax1.set_xlabel`,生成当前步骤需要的结果或可视化。
ax1.set_xlabel('$w_1$', fontsize=20)
# 执行 `ax1.set_ylabel`,生成当前步骤需要的结果或可视化。
ax1.set_ylabel('$w_2$', fontsize=20)
# 执行 `ax1.axhline`,生成当前步骤需要的结果或可视化。
ax1.axhline(0, color='black', lw=0.5)
# 执行 `ax1.axvline`,生成当前步骤需要的结果或可视化。
ax1.axvline(0, color='black', lw=0.5)
# 执行 `ax1.set_aspect`,生成当前步骤需要的结果或可视化。
ax1.set_aspect('equal', adjustable='box')
# 执行 `ax1.legend`,生成当前步骤需要的结果或可视化。
ax1.legend()
# 教学说明:--- Ridge Plot (L2) ---。
ax2.contour(input_feature_matrix, target_grid_values, loss_contour, levels=np.logspace(-1, 1, 10), colors='gray', linestyles='--')
# 执行 `ax2.contour`,生成当前步骤需要的结果或可视化。
ax2.contour(input_feature_matrix, target_grid_values, l2_norm_sq, levels=[1.1**2], colors='red', linewidths=2.5)
ax2.plot(0.6, 0.92, 'bo', markersize=10, label='最优解') # 教学说明:Example solution point not on axis。
ax2.scatter(w1_opt, w2_opt, marker='x', color='blue', s=100, label='OLS解')  # 标出 Lasso 约束下用于比较的未惩罚最优点
# 执行 `ax2.set_title`,生成当前步骤需要的结果或可视化。
ax2.set_title('Ridge (L2 约束)', fontsize=20)
# 执行 `ax2.set_xlabel`,生成当前步骤需要的结果或可视化。
ax2.set_xlabel('$w_1$', fontsize=20)
# 执行 `ax2.set_ylabel`,生成当前步骤需要的结果或可视化。
ax2.set_ylabel('$w_2$', fontsize=20)
# 执行 `ax2.axhline`,生成当前步骤需要的结果或可视化。
ax2.axhline(0, color='black', lw=0.5)
# 执行 `ax2.axvline`,生成当前步骤需要的结果或可视化。
ax2.axvline(0, color='black', lw=0.5)
# 执行 `ax2.set_aspect`,生成当前步骤需要的结果或可视化。
ax2.set_aspect('equal', adjustable='box')
# 执行 `ax2.legend`,生成当前步骤需要的结果或可视化。
ax2.legend()
# 执行 `plt.tight_layout`,生成当前步骤需要的结果或可视化。
plt.tight_layout(rect=[0, 0.03, 1, 0.95])  # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
Lasso (左) vs. Ridge (右) 的几何解释
Figure 1: Lasso (左) vs. Ridge (右) 的几何解释

5.3 逻辑回归 (Logistic Regression)

现在我们回到分类问题。如果直接用线性回归的输出 \(\mathbf{w}^T\mathbf{x}+b\) 来做分类,会有什么问题?

  1. 输出值域不匹配: 输出值域是 \((-\infty, +\infty)\),而 Sigmoid 产生的有限输入概率严格落在 \((0, 1)\)
  2. 对离群点敏感: 一个远离决策边界的离群点,会极大地影响回归线的位置,从而改变分类结果。

逻辑回归通过一个巧妙的“挤压”函数解决了这个问题。

Sigmoid 函数:从实数域到概率的映射

逻辑回归把伯努利条件均值建模为线性分数的 Sigmoid(Logistic)变换,并用极大似然/交叉熵估计参数。

\[ \large{\sigma(z) = \frac{1}{1 + e^{-z}}} \]

其中 \(z = \mathbf{w}^T\mathbf{x} + b\)

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
import numpy as np
# 导入依赖以支持本页的数据处理、建模或可视化。
import matplotlib.pyplot as plt
# 定义函数 `sigmoid`,复用当前教学案例的计算逻辑。
def sigmoid(linear_predictor_values):  # 定义当前教学案例所需的函数。
    # 返回当前函数结果,供后续评估或展示使用。
    return 1 / (1 + np.exp(-linear_predictor_values))  # 返回当前步骤的计算结果供后续使用。
linear_predictor_values = np.linspace(-8, 8, 200)
probability_value = sigmoid(linear_predictor_values)
# 执行 `plt.figure`,生成当前步骤需要的结果或可视化。
plt.figure(figsize=(8, 3.2))  # 展示当前步骤的结果。
# 执行 `plt.plot`,生成当前步骤需要的结果或可视化。
plt.plot(linear_predictor_values, probability_value, color='crimson', linewidth=2.5)  # 展示当前步骤的结果。
# 执行 `plt.axhline`,生成当前步骤需要的结果或可视化。
plt.axhline(y=0.5, color='grey', linestyle='--')  # 展示当前步骤的结果。
# 执行 `plt.axvline`,生成当前步骤需要的结果或可视化。
plt.axvline(x=0, color='grey', linestyle='--')  # 展示当前步骤的结果。
# 执行 `plt.title`,生成当前步骤需要的结果或可视化。
plt.title('Sigmoid 函数', fontsize=50)  # 放大图题以满足投影阅读距离。
# 执行 `plt.xlabel`,生成当前步骤需要的结果或可视化。
plt.xlabel('线性得分 z', fontsize=50)  # 放大横轴标题以满足投影阅读距离。
# 执行 `plt.ylabel`,生成当前步骤需要的结果或可视化。
plt.ylabel('概率 σ(z)', fontsize=50)  # 放大纵轴标题以满足投影阅读距离。
plt.tick_params(axis='both', labelsize=50)  # 放大刻度文字以满足投影阅读距离。
# 执行 `plt.yticks`,生成当前步骤需要的结果或可视化。
plt.yticks([0, 0.5, 1.0])  # 展示当前步骤的结果。
# 执行 `plt.grid`,生成当前步骤需要的结果或可视化。
plt.grid(True)  # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
Sigmoid 函数曲线
Figure 2: Sigmoid 函数曲线

Sigmoid 的两个边界性质

  1. 输出范围在 \((0,1)\),因此可以参数化伯努利概率;仅有有界输出并不足以保证概率校准,仍需在验证数据上检查校准。
  2. \(z=0\) 时,\(\sigma(z)=0.5\);当 \(z \to +\infty\)\(\sigma(z) \to 1\);当 \(z \to -\infty\)\(\sigma(z) \to 0\)

Sigmoid 预测—揭示:概率与敏感度

不使用计算器,先预测 \(z\in\{-2,0,2\}\) 时概率的大小顺序;再判断梯度 \(\sigma'(z)=\sigma(z)[1-\sigma(z)]\) 在哪里最大。

逐步确认

  • \(\sigma(-2)=1/(1+e^2)\approx0.119\)\(\sigma(0)=0.500\)\(\sigma(2)\approx0.881\)
  • \(\sigma'(0)=0.25\),而 \(\sigma'(\pm2)\approx0.105\)
  • 因此模型在边界 \(z=0\) 附近对分数变化最敏感;在两端趋于饱和,但有限 \(z\) 的概率始终严格位于 \((0,1)\)

逻辑回归的概率解释

逻辑回归模型假设了样本属于正类 (y=1) 的概率为:

\[ \large{P(y=1 | \mathbf{x}; \mathbf{w}, b) = \sigma(\mathbf{w}^T\mathbf{x} + b)} \]

那么,属于负类 (y=0) 的概率就是:

\[ \large{P(y=0 | \mathbf{x}; \mathbf{w}, b) = 1 - P(y=1 | \mathbf{x}; \mathbf{w}, b)} \]

通常以 0.5 为阈值进行分类决策:如果 \(P(y=1 | \mathbf{x}) > 0.5\)(即 \(\mathbf{w}^T\mathbf{x} + b > 0\)),则预测为 1,否则为 0。

逻辑回归的损失函数:交叉熵

逻辑回归不是用均方误差来优化的,而是采用源自最大似然估计 (Maximum Likelihood Estimation, MLE) 的思想。

  • 对整个数据集,我们希望最大化观测到当前标签的联合概率(即似然函数)。

  • 取对数并取反后,就得到了需要最小化的损失函数,即对数损失 (Log Loss)二元交叉熵 (Binary Cross-Entropy)

\[ \large{J(\mathbf{w}, b) = -\frac{1}{N} \sum_{n=1}^N \left[ y_n \log(\hat{p}_n) + (1-y_n) \log(1 - \hat{p}_n) \right]} \]

其中 \(\hat{p}_n = \sigma(\mathbf{w}^T\mathbf{x}_n + b)\)。这个损失函数是凸的,可以通过梯度下降等方法高效求解。

交叉熵损失的直观理解

交叉熵损失的直观理解 图中以“当真实标签 y=1 时、损失 = -log(p̂)、损失、p̂ (预测概率)、0”呈现“交叉熵损失的直观理解”涉及的对象、方向或比较关系。 当真实标签 y=1 时 损失 = -log(p̂) 损失 预测概率 p̂ 0 1 p̂ → 1 时,损失 → 0 当真实标签 y=0 时 损失 = -log(1-p̂) 损失 预测概率 p̂ 0 1 p̂ → 0 时,损失 → 0

学习提示:SVM 与多分类可在课后选学

核心内容提示:SVM 与多分类为拓展内容;核心内容从逻辑损失直接进入类别不平衡

5.4 支持向量机 (SVM)

拓展内容提示:核心内容从逻辑回归损失直接进入类别不平衡,然后完成主要案例;SVM 与多分类完成核心内容后选讲。

逻辑回归找到了一个能划分数据的边界,但它是不是“最好”的边界呢?

  • 看下图,有多条线都能完美分开两类数据。

  • SVM 的核心思想是:不仅要分开,还要以最大的“间隔 (margin)”分开

  • 这个最鲁棒的决策边界,离两边最近的样本点都尽可能的远。

图解:最大间隔选择更稳健的分界线

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
import numpy as np
# 导入依赖以支持本页的数据处理、建模或可视化。
import matplotlib.pyplot as plt
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.datasets import make_blobs
input_feature_matrix, target_values = make_blobs(n_samples=40, centers=2, random_state=8, cluster_std=0.8)
# 执行 `plt.figure`,生成当前步骤需要的结果或可视化。
plt.figure(figsize=(8, 6))  # 展示当前步骤的结果。
# 执行 `plt.scatter`,生成当前步骤需要的结果或可视化。
plt.scatter(input_feature_matrix[:, 0], input_feature_matrix[:, 1], c=target_values, s=50, cmap='winter', edgecolors='k')  # 展示当前步骤的结果。
x_fit = np.linspace(plt.xlim()[0], plt.xlim()[1], 100)
# 对照三条候选分界线,突出最大间隔的决策边界。
plt.plot(x_fit, -1 * x_fit + 7, '-k', label='分界线 A (最优)')  # 展示当前步骤的结果。
# 执行 `plt.plot`,生成当前步骤需要的结果或可视化。
plt.plot(x_fit, -0.6 * x_fit + 8.5, '--k', label='分界线 B (离蓝点太近)')  # 展示当前步骤的结果。
# 执行 `plt.plot`,生成当前步骤需要的结果或可视化。
plt.plot(x_fit, -1.5 * x_fit + 3.5, ':k', label='分界线 C (离绿点太近)')  # 展示当前步骤的结果。
# 执行 `plt.title`,生成当前步骤需要的结果或可视化。
plt.title('SVM 的动机:寻找最优决策边界', fontsize=24)  # 放大图题以满足投影阅读距离。
# 执行 `plt.xlabel`,生成当前步骤需要的结果或可视化。
plt.xlabel('特征 1', fontsize=22)  # 放大横轴标题以满足投影阅读距离。
# 执行 `plt.ylabel`,生成当前步骤需要的结果或可视化。
plt.ylabel('特征 2', fontsize=22)  # 放大纵轴标题以满足投影阅读距离。
# 执行 `plt.legend`,生成当前步骤需要的结果或可视化。
plt.legend(fontsize=20)  # 放大图例以满足投影阅读距离。
plt.tick_params(axis='both', labelsize=20)
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
哪条分界线是最好的?
Figure 3: 哪条分界线是最好的?

SVM 的核心概念:间隔与支持向量

  • 决策边界: \(\mathbf{w}^T\mathbf{x} + b = 0\)
  • 间隔 (Margin): 决策边界与两侧数据点之间的“空白区域”。SVM 的目标是最大化这个区域的宽度。
  • 支持向量 (Support Vectors):
    • 在线性可分的硬间隔情形,它们恰好落在间隔边界上;在软间隔 SVM 中,它们对应非零对偶系数,可位于边界上、间隔内,甚至违反间隔。

    • 只有当一个非支持样本移动后仍保持约束不活跃时,它才不会改变当前解。

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
import numpy as np
# 导入依赖以支持本页的数据处理、建模或可视化。
import matplotlib.pyplot as plt
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.svm import SVC
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.datasets import make_blobs
input_feature_matrix, target_values = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=0.60)
model = SVC(kernel='linear', C=1E10)
# 执行 `model.fit`,生成当前步骤需要的结果或可视化。
model.fit(input_feature_matrix, target_values)
{'toy_observations': len(target_values), 'support_vectors': len(model.support_vectors_)}  # 核对机制示意的支持向量数量,无需跨单元状态
Table 1
{'toy_observations': 50, 'support_vectors': 3}

教学机制附录:SVM 间隔与支持向量图

Code
# 执行 `plt.figure`,生成当前步骤需要的结果或可视化。
plt.figure(figsize=(9, 6))  # 展示当前步骤的结果。
# 执行 `plt.scatter`,生成当前步骤需要的结果或可视化。
plt.scatter(input_feature_matrix[:, 0], input_feature_matrix[:, 1], c=target_values, s=50, cmap='winter', edgecolors='k')  # 展示当前步骤的结果。
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xx = np.linspace(xlim[0], xlim[1], 30)
yy = np.linspace(ylim[0], ylim[1], 30)
response_surface, polynomial_design_matrix = np.meshgrid(yy, xx)
xy = np.vstack([polynomial_design_matrix.ravel(), response_surface.ravel()]).T
transformed_feature_matrix = model.decision_function(xy).reshape(polynomial_design_matrix.shape)
# 绘制决策边界和间隔
ax.contour(polynomial_design_matrix, response_surface, transformed_feature_matrix, colors='k', levels=[-1, 0, 1], alpha=0.8,
           linestyles=['--', '-', '--'])
# 绘制支持向量
ax.scatter(model.support_vectors_[:, 0], model.support_vectors_[:, 1], s=150,
           linewidth=2, facecolors='none', edgecolors='red', label='支持向量')
# 执行 `plt.title`,生成当前步骤需要的结果或可视化。
plt.title('SVM 的核心组件', fontsize=20)  # 展示当前步骤的结果。
# 执行 `plt.xlabel`,生成当前步骤需要的结果或可视化。
plt.xlabel('特征 1')  # 展示当前步骤的结果。
# 执行 `plt.ylabel`,生成当前步骤需要的结果或可视化。
plt.ylabel('特征 2')  # 展示当前步骤的结果。
# 执行 `plt.legend`,生成当前步骤需要的结果或可视化。
plt.legend()  # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
二维合成点、线性决策边界、两条间隔线和圈出的支持向量
Figure 4: 合成二维数据上的 SVM 间隔与支持向量

SVM 的数学构建 (线性可分)

  • 为了最大化间隔,我们首先定义间隔的宽度。

  • 通过缩放 \(\mathbf{w}\)\(b\),我们可以规定对于支持向量 \(\mathbf{x}_s\),它们满足 \(|\mathbf{w}^T\mathbf{x}_s + b| = 1\)

  • 点到超平面的距离为 \(\frac{|\mathbf{w}^T\mathbf{x} + b|}{\|\mathbf{w}\|}\)

  • 所以,支持向量到超平面的距离为 \(1/\|\mathbf{w}\|\)

整个间隔宽度 (margin) 就是 \(2 / \|\mathbf{w}\|\)

最大化间隔 \(\iff\) 最大化 \(2 / \|\mathbf{w}\| \iff\) 最小化 \(\|\mathbf{w}\| \iff\) 最小化 \(\frac{1}{2}\|\mathbf{w}\|^2\)

同时,所有点都必须被正确分类,即对于每个样本 \((\mathbf{x}_n, y_n)\)(其中\(y_n \in \{-1, 1\}\)): \(y_n(\mathbf{w}^T\mathbf{x}_n + b) \ge 1\)

SVM 的优化问题 (硬间隔)

综上,线性可分SVM (硬间隔SVM) 的优化问题可以写成:

\[ \large{\min_{\mathbf{w}, b} \quad \frac{1}{2}\|\mathbf{w}\|^2} \]

\[ \large{\text{subject to} \quad y_n(\mathbf{w}^T\mathbf{x}_n + b) \ge 1, \quad \forall n=1, \ldots, N} \]

这是一个带不等式约束的凸二次规划问题。可以通过拉格朗日对偶等方法求解。

现实问题:当数据线性不可分时

在现实世界的经济数据中,数据几乎不可能是完美线性可分的。总会有一些噪声或异常点。

如果强行使用硬间隔SVM,要么找不到解,要么找到的边界非常差,对噪声点过拟合。

解决方案

引入软间隔 (Soft Margin),允许模型在一定程度上“犯错”。

软间隔 SVM:引入松弛变量

我们为每个样本引入一个松弛变量 (slack variable) \(\xi_n \ge 0\)

约束条件放宽为:

\[ \large{y_n(\mathbf{w}^T\mathbf{x}_n + b) \ge 1 - \xi_n} \]

  • 如果 \(\xi_n = 0\),样本被正确分类且在间隔外。
  • 如果 \(0 < \xi_n < 1\),样本在间隔内,但仍被正确分类。
  • 如果 \(\xi_n = 1\),约束允许样本恰好落在决策边界上,不能断言其严格正确分类。
  • 如果 \(\xi_n > 1\),约束允许样本越过决策边界而被错误分类。

软间隔目标:在间隔宽度与违约之间权衡

同时,我们在目标函数中对这些“错误”进行惩罚:

\[ \large{\min_{\mathbf{w}, b, \mathbf{\xi}} \quad \frac{1}{2}\|\mathbf{w}\|^2 + C \sum_{n=1}^N \xi_n} \]

超参数 C 的作用:调节间隔与误差的权衡

\(C\) 是一个非常重要的超参数,它控制着对松弛变量的惩罚力度,可以看作是正则化参数 \(\lambda\) 的倒数。

  • \(C\)
    • 对错误的惩罚较小。

    • 模型更倾向于一个宽的间隔,即使这意味着有一些点会处在间隔内甚至被错分。

    • 容忍度高,正则化强,可能欠拟合

  • \(C\)
    • 对错误的惩罚很大。

    • 模型会尽力将每个点都正确分类,这可能导致一个窄的间隔,对训练数据拟合得很好。

    • 容忍度低,正则化弱,可能过拟合

SVM 参数 C:间隔与误差的图形比较

超参数 C 的作用:调节间隔与误差的权衡 图中以“SVM 间隔与误差的权衡 (C 参数)、The Margin vs. Error Trade-off、小 C: 优先考虑宽间隔 (允许误差)、宽间隔、(Wider Margin)”呈现“超参数 C 的作用:调节间隔与误差的权衡”涉及的对象、方向或比较关系。 SVM 间隔与误差的权衡 (C 参数) The Margin vs. Error Trade-off 小 C:宽间隔 宽间隔;允许少量误差 大 C:窄间隔 误差更少;过拟合风险更高

5.5 多类线性模型

我们已经讨论了二分类问题,但在经济活动中,分类任务常常涉及多个类别。例如:

  • 将客户分为“高价值”、“中价值”、“低价值”三类。
  • 识别手写数字 (0-9,共10类)。
  • 预测经济处于“复苏”、“繁荣”、“衰退”、“萧条”哪个阶段。

如何将二分类模型扩展到多分类场景?

策略1:一对余 (One-vs-Rest, OvR)

为每一个类别训练一个二分类器,该分类器旨在将“本类别”与“所有其他类别”分开。

  • 对于 K 个类别,需要训练 K 个分类器。
  • 预测时比较 K 个分类器的决策分数;只有训练与缩放方案一致或经过校准时,这些分数才可直接比较,且它们不自动等于概率置信度。
策略1:一对余 (One-vs-Rest, OvR) 三个面板复用同一组 A、B、C 样本。每个面板只保留本类别的颜色,并将另外两类合并为灰色负类;三条虚线边界分别正确分开 A 与 B+C、B 与 A+C、C 与 A+B。 一对余 (One-vs-Rest, OvR) 策略 分类器 1 A vs. B+C 分类器 2 B vs. A+C 分类器 3 C vs. A+B

策略2:一对一 (One-vs-One, OvO)

为每一对类别组合训练一个二分类器。

  • 对于 K 个类别,需要训练 \(K(K-1)/2\) 个分类器。

  • 预测时,新样本在所有分类器中投票;最高票唯一时输出该类别。

  • 若最高票并列,采用预先声明的确定性规则(本讲使用各二分类器的带符号置信度加总后取最大者;仍并列时按固定类别顺序)。

  • 原始票数与未校准置信度都不是多分类概率。

策略2:一对一 (One-vs-One, OvO) 图中以“一对一 (One-vs-One, OvO) 策略、分类器 1: 类别 A vs. B、分类器 2: 类别 A vs. C、分类器 3: 类别 B vs. C”呈现“策略2:一对一 (One-vs-One, OvO)”涉及的对象、方向或比较关系。 一对一 (One-vs-One, OvO) 策略 分类器 1 类别 A vs. B 分类器 2 类别 A vs. C 分类器 3 类别 B vs. C

OvR vs. OvO 对比

特性 一对余 (OvR) 一对一 (OvO)
分类器数量 K K(K-1)/2
训练数据 每个分类器使用全部数据,但可能不平衡 每个分类器只使用对应两类;是否平衡取决于这两类的样本数
计算权衡 训练 K 个全样本分类器 共训练 K(K-1)/2 个较小的两类分类器;单个任务更小,但 K 很大时总成本可能更高
常见算法 Logistic Regression (默认) SVM (通常使用)

直接扩展:Softmax 回归

另一种更直接的方法是Softmax 回归,它是逻辑回归在多分类问题上的推广。

对于 K 个类别,模型需要学习 K 组权重向量 \(\{\mathbf{w}_1, \ldots, \mathbf{w}_K\}\)。对于一个样本 \(\mathbf{x}\),我们计算 K 个得分: \(s_k(\mathbf{x}) = \mathbf{w}_k^T \mathbf{x} + b_k\)

然后,使用 Softmax 函数 将这些得分转换为概率分布:

\[ \large{P(y=k | \mathbf{x}) = \text{softmax}(s_k) = \frac{e^{s_k(\mathbf{x})}}{\sum_{j=1}^K e^{s_j(\mathbf{x})}}} \]

  • 所有类别的概率加起来为1。
  • 损失函数是交叉熵损失的多分类版本。

5.6 类不平衡问题 (Class Imbalance)

在许多重要的现实应用中,我们关心的事件可能是非常罕见的。

  • 金融欺诈检测: 绝大多数交易是合法的,只有极少数是欺诈。
  • 罕见病诊断: 绝大多数人是健康的。
  • 广告点击率预测: 用户看到广告后,点击的比例非常低。

这种情况被称为类不平衡 (Class Imbalance)。例如,数据集中 99% 是负样本,只有 1% 是正样本。

为什么类不平衡是个问题?准确率的悖论

标准的机器学习模型,其目标是最大化整体准确率 (Accuracy)

  • 在一个 99% 负样本的数据集上,一个“愚蠢”的模型只要把所有样本都预测为负类,就能达到 99% 的准确率。

  • 但这个模型毫无用处,因为它一个正样本也找不出来。

核心问题

模型被多数类主导,忽略了对少数类的学习。我们需要使用更合适的评估指标。

更好的评估指标:混淆矩阵

混淆矩阵 (Confusion Matrix) 是一个表格,用于可视化分类模型的性能。

混淆矩阵 一个2x2的表格,展示了分类模型的性能,包括TP, FN, FP, TN。 混淆矩阵 (Confusion Matrix) 预测值 实际值 TP 真阳性True Positive 正类 正类 FN 假阴性False Negative FP 假阳性False Positive 负类 TN 真阴性True Negative 负类

关键指标:精确率与召回率

基于混淆矩阵,我们可以定义两个更重要的指标:

  • 精确率 (Precision): 在所有预测为正类的样本中,有多少是真正的正类?

    \[ \large{\text{Precision} = \frac{TP}{TP + FP}} \]

    衡量模型的“查准率”,越高越好。

  • 召回率 (Recall): 在所有实际为正类的样本中,有多少被模型成功找出来了?

    \[ \large{\text{Recall} = \frac{TP}{TP + FN}} \]

    衡量模型的“查全率”,越高越好。

在欺诈检测中,我们非常关心召回率(不能漏掉欺诈交易)。

处理方法1:数据重采样 (Resampling)

这是最直接的方法,从数据层面解决不平衡。分为两种策略:

处理方法1:数据重采样 (Resampling) 图中以“处理类别不平衡:数据重采样方法、1. 原始不平衡数据、2. 欠采样 (Undersampling)、删除部分多数类样本、3. 过采样 (Oversampling)”呈现“处理方法1:数据重采样 (Resampling)”涉及的对象、方向或比较关系。 处理类别不平衡:数据重采样方法 1. 原始不平衡数据 2. 欠采样 Undersampling 删除多数类样本 3. 过采样 Oversampling 增加少数类样本

采样方法的优缺点

欠采样 (Undersampling)

  • 优点: 训练速度快。
  • 缺点: 可能会丢失多数类中的重要信息。
  • 代表算法: EasyEnsemble, BalanceCascade

过采样 (Oversampling)

  • 优点: 不会丢失信息。
  • 缺点: 可能会导致对少数类的过拟合。
  • 代表算法: SMOTE (合成少数类过采样技术)。

核心内容提示:SMOTE 为拓展内容;核心内容从重采样比较直接进入算法层调整,再完成主要案例。

SMOTE 算法详解

SMOTE (Synthetic Minority Over-sampling Technique) 是最流行和有效的过采样技术之一。

核心思想

  • 为每个少数类样本,找到它的 k 个最近邻(也是少数类样本)。

  • 然后,在该样本与这 k 个邻居之间的连线上,随机选择一点,生成一个新的合成样本。

这相当于在少数类集中的区域内进行“插值”,创造出既与原始数据相似又略有不同的新数据,扩大了少数类的决策区域。

SMOTE 算法示意图

SMOTE 算法示意图 图中以“过采样技术: SMOTE 详解、1. 原始不平衡数据、多数类、少数类、2. SMOTE 合成新样本”呈现“SMOTE 算法示意图”涉及的对象、方向或比较关系。 过采样技术: SMOTE 详解 1. 原始不平衡数据 多数类 少数类 2. SMOTE 合成新样本 ① 选择样本 A ② 近邻 B ③ 新样本位于 A–B 连线上

处理方法2:算法层面调整

除了修改数据,我们还可以调整学习算法本身。

  • 调整类别权重 (Class Weight): 在损失函数中,为少数类样本的错误分配一个更大的权重(惩罚)。
    • 例如,在scikit-learn中设置class_weight='balanced'
    • 这使得模型在优化时更加关注少数类的分类正确性。
  • 修改决策阈值:
    • 逻辑回归默认以 0.5 作为分类阈值。

    • 对于不平衡问题,我们可以根据需要调整这个阈值(例如,降低到0.3)来提高对少数类的召回率。

核心内容与学习目标回顾

  • 核心必会(70 分钟): 线性回归、正则化、逻辑回归与样本外评价。拓展(20 分钟或课后):SVM、多分类与 SMOTE。

  • 回顾开场目标:①由损失函数写出线性模型;

  • ②解释 Ridge/Lasso 的约束差异;

  • ③区分线性得分、概率与校准;

  • ④在不混用训练与测试数据的前提下处理类别不平衡。

  • 回顾题: 若预测误差从 2 增到 4,平方损失贡献扩大几倍?

  • 答案:\(2^2=4\) 增到 \(4^2=16\),扩大 4 倍;这解释了 MSE 对大残差更敏感。

形成性检查 1:正则化

Ridge 与 Lasso 哪一个更常产生精确为 0 的系数?为什么?

答案

Lasso。L1 约束的尖角更容易与损失等高线在坐标轴相切;Ridge 的 L2 球通常只连续收缩而不置零。

主要案例:福耀玻璃 Ridge 收益预测

  • 本例数据:
    • 来源:data/stock/stock_price_pre_adjusted.h5,key=data,福耀玻璃 600660.XSHG

    • 任务:用 2018–2024 年 closevolume 构造 1–5 日滞后收益与成交量增速,预测下一日收益。

    • 切分:按时间前 80% 训练、后 20% 测试。

  • 邻近方法来源: Hoerl & Kennard (1970), Ridge 回归;先划定时间范围并暂不查看测试期标签,随后只用训练期数据选择正则化强度。
Code
from pathlib import Path  # 使用统一路径对象定位数据文件

import pandas as pd  # 导入表格工具以读取本地行情
from sklearn.pipeline import make_pipeline  # 在同一训练数据内完成缩放和模型拟合
from sklearn.preprocessing import StandardScaler  # 统一各滞后特征尺度
from sklearn.linear_model import Ridge  # 使用 L2 正则化稳定相关特征系数
from sklearn.metrics import mean_squared_error  # 评价连续预测的样本外误差
# 公网下载:https://assets.qiufei.site/data/stock/stock_price_pre_adjusted.h5
# 下载后把下一行改为本机文件位置;按课程结构存放时可用 Path("data/stock/stock_price_pre_adjusted.h5")。
# Windows:Path(r"C:\qiufei\data\stock\stock_price_pre_adjusted.h5")
# macOS:Path("/Users/你的用户名/data/stock/stock_price_pre_adjusted.h5")
# Linux:Path("/home/你的用户名/data/stock/stock_price_pre_adjusted.h5")
price_path = Path("/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5")
price_rows = pd.read_hdf(price_path, key='data', where=['order_book_id=="600660.XSHG"', 'date>=Timestamp("2018-01-01")', 'date<=Timestamp("2024-12-31")'], columns=['close', 'volume'])  # 仅载入目标公司、时期和字段
ridge_frame = price_rows.reset_index().sort_values('date')  # 按交易日建立预测顺序
ridge_frame['return_t'] = ridge_frame['close'].pct_change()  # 构造基础日收益率
feature_names = [f'return_lag_{lag_day}' for lag_day in range(1, 6)]  # 预先声明五个历史收益特征名
for lag_day, feature_name in enumerate(feature_names, start=1): ridge_frame[feature_name] = ridge_frame['return_t'].shift(lag_day - 1)  # 将可得历史收益写入对应滞后列
ridge_frame['volume_growth_t'] = ridge_frame['volume'].pct_change()  # 添加当日成交量增速
ridge_frame['target_return_t1'] = ridge_frame['return_t'].shift(-1)  # 定义下一日收益率标签
ridge_frame['target_date_t1'] = ridge_frame['date'].shift(-1)  # 保存标签实现日以清除跨窗口样本
ridge_frame = ridge_frame.replace([float('inf'), float('-inf')], pd.NA).dropna()  # 删除变换造成的非有限观测
Code
feature_names = feature_names + ['volume_growth_t']  # 将成交量信息加入模型矩阵
split_row = int(len(ridge_frame) * 0.8)  # 固定前百分之八十为训练期
test_start_date = ridge_frame.iloc[split_row]['date']  # 从完整序列确定测试起点
train_rows = ridge_frame[(ridge_frame['date'] < test_start_date) & (ridge_frame['target_date_t1'] < test_start_date)]  # 清除到测试期才实现的训练标签
test_rows = ridge_frame[ridge_frame['date'] >= test_start_date]  # 确定测试特征窗口
assert train_rows['target_date_t1'].max() < test_rows['date'].min()  # 确认标签实现边界
pd.Series({'train_end': train_rows['date'].max(), 'test_start': test_rows['date'].min()})  # 只核对时间边界,不读取测试标签或候选模型的测试误差
train_end    2023-08-04
test_start   2023-08-08
dtype: datetime64[ns]

形成性检查 2:概率与阈值

逻辑回归得分 \(z=0\) 时概率是多少?若漏报成本上升,分类阈值应通常上调还是下调?

答案

\(\sigma(0)=1/(1+e^0)=0.5\)。对正类漏报成本上升时通常下调阈值,以换取更高召回率;阈值应由损失而非习惯决定。

分步练习:选择 \(\alpha\)

  • 任务: 在训练期内用扩展窗口比较 \(\alpha\in\{0.1,1,10,100\}\),确定 selected_alpha 后重拟合一次,再打开固定测试期并与训练均值基准同表检查。
  • 完整解答:
    • 每个窗口都重新拟合 StandardScaler 与 Ridge;以平均验证 MSE 最小的 \(\alpha\) 为选择。

    • 用完整训练期重拟合后只报告一次测试 MSE,并与训练均值基准同表展示。

    • 禁止用测试期选 \(\alpha\)

Code
from sklearn.model_selection import TimeSeriesSplit  # 构造只向未来验证的扩展窗口
alpha_rows = []  # 收集每折每个正则强度的验证 MSE
for fold_id, (fit_index, validation_index) in enumerate(TimeSeriesSplit(n_splits=5).split(train_rows), start=1):  # 生成五个时间窗口
    fold_train = train_rows.iloc[fit_index]  # 取得历史训练段
    fold_validation = train_rows.iloc[validation_index]  # 取得随后验证段
    fold_train = fold_train[fold_train['target_date_t1'] < fold_validation['date'].min()]  # 清除到验证折才实现的训练标签
    assert fold_train['target_date_t1'].max() < fold_validation['date'].min()  # 确认逐折标签实现边界
    for alpha_value in [0.1, 1.0, 10.0, 100.0]:  # 比较预先声明的候选正则强度
        fold_model = make_pipeline(StandardScaler(), Ridge(alpha=alpha_value)).fit(fold_train[feature_names], fold_train['target_return_t1'])  # 在当前训练段完成缩放和 Ridge 拟合
        fold_prediction = fold_model.predict(fold_validation[feature_names])  # 对随后验证段生成预测
        alpha_rows.append({'fold': fold_id, 'alpha': alpha_value, 'validation_mse': mean_squared_error(fold_validation['target_return_t1'], fold_prediction)})  # 保存逐折误差
alpha_table = pd.DataFrame(alpha_rows)  # 整理所有验证结果
alpha_summary = alpha_table.groupby('alpha', as_index=False)['validation_mse'].mean().rename(columns={'validation_mse': 'mse'}).sort_values('mse')  # 用统一 MSE 字段汇总验证阶段
alpha_summary.insert(0, 'stage', 'mean_time_validation')  # 明确这些行属于训练期时间验证
selected_alpha = float(alpha_summary.iloc[0]['alpha'])  # 只依据训练期验证确定正则强度
final_ridge = make_pipeline(StandardScaler(), Ridge(alpha=selected_alpha)).fit(train_rows[feature_names], train_rows['target_return_t1'])  # 用完整训练期重拟合确定模型
final_prediction = final_ridge.predict(test_rows[feature_names])  # 对固定测试期只预测一次
final_baseline = [train_rows['target_return_t1'].mean()] * len(test_rows)  # 以训练期均值建立可重复基准
pd.concat([alpha_summary[['stage', 'alpha', 'mse']], pd.DataFrame([{'stage': 'test_model', 'alpha': selected_alpha, 'mse': mean_squared_error(test_rows['target_return_t1'], final_prediction)}, {'stage': 'test_baseline', 'alpha': pd.NA, 'mse': mean_squared_error(test_rows['target_return_t1'], final_baseline)}])], ignore_index=True)  # 用阶段、参数、MSE 的长表分开验证与测试证据
Table 2
stage alpha mse
0 mean_time_validation 100.0 0.000508
1 mean_time_validation 10.0 0.000509
2 mean_time_validation 1.0 0.000509
3 mean_time_validation 0.1 0.000509
4 test_model 100.0 0.000252
5 test_baseline <NA> 0.000249

分步练习:测试期解读

  • 测试期解读:
    • 验证期选定 \(\alpha=100\),但 Ridge 测试 MSE 为 0.0002521479,高于训练均值基准的 0.0002491785。

    • 因此 Ridge 未在最后的测试中胜过基准,现有证据不支持实际使用。

综合练习

  • 将任务改为恒瑞医药 600276.XSHG 下一日下跌分类,比较逻辑回归的阈值 0.5 与按 4:1 漏报成本选择的阈值;

  • 提交 AP(平均精度)、召回率、精确率和混淆矩阵。

  • 这里的 \(0.2=1/(4+1)\) 仅在概率已校准、漏报:误报成本为 4:1、正确决策成本相同且取 0 时是 Bayes 阈值;实际使用仍须在验证期检验。

  • 指标约定: 本讲代码的 AP 来自 average_precision_score,按召回增量加权精确率;它不等于对经验 PR 曲线做梯形积分得到的 PR-AUC。

  • 完整答案提醒:
    • 说明时间切分,只用训练期拟合预处理;

    • predict_proba 产生概率,再用布尔阈值生成两组预测;

    • 报告四项指标,并解释成本权衡与“预测关联不代表因果关系”的限制。

综合练习完整答案:恒瑞医药逻辑回归

Table 3
Code
from sklearn.linear_model import LogisticRegression  # 估计可解释的下跌概率基准
from sklearn.metrics import average_precision_score, confusion_matrix, precision_score, recall_score  # 评价不平衡分类与成本阈值
from pathlib import Path  # 使用统一路径对象定位数据文件
# 公网下载:https://assets.qiufei.site/data/stock/stock_price_pre_adjusted.h5
# 下载后把下一行改为本机文件位置;按课程结构存放时可用 Path("data/stock/stock_price_pre_adjusted.h5")。
# Windows:Path(r"C:\qiufei\data\stock\stock_price_pre_adjusted.h5")
# macOS:Path("/Users/你的用户名/data/stock/stock_price_pre_adjusted.h5")
# Linux:Path("/home/你的用户名/data/stock/stock_price_pre_adjusted.h5")
price_path = Path("/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5")
transfer_rows = pd.read_hdf(price_path, key='data', where=['order_book_id=="600276.XSHG"', 'date>=Timestamp("2018-01-01")', 'date<=Timestamp("2024-12-31")'], columns=['close', 'volume'])  # 选择恒瑞医药、时期和必要字段
transfer_frame = transfer_rows.reset_index().sort_values('date')  # 按预测发生顺序排列交易日
transfer_frame['return_t'] = transfer_frame['close'].pct_change()  # 构造当日收益率
transfer_frame['return_5d_t'] = transfer_frame['close'].pct_change(5)  # 构造五日收益率
transfer_frame['volatility_5d_t'] = transfer_frame['return_t'].rolling(5).std()  # 构造历史五日波动率
transfer_frame['volume_growth_t'] = transfer_frame['volume'].pct_change()  # 构造成交量增速
transfer_frame['future_return_t1'] = transfer_frame['return_t'].shift(-1)  # 先保留连续未来收益
transfer_frame['target_date_t1'] = transfer_frame['date'].shift(-1)  # 保存标签实现日
transfer_frame = transfer_frame.replace([float('inf'), float('-inf')], pd.NA).dropna()  # 在标签整数化前删除未知未来和非有限记录
transfer_frame['down_t1'] = (transfer_frame['future_return_t1'] < 0).astype(int)  # 仅对已观测未来收益构造类别
transfer_train = transfer_frame[(transfer_frame['date'] <= '2022-12-31') & (transfer_frame['target_date_t1'] < '2023-01-01')]  # 清除到测试期才实现的训练标签
transfer_test = transfer_frame[transfer_frame['date'] >= '2023-01-01']  # 确定 2023—2024 测试期
assert transfer_train['target_date_t1'].max() < transfer_test['date'].min()  # 确认标签实现边界
transfer_features = ['return_t', 'return_5d_t', 'volatility_5d_t', 'volume_growth_t']  # 固定四个 t 时点可得特征
transfer_logit = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, random_state=42)).fit(transfer_train[transfer_features], transfer_train['down_t1'])  # 只用训练期完成缩放和 Logit 拟合
transfer_probability = transfer_logit.predict_proba(transfer_test[transfer_features])[:, 1]  # 对测试期生成概率
Code
threshold_output = []  # 收集默认阈值与成本阈值结果
for probability_threshold in [0.5, 0.2]:  # 在概率校准、漏报误报成本4:1且正确决策零成本的假设下比较Bayes阈值0.2
    transfer_prediction = transfer_probability >= probability_threshold  # 将同一概率转为类别决策
    threshold_matrix = confusion_matrix(transfer_test['down_t1'], transfer_prediction, labels=[0, 1])  # 固定四格矩阵的类别顺序
    threshold_output.append({'threshold': probability_threshold, 'average_precision': average_precision_score(transfer_test['down_t1'], transfer_probability), 'precision': precision_score(transfer_test['down_t1'], transfer_prediction, zero_division=0), 'recall': recall_score(transfer_test['down_t1'], transfer_prediction), 'tn': threshold_matrix[0, 0], 'fp': threshold_matrix[0, 1], 'fn': threshold_matrix[1, 0], 'tp': threshold_matrix[1, 1]})  # 保存题目要求的全部指标并明确估计量名称
pd.DataFrame(threshold_output)  # 展示两种阈值的真实成本权衡
Table 4
threshold average_precision precision recall tn fp fn tp
0 0.5 0.543929 0.532468 0.484252 121 108 131 123
1 0.2 0.543929 0.525880 1.000000 0 229 0 254

恒瑞案例结果:AP 与分类阈值回答不同问题

  • 真实 AP(平均精度)为 0.543929;它不是经验 PR 曲线的梯形面积。

  • 阈值 0.5 的精确率/召回率为 0.532468/0.484252,混淆矩阵 \([[121,108],[131,123]]\)

  • 阈值 0.2 的精确率/召回率为 0.525880/1.000000,矩阵为 \([[0,229],[0,254]]\),即把全部样本判为下跌。

  • 测试样本数为 483,最后特征日为 2024-12-30,未知未来标签已在整数化前删除。

  • 阈值 0.2 依赖校准概率、4:1 成本比与零正确决策成本等假设,应在验证期确定;它不是普适阈值或因果结论。

形成性检查 3:SMOTE 泄漏

先对全数据做 SMOTE,再划分训练/测试集是否正确?

答案:不正确。 合成点会使用未来测试邻居信息。SMOTE 只能在每个训练折内部执行,验证/测试集保持原始分布。

来源与延伸阅读

  • Hoerl & Kennard (1970), ridge regression;Tibshirani (1996), lasso。
  • Cortes & Vapnik (1995), support-vector networks。
  • Chawla et al. (2002), SMOTE。
  • 数据:本地 A 股前复权行情数据;文件、key、字段、时期和切分见 前面的案例。

本章总结:线性模型的统一视角

课后选学

完成核心内容后,可以继续学习SVM 与多分类SMOTE

本章我们学习了线性模型的“全家桶”,但它们背后有统一的思想:

  1. 核心引擎: 所有模型都始于一个线性函数 \(\mathbf{w}^T\mathbf{x} + b\)
  2. 任务适配:
    • 回归: 直接使用线性输出。
    • 分类: 通过 Sigmoid/Softmax 函数将输出映射为概率。
    • SVM: 关注输出的几何间隔。
  3. 优化目标: 通过定义不同的损失函数正则化项,来学习最优的权重 \(\mathbf{w}\)
    • 损失函数: 均方误差、交叉熵、Hinge损失(SVM)。
    • 正则化: L1, L2范数。

线性模型家族一览

特征 × 权重 → 线性得分 → 任务链接函数 → 预测与损失

掌握了这些组合,你就掌握了解决大量预测问题的基础工具。

谢谢!

Q & A