Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
if (b->n_nodes == b->cap_nodes) {
size_t newcap = b->cap_nodes ? b->cap_nodes * 2 : 64;
b->nodes = (double*)realloc(b->nodes, newcap * 6 * sizeof(double));
b->cap_nodes = newcap;
}
slot = b->nodes + b->n_nodes * 6;
slot[0] = f0; slot[1] = f1; slot[2] = f2;
slot[3] = f3; slot[4] = f4; slot[5] = f5;
return (int)(b->n_nodes++);
}
/* Appends n (idx[k], val[k]) pairs and returns the offset they start
* at -- the `coff` an oblique node record stores. */
static int tb_push_coef(TreeBuf *b, const int *idx, const double *val,
int n) {
int off = (int)b->n_idx;
if (b->n_idx + (size_t)n > b->cap_idx) {
size_t newcap = b->cap_idx ? b->cap_idx * 2 : 64;
if (newcap < b->n_idx + (size_t)n) newcap = b->n_idx + (size_t)n;
b->idx = (int*)realloc(b->idx, newcap * sizeof(int));
b->cap_idx = newcap;
}
if (b->n_val + (size_t)n > b->cap_val) {
size_t newcap = b->cap_val ? b->cap_val * 2 : 64;
if (newcap < b->n_val + (size_t)n) newcap = b->n_val + (size_t)n;
b->val = (double*)realloc(b->val, newcap * sizeof(double));
b->cap_val = newcap;
}
memcpy(b->idx + b->n_idx, idx, (size_t)n * sizeof(int));
memcpy(b->val + b->n_val, val, (size_t)n * sizeof(double));
b->n_idx += n;
b->n_val += n;
return off;
}
/* splitmix64 -- fast, well-mixed, and per-stream state fits in one
* uint64_t, which is all a thread-private PRNG needs here. Not
* cryptographic; doesn't need to be. */
static uint64_t sm64_next(uint64_t *s) {
uint64_t z = (*s += 0x9E3779B97F4A7C15ULL);
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
static double sm64_drand(uint64_t *s) {
return (double)(sm64_next(s) >> 11) * (1.0 / 9007199254740992.0);
}
static double _ts_randn(uint64_t *s) {
double u1 = sm64_drand(s);
double u2;
if (u1 == 0.0) u1 = 1e-12;
u2 = sm64_drand(s);
return sqrt(-2.0 * log(u1)) * cos(6.283185307179586 * u2);
}
/* Thread-safe twin of _build_node_c: same split algorithm, but reads
* randomness from a thread-private splitmix64 stream instead of
* Drand01(), and writes into a TreeBuf instead of allocating Perl AVs
* -- so it touches no interpreter-global state and is safe to call
* concurrently from an OpenMP parallel region, one tree per thread. */
static int _build_node_packed(const double* x, int nf, int* idxs, int size,
int depth, int limit, int mode_flag,
int ext_active, TreeBuf *buf, uint64_t *rng) {
double *lo, *hi;
int *varying, nv, f, my_idx;
if (depth >= limit || size <= 1) {
my_idx = tb_push_node(buf, 0.0, (double)size, 0.0, 0.0, 0.0, 0.0);
free(idxs);
return my_idx;
}
lo = (double*)malloc(nf * sizeof(double));
hi = (double*)malloc(nf * sizeof(double));
for (f = 0; f < nf; f++) {
lo[f] = HUGE_VAL;
hi[f] = -HUGE_VAL;
}
for (int i = 0; i < size; i++) {
const double* row = x + (size_t)idxs[i] * (size_t)nf;
/* See the matching comment in _build_node_c: no isnan() guard
* needed, since NaN < x / NaN > x are always false already --
* that's what lets this vectorize as a plain min/max scan.
* omp simd here is thread-safe to call from inside the caller's
* omp parallel region: it's a per-thread vectorization hint,
* not a team construct, so it doesn't nest into anything. */
#ifdef _OPENMP
#pragma omp simd
#endif
for (int f2 = 0; f2 < nf; f2++) {
double v = row[f2];
if (v < lo[f2]) lo[f2] = v;
if (v > hi[f2]) hi[f2] = v;
}
}
varying = (int*)malloc(nf * sizeof(int));
nv = 0;
for (f = 0; f < nf; f++) {
if (lo[f] < hi[f]) varying[nv++] = f;
}
if (nv == 0) {
free(lo); free(hi); free(varying);
my_idx = tb_push_node(buf, 0.0, (double)size, 0.0, 0.0, 0.0, 0.0);
free(idxs);
return my_idx;
}
if (mode_flag == 0) {
int attr = varying[(int)(sm64_drand(rng) * nv)];
double split = lo[attr] + sm64_drand(rng) * (hi[attr] - lo[attr]);
int *lidx = (int*)malloc(size * sizeof(int));
int *ridx = (int*)malloc(size * sizeof(int));
int ln = 0, rn = 0, i, li, ri;
for (i = 0; i < size; i++) {
int row = idxs[i];
double v = x[(size_t)row * (size_t)nf + attr];
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# rather than the full file -- the only fill bounded-memory fitting can form.
#
# Args:
# $pool :: hashref of row index => arrayref of cells, as returned by
# _gather_stream or _gather_indexed. Rewritten in place.
#
# Returns: nothing. Under zero/impute every row in $pool comes back dense;
# under impute the learned fill is also stored in $self->{missing_fill}.
# die and nan return untouched (die data is already dense, nan wants its
# undefs kept).
#
# Example:
# my $pool = $self->_gather_indexed( $path, $offsets, $want );
# $self->_apply_missing_to_pool($pool); # undef cells now carry the fill
sub _apply_missing_to_pool {
my ( $self, $pool ) = @_;
my $m = $self->{missing};
return if $m eq 'die' || $m eq 'nan'; # die: already dense; nan: keep undef
my $nf = $self->{n_features};
my $fill;
if ( $m eq 'impute' ) {
$fill = $self->_compute_impute_fill( [ values %$pool ] );
$self->{missing_fill} = $fill;
delete $self->{_fill_packed};
} else { # zero
$fill = [ (0) x $nf ];
}
for my $i ( keys %$pool ) {
my $r = $pool->{$i};
$pool->{$i} = [ map { defined $r->[$_] ? $r->[$_] : $fill->[$_] } 0 .. $nf - 1 ];
}
return;
} ## end sub _apply_missing_to_pool
# Streaming counterpart of _learn_contamination_threshold: learn the exact
# score cutoff for the contamination rate without holding every score. k+1
# largest scores are enough for _threshold_from_ranked's boundary logic; a
# min-heap keeps them in one scoring pass, and (only under a boundary tie) one
# extra pass resolves the tie block's edges.
#
# Args:
# $path :: path to the CSV file the model was just fitted from.
# $skip_first :: true to drop the first non-blank line as a header.
# $n :: the census row count, which fixes how many rows k stands for.
# $c_scan :: true to let the C packer coerce cells, skipping the Perl
# numeric validation on the scoring passes. See _stream_scores.
#
# Returns: nothing. Sets $self->{threshold} to the same value the in-RAM
# _learn_contamination_threshold would have produced for this data.
#
# Example:
# $self->_learn_contamination_threshold_streaming( 'train.csv', 1, 1_000_000, 1 );
sub _learn_contamination_threshold_streaming {
my ( $self, $path, $skip_first, $n, $c_scan ) = @_;
my $k = int( $self->{contamination} * $n + 0.5 );
$k = 1 if $k < 1;
$k = $n if $k > $n;
# Whole set flagged: sit the cut just below the global minimum score.
if ( $k >= $n ) {
my $min;
$self->_stream_scores( $path, $skip_first, sub { $min = $_[0] if !defined $min || $_[0] < $min }, $c_scan );
$self->{threshold} = $min - 1e-9;
return;
}
# One scoring pass keeps the k+1 largest scores (a min-heap rooted at the
# smallest kept). contamination <= 0.5 bounds k at n/2, so this tail is
# always the smaller side of the split.
my @heap;
my $cap = $k + 1;
$self->_stream_scores(
$path,
$skip_first,
sub {
my $s = $_[0];
if ( @heap < $cap ) { _heap_push( \@heap, $s ) }
elsif ( $s > $heap[0] ) { _heap_replace_root( \@heap, $s ) }
},
$c_scan
);
my @desc = sort { $b <=> $a } @heap; # k+1 largest, descending
my $v = $desc[ $k - 1 ]; # k-th largest
my $lo = $desc[$k]; # (k+1)-th largest
# Clean gap at the boundary: cut midway, exactly as _threshold_from_ranked.
if ( $lo < $v ) {
$self->{threshold} = ( $v + $lo ) / 2.0;
return;
}
# A tie block of value $v straddles rank k. Reproduce _threshold_from_ranked's
# tie branch: locate the block's edges (i = first index at $v, j = first
# index below it) and the neighbouring scores with one more pass.
my ( $cnt_gt, $cnt_eq, $above, $below ) = ( 0, 0, undef, undef );
$self->_stream_scores(
$path,
$skip_first,
sub {
my $s = $_[0];
if ( $s > $v ) {
$cnt_gt++;
$above = $s if !defined $above || $s < $above; # smallest > $v
} elsif ( $s == $v ) {
$cnt_eq++;
} else {
$below = $s if !defined $below || $s > $below; # largest < $v
}
},
$c_scan
);
my $i = $cnt_gt; # first rank holding $v
my $j = $cnt_gt + $cnt_eq; # first rank below $v
if ( $i > 0 && ( $k - $i ) < ( $j - $k ) ) {
$self->{threshold} = ( $above + $v ) / 2.0; # exclude the block
} elsif ( $j < $n ) {
$self->{threshold} = ( $v + $below ) / 2.0; # include the block
( run in 1.617 second using v1.01-cache-2.11-cpan-0fb53d1c279 )