Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
walking trees per sample as soon as the outcome is
decided. The threshold argument/default of the
predict methods is therefore the PER-TREE cutoff
here, not a forest-level score cutoff.
score_samples() returns the fraction of trees
voting anomalous -- still in [0, 1], but discrete
in steps of 1/n_trees. contamination composes: fit()
learns the per-tree cutoff that flags the requested
fraction of the training set.
default :: mean
- parallel_fit :: positive integer N => build the trees across N forked
worker processes during fit(). Each worker gets a derived seed
(parent seed + worker_id * 1009) so the parallel fit is
reproducible across runs at fixed worker count -- but the trees
produced are NOT bit-identical to a serial fit with the same
seed, because the RNG draws happen in a different order.
Inference is unaffected. Falls back silently to serial on
platforms without a real fork() (e.g. Windows without Cygwin).
default :: undef (serial)
- use_c :: boolean, override whether the Inline::C backend is used for
both scoring and fit()'s tree builder. When false the instance
falls back to pure Perl for both even if the C backend compiled
successfully. When true (or unset) the C backend is used if
available ($HAS_C). fit() with use_c on produces bit-identical
trees to use_c off for the same seed -- only build speed differs.
default :: $HAS_C
- use_openmp :: boolean, override whether OpenMP parallel scoring is
used inside score_all_xs(). When false the C tree walk runs
single-threaded even if OpenMP was linked in. Ignored when
use_c is false (pure Perl has no OpenMP path).
default :: $HAS_OPENMP
- use_openmp_fit :: boolean, build fit()'s trees across OpenMP threads
(one tree per thread) instead of the single-threaded C builder.
Opt-in and off by default: unlike use_c/use_openmp, this changes
which trees get built. Perl's RNG isn't safe to call from
multiple OS threads sharing one interpreter, so this path seeds
an independent PRNG per tree from the tree index rather than
Drand01() -- trees differ from the use_c (single-threaded)
and pure-Perl paths even with the same seed, though a fixed
seed and n_trees still reproduce the same trees regardless of
OMP_NUM_THREADS or scheduling. Does NOT compose with
parallel_fit: a forked child starting its own OpenMP region
after the parent process has used OpenMP for anything can
hang (a general fork()+libgomp limitation), so parallel_fit's
workers always use the single-threaded C builder regardless
of this setting -- setting both just means parallel_fit wins.
Ignored (clamped to 0) when use_c is false or OpenMP isn't
linked in.
default :: 0
- feature_names :: optional arrayref of per-feature labels enabling the
*_tagged methods (and required by mungers below).
default :: undef
- mungers :: optional hashref of declarative L<Algorithm::ToNumberMunger>
specs, keyed as that module's compile() expects (scalar mungers by
their output tag, expanding mungers by any label with an 'into'
list, combining mungers by their output tag with a 'from' list).
When set, every tagged row -- the *_tagged methods, fit_tagged,
and tagged_row_to_array -- is munged from raw values (strings,
timestamps, status codes, ...) into numbers through the compiled
plan, and munge_rows() applies the scalar mungers to positional
rows. Requires feature_names; the plan compiles against them, so
any spec error croaks here in new(). Algorithm::ToNumberMunger is
an optional dependency, required only when a spec is given (or a
loaded model carrying one is used with tagged data). The spec is
saved with the model, so a loaded model munges scoring input
exactly as it did training input. See L</MUNGERS> for details
and caveats.
default :: undef
- schema_version :: optional opaque string identifying the revision of
the variable schema this model was built against. Never parsed
or compared numerically; saved with the model and shown by
`iforest info`. Usually set from a prototype (see
L</PROTOTYPES>) rather than passed directly.
default :: undef
- schema_description :: optional opaque free-text description of what
the variable schema is. Same handling as schema_version.
default :: undef
- feature_descriptions :: optional hashref of 'feature name => free
text' describing individual features. Requires feature_names;
every key must name an entry there (a description for a feature
that does not exist croaks -- it is either a typo or a stale
leftover from a schema change). Partial coverage is fine.
Saved with the model and shown beside each tag by
`iforest info`.
default :: undef
Note: log2 under Perl is as below...
log($psi) / log(2)
=cut
sub new {
my ( $class, %args ) = @_;
my $mode = $args{mode} // 'axis';
croak "mode must be 'axis' or 'extended'"
unless $mode eq 'axis' || $mode eq 'extended';
# How fit() treats undef (missing) feature cells. Scoring always
# tolerates undef regardless of this setting -- it governs fit only.
# die :: croak if the training data contains any undef cell (default)
# zero :: treat a missing cell as the value 0
# impute :: replace a missing cell with the per-feature mean/median
# learned from the present values at fit time
# nan :: build ranges over present values only and route a point
# missing the split feature consistently to one branch, at
# both fit and score time
my $missing = $args{missing} // 'die';
croak "missing must be one of: die, zero, impute, nan"
unless $missing =~ /\A(?:die|zero|impute|nan)\z/;
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
my $scores = $self->score_samples( $self->_to_arrayref($data) );
return [ map { $_ >= $threshold ? 1 : 0 } @$scores ];
} ## end sub predict
=head2 predict_tagged(\%row, $threshold)
Predicts whether a single sample is an anomaly using a hashref of named
feature values. The model must have been fitted (or loaded from a model
that was fitted) with feature names stored via C<feature_names>.
C<$threshold> defaults the same way as in C<predict>.
Returns a scalar 1 (anomaly) or 0 (normal).
my $label = $forest->predict_tagged(
{ cpu => 0.9, mem => 0.4, disk => 0.1 },
);
Croaks if the model has no stored feature names, if the hashref contains a
key that is not a known feature name, or if a feature name is absent from the
hashref.
=cut
=head2 tagged_row_to_array(\%row, $caller)
Validates a hashref of named feature values against the model's stored
C<feature_names> and returns a positional arrayref ready to pass to any
of the scoring or prediction methods.
C<$caller> is a string used in error messages to identify which method
triggered the validation (pass the calling method's name).
my $vec = $forest->tagged_row_to_array(\%row, 'my_method');
# returns e.g. [0.9, 0.4, 0.1] ordered by feature_names
Croaks if:
=over 4
=item * C<$row> is not a hashref
=item * the model has no stored C<feature_names>
=item * the hashref contains a key that is not a known feature name
=item * a feature name is absent from the hashref
=back
=cut
sub tagged_row_to_array {
my ( $self, $row, $caller ) = @_;
croak "$caller requires a hashref"
unless ref $row eq 'HASH';
# With mungers configured the compiled plan owns the row assembly:
# it knows the real input fields (munger 'from' sources included,
# which need not be tags at all) and croaks on a missing one. Extra
# keys are ignored -- with expanders and combiners in play, "the
# exact key set" is the plan's knowledge, not feature_names'.
if ( my $plan = _plan($self) ) {
my $vec = eval { $plan->apply_named($row) };
croak "$caller: $@" if $@;
return $vec;
}
croak "this model has no stored feature_names; " . "refit with -t tags or pass feature_names to new()"
unless defined $self->{feature_names}
&& ref $self->{feature_names} eq 'ARRAY'
&& @{ $self->{feature_names} };
my @names = @{ $self->{feature_names} };
my @unknown = grep {
my $k = $_;
!grep { $_ eq $k } @names
} keys %$row;
croak "unknown feature name(s) in hashref: " . join( ', ', sort @unknown )
if @unknown;
my @missing = grep { !exists $row->{$_} } @names;
croak "missing feature name(s) in hashref: " . join( ', ', @missing )
if @missing;
return [ map { $row->{$_} } @names ];
} ## end sub tagged_row_to_array
sub predict_tagged {
my ( $self, $row, $threshold ) = @_;
my $vec = $self->tagged_row_to_array( $row, 'predict_tagged' );
my $result = $self->predict( [$vec], $threshold );
return $result->[0];
}
=head2 munge_rows(\@rows)
Applies the model's scalar mungers to positional rows (arrayrefs in
C<feature_names> order), returning a new arrayref of munged rows. A
model without C<mungers> returns the input unchanged, so callers such
as the CLI can pass every dataset through unconditionally.
Croaks if the munger set contains expanding or combining mungers --
their inputs are named source fields that positional rows cannot
express; use the tagged methods (or L</fit_tagged>) for those.
my $numeric = $iforest->munge_rows(\@raw_rows);
=cut
sub munge_rows {
my ( $self, $rows ) = @_;
croak "munge_rows() expects an arrayref of rows"
unless ref $rows eq 'ARRAY';
my $plan = _plan($self);
return $rows unless $plan;
my @out;
for my $i ( 0 .. $#$rows ) {
my $munged = eval { $plan->apply_positional( $rows->[$i] ) };
croak "munge_rows (row $i): $@" if $@;
push @out, $munged;
}
return \@out;
} ## end sub munge_rows
=head2 score_samples(\@data)
Returns an arrayref of anomaly scores, between 0 and 1.
Scores near 1 are strong anomalies (isolated quickly).
Scores well below 0.5 are normal.
Scores ~0.5 means the points are hard to tell apart.
Under C<< voting => 'majority' >> the returned value is instead the
fraction of trees voting the sample anomalous at the model's decision
threshold (the contamination-learned cutoff if present, otherwise 0.5) --
still in [0, 1], but discrete in steps of C<1/n_trees>, with a majority
label corresponding to a fraction strictly above 0.5.
my $scores = $forest->score_samples(\@data);
print "x, y, score\n";
my $int=0;
while (defined($data[$int])) {
print $data[$int][0].', '.$data[$int][1].', '.$scores->[$int]."\n";
$int++;
}
=cut
sub score_samples {
my ( $self, $data ) = @_;
$self->_check_fitted;
my $c = $self->{c_psi};
my $trees = $self->{trees};
my $t = scalar @$trees;
# Majority voting: the "score" is the fraction of trees voting the
# sample anomalous at the model's decision threshold (contamination-
( run in 0.844 second using v1.01-cache-2.11-cpan-a9496e3eb41 )