文章目录
- 在了解了 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’ 一项,而正则化也是机器学习中非常通用的一项操作。
- 接下来,我们就正则化的相关内容展开讨论。
1. 正则化(Regularization)的基本概念
从说明文档中得知,就是在 sklearn 中,逻辑回归模型是默认进行正则化的,即上文所述 “Regularization is applied by default”,这是一种在机器学习建模过程中常见的用法,但并非统计学常用方法。
据此我们也知道了统计学和机器学习方法之间的又一个区别,并且能够清楚的感受到 sklearn 是一个非常“机器学习”的算法库,很多时候会从便于机器学习建模的角度出发对算法进行微调。
这也是 sklearn 算法库的一大特性,这个特性在导致其非常易用的同时,也使得其很多算法和原始提出的算法会存在略微的区别,这点也是初学者需要注意的。
什么是正则化/如何进行正则化
为何需要正则化
正则化的过程比不复杂,但何时需要进行正则化呢?
一般来说,正则化核心的作用是缓解模型过拟合倾向。
此外,由于加入正则化项后损失函数的形体发生了变化,因此也会影响损失函数的求解过程,在某些时候,加入了正则化项之后会让损失函数的求解变得更加高效。
如此前介绍的岭回归,其实就是在线性回归的损失函数基础上加入了 w 的 1- 范数,而 Lasso 则是加入了 w 的 2- 范数。并且,对于逻辑回归来说,如果加入 l 2 l2l2 正则化项,损失函数就会变成严格的凸函数。
经验风险与结构风险
要讨论正则化是如何缓解过拟合倾向的问题,需要引入两个非常重要的概念:经验风险和结构风险。
在我们构建损失函数求最小值的过程,其实就是依据以往经验(也就是训练数据)追求风险最小(以往数据误差最小)的过程,而在给定一组参数后计算得出的损失函数的损失值,其实就是经验风险。
而所谓结构风险,我们可以将其等价为模型复杂程度,模型越复杂,模型结构风险就越大。而正则化后的损失函数在进行最小值求解的过程中,其实是希望损失函数本身和正则化项都取得较小的值,即模型的经验风险和结构风险能够同时得到控制。
模型的经验风险需要被控制不难理解,因为我们希望模型能够尽可能的捕捉原始数据中的规律,但为何模型的结构风险也需要被控制?
核心原因在于,尽管在一定范围内模型复杂度增加能够有效提升模型性能,但模型过于复杂可能会导致另一个非常常见的问题——模型过拟合,但总的来说,一旦模型过拟合了,尽管模型经验风险在降低、但模型的泛化能力会下降。
因此,为了控制模型过拟合倾向,我们可以把模型结构风险纳入损失函数中一并考虑,当模型结构风险的增速高于损失值降低的收益时,我们就需要停止参数训练(迭代)。
- 同时要求模型性能和模型复杂度都在一个合理的范围内,其实等价于希望训练得到一个较小的模型同时具有较好的解释数据的能力(规律捕捉能力),这也符合奥卡姆剃刀原则。
2. 过拟合概念介绍
此前我们曾深入探讨过关于机器学习建模有效性的问题,彼时我们得出的结论是当训练数据和新数据具有规律的一致性时,才能够进行建模,而只有挖掘出贯穿始终的规律(同时影响训练数据和新数据的规律),模型才能够进行有效预测。
不过,既然有些贯穿始终的全局规律,那就肯定存在一些只影响了一部分数据的局部规律。一般来说,由于全局规律影响数据较多,因此更容易被挖掘。
而局部规律只影响部分数据,因此更难被挖掘,因此从较为宽泛的角度来看,但伴随着模型性能提升,也是能够捕获很多局部规律的。
但是需要知道的是,局部规律对于新数据的预测并不能起到正面的作用,反而会影响预测结果,此时就出现模型过拟合现象。我们可以通过如下实例进行说明:
# 设计随机数种子 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^其中 r 是人为制造的随机噪声,在 [-0.1,0.1] 之间服从均匀分布。
然后我们借助 numpy 的 polyfit 函数来进行多项式拟合,polyfit 函数会根据设置的多项式阶数,在给定数据的基础上利用最小二乘法进行拟合,并返回拟合后各阶系数。
同时,当系数计算完成后,我们还常用 ploy1d 函数逆向构造多项式方程,进而利用方程求解 y。
例如人为制造一个二阶多项式方程然后进行二阶拟合实验
y0 = x ** 2 np.polyfit(x, y0, 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)
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 阶多项式拟合的时候蓝色拟合曲线即无法捕捉数据集的分布规律,离数据集背后客观规律也很远,而三阶多项式在这两方面表现良好,十阶多项式则在数据集分布规律捕捉上表现良好,单同样偏离红色曲线较远。
此时一阶多项式实际上就是欠拟合,而十阶多项式则过分捕捉噪声数据的分布规律,而噪声之所以被称作噪声,是因为其分布本身毫无规律可言,或者其分布规律毫无价值(如此处噪声分布为均匀分布)。
因此就算十阶多项式在当前训练数据集上拟合度很高,但其捕捉到的无用规律无法推广到新的数据集上,因此该模型在测试数据集上执行过程将会有很大误差。即模型训练误差很小,但泛化误差很大。
3. 正则化进行特征筛选与缓解过拟合倾向
接下来,我们尝试如何通过在模型中加入正则化项来缓解 10 阶多项式回归的过拟合倾向。
为了更加符合 sklearn 的建模风格、从而能够使用 sklearn 的诸多方法,我们将上述 10 阶多项式建模转化为一个等价的形式,即在原始数据中衍生出几个特征,分别是x3.x2......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?
对其参数进行解释:
Name | Description |
degree | 最高阶数 |
interaction_only | 是否只包含交叉项,交叉项指的是不同特征的乘结果 |
include_bias | 是否只包含0阶计算结果、偏置项 |
计算模式 | 默认C模式,F模式能提高单独评估器计算效率,但会影响机器学习流中其他评估器 |
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]]) # 二阶特征衍生只包含交叉项 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 的讨论我们知道,在线性回归中加入 l 2 l2l2 正则化,实际上就是岭回归(Ridge),而加入 l 1 l1l1 正则化,则变成了 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')
不难发现,l 2 l2l2 正则化对过拟合倾向有较为明显的抑制作用。接下来尝试 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]) 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 的惩罚力度更强,并且迅速将一些参数清零,而这些被清零的参数,则代表对应的参数在实际建模过程中并不重要,从而达到特种重要性筛选的目的。
而在实际的建模过程中,l 2 l2l2 正则化往往应用于缓解过拟合趋势,而 l 1 l1l1 正则化往往被用于特征筛选的场景中。
其实特征重要性和(线性方程中)特征对应系数大小并没有太大的关系,判断特种是否重要的核心还是在于观察抛弃某些特征后,建模结果是否会发生显著影响。
有上述过程,我们不难发现,l 2 l2l2 缓解过拟合效果更好(相比 l 1 l1l1 正则化,l 2 l2l2 正则化在参数筛选时过程更容易控制),而 l 1 l1l1 正则化的运算结果说明,上述 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% 的特征确实“不太重要”。
当然,在删除这些数据之后,模型过拟合的趋势略微有所好转,那如果我们继续加上 l 2 l2l2 正则化呢?会不会有更好防止过拟合的效果?
# 特征减少,可以适度放大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)')
不难发现,模型整体过拟合倾向被更进一步抑制,整体拟合效果较好。
此处虽然重点介绍关于 l 1 l1l1 正则化和 l 2 l2l2 正则化对模型过拟合效果抑制的效果,但实际上,从上述过程中,我们其实能够总结一套建模策略:
(1) 当模型效果(往往是线性模型)不佳时,可以考虑通过特征衍生的方式来进行数据的“增强”;
(2) 如果出现过拟合趋势,则首先可以考虑进行不重要特征的筛选,过多的无关特征其实也会影响模型对于全局规律的判断,当然此时可以考虑使用 l 1 l1l1 正则化配合线性方程进行特征重要性筛选,剔除不重要的特征,保留重要特征;
(3) 对于过拟合趋势的抑制,仅仅踢出不重要特征还是不够的,对于线性方程类的模型来说,l 2 l2l2 正则化则是缓解过拟合非常好的方法,配合特征筛选,能够快速的缓解模型过拟合倾向;
当然,除此以外,还有一些注意事项:
(1) 首先,哪怕不进行特征筛选,l 2 l2l2 正则化也可以帮助线性方程抑制过拟合,但特征太多其实会影响 l 2 l2l2 正则化的参数取值范围,进而影响 alpha 参数惩罚力度的有效性;
(2) 其次,上述参数的选取和过拟合倾向的判断,其实还是主观判断成分较多,一个更加严谨的流程是,先进行数据集的划分,然后选取更能表示模型泛化能力的评估指标,然后将特征提取(如果要做的话)、l 2 l2l2 正则化后的线性方程组成一个 Pipeline,再利用网格搜索,确定一组最优的参数组合。
(3) 最后,需要强调的是,并非所有模型都需要/可以通过正则化来进行过拟合修正,典型的可以通过正则化来进行过拟合倾向修正的模型主要有线性回归、逻辑回归、LDA、SVM 以及一些 PCA 衍生算法(如 SparsePCA)。而树模型则不用通过正则化来进行过拟合修正。