原文:HTML
作者:Chris
文章目录
- Evaluating and selecting models with K-fold Cross Validation
- K-fold Cross Validation
- Creating a Keras model with K-fold Cross Validation
训练有监督的机器学习模型时,您可能会尝试多种模型,以了解它们的优劣。这个过程的一部分可能是如何客观地比较模型的问题?为此,将数据集划分为训练集和测试集。从完整数据集中分割出一小部分就可以创建一测试集,其遵循两个假设:(1) 还没有被模型看到,即没有数据泄露问题;(2) 假设测试集数据近似于真实世界场景的分布。
在生成这样的拆分时,应该确保拆分是相对无偏的(unbiased)。本文将介绍一种这样做的技术——K折交叉验证(K-fold Cross Validation)。首先介绍简单的拆分策略。
Evaluating and selecting models with K-fold Cross Validation
训练一个监督机器学习模型涉及使用训练集改变模型权重。训练结束后,用新的数据(即测试集)对训练后的模型进行测试,以了解它在实际生活中的表现。当你对模型的性能感到满意时,可以对整个数据集再次进行训练,以最终确定模型并在生产中使用它(Bogdanovist,n.d)。本质上,K折交叉验证是一种更好的检验模型性能的方式。
先来看看为什么需要将数据集分割为训练集和测试集。为什么不能简单地用所有数据训练模型,然后将结果与其他模型进行比较?然后,再看看高效简单的 simple hold-out 拆分。
Why using train/test splits? – On finding a model that works for you
假设我们正在训练一些模型来对数字图像进行分类。我们训练了一个支持向量机(SVM),一个卷积神经网络(CNN)和一个紧密连接的神经网络(DNN),当然,希望他们在这种情况下能预测“ 5”:
目标是使用在生产中表现最佳的模型,中心问题就变成了:每个模型的表现如何?根据它们的性能,选择一种可以在现实生活中使用的模型。
但是,如果希望确定模型的性能,则应该生成一系列预测,最好是数千个甚至更多,以便计算准确性或损失等指标。
为什么在评估机器学习模型时需要在训练数据和测试数据之间生成划分。为此,我们需要了解高级的有监督机器学习过程:
内容如下:
- 第一步,将所有训练样本(左侧蓝色)前馈到机器学习模型,该模型生成预测(右侧蓝色)。
- 第二步,将预测结果与真实目标进行比较,从而计算出损失值。
- 随后,可以反向传播损失值的梯度,通过改变权重,使模型降低预测误差来优化模型。
- 重复以上迭代过程,直到达到收敛条件为止。
模型会根据数据产生的损失进行改进。如果模型训练的时间足够长,它就可以拟合数据集。如果训练的时间太长可能导致过度拟合。将数据集分割成训练数据和测试数据可以解决这个问题。通过训练数据来训练模型,你可以让它在你想要的时间内进行训练。测试数据来评估模型性能。如果模型过拟合,它在测试数据上的表现会非常差。
A naïve approach: simple hold-out split
假设有一个包含10000个样本的数据集,可以将其按照8:2的比例分割成训练集和测试集。
我们将这种简单的保留分离称为“hold out”,因为我们仅“保留”了最后的2000个样本(Chollet,2017年)。
这可能是一种非常有效的方法。而且计算开销小。但是,这也是一种非常简单的方法,因为必须注意一些极端情况(Chollet,2017年):
- 数据代表性(Data representativeness):进行划分之后,需要尽可能保证训练集和测试集的数据分布一致。如果数据集的前一部分为冰激凌图片,而后一部分只有咖啡图片,当在这种情况下进行拆分时就会有麻烦。随机打乱可能会帮助解决这个问题。
- 时间序列(The arrow of time):如果有一个时间序列数据集,即数据都是按时间顺序排列的。如果随机打乱,则可能导致数据泄露。这种时间上的泄漏对模型性能没有好处。
- 数据冗余(Data redundancy):如果一些样本不止一次出现,那么简单的抱出拆分与随机打乱可能会在训练和测试数据集之间引入冗余。也就是说,相同的样本属于两个数据集。这也是个问题,因为用于训练的数据会因此隐性地泄漏到测试数据集中。
由此可知,虽然简单的基于保留拆分(hold-out split)的方法可能有效并且节省计算开销,但它还需要监视这些极端情况。
K-fold Cross Validation
一种更昂贵,更复杂的方法是执行K折交叉验证(K-fold Cross Validation)。数据集被分割成K个大小相等的子集。K-1个用于训练,而一个用于测试。这个过程重复K次,每次使用不同的子集进行测试。
假设有一个包含10000个样本的数据集,进行5折交叉验证(K = 5):
对于每个拆分,将训练相同的模型,并按分别显示性能。为了进行评估,可以将所有折数平均。虽然这会产生更好的估计,但K折交叉验证也会增加训练成本:在上述K = 5的情况下,模型必须训练5次。
如果没有任何计算限制,则不妨尝试一种特殊的K折交叉验证案例,称为留一法交叉验证(Leave One Out,LOOCV,Khandelwal,2019)。LOOCV中的K = N,其中N是数据集中的样本数。随着训练模型的数量最大化,模型性能平均值的精度也最大化,但是由于必须训练的模型数量庞大,因此训练成本也将非常大。
如果有二分类问题,可以看一看分层交叉验证(Stratified Cross Validation,Khandelwal,2019)。通过确保目标类别在拆分中的平均分配,它扩展了K折交叉验证。这样可以确保分类问题得到平衡。由于样本的分配方式,它不适用于多类分类。
最后,如果你有时间序列数据集,则可能希望使用时间序列交叉验证(Khandelwal,2019)。在这里查看其工作方式。
Creating a Keras model with K-fold Cross Validation
我们将使用卷积神经网络,该网络可用于将CIFAR-10图像分为10类。
这是原始CIFAR-10 CNN分类器的完整模型代码,可在添加K折交叉验证时使用:
CIFAR-10 TF CODE
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D
from tensorflow.keras.losses import sparse_categorical_crossentropy
from tensorflow.keras.optimizers import Adam
import matplotlib.pyplot as plt
# Model configuration
batch_size = 50
img_width, img_height, img_num_channels = 32, 32, 3
loss_function = sparse_categorical_crossentropy
no_classes = 100
no_epochs = 100
optimizer = Adam()
verbosity = 1
# Load CIFAR-10 data
(input_train, target_train), (input_test, target_test) = cifar10.load_data()
# Determine shape of the data
input_shape = (img_width, img_height, img_num_channels)
# Parse numbers as floats
input_train = input_train.astype('float32')
input_test = input_test.astype('float32')
# Normalize data
input_train = input_train / 255
input_test = input_test / 255
# Create the model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dense(128, activation='relu'))
model.add(Dense(no_classes, activation='softmax'))
# Compile the model
model.compile(loss=loss_function,
optimizer=optimizer,
metrics=['accuracy'])
# Fit data to model
history = model.fit(input_train, target_train,
batch_size=batch_size,
epochs=no_epochs,
verbose=verbosity)
# Generate generalization metrics
score = model.evaluate(input_test, target_test, verbose=0)
print(f'Test loss: {score[0]} / Test accuracy: {score[1]}')
# Visualize history
# Plot history: Loss
plt.plot(history.history['val_loss'])
plt.title('Validation loss history')
plt.ylabel('Loss value')
plt.xlabel('No. epoch')
plt.show()
# Plot history: Accuracy
plt.plot(history.history['val_accuracy'])
plt.title('Validation accuracy history')
plt.ylabel('Accuracy value (%)')
plt.xlabel('No. epoch')
plt.show()
Full model CV code
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D
from tensorflow.keras.losses import sparse_categorical_crossentropy
from tensorflow.keras.optimizers import Adam
from sklearn.model_selection import KFold
import numpy as np
# Model configuration
batch_size = 50
img_width, img_height, img_num_channels = 32, 32, 3
loss_function = sparse_categorical_crossentropy
no_classes = 100
no_epochs = 25
optimizer = Adam()
verbosity = 1
num_folds = 10
# Load CIFAR-10 data
(input_train, target_train), (input_test, target_test) = cifar10.load_data()
# Determine shape of the data
input_shape = (img_width, img_height, img_num_channels)
# Parse numbers as floats
input_train = input_train.astype('float32')
input_test = input_test.astype('float32')
# Normalize data
input_train = input_train / 255
input_test = input_test / 255
# Define per-fold score containers
acc_per_fold = []
loss_per_fold = []
# Merge inputs and targets
inputs = np.concatenate((input_train, input_test), axis=0)
targets = np.concatenate((target_train, target_test), axis=0)
# Define the K-fold Cross Validator
kfold = KFold(n_splits=num_folds, shuffle=True)
# K-fold Cross Validation model evaluation
fold_no = 1
for train, test in kfold.split(inputs, targets):
# Define the model architecture
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dense(128, activation='relu'))
model.add(Dense(no_classes, activation='softmax'))
# Compile the model
model.compile(loss=loss_function,
optimizer=optimizer,
metrics=['accuracy'])
# Generate a print
print('------------------------------------------------------------------------')
print(f'Training for fold {fold_no} ...')
# Fit data to model
history = model.fit(inputs[train], targets[train],
batch_size=batch_size,
epochs=no_epochs,
verbose=verbosity)
# Generate generalization metrics
scores = model.evaluate(inputs[test], targets[test], verbose=0)
print(f'Score for fold {fold_no}: {model.metrics_names[0]} of {scores[0]}; {model.metrics_names[1]} of {scores[1]*100}%')
acc_per_fold.append(scores[1] * 100)
loss_per_fold.append(scores[0])
# Increase fold number
fold_no = fold_no + 1
# == Provide average scores ==
print('------------------------------------------------------------------------')
print('Score per fold')
for i in range(0, len(acc_per_fold)):
print('------------------------------------------------------------------------')
print(f'> Fold {i+1} - Loss: {loss_per_fold[i]} - Accuracy: {acc_per_fold[i]}%')
print('------------------------------------------------------------------------')
print('Average scores for all folds:')
print(f'> Accuracy: {np.mean(acc_per_fold)} (+- {np.std(acc_per_fold)})')
print(f'> Loss: {np.mean(loss_per_fold)}')
print('------------------------------------------------------------------------')
最后,在第10折后,它应显示总览以及每折的结果和平均值:
------------------------------------------------------------------------
Score per fold
------------------------------------------------------------------------
> Fold 1 - Loss: 2.4094747734069824 - Accuracy: 67.96666383743286%
------------------------------------------------------------------------
> Fold 2 - Loss: 1.768296229839325 - Accuracy: 67.03333258628845%
------------------------------------------------------------------------
> Fold 3 - Loss: 2.4695983460744224 - Accuracy: 66.46666526794434%
------------------------------------------------------------------------
> Fold 4 - Loss: 2.363724467277527 - Accuracy: 66.28333330154419%
------------------------------------------------------------------------
> Fold 5 - Loss: 2.083754387060801 - Accuracy: 65.51666855812073%
------------------------------------------------------------------------
> Fold 6 - Loss: 2.2160572570165 - Accuracy: 65.6499981880188%
------------------------------------------------------------------------
> Fold 7 - Loss: 1.7227793588638305 - Accuracy: 66.76666736602783%
------------------------------------------------------------------------
> Fold 8 - Loss: 2.357142448425293 - Accuracy: 67.25000143051147%
------------------------------------------------------------------------
> Fold 9 - Loss: 1.553109979470571 - Accuracy: 65.54999947547913%
------------------------------------------------------------------------
> Fold 10 - Loss: 2.426255855560303 - Accuracy: 66.03333353996277%
------------------------------------------------------------------------
Average scores for all folds:
> Accuracy: 66.45166635513306 (+- 0.7683473645622098)
> Loss: 2.1370193102995554
------------------------------------------------------------------------