从龙格现象到机器学习过拟合:一个NumPy实验的启示

在数据科学和机器学习领域,我们常常被一个看似矛盾的现象所困扰:为什么一个在训练数据上表现近乎完美的模型,在实际应用中却可能一败涂地?这个问题背后隐藏着机器学习最核心的概念之一——过拟合。今天,我们不从抽象的理论出发,而是通过一个经典的数学现象——龙格现象(Runge Phenomenon),用Python和NumPy亲手构建实验,直观理解这个困扰无数数据科学家的难题。

龙格现象最早由德国数学家卡尔·龙格(Carl Runge)在1901年发现,它展示了高次多项式插值可能带来的灾难性后果。有趣的是,这种现象与机器学习中的过拟合问题有着惊人的相似性。通过这个实验,你不仅能理解龙格现象本身,更能获得对模型复杂度、泛化能力等机器学习核心概念的直观把握。我们将使用Jupyter Notebook环境,从定义函数、采样、拟合到可视化,一步步重现这个经典现象,并思考它对机器学习实践的启示。

1. 实验准备与环境设置

在开始我们的探索之前,让我们先准备好实验环境。这个实验只需要Python和一些基础的科学计算库,大多数数据科学环境都已预装了这些工具。

首先,我们需要导入必要的Python库:

import numpy as np
import matplotlib.pyplot as plt
from numpy.polynomial import Polynomial

提示:如果你使用的是Jupyter Notebook,记得在开头添加%matplotlib inline魔法命令,以便在笔记本中直接显示图形。

我们将研究的函数是龙格最初使用的经典例子:

f(x) = 1 / (1 + 25x²)

这个函数在区间[-1,1]上表现良好,但当我们在等距节点上使用高次多项式进行插值时,就会出现明显的振荡现象,特别是在区间端点附近。

让我们先定义这个函数并绘制它的图像:

def runge_function(x):
    return 1 / (1 + 25 * x**2)

x = np.linspace(-2, 2, 10000)
y = runge_function(x)

plt.figure(figsize=(10, 6))
plt.plot(x, y, label='True function')
plt.title("Runge Function")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.legend()
plt.show()

这段代码会生成一个平滑的钟形曲线,这是我们希望用多项式来逼近的目标函数。接下来,我们将从这个函数中采样一些点作为我们的"训练数据"。

2. 采样与多项式拟合

在机器学习中,我们通常只有有限的样本数据,而不是完整的真实函数。为了模拟这种情况,我们从Runge函数中均匀采样一些点作为我们的训练数据。

n_samples = 7  # 使用较少的样本点来突出现象
testx = np.linspace(-2, 2, n_samples)
testy = runge_function(testx)

plt.figure(figsize=(10, 6))
plt.scatter(testx, testy, color='red', label='Sample points')
plt.plot(x, y, label='True function')
plt.title(f"Runge Function with {n_samples} Sample Points")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.legend()
plt.show()

现在,我们将尝试用不同次数的多项式来拟合这些样本点。我们将比较3次、5次和7次多项式的拟合效果。

degrees = [3, 5, 7]
colors = ['green', 'orange', 'red']
polynomials = []

plt.figure(figsize=(10, 6))
plt.scatter(testx, testy, color='blue', label='Sample points')
plt.plot(x, y, label='True function')

for degree, color in zip(degrees, colors):
    coeffs = np.polyfit(testx, testy, degree)
    poly = np.poly1d(coeffs)
    polynomials.append(poly)
    plt.plot(x, poly(x), color=color, label=f'Degree {degree}')

plt.title("Polynomial Fits of Different Degrees")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.legend()
plt.show()

观察这些拟合曲线,我们能看到什么?低次多项式(如3次)虽然不能完美通过所有样本点,但整体上较好地捕捉了真实函数的形状。而随着多项式次数的增加,曲线开始剧烈振荡,特别是在区间边缘附近——这就是龙格现象的典型表现。

3. 训练误差与测试误差的对比

在机器学习中,我们通常关注两个关键指标:训练误差(模型在训练数据上的表现)和测试误差(模型在新数据上的表现)。让我们计算一下不同多项式在这两个指标上的表现。

首先定义计算均方误差的函数:

def mse(y_true, y_pred):
    return np.mean((y_true - y_pred)**2)

然后计算训练误差:

train_errors = []
for poly in polynomials:
    y_pred = poly(testx)
    train_errors.append(mse(testy, y_pred))

print("Training MSE:")
for degree, error in zip(degrees, train_errors):
    print(f"Degree {degree}: {error:.2e}")

不出所料,随着多项式次数的增加,训练误差会越来越小——高次多项式可以更精确地通过训练点。但是,这是以在测试点(即真实函数上的其他点)上表现变差为代价的。

让我们在更密集的点集上计算测试误差:

x_test = np.linspace(-2, 2, 1000)
y_test = runge_function(x_test)

test_errors = []
for poly in polynomials:
    y_pred = poly(x_test)
    test_errors.append(mse(y_test, y_pred))

print("\nTest MSE:")
for degree, error in zip(degrees, test_errors):
    print(f"Degree {degree}: {error:.2e}")

这个结果揭示了机器学习中过拟合的核心特征:随着模型复杂度(这里表现为多项式次数)的增加,训练误差持续降低,但测试误差(泛化误差)却先减小后增大。模型开始"记忆"训练数据中的噪声和特定特征,而不是学习底层的一般规律。

4. 从龙格现象到机器学习实践

龙格现象不仅仅是一个有趣的数学现象,它对机器学习实践有着深刻的启示。让我们探讨几个关键点:

模型复杂度与泛化能力的权衡

  • 欠拟合区域:模型过于简单(如低次多项式),无法捕捉数据中的基本模式,表现为训练误差和测试误差都较高。
  • 合适复杂度区域:模型复杂度适中,能够很好地泛化,测试误差达到最低点。
  • 过拟合区域:模型过于复杂(如高次多项式),完美拟合训练数据但泛化能力差,训练误差很低但测试误差很高。

应对过拟合的策略

  1. 增加训练数据量:龙格现象在等距节点上表现最明显,但如果使用切比雪夫节点(Chebyshev nodes)进行采样,可以显著减轻振荡现象。类似地,在机器学习中,更多的训练数据可以帮助模型更好地泛化。

  2. 正则化技术:在多项式拟合中,我们可以通过限制系数的大小来抑制剧烈振荡。这对应于机器学习中的L1/L2正则化,通过在损失函数中添加惩罚项来限制模型参数的大小。

  3. 交叉验证:使用验证集来评估模型性能,选择在验证集上表现最好的模型复杂度,而不是单纯追求训练集上的完美拟合。

  4. 模型选择:有时问题不在于多项式次数,而在于模型本身的形式。对于Runge函数,有理函数逼近可能比多项式更合适。在机器学习中,这意味着要根据问题特点选择合适的模型架构。

实际应用中的启示

  • 不要盲目追求训练集上的高精度,这可能意味着模型已经过拟合。
  • 监控训练误差和验证误差的对比,当两者差距变大时,可能是过拟合的信号。
  • 在资源允许的情况下,收集更多高质量数据往往是最有效的解决方案。
  • 理解数据的底层特性有助于选择合适的模型和预处理方法。

通过这个简单的多项式拟合实验,我们直观地理解了机器学习中过拟合的本质。下次当你面对一个复杂模型在训练数据上表现完美但实际应用不佳时,不妨回想一下龙格现象——有时候,简单才是美。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐