【机器学习实战笔记 4】Scikit-Learn使用与进阶二

《Scikit-Learn使用与进阶 二》

1 逻辑回归手动调参实验

  在补充了一系列关于正则化的基础理论以及sklearn中逻辑回归评估器的参数解释之后,接下来,我们尝试借助sklearn中的逻辑回归评估器,来执行包含特征衍生和正则化过程的建模试验,同时探索模型经验风险和结构风险之间的关系。一方面巩固此前介绍的相关内容,同时也进一步加深对于Pipeline的理解并熟练对其的使用。当然更关键的一点,本节的实验将为下一小节的网格搜索调参做铺垫,并在后续(Lesson 6.6)借助网格搜索工具,给出更加完整、更加自动化、并且效果更好的调参策略。

一、数据准备与评估器构造

  首先需要进行数据准备。为了更好的配合进行模型性能与各种方法效果的测试,此处先以手动创建数据集为例进行试验。

1.数据准备

  我们曾介绍到关于逻辑回归的决策边界实际上就是逻辑回归的线性方程这一特性,并由此探讨了一元函数与二维平面的决策边界之间的关系,据此我们可以创建一个满足分类边界为 y 2 = − x + 1.5 y^2=-x+1.5 y2=x+1.5的分布,创建方法如下:

# 科学计算模块
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.datasets import load_iris

# 实用函数
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

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)

2.构建机器学习流

  接下来,调用逻辑回归中的相关类,来进行模型构建。很明显,面对上述曲线边界的问题,通过简单的逻辑回归无法达到较好的预测效果,因此需要借助此前介绍的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则是逻辑回归中控制正则化及惩罚力度的相关参数,该组参数能够很好的控制模型对于训练数据规律的挖掘程度,当然,最终的建模目标是希望构建一个很好挖掘全局规律的模型,即一方面我们希望模型尽可能挖掘数据规律,另一方面我们又不希望模型过拟合。

上述过程有两点需要注意:首先,复杂模型的建模往往会有非常多的参数需要考虑,但一般来说我们会优先考虑影响最终建模效果的参数(如影响模型前拟合、过拟合的参数),然后再考虑影响训练过程的参数(如调用几核心进行计算、采用何种迭代求解方法等),前者往往是需要调整的核心参数;其次,上述实例化逻辑回归模型时,我们适当提高了最大迭代次数,这是一般复杂数据建模时都需要调整的参数。

二、评估器训练与过拟合实验

接下来进行模型训练,并且尝试进行手动调参来控制模型拟合度。

  • 建模结果观察与决策边界函数
      接下来测试模型性能,首先是不进行特征衍生时的逻辑回归建模结果:

绘制决策边界观察

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')

过拟合实验

# 拟合模型 度数为1时候
pr2 = plr(degree=2)
pr2.fit(X_train, y_train)
pr2.score(X_train, y_train),pr2.score(X_test, y_test)
# 测试函数性能
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)
plot_decision_boundary(X, y, pr2)

在这里插入图片描述
能够发现,模型效果有了明显提升,这里首先我们可以通过训练完的逻辑回归模型参数个数来验证当前数据特征数量:
  此处我们可以通过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)

在这里插入图片描述

  在基本验证上述代码执行过程无误之后,接下来我们可以尝试通过衍生更高阶特征来提高模型复杂度,并观察在提高模型复杂度的过程中训练误差和测试误差是如何变化的。
观测不同回归模型最高度数的准确率

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)
    
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正则化来抑制过拟合影响,并且从上一小节我们得知,一个比较好的建模流程是先进行数据增强(特征衍生),来提升模型表现,然后再通过正则化的方式来抑制过拟合倾向。接下来,我们就上述问题来进行相关尝试

# 测试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)
plot_decision_boundary(X, y, pl2)

在这里插入图片描述
  尽管从决策边界上观察并不明显,但从最终建模结果来看,正则化确实起到了抑制过拟合的效果。接下来我们尝试手动对上述模型进行调参,尝试能否提高模型表现。此处我们采用一个非常朴素的想法来进行调参,即使用degree、C和正则化选项(l1或l2)的不同组合来进行调参,试图从中选择一组能够让模型表现最好的参数,并且先从degree开始进行搜索:

  • l1正则化下最优特征衍生阶数
# 用于存储不同模型训练准确率与测试准确率的列表
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)

在这里插入图片描述

  • l2正则化下最优特征衍生阶数
# 用于存储不同模型训练准确率与测试准确率的列表
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)

在这里插入图片描述
此处我们选取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_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)

在这里插入图片描述
最终,我们通过蛮力搜索,确定了一组能够让测试集准确率取得最大值的参数组合:degree=15, penalty=‘l2’, C=1.0,此时测试集准确率为0.8。

  • 手动调参评价
      尽管上述过程能够帮助我们最终找到一组相对比较好的参数组合,最终建模结果相比此前,也略有提升,但上述手动调参过程存在三个致命问题:
      (1).过程不够严谨,诸如测试集中测试结果不能指导建模、参数选取及搜索区间选取没有理论依据等问题仍然存在;
      (2).执行效率太低,如果面对更多的参数(这是更一般的情况),手动执行过程效率太低,无法进行超大规模的参数挑选;
      (3).结果不够精确,一次建模结果本身可信度其实并不高,我们很难证明上述挑选出来的参数就一定在未来数据预测中拥有较高准确率。
      而要解决这些问题,我们就需要补充关于机器学习调参的理论基础,以及掌握更多更高效的调参工具。正因如此,我们将在下一小节详细介绍关于机器学习调参的基本理论以及sklearn中的网格搜索调参工具,而后我们再借助更完整的理论、更高效的工具对上述问题进行解决。

2 机器学习调参基础理论与网格搜索

  在上一小节执行完手动调参之后,接下来我们重点讨论关于机器学习调参的理论基础,并且介绍sklearn中调参的核心工具——GridSearchCV。

一、机器学习调参理论基础

  在利用sklearn进行机器学习调参之前,我们先深入探讨一些和调参相关的机器学习基础理论。尽管我们都知道,调参其实就是去寻找一组最优参数,但最优参数中的“最优”如何定义?面对模型中的众多参数又该如何“寻找”?要回答这些问题,我们就必须补充更加完整的关于机器学习中参数和调参的理论知识。

1.机器学习调参目标及基本方法

  首先需要明确的一点,我们针对哪一类参数进行调参,以及围绕什么目的进行调参?

  • 参数与超参数
      根据此前对参数的划分,我们知道,影响机器学习建模结果的参数有两类,其一是参数,其二是超参数。其中参数的数值计算由一整套数学过程决定,在选定方法后,其计算过程基本不需要人工参与。因此我们经常说的模型调参,实际上是调整模型超参数。超参数种类繁多,而且无法通过一个严谨的数学流程给出最优解,因此需要人工参与进行调节。
      而在围绕具体的机器学习评估器进行调参时,其实就是在调整评估器实例化过程中所涉及到的那些超参数,例如此前进行逻辑回归参数解释时的超参数,当然,这也是我们为什么需要对评估器进行如此详细的超参数的解释的原因之一。
参数解释
penalty正则化项
dual是否求解对偶问题*
tol迭代停止条件:两轮迭代损失值差值小于tol时,停止迭代
C经验风险和结构风险在损失函数中的权重
fit_intercept线性方程中是否包含截距项
intercept_scaling相当于此前讨论的特征最后一列全为1的列,当使用liblinear求解参数时用于捕获截距
class_weight各类样本权重*
random_state随机数种子
solver损失函数求解方法*
max_iter求解参数时最大迭代次数,迭代过程满足max_iter或tol其一即停止迭代
multi_class多分类问题时求解方法*
verbose是否输出任务进程
warm_start是否使用上次训练结果作为本次运行初始参数
l1_ratio当采用弹性网正则化时, l 1 l1 l1正则项权重,就是损失函数中的 ρ \rho ρ
  • 超参数调整目标
      那么紧接着的问题就是,超参数的调整目标是什么?是提升模型测试集的预测效果么?
      无论是机器学习还是统计模型,只要是进行预测的模型,其实核心的建模目标都是为了更好的进行预测,也就是希望模型能够有更好的预测未来的能力,换而言之,就是希望模型能够有更强的泛化能力。而我们曾谈到,机器学习类算法的可信度来源则是训练集和测试集的划分理论,也就是机器学习会认为,只要能够在模拟真实情况的测试集上表现良好,模型就能够具备良好的泛化能力。也就是说,超参数调整的核心目的是为了提升模型的泛化能力,而测试集上的预测效果只是模型泛化能力的一个具体表现,并且相比与一次测试集上的运行结果,其实借助交叉验证,能够提供更有效、更可靠的模型泛化能力的证明。
  • 交叉验证与评估指标
      如果需要获得更可靠的模型泛化能力的证明,则需要进行交叉验证,通过多轮的验证,来获得模型的更为一般、同时也更为准确的运行结果。当然,我们还需要谨慎的选择一个合适的评估指标对其进行结果评估。
  • 如何提升模型泛化能力
      如果拥有了一个更加可信的、用于验证模型是否具有泛化能力的评估方式之后,那么接下来的问题就是,我们应该如何提升模型泛化能力呢?
      当然,这其实是一个很大的问题,我们可以通过更好的选择模型(甚至是模型创新)、更好的特征工程、更好的模型训练等方法来提高模型泛化能力,而此处我们将要介绍的,是围绕某个具体的模型、通过更好的选择模型中的超参数,来提高模型的泛化能力。不过正如此前所说,超参数无法通过一个严谨的数学流程给出最优解,因此超参数的选择其实是经验+一定范围内枚举(也就是网格搜索)的方法来决定的。这个过程虽然看起来不是那么的cooooool,但确实目前机器学习超参数选择的通用方式,并且当我们深入进行了解之后就会发现,尽管是经验+枚举,但经验的积累和枚举技术的掌握,其实也是算法工程师建模水平的重要证明。

2.基于网格搜索的超参数的调整方法

  在了解机器学习中调参的基础理论之后,接下来我们考虑一个更加具体的调参流程。实际上,尽管对于机器学习来说超参数众多,但能够对模型的建模结果产生决定性影响的超参数却不多,对于大多数超参数,我们都主要采用“经验结合实际”的方式来决定超参数的取值,如数据集划分比例、交叉验证的折数等等,而对于一些如正则化系数、特征衍生阶数等,则需要采用一个流程来对其进行调节。而这个流程,一般来说就是进行搜索与枚举,或者也被称为网格搜索(gridsearch)。
  所谓搜索与枚举,指的是将备选的参数一一列出,多个不同参数的不同取值最终将组成一个参数空间(parameter space),在这个参数空间中选取不同的值带入模型进行训练,最终选取一组最优的值作为模型的最终超参数,当然,正如前面所讨论的,此处“最优”的超参数,应该是那些尽可能让模型泛化能力更好的参数。当然,在这个过程中,有两个核心问题需要注意,其一是参数空间的构成,其二是选取能够代表模型泛化能力的评估指标。接下来我们对其进行逐个讨论。

2.1 参数空间
  • 参数空间的定义
      所谓参数空间,其实就是我们挑选出来的、接下来需要通过枚举和搜索来进行数值确定的参数取值范围所构成的空间。例如对于逻辑回归模型来说,如果选择penalty参数和C来进行搜索调参,则这两个参数就是参数空间的不同维度,而这两个参数的不同取值就是这个参数空间中的一系列点,例如(penalty=‘l1’, C=1)、(penalty=‘l1’, C=0.9)、(penalty=‘l2’, C=0.8)等等,就是这个参数空间内的一系列点,接下来我们就需要从中挑选组一个最优组合。
  • 参数空间构造思路
      那么我们需要带入那些参数去构造这个参数空间呢?也就是我们需要选择那些参数进行调参呢?切记,调参的目的是为了提升模型的泛化能力,而保证泛化能力的核心是同时控制模型的经验风险和结构风险(既不让模型过拟合也不让模型前拟合),因此,对于逻辑回归来说,我们需要同时带入能够让模型拟合度增加、同时又能抑制模型过拟合倾向的参数来构造参数空间,即需要带入特征衍生的相关参数、以及正则化的相关参数。

一个建模流程中的特征衍生的相关参数,也是可以带入同一个参数空间进行搜索的。

2.2 交叉验证与评估指标

  实际的超参数的搜索过程和我们上面讨论的模型结构风险一节中的参数选取过程略有不同,此前我们的过程是:先在训练集中训练模型,然后计算训练误差和泛化误差,通过二者误差的比较来观察模型是过拟合还是欠拟合(即评估模型泛化能力),然后再决定这些超参数应该如何调整。而在一个更加严谨的过程中,我们需要将上述“通过对比训练误差和测试误差的差异,来判断过拟合还是欠拟合”的这个偏向主观的过程变成一个更加客观的过程,即我们需要找到一个能够基于目前模型建模结果的、能代表模型泛化能力的评估指标,这即是模型建模流程更加严谨的需要,同时也是让测试集回归其本来定位的需要。

  • 评估指标选取
      而这个评估指标,对于分类模型来说,一般来说就是ROC-AUC或F1-Score,并且是基于交叉验证之后的指标。我们通常会选取ROC-AUC或F1-Score,其实也是因为这两个指标的敏感度要强于准确率(详见Lesson 5中的讨论),并且如果需要重点识别模型识别1类的能力,则可考虑F1-Score,其他时候更推荐使用ROC-AUC。
  • 交叉验证过程
      而为何要进行交叉验证,则主要原因是超参数的调整也需要同时兼顾模型的结构风险和经验风险,而能够表示模型结构风险的,就是不带入模型训练、但是能够对模型建模结果进行评估并且指导模型进行调整的验证集上的评估结果。

  上述过程可以具体表示成如下步骤:

  • 在训练集中进行验证集划分(几折待定);
  • 带入训练集进行建模、带入验证集进行验证,并输出验证集上的模型评估指标;
  • 计算多组验证集上的评估指标的均值,作为该超参数下模型最终表现。

  因此,在大多数情况下,网格搜索(gridsearch)都是和交叉验证(CV)同时出现的,这也是为什么sklearn中执行网格搜索的类名称为GridSearchCV的原因。
  另外需要强调的一点是,由于交叉验证的存在,此时测试集的作用就变成了验证网格搜索是否有效,而非去验证模型是否有效(此时模型是否有效由验证集来验证)。由于此时我们提交给测试集进行测试的,都是经过交叉验证挑选出来的最好的一组参数、或者说至少是在验证集上效果不错的参数(往往也是评估指标比较高的参数),而此时如果模型在测试集上运行效果不好、或者说在测试集上评估指标表现不佳,则说明模型仍然还是过拟合,之前执行的网格搜索过程并没有很好的控制住模型的结构风险,据此我们需要调整此前的调参策略,如调整参数空间、或者更改交叉验证策略等。
  当然,如果是对网格搜索的过程比较自信,也可以不划分测试集,直接带入全部数据进行模型训练。

二、基于Scikit-Learn的网格搜索调参

1.sklearn中网格搜索的基本说明

  官网文档重点指出了网格搜索中的核心要素,分别是:评估器、参数空间、搜索策略、交叉验证以及评估指标。其中参数空间、交叉验证以及评估指标我们都在此前介绍过了,而根据下文的介绍,sklearn中实际上是集成了两种不同的进行参数搜索的方法,分别是GridSearchCVRandomizedSearchCV
  尽管都是进行网格搜索,但两种方法还是各有不同,GridSearchCV会尝试参数空间内的所有组合,而RandomizedSearchCV则会先进行采样再来进行搜索,即对某个参数空间的某个随机子集进行搜索。并且上文重点强调,这两种方法都支持先两两比对、然后逐层筛选的方法来进行参数筛选,即HalvingGridSearchCV和HalvingRandomSearchCV方法。注意,这是sklearn最新版、也就是0.24版才支持的功能,该功能的出现也是0.24版最大的改动之一,而该功能的加入,也将进一步减少网格搜索所需计算资源、加快网格搜索的速度。

2.sklearn中GridSearchCV的参数解释

  接下来我们详细介绍GridSearchCV的相关参数,我们知道该方法的搜索策略是“全搜索”,即对参数空间内的所有参数进行搜索,该方法在model_selection模块下,同样也是以评估器形式存在,我们可以通过如下方式进行导入:

from sklearn.model_selection import GridSearchCV
GridSearchCV?
NameDescription
estimator调参对象,某评估器
param_grid参数空间,可以是字典或者字典构成的列表,稍后介绍参数空间的创建方法
scoring评估指标,支持同时输出多个参数
n_jobs设置工作时参与计算的CPU核数
iid交叉验证时各折数据是否独立,该参数已在0.22版中停用,将在0.24版中弃用,此处不做介绍
refit挑选评估指标和最佳参数,在完整数据集上进行训练
cv交叉验证的折数
verbose输出工作日志形式
pre_dispatch多任务并行时任务划分数量
error_score当网格搜索报错时返回结果,选择’raise’时将直接报错并中断训练过程,其他情况会显示警告信息后继续完成训练
return_train_score在交叉验证中是否显示训练集中参数得分

  整体来看,上面的主要参数分为三类,分别是核心参数、评估参数和性能参数。

  • 核心参数
      所谓性能参数,也就是涉及评估器训练(fit)的最核心参数,也就是estimator参数和param_grid参数,同时也是实例化评估器过程中最重要的参数。
  • 评估参数
      所谓评估参数,指的是涉及到不同参数训练结果评估过程方式的参数,主要是scoring、refit和cv三个参数。当然这三个参数都不是必要参数,但这三个参数却是直接决定模型结果评估过程、并且对最终模型参数选择和模型泛化能力提升直观重要的三个参数。这三个参数各自都有一个默认值,我们先解释在默认值情况下这三个参数的运作方式,然后在进阶应用阶段讨论如何对这三个参数进行修改。
      首先是关于scoring参数的选取,scoring表示选取哪一项评估指标来对模型结果进行评估。而根据参数说明文档我们知道,在默认情况下scoring的评估指标就是评估器的.score方法默认的评估指标,对于逻辑回归来说也就是准确率。也就是说在默认情况下如果是围绕逻辑回归进行网格搜索,则默认评估指标是准确率。此外,scoring参数还支持直接输入可调用对象(评估函数)、代表评估函数运行方式的字符串、字典或者list。而refit参数则表示选择一个用于评估最佳模型的评估指标,然后在最佳参数的情况下整个训练集上进行对应评估指标的计算。而cv则是关于交叉验证的相关参数,默认情况下进行5折交叉验证,并同时支持自定义折数的交叉验证、输入交叉验证评估器的交叉验证、以及根据指定方法进行交叉验证等方法。当然此组参数有非常多的设计方法,我们将在进阶应用阶段进行进一步的详解。
  • 性能参数
      第三组则是关于网格搜索执行性能相关的性能参数,主要包括n_jobs和pre_dispatch参数两个,用于规定调用的核心数和一个任务按照何种方式进行并行运算。在网格搜索中,由于无需根据此前结果来确定后续计算方法,所以可以并行计算。在默认情况下并行任务的划分数量和n_jobs相同。当然,这组参数的合理设置能够一定程度提高模型网格搜索效率,但如果需要大幅提高执行速度,建议使用RandomizedSearchCV、或者使用Halving方法来进行加速。

3.sklearn中GridSearchCV的使用方法

  在了解了GridSearchCV的基本方法之后,接下来我们以逻辑回归在鸢尾花数据集上建模为例,来尝试使用GridSearchCV方法进行网格调参,并同时介绍网格搜索的一般流程:

3.1 GridSearchCV评估器训练过程
  • Step 1.创建评估器
      首先我们还是需要实例化一个评估器,这里可以是一个模型、也可以是一个机器学习流,网格搜索都可以对其进行调参。此处我们先从简单入手,尝试实例化逻辑回归模型并对其进行调参。
# 数据导入
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=24)
clf = LogisticRegression(max_iter=int(1e6), solver='saga')
  • Step 2.创建参数空间
      接下来,我们就需要挑选评估器中的超参数构造参数空间,需要注意的是,我们需要挑选能够控制模型拟合度的超参数来进行参数空间的构造,例如挑选类似verbose、n_jobs等此类参数构造参数是毫无意义的。此处我们挑选penalty和C这两个参数来进行参数空间的构造。参数空间首先可以是一个字典:
param_grid_simple = {'penalty': ['l1', 'l2'],
                     'C': [1, 0.5, 0.1, 0.05, 0.01]}

其中,字典的Key用参数的字符串来代表不同的参数,对应的Value则用列表来表示对应参数不同的取值范围。也就是字典的Key是参数空间的维度,而Value则是不同纬度上可选的取值。而后续的网格搜索则是在上述参数的不同组合中挑选出一组最优的参数取值。
  当然,由于如此构造方法,此处自然会衍生出一个新的问题,那就是如果某个维度的参数取值对应一组新的参数,应该如何处理?例如,对于逻辑回归来说,如果penalty参数中选择弹性网参数,则会衍生出一个新的参数l1_ratio,如果我们还想考虑penalty参数选取elasticnet参数,并且同时评估l1_ratio取不同值时模型效果,则无法将上述参数封装在一个参数空间内,因为当penalty取其他值时l1_ratio并不存在。为了解决这个问题,我们可以创造多个参数空间(字典),然后将其封装在一个列表中,而该列表则表示多个参数空间的集成。例如上述问题可以进行如下表示:

param_grid_ra = [
    {'penalty': ['l1', 'l2'], 'C': [1, 0.5, 0.1, 0.05, 0.01]}, 
    {'penalty': ['elasticnet'], 'C': [1, 0.5, 0.1, 0.05, 0.01], 'l1_ratio': [0.3, 0.6, 0.9]}
]

即可表示网格搜索在l1+1、l1+0.5…空间与elasticnet+1+0.3、elasticnet+1+0.6…空间同时进行搜索。

  • Step 3.实例化网格搜索评估器
      和sklearn中其他所有评估器一样,网格搜索的评估器的使用也是先实例化然后进行对其进行训练。此处先实例化一个简单的网格搜索评估器,需要输入此前设置的评估器和参数空间。
search = GridSearchCV(estimator=clf,
                      param_grid=param_grid_simple)
  • Step 4.训练网格搜索评估器
search.fit(X_train, y_train)
3.2 GridSearchCV评估器结果查看

  此处我们先介绍关于网格搜索类的所有属性和方法,再来查看挑选其中重要属性的结果进行解读。

NameDescription
cv_results_交叉验证过程中的重要结果
best_estimator_最终挑选出的最优
best_score_在最优参数情况下,训练集的交叉验证的平均得分
best_params_最优参数组合
best_index_CV过程会对所有参数组合标号,该参数表示最优参数组合的标号
scorer在最优参数下,计算模型得分的方法
n_splits_交叉验证的折数
  • best_estimator_:训练完成后的最佳评估器
      实际上返回的就是带有网格搜索挑选出来的最佳参数(超参数)的评估器。
search.best_estimator_

上述评估器就相当于一个包含最佳参数的逻辑回归评估器,可以调用逻辑回归评估器的所有属性:

# 查看参数
search.best_estimator_.coef_

# 查看训练误差、测试误差
search.best_estimator_.score(X_train,y_train), search.best_estimator_.score(X_test,y_test)

# 查看参数
search.best_estimator_.get_params()

search.best_score_

  在默认情况下(未修改网格搜索评估器中评估指标参数时),此处的score就是准确率。此处有两点需要注意:
  其一:该指标和训练集上整体准确率不同,该指标是交叉验证时验证集准确率的平均值,而不是所有数据的准确率;
  其二:该指标是网格搜索在进行参数挑选时的参照依据。

  至此,我们就执行了一个完整的网格搜索的调参过程。但该过程大多只使用了默认参数在小范围内进行的运算,如果我们希望更换模型评估指标、并且在一个更加完整的参数范围内进行搜索,则需要对上述过程进行修改,并更近一步掌握关于评估器中scoring参数和refit参数的相关使用方法,相关内容我们将在下一小节进行详细讨论。

3 多分类评估指标的macro与weighted过程

  在正式讨论关于网格搜索的进阶使用方法之前,我们需要先补充一些关于多分类问题的评估指标计算过程。在此前的课程中,我们曾经介绍过分类模型在解决多分类问题时的不同策略,同时也介绍过二分类问题的更高级评估指标,如f1-score和roc-auc等,接下来我们将详细讨论关于多分类预测结果在f1-socre和roc-auc中的评估过程,以及在sklearn中如何调用函数进行计算。

# 数据准备
from sklearn.datasets import load_iris
from sklearn.metrics import precision_score,recall_score,f1_score

precision_score?

具体参数含义解释如下:

NameDescription
y_true数据集真实标签
y_pred标签预测结果
labels允许以列表形式输入其他形态的标签,一般不进行修改
pos_labelpositive类别标签
average多分类时指标计算方法
sample_weight不同类别的样本权重
zero_division当分母为0时返回结果

其中,需要重点介绍多分类问题时average参数不同取值时的计算方法。此处以recall为例进行计算,重点介绍当average取值为’macro’、'micro’和’weighted’的情况,其他指标也类似,例如有简单多分类问题如下:
在这里插入图片描述
我们令1类标签为0、2类标签为1、3类标签为2,则上述数据集真实标签为:

y_true = np.array([0, 1, 2, 2, 0, 1, 1, 2, 0, 2])
# 并且最终分类预测结果为:
y_pred = np.array([0, 1, 0, 2, 2, 1, 2, 2, 0, 2])

据此我们可以构造多分类混淆矩阵如下:
在这里插入图片描述
据此我们可以计算三个类别的TP和FN:

# 真正例true positive
tp1 = 2
tp2 = 2
tp3 = 3
# 假负例 false negative
fn1 = 1
fn2 = 1
fn3 = 1

接下来有两种计算recall的方法,其一是先计算每个类别的recall,然后求均值:
macro: 这也就是average参数取值为macro时的计算结果

re1 = 2/3
re2 = 2/3
re3 = 3/4
np.mean([re1, re2, re3])
# 等价于
recall_score(y_true, y_pred, average='macro')

weighted:当然,如果上述手动实现过程不求均值,而是根据每个类别的数量进行加权求和,则就是参数average参数取值为weighted时的结果:

re1 * 3/10 + re2 * 3/10 + re3 * 4/10
recall_score(y_true, y_pred, average='weighted')

micro:当然,还有另外一种计算方法,那就是先计算整体的TP和FN,然后根据整体TP和FN计算recall:

tp = tp1 + tp2 + tp3
fn = fn1 + fn2 + fn3
recall = tp / (tp+fn)
recall_score(y_true, y_pred, average='micro')

  对于上述三个不同参数的选取,首先如果是样本不平衡问题(如果是要侧重训练模型判别小类样本的能力的情况下)、则应排除weighted参数,以避免赋予大类样本更高的权重。除此以外,在大多数情况下这三个不同的参数其实并不会对最后评估器的选取结果造成太大影响,只是在很多要求严谨的场合下需要说明多分类的评估结果的计算过程,此时需要简单标注下是按照何种方法进行的计算。
  不过,如果是混淆矩阵中相关指标和roc-auc指标放在一起讨论,由于新版sklearn中roc-auc本身不支持在多分类时按照micro计算、只支持macro计算,因此建议混淆矩阵的多分类计算过程也选择macro过程,以保持一致。后续在没有进行其他特殊说明的情况下,课上统一采用macro指标进行多分类问题评估指标的计算。

不过值得注意的是,还有一种观点,尽管micro和macro方法在混淆矩阵相关指标的计算过程中差别不大,在roc-auc中,macro指标并不利于非平衡样本的计算(混淆矩阵中可以通过positive的类别选择来解决这一问题),需要配合ovr分类方法才能够有所改善。

  • 多分类ROC-AUC评估指标
from sklearn.metrics import roc_auc_score
y_true = np.array([1, 0, 0, 1, 0, 1])
y_pred = np.array([0.9, 0.7, 0.2, 0.7, 0.4, 0.8])
roc_auc_score(y_true, y_pred)

roc_auc_score?
NameDescription
max_fprfpr最大值,fpr是roc曲线的横坐标
multi_class分类器在进行多分类时进行的多分类问题处理策略

  此处需要注意的是关于multi_class参数的选择。一般来说sklearn中的multi_class参数都是二分类器中用于解决多元分类问题时的参数(如逻辑回归),而由于roc-auc需要分类结果中的概率来完成最终计算,因此需要知道概率结果对应分类标签——即到底是以ovo还是ovr模式在进行多分类,因此如果是进行多分类roc-auc计算时,需要对其进行明确说明。
  不过对于多分类逻辑回归来说,无论是ovr还是mvm策略,最终分类结果其实都可以看成是ovr分类结果,因此如果是多分类逻辑回归计算roc-auc,需要设置multi_class参数为ovr。同时由于根据roc-auc的函数参数说明可知,在multi_class参数取为ovr时,average参数取值为macro时能够保持一个较高的偏态样本敏感性,因此对于roc-auc来说,大多数时候average参数建议取值为macro。总结一下,对于roc-auc进行多分类问题评估时,建议选择的参数组合是ovr/ovo+macro,而ovr/ovo的参数选择需要根据具体的多分类模型来定,如果是围绕逻辑回归多分类评估器来进行结果评估,则建议roc-auc和逻辑回归评估器的multi_class参数都选择ovr。

在新版的sklearn中,roc-auc函数的multi_class参数已不支持micro参数,面对多分类问题,该参数只能够在macro和weighted中进行选择。

4 GridSearchCV的进阶使用方法

  我们已经完整总结了机器学习调参的基本理论,同时介绍了sklearn中网格搜索(GridSearchCV)评估器的参数及基本使用方法。本节我们将进一步介绍网格搜索的进阶使用方法,并同时补充多分类问题评估指标在sklearn中实现的相关方法,然后围绕Lesson 6.4中提出的问题给出一个基于网格搜索的解决方案。

一、借助机器学习流构建全域参数搜索空间

  首先是关于评估器全参数的设置方法。在此前的实验中,我们只是保守的选取了部分我们觉得会对模型产生比较大影响的超参数来构建参数空间,但在实际场景中,调参应该是纳入所有对模型结果有影响的参数进行搜索、并且是全流程中的参数来进行搜索。也就是说我们设置参数的空间的思路不应该更加“激进”一些,首先是对逻辑回归评估器来说,应该是排除无用的参数外纳入所有参数进行调参,并且就逻辑回归模型来说,往往我们需要在模型训练前进行特征衍生以增强模型表现,因此我们应该先构建一个包含多项式特征衍生的机器学习流、然后围绕这个机器学习流进行参数搜索,这才是一个更加完整的调参过程。

# 科学计算模块
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 GridSearchCV

# 实用函数
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 数据准备
from sklearn.datasets import load_iris

np.random.seed(24)
X = np.random.normal(0, 1, size=(1000, 2))
y = np.array(X[:,0]+X[:, 1]**2 < 1.5, int)

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)

# 构造机器学习流
pipe = make_pipeline(PolynomialFeatures(), 
                     StandardScaler(), 
                     LogisticRegression(max_iter=int(1e6)))

# 构造参数空间
param_grid = [
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['l1'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__solver': ['saga']}, 
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['l2'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']},
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['elasticnet'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__l1_ratio': np.arange(0.1, 1, 0.1).tolist(), 'logisticregression__solver': ['saga']}
]

二、优化评估指标选取

1.高级评估指标的选用方法

  根据此前介绍,如果需要更好的验证模型本身泛化能力,建议使用f1-score或者roc-auc,当然调整网格搜索过程的模型评估指标过程其实并不难理解,核心就是修改scoring参数取值。但由于涉及到在参数中调用评估函数,因此需要补充一些关于常用分类评估指标在sklearn中的函数使用方法,以及不同评估指标函数在不同参数取值时在网格搜索评估器中的调用方法。

GridSearchCV?

  从评估器的说明文档中能够看出,scoring参数最基础的情况下可以选择输入str(字符串)或者callable(可调用)对象,也就是可以输入指代某个评估过程的字符串(一个字符串代表不同参数取值下的某评估函数),或者直接输入某评估指标函数(或者通过make_score函数创建的函数),来进行模型结果的评估。当然,也可以在该参数位上直接输入一个字典或者list,其中,如果是字典的话字典的value需要是str(字符串)或者callable(可调用)对象。这里看似复杂但实际上不难理解,由于sklearn中的评估指标函数一般都是有多个不同参数,而不同参数代表不同的计算过程,因此这些评估指标函数作为参数输入网格搜索评估器中的时候,必须通过“某种方式”确定这些参数取值,因此就有了如下方法,即通过字符串对应表来查看不同字符串所代表的不同参数取值下的评估指标函数,如下所示:
在这里插入图片描述
不难看出,在网格搜索中输出评估指标参数,和调用评估指标函数进行数据处理还是有很大的区别。例如,metrics.roc_auc_score函数能够同时处理多分类问题和二分类问题,但如果作为参数输入到网格搜索中,roc_auc参数只能指代metrics.roc_auc_score函数的二分类功能,如果需要进行多分类,则需要在scoring参数中输入roc_auc_ovrroc_auc_ovo或者roc_auc_ovr_weightedroc_auc_ovo_weighted。我们先简单尝试在scoring中输入字符串的基本操作,然后在深入解释roc-auc评估指标的使用方法。

from sklearn.metrics import roc_auc_score
roc_auc_score?

例如字符串roc_auc_ovr就代表roc_auc_score函数中multi_class参数取值为ovr时的计算流程,也就是说,当网格搜索的scoring参数取值为字符串roc_auc_ovr时,就代表调用了multi_class=`ovr`、而其他参数选用默认参数的roc_auc_score函数作为模型评估函数。

GridSearchCV(estimator=pipe,
             param_grid=param_grid,
             scoring='roc_auc_ovr')

acc(search.best_estimator_, X_train, y_train)

2.同时输入多组评估指标

  当然,有的时候我们可能需要同时看不同参数下多项评估指标的结果,此时我们就可以在scoring中输入列表、元组或者字典,当然字典对象会较为常用,例如如果我们需要同时选用roc-auc和accuracy作为模型评估指标,则需要创建如下字典:

from sklearn.metrics import make_scorer
scoring = {'AUC': 'roc_auc', 'Accuracy': make_scorer(accuracy_score)}

search = GridSearchCV(estimator=clf,
                      param_grid=param_grid_simple, 
                      scoring=scoring, 
                      refit='Accuracy')

{'AUC': make_scorer(roc_auc_score), 'Accuracy': 'accuracy'}

不过,需要注意的是,尽管此时网格搜索评估器将同时计算一组参数下的多个评估指标结果并输出,但我们只能选取其中一个评估指标作为挑选超参数的依据,而其他指标尽管仍然会计算,但结果只作参考。而refit参数中输入的评估指标,就是最终选择参数的评估指标

尽管网格搜索支持依据不同的评估指标进行参数搜索,但最终选择何种参数,可以参考如下依据:

  • 有明确模型评估指标的
      在很多竞赛或者项目算法验收环节,可能都会存在较为明确的模型评估指标,例如模型排名根据f1-score计算结果得出等。在该情况下,应当尽量选择要求的评估指标。

  • 没有明确模型评估指标的
      但是,如果没有明确的评估指标要求,则选择评估指标最核心的依据就是尽可能提升/确保模型的泛化能力。此时,根据Lesson 5中对各评估指标的讨论结果,如果数据集的各类别并没有明确的差异,在算力允许的情况下,应当优先考虑roc-auc;而如果希望重点提升模型对类别1(或者某类别)的识别能力,则可以优先考虑f1-score作为模型评估指标。

三、优化后建模流程

  接下来,依据上述优化后的过程,来执行网格搜索。完整流程如下:

# 构造机器学习流
pipe = make_pipeline(PolynomialFeatures(), 
                     StandardScaler(), 
                     LogisticRegression(max_iter=int(1e6)))

# 构造参数空间
param_grid = [
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['l1'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__solver': ['saga']}, 
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['l2'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']},
    {'polynomialfeatures__degree': np.arange(2, 10).tolist(), 'logisticregression__penalty': ['elasticnet'], 'logisticregression__C': np.arange(0.1, 2, 0.1).tolist(), 'logisticregression__l1_ratio': np.arange(0.1, 1, 0.1).tolist(), 'logisticregression__solver': ['saga']}
]

# 实例化网格搜索评估器
search = GridSearchCV(estimator=pipe,
                      param_grid=param_grid, 
                      scoring='roc_auc', 
                      n_jobs=5)
# 执行训练
search.fit(X_train, y_train)
# 查看结果
search.best_score_

search.best_params_

search.best_estimator_.score(X_train,y_train)

search.best_estimator_.score(X_test,y_test)

accuracy_score(search.best_estimator_.predict(X_train), y_train)

accuracy_score(search.best_estimator_.predict(X_test), y_test)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wilber的技术分享

若有所收获请打赏哦 十分感谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值