在构建机器学习模型前,我们必须理解交叉验证是什么,如何在数据集上选择最优的交叉验证方案。
有许多关于交叉验证的定义,一句话总结:交叉验证是构建机器学习模型的一道处理步骤,帮我们保证模型对数据的准确拟合,同时保证不会过拟合。
这里引入了一个新词,过拟合。以著名的 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 为样本数量。
下边用代码演示一下如何对回归任务的数据集进行 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 的方法,需要你自己处理。