行莫
行莫
发布于 2026-01-21 / 3 阅读
0
0

拟合

拟合

1. 拟合的核心概念

1.1 专业定义

**拟合(Fitting)**是指机器学习模型通过学习训练数据,找到能够描述数据内在规律的函数或模式的过程。

从更严谨的角度来说:

  • 经验风险最小化:拟合的目标是让模型在训练数据上的预测误差最小化
  • 泛化能力:理想的拟合不仅要让模型在训练数据上表现好,还要能在新的、未见过的数据上表现良好,也可以理解为举一反三的能力
  • 模型逼近数据规律:拟合的本质是让模型函数尽可能接近数据的真实生成规律

1.2 数学表达

拟合的核心逻辑可以用最小二乘损失函数来表达:

$$
L = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2
$$

其中:

  • $y_i$:第 $i$ 个样本的真实值(观测值)
  • $\hat{y}_i$:第 $i$ 个样本的模型预测值
  • $n$:样本数量

核心思想:让 $\hat{y}_i$(模型预测值)尽可能接近 $y_i$(真实值),即最小化预测误差的平方和。

1.3 通俗解读

拟合的本质:让模型学到数据的"真实规律",而不是噪声或偶然特征。

  • 如果模型太简单,学不到核心规律 → 欠拟合
  • 如果模型刚好学到核心规律 → 拟合(理想状态)
  • 如果模型太复杂,把噪声也当规律学了 → 过拟合

模型 = 算法 + 训练后的参数
三个关键要素:
算法(Algorithm):学习方法
例如:线性回归、决策树、神经网络
定义模型的结构和训练方式
数据(Data):训练材料
用于训练模型的样本
数据本身不是模型的一部分,而是模型的“输入”
参数(Parameters):训练后学到的权重
例如:线性回归的系数 $w$ 和偏置 $b$
这是模型的核心,是算法在数据上训练后得到的

2. 拟合的直观理解

直说”拟合“大家会觉得比较抽象,下面举几个实际的例子帮助理解”拟合“概念。

类比 1:学习场景

幼儿园小朋友学习加法运算,都是10以内的两个数字相加。

  • 欠拟合:没学会,连简单题也做错

    • 对应:模型太简单,没抓住数据的核心规律
    • 表现:训练误差大,测试误差也大
  • 拟合(理想状态):学生理解知识点,能做对基础题和变式题,比如三个数相加也能做对

    • 对应:模型学到核心规律,泛化能力强
    • 表现:训练误差小,测试误差也小
  • 过拟合:学生死记硬背例题只会做固定题型,换题型就错,例如同样的两个数换了顺序就错

    • 对应:模型学了噪声和偶然特征,泛化能力差
    • 表现:训练误差很小,但测试误差很大

类比 2:物理场景

想象地面上散落着一些钉子,你要用一根铁丝去贴合它们:

数据点 = 散落在地面的钉子(有些钉子可能位置不准,代表噪声)拟合曲线 = 一根铁丝

  • 欠拟合:铁丝太硬(模型太简单),无法贴近多数钉子

    • 铁丝形状固定,无法弯曲,只能大致经过钉子区域
    • 对应:模型复杂度不足,无法捕捉数据的主要趋势
  • 拟合(理想状态):铁丝软硬适中,能贴合多数钉子

    • 铁丝能弯曲,贴合大部分钉子的位置,但不会过度弯曲
    • 对应:模型复杂度适中,捕捉核心趋势,忽略噪声
  • 过拟合:铁丝太软(模型太复杂),缠绕每个钉子

    • 铁丝过度弯曲,连位置不准的钉子(噪声)也去贴合
    • 对应:模型过度复杂,把噪声也当规律学了

3. 演示:欠拟合 / 拟合 / 过拟合

让我们通过代码和图形,直观地看到三种拟合状态的差异。

真实模型(ε为高斯噪声): $y = 2x^2 + 3x + 1 + \varepsilon$

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体,避免中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False

# ========== 第一步:生成含噪声的真实数据 ==========
# 真实模型:y = 2x² + 3x + 1 + ε(ε为高斯噪声)

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

# 生成100个样本点,x在[-5, 5]范围内随机分布
n_samples = 100
X = np.random.uniform(-5, 5, n_samples).reshape(-1, 1)
X_sorted = np.sort(X, axis=0)  # 排序后的X,用于绘制平滑曲线

# 计算真实值(无噪声)
y_true = 2 * X.flatten()**2 + 3 * X.flatten() + 1

# 添加高斯噪声(标准差为5),模拟真实数据
noise = np.random.normal(0, 5, n_samples)
y = y_true + noise

# ========== 第二步:训练三类模型 ==========

# 1. 欠拟合:线性模型(y = ax + b)
linear_model = LinearRegression()
linear_model.fit(X, y)
y_pred_underfit = linear_model.predict(X_sorted)

# 2. 理想拟合:二次多项式模型(y = ax² + bx + c)
poly2_model = Pipeline([
    ('poly', PolynomialFeatures(degree=2)),
    ('linear', LinearRegression())
])
poly2_model.fit(X, y)
y_pred_fit = poly2_model.predict(X_sorted)

# 3. 过拟合:10次多项式模型(y = a₁₀x¹⁰ + ... + a₀)
poly10_model = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('linear', LinearRegression())
])
poly10_model.fit(X, y)
y_pred_overfit = poly10_model.predict(X_sorted)

# ========== 第三步:计算拟合误差(MSE) ==========

mse_underfit = mean_squared_error(y, linear_model.predict(X))
mse_fit = mean_squared_error(y, poly2_model.predict(X))
mse_overfit = mean_squared_error(y, poly10_model.predict(X))

print("=" * 60)
print("训练集上的均方误差(MSE):")
print(f"欠拟合(线性模型):    {mse_underfit:.2f}")
print(f"理想拟合(二次多项式):  {mse_fit:.2f}")
print(f"过拟合(10次多项式):    {mse_overfit:.2f}")
print("=" * 60)

============================================================
训练集上的均方误差(MSE):
欠拟合(线性模型):    263.08
理想拟合(二次多项式):  19.43
过拟合(10次多项式):    18.50
============================================================

3.1 图示解读

让我们分析每个子图的特征:

图1:欠拟合示例

  • 特征:红色拟合曲线(直线)无法贴合数据的整体趋势
  • 问题:模型太简单(线性),无法捕捉数据的非线性规律(真实模型是二次函数)
  • 表现:训练误差(MSE)较大,曲线与数据点偏差明显
  • 类比:就像用直尺去测量弯曲的路径,永远无法准确贴合

图2:理想拟合示例

  • 特征:红色拟合曲线(二次曲线)能够很好地贴合数据的主要趋势
  • 优势:模型复杂度适中(二次多项式),刚好匹配真实模型
  • 表现:训练误差(MSE)较小,曲线平滑且贴合数据点
  • 类比:就像用合适的工具去贴合钉子,既贴合又不过度弯曲

图3:过拟合示例

  • 特征:红色拟合曲线(10次多项式)过度弯曲,试图经过每个数据点
  • 问题:模型太复杂,连噪声(随机误差)也去拟合
  • 表现:训练误差(MSE)很小,但曲线波动剧烈,泛化能力差
  • 类比:就像铁丝太软,连位置不准的钉子也去缠绕,导致整体形状扭曲

4. 欠拟合、过拟合问题成因分析

理解拟合问题的成因,有助于我们找到针对性的解决方案。我们可以从模型偏差-方差权衡理论出发,从三个维度分析:

4.1 欠拟合(高偏差、低方差)

核心特征:模型在训练集和测试集上表现都很差

核心原因:模型复杂度不足

  • 用线性模型去拟合非线性数据
  • 模型表达能力有限,无法捕捉数据的复杂规律

其他原因:

  1. 特征工程不到位

    • 关键特征缺失(如用 $x$ 去拟合 $x^2$ 的规律)
    • 特征选择不当,遗漏了重要信息
  2. 训练迭代次数不足

    • 对于迭代式模型(如梯度下降),模型还没学够就停止了
    • 参数更新不充分,模型未达到最优状态

通俗总结模型太简单,学不会数据的核心规律

4.2 过拟合(低偏差、高方差)

核心特征:模型在训练集上表现很好,但在测试集上表现很差

核心原因:模型复杂度过高

  • 高次多项式、深层神经网络等复杂模型
  • 模型参数过多,容易记住训练数据的细节和噪声

其他原因:

  1. 数据质量差

    • 噪声多、异常值未处理
    • 数据标注错误,引入错误信息
  2. 数据量不足

    • 训练样本太少,模型"死记硬背"少量数据
    • 无法学到真正的规律,只能记住具体样本
  3. 训练过度

    • 迭代次数过多,模型过度拟合训练数据
    • 把训练集中的噪声和偶然特征也当规律学了

通俗总结模型太复杂,把噪声当规律学了

4.3 偏差-方差权衡

偏差(Bias):模型预测值与真实值的差距

  • 高偏差 → 欠拟合(模型太简单,预测不准)

方差(Variance):模型预测值的波动程度

  • 高方差 → 过拟合(模型太复杂,预测不稳定)

理想状态低偏差 + 低方差 = 模型既准确又稳定

5. 欠拟合、过拟合问题优化方案

5.1 欠拟合的优化方案

方法1:增加模型复杂度

原理:将简单模型(如线性模型)替换为更复杂的模型(如多项式模型)

# ========== 方法1:增加模型复杂度 ==========
# 对比线性模型(欠拟合)和二次多项式模型(理想拟合)

# 生成新的测试数据
X_test = np.linspace(-5, 5, 50).reshape(-1, 1)
y_test_true = 2 * X_test.flatten()**2 + 3 * X_test.flatten() + 1
y_test = y_test_true + np.random.normal(0, 5, 50)

# 训练线性模型(欠拟合)
linear_model_opt = LinearRegression()
linear_model_opt.fit(X, y)
mse_train_linear = mean_squared_error(y, linear_model_opt.predict(X))
mse_test_linear = mean_squared_error(y_test, linear_model_opt.predict(X_test))

# 训练二次多项式模型(优化后)
poly2_model_opt = Pipeline([
    ('poly', PolynomialFeatures(degree=2)),
    ('linear', LinearRegression())
])
poly2_model_opt.fit(X, y)
mse_train_poly2 = mean_squared_error(y, poly2_model_opt.predict(X))
mse_test_poly2 = mean_squared_error(y_test, poly2_model_opt.predict(X_test))

方法2:补充关键特征

原理:添加高阶特征(如 $x^2$、$x^3$),让模型能够捕捉非线性规律

# ========== 方法2:补充关键特征 ==========
# 对比只有x特征和添加x²特征的效果

# 只有x特征(欠拟合)
X_simple = X  # 只有原始特征
linear_simple = LinearRegression()
linear_simple.fit(X_simple, y)
mse_train_simple = mean_squared_error(y, linear_simple.predict(X_simple))

# 添加x²特征(优化后)
X_with_square = np.hstack([X, X**2])  # 添加x²特征
linear_with_features = LinearRegression()
linear_with_features.fit(X_with_square, y)
mse_train_features = mean_squared_error(y, linear_with_features.predict(X_with_square))

方法3:延长训练迭代次数

原理:对于迭代式模型(如梯度下降),增加训练轮数,让模型充分学习

5.2 过拟合的优化方案

方法1:正则化(L1正则 - Lasso)

在损失函数中添加参数的绝对值之和作为惩罚项。

$$L = \text{原损失函数} + \lambda \sum_{i=1}^{n} |w_i|$$
其中:
$\lambda$:正则化强度(超参数)
$|w_i|$:第 $i$ 个参数的绝对值

特点

  • 产生稀疏模型:很多参数会被压缩到 0
  • 自动特征选择:不重要的特征系数变为 0
  • 适合特征数量多、但只有少数特征重要的场景

不适合本文中的示例问题,不适合使用。

代码示例

from sklearn.linear_model import Lasso

# L1正则化
lasso = Lasso(alpha=1.0)  # alpha是正则化强度λ
lasso.fit(X_train, y_train)
# 很多特征的系数会被压缩到0
print(lasso.coef_)  # 很多系数为0

方法2:正则化(L2正则 - Ridge)

在损失函数中添加参数的平方和作为惩罚项。

$$L = \text{原损失函数} + \lambda \sum_{i=1}^{n} w_i^2$$
其中:
$\lambda$:正则化强度(超参数)
$w_i^2$:第 $i$ 个参数的平方

特点

  • 参数平滑缩小:所有参数都会变小,但不会完全为 0
  • 防止参数过大:避免极端参数值
  • 适合所有特征都重要,但需要防止过拟合的场景
from sklearn.linear_model import Ridge, Lasso

# ========== 方法1:正则化(L2正则 - Ridge) ==========
# 对比10次多项式(过拟合)和带正则化的10次多项式

# 生成测试集(用于评估泛化能力)
X_test_reg = np.random.uniform(-5, 5, 50).reshape(-1, 1)
y_test_reg_true = 2 * X_test_reg.flatten()**2 + 3 * X_test_reg.flatten() + 1
y_test_reg = y_test_reg_true + np.random.normal(0, 5, 50)

# 10次多项式(过拟合,无正则化)
poly10_no_reg = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('linear', LinearRegression())
])
poly10_no_reg.fit(X, y)
mse_train_no_reg = mean_squared_error(y, poly10_no_reg.predict(X))
mse_test_no_reg = mean_squared_error(y_test_reg, poly10_no_reg.predict(X_test_reg))

# 10次多项式 + L2正则化(Ridge,优化后)
poly10_ridge = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('ridge', Ridge(alpha=1.0))  # alpha是正则化强度
])
poly10_ridge.fit(X, y)
mse_train_ridge = mean_squared_error(y, poly10_ridge.predict(X))
mse_test_ridge = mean_squared_error(y_test_reg, poly10_ridge.predict(X_test_reg))

方法3:交叉验证

原理:将数据分成K折,轮流用K-1折训练,1折验证,评估模型的泛化能力

from sklearn.model_selection import cross_val_score, KFold

# ========== 方法2:交叉验证 ==========
# 使用K折交叉验证评估模型泛化能力,选择最佳模型复杂度

# 准备数据
X_cv = X
y_cv = y

# 测试不同多项式次数
degrees = [1, 2, 3, 5, 10, 15]
cv_scores = []
train_scores = []

kfold = KFold(n_splits=5, shuffle=True, random_state=42)

for degree in degrees:
    # 创建模型
    model = Pipeline([
        ('poly', PolynomialFeatures(degree=degree)),
        ('linear', LinearRegression())
    ])
    
    # 交叉验证得分(负MSE,所以越大越好)
    cv_mse_scores = -cross_val_score(model, X_cv, y_cv, cv=kfold, scoring='neg_mean_squared_error')
    cv_scores.append(cv_mse_scores.mean())
    
    # 训练集得分
    model.fit(X_cv, y_cv)
    train_pred = model.predict(X_cv)
    train_scores.append(mean_squared_error(y_cv, train_pred))

======================================================================
方法2:交叉验证结果
======================================================================
多项式次数           训练MSE           交叉验证MSE              说明
----------------------------------------------------------------------
1               263.08          275.54               欠拟合
2               19.43           20.68                理想拟合 ← 最佳
3               19.31           21.80                过拟合
5               19.04           24.00                过拟合
10              18.50           29.16                过拟合
15              17.92           63.29                过拟合
======================================================================

方法4:数据增强

原理:对现有数据添加微小噪声或变换,模拟更多样本,防止模型过度记忆训练数据

# ========== 方法4:数据增强 ==========
# 通过添加噪声和变换,扩充训练数据

# 原始数据(样本少,容易过拟合)
X_original = X[:30]  # 只用30个样本
y_original = y[:30]

# 数据增强:添加微小噪声
n_augment = 3  # 每个样本生成3个增强样本
X_augmented = [X_original]
y_augmented = [y_original]

for _ in range(n_augment):
    # 添加微小的高斯噪声
    X_noise = X_original + np.random.normal(0, 0.3, X_original.shape)
    y_noise = y_original + np.random.normal(0, 1, y_original.shape)
    X_augmented.append(X_noise)
    y_augmented.append(y_noise)

X_augmented = np.vstack(X_augmented)
y_augmented = np.hstack(y_augmented)

# 训练:原始数据(容易过拟合)
poly10_original = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('linear', LinearRegression())
])
poly10_original.fit(X_original, y_original)
mse_train_original = mean_squared_error(y_original, poly10_original.predict(X_original))
mse_test_original = mean_squared_error(y_test_reg, poly10_original.predict(X_test_reg))

# 训练:增强数据(优化后)
poly10_augmented = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('linear', LinearRegression())
])
poly10_augmented.fit(X_augmented, y_augmented)
mse_train_augmented = mean_squared_error(y_augmented, poly10_augmented.predict(X_augmented))
mse_test_augmented = mean_squared_error(y_test_reg, poly10_augmented.predict(X_test_reg))

方法5:简化模型

原理:减少模型复杂度(如降低多项式次数、减少神经网络层数),防止模型过度拟合

# ========== 方法5:简化模型 ==========
# 对比10次多项式(过拟合)和2次多项式(简化后)

# 10次多项式(过拟合)
poly10_complex = Pipeline([
    ('poly', PolynomialFeatures(degree=10)),
    ('linear', LinearRegression())
])
poly10_complex.fit(X, y)
mse_train_complex = mean_squared_error(y, poly10_complex.predict(X))
mse_test_complex = mean_squared_error(y_test_reg, poly10_complex.predict(X_test_reg))

# 2次多项式(简化后)
poly2_simple = Pipeline([
    ('poly', PolynomialFeatures(degree=2)),
    ('linear', LinearRegression())
])
poly2_simple.fit(X, y)
mse_train_simple = mean_squared_error(y, poly2_simple.predict(X))
mse_test_simple = mean_squared_error(y_test_reg, poly2_simple.predict(X_test_reg))

6. 总结

6.1 核心总结

拟合的本质是平衡模型复杂度,让偏差方差达到最优平衡点。

  • 偏差-方差权衡:模型太简单 → 高偏差(欠拟合);模型太复杂 → 高方差(过拟合)
  • 理想状态:模型复杂度适中,既能捕捉数据规律,又不会过度拟合噪声
  • 优化策略
    • 欠拟合:增加模型复杂度、补充特征、延长训练
    • 过拟合:正则化、交叉验证、数据增强、简化模型

评论