WARNING
🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。
s04 偏差-方差权衡 — demo.py 代码详解
运行方式
cd docs/ml/foundations/bias-variance/code
python demo.py代码逐段详解
第1步:导入库 — 每个库是做什么的
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import KFold
from sklearn.pipeline import make_pipelineos:文件路径操作,创建images/目录numpy:数值计算核心。关键用法:np.sin()生成正弦波数据,np.linalg.pinv()伪逆求解正规方程,np.hstack()构建多项式特征矩阵,np.sign()计算 L1 正则化梯度中的符号函数,np.logspace()生成对数均匀的序列, np.argmin()找到最优模型复杂度matplotlib:绘图,包括多项式拟合对比图、Bias-Variance 曲线、正则化对比图、系数路径图、交叉验证图sklearn.preprocessing.PolynomialFeatures:sklearn 的多项式特征生成器(用于对比验证,demo 中同时使用了自己的手动实现)sklearn.linear_model.LinearRegression, Ridge, Lasso:sklearn 的线性和正则化回归模型,用作基准对比sklearn.model_selection.KFold:sklearn 的 K-Fold 交叉验证分割器sklearn.pipeline.make_pipeline:构建处理管道,将多项式特征生成 + 回归模型串联为一个整体
第2步:数据生成 — 正弦波 + 噪声
def generate_sine_data(n_samples=80, noise_std=0.3, random_seed=42):生成模拟数据,真实函数为:
加上高斯噪声
关键步骤:
np.random.uniform(0, 1, n_samples)在均匀采样 80 个点 np.sort(X)对排序——这纯粹是为了画曲线美观,不是训练必需 np.sin(2 * np.pi * X)计算真实函数值- 加上
np.random.randn(n_samples) * noise_std的高斯噪声
选择正弦函数是因为它是非线性的——低次多项式无法很好地拟合(欠拟合),而高次多项式会拟合到噪声(过拟合),这正好展示了模型复杂度的权衡。
第3步:多项式特征生成
def polynomial_features(X, degree):
X = X.reshape(-1, 1)
return np.hstack([X ** d for d in range(degree + 1)])这个函数将一维特征
X.reshape(-1, 1)确保是列向量 [X ** d for d in range(degree + 1)]生成一个列表,每个元素是一列np.hstack()将所有列横向拼接成的矩阵
第一列全 1(
第4步:多项式拟合(正规方程)
def fit_polynomial(X, y, degree):
Phi = polynomial_features(X, degree)
theta = np.linalg.pinv(Phi.T @ Phi) @ Phi.T @ y
return theta对于多项式回归,正规方程的形式完全一致:
其中
关键细节:使用 np.linalg.pinv()(伪逆/Moore-Penrose 逆)而不是 np.linalg.inv()。当多项式次数较高时,
第5步:带正则化的线性回归
5.1 正则化的动机
当多项式次数过高时(如 degree=15),模型有过多的自由度来"记住"每个训练数据点的位置,包括噪声。曲线会剧烈震荡,在训练数据上误差很小但在测试数据上误差很大——这就是过拟合。
正则化的核心思想:在损失函数中添加对参数大小的惩罚项,约束模型复杂度。
5.2 L2 正则化(Ridge 回归)
是正则化强度: 越大,参数被压缩得越厉害 - 偏置项
通常不参与正则化——因为偏置只影响截距,不反映模型复杂度
梯度(加入正则化项后):
代码实现:
if self.reg_type == 'l2':
reg_loss = self.lambda_ * np.sum(weights_no_bias ** 2)
dw_reg[1:] = 2.0 * self.lambda_ * weights_no_bias # 偏置项不参与5.3 L1 正则化(Lasso 回归)
L1 与 L2 的关键区别:
- L2 使权重趋近于 0 但通常不为 0
- L1 倾向于产生稀疏解——许多权重精确为 0,实现内置的特征选择
梯度的正则化部分:
elif self.reg_type == 'l1':
reg_loss = self.lambda_ * np.sum(np.abs(weights_no_bias))
dw_reg[1:] = self.lambda_ * np.sign(weights_no_bias)np.sign(w) 返回每个权重的符号(
几何直觉:L2 的约束区域是圆形(球面),L1 的约束区域是菱形。损失函数的等高线与菱形的角(坐标轴上)最先接触的概率最大,而落在坐标轴上意味着某些权重精确为 0。
第6步:K-Fold 交叉验证
def kfold_cross_validation(X, y, k=5, degree=3, lambda_=0.0, reg_type='none'):为什么需要交叉验证? 正则化强度
K-Fold 流程:
- 将数据分成
等份(折/fold) - 对于每一折
: - 第
份作为验证集 - 其余
份作为训练集 - 训练模型并在验证集上计算 MSE
- 第
- 返回
次验证 MSE 的平均值
代码实现中,np.setdiff1d(np.arange(n), val_idx) 用于从全量索引中排除验证集索引,得到训练集索引。这是一种"不重叠"的划分方式——每个样本恰好被用作验证一次。
第7步:可视化套件
7.1 多项式拟合对比图
plot_polynomial_fits() 在
- 欠拟合(degree=1-2):曲线过于平滑,无法捕捉正弦波的起伏,训练 MSE 和测试 MSE 都高
- 拟合良好(degree=3-7):曲线贴合正弦波形状,训练和测试 MSE 都低
- 过拟合(degree=12-15):曲线剧烈震荡,每个训练点都被穿过,训练 MSE 极低但测试 MSE 高
代码通过比较训练和测试 MSE 的大小关系来自动判断拟合质量:
train_mse > 0.15:欠拟合deg > 12 and test_mse > 3 * train_mse:过拟合- 其余:拟合良好
7.2 Bias-Variance U 形曲线
plot_bias_variance_curve() 绘制训练误差和验证误差随多项式次数的变化:
- 训练误差(蓝线):随次数增加单调递减——模型越复杂,越能"记住"训练数据
- 验证误差(红线):呈 U 形——先降后升。最低点对应最优模型复杂度
代码用 np.argmin(val_errors) 找到验证误差最小的多项式次数,用绿色虚线标注。U 形曲线的左侧是"欠拟合区域",右侧是"过拟合区域"。
7.3 正则化效果对比
plot_regularization_effect() 对 degree=15 的严重过拟合模型应用不同正则化策略:
- 无正则化:曲线剧烈震荡,训练 MSE 很低但验证 MSE 很高
- L2 (
):曲线变得平滑,大幅降低了过拟合 - L1 (
):类似 L2,但部分系数被精确推到零 - L2 (
):更强的正则化,曲线非常平滑但可能开始欠拟合(过于平滑,跟不上正弦波的弯曲)
7.4 系数路径图
plot_coefficient_paths() 展示回归系数如何随
- L2(Ridge):所有系数从初始值连续平滑地衰减到接近零,但通常不精确为零
- L1(Lasso):随着
增大,系数逐个被"推"到精确的零——展示了 L1 的稀疏性
横轴是 np.logspace(-4, 2, 50) 在对数尺度上均匀采样 50 个
7.5 交叉验证选择模型
plot_cv_results() 对每个多项式次数计算 5-Fold 交叉验证的平均误差,画出 CV 误差曲线。最优多项式次数是 CV 误差最小的那个。
ax.fill_between() 绘制了
关键概念速查表
| 概念 | 数学形式 | 代码位置 | 关键说明 |
|---|---|---|---|
| 多项式特征 | polynomial_features() | 将线性模型变为非线性 | |
| 欠拟合 | 训练误差高 + 验证误差高 | 可视化判断 | 模型太简单,没学到规律 |
| 过拟合 | 训练误差低 + 验证误差高 | 可视化判断 | 模型记噪声,泛化差 |
| L2 正则化 | RegularizedLinearRegression | 权重衰减,平滑压缩 | |
| L1 正则化 | RegularizedLinearRegression | 产生稀疏解,特征选择 | |
| K-Fold CV | kfold_cross_validation() | 数据驱动选择超参数 | |
| Bias² | 理论概念 | 平均预测与真实的差距 | |
| Variance | 理论概念 | 预测在不同训练集上的波动 | |
| 系数路径 | plot_coefficient_paths() | 观察正则化强度的效果 | |
| 伪逆 | np.linalg.pinv() | fit_polynomial() | 稳定求解近似奇异矩阵 |
源码位置
clone 后打开(相对仓库根目录):
docs/ml/foundations/bias-variance/code/demo.py