數據的異常檢測是一個難題, 面臨許多挑戰, 其中包括:
- 定義一個正常表現的範圍是比較困難的, 異常值和正常值有時候邊界並不是特別明顯
- 某些惡意行爲會僞裝成正常值, 難以發現
- 大多數領域的正常行爲只能在一段時間內有效, 對於未來的普適性並不是很高
- 對於異常的概念會由於應用的不同而不同
- 缺少帶有標記的數據
- 數據的噪聲可能有較大的影響
分析異常數據有多種方案, 包括:
- 基於分類的手段
- 基於最近鄰算法
- 基於聚類
- 基於統計方法
- 基於信息理論
- 基於特徵理論
我們這次着重介紹的是時序數據的異常檢測, 我們來討論討論LOF方法, 並且給出相應的代碼實現
1. LOF方法簡介
該方法源自於論文Conformalized density- and distance-based anomaly detection in time-series data
LOF方法也就是Local Outlier Factor的縮寫
首先我們需要引入一些符號:
k: 類似於KNN中的k, 代表第k個相鄰的
dist(a,b): 表示a和b之間的距離, 可以是幾何距離, 也可以是曼哈頓距離等
LOF方法使用對於第k個鄰居的反向平均距離(Inverted average distance)來進行一個密度的測量, 我們記作loc_dens
同時我們給出其中的reach_dist
其中NN_k(x)是x的第k個近鄰, 而reach_dist是爲了當x和o彼此靠近的時候減少統計波動
我們計算出密度之後, 就要利用該密度和其他近鄰的點進行比較, 進而我們就可以計算出異常程度的分數, 記爲LOF, 按如下方法進行計算:
如果LOF越大則說明異常程度越高
2. LOF-ICAD方法
基於LOF方法, 論文給出了一種特徵抽取的方法, 進一步提高了精度
這裏直接給出算法的細節:
輸入:
- 窗口長度L
- 合適的訓練集合的大小T
- 修正集合的大小C
- 時間序列(x1, … , x(T+C+L-1))
- 測試的值x(T+C+L)
- 密度測量NCM
輸出(異常分數p, 從0到1):
步驟:
1. 將時間序列(x1, … , x(T+C+L-1))映射到矩陣X, 其中矩陣X是L x (T+C)的矩陣
舉個例子, 比如對於時間序列(1, 2, 3, 4, 5, 6), T=2, C=2, L=3
則生成X矩陣爲
1, 2, 3, 4
2, 3, 4, 5
3, 4, 5, 6
2. 將矩陣X劃分成訓練矩陣X(T)(L x T大小)以及修正矩陣X(C)(L x C大小)
如上述例子, X(T)爲:
1, 2
2, 3
3, 4
3. 計算NCM值(α1, …, αC)對於修正矩陣X(C)的每一行(應該會有L行)
具體的NCM值的計算也就是LOF的計算方式得到
4. 對序列最後的x(T+C+L-1)計算NCM值
5. 計算異常程度分數p
如果p的分數特別高, 則異常程度相應地越高
3. Java實現
首先給出LOF算法的實現
package LOF;
import java.util.ArrayList;
/**
* Local Outlier Factor
*
* @author mezereon E-mail:[email protected]
* @since 18-4-12
*/
public class LOF {
private int k;
public LOF(int k) {
this.k = k;
}
/**
* 返回異常程度的分數, 越接近1則越異常
*
* @param knn 輸入一個時序數據生成的旋轉矩陣
* @param x 輸入測試的序列
*/
public double getLOF(double[][] knn, double[] x) {
double sum = 0;
for (double[] o : knn) {
sum += getLocDens(knn, o) / getLocDens(knn, x);
}
return sum / k;
}
/**
* 獲取local density
*
* @param knn 輸入一個時序數據生成的旋轉矩陣
* @param x 輸入測試的序列
*/
public double getLocDens(double[][] knn, double[] x) {
double[] nnk = findKthPoint(knn, x);
double sum = 0;
for (double[] o : knn) {
sum += reachDist(o, x, nnk);
}
return sum / k;
}
/**
* 找到第k個相似的序列
*
* @param knn 輸入一個時序數據生成的旋轉矩陣
* @param x 輸入測試的序列
*/
public double[] findKthPoint(double[][] knn, double[] x) {
ArrayList list = new ArrayList();
for (int i = 0; i < knn.length; i++) {
list.add(knn[i]);
}
int index = 0;
double minDist = dist(knn[0], x);
for (int i = 0; i < k; i++) {
index = 0;
minDist = dist((double[]) list.get(0), x);
for (int j = 0; j < list.size(); j++) {
if (minDist > dist((double[]) list.get(j), x)) {
minDist = dist((double[]) list.get(j), x);
index = j;
}
}
if (i != k - 1) {
list.remove(index);
}
}
return (double[]) list.get(index);
}
/**
* 返回與相似序列的距離比較之下的較大值
*
* @param o 輸入序列
* @param x 測試序列
* @param nnk 第k相似的序列
*/
public double reachDist(double[] o, double[] x, double[] nnk) {
return Math.max(dist(o, x), dist(nnk, x));
}
/**
* 返回序列之間的歐幾里德距離
*
* @param nnk 第k相似的序列
* @param x 測試序列
*/
private double dist(double[] nnk, double[] x) {
double sum = 0;
for (int i = 0; i < nnk.length; i++) {
sum += (nnk[i] - x[i]) * (nnk[i] - x[i]);
}
return Math.sqrt(sum);
}
public int getK() {
return k;
}
public void setK(int k) {
this.k = k;
}
}
給出LOF-ICAD的實現
package LOF;
import Tool.DetectTool;
import Util.MatrixUtil;
/**
* @author mezereon E-mail:[email protected]
* @since 18-4-26
*/
public class LOFDetectTool implements DetectTool {
private int T;// 時間序列用來訓練的長度
private int L;// 時間序列的所利用的窗口長度
private int K = 1;// LOF算法中的k值, 默認設置爲1, 也就是取歷史最相似的序列進行預測
/**
* LOF檢測工具的構造方法
*
* @param T 時間序列用來訓練的長度
* @param L 時間序列的所利用的窗口長度
*/
public LOFDetectTool(int T, int L) {
this.T = T;
this.L = L;
}
/**
* 利用LOF進行時間序列分析
* 打印最後一段窗口的異常分數, 越接近1則越異常
*/
public void timeSeriesAnalyse(double[] series) {
// 利用T和L, 以及時間序列生成測試矩陣
double[][] mat = MatrixUtil.getMat(series, T, series.length - T - L + 1, L);
//一個窗口大小的測試序列, 默認是原序列中最後窗口大小的序列
double[] test = MatrixUtil.getTestSeries(series, series.length - L - 1, L);;
double[][] matC = MatrixUtil.getMatC(mat, T, series.length - T - L + 1, L);
double[][] matT = MatrixUtil.getMatT(mat, T, series.length - T - L + 1, L);
LOF lof = new LOF(K);
double[] ncmForC = new double[matC.length];
for (int i = 0; i < matC.length; i++) {
ncmForC[i] = lof.getLOF(matT, matC[i]);
}
double ncmForTest = lof.getLOF(matT, test);
double count = 0;
for (double x : ncmForC) {
if (ncmForTest <= x) {
count++;
}
}
count /= matC.length;
System.out.println("Anomaly Score is "+count);
}
}
給出具體的Test類
public class LOFDetectToolTest {
public double[] testData;
@Before
public void setUp() throws Exception {
testData = FileTool.getData("data.json");
}
@Test
public void timeSeriesAnalyse() throws Exception {
LOFDetectTool lofDetectTool = new LOFDetectTool(200, 50);
lofDetectTool.timeSeriesAnalyse(testData);
}
}
4. 測試
對於上圖序列計算得到的窗口異常分數爲0.007092198
我們給它加一個峯值
得到的窗口異常分數爲0.950354609
源碼以及測試數據我已經放到github了
地址爲https://github.com/MezereonXP/AnomalyDetectTool
其中包括自己編寫的多種異常檢測的工具類, 歡迎使用
希望大家多多Star, 有什麼問題可以提issue給我, 或者發郵件到我的郵箱 [email protected]
下一篇, 將會介紹利用指數平滑進行異常檢測的方法