当前位置: 首页 > news >正文

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

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

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

工具包作用
numpyPython 科学计算基础库,用于数值运算、数组处理
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 ShapeParam #
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 ShapeParam #
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)、增加训练数据来缓解
http://www.jsqmd.com/news/794151/

相关文章:

  • Python 入门 01|Python 环境准备(下载+安装+配置PATH)
  • 深入解析ATB总线:CoreSight调试架构的核心技术
  • 信息安全工程师-恶意代码分析与防护体系:技术、产品与落地全指南
  • RFID固定资产盘点为什么越来越快?很多公司已经开始用PDA+RFID打印机了
  • 无人机载雷达地杂波建模抑制与FPGA实现技术【附代码】
  • 半导体堆叠芯片热瞬态测试技术与结构函数分析
  • Claude Code团队成员揭秘:AI时代为什么应该抛弃Markdown,转用HTML
  • Arm SoC迁移中的实时行为预测与多核优化实践
  • Servlet+JQuery实现数据库数据渲染到前端页面
  • 如何一键获取九大网盘直链?LinkSwift下载助手完整使用教程
  • 原来武汉这些公司居然能拍出这么出色的广告宣传片?
  • 图神经网络在边缘计算中的物理场重建应用
  • LeetCode 只出现一次的数字题解
  • 芯片供应链安全:从疫情危机到绿色基建的可信溯源实践
  • 【c++面向对象编程】第2篇:类与对象(一):定义第一个类——成员变量与成员函数
  • GD32C103RBT6 GPIO 库函数详解
  • 从一篇文章到一个 AI 排毒系统,我花了 7 天
  • 技术沟通中的语义陷阱:识别与清除过时术语的工程实践
  • LeetCode 数组中两个元素的最大与题解
  • 逆向Soul App客户端证书:从定位到解密,打通SSL双向校验抓包之路
  • 把“贪吃蛇”做成塔防Boss,这个Unity模板是怎么设计的?附完整变现思路
  • esptool闪存擦除机制深度解析:从硬件限制到工程实践的最佳策略
  • AI智能体企业级身份管理:基于Active Directory的agent-directory部署与实战
  • 大数据 机器学习毕业设计项目选题建议
  • Vagrant封装工具:快速搭建Claude API本地开发环境
  • Letta框架:全栈AI应用开发,从模型集成到部署上线的完整解决方案
  • 避坑指南:用Python爬携程旅游信息时,如何应对页面结构变化和反爬?
  • 社区Helm Charts仓库实战:从设计理念到应用部署全解析
  • 【c++面向对象编程】第3篇:类与对象(二):构造函数与析构函数
  • 法律智能体构建指南:从LLM与RAG技术到合同审查实战