机器学习系列笔记八:多项式回归[下]

机器学习系列笔记八:多项式回归[下]

在上一节,我们引入了多项式回归,并通过一些简单的编程来实现了它,然后,我们通过实验分析出了在回归问题中常见的两种问题:过拟合与欠拟合。

我们提到,测试集的意义就在于在投入生产环境之前就能发现这些问题,但是实际上仅凭测试集也是无法完全发现这个问题的。

所以就有了下面的交叉验证的方案。

同时,即便能识别出过拟合、欠拟合,但是如何解决这两个问题尤其是过拟合问题才是我们的根本目的,所以就有了下面的模型正则化方案。

我们希望通过在这一小节学会与更多与模型调试相关的技能。

验证数据集与交叉验证

在上一节我们学习了多项式回归,以及它的实现,最后通过了一个小实验说明了欠拟合和过拟合的概念和判断原则。

通过将数据集切割为训练数据集和测试数据集,可以有效避免模型泛化能力弱而不自知,就直接投入生产环境的情况。

在这里插入图片描述

验证数据集

虽然train_test_split可以在很多时候可以让我们及时发现模型的过拟合(欠拟合),但是有一个问题仍然存在:

问题:针对特定测试数据集出现了过拟合

解决方案:

我们将整个数据分成三个部分:

  • 训练数据集
  • 验证数据集 :调整超参数使用的数据集
  • 测试数据集:作为衡量最终模型性能的数据集

我们训练好了模型之后,将验证数据集送入模型看看模型相应的效果是怎样,如果效果不好就继续训练模型,直到模型训练好的参数达到我们期望。

原来的情况是训练数据集和测试数据集都参与模型的训练,而划分为三个部分后,训练数据集就不再参与模型的训练了,这样就避免了模型针对测试数据集出现了过拟合。

最后,基于验证数据集的设定,就有了下面的交叉验证(Cross Validation)的方案。

交叉验证

在这里插入图片描述

上图画的不是很好,有一些歧义需要口头表达一下:

  • A、B、C三部分数据是随机取自训练数据集的,与测试数据集无关
  • 测试数据集不会参与到模型的训练中

接下来,我们通过代码来实现一下交叉验证。

交叉验证的实现
import numpy as np
from sklearn import datasets
digits = datasets.load_digits()
X = digits.data
y = digits.target

使用普通的train_test_split调参测试

from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.4,random_state=666)
from sklearn.neighbors import KNeighborsClassifier

best_score,best_k,best_p = 0,0,0
for k in range(2,11):
    for p in range(1,5):
        knn_clf = KNeighborsClassifier(weights="distance",n_neighbors=k,p=p)
        knn_clf.fit(X_train,y_train)
        score = knn_clf.score(X_test,y_test)
        if score > best_score:
            best_score,best_k,best_p=score,k,p

print("Best k:",best_k)
print("Best p:",best_p)
print("Best Score:",best_score)
Best k: 3
Best p: 4
Best Score: 0.9860917941585535

使用交叉验证的方式调参测试

from sklearn.model_selection import cross_val_score

knn_clf = KNeighborsClassifier()
cross_val_score(knn_clf,X_train,y_train)
array([0.98895028, 0.97777778, 0.96629213])

可以看到对于三个部分的数据都有其对应的预测准确率

from sklearn.neighbors import KNeighborsClassifier
from warnings import simplefilter
simplefilter(action='ignore', category=FutureWarning)

best_score,best_k,best_p = 0,0,0
for k in range(2,11):
    for p in range(1,5):
        knn_clf = KNeighborsClassifier(weights="distance",n_neighbors=k,p=p)
#          knn_clf.fit(X_train,y_train)
        scores = cross_val_score(knn_clf,X_train,y_train)
#         score = knn_clf.score(X_test,y_test)
        score = np.mean(scores)
        if score > best_score:
            best_score,best_k,best_p=score,k,p

print("Best k:",best_k)
print("Best p:",best_p)
print("Best Score:",best_score)
Best k: 2
Best p: 2
Best Score: 0.9823599874006478

利用查找到的最好的超参数来重新设置模型

best_knn_clf = KNeighborsClassifier(weights="distance",n_neighbors=2,p=2)
best_knn_clf.fit(X_train,y_train)
best_knn_clf.score(X_test,y_test)
0.980528511821975
回顾网格搜索

其实在scikit-learn的网格搜索类内部就是使用交叉验证来实现最佳超参数搜索的

from sklearn.model_selection import GridSearchCV
# GridSearchCV中的CV就是交叉验证的意思

param_grid = [
    {
        "weights":['distance'],
        'n_neighbors':[i for i in range(2,11)],
        'p':[i for i in range(1,6)]
    }
]

grid_search = GridSearchCV(knn_clf,param_grid,verbose=1,n_jobs=-1)
grid_search.fit(X_train,y_train)
Fitting 3 folds for each of 45 candidates, totalling 135 fits


[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done  34 tasks      | elapsed:    5.6s
[Parallel(n_jobs=-1)]: Done 135 out of 135 | elapsed:   29.9s finished

GridSearchCV(cv='warn', error_score='raise-deprecating',
             estimator=KNeighborsClassifier(algorithm='auto', leaf_size=30,
                                            metric='minkowski',
                                            metric_params=None, n_jobs=None,
                                            n_neighbors=10, p=4,
                                            weights='distance'),
             iid='warn', n_jobs=-1,
             param_grid=[{'n_neighbors': [2, 3, 4, 5, 6, 7, 8, 9, 10],
                          'p': [1, 2, 3, 4, 5], 'weights': ['distance']}],
             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
             scoring=None, verbose=1)

Fitting 3 folds for each of 45 candidates, totalling 135 fits的意思如下:

  • 需要对5*9=45组参数进行搜索
  • 每组参数又要3个模型来计算他们的平均值(3是因为交叉验证)
  • 所以总共需要进行45*3=135次训练
grid_search.best_score_
0.9823747680890538
grid_search.best_params_
{'n_neighbors': 2, 'p': 2, 'weights': 'distance'}
best_knn_clf = grid_search.best_estimator_
best_knn_clf.score(X_test,y_test)
0.980528511821975

补充:可以通过在cross_val_score函数中指定cv=split_num,来指定需要将训练集分为多少份数据来进行交叉验证

cross_val_score(knn_clf,X_train,y_train,cv=5)
array([0.99543379, 0.96803653, 0.98148148, 0.97196262, 0.97619048])

同样,在GridSearchCV的构造函数中也可以传入cv参数来指定分为多少份数据

grid_search5 = GridSearchCV(knn_clf,param_grid,verbose=1,cv=5)
grid_search5.fit(X_train,y_train)
Fitting 5 folds for each of 45 candidates, totalling 225 fits


[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done 225 out of 225 | elapsed:   32.3s finished

GridSearchCV(cv=5, error_score='raise-deprecating',
             estimator=KNeighborsClassifier(algorithm='auto', leaf_size=30,
                                            metric='minkowski',
                                            metric_params=None, n_jobs=None,
                                            n_neighbors=10, p=4,
                                            weights='distance'),
             iid='warn', n_jobs=None,
             param_grid=[{'n_neighbors': [2, 3, 4, 5, 6, 7, 8, 9, 10],
                          'p': [1, 2, 3, 4, 5], 'weights': ['distance']}],
             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
             scoring=None, verbose=1)
对交叉验证的补充

对于k-folds交叉验证,意味着需要将训练数据集分为k份,称之为k-folds cross validation

缺点:每次训练k个模型,相当于整体性能慢了k倍。

在极端情况下,这种k-folds交叉验证可以变成叫做留一法LOO-CV这样的一种交叉验证方式

把训练数据集分为m份,每次都将m-1份样本用于训练,然后通过剩下的那一份样本来判定预测的结果称为留一法Leave-One-Out Cross Validation

这样做将完全不受随机的影响,最接近模型真正的性能指标。

缺点:计算量巨大

偏方误差权衡

偏差与方差

首先我们需要了解偏方误差(偏差+方差)是什么,如下图所示:

在这里插入图片描述
偏差: 描述的是预测值(估计值)的期望与真实值之间的差距。偏差越大,越偏离真实数据,如下图第二行所示。

方差: 描述的是预测值的变化范围,离散程度,也就是离其期望值的距离。方差越大,数据的分布越分散,如下图右列所示。

参考自 偏差和方差有什么区别? - Jason Gu的回答 - 知乎 https://www.zhihu.com/question/20448464/answer/20039077

模型误差组成

一般而言,模型误差由如下式子表示:
模 型 误 差 = 偏 差 ( B i a s ) + 方 差 ( V a r i a n c e ) + 不 可 避 免 的 误 差 模型误差 = 偏差(Bias) + 方差(Variance) + 不可避免的误差 =(Bias)+(Variance)+
导致偏差的主要原因:

  • 对问题本身的假设不正确
    • 如:非线性数据使用线性回归
    • 如:用学生的名字来预测其成绩(特征直接高度不相关)

欠拟合 under-fiting

导致方差的主要原因

数据的一点点扰动都会较大地影响模型。换言之我们的模型没有完全学习到问题的实质,而学习到了很多的噪音。

同样原因是使用的模型太过复杂。如高阶多项式回归。

过拟合 over-fiting

算法与偏方差

有一些算法天生是高方差的算法,如KNN。

非参数学习通常都是高方差算法。因为不对数据进行任何假设,所以导致这类算法的模型是对训练数据高度敏感的,所以一旦通过测试集来预测就会产生较大的方差。

有一些算法天生是高偏差的算法,如线性回归。

参数学习通常都是高偏差算法,因为对数据进行了极强的假设,一旦假设本身有问题,比如前面用线性模型来预测非线性数据,就会产生较大的偏差。

大多数算法具有相应的参数,可以调整偏差与方差,比如KNN算法中的k

k越小,模型越复杂,相应的模型方差越大,偏差越小,直到k达到最大值=样本总数的时候,就达到了这个算法的偏差最小方差最大。

偏差与方差的关系

偏差和方差通常是矛盾的。

  • 降低偏差,方差就会增大
  • 降低方差,对应地就会提高偏差

我们地目标往往是期望能将算法所产生的偏差与方差达到一个平衡。这也是我们在调参的时候主要做的事情,

机器学习的主要挑战是来自于方差的,而解决高方差的通常手段如下:

  • 降低模型复杂度

  • 减少数据维度;降噪(PCA)

  • 增加样本数

    深度学习

  • 使用验证集

  • 模型正则化

模型正则化

模型正则化是一种有效缓解模型过拟合问题的手段,其本质是对参数 θ \theta θ 的约束。 y = θ T X y=\theta^TX y=θTX,当 θ \theta θ 变小时,相应的x就没有意义了,相当于x变小了。

通常的解释是,越小的权重,模型复杂度越低(例如特征X剧烈变化时,由于 θ \theta θ 很小,y的变化就会比较小),因此能够更简单有效的描绘数据,所以我们倾向于选择较小的权重。

言而总之,模型正则化的作用是通过限制参数的大小来抑制过拟合。

模型正则化的实现原理

既然我们想通过限制参数的大小来对模型进行正则化,其实我们只需要对损失函数稍作修改就能达到我们的目的。

原来目标函数的设定:

目标:使得
∑ i = 1 m ( y ( i ) − θ 0 − θ 1 X 1 ( i ) − θ 2 X 2 ( i ) − . . . − θ n X n ( i ) ) 2 \sum_{i=1}^m{\left( y^{\left( i \right)}-\theta _0-\theta _1X_{1}^{\left( i \right)}-\theta _2X_{2}^{\left( i \right)}-...-\theta _nX_{n}^{\left( i \right)} \right) ^2} i=1m(y(i)θ0θ1X1(i)θ2X2(i)...θnXn(i))2 尽可能地小,故通过变换可以推导出:

目标:使得 J ( θ ) = M S E ( y , y ^ ; θ ) J\left( \theta \right) =MSE\left( y,\widehat{y};\theta \right) J(θ)=MSE(y,y ;θ) 尽可能地小

加入模型正则化后的目标函数

加入模型正则化,目标:使得 J ( θ ) = M S E ( y , y ^ ; θ ) + α 1 2 ∑ i = 1 n θ i 2 J\left( \theta \right) =MSE\left( y,\widehat{y};\theta \right) +\alpha \frac{1}{2}\sum_{i=1}^n{\theta _{i}^{2}} J(θ)=MSE(y,y ;θ)+α21i=1nθi2 尽可能的小。

通过对原有的目标函数中加入所有 θ i \theta_i θi的平方和,就可以使得梯度下降法在考虑降低MSE的同时,将 θ \theta θ 也考虑到其中。

在上式中, α \alpha α 是一个超参数,我们需要通过多次测试来得到它合适的取值,一般来说 α \alpha α 的取值越大,模型的预测曲线越平滑,反之亦然。

对于这种正则化模型的方式,通常称之为岭回归 Ridge Regression

岭回归方式实现

首先我们准备数据

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)
x = np.random.uniform(-3,3,size=100)
X = x.reshape(-1,1)
y = 0.5 * x + 3 + np.random.normal(0,1,size=len(x)) 
plt.scatter(x,y)
plt.show()

在这里插入图片描述

使用多项式回归对样本数据进行拟合

# 分割数据集
from sklearn.model_selection import train_test_split
np.random.seed(666)
X_train,X_test,y_train,y_test = train_test_split(X,y)
# 定义多项式回归模型
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

def polynomialRgression(degree):
    return Pipeline([
        ('poly',PolynomialFeatures(degree=degree)),
        ('std_scaler',StandardScaler()),
        ('lin_reg',LinearRegression())
    ])

poly_reg = polynomialRgression(degree=20)
poly_reg.fit(X_train,y_train)
y_poly_predict = poly_reg.predict(X_test)
mean_squared_error(y_test,y_poly_predict)
167.9401085999025

可视化预测结果

X_plot = np.linspace(-3,3,100).reshape(-1,1)
y_plot = poly_reg.predict(X_plot)

plt.scatter(x,y)
plt.plot(X_plot,y_plot,color='r')
plt.axis([-3,3,0,6])
plt.show()

在这里插入图片描述

这显然是过拟合的。为方便后面的实现,将绘图的代码封装到一个方法内

def plot_model(model):
    X_plot = np.linspace(-3,3,100).reshape(100,1)
    y_plot = model.predict(X_plot)
    plt.scatter(x,y)
    plt.plot(X_plot,y_plot,color='r')
    plt.axis([-3,3,0,6])
    plt.show()  

使用岭回归来对模型进行正则化

from sklearn.linear_model import Ridge

# ridge = Ridge(alpha=1) 
def RidgeRegression(degree,alpha):
    return Pipeline([
        ('poly',PolynomialFeatures(degree=degree)),
        ('std_scaler',StandardScaler()),
        ('ridge_reg',Ridge(alpha=alpha))
    ])
ridge1_reg = RidgeRegression(degree=20,alpha=0.0001)
ridge1_reg.fit(X_train,y_train)

y1_predict = ridge1_reg.predict(X_test)
mean_squared_error(y_test,y1_predict)
1.3233492754136291

可以看到,通过正则化模型,我们模型的预测方差从167.9401085999025下降到了1.3233492754136291

plot_model(ridge1_reg)

在这里插入图片描述

ridge2_reg = RidgeRegression(degree=20,alpha=1)
ridge2_reg.fit(X_train,y_train)

y2_predict = ridge2_reg.predict(X_test)
mean_squared_error(y_test,y2_predict)
1.1888759304218461
plot_model(ridge2_reg)

在这里插入图片描述

ridge3_reg = RidgeRegression(degree=20,alpha=100)
ridge3_reg.fit(X_train,y_train)

y3_predict = ridge3_reg.predict(X_test)
mean_squared_error(y_test,y3_predict)
1.3196456113086197
plot_model(ridge3_reg)

在这里插入图片描述

可以看到随着 α \alpha α 的增大,模型预测曲线是越来越平滑的,我们需要找到它合适的取值,使得模型的均方差最低。

LASSO Regression

除了岭回归,还有一个模型正则化的方法称为LASSO Regression,它的目标函数设定原理于岭回归类似,只是将拖尾的 θ 2 \theta^2 θ2 改为了量级更低的 ∣ θ ∣ |\theta| θ

其损失函数的设定如下:
J ( θ ) = M S E ( y , y ^ ; θ ) + α   ∑ i = 1 n ∣ θ i ∣ J\left( \theta \right) =MSE\left( y,\widehat{y};\theta \right) +\alpha \ \sum_{i=1}^n{|\theta _{i}|} J(θ)=MSE(y,y ;θ)+α i=1nθi
Ridge Regression于LASSO Regression的比较如下:

在这里插入图片描述 在这里插入图片描述

LASSO趋向于使得一部分 θ \theta θ 值变为0,所以可作为特征选择用。

LASSO Regression 的实现

和岭回归的实现方式一样,我们通过管道来完成

from sklearn.linear_model import Lasso

def LassoRegression(degree,alpha):
    return Pipeline([
        ('poly',PolynomialFeatures(degree=degree)),
        ('std_scaler',StandardScaler()),
        ('lasso_reg',Lasso(alpha=alpha))
    ])
lasso1_reg = LassoRegression(20,0.01)
lasso1_reg.fit(X_train,y_train)
y1_predict = lasso1_reg.predict(X_test)
mean_squared_error(y_test,y1_predict)
1.149608084325997
plot_model(lasso1_reg)

在这里插入图片描述

我们继续通过改变 α \alpha α来观察模型的拟合曲线

lasso2_reg = LassoRegression(20,0.1)
lasso2_reg.fit(X_train,y_train)
y2_predict = lasso2_reg.predict(X_test)
mean_squared_error(y_test,y2_predict)
1.1213911351818648
plot_model(lasso2_reg)

在这里插入图片描述

lasso3_reg = LassoRegression(20,1)
lasso3_reg.fit(X_train,y_train)
y3_predict = lasso3_reg.predict(X_test)
mean_squared_error(y_test,y3_predict)
1.8408939659515595
plot_model(lasso3_reg)

在这里插入图片描述

显然对于LASSO Regression正则化当前模型,取 α = 1 \alpha=1 α=1 是不合适的。

L1、L2正则项,弹性网

首先在矩阵分析领域中,对向量和矩阵有一个计量标准称之为范数,其中针对向量有一个非常重要的范数称之为向量P范数(LP)。其定义如下:
∣ ∣ x ∣ ∣ p = ( ∑ i = 1 n ∣ x i ∣ p ) 1 p ||x||_p = (\sum_{i=1}^{n}|x_i|^p)^{\frac{1}{p}} xp=(i=1nxip)p1
由此,上面提到的

  • Ridge Regression中损失函数的附加项 ∑ i = 1 n ( θ i ) 2 \sum^n_{i=1}(\theta_i)^2 i=1n(θi)2 也称之为L2正则项
  • LASSO Regression中损失函数的附加项 ∑ i = 1 n ∣ θ i ∣ \sum^n_{i=1}|\theta_i| i=1nθi 也称之为L1正则项

同样,依据LP范数,也会有其余的Ln正则项如: ∑ i = 1 n ( θ i ) n \sum^n_{i=1}(\theta_i)^n i=1n(θi)n

基于L1正则项和L2正则项,从而就有了弹性网Elastic NeT,其定义是基于二者的融合的,如下:
J ( θ ) = M S E ( y , y ^ ; θ ) + r α ∑ i = 1 n ∣ θ i ∣ + 1 − r 2 α ∑ i = 1 n θ i 2 J\left( \theta \right) =MSE\left( y,\widehat{y};\theta \right) +r\alpha\sum^n_{i=1}|\theta_i| + \frac{1-r}{2}\alpha\sum_{i=1}^n{\theta _{i}^{2}} J(θ)=MSE(y,y ;θ)+rαi=1nθi+21rαi=1nθi2

它结合了岭回归和LASSO回归的特性,所以被称作弹性网。

在这里插入图片描述

总结

在这里插入图片描述

参考致谢

liuyubobo:https://github.com/liuyubobobo/Play-with-Machine-Learning-Algorithms

liuyubobo:https://coding.imooc.com/class/169.html

莫烦Python:https://www.bilibili.com/video/BV1xW411Y7Qd?from=search&seid=11773783205368546023

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值