欢迎各位同学学习python信用评分卡建模视频系列教程(附代码, 博主录制) :
(微信二维码扫一扫报名)
什么是训练和测试分组? 这是将数据集分为多个部分。我们使用一个零件训练模型,而在另一个零件上测试其有效性。
在本文中,我们的重点是为2种资产之间的关系建模的正确方法。
我们将检查债券是否可用作标准普尔500指数的领先指标。
目录
建模中的数据拆分是什么?
什么是训练集?
什么是验证集?
什么是测试仪?
为什么我们需要拆分数据?
前瞻性偏见
过度拟合
不合身
如何训练我们的模型?
我们如何使用验证集?
使用验证集进行超参数调整
我们如何在测试集中测试模型?
什么是交叉验证,为什么要使用它?
标准数据的交叉验证
K折交叉验证
具有K折交叉验证的超参数调整
问题数据的替代技术
分层K折
组K折
时间序列数据的交叉验证
前向嵌套式交叉验证
建模中的数据拆分是什么?
资源数据拆分是将数据拆分为3组的过程:
- 我们用于设计模型的数据(训练集)
- 我们用于完善模型的数据(验证集)
- 我们用来测试模型的数据(测试集)
如果不拆分数据,则可以使用与训练模型相同的数据来测试模型。
例
如果该模型是专门为2008年苹果股票设计的交易策略,并且我们在2008年对其在苹果股票上的有效性进行了测试,那么它当然会做得很好。
我们需要根据2009年的数据进行测试。因此,2008年是我们的训练集,2009年是我们的测试集。
回顾什么是训练,验证和测试集…
什么是训练集?
训练集是我们分析(训练)以设计模型中的规则的数据集。
训练集也称为样本内数据或训练数据。
什么是验证集?
验证集是我们训练模型时用来评估这些规则对新数据的执行情况的有效数据。
它也是我们用来调整模型参数和输入特征的集合,以便为我们提供新数据可能获得的最佳性能。
什么是测试仪?
测试集是我们没有用来训练模型的数据集,也不是在验证集中使用的数据集,这些信息用来告知我们对参数/输入特征的选择。
一旦我们决定了最终模型,我们将把它用作最终测试,以最佳估计我们的模型在全新数据上的成功程度。
测试集也称为样本外数据或测试数据。
为什么我们需要拆分数据?
为了防止前瞻性偏见,过度拟合和过度拟合。
- 前瞻性偏见:基于未知数据构建模型。
- 过度拟合:这是设计模型的过程,该模型是如此紧密地适应历史数据,以至于将来变得无效。
- 欠拟合:这是设计模型的过程,该模型过于宽松地适应了历史数据,以至于将来变得无效。
前瞻性偏见
让我们用一个例子来说明。
这是2013年至2020年亚马逊的股票表现。
资料来源:Tradingview.com
哇,它的发展趋势相当平稳。我将设计一种交易模型,随着它的发展趋势向亚马逊投资。
然后,我在同一数据集(2013年至2020年)上测试我的交易模型。
令我惊讶的是,该模型的表现出色,我赚了很多假想的钱。你不说!
从2013年开始测试交易模型时,它知道亚马逊2014年的股票表现如何,因为在设计交易模型时我们考虑了2014年的数据。
据说该模型已经“展望了未来”。
因此,我们的模型存在前瞻性偏差。我们基于不应该知道的数据构建了一个模型。
过度拟合
从最简单的意义上讲,训练时,模型会尝试学习如何将输入要素(可用数据)映射到目标(我们要预测的目标)。
过度拟合是用于描述模型何时对训练数据“太好”学习了这种关系的术语。
“太好了”,我们的意思是说它已经太紧密地了解了这种关系-它所看到的趋势/相关性/联系比实际存在的要多。
我们可以将其视为一个模型,该模型吸收了训练数据中过多的“噪声”,学习将训练数据的确切和非常特定的特征映射到目标,而实际上这些都是一次性事件/关联,不能代表数据中通常存在的更广泛的模式。
这样,该模型对于训练数据表现非常好,但与新数据相比却比较困难。从训练数据中得出的模式不能很好地推广到新的看不见的数据。
这几乎总是使模型过于复杂的结果-使其相对于数据中存在的“实际”数量的模式具有太多的规则和/或特征。这也可能是由于我们要训练的观测值(训练数据)具有太多特征的结果。
例如,在极端情况下,假设我们有1000条训练数据,而模型中有1000条“规则”。从本质上讲,它可以学习构建说明以下内容的规则:
- 规则1:将所有特征非常接近x1,y1,z1(恰好是训练数据1的特征)的数据映射到训练数据1的目标值。
- 规则2:将特征非常接近x2,y2,z2的所有数据映射到训练数据的目标值2。
- …
- 规则1000:将特征非常接近x1000,y1000,z1000的所有数据映射到训练数据1000的目标值。
这样的模型在训练数据上将表现出色,但在任何与训练示例略有不同的新数据上可能几乎毫无用处。
您可以在这里阅读有关过度拟合的更多信息:什么是过度拟合交易?
不合身
相比之下,欠拟合是模型过于不明确的情况。也就是说,它还没有真正了解到训练数据和目标变量之间的任何有意义的关系。
这样的模型在训练数据或任何新数据上都不会表现良好。
在实践中,这比过度拟合更为罕见,并且通常是因为模型太简单了,例如,假设将线性回归模型拟合到非线性数据,或者将最大深度为2的随机森林模型拟合到具有存在许多功能。
通常,您想开发一个模型,以捕获训练数据中尽可能多的模式,这些模式仍然可以很好地概括(适用于)新的看不见的数据。
换句话说,我们希望模型既不过度拟合也不过度拟合,而恰到好处。
如何训练我们的模型
要了解这些概念如何在现实中发挥作用,让我们尝试构建一个实际模型。
我们的模型:检查昨天的2-10债券价差能否预测今天的SPX价格。
我们将使用一些称为:
- “ 2-10债券利差”,即10年期美国国库券固定期限利率与2年期美国国库券固定期限利率之间的差额)和
- “ SPX,SPCFD:比较”,它是500家最大的美国上市公司的市值加权指数。
在视觉上,我们的数据如下所示:
蓝色债券利差,红色SPX价格
让我们继续进行一些示例性的市场数据,以2日至10年期美国国债兑SPX债券的收盘价差:
import numpy as np import pandas as pd from pathlib import Path df = pd.read_csv(Path("QUANDL_FRED_T10Y2Y, 1D 80PERCENT.csv"))
请注意,在进行任何建模之前,我们要使2-10(t-1)的SPX(t)回归,因此将2-10美国债券的利差滞后1天,因为正如我们所说,我们要检查昨天的2- 10值对今天的SPX值有任何影响。我们还应该使用收益(从最后一天开始的比例价格变化),而不是今天的实际价格。
改变债券利差,以便使昨天的债券利差相对于今天的价格进行回归:
df['2-10 Bond Spread'] = df['2-10 Bond Spread'].shift(1)
从昨天的价格而不是今天的绝对价格减去(百分比)变化的回归:
df['returns']=df['SPX, SPCFD: Compare']/df['SPX, SPCFD: Compare'].shift(1) - 1 df
现在,通过删除第一行进行清理,因为第一行现在在“ 2-10 Bond Spread”和“ returns”列中均不适用,因为我们将“ 2-10 Bond Spread”上移了一个:
df = df[1:] # remove first row with an N/A df
最后,将数据帧的索引设置为时间列的值而不是任意整数将很方便,因为数据的时间顺序很重要。
请注意,时间列的类型当前为字符串(您可以使用type(x)函数自行检查),因此我们首先将其设置为datetime64,然后将索引设置为时间列:
df['time'] = df['time'].astype('datetime64[ns]') # change "time" column type from str to datetime64 df.set_index('time', inplace=True) # set time column as the index df
现在,让我们使用所有数据来构建我们的模型,然后使用相同的数据测试我们的模型,看看会发生什么。
首先,为了说明起见,从sklearn导入一个非常易于使用的回归模型,而mean_squared_error函数可帮助我们生成均方根评估函数以测试模型:
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error
请注意,在sklearn中,您经常会找到同一算法的“回归”和“分类”版本(例如,在本例中为RandomForest)。当您要预测有限数量的类别(例如马,鞋,鸭)时,您只想使用分类器版本;而当您尝试预测连续的数值输出时(如我们在此处),则只需要使用回归器版本。
现在让我们创建训练数据框和目标数据框:
# set the training data columns and target variable y_train = df["returns"] X_train = df.drop(columns=["SPX, SPCFD: Compare", "returns"])
并初始化带有几个超参数值集的RandomForestRegressor。注意:
- random_state只是控制算法的初始化参数-如果我们明确定义它,我们将获得完全可重复的结果,否则将对其进行随机选择,因此最终模型每次都会略有变化
- max_depth是一个变量,用于控制在森林中生成的决策树的深度-深度越大,模型可以在数据上进行的拆分越多-作为2 ^(max_depth)的函数,使模型变得更加复杂
- n_estimators是“森林”中单个决策树的数量
random_forest = RandomForestRegressor(max_depth=5, n_estimators=100, random_state=1)
让我们在所有数据上拟合(训练)模型:
random_forest.fit(X_train, y_train)
并在完全相同的相同数据上检查拟合模型的均方根精度:
root_mean_squared_error = np.sqrt(mean_squared_error(y_train, random_forest.predict(X_train))) root_mean_squared_error
0.009310731251200473
好的,所以我们的模型的基准性能有0.00993的误差。
请注意,这非常可怕,因为平均回报率(您可以使用(np.abs(y_train).mean()进行计算)为0.0065,因此我们的平均误差约为每日平均回报率的143%,因此显然我们的模型不是很准确。
不过,这并不令人感到意外,因为我们只使用单个数值(昨天的债券利差)和未经校准的(也许是不合适的类型)模型来估计每日收益-如果市场真的很容易预测我们都会有钱的!
不过,这并不重要,出于本文的目的,我们将忽略客观上令人震惊的结果-重点是使用数据集为我们正在探索的主题提供说明性代码,而不是模型的实际技能。
无论如何,我们仍然可以摆弄模型的超参数来尝试改善性能。
例如,我们可以增加随机森林的最大深度(如果我们记得允许模型对数据进行更多分割,从而变得更加复杂):
random_forest = RandomForestRegressor(max_depth=10, n_estimators=100, random_state=1) random_forest.fit(X_train, y_train) root_mean_squared_error = np.sqrt(mean_squared_error(y_train, random_forest.predict(X_train))) root_mean_squared_error
0.009083679274932605
好的,大约有2.4%的改善。
并使其更加复杂:
random_forest = RandomForestRegressor(max_depth=50, n_estimators=100, random_state=1) random_forest.fit(X_train, y_train) root_mean_squared_error = np.sqrt(mean_squared_error(y_train, random_forest.predict(X_train))) root_mean_squared_error
0.008943288173833334
从我们的出发点开始,现在几乎提高了4%。
因此,既然我们已经对其进行了一些改进,那么当暴露于全新数据时该模型的性能如何?
在这里,我们将假装在实时情况下部署模型,并在野外遇到了一些新数据。我们将从同一个源(但从训练数据开始按时间顺序向前移动的时间点)加载更多数据,并执行完全相同的预处理步骤:
unseen_data = pd.read_csv(Path("unseen_data.csv")) unseen_data['2-10 Bond Spread'] = unseen_data['2-10 Bond Spread'].shift(1) unseen_data['returns']= unseen_data['SPX, SPCFD: Compare']/unseen_data['SPX, SPCFD: Compare'].shift(1) - 1 unseen_data = unseen_data[1:] unseen_data['time'] = unseen_data['time'].astype('datetime64[ns]') # change "time" column type from str to datetime64 unseen_data.set_index('time', inplace=True) # set time column as the index unseen_data
最后,再次设置输入变量和目标变量,并测试一些新数据的性能:
y_unseen = unseen_data["returns"] X_unseen = unseen_data.drop(columns=["SPX, SPCFD: Compare", "returns"]) root_mean_squared_error = np.sqrt(mean_squared_error(y_unseen, random_forest.predict(X_unseen))) root_mean_squared_error
0.018704827013423832
这使我们的均方根误差约为我们以前用于训练和测试的数据的〜2 09%,不如我们可能希望/期望的那么好……
这清楚地表明了过度行动。
我们如何使用验证集?
因此很明显,我们不能仅仅使用模型的训练数据来评估模型在新数据上的表现。
我们需要根据一些未经训练的数据来评估其性能,以更好地了解其在野外的性能。
输入验证集。
从现在开始,我们将训练数据分为两组。我们将保留大部分数据用于训练,但会分离出一小部分以供验证。
一个好的经验法则是在70:30到80:20 training:validation拆分之间使用一些东西。
考特尼·科克伦(Courtney Cochrane):https : //towardsdatascience.com/time-series-nested-cross-validation-76adba623eb9为此,我们可以简单地将数据长度的特定部分四舍五入为整数,然后将数据帧分成两部分,如下所示:
y = df["returns"] X = df.drop(columns=["SPX, SPCFD: Compare", "returns"]) train_fraction = 0.8 split_point = int(train_fraction *len(X)) # (len(X) and len(y) are the same anyway) X_train = X[0:split_point] X_valid = X[split_point:] y_train= y[0:split_point] y_valid= y[split_point:]
或者如您在许多地方可能看到的那样,使用sklearn有用的预建train_test_split函数,如下所示:
from sklearn.model_selection import train_test_split X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.8,test_size=0.2, random_state=101)
如果只填写一个,则train_size和test_size会自动互补,而random_state是数据拆分方式的种子-如果将来使用相同的种子,则可以确保每个数据中的数据完全相同训练和验证集与以前一样。
print("len(df): {}, split_point: {}, len(X_train): {}, len(X_valid): {}, len(y_train): {}, len(y_valid): {}".format(len(df), split_point, len(X_train), len(X_valid), len(y_train), len(y_valid))) len(df): 1998, split_point: 1598, len(X_train): 1598, len(X_valid): 400, len(y_train): 1598, len(y_valid): 400
验证集实质上使我们能够检查模型的“过度拟合”或“不足拟合”。
它使我们既可以将模型的复杂性调整到最佳点,又可以更好地估计该模型在不可见数据的情况下的性能,因为该模型不使用验证数据进行训练。
请注意,验证准确性将比训练准确性低是完全正常的(甚至是可能的)。实际上,如果它们非常相似,则可以很好地表明您的模型可能不够复杂(拟合不足)。
也就是说训练的准确性并不重要。
唯一重要的是获得最佳的验证准确性,因为这实际上在某种程度上反映了模型在野外的性能。
一般而言,增加模型复杂度应(通常不考虑随机性)通常会导致训练精度提高,并且随着模型发现更多更好的模式,增加模型复杂度还会导致验证精度提高。
但是,最终,这些模式将变得过于特定于训练数据,无法很好地推广,因此验证准确性将开始下降。
使用验证集进行超参数调整
我们将使用验证集将模型的复杂性磨练到最佳位置,如下图所示:
现在让我们开始处理我们的数据。
下面,我们简单地遍历max_depths列表,将模型拟合到每个最大深度,然后评估训练集和验证集上的误差并绘制出这些图。我们要更改以更改模型复杂度的唯一变量是max_depth,其他所有内容每次都保持不变,因此max_depth唯一负责模型的复杂性。
Matplotlib.pyplot是Python中使用的“标准”绘图库。如果您以前从未遇到过,这是制作一些简单绘图的快速速成课程:https : //matplotlib.org/tutorials/introductory/pyplot.html
import matplotlib.pyplot as plt train_errors = [] valid_errors = [] param_range = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,20,30,40,50,75,100] for max_depth in param_range: random_forest = RandomForestRegressor(max_depth=max_depth, n_estimators=100, random_state=1) random_forest.fit(X_train, y_train) train_errors.append(np.sqrt(mean_squared_error(y_train, random_forest.predict(X_train)))) valid_errors.append(np.sqrt(mean_squared_error(y_valid, random_forest.predict(X_valid)))) plt.xlabel('max_depth') plt.ylabel('root mean_squared_error') plt.plot(param_range, train_errors, label="train rmse") plt.plot(param_range, valid_errors, label="validation rmse") plt.legend() plt.show()
正如预期的那样,随着我们增加max_depth(增加模型的复杂性),训练精度在整个1-100范围内一直在不断提高-首先是迅速提高,但之后仍然缓慢提高。
另一方面,验证准确性会立即变差,并且随着我们增加max_depth而不会停止变差。
这表明模型已经“太复杂”(或处于最佳复杂度),最大深度为1。
通常,我们希望验证准确性至少会提高一小段时间,然后再从一个非常简单的模型回归到非常复杂的模型,但是通常,我们还是希望使用包含多个数据的训练数据来学习从这样的最大深度1开始,返回最佳验证性能几乎可以肯定是输入数据非常简单和/或缺乏训练数据的结果。
无论如何,让我们继续以max_depth为1重新拟合模型,并准确查看其性能。
请注意,由于我们拥有理论上最好的max_depth,因此我们将恢复为在此处使用X和y(完整数据集)来重新训练模型,从而使训练数据与第一个针对未见数据截断而预测的模型的数据完全匹配如此小的数据集的20%(因为我们最近进行了训练:验证拆分)可能会导致我们的模型在不考虑最大深度的情况下表现更差。这样我们就可以保持比较一致:
random_forest = RandomForestRegressor(max_depth=1, n_estimators=100, random_state=1) random_forest.fit(X, y) root_mean_squared_error = np.sqrt(mean_squared_error(y, random_forest.predict(X))) root_mean_squared_error
0.00942170960852716
最大深度为50时,比我们在训练集上取得的最好成绩差约5.4%
root_mean_squared_error = np.sqrt(mean_squared_error(y_unseen, random_forest.predict(X_unseen))) root_mean_squared_error
0.017679233094329314
但是,看不见的数据要好约5.5%!
在这里,我们已成功地将验证集用于这两个方面:
- 让我们更好地估计如何处理看不见的数据。
- 通过减少过拟合/欠拟合来改善样本外(看不见)数据的性能。
我们如何在测试集中测试模型?
因此,如果模型从不对验证数据进行训练,那么验证数据不是对模型在野外如何执行的完美估计吗?
好吧,差不多。
但不完全是。
原因是,通过使用验证数据将我们的模型调整为最佳的通用性能,我们固有地显示出对模型超参数值和专门针对此验证集优化性能的数据特征的轻微偏差。
实际上,我们已经过度拟合了验证集。
请注意,与训练数据相比,该数据集的过拟合程度要小得多,并且验证数据集的性能通常会为野外性能提供一个粗略的估算(假设您创建的验证数据集没有数据泄漏,更多内容请参见稍后!)。
正是出于这个原因,虽然我们从现有的数据中分离出进一步的测试集,我们不碰,直到我们有我们的模型的最终版本完全功能设计和调整。
进行其他拆分后,原始可用数据现在应如下所示:
我们现在可以使用类似70:20:10的比例。我们可以使用任何想要分割数据帧的方式,但是一个选择是只使用train_test_split()两次。
请注意,0.875 * 0.8 = 0.7,所以这两个拆分的最终效果是将原始数据按70:20:10的比例拆分为训练/验证/测试集:
# split the full data 80:20 into training:validation sets X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.8, random_state=101) # split training data 87.5:12.5 into training:testing sets X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, train_size=0.875, random_state=101) print("len(X): {} len(y): {} \nlen(X_train): {}, len(X_valid): {}, len(X_test): \ {} \nlen(y_train): {}, len(y_valid): {}, len(y_test): {}".format(len(X), len(y),\ len(X_train), len(X_valid), len(X_test), len(y_train), len(y_valid), \ len(y_test)))
len(X): 1998 len(y): 1998
len(X_train): 1398, len(X_valid): 400, len(X_test): 200
len(y_train): 1398, len(y_valid): 400, len(y_test): 200
我们可以根据需要花费大量时间和精力来优化验证集的性能。
但是我们必须诚实地说-当我们完成时,我们必须完成,并且必须将随后在测试集上得到的任何结果作为我们对新数据的可能结果。
然后,我们无法继续尝试再次进行优化以进一步提高测试集的性能。这样做将再次产生过度拟合的偏见。
什么是交叉验证,为什么要使用它?
太好了,因此我们现在将数据分为三种方式-我们将大量数据用于训练,保留合理数量的数据进行验证,并保留少量用于最终测试的保留数据。
但是您可能会想,我们现在是否会丢失大量可用于训练的数据?这会不会降低我们的模型精度?
另外,有什么方法可以避免过度拟合验证集?
那么,所有三个问题的答案都是肯定的!
除了使用单个验证集,我们还可以使用许多验证集。
我们可以进行许多训练:验证拆分,并循环我们每次用于验证的数据的哪一部分,以便最终,在每次训练:验证拆分相结合时,所有数据至少已被用于验证一次,并且至少已被用于一次验证训练。
对于标准数据和时间序列数据,执行交叉验证的方式有所不同,但通常可以为我们带来以下好处:
- 我们(在多个分组中)将100%的训练+验证数据用于训练,这可以消除初始训练集可能高度偏向并且包含许多极端数据类型/发生率示例,或者不包含任何示例的问题重要数据类型/出现的示例
- 我们要(逐段)验证所有数据:不要屈服于小型验证集中的任何高方差实例,在这些情况下,验证集中同样包含异常事件的异常高或异常低。
- 我们将对所有数据的平均性能进行平均,从而使我们对模型技能的估计以及对模型对输入数据的扰动有多大的实际印象更加自信。
- 由于我们试图在许多验证集(而不是一个特定的验证集)上最大化平均性能,因此我们*自动构建了一个不太适合的模型(因此更具通用性),因此我们不能无意中调整仅用于超参数设置的设置。适用于非常具体的验证集。
标准数据的交叉验证
我们可以通过几种不同的方式执行交叉验证,但是对于非时间序列数据,最流行(且易于理解和有效)的技术之一是K折交叉验证。
请注意,这里确实有时间序列数据,因此K折交叉验证实际上是不适合使用的技术(由于原因,我们将在短期内进行讨论),但现在为了生成一些数据,我们暂时将其忽略。具有相同数据集的示例代码。
K折交叉验证
通过K折交叉验证,我们将训练数据分成了k个大小相等的集合(“折数”),将一个集合作为我们的验证集合,并将另一个集合合并为我们的训练集合。然后,我们循环使用哪个折叠作为验证集,直到我们进行了k次训练和验证为止-每次使用唯一的train:validation拆分。
您可以选择任何喜欢的k值,但根据所有数据科学家的集体经验,k = 5或k = 10(以及介于两者之间的所有值)是常见且有效的选择:k = 5表示80:20的训练:validation split和k = 10 a 90:10 split等
该过程可以总结如下:
- 从数据中分离出最终的保持测试集(如果我们有大量数据,则可能约为10%)。
- 随机混洗剩余数据。
- 将此数据拆分为k个大小相等的集合/折叠。
- 对于每个独特的折痕:
- 使用此折叠作为验证折叠
- 结合其他k-1折作为训练数据
- 用训练数据拟合模型
- 用验证倍数评估模型
- 保留评估得分,丢弃模型,并以新的验证折数从4.1重新开始
- 根据整个k个验证分数评估模型,如果您不满意,请进行调整并从1开始重复。
- 当您最终满意时,将所有k折合并为一个完整的训练数据集,再次进行训练,然后对保持测试集进行最终测试。
从图形上看,该过程如下所示:
约瑟夫·尼尔森(Joseph Nelson)的视觉表示-@josephofiowa
因此,现在整个训练/验证/测试拆分过程如下所示:
https://scikit-learn.org/stable/modules/cross_validation.html
让我们尝试将K折交叉验证与我们的数据集结合起来!
虽然我们可以编写许多(简单但冗长的)代码来从头开始实现这样的过程,但幸运的是sklearn库再次借助方便的预构建函数来帮助我们!
(您可以在此处了解更多信息:https : //scikit-learn.org/stable/modules/cross_validation.html)
from sklearn.model_selection import cross_val_score
这个神奇的功能将为我们非常轻松地处理整个过程,但是请深入阅读文档以了解其工作原理以及可用的变体!
无论如何,让我们向模型传递cross_val_score()函数以及所需参数,X和y数据(应该是所有数据减去最终保持测试集),一种评分方法(我们将使用neg_mean_squared_error并调整为RMSE),以及要使用的k值(即“ cv”参数):
cross_val_scores = cross_val_score(RandomForestRegressor(max_depth=1, n_estimators=100, random_state=1),\ X, y, scoring='neg_mean_squared_error', cv=5)
将分数从负均方误差调整为均方根误差,以与我们之前的分数一致:
cross_val_scores = np.sqrt(np.abs(cross_val_scores)) print(cross_val_scores) print("mean:", np.mean(cross_val_scores))
[0.01360711 0.00861119 0.00715738 0.00947426 0.0067502 ]
mean: 0.009120025719774182
如您所见,针对不同的训练,我们的验证分数波动很大:验证分裂!最糟糕的错误是〜102% 更大的比最小的!
这些疯狂的差异可能主要是由于我们的数据集太小而引起的,以及K折交叉验证不适用于时间序列数据的事实(因此,我们的某些训练:验证拆分可能比其他方法更合适),但事实并非如此。着重说明了性能在各个分割之间的差异可能很大,因此对所有模型/分割取平均值非常重要,因为最终所有数据都会用于一次验证!
请注意,作为一个事实,即很小的验证集会导致较高的方差(因为其中包含的少量数据每次拆分都会发生很大变化),我们可以设置k = 50并再次运行交叉验证:
cross_val_scores = cross_val_score(RandomForestRegressor(max_depth=1, n_estimators=100, random_state=1),\ X_train, y_train, scoring='neg_mean_squared_error', cv=50) # change neg_mean_squared error to mean_squared_error cross_val_scores = np.sqrt(np.abs(cross_val_scores)) print(cross_val_scores) print("mean:", np.mean(cross_val_scores))
[0.01029598 0.00922735 0.00553913 0.00900553 0.0110392 0.01333214
0.0115197 0.00933864 0.00664628 0.004857 0.0135743 0.00595552
0.00706495 0.00944506 0.01080077 0.00842491 0.01044174 0.0126128
0.00869932 0.00846706 0.00762137 0.01478009 0.00772207 0.01305496
0.00673948 0.00801689 0.01060272 0.01137826 0.0069177 0.01071186
0.0083437 0.00905157 0.00803609 0.00893249 0.01002789 0.00802375
0.00934506 0.01199787 0.00686557 0.01114371 0.00862676 0.00830973
0.00935762 0.00815328 0.00868262 0.00938199 0.00926949 0.00627161
0.00922161 0.00771521]
mean: 0.00921180787871304
请注意,均值非常相似,但是方差甚至更大-性能最差的结果的误差比最优结果大204%!
这就是为什么我们喜欢在5到10之间选择一个k值的原因:验证集足够大,不会显示太多设置方差的设置,但又不算太大,以至于它们占用了大量训练数据,训练数据集中的偏见严重,缺少训练所需的数据。
具有K折交叉验证的超参数调整
因此,您可能还记得,交叉验证的要点之一是减少训练集中的偏差和验证集中的差异。
另一个重要方面是通过迫使我们找到在许多验证集上提供最佳平均性能的超参数值来减少对验证集的过度拟合。
在我们发现针对我们的特定训练:验证拆分之前,max_depth为1导致最佳性能,因此我们得出结论,此max_depth将为我们提供新数据的最佳性能。
让我们回想一下它的外观:
也就是说,完全有可能max_depth仅1对于该特定验证集是最佳的-可能不是在许多验证集上平均的最佳max_depth。
让我们再次针对max_depth运行超参数校准,但是这次使用5个不同的验证集上的交叉验证进行了校准。
为此,我们将使用sklearn- validation_curve ()中的另一个函数。
from sklearn.model_selection import validation_curve
它与cross_val_scores()非常相似,但是让我们在运行交叉验证的同时更改超参数(即,它对我们所变化的超参数的每个特定值执行一次完整的交叉验证过程):
train_scores, valid_scores = validation_curve(RandomForestRegressor(n_estimators=100, random_state=1), X_train, y_train, "max_depth", param_range, scoring='neg_mean_squared_error', cv=5) train_scores = np.sqrt(np.abs(train_scores)) valid_scores = np.sqrt(np.abs(valid_scores)) train_scores_mean = np.mean(train_scores, axis=1) valid_scores_mean = np.mean(valid_scores, axis=1) plt.title("Validation Curve with Random Forest") plt.xlabel("max_depth") plt.ylabel("RMSE") plt.plot(param_range, train_scores_mean, label="train rmse") plt.plot(param_range, valid_scores_mean, label="validation rmse") plt.legend() plt.show()
好的,结果几乎完全相同,但是这次我们可以肯定的是,我们做出了一个不错的选择!
一般而言,如果我们的数据有些复杂,并且在max_depth的任何级别上的过度拟合都没有那么明确,我们可能会发现,当交叉验证而不是在a上进行验证时,不同的超参数给出了最佳结果单套。
问题数据的替代技术
K折交叉验证通常会为您带来良好的结果,但有时根据数据的结构/分布它可能会给我们带来问题。
首先是在数据中有很多极端的例子,而我们在训练,验证和测试集之间却得不到很好的分配。
例如,分类任务中可能没有一些类的示例。如果这些(运气不好)主要只出现在验证或测试集中,那么我们的模型将永远不会/几乎不会在训练中遇到它们,并且几乎肯定会在对它们进行分类时表现不佳。
同样,在回归意义上,如果有一些极端目标值的示例(低或高),并且仅在我们的一些验证和测试集中出现,那么我们的模型在遇到它们时也不太可能做得很好。
此外,如果“有问题的”数据仅出现在训练中,而没有出现在我们的测试集中,则我们可能会对模型技能获得过分乐观的估计,我们希望训练/验证/测试分布是尽可能相似。
分层K折
分层K折是一个很好的解决方案。
这是k倍的变化形式, 每个目标类别的样本所占百分比与训练,验证和测试集中的完整数据集中的百分比大致相同。
您可以像这样导入和设置它:
from sklearn.model_selection import StratifiedKFold skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
请注意,它会返回您应该使用的训练/测试(或训练/验证)拆分的索引,因此,您每次必须手动配置拆分,如下所示:
for train_index, test_index in skfold.split(X, y): X_train, X_test= X[train_index], X[test_index] y_train, y_test= y[train_index], y[test_index] # TRAIN AND VALIDATE WITH THIS SPLIT
分层K折仅可直接用于分类数据,但是要让它对回归数据产生类似效果的一种简便而又简便的方法是将回归目标划分为狭窄的带,从而将问题转化为伪随机数据。分类。
您甚至可以临时向数据中添加等于合并目标值的新列,将该临时列分配为目标变量,基于此创建“分层”折叠,然后删除您创建的此额外数据列并恢复为使用精确的数值作为实际训练,验证和测试的目标变量。
K折
另一个可能的问题是数据中存在明显的组结构。
例如,我们有一个场景,其中从不同的主题收集数据样本,但是在某些(或全部)情况下,每个主题收集多个样本。
可以考虑尝试估算从货船上卸下的集装箱离开码头之前在船坞中停留了多长时间。每艘船可能会卸下数百个集装箱,因此长期来看,有明显的集装箱数据分组。如果该模型具有足够的灵活性来学习高度的集装箱特定功能,那么即使将来在训练/验证/测试拆分过程中表现出色,该模型也无法很好地推广到将来从不同船上卸载的集装箱。同一艘船。
GroupKFold是K折的一种变体,可确保同一组不在不同的集合中表示-即,来自同一组的所有数据实例仅在训练,验证或测试集中的一个中存在。
您可以使用与StratifiedKfold完全相同的方式使用它:
from sklearn.model_selection import GroupKFold X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10] y = ["a", "b", "b", "b", "c", "c", "c", "d", "d", "d"] groups = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3] gkf = GroupKFold(n_splits=3) for train, test in gkf.split(X, y, groups=groups): print("TRAIN INDEXES: {} TEST INDEXES: {}".format(train, test))
TRAIN INDEXES: [0 1 2 3 4 5] TEST INDEXES: [6 7 8 9]
TRAIN INDEXES: [0 1 2 6 7 8 9] TEST INDEXES: [3 4 5]
TRAIN INDEXES: [3 4 5 6 7 8 9] TEST INDEXES: [0 1 2]
如您所见,每个分组的数据完全显示在训练集中或测试集中。
时间序列数据的交叉验证
K倍交叉验证(及其变体)在时间序列数据上的效果很差,因为它们不遵守数据的时间顺序。
每个训练,验证和测试部分的数据都是随机选择的,因此我们几乎总是会在验证和测试集之前和之后都得到一些训练数据。
如果数据中的模式高度依赖于它们的发生时间以及其他特征,那么这实质上是数据泄漏的一种形式,因为我们使用未来的一些信息来预测过去和现在,从而导致过于乐观的估计模型技巧。
在某些情况下这可能是一场灾难,因为在某些情况下,如果之前和之后给出了信息,则在某些中等时间事件中填写空白会容易得多。
例如,思考市场-一个一百万美元的问题(或一万亿美元?)来预测它们将如何使用当前数据进行未来变化,但如果给出的话,则更容易猜测在给定时间范围内的价格波动情况来自问题期前后的价格数据(尤其是在较短的时间范围内)。
请注意,您的数据中明确存在“数据观察时间”列并不一定会强制定义是否应将某些数据视为时间序列。数据中可能有观察到的时间列,并且数据不是很依赖时间,因此您必须考虑数据的性质。
例如,人类的骨骼测量值与身高的相关关系在整个千年中可能略有变化,但是在数年甚至数十年的时间里,时间并不是很重要的组成部分,即使在数据中有观察时间也是如此。另一方面,市场价格走势对时间高度敏感,甚至是分钟和秒。
因此,对于时间序列数据,至关重要的是测试集严格由按时间顺序排列在验证和训练集之后的数据组成,同样,验证数据按时间顺序排列在训练集之后。
前向嵌套式交叉验证
为了实现这一目标,同时仍然能够使用我们的大部分数据进行验证(以及本例中的测试和结果),我们可以使用一种称为前行嵌套交叉验证的技术。
这个想法很简单。
过去,我们仅使用一小部分数据。
然后我们:
- 使用该集中的最新数据作为我们的测试数据
- 将之前的数据用作验证集
- 使用之前的所有数据作为我们的训练集
- 及时扩展我们的数据集,并重复1-3,直到测试集中的数据赶上今天为止。
该过程应如下所示:
考特尼·科克伦(Courtney Cochrane):https : //towardsdatascience.com/time-series-nested-cross-validation-76adba623eb9
虽然这可能看起来技术上棘手的实施,sklearn有(不出所料)另一个有用的功能来帮助我们解决这个- TimeSeriesSplit() 。
from sklearn.model_selection import TimeSeriesSplit
首先,请确保您的数据框已将其索引设置为相关的时间序列(我们一开始就已经这样做了),以确保索引按时间顺序排列。
然后使用所需的前向拆分数量创建一个TimeSeriesSplit()对象(n = 5给出5个具有相同大小的测试集的前向循环):
tscv = TimeSeriesSplit(n_splits=5)
当在数据帧上使用时,tscv以生成方式返回training:test的索引。
下面,我创建了一些更简单的数据来显示输出,以便更轻松地说明发生了什么:
X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4],[1, 2], [3, 4], [1, 2], [3, 4]]) y = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) for train_index, test_index in tscv.split(X): print("TRAIN:", train_index, "TEST:", test_index) X_train, X_test = X[train_index], X[test_index] y_train, y_test = y[train_index], y[test_index]
TRAIN: [0 1 2 3 4] TEST: [5]
TRAIN: [0 1 2 3 4 5] TEST: [6]
TRAIN: [0 1 2 3 4 5 6] TEST: [7]
TRAIN: [0 1 2 3 4 5 6 7] TEST: [8]
TRAIN: [0 1 2 3 4 5 6 7 8] TEST: [9]
这为我们提供了前瞻性的训练:测试部门精美。
现在,我们可以简单地根据每个前向实例中火车集中的最终数据的索引来截取我们想要的任何分数,以形成验证数据,因为无论如何它都是按时间序列索引按时间顺序排序的:
for train_index, test_index in tscv.split(X): # 80:20 training:validation inner loop split inner_split_point = int(0.8*len(train_index)) valid_index = train_index[inner_split_point:] train_index = train_index[:inner_split_point] print("TRAIN:", train_index, "VALID:", valid_index, "TEST:", test_index) X_train, X_valid, X_test = X[train_index], X[valid_index], X[test_index] y_train, y_valid, y_test = y[train_index], y[valid_index], y[test_index]
TRAIN: [0 1 2 3] VALID: [4] TEST: [5]
TRAIN: [0 1 2 3] VALID: [4 5] TEST: [6]
TRAIN: [0 1 2 3 4] VALID: [5 6] TEST: [7]
TRAIN: [0 1 2 3 4 5] VALID: [6 7] TEST: [8]
TRAIN: [0 1 2 3 4 5 6] VALID: [7 8] TEST: [9]
完善!(尽管要注意,我们只需要几个与n_splits大小有关的数据样本即可保证所有非空集-但这实际上不太可能成为问题!)
现在,我们可以在内部循环中每次简单地以一组(非交叉验证)方式执行训练和验证。我们不会再通过一个示例进行介绍,因为它可以完全按照“通过验证集进行的超参数调整”部分中的说明执行。
最后,请注意,一旦完成了所选的训练变体,交叉验证和测试模型,就值得将训练,验证和测试集的全部三个组合在一起,并在最后进行一次再训练。
最终的重组使模型可以从中学习的数据最大化(现在我们已经找到了最佳功能和超参数设置),因此可能会导致模型更加有效和健壮!
希望本文能为您提供一些新的想法,并加深您对训练,验证和测试集背后原因的理解。
您可以在此处找到本文中使用的代码和随附的数据集:https : //github.com/GregBland/train_val_test_sets_article
参考https://algotrading101.com/learn/train-test-split/
欢迎学习更多python金融风控评分卡模型和数据分析微专业课
(微信二维码扫一扫报名)