机器学习-参数选择与模型验证

0

在很多时候并不能直接将数据以可视化的形式表达出来,这就意味着无法以直观的方式检验模型的分类效果。本节介绍机器学习中常用的模型验证方法以及如何选择恰当的超参数。

模型检验方法

交叉验证

最简单验证模型的方法就是调用模型的 .score() 方法,检查给定数据的准确率。例如,对于以下数据:

接下来建立一个决策树模型并用 .score() 方法检查训练数据的准确率:

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier()
model.fit(X, y_true)

DecisionTreeClassifier()

model.score(X, y_true)
1.0

结果显示模型的准确率是 100% 。当然,这种验证方法是不合理的,因为以上并没有对决策树模型做任何约束,此时可能已经发生了过拟合,因此可以 100% 拟合训练数据。

解决过拟合的一种思路是引入测试数据,测试数据不供模型学习,可以防止过拟合的影响。但是有时不易获取测试数据,这就需要将现有数据拆分为训练集和测试集:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
                X, y_true, random_state=6, train_size=0.7)
model.fit(X_train, y_train)
model.score(X_test, y_test)
0.9142857142857143

以上函数将现有数据拆分为训练集和测试集,其中训练集占所有数据的 70% 。对测试集应用 .score() 方法的结果表明,模型的准确率为 91.4% ,更接近模型真实的准确率。

直接将数据分为训练集和测试集的缺点是模型失去了一部分可用于训练的数据,且这部分数据中可能包含某些关键的信息(比如支持向量)。一个更好的思路是将数据集划分为若干部分,每次将一个部分用于检验,其它部分用于训练,这样数据集的每个部分都能用于训练和检验。这种策略称为交叉验证(cross validation)。下图展示了一个 4 轮交叉验证的构成:

手动拆分数据比较繁琐,不过可以借助 Scikit-Learn 提供的工具来处理:

from sklearn.model_selection import cross_val_score

scores = cross_val_score(model, X, y_true, cv=5)
scores
array([0.975, 0.9 , 0.975, 0.875, 0.925])

可以通过 cv 参数修改交叉验证的轮数,默认采用 5 轮交叉验证。对每轮交叉验证的准确率取均值,就非常接近实际的准确率了:

scores.mean()
0.93

一种极端的情况是将交叉验证的轮数设置为和样本的个数一样多,这种情况下每次只使用一个样本用于验证,而其它的样本都用于训练,这称为留一(leave-one-out, LOO)交叉检验。要使用留一交叉验证不能直接将 cv 设置为样本的大小,而是需要借助 LeaveOneOut 类:

from sklearn.model_selection import LeaveOneOut
scores = cross_val_score(model, X, y_true, cv=LeaveOneOut())
scores.mean()
0.925

在之前的学习中认识了模型的过拟合现象。过拟合现象就是随着模型的复杂度上升,虽然对数据的拟合效果变好,但是模型的泛化程度下降,对新的数据不能很好地处理。反应在交叉验证下,就是训练集效果越来越好,但是测试集效果反而越来越差:

这个曲线同时表明训练集的准确率总是比验证集的准确率高,并且随着模型复杂度的提升,训练集的准确率越来越高,甚至趋近 100% ;而验证集的准确率增长到某个值后反而开始下降。

混淆矩阵

以上均使用准确率作为衡量模型好坏的指标,但这可能会导致一些问题。一个准确率较高的模型不一定效果就好。例如,对于以下数据:

假设使用逻辑回归模型拟合这些数据,并且不对数据添加额外特征,那么得到模型的准确率为:

from sklearn.linear_model import LogisticRegression
model = LogisticRegression().fit(X, y_true)
cross_val_score(model, X, y_true).mean()
0.8181818181818183

82% 的准确率看似不错,但是注意到准确率会存在以下问题:对于二分类问题,如果两个分类样本数相近,那么即便模型随便预测,总体的准确率也接近 50% ;如果两个分类样本数相差较大,那么模型只要倾向于预测样本数较多的那个分类,准确率就不会差。例如对于以上数据,假设某个模型均预测为蓝色类别,那么它的准确率为:

from sklearn.metrics import accuracy_score
accuracy_score(y_true, np.zeros_like(y_true))
0.8909090909090909

结果甚至比逻辑回归还要好,但这种模型肯定是完全没有研究和使用价值的。

对于二分类模型,只有预测正确(True)和错误(False)的可能。记两种类别为正类(Positive)和负类(Negative),那么一个模型的预测结果只有以下四种情况:

Positive(预测真) Negative(预测假)
True(实际真) True Positive(TF) 正确肯定 False Negative(FN) 漏报
False(实际假) False Positive(FP) 虚报 True Negative(TN) 正确否定

所有的评估参数都由该表中的元素组成,该表也被称为混淆矩阵(confusion matrix)。在之前一直使用准确(accuracy)率衡量模型,它的公式如下:

\\[ \text{accuracy} = \frac{\text{True}}{\text{All}} = \frac{\text{TP}+\text{TN}}{\text{TP}+\text{FN}+\text{TN}+\text{FP}} \\]

这个公式的问题就在于它同时考虑了正类和反类,但两者的重要性是不一样的:样本多的类别对准确率的影响更大,但并不意味着模型应该优先满足样本多的类别的准确率。如果模型更关心正类的分类质量,那么有两个指标可以说明正类的分类效果好坏:

精确(precision)率:精确率用于衡量预测为正类的样本预测对的概率,即:

\\[ \text{precision} = \frac{\text{True}}{\text{Predicted Positive}} = \frac{\text{TP}}{\text{TP}+\text{FP}} \\]

如果模型的精确率高,说明预测的正类大多都预测对了。不过也不能一昧只提高精确率,那样的话模型为了确保预测的正类是对的,会倾向于减少预测为正类的样本数量。另一个指标召回率(recall)用于衡量实际为正类的样本预测对的概率,即:

\\[ \text{recall} = \frac{\text{True}}{\text{Actual Positive}} = \frac{\text{TP}}{\text{TP}+\text{FP}} \\]

召回率高说明实际的正类大多都找出来了。同样,一昧提高召回率也会使得模型为了尽可能找出所有正类,倾向于增加预测为正类的样本数量。

容易看出,精确率和召回率之间具有互补关系:如果一个模型的精确率和召回率都很高,说明模型既能找出数据集中的正类,同时也能排除不属于正类的样本(找出所有的负类),那么这个模型的效果很好。通过组合精确率和召回率可以得到一个比较通用的指标,例如常用的 \\( F_1 \\) 分数:

\\[ F_1 = \frac{2}{ \displaystyle{\frac{1}{\text{precision}}} + \displaystyle{\frac{1}{\text{recall}}} } \\]

\\( F_1 \\) 分数使用调和平均数组合这两个指标。调和平均数是总体各统计变量倒数的算术平均数的倒数,由于不知道精确率和召回率哪个指标对总体的贡献更大,因此不能使用加权平均值,而调和平均数会赋予低值更高的权重,只有当召回率和精度都很高时才能得到较高的 \\( F_1 \\) 分数。

在 Scikit-Learn 中,可以通过 metrics 模块提供的工具计算 \\( F_1 \\) 分数。

from sklearn.metrics import f1_score
f1_score(y_true, model.predict(X))
0.4

计算得到模型的 \\( F_1 \\) 分数只有 0.4 ,说明对正类的分类效果完全不像整体的准确率一样还算不错。

有时可能需要模型必须保证找到的正类都是对的,而允许模型存在一定的漏检(例如在登录时的用户面部识别,如果将错误的样本划归为正类会招致严重的损失);有时可能需要确保模型没有漏检,而允许模型找到的正类不必都是对的(例如查找嫌疑人时的面部识别,如果遗漏一个正类会导致严重的后果)。这时可能会更关心精确率和召回率的其中一个,除了上文中见到的准确率计算,该模块也包含精确率和召回率的计算函数,可以组合它们拼凑一个需要的指标。

评估曲线

以上的准确率、精确度、\\( F_1 \\) 值等都只是一个单一的数值指标,如果想观察分类算法在不同的参数下的表现情况,就可以使用一条曲线描述。

ROC(Receiver Operating Characteristic, 受试者工作特征)曲线是一种用于评估二元分类器性能的工具。它的横纵坐标都是来自混淆矩阵的组合指标:

  • 横坐标为假正例率(False Positive Rate, FPR),表示在所有真反例中,分类器错误识别为正例的比例 \\( \text{FPR} = \dfrac{\text{FP}}{\text{FP}+\text{TN}} \\)
  • 纵坐标为真正例率(True Positive Rate, TPR),表示在所有真正例中,分类器正确识别为正例的比例 \\( \text{TPR} = \dfrac{\text{TP}}{\text{TP}+\text{FN}} \\) ,就是刚才介绍的召回率

ROC 曲线的特点是,它能够展示分类器在不同阈值下的性能表现。这里的阈值指的是分类器判断一个样本为正例的概率的截断值。一般情况下,当阈值设定为 0.5 时,分类器将所有概率大于 0.5 的样本判断为正例,而将所有概率小于等于 0.5 的样本判断为反例。

提高阈值会使预测为正类的样本数量减少,但预测的样本是正类的可能性都很高,因此可以提高模型的精确率。同样地,降低阈值会让模型将一些不太可能是正类的样本预测为正类,但也可能找出更多实际是正类的样本,因此可以提高模型的召回率。

ROC 曲线呈现了不同阈值下的假正例率和真正例率,如下图所示:

一个良好的模型应该拥有高真正例率(TPR)和低假正例率(FRP),即尽可能找出属于该组的,但也要排除不属于该组的。一个完美的模型希望假警报率接近 0 ,命中率接近 1 ,即左上角顶点。一般的模型不可能达到该值,只能尽可能接近它,反应在图形上就是 ROC 曲线最接近左上角的一点,为模型的最优情况。

sklearn 中,绝大多数分类器都有 .predict_proba() 方法,可以得到该模型判断样本为每个分类的概率:

model.predict_proba(X)
array([[0.0925382 , 0.9074618 ], [0.24097764, 0.75902236], [0.05459936, 0.94540064], ..., [0.20851233, 0.79148767]])

对于二分类模型,判断样本为正例或反例的概率加起来应该等于 1.0 。

有了这些概率数据,就可以通过设置不同阈值来观察假正例率和真正例率的变化。sklearn 提供了如下工具来计算 \\( \text{TPR} \\)\\( \text{FPR} \\) 在不同阈值下的表现情况:

from sklearn.metrics import roc_curve
FPR, TPR, thres = roc_curve(y_true,
                    model.predict_proba(X)[:, 1])

这里采用类别 1 作为正类。计算得到的结果是排序后的,可以直接调用 matplotlib 绘制曲线,得到的结果可能是这样的:

在数值上通常使用 AUC(Area Under the ROC Curve, ROC 曲线下面积)作为衡量分类器性能的指标,AUC 取值范围为 \\( (0.5,1) \\) ,越接近 1 表示分类器的性能越好。通常达到 0.75 表示可以接受,0.85 表示非常不错。

如果不是为了可视化,一般可以使用以下工具直接计算 AUC 值:

from sklearn.metrics import roc_auc_score
roc_auc_score(y_true, model.predict_proba(X)[:, 1])
0.8382

进一步地,通过绘制 K-S 曲线可以分析模型随阈值变化的特性。K-S(Kolmogorov-Smirnov)检验用于测量两个累积分布是否一致。在二分类问题中,K-S 检验用于度量正分布和负分布之间的分离程度。

随着阈值的上升,真正例率(TPR)可以理解为累积正样本率,假正例率(FPR)可以理解为累积负样本率,因此可以使用这两个指标绘制 K-S 曲线。绘制时一般将阈值作为横坐标,真正例率(TPR)和假正例率(FPR)之差作为纵坐标,如下图所示:

K-S 曲线有助于调整模型的决策阈值。K-S 值是两个两个累积分布曲线的最大垂直距离。在二分类问题中,它是曲线的纵坐标最大值。K-S 值的取值范围为 0~1 ,较高的值表示正类和负类之间的分离更好,即模型的分类效果更好。不过应该注意的是,K-S 检验假设数据是独立同分布的,在实践中可能未必如此。

参数选择方法

在了解了如何评价一个模型以及阈值对模型的影响后,接下来的问题是如何才能得到最优的模型,这就涉及到为模型选择合适的参数。

鸢尾花数据集

接下来介绍著名的鸢尾花数据集,它是一个经典的多分类数据,经常用于检验新模型的分类效果。它是 sklearn 内置的数据集之一,可以通过以下方法加载它:

from sklearn.datasets import load_iris
iris = load_iris()

得到的是一个类似字典的对象,它有以下键:

iris.keys()
dict_keys(['data', 'target', 'frame', 'target_names', 'DESCR', 'feature_names', 'filename', 'data_module'])

主要用到的键对应的值的含义为:

  • feature_names :特征名称
  • data :特征数据,是一个二维数组对象
  • target :目标数据
  • target_names

这些数据大致分布如下:

iris['data']iris['target'] 符合模型对特征矩阵 X 和目标数据 y 的要求,因此可以直接用于训练模型。

网格搜索

如果不确定某个参数要如何选择,需要多次调整参数并观察模型指标的变化,以此选择参数的最优值。而如果有多个参数需要选择,那么也需要调整参数的组合,并从中选择最优的结果。

网格搜索是一种通过穷举搜索来选择最优参数组合的手段,它将使用所有候选参数的组合来循环建立模型,并选取表现最好的参数作为最终结果。

网格搜索的思路很简单,就是暴力穷举参数组合并观察效果。Scikit-Learn 提供了 GridSearchCV 工具来实现此过程的自动化。如果现在已经有了一个模型和一些候选参数:

clf_tree = DecisionTreeClassifier()
params_tree = {
    'min_samples_split': [2, 4, 6, 10],
    'min_samples_leaf': [1, 3, 6, 10]
}

那么首先需要创建一个网格搜索器:

grid_search = GridSearchCV(clf_tree, params_tree,
                           scoring='f1_weighted', cv=5)

这样,GridSearchCV 会使用 5 轮交叉验证评估每个参数组合的效果,防止过拟合的影响。这里的评估标准 "f1_weighted" 指的是加权 F1 分数(Weighted F1-Score),加权 F1 分数是对每个类别的 F1 分数进行加权平均,权重为各类别的样本数量占总样本数量的比例。关于 sklearn 提供的更多检验标准,可以参考官方文档

有了评估器以后,就可以使用 .fit() 方法,表示应用到数据

grid_search.fit(iris['data'], iris['target'])

应用数据之后,就可以获取最优的参数组合,例如:

print(grid_search.best_params_, grid_search.best_score_)
{'min_samples_leaf': 1, 'min_samples_split': 6} 0.9664818612187034

如果只是想检查参数组合对模型分数的影响,也可以通过 .cv_results_ 属性获取。它是一个很详细的数据,同时还记录了训练时间、验证时间等信息。

GridSearchCVrefit 参数默认被设为 True ,这代表找到了最优的参数组合后,它将立刻被应用于训练模型,并且所有的数据都将作为训练集,这个完善的模型可以通过 .best_estimator_ 获取,并直接参与 .predict() 的决策中。

最后要注意的是,如果这个模型是使用 make_pipeline() 与预处理结合得到的多个步骤,那么每个步骤的参数要用两个下划线区分,整个字典看起来类似这样:

params = {
    'logisticregression__penalty': ['l1', 'l2', 'none'],
    'logisticregression__C': [0.2, 1.0, 4.0],
    'polynomialfeatures__degree': [3, 4, 5]
}

随机搜索

网格搜索的缺点是如果参数列表很多,那么搜索范围会变得很大,再加上交叉验证会严重拖慢搜索进度。此时可以使用随机搜索 RandomizedSearchCV ,它只会挑选部分而不是全部的参数组合,再从这部分随机组合中挑选最优结果。这个类的用法与 GridSearchCV 大致相同。

随机搜索不仅速度更快,而且对是连续值的参数效果更好,因为随机搜索可以很方便地抽取大量参数用于检验,每个参数可能差别很小,完整训练的代价太大。

京ICP备2021034974号
contact me by hello@frozencandles.fun