Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
for (t = 0; t < n_trees; t++) {
HV* tree = _ol_tree_hv(aTHX_ trees, t);
SV** rsv = hv_fetch(tree, "root", 4, 0);
AV* node;
SV** slots;
int depth = 0;
if (!rsv || !*rsv || !SvOK(*rsv)) continue;
node = (AV*)SvRV(*rsv);
slots = AvARRAY(node);
while (SvIV(slots[OL_TYPE]) != 0) {
int attr = (int)SvIV(slots[OL_ATTR]);
double split = SvNV(slots[OL_SPLIT]);
node = (AV*)SvRV(slots[(x[attr] < split) ? OL_LEFT : OL_RIGHT]);
slots = AvARRAY(node);
depth++;
}
sum += (double)depth + _ol_rpl((double)SvIV(slots[OL_COUNT]), eta);
}
free(x);
return sum;
}
__INLINE_C__
# IF_NO_C=1 skips even attempting to set up the C backend -- useful for
# forcing the pure-Perl path without touching every constructor call
# (use_c => 0), e.g. to get a clean timing baseline or to avoid the
# compile attempt's overhead/noise in a container known to lack a
# compiler. Everything below is skipped and $HAS_C stays 0.
unless ( $ENV{IF_NO_C} ) {
# Defaults recorded when `perl Makefile.PL` ran. Makefile.PL generates
# Algorithm::Classifier::IsolationForest::BuildFlags, capturing the
# IF_* values active at configure time plus whether a prebuilt object
# was scheduled for install (see "Compile at install time" in the POD
# below). From a plain source checkout the generated file is absent,
# the hard defaults here apply, and no prebuilt object is looked for.
my ( $def_opt, $def_arch, $def_no_omp, $prebuilt ) = ( '-O3', '', 0, 0 );
{
local $@;
my $rec = eval {
require Algorithm::Classifier::IsolationForest::BuildFlags;
Algorithm::Classifier::IsolationForest::BuildFlags::flags();
};
if ( ref $rec eq 'HASH' ) {
$def_opt = $rec->{opt} if defined $rec->{opt};
$def_arch = $rec->{arch} if defined $rec->{arch};
$def_no_omp = $rec->{no_openmp} ? 1 : 0;
$prebuilt = $rec->{prebuilt} ? 1 : 0;
}
}
# -O3 is the usual default: it's safe to enable unconditionally and
# matters here -- the extended-mode oblique dot product is wrapped in
# `#pragma omp simd`, but without aggressive optimization the compiler
# may still emit scalar code. Use OPTIMIZE (not CCFLAGS) -- CCFLAGS is
# prepended to the cc line and would be shadowed by Perl's own `-O2 -g`
# that ExtUtils::MakeMaker appends afterward (last `-O` wins in gcc).
# IF_OPT overrides the level itself (e.g. IF_OPT=-O2 to work around a
# miscompile, or to shorten build time while developing); it's
# validated against a fixed set of GCC/Clang -O flags rather than
# interpolated as-is, since this string eventually reaches a shell
# command line via ExtUtils::MakeMaker.
my $opt = $def_opt;
if ( defined $ENV{IF_OPT} ) {
if ( $ENV{IF_OPT} =~ /\A-O[0123sgz]\z/ ) {
$opt = $ENV{IF_OPT};
} else {
warn "Algorithm::Classifier::IsolationForest: ignoring invalid "
. "IF_OPT value '$ENV{IF_OPT}' (expected one of -O0 -O1 -O2 "
. "-O3 -Os -Og -Oz); using $opt\n";
}
}
# -march=<value> lets the compiler target specific instruction-set
# extensions (AVX2 gather + FMA, etc.) for the oblique dot product
# and the fit-time min/max scan's `#pragma omp simd` loops.
#
# IF_ARCH=<value> sets it explicitly (e.g. "x86-64-v3", "skylake",
# "znver3") -- validated against a conservative identifier charset
# since, like IF_OPT, it flows into a compiler command line.
# IF_NATIVE=1 remains as shorthand for IF_ARCH=native and is used
# when IF_ARCH isn't set. Prefer a specific IF_ARCH value over
# IF_NATIVE on a machine you don't control exclusively: blanket
# -march=native pulls in whatever the build host has, including
# AVX-512 on some Intel CPUs, which is known to trigger clock
# throttling under sustained heavy use and can make throughput
# *worse* than a conservative target like x86-64-v3 (AVX2, no
# AVX-512). Either way, the cached artefact under _Inline/ is then
# pinned to that instruction set, so leave both unset if the
# directory is shared across machines with different CPUs.
my $arch = $def_arch;
if ( defined $ENV{IF_ARCH} ) {
if ( $ENV{IF_ARCH} eq '' or $ENV{IF_ARCH} eq 'none' ) {
# Explicit opt-out: overrides an arch recorded at configure
# time (there is no other way to request a plain build on
# an install configured with IF_ARCH).
$arch = '';
} elsif ( $ENV{IF_ARCH} =~ /\A[A-Za-z0-9_.+=-]+\z/ ) {
$arch = $ENV{IF_ARCH};
} else {
warn "Algorithm::Classifier::IsolationForest: ignoring invalid " . "IF_ARCH value '$ENV{IF_ARCH}'\n";
}
} elsif ( $ENV{IF_NATIVE} ) {
$arch = 'native';
}
# -ffp-contract=off rides along with any -march: once the target
# has FMA (x86-64-v3, most -march=native hosts), the compiler may
# otherwise contract a*b+c expressions into fused multiply-adds
# whose different rounding breaks the documented guarantee that
# use_c => 1 and use_c => 0 build bit-identical trees (one ulp in a
# split value cascades into a structurally different tree). The
# -march speedup comes from AVX2 vectorization, not contraction,
# so this costs little (verified against the fit-determinism and
# scoring-parity tests).
my $opt_level = $opt;
$opt_level .= " -march=$arch -ffp-contract=off" if length $arch;
# IF_NO_OPENMP=1 forces the serial C build: the OpenMP compile attempt
# is skipped, so the object has no libgomp linkage and never starts an
# OpenMP runtime in the process. Distinct from OMP_NUM_THREADS=1,
# which runs the parallel code on a single thread but still loads
# libgomp. An explicit IF_NO_OPENMP=0 re-enables OpenMP over a
# no-openmp configure-time default.
my $no_omp
= defined $ENV{IF_NO_OPENMP}
? ( $ENV{IF_NO_OPENMP} ? 1 : 0 )
: $def_no_omp;
# The prebuilt object is only trusted when the effective flags match
# what it was compiled with; any difference -- or an explicit
# IF_RUNTIME_BUILD=1 -- falls through to the classic runtime Inline::C
# build below, which honours the requested flags via the MD5-keyed
# _Inline/ cache exactly as before prebuilt support existed.
# IF_INSTALL_BUILD is the `make` rule driving the install-time compile
# (see Makefile.PL); it must never short-circuit into loading an
# older object.
my $use_prebuilt
= $prebuilt
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
thereby also fixes what the prebuilt object was compiled with. At run
time the recorded values serve as the defaults, so a process started
with no C<IF_*> variables set uses the prebuilt object as-is.
Setting C<IF_*> variables at run time keeps working exactly as in
releases without prebuilt support: if the requested flags differ from
the recorded ones, the prebuilt object (compiled with the wrong flags
for the request) is skipped and the module compiles at first load into
C<_Inline/> -- which does need C<Inline::C> and a compiler on that
machine. Two related knobs exist:
=over 4
=item * C<IF_RUNTIME_BUILD=1> -- ignore the prebuilt object
unconditionally and compile at first load even though the requested
flags match the recorded ones. Useful when the installed object is
suspect (built on a different CPU than it now runs on, linked against a
libgomp that has since changed) or to A/B a fresh local build against
the shipped one.
=item * C<IF_INSTALL_BUILD=1> -- internal; set by the generated
Makefile rule that performs the install-time compile. Not meant for
manual use.
=back
If the prebuilt object cannot be loaded for any reason (deleted, built
against a different perl, version mismatch after an upgrade), the
module quietly falls through the same chain as always: runtime
Inline::C build first, pure Perl last.
=head2 Tuning the C build
These environment variables are read once, the first time the module is
loaded, so they must be set before that -- e.g. in the shell before
running a script, not via C<%ENV> inside the script itself. They are
also read by C<perl Makefile.PL> to pick the flags baked into the
prebuilt object (see above); at run time they override the recorded
configure-time values, at the price of a runtime compile.
=over 4
=item * C<IF_NO_C=1> -- skip attempting to build the C backend entirely.
Equivalent to constructing every instance with C<use_c =E<gt> 0>, but
without needing to touch every call site; useful for a clean pure-Perl
timing baseline, or to avoid the compile attempt's overhead/noise on a
host known to lack a C compiler (the attempt already fails gracefully
without this, so it's a convenience, not a correctness fix).
=item * C<IF_OPT=-O2> (or C<-O0>/C<-O1>/C<-Os>/C<-Og>/C<-Oz>) -- override
the default C<-O3>, e.g. to shorten build time while iterating, or work
around a miscompile on an unusual toolchain. Invalid values are ignored
with a warning rather than passed through, since this string reaches a
compiler command line.
=item * C<IF_ARCH=E<lt>valueE<gt>> -- adds C<-march=E<lt>valueE<gt>> so the
compiler can target specific instruction-set extensions (AVX2 gather +
FMA, etc.) for the extended-mode oblique dot product and the fit-time
min/max scan's C<#pragma omp simd> loops. Accepts values like
C<x86-64-v3>, C<skylake>, or C<znver3> -- whatever your compiler's
C<-march=> accepts. Also validated (a restricted character set, not
passed through as-is) for the same reason as C<IF_OPT>. The special
value C<none> (or an empty string) opts out of any arch recorded at
configure time, yielding a plain build. Whenever a C<-march> is in
effect the build also adds C<-ffp-contract=off>: with FMA available
the compiler would otherwise contract C<a*b+c> into fused
multiply-adds whose different rounding breaks the guarantee that
C<use_c =E<gt> 1> and C<use_c =E<gt> 0> build bit-identical trees (the
C<-march> speedup comes from vectorization, not contraction, so this
costs essentially nothing).
=item * C<IF_NATIVE=1> -- shorthand for C<IF_ARCH=native>; ignored if
C<IF_ARCH> is also set. Prefer a specific C<IF_ARCH> value over this on
a machine you don't control exclusively (a shared build host, a
container base image): blanket C<-march=native> pulls in whatever
instruction sets the build host happens to have, including AVX-512 on
some Intel CPUs -- which is known to trigger clock throttling under
sustained heavy use and can make throughput I<worse> than a
conservative target like C<x86-64-v3> (AVX2, no AVX-512). If in doubt,
benchmark both before committing to one.
=item * C<IF_NO_OPENMP=1> -- build (or select) the serial C backend: the
OpenMP compile attempt is skipped entirely, so the resulting object has
no libgomp linkage and never starts an OpenMP runtime inside the
process. This differs from C<OMP_NUM_THREADS=1>, which merely runs the
parallel code on one thread but still loads libgomp. Set at
C<perl Makefile.PL> time it yields a serial prebuilt object; set at run
time against an OpenMP prebuilt install it triggers a runtime serial
build (needing a compiler). An explicit C<IF_NO_OPENMP=0> re-enables
OpenMP over a serial configure-time default.
=back
Whichever of these are used, the cached artefact under C<_Inline/> is
pinned to that build's instruction set -- delete C<_Inline/> (or use a
separate one per host) if the directory is shared across machines with
different CPUs, or a stale binary built for a narrower instruction set
than the current host will simply keep being reused.
=head2 Tuning the OpenMP runtime
These are standard OpenMP environment variables libgomp already reads
at run time (set before running your script, no module-specific
handling needed) -- listed here because they matter most for exactly
the workloads this module has: C<score_all_xs>'s per-point parallel
loop and C<use_openmp_fit>'s per-tree parallel loop.
=over 4
=item * C<OMP_NUM_THREADS=N> -- caps how many threads a parallel region
uses. Useful to leave headroom for other work sharing the machine, or
to pin down C<use_openmp_fit> reproducibility checks (see its docs
above: results don't depend on this, but it's a natural thing to vary
when confirming that).
=item * C<OMP_PROC_BIND=close> / C<OMP_PLACES=cores> -- on multi-socket
or otherwise NUMA machines, pins each thread to a core near where its
data already lives instead of letting the OS scheduler migrate threads
across sockets mid-run. Both C<score_all_xs> (each thread scans its own
slice of the packed query buffer) and C<use_openmp_fit> (each thread
builds one tree from packed training data) benefit from this when the
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
if ( defined( $args{seed} ) ) {
$args{seed} = abs( int( $args{seed} ) );
}
# Clamp the accel knobs against what the build actually has. Passing
# use_c => 1 on a machine where Inline::C never compiled would otherwise
# leave score_samples() calling an undefined XS sub at first use.
# OpenMP is meaningless without the C tree walk, so force it off
# whenever the C backend is off -- matches the documented
# "Ignored when use_c is false" semantics.
my $use_c
= defined $args{use_c}
? ( $args{use_c} && $HAS_C ? 1 : 0 )
: $HAS_C;
my $use_openmp
= defined $args{use_openmp}
? ( $args{use_openmp} && $HAS_OPENMP ? 1 : 0 )
: $HAS_OPENMP;
$use_openmp = 0 unless $use_c;
# Opt-in only (default 0, not $HAS_OPENMP): this path changes which
# trees fit() builds (see docs above), unlike use_c/use_openmp which
# only change speed. Clamped the same way use_openmp is.
my $use_openmp_fit = ( $args{use_openmp_fit} && $HAS_OPENMP && $use_c ) ? 1 : 0;
my $self = {
n_trees => $args{n_trees} // 100,
sample_size => $args{sample_size} // 256,
max_depth => $args{max_depth}, # undef => auto
seed => $args{seed}, # undef => non-deterministic
mode => $mode,
extension_level => $args{extension_level}, # undef => max, resolved in fit()
contamination => $args{contamination}, # undef => no learned threshold
parallel_fit => $args{parallel_fit}, # undef/0/1 => serial; N>1 => fork
missing => $missing, # die|zero|impute|nan
impute_with => $impute_with, # mean|median (impute mode only)
voting => $voting, # mean|majority (scoring-time aggregation)
missing_fill => undef, # per-feature fill, learned in fit() if impute
_use_c => $use_c,
_use_openmp => $use_openmp,
_use_openmp_fit => $use_openmp_fit,
threshold => undef, # learned in fit() if contamination set
trees => [],
c_psi => undef, # c(psi), set during fit()
n_features => undef,
feature_names => $args{feature_names}, # optional arrayref of per-feature labels
mungers => undef, # optional Algorithm::ToNumberMunger spec hash
# Opaque schema metadata, usually set via new_from_prototype and
# persisted with the model. Never parsed -- documentation that
# travels with the model file.
schema_version => $args{schema_version},
schema_description => $args{schema_description},
feature_descriptions => $args{feature_descriptions},
};
for my $doc (qw(schema_version schema_description)) {
croak "$doc must be a plain string"
if defined $self->{$doc} && ref $self->{$doc};
}
_validate_feature_descriptions( $self->{feature_names}, $self->{feature_descriptions} )
if defined $self->{feature_descriptions};
# Optional Algorithm::ToNumberMunger integration: a declarative spec
# hash compiled into a plan that turns raw tagged values into numbers.
# Compiled eagerly so every spec error surfaces here rather than at
# first scoring; the module itself is only required when a spec is
# actually given, keeping it an optional dependency.
if ( defined $args{mungers} ) {
croak "mungers must be a hashref of 'tag => munger spec'"
unless ref $args{mungers} eq 'HASH';
croak "mungers requires feature_names (the munger plan compiles against them)"
unless ref $self->{feature_names} eq 'ARRAY' && @{ $self->{feature_names} };
$self->{mungers} = $args{mungers};
$self->{_munger_plan} = _compile_mungers( $self->{feature_names}, $self->{mungers} );
$self->{munger_module_version} = $Algorithm::ToNumberMunger::VERSION;
}
croak "n_trees must be >= 1" unless $self->{n_trees} >= 1;
croak "sample_size must be >= 1" unless $self->{sample_size} >= 1;
croak "extension_level must be >= 0"
if defined $self->{extension_level} && $self->{extension_level} < 0;
croak "contamination must be a number in (0, 0.5]"
if defined $self->{contamination}
&& !( $self->{contamination} > 0 && $self->{contamination} <= 0.5 );
croak "parallel_fit must be a positive integer"
if defined $self->{parallel_fit}
&& ( $self->{parallel_fit} !~ /^\d+$/ || $self->{parallel_fit} < 1 );
return bless $self, $class;
} ## end sub new
=head2 decision_threshold
The score cutoff C<predict> uses by default; undef unless C<contamination> was
set.
=cut
sub decision_threshold { return $_[0]->{threshold} }
=head2 set_voting
Switches the scoring-time aggregation between C<'mean'> and C<'majority'> on an
existing model and returns C<$self> (so it chains). The forest itself is
identical in both modes -- only the way per-tree results are combined changes
-- so this never rebuilds a single tree.
$iforest->set_voting('majority');
$iforest->set_voting('mean', \@training_data);
The one thing that does not carry over is a C<contamination>-learned
L</decision_threshold>. That cutoff is a quantile of whichever per-point
quantity the mode thresholds against -- the averaged anomaly score under
C<'mean'>, the per-tree majority pivot under C<'majority'> -- and those live in
different spaces, so a threshold learned in one mode flags the wrong fraction
in the other. When the model was fitted with C<contamination>, C<set_voting>
therefore relearns the threshold for the target mode, which requires the
original training data to be passed as the second argument (the model does not
retain it). Switching a model that had no C<contamination> needs no data:
C<predict> falls back to C<0.5>, which is meaningful in both modes.
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
"max_leaf_samples": 32,
"contamination": 0.02
}
}
The fields, top to bottom...
- format :: required, always the string
'Algorithm::Classifier::IsolationForest::Prototype'. A prototype
handed to load() (or a model handed to the prototype methods)
dies with a clear message instead of half-working.
- version :: the prototype format version; this release reads version 1.
default :: 1
- class :: required, 'batch' (this class) or 'online'
(L<Algorithm::Classifier::IsolationForest::Online>). Prototypes
are self-describing; `iforest fit` refuses an online prototype
and `iforest stream` refuses a batch one. Two model types with
the same variables means two prototype files.
- schema_version :: required opaque string, never parsed or compared
numerically. User-owned: bump it when the variable schema
changes.
- schema_description :: required opaque free-text string describing
what this variable schema is, so a model file explains itself
months later.
- schema :: required object holding the variable schema.
feature_names is required (order = CSV column order); the
optional keys are feature_descriptions ('feature name => free
text', every key must name an entry in feature_names, partial
coverage fine), mungers (see L</MUNGERS>), missing, and -- batch
prototypes only -- impute_with. Unknown keys croak.
- params :: optional object of tuning knobs, whitelisted per class.
Batch: n_trees, sample_size, max_depth, mode, extension_level,
contamination, voting, seed. Online: n_trees, window_size,
max_leaf_samples, growth, subsample, contamination, seed.
Unknown keys croak -- a typo'd knob silently falling back to its
default is exactly the failure mode a prototype exists to
prevent. Machine-local knobs (use_c, use_openmp, use_openmp_fit,
parallel_fit) are rejected: they describe the box the model runs
on, not the model.
=cut
# Per-class whitelists for a prototype's params block (and
# new_from_prototype's %overrides) and its schema block. Machine-local
# knobs are deliberately absent from the params lists.
my %PROTO_PARAM_KEYS = (
batch => { map { $_ => 1 } qw(n_trees sample_size max_depth mode extension_level contamination voting seed) },
online => { map { $_ => 1 } qw(n_trees window_size max_leaf_samples growth subsample contamination seed) },
);
my %PROTO_SCHEMA_KEYS = (
batch => { map { $_ => 1 } qw(feature_names feature_descriptions mungers missing impute_with) },
online => { map { $_ => 1 } qw(feature_names feature_descriptions mungers missing) },
);
=head2 validate_prototype($proto)
Structurally validates a prototype -- a hashref or a JSON string -- and
returns the decoded hashref; croaks describing the first problem found.
Validation is structural only (no munger compilation), so it does not
require Algorithm::ToNumberMunger even for a munger-bearing prototype.
my $proto = Algorithm::Classifier::IsolationForest->validate_prototype($json);
=cut
sub validate_prototype {
my ( $class, $proto ) = @_;
if ( !ref $proto ) {
my $decoded = eval { JSON::PP->new->decode($proto) };
croak "prototype did not parse as JSON: $@" if $@;
$proto = $decoded;
}
croak "not an IsolationForest prototype (expected a JSON object)"
unless ref $proto eq 'HASH';
croak "not an IsolationForest prototype (format is not " . "'Algorithm::Classifier::IsolationForest::Prototype')"
unless defined $proto->{format}
&& !ref $proto->{format}
&& $proto->{format} eq 'Algorithm::Classifier::IsolationForest::Prototype';
my $version = $proto->{version} // 1;
croak "prototype format version '$version' is newer than this module understands (max 1)"
if !ref $version && $version =~ /^\d+$/ && $version > 1;
for my $k ( sort keys %$proto ) {
croak "prototype has unknown top-level key '$k'"
unless $k =~ /\A(?:format|version|class|schema_version|schema_description|schema|params)\z/;
}
my $which = $proto->{class};
croak "prototype needs a class of 'batch' or 'online'"
unless defined $which && !ref $which && $which =~ /\A(?:batch|online)\z/;
for my $req (qw(schema_version schema_description)) {
croak "prototype needs a non-empty $req string"
unless defined $proto->{$req} && !ref $proto->{$req} && length $proto->{$req};
}
my $schema = $proto->{schema};
croak "prototype needs a schema object" unless ref $schema eq 'HASH';
for my $k ( sort keys %$schema ) {
croak "prototype schema has unknown key '$k' for a $which prototype (allowed: "
. join( ', ', sort keys %{ $PROTO_SCHEMA_KEYS{$which} } ) . ')'
unless $PROTO_SCHEMA_KEYS{$which}{$k};
}
my $tags = $schema->{feature_names};
croak "prototype schema needs a non-empty feature_names array"
unless ref $tags eq 'ARRAY' && @$tags;
for my $t (@$tags) {
croak "prototype feature_names entries must be non-empty strings"
unless defined $t && !ref $t && length $t;
}
_validate_feature_descriptions( $tags, $schema->{feature_descriptions} )
if defined $schema->{feature_descriptions};
croak "prototype schema mungers must be an object of 'tag => munger spec'"
if defined $schema->{mungers} && ref $schema->{mungers} ne 'HASH';
for my $str (qw(missing impute_with)) {
croak "prototype schema $str must be a plain string"
if defined $schema->{$str} && ref $schema->{$str};
}
my $params = $proto->{params};
croak "prototype params must be an object of tuning knobs"
if defined $params && ref $params ne 'HASH';
for my $k ( sort keys %{ $params || {} } ) {
croak "prototype params has unknown key '$k' for a $which prototype (allowed: "
. join( ', ', sort keys %{ $PROTO_PARAM_KEYS{$which} } )
. '; machine-local knobs like use_c are deliberately not allowed)'
unless $PROTO_PARAM_KEYS{$which}{$k};
}
return $proto;
} ## end sub validate_prototype
=head2 new_from_prototype($proto, %overrides)
Creates a fresh, unfitted model from a prototype (a hashref or a JSON
string) and returns it -- an instance of whichever class the prototype's
C<class> field names, so like C<load()> this is a single entry point for
both model types. Croaks on any validation failure; a munger-bearing
prototype compiles its plan here, so a bogus munger spec dies at
creation (and needs Algorithm::ToNumberMunger installed).
C<%overrides> merge over the prototype's C<params> block -- per-run
knobs like C<seed> -- and are held to the same per-class whitelist.
Overriding the schema itself (feature_names, feature_descriptions,
mungers, missing, impute_with, schema_version, schema_description)
croaks: the schema is the prototype's, full stop; edit the prototype.
my $oif = Algorithm::Classifier::IsolationForest->new_from_prototype(
$proto_json,
seed => 42,
);
=cut
sub new_from_prototype {
my ( $class, $proto, %overrides ) = @_;
$proto = $class->validate_prototype($proto);
my $which = $proto->{class};
my $schema = $proto->{schema};
for my $k ( sort keys %overrides ) {
croak "new_from_prototype: '$k' is part of the prototype's schema and may not "
. "be overridden; edit the prototype instead"
if $k
=~ /\A(?:feature_names|feature_descriptions|mungers|missing|impute_with|schema_version|schema_description)\z/;
croak "new_from_prototype: unknown override '$k' for a $which prototype (allowed: "
. join( ', ', sort keys %{ $PROTO_PARAM_KEYS{$which} } ) . ')'
unless $PROTO_PARAM_KEYS{$which}{$k};
}
my %args = (
%{ $proto->{params} || {} },
%overrides,
feature_names => $schema->{feature_names},
schema_version => $proto->{schema_version},
schema_description => $proto->{schema_description},
);
for my $k (qw(feature_descriptions mungers missing impute_with)) {
$args{$k} = $schema->{$k} if defined $schema->{$k};
}
if ( $which eq 'online' ) {
require Algorithm::Classifier::IsolationForest::Online;
return Algorithm::Classifier::IsolationForest::Online->new(%args);
}
return Algorithm::Classifier::IsolationForest->new(%args);
} ## end sub new_from_prototype
=head2 load_prototype($path, %overrides)
L</new_from_prototype($proto, %overrides)> from a file.
my $iforest = Algorithm::Classifier::IsolationForest->load_prototype(
'proto.json', seed => 42 );
=cut
sub load_prototype {
my ( $class, $path, %overrides ) = @_;
my $raw = read_file($path);
return $class->new_from_prototype( $raw, %overrides );
}
=head2 to_prototype
Returns a prototype JSON string extracted from this model: its variable
schema (feature_names, feature_descriptions, mungers, missing policy)
plus its current tuning knobs. This closes the loop -- extract a
prototype from a good model and periodically create fresh models with an
identical schema, the natural retrain workflow -- and means hand-writing
a prototype is never mandatory.
Croaks when the model has no C<feature_names>: a prototype's variable
schema needs named variables. A model with no recorded
C<schema_version> / C<schema_description> (fitted before prototype
support, or without the knobs) gets placeholder values, since both are
required in the file -- edit them in and bump from there. C<seed> and
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
push @coef_val, $coef_for{$k} + 0.0;
}
} else {
for my $i ( 0 .. $num - 1 ) {
push @coef_idx, int( $idx_arr->[$i] );
push @coef_val, $coef_arr->[$i] + 0.0;
}
}
my $li = $assign->( $node->[4] );
my $ri = $assign->( $node->[5] );
$node_data[$my_idx] = [ 2.0, $coef_off + 0.0, $num + 0.0, $li + 0.0, $ri + 0.0, $b + 0.0, ];
} ## end else [ if ( $node->[0] == _NODE_LEAF ) ]
return $my_idx;
}; ## end $assign = sub
$assign->($root);
my $nodes_packed = pack( 'd*', map { @$_ } @node_data );
my $idx_packed = @coef_idx ? pack( 'l*', @coef_idx ) : pack('l*');
my $val_packed = @coef_val ? pack( 'd*', @coef_val ) : pack('d*');
return ( $nodes_packed, $idx_packed, $val_packed );
} ## end sub _pack_tree
# Build packed C-ready representations for all trees and store them in
# $self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}.
# Called after fit() and from_json() when _use_c is true. n_features is
# threaded through so _pack_tree can spot the dense-pack opportunity.
sub _rebuild_c_trees {
my ($self) = @_;
my ( @c_nodes, @c_coef_idx, @c_coef_val );
for my $tree ( @{ $self->{trees} } ) {
my ( $np, $ip, $vp ) = _pack_tree( $tree, $self->{n_features} );
push @c_nodes, $np;
push @c_coef_idx, $ip;
push @c_coef_val, $vp;
}
$self->{_c_nodes} = \@c_nodes;
$self->{_c_coef_idx} = \@c_coef_idx;
$self->{_c_coef_val} = \@c_coef_val;
} ## end sub _rebuild_c_trees
sub _check_fitted {
my ($self) = @_;
croak "model is not fitted yet; call fit() first"
unless ref $self->{trees} eq 'ARRAY' && @{ $self->{trees} };
}
# ---------------------------------------------------------------------------
# Optional Algorithm::ToNumberMunger integration -- see the MUNGERS POD
# section. Both helpers are plain functions (not methods) so the Online
# class's delegating methods can hand them their own $self: the plan and
# spec live in the same hash slots on either class.
# ---------------------------------------------------------------------------
# Validate a feature_descriptions hash against the feature names: every
# described feature must exist (a description for one that does not is
# either a typo or a stale leftover from a schema change) and every
# description must be a plain string. Partial coverage is fine. A plain
# function, like the munger helpers, so both classes and the prototype
# validator can call it.
sub _validate_feature_descriptions {
my ( $tags, $fd ) = @_;
croak "feature_descriptions must be a hashref of 'feature name => description'"
unless ref $fd eq 'HASH';
croak "feature_descriptions requires feature_names to validate against"
unless ref $tags eq 'ARRAY' && @$tags;
my %known = map { $_ => 1 } @$tags;
for my $k ( sort keys %$fd ) {
croak "feature_descriptions describes '$k', which is not in feature_names"
unless $known{$k};
croak "feature_descriptions entry for '$k' must be a plain string"
if ref $fd->{$k};
}
return 1;
} ## 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.
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.
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.
{
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.
#-------------------------------------------------------------------------------
sub _fit_trees_parallel {
my ( $self, $data, $psi, $limit, $workers ) = @_;
require Storable;
require POSIX;
( run in 0.561 second using v1.01-cache-2.11-cpan-f4a522933cf )