IsolationForest 决策得分到概率算法的转换

Conversion of IsolationForest decision score to probability algorithm

我想创建一个通用函数来将 sklearn's IsolationForest 的输出 decision_scores 转换为真实概率 [0.0, 1.0]

我知道并已阅读,the original paper并且我从数学上理解该函数的输出不是概率,而是每个基本估计器构建的路径长度的平均值以隔离异常。

问题

我想将该输出转换为 tuple (x,y) 形式的概率,其中 x=P(anomaly)y=1-x

当前方法

def convert_probabilities(predictions, scores):
    from sklearn.preprocessing import MinMaxScaler

    new_scores = [(1,1) for _ in range(len(scores))]

    anomalous_idxs = [i for i in (range(len(predictions))) if predictions[i] == -1]
    regular_idxs = [i for i in (range(len(predictions))) if predictions[i] == 1]

    anomalous_scores = np.asarray(np.abs([scores[i] for i in anomalous_idxs]))
    regular_scores = np.asarray(np.abs([scores[i] for i in regular_idxs]))

    scaler = MinMaxScaler()

    anomalous_scores_scaled = scaler.fit_transform(anomalous_scores.reshape(-1,1))
    regular_scores_scaled = scaler.fit_transform(regular_scores.reshape(-1,1))

    for i, j in zip(anomalous_idxs, range(len(anomalous_scores_scaled))):
        new_scores[i] = (anomalous_scores_scaled[j][0], 1-anomalous_scores_scaled[j][0])
    
    for i, j in zip(regular_idxs, range(len(regular_scores_scaled))):
        new_scores[i] = (1-regular_scores_scaled[j][0], regular_scores_scaled[j][0])

    return new_scores

modified_scores = convert_probabilities(model_predictions, model_decisions)

最小的、可重现的例子

import pandas as pd
from sklearn.datasets import make_classification, load_iris
from sklearn.ensemble import IsolationForest
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split

# Get data
X, y = load_iris(return_X_y=True, as_frame=True)
anomalies, anomalies_classes = make_classification(n_samples=int(X.shape[0]*0.05), n_features=X.shape[1], hypercube=False, random_state=60, shuffle=True)
anomalies_df = pd.DataFrame(data=anomalies, columns=X.columns)

# Split into train/test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=60)

# Combine testing data
X_test['anomaly'] = 1
anomalies_df['anomaly'] = -1
X_test = X_test.append(anomalies_df, ignore_index=True)
y_test = X_test['anomaly']
X_test.drop('anomaly', inplace=True, axis=1)

# Build a model
model = IsolationForest(n_jobs=1, bootstrap=False, random_state=60)

# Fit it
model.fit(X_train)

# Test it
model_predictions = model.predict(X_test)
model_decisions = model.decision_function(X_test)

# Print results
for a,b,c in zip(y_test, model_predictions, model_decisions):
    print_str = """
    Class: {} | Model Prediction: {} | Model Decision Score: {}
    """.format(a,b,c)

    print(print_str)

问题

modified_scores = convert_probabilities(model_predictions, model_decisions)

# Print results
for a,b in zip(model_predictions, modified_scores):
    ans = False
    if a==-1:
        if b[0] > b[1]:
            ans = True
        else:
            ans = False
    elif a==1:
        if b[1] > b[0]:
            ans=True
        else:
            ans=False
    print_str = """
    Model Prediction: {} | Model Decision Score: {} | Correct: {}
    """.format(a,b, str(ans))

    print(print_str)

显示一些奇怪的结果,例如:

Model Prediction: 1 | Model Decision Score: (0.17604259932311161, 0.8239574006768884) | Correct: True
Model Prediction: 1 | Model Decision Score: (0.7120367886017022, 0.28796321139829784) | Correct: False
Model Prediction: 1 | Model Decision Score: (0.7251531538304419, 0.27484684616955807) | Correct: False
Model Prediction: -1 | Model Decision Score: (0.16776449326185877, 0.8322355067381413) | Correct: False
Model Prediction: 1 | Model Decision Score: (0.8395087028516501, 0.1604912971483499) | Correct: False

模型预测:1 |模型决策分数:(0.0,1.0)|正确:正确

怎么可能预测是-1 (anomaly),概率只有37%呢?或者预测是1 (normal),但是概率是26%?

请注意,玩具数据集已标记,但无监督异常检测算法显然假定没有标签。

为什么会这样

您正在观察无意义的概率,因为您正在为异常值和异常值拟合不同的缩放器。因此,如果您的决策分数范围是 [0.5, 1.5] 内点,您将把这些分数映射到概率 [0, 1]。此外,如果异常值的决策分数范围是 [-1.5, -0.5],那么您也将把这些分数映射到概率 [0, 1]。如果决策得分为 1.5-0.5,您最终将成为内点的概率设置为 1。这显然不是您想要的,您希望具有决策分数 -0.5 的观察结果的概率低于具有决策分数 1.5.

的观察结果的概率

第一个选项

第一个解决方案是为所有乐谱配备一个定标器。这也将大大简化您的转换功能,如下所示:

def convert_probabilities(predictions, scores):

    scaler = MinMaxScaler()

    scores_scaled = scaler.fit_transform(scores.reshape(-1,1))
    new_scores = np.concatenate((1-scores_scaled, scores_scaled), axis=1)

    return new_scores

这将是具有所需属性的 (probability of being an outlier, probability of being an inlier) 元组。

这种方法的局限性

此方法的主要局限之一是无法保证内点和异常点之间的概率截止值为 0.5,这是最直观的选择。您最终可能会遇到这样的场景:“如果成为异常值的概率小于 60%,则模型预测它是异常值”。

第二个选项

第二个选项更接近你想要做的。您确实为每个类别安装了一个缩放器,但是,与您所做的不同,两个缩放器的值都不在同一范围内 return 。您可以将离群值设置为 [0, 0.5],将离群值设置为 [0.5, 1]。这样做的好处是它会在 0.5 处创建一个直观的决策边界,其中上面的所有概率都是内点,反之亦然。它看起来像这样:

def convert_probabilities(predictions, scores):

    scaler_inliers = MinMaxScaler((0.5, 1))
    scaler_outliers = MinMaxScaler((0, 0.5))

    scores_inliers_scaled = scaler_inliers.fit_transform(scores[predictions == 1].reshape(-1,1))
    scores_outliers_scaled = scaler_outliers.fit_transform(scores[predictions == -1].reshape(-1,1))
    scores_scaled = np.zeros((len(scores), 1))
    scores_scaled[predictions == 1] = scores_inliers_scaled
    scores_scaled[predictions == -1] = scores_outliers_scaled
    new_scores = np.concatenate((1-scores_scaled, scores_scaled), axis=1)

    return new_scores

这种方法的局限性

主要限制是如何将两个定标器组合在一起。在上面的代码示例中,两者都在 0.5 处连接,这意味着“最佳异常值”和“最差异常值”具有相同的概率 0.5。但是,他们没有相同的决策分数。因此,一种选择是将缩放范围更改为 [0, 0.49], and [0.51, 1]` 左右,但如您所见,这变得更加随意。

您在这里遇到了三个不同的问题。首先,不能保证你从 IsolationForest 得到的分数越低,样本是离群值的概率也越高。我的意思是,如果对于一堆样本,你在 (-0.3 : -0.2)(0.1 : 0.2) 范围内得到 model_decision 分数,这并不一定意味着第一批是异常值的概率更高(但是 通常会是)。

第二个问题是分数到概率的实际映射函数。因此 假设 较低的分数对应于较低的正常样本概率(以及样本异常的较高概率),从分数到概率的映射不一定是线性函数(例如 MinMaxScaler)。对于您的数据,您可能需要找到自己的函数。它可以是@Jon Nordby 建议的分段线性函数。我个人更喜欢使用 logistic function 将分数映射到概率。在这种情况下,使用 model_decisions 以零为中心尤其有益,负值表示异常。所以你可以使用像

这样的东西
def logf(x, alfa=10): 
    return 1/(1 + np.exp( -alfa * x ))

用于从分数到概率的映射。 Alpha 参数控制值在决策边界周围的紧密程度。同样,这不一定是最好的映射功能,它只是我喜欢使用的功能。

上一期与第一期相连,应该可以解答你的问题。即使 通常 分数与不异常的概率相关,也不能保证对于 所有 样本都是如此。所以可能会出现某个得分为0.1的点是异常,而得分为-0.1的是正常点被误检测为异常。样本是否异常由 model_decisions 是否小于零来决定。对于得分接近于零的样本,错误的概率更高。

虽然几个月后,这个问题有了答案。

A paper was published in 2011 试图展示关于这个主题的研究;将异常分数统一为概率。

事实上,pyod library has a common predict_proba方法,它提供了使用这种统一方法的选项。

这是一个代码实现(受their source影响):

def convert_probabilities(data, model):
    decision_scores = model.decision_function(data)
    probs = np.zeros([data.shape[0], int(model.classes)])
    pre_erf_score = ( decision_scores - np.mean(decision_scores) ) / ( np.std(decision_scores) * np.sqrt(2) )
    erf_score = erf(pre_erf_score)
    probs[:, 1] = erf_score.clip(0, 1).ravel()
    probs[:, 0] = 1 - probs[:, 1]
    return probs

(作为参考,pyod 确实有一个 Isolation Forest implementation