动手学数据分析(5)——数据建模与评估

本文为Datawhale8月组队学习——动手学数据分析课程的系列学习笔记。

Datawhale-动手学数据分析

数据来源

Kaggle小白入门首选练手项目——Kaggle-泰坦尼克号存活率

Ch3 数据建模与评估

数据建模部分主要涉及模型的选择和评估方法,实际上这部分的内容已经超出了数据分析入门课程的范畴,数据建模涉及另外一套完整的理论体系,仅仅两天学习是完全不够的,需要后续的补充完善。

由于机器学习算法个人尚在入门阶段,数据建模和评估部分的细节有待深挖,只列举了少数的一些概念和相关的sklearn模块。笔记主要参考了南大周志华教授的《机器学习》(西瓜书)。

数据建模

在对数据进行预处理和探索性分析后,我们已经对数据集的组成、特征分布有了相应的了解,此时可以根据建模需要以及数据集特点选定自变量因变量,划分数据集后,选定相应的学习算法或模型对数据进行建模。

数据集的划分

为了验证模型的泛化能力,需要将数据集拆分为训练集与测试集,在训练集上训练模型,测试集上测试模型的效果。常见的数据集划分方法包括:

  • 留出法(Hold out)
  • 交叉验证法(Cross Validation)
  • 自助法(Bootstrapping)

其中,交叉验证法还衍生出了留一法(Leave-One-Out,简称 LOO)。

留出法

直接将数据集$D$划分为两个互斥的部分,其中一部分作为训练集$S$,另一部分用作测试集$T$。训练集和测试集的比例一般为7:3。

  1. 尽可能保持数据分布的一致性,避免因数据划分过程引入的额外偏差而对最终结果产生影响。在分类中,保留类别比例的采样方法称为分层采样(Stratified Sampling)

  2. 一般要采用若干次随机划分、重复进行实验评估后取平均值作为留出法的评估结果。如果只做一次分割,模型对训练集、验证集和测试集的样本数比例,还有分割后数据的分布是否和原始数据集的分布相同等因素会比较敏感。

交叉验证法

将数据集$D$划分为$k$个大小近似的互斥子集,每个子集都尽可能保证数据分布的一致性(分层采样)。每次将$k-1$个子集的并集作为训练集,剩下的1个子集作为测试集。交叉验证重复$k$次,每个子样本验证一次,平均$k$次的测试结果,最终得到一个单一估测。这种划分数据集的测试方法称作 $k$ 折交叉验证(k-fold cross validation)

采用交叉验证方法时需要将学习数据样本分为两部分:训练数据样本和验证数据样本。并且为了得到更好的学习效果,无论训练样本还是验证样本都要尽可能参与学习。一般选取10重交叉验证即可达到好的学习效果。

与留出法相似,将数据集$D$划分为$K$个子集同样存在多种划分方式。划分方式引入的差别使得 $k$ 折交叉验证通常要随机使用不同的划分重复$p$次,最终的评估结果是这 $p$ 次 $k$ 折交叉验证结果的均值,常见的有10 次10折交叉验证。

使用交叉验证方法的目的主要有3个:

  • 从有限的学习数据中获取尽可能多的有效信息
  • 交叉验证从多个方向开始学习样本的,可以有效避免陷入局部最小值
  • 可以在一定程度上避免过拟合问题

留一法(Leave-One-Out)

即当交叉验证的子集容量均为1的情况,相当于对于每个样本进行一次预测。这种方法的评估效果往往比较准确,但对于数据集很大的情况,计算开销过于庞大,且其估计效果不一定好于其他方法。

自助法(Bootstrapping)

用于解决留出法、交叉验证法由于训练集与原数据集$D$规模不一致带来的偏差,以及留一法计算开销过大的问题。

基本思路:即在含有$m$个样本的数据集$D$中,进行$m$次有放回地随机抽样,组成的新数据集$D’$作为训练集,$D\backslash D’$ 作为测试集。

这样得到的训练集规模与原始数据集一致,且$m$趋向于无穷时,初始数据集约有$1/e=36.8%$的样本不会出现在训练集中(作为测试集)。这种方法适合数据集较小无法划分训练测试集的情况,并且能从初始数据集中产生多个不同的数据集,但该方法产生的数据集分布会与原数据集产生偏差。

初始数据量足够时,更多地采用前两种方法。数据集较小难以划分时,可以采用自助法。

代码实现

涉及sklearn的以下函数:

  • sklearn.model_selection.train_test_split()
  • sklearn.model_selection.cross_val_score()

函数方法的使用不做过多说明,使用的方法和用到的基本参数见代码示例,具体包含的参数可以查阅sklearn对应的API文档。

1
2
3
4
5
## 留出法
from sklearn.model_selection import train_test_split

# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X,y,stratify = y,test_size=0.3,random_state = 0)

random_state默认为False,当其设定为整数,则划分的数据集总是一致的,一般用于复现结果。stratify参数则代表分层抽样的分层依据,此处即:按照y的构成分层抽样。

1
2
3
4
5
6
7
8
9
10
11
12
## 交叉验证法

# 该模块中,有用于划分几折的方法,也有交叉验证输出k折准确率的方法
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression

# 训练模型,计算交叉验证各次的分类预测精度
lr = LogisticRegression(C=100,max_iter=1000)
scores = cross_val_score(lr, X_train, y_train, cv=10)

# 求均值输出
print('交叉验证精度平均值为:',scores.mean())

学习算法的选择

首先需要确定问题的类型,是监督学习还是无监督学习?如果是监督学习,是分类问题还是回归问题?如果是分类问题,是二分类问题还是多分类问题?确定问题的类型可以帮助我们找到适合该类问题的相应算法。

在划定问题类型后,已经可以排除掉很多的算法。接着需要了解数据的类型和形态。有些算法可以利用较小的样本集合工作,而另一些算法则需要海量的样本。特定的算法对特定类型的数据起作用。在之前的预处理和探索性分析中,我们已经对数据有了一定的了解,可以根据数据集的大小和特征稀疏度来进行选择。(需要建立在对模型原理和特点充分了解的基础上)此外,还需要考虑模型的复杂度是否符合实际的约束条件,如模型训练速度、预测速度与数据容量。

模型的复杂度是一个影响算法选择的重要标准。复杂的模型具备下列特征:

  • 依赖于更多的特征进行学习和预测(例如,使用十个而不是两个特征来预测目标)
  • 依赖于更复杂的特征工程(例如,使用多项式特征、交互特征或主成分)
  • 更大的计算开销(例如,需要一个由 100 棵决策树组成的随机森林,而不是一棵单独的决策树)

除此之外,同样的算法可以基于参数的个数和超参数的选择而变得更加复杂。例如:

  • 回归模型可以拥有更多的特征,或者多项式项和交互项。
  • 决策树可以拥有更大或更小的深度。

将相同的算法变得更加复杂增加了发生过拟合的几率。

这里需要对大部分的机器学习算法有所了解,学习相应模型的原理,不然只能成为一个调包侠。

【思考】

  • 为什么线性模型可以进行分类任务,背后是什么样的数学关系?
    • 线性模型可以拟合出类别之间的分界线,模型根据该分界线来进行分类。
  • 对于多分类问题,线性模型是怎么进行分类的?
    • 对于多分类问题模型就可以训练得到多条分界线,将整个数据集划分为多个区域,每个区域对应一个类别

训练模型并输出预测结果

sklearn涵盖了很大一部分的机器学习模型,但基本的调用方式都是一致的:

  1. 确定要使用的算法,定位其在sklearn中的位置
  2. 查找算法的初始化属性(超参数),创建一个相应算法的实例,如:lr= LogisticRegression(C=100,max_iter=500)
  3. 调用算法的fit方法,传入训练集以进行模型的训练
  4. 调用算法的predict()predict_proba()(部分算法含有decision_function()方法)来获得训练好的模型输出的结果
  5. 输出准确率(对于分类问题,score()方法)或代价(对于回归问题),评估模型输出的结果

举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定位,导包
from sklearn.ensemble import RandomForestClassifier
# 创建对象,设定模型超参数
rfc = RandomForestClassifier(max_depth = 6)
# 训练模型
rfc.fit(X_train, y_train)
# 预测结果
y_predict = rfc.predict(X_test)
# 计算预测准确率
# 方式1,自己写的
accuracy = (y_predict == y_test).sum()/y_predict.size
print('测试集预测准确度为:',accuracy)
# 方式2,自带的方法
print('训练集集预测准确度为:',rfc.score(X_train, y_train))
print('测试集预测准确度为:',rfc.score(X_test, y_test))

模型超参数的选择,往往借助试错法(try error)。除了训练集与测试集,一般会从数据集中取出单独的一部分作为验证集(validation set)用于超参数的优化,超参数的优化也是机器学习与深度学习重要的组成部分。

对于某些应用场景,如违约概率预测,需要给出可能性/不确定度,分类器一般情况下有两种方法估计不确定度:

  • 概率的形式,如predict_proba()
  • 置信分,如decision_function()

前者返回模型输出的一组,测试样本属于各个类别的概率。基于每个样本的一组概率值,一种决策方式为将概率从大到小排布,取最高者作为模型最终决策结果;另一种决策方式则只认可概率超过阈值的类别为决策结果,对于不平衡类别数据而言,多类别数据会学到更多信息,所以也就可以适当增加其决策的难度,比如设置大于0.6才能分为1。

后者适用于二分类问题,默认返回一个置信分,置信分大于0,模型认为输入测试样本属于第一类,反之认为其属于第二类。

模型评估

模型的评估也是建立在数据预测的具体需求上的,这里介绍的评估指标主要面向分类问题。

混淆矩阵

根据二分类的分类结果可以将样例分为四类:真正例(TP)、假正例(FP)、真反例(TN)和假反例(FN),一图以概之:

基于样例的四种分类矩阵——混淆矩阵,衍生出了查准率、查全率等一系列模型的评价指标。

准确率(accuracy)

一般输出的结果,即预测结果和原来样本有多少一致。

查准率(precision)

针对预测结果而言的,表示的是预测为正的样本中有多少是真正的正样本。预测为正就有两种可能了,一种就是把正类预测为正类(TP),另一种就是把负类预测为正类(FP)。
$$
P=\frac{TP}{TP+FP}
$$

查全率(recall)

针对原来的样本而言的,表示的是样本中的正例有多少被预测正确了。也有两种可能,一种是把原来的正类预测成正类(TP),另一种就是把原来的正类预测为负类(FN)。
$$
R = \frac{TP}{TP+FN}
$$
二者仅仅分母不同,结合混淆矩阵即可加深理解:

PR曲线

PR曲线以查准率为纵轴,查全率为横轴。

PR曲线直观地显示出学习器在样本总体上的查全率、 查准率,在进行比较时,有两种情况:

  • 一个学习器的PR曲线被另一个学习器的曲线完全包住:后者的性能优于前者(双高)
  • 两个学习器的PR曲线相交:使用曲线下面积进行比较(一定程度上表征了学习器在查准率和查全 率上取得相对双高的比例)

由于面积不易计算,更多地使用到下方的几种度量指标。

F1度量

F1,即查准率与查全率的调和平均(倒数的平均),可以作为综合衡量查准率与查全率的指标:
$$
F1=\frac{2\times P\times R}{P+R}=\frac{1}{2}(\frac{1}{P}+\frac{1}{R})
$$
对于不同的使用场景,人们对学习器的偏好指标是不同的,例如商品推荐中,平台更希望推荐的内容的确是用户感兴趣的(查准率),而其他场景下,例如缺陷零件检验中,人们更偏好查出更多可能存在缺陷的零件(查全率)。因此诞生了$F1$的一般形式$F_{\beta}$,$\beta$代表查全率对查准率的相对重要性:
$$
F_\beta=\frac{(1+\beta^2)\times P\times R}{(\beta^2\times P)+R}=\frac{1}{1+\beta^2}(\frac{1}{P}+\frac{\beta^2}{R})
$$

代码实现

主要使用到sklearn.metrics模块中以下的一些方法:

  • confusion_matrix(y_test, y_test_predict)
  • classification_report(y_test, y_test_predict)

该模块中还包含了很多其他的评价指标以供使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 导入对应模块
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
# 模型预测结果
lr.fit(X_train, y_train)
y_test_predict = lr.predict(X_test)
# 输出混淆矩阵
confusion_matrix(y_test, y_test_predict)
# 也可以以热力图的形式来输出混淆矩阵
#import seaborn as sns
#plt.figure(figsize=(8,6))
#sns.heatmap(confusion_matrix(y_train, rfc.predict(X_train)),annot=True,cmap='Blues')
#plt.xlabel('Predicted labels')
#plt.ylabel('True labels')
#plt.show()
# 精确率、召回率以及F1
print(classification_report(y_test, y_test_predict))

返回结果如下:

ROC与AUC

ROC(受试者工作特征)曲线以真正例率为纵轴,假正例率为横轴绘制曲线。图中每个点则是不同截断点下计算的FPR和TPR,截断点相当于模型对于样本的概率输出,也可以说是打分Score。

ROC计算过程:

  1. 得到所有样本的概率输出(属于正样本的概率)
  2. 根据每个测试样本属于正样本的概率值从大到小排序
  3. 从高到低,依次将Score值作为阈值threshold,当测试样本属于正样本的概率大于等于这个threshold时,认为它为正样本,否则为负样本。
  4. 每次选取一个不同的threshold,就可以得到一组FPR和TPR,即ROC曲线上的一点。
  5. 将点依次连线,构成ROC

ROC可以评判学习器得到实值排序质量的好坏,而排序质量又体现了综合考虑学习器在不同任务下的期望泛化性能,因此,ROC是研究学习器泛化性能的一项有力工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
## ROC曲线的绘制

# 导入绘制ROC曲线的模块
from sklearn.metrics import roc_curve
# 得到ROC曲线的系列值
fpr, tpr, threshold = roc_curve(y_test, lr.decision_function(X_test))
# 绘制ROC曲线
sns.lineplot(x=fpr,y=tpr,label='ROC')
close_zero = np.argmin(np.abs(threshold))
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero", fillstyle="none", c='steelblue', mew=2)
plt.legend()

AUC即ROC曲线下方的面积,AUC考虑的是样本预测的排序质量,因此它与排序误差有紧密联系,而排序误差实际就是ROC曲线上方的面积,具体数学解释可以参考西瓜书P35。

1
2
# 计算AUC
roc_auc_score(y_test, lr.decision_function(X_test))

【思考】

  • 对于多分类问题如何绘制ROC曲线?

    • 两两一组计算TPR/FPR,再取平均,这种方法得到的ROC可以称作宏观ROC
    • 两两一组计算TP/FP/TN/FN,取平均后计算TPR/FPR,称为微观ROC
  • 从ROC曲线能得到什么信息?这些信息可以做什么?

    • 模型的泛化性能,即学习器在不同任务下的期望泛化性能,可以评价不同模型的好坏

评论