第5章模型评估与优化 本章内容 算法链与管道 交叉验证 模型评价指标 处理类的不平衡问题 网格搜索优化模型 5.1算法链与管道 大多数机器学习应用不仅需要应用单个算法,而且还需要将许多不同的处理步骤和机器学习模型链接在一起,这就是算法链。管道(Pipeline)指的是简化构建变换和模型链的过程。本节将介绍如何使用Pipeline类来简化构建变换和模型链的过程,以帮助快速构建模型。 微课视频 5.1.1用管道方法简化工作流 管道可以理解为一个容器,然后把需要进行的操作都封装在这个管道里面进行操作,比如数据标准化、特征降维、主成分分析、模型预测等。为了便于理解,下面举一个实例来讲解。 1. 数据导入与预处理 本次导入威斯康星乳腺癌(Breast Cancer Wisconsin)二分类数据集,它包括569个样本。首列为主键id,第2列为类别值(M=恶性肿瘤,B=良性肿瘤),第3~32列为30个根据细胞核的数字化图像计算出的实数特征值。 代码清单51: 算法链与管道 1) 导入数据集 导入威斯康星乳腺癌数据集,输出前5条记录,如图5.1所示。 1In[1]: 2import pandas as pd 3bcdf = pd.read_csv('wdbc.csv' ,header=None) 4bcdf.head() 5Out[1]: 图5.1数据集前5条记录 2) 移除次要特征 首先移除id以及Unnamed: 32这两个不需要的列,然后利用map映射函数将目标值(M,B)转为(1,0)。最后移除diagnosis列,移除之前将其保存到y。移除次要特征之后的结果如图5.2所示。 1In[2]: 2bcdf.drop(['id','Unnamed: 32'], axis = 1, inplace = True) 3bcdf['diagnosis'] = bcdf['diagnosis'].map({'M':1, 'B':0}) 4# 将目标转为0-1变量 5X = bcdf 6y = bcdf.diagnosis 7bcdf.drop('diagnosis', axis = 1, inplace = True) 8bcdf.head() 9Out[2]: 图5.2移除次要特征后的前5条记录 3) 划分训练及验证集 代码如下。 1In[3]: 2from sklearn.model_selection import train_test_split 3X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=1) 2. 使用管道创建工作流 很多机器学习算法要求特征取值范围要相同,因此需要对特征做标准化处理。此外,为了获取更好的分类效果,还应将原始的30维特征压缩至更少维度,这就需要用主成分分析(PCA)来完成,降维之后就可以利用各种模型进行回归预测了。 Pipeline对象接收元组构成的列表作为输入,每个元组第一个值作为变量名,元组第二个元素是sklearn中的transformer或estimator。管道中间每一步由sklearn中的transformer构成,最后一步是一个estimator。 本次数据集中,管道包含两个中间步骤: StandardScaler和PCA,两者都属于transformer,而Logistic回归分类器属于estimator。 本案例中,当管道Pipeline执行fit方法时,实际上是分以下几步完成的。 (1) StandardScaler执行fit和transform方法; (2) 将转换后的数据输入PCA; (3) PCA同样执行fit和transform方法; (4) 最后将数据输入LogisticRegression,训练一个LR模型。 对于管道来说,中间有多少个transformer都是可以的,这样就极大地简化了原本复杂的操作。管道的具体工作流程如图5.3所示。 图5.3管道的工作流程 Pipeline类的Pipeline函数可以把多个“处理数据的结点”按顺序打包在一起,数据将前一结点处理之后的结果,转到下一结点继续处理。当训练样本数据送进Pipeline进行处理时,它会逐个调用结点的fit和transform方法,最终用最后一个结点的fit方法来拟合数据。使用过程中要注意的是,管道执行fit方法,而transformer要执行fit_transform。 本案例Pipeline的代码实现如下。 1In[4]: 2from sklearn.preprocessing import StandardScaler 3# 用于数据标准化 4from sklearn.decomposition import PCA # 用于特征降维 5from sklearn.linear_model import LogisticRegression 6# 用于模型预测 7from sklearn.pipeline import Pipeline 8pipeline = Pipeline([('scl', StandardScaler()), ('pca', PCA(n_components=2)), ('clf', LogisticRegression(random_state=1))]) 9pipeline.fit(X_train, y_train) 10print('Accuracy: %.3f' % pipeline.score(X_test, y_test)) 11y_pred = pipeline.predict(X_test) 12Out[4]: 13Accuracy: 0.959 微课视频 5.1.2通用的管道接口 Pipeline类不但可用于预处理和分类,实际上还可以将任意数量的估计器连接在一起。例如,可以构建一个包含特征提取、特征选择、缩放和分类的管道,总共4个步骤。当然,最后一步可以用回归或聚类代替分类。 对于管道中估计器的唯一要求就是,除了最后一步之外的所有步骤都需要具有transform方法,这样它们可以生成新的数据表示,以供下一个步骤使用。在调用Pipeline.fit的过程中,管道内部依次对每个步骤调用fit和transform方法,其输入是前一个步骤中transform方法的输出。对于管道中的最后一步,则仅调用fit。 1. 用make_pipeline创建管道 利用传统语法创建管道较为麻烦,Pipeline类提供了一个很方便的函数make_pipeline,可以创建管道并根据每个步骤所属的类为其自动命名。如果多个步骤属于同一类,则会自动附加一个数字。 1In[5]: 2from sklearn.pipeline import make_pipeline 3from sklearn.preprocessing import StandardScaler 4from sklearn.decomposition import PCA 5# 创建管道 6pipe = make_pipeline(StandardScaler(), PCA(n_components=2), StandardScaler()) 7print(pipe.steps) 8Out[5]: 9[('standardscaler-1', StandardScaler(copy=True, with_mean=True, with_std=True)), ('pca', PCA(copy=True, iterated_power='auto', n_components=2, random_state=None, svd_solver='auto', tol=0.0, whiten=False)), ('standardscaler-2', StandardScaler(copy=True, with_mean=True, with_std=True))] 可以清楚地看到,管道中的两个StandardScaler同属一个类,所以被自动命名为standardscaler1和standardscaler2。 2. 访问步骤属性 Pipeline类对象的步骤属性是由元组组成的步骤列表;如果想访问管道中某一步骤的属性,比如访问上述PCA提取的成分,最简单的方法是通过named_steps 属性,它是一个字典,将步骤名称映射为估计器。 1In[6]: 2# 访问步骤属性 3pipe.fit(bcdf) 4components = pipe.named_steps["pca"].components_ 5#从PCA步骤中提取两个主成分 6print(components.shape) 7Out[6]: 8(2, 30) 3. 访问网格搜索管道中的属性 使用管道主要是为了进行网格搜索。较为常见的任务是在网格搜索内访问管道的某些步骤。 下面举一个例子,假如要对威斯康星乳腺癌数据集上的LogisticRegression分类器进行网格搜索。 1In[7]: 2from sklearn.preprocessing import StandardScaler 3# 进行数据标准化 4from sklearn.model_selection import GridSearchCV 5# 网格搜索 6from sklearn.linear_model import LogisticRegression 7# 逻辑回归模型预测 8# 构建管道 9pipe = make_pipeline(StandardScaler(), LogisticRegression()) 10# 对参数C在0.01至100之间进行搜索 11param_grid = {"logisticregression__C":[0.01, 0.1, 1, 10, 100]} 12# 在数据集上对网格搜索进行拟合 13grid = GridSearchCV(pipe, param_grid, cv=5) 14grid.fit(X_train, y_train) 15# 输出最优估计器 16print(grid.best_estimator_) 17Out[7]: 18Pipeline(memory=None,steps=[('standardscaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('logisticregression', LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,intercept_scaling=1, max_iter=100, multi_class='ovr',n_jobs=1,penalty='l2', random_state=None, solver='liblinear',tol=0.0001,verbose=0, warm_start=False))]) 从输出结果可以明显地看出,best_estimator_本质上是一个管道,它包含两个步骤: StandardScaler和LogisticRegression。 接下来可以使用管道的named_steps 属性来访问LogisticRegression步骤。 1In[8]: 2# 访问网格搜索管道中的属性 3print(grid.best_estimator_.named_steps["logisticregression"]) 4Out[8]: 5LogisticRegression(C=10) 6#获取LogisticRegression实例后,就可以访问与每个输入特征相关的系数了 7In[9]: 8# 访问输入特征相关的系数 9print(grid.best_estimator_.named_steps["logisticregression"].coef_) 10Out[9]: 11[[ 0.450856950.727995470.435051060.51008668-0.06872988-0.35212576 120.908283960.5220248 0.09572507-0.4305921 1.14523641-0.32090688 130.803497860.860302330.03973987-0.568467320.203591270.0297218 14-0.44116833-0.787507820.800013341.307211310.705077770.81499364 151.014918810.225659121.156324530.712589931.057548290.06141073]] 5.2交叉验证 交叉验证(CrossValidation)主要用于防止模型过于复杂而引起的过拟合,是一种评价数据集泛化能力的统计方法。其基本思想是将原始数据划分为训练集(Train_Set)和测试集(Test_Set)。训练集用来对模型进行训练,测试集用来测试训练得到的模型,以此作为模型的评价指标。 交叉验证比单次划分训练集和测试集的方法更加稳定,在交叉验证中,数据被多次划分,并且需要训练多个模型。最常用的交叉验证是K折交叉验证(KFold CrossValidation)以及分层K折交叉验证(Stratified KFold Cross Validation)。 微课视频 5.2.1K折交叉验证 1. K折交叉验证的原理 K折交叉验证(KFold CrossValidation)将数据集等比例划分成K份,每份叫作折(Fold)。以其中的一份作为测试集,其他的K-1份数据作为训练集,这样就完成了一次验证。因此,K折交叉验证只有实验K次才算完整地完成,也就是说K折交叉验证实际是把验证重复做了K次,每次验证都是从K份选取一份不同的部分作为测试集(从而保证K份的数据都分别做过测试集),剩下的K-1份当作训练集,最后取K次准确率的平均值作为最终模型的评价指标。 K折交叉验证可以有效避免过拟合和欠拟合状态的发生,K值的选择可以根据实际情况调节。 图5.4展示了K=5时K折交叉验证的完整过程。 图5.45折交叉验证 下面举例来模拟一下图5.4的过程,首先导入所需模块,X测试集中有10个数据,然后调用K折交叉验证函数,这里设置参数n_splits为5,即进行5折交叉验证,最后用一个循环输出每一折的训练集和测试集的划分。 1In[10]: 2from sklearn.model_selection import KFold 3X=['a','b','c','d','e','f','g','h','i','j'] 4kf = KFold(n_splits=5) 5for train,test in kf.split(X): 6print("训练集%s---测试集%s" %(train,test)) 7Out[10]: 8训练集[2 3 4 5 6 7 8 9]---测试集[0 1] 9训练集[0 1 4 5 6 7 8 9]---测试集[2 3] 10训练集[0 1 2 3 6 7 8 9]---测试集[4 5] 11训练集[0 1 2 3 4 5 8 9]---测试集[6 7] 12训练集[0 1 2 3 4 5 6 7]---测试集[8 9] 2. K折交叉验证的实现 实际使用的时候没必要像上面那样写,因为sklearn已经封装好了相关方法,可以直接去调用这些方法,根据K折交叉验证的原理,选取K=10时,在威斯康星乳腺癌数据集上K折交叉验证的代码实现如下。 代码清单52: 交叉验证 1In[11]: 2# 使用K-Fold交叉验证来评估模型性能 3import numpy as np 4from sklearn.model_selection import cross_val_score 5scores = cross_val_score(estimator=pipe, 6X=X_train, 7y=y_train, 8cv=10, 9n_jobs=1) 10#输出K折交叉验证的得分 11print('CV accuracy scores: %s' % scores) 12#用np.mean()来计算均值,用np.std()来计算标准差 13print('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores))) 14Out[11]: 15CV accuracy scores: [0.9750.950.9751.1.1.1.1. 0.97435897 1.] 16CV accuracy: 0.987 +/- 0.017 其中,pipe是5.1节创建的管道,X_train,y_train是5.1节划分好的训练集,参数CV表示折数即K值。 微课视频 5.2.2分层K折交叉验证 如图5.5所示,K折交叉验证每次划分时对数据进行均分,假如数据集有3类,抽取出来的也正好是按照类别划分的3类,也就是说第一折的标签全是0类,第二折全是1类,第三折全是2类。这样划分数据集导致的后果是训练模型时没有学习到测试集中数据的特点,从而导致模型得分很低。 图5.5K折交叉验证 下面来模拟图5.5的过程,首先导入所需模块,X测试集中有9个数据,然后调用K折交叉验证函数,并设置参数n_splits为3,即进行3折交叉验证,最后用一个循环输出每一折的训练集和测试集的划分。 1In[12]: 2from sklearn.model_selection import KFold 3X=['a','b','c','d','e','f','g','h','i'] 4y=[1,1,1,2,2,2,3,3,3] 5kf = KFold(n_splits=3) 6for train,test in kf.split(X,y): 7print("训练集%s---测试集%s" %(train,test)) 8Out[12]: 9训练集[3 4 5 6 7 8]---测试集[0 1 2] 10训练集[0 1 2 6 7 8]---测试集[3 4 5] 11训练集[0 1 2 3 4 5]---测试集[6 7 8] 从第一次划分来看,并没有学习到测试集[0 1 2]中数据的特点,第二次划分也没有学习到测试集[3 4 5]中数据的特点,最后一次划分同样也没有学习到测试集[6 7 8]中数据的特点。 为了避免K折交叉验证出现的上述情况,又出现了以下几种交叉验证方式。 1. 分层K折交叉验证 分层K折交叉验证(Stratified KFold Cross Validation)同样属于交叉验证类型,分层的意思是在每一折中都保持着原始数据中各个类别的比例关系,比如: 原始数据有3类,比例为1∶2∶3,采用3折分层交叉验证,那么划分的3折中,每一折中的数据类别保持着1∶2∶3的比例,因为这样的验证结果更加可信,如图5.6所示。 图5.6分层K折交叉验证 下面来模拟一下图5.6的过程,首先导入所需模块,X测试集中有9个数据,然后调用StratifiedKFold即分层K折交叉验证函数,并设置参数n_splits为3,即进行3折分层交叉验证,最后用一个循环输出每一折的训练集和测试集的划分。 1In[13]: 2from sklearn.model_selection import StratifiedKFold 3X=['a','b','c','d','e','f','g','h','i'] 4y=[1,1,1,2,2,2,3,3,3] 5skf = StratifiedKFold(n_splits=3) 6for train,test in skf.split(X,y): 7print("训练集%s---测试集%s" %(train,test)) 8Out[13]: 9训练集[1 2 4 5 7 8]---测试集[0 3 6] 10训练集[0 2 3 5 6 8]---测试集[1 4 7] 11训练集[0 1 3 4 6 7]---测试集[2 5 8] 从结果可以清楚地看到,与K折交叉验证不同的是,分层K折交叉验证测试集每一次的划分都是从每个类别中各取一个数据,即每一折中的数据类别保持着1∶1∶1的比例,这样就可以充分学习到每一类中数据的特点,从而提高了模型的得分。 2. 留一法交叉验证 留一法交叉验证(Leave One Out CrossValidation)是一种特殊的交叉验证方式。顾名思义,如果样本容量为n,则K=n,进行n折交叉验证,每次留下一个样本进行验证。由于每一折中几乎所有的样本皆用于训练模型,因此最接近原始样本的分布,这样评估所得的结果比较可靠。但其缺点也很明显,就是比较耗时,因此适合于数据集比较小的场合。 1In[14]: 2from sklearn.model_selection import LeaveOneOut 3X=['a','b','c','d','e','f','g','h','i','j'] 4y=[1,1,1,2,2,2,3,3,3,3] 5loo=LeaveOneOut() 6for train,test in loo.split(X,y): 7print("训练集%s---测试集%s" %(train,test)) 8Out[14]: 9训练集[1 2 3 4 5 6 7 8 9]---测试集[0] 10训练集[0 2 3 4 5 6 7 8 9]---测试集[1] 11训练集[0 1 3 4 5 6 7 8 9]---测试集[2] 12训练集[0 1 2 4 5 6 7 8 9]---测试集[3] 13训练集[0 1 2 3 5 6 7 8 9]---测试集[4] 14训练集[0 1 2 3 4 6 7 8 9]---测试集[5] 15训练集[0 1 2 3 4 5 7 8 9]---测试集[6] 16训练集[0 1 2 3 4 5 6 8 9]---测试集[7] 17训练集[0 1 2 3 4 5 6 7 9]---测试集[8] 18训练集[0 1 2 3 4 5 6 7 8]---测试集[9] 3. 打乱划分交叉验证 打乱划分交叉验证(ShuffleSplit CrossValidation)是一种非常灵活的交叉验证。该方法控制更为灵活,可以控制每次划分时训练集和测试集的比例(通过train_size和test_size来控制),以及划分迭代次数(通过n_splits来控制)。这种灵活的控制,甚至可以存在数据既不在训练集也不在测试集的情况。 1In[15]: 2from sklearn.model_selection import ShuffleSplit 3X=['a','b','c','d','e','f','g','h','i','j'] 4ssp = ShuffleSplit(test_size=.4, train_size=.4, n_splits=5) 5for train,test in ssp.split(X): 6print("训练集%s---测试集%s" %(train,test)) 7Out[15]: 8训练集[8 0 4 6]---测试集[7 1 9 2] 9训练集[6 0 4 5]---测试集[8 1 9 7] 10训练集[0 2 4 6]---测试集[3 1 5 8] 11训练集[9 3 1 7]---测试集[6 5 0 2] 12训练集[5 0 8 3]---测试集[1 2 9 7] 从第一次划分可以看出,3和5既不在训练集也不在测试集中。 4. 分组交叉验证 分组交叉验证(Group CrossValidation)适用于数据中的分组高度相关的情况,即组内的各个变量之间不是独立的,而组间是独立的,也就是说测试集中的样本组别不能来自训练集中样本的组别。这种例子常见于医疗应用,可能拥有来自同一名病人的多个样本,但想要将其泛化到新的病人。 下面这个示例用到了一个由groups数组指定分组的模拟数据集。这个数据集包含了6个数据样本,groups指定了该数据样本所属的分组,共分为2组,其中前2个样本为一组,后4个样本为一组,划分迭代次数为2。 1In[16]: 2from sklearn.model_selection import GroupKFold 3X=['a','b','c','d','e','f'] 4y=[0,0,1,1,1,1] 5groups=[1,1,2,2,2,2] 6gkf = GroupKFold(n_splits=2) 7for train,test in gkf.split(X, y, groups=groups): 8print("训练集%s---测试集%s" %(train,test)) 9Out[16]: 10训练集[0 1]---测试集[2 3 4 5] 11训练集[2 3 4 5]---测试集[0 1] 5.3模型评价指标 对于一个模型来说,如何评价该模型的好坏,针对不同的问题需要不同的模型评价标准,也就是评估模型的泛化能力,这是机器学习中的一个关键性的问题。具体来讲,评价指标有两个作用: 首先了解模型的泛化能力,可以通过同一个指标来对比不同模型,从而知道哪个模型相对较好,哪个模型相对较差; 其次可以通过这个指标来逐步优化模型。因此,在选择模型与调参时,选择正确的指标是很重要的。本节将主要介绍分类模型的各种评价指标以及ROC和AUC。 5.3.1误分类的不同影响 误分类是指将被调查对象的特征错误地分到原本不属于它的类别中。例如将一个新型冠状病毒肺炎(Corona Virus Disease 2019,COVID19)患者错误地诊断为健康人,这种误分类导致该患者不能得到及时的治疗,严重一点的话可能导致患者的死亡。把这种错误的阳性预测叫假阳性(False Positive),这种错误属于第一类错误(Type I Error); 相反,一个健康的人被错误的诊断为新型冠状病毒肺炎,不但会给患者造成不必要的物质上的损失,更重要的是会给患者带来精神上的极大痛苦。把这种错误的阴性预测叫假阴性(False Negative),这种错误属于第二类错误(Type Ⅱ Error)。 所有的分类器都存在偏好,因此都存在误分类的现象,但是可以通过调整分类器的阈值,比如高的召回率或者高的准确率。这样就可以通过设定分类器的阈值来避免不同类型的错误,这样模型才有实际的应用价值。 5.3.2混淆矩阵 混淆矩阵(Confusion matrix)是表示精度评价的一种标准格式,用n行n列的矩阵来表示。混淆矩阵是总结分类模型预测结果的情形分析表,以矩阵形式将数据集中的记录按照真实的类别与分类模型预测的类别进行汇总。其中矩阵的行表示真实值,矩阵的列表示预测值。 在学习混淆矩阵之前,首先明确几个分类评估指标中相关符号的含义: 1. 真阳性(True Positive,TP): 将正类预测为正类的次数,即真实值和预测值均为1的次数。 2. 假阳性(False Positive,FP): 将负类预测为正类的次数,即真实值为0,而预测值为1的次数。 3. 真阴性(True Negative,TN): 将负类预测为负类的次数,即真实值和预测值均为0的次数。 4. 假阴性(False Negative,FN): 将正类预测为负类的次数,即真实值为1,而预测值为0的次数。 下面先以二分类为例,看下混淆矩阵的表现形式,如表5.1所示。 表5.1二分类的混淆矩阵 真实值预测值 负类(N)正类(P) 负类(N)真阴性(TN)假阳性(FP) 正类(P)假阴性(FN)真阳性(TP) 也可以画出二分类的混淆矩阵的解释图,如图5.7所示。 代码清单53: 模型评价指标 1In[17]: 2import mglearn 3mglearn.plots.plot_binary_confusion_matrix() 4Out[17]: 图5.7二分类混淆矩阵 在Scikitlearn中,提供了混淆矩阵函数sklearn.metrics.confusion_matrix的API接口,可以用于绘制混淆矩阵,如下所示。 1sklearn.metrics.confusion_matrix( 2y_true, # 样本真实的分类标签列表 3y_pred, # 样本预测的分类结果列表 4labels=None, # 给出的类别,通过这个可对类别进行选择 5sample_weight=None # 样本权重 6) 首先举个简单的例子。 1In[18]: 2from sklearn.metrics import confusion_matrix 3y_true=[0, 1, 0, 1] 4y_pred=[1, 1, 1, 0] 5print(confusion_matrix(y_true, y_pred)) 6Out[18]: 7[[0 2] 8 [1 1]] 输出了一个2×2的矩阵,该矩阵代表的含义如下。 第一行的0,即真阴性(TN),表示将真实值0预测为0的次数,很显然,真实值里面有两个0,但都预测错误(预测为1)了,因此TN的值是0。第一行的2,即假阳性(FP),表示将真实值0错误预测为1的次数,很显然是2次,因此FP的值是2。第二行前面的1,即假阴性(FN),表示将1错误预测为0的次数,很明显是1次,因此FN的值是1; 第二行后面的1,即真阳性(TP),表示将1预测正确的次数,很明显也是1次,所以TP的值是1。 也可以通过confusion_matrix直接得到TN、FP、FN、TP这四个值,如下所示。 1In[19]: 2TN, FP, FN, TP = confusion_matrix([0, 1, 0, 1], [1, 1, 1, 0]).ravel() 3print(TN, FP, FN, TP) 4Out[19]: 50 2 1 1 理解了二分类的混淆矩阵,接下来来看多分类模型的混淆矩阵,以Scikitlearn官方所提供的例子为例。 1In[20]: 2from sklearn.metrics import multilabel_confusion_matrix # sklearn version >= 0.21 3y_true = ["cat", "ant", "cat", "cat", "ant", "bird"] 4y_pred = ["ant", "ant", "cat", "cat", "ant", "cat"] 5mcm = multilabel_confusion_matrix(y_true, y_pred,labels=["ant", "bird", "cat"]) 6mcm 7Out[20]: 8array([[[3, 1], 9[0, 2]], 10[[5, 0], 11[1, 0]], 12[[2, 1], 13[1, 2]]], dtype=int64) 本例显然是个三分类问题,mcm可以看作返回了三个二分类混淆矩阵,在每一个二分类混淆矩阵中,如图5.7所示,TN在[0, 0],FP在[0, 1],FN在[1, 0],TP在[1,1]。 以第一个类别ant为例,正样本预测对的有2次,正样本预测错的是0次,它的负样本(bird、cat)预测成正样本的有1次,负样本预测对的有3次(其中bird预测成cat,也算对,因为它们都是负样本)。 如果对官方的例子还不明白的话,接下来将上面的混淆矩阵可视化,最简单的方法是使用seaborn的热力图。热力图是机器学习数据可视化比较常用的显示方式,它通过颜色变化程度以直观反映出热点分布、区域聚集等相关数据信息。 代码如下,其输出如图5.8所示。 1In[21]: 2import pandas as pd 3import seaborn as sns 4from sklearn.metrics import confusion_matrix 5y_true = ["cat", "ant", "cat", "cat", "ant", "bird"] 6y_pred = ["ant", "ant", "cat", "cat", "ant", "cat"] 7cm=confusion_matrix(y_true, y_pred, labels=["ant", "bird", "cat"]) 8df=pd.DataFrame(cm,index=["ant", "bird", "cat"],columns=["ant", "bird", "cat"]) 9sns.heatmap(df,annot=True) 10Out[21]: 图5.8热力图绘制的混淆矩阵 正如前面所讲,从图5.8中可以明显地看出: 混淆矩阵的每一行数据之和代表该类别真实的数目,每一列之和代表该类别预测的数目,矩阵的对角线上的数值代表被正确预测的样本数目。 纵轴的标签表示真实属性,而横轴的标签表示分类的预测结果。以此热力图的第一行第一列这个数字2为例,它表示ant被成功分类成为ant的样本数目; 同理,第三行第一列的数字1表示cat被分类成ant的样本数目,以此类推。 5.3.3分类的不确定性 在开始深入探究如何使用不确定性来调试和解释模型之前,先来理解为什么不确定性如此重要。 1. 分类的不确定性 一个显著的例子就是高风险应用。假设你正在设计一个模型,用以辅助医生决定对患者的最佳治疗方案。在这种情况下,不仅要关注模型预测结果的准确性,还要关注模型对预测结果的确定性程度。如果结果的不确定性过高,那么医生应该慎重考虑。自动驾驶汽车是另外一个有趣的例子。当模型不确定道路上是否有行人时,可以使用此信息来减慢车速或者触发警报,便于驾驶员进行处理。 2. 不确定度指标 不确定度指标实际上反映了数据依赖于模型和参数的不确定程度。Scikitlearn接口中有两个函数可以用于获取分类器的不确定度估计,即分类器预测某个测试点属于某个类别的置信程度。这两个函数分别是decision_function和predict_proba。 下面对这两个函数进行简单的介绍: 1) decision_function 对于二分类的情况,decision_function(决策函数)的输出可以在任意范围取值,返回值的形状是n_samples,注意这是一个一维数组,它为每个样本都返回一个浮点数,正值表示对正类(classes_属性的第二个元素)的置信程度,负值表示对负类(classes_属性的第一个元素)的置信程度,绝对值越大表示置信度越高。 对于多分类情况,decision_function返回值的形状是(n_samples,n_classes),每一列对应每个类别的“确定性分数”,分数较高的类别可能性更大。 2) predict_proba predict_proba(预测概率)的输出是每个类别的概率,总是为0~1,两个类别的元素(概率)之和始终为1。不管是二分类还是多分类,predict_proba函数返回值的形状均为n_samples,n_classes。 下面以SVM二分类为例,来分析一下结果。 1In[22]: 2import numpy as np 3from sklearn.svm import SVC 4 5X = np.array([[1,2,3], 6[1,3,4], 7[2,1,2], 8[4,5,6], 9[3,5,3], 10[1,7,2]]) 11 12y = np.array([3, 3, 3, 2, 2, 2]) 13 14clf = SVC(probability=True) 15clf.fit(X, y) 16print("decision_function: \n",clf.decision_function(x)) 17print("predict:",clf.predict(x)) 18print("predict_proba: \n",clf.predict_proba(x)) 19print("classes_: ",clf.classes_) 20Out[22]: 21decision_function: 22 [ 1.000890360.644936010.97960658-1.00023781 -0.9995244-1.00023779] 23predict: [3 3 3 2 2 2] 24predict_proba: 25[[0.091579720.90842028] 26[0.204352020.79564798] 27[0.096332530.90366747] 28[0.948588030.05141197] 29[0.948493930.05150607] 30[0.948588030.05141197]] 31classes_:[2 3] 如前面所讲,二分类问题的decision_function函数返回结果的形状与样本数量相同,且返回结果的数值表示模型预测样本属于正类的可信度。decision_function函数返回的浮点数是可以带符号的,大于0表示正类的可信度大于负类,否则表示正类的可信度小于负类。 所以对于前3个样本,decision_function都认为是正类的可信度高(大于0),后3个样本是负类的可信度高(小于0)。 接下来看一下predict函数的结果,前3个预测为正类3,后3个样本预测为负类2。 再看一下predict_proba函数预测的样本所属的类别概率,可以看到前3个样本属于类别3(正类)的概率更大,后3个样本属于类别2(负类)的概率更大。两个类别的概率之和始终为1,即每个列表中两个元素之和是1。 最后,二分类情况下classes_中的第一个标签“2”代表负类,第二个标签“3”代表正类。 微课视频 5.3.4准确率召回率曲线 在二分类混淆矩阵中,可以很容易地看出,主对角线上(TN与TP)是全部预测正确的,副对角线上(FN与FP)是全部预测错误的。因此当得到模型的混淆矩阵后,就需要去观察对角线上的数量是否大,副对角线上的数量是否小。但是,混淆矩阵里面统计的是个数,有时候面对大量的数据,仅仅算个数,很难衡量模型的优劣。因此混淆矩阵在基本的统计结果上又延伸了如下几个指标。 1. 精确率 精确率(Accuracy)即分类正确的样本数占总样本的比例。 Accuracy=TP+TNTP+TN+FP+FN(51) 以5.3.2节的例子为例,来计算一下它的Accuracy。 1In[23]: 2from sklearn.metrics import confusion_matrix 3y_test=[0, 1, 0, 1] 4y_predict=[1, 1, 1, 0] 5TN, FP, FN, TP = confusion_matrix(y_test, y_predict).ravel() 6print(TN, FP, FN, TP) 7Out[23]: 80 2 1 1 9In[24]: 10from sklearn.metrics import accuracy_score 11accuracy = accuracy_score(y_test, y_predict) 12print("Accuracy =",accuracy) 13Out[24]: 14Accuracy= 0.25 很明显Accuracy=1+01+0+2+1=0.25。 一般来说系统的精确率越高,性能越好。但是,对于正负样本数量极不均衡的情况,只通过精确率往往难以反映出系统的真实性能。比如,对于一个地震预测系统,假设所有样本中,1000天中有1天发生地震(0: 不地震,1: 地震),分类器不假思索地将所有样本分类为0,即可得到99.99%的精确率,但当地震真正来临时,并不能成功预测,这种结果是我们不能接受的。 2. 准确率 准确率(Precision)又叫查准率,其含义为预测为真的样本中实际为真的样本的占比。 Precision=TPTP+FP(52) 1In[25]: 2from sklearn.metrics import precision_score 3precision = precision_score(y_test, y_predict) 4print("Precision=",precision) 5Out[25]: 6Precision= 0.3333333333333333 很明显Precision=11+2≈0.3333333333333333。 3.召回率 召回率(Recall)也叫敏感性(Sensitivity),又叫查全率。这个指标表示正样本中预测对的占总正样本的比例。顾名思义,“查全”表明预测为真覆盖到了多少实际为真的样本,换句话说遗漏了多少。 Recall=TPTP+FN(53) 1In[26]: 2from sklearn.metrics import recall_score 3recall = recall_score(y_test, y_predict) 4print("Recall=",recall) 5Out[26]: 6Recall= 0.5 很明显Recall=11+1=0.5。 准确率和召回率是此消彼长的,即准确率提高了,召回率就会降低。因此,在不同的应用场景下,我们对准确率和召回率的关注点不同。例如,在假币预测、股票预测的时候,我们更关心的是准确率; 而在地震预测、肿瘤预测的时候,我们更关注召回率,即地震或患病时预测错了的情况应该越少越好。 4. F1值 F1值(F1Score)是用来衡量分类模型综合性能的一种指标。它同时兼顾了分类模型的准确率和召回率。F1值可以看作模型准确率和召回率的调和平均数,它的最大值是1,最小值是0。 调和平均数的性质是,当准确率和召回率二者都非常高的时候,它们的调和平均才会高。如果其中之一很低,调和平均就会被拉得接近于很低的那个数。 F1=2×Precision×RecallPrecision+Recall(54) 1In[27]: 2from sklearn.metrics import f1_score 3f1_Score=f1_score(y_test, y_predict) 4print("F1-Score=",f1_Score) 5Out[27]: 6F1-Score= 0.4 很明显F1=2×0.3333333333333333×0.50.3333333333333333+0.5≈0.4。 5. 准确率召回率曲线 如上所述,准确率召回率(查准率查全率)是一对相互矛盾的性能指标,而事实上我们期望的是既能保证查准率,又能提升查全率。 准确率召回率曲线(PrecisionRecall曲线,PR曲线)也叫 查准率查全率曲线。对分类问题来讲,通过不断调整分类器的阈值,可以得到不同的PrecisionRecall值,遍历所有可能的阈值,从而可以得到一条曲线(纵坐标为Precision,横坐标为Recall)。通常随着分类阈值从大到小变化,查准率减小,而查全率增加,如图5.9所示。 图5.9PR曲线 从图5.9中可以看出,查准率和查全率是一对相互矛盾的性能指标,比较两个分类器优劣时,显然是查得又准又全的比较好,也就是PR曲线往坐标(1,1)的位置越靠近越好。 通常情况下,PR曲线在分类、检索等领域有着广泛的使用,用来表现分类、检索的性能。比如做病患检测、垃圾邮件过滤时,在保证查准率的前提下,提升查全率。 Scikitlearn提供的接口precision_recall_curve函数是用来绘制PR曲线,其函数原型如下。 1sklearn.metrics.precision_recall_curve( 2y_true, # y_true是在范围{0,1}或{-1,1}中真实的二进制标签 3probas_pred, #array, shape = [n_samples] 估计概率或决策函数 4pos_label=None, # 正类样本标签 5sample_weight=None # 采样权重 6) 返回值为Precision、Recall和 thresholds(所选阈值)。 接下来,使用precision_recall_curve函数来画出PR曲线,如图5.10所示。 1In[28]: 2import matplotlib 3import numpy as np 4import matplotlib.pyplot as plt 5from sklearn.metrics import precision_recall_curve 6 7#y_true为样本实际的类别,y_scores为样本为正例的概率 8y_true = np.array([1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0]) 9y_scores = np.array([0.9, 0.75, 0.86, 0.47, 0.55, 0.56, 0.74, 0.62, 0.5, 0.86, 0.8, 0.47, 0.44, 0.67, 0.43, 0.4, 0.52, 0.4, 0.35, 0.1]) 10 11plt.figure("P-R Curve") 12plt.title('Precision/Recall Curve') 13plt.xlabel('Recall') 14plt.ylabel('Precision') 15 16precision, recall, thresholds = precision_recall_curve(y_true, y_scores) 17plt.plot(recall,precision) 18plt.show() 19Out[28]: 图5.10PR曲线 微课视频 5.3.5受试者工作特征(ROC)与AUC 1. 受试者工作特征 受试者工作特征曲线(Receiver Operator Characteristic Curve, ROC曲线)通常被用来评价一个二值分类器的优劣。ROC曲线的横坐标是假阳性率(False Positive Rate,FPR),纵坐标是真阳性率(True Positive Rate,TPR)。 TPR表示在所有实际为阳性的样本中,被正确地判断为阳性的比率,即TPR=TPTP+FN。而FPR表示在所有实际为阴性的样本中,被错误地判断为阳性之比率,即FPR=FPFP+TN。 TPR越高,FPR越低,则可以证明分类器分类效果越好。但是两者又是相互矛盾的,所以单凭TPR和FPR的两个值是没有办法比较两个分类器的好坏的,因此提出了ROC曲线。 对某个分类器而言,可以根据其在测试样本上的表现得到一个TPR和FPR点对。这样,此分类器就可以映射为ROC平面上的一个点。调整这个分类器在分类时使用的阈值,就可以得到一个经过(0, 0)和(1, 1)的曲线,该曲线就是此分类器的ROC曲线。如图5.11所示。 图5.11ROC曲线 一般情况下,这个曲线都应该处于(0, 0)和(1, 1)连线的上方。因为(0, 0)和(1, 1)连线形成的ROC曲线实际上代表的是一个随机分类器。如果很不幸得到一个位于此直线下方的分类器的话,一个直观的补救办法就是把所有的预测结果反向处理。 ROC曲线反映出TPR的增加以FPR的增加为代价,最靠近坐标左上方的点为TPR和FPR均较高的临界值,ROC曲线下的面积是模型准确率的度量。 Sklearn提供的接口roc_curve函数用来绘制ROC曲线。 1fpr, tpr, thresholds=sklearn.metrics.roc_curve( 2y_true, # y_true是在范围{0,1}或{-1,1}中真实的二进制标签。如果标签不是二进制的,则应该显式地给出pos_label 3y_score, # 预测得分 4pos_label=None, # 正类样本标签 5sample_weight=None, # 即采样权重,可选取其中的一部分进行计算 6drop_intermediate=True # 即可选择去掉一些对于ROC性能不利的阈值,使得得到的曲线有更好的表现性能 7) 其中,返回值thresholds是所选择的不同的阈值,将预测结果scores按照降序排列。 接下来以Scikitlearn官网给出的例子为例,代码如下。 1In[29]: 2import numpy as np 3from sklearn import metrics 4y = np.array([1, 1, 2, 2]) 5scores = np.array([0.1, 0.4, 0.35, 0.8]) 6fpr, tpr, thresholds = metrics.roc_curve(y, scores, pos_label=2) # 负类标签为1,正类为2 7print("FPR:",fpr) 8print("TPR:",tpr) 9print("thresholds:",thresholds) 10Out[29]: 11FPR: [0. 0. 0.5 0.5 1. ] 12TPR: [0. 0.5 0.5 1. 1. ] 13thresholds: [1.8 0.8 0.4 0.35 0.1 ] thresholds为将预测结果scores从大到小排列的结果。 最后画出ROC曲线,如图5.11所示。 1In[30]: 2import matplotlib.pyplot as plt 3 4plt.plot(fpr,tpr,marker = 'o') 5plt.xlabel('FPR') 6plt.ylabel('TPR') 7plt.title('Receiver Operating Characteristic Example') 8plt.show() 9Out[30]: 2. AUC AUC(Area Under ROC Curve)是一种用来度量分类模型好坏的一个标准,假设分类器的输出是样本属于正类的score(置信度),则AUC的物理意义为,任取一对(正、负)样本,正样本的score大于负样本的score的概率。AUC值为ROC曲线所覆盖的区域面积。显然,AUC越大,分类器分类效果越好。 AUC=1是完美分类器。采用这个预测模型时,不管设定什么阈值都能得出完美预测。但绝大多数预测的场合,不存在完美分类器。 0.5<AUC<1,优于随机猜测,若妥善设定阈值,分类器将具有预测价值。 AUC=0.5,跟随机猜测一样,模型没有预测价值。 AUC<0.5,比随机猜测还差,但只要总是反预测而行,就优于随机猜测。 Scikitlearn计算ROC曲线下面积AUC有两种方法。 1) AUC函数 使用sklearn.metrics.auc函数,它是一个通用方法,根据梯形规则计算曲线下面积,其函数原型如下。 1sklearn.metrics.auc( 2x, # x坐标,即FPR,它必须是单调递增或单调递减的 3y, # y坐标,即TPR 4reorder=False #默认值 5) 下面用AUC函数来计算图5.11所示ROC曲线下的面积。 1In[31]: 2import numpy as np 3from sklearn import metrics 4y = np.array([1, 1, 2, 2]) 5pred = np.array([0.1, 0.4, 0.35, 0.8]) 6fpr, tpr, thresholds = metrics.roc_curve(y, pred, pos_label=2) 7metrics.auc(fpr, tpr) 8Out[31]: 90.75 2) roc_auc_score函数 使用sklearn.metrics.roc_auc_score,根据预测得分计算接受者工作特征曲线(ROC曲线)下的面积,其函数原型如下: 1sklearn.metrics.roc_auc_score( 2y_true, #真实的标签 3y_score, #预测得分 4average='macro', #{‘micro’, ‘macro’, ‘samples’, ‘weighted’} or None, default=’macro’ 5sample_weight=None, #样本权重 6max_fpr=None, #float,> 0 and <= 1, default=None 7multi_class='raise', #{‘raise’, ‘ovr’, ‘ovo’}, default=’raise’ 8labels=None # 数组的形状(n_classes,), default=None 9) 下面用roc_auc_score来计算图5.11所示ROC曲线下的面积。 1In[32]: 2import numpy as np 3from sklearn.metrics import roc_auc_score 4y_true = np.array([0, 0, 1, 1]) 5y_scores = np.array([0.1, 0.4, 0.35, 0.8]) 6roc_auc_score(y_true, y_scores) 7Out[32]: 80.75 最后,以威斯康星乳腺癌二分类数据集为例,结合5.2节的交叉验证以及ROC与AUC,画出ROC曲线并求出面积AUC,完整代码如下,输出结果如图5.12所示。 1In[33]: 2#导入所需要的模块 3import pandas as pd 4import numpy as np 5import matplotlib.pyplot as plt 6from sklearn import svm 7from sklearn.metrics import roc_curve, auc 8from sklearn.model_selection import StratifiedKFold 9 10#导入威斯康星乳腺癌数据集并进行预处理 11bcdf = pd.read_csv('wdbc.csv') 12bcdf.drop(['id','Unnamed: 32'], axis = 1, inplace = True) 13bcdf['diagnosis'] = bcdf['diagnosis'].map({'M':1, 'B':0}) 14X = bcdf 15y = bcdf.diagnosis 16bcdf.drop('diagnosis', axis = 1, inplace = True) 17n_samples, n_features = X.shape 18 19# 增加噪声特征 20random_state = np.random.RandomState(0) 21X = np.c_[X, random_state.randn(n_samples, 200 * n_features)] 22 23cv = StratifiedKFold(n_splits=5) #利用分层K折交叉验证,将数据划分为5份 24# SVC模型 25classifier = svm.SVC(kernel='linear', probability=True,random_state=random_state) 26# 画平均ROC曲线的两个参数 27mean_tpr = 0.0 # 用来记录画平均ROC曲线的信息 28mean_fpr = np.linspace(0, 1, 100) 29cnt = 0 30for i, (train, test) in enumerate(cv.split(X,y)): # 利用模型划分数据集和目标变量 31cnt +=1 32probas_ = classifier.fit(X[train], y[train]).predict_proba(X[test]) # 训练模型并预测概率 33fpr, tpr, thresholds = roc_curve(y[test], probas_[:, 1]) # 调用roc_curve函数 34mean_tpr += np.interp(mean_fpr, fpr, tpr) # 累计每次循环的总值后以求平均值 35mean_tpr[0] = 0.0 # 坐标以0为起点 36roc_auc = auc(fpr, tpr) # 求AUC面积 37# 画出当前分割数据的ROC曲线 38plt.plot(fpr, tpr, lw=1, label='ROC fold {0:.2f} (area = {1:.2f})'.format(i, roc_auc)) 39plt.plot([0, 1], [0, 1], '--', color=(0.6, 0.6, 0.6), label='Luck') 40# 画对角线 41 42mean_tpr /= cnt # 求平均值 43mean_tpr[-1] = 1.0 # 坐标以1为终点 44mean_auc = auc(mean_fpr, mean_tpr) # 求平均AUC面积 45 46plt.plot(mean_fpr, mean_tpr, 'k--',label='Mean ROC (area = {0:.2f})'.format(mean_auc), lw=2) 47# 设置x、y轴的上下限,设置宽一点,以免和边缘重合,以便更好地观察图像的整体 48plt.xlim([-0.05, 1.05]) 49plt.ylim([-0.05, 1.05]) 50plt.xlabel('False Positive Rate') 51plt.ylabel('True Positive Rate') 52plt.title('Breast Cancer Wisconsin ROC') 53plt.legend(loc="lower right") 54plt.show 55Out[33]: 图5.12威斯康星乳腺癌ROC曲线 微课视频 5.3.6多分类指标 前面介绍了二分类的评价指标,多分类问题的所有评价指标基本上都来自二分类指标,但是要对所有类别进行平均。具体来讲,评价多分类问题时,通常把多分类问题分解为n个二分类问题,每次以其中一个类为正类,其余类统一为负类,并计算各种二分类指标,最后再对所有类别进行平均进而得到多分类评价指标。 多类分类评估有三种办法,分别对应sklearn.metrics中参数average值为micro、macro和weighted的情况,三种方法所求的值一般也不同。 1. 多分类的Accuracy、Precision、Recall和F1score指标 (1) macro: 分别计算第i类的Precision、Recall和F1score(把第i类当作正类,其余所有类统一为负类),然后进行平均。 (2) micro: 计算所有类别中FP、FN和TP的总数,然后利用这些数来分别计算Precision、Recall和F1score,这些值都等于Accuracy的值。 (3) weighted: 是为了解决macro中没有考虑样本不均衡的情况,在计算Precision与Recall时,各个类别的Precision 与Recall要乘以该类在总样本中的占比来求和。 接下来以三分类为例,分别来计算参数average值为micro、macro和weighted时的Precision、Recall和F1score。 首先生成一组数据,数据分为-1、0、1三类,真实数据y_true中,一共有30个-1、240个0和30个1。 生成数据并计算混淆矩阵,代码如下。 1In[34]: 2import numpy as np 3from sklearn.metrics import confusion_matrix 4y_true = np.array([-1]*30 + [0]*240 + [1]*30) 5y_pred = np.array([-1]*10 + [0]*10 + [1]*10 + 6[-1]*40 + [0]*160 + [1]*40 + 7[-1]*5 + [0]*5 + [1]*20) 8confusion_matrix(y_true, y_pred) 9Out[34]: 10array([[ 10, 10, 10], 11[ 40, 160, 40], 12[ 5, 5, 20]], dtype=int64) 计算参数average值为macro的情况。 1In[35]: 2from sklearn.metrics import precision_score,recall_score,f1_score 3precision=precision_score(y_true, y_pred, average="macro") 4recall=recall_score(y_true, y_pred, average="macro") 5f1=f1_score(y_true, y_pred, average="macro") 6print("precision:",precision) 7print("recall:",recall) 8print("f1:",f1) 9Out[35]: 10precision: 0.46060606060606063 11recall: 0.5555555555555555 12f1: 0.4687928183321521 计算参数average值为micro的情况。 1In[36]: 2precision=precision_score(y_true, y_pred, average="micro") 3recall=recall_score(y_true, y_pred, average="micro") 4f1=f1_score(y_true, y_pred, average="micro") 5print("precision:",precision) 6print("recall:",recall) 7print("f1:",f1) 8Out[36]: 9precision: 0.6333333333333333 10recall: 0.6333333333333333 11f1: 0.6333333333333333 由于micro算法把所有的类放在一起算,比如Precision,就是把所有类的TP加起来,再除以所有类的TP和FP相加之和。因此micro方法下的Precision、Recall和F1score的值都等于Accuracy。 计算参数average值为weighted的情况。 1In[37]: 2precision=precision_score(y_true, y_pred, average="weighted") 3recall=recall_score(y_true, y_pred, average="weighted") 4f1=f1_score(y_true, y_pred, average="weighted") 5print("precision:",precision) 6print("recall:",recall) 7print("f1:",f1) 8Out[37]: 9precision: 0.7781818181818182 10recall: 0.6333333333333333 11f1: 0.6803968816442238 2. 多分类的ROC曲线及AUC 计算多分类的ROC曲线以及AUC值时,若使用roc_auc_score函数,则参数average要设置为average="micro"或"macro"; 若使用AUC函数,则采用fpr["micro"]、tpr["micro"]或者fpr["macro"]、tpr["macro"]。 以鸢尾花数据集为例,代码如下,输出的ROC曲线如图5.13所示。 1In[38]: 2import numpy as np 3import matplotlib.pyplot as plt 4from itertools import cycle 5from sklearn import svm, datasets 6from sklearn.metrics import roc_curve, auc 7from sklearn.model_selection import train_test_split 8from sklearn.preprocessing import label_binarize 9from sklearn.multiclass import OneVsRestClassifier 10from scipy import interp 11 12# 加载数据 13iris = datasets.load_iris() 14x = iris.data 15y = iris.target 16# 将标签二值化,即变成[1 0 0] [0 0 1] [0 1 0] 17y = label_binarize(y, classes=[0, 1, 2]) 18 19# 设置种类 20n_classes = y.shape[1] 21 22# 训练模型并预测 23random_state = np.random.RandomState(0) 24n_samples, n_features = x.shape 25 26x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=.5,random_state=0) 27 28classifier = OneVsRestClassifier(svm.SVC(kernel='linear', probability=True,random_state=random_state)) 29y_score = classifier.fit(x_train, y_train).decision_function(x_test) 30 31# 计算每一类的ROC 32fpr = dict() 33tpr = dict() 34roc_auc = dict() 35for i in range(n_classes): # 遍历三个类别 36fpr[i], tpr[i], _ = roc_curve(y_test[:, i], y_score[:, i]) 37roc_auc[i] = auc(fpr[i], tpr[i]) 38 39# Compute micro-average ROC curve and ROC area(micro方法) 40fpr["micro"], tpr["micro"], _ = roc_curve(y_test.ravel(), y_score.ravel()) 41roc_auc["micro"] = auc(fpr["micro"], tpr["micro"]) 42 43# Compute macro-average ROC curve and ROC area(macro方法) 44# First aggregate all false positive rates 45all_fpr = np.unique(np.concatenate([fpr[i] for i in range(n_classes)])) 46# Then interpolate all ROC curves at this points 47mean_tpr = np.zeros_like(all_fpr) 48for i in range(n_classes): 49mean_tpr += interp(all_fpr, fpr[i], tpr[i]) 50 51# Finally average it and compute AUC 52mean_tpr /= n_classes 53fpr["macro"] = all_fpr 54tpr["macro"] = mean_tpr 55roc_auc["macro"] = auc(fpr["macro"], tpr["macro"]) 56 57# Plot all ROC curves 58lw=2 59plt.figure() 60plt.plot(fpr["micro"], tpr["micro"], 61label='micro-average ROC curve (area = {0:0.2f})' 62''.format(roc_auc["micro"]), 63color='deeppink', linestyle=':', linewidth=4) 64 65plt.plot(fpr["macro"], tpr["macro"], 66label='macro-average ROC curve (area = {0:0.2f})' 67''.format(roc_auc["macro"]), 68color='navy', linestyle=':', linewidth=4) 69 70colors = cycle(['aqua', 'darkorange', 'cornflowerblue']) 71for i, color in zip(range(n_classes), colors): 72plt.plot(fpr[i], tpr[i], color=color, lw=lw, 73label='ROC curve of class {0} (area = {1:0.2f})' 74''.format(i, roc_auc[i])) 75 76plt.plot([0, 1], [0, 1], 'k--', lw=lw) 77plt.xlim([0.0, 1.0]) 78plt.ylim([0.0, 1.05]) 79plt.xlabel('False Positive Rate') 80plt.ylabel('True Positive Rate') 81plt.title('Receiver Operating Characteristic of multi-class') 82plt.legend(loc="lower right") 83plt.show() 84Out[38]: 图5.13鸢尾花ROC曲线 3. classification_report sklearn中的classification_report函数用于显示主要分类指标的文本报告。在报告中显示每个类的Precision、Recall和F1score以及Accuracy(Micro平均)、Macro平均、Weighted 平均等信息,是评价模型便捷且全面的工具。 1sklearn.metrics.classification_report( 2y_true, #一维数组,或标签指示器数组/稀疏矩阵,目标值 3y_pred, #一维数组,或标签指示器数组/稀疏矩阵,分类器返回的估计值 4labels=None, #array,shape = [n_labels],报表中包含的标签索引的可选列表 5target_names=None, #字符串列表,与标签匹配的可选显示名称(相同顺序) 6sample_weight=None, #类似于shape = [n_samples]的数组,可选项,样本权重 7digits=2, #输出浮点值的位数,如果 output_dict=True,此参数不起作用 8output_dict=False #为True则评估结果以字典形式返回 9) 1In[39]: 2from sklearn.metrics import classification_report 3y_true = [0, 1, 2, 2, 2] 4y_pred = [0, 0, 2, 2, 1] 5target_names = ['class A', 'class B', 'class C'] 6print(classification_report(y_true, y_pred, target_names=target_names)) 7Out[39]: 8precisionrecallf1-scoresupport 9 10class A0.50 1.000.671 11class B 0.00 0.00 0.00 1 12class C 1.00 0.67 0.80 3 13 14accuracy 0.60 5 15macro avg 0.50 0.56 0.49 5 16weighted avg 0.70 0.60 0.61 5 其中,support(支持度)指原始的真实数据中属于该类的数目,即每个标签出现的次数。 微课视频 5.3.7回归指标 回归模型是机器学习中很重要的一类模型,不同于常见的分类模型,分类问题的评价指标是准确率,回归算法的评价指标主要是MSE、RMSE、MAE、MedAE、R2、EVS。 1. 均方误差 均方误差(Mean Squared Error,MSE),是反映估计量与被估计量之间差异程度的一种度量,其值越小说明拟合效果越好,所以常被用作线性回归的损失函数。 MSE的计算公式如式(55)所示。 MSE=1n∑ni=1(yi-y^i)2 (55) sklearn使用mean_squared_error函数计算均方误差。 1In[40]: 2from sklearn.metrics import mean_squared_error 3y_true = [3, -0.5, 2, 7] 4y_pred = [2.5, 0.0, 2, 8] 5mean_squared_error(y_true, y_pred) 6Out[40]: 70.375 2. 平均绝对误差 平均绝对误差(Mean Absolute Error,MAE),预测目标值和实际目标值之间误差的绝对值的平均数,可以更好地反映预测值误差的实际情况,其值越小越好。 MAE的计算公式如式(56)所示。 MAE=1n∑ni=1|yi-y^i|(56) sklearn使用mean_absolute_error 函数计算平均绝对误差。 1In[41]: 2from sklearn.metrics import mean_absolute_error 3y_true = [3, -0.5, 2, 7] 4y_pred = [2.5, 0.0, 2, 8] 5mean_absolute_error(y_true, y_pred) 6Out[41]: 70.5 3. 中位绝对误差 中位绝对误差(Median Absolute Error,MedAE)通过取目标值和预测值之间的所有绝对差值的中值来计算损失,其值越小越好。 MedAE的计算公式如式(57)所示。 MedAE(y,y^)=median(|y1-y^1|,…,|yn- y^n|)(57) sklearn使用median_absolute_error函数计算中位绝对误差。 1In[42]: 2from sklearn.metrics import median_absolute_error 3y_true = [3, -0.5, 2, 7] 4y_pred = [2.5, 0.0, 2, 8] 5median_absolute_error(y_true, y_pred) 6Out[42]: 70.5 4. R2决定系数 R2决定系数(RSquared)表示回归方程在多大程度上解释了因变量的变化,或者说方程对观测值的拟合程度如何。R2决定系数的最优值为1(完全拟合); 为0时,说明模型和样本基本没有关系; 也可为负,为负时说明模型非常差。 RSquared计算公式为: R2(y,y^)=1-∑ni=1(yi-y^i)2∑ni=1(yi-y-)2(58) sklearn使用r2_score函数计算R2决定系数。 1In[43]: 2from sklearn.metrics import r2_score 3y_true = [3, -0.5, 2, 7] 4y_pred = [2.5, 0.0, 2, 8] 5r2_score(y_true, y_pred) 6Out[43]: 70.9486081370449679 5. 可释方差得分 可释方差得分(Explained Variance Score,EVS),也叫解释方差得分。表征模型中残差的方差在整个数据集所占的比重的变量,可释方差值最好的分数是1.0,分数越低效果越差。 计算公式为: explained_variance(y,y^)=1-var{y-y^}var{y} (59) sklearn使用explained_variance_score函数计算可释方差得分。 1In[44]: 2from sklearn.metrics import explained_variance_score 3y_true = [3, -0.5, 2, 7] 4y_pred = [2.5, 0.0, 2, 8] 5explained_variance_score(y_true, y_pred) 6Out[44]: 70.9571734475374732 微课视频 5.3.8在模型选择中使用评估指标 前面详细讨论了若干种评估方法,本节以手写数字数据集(digits)为例,并根据真实情况和具体模型来应用前面所学的分类指标及回归指标。 1. 分类指标的应用 首先导入digits数据集。 1In[45]: 2import pandas as pd 3from sklearn.datasets import load_digits 4digits = load_digits() 查看模块的属性列表。 1In[46]: 2dir(digits) 3Out[46]: 4['DESCR', 'data', 'feature_names', 'frame', 'images', 'target', 'target_names'] 样本集中第一个手写数字“0”的图像特征。 1In[47]: 2print(digits.images[0]) 3Out[47]: 4[[ 0.0.5. 13.9.1.0.0.] 5[ 0.0. 13. 15. 10. 15.5.0.] 6[ 0.3. 15.2.0. 11.8.0.] 7[ 0.4. 12.0.0.8.8.0.] 8[ 0.5.8.0.0.9.8.0.] 9[ 0.4. 11.0.1. 12.7.0.] 10[ 0.2. 14.5. 10. 12.0.0.] 11[ 0.0.6. 13. 10.0.0.0.]] 样本集中第一个手写数字“0”的可视化,如图5.14所示。 1In[48]: 2import matplotlib.pyplot as plt 3plt.imshow(digits.images[0],cmap='binary') 4plt.show() 5Out[48]: 图5.14手写“0”的可视化 划分数据集,其中20%用于测试,80%用于训练。 1In[49]: 2from sklearn.model_selection import train_test_split 3X_train,X_test,y_train,y_test=train_test_split(x,y,test_size=0.2,random_state=0) 4print("x.shape:",x.shape,"y.shape:",y.shape) 5print("X_train:", X_train.shape) 6print("y_train:", y_train.shape) 7print("X_test:", X_test.shape) 8print("y_test:", y_test.shape) 9Out[49]: 10x.shape: (1797, 64) y.shape: (1797,) 11X_train: (1437, 64) 12y_train: (1437,) 13X_test: (360, 64) 14y_test: (360,) 训练模型并调优。 1In[50]: 2from sklearn.model_selection import GridSearchCV 3from sklearn.svm import SVC 4param_grid = { 'C': [0.001, 0.01, 1, 10], 'gamma': [0.001, 0.01, 1, 10], 'kernel': ['linear', 'rbf']} 5grid = GridSearchCV(SVC(), param_grid=param_grid) 6grid.fit(X_train, y_train) 7print("Best parameters:", grid.best_params_) 8print("Best score: {:.3f}".format(grid.best_score_)) 9print("Test set accuracy: {:.3f}".format(grid.score(X_test, y_test))) 10print("Best estimator: {}".format(grid.best_estimator_)) 11Out[50]: 12Best parameters: {'C': 1, 'gamma': 0.001, 'kernel': 'rbf'} 13Best score: 0.991 14Test set accuracy: 0.992 15Best estimator: SVC(C=1, gamma=0.001) 调用classification_report输出各评估指标的值。 1In[51]: 2from sklearn.metrics import classification_report 3predicted = grid.best_estimator_.predict(X_test) 4print(classification_report(y_test, predicted)) 5Out[51]: 6precisionrecallf1-scoresupport 7 801.001.001.0027 910.971.000.9935 1021.001.001.0036 1131.001.001.0029 1241.001.001.0030 1350.970.970.9740 1461.001.001.0044 1571.001.001.0039 1681.000.970.9939 1790.980.980.9841 18 19accuracy0.99360 20macro avg0.990.990.99360 21weighted avg0.990.990.99360 最后可视化混淆矩阵,如图5.15所示。 1In[52]: 2import seaborn as sn 3from sklearn.metrics import confusion_matrix 4cm=confusion_matrix(y_test,predicted) 5sn.heatmap(cm, annot=True) 6Out[52]: 图5.15热力图绘制的混淆矩阵(见彩插) 从上面的热力图可以看出,模型仅有一次将数字1识别为8,一次将数字5识别为9,一次将数字9识别为5。除此之外,模型全部识别正确。 2. 回归指标的应用 具体代码如下。 1In[53]: 2from sklearn.datasets import load_digits 3from sklearn.model_selection import train_test_split 4from sklearn.linear_model import LogisticRegression 5from sklearn.metrics import r2_score 6from sklearn.metrics import explained_variance_score 7from sklearn.metrics import mean_absolute_error 8from sklearn.metrics import mean_squared_error 9from sklearn.metrics import median_absolute_error 10# 导入digits数据集 11digits = load_digits() 12n_samples=len(digits.images) 13x=digits.images.reshape((n_samples,-1)) 14y=digits.target 15# 划分数据集,其中20%用于测试,80%用于训练 16x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.2,random_state=0) 17# 训练数据 18lr = LogisticRegression() 19lr.fit(x_train,y_train) 20y_pre_lr = lr.predict(x_test) 21lr_R2 = r2_score(y_test,y_pre_lr) 22lr_EVS=explained_variance_score(y_test, y_pre_lr) 23lr_MAE=mean_absolute_error(y_test, y_pre_lr) 24lr_medAE=median_absolute_error(y_test, y_pre_lr) 25lr_MSE=mean_squared_error(y_test, y_pre_lr) 26# 输出评估指标 27print("R2决定系数:",lr_R2) 28print("可释方差:",lr_EVS) 29print("平均绝对误差:",lr_MAE) 30print("中位绝对误差:",lr_medAE) 31print("均方误差:",lr_MSE) 32Out[53]: 33R2决定系数: 0.8996259447195352 34可释方差: 0.8997035535251644 35平均绝对误差: 0.1527777777777778 36中位绝对误差: 0.0 37均方误差: 0.8083333333333333 最后可视化混淆矩阵,如图5.16所示。 图5.16热力图绘制的混淆矩阵(见彩插) 1In[54]: 2import seaborn as sn 3from sklearn.metrics import confusion_matrix 4cm=confusion_matrix(y_test,y_pre_lr) 5sn.heatmap(cm, annot=True) 6Out[54]: 从图5.16中可以看出,由于未对模型进行调优,因此识别效果明显不如图5.15。 微课视频 5.4处理类的不平衡问题 在机器学习的实践中,通常会遇到实际数据中正负样本比例不平衡的情况,也叫数据倾斜。对于数据倾斜的情况,如果选取的算法不合适,或者评价指标不合适,那么对于实际应用线上时效果往往不尽人意,所以如何解决数据不平衡问题是实际生产中非常常见且重要的问题。 5.4.1类别不平衡问题 类别不平衡(ClassImbalance)是指分类任务中不同类别的训练样例数目差别很大的情况。在现实的分类学习任务中,经常会遇到类别不平衡的现象,比如在二分类问题中,通常假设正负类别相对均衡,然而实际应用中类别不平衡的现象是非常常见的,比如疾病检测、产品抽检、邮件过滤、信用卡欺诈等。 如果不同类别的训练样例数目稍有差别,通常影响不大,但若差别很大,则会对学习过程造成困扰。如图5.17所示,产品抽检数据集中有998个反例,但是正例只有2个,那么学习方法只需要返回一个永远将新样本预测为反例的学习器,就能达到99.8%的精度; 然而这样的学习器没有任何实际价值,因为它不能预测出任何正例,因此有必要了解类别不平衡问题处理的基本方法。 图5.17抽检数据集正反例对比图 如何解决机器学习中类别不平衡问题呢?严格地讲,任何数据集都存在数据不平衡的现象,这往往是由问题本身决定的,处理时只需要关注那些分布差别比较悬殊的; 另外,虽然很多数据集都包含多个类别,但这里着重考虑二分类,因为在解决了二分类中的数据不平衡问题后,推而广之,就能得到多分类情况下的解决方案。 5.4.2解决类别不平衡问题 1. 采样法 采样法是通过对训练集进行预处理,使其从不平衡转变为较平衡的数据集的方法。该方法是较为常用的方法,并且通常情况下比较有效。采样法分为欠采样和过采样两种。 1) 欠采样 欠采样(Undersampling)通过减少分类中多数类的样本数量,使得正例、反例数目平衡。最直接的方法是随机地去掉一些多数类样本,减小多数类的规模,然后再进行学习。该方法的缺点是会丢失多数类样本中的一些重要信息。 2) 过采样 过采样(Oversampling)通过增加分类中少数类样本的数量,使得正例、反例数目平衡。最直接的方法是简单复制少数类样本,形成多条记录,然后再进行学习。该方法的缺点是如果样本特征少,可能导致过拟合。 2. 惩罚权重 该方法在算法实现过程中,对于分类中不同样本数量的类别分别赋予不同的权重,即小样本数量类别赋予较高权重,而大样本数量类别赋予较低权重,然后进行计算和建模。使用这种方法时需要对样本作额外处理,只需要在算法模型的参数中进行相应设置即可。很多模型和算法中都有基于类别参数的调整设置,以Scikitlearn中的SVM为例,通过将class_weight参数设置为balanced即可。这样SVM会将权重设置为与不同类别样本数量成反比的权重来做自动均衡处理,因此该方法是更加简单且高效的方法。 代码清单54: 处理类的不平衡问题 对Scikitlearn官网的例子稍加改动为例,具体代码如下,输出如图5.18所示。 1In[55]: 2import numpy as np 3import matplotlib.pyplot as plt 4from sklearn import svm 5from sklearn.datasets import make_blobs 6 7# we create two clusters of random points 8n_samples_1 = 1000 9n_samples_2 = 100 10centers = [[0.0, 0.0], [2.0, 2.0]] 11clusters_std = [1.5, 0.5] 12X, y = make_blobs(n_samples=[n_samples_1, n_samples_2], 13centers=centers, 14cluster_std=clusters_std, 15random_state=0, shuffle=False) 16 17# fit the model and get the separating hyperplane 18clf = svm.SVC(kernel='linear', C=1.0) 19clf.fit(X, y) 20 21# fit the model and get the separating hyperplane using weighted classes wclf = svm.SVC(kernel='linear', class_weight= 'balanced' ) # 官网中class_weight={1: 10} 22wclf.fit(X, y) 23 24# plot the samples 25plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Paired, edgecolors='k') 26 27# plot the decision functions for both classifiers 28ax = plt.gca() 29xlim = ax.get_xlim() 30ylim = ax.get_ylim() 31 32# create grid to evaluate model 33xx = np.linspace(xlim[0], xlim[1], 30) 34yy = np.linspace(ylim[0], ylim[1], 30) 35YY, XX = np.meshgrid(yy, xx) 36xy = np.vstack([XX.ravel(), YY.ravel()]).T 37 38# get the separating hyperplane 39Z = clf.decision_function(xy).reshape(XX.shape) 40 41# plot decision boundary and margins 42a = ax.contour(XX, YY, Z, colors='k', levels=[0], alpha=0.5, linestyles=['-']) 43 44# get the separating hyperplane for weighted classes 45Z = wclf.decision_function(xy).reshape(XX.shape) 46 47# plot decision boundary and margins for weighted classes 48b = ax.contour(XX, YY, Z, colors='r', levels=[0], alpha=0.5, linestyles=['-']) 49plt.legend([a.collections[0], b.collections[0]], ["non weighted", "weighted"],loc="upper right") 50plt.show() 51Out[55]: 图5.18用SVC最优分离超平面(见彩插) 首先用平面SVC找到分离平面(non weighted,用蓝色线表示),然后通过将class_weight参数设置为balanced,自动校正不平衡类别并绘制分离超平面(weighted,用红色线表示)。 5.5网格搜索优化模型 机器学习应用中,有两种类型的参数: 一种是从训练集中学习到的参数,例如逻辑回归的权重; 另一种是为了使学习算法达到最优而可调节的参数,即需要预先优化设置而非通过训练得到的参数,例如逻辑回归中的正则化参数或决策树中的深度参数、人工神经网络模型中隐藏层层数和每层的结点个数、正则项中常数大小等,这种可调节的参数称为超参数(Hyperparameters)。超参数选择不恰当的话,就会出现欠拟合或者过拟合的问题。 在选择超参数的时候,有两种途径: 一种是凭经验微调; 另一种就是选择不同大小的参数,代入模型中,挑选表现最好的参数。第一种微调的方法是手工调制超参数,直到找到一个好的超参数组合,但是这么做的话非常耗时,也可能没有时间探索多种组合。所以最常用的方法就是利用网格搜索,通过调参来评估一个模型的泛化能力。 微课视频 5.5.1简单网格搜索选择超参数 网格搜索是模型超参数(即需要预先优化设置而非通过训练得到的参数)的优化技术,常用于优化三个或者更少数量的超参数,本质是穷举法。对于每个超参数,使用者选择一个较小的有限集去探索。然后,由这些超参数的笛卡儿乘积得到若干组超参数。网格搜索使用每组超参数训练模型,挑选验证集误差最小的超参数作为最优的超参数。 网格搜索采用的是穷举法的思路,其计算复杂度将随需要优化的超参数规模指数增长。因此该方法只适用于规模很小的超参数优化问题。当超参数规模较大时,随机搜索将会是更高效的超参数优化方法。 现在,要用网格搜索这个更加强大的超参数优化工具来找到超参数值的最优组合,从而进一步改善模型的性能。以鸢尾花数据集为例,通过调节SVM分类器的C和gamma参数,在2个参数上使用 for 循环,对每种参数组合分别训练并评估一个分类器,实现简单的网格搜索。 代码清单55: 网格搜索优化模型 1In[56]: 2from sklearn.datasets import load_iris 3iris_dataset = load_iris() 4In[57]: 5# 用简单网格搜索选择超参数 6from sklearn.model_selection import train_test_split 7from sklearn.svm import SVC 8X_train, X_test, y_train, y_test = train_test_split( iris.data, iris.target, random_state=0) 9print("training set: {} test set: {}".format( X_train.shape[0], X_test.shape[0])) 10best_score = 0 # 存储当前最好分数 11for gamma in [0.001, 0.01, 0.1, 1, 10, 100]: # gamma参数 12for C in [0.001, 0.01, 0.1, 1, 10, 100]: # C参数 13# 对gamma和C参数的每种组合都训练一个SVC 14svm = SVC(gamma=gamma, C=C) 15svm.fit(X_train, y_train) 16# 在测试集上对SVC进行评估 17score = svm.score(X_test, y_test) 18# 保存更高的分数和对应的参数 19if score > best_score: 20best_score = score 21best_parameters = {'C': C, 'gamma': gamma} 22print("Best score: {:.2f}".format(best_score)) 23print("Best parameters: {}".format(best_parameters)) 24Out[57]: 25Size of training set: 112 size of test set: 38 26Best score: 0.97 27Best parameters: {'C': 100, 'gamma': 0.001} 得分为97%,看起来还不错。但值得注意的是,将原始数据集划分成训练集和测试集以后,其中测试集除了用作调整参数,也用来测量模型的好坏。这样做导致最终的评分结果比实际效果好,因为测试集在调参过程中被送到模型里,而我们的目的是将训练模型应用到未曾见过的新数据上。 微课视频 5.5.2验证集用于选择超参数 已经知道,用测试集估计学习器的泛化误差,其重点在于测试样本不能以任何形式参与到模型的选择之中,包括超参数的设定,否则将导致过拟合。基于这个原因,测试集中的样本不能用于验证集。因此,只能从训练数据中构建验证集,对训练集再进行一次划分,分为训练集和验证集。这样划分的结果就是: 原始数据划分为3份,分别为训练集(Training Set)、验证集(Validation Set)和测试集(Testing Set)。其中训练集用于学习参数(即训练模型); 验证集用于估计训练中或训练后的泛化误差,更新超参数(即挑选超参数); 而测试集用来衡量模型表现的好坏(即评价模型的泛化能力)。 这样一来就将原始数据集划分为如图5.19所示的训练集、验证集及测试集3个数据集。 1In[58]: 2import mglearn 3mglearn.plots.plot_threefold_split() 4Out[58]: 图5.19训练集、验证集和测试集 具体流程如图5.20所示。 图5.20验证集用于选择超参数的流程 将数据集划分为训练集、验证集和测试集后,利用验证集选定最佳参数,用找到的最优超参数重新构建模型,并同时在训练集和验证集上进行训练,以便使用尽可能多的数据来构建模型,从而获得较好的评估结果。 具体的代码实现如下。 1In[59]: 2from sklearn.svm import SVC 3# 将数据划分为训练集与测试集 4X_trainval, X_test, y_trainval, y_test = train_test_split(iris.data, iris.target, random_state=0) 5# 将训练集划分为训练集与验证集 6X_train, X_valid, y_train, y_valid = train_test_split(X_trainval, y_trainval, random_state=1) 7print("training set: {} validation set: {} test set:" " {}\n".format(X_train.shape[0], X_valid.shape[0], X_test.shape[0])) 8 9best_score = 0 10 11for gamma in [0.001, 0.01, 0.1, 1, 10, 100]: # gamma参数 12for C in [0.001, 0.01, 0.1, 1, 10, 100]: # C参数 13# 对gamma和C参数的每种组合都训练一个SVC 14svm = SVC(gamma=gamma, C=C) 15svm.fit(X_train, y_train) 16# 在验证集上评估SVC 17score = svm.score(X_valid, y_valid) 18# 保存更高的分数和对应的参数 19if score > best_score: 20best_score = score 21best_parameters = {'C': C, 'gamma': gamma} 22# 在训练+验证集上重新构建一个模型,并在测试集上进行评估 23svm = SVC(**best_parameters) 24svm.fit(X_trainval, y_trainval) 25test_score = svm.score(X_test, y_test) 26 27print("Best score on validation set: {:.2f}".format(best_score)) 28print("Best parameters: ", best_parameters) 29print("Test set score with best parameters: {:.2f}".format(test_score)) 30Out[59]: 31training set: 84 validation set: 28 test set: 38 32 33Best score on validation set: 0.96 34Best parameters: {'C': 10, 'gamma': 0.001} 35Test set score with best parameters: 0.92 从输出结果可以看到,验证集上的最高分数是96%,比之前的97%低了1%,主要因为这次使用了更少的数据(一部分被划分为了验证集)来训练模型。测试集上的分数为92%,这个分数实际反映了模型的泛化能力,也就是说模型仅对92%的新数据进行了正确的分类,而不是之前认为的97%。 微课视频 5.5.3带交叉验证的网格搜索 尽管将数据集划分为训练集、验证集和测试集的方法相对有用,可行性较高。但是该方法对数据的划分比较敏感,也就是说其最终的表现好坏与初始数据的划分结果有很大的关系,且有时候泛化性能较低,为了得到更好的泛化性能的更好估计,可以通过交叉验证来评估每种组合的性能并以此来降低偶然性。 1In[60]: 2from sklearn.model_selection import cross_val_score 3 4best_score = 0 5for gamma in [0.001,0.01,1,10,100]: 6for c in [0.001,0.01,1,10,100]: 7# 对于每种参数可能的组合,进行一次训练 8svm = SVC(gamma=gamma,C=c) 9# 5 折交叉验证 10scores = cross_val_score(svm,X_trainval,y_trainval,cv=5) 11score = scores.mean() 12# 找到表现最好的参数 13if score > best_score: 14best_score = score 15best_parameters = {'gamma':gamma,"C":c} 16 17# 使用最佳参数,构建新的模型 18svm = SVC(**best_parameters) 19 20# 使用训练集和验证集进行训练 21svm.fit(X_trainval,y_trainval) 22 23# 模型评估 24test_score = svm.score(X_test,y_test) 25 26print('Best score on validation set :{:.2f}'.format(best_score)) 27print('Best parameters:{}'.format(best_parameters)) 28print('Best score on test set:{:.2f}'.format(test_score)) 29Out[60]: 30Best score on validation set :0.97 31Best parameters:{'gamma': 0.01, 'C': 100} 32Best score on test set:0.97 从运行结果可以看出,验证集上的最高分数是97%,比5.5.2节提高了1%; 测试集上的分数为97%,比5.5.2节提高了5%,说明交叉验证的使用进一步提高了模型的泛化能力。 在实际应用中,交叉验证经常与网格搜索进行结合,即带交叉验证的网格搜索(Grid Search with Cross Validation),并以此作为参数评价的一种常用方法。为此Scikitlearn提供了GridSearchCV类,它以估计器(Estimator)的形式实现了这种方法。 1. GridSearchCV简介 GridSearchCV存在的意义就是自动调参,只要把参数输进去,它就能给出最优化结果和参数。GridSearchCV其实可以拆分为GridSearch和CV两部分,即网格搜索和交叉验证。网格搜索,搜索的是参数,即在指定的参数范围内,按步长依次调整参数,利用调整的参数训练学习器,从所有的参数中找到在验证集上精度最高的参数,这其实是一个训练和比较的过程。交叉验证的目的是提高模型的泛化能力,得到可靠稳定的模型。 GridSearchCV可以保证在指定的参数范围内找到精度最高的参数,但是这也是网格搜索的缺陷所在: 它要求遍历所有可能参数的组合,在面对大数据集和多参数的情况下,非常耗时。 2. GridSearchCV类构造方法参数说明 GridSearchCV类的构造方法的语法格式如下。 __init__(self, estimator, param_grid, scoring=None, fit_params=None,n_jobs=1, iid=True, refit=True, cv=None, verbose=0, pre_dispatch='2*n_jobs', error_score='raise', return_train_score='warn') GridSearchCV类构造方法各参数如下。 1) estimator 选择使用的分类器,并且传入除需要确定最佳的参数之外的其他参数。每一个分类器都需要一个scoring参数或者score方法,举例如下。 estimator = RandomForestClassifier(min_sample_split=100,min_samples_leaf = 20,max_depth = 8,max_features = 'sqrt' , random_state =10) 2) param_grid 需要最优化的参数的取值,值为字典或者列表,举例如下。 param_grid = {'kernel': ['linear', 'rbf'], 'gamma': [0.001,0.01,1,10,100], 'C': [0.001,0.01,1,10,100]} 3) scoring 模型评价标准,默认为None,这时需要使用score函数; 根据所选模型不同,评价准则不同,字符串(函数名)或可调用对象需要其函数签名,形如scorer(estimator,X,y); 如果是None,则使用estimator的误差估计函数。 4) fit_params 该参数通常取None。 5) n_jobs n_jobs: 并行数。n_jobs为-1表示跟CPU核数一致,默认值为1。 6) iid iid: 默认为True,为True时,默认为各个样本fold概率分布一致,误差估计为所有样本之和,而非各个fold的平均。 7) refit 默认为True,程序将会以交叉验证训练集得到的最佳参数,重新对所有可能的训练集与开发集进行,作为最终用于性能评估的最佳模型参数。即在搜索参数结束后,用最佳参数结果再次fit一遍全部数据集。 8) cv 交叉验证参数,默认为None,使用3折交叉验证。指定fold数量,默认为3,也可以是yield训练/测试数据的生成器。 9) verbose verbose: 日志冗长度。为0表示不输出训练过程,为1表示偶尔输出,大于1表示对每个子模型都输出。 10) pre_dispatch 指定总的并行任务数,当n_jobs大于1时,数据将在每个运行点进行复制,这可能导致OOM,而设置pre_dispatch参数,则可以预先划分总的任务数量,使数据最多被复制pre_dispatch次。 11) error_score 该参数通常取raise。 12) return_train_score 该参数通常取warn。如果取False,cv_results_属性将不包括训练分数。 3. GridSearchCV对象属性说明 1) cv_results 具有键作为列标题和值作为列的字典,可以导入DataFrame中,params键用于存储所有参数候选项的参数设置列表。 2) best_estimator 通过搜索选择的估计器,即在左侧数据上给出最高分数(或指定的最小损失)的估计器。如果refit = False,则该属性不可用。 3) best_score best_estimator的交叉验证平均分数。 4) best_params_ 在保存数据上给出最佳结果的参数设置。 5) best_index_ 对应于最佳候选参数设置的索引。 6) scorer_ 选出最佳参数的估计器所使用的评分器。 7) n_splits 交叉验证时折叠或迭代的数量。 8) refit_time 在整个数据集的基础上选出最佳模型所耗费的时间。 4. GridSearchCV对象的方法 1) decision_function(X) 使用找到的参数最好的分类器调用decision_function,仅当refit参数为True且估计器有decision_function方法时可用。 2) fit(X, y=None, groups=None, **fit_params) 遍历所有参数组合,对模型进行训练。 3) get_params(deep=True) 获取估计器的参数。 4) predict(self, X) 用找到的最佳参数预测模型结果,仅当refit参数为True且估计器含有predict方法时才可用。 5) predict_log_proba(X) 调用最佳模型的predict_log_proba方法,仅当refit参数为True且估计器有predict_log_proba方法时才可用。 6) predict_proba(X) 调用最佳模型的predict_proba方法,仅当refit参数为True且估计器有predict_proba方法时才可用。 7) score(X, y=None) 如果预估器已经选出最优的分类器,则返回给定数据集的得分。 8) set_params(**params) 设置模型的参数。 9) transform(X) 调用最优分类器对X进行转换,仅当refit参数为True且估计器有transform方法时才可用。 要使用GridSearchCV类,首先需要用一个字典指定要搜索的参数名称,字典的值是想要尝试的参数设置。如果C和gamma想要尝试的取值为0.001,0.01,0.1,1,10和100, kernel想要尝试linear和rbf,可以将其转化为下面的字典。 下面来调节SVM分类器的C、kernel、gamma参数,完整的代码如下。 1In[61]: 2from sklearn.datasets import load_iris 3from sklearn.model_selection import train_test_split 4from sklearn.model_selection import GridSearchCV 5from sklearn.svm import SVC 6 7iris = load_iris() 8X_train, X_test, y_train, y_test = train_test_split( 9iris.data, iris.target, random_state=0) 10 11param_grid = { 'C': [0.001, 0.01, 1, 10, 100], 'gamma': [0.001, 0.01, 1, 10, 100], 'kernel': ['linear', 'rbf']} 12grid_search_svc=GridSearchCV(estimator=SVC(), 13param_grid=param_grid, scoring='accuracy', cv=10, n_jobs=-1) 14grid_search_svc = grid_search_svc.fit(X_train, y_train) 15 16print('Best score on validation set :{:.2f}'.format(grid_search_svc.best_score_)) 17print('Best parameters:{}'.format(grid_search_svc.best_params_)) 18print('Best score on test set:{:.2f}'.format(grid_search_svc.score(X_test, y_test))) 19print("Best estimator:\n{}".format(grid_search_svc.best_estimator_)) 20Out[61]: 21Best score on validation set :0.98 22Best parameters:{'C': 1, 'gamma': 0.001, 'kernel': 'linear'} 23Best score on test set:0.97 24Best estimator: 25SVC(C=1, gamma=0.001, kernel='linear') 上例中划分数据集、运行网格搜索并评估最终参数的完整过程如图5.21所示。 1In[62]: 2mglearn.plots.plot_grid_search_overview() 3Out[62]: 图5.21划分数据集、运行网格搜索并评估最终参数的完整过程 网格搜索的结果可以在cv_results_属性中找到(sklearn 2.0版本以下用grid_scores_属性查看),它是一个字典,保存了搜索的所有内容,代表搜索的整个过程,其输出如表5.2所示。 1In[63]: 2import pandas as pd 3results = pd.DataFrame(grid_search_svc.cv_results_) 4display(results) 5Out[63]: 表5.2cv_results_属性的值 idmean_fit_ time…paramssplit0_test_ score…split4_test_ scoremean_test_ score… 00.0018…{'C': 0.001, 'gamma': 0.001, 'kernel': 'linear'} 0.347826…0.4090910.366403… 10.002…{'C': 0.001, 'gamma': 0.001, 'kernel': 'rbf'} 0.347826…0.4090910.366403… 20.001…{'C': 0.001, 'gamma': 0.01, 'kernel': 'linear'} 0.347826…0.4090910.366403… ……………………… 220… {'C': 1, 'gamma': 0.01, 'kernel': 'linear'} 1…0.9545450.973123… ……………………… 470.0014… {'C': 100, 'gamma': 10, 'kernel': 'rbf'} 0.869565…0.9545450.911067… 480.0008…{'C': 100, 'gamma': 100, 'kernel': 'linear'} 0.956522…0.9545450.955336… 490.0016…{'C': 100, 'gamma': 100, 'kernel': 'rbf'} 0.521739…0.6818180.581423… 从输出结果可以看出,要想使用5折交叉验证对C、gamma以及kernel特定取值的SVM的精度进行评估,总共需要训练5×5×2 = 50轮。每一轮尝试其中一种C、gamma以及kernel的组合,并且每一轮中需要交叉验证5次。最终从中找出在验证集上平均得分最高的参数组合,即第23轮中(用灰色表示)验证集上平均最高得分0.973123,参数组合为{'C': 1, 'gamma': 0.01, 'kernel': 'linear'}。 5.6本章小结 本章首先讨论了算法链与管道。管道可以理解为一个容器,然后把需要进行的操作封装在其中进行操作,比如数据标准化、特征降维、主成分分析、模型预测等。Pipeline类不但可用于预处理和分类,还可以将任意数量的估计器连接,极大地方便了使用。 接着讨论了交叉验证、模型评价指标以及处理类的不平衡问题。交叉验证主要用于防止模型过于复杂而引起的过拟合问题。模型评价指标是评估模型的泛化能力,这是机器学习中的一个关键性的问题。评价指标的作用是了解模型的泛化能力,通过这些指标来逐步优化模型。在实际应用中,分类问题很少会遇到平衡的类别,因此需要了解这些分类不平衡的后果,并选择相应的评估指标。 最后讨论了在机器学习中如何通过网格搜索对模型调优。调优的过程就是寻找超参数的过程,如果超参数选择不恰当,模型就会出现欠拟合或者过拟合的问题。尽管将数据集划分为训练集、验证集和测试集的方法相对有用、可行性较高,但是该方法对数据的划分比较敏感,也就是说其最终的表现好坏与初始数据的划分结果有很大的关系,且有时候泛化性能较低。为了更好地提高模型的泛化能力,最好选择带交叉验证的网格搜索来评估每种组合的性能并以此来降低偶然性。 习题 1. 什么是算法链和管道?它们有什么作用? 2. 为什么要进行交叉验证? 3. K折交叉验证、分层K折交叉验证、留一法交叉验证、打乱划分交叉验证以及分组交叉验证之间有什么区别? 4. 为什么要对模型进行评价? 5. 解释一下什么是混淆矩阵。 6. 举例说明分类问题的评价指标都有哪些。 7. 举例说明回归问题的评价指标都有哪些。 8. 用GridSearchCV对5.3.8节In[53]的逻辑回归模型进行调优,并输出相关评价指标及混淆矩阵。 9. 将5.5.2节In[59]中random_state设置为117,观察输出情况,并分析造成这种结果的原因是什么。