掺假:测试异常值检测器的技术

原文:towardsdatascience.com/doping-a-technique-to-test-outlier-detectors-3f6b847ab8d4?source=collection_archive---------2-----------------------#2024-07-09

使用精心制作的合成数据比较和评估异常值检测器

https://medium.com/@wkennedy934?source=post_page---byline--3f6b847ab8d4--------------------------------https://towardsdatascience.com/?source=post_page---byline--3f6b847ab8d4-------------------------------- W Brett Kennedy

·发表于Towards Data Science ·14 分钟阅读·2024 年 7 月 9 日

本文是我关于异常值检测系列文章的一部分,之前发布过关于计数异常值检测器频繁模式异常值因子的文章,并提供了我书籍Python 中的异常值检测的另一个摘录。

本文探讨了测试和评估异常值检测器的问题,这是一个众所周知的难题,并提出了一种解决方案,有时被称为掺假。通过掺假,真实数据行会被修改(通常是随机的),但修改的方式确保它们在某些方面很可能是异常值,因此应该被异常值检测器检测到。然后,我们可以通过评估检测器检测掺假记录的能力来评估它们的表现。

本文专门讨论表格数据,但同样的思路也可以应用于其他形式的数据,包括文本、图像、音频、网络数据等。

测试和评估其他类型的模型

如果你熟悉离群点检测,你可能也至少在某种程度上熟悉回归和分类问题的预测模型。对于这些问题,我们有标注数据,因此在调整模型时评估每个选项相对简单(选择最佳的预处理、特征、超参数等);同时,估计模型的准确性(它在未见过的数据上的表现)也相对容易:我们只需使用训练-验证-测试拆分,或者更好地使用交叉验证。由于数据是标注的,我们可以直接看到模型在标注测试数据上的表现。

但在离群点检测中,没有标注数据,问题也要困难得多;我们没有客观的方法来判断离群点检测器评分最高的记录是否确实是数据集中最具统计异常性的记录。

以聚类为例,我们同样没有数据的标签,但至少可以衡量聚类的质量:我们可以确定聚类内部的一致性如何,以及聚类之间的差异有多大。通过使用某种距离度量(如曼哈顿距离或欧几里得距离),我们可以衡量一个聚类内的记录彼此之间的接近程度,以及不同聚类之间的远离程度。

所以,给定一组可能的聚类,能够定义一个合理的度量标准(如轮廓系数),并确定哪一个聚类是首选的,至少在这个度量标准下是这样。也就是说,就像预测问题一样,我们可以为每个聚类计算一个分数,并选择表现最好的聚类。

然而,在离群点检测中,我们没有类似的东西可以使用。任何试图量化记录异常程度的系统,或是试图判断给定的两条记录中哪一条更为异常的系统,实际上都是一个离群点检测算法。

例如,我们可以使用熵作为离群点检测方法,然后检查完整数据集的熵,以及在移除任何被识别为强离群点的记录后,数据集的熵。从某种意义上说,这种做法是有效的;熵是衡量离群点存在的一种有用指标。但我们不能假设熵是该数据集中离群点的最终定义;离群点检测的一个基本特性是,没有离群点的最终定义。

一般来说,如果我们有任何方式来尝试评估离群点检测系统检测到的离群点(或者,像前一个例子中一样,评估包含和不包含已识别离群点的数据集),这实际上就是一个离群点检测系统本身,使用它来评估已找到的离群点会变得循环无效。

因此,评估离群点检测系统相当困难,实际上没有一个很好的方法,至少使用现有的真实数据无法做到这一点。

但是,我们仍然可以创建合成测试数据(以一种可以假定合成数据主要是异常值的方式)。有了这些数据,我们可以确定异常值检测器倾向于比真实记录给合成记录更高的分数的程度。

有许多方法可以创建合成数据,我们在书籍中介绍了这些方法,但在本文中,我们专注于一种方法,即掺毒。

掺毒数据记录

掺毒数据记录指的是对现有数据记录进行轻微修改,通常只改变每条记录中的一个或少数几个单元格的值。

如果被检查的数据是例如与一个由特许经营店组成的公司财务表现相关的表格,我们可能会为每个特许经营店创建一行数据,我们的目标可能是识别出这些数据中最异常的记录。假设我们有以下特征:

  • 特许经营的年龄

  • 当前所有者拥有的年数

  • 去年销售数量

  • 去年总销售额的美元数

以及一些其他特征。

一条典型记录可能包含如下四个特征的值:20 岁,5 年与当前所有者的合作,去年 10,000 笔独特销售,总销售额为 500,000 美元。

我们可以通过调整某个值为一个罕见的值来创建该记录的掺毒版本,例如,将特许经营的年龄设置为 100 年。这是可以做到的,并且可以对正在测试的检测器进行快速的烟雾测试——很可能任何检测器都会识别出这个值是异常的(假设 100 是一个罕见的值),虽然我们可能能够排除一些无法可靠检测到这种修改记录的检测器。

我们不一定会排除某种异常值检测器(例如 kNN、Entropy 或 Isolation Forest)的使用,而是会排除异常值检测器的类型、预处理方法、超参数以及检测器的其他特性。例如,我们可能会发现,某些超参数下的 kNN 检测器表现良好,而其他超参数下的 kNN 检测器则表现不好(至少对于我们测试的掺毒记录来说是这样)。

然而,通常大部分测试都会创建更微妙的异常值。在这个例子中,我们可以将总销售额从 500,000 美元改为 100,000 美元,虽然 100,000 美元仍然可能是一个典型值,但 10,000 笔独特销售与 100,000 美元的总销售额的组合对于这个数据集来说可能是异常的。也就是说,在掺毒时,我们通常创建的是具有异常值组合的记录,尽管有时也会创建单个异常值。

在记录中更改一个值时,具体如何使该行成为离群值(假设它确实成为了离群值)并不明确,但我们可以假设大多数表格之间的特征是有联系的。在此示例中,将美元值改为 100,000,可能(以及创造一个异常的销售数量和销售额组合)很可能会由于特许经营的年龄或当前拥有者的年限,创造出一个不寻常的组合。

然而,对于某些表格来说,特征之间没有关联,或者仅有少数且较弱的关联。这种情况较为罕见,但确实可能发生。对于这种类型的数据,没有异常值组合的概念,只有异常单一值。尽管这种情况罕见,但实际上它是一个更简单的处理案例:检测离群值更容易(我们只需检查单一的异常值),评估检测器也更容易(我们只需检查我们是否能检测到异常单一值)。不过,本文的其余部分将假设特征之间存在某些关联,并且大多数异常情况将是值的异常组合。

处理掺入数据

大多数离群值检测器(有少数例外)有独立的训练和预测步骤。因此,大多数离群值检测器与预测模型类似。在训练步骤中,评估训练数据并识别数据中的正常模式(例如,记录之间的正常距离、频繁项集、簇、特征之间的线性关系等)。然后,在预测步骤中,将测试数据集(可能与训练数据相同,也可能是不同的数据)与训练期间发现的模式进行比较,并为每一行分配一个离群值得分(或在某些情况下,一个二元标签)。

鉴于此,我们可以通过两种主要方式处理掺入数据:

  1. 在训练数据中包含掺入记录

我们可以在训练数据中包含少量的掺入记录,然后也使用这些数据进行测试。这可以测试我们在当前可用数据中检测离群值的能力。这是离群值检测中的常见任务:给定一组数据,我们通常希望找出数据集中的离群值(尽管也可能希望找出后续数据中的离群值——相对于该训练数据的规范来说是异常的记录)。

通过这样做,我们可以仅使用少量的掺入记录进行测试,因为我们不希望显著影响数据的整体分布。然后,我们检查是否能够将这些记录识别为离群值。一个关键的测试是,在训练数据中包含原始版本和掺入版本的掺入记录,以确定检测器是否将掺入版本的得分显著高于相同记录的原始版本。

然而,我们也希望检查掺杂记录是否普遍被评为最高(理解为一些原始未修改记录可能比掺杂记录更为异常,而且某些掺杂记录可能并不异常)。

鉴于我们只能用少量掺杂记录进行测试,这个过程可能会重复多次。

然而,掺杂数据仅用于以这种方式评估检测器。在为生产创建最终模型时,我们将仅使用原始(真实)数据进行训练。

如果我们能够可靠地检测到数据中的掺杂记录,我们可以合理地相信,我们能够识别出同一数据集中的其他异常值,至少是类似掺杂记录的异常值(但不一定是那些更加微妙的异常值——因此我们希望包括一些 reasonably subtle 的掺杂记录进行测试)。

2. 仅在测试数据中包含掺杂记录

也可以仅使用真实数据进行训练(我们可以假设这些数据大多数不是异常值),然后同时使用真实数据和掺杂数据进行测试。这允许我们在相对干净的数据上进行训练(真实数据中的一些记录可能是异常值,但大多数是典型的,并且不会因为掺杂记录而受到污染)。

它还允许我们使用可能会投入生产的实际异常值检测器进行测试(具体取决于它们在掺杂数据上的表现——无论是与我们测试的其他检测器相比,还是与我们对检测器最小表现的预期相比)。

这测试了我们在未来数据中检测异常值的能力。这是异常值检测中的另一个常见场景:我们有一个可以假定为相对干净的数据集(要么没有异常值,要么仅包含少量典型的异常值,并且没有极端异常值),我们希望将未来的数据与之进行比较。

仅使用真实数据进行训练,并使用真实和掺杂数据进行测试时,我们可以根据需要测试任何量的掺杂数据,因为掺杂数据仅用于测试而非训练。这使得我们能够创建一个较大且因此更可靠的测试数据集。

创建掺杂数据的算法

有多种方法可以创建掺杂数据,包括在《Python 中的异常值检测》中介绍的几种方法,每种方法都有其优缺点。为了简化,在本文中我们只介绍一种方法,其中数据是以相当随机的方式进行修改的:修改的单元格是随机选择的,替代原始值的新值是随机创建的。

这样做时,某些掺杂记录可能并不真正是异常的,但在大多数情况下,随机赋值会破坏特征之间的一个或多个关联。尽管如此,我们可以假设掺杂记录大多是异常的,尽管根据其创建方式,可能只是轻微异常。

示例

在这里,我们通过一个例子,使用真实的数据集,修改它并进行测试,以查看修改的检测效果。

在这个例子中,我们使用了一个在 OpenML 上可用的数据集,名为 abalone(www.openml.org/search?type=data&sort=runs&id=42726&status=active,该数据集在公共许可证下可用)。

尽管可以进行其他预处理,但在这个例子中,我们对分类特征进行独热编码,并使用 RobustScaler 对数值特征进行缩放。

我们使用了三种离群点检测器进行测试:Isolation Forest、LOF 和 ECOD,这些检测器都可以在流行的PyOD库中找到(必须先通过 pip 安装才能执行)。

我们还使用 Isolation Forest 来清理数据(去除任何强烈的离群点),以便在进行任何训练或测试之前进行处理。这个步骤并非必须,但在离群点检测中通常是有用的。

这是上述两种方法中的第二种方法的例子,我们在原始数据上进行训练,并用原始数据和掺假数据进行测试。

import numpy as np
import pandas as pd
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import RobustScaler
import matplotlib.pyplot as plt
import seaborn as sns
from pyod.models.iforest import IForest
from pyod.models.lof import LOF
from pyod.models.ecod import ECOD

# Collect the data
data = fetch_openml('abalone', version=1) 
df = pd.DataFrame(data.data, columns=data.feature_names)
df = pd.get_dummies(df)
df = pd.DataFrame(RobustScaler().fit_transform(df), columns=df.columns)

# Use an Isolation Forest to clean the data
clf = IForest() 
clf.fit(df)
if_scores = clf.decision_scores_
top_if_scores = np.argsort(if_scores)[::-1][:10]
clean_df = df.loc[[x for x in df.index if x not in top_if_scores]].copy()

# Create a set of doped records
doped_df = df.copy() 
for i in doped_df.index:
  col_name = np.random.choice(df.columns)
  med_val = clean_df[col_name].median()
  if doped_df.loc[i, col_name] > med_val:
    doped_df.loc[i, col_name] = \   
      clean_df[col_name].quantile(np.random.random()/2)
  else:
    doped_df.loc[i, col_name] = \
       clean_df[col_name].quantile(0.5 + np.random.random()/2)

# Define a method to test a specified detector. 
def test_detector(clf, title, df, clean_df, doped_df, ax): 
  clf.fit(clean_df)
  df = df.copy()
  doped_df = doped_df.copy()
  df['Scores'] = clf.decision_function(df)
  df['Source'] = 'Real'
  doped_df['Scores'] = clf.decision_function(doped_df)
  doped_df['Source'] = 'Doped'
  test_df = pd.concat([df, doped_df])
  sns.boxplot(data=test_df, orient='h', x='Scores', y='Source', ax=ax)
  ax.set_title(title)

# Plot each detector in terms of how well they score doped records 
# higher than the original records
fig, ax = plt.subplots(nrows=1, ncols=3, sharey=True, figsize=(10, 3)) 
test_detector(IForest(), "IForest", df, clean_df, doped_df, ax[0])
test_detector(LOF(), "LOF", df, clean_df, doped_df, ax[1])
test_detector(ECOD(), "ECOD", df, clean_df, doped_df, ax[2])
plt.tight_layout()
plt.show()

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/592ffaafb6f576b9d48038f97e15cbc7.png

在这里,为了创建掺假记录,我们复制了完整的原始记录集,因此掺假记录与原始记录的数量相等。对于每个掺假记录,我们随机选择一个特征进行修改。如果原始值低于中位数,我们创建一个高于中位数的随机值;如果原始值高于中位数,我们创建一个低于中位数的随机值。

在这个例子中,我们看到,虽然 IF 确实对掺假记录的评分较高,但差异并不显著。LOF 在区分掺假记录方面表现出色,至少对于这种掺假方式来说是这样。ECOD 是一个仅检测异常小值或异常大值的检测器,它不测试异常组合。由于这个例子中的掺假并没有创建极端值,只是一些不寻常的组合,因此 ECOD 无法区分掺假记录和原始记录。

这个例子使用箱形图来比较检测器,但通常我们会使用一个客观评分,通常是 AUROC(接收者操作特征曲线下面积)得分来评估每个检测器。我们通常还会测试多种模型类型、预处理方法和参数组合。

替代性掺假方法

上述方法往往会创建违反特征之间正常关联的掺假记录,但可以使用其他掺假技术来增加这种情况的可能性。例如,首先考虑分类列,我们可以选择一个新的值,使得同时满足以下条件:

  1. 新值与原始值不同

  2. 新值与根据行中其他值预测的值不同。为实现这一点,我们可以创建一个预测模型,预测该列当前的值,例如使用随机森林分类器。

对于数值数据,我们可以通过将每个数值特征划分为四个四分位数(或者若干个分位数,但至少是三个)来实现等效效果。对于数值特征中的每个新值,我们接着选择一个值,使得:

  1. 新值与原值处于不同的四分位数

  2. 新值与根据行中其他值预测的值处于不同的四分位数

例如,如果原值在 Q1,而预测值在 Q2,那么我们可以随机选择 Q3 或 Q4 中的值。这样,新值很可能会违反特征之间的正常关系。

创建测试数据集套件

一旦数据被篡改,就没有明确的方法来衡量记录的异常程度。然而,我们可以假设,平均而言,修改的特征越多,修改的程度越大,篡改后的记录就会越异常。我们可以利用这一点来创建多个测试集,而不是单一的测试集,这样可以更准确地评估异常值检测器的表现。

例如,我们可以创建一组非常明显的篡改记录(每条记录中修改了多个特征,每个特征的值与原值有显著差异),一组非常微妙的篡改记录(仅修改了一个特征,且与原值没有显著差异),以及介于两者之间的多个难度等级。这有助于很好地区分不同的检测器。

因此,我们可以创建一个测试集套件,其中每个测试集的(大致估算的)难度基于修改的特征数量和修改的程度。我们还可以有不同的集,其中修改了不同的特征,因为某些特征中的异常值可能更相关,或者更容易或更难被检测到。

然而,重要的是,任何进行的篡改都应该代表在实际数据中可能出现的异常值类型。理想情况下,篡改后的记录集还应很好地覆盖你希望检测到的异常值范围。

如果这些条件得到满足,并且创建了多个测试集,这对于选择表现最佳的检测器并评估它们在未来数据上的表现非常有效。我们无法预测将检测到多少异常值,也无法预测会出现多少假阳性和假阴性——这些很大程度上取决于你所遇到的数据,而在异常值检测中,这是非常难以预测的。但是,我们可以大致了解你可能会检测到哪些类型的异常值,以及哪些类型的异常值可能无法检测到。

可能更重要的是,我们还能够有效地创建一个异常值检测器的集成。在异常值检测中,集成通常对于大多数项目都是必要的。因为一些检测器能捕捉到某些类型的异常值,但会遗漏其他类型,而其他检测器则能够捕捉并遗漏其他类型的异常值,所以我们通常只能通过使用多个检测器,可靠地捕捉到我们感兴趣的异常值范围。

创建集成模型本身是一个庞大且复杂的领域,且与预测模型的集成不同。但对于本文,我们可以指出,了解每种检测器能够检测哪些类型的异常值,可以帮助我们判断哪些检测器是冗余的,哪些能够检测出大多数其他检测器无法识别的异常值。

结论

很难评估任何给定的异常值检测器在当前数据中识别异常值的效果,更难评估它在未来(未见过的)数据上的表现。对于两个或更多的异常值检测器,也很难评估哪个在当前和未来数据上表现更好。

然而,我们有多种方法可以使用合成数据来估计这些值。在本文中,我们至少快速地概述了一种基于对真实记录进行掺杂并评估我们如何能够将这些记录的得分提高到超过原始数据的程度的方法(跳过了许多细节,但涵盖了主要思想)。虽然这些方法并不完美,但它们非常有价值,而且在进行异常值检测时,往往没有其他实用的替代方案。

所有图片均来自作者。

AI智能图表创作平台,轻松对话绘图 Next AI Draw.io 是一款融合大语言模型与 draw.io 的创新型图表绘制平台。无需掌握复杂的绘图规则,只需通过自然语言输入,即可完成图表构建、修改与增强,帮助开发者和可视化创作者大幅提升效率。无论你是想绘制 AWS 架构图、GCP 拓扑,还是一个带有动画连接器的系统结构图,这款工具都能通过智能对话快速呈现。 核心亮点 LLM驱动的图表构建 通过 Chat 接口与 AI 对话,快速生成符合语义的图表,轻松支持 draw.io XML 格式解析。 图像识别与复制增强 上传一张已有图表或架构草图,AI 自动识别结构并重建图表,可进一步优化样式或内容。 图表版本管理 内置图表历史记录系统,支持版本切换与回滚,便于团队协作与修改回溯。 交互式绘图对话体验 内置对话界面,可边聊边画图,所见即所得,轻松优化图表结构与排版。 多云架构模板一键生成 支持 AWS、GCP、Azure 架构图自动生成,适配图标库,适合开发、运维、架构师使用。 GCP架构图 动画连接器 支持为图表元素添加动态连接器,提升图表交互性与演示感。 技术架构与支持 Next.js:提供稳定高性能的前端体验 Vercel AI SDK:整合流式对话与多模型支持 react-drawio:实现图表编辑与可视化渲染 多模型接入:支持 OpenAI、Anthropic、Google、Azure、DeepSeek、Ollama 等主流 AI API claude-sonnet-4-5 专项训练:在 AWS 架构图任务上表现优异
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值