我最近收到银行发来的短信:
“我们已暂时冻结您的卡。请确认您是否在 [vendor] 进行了一笔金额为 [amount] 的交易?”
幸好,那只是我在不太合理的时间点进行网购。但这也让我开始思考……银行如何在每天处理的成千上万笔交易中标记可疑交易?
识别欺诈是 机器学习(ML) 中一个更广泛问题—— 异常检测 ——的一个具体场景。解决这个问题的方法很多,但本文将聚焦于其中最常用的一种: Isolation Forest 。
实践中的异常检测
打击欺诈、网络防御、发现离群点、标记故障设备,这些看似不同的问题其实有一个共同点——并没有明确的标签或定义来界定什么是异常。很多时候,异常既稀少又隐蔽,难以从海量的正常数据中识别出来。
一种思路是使用 预测式 机器学习 来构建异常检测系统。比如,我们可以收集欺诈交易样本,然后利用交易金额、时间、地点、商户类型等特征,训练一个模型来区分正常与异常交易。问题在于,这种方法通常只能发现与历史相似的欺诈类型。
欺诈者会不断改变策略,尝试全新的作案方式,其中很多都出乎意料。因此,常常使用 无监督方法 来检测异常。它们通过比较所有交易,识别出具有不寻常特征值的样本。重要的是,这意味着我们无需事先为交易打标签。
难以归类的行为并不只存在于欺诈。在制造业中,机器可能会以各种意料之外的方式随时发生故障。在 网络安全 中,新型攻击不断被发现与修补。在 医学 中,罕见病症可能隐藏在大量检测结果之中。应用场景不胜枚举,下面就来看看一种无监督方法如何发现异常。
什么是 Isolation Forest?
顾名思义, Isolation Forest 是一种 基于树的 方法。它与随机森林类似,但其基本构件不同。
随机森林使用基于基尼不纯度的分裂来构建决策树,这需要目标变量;而 Isolation Forest 使用“隔离树”。该方法通过集合这些树来为每个样本计算异常分数。
隔离树
在计算异常分数之前,值得先理解这一基本构件如何工作。假设我们试图检测欺诈交易。为简单起见,我们评估 1000 笔交易(样本),并考虑数据集中的两个变量:金额( x 1 )和交易时间( x 2 )。
构建一棵隔离树时,我们从根节点包含全部或部分样本开始。在下方的 图 1 中,您可以看到我们使用了 256 个样本(稍后解释这个数的来由)。
图 1:通过递归随机分裂创建的隔离树。每个节点标注了样本数量。
随后:
随机选择一个特征( x 1 或 x 2 )。
在当前节点样本的该特征取值范围内,随机选择一个分裂阈值。
根据分裂阈值将数据划分为两个子节点。
对每个子节点递归重复上述过程,直到满足以下任一条件:
每个叶节点仅包含一个样本。
达到预设的最大深度。
请注意,每一步中特征的取值范围都会改变。也就是说,我们使用当前节点样本在该特征上的最小值和最大值。这样的动态调整可确保分裂有意义,且节点不会出现零样本。
图 2 能帮助我们直观理解为何该过程会隔离离群点。样本 A 、 B 、 和 C 看起来都与其他交易不同。
图 2:交易的散点图
B 很可能最先被隔离,因为它可以通过任一特征的一次分裂就与其他交易分开。
A 则可能通过一次 x 2 分裂被隔离。
C 可能需要更长时间,因为需要同时利用两个特征来隔离。也就是说, C 的金额与时间各自都显得正常,但“在该时段上金额不正常”。
我们可能会走运,第一次就隔离出这些样本中的一个,但通常需要多试几次。关键在于,平均而言, B 所需的分裂次数少于 A , A 少于 C ,且这三点所需的分裂次数都少于其他交易。那么,我们如何量化这种“平均表现”呢?
计算异常分数
隔离异常所需的分裂次数,决定了样本到叶节点的路径长度。当一片隔离树的森林对某些样本持续给出较短的路径长度时,这些样本很可能是异常。我们通过在所有树上聚合路径长度来捕捉这种平均行为,即计算异常分数。
要为样本 x 计算异常分数,先定义几个量:
路径长度 h(x) 是到达该样本叶节点需经过的边(分裂)数。若直接取平均 E[h(x)] 会有问题,因为更大的树(包含更多样本)通常会有更长的路径,即便对异常也是如此。为此,我们需要基于样本量 n 的归一化量:
该值源自 二叉搜索树(BST) 理论,表示在 BST 中一次不成功搜索的平均路径长度。由于隔离树与 BST 具有等价的结构,我们将 c(n) 视为一个样本(无论是否异常)的期望路径长度。注意,如果设置了树的最大深度,该值需要相应调整。
最后,我们即可计算异常分数 s(x,n) :
该分数介于 0 与 1 之间。解读时可考虑三种情形:
若某点为异常,则 E[h(x)] < c(n),且 s → 1
若某点非异常,则 E[h(x)] > c(n),且 s → 0
若 E[h(x)] = c(n),则 s = 0.5
换言之,当某样本的平均路径长度短于期望平均路径长度时,其异常分数更接近 1。不过,正如我们在实现时会看到, Scikit-learn 包对该分数还进行了进一步调整。
用 Python 和 Scikit-learn 实现 Isolation Forest
我们一步步来,从导入依赖开始。
导入
我们使用常见的 Python 数据处理与可视化包(第 2–5 行),并使用 Scikit-learn 中的 Isolation Forest 实现(第 7 行),最后一个包用于导入数据集(第 8 行)。
import pandas as pd
import numpy as np
import matplotlib. pyplot as plt
import seaborn as sns
from sklearn. ensemble import IsolationForest
from ucimlrepo import fetch_ucirepo技术支持
数据集
我们直接从 UCI 机器学习资源库 加载数据。这里使用 Air Quality 数据集(CC BY 4.0),其中包含来自意大利某城市的一台传感器的 9,358 条空气质量测量。在该数据集的语境下,异常可以理解为指示污染水平异常偏高的传感器读数。
air_quality = fetch_ucirepo( id = 360 ) 技术支持
数据清洗
在应用模型前,我们先做一些清洗操作。先从数据集中选取所有特征(第 2 行),再选取 4 个特征的子集(第 5 行)。这些特征都是空气中不同金属氧化物(即污染物)的测量值。随后我们删除包含缺失值的行(第 8–9 行)。最终剩余 6,941 条样本,下面可以看到特征集的一个快照。
data = air_quality. data. features
features = data[ [ 'CO(GT)' , 'C6H6(GT)' , 'NOx(GT)' , 'NO2(GT)' ] ]
features = features. replace( - 200 , np. nan)
features = features. dropna( )
print ( features. shape)
features. head( ) 技术支持
定义训练参数
最后一步是定义用于训练 Isolation Forest 的参数。在这里,我们将使用以下三个值:
n_estimators:集成中的隔离树数量。Isolation Forest 论文中使用了 100。研究者通过实验发现,这个值在多种数据集上表现良好。
contamination:我们预计为异常的数据点比例。
sample_size:训练每棵隔离树所用的样本数。常用值为 256,它使我们无需设置最大树深作为停止条件,因为可期望合理的最大树深约为 log(256) = 8。
与其他参数不同,contamination 的取值没有那么“硬”的依据。它可以来自以往经验。比如,在之前的分析中,我们发现 1% 的读数指示污染水平较高。它也可能受资源限制所驱动。例如,在分析欺诈交易时,您可能只来得及对最差的 5% 分数的交易做进一步分析。可视化异常分数时,我们会看到它们如何基于该值进行调整。
n_estimators = 100
contamination = 0.01
sample_size = 256 技术支持
训练 Isolation Forest
现在可以开始训练模型了。如果您熟悉 Sklearn,下面的流程应该并不陌生。我们用上述参数初始化模型(第 2–5 行),然后在特征集上进行训练(第 6 行)。
iso_forest = IsolationForest( n_estimators= n_estimators,
contamination= contamination,
max_samples= sample_size,
random_state= 42 )
iso_forest. fit( features) 技术支持
计算异常分数
训练好的模型提供了两个有用的函数:
decision_function:以与理论部分类似的方式计算异常分数。
predict :基于 contamination 值给出二元标签。在我们的设置中,异常分数最差的 1% 样本被标记为 -1,其余样本标记为 1。
我们将这两个函数的输出添加到数据集中(第 2–4 行)。统计异常标签(第 6 行)后可以看到,有 70 个样本被标记为异常。
data = data. loc[ features. index] . copy( )
data[ 'anomaly_score' ] = iso_forest. decision_function( features)
data[ 'anomaly' ] = iso_forest. predict( features)
data[ 'anomaly' ] . value_counts( ) 技术支持
我们还可以进一步把所有异常分数可视化,代码如下:
plt. figure( figsize= ( 10 , 5 ) )
normal = data[ data[ 'anomaly' ] == 1 ]
plt. scatter( normal. index, normal[ 'anomaly_score' ] , label= 'Normal' )
anomalies = data[ data[ 'anomaly' ] == - 1 ]
plt. scatter( anomalies. index, anomalies[ 'anomaly_score' ] , label= 'Anomaly' )
plt. xlabel( "Instance" )
plt. ylabel( "Anomaly Score" )
plt. legend( )
plt. show( ) 技术支持
橙色标出的 70 个样本拥有最低的异常分数。它们是潜在异常,下一步需要进一步排查。
此时您也许有些困惑:我们不是说“更接近 1 的分数表示更可能异常”吗?
上面散点图中的分数已经过调整。为此,库首先计算一个 偏移量 ,即基于 contamination 值的异常分数分位数。在我们的例子中,偏移量为 0.99 分位。最终分数为 offset - score 。这样处理后,小于 0 的分数都提示潜在异常。
进一步分析
借助上面的散点图,我们可以定位潜在异常。但它不会告诉我们“为何”被判为异常。为此需要进一步分析。
例如,下面的代码绘制了所有样本的 CO(GT) 与 NO2(GT) 的散点图,并根据是否被分类为异常进行着色。
plt. figure( figsize= ( 5 , 5 ) )
plt. scatter( normal[ 'CO(GT)' ] , normal[ 'NO2(GT)' ] , label= 'Normal' )
plt. scatter( anomalies[ 'CO(GT)' ] , anomalies[ 'NO2(GT)' ] , label= 'Anomaly' )
plt. xlabel( "CO(GT)" )
plt. ylabel( "NO2(GT)" )
plt. legend( )
plt. show( ) 技术支持
从图中可以看到,CO(GT) 这个污染物指标尤为重要。注意,许多异常样本在该特征上的取值大于 6。然而,这并不能解释所有异常:有些样本在两个特征上都很高,另一些则似乎并非由图中的任一特征导致异常。这表明其他污染物指标也对分类有贡献。
Isolation Forest 的优势
这引出了 Isolation Forest 相较其他异常检测方法的主要优势。其一是能够处理 高维数据 。如上所示,样本可以基于所有特征被判为异常,而且该方法在远超四个特征的情况下也能很好工作。另一个关键优势是能发现多种类型的异常。
作为对比,一个简单的异常检测方法是 z 分数。若仅逐个特征使用,它会依据特征值与其均值的偏离程度来判定异常。然而这只覆盖了一类异常。这种方法还假设特征服从正态分布,并要求我们检查数据集中每个特征的 z 分数。
而在 Isolation Forest 中,我们不对特征分布作假设。因此,我们可以发现那些“非因远离均值而异常”的样本。而且,我们还能同时利用所有特征进行分类。在处理大型数据集时,这能够显著简化分析。
另外需要考虑的是,Isolation Forest 具有线性 时间复杂度 ,且属于无监督方法。这使其非常适合在缺乏先验知识的大型数据集中检测异常。这在诸如 欺诈检测 等真实世界应用中尤为常见,因为新型欺诈会不断在海量正常交易中涌现。
Isolation Forest 的局限
Isolation Forest 的一个显著局限是在小数据集上的表现可能不佳。这与隔离树构建的随机过程有关:总有可能出现正常样本路径较短、反之异常样本路径较长的情况。在小数据集中,这种情况更容易发生。换言之,要让 Isolation Forest 发挥作用,我们需要足够的数据来平滑随机性。
另一个局限,如前所述,Isolation Forest 只能识别潜在异常,却无法解释为何某些样本会被视为异常。为此需要进一步分析。而“能识别复杂异常”的优势在此处也会成为挑战——要完整解释一个异常,往往需要大量工作与 领域知识 。
结论
本文探讨了 Isolation Forest,这一在多个领域都适用的强大异常检测工具。我们看到,其通过随机划分来“隔离异常”的独特思路,相较传统方法具有优势:能处理高维数据、识别复杂异常模式,并具备良好的可扩展性。
结合 Python 与 Scikit-learn 的示例也展示了其易用性。
尽管 Isolation Forest 能在缺乏先验知识的情况下有效识别潜在异常,但它也存在局限,尤其是在小数据集场景下的表现,以及无法解释具体样本为何为异常的不足。
我最近收到银行发来的短信:
“我们已暂时冻结您的卡。请确认您是否在 [vendor] 进行了一笔金额为 [amount] 的交易?”
幸好,那只是我在不太合理的时间点进行网购。但这也让我开始思考……银行如何在每天处理的成千上万笔交易中标记可疑交易?
识别欺诈是 机器学习(ML)中一个更广泛问题——异常检测——的一个具体场景。解决这个问题的方法很多,但本文将聚焦于其中最常用的一种:Isolation Forest。
实践中的异常检测
打击欺诈、网络防御、发现离群点、标记故障设备,这些看似不同的问题其实有一个共同点——并没有明确的标签或定义来界定什么是异常。很多时候,异常既稀少又隐蔽,难以从海量的正常数据中识别出来。
一种思路是使用预测式 机器学习来构建异常检测系统。比如,我们可以收集欺诈交易样本,然后利用交易金额、时间、地点、商户类型等特征,训练一个模型来区分正常与异常交易。问题在于,这种方法通常只能发现与历史相似的欺诈类型。
欺诈者会不断改变策略,尝试全新的作案方式,其中很多都出乎意料。因此,常常使用无监督方法来检测异常。它们通过比较所有交易,识别出具有不寻常特征值的样本。重要的是,这意味着我们无需事先为交易打标签。
难以归类的行为并不只存在于欺诈。在制造业中,机器可能会以各种意料之外的方式随时发生故障。在网络安全中,新型攻击不断被发现与修补。在医学中,罕见病症可能隐藏在大量检测结果之中。应用场景不胜枚举,下面就来看看一种无监督方法如何发现异常。
什么是 Isolation Forest?
顾名思义,Isolation Forest是一种基于树的方法。它与随机森林类似,但其基本构件不同。
随机森林使用基于基尼不纯度的分裂来构建决策树,这需要目标变量;而 Isolation Forest 使用“隔离树”。该方法通过集合这些树来为每个样本计算异常分数。
隔离树
在计算异常分数之前,值得先理解这一基本构件如何工作。假设我们试图检测欺诈交易。为简单起见,我们评估 1000 笔交易(样本),并考虑数据集中的两个变量:金额(x1)和交易时间(x2)。
构建一棵隔离树时,我们从根节点包含全部或部分样本开始。在下方的图 1中,您可以看到我们使用了 256 个样本(稍后解释这个数的来由)。
图 1:通过递归随机分裂创建的隔离树。每个节点标注了样本数量。
随后:
请注意,每一步中特征的取值范围都会改变。也就是说,我们使用当前节点样本在该特征上的最小值和最大值。这样的动态调整可确保分裂有意义,且节点不会出现零样本。
图 2能帮助我们直观理解为何该过程会隔离离群点。样本 A、B、和 C看起来都与其他交易不同。
图 2:交易的散点图
B 很可能最先被隔离,因为它可以通过任一特征的一次分裂就与其他交易分开。
A 则可能通过一次 x2 分裂被隔离。
C 可能需要更长时间,因为需要同时利用两个特征来隔离。也就是说,C 的金额与时间各自都显得正常,但“在该时段上金额不正常”。
我们可能会走运,第一次就隔离出这些样本中的一个,但通常需要多试几次。关键在于,平均而言,B 所需的分裂次数少于 A,A 少于 C,且这三点所需的分裂次数都少于其他交易。那么,我们如何量化这种“平均表现”呢?
计算异常分数
隔离异常所需的分裂次数,决定了样本到叶节点的路径长度。当一片隔离树的森林对某些样本持续给出较短的路径长度时,这些样本很可能是异常。我们通过在所有树上聚合路径长度来捕捉这种平均行为,即计算异常分数。
要为样本 x 计算异常分数,先定义几个量:
路径长度 h(x) 是到达该样本叶节点需经过的边(分裂)数。若直接取平均 E[h(x)] 会有问题,因为更大的树(包含更多样本)通常会有更长的路径,即便对异常也是如此。为此,我们需要基于样本量 n 的归一化量:
该值源自二叉搜索树(BST)理论,表示在 BST 中一次不成功搜索的平均路径长度。由于隔离树与 BST 具有等价的结构,我们将 c(n) 视为一个样本(无论是否异常)的期望路径长度。注意,如果设置了树的最大深度,该值需要相应调整。
最后,我们即可计算异常分数 s(x,n):
换言之,当某样本的平均路径长度短于期望平均路径长度时,其异常分数更接近 1。不过,正如我们在实现时会看到,Scikit-learn 包对该分数还进行了进一步调整。
用 Python 和 Scikit-learn 实现 Isolation Forest
我们一步步来,从导入依赖开始。
导入
我们使用常见的 Python 数据处理与可视化包(第 2–5 行),并使用 Scikit-learn 中的 Isolation Forest 实现(第 7 行),最后一个包用于导入数据集(第 8 行)。
数据集
我们直接从UCI 机器学习资源库加载数据。这里使用Air Quality 数据集(CC BY 4.0),其中包含来自意大利某城市的一台传感器的 9,358 条空气质量测量。在该数据集的语境下,异常可以理解为指示污染水平异常偏高的传感器读数。
数据清洗
在应用模型前,我们先做一些清洗操作。先从数据集中选取所有特征(第 2 行),再选取 4 个特征的子集(第 5 行)。这些特征都是空气中不同金属氧化物(即污染物)的测量值。随后我们删除包含缺失值的行(第 8–9 行)。最终剩余 6,941 条样本,下面可以看到特征集的一个快照。
定义训练参数
最后一步是定义用于训练 Isolation Forest 的参数。在这里,我们将使用以下三个值:
n_estimators:集成中的隔离树数量。Isolation Forest 论文中使用了100。研究者通过实验发现,这个值在多种数据集上表现良好。contamination:我们预计为异常的数据点比例。sample_size:训练每棵隔离树所用的样本数。常用值为256,它使我们无需设置最大树深作为停止条件,因为可期望合理的最大树深约为 log(256) = 8。与其他参数不同,contamination 的取值没有那么“硬”的依据。它可以来自以往经验。比如,在之前的分析中,我们发现 1% 的读数指示污染水平较高。它也可能受资源限制所驱动。例如,在分析欺诈交易时,您可能只来得及对最差的 5% 分数的交易做进一步分析。可视化异常分数时,我们会看到它们如何基于该值进行调整。
训练 Isolation Forest
现在可以开始训练模型了。如果您熟悉 Sklearn,下面的流程应该并不陌生。我们用上述参数初始化模型(第 2–5 行),然后在特征集上进行训练(第 6 行)。
计算异常分数
训练好的模型提供了两个有用的函数:
decision_function:以与理论部分类似的方式计算异常分数。predict:基于 contamination 值给出二元标签。在我们的设置中,异常分数最差的 1% 样本被标记为 -1,其余样本标记为 1。我们将这两个函数的输出添加到数据集中(第 2–4 行)。统计异常标签(第 6 行)后可以看到,有 70 个样本被标记为异常。
我们还可以进一步把所有异常分数可视化,代码如下:
橙色标出的 70 个样本拥有最低的异常分数。它们是潜在异常,下一步需要进一步排查。
此时您也许有些困惑:我们不是说“更接近 1 的分数表示更可能异常”吗?
上面散点图中的分数已经过调整。为此,库首先计算一个偏移量,即基于 contamination 值的异常分数分位数。在我们的例子中,偏移量为 0.99 分位。最终分数为 offset - score。这样处理后,小于 0 的分数都提示潜在异常。
进一步分析
借助上面的散点图,我们可以定位潜在异常。但它不会告诉我们“为何”被判为异常。为此需要进一步分析。
例如,下面的代码绘制了所有样本的 CO(GT) 与 NO2(GT) 的散点图,并根据是否被分类为异常进行着色。
从图中可以看到,CO(GT) 这个污染物指标尤为重要。注意,许多异常样本在该特征上的取值大于 6。然而,这并不能解释所有异常:有些样本在两个特征上都很高,另一些则似乎并非由图中的任一特征导致异常。这表明其他污染物指标也对分类有贡献。
Isolation Forest 的优势
这引出了 Isolation Forest 相较其他异常检测方法的主要优势。其一是能够处理高维数据。如上所示,样本可以基于所有特征被判为异常,而且该方法在远超四个特征的情况下也能很好工作。另一个关键优势是能发现多种类型的异常。
作为对比,一个简单的异常检测方法是 z 分数。若仅逐个特征使用,它会依据特征值与其均值的偏离程度来判定异常。然而这只覆盖了一类异常。这种方法还假设特征服从正态分布,并要求我们检查数据集中每个特征的 z 分数。
而在 Isolation Forest 中,我们不对特征分布作假设。因此,我们可以发现那些“非因远离均值而异常”的样本。而且,我们还能同时利用所有特征进行分类。在处理大型数据集时,这能够显著简化分析。
另外需要考虑的是,Isolation Forest 具有线性时间复杂度,且属于无监督方法。这使其非常适合在缺乏先验知识的大型数据集中检测异常。这在诸如欺诈检测等真实世界应用中尤为常见,因为新型欺诈会不断在海量正常交易中涌现。
Isolation Forest 的局限
Isolation Forest 的一个显著局限是在小数据集上的表现可能不佳。这与隔离树构建的随机过程有关:总有可能出现正常样本路径较短、反之异常样本路径较长的情况。在小数据集中,这种情况更容易发生。换言之,要让 Isolation Forest 发挥作用,我们需要足够的数据来平滑随机性。
另一个局限,如前所述,Isolation Forest 只能识别潜在异常,却无法解释为何某些样本会被视为异常。为此需要进一步分析。而“能识别复杂异常”的优势在此处也会成为挑战——要完整解释一个异常,往往需要大量工作与领域知识。
结论
本文探讨了 Isolation Forest,这一在多个领域都适用的强大异常检测工具。我们看到,其通过随机划分来“隔离异常”的独特思路,相较传统方法具有优势:能处理高维数据、识别复杂异常模式,并具备良好的可扩展性。
结合 Python 与 Scikit-learn 的示例也展示了其易用性。
尽管 Isolation Forest 能在缺乏先验知识的情况下有效识别潜在异常,但它也存在局限,尤其是在小数据集场景下的表现,以及无法解释具体样本为何为异常的不足。