今天我们来讨论拟合的问题
在之前的篇幅,主要讨论的是线性回归的问题,不管是一元、多元、多项式,本质都是线性回归问题。线性回归在机器学习中属于“监督学习”,也就是使用已有的、预定义的“训练数据”集合,训练系统,在解释未知数据时,也能够很好的解释
而模型训练完成之后,可能会有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通常取5或10),依次选取第i个子集作为验证集,其余k-1个子集作为训练集,重复k次,每次计算模型性能指标(如准确率、F1值等),最终结果为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!
所谓的正则化,就是:
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阶
由于超参数无法通过模型自己去学习,所以需要通过多种方法去尝试、调优,而超参数调优的是一件非常非常复杂的工作,涉及到多种不同模型,有很多不同的方法。这里我们看的是单个超参数(比如多项式的阶数),目的也很简单,就是通过不同超参数的表现,查看过拟合的情况
列一下一些常见的超参数,而对应的模型在今后的文章中多少都会涉及到
模型 | 超参数 | 含义 |
---|---|---|
线性回归(带正则) | 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
参与评论
手机查看
返回顶部