Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

lib/Algorithm/Classifier/IsolationForest.pm  view on Meta::CPAN

					);
					$HAS_C    = 1;
					$C_SOURCE = 'runtime';
				};
			} ## end unless ( $HAS_C or $no_omp )
			unless ($HAS_C) {
				local $@;
				eval {
					require Inline;
					Inline->import(
						C        => $serial_tag . $C_CODE,
						OPTIMIZE => $opt_level,
						LIBS     => '-lm',
					);
					$HAS_C    = 1;
					$C_SOURCE = 'runtime';
				};
			} ## end unless ($HAS_C)
		} ## end else [ if ( $ENV{IF_INSTALL_BUILD} ) ]
		$OPT_LEVEL = $opt_level if $HAS_C;

	} ## end unless ( $ENV{IF_NO_C} )
	$HAS_OPENMP = ( $HAS_C && defined &has_openmp_xs && has_openmp_xs() ) ? 1 : 0;
	$HAS_SIMD   = ( $HAS_C && defined &has_simd_xs   && has_simd_xs() )   ? 1 : 0;
}

=encoding UTF-8

=head1 NAME

Algorithm::Classifier::IsolationForest - unsupervised anomaly detection via Isolation Forest or Extended Isolation Forest

=head1 SYNOPSIS

    use Algorithm::Classifier::IsolationForest;

    my @data = ([0.1, -0.2], [0.0, 0.1], [5.0, 6.0], ...);

    # Classic, axis-parallel Isolation Forest
    my $iforest = Algorithm::Classifier::IsolationForest->new(
        n_trees     => 100,
        sample_size => 256,
        seed        => 42,
    );
    $iforest->fit(\@data);

    my $scores = $iforest->score_samples(\@data);  # arrayref, each in (0,1]
    my $flags  = $iforest->predict(\@data, 0.6);    # arrayref of 0/1

    # Save and reload
    $iforest->save('model.json');
    my $reloaded = Algorithm::Classifier::IsolationForest->load('model.json');

    # Extended Isolation Forest (oblique hyperplane splits)
    my $eif = Algorithm::Classifier::IsolationForest->new(
        mode => 'extended',
        seed => 42,
    );
    $eif->fit(\@data);

    # Parallel training (fork-based, Unix-like platforms): build the
    # n_trees across several worker processes.
    my $iforest = Algorithm::Classifier::IsolationForest->new(
        n_trees      => 200,
        sample_size  => 256,
        seed         => 42,
        parallel_fit => 4,        # 4 forked workers
    );
    $iforest->fit(\@data);

    # Pre-pack a dataset to skip the per-call input-walk cost when the
    # same data gets scored many times (interactive tuning, dashboards).
    my $packed = $iforest->pack_data(\@data);
    my $scores = $iforest->score_samples($packed);
    my $flags  = $iforest->predict($packed, 0.6);

    # Get scores and labels as two flat arrayrefs in one call -- cheaper
    # than score_predict_samples when you don't need the paired shape.
    my ($s, $l) = $iforest->score_predict_split(\@data, 0.6);

=head1 DESCRIPTION

Isolation Forest (Liu, Fei Tony & Ting, Kai & Zhou, Zhi-Hua, 2008) detects anomalies by random
partitioning rather than by modelling normal points. Each tree repeatedly
splits the data. Points that get isolated after only a few splits are likely
anomalies. The score is the average isolation depth across many trees,
normalised so values approach 1 for anomalies and stay below 0.5 for normal
points.

In extended mode the module implements the Extended Isolation Forest
variant. Each split is a random hyperplane instead of an axis-aligned cut,
which removes the rectangular, axis-aligned bias in the score field and
tends to help on elongated or multi-modal data.

With C<< voting => 'majority' >> the module implements the Majority Voting
Isolation Forest (MVIForest) aggregation: each tree votes a sample
anomalous or normal against the decision threshold and the label is the
majority of the votes, with prediction stopping early once the majority is
reached.  Trees are built identically either way, so this composes with
both axis and extended mode, and an existing model can be flipped between
the two modes with L</set_voting> without refitting; see C<voting> under
L</new(%args)>.

For data that arrives as a stream and may drift over time, the companion
class L<Algorithm::Classifier::IsolationForest::Online> implements Online
Isolation Forest (Leveni et al. 2024): no C<fit()>, instead points are
learned as they arrive and forgotten once they age out of a sliding
window.  Models saved by either class can be loaded through L<load|/load($path)>,
which dispatches on the stored format tag.

Throughout these docs, B<psi> is the paper's ψ: the number of points each
tree is built from, which C<sample_size> sets and which other
implementations often call I<max samples>.  See L</REFERENCES>.

=head1 NATIVE ACCELERATION (Inline::C and OpenMP)

Both the scoring hot path (C<score_samples>, C<predict>, C<path_lengths>,
C<score_predict_samples>, and C<score_predict_split>) and the C<fit()>
tree builder are automatically accelerated through
L<Inline::C> when it is installed and a working C compiler is reachable.
If the toolchain also accepts C<-fopenmp> and can link against

lib/Algorithm/Classifier/IsolationForest.pm  view on Meta::CPAN

} ## end sub _validate_feature_descriptions

# Compile a munger spec against the model's feature names.  Requires
# Algorithm::ToNumberMunger on demand -- it is an optional dependency --
# and lets its compile() croak on any spec problem.
#
# Args:
#   $tags :: the model's feature_names, an arrayref of strings in column
#            order.  Must be non-empty.
#   $mungers :: the munger spec hashref, shaped as
#               Algorithm::ToNumberMunger->compile expects it.
#
# Returns: the compiled plan object.  Croaks when feature_names are
# missing, when the module cannot be loaded, or when compile() rejects the
# spec.
#
# Example:
#   my $plan = _compile_mungers( [ 'method', 'path_len' ],
#       { method => { munger => 'http_method_enum', default => -1 } } );
sub _compile_mungers {
	my ( $tags, $mungers ) = @_;
	croak "this model has mungers but no feature_names to compile them against"
		unless ref $tags eq 'ARRAY' && @$tags;
	local $@;
	eval { require Algorithm::ToNumberMunger; 1 }
		or croak "this model has mungers configured but Algorithm::ToNumberMunger "
		. "could not be loaded; install it to use tagged data with this model: $@";
	return Algorithm::ToNumberMunger->compile(
		tags    => $tags,
		mungers => $mungers,
	);
} ## end sub _compile_mungers

# The compiled plan for this model, or undef when no mungers are
# configured.  Compiled lazily (memoised in _munger_plan) so from_json
# does not need Algorithm::ToNumberMunger installed unless tagged data
# is actually used; new() populates the slot eagerly instead, surfacing
# spec errors at construction.
#
# A plain function rather than a method, like the two helpers above, so the
# Online class can hand it its own $self.
#
# Args: none beyond the model, which is passed positionally.
#
# Returns: the compiled Algorithm::ToNumberMunger plan, or undef when the
# model carries no munger spec.  The plan is memoised in _munger_plan, so
# the compile happens at most once per model.
#
# Example:
#   if ( my $plan = _plan($self) ) {
#       my $vec = $plan->apply_named( { method => 'GET', host => 'h' } );
#   }
sub _plan {
	my ($self) = @_;
	return undef unless $self->{mungers};
	$self->{_munger_plan} //= _compile_mungers( $self->{feature_names}, $self->{mungers} );
	return $self->{_munger_plan};
}

# Memoised "does this perl have a real fork()?".  False on Windows
# without Cygwin; true on every Unix-like platform.  fit() consults it
# before honouring parallel_fit, which is how that option degrades to a
# serial fit instead of failing.
#
# Args: none.
#
# Returns: 1 when Config's d_fork is defined, 0 otherwise.  Config is
# loaded on the first call only and the answer cached for the process.
#
# Example:
#   $self->_fit_trees_parallel(...) if $workers > 1 && _fork_supported();
{
	my $cached;

	sub _fork_supported {
		return $cached if defined $cached;
		require Config;
		$cached
			= ( ( $Config::Config{d_fork} || '' ) eq 'define' ) ? 1 : 0;
		return $cached;
	}
}

#-------------------------------------------------------------------------------
# Fork-based parallel tree builder.  Used by fit() when parallel_fit > 1
# and the platform has a real fork().  Divides n_trees evenly among
# workers; each child seeds its own RNG ($seed + worker_id * 1009 so
# fixed-worker-count runs are reproducible), builds its share (via the
# C builder when _use_c is on, same as the non-parallel path), and
# returns the trees to the parent via Storable on a one-shot pipe.
#
# The trees that come back differ from a serial fit with the same seed
# because the RNG draws happen in a different order -- this is documented
# as part of the parallel_fit contract.
#
# Args:
#   $data :: the prepared training set, an arrayref of feature-value
#            arrayrefs.  Each worker inherits it through the fork, so it is
#            never serialised.
#   $psi :: the per-tree sub-sample size from _resolve_geometry.
#   $limit :: the tree height limit from _resolve_geometry.
#   $workers :: how many children to fork.  Clamped down to n_trees, since
#               a worker with no trees to build is pure overhead.
#
# Returns: an arrayref of all n_trees trees in worker order -- worker 0's
# share first, then worker 1's, and so on, which is what makes the forest
# reproducible.  Croaks when a fork or pipe fails, when a worker exits
# non-zero, or when its trees come back unreadable.
#
# Example:
#   $self->{trees} = $self->_fit_trees_parallel( $train, 256, 8, 4 );
#-------------------------------------------------------------------------------
sub _fit_trees_parallel {
	my ( $self, $data, $psi, $limit, $workers ) = @_;
	require Storable;
	require POSIX;

	my $n_trees = $self->{n_trees};
	$workers = $n_trees if $workers > $n_trees;

	# Divide n_trees as evenly as possible across workers.



( run in 1.411 second using v1.01-cache-2.11-cpan-64ef6c95b5d )