05 其他基础监督学习方法

第5章:超越线性模型的局限

高级监督学习方法

课程回顾:线性模型的基石与局限

我们已经掌握了强大的线性模型,它们是计量经济学的核心。

核心假设: 目标变量与特征之间存在 线性关系

  • 线性回归: \(y = w^T x + b\)
  • 逻辑回归: \(P(y=1|x) = \sigma(w^T x + b)\)

但现实世界… 往往不是线性的。

现实的挑战:当线性假设失效

当关系是非线性时,强行使用线性模型会导致系统性的预测偏差和错误的结论。

线性模型拟合非线性数据 一张图表显示了一条二次曲线上的散点,以及一条试图拟合这些点的直线,展示了欠拟合的情况。 X (例如: 研发投入) Y (例如: 公司利润) 线性模型拟合 (欠拟合) 真实关系

本章学习目标:构建我们的非线性工具箱

本章我们将学习一系列强大的监督学习方法,以捕捉复杂的非线性关系。

  • 样条模型 (Splines): 通过分段函数灵活地拟合数据。
  • 广义可加模型 (GAMs): 将非线性模型扩展到多预测变量,同时保持可解释性。
  • 支持向量机 (SVM): 一种强大的分类技术,通过“核技巧”处理非线性边界。
  • K近邻 (KNN): 一种简单、直观、非参数的分类和回归方法。

经济学中的非线性关系无处不在

线性假设的失效并非个例,而是常态。

经济学中的非线性关系案例 四张迷你图表分别展示了累进税率、边际报酬递减、广告饱和效应和波动率微笑四种非线性关系。 税收制度 分段线性 (累进税率) 生产函数 边际报酬递减 广告效应 饱和效应 资产波动率 “波动率微笑”现象

解决方案一:回归样条 (Regression Splines)

核心思想: 与其用一个复杂的全局函数拟合所有数据,不如将数据的定义域切分成多个区域,在每个区域内用简单的函数(如多项式)来拟合。

这就像修建一条穿过山脉的铁路:我们不会试图用一个巨大的圆弧来匹配整个山脉轮廓,而是在不同路段使用不同的坡度和曲线,然后将它们平滑地连接起来。

样条的关键概念:结点 (Knots)

结点 (Knot) 是我们分割数据区域的点,也是模型函数形式发生变化的地方。

样条的结点 一条X轴上标有“区域1”,“区域2”,“区域3”,分割点被称为“结点”。 x 结点 c₁ 结点 c₂ 区域 1 区域 2 区域 3 f₁(x) f₂(x) f₃(x)

在累进税率的例子中,每个税率区间的起点就是一个天然的结点

一个简单的分段线性样条模型

假设我们只有一个结点 \(c\)。我们可以将模型定义为:

\[ \large{ f(x) = \begin{cases} w_{01} + w_{11}x, & \text{如果 } x \le c \\ w_{02} + w_{12}x, & \text{如果 } x > c \end{cases} } \]

这个模型在 \(x \le c\) 的区域使用一条直线,在 \(x > c\) 的区域使用另一条直线。

分段拟合的问题:函数不连续

如果我们独立地在每个区域估计参数,拟合出的函数在结点处可能会出现“跳跃”,这在多数经济场景中不合逻辑。

不连续的分段函数 图表显示在结点c处,两条拟合线没有连接在一起,形成了一个跳跃。 结点 c 不合逻辑的“跳跃”!

解决方案:施加连续性约束

为了让函数在结点处平滑连接,我们可以施加约束。

  • C⁰ 连续 (函数本身连续): 要求函数在结点 \(c\) 处的值必须相等。
    • 结果: 两条线在结点处相交,形成一个拐点
  • C¹ 连续 (一阶导数连续): 要求函数在结点 \(c\) 处的斜率也必须相等。
    • 结果: 曲线在结点处平滑过渡
  • C² 连续 (二阶导数连续): 要求函数在结点 \(c\) 处的曲率也必须相等。
    • 结果: 曲线看起来更加平滑自然

可视化连续性约束

不同阶的连续性 三张小图分别展示了C0(有拐点)、C1(平滑)、C2(更平滑)的曲线连接。 C⁰ 连续 (有拐点) C¹ 连续 (平滑) C² 连续 (更平滑)

对于一个 \(d\) 次多项式样条,我们通常要求函数及其直到 \(d-1\) 阶的导数在结点处都是连续的。

更灵活的选择:三次样条 (Cubic Spline)

在实践中,最常用的是三次样条

  • 形式: 在每个结点之间的区域,使用三次多项式 \(f(x) = w_0 + w_1x + w_2x^2 + w_3x^3\) 进行拟合。
  • 约束: 在每个结点处,我们施加函数值、一阶导数和二阶导数都连续的约束。
  • 优点: 这使得拟合出的曲线既非常灵活,又看起来非常平滑自然,没有奇怪的拐点。

可视化案例:税前收入与税后收入

让我们用一个具体的例子来理解样条。美国联邦个人所得税的税率是分级的,这创造了一个天然的分段函数关系。

Figure 1 展示了简化的税前和税后收入关系。蓝线(税后收入)在不同区间有不同的斜率,但在税率变化点(结点)是连续的。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

# Set professional plot style
mpl.rcParams.update({
    'font.size': 14,
    'axes.labelsize': 16,
    'axes.titlesize': 18,
    'xtick.labelsize': 12,
    'ytick.labelsize': 12,
    'legend.fontsize': 14,
    'figure.figsize': [10, 5.5],
    'font.family': 'sans-serif',
    'axes.spines.top': False,
    'axes.spines.right': False,
    'axes.grid': True,
    'grid.linestyle': '--',
    'grid.alpha': 0.6
})

def calculate_post_tax_income(pre_tax_income):
    # 2023 US Federal Tax Brackets for a single filer (simplified)
    brackets = [(0, 0.10), (11000, 0.12), (44725, 0.22), (95375, 0.24), (182100, 0.32), (231250, 0.35)]
    tax = 0
    income_left = pre_tax_income
    
    if income_left > brackets[-1][0]:
        tax += (income_left - brackets[-1][0]) * 0.37
        income_left = brackets[-1][0]
        
    for i in range(5, 0, -1):
        if income_left > brackets[i-1][0]:
            taxable_in_bracket = income_left - brackets[i-1][0]
            tax += taxable_in_bracket * brackets[i][1]
            income_left = brackets[i-1][0]
    
    tax += income_left * brackets[0][1]
    return pre_tax_income - tax

pre_tax_incomes = np.linspace(0, 250000, 500)
post_tax_incomes = np.array([calculate_post_tax_income(income) for income in pre_tax_incomes])
knots = [11000, 44725, 95375, 182100, 231250]  # Tax bracket boundaries
knot_post_tax = [calculate_post_tax_income(k) for k in knots]

fig, ax = plt.subplots()
ax.plot(pre_tax_incomes / 1000, post_tax_incomes / 1000, label='税后收入 (Post-Tax Income)', linewidth=2.5, color='#2980b9')
ax.plot(pre_tax_incomes / 1000, pre_tax_incomes / 1000, label='无税情况 (Pre-Tax Income)', linestyle='--', color='#7f8c8d', alpha=0.8)
ax.scatter(np.array(knots) / 1000, np.array(knot_post_tax) / 1000, color='#c0392b', zorder=5, label='税率变化点 (结点)')

ax.set_xlabel('税前收入 (千美元)')
ax.set_ylabel('收入 (千美元)')
ax.set_title('税前与税后收入关系:一个天然的样条函数')
ax.legend()
ax.set_xlim(0, 250)
ax.set_ylim(0, 250)
plt.show()
Figure 1: 税前收入与税后收入的关系呈现出分段线性的特征

实践中的挑战:如何选择结点?

样条的灵活性也带来了它最大的挑战:我们应该在哪里放置结点?以及,应该放置多少个结点?

  • 结点过多: 模型过于灵活,可能导致对数据的过拟合 (Overfitting)。它会去拟合数据中的噪声,而不是真实的潜在关系。
  • 结点过少: 模型不够灵活,无法捕捉到数据中的非线性结构,导致欠拟合 (Underfitting)

结点选择的影响

结点数量对拟合的影响 两张图,左边显示结点过少导致欠拟合,右边显示结点过多导致过拟合。 结点过少 (欠拟合) 结点过多 (过拟合)

常见策略: 通过交叉验证将结点数量和位置视为超参数来选择最佳组合。

解决方案二:平滑样条 (Smoothing Splines)

回归样条需要我们手动选择结点,这很麻烦。平滑样条提供了一种更优雅的解决方案。

核心思想: 我们不再试图选择少数几个结点,而是在每一个数据点 \(x^{(i)}\) 上都放置一个结点。然后,我们通过一个正则化项来控制函数的“平滑度”或“弯曲度”,从而避免过拟合。

平滑样条的代价函数

平滑样条的目标是找到一个函数 \(f(x)\),使其最小化以下代价函数:

\[ \large{\underbrace{\sum_{i=1}^{n} (y^{(i)} - f(x^{(i)}))^2}_{\text{拟合优度项 (RSS)}} + \underbrace{\lambda \int [f''(t)]^2 dt}_{\text{平滑度惩罚项}}} \]

这个代价函数体现了一种经典的偏差-方差权衡 (Bias-Variance Tradeoff)

代价函数解析:拟合优度

第一项: \(\sum_{i=1}^{n} (y^{(i)} - f(x^{(i)}))^2\)

  • 这是我们熟悉的残差平方和 (RSS)
  • 它驱使模型尽可能地贴近数据点,以减少拟合误差。
  • 单独最小化这一项会导致模型穿过所有数据点,造成严重过拟合

代价函数解析:平滑度惩罚

第二项: \(\lambda \int [f''(t)]^2 dt\)

  • \(f''(t)\) 是函数的二阶导数,衡量其曲率弯曲度
  • \(\int [f''(t)]^2 dt\) 计算了函数在整个定义域上的总弯曲程度
  • 这一项惩罚“摇摆不定”的函数,鼓励模型选择更平滑的曲线。
函数弯曲度与二阶导数 两张图,左图显示一条摇摆的曲线,其二阶导数很大;右图显示平滑曲线,其二阶导数较小。 “摇摆”的函数 f(x) 大的 ∫[f''(t)]² dt (高惩罚) 平滑的函数 f(x) 小的 ∫[f''(t)]² dt (低惩罚)

理解平滑参数 \(\lambda\) 的作用

平滑参数 \(\lambda \ge 0\) 控制着拟合优度函数平滑度之间的权衡。

\(\lambda \to 0\) 时: * 惩罚项消失。 * 模型只关心拟合数据。 * 结果: 严重过拟合

\(\lambda \to \infty\) 时: * 模型极力避免任何弯曲。 * 唯一二阶导数为0的函数是直线。 * 结果: 线性回归 (欠拟合)


结论: \(\lambda\) 是一个需要通过交叉验证等方法仔细选择的关键超参数。

Python实现:用Scipy拟合平滑样条

现在,让我们用Python代码来直观地感受平滑样条。我们将生成一些带有噪声的正弦函数数据,然后用scipy.interpolate.UnivariateSpline来拟合它。

这个函数有一个关键参数 s,它与我们理论中的 \(\lambda\) 概念类似,用于控制平滑的程度。s 越小,拟合越接近插值(过拟合);s 越大,拟合越平滑。

步骤1: 导入必要的库

首先,我们导入 numpy 用于数值计算,scipy.stats.norm 用于生成噪声,scipy.interpolate.UnivariateSpline 用于拟合样条,以及 matplotlib.pyplot 用于可视化。

import numpy as np
from scipy.stats import norm
from scipy.interpolate import UnivariateSpline
import matplotlib.pyplot as plt
import matplotlib as mpl

# Set professional plot style for all subsequent plots
mpl.rcParams.update({
    'font.size': 14, 'axes.labelsize': 16, 'axes.titlesize': 18,
    'xtick.labelsize': 12, 'ytick.labelsize': 12, 'legend.fontsize': 14,
    'figure.figsize': [10, 5.5], 'font.family': 'sans-serif',
    'axes.spines.top': False, 'axes.spines.right': False,
    'axes.grid': True, 'grid.linestyle': '--', 'grid.alpha': 0.6
})

步骤2: 生成合成数据

我们生成一个以 \(y = \sin(x)\) 为基础,并加上一些正态分布噪声 \(\epsilon\) 的数据集。这模拟了一个存在非线性关系但被随机因素干扰的真实场景。

# 设置随机种子以保证结果可复现
np.random.seed(42)

# 生成x和y数据
x_synthetic = np.linspace(0, 10, 100)
y_synthetic = np.sin(x_synthetic) + norm.rvs(0, 0.2, size=x_synthetic.size)

步骤3: 拟合平滑样条模型

我们调用 UnivariateSpline,传入我们的 xy 数据,并设置平滑参数 s。这里我们选择 s=0.5 作为一个适中的初始值。

# s参数控制平滑度,s越大越平滑
spline_synthetic = UnivariateSpline(x_synthetic, y_synthetic, s=0.5)

步骤4: 可视化拟合结果

我们将原始数据点和拟合出的样条曲线绘制在同一张图上,以直观地评估拟合效果。

fig, ax = plt.subplots()
ax.scatter(x_synthetic, y_synthetic, label='原始数据点 (Synthetic Data)', color='navy', s=20, alpha=0.6)
ax.plot(x_synthetic, spline_synthetic(x_synthetic), label='平滑样条拟合 (Smoothing Spline)', color='orange', linewidth=3)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('平滑样条拟合合成数据')
ax.legend()
plt.show()
Figure 2: 平滑样条能够很好地捕捉到数据的非线性趋势,同时忽略噪声

探索平滑参数s的影响

为了更好地理解平滑参数的作用,让我们尝试不同的 s 值,并观察拟合曲线的变化。

我们将在同一张图上绘制 s 取值为 0.1 (欠平滑/过拟合), 1 (适中平滑), 和 10 (过度平滑/欠拟合) 的结果。

spline_overfit = UnivariateSpline(x_synthetic, y_synthetic, s=0.1)
spline_good = UnivariateSpline(x_synthetic, y_synthetic, s=1)
spline_underfit = UnivariateSpline(x_synthetic, y_synthetic, s=10)

fig, ax = plt.subplots()
ax.scatter(x_synthetic, y_synthetic, label='原始数据点', color='black', s=15, alpha=0.4)
ax.plot(x_synthetic, spline_overfit(x_synthetic), label='欠平滑 (s=0.1, 过拟合)', color='red', linestyle='--')
ax.plot(x_synthetic, spline_good(x_synthetic), label='适中平滑 (s=1)', color='green', linewidth=3)
ax.plot(x_synthetic, spline_underfit(x_synthetic), label='过度平滑 (s=10, 欠拟合)', color='blue', linestyle='-.')
ax.set_title('平滑参数 s 的影响')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
plt.show()
Figure 3: 平滑参数s对拟合曲线的形态有决定性影响

从单变量到多变量:广义可加模型 (GAM)

样条模型在处理单个非线性特征时非常有效。但现实中的经济模型通常有多个预测变量。我们如何将样条的思想扩展到多维情况?

答案是广义可加模型 (Generalized Additive Model, GAM)

GAM 的数学形式

  • 多元线性回归模型: \[ \large{y = w_0 + w_1 x_1 + w_2 x_2 + \dots + w_m x_m} \]
  • 广义可加模型 (GAM): \[ \large{y = w_0 + f_1(x_1) + f_2(x_2) + \dots + f_m(x_m)} \]

这里的关键区别在于,我们将线性的 \(w_j x_j\) 项替换为了一个非线性的平滑函数 \(f_j(x_j)\)。这个 \(f_j\) 通常就是一个样条函数

GAM的直观理解:模块化建模

可以把GAM想象成用乐高积木搭建模型。每个特征的平滑函数 \(f_j(x_j)\) 都是一个独立的“积木块”,模型最终的预测是把这些积木块的效果“加”起来。

广义可加模型的模块化结构 三个独立的图表(f1, f2, f3)通过加号连接,最终指向预测结果y。 f₁(x₁) + f₂(x₂) + ... y

GAM 的巨大优势:保持了可解释性

尽管GAM是一个非线性模型,但它通过其可加性 (additivity) 结构,奇迹般地保留了大部分线性模型的可解释性。

  • 我们可以独立地检查每一个函数 \(f_j(x_j)\) 的图形。
  • \(f_j(x_j)\) 的图形揭示了特征 \(x_j\) 对目标变量 \(y\)非线性影响,同时控制了其他所有变量。

模拟GAM输出:洞察非线性关系

假设我们用GAM预测房价,我们可以得到类似下图的洞察:

模拟的GAM组件图 两张图,左边显示房价与房屋面积的非线性关系,右边显示房价与建造年份的非线性关系。 f(房屋面积) 面积 对房价的影响 f(建造年份) 年份

GAM 的局限性:忽略了特征交互

GAM最大的局限性在于其可加性的假设。

该模型假设每个特征 \(x_j\)\(y\) 的影响独立于其他特征 \(x_k\) 的值。它无法自动捕捉特征之间的交互作用 (interaction effects)

例如: 广告支出 (\(x_1\)) 对销售额 (\(y\)) 的影响,可能取决于当前的季节 (\(x_2\))。在夏季,冰淇淋广告的效果可能远好于冬季。这种“效果的加成”是标准GAM无法捕捉的。

全新视角:支持向量机 (SVM)

接下来,我们将学习一个在机器学习领域极具影响力的模型:支持向量机 (Support Vector Machine, SVM)

SVM 最初是为分类问题设计的,但也可以扩展到回归问题 (SVR)。它的核心思想与我们之前学过的模型有很大不同,它关注的是位于决策边界上的数据点。

SVM 的基本思想:哪条分界线最好?

想象一个二分类问题。我们可以找到很多条直线(或超平面)来分开两类数据点。

多条可能的决策边界 一堆蓝色和红色的点,三条不同的直线都分开了它们。 哪一条是最好的?

SVM 的回答是:那条能够以最大“间隔”(Margin)将两类数据分开的线是最好的。

SVM 的核心:最大化分类间隔

间隔 (Margin): 决策边界与离它最近的任何一类数据点之间的距离。一个更大的间隔意味着我们的决策边界更鲁棒。

SVM的最大间隔图示 图示了决策边界、间隔和支持向量。 决策边界 支持向量 最大间隔

核心概念:支持向量 (Support Vectors)

那些恰好位于间隔边界上的数据点,被称为支持向量 (Support Vectors)

  • 它们是“支撑”起整个决策边界的关键点。
  • 如果移动任何一个支持向量,决策边界就会改变。
  • 如果移动任何一个非支持向量(远离边界的点),决策边界不会改变。

这是SVM的核心洞察: 决策边界仅由一小部分最难分类的数据点(支持向量)决定。

从硬间隔到软间隔:处理非线性可分数据

“最大间隔分类器”有一个很强的假设:数据是线性可分的。如果数据有重叠,怎么办?我们引入软间隔 (Soft Margin)

软间隔SVM 图示软间隔分类器,允许一些点在间隔内或者被错误分类。 错误分类! 在间隔内!

权衡: 模型需要在最大化间隔减少分类错误之间找到一个平衡点。这个平衡由一个超参数 \(C\) (代价参数) 来控制。

代价参数 C 的作用

  • 小的 C: 更大的间隔,容忍更多的分类错误(更“软”)。可能导致高偏差
  • 大的 C: 更小的间隔,严格惩罚分类错误。可能导致高方差(过拟合)

SVM 的代价函数:合页损失 (Hinge Loss)

SVM 的这种思想体现在其独特的损失函数——合页损失 (Hinge Loss) 中。

\[ \large{J(w) = \frac{1}{n} \sum_{i=1}^{n} \max(0, 1 - y^{(i)} f(x^{(i)})) + \lambda ||w||^2} \]

  • 合页损失项: \(\max(0, 1 - y^{(i)} f(x^{(i)}))\)
    • 对于被正确分类且与决策边界保持足够距离(\(y^{(i)} f(x^{(i)}) \ge 1\))的点,其损失为零
  • 正则化项: \(\lambda ||w||^2\)
    • 与最大化间隔在数学上是等价的。

合页损失 vs. 逻辑损失:可视化对比

Figure 4 直观地展示了两种损失函数的区别。\(y \cdot f(x)\) 的值越大,代表分类越正确且置信度越高。

def hinge_loss(z): return np.maximum(0, 1 - z)
def log_loss(z): return np.log(1 + np.exp(-z))

z = np.linspace(-2, 3, 200)

fig, ax = plt.subplots(figsize=(8.5, 5))
ax.plot(z, hinge_loss(z), label='合页损失 (Hinge Loss)', color='#2980b9', linewidth=3)
ax.plot(z, log_loss(z), label='逻辑损失 (Log Loss)', color='#c0392b', linewidth=2, linestyle='--')
ax.axhline(0, color='black', linewidth=0.8)
ax.axvline(0, color='black', linewidth=0.8)
ax.axvline(1, color='grey', linewidth=1, linestyle=':')
ax.text(1, -0.2, '1', ha='center', fontsize=12)
ax.set_xlabel('$y \cdot f(x)$ (分类正确性得分)')
ax.set_ylabel('损失 (Loss)')
ax.set_title('损失函数对比')
ax.legend()
ax.set_ylim(-0.2, 3)
ax.set_xlim(-2, 3)
plt.show()
Figure 4: 合页损失(蓝线)与逻辑损失(红线)的比较

终极武器:核技巧 (The Kernel Trick)

线性SVM只能找到线性的决策边界。如果数据本身需要一个曲线边界才能分开呢?

笨方法: 手动添加特征。例如,如果原始特征是 \(x_1\),我们可以创建一个新特征 \(x_2 = x_1^2\)。然后在这个新的二维空间 \((x_1, x_1^2)\) 中,数据可能就变成线性可分的了。

问题: 我们怎么知道要添加什么特征?如果原始特征维度很高,创建新特征会导致维度爆炸,计算成本极高。

优雅的解决方案: 核技巧 (The Kernel Trick)

核技巧的可视化:从一维到二维

假设我们有一维数据,无法用一个点(一维的“线”)来分开。

Figure 5 展示了这种情况。红色的x和蓝色的点混杂在一起,无法线性分离。

np.random.seed(0)
X_1d = np.sort(np.random.normal(0, 1, 100))
y_1d = (X_1d > -0.8) & (X_1d < 0.8)

fig, ax = plt.subplots(figsize=(10, 2))
ax.scatter(X_1d[y_1d == 0], np.zeros(np.sum(y_1d==0)), c='#c0392b', marker='x', s=60, label='类别 0', linewidth=1.5)
ax.scatter(X_1d[y_1d == 1], np.zeros(np.sum(y_1d==1)), c='#2980b9', marker='o', s=50, label='类别 1', alpha=0.8)
ax.set_yticks([])
ax.set_xlabel('x')
ax.set_title('一维空间中线性不可分的数据')
ax.legend(loc='center right')
ax.spines['left'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.grid(False)
plt.show()
Figure 5: 在一维空间中,这些数据点无法被一个点线性分离

核技巧的可视化:映射到高维空间

现在,我们做一个简单的映射:将每个点 \(x\) 映射到一个二维空间 \((x, x^2)\)

Figure 6 展示了映射后的结果。原本在一维混杂的数据,在二维空间形成了一个抛物线。现在,我们可以轻易地用一条直线将它们分开了!

X_2d = np.c_[X_1d, X_1d**2]

fig, ax = plt.subplots()
ax.scatter(X_2d[y_1d == 0, 0], X_2d[y_1d == 0, 1], c='#c0392b', marker='x', s=60, label='类别 0', linewidth=1.5)
ax.scatter(X_2d[y_1d == 1, 0], X_2d[y_1d == 1, 1], c='#2980b9', marker='o', s=50, label='类别 1', alpha=0.8)
x_line = np.linspace(-2.5, 2.5, 100)
y_line = np.full_like(x_line, 0.6)
ax.plot(x_line, y_line, color='#27ae60', label='决策边界', linewidth=3)
ax.set_xlabel('x')
ax.set_ylabel('$x^2$')
ax.set_title('映射到二维空间后数据变为线性可分')
ax.legend()
plt.show()
Figure 6: 将数据映射到二维空间 (x, x^2) 后,可以用一条直线分离

核技巧的数学精髓

关键洞察: 在SVM的求解过程中,我们实际上不需要知道高维空间中点的具体坐标,我们只需要知道数据点在高维空间中的点积 (dot product)

核函数 (Kernel Function) \(K(x^{(i)}, x^{(j)})\) 就是一个能高效计算高维空间中点积的函数,它无需先进行显式的维度转换。

\[ \large{K(x^{(i)}, x^{(j)}) = \phi(x^{(i)}) \cdot \phi(x^{(j)})} \]

其中 \(\phi(x)\) 是将数据从低维映射到高维的函数。我们使用 \(K\) 来计算,从而避免了计算和存储巨大的 \(\phi(x)\) 向量。

常用核函数及其作用

有多种常用的核函数,它们对应着不同类型的非线性决策边界。

核函数 公式 直觉与超参数
线性核 (Linear) \(K(x^{(i)}, x^{(j)}) = x^{(i)} \cdot x^{(j)}\) 不进行维度提升,就是标准的线性SVM。
多项式核 (Polynomial) \(K(x^{(i)}, x^{(j)}) = (c + x^{(i)} \cdot x^{(j)})^d\) 创造多项式组合特征。超参数 d (degree) 控制决策边界的弯曲程度。d 越大,模型越复杂。
径向基函数核 (RBF) \(K(x^{(i)}, x^{(j)}) = \exp(-\gamma \|x^{(i)} - x^{(j)}\|^2)\) 创造非常复杂的、局部化的决策边界。可以看作是无限维度的映射。超参数 gamma 控制单个样本的影响范围。gamma 越大,影响范围越小,决策边界越“崎岖”,更容易过拟合。

最后一种方法:K-近邻 (KNN)

最后,我们来学习一种非常不同但极其直观的方法:K-近邻 (K-Nearest Neighbors, KNN)

  • 性质: 它是一种非参数 (non-parametric) 方法,意味着它不对数据的潜在分布做任何假设。它也是一种懒惰学习 (lazy learning) 算法,因为它没有一个显式的“训练”阶段。
  • 核心思想: “物以类聚,人以群分”。要预测一个新数据点的类别,我们只需查看它在特征空间中最近的 K 个邻居,然后采取“少数服从多数”的原则。

KNN算法的可视化步骤

如何用KNN预测一个新点(灰色问号)的类别? (假设 K=5)

KNN分类过程 一个新点被五个最近的邻居包围,其中三个是蓝色,两个是红色,因此新点被分类为蓝色。 ? 1. 找到 K=5 个最近的邻居 2. 投票: 3个蓝色, 2个红色. ➡️ 结论: 蓝色

KNN算法的关键要素

  1. 距离度量 (Distance Metric): 如何衡量“远近”?
    • 欧几里得距离 (Euclidean Distance): 空间中两点间的直线距离。
    • 曼哈顿距离 (Manhattan Distance): “城市街区”距离。
  2. 邻居数量 K:
    • 这是一个关键的超参数,它控制了模型的偏差-方差权衡
    • K 太小: 模型对噪声敏感,决策边界复杂,导致高方差(过拟合)
    • K 太大: 模型过度平滑,忽略局部结构,导致高偏差(欠拟合)

K值对决策边界的影响

K值对KNN决策边界的影响 两张图,左图K=1时决策边界很复杂,右图K=20时决策边界很平滑。 K = 1 (高方差) 决策边界非常“崎岖” K = 20 (高偏差) 决策边界非常平滑

重要提示: 由于KNN完全基于距离计算,因此在使用前对特征进行标准化 (Standardization) 至关重要!

代码实战:SVM 与 KNN 分类器应用

任务: 我们将使用 sklearn 内置的威斯康星州乳腺癌数据集作为示例,构建分类器预测一个肿瘤是良性还是恶性。这是一个经典的、干净的二分类数据集,非常适合演示。

模型: 1. 线性SVM分类器 2. KNN分类器

特征: 我们将选择4个特征来简化问题:'mean radius', 'mean texture', 'mean perimeter', 'mean area'

步骤1: 导入库并加载数据

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_breast_cancer

# 加载数据
cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = pd.Series(cancer.target)

# 为保持与原始幻灯片的一致性,我们只选择4个特征
fea_cols = ['mean radius', 'mean texture', 'mean perimeter', 'mean area']
X = X[fea_cols]

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
print('训练数据维度:', X_train.shape)
训练数据维度: (398, 4)

步骤2: 数据预处理 - 标准化

由于SVM和KNN都对特征的尺度敏感,标准化是一个必不可少的步骤。我们将使用 StandardScaler 来处理。

# 创建StandardScaler对象
scaler = StandardScaler()

# 在训练数据上拟合scaler并转换训练数据
X_train_scaled = scaler.fit_transform(X_train)

# 使用同一个scaler转换测试数据 (重要!)
X_test_scaled = scaler.transform(X_test)

# 为了方便查看,转换为DataFrame
X_train_scaled_df = pd.DataFrame(X_train_scaled, columns=fea_cols)
print('标准化后的训练数据(前5行):')
print(X_train_scaled_df.head().round(4))
标准化后的训练数据(前5行):
   mean radius  mean texture  mean perimeter  mean area
0      -0.1235       -0.2968         -0.1705    -0.2086
1      -0.2283       -0.6580         -0.2538    -0.2965
2       0.1455       -1.2306          0.2458    -0.0102
3      -0.3585       -0.6722         -0.4009    -0.4000
4      -0.1575        0.9672         -0.2088    -0.2415

步骤3: 训练和评估线性SVM分类器

我们创建一个使用线性核的SVC(Support Vector Classifier)对象,在标准化的数据上进行训练,并在测试集上评估其准确率。

# 创建一个线性核的SVM分类器
clf_svm = SVC(kernel='linear', random_state=42)

# 在标准化的训练数据上训练模型
clf_svm.fit(X_train_scaled, y_train)

# 在测试集上进行预测
y_pred_svm = clf_svm.predict(X_test_scaled)

# 计算并打印准确率
accuracy_svm = accuracy_score(y_test, y_pred_svm)
print(f'线性SVM分类器的准确率: {accuracy_svm:.4f}')
线性SVM分类器的准确率: 0.9181

步骤4: 训练和评估KNN分类器

接下来,我们创建一个KNN分类器。我们将超参数K(n_neighbors)设置为5,这是一个常见的初始值。

# 创建一个K=5的KNN分类器
clf_knn = KNeighborsClassifier(n_neighbors=5)

# 在标准化的训练数据上训练模型
clf_knn.fit(X_train_scaled, y_train)

# 在测试集上进行预测
y_pred_knn = clf_knn.predict(X_test_scaled)

# 计算并打印准确率
accuracy_knn = accuracy_score(y_test, y_pred_knn)
print(f'KNN (K=5) 分类器的准确率: {accuracy_knn:.4f}')
KNN (K=5) 分类器的准确率: 0.9123

习题解析与代码实现

现在,我们来完成编程练习,以加深对这些模型超参数的理解。

练习1: 探索SVM中RBF核与gamma参数

任务: 使用 rbf 核重新训练SVM模型,并尝试将 gamma 参数分别设为 0.01, 0.1, 1, 10, 20,观察预测表现的变化。gamma 定义了单个训练样本的影响范围。

gamma_values = [0.01, 0.1, 1, 5, 10, 20]
results = []

for g in gamma_values:
    clf_svm_rbf = SVC(kernel='rbf', gamma=g, random_state=42)
    clf_svm_rbf.fit(X_train_scaled, y_train)
    y_pred_rbf = clf_svm_rbf.predict(X_test_scaled)
    acc = accuracy_score(y_test, y_pred_rbf)
    results.append({'gamma': g, 'accuracy': f'{acc:.4f}'})

print(pd.DataFrame(results).to_string(index=False))
 gamma accuracy
  0.01   0.9006
  0.10   0.9064
  1.00   0.9181
  5.00   0.9181
 10.00   0.9181
 20.00   0.9064

分析: 我们可以看到,随着 gamma 值的增大,模型的准确率先升后降。当 gamma 过大时,模型开始记忆训练数据,导致泛化能力下降,这是典型的过拟合

练习2: 探索KNN中近邻数量K的影响

任务: 在KNN分类器中,将近邻数量 n_neighbors (K) 分别调整为 1, 3, 6, 10, 20,并输出模型的预测表现。

k_values = [1, 3, 6, 10, 20]
results_knn = []

for k in k_values:
    clf_knn_k = KNeighborsClassifier(n_neighbors=k)
    clf_knn_k.fit(X_train_scaled, y_train)
    y_pred_k = clf_knn_k.predict(X_test_scaled)
    acc_k = accuracy_score(y_test, y_pred_k)
    results_knn.append({'K (n_neighbors)': k, 'accuracy': f'{acc_k:.4f}'})

print(pd.DataFrame(results_knn).to_string(index=False))
 K (n_neighbors) accuracy
               1   0.9181
               3   0.9181
               6   0.9123
              10   0.8947
              20   0.9181

分析: K=1时模型过于复杂(高方差),而随着K的增加,模型决策边界变得更平滑,准确率先升后降。当K过大时,模型可能变得过于简单(高偏差)。

练习3: 探索KNN中距离度量的影响

任务: KNN分类器默认使用欧几里得距离。请在K=6时,改用曼哈顿距离 (cityblock),并检验结果是否有不同。

# 欧几里得距离 (默认)
knn_euclidean = KNeighborsClassifier(n_neighbors=6, metric='euclidean')
knn_euclidean.fit(X_train_scaled, y_train)
y_pred_euc = knn_euclidean.predict(X_test_scaled)
acc_euc = accuracy_score(y_test, y_pred_euc)
print(f'KNN (K=6, 欧几里得距离) 准确率: {acc_euc:.4f}')

# 曼哈顿距离
knn_manhattan = KNeighborsClassifier(n_neighbors=6, metric='cityblock')
knn_manhattan.fit(X_train_scaled, y_train)
y_pred_man = knn_manhattan.predict(X_test_scaled)
acc_man = accuracy_score(y_test, y_pred_man)
print(f'KNN (K=6, 曼哈顿距离) 准确率: {acc_man:.4f}')
KNN (K=6, 欧几里得距离) 准确率: 0.9123
KNN (K=6, 曼哈顿距离) 准确率: 0.9123

分析: 在这个特定的数据集和K值下,两种距离度量得到了相似的结果。在其他数据(特别是高维数据)中,曼哈顿距离有时可能比欧几里得距离表现得更稳健。

开放性思考:广义可加模型(GAM)的应用场景

问题: 请思考在什么具体的(经济学或金融学)场景中我们需要用到广义可加模型?

回答思路: GAM 的核心优势在于处理非线性关系的同时保持可解释性。因此,它最适用于那些我们不仅想预测结果,更想理解各个因素如何影响结果的场景。

场景 变量关系 为何适用GAM?
个人薪酬模型 预测薪酬 (y) 经验(x1): 薪酬随经验增长,但增速可能放缓 (非线性)。年龄(x2): 薪酬与年龄可能是倒U型关系。GAM可以分别捕捉这两种效应并展示其曲线。
房地产定价 预测房价 (y) 房屋面积(x1): 存在边际效用递减。楼层(x2): 过低或过高可能不受欢迎,呈非线性关系。建造年份(x3): 老房子可能因历史价值而贵,也可能因老旧而便宜。
信用评分 预测违约概率 (y) 债务收入比(x1): 风险并非线性增加,可能在某个阈值后急剧上升。账户历史长度(x2): 历史过短或过长都可能与风险相关。

总结:我们的非线性工具箱

今天,我们为自己的计量经济学工具箱增添了四件强大的新武器。

  • 样条 (Splines):
    • 通过分段多项式提供了极大的灵活性,是构建更复杂模型的基础。
  • 广义可加模型 (GAMs):
    • 将非线性建模扩展到多变量,同时通过可加性保持了宝贵的可解释性。
  • 支持向量机 (SVM):
    • 基于“最大间隔”思想的强大分类器,通过核技巧能有效处理复杂的非线性边界。
  • K-近邻 (KNN):
    • 一种简单、直观的非参数方法,其核心是基于“邻居”进行投票。

如何选择合适的模型?

模型 主要优点 主要缺点 适用场景
样条/GAM 可解释性强,能灵活捕捉非线性关系 难以捕捉特征交互 当理解变量如何影响结果至关重要时
SVM (核) 对高维数据有效,能构建非常复杂的决策边界 计算成本高,可解释性较差(“黑箱”) 分类问题,特别是特征维度高、边界复杂时
KNN 模型简单直观,非参数 对大数据集预测慢,对特征尺度敏感,需要选择K 数据量不大,需要一个快速的基线模型时

核心思想: 每种模型都有其独特的优势和适用场景。作为经济学家和数据科学家,我们的任务是理解这些工具的原理和权衡,为我们的问题选择最合适的模型。