Python 拟合、欠拟合与过拟合

521 阅读6分钟

一、什么是拟合(Fit)

拟合 = 让模型学会数据里的规律。

  • 欠拟合(Underfitting)
    模型太简单,连基本趋势都没学会。
  • 过拟合(Overfitting)
    模型太复杂,把训练数据的噪声都记住了,导致新数据表现很差。

欠拟合 = 学得太少
过拟合 = 学得太死


二、直观理解(用曲线拟合作类比)

假设真实曲线很复杂(例如 S 型曲线):

  • 欠拟合(模型太简单)
    用一条直线去拟合 S 型 → 明显不行。
  • 正常拟合
    用一个适度的多项式 → 刚好贴近真实趋势。
  • 过拟合(模型太复杂)
    用 15 次多项式逼近 10 个点 → 训练误差小,但曲线左右乱跳。

三、经典 Python 代码示例(Polynomial Regression)

以下示例用正弦波 + 噪声的数据来演示:

  • degree = 1 → 欠拟合
  • degree = 4 → 正常
  • degree = 15 → 过拟合

📦 依赖

pip install scikit-learn matplotlib numpy

🧪 完整代码

# 导入必要的Python库
# numpy: 用于数值计算的基础库
# matplotlib.pyplot: 用于数据可视化的绘图库
# PolynomialFeatures: scikit-learn中的多项式特征生成器
# LinearRegression: scikit-learn中的线性回归模型
# mean_squared_error: scikit-learn中的均方误差计算函数
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# ==================== 数据生成阶段 ====================
# 设置随机种子为0,确保每次运行程序时生成的随机数序列相同,使实验结果可重现
np.random.seed(0)

# 在[0, 2π]区间内生成30个均匀分布的点,作为输入特征X
# np.linspace(start, stop, num): 在指定区间内生成指定数量的等间距点
X = np.linspace(0, 2 * np.pi, 30)

# 生成目标变量y:基于正弦函数并添加高斯白噪声
# np.sin(X): 计算X中每个元素的正弦值,构成基本的正弦波形
# np.random.randn(30): 生成30个服从标准正态分布(均值为0,标准差为1)的随机数
# 0.2 * np.random.randn(30): 将标准正态分布的随机数缩放0.2倍,作为噪声项
# 最终y是真实的正弦函数值加上小幅度的随机噪声,模拟现实世界中的测量数据
y = np.sin(X) + 0.2 * np.random.randn(30)

# 将一维数组X重塑为二维数组,每行一个样本,每列一个特征
# reshape(-1, 1): -1表示自动推断该维度的大小,1表示每行只有一个特征
# 这是scikit-learn算法要求的数据格式
X = X.reshape(-1, 1)

# ==================== 定义拟合和绘图函数 ====================
def fit_and_plot(degree, subplot):
    """
    使用指定degree阶的多项式对数据进行拟合并可视化结果

    参数说明:
    degree: int类型,表示多项式的最高次数
    subplot: int类型,表示在图形中的子图位置(1, 2, 或 3)
    """

    # 创建多项式特征转换器对象,指定多项式的最高次数
    # PolynomialFeatures(degree=d)会将原始特征x转换为[1, x, x^2, ..., x^d]的特征向量
    poly = PolynomialFeatures(degree=degree)

    # 对原始特征X进行多项式特征转换
    # fit_transform方法首先学习数据的统计特性,然后执行特征转换
    # X_poly是一个二维数组,形状为[n_samples, degree+1]
    X_poly = poly.fit_transform(X)

    # 创建线性回归模型对象
    # LinearRegression实现了普通最小二乘法线性回归
    model = LinearRegression()

    # 使用多项式特征X_poly和目标值y训练线性回归模型
    # 模型会学习到一个线性组合的权重系数,使得预测值尽可能接近真实值
    model.fit(X_poly, y)

    # ==================== 模型预测阶段 ====================
    # 生成更密集的测试数据点用于绘制平滑的拟合曲线
    # 在[0, 2π]区间内生成200个点,比原始数据点更密集
    X_test = np.linspace(0, 2 * np.pi, 200).reshape(-1, 1)

    # 使用之前训练好的多项式特征转换器对测试数据进行特征转换
    # 注意这里使用的是transform而不是fit_transform,因为我们不需要重新学习统计特性
    X_test_poly = poly.transform(X_test)

    # 使用训练好的线性回归模型对测试数据进行预测
    # predict方法返回模型对输入特征的预测值
    y_pred = model.predict(X_test_poly)

    # ==================== 结果可视化阶段 ====================
    # 在指定的子图位置激活绘图区域
    # subplot(1, 3, subplot)表示创建1行3列的子图布局,subplot指定当前操作的子图索引
    plt.subplot(1, 3, subplot)

    # 绘制原始数据的散点图
    # scatter方法用于绘制散点图,X是横坐标,y是纵坐标
    # color="black"指定散点颜色为黑色,label="data"设置图例标签
    plt.scatter(X, y, color="black", label="data")

    # 绘制模型的预测曲线
    # plot方法用于绘制连续曲线,X_test是横坐标,y_pred是纵坐标
    # label=f"degree={degree}"使用f-string格式化字符串设置图例标签
    plt.plot(X_test, y_pred, label=f"degree={degree}")

    # 设置当前子图的标题
    # f"degree={degree}"使用f-string格式化字符串显示多项式的阶数
    plt.title(f"degree={degree}")

    # 显示图例,自动使用之前设置的label参数
    plt.legend()

# ==================== 创建图形窗口 ====================
# 创建一个新的图形窗口并设置图形尺寸
# figsize=(15, 4)指定图形的宽度为15英寸,高度为4英寸
plt.figure(figsize=(15, 4))

# ==================== 分别展示三种拟合情况 ====================
# 第一幅子图:欠拟合情况 - 使用1次多项式(线性模型)
# 1次多项式只能拟合直线,对于非线性的正弦函数会产生较大的偏差
# subplot参数为1,表示在第一幅子图中绘制结果
fit_and_plot(1, 1)

# 第二幅子图:良好拟合情况 - 使用4次多项式
# 4次多项式具有足够的灵活性来逼近正弦函数的形状
# subplot参数为2,表示在第二幅子图中绘制结果
fit_and_plot(4, 2)

# 第三幅子图:过拟合情况 - 使用15次多项式
# 15次多项式过于灵活,会过度拟合训练数据中的噪声
# subplot参数为3,表示在第三幅子图中绘制结果
fit_and_plot(15, 3)

# 显示所有绘制的图形
# show方法将图形渲染到屏幕上
plt.show()

效果


四、训练效果对比

模型复杂度结果原因
degree = 1欠拟合模型太简单
degree = 4正常拟合复杂度刚好
degree = 15过拟合学到了噪声

五、工程实践中如何避免过拟合?

  1. 降低模型复杂度(减少多项式次数、降低树深度等)
  2. 正则化 L1/L2
  3. 交叉验证(k-fold)
  4. 增加数据量
  5. 早停法(Early stopping,神经网络常用)
  6. Dropout(深度学习)