过拟合及其对抗方法:模型选择与多项式拟合实验

理解过拟合现象,掌握验证集、交叉验证等模型选择方法,通过多项式拟合实验探索欠拟合与过拟合

发布于 2025-04-25 更新于 2025-04-252,129 字 5 分钟
过拟合及其对抗方法:模型选择与多项式拟合实验的文章封面
展开文章目录
  1. 1. 训练误差与泛化误差
  2. 2. 过拟合及其对抗方法
  3. 2.1 拟合状态的分类
  4. 2.2 防止过拟合的方法
  5. 3. 使用验证集指导模型选择
  6. 3.1 验证集
  7. 3.2 K 折交叉验证
  8. 4. 使用多项式探索拟合
  9. 4.1 多项式数据生成
  10. 4.2 核心函数实现
  11. 4.3 训练与测试函数
  12. 5. 拟合实验结果
  13. 5.1 简单模型欠拟合
  14. 5.2 复杂模型过拟合
  15. 5.3 合适模型合适拟合
  16. 参考资料

1. 训练误差与泛化误差

在机器学习中,我们需要区分两个重要的概念:

在实践中无法获得总体,而只能在总体随机抽取未在训练集中使用过的数据作为测试集,用测试集估计总体的泛化误差。


2. 过拟合及其对抗方法

只有当模型学到了真正的泛化模式,捕捉到了总体规律,才能作出有效的预测。

2.1 拟合状态的分类

拟合状态

欠拟合 (underfitting)

一开始,模型在训练集上表现一般,处于欠拟合状态。这通常意味着:

  • 模型过于简单,无法捕捉数据的复杂模式
  • 训练时间不足
  • 特征选择不当

过拟合 (overfitting)

随着训练的进行,当模型在训练集上表现良好,但在新数据上的误差较高时,认为发生了过拟合。这意味着:

  • 模型过于复杂,捕捉到了来自训练数据的噪声和不必要的细节
  • 没有学习到数据的总体模式或规律
  • 泛化能力差

合适拟合 (good fit)

理想状态下,模型既能在训练集上表现良好,又能在测试集上保持较好的性能,实现了良好的泛化。

文章配图

2.2 防止过拟合的方法

为了实现合适拟合并防止过拟合,有效的方法包括:

  • 收集更大的训练集,有助于得到更全面的模式
  • 选用复杂度适中的模型(但过于简单的模型可能表达力不足)
  • 使用正则化技术(如L1L_1L2L_2正则化)惩罚模型的复杂度
  • 训练时随机丢弃 (dropout) 部分神经元,减少模型复杂度
  • 在验证集误差开始增加时**提前停止 (early stopping)**训练
  • 使用数据增强的方式(旋转、平移、缩放等)生成更多样的训练数据
  • 使用交叉验证选择模型的最佳参数

文章配图


3. 使用验证集指导模型选择

模型选择是确定模型类别(如决策树之于线性模型)及其超参数(如批量大小、学习率等)的过程。

例如,希望在 MLP 模型中,找到最合适的隐藏层数量、隐藏单元数量、不同激活函数的最佳组合。

3.1 验证集

在模型选择过程中,训练集的泛化误差由测试集估计。但若在模型选择好前就使用了测试集,将带来过拟合的风险。理想情况下,每个测试数据只使用一次就丢弃,但实践中难以有充足的数据支持。

3.2 K 折交叉验证

当训练数据稀缺时,甚至难以组织完整的验证集。幸运的是,K 折交叉验证 (K-fold cross-validation) 通过将数据集划分为 K 个子集(折 fold),多次训练和评估模型,提高了数据的利用率。

K折交叉验证

K 折交叉验证的实现步骤:

  1. 数据划分:将完整的数据集随机分成 K 个(近似)等大的子集

    • 选择 K 值为 5 或 10,能在计算成本和评估稳定性间权衡平衡
  2. 迭代训练与验证:每次选择 K-1 个子集作为训练集,余下的一个子集作为验证集,训练模型,评估模型性能

  3. 重复迭代:重复迭代 K 次,每次都使用不同的验证集

  4. 计算平均性能:将 K 次迭代的结果(如准确率、精确率、召回率等)求均值以作为模型的整体性能

K 折交叉验证的优势:


4. 使用多项式探索拟合

4.1 多项式数据生成

我们使用以下三阶多项式生成数据集进行实验:

y=5.0+1.2x3.4x22!+5.6x33!+ϵy = 5.0 + 1.2x - 3.4\frac{x^2}{2!} + 5.6\frac{x^3}{3!} + \epsilon

其中:

  • 为了避免较大的梯度值和损失值,将原始的系数除以i!i!
  • 噪声项ϵN(0,0.12)\epsilon \sim \mathcal{N}(0, 0.1^2)

更通用的nn阶多项式形式为:

y=i=0naii!xi+ϵy = \sum_{i=0}^{n} \frac{a_i}{i!} x^i + \epsilon

其中,aia_i为第ii项系数,噪声项ϵN(0,σ2)\epsilon \sim \mathcal{N}(0, \sigma^2)

4.2 核心函数实现

多项式数据生成函数

def polynomial_gen(polynomial_coefficients: list, size, noise_std=0.1) -> tuple[Tensor, Tensor]:
    """
    生成 n 阶多项式数据
    .. math:: y = \\sum_{i=0}^{n} \\frac{a_i}{i!} x^i + \\epsilon
    :param polynomial_coefficients: a_i
    :param size: 生成的数据集大小
    :param noise_std: 多项式的噪声项
    :return: 特征张量与标签张量的元组
    """
    polynomial_coefficients = np.array(polynomial_coefficients)
    orders = np.arange(len(polynomial_coefficients))

    x = np.random.normal(size=(size, len(polynomial_coefficients)))
    features = np.power(x, orders) / np.vectorize(math.factorial)(orders)
    labels = features @ polynomial_coefficients
    labels += np.random.normal(scale=noise_std, size=labels.shape)
    return torch.from_numpy(features).to(torch.float32), torch.from_numpy(labels).to(torch.float32)

特征维度调整函数

为了在模型选择时方便缩放模型复杂度,即改变假设的多项式阶数:

def resize_features(features: torch.Tensor, to_columns: int) -> torch.Tensor:
    """
    为每一列生成新的随机数据并扩展特征张量

    :param features: 原始特征张量
    :param to_columns: 新特征张量的目标列数(编号从 1 开始)
    :return: 扩展后的特征张量
    """
    num_samples, num_features = features.shape
    if to_columns > num_features:
        new_data = torch.randn(num_samples, to_columns - num_features)
        expanded_features = torch.cat((features, new_data), dim=1)
        return expanded_features
    else:
        return features[:, :to_columns]

数据集划分函数

def split_dataset(features: torch.Tensor, labels: torch.Tensor, train_ratio: float = 0.8) -> tuple:
    """
    将数据集划分为训练集和验证集

    :param features: 特征张量
    :param labels: 标签张量
    :param train_ratio: 训练集的比例,默认值为 0.8
    :return: 训练集和验证集的特征及标签
    """
    num_samples = features.size(0)
    num_train = int(num_samples * train_ratio)

    indices = torch.randperm(num_samples)

    train_indices = indices[:num_train]
    val_indices = indices[num_train:]

    train_features = features[train_indices]
    train_labels = labels[train_indices]
    val_features = features[val_indices]
    val_labels = labels[val_indices]

    return train_features, train_labels, val_features, val_labels

4.3 训练与测试函数

def train_test(train_features, train_labels, test_features, test_labels, epochs=1000):
    net = nn.Sequential(nn.Linear(in_features=train_features.size(1), out_features=1, bias=False))
    net.apply(lambda m: nn.init.normal_(m.weight, mean=0, std=0.01) if isinstance(m, nn.Linear) else None)

    criterion = nn.MSELoss()
    optimizer = optim.SGD(net.parameters(), LR)

    # 用于保存损失和 MSE 的列表
    train_losses = []
    val_losses = []
    train_mses = []
    val_mses = []

    for epoch in range(epochs):
        # 训练阶段
        net.train()
        optimizer.zero_grad()
        train_outputs = net(train_features).squeeze()
        train_loss = criterion(train_outputs, train_labels)
        train_loss.backward()
        optimizer.step()

        # 验证阶段
        net.eval()
        with torch.no_grad():
            val_outputs = net(test_features).squeeze()
            val_loss = criterion(val_outputs, test_labels)

        # 记录损失和 MSE
        train_losses.append(train_loss.item())
        val_losses.append(val_loss.item())
        train_mses.append(calculate_accuracy(train_outputs, train_labels))
        val_mses.append(calculate_accuracy(val_outputs, test_labels))

        # 输出训练和验证的损失
        if (epoch + 1) % 100 == 0:
            print(f'Epoch [{epoch + 1:4}/{epochs}]:'
                  f'Train Loss: {train_loss.item():.4f}, Train MSE: {train_mses[-1]:.4f}, '
                  f'Validation Loss: {val_loss.item():.4f}, Validation MSE: {val_mses[-1]:.4f}')

    param = net[0].weight.data.numpy().flatten()
    return param, train_losses, val_losses, train_mses, val_mses
查看完整代码实现
import math
import numpy as np
import torch
from matplotlib import pyplot as plt
from torch import Tensor, nn, optim

LR = 0.01

def calculate_accuracy(outputs: Tensor, labels: Tensor) -> float:
    """
    计算模型的精度

    :param outputs: 模型输出
    :param labels: 真实标签
    :return: 精度
    """
    with torch.no_grad():
        # 计算均方误差(MSE)
        mse = nn.MSELoss()(outputs, labels)
        return mse.item()  # 返回损失值

def plot_training_history(train_losses, val_losses, train_mses, val_mses):
    """
    绘制训练损失、验证损失、训练 MSE 和验证 MSE 的变化过程

    :param train_losses: 训练损失列表
    :param val_losses: 验证损失列表
    :param train_mses: 训练 MSE 列表
    :param val_mses: 验证 MSE 列表
    """

    # 绘制损失变化
    plt.subplot(1, 2, 1)
    plt.plot(train_losses, label='Train Loss', color='#81BBF8')
    plt.plot(val_losses, label='Validation Loss', color='#0C68CA')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Training and Validation Loss')
    plt.yscale('log')  # 设置纵坐标为对数坐标
    plt.legend()

    # 绘制 MSE 变化
    plt.subplot(1, 2, 2)
    plt.plot(train_mses, label='Train MSE', color='#F1A2AB')
    plt.plot(val_mses, label='Validation MSE', color='#AD1A2B')
    plt.xlabel('Epoch')
    plt.ylabel('MSE')
    plt.title('Training and Validation MSE')
    plt.yscale('log')  # 设置纵坐标为对数坐标
    plt.legend()

    plt.tight_layout()  # 自动调整子图间距
    plt.show()  # 展示图表

5. 拟合实验结果

通过改变多项式的阶数来改变模型的容量,我们可以观察不同拟合状态的表现。

5.1 简单模型欠拟合

欠拟合实验

coeffs = [5.0, 1.2, -3.4, 5.6]  # 三阶多项式,共 4 项系数
SIZE = 1000
EPOCHS = 1000

features, labels = polynomial_gen(coeffs, SIZE)
features = resize_features(features, to_columns=2)  # 只使用2个特征

train_f, train_l, val_f, val_l = split_dataset(features, labels)
learned_coeffs, *record = train_test(train_f, train_l, val_f, val_l, epochs=EPOCHS)
真实的多项式系数:[5.0, 1.2, -3.4, 5.6]
习得的多项式系数:[3.1565626 1.1513788]

Epoch [1000/1000]:Train Loss: 18.4820, Train MSE: 18.4820, 
                  Validation Loss: 25.3429, Validation MSE: 25.3429

文章配图

5.2 复杂模型过拟合

过拟合实验

coeffs = [5.0, 1.2, -3.4, 5.6]  # 三阶多项式,共 4 项系数
features = resize_features(features, to_columns=7)  # 使用7个特征

train_f, train_l, val_f, val_l = split_dataset(features, labels)
learned_coeffs, *record = train_test(train_f, train_l, val_f, val_l, epochs=EPOCHS)
真实的多项式系数:[5.0, 1.2, -3.4, 5.6]
习得的多项式系数:[ 4.9833040e+00  1.1989247e+00 -3.3791604e+00  5.5992527e+00
                    3.9804489e-03 -4.9315928e-03  6.2198155e-03]

Epoch [1000/1000]:Train Loss: 0.0101, Train MSE: 0.0101, 
                  Validation Loss: 0.0101, Validation MSE: 0.0101

文章配图

5.3 合适模型合适拟合

合适拟合实验

coeffs = [5.0, 1.2, -3.4, 5.6]  # 三阶多项式,共 4 项系数
features = resize_features(features, to_columns=4)  # 使用4个特征

train_f, train_l, val_f, val_l = split_dataset(features, labels)
learned_coeffs, *record = train_test(train_f, train_l, val_f, val_l, epochs=EPOCHS)
真实的多项式系数:[5.0, 1.2, -3.4, 5.6]
习得的多项式系数:[ 5.004878   1.1933998 -3.4009356  5.6035047]

Epoch [1000/1000]:Train Loss: 0.0096, Train MSE: 0.0096, 
                  Validation Loss: 0.0104, Validation MSE: 0.0104

文章配图


参考资料

如果这篇记录对你有帮助,可以留下一句回应。

前往留言

DISCUSSION

讨论与回应

欢迎补充细节、指出问题,或分享与这篇文章有关的经验。

昵称与邮箱为必填项,邮箱仅用于头像和回复通知,不会公开。网址可以留空。

正在准备留言区…

演示赞赏界面

谢谢你愿意支持长期写作。

第一阶段不会发起支付或收集任何信息。接入真实赞赏渠道后,这里会展示清楚的金额、渠道和完成状态。

输入关键词开始搜索 · 按 Esc 关闭
打开完整搜索页 →