AAAMLP-Chapter-2: Cross Validation

167 阅读11分钟

在构建机器学习模型前,我们必须理解交叉验证是什么,如何在数据集上选择最优的交叉验证方案。

有许多关于交叉验证的定义,一句话总结:交叉验证是构建机器学习模型的一道处理步骤,帮我们保证模型对数据的准确拟合,同时保证不会过拟合。

这里引入了一个新词,过拟合。以著名的 red wine quality 数据集为例,来解释什么是过拟合。

数据集下载地址:

该数据集包含 11 种不同特征,用以描述红酒质量:

  • fixed acidity 固定酸度

  • volatile acidity 挥发酸度

  • citric acid 柠檬酸

  • residual sugar 残糖

  • chlorides 氯化物

  • free sulfur dioxide 游离二氧化硫

  • total sulfur dioxide 全部二氧化硫

  • density 密度

  • pH pH值

  • sulphates 硫酸盐

  • alcohol 酒精度

接下来,需要我们基于上述特征,预测红酒质量,质量值在 0-10 之间。


首先,用代码加载数据,并简单查看原始数据。

import pandas as pd

df = pd.read_csv('winequality-red.csv')
print(df.shape)
print()
print(df.head())
print()
print(df.dtypes)
print()
print(df['quality'].unique())

输出结果中,可以看到 quality 字段的类型为 int ,去重后的取值范围为 [5 6 7 4 8 3] ,这表示该问题属于分类问题。

我们也可以将其视为回归问题,只需将 quality 字段的取值转为 0-10 之间的实数。

为了简化起见,将其作为分类问题处理。

下边将目标值 quality 的标签进行重排序。

quality_mapping = {
    3: 0,
    4: 1,
    5: 2,
    6: 3,
    7: 4,
    8: 5
}
df.loc[:, 'quality'] = df.quality.map(quality_mapping)

在检查过数据,并将其视作分类问题后,应该会想到许多可用的处理算法,比如神经网络。

但是一开始就使用神经网络方法,属于过分延伸,让人难以接受,因此,让我们使用简单的、易于可视化的方法:决策树。


在开始理解过拟合之前,首先将数据分成两个部分。

该数据集包含 1599 个样本,随机选择 1000 个样本为训练集, 剩下的样本为测试集。

# Shuffle and Re-Index
df = df.sample(frac=1).reset_index(drop=True)
df_train = df.head(1000)
df_test = df.tail(599)

接下来,训练决策树模型。

from sklearn import tree, metrics

clf = tree.DecisionTreeClassifier(max_depth=3)
cols = [
    'fixed acidity',
    'volatile acidity',
    'citric acid',
    'residual sugar',
    'chlorides',
    'free sulfur dioxide',
    'total sulfur dioxide',
    'density',
    'pH',
    'sulphates',
    'alcohol'
]
clf.fit(df_train[cols], df_train.quality)

这里将决策树分类器的参数 max_depth 设置为 3,其他参数保持默认值。

现在,测试模型在训练集和测试集上的准确率。

train_predications = clf.predict(df_train[cols])
test_predications = clf.predict(df_test[cols])
train_accuracy = metrics.accuracy_score(
    df_train.quality, train_predications
)
test_accuracy = metrics.accuracy_score(
    df_test.quality, test_predications
)
print(train_accuracy, test_accuracy)

训练与测试准确率分别为 58.9% 和 54.25% 。

接着,将决策树分类器的 max_depth 参数增加到 7,重新执行上述处理过程,得到的准确率将会分别为 76.6% 和 57.3% 。

准确率是模型的一个很直观的度量,但对于该问题,准确率可能不是最优的度量。

那么,将不同 max_depth 参数的决策树计算的准确率绘制在同一个图中,能得到什么样的结果?

from sklearn import tree, metrics
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

matplotlib.rc('xtick', labelsize=20)
matplotlib.rc('ytick', labelsize=20)
%matplotlib inline

train_accuracies = [0.5]
test_accuracies = [0.5]

for depth in range(1, 25):
    clf = tree.DecisionTreeClassifier(max_depth=depth)
    cols = [
        'fixed acidity',
        'volatile acidity',
        'citric acid',
        'residual sugar',
        'chlorides',
        'free sulfur dioxide',
        'total sulfur dioxide',
        'density',
        'pH',
        'sulphates',
        'alcohol'
    ]
    clf.fit(df_train[cols], df_train.quality)
    
    train_predications = clf.predict(df_train[cols])
    test_predications = clf.predict(df_test[cols])
    train_accuracy = metrics.accuracy_score(
    df_train.quality, train_predications
    )
    test_accuracy = metrics.accuracy_score(
        df_test.quality, test_predications
    )

    train_accuracies.append(train_accuracy)
    test_accuracies.append(test_accuracy)

plt.figure(figsize=(10, 5))
sns.set_style('whitegrid')
plt.plot(train_accuracies, label='train_accuracy')
plt.plot(test_accuracies, label='test_accuracy')
plt.legend(loc='upper left', prop={'size': 15})
plt.xticks(range(0, 26, 5))
plt.xlabel('max_depth', size=20)
plt.ylabel('accuracy', size=20)
plt.show()

得到的图像如下图所示。

可以看到随着 max_depth 的增加,训练准确率不断提高,而测试准确率则基本不变。

这说明决策树模型随着深度的提升,它对训练数据的学习效果越来越好,但不再提升测试效果。

这种现象就是 过拟合 。过拟合意味着模型对于训练集以外数据的泛化能力不再提升。


上述样例中,某人可以训练一个 max_depth 参数非常大的模型,能够在训练集上给出完美预测,但该模型拿来预测真实世界的数据时,并不能提供同样好的预测效果,甚至可能不起作用。

有人可能会说,上边讨论的并不是过拟合,因为测试集上的准确率几乎保持不变。

另一个过拟合的定义是:当测试损失增加时,仍然优化训练损失。该定义常在神经网络中使用。

在训练神经网络时,我们必须同时监测模型在训练集和测试集上的损失变化。

如果我们在训练样本数量稀少,而网络参数数量庞大的情况下,训练神经网络模型,我们将会观察到以下现象:在训练开始时,模型在训练集和测试集上的损失值同时减少;到某一时刻,测试集损失值到达其最小点,随后开始增大,同时训练集损失值仍然持续减少。

因此我们需要在测试集损失值到达其最小点时,停止训练。

这才是最通用的对过拟合的解释。


现在,我们可以回过头来讲什么是交叉验证 Cross Validation 。

在解释过拟合时,我们将数据划分为两部分。用于训练的部分为训练集,用于检查模型效果的部分为测试集。该方法也是一种交叉验证方式,叫流出法 hold-out set 。

有许多种不同的交叉验证方式,这是提升模型泛化能力的关键步骤,用以提升模型在未知样本上的效果。

选择恰当交叉验证方式,取决于要处理的数据集。

这些交叉验证方式包括:

  • k-fold cross-validation

  • stratified k-fold cross-validation

  • hold-out based validation

  • leave-one-out cross-validation

  • group k-fold cross-validation

交叉验证是将训练数据分为多个部分,在一些部分上进行训练,在剩下的部分上进行测试,如下图所示。

图中将数据集中的样本 Sample 和目标值 Target 绑定在一起,划分为两个集合,训练集和验证集。许多人会将数据集划分为三个集合,训练集、验证集和测试集。


也可以将数据集划分为 K 个大小相等的不同集合,这种方式即为 k-fold cross-validation ,k-折交叉验证。

我们可以使用 scikit-learn 包,把任何数据划分为 k 个大小相等的部分。

这样数据集中每个样本都会有一个分组 id,取值范围是 0 到 k-1 。

import pandas as pd
from sklearn import model_selection

df = pd.read_csv('winequality-red.csv')
df['kfold'] = -1
df = df.sample(frac=1).reset_index(drop=True)

kf = model_selection.KFold(n_splits=5)
for fold, (trn_, val_) in enumerate(kf.split(X=df)):
    df.loc[val_, 'kfold'] = fold
df.to_csv('winequality-red-folds.csv', index=False)

你可以对任何数据集做此处理。例如,当你有一堆图片时,可以创建一个包含图片id、图片路径、图片标签的 csv 文件,并对其执行上述处理。


下一个重要的交叉验证类型是:stratified k-fold cross-validation,分层 k-折交叉验证。

如果你手上有一个不平衡的、用于二分类的数据集,其中 90% 的样本为正例、10% 的样本为负例。对该数据集上进行简单 k-fold 交叉验证,显然会导致负样本完全集中在某个划分里。

这种情况下,使用 stratified k-fold cross-validation 效果更好,该方法会按标签值的比例进行划分,保证划分中标签值比例与全部数据中标签值比例相同。因此,在各划分中,都有 90% 的正样本和 10% 的负样本。这样,无论选择何种度量方式,在划分中都可得到相似的度量结果。

下边简单修改 k-fold 交叉验证代码,演示 stratified k-fold 交叉验证处理。

import pandas as pd
from sklearn import model_selection

df = pd.read_csv('winequality-red.csv')
df['kfold'] = -1
df = df.sample(frac=1).reset_index(drop=True)

y = df.quality.values
kf = model_selection.StratifiedKFold(n_splits=5)
for fold, (trn_, val_) in enumerate(kf.split(X=df, y=y)):
    df.loc[val_, 'kfold'] = fold
df.to_csv('winequality-red-folds.csv', index=False)

对于红酒数据集,可视化查看标签分布。

df = pd.read_csv('winequality-red.csv')
b = sns.countplot(x='quality', data=df)
b.set_xlabel('quality', fontsize=20)
b.set_ylabel('count', fontsize=20)

标签分布图如下所示,我们可以从图中得出结论:标签值分布非常不平衡,一些类型有超多样本,一些类型则没有那么多。

如果在这个数据集上简单的使用 k-fold 方法,得到的每个 fold 中的标签分布不可能一样。因此,需要使用 stratified k-fold 方法。

至此,我们可以得出一个简单的规则:如果当前问题是一个标准分类问题,那么直接选择 stratified k-fold cross-validation 。


对于海量数据,我们应该如何处理呢?

假设我们有一个包含 1000k 个样本的数据集,对其进行 5 fold cross-validation 意味着在每折训练时使用 800k 个样本,在验证时使用 200k 个样本。

根据我们选择的算法,在这个规模的数据集上进行训练和验证将会非常耗时。

在这种情况下,我们可以选择使用 hold-out based validation。

创建 hold-out 的处理过程和 stratified k-fold 一样。对于包含 1000k 个样本的数据集,我们可以创建 10 个 fold,并选择其中一个作为 hold-out。这表示我们选择了 100k 个样本作为 hold-out,然后在剩下的 900k 个样本上计算 loss 、accuracy 等度量。

Hold-out 方法在处理时间序列数据中经常使用。假设我们拥有某商店 2015-2019 年的所有数据,目标是预测 2020 年商店的销售额。这种情况下,我们选择 2019 年所有数据做 hold-out,只使用 2015-2018 年的数据来训练模型。

很多时候,我们在小数据集上创建了较大的验证集,这意味着模型训练时会丢失许多样本。

这种情况下,我们可以选择 N fold 交叉验证方法,N 为所有样本数量。这表示在每折训练中,模型的训练样本数为 N-1,验证样本为 1 。需注意,该方法在模型计算速度不够快时,时间开销很大。但该方法通常在小数据集上使用,一般不会有太大影响。


现在,我们可以进一步讨论回归问题。

对于回归问题,我们可以使用除 stratified k-fold 以外的所有上述交叉验证方法。

这只是说不能直接使用 stratified k-fold,有许多修改方法可以让我们在回归问题中使用 stratified k-fold 。

简单 k-fold 在所有回归问题上都能直接使用,但是如果目标值分布连续、不均衡,我们还是需要使用 stratified k-fold 。

最简单的方法是,将目标值分到两个宽度相等的 bin (箱) 中 ,然后以 bin id 为目标值,使用 stratified k-fold 方法。

有一些选择适当的 bin 个数的方法。

如果数据集数量在 100k 数量级,不用关心 bin 的最恰当数量是多少,直接选择 10 或者 20。

如果数据集中没有太多数据,可使用 Sturge‘s Rule 来计算恰当的 bin 数,其中 N 为样本数量。

NumberOfBins=1+log2(N)NumberOfBins = 1 + log_2(N)

下边用代码演示一下如何对回归任务的数据集进行 stratified k-fold 处理。

import numpy as np
import pandas as pd
from sklearn import datasets, model_selection

def create_folds(data):
    data['kfold'] = -1
    data = data.sample(frac=1).reset_index(drop=True)
    
    num_bins = int(np.floor(1 + np.log2(len(data))))
    data.loc[:, 'bins'] = pd.cut(
        data['target'],
        bins=num_bins,
        labels=False
    )

    kf = model_selection.StratifiedKFold(n_splits=5)
    for f, (t_, v_) in enumerate(kf.split(X=data, y=data.bins.values)):
        data.loc[v_, 'kfold'] = f
    data = data.drop('bins', axis=1)
    return data

X, y = datasets.make_regression(
    n_samples=15000,
    n_features=100,
    n_targets=1
)
df = pd.DataFrame(
    X, 
    columns=[f'f_{i}' for i in range(X.shape[1])]
)
df.loc[:, 'target'] = y
df = create_folds(df)

在构建机器学习模型时,交叉验证是第一步,也是最关键的一步。

如果你想做特征工程,首先得划分数据。

如果你想训练模型,首先得划分数据。

如果你的验证数据能够表达训练数据和真实世界数据的特征,你将会训练出高度泛化的模型。

本章介绍的交叉验证方法可以用于任何机器学习任务,但你要时刻牢记,交叉验证方法的选择高度依赖数据集和目标问题。

例如,要构建一个二分类模型,根据皮肤癌病人的皮肤图片,预测一个概率值,表示癌症是良性或恶性的。

这类数据集中,可能训练集里有很多同一个病人的图片。

要构建一个优秀的交叉验证系统,必须使用 stratified k-folds,同时要保证训练集中的病人不会出现在验证集中。

幸运的是,scikit-learn 提供了一种名为 GroupKFold 的交叉验证方法。在这个例子中,可以将病人分组处理。但是没有直接结合 GroupKFold 和 StratifiedKFold 的方法,需要你自己处理。