行莫
行莫
发布于 2025-11-22 / 6 阅读
0
0

机器学习拟合概念详解:从欠拟合到过拟合

机器学习拟合概念详解:从欠拟合到过拟合

引言

想象一下三种不同的学习方式:

  • 欠拟合:就像学生没学会,练习题都做不对
  • 正常拟合:就像学生学会了方法,练习题和新题都能做
  • 过拟合:就像学生只记住了练习题,新题不会做

理解拟合概念,就像理解学习的三种状态。本文将用生动的类比、数学表示和实际代码,帮你深入理解机器学习的拟合概念。


第一部分:拟合(Fitting)是什么?

什么是拟合?

**拟合(Fitting)**是模型学习数据中的模式,使模型能够很好地描述数据。

类比理解:

  • 就像用一条曲线拟合数据点
  • 就像学习数据的规律
  • 就像找到数据的最佳描述

数学表示

给定训练数据集:
$$
D = {(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)}
$$

拟合的目标:
找到一个函数 $f$,使得:
$$
f(x_i) \approx y_i, \quad \forall i = 1, 2, ..., n
$$

类比理解:

  • $x_i$:就像输入(题目)
  • $y_i$:就像输出(答案)
  • $f$:就像学会的方法
  • 拟合:就像学会方法,能够解题目

拟合的两种误差

1. 训练误差(Training Error)

定义:
$$
E_{train} = \frac{1}{n} \sum_{i=1}^{n} \ell(f(x_i), y_i)
$$

类比理解:

  • 就像练习题的错误率
  • 就像在训练数据上的表现

2. 泛化误差(Generalization Error)

定义:
$$
E_{gen} = \mathbb{E}_{(x,y) \sim P} [\ell(f(x), y)]
$$

其中 $P$ 是数据的真实分布。

类比理解:

  • 就像新题的错误率
  • 就像在未见过的数据上的表现

拟合的目标

理想情况:

  • 训练误差小:在训练数据上表现好
  • 泛化误差小:在新数据上表现好
  • 训练误差 ≈ 泛化误差:模型泛化能力强

类比理解:

  • 就像既能在练习题上做对,也能在新题上做对
  • 就像既学会了方法,也能应用方法

第二部分:欠拟合(Underfitting)- "没学会"

什么是欠拟合?

**欠拟合(Underfitting)**是模型过于简单,无法学习数据中的模式。

类比理解:

  • 就像学生没学会,练习题都做不对
  • 就像用直线拟合曲线数据
  • 就像工具太简单,无法完成任务

欠拟合的特征

表现:

  • 训练误差大:在训练数据上表现差
  • 验证误差大:在验证数据上表现差
  • 训练误差 ≈ 验证误差:但都很高

类比理解:

  • 就像练习题做不对,新题也做不对
  • 就像既没学会方法,也不会应用

欠拟合的数学表示

假设:

  • 真实函数:$y = x^2 + \epsilon$(二次函数)
  • 模型:$f(x) = ax + b$(线性函数)

结果:

  • 模型太简单,无法拟合二次关系
  • 训练误差和验证误差都很大

欠拟合的示例

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# 生成数据(二次关系)
np.random.seed(42)
X = np.linspace(-3, 3, 100).reshape(-1, 1)
y = X.flatten()**2 + np.random.normal(0, 0.5, 100)

# 划分数据集
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

# 欠拟合模型(线性模型)
model_underfit = LinearRegression()
model_underfit.fit(X_train, y_train)

# 预测
y_train_pred_underfit = model_underfit.predict(X_train)
y_test_pred_underfit = model_underfit.predict(X_test)

# 计算误差
train_error_underfit = mean_squared_error(y_train, y_train_pred_underfit)
test_error_underfit = mean_squared_error(y_test, y_test_pred_underfit)

print("欠拟合模型:")
print(f"训练误差:{train_error_underfit:.3f}")
print(f"测试误差:{test_error_underfit:.3f}")

# 可视化
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.scatter(X_train, y_train, alpha=0.5, label='训练数据')
plt.plot(X_train, y_train_pred_underfit, 'r-', linewidth=2, label='模型预测')
plt.title(f'欠拟合(训练误差:{train_error_underfit:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.subplot(1, 2, 2)
plt.scatter(X_test, y_test, alpha=0.5, label='测试数据')
plt.plot(X_test, y_test_pred_underfit, 'r-', linewidth=2, label='模型预测')
plt.title(f'欠拟合(测试误差:{test_error_underfit:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

输出示例:

欠拟合模型:
训练误差:3.456
测试误差:3.789

可视化结果:

  • 模型是一条直线,无法拟合二次曲线
  • 训练误差和测试误差都很大
  • 模型过于简单

欠拟合的原因

1. 模型复杂度不足

类比理解:

  • 就像工具太简单,无法完成任务
  • 就像用直线拟合曲线数据

例子:

  • 用线性模型拟合非线性数据
  • 用简单模型拟合复杂数据

2. 特征不足

类比理解:

  • 就像信息不够,无法做出正确判断
  • 就像缺少关键特征

例子:

  • 只用面积预测房价,缺少房间数、地段等特征
  • 只用像素值识别图像,缺少纹理、形状等特征

3. 训练不充分

类比理解:

  • 就像练习不够,没学会
  • 就像训练时间太短

例子:

  • 训练轮数太少
  • 学习率太小,收敛太慢

4. 正则化过强

类比理解:

  • 就像限制太多,无法学习
  • 就像约束太强,无法发挥

例子:

  • L1/L2正则化系数太大
  • Dropout比例太高

解决欠拟合的方法

1. 增加模型复杂度

# 使用多项式特征
poly_features = PolynomialFeatures(degree=2)
X_train_poly = poly_features.fit_transform(X_train)
X_test_poly = poly_features.transform(X_test)

model_complex = LinearRegression()
model_complex.fit(X_train_poly, y_train)

y_train_pred_complex = model_complex.predict(X_train_poly)
y_test_pred_complex = model_complex.predict(X_test_poly)

train_error_complex = mean_squared_error(y_train, y_train_pred_complex)
test_error_complex = mean_squared_error(y_test, y_test_pred_complex)

print("增加复杂度后:")
print(f"训练误差:{train_error_complex:.3f}")
print(f"测试误差:{test_error_complex:.3f}")

2. 增加特征

# 添加更多特征
X_train_features = np.hstack([X_train, X_train**2, X_train**3])
X_test_features = np.hstack([X_test, X_test**2, X_test**3])

model_features = LinearRegression()
model_features.fit(X_train_features, y_train)

3. 减少正则化

from sklearn.linear_model import Ridge

# 减少正则化系数
model_less_regularization = Ridge(alpha=0.01)  # 较小的alpha
model_less_regularization.fit(X_train, y_train)

4. 增加训练时间

from sklearn.neural_network import MLPRegressor

# 增加训练轮数
model_more_iter = MLPRegressor(hidden_layer_sizes=(50,), 
                               max_iter=1000,  # 增加迭代次数
                               random_state=42)
model_more_iter.fit(X_train, y_train)

第三部分:过拟合(Overfitting)- "只记住了"

什么是过拟合?

**过拟合(Overfitting)**是模型过于复杂,记住了训练数据的细节,但无法泛化到新数据。

类比理解:

  • 就像学生只记住了练习题,新题不会做
  • 就像用高次多项式拟合数据,记住了每个点
  • 就像工具太复杂,记住了训练数据,但不会应用

过拟合的特征

表现:

  • 训练误差小:在训练数据上表现很好
  • 验证误差大:在验证数据上表现差
  • 训练误差 << 验证误差:差距很大

类比理解:

  • 就像练习题做得很好,但新题做不对
  • 就像记住了方法,但不会应用

过拟合的数学表示

假设:

  • 真实函数:$y = x^2 + \epsilon$(二次函数)
  • 模型:$f(x) = a_0 + a_1x + a_2x^2 + ... + a_{10}x^{10}$(10次多项式)

结果:

  • 模型太复杂,拟合了每个训练点
  • 训练误差很小,但验证误差很大

过拟合的示例

from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline

# 过拟合模型(高次多项式)
degree_overfit = 15  # 15次多项式
poly_overfit = PolynomialFeatures(degree=degree_overfit)
X_train_poly_overfit = poly_overfit.fit_transform(X_train)
X_test_poly_overfit = poly_overfit.transform(X_test)

model_overfit = LinearRegression()
model_overfit.fit(X_train_poly_overfit, y_train)

# 预测
y_train_pred_overfit = model_overfit.predict(X_train_poly_overfit)
y_test_pred_overfit = model_overfit.predict(X_test_poly_overfit)

# 计算误差
train_error_overfit = mean_squared_error(y_train, y_train_pred_overfit)
test_error_overfit = mean_squared_error(y_test, y_test_pred_overfit)

print("过拟合模型:")
print(f"训练误差:{train_error_overfit:.3f}")
print(f"测试误差:{test_error_overfit:.3f}")
print(f"误差差距:{test_error_overfit - train_error_overfit:.3f}")

# 可视化
plt.figure(figsize=(12, 5))

# 生成平滑曲线用于可视化
X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
X_plot_poly = poly_overfit.transform(X_plot)
y_plot_pred = model_overfit.predict(X_plot_poly)

plt.subplot(1, 2, 1)
plt.scatter(X_train, y_train, alpha=0.5, label='训练数据')
plt.plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
plt.title(f'过拟合(训练误差:{train_error_overfit:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.subplot(1, 2, 2)
plt.scatter(X_test, y_test, alpha=0.5, label='测试数据')
plt.plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
plt.title(f'过拟合(测试误差:{test_error_overfit:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

输出示例:

过拟合模型:
训练误差:0.012
测试误差:2.345
误差差距:2.333

可视化结果:

  • 模型曲线非常复杂,几乎经过每个训练点
  • 训练误差很小,但测试误差很大
  • 模型过于复杂,无法泛化

过拟合的原因

1. 模型过于复杂

类比理解:

  • 就像工具太复杂,记住了训练数据
  • 就像用高次多项式拟合数据

例子:

  • 神经网络层数太多、神经元太多
  • 决策树深度太深
  • 多项式次数太高

2. 训练数据不足

类比理解:

  • 就像练习题太少,只记住了这几道题
  • 就像样本太少,模型记住了每个样本

例子:

  • 只有10个样本,但模型有100个参数
  • 数据量远小于模型复杂度

3. 训练时间过长

类比理解:

  • 就像练习过度,只记住了练习题
  • 就像训练太久,记住了训练数据

例子:

  • 训练轮数太多
  • 学习率太小,过度优化

4. 噪声数据

类比理解:

  • 就像练习题有错误,学会了错误的方法
  • 就像数据有噪声,模型学习了噪声

例子:

  • 训练数据包含噪声
  • 模型学习了噪声而不是真实模式

5. 特征过多

类比理解:

  • 就像信息太多,记住了无关信息
  • 就像特征太多,学习了噪声特征

例子:

  • 特征数量接近或超过样本数量
  • 包含无关特征

解决过拟合的方法

1. 增加正则化

from sklearn.linear_model import Ridge, Lasso

# L2正则化(Ridge)
model_ridge = Ridge(alpha=1.0)  # 正则化系数
model_ridge.fit(X_train_poly_overfit, y_train)

# L1正则化(Lasso)
model_lasso = Lasso(alpha=0.1)
model_lasso.fit(X_train_poly_overfit, y_train)

类比理解:

  • 就像限制模型复杂度
  • 就像防止模型过于复杂

2. 减少模型复杂度

# 使用较低次数的多项式
degree_optimal = 2  # 2次多项式
poly_optimal = PolynomialFeatures(degree=degree_optimal)
X_train_poly_optimal = poly_optimal.fit_transform(X_train)
X_test_poly_optimal = poly_optimal.transform(X_test)

model_optimal = LinearRegression()
model_optimal.fit(X_train_poly_optimal, y_train)

3. 增加训练数据

# 生成更多训练数据
X_train_more = np.linspace(-3, 3, 500).reshape(-1, 1)
y_train_more = X_train_more.flatten()**2 + np.random.normal(0, 0.5, 500)

# 用更多数据训练
model_more_data = LinearRegression()
model_more_data.fit(X_train_poly_overfit, y_train_more)

类比理解:

  • 就像做更多练习题,学会方法而不是记住题目
  • 就像增加样本,让模型学习模式而不是记忆数据

4. 早停(Early Stopping)

from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import validation_curve

# 早停:当验证误差不再下降时停止训练
model_early_stop = MLPRegressor(hidden_layer_sizes=(50,),
                                max_iter=1000,
                                early_stopping=True,  # 启用早停
                                validation_fraction=0.2,
                                random_state=42)
model_early_stop.fit(X_train, y_train)

类比理解:

  • 就像在最佳时机停止练习
  • 就像防止过度训练

5. Dropout(神经网络)

# Dropout:随机丢弃一些神经元
from tensorflow import keras
from tensorflow.keras import layers

model_dropout = keras.Sequential([
    layers.Dense(64, activation='relu'),
    layers.Dropout(0.5),  # 50%的神经元被丢弃
    layers.Dense(32, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

类比理解:

  • 就像随机忽略一些信息,防止过度记忆
  • 就像强制模型学习更通用的模式

6. 集成方法

from sklearn.ensemble import RandomForestRegressor

# 随机森林:多个简单模型的集成
model_ensemble = RandomForestRegressor(
    n_estimators=100,
    max_depth=5,  # 限制深度,防止过拟合
    random_state=42
)
model_ensemble.fit(X_train, y_train)

类比理解:

  • 就像多个简单模型投票,减少过拟合
  • 就像多个专家共同决策,更可靠

第四部分:正常拟合(Good Fitting)- "学会了"

什么是正常拟合?

**正常拟合(Good Fitting)**是模型既能在训练数据上表现好,也能在新数据上表现好。

类比理解:

  • 就像学生学会了方法,练习题和新题都能做
  • 就像用合适的曲线拟合数据
  • 就像工具合适,既能完成任务,也能应用

正常拟合的特征

表现:

  • 训练误差小:在训练数据上表现好
  • 验证误差小:在验证数据上表现好
  • 训练误差 ≈ 验证误差:差距很小

类比理解:

  • 就像练习题做得对,新题也做得对
  • 就像既学会了方法,也能应用方法

正常拟合的示例

# 正常拟合模型(2次多项式,适合二次数据)
degree_good = 2
poly_good = PolynomialFeatures(degree=degree_good)
X_train_poly_good = poly_good.fit_transform(X_train)
X_test_poly_good = poly_good.transform(X_test)

model_good = LinearRegression()
model_good.fit(X_train_poly_good, y_train)

# 预测
y_train_pred_good = model_good.predict(X_train_poly_good)
y_test_pred_good = model_good.predict(X_test_poly_good)

# 计算误差
train_error_good = mean_squared_error(y_train, y_train_pred_good)
test_error_good = mean_squared_error(y_test, y_test_pred_good)

print("正常拟合模型:")
print(f"训练误差:{train_error_good:.3f}")
print(f"测试误差:{test_error_good:.3f}")
print(f"误差差距:{abs(test_error_good - train_error_good):.3f}")

# 可视化
plt.figure(figsize=(12, 5))

X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
X_plot_poly = poly_good.transform(X_plot)
y_plot_pred = model_good.predict(X_plot_poly)

plt.subplot(1, 2, 1)
plt.scatter(X_train, y_train, alpha=0.5, label='训练数据')
plt.plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
plt.title(f'正常拟合(训练误差:{train_error_good:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.subplot(1, 2, 2)
plt.scatter(X_test, y_test, alpha=0.5, label='测试数据')
plt.plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
plt.title(f'正常拟合(测试误差:{test_error_good:.3f})')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

输出示例:

正常拟合模型:
训练误差:0.234
测试误差:0.267
误差差距:0.033

可视化结果:

  • 模型曲线平滑,很好地拟合了数据
  • 训练误差和测试误差都小,且接近
  • 模型复杂度合适,泛化能力强

三种拟合的对比

# 对比三种拟合
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

models_info = [
    (model_underfit, X_train, y_train, X_test, y_test, 
     train_error_underfit, test_error_underfit, "欠拟合"),
    (model_good, X_train_poly_good, y_train, X_test_poly_good, y_test,
     train_error_good, test_error_good, "正常拟合"),
    (model_overfit, X_train_poly_overfit, y_train, X_test_poly_overfit, y_test,
     train_error_overfit, test_error_overfit, "过拟合")
]

for idx, (model, X_tr, y_tr, X_te, y_te, err_tr, err_te, title) in enumerate(models_info):
    # 预测
    y_tr_pred = model.predict(X_tr)
    y_te_pred = model.predict(X_te)
    
    # 可视化测试集
    axes[idx].scatter(X_te, y_te, alpha=0.5, label='测试数据')
    
    # 对于多项式模型,绘制平滑曲线
    if idx == 1:  # 正常拟合
        X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
        X_plot_poly = poly_good.transform(X_plot)
        y_plot_pred = model.predict(X_plot_poly)
        axes[idx].plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
    elif idx == 2:  # 过拟合
        X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
        X_plot_poly = poly_overfit.transform(X_plot)
        y_plot_pred = model.predict(X_plot_poly)
        axes[idx].plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
    else:  # 欠拟合
        X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
        y_plot_pred = model.predict(X_plot)
        axes[idx].plot(X_plot, y_plot_pred, 'r-', linewidth=2, label='模型预测')
    
    axes[idx].set_title(f'{title}\n训练误差:{err_tr:.3f}, 测试误差:{err_te:.3f}')
    axes[idx].set_xlabel('x')
    axes[idx].set_ylabel('y')
    axes[idx].legend()
    axes[idx].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 打印对比表
print("\n三种拟合对比:")
print("=" * 60)
print(f"{'类型':<15} {'训练误差':<15} {'测试误差':<15} {'误差差距':<15}")
print("=" * 60)
print(f"{'欠拟合':<15} {train_error_underfit:<15.3f} {test_error_underfit:<15.3f} {abs(test_error_underfit - train_error_underfit):<15.3f}")
print(f"{'正常拟合':<15} {train_error_good:<15.3f} {test_error_good:<15.3f} {abs(test_error_good - train_error_good):<15.3f}")
print(f"{'过拟合':<15} {train_error_overfit:<15.3f} {test_error_overfit:<15.3f} {abs(test_error_overfit - train_error_overfit):<15.3f}")
print("=" * 60)

输出:

三种拟合对比:
============================================================
类型            训练误差         测试误差         误差差距        
============================================================
欠拟合          3.456           3.789           0.333
正常拟合        0.234           0.267           0.033
过拟合          0.012           2.345           2.333
============================================================

第五部分:泛化(Generalization)- "会应用"

什么是泛化?

**泛化(Generalization)**是模型在未见过的数据上表现良好的能力。

类比理解:

  • 就像学会了方法,新题也能做
  • 就像学会了技能,新情况也能应用
  • 就像学会了规律,新数据也能预测

泛化的数学表示

泛化误差:
$$
E_{gen} = \mathbb{E}_{(x,y) \sim P} [\ell(f(x), y)]
$$

其中 $P$ 是数据的真实分布。

泛化差距(Generalization Gap):
$$
\text{Gap} = E_{gen} - E_{train}
$$

类比理解:

  • 泛化误差:就像在新题上的错误率
  • 泛化差距:就像新题和练习题的差距

泛化能力评估

from sklearn.model_selection import cross_val_score

# 使用交叉验证评估泛化能力
cv_scores = cross_val_score(model_good, X_train_poly_good, y_train, 
                           cv=5, scoring='neg_mean_squared_error')

print("交叉验证结果(评估泛化能力):")
print(f"各折得分:{-cv_scores}")
print(f"平均得分:{-cv_scores.mean():.3f}")
print(f"标准差:{cv_scores.std():.3f}")

影响泛化的因素

1. 模型复杂度

偏差-方差权衡(Bias-Variance Tradeoff):

偏差(Bias):

  • 模型本身的误差
  • 欠拟合时偏差大

方差(Variance):

  • 模型对训练数据变化的敏感性
  • 过拟合时方差大

总误差:
$$
\text{Total Error} = \text{Bias}^2 + \text{Variance} + \text{Irreducible Error}
$$

类比理解:

  • 偏差:就像方法的系统性误差
  • 方差:就像方法的不稳定性
  • 总误差:就像总体错误率

2. 训练数据量

类比理解:

  • 数据越多,泛化越好
  • 就像练习题越多,越能学会方法
# 不同数据量对泛化能力的影响
data_sizes = [50, 100, 200, 500, 1000]
generalization_errors = []

for size in data_sizes:
    X_subset = X_train[:size]
    y_subset = y_train[:size]
    
    X_subset_poly = poly_good.transform(X_subset)
    model_subset = LinearRegression()
    model_subset.fit(X_subset_poly, y_subset)
    
    # 在测试集上评估
    y_test_pred = model_subset.predict(X_test_poly_good)
    test_error = mean_squared_error(y_test, y_test_pred)
    generalization_errors.append(test_error)

# 可视化
plt.figure(figsize=(10, 6))
plt.plot(data_sizes, generalization_errors, 'o-', linewidth=2, markersize=8)
plt.xlabel('训练数据量')
plt.ylabel('测试误差(泛化误差)')
plt.title('数据量对泛化能力的影响')
plt.grid(True, alpha=0.3)
plt.show()

3. 数据质量

类比理解:

  • 数据质量越高,泛化越好
  • 就像练习题质量越高,越能学会方法

4. 特征质量

类比理解:

  • 特征越相关,泛化越好
  • 就像信息越有用,越能做出正确判断

第六部分:学习曲线(Learning Curves)

什么是学习曲线?

**学习曲线(Learning Curves)**展示模型性能随训练数据量或训练轮数的变化。

类比理解:

  • 就像学习进度曲线
  • 就像练习效果曲线

学习曲线的类型

1. 训练数据量学习曲线

from sklearn.model_selection import learning_curve

# 计算学习曲线
train_sizes, train_scores, val_scores = learning_curve(
    model_good,
    X_train_poly_good, y_train,
    cv=5,
    scoring='neg_mean_squared_error',
    train_sizes=np.linspace(0.1, 1.0, 10),
    n_jobs=-1
)

# 计算均值和标准差
train_scores_mean = -train_scores.mean(axis=1)
train_scores_std = train_scores.std(axis=1)
val_scores_mean = -val_scores.mean(axis=1)
val_scores_std = val_scores.std(axis=1)

# 可视化
plt.figure(figsize=(10, 6))
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                 train_scores_mean + train_scores_std, alpha=0.1, color='r')
plt.fill_between(train_sizes, val_scores_mean - val_scores_std,
                 val_scores_mean + val_scores_std, alpha=0.1, color='g')
plt.plot(train_sizes, train_scores_mean, 'o-', color='r', label='训练误差')
plt.plot(train_sizes, val_scores_mean, 'o-', color='g', label='验证误差')
plt.xlabel('训练样本数')
plt.ylabel('MSE')
plt.title('学习曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

学习曲线的解读:

正常拟合:

  • 训练误差和验证误差都下降
  • 最终训练误差 ≈ 验证误差
  • 两条曲线接近

欠拟合:

  • 训练误差和验证误差都很高
  • 两条曲线接近但都很高
  • 增加数据不会改善

过拟合:

  • 训练误差很低,验证误差较高
  • 两条曲线差距大
  • 增加数据可能改善

2. 模型复杂度学习曲线

from sklearn.model_selection import validation_curve

# 不同多项式次数
degrees = range(1, 16)
train_scores_deg = []
val_scores_deg = []

for degree in degrees:
    poly = PolynomialFeatures(degree=degree)
    X_train_poly = poly.fit_transform(X_train)
    X_val_poly = poly.transform(X_test[:len(X_test)//2])  # 使用部分测试集作为验证集
    
    model = LinearRegression()
    model.fit(X_train_poly, y_train)
    
    train_pred = model.predict(X_train_poly)
    val_pred = model.predict(X_val_poly)
    
    train_scores_deg.append(mean_squared_error(y_train, train_pred))
    val_scores_deg.append(mean_squared_error(y_test[:len(X_test)//2], val_pred))

# 可视化
plt.figure(figsize=(10, 6))
plt.plot(degrees, train_scores_deg, 'o-', label='训练误差', linewidth=2)
plt.plot(degrees, val_scores_deg, 'o-', label='验证误差', linewidth=2)
plt.xlabel('多项式次数(模型复杂度)')
plt.ylabel('MSE')
plt.title('模型复杂度学习曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.axvline(x=2, color='r', linestyle='--', label='最优复杂度')
plt.legend()
plt.show()

复杂度曲线的解读:

  • 左侧(低复杂度):欠拟合,训练误差和验证误差都高
  • 中间(合适复杂度):正常拟合,训练误差和验证误差都低且接近
  • 右侧(高复杂度):过拟合,训练误差低但验证误差高

第七部分:偏差-方差分解(Bias-Variance Decomposition)

什么是偏差-方差分解?

偏差-方差分解将模型的泛化误差分解为偏差、方差和不可约误差。

数学表示:
$$
E[(y - f(x))^2] = \text{Bias}^2 + \text{Variance} + \text{Irreducible Error}
$$

其中:

  • 偏差(Bias):$E[f(x)] - y$ 的平方
  • 方差(Variance):$E[(f(x) - E[f(x)])^2]$
  • 不可约误差(Irreducible Error):数据本身的噪声

类比理解:

  • 偏差:就像方法的系统性误差
  • 方差:就像方法的不稳定性
  • 不可约误差:就像数据本身的噪声

偏差-方差权衡

欠拟合:

  • 偏差大,方差小
  • 模型太简单,无法学习模式

正常拟合:

  • 偏差小,方差小
  • 模型复杂度合适

过拟合:

  • 偏差小,方差大
  • 模型太复杂,对数据敏感

可视化偏差-方差权衡

# 模拟偏差-方差权衡
np.random.seed(42)
n_samples = 100
n_models = 100

# 生成数据
X_sim = np.linspace(0, 1, n_samples).reshape(-1, 1)
y_true = np.sin(2 * np.pi * X_sim.flatten())
y_noisy = y_true + np.random.normal(0, 0.1, n_samples)

# 不同复杂度的模型
degrees_sim = [1, 3, 15]
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for idx, degree in enumerate(degrees_sim):
    predictions = []
    
    # 训练多个模型(模拟方差)
    for _ in range(n_models):
        # 随机采样数据
        indices = np.random.choice(n_samples, n_samples, replace=True)
        X_boot = X_sim[indices]
        y_boot = y_noisy[indices]
        
        # 训练模型
        poly = PolynomialFeatures(degree=degree)
        X_poly = poly.fit_transform(X_boot)
        model = LinearRegression()
        model.fit(X_poly, y_boot)
        
        # 预测
        X_plot = np.linspace(0, 1, 100).reshape(-1, 1)
        X_plot_poly = poly.transform(X_plot)
        y_pred = model.predict(X_plot_poly)
        predictions.append(y_pred)
    
    predictions = np.array(predictions)
    
    # 可视化
    axes[idx].plot(X_sim, y_true, 'g-', linewidth=3, label='真实函数')
    axes[idx].scatter(X_sim, y_noisy, alpha=0.3, s=10, label='训练数据')
    
    # 绘制多个预测(显示方差)
    for pred in predictions[::10]:  # 每10个显示一个
        axes[idx].plot(X_plot, pred, 'r-', alpha=0.1, linewidth=1)
    
    # 绘制平均预测(显示偏差)
    mean_pred = predictions.mean(axis=0)
    axes[idx].plot(X_plot, mean_pred, 'b-', linewidth=2, label='平均预测')
    
    axes[idx].set_title(f'次数={degree}')
    axes[idx].set_xlabel('x')
    axes[idx].set_ylabel('y')
    axes[idx].legend()
    axes[idx].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

第八部分:实际应用中的拟合问题

如何诊断拟合问题?

1. 查看训练和验证误差

def diagnose_fitting(train_error, val_error, threshold=0.1):
    """诊断拟合问题"""
    error_gap = abs(val_error - train_error)
    error_ratio = val_error / train_error if train_error > 0 else float('inf')
    
    print("拟合诊断:")
    print(f"训练误差:{train_error:.3f}")
    print(f"验证误差:{val_error:.3f}")
    print(f"误差差距:{error_gap:.3f}")
    print(f"误差比例:{error_ratio:.2f}")
    
    if train_error > threshold and val_error > threshold:
        if abs(train_error - val_error) < threshold:
            print("诊断:欠拟合")
            print("建议:增加模型复杂度、增加特征、减少正则化")
    elif train_error < threshold and val_error > threshold * 2:
        print("诊断:过拟合")
        print("建议:增加正则化、减少模型复杂度、增加数据、使用Dropout")
    else:
        print("诊断:正常拟合")
        print("建议:保持当前设置")

# 诊断示例
print("=" * 50)
diagnose_fitting(train_error_underfit, test_error_underfit)
print("\n" + "=" * 50)
diagnose_fitting(train_error_good, test_error_good)
print("\n" + "=" * 50)
diagnose_fitting(train_error_overfit, test_error_overfit)

2. 使用学习曲线

# 学习曲线诊断
def diagnose_with_learning_curve(model, X_train, y_train, X_val, y_val):
    """使用学习曲线诊断"""
    train_sizes = np.linspace(0.1, 1.0, 10)
    train_errors = []
    val_errors = []
    
    for size in train_sizes:
        n_samples = int(len(X_train) * size)
        X_subset = X_train[:n_samples]
        y_subset = y_train[:n_samples]
        
        model.fit(X_subset, y_subset)
        
        train_pred = model.predict(X_subset)
        val_pred = model.predict(X_val)
        
        train_errors.append(mean_squared_error(y_subset, train_pred))
        val_errors.append(mean_squared_error(y_val, val_pred))
    
    # 可视化
    plt.figure(figsize=(10, 6))
    plt.plot(train_sizes, train_errors, 'o-', label='训练误差')
    plt.plot(train_sizes, val_errors, 'o-', label='验证误差')
    plt.xlabel('训练样本比例')
    plt.ylabel('MSE')
    plt.title('学习曲线诊断')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()
    
    # 诊断
    final_train_error = train_errors[-1]
    final_val_error = val_errors[-1]
    
    if final_train_error > 1.0 and final_val_error > 1.0:
        print("诊断:欠拟合(两条曲线都高)")
    elif final_train_error < 0.1 and final_val_error > 1.0:
        print("诊断:过拟合(训练误差低,验证误差高)")
    else:
        print("诊断:正常拟合(两条曲线都低且接近)")

实际案例:房价预测

# 实际案例:房价预测
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# 生成房价数据
np.random.seed(42)
n_samples = 500
X_house = np.random.rand(n_samples, 5)  # 5个特征
y_house = (X_house[:, 0] * 100 + 
           X_house[:, 1] * 50 + 
           X_house[:, 2] * 30 + 
           np.random.normal(0, 10, n_samples))

X_train_house, X_test_house, y_train_house, y_test_house = train_test_split(
    X_house, y_house, test_size=0.2, random_state=42
)

# 不同复杂度的模型
models_house = {
    '欠拟合(简单)': RandomForestRegressor(n_estimators=1, max_depth=1, random_state=42),
    '正常拟合(合适)': RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
    '过拟合(复杂)': RandomForestRegressor(n_estimators=500, max_depth=20, random_state=42)
}

results = {}
for name, model in models_house.items():
    model.fit(X_train_house, y_train_house)
    
    train_pred = model.predict(X_train_house)
    test_pred = model.predict(X_test_house)
    
    train_error = mean_squared_error(y_train_house, train_pred)
    test_error = mean_squared_error(y_test_house, test_pred)
    
    results[name] = {
        'train_error': train_error,
        'test_error': test_error,
        'gap': abs(test_error - train_error)
    }
    
    print(f"\n{name}:")
    print(f"  训练误差:{train_error:.3f}")
    print(f"  测试误差:{test_error:.3f}")
    print(f"  误差差距:{abs(test_error - train_error):.3f}")

总结

核心概念回顾

  1. 拟合:模型学习数据中的模式

    • 类比:学会解题方法
  2. 欠拟合:模型太简单,无法学习模式

    • 特征:训练误差和验证误差都大
    • 原因:模型复杂度不足、特征不足、训练不充分
    • 类比:没学会,练习题和新题都做不对
  3. 正常拟合:模型复杂度合适,泛化能力强

    • 特征:训练误差和验证误差都小且接近
    • 类比:学会了方法,练习题和新题都能做
  4. 过拟合:模型太复杂,记住了训练数据

    • 特征:训练误差小,验证误差大
    • 原因:模型过于复杂、数据不足、训练过度
    • 类比:只记住了练习题,新题不会做
  5. 泛化:模型在未见过的数据上表现良好

    • 类比:学会了方法,新题也能做

关键要点

偏差-方差权衡:

  • 欠拟合:偏差大,方差小
  • 正常拟合:偏差小,方差小
  • 过拟合:偏差小,方差大

诊断方法:

  • 查看训练和验证误差
  • 使用学习曲线
  • 使用交叉验证

解决方法:

  • 欠拟合:增加复杂度、增加特征、减少正则化
  • 过拟合:增加正则化、减少复杂度、增加数据、使用Dropout

类比总结

理解拟合概念,就像理解学习的三种状态:

  • 欠拟合:就像学生没学会,练习题和新题都做不对
  • 正常拟合:就像学生学会了方法,练习题和新题都能做
  • 过拟合:就像学生只记住了练习题,新题不会做

掌握拟合概念,你就能诊断和解决模型问题!


参考资料

  • Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer.
  • Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer.
  • Géron, A. (2019). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (2nd ed.). O'Reilly Media.

评论