课程回顾:线性模型的基石与局限
我们已经掌握了强大的线性模型,它们是计量经济学的核心。
核心假设: 目标变量与特征之间存在 线性关系。
- 线性回归: \(y = w^T x + b\)
- 逻辑回归: \(P(y=1|x) = \sigma(w^T x + b)\)
现实的挑战:当线性假设失效
当关系是非线性时,强行使用线性模型会导致系统性的预测偏差和错误的结论。
本章学习目标:构建我们的非线性工具箱
本章我们将学习一系列强大的监督学习方法,以捕捉复杂的非线性关系。
- 样条模型 (Splines): 通过分段函数灵活地拟合数据。
- 广义可加模型 (GAMs): 将非线性模型扩展到多预测变量,同时保持可解释性。
- 支持向量机 (SVM): 一种强大的分类技术,通过“核技巧”处理非线性边界。
- K近邻 (KNN): 一种简单、直观、非参数的分类和回归方法。
经济学中的非线性关系无处不在
线性假设的失效并非个例,而是常态。
解决方案一:回归样条 (Regression Splines)
核心思想: 与其用一个复杂的全局函数拟合所有数据,不如将数据的定义域切分成多个区域,在每个区域内用简单的函数(如多项式)来拟合。
这就像修建一条穿过山脉的铁路:我们不会试图用一个巨大的圆弧来匹配整个山脉轮廓,而是在不同路段使用不同的坡度和曲线,然后将它们平滑地连接起来。
样条的关键概念:结点 (Knots)
结点 (Knot) 是我们分割数据区域的点,也是模型函数形式发生变化的地方。
在累进税率的例子中,每个税率区间的起点就是一个天然的结点。
一个简单的分段线性样条模型
假设我们只有一个结点 \(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\) 处的曲率也必须相等。
可视化连续性约束
对于一个 \(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()
实践中的挑战:如何选择结点?
样条的灵活性也带来了它最大的挑战:我们应该在哪里放置结点?以及,应该放置多少个结点?
- 结点过多: 模型过于灵活,可能导致对数据的过拟合 (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\) 计算了函数在整个定义域上的总弯曲程度。
- 这一项惩罚“摇摆不定”的函数,鼓励模型选择更平滑的曲线。
理解平滑参数 \(\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
,传入我们的 x
和 y
数据,并设置平滑参数 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()
探索平滑参数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()
从单变量到多变量:广义可加模型 (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)\) 都是一个独立的“积木块”,模型最终的预测是把这些积木块的效果“加”起来。
GAM 的巨大优势:保持了可解释性
尽管GAM是一个非线性模型,但它通过其可加性 (additivity) 结构,奇迹般地保留了大部分线性模型的可解释性。
- 我们可以独立地检查每一个函数 \(f_j(x_j)\) 的图形。
- \(f_j(x_j)\) 的图形揭示了特征 \(x_j\) 对目标变量 \(y\) 的非线性影响,同时控制了其他所有变量。
模拟GAM输出:洞察非线性关系
假设我们用GAM预测房价,我们可以得到类似下图的洞察:
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): 决策边界与离它最近的任何一类数据点之间的距离。一个更大的间隔意味着我们的决策边界更鲁棒。
核心概念:支持向量 (Support Vectors)
那些恰好位于间隔边界上的数据点,被称为支持向量 (Support Vectors)。
- 它们是“支撑”起整个决策边界的关键点。
- 如果移动任何一个支持向量,决策边界就会改变。
- 如果移动任何一个非支持向量(远离边界的点),决策边界不会改变。
这是SVM的核心洞察: 决策边界仅由一小部分最难分类的数据点(支持向量)决定。
从硬间隔到软间隔:处理非线性可分数据
“最大间隔分类器”有一个很强的假设:数据是线性可分的。如果数据有重叠,怎么办?我们引入软间隔 (Soft Margin)。
权衡: 模型需要在最大化间隔和减少分类错误之间找到一个平衡点。这个平衡由一个超参数 \(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()
终极武器:核技巧 (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()
核技巧的可视化:映射到高维空间
现在,我们做一个简单的映射:将每个点 \(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()
核技巧的数学精髓
关键洞察: 在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算法的关键要素
- 距离度量 (Distance Metric): 如何衡量“远近”?
- 欧几里得距离 (Euclidean Distance): 空间中两点间的直线距离。
- 曼哈顿距离 (Manhattan Distance): “城市街区”距离。
- 邻居数量 K:
- 这是一个关键的超参数,它控制了模型的偏差-方差权衡。
- K 太小: 模型对噪声敏感,决策边界复杂,导致高方差(过拟合)。
- K 太大: 模型过度平滑,忽略局部结构,导致高偏差(欠拟合)。
K值对决策边界的影响
重要提示: 由于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)
步骤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}')
步骤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 的核心优势在于处理非线性关系的同时保持可解释性。因此,它最适用于那些我们不仅想预测结果,更想理解各个因素如何影响结果的场景。
个人薪酬模型 |
预测薪酬 (y) |
经验(x1): 薪酬随经验增长,但增速可能放缓 (非线性)。年龄(x2): 薪酬与年龄可能是倒U型关系。GAM可以分别捕捉这两种效应并展示其曲线。 |
房地产定价 |
预测房价 (y) |
房屋面积(x1): 存在边际效用递减。楼层(x2): 过低或过高可能不受欢迎,呈非线性关系。建造年份(x3): 老房子可能因历史价值而贵,也可能因老旧而便宜。 |
信用评分 |
预测违约概率 (y) |
债务收入比(x1): 风险并非线性增加,可能在某个阈值后急剧上升。账户历史长度(x2): 历史过短或过长都可能与风险相关。 |
总结:我们的非线性工具箱
今天,我们为自己的计量经济学工具箱增添了四件强大的新武器。
- 样条 (Splines):
- 通过分段多项式提供了极大的灵活性,是构建更复杂模型的基础。
- 广义可加模型 (GAMs):
- 将非线性建模扩展到多变量,同时通过可加性保持了宝贵的可解释性。
- 支持向量机 (SVM):
- 基于“最大间隔”思想的强大分类器,通过核技巧能有效处理复杂的非线性边界。
- K-近邻 (KNN):
- 一种简单、直观的非参数方法,其核心是基于“邻居”进行投票。
如何选择合适的模型?
样条/GAM |
可解释性强,能灵活捕捉非线性关系 |
难以捕捉特征交互 |
当理解变量如何影响结果至关重要时 |
SVM (核) |
对高维数据有效,能构建非常复杂的决策边界 |
计算成本高,可解释性较差(“黑箱”) |
分类问题,特别是特征维度高、边界复杂时 |
KNN |
模型简单直观,非参数 |
对大数据集预测慢,对特征尺度敏感,需要选择K |
数据量不大,需要一个快速的基线模型时 |
核心思想: 每种模型都有其独特的优势和适用场景。作为经济学家和数据科学家,我们的任务是理解这些工具的原理和权衡,为我们的问题选择最合适的模型。