Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
} elsif ( $self->{_use_c} ) {
$self->{trees}
= $self->_build_forest_c( $train, $psi, $limit, $self->{n_trees} );
} else {
my @trees;
for ( 1 .. $self->{n_trees} ) {
my $sample = _subsample( $train, $psi );
push @trees, $self->_build_tree( $sample, 0, $limit );
}
$self->{trees} = \@trees;
}
# On a re-fit, packed scoring buffers from the previous fit are still
# sitting on the object; score_samples() below would pick them up and
# learn the contamination threshold against the OLD forest. Drop them
# so the training-set scoring runs pure-Perl against the trees just
# built; _rebuild_c_trees repacks from the new trees at the end.
delete @$self{qw(_c_nodes _c_coef_idx _c_coef_val)};
# If a contamination rate was requested, learn the score cutoff that flags
# that fraction of the training set. The threshold lands midway inside a
# real gap between flagged and unflagged training scores (ties at the
# k-boundary shift the cut to the nearest gap -- see
# _threshold_from_ranked), so it sits strictly between attainable values:
# unambiguous and robust to the tiny float rounding introduced by JSON
# serialisation.
#
# Under voting => 'majority' the value predict() thresholds against is
# the PER-TREE score, so the quantity to rank is each training point's
# majority pivot -- the per-tree cutoff at which that point loses its
# majority (see _majority_pivot_scores). A point is flagged iff its
# pivot >= threshold, exactly the relation the mean-mode score has, so
# the midpoint selection below serves both modes unchanged.
$self->_learn_contamination_threshold($train)
if defined $self->{contamination};
$self->_rebuild_c_trees() if $self->{_use_c};
return $self;
} ## end sub fit
=head2 pack_data(\@data)
Returns an opaque, blessed wrapper around the input dataset that the
scoring methods can use directly, skipping the per-call work of walking
the arrayref-of-arrayrefs and converting each cell into a double. At
high feature counts this is a meaningful win when the same dataset is
scored repeatedly (e.g. interactive threshold tuning, dashboards,
plotting that updates as parameters change).
Requires the Inline::C backend; croaks if C<use_c> is false.
my $packed = $forest->pack_data(\@data);
# Now any of these accept either an arrayref or the packed wrapper:
my $scores = $forest->score_samples($packed);
my $flags = $forest->predict($packed, 0.6);
my ($s, $l) = $forest->score_predict_split($packed);
The wrapper has C<n_pts> and C<n_feats> accessors for introspection.
The feature count is matched against the model on every call; passing a
packed dataset built for a different feature count is a fatal error.
=cut
=head2 path_lengths(\@data)
Returns an arrayref of the mean isolation depth per sample, for inspection.
my $lengths = $forest->path_lengths(\@data);
print "x, y, length\n";
my $int=0;
while (defined($data[$int])) {
print $data[$int][0].', '.$data[$int][1].', '.$lengths->[$int]."\n";
$int++;
}
=cut
sub path_lengths {
my ( $self, $data ) = @_;
$self->_check_fitted;
my $trees = $self->{trees};
my $t = scalar @$trees;
if ( $self->{_use_c} && $self->{_c_nodes} ) {
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data);
my $sums_packed = "\0" x ( $n_pts * 8 );
score_all_xs(
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val},
$x_packed, $sums_packed, $n_pts,
$nf, $t, $self->{_use_openmp}
);
my $result = [];
finalize_path_lengths_xs( $sums_packed, $n_pts, $t + 0.0, $result );
return $result;
} ## end if ( $self->{_use_c} && $self->{_c_nodes} )
$data = $self->_prepare_perl_input($data);
my $nan = $self->{missing} eq 'nan' ? 1 : 0;
# Pure-Perl fallback (tree-outer, sample-inner for cache locality).
my @sums = (0) x @$data;
for my $tree (@$trees) {
for my $i ( 0 .. $#$data ) {
$sums[$i] += _path_length( $data->[$i], $tree, 0, $nan );
}
}
return [ map { $_ / $t } @sums ];
} ## end sub path_lengths
=head2 predict(\@data, $threshold)
Returns an arrayref of 0/1 labels for the specified data.
If threshold is not specified it uses the contamination-learned cutoff (if
C<fit> was called with C<contamination>), otherwise 0.5.
Under C<< voting => 'majority' >> the threshold is the per-tree score
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
} ## end if ( $self->{voting} eq 'majority' )
# Fast path: threshold the raw path-length sums directly, skipping the
# per-point exp() and the intermediate scores arrayref.
# Derivation: score = exp(-sum * log(2) / (c*t))
# so score >= T iff sum <= -log(T) * c * t / log(2)
# Only valid for a normal threshold in (0, 1) and a positive c.
if ( $self->{_use_c}
&& $self->{_c_nodes}
&& $self->{c_psi} > 0
&& $threshold > 0
&& $threshold < 1 )
{
my $trees = $self->{trees};
my $t = scalar @$trees;
my $c = $self->{c_psi};
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data);
my $sums_packed = "\0" x ( $n_pts * 8 );
score_all_xs(
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val},
$x_packed, $sums_packed, $n_pts,
$nf, $t, $self->{_use_openmp}
);
my $sum_threshold = -log($threshold) * $c * $t / log(2);
my $result = [];
predict_sums_xs( $sums_packed, $n_pts, $sum_threshold, $result );
return $result;
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ...)
# Fallback: edge thresholds, c==0, or no C backend.
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';
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 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.
( run in 1.237 second using v1.01-cache-2.11-cpan-c966e8aa7e8 )