机器学习之评估与偏差方差分析

一.机器学习评估与偏差方差分析

1.项目初始化:导入工具包

工具包 作用
numpy Python 科学计算基础库,用于数值运算、数组处理
matplotlib 数据可视化库,用于绘制训练 / 测试数据、模型曲线
scikit-learn 机器学习工具库,提供数据集划分、线性回归、多项式特征、模型评估等功能
TensorFlow/Keras 深度学习框架(本次作业中主要用于设置浮点精度、日志控制)
复制代码
# 导入基础库
import numpy as np
import matplotlib.pyplot as plt

# 导入sklearn工具
from sklearn.linear_model import LinearRegression, Ridge  # 线性回归与带正则化的回归
from sklearn.preprocessing import StandardScaler, PolynomialFeatures  # 特征标准化、多项式特征生成
from sklearn.model_selection import train_test_split  # 数据集划分
from sklearn.metrics import mean_squared_error  # 均方误差评估

# 导入TensorFlow并配置日志(屏蔽无关警告)
import tensorflow as tf
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
tf.keras.backend.set_floatx('float64')  # 设置浮点精度为float64,避免数值误差
  • 配置 TensorFlow 日志级别为ERROR,是为了屏蔽训练过程中大量无关的信息警告,让输出更简洁。
  • float64精度比默认的float32更高,在多项式回归中能减少高阶运算的数值误差。

2.模型评估基础:数据集划分与误差计算

(1) 为什么要划分数据集?

模型在训练数据上拟合得好,不代表能在 ** 新数据(未见过的数据)** 上表现好,因此需要:

  1. 将数据分为训练集(Train Set)测试集(Test Set)
    • 训练集:用于拟合模型参数(如线性回归的权重w和偏置b
    • 测试集:用于评估模型在 "新数据" 上的泛化能力,模拟真实场景下的性能
  2. 建议:测试集占比 20%-40%,常见为 30%/33%

(2) 数据集划分代码与结果分析

复制代码
# 生成带噪声的二次函数数据
X,y,x_ideal,y_ideal = gen_data(18, 2, 0.7)
print("X.shape", X.shape, "y.shape", y.shape)  # 输出:X.shape (18,) y.shape (18,)

# 划分训练集和测试集(测试集占33%)
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.33, random_state=1)
print("X_train.shape", X_train.shape, "y_train.shape", y_train.shape)  # 输出:(12,) (12,)
print("X_test.shape", X_test.shape, "y_test.shape", y_test.shape)      # 输出:(6,) (6,)
  • random_state=1:固定随机种子,确保每次运行代码划分结果一致,方便复现实验
  • 结果解读:18 条数据中,12 条作为训练集(67%),6 条作为测试集(33%),符合课程建议的划分比例。

(3) 数据可视化:训练集 vs 测试集

复制代码
fig, ax = plt.subplots(1,1,figsize=(4,4))
# 绘制理想曲线(无噪声的二次函数)
ax.plot(x_ideal, y_ideal, "--", color = "orangered", label="y_ideal", lw=1)
ax.set_title("Training, Test",fontsize = 14)
ax.set_xlabel("x")
ax.set_ylabel("y")
# 绘制训练集(红色)和测试集(蓝色)数据点
ax.scatter(X_train, y_train, color = "red", label="train")
ax.scatter(X_test, y_test, color = "dlc["dlblue"]", label="test")
ax.legend(loc='upper left')
plt.show()
  • 红色点:训练集,模型拟合时会直接用到这些数据
  • 蓝色点:测试集,模型训练时看不到,仅用于评估泛化能力
  • 橙色虚线:无噪声的 "真实" 曲线,作为模型拟合效果的参考

(4)误差计算:均方误差(MSE)

(a)核心公式

线性回归模型的误差评估公式:

  • mtest:测试集样本数量
  • fw,b(xtest(i)):模型对第i个测试样本的预测值
  • ytest(i):第i个测试样本的真实值
  • 除以2mtest是为了后续梯度下降时简化求导结果,本质和均方误差(MSE)的核心逻辑一致
(b)自定义误差计算函数
复制代码
def eval_mse(y, yhat):
    """
    Calculate the mean squared error on a data set.
    Args:
        y    : (ndarray Shape (m,) or (m,1))  target value of each example
        yhat : (ndarray Shape (m,) or (m,1))  predicted value of each example
    Returns:
        err: (scalar)
    """
    m = len(y)
    err = 0.0
    for i in range(m):
        err += (y[i] - yhat[i])**2  # 计算每个样本的预测误差平方和
    err /= 2*m  # 除以2倍样本数,得到最终误差
    return(err)
  • 测试用例:

    复制代码
    y_hat = np.array([2.4, 4.2])
    y_tmp = np.array([2.3, 4.1])
    eval_mse(y_hat, y_tmp)  # 计算结果:((0.1)^2 + (0.1)^2)/(2*2) = 0.01/2 = 0.005
  • 单元测试通过,说明函数实现正确。


3.过拟合问题:训练误差 vs 测试误差

(1)实验:高阶多项式回归的过拟合现象

复制代码
# 构建10阶多项式模型(高度复杂)
degree = 10
lmodel = lin_model(degree)
lmodel.fit(X_train, y_train)  # 在训练集上拟合模型

# 计算训练集误差
yhat = lmodel.predict(X_train)
err_train = lmodel.mse(y_train, yhat)

# 计算测试集误差
yhat = lmodel.predict(X_test)
err_test = lmodel.mse(y_test, yhat)

print(f"training err {err_train:0.2f}, test err {err_test:0.2f}")
# 输出:training err 58.01, test err 171215.01
  • 关键现象:训练误差极低,测试误差极高
    • 模型在训练集上拟合得 "过于完美",甚至学到了训练数据中的噪声
    • 但对从未见过的测试数据,预测效果极差,这就是过拟合(Overfitting)

(2) 过拟合的本质

  1. 模型复杂度太高(10 阶多项式),远高于数据本身的真实复杂度(2 阶)
  2. 模型过度学习了训练数据中的噪声和随机波动,失去了泛化能力
  3. 对应机器学习中的 "高方差" 问题:模型对训练数据的微小变化极其敏感,泛化能力差

4.模型优化进阶:划分训练集、交叉验证集、测试集

(1)为什么需要交叉验证集?

如果用测试集来调整模型超参数(如多项式阶数、正则化参数),会导致模型 "泄露测试集信息",最终测试集无法再作为独立的泛化能力评估标准。因此需要将数据分为三部分:

数据集 占比(典型) 作用
训练集 60% 拟合模型参数(如wb
交叉验证集(验证集) 20% 调整模型超参数(如多项式阶数、正则化强度)
测试集 20% 最终评估模型泛化能力,全程不参与模型训练和超参数调整

(2) 三数据集划分代码

复制代码
# 生成40条数据
X,y, x_ideal,y_ideal = gen_data(40, 5, 0.7)
print("X.shape", X.shape, "y.shape", y.shape)  # 输出:(40,) (40,)

# 第一步:划分训练集(60%)和"临时集"(40%)
X_train, X_, y_train, y_ = train_test_split(X,y,test_size=0.40, random_state=1)
# 第二步:将"临时集"划分为交叉验证集(20%)和测试集(20%)
X_cv, X_test, y_cv, y_test = train_test_split(X_,y_,test_size=0.50, random_state=1)

print("X_train.shape", X_train.shape, "y_train.shape", y_train.shape)  # (24,) (24,)
print("X_cv.shape", X_cv.shape, "y_cv.shape", y_cv.shape)              # (8,) (8,)
print("X_test.shape", X_test.shape, "y_test.shape", y_test.shape)      # (8,) (8,)
  • 结果解读:40 条数据中,24 条训练集,8 条交叉验证集,8 条测试集,符合 60%/20%/20% 的划分比例。

(3)数据可视化:三数据集分布

复制代码
fig, ax = plt.subplots(1,1,figsize=(4,4))
ax.plot(x_ideal, y_ideal, "--", color = "orangered", label="y_ideal", lw=1)
ax.set_title("Training, CV, Test",fontsize = 14)
ax.set_xlabel("x")
ax.set_ylabel("y")

ax.scatter(X_train, y_train, color = "red", label="train")          # 训练集:红色
ax.scatter(X_cv, y_cv, color = "dlc["dlorange"]", label="cv")       # 交叉验证集:橙色
ax.scatter(X_test, y_test, color = "dlc["dlblue"]", label="test")    # 测试集:蓝色
ax.legend(loc='upper left')
plt.show()

5.偏差与方差分析:选择最优模型复杂度

(1)偏差与方差的核心概念

  • 高偏差(欠拟合):模型复杂度太低,连训练数据的基本规律都没学到,训练误差和交叉验证误差都很高
  • 高方差(过拟合):模型复杂度太高,过度学习训练数据噪声,训练误差低但交叉验证误差高
  • 理想状态:训练误差和交叉验证误差都较低,且两者差距小

(2)实验:寻找最优多项式阶数

通过尝试不同阶数的多项式模型,观察训练误差和交叉验证误差的变化趋势,找到最优阶数:

复制代码
max_degree = 9
err_train = np.zeros(max_degree)  # 存储不同阶数的训练误差
err_cv = np.zeros(max_degree)     # 存储不同阶数的交叉验证误差
x = np.linspace(0,int(X.max()),100)
y_pred = np.zeros((100,max_degree))  # 存储不同阶数模型的预测曲线

for degree in range(max_degree):
    lmodel = lin_model(degree+1)  # 尝试1-9阶多项式
    lmodel.fit(X_train, y_train)  # 训练模型
    # 计算训练误差
    yhat = lmodel.predict(X_train)
    err_train[degree] = lmodel.mse(y_train, yhat)
    # 计算交叉验证误差
    yhat = lmodel.predict(X_cv)
    err_cv[degree] = lmodel.mse(y_cv, yhat)
    # 存储模型预测曲线
    y_pred[:,degree] = lmodel.predict(x)

# 找到交叉验证误差最低的阶数(最优阶数)
optimal_degree = np.argmin(err_cv)+1

(3) 误差趋势解读

  • 随着多项式阶数增加:
    1. 训练误差持续下降:模型复杂度越高,越能拟合训练数据(包括噪声)
    2. 交叉验证误差先降后升
      • 前期:阶数低,模型欠拟合,交叉验证误差高;随着阶数增加,模型复杂度匹配数据规律,误差下降
      • 后期:阶数过高,模型过拟合训练数据噪声,交叉验证误差开始上升
  • 最优阶数:交叉验证误差最低的点,此时模型既不过拟合也不欠拟合,泛化能力最好

6.正则化参数调整:解决过拟合的另一种方法

(1)正则化的作用

在损失函数中加入正则项(L2 正则),惩罚模型的高次项权重,降低模型复杂度,缓解过拟合问题。带 L2 正则的损失函数:

  • λ:正则化参数,控制正则化强度
    • λ=0:无正则化,模型容易过拟合
    • λ过大:过度惩罚权重,模型变得过于简单,容易欠拟合

(2) 实验:调整正则化参数λ

复制代码
lambda_range = np.array([0.0, 1e-6, 1e-5, 1e-4, 1e-3,1e-2, 1e-1,1,10,100])
num_steps = len(lambda_range)
degree = 10  # 固定高阶多项式(易过拟合)
err_train = np.zeros(num_steps)
err_cv = np.zeros(num_steps)
x = np.linspace(0,int(X.max()),100)
y_pred = np.zeros((100,num_steps))

for i in range(num_steps):
    lambda_ = lambda_range[i]
    # 带正则化的多项式回归模型
    lmodel = lin_model(degree, regularization=True, lambda_=lambda_)
    lmodel.fit(X_train, y_train)
    # 计算训练误差
    yhat = lmodel.predict(X_train)
    err_train[i] = lmodel.mse(y_train, yhat)
    # 计算交叉验证误差
    yhat = lmodel.predict(X_cv)
    err_cv[i] = lmodel.mse(y_cv, yhat)
    # 存储模型预测曲线
    y_pred[:,i] = lmodel.predict(x)

# 找到交叉验证误差最低的正则化参数
optimal_reg_idx = np.argmin(err_cv)

(3)正则化效果解读

  • 随着λ增大:
    1. 模型从过拟合(高方差)逐渐变为欠拟合(高偏差)
    2. 训练误差逐渐上升(正则化限制了模型拟合训练数据的能力)
    3. 交叉验证误差先降后升:
      • 前期:λ过小,正则化不足,模型仍过拟合,交叉验证误差高
      • 中期:λ适中,正则化有效缓解过拟合,交叉验证误差最低
      • 后期:λ过大,正则化过度,模型欠拟合,交叉验证误差再次升高
  • 最优λ:交叉验证误差最低的点,此时正则化强度刚好平衡了模型复杂度和泛化能力。

7.解决过拟合的终极方案:增加训练数据

(1)核心结论

当模型过拟合(高方差)时,增加训练集样本数量可以有效提升模型泛化能力:

  • 更多的训练数据能让模型学习到数据的真实规律,而不是被个别样本的噪声误导
  • 随着训练集增大,训练误差和交叉验证误差会逐渐收敛到相近的低值,模型泛化能力显著提升

(2)实验验证

复制代码
# 模拟不同训练集大小下的模型表现
X_train, y_train, X_cv, y_cv, x, y_pred, err_train, err_cv, m_range,degree = tune_m()
plt_tune_m(X_train, y_train, X_cv, y_cv, x, y_pred, err_train, err_cv, m_range, degree)
  • 可视化结果解读:
    1. 左图:训练集越大,模型拟合曲线越平滑,越接近真实数据规律,过拟合现象消失
    2. 右图:随着训练集大小增加,训练误差和交叉验证误差逐渐收敛,差距越来越小
  • 关键注意点:增加数据对欠拟合(高偏差)无效,因为欠拟合是模型复杂度不足,和数据量无关。

8.核心总结:模型优化的完整流程

  1. 数据划分:将数据分为训练集、交叉验证集、测试集(60%/20%/20%)
  2. 评估误差:用均方误差(MSE)评估模型在训练集和交叉验证集上的表现
  3. 判断偏差 / 方差
    • 高偏差:训练误差和交叉验证误差都很高 → 提升模型复杂度(如增加多项式阶数)
    • 高方差:训练误差低、交叉验证误差高 → 降低模型复杂度(如降低阶数、增加正则化)或增加训练数据
  4. 超参数调优:用交叉验证集选择最优多项式阶数、正则化参数λ
  5. 最终评估:用测试集评估最终模型的泛化能力,全程不参与训练和调参

9.避坑指南

  1. 测试集不能用于调参:一旦用测试集调整超参数,就失去了其作为 "独立泛化评估" 的意义,必须用交叉验证集调参
  2. 过拟合≠训练误差低 :训练误差低只是过拟合的表象,核心是泛化能力差(测试误差高)
  3. 正则化不是越大越好:过度正则化会导致模型欠拟合,必须通过交叉验证找到最优λ
  4. 增加数据只解决高方差:如果模型欠拟合,再多数据也无法提升性能,此时需要提升模型复杂度

二.神经网络分类篇

1.数据集准备:分类任务的三划分

(1) 数据生成与划分

复制代码
# 生成带聚类的分类数据集
X, y, centers, classes, std = gen_blobs()

# 划分训练集、交叉验证集、测试集(CV占比放大,突出重点)
X_train, X_, y_train, y_ = train_test_split(X,y,test_size=0.50, random_state=1)
X_cv, X_test, y_cv, y_test = train_test_split(X_,y_,test_size=0.20, random_state=1)

print("X_train.shape:", X_train.shape, "X_cv.shape:", X_cv.shape, "X_test.shape:", X_test.shape)
# 输出:X_train.shape: (400, 2) X_cv.shape: (320, 2) X_test.shape: (80, 2)
  • 数据集说明:6 个聚类中心的二维数据,存在部分模糊边界样本(易被误分类)
  • 划分逻辑:训练集 400 条(50%)、交叉验证集 320 条(40%)、测试集 80 条(10%),放大 CV 集占比以突出模型调参重点

(2)数据可视化

复制代码
plt_train_eq_dist(X_train, y_train,classes, X_cv, y_cv, centers, std)
  • 训练集(圆点)与交叉验证集(三角形)混合显示,模糊边界的样本会同时受多个聚类影响
  • 理想模型:基于中心点距离构建等距边界,对约 8% 的数据存在不可避免的误分类

2.分类模型评估:分类误差计算

(1)核心公式

分类误差(误分类比例)定义:

​即:误分类样本数 / 总样本数,误差越低,模型分类效果越好。

(2)自定义分类误差函数

复制代码
def eval_cat_err(y, yhat):
    """
    Calculate the categorization error
    Args:
        y    : (ndarray Shape (m,) or (m,1))  target value of each example
        yhat : (ndarray Shape (m,) or (m,1))  predicted value of each example
    Returns:
        cerr: (scalar)
    """
    m = len(y)
    incorrect = 0
    for i in range(m):
        if yhat[i] != y[i]:
            incorrect += 1
    cerr = incorrect/m
    return(cerr)

(3) 单元测试验证

复制代码
# 测试用例1:1个误分类样本,共3个样本 → 误差=1/3≈0.333
y_hat = np.array([1, 2, 0])
y_tmp = np.array([1, 2, 3])
print(f"categorization error {np.squeeze(eval_cat_err(y_hat, y_tmp)):0.3f}, expected:0.333")

# 测试用例2:1个误分类样本,共4个样本 → 误差=1/4=0.250
y_hat = np.array([[1], [2], [0], [3]])
y_tmp = np.array([[1], [2], [1], [3]])
print(f"categorization error {np.squeeze(eval_cat_err(y_hat, y_tmp)):0.3f}, expected:0.250")
  • 单元测试全部通过,说明函数实现正确。

3.模型对比:复杂模型 vs 简单模型

(1) 复杂三层神经网络模型

(a)模型构建代码
复制代码
tf.random.set_seed(1234)
model = Sequential(
    [
        tf.keras.layers.Dense(120, activation="relu"),
        tf.keras.layers.Dense(40, activation="relu"),
        tf.keras.layers.Dense(6, activation="linear")
    ], name="Complex"
)

model.compile(
    loss=SparseCategoricalCrossentropy(from_logits=True),
    optimizer=tf.keras.optimizers.Adam(lr=0.01),
)

# 模型训练
model.fit(
    X_train, y_train,
    epochs=1000
)
(b)模型结构与参数
复制代码
model.summary()
Layer (type) Output Shape Param #
Dense (None, 120) 360
Dense_1 (None, 40) 4840
Dense_2 (None, 6) 246
  • 总参数:5446 个,模型复杂度高
(c)模型误差计算
复制代码
# 预测函数:将模型输出的logits转换为类别
model_predict = lambda X1: np.argmax(tf.nn.softmax(model.predict(X1)).numpy(),axis=1)

# 计算训练集和交叉验证集误差
training_cerr_complex = eval_cat_err(y_train, model_predict(X_train))
cv_cerr_complex = eval_cat_err(y_cv, model_predict(X_cv))

print(f"categorization error, training, complex model: {training_cerr_complex:0.3f}")
print(f"categorization error, cv, complex model: {cv_cerr_complex:0.3f}")
# 输出:
# training: 0.003(极低)
# cv: 0.122(显著高于训练误差)
(d)结果解读:高方差(过拟合)
  • 训练误差极低(几乎完美拟合训练数据),但交叉验证误差很高
  • 模型过度学习了训练数据中的噪声和异常值,泛化能力差,属于典型的过拟合

(2) 简单双层神经网络模型

(a)模型构建代码
复制代码
tf.random.set_seed(1234)
model_s = Sequential(
    [
        tf.keras.layers.Dense(6, activation="relu"),
        tf.keras.layers.Dense(6, activation="linear")
    ], name = "Simple"
)

model_s.compile(
    loss=SparseCategoricalCrossentropy(from_logits=True),
    optimizer=tf.keras.optimizers.Adam(lr=0.01),
)

# 模型训练
model_s.fit(
    X_train,y_train,
    epochs=1000
)
(b)模型结构与参数
复制代码
model_s.summary()
Layer (type) Output Shape Param #
Dense_3 (None, 6) 18
Dense_4 (None, 6) 42
  • 总参数:60 个,模型复杂度极低
(c)模型误差计算
复制代码
# 预测函数
model_predict_s = lambda X1: np.argmax(tf.nn.softmax(model_s.predict(X1)).numpy(),axis=1)

# 计算训练集和交叉验证集误差
training_cerr_simple = eval_cat_err(y_train, model_predict_s(X_train))
cv_cerr_simple = eval_cat_err(y_cv, model_predict_s(X_cv))

print(f"categorization error, training, simple model: {training_cerr_simple:0.3f}, complex model: {training_cerr_complex:0.3f}")
print(f"categorization error, cv, simple model: {cv_cerr_simple:0.3f}, complex model: {cv_cerr_complex:0.3f}")
# 输出:
# training: 0.062(略高)
# cv: 0.087(与训练误差差距小,且显著低于复杂模型)
(d)结果解读:泛化能力更优
  • 训练误差略高于复杂模型,但交叉验证误差显著更低
  • 模型没有过度拟合训练数据,泛化能力更强,更接近数据的真实分布

4.核心结论:偏差 - 方差权衡(分类任务版)

模型类型 训练误差 交叉验证误差 偏差 / 方差问题 核心特征
复杂模型 极低(0.003) 较高(0.122) 高方差(过拟合) 过度拟合训练数据,对噪声敏感,泛化能力差
简单模型 略高(0.062) 较低(0.087) 偏差 - 方差平衡 训练误差与 CV 误差差距小,泛化能力更强
  1. 模型复杂度不是越高越好:过高的复杂度会导致过拟合,在训练集上表现完美但在新数据上表现极差
  2. 交叉验证集的核心作用:区分模型是过拟合还是欠拟合,是模型调参的 "金标准"
  3. 分类误差的解读
    • 训练误差低、CV 误差高 → 过拟合(高方差)
    • 训练误差和 CV 误差都高 → 欠拟合(高偏差)
    • 训练误差略高、CV 误差低且差距小 → 模型泛化能力最优

5.分类任务的特殊注意点

  1. 损失函数的选择
    • 目标值是类别索引(非独热编码)时,必须使用SparseCategoricalCrossentropy,而非CategoricalCrossentropy
    • from_logits=True:告诉损失函数输入是模型的原始输出(未经过 softmax),由损失函数内部计算 softmax,避免数值不稳定
  2. 预测函数的转换
    • 模型输出是 logits(未归一化的概率),需要通过tf.nn.softmax转换为概率,再用np.argmax得到类别索引
  3. 模型复杂度的控制
    • 神经网络的复杂度由层数、每层单元数共同决定,复杂模型参数更多,更容易过拟合
    • 当模型过拟合时,可通过减少层数 / 单元数、增加正则化(如 Dropout)、增加训练数据来缓解
相关推荐
消失的旧时光-19431 小时前
C语言对象模型系列(四)《Linux 内核里的 container_of 到底是什么黑魔法?》—— 一篇讲透 Linux 内核的“对象模型”核心技巧
linux·c语言·算法
AI_Ming2 小时前
从0开始学AI:层归一化,原来是这回事!
算法·ai编程
WL_Aurora2 小时前
备战蓝桥杯国赛【Day 8】
算法·蓝桥杯
智者知已应修善业2 小时前
【51单片机模拟生日蜡烛】2023-10-10
c++·经验分享·笔记·算法·51单片机
MediaTea2 小时前
Scikit-learn:从数据到结构——无监督学习的最小闭环
人工智能·学习·算法·机器学习·scikit-learn
智者知已应修善业2 小时前
【51单片机如何让LED灯从一亮到八,再从八亮到一】2023-10-13
c++·经验分享·笔记·算法·51单片机
qeen872 小时前
【数据结构】二叉树相关经典函数C语言实现
c语言·数据结构·c++·笔记·学习·算法·二叉树
良木生香3 小时前
【C++初阶】STL——List从入门到应用完全指南(1)
开发语言·数据结构·c++·程序人生·算法·蓝桥杯·学习方法
WL_Aurora3 小时前
【每日一题】贪心
python·算法