Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/33-parallel-fit.t view on Meta::CPAN
my @query = (
[ 0.1, -0.2, 0.0 ], # inlier-like
[ 9, 9, 9 ], # obvious outlier
);
my $can_fork = ( $Config{d_fork} || '' ) eq 'define';
subtest 'parallel_fit produces a valid model' => sub {
plan skip_all => 'no fork() on this platform' unless $can_fork;
my $f = $CLASS->new(
n_trees => 50,
sample_size => 256,
seed => 42,
parallel_fit => 4,
);
$f->fit( \@train );
is( scalar @{ $f->{trees} }, 50, 'tree count matches n_trees' );
my $s = $f->score_samples( \@query );
is( scalar @$s, 2, 'two scores returned' );
cmp_ok( $s->[0], '>=', 0, 'inlier score >= 0' );
cmp_ok( $s->[0], '<=', 1, 'inlier score <= 1' );
cmp_ok( $s->[1], '>', $s->[0], 'outlier scores strictly higher than inlier (parallel-fit model is sane)' );
}; ## end 'parallel_fit produces a valid model' => sub
subtest 'parallel_fit is reproducible across runs at fixed worker count' => sub {
plan skip_all => 'no fork() on this platform' unless $can_fork;
my $f1 = $CLASS->new(
n_trees => 30,
sample_size => 256,
seed => 99,
parallel_fit => 3,
)->fit( \@train );
my $f2 = $CLASS->new(
n_trees => 30,
sample_size => 256,
seed => 99,
parallel_fit => 3,
)->fit( \@train );
my $s1 = $f1->score_samples( \@query );
my $s2 = $f2->score_samples( \@query );
my $diffs = grep { $s1->[$_] != $s2->[$_] } 0 .. $#$s1;
is( $diffs, 0, 'two parallel fits with same seed + workers give identical scores' );
}; ## end 'parallel_fit is reproducible across runs at fixed worker count' => sub
subtest 'parallel_fit number must be a positive integer' => sub {
eval { $CLASS->new( parallel_fit => -1 ) };
like( $@, qr/parallel_fit/, 'negative integer rejected' );
eval { $CLASS->new( parallel_fit => 'abc' ) };
like( $@, qr/parallel_fit/, 'non-numeric rejected' );
};
subtest 'parallel_fit=1 is equivalent to serial' => sub {
# n_trees > 1 and parallel_fit == 1 hits the same serial branch as
# parallel_fit undef. Just verify it produces a working model.
my $f = $CLASS->new(
n_trees => 20,
sample_size => 256,
seed => 7,
parallel_fit => 1,
);
$f->fit( \@train );
is( scalar @{ $f->{trees} }, 20, 'tree count matches n_trees' );
my $s = $f->score_samples( \@query );
cmp_ok( $s->[1], '>', $s->[0], 'model separates outlier from inlier' );
}; ## end 'parallel_fit=1 is equivalent to serial' => sub
subtest 'parallel_fit works for extended (EIF) mode' => sub {
plan skip_all => 'no fork() on this platform' unless $can_fork;
my $f = $CLASS->new(
n_trees => 40,
sample_size => 256,
mode => 'extended',
seed => 11,
parallel_fit => 4,
);
$f->fit( \@train );
is( scalar @{ $f->{trees} }, 40, 'tree count matches n_trees' );
is( $f->{mode}, 'extended', 'mode preserved across parallel fit' );
# Sanity: extended-mode parallel-built model should still separate
# the obvious outlier from the inlier.
my $s = $f->score_samples( \@query );
cmp_ok( $s->[1], '>', $s->[0], 'outlier scores higher than inlier' );
cmp_ok( $s->[0], '>=', 0, 'inlier score in [0,1]' );
cmp_ok( $s->[1], '<=', 1, 'outlier score in [0,1]' );
}; ## end 'parallel_fit works for extended (EIF) mode' => sub
subtest 'parallel_fit + to_json/from_json round-trips' => sub {
plan skip_all => 'no fork() on this platform' unless $can_fork;
my $f1 = $CLASS->new(
n_trees => 25,
sample_size => 256,
seed => 17,
parallel_fit => 3,
)->fit( \@train );
my $json = $f1->to_json;
ok( length $json > 100, 'JSON has plausible body length' );
my $f2 = $CLASS->from_json($json);
is( scalar @{ $f2->{trees} }, 25, 'restored tree count matches' );
my $s1 = $f1->score_samples( \@query );
my $s2 = $f2->score_samples( \@query );
my $diffs = grep { abs( $s1->[$_] - $s2->[$_] ) > 1e-9 } 0 .. $#$s1;
is( $diffs, 0, 'restored model produces the same scores as the parallel-fit original' );
}; ## end 'parallel_fit + to_json/from_json round-trips' => sub
subtest 'parallel_fit with n_trees < workers caps workers at n_trees' => sub {
plan skip_all => 'no fork() on this platform' unless $can_fork;
( run in 0.887 second using v1.01-cache-2.11-cpan-995e09ba956 )