机器学习之评估与偏差方差分析
一.机器学习评估与偏差方差分析
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) 为什么要划分数据集?
模型在训练数据上拟合得好,不代表能在 ** 新数据(未见过的数据)** 上表现好,因此需要:
- 将数据分为训练集(Train Set)和测试集(Test Set)
- 训练集:用于拟合模型参数(如线性回归的权重
w和偏置b) - 测试集:用于评估模型在 “新数据” 上的泛化能力,模拟真实场景下的性能
- 训练集:用于拟合模型参数(如线性回归的权重
- 建议:测试集占比 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) 过拟合的本质
- 模型复杂度太高(10 阶多项式),远高于数据本身的真实复杂度(2 阶)
- 模型过度学习了训练数据中的噪声和随机波动,失去了泛化能力
- 对应机器学习中的 “高方差” 问题:模型对训练数据的微小变化极其敏感,泛化能力差
4.模型优化进阶:划分训练集、交叉验证集、测试集
(1)为什么需要交叉验证集?
如果用测试集来调整模型超参数(如多项式阶数、正则化参数),会导致模型 “泄露测试集信息”,最终测试集无法再作为独立的泛化能力评估标准。因此需要将数据分为三部分:
| 数据集 | 占比(典型) | 作用 |
|---|---|---|
| 训练集 | 60% | 拟合模型参数(如w、b) |
| 交叉验证集(验证集) | 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) 误差趋势解读
- 随着多项式阶数增加:
- 训练误差持续下降:模型复杂度越高,越能拟合训练数据(包括噪声)
- 交叉验证误差先降后升:
- 前期:阶数低,模型欠拟合,交叉验证误差高;随着阶数增加,模型复杂度匹配数据规律,误差下降
- 后期:阶数过高,模型过拟合训练数据噪声,交叉验证误差开始上升
- 最优阶数:交叉验证误差最低的点,此时模型既不过拟合也不欠拟合,泛化能力最好
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)正则化效果解读
- 随着λ增大:
- 模型从过拟合(高方差)逐渐变为欠拟合(高偏差)
- 训练误差逐渐上升(正则化限制了模型拟合训练数据的能力)
- 交叉验证误差先降后升:
- 前期:λ过小,正则化不足,模型仍过拟合,交叉验证误差高
- 中期:λ适中,正则化有效缓解过拟合,交叉验证误差最低
- 后期:λ过大,正则化过度,模型欠拟合,交叉验证误差再次升高
- 最优λ:交叉验证误差最低的点,此时正则化强度刚好平衡了模型复杂度和泛化能力。
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)- 可视化结果解读:
- 左图:训练集越大,模型拟合曲线越平滑,越接近真实数据规律,过拟合现象消失
- 右图:随着训练集大小增加,训练误差和交叉验证误差逐渐收敛,差距越来越小
- 关键注意点:增加数据对欠拟合(高偏差)无效,因为欠拟合是模型复杂度不足,和数据量无关。
8.核心总结:模型优化的完整流程
- 数据划分:将数据分为训练集、交叉验证集、测试集(60%/20%/20%)
- 评估误差:用均方误差(MSE)评估模型在训练集和交叉验证集上的表现
- 判断偏差 / 方差:
- 高偏差:训练误差和交叉验证误差都很高 → 提升模型复杂度(如增加多项式阶数)
- 高方差:训练误差低、交叉验证误差高 → 降低模型复杂度(如降低阶数、增加正则化)或增加训练数据
- 超参数调优:用交叉验证集选择最优多项式阶数、正则化参数λ
- 最终评估:用测试集评估最终模型的泛化能力,全程不参与训练和调参
9.避坑指南
- 测试集不能用于调参:一旦用测试集调整超参数,就失去了其作为 “独立泛化评估” 的意义,必须用交叉验证集调参
- 过拟合≠训练误差低:训练误差低只是过拟合的表象,核心是泛化能力差(测试误差高)
- 正则化不是越大越好:过度正则化会导致模型欠拟合,必须通过交叉验证找到最优λ
- 增加数据只解决高方差:如果模型欠拟合,再多数据也无法提升性能,此时需要提升模型复杂度
二.神经网络分类篇
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 误差差距小,泛化能力更强 |
- 模型复杂度不是越高越好:过高的复杂度会导致过拟合,在训练集上表现完美但在新数据上表现极差
- 交叉验证集的核心作用:区分模型是过拟合还是欠拟合,是模型调参的 “金标准”
- 分类误差的解读:
- 训练误差低、CV 误差高 → 过拟合(高方差)
- 训练误差和 CV 误差都高 → 欠拟合(高偏差)
- 训练误差略高、CV 误差低且差距小 → 模型泛化能力最优
5.分类任务的特殊注意点
- 损失函数的选择:
- 目标值是类别索引(非独热编码)时,必须使用
SparseCategoricalCrossentropy,而非CategoricalCrossentropy from_logits=True:告诉损失函数输入是模型的原始输出(未经过 softmax),由损失函数内部计算 softmax,避免数值不稳定
- 目标值是类别索引(非独热编码)时,必须使用
- 预测函数的转换:
- 模型输出是 logits(未归一化的概率),需要通过
tf.nn.softmax转换为概率,再用np.argmax得到类别索引
- 模型输出是 logits(未归一化的概率),需要通过
- 模型复杂度的控制:
- 神经网络的复杂度由层数、每层单元数共同决定,复杂模型参数更多,更容易过拟合
- 当模型过拟合时,可通过减少层数 / 单元数、增加正则化(如 Dropout)、增加训练数据来缓解
