異常檢測介紹(4)
基於相似度的方法
概述
“異常” 通常是一個主觀的判斷,什麼樣的資料被認為是“異常"的,需要結合業務背景和環境來具體分析確定。
實際上,資料通常嵌入在大量的噪聲中,而我們所說的“異常值”通常指具有特定業務意義的那一類特殊的異常值。噪聲可以視作特性較弱的異常值,沒有被分析的價值。噪聲和異常之間、正常資料和噪聲之間的邊界都是模糊的。異常值通常具有更高的離群程度分數值,同時也更具有可解釋性。
在普通的資料處理中,我們常常需要保留正常資料,而對噪聲和異常值的特性則基本忽略。但在異常檢測中,我們弱化了“噪聲”和“正常資料”之間的區別,專注於那些具有有價值特性的異常值。在基於相似度的方法中,主要思想是異常點的表示與正常點不同。
基於距離的度量
基於距離的方法是一種常見的適用於各種資料域的異常檢測演算法,它基於最近鄰距離來定義異常值。此類方法不僅適用於多維數值資料,在其他許多領域,例如分類資料,文字資料,時間序列資料和序列資料等方面也有廣泛的應用。
基於距離的異常檢測有這樣一個前提假設,即異常點的 k k k近鄰距離要遠大於正常點。解決問題的最簡單方法是使用巢狀迴圈。第一層迴圈遍歷每個資料,第二層迴圈進行異常判斷,需要計算當前點與其他點的距離,一旦已識別出多於 k k k個數據點與當前點的距離在 D D D之內,則將該點自動標記為非異常值。這樣計算的時間複雜度為 O ( N 2 ) O(N^{2}) O(N2),當資料量比較大時,這樣計算是極不划算的。因此,需要修剪方法以加快距離計算。
基於單元的方法
在基於單元格的技術中,資料空間被劃分為單元格,單元格的寬度是閾值 D D D和資料維數的函式。具體地說,每個維度被劃分成寬度最多為 D 2 ⋅ d \frac{D}{2\cdot \sqrt{d}} 2⋅d D單元格。在給定的單元以及相鄰的單元中存在的資料點滿足某些特性,這些特性可以讓資料被更有效的處理。
以二維情況為例,此時網格間的距離為
D
2
⋅
d
\frac{D}{2\cdot \sqrt{d}}
2⋅d
D需要記住的一點是,網格單元的數量基於資料空間的分割槽,並且與資料點的數量無關。這是決定該方法在低維資料上的效率的重要因素,在這種情況下,網格單元的數量可能不多。另一方面,此方法不適用於更高維度的資料。對於給定的單元格,其
L
1
L_{1}
- 單元格中兩點之間的距離最多為 D / 2 D/2 D/2。
- 一個點與 L 1 L_{1} L1鄰接點之間的距離最大為 D D D。
- 一個點與它的 L r L_{r} Lr鄰居(其中 r > 2 r>2 r>2)中的一個點之間的距離至少為D。
唯一無法直接得出結論的是 L 2 L_{2} L2中的單元格。這表示特定單元中資料點的不確定性區域。對於這些情況,需要明確執行距離計算。同時,可以定義許多規則,以便立即將部分資料點確定為異常值或非異常值。規則如下:
- 如果一個單元格中包含超過 k k k個數據點及其 L 1 L_{1} L1鄰居,那麼這些資料點都不是異常值。
- 如果單元 A A A及其相鄰 L 1 L_{1} L1和 L 2 L_{2} L2中包含少於 k k k個數據點,則單元 A A A中的所有點都是異常值。
此過程的第一步是將部分資料點直接標記為非異常值(如果由於第一個規則而導致它們的單元格包含 k k k個點以上)。此外,此類單元格的所有相鄰單元格僅包含非異常值。為了充分利用第一條規則的修剪能力,確定每個單元格及其 L 1 L_{1} L1鄰居中點的總和。如果總數大於 k k k,則所有這些點也都標記為非離群值。
接下來,利用第二條規則的修剪能力。對於包含至少一個數據點的每個單元格 A A A,計算其中的點數及其 L 1 L_{1} L1和 L 2 L_{2} L2鄰居的總和。如果該數字不超過 k k k,則將單元格 A A A中的所有點標記為離群值。此時,許多單元可能被標記為異常值或非異常值。
對於此時仍未標記為異常值或非異常值的單元格中的資料點需要明確計算其 k k k最近鄰距離。即使對於這樣的資料點,通過使用單元格結構也可以更快地計算出 k k k個最近鄰的距離。考慮到目前為止尚未被標記為異常值或非異常值的單元格 A A A。這樣的單元可能同時包含異常值和非異常值。單元格 A A A中資料點的不確定性主要存在於該單元格的 L 2 L_{2} L2鄰居中的點集。無法通過規則知道 A A A的 L 2 L_{2} L2鄰居中的點是否在閾值距離 D D D內,為了確定單元 A A A中資料點與其 L 2 L_{2} L2鄰居中的點集在閾值距離 D D D內的點數,需要進行顯式距離計算。對於那些在 L 1 L_{1} L1和 L 2 L_{2} L2中不超過 k k k個且距離小於 D D D的資料點,則宣告為異常值。需要注意,僅需要對單元 A A A中的點到單元 A A A的 L 2 L_{2} L2鄰居中的點執行顯式距離計算。這是因為已知 L 1 L_{1} L1鄰居中的所有點到 A A A中任何點的距離都小於 D D D,並且已知 L r L_{r} Lr中 ( r > 2 ) (r>2) (r>2)的所有點與 A A A上任何點的距離至少為 D D D。因此,可以在距離計算中實現額外的節省。
基於索引的方法
對於一個給定資料集,基於索引的方法利用多維索引結構(如 R R R樹、 k − d k-d k−d樹)來搜尋每個資料物件 A A A在半徑 D D D範圍內的相鄰點。設 M M M是一個異常值在其 D − D- D−鄰域內允許含有物件的最多個數,若發現某個資料物件 A A A的 D − D- D−鄰域內出現 M + 1 M+1 M+1甚至更多個相鄰點,則判定物件 A A A不是異常值。該演算法時間複雜度在最壞情況下為 O ( k N 2 ) O(kN^{2}) O(kN2),其中 k k k是資料集維數, N N N是資料集包含物件的個數。該演算法在資料集的維數增加時具有較好的擴充套件性,但是時間複雜度的估算僅考慮了搜尋時間,而構造索引的任務本身就需要密集複雜的計算量。
基於密度的度量
基於密度的演算法主要有區域性離群因子(Local Outlier Factor,LOF,以及LOCl、CLOF等基於LOF的改進演算法。下面我們以LOF為例來進行詳細的介紹和實踐。
基於距離的檢測適用於各個叢集的密度較為均勻的情況。在下圖中,離群點 B B B容易被檢出,而若要檢測出較為接近叢集的離群點 A A A,則可能會將一些叢集邊緣的點當作離群點丟棄。而LOF等基於密度的演算法則可以較好地適應密度不同的叢集情況。
那麼,這個基於密度的度量值是怎麼得來的呢?還是要從距離的計算開始。類似k近鄰的思路,首先我們也需要來定義一個“k-距離”。
k-距離(k-distance§)
對於資料集 D D D中的某一個物件 p p p,與其距離最近的 k k k個相鄰點的最遠距離表示為 k − d i s t a n c e ( p ) k-distance(p) k−distance(p),定義為給定點 p p p和資料集 D D D中物件 o o o之間的距離 d ( p , o ) d(p,o) d(p,o),滿足:
- 在集合 D D D中至少有 k k k個點 o ′ o' o′,其中 o ′ ∈ D { p } o'\in D\left \{ p \right \} o′∈D{p},滿足 d ( p , o ′ ) ⩽ d ( p , o ) d(p,o')\leqslant d(p,o) d(p,o′)⩽d(p,o)
- 在集合 D D D中最多有 k − 1 k-1 k−1個點 o ′ o' o′,其中 o ′ ∈ D { p } o'\in D\left \{ p \right \} o′∈D{p},滿足 d ( p , o ′ ) < d ( p , o ) d(p,o')< d(p,o) d(p,o′)<d(p,o)
直觀一些理解,就是以物件 o o o為中心,對資料集 D D D中的所有點到 o o o的距離進行排序,距離物件 o o o第 k k k近的點 p p p與 o o o之間的距離就是 k − k- k−距離。
總之,點 P P P是距離 O O O最近的第 k k k個點。
k-領域(k-distance neighborhood)
由
k
−
k-
k−距離,我們擴充套件到一個點的集合一一到物件
o
o
o的距離小於等於
k
−
k-
k−距離的所有點的集合,我們稱之為
k
−
k-
k−鄰域:
N
k
−
d
i
s
t
a
n
c
e
(
p
)
(
P
)
=
{
q
∈
D
∖
{
p
}
∣
d
(
p
,
q
)
⩽
k
−
d
i
s
t
a
n
c
e
(
p
)
}
N_{k-distance(p)}(P)=\left \{ q\in D\setminus \left \{ p \right \} |d(p,q)\leqslant k-distance(p) \right \}
Nk−distance(p)(P)={q∈D∖{p}∣d(p,q)⩽k−distance(p)}
在二維平面上展示出來的話,物件
o
o
o的
k
−
k-
k−鄰域實際上就是以物件
o
o
o為圓心、
k
−
k-
k−距離為半徑圍成的圓形區域。就是說,
k
−
k-
k−鄰域已經從“距離”這個概念延伸到“空間”了,
可達距離(reachability distance)
有了鄰域的概念,我們可以按照到物件 o o o的距離遠近,將資料集 D D D內的點按照到 o o o的距離分為兩類:
- 若 p i p_{i} pi在物件 o o o的 k − k- k−鄰域內,則可達距離就是給定點 p p p關於物件 o o o的 k − k- k−距離;
- 若 p i p_{i} pi在物件 o o o的 k − k- k−鄰域外,則可達距離就是給定點 p p p關於物件 o o o的實際距離。
給定點
p
p
p關於物件
o
o
o的可達距離用數學公式可以表示為:
r
e
a
c
h
−
d
i
s
t
k
(
p
,
o
)
=
m
a
x
{
k
−
d
i
s
t
a
n
c
e
(
o
)
,
d
(
p
,
o
)
}
reach-dist_{k}(p,o)=max\left \{ k-distance(o),d(p,o) \right \}
reach−distk(p,o)=max{k−distance(o),d(p,o)}
這樣的分類處理可以簡化後續的計算,同時讓得到的數值區分度更高。
注:點 P P P到點 O O O的第 k k k可達距離至少是點 O O O的第 k k k距離。距離 O O O點最近的 k k k個點,它們到 O O O的可達距離被認為是相等的,且都等於 d k ( O ) d_{k}(O) dk(O)。
區域性可達密度(local reachability density):
我們可以將“密度”直觀地理解為點的聚集程度,就是說,點與點之間距離越短,則密度越大。在這裡,我們使用資料集 D D D中給定點 p p p與物件 o o o的 k − k- k−鄰域內所有點的可達距離平均值的倒數(注意,不是導數)來定義區域性可達密度。
給定點
p
p
p的區域性可達密度計算公式為:
l
r
d
M
i
n
P
t
s
(
p
)
=
1
/
(
∑
o
∈
N
M
i
n
P
t
s
(
p
)
r
e
a
c
h
−
d
i
s
t
M
i
n
P
t
s
(
p
,
o
)
∣
N
M
i
n
P
t
s
(
p
)
∣
)
lrd_{MinPts}(p)=1/(\frac{\sum _{o\in N_{MinPts}(p)}reach-dist_{MinPts}(p,o)}{\left | N_{MinPts}(p) \right |})
lrdMinPts(p)=1/(∣NMinPts(p)∣∑o∈NMinPts(p)reach−distMinPts(p,o))
公式表示點
p
p
p的第
k
k
k領域內所有點到
p
p
p的平均可達距離。如果
p
p
p和周圍領域點是同一簇,那麼可達距離越可能為較小的
d
k
(
o
)
d_{k}(o)
dk(o),導致可達距離之和越小,區域性可達密度越大。如果
o
o
o和周圍領域點較遠,那麼可達距離可能會取較大值
d
(
o
,
p
)
d(o,p)
d(o,p),導致可達距離之和越大,區域性可達密度越小。
由公式可以看出,這裡是對給定點 p p p進行度量,計算其鄰域內的所有物件 o o o到給定點 p p p的可達距離平均值。給定點 p p p的區域性可達密度越高,越可能與其鄰域內的點屬於同一簇;密度越低,越可能是離群點。
區域性異常因子
L O F M i n P t s ( p ) = ∑ o ∈ N M i n P t s ( p ) l r d M i n P t s ( o ) l r d M i n P t s ( p ) ∣ N M i n P t s ( p ) ∣ L O F_{MinPts}(p)=\frac{\sum_{o \in N_{MinPts}(p)} \frac{lrd_{MinPts}(\mathrm{o})}{lrd_{MinPts}(p)}}{\left|N_{MinPts}(p)\right|} LOFMinPts(p)=∣NMinPts(p)∣∑o∈NMinPts(p)lrdMinPts(p)lrdMinPts(o)
表示點p的鄰域 N k ( p ) N_k(p) Nk(p)內其他點的區域性可達密度與點 p p p的區域性可達密度之比的平均數。如果這個比值越接近1,說明 o o o的鄰域點密度差不多, o o o可能和鄰域同屬一簇;如果這個比值小於1,說明 o o o的密度高於其鄰域點密度, o o o為密集點;如果這個比值大於1,說明 o o o的密度小於其鄰域點密度, o o o可能是異常點。這裡轉而度量 p p p,而不是 o o o
最終得出的LOF數值,就是我們所需要的離群點分數。在sklearn中有LocalOutlierFactor庫,可以直接呼叫。下面來直觀感受一下LOF的影象呈現效果。
LocalOutlierFactor庫可以用於對單個數據集進行無監督的離群檢測,也可以基於已有的正常資料集對新資料集進行新穎性檢測。在這裡我們進行單個數據集的無監督離群檢測。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
首先構造一個含有叢集和離群點的資料集。該資料集包含兩個密度不同的正態分佈叢集和一些離群點。但是,這裡我們手工對資料點的標註其實是不準確的,可能有一些隨機點會散落在叢集內部,而一些叢集點由於正態分佈的特性,會與其餘點的距離相對遠一些。在這裡我們無法進行區分,所以按照生成方式統一將它們標記為“叢集內部的點”或者“離群點”。
np.random.seed(61)
# 構造兩個資料點叢集
X_inliers1 = 0.2 * np.random.randn(100, 2)
X_inliers2 = 0.5 * np.random.randn(100, 2)
X_inliers = np.r_[X_inliers1 + 2, X_inliers2 - 2]
# 構造一些離群的點
X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))
# 拼成訓練集
X = np.r_[X_inliers, X_outliers]
n_outliers = len(X_outliers)
ground_truth = np.ones(len(X), dtype=int)
# 打標籤,群內點構造離群值為1,離群點構造離群值為-1
ground_truth[-n_outliers:] = -1
plt.title('構造資料集 (LOF)')
plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='叢集點')
plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='離群點')
plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))
legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()
然後使用LocalOutlierFactor庫對構造資料集進行訓練,得到訓練的標籤和訓練分數(區域性離群值)。為了便於圖形化展示,這裡對訓練分數進行了一些轉換。
# 訓練模型(找出每個資料的實際離群值)
clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)
# 對單個數據集進行無監督檢測時,以1和-1分別表示非離群點與離群點
y_pred = clf.fit_predict(X)
# 找出構造離群值與實際離群值不同的點
n_errors = y_pred != ground_truth
X_pred = np.c_[X,n_errors]
X_scores = clf.negative_outlier_factor_
# 實際離群值有正有負,轉化為正數並保留其差異性(不是直接取絕對值)
X_scores_nor = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())
X_pred = np.c_[X_pred,X_scores_nor]
X_pred = pd.DataFrame(X_pred,columns=['x','y','pred','scores'])
X_pred_same = X_pred[X_pred['pred'] == False]
X_pred_different = X_pred[X_pred['pred'] == True]
# 直觀地看一看資料
X_pred
將訓練分數(離群程度)用圓直觀地表示出來,並對構造標籤與訓練標籤不一致的資料用不同顏色的圓進行標註。
plt.title('區域性離群因子檢測 (LOF)')
plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='叢集點')
plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='離群點')
# 以標準化之後的區域性離群值為半徑畫圓,以圓的大小直觀表示出每個資料點的離群程度
plt.scatter(X_pred_same.values[:,0], X_pred_same.values[:, 1],
s=1000 * X_pred_same.values[:, 3], edgecolors='c',
facecolors='none', label='標籤一致')
plt.scatter(X_pred_different.values[:, 0], X_pred_different.values[:, 1],
s=1000 * X_pred_different.values[:, 3], edgecolors='violet',
facecolors='none', label='標籤不同')
plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))
legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()
可以看出,模型成功區分出了大部分的離群點,一些因為隨機原因散落在叢集內部的“離群點”也被識別為叢集內部的點,但是一些與叢集略為分散的“叢集點”則被識別為離群點。
同時可以看出,模型對於不同密度的叢集有著較好的區分度,對於低密度叢集與高密度叢集使用了不同的密度閾值來區分是否離群點。
因此,我們從直觀上可以得到一個印象,即基於LOF模型的離群點識別在某些情況下,可能比基於某種統計學分佈規則的識別更加符合實際情況。
參考資料
- LOF: Identifying Density-Based Local Outliers
- https://scikit-learn.org/stable/auto_examples/neighbors/plot_lof_outlier_detection.html?highlight=lof
- https://zhuanlan.zhihu.com/p/37753692