一.机器学习评估与偏差方差分析
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)、增加训练数据来缓解