Erlo

彩笔运维勇闯机器学习#sql_zs#拟合

2025-09-01 11:30:25 发布   10 浏览  
页面报错/反馈
收藏 点赞

前言

今天我们来讨论拟合的问题

在之前的篇幅,主要讨论的是线性回归的问题,不管是一元、多元、多项式,本质都是线性回归问题。线性回归在机器学习中属于“监督学习”,也就是使用已有的、预定义的“训练数据”集合,训练系统,在解释未知数据时,也能够很好的解释

而模型训练完成之后,可能会有3中状态:“欠拟合”、“最佳适配”、“过拟合”。本小节就来消息讨论一下,怎么判断训练出来的模型处于什么样的状态

过拟合

老规矩,先运行起来,再探索原理

import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score

np.random.seed(0)
X = np.linspace(0, 1, 30)
y_true = np.sin(2 * np.pi * X)
y = y_true + np.random.normal(0, 0.2, X.shape)

X_train, X_test, y_train, y_test = train_test_split(X.reshape(-1, 1), y, test_size=0.3)

degree = 10
model = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('line', LinearRegression())
])
model.fit(X_train, y_train)

y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

mse_train = mean_squared_error(y_train, y_train_pred)
r2_train = r2_score(y_train, y_train_pred)
mse_test = mean_squared_error(y_test, y_test_pred)
r2_test = r2_score(y_test, y_test_pred)

print(f"训练集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"验证集 MSE: {mse_test:.4f} ,R²:{r2_test}")

数据是由sin函数加上一些噪点组成的,按照37比例分成训练集与测试集。而模型则是最高阶为10的多项式

脚本!启动:
watermarked-fit_regression_1_1

在训练数据上表现不错,但是在测试数据上表现就不行了,误差明显上升,调整系数R²也下降了,这就是所谓的过拟合现象

交叉验证

从上面看到,将训练数据手动划分为两部分,训练集与测试集,通过测试集,就发现了模型的过拟合现象。那将训练数据多次划分,并且重复训练与验证,就能有更大的概率提前发现模型过拟合情况。当然,手动做这个工作耗时耗力,而本小节要讨论的交叉验证就是为了完成这个工作的

留出法

这在之前的演示中已经给出来了,就是主动划分训练集与测试集,通过random_state来决定每次划分的集合不同

  • 优点:简单易用
  • 缺点:结果受单次划分影响大,尤其在小数据集中波动性高
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

k折交叉验证

将数据集均等分为k个子集(k通常取5或10),依次选取第i个子集作为验证集,其余k-1个子集作为训练集,重复k次,每次计算模型性能指标(如准确率、F1值等),最终结果为k次验证的平均值。本质上就是多次计算去平均值

  • 优点:降低数据划分的随机性,结果更稳定
  • 缺点:计算成本较高(需训练k次模型)
kf = KFold(n_splits=5, shuffle=True, random_state=0)
from sklearn.model_selection import KFold, cross_val_score

kf = KFold(n_splits=5, shuffle=True, random_state=0)

neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores
print("5折MSE:{}".format(np.round(mse_scores, 2)))
print("平均MSE:{} n".format(round(np.mean(mse_scores), 2)))

r2_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring="r2")
print("5折R²分数:{}".format(r2_scores))
print("平均R²:{}n".format(r2_scores.mean()))

这里也可以使用cross_validate获取多个指标

脚本!启动:

watermarked-fit_regression_1_2

由于k折交叉验证非常常用,可以适应大部分情况,这里给出第二种写法,灵活使用

mse_list = []
r2_list = []

for train_index, test_index in kf.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    model.fit(X_train.reshape(-1, 1), y_train.reshape(-1, 1))
    y_pred = model.predict(X_test.reshape(-1, 1))

    mse = mean_squared_error(y_test.reshape(-1, 1), y_pred)
    r2 = r2_score(y_test.reshape(-1, 1), y_pred)

    mse_list.append(mse)
    r2_list.append(r2)

print("5折MSE:{}".format(np.round(mse_list, 2)))
print("平均MSE:{} n".format(round(np.mean(mse_list), 2)))

print("5折R²分数:{}".format(np.round(r2_list, 2)))
print("平均R²:{}n".format(round(np.mean(r2_list), 4)))

第二种写法更是解释了,k折交叉验证本质就是自动划分训练集与测试集,然后再去进行模型训练

综上所述,在某个定义域内(0~1),10阶多项式去解释sin函数(加入噪点),平均mse是0.25,平均R²是0.56,模型泛化能力是非常差的

最后补充一点:

n_splits这个参数不但控制了折数,还控制了训练集与测试集的比例,比如n_splits=5,每次用 4/5 的数据做训练集,1/5 做测试集;比如n_splits=3,每次用 2/3 的数据做训练集,1/3 做测试集

留一交叉验证

k折交叉是按照比例,按照折数(通常5折或10折),对训练集与测试集进行“比例”划分,由n_splits参数控制。而留一交叉每次只会选择1个样本作为测试机,其余的为训练集,然后遍历整个样本进行训练

举个例子,如果样本数为[1,2,3,4,5]

训练集 测试集
[1,2,3,4] [5]
[1,2,3,5] [4]
[1,2,4,5] [3]
[1,2,3,5] [2]
[2,3,4,5] [1]

如果样本量很小的情况,那留一交叉验证就非常适合,因为每个样本都被用作验证集一次,不浪费任何一个数据点。但是一旦样本数量变多,那训练的速度就会非常慢

from sklearn.model_selection import LeaveOneOut, cross_val_score

loo = LeaveOneOut()
neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=loo, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores

print("平均MSE:{} n".format(round(np.mean(mse_scores), 2)))

watermarked-fit_regression_1_3

小结

还有2个常用的分层k折交叉验证、时间序列交叉验证,这里做一个对比,就不展开细说了

方法 适用场景 优点 缺点
留出法 大数据集快速验证 计算快 结果受单次划分影响大
k折交叉验证 通用场景 结果稳健 计算成本中等
留一法(LOO) 极小数据集 无偏差 计算成本极高
分层k折 类别不平衡数据 保持类别分布 仅适用于分类问题
时间序列CV 时间相关数据 防止未来信息泄露 必须按时间顺序划分

学习曲线

通过训练误差与测试误差,来判断模型是否过拟合:

  • 欠拟合:训练误差和验证误差都很高,模型太简单
  • 过拟合:训练误差很低,但验证误差很高,模型太复杂
  • 恰到好处:训练误差和验证误差都低,并且两者接近
train_sizes, train_scores, valid_scores = learning_curve(
    model, X_train, y_train, cv=5, n_jobs=-1,
    train_sizes=np.linspace(0.1, 1.0, 10)
)

train_scores_mean = np.mean(train_scores, axis=1)
valid_scores_mean = np.mean(valid_scores, axis=1)

plt.figure()
plt.plot(train_sizes, train_scores_mean, 'o-', color='r', label='Training score')
plt.plot(train_sizes, valid_scores_mean, 'o-', color='g', label='Validation score')

plt.xlabel('Training examples')
plt.ylabel('Score')
plt.title('Learning Curve')
plt.legend(loc='best')
plt.grid(True)
plt.show()

脚本!启动:

watermarked-fit_regression_1_4

这图一看就不正常,我们丢进gpt,让它帮我们分析一下

watermarked-fit_regression_1_5

watermarked-fit_regression_1_6

验证曲线

用来评估模型性能与某个超参数之间关系的一种可视化工具。而所谓的超参数,则是模型中必须要设置的参数,比如多项式中的阶数degree、lasso|ridge中的alpha等等

from sklearn.model_selection import validation_curve
from sklearn.model_selection import train_test_split

param_range = np.arange(1, 15)
train_scores, valid_scores = validation_curve(
    model, X.reshape(-1, 1), y,
    param_name='poly__degree',
    param_range=param_range,
    cv=5,
    scoring='r2'
)

train_mean = np.mean(train_scores, axis=1)
valid_mean = np.mean(valid_scores, axis=1)

plt.figure(figsize=(8, 5))
plt.plot(param_range, train_mean, label='Training Score', marker='o', color='r')
plt.plot(param_range, valid_mean, label='Validation Score', marker='o', color='g')
plt.xlabel('Polynomial Degree')
plt.ylabel('R² Score')
plt.legend(loc='best')
plt.grid(True)
plt.xticks(param_range)
plt.show()

脚本!启动:

watermarked-fit_regression_1_7

懒了,直接丢ai!

watermarked-fit_regression_1_8

watermarked-fit_regression_1_9

watermarked-fit_regression_1_10

正则化

所谓的正则化,就是:

  • L1 正则化(Lasso 回归)
    • 在损失函数中添加模型参数的绝对值之和
    • 特点:倾向于将某些参数压缩到 0,从而实现特征选择
  • L2 正则化(Ridge 回归)
    • 在损失函数中添加模型参数的平方和
    • 特点:倾向于将参数值缩小,但不会完全压缩到 0
  • 弹性网络(Elastic Net)
    • 结合 L1 和 L2 正则化

lasso与ridge我们之前在线性回归的时候用过,用来降低无用特征的对结果的影响,而lasso与ridge也可以抑制高阶项系数

用lasso来测试一下

from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV

lassoCV = LassoCV(alphas=np.logspace(-4, 0, 20), cv=5, max_iter=1000000)
lasso = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('scaler', StandardScaler()),
    ('lasso', lassoCV)
])

lasso.fit(X_train, y_train)
lasso_train_pred = lasso.predict(X_train)
lasso_test_pred = lasso.predict(X_test)

mse_train = mean_squared_error(y_train, lasso_train_pred)
r2_train = r2_score(y_train, lasso_train_pred)
mse_test = mean_squared_error(y_test, lasso_test_pred)
r2_test = r2_score(y_test, lasso_test_pred)

print('===='*20)
print('lasso:n')
print(f"训练集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"验证集 MSE: {mse_test:.4f} ,R²:{r2_test}")

lasso与之前的使用方式不同,使用了LassoCV,新方式可以自动选择alpha,并且会尝试所有的alpha可能值,再加上交叉验证,使得lasso回归的结果达到最佳状态

脚本!启动:

watermarked-fit_regression_1_11

通过正则化L1,也就是lasso回归,能够答复提高模型的泛化能力,其实和之前线性回归去掉无用特征一样,在高阶多项式中,lasso回归一样能够去掉无用的阶数,保留真正影响结果的阶数

print('lasso回归系数')
print(lasso.named_steps['lasso'].coef_)

watermarked-fit_regression_1_12

由此可见,lasso删除了,0、3、4、7、8、9阶,保留了1、2、5、6、10阶

超参数与普通参数

  • 普通参数是自己学习到的,比如线性回归中的回归系数、截距
  • 超参数是模型训练之前就要设置的,比如多项式的阶数degree

由于超参数无法通过模型自己去学习,所以需要通过多种方法去尝试、调优,而超参数调优的是一件非常非常复杂的工作,涉及到多种不同模型,有很多不同的方法。这里我们看的是单个超参数(比如多项式的阶数),目的也很简单,就是通过不同超参数的表现,查看过拟合的情况

列一下一些常见的超参数,而对应的模型在今后的文章中多少都会涉及到

模型 超参数 含义
线性回归(带正则) alpha(Ridge/Lasso) 正则化强度
决策树 max_depth 树的最大深度
K 近邻 n_neighbors 邻居个数
SVM C、gamma 惩罚系数 / 核函数参数
多项式回归 degree 多项式的阶数
神经网络 learning_rate、batch_size、epochs 学习速率 / 批量大小 / 训练轮数

小结

本文通过一个过拟合的例子,使用不同的方法,交叉验证、学习曲线、正则化等方法验证了怎么去评估模型过拟合

联系我

  • 联系我,做深入的交流


至此,本文结束
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...

本文来自博客园,作者:it排球君,转载请注明原文链接:https://www.cnblogs.com/MrVolleyball/p/19067789

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。

登录查看全部

参与评论

评论留言

还没有评论留言,赶紧来抢楼吧~~

手机查看

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认