异常检测算法 – 孤立森林(Isolation Forest)剖析
《异常检测——从经典算法到深度学习》1 基于孤立森林的异常检测算法
论文地址:Isolation Forest
孤立森林(isolation Forest)是一种高效的异常检测算法,它和随机森林类似,但每次选择划分属性和划分点(值)时都是随机的,而不是根据信息增益或基尼指数来选择。
Forest算法是由南京大学的周志华和澳大利亚莫纳什大学的Fei Tony Liu,Kai Ming Ting等人共同提出,用于挖掘数据,它是适用于连续数据(Continuous numerical data)的异常检测,将异常定义为“容易被孤立的离群点(more likely to be separated)”——可以理解为分布稀疏且离密度高的群体较远的点。用统计学来解释,在数据空间里面,分布稀疏的区域表示数据发生在此区域的概率很低,因此可以认为落在这些区域里的数据是异常的。通常用于网络安全中的攻击检测和流量异常等分析,金融机构则用于挖掘出欺诈行为。对于找出的异常数据,然后要么直接清除异常数据,如数据清理中的去噪数据,要么深入分析异常数据,比如分析攻击,欺诈的行为特征。
孤立森林(Isolation Forest) 又名孤立森林,是一种从异常点出发,通过指定规则进行划分,根据划分次数进行判断的异常检测方法。由周志华教授等人提出,对原论文感兴趣的不妨下载看看。下载地址
核心思路:循环分割(孤立),越容易被分割的判定为异常可能性越大。因为异常具有“少而不同”这一特点,所以异常数据比正常数据更容易被孤立。利用孤立森林,完成分割后,在最终形成的树中,异常将更加接近于根的位置。
孤立树: 设 T T T 为孤立树的一个节点, T T T 是没有子节点的外节点,或者一个具有测试条件的内节点,它有两个子节点 ( T l , T r ) (T_l,T_r) (Tl,Tr))。测试条件由属性 q q q 和分割值 p p p 组成,根据测试条件 q < p q q<p
路径长:样本点 x x x 的路径长度 h ( x ) h(x) h(x) 为从iTree的根节点到叶子节点所经过的边的数量。
异常得分:给定一个包含 n n n 个样本的数据集,树的平均路径长度为
c ( n ) = 2 H ( n − 1 ) − 2 ( n − 1 ) n c(n)=2H(n-1)-\frac{2(n-1)}{n} c(n)=2H(n−1)−n2(n−1)
其中 H ( i ) H(i) H(i) 为调和数,可以被估计为 ln ( i ) + 0.5772156649 \ln(i)+0.5772156649 ln(i)+0.5772156649。 c ( n ) c(n) c(n) 为给定样本 n n n 时路径长度的平均值,用来标准化样本 x x x 的路径长度 h ( x ) h(x) h(x)。
样本 x x x 的异常得分定义为:
s ( x , n ) = 2 − E ( h ( x ) ) / c ( n ) s(x,n)=2^{-{{E(h(x))}/{c(n)}}} s(x,n)=2−E(h(x))/c(n)
其中 E ( h ( x ) E(h(x) E(h(x) 为样本 x x x 在一批孤立树中的路径长度的期望。
Forest 属于Non-parametric
和unsupervised
的方法,即不用定义数学模型也不需要有标记的训练。对于如何查找哪些点是否容易被孤立(isolated),iForest
使用了一套非常高效的策略。假设我们用一个随机超平面来切割(split)数据空间(data space),切一次可以生成两个子空间(详细拿刀切蛋糕一分为二)。之后我们再继续用一个随机超平面来切割每个子空间,循环下去,直到每个子空间里面只有一个数据点为止。直观上来讲,我们可以发现那些密度很高的簇是被切分很多次才会停止切割,但是那些密度很低的点很容易很早就停到一个子空间看了。
iForest
算法得益于随机森林的思想,与随机森林由大量决策树组成一样,iForest
森林也由大量的二叉树组成,iForest
中的树叫 isolation tree
,简称 iTree
,iTree
树和决策树不太一样,其构建过程也比决策树简单,是一个完全随机的过程。
假设数据集有 N 条数据,构建一颗 ITree时,从 N条数据中均匀抽样(一般是无放回抽样)出 n 个样本出来,作为这棵树的训练样本。在样本中,随机选出一个特征,并在这个特征的所有值范围内(最小值和最大值之间)随机选一个值,对样本进行二叉划分,将样本中小于该值的划分到节点的左边,大于等于该值的划分到节点的右边。由此得到一个分裂条件和左右两边的数据集,然后分别在左右两边的数据集上重复上面的过程,直到数据集只有一条记录或者达到了树的限定高度。
由于异常数据较小且特征值和正常数据差别很大。因此,构建 iTree的时候,异常数据离根更近,而正常数据离根更远。一颗ITree的结果往往不可信,iForest算法通过多次抽样,构建多颗二叉树。最后整合所有树的结果,并取平均深度作为最终的输出深度,由此计算数据点的异常分支。
孤立树算法
iTree(X, e, L)
Inputs: X->输入数据,e->当前生成树的高度,L->生成树的最大高度
Output: 生成树(即孤立树iTree)
(1) if e >= L or |X| <= 1 then
达到终止条件
(2) return exNode{Size<-|X|}
(3) else
(4) 令Q为X中的一组属性
(5) 随机选择Q中的一个属性q
(6) 对于属性q,随机在[min,max]中选一个划分点p
(7) Xl <- fiter(X,q<p)
(8) Xr <- fiter(X,q>=p)
(9) return inNode{ 把节点添加到树
Left<-iTree(Xl,e+1,L),
Right<-iTree(Xr,e+1,L),
SplitAtt<-q,
SplitValue<-p
}
(10) end if
孤立森林算法
iForest(X, t, w)
Inputs: X->输入数据,t->树的个数,w->子采样大小
Output: t颗 iTree
(1) 初始化 iTree
(2) 设置高度限制 L = ceiling(log2(w))
(3) for i in (t):
(4) X0 <- sample(X,w)
(5) Forest <- Forest ∪ iTree(X0,0,L)
(6) end for
(7) return Forest
路径长算法
PathLength(x,T,e)
Inputs: x->一个实例,T->一棵iTree,e->当前路径长度,初始化为0
Output: x的路径长
(1) if T是一个外节点 then
(2) return e + c(T.size) {c(.)已经定义过}
(3) end if
(4) a <- T.splitAtt
(5) if x_a < T.splitvalue then
(6) return PathLength(x,T.left,e+1)
(7) else
(8) return PathLength(x,T.right,e+1)
(9) end if
n_estimators:构建多少个itree,int,optional (default=100)指定该森林中生成的随机树数量
max_samples:采样数,自动是256,int,optional(default='auto)用来训练随机树的样本数量,即子采样的大小:
contamination:c(n)默认是0.1,float in (0, 0.5),optional(default=0.1),取值范围为(0, 0.5),表示异常数据占给定的数据集的比例,就是数据集中污染的数量,定义该参数值的作用是在决策函数中定义阈值。如果设置为“auto”,则决策函数的阈值就和论文一样,在版本0.20中有变换:默认值从0.1变为0.22的auto。
max_features:最大特征数,默认为1,int or float,optional,指定从总样本X中抽取来训练每棵树iTree 的属性的数量,默认只使用一个属性
bootstrap:boolean,optional(default = False),构建Tree时,下次是否替换采样,为True为替换,则各个树可放回地对训练数据进行采样;为False为不替换,即执行不放回的采样
n_jobs:int or None, optional (default = None), 在运行 fit() 和 predict() 函数时并行运行的作业数量。除了在 joblib.parallel_backend 上下文的情况下,None表示为1,设置为 -1 则表示使用所有可以使用的处理器
behaviour:str,default=‘old’,决策函数 decision_function 的行为,可以是“old”和‘new’。设置为 behavior='new’将会让 decision_function 去迎合其它异常检测算法的API,这在未来将会设置为默认值。
random_state:int,RandomState instance or None,optional(default=None)
verbose:int,optional(default=0)控制树构建过程的冗长性
warm_start:bool,optional(default=False),当设置为TRUE时,重用上一次调用的结果去 fit,添加更多的树到上一次的森林1集合中;否则就 fit一整个新的森林
ROC指标(Receiver Operating Characteristic)是一种用于评估二分类模型性能的指标。ROC曲线是以真正率(True Positive Rate,TPR)为纵轴,假正率(False Positive Rate,FPR)为横轴的曲线。
在二分类模型中,真正率(TPR)是指所有实际为正例的样本中,被正确地判定为正例的样本占比;假正率(FPR)是指所有实际为负例的样本中,被错误地判定为正例的样本占比。
ROC曲线越接近左上角,表示模型的性能越好。通常情况下,将ROC曲线下的面积称为AUC(Area Under Curve),AUC的值越大,表示模型的性能越好。
除了AUC之外,还可以根据ROC曲线的形状来评估模型的性能。ROC曲线越凸向左上角,表示模型的性能越好;如果ROC曲线几乎是一条直线,则表示模型性能较差。
ROC指标通常用于评估样本不平衡的二分类问题,例如欺诈检测、疾病诊断等领域。
ROC指标的取值范围在0到1之间,其中,AUC的取值范围也在0到1之间。当AUC的值为1时,表示模型的性能完美,即所有正例都被正确地分类为正例,所有负例都被正确地分类为负例;当AUC的值为0.5时,表示模型的性能等同于随机猜测,即模型的预测结果没有任何意义;当AUC的值小于0.5时,表示模型的性能比随机猜测还要差。
ROC曲线上的点的坐标也在0到1之间,表示模型在不同的阈值下的真正率和假正率。通常情况下,ROC曲线越靠近左上角,表示模型的性能越好,而ROC曲线越靠近45度对角线,则表示模型性能较差。
在机器学习和信息检索中,Precision、Recall和Accuracy是三个常用的评估指标,它们分别衡量了模型的不同方面的性能。
Precision(精确率):用于衡量分类器预测为正类的样本中有多少个真正是正类的比例。Precision的计算公式为:
P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP+FP} Precision=TP+FPTP
其中,TP表示真正例(True Positive),FP表示假正例(False Positive)。
Recall(召回率):用于衡量正类样本中有多少个被分类器正确地预测出来了。Recall的计算公式为:
R e c a l l = T P T P + F N Recall = \frac{TP}{TP+FN} Recall=TP+FNTP
其中,TP表示真正例(True Positive),FN表示假负例(False Negative)。
Accuracy(准确率):用于衡量分类器正确预测的样本数占总样本数的比例。Accuracy的计算公式为:
A c c u r a c y = T P + T N T P + T N + F P + F N Accuracy = \frac{TP+TN}{TP+TN+FP+FN} Accuracy=TP+TN+FP+FNTP+TN
其中,TP表示真正例(True Positive),TN表示真负例(True Negative),FP表示假正例(False Positive),FN表示假负例(False Negative)。
从上述公式可以看出,Precision关注的是分类器预测的正类样本中有多少是真正的正类,而Recall关注的是真正的正类样本中有多少被分类器正确地预测出来了。Accuracy则是对整个分类器的性能进行综合考虑。需要注意的是,这三个指标并不是绝对好坏的评判标准,而是需要根据具体问题和任务来选择合适的指标。例如,在不平衡的数据集中,Precision可能更重要,而在一些强烈关注误报率的应用场景中,Precision也更为关键。
precision_n_scores是一个评估信息检索系统的指标,用于衡量系统返回的前n个结果的precision。
from pyod.models.iforest import IForest
import numpy as np
import pandas as pd
from pyod.utils.data import evaluate_print
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt
import pickle
data_origin_train = list((pd.read_csv("./node_metric.csv")).groupby('node'))[0][1]
data_origin_test = list((pd.read_csv("./node_metric3.csv")).groupby('node'))[0][1]
data_test = data_origin_test.drop(['node','metric','label'],axis=1)
data_train = data_origin_train.drop(['node','metric','label'],axis=1)
y_test = data_origin_test['label'].values
y_train = data_origin_train['label'].values
X_train =data_train.values
X_test = data_test.values
# 孤立森林
clf_name = 'IForest'
clf = IForest(n_estimators=150, max_samples=256, contamination=0.008, random_state=42)
clf.fit(X_train)
y_train_pred = clf.labels_ # 0:正常 1:异常
y_train_scores = clf.decision_scores_ # 原始的异常分数
print("\nOn Training Data:")
print(classification_report(y_train, y_train_pred))
y_test_pred = clf.predict(X_test) # 0:正常 1:异常
y_test_scores = clf.decision_function(X_test) # 异常分数
print("\nOn Test Data:")
print(classification_report(y_test, y_test_pred))
On Training Data:
precision recall f1-score support
0.0 1.00 1.00 1.00 2857
1.0 1.00 1.00 1.00 24
accuracy 1.00 2881
macro avg 1.00 1.00 1.00 2881
weighted avg 1.00 1.00 1.00 2881
On Test Data:
precision recall f1-score support
0 1.00 0.98 0.99 342
1 0.73 1.00 0.84 19
accuracy 0.98 361
macro avg 0.87 0.99 0.92 361
weighted avg 0.99 0.98 0.98 361