小心数据泄露——机器学习模型中的潜在陷阱

作者:Shon Mendelson,数据科学家@JFrog

2022年1月11日

16分钟阅读

背景

考虑以下场景:你已经在一个机器学习模型上工作了几个月,其中包含了所有的基本元素,包括特征工程、特征选择、模型选择、超参数调优等。业务批准,您部署模型,突然之间,对于正在进行的数据,结果没有加起来。造成这种结果差异的一个常见(也是痛苦的)原因可能是数据泄漏。在下一篇文章中,我将解释什么是数据泄漏,以及什么可能导致数据泄漏。我还将建议一种在开发阶段处理数据泄漏的简单方法。

数据泄漏和技术术语

数据泄漏指的是机器学习模型的创建者所犯的错误,在模型的训练过程中,关于目标变量的信息泄漏到模型的输入中;这些信息在我们想要预测的持续数据中是无法获得的。

训练集-用于训练模型的标记数据集。

验证设置-用于评估训练模型的标记数据集。

正在进行的数据(生产)-训练模型预测并创建预测标签的未标记数据。

数据收集-从数据库/源系统等中提取数据的过程。

数据预处理-转换原始数据以使其适用于我们的模型的过程。例如,缩放基于优化的算法(神经网络,支持向量机,逻辑回归等)的数字特征。

数据泄露挑战

与我们可以通过测量训练-验证评估指标之间的差异来检测的过拟合不同,数据泄漏更难检测,因为你的训练和验证都可以获得很好的结果,但正在进行的数据的结果将大大降低。一旦模型已经部署到您的生产环境中,识别泄漏是不理想的。对我们来说,在开发阶段就能识别它是很困难的。

数据是如何泄漏的?

数据泄漏可以在许多类型的现实生活场景中暴露出来。我们将检查一个与泄漏源相关的示例数据集,泄漏源可能由以下原因引起:错误的数据收集、错误的数据处理或有偏差的抽样。

考虑以下数据集:

上面的表是事务数据的一个示例。每个ID代表一个人,每行代表特定时间戳上的一个操作(访问公司网站页面、购买等)。它还包括电话号码和一个分类变量,用于指示电话号码是有效的、无效的还是未提供的。类表明该人是否购买了该产品(二元变量)。

为了更好地理解它,让我们以第一个ID为例:这个人在buy-now页面(前两行)访问了两次,购买了产品(第三行),并在激活页面(第四行)访问了一次。他提供了有效的电话号码。它的类值是1,因为他购买了该产品。

让我们深入研究可能导致数据泄露的错误。

错误的数据收集

当我们包含来自未来事件的信息时,可能会发生数据泄漏。例如,我们可以将激活页面的访问次数作为一个解释变量。但如果我们这样做没有时间过滤,我们将教导模型:P(购买|激活页面的访问次数= 0)= 0。

因为我们的目标是预测一个人是否会在实际购买之前购买我们的产品,这个规则不适用于正在进行的数据,其中的人还没有购买产品,因此,可能永远不会访问激活页面,尽管他们有购买产品的意图(至少其中一些)。

更复杂的情况是两个事件同时发生。例如,我们可以看到购买该产品的每个人都在购买前几分钟访问了立即购买页面。如果我们不过滤掉这些事件,我们将再次告诉我们的模型,每个购买产品的人都会访问立即购买页面。这可能在购买之前是正确的,但可能在购买前几个小时,这个人从未访问过立即购买页面,尽管他已经决定购买该产品。

为了避免这种数据泄漏,我们需要将训练数据中的每个ID定义为其ID相关日期,以过滤掉在此之后发生的事件。的相关日期可以是观察值从类别0变为类别1的日期(在我们的示例中是购买日期),或者,根据业务需要,我们可以定义我们希望多早预测类别的变化并定义相关日期相应的行动。

数据收集引起的数据泄漏的另一种情况是,当一个功能(或几个功能)具有依赖于目标变量的动态更新时。按照上面的示例,我们可以从电话号码创建一个具有三个级别的分类特征——未提供、有效和无效。到目前为止一切顺利。但是,如果在购买之后要求该人提供有效的电话号码,则会使其变得更加复杂,因为功能值可能会因购买而更改。我们的模型将学习到P(购买|电话号码无效或未提供)= 0,这也是不正确的。

为了避免这种数据泄漏,请确保您准确地了解每个特性是如何收集的,以及是否要根据类事件更新它。

错误的数据预处理

您创建了一个查询来生成训练数据,您的目标是拟合一个监督模型。在大多数情况下,需要进行一些预处理。预处理可以是无监督的,也可以是有监督的,当涉及到数据泄漏时,后者要危险得多。

监督预处理的一个例子是目标编码。在目标编码中,分类特征被编码为目标变量的均值。如果它应用于所有数据而没有分离到训练和验证,那么编码的特征似乎比它更好,因为它包含了关于目标变量的信息,这些信息在没有标签的持续数据中是不可用的。

无监督的预处理(例如,缩放)可能不会导致目标泄漏,但如果应用不当,也会导致模型的不可靠评估。

常用的方法是使用训练数据计算预处理的参数,并将其应用于训练数据和正在进行的数据。也就是说,对训练数据使用fit_transform(),对正在进行的数据使用transform()。如果你这样做,在验证阶段做同样的事情,即,将数据拆分为训练和验证,计算训练上的缩放参数(或任何其他预处理),然后将其应用于训练和验证数据。

为了避免这种数据泄漏/不可靠的评估,您可以使用scikit-learn管道

有偏见的抽样

在许多实际应用程序中,标记的数据没有得到很好的组织。按照上面的例子-正面类定义良好(购买产品的人),以及他们的相关日期是购买日期。

对于不购物的人群来说,这有点棘手。如果您的营销团队记录了他们何时定义一个人为不合格(即,~0购买机会)-您可以将其用作他们成为第0类的日期。但是,否则,你怎么知道他们什么时候没有购买产品(过滤掉前面讨论的未来事件)?也许他们还没有购买产品,但他们还在评估产品?

如果你选择今天相关的日期,你会得到一个有偏见的群体——积极的群体在历史上分布得很广,而消极的群体关注的是最近的数据。购买产品的人在不同的时间戳中购买,而没有购买的人都将获得相关日期=今天。您的模型将学习如何区分最近的数据和旧的数据,而不是学习如何区分购买产品的人和不购买产品的人。

数据泄漏不仅出现在表格数据中,也出现在NLP。假设你的目标是建立一个分类器来区分积极和消极的情绪(假设没有确切的公开数据集),所以你让一个朋友写许多积极情绪的句子,另一个朋友写许多消极情绪的句子。这些句子可能包含一些与句子是肯定的还是否定的事实无关的信息。例如,如果积极情绪写作者倾向于比消极情绪写作者写更短的句子——你教你的模型:P(积极句子短)~ 1,这在现实生活中的持续数据中不一定是正确的。

数据泄露将摧毁你的机器学习模型

我知道我不能选择一个比这更戏剧性的标题,但我真的相信,当你的模型包含泄漏时,你不能相信它对正在进行的数据(至少不像你在验证阶段那样信任)。在验证阶段,评估指标将更加乐观,但这并不是唯一的问题。此外,包含泄漏的特征在训练中会比在持续数据中得到更好的处理;因此,我们的模型将被错误地构建,因为:

  1. 泄漏的特征将在模型中获得比它们应有的更高的权重(例如,基于决策树的模型的树的更高级别)。
  2. 我们将无法了解泄漏特征与模型中其他特征之间的真实相互作用。
  3. 我们可能会基于泄漏特征应用错误的特征工程。

想想看,如果你教你的模型一个与目标变量的直接关系(一个在正在进行的数据中没有观察到的关系),比如上面的例子P(购买|电话号码未提供或无效)= 0,你就不会让你的模型学习在电话号码未提供或无效的情况下影响购买概率的因素。

如何检测数据泄漏?

如果我们的目标是检测正在进行的预测中的泄漏,我们可以监视特征分布之间的差异(例如,使用KL散度对于一个分类特征)。虽然特性分布的变化不一定会导致数据泄漏,但也有可能概念漂移。但至少如果我们指出了这样一个特性,我们就可以验证我们收集和处理它的方式。

我们还可以监控评估指标,如果有很大的差异,它可以指向数据泄漏(也可以是过拟合,但让我们假设我们在开发阶段确保我们的模型没有过拟合)。
这些重要的概念也应该应用于长期监测。为了在开发阶段检测泄漏,我建议一个简单而有效的方法,即基于决策树的模型:

  1. 训练基于决策树的模型。
  2. 绘制特征的重要性。
  3. 如果你看到一个树干的形状,这意味着最好的特征和第二个特征之间的差距非常大。这意味着您可以只使用第一个功能并获得与使用所有功能所获得的相同(或几乎相同)的结果。我并不是说不存在仅使用一两个特性就可以预测目标变量的情况,但对于大多数实际应用程序来说,情况并非如此。

与上面的用例保持一致,让我们来演示一下这个想法:

从sklearn中导入pandas作为pd导入numpy作为np。导入RandomForestClassifier导入matplotlib从datetime中导入随机,从sklearn中导入timedelta。从sklearn中导入train_test_split, cross_val_score。指标导入f1_score

首先,我模拟了上面示例中所示的事务数据。对于那些购买了产品的人,我在购买后的激活页面模拟了一次访问。

模拟数据

db_size = 10000 n_ids = 2500 df = pd.DataFrame() df[' ID '] = np.random。Randint (n_ids, size=db_size) events_types = [' blog_page ', ' product_page ', ' pricing_page ', ' purchase '] df[' event_type '] = random。Choices (events_types, k=db_size, cum_weights=[0.5, 0.8, 0.98, 1]) df[' event_timestamp '] =[pd.to_datetime(" today ") - timedelta(hours=np.random. 1)Randint (24*30, size=1)[0].item()) for I in range(db_size)] df.loc[df. size]Event_type == ' purchase ', ' event_timestamp '] = df.loc[df. info]Event_type == ' purchase ', ' event_timestamp '] + timedelta(days=30)

在购买后的激活页面中模拟访问

activations_df = pd.DataFrame(columns=df.columns) i = 0 for find in df[df. columns]。Event_type == ' purchase ']。指数:activations_df。Loc [db_size + i] = [df. size]loc[ind, ' ID '], ' activation_page ', df。Loc [ind, ' event_timestamp '] + timedelta(minutes=np.random。Randint (60, size=1)[0].item())] I += 1 df = pd。Concat ([df, activations_df],坐标轴=0)

之后,我为购买产品的人添加了正面标签。从那些还没有购买的顾客中,我随机选择了一些被贴上负面标签(比如被我们的营销团队标记为不合格)。其余的没有标签,因为他们既没有购买产品,也没有被标记为不合格。它们将是我们这次演示的持续数据。

添加标签

Purchase_ids = df.loc[df. loc]。event_type == ' purchase ', ' ID '].unique() none_purchase_ids_size = purchase_ids。Shape [0] * 10df [' class '] = np。nan df.loc[df[' ID '].isin(purchase_ids), ' class '] = 1 none_purchase_ids = random.sample(list(df [' class '].isnull(), ' ID '].unique()), none_purchase_size) df.loc[df[' ID '].isin(none_purchase_ids), ' class '] = 0 . nan df.loc[df[' ID '].isin(none_purchase_ids), ' class ']] = 0

分为训练数据(带标签)和持续数据(不带标签)

df_train = df [~ df(类的).isnull ()] df_test = df (df(类的).isnull ())

以下函数用于分析。第一个函数用于聚合事务数据—对于每种事件类型,我计算了它作为解释变量出现的次数。第二个函数用于绘制特征的重要性。

有用的功能

功能聚合

defaggregate_data (data, is_train=True): by = [' ID ', ' class '] if is_train else [' ID '] data = data. if = ' ' ID ' . ' ' 'groupby(由= as_index = False)。Agg (count_blog=(' event_type ', lambda x: np。Sum (x == ' blog_page ')), count_product=(' event_type ', lambda x: np。Sum (x == ' product_page ')), count_pricing=(' event_type ', lambda x: np。Sum (x == ' pricing_page ')), count_activation=(' event_type ', lambda x: np。Sum (x == ' activation_page ')))返回数据

情节特点

def plot_feature_importance(X_train, model): features = X_train。列的重要性=模型。Feature_importances_ indices = np.argsort(importances)title(' Feature Importance ') plt.barh(range(len(indexes)), importances[indexes], color= ' b ', align= ' center ') plt.yticks(range(len(indexes)), [features[i] for i in indexes]) plt。xlabel(' Relative Importance ')

这里我使用了上面的函数来聚合正在进行的数据。

汇总正在进行的数据

df_test = aggregate_data(df_test, is_train=False) X_ongoing = df_test。下降((“ID”),轴= 1)

下面是一个数据泄漏的例子。我没有过滤掉未来的事件。

数据泄漏(包括未来事件)

df_train_leak = df_train[df_train. cn]= ' purchase '] df_train_leak = aggregate_data(df_train_leak) X, y = df_train_leak。drop([' ID ', ' class '], axis=1), df_train_leak [' class '] rf = RandomForestClassifier(class_weight= ' balanced ')

评价

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)fit(X_train, y_train) y_pred= rf.predict(X_test) score = f1_score(y_true=y_test, y_pred=y_pred) print(' F1成绩为:' + str(np。轮(得分,2)))
F1得分为1.0

我的评估是完美的。但这并不是因为我开发了一个很棒的模型。因为我有数据泄露导致我的模型学习P(Purchase| visit in activation page) = 1。


特征重要性图也表明count_activation特性非常好。“好得令人难以置信”。

对持续数据的预测

y_pred_leak = r .predict(X_ongoing) positive_frac_predict = y_pred_leak .mean() print(' The forecasts for ' + str(np。四舍五入(positive_frac_predicting, 2) * 100) + ' %的人是积极的(购买)')
0%的人的预测是肯定的(购买)

我们还可以看到,即使我们的评估是完美的,我们的模型预测,没有人会购买我们的产品在持续的数据。很伤心。

这里我从排除未来事件开始。

无数据泄漏(不包括未来事件)

购买日期= df_train.loc[df_train. loc]event_type == ' purchase ', [' ID ', ' event_timestamp ']] purchase_dates。columns = [' ID ', ' purchase_timestamp '] df_train_no_leak = df_train。合并(purchase_dates, how= ' left ') df_train_no_leak = df_train_no_leak [(df_train_no_leak .)]event_timestamp < df_train_no_leakage.purchase_timestamp. timestamp) | (df_train_no_leakage.purchase_timestamp.isnull())] df_train_no_leak = aggregate_data(df_train_no_leak) X, y = df_train_no_leak . X, y = df_train_no_leak。drop([' ID ', ' class '], axis=1), df_train_no_leak [' class '] rf = RandomForestClassifier(class_weight= ' balanced ')

评价

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)fit(X_train, y_train) y_pred= rf.predict(X_test) score = f1_score(y_true=y_test, y_pred=y_pred) print(' F1成绩为:' + str(np。轮(得分,2)))
F1得分为:0.07

我们可以看到,评价要低得多。

plot_feature_importance (X_train = X_train模型= rf)


特征重要性看起来正常。

对持续数据的预测

y_pred_no_leak = r .predict(X_ongoing) positive_frac_predict = y_pred_no_leak .mean() print(' The forecasts for ' + str(np. net))四舍五入(positive_frac_predicting, 2) * 100) + ' %的人是积极的(购买)')
26%的人的预测是积极的(购买)

对于正在进行的数据,一些预测作为潜在的购买我们的产品。

扩展-计算机视觉中的数据泄漏

在计算机视觉中,不相关的信息可能会泄漏到图像中,这可能会使神经网络学习到一种规则,这种规则在开发阶段非常棒,但对正在进行的数据毫无用处。

假设你的目标是通过一张图片来区分健康的和受感染的植物。为此,你设计一个实验来获得一些标记数据。跳过它的生物学方面——假设你需要应用一些过程来使一些植物感染(阳性类),并说这个过程需要一天以上的时间。你如何记住哪些植物是感染过程的一部分,哪些不是?最简单的解决方法就是标记它们。A组是受感染的植物,B组是健康的植物。你最终得到的是图像和标签。唯一的问题是,这些图像包含了关于目标变量的信息——标记为“A”的正类和标记为“B”的负类。

如果你直接在图像上训练一个深度神经网络,它将学会识别字母“a”或“B”来区分植物是感染了还是健康。在现实世界中,植物不会被标记,因此,从图像中学习标签的模型将为正在进行的数据崩溃。

另一个类似的例子是,不同的班级在一天中的不同时间被描绘出来。因此,不是学习受感染植物的特征,而是学习如何区分白天和黑夜。

最后的话

希望您现在可以看到数据泄漏现象是多么普遍,以及每个数据科学家应该如何熟悉它。虽然小心谨慎很重要,但重要的是要记住,并不是评估指标或特征分布的每次变化都是导致数据泄漏的直接原因。然而,通过理解特征与目标变量之间的关系,并确切地知道特征是如何收集的,您仍然能够密切关注可能对模型造成损害的数据泄漏。

演讲者

铁城Mendelson

铁城Mendelson

数据科学家@JFrog

作为JFrog的数据科学家,Shon负责实施和管理业务部门的机器学习项目。在JFrog工作之前,他研究和开发了使用机器学习、深度学习和计算机视觉技术的精准农业算法。Shon拥有统计与经济学学士学位和工业工程硕士学位,专注于数据科学。作为论文的一部分,Shon开发了一种用于数据流的新颖性检测算法,该算法已发表,并在一个领先的机器学习会议上受到高度评价。