前言
今天我们来讨论拟合的问题
在之前的篇幅,主要讨论的是线性回归的问题,不管是一元、多元、多项式,本质都是线性回归问题。线性回归在机器学习中属于“监督学习”,也就是使用已有的、预定义的“训练数据”集合,训练系统,在解释未知数据时,也能够很好的解释
而模型训练完成之后,可能会有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的多项式
脚本!启动:
在训练数据上表现不错,但是在测试数据上表现就不行了,误差明显上升,调整系数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获取多个指标
脚本!启动:
由于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)))
复制代码
小结
还有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()
复制代码 脚本!启动:
这图一看就不正常,我们丢进gpt,让它帮我们分析一下
验证曲线
用来评估模型性能与某个超参数之间关系的一种可视化工具。而所谓的超参数,则是模型中必须要设置的参数,比如多项式中的阶数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()
复制代码 脚本!启动:
懒了,直接丢ai!
正则化
所谓的正则化,就是:
- L1 正则化(Lasso 回归)
- 在损失函数中添加模型参数的绝对值之和
- 特点:倾向于将某些参数压缩到 0,从而实现特征选择
- L2 正则化(Ridge 回归)
- 在损失函数中添加模型参数的平方和
- 特点:倾向于将参数值缩小,但不会完全压缩到 0
- 弹性网络(Elastic Net)
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回归的结果达到最佳状态
脚本!启动:
通过正则化L1,也就是lasso回归,能够答复提高模型的泛化能力,其实和之前线性回归去掉无用特征一样,在高阶多项式中,lasso回归一样能够去掉无用的阶数,保留真正影响结果的阶数- print('lasso回归系数')
- print(lasso.named_steps['lasso'].coef_)
复制代码
由此可见,lasso删除了,0、3、4、7、8、9阶,保留了1、2、5、6、10阶
超参数与普通参数
- 普通参数是自己学习到的,比如线性回归中的回归系数、截距
- 超参数是模型训练之前就要设置的,比如多项式的阶数degree
由于超参数无法通过模型自己去学习,所以需要通过多种方法去尝试、调优,而超参数调优的是一件非常非常复杂的工作,涉及到多种不同模型,有很多不同的方法。这里我们看的是单个超参数(比如多项式的阶数),目的也很简单,就是通过不同超参数的表现,查看过拟合的情况
列一下一些常见的超参数,而对应的模型在今后的文章中多少都会涉及到
模型超参数含义线性回归(带正则)alpha(Ridge/Lasso)正则化强度决策树max_depth树的最大深度K 近邻n_neighbors邻居个数SVMC、gamma惩罚系数 / 核函数参数多项式回归degree多项式的阶数神经网络learning_rate、batch_size、epochs学习速率 / 批量大小 / 训练轮数小结
本文通过一个过拟合的例子,使用不同的方法,交叉验证、学习曲线、正则化等方法验证了怎么去评估模型过拟合
联系我
至此,本文结束
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |