机器学习系列笔记八:多项式回归[下]
在上一节,我们引入了多项式回归,并通过一些简单的编程来实现了它,然后,我们通过实验分析出了在回归问题中常见的两种问题:过拟合与欠拟合。
我们提到,测试集的意义就在于在投入生产环境之前就能发现这些问题,但是实际上仅凭测试集也是无法完全发现这个问题的。
所以就有了下面的交叉验证的方案。
同时,即便能识别出过拟合、欠拟合,但是如何解决这两个问题尤其是过拟合问题才是我们的根本目的,所以就有了下面的模型正则化方案。
我们希望通过在这一小节学会与更多与模型调试相关的技能。
文章目录
验证数据集与交叉验证
在上一节我们学习了多项式回归,以及它的实现,最后通过了一个小实验说明了欠拟合和过拟合的概念和判断原则。
通过将数据集切割为训练数据集和测试数据集,可以有效避免模型泛化能力弱而不自知,就直接投入生产环境的情况。
验证数据集
虽然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=1∑m(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 ;θ)+α21∑i=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=1∑n∣θ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}}
∣∣x∣∣p=(i=1∑n∣xi∣p)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=1∑n∣θi∣+21−rαi=1∑nθ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