动态

详情 返回 返回

彩筆運維勇闖機器學習--孤立森林 - 动态 详情

前言

孤立森林,一種非常高效快速的異常檢測算法

開始探索

scikit-learn

import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest

rng = np.random.RandomState(0)

X_train = 0.3 * rng.randn(100, 2)
X_outliers = rng.uniform(low=-2, high=2, size=(10, 2))

clf = IsolationForest(n_estimators=100, max_samples='auto', contamination='auto', random_state=rng)
clf.fit(X_train)

y_pred_train = clf.predict(X_train)
y_pred_outliers = clf.predict(X_outliers)

plt.title("Isolation Forest")
plt.scatter(X_train[:, 0], X_train[:, 1], color='b', label="Normal")
plt.scatter(X_outliers[:, 0], X_outliers[:, 1], color='r', label="Outliers")

plt.legend()
plt.axis('tight')
plt.show()

腳本!啓動:

watermarked-isolation_forest_1_4

深入理解

類似於隨機森林,但每棵樹不使用信息增益或基尼係數等指標,而是隨機選擇一個特徵,在該特徵的最小值和最大值之間隨機選一個切分值,將數據集分成兩部分,又在每個部分隨機最大值與最小值之間隨機選一個切分支,不斷遞歸。指導到達指定深度或者當前節點只有1個樣本

構造如此的樹n棵,組成森林,開始計算每個樣本在每棵樹的平均路徑長度(葉子節點的深度depth),計算異常分數

\[\begin{cases} c(n)=2H(n-1)-\frac{2(n-1)}{n} \\ H(n)=\sum_{i=1}^{n} \frac{1}{i} \\ s(x,n) = 2^{-\frac{E(x)}{c(n)}} \end{cases} \]

  • \(s \approx 1\),強異常點,很容易被孤立
  • \(0.5 \le s < 1\),可能是異常點,越接近1越是異常點,需要配合其他參數來確定,比如異常點比例
  • \(s < 0.5\),正常點

舉例説明

假設有以下樣本: [1, 1.5, 1.8, 2.0, 2.3, 10]

構造第一棵樹:

1)第一層:depth=1,隨機選擇劃分值:1 < split < 10 的區間中選擇 split = 5

  • 左子樹:[1, 1.5, 1.8, 2.0, 2.3]
  • 右子樹:[10.0]

2)第二層:depth=2,隨機選擇劃分值:1 < split < 2.3 的區間中選擇 split = 1.6

  • 左子樹:[1, 1.5]
  • 右子樹:[1.8, 2.0, 2.3]

3)第三層:depth=3,左子樹,隨機選擇劃分值:1 < split < 1.5 的區間中選擇 split = 1.2

  • 左子樹:[1]
  • 右子樹:[1.5]

4)第三層:depth=3,右子樹,隨機選擇劃分值:1.8 < split < 2.3 的區間中選擇 split = 2.1

  • 左子樹:[1.8, 2.0]
  • 右子樹:[2.3]

5)第四層:depth=4,隨機選擇劃分值:1.8 < split < 2.9 的區間中選擇 split = 1.9

  • 左子樹:[1.8]
  • 右子樹:[2.9]

6)計算路徑

watermarked-isolation_forest_1_1

樣本值 路徑長度
1.0 3
1.5 3
1.8 4
2.0 4
2.3 3
10.0 1

重複構造第n棵樹,得出路徑,計算路徑平均值

樣本值 第1棵樹路徑 第2棵樹路徑 第n棵樹路徑 平均路徑
1.0 3 3 .. 3
1.5 3 3 .. 3
1.8 4 4 .. 4
2.0 4 4 .. 4
2.3 3 3 .. 3
10.0 1 1 .. 1

計算異常得分

\[s(x,n) = 2^{-\frac{E(x)}{c(n)}} \]

1)計算樣本(1.0):

  • 樣本長度:\(E(x) = 3\)
  • 樣本規模 \(n=6\) 的平均路徑期望:

\[\begin{cases} c(n)=2H(n-1)-\frac{2(n-1)}{n} \\ H(n)=\sum_{i=1}^{n} \frac{1}{i} \end{cases} \]

\[c(6)=2H(n-1)-\frac{2(n-1)}{n}=2·(1+\frac{1}{2}+\frac{1}{3}+\frac{1}{4}+\frac{1}{5}) - \frac{2(6-1)}{6} \approx 2.8999 \]

\[s(1.0) = 2^{-\frac{E(x)}{c(n)}} = 2^{-\frac{3}{2.8999}} \approx 0.4882 \]

2)計算所有樣本

樣本值 平均長度 異常得分
1.0 3 0.4882
1.5 3 0.4882
1.8 4 0.3844
2.0 4 0.3844
2.3 3 0.4882
10.0 1 0.7874

判斷異常點:

  • 路徑長度越短的越異常,比如10.0的路徑長度為1,在第一次分割的時候就被孤立了
  • 異常分數越高就是異常點

sklearn中的異常分數

from sklearn.ensemble import IsolationForest
import numpy as np

X = np.array([[1], [1.5], [1.8], [2], [2.3], [10]])

clf = IsolationForest(random_state=0, contamination='auto')
clf.fit(X)

pred = clf.predict(X)
score = clf.decision_function(X)

for x, p, s in zip(X, pred, score):
    print(f"樣本 {x[0]:>4} -> {'異常' if p==-1 else '正常'} | 異常分數(decision_function): {s:.4f}")

腳本!啓動:

watermarked-isolation_forest_1_2

問題出現了:

  • sklearn的分數和手工計算的並不一樣
  • 為什麼1.0被當成異常了
  • 分數越小反而越異常

先看第一個問題,sklearn的分數和手工計算的並不一樣。首先,每棵樹是採用部分的樣本來計算,而不是採用所有的樣本n=6來計算的。其次,在上面的手工計算中,期望路徑長度\(c(n)\)中的\(H(n)\),並不是由這個公式計算的

\[\begin{cases} c(n)=2H(n-1)-\frac{2(n-1)}{n} \\ H(n)=\sum_{i=1}^{n} \frac{1}{i} \end{cases} \]

這個公式一旦n的數量增大,\(H(n)\)的計算將會帶來很大的計算消耗,通常使用另外一個公式計算近似值:

\[H(n) \approx ln(n) + \gamma ,其中\gamma \approx 0.5772(歐拉常數) \]

以上兩點原因,帶來的就是sklearn計算異常分數與手工計算不一樣

再看第二個問題,為什麼1.0被當成異常了

只需要調整一個參數,contamination=0.1就可以解決這個問題了

watermarked-isolation_forest_1_3

contamination用來調節異常比例的參數,如果是auto,那麼異常比例為33.3%,6個樣本,那麼異常點就是2個。手動調整為0.1,那就告訴模型只有1個異常點,那麼最不正常的就是10.0

最後第三個問題,分數越小反而越異常。這明顯是計算方式不一樣造成的,這裏直接解析一下源碼,版本:scikit-learn:1.6.1

  • decision_function函數

        def decision_function(self, X):
            return self.score_samples(X) - self.offset_
    
  • score_samples函數返回的是:經過公式\(s(x,n) = 2^{-\frac{E(x)}{c(n)}}\)計算的相反數

        def score_samples(self, X):
            ...
            return self._score_samples(X)
    
        def _score_samples(self, X):
            return -self._compute_chunked_score_samples(X)
    
        def _compute_chunked_score_samples(self, X):
            ...
    
            for sl in slices:
                # compute score on the slices of test samples:
                scores[sl] = self._compute_score_samples(X[sl], subsample_features)
    
            return scores
    
        def _compute_score_samples(self, X, subsample_features):
            ...
            scores = 2 ** (
                # For a single training sample, denominator and depth are 0.
                # Therefore, we set the score manually to 1.
                -np.divide(
                    depths, denominator, out=np.ones_like(depths), where=denominator != 0
                )
            )
            return scores
    
  • self.offset_是根據整個樣本異常分數,再加上異常比例參數contamination的中位數計算出來的

          self.offset_ = np.percentile(self._score_samples(X), 100.0 * self.contamination)
    
    

看到這裏,我就想説,複雜就行了,經過這麼複雜的計算,與手動計算出來的肯定不一樣

小結

在sklearn中

  • 找到孤立點,contamination是一個非常重要的參數,它決定了每個節點的分數以及後續確定是否異常
  • 快速找到孤立點,直接通過pred函數即可,-1是孤立點,1是正常點
  • 想要獲取點的評分,通過decision_function函數獲取評分,與理論公式不同,評分越低反而越異常

小結

  • 聯繫我,做深入的交流

至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...

Add a new 评论

Some HTML is okay.