学习安排:从任务到样本外评价
目标: 定义 \((X,y)\) 与预测时点;按时间切分训练/验证/测试;由误判成本选择指标;解释损失、梯度与学习率。
学习顺序:
先修先答: \([2,3]\cdot[4,-1]\) 等于多少?测试集为何不能参与调参?先独立写答案,再揭示。
- 反馈: 内积为 \(5\);测试集参与调参会把样本外信息反馈给模型,使误差偏乐观。若任一项不清楚,先复习向量内积与数据切分。
核心问题:经济学家为何要学习机器学习?
传统计量经济学与机器学习是解决问题的两种不同“文化”。
- 计量经济学: 核心在于因果推断 (Causal Inference)。
- 目标是理解世界“为何”如此运转。
- 关心的是参数估计的无偏性和一致性。
- 问题范例:最低工资上调 导致 失业率变化了多少?
- 机器学习: 核心在于预测 (Prediction)。
- 目标是预测世界“将会”发生什么。
- 关心的是模型在未知数据上的泛化能力。
- 问题范例:根据当前宏观指标,预测下季度的GDP增长率。
两种文化的视觉对比
两种方法论在模型选择和目标上存在根本差异。
为何预测对经济学家很重要?
在数据驱动的时代,预测能力本身就是一种强大的经济工具。
- 金融市场: 预测资产价格、波动性和信用风险。
- 宏观经济: 预测通货膨胀、GDP增长和失业率,为政策制定提供依据。
- 企业决策: 预测产品销量、客户流失率和供应链需求。
- 政策评估: 预测一项政策(如税收减免)可能带来的经济影响。
因果推断解释过去,精准预测洞见未来。两者结合,威力倍增。
本章目标:构建机器学习的完整思维框架
学完本章,你将能从“四大支柱”的角度,系统性地理解任何机器学习项目。
支柱 I: 定义问题 (Framing)
这是所有工作的起点。
在开始任何技术细节之前,必须清晰地定义业务问题,并将其转化为一个明确的机器学习任务。
- 你要预测什么?
- 一个连续的数值 (e.g., 明天的股价) \(\rightarrow\) 回归 (Regression)
- 一个离散的类别 (e.g., 客户是否会违约) \(\rightarrow\) 分类 (Classification)
- 你拥有什么数据?
- 数据是否带有我们想预测的“答案”(即标签
y)?
- 是 \(\rightarrow\) 监督学习 (Supervised Learning)
- 否 \(\rightarrow\) 无监督学习 (Unsupervised Learning)
支柱 II: 定义模型 (Modeling)
模型,本质上是一个数学函数 \(f(x, \theta)\),它试图捕捉输入特征 \(x\) 和输出 \(y\) 之间的关系。
- \(x\): 输入的特征向量 (e.g., 房屋面积、地段)。
- \(\theta\): 模型的参数。这些是需要通过“学习”来确定的值 (e.g., 线性回归中的系数)。
- \(f\): 函数的形式。这是我们作为建模者需要选择的。
模型的选择范围极广:
- 简单模型: 线性回归、逻辑回归 (可解释性强)。
- 复杂模型: 随机森林、梯度提升树、神经网络 (预测能力强)。
支柱 III: 定义“好坏” (Evaluation)
如何客观地衡量一个模型的好坏?我们需要一个评估指标。
- 这个指标必须能反映业务目标。
- 开发阶段在独立验证期计算;模型设置确定后,再在从未参与选择的测试期做一次最终报告。
常见的评估指标:
- 回归任务:
- 均方误差 (Mean Squared Error, MSE)
- 决定系数 (R-squared, R²)
- 分类任务:
- 准确率 (Accuracy)
- 精确率 (Precision)、召回率 (Recall)、F1-Score
支柱 IV: 定义“学习” (Optimization)
“学习”的过程,就是自动寻找最佳参数 \(\theta\) 的过程。
我们首先定义一个损失函数 (Loss Function) \(J(\theta)\),它衡量在当前参数 \(\theta\) 下,模型的预测有多差。损失越小,模型越好。
然后,我们使用一个优化算法 (Optimizer),如梯度下降法,来系统地、迭代地调整参数 \(\theta\),以找到使损失函数 \(J(\theta)\) 最小化的那组值 \(\theta^*\)。
\[ \large \theta^* = \arg\min_{\theta} J(\theta) \]
什么是机器学习?—— 从数据中学习函数
机器学习 (ML) 的本质是让计算机从数据中自动地、而不是通过显式编程,学习出一个函数,这个函数能对未知数据做出预测。
机器学习的数据表示:万物皆可为向量
在机器学习中,我们需要将现实世界中的事物转化为计算机可以理解的语言——数字。
- 数据集 (Dataset):
N 个样本的集合 \(X = \{x_1, x_2, \dots, x_N\}\)。
- 样本 (Sample): 每一个独立的数据点 (一栋房子、一个客户)。
- 特征 (Feature): 描述一个样本的各个维度 (面积、卧室数量)。
- 特征向量 (Feature Vector): 一个样本的所有特征组成的向量 \(x = (x_{\text{面积}}, x_{\text{卧室}}, \dots)^T\)。
- 标签 (Label): 我们希望预测的目标值
y (房价)。
从现实世界到数学对象
这个转换过程是数据预处理的核心。
一个样本 = d 维空间中的一个点
一旦我们将样本表示为特征向量,每个样本就可以被看作是 d 维特征空间中的一个点。
这为我们用几何的视角理解机器学习算法提供了基础。
案例:用本地 A 股数据理解特征向量
以江苏恒瑞医药 600276.XSHG 的本地前复权日行情为例:用当日收盘价和成交量表示一个样本,以下一交易日收盘价为标签。
| 2023-01-03 |
37.9796 |
25,756,493 |
38.3056 |
| 2023-01-04 |
38.3056 |
25,766,800 |
39.0763 |
- 样本:一行代表一个交易日。
- 特征向量:\(x_t=(\text{close}_t,\text{volume}_t)^T\),是二维空间中的一个点。
- 标签:\(y_t=\text{close}_{t+1}\),必须按时间对齐,不得泄漏到特征中。
本例数据
data/stock/stock_price_pre_adjusted.h5 / key data;字段 close, volume;价格为前复权价格,成交量单位为股;代码只读取本例所需的公司、日期和字段。
机器学习的三大主要类别
根据我们拥有的数据(特别是是否拥有标签 y),机器学习任务可以分为三大类。
- 监督学习 (Supervised Learning)
- 无监督学习 (Unsupervised Learning)
- 强化学习 (Reinforcement Learning)
我们将逐一介绍。
类别 1: 监督学习 (Supervised Learning)
当数据带有明确的“答案”或“标签”时使用。
类别 2:无监督学习
当数据没有“答案”,我们想发现其内在结构时使用。
类别 3: 强化学习 (Reinforcement Learning)
当我们需要通过与环境的“试错”来学习最优策略时使用。
聚焦监督学习:回归 vs. 分类
在经济和商业应用中,绝大多数任务都属于监督学习。它又可根据标签 y 的类型分为两大任务。
- 回归 (Regression):
- 目标: 预测一个连续的数值。
- 输出: \(y \in \mathbb{R}\)
- 例子: 预测GDP增长率、公司销售额。
- 分类 (Classification):
- 目标: 预测一个离散的类别。
- 输出: \(y \in \{C_1, C_2, \dots, C_K\}\)
- 例子: 判断客户是否会流失、交易是否为欺诈。
回归与分类的几何直观
支柱 III: 如何评估模型的好坏?
我们如何知道训练出的模型 \(f(x; \theta)\) 是一个“好”模型?
核心原则
训练期拟合参数,验证期选择特征、超参数与停止点;只有流程确定后,才在从未触碰的测试期做一次最终泛化检查。
这引出了机器学习中最重要的实践:测试期不能参与模型选择。只有模型和参数都已事先确定时,简单的训练/测试二分才足够。
黄金法则:分开使用训练、验证与测试数据
训练与验证期内部再按时间分训练与验证;测试期不参与任何模型选择。
为何必须划分?—— 过拟合的幽灵
过拟合 (Overfitting) 是指模型对训练数据学习得“太好”,以至于把数据中的噪声和偶然性也当作了普适规律。
- 表现: 在训练集上表现极好,但在测试集上表现很差。
- 原因: 模型过于复杂,相对于数据量来说自由度太高。
验证集像练习赛,可用于改进;测试集像最后一次正式检验。看过测试结果再改模型,就已把它变成验证集。
分类任务的评估基石:混淆矩阵
对于二分类问题(例如,预测客户是否违约),所有评估指标都源于一个简单的表格:混淆矩阵 (Confusion Matrix)。
| 实际为正例 |
真正例 (True Positive, TP) |
假反例 (False Negative, FN) |
| 实际为负例 |
假正例 (False Positive, FP) |
真反例 (True Negative, TN) |
- 正例 (Positive): 我们关心的事件,如“违约”、“欺诈”。
- 反例 (Negative): 其他情况,如“不违约”。
- 真/假 (True/False): 指的是预测是否正确。
理解混淆矩阵的四个象限
- TP: 预测正确,确实违约了。(命中)
- TN: 预测正确,确实没违约。(正确拒绝)
- FP: 预测错误,预测会违约,结果没有。(误报,Type I Error)
- FN: 预测错误,预测不违约,结果违约了。(漏报,Type II Error)
在金融风控等领域,FN (漏掉一个坏客户) 的代价通常远高于 FP (误判一个好客户)。
分类指标(1): 准确率 (Accuracy)
准确率衡量的是模型预测正确的样本占总样本的比例。
\[ \large \text{Accuracy} = \frac{\text{预测正确的样本数}}{\text{总样本数}} = \frac{TP + TN}{TP + TN + FP + FN} \]
优点: 非常直观,容易理解。
缺点
在类别不平衡的数据集上具有强烈的误导性。
准确率陷阱:一个例子
想象一个信用卡欺诈检测场景:
- 总交易笔数: 10,000
- 正常交易: 9,990 (99.9%)
- 欺诈交易: 10 (0.1%)
如果一个“懒惰”模型将所有交易都预测为“正常”,它的表现如何?
- TP = 0, TN = 9990
- FP = 0, FN = 10
- Accuracy = (0 + 9990) / 10000 = 99.9%
这个模型准确率极高,但毫无用处,因为它一个欺诈案例也找不到。
分类指标(2): 精确率 (Precision)
精确率衡量的是所有被预测为正例的样本中,真正是正例的比例。
\[ \large \text{Precision} = \frac{TP}{TP + FP} \]
- 业务含义: “在我发出的所有欺诈警报中,有多少是真的?”
- 关注点: 预测的纯度。高精确率意味着更少的误报 (FP)。
精确率图示:交集表示真正例
分类指标(3): 召回率 (Recall)
召回率衡量的是所有真正是正例的样本中,被我们成功找出来的比例。
\[ \large \text{Recall} = \frac{TP}{TP + FN} \]
- 业务含义: “在所有实际发生的欺诈案中,我的模型成功识别了多少?”
- 关注点: 找得有多全。高召回率意味着更少的漏报 (FN)。
召回率图示:实际正例中有多少被命中
精确率与召回率:一个永恒的权衡
在固定评分模型上,阈值变化通常带来精确率—召回率的经验权衡,但精确率并不随阈值单调变化。
- 降低门槛:
- 提高门槛:预测为正的集合只会缩小,因此 TP 与 FP 都不会增加,召回率不会上升;精确率同样不保证单调变化。
经验精确率-召回率 (Precision-Recall, PR) 曲线因此是阶梯状的,局部甚至可能同时提高精确率和召回率;阈值应由验证期业务成本选择,而不是套用机械的反向关系。
业务决策
我们需要根据FP和FN的不同业务成本,来决定在这个权衡中选择哪一个平衡点。
精确率-召回率权衡的可视化
分类指标(4): F1-Score
为了综合考量精确率和召回率,我们使用 F1-Score,它是两者的调和平均数。
\[ \large F_1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}} \]
- 只有当精确率和召回率都比较高时,F1-Score才会高。
- 如果其中一个指标很低,F1-Score也会被拉低。
- 它是在不平衡数据集上比准确率更鲁棒的单一评估指标。
教学机制示意:由固定计数计算分类指标(1/2)
让我们用一个虚拟的信用违约预测例子来演示如何计算这些指标。
Code
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
# 导入依赖以支持本页的数据处理、建模或可视化。
import seaborn as sns
# 导入依赖以支持本页的数据处理、建模或可视化。
import matplotlib.pyplot as plt
import pandas as pd # 用带名称的序列报告固定计数指标
# 设置matplotlib中文字体支持
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
import numpy as np # 构造固定计数的分类标签并支持随机排列
# --- 创建模拟数据 (类别不平衡) ---
# 假设有1000个客户,其中50个实际违约 (5%)
y_true = np.array([0]*950 + [1]*50)
# 假设模型预测出了40个违约,其中30个是正确的 (TP=30),10个是错误的 (FP=10)
# 那么,在50个实际违约中,模型漏掉了20个 (FN=20)
y_pred = np.array([0]*940 + [1]*10 + [0]*20 + [1]*30)
# 创建一个随机排列的索引
probability_value = np.random.permutation(len(y_true))
y_true, y_pred = y_true[probability_value], y_pred[probability_value]
# 1. 计算各项指标
accuracy = accuracy_score(y_true, y_pred)
precision = precision_score(y_true, y_pred, zero_division=0)
recall = recall_score(y_true, y_pred, zero_division=0)
f1 = f1_score(y_true, y_pred, zero_division=0)
cm = confusion_matrix(y_true, y_pred)
pd.Series({ # 输出与固定计数一致的指标
'accuracy': accuracy, 'precision': precision, 'recall': recall, 'f1': f1,
'tn': cm[0, 0], 'fp': cm[0, 1], 'fn': cm[1, 0], 'tp': cm[1, 1]
})
教学机制示意:固定计数的混淆矩阵(2/2)
Code
fig, ax = plt.subplots(figsize=(6, 4.5))
# 执行 `sns.heatmap`,生成当前步骤需要的结果或可视化。
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['不违约', '违约'],
yticklabels=['不违约', '违约'], ax=ax, annot_kws={"size": 34})
ax.collections[0].colorbar.ax.tick_params(labelsize=36) # Reveal 缩放后仍保持投影可读。
# 执行 `ax.set_ylabel`,生成当前步骤需要的结果或可视化。
ax.set_ylabel('实际', fontsize=32)
# 执行 `ax.set_xlabel`,生成当前步骤需要的结果或可视化。
ax.set_xlabel('预测', fontsize=32)
# 执行 `ax.set_title`,生成当前步骤需要的结果或可视化。
ax.set_title('混淆矩阵', fontsize=34)
ax.tick_params(axis='both', labelsize=32)
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show() # 展示当前步骤的结果。
回归任务的评估指标:均方误差 (MSE)
对于回归任务(预测连续值),最常用的评估指标是均方误差 (Mean Squared Error, MSE)。
\[ \large \text{MSE} = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2 \]
- \(y_i\) 是第 \(i\) 个样本的真实值。
- \(\hat{y}_i\) 是模型对第 \(i\) 个样本的预测值。
- \((y_i - \hat{y}_i)\) 是残差 (residual)。
MSE计算的是残差平方的平均值。因为它使用了平方,所以它对大的误差给予了更重的惩罚。
均方误差 (MSE) 的可视化
学习的本质:优化
我们已经定义了模型和评估标准,但机器究竟是如何“学习”的呢?学习的本质是一个优化 (Optimization) 过程。
我们定义一个损失函数 (Loss Function) \(J(\theta)\),它用来衡量在训练集上,当前参数 \(\theta\) 下模型的预测有多差。损失函数的值越小,模型表现越好。
“学习”的目标就是找到一组参数 \(\theta^*\),使得损失函数 \(J(\theta)\) 最小化。
对于回归问题,MSE就是最常用的损失函数。
损失函数:优化过程中的“导航地图”
损失函数 \(J(\theta)\) 描绘了一个“地形图”,其中地面的高度就是损失值。我们的目标是从某个随机的出发点,走到这个地形的最低谷。
Code
fig = plt.figure(figsize=(9, 6))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(input_feature_matrix, target_grid_values, transformed_feature_matrix, cmap='viridis', edgecolor='none', alpha=0.85, rstride=2, cstride=2)
global_min_value = loss_function_2d(2, 3)
local_min_value = loss_function_2d(-0.5, -0.5)
# 标记点
# 执行 `ax.scatter`,生成当前步骤需要的结果或可视化。
ax.scatter(2, 3, global_min_value, color='red', s=120, zorder=10, depthshade=False)
# 执行 `ax.scatter`,生成当前步骤需要的结果或可视化。
ax.scatter(-0.5, -0.5, local_min_value, color='orange', s=120, zorder=10, depthshade=False)
# 执行 `ax.set_title`,生成当前步骤需要的结果或可视化。
ax.set_title(title_text, fontsize=46)
# 执行 `ax.set_xlabel`,生成当前步骤需要的结果或可视化。
ax.set_xlabel(xlabel_text, fontsize=40, labelpad=12)
# 执行 `ax.set_ylabel`,生成当前步骤需要的结果或可视化。
ax.set_ylabel(ylabel_text, fontsize=40, labelpad=12)
# 执行 `ax.set_zlabel`,生成当前步骤需要的结果或可视化。
ax.set_zlabel(zlabel_text, fontsize=40, labelpad=12)
ax.tick_params(labelsize=38)
ax.set_xticks([-1, 2, 5])
ax.set_yticks([-1, 2, 5])
ax.set_zticks([0, 10, 20])
# 执行 `ax.view_init`,生成当前步骤需要的结果或可视化。
ax.view_init(elev=30., azim=120)
fig.canvas.draw()
global_x, global_y, _ = proj3d.proj_transform(2, 3, global_min_value, ax.get_proj())
local_x, local_y, _ = proj3d.proj_transform(-0.5, -0.5, local_min_value, ax.get_proj())
annotation_box = dict(boxstyle='round,pad=0.18', facecolor='white', edgecolor='none', alpha=0.92)
annotation_arrow = dict(arrowstyle='-', color='#555555', linewidth=2.5)
ax.annotate(global_min_text, xy=(global_x, global_y), xytext=(-92, -64), textcoords='offset points',
color='#A61B29', fontsize=42, ha='center', va='top', bbox=annotation_box,
arrowprops=annotation_arrow, zorder=20)
ax.annotate(local_min_text, xy=(local_x, local_y), xytext=(76, 46), textcoords='offset points',
color='#8A4A00', fontsize=42, ha='center', va='bottom', bbox=annotation_box,
arrowprops=annotation_arrow, zorder=20)
# 执行 `fig.tight_layout`,生成当前步骤需要的结果或可视化。
fig.tight_layout()
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show() # 展示当前步骤的结果。
分类任务的损失函数:交叉熵 (Cross-Entropy)
对于 \(C\) 分类问题,我们常用交叉熵损失 (Cross-Entropy Loss),其一般定义为:
\[ \large L(\theta) = - \frac{1}{N} \sum_{i=1}^N \sum_{c=1}^C y_{ic} \log(\hat{y}_{ic}) \]
- \(y_{ic}\): 真实标签的独热 (One-Hot) 指示变量——若样本 \(i\) 的真实类别为 \(c\),则 \(y_{ic} = 1\),否则为 \(0\)。
- \(\hat{y}_{ic}\): 模型预测样本 \(i\) 属于类别 \(c\) 的概率。
直观理解: 衡量模型预测的概率分布与真实的概率分布之间的“距离”。
交叉熵的二分类特例 (Binary Cross-Entropy)
当 \(C = 2\) 时,一般公式就退化为我们熟悉的二分类交叉熵:
\[ \large L(\theta) = - \frac{1}{N} \sum_{i=1}^N \left[ y_i \log(\hat{y}_i) + (1-y_i) \log(1-\hat{y}_i) \right] \]
其中 \(y_i \in \{0, 1\}\) 是真实标签,\(\hat{y}_i \in (0, 1)\) 是模型预测为类别 1 的概率。
这个函数有一个很好的特性:当模型做出非常自信且错误的预测时,损失会变得非常大,从而给模型一个强烈的“惩罚”信号。
支柱 IV: 定义“学习” (Optimization)
我们有了地图(损失函数),但如何找到下山的路?
最经典、最重要的方法是梯度下降法 (Gradient Descent)。
梯度下降法的数学原理
梯度下降是一个迭代算法。在每一步 t,都按照以下规则更新参数 \(\theta\):
\[ \large \theta_{t+1} = \theta_t - \eta \nabla J(\theta_t) \]
- \(\theta_t\): 参数在第
t 步的值。
- \(\nabla J(\theta_t)\): 损失函数 \(J\) 在 \(\theta_t\) 处的梯度。它是一个向量,指向函数值上升最快的方向。
- \(\eta\): 学习率 (Learning Rate),它是一个超参数,控制我们每一步“走多远”。
- \(-\eta \nabla J(\theta_t)\): 我们沿着梯度相反的方向走一小步。
我们不断重复这个过程,直到参数收敛。
梯度下降法的直观理解
想象你站在一个浓雾笼罩的山坡上,能见度只有你脚下的一小块地方。为了最快下山,你应该沿着当前位置最陡峭的方向迈出一步。
在数学上,函数在某一点梯度 (Gradient) 的反方向,就是函数值下降最快的方向——这正是更新规则中 \(-\eta \nabla J(\theta_t)\) 的含义。
梯度下降法的可视化
学习率(η):决定优化的效果和速度
Code
def gradient_path(learning_rate, step_count): # 计算给定学习率的梯度下降路径
current_parameter = -1.8 # 固定三组路径的共同起点
parameter_path = [current_parameter] # 保存每一步参数位置
loss_path = [current_parameter**2] # 保存每一步二次损失
for gradient_step_index in range(step_count): # 按指定步数执行梯度更新
current_parameter -= learning_rate * (2 * current_parameter) # 沿负梯度移动参数
parameter_path.append(current_parameter) # 记录更新后的参数
loss_path.append(current_parameter**2) # 记录更新后的损失
return parameter_path, loss_path # 返回可直接绘制的路径
learning_axis = np.linspace(-2, 2, 400) # 准备共同的参数轴
learning_loss = learning_axis**2 # 计算共同的二次损失曲线
small_path = gradient_path(.15, 10) # 预先计算学习率过小的路径
good_path = gradient_path(.8, 5) # 预先计算中等学习率的路径
large_path = gradient_path(1.02, 5) # 预先计算学习率过大的路径
Code
# 定义函数 `plot_learning_rate_analogy`,复用当前教学案例的计算逻辑。
def plot_learning_rate_analogy(): # 定义当前教学案例所需的函数。
fig, axs = plt.subplots(1, 3, figsize=(14, 4.2), sharey=True); fig.suptitle('学习率(η)的影响', fontsize=28)
# 学习率太小
ax1 = axs[0]
# 执行 `ax1.plot`,生成当前步骤需要的结果或可视化。
ax1.plot(learning_axis, learning_loss, color='#0d6efd')
path_x, path_y = small_path
# 执行 `ax1.plot`,生成当前步骤需要的结果或可视化。
ax1.plot(path_x, path_y, 'o-', color='#dc3545', markersize=5, mfc='white', mew=1.5)
# 执行 `ax1.set_title`,生成当前步骤需要的结果或可视化。
ax1.set_title('η 太小:收敛缓慢', fontsize=24); ax1.set_xlabel('θ', fontsize=24); ax1.set_ylabel('J(θ)', fontsize=24)
# 学习率合适
ax2 = axs[1]
# 执行 `ax2.plot`,生成当前步骤需要的结果或可视化。
ax2.plot(learning_axis, learning_loss, color='#0d6efd')
path_x, path_y = good_path
# 执行 `ax2.plot`,生成当前步骤需要的结果或可视化。
ax2.plot(path_x, path_y, 'o-', color='#198754', markersize=5, mfc='white', mew=1.5)
# 执行 `ax2.set_title`,生成当前步骤需要的结果或可视化。
ax2.set_title('η 合适:高效收敛', fontsize=24); ax2.set_xlabel('θ', fontsize=24)
# 学习率太大
ax3 = axs[2]
# 执行 `ax3.plot`,生成当前步骤需要的结果或可视化。
ax3.plot(learning_axis, learning_loss, color='#0d6efd')
path_x, path_y = large_path
# 执行 `ax3.plot`,生成当前步骤需要的结果或可视化。
ax3.plot(path_x, path_y, 'o-', color='#ffc107', markersize=5, mfc='white', mew=1.5)
# 执行 `ax3.set_title`,生成当前步骤需要的结果或可视化。
ax3.set_title('η 太大:振荡甚至发散', fontsize=24); ax3.set_xlabel('θ', fontsize=24)
# 遍历 `axs`,逐项形成可比较的验证证据。
for ax in axs: # 遍历当前教学对象以完成重复计算。
# 执行 `ax.spines[['top', 'right']].set_visible`,生成当前步骤需要的结果或可视化。
ax.spines[['top', 'right']].set_visible(False)
# 执行 `ax.set_ylim`,生成当前步骤需要的结果或可视化。
ax.set_ylim(-0.5, 4)
ax.tick_params(axis='both', labelsize=22)
# 执行 `ax.grid`,生成当前步骤需要的结果或可视化。
ax.grid(True, linestyle='--', alpha=0.6)
# 执行 `plt.tight_layout`,生成当前步骤需要的结果或可视化。
plt.tight_layout(rect=[0, 0, 1, 0.94]) # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show() # 展示当前步骤的结果。
# 执行 `plot_learning_rate_analogy`,生成当前步骤需要的结果或可视化。
plot_learning_rate_analogy()
- \(\eta\) 太小: 更新缓慢,收敛所需步数多。
- \(\eta\) 太大: 会越过最低点或来回振荡,甚至发散。
梯度下降的变种:处理大规模数据
当我们的训练集非常大时,计算整个数据集的梯度会非常耗时。为此,我们发展出了梯度下降的变种:批量梯度下降 (Batch Gradient Descent, BGD)、随机梯度下降 (Stochastic Gradient Descent, SGD) 和小批量梯度下降 (Mini-batch Gradient Descent, MBGD)。
梯度下降变种的对比
| 批量梯度下降 (BGD) |
使用所有训练样本 |
梯度方向准确,收敛路径平滑 |
计算成本高,速度慢 |
| 随机梯度下降 (SGD) |
随机选取一个样本 |
速度快,能够跳出局部最优 |
梯度随机性大,收敛路径嘈杂 |
| 小批量梯度下降 (MBGD) |
随机选取一小批样本 (e.g., 32) |
结合BGD和SGD优点,是默认选择 |
需额外设置批量大小 (batch size) |
核心内容提示:进阶优化器为拓展内容;核心内容直接进入福耀玻璃主要案例。
进阶优化器:让下山之路更智能
拓展内容提示:核心内容在学习率检查后直接进入福耀玻璃主要案例;本节只在完成主要练习与总结后选讲,结束后进入最终总结。
基础的梯度下降法有时会在复杂的损失地形中遇到困难。现代深度学习中,我们使用更先进的优化器。
- 动量法 (Momentum)
- 思想: 模拟物理学中的动量。梯度更新时不仅考虑当前梯度,还考虑上一步的更新方向,像一个滚下山的小球。
- 效果: 帮助算法冲出平坦区域和局部最小值,加速收敛。
- Adam (Adaptive Moment Estimation)
- 思想: 结合了动量法和自适应学习率(为每个参数独立调整学习率)。
- 效果: 在各种任务中都表现出色,通常是首选的默认优化器。
对于初学者
直接使用 Adam 优化器通常能得到很好的结果。
进入主要案例前的回顾
用这一页回顾开场目标:把商业问题写成 \((X,y)\) 与预测时点;
按时间顺序划分训练/验证/测试期;
由误判成本选择分类指标;
解释损失、梯度与学习率的关系。
开始前回顾: 向量 \([2,3]\) 与权重 \([4,-1]\) 的内积是多少?为什么测试集不能参与调参?
- 答案: 内积为 \(2\times4+3\times(-1)=5\);测试集参与调参会把样本外信息反馈给模型,使泛化误差偏乐观。
形成性检查 1:先定义任务
在交易日 \(t\) 收盘后,用当日及更早信息预测福耀玻璃下一交易日收益率。下列哪项是正确标签?
- A. \(r_t\) B. \(r_{t+1}\) C. 当日成交量 D. 全样本均值
答案:B。 特征在 \(t\) 时点可得,标签是未来的 \(r_{t+1}\);若把 \(r_{t+1}\) 放进特征即发生前视泄漏。
主要案例:福耀玻璃下一日收益率
- 本例数据:
来源:data/stock/stock_price_pre_adjusted.h5,key=data,公司为福耀玻璃 600660.XSHG。
口径:2018-01-01—2024-12-31;close 为前复权价格,volume 为股数。
切分:按日期前 60% / 中 20% / 后 20% 划分训练、验证与测试期。
- 邻近方法来源: scikit-learn 模型评价指南;以下代码把该原则落实为严格时间切分与样本外 MSE。
Code
from pathlib import Path # 使用统一路径对象定位数据文件
import pandas as pd # 导入表格工具以读取本地 HDF5 数据
from sklearn.linear_model import LinearRegression # 使用线性回归建立可解释基准
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']) # 仅载入目标公司、时期和字段
model_frame = price_rows.reset_index().sort_values('date') # 恢复日期列并保证时间顺序
model_frame['return_t'] = model_frame['close'].pct_change() # 从前复权收盘价计算当日收益率
model_frame['volume_growth_t'] = model_frame['volume'].pct_change() # 构造成交量增速特征
model_frame['target_return_t1'] = model_frame['return_t'].shift(-1) # 定义下一交易日收益率标签
model_frame['target_date_t1'] = model_frame['date'].shift(-1) # 显式保存标签实现日以便清除跨边界样本
model_frame = model_frame.replace([float('inf'), float('-inf')], pd.NA).dropna() # 删除变换产生的非有限观测
Code
train_end = int(len(model_frame) * 0.6) # 固定前百分之六十为训练期
validation_end = int(len(model_frame) * 0.8) # 固定随后百分之二十为验证期
validation_start_date = model_frame.iloc[train_end]['date'] # 从完整时间序列确定验证起点
test_start_date = model_frame.iloc[validation_end]['date'] # 从完整时间序列确定测试起点
train_rows = model_frame[(model_frame['date'] < validation_start_date) & (model_frame['target_date_t1'] < validation_start_date)] # 清除到验证期才实现的训练标签
validation_rows = model_frame[(model_frame['date'] >= validation_start_date) & (model_frame['date'] < test_start_date) & (model_frame['target_date_t1'] < test_start_date)] # 清除到测试期才实现的验证标签
test_rows = model_frame[model_frame['date'] >= test_start_date] # 最后窗口暂不查看,直到模型设置确定
assert train_rows['target_date_t1'].max() < validation_rows['date'].min() and validation_rows['target_date_t1'].max() < test_rows['date'].min() # 确认标签实现日不跨窗口
feature_names = ['return_t', 'volume_growth_t'] # 明确 t 时点可获得的信息集
return_model = LinearRegression().fit(train_rows[feature_names], train_rows['target_return_t1']) # 只在训练期估计模型
validation_prediction = return_model.predict(validation_rows[feature_names]) # 在验证期评价当前基准,不触碰测试期
pd.Series({'validation_mse': mean_squared_error(validation_rows['target_return_t1'], validation_prediction), 'train_end': train_rows['date'].max(), 'validation_start': validation_rows['date'].min(), 'test_start': test_rows['date'].min()}) # 报告验证结果与测试期起点
validation_mse 0.000318
train_end 2022-03-14 00:00:00
validation_start 2022-03-16 00:00:00
test_start 2023-08-07 00:00:00
dtype: object
形成性检查 2:评价证据
若违约样本只占 2%,模型把所有客户判为“不违约”,准确率是多少?它是否有业务价值?
答案
准确率为 98%,但违约召回率为 0;在漏报成本高的任务中应同时报告召回率、精确率、精确率-召回率曲线下面积 (Area Under the Precision-Recall Curve, PR-AUC) 与混淆矩阵,而不是只报准确率。
分步练习:加入一个滞后项
Code
from sklearn.linear_model import LinearRegression # 复用同一线性基准以隔离新增滞后项的影响
from sklearn.metrics import mean_squared_error # 用固定测试期均方误差比较两组特征
lag_answer = model_frame.copy() # 从主要案例的清洗后数据建立答案副本
lag_answer['return_t2'] = lag_answer['return_t'].shift(1) # 加入在 t 时点已知的第二个收益滞后
lag_answer = lag_answer.dropna() # 仅删除新增滞后产生的首个缺失观测
fixed_train_end = train_rows['date'].max() # 确定主要案例的训练截止日
fixed_validation_end = validation_rows['date'].max() # 确定验证截止日,其后的测试期暂不查看
lag_train = lag_answer[lag_answer['date'] <= fixed_train_end] # 保持原训练期不变
lag_validation = lag_answer[(lag_answer['date'] > fixed_train_end) & (lag_answer['date'] <= fixed_validation_end)] # 只在中间窗口选择
lag_test = lag_answer[lag_answer['date'] > fixed_validation_end] # 最后窗口在选择期间暂不查看
baseline_features = ['return_t', 'volume_growth_t'] # 声明原基准信息集
extended_features = ['return_t', 'return_t2', 'volume_growth_t'] # 声明加入滞后后的信息集
baseline_fit = LinearRegression().fit(lag_train[baseline_features], lag_train['target_return_t1']) # 在固定训练期重拟合基准
extended_fit = LinearRegression().fit(lag_train[extended_features], lag_train['target_return_t1']) # 在同一训练期拟合扩展模型
baseline_val_mse = mean_squared_error(lag_validation['target_return_t1'], baseline_fit.predict(lag_validation[baseline_features])) # 用验证期评价基准
extended_val_mse = mean_squared_error(lag_validation['target_return_t1'], extended_fit.predict(lag_validation[extended_features])) # 用同一验证期评价扩展模型
selected_features = extended_features if extended_val_mse < baseline_val_mse else baseline_features # 只根据验证证据确定模型设置
lag_development = lag_answer[lag_answer['date'] <= fixed_validation_end] # 合并训练与验证期用于最终拟合
final_fit = LinearRegression().fit(lag_development[selected_features], lag_development['target_return_t1']) # 确定后重拟合一次
final_test_mse = mean_squared_error(lag_test['target_return_t1'], final_fit.predict(lag_test[selected_features])) # 对测试期只检查一次
pd.Series({'baseline_validation_mse': baseline_val_mse, 'extended_validation_mse': extended_val_mse, 'selected_features': ', '.join(selected_features), 'final_test_mse': final_test_mse, 'test_start': lag_test['date'].min()}) # 清楚区分选择证据与最终检查
综合练习完整答案:模型与关键输出
Code
transfer_baseline = LinearRegression().fit(transfer_train[transfer_baseline_features], transfer_train['target_return_t1']) # 在完整训练期拟合基准
transfer_extended = LinearRegression().fit(transfer_train[transfer_extended_features], transfer_train['target_return_t1']) # 在同一训练期拟合扩展模型
transfer_baseline_val_mse = mean_squared_error(transfer_validation['target_return_t1'], transfer_baseline.predict(transfer_validation[transfer_baseline_features])) # 记录基准验证误差
transfer_extended_val_mse = mean_squared_error(transfer_validation['target_return_t1'], transfer_extended.predict(transfer_validation[transfer_extended_features])) # 记录扩展模型验证误差
transfer_selected_features = transfer_extended_features if transfer_extended_val_mse < transfer_baseline_val_mse else transfer_baseline_features # 只按验证期确定特征
transfer_development = pd.concat([transfer_train, transfer_validation]) # 模型设置确定后合并训练与验证期
transfer_final = LinearRegression().fit(transfer_development[transfer_selected_features], transfer_development['target_return_t1']) # 用全部训练与验证期重拟合确定模型
transfer_prediction = transfer_final.predict(transfer_test[transfer_selected_features]) # 对测试期只预测一次
transfer_residual = transfer_test['target_return_t1'].to_numpy() - transfer_prediction # 计算残差供诊断图使用
pd.Series({'train_n': len(transfer_train), 'validation_n': len(transfer_validation), 'test_n': len(transfer_test), 'baseline_validation_mse': transfer_baseline_val_mse, 'extended_validation_mse': transfer_extended_val_mse, 'selected_features': ', '.join(transfer_selected_features), 'final_test_mse': mean_squared_error(transfer_test['target_return_t1'], transfer_prediction)}) # 严格区分选择证据与测试检查
综合练习完整答案:关键结论
真实运行得到训练/验证/测试样本数 967/241/483。
验证期基准与扩展 MSE 分别为 0.000546119023/0.000544403032,因此只依据验证证据确定含五日波动率的扩展特征;
在 2018–2022 训练与验证期重拟合后,测试期的一次最终 MSE 为 0.000398683922。
最后特征日为 2024-12-30;该关联不能解释为因果效应。
综合练习完整答案:残差诊断
Code
import matplotlib.pyplot as plt # 绘制按时间排序的样本外残差
fig, ax = plt.subplots(figsize=(9, 3.2)) # 使用适合单页展示的横向画布
ax.plot(transfer_test['date'], transfer_residual, color='#006F99', linewidth=1) # 展示残差的时间聚集与极端值
ax.axhline(0, color='#536164', linestyle='--', linewidth=1) # 标出无系统偏差的参考线
ax.set(xlabel='日期', ylabel='实际收益 − 预测收益', title='测试期残差') # 明确坐标业务含义
ax.tick_params(axis='both', labelsize=24)
ax.xaxis.label.set_size(26); ax.yaxis.label.set_size(26); ax.title.set_size(28)
import matplotlib.dates as mdates
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y\n%m'))
fig.tight_layout() # 避免轴标签越出图形边界
plt.show() # 输出残差图供诊断
残差均值约 0.00051、标准差约 0.01998;均值接近零不代表风险可忽略,尾部误差仍主导平方损失。
形成性检查 3:优化是否成功?
训练损失持续下降而验证损失先降后升,应该继续训练吗?
答案
不应仅凭训练损失继续。该轨迹提示过拟合;应在独立验证期选择停止点,再用未触碰的测试期做一次最终评价。
来源与延伸阅读
- Shalev-Shwartz & Ben-David, Understanding Machine Learning, Chapters 2–5。
- Hastie, Tibshirani & Friedman, The Elements of Statistical Learning, Chapters 2 and 7。
- scikit-learn User Guide:model selection、metrics 与 linear models。
- 数据:本地 A 股前复权行情数据;本页只声明仓库内可以检查的文件、key、字段和筛选,不推断未记录的数据供应商。
本章总结:机器学习的四大支柱
课后选学
完成核心内容后,可以继续学习进阶优化器,随后进入最终总结。
我们今天建立了一个完整的框架来思考机器学习问题。
. . .
1. 定义问题 (Framing)
- 任务: 回归 vs. 分类
- 学习类型: 监督 vs. 无监督
2. 定义模型 (Modeling)
- 选择一个函数 \(f(x, \theta)\)
- e.g., 线性回归
3. 定义“好坏” (Evaluation)
- 核心: 训练/验证期完成选择;确定后只在测试期检查一次
- 回归: MSE, R²
- 分类: F1-Score, Recall
4. 定义“学习” (Optimization)
- 最小化损失函数 \(J(\theta)\)
- 梯度下降是核心算法