90 分钟学习安排:损失 → 正则化 → 概率决策
- 反馈: 从 \(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),是模型的基准线。
线性模型的内部构造
这张图展示了线性模型如何将输入特征组合成一个预测值。
几何直觉:决策超平面
线性模型的数学表达式 \(\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\))。
这个超平面将特征空间一分为二,构成了所有线性分类器的决策边界。
超平面在不同维度下的形态
案例:二维空间中的线性分类
权重向量 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)\)。
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损失函数的几何直观
最小二乘法,顾名思义,就是寻找一条线,使得所有数据点到该线的纵向距离(残差)的平方和最小。
求解方法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:梯度下降的核心思想
当特征数量巨大时,我们通常使用梯度下降法来迭代求解。
核心思想
像一个盲人下山,每次都沿着当前位置最陡峭的方向(梯度的反方向)走一小步,直到到达山谷(损失函数的最小值)。
梯度下降:优化路径
随机初始化 \(\mathbf{w}\) 和 \(b\)。
计算损失函数关于 \(\mathbf{w}\) 和 \(b\) 的梯度。
沿着梯度的反方向更新参数: \(\mathbf{w} \leftarrow \mathbf{w} - \eta \nabla_{\mathbf{w}}J\) \(b \leftarrow b - \eta \nabla_{b}J\)
重复步骤 2 和 3,直到收敛。 (\(\eta\) 是学习率 (learning rate))
问题:当特征过多或相关时,线性回归会怎样?
标准线性回归(即最小二乘法)在某些情况下会遇到麻烦:
- 过拟合 (Overfitting): 当特征数量 \(d\) 接近或超过样本数量 \(N\) 时,模型会变得过于复杂,完美拟合训练数据,但在新数据上表现很差。
- 多重共线性 (Multicollinearity):
- 表现:特征高度相关,例如同时使用房屋面积和房间数。
- 机制:矩阵 \(\mathbf{X}^T\mathbf{X}\) 接近奇异。
- 后果:权重 \(\mathbf{w}\) 极不稳定且难以解释。
过拟合的直观表现
过拟合模型学习到了训练数据中的“噪声”,而不是底层的“信号”。
核心权衡:偏差 vs. 方差
- 偏差 (Bias): 模型的预测值与真实值之间的系统性差异。高偏差意味着模型过于简单(欠拟合)。
- 方差 (Variance): 模型在不同训练集上预测结果的变动性。高方差意味着模型对训练数据过于敏感(过拟合)。
我们的目标是找到一个在偏差和方差之间取得良好平衡的模型。
偏差—方差权衡:图形总结
解决方案:正则化 (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() # 展示当前步骤的结果。
5.3 逻辑回归 (Logistic Regression)
现在我们回到分类问题。如果直接用线性回归的输出 \(\mathbf{w}^T\mathbf{x}+b\) 来做分类,会有什么问题?
- 输出值域不匹配: 输出值域是 \((-\infty, +\infty)\),而 Sigmoid 产生的有限输入概率严格落在 \((0, 1)\)。
- 对离群点敏感: 一个远离决策边界的离群点,会极大地影响回归线的位置,从而改变分类结果。
逻辑回归通过一个巧妙的“挤压”函数解决了这个问题。
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 的两个边界性质
- 输出范围在 \((0,1)\),因此可以参数化伯努利概率;仅有有界输出并不足以保证概率校准,仍需在验证数据上检查校准。
- 当 \(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) 的思想。
\[ \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)\)。这个损失函数是凸的,可以通过梯度下降等方法高效求解。
交叉熵损失的直观理解
学习提示:SVM 与多分类可在课后选学
核心内容提示:SVM 与多分类为拓展内容;核心内容从逻辑损失直接进入类别不平衡。
5.4 支持向量机 (SVM)
拓展内容提示:核心内容从逻辑回归损失直接进入类别不平衡,然后完成主要案例;SVM 与多分类完成核心内容后选讲。
逻辑回归找到了一个能划分数据的边界,但它是不是“最好”的边界呢?
图解:最大间隔选择更稳健的分界线
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() # 展示当前步骤的结果。
教学机制附录: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() # 展示当前步骤的结果。
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\) 的倒数。
SVM 参数 C:间隔与误差的图形比较
5.5 多类线性模型
我们已经讨论了二分类问题,但在经济活动中,分类任务常常涉及多个类别。例如:
- 将客户分为“高价值”、“中价值”、“低价值”三类。
- 识别手写数字 (0-9,共10类)。
- 预测经济处于“复苏”、“繁荣”、“衰退”、“萧条”哪个阶段。
如何将二分类模型扩展到多分类场景?
策略1:一对余 (One-vs-Rest, OvR)
为每一个类别训练一个二分类器,该分类器旨在将“本类别”与“所有其他类别”分开。
- 对于 K 个类别,需要训练 K 个分类器。
- 预测时比较 K 个分类器的决策分数;只有训练与缩放方案一致或经过校准时,这些分数才可直接比较,且它们不自动等于概率置信度。
策略2:一对一 (One-vs-One, OvO)
为每一对类别组合训练一个二分类器。
对于 K 个类别,需要训练 \(K(K-1)/2\) 个分类器。
预测时,新样本在所有分类器中投票;最高票唯一时输出该类别。
若最高票并列,采用预先声明的确定性规则(本讲使用各二分类器的带符号置信度加总后取最大者;仍并列时按固定类别顺序)。
原始票数与未校准置信度都不是多分类概率。
OvR vs. 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)。
核心问题
模型被多数类主导,忽略了对少数类的学习。我们需要使用更合适的评估指标。
更好的评估指标:混淆矩阵
混淆矩阵 (Confusion Matrix) 是一个表格,用于可视化分类模型的性能。
关键指标:精确率与召回率
基于混淆矩阵,我们可以定义两个更重要的指标:
精确率 (Precision): 在所有预测为正类的样本中,有多少是真正的正类?
\[ \large{\text{Precision} = \frac{TP}{TP + FP}} \]
衡量模型的“查准率”,越高越好。
召回率 (Recall): 在所有实际为正类的样本中,有多少被模型成功找出来了?
\[ \large{\text{Recall} = \frac{TP}{TP + FN}} \]
衡量模型的“查全率”,越高越好。
在欺诈检测中,我们非常关心召回率(不能漏掉欺诈交易)。
处理方法1:数据重采样 (Resampling)
这是最直接的方法,从数据层面解决不平衡。分为两种策略:
采样方法的优缺点
欠采样 (Undersampling)
- 优点: 训练速度快。
- 缺点: 可能会丢失多数类中的重要信息。
- 代表算法: EasyEnsemble, BalanceCascade
过采样 (Oversampling)
- 优点: 不会丢失信息。
- 缺点: 可能会导致对少数类的过拟合。
- 代表算法: SMOTE (合成少数类过采样技术)。
核心内容提示:SMOTE 为拓展内容;核心内容从重采样比较直接进入算法层调整,再完成主要案例。
SMOTE 算法详解
SMOTE (Synthetic Minority Over-sampling Technique) 是最流行和有效的过采样技术之一。
核心思想
这相当于在少数类集中的区域内进行“插值”,创造出既与原始数据相似又略有不同的新数据,扩大了少数类的决策区域。
SMOTE 算法示意图
处理方法2:算法层面调整
除了修改数据,我们还可以调整学习算法本身。
- 调整类别权重 (Class Weight): 在损失函数中,为少数类样本的错误分配一个更大的权重(惩罚)。
- 例如,在
scikit-learn中设置class_weight='balanced'。
- 这使得模型在优化时更加关注少数类的分类正确性。
- 修改决策阈值:
核心内容与学习目标回顾
- 答案: 从 \(2^2=4\) 增到 \(4^2=16\),扩大 4 倍;这解释了 MSE 对大残差更敏感。
形成性检查 1:正则化
Ridge 与 Lasso 哪一个更常产生精确为 0 的系数?为什么?
答案
Lasso。L1 约束的尖角更容易与损失等高线在坐标轴相切;Ridge 的 L2 球通常只连续收缩而不置零。
主要案例:福耀玻璃 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 后重拟合一次,再打开固定测试期并与训练均值基准同表检查。
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 的长表分开验证与测试证据
综合练习
将任务改为恒瑞医药 600276.XSHG 下一日下跌分类,比较逻辑回归的阈值 0.5 与按 4:1 漏报成本选择的阈值;
提交 AP(平均精度)、召回率、精确率和混淆矩阵。
这里的 \(0.2=1/(4+1)\) 仅在概率已校准、漏报:误报成本为 4:1、正确决策成本相同且取 0 时是 Bayes 阈值;实际使用仍须在验证期检验。
指标约定: 本讲代码的 AP 来自 average_precision_score,按召回增量加权精确率;它不等于对经验 PR 曲线做梯形积分得到的 PR-AUC。
综合练习完整答案:恒瑞医药逻辑回归
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) # 展示两种阈值的真实成本权衡
恒瑞案例结果: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。
本章我们学习了线性模型的“全家桶”,但它们背后有统一的思想:
- 核心引擎: 所有模型都始于一个线性函数 \(\mathbf{w}^T\mathbf{x} + b\)。
- 任务适配:
- 回归: 直接使用线性输出。
- 分类: 通过 Sigmoid/Softmax 函数将输出映射为概率。
- SVM: 关注输出的几何间隔。
- 优化目标: 通过定义不同的损失函数和正则化项,来学习最优的权重 \(\mathbf{w}\)。
- 损失函数: 均方误差、交叉熵、Hinge损失(SVM)。
- 正则化: L1, L2范数。
线性模型家族一览
特征 × 权重 → 线性得分 → 任务链接函数 → 预测与损失
掌握了这些组合,你就掌握了解决大量预测问题的基础工具。