blob: 6f8ae523b792a49a2e228141bfac943cc618eee9 [file]
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------
# Builtin function that calculates the anomaly score as described in [Liu2008]
# for a set of samples `X` based on an iForest model.
#
# [Liu2008]:
# Liu, F. T., Ting, K. M., & Zhou, Z. H.
# (2008, December).
# Isolation forest.
# In 2008 eighth ieee international conference on data mining (pp. 413-422).
# IEEE.
#
# .. code-block:: python
#
# >>> import numpy as np
# >>> from systemds.context import SystemDSContext
# >>> from systemds.operator.algorithm import outlierByIsolationForest, outlierByIsolationForestApply
# >>> with SystemDSContext() as sds:
# ... # Create training data: 20 points clustered near origin
# ... X_train = sds.from_numpy(np.array([
# ... [0.0, 0.0], [0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4],
# ... [0.5, 0.5], [0.6, 0.6], [0.7, 0.7], [0.8, 0.8], [0.9, 0.9],
# ... [1.0, 1.0], [1.1, 1.1], [1.2, 1.2], [1.3, 1.3], [1.4, 1.4],
# ... [1.5, 1.5], [1.6, 1.6], [1.7, 1.7], [1.8, 1.8], [1.9, 1.9]
# ... ]))
# ... model = outlierByIsolationForest(X_train, n_trees=100, subsampling_size=10, seed=42)
# ... X_test = sds.from_numpy(np.array([[1.0, 1.0], [100.0, 100.0]]))
# ... scores = outlierByIsolationForestApply(model, X_test).compute()
# ... print(scores.shape)
# ... print(scores[1, 0] > scores[0, 0])
# ... print(scores[1, 0] > 0.5)
# (2, 1)
# True
# True
#
#
# INPUT:
# ---------------------------------------------------------------------------------------------
# iForestModel The trained iForest model as returned by outlierByIsolationForest
# X Samples to calculate the anomaly score for. X must contain every feature referenced
# by an internal model node
#
# To compare implementations element by element, apply the same serialized iForestModel in each
# implementation. Independently trained forests are generally different even when given the same
# numeric seed because RNG and sampling semantics are implementation-specific.
# ---------------------------------------------------------------------------------------------
#
# OUTPUT:
# ---------------------------------------------------------------------------------------------
# anomaly_scores Column vector of anomaly scores corresponding to the samples in X.
# Samples with an anomaly score > 0.5 are generally considered to be outliers
# ---------------------------------------------------------------------------------------------
s_outlierByIsolationForestApply = function(List[Unknown] iForestModel, Matrix[Double] X)
return(Matrix[Double] anomaly_scores)
{
anomaly_scores = m_outlierByIsolationForestApply(iForestModel, X)
}
m_outlierByIsolationForestApply = function(List[Unknown] iForestModel, Matrix[Double] X)
return(Matrix[Double] anomaly_scores)
{
if (nrow(X) < 1)
stop("outlierByIsolationForestApply: X must contain at least one row.")
M = as.matrix(iForestModel["model"])
subsampling_size = as.integer(as.scalar(iForestModel["subsampling_size"]))
if (subsampling_size <= 1)
stop("outlierByIsolationForestApply: model subsampling_size must be greater than 1.")
if (nrow(M) < 1)
stop("outlierByIsolationForestApply: model must contain at least one tree.")
height_limit = ceil(log(subsampling_size, 2))
tree_size = 2 * (2^(height_limit + 1) - 1)
if (ncol(M) != tree_size)
stop("outlierByIsolationForestApply: model has an invalid number of columns.")
anomaly_scores = matrix(0, rows = nrow(X), cols = 1)
for (i_x in 1:nrow(X))
anomaly_scores[i_x, 1] = m_score(M, X[i_x, ], subsampling_size)
}
# Calculates the PathLength as defined in [Liu2008] based on a sample x
#
# INPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# NAME TYPE DEFAULT MEANING
# ---------------------------------------------------------------------------------------------
# M Matrix[Double] The linearized iTree model
# x Matrix[Double] The sample to calculate the PathLength
#
# ---------------------------------------------------------------------------------------------
# OUTPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# PathLength The PathLength for the sample
# ---------------------------------------------------------------------------------------------
m_PathLength = function(Matrix[Double] M, Matrix[Double] x)
return(Double PathLength)
{
[nrEdgesTraversed, externalNodeSize] = s_traverseITree(M, x)
if (externalNodeSize <= 1) {
PathLength = nrEdgesTraversed
}
else {
PathLength = nrEdgesTraversed + s_cn(externalNodeSize)
}
}
# Traverses an iTree based on a sample x
#
# INPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# NAME TYPE DEFAULT MEANING
# ---------------------------------------------------------------------------------------------
# M Matrix[Double] The linearized iTree model to traverse
# x Matrix[Double] The sample to traverse the iTree with
#
# ---------------------------------------------------------------------------------------------
# OUTPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# nrEdgesTraversed The number of edges traversed until an external node was reached
# externalNodeSize The size of the external node assigned during training
# ---------------------------------------------------------------------------------------------
s_traverseITree = function(Matrix[Double] M, Matrix[Double] x)
return(Integer nrEdgesTraversed, Integer externalNodeSize)
{
if (nrow(x) != 1)
stop("s_traverseITree: x must be a single row.")
nrEdgesTraversed = 0
externalNodeSize = 0
is_external_node = FALSE
node_id = 1
while (!is_external_node) {
node_start_idx = node_id * 2 - 1
if (node_start_idx + 1 > ncol(M))
stop("s_traverseITree: invalid iTree model; node index is out of bounds.")
split_feature = as.integer(as.scalar(M[1, node_start_idx]))
node_value = as.scalar(M[1, node_start_idx + 1])
if (split_feature > 0) {
if (split_feature > ncol(x))
stop("s_traverseITree: model split feature exceeds the input width.")
nrEdgesTraversed = nrEdgesTraversed + 1
x_val = as.scalar(x[1, split_feature])
# Training uses the same < / >= partition at every internal node.
if (x_val < node_value)
node_id = node_id * 2
else
node_id = node_id * 2 + 1
}
else if (split_feature == 0) {
if (node_value < 1)
stop("s_traverseITree: invalid iTree model; external-node size must be positive.")
externalNodeSize = as.integer(node_value)
is_external_node = TRUE
}
else {
stop("s_traverseITree: invalid iTree model; reached a placeholder node.")
}
}
}
# This function gives the average path length of unsuccessful search in BST `c(n)`
# for `n` nodes as given in [Liu2008]. This function is used to normalize the path length
#
# INPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# NAME TYPE DEFAULT MEANING
# ---------------------------------------------------------------------------------------------
# n Int Number of samples in the external node for which c(n)
# should be calculated
# ---------------------------------------------------------------------------------------------
# OUTPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# cn Value for c(n)
# ---------------------------------------------------------------------------------------------
s_cn = function(Integer n)
return(Double cn)
{
if (n <= 1)
stop("s_cn: n must be greater than 1.")
# The logarithmic approximation has noticeable error for small n, so H(n-1)
# is evaluated directly below 1000.
if (n < 1000) {
indices = seq(1, n - 1)
H_nminus1 = sum(1 / indices)
}
else {
# Euler–Mascheroni's constant
eulergamma = 0.57721566490153
H_nminus1 = log(n - 1) + eulergamma
}
cn = 2 * H_nminus1 - 2 * (n - 1) / n
}
# Scores a sample `x` according to the score function `s(x, n)` described in [Liu2008].
#
# INPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# NAME TYPE DEFAULT MEANING
# ---------------------------------------------------------------------------------------------
# M Matrix[Double] iForest model used to score
# x Matrix[Double] Sample to be scored
# n Int Subsample size the iTrees were built from
# ---------------------------------------------------------------------------------------------
# OUTPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# score The anomaly score for x
# ---------------------------------------------------------------------------------------------
m_score = function(Matrix[Double] M, Matrix[Double] x, Integer n)
return(Double score)
{
if (n <= 1)
stop("m_score: n must be greater than 1.")
if (nrow(x) != 1)
stop("m_score: x must be a single row.")
if (nrow(M) < 1)
stop("m_score: model must contain at least one tree.")
# Only the mean path length is required, so no intermediate vector is built.
path_length_sum = 0.0
for (i_iTree in 1:nrow(M))
path_length_sum = path_length_sum + m_PathLength(M[i_iTree, ], x)
avg_path_length = path_length_sum / nrow(M)
score = 2^-(avg_path_length / s_cn(n))
}