在了解了sklearn的一些常用的操作之后,接下来,我们来详细探讨关于正则化的相关内容,并就sklearn中的逻辑回归的参数进行详细解释。需要注意的是,由于sklearn内部参数的一致性,有许多参数不仅是逻辑回归的参数,更是大多数分类模型的通用参数。
# 科学计算模块
import numpy as np
import pandas as pd
# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt
# 自定义模块
from ML_basic_function import *
在上一小节中,我们已经尝试着利用逻辑回归构建了一个多分类模型,得益于sklearn中良好的默认参数设置,我们在对sklearn中内部构造基本没有任何了解的情况下就完成了相关模型的建模。但需要知道的事,逻辑回归作为一个诞生时间较早并且拥有深厚统计学背景的模型,其实是拥有非常多变种应用方法的,虽然我们在Lesson 4中就逻辑回归的基本原理、基础公式以及分类性能进行了较长时间的探讨,但实际上逻辑回归算法的模型形态和应用方式远不仅于此。而在sklearn中,则提供了非常丰富的逻辑回归的可选的算法参数,相当于是提供了一个集大成者的逻辑回归模型。
当然,首先我们需要介绍一个非常重要的机器学习中的概念,正则化。在逻辑回归的说明文档中,第一个参数就是关于正则化的一个选项:
from sklearn.linear_model import LogisticRegression
LogisticRegression?
也就是penalty='l2’一项,而正则化也是机器学习中非常通用的一项操作。接下来,我们就正则化的相关内容展开讨论。
从说明文档中得知,就是在sklearn中,逻辑回归模型是默认进行正则化的,即上文所述“Regularization is applied by default”,这是一种在机器学习建模过程中常见的用法,但并非统计学常用方法。据此我们也知道了统计学和机器学习方法之间的又一个区别,并且能够清楚的感受到sklearn是一个非常“机器学习”的算法库,很多时候会从便于机器学习建模的角度出发对算法进行微调,而这也是sklearn算法库的一大特性,这个特性在导致其非常易用的同时,也使得其很多算法和原始提出的算法会存在略微的区别,这点也是初学者需要注意的。
其实机器学习中正则化(regularization)的外在形式非常简单,就是在模型的损失函数中加上一个正则化项(regularizer),有时也被称为惩罚项(penalty term),如下方程所示,其中L为损失函数,J为正则化项。通常来说,正则化项往往是关于模型参数的1-范数或者2-范数,当然也有可能是这两者的某种结合,例如sklearn的逻辑回归中的弹性网正则化项,其中加入模型参数的1-范数的正则化也被称为 l 1 l1 l1正则化,加入模型参数的2-范数的正则化也被称为 l 2 l2 l2正则化。
1 N ∑ i = 1 N L ( y i , f ( x i ) ) + λ J ( f ) \frac{1}{N}\sum^{N}_{i=1}L(y_i,f(x_i))+\lambda J{(f)} N1i=1∑NL(yi,f(xi))+λJ(f)
正则化的过程比不复杂,但何时需要进行正则化呢?
一般来说,正则化核心的作用是缓解模型过拟合倾向,此外,由于加入正则化项后损失函数的形体发生了变化,因此也会影响损失函数的求解过程,在某些时候,加入了正则化项之后会让损失函数的求解变得更加高效。如此前介绍的岭回归,其实就是在线性回归的损失函数基础上加入了w的1-范数,而Lasso则是加入了w的2-范数。并且,对于逻辑回归来说,如果加入 l 2 l2 l2正则化项,损失函数就会变成严格的凸函数。
要讨论正则化是如何缓解过拟合倾向的问题,需要引入两个非常重要的概念:经验风险和结构风险。
在我们构建损失函数求最小值的过程,其实就是依据以往经验(也就是训练数据)追求风险最小(以往数据误差最小)的过程,而在给定一组参数后计算得出的损失函数的损失值,其实就是经验风险。而所谓结构风险,我们可以将其等价为模型复杂程度,模型越复杂,模型结构风险就越大。而正则化后的损失函数在进行最小值求解的过程中,其实是希望损失函数本身和正则化项都取得较小的值,即模型的经验风险和结构风险能够同时得到控制。
模型的经验风险需要被控制不难理解,因为我们希望模型能够尽可能的捕捉原始数据中的规律,但为何模型的结构风险也需要被控制?核心原因在于,尽管在一定范围内模型复杂度增加能够有效提升模型性能,但模型过于复杂可能会导致另一个非常常见的问题——模型过拟合,关于模型过拟合的概念我们稍后会进行更加详细的介绍,但总的来说,一旦模型过拟合了,尽管模型经验风险在降低、但模型的泛化能力会下降。因此,为了控制模型过拟合倾向,我们可以把模型结构风险纳入损失函数中一并考虑,当模型结构风险的增速高于损失值降低的收益时,我们就需要停止参数训练(迭代)。
同时要求模型性能和模型复杂度都在一个合理的范围内,其实等价于希望训练得到一个较小的模型同时具有较好的解释数据的能力(规律捕捉能力),这也符合奥卡姆剃刀原则。
此前我们曾深入探讨过关于机器学习建模有效性的问题,彼时我们得出的结论是当训练数据和新数据具有规律的一致性时,才能够进行建模,而只有挖掘出贯穿始终的规律(同时影响训练数据和新数据的规律),模型才能够进行有效预测。不过,既然有些贯穿始终的全局规律,那就肯定存在一些只影响了一部分数据的局部规律。一般来说,由于全局规律影响数据较多,因此更容易被挖掘,而局部规律只影响部分数据,因此更难被挖掘,因此从较为宽泛的角度来看,但伴随着模型性能提升,也是能够捕获很多局部规律的。但是需要知道的是,局部规律对于新数据的预测并不能起到正面的作用,反而会影响预测结果,此时就出现模型过拟合现象。我们可以通过如下实例进行说明:
# 设计随机数种子
np.random.seed(123)
# 创建数据
n_dots = 20
x = np.linspace(0, 1, n_dots) # 从0到1,等宽排布的20个数
y = np.sqrt(x) + 0.2*np.random.rand(n_dots) - 0.1
x
#array([0. , 0.05263158, 0.10526316, 0.15789474, 0.21052632,
# 0.26315789, 0.31578947, 0.36842105, 0.42105263, 0.47368421,
# 0.52631579, 0.57894737, 0.63157895, 0.68421053, 0.73684211,
# 0.78947368, 0.84210526, 0.89473684, 0.94736842, 1. ])
y
#array([0.03929384, 0.1866436 , 0.26981313, 0.40762266, 0.50272526,
# 0.49761047, 0.65810433, 0.64394293, 0.64507206, 0.66667071,
# 0.69411185, 0.80669585, 0.78243386, 0.73910577, 0.83800393,
# 0.9361224 , 0.85416128, 0.88099565, 0.9796388 , 1.00636552])
其中,x是一个0到1之间等距分布20个点组成的ndarray, y = x + r y=\sqrt{x}+r y=x+r,其中r是人为制造的随机噪声,在[-0.1,0.1]之间服从均匀分布。然后我们借助numpy的polyfit函数来进行多项式拟合,polyfit函数会根据设置的多项式阶数,在给定数据的基础上利用最小二乘法进行拟合,并返回拟合后各阶系数。该函数更多相关参数详见numpy.polynomial.polynomial.polyfit官网API讲解。同时,当系数计算完成后,我们还常用ploy1d函数逆向构造多项式方程,进而利用方程求解y,该函数用法参见numpy.poly1d官网说明。
例如人为制造一个二阶多项式方程然后进行二阶拟合实验
y0 = x ** 2
np.polyfit(x, y0, 2) #2阶多项式来进行拟合
#array([1.00000000e+00, 4.16603364e-17, 1.85278864e-17])
能够得出多项式各阶系数,而根据该系数可用ploy1d逆向构造多项式方程
p = np.poly1d(np.polyfit(x, y0, 2))
print(p)
# 2
#1 x + 4.166e-17 x + 1.853e-17
能够看到多项式结构基本和原多项式保持一致,此时生成的p对象相当于是一个多项式方程,可通计算输入参数的多项式输出结果
p(-1)
#0.9999999999999998
np.poly1d(np.polyfit(x, y, 3))
#poly1d([ 1.90995297, -3.61611811, 2.6742144 , 0.04912333])
接下来,进行多项式拟合。分别利用1阶x多项式、3阶x多项式和10阶x多项式来拟合y。并利用图形观察多项式的拟合度,首先我们可定义一个辅助画图函数,方便后续我们将图形画于一张画布中,进而方便观察
def plot_polynomial_fit(x, y, deg):
p = np.poly1d(np.polyfit(x, y, deg))
t = np.linspace(0, 1, 200)
plt.plot(x, y, 'ro', t, p(t), '-', t, np.sqrt(t), 'r--')
其中,t为[0,1]中等距分布的100个点,而p是deg参数决定的多项式回归拟合方程,p(t)即为拟合方程x输入t值时多项式输出结果,此处plot_polynomial_fit函数用于生成同时包含(x,y)原始值组成的红色点图、(t,p(t))组成的默认颜色的曲线图、(t,np.sqrt(t))构成的红色虚线曲线图。测试3阶多项式拟合结果
plot_polynomial_fit(x, y, 3)
这里需要注意(x,y)组成的红色点图相当于带有噪声的二维空间数据分布,(t, p(t))构成的蓝色曲线相当于3阶多项式拟合原数据集((x, y)数据集)后的结果,而原始数据集包含的客观规律实际上是 y = x y=\sqrt{x} y=x,因此最后红色的虚线(t, np.sqrt(t))实际上是代表红色点集背后的客观规律,即我们希望拟合多项式(蓝色曲线)能够尽可能的拟合代表客观规律的红色虚线,而不是被噪声数据所吸引偏离红色虚线位置,同时也不希望完全没有捕捉到红色曲线的规律。接下来,我们尝试将1阶拟合、3阶拟合和10阶拟合绘制在一张图中。
plt.figure(figsize=(18, 4), dpi=200)
titles = ['Under Fitting', 'Fitting', 'Over Fitting']
for index, deg in enumerate([1, 3, 10]):
plt.subplot(1, 3, index + 1)
plot_polynomial_fit(x, y, deg)
plt.title(titles[index], fontsize=20)
根据最终的输出结果我们能够清楚的看到,1阶多项式拟合的时候蓝色拟合曲线即无法捕捉数据集的分布规律,离数据集背后客观规律也很远,而三阶多项式在这两方面表现良好,十阶多项式则在数据集分布规律捕捉上表现良好,单同样偏离红色曲线较远。此时一阶多项式实际上就是欠拟合,而十阶多项式则过分捕捉噪声数据的分布规律,而噪声之所以被称作噪声,是因为其分布本身毫无规律可言,或者其分布规律毫无价值(如此处噪声分布为均匀分布),因此就算十阶多项式在当前训练数据集上拟合度很高,但其捕捉到的无用规律无法推广到新的数据集上,因此该模型在测试数据集上执行过程将会有很大误差。即模型训练误差很小,但泛化误差很大。
接下来,我们尝试如何通过在模型中加入正则化项来缓解10阶多项式回归的过拟合倾向。
为了更加符合sklearn的建模风格、从而能够使用sklearn的诸多方法,我们将上述10阶多项式建模转化为一个等价的形式,即在原始数据中衍生出几个特征,分别是 x 2 x^2 x2、 x 3 x^3 x3、…、 x 10 x^{10} x10,然后带入线性回归方程进行建模
x_l = []
for i in range(10):
x_temp = np.power(x, i+1).reshape(-1, 1)
x_l.append(x_temp)
X = np.concatenate(x_l, 1)
X[:2]
#array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
# 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
# 0.00000000e+00, 0.00000000e+00],
# [5.26315789e-02, 2.77008310e-03, 1.45793847e-04, 7.67336039e-06,
# 4.03861073e-07, 2.12558460e-08, 1.11872874e-09, 5.88804597e-11,
# 3.09897157e-12, 1.63103767e-13]])
y
#array([0.03929384, 0.1866436 , 0.26981313, 0.40762266, 0.50272526,
# 0.49761047, 0.65810433, 0.64394293, 0.64507206, 0.66667071,
# 0.69411185, 0.80669585, 0.78243386, 0.73910577, 0.83800393,
# 0.9361224 , 0.85416128, 0.88099565, 0.9796388 , 1.00636552])
当然,上述过程其实也就是比较简单的一种特征衍生方法,该方法也可以通过sklearn中的PolynomialFeatures类来进行实现。
from sklearn.preprocessing import PolynomialFeatures
# 查看帮助文档
PolynomialFeatures?
x.reshape(-1, 1)[:2]
#array([[0. ],
# [0.05263158]])
# 二阶特征衍生
PolynomialFeatures(degree=2).fit_transform(x.reshape(-1, 1))[:2]
#array([[1. , 0. , 0. ],
# [1. , 0.05263158, 0.00277008]])
#原始的x零次方,x一次方,x二次方
# 二阶特征衍生只包含交叉项
PolynomialFeatures(degree=2, interaction_only=True).fit_transform(x.reshape(-1, 1))[:2]
#array([[1. , 0. ],
# [1. , 0.05263158]])
poly = PolynomialFeatures(degree = 10, include_bias=False)
poly.fit_transform(x.reshape(-1, 1))[:2]
#array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
# 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
# 0.00000000e+00, 0.00000000e+00],
# [5.26315789e-02, 2.77008310e-03, 1.45793847e-04, 7.67336039e-06,
# 4.03861073e-07, 2.12558460e-08, 1.11872874e-09, 5.88804597e-11,
# 3.09897157e-12, 1.63103767e-13]])
接下来,围绕特征衍生后的新数据来进行线性回归建模。
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(X, y)
lr.coef_
#array([ 6.26103457e+00, -1.19764265e+02, 1.42603456e+03, -8.87839988e+03,
# 3.20918671e+04, -7.14049022e+04, 9.93100740e+04, -8.41213555e+04,
# 3.96752034e+04, -7.98404881e+03])
# 查看过拟合时MSE
from sklearn.metrics import mean_squared_error
mean_squared_error(lr.predict(X), y)
#0.001172668222879593
# 观察建模结果
t = np.linspace(0, 1, 200)
plt.plot(x, y, 'ro', x, lr.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('10-degree')
接下来,我们尝试在线性回归的损失函数中引入正则化,来缓解10阶特征衍生后的过拟合问题。根据Lesson 3.3的讨论我们知道,在线性回归中加入l2正则化,实际上就是岭回归(Ridge),而加入l1正则化,则变成了Lasso。因此,我们分别考虑围绕上述模型进行岭回归和Lasso的建模。
# 导入岭回归和Lasso
from sklearn.linear_model import Ridge,Lasso
Ridge?
# 参数越多、模型越简单、相同的alpha惩罚力度越大
reg_rid = Ridge(alpha=0.005)
reg_rid.fit(X, y)
#Ridge(alpha=0.005)
reg_rid.coef_
#array([ 1.69951452e+00, -7.27654755e-01, -5.16601900e-01, -9.16814563e-02,
# 1.44069563e-01, 2.10532895e-01, 1.77803630e-01, 9.77891137e-02,
# 9.12868410e-04, -9.69907721e-02])
mean_squared_error(reg_rid.predict(X), y)
#0.0021197020660901986
# 观察惩罚效果
t = np.linspace(0, 1, 200)
plt.subplot(121)
plt.plot(x, y, 'ro', x, reg_rid.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('Ridge(alpha=0.005)')
plt.subplot(122)
plt.plot(x, y, 'ro', x, lr.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('LinearRegression')
不难发现,l2正则化对过拟合倾向有较为明显的抑制作用。接下来尝试Lasso
Lasso?
reg_las = Lasso(alpha=0.001)
reg_las.fit(X, y)
#Lasso(alpha=0.001)
reg_las.coef_
#array([ 1.10845364, -0. , -0.37211179, -0. , -0. ,
# 0. , 0. , 0. , 0. , 0.05080217])
#使用L1可以得到稀疏的权值;用L2可以得到平滑的权值
mean_squared_error(reg_las.predict(X), y)
#0.004002917874293844
t = np.linspace(0, 1, 200)
plt.subplot(121)
plt.plot(x, y, 'ro', x, reg_las.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('Lasso(alpha=0.001)')
plt.subplot(122)
plt.plot(x, y, 'ro', x, lr.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('LinearRegression')
我们发现,Lasso的惩罚力度更强,并且迅速将一些参数清零,而这些被清零的参数,则代表对应的参数在实际建模过程中并不重要,从而达到特种重要性筛选的目的。而在实际的建模过程中,l2正则化往往应用于缓解过拟合趋势,而l1正则化往往被用于特征筛选的场景中。
其实特征重要性和(线性方程中)特征对应系数大小并没有太大的关系,判断特种是否重要的核心还是在于观察抛弃某些特征后,建模结果是否会发生显著影响。
有上述过程,我们不难发现,l2缓解过拟合效果更好(相比l1正则化,l2正则化在参数筛选时过程更容易控制),而l1正则化的运算结果说明,上述10个特征中,第一个、第三个和最后一个特征相对重要。而特征重要的含义,其实是代表哪怕带入上述3个特征建模,依然能够达到带入所有特征建模的效果。我们可以通过下述实验进行验证:
# 挑选特征,构建新的特征矩阵
X_af = X[:, [0, 2, 9]]
lr_af = LinearRegression()
lr_af.fit(X_af, y)
#LinearRegression()
lr_af.coef_
#array([ 1.45261658, -0.93936141, 0.39449483])
mean_squared_error(lr_af.predict(X_af), y)
#0.0027510973386944155
lr_af.predict(X_af)
#array([0.12785414, 0.2041707 , 0.27966553, 0.35351693, 0.42490323,
# 0.49300315, 0.55699719, 0.61607193, 0.66943127, 0.71632121,
# 0.75607951, 0.788227 , 0.8126252 , 0.82973402, 0.84101554,
# 0.84954371, 0.86089679, 0.88442936, 0.93504335, 1.03560415])
t = np.linspace(0, 1, 200)
plt.subplot(121)
plt.plot(x, y, 'ro', x, lr_af.predict(X_af), '-', t, np.sqrt(t), 'r--')
plt.subplot(122)
plt.plot(x, y, 'ro', x, lr.predict(X), '-', t, np.sqrt(t), 'r--')
我们发现,哪怕删掉了70%的特征,最终建模结果仍然还是未收到太大的影响,从侧面也说明剩下70%的特征确实“不太重要”。当然,在删除这些数据之后,模型过拟合的趋势略微有所好转,那如果我们继续加上l2正则化呢?会不会有更好防止过拟合的效果?
# 特征减少,可以适度放大alpha
reg_rid_af = Ridge(alpha=0.05)
reg_rid_af.fit(X_af, y)
#Ridge(alpha=0.05)
reg_rid_af.coef_
#array([ 1.02815296, -0.31070552, 0.06374435])
mean_squared_error(reg_rid_af.predict(X_af), y)
#0.004383156990375146
t = np.linspace(0, 1, 200)
plt.subplot(121)
plt.plot(x, y, 'ro', x, reg_rid_af.predict(X_af), '-', t, np.sqrt(t), 'r--')
plt.title('Ridge_af(alpha=0.05)')
plt.subplot(122)
plt.plot(x, y, 'ro', x, reg_rid.predict(X), '-', t, np.sqrt(t), 'r--')
plt.title('Ridge(alpha=0.005)')
不难发现,模型整体过拟合倾向被更进一步抑制,整体拟合效果较好。
此处虽然重点介绍关于l1正则化和l2正则化对模型过拟合效果抑制的效果,但实际上,从上述过程中,我们其实能够总结一套建模策略:
当然,除此以外,还有一些注意事项:
在补充了关于正则化的相关内容之后,接下来,我们来详细讨论关于逻辑回归的参数解释。
首先,先对上述逻辑回归的说明文档中的内容进行解释。
在了解了正则化的相关内容后,接下来我们观察sklearn官网中给出的逻辑回归加入正则化后的损失函数表达式,我们发现该表达式和此前我们推到的交叉熵损失函数的表达式还是略有差异,核心原因是sklearn在二分类的时候默认两个类别的标签取值为-1和1,而不是0和1。我们曾在Lesson 4.2中进行了非常详细的关于逻辑回归损失函数的推导,此处的数学推导,我们只需将Lesson 4.2中公式当中的y的取值改为-1和1即可,相关推导过程留作课后习练习。
相比原始损失函数,正则化后的损失函数有两处发生了变化,其一是在原损失函数基础上乘以了系数C,其二则是加入了正则化项。其中系数C也是超参数,需要人工输入,用于调整经验风险部分和结构风险部分的权重,C越大,经验风险部分权重越大,反之结构风险部分权重越大。此外,在 l 2 l2 l2正则化时,采用的 w T w 2 \frac{w^Tw}{2} 2wTw表达式,其实相当于是各参数的平方和除以2,在求最小值时本质上和w的2-范数起到的作用相同,省去开平方是为了简化运算,而除以2则是为了方便后续求导运算,和2次方结果相消。
另外,sklearn中还提供了弹性网正则化方法,其实是通过 ρ \rho ρ控制 l 1 l1 l1正则化和 l 2 l2 l2正则化惩罚力度的权重,是一个更加综合的解决方案。不过代价是增加了一个超参数 ρ \rho ρ,并且由于损失函数形态发生了变化,导致部分优化方法无法使用。
在上述的一系列基础内容铺垫之后,接下来我们对逻辑回归评估器中的参数进行详细解释:
LogisticRegression?
#Init signature:
#LogisticRegression(
# penalty='l2',
# *,
# dual=False,
# tol=0.0001,
# C=1.0,
# fit_intercept=True,
# intercept_scaling=1,
# class_weight=None,
# random_state=None,
# solver='lbfgs',
# max_iter=100,
# multi_class='auto',
# verbose=0,
# warm_start=False,
# n_jobs=None,
# l1_ratio=None,
)
对偶问题是约束条件相反、求解方向也相反的问题,当数据集过小而特征较多时,求解对偶问题能一定程度降低运算复杂度,其他情况建议保留默认参数取值。
class_weight其实代表各类样本在进行损失函数计算时的数值权重,例如假设一个二分类问题,0、1两类的样本比例是2:1,此时可以输入一个字典类型对象用于说明两类样本在进行损失值计算时的权重,例如输入:{0:1, 1:3},则代表1类样本的每一条数据在进行损失函数值的计算时都会在原始数值上*3。而当我们将该参数选为balanced
时,则会自动将这个比例调整为真实样本比例的反比,以达到平衡的效果。
其实除了最小二乘法和梯度下降以外,还有非常多的关于损失函数的求解方法,而选择损失函数的参数,就是solver参数。
而当前损失函数到底采用何种优化方法进行求解,其实最终目的是希望能够更快(计算效率更高)更好(准确性更高)的来进行求解,而硬性的约束条件是损失函数的形态,此外则是用户自行选择的空间。
我们很难一次性将所有的优化方法一次性介绍完整,但我们可以给出何时应该用哪种solver的参考列表,也就是官网给出的列表:
逻辑回归可选的优化方法包括:
对于逻辑回归来说,求解损失函数的硬性约束其实就是多分类问题时采用的策略以及加入的惩罚项,所以大多数情况,我们会优先根据多分类问题的策略及惩项来选取优化算法,其次,如果有多个算法可选,那么我们可以根据其他情况来进行求解器的选取,如:
multi_class:选用何种方法进行多分类问题求解
可选OVR和MVM,当然默认情况是auto,此时模型会优先根据惩罚项和solver选择OVR还是MVM,但一般来说,MVM效果会好于OVR。
至此,我们即完成逻辑回归所有参数的解释,当然对于这些参数更深层次的理解,则需要长期的积淀、以及其他知识的补充。下一小节,也是Lesson 6的最后一小节,我们将利用sklearn中的逻辑回归、结合此前介绍的正则化相关内容,来进行一次综合调参练习。深化正则化方法使用情况的理解、同时进一步介绍sklearn中另一个至关重要的方法——网格搜索。
什么样的分类策略、什么样的正则项会影响到损失函数构造,会进一步影响到用什么solver
(1)根据behavior选择solvor
(2)选择正则化项
(3)根据(1)(2)选择分类策略
在补充了一系列关于正则化的基础理论以及sklearn中逻辑回归评估器的参数解释之后,接下来,我们尝试借助sklearn中的逻辑回归评估器,来执行包含特征衍生和正则化过程的建模试验,同时探索模型经验风险和结构风险之间的关系。一方面巩固此前介绍的相关内容,同时也进一步加深对于Pipeline的理解并熟练对其的使用。当然更关键的一点,本节的实验将为下一小节的网格搜索调参做铺垫,并在后续(Lesson 6.6)借助网格搜索工具,给出更加完整、更加自动化、并且效果更好的调参策略。
# 科学计算模块
import numpy as np
import pandas as pd
# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt
# 自定义模块
from ML_basic_function import *
# Scikit-Learn相关模块
# 评估器类
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
# 实用函数
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
首先需要进行数据准备。为了更好的配合进行模型性能与各种方法效果的测试,此处先以手动创建数据集为例进行试验。
在Lesson 5.1的阅读部分内容中,我们曾介绍到关于逻辑回归的决策边界实际上就是逻辑回归的线性方程这一特性,并由此探讨了一元函数与二维平面的决策边界之间的关系,据此我们可以创建一个满足分类边界为 y 2 = − x + 1.5 y^2=-x+1.5 y2=−x+1.5的分布,创建方法如下:
np.random.seed(24)
X = np.random.normal(0, 1, size=(1000, 2))
y = np.array(X[:,0]+X[:, 1]**2 < 1.5, int)
plt.scatter(X[:, 0], X[:, 1], c=y)
此时边界为 y 2 = − x + 1.5 y^2=-x+1.5 y2=−x+1.5,而选取分类边界的哪一侧为正类哪一侧为负类(即不等号的方向),其实并不影响后续模型建模。而利用分类边界来划分数据类别,其实也是一种为这个分类数据集赋予一定规律的做法。
同样,为了更好地贴近真实情况,我们在上述分类边界的规律上再人为增加一些扰动项,即让两个类别的分类边界不是那么清晰,具体方法如下:
np.random.seed(24)
for i in range(200):
y[np.random.randint(1000)] = 1
y[np.random.randint(1000)] = 0
plt.scatter(X[:, 0], X[:, 1], c=y)
当然,如果要增加分类难度,可以选取更多的点随机赋予类别。在整体数据准备完毕后,接下来进行数据集的切分:
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state = 42)
至此,数据准备工作全部完成。
接下来,调用逻辑回归中的相关类,来进行模型构建。很明显,面对上述曲线边界的问题,通过简单的逻辑回归无法达到较好的预测效果,因此需要借助此前介绍的PolynomialFeatures来进行特征衍生,或许能够提升模型表现,此外我们还需要对数据进行标准化处理,以训练过程稳定性及模型训练效率,当然我们还可以通过Pipeline将这些过程封装在一个机器学习流中,以简化调用过程。并且我们知道,整个建模过程我们需要测试在不同强度的数据衍生下,模型是否会出现过拟合倾向,同时如果出现过拟合之后应该如何调整。因此我们可以将上述Pipeline封装在一个函数中,通过该函数我们可以非常便捷进行核心参数的设置,同时也能够重复实例化不同的评估器以支持重复试验。
def plr(degree=1, penalty='none', C=1.0):
pipe = make_pipeline(PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
LogisticRegression(penalty=penalty, tol=1e-4, C=C, max_iter=int(1e6)))
return pipe
其中,和数据增强的强度相关的参数是degree,决定了衍生特征的最高阶数,而penalty、C则是逻辑回归中控制正则化及惩罚力度的相关参数,该组参数能够很好的控制模型对于训练数据规律的挖掘程度,当然,最终的建模目标是希望构建一个很好挖掘全局规律的模型,即一方面我们希望模型尽可能挖掘数据规律,另一方面我们又不希望模型过拟合。
其中,和数据增强的强度相关的参数是degree,决定了衍生特征的最高阶数,而penalty、C则是逻辑回归中控制正则化及惩罚力度的相关参数,该组参数能够很好的控制模型对于训练数据规律的挖掘程度,当然,最终的建模目标是希望构建一个很好挖掘全局规律的模型,即一方面我们希望模型尽可能挖掘数据规律,另一方面我们又不希望模型过拟合。
上述过程有两点需要注意:首先,复杂模型的建模往往会有非常多的参数需要考虑,但一般来说我们会优先考虑影响最终建模效果的参数(如影响模型欠拟合、过拟合的参数),然后再考虑影响训练过程的参数(如调用几核心进行计算、采用何种迭代求解方法等),前者往往是需要调整的核心参数;其次,上述实例化逻辑回归模型时,我们适当提高了最大迭代次数,这是一般复杂数据建模时都需要调整的参数。
合理的设置调参范围,是调好参数的第一步。
接下来进行模型训练,并且尝试进行手动调参来控制模型拟合度。
pl1 = plr()
当然,函数接口只给了部分核心参数,但如果想调整更多的模型参数,还可以通过使用此前介绍的set_params方法来进行调整:
pl1.get_params()
# {'memory': None,
# 'steps': [('polynomialfeatures',
# PolynomialFeatures(degree=1, include_bias=False)),
# ('standardscaler', StandardScaler()),
# ('logisticregression',
# LogisticRegression(max_iter=1000000, penalty='none'))],
# 'verbose': False,
# 'polynomialfeatures': PolynomialFeatures(degree=1, include_bias=False),
# 'standardscaler': StandardScaler(),
# 'logisticregression': LogisticRegression(max_iter=1000000, penalty='none'),
# 'polynomialfeatures__degree': 1,
# 'polynomialfeatures__include_bias': False,
# 'polynomialfeatures__interaction_only': False,
# 'polynomialfeatures__order': 'C',
# 'standardscaler__copy': True,
# 'standardscaler__with_mean': True,
# 'standardscaler__with_std': True,
# 'logisticregression__C': 1.0,
# 'logisticregression__class_weight': None,
# 'logisticregression__dual': False,
# 'logisticregression__fit_intercept': True,
# 'logisticregression__intercept_scaling': 1,
# 'logisticregression__l1_ratio': None,
# 'logisticregression__max_iter': 1000000,
# 'logisticregression__multi_class': 'auto',
# 'logisticregression__n_jobs': None,
# 'logisticregression__penalty': 'none',
# 'logisticregression__random_state': None,
# 'logisticregression__solver': 'lbfgs',
# 'logisticregression__tol': 0.0001,
# 'logisticregression__verbose': 0,
# 'logisticregression__warm_start': False}
pl1.get_params()['polynomialfeatures__include_bias']
#False
# 调整PolynomialFeatures评估器中的include_bias参数
pl1.set_params(polynomialfeatures__include_bias=True)
#Pipeline(steps=[('polynomialfeatures', PolynomialFeatures(degree=1)),
# ('standardscaler', StandardScaler()),
# ('logisticregression',
# LogisticRegression(max_iter=1000000, penalty='none'))])
pl1.get_params()['polynomialfeatures__include_bias']
#True
接下来测试模型性能,首先是不进行特征衍生时的逻辑回归建模结果:
pr1 = plr()
pr1.fit(X_train, y_train)
pr1.score(X_train, y_train),pr1.score(X_test, y_test)
#(0.6985714285714286, 0.7066666666666667)
我们发现,模型整体拟合效果并不好,我们可以借助此前定义的决策边界来进行一个更加直观的模型建模结果的观察。当然,由于此时我们是调用sklearn的模型,因此需要在此前的决策边界绘制函数基础上略微进行修改:
def plot_decision_boundary(X, y, model):
"""
决策边界绘制函数
"""
# 以两个特征的极值+1/-1作为边界,并在其中添加1000个点
x1, x2 = np.meshgrid(np.linspace(X[:, 0].min()-1, X[:, 0].max()+1, 1000).reshape(-1,1),
np.linspace(X[:, 1].min()-1, X[:, 1].max()+1, 1000).reshape(-1,1))
# 将所有点的横纵坐标转化成二维数组
X_temp = np.concatenate([x1.reshape(-1, 1), x2.reshape(-1, 1)], 1)
# 对所有点进行模型类别预测
yhat_temp = model.predict(X_temp)
yhat = yhat_temp.reshape(x1.shape)
# 绘制决策边界图像
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(['#EF9A9A','#90CAF9'])
plt.contourf(x1, x2, yhat, cmap=custom_cmap)
plt.scatter(X[(y == 0).flatten(), 0], X[(y == 0).flatten(), 1], color='red')
plt.scatter(X[(y == 1).flatten(), 0], X[(y == 1).flatten(), 1], color='blue')
# 测试函数性能
plot_decision_boundary(X, y, pr1)
不难看出,逻辑回归在不进行数据衍生的情况下,只能捕捉线性边界,当然这也是模型目前性能欠佳的核心原因。当然,我们尝试衍生2次项特征再来进行建模:
pr2 = plr(degree=2)
pr2.fit(X_train, y_train)
pr2.score(X_train, y_train),pr2.score(X_test, y_test)
#(0.7914285714285715, 0.7866666666666666)
plot_decision_boundary(X, y, pr2)
能够发现,模型效果有了明显提升,这里首先我们可以通过训练完的逻辑回归模型参数个数来验证当前数据特征数量:
pr2.named_steps
#{'polynomialfeatures': PolynomialFeatures(include_bias=False),
# 'standardscaler': StandardScaler(),
# 'logisticregression': LogisticRegression(max_iter=1000000, penalty='none')}
pr2.named_steps['logisticregression'].coef_
#array([[-0.81012988, 0.04384694, -0.48583038, 0.02977868, -1.12352417]])
此处我们可以通过Pipeline中的named_steps来单独调用机器学习流中的某个评估器,从而能够进一步查看该评估器的相关属性,named_steps返回结果同样也是一个字典,通过key来调用对应评估器。当然该字典中的key名称其实是对应评估器类的函数(如果有的话)。最后查看模型总共5个参数,对应训练数据总共5个特征,说明最高次方为二次方、并且存在交叉项目的特征衍生顺利执行。(当前5个特征为 x 1 x_1 x1、 x 1 2 x_1^2 x12、 x 2 x_2 x2、 x 2 2 x_2^2 x22、 x 1 x 2 x_1x_2 x1x2)
而模型在进行特征衍生之后为何会出现一个类似圆形的边界?其实当我们在进行特征衍生的时候,就相当于是将原始数据集投射到一个高维空间,而在高维空间内的逻辑回归模型,实际上是构建了一个高维空间内的超平面(高维空间的“线性边界”)在进行类别划分。而我们现在看到的原始特征空间的决策边界,实际上就是高维空间的决策超平面在当前特征空间的投影。而由此我们也知道了特征衍生至于逻辑回归模型效果提升的实际作用,就是突破了逻辑回归在原始特征空间中的线性边界的束缚。而经过特征衍生的逻辑回归模型,也将在原始特征空间中呈现出非线性的决策边界的特性。
需要知道的是,尽管这种特征的衍生看起来很强大,能够帮逻辑回归在原始特征空间中构建非线性的决策边界,但同时需要知道的是,这种非线性边界其实也是受到特征衍生方式的约束的,无论是几阶的特征衍生,能够投射到的高维空间都是有限的,而我们最终也只能在这些有限的高维空间中寻找一个最优的超平面。
当然,我们可以进一步进行10阶特征的衍生,然后构建一个更加复杂的模型:
pr3 = plr(degree=10)
pr3.fit(X_train, y_train)
pr3.score(X_train, y_train),pr3.score(X_test, y_test)
#(0.8314285714285714, 0.78)
如果在运行过程中显示上述警告信息,首先需要知道的是警告并不影响最终模型结果的使用,其次,上述警告信息其实是很多进行数值解求解过程都会面临的典型问题,就是迭代次数(max_iter)用尽,但并没有收敛到tol参数设置的区间。解决该问题一般有三种办法,其一是增加max_iter迭代次数,其二就是增加收敛区间,其三则是加入正则化项。加入正则化项的相关方法我们稍后尝试,而对于前两种方法来说,一般来说,如果我们希望结果更加稳定、更加具有可信度,则可以考虑增加迭代次数而保持一个较小的收敛区间,但此处由于我们本身只设置了1000条数据,较小的数据量是目前无法收敛止较小区间的根本原因,因此此处建议稍微扩大收敛区间以解决上述问题。
其实我们还可以通过更换迭代方法来解决上述问题,但限于逻辑回归模型的特殊性,此处不建议更换迭代方法。
而要求改tol参数,则可以使用前面介绍的set_param方法来进行修改:
pr3.get_params()
#{'memory': None,
# 'steps': [('polynomialfeatures',
# PolynomialFeatures(degree=10, include_bias=False)),
# ('standardscaler', StandardScaler()),
# ('logisticregression',
# LogisticRegression(max_iter=1000000, penalty='none'))],
# 'verbose': False,
# 'polynomialfeatures': PolynomialFeatures(degree=10, include_bias=False),
# 'standardscaler': StandardScaler(),
# 'logisticregression': LogisticRegression(max_iter=1000000, penalty='none'),
# 'polynomialfeatures__degree': 10,
# 'polynomialfeatures__include_bias': False,
# 'polynomialfeatures__interaction_only': False,
# 'polynomialfeatures__order': 'C',
# 'standardscaler__copy': True,
# 'standardscaler__with_mean': True,
# 'standardscaler__with_std': True,
# 'logisticregression__C': 1.0,
# 'logisticregression__class_weight': None,
# 'logisticregression__dual': False,
# 'logisticregression__fit_intercept': True,
# 'logisticregression__intercept_scaling': 1,
# 'logisticregression__l1_ratio': None,
# 'logisticregression__max_iter': 1000000,
# 'logisticregression__multi_class': 'auto',
# 'logisticregression__n_jobs': None,
# 'logisticregression__penalty': 'none',
# 'logisticregression__random_state': None,
# 'logisticregression__solver': 'lbfgs',
# 'logisticregression__tol': 0.0001,
# 'logisticregression__verbose': 0,
# 'logisticregression__warm_start': False}
pr3 = plr(degree=10)
pr3.set_params(logisticregression__tol=1e-2)
#Pipeline(steps=[('polynomialfeatures',
# PolynomialFeatures(degree=10, include_bias=False)),
# ('standardscaler', StandardScaler()),
# ('logisticregression',
# LogisticRegression(max_iter=1000000, penalty='none',
# tol=0.01))])
pr3.fit(X_train, y_train)
pr3.score(X_train, y_train),pr3.score(X_test, y_test)
#(0.8314285714285714, 0.79)
不过,因为放宽了收敛条件,最后收敛结果也会略受影响,在参数设置时需要谨慎选择。此外,我们可以绘制pr3中逻辑回归的决策边界,不难看出,模型已呈现出过拟合倾向。
plot_decision_boundary(X, y, pr3)
在基本验证上述代码执行过程无误之后,接下来我们可以尝试通过衍生更高阶特征来提高模型复杂度,并观察在提高模型复杂度的过程中训练误差和测试误差是如何变化的。
# 用于存储不同模型训练准确率与测试准确率的列表
score_l = []
# 实例化多组模型,测试模型效果
for degree in range(1, 21):
pr_temp = plr(degree=degree)
pr_temp.fit(X_train, y_train)
score_temp = [pr_temp.score(X_train, y_train),pr_temp.score(X_test, y_test)]
score_l.append(score_temp)
np.array(score_l)
#array([[0.69857143, 0.70666667],
# [0.79142857, 0.78666667],
# [0.79428571, 0.78333333],
# [0.79428571, 0.77666667],
# [0.80285714, 0.79 ],
# [0.8 , 0.78333333],
# [0.83142857, 0.77 ],
# [0.83 , 0.77666667],
# [0.83142857, 0.78 ],
# [0.83857143, 0.78 ],
# [0.83714286, 0.78666667],
# [0.84428571, 0.79333333],
# [0.84571429, 0.79 ],
# [0.84428571, 0.79333333],
# [0.84857143, 0.79333333],
# [0.84857143, 0.78666667],
# [0.85 , 0.79333333],
# [0.84857143, 0.78333333],
# [0.85142857, 0.78333333]])
plt.plot(list(range(1, 21)), np.array(score_l)[:,0], label='train_acc')
plt.plot(list(range(1, 21)), np.array(score_l)[:,1], label='test_acc')
plt.legend(loc = 4)
最终,我们能够较为明显的看出,伴随着模型越来越复杂(特征越来越多),训练集准确率逐渐提升,但测试集准确率却在一段时间后开始下降,说明模型经历了由开始的欠拟合到拟合再到过拟合的过程,和上一小节介绍的模型结构风险伴随模型复杂度提升而提升的结论一致。
而接下来的问题就是,如何同时控制结构风险和经验风险。根据上一小节的介绍,采用正则化将是一个不错的选择,当然我们也可以直接从上图中的曲线变化情况来挑选最佳的特征衍生个数。接下来,我们尝试对上述模型进行手动调参。
根据上一小节的介绍,我们知道,对于过拟合,我们可以通过l1或l2正则化来抑制过拟合影响,并且从上一小节我们得知,一个比较好的建模流程是先进行数据增强(特征衍生),来提升模型表现,然后再通过正则化的方式来抑制过拟合倾向。接下来,我们就上述问题来进行相关尝试
# 测试l1正则化
pl1 = plr(degree=10, penalty='l1', C=1.0)
pl1.fit(X_train, y_train)
pl1.set_params(logisticregression__solver='saga')
# Pipeline(steps=[('polynomialfeatures',
# PolynomialFeatures(degree=10, include_bias=False)),
# ('standardscaler', StandardScaler()),
# ('logisticregression',
# LogisticRegression(max_iter=1000000, penalty='l1',
# solver='saga'))])
pl1.fit(X_train, y_train)
pl1.score(X_train, y_train),pl1.score(X_test, y_test)
#(0.7914285714285715, 0.7833333333333333)
# 测试l2正则化
pl2 = plr(degree=10, penalty='l2', C=1.0).fit(X_train, y_train)
pl2.score(X_train, y_train),pl2.score(X_test, y_test)
#(0.8071428571428572, 0.79)
pr3.score(X_train, y_train),pr3.score(X_test, y_test)
#(0.8314285714285714, 0.79)
plot_decision_boundary(X, y, pr3)
plot_decision_boundary(X, y, pl1)
plot_decision_boundary(X, y, pl2)
尽管从决策边界上观察并不明显,但从最终建模结果来看,正则化确实起到了抑制过拟合的效果。接下来我们尝试手动对上述模型进行调参,尝试能否提高模型表现。此处我们采用一个非常朴素的想法来进行调参,即使用degree、C和正则化选项(l1或l2)的不同组合来进行调参,试图从中选择一组能够让模型表现最好的参数,并且先从degree开始进行搜索:
# 用于存储不同模型训练准确率与测试准确率的列表
score_l1 = []
# 实例化多组模型,测试模型效果
for degree in range(1, 21):
pr_temp = plr(degree=degree, penalty='l1')
pr_temp.set_params(logisticregression__solver='saga')
pr_temp.fit(X_train, y_train)
score_temp = [pr_temp.score(X_train, y_train),pr_temp.score(X_test, y_test)]
score_l1.append(score_temp)
# 观察最终结果
plt.plot(list(range(1, 21)), np.array(score_l1)[:,0], label='train_acc')
plt.plot(list(range(1, 21)), np.array(score_l1)[:,1], label='test_acc')
plt.legend(loc = 4)
score_l1
#[[0.7, 0.7133333333333334],
# [0.79, 0.79],
# [0.7885714285714286, 0.7933333333333333],
# [0.7914285714285715, 0.7933333333333333],
# [0.7885714285714286, 0.7833333333333333],
# [0.7928571428571428, 0.7833333333333333],
# [0.7957142857142857, 0.7766666666666666],
# [0.7914285714285715, 0.7833333333333333],
# [0.7957142857142857, 0.78],
# [0.7914285714285715, 0.7833333333333333],
# [0.7914285714285715, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.79],
# [0.7871428571428571, 0.79],
# [0.7871428571428571, 0.79],
# [0.7871428571428571, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.7933333333333333]]
此处我们选取3阶为下一步搜索参数时确定的degree参数取值。
# 用于存储不同模型训练准确率与测试准确率的列表
score_l2 = []
# 实例化多组模型,测试模型效果
for degree in range(1, 21):
pr_temp = plr(degree=degree, penalty='l2')
pr_temp.fit(X_train, y_train)
score_temp = [pr_temp.score(X_train, y_train),pr_temp.score(X_test, y_test)]
score_l2.append(score_temp)
# 观察最终结果
plt.plot(list(range(1, 21)), np.array(score_l2)[:,0], label='train_acc')
plt.plot(list(range(1, 21)), np.array(score_l2)[:,1], label='test_acc')
plt.legend(loc = 4)
score_l2
# [[0.7, 0.7066666666666667],
# [0.79, 0.7966666666666666],
# [0.79, 0.7833333333333333],
# [0.7914285714285715, 0.79],
# [0.7914285714285715, 0.78],
# [0.7985714285714286, 0.7933333333333333],
# [0.7971428571428572, 0.7833333333333333],
# [0.8, 0.7866666666666666],
# [0.8057142857142857, 0.79],
# [0.8071428571428572, 0.79],
# [0.8085714285714286, 0.79],
# [0.8071428571428572, 0.7933333333333333],
# [0.81, 0.7933333333333333],
# [0.81, 0.7966666666666666],
# [0.8128571428571428, 0.7966666666666666],
# [0.8114285714285714, 0.7966666666666666],
# [0.8128571428571428, 0.7966666666666666],
# [0.8128571428571428, 0.7966666666666666],
# [0.8128571428571428, 0.7966666666666666],
# [0.8128571428571428, 0.7966666666666666]]
此处我们选取15阶为下一步搜索参数时确定的degree参数取值。接下来继续搜索C的取值:
# 用于存储不同模型训练准确率与测试准确率的列表
score_l1_3 = []
# 实例化多组模型,测试模型效果
for C in np.arange(0.5, 2, 0.1):
pr_temp = plr(degree=3, penalty='l1', C=C)
pr_temp.set_params(logisticregression__solver='saga')
pr_temp.fit(X_train, y_train)
score_temp = [pr_temp.score(X_train, y_train),pr_temp.score(X_test, y_test)]
score_l1_3.append(score_temp)
# 观察最终结果
plt.plot(list(np.arange(0.5, 2, 0.1)), np.array(score_l1_3)[:,0], label='train_acc')
plt.plot(list(np.arange(0.5, 2, 0.1)), np.array(score_l1_3)[:,1], label='test_acc')
plt.legend(loc = 4)
score_l1_3
#[[0.7871428571428571, 0.7966666666666666],
# [0.7885714285714286, 0.7966666666666666],
# [0.7885714285714286, 0.7966666666666666],
# [0.7885714285714286, 0.7966666666666666],
# [0.7885714285714286, 0.7966666666666666],
# [0.7885714285714286, 0.7933333333333333],
# [0.7885714285714286, 0.7933333333333333],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.79],
# [0.7885714285714286, 0.7866666666666666],
# [0.79, 0.7866666666666666],
# [0.7914285714285715, 0.7866666666666666],
# [0.7914285714285715, 0.7866666666666666],
# [0.7928571428571428, 0.7866666666666666]]
# 用于存储不同模型训练准确率与测试准确率的列表
score_l2_15 = []
# 实例化多组模型,测试模型效果
for C in np.arange(0.5, 2, 0.1):
pr_temp = plr(degree=15, penalty='l2', C=C)
pr_temp.fit(X_train, y_train)
score_temp = [pr_temp.score(X_train, y_train),pr_temp.score(X_test, y_test)]
score_l2_15.append(score_temp)
# 观察最终结果
plt.plot(list(np.arange(0.5, 2, 0.1)), np.array(score_l2_15)[:,0], label='train_acc')
plt.plot(list(np.arange(0.5, 2, 0.1)), np.array(score_l2_15)[:,1], label='test_acc')
plt.legend(loc = 4)
score_l2_15score_l2_15
# [[0.8057142857142857, 0.7866666666666666],
# [0.8071428571428572, 0.7866666666666666],
# [0.8057142857142857, 0.7866666666666666],
# [0.8071428571428572, 0.7866666666666666],
# [0.81, 0.79],
# [0.8128571428571428, 0.7966666666666666],
# [0.8114285714285714, 0.7966666666666666],
# [0.8128571428571428, 0.8],
# [0.8142857142857143, 0.8],
# [0.8157142857142857, 0.8],
# [0.8142857142857143, 0.8],
# [0.8142857142857143, 0.8],
# [0.8142857142857143, 0.8],
# [0.8128571428571428, 0.8],
# [0.8128571428571428, 0.8]]
最终,我们通过蛮力搜索,确定了一组能够让测试集准确率取得最大值的参数组合:degree=15, penalty=‘l2’, C=1.0,此时测试集准确率为0.8。
尽管上述过程能够帮助我们最终找到一组相对比较好的参数组合,最终建模结果相比此前,也略有提升,但上述手动调参过程存在三个致命问题:
(1).过程不够严谨,诸如测试集中测试结果不能指导建模、参数选取及搜索区间选取没有理论依据等问题仍然存在;
(2).执行效率太低,如果面对更多的参数(这是更一般的情况),手动执行过程效率太低,无法进行超大规模的参数挑选;
(3).结果不够精确,一次建模结果本身可信度其实并不高,我们很难证明上述挑选出来的参数就一定在未来数据预测中拥有较高准确率。
而要解决这些问题,我们就需要补充关于机器学习调参的理论基础,以及掌握更多更高效的调参工具。正因如此,我们将在下一小节详细介绍关于机器学习调参的基本理论以及sklearn中的网格搜索调参工具,而后我们再借助更完整的理论、更高效的工具对上述问题进行解决。