跳至主要内容

异常检测综合入门

一篇关于掌握异常检测基础的教程——涵盖概念、术语与代码。
· 14分钟 阅读

使用 AI 探索

每个人都喜欢与众不同、脱颖而出。但作为数据科学家,您并不希望数据点也“与众不同”。数据集中的发散数据点或异常,是几乎所有数据项目中最危险的数据质量问题之一。

如果您只接触过那些打磨过的开源数据集(通常没有离群点),上面这句话可能会让您惊讶。然而,现实世界的数据集总会与“正常样本”存在一些差异。检测并妥善处理它们,就是您的工作。

在本文中,您将学习这一过程的基本理念,这一过程通常称为异常检测:

  1. 异常对项目的负面影响。
  2. 检测异常的重要性。
  3. 异常检测的真实应用场景。
  4. 异常(anomalies)、离群点(outliers)与新奇点(novelties)的区别。
  5. 异常类型与异常检测方法。
  6. 如何用 Python 构建异常检测算法。
  7. 如何应对异常检测中的挑战。

读完本文,您将掌握异常检测的基本原理,并有信心在项目中减少离群点的干扰。

什么是异常检测?

异常检测(有时也称离群点检测)是指在数据集中寻找与预期或“正常行为”明显偏离的模式或实例的过程。

“正常”和“异常”的定义会因情境而大不相同。下面是一些异常检测的实际示例。

1. 金融交易

正常:伦敦某个人的日常消费和稳定的支出模式。

离群:同一账户在爱尔兰发生一笔巨额取款,暗示潜在欺诈。

2. 网络安全中的网络流量

正常:常规通信、稳定的数据传输以及对协议的遵循。

离群:数据传输量突然增加或使用未知协议,表明潜在入侵或恶意软件。

3. 患者生命体征监测

正常:心率稳定、血压平稳

离群:心率突然上升且血压下降,提示潜在急症或设备故障。

异常检测包含多种无监督方法来识别发散样本。数据专家会根据异常类型、上下文以及数据集的结构和特征来选择方法。我们将在后文逐一介绍。

异常检测的真实应用

尽管上面已经看到一些示例,我们再来看一个关于异常检测在金融领域如何工作的真实故事。

四夺 NBA 总冠军的沙奎尔·奥尼尔(Shaq O’Neal)从迈阿密热火被交易到菲尼克斯太阳。抵达太阳队提供的空公寓后,他想在半夜立刻把公寓布置好。于是,他去了沃尔玛,准备刷一笔据称是沃尔玛有史以来最大的消费——7 万美元。至少他试过了;但他的卡被拒了。

他纳闷到底出了什么问题(他不可能没钱!)。凌晨 2 点,美国运通的安保给他打来电话,说他的卡疑似被盗,因为有人试图在菲尼克斯的沃尔玛刷 7 万美元(观看这段轶事了解完整故事)。

除了金融和欺诈检测之外,还有很多异常检测的真实应用:

  • 网络安全
  • 医疗健康
  • 工业设备监测
  • 网络入侵检测
  • 电网监控
  • 电商与用户行为分析
  • 制造业质量控制

异常检测深度融入我们日常使用的服务中,而我们往往察觉不到它的存在。

异常检测在数据科学中的重要性

数据科学中,数据是最宝贵的资产,而异常则是对其质量最具破坏性的威胁。糟糕的数据质量意味着糟糕的:

  • 统计检验
  • 数据看板
  • 机器学习模型
  • 决策

最终,这会动摇明智决策的基础。

异常会通过引入并不存在的模式来扭曲统计分析,导致错误结论和不可靠的预测。由于它们往往是数据集中的极端值,异常通常会偏移分布中最重要的两个特征:均值与标准差。

几乎所有机器学习模型的内部机制都高度依赖这两项度量,因此及时检测异常至关重要。

异常的类型

异常检测大致包含两类实践:离群点检测和新奇点检测。

离群点是仅存在于训练数据中的异常或极端数据点。相较之下,新奇点是相对于原始(训练)数据而言全新或此前未见的实例。

例如,考虑某城市的每日气温数据集。大多数日子的气温在 20°C 到 30°C 之间。然而某一天出现了 40°C 的峰值。这个极端温度就是离群点,因为它显著偏离日常气温范围。

再想象一下,该城市安装了一套更新、更准确的气象监测站。结果是数据集开始持续记录略高的温度,范围在 25°C 到 35°C。这个持续升高的温度就是新奇点,代表了由改进的监测系统引入的新模式。

而“异常”是一个更宽泛的术语,同时涵盖离群点与新奇点。它可用于在任意情境下指代任何不正常的实例。

识别异常的类型至关重要,因为这能帮助您选择合适的检测算法。

离群点的类型

与异常相似,离群点也有两类:单变量与多变量。根据类型不同,我们会采用不同的检测算法。

  1. 单变量离群点只存在于单个变量或特征中。它们是在该特征的典型数值范围之外的极端或异常值。
  2. 多变量离群点则是在同时结合多个变量的取值时被发现的。

例如,考虑某社区的房价数据集。大多数房屋价格在 20 万到 40 万美元之间,但有一套 A 房的价格高达 100 万美元。若只分析价格,A 房显然是离群点。

现在,让我们给数据集再加入两个变量:面积和卧室数量。当我们同时考虑面积、卧室数量和价格时,显得异常的却是 B 房:

  • 它的面积只有平均房屋的一半。
  • 它只有一间卧室。
  • 它的价格却在高位,为 38 万美元。

单独看这些变量时,它们似乎都很普通。只有把它们放在一起,我们才发现 B 房是明显的多变量离群点。

异常检测方法及其适用场景

异常检测算法会因离群点类型和数据集结构的不同而有所差异。

对于单变量离群点检测,最常见的方法有:

  1. Z 分数(标准分):z 分数衡量数据点距离均值有多少个标准差。一般而言,z 分数超过 3 的样本会被判为离群点。
  2. 四分位距(IQR):IQR 是分布的第一四分位数(Q1)与第三四分位数(Q3)之间的范围。当某个样本超出 Q1 或 Q3 加减 IQR 的某个倍数时,即被视为离群点。最常用的倍数是 1.5,即离群区间为 [Q1−1.5×IQR, Q3+1.5×IQR]。
  3. 改进的 z 分数:类似 z 分数,但使用中位数和中位绝对偏差(MAD)来寻找离群点。由于均值和标准差容易被离群点扭曲,改进的 z 分数通常更稳健。

对于多变量离群点,我们通常使用机器学习算法。凭借其深度与能力,它们能在复杂数据集中发现精细的模式:

  1. Isolation Forest(孤立森林):使用一组孤立树(类似决策树)递归划分复杂数据集,直到每个样本被孤立。最先被孤立的样本被视为离群点。
  2. 局部离群因子(LOF):LOF 度量某个样本相对于其邻居的局部密度偏差。密度显著较低的点被视为离群点。
  3. 聚类技术:如 k-means 或层次聚类将数据集划分为若干组。不属于任何组或自成一小簇的点被视为离群点。
  4. 基于角度的离群检测(ABOD):ABOD 计算各点之间的夹角。夹角异常的实例可视为离群点。

除了异常类型,您还应考虑数据集特征与项目约束。例如,Isolation Forest 几乎适用于任何数据集,但作为集成方法,它训练更慢、计算量更大。相比之下,LOF 训练非常快,但表现可能不如 Isolation Forest。

您可以在 Python Outlier Detection(PyOD)包的 55 个数据集上查看常见异常检测算法的对比。

用 Python 构建异常检测模型

如同几乎所有任务,Python 中有许多库可用于异常检测。最佳选择包括:

  • Python Outlier Detection(PyOD)
  • Scikit-learn

scikit-learn 提供了五种经典的机器学习算法(可用于单变量与多变量离群点),而 PyOD 则包含 30 多种算法,从 MAD 等简单方法到复杂的深度学习模型。您也可以使用 TensorFlow 或 PyTorch 构建自定义模型,但这超出了本文范围。

单变量异常检测

我更偏好使用 pyod,因为它算法丰富且 API 与 sklearn 一致。用 PyOD 仅需几行代码即可在数据集中发现并提取离群点。下面是对单变量数据集使用 MAD 的示例:

import pandas as pd
import seaborn as sns
from pyod.models.mad import MAD

# Load a sample dataset
diamonds = sns.load_dataset("diamonds")
# Extract the feature we want
X = diamonds[["price"]]

# Initialize and fit a model
mad = MAD().fit(X)

# Extract the outlier labels
labels = mad.labels_

>>> pd.Series(labels).value_counts()
0    49708
1     4232
Name: count, dtype: int64

我们逐行解析这段代码。首先,加载用于数据处理、加载数据集的必要库,以及用于离群点检测模型的 pyod。接着,加载 Seaborn 内置的 Diamonds 数据集,并提取钻石价格。

然后,我们用一行代码初始化并拟合中位绝对偏差(MAD)模型到 X。接着,通过 mad 的 labels_ 属性提取内点与离群点标签到 labels。

最后打印 labels 的各值计数可以看到,49708 属于类别 0(内点),而 4232 属于类别 1(离群点)。如果要从原始数据集中移除离群点,可以对 diamonds 使用 pandas 子集筛选:

outlier_free = diamonds[labels == 0]

>>> len(outlier_free)
49708

labels == 0 会创建一个由 True/False 组成的布尔数组,其中 True 表示内点。

多变量异常检测

创建多变量异常检测模型的流程也类似。但如果存在分类型特征,多变量离群点检测需要额外的预处理步骤:

>>> diamonds.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 53940 entries, 0 to 53939
Data columns (total 10 columns):
#   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
0   carat    53940 non-null  float64
1   cut      53940 non-null  category
2   color    53940 non-null  category
3   clarity  53940 non-null  category
4   depth    53940 non-null  float64
5   table    53940 non-null  float64
6   price    53940 non-null  int64  
7   x        53940 non-null  float64
8   y        53940 non-null  float64
9   z        53940 non-null  float64
dtypes: category(3), float64(6), int64(1)
memory usage: 3.0 MB

由于 pyod 期望所有特征为数值型,我们需要对分类变量进行编码。我们将使用 Sklearn 来完成:

from sklearn.preprocessing import OrdinalEncoder

# Initialize the encoder
oe = OrdinalEncoder()

# Extract the categorical feature names
cats = diamonds.select_dtypes(include="category").columns.tolist()

# Encode the categorical features
cats_encoded = oe.fit_transform(diamonds[cats])

# Replace the old values with encoded values
diamonds.loc[:, cats] = cats_encoded

>>> diamonds.head()

Diamond dataset

我们再逐行解析代码。首先,导入并初始化可对有序分类特征进行编码的 OrdinalEncoder 类。有序特征是具有自然顺序类别的变量,如本例中的钻石质量评定。切工(cut)和净度(clarity)是有序的,而颜色(color)并非如此。但为简化起见,此处将其视为有序。

然后,我们使用 Pandas DataFrame 的 select_dtypes 方法提取分类特征名。再串联 .columns 与 .tolist() 获取列名列表并命名为 cats。

接着,使用该列表与 oe 对目标特征进行转换。最后,借助 .loc 的便捷用法,将旧的文本取值替换为数值。

在拟合多变量模型之前,我们将提取特征数组 X。diamonds 数据集的目标是根据特征预测钻石价格。因此,X 将包含除 price 外的所有列:

X = diamonds.drop("price", axis=1)
y = diamonds[["price"]]
Now, let’s build and fit the model:
from pyod.models.iforest import IForest

# Create a model with 10000 trees
iforest = IForest(n_estimators=10000)
iforest.fit(X)  # This will take a minute

# Extract the labels
labels = iforest.labels_

IForest 基学习器的树越多,将模型拟合到数据集所需的时间就越长。

得到标签后,我们可以从原始数据中移除离群点:

X_outlier_free = X[labels == 0]
y_outlier_free = X[labels == 0]

>>> len(X_outlier_free)
48546

>>> # The length of the original dataset
>>> len(diamonds)
53940

该模型找到了 5000 多个离群点!

异常检测的挑战

由于异常检测的无监督特性,它可能比其他机器学习任务更具挑战性。不过,大多数挑战都可以通过多种方法加以缓解(参见下一节的资源)。

在离群点检测中,我们需要提出两个问题:

  • 找到的离群点真的就是离群点吗?
  • 模型是否找到了数据中的所有离群点?

在监督学习中,我们可以轻松通过将模型在测试集上的预测与真实标签比对来检查其表现。但在离群点检测中,我们无法这样做,因为没有现成的标签告诉我们哪些是内点、哪些是离群点。

因此,从 diamonds 数据集中找到的 5000 多个离群点,其中一部分甚至大部分,可能并不是真正的离群点!我们无法确知。模型可能把一些内点标成了离群点,同时又漏掉了一些真正的离群点。

这种对污染率(数据集中离群点所占比例)不确定的问题,是异常检测中最大的难题。因为它,我们既无法可靠评估离群分类器的性能,也无法验证其结果。

基于此,pyod 中的所有估计器都有一个名为 contamination 的参数,默认设为 0.1。作为机器学习工程师,您需要自行调参。

当然,有时也有替代方案。例如,Sklearn 提供的 IsolationForest 模型内部包含自动估计污染率的算法。但 IsolationForest 并非解决所有离群检测问题的银弹。

异常检测中的另一个问题是数据不平衡。与正常样本相比,异常通常很少见,导致数据集不平衡。这种不平衡会使得区分真正的异常与多数类内部的不规则点变得困难。

应对这些问题(以及本文未覆盖的更多问题)需要选择合适的算法、进行超参数调优、特征选择、处理类别不平衡等。

总结与后续

我们刚刚完成了您(大概)对异常检测这个迷人世界的首次接触。本教程中的基本概念与技能,足以支撑您进一步探索异常检测。

若想加深理解,可以参考以下资源:

Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题