【机器学习】图解机器学习中的 12 种交叉验证技术
今天我给大家盘点下机器学习中所使用的交叉验证器都有哪些,用最直观的图解方式来帮助大家理解他们是如何工作的。
数据集说明
数据集来源于kaggle M5 Forecasting - Accuracy[1]
该任务是尽可能精确地预测沃尔玛在美国销售的各种产品的单位销售额(demand)。本文将使用其中的一部分数据。
该数据样例如下。
数据集的划分需要根据交叉验证基本原理来操作。首先需要将所有数据集划分为训练集和测试集,再再训练集中利用交叉验证划分训练集和验证集,如下图所示。
首先按照日期date划分测试集和训练集,如下图所示。
为本次演示需求,创造了一些新的特征,最终筛选并使用了如下几个变量。
Training columns:
['sell_price', 'year', 'month', 'dayofweek', 'lag_7',
'rmean_7_7', 'demand_month_mean', 'demand_month_max',
'demandmonth_max_to_min_diff', 'demand_dayofweek_mean',
'demand_dayofweek_median', 'demand_dayofweek_max']
设置如下两个全局变量,以及用来存储每种交叉验证得分结果的DataFrame
SEED = 888 # 为了再现NFOLDS = 5 # 设置K折验证的折数stats = pd.DataFrame(columns=['K-Fold Variation','CV-RMSE','TEST-RMSE'])
交叉验证
交叉验证(Cross Validation) 是在机器学习建立模型和验证模型参数时常用的方法。顾名思义,就是重复的使用数据,把得到的样本数据进行切分,组合为不同的训练集和测试集。用训练集来训练模型,测试集来评估模型的好坏。
交叉验证的目的
从有限的学习数据中获取尽可能多的有效信息。 交叉验证从多个方向开始学习样本的,可以有效地避免陷入局部最小值。 可以在一定程度上避免过拟合问题。
交叉验证的种类
根据切分的方法不同,交叉验证分为下面三种:
第一种是简单交叉验证
首先,随机的将样本数据分为两部分(比如:70%的训练集,30%的测试集),然后用训练集来训练模型,在测试集上验证模型及参数。接着再把样本打乱,重新选择训练集和测试集,继续训练数据和检验模型。最后选择损失函数评估最优的模型和参数。
第二种是K折交叉验证(K-Fold Cross Validation)
和第一种方法不同, 折交叉验证会把样本数据随机的分成 份,每次随机的选择 份作为训练集,剩下的1份做测试集。当这一轮完成后,重新随机选择 份来训练数据。若干轮(小于 )之后,选择损失函数评估最优的模型和参数。
第三种是留一交叉验证(Leave-one-out Cross Validation)
它是第二种情况的特例,此时 等于样本数 ,这样对于 个样本,每次选择 个样本来训练数据,留一个样本来验证模型预测的好坏。此方法主要用于样本量非常少的情况,比如对于普通适中问题, 小于50时,一般采用留一交叉验证。
下面将用图解方法详细介绍12种交叉验证方法,主要参考scikit-learn官网[2]介绍。
交叉验证器
01 K折交叉验证--没有打乱
折交叉验证器 KFold,提供训练/验证索引以拆分训练/验证集中的数据。将数据集拆分为 个连续的折叠(默认情况下不改组)。然后将每个折叠用作一次验证,而剩余的 个折叠形成训练集。
from sklearn.model_selection import KFold
KFold(n_splits= NFOLDS, shuffle=False, random_state=None)
CV mean score: 23.64240, std: 1.8744.Out of sample (test) score: 20.455980
不建议使用这种类型的交叉验证来处理时间序列数据,因为它忽略了数据的连贯性。实际的测试数据是将来的一个时期。
如下图所示,黑色部分为被用作的验证的一个折叠,而黄色部分为被用作训练的 个折叠。
另外数据分布图是5折交叉验证中每个验证数据集(黑色部分),及实际用作验证模型的数据集的组合分布图。
02 K折交叉验证--打乱的
K折交叉验证器KFold设置参数shuffle=True
from sklearn.model_selection import KFold
KFold(n_splits= NFOLDS, random_state=SEED, shuffle=True)
CV mean score: 22.65849, std: 1.4224.Out of sample (test) score: 20.508801
在每次迭代中,五分之一的数据仍然是验证集,但这一次它是随机分布在整个数据中。与前面一样,在一个迭代中用作验证的每个示例永远不会在另一个迭代中用作验证。
如下图所示,黑色部分为被用作验证的数据集,很明显,验证集数据是被打乱了的。
03 随机排列交叉验证
随机排列交叉验证器ShuffleSplit,生成索引以将数据拆分为训练集和验证集。
注意:与其他交叉验证策略相反,随机拆分并不能保证所有折叠都会不同,尽管对于大型数据集来说z这是很有可能。
from sklearn.model_selection import ShuffleSplit
ShuffleSplit(n_splits= NFOLDS,
random_state=SEED,
train_size=0.7,
test_size=0.2)
# 还有0.1的数据是没有被取到的
CV mean score: 22.93248, std: 1.0090.Out of sample (test) score: 20.539504
ShuffleSplit将在每次迭代过程中随机抽取整个数据集,生成一个训练集和一个验证集。test_size
和train_size
参数控制每次迭代的验证和训练集的大小。因为我们在每次迭代中都是从整个数据集采样,所以在一次迭代中选择的值,可以在另一次迭代中再次选择。
由于部分数据未包含在训练中,该方法比普通的k倍交叉验证更快。
如下图所示,黑色部分为被用作验证的数据集,橙色是被用作训练的数据集,而白色部分为未被包含在训练和验证集中的数据集。
04 分层K折交叉验证--没有打乱
分层 折交叉验证器StratifiedKFold。
提供训练/验证索引以拆分训练/验证集中的数据。这个交叉验证对象是 KFold
的一种变体,它返回分层折叠。通过保留每个类别的样本百分比来进行折叠。
from sklearn.model_selection import StratifiedKFold
StratifiedKFold(n_splits= NFOLDS, shuffle=False)
CV mean score: 22.73248, std: 0.4955.Out of sample (test) score: 20.599119
就跟普通的 折交叉验证类似,但是每折包含每个目标样本的大约相同的百分比。更好地使用分类而不是回归。
其中有几点需要注意:
生成验证集中,使每次切分的训练/验证集中的包含类别分布相同或尽可能接近。 当 shuffle=False
时,将保留数据集排序中的顺序依赖关系。也就是说,某些验证集中来自类 k 的所有样本在 y 中是连续的。生成验证集大小一致,即最小和最大验证集数据数量,最多也就相差一个样本。
如下图所示,在没有打乱的情况下,验证集(图中黑色部分)分布是有一定的规律的。
且从下面的数据分布图可见,5折交叉验证数据密度分布曲线基本重合,说明虽然划分的样本不同,但其分布基本一致。
05 分层K折交叉验证--打乱的
对于每个目标,折叠包大约相同百分比的样本,但首先数据被打乱。这里需要注意的是,该交叉验证的拆分数据方法是一致的,仅仅是在拆分前,先打乱数据的排列,再进行分层 折交叉验证。
from sklearn.model_selection import StratifiedKFold
StratifiedKFold(n_splits= NFOLDS, random_state=SEED,
shuffle=True)
CV mean score: 22.47692, std: 0.9594.Out of sample (test) score: 20.618389
如下图所示,打乱的分层K折交叉验证的验证集是没有规律、随机分布的。
该交叉验证的数据分布与未被打乱的分层K折交叉验证基本一致。
06 分组K折交叉验证
具有非重叠组的 折迭代器变体GroupKFold。
同一组不会出现在两个不同的折叠中(不同组的数量必须至少等于折叠的数量)。这些折叠是近似平衡的,因为每个折叠中不同组的数量是近似相同的。
可以从数据集的另一特定列(年)来定义组。确保同一组中不同时处于训练集和验证集中。
该交叉验证器分组是在方法split
中参数groups
来体现出来的。
from sklearn.model_selection import GroupKFold
groups = train['year'].tolist()
groupfolds = GroupKFold(n_splits=NFOLDS)
groupfolds.split(X_train,Y_train, groups=groups)
CV mean score: 23.21066, std: 2.7148.Out of sample (test) score: 20.550477
如下图所示,由于数据集原因(不是包含5个整年(组)),因此5折交叉验证中,并不能保证没次都包含相同数据数量的验证集。
在上一个示例中,我们使用年作为组,在下一个示例中使用月作为组。大家可以通过下面图可以很明显地看看有什么区别。
from sklearn.model_selection import GroupKFold
groups = train['month'].tolist()
groupfolds = GroupKFold(n_splits=NFOLDS)
groupfolds.split(X_train,Y_train, groups=groups)
CV mean score: 22.32342, std: 3.9974.Out of sample (test) score: 20.481986
如下图所示,每次迭代均是以月为组来取验证集。
07 分组K折交叉验证--留一组
留一组交叉验证器LeaveOneGroupOut。
根据第三方提供的整数组数组保留样本。此组信息可用于编码任意特定于域的预定义交叉验证折叠。
因此,每个训练集由除与特定组相关的样本之外的所有样本构成。
例如,组可以是样本收集的年份、月份等,因此允许针对基于时间的拆分进行交叉验证。
from sklearn.model_selection import LeaveOneGroupOut
groups = train['month'].tolist()
n_folds = train['month'].nunique()
logroupfolds = LeaveOneGroupOut()
logroupfolds.split(X_train,Y_train, groups=groups)
CV mean score: 22.48503, std: 5.6201.Out of sample (test) score: 20.468222
在每次迭代中,模型都使用留一组之外的所有组的样本进行训练。如果以月份为组,则执行12次迭代。
由下图可以看到该分组K折交叉验证的拆分数据方法。
08 分组K折交叉验证--留N组
LeavePGroupsOut将 P
组留在交叉验证器之外,例如,组可以是样本收集的年份,因此允许针对基于时间的拆分进行交叉验证。
LeavePGroupsOut 和 LeaveOneGroupOut 的区别在于,前者使用所有样本分配到P
不同的组值来构建测试集,而后者使用所有分配到相同组的样本。
通过参数n_groups
设置要在测试拆分中排除的组数。
from sklearn.model_selection import LeavePGroupsOut
groups = train['year'].tolist()
lpgroupfolds = LeavePGroupsOut(n_groups=2)
lpgroupfolds.split(X_train,Y_train, groups=groups)
CV mean score: 23.92578, std: 1.2573.Out of sample (test) score: 90.222850
由下图可知,因K=5
,n_groups=2
,所以共分为10种情况,每种划分的验证集均不相同。
09 随机排列的分组K折交叉验证
Shuffle-Group(s)-Out 交叉验证迭代器GroupShuffleSplit
GroupShuffleSplit迭代器为ShuffleSplit和LeavePGroupsOut的两种方法的结合,并生成一个随机分区序列,其中每个分区都会保留组的一个子集。
例如,组可以是样本收集的年份,因此允许针对基于时间的拆分进行交叉验证。
LeavePGroupsOut 和 GroupShuffleSplit 之间的区别在于,前者使用大小P
唯一组的所有子集生成拆分,而 GroupShuffleSplit 生成用户确定数量的随机验证拆分,每个拆分都有用户确定的唯一组比例。
例如,与LeavePGroupsOut(p=10)
相比,一个计算强度较小的替代方案是 GroupShuffleSplit(test_size=10, n_splits=100)
。
注意:参数test_size
和train_size
指的是组,而不是样本,像在 ShuffleSplit 中一样
定义组,并在每次迭代中随机抽样整个数据集,以生成一个训练集和一个验证集。
from sklearn.model_selection import GroupShuffleSplit
groups = train['month'].tolist()
rpgroupfolds = GroupShuffleSplit(n_splits=NFOLDS, train_size=0.7,
test_size=0.2, random_state=SEED)
rpgroupfolds.split(X_train,Y_train, groups=groups)
CV mean score: 21.62334, std: 2.5657.Out of sample (test) score: 20.354134
从图中可见,断开(白色)部分为未取到的数据集,每一行中每段(以白色空白为界)中验证集(黑色)比例及位置都是一致的。而不同行之间验证集的位置是不同的。
10 时间序列交叉验证
时间序列数据的特征在于时间上接近的观测值之间的相关性(自相关)。然而,经典的交叉验证技术,例如 KFold 和 ShuffleSplit假设样本是独立的和同分布的,并且会导致时间序列数据的训练和测试实例之间不合理的相关性(产生对泛化误差的不良估计)。
因此,在“未来”观察中评估我们的模型的时间序列数据非常重要,这与用于训练模型的观察最不相似。为了实现这一点,提供了一种解决方案TimeSeriesSplit。
TimeSeriesSplit是KFold的变体,它首先返回 折叠成训练集和 第 折叠作为验证集。请注意,与标准交叉验证方法不同,连续训练集是它们之前的超集。此外,它将所有剩余数据添加到第一个训练分区,该分区始终用于训练模型。
from sklearn.model_selection import TimeSeriesSplit
timeSeriesSplit = TimeSeriesSplit(n_splits= NFOLDS)
CV mean score: 24.32591, std: 2.0312.Out of sample (test) score: 20.999613
这种方法建议用于时间序列数据。在时间序列分割中,训练集通常分为两部分。第一部分始终是训练集,而后一部分是验证集。
由下图可知,验证集的长度保持不变,而训练集随着每次迭代的不断增大。
11 封闭时间序列交叉验证
这是自定义的一种交叉验证方法。该方法函数见文末函数附录。
btscv = BlockingTimeSeriesSplit(n_splits=NFOLDS)
CV mean score: 22.57081, std: 6.0085.Out of sample (test) score: 19.896889
由下图可见,训练和验证集在每次迭代中都是唯一的。没有值被使用两次。列车集总是在验证之前。由于在较少的样本中训练,它也比其他交叉验证方法更快。
12 清除K折交叉验证
这是基于_BaseKFold的一种交叉验证方法。在每次迭代中,在训练集之前和之后,我们会删除一些样本。
cont = pd.Series(train.index)
purgedfolds=PurgedKFold(n_splits=NFOLDS,
t1=cont, pctEmbargo=0.0)
CV mean score: 23.64854, std: 1.9370.Out of sample (test) score: 20.589597
由下图可看出,训练集前后删除了一些样本。且其划分训练集和验证集的方法与基础不打乱的KFold
一致。
将embargo
设置为大于0的值,将在验证集之后删除额外的样本。
cont = pd.Series(train.index)
purgedfolds=PurgedKFold(n_splits=NFOLDS,t1=cont,pctEmbargo=0.1)
CV mean score: 23.87267, std: 1.7693.Out of sample (test) score: 20.414387
由下图可看出,不仅在训练集前后删除了部分样本,在验证集后面也删除了一些样本,这些样本的大小将取决于参数embargo
的大小。
各交叉验证结果比较
cm = sns.light_palette('green', as_cmap=True, reverse=True)
stats.style.background_gradient(cmap=cm)
附录
封闭时间序列交叉验证函数
class BlockingTimeSeriesSplit(): def __init__(self, n_splits): self.n_splits = n_splits
def get_n_splits(self, X, y, groups): return self.n_splits
def split(self, X, y=None, groups=None): n_samples = len(X) k_fold_size = n_samples // self.n_splits indices = np.arange(n_samples)
margin = 0 for i in range(self.n_splits): start = i * k_fold_size stop = start + k_fold_size mid = int(0.9 * (stop - start)) + start yield indices[start: mid], indices[mid + margin: stop]
清除K折交叉验证函数
from sklearn.model_selection._split import _BaseKFold
class PurgedKFold(_BaseKFold):
'''
扩展KFold类以处理跨越间隔的标签
在训练集中剔除了重叠的测试标记间隔
假设测试集是连续的(shuffle=False),中间有w/o训练样本
'''
def __init__(self, n_splits=3, t1=None, pctEmbargo=0.1):
if not isinstance(t1, pd.Series):
raise ValueError('Label Through Dates must be a pd.Series')
super(PurgedKFold,self).__init__(n_splits, shuffle=False, random_state=None)
self.t1 = t1
self.pctEmbargo = pctEmbargo
def split(self,X,y=None,groups=None):
X = pd.DataFrame(X)
if (X.index==self.t1.index).sum()!=len(self.t1):
raise ValueError('X and ThruDateValues must have the same index')
indices = np.arange(X.shape[0])
mbrg = int(X.shape[0] * self.pctEmbargo)
test_starts=[(i[0],i[-1]+1) for i in np.array_split(np.arange(X.shape[0]), self.n_splits)]
for i,j in test_starts:
t0 = self.t1.index[i] # 测试集的开始
test_indices = indices[i:j]
maxT1Idx = self.t1.index.searchsorted(self.t1[test_indices].max())
train_indices = self.t1.index.searchsorted(self.t1[self.t1<=t0].index)
if maxT1Idx < X.shape[0]: # 右边的训练集带有 embargo)
train_indices = np.concatenate((train_indices, indices[maxT1Idx+mbrg:]))
yield train_indices,test_indices
参考资料
数据集: https://www.kaggle.com/c/m5-forecasting-accuracy
[2]
交叉验证: https://scikit-learn.org/stable/modules/classes.html