教你用java實現時序數據異常檢測(1)LOF-ICAD方法

數據的異常檢測是一個難題, 面臨許多挑戰, 其中包括:
- 定義一個正常表現的範圍是比較困難的, 異常值和正常值有時候邊界並不是特別明顯
- 某些惡意行爲會僞裝成正常值, 難以發現
- 大多數領域的正常行爲只能在一段時間內有效, 對於未來的普適性並不是很高
- 對於異常的概念會由於應用的不同而不同
- 缺少帶有標記的數據
- 數據的噪聲可能有較大的影響

分析異常數據有多種方案, 包括:
- 基於分類的手段
- 基於最近鄰算法
- 基於聚類
- 基於統計方法
- 基於信息理論
- 基於特徵理論

我們這次着重介紹的是時序數據的異常檢測, 我們來討論討論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
reach_dist
其中NN_k(x)是x的第k個近鄰, 而reach_dist是爲了當x和o彼此靠近的時候減少統計波動

我們計算出密度之後, 就要利用該密度和其他近鄰的點進行比較, 進而我們就可以計算出異常程度的分數, 記爲LOF, 按如下方法進行計算:
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
具體的NCM值的計算也就是LOF的計算方式得到
4. 對序列最後的x(T+C+L-1)計算NCM值
計算序列末尾的NCM
5. 計算異常程度分數p
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. 測試

測試序列1
對於上圖序列計算得到的窗口異常分數爲0.007092198
我們給它加一個峯值
測試序列2
得到的窗口異常分數爲0.950354609

源碼以及測試數據我已經放到github了
地址爲https://github.com/MezereonXP/AnomalyDetectTool
其中包括自己編寫的多種異常檢測的工具類, 歡迎使用
希望大家多多Star, 有什麼問題可以提issue給我, 或者發郵件到我的郵箱 [email protected]

下一篇, 將會介紹利用指數平滑進行異常檢測的方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章